Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ai-autopilot-local-runner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@gemstack/ai-autopilot": minor
---

Add `LocalRunner`, the first real adapter behind the runner seam. Where `FakeRunner` simulates a workspace in memory, `LocalRunner` boots each workspace as a real temp directory on the host: real files via `node:fs` (path-traversal guarded to the workspace root), real commands via `child_process` (shell, per-command `cwd`/`env`/`timeoutMs`), a localhost `preview`, and a `dispose()` that removes the workspace. It is the reference the sandboxed adapters (WebContainer, Docker, Flue) mirror. It runs commands unsandboxed on the host, so it is documented for trusted/CI use only, not untrusted agent-authored code. Part of the ai-autopilot epic (#97), issue #106.
167 changes: 167 additions & 0 deletions docs/.vitepress/theme/PackageGrid.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<script setup lang="ts">
import { withBase } from 'vitepress'

// User-facing packages only. Inclusion test: "would an app author `npm i` this
// directly?" Non-user-facing tooling/infra (e.g. the universal-* engines) stay
// off the grid so the list is signal, not noise. Grows as packages graduate.
interface Pkg {
name: string
description: string
link: string
}
interface Category {
title: string
tagline: string
packages: Pkg[]
}

const categories: Category[] = [
{
title: 'AI runtime',
tagline: 'Build, extend, and orchestrate agents. The AI family, all on ai-sdk.',
packages: [
{
name: 'ai-sdk',
description:
'The agent runtime: providers, the agent loop, tools, streaming, structured output, memory, and evals.',
link: '/packages/ai-sdk/',
},
{
name: 'ai-skills',
description:
'Portable capability bundles: load SKILL.md skills and compose them onto an agent on demand.',
link: '/packages/ai-skills',
},
{
name: 'ai-autopilot',
description:
'Multi-agent orchestration: a Supervisor that plans, dispatches subagents, and synthesizes the result.',
link: '/packages/ai-autopilot',
},
{
name: 'ai-mcp',
description:
'The agent ↔ MCP bridge: consume a remote server’s tools, or expose an agent as an MCP server.',
link: '/packages/ai-mcp',
},
],
},
{
title: 'Protocol & connectors',
tagline: 'Author MCP servers and connect agents to external services.',
packages: [
{
name: 'mcp',
description:
'A standalone framework for authoring MCP servers: tools, resources, prompts, OAuth 2.1, a neutral HTTP handler.',
link: '/packages/mcp',
},
{
name: 'connectors',
description:
'The connector contract: declare a tool connector to an external service and compose any number into one MCP server.',
link: '/packages/connectors',
},
{
name: 'connector-github',
description:
'First-party GitHub connector: read and act on issues, pull requests, and repository files.',
link: '/packages/connector-github',
},
{
name: 'connector-google-drive',
description:
'First-party Google Drive connector: browse, read, and share Drive files.',
link: '/packages/connector-google-drive',
},
],
},
]
</script>

<template>
<div class="pkg-grid">
<section v-for="cat in categories" :key="cat.title" class="pkg-cat">
<header class="pkg-cat__head">
<h2 class="pkg-cat__title">{{ cat.title }}</h2>
<p class="pkg-cat__tagline">{{ cat.tagline }}</p>
</header>
<ul class="pkg-cat__list">
<li v-for="pkg in cat.packages" :key="pkg.name">
<a class="pkg-card" :href="withBase(pkg.link)">
<span class="pkg-card__name">
<span class="pkg-card__scope">@gemstack/</span>{{ pkg.name }}
</span>
<span class="pkg-card__desc">{{ pkg.description }}</span>
</a>
</li>
</ul>
</section>
</div>
</template>

<style scoped>
.pkg-grid {
margin-top: 2rem;
}
.pkg-cat + .pkg-cat {
margin-top: 2.75rem;
}
.pkg-cat__head {
border-top: 1px solid var(--vp-c-divider);
padding-top: 1rem;
}
.pkg-cat__title {
margin: 0;
font-size: 1.05rem;
font-weight: 600;
letter-spacing: -0.01em;
border: 0;
padding: 0;
}
.pkg-cat__tagline {
margin: 0.25rem 0 0;
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.pkg-cat__list {
list-style: none;
margin: 1rem 0 0;
padding: 0;
display: grid;
gap: 12px;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
.pkg-card {
display: flex;
flex-direction: column;
height: 100%;
padding: 16px 18px;
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
transition: border-color 0.2s, background-color 0.2s, transform 0.15s;
text-decoration: none;
}
.pkg-card:hover {
border-color: var(--vp-c-brand-1);
background: var(--vp-c-bg);
transform: translateY(-2px);
}
.pkg-card__name {
font-family: var(--vp-font-family-mono);
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-brand-1);
}
.pkg-card__scope {
color: var(--vp-c-text-3);
font-weight: 400;
}
.pkg-card__desc {
margin-top: 8px;
color: var(--vp-c-text-2);
font-size: 0.85rem;
line-height: 1.5;
}
</style>
4 changes: 4 additions & 0 deletions docs/.vitepress/theme/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import DefaultTheme from 'vitepress/theme'
import './custom.css'
import type { Theme } from 'vitepress'
import PackageGrid from './PackageGrid.vue'

const theme: Theme = {
extends: DefaultTheme,
enhanceApp({ app }) {
app.component('PackageGrid', PackageGrid)
},
}

export default theme
13 changes: 3 additions & 10 deletions docs/packages/index.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
# The GemStack family

All packages publish under the **`@gemstack/`** scope (e.g. `npm install @gemstack/ai-sdk`). Each is standalone and framework-agnostic; they compose, but you adopt only what you need.

| Package | Description |
|---|---|
| [`ai-sdk`](/packages/ai-sdk/) | The agent runtime: providers, the agent loop, tools, streaming, middleware, structured output, memory, and evals. The engine the rest of the AI family builds on. |
| [`ai-skills`](/packages/ai-skills) | Portable capability bundles: load `SKILL.md` skills (instructions + tools + resources) and compose them onto an agent on demand. |
| [`ai-autopilot`](/packages/ai-autopilot) | Orchestration: a Supervisor that plans, dispatches subagents (bounded concurrency + budget guardrails), and synthesizes the result. |
| [`ai-mcp`](/packages/ai-mcp) | The agent/MCP bridge: consume a remote MCP server's tools as agent tools, and expose an agent as an MCP server. |
| [`mcp`](/packages/mcp) | A standalone framework for *authoring* MCP servers: tools, resources, prompts, decorators, OAuth 2.1, a framework-neutral HTTP handler, and a test client. Agent-agnostic. |
| [`connectors`](/packages/connectors) | The connector contract: `defineConnector` declares a tool connector to an external service; `mountConnectors` composes any number into one `@gemstack/mcp` server. First-party connectors: [GitHub](/packages/connector-github), [Google Drive](/packages/connector-google-drive). |
All packages publish under the **`@gemstack/`** scope (e.g. `npm install @gemstack/ai-sdk`). Each is standalone and framework-agnostic; they compose, but you adopt only what you need. The grid below lists the packages an app author installs directly, grouped by what they do.

<PackageGrid />

## How they fit together

Expand Down
36 changes: 36 additions & 0 deletions examples/autopilot-quickstart/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# @gemstack/example-autopilot-quickstart

A runnable, end-to-end quickstart for [`@gemstack/ai-autopilot`](../../packages/ai-autopilot) — the four layers of the epic composed into one "build a feature" flow:

```
personas → Supervisor → runner (sandbox) → surfaces
```

A lead planner decomposes the task **"Add a paginated Orders page backed by an orders table"** and routes each subtask to a stack-aware **persona** (`universal-orm-modeler`, `vike-page-builder`, `ui-intent-designer`). The **Supervisor** dispatches them; each persona worker acts inside a **runner** sandbox — writing Vike/ORM files through `runnerTools` — and progress is rendered through the **surfaces** (a terminal sink for live output, plus a background handle exposing events + result).

It runs **offline**: `AiFake` scripts the model, so there's no API key and the output is deterministic.

## Run it

```bash
pnpm install && pnpm build # from the repo root, to build the packages first
pnpm --filter @gemstack/example-autopilot-quickstart start
```

You'll see the live plan, the files written into the sandbox, a build + preview URL, and the synthesized result.

## Test it

```bash
pnpm --filter @gemstack/example-autopilot-quickstart test
```

## Going real

The only fakes here are `AiFake` (the model) and `FakeRunner` (the sandbox). To run it for real, drop `AiFake` and give the personas real model strings, and swap `FakeRunner` for a real runner adapter (a `FlueRunner`, WebContainer, or Docker sandbox) — the `Runner` interface is the same, so nothing else in the flow changes.

## What each file shows

- **`src/autopilot.ts`** — the composition: `personaWorkers` + `runnerTools`, `agentPlanner` fed the `personaRoster`, a `Supervisor`, and `launchAutopilot` + `terminalSink` for the surfaces.
- **`src/main.ts`** — the runnable demo.
- **`src/autopilot.test.ts`** — asserts the four layers actually compose (plan routes by persona, files land in the sandbox, surfaces capture events).
23 changes: 23 additions & 0 deletions examples/autopilot-quickstart/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@gemstack/example-autopilot-quickstart",
"version": "0.0.0",
"private": true,
"description": "Runnable end-to-end quickstart for @gemstack/ai-autopilot: personas + Supervisor + runner + surfaces composed into one build-a-feature flow, offline via AiFake.",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit",
"test": "tsc -p tsconfig.test.json && cd dist-test && node --test",
"clean": "rm -rf dist-test",
"start": "tsx src/main.ts"
},
"dependencies": {
"@gemstack/ai-autopilot": "workspace:^",
"@gemstack/ai-sdk": "workspace:^",
"zod": "^4.0.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"tsx": "^4.19.0",
"typescript": "^5.4.0"
}
}
41 changes: 41 additions & 0 deletions examples/autopilot-quickstart/src/autopilot.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { runQuickstart, TASK } from './autopilot.js'

