Skip to main content

systemprompt_agent/services/a2a_server/processing/task_builder/
builders.rs

1//! Concrete [`Task`] builders for each completion path.
2//!
3//! Thin constructors over [`TaskBuilder`] (completed, canceled, mock,
4//! submitted) plus [`build_multiturn_task`], which reconstructs the full
5//! message history and per-tool artifacts for a multi-turn agentic run.
6
7use 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}