Skip to main content

harn_vm/composition/
types.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use crate::agent_events::{ToolCallErrorCategory, ToolCallStatus, ToolExecutor};
5use crate::tool_annotations::{SideEffectLevel, ToolAnnotations};
6
7use super::manifest::{BindingManifest, BindingManifestEntry};
8
9pub const COMPOSITION_EXECUTION_SCHEMA_VERSION: u32 = 1;
10
11/// Stable failure taxonomy for a composition run. Tool-level failures stay on
12/// [`CompositionChildResult`]; this classifies why the parent composition
13/// itself failed or stopped.
14#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum CompositionFailureCategory {
17    /// The snippet language is unknown or not enabled by the current host.
18    UnsupportedLanguage,
19    /// The snippet or manifest did not validate before execution.
20    SchemaValidation,
21    /// Capability policy rejected the requested side-effect ceiling or a child
22    /// operation.
23    PolicyDenied,
24    /// A child binding returned an error.
25    ChildToolError,
26    /// The executor failed before it could attribute the error to a child call.
27    ExecutionError,
28    /// The run exceeded its time or step budget.
29    Timeout,
30    /// The host or caller cancelled the run.
31    Cancelled,
32    /// Fallback when a producer cannot classify the failure.
33    Unknown,
34}
35
36impl CompositionFailureCategory {
37    pub const ALL: [Self; 8] = [
38        Self::UnsupportedLanguage,
39        Self::SchemaValidation,
40        Self::PolicyDenied,
41        Self::ChildToolError,
42        Self::ExecutionError,
43        Self::Timeout,
44        Self::Cancelled,
45        Self::Unknown,
46    ];
47
48    pub fn as_str(self) -> &'static str {
49        match self {
50            Self::UnsupportedLanguage => "unsupported_language",
51            Self::SchemaValidation => "schema_validation",
52            Self::PolicyDenied => "policy_denied",
53            Self::ChildToolError => "child_tool_error",
54            Self::ExecutionError => "execution_error",
55            Self::Timeout => "timeout",
56            Self::Cancelled => "cancelled",
57            Self::Unknown => "unknown",
58        }
59    }
60}
61
62/// Identity and policy envelope for one composition run.
63#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
64#[serde(default)]
65pub struct CompositionRunEnvelope {
66    /// Runtime-unique id used to correlate child calls and terminal events.
67    pub run_id: String,
68    /// Snippet frontend (`harn`, `typescript`, `javascript`, ...).
69    pub language: String,
70    /// `sha256:<hex>` digest over the language and snippet bytes.
71    pub snippet_hash: String,
72    /// `sha256:<hex>` digest over the binding manifest shown to the model.
73    pub binding_manifest_hash: String,
74    /// Highest side-effect level requested by the parent run.
75    pub requested_side_effect_ceiling: SideEffectLevel,
76    /// Captured stdout-like text emitted by the composition executor.
77    pub stdout: Option<String>,
78    /// Captured stderr-like text emitted by the composition executor.
79    pub stderr: Option<String>,
80    /// Artifact descriptors/handles emitted by the composition executor.
81    pub artifacts: Vec<Value>,
82    /// Structured result returned by the snippet.
83    pub result: Option<Value>,
84    /// Parent-run failure class, absent for successful finishes.
85    pub failure_category: Option<CompositionFailureCategory>,
86    /// Human-readable parent-run error, absent for successful finishes.
87    pub error: Option<String>,
88    /// Runtime wall-clock duration when a producer has measured it.
89    pub duration_ms: Option<u64>,
90    /// Forward-compatible producer metadata. Consumers must ignore unknown keys.
91    pub metadata: Value,
92}
93
94impl Default for CompositionRunEnvelope {
95    fn default() -> Self {
96        Self {
97            run_id: String::new(),
98            language: String::new(),
99            snippet_hash: String::new(),
100            binding_manifest_hash: String::new(),
101            requested_side_effect_ceiling: SideEffectLevel::ReadOnly,
102            stdout: None,
103            stderr: None,
104            artifacts: Vec::new(),
105            result: None,
106            failure_category: None,
107            error: None,
108            duration_ms: None,
109            metadata: Value::Object(serde_json::Map::new()),
110        }
111    }
112}
113
114impl CompositionRunEnvelope {
115    pub fn read_only(
116        run_id: impl Into<String>,
117        language: impl Into<String>,
118        snippet_hash: impl Into<String>,
119        binding_manifest_hash: impl Into<String>,
120    ) -> Self {
121        Self {
122            run_id: run_id.into(),
123            language: language.into(),
124            snippet_hash: snippet_hash.into(),
125            binding_manifest_hash: binding_manifest_hash.into(),
126            requested_side_effect_ceiling: SideEffectLevel::ReadOnly,
127            ..Self::default()
128        }
129    }
130}
131
132/// Child tool call made by a composition snippet. This is intentionally close
133/// to `AgentEvent::ToolCall`, but includes parent-run correlation and the
134/// policy/annotation context the composition executor used when deciding
135/// whether the call was allowed.
136#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
137#[serde(default)]
138pub struct CompositionChildCall {
139    pub run_id: String,
140    pub tool_call_id: String,
141    pub tool_name: String,
142    pub operation_index: u64,
143    pub annotations: Option<ToolAnnotations>,
144    pub requested_side_effect_level: SideEffectLevel,
145    pub policy_context: Value,
146    pub raw_input: Value,
147}
148
149impl Default for CompositionChildCall {
150    fn default() -> Self {
151        Self {
152            run_id: String::new(),
153            tool_call_id: String::new(),
154            tool_name: String::new(),
155            operation_index: 0,
156            annotations: None,
157            requested_side_effect_level: SideEffectLevel::None,
158            policy_context: Value::Object(serde_json::Map::new()),
159            raw_input: Value::Null,
160        }
161    }
162}
163
164/// Terminal or intermediate result for a child binding operation. Consumers
165/// should pair this with the corresponding [`CompositionChildCall`] to recover
166/// the policy annotations and requested side-effect level for the operation.
167#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
168#[serde(default)]
169pub struct CompositionChildResult {
170    pub run_id: String,
171    pub tool_call_id: String,
172    pub tool_name: String,
173    pub operation_index: u64,
174    pub status: ToolCallStatus,
175    pub raw_output: Option<Value>,
176    pub error: Option<String>,
177    pub error_category: Option<ToolCallErrorCategory>,
178    pub executor: Option<ToolExecutor>,
179    pub duration_ms: Option<u64>,
180    pub execution_duration_ms: Option<u64>,
181}
182
183impl Default for CompositionChildResult {
184    fn default() -> Self {
185        Self {
186            run_id: String::new(),
187            tool_call_id: String::new(),
188            tool_name: String::new(),
189            operation_index: 0,
190            status: ToolCallStatus::Pending,
191            raw_output: None,
192            error: None,
193            error_category: None,
194            executor: None,
195            duration_ms: None,
196            execution_duration_ms: None,
197        }
198    }
199}
200
201#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
202#[serde(default)]
203pub struct CompositionExecutionLimits {
204    pub max_operations: u64,
205    pub timeout_ms: Option<u64>,
206    pub max_output_bytes: u64,
207}
208
209impl Default for CompositionExecutionLimits {
210    fn default() -> Self {
211        Self {
212            max_operations: 64,
213            timeout_ms: Some(10_000),
214            max_output_bytes: 64 * 1024,
215        }
216    }
217}
218
219#[derive(Clone, Debug, Serialize, Deserialize)]
220#[serde(default)]
221pub struct CompositionExecutionRequest {
222    pub session_id: Option<String>,
223    pub run_id: String,
224    pub language: String,
225    pub snippet: String,
226    pub manifest: BindingManifest,
227    pub requested_side_effect_ceiling: SideEffectLevel,
228    pub limits: CompositionExecutionLimits,
229    pub metadata: Value,
230}
231
232impl Default for CompositionExecutionRequest {
233    fn default() -> Self {
234        Self {
235            session_id: None,
236            run_id: String::new(),
237            language: "harn".to_string(),
238            snippet: String::new(),
239            manifest: BindingManifest::default(),
240            requested_side_effect_ceiling: SideEffectLevel::ReadOnly,
241            limits: CompositionExecutionLimits::default(),
242            metadata: Value::Object(serde_json::Map::new()),
243        }
244    }
245}
246
247#[derive(Clone, Debug, Serialize, Deserialize)]
248pub struct CompositionExecutionReport {
249    pub schema_version: u32,
250    pub ok: bool,
251    pub run: CompositionRunEnvelope,
252    pub child_calls: Vec<CompositionChildCall>,
253    pub child_results: Vec<CompositionChildResult>,
254    pub summary: String,
255}
256
257#[derive(Clone, Debug, Serialize, Deserialize)]
258pub struct CompositionToolOutput {
259    pub value: Option<Value>,
260    pub error: Option<String>,
261    pub error_category: Option<ToolCallErrorCategory>,
262    pub executor: Option<ToolExecutor>,
263}
264
265impl CompositionToolOutput {
266    pub fn ok(value: Value) -> Self {
267        Self {
268            value: Some(value),
269            error: None,
270            error_category: None,
271            executor: Some(ToolExecutor::HarnBuiltin),
272        }
273    }
274
275    pub fn error(message: impl Into<String>, category: ToolCallErrorCategory) -> Self {
276        Self {
277            value: None,
278            error: Some(message.into()),
279            error_category: Some(category),
280            executor: Some(ToolExecutor::HarnBuiltin),
281        }
282    }
283}
284
285#[async_trait::async_trait(?Send)]
286pub trait CompositionToolHost {
287    async fn call(&self, binding: &BindingManifestEntry, input: Value) -> CompositionToolOutput;
288}