describe('autopilot quickstart: the four layers compose end-to-end', () => {
it('plans by persona, dispatches, acts in the sandbox, and surfaces progress', async () => {
const lines: string[] = []
const result = await runQuickstart(line => lines.push(line))

// Supervisor: three subtasks, each routed to a stack persona, all succeeded.
assert.equal(result.run.plan.length, 3)
assert.deepEqual(
result.run.plan.map(s => s.worker).sort(),
['ui-intent-designer', 'universal-orm-modeler', 'vike-page-builder'],
)
assert.ok(result.run.results.every(r => r.ok), 'every subtask succeeded')

// Runner: each persona worker wrote its file into the sandbox via runnerTools.
assert.ok('database/schema.ts' in result.files)
assert.ok('pages/orders/+Page.jsx' in result.files)
assert.match(result.files['database/schema.ts']!, /orders/)
// the seed file is still there
assert.ok('package.json' in result.files)

// Runner: the post-build exec ran and a preview URL was exposed.
assert.equal(result.build.exitCode, 0)
assert.match(result.build.stdout, /built/)
assert.match(result.previewUrl, /:5173$/)

// Surfaces: the terminal sink printed a plan line and per-subtask lines;
// the background handle captured the same events.
assert.ok(lines.some(l => l.includes('plan:')), 'terminal printed the plan')
assert.ok(lines.some(l => l.includes('✓')), 'terminal printed a completed subtask')
assert.equal(result.events[0]!.type, 'plan')
assert.equal(result.events.at(-1)!.type, 'synthesize')
})

it('exposes the task constant for the runnable demo', () => {
assert.match(TASK, /Orders/)
})
})
Loading
Loading