1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::BTreeSet;
4
5use crate::agent_events::{ToolCallErrorCategory, ToolCallStatus, ToolExecutor};
6use crate::tool_annotations::{SideEffectLevel, ToolAnnotations};
7
8use super::manifest::{BindingManifest, BindingManifestEntry};
9
10pub const COMPOSITION_EXECUTION_SCHEMA_VERSION: u32 = 1;
11
12#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum CompositionFailureCategory {
18 UnsupportedLanguage,
20 SchemaValidation,
22 PolicyDenied,
25 ChildToolError,
27 ExecutionError,
29 Timeout,
31 Cancelled,
33 Unknown,
35}
36
37impl CompositionFailureCategory {
38 pub const ALL: [Self; 8] = [
39 Self::UnsupportedLanguage,
40 Self::SchemaValidation,
41 Self::PolicyDenied,
42 Self::ChildToolError,
43 Self::ExecutionError,
44 Self::Timeout,
45 Self::Cancelled,
46 Self::Unknown,
47 ];
48
49 pub fn as_str(self) -> &'static str {
50 match self {
51 Self::UnsupportedLanguage => "unsupported_language",
52 Self::SchemaValidation => "schema_validation",
53 Self::PolicyDenied => "policy_denied",
54 Self::ChildToolError => "child_tool_error",
55 Self::ExecutionError => "execution_error",
56 Self::Timeout => "timeout",
57 Self::Cancelled => "cancelled",
58 Self::Unknown => "unknown",
59 }
60 }
61}
62
63#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
65#[serde(default)]
66pub struct CompositionRunEnvelope {
67 pub run_id: String,
69 pub language: String,
71 pub snippet_hash: String,
73 pub binding_manifest_hash: String,
75 pub requested_side_effect_ceiling: SideEffectLevel,
77 pub stdout: Option<String>,
79 pub stderr: Option<String>,
81 pub artifacts: Vec<Value>,
83 pub result: Option<Value>,
85 pub failure_category: Option<CompositionFailureCategory>,
87 pub error: Option<String>,
89 pub duration_ms: Option<u64>,
91 pub metadata: Value,
93}
94
95impl Default for CompositionRunEnvelope {
96 fn default() -> Self {
97 Self {
98 run_id: String::new(),
99 language: String::new(),
100 snippet_hash: String::new(),
101 binding_manifest_hash: String::new(),
102 requested_side_effect_ceiling: SideEffectLevel::ReadOnly,
103 stdout: None,
104 stderr: None,
105 artifacts: Vec::new(),
106 result: None,
107 failure_category: None,
108 error: None,
109 duration_ms: None,
110 metadata: Value::Object(serde_json::Map::new()),
111 }
112 }
113}
114
115impl CompositionRunEnvelope {
116 pub fn read_only(
117 run_id: impl Into<String>,
118 language: impl Into<String>,
119 snippet_hash: impl Into<String>,
120 binding_manifest_hash: impl Into<String>,
121 ) -> Self {
122 Self {
123 run_id: run_id.into(),
124 language: language.into(),
125 snippet_hash: snippet_hash.into(),
126 binding_manifest_hash: binding_manifest_hash.into(),
127 requested_side_effect_ceiling: SideEffectLevel::ReadOnly,
128 ..Self::default()
129 }
130 }
131}
132
133#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
138#[serde(default)]
139pub struct CompositionChildCall {
140 pub run_id: String,
141 pub tool_call_id: String,
142 pub tool_name: String,
143 pub operation_index: u64,
144 pub annotations: Option<ToolAnnotations>,
145 pub requested_side_effect_level: SideEffectLevel,
146 pub policy_context: Value,
147 pub raw_input: Value,
148}
149
150impl Default for CompositionChildCall {
151 fn default() -> Self {
152 Self {
153 run_id: String::new(),
154 tool_call_id: String::new(),
155 tool_name: String::new(),
156 operation_index: 0,
157 annotations: None,
158 requested_side_effect_level: SideEffectLevel::None,
159 policy_context: Value::Object(serde_json::Map::new()),
160 raw_input: Value::Null,
161 }
162 }
163}
164
165#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
169#[serde(default)]
170pub struct CompositionChildResult {
171 pub run_id: String,
172 pub tool_call_id: String,
173 pub tool_name: String,
174 pub operation_index: u64,
175 pub status: ToolCallStatus,
176 pub raw_output: Option<Value>,
177 pub error: Option<String>,
178 pub error_category: Option<ToolCallErrorCategory>,
179 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub error_details: Option<Value>,
183 pub executor: Option<ToolExecutor>,
184 pub duration_ms: Option<u64>,
185 pub execution_duration_ms: Option<u64>,
186 pub attempt: u32,
187 pub retry_attempts: u32,
188 pub retry_errors: Vec<String>,
189 pub retry_delays_ms: Vec<u64>,
190}
191
192impl Default for CompositionChildResult {
193 fn default() -> Self {
194 Self {
195 run_id: String::new(),
196 tool_call_id: String::new(),
197 tool_name: String::new(),
198 operation_index: 0,
199 status: ToolCallStatus::Pending,
200 raw_output: None,
201 error: None,
202 error_category: None,
203 error_details: None,
204 executor: None,
205 duration_ms: None,
206 execution_duration_ms: None,
207 attempt: 1,
208 retry_attempts: 0,
209 retry_errors: Vec::new(),
210 retry_delays_ms: Vec::new(),
211 }
212 }
213}
214
215#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
216#[serde(default)]
217pub struct CompositionExecutionLimits {
218 pub max_operations: u64,
219 pub timeout_ms: Option<u64>,
220 pub max_output_bytes: u64,
221 pub max_concurrent_operations: usize,
222 pub max_concurrent_per_server: usize,
223}
224
225impl Default for CompositionExecutionLimits {
226 fn default() -> Self {
227 Self {
228 max_operations: 64,
229 timeout_ms: Some(10_000),
230 max_output_bytes: 64 * 1024,
231 max_concurrent_operations: 16,
232 max_concurrent_per_server: 4,
233 }
234 }
235}
236
237#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
238#[serde(default)]
239pub struct CompositionRetryPolicy {
240 pub max_attempts: u32,
241 pub base_delay_ms: u64,
242 pub max_delay_ms: u64,
243 pub honor_retry_after: bool,
244}
245
246impl Default for CompositionRetryPolicy {
247 fn default() -> Self {
248 Self {
249 max_attempts: 3,
250 base_delay_ms: 100,
251 max_delay_ms: 2_000,
252 honor_retry_after: true,
253 }
254 }
255}
256
257#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
258#[serde(default)]
259pub struct CompositionMcpPolicy {
260 pub trusted_servers: BTreeSet<String>,
261 pub trust_annotations: bool,
262 pub retry: CompositionRetryPolicy,
263 pub call_timeout_ms: Option<u64>,
264}
265
266#[derive(Clone, Debug, Serialize, Deserialize)]
267#[serde(default)]
268pub struct CompositionExecutionRequest {
269 pub session_id: Option<String>,
270 pub run_id: String,
271 pub language: String,
272 pub snippet: String,
273 pub manifest: BindingManifest,
274 pub requested_side_effect_ceiling: SideEffectLevel,
275 pub limits: CompositionExecutionLimits,
276 pub mcp_policy: CompositionMcpPolicy,
277 pub metadata: Value,
278}
279
280impl Default for CompositionExecutionRequest {
281 fn default() -> Self {
282 Self {
283 session_id: None,
284 run_id: String::new(),
285 language: "harn".to_string(),
286 snippet: String::new(),
287 manifest: BindingManifest::default(),
288 requested_side_effect_ceiling: SideEffectLevel::ReadOnly,
289 limits: CompositionExecutionLimits::default(),
290 mcp_policy: CompositionMcpPolicy::default(),
291 metadata: Value::Object(serde_json::Map::new()),
292 }
293 }
294}
295
296#[derive(Clone, Debug, Serialize, Deserialize)]
297pub struct CompositionExecutionReport {
298 pub schema_version: u32,
299 pub ok: bool,
300 pub run: CompositionRunEnvelope,
301 pub child_calls: Vec<CompositionChildCall>,
302 pub child_results: Vec<CompositionChildResult>,
303 pub summary: String,
304}
305
306#[derive(Clone, Debug, Serialize, Deserialize)]
307pub struct CompositionToolOutput {
308 pub value: Option<Value>,
309 pub error: Option<String>,
310 pub error_category: Option<ToolCallErrorCategory>,
311 pub executor: Option<ToolExecutor>,
312}
313
314impl CompositionToolOutput {
315 pub fn ok(value: Value) -> Self {
316 Self {
317 value: Some(value),
318 error: None,
319 error_category: None,
320 executor: Some(ToolExecutor::HarnBuiltin),
321 }
322 }
323
324 pub fn error(message: impl Into<String>, category: ToolCallErrorCategory) -> Self {
325 Self {
326 value: None,
327 error: Some(message.into()),
328 error_category: Some(category),
329 executor: Some(ToolExecutor::HarnBuiltin),
330 }
331 }
332}
333
334#[async_trait::async_trait]
335pub trait CompositionToolHost: Send + Sync {
336 async fn call(&self, binding: &BindingManifestEntry, input: Value) -> CompositionToolOutput;
337}