Skip to content
Closed
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
2 changes: 2 additions & 0 deletions docs/introduction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ AG-UI provides:
communication
- **Best practices** for chat, streaming state updates, human-in-the-loop and
shared state
- **Thinking transparency** with dedicated thinking message events that expose
agent reasoning processes to users

## Existing Integrations

Expand Down
1 change: 1 addition & 0 deletions docs/quickstart/introduction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Think of it like adding a universal translator to your agent. Instead of buildin

Agents integrating with AG-UI can:
- **Stream responses** - Real-time text that appears as it's generated
- **Show thinking** - Expose internal reasoning through thinking message events
- **Call client-side tools** - Your agent can use functions and services defined by clients
- **Share state** - Your agent's state is bidirectional shared state
- **Execute universally** - Integrate with any AG-UI compatible client application
Expand Down
64 changes: 64 additions & 0 deletions docs/sdk/python/core/events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ class EventType(str, Enum):
TEXT_MESSAGE_START = "TEXT_MESSAGE_START"
TEXT_MESSAGE_CONTENT = "TEXT_MESSAGE_CONTENT"
TEXT_MESSAGE_END = "TEXT_MESSAGE_END"
THINKING_TEXT_MESSAGE_START = "THINKING_TEXT_MESSAGE_START"
THINKING_TEXT_MESSAGE_CONTENT = "THINKING_TEXT_MESSAGE_CONTENT"
THINKING_TEXT_MESSAGE_END = "THINKING_TEXT_MESSAGE_END"
TOOL_CALL_START = "TOOL_CALL_START"
TOOL_CALL_ARGS = "TOOL_CALL_ARGS"
TOOL_CALL_END = "TOOL_CALL_END"
Expand Down Expand Up @@ -210,6 +213,64 @@ class TextMessageEndEvent(BaseEvent):
| ------------ | ----- | ----------------------------------------- |
| `message_id` | `str` | Matches the ID from TextMessageStartEvent |

## Thinking Message Events

These events represent the lifecycle of thinking messages that show an agent's internal reasoning process.

### ThinkingTextMessageStartEvent

`from ag_ui.core import ThinkingTextMessageStartEvent`

Signals the start of a thinking message.

```python
class ThinkingTextMessageStartEvent(BaseEvent):
type: Literal[EventType.THINKING_TEXT_MESSAGE_START]
thinking_id: str
```

| Property | Type | Description |
| ------------- | ----- | ------------------------------------- |
| `thinking_id` | `str` | Unique identifier for the thinking sequence |

### ThinkingTextMessageContentEvent

`from ag_ui.core import ThinkingTextMessageContentEvent`

Represents a chunk of content in a streaming thinking message.

```python
class ThinkingTextMessageContentEvent(BaseEvent):
type: Literal[EventType.THINKING_TEXT_MESSAGE_CONTENT]
thinking_id: str
delta: str # Non-empty string

def model_post_init(self, __context):
if len(self.delta) == 0:
raise ValueError("Delta must not be an empty string")
```

| Property | Type | Description |
| ------------- | ----- | -------------------------------------------------- |
| `thinking_id` | `str` | Matches the ID from ThinkingTextMessageStartEvent |
| `delta` | `str` | Thinking content chunk (non-empty) |

### ThinkingTextMessageEndEvent

`from ag_ui.core import ThinkingTextMessageEndEvent`

Signals the end of a thinking message.

```python
class ThinkingTextMessageEndEvent(BaseEvent):
type: Literal[EventType.THINKING_TEXT_MESSAGE_END]
thinking_id: str
```

| Property | Type | Description |
| ------------- | ----- | -------------------------------------------------- |
| `thinking_id` | `str` | Matches the ID from ThinkingTextMessageStartEvent |

## Tool Call Events

