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#[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 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('&', "&").replace('<', "<").replace('>', ">").replace('"', """).replace('\'', "'")
166}