Skip to content

Commit 52144dc

Browse files
authored
feat: add AG-UI (AGUI) as fourth first-class protocol mode (#858)
* feat: add AG-UI (AGUI) as fourth first-class protocol mode Add AGUI protocol support across the full CLI stack: - Schema: Add 'AGUI' to ProtocolModeSchema, PROTOCOL_FRAMEWORK_MATRIX (Strands, LangChain_LangGraph, GoogleADK), and RESERVED_PROJECT_NAMES - Types: New agui-types.ts with 27 event type enum, typed interfaces, parseAguiEvent parser, and buildAguiRunInput helper - Templates: Python AGUI agent templates for Strands (ag-ui-strands), LangGraph (ag-ui-langgraph), and GoogleADK (ag-ui-adk) frameworks - Invoke: invokeAguiRuntime with dual-stream architecture (typed events for TUI, text-only for CLI), local dev invokeAguiStreaming with RunAgentInput body, protocol dispatch in invokeForProtocol - TUI: Rich AGUI event rendering with MessagePart type (text, tool_call, reasoning, error) in InvokeScreen, AGUI placeholder text in DevScreen - Validation: Updated error messages and help text to include AGUI - Tests: 24 unit tests for parseAguiEvent/buildAguiRunInput, snapshot updates for new template files * fix: address review findings for AGUI protocol implementation HIGH fixes: - Add sessionId to AguiInvokeOptions, pass as runtimeSessionId (H-1) - Throw early for bearerToken on AGUI (not yet supported) (H-2) - Add bedrock-agentcore dep to all 3 template pyproject.toml files (H-4/5/6) - Fix LangGraph /ping to return "healthy" not "ok" (H-7) - Match TOOL_CALL_RESULT to tool_call parts by toolCallId, not position (H-12) - Add complete enum coverage to agui-types test (H-15) MEDIUM fixes: - Fix langchain version pin from 1.2.0 (nonexistent) to 0.3.0 (M-11) - Remove invalid allow_credentials=True with wildcard CORS (M-12) - Replace in-place parts mutation with immutable updates for React safety (M-5) - Surface readLoop errors in consumer generators instead of swallowing (M-1) - Disable retry once streaming starts to prevent duplicate output (M-3) - Handle TEXT_MESSAGE_CHUNK events alongside TEXT_MESSAGE_CONTENT (M-2) - Update gemini model from 2.0-flash to 2.5-flash in GoogleADK (M-8) - Add missing event type exports to barrel index.ts (M-18) LOW fixes: - Move AGUI imports to top-level in action.ts (L-1) - Gate OTEL_SDK_DISABLED on LOCAL_DEV env var in Strands template (L-9) - Add explanatory comment for LANGGRAPH_FAST_API env var (L-10) * fix: add AGUI to TUI protocol picker and dev mode dispatch - Add AGUI option to PROTOCOL_OPTIONS in generate/types.ts so users can select AGUI from the interactive create/add wizards - Add AGUI case to useDevServer.ts sendMessage dispatch so local dev TUI sends correct RunAgentInput body via invokeAguiStreaming - Add AGUI case to dev/command.tsx non-interactive dispatch so agentcore dev "prompt" uses invokeForProtocol('AGUI') * fix: A2A GoogleADK template passes model=None to Agent constructor load_model() returns None (it only sets GOOGLE_API_KEY env var as a side effect). Passing model=load_model() to Agent() results in model=None, causing the agent to either crash or use a default model. Fix: call load_model() standalone for the side effect, then pass the model ID string directly to Agent(). * chore: update protocol references to include AGUI across CLI - AddScreen description: 'HTTP, MCP, A2A' → includes AGUI - create --protocol help text: includes AGUI - JSDoc comments in agent/types.ts, templates/types.ts, agent-env.ts - codezip-dev-server comment: 'MCP/A2A' → 'MCP/A2A/AGUI' - agent-env.test.ts: add AGUI to protocol acceptance test * fix: add InvokeLogger to AGUI CLI path and improve UX polish - Add InvokeLogger to AGUI CLI invoke block (action.ts) for prompt/response logging and log file creation — parity with HTTP invoke path - Track RUN_ERROR events in textStream and return success: false when agent errors are detected - Pass sessionId and logger to invokeAguiRuntime options - Improve AGUI protocol picker description from circular 'AG-UI agent-to-user interaction protocol' to actionable 'Stream rich agent events to frontends (AG-UI)' * fix: template bugs found during deployment testing Bugs found by deploying all 3 AGUI frameworks to AWS and invoking: - Bump ag-ui-strands to >= 0.1.4 (0.1.3 crashes on strands >= 1.19.0 due to accessing removed private attr agent.state._state) - Remove parallel_tool_calls=False from LangGraph template (Bedrock rejects this OpenAI-specific parameter with ValidationException) - Remove aws-opentelemetry-distro from GoogleADK template (conflicts with google-adk >= 1.16.0 OpenTelemetry dependencies — agents using this template should set instrumentation.enableOtel: false) * fix: add ToolNode + ReAct loop to AGUI LangGraph template The AGUI LangGraph template had a single-node graph (chat → END) with no tool execution loop. When the model called add_numbers, the graph exited without executing the tool or generating a text response, producing "(no content in AGUI response)" in agentcore dev. Template fix: - Add ToolNode(tools=backend_tools) as a "tools" node - Replace set_finish_point("chat") with tools_condition conditional edge - Add edge from "tools" back to "chat" for the ReAct loop - Separate backend_tools list from frontend tools (state["tools"]) This matches the standard LangGraph ReAct pattern (agent → tools → agent → ... → END) and how the HTTP/A2A templates use create_react_agent. Dev invoke fix: - invoke-agui.ts now tracks TOOL_CALL_START/ARGS/END/RESULT events - When no text is produced but tool calls were seen, surfaces them as [Tool: name(args)] instead of generic "(no content)" message * fix: address all review findings from AG-UI protocol code review 16 issues from 4-lane parallel code review, all addressed: Critical fixes: - Strands template: use session_manager_provider from ag-ui-strands 0.1.7 instead of hardcoded "default-session"/"default-user" - Dev client: persist threadId per session for multi-turn conversations - CRLF handling: use /\r?\n/ in SSE parsers (invoke-agui + invoke.ts) - Malformed JSON no longer yielded as content (shared parser skips) - Unbounded aguiEvents array replaced with bounded cursor-based pruning Structural improvements: - Unified SSE parser (agui-parser.ts) replaces two divergent parsers in invoke-agui.ts (dev) and agentcore.ts (deployed). Net -39 LOC. - Dual-consumer support with singleConsumer mode for dev path - AguiEvent type union completed (4 missing members added) - Dynamic imports converted to static where non-intentional (AGENTS.md) Python template fixes: - LangGraph: add LangchainInstrumentor + dep, remove unused END import, MemorySaver already removed in prior commit - GoogleADK: remove dead load_model() + bedrock-agentcore dep, remove hardcoded user_id (ADK defaults to per-thread identity) - Strands: bump ag-ui-strands pin to >= 0.1.7 enableOtel plumbing: - Dockerfile CMD conditional on enableOtel (Handlebars) - enableOtel threaded through AgentRenderConfig + BaseRenderer - Import path: ProtocolModeSchema.safeParse replaces unsafe as-cast - Import path: MCP enableOtel clamped regardless of YAML value - GoogleADK uses plain opentelemetry-distro (aws-distro conflicts) DX + testing: - formatZodIssue falls back to issue.code instead of literal "undefined" - New dockerfile-render.test.ts covers both enableOtel branches - All snapshots updated * fix: add AGUI to JSON schema protocol enum The static JSON schema file used for CDK validation was not updated when AGUI was added to the Zod schema. This caused CDK synth to reject protocol: "AGUI" with a misleading validation error. * fix: restore MemorySaver in AGUI LangGraph template ag_ui_langgraph calls aget_state(config) with thread_id which requires a checkpointer. Without it, every invocation throws ValueError: No checkpointer set. The original msgpack crash only triggers with numbers exceeding 2^63 (ormsgpack limitation), not with normal large numbers. Bug bash confirmed: 325435 + 435634563456456 works correctly with MemorySaver present. * fix: address final review findings in AGUI parser - Wrap reader.releaseLock() in try/catch to prevent error masking if lock is already released (HIGH from code review) - Replace textStream! non-null assertion with runtime guard (MEDIUM from code review) * fix: use toolCallId for TOOL_CALL_RESULT matching in dev client Previously matched by activeToolName which was already reset to '' by TOOL_CALL_END. The find() never matched, falling through to the last tool call — wrong for parallel tool calls. Now matches by toolCallId which is the unique identifier AG-UI provides per tool invocation. * revert: remove manual JSON schema edit (auto-generated during release) The schemas/ directory is auto-regenerated from Zod schemas during the release workflow. AGUI is already in ProtocolModeSchema (constants.ts) and will appear in the JSON schema on next release. * fix: add configurable PORT env var to AGUI templates + update snapshots All 3 AGUI templates now read PORT from env with default 8080: uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", "8080"))) Addresses PR review comment requesting configurable port for local testing. * fix: use AG-UI in user-facing strings instead of AGUI Schema enum stays 'AGUI' (internal), but TUI display text uses 'AG-UI' which is the protocol's official name. * fix: restore credential wiring in AGUI GoogleADK template The template was missing load_model() call and bedrock-agentcore dep, so GOOGLE_API_KEY was never set from the AgentCore credential. Both dev mode and deployed agents failed with "No API key provided." * fix: convert AGUI dynamic import to static in invokeForProtocol AGENTS.md requires all imports at top of file. The dynamic import had no meaningful performance benefit — AGUI parser is ~4KB in a 2.1MB CLI.
1 parent 13f16d3 commit 52144dc

61 files changed

Lines changed: 3068 additions & 33 deletions

Some content is hidden

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

src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap

Lines changed: 858 additions & 1 deletion
Large diffs are not rendered by default.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`Dockerfile enableOtel rendering > renders opentelemetry-instrument CMD when enableOtel is true > Dockerfile-enableOtel-true 1`] = `
4+
"FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
5+
6+
ARG UV_DEFAULT_INDEX
7+
ARG UV_INDEX
8+
9+
WORKDIR /app
10+
11+
ENV UV_SYSTEM_PYTHON=1 \\
12+
UV_COMPILE_BYTECODE=1 \\
13+
UV_NO_PROGRESS=1 \\
14+
PYTHONUNBUFFERED=1 \\
15+
DOCKER_CONTAINER=1 \\
16+
UV_DEFAULT_INDEX=\${UV_DEFAULT_INDEX} \\
17+
UV_INDEX=\${UV_INDEX} \\
18+
PATH="/app/.venv/bin:$PATH"
19+
20+
RUN useradd -m -u 1000 bedrock_agentcore
21+
22+
COPY pyproject.toml uv.lock ./
23+
RUN uv sync --frozen --no-dev --no-install-project
24+
25+
COPY --chown=bedrock_agentcore:bedrock_agentcore . .
26+
RUN uv sync --frozen --no-dev
27+
28+
USER bedrock_agentcore
29+
30+
# AgentCore Runtime service contract ports
31+
# https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-service-contract.html
32+
# 8080: HTTP Mode
33+
# 8000: MCP Mode
34+
# 9000: A2A Mode
35+
EXPOSE 8080 8000 9000
36+
37+
CMD ["opentelemetry-instrument", "python", "-m", "main"]
38+
"
39+
`;
40+
41+
exports[`Dockerfile enableOtel rendering > renders plain python CMD when enableOtel is false > Dockerfile-enableOtel-false 1`] = `
42+
"FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
43+
44+
ARG UV_DEFAULT_INDEX
45+
ARG UV_INDEX
46+
47+
WORKDIR /app
48+
49+
ENV UV_SYSTEM_PYTHON=1 \\
50+
UV_COMPILE_BYTECODE=1 \\
51+
UV_NO_PROGRESS=1 \\
52+
PYTHONUNBUFFERED=1 \\
53+
DOCKER_CONTAINER=1 \\
54+
UV_DEFAULT_INDEX=\${UV_DEFAULT_INDEX} \\
55+
UV_INDEX=\${UV_INDEX} \\
56+
PATH="/app/.venv/bin:$PATH"
57+
58+
RUN useradd -m -u 1000 bedrock_agentcore
59+
60+
COPY pyproject.toml uv.lock ./
61+
RUN uv sync --frozen --no-dev --no-install-project
62+
63+
COPY --chown=bedrock_agentcore:bedrock_agentcore . .
64+
RUN uv sync --frozen --no-dev
65+
66+
USER bedrock_agentcore
67+
68+
# AgentCore Runtime service contract ports
69+
# https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-service-contract.html
70+
# 8080: HTTP Mode
71+
# 8000: MCP Mode
72+
# 9000: A2A Mode
73+
EXPOSE 8080 8000 9000
74+
75+
CMD ["python", "-m", "main"]
76+
"
77+
`;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as fs from 'fs';
2+
import Handlebars from 'handlebars';
3+
import * as path from 'path';
4+
import { describe, expect, it } from 'vitest';
5+
6+
const DOCKERFILE_PATH = path.resolve(__dirname, '..', 'container', 'python', 'Dockerfile');
7+
8+
describe('Dockerfile enableOtel rendering', () => {
9+
const template = Handlebars.compile(fs.readFileSync(DOCKERFILE_PATH, 'utf-8'));
10+
11+
it('renders opentelemetry-instrument CMD when enableOtel is true', () => {
12+
const rendered = template({ entrypoint: 'main', enableOtel: true });
13+
expect(rendered).toMatchSnapshot('Dockerfile-enableOtel-true');
14+
expect(rendered).toContain('opentelemetry-instrument');
15+
expect(rendered).not.toContain('CMD ["python", "-m"');
16+
});
17+
18+
it('renders plain python CMD when enableOtel is false', () => {
19+
const rendered = template({ entrypoint: 'main', enableOtel: false });
20+
expect(rendered).toMatchSnapshot('Dockerfile-enableOtel-false');
21+
expect(rendered).toContain('CMD ["python", "-m"');
22+
expect(rendered).not.toContain('opentelemetry-instrument');
23+
});
24+
});

