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
111 changes: 111 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Common Development Commands

### TypeScript SDK (Main Development)
```bash
# Navigate to typescript-sdk directory for all TypeScript work
cd typescript-sdk

# Install dependencies (using pnpm)
pnpm install

# Build all packages
pnpm build

# Run development mode
pnpm dev

# Run linting
pnpm lint

# Run type checking
pnpm check-types

# Run tests
pnpm test

# Format code
pnpm format

# Clean build artifacts
pnpm clean

# Full clean build
pnpm build:clean
```

### Python SDK
```bash
# Navigate to python-sdk directory
cd python-sdk

# Install dependencies (using poetry)
poetry install

# Run tests
python -m unittest discover tests

# Build distribution
poetry build
```

### Running Specific Integration Tests
```bash
# For TypeScript packages/integrations
cd typescript-sdk/packages/<package-name>
pnpm test

# For running a single test file
cd typescript-sdk/packages/<package-name>
pnpm test -- path/to/test.spec.ts
```

## High-Level Architecture

AG-UI is an event-based protocol that standardizes agent-user interactions. The codebase is organized as a monorepo with the following structure:

### Core Protocol Architecture
- **Event-Driven Communication**: All agent-UI communication happens through typed events (BaseEvent and its subtypes)
- **Transport Agnostic**: Protocol supports SSE, WebSockets, HTTP binary, and custom transports
- **Observable Pattern**: Uses RxJS Observables for streaming agent responses

### Key Abstractions
1. **AbstractAgent**: Base class that all agents must implement with a `run(input: RunAgentInput) -> Observable<BaseEvent>` method
2. **HttpAgent**: Standard HTTP client supporting SSE and binary protocols for connecting to agent endpoints
3. **Event Types**: Lifecycle events (RUN_STARTED/FINISHED), message events (TEXT_MESSAGE_*), tool events (TOOL_CALL_*), and state management events (STATE_SNAPSHOT/DELTA)

### Repository Structure
- `/typescript-sdk/`: Main TypeScript implementation
- `/packages/`: Core protocol packages (@ag-ui/core, @ag-ui/client, @ag-ui/encoder, @ag-ui/proto)
- `/integrations/`: Framework integrations (langgraph, mastra, crewai, etc.)
- `/apps/`: Example applications including the AG-UI Dojo demo viewer
- `/python-sdk/`: Python implementation of the protocol
- `/docs/`: Documentation site content

### Integration Pattern
Each framework integration follows a similar pattern:
1. Implements the AbstractAgent interface
2. Translates framework-specific events to AG-UI protocol events
3. Provides both TypeScript client and Python server implementations
4. Includes examples demonstrating key AG-UI features (agentic chat, generative UI, human-in-the-loop, etc.)

### State Management
- Uses STATE_SNAPSHOT for complete state representations
- Uses STATE_DELTA with JSON Patch (RFC 6902) for efficient incremental updates
- MESSAGES_SNAPSHOT provides conversation history

### Multiple Sequential Runs
- AG-UI supports multiple sequential runs in a single event stream
- Each run must complete (RUN_FINISHED) before a new run can start (RUN_STARTED)
- Messages accumulate across runs (e.g., messages from run1 + messages from run2)
- State continues to evolve across runs unless explicitly reset with STATE_SNAPSHOT
- Run-specific tracking (active messages, tool calls, steps) resets between runs

### Development Workflow
- Turbo is used for monorepo build orchestration
- Each package has independent versioning
- Integration tests demonstrate protocol compliance
- The AG-UI Dojo app showcases all protocol features with live examples
24 changes: 20 additions & 4 deletions docs/concepts/events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,10 @@ for an incoming message, such as creating a new message bubble with a loading
indicator. The `role` property identifies whether the message is coming from the
assistant or potentially another participant in the conversation.

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

### TextMessageContent

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

### TextMessageChunk

A self-contained text message event that combines start, content, and end.

The `TextMessageChunk` event provides a convenient way to send complete text messages
in a single event instead of the three-event sequence (start, content, end). This is
particularly useful for simple messages or when the entire content is available at once.
The event includes both the message metadata and content, making it more efficient for
non-streaming scenarios.

| Property | Description |
| ----------- | ------------------------------------------------------------------------------------- |
| `messageId` | Optional unique identifier for the message |
| `role` | Optional role of the sender ("developer", "system", "assistant", "user", "tool") |
| `delta` | Optional text content of the message |

## Tool Call Events

These events represent the lifecycle of tool calls made by agents. Tool calls
Expand Down
9 changes: 6 additions & 3 deletions python-sdk/ag_ui/core/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@

from pydantic import Field

from .types import ConfiguredBaseModel, Message, State
from .types import ConfiguredBaseModel, Message, State, Role

# Text messages can have any role except "tool"
TextMessageRole = Literal["developer", "system", "assistant", "user"]


class EventType(str, Enum):
Expand Down Expand Up @@ -55,7 +58,7 @@ class TextMessageStartEvent(BaseEvent):
"""
type: Literal[EventType.TEXT_MESSAGE_START] = EventType.TEXT_MESSAGE_START # pyright: ignore[reportIncompatibleVariableOverride]
message_id: str
role: Literal["assistant"] = "assistant"
role: TextMessageRole = "assistant"


class TextMessageContentEvent(BaseEvent):
Expand All @@ -80,7 +83,7 @@ class TextMessageChunkEvent(BaseEvent):
"""
type: Literal[EventType.TEXT_MESSAGE_CHUNK] = EventType.TEXT_MESSAGE_CHUNK # pyright: ignore[reportIncompatibleVariableOverride]
message_id: Optional[str] = None
role: Optional[Literal["assistant"]] = None
role: Optional[TextMessageRole] = None
delta: Optional[str] = None

