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}