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