supercode_interchange/orchestration/codec/
loss.rs1use std::fs;
10use std::path::Path;
11
12use schemars::JsonSchema;
13use serde::{Deserialize, Serialize};
14
15use crate::orchestration::{Orchestration, SpeakerPolicy};
16use crate::Result;
17
18pub const LOSS_REPORT_FILE: &str = "supercode-export-report.json";
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
23#[serde(rename_all = "snake_case")]
24pub enum Carry {
25 Exact,
27 Folded,
29 Dropped,
31}
32
33#[derive(Debug, Clone, Copy)]
35pub struct FeatureCarry {
36 pub feature: &'static str,
38 pub carry: Carry,
40 pub how: &'static str,
42}
43
44pub const HERMES_FEATURES: &[FeatureCarry] = &[
46 FeatureCarry {
47 feature: "agents",
48 carry: Carry::Folded,
49 how: "every agent folds into its profile; Hermes sees one doer per profile",
50 },
51 FeatureCarry {
52 feature: "routes.agent",
53 carry: Carry::Folded,
54 how: "a route names its agent's profile",
55 },
56 FeatureCarry {
57 feature: "bindings.agent",
58 carry: Carry::Folded,
59 how: "a session is keyed by its profile (agent:<profile>:…)",
60 },
61 FeatureCarry {
62 feature: "conversations.speaker",
63 carry: Carry::Dropped,
64 how: "Hermes has one agent per surface; a multi-agent speaker policy is not written",
65 },
66 FeatureCarry {
67 feature: "conversations.participants",
68 carry: Carry::Dropped,
69 how: "Hermes keeps no participant list",
70 },
71 FeatureCarry {
72 feature: "transfers",
73 carry: Carry::Dropped,
74 how: "Hermes has no transfer between agents",
75 },
76 FeatureCarry {
77 feature: "handoffs",
78 carry: Carry::Exact,
79 how: "the session row's handoff_* columns",
80 },
81 FeatureCarry {
82 feature: "usage",
83 carry: Carry::Dropped,
84 how: "usage stays in the native folder; Hermes keeps its own token counts",
85 },
86 FeatureCarry {
87 feature: "budgets",
88 carry: Carry::Dropped,
89 how: "Hermes has no budget gate",
90 },
91 FeatureCarry {
92 feature: "jobs.session_target",
93 carry: Carry::Dropped,
94 how: "Hermes runs each fire in its own session; the key rides unread in jobs.json",
95 },
96];
97
98pub const OPENCLAW_FEATURES: &[FeatureCarry] = &[
100 FeatureCarry {
101 feature: "agents",
102 carry: Carry::Exact,
103 how: "agents.* entries",
104 },
105 FeatureCarry {
106 feature: "routes.agent",
107 carry: Carry::Exact,
108 how: "bindings[].agentId",
109 },
110 FeatureCarry {
111 feature: "bindings.agent",
112 carry: Carry::Exact,
113 how: "the session key's agent segment",
114 },
115 FeatureCarry {
116 feature: "conversations.speaker",
117 carry: Carry::Dropped,
118 how: "OpenClaw routes a surface to one agent; a multi-agent speaker policy is not written",
119 },
120 FeatureCarry {
121 feature: "conversations.participants",
122 carry: Carry::Dropped,
123 how: "OpenClaw keeps no participant list",
124 },
125 FeatureCarry {
126 feature: "transfers",
127 carry: Carry::Dropped,
128 how: "OpenClaw has no transfer between agents",
129 },
130 FeatureCarry {
131 feature: "handoffs",
132 carry: Carry::Dropped,
133 how: "OpenClaw has no session handoff",
134 },
135 FeatureCarry {
136 feature: "usage",
137 carry: Carry::Dropped,
138 how: "usage stays in the native folder",
139 },
140 FeatureCarry {
141 feature: "budgets",
142 carry: Carry::Dropped,
143 how: "OpenClaw has no budget gate",
144 },
145 FeatureCarry {
146 feature: "jobs.session_target",
147 carry: Carry::Exact,
148 how: "cron jobs' sessionTarget",
149 },
150];
151
152#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
154pub struct LossItem {
155 pub feature: String,
157 pub carry: Carry,
159 pub item: String,
161 pub how: String,
163}
164
165#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
167pub struct LossReport {
168 pub format: String,
170 pub items: Vec<LossItem>,
172}
173
174pub fn loss_report(format: &str, o: &Orchestration, table: &[FeatureCarry]) -> LossReport {
176 let mut items = Vec::new();
177 let mut push = |feature: &str, item: String| {
178 if let Some(f) = table.iter().find(|f| f.feature == feature) {
179 if f.carry != Carry::Exact {
180 items.push(LossItem {
181 feature: feature.into(),
182 carry: f.carry,
183 item,
184 how: f.how.into(),
185 });
186 }
187 }
188 };
189 for (name, decl) in &o.agents {
190 push("agents", format!("{name} (profile {})", decl.profile));
191 }
192 for (pname, profile) in &o.profiles {
193 for route in &profile.routes {
194 if route.agent != *pname && !o.profiles.contains_key(&route.agent) {
195 push(
196 "routes.agent",
197 format!("route to {} in {pname}", route.agent),
198 );
199 }
200 }
201 for (slot, binding) in &profile.bindings {
202 if binding
203 .agent
204 .as_deref()
205 .is_some_and(|a| a != pname.as_str())
206 {
207 push("bindings.agent", format!("{pname} {slot}"));
208 }
209 }
210 if !profile.budgets.is_empty() {
211 push(
212 "budgets",
213 format!("{} budget(s) on {pname}", profile.budgets.len()),
214 );
215 }
216 for (id, job) in &profile.jobs {
217 if let Some(target) = &job.session_target {
218 push("jobs.session_target", format!("{pname} job {id}: {target}"));
219 }
220 }
221 }
222 for (id, conversation) in &o.conversations {
223 if !matches!(conversation.speaker, SpeakerPolicy::Addressed { .. }) {
224 push("conversations.speaker", id.clone());
225 }
226 if !conversation.participants.is_empty() {
227 push(
228 "conversations.participants",
229 format!("{id}: {} participant(s)", conversation.participants.len()),
230 );
231 }
232 }
233 for t in &o.transfers {
234 push(
235 "transfers",
236 format!("{} → {} in {}", t.from, t.to, t.conversation),
237 );
238 }
239 for h in &o.handoffs {
240 push("handoffs", format!("{} → {}", h.session, h.to));
241 }
242 for (session, usage) in &o.usage {
243 push(
244 "usage",
245 format!("{session}: {} record(s)", usage.usage.len()),
246 );
247 }
248 LossReport {
249 format: format.into(),
250 items,
251 }
252}
253
254pub fn write_loss_report(dest: &Path, report: &LossReport) -> Result<()> {
256 fs::create_dir_all(dest)?;
257 fs::write(
258 dest.join(LOSS_REPORT_FILE),
259 serde_json::to_string_pretty(report).unwrap() + "\n",
260 )?;
261 Ok(())
262}