Skip to main content

ag_ui/event/
tool.rs

1//! Tool invocation: `TOOL_CALL_*`.
2
3use serde::{Deserialize, Serialize};
4
5use crate::event::BaseEvent;
6use crate::ids::{MessageId, SubagentRunId, ToolCallId};
7
8/// Opens a tool call. Arguments follow as `TOOL_CALL_ARGS` deltas.
9#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase")]
11#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
12#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
13pub struct ToolCallStartEvent {
14    /// Timestamp and raw provider event.
15    #[serde(flatten)]
16    pub base: BaseEvent,
17    /// Id correlating this call with its args, end and result.
18    pub tool_call_id: ToolCallId,
19    /// Name of the tool being called.
20    pub tool_call_name: String,
21    /// The assistant message that requested the call.
22    ///
23    /// A JSON `null` here deserializes to `None`: producers whose serializers
24    /// emit nulls for absent optionals (notably the .NET Microsoft Agent
25    /// Framework adapter) must not abort a run on their first tool call.
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub parent_message_id: Option<MessageId>,
28    /// The subagent that produced this event; absent means the parent agent.
29    /// A JSON `null` is rejected — see [`crate::event::subagent`]. A tool
30    /// call belongs to the message `parent_message_id` names, so a tag that
31    /// disagrees with that message's owner is a protocol error; an untagged
32    /// call inherits the message's owner.
33    #[serde(
34        default,
35        deserialize_with = "crate::serde_util::reject_null",
36        skip_serializing_if = "Option::is_none"
37    )]
38    pub subagent_run_id: Option<SubagentRunId>,
39}
40
41impl ToolCallStartEvent {
42    /// Opens a call to `tool_call_name`.
43    pub fn new(tool_call_id: impl Into<ToolCallId>, tool_call_name: impl Into<String>) -> Self {
44        Self {
45            base: BaseEvent::default(),
46            tool_call_id: tool_call_id.into(),
47            tool_call_name: tool_call_name.into(),
48            parent_message_id: None,
49            subagent_run_id: None,
50        }
51    }
52}
53
54/// Appends a chunk of the argument JSON for an open tool call.
55///
56/// The deltas concatenate to a JSON string; individual deltas are usually not
57/// valid JSON on their own.
58#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
59#[serde(rename_all = "camelCase")]
60#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
61#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
62pub struct ToolCallArgsEvent {
63    /// Timestamp and raw provider event.
64    #[serde(flatten)]
65    pub base: BaseEvent,
66    /// The call being appended to.
67    pub tool_call_id: ToolCallId,
68    /// The argument-JSON fragment.
69    pub delta: String,
70    /// The subagent that produced this event; absent means the parent agent.
71    /// A JSON `null` is rejected — see [`crate::event::subagent`].
72    #[serde(
73        default,
74        deserialize_with = "crate::serde_util::reject_null",
75        skip_serializing_if = "Option::is_none"
76    )]
77    pub subagent_run_id: Option<SubagentRunId>,
78}
79
80impl ToolCallArgsEvent {
81    /// Appends `delta` to the call's arguments.
82    pub fn new(tool_call_id: impl Into<ToolCallId>, delta: impl Into<String>) -> Self {
83        Self {
84            base: BaseEvent::default(),
85            tool_call_id: tool_call_id.into(),
86            delta: delta.into(),
87            subagent_run_id: None,
88        }
89    }
90}
91
92/// Closes a tool call. The arguments are complete.
93#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
94#[serde(rename_all = "camelCase")]
95#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
96#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
97pub struct ToolCallEndEvent {
98    /// Timestamp and raw provider event.
99    #[serde(flatten)]
100    pub base: BaseEvent,
101    /// The call being closed.
102    pub tool_call_id: ToolCallId,
103    /// The subagent that produced this event; absent means the parent agent.
104    /// A JSON `null` is rejected — see [`crate::event::subagent`].
105    #[serde(
106        default,
107        deserialize_with = "crate::serde_util::reject_null",
108        skip_serializing_if = "Option::is_none"
109    )]
110    pub subagent_run_id: Option<SubagentRunId>,
111}
112
113impl ToolCallEndEvent {
114    /// Closes the call.
115    pub fn new(tool_call_id: impl Into<ToolCallId>) -> Self {
116        Self {
117            base: BaseEvent::default(),
118            tool_call_id: tool_call_id.into(),
119            subagent_run_id: None,
120        }
121    }
122}
123
124/// A self-contained tool-call update: start, args and end folded into one.
125#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
126#[serde(rename_all = "camelCase")]
127#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
128#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
129pub struct ToolCallChunkEvent {
130    /// Timestamp and raw provider event.
131    #[serde(flatten)]
132    pub base: BaseEvent,
133    /// The call this chunk belongs to.
134    #[serde(default, skip_serializing_if = "Option::is_none")]
135    pub tool_call_id: Option<ToolCallId>,
136    /// Name of the tool being called.
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub tool_call_name: Option<String>,
139    /// The assistant message that requested the call. A JSON `null` reads as
140    /// absent, as in [`ToolCallStartEvent`].
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub parent_message_id: Option<MessageId>,
143    /// The argument-JSON fragment.
144    #[serde(default, skip_serializing_if = "Option::is_none")]
145    pub delta: Option<String>,
146    /// The subagent that produced this event; absent means the parent agent.
147    /// A JSON `null` is rejected — see [`crate::event::subagent`].
148    #[serde(
149        default,
150        deserialize_with = "crate::serde_util::reject_null",
151        skip_serializing_if = "Option::is_none"
152    )]
153    pub subagent_run_id: Option<SubagentRunId>,
154}
155
156impl ToolCallChunkEvent {
157    /// Builds a chunk carrying a call id, tool name and argument delta.
158    pub fn new(
159        tool_call_id: Option<ToolCallId>,
160        tool_call_name: Option<String>,
161        delta: Option<String>,
162    ) -> Self {
163        Self {
164            base: BaseEvent::default(),
165            tool_call_id,
166            tool_call_name,
167            parent_message_id: None,
168            delta,
169            subagent_run_id: None,
170        }
171    }
172}
173
174/// The result of a tool call, as a message appended to the thread.
175#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
176#[serde(rename_all = "camelCase")]
177#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
178#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
179pub struct ToolCallResultEvent {
180    /// Timestamp and raw provider event.
181    #[serde(flatten)]
182    pub base: BaseEvent,
183    /// Id of the tool message carrying the result.
184    pub message_id: MessageId,
185    /// The call this result answers.
186    pub tool_call_id: ToolCallId,
187    /// The result, already rendered to a string.
188    pub content: String,
189    /// Always `"tool"` when present.
190    #[serde(default, skip_serializing_if = "Option::is_none")]
191    pub role: Option<ToolResultRole>,
192    /// The subagent that *executed* the call; absent means the parent agent.
193    /// A JSON `null` is rejected — see [`crate::event::subagent`]. Attributed
194    /// independently of the call it answers, on purpose: a frontend-executed
195    /// tool, or a supervisor running a call on a subagent's behalf, produces
196    /// a result whose owner is not the caller.
197    #[serde(
198        default,
199        deserialize_with = "crate::serde_util::reject_null",
200        skip_serializing_if = "Option::is_none"
201    )]
202    pub subagent_run_id: Option<SubagentRunId>,
203}
204
205impl ToolCallResultEvent {
206    /// Reports the result of a call.
207    pub fn new(
208        message_id: impl Into<MessageId>,
209        tool_call_id: impl Into<ToolCallId>,
210        content: impl Into<String>,
211    ) -> Self {
212        Self {
213            base: BaseEvent::default(),
214            message_id: message_id.into(),
215            tool_call_id: tool_call_id.into(),
216            content: content.into(),
217            role: None,
218            subagent_run_id: None,
219        }
220    }
221}
222
223/// The single role a [`ToolCallResultEvent`] may declare.
224#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
225#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
226#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
227pub enum ToolResultRole {
228    /// `"tool"`.
229    #[default]
230    #[serde(rename = "tool")]
231    Tool,
232}