Skip to content

Commit 28714dc

Browse files
authored
Support multiple runs in one request (ag-ui-protocol#290)
* allow overlapping events * fix message content * add CLAUDE.md * feat: export RunAgentResult type from agent module * feat: export BaseEventSchema for external usage * support multiple runs * support multiple runs * pre release * export HttpAgentConfig, RunAgentParameters * release v0.0.38-alpha.1 * add role property to legacy events * v0.0.37-alpha.0
1 parent d0d3f25 commit 28714dc

29 files changed

+5085
-793
lines changed

CLAUDE.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Common Development Commands
6+
7+
### TypeScript SDK (Main Development)
8+
```bash
9+
# Navigate to typescript-sdk directory for all TypeScript work
10+
cd typescript-sdk
11+
12+
# Install dependencies (using pnpm)
13+
pnpm install
14+
15+
# Build all packages
16+
pnpm build
17+
18+
# Run development mode
19+
pnpm dev
20+
21+
# Run linting
22+
pnpm lint
23+
24+
# Run type checking
25+
pnpm check-types
26+
27+
# Run tests
28+
pnpm test
29+
30+
# Format code
31+
pnpm format
32+
33+
# Clean build artifacts
34+
pnpm clean
35+
36+
# Full clean build
37+
pnpm build:clean
38+
```
39+
40+
### Python SDK
41+
```bash
42+
# Navigate to python-sdk directory
43+
cd python-sdk
44+
45+
# Install dependencies (using poetry)
46+
poetry install
47+
48+
# Run tests
49+
python -m unittest discover tests
50+
51+
# Build distribution
52+
poetry build
53+
```
54+
55+
### Running Specific Integration Tests
56+
```bash
57+
# For TypeScript packages/integrations
58+
cd typescript-sdk/packages/<package-name>
59+
pnpm test
60+
61+
# For running a single test file
62+
cd typescript-sdk/packages/<package-name>
63+
pnpm test -- path/to/test.spec.ts
64+
```
65+
66+
## High-Level Architecture
67+
68+
AG-UI is an event-based protocol that standardizes agent-user interactions. The codebase is organized as a monorepo with the following structure:
69+
70+
### Core Protocol Architecture
71+
- **Event-Driven Communication**: All agent-UI communication happens through typed events (BaseEvent and its subtypes)
72+
- **Transport Agnostic**: Protocol supports SSE, WebSockets, HTTP binary, and custom transports
73+
- **Observable Pattern**: Uses RxJS Observables for streaming agent responses
74+
75+
### Key Abstractions
76+
1. **AbstractAgent**: Base class that all agents must implement with a `run(input: RunAgentInput) -> Observable<BaseEvent>` method
77+
2. **HttpAgent**: Standard HTTP client supporting SSE and binary protocols for connecting to agent endpoints
78+
3. **Event Types**: Lifecycle events (RUN_STARTED/FINISHED), message events (TEXT_MESSAGE_*), tool events (TOOL_CALL_*), and state management events (STATE_SNAPSHOT/DELTA)
79+
80+
### Repository Structure
81+
- `/typescript-sdk/`: Main TypeScript implementation
82+
- `/packages/`: Core protocol packages (@ag-ui/core, @ag-ui/client, @ag-ui/encoder, @ag-ui/proto)
83+
- `/integrations/`: Framework integrations (langgraph, mastra, crewai, etc.)
84+
- `/apps/`: Example applications including the AG-UI Dojo demo viewer
85+
- `/python-sdk/`: Python implementation of the protocol
86+
- `/docs/`: Documentation site content
87+
88+
### Integration Pattern
89+
Each framework integration follows a similar pattern:
90+
1. Implements the AbstractAgent interface
91+
2. Translates framework-specific events to AG-UI protocol events
92+
3. Provides both TypeScript client and Python server implementations
93+
4. Includes examples demonstrating key AG-UI features (agentic chat, generative UI, human-in-the-loop, etc.)
94+
95+
### State Management
96+
- Uses STATE_SNAPSHOT for complete state representations
97+
- Uses STATE_DELTA with JSON Patch (RFC 6902) for efficient incremental updates
98+
- MESSAGES_SNAPSHOT provides conversation history
99+
100+
### Multiple Sequential Runs
101+
- AG-UI supports multiple sequential runs in a single event stream
102+
- Each run must complete (RUN_FINISHED) before a new run can start (RUN_STARTED)
103+
- Messages accumulate across runs (e.g., messages from run1 + messages from run2)
104+
- State continues to evolve across runs unless explicitly reset with STATE_SNAPSHOT
105+
- Run-specific tracking (active messages, tool calls, steps) resets between runs
106+
107+
### Development Workflow
108+
- Turbo is used for monorepo build orchestration
109+
- Each package has independent versioning
110+
- Integration tests demonstrate protocol compliance
111+
- The AG-UI Dojo app showcases all protocol features with live examples

docs/concepts/events.mdx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,10 +194,10 @@ for an incoming message, such as creating a new message bubble with a loading
194194
indicator. The `role` property identifies whether the message is coming from the
195195
assistant or potentially another participant in the conversation.
196196

197-
| Property | Description |
198-
| ----------- | ---------------------------------------------- |
199-
| `messageId` | Unique identifier for the message |
200-
| `role` | Role of the message sender (e.g., "assistant") |
197+
| Property | Description |
198+
| ----------- | --------------------------------------------------------------------------------- |
199+
| `messageId` | Unique identifier for the message |
200+
| `role` | Role of the message sender ("developer", "system", "assistant", "user", "tool") |
201201

202202
### TextMessageContent
203203

@@ -231,6 +231,22 @@ automatic scrolling to ensure the full message is visible.
231231
| ----------- | -------------------------------------- |
232232
| `messageId` | Matches the ID from `TextMessageStart` |
233233

234+
### TextMessageChunk
235+
236+
A self-contained text message event that combines start, content, and end.
237+
238+
The `TextMessageChunk` event provides a convenient way to send complete text messages
239+
in a single event instead of the three-event sequence (start, content, end). This is
240+
particularly useful for simple messages or when the entire content is available at once.
241+
The event includes both the message metadata and content, making it more efficient for
242+
non-streaming scenarios.
243+
244+
| Property | Description |
245+
| ----------- | ------------------------------------------------------------------------------------- |
246+
| `messageId` | Optional unique identifier for the message |
247+
| `role` | Optional role of the sender ("developer", "system", "assistant", "user", "tool") |
248+
| `delta` | Optional text content of the message |
249+
234250
## Tool Call Events
235251

236252
These events represent the lifecycle of tool calls made by agents. Tool calls

python-sdk/ag_ui/core/events.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77

88
from pydantic import Field
99

10-
from .types import ConfiguredBaseModel, Message, State
10+
from .types import ConfiguredBaseModel, Message, State, Role
11+
12+
# Text messages can have any role except "tool"
13+
TextMessageRole = Literal["developer", "system", "assistant", "user"]
1114

1215

1316
class EventType(str, Enum):
@@ -55,7 +58,7 @@ class TextMessageStartEvent(BaseEvent):
5558
"""
5659
type: Literal[EventType.TEXT_MESSAGE_START] = EventType.TEXT_MESSAGE_START # pyright: ignore[reportIncompatibleVariableOverride]
5760
message_id: str
58-
role: Literal["assistant"] = "assistant"
61+
role: TextMessageRole = "assistant"
5962

6063

6164
class TextMessageContentEvent(BaseEvent):
@@ -80,7 +83,7 @@ class TextMessageChunkEvent(BaseEvent):
8083
"""
8184
type: Literal[EventType.TEXT_MESSAGE_CHUNK] = EventType.TEXT_MESSAGE_CHUNK # pyright: ignore[reportIncompatibleVariableOverride]
8285
message_id: Optional[str] = None
83-
role: Optional[Literal["assistant"]] = None
86+
role: Optional[TextMessageRole] = None
8487
delta: Optional[str] = None
8588

8689
class ThinkingTextMessageStartEvent(BaseEvent):
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"""Tests for text message events with different roles."""
2+
3+
import unittest
4+
from pydantic import ValidationError
5+
from ag_ui.core import (
6+
EventType,
7+
TextMessageStartEvent,
8+
TextMessageContentEvent,
9+
TextMessageEndEvent,
10+
TextMessageChunkEvent,
11+
Role,
12+
)
13+
14+
# Test all available roles for text messages (excluding "tool")
15+
TEXT_MESSAGE_ROLES = ["developer", "system", "assistant", "user"]
16+
17+
18+
class TestTextMessageRoles(unittest.TestCase):
19+
"""Test text message events with different roles."""
20+
21+
def test_text_message_start_with_all_roles(self) -> None:
22+
"""Test TextMessageStartEvent with different roles."""
23+
for role in TEXT_MESSAGE_ROLES:
24+
with self.subTest(role=role):
25+
event = TextMessageStartEvent(
26+
message_id="test-msg",
27+
role=role,
28+
)
29+
30+
self.assertEqual(event.type, EventType.TEXT_MESSAGE_START)
31+
self.assertEqual(event.message_id, "test-msg")
32+
self.assertEqual(event.role, role)
33+
34+
def test_text_message_chunk_with_all_roles(self) -> None:
35+
"""Test TextMessageChunkEvent with different roles."""
36+
for role in TEXT_MESSAGE_ROLES:
37+
with self.subTest(role=role):
38+
event = TextMessageChunkEvent(
39+
message_id="test-msg",
40+
role=role,
41+
delta=f"Hello from {role}",
42+
)
43+
44+
self.assertEqual(event.type, EventType.TEXT_MESSAGE_CHUNK)
45+
self.assertEqual(event.message_id, "test-msg")
46+
self.assertEqual(event.role, role)
47+
self.assertEqual(event.delta, f"Hello from {role}")
48+
49+
def test_text_message_chunk_without_role(self) -> None:
50+
"""Test TextMessageChunkEvent without role (should be optional)."""
51+
event = TextMessageChunkEvent(
52+
message_id="test-msg",
53+
delta="Hello without role",
54+
)
55+
56+
self.assertEqual(event.type, EventType.TEXT_MESSAGE_CHUNK)
57+
self.assertEqual(event.message_id, "test-msg")
58+
self.assertIsNone(event.role)
59+
self.assertEqual(event.delta, "Hello without role")
60+
61+
def test_multiple_messages_different_roles(self) -> None:
62+
"""Test creating multiple messages with different roles."""
63+
events = []
64+
65+
for role in TEXT_MESSAGE_ROLES:
66+
start_event = TextMessageStartEvent(
67+
message_id=f"msg-{role}",
68+
role=role,
69+
)
70+
content_event = TextMessageContentEvent(
71+
message_id=f"msg-{role}",
72+
delta=f"Message from {role}",
73+
)
74+
end_event = TextMessageEndEvent(
75+
message_id=f"msg-{role}",
76+
)
77+
78+
events.extend([start_event, content_event, end_event])
79+
80+
# Verify we have 3 events per role
81+
self.assertEqual(len(events), len(TEXT_MESSAGE_ROLES) * 3)
82+
83+
# Verify each start event has the correct role
84+
for i, role in enumerate(TEXT_MESSAGE_ROLES):
85+
start_event = events[i * 3]
86+
self.assertIsInstance(start_event, TextMessageStartEvent)
87+
self.assertEqual(start_event.role, role)
88+
self.assertEqual(start_event.message_id, f"msg-{role}")
89+
90+
def test_text_message_serialization(self) -> None:
91+
"""Test that text message events serialize correctly with roles."""
92+
for role in TEXT_MESSAGE_ROLES:
93+
with self.subTest(role=role):
94+
event = TextMessageStartEvent(
95+
message_id="test-msg",
96+
role=role,
97+
)
98+
99+
# Convert to dict and back
100+
event_dict = event.model_dump()
101+
self.assertEqual(event_dict["role"], role)
102+
self.assertEqual(event_dict["type"], EventType.TEXT_MESSAGE_START)
103+
self.assertEqual(event_dict["message_id"], "test-msg")
104+
105+
# Recreate from dict
106+
new_event = TextMessageStartEvent(**event_dict)
107+
self.assertEqual(new_event.role, role)
108+
self.assertEqual(new_event, event)
109+
110+
def test_invalid_role_rejected(self) -> None:
111+
"""Test that invalid roles are rejected."""
112+
# Test with completely invalid role
113+
with self.assertRaises(ValidationError):
114+
TextMessageStartEvent(
115+
message_id="test-msg",
116+
role="invalid_role", # type: ignore
117+
)
118+
119+
# Test that 'tool' role is not allowed for text messages
120+
with self.assertRaises(ValidationError):
121+
TextMessageStartEvent(
122+
message_id="test-msg",
123+
role="tool", # type: ignore
124+
)
125+
126+
# Test that 'tool' role is not allowed for chunks either
127+
with self.assertRaises(ValidationError):
128+
TextMessageChunkEvent(
129+
message_id="test-msg",
130+
role="tool", # type: ignore
131+
delta="Tool message",
132+
)
133+
134+
def test_text_message_start_default_role(self) -> None:
135+
"""Test that TextMessageStartEvent defaults to 'assistant' role."""
136+
event = TextMessageStartEvent(
137+
message_id="test-msg",
138+
)
139+
140+
self.assertEqual(event.type, EventType.TEXT_MESSAGE_START)
141+
self.assertEqual(event.message_id, "test-msg")
142+
self.assertEqual(event.role, "assistant") # Should default to assistant
143+
144+
145+
if __name__ == "__main__":
146+
unittest.main()

typescript-sdk/packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "create-ag-ui-app",
33
"author": "Markus Ecker <[email protected]>",
4-
"version": "0.0.38",
4+
"version": "0.0.39-alpha.0",
55
"private": false,
66
"publishConfig": {
77
"access": "public"

typescript-sdk/packages/client/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@ag-ui/client",
33
"author": "Markus Ecker <[email protected]>",
4-
"version": "0.0.36",
4+
"version": "0.0.37-alpha.0",
55
"private": false,
66
"publishConfig": {
77
"access": "public"

0 commit comments

Comments
 (0)