Skip to content

Commit 39775da

Browse files
bloveclaude
andauthored
feat(cockpit): ag-ui/json-render — AG-UI shared-state generative UI (#608)
* docs(cockpit): spec for AG-UI backend-spec examples (json-render + a2ui) PR-B of the generative-UI effort: two cockpit examples demonstrating backend-sent UI specs over AG-UI — ag-ui/json-render (agent emits a json-render spec) and ag-ui/a2ui (agent emits an A2UI envelope). Faithful ports of the chat/generative-ui and chat/a2ui LangGraph examples onto the ag-ui-langgraph runtime, same airline domains. Purely example work: the content classifier is adapter-agnostic and the AG-UI reducer preserves raw content, so no chat-lib or adapter changes. One spec, two PRs (json-render first). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(cockpit): revise AG-UI spec-examples spec for idiomatic state delivery Research found json-render's dashboard data (custom get_stream_writer events) does not survive ag-ui-langgraph. Revise PR-B1 to deliver data as AG-UI agent shared state (STATE_SNAPSHOT/STATE_DELTA): backend stores data in graph state (output-schema-visible); add one small runtime-neutral chat-lib effect syncing agent.state() into the render store. Makes ag-ui/json-render the cockpit's first AG-UI shared-state demo. a2ui (PR-B2) unaffected (data rides in the envelope content). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(cockpit): implementation plan for ag-ui/json-render (PR-B1) Vertical-slice-first plan: chat-lib state-sync effect, a minimal end-to-end slice proving the STATE_SNAPSHOT data path, then the full generative-ui graph port (data in graph state), 6 dashboard views, full e2e fixtures, and registry/manifest wiring. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(chat): sync agent state signal into the render store Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(cockpit): ag-ui/json-render vertical slice (state-bound render proof) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(cockpit): port full dashboard graph to ag-ui/json-render (data in state) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(cockpit): full dashboard views for ag-ui/json-render Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(cockpit): full ag-ui/json-render e2e (state-bound dashboard) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(cockpit): register ag-ui/json-render capability (route, manifest) 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 521168f commit 39775da

49 files changed

Lines changed: 4371 additions & 2 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/cockpit/scripts/capability-registry.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export const capabilities: readonly Capability[] = [
5353
{ id: 'ag-ui-interrupts', product: 'ag-ui', topic: 'interrupts', angularProject: 'cockpit-ag-ui-interrupts-angular', port: 4320, pythonPort: 5320, pythonDir: 'cockpit/ag-ui/interrupts/python' },
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' },
56+
{ 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' },
5657
] as const;
5758

5859
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
@@ -14,6 +14,7 @@ import { langgraphDeploymentRuntimePythonModule } from '../../../../cockpit/lang
1414
import { agUiInterruptsPythonModule } from '../../../../cockpit/ag-ui/interrupts/python/src/index';
1515
import { agUiStreamingPythonModule } from '../../../../cockpit/ag-ui/streaming/python/src/index';
1616
import { agUiToolViewsPythonModule } from '../../../../cockpit/ag-ui/tool-views/python/src/index';
17+
import { agUiJsonRenderPythonModule } from '../../../../cockpit/ag-ui/json-render/python/src/index';
1718
import { deepAgentsMemoryPythonModule } from '../../../../cockpit/deep-agents/memory/python/src/index';
1819
import { deepAgentsPlanningPythonModule } from '../../../../cockpit/deep-agents/planning/python/src/index';
1920
import { deepAgentsFilesystemPythonModule } from '../../../../cockpit/deep-agents/filesystem/python/src/index';
@@ -87,6 +88,7 @@ const capabilityModules = [
8788
agUiInterruptsPythonModule,
8889
agUiStreamingPythonModule,
8990
agUiToolViewsPythonModule,
91+
agUiJsonRenderPythonModule,
9092
deepAgentsMemoryPythonModule,
9193
deepAgentsPlanningPythonModule,
9294
deepAgentsFilesystemPythonModule,
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"fixtures": [
3+
{
4+
"match": {
5+
"userMessage": "Show me a dashboard of airline operations.",
6+
"hasToolResult": true
7+
},
8+
"response": {
9+
"content": "Here's your airline operations dashboard with live KPI data."
10+
}
11+
},
12+
{
13+
"match": {
14+
"userMessage": "Show me a dashboard of airline operations."
15+
},
16+
"response": {
17+
"toolCalls": [
18+
{
19+
"name": "render_spec",
20+
"arguments": "{\"elements\":{\"root\":{\"type\":\"dashboard_grid\",\"children\":[\"stats_row\",\"charts_row\",\"table_section\"]},\"stats_row\":{\"type\":\"container\",\"props\":{\"direction\":\"row\"},\"children\":[\"on_time_card\",\"flights_card\",\"delay_card\",\"load_card\"]},\"on_time_card\":{\"type\":\"stat_card\",\"props\":{\"label\":\"On-time %\",\"value\":{\"$state\":\"/on_time/value\"},\"delta\":{\"$state\":\"/on_time/delta\"}}},\"flights_card\":{\"type\":\"stat_card\",\"props\":{\"label\":\"Flights Today\",\"value\":{\"$state\":\"/flights_today/value\"},\"delta\":{\"$state\":\"/flights_today/delta\"}}},\"delay_card\":{\"type\":\"stat_card\",\"props\":{\"label\":\"Avg Delay (min)\",\"value\":{\"$state\":\"/avg_delay/value\"},\"delta\":{\"$state\":\"/avg_delay/delta\"}}},\"load_card\":{\"type\":\"stat_card\",\"props\":{\"label\":\"Load Factor\",\"value\":{\"$state\":\"/load_factor/value\"},\"delta\":{\"$state\":\"/load_factor/delta\"}}},\"charts_row\":{\"type\":\"container\",\"props\":{\"direction\":\"row\"},\"children\":[\"trend_chart\",\"airline_chart\"]},\"trend_chart\":{\"type\":\"line_chart\",\"props\":{\"title\":\"On-time % Trend\",\"data\":{\"$state\":\"/on_time_trend\"},\"xKey\":\"month\",\"yKey\":\"on_time_pct\"}},\"airline_chart\":{\"type\":\"bar_chart\",\"props\":{\"title\":\"Flights by Airline (Daily)\",\"data\":{\"$state\":\"/flights_by_airline\"},\"labelKey\":\"airline\",\"valueKey\":\"count\"}},\"table_section\":{\"type\":\"data_grid\",\"props\":{\"title\":\"Recent Disruptions\",\"rows\":{\"$state\":\"/recent_disruptions\"},\"columns\":[\"flight_number\",\"type\",\"minutes\",\"route\",\"date\"]}}},\"root\":\"root\"}",
21+
"id": "call_render_spec_001"
22+
},
23+
{
24+
"name": "query_airline_kpis",
25+
"arguments": "{}",
26+
"id": "call_query_kpis_001"
27+
},
28+
{
29+
"name": "query_on_time_trend",
30+
"arguments": "{\"months\":12}",
31+
"id": "call_query_trend_001"
32+
},
33+
{
34+
"name": "query_flights_by_airline",
35+
"arguments": "{}",
36+
"id": "call_query_airlines_001"
37+
},
38+
{
39+
"name": "query_recent_disruptions",
40+
"arguments": "{\"limit\":5}",
41+
"id": "call_query_disruptions_001"
42+
}
43+
]
44+
}
45+
}
46+
]
47+
}
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-json-render-angular');
7+
8+
export default createAgUiGlobalSetup({
9+
pythonCwd: 'cockpit/ag-ui/json-render/python',
10+
backendPort: ports.langgraph,
11+
angularProject: 'cockpit-ag-ui-json-render-angular',
12+
angularPort: ports.angular,
13+
fixturesDir: resolve(__dirname, 'fixtures'),
14+
});
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 { test, expect } from '@playwright/test';
3+
import { submitAndWaitForResponse } from '@threadplane-internal/e2e-harness';
4+
5+
test('json-render: dashboard renders with STATE_SNAPSHOT-bound KPI values', async ({ page }) => {
6+
await submitAndWaitForResponse(page, 'Show me a dashboard of airline operations.');
7+
// Spec content mounts the GenUI tree; the KPI numbers arrive via
8+
// STATE_SNAPSHOT (graph state → agent.state() → render store).
9+
await expect(page.locator('chat-generative-ui').first()).toBeVisible({ timeout: 30000 });
10+
await expect(page.locator('chat-generative-ui')).not.toHaveCount(0);
11+
// At least one stat card shows a non-skeleton value (proves the data path).
12+
await expect(page.locator('app-stat-card .stat-card__value').first()).toBeVisible({ timeout: 30000 });
13+
await expect(page.locator('app-stat-card .stat-card__value').first()).not.toBeEmpty();
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/json-render/angular/e2e/fixtures/json-render.json.
4+
//
5+
// Prerequisites:
6+
// 1. Start the uvicorn backend in record mode (OPENAI_API_KEY set, no aimock):
7+
// cd cockpit/ag-ui/json-render/python && uv run uvicorn src.server:app --port 5323
8+
// 2. Start the Angular dev server:
9+
// npx nx serve cockpit-ag-ui-json-render-angular
10+
// 3. Run this harness via:
11+
// npx playwright test --config cockpit/ag-ui/json-render/angular/e2e/playwright.config.ts \
12+
// manual/json-render.manual.ts --headed
13+
import { expect, test } from '@playwright/test';
14+
15+
test.describe('AG-UI JSON Render Example', () => {
16+
test.beforeEach(async ({ page }) => {
17+
await page.goto('http://localhost:4323');
18+
await page.waitForSelector('app-json-render', { state: 'attached' });
19+
});
20+
21+
test('renders the airline dashboard for a dashboard request', async ({ page }) => {
22+
await page.fill('textarea[name="messageText"]', 'Show me a dashboard of airline operations.');
23+
await page.click('button[type="submit"]');
24+
await expect(page.locator('app-stat-card')).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-json-render-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-json-render-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-json-render-angular",
3+
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
4+
"sourceRoot": "cockpit/ag-ui/json-render/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/json-render/angular",
13+
"browser": ""
14+
},
15+
"browser": "cockpit/ag-ui/json-render/angular/src/main.ts",
16+
"tsConfig": "cockpit/ag-ui/json-render/angular/tsconfig.app.json",
17+
"styles": ["cockpit/ag-ui/json-render/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/json-render/angular/src/environments/environment.ts",
34+
"with": "cockpit/ag-ui/json-render/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/json-render/angular/src/environments/environment.ts",
45+
"with": "cockpit/ag-ui/json-render/angular/src/environments/environment.development.ts"
46+
}
47+
],
48+
"browser": "cockpit/ag-ui/json-render/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-json-render-angular:build:production" },
58+
"development": { "buildTarget": "cockpit-ag-ui-json-render-angular:build:development" },
59+
"cockpit": { "buildTarget": "cockpit-ag-ui-json-render-angular:build:cockpit" }
60+
},
61+
"defaultConfiguration": "development",
62+
"options": {
63+
"proxyConfig": "cockpit/ag-ui/json-render/angular/proxy.conf.mjs"
64+
}
65+
},
66+
"smoke": {
67+
"executor": "nx:run-commands",
68+
"options": {
69+
"cwd": "cockpit/ag-ui/json-render/angular",
70+
"command": "npx tsx -e \"import { agUiJsonRenderAngularModule } from './src/index.ts'; const module = agUiJsonRenderAngularModule; if (module.id !== 'ag-ui-json-render-angular' || module.title !== 'AG-UI JSON Render (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/json-render/angular/e2e/playwright.config.ts"
77+
}
78+
}
79+
},
80+
"tags": ["scope:cockpit-e2e", "scope:cockpit-examples"]
81+
}

0 commit comments

Comments
 (0)