Skip to content

Commit d02c1f4

Browse files
bloveclaude
andauthored
feat(cockpit): ag-ui/a2ui — A2UI booking flow over AG-UI (#614)
* docs(cockpit): implementation plan for ag-ui/a2ui (PR-B2) Port of chat/a2ui booking flow onto the AG-UI runtime — purely example work (a2ui surface rides in the message-content envelope; customEvents already landed via #606). 4 tasks: backend, frontend, e2e gate, wiring. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(cockpit): ag-ui/a2ui python backend (booking flow over AG-UI) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(cockpit): ag-ui/a2ui angular app (a2uiBasicCatalog over AG-UI) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(cockpit): ag-ui/a2ui e2e (booking surface renders over AG-UI) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(cockpit): register ag-ui/a2ui capability (route, manifest) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(cockpit): correct ag-ui/a2ui guide to AG-UI adapter wiring The guide was copied from chat/a2ui and showed the LangGraph adapter (provideAgent apiUrl/assistantId, injectAgent from @threadplane/langgraph). Update the code snippets to the AG-UI form (provideAgent({url}) + injectAgent from @threadplane/ag-ui) to match the actual example. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 48a4a98 commit d02c1f4

39 files changed

Lines changed: 3610 additions & 2 deletions

apps/cockpit/scripts/capability-registry.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export const capabilities: readonly Capability[] = [
5454
{ id: 'ag-ui-streaming', product: 'ag-ui', topic: 'streaming', angularProject: 'cockpit-ag-ui-streaming-angular', port: 4321, pythonPort: 5321, pythonDir: 'cockpit/ag-ui/streaming/python' },
5555
{ id: 'ag-ui-tool-views', product: 'ag-ui', topic: 'tool-views', angularProject: 'cockpit-ag-ui-tool-views-angular', port: 4322, pythonPort: 5322, pythonDir: 'cockpit/ag-ui/tool-views/python' },
5656
{ id: 'ag-ui-json-render', product: 'ag-ui', topic: 'json-render', angularProject: 'cockpit-ag-ui-json-render-angular', port: 4323, pythonPort: 5323, pythonDir: 'cockpit/ag-ui/json-render/python' },
57+
{ id: 'ag-ui-a2ui', product: 'ag-ui', topic: 'a2ui', angularProject: 'cockpit-ag-ui-a2ui-angular', port: 4324, pythonPort: 5324, pythonDir: 'cockpit/ag-ui/a2ui/python' },
5758
] as const;
5859

5960
export function findCapability(id: string): Capability | undefined {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { agUiInterruptsPythonModule } from '../../../../cockpit/ag-ui/interrupts
1515
import { agUiStreamingPythonModule } from '../../../../cockpit/ag-ui/streaming/python/src/index';
1616
import { agUiToolViewsPythonModule } from '../../../../cockpit/ag-ui/tool-views/python/src/index';
1717
import { agUiJsonRenderPythonModule } from '../../../../cockpit/ag-ui/json-render/python/src/index';
18+
import { agUiA2uiPythonModule } from '../../../../cockpit/ag-ui/a2ui/python/src/index';
1819
import { deepAgentsMemoryPythonModule } from '../../../../cockpit/deep-agents/memory/python/src/index';
1920
import { deepAgentsPlanningPythonModule } from '../../../../cockpit/deep-agents/planning/python/src/index';
2021
import { deepAgentsFilesystemPythonModule } from '../../../../cockpit/deep-agents/filesystem/python/src/index';
@@ -89,6 +90,7 @@ const capabilityModules = [
8990
agUiStreamingPythonModule,
9091
agUiToolViewsPythonModule,
9192
agUiJsonRenderPythonModule,
93+
agUiA2uiPythonModule,
9294
deepAgentsMemoryPythonModule,
9395
deepAgentsPlanningPythonModule,
9496
deepAgentsFilesystemPythonModule,
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// SPDX-License-Identifier: MIT
2+
import { test, expect } from '@playwright/test';
3+
import { submitAndWaitForResponse } from '@threadplane-internal/e2e-harness';
4+
5+
test('a2ui: booking prompt renders an a2ui-surface over AG-UI', async ({ page }) => {
6+
await submitAndWaitForResponse(page, 'I want to fly LAX to JFK');
7+
// The BookingFormSpec → A2UI envelope arrives in AI message content;
8+
// the content classifier mounts <a2ui-surface> against a2uiBasicCatalog.
9+
await expect(page.locator('a2ui-surface').first()).toBeVisible({ timeout: 30000 });
10+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"fixtures": [
3+
{
4+
"match": {
5+
"userMessage": "I want to fly LAX to JFK"
6+
},
7+
"response": {
8+
"toolCalls": [
9+
{
10+
"name": "BookingFormSpec",
11+
"arguments": "{\"surface_id\":\"booking\",\"data_model\":{\"origin\":\"LAX\",\"dest\":\"JFK\",\"date\":\"\",\"passengers\":1,\"fare_class\":\"Economy\"},\"components\":[{\"id\":\"root\",\"component\":{\"Column\":{\"children\":{\"explicitList\":[\"card\"]}}}},{\"id\":\"card\",\"component\":{\"Card\":{\"child\":\"card_col\"}}},{\"id\":\"card_col\",\"component\":{\"Column\":{\"children\":{\"explicitList\":[\"title\",\"origin\",\"dest\",\"date\",\"passengers\",\"fare\",\"submit\"]}}}},{\"id\":\"title\",\"component\":{\"Text\":{\"text\":\"Book a flight\",\"usageHint\":\"h2\"}}},{\"id\":\"origin\",\"component\":{\"MultipleChoice\":{\"label\":\"Origin\",\"options\":[{\"label\":\"LAX\",\"value\":\"LAX\"},{\"label\":\"JFK\",\"value\":\"JFK\"},{\"label\":\"SFO\",\"value\":\"SFO\"},{\"label\":\"ORD\",\"value\":\"ORD\"},{\"label\":\"BOS\",\"value\":\"BOS\"},{\"label\":\"ATL\",\"value\":\"ATL\"},{\"label\":\"DFW\",\"value\":\"DFW\"},{\"label\":\"SEA\",\"value\":\"SEA\"},{\"label\":\"MIA\",\"value\":\"MIA\"},{\"label\":\"DEN\",\"value\":\"DEN\"}],\"selections\":{\"path\":\"/origin\"},\"maxAllowedSelections\":1}}},{\"id\":\"dest\",\"component\":{\"MultipleChoice\":{\"label\":\"Destination\",\"options\":[{\"label\":\"LAX\",\"value\":\"LAX\"},{\"label\":\"JFK\",\"value\":\"JFK\"},{\"label\":\"SFO\",\"value\":\"SFO\"},{\"label\":\"ORD\",\"value\":\"ORD\"},{\"label\":\"BOS\",\"value\":\"BOS\"},{\"label\":\"ATL\",\"value\":\"ATL\"},{\"label\":\"DFW\",\"value\":\"DFW\"},{\"label\":\"SEA\",\"value\":\"SEA\"},{\"label\":\"MIA\",\"value\":\"MIA\"},{\"label\":\"DEN\",\"value\":\"DEN\"}],\"selections\":{\"path\":\"/dest\"},\"maxAllowedSelections\":1}}},{\"id\":\"date\",\"component\":{\"TextField\":{\"label\":\"Departure date (YYYY-MM-DD)\",\"text\":{\"path\":\"/date\"},\"textFieldType\":\"date\"}}},{\"id\":\"passengers\",\"component\":{\"TextField\":{\"label\":\"Passengers\",\"text\":{\"path\":\"/passengers\"},\"textFieldType\":\"number\"}}},{\"id\":\"fare\",\"component\":{\"MultipleChoice\":{\"label\":\"Fare class\",\"options\":[{\"label\":\"Economy\",\"value\":\"Economy\"},{\"label\":\"Premium\",\"value\":\"Premium\"},{\"label\":\"Business\",\"value\":\"Business\"},{\"label\":\"First\",\"value\":\"First\"}],\"selections\":{\"path\":\"/fare_class\"},\"maxAllowedSelections\":1}}},{\"id\":\"submit_label\",\"component\":{\"Text\":{\"text\":\"Search flights\"}}},{\"id\":\"submit\",\"component\":{\"Button\":{\"child\":\"submit_label\",\"primary\":true,\"action\":{\"name\":\"bookingSubmit\",\"context\":[{\"key\":\"formId\",\"value\":\"booking\"},{\"key\":\"origin\",\"value\":{\"path\":\"/origin\"}},{\"key\":\"dest\",\"value\":{\"path\":\"/dest\"}},{\"key\":\"date\",\"value\":{\"path\":\"/date\"}},{\"key\":\"passengers\",\"value\":{\"path\":\"/passengers\"}},{\"key\":\"fare_class\",\"value\":{\"path\":\"/fare_class\"}}]}}}}]}",
12+
"id": "call_DiKu9KXnOKtbRcNBtDlXwTzx"
13+
}
14+
]
15+
}
16+
},
17+
{
18+
"match": {
19+
"userMessage": "I want to fly SFO to SEA"
20+
},
21+
"response": {
22+
"toolCalls": [
23+
{
24+
"name": "BookingFormSpec",
25+
"arguments": "{\"surface_id\":\"booking\",\"data_model\":{\"origin\":\"SFO\",\"dest\":\"SEA\",\"date\":\"\",\"passengers\":1,\"fare_class\":\"Economy\"},\"components\":[{\"id\":\"root\",\"component\":{\"Column\":{\"children\":{\"explicitList\":[\"card\"]}}}},{\"id\":\"card\",\"component\":{\"Card\":{\"child\":\"card_col\"}}},{\"id\":\"card_col\",\"component\":{\"Column\":{\"children\":{\"explicitList\":[\"title\",\"origin\",\"dest\",\"date\",\"passengers\",\"fare\",\"submit\"]}}}},{\"id\":\"title\",\"component\":{\"Text\":{\"text\":\"Book a flight\",\"usageHint\":\"h2\"}}},{\"id\":\"origin\",\"component\":{\"MultipleChoice\":{\"label\":\"Origin\",\"options\":[{\"label\":\"LAX\",\"value\":\"LAX\"},{\"label\":\"JFK\",\"value\":\"JFK\"},{\"label\":\"SFO\",\"value\":\"SFO\"},{\"label\":\"ORD\",\"value\":\"ORD\"},{\"label\":\"BOS\",\"value\":\"BOS\"},{\"label\":\"ATL\",\"value\":\"ATL\"},{\"label\":\"DFW\",\"value\":\"DFW\"},{\"label\":\"SEA\",\"value\":\"SEA\"},{\"label\":\"MIA\",\"value\":\"MIA\"},{\"label\":\"DEN\",\"value\":\"DEN\"}],\"selections\":{\"path\":\"/origin\"},\"maxAllowedSelections\":1}}},{\"id\":\"dest\",\"component\":{\"MultipleChoice\":{\"label\":\"Destination\",\"options\":[{\"label\":\"LAX\",\"value\":\"LAX\"},{\"label\":\"JFK\",\"value\":\"JFK\"},{\"label\":\"SFO\",\"value\":\"SFO\"},{\"label\":\"ORD\",\"value\":\"ORD\"},{\"label\":\"BOS\",\"value\":\"BOS\"},{\"label\":\"ATL\",\"value\":\"ATL\"},{\"label\":\"DFW\",\"value\":\"DFW\"},{\"label\":\"SEA\",\"value\":\"SEA\"},{\"label\":\"MIA\",\"value\":\"MIA\"},{\"label\":\"DEN\",\"value\":\"DEN\"}],\"selections\":{\"path\":\"/dest\"},\"maxAllowedSelections\":1}}},{\"id\":\"date\",\"component\":{\"TextField\":{\"label\":\"Departure date (YYYY-MM-DD)\",\"text\":{\"path\":\"/date\"},\"textFieldType\":\"date\"}}},{\"id\":\"passengers\",\"component\":{\"TextField\":{\"label\":\"Passengers\",\"text\":{\"path\":\"/passengers\"},\"textFieldType\":\"number\"}}},{\"id\":\"fare\",\"component\":{\"MultipleChoice\":{\"label\":\"Fare class\",\"options\":[{\"label\":\"Economy\",\"value\":\"Economy\"},{\"label\":\"Premium\",\"value\":\"Premium\"},{\"label\":\"Business\",\"value\":\"Business\"},{\"label\":\"First\",\"value\":\"First\"}],\"selections\":{\"path\":\"/fare_class\"},\"maxAllowedSelections\":1}}},{\"id\":\"submit\",\"component\":{\"Button\":{\"child\":\"submit_label\",\"primary\":true,\"action\":{\"name\":\"bookingSubmit\",\"context\":[{\"key\":\"formId\",\"value\":\"booking\"},{\"key\":\"origin\",\"value\":{\"path\":\"/origin\"}},{\"key\":\"dest\",\"value\":{\"path\":\"/dest\"}},{\"key\":\"date\",\"value\":{\"path\":\"/date\"}},{\"key\":\"passengers\",\"value\":{\"path\":\"/passengers\"}},{\"key\":\"fare_class\",\"value\":{\"path\":\"/fare_class\"}}]}}}},{\"id\":\"submit_label\",\"component\":{\"Text\":{\"text\":\"Search flights\"}}}]}",
26+
"id": "call_Miv5QrUKT95KeUOO7xzPGSEl"
27+
}
28+
]
29+
}
30+
}
31+
]
32+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// SPDX-License-Identifier: MIT
2+
import { resolve } from 'node:path';
3+
import { portsFor } from '../../../../../cockpit/ports.mjs';
4+
import { createAgUiGlobalSetup } from '@threadplane-internal/e2e-harness';
5+
6+
const ports = portsFor('cockpit-ag-ui-a2ui-angular');
7+
8+
export default createAgUiGlobalSetup({
9+
pythonCwd: 'cockpit/ag-ui/a2ui/python',
10+
backendPort: ports.langgraph,
11+
angularProject: 'cockpit-ag-ui-a2ui-angular',
12+
angularPort: ports.angular,
13+
fixturesDir: resolve(__dirname, 'fixtures'),
14+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// SPDX-License-Identifier: MIT
2+
// Manual record-mode harness. Run against a live OpenAI key to capture new
3+
// fixture entries into cockpit/ag-ui/a2ui/angular/e2e/fixtures/a2ui.json.
4+
//
5+
// Prerequisites:
6+
// 1. Start the uvicorn backend in record mode (OPENAI_API_KEY set, no aimock):
7+
// cd cockpit/ag-ui/a2ui/python && uv run uvicorn src.server:app --port 5324
8+
// 2. Start the Angular dev server:
9+
// npx nx serve cockpit-ag-ui-a2ui-angular
10+
// 3. Run this harness via:
11+
// npx playwright test --config cockpit/ag-ui/a2ui/angular/e2e/playwright.config.ts \
12+
// manual/a2ui.manual.ts --headed
13+
import { expect, test } from '@playwright/test';
14+
15+
test.describe('AG-UI A2UI Example', () => {
16+
test.beforeEach(async ({ page }) => {
17+
await page.goto('http://localhost:4324');
18+
await page.waitForSelector('app-a2ui', { state: 'attached', timeout: 10000 });
19+
});
20+
21+
test('renders the a2ui surface for a booking request', async ({ page }) => {
22+
await page.fill('textarea[name="messageText"]', 'I want to fly LAX to JFK');
23+
await page.click('button[type="submit"]');
24+
await expect(page.locator('a2ui-surface')).toBeVisible({ timeout: 30000 });
25+
});
26+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// SPDX-License-Identifier: MIT
2+
import { defineConfig, devices } from '@playwright/test';
3+
import { portsFor } from '../../../../../cockpit/ports.mjs';
4+
5+
const { angular: angularPort } = portsFor('cockpit-ag-ui-a2ui-angular');
6+
7+
export default defineConfig({
8+
testDir: '.',
9+
testMatch: '**/*.spec.ts',
10+
fullyParallel: false,
11+
workers: 1,
12+
retries: process.env.CI ? 2 : 0,
13+
reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list',
14+
use: {
15+
baseURL: `http://localhost:${angularPort}`,
16+
trace: 'retain-on-failure',
17+
},
18+
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
19+
globalSetup: './global-setup-impl.ts',
20+
globalTeardown: require.resolve('../../../../../libs/e2e-harness/src/global-teardown'),
21+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2022",
4+
"module": "ES2022",
5+
"moduleResolution": "Bundler",
6+
"esModuleInterop": true,
7+
"strict": true,
8+
"skipLibCheck": true,
9+
"noEmit": true,
10+
"types": [
11+
"node"
12+
],
13+
"baseUrl": "../../../../..",
14+
"paths": {
15+
"@threadplane-internal/e2e-harness": [
16+
"libs/e2e-harness/src/index.ts"
17+
],
18+
"@threadplane-internal/e2e-harness/global-teardown": [
19+
"libs/e2e-harness/src/global-teardown.ts"
20+
]
21+
},
22+
"allowJs": true
23+
},
24+
"include": [
25+
"**/*.ts"
26+
],
27+
"exclude": [
28+
"node_modules",
29+
"test-results",
30+
"playwright-report"
31+
]
32+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "@threadplane/cockpit-ag-ui-a2ui-angular",
3+
"private": true,
4+
"version": "0.0.1",
5+
"peerDependencies": {
6+
"@threadplane/ag-ui": "*",
7+
"@threadplane/chat": "*"
8+
},
9+
"license": "MIT",
10+
"sideEffects": false
11+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
{
2+
"name": "cockpit-ag-ui-a2ui-angular",
3+
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
4+
"sourceRoot": "cockpit/ag-ui/a2ui/angular/src",
5+
"projectType": "application",
6+
"targets": {
7+
"build": {
8+
"executor": "@angular/build:application",
9+
"outputs": ["{options.outputPath.base}"],
10+
"options": {
11+
"outputPath": {
12+
"base": "dist/cockpit/ag-ui/a2ui/angular",
13+
"browser": ""
14+
},
15+
"browser": "cockpit/ag-ui/a2ui/angular/src/main.ts",
16+
"tsConfig": "cockpit/ag-ui/a2ui/angular/tsconfig.app.json",
17+
"styles": ["cockpit/ag-ui/a2ui/angular/src/styles.css"]
18+
},
19+
"configurations": {
20+
"production": {
21+
"budgets": [
22+
{ "type": "initial", "maximumWarning": "1mb", "maximumError": "1.5mb" },
23+
{ "type": "anyComponentStyle", "maximumWarning": "10kb", "maximumError": "16kb" }
24+
],
25+
"outputHashing": "none"
26+
},
27+
"development": {
28+
"optimization": false,
29+
"extractLicenses": false,
30+
"sourceMap": true,
31+
"fileReplacements": [
32+
{
33+
"replace": "cockpit/ag-ui/a2ui/angular/src/environments/environment.ts",
34+
"with": "cockpit/ag-ui/a2ui/angular/src/environments/environment.development.ts"
35+
}
36+
]
37+
},
38+
"cockpit": {
39+
"optimization": false,
40+
"extractLicenses": false,
41+
"sourceMap": true,
42+
"fileReplacements": [
43+
{
44+
"replace": "cockpit/ag-ui/a2ui/angular/src/environments/environment.ts",
45+
"with": "cockpit/ag-ui/a2ui/angular/src/environments/environment.development.ts"
46+
}
47+
],
48+
"browser": "cockpit/ag-ui/a2ui/angular/src/main.cockpit.ts"
49+
}
50+
},
51+
"defaultConfiguration": "production"
52+
},
53+
"serve": {
54+
"continuous": true,
55+
"executor": "@angular/build:dev-server",
56+
"configurations": {
57+
"production": { "buildTarget": "cockpit-ag-ui-a2ui-angular:build:production" },
58+
"development": { "buildTarget": "cockpit-ag-ui-a2ui-angular:build:development" },
59+
"cockpit": { "buildTarget": "cockpit-ag-ui-a2ui-angular:build:cockpit" }
60+
},
61+
"defaultConfiguration": "development",
62+
"options": {
63+
"proxyConfig": "cockpit/ag-ui/a2ui/angular/proxy.conf.mjs"
64+
}
65+
},
66+
"smoke": {
67+
"executor": "nx:run-commands",
68+
"options": {
69+
"cwd": "cockpit/ag-ui/a2ui/angular",
70+
"command": "npx tsx -e \"import { agUiA2uiAngularModule } from './src/index.ts'; const module = agUiA2uiAngularModule; if (module.id !== 'ag-ui-a2ui-angular' || module.title !== 'AG-UI A2UI (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\""
71+
}
72+
},
73+
"e2e": {
74+
"executor": "@nx/playwright:playwright",
75+
"options": {
76+
"config": "cockpit/ag-ui/a2ui/angular/e2e/playwright.config.ts"
77+
}
78+
}
79+
},
80+
"tags": ["scope:cockpit-e2e", "scope:cockpit-examples"]
81+
}

0 commit comments

Comments
 (0)