These events represent the lifecycle of tool calls made by agents.
Expand Down Expand Up @@ -392,6 +453,9 @@ Event = Annotated[
TextMessageStartEvent,
TextMessageContentEvent,
TextMessageEndEvent,
ThinkingTextMessageStartEvent,
ThinkingTextMessageContentEvent,
ThinkingTextMessageEndEvent,
ToolCallStartEvent,
ToolCallArgsEvent,
ToolCallEndEvent,
Expand Down
24 changes: 22 additions & 2 deletions docs/sdk/python/core/types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ messages in the system.
Represents the possible roles a message sender can have.

```python
Role = Literal["developer", "system", "assistant", "user", "tool"]
Role = Literal["developer", "system", "assistant", "user", "tool", "thinking"]
```

### DeveloperMessage
Expand Down Expand Up @@ -155,6 +155,26 @@ class ToolMessage(ConfiguredBaseModel):
| `tool_call_id` | `str` | ID of the tool call this message responds to |
| `error` | `Optional[str]` | Error message if the tool call failed |

### ThinkingMessage

`from ag_ui.core import ThinkingMessage`

Represents a thinking message that shows an agent's internal reasoning process.

```python
class ThinkingMessage(BaseMessage):
role: Literal["thinking"]
content: str
```

| Property | Type | Description |
| ----------- | --------------------- | ----------------------------------------------- |
| `id` | `str` | Unique identifier for the message |
| `role` | `Literal["thinking"]` | Role of the message sender, fixed as "thinking" |
| `content` | `str` | Text content of the thinking (required) |
| `name` | `Optional[str]` | Optional name of the sender |
| `timestamp` | `Optional[int]` | Optional timestamp for temporal tracking |

### Message

`from ag_ui.core import Message`
Expand All @@ -163,7 +183,7 @@ A union type representing any type of message in the system.

```python
Message = Annotated[
Union[DeveloperMessage, SystemMessage, AssistantMessage, UserMessage, ToolMessage],
Union[DeveloperMessage, SystemMessage, AssistantMessage, UserMessage, ToolMessage, ThinkingMessage],
Field(discriminator="role")
]
```
Expand Down
8 changes: 7 additions & 1 deletion python-sdk/ag_ui/core/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from pydantic import Field

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

# Text messages can have any role except "tool"
TextMessageRole = Literal["developer", "system", "assistant", "user"]
Expand Down Expand Up @@ -91,19 +91,22 @@ class ThinkingTextMessageStartEvent(BaseEvent):
Event indicating the start of a thinking text message.
"""
type: Literal[EventType.THINKING_TEXT_MESSAGE_START] = EventType.THINKING_TEXT_MESSAGE_START # pyright: ignore[reportIncompatibleVariableOverride]
thinking_id: str

class ThinkingTextMessageContentEvent(BaseEvent):
"""
Event indicating a piece of a thinking text message.
"""
type: Literal[EventType.THINKING_TEXT_MESSAGE_CONTENT] = EventType.THINKING_TEXT_MESSAGE_CONTENT # pyright: ignore[reportIncompatibleVariableOverride]
delta: str = Field(min_length=1)
thinking_id: str

class ThinkingTextMessageEndEvent(BaseEvent):
"""
Event indicating the end of a thinking text message.
"""
type: Literal[EventType.THINKING_TEXT_MESSAGE_END] = EventType.THINKING_TEXT_MESSAGE_END # pyright: ignore[reportIncompatibleVariableOverride]
thinking_id: str

class ToolCallStartEvent(BaseEvent):
"""
Expand Down Expand Up @@ -256,6 +259,9 @@ class StepFinishedEvent(BaseEvent):
TextMessageContentEvent,
TextMessageEndEvent,
TextMessageChunkEvent,
ThinkingTextMessageStartEvent,
ThinkingTextMessageContentEvent,
ThinkingTextMessageEndEvent,
ToolCallStartEvent,
ToolCallArgsEvent,
ToolCallEndEvent,
Expand Down
11 changes: 9 additions & 2 deletions python-sdk/ag_ui/core/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class BaseMessage(ConfiguredBaseModel):
role: str
content: Optional[str] = None
name: Optional[str] = None
timestamp: Optional[int] = None


class DeveloperMessage(BaseMessage):
Expand Down Expand Up @@ -88,13 +89,19 @@ class ToolMessage(ConfiguredBaseModel):
tool_call_id: str
error: Optional[str] = None

class ThinkingMessage(BaseMessage):
"""
A thinking message.
"""
role: Literal["thinking"] = "thinking"
content: str

Message = Annotated[
Union[DeveloperMessage, SystemMessage, AssistantMessage, UserMessage, ToolMessage],
Union[DeveloperMessage, SystemMessage, AssistantMessage, UserMessage, ToolMessage, ThinkingMessage],
Field(discriminator="role")
]

Role = Literal["developer", "system", "assistant", "user", "tool"]
Role = Literal["developer", "system", "assistant", "user", "tool", "thinking"]


class Context(ConfiguredBaseModel):
Expand Down
138 changes: 138 additions & 0 deletions python-sdk/tests/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
TextMessageStartEvent,
TextMessageContentEvent,
TextMessageEndEvent,
ThinkingTextMessageStartEvent,
ThinkingTextMessageContentEvent,
ThinkingTextMessageEndEvent,
ToolCallStartEvent,
ToolCallArgsEvent,
ToolCallEndEvent,
Expand Down Expand Up @@ -90,6 +93,105 @@ def test_text_message_end(self):
self.assertEqual(serialized["type"], "TEXT_MESSAGE_END")
self.assertEqual(serialized["messageId"], "msg_123")

def test_thinking_text_message_start(self):
"""Test creating and serializing a ThinkingTextMessageStartEvent event"""
event = ThinkingTextMessageStartEvent(
thinking_id="think_123",
timestamp=1648214400000
)
self.assertEqual(event.thinking_id, "think_123")

# Test serialization
serialized = event.model_dump(by_alias=True)
self.assertEqual(serialized["type"], "THINKING_TEXT_MESSAGE_START")
self.assertEqual(serialized["thinkingId"], "think_123")
self.assertEqual(serialized["timestamp"], 1648214400000)

def test_thinking_text_message_content(self):
"""Test creating and serializing a ThinkingTextMessageContentEvent event"""
event = ThinkingTextMessageContentEvent(
thinking_id="think_123",
delta="Let me think about this...",
timestamp=1648214400000
)
self.assertEqual(event.thinking_id, "think_123")
self.assertEqual(event.delta, "Let me think about this...")

# Test serialization
serialized = event.model_dump(by_alias=True)
self.assertEqual(serialized["type"], "THINKING_TEXT_MESSAGE_CONTENT")
self.assertEqual(serialized["thinkingId"], "think_123")
self.assertEqual(serialized["delta"], "Let me think about this...")

def test_thinking_text_message_end(self):
"""Test creating and serializing a ThinkingTextMessageEndEvent event"""
event = ThinkingTextMessageEndEvent(
thinking_id="think_123",
timestamp=1648214400000
)
self.assertEqual(event.thinking_id, "think_123")

# Test serialization
serialized = event.model_dump(by_alias=True)
self.assertEqual(serialized["type"], "THINKING_TEXT_MESSAGE_END")
self.assertEqual(serialized["thinkingId"], "think_123")

def test_thinking_message_event_lifecycle(self):
"""Test a complete thinking message event lifecycle"""
thinking_id = "think_lifecycle_456"
timestamp_base = 1648214400000

# Start event
start_event = ThinkingTextMessageStartEvent(
thinking_id=thinking_id,
timestamp=timestamp_base
)

# Content events
content_events = [
ThinkingTextMessageContentEvent(
thinking_id=thinking_id,
delta="First, I need to understand the problem...",
timestamp=timestamp_base + 100
),
ThinkingTextMessageContentEvent(
thinking_id=thinking_id,
delta=" Then I should consider the constraints...",
timestamp=timestamp_base + 200
),
ThinkingTextMessageContentEvent(
thinking_id=thinking_id,
delta=" Finally, I can formulate a solution.",
timestamp=timestamp_base + 300
)
]

# End event
end_event = ThinkingTextMessageEndEvent(
thinking_id=thinking_id,
timestamp=timestamp_base + 400
)

# Verify all events have the same thinking_id
all_events = [start_event] + content_events + [end_event]
for event in all_events:
self.assertEqual(event.thinking_id, thinking_id)

# Verify event types
self.assertEqual(start_event.type, EventType.THINKING_TEXT_MESSAGE_START)
for content_event in content_events:
self.assertEqual(content_event.type, EventType.THINKING_TEXT_MESSAGE_CONTENT)
self.assertEqual(end_event.type, EventType.THINKING_TEXT_MESSAGE_END)

def test_thinking_content_validation(self):
"""Test validation constraints for thinking content events"""
# Content delta cannot be empty
with self.assertRaises(ValueError):
ThinkingTextMessageContentEvent(
thinking_id="think_invalid",
delta="" # Empty delta, should fail
)

def test_tool_call_start(self):
"""Test creating and serializing a ToolCallStartEvent event"""
event = ToolCallStartEvent(
Expand Down Expand Up @@ -327,6 +429,22 @@ def test_event_union_deserialization(self):
"delta": "Hello!",
"timestamp": 1648214400000
},
{
"type": "THINKING_TEXT_MESSAGE_START",
"thinkingId": "think_start",
"timestamp": 1648214400000
},
{
"type": "THINKING_TEXT_MESSAGE_CONTENT",
"thinkingId": "think_content",
"delta": "Let me think...",
"timestamp": 1648214400000
},
{
"type": "THINKING_TEXT_MESSAGE_END",
"thinkingId": "think_end",
"timestamp": 1648214400000
},
{
"type": "TOOL_CALL_START",
"toolCallId": "call_start",
Expand All @@ -349,6 +467,9 @@ def test_event_union_deserialization(self):
expected_types = [
TextMessageStartEvent,
TextMessageContentEvent,
ThinkingTextMessageStartEvent,
ThinkingTextMessageContentEvent,
ThinkingTextMessageEndEvent,
ToolCallStartEvent,
StateSnapshotEvent,
RunErrorEvent
Expand Down Expand Up @@ -380,6 +501,16 @@ def test_serialization_round_trip(self):
message_id="msg_123",
delta="Hello, world!"
),
ThinkingTextMessageStartEvent(
thinking_id="think_123"
),
ThinkingTextMessageContentEvent(
thinking_id="think_123",
delta="Let me consider this..."
),
ThinkingTextMessageEndEvent(
thinking_id="think_123"
),
ToolCallStartEvent(
tool_call_id="call_123",
tool_call_name="get_weather"
Expand Down Expand Up @@ -419,6 +550,13 @@ def test_serialization_round_trip(self):
elif isinstance(original_event, TextMessageContentEvent):
self.assertEqual(deserialized_event.message_id, original_event.message_id)
self.assertEqual(deserialized_event.delta, original_event.delta)
elif isinstance(original_event, ThinkingTextMessageStartEvent):
self.assertEqual(deserialized_event.thinking_id, original_event.thinking_id)
elif isinstance(original_event, ThinkingTextMessageContentEvent):
self.assertEqual(deserialized_event.thinking_id, original_event.thinking_id)
self.assertEqual(deserialized_event.delta, original_event.delta)
elif isinstance(original_event, ThinkingTextMessageEndEvent):
self.assertEqual(deserialized_event.thinking_id, original_event.thinking_id)
elif isinstance(original_event, ToolCallStartEvent):
self.assertEqual(deserialized_event.tool_call_id, original_event.tool_call_id)
self.assertEqual(deserialized_event.tool_call_name, original_event.tool_call_name)
Expand Down
Loading
Loading