Skip to main content

harn_vm/composition/
crystallization.rs

1use std::collections::BTreeSet;
2
3use serde_json::Value;
4
5use super::events::composition_report_events;
6use super::types::CompositionExecutionReport;
7
8pub fn composition_crystallization_trace(
9    report: &CompositionExecutionReport,
10    options: &Value,
11) -> Value {
12    let trace_id = options
13        .get("id")
14        .and_then(Value::as_str)
15        .map(ToOwned::to_owned)
16        .unwrap_or_else(|| format!("composition_{}", report.run.run_id));
17    let mut capabilities = BTreeSet::new();
18    for call in &report.child_calls {
19        if let Some(annotations) = &call.annotations {
20            for (domain, ops) in &annotations.capabilities {
21                for op in ops {
22                    capabilities.insert(format!("{domain}.{op}"));
23                }
24            }
25        }
26    }
27    let parent_parameters = serde_json::json!({
28        "language": report.run.language,
29        "snippet_hash": report.run.snippet_hash,
30        "binding_manifest_hash": report.run.binding_manifest_hash,
31        "requested_side_effect_ceiling": report.run.requested_side_effect_ceiling,
32    });
33    let mut actions = vec![serde_json::json!({
34        "id": "composition_parent",
35        "kind": "composition_run",
36        "name": "execute_composition",
37        "inputs": parent_parameters,
38        "parameters": parent_parameters,
39        "output": report.run.result,
40        "observed_output": report.run.result,
41        "capabilities": capabilities.into_iter().collect::<Vec<_>>(),
42        "side_effects": [],
43        "duration_ms": report.run.duration_ms.unwrap_or(0),
44        "deterministic": true,
45        "fuzzy": false,
46        "metadata": {
47            "source_kind": "composition_parent_run",
48            "composition_run_id": report.run.run_id,
49            "composition_schema_version": report.schema_version,
50            "child_count": report.child_calls.len(),
51            "ok": report.ok,
52            "failure_category": report.run.failure_category,
53        }
54    })];
55    actions.extend(report.child_calls.iter().map(|call| {
56        let result = report
57            .child_results
58            .iter()
59            .find(|result| result.tool_call_id == call.tool_call_id);
60        let capabilities = call
61            .annotations
62            .as_ref()
63            .map(|annotations| {
64                annotations
65                    .capabilities
66                    .iter()
67                    .flat_map(|(domain, ops)| ops.iter().map(move |op| format!("{domain}.{op}")))
68                    .collect::<Vec<_>>()
69            })
70            .unwrap_or_default();
71        serde_json::json!({
72            "id": format!("composition_child_{}", call.operation_index),
73            "kind": "tool_call",
74            "name": call.tool_name,
75            "inputs": call.raw_input,
76            "parameters": call.raw_input,
77            "output": result.and_then(|result| result.raw_output.clone()),
78            "observed_output": result.and_then(|result| result.raw_output.clone()),
79            "capabilities": capabilities,
80            "side_effects": [],
81            "duration_ms": result.and_then(|result| result.duration_ms).unwrap_or(0),
82            "deterministic": true,
83            "fuzzy": false,
84            "metadata": {
85                "source_kind": "composition_child_call",
86                "composition_run_id": report.run.run_id,
87                "composition_tool_call_id": call.tool_call_id,
88                "requested_side_effect_level": call.requested_side_effect_level,
89                "annotations": call.annotations,
90                "policy_context": call.policy_context,
91                "status": result.map(|result| result.status),
92                "error_category": result.and_then(|result| result.error_category),
93            }
94        })
95    }));
96    let replay_run = composition_replay_run(report, &trace_id);
97    serde_json::json!({
98        "version": 1,
99        "id": trace_id,
100        "source": "composition_run",
101        "source_hash": report.run.snippet_hash,
102        "workflow_id": options.get("workflow_id").and_then(Value::as_str).unwrap_or("composition_candidate"),
103        "flow": {
104            "trace_id": report.run.run_id,
105            "agent_run_id": options.get("agent_run_id").and_then(Value::as_str),
106            "transcript_ref": options.get("transcript_ref").and_then(Value::as_str),
107        },
108        "actions": actions,
109        "replay_run": replay_run,
110        "replay_allowlist": [
111            {
112                "path": "/run_id",
113                "reason": "run ids are allocated per execution"
114            },
115            {
116                "path": "/effect_receipts/*/run_id",
117                "reason": "composition receipts retain source run lineage"
118            },
119            {
120                "path": "/effect_receipts/*/tool_call_id",
121                "reason": "composition child call ids include the source run id"
122            },
123            {
124                "path": "/policy_decisions/*/run_id",
125                "reason": "composition policy decisions retain source run lineage"
126            },
127            {
128                "path": "/policy_decisions/*/tool_call_id",
129                "reason": "composition policy decision ids include the source run id"
130            }
131        ],
132        "metadata": {
133            "source_kind": "composition_run",
134            "composition_schema_version": report.schema_version,
135            "run_id": report.run.run_id,
136            "snippet_hash": report.run.snippet_hash,
137            "binding_manifest_hash": report.run.binding_manifest_hash,
138            "requested_side_effect_ceiling": report.run.requested_side_effect_ceiling,
139            "ok": report.ok,
140            "failure_category": report.run.failure_category,
141            "child_count": report.child_calls.len(),
142        },
143    })
144}
145
146fn composition_replay_run(report: &CompositionExecutionReport, trace_id: &str) -> Value {
147    let event_log_entries = composition_report_events(trace_id, report)
148        .into_iter()
149        .filter_map(|event| serde_json::to_value(event).ok())
150        .collect::<Vec<_>>();
151    let mut effect_receipts = vec![serde_json::json!({
152        "kind": "composition_parent",
153        "run_id": report.run.run_id,
154        "schema_version": report.schema_version,
155        "snippet_hash": report.run.snippet_hash,
156        "binding_manifest_hash": report.run.binding_manifest_hash,
157        "requested_side_effect_ceiling": report.run.requested_side_effect_ceiling,
158        "ok": report.ok,
159        "failure_category": report.run.failure_category,
160        "result": report.run.result,
161        "stdout": report.run.stdout,
162    })];
163    let mut policy_decisions = Vec::new();
164    for call in &report.child_calls {
165        let result = report
166            .child_results
167            .iter()
168            .find(|result| result.tool_call_id == call.tool_call_id);
169        effect_receipts.push(serde_json::json!({
170            "kind": "composition_child",
171            "run_id": report.run.run_id,
172            "tool_call_id": call.tool_call_id,
173            "tool_name": call.tool_name,
174            "operation_index": call.operation_index,
175            "requested_side_effect_level": call.requested_side_effect_level,
176            "input": call.raw_input,
177            "status": result.map(|result| result.status),
178            "error_category": result.and_then(|result| result.error_category),
179            "output": result.and_then(|result| result.raw_output.clone()),
180        }));
181        policy_decisions.push(serde_json::json!({
182            "kind": "composition_child_policy",
183            "run_id": report.run.run_id,
184            "tool_call_id": call.tool_call_id,
185            "tool_name": call.tool_name,
186            "requested_side_effect_level": call.requested_side_effect_level,
187            "policy_context": call.policy_context,
188        }));
189    }
190    serde_json::json!({
191        "run_id": report.run.run_id,
192        "event_log_entries": event_log_entries,
193        "effect_receipts": effect_receipts,
194        "policy_decisions": policy_decisions,
195    })
196}