systemprompt_agent/services/a2a_server/processing/task_builder/
builders.rs1use super::TaskBuilder;
11use super::helpers::content_to_json;
12use super::history::{BuildHistoryParams, build_history};
13use crate::models::a2a::{
14 Artifact, DataPart, Message, MessageRole, Part, Task, TaskState, TaskStatus, TextPart,
15};
16use crate::services::mcp::parse_wire_result;
17use serde_json::json;
18use systemprompt_identifiers::{ContextId, MessageId, TaskId};
19use systemprompt_models::a2a::{ArtifactMetadata, TaskMetadata, agent_names};
20use systemprompt_models::{CallToolResult, ToolCall};
21
22pub fn build_completed_task(
23 task_id: TaskId,
24 context_id: ContextId,
25 response_text: String,
26 user_message: Message,
27 artifacts: Vec<Artifact>,
28) -> Task {
29 TaskBuilder::new(context_id)
30 .with_task_id(task_id)
31 .with_state(TaskState::Completed)
32 .with_response_text(response_text)
33 .with_user_message(user_message)
34 .with_artifacts(artifacts)
35 .build()
36}
37
38pub fn build_canceled_task(task_id: TaskId, context_id: ContextId) -> Task {
39 TaskBuilder::new(context_id)
40 .with_task_id(task_id)
41 .with_state(TaskState::Canceled)
42 .with_response_text("Task was canceled.".to_owned())
43 .build()
44}
45
46pub fn build_mock_task(task_id: TaskId) -> Task {
47 TaskBuilder::new(ContextId::derived_from_task(&task_id))
48 .with_task_id(task_id)
49 .with_state(TaskState::Completed)
50 .with_response_text("Task completed successfully.".to_owned())
51 .build()
52}
53
54pub fn build_submitted_task(
55 task_id: TaskId,
56 context_id: ContextId,
57 user_message: Message,
58 agent_name: &str,
59) -> Task {
60 Task {
61 id: task_id,
62 context_id,
63 status: TaskStatus {
64 state: TaskState::Submitted,
65 message: None,
66 timestamp: Some(chrono::Utc::now()),
67 },
68 history: Some(vec![user_message]),
69 artifacts: None,
70 metadata: Some(TaskMetadata::new_agent_message(agent_name.to_owned())),
71 created_at: Some(chrono::Utc::now()),
72 last_modified: Some(chrono::Utc::now()),
73 }
74}
75
76#[derive(Debug)]
77pub struct BuildMultiturnTaskParams {
78 pub context_id: ContextId,
79 pub task_id: TaskId,
80 pub user_message: Message,
81 pub tool_calls: Vec<ToolCall>,
82 pub tool_results: Vec<CallToolResult>,
83 pub final_response: String,
84 pub total_iterations: usize,
85}
86
87pub fn build_multiturn_task(params: BuildMultiturnTaskParams) -> Task {
88 let BuildMultiturnTaskParams {
89 context_id,
90 task_id,
91 user_message,
92 tool_calls,
93 tool_results,
94 final_response,
95 total_iterations,
96 } = params;
97 let ctx_id = context_id;
98
99 let history = build_history(BuildHistoryParams {
100 ctx_id: &ctx_id,
101 task_id: &task_id,
102 user_message,
103 tool_calls: &tool_calls,
104 tool_results: &tool_results,
105 final_response: &final_response,
106 });
107
108 let artifacts = build_artifacts(&ctx_id, &task_id, &tool_calls, &tool_results);
109
110 Task {
111 id: task_id.clone(),
112 context_id: ctx_id.clone(),
113 status: TaskStatus {
114 state: TaskState::Completed,
115 message: Some(Message {
116 role: MessageRole::Agent,
117 parts: vec![Part::Text(TextPart {
118 text: final_response,
119 })],
120 message_id: MessageId::generate(),
121 task_id: Some(task_id),
122 context_id: ctx_id,
123 metadata: None,
124 extensions: None,
125 reference_task_ids: None,
126 }),
127 timestamp: Some(chrono::Utc::now()),
128 },
129 history: Some(history),
130 artifacts: if artifacts.is_empty() {
131 None
132 } else {
133 Some(artifacts)
134 },
135 metadata: Some(
136 TaskMetadata::new_agent_message(agent_names::SYSTEM.to_owned())
137 .with_extension("total_iterations".to_owned(), json!(total_iterations))
138 .with_extension("total_tools_called".to_owned(), json!(tool_calls.len())),
139 ),
140 created_at: Some(chrono::Utc::now()),
141 last_modified: Some(chrono::Utc::now()),
142 }
143}
144
145fn build_artifacts(
146 ctx_id: &ContextId,
147 task_id: &TaskId,
148 tool_calls: &[ToolCall],
149 tool_results: &[CallToolResult],
150) -> Vec<Artifact> {
151 tool_results
152 .iter()
153 .enumerate()
154 .filter_map(|(idx, result)| {
155 let tool_call = tool_calls.get(idx)?;
156 let tool_name = &tool_call.name;
157 let call_id = tool_call.ai_tool_call_id.as_ref();
158 let is_error = result.is_error?;
159
160 let parsed = parse_wire_result(result)
161 .map_err(|e| {
162 tracing::debug!(tool_name = %tool_name, error = %e, "Failed to parse tool response, skipping artifact");
163 e
164 })
165 .ok()?;
166
167 let mut data_map = serde_json::Map::new();
168 data_map.insert("call_id".to_owned(), json!(call_id));
169 data_map.insert("tool_name".to_owned(), json!(tool_name));
170 data_map.insert("output".to_owned(), content_to_json(&result.content));
171 data_map.insert(
172 "status".to_owned(),
173 json!(if is_error { "error" } else { "success" }),
174 );
175
176 Some(Artifact {
177 id: parsed.artifact_id,
178 title: Some(format!("tool_execution_{}", idx + 1)),
179 description: Some(format!("Result from tool: {tool_name}")),
180 parts: vec![Part::Data(DataPart { data: data_map })],
181 extensions: vec![],
182 metadata: ArtifactMetadata::new(
183 "tool_execution".to_owned(),
184 ctx_id.clone(),
185 task_id.clone(),
186 )
187 .with_mcp_execution_id(call_id.to_owned())
188 .with_tool_name(tool_name.clone())
189 .with_execution_index(idx),
190 })
191 })
192 .collect()
193}