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