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
3 changes: 3 additions & 0 deletions apps/cockpit/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
],
},
Expand Down
3 changes: 2 additions & 1 deletion apps/cockpit/src/lib/content-bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const LANG_MAP: Record<string, string> = {
ts: 'typescript',
tsx: 'tsx',
js: 'javascript',
mjs: 'javascript',
jsx: 'jsx',
py: 'python',
md: 'markdown',
Expand Down Expand Up @@ -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));
Expand Down
33 changes: 33 additions & 0 deletions apps/cockpit/src/lib/route-resolution.spec.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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);
}
});
});

Expand Down
67 changes: 67 additions & 0 deletions cockpit/runtimes/mastra/angular/docs/guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Runtimes — Mastra

Third entry on the one-capability-many-runtimes axis
(`cockpit/runtimes/<runtime>/`): 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/<topic>`, 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.
29 changes: 29 additions & 0 deletions cockpit/runtimes/mastra/angular/prompts/mastra-backend.md
Original file line number Diff line number Diff line change
@@ -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/<topic>` 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.
16 changes: 16 additions & 0 deletions cockpit/runtimes/mastra/angular/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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,
};
Loading