Skip to main content

aether_core/events/
tool_event.rs

1use super::SubAgentProgressPayload;
2use llm::{ChatMessage, ContentBlock, MessageId, ToolCallError, ToolCallRequest, ToolCallResult, ToolDefinition};
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use utils::display_meta::ToolResultMeta;
6
7/// Tool call lifecycle events.
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
9#[serde(tag = "type", rename_all = "snake_case")]
10pub enum ToolEvent {
11    InputStarted {
12        id: String,
13        name: String,
14    },
15    InputDelta {
16        id: String,
17        chunk: String,
18    },
19    Call {
20        request: ToolCallRequest,
21    },
22    Progress {
23        request: ToolCallRequest,
24        progress: f64,
25        total: Option<f64>,
26        message: Option<String>,
27    },
28    SubAgentProgress {
29        request: ToolCallRequest,
30        payload: Box<SubAgentProgressPayload>,
31    },
32    DisplayUpdate {
33        request: ToolCallRequest,
34        meta: ToolResultMeta,
35    },
36    TaskCreated {
37        request: ToolCallRequest,
38        task_id: String,
39        status_message: Option<String>,
40    },
41    TaskStatus {
42        request: ToolCallRequest,
43        task_id: String,
44        status: String,
45        status_message: Option<String>,
46    },
47    TaskCompleted {
48        request: ToolCallRequest,
49        task_id: String,
50        result: ToolCallResult,
51        result_meta: Option<ToolResultMeta>,
52    },
53    TaskFailed {
54        request: ToolCallRequest,
55        task_id: String,
56        error: ToolCallError,
57    },
58    TaskCancelled {
59        request: ToolCallRequest,
60        task_id: String,
61    },
62    Result {
63        result: ToolCallResult,
64        result_meta: Option<ToolResultMeta>,
65    },
66    Error {
67        error: ToolCallError,
68    },
69    DefinitionsUpdated {
70        tools: Vec<ToolDefinition>,
71    },
72}
73
74impl ToolEvent {
75    /// The context message describing a terminal background-task event, or
76    /// `None` for every other event.
77    pub fn task_context_message(&self) -> Option<ChatMessage> {
78        let (request, task_id, status, body) = match self {
79            Self::TaskCompleted { request, task_id, result, .. } => {
80                (request, task_id, "completed", result.result.as_str())
81            }
82            Self::TaskFailed { request, task_id, error } => (request, task_id, "failed", error.error.as_str()),
83            Self::TaskCancelled { request, task_id } => (request, task_id, "cancelled", TASK_CANCELLED_BODY),
84            _ => return None,
85        };
86        Some(task_result_message(request, task_id, status, body))
87    }
88}
89
90#[derive(Debug, Clone, PartialEq)]
91pub struct TaskOutcome {
92    pub request: ToolCallRequest,
93    pub task_id: String,
94    pub state: TaskOutcomeState,
95}
96
97#[derive(Debug, Clone, PartialEq)]
98pub enum TaskOutcomeState {
99    Completed { result: ToolCallResult, result_meta: Option<ToolResultMeta> },
100    Failed { error: ToolCallError },
101    Cancelled,
102}
103
104impl TaskOutcome {
105    pub fn context_message(&self) -> ChatMessage {
106        let (status, body) = self.status_body();
107        task_result_message(&self.request, &self.task_id, status, body)
108    }
109
110    pub fn content_blocks(&self) -> Vec<ContentBlock> {
111        let (status, body) = self.status_body();
112        task_result_content(&self.request, &self.task_id, status, body)
113    }
114
115    fn status_body(&self) -> (&str, &str) {
116        match &self.state {
117            TaskOutcomeState::Completed { result, .. } => ("completed", result.result.as_str()),
118            TaskOutcomeState::Failed { error } => ("failed", error.error.as_str()),
119            TaskOutcomeState::Cancelled => ("cancelled", TASK_CANCELLED_BODY),
120        }
121    }
122}
123
124impl From<TaskOutcome> for ToolEvent {
125    fn from(outcome: TaskOutcome) -> Self {
126        let TaskOutcome { request, task_id, state } = outcome;
127        match state {
128            TaskOutcomeState::Completed { result, result_meta } => {
129                Self::TaskCompleted { request, task_id, result, result_meta }
130            }
131            TaskOutcomeState::Failed { error } => Self::TaskFailed { request, task_id, error },
132            TaskOutcomeState::Cancelled => Self::TaskCancelled { request, task_id },
133        }
134    }
135}
136
137pub fn task_created_result(request: &ToolCallRequest, task_id: &str) -> ToolCallResult {
138    ToolCallResult {
139        id: request.id.clone(),
140        name: request.name.clone(),
141        arguments: request.arguments.clone(),
142        result: format!(
143            "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."
144        ),
145    }
146}
147
148const TASK_CANCELLED_BODY: &str = "The background task was cancelled and will not produce a result.";
149
150fn task_result_message(request: &ToolCallRequest, task_id: &str, status: &str, body: &str) -> ChatMessage {
151    ChatMessage::user_with_id(MessageId::task_result(task_id), task_result_content(request, task_id, status, body))
152}
153
154fn task_result_content(request: &ToolCallRequest, task_id: &str, status: &str, body: &str) -> Vec<ContentBlock> {
155    let content = format!(
156        "<task-result task-id=\"{}\" tool=\"{}\" status=\"{status}\">{}</task-result>",
157        escape_xml(task_id),
158        escape_xml(&request.name),
159        escape_xml(body),
160    );
161    vec![ContentBlock::text(content)]
162}
163
164fn escape_xml(value: &str) -> String {
165    value.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;").replace('"', "&quot;").replace('\'', "&apos;")
166}