From c748f9ca28fb1e221cd85c2ecb497a851497c152 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 31 Aug 2026 19:41:08 -0700 Subject: [PATCH] docs(cockpit): bring the Mastra runtime example to content parity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mastra topic's cockpit page rendered an empty docs pane and no backend code: its descriptor declared no docsAssetPaths or backendAssetPaths and only one prompt, versus the aws-strands and microsoft-agent-framework siblings (guide.md + two backend files + a backend-build prompt). - Add cockpit/runtimes/mastra/angular/docs/guide.md matching the siblings' structure: what the example demonstrates (camping planner over @ag-ui/mastra), the suspend/approval interrupt flow and its command.interruptEvent resume shape, working memory as STATE_SNAPSHOT/STATE_DELTA, the hand-written Node hosting service at deployments/ag-ui-mastra/, and the honest subagents note (upstream reserves ACTIVITY for background tasks). - Add prompts/mastra-backend.md — the backend-build prompt siblings keep in their python lane; mastra has no python lane so both prompts live beside the Angular app. - Descriptor: declare docsAssetPaths, backendAssetPaths (deployments/ag-ui-mastra/{agents,server}.mjs — the topic's real backend lives outside cockpit/ by design), runtimeUrl, devPort. - apps/cockpit: trace deployments/ag-ui-mastra/*.mjs into the deployed bundle, map .mjs to javascript highlighting, and extract JSDoc sections from .mjs backend files. - Extend the #910 wiring-guard spec: assert the new mastra fields and that every declared mastra asset exists on disk (the content bundle degrades to 'File not found' silently otherwise). Co-Authored-By: Claude Opus 5 --- apps/cockpit/next.config.ts | 3 + apps/cockpit/src/lib/content-bundle.ts | 3 +- apps/cockpit/src/lib/route-resolution.spec.ts | 33 +++++++++ cockpit/runtimes/mastra/angular/docs/guide.md | 67 +++++++++++++++++++ .../mastra/angular/prompts/mastra-backend.md | 29 ++++++++ cockpit/runtimes/mastra/angular/src/index.ts | 16 +++++ 6 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 cockpit/runtimes/mastra/angular/docs/guide.md create mode 100644 cockpit/runtimes/mastra/angular/prompts/mastra-backend.md diff --git a/apps/cockpit/next.config.ts b/apps/cockpit/next.config.ts index 7f84955dd..6801053d8 100644 --- a/apps/cockpit/next.config.ts +++ b/apps/cockpit/next.config.ts @@ -13,6 +13,9 @@ export const nextConfig: WithNxOptions = { '../../cockpit/**/*.md', '../../cockpit/**/*.py', '../../cockpit/**/*.ts', + // The mastra runtime's backend assets live outside cockpit/ — the + // Node hosting service IS that topic's backend (no python lane). + '../../deployments/ag-ui-mastra/*.mjs', '../../nx.json', ], }, diff --git a/apps/cockpit/src/lib/content-bundle.ts b/apps/cockpit/src/lib/content-bundle.ts index a9ecc89f4..008ca07d1 100644 --- a/apps/cockpit/src/lib/content-bundle.ts +++ b/apps/cockpit/src/lib/content-bundle.ts @@ -63,6 +63,7 @@ const LANG_MAP: Record = { ts: 'typescript', tsx: 'tsx', js: 'javascript', + mjs: 'javascript', jsx: 'jsx', py: 'python', md: 'markdown', @@ -129,7 +130,7 @@ export async function getContentBundle( // Extract doc sections const fileName = path.split('/').pop() ?? path; - if (path.endsWith('.ts') || path.endsWith('.tsx')) { + if (path.endsWith('.ts') || path.endsWith('.tsx') || path.endsWith('.mjs')) { docSections.push(...extractTsDocSections(source, fileName)); } else if (path.endsWith('.py')) { docSections.push(...extractPyDocSections(source, fileName)); diff --git a/apps/cockpit/src/lib/route-resolution.spec.ts b/apps/cockpit/src/lib/route-resolution.spec.ts index 7b0956eeb..ddf498b39 100644 --- a/apps/cockpit/src/lib/route-resolution.spec.ts +++ b/apps/cockpit/src/lib/route-resolution.spec.ts @@ -1,3 +1,5 @@ +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; import { cockpitManifest } from '@threadplane/cockpit-registry'; import { @@ -117,8 +119,39 @@ describe('runtimes capability presentation', () => { 'cockpit/runtimes/mastra/angular/src/app/mastra.component.ts' ); expect(presentation.promptAssetPaths).toEqual([ + 'cockpit/runtimes/mastra/angular/prompts/mastra-backend.md', 'cockpit/runtimes/mastra/angular/prompts/mastra.md', ]); + // Backend assets deliberately point outside cockpit/: the topic's + // backend is the Node hosting service, not a cockpit/ Python lane. + expect(presentation.backendAssetPaths).toEqual([ + 'deployments/ag-ui-mastra/agents.mjs', + 'deployments/ag-ui-mastra/server.mjs', + ]); + expect(presentation.docsAssetPaths).toEqual([ + 'cockpit/runtimes/mastra/angular/docs/guide.md', + ]); + expect(presentation.runtimeUrl).toBe('runtimes/mastra'); + expect(presentation.devPort).toBe(4332); + + // Every declared asset must exist on disk — the content bundle renders + // "File not found" instead of failing, so a typo here is silent in CI. + // Same workspace-root discovery as content-bundle.ts: walk up from CWD + // until nx.json (vitest may run from the app dir or the workspace root). + let workspaceRoot = process.cwd(); + while (!existsSync(join(workspaceRoot, 'nx.json'))) { + const parent = join(workspaceRoot, '..'); + if (parent === workspaceRoot) break; + workspaceRoot = parent; + } + for (const path of [ + ...presentation.promptAssetPaths, + ...presentation.codeAssetPaths, + ...presentation.backendAssetPaths, + ...presentation.docsAssetPaths, + ]) { + expect(existsSync(join(workspaceRoot, path)), `missing asset: ${path}`).toBe(true); + } }); }); diff --git a/cockpit/runtimes/mastra/angular/docs/guide.md b/cockpit/runtimes/mastra/angular/docs/guide.md new file mode 100644 index 000000000..d60340a11 --- /dev/null +++ b/cockpit/runtimes/mastra/angular/docs/guide.md @@ -0,0 +1,67 @@ +# Runtimes — Mastra + +Third entry on the one-capability-many-runtimes axis +(`cockpit/runtimes//`): the same neutral `Agent` contract and the +same `@threadplane/chat` UI primitives as every other AG-UI example, over a +backend that is genuinely not LangGraph — and, uniquely on this axis, not +Python either. + +## What it demonstrates + +| Surface | How | +| --- | --- | +| Messages | Streamed assistant text (`TEXT_MESSAGE_CHUNK`) from a Mastra `Agent` via the `@ag-ui/mastra` bridge. | +| Tool calls | `check_conditions` executes server-side, no pause. | +| Shared state | Working memory bridged honestly: the agent's `packing_list` working-memory schema streams as a `STATE_SNAPSHOT` plus real JSON-Patch `STATE_DELTA` events while the agent updates the list (measured in the spike's 04a capture) — the only runtime on this axis that emits deltas. | +| Interrupts | `reserve_campsite` suspends the run via its `suspendSchema`/`resumeSchema` pair; the bridge emits a `CUSTOM on_interrupt` payload (`{ toolCallId, toolName, suspendPayload, runId }`) followed by the protocol-standard `RUN_FINISHED.outcome = { type: 'interrupt', interrupts: [...] }`. The adapter resumes with the Mastra wire shape `forwardedProps.command = { resume, interruptEvent: { toolCallId, runId } }`. | +| Subagents | Not demonstrated — upstream reserves `ACTIVITY_*` events for background tasks, so delegation has no per-subagent stream (measured red in the 2026-08-31 runtime matrix). | + +Unlike the Python-lane runtimes, the interrupt path here does use a `CUSTOM +on_interrupt` event — it is the one convention the Mastra bridge shares with +the LangGraph bridge — but the run still finishes with the outcome-provenance +`RUN_FINISHED` shape added in #888/#889/#891, so the reducer treats all three +runtimes identically. + +Suspend/resume REQUIRES persistent storage: Mastra writes suspended-run +snapshots to LibSQL file storage and resume loads them back, so an in-memory +store would orphan every pending approval across HTTP requests. + +## The hosting service (Node lane) + +Upstream `@ag-ui/mastra` ships no plain AG-UI HTTP endpoint — only the +in-process `MastraAgent` bridge and a CopilotKit runtime mount. The backend +is therefore the hand-written Node service `deployments/ag-ui-mastra/`: +`server.mjs` subscribes to `MastraAgent.run(input)` (the raw AG-UI event +Observable) and encodes each event as one SSE `data:` frame — exactly what +`@ag-ui/client`'s `HttpAgent` consumes. It mirrors the Python lane's +behavior contract (`GET /ok` unauthenticated, `X-Internal-Token` on every +other route, topics at `POST /agent/`, Observable errors mapped to a +`RUN_ERROR` frame). The agent itself lives in `agents.mjs`, next to the +shim, because there is no per-example Python module to stage into a +generated deployment. This is why `cockpit/runtimes/mastra/` has no +`python/` directory and its assets live in the `angular` lane. + +## Model client + +Mastra's model router resolves the plain string `openai/gpt-4o-mini` on +`OPENAI_API_KEY` — no provider SDK wiring. `OPENAI_BASE_URL` is honored, +which is how the aimock e2e harness intercepts model calls without a code +fork. + +## Running locally + +```sh +npx tsx apps/cockpit/scripts/serve-example.ts --capability=rt-mastra +``` + +Angular dev server on :4332. The serve script only auto-starts Python +backends, so start the Node service manually (see +`deployments/ag-ui-mastra/README.md`): + +```sh +cd deployments/ag-ui-mastra && npm ci +AG_UI_INTERNAL_TOKEN=dev-local-token OPENAI_API_KEY=sk-... PORT=5332 node server.mjs +``` + +The example's dev proxy rewrites `/agent` to +`http://localhost:5332/agent/mastra` and injects the dev token. diff --git a/cockpit/runtimes/mastra/angular/prompts/mastra-backend.md b/cockpit/runtimes/mastra/angular/prompts/mastra-backend.md new file mode 100644 index 000000000..81be13634 --- /dev/null +++ b/cockpit/runtimes/mastra/angular/prompts/mastra-backend.md @@ -0,0 +1,29 @@ +# Prompt: camping trip planner on Mastra + +Build a Mastra agent exposed over AG-UI: + +- Resolve the model through Mastra's model router with the plain string + `openai/gpt-4o-mini` (honors `OPENAI_API_KEY` and `OPENAI_BASE_URL`) — no + provider SDK wiring. +- Add a plain backend tool (`check_conditions`, via `createTool`) that + executes server-side without pausing. Keep it deterministic so e2e + fixtures stay stable. +- Make `reserve_campsite` a human-in-the-loop tool: give it a + `suspendSchema`/`resumeSchema` pair and call `suspend(...)` on the first + invocation; the AG-UI bridge signals it as a `CUSTOM on_interrupt` payload + followed by the protocol-standard `RUN_FINISHED` interrupt outcome, and + the resume arrives as `forwardedProps.command = { resume, + interruptEvent: { toolCallId, runId } }`. +- Give the agent `Memory` with a `workingMemory` schema (`packing_list`) + backed by file-based `LibSQLStore` storage. Working memory bridges to + AG-UI shared state as a `STATE_SNAPSHOT` plus real JSON-Patch + `STATE_DELTA` events. File-backed storage is REQUIRED: suspended-run + snapshots persist there, and resume loads them back across HTTP requests. +- Upstream `@ag-ui/mastra` ships no plain AG-UI HTTP endpoint, so + hand-write the hosting service: for each `POST /agent/` request, + construct a fresh `MastraAgent` bridge (`resourceId` keyed by the AG-UI + `threadId`), subscribe to `run(input)`, and write one SSE `data:` frame + per event. Map Observable errors to a `RUN_ERROR` frame, never a dropped + socket. +- Do not add a multi-agent surface — Mastra reserves `ACTIVITY_*` events + for background tasks, so delegation has no per-subagent stream. diff --git a/cockpit/runtimes/mastra/angular/src/index.ts b/cockpit/runtimes/mastra/angular/src/index.ts index f1af94166..315e34e66 100644 --- a/cockpit/runtimes/mastra/angular/src/index.ts +++ b/cockpit/runtimes/mastra/angular/src/index.ts @@ -11,6 +11,10 @@ export interface CockpitCapabilityModule { docsPath: string; promptAssetPaths: string[]; codeAssetPaths: string[]; + backendAssetPaths: string[]; + docsAssetPaths: string[]; + runtimeUrl?: string; + devPort?: number; } export const runtimesMastraAngularModule: CockpitCapabilityModule = { @@ -25,10 +29,22 @@ export const runtimesMastraAngularModule: CockpitCapabilityModule = { title: 'Runtimes — Mastra (Angular)', docsPath: '/docs/runtimes/core-capabilities/mastra/overview/angular', promptAssetPaths: [ + 'cockpit/runtimes/mastra/angular/prompts/mastra-backend.md', 'cockpit/runtimes/mastra/angular/prompts/mastra.md', ], codeAssetPaths: [ 'cockpit/runtimes/mastra/angular/src/app/mastra.component.ts', 'cockpit/runtimes/mastra/angular/src/app/app.config.ts', ], + // The Mastra backend is the Node AG-UI service, not a cockpit/ Python + // lane — these paths intentionally point outside cockpit/ (the cockpit + // app reads workspace-root-relative paths and its file tracing stages + // this directory explicitly). + backendAssetPaths: [ + 'deployments/ag-ui-mastra/agents.mjs', + 'deployments/ag-ui-mastra/server.mjs', + ], + docsAssetPaths: ['cockpit/runtimes/mastra/angular/docs/guide.md'], + runtimeUrl: 'runtimes/mastra', + devPort: 4332, };