From 7b76a2069cbb7ce562e8a01a35aba1ceb3752ccb Mon Sep 17 00:00:00 2001 From: Suleiman Shahbari Date: Thu, 2 Jul 2026 11:45:14 +0300 Subject: [PATCH] docs(ai-autopilot): end-to-end quickstart example (personas + supervisor + runner + surfaces) A runnable, offline (AiFake) example that composes all four epic layers into one build-a-feature flow: a planner routes subtasks to stack personas, the Supervisor dispatches them, each worker writes Vike/ORM files inside a runner sandbox via runnerTools, and the surfaces render live progress + a background handle. Doubles as the integration contract a real FlueRunner must satisfy. Private workspace example (@gemstack/example-autopilot-quickstart), in turbo typecheck+test. Capstone for epic #97. --- examples/autopilot-quickstart/README.md | 36 +++++ examples/autopilot-quickstart/package.json | 23 +++ .../src/autopilot.test.ts | 41 +++++ .../autopilot-quickstart/src/autopilot.ts | 150 ++++++++++++++++++ examples/autopilot-quickstart/src/main.ts | 23 +++ examples/autopilot-quickstart/tsconfig.json | 5 + .../autopilot-quickstart/tsconfig.test.json | 5 + pnpm-lock.yaml | 22 +++ 8 files changed, 305 insertions(+) create mode 100644 examples/autopilot-quickstart/README.md create mode 100644 examples/autopilot-quickstart/package.json create mode 100644 examples/autopilot-quickstart/src/autopilot.test.ts create mode 100644 examples/autopilot-quickstart/src/autopilot.ts create mode 100644 examples/autopilot-quickstart/src/main.ts create mode 100644 examples/autopilot-quickstart/tsconfig.json create mode 100644 examples/autopilot-quickstart/tsconfig.test.json diff --git a/examples/autopilot-quickstart/README.md b/examples/autopilot-quickstart/README.md new file mode 100644 index 0000000..0a27288 --- /dev/null +++ b/examples/autopilot-quickstart/README.md @@ -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). diff --git a/examples/autopilot-quickstart/package.json b/examples/autopilot-quickstart/package.json new file mode 100644 index 0000000..bff0539 --- /dev/null +++ b/examples/autopilot-quickstart/package.json @@ -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" + } +} diff --git a/examples/autopilot-quickstart/src/autopilot.test.ts b/examples/autopilot-quickstart/src/autopilot.test.ts new file mode 100644 index 0000000..daf7d82 --- /dev/null +++ b/examples/autopilot-quickstart/src/autopilot.test.ts @@ -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/) + }) +}) diff --git a/examples/autopilot-quickstart/src/autopilot.ts b/examples/autopilot-quickstart/src/autopilot.ts new file mode 100644 index 0000000..b1a4294 --- /dev/null +++ b/examples/autopilot-quickstart/src/autopilot.ts @@ -0,0 +1,150 @@ +import { AiFake, agent, type ToolCall } from '@gemstack/ai-sdk' +import { + Supervisor, + agentPlanner, + stackPersonas, + personaRoster, + personaInstructions, + personaTools, + FakeRunner, + runnerTools, + terminalSink, + launchAutopilot, + type RunnerSession, + type SupervisorEvent, + type SupervisorRun, +} from '@gemstack/ai-autopilot' + +/** + * The end-to-end shape of the ai-autopilot epic, in one flow: + * + * personas → Supervisor → runner (sandbox) → surfaces + * + * A lead planner decomposes a build task and routes each subtask to a + * stack-aware **persona**; the **Supervisor** dispatches them; each persona + * worker acts inside a **runner** sandbox (writing Vike/ORM files via + * `runnerTools`); progress is rendered through the **surfaces** (a terminal + * sink plus a background handle with a live stream). + * + * It runs offline: `AiFake` scripts the model, so there is no API key and the + * output is deterministic. Swapping `FakeRunner` for a real runner and dropping + * the fake is the only change needed to run it for real. + */ + +/** The feature we ask autopilot to build. */ +export const TASK = 'Add a paginated Orders page backed by an orders table' + +/** Each subtask, the persona that should own it, and the file it writes. */ +const WORK = [ + { + worker: 'universal-orm-modeler', + description: 'Define the orders schema and a migration', + file: 'database/schema.ts', + contents: "export const orders = table('orders', { id: id(), total: integer(), createdAt: timestamp() })\n", + }, + { + worker: 'vike-page-builder', + description: 'Build the /orders page that lists orders, paginated', + file: 'pages/orders/+Page.jsx', + contents: "export default function Page({ orders }) { return }\n", + }, + { + worker: 'ui-intent-designer', + description: 'Express the orders list as intent, not hardcoded markup', + file: 'pages/orders/+config.js', + contents: "export default { meta: { OrderList: { env: { server: true, client: true } } } }\n", + }, +] as const + +/** Script the fake: step 0 is the planner's JSON; then each worker writes its file. */ +function scriptModel(fake: AiFake): void { + const plannerOutput = JSON.stringify(WORK.map(w => ({ description: w.description, worker: w.worker }))) + const workerSteps = WORK.flatMap((w, i) => { + const toolCalls: ToolCall[] = [ + { id: `write-${i}`, name: 'write_file', arguments: { path: w.file, contents: w.contents } }, + ] + return [{ toolCalls }, { text: `Wrote ${w.file}` }] + }) + // concurrency: 1 makes the provider-call order deterministic: + // 0 = planner, then each worker's (tool-call, final-text) pair in plan order. + fake.respondWithSequence([{ text: plannerOutput }, ...workerSteps]) +} + +/** Build one worker agent per persona, each with hands inside the sandbox. */ +function personaWorkersWithSandbox(session: RunnerSession): Record> { + const sandbox = runnerTools(session) + return Object.fromEntries( + stackPersonas.map(p => [ + p.name, + agent({ instructions: personaInstructions(p), tools: [...personaTools(p), ...sandbox] }), + ]), + ) +} + +export interface QuickstartResult { + run: SupervisorRun + /** Every event, in order (from the background handle). */ + events: SupervisorEvent[] + /** Files the workers wrote into the sandbox. */ + files: Record + /** Output of the post-build `exec`. */ + build: { stdout: string; exitCode: number } + /** The preview URL the sandbox exposed. */ + previewUrl: string +} + +/** + * Run the whole flow once and return everything the surfaces exposed. + * + * @param write where terminal-surface lines go (default: no-op; `main.ts` prints). + */ +export async function runQuickstart(write: (line: string) => void = () => {}): Promise { + const fake = AiFake.fake() + scriptModel(fake) + try { + // Runner: an in-memory sandbox seeded with a minimal Vike project. + const runner = new FakeRunner({ + onExec: cmd => + cmd.includes('build') + ? { stdout: 'orders page built', stderr: '', exitCode: 0 } + : { stdout: '', stderr: '', exitCode: 0 }, + }) + const session = await runner.boot({ files: { 'package.json': '{ "name": "shop" }\n' } }) + + // Personas → Supervisor. The planner is told the roster so it routes by role. + const planner = agentPlanner( + agent(`You are the lead engineer. Decompose the task and route each subtask to a persona.\n\n${personaRoster(stackPersonas)}`), + ) + const start = (onEvent: (e: SupervisorEvent) => void) => + new Supervisor({ + plan: planner, + workers: personaWorkersWithSandbox(session), + concurrency: 1, + onEvent, + }).run(TASK) + + // Surfaces: one run feeds both the terminal (live) and a background handle. + const terminal = terminalSink({ write }) + const handle = launchAutopilot(onEvent => + start(e => { + terminal(e) + onEvent(e) + }), + ) + const run = await handle.result() + + // Runner again: build the app and grab a preview URL. + const build = await session.exec('pnpm build') + const preview = session.preview ? await session.preview({ port: 5173 }) : { url: '' } + + return { + run, + events: handle.events(), + files: session.snapshot(), + build: { stdout: build.stdout, exitCode: build.exitCode }, + previewUrl: preview.url, + } + } finally { + fake.restore() + } +} diff --git a/examples/autopilot-quickstart/src/main.ts b/examples/autopilot-quickstart/src/main.ts new file mode 100644 index 0000000..7c300bc --- /dev/null +++ b/examples/autopilot-quickstart/src/main.ts @@ -0,0 +1,23 @@ +import { runQuickstart, TASK } from './autopilot.js' + +/** Run the quickstart and print what each surface exposed. Offline (AiFake). */ +async function main(): Promise { + console.log(`Task: ${TASK}\n`) + console.log('--- live progress (terminal surface) ---') + const result = await runQuickstart(line => process.stdout.write(line + '\n')) + + console.log('\n--- files written into the sandbox (runner) ---') + for (const path of Object.keys(result.files).sort()) console.log(` ${path}`) + + console.log('\n--- build + preview (runner) ---') + console.log(` build: exit ${result.build.exitCode} — ${result.build.stdout}`) + console.log(` preview: ${result.previewUrl}`) + + console.log('\n--- synthesized result (Supervisor) ---') + console.log(result.run.text.replace(/^/gm, ' ')) +} + +main().catch(err => { + console.error(err) + process.exitCode = 1 +}) diff --git a/examples/autopilot-quickstart/tsconfig.json b/examples/autopilot-quickstart/tsconfig.json new file mode 100644 index 0000000..404aab4 --- /dev/null +++ b/examples/autopilot-quickstart/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "noEmit": true, "rootDir": "src" }, + "include": ["src"] +} diff --git a/examples/autopilot-quickstart/tsconfig.test.json b/examples/autopilot-quickstart/tsconfig.test.json new file mode 100644 index 0000000..eebda2f --- /dev/null +++ b/examples/autopilot-quickstart/tsconfig.test.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "outDir": "dist-test", "rootDir": "src" }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39f1e17..2eb1ca6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,6 +27,28 @@ importers: specifier: ^3.5.29 version: 3.5.39(typescript@5.9.3) + examples/autopilot-quickstart: + dependencies: + '@gemstack/ai-autopilot': + specifier: workspace:^ + version: link:../../packages/ai-autopilot + '@gemstack/ai-sdk': + specifier: workspace:^ + version: link:../../packages/ai-sdk + zod: + specifier: ^4.0.0 + version: 4.4.3 + devDependencies: + '@types/node': + specifier: ^20.0.0 + version: 20.19.43 + tsx: + specifier: ^4.19.0 + version: 4.22.4 + typescript: + specifier: ^5.4.0 + version: 5.9.3 + examples/connectors-quickstart: dependencies: '@gemstack/connectors':