Skip to content

Commit 0b9da21

Browse files
authored
Merge branch 'main' into blove/library-pages
2 parents 82b5365 + 8c48de1 commit 0b9da21

26 files changed

Lines changed: 1197 additions & 37 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { cockpitManifest } from '@threadplane/cockpit-registry';
2+
import { capabilities } from './scripts/capability-registry';
3+
import {
4+
buildNavigationTree,
5+
capabilityModules,
6+
} from './src/lib/route-resolution';
7+
8+
/**
9+
* The cockpit site is assembled from three lists that nothing forced to agree:
10+
*
11+
* - `apps/cockpit/scripts/capability-registry.ts` — what serve/build/deploy know about;
12+
* - `libs/cockpit-registry` `cockpitManifest` — what the Next route can resolve;
13+
* - `capabilityModules` in `route-resolution.ts` — what supplies a page's assets.
14+
*
15+
* When the `runtimes` product shipped, only the first list learned about it, so
16+
* `/runtimes/core-capabilities/<topic>/overview/<lang>` threw
17+
* "No manifest entry found …" and every runtime page 500'd in production while
18+
* the whole suite stayed green. These assertions are the missing coupling.
19+
*/
20+
describe('cockpit capability wiring', () => {
21+
const manifestKey = (e: { product: string; section: string; topic: string }) =>
22+
`${e.product}/${e.section}/${e.topic}`;
23+
24+
it('gives every registered capability a resolvable manifest entry', () => {
25+
const manifestKeys = new Set(cockpitManifest.map(manifestKey));
26+
27+
const unroutable = capabilities
28+
.map((capability) => `${capability.product}/core-capabilities/${capability.topic}`)
29+
.filter((key) => !manifestKeys.has(key));
30+
31+
expect(unroutable).toEqual([]);
32+
});
33+
34+
it('gives every registered capability a cockpit module in route-resolution', () => {
35+
const moduleKeys = new Set(
36+
capabilityModules.map((module) => manifestKey(module.manifestIdentity))
37+
);
38+
39+
const unwired = capabilities
40+
.map((capability) => `${capability.product}/core-capabilities/${capability.topic}`)
41+
.filter((key) => !moduleKeys.has(key));
42+
43+
expect(unwired).toEqual([]);
44+
});
45+
46+
it('points every cockpit module at a capability that still exists', () => {
47+
const capabilityKeys = new Set(
48+
capabilities.map(
49+
(capability) => `${capability.product}/core-capabilities/${capability.topic}`
50+
)
51+
);
52+
53+
const orphans = capabilityModules
54+
.map((module) => manifestKey(module.manifestIdentity))
55+
.filter((key) => !capabilityKeys.has(key));
56+
57+
expect(orphans).toEqual([]);
58+
});
59+
60+
it('surfaces every manifest product in the navigation tree', () => {
61+
const manifestProducts = [...new Set(cockpitManifest.map((entry) => entry.product))];
62+
const navigationProducts = buildNavigationTree(cockpitManifest).map(
63+
(product) => product.product
64+
);
65+
66+
expect([...manifestProducts].sort()).toEqual([...navigationProducts].sort());
67+
68+
for (const product of buildNavigationTree(cockpitManifest)) {
69+
const entries = product.sections.flatMap((section) => section.entries);
70+
expect({ product: product.product, empty: entries.length === 0 }).toEqual({
71+
product: product.product,
72+
empty: false,
73+
});
74+
}
75+
});
76+
77+
it('keeps every registry product inside the CockpitProduct union', () => {
78+
// `cockpitManifest` is typed `CockpitManifestEntry[]`, so a product that is
79+
// not in the union cannot appear here — the runtime check is that the
80+
// registry's products are all representable in the manifest.
81+
const manifestProducts = new Set<string>(cockpitManifest.map((entry) => entry.product));
82+
const registryProducts = [...new Set(capabilities.map((c) => c.product))];
83+
84+
expect(registryProducts.filter((p) => !manifestProducts.has(p))).toEqual([]);
85+
});
86+
});

apps/cockpit/next.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ export const nextConfig: WithNxOptions = {
1313
'../../cockpit/**/*.md',
1414
'../../cockpit/**/*.py',
1515
'../../cockpit/**/*.ts',
16+
// The mastra runtime's backend assets live outside cockpit/ — the
17+
// Node hosting service IS that topic's backend (no python lane).
18+
'../../deployments/ag-ui-mastra/*.mjs',
1619
'../../nx.json',
1720
],
1821
},

