Skip to content

Commit d1768d8

Browse files
authored
Add result event (#130)
* implement tool result * add docs
1 parent 3429bae commit d1768d8

File tree

10 files changed

+243
-4
lines changed

10 files changed

+243
-4
lines changed

docs/concepts/events.mdx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,9 @@ sequenceDiagram
255255
256256
Note over Agent,Client: Tool call completes
257257
Agent->>Client: ToolCallEnd
258+
259+
Note over Agent,Client: Tool execution result
260+
Agent->>Client: ToolCallResult
258261
```
259262

260263
The `ToolCallArgs` events each contain a `delta` field with a chunk of the
@@ -314,6 +317,26 @@ received the results.
314317
| ------------ | ----------------------------------- |
315318
| `toolCallId` | Matches the ID from `ToolCallStart` |
316319

320+
### ToolCallResult
321+
322+
Provides the result of a tool call execution.
323+
324+
The `ToolCallResult` event delivers the output or result from a tool that was
325+
previously invoked by the agent. This event is sent after the tool has been
326+
executed by the system and contains the actual output generated by the tool.
327+
Unlike the streaming pattern of tool call specification (start, args, end), the
328+
result is delivered as a complete unit since tool execution typically produces a
329+
complete output. Frontends can use this event to display tool results to users,
330+
append them to the conversation history, or trigger follow-up actions based on
331+
the tool's output.
332+
333+
| Property | Description |
334+
| ------------ | ----------------------------------------------------------- |
335+
| `messageId` | ID of the conversation message this result belongs to |
336+
| `toolCallId` | Matches the ID from the corresponding `ToolCallStart` event |
337+
| `content` | The actual result/output content from the tool execution |
338+
| `role` | Optional role identifier, typically "tool" for tool results |
339+
317340
## State Management Events
318341

319342
These events are used to manage and synchronize the agent's state with the

docs/sdk/js/core/events.mdx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ enum EventType {
2222
TOOL_CALL_START = "TOOL_CALL_START",
2323
TOOL_CALL_ARGS = "TOOL_CALL_ARGS",
2424
TOOL_CALL_END = "TOOL_CALL_END",
25+
TOOL_CALL_RESULT = "TOOL_CALL_RESULT",
2526
STATE_SNAPSHOT = "STATE_SNAPSHOT",
2627
STATE_DELTA = "STATE_DELTA",
2728
MESSAGES_SNAPSHOT = "MESSAGES_SNAPSHOT",
@@ -247,6 +248,27 @@ type ToolCallEndEvent = BaseEvent & {
247248
| ------------ | -------- | -------------------------------------- |
248249
| `toolCallId` | `string` | Matches the ID from ToolCallStartEvent |
249250
251+
### ToolCallResultEvent
252+
253+
Provides the result of a tool call execution.
254+
255+
```typescript
256+
type ToolCallResultEvent = BaseEvent & {
257+
type: EventType.TOOL_CALL_RESULT
258+
messageId: string
259+
toolCallId: string
260+
content: string
261+
role?: "tool"
262+
}
263+
```
264+
265+
| Property | Type | Description |
266+
| ------------ | ------------------- | ----------------------------------------------------------- |
267+
| `messageId` | `string` | ID of the conversation message this result belongs to |
268+
| `toolCallId` | `string` | Matches the ID from the corresponding ToolCallStartEvent |
269+
| `content` | `string` | The actual result/output content from the tool execution |
270+
| `role` | `"tool"` (optional) | Optional role identifier, typically "tool" for tool results |
271+
250272
## State Management Events
251273
252274
These events are used to manage agent state.
@@ -344,6 +366,7 @@ const EventSchemas = z.discriminatedUnion("type", [
344366
ToolCallStartEventSchema,
345367
ToolCallArgsEventSchema,
346368
ToolCallEndEventSchema,
369+
ToolCallResultEventSchema,
347370
StateSnapshotEventSchema,
348371
StateDeltaEventSchema,
349372
MessagesSnapshotEventSchema,

docs/sdk/python/core/events.mdx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class EventType(str, Enum):
2525
TOOL_CALL_START = "TOOL_CALL_START"
2626
TOOL_CALL_ARGS = "TOOL_CALL_ARGS"
2727
TOOL_CALL_END = "TOOL_CALL_END"
28+
TOOL_CALL_RESULT = "TOOL_CALL_RESULT"
2829
STATE_SNAPSHOT = "STATE_SNAPSHOT"
2930
STATE_DELTA = "STATE_DELTA"
3031
MESSAGES_SNAPSHOT = "MESSAGES_SNAPSHOT"
@@ -265,6 +266,28 @@ class ToolCallEndEvent(BaseEvent):
265266
| -------------- | ----- | -------------------------------------- |
266267
| `tool_call_id` | `str` | Matches the ID from ToolCallStartEvent |
267268

269+
### ToolCallResultEvent
270+
271+
`from ag_ui.core import ToolCallResultEvent`
272+
273+
Provides the result of a tool call execution.
274+
275+
```python
276+
class ToolCallResultEvent(BaseEvent):
277+
message_id: str
278+
type: Literal[EventType.TOOL_CALL_RESULT]
279+
tool_call_id: str
280+
content: str
281+
role: Optional[Literal["tool"]] = None
282+
```
283+
284+
| Property | Type | Description |
285+
| -------------- | --------------------------- | ----------------------------------------------------------- |
286+
| `message_id` | `str` | ID of the conversation message this result belongs to |
287+
| `tool_call_id` | `str` | Matches the ID from the corresponding ToolCallStartEvent |
288+
| `content` | `str` | The actual result/output content from the tool execution |
289+
| `role` | `Optional[Literal["tool"]]` | Optional role identifier, typically "tool" for tool results |
290+
268291
## State Management Events
269292

270293
These events are used to manage agent state.
@@ -370,6 +393,7 @@ Event = Annotated[
370393
ToolCallStartEvent,
371394
ToolCallArgsEvent,
372395
ToolCallEndEvent,
396+
ToolCallResultEvent,
373397
StateSnapshotEvent,
374398
StateDeltaEvent,
375399
MessagesSnapshotEvent,

python-sdk/ag_ui/core/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,16 @@
99
TextMessageContentEvent,
1010
TextMessageEndEvent,
1111
TextMessageChunkEvent,
12+
ThinkingTextMessageStartEvent,
13+
ThinkingTextMessageContentEvent,
14+
ThinkingTextMessageEndEvent,
1215
ToolCallStartEvent,
1316
ToolCallArgsEvent,
1417
ToolCallEndEvent,
1518
ToolCallChunkEvent,
19+
ToolCallResultEvent,
20+
ThinkingStartEvent,
21+
ThinkingEndEvent,
1622
StateSnapshotEvent,
1723
StateDeltaEvent,
1824
MessagesSnapshotEvent,
@@ -58,6 +64,7 @@
5864
"ToolCallArgsEvent",
5965
"ToolCallEndEvent",
6066
"ToolCallChunkEvent",
67+
"ToolCallResultEvent",
6168
"ThinkingStartEvent",
6269
"ThinkingEndEvent",
6370
"StateSnapshotEvent",

python-sdk/ag_ui/core/events.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class EventType(str, Enum):
2424
TOOL_CALL_ARGS = "TOOL_CALL_ARGS"
2525
TOOL_CALL_END = "TOOL_CALL_END"
2626
TOOL_CALL_CHUNK = "TOOL_CALL_CHUNK"
27+
TOOL_CALL_RESULT = "TOOL_CALL_RESULT"
2728
THINKING_START = "THINKING_START"
2829
THINKING_END = "THINKING_END"
2930
STATE_SNAPSHOT = "STATE_SNAPSHOT"
@@ -144,6 +145,16 @@ class ToolCallChunkEvent(BaseEvent):
144145
parent_message_id: Optional[str] = None
145146
delta: Optional[str] = None
146147

148+
class ToolCallResultEvent(BaseEvent):
149+
"""
150+
Event containing the result of a tool call.
151+
"""
152+
message_id: str
153+
type: Literal[EventType.TOOL_CALL_RESULT]
154+
tool_call_id: str
155+
content: str
156+
role: Optional[Literal["tool"]] = None
157+
147158
class ThinkingStartEvent(BaseEvent):
148159
"""
149160
Event indicating the start of a thinking step event.
@@ -252,6 +263,7 @@ class StepFinishedEvent(BaseEvent):
252263
ToolCallArgsEvent,
253264
ToolCallEndEvent,
254265
ToolCallChunkEvent,
266+
ToolCallResultEvent,
255267
StateSnapshotEvent,
256268
StateDeltaEvent,
257269
MessagesSnapshotEvent,

typescript-sdk/packages/client/src/agent/__tests__/legacy-bridged.test.ts

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import { toArray } from "rxjs/operators";
2-
import { v4 as uuidv4 } from "uuid";
3-
import { LegacyRuntimeProtocolEvent } from "../../legacy/types";
42
import { EventType, BaseEvent, RunAgentInput } from "@ag-ui/core";
53
import { AbstractAgent } from "../../agent/agent";
64
import { Observable, lastValueFrom } from "rxjs";
@@ -86,6 +84,63 @@ class ChunkTestAgent extends AbstractAgent {
8684
}
8785
}
8886

87+
// Agent that emits tool call events with results
88+
class ToolCallTestAgent extends AbstractAgent {
89+
protected run(input: RunAgentInput): Observable<BaseEvent> {
90+
const toolCallId = "test-tool-call-id";
91+
const toolCallName = "get_weather";
92+
return new Observable<BaseEvent>((observer) => {
93+
observer.next({
94+
type: EventType.RUN_STARTED,
95+
threadId: input.threadId,
96+
runId: input.runId,
97+
timestamp: Date.now(),
98+
} as BaseEvent);
99+
100+
// Start tool call
101+
observer.next({
102+
type: EventType.TOOL_CALL_START,
103+
toolCallId,
104+
toolCallName,
105+
timestamp: Date.now(),
106+
} as BaseEvent);
107+
108+
// Tool call arguments
109+
observer.next({
110+
type: EventType.TOOL_CALL_ARGS,
111+
toolCallId,
112+
delta: '{"location": "San Francisco"}',
113+
timestamp: Date.now(),
114+
} as BaseEvent);
115+
116+
// End tool call
117+
observer.next({
118+
type: EventType.TOOL_CALL_END,
119+
toolCallId,
120+
timestamp: Date.now(),
121+
} as BaseEvent);
122+
123+
// Tool call result
124+
observer.next({
125+
messageId: "test-message-id",
126+
type: EventType.TOOL_CALL_RESULT,
127+
toolCallId,
128+
content: "The weather in San Francisco is 72°F and sunny.",
129+
timestamp: Date.now(),
130+
} as BaseEvent);
131+
132+
observer.next({
133+
type: EventType.RUN_FINISHED,
134+
threadId: input.threadId,
135+
runId: input.runId,
136+
timestamp: Date.now(),
137+
} as BaseEvent);
138+
139+
observer.complete();
140+
});
141+
}
142+
}
143+
89144
describe("AbstractAgent.legacy_to_be_removed_runAgentBridged", () => {
90145
beforeEach(() => {
91146
jest.clearAllMocks();
@@ -96,7 +151,6 @@ describe("AbstractAgent.legacy_to_be_removed_runAgentBridged", () => {
96151
const agent = new TestAgent({
97152
threadId: "test-thread-id",
98153
agentId: "test-agent-id",
99-
debug: true,
100154
});
101155

102156
// Get the observable that emits legacy events
@@ -241,7 +295,6 @@ describe("AbstractAgent.legacy_to_be_removed_runAgentBridged", () => {
241295
const agent = new ChunkTestAgent({
242296
threadId: "test-thread-id",
243297
agentId: "test-agent-id",
244-
debug: true,
245298
});
246299

247300
// Get the observable that emits legacy events
@@ -280,4 +333,57 @@ describe("AbstractAgent.legacy_to_be_removed_runAgentBridged", () => {
280333
active: false,
281334
});
282335
});
336+
337+
it("should transform tool call events with results into legacy events with correct tool name", async () => {
338+
// Setup agent with mock IDs
339+
const agent = new ToolCallTestAgent({
340+
threadId: "test-thread-id",
341+
agentId: "test-agent-id",
342+
});
343+
344+
// Get the observable that emits legacy events
345+
const legacy$ = agent.legacy_to_be_removed_runAgentBridged();
346+
347+
// Collect all emitted events
348+
const legacyEvents = await lastValueFrom(legacy$.pipe(toArray()));
349+
350+
// Verify events are in correct legacy format
351+
expect(legacyEvents).toHaveLength(5); // ActionExecutionStart, ActionExecutionArgs, ActionExecutionEnd, ActionExecutionResult, AgentStateMessage
352+
353+
// ActionExecutionStart
354+
expect(legacyEvents[0]).toMatchObject({
355+
type: "ActionExecutionStart",
356+
actionExecutionId: "test-tool-call-id",
357+
actionName: "get_weather",
358+
});
359+
360+
// ActionExecutionArgs
361+
expect(legacyEvents[1]).toMatchObject({
362+
type: "ActionExecutionArgs",
363+
actionExecutionId: "test-tool-call-id",
364+
args: '{"location": "San Francisco"}',
365+
});
366+
367+
// ActionExecutionEnd
368+
expect(legacyEvents[2]).toMatchObject({
369+
type: "ActionExecutionEnd",
370+
actionExecutionId: "test-tool-call-id",
371+
});
372+
373+
// ActionExecutionResult - this should include the tool name
374+
expect(legacyEvents[3]).toMatchObject({
375+
type: "ActionExecutionResult",
376+
actionExecutionId: "test-tool-call-id",
377+
actionName: "get_weather", // This verifies the tool name is correctly included
378+
result: "The weather in San Francisco is 72°F and sunny.",
379+
});
380+
381+
// Final AgentStateMessage
382+
expect(legacyEvents[4]).toMatchObject({
383+
type: "AgentStateMessage",
384+
threadId: "test-thread-id",
385+
agentName: "test-agent-id",
386+
active: false,
387+
});
388+
});
283389
});

typescript-sdk/packages/client/src/apply/default.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
CustomEvent,
1313
BaseEvent,
1414
AssistantMessage,
15+
ToolCallResultEvent,
16+
ToolMessage,
1517
} from "@ag-ui/core";
1618
import { mergeMap } from "rxjs/operators";
1719
import { structuredClone_ } from "../utils";
@@ -151,6 +153,21 @@ export const defaultApplyEvents = (...args: Parameters<ApplyEvents>): ReturnType
151153
return emitNoUpdate();
152154
}
153155

156+
case EventType.TOOL_CALL_RESULT: {
157+
const { messageId, toolCallId, content, role } = event as ToolCallResultEvent;
158+
159+
const toolMessage: ToolMessage = {
160+
id: messageId,
161+
toolCallId,
162+
role: role || "tool",
163+
content: content,
164+
};
165+
166+
messages.push(toolMessage);
167+
168+
return emitNoUpdate();
169+
}
170+
154171
case EventType.STATE_SNAPSHOT: {
155172
const { snapshot } = event as StateSnapshotEvent;
156173

typescript-sdk/packages/client/src/chunks/transform.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export const transformChunks =
8484
case EventType.TOOL_CALL_START:
8585
case EventType.TOOL_CALL_ARGS:
8686
case EventType.TOOL_CALL_END:
87+
case EventType.TOOL_CALL_RESULT:
8788
case EventType.STATE_SNAPSHOT:
8889
case EventType.STATE_DELTA:
8990
case EventType.MESSAGES_SNAPSHOT:

0 commit comments

Comments
 (0)