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//!
7//! Copyright (c) systemprompt.io — Business Source License 1.1.
8//! See <https://systemprompt.io> for licensing details.
9
10use 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}