src/assets/container/python/Dockerfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,8 @@ USER bedrock_agentcore
3131
# 9000: A2A Mode
3232
EXPOSE 8080 8000 9000
3333

34+
{{#if enableOtel}}
3435
CMD ["opentelemetry-instrument", "python", "-m", "{{entrypoint}}"]
36+
{{else}}
37+
CMD ["python", "-m", "{{entrypoint}}"]
38+
{{/if}}

src/assets/python/a2a/googleadk/base/main.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from bedrock_agentcore.runtime import serve_a2a
88
from model.load import load_model
99

10+
load_model() # Sets GOOGLE_API_KEY env var (returns None)
11+
1012

1113
def add_numbers(a: int, b: int) -> int:
1214
"""Return the sum of two numbers."""
@@ -73,7 +75,7 @@ def list_files(directory: str = "") -> str:
7375
"""
7476

7577
agent = Agent(
76-
model=load_model(),
78+
model="gemini-2.5-flash",
7779
name="{{ name }}",
7880
description="A helpful assistant that can use tools.",
7981
instruction=AGENT_INSTRUCTION,
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# {{ name }}
2+
3+
An AG-UI agent deployed on Amazon Bedrock AgentCore using Google ADK.
4+
5+
## Overview
6+
7+
This agent implements the AG-UI protocol using Google's Agent Development Kit, enabling rich agent-user interaction via the AG-UI event stream.
8+
9+
## Local Development
10+
11+
```bash
12+
uv sync
13+
uv run python main.py
14+
```
15+
16+
The agent starts on port 8080 and serves requests at `/invocations`.
17+
18+
## Health Check
19+
20+
```
21+
GET /ping
22+
```
23+
24+
Returns `{"status": "healthy"}`.
25+
26+
## Deploy
27+
28+
```bash
29+
agentcore deploy
30+
```
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Environment variables
2+
.env
3+
4+
# Python
5+
__pycache__/
6+
*.py[cod]
7+
*$py.class
8+
*.so
9+
.Python
10+
build/
11+
develop-eggs/
12+
dist/
13+
downloads/
14+
eggs/
15+
.eggs/
16+
lib/
17+
lib64/
18+
parts/
19+
sdist/
20+
var/
21+
wheels/
22+
*.egg-info/
23+
.installed.cfg
24+
*.egg
25+
26+
# Virtual environments
27+
.venv/
28+
venv/
29+
ENV/
30+
env/
31+
32+
# IDE
33+
.vscode/
34+
.idea/
35+
*.swp
36+
*.swo
37+
*~
38+
39+
# OS
40+
.DS_Store
41+
Thumbs.db
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import os
2+
import uvicorn
3+
from google.adk.agents import LlmAgent
4+
from ag_ui_adk import ADKAgent, AGUIToolset, create_adk_app
5+
from model.load import load_model
6+
7+
load_model()
8+
9+
agent = LlmAgent(
10+
name="{{ name }}",
11+
model="gemini-2.5-flash",
12+
instruction="You are a helpful assistant.",
13+
tools=[AGUIToolset()],
14+
)
15+
16+
adk_agent = ADKAgent(
17+
adk_agent=agent,
18+
app_name="{{ name }}",
19+
use_in_memory_services=True,
20+
)
21+
22+
app = create_adk_app(adk_agent, path="/invocations")
23+
24+
25+
@app.get("/ping")
26+
async def ping():
27+
return {"status": "healthy"}
28+
29+
30+
if __name__ == "__main__":
31+
uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", "8080")))
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Package marker
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import os
2+
from bedrock_agentcore.identity.auth import requires_api_key
3+
4+
IDENTITY_PROVIDER_NAME = "{{identityProviders.[0].name}}"
5+
IDENTITY_ENV_VAR = "{{identityProviders.[0].envVarName}}"
6+
7+
8+
@requires_api_key(provider_name=IDENTITY_PROVIDER_NAME)
9+
def _agentcore_identity_api_key_provider(api_key: str) -> str:
10+
"""Fetch API key from AgentCore Identity."""
11+
return api_key
12+
13+
14+
def _get_api_key() -> str:
15+
"""
16+
Uses AgentCore Identity for API key management in deployed environments.
17+
For local development, run via 'agentcore dev' which loads agentcore/.env.
18+
"""
19+
if os.getenv("LOCAL_DEV") == "1":
20+
api_key = os.getenv(IDENTITY_ENV_VAR)
21+
if not api_key:
22+
raise RuntimeError(
23+
f"{IDENTITY_ENV_VAR} not found. Add {IDENTITY_ENV_VAR}=your-key to .env.local"
24+
)
25+
return api_key
26+
return _agentcore_identity_api_key_provider()
27+
28+
29+
def load_model() -> None:
30+
"""
31+
Set up Gemini API key authentication.
32+
Uses AgentCore Identity for API key management in deployed environments,
33+
and falls back to .env file for local development.
34+
Sets the GOOGLE_API_KEY environment variable for the Google ADK.
35+
"""
36+
api_key = _get_api_key()
37+
# Use Google AI Studios API Key Authentication.
38+
# https://google.github.io/adk-docs/agents/models/#google-ai-studio
39+
os.environ["GOOGLE_API_KEY"] = api_key
40+
# Set to TRUE is using Google Vertex AI, Set to FALSE for Google AI Studio
41+
os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "FALSE"

0 commit comments

Comments
 (0)