systemprompt_agent/services/a2a_server/processing/task_builder/
builders.rs1use super::TaskBuilder;
2use super::helpers::{content_to_json, extract_text_from_content};
3use crate::models::a2a::{
4 Artifact, DataPart, Message, Part, Task, TaskState, TaskStatus, TextPart,
5};
6use crate::services::mcp::parse_tool_response;
7use serde_json::json;
8use systemprompt_identifiers::{ContextId, MessageId, TaskId};
9use systemprompt_models::a2a::{ArtifactMetadata, TaskMetadata, agent_names};
10use systemprompt_models::{CallToolResult, ToolCall};
11
12pub fn build_completed_task(
13 task_id: TaskId,
14 context_id: ContextId,
15 response_text: String,
16 user_message: Message,
17 artifacts: Vec<Artifact>,
18) -> Task {
19 TaskBuilder::new(context_id)
20 .with_task_id(task_id)
21 .with_state(TaskState::Completed)
22 .with_response_text(response_text)
23 .with_user_message(user_message)
24 .with_artifacts(artifacts)
25 .build()
26}
27
28pub fn build_canceled_task(task_id: TaskId, context_id: ContextId) -> Task {
29 TaskBuilder::new(context_id)
30 .with_task_id(task_id)
31 .with_state(TaskState::Canceled)
32 .with_response_text("Task was canceled.".to_string())
33 .build()
34}
35
36pub fn build_mock_task(task_id: TaskId) -> Task {
37 let mock_context_id = ContextId::generate();
38 TaskBuilder::new(mock_context_id)
39 .with_task_id(task_id)
40 .with_state(TaskState::Completed)
41 .with_response_text("Task completed successfully.".to_string())
42 .build()
43}
44
45pub fn build_submitted_task(
46 task_id: TaskId,
47 context_id: ContextId,
48 user_message: Message,
49 agent_name: &str,
50) -> Task {
51 Task {
52 id: task_id,
53 context_id,
54 kind: "task".to_string(),
55 status: TaskStatus {
56 state: TaskState::Submitted,
57 message: None,
58 timestamp: Some(chrono::Utc::now()),
59 },
60 history: Some(vec![user_message]),
61 artifacts: None,
62 metadata: Some(TaskMetadata::new_agent_message(agent_name.to_string())),
63 }
64}
65
66#[derive(Debug)]
67pub struct BuildMultiturnTaskParams {
68 pub context_id: ContextId,
69 pub task_id: TaskId,
70 pub user_message: Message,
71 pub tool_calls: Vec<ToolCall>,
72 pub tool_results: Vec<CallToolResult>,
73 pub final_response: String,
74 pub total_iterations: usize,
75}
76
77pub fn build_multiturn_task(params: BuildMultiturnTaskParams) -> Task {
78 let BuildMultiturnTaskParams {
79 context_id,
80 task_id,
81 user_message,
82 tool_calls,
83 tool_results,
84 final_response,
85 total_iterations,
86 } = params;
87 let ctx_id = context_id;
88
89 let history = build_history(BuildHistoryParams {
90 ctx_id: &ctx_id,
91 task_id: &task_id,
92 user_message,
93 tool_calls: &tool_calls,
94 tool_results: &tool_results,
95 final_response: &final_response,
96 });
97
98 let artifacts = build_artifacts(&ctx_id, &task_id, &tool_calls, &tool_results);
99
100 Task {
101 id: task_id.clone(),
102 context_id: ctx_id.clone(),
103 kind: "task".to_string(),
104 status: TaskStatus {
105 state: TaskState::Completed,
106 message: Some(Message {
107 role: "agent".to_string(),
108 parts: vec![Part::Text(TextPart {
109 text: final_response,
110 })],
111 id: MessageId::generate(),
112 task_id: Some(task_id),
113 context_id: ctx_id,
114 kind: "message".to_string(),
115 metadata: None,
116 extensions: None,
117 reference_task_ids: None,
118 }),
119 timestamp: Some(chrono::Utc::now()),
120 },
121 history: Some(history),
122 artifacts: if artifacts.is_empty() {
123 None
124 } else {
125 Some(artifacts)
126 },
127 metadata: Some(
128 TaskMetadata::new_agent_message(agent_names::SYSTEM.to_string())
129 .with_extension("total_iterations".to_string(), json!(total_iterations))
130 .with_extension("total_tools_called".to_string(), json!(tool_calls.len())),
131 ),
132 }
133}
134
135struct BuildHistoryParams<'a> {
136 ctx_id: &'a ContextId,
137 task_id: &'a TaskId,
138 user_message: Message,
139 tool_calls: &'a [ToolCall],
140 tool_results: &'a [CallToolResult],
141 final_response: &'a str,
142}
143
144fn build_history(params: BuildHistoryParams<'_>) -> Vec<Message> {
145 let BuildHistoryParams {
146 ctx_id,
147 task_id,
148 user_message,
149 tool_calls,
150 tool_results,
151 final_response,
152 } = params;
153 let mut history = Vec::new();
154 history.push(user_message);
155
156 let mut iteration = 1;
157 let mut call_idx = 0;
158
159 while call_idx < tool_calls.len() {
160 let iteration_calls: Vec<_> = tool_calls
161 .iter()
162 .skip(call_idx)
163 .take_while(|_| call_idx < tool_calls.len())
164 .cloned()
165 .collect();
166
167 if iteration_calls.is_empty() {
168 break;
169 }
170
171 history.push(Message {
172 role: "agent".to_string(),
173 parts: vec![Part::Text(TextPart {
174 text: format!("Executing {} tool(s)...", iteration_calls.len()),
175 })],
176 id: MessageId::generate(),
177 task_id: Some(task_id.clone()),
178 context_id: ctx_id.clone(),
179 kind: "message".to_string(),
180 metadata: Some(json!({
181 "iteration": iteration,
182 "tool_calls": iteration_calls.iter().map(|tc| {
183 json!({"id": tc.ai_tool_call_id.as_ref(), "name": tc.name})
184 }).collect::<Vec<_>>()
185 })),
186 extensions: None,
187 reference_task_ids: None,
188 });
189
190 let results_text = iteration_calls
191 .iter()
192 .enumerate()
193 .filter_map(|(idx, call)| {
194 let result_idx = call_idx + idx;
195 tool_results.get(result_idx).map(|r| {
196 let content_text = extract_text_from_content(&r.content);
197 format!("Tool '{}' result: {}", call.name, content_text)
198 })
199 })
200 .collect::<Vec<_>>()
201 .join("\n");
202
203 history.push(Message {
204 role: "user".to_string(),
205 parts: vec![Part::Text(TextPart { text: results_text })],
206 id: MessageId::generate(),
207 task_id: Some(task_id.clone()),
208 context_id: ctx_id.clone(),
209 kind: "message".to_string(),
210 metadata: Some(json!({
211 "iteration": iteration,
212 "tool_results": true
213 })),
214 extensions: None,
215 reference_task_ids: None,
216 });
217
218 call_idx += iteration_calls.len();
219 iteration += 1;
220 }
221
222 history.push(Message {
223 role: "agent".to_string(),
224 parts: vec![Part::Text(TextPart {
225 text: final_response.to_string(),
226 })],
227 id: MessageId::generate(),
228 task_id: Some(task_id.clone()),
229 context_id: ctx_id.clone(),
230 kind: "message".to_string(),
231 metadata: Some(json!({
232 "iteration": iteration,
233 "final_synthesis": true
234 })),
235 extensions: None,
236 reference_task_ids: None,
237 });
238
239 history
240}
241
242fn build_artifacts(
243 ctx_id: &ContextId,
244 task_id: &TaskId,
245 tool_calls: &[ToolCall],
246 tool_results: &[CallToolResult],
247) -> Vec<Artifact> {
248 tool_results
249 .iter()
250 .enumerate()
251 .filter_map(|(idx, result)| {
252 let tool_call = tool_calls.get(idx)?;
253 let tool_name = &tool_call.name;
254 let call_id = tool_call.ai_tool_call_id.as_ref();
255 let is_error = result.is_error?;
256
257 let structured_content = result.structured_content.as_ref()?;
258 let parsed = parse_tool_response(structured_content)
259 .map_err(|e| {
260 tracing::debug!(tool_name = %tool_name, error = %e, "Failed to parse tool response, skipping artifact");
261 e
262 })
263 .ok()?;
264
265 let mut data_map = serde_json::Map::new();
266 data_map.insert("call_id".to_string(), json!(call_id));
267 data_map.insert("tool_name".to_string(), json!(tool_name));
268 data_map.insert("output".to_string(), content_to_json(&result.content));
269 data_map.insert(
270 "status".to_string(),
271 json!(if is_error { "error" } else { "success" }),
272 );
273
274 Some(Artifact {
275 id: parsed.artifact_id,
276 name: Some(format!("tool_execution_{}", idx + 1)),
277 description: Some(format!("Result from tool: {tool_name}")),
278 parts: vec![Part::Data(DataPart { data: data_map })],
279 extensions: vec![],
280 metadata: ArtifactMetadata::new(
281 "tool_execution".to_string(),
282 ctx_id.clone(),
283 task_id.clone(),
284 )
285 .with_mcp_execution_id(call_id.to_string())
286 .with_tool_name(tool_name.clone())
287 .with_execution_index(idx),
288 })
289 })
290 .collect()
291}