Skip to content

Commit e59a01d

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 1c975c6 commit e59a01d

55 files changed

Lines changed: 4984 additions & 18 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 --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-aws-strands-python --skip-nx-cache
256256

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

.github/workflows/deploy-ag-ui.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
branches: [main]
66
paths:
77
- 'cockpit/ag-ui/**/python/**'
8+
- 'cockpit/runtimes/**/python/**'
89
- 'apps/cockpit/scripts/capability-registry.ts'
910
- 'scripts/generate-ag-ui-deployment-config.ts'
1011
- 'deployments/ag-ui-dev/**'

apps/cockpit/scripts/capability-registry.ts

Lines changed: 5 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;
@@ -76,6 +76,10 @@ export const capabilities: readonly Capability[] = [
7676
{ id: 'ag-ui-client-tools', product: 'ag-ui', topic: 'client-tools', angularProject: 'cockpit-ag-ui-client-tools-angular', port: 4325, pythonPort: 5325, pythonDir: 'cockpit/ag-ui/client-tools/python' },
7777
{ 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' },
7878
{ id: 'ag-ui-subagents', product: 'ag-ui', topic: 'subagents', angularProject: 'cockpit-ag-ui-subagents-angular', port: 4326, pythonPort: 5326, pythonDir: 'cockpit/ag-ui/subagents/python' },
79+
// Runtime-portability examples (one capability, many runtimes; AG-UI-served
80+
// like the ag-ui caps, but the backend is genuinely non-LangGraph).
81+
// Ports 4330/5330 are reserved for rt-maf (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' },
7983
] as const;
8084

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

cockpit/ports.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export const PORTS = Object.freeze({
3232
'cockpit-ag-ui-a2ui-angular': { angular: 4324, langgraph: 5324 },
3333
'cockpit-ag-ui-client-tools-angular': { angular: 4325, langgraph: 5325 },
3434
'cockpit-ag-ui-subagents-angular': { angular: 4326, langgraph: 5326 },
35+
'cockpit-runtimes-aws-strands-angular': { angular: 4331, langgraph: 5331 },
3536
'cockpit-chat-a2ui-angular': { angular: 4511, langgraph: 5511 },
3637
'cockpit-chat-debug-angular': { angular: 4509, langgraph: 5509 },
3738
'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+
}

0 commit comments

Comments
 (0)