class ThinkingTextMessageStartEvent(BaseEvent):
Expand Down
146 changes: 146 additions & 0 deletions python-sdk/tests/test_text_roles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"""Tests for text message events with different roles."""

import unittest
from pydantic import ValidationError
from ag_ui.core import (
EventType,
TextMessageStartEvent,
TextMessageContentEvent,
TextMessageEndEvent,
TextMessageChunkEvent,
Role,
)

# Test all available roles for text messages (excluding "tool")
TEXT_MESSAGE_ROLES = ["developer", "system", "assistant", "user"]


class TestTextMessageRoles(unittest.TestCase):
"""Test text message events with different roles."""

def test_text_message_start_with_all_roles(self) -> None:
"""Test TextMessageStartEvent with different roles."""
for role in TEXT_MESSAGE_ROLES:
with self.subTest(role=role):
event = TextMessageStartEvent(
message_id="test-msg",
role=role,
)

self.assertEqual(event.type, EventType.TEXT_MESSAGE_START)
self.assertEqual(event.message_id, "test-msg")
self.assertEqual(event.role, role)

def test_text_message_chunk_with_all_roles(self) -> None:
"""Test TextMessageChunkEvent with different roles."""
for role in TEXT_MESSAGE_ROLES:
with self.subTest(role=role):
event = TextMessageChunkEvent(
message_id="test-msg",
role=role,
delta=f"Hello from {role}",
)

self.assertEqual(event.type, EventType.TEXT_MESSAGE_CHUNK)
self.assertEqual(event.message_id, "test-msg")
self.assertEqual(event.role, role)
self.assertEqual(event.delta, f"Hello from {role}")

def test_text_message_chunk_without_role(self) -> None:
"""Test TextMessageChunkEvent without role (should be optional)."""
event = TextMessageChunkEvent(
message_id="test-msg",
delta="Hello without role",
)

self.assertEqual(event.type, EventType.TEXT_MESSAGE_CHUNK)
self.assertEqual(event.message_id, "test-msg")
self.assertIsNone(event.role)
self.assertEqual(event.delta, "Hello without role")

def test_multiple_messages_different_roles(self) -> None:
"""Test creating multiple messages with different roles."""
events = []

for role in TEXT_MESSAGE_ROLES:
start_event = TextMessageStartEvent(
message_id=f"msg-{role}",
role=role,
)
content_event = TextMessageContentEvent(
message_id=f"msg-{role}",
delta=f"Message from {role}",
)
end_event = TextMessageEndEvent(
message_id=f"msg-{role}",
)

events.extend([start_event, content_event, end_event])

# Verify we have 3 events per role
self.assertEqual(len(events), len(TEXT_MESSAGE_ROLES) * 3)

# Verify each start event has the correct role
for i, role in enumerate(TEXT_MESSAGE_ROLES):
start_event = events[i * 3]
self.assertIsInstance(start_event, TextMessageStartEvent)
self.assertEqual(start_event.role, role)
self.assertEqual(start_event.message_id, f"msg-{role}")

def test_text_message_serialization(self) -> None:
"""Test that text message events serialize correctly with roles."""
for role in TEXT_MESSAGE_ROLES:
with self.subTest(role=role):
event = TextMessageStartEvent(
message_id="test-msg",
role=role,
)

# Convert to dict and back
event_dict = event.model_dump()
self.assertEqual(event_dict["role"], role)
self.assertEqual(event_dict["type"], EventType.TEXT_MESSAGE_START)
self.assertEqual(event_dict["message_id"], "test-msg")

# Recreate from dict
new_event = TextMessageStartEvent(**event_dict)
self.assertEqual(new_event.role, role)
self.assertEqual(new_event, event)

def test_invalid_role_rejected(self) -> None:
"""Test that invalid roles are rejected."""
# Test with completely invalid role
with self.assertRaises(ValidationError):
TextMessageStartEvent(
message_id="test-msg",
role="invalid_role", # type: ignore
)

# Test that 'tool' role is not allowed for text messages
with self.assertRaises(ValidationError):
TextMessageStartEvent(
message_id="test-msg",
role="tool", # type: ignore
)

# Test that 'tool' role is not allowed for chunks either
with self.assertRaises(ValidationError):
TextMessageChunkEvent(
message_id="test-msg",
role="tool", # type: ignore
delta="Tool message",
)

def test_text_message_start_default_role(self) -> None:
"""Test that TextMessageStartEvent defaults to 'assistant' role."""
event = TextMessageStartEvent(
message_id="test-msg",
)

self.assertEqual(event.type, EventType.TEXT_MESSAGE_START)
self.assertEqual(event.message_id, "test-msg")
self.assertEqual(event.role, "assistant") # Should default to assistant


if __name__ == "__main__":
unittest.main()
2 changes: 1 addition & 1 deletion typescript-sdk/packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "create-ag-ui-app",
"author": "Markus Ecker <[email protected]>",
"version": "0.0.38",
"version": "0.0.39-alpha.0",
"private": false,
"publishConfig": {
"access": "public"
Expand Down
2 changes: 1 addition & 1 deletion typescript-sdk/packages/client/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@ag-ui/client",
"author": "Markus Ecker <[email protected]>",
"version": "0.0.36",
"version": "0.0.37-alpha.0",
"private": false,
"publishConfig": {
"access": "public"
Expand Down
Loading
Loading