Skip to content

Commit ced23ea

Browse files
bloveclaude
andcommitted
feat(cockpit): AWS Strands runtime example (rt-strands)
Second entry on the one-capability-many-runtimes axis (cockpit/runtimes/aws-strands, /agent/aws-strands, ports 4331/5331): a genuinely non-LangGraph AG-UI backend behind the neutral Agent contract. - Strands agent on the native OpenAI provider (no AWS creds), with a server-side tool, SNAPSHOT-only shared state via per-tool ToolBehavior hooks (complete-state returns; the bridge never emits STATE_DELTA), and an interrupt tool resumed through the protocol-standard top-level resume entries keyed by interruptId. - ag-ui-strands pinned to a git ref of ag-ui-protocol/ag-ui (PyPI 0.3.0 is stale and crashes on multi-agent routes); the generator's requirements union now carries PEP 508 direct-URL requirements, and the deployment Dockerfile installs git for pip. - 'aws-strands' framework adapter in the deployment generator; drift artifacts regenerated; aimock-replay e2e for the interrupt approval flow; live interrupt round-trip verified against the Phase 1 spike's captured wire shape. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 1b89f15 commit ced23ea

54 files changed

Lines changed: 4976 additions & 10 deletions

Some content is hidden

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

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ jobs:
252252
node-version: 22
253253
cache: npm
254254
- run: npm ci
255-
- run: npx nx run-many -t smoke --projects=cockpit-ag-ui-interrupts-python,cockpit-ag-ui-streaming-python,cockpit-deep-agents-planning-python,cockpit-deep-agents-filesystem-python,cockpit-deep-agents-subagents-python,cockpit-deep-agents-memory-python,cockpit-deep-agents-skills-python,cockpit-deep-agents-sandboxes-python,cockpit-langgraph-persistence-python,cockpit-langgraph-durable-execution-python,cockpit-langgraph-streaming-python,cockpit-langgraph-interrupts-python,cockpit-langgraph-memory-python,cockpit-langgraph-subgraphs-python,cockpit-langgraph-time-travel-python,cockpit-langgraph-deployment-runtime-python,cockpit-chat-messages-python,cockpit-render-spec-rendering-python,cockpit-runtimes-microsoft-agent-framework-python --skip-nx-cache
255+
- run: npx nx run-many -t smoke --projects=cockpit-ag-ui-interrupts-python,cockpit-ag-ui-streaming-python,cockpit-deep-agents-planning-python,cockpit-deep-agents-filesystem-python,cockpit-deep-agents-subagents-python,cockpit-deep-agents-memory-python,cockpit-deep-agents-skills-python,cockpit-deep-agents-sandboxes-python,cockpit-langgraph-persistence-python,cockpit-langgraph-durable-execution-python,cockpit-langgraph-streaming-python,cockpit-langgraph-interrupts-python,cockpit-langgraph-memory-python,cockpit-langgraph-subgraphs-python,cockpit-langgraph-time-travel-python,cockpit-langgraph-deployment-runtime-python,cockpit-chat-messages-python,cockpit-render-spec-rendering-python,cockpit-runtimes-microsoft-agent-framework-python,cockpit-runtimes-aws-strands-python --skip-nx-cache
256256

257257
cockpit-deploy-smoke:
258258
name: Cockpit — deploy smoke dry-run

apps/cockpit/scripts/capability-registry.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* vs `src/agent.py` exposing `agent` for Microsoft Agent Framework), and
1111
* the FastAPI mount call. Omitted means 'langgraph'.
1212
*/
13-
export type CapabilityFramework = 'langgraph' | 'microsoft-agent-framework';
13+
export type CapabilityFramework = 'langgraph' | 'microsoft-agent-framework' | 'aws-strands';
1414

