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