apps/cockpit/src/lib/content-bundle.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ const LANG_MAP: Record<string, string> = {
6363
ts: 'typescript',
6464
tsx: 'tsx',
6565
js: 'javascript',
66+
mjs: 'javascript',
6667
jsx: 'jsx',
6768
py: 'python',
6869
md: 'markdown',
@@ -129,7 +130,7 @@ export async function getContentBundle(
129130

130131
// Extract doc sections
131132
const fileName = path.split('/').pop() ?? path;
132-
if (path.endsWith('.ts') || path.endsWith('.tsx')) {
133+
if (path.endsWith('.ts') || path.endsWith('.tsx') || path.endsWith('.mjs')) {
133134
docSections.push(...extractTsDocSections(source, fileName));
134135
} else if (path.endsWith('.py')) {
135136
docSections.push(...extractPyDocSections(source, fileName));

apps/cockpit/src/lib/navigation-labels.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ export const PRODUCT_LABELS: Record<string, string> = {
33
'langgraph': 'LangGraph',
44
'render': 'Render',
55
'chat': 'Chat',
6+
'runtimes': 'Runtimes',
67
};
78

89
export function stripProductPrefix(title: string): string {
9-
const prefixes = ['Deep Agents ', 'LangGraph ', 'Render ', 'Chat '];
10+
const prefixes = ['Deep Agents ', 'LangGraph ', 'Render ', 'Chat ', 'Runtimes '];
1011
for (const p of prefixes) {
1112
if (title.startsWith(p)) return title.slice(p.length);
1213
}

apps/cockpit/src/lib/route-resolution.spec.ts

Lines changed: 104 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { existsSync } from 'node:fs';
2+
import { join } from 'node:path';
13
import { describe, expect, it } from 'vitest';
24
import { cockpitManifest } from '@threadplane/cockpit-registry';
35
import {
@@ -47,22 +49,109 @@ describe('buildNavigationTree', () => {
4749
it('groups manifest entries by product and section', () => {
4850
const tree = buildNavigationTree(cockpitManifest);
4951

50-
expect(tree).toHaveLength(5);
51-
expect(tree[0]).toMatchObject({
52-
product: 'deep-agents',
53-
});
54-
expect(tree[1]).toMatchObject({
55-
product: 'langgraph',
56-
});
57-
expect(tree[2]).toMatchObject({
58-
product: 'ag-ui',
59-
});
60-
expect(tree[3]).toMatchObject({
61-
product: 'render',
62-
});
63-
expect(tree[4]).toMatchObject({
64-
product: 'chat',
52+
expect(tree.map((product) => product.product)).toEqual([
53+
'deep-agents',
54+
'langgraph',
55+
'ag-ui',
56+
'render',
57+
'chat',
58+
'runtimes',
59+
]);
60+
});
61+
62+
it('lists every runtimes topic under core-capabilities', () => {
63+
const runtimes = buildNavigationTree(cockpitManifest).find(
64+
(product) => product.product === 'runtimes'
65+
);
66+
const coreCapabilities = runtimes?.sections.find(
67+
(section) => section.section === 'core-capabilities'
68+
);
69+
70+
expect(coreCapabilities?.entries.map((entry) => entry.topic)).toEqual([
71+
'microsoft-agent-framework',
72+
'aws-strands',
73+
'mastra',
74+
]);
75+
});
76+
});
77+
78+
describe('runtimes capability presentation', () => {
79+
const resolveRuntime = (topic: string, language: 'python' | 'typescript' = 'python') =>
80+
resolveCockpitEntry({
81+
manifest: cockpitManifest,
82+
product: 'runtimes',
83+
section: 'core-capabilities',
84+
topic,
85+
page: 'overview',
86+
language,
6587
});
88+
89+
it('resolves each runtime topic instead of throwing', () => {
90+
for (const topic of ['microsoft-agent-framework', 'aws-strands', 'mastra']) {
91+
expect(resolveRuntime(topic)).toMatchObject({
92+
product: 'runtimes',
93+
topic,
94+
entryKind: 'capability',
95+
});
96+
}
97+
});
98+
99+
it('serves Python-lane runtimes from their registered module assets', () => {
100+
const presentation = getCapabilityPresentation(resolveRuntime('aws-strands'));
101+
102+
expect(presentation.kind).toBe('capability');
103+
if (presentation.kind !== 'capability') return;
104+
expect(presentation.runtimeUrl).toBe('runtimes/aws-strands');
105+
expect(presentation.backendAssetPaths).toContain(
106+
'cockpit/runtimes/aws-strands/python/src/agent.py'
107+
);
108+
});
109+
110+
it('falls back to the Angular-lane module for a runtime with no Python lane', () => {
111+
const presentation = getCapabilityPresentation(resolveRuntime('mastra'));
112+
113+
expect(presentation.kind).toBe('capability');
114+
if (presentation.kind !== 'capability') return;
115+
// The manifest entry's language is 'python' (the canonical URL lane) but
116+
// Mastra's only descriptor is the Angular one — the lookup must still find
117+
// it rather than falling through to non-existent cockpit/runtimes/mastra/python paths.
118+
expect(presentation.codeAssetPaths).toContain(
119+
'cockpit/runtimes/mastra/angular/src/app/mastra.component.ts'
120+
);
121+
expect(presentation.promptAssetPaths).toEqual([
122+
'cockpit/runtimes/mastra/angular/prompts/mastra-backend.md',
123+
'cockpit/runtimes/mastra/angular/prompts/mastra.md',
124+
]);
125+
// Backend assets deliberately point outside cockpit/: the topic's
126+
// backend is the Node hosting service, not a cockpit/ Python lane.
127+
expect(presentation.backendAssetPaths).toEqual([
128+
'deployments/ag-ui-mastra/agents.mjs',
129+
'deployments/ag-ui-mastra/server.mjs',
130+
]);
131+
expect(presentation.docsAssetPaths).toEqual([
132+
'cockpit/runtimes/mastra/angular/docs/guide.md',
133+
]);
134+
expect(presentation.runtimeUrl).toBe('runtimes/mastra');
135+
expect(presentation.devPort).toBe(4332);
136+
137+
// Every declared asset must exist on disk — the content bundle renders
138+
// "File not found" instead of failing, so a typo here is silent in CI.
139+
// Same workspace-root discovery as content-bundle.ts: walk up from CWD
140+
// until nx.json (vitest may run from the app dir or the workspace root).
141+
let workspaceRoot = process.cwd();
142+
while (!existsSync(join(workspaceRoot, 'nx.json'))) {
143+
const parent = join(workspaceRoot, '..');
144+
if (parent === workspaceRoot) break;
145+
workspaceRoot = parent;
146+
}
147+
for (const path of [
148+
...presentation.promptAssetPaths,
149+
...presentation.codeAssetPaths,
150+
...presentation.backendAssetPaths,
151+
...presentation.docsAssetPaths,
152+
]) {
153+
expect(existsSync(join(workspaceRoot, path)), `missing asset: ${path}`).toBe(true);
154+
}
66155
});
67156
});
68157

apps/cockpit/src/lib/route-resolution.ts

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { agUiToolViewsPythonModule } from '../../../../cockpit/ag-ui/tool-views/
1818
import { agUiJsonRenderPythonModule } from '../../../../cockpit/ag-ui/json-render/python/src/index';
1919
import { agUiClientToolsPythonModule } from '../../../../cockpit/ag-ui/client-tools/python/src/index';
2020
import { agUiA2uiPythonModule } from '../../../../cockpit/ag-ui/a2ui/python/src/index';
21+
import { agUiSubagentsPythonModule } from '../../../../cockpit/ag-ui/subagents/python/src/index';
2122
import { deepAgentsMemoryPythonModule } from '../../../../cockpit/deep-agents/memory/python/src/index';
2223
import { deepAgentsPlanningPythonModule } from '../../../../cockpit/deep-agents/planning/python/src/index';
2324
import { deepAgentsFilesystemPythonModule } from '../../../../cockpit/deep-agents/filesystem/python/src/index';
@@ -41,6 +42,11 @@ import { chatGenerativeUiPythonModule } from '../../../../cockpit/chat/generativ
4142
import { chatDebugPythonModule } from '../../../../cockpit/chat/debug/python/src/index';
4243
import { chatThemingPythonModule } from '../../../../cockpit/chat/theming/python/src/index';
4344
import { chatA2uiPythonModule } from '../../../../cockpit/chat/a2ui/python/src/index';
45+
import { runtimesMicrosoftAgentFrameworkPythonModule } from '../../../../cockpit/runtimes/microsoft-agent-framework/python/src/index';
46+
import { runtimesAwsStrandsPythonModule } from '../../../../cockpit/runtimes/aws-strands/python/src/index';
47+
// Mastra has no Python lane — its backend is the Node AG-UI service
48+
// deployments/ag-ui-mastra — so its descriptor lives beside the Angular app.
49+
import { runtimesMastraAngularModule } from '../../../../cockpit/runtimes/mastra/angular/src/index';
4450

4551
export interface ResolveCockpitEntryOptions {
4652
manifest: CockpitManifestEntry[];
@@ -79,7 +85,33 @@ export type CapabilityPresentation =
7985
devPort?: number;
8086
};
8187

82-
const capabilityModules = [
88+
/**
89+
* Shape a `cockpit/**\/src/index.ts` descriptor must satisfy to be wired into
90+
* the cockpit. Each example declares its own structural copy of this interface
91+
* (standalone-examples rule), so the fields diverge: the Angular lane carries
92+
* no backend/docs assets. Declaring the element type here keeps the registry
93+
* heterogeneous without widening every reader to a union.
94+
*/
95+
export interface RegisteredCapabilityModule {
96+
id: string;
97+
manifestIdentity: {
98+
product: string;
99+
section: string;
100+
topic: string;
101+
page: string;
102+
language: string;
103+
};
104+
title: string;
105+
docsPath: string;
106+
promptAssetPaths: string[];
107+
codeAssetPaths: string[];
108+
backendAssetPaths?: string[];
109+
docsAssetPaths?: string[];
110+
runtimeUrl?: string;
111+
devPort?: number;
112+
}
113+
114+
export const capabilityModules: RegisteredCapabilityModule[] = [
83115
langgraphStreamingPythonModule,
84116
langgraphPersistencePythonModule,
85117
langgraphInterruptsPythonModule,
@@ -95,6 +127,7 @@ const capabilityModules = [
95127
agUiJsonRenderPythonModule,
96128
agUiClientToolsPythonModule,
97129
agUiA2uiPythonModule,
130+
agUiSubagentsPythonModule,
98131
deepAgentsMemoryPythonModule,
99132
deepAgentsPlanningPythonModule,
100133
deepAgentsFilesystemPythonModule,
@@ -118,6 +151,9 @@ const capabilityModules = [
118151
chatDebugPythonModule,
119152
chatThemingPythonModule,
120153
chatA2uiPythonModule,
154+
runtimesMicrosoftAgentFrameworkPythonModule,
155+
runtimesAwsStrandsPythonModule,
156+
runtimesMastraAngularModule,
121157
];
122158

123159
export const toCockpitPath = (entry: CockpitManifestEntry): string =>
@@ -183,7 +219,14 @@ export const resolveCockpitEntry = ({
183219
export const buildNavigationTree = (
184220
manifest: CockpitManifestEntry[]
185221
): NavigationProduct[] => {
186-
const products: CockpitManifestEntry['product'][] = ['deep-agents', 'langgraph', 'ag-ui', 'render', 'chat'];
222+
const products: CockpitManifestEntry['product'][] = [
223+
'deep-agents',
224+
'langgraph',
225+
'ag-ui',
226+
'render',
227+
'chat',
228+
'runtimes',
229+
];
187230
const sections: CockpitManifestEntry['section'][] = [
188231
'getting-started',
189232
'core-capabilities',
@@ -221,14 +264,22 @@ export const getCapabilityPresentation = (
221264
};
222265
}
223266

224-
const module = capabilityModules.find(
225-
(candidate) =>
226-
candidate.manifestIdentity.product === entry.product &&
227-
candidate.manifestIdentity.section === entry.section &&
228-
candidate.manifestIdentity.topic === entry.topic &&
229-
candidate.manifestIdentity.page === entry.page &&
230-
candidate.manifestIdentity.language === entry.language
231-
);
267+
const matchesIdentity = (candidate: RegisteredCapabilityModule): boolean =>
268+
candidate.manifestIdentity.product === entry.product &&
269+
candidate.manifestIdentity.section === entry.section &&
270+
candidate.manifestIdentity.topic === entry.topic &&
271+
candidate.manifestIdentity.page === entry.page;
272+
273+
// Prefer the module whose lane matches the requested language. Fall back to
274+
// the topic's only module when no lane matches: a topic with no Python lane
275+
// (runtimes/mastra) still resolves to its real assets instead of silently
276+
// falling through to the manifest's generic, non-existent Python paths.
277+
const module =
278+
capabilityModules.find(
279+
(candidate) =>
280+
matchesIdentity(candidate) &&
281+
candidate.manifestIdentity.language === entry.language
282+
) ?? capabilityModules.find(matchesIdentity);
232283

233284
return {
234285
kind: 'capability',

apps/website/content/docs/choosing-an-adapter/index.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,9 @@ AWS Strands and Microsoft Agent Framework both read the protocol-standard top-le
123123
The LangGraph bridge reads `forwardedProps.command.resume`.
124124
You pass one neutral `submit({ resume })`, and the adapter derives the wire shape from how the interrupt arrived.
125125

126-
**The Mastra row was measured, but not in the hosted demo.**
126+
**The Mastra row is hosted on its own lane.**
127127
Its cells come from a real Mastra server driven with live model calls, and its transcripts are committed and replayed like the others.
128-
Unlike the Strands and Microsoft Agent Framework rows, it is not yet running in the hosted demo deployment.
128+
Unlike the Strands and Microsoft Agent Framework rows, it is not served by the shared FastAPI deployment: upstream ships no plain AG-UI HTTP endpoint, so its backend is a separate Node service.
129129

130130
**Subagents are red for every third-party runtime, and none of those reds are a bug.**
131131
The three runtimes do not fail to implement one thing; they model delegation three different ways.

0 commit comments

Comments
 (0)