1515
export interface Capability {
1616
id: string;
@@ -79,6 +79,7 @@ export const capabilities: readonly Capability[] = [
7979
// Runtime-portability examples (one capability, many runtimes; AG-UI-served
8080
// like the ag-ui caps, but the backend is genuinely non-LangGraph)
8181
{ id: 'rt-maf', product: 'runtimes', topic: 'microsoft-agent-framework', angularProject: 'cockpit-runtimes-microsoft-agent-framework-angular', port: 4330, pythonPort: 5330, pythonDir: 'cockpit/runtimes/microsoft-agent-framework/python', framework: 'microsoft-agent-framework' },
82+
{ id: 'rt-strands', product: 'runtimes', topic: 'aws-strands', angularProject: 'cockpit-runtimes-aws-strands-angular', port: 4331, pythonPort: 5331, pythonDir: 'cockpit/runtimes/aws-strands/python', framework: 'aws-strands' },
8283
] as const;
8384

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

cockpit/ports.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export const PORTS = Object.freeze({
3333
'cockpit-ag-ui-client-tools-angular': { angular: 4325, langgraph: 5325 },
3434
'cockpit-ag-ui-subagents-angular': { angular: 4326, langgraph: 5326 },
3535
'cockpit-runtimes-microsoft-agent-framework-angular': { angular: 4330, langgraph: 5330 },
36+
'cockpit-runtimes-aws-strands-angular': { angular: 4331, langgraph: 5331 },
3637
'cockpit-chat-a2ui-angular': { angular: 4511, langgraph: 5511 },
3738
'cockpit-chat-debug-angular': { angular: 4509, langgraph: 5509 },
3839
'cockpit-chat-generative-ui-angular': { angular: 4508, langgraph: 5508 },
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// SPDX-License-Identifier: MIT
2+
import { test, expect } from '@playwright/test';
3+
4+
// Second proof outside unit tests that the neutral Agent contract's
5+
// interrupt path works against a genuinely non-LangGraph AG-UI backend: the
6+
// AWS Strands bridge signals interrupts via the protocol-standard
7+
// RUN_FINISHED outcome (never CUSTOM on_interrupt), and resume goes out as
8+
// the top-level `resume` entry array keyed by interruptId.
9+
test.describe('cockpit runtimes/aws-strands: meeting booking approval', () => {
10+
test('approval card displays the pending book_meeting call', async ({ page }) => {
11+
await page.goto('/');
12+
await page.getByText('Book the Q3 roadmap review').click();
13+
const dialog = page.locator('dialog.chat-approval-card');
14+
await expect(dialog).toBeVisible({ timeout: 30_000 });
15+
await expect(dialog).toContainText('Booking approval required');
16+
await expect(dialog).toContainText('Q3 roadmap review');
17+
await expect(dialog).toContainText('Tuesday 10:00');
18+
});
19+
20+
test('shared state panel snapshots availability and the pending booking', async ({ page }) => {
21+
await page.goto('/');
22+
await page.getByText('Book the Q3 roadmap review').click();
23+
const panel = page.getByTestId('schedule-state');
24+
// From check_availability's state_from_result snapshot.
25+
await expect(panel).toContainText('Tuesday', { timeout: 30_000 });
26+
await expect(panel).toContainText('10:00');
27+
// From book_meeting's state_from_args snapshot (fires before the interrupt).
28+
await expect(panel.getByTestId('booking-state')).toContainText('pending', { timeout: 30_000 });
29+
});
30+
31+
test('Approve resumes the run and the meeting is booked', async ({ page }) => {
32+
await page.goto('/');
33+
await page.getByText('Book the Q3 roadmap review').click();
34+
const dialog = page.locator('dialog.chat-approval-card');
35+
await expect(dialog).toBeVisible({ timeout: 30_000 });
36+
await dialog.getByRole('button', { name: 'Approve' }).click();
37+
await expect(page.getByText(/is booked for Tuesday 10:00/i)).toBeVisible({ timeout: 30_000 });
38+
});
39+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"fixtures": [
3+
{
4+
"match": { "userMessage": "roadmap review", "hasToolResult": true, "turnIndex": 2 },
5+
"response": {
6+
"content": "Your Q3 roadmap review is booked for Tuesday 10:00."
7+
}
8+
},
9+
{
10+
"match": { "userMessage": "roadmap review", "hasToolResult": true, "turnIndex": 1 },
11+
"response": {
12+
"toolCalls": [
13+
{
14+
"name": "book_meeting",
15+
"arguments": {
16+
"topic": "Q3 roadmap review",
17+
"slot": "Tuesday 10:00"
18+
}
19+
}
20+
]
21+
}
22+
},
23+
{
24+
"match": { "userMessage": "roadmap review" },
25+
"response": {
26+
"toolCalls": [
27+
{
28+
"name": "check_availability",
29+
"arguments": { "day": "Tuesday" }
30+
}
31+
]
32+
}
33+
}
34+
]
35+
}
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-runtimes-aws-strands-angular');
7+
8+
export default createAgUiGlobalSetup({
9+
pythonCwd: 'cockpit/runtimes/aws-strands/python',
10+
backendPort: ports.langgraph,
11+
angularProject: 'cockpit-runtimes-aws-strands-angular',
12+
angularPort: ports.angular,
13+
fixturesDir: resolve(__dirname, 'fixtures'),
14+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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-runtimes-aws-strands-angular');
6+
7+
8+
export default defineConfig({
9+
testDir: '.',
10+
testMatch: '**/*.spec.ts',
11+
fullyParallel: false,
12+
workers: 1,
13+
retries: process.env.CI ? 2 : 0,
14+
reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list',
15+
use: {
16+
baseURL: `http://localhost:${angularPort}`,
17+
trace: 'retain-on-failure',
18+
},
19+
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
20+
globalSetup: './global-setup-impl.ts',
21+
globalTeardown: require.resolve('../../../../../libs/e2e-harness/src/global-teardown'),
22+
});
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-runtimes-aws-strands-angular",
3+
"private": true,
4+
"version": "0.0.1",
5+
"peerDependencies": {
6+
"@threadplane/chat": "*",
7+
"@threadplane/ag-ui": "*"
8+
},
9+
"license": "MIT",
10+
"sideEffects": false
11+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
{
2+
"name": "cockpit-runtimes-aws-strands-angular",
3+
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
4+
"sourceRoot": "cockpit/runtimes/aws-strands/angular/src",
5+
"projectType": "application",
6+
"targets": {
7+
"build": {
8+
"executor": "@angular/build:application",
9+
"outputs": [
10+
"{options.outputPath.base}"
11+
],
12+
"options": {
13+
"outputPath": {
14+
"base": "dist/cockpit/runtimes/aws-strands/angular",
15+
"browser": ""
16+
},
17+
"browser": "cockpit/runtimes/aws-strands/angular/src/main.ts",
18+
"tsConfig": "cockpit/runtimes/aws-strands/angular/tsconfig.app.json",
19+
"styles": [
20+
"cockpit/runtimes/aws-strands/angular/src/styles.css"
21+
]
22+
},
23+
"configurations": {
24+
"production": {
25+
"budgets": [
26+
{
27+
"type": "initial",
28+
"maximumWarning": "1mb",
29+
"maximumError": "1.5mb"
30+
},
31+
{
32+
"type": "anyComponentStyle",
33+
"maximumWarning": "10kb",
34+
"maximumError": "16kb"
35+
}
36+
],
37+
"outputHashing": "none"
38+
},
39+
"development": {
40+
"optimization": false,
41+
"extractLicenses": false,
42+
"sourceMap": true,
43+
"fileReplacements": [
44+
{
45+
"replace": "cockpit/runtimes/aws-strands/angular/src/environments/environment.ts",
46+
"with": "cockpit/runtimes/aws-strands/angular/src/environments/environment.development.ts"
47+
}
48+
]
49+
},
50+
"cockpit": {
51+
"optimization": false,
52+
"extractLicenses": false,
53+
"sourceMap": true,
54+
"fileReplacements": [
55+
{
56+
"replace": "cockpit/runtimes/aws-strands/angular/src/environments/environment.ts",
57+
"with": "cockpit/runtimes/aws-strands/angular/src/environments/environment.development.ts"
58+
}
59+
],
60+
"browser": "cockpit/runtimes/aws-strands/angular/src/main.cockpit.ts"
61+
}
62+
},
63+
"defaultConfiguration": "production"
64+
},
65+
"serve": {
66+
"continuous": true,
67+
"executor": "@angular/build:dev-server",
68+
"configurations": {
69+
"production": {
70+
"buildTarget": "cockpit-runtimes-aws-strands-angular:build:production"
71+
},
72+
"development": {
73+
"buildTarget": "cockpit-runtimes-aws-strands-angular:build:development"
74+
},
75+
"cockpit": {
76+
"buildTarget": "cockpit-runtimes-aws-strands-angular:build:cockpit"
77+
}
78+
},
79+
"defaultConfiguration": "development",
80+
"options": {
81+
"proxyConfig": "cockpit/runtimes/aws-strands/angular/proxy.conf.mjs"
82+
}
83+
},
84+
"smoke": {
85+
"executor": "nx:run-commands",
86+
"options": {
87+
"cwd": "cockpit/runtimes/aws-strands/angular",
88+
"command": "npx tsx -e \"import { runtimesAwsStrandsAngularModule } from './src/index.ts'; const module = runtimesAwsStrandsAngularModule; if (module.id !== 'runtimes-aws-strands-angular' || module.title !== 'Runtimes — AWS Strands (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\""
89+
}
90+
},
91+
"e2e": {
92+
"executor": "@nx/playwright:playwright",
93+
"options": {
94+
"config": "cockpit/runtimes/aws-strands/angular/e2e/playwright.config.ts"
95+
}
96+
}
97+
},
98+
"tags": [
99+
"scope:cockpit-e2e",
100+
"scope:cockpit-examples"
101+
]
102+
}

0 commit comments

Comments
 (0)