Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions docs/AGUI_INTEGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# AG-UI Protocol Integration

Guide for using the AG-UI (Agent-User Interaction) protocol patterns in FAST.

---

## Overview

[AG-UI](https://docs.ag-ui.com/concepts/overview) is an open protocol that defines a standard SSE event format for agent-to-frontend communication. Instead of each framework emitting its own event schema (Strands events, LangChain message chunks, etc.), AG-UI provides a unified event vocabulary: `TEXT_MESSAGE_CONTENT`, `TOOL_CALL_START`, `TOOL_CALL_ARGS`, `TOOL_CALL_RESULT`, `RUN_FINISHED`, and so on.

FAST includes two AG-UI agent patterns:

| Pattern | Framework | Location |
|---------|-----------|----------|
| `agui-strands-agent` | Strands + `ag-ui-strands` | `agent_patterns/agui-strands-agent/` |
| `agui-langgraph-agent` | LangGraph + `copilotkit` | `agent_patterns/agui-langgraph-agent/` |

Both patterns use `BedrockAgentCoreApp` as the entrypoint (same as the HTTP patterns), which means AgentCore Runtime headers (WorkloadAccessToken, Authorization, Session-Id) are available for Gateway auth, Memory, and secure user identity extraction.

---

## How It Works

### Architecture

```
Frontend (Amplify)
│ POST /invocations (AG-UI RunAgentInput payload)
AgentCore Runtime
│ Proxies request to container port 8080
│ Injects: WorkloadAccessToken, Authorization, Session-Id headers
Agent Container
│ BedrockAgentCoreApp reads headers → sets ContextVars
│ @entrypoint handler creates agent, runs it
AG-UI Wrapper (StrandsAgent / LangGraphAGUIAgent)
│ Translates framework events → AG-UI SSE events
Frontend Parser (parsers/agui.ts)
│ Maps AG-UI events → StreamEvent types
ChatInterface.tsx renders messages
```

### Request Flow

1. The frontend sends an AG-UI `RunAgentInput` payload (with `threadId`, `messages`, `runId`)
2. AgentCore Runtime proxies the request, injecting auth headers
3. `BedrockAgentCoreApp` reads headers and populates `BedrockAgentCoreContext` (ContextVars)
4. The `@entrypoint` handler extracts user identity from the JWT, creates the agent with Memory and Gateway tools
5. The AG-UI wrapper translates framework streaming events into AG-UI SSE events
6. The frontend `parseAguiChunk` parser maps AG-UI events to the shared `StreamEvent` types

### AG-UI vs HTTP Protocol on AgentCore Runtime

AgentCore Runtime supports both `HTTP` and `AGUI` server protocols. The difference is minimal: with `AGUI`, platform-level errors are returned as AG-UI-compliant `RUN_ERROR` events in the SSE stream (HTTP 200) instead of HTTP error codes. Everything else — auth, session headers, payload passthrough — is identical.

The AG-UI patterns in FAST deploy with `HTTP` protocol, which works correctly because the agent container handles AG-UI event formatting internally.

---

## Agent Patterns

### AG-UI Strands (`agui-strands-agent`)

**Location**: `agent_patterns/agui-strands-agent/`

Uses `ag-ui-strands` (`StrandsAgent`) to wrap a Strands `Agent`. The agent is created per-request inside the `@entrypoint` handler, ensuring each request gets a fresh `Agent` with the correct `session_manager` and fresh MCP client connections.

**Includes**: AgentCore Memory, Gateway MCP tools, Code Interpreter, AG-UI SSE streaming.

### AG-UI LangGraph (`agui-langgraph-agent`)

**Location**: `agent_patterns/agui-langgraph-agent/`

Uses the `copilotkit` python library (`LangGraphAGUIAgent`) to wrap a LangGraph compiled graph. Uses `ActorAwareLangGraphAgent`, a subclass that rebuilds the graph per-request to ensure fresh Gateway MCP tool connections with valid tokens.

**Includes**: AgentCore Memory (checkpointer), Gateway MCP tools, Code Interpreter, CopilotKit middleware, AG-UI SSE streaming.

---

## Frontend

### Parser Auto-Selection

The AG-UI parser is automatically selected based on the pattern name prefix. Any pattern starting with `agui-` uses the AG-UI parser (`parsers/agui.ts`). Unlike the HTTP patterns — which each require a framework-specific parser (Strands, LangGraph, Claude) to handle their different streaming formats — all AG-UI patterns share a single parser. This is one of the key benefits of the AG-UI protocol: the backend framework is abstracted away behind a standard event vocabulary, so the frontend doesn't need to know whether the agent uses Strands or LangGraph.

See `frontend/src/lib/agentcore-client/client.ts` for the parser selection logic and `infra-cdk/config.yaml` comments for the full prefix-to-parser mapping.

### AG-UI Payload Format

The frontend automatically sends the correct payload format based on the pattern prefix. AG-UI patterns receive a `RunAgentInput` payload (with `threadId`, `messages`, `runId`), while HTTP patterns receive the standard `{ prompt, runtimeSessionId }` format. This is handled by `AgentCoreClient.invoke()`.

---

## Deployment

Set the pattern in `infra-cdk/config.yaml`:

```yaml
backend:
pattern: agui-strands-agent # or agui-langgraph-agent
deployment_type: docker
```

No CDK changes are required. The AG-UI patterns deploy as standard HTTP containers on AgentCore Runtime.

---

## CopilotKit Integration
Comment thread
kaleko marked this conversation as resolved.

[CopilotKit](https://www.copilotkit.ai/) is a React UI library that natively understands the AG-UI protocol. While FAST's built-in frontend includes a lightweight AG-UI parser for basic chat streaming, CopilotKit provides a richer set of capabilities for building agent-powered applications. Fullstack FAST applications with deeper CopilotKit integration can be found in the FAST samples repository (coming soon).

---

## Additional Resources

- [AG-UI Protocol Documentation](https://docs.ag-ui.com/concepts/overview)
- [ag-ui-strands on PyPI](https://pypi.org/project/ag-ui-strands/)
- [CopilotKit Documentation](https://docs.copilotkit.ai/)
- [Strands AG-UI Integration Guide](https://strandsagents.com/docs/community/integrations/ag-ui/)
32 changes: 16 additions & 16 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 28 additions & 10 deletions frontend/src/lib/agentcore-client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,29 @@ import type { AgentCoreConfig, AgentPattern, ChunkParser, StreamCallback } from
import { parseStrandsChunk } from "./parsers/strands"
import { parseLanggraphChunk } from "./parsers/langgraph"
import { parseClaudeAgentSdkChunk } from "./parsers/claude-agent-sdk"
import { parseAguiChunk } from "./parsers/agui"
import { readSSEStream } from "./utils/sse"

const PARSERS: Record<AgentPattern, ChunkParser> = {
"strands-single-agent": parseStrandsChunk,
"langgraph-single-agent": parseLanggraphChunk,
"claude-agent-sdk-single-agent": parseClaudeAgentSdkChunk,
"claude-agent-sdk-multi-agent": parseClaudeAgentSdkChunk,
/** Resolve parser from pattern prefix. Defaults to strands parser. */
function getParser(pattern: AgentPattern): ChunkParser {
if (pattern.startsWith("agui-")) return parseAguiChunk
if (pattern.startsWith("langgraph-")) return parseLanggraphChunk
if (pattern.startsWith("claude-")) return parseClaudeAgentSdkChunk
if (pattern.startsWith("strands-")) return parseStrandsChunk
return parseStrandsChunk
}

export class AgentCoreClient {
private runtimeArn: string
private region: string
private pattern: AgentPattern
private parser: ChunkParser

constructor(config: AgentCoreConfig) {
this.runtimeArn = config.runtimeArn
this.region = config.region ?? "us-east-1"
this.parser = PARSERS[config.pattern]
this.pattern = config.pattern
this.parser = getParser(config.pattern)
}

generateSessionId(): string {
Expand All @@ -44,6 +49,22 @@ export class AgentCoreClient {

const traceId = `1-${Math.floor(Date.now() / 1000).toString(16)}-${crypto.randomUUID()}`

// Build payload based on pattern — AG-UI protocol expects a different format
const body = this.pattern.startsWith("agui-")
? {
threadId: sessionId,
runId: crypto.randomUUID(),
messages: [{ id: crypto.randomUUID(), role: "user", content: query }],
state: {},
tools: [],
context: [],
forwardedProps: {},
}
: {
prompt: query,
runtimeSessionId: sessionId,
}

// User identity is extracted server-side from the validated JWT token
// (Authorization header), not sent in the payload body. This prevents
// impersonation via prompt injection.
Expand All @@ -55,10 +76,7 @@ export class AgentCoreClient {
"Content-Type": "application/json",
"X-Amzn-Bedrock-AgentCore-Runtime-Session-Id": sessionId,
},
body: JSON.stringify({
prompt: query,
runtimeSessionId: sessionId,
}),
body: JSON.stringify(body),
})

if (!response.ok) {
Expand Down
81 changes: 81 additions & 0 deletions frontend/src/lib/agentcore-client/parsers/agui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import type { ChunkParser } from "../types"

/**
* Parses SSE chunks from AG-UI (ag-ui-strands) agents.
*
* AG-UI events arrive as `data: <JSON>` where each JSON object has a `type` field:
* RUN_STARTED, STATE_SNAPSHOT, TEXT_MESSAGE_START, TEXT_MESSAGE_CONTENT,
* TEXT_MESSAGE_END, TOOL_CALL_START, TOOL_CALL_ARGS, TOOL_CALL_END,
* TOOL_CALL_RESULT, RUN_FINISHED
*/
export const parseAguiChunk: ChunkParser = (line, callback) => {
if (!line.startsWith("data: ")) return

const data = line.substring(6).trim()
if (!data) return

try {
const json = JSON.parse(data)
const eventType: string = json.type

switch (eventType) {
case "TEXT_MESSAGE_CONTENT":
callback({ type: "text", content: json.delta ?? "" })
break

case "TOOL_CALL_START":
callback({
type: "tool_use_start",
toolUseId: json.toolCallId,
name: json.toolCallName,
})
break

case "TOOL_CALL_ARGS":
callback({
type: "tool_use_delta",
toolUseId: json.toolCallId,
input: json.delta ?? "",
})
break

case "TOOL_CALL_RESULT":
callback({
type: "tool_result",
toolUseId: json.toolCallId,
result: json.content ?? "",
})
break

case "RUN_FINISHED":
callback({ type: "result", stopReason: "end_turn" })
callback({ type: "lifecycle", event: "run_finished" })
break

case "RUN_STARTED":
callback({ type: "lifecycle", event: "run_started" })
break

case "TEXT_MESSAGE_START":
callback({ type: "lifecycle", event: "message_start" })
break

case "TEXT_MESSAGE_END":
callback({ type: "lifecycle", event: "message_end" })
break

case "STATE_SNAPSHOT":
case "TOOL_CALL_END":
// Informational — no action needed
break

default:
console.debug("Unhandled AG-UI event type:", eventType)
}
} catch {
console.debug("Failed to parse AG-UI event:", data)
}
}
Loading
Loading