Skip to main content

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

1use 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}