Skip to main content

aether_core/events/
tool_event.rs

1use super::SubAgentProgressPayload;
2use llm::types::IsoString;
3use llm::{ChatMessage, ContentBlock, MessageId, ToolCallError, ToolCallRequest, ToolCallResult, ToolDefinition};
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use utils::display_meta::ToolResultMeta;
7
8/// Tool call lifecycle events.
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
10#[serde(tag = "type", rename_all = "snake_case")]
11pub enum ToolEvent {
12    /// The LLM requested a tool call; arguments may still be streaming.
13    Call { request: ToolCallRequest },
14    /// A chunk of streamed tool call arguments.
15    CallUpdate { tool_call_id: String, chunk: String },
16    /// The tool began executing.
17    ExecutionStarted { tool_id: String, tool_name: String },
18    /// Progress reported by an executing tool.
19    Progress { request: ToolCallRequest, progress: f64, total: Option<f64>, message: Option<String> },
20    /// Progress from a sub-agent spawned by this tool call, carrying the child's own event.
21    SubAgentProgress { request: ToolCallRequest, payload: Box<SubAgentProgressPayload> },
22    /// An executing tool refreshed how it should be displayed.
23    DisplayUpdate { request: ToolCallRequest, meta: ToolResultMeta },
24    /// A background task was created by a tool call response.
25    TaskCreated { request: ToolCallRequest, task_id: String, status_message: Option<String> },
26    /// The background task reported a new status.
27    TaskStatus { request: ToolCallRequest, task_id: String, status: String, status_message: Option<String> },
28    /// The background task completed successfully.
29    TaskCompleted {
30        request: ToolCallRequest,
31        task_id: String,
32        result: ToolCallResult,
33        result_meta: Option<ToolResultMeta>,
34    },
35    /// The background task failed.
36    TaskFailed { request: ToolCallRequest, task_id: String, error: ToolCallError },
37    /// The background task was cancelled.
38    TaskCancelled { request: ToolCallRequest, task_id: String },
39    /// The tool completed successfully.
40    Result { result: ToolCallResult, result_meta: Option<ToolResultMeta> },
41    /// The tool failed.
42    Error { error: ToolCallError },
43    /// The set of available tool definitions changed.
44    DefinitionsUpdated { tools: Vec<ToolDefinition> },
45}
46
47impl ToolEvent {
48    /// The context message describing a terminal background-task event, or
49    /// `None` for every other event.
50    pub fn task_context_message(&self) -> Option<ChatMessage> {
51        let (request, task_id, status, body) = match self {
52            Self::TaskCompleted { request, task_id, result, .. } => {
53                (request, task_id, "completed", result.result.as_str())
54            }
55            Self::TaskFailed { request, task_id, error } => (request, task_id, "failed", error.error.as_str()),
56            Self::TaskCancelled { request, task_id } => (request, task_id, "cancelled", TASK_CANCELLED_BODY),
57            _ => return None,
58        };
59        Some(task_result_message(request, task_id, status, body))
60    }
61}
62
63#[derive(Debug, Clone, PartialEq)]
64pub struct TaskOutcome {
65    pub request: ToolCallRequest,
66    pub task_id: String,
67    pub state: TaskOutcomeState,
68}
69
70#[derive(Debug, Clone, PartialEq)]
71pub enum TaskOutcomeState {
72    Completed { result: ToolCallResult, result_meta: Option<ToolResultMeta> },
73    Failed { error: ToolCallError },
74    Cancelled,
75}
76
77impl TaskOutcome {
78    pub fn context_message(&self) -> ChatMessage {
79        let (status, body) = self.status_body();
80        task_result_message(&self.request, &self.task_id, status, body)
81    }
82
83    pub fn content_blocks(&self) -> Vec<ContentBlock> {
84        let (status, body) = self.status_body();
85        task_result_content(&self.request, &self.task_id, status, body)
86    }
87
88    fn status_body(&self) -> (&str, &str) {
89        match &self.state {
90            TaskOutcomeState::Completed { result, .. } => ("completed", result.result.as_str()),
91            TaskOutcomeState::Failed { error } => ("failed", error.error.as_str()),
92            TaskOutcomeState::Cancelled => ("cancelled", TASK_CANCELLED_BODY),
93        }
94    }
95}
96
97impl From<TaskOutcome> for ToolEvent {
98    fn from(outcome: TaskOutcome) -> Self {
99        let TaskOutcome { request, task_id, state } = outcome;
100        match state {
101            TaskOutcomeState::Completed { result, result_meta } => {
102                Self::TaskCompleted { request, task_id, result, result_meta }
103            }
104            TaskOutcomeState::Failed { error } => Self::TaskFailed { request, task_id, error },
105            TaskOutcomeState::Cancelled => Self::TaskCancelled { request, task_id },
106        }
107    }
108}
109
110pub fn task_created_result(request: &ToolCallRequest, task_id: &str) -> ToolCallResult {
111    ToolCallResult {
112        id: request.id.clone(),
113        name: request.name.clone(),
114        arguments: request.arguments.clone(),
115        result: format!(
116            "This tool is running as a background task, id: {task_id}. The result will be automatically injected into context when it completes, you may continue working."
117        ),
118    }
119}
120
121const TASK_CANCELLED_BODY: &str = "The background task was cancelled and will not produce a result.";
122
123fn task_result_message(request: &ToolCallRequest, task_id: &str, status: &str, body: &str) -> ChatMessage {
124    ChatMessage::User {
125        message_id: MessageId::task_result(task_id),
126        content: task_result_content(request, task_id, status, body),
127        timestamp: IsoString::now(),
128    }
129}
130
131fn task_result_content(request: &ToolCallRequest, task_id: &str, status: &str, body: &str) -> Vec<ContentBlock> {
132    let content = format!(
133        "<task-result task-id=\"{}\" tool=\"{}\" status=\"{status}\">{}</task-result>",
134        escape_xml(task_id),
135        escape_xml(&request.name),
136        escape_xml(body),
137    );
138    vec![ContentBlock::text(content)]
139}
140
141fn escape_xml(value: &str) -> String {
142    value.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;").replace('"', "&quot;").replace('\'', "&apos;")
143}