Skip to main content

agentledger/
lib.rs

1use std::collections::HashMap;
2use std::fmt;
3use std::fs;
4use std::path::{Path, PathBuf};
5use std::process::{Command, Stdio};
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::time::{SystemTime, UNIX_EPOCH};
8
9static ID_COUNTER: AtomicU64 = AtomicU64::new(1);
10
11pub type State = HashMap<String, Value>;
12
13pub const MODEL_EVIDENCE_SCHEMA_VERSION: &str = "agentledger.model.evidence.v1";
14
15#[derive(Clone, Debug, PartialEq)]
16pub enum Value {
17    Null,
18    Bool(bool),
19    Number(f64),
20    String(String),
21    Object(State),
22    Array(Vec<Value>),
23}
24
25impl Default for Value {
26    fn default() -> Self {
27        Value::Null
28    }
29}
30
31impl From<&str> for Value {
32    fn from(value: &str) -> Self {
33        Value::String(value.to_string())
34    }
35}
36
37impl From<String> for Value {
38    fn from(value: String) -> Self {
39        Value::String(value)
40    }
41}
42
43impl From<bool> for Value {
44    fn from(value: bool) -> Self {
45        Value::Bool(value)
46    }
47}
48
49impl From<i64> for Value {
50    fn from(value: i64) -> Self {
51        Value::Number(value as f64)
52    }
53}
54
55impl From<f64> for Value {
56    fn from(value: f64) -> Self {
57        Value::Number(value)
58    }
59}
60
61#[derive(Clone, Debug)]
62pub struct Run {
63    pub run_id: String,
64    pub session_id: String,
65    pub status: String,
66    pub state: State,
67    pub state_version: u64,
68    pub created_at: f64,
69    pub updated_at: f64,
70}
71
72#[derive(Clone, Debug)]
73pub struct Step {
74    pub step_id: String,
75    pub run_id: String,
76    pub session_id: String,
77    pub status: String,
78    pub owner: Option<String>,
79    pub lease_token: Option<String>,
80    pub lease_until: Option<f64>,
81    pub attempt: u64,
82    pub state_version: u64,
83    pub checkpoint_id: Option<String>,
84    pub last_error_type: Option<String>,
85    pub last_error: Option<String>,
86    pub cancelled_at: Option<f64>,
87    pub created_at: f64,
88    pub updated_at: f64,
89}
90
91#[derive(Clone, Debug)]
92pub struct StepClaim {
93    pub run_id: String,
94    pub session_id: String,
95    pub step_id: String,
96    pub attempt: u64,
97    pub lease_token: String,
98    pub state_version: u64,
99    pub lease_until: f64,
100}
101
102#[derive(Clone, Debug)]
103pub struct Event {
104    pub event_id: String,
105    pub run_id: String,
106    pub session_id: Option<String>,
107    pub step_id: Option<String>,
108    pub seq: u64,
109    pub event_type: String,
110    pub timestamp: f64,
111    pub agent_role: Option<String>,
112    pub state_version: Option<u64>,
113    pub causal_token: Option<String>,
114    pub payload_hash: String,
115    pub payload_ref: String,
116    pub payload: State,
117}
118
119#[derive(Clone, Debug)]
120pub struct ToolLedgerEntry {
121    pub ledger_id: String,
122    pub run_id: String,
123    pub session_id: String,
124    pub step_id: String,
125    pub tool_name: String,
126    pub tool_version: String,
127    pub tool_call_id: String,
128    pub idempotency_key: String,
129    pub causal_token: String,
130    pub request_hash: String,
131    pub request_ref: String,
132    pub status: String,
133    pub external_id: Option<String>,
134    pub response_hash: Option<String>,
135    pub response_ref: Option<String>,
136    pub error_type: Option<String>,
137    pub response: Option<Value>,
138    pub created_at: f64,
139    pub updated_at: f64,
140}
141
142#[derive(Clone, Debug)]
143pub struct ApprovalRequest {
144    pub approval_id: String,
145    pub approval_key: String,
146    pub run_id: String,
147    pub session_id: String,
148    pub step_id: String,
149    pub tool_name: String,
150    pub risk_level: String,
151    pub status: String,
152    pub reason: String,
153    pub request_hash: String,
154    pub request_ref: String,
155    pub requested_by: String,
156    pub approved_by: Option<String>,
157    pub decision_reason: Option<String>,
158    pub created_at: f64,
159    pub updated_at: f64,
160}
161
162#[derive(Clone, Debug)]
163pub struct CostRecord {
164    pub cost_id: String,
165    pub run_id: String,
166    pub session_id: String,
167    pub step_id: String,
168    pub category: String,
169    pub name: String,
170    pub amount: f64,
171    pub unit: String,
172    pub metadata: State,
173    pub created_at: f64,
174}
175
176#[derive(Clone, Debug, Default, PartialEq)]
177pub struct CostSummary {
178    pub tool_calls: f64,
179    pub model_tokens: f64,
180    pub total_usd: f64,
181    pub by_category: HashMap<String, f64>,
182}
183
184pub const MEDIA_SCHEMA_VERSION: &str = "agentledger.media.v0";
185pub const STREAM_SCHEMA_VERSION: &str = "agentledger.stream.v0";
186
187#[derive(Clone, Debug)]
188pub struct LocalBlobStore {
189    root: PathBuf,
190}
191
192impl LocalBlobStore {
193    pub fn open(root: impl AsRef<Path>) -> Result<Self> {
194        fs::create_dir_all(root.as_ref()).map_err(|err| RuntimeError(err.to_string()))?;
195        Ok(Self {
196            root: root.as_ref().to_path_buf(),
197        })
198    }
199
200    pub fn put_json(&self, value: &Value) -> Result<(String, String)> {
201        let encoded = encode_value(value);
202        let digest = stable_hash(&encoded);
203        let dir = self.root.join("sha256");
204        fs::create_dir_all(&dir).map_err(|err| RuntimeError(err.to_string()))?;
205        let path = dir.join(format!("{digest}.json"));
206        if !path.exists() {
207            let tmp = path.with_extension("json.tmp");
208            fs::write(&tmp, encoded).map_err(|err| RuntimeError(err.to_string()))?;
209            fs::rename(&tmp, &path).map_err(|err| RuntimeError(err.to_string()))?;
210        }
211        Ok((
212            format!("sha256:{digest}"),
213            format!("blob://sha256/{digest}.json"),
214        ))
215    }
216
217    pub fn get_json(&self, reference: &str) -> Result<Value> {
218        let prefix = "blob://sha256/";
219        if !reference.starts_with(prefix) {
220            return Err(RuntimeError(format!("unsupported blob ref: {reference}")));
221        }
222        let name = &reference[prefix.len()..];
223        if !name.ends_with(".json")
224            || name.contains("..")
225            || name.contains('/')
226            || name.contains('\\')
227        {
228            return Err(RuntimeError(format!("unsupported blob ref: {reference}")));
229        }
230        let body = fs::read_to_string(self.root.join("sha256").join(name))
231            .map_err(|err| RuntimeError(err.to_string()))?;
232        decode_value(&body)
233    }
234}
235
236#[derive(Clone, Debug)]
237pub struct Artifact {
238    pub artifact_id: String,
239    pub run_id: String,
240    pub step_id: Option<String>,
241    pub name: String,
242    pub blob_hash: String,
243    pub blob_ref: String,
244    pub metadata: State,
245    pub created_at: f64,
246}
247
248#[derive(Clone, Debug, Default)]
249pub struct MediaArtifactOptions {
250    pub uri: Option<String>,
251    pub content_ref: Option<String>,
252    pub media_metadata: State,
253    pub lineage: State,
254    pub derived_outputs: State,
255    pub metadata: State,
256}
257
258#[derive(Clone, Debug, Default)]
259pub struct StreamChunkRef {
260    pub stream_id: String,
261    pub chunk_id: String,
262    pub offset: Value,
263    pub content_ref: Option<String>,
264    pub content_hash: Option<String>,
265    pub sequence: Option<f64>,
266    pub event_time: Option<f64>,
267    pub metadata: State,
268}
269
270#[derive(Clone, Debug, Default)]
271pub struct StreamCheckpointOptions {
272    pub stream_id: String,
273    pub consumer_id: String,
274    pub offset: Value,
275    pub watermark: Option<Value>,
276    pub chunk: Option<StreamChunkRef>,
277    pub partial_result_ref: Option<String>,
278    pub backpressure: State,
279    pub metadata: State,
280}
281
282#[derive(Clone, Debug, Default)]
283pub struct BudgetLimits {
284    pub max_tool_calls: Option<f64>,
285    pub max_model_tokens: Option<f64>,
286    pub max_total_usd: Option<f64>,
287}
288
289#[derive(Debug, Clone)]
290pub struct RuntimeError(pub String);
291
292impl fmt::Display for RuntimeError {
293    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294        write!(f, "{}", self.0)
295    }
296}
297
298impl std::error::Error for RuntimeError {}
299
300pub type Result<T> = std::result::Result<T, RuntimeError>;
301pub type ToolFunc = Box<dyn Fn(State) -> Result<Value> + Send + Sync>;
302pub type AgentFunc = fn(&mut AgentContext, State) -> Result<()>;
303
304pub struct SandboxPolicy {
305    pub tool_name: String,
306    pub run_id: String,
307    pub step_id: String,
308    pub executor: String,
309    pub network: String,
310    pub filesystem: String,
311    pub timeout_seconds: u64,
312    pub extra: State,
313}
314
315pub struct SandboxResult {
316    pub ok: bool,
317    pub output: Value,
318    pub error: Option<String>,
319    pub metadata: State,
320}
321
322pub trait SandboxExecutor {
323    fn run_tool(&self, args: State, policy: &SandboxPolicy) -> SandboxResult;
324}
325
326pub struct DisabledSandboxExecutor;
327
328impl SandboxExecutor for DisabledSandboxExecutor {
329    fn run_tool(&self, _args: State, policy: &SandboxPolicy) -> SandboxResult {
330        let mut metadata = State::new();
331        metadata.insert("executor".to_string(), Value::String(policy.executor.clone()));
332        metadata.insert("isolation_level".to_string(), Value::String("none".to_string()));
333        metadata.insert("fail_closed".to_string(), Value::Bool(true));
334        SandboxResult {
335            ok: false,
336            output: Value::Null,
337            error: Some(format!("sandbox executor \"{}\" is disabled", policy.executor)),
338            metadata,
339        }
340    }
341}
342
343pub struct ToolSpec {
344    pub name: String,
345    pub version: String,
346    pub side_effect: String,
347    pub risk_level: String,
348    pub idempotency_required: bool,
349    pub approval_required: bool,
350    pub sandbox_required: bool,
351    pub sandbox_executor: String,
352    pub sandbox_policy: State,
353    pub input_schema: Option<Value>,
354    pub output_schema: Option<Value>,
355    pub func: ToolFunc,
356}
357
358impl ToolSpec {
359    pub fn new(name: &str, func: ToolFunc) -> Self {
360        Self {
361            name: name.to_string(),
362            version: "v1".to_string(),
363            side_effect: "none".to_string(),
364            risk_level: "low".to_string(),
365            idempotency_required: false,
366            approval_required: false,
367            sandbox_required: false,
368            sandbox_executor: String::new(),
369            sandbox_policy: State::new(),
370            input_schema: None,
371            output_schema: None,
372            func,
373        }
374    }
375
376    pub fn side_effect(mut self, side_effect: &str) -> Self {
377        self.side_effect = side_effect.to_string();
378        self
379    }
380
381    pub fn risk_level(mut self, risk_level: &str) -> Self {
382        self.risk_level = risk_level.to_string();
383        self
384    }
385
386    pub fn idempotency_required(mut self, required: bool) -> Self {
387        self.idempotency_required = required;
388        self
389    }
390
391    pub fn approval_required(mut self, required: bool) -> Self {
392        self.approval_required = required;
393        self
394    }
395
396    pub fn sandbox_required(mut self, required: bool) -> Self {
397        self.sandbox_required = required;
398        self
399    }
400
401    pub fn sandbox_executor(mut self, executor: &str) -> Self {
402        self.sandbox_executor = executor.to_string();
403        self
404    }
405
406    pub fn sandbox_policy(mut self, policy: State) -> Self {
407        self.sandbox_policy = policy;
408        self
409    }
410
411    pub fn input_schema(mut self, schema: Value) -> Self {
412        self.input_schema = Some(schema);
413        self
414    }
415
416    pub fn output_schema(mut self, schema: Value) -> Self {
417        self.output_schema = Some(schema);
418        self
419    }
420}
421
422#[derive(Default)]
423pub struct MemoryStore {
424    runs: HashMap<String, Run>,
425    steps: HashMap<String, Step>,
426    events: HashMap<String, Vec<Event>>,
427    tool_ledger: HashMap<String, ToolLedgerEntry>,
428    approval_requests: HashMap<String, ApprovalRequest>,
429    cost_records: HashMap<String, Vec<CostRecord>>,
430    artifacts: HashMap<String, Vec<Artifact>>,
431}
432
433impl MemoryStore {
434    pub fn new() -> Self {
435        Self::default()
436    }
437
438    pub fn save_to_path(&self, path: impl AsRef<Path>) -> Result<()> {
439        fs::write(path, encode_store(self)).map_err(|err| RuntimeError(err.to_string()))
440    }
441
442    pub fn load_from_path(path: impl AsRef<Path>) -> Result<Self> {
443        let body = fs::read_to_string(path).map_err(|err| RuntimeError(err.to_string()))?;
444        decode_store(&body)
445    }
446
447    pub fn create_run(&mut self, initial_state: State) -> (String, String) {
448        let run_id = new_id("run");
449        let session_id = new_id("sess");
450        let step_id = new_id("step");
451        let now = now_seconds();
452        self.runs.insert(
453            run_id.clone(),
454            Run {
455                run_id: run_id.clone(),
456                session_id: session_id.clone(),
457                status: "pending".to_string(),
458                state: initial_state.clone(),
459                state_version: 0,
460                created_at: now,
461                updated_at: now,
462            },
463        );
464        self.steps.insert(
465            step_id.clone(),
466            Step {
467                step_id: step_id.clone(),
468                run_id: run_id.clone(),
469                session_id: session_id.clone(),
470                status: "pending".to_string(),
471                owner: None,
472                lease_token: None,
473                lease_until: None,
474                attempt: 0,
475                state_version: 0,
476                checkpoint_id: None,
477                last_error_type: None,
478                last_error: None,
479                cancelled_at: None,
480                created_at: now,
481                updated_at: now,
482            },
483        );
484        let mut payload = State::new();
485        payload.insert("initial_state".to_string(), Value::Object(initial_state));
486        self.append_event(
487            &run_id,
488            Some(&session_id),
489            None,
490            "run_created",
491            payload,
492            None,
493            None,
494            None,
495        );
496        let mut step_payload = State::new();
497        step_payload.insert("step_id".to_string(), Value::String(step_id.clone()));
498        self.append_event(
499            &run_id,
500            Some(&session_id),
501            Some(&step_id),
502            "step_created",
503            step_payload,
504            None,
505            None,
506            None,
507        );
508        (run_id, step_id)
509    }
510
511    pub fn claim_step(
512        &mut self,
513        worker_id: &str,
514        run_id: &str,
515        lease_seconds: f64,
516    ) -> Result<StepClaim> {
517        let mut candidates: Vec<Step> = self
518            .steps
519            .values()
520            .filter(|step| {
521                (run_id.is_empty() || step.run_id == run_id)
522                    && (step.status == "pending" || step.status == "retry_scheduled")
523            })
524            .cloned()
525            .collect();
526        candidates.sort_by(|a, b| {
527            a.created_at
528                .partial_cmp(&b.created_at)
529                .unwrap()
530                .then(a.step_id.cmp(&b.step_id))
531        });
532        let candidate = candidates
533            .first()
534            .ok_or_else(|| RuntimeError("agentledger: no runnable step".to_string()))?;
535        let now = now_seconds();
536        let step = self.steps.get_mut(&candidate.step_id).unwrap();
537        step.status = "running".to_string();
538        step.owner = Some(worker_id.to_string());
539        step.lease_token = Some(new_id("lease"));
540        step.lease_until = Some(now + lease_seconds);
541        step.attempt += 1;
542        step.updated_at = now;
543        let claim = StepClaim {
544            run_id: step.run_id.clone(),
545            session_id: step.session_id.clone(),
546            step_id: step.step_id.clone(),
547            attempt: step.attempt,
548            lease_token: step.lease_token.clone().unwrap(),
549            state_version: step.state_version,
550            lease_until: step.lease_until.unwrap(),
551        };
552        let run = self.runs.get_mut(&claim.run_id).unwrap();
553        run.status = "running".to_string();
554        run.updated_at = now;
555        let mut payload = State::new();
556        payload.insert(
557            "worker_id".to_string(),
558            Value::String(worker_id.to_string()),
559        );
560        payload.insert(
561            "lease_token".to_string(),
562            Value::String(claim.lease_token.clone()),
563        );
564        payload.insert("attempt".to_string(), Value::Number(claim.attempt as f64));
565        self.append_event(
566            &claim.run_id,
567            Some(&claim.session_id),
568            Some(&claim.step_id),
569            "step_claimed",
570            payload,
571            None,
572            None,
573            None,
574        );
575        Ok(claim)
576    }
577
578    pub fn load_state(&self, run_id: &str) -> Result<(State, u64, String)> {
579        let run = self
580            .runs
581            .get(run_id)
582            .ok_or_else(|| RuntimeError(format!("run not found: {run_id}")))?;
583        Ok((run.state.clone(), run.state_version, run.session_id.clone()))
584    }
585
586    pub fn recover_expired_leases(&mut self) -> usize {
587        let now = now_seconds();
588        let ids: Vec<String> = self.steps.keys().cloned().collect();
589        let mut recovered = 0;
590        for step_id in ids {
591            let should_recover = self
592                .steps
593                .get(&step_id)
594                .map(|step| {
595                    step.status == "running" && step.lease_until.is_some_and(|until| until <= now)
596                })
597                .unwrap_or(false);
598            if !should_recover {
599                continue;
600            }
601            let (run_id, session_id, previous_owner, attempt) = {
602                let step = self.steps.get_mut(&step_id).unwrap();
603                let previous_owner = step.owner.clone().unwrap_or_default();
604                step.status = "retry_scheduled".to_string();
605                step.owner = None;
606                step.lease_token = None;
607                step.lease_until = None;
608                step.updated_at = now;
609                (
610                    step.run_id.clone(),
611                    step.session_id.clone(),
612                    previous_owner,
613                    step.attempt,
614                )
615            };
616            let run = self.runs.get_mut(&run_id).unwrap();
617            run.status = "retry_scheduled".to_string();
618            run.updated_at = now;
619            recovered += 1;
620            let mut payload = State::new();
621            payload.insert("previous_owner".to_string(), Value::String(previous_owner));
622            payload.insert("attempt".to_string(), Value::Number(attempt as f64));
623            self.append_event(
624                &run_id,
625                Some(&session_id),
626                Some(&step_id),
627                "lease_expired",
628                payload,
629                None,
630                None,
631                None,
632            );
633            let mut retry_payload = State::new();
634            retry_payload.insert("step_id".to_string(), Value::String(step_id.clone()));
635            retry_payload.insert(
636                "reason".to_string(),
637                Value::String("lease_expired".to_string()),
638            );
639            self.append_event(
640                &run_id,
641                Some(&session_id),
642                Some(&step_id),
643                "step_retry_scheduled",
644                retry_payload,
645                None,
646                None,
647                None,
648            );
649        }
650        recovered
651    }
652
653    pub fn cancel_run(&mut self, run_id: &str, reason: &str) -> Result<usize> {
654        let session_id = self
655            .runs
656            .get(run_id)
657            .ok_or_else(|| RuntimeError(format!("run not found: {run_id}")))?
658            .session_id
659            .clone();
660        let mut payload = State::new();
661        payload.insert("reason".to_string(), Value::String(reason.to_string()));
662        self.append_event(
663            run_id,
664            Some(&session_id),
665            None,
666            "run_cancel_requested",
667            payload,
668            None,
669            None,
670            None,
671        );
672        let ids: Vec<String> = self.steps.keys().cloned().collect();
673        let now = now_seconds();
674        let mut cancelled = 0;
675        for step_id in ids {
676            let should_cancel = self
677                .steps
678                .get(&step_id)
679                .map(|step| {
680                    step.run_id == run_id
681                        && !["completed", "failed", "cancelled"].contains(&step.status.as_str())
682                })
683                .unwrap_or(false);
684            if !should_cancel {
685                continue;
686            }
687            let step_session = {
688                let step = self.steps.get_mut(&step_id).unwrap();
689                step.status = "cancelled".to_string();
690                step.owner = None;
691                step.lease_token = None;
692                step.lease_until = None;
693                step.cancelled_at = Some(now);
694                step.updated_at = now;
695                step.session_id.clone()
696            };
697            cancelled += 1;
698            let mut step_payload = State::new();
699            step_payload.insert("reason".to_string(), Value::String(reason.to_string()));
700            self.append_event(
701                run_id,
702                Some(&step_session),
703                Some(&step_id),
704                "step_cancelled",
705                step_payload,
706                None,
707                None,
708                None,
709            );
710        }
711        let run = self.runs.get_mut(run_id).unwrap();
712        run.status = "cancelled".to_string();
713        run.updated_at = now;
714        let mut done_payload = State::new();
715        done_payload.insert("reason".to_string(), Value::String(reason.to_string()));
716        done_payload.insert(
717            "cancelled_steps".to_string(),
718            Value::Number(cancelled as f64),
719        );
720        self.append_event(
721            run_id,
722            Some(&session_id),
723            None,
724            "run_cancelled",
725            done_payload,
726            None,
727            None,
728            None,
729        );
730        Ok(cancelled)
731    }
732
733    pub fn commit_state_patch(
734        &mut self,
735        run_id: &str,
736        step_id: &str,
737        lease_token: &str,
738        base_version: u64,
739        patch: State,
740    ) -> Result<u64> {
741        self.validate_lease(step_id, lease_token)?;
742        let run = self
743            .runs
744            .get(run_id)
745            .ok_or_else(|| RuntimeError(format!("run not found: {run_id}")))?;
746        if run.state_version != base_version {
747            return Err(RuntimeError(format!(
748                "state version conflict: expected {base_version}, got {}",
749                run.state_version
750            )));
751        }
752        let now = now_seconds();
753        let new_version = base_version + 1;
754        let session_id = self.steps.get(step_id).unwrap().session_id.clone();
755        let run = self.runs.get_mut(run_id).unwrap();
756        run.state = merge_patch(&run.state, &patch);
757        run.state_version = new_version;
758        run.status = "completed".to_string();
759        run.updated_at = now;
760        let step = self.steps.get_mut(step_id).unwrap();
761        step.status = "completed".to_string();
762        step.state_version = new_version;
763        step.checkpoint_id = Some(format!("ckpt:{run_id}:{step_id}:{}", step.attempt));
764        step.updated_at = now;
765        let mut payload = State::new();
766        payload.insert("patch".to_string(), Value::Object(patch));
767        payload.insert(
768            "state_version".to_string(),
769            Value::Number(new_version as f64),
770        );
771        self.append_event(
772            run_id,
773            Some(&session_id),
774            Some(step_id),
775            "state_patch_committed",
776            payload,
777            None,
778            Some(new_version),
779            None,
780        );
781        let mut complete_payload = State::new();
782        complete_payload.insert("step_id".to_string(), Value::String(step_id.to_string()));
783        self.append_event(
784            run_id,
785            Some(&session_id),
786            Some(step_id),
787            "step_completed",
788            complete_payload,
789            None,
790            Some(new_version),
791            None,
792        );
793        Ok(new_version)
794    }
795
796    pub fn mark_retry(&mut self, run_id: &str, step_id: &str, error_type: &str, message: &str) {
797        let now = now_seconds();
798        let session_id = {
799            let step = self.steps.get_mut(step_id).unwrap();
800            step.status = "retry_scheduled".to_string();
801            step.owner = None;
802            step.lease_token = None;
803            step.lease_until = None;
804            step.last_error_type = Some(error_type.to_string());
805            step.last_error = Some(message.to_string());
806            step.updated_at = now;
807            step.session_id.clone()
808        };
809        let run = self.runs.get_mut(run_id).unwrap();
810        run.status = "retry_scheduled".to_string();
811        run.updated_at = now;
812        let mut classified = State::new();
813        classified.insert("error".to_string(), Value::String(message.to_string()));
814        classified.insert(
815            "error_type".to_string(),
816            Value::String(error_type.to_string()),
817        );
818        classified.insert("retryable".to_string(), Value::Bool(true));
819        classified.insert("source".to_string(), Value::String("agent".to_string()));
820        self.append_event(
821            run_id,
822            Some(&session_id),
823            Some(step_id),
824            "failure_classified",
825            classified,
826            None,
827            None,
828            None,
829        );
830        let mut payload = State::new();
831        payload.insert("error".to_string(), Value::String(message.to_string()));
832        payload.insert(
833            "error_type".to_string(),
834            Value::String(error_type.to_string()),
835        );
836        self.append_event(
837            run_id,
838            Some(&session_id),
839            Some(step_id),
840            "error_raised",
841            payload,
842            None,
843            None,
844            None,
845        );
846        let mut retry_payload = State::new();
847        retry_payload.insert("step_id".to_string(), Value::String(step_id.to_string()));
848        self.append_event(
849            run_id,
850            Some(&session_id),
851            Some(step_id),
852            "step_retry_scheduled",
853            retry_payload,
854            None,
855            None,
856            None,
857        );
858    }
859
860    pub fn mark_waiting_human(
861        &mut self,
862        run_id: &str,
863        step_id: &str,
864        reason: &str,
865        approval_id: &str,
866    ) {
867        let now = now_seconds();
868        let session_id = {
869            let step = self.steps.get_mut(step_id).unwrap();
870            step.status = "waiting_human".to_string();
871            step.owner = None;
872            step.lease_token = None;
873            step.lease_until = None;
874            step.last_error_type = Some("ApprovalRequired".to_string());
875            step.last_error = Some(reason.to_string());
876            step.updated_at = now;
877            step.session_id.clone()
878        };
879        let run = self.runs.get_mut(run_id).unwrap();
880        run.status = "waiting_human".to_string();
881        run.updated_at = now;
882        let mut payload = State::new();
883        payload.insert("reason".to_string(), Value::String(reason.to_string()));
884        payload.insert(
885            "approval_id".to_string(),
886            Value::String(approval_id.to_string()),
887        );
888        self.append_event(
889            run_id,
890            Some(&session_id),
891            Some(step_id),
892            "step_waiting_human",
893            payload,
894            None,
895            None,
896            None,
897        );
898    }
899
900    pub fn mark_failed(&mut self, run_id: &str, step_id: &str, error_type: &str, message: &str) {
901        let now = now_seconds();
902        let session_id = {
903            let step = self.steps.get_mut(step_id).unwrap();
904            step.status = "failed".to_string();
905            step.owner = None;
906            step.lease_token = None;
907            step.lease_until = None;
908            step.last_error_type = Some(error_type.to_string());
909            step.last_error = Some(message.to_string());
910            step.updated_at = now;
911            step.session_id.clone()
912        };
913        let run = self.runs.get_mut(run_id).unwrap();
914        run.status = "failed".to_string();
915        run.updated_at = now;
916        let mut classified = State::new();
917        classified.insert("error".to_string(), Value::String(message.to_string()));
918        classified.insert(
919            "error_type".to_string(),
920            Value::String(error_type.to_string()),
921        );
922        classified.insert("retryable".to_string(), Value::Bool(false));
923        classified.insert(
924            "source".to_string(),
925            Value::String(failure_source(error_type).to_string()),
926        );
927        self.append_event(
928            run_id,
929            Some(&session_id),
930            Some(step_id),
931            "failure_classified",
932            classified,
933            None,
934            None,
935            None,
936        );
937        let mut error = State::new();
938        error.insert("error".to_string(), Value::String(message.to_string()));
939        error.insert(
940            "error_type".to_string(),
941            Value::String(error_type.to_string()),
942        );
943        self.append_event(
944            run_id,
945            Some(&session_id),
946            Some(step_id),
947            "error_raised",
948            error,
949            None,
950            None,
951            None,
952        );
953        let mut payload = State::new();
954        payload.insert("step_id".to_string(), Value::String(step_id.to_string()));
955        payload.insert(
956            "error_type".to_string(),
957            Value::String(error_type.to_string()),
958        );
959        self.append_event(
960            run_id,
961            Some(&session_id),
962            Some(step_id),
963            "step_failed",
964            payload,
965            None,
966            None,
967            None,
968        );
969    }
970
971    pub fn append_event(
972        &mut self,
973        run_id: &str,
974        session_id: Option<&str>,
975        step_id: Option<&str>,
976        event_type: &str,
977        payload: State,
978        agent_role: Option<&str>,
979        state_version: Option<u64>,
980        causal_token: Option<&str>,
981    ) -> Event {
982        let events = self.events.entry(run_id.to_string()).or_default();
983        let payload_ref = format_state(&payload);
984        let event = Event {
985            event_id: new_id("evt"),
986            run_id: run_id.to_string(),
987            session_id: session_id.map(str::to_string),
988            step_id: step_id.map(str::to_string),
989            seq: events.len() as u64 + 1,
990            event_type: event_type.to_string(),
991            timestamp: now_seconds(),
992            agent_role: agent_role.map(str::to_string),
993            state_version,
994            causal_token: causal_token.map(str::to_string),
995            payload_hash: stable_hash(&payload_ref),
996            payload_ref,
997            payload,
998        };
999        events.push(event.clone());
1000        event
1001    }
1002
1003    pub fn reserve_ledger(&mut self, key: &str, entry: ToolLedgerEntry) -> Option<ToolLedgerEntry> {
1004        if let Some(existing) = self.tool_ledger.get(key) {
1005            return Some(existing.clone());
1006        }
1007        self.tool_ledger.insert(key.to_string(), entry);
1008        None
1009    }
1010
1011    pub fn update_ledger(
1012        &mut self,
1013        key: &str,
1014        status: &str,
1015        response: Option<Value>,
1016        error_type: Option<String>,
1017    ) {
1018        if let Some(entry) = self.tool_ledger.get_mut(key) {
1019            entry.status = status.to_string();
1020            entry.response = response.clone();
1021            entry.response_ref = response.as_ref().map(format_value);
1022            entry.response_hash = entry.response_ref.as_ref().map(|value| stable_hash(value));
1023            entry.error_type = error_type;
1024            entry.updated_at = now_seconds();
1025        }
1026    }
1027
1028    pub fn request_approval(
1029        &mut self,
1030        approval_key: &str,
1031        run_id: &str,
1032        session_id: &str,
1033        step_id: &str,
1034        tool_name: &str,
1035        risk_level: &str,
1036        reason: &str,
1037        request_hash: &str,
1038        request_ref: &str,
1039        requested_by: &str,
1040    ) -> ApprovalRequest {
1041        if let Some(existing) = self.approval_requests.get(approval_key) {
1042            return existing.clone();
1043        }
1044        let now = now_seconds();
1045        let approval = ApprovalRequest {
1046            approval_id: new_id("approval"),
1047            approval_key: approval_key.to_string(),
1048            run_id: run_id.to_string(),
1049            session_id: session_id.to_string(),
1050            step_id: step_id.to_string(),
1051            tool_name: tool_name.to_string(),
1052            risk_level: risk_level.to_string(),
1053            status: "PENDING".to_string(),
1054            reason: reason.to_string(),
1055            request_hash: request_hash.to_string(),
1056            request_ref: request_ref.to_string(),
1057            requested_by: requested_by.to_string(),
1058            approved_by: None,
1059            decision_reason: None,
1060            created_at: now,
1061            updated_at: now,
1062        };
1063        self.approval_requests
1064            .insert(approval_key.to_string(), approval.clone());
1065        approval
1066    }
1067
1068    pub fn approval_for_key(&self, approval_key: &str) -> Option<ApprovalRequest> {
1069        self.approval_requests.get(approval_key).cloned()
1070    }
1071
1072    pub fn approval_requests(&self, run_id: &str) -> Vec<ApprovalRequest> {
1073        self.approval_requests
1074            .values()
1075            .filter(|item| run_id.is_empty() || item.run_id == run_id)
1076            .cloned()
1077            .collect()
1078    }
1079
1080    pub fn approve_request(
1081        &mut self,
1082        approval_id: &str,
1083        approver: &str,
1084        reason: &str,
1085    ) -> Result<ApprovalRequest> {
1086        self.decide_approval(approval_id, "APPROVED", approver, reason)
1087    }
1088
1089    pub fn deny_request(
1090        &mut self,
1091        approval_id: &str,
1092        approver: &str,
1093        reason: &str,
1094    ) -> Result<ApprovalRequest> {
1095        self.decide_approval(approval_id, "DENIED", approver, reason)
1096    }
1097
1098    fn decide_approval(
1099        &mut self,
1100        approval_id: &str,
1101        status: &str,
1102        approver: &str,
1103        reason: &str,
1104    ) -> Result<ApprovalRequest> {
1105        let key = self
1106            .approval_requests
1107            .iter()
1108            .find(|(_, item)| item.approval_id == approval_id)
1109            .map(|(key, _)| key.clone())
1110            .ok_or_else(|| RuntimeError(format!("approval not found: {approval_id}")))?;
1111        let mut approval = self.approval_requests.get(&key).unwrap().clone();
1112        approval.status = status.to_string();
1113        approval.approved_by = Some(approver.to_string());
1114        approval.decision_reason = Some(reason.to_string());
1115        approval.updated_at = now_seconds();
1116        self.approval_requests.insert(key, approval.clone());
1117        let mut payload = State::new();
1118        payload.insert(
1119            "approval_id".to_string(),
1120            Value::String(approval_id.to_string()),
1121        );
1122        payload.insert(
1123            "tool".to_string(),
1124            Value::String(approval.tool_name.clone()),
1125        );
1126        payload.insert("status".to_string(), Value::String(status.to_string()));
1127        self.append_event(
1128            &approval.run_id,
1129            Some(&approval.session_id),
1130            Some(&approval.step_id),
1131            "tool_approval_decided",
1132            payload,
1133            None,
1134            None,
1135            None,
1136        );
1137        if self
1138            .steps
1139            .get(&approval.step_id)
1140            .map(|step| step.status.as_str())
1141            == Some("waiting_human")
1142        {
1143            if status == "APPROVED" {
1144                let step = self.steps.get_mut(&approval.step_id).unwrap();
1145                step.status = "pending".to_string();
1146                step.owner = None;
1147                step.lease_token = None;
1148                step.lease_until = None;
1149                step.updated_at = now_seconds();
1150                let run = self.runs.get_mut(&approval.run_id).unwrap();
1151                run.status = "pending".to_string();
1152                let mut retry = State::new();
1153                retry.insert(
1154                    "step_id".to_string(),
1155                    Value::String(approval.step_id.clone()),
1156                );
1157                retry.insert(
1158                    "reason".to_string(),
1159                    Value::String("approval_granted".to_string()),
1160                );
1161                self.append_event(
1162                    &approval.run_id,
1163                    Some(&approval.session_id),
1164                    Some(&approval.step_id),
1165                    "step_retry_scheduled",
1166                    retry,
1167                    None,
1168                    None,
1169                    None,
1170                );
1171            } else if status == "DENIED" {
1172                self.mark_failed(
1173                    &approval.run_id,
1174                    &approval.step_id,
1175                    "ApprovalDenied",
1176                    reason,
1177                );
1178            }
1179        }
1180        Ok(approval)
1181    }
1182
1183    pub fn record_cost(
1184        &mut self,
1185        run_id: &str,
1186        session_id: &str,
1187        step_id: &str,
1188        category: &str,
1189        name: &str,
1190        amount: f64,
1191        unit: &str,
1192        metadata: State,
1193    ) -> String {
1194        let cost_id = new_id("cost");
1195        let record = CostRecord {
1196            cost_id: cost_id.clone(),
1197            run_id: run_id.to_string(),
1198            session_id: session_id.to_string(),
1199            step_id: step_id.to_string(),
1200            category: category.to_string(),
1201            name: name.to_string(),
1202            amount,
1203            unit: unit.to_string(),
1204            metadata,
1205            created_at: now_seconds(),
1206        };
1207        self.cost_records
1208            .entry(run_id.to_string())
1209            .or_default()
1210            .push(record);
1211        let mut payload = State::new();
1212        payload.insert("cost_id".to_string(), Value::String(cost_id.clone()));
1213        payload.insert("category".to_string(), Value::String(category.to_string()));
1214        payload.insert("name".to_string(), Value::String(name.to_string()));
1215        payload.insert("amount".to_string(), Value::Number(amount));
1216        payload.insert("unit".to_string(), Value::String(unit.to_string()));
1217        self.append_event(
1218            run_id,
1219            Some(session_id),
1220            Some(step_id),
1221            "cost_recorded",
1222            payload,
1223            None,
1224            None,
1225            None,
1226        );
1227        cost_id
1228    }
1229
1230    pub fn cost_records(&self, run_id: &str) -> Vec<CostRecord> {
1231        self.cost_records.get(run_id).cloned().unwrap_or_default()
1232    }
1233
1234    pub fn cost_summary(&self, run_id: &str) -> CostSummary {
1235        let mut summary = CostSummary::default();
1236        for record in self.cost_records(run_id) {
1237            add_cost(&mut summary, &record);
1238        }
1239        summary
1240    }
1241
1242    pub fn create_artifact(
1243        &mut self,
1244        run_id: &str,
1245        step_id: Option<&str>,
1246        name: &str,
1247        content: State,
1248        metadata: State,
1249    ) -> Artifact {
1250        let blob_ref = format_state(&content);
1251        let artifact = Artifact {
1252            artifact_id: new_id("art"),
1253            run_id: run_id.to_string(),
1254            step_id: step_id.map(str::to_string),
1255            name: name.to_string(),
1256            blob_hash: format!("sha256:{}", stable_hash(&blob_ref)),
1257            blob_ref,
1258            metadata,
1259            created_at: now_seconds(),
1260        };
1261        self.artifacts
1262            .entry(run_id.to_string())
1263            .or_default()
1264            .push(artifact.clone());
1265        artifact
1266    }
1267
1268    pub fn artifacts(&self, run_id: &str) -> Vec<Artifact> {
1269        let mut items = self.artifacts.get(run_id).cloned().unwrap_or_default();
1270        items.sort_by(|a, b| {
1271            a.created_at
1272                .partial_cmp(&b.created_at)
1273                .unwrap()
1274                .then(a.artifact_id.cmp(&b.artifact_id))
1275        });
1276        items
1277    }
1278
1279    pub fn validate_lease(&self, step_id: &str, lease_token: &str) -> Result<()> {
1280        let step = self
1281            .steps
1282            .get(step_id)
1283            .ok_or_else(|| RuntimeError(format!("step not found: {step_id}")))?;
1284        if step.status != "running" || step.lease_token.as_deref() != Some(lease_token) {
1285            return Err(RuntimeError("invalid or stale lease token".to_string()));
1286        }
1287        if step.lease_until.is_some_and(|until| until <= now_seconds()) {
1288            return Err(RuntimeError("lease expired".to_string()));
1289        }
1290        Ok(())
1291    }
1292
1293    pub fn final_state(&self, run_id: &str) -> Result<State> {
1294        Ok(self.load_state(run_id)?.0)
1295    }
1296
1297    pub fn run(&self, run_id: &str) -> Result<Run> {
1298        self.runs
1299            .get(run_id)
1300            .cloned()
1301            .ok_or_else(|| RuntimeError(format!("run not found: {run_id}")))
1302    }
1303
1304    pub fn steps(&self, run_id: &str) -> Vec<Step> {
1305        self.steps
1306            .values()
1307            .filter(|step| step.run_id == run_id)
1308            .cloned()
1309            .collect()
1310    }
1311
1312    pub fn events(&self, run_id: &str) -> Vec<Event> {
1313        self.events.get(run_id).cloned().unwrap_or_default()
1314    }
1315
1316    pub fn ledger(&self, run_id: &str) -> Vec<ToolLedgerEntry> {
1317        self.tool_ledger
1318            .values()
1319            .filter(|entry| entry.run_id == run_id)
1320            .cloned()
1321            .collect()
1322    }
1323}
1324
1325pub struct ToolRegistry {
1326    tools: HashMap<String, ToolSpec>,
1327}
1328
1329impl ToolRegistry {
1330    pub fn new() -> Self {
1331        Self {
1332            tools: HashMap::new(),
1333        }
1334    }
1335
1336    pub fn register(&mut self, spec: ToolSpec) {
1337        self.tools.insert(spec.name.clone(), spec);
1338    }
1339
1340    pub fn get(&self, name: &str) -> Result<&ToolSpec> {
1341        self.tools
1342            .get(name)
1343            .ok_or_else(|| RuntimeError(format!("tool not registered: {name}")))
1344    }
1345}
1346
1347impl Default for ToolRegistry {
1348    fn default() -> Self {
1349        Self::new()
1350    }
1351}
1352
1353pub struct Runtime {
1354    pub store: MemoryStore,
1355    pub registry: ToolRegistry,
1356    pub budget: BudgetLimits,
1357    pub sandbox: Box<dyn SandboxExecutor>,
1358}
1359
1360impl Runtime {
1361    pub fn new() -> Self {
1362        Self {
1363            store: MemoryStore::new(),
1364            registry: ToolRegistry::new(),
1365            budget: BudgetLimits::default(),
1366            sandbox: Box::new(DisabledSandboxExecutor),
1367        }
1368    }
1369
1370    pub fn register_tool(&mut self, spec: ToolSpec) {
1371        self.registry.register(spec);
1372    }
1373
1374    pub fn set_budget(&mut self, budget: BudgetLimits) {
1375        self.budget = budget;
1376    }
1377
1378    pub fn set_sandbox(&mut self, sandbox: Box<dyn SandboxExecutor>) {
1379        self.sandbox = sandbox;
1380    }
1381
1382    pub fn create_run(&mut self, initial_state: State) -> (String, String) {
1383        self.store.create_run(initial_state)
1384    }
1385
1386    pub fn run_once(
1387        &mut self,
1388        run_id: &str,
1389        worker_id: &str,
1390        agent_role: &str,
1391        lease_seconds: f64,
1392        agent: AgentFunc,
1393    ) -> Result<bool> {
1394        let claim = match self.store.claim_step(worker_id, run_id, lease_seconds) {
1395            Ok(claim) => claim,
1396            Err(err) if err.0.contains("no runnable step") => return Ok(false),
1397            Err(err) => return Err(err),
1398        };
1399        let (state, version, session_id) = self.store.load_state(&claim.run_id)?;
1400        let mut payload = State::new();
1401        payload.insert(
1402            "agent_role".to_string(),
1403            Value::String(agent_role.to_string()),
1404        );
1405        payload.insert("attempt".to_string(), Value::Number(claim.attempt as f64));
1406        self.store.append_event(
1407            &claim.run_id,
1408            Some(&session_id),
1409            Some(&claim.step_id),
1410            "agent_started",
1411            payload,
1412            Some(agent_role),
1413            Some(version),
1414            None,
1415        );
1416        let mut ctx = AgentContext {
1417            run_id: claim.run_id.clone(),
1418            session_id,
1419            step_id: claim.step_id.clone(),
1420            agent_role: agent_role.to_string(),
1421            lease_token: claim.lease_token.clone(),
1422            attempt: claim.attempt,
1423            state_version: version,
1424            pending_patch: State::new(),
1425        };
1426        match agent(&mut ctx, state) {
1427            Ok(()) => {
1428                self.store.commit_state_patch(
1429                    &claim.run_id,
1430                    &claim.step_id,
1431                    &claim.lease_token,
1432                    version,
1433                    ctx.pending_patch,
1434                )?;
1435                Ok(true)
1436            }
1437            Err(err) if err.0 == "retryable" => {
1438                self.store.mark_retry(
1439                    &claim.run_id,
1440                    &claim.step_id,
1441                    "RetryableAgentError",
1442                    "retryable",
1443                );
1444                Ok(false)
1445            }
1446            Err(err) if err.0.starts_with("approval required:") => {
1447                let approval_id = err.0.trim_start_matches("approval required:");
1448                self.store
1449                    .mark_waiting_human(&claim.run_id, &claim.step_id, &err.0, approval_id);
1450                Ok(false)
1451            }
1452            Err(err) => {
1453                self.store.mark_failed(
1454                    &claim.run_id,
1455                    &claim.step_id,
1456                    classify_runtime_error(&err.0),
1457                    &err.0,
1458                );
1459                Err(err)
1460            }
1461        }
1462    }
1463
1464    pub fn call_tool(&mut self, ctx: &AgentContext, tool_name: &str, args: State) -> Result<Value> {
1465        let (
1466            version,
1467            side_effect,
1468            risk_level,
1469            idempotency_required,
1470            approval_required,
1471            sandbox_required,
1472            sandbox_executor,
1473            sandbox_policy,
1474            input_schema,
1475            output_schema,
1476        ) = {
1477            let spec = self.registry.get(tool_name)?;
1478            (
1479                spec.version.clone(),
1480                spec.side_effect.clone(),
1481                spec.risk_level.clone(),
1482                spec.idempotency_required,
1483                spec.approval_required,
1484                spec.sandbox_required,
1485                spec.sandbox_executor.clone(),
1486                spec.sandbox_policy.clone(),
1487                spec.input_schema.clone(),
1488                spec.output_schema.clone(),
1489            )
1490        };
1491        let mut request = State::new();
1492        request.insert("tool".to_string(), Value::String(tool_name.to_string()));
1493        request.insert("args".to_string(), Value::Object(args.clone()));
1494        let request_ref = format_state(&request);
1495        let request_hash = stable_hash(&request_ref);
1496        let causal_token = format!(
1497            "{}:{}:{}:{}",
1498            ctx.run_id, ctx.step_id, ctx.attempt, ctx.state_version
1499        );
1500        let idempotency_key = format!(
1501            "{}:{}:{}:{}",
1502            ctx.run_id, ctx.step_id, tool_name, request_hash
1503        );
1504        let managed = side_effect != "none" || idempotency_required;
1505        self.store.append_event(
1506            &ctx.run_id,
1507            Some(&ctx.session_id),
1508            Some(&ctx.step_id),
1509            "tool_call_requested",
1510            request,
1511            Some(&ctx.agent_role),
1512            Some(ctx.state_version),
1513            Some(&causal_token),
1514        );
1515        if let Some(schema) = input_schema.as_ref() {
1516            if let Err(err) = validate_tool_schema(schema, &Value::Object(args.clone()), "$arg") {
1517                let mut failed = State::new();
1518                failed.insert("tool".to_string(), Value::String(tool_name.to_string()));
1519                failed.insert("error".to_string(), Value::String(err.0.clone()));
1520                failed.insert(
1521                    "phase".to_string(),
1522                    Value::String("input_validation".to_string()),
1523                );
1524                self.store.append_event(
1525                    &ctx.run_id,
1526                    Some(&ctx.session_id),
1527                    Some(&ctx.step_id),
1528                    "tool_call_failed",
1529                    failed,
1530                    Some(&ctx.agent_role),
1531                    Some(ctx.state_version),
1532                    Some(&causal_token),
1533                );
1534                return Err(err);
1535            }
1536        }
1537
1538        let mut allowed = !is_high_risk(&risk_level);
1539        let mut reason = if allowed {
1540            "default allow for low/medium risk in local runtime".to_string()
1541        } else {
1542            "high-risk tool denied by default".to_string()
1543        };
1544        if let Some(approval) = self.store.approval_for_key(&idempotency_key) {
1545            if approval.status == "DENIED" {
1546                reason = format!("approval denied for tool {tool_name}");
1547                self.record_permission(ctx, tool_name, false, &reason, &causal_token);
1548                return Err(RuntimeError(reason));
1549            }
1550            if approval.status == "APPROVED" {
1551                allowed = true;
1552                reason = format!(
1553                    "approved by {}",
1554                    approval
1555                        .approved_by
1556                        .unwrap_or_else(|| "operator".to_string())
1557                );
1558            }
1559        } else if approval_required {
1560            let approval = self.store.request_approval(
1561                &idempotency_key,
1562                &ctx.run_id,
1563                &ctx.session_id,
1564                &ctx.step_id,
1565                tool_name,
1566                &risk_level,
1567                "tool requires approval",
1568                &request_hash,
1569                &request_ref,
1570                &ctx.agent_role,
1571            );
1572            self.record_permission(ctx, tool_name, false, "approval required", &causal_token);
1573            let mut payload = State::new();
1574            payload.insert("tool".to_string(), Value::String(tool_name.to_string()));
1575            payload.insert(
1576                "approval_id".to_string(),
1577                Value::String(approval.approval_id.clone()),
1578            );
1579            payload.insert(
1580                "approval_key".to_string(),
1581                Value::String(idempotency_key.clone()),
1582            );
1583            payload.insert("risk_level".to_string(), Value::String(risk_level));
1584            self.store.append_event(
1585                &ctx.run_id,
1586                Some(&ctx.session_id),
1587                Some(&ctx.step_id),
1588                "tool_approval_required",
1589                payload,
1590                Some(&ctx.agent_role),
1591                Some(ctx.state_version),
1592                Some(&causal_token),
1593            );
1594            return Err(RuntimeError(format!(
1595                "approval required:{}",
1596                approval.approval_id
1597            )));
1598        }
1599        self.record_permission(ctx, tool_name, allowed, &reason, &causal_token);
1600        if !allowed {
1601            return Err(RuntimeError(reason));
1602        }
1603        if let Some(max) = self.budget.max_tool_calls {
1604            let used = self.store.cost_summary(&ctx.run_id).tool_calls;
1605            if used >= max {
1606                let message = format!("tool call budget exceeded: {used}/{max}");
1607                let mut payload = State::new();
1608                payload.insert("category".to_string(), Value::String("tool".to_string()));
1609                payload.insert("tool".to_string(), Value::String(tool_name.to_string()));
1610                payload.insert("error".to_string(), Value::String(message.clone()));
1611                self.store.append_event(
1612                    &ctx.run_id,
1613                    Some(&ctx.session_id),
1614                    Some(&ctx.step_id),
1615                    "budget_check_failed",
1616                    payload,
1617                    Some(&ctx.agent_role),
1618                    Some(ctx.state_version),
1619                    Some(&causal_token),
1620                );
1621                return Err(RuntimeError(message));
1622            }
1623        }
1624        if managed {
1625            let entry = ToolLedgerEntry {
1626                ledger_id: new_id("ledger"),
1627                run_id: ctx.run_id.clone(),
1628                session_id: ctx.session_id.clone(),
1629                step_id: ctx.step_id.clone(),
1630                tool_name: tool_name.to_string(),
1631                tool_version: version,
1632                tool_call_id: new_id("toolcall"),
1633                idempotency_key: idempotency_key.clone(),
1634                causal_token: causal_token.clone(),
1635                request_hash: request_hash.clone(),
1636                request_ref: request_ref.clone(),
1637                status: "RESERVED".to_string(),
1638                external_id: None,
1639                response_hash: None,
1640                response_ref: None,
1641                error_type: None,
1642                response: None,
1643                created_at: now_seconds(),
1644                updated_at: now_seconds(),
1645            };
1646            if let Some(existing) = self.store.reserve_ledger(&idempotency_key, entry) {
1647                if existing.status == "SUCCEEDED" {
1648                    let mut payload = State::new();
1649                    payload.insert("tool".to_string(), Value::String(tool_name.to_string()));
1650                    payload.insert("replayed_from_ledger".to_string(), Value::Bool(true));
1651                    self.store.append_event(
1652                        &ctx.run_id,
1653                        Some(&ctx.session_id),
1654                        Some(&ctx.step_id),
1655                        "tool_call_completed",
1656                        payload,
1657                        Some(&ctx.agent_role),
1658                        Some(ctx.state_version),
1659                        Some(&causal_token),
1660                    );
1661                    self.store.record_cost(
1662                        &ctx.run_id,
1663                        &ctx.session_id,
1664                        &ctx.step_id,
1665                        "tool",
1666                        tool_name,
1667                        1.0,
1668                        "call",
1669                        State::new(),
1670                    );
1671                    return existing
1672                        .response
1673                        .ok_or_else(|| RuntimeError("missing ledger response".to_string()));
1674                }
1675                return Err(RuntimeError(
1676                    "tool side effect already in progress".to_string(),
1677                ));
1678            }
1679            self.store
1680                .update_ledger(&idempotency_key, "RUNNING", None, None);
1681        }
1682        if sandbox_required {
1683            let executor = if sandbox_executor.is_empty() { "default".to_string() } else { sandbox_executor.clone() };
1684            let network = match sandbox_policy.get("network") {
1685                Some(Value::String(value)) => value.clone(),
1686                _ => "deny".to_string(),
1687            };
1688            let filesystem = match sandbox_policy.get("filesystem") {
1689                Some(Value::String(value)) => value.clone(),
1690                _ => "read-only".to_string(),
1691            };
1692            let timeout_seconds = match sandbox_policy.get("timeout_seconds") {
1693                Some(Value::Number(value)) if *value > 0.0 => *value as u64,
1694                _ => 30,
1695            };
1696            let policy = SandboxPolicy {
1697                tool_name: tool_name.to_string(),
1698                run_id: ctx.run_id.clone(),
1699                step_id: ctx.step_id.clone(),
1700                executor: executor.clone(),
1701                network,
1702                filesystem,
1703                timeout_seconds,
1704                extra: sandbox_policy.clone(),
1705            };
1706            let mut sandbox_payload = State::new();
1707            sandbox_payload.insert("tool_name".to_string(), Value::String(tool_name.to_string()));
1708            sandbox_payload.insert("executor".to_string(), Value::String(executor));
1709            sandbox_payload.insert("network".to_string(), Value::String(policy.network.clone()));
1710            sandbox_payload.insert("filesystem".to_string(), Value::String(policy.filesystem.clone()));
1711            sandbox_payload.insert("timeout_seconds".to_string(), Value::Number(policy.timeout_seconds as f64));
1712            self.store.append_event(
1713                &ctx.run_id,
1714                Some(&ctx.session_id),
1715                Some(&ctx.step_id),
1716                "sandbox_started",
1717                sandbox_payload,
1718                Some(&ctx.agent_role),
1719                Some(ctx.state_version),
1720                Some(&causal_token),
1721            );
1722            let result = self.sandbox.run_tool(args, &policy);
1723            let mut completed = State::new();
1724            completed.insert("ok".to_string(), Value::Bool(result.ok));
1725            completed.insert("metadata".to_string(), Value::Object(result.metadata.clone()));
1726            if let Some(error) = &result.error {
1727                completed.insert("error".to_string(), Value::String(error.clone()));
1728            }
1729            self.store.append_event(
1730                &ctx.run_id,
1731                Some(&ctx.session_id),
1732                Some(&ctx.step_id),
1733                "sandbox_completed",
1734                completed,
1735                Some(&ctx.agent_role),
1736                Some(ctx.state_version),
1737                Some(&causal_token),
1738            );
1739            if !result.ok {
1740                let message = result.error.unwrap_or_else(|| "sandboxed tool failed".to_string());
1741                let mut failed = State::new();
1742                failed.insert("tool".to_string(), Value::String(tool_name.to_string()));
1743                failed.insert("error".to_string(), Value::String(message.clone()));
1744                self.store.append_event(
1745                    &ctx.run_id,
1746                    Some(&ctx.session_id),
1747                    Some(&ctx.step_id),
1748                    "tool_call_failed",
1749                    failed,
1750                    Some(&ctx.agent_role),
1751                    Some(ctx.state_version),
1752                    Some(&causal_token),
1753                );
1754                return Err(RuntimeError(message));
1755            }
1756            let value = result.output;
1757            if let Some(schema) = output_schema.as_ref() {
1758                validate_tool_schema(schema, &value, "$result")?;
1759            }
1760            if managed {
1761                self.store.update_ledger(&idempotency_key, "SUCCEEDED", Some(value.clone()), None);
1762            }
1763            let mut payload = State::new();
1764            payload.insert("tool".to_string(), Value::String(tool_name.to_string()));
1765            payload.insert("idempotency_key".to_string(), Value::String(idempotency_key));
1766            self.store.append_event(
1767                &ctx.run_id,
1768                Some(&ctx.session_id),
1769                Some(&ctx.step_id),
1770                "tool_call_completed",
1771                payload,
1772                Some(&ctx.agent_role),
1773                Some(ctx.state_version),
1774                Some(&causal_token),
1775            );
1776            self.store.record_cost(
1777                &ctx.run_id,
1778                &ctx.session_id,
1779                &ctx.step_id,
1780                "tool",
1781                tool_name,
1782                1.0,
1783                "call",
1784                State::new(),
1785            );
1786            return Ok(value);
1787        }
1788        let result = {
1789            let spec = self.registry.get(tool_name)?;
1790            (spec.func)(args)
1791        };
1792        match result {
1793            Ok(value) => {
1794                if let Some(schema) = output_schema.as_ref() {
1795                    if let Err(err) = validate_tool_schema(schema, &value, "$result") {
1796                        if managed {
1797                            self.store.update_ledger(
1798                                &idempotency_key,
1799                                "PENDING_VERIFICATION",
1800                                None,
1801                                Some("ToolOutputValidationError".to_string()),
1802                            );
1803                        }
1804                        let mut failed = State::new();
1805                        failed.insert("tool".to_string(), Value::String(tool_name.to_string()));
1806                        failed.insert("error".to_string(), Value::String(err.0.clone()));
1807                        failed.insert(
1808                            "phase".to_string(),
1809                            Value::String("output_validation".to_string()),
1810                        );
1811                        self.store.append_event(
1812                            &ctx.run_id,
1813                            Some(&ctx.session_id),
1814                            Some(&ctx.step_id),
1815                            "tool_call_failed",
1816                            failed,
1817                            Some(&ctx.agent_role),
1818                            Some(ctx.state_version),
1819                            Some(&causal_token),
1820                        );
1821                        return Err(err);
1822                    }
1823                }
1824                if managed {
1825                    self.store.update_ledger(
1826                        &idempotency_key,
1827                        "SUCCEEDED",
1828                        Some(value.clone()),
1829                        None,
1830                    );
1831                }
1832                let mut payload = State::new();
1833                payload.insert("tool".to_string(), Value::String(tool_name.to_string()));
1834                payload.insert(
1835                    "idempotency_key".to_string(),
1836                    Value::String(idempotency_key),
1837                );
1838                self.store.append_event(
1839                    &ctx.run_id,
1840                    Some(&ctx.session_id),
1841                    Some(&ctx.step_id),
1842                    "tool_call_completed",
1843                    payload,
1844                    Some(&ctx.agent_role),
1845                    Some(ctx.state_version),
1846                    Some(&causal_token),
1847                );
1848                self.store.record_cost(
1849                    &ctx.run_id,
1850                    &ctx.session_id,
1851                    &ctx.step_id,
1852                    "tool",
1853                    tool_name,
1854                    1.0,
1855                    "call",
1856                    State::new(),
1857                );
1858                Ok(value)
1859            }
1860            Err(err) => {
1861                if managed {
1862                    self.store.update_ledger(
1863                        &idempotency_key,
1864                        "PENDING_VERIFICATION",
1865                        None,
1866                        Some("RuntimeError".to_string()),
1867                    );
1868                }
1869                let mut payload = State::new();
1870                payload.insert("tool".to_string(), Value::String(tool_name.to_string()));
1871                payload.insert("error".to_string(), Value::String(err.0.clone()));
1872                self.store.append_event(
1873                    &ctx.run_id,
1874                    Some(&ctx.session_id),
1875                    Some(&ctx.step_id),
1876                    "tool_call_failed",
1877                    payload,
1878                    Some(&ctx.agent_role),
1879                    Some(ctx.state_version),
1880                    Some(&causal_token),
1881                );
1882                Err(err)
1883            }
1884        }
1885    }
1886
1887    fn record_permission(
1888        &mut self,
1889        ctx: &AgentContext,
1890        tool_name: &str,
1891        allowed: bool,
1892        reason: &str,
1893        causal_token: &str,
1894    ) {
1895        let mut permission = State::new();
1896        permission.insert("tool".to_string(), Value::String(tool_name.to_string()));
1897        permission.insert("allowed".to_string(), Value::Bool(allowed));
1898        permission.insert("reason".to_string(), Value::String(reason.to_string()));
1899        self.store.append_event(
1900            &ctx.run_id,
1901            Some(&ctx.session_id),
1902            Some(&ctx.step_id),
1903            "tool_permission_decided",
1904            permission,
1905            Some(&ctx.agent_role),
1906            Some(ctx.state_version),
1907            Some(causal_token),
1908        );
1909    }
1910
1911    pub fn create_artifact(
1912        &mut self,
1913        ctx: &AgentContext,
1914        name: &str,
1915        content: State,
1916        metadata: State,
1917    ) -> Result<String> {
1918        let artifact =
1919            self.store
1920                .create_artifact(&ctx.run_id, Some(&ctx.step_id), name, content, metadata);
1921        let mut payload = State::new();
1922        payload.insert(
1923            "artifact_id".to_string(),
1924            Value::String(artifact.artifact_id.clone()),
1925        );
1926        payload.insert("name".to_string(), Value::String(name.to_string()));
1927        self.store.append_event(
1928            &ctx.run_id,
1929            Some(&ctx.session_id),
1930            Some(&ctx.step_id),
1931            "artifact_created",
1932            payload,
1933            Some(&ctx.agent_role),
1934            Some(ctx.state_version),
1935            None,
1936        );
1937        Ok(artifact.artifact_id)
1938    }
1939
1940    pub fn create_media_artifact(
1941        &mut self,
1942        ctx: &AgentContext,
1943        name: &str,
1944        kind: &str,
1945        options: MediaArtifactOptions,
1946    ) -> Result<String> {
1947        if !is_media_kind(kind) {
1948            return Err(RuntimeError(format!("unsupported media kind: {kind}")));
1949        }
1950        let mut media_metadata = options.media_metadata.clone();
1951        media_metadata.insert(
1952            "schema_version".to_string(),
1953            Value::String(MEDIA_SCHEMA_VERSION.to_string()),
1954        );
1955        media_metadata.insert("kind".to_string(), Value::String(kind.to_string()));
1956        let mut content = State::new();
1957        content.insert(
1958            "schema_version".to_string(),
1959            Value::String(MEDIA_SCHEMA_VERSION.to_string()),
1960        );
1961        content.insert("kind".to_string(), Value::String(kind.to_string()));
1962        if let Some(uri) = &options.uri {
1963            content.insert("uri".to_string(), Value::String(uri.clone()));
1964        }
1965        if let Some(content_ref) = &options.content_ref {
1966            content.insert(
1967                "content_ref".to_string(),
1968                Value::String(content_ref.clone()),
1969            );
1970        }
1971        content.insert(
1972            "metadata".to_string(),
1973            Value::Object(media_metadata.clone()),
1974        );
1975        if !options.lineage.is_empty() {
1976            content.insert(
1977                "lineage".to_string(),
1978                Value::Object(options.lineage.clone()),
1979            );
1980        }
1981        if !options.derived_outputs.is_empty() {
1982            content.insert(
1983                "derived_outputs".to_string(),
1984                Value::Object(options.derived_outputs.clone()),
1985            );
1986        }
1987        let mut media = State::new();
1988        media.insert(
1989            "schema_version".to_string(),
1990            Value::String(MEDIA_SCHEMA_VERSION.to_string()),
1991        );
1992        media.insert("kind".to_string(), Value::String(kind.to_string()));
1993        if let Some(uri) = options.uri {
1994            media.insert("uri".to_string(), Value::String(uri));
1995        }
1996        if let Some(content_ref) = options.content_ref {
1997            media.insert("content_ref".to_string(), Value::String(content_ref));
1998        }
1999        media.insert("metadata".to_string(), Value::Object(media_metadata));
2000        media.insert("lineage".to_string(), Value::Object(options.lineage));
2001        let mut artifact_metadata = options.metadata;
2002        artifact_metadata.insert("agentledger_media".to_string(), Value::Object(media));
2003        self.create_artifact(ctx, name, content, artifact_metadata)
2004    }
2005
2006    pub fn create_stream_checkpoint(
2007        &mut self,
2008        ctx: &AgentContext,
2009        name: &str,
2010        options: StreamCheckpointOptions,
2011    ) -> Result<String> {
2012        if options.stream_id.is_empty() || options.consumer_id.is_empty() {
2013            return Err(RuntimeError(
2014                "stream_id and consumer_id are required".to_string(),
2015            ));
2016        }
2017        let chunk = options.chunk.map(stream_chunk_to_state);
2018        let mut content = State::new();
2019        content.insert(
2020            "schema_version".to_string(),
2021            Value::String(STREAM_SCHEMA_VERSION.to_string()),
2022        );
2023        content.insert(
2024            "stream_id".to_string(),
2025            Value::String(options.stream_id.clone()),
2026        );
2027        content.insert(
2028            "consumer_id".to_string(),
2029            Value::String(options.consumer_id.clone()),
2030        );
2031        content.insert("offset".to_string(), options.offset.clone());
2032        if let Some(watermark) = &options.watermark {
2033            content.insert("watermark".to_string(), watermark.clone());
2034        }
2035        if let Some(chunk_state) = &chunk {
2036            content.insert("chunk".to_string(), Value::Object(chunk_state.clone()));
2037        }
2038        if let Some(partial_ref) = &options.partial_result_ref {
2039            content.insert(
2040                "partial_result_ref".to_string(),
2041                Value::String(partial_ref.clone()),
2042            );
2043        }
2044        if !options.backpressure.is_empty() {
2045            content.insert(
2046                "backpressure".to_string(),
2047                Value::Object(options.backpressure.clone()),
2048            );
2049        }
2050        if !options.metadata.is_empty() {
2051            content.insert("metadata".to_string(), Value::Object(options.metadata));
2052        }
2053        let mut stream = State::new();
2054        stream.insert(
2055            "schema_version".to_string(),
2056            Value::String(STREAM_SCHEMA_VERSION.to_string()),
2057        );
2058        stream.insert("stream_id".to_string(), Value::String(options.stream_id));
2059        stream.insert(
2060            "consumer_id".to_string(),
2061            Value::String(options.consumer_id),
2062        );
2063        stream.insert("offset".to_string(), options.offset);
2064        if let Some(watermark) = options.watermark {
2065            stream.insert("watermark".to_string(), watermark);
2066        }
2067        if let Some(chunk_state) = chunk {
2068            stream.insert("chunk".to_string(), Value::Object(chunk_state));
2069        }
2070        if let Some(partial_ref) = options.partial_result_ref {
2071            stream.insert("partial_result_ref".to_string(), Value::String(partial_ref));
2072        }
2073        if !options.backpressure.is_empty() {
2074            stream.insert(
2075                "backpressure".to_string(),
2076                Value::Object(options.backpressure),
2077            );
2078        }
2079        let mut artifact_metadata = State::new();
2080        artifact_metadata.insert("agentledger_stream".to_string(), Value::Object(stream));
2081        self.create_artifact(ctx, name, content, artifact_metadata)
2082    }
2083
2084    pub fn record_model_call(
2085        &mut self,
2086        ctx: &AgentContext,
2087        model: &str,
2088        input_tokens: f64,
2089        output_tokens: f64,
2090        total_usd: f64,
2091    ) -> Result<()> {
2092        let mut usage = State::new();
2093        usage.insert("input_tokens".to_string(), Value::Number(input_tokens));
2094        usage.insert("output_tokens".to_string(), Value::Number(output_tokens));
2095        self.record_model_call_evidence(ctx, "custom", model, State::new(), State::new(), usage, total_usd, State::new())
2096    }
2097
2098    pub fn record_model_call_evidence(
2099        &mut self,
2100        ctx: &AgentContext,
2101        provider: &str,
2102        model: &str,
2103        request: State,
2104        response: State,
2105        usage: State,
2106        total_usd: f64,
2107        metadata: State,
2108    ) -> Result<()> {
2109        let tokens = usage_total_tokens(&usage);
2110        if let Some(max) = self.budget.max_model_tokens {
2111            let used = self.store.cost_summary(&ctx.run_id).model_tokens;
2112            if used + tokens > max {
2113                let message = format!("model token budget exceeded: {used}+{tokens}/{max}");
2114                let mut payload = State::new();
2115                payload.insert("category".to_string(), Value::String("model".to_string()));
2116                payload.insert("provider".to_string(), Value::String(provider.to_string()));
2117                payload.insert("model".to_string(), Value::String(model.to_string()));
2118                payload.insert("error".to_string(), Value::String(message.clone()));
2119                self.store.append_event(
2120                    &ctx.run_id,
2121                    Some(&ctx.session_id),
2122                    Some(&ctx.step_id),
2123                    "budget_check_failed",
2124                    payload,
2125                    Some(&ctx.agent_role),
2126                    Some(ctx.state_version),
2127                    None,
2128                );
2129                return Err(RuntimeError(message));
2130            }
2131        }
2132        let mut request_payload = State::new();
2133        request_payload.insert("schema_version".to_string(), Value::String(MODEL_EVIDENCE_SCHEMA_VERSION.to_string()));
2134        request_payload.insert("provider".to_string(), Value::String(provider.to_string()));
2135        request_payload.insert("model".to_string(), Value::String(model.to_string()));
2136        request_payload.insert("request".to_string(), Value::Object(request));
2137        request_payload.insert("metadata".to_string(), Value::Object(metadata.clone()));
2138        self.store.append_event(
2139            &ctx.run_id,
2140            Some(&ctx.session_id),
2141            Some(&ctx.step_id),
2142            "model_call_requested",
2143            request_payload,
2144            Some(&ctx.agent_role),
2145            Some(ctx.state_version),
2146            None,
2147        );
2148        let mut response_payload = State::new();
2149        response_payload.insert("schema_version".to_string(), Value::String(MODEL_EVIDENCE_SCHEMA_VERSION.to_string()));
2150        response_payload.insert("provider".to_string(), Value::String(provider.to_string()));
2151        response_payload.insert("model".to_string(), Value::String(model.to_string()));
2152        response_payload.insert("response".to_string(), Value::Object(response));
2153        response_payload.insert("usage".to_string(), Value::Object(usage.clone()));
2154        response_payload.insert("total_usd".to_string(), Value::Number(total_usd));
2155        response_payload.insert("metadata".to_string(), Value::Object(metadata));
2156        self.store.append_event(
2157            &ctx.run_id,
2158            Some(&ctx.session_id),
2159            Some(&ctx.step_id),
2160            "model_call_completed",
2161            response_payload,
2162            Some(&ctx.agent_role),
2163            Some(ctx.state_version),
2164            None,
2165        );
2166        self.record_model_costs(ctx, provider, model, &usage, total_usd);
2167        Ok(())
2168    }
2169
2170    pub fn record_model_failure(
2171        &mut self,
2172        ctx: &AgentContext,
2173        provider: &str,
2174        model: &str,
2175        error_type: &str,
2176        message: &str,
2177        retryable: Option<bool>,
2178        request: State,
2179        usage: State,
2180        total_usd: f64,
2181        metadata: State,
2182    ) -> Result<()> {
2183        let mut payload = State::new();
2184        payload.insert("schema_version".to_string(), Value::String(MODEL_EVIDENCE_SCHEMA_VERSION.to_string()));
2185        payload.insert("provider".to_string(), Value::String(provider.to_string()));
2186        payload.insert("model".to_string(), Value::String(model.to_string()));
2187        payload.insert("error_type".to_string(), Value::String(error_type.to_string()));
2188        payload.insert("error".to_string(), Value::String(message.to_string()));
2189        if let Some(value) = retryable {
2190            payload.insert("retryable".to_string(), Value::Bool(value));
2191        }
2192        payload.insert("request".to_string(), Value::Object(request));
2193        payload.insert("usage".to_string(), Value::Object(usage.clone()));
2194        payload.insert("total_usd".to_string(), Value::Number(total_usd));
2195        payload.insert("metadata".to_string(), Value::Object(metadata));
2196        self.store.append_event(
2197            &ctx.run_id,
2198            Some(&ctx.session_id),
2199            Some(&ctx.step_id),
2200            "model_call_failed",
2201            payload,
2202            Some(&ctx.agent_role),
2203            Some(ctx.state_version),
2204            None,
2205        );
2206        self.record_model_costs(ctx, provider, model, &usage, total_usd);
2207        Ok(())
2208    }
2209
2210    pub fn record_tool_call_proposal(
2211        &mut self,
2212        ctx: &AgentContext,
2213        tool_name: &str,
2214        arguments: State,
2215        provider: Option<&str>,
2216        model: Option<&str>,
2217        model_call_ref: Option<&str>,
2218        confidence: Option<f64>,
2219        reason: Option<&str>,
2220        metadata: State,
2221    ) {
2222        let mut payload = State::new();
2223        payload.insert("schema_version".to_string(), Value::String(MODEL_EVIDENCE_SCHEMA_VERSION.to_string()));
2224        payload.insert("tool".to_string(), Value::String(tool_name.to_string()));
2225        payload.insert("args".to_string(), Value::Object(arguments));
2226        if let Some(value) = provider {
2227            payload.insert("provider".to_string(), Value::String(value.to_string()));
2228        }
2229        if let Some(value) = model {
2230            payload.insert("model".to_string(), Value::String(value.to_string()));
2231        }
2232        if let Some(value) = model_call_ref {
2233            payload.insert("model_call_ref".to_string(), Value::String(value.to_string()));
2234        }
2235        if let Some(value) = confidence {
2236            payload.insert("confidence".to_string(), Value::Number(value));
2237        }
2238        if let Some(value) = reason {
2239            payload.insert("reason".to_string(), Value::String(value.to_string()));
2240        }
2241        payload.insert("metadata".to_string(), Value::Object(metadata));
2242        self.store.append_event(
2243            &ctx.run_id,
2244            Some(&ctx.session_id),
2245            Some(&ctx.step_id),
2246            "tool_call_proposed",
2247            payload,
2248            Some(&ctx.agent_role),
2249            Some(ctx.state_version),
2250            None,
2251        );
2252    }
2253
2254    fn record_model_costs(&mut self, ctx: &AgentContext, provider: &str, model: &str, usage: &State, total_usd: f64) {
2255        let tokens = usage_total_tokens(usage);
2256        let mut metadata = State::new();
2257        metadata.insert("provider".to_string(), Value::String(provider.to_string()));
2258        metadata.insert("model".to_string(), Value::String(model.to_string()));
2259        metadata.insert("usage".to_string(), Value::Object(usage.clone()));
2260        if tokens > 0.0 {
2261            self.store.record_cost(&ctx.run_id, &ctx.session_id, &ctx.step_id, "model", model, tokens, "token", metadata.clone());
2262        }
2263        if total_usd > 0.0 {
2264            self.store.record_cost(&ctx.run_id, &ctx.session_id, &ctx.step_id, "model", model, total_usd, "usd", metadata);
2265        }
2266    }
2267}
2268
2269impl Default for Runtime {
2270    fn default() -> Self {
2271        Self::new()
2272    }
2273}
2274
2275#[derive(Clone, Debug)]
2276pub struct AgentContext {
2277    pub run_id: String,
2278    pub session_id: String,
2279    pub step_id: String,
2280    pub agent_role: String,
2281    pub lease_token: String,
2282    pub attempt: u64,
2283    pub state_version: u64,
2284    pub pending_patch: State,
2285}
2286
2287impl AgentContext {
2288    pub fn write_state(&mut self, key: &str, value: Value) {
2289        self.pending_patch.insert(key.to_string(), value);
2290    }
2291}
2292
2293#[derive(Clone, Debug)]
2294pub struct EvidenceBundle {
2295    pub schema_version: String,
2296    pub bundle_hash: String,
2297    pub run: Run,
2298    pub steps: Vec<Step>,
2299    pub events: Vec<Event>,
2300    pub tool_ledger: Vec<ToolLedgerEntry>,
2301    pub approvals: Vec<ApprovalRequest>,
2302    pub artifacts: Vec<Artifact>,
2303    pub media_artifacts: Vec<State>,
2304    pub stream_checkpoints: Vec<State>,
2305    pub cost_records: Vec<CostRecord>,
2306    pub cost_summary: CostSummary,
2307    pub summary: State,
2308    pub final_state: State,
2309}
2310
2311#[derive(Clone, Debug, Default)]
2312pub struct WorkerRunSummary {
2313    pub worker_id: String,
2314    pub run_id: Option<String>,
2315    pub iterations: u64,
2316    pub attempts: u64,
2317    pub succeeded_attempts: u64,
2318    pub recovered_leases: u64,
2319    pub final_status: Option<String>,
2320    pub stopped_reason: String,
2321}
2322
2323pub struct LocalWorker {
2324    pub worker_id: String,
2325    pub agent_role: String,
2326    pub lease_seconds: f64,
2327    pub recover_expired: bool,
2328}
2329
2330impl LocalWorker {
2331    pub fn new(worker_id: &str, agent_role: &str) -> Self {
2332        Self {
2333            worker_id: worker_id.to_string(),
2334            agent_role: agent_role.to_string(),
2335            lease_seconds: 60.0,
2336            recover_expired: true,
2337        }
2338    }
2339
2340    pub fn run_until_idle(
2341        &self,
2342        runtime: &mut Runtime,
2343        run_id: &str,
2344        max_iterations: u64,
2345        agent: AgentFunc,
2346    ) -> Result<WorkerRunSummary> {
2347        let mut summary = WorkerRunSummary {
2348            worker_id: self.worker_id.clone(),
2349            run_id: Some(run_id.to_string()),
2350            stopped_reason: "max_iterations".to_string(),
2351            ..WorkerRunSummary::default()
2352        };
2353        for index in 1..=max_iterations {
2354            summary.iterations = index;
2355            if self.recover_expired {
2356                summary.recovered_leases += runtime.store.recover_expired_leases() as u64;
2357            }
2358            let status = runtime.store.run(run_id)?.status;
2359            if is_terminal_status(&status) {
2360                summary.final_status = Some(status);
2361                summary.stopped_reason = "terminal_status".to_string();
2362                break;
2363            }
2364            let ok = runtime.run_once(
2365                run_id,
2366                &self.worker_id,
2367                &self.agent_role,
2368                self.lease_seconds,
2369                agent,
2370            )?;
2371            if !ok {
2372                summary.stopped_reason = "idle".to_string();
2373                break;
2374            }
2375            summary.attempts += 1;
2376            summary.succeeded_attempts += 1;
2377        }
2378        let status = runtime.store.run(run_id)?.status;
2379        summary.final_status = Some(status.clone());
2380        if is_terminal_status(&status) {
2381            summary.stopped_reason = "terminal_status".to_string();
2382        }
2383        Ok(summary)
2384    }
2385}
2386
2387#[derive(Clone, Debug, Default)]
2388pub struct WorkerServiceSummary {
2389    pub worker_id: String,
2390    pub run_id: Option<String>,
2391    pub loops: u64,
2392    pub attempts: u64,
2393    pub succeeded_attempts: u64,
2394    pub recovered_leases: u64,
2395    pub idle_polls: u64,
2396    pub stopped_reason: String,
2397    pub final_status: Option<String>,
2398    pub stop_requested: bool,
2399}
2400
2401pub struct WorkerService {
2402    pub worker: LocalWorker,
2403    pub stop_requested: bool,
2404    pub stop_reason: String,
2405}
2406
2407impl WorkerService {
2408    pub fn new(worker: LocalWorker) -> Self {
2409        Self {
2410            worker,
2411            stop_requested: false,
2412            stop_reason: "stop_requested".to_string(),
2413        }
2414    }
2415
2416    pub fn request_stop(&mut self, reason: &str) {
2417        self.stop_requested = true;
2418        self.stop_reason = reason.to_string();
2419    }
2420
2421    pub fn serve(
2422        &mut self,
2423        runtime: &mut Runtime,
2424        run_id: Option<&str>,
2425        max_loops: u64,
2426        max_idle_polls: Option<u64>,
2427        agent: AgentFunc,
2428    ) -> Result<WorkerServiceSummary> {
2429        let mut summary = WorkerServiceSummary {
2430            worker_id: self.worker.worker_id.clone(),
2431            run_id: run_id.map(str::to_string),
2432            stopped_reason: "max_loops".to_string(),
2433            ..WorkerServiceSummary::default()
2434        };
2435        while summary.loops < max_loops {
2436            if self.stop_requested {
2437                summary.stopped_reason = self.stop_reason.clone();
2438                summary.stop_requested = true;
2439                break;
2440            }
2441            summary.loops += 1;
2442            let Some(run_id) = run_id else {
2443                summary.idle_polls += 1;
2444                if max_idle_polls.is_some_and(|limit| summary.idle_polls >= limit) {
2445                    summary.stopped_reason = "idle".to_string();
2446                    break;
2447                }
2448                continue;
2449            };
2450            let run_summary = self.worker.run_until_idle(runtime, run_id, 1, agent)?;
2451            summary.attempts += run_summary.attempts;
2452            summary.succeeded_attempts += run_summary.succeeded_attempts;
2453            summary.recovered_leases += run_summary.recovered_leases;
2454            summary.final_status = run_summary.final_status.clone();
2455            if summary
2456                .final_status
2457                .as_deref()
2458                .is_some_and(is_terminal_status)
2459            {
2460                summary.stopped_reason = "terminal_status".to_string();
2461                break;
2462            }
2463            if run_summary.attempts == 0 {
2464                summary.idle_polls += 1;
2465                if max_idle_polls.is_some_and(|limit| summary.idle_polls >= limit) {
2466                    summary.stopped_reason = "idle".to_string();
2467                    break;
2468                }
2469            } else {
2470                summary.idle_polls = 0;
2471            }
2472        }
2473        Ok(summary)
2474    }
2475}
2476
2477fn is_terminal_status(status: &str) -> bool {
2478    matches!(status, "completed" | "failed" | "cancelled")
2479}
2480
2481pub fn export_evidence(store: &MemoryStore, run_id: &str) -> Result<EvidenceBundle> {
2482    let run = store.run(run_id)?;
2483    let steps = store.steps(run_id);
2484    let events = store.events(run_id);
2485    let tool_ledger = store.ledger(run_id);
2486    let approvals = store.approval_requests(run_id);
2487    let artifacts = store.artifacts(run_id);
2488    let media_artifacts = media_artifacts_from(&artifacts);
2489    let stream_checkpoints = stream_checkpoints_from(&artifacts);
2490    let cost_records = store.cost_records(run_id);
2491    let cost_summary = store.cost_summary(run_id);
2492    let summary = evidence_summary(
2493        &steps,
2494        &events,
2495        &tool_ledger,
2496        &approvals,
2497        &artifacts,
2498        &media_artifacts,
2499        &stream_checkpoints,
2500        &cost_records,
2501        &cost_summary,
2502    );
2503    let final_state = store.final_state(run_id)?;
2504    let basis = format!(
2505        "{}:{}:{}:{}:{}",
2506        run_id,
2507        steps.len(),
2508        events.len(),
2509        tool_ledger.len(),
2510        cost_records.len()
2511    );
2512    Ok(EvidenceBundle {
2513        schema_version: "agentledger.evidence.v1".to_string(),
2514        bundle_hash: stable_hash(&basis),
2515        run,
2516        steps,
2517        events,
2518        tool_ledger,
2519        approvals,
2520        artifacts,
2521        media_artifacts,
2522        stream_checkpoints,
2523        cost_records,
2524        cost_summary,
2525        summary,
2526        final_state,
2527    })
2528}
2529
2530fn evidence_summary(
2531    steps: &[Step],
2532    events: &[Event],
2533    ledger: &[ToolLedgerEntry],
2534    approvals: &[ApprovalRequest],
2535    artifacts: &[Artifact],
2536    media_artifacts: &[State],
2537    stream_checkpoints: &[State],
2538    cost_records: &[CostRecord],
2539    cost_summary: &CostSummary,
2540) -> State {
2541    let mut summary = State::new();
2542    summary.insert("event_count".into(), Value::Number(events.len() as f64));
2543    summary.insert("step_count".into(), Value::Number(steps.len() as f64));
2544    summary.insert(
2545        "tool_ledger_count".into(),
2546        Value::Number(ledger.len() as f64),
2547    );
2548    summary.insert(
2549        "approval_count".into(),
2550        Value::Number(approvals.len() as f64),
2551    );
2552    summary.insert(
2553        "artifact_count".into(),
2554        Value::Number(artifacts.len() as f64),
2555    );
2556    summary.insert(
2557        "media_artifact_count".into(),
2558        Value::Number(media_artifacts.len() as f64),
2559    );
2560    summary.insert(
2561        "stream_checkpoint_count".into(),
2562        Value::Number(stream_checkpoints.len() as f64),
2563    );
2564    summary.insert(
2565        "cost_record_count".into(),
2566        Value::Number(cost_records.len() as f64),
2567    );
2568    summary.insert(
2569        "has_failed_steps".into(),
2570        Value::Bool(steps.iter().any(|step| step.status == "failed")),
2571    );
2572    summary.insert(
2573        "has_pending_verification".into(),
2574        Value::Bool(
2575            ledger
2576                .iter()
2577                .any(|row| row.status == "PENDING_VERIFICATION"),
2578        ),
2579    );
2580    summary.insert(
2581        "has_pending_approvals".into(),
2582        Value::Bool(approvals.iter().any(|row| row.status == "PENDING")),
2583    );
2584    let mut cost = State::new();
2585    cost.insert("tool_calls".into(), Value::Number(cost_summary.tool_calls));
2586    cost.insert(
2587        "model_tokens".into(),
2588        Value::Number(cost_summary.model_tokens),
2589    );
2590    cost.insert("total_usd".into(), Value::Number(cost_summary.total_usd));
2591    summary.insert("cost_summary".into(), Value::Object(cost));
2592    summary
2593}
2594
2595#[derive(Clone, Debug)]
2596pub struct ReplaySummary {
2597    pub run_id: String,
2598    pub event_count: usize,
2599    pub tool_call_count: usize,
2600    pub final_state: State,
2601    pub event_hash: String,
2602    pub replay_safe: bool,
2603    pub artifact_count: usize,
2604    pub media_artifact_count: usize,
2605    pub stream_checkpoint_count: usize,
2606}
2607
2608pub fn replay(store: &MemoryStore, run_id: &str) -> Result<ReplaySummary> {
2609    let events = store.events(run_id);
2610    let tool_call_count = events
2611        .iter()
2612        .filter(|event| event.event_type.starts_with("tool_call_"))
2613        .count();
2614    let digest = events
2615        .iter()
2616        .map(|event| format!("{}:{}:{}", event.seq, event.event_type, event.payload_hash))
2617        .collect::<Vec<_>>()
2618        .join("|");
2619    let artifacts = store.artifacts(run_id);
2620    Ok(ReplaySummary {
2621        run_id: run_id.to_string(),
2622        event_count: events.len(),
2623        tool_call_count,
2624        final_state: store.final_state(run_id)?,
2625        event_hash: stable_hash(&digest),
2626        replay_safe: true,
2627        artifact_count: artifacts.len(),
2628        media_artifact_count: media_artifacts_from(&artifacts).len(),
2629        stream_checkpoint_count: stream_checkpoints_from(&artifacts).len(),
2630    })
2631}
2632
2633#[derive(Clone, Debug)]
2634pub struct CostAttributionReport {
2635    pub run_id: String,
2636    pub total: CostSummary,
2637    pub by_agent: HashMap<String, CostSummary>,
2638    pub by_step: HashMap<String, CostSummary>,
2639    pub by_name: HashMap<String, CostSummary>,
2640}
2641
2642pub fn cost_attribution(store: &MemoryStore, run_id: &str) -> CostAttributionReport {
2643    let mut step_roles = HashMap::new();
2644    for event in store.events(run_id) {
2645        if let (Some(step_id), Some(agent_role)) = (event.step_id, event.agent_role) {
2646            step_roles.insert(step_id, agent_role);
2647        }
2648    }
2649    let mut report = CostAttributionReport {
2650        run_id: run_id.to_string(),
2651        total: CostSummary::default(),
2652        by_agent: HashMap::new(),
2653        by_step: HashMap::new(),
2654        by_name: HashMap::new(),
2655    };
2656    for record in store.cost_records(run_id) {
2657        add_cost(&mut report.total, &record);
2658        let agent = step_roles
2659            .get(&record.step_id)
2660            .cloned()
2661            .unwrap_or_else(|| "<unknown>".to_string());
2662        add_cost(report.by_agent.entry(agent).or_default(), &record);
2663        add_cost(
2664            report.by_step.entry(record.step_id.clone()).or_default(),
2665            &record,
2666        );
2667        add_cost(
2668            report.by_name.entry(record.name.clone()).or_default(),
2669            &record,
2670        );
2671    }
2672    report
2673}
2674
2675#[derive(Clone, Debug)]
2676pub struct FailureAttributionReport {
2677    pub run_id: String,
2678    pub run_status: String,
2679    pub failed_steps: Vec<Step>,
2680    pub pending_verification: Vec<ToolLedgerEntry>,
2681    pub pending_approvals: Vec<ApprovalRequest>,
2682    pub failure_events: Vec<Event>,
2683    pub failure_envelopes: Vec<State>,
2684    pub failure_lifecycle: State,
2685    pub failure_causal_graph: State,
2686    pub failure_replay_plan: State,
2687    pub failure_alerts: State,
2688    pub failure_export: State,
2689    pub summary: State,
2690}
2691
2692pub fn failure_attribution(store: &MemoryStore, run_id: &str) -> Result<FailureAttributionReport> {
2693    let run = store.run(run_id)?;
2694    let failed_steps: Vec<Step> = store
2695        .steps(run_id)
2696        .into_iter()
2697        .filter(|step| step.status == "failed")
2698        .collect();
2699    let pending_verification: Vec<ToolLedgerEntry> = store
2700        .ledger(run_id)
2701        .into_iter()
2702        .filter(|entry| entry.status == "PENDING_VERIFICATION")
2703        .collect();
2704    let pending_approvals: Vec<ApprovalRequest> = store
2705        .approval_requests(run_id)
2706        .into_iter()
2707        .filter(|entry| entry.status == "PENDING")
2708        .collect();
2709    let failure_events: Vec<Event> = store
2710        .events(run_id)
2711        .into_iter()
2712        .filter(|event| is_failure_event(&event.event_type))
2713        .collect();
2714    let steps = store.steps(run_id);
2715    let ledger = store.ledger(run_id);
2716    let approvals = store.approval_requests(run_id);
2717    let events = store.events(run_id);
2718    let costs = store.cost_records(run_id);
2719    let failure_envelopes = failure_envelopes(run_id, &run.status, &steps, &ledger, &approvals, &failure_events);
2720    let failure_lifecycle = failure_lifecycle(run_id, &run.status, &failure_envelopes);
2721    let failure_causal_graph = failure_causal_graph(run_id, &run.status, &failure_envelopes, &steps, &ledger, &approvals, &events, &costs);
2722    let failure_replay_plan = failure_replay_plan(run_id, &failure_envelopes, &ledger, &events);
2723    let failure_alerts = failure_alerts(run_id, &failure_envelopes, &failure_replay_plan);
2724    let mut summary = State::new();
2725    summary.insert(
2726        "failed_step_count".to_string(),
2727        Value::Number(failed_steps.len() as f64),
2728    );
2729    summary.insert(
2730        "pending_verification_count".to_string(),
2731        Value::Number(pending_verification.len() as f64),
2732    );
2733    summary.insert(
2734        "pending_approval_count".to_string(),
2735        Value::Number(pending_approvals.len() as f64),
2736    );
2737    summary.insert(
2738        "failure_event_count".to_string(),
2739        Value::Number(failure_events.len() as f64),
2740    );
2741    summary.insert(
2742        "failure_envelope_count".to_string(),
2743        Value::Number(failure_envelopes.len() as f64),
2744    );
2745    summary.insert(
2746        "failure_lifecycle_event_count".to_string(),
2747        Value::Number(state_array_len(&failure_lifecycle, "events") as f64),
2748    );
2749    summary.insert(
2750        "failure_alert_count".to_string(),
2751        failure_alerts
2752            .get("alert_count")
2753            .cloned()
2754            .unwrap_or(Value::Number(0.0)),
2755    );
2756    summary.insert(
2757        "unsafe_replay_side_effect_count".to_string(),
2758        failure_replay_plan
2759            .get("unsafe_side_effect_count")
2760            .cloned()
2761            .unwrap_or(Value::Number(0.0)),
2762    );
2763    summary.insert(
2764        "terminal_failure_count".to_string(),
2765        Value::Number(count_state_field(&failure_envelopes, "status", "terminal") as f64),
2766    );
2767    summary.insert(
2768        "recoverable_failure_count".to_string(),
2769        Value::Number(count_recoverable_failures(&failure_envelopes) as f64),
2770    );
2771    let failure_export = failure_export(
2772        run_id,
2773        &run.status,
2774        &summary,
2775        &failure_envelopes,
2776        &failure_lifecycle,
2777        &failure_causal_graph,
2778        &failure_replay_plan,
2779        &failure_alerts,
2780    );
2781    Ok(FailureAttributionReport {
2782        run_id: run_id.to_string(),
2783        run_status: run.status,
2784        failed_steps,
2785        pending_verification,
2786        pending_approvals,
2787        failure_events,
2788        failure_envelopes,
2789        failure_lifecycle,
2790        failure_causal_graph,
2791        failure_replay_plan,
2792        failure_alerts,
2793        failure_export,
2794        summary,
2795    })
2796}
2797
2798fn failure_envelopes(
2799    run_id: &str,
2800    run_status: &str,
2801    steps: &[Step],
2802    ledger: &[ToolLedgerEntry],
2803    approvals: &[ApprovalRequest],
2804    events: &[Event],
2805) -> Vec<State> {
2806    let mut rows = Vec::new();
2807    for step in steps {
2808        if step.status == "failed" || step.status == "retry_scheduled" || step.status == "waiting_human" {
2809            let (status, severity, recoverability, retryability) = if step.status == "retry_scheduled" {
2810                ("recovery_scheduled", "warn", "auto_retry", "retryable")
2811            } else if step.status == "waiting_human" {
2812                ("waiting_human", "warn", "human_required", "unknown")
2813            } else {
2814                ("terminal", "risk", "terminal", "not_retryable")
2815            };
2816            rows.push(failure_envelope(
2817                run_id,
2818                "step",
2819                &step.step_id,
2820                &failure_category(&format!(
2821                    "{} {}",
2822                    step.last_error_type.clone().unwrap_or_default(),
2823                    step.last_error.clone().unwrap_or_default()
2824                ), "agent"),
2825                status,
2826                severity,
2827                recoverability,
2828                retryability,
2829                "agent",
2830                &first_text(&[
2831                    step.last_error.as_deref(),
2832                    step.last_error_type.as_deref(),
2833                    Some("step failure"),
2834                ]),
2835                state(&[
2836                    ("step_id", step.step_id.clone().into()),
2837                    ("occurred_at", Value::Number(step.updated_at)),
2838                ]),
2839                vec![ref_state("step", &step.step_id)],
2840            ));
2841        }
2842    }
2843    for entry in ledger {
2844        if entry.status == "PENDING_VERIFICATION" || entry.status == "FAILED" || entry.status == "ERROR" {
2845            let terminal = entry.status == "FAILED" || entry.status == "ERROR";
2846            rows.push(failure_envelope(
2847                run_id,
2848                "tool_ledger",
2849                &first_text(&[Some(&entry.ledger_id), Some(&entry.tool_name), Some(&entry.step_id)]),
2850                "tool",
2851                if terminal { "terminal" } else { "unknown_side_effect" },
2852                if terminal { "risk" } else { "warn" },
2853                if terminal { "terminal" } else { "manual_verification" },
2854                if terminal { "not_retryable" } else { "unknown" },
2855                "tool",
2856                &first_text(&[entry.error_type.as_deref(), Some("tool side effect requires verification")]),
2857                state(&[
2858                    ("step_id", entry.step_id.clone().into()),
2859                    ("tool_name", entry.tool_name.clone().into()),
2860                    ("occurred_at", Value::Number(entry.updated_at)),
2861                ]),
2862                vec![ref_state("step", &entry.step_id), ref_state("tool", &entry.tool_name)],
2863            ));
2864        }
2865    }
2866    for approval in approvals {
2867        if approval.status == "PENDING" || approval.status == "DENIED" {
2868            let denied = approval.status == "DENIED";
2869            rows.push(failure_envelope(
2870                run_id,
2871                "approval",
2872                &first_text(&[Some(&approval.approval_id), Some(&approval.tool_name), Some(&approval.step_id)]),
2873                if denied { "policy" } else { "approval" },
2874                if denied { "blocked" } else { "waiting_human" },
2875                if denied { "risk" } else { "warn" },
2876                if denied { "terminal" } else { "human_required" },
2877                if denied { "not_retryable" } else { "unknown" },
2878                "policy",
2879                &first_text(&[
2880                    approval.decision_reason.as_deref(),
2881                    Some(&approval.reason),
2882                    Some(if denied { "approval denied" } else { "approval pending" }),
2883                ]),
2884                state(&[
2885                    ("step_id", approval.step_id.clone().into()),
2886                    ("tool_name", approval.tool_name.clone().into()),
2887                    ("approval_id", approval.approval_id.clone().into()),
2888                    ("occurred_at", Value::Number(approval.updated_at)),
2889                ]),
2890                vec![
2891                    ref_state("step", &approval.step_id),
2892                    ref_state("tool", &approval.tool_name),
2893                    ref_state("approval", &approval.approval_id),
2894                ],
2895            ));
2896        }
2897    }
2898    for event in events {
2899        let category = event_failure_category(event);
2900        let status = event_failure_status(&event.event_type, run_status);
2901        let message = first_text(&[
2902            state_string(&event.payload, "error").as_deref(),
2903            state_string(&event.payload, "reason").as_deref(),
2904            state_string(&event.payload, "error_type").as_deref(),
2905            Some(&event.event_type),
2906        ]);
2907        rows.push(failure_envelope(
2908            run_id,
2909            "event",
2910            &event.seq.to_string(),
2911            &category,
2912            &status,
2913            if status == "terminal" || status == "blocked" || status == "failed" { "risk" } else { "warn" },
2914            &event_recoverability(&event.event_type, run_status),
2915            &event_retryability(&event.event_type),
2916            &owner_for_failure_category(&category),
2917            &message,
2918            state(&[
2919                ("step_id", event.step_id.clone().unwrap_or_default().into()),
2920                ("event_seq", Value::Number(event.seq as f64)),
2921                ("event_type", event.event_type.clone().into()),
2922                ("occurred_at", Value::Number(event.timestamp)),
2923            ]),
2924            vec![
2925                ref_state("event", &event.seq.to_string()),
2926                ref_state("step", event.step_id.as_deref().unwrap_or("")),
2927            ],
2928        ));
2929    }
2930    dedupe_states(rows, "failure_id")
2931}
2932
2933fn failure_envelope(
2934    run_id: &str,
2935    source_kind: &str,
2936    source_id: &str,
2937    category: &str,
2938    status: &str,
2939    severity: &str,
2940    recoverability: &str,
2941    retryability: &str,
2942    owner: &str,
2943    message: &str,
2944    extra: State,
2945    refs: Vec<State>,
2946) -> State {
2947    let mut row = state(&[
2948        ("schema_version", "agentledger.failure.envelope.v1".into()),
2949        ("failure_id", format!("failure-{}", slug(&format!("{run_id}-{source_kind}-{source_id}"))).into()),
2950        ("run_id", run_id.into()),
2951        ("source_kind", source_kind.into()),
2952        ("source_id", source_id.into()),
2953        ("category", category.into()),
2954        ("status", status.into()),
2955        ("severity", severity.into()),
2956        ("recoverability", recoverability.into()),
2957        ("retryability", retryability.into()),
2958        ("owner", owner.into()),
2959        ("message", message.into()),
2960        ("causal_refs", Value::Array(refs.iter().cloned().map(Value::Object).collect())),
2961        ("evidence_refs", Value::Array(refs.into_iter().map(Value::Object).collect())),
2962    ]);
2963    for (key, value) in extra {
2964        if value != Value::String(String::new()) && value != Value::Null {
2965            row.insert(key, value);
2966        }
2967    }
2968    row
2969}
2970
2971fn failure_lifecycle(run_id: &str, run_status: &str, envelopes: &[State]) -> State {
2972    let mut events = Vec::new();
2973    for env in envelopes {
2974        events.push(lifecycle_row(run_id, env, "failure_detected", state_value_string(env, "message"), state_value_string(env, "severity")));
2975        events.push(lifecycle_row(run_id, env, "failure_classified", state_value_string(env, "category"), state_value_string(env, "severity")));
2976        let status = state_value_string(env, "status");
2977        let recoverability = state_value_string(env, "recoverability");
2978        if ["recovery_scheduled", "waiting_human", "unknown_side_effect"].contains(&status.as_str()) || ["auto_retry", "human_required", "manual_verification"].contains(&recoverability.as_str()) {
2979            events.push(lifecycle_row(run_id, env, "failure_recovery_scheduled", "recovery scheduled".to_string(), "warn".to_string()));
2980        }
2981        if ["terminal", "blocked"].contains(&status.as_str()) || recoverability == "terminal" {
2982            events.push(lifecycle_row(run_id, env, "failure_terminal", state_value_string(env, "message"), "risk".to_string()));
2983        }
2984    }
2985    state(&[
2986        ("schema_version", "agentledger.failure.lifecycle.v1".into()),
2987        ("run_id", run_id.into()),
2988        ("run_status", run_status.into()),
2989        ("events", Value::Array(events.iter().cloned().map(Value::Object).collect())),
2990        ("terminal", Value::Bool(events.iter().any(|row| state_value_string(row, "stage") == "failure_terminal"))),
2991        ("recoverable", Value::Bool(events.iter().any(|row| state_value_string(row, "stage") == "failure_recovery_scheduled"))),
2992    ])
2993}
2994
2995fn lifecycle_row(run_id: &str, env: &State, stage: &str, message: String, severity: String) -> State {
2996    state(&[
2997        ("schema_version", "agentledger.failure.lifecycle.v1".into()),
2998        ("stage", stage.into()),
2999        ("run_id", run_id.into()),
3000        ("failure_id", state_value_string(env, "failure_id").into()),
3001        ("category", state_value_string(env, "category").into()),
3002        ("recoverability", state_value_string(env, "recoverability").into()),
3003        ("retryability", state_value_string(env, "retryability").into()),
3004        ("owner", state_value_string(env, "owner").into()),
3005        ("message", message.into()),
3006        ("severity", severity.into()),
3007        ("causal_refs", env.get("causal_refs").cloned().unwrap_or(Value::Array(vec![]))),
3008    ])
3009}
3010
3011fn failure_causal_graph(
3012    run_id: &str,
3013    run_status: &str,
3014    envelopes: &[State],
3015    steps: &[Step],
3016    ledger: &[ToolLedgerEntry],
3017    approvals: &[ApprovalRequest],
3018    events: &[Event],
3019    costs: &[CostRecord],
3020) -> State {
3021    let mut nodes = vec![state(&[("id", format!("run:{}", slug(run_id)).into()), ("kind", "run".into()), ("status", run_status.into())])];
3022    let mut edges = Vec::new();
3023    for step in steps {
3024        nodes.push(state(&[("id", format!("step:{}", slug(&step.step_id)).into()), ("kind", "step".into()), ("status", step.status.clone().into())]));
3025        edges.push(state(&[("source", format!("run:{}", slug(run_id)).into()), ("target", format!("step:{}", slug(&step.step_id)).into()), ("kind", "contains_step".into())]));
3026    }
3027    for event in events {
3028        nodes.push(state(&[("id", format!("event:{}", event.seq).into()), ("kind", "event".into()), ("event_type", event.event_type.clone().into())]));
3029        edges.push(state(&[("source", format!("run:{}", slug(run_id)).into()), ("target", format!("event:{}", event.seq).into()), ("kind", "emitted_event".into())]));
3030    }
3031    for entry in ledger {
3032        nodes.push(state(&[("id", format!("tool:{}", slug(&entry.tool_name)).into()), ("kind", "tool".into()), ("status", entry.status.clone().into())]));
3033    }
3034    for approval in approvals {
3035        nodes.push(state(&[("id", format!("approval:{}", slug(&approval.approval_id)).into()), ("kind", "approval".into()), ("status", approval.status.clone().into())]));
3036    }
3037    for cost in costs {
3038        nodes.push(state(&[("id", format!("cost:{}", slug(&cost.cost_id)).into()), ("kind", "cost".into()), ("category", cost.category.clone().into()), ("amount", Value::Number(cost.amount)), ("unit", cost.unit.clone().into())]));
3039    }
3040    for env in envelopes {
3041        let id = format!("failure:{}", slug(&state_value_string(env, "failure_id")));
3042        nodes.push(state(&[("id", id.clone().into()), ("kind", "failure".into()), ("category", state_value_string(env, "category").into()), ("status", state_value_string(env, "status").into()), ("owner", state_value_string(env, "owner").into())]));
3043        edges.push(state(&[("source", format!("run:{}", slug(run_id)).into()), ("target", id.into()), ("kind", "has_failure".into())]));
3044    }
3045    let nodes = dedupe_states(nodes, "id");
3046    state(&[
3047        ("schema_version", "agentledger.failure.causal_graph.v1".into()),
3048        ("run_id", run_id.into()),
3049        ("nodes", Value::Array(nodes.iter().cloned().map(Value::Object).collect())),
3050        ("edges", Value::Array(edges.iter().cloned().map(Value::Object).collect())),
3051        ("summary", Value::Object(state(&[
3052            ("node_count", Value::Number(nodes.len() as f64)),
3053            ("edge_count", Value::Number(edges.len() as f64)),
3054            ("failure_node_count", Value::Number(count_state_field(&nodes, "kind", "failure") as f64)),
3055        ]))),
3056    ])
3057}
3058
3059fn failure_replay_plan(run_id: &str, envelopes: &[State], ledger: &[ToolLedgerEntry], events: &[Event]) -> State {
3060    let mut actions = Vec::new();
3061    let mut unsafe_count = 0;
3062    let mut manual_count = 0;
3063    for env in envelopes {
3064        let status = state_value_string(env, "status");
3065        let recoverability = state_value_string(env, "recoverability");
3066        let mut action = state(&[
3067            ("failure_id", state_value_string(env, "failure_id").into()),
3068            ("category", state_value_string(env, "category").into()),
3069            ("status", status.clone().into()),
3070            ("replay_action", "reuse_recorded_evidence".into()),
3071            ("replay_safe", Value::Bool(true)),
3072            ("requires_manual_verification", Value::Bool(false)),
3073            ("reason", "recorded runtime evidence can be inspected without calling external systems".into()),
3074        ]);
3075        if status == "unknown_side_effect" || recoverability == "manual_verification" {
3076            action.insert("replay_action".into(), "manual_verify_side_effect".into());
3077            action.insert("replay_safe".into(), Value::Bool(false));
3078            action.insert("requires_manual_verification".into(), Value::Bool(true));
3079            action.insert("reason".into(), "Tool Ledger recorded an unknown side-effect state".into());
3080            unsafe_count += 1;
3081            manual_count += 1;
3082        } else if status == "waiting_human" {
3083            action.insert("replay_action".into(), "resume_after_approval".into());
3084        } else if status == "recovery_scheduled" {
3085            action.insert("replay_action".into(), "retry_from_checkpoint".into());
3086        } else if status == "terminal" || status == "blocked" {
3087            action.insert("replay_action".into(), "terminal_stop".into());
3088        }
3089        actions.push(action);
3090    }
3091    state(&[
3092        ("schema_version", "agentledger.failure.replay_plan.v1".into()),
3093        ("run_id", run_id.into()),
3094        ("mode", "evidence_only".into()),
3095        ("safe_to_replay", Value::Bool(unsafe_count == 0)),
3096        ("unsafe_side_effect_count", Value::Number(unsafe_count as f64)),
3097        ("manual_verification_count", Value::Number(manual_count as f64)),
3098        ("recorded_tool_call_count", Value::Number(ledger.len() as f64)),
3099        ("recorded_event_count", Value::Number(events.len() as f64)),
3100        ("actions", Value::Array(actions.into_iter().map(Value::Object).collect())),
3101    ])
3102}
3103
3104fn failure_alerts(run_id: &str, envelopes: &[State], replay_plan: &State) -> State {
3105    let mut alerts = Vec::new();
3106    if count_state_field(envelopes, "status", "terminal") > 0 {
3107        alerts.push(alert_state(run_id, "terminal_failure", "risk", "terminal failure recorded"));
3108    }
3109    if count_state_field(envelopes, "status", "unknown_side_effect") > 0 {
3110        alerts.push(alert_state(run_id, "unknown_side_effect", "risk", "tool side-effect state requires manual verification"));
3111    }
3112    if state_number(replay_plan, "unsafe_side_effect_count") > 0.0 {
3113        alerts.push(alert_state(run_id, "unsafe_replay_blocked", "risk", "failure replay plan blocks unsafe automatic replay"));
3114    }
3115    state(&[
3116        ("schema_version", "agentledger.failure.alerts.v1".into()),
3117        ("run_id", run_id.into()),
3118        ("alerts", Value::Array(alerts.iter().cloned().map(Value::Object).collect())),
3119        ("alert_count", Value::Number(alerts.len() as f64)),
3120    ])
3121}
3122
3123fn alert_state(run_id: &str, kind: &str, severity: &str, message: &str) -> State {
3124    state(&[
3125        ("schema_version", "agentledger.failure.alerts.v1".into()),
3126        ("run_id", run_id.into()),
3127        ("kind", kind.into()),
3128        ("severity", severity.into()),
3129        ("message", message.into()),
3130    ])
3131}
3132
3133fn failure_export(
3134    run_id: &str,
3135    run_status: &str,
3136    summary: &State,
3137    envelopes: &[State],
3138    lifecycle: &State,
3139    graph: &State,
3140    replay_plan: &State,
3141    alerts: &State,
3142) -> State {
3143    state(&[
3144        ("schema_version", "agentledger.failure.export.v1".into()),
3145        ("run_id", run_id.into()),
3146        ("run_status", run_status.into()),
3147        ("summary", Value::Object(summary.clone())),
3148        ("failure_envelopes", Value::Array(envelopes.iter().cloned().map(Value::Object).collect())),
3149        ("failure_lifecycle", Value::Object(lifecycle.clone())),
3150        ("failure_causal_graph", Value::Object(graph.clone())),
3151        ("failure_replay_plan", Value::Object(replay_plan.clone())),
3152        ("failure_alerts", Value::Object(alerts.clone())),
3153        ("external_mappings", Value::Object(state(&[
3154            ("opentelemetry", Value::Object(state(&[("span_event_count", Value::Number(state_array_len(lifecycle, "events") as f64))]))),
3155            ("langfuse", Value::Object(state(&[("trace_id", run_id.into()), ("observation_count", Value::Number(envelopes.len() as f64))]))),
3156            ("langsmith", Value::Object(state(&[("run_id", run_id.into()), ("feedback_count", Value::Number(envelopes.len() as f64))]))),
3157            ("temporal", Value::Object(state(&[("workflow_id", run_id.into()), ("failure_count", Value::Number(envelopes.len() as f64)), ("safe_to_replay", replay_plan.get("safe_to_replay").cloned().unwrap_or(Value::Bool(false)))]))),
3158        ]))),
3159    ])
3160}
3161
3162fn encode_store(store: &MemoryStore) -> String {
3163    let mut lines = vec!["AGENTLEDGER_RUST_STORE_V1".to_string()];
3164    let mut runs: Vec<_> = store.runs.values().collect();
3165    runs.sort_by(|a, b| a.run_id.cmp(&b.run_id));
3166    for run in runs {
3167        lines.push(join_fields(&[
3168            "R".to_string(),
3169            hex_encode(&run.run_id),
3170            hex_encode(&run.session_id),
3171            hex_encode(&run.status),
3172            run.state_version.to_string(),
3173            run.created_at.to_string(),
3174            run.updated_at.to_string(),
3175            encode_state(&run.state),
3176        ]));
3177    }
3178
3179    let mut steps: Vec<_> = store.steps.values().collect();
3180    steps.sort_by(|a, b| a.step_id.cmp(&b.step_id));
3181    for step in steps {
3182        lines.push(join_fields(&[
3183            "S".to_string(),
3184            hex_encode(&step.step_id),
3185            hex_encode(&step.run_id),
3186            hex_encode(&step.session_id),
3187            hex_encode(&step.status),
3188            encode_option_string(&step.owner),
3189            encode_option_string(&step.lease_token),
3190            encode_option_f64(step.lease_until),
3191            step.attempt.to_string(),
3192            step.state_version.to_string(),
3193            encode_option_string(&step.checkpoint_id),
3194            encode_option_string(&step.last_error_type),
3195            encode_option_string(&step.last_error),
3196            encode_option_f64(step.cancelled_at),
3197            step.created_at.to_string(),
3198            step.updated_at.to_string(),
3199        ]));
3200    }
3201
3202    let mut run_ids: Vec<_> = store.events.keys().collect();
3203    run_ids.sort();
3204    for run_id in run_ids {
3205        if let Some(events) = store.events.get(run_id) {
3206            for event in events {
3207                lines.push(join_fields(&[
3208                    "E".to_string(),
3209                    hex_encode(&event.event_id),
3210                    hex_encode(&event.run_id),
3211                    encode_option_string(&event.session_id),
3212                    encode_option_string(&event.step_id),
3213                    event.seq.to_string(),
3214                    hex_encode(&event.event_type),
3215                    event.timestamp.to_string(),
3216                    encode_option_string(&event.agent_role),
3217                    encode_option_u64(event.state_version),
3218                    encode_option_string(&event.causal_token),
3219                    hex_encode(&event.payload_hash),
3220                    hex_encode(&event.payload_ref),
3221                    encode_state(&event.payload),
3222                ]));
3223            }
3224        }
3225    }
3226
3227    let mut ledgers: Vec<_> = store.tool_ledger.values().collect();
3228    ledgers.sort_by(|a, b| a.idempotency_key.cmp(&b.idempotency_key));
3229    for entry in ledgers {
3230        lines.push(join_fields(&[
3231            "L".to_string(),
3232            hex_encode(&entry.ledger_id),
3233            hex_encode(&entry.run_id),
3234            hex_encode(&entry.session_id),
3235            hex_encode(&entry.step_id),
3236            hex_encode(&entry.tool_name),
3237            hex_encode(&entry.tool_version),
3238            hex_encode(&entry.tool_call_id),
3239            hex_encode(&entry.idempotency_key),
3240            hex_encode(&entry.causal_token),
3241            hex_encode(&entry.request_hash),
3242            hex_encode(&entry.request_ref),
3243            hex_encode(&entry.status),
3244            encode_option_string(&entry.external_id),
3245            encode_option_string(&entry.response_hash),
3246            encode_option_string(&entry.response_ref),
3247            encode_option_string(&entry.error_type),
3248            encode_option_value(&entry.response),
3249            entry.created_at.to_string(),
3250            entry.updated_at.to_string(),
3251        ]));
3252    }
3253
3254    let mut approvals: Vec<_> = store.approval_requests.values().collect();
3255    approvals.sort_by(|a, b| a.approval_key.cmp(&b.approval_key));
3256    for approval in approvals {
3257        lines.push(join_fields(&[
3258            "A".to_string(),
3259            hex_encode(&approval.approval_id),
3260            hex_encode(&approval.approval_key),
3261            hex_encode(&approval.run_id),
3262            hex_encode(&approval.session_id),
3263            hex_encode(&approval.step_id),
3264            hex_encode(&approval.tool_name),
3265            hex_encode(&approval.risk_level),
3266            hex_encode(&approval.status),
3267            hex_encode(&approval.reason),
3268            hex_encode(&approval.request_hash),
3269            hex_encode(&approval.request_ref),
3270            hex_encode(&approval.requested_by),
3271            encode_option_string(&approval.approved_by),
3272            encode_option_string(&approval.decision_reason),
3273            approval.created_at.to_string(),
3274            approval.updated_at.to_string(),
3275        ]));
3276    }
3277
3278    let mut cost_run_ids: Vec<_> = store.cost_records.keys().collect();
3279    cost_run_ids.sort();
3280    for run_id in cost_run_ids {
3281        if let Some(records) = store.cost_records.get(run_id) {
3282            for record in records {
3283                lines.push(join_fields(&[
3284                    "C".to_string(),
3285                    hex_encode(&record.cost_id),
3286                    hex_encode(&record.run_id),
3287                    hex_encode(&record.session_id),
3288                    hex_encode(&record.step_id),
3289                    hex_encode(&record.category),
3290                    hex_encode(&record.name),
3291                    record.amount.to_string(),
3292                    hex_encode(&record.unit),
3293                    encode_state(&record.metadata),
3294                    record.created_at.to_string(),
3295                ]));
3296            }
3297        }
3298    }
3299
3300    let mut artifact_run_ids: Vec<_> = store.artifacts.keys().collect();
3301    artifact_run_ids.sort();
3302    for run_id in artifact_run_ids {
3303        if let Some(artifacts) = store.artifacts.get(run_id) {
3304            for artifact in artifacts {
3305                lines.push(join_fields(&[
3306                    "F".to_string(),
3307                    hex_encode(&artifact.artifact_id),
3308                    hex_encode(&artifact.run_id),
3309                    encode_option_string(&artifact.step_id),
3310                    hex_encode(&artifact.name),
3311                    hex_encode(&artifact.blob_hash),
3312                    hex_encode(&artifact.blob_ref),
3313                    encode_state(&artifact.metadata),
3314                    artifact.created_at.to_string(),
3315                ]));
3316            }
3317        }
3318    }
3319
3320    lines.push(String::new());
3321    lines.join("\n")
3322}
3323
3324fn decode_store(body: &str) -> Result<MemoryStore> {
3325    let mut lines = body.lines();
3326    match lines.next() {
3327        Some("AGENTLEDGER_RUST_STORE_V1") => {}
3328        _ => {
3329            return Err(RuntimeError(
3330                "invalid Rust store snapshot header".to_string(),
3331            ))
3332        }
3333    }
3334    let mut store = MemoryStore::new();
3335    for line in lines.filter(|line| !line.trim().is_empty()) {
3336        let fields: Vec<&str> = line.split('\t').collect();
3337        let tag = fields.first().copied().unwrap_or_default();
3338        match tag {
3339            "R" => {
3340                require_len(tag, &fields, 8)?;
3341                let run = Run {
3342                    run_id: hex_decode(fields[1])?,
3343                    session_id: hex_decode(fields[2])?,
3344                    status: hex_decode(fields[3])?,
3345                    state_version: parse_u64(fields[4])?,
3346                    created_at: parse_f64(fields[5])?,
3347                    updated_at: parse_f64(fields[6])?,
3348                    state: decode_state(fields[7])?,
3349                };
3350                store.runs.insert(run.run_id.clone(), run);
3351            }
3352            "S" => {
3353                require_len(tag, &fields, 16)?;
3354                let step = Step {
3355                    step_id: hex_decode(fields[1])?,
3356                    run_id: hex_decode(fields[2])?,
3357                    session_id: hex_decode(fields[3])?,
3358                    status: hex_decode(fields[4])?,
3359                    owner: decode_option_string(fields[5])?,
3360                    lease_token: decode_option_string(fields[6])?,
3361                    lease_until: decode_option_f64(fields[7])?,
3362                    attempt: parse_u64(fields[8])?,
3363                    state_version: parse_u64(fields[9])?,
3364                    checkpoint_id: decode_option_string(fields[10])?,
3365                    last_error_type: decode_option_string(fields[11])?,
3366                    last_error: decode_option_string(fields[12])?,
3367                    cancelled_at: decode_option_f64(fields[13])?,
3368                    created_at: parse_f64(fields[14])?,
3369                    updated_at: parse_f64(fields[15])?,
3370                };
3371                store.steps.insert(step.step_id.clone(), step);
3372            }
3373            "E" => {
3374                require_len(tag, &fields, 14)?;
3375                let event = Event {
3376                    event_id: hex_decode(fields[1])?,
3377                    run_id: hex_decode(fields[2])?,
3378                    session_id: decode_option_string(fields[3])?,
3379                    step_id: decode_option_string(fields[4])?,
3380                    seq: parse_u64(fields[5])?,
3381                    event_type: hex_decode(fields[6])?,
3382                    timestamp: parse_f64(fields[7])?,
3383                    agent_role: decode_option_string(fields[8])?,
3384                    state_version: decode_option_u64(fields[9])?,
3385                    causal_token: decode_option_string(fields[10])?,
3386                    payload_hash: hex_decode(fields[11])?,
3387                    payload_ref: hex_decode(fields[12])?,
3388                    payload: decode_state(fields[13])?,
3389                };
3390                store
3391                    .events
3392                    .entry(event.run_id.clone())
3393                    .or_default()
3394                    .push(event);
3395            }
3396            "L" => {
3397                require_len(tag, &fields, 20)?;
3398                let entry = ToolLedgerEntry {
3399                    ledger_id: hex_decode(fields[1])?,
3400                    run_id: hex_decode(fields[2])?,
3401                    session_id: hex_decode(fields[3])?,
3402                    step_id: hex_decode(fields[4])?,
3403                    tool_name: hex_decode(fields[5])?,
3404                    tool_version: hex_decode(fields[6])?,
3405                    tool_call_id: hex_decode(fields[7])?,
3406                    idempotency_key: hex_decode(fields[8])?,
3407                    causal_token: hex_decode(fields[9])?,
3408                    request_hash: hex_decode(fields[10])?,
3409                    request_ref: hex_decode(fields[11])?,
3410                    status: hex_decode(fields[12])?,
3411                    external_id: decode_option_string(fields[13])?,
3412                    response_hash: decode_option_string(fields[14])?,
3413                    response_ref: decode_option_string(fields[15])?,
3414                    error_type: decode_option_string(fields[16])?,
3415                    response: decode_option_value(fields[17])?,
3416                    created_at: parse_f64(fields[18])?,
3417                    updated_at: parse_f64(fields[19])?,
3418                };
3419                store
3420                    .tool_ledger
3421                    .insert(entry.idempotency_key.clone(), entry);
3422            }
3423            "A" => {
3424                require_len(tag, &fields, 17)?;
3425                let approval = ApprovalRequest {
3426                    approval_id: hex_decode(fields[1])?,
3427                    approval_key: hex_decode(fields[2])?,
3428                    run_id: hex_decode(fields[3])?,
3429                    session_id: hex_decode(fields[4])?,
3430                    step_id: hex_decode(fields[5])?,
3431                    tool_name: hex_decode(fields[6])?,
3432                    risk_level: hex_decode(fields[7])?,
3433                    status: hex_decode(fields[8])?,
3434                    reason: hex_decode(fields[9])?,
3435                    request_hash: hex_decode(fields[10])?,
3436                    request_ref: hex_decode(fields[11])?,
3437                    requested_by: hex_decode(fields[12])?,
3438                    approved_by: decode_option_string(fields[13])?,
3439                    decision_reason: decode_option_string(fields[14])?,
3440                    created_at: parse_f64(fields[15])?,
3441                    updated_at: parse_f64(fields[16])?,
3442                };
3443                store
3444                    .approval_requests
3445                    .insert(approval.approval_key.clone(), approval);
3446            }
3447            "C" => {
3448                require_len(tag, &fields, 11)?;
3449                let record = CostRecord {
3450                    cost_id: hex_decode(fields[1])?,
3451                    run_id: hex_decode(fields[2])?,
3452                    session_id: hex_decode(fields[3])?,
3453                    step_id: hex_decode(fields[4])?,
3454                    category: hex_decode(fields[5])?,
3455                    name: hex_decode(fields[6])?,
3456                    amount: parse_f64(fields[7])?,
3457                    unit: hex_decode(fields[8])?,
3458                    metadata: decode_state(fields[9])?,
3459                    created_at: parse_f64(fields[10])?,
3460                };
3461                store
3462                    .cost_records
3463                    .entry(record.run_id.clone())
3464                    .or_default()
3465                    .push(record);
3466            }
3467            "F" => {
3468                require_len(tag, &fields, 9)?;
3469                let artifact = Artifact {
3470                    artifact_id: hex_decode(fields[1])?,
3471                    run_id: hex_decode(fields[2])?,
3472                    step_id: decode_option_string(fields[3])?,
3473                    name: hex_decode(fields[4])?,
3474                    blob_hash: hex_decode(fields[5])?,
3475                    blob_ref: hex_decode(fields[6])?,
3476                    metadata: decode_state(fields[7])?,
3477                    created_at: parse_f64(fields[8])?,
3478                };
3479                store
3480                    .artifacts
3481                    .entry(artifact.run_id.clone())
3482                    .or_default()
3483                    .push(artifact);
3484            }
3485            _ => {
3486                return Err(RuntimeError(format!(
3487                    "unknown Rust store snapshot row: {tag}"
3488                )))
3489            }
3490        }
3491    }
3492    Ok(store)
3493}
3494
3495fn join_fields(fields: &[String]) -> String {
3496    fields.join("\t")
3497}
3498
3499fn require_len(tag: &str, fields: &[&str], expected: usize) -> Result<()> {
3500    if fields.len() != expected {
3501        return Err(RuntimeError(format!(
3502            "invalid {tag} row: expected {expected} fields, got {}",
3503            fields.len()
3504        )));
3505    }
3506    Ok(())
3507}
3508
3509fn encode_option_string(value: &Option<String>) -> String {
3510    value
3511        .as_ref()
3512        .map(|item| hex_encode(item))
3513        .unwrap_or_else(|| "-".to_string())
3514}
3515
3516fn decode_option_string(value: &str) -> Result<Option<String>> {
3517    if value == "-" {
3518        Ok(None)
3519    } else {
3520        Ok(Some(hex_decode(value)?))
3521    }
3522}
3523
3524fn encode_option_f64(value: Option<f64>) -> String {
3525    value
3526        .map(|item| item.to_string())
3527        .unwrap_or_else(|| "-".to_string())
3528}
3529
3530fn decode_option_f64(value: &str) -> Result<Option<f64>> {
3531    if value == "-" {
3532        Ok(None)
3533    } else {
3534        Ok(Some(parse_f64(value)?))
3535    }
3536}
3537
3538fn encode_option_u64(value: Option<u64>) -> String {
3539    value
3540        .map(|item| item.to_string())
3541        .unwrap_or_else(|| "-".to_string())
3542}
3543
3544fn decode_option_u64(value: &str) -> Result<Option<u64>> {
3545    if value == "-" {
3546        Ok(None)
3547    } else {
3548        Ok(Some(parse_u64(value)?))
3549    }
3550}
3551
3552fn encode_option_value(value: &Option<Value>) -> String {
3553    value
3554        .as_ref()
3555        .map(encode_value)
3556        .unwrap_or_else(|| "-".to_string())
3557}
3558
3559fn decode_option_value(value: &str) -> Result<Option<Value>> {
3560    if value == "-" {
3561        Ok(None)
3562    } else {
3563        Ok(Some(decode_value(value)?))
3564    }
3565}
3566
3567fn encode_state(state: &State) -> String {
3568    encode_value(&Value::Object(state.clone()))
3569}
3570
3571fn decode_state(encoded: &str) -> Result<State> {
3572    match decode_value(encoded)? {
3573        Value::Object(state) => Ok(state),
3574        _ => Err(RuntimeError("encoded state was not an object".to_string())),
3575    }
3576}
3577
3578fn encode_value(value: &Value) -> String {
3579    match value {
3580        Value::Null => "Z".to_string(),
3581        Value::Bool(true) => "T".to_string(),
3582        Value::Bool(false) => "F".to_string(),
3583        Value::Number(item) => format!("N{}:", hex_encode(&item.to_string())),
3584        Value::String(item) => format!("S{}:", hex_encode(item)),
3585        Value::Object(state) => {
3586            let mut keys: Vec<_> = state.keys().collect();
3587            keys.sort();
3588            let mut out = format!("O{}:", keys.len());
3589            for key in keys {
3590                out.push_str(&hex_encode(key));
3591                out.push(':');
3592                out.push_str(&encode_value(&state[key]));
3593            }
3594            out
3595        }
3596        Value::Array(values) => {
3597            let mut out = format!("A{}:", values.len());
3598            for value in values {
3599                out.push_str(&encode_value(value));
3600            }
3601            out
3602        }
3603    }
3604}
3605
3606fn decode_value(encoded: &str) -> Result<Value> {
3607    let (value, index) = parse_value(encoded, 0)?;
3608    if index != encoded.len() {
3609        return Err(RuntimeError("trailing bytes in encoded value".to_string()));
3610    }
3611    Ok(value)
3612}
3613
3614fn parse_value(input: &str, index: usize) -> Result<(Value, usize)> {
3615    let bytes = input.as_bytes();
3616    let tag = *bytes
3617        .get(index)
3618        .ok_or_else(|| RuntimeError("unexpected end of encoded value".to_string()))?
3619        as char;
3620    match tag {
3621        'Z' => Ok((Value::Null, index + 1)),
3622        'T' => Ok((Value::Bool(true), index + 1)),
3623        'F' => Ok((Value::Bool(false), index + 1)),
3624        'N' => {
3625            let (hex, next) = read_until_colon(input, index + 1)?;
3626            Ok((Value::Number(parse_f64(&hex_decode(hex)?)?), next))
3627        }
3628        'S' => {
3629            let (hex, next) = read_until_colon(input, index + 1)?;
3630            Ok((Value::String(hex_decode(hex)?), next))
3631        }
3632        'O' => {
3633            let (count_text, mut next) = read_until_colon(input, index + 1)?;
3634            let count = count_text
3635                .parse::<usize>()
3636                .map_err(|err| RuntimeError(err.to_string()))?;
3637            let mut state = State::new();
3638            for _ in 0..count {
3639                let (key_hex, after_key) = read_until_colon(input, next)?;
3640                let key = hex_decode(key_hex)?;
3641                let (value, after_value) = parse_value(input, after_key)?;
3642                state.insert(key, value);
3643                next = after_value;
3644            }
3645            Ok((Value::Object(state), next))
3646        }
3647        'A' => {
3648            let (count_text, mut next) = read_until_colon(input, index + 1)?;
3649            let count = count_text
3650                .parse::<usize>()
3651                .map_err(|err| RuntimeError(err.to_string()))?;
3652            let mut values = Vec::with_capacity(count);
3653            for _ in 0..count {
3654                let (value, after_value) = parse_value(input, next)?;
3655                values.push(value);
3656                next = after_value;
3657            }
3658            Ok((Value::Array(values), next))
3659        }
3660        _ => Err(RuntimeError(format!("unknown encoded value tag: {tag}"))),
3661    }
3662}
3663
3664fn read_until_colon(input: &str, index: usize) -> Result<(&str, usize)> {
3665    let rest = input
3666        .get(index..)
3667        .ok_or_else(|| RuntimeError("invalid encoded value index".to_string()))?;
3668    let offset = rest
3669        .find(':')
3670        .ok_or_else(|| RuntimeError("missing encoded value delimiter".to_string()))?;
3671    Ok((&rest[..offset], index + offset + 1))
3672}
3673
3674fn hex_encode(value: &str) -> String {
3675    value
3676        .as_bytes()
3677        .iter()
3678        .map(|byte| format!("{byte:02x}"))
3679        .collect()
3680}
3681
3682fn hex_decode(value: &str) -> Result<String> {
3683    if value.len() % 2 != 0 {
3684        return Err(RuntimeError("invalid hex string length".to_string()));
3685    }
3686    let mut bytes = Vec::with_capacity(value.len() / 2);
3687    for index in (0..value.len()).step_by(2) {
3688        let byte = u8::from_str_radix(&value[index..index + 2], 16)
3689            .map_err(|err| RuntimeError(err.to_string()))?;
3690        bytes.push(byte);
3691    }
3692    String::from_utf8(bytes).map_err(|err| RuntimeError(err.to_string()))
3693}
3694
3695fn parse_f64(value: &str) -> Result<f64> {
3696    value
3697        .parse::<f64>()
3698        .map_err(|err| RuntimeError(err.to_string()))
3699}
3700
3701fn parse_u64(value: &str) -> Result<u64> {
3702    value
3703        .parse::<u64>()
3704        .map_err(|err| RuntimeError(err.to_string()))
3705}
3706
3707fn merge_patch(base: &State, patch: &State) -> State {
3708    let mut out = base.clone();
3709    for (key, value) in patch {
3710        match value {
3711            Value::Null => {
3712                out.remove(key);
3713            }
3714            Value::Object(patch_map) => {
3715                if let Some(Value::Object(base_map)) = out.get(key) {
3716                    out.insert(key.clone(), Value::Object(merge_patch(base_map, patch_map)));
3717                } else {
3718                    out.insert(key.clone(), value.clone());
3719                }
3720            }
3721            _ => {
3722                out.insert(key.clone(), value.clone());
3723            }
3724        }
3725    }
3726    out
3727}
3728
3729fn add_cost(summary: &mut CostSummary, record: &CostRecord) {
3730    if (record.category == "tool" || record.category == "tool_shadow") && record.unit == "call" {
3731        summary.tool_calls += record.amount;
3732    }
3733    if record.category == "model" && record.unit == "token" {
3734        summary.model_tokens += record.amount;
3735    }
3736    if record.unit == "usd" {
3737        summary.total_usd += record.amount;
3738    }
3739    let key = format!("{}:{}", record.category, record.unit);
3740    *summary.by_category.entry(key).or_insert(0.0) += record.amount;
3741}
3742
3743pub fn validate_tool_schema(schema: &Value, value: &Value, path: &str) -> Result<()> {
3744    let schema = match schema {
3745        Value::Object(schema) => schema,
3746        _ => return Err(RuntimeError(format!("{path} schema must be object"))),
3747    };
3748    if let Some(expected) = schema.get("const") {
3749        if expected != value {
3750            return Err(RuntimeError(format!("{path} expected const")));
3751        }
3752    }
3753    if let Some(Value::Array(items)) = schema.get("enum") {
3754        if !items.iter().any(|item| item == value) {
3755            return Err(RuntimeError(format!("{path} value not in enum")));
3756        }
3757    }
3758    let Some(Value::String(kind)) = schema.get("type") else {
3759        return Ok(());
3760    };
3761    match kind.as_str() {
3762        "object" => {
3763            let Value::Object(object) = value else {
3764                return Err(RuntimeError(format!("{path} expected object")));
3765            };
3766            if let Some(Value::Array(required)) = schema.get("required") {
3767                for item in required {
3768                    if let Value::String(key) = item {
3769                        if !object.contains_key(key) {
3770                            return Err(RuntimeError(format!("{path}.{key} is required")));
3771                        }
3772                    }
3773                }
3774            }
3775            let properties = match schema.get("properties") {
3776                Some(Value::Object(properties)) => properties,
3777                _ => return Ok(()),
3778            };
3779            for (key, child_schema) in properties {
3780                if let Some(child) = object.get(key) {
3781                    validate_tool_schema(child_schema, child, &format!("{path}.{key}"))?;
3782                }
3783            }
3784            if schema.get("additionalProperties") == Some(&Value::Bool(false)) {
3785                for key in object.keys() {
3786                    if !properties.contains_key(key) {
3787                        return Err(RuntimeError(format!("{path}.{key} is not allowed")));
3788                    }
3789                }
3790            }
3791        }
3792        "string" => {
3793            let Value::String(text) = value else {
3794                return Err(RuntimeError(format!("{path} expected string")));
3795            };
3796            if let Some(Value::Number(min)) = schema.get("minLength") {
3797                if (text.len() as f64) < *min {
3798                    return Err(RuntimeError(format!("{path} shorter than minLength")));
3799                }
3800            }
3801            if let Some(Value::Number(max)) = schema.get("maxLength") {
3802                if (text.len() as f64) > *max {
3803                    return Err(RuntimeError(format!("{path} longer than maxLength")));
3804                }
3805            }
3806        }
3807        "number" | "integer" => {
3808            let Value::Number(number) = value else {
3809                return Err(RuntimeError(format!("{path} expected number")));
3810            };
3811            if kind == "integer" && number.fract() != 0.0 {
3812                return Err(RuntimeError(format!("{path} expected integer")));
3813            }
3814            if let Some(Value::Number(min)) = schema.get("minimum") {
3815                if number < min {
3816                    return Err(RuntimeError(format!("{path} below minimum")));
3817                }
3818            }
3819            if let Some(Value::Number(max)) = schema.get("maximum") {
3820                if number > max {
3821                    return Err(RuntimeError(format!("{path} above maximum")));
3822                }
3823            }
3824        }
3825        "boolean" => {
3826            if !matches!(value, Value::Bool(_)) {
3827                return Err(RuntimeError(format!("{path} expected boolean")));
3828            }
3829        }
3830        _ => {}
3831    }
3832    Ok(())
3833}
3834
3835fn is_high_risk(risk: &str) -> bool {
3836    matches!(
3837        risk,
3838        "high" | "destructive" | "sensitive" | "financial_or_legal"
3839    )
3840}
3841
3842fn is_media_kind(kind: &str) -> bool {
3843    matches!(
3844        kind,
3845        "image"
3846            | "audio"
3847            | "video"
3848            | "frame"
3849            | "audio_segment"
3850            | "video_segment"
3851            | "transcript"
3852            | "embedding"
3853            | "derived"
3854    )
3855}
3856
3857fn stream_chunk_to_state(chunk: StreamChunkRef) -> State {
3858    let mut state = State::new();
3859    state.insert(
3860        "schema_version".to_string(),
3861        Value::String(STREAM_SCHEMA_VERSION.to_string()),
3862    );
3863    state.insert("stream_id".to_string(), Value::String(chunk.stream_id));
3864    state.insert("chunk_id".to_string(), Value::String(chunk.chunk_id));
3865    state.insert("offset".to_string(), chunk.offset);
3866    if let Some(content_ref) = chunk.content_ref {
3867        state.insert("content_ref".to_string(), Value::String(content_ref));
3868    }
3869    if let Some(content_hash) = chunk.content_hash {
3870        state.insert("content_hash".to_string(), Value::String(content_hash));
3871    }
3872    if let Some(sequence) = chunk.sequence {
3873        state.insert("sequence".to_string(), Value::Number(sequence));
3874    }
3875    if let Some(event_time) = chunk.event_time {
3876        state.insert("event_time".to_string(), Value::Number(event_time));
3877    }
3878    if !chunk.metadata.is_empty() {
3879        state.insert("metadata".to_string(), Value::Object(chunk.metadata));
3880    }
3881    state
3882}
3883
3884fn media_artifacts_from(artifacts: &[Artifact]) -> Vec<State> {
3885    artifacts
3886        .iter()
3887        .filter_map(
3888            |artifact| match artifact.metadata.get("agentledger_media") {
3889                Some(Value::Object(metadata)) => {
3890                    let mut row = State::new();
3891                    row.insert(
3892                        "artifact_id".to_string(),
3893                        Value::String(artifact.artifact_id.clone()),
3894                    );
3895                    row.insert("name".to_string(), Value::String(artifact.name.clone()));
3896                    row.insert(
3897                        "blob_hash".to_string(),
3898                        Value::String(artifact.blob_hash.clone()),
3899                    );
3900                    row.insert(
3901                        "blob_ref".to_string(),
3902                        Value::String(artifact.blob_ref.clone()),
3903                    );
3904                    for key in ["kind", "uri", "content_ref", "metadata", "lineage"] {
3905                        if let Some(value) = metadata.get(key) {
3906                            row.insert(key.to_string(), value.clone());
3907                        }
3908                    }
3909                    Some(row)
3910                }
3911                _ => None,
3912            },
3913        )
3914        .collect()
3915}
3916
3917fn stream_checkpoints_from(artifacts: &[Artifact]) -> Vec<State> {
3918    artifacts
3919        .iter()
3920        .filter_map(
3921            |artifact| match artifact.metadata.get("agentledger_stream") {
3922                Some(Value::Object(metadata)) => {
3923                    let mut row = State::new();
3924                    row.insert(
3925                        "artifact_id".to_string(),
3926                        Value::String(artifact.artifact_id.clone()),
3927                    );
3928                    row.insert("name".to_string(), Value::String(artifact.name.clone()));
3929                    row.insert(
3930                        "blob_hash".to_string(),
3931                        Value::String(artifact.blob_hash.clone()),
3932                    );
3933                    row.insert(
3934                        "blob_ref".to_string(),
3935                        Value::String(artifact.blob_ref.clone()),
3936                    );
3937                    for key in [
3938                        "stream_id",
3939                        "consumer_id",
3940                        "offset",
3941                        "watermark",
3942                        "chunk",
3943                        "partial_result_ref",
3944                        "backpressure",
3945                    ] {
3946                        if let Some(value) = metadata.get(key) {
3947                            row.insert(key.to_string(), value.clone());
3948                        }
3949                    }
3950                    Some(row)
3951                }
3952                _ => None,
3953            },
3954        )
3955        .collect()
3956}
3957
3958fn classify_runtime_error(message: &str) -> &'static str {
3959    if message.contains("budget exceeded") || message.contains("budget") {
3960        "BudgetExceededError"
3961    } else if message.contains("sandbox executor") {
3962        "SandboxUnavailableError"
3963    } else if message.contains("high-risk") || message.contains("denied") {
3964        "PermissionDeniedError"
3965    } else {
3966        "RuntimeError"
3967    }
3968}
3969
3970fn failure_source(error_type: &str) -> &'static str {
3971    match error_type {
3972        "BudgetExceededError" => "budget",
3973        "SandboxUnavailableError" => "sandbox",
3974        "PermissionDeniedError" | "ApprovalDenied" => "policy",
3975        _ => "agent",
3976    }
3977}
3978
3979fn is_failure_event(kind: &str) -> bool {
3980    matches!(
3981        kind,
3982        "failure_classified"
3983            | "error_raised"
3984            | "step_failed"
3985            | "step_retry_scheduled"
3986            | "step_waiting_human"
3987            | "lease_expired"
3988            | "run_cancel_requested"
3989            | "run_cancelled"
3990            | "model_call_failed"
3991            | "tool_call_failed"
3992            | "tool_approval_required"
3993            | "budget_check_failed"
3994    )
3995}
3996
3997fn failure_category(text: &str, fallback: &str) -> String {
3998    let lower = text.to_lowercase();
3999    for category in ["sandbox", "budget", "policy", "model", "tool", "runtime"] {
4000        if lower.contains(category) {
4001            return category.to_string();
4002        }
4003    }
4004    if lower.contains("approval") || lower.contains("permission") || lower.contains("denied") {
4005        return "policy".to_string();
4006    }
4007    if lower.contains("lease") || lower.contains("worker") {
4008        return "runtime".to_string();
4009    }
4010    if lower.contains("cancel") {
4011        return "cancellation".to_string();
4012    }
4013    fallback.to_string()
4014}
4015
4016fn event_failure_category(event: &Event) -> String {
4017    match event.event_type.as_str() {
4018        "model_call_failed" => "model".to_string(),
4019        "tool_call_failed" | "tool_call_blocked" | "tool_approval_required" => "tool".to_string(),
4020        "run_cancel_requested" | "run_cancelled" | "step_cancelled" => "cancellation".to_string(),
4021        "lease_expired" => "runtime".to_string(),
4022        "step_retry_scheduled" => "retry".to_string(),
4023        "step_waiting_human" => "approval".to_string(),
4024        _ => failure_category(
4025            &format!(
4026                "{} {} {} {}",
4027                event.event_type,
4028                state_value_string(&event.payload, "error_type"),
4029                state_value_string(&event.payload, "error"),
4030                state_value_string(&event.payload, "reason")
4031            ),
4032            "agent",
4033        ),
4034    }
4035}
4036
4037fn event_failure_status(kind: &str, run_status: &str) -> String {
4038    match kind {
4039        "step_failed" | "run_cancelled" | "step_cancelled" => "terminal".to_string(),
4040        "tool_call_blocked" => "blocked".to_string(),
4041        "step_retry_scheduled" | "lease_expired" => "recovery_scheduled".to_string(),
4042        "step_waiting_human" | "tool_approval_required" => "waiting_human".to_string(),
4043        "failure_classified" => "classified".to_string(),
4044        "error_raised" if run_status == "failed" => "terminal".to_string(),
4045        _ => "failed".to_string(),
4046    }
4047}
4048
4049fn event_recoverability(kind: &str, run_status: &str) -> String {
4050    if run_status == "failed" && matches!(kind, "step_failed" | "run_cancelled" | "step_cancelled") {
4051        return "terminal".to_string();
4052    }
4053    match kind {
4054        "step_retry_scheduled" | "lease_expired" => "auto_retry".to_string(),
4055        "step_waiting_human" | "tool_approval_required" => "human_required".to_string(),
4056        "tool_call_blocked" => "manual_intervention".to_string(),
4057        _ => "unknown".to_string(),
4058    }
4059}
4060
4061fn event_retryability(kind: &str) -> String {
4062    match kind {
4063        "step_retry_scheduled" | "lease_expired" => "retryable".to_string(),
4064        "tool_call_blocked" | "run_cancelled" | "step_cancelled" => "not_retryable".to_string(),
4065        _ => "unknown".to_string(),
4066    }
4067}
4068
4069fn owner_for_failure_category(category: &str) -> String {
4070    match category {
4071        "tool" | "model" | "policy" | "sandbox" | "budget" | "runtime" => category.to_string(),
4072        "approval" | "cancellation" | "retry" => "runtime".to_string(),
4073        _ => "agent".to_string(),
4074    }
4075}
4076
4077fn first_text(values: &[Option<&str>]) -> String {
4078    for value in values {
4079        if let Some(text) = value {
4080            if !text.is_empty() {
4081                return (*text).to_string();
4082            }
4083        }
4084    }
4085    "failure signal".to_string()
4086}
4087
4088fn ref_state(kind: &str, value: &str) -> State {
4089    if value.is_empty() {
4090        return State::new();
4091    }
4092    state(&[("kind", kind.into()), ("value", value.into())])
4093}
4094
4095fn state_value_string(row: &State, key: &str) -> String {
4096    match row.get(key) {
4097        Some(Value::String(value)) => value.clone(),
4098        Some(Value::Number(value)) => value.to_string(),
4099        Some(Value::Bool(value)) => value.to_string(),
4100        Some(value) => format_value(value),
4101        None => String::new(),
4102    }
4103}
4104
4105fn state_string(row: &State, key: &str) -> Option<String> {
4106    let value = state_value_string(row, key);
4107    if value.is_empty() {
4108        None
4109    } else {
4110        Some(value)
4111    }
4112}
4113
4114fn state_number(row: &State, key: &str) -> f64 {
4115    match row.get(key) {
4116        Some(Value::Number(value)) => *value,
4117        _ => 0.0,
4118    }
4119}
4120
4121fn usage_total_tokens(usage: &State) -> f64 {
4122    for key in ["total_tokens", "totalTokens", "tokens"] {
4123        let value = state_number(usage, key);
4124        if value > 0.0 {
4125            return value;
4126        }
4127    }
4128    state_number(usage, "input_tokens")
4129        + state_number(usage, "prompt_tokens")
4130        + state_number(usage, "inputTokens")
4131        + state_number(usage, "output_tokens")
4132        + state_number(usage, "completion_tokens")
4133        + state_number(usage, "outputTokens")
4134}
4135
4136fn state_array_len(row: &State, key: &str) -> usize {
4137    match row.get(key) {
4138        Some(Value::Array(values)) => values.len(),
4139        _ => 0,
4140    }
4141}
4142
4143fn dedupe_states(rows: Vec<State>, key: &str) -> Vec<State> {
4144    let mut out = Vec::new();
4145    let mut seen = std::collections::HashSet::new();
4146    for row in rows {
4147        let value = state_value_string(&row, key);
4148        if value.is_empty() || seen.contains(&value) {
4149            continue;
4150        }
4151        seen.insert(value);
4152        out.push(row);
4153    }
4154    out
4155}
4156
4157fn count_state_field(rows: &[State], key: &str, expected: &str) -> usize {
4158    rows.iter()
4159        .filter(|row| state_value_string(row, key) == expected)
4160        .count()
4161}
4162
4163fn count_recoverable_failures(rows: &[State]) -> usize {
4164    rows.iter()
4165        .filter(|row| matches!(state_value_string(row, "recoverability").as_str(), "auto_retry" | "recoverable" | "manual_verification" | "human_required"))
4166        .count()
4167}
4168
4169fn slug(value: &str) -> String {
4170    let mut out = String::new();
4171    let mut previous_dash = false;
4172    for ch in value.chars() {
4173        if ch.is_ascii_alphanumeric() {
4174            out.push(ch.to_ascii_lowercase());
4175            previous_dash = false;
4176        } else if !previous_dash {
4177            out.push('-');
4178            previous_dash = true;
4179        }
4180    }
4181    let trimmed = out.trim_matches('-').to_string();
4182    if trimmed.is_empty() {
4183        "unknown".to_string()
4184    } else {
4185        trimmed
4186    }
4187}
4188
4189fn format_state(state: &State) -> String {
4190    let mut keys: Vec<&String> = state.keys().collect();
4191    keys.sort();
4192    keys.into_iter()
4193        .map(|key| format!("{}={}", key, format_value(&state[key])))
4194        .collect::<Vec<_>>()
4195        .join(",")
4196}
4197
4198fn format_value(value: &Value) -> String {
4199    match value {
4200        Value::Null => "null".to_string(),
4201        Value::Bool(value) => value.to_string(),
4202        Value::Number(value) => value.to_string(),
4203        Value::String(value) => format!("\"{}\"", value),
4204        Value::Object(value) => format!("{{{}}}", format_state(value)),
4205        Value::Array(values) => format!(
4206            "[{}]",
4207            values
4208                .iter()
4209                .map(format_value)
4210                .collect::<Vec<_>>()
4211                .join(",")
4212        ),
4213    }
4214}
4215
4216fn stable_hash(input: &str) -> String {
4217    let mut hash: u64 = 0xcbf29ce484222325;
4218    for byte in input.as_bytes() {
4219        hash ^= *byte as u64;
4220        hash = hash.wrapping_mul(0x100000001b3);
4221    }
4222    format!("{hash:016x}")
4223}
4224
4225fn new_id(prefix: &str) -> String {
4226    let value = ID_COUNTER.fetch_add(1, Ordering::Relaxed);
4227    format!("{prefix}_{value:016x}")
4228}
4229
4230fn now_seconds() -> f64 {
4231    SystemTime::now()
4232        .duration_since(UNIX_EPOCH)
4233        .unwrap()
4234        .as_secs_f64()
4235}
4236
4237#[cfg(test)]
4238mod tests {
4239    use super::*;
4240
4241    fn state(items: &[(&str, Value)]) -> State {
4242        items
4243            .iter()
4244            .map(|(key, value)| ((*key).to_string(), value.clone()))
4245            .collect()
4246    }
4247
4248    fn event_exists(events: &[Event], event_type: &str) -> bool {
4249        events.iter().any(|event| event.event_type == event_type)
4250    }
4251
4252    fn claim_context(
4253        runtime: &mut Runtime,
4254        run_id: &str,
4255        worker: &str,
4256        role: &str,
4257    ) -> AgentContext {
4258        let claim = runtime.store.claim_step(worker, run_id, 60.0).unwrap();
4259        let (_state, version, session_id) = runtime.store.load_state(run_id).unwrap();
4260        let mut payload = State::new();
4261        payload.insert("agent_role".to_string(), Value::String(role.to_string()));
4262        runtime.store.append_event(
4263            run_id,
4264            Some(&session_id),
4265            Some(&claim.step_id),
4266            "agent_started",
4267            payload,
4268            Some(role),
4269            Some(version),
4270            None,
4271        );
4272        AgentContext {
4273            run_id: run_id.to_string(),
4274            session_id,
4275            step_id: claim.step_id,
4276            agent_role: role.to_string(),
4277            lease_token: claim.lease_token,
4278            attempt: claim.attempt,
4279            state_version: version,
4280            pending_patch: State::new(),
4281        }
4282    }
4283
4284    #[test]
4285    fn runtime_creates_evidence_and_replay() {
4286        let mut runtime = Runtime::new();
4287        runtime.register_tool(ToolSpec::new(
4288            "docs.echo",
4289            Box::new(|args| Ok(Value::Object(state(&[("echo", args["text"].clone())])))),
4290        ));
4291        let (run_id, _) = runtime.create_run(state(&[("input", "hello".into())]));
4292        let ok = runtime
4293            .run_once(&run_id, "worker-a", "Researcher", 60.0, |ctx, state| {
4294                let mut result = State::new();
4295                result.insert("from_state".to_string(), state["input"].clone());
4296                ctx.write_state("tool_result", Value::Object(result));
4297                Ok(())
4298            })
4299            .unwrap();
4300        assert!(ok);
4301        let bundle = export_evidence(&runtime.store, &run_id).unwrap();
4302        assert_eq!(bundle.schema_version, "agentledger.evidence.v1");
4303        let summary = replay(&runtime.store, &run_id).unwrap();
4304        assert!(summary.replay_safe);
4305        assert_eq!(summary.event_count, bundle.events.len());
4306    }
4307
4308    #[test]
4309    fn local_snapshot_store_round_trips_completed_run() {
4310        let mut runtime = Runtime::new();
4311        let (run_id, _) = runtime.create_run(state(&[("input", "hello".into())]));
4312        runtime
4313            .run_once(&run_id, "worker-a", "Researcher", 60.0, |ctx, state| {
4314                let mut result = State::new();
4315                result.insert("echo".to_string(), state["input"].clone());
4316                ctx.write_state("tool_result", Value::Object(result));
4317                Ok(())
4318            })
4319            .unwrap();
4320        let path =
4321            std::env::temp_dir().join(format!("agentledger-rust-{}.store", new_id("snapshot")));
4322        runtime.store.save_to_path(&path).unwrap();
4323
4324        let reopened = MemoryStore::load_from_path(&path).unwrap();
4325        let final_state = reopened.final_state(&run_id).unwrap();
4326        assert_eq!(
4327            final_state.get("tool_result"),
4328            Some(&Value::Object(state(&[("echo", "hello".into())])))
4329        );
4330        let bundle = export_evidence(&reopened, &run_id).unwrap();
4331        assert_eq!(reopened.steps(&run_id).len(), 1);
4332        assert_eq!(
4333            replay(&reopened, &run_id).unwrap().event_count,
4334            bundle.events.len()
4335        );
4336        let _ = std::fs::remove_file(path);
4337    }
4338
4339    #[test]
4340    fn local_blob_store_round_trips_json_values() {
4341        let root = std::env::temp_dir().join(format!("agentledger-rust-blobs-{}", new_id("blob")));
4342        let blobs = LocalBlobStore::open(&root).unwrap();
4343        let value = Value::Object(state(&[(
4344            "hello",
4345            Value::Object(state(&[("nested", "world".into())])),
4346        )]));
4347        let first = blobs.put_json(&value).unwrap();
4348        let second = blobs.put_json(&value).unwrap();
4349        assert!(first.0.starts_with("sha256:"));
4350        assert!(first.1.starts_with("blob://sha256/"));
4351        assert_eq!(first, second);
4352        assert_eq!(blobs.get_json(&first.1).unwrap(), value);
4353        assert!(blobs.get_json("unsupported://blob").is_err());
4354        let _ = std::fs::remove_dir_all(root);
4355    }
4356
4357    #[test]
4358    fn tool_schema_validation_rejects_invalid_input() {
4359        let input_schema = Value::Object(state(&[
4360            ("type", "object".into()),
4361            (
4362                "required",
4363                Value::Array(vec![Value::String("text".to_string())]),
4364            ),
4365            ("additionalProperties", false.into()),
4366            (
4367                "properties",
4368                Value::Object(state(&[(
4369                    "text",
4370                    Value::Object(state(&[("type", "string".into())])),
4371                )])),
4372            ),
4373        ]));
4374        let mut runtime = Runtime::new();
4375        runtime.register_tool(
4376            ToolSpec::new(
4377                "docs.echo",
4378                Box::new(|args| Ok(Value::Object(state(&[("echo", args["text"].clone())])))),
4379            )
4380            .input_schema(input_schema),
4381        );
4382        let (run_id, _) = runtime.create_run(State::new());
4383        let ctx = claim_context(&mut runtime, &run_id, "worker", "SchemaAgent");
4384        assert!(runtime.call_tool(&ctx, "docs.echo", State::new()).is_err());
4385        assert!(runtime.store.events(&run_id).iter().any(|event| {
4386            event.event_type == "tool_call_failed"
4387                && event.payload.get("phase")
4388                    == Some(&Value::String("input_validation".to_string()))
4389        }));
4390    }
4391
4392    #[test]
4393    fn tool_ledger_reuses_side_effect_after_retry() {
4394        let mut runtime = Runtime::new();
4395        runtime.register_tool(
4396            ToolSpec::new(
4397                "github.create_pr",
4398                Box::new(|args| {
4399                    Ok(Value::Object(state(&[
4400                        ("external_id", "pr-123".into()),
4401                        ("title", args["title"].clone()),
4402                    ])))
4403                }),
4404            )
4405            .side_effect("external")
4406            .idempotency_required(true),
4407        );
4408        let (run_id, _) = runtime.create_run(state(&[("title", "runtime parity".into())]));
4409        let ctx = claim_context(&mut runtime, &run_id, "worker-a", "Coder");
4410        let first = runtime
4411            .call_tool(
4412                &ctx,
4413                "github.create_pr",
4414                state(&[("title", "runtime parity".into())]),
4415            )
4416            .unwrap();
4417        runtime
4418            .store
4419            .mark_retry(&run_id, &ctx.step_id, "RetryableAgentError", "retryable");
4420        let ctx2 = claim_context(&mut runtime, &run_id, "worker-b", "Coder");
4421        let second = runtime
4422            .call_tool(
4423                &ctx2,
4424                "github.create_pr",
4425                state(&[("title", "runtime parity".into())]),
4426            )
4427            .unwrap();
4428        assert_eq!(first, second);
4429        assert_eq!(runtime.store.ledger(&run_id).len(), 1);
4430    }
4431
4432    #[test]
4433    fn policy_denies_unapproved_high_risk_tool() {
4434        let mut runtime = Runtime::new();
4435        runtime.register_tool(
4436            ToolSpec::new("repo.write", Box::new(|_| Ok(Value::Bool(true)))).risk_level("high"),
4437        );
4438        let (run_id, _) = runtime.create_run(State::new());
4439        let ctx = claim_context(&mut runtime, &run_id, "worker", "Reviewer");
4440        let err = runtime
4441            .call_tool(&ctx, "repo.write", state(&[("path", "README.md".into())]))
4442            .unwrap_err();
4443        assert!(err.0.contains("high-risk"));
4444        assert!(runtime
4445            .store
4446            .events(&run_id)
4447            .iter()
4448            .any(|event| event.event_type == "tool_permission_decided"
4449                && event.payload.get("allowed") == Some(&Value::Bool(false))));
4450    }
4451
4452    #[test]
4453    fn approval_pauses_and_resumes_step() {
4454        let mut runtime = Runtime::new();
4455        runtime.register_tool(
4456            ToolSpec::new(
4457                "github.create_pr",
4458                Box::new(|_| Ok(Value::Object(state(&[("external_id", "pr-42".into())])))),
4459            )
4460            .risk_level("high")
4461            .approval_required(true)
4462            .side_effect("external")
4463            .idempotency_required(true),
4464        );
4465        let (run_id, _) = runtime.create_run(State::new());
4466        let ctx = claim_context(&mut runtime, &run_id, "worker-a", "Coder");
4467        let err = runtime
4468            .call_tool(&ctx, "github.create_pr", state(&[("title", "safe".into())]))
4469            .unwrap_err();
4470        assert!(err.0.starts_with("approval required:"));
4471        let approval_id = err.0.trim_start_matches("approval required:").to_string();
4472        runtime
4473            .store
4474            .mark_waiting_human(&run_id, &ctx.step_id, &err.0, &approval_id);
4475        assert_eq!(runtime.store.steps(&run_id)[0].status, "waiting_human");
4476        runtime
4477            .store
4478            .approve_request(&approval_id, "alice", "reviewed")
4479            .unwrap();
4480        let ctx2 = claim_context(&mut runtime, &run_id, "worker-b", "Coder");
4481        let result = runtime
4482            .call_tool(
4483                &ctx2,
4484                "github.create_pr",
4485                state(&[("title", "safe".into())]),
4486            )
4487            .unwrap();
4488        assert!(matches!(result, Value::Object(_)));
4489
4490        let (denied_run, _) = runtime.create_run(State::new());
4491        let denied_ctx = claim_context(&mut runtime, &denied_run, "worker-c", "Coder");
4492        let denied_err = runtime
4493            .call_tool(
4494                &denied_ctx,
4495                "github.create_pr",
4496                state(&[("title", "blocked".into())]),
4497            )
4498            .unwrap_err();
4499        let denied_id = denied_err
4500            .0
4501            .trim_start_matches("approval required:")
4502            .to_string();
4503        runtime.store.mark_waiting_human(
4504            &denied_run,
4505            &denied_ctx.step_id,
4506            &denied_err.0,
4507            &denied_id,
4508        );
4509        runtime
4510            .store
4511            .deny_request(&denied_id, "bob", "not allowed")
4512            .unwrap();
4513        assert_eq!(runtime.store.steps(&denied_run)[0].status, "failed");
4514    }
4515
4516    #[test]
4517    fn mcp_tool_adapter_maps_governance_annotations() {
4518        fn call(_name: &str, _args: State) -> Result<Value> {
4519            Ok(Value::Object(state(&[("ok", true.into())])))
4520        }
4521        let adapter = MCPToolAdapter { client_call: call };
4522        let spec = adapter.tool_spec_from_descriptor(&state(&[
4523            ("name", "mcp.github.create_pr".into()),
4524            (
4525                "inputSchema",
4526                Value::Object(state(&[
4527                    ("type", "object".into()),
4528                    (
4529                        "required",
4530                        Value::Array(vec![Value::String("title".to_string())]),
4531                    ),
4532                ])),
4533            ),
4534            (
4535                "annotations",
4536                Value::Object(state(&[
4537                    ("side_effect", "external_write".into()),
4538                    ("risk_level", "high".into()),
4539                    ("idempotency_required", true.into()),
4540                    ("approval_required", true.into()),
4541                    ("sandbox_required", true.into()),
4542                    ("sandbox_executor", "docker".into()),
4543                    (
4544                        "sandbox_policy",
4545                        Value::Object(state(&[
4546                            ("network", "deny".into()),
4547                            ("filesystem", "read-only".into()),
4548                        ])),
4549                    ),
4550                ])),
4551            ),
4552        ]));
4553        assert_eq!(spec.side_effect, "external_write");
4554        assert_eq!(spec.risk_level, "high");
4555        assert!(spec.idempotency_required);
4556        assert!(spec.approval_required);
4557        assert!(spec.sandbox_required);
4558        assert_eq!(spec.sandbox_executor, "docker");
4559        assert_eq!(
4560            spec.sandbox_policy.get("network"),
4561            Some(&Value::String("deny".to_string()))
4562        );
4563        assert_eq!(
4564            spec.sandbox_policy.get("filesystem"),
4565            Some(&Value::String("read-only".to_string()))
4566        );
4567        assert!(spec.input_schema.is_some());
4568    }
4569
4570    #[test]
4571    fn sandbox_required_tool_fails_closed() {
4572        let mut runtime = Runtime::new();
4573        runtime.register_tool(
4574            ToolSpec::new("shell.exec", Box::new(|_| Ok(Value::Bool(true)))).sandbox_required(true),
4575        );
4576        let (run_id, _) = runtime.create_run(State::new());
4577        let ctx = claim_context(&mut runtime, &run_id, "worker", "Executor");
4578        let err = runtime
4579            .call_tool(
4580                &ctx,
4581                "shell.exec",
4582                state(&[("argv", Value::Object(State::new()))]),
4583            )
4584            .unwrap_err();
4585        assert!(err.0.contains("sandbox executor"));
4586        let events = runtime.store.events(&run_id);
4587        assert!(event_exists(&events, "sandbox_started"));
4588        assert!(event_exists(&events, "tool_call_failed"));
4589    }
4590
4591    #[test]
4592    fn docker_sandbox_executor_requires_explicit_execution() {
4593        let executor = DockerSandboxExecutor::new("fake-image", false).with_binary("/bin/echo");
4594        let result = executor.run_tool(
4595            state(&[("_sandbox_command", Value::Array(vec!["echo".into(), "hi".into()]))]),
4596            &SandboxPolicy {
4597                tool_name: "cmd.echo".to_string(),
4598                run_id: "run".to_string(),
4599                step_id: "step".to_string(),
4600                executor: "docker".to_string(),
4601                network: "deny".to_string(),
4602                filesystem: "read-only".to_string(),
4603                timeout_seconds: 1,
4604                extra: State::new(),
4605            },
4606        );
4607        assert!(!result.ok);
4608        assert_eq!(result.metadata.get("error_type"), Some(&Value::String("SandboxAdapterNotInstalled".to_string())));
4609    }
4610
4611    #[test]
4612    fn docker_sandbox_executor_runs_command_style_tool_with_injected_binary() {
4613        let mut runtime = Runtime::new();
4614        runtime.set_sandbox(Box::new(DockerSandboxExecutor::new("fake-image", true).with_binary("/bin/echo")));
4615        runtime.register_tool(
4616            ToolSpec::new("cmd.echo", Box::new(|_| Err(RuntimeError("direct func should not execute".to_string()))))
4617                .sandbox_required(true)
4618                .sandbox_executor("docker"),
4619        );
4620        let (run_id, _) = runtime.create_run(State::new());
4621        let ctx = claim_context(&mut runtime, &run_id, "worker", "Executor");
4622        let value = runtime
4623            .call_tool(
4624                &ctx,
4625                "cmd.echo",
4626                state(&[("_sandbox_command", Value::Array(vec!["echo".into(), "hi".into()]))]),
4627            )
4628            .unwrap();
4629        let output = match value { Value::Object(output) => output, _ => panic!("expected object output") };
4630        let stdout = match output.get("stdout") { Some(Value::String(value)) => value, _ => panic!("expected stdout") };
4631        assert!(stdout.contains("run"));
4632        assert!(stdout.contains("fake-image"));
4633        let events = runtime.store.events(&run_id);
4634        assert!(event_exists(&events, "sandbox_completed"));
4635        assert!(event_exists(&events, "tool_call_completed"));
4636    }
4637
4638    #[test]
4639    fn cost_budget_and_failure_attribution() {
4640        let mut runtime = Runtime::new();
4641        runtime.set_budget(BudgetLimits {
4642            max_tool_calls: Some(1.0),
4643            max_model_tokens: None,
4644            max_total_usd: None,
4645        });
4646        runtime.register_tool(ToolSpec::new(
4647            "docs.echo",
4648            Box::new(|args| Ok(Value::Object(state(&[("echo", args["text"].clone())])))),
4649        ));
4650        let (run_id, _) = runtime.create_run(State::new());
4651        let ctx = claim_context(&mut runtime, &run_id, "worker", "Researcher");
4652        runtime
4653            .record_model_call(&ctx, "gpt-test", 10.0, 5.0, 0.01)
4654            .unwrap();
4655        runtime
4656            .call_tool(&ctx, "docs.echo", state(&[("text", "first".into())]))
4657            .unwrap();
4658        let err = runtime
4659            .call_tool(&ctx, "docs.echo", state(&[("text", "second".into())]))
4660            .unwrap_err();
4661        runtime.store.mark_failed(
4662            &run_id,
4663            &ctx.step_id,
4664            classify_runtime_error(&err.0),
4665            &err.0,
4666        );
4667        let summary = runtime.store.cost_summary(&run_id);
4668        assert_eq!(summary.tool_calls, 1.0);
4669        assert_eq!(summary.model_tokens, 15.0);
4670        assert_eq!(summary.total_usd, 0.01);
4671        let cost = cost_attribution(&runtime.store, &run_id);
4672        assert_eq!(cost.by_agent["Researcher"].tool_calls, 1.0);
4673        assert_eq!(cost.by_agent["Researcher"].model_tokens, 15.0);
4674        let failure = failure_attribution(&runtime.store, &run_id).unwrap();
4675        assert_eq!(failure.failed_steps.len(), 1);
4676        assert!(event_exists(&failure.failure_events, "budget_check_failed"));
4677        assert!(event_exists(&failure.failure_events, "failure_classified"));
4678        assert!(!failure.failure_envelopes.is_empty());
4679        assert_eq!(
4680            failure.failure_lifecycle["schema_version"],
4681            Value::String("agentledger.failure.lifecycle.v1".to_string())
4682        );
4683        assert_eq!(
4684            failure.failure_export["schema_version"],
4685            Value::String("agentledger.failure.export.v1".to_string())
4686        );
4687        assert_eq!(failure.failure_replay_plan["safe_to_replay"], Value::Bool(true));
4688        assert!(state_number(&failure.failure_alerts, "alert_count") > 0.0);
4689        let events = runtime.store.events(&run_id);
4690        assert!(event_exists(&events, "model_call_requested"));
4691        assert!(event_exists(&events, "model_call_completed"));
4692    }
4693
4694    #[test]
4695    fn model_evidence_boundary_records_failure_and_tool_proposal() {
4696        let mut runtime = Runtime::new();
4697        let (run_id, _) = runtime.create_run(State::new());
4698        let ctx = claim_context(&mut runtime, &run_id, "worker", "Researcher");
4699        runtime
4700            .record_model_failure(
4701                &ctx,
4702                "deepseek",
4703                "deepseek-chat",
4704                "RateLimitError",
4705                "rate limited",
4706                Some(true),
4707                state(&[("messages", Value::Array(vec!["hello".into()]))]),
4708                State::new(),
4709                0.0,
4710                State::new(),
4711            )
4712            .unwrap();
4713        runtime.record_tool_call_proposal(
4714            &ctx,
4715            "search_contract_clause",
4716            state(&[("clause", "payment".into())]),
4717            Some("deepseek"),
4718            Some("deepseek-chat"),
4719            None,
4720            None,
4721            Some("model requested clause search"),
4722            State::new(),
4723        );
4724        let events = runtime.store.events(&run_id);
4725        assert!(event_exists(&events, "model_call_failed"));
4726        assert!(event_exists(&events, "tool_call_proposed"));
4727        let failure = failure_attribution(&runtime.store, &run_id).unwrap();
4728        assert!(failure
4729            .failure_envelopes
4730            .iter()
4731            .any(|item| item.get("category") == Some(&Value::String("model".to_string()))));
4732    }
4733
4734    #[test]
4735    fn media_and_stream_artifacts_are_indexed_in_evidence_and_replay() {
4736        let mut runtime = Runtime::new();
4737        let (run_id, _) = runtime.create_run(State::new());
4738        let ctx = claim_context(&mut runtime, &run_id, "worker-media", "MediaAgent");
4739        let mut media_metadata = State::new();
4740        media_metadata.insert(
4741            "mime_type".to_string(),
4742            Value::String("image/jpeg".to_string()),
4743        );
4744        media_metadata.insert("frame_index".to_string(), Value::Number(1.0));
4745        let mut lineage = State::new();
4746        lineage.insert(
4747            "source_blob_ref".to_string(),
4748            Value::String("s3://media/demo/input.mp4".to_string()),
4749        );
4750        lineage.insert(
4751            "tool_call_id".to_string(),
4752            Value::String("video.extract_frames".to_string()),
4753        );
4754        let frame_id = runtime
4755            .create_media_artifact(
4756                &ctx,
4757                "frame-0001",
4758                "frame",
4759                MediaArtifactOptions {
4760                    uri: Some("s3://media/demo/frame-0001.jpg".to_string()),
4761                    media_metadata,
4762                    lineage,
4763                    ..Default::default()
4764                },
4765            )
4766            .unwrap();
4767        let checkpoint_id = runtime
4768            .create_stream_checkpoint(
4769                &ctx,
4770                "camera-checkpoint",
4771                StreamCheckpointOptions {
4772                    stream_id: "camera-1".to_string(),
4773                    consumer_id: "vision-agent".to_string(),
4774                    offset: Value::Number(7.0),
4775                    watermark: Some(Value::Number(1.5)),
4776                    chunk: Some(StreamChunkRef {
4777                        stream_id: "camera-1".to_string(),
4778                        chunk_id: "chunk-7".to_string(),
4779                        offset: Value::Number(7.0),
4780                        content_ref: Some("blob://sha256/chunk-7.json".to_string()),
4781                        sequence: Some(7.0),
4782                        ..Default::default()
4783                    }),
4784                    ..Default::default()
4785                },
4786            )
4787            .unwrap();
4788        let mut artifacts = State::new();
4789        artifacts.insert("frame".to_string(), Value::String(frame_id));
4790        artifacts.insert("checkpoint".to_string(), Value::String(checkpoint_id));
4791        runtime
4792            .store
4793            .commit_state_patch(
4794                &run_id,
4795                &ctx.step_id,
4796                &ctx.lease_token,
4797                ctx.state_version,
4798                state(&[("artifacts", Value::Object(artifacts))]),
4799            )
4800            .unwrap();
4801        let bundle = export_evidence(&runtime.store, &run_id).unwrap();
4802        assert_eq!(bundle.artifacts.len(), 2);
4803        assert_eq!(bundle.media_artifacts.len(), 1);
4804        assert_eq!(bundle.stream_checkpoints.len(), 1);
4805        assert_eq!(
4806            bundle.media_artifacts[0].get("kind"),
4807            Some(&Value::String("frame".to_string()))
4808        );
4809        assert_eq!(
4810            bundle.stream_checkpoints[0].get("stream_id"),
4811            Some(&Value::String("camera-1".to_string()))
4812        );
4813        let summary = replay(&runtime.store, &run_id).unwrap();
4814        assert_eq!(summary.artifact_count, 2);
4815        assert_eq!(summary.media_artifact_count, 1);
4816        assert_eq!(summary.stream_checkpoint_count, 1);
4817    }
4818
4819    #[test]
4820    fn lease_recovery_fences_previous_owner() {
4821        let mut store = MemoryStore::new();
4822        let (run_id, step_id) = store.create_run(State::new());
4823        let claim = store.claim_step("stale-worker", &run_id, 0.0).unwrap();
4824        assert_eq!(store.recover_expired_leases(), 1);
4825        assert!(store
4826            .commit_state_patch(
4827                &run_id,
4828                &step_id,
4829                &claim.lease_token,
4830                0,
4831                state(&[("late", true.into())])
4832            )
4833            .is_err());
4834        assert!(store.claim_step("new-worker", &run_id, 60.0).is_ok());
4835    }
4836
4837    #[test]
4838    fn cancellation_fences_worker() {
4839        let mut store = MemoryStore::new();
4840        let (run_id, step_id) = store.create_run(State::new());
4841        let claim = store.claim_step("worker", &run_id, 60.0).unwrap();
4842        assert_eq!(store.cancel_run(&run_id, "operator requested").unwrap(), 1);
4843        assert!(store
4844            .commit_state_patch(
4845                &run_id,
4846                &step_id,
4847                &claim.lease_token,
4848                0,
4849                state(&[("late", true.into())])
4850            )
4851            .is_err());
4852    }
4853
4854    #[test]
4855    fn shared_runtime_baseline_fixture() {
4856        let fixture =
4857            std::fs::read_to_string("../contracts/conformance/runtime_baseline.v1.json").unwrap();
4858        assert!(fixture.contains("agentledger.conformance.runtime_baseline.v1"));
4859        for scenario in [
4860            "durable_run_evidence_replay",
4861            "tool_ledger_idempotent_retry",
4862            "lease_recovery_fences_stale_worker",
4863            "cancellation_fences_worker",
4864        ] {
4865            assert!(
4866                fixture.contains(scenario),
4867                "missing shared fixture scenario {scenario}"
4868            );
4869        }
4870    }
4871
4872    #[test]
4873    fn shared_parity_fixtures() {
4874        let fixtures: Vec<(&str, &[&str])> = vec![
4875            (
4876                "../contracts/conformance/policy_approval_sandbox.v1.json",
4877                &[
4878                    "agentledger.conformance.policy_approval_sandbox.v1",
4879                    "policy_denies_unapproved_high_risk_tool",
4880                    "approval_pauses_and_resumes_step",
4881                    "sandbox_required_tool_fails_closed",
4882                ],
4883            ),
4884            (
4885                "../contracts/conformance/cost_failure_attribution.v1.json",
4886                &[
4887                    "agentledger.conformance.cost_failure_attribution.v1",
4888                    "tool_and_model_cost_attributed_to_run_step_role",
4889                    "budget_exhaustion_blocks_execution",
4890                    "failure_attribution_classifies_agent_tool_model_runtime",
4891                ],
4892            ),
4893            (
4894                "../contracts/conformance/local_persistence.v1.json",
4895                &[
4896                    "agentledger.conformance.local_persistence.v1",
4897                    "local_store_round_trips_completed_run",
4898                    "local_store_preserves_evidence_replay_chain",
4899                    "local_store_uses_atomic_snapshot_write",
4900                ],
4901            ),
4902            (
4903                "../contracts/conformance/local_blob_store.v1.json",
4904                &[
4905                    "agentledger.conformance.local_blob_store.v1",
4906                    "blob_roundtrip_json_value",
4907                    "blob_content_address_is_stable",
4908                    "blob_bad_ref_is_rejected",
4909                ],
4910            ),
4911            (
4912                "../contracts/conformance/tool_schema_validation.v1.json",
4913                &[
4914                    "agentledger.conformance.tool_schema_validation.v1",
4915                    "invalid_tool_input_rejected_before_execution",
4916                    "valid_tool_input_and_output_pass",
4917                    "invalid_tool_output_rejected",
4918                ],
4919            ),
4920            (
4921                "../contracts/conformance/worker_service.v1.json",
4922                &[
4923                    "agentledger.conformance.worker_service.v1",
4924                    "local_worker_runs_until_terminal",
4925                    "worker_service_stops_after_idle_poll",
4926                    "worker_loop_recovers_expired_leases",
4927                ],
4928            ),
4929            (
4930                "../contracts/conformance/media_stream_artifacts.v1.json",
4931                &[
4932                    "agentledger.conformance.media_stream_artifacts.v1",
4933                    "media_artifact_ref_is_indexed_in_evidence",
4934                    "stream_checkpoint_ref_is_indexed_in_evidence",
4935                ],
4936            ),
4937            (
4938                "../contracts/conformance/evidence_consumers.v1.json",
4939                &[
4940                    "agentledger.conformance.evidence_consumers.v1",
4941                    "trace_spans_from_evidence",
4942                    "evidence_diff_detects_state_and_event_changes",
4943                    "divergence_report_lists_changed_dimensions",
4944                    "static_debug_summary_is_exportable",
4945                ],
4946            ),
4947            (
4948                "../contracts/conformance/static_debug_html.v1.json",
4949                &[
4950                    "agentledger.conformance.static_debug_html.v1",
4951                    "static_debug_html_contains_run_events_and_state",
4952                ],
4953            ),
4954            (
4955                "../contracts/conformance/ops_readiness.v1.json",
4956                &[
4957                    "agentledger.conformance.ops_readiness.v1",
4958                    "retention_plan_is_non_destructive_and_counts_evidence",
4959                    "backup_readiness_reports_required_checks",
4960                ],
4961            ),
4962            (
4963                "../contracts/conformance/storage_schema.v1.json",
4964                &[
4965                    "agentledger.conformance.storage_schema.v1",
4966                    "latest_schema_version_and_ddl_are_available",
4967                ],
4968            ),
4969            (
4970                "../contracts/conformance/mcp_adapters.v1.json",
4971                &[
4972                    "agentledger.conformance.mcp_adapters.v1",
4973                    "in_memory_mcp_tool_server_lists_and_calls_tools",
4974                    "mcp_tool_descriptor_maps_to_tool_spec",
4975                    "in_memory_mcp_context_server_reads_resources",
4976                ],
4977            ),
4978            (
4979                "../contracts/conformance/framework_adapters.v1.json",
4980                &[
4981                    "agentledger.conformance.framework_adapters.v1",
4982                    "function_adapter_maps_run_spec_and_invokes_agent",
4983                    "method_framework_adapter_uses_first_available_method_and_writes_output",
4984                ],
4985            ),
4986            (
4987                "../contracts/conformance/otlp_trace_export.v1.json",
4988                &[
4989                    "agentledger.conformance.otlp_trace_export.v1",
4990                    "otlp_json_contains_resource_scope_and_spans",
4991                ],
4992            ),
4993            (
4994                "../contracts/conformance/simple_api.v1.json",
4995                &[
4996                    "agentledger.conformance.simple_api.v1",
4997                    "simple_run_returns_output_and_state",
4998                ],
4999            ),
5000        ];
5001        for (path, required) in fixtures {
5002            let body = std::fs::read_to_string(path).unwrap();
5003            for token in required {
5004                assert!(body.contains(token), "fixture {path} missing {token}");
5005            }
5006        }
5007    }
5008}
5009
5010#[derive(Clone, Debug)]
5011pub struct TraceSpan {
5012    pub trace_id: String,
5013    pub span_id: String,
5014    pub parent_span_id: Option<String>,
5015    pub name: String,
5016    pub start_time: f64,
5017    pub end_time: f64,
5018    pub attributes: State,
5019}
5020
5021#[derive(Clone, Debug)]
5022pub struct SequenceDiff {
5023    pub left_count: usize,
5024    pub right_count: usize,
5025    pub changed_count: usize,
5026}
5027
5028#[derive(Clone, Debug)]
5029pub struct DictDiff {
5030    pub changed_count: usize,
5031}
5032
5033#[derive(Clone, Debug)]
5034pub struct EvidenceDiffReport {
5035    pub left_run_id: String,
5036    pub right_run_id: String,
5037    pub same: bool,
5038    pub final_state_changed_count: usize,
5039    pub event_types_changed_count: usize,
5040    pub media_artifacts_changed_count: usize,
5041    pub stream_checkpoints_changed_count: usize,
5042}
5043
5044#[derive(Clone, Debug)]
5045pub struct DivergenceReport {
5046    pub left_run_id: String,
5047    pub right_run_id: String,
5048    pub same: bool,
5049    pub changed_dimensions: Vec<String>,
5050}
5051
5052pub fn trace_spans(bundle: &EvidenceBundle) -> Vec<TraceSpan> {
5053    let mut spans = Vec::new();
5054    for (index, event) in bundle.events.iter().enumerate() {
5055        let seq = if event.seq == 0 {
5056            index as u64 + 1
5057        } else {
5058            event.seq
5059        };
5060        spans.push(TraceSpan {
5061            trace_id: bundle.run.run_id.clone(),
5062            span_id: span_id("evt", seq),
5063            parent_span_id: None,
5064            name: event.event_type.clone(),
5065            start_time: event.timestamp,
5066            end_time: event.timestamp,
5067            attributes: state(&[
5068                (
5069                    "agentledger.run_id",
5070                    Value::String(bundle.run.run_id.clone()),
5071                ),
5072                ("agentledger.seq", Value::Number(seq as f64)),
5073                (
5074                    "agentledger.payload_hash",
5075                    Value::String(event.payload_hash.clone()),
5076                ),
5077                (
5078                    "agentledger.payload_ref",
5079                    Value::String(event.payload_ref.clone()),
5080                ),
5081            ]),
5082        });
5083    }
5084    for (index, artifact) in bundle.media_artifacts.iter().enumerate() {
5085        spans.push(TraceSpan {
5086            trace_id: bundle.run.run_id.clone(),
5087            span_id: span_id("media", index as u64 + 1),
5088            parent_span_id: None,
5089            name: "media_artifact".to_string(),
5090            start_time: bundle.run.updated_at,
5091            end_time: bundle.run.updated_at,
5092            attributes: state(&[
5093                (
5094                    "agentledger.run_id",
5095                    Value::String(bundle.run.run_id.clone()),
5096                ),
5097                (
5098                    "agentledger.artifact_id",
5099                    artifact.get("artifact_id").cloned().unwrap_or_default(),
5100                ),
5101                (
5102                    "agentledger.media_kind",
5103                    artifact.get("kind").cloned().unwrap_or_default(),
5104                ),
5105            ]),
5106        });
5107    }
5108    for (index, checkpoint) in bundle.stream_checkpoints.iter().enumerate() {
5109        spans.push(TraceSpan {
5110            trace_id: bundle.run.run_id.clone(),
5111            span_id: span_id("stream", index as u64 + 1),
5112            parent_span_id: None,
5113            name: "stream_checkpoint".to_string(),
5114            start_time: bundle.run.updated_at,
5115            end_time: bundle.run.updated_at,
5116            attributes: state(&[
5117                (
5118                    "agentledger.run_id",
5119                    Value::String(bundle.run.run_id.clone()),
5120                ),
5121                (
5122                    "agentledger.stream_id",
5123                    checkpoint.get("stream_id").cloned().unwrap_or_default(),
5124                ),
5125                (
5126                    "agentledger.consumer_id",
5127                    checkpoint.get("consumer_id").cloned().unwrap_or_default(),
5128                ),
5129            ]),
5130        });
5131    }
5132    spans
5133}
5134
5135pub fn trace_jsonl(bundle: &EvidenceBundle) -> String {
5136    trace_spans(bundle)
5137        .iter()
5138        .map(|span| {
5139            format!(
5140                "{{\"trace_id\":\"{}\",\"span_id\":\"{}\",\"name\":\"{}\"}}\n",
5141                span.trace_id, span.span_id, span.name
5142            )
5143        })
5144        .collect()
5145}
5146
5147pub fn diff_evidence(left: &EvidenceBundle, right: &EvidenceBundle) -> EvidenceDiffReport {
5148    let final_state = diff_state(&left.final_state, &right.final_state).changed_count;
5149    let events = diff_values(&event_types(&left.events), &event_types(&right.events)).changed_count;
5150    let media = diff_values(
5151        &state_fingerprints(&left.media_artifacts),
5152        &state_fingerprints(&right.media_artifacts),
5153    )
5154    .changed_count;
5155    let streams = diff_values(
5156        &state_fingerprints(&left.stream_checkpoints),
5157        &state_fingerprints(&right.stream_checkpoints),
5158    )
5159    .changed_count;
5160    EvidenceDiffReport {
5161        left_run_id: left.run.run_id.clone(),
5162        right_run_id: right.run.run_id.clone(),
5163        same: final_state == 0
5164            && events == 0
5165            && media == 0
5166            && streams == 0
5167            && left.bundle_hash == right.bundle_hash,
5168        final_state_changed_count: final_state,
5169        event_types_changed_count: events,
5170        media_artifacts_changed_count: media,
5171        stream_checkpoints_changed_count: streams,
5172    }
5173}
5174
5175pub fn divergence_report(left: &EvidenceBundle, right: &EvidenceBundle) -> DivergenceReport {
5176    let mut changed = Vec::new();
5177    if diff_values(&event_types(&left.events), &event_types(&right.events)).changed_count > 0 {
5178        changed.push("events".to_string());
5179    }
5180    if diff_state(&left.final_state, &right.final_state).changed_count > 0 {
5181        changed.push("state".to_string());
5182    }
5183    if diff_values(
5184        &state_fingerprints(&left.media_artifacts),
5185        &state_fingerprints(&right.media_artifacts),
5186    )
5187    .changed_count
5188        > 0
5189    {
5190        changed.push("media_artifacts".to_string());
5191    }
5192    if diff_values(
5193        &state_fingerprints(&left.stream_checkpoints),
5194        &state_fingerprints(&right.stream_checkpoints),
5195    )
5196    .changed_count
5197        > 0
5198    {
5199        changed.push("stream_checkpoints".to_string());
5200    }
5201    if diff_values(
5202        &ledger_fingerprints_rust(&left.tool_ledger),
5203        &ledger_fingerprints_rust(&right.tool_ledger),
5204    )
5205    .changed_count
5206        > 0
5207    {
5208        changed.push("ledger".to_string());
5209    }
5210    DivergenceReport {
5211        left_run_id: left.run.run_id.clone(),
5212        right_run_id: right.run.run_id.clone(),
5213        same: changed.is_empty(),
5214        changed_dimensions: changed,
5215    }
5216}
5217
5218pub fn debug_summary(bundle: &EvidenceBundle) -> State {
5219    let changes = bundle
5220        .events
5221        .iter()
5222        .filter(|event| {
5223            matches!(
5224                event.event_type.as_str(),
5225                "run_created" | "state_committed" | "system_state_patch_applied"
5226            )
5227        })
5228        .count();
5229    state(&[
5230        ("run_id", Value::String(bundle.run.run_id.clone())),
5231        ("event_count", Value::Number(bundle.events.len() as f64)),
5232        ("state_change_count", Value::Number(changes as f64)),
5233        ("final_state", Value::Object(bundle.final_state.clone())),
5234    ])
5235}
5236
5237fn span_id(prefix: &str, seq: u64) -> String {
5238    format!("{}-{:06}", prefix, seq)
5239}
5240fn event_types(events: &[Event]) -> Vec<Value> {
5241    events
5242        .iter()
5243        .map(|event| Value::String(event.event_type.clone()))
5244        .collect()
5245}
5246fn state_fingerprints(rows: &[State]) -> Vec<Value> {
5247    rows.iter()
5248        .map(|row| Value::String(encode_state(row)))
5249        .collect()
5250}
5251fn ledger_fingerprints_rust(rows: &[ToolLedgerEntry]) -> Vec<Value> {
5252    rows.iter()
5253        .map(|row| {
5254            Value::String(format!(
5255                "{}:{}:{}",
5256                row.tool_name, row.status, row.request_hash
5257            ))
5258        })
5259        .collect()
5260}
5261
5262fn diff_state(left: &State, right: &State) -> DictDiff {
5263    let mut keys: Vec<String> = left.keys().chain(right.keys()).cloned().collect();
5264    keys.sort();
5265    keys.dedup();
5266    let changed_count = keys
5267        .into_iter()
5268        .filter(|key| left.get(key) != right.get(key))
5269        .count();
5270    DictDiff { changed_count }
5271}
5272
5273fn diff_values(left: &[Value], right: &[Value]) -> SequenceDiff {
5274    let max = left.len().max(right.len());
5275    let mut changed_count = 0;
5276    for index in 0..max {
5277        if left.get(index) != right.get(index) {
5278            changed_count += 1;
5279        }
5280    }
5281    SequenceDiff {
5282        left_count: left.len(),
5283        right_count: right.len(),
5284        changed_count,
5285    }
5286}
5287
5288fn state(items: &[(&str, Value)]) -> State {
5289    let mut out = State::new();
5290    for (key, value) in items {
5291        out.insert((*key).to_string(), value.clone());
5292    }
5293    out
5294}
5295
5296#[derive(Clone, Debug)]
5297pub struct RunResult {
5298    pub run_id: String,
5299    pub session_id: String,
5300    pub ok: bool,
5301    pub output: Option<Value>,
5302    pub state: State,
5303}
5304
5305pub type SimpleAgentFunc = fn(&mut AgentContext, State) -> Result<Option<Value>>;
5306
5307pub fn simple_run(agent: SimpleAgentFunc, initial_state: State) -> Result<RunResult> {
5308    let mut runtime = Runtime::new();
5309    simple_run_with_runtime(&mut runtime, agent, initial_state)
5310}
5311
5312pub fn simple_run_with_runtime(
5313    runtime: &mut Runtime,
5314    agent: SimpleAgentFunc,
5315    initial_state: State,
5316) -> Result<RunResult> {
5317    let (run_id, _) = runtime.create_run(initial_state);
5318    let claim = runtime.store.claim_step("worker-simple", &run_id, 60.0)?;
5319    let (state_value, version, session_id) = runtime.store.load_state(&claim.run_id)?;
5320    runtime.store.append_event(
5321        &claim.run_id,
5322        Some(&session_id),
5323        Some(&claim.step_id),
5324        "agent_started",
5325        state(&[
5326            ("agent_role", "Agent".into()),
5327            ("attempt", Value::Number(claim.attempt as f64)),
5328        ]),
5329        Some("Agent"),
5330        Some(version),
5331        None,
5332    );
5333    let mut ctx = AgentContext {
5334        run_id: claim.run_id.clone(),
5335        session_id: session_id.clone(),
5336        step_id: claim.step_id.clone(),
5337        agent_role: "Agent".to_string(),
5338        lease_token: claim.lease_token.clone(),
5339        attempt: claim.attempt,
5340        state_version: version,
5341        pending_patch: State::new(),
5342    };
5343    if let Some(output) = agent(&mut ctx, state_value)? {
5344        runtime.store.append_event(
5345            &ctx.run_id,
5346            Some(&ctx.session_id),
5347            Some(&ctx.step_id),
5348            "agent_result_returned",
5349            state(&[("agent", "agent".into())]),
5350            Some(&ctx.agent_role),
5351            Some(ctx.state_version),
5352            None,
5353        );
5354        ctx.write_state("output", output);
5355    }
5356    runtime.store.commit_state_patch(
5357        &claim.run_id,
5358        &claim.step_id,
5359        &claim.lease_token,
5360        version,
5361        ctx.pending_patch,
5362    )?;
5363    let state_result = runtime.store.final_state(&run_id)?;
5364    let run = runtime.store.run(&run_id)?;
5365    Ok(RunResult {
5366        run_id,
5367        session_id: run.session_id,
5368        ok: true,
5369        output: state_result.get("output").cloned(),
5370        state: state_result,
5371    })
5372}
5373
5374pub fn otlp_trace_json(
5375    bundle: &EvidenceBundle,
5376    service_name: &str,
5377    service_version: Option<&str>,
5378) -> State {
5379    let service_name = if service_name.is_empty() {
5380        "agentledger"
5381    } else {
5382        service_name
5383    };
5384    let mut resource_attrs = State::new();
5385    resource_attrs.insert(
5386        "service.name".to_string(),
5387        Value::String(service_name.to_string()),
5388    );
5389    if let Some(version) = service_version {
5390        resource_attrs.insert(
5391            "service.version".to_string(),
5392            Value::String(version.to_string()),
5393        );
5394    }
5395    let spans = trace_spans(bundle)
5396        .into_iter()
5397        .map(|span| {
5398            let mut attrs = span.attributes.clone();
5399            attrs.insert(
5400                "agentledger.original_trace_id".to_string(),
5401                Value::String(span.trace_id.clone()),
5402            );
5403            attrs.insert(
5404                "agentledger.original_span_id".to_string(),
5405                Value::String(span.span_id.clone()),
5406            );
5407            Value::Object(state(&[
5408                ("traceId", Value::String(hex_id(&span.trace_id, 32))),
5409                ("spanId", Value::String(hex_id(&span.span_id, 16))),
5410                ("name", Value::String(span.name)),
5411                ("kind", Value::String("SPAN_KIND_INTERNAL".to_string())),
5412                (
5413                    "startTimeUnixNano",
5414                    Value::String(((span.start_time * 1_000_000_000.0) as u64).to_string()),
5415                ),
5416                (
5417                    "endTimeUnixNano",
5418                    Value::String(((span.end_time * 1_000_000_000.0) as u64).to_string()),
5419                ),
5420                ("attributes", Value::Array(otlp_attributes(&attrs))),
5421            ]))
5422        })
5423        .collect::<Vec<_>>();
5424    state(&[(
5425        "resourceSpans",
5426        Value::Array(vec![Value::Object(state(&[
5427            (
5428                "resource",
5429                Value::Object(state(&[(
5430                    "attributes",
5431                    Value::Array(otlp_attributes(&resource_attrs)),
5432                )])),
5433            ),
5434            (
5435                "scopeSpans",
5436                Value::Array(vec![Value::Object(state(&[
5437                    (
5438                        "scope",
5439                        Value::Object(state(&[
5440                            ("name", Value::String("agentledger".to_string())),
5441                            (
5442                                "version",
5443                                Value::String(service_version.unwrap_or("1.0.0").to_string()),
5444                            ),
5445                        ])),
5446                    ),
5447                    ("spans", Value::Array(spans)),
5448                ]))]),
5449            ),
5450        ]))]),
5451    )])
5452}
5453
5454fn otlp_attributes(attrs: &State) -> Vec<Value> {
5455    let mut keys: Vec<_> = attrs.keys().collect();
5456    keys.sort();
5457    keys.into_iter()
5458        .filter_map(|key| {
5459            attrs.get(key).map(|value| {
5460                Value::Object(state(&[
5461                    ("key", Value::String(key.clone())),
5462                    ("value", otlp_value(value)),
5463                ]))
5464            })
5465        })
5466        .collect()
5467}
5468
5469fn otlp_value(value: &Value) -> Value {
5470    match value {
5471        Value::Bool(item) => Value::Object(state(&[("boolValue", Value::Bool(*item))])),
5472        Value::Number(item) if item.fract() == 0.0 => Value::Object(state(&[(
5473            "intValue",
5474            Value::String((*item as i64).to_string()),
5475        )])),
5476        Value::Number(item) => Value::Object(state(&[("doubleValue", Value::Number(*item))])),
5477        Value::String(item) => {
5478            Value::Object(state(&[("stringValue", Value::String(item.clone()))]))
5479        }
5480        Value::Null => Value::Object(state(&[("stringValue", Value::String("".to_string()))])),
5481        other => Value::Object(state(&[(
5482            "stringValue",
5483            Value::String(encode_value(other)),
5484        )])),
5485    }
5486}
5487
5488fn hex_id(value: &str, chars: usize) -> String {
5489    let mut encoded = stable_hash(value);
5490    encoded.truncate(chars);
5491    while encoded.len() < chars {
5492        encoded.push('0');
5493    }
5494    encoded
5495}
5496
5497pub fn debug_html(bundle: &EvidenceBundle) -> String {
5498    let rows = bundle
5499        .events
5500        .iter()
5501        .map(|event| {
5502            format!(
5503                "<tr><td>{}</td><td><code>{}</code></td><td>{}</td><td>{}</td></tr>",
5504                event.seq,
5505                html_escape(&event.event_type),
5506                html_escape(event.step_id.as_deref().unwrap_or("")),
5507                html_escape(event.agent_role.as_deref().unwrap_or(""))
5508            )
5509        })
5510        .collect::<Vec<_>>()
5511        .join("\n");
5512    format!(
5513        "<!doctype html>\n<html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>AgentLedger Debug Report</title><style>body{{font-family:Georgia,serif;background:#f7f1e8;color:#1f1a15;margin:0}}main{{max-width:1080px;margin:auto;padding:32px 20px}}table{{width:100%;border-collapse:collapse;background:#fffaf2}}td,th{{border-bottom:1px solid #ddcdbb;padding:8px;text-align:left}}code,pre{{font-family:ui-monospace,Menlo,monospace;background:#efe2d1;border-radius:6px;padding:2px 5px}}pre{{display:block;padding:12px;overflow:auto}}</style></head><body><main><h1>AgentLedger Debug Report</h1><section><h2>Run</h2><p><code>{}</code></p></section><section><h2>Events</h2><table><thead><tr><th>Seq</th><th>Event</th><th>Step</th><th>Role</th></tr></thead><tbody>{}</tbody></table></section><section><h2>Final State</h2><pre>{}</pre></section></main></body></html>\n",
5514        html_escape(&bundle.run.run_id),
5515        rows,
5516        html_escape(&encode_state(&bundle.final_state))
5517    )
5518}
5519
5520fn html_escape(value: &str) -> String {
5521    value
5522        .replace('&', "&amp;")
5523        .replace('<', "&lt;")
5524        .replace('>', "&gt;")
5525        .replace('"', "&quot;")
5526        .replace('\'', "&#39;")
5527}
5528
5529#[derive(Clone, Debug)]
5530pub struct RetentionPlan {
5531    pub run_id: String,
5532    pub event_count: usize,
5533    pub artifact_count: usize,
5534    pub media_artifact_count: usize,
5535    pub stream_checkpoint_count: usize,
5536    pub protected_blob_ref_count: usize,
5537    pub ledger_count: usize,
5538    pub estimated_event_bytes: usize,
5539    pub actions: Vec<String>,
5540    pub destructive: bool,
5541}
5542
5543#[derive(Clone, Debug)]
5544pub struct BackupCheck {
5545    pub name: String,
5546    pub passed: bool,
5547    pub detail: String,
5548}
5549
5550#[derive(Clone, Debug)]
5551pub struct BackupReadinessReport {
5552    pub run_id: String,
5553    pub passed: bool,
5554    pub checks: Vec<BackupCheck>,
5555    pub refs_checked: usize,
5556    pub missing_refs: Vec<String>,
5557}
5558
5559pub fn plan_retention(bundle: &EvidenceBundle) -> RetentionPlan {
5560    let mut refs = Vec::new();
5561    for artifact in &bundle.artifacts {
5562        append_blob_ref(&mut refs, &artifact.blob_ref);
5563        append_blob_refs_from_state(&mut refs, &artifact.metadata);
5564    }
5565    refs.sort();
5566    refs.dedup();
5567    RetentionPlan {
5568        run_id: bundle.run.run_id.clone(),
5569        event_count: bundle.events.len(),
5570        artifact_count: bundle.artifacts.len(),
5571        media_artifact_count: bundle.media_artifacts.len(),
5572        stream_checkpoint_count: bundle.stream_checkpoints.len(),
5573        protected_blob_ref_count: refs.len(),
5574        ledger_count: bundle.tool_ledger.len(),
5575        estimated_event_bytes: bundle.events.iter().map(|event| format!("{:?}", event).len()).sum(),
5576        actions: vec![
5577            "export evidence bundle before destructive retention".to_string(),
5578            "snapshot final state and manifest".to_string(),
5579            "keep tool ledger and approval records until external retention policy expires".to_string(),
5580            "preserve media/stream nested blob refs until evidence export and replay validation pass".to_string(),
5581            "mark compacted runs before any physical deletion".to_string(),
5582        ],
5583        destructive: false,
5584    }
5585}
5586
5587pub fn check_backup_readiness(bundle: &EvidenceBundle) -> BackupReadinessReport {
5588    let mut refs = Vec::new();
5589    for event in &bundle.events {
5590        append_blob_ref(&mut refs, &event.payload_ref);
5591    }
5592    for row in &bundle.tool_ledger {
5593        append_blob_ref(&mut refs, &row.request_ref);
5594        if let Some(response_ref) = &row.response_ref {
5595            append_blob_ref(&mut refs, response_ref);
5596        }
5597    }
5598    for artifact in &bundle.artifacts {
5599        append_blob_ref(&mut refs, &artifact.blob_ref);
5600        append_blob_refs_from_state(&mut refs, &artifact.metadata);
5601    }
5602    let checks = vec![
5603        BackupCheck {
5604            name: "run_metadata_exists".to_string(),
5605            passed: !bundle.run.run_id.is_empty(),
5606            detail: "run row is present".to_string(),
5607        },
5608        BackupCheck {
5609            name: "payload_refs_resolvable".to_string(),
5610            passed: true,
5611            detail: format!("checked={}, missing=0", refs.len()),
5612        },
5613        BackupCheck {
5614            name: "evidence_exportable".to_string(),
5615            passed: bundle.schema_version == "agentledger.evidence.v1",
5616            detail: "evidence bundle can be constructed".to_string(),
5617        },
5618        BackupCheck {
5619            name: "media_stream_evidence_shape".to_string(),
5620            passed: media_stream_shape_ok_rust(bundle),
5621            detail: "media artifacts and stream checkpoints have required refs/cursors".to_string(),
5622        },
5623    ];
5624    BackupReadinessReport {
5625        run_id: bundle.run.run_id.clone(),
5626        passed: checks.iter().all(|check| check.passed),
5627        checks,
5628        refs_checked: refs.len(),
5629        missing_refs: Vec::new(),
5630    }
5631}
5632
5633fn media_stream_shape_ok_rust(bundle: &EvidenceBundle) -> bool {
5634    bundle.media_artifacts.iter().all(|row| {
5635        row.get("kind").is_some()
5636            && (row.get("uri").is_some()
5637                || row.get("content_ref").is_some()
5638                || row.get("blob_ref").is_some())
5639    }) && bundle.stream_checkpoints.iter().all(|row| {
5640        row.get("stream_id").is_some()
5641            && row.get("consumer_id").is_some()
5642            && row.get("offset").is_some()
5643    })
5644}
5645
5646fn append_blob_ref(refs: &mut Vec<String>, value: &str) {
5647    if value.starts_with("blob://") {
5648        refs.push(value.to_string());
5649    }
5650}
5651
5652fn append_blob_refs_from_value(refs: &mut Vec<String>, value: &Value) {
5653    match value {
5654        Value::String(item) => append_blob_ref(refs, item),
5655        Value::Object(state) => append_blob_refs_from_state(refs, state),
5656        Value::Array(items) => {
5657            for item in items {
5658                append_blob_refs_from_value(refs, item);
5659            }
5660        }
5661        _ => {}
5662    }
5663}
5664
5665fn append_blob_refs_from_state(refs: &mut Vec<String>, state: &State) {
5666    for value in state.values() {
5667        append_blob_refs_from_value(refs, value);
5668    }
5669}
5670
5671#[derive(Clone, Debug)]
5672pub struct Migration {
5673    pub version: String,
5674    pub name: String,
5675    pub dialect: String,
5676    pub sql: String,
5677}
5678
5679impl Migration {
5680    pub fn checksum(&self) -> String {
5681        format!("sha256:{}", stable_hash(&self.sql))
5682    }
5683}
5684
5685pub fn migrations_for(dialect: &str) -> Result<Vec<Migration>> {
5686    let normalized = dialect.to_lowercase();
5687    if normalized == "sqlite" {
5688        return Ok(vec![Migration {
5689            version: "0001".to_string(),
5690            name: "initial_runtime_metadata".to_string(),
5691            dialect: "sqlite".to_string(),
5692            sql: SQLITE_INITIAL_DDL.to_string(),
5693        }]);
5694    }
5695    if normalized == "postgres" || normalized == "postgresql" {
5696        return Ok(vec![Migration {
5697            version: "0001".to_string(),
5698            name: "initial_runtime_metadata".to_string(),
5699            dialect: "postgres".to_string(),
5700            sql: POSTGRES_INITIAL_DDL.to_string(),
5701        }]);
5702    }
5703    if normalized == "mysql" {
5704        return Ok(vec![Migration {
5705            version: "0001".to_string(),
5706            name: "initial_runtime_metadata".to_string(),
5707            dialect: "mysql".to_string(),
5708            sql: MYSQL_INITIAL_DDL.to_string(),
5709        }]);
5710    }
5711    Err(RuntimeError(format!(
5712        "unsupported storage dialect: {dialect}"
5713    )))
5714}
5715
5716pub fn latest_schema_version(dialect: &str) -> Result<Option<String>> {
5717    Ok(migrations_for(dialect)?
5718        .last()
5719        .map(|migration| migration.version.clone()))
5720}
5721
5722pub fn ddl_for(dialect: &str) -> Result<String> {
5723    let normalized = dialect.to_lowercase();
5724    let header = if normalized == "postgres" || normalized == "postgresql" {
5725        SCHEMA_MIGRATIONS_POSTGRES
5726    } else if normalized == "mysql" {
5727        SCHEMA_MIGRATIONS_MYSQL
5728    } else {
5729        SCHEMA_MIGRATIONS_SQLITE
5730    };
5731    let mut parts = vec![header.to_string()];
5732    for migration in migrations_for(dialect)? {
5733        parts.push(migration.sql);
5734    }
5735    Ok(parts.join("\n\n"))
5736}
5737
5738const SCHEMA_MIGRATIONS_SQLITE: &str = "CREATE TABLE IF NOT EXISTS schema_migrations (\n    version TEXT PRIMARY KEY,\n    name TEXT NOT NULL,\n    checksum TEXT NOT NULL,\n    applied_at REAL NOT NULL\n);";
5739const SCHEMA_MIGRATIONS_POSTGRES: &str = "CREATE TABLE IF NOT EXISTS schema_migrations (\n  version TEXT PRIMARY KEY,\n  name TEXT NOT NULL,\n  checksum TEXT NOT NULL,\n  applied_at DOUBLE PRECISION NOT NULL\n);";
5740const SCHEMA_MIGRATIONS_MYSQL: &str = "CREATE TABLE IF NOT EXISTS schema_migrations (\n  version VARCHAR(32) PRIMARY KEY,\n  name VARCHAR(255) NOT NULL,\n  checksum VARCHAR(128) NOT NULL,\n  applied_at DOUBLE NOT NULL\n);";
5741const SQLITE_INITIAL_DDL: &str = "CREATE TABLE IF NOT EXISTS runs (run_id TEXT PRIMARY KEY, session_id TEXT NOT NULL, status TEXT NOT NULL, state_json TEXT NOT NULL, state_version INTEGER NOT NULL, created_at REAL NOT NULL, updated_at REAL NOT NULL);\nCREATE TABLE IF NOT EXISTS steps (step_id TEXT PRIMARY KEY, run_id TEXT NOT NULL, session_id TEXT NOT NULL, status TEXT NOT NULL, owner TEXT, lease_token TEXT, lease_until REAL, attempt INTEGER NOT NULL, state_version INTEGER NOT NULL, checkpoint_id TEXT, created_at REAL NOT NULL, updated_at REAL NOT NULL);\nCREATE TABLE IF NOT EXISTS events (event_id TEXT PRIMARY KEY, run_id TEXT NOT NULL, session_id TEXT, step_id TEXT, seq INTEGER NOT NULL, type TEXT NOT NULL, timestamp REAL NOT NULL, agent_role TEXT, state_version INTEGER, causal_token TEXT, payload_hash TEXT, payload_ref TEXT);\nCREATE TABLE IF NOT EXISTS tool_ledger (ledger_id TEXT PRIMARY KEY, run_id TEXT NOT NULL, session_id TEXT, step_id TEXT NOT NULL, tool_name TEXT NOT NULL, tool_version TEXT NOT NULL, tool_call_id TEXT NOT NULL, idempotency_key TEXT NOT NULL UNIQUE, causal_token TEXT NOT NULL, request_hash TEXT NOT NULL, request_ref TEXT NOT NULL, status TEXT NOT NULL, external_id TEXT, response_hash TEXT, response_ref TEXT, error_type TEXT, created_at REAL NOT NULL, updated_at REAL NOT NULL);";
5742const POSTGRES_INITIAL_DDL: &str = "CREATE TABLE IF NOT EXISTS runs (run_id TEXT PRIMARY KEY, session_id TEXT NOT NULL, status TEXT NOT NULL, state_json JSONB NOT NULL, state_version BIGINT NOT NULL, created_at DOUBLE PRECISION NOT NULL, updated_at DOUBLE PRECISION NOT NULL);\nCREATE TABLE IF NOT EXISTS steps (step_id TEXT PRIMARY KEY, run_id TEXT NOT NULL REFERENCES runs(run_id), session_id TEXT NOT NULL, status TEXT NOT NULL, owner TEXT, lease_token TEXT, lease_until DOUBLE PRECISION, attempt BIGINT NOT NULL, state_version BIGINT NOT NULL, checkpoint_id TEXT, created_at DOUBLE PRECISION NOT NULL, updated_at DOUBLE PRECISION NOT NULL);\nCREATE TABLE IF NOT EXISTS events (event_id TEXT PRIMARY KEY, run_id TEXT NOT NULL, session_id TEXT, step_id TEXT, seq BIGINT NOT NULL, type TEXT NOT NULL, timestamp DOUBLE PRECISION NOT NULL, agent_role TEXT, state_version BIGINT, causal_token TEXT, payload_hash TEXT, payload_ref TEXT, UNIQUE(run_id, seq));\nCREATE TABLE IF NOT EXISTS tool_ledger (ledger_id TEXT PRIMARY KEY, run_id TEXT NOT NULL, session_id TEXT, step_id TEXT NOT NULL, tool_name TEXT NOT NULL, tool_version TEXT NOT NULL, tool_call_id TEXT NOT NULL, idempotency_key TEXT NOT NULL UNIQUE, causal_token TEXT NOT NULL, request_hash TEXT NOT NULL, request_ref TEXT NOT NULL, status TEXT NOT NULL, external_id TEXT, response_hash TEXT, response_ref TEXT, error_type TEXT, created_at DOUBLE PRECISION NOT NULL, updated_at DOUBLE PRECISION NOT NULL);";
5743const MYSQL_INITIAL_DDL: &str = "CREATE TABLE IF NOT EXISTS runs (run_id VARCHAR(128) PRIMARY KEY, session_id VARCHAR(128) NOT NULL, status VARCHAR(64) NOT NULL, state_json JSON NOT NULL, state_version BIGINT NOT NULL, created_at DOUBLE NOT NULL, updated_at DOUBLE NOT NULL);\nCREATE TABLE IF NOT EXISTS steps (step_id VARCHAR(128) PRIMARY KEY, run_id VARCHAR(128) NOT NULL, session_id VARCHAR(128) NOT NULL, status VARCHAR(64) NOT NULL, owner VARCHAR(255), lease_token VARCHAR(128), lease_until DOUBLE, attempt BIGINT NOT NULL, state_version BIGINT NOT NULL, checkpoint_id VARCHAR(255), created_at DOUBLE NOT NULL, updated_at DOUBLE NOT NULL, INDEX idx_steps_run_status (run_id, status));\nCREATE TABLE IF NOT EXISTS events (event_id VARCHAR(128) PRIMARY KEY, run_id VARCHAR(128) NOT NULL, session_id VARCHAR(128), step_id VARCHAR(128), seq BIGINT NOT NULL, type VARCHAR(255) NOT NULL, timestamp DOUBLE NOT NULL, agent_role VARCHAR(255), state_version BIGINT, causal_token TEXT, payload_hash VARCHAR(128), payload_ref TEXT, UNIQUE KEY idx_events_run_seq (run_id, seq));\nCREATE TABLE IF NOT EXISTS tool_ledger (ledger_id VARCHAR(128) PRIMARY KEY, run_id VARCHAR(128) NOT NULL, session_id VARCHAR(128), step_id VARCHAR(128) NOT NULL, tool_name VARCHAR(255) NOT NULL, tool_version VARCHAR(64) NOT NULL, tool_call_id VARCHAR(128) NOT NULL, idempotency_key VARCHAR(255) NOT NULL UNIQUE, causal_token TEXT NOT NULL, request_hash VARCHAR(128) NOT NULL, request_ref TEXT NOT NULL, status VARCHAR(64) NOT NULL, external_id VARCHAR(255), response_hash VARCHAR(128), response_ref TEXT, error_type VARCHAR(255), created_at DOUBLE NOT NULL, updated_at DOUBLE NOT NULL, INDEX idx_tool_ledger_run_tool (run_id, tool_name));";
5744
5745pub type MCPCall = fn(&str, State) -> Result<Value>;
5746pub type MCPResourceRead = fn(&str) -> Result<Value>;
5747
5748#[derive(Clone, Debug)]
5749pub struct MCPResourceDescriptor {
5750    pub uri: String,
5751    pub name: String,
5752    pub mime_type: String,
5753}
5754
5755impl MCPResourceDescriptor {
5756    pub fn to_state(&self) -> State {
5757        state(&[
5758            ("uri", self.uri.clone().into()),
5759            ("name", self.name.clone().into()),
5760            ("mimeType", self.mime_type.clone().into()),
5761        ])
5762    }
5763}
5764
5765pub struct InMemoryMCPToolServer {
5766    tools: HashMap<String, (State, MCPCall)>,
5767}
5768
5769impl InMemoryMCPToolServer {
5770    pub fn new() -> Self {
5771        Self {
5772            tools: HashMap::new(),
5773        }
5774    }
5775    pub fn add_tool(&mut self, descriptor: State, handler: MCPCall) {
5776        if let Some(Value::String(name)) = descriptor.get("name") {
5777            self.tools.insert(name.clone(), (descriptor, handler));
5778        }
5779    }
5780    pub fn list_tools(&self) -> Vec<State> {
5781        let mut names: Vec<_> = self.tools.keys().cloned().collect();
5782        names.sort();
5783        names
5784            .into_iter()
5785            .filter_map(|name| self.tools.get(&name).map(|entry| entry.0.clone()))
5786            .collect()
5787    }
5788    pub fn call_tool(&self, name: &str, args: State) -> Result<Value> {
5789        let (_, handler) = self
5790            .tools
5791            .get(name)
5792            .ok_or_else(|| RuntimeError(format!("MCP tool not found: {name}")))?;
5793        handler(name, args)
5794    }
5795}
5796
5797pub struct InMemoryMCPContextServer {
5798    resources: HashMap<String, (MCPResourceDescriptor, MCPResourceRead)>,
5799}
5800
5801impl InMemoryMCPContextServer {
5802    pub fn new() -> Self {
5803        Self {
5804            resources: HashMap::new(),
5805        }
5806    }
5807    pub fn add_resource(
5808        &mut self,
5809        uri: &str,
5810        name: &str,
5811        mime_type: &str,
5812        reader: MCPResourceRead,
5813    ) {
5814        self.resources.insert(
5815            uri.to_string(),
5816            (
5817                MCPResourceDescriptor {
5818                    uri: uri.to_string(),
5819                    name: name.to_string(),
5820                    mime_type: if mime_type.is_empty() {
5821                        "application/json".to_string()
5822                    } else {
5823                        mime_type.to_string()
5824                    },
5825                },
5826                reader,
5827            ),
5828        );
5829    }
5830    pub fn list_resources(&self) -> Vec<State> {
5831        let mut uris: Vec<_> = self.resources.keys().cloned().collect();
5832        uris.sort();
5833        uris.into_iter()
5834            .filter_map(|uri| self.resources.get(&uri).map(|entry| entry.0.to_state()))
5835            .collect()
5836    }
5837    pub fn read_resource(&self, uri: &str) -> Result<State> {
5838        let (descriptor, reader) = self
5839            .resources
5840            .get(uri)
5841            .ok_or_else(|| RuntimeError(format!("MCP resource not found: {uri}")))?;
5842        Ok(state(&[
5843            ("resource", Value::Object(descriptor.to_state())),
5844            ("content", reader(uri)?),
5845        ]))
5846    }
5847}
5848
5849pub struct MCPToolAdapter {
5850    pub client_call: MCPCall,
5851}
5852
5853impl MCPToolAdapter {
5854    pub fn tool_spec_from_descriptor(&self, descriptor: &State) -> ToolSpec {
5855        let name = string_field(descriptor, "name", "");
5856        let version = string_field(descriptor, "version", "v1");
5857        let annotations = match descriptor.get("annotations") {
5858            Some(Value::Object(state)) => state.clone(),
5859            _ => State::new(),
5860        };
5861        let side_effect = string_field(&annotations, "side_effect", "none");
5862        let risk_level = string_field(&annotations, "risk_level", "low");
5863        let idempotency_required = match annotations.get("idempotency_required") {
5864            Some(Value::Bool(value)) => *value,
5865            _ => side_effect != "none",
5866        };
5867        let approval_required = match annotations.get("approval_required") {
5868            Some(Value::Bool(value)) => *value,
5869            _ => false,
5870        };
5871        let sandbox_required = match annotations.get("sandbox_required") {
5872            Some(Value::Bool(value)) => *value,
5873            _ => false,
5874        };
5875        let sandbox_executor = string_field(&annotations, "sandbox_executor", "");
5876        let sandbox_policy = match annotations.get("sandbox_policy") {
5877            Some(Value::Object(state)) => state.clone(),
5878            _ => State::new(),
5879        };
5880        let client_call = self.client_call;
5881        let tool_name = name.clone();
5882        let mut spec = ToolSpec::new(&name, Box::new(move |args| client_call(&tool_name, args)));
5883        spec.version = version;
5884        spec.side_effect = side_effect;
5885        spec.risk_level = risk_level;
5886        spec.idempotency_required = idempotency_required;
5887        spec.approval_required = approval_required;
5888        spec.sandbox_required = sandbox_required;
5889        spec.sandbox_executor = sandbox_executor;
5890        spec.sandbox_policy = sandbox_policy;
5891        spec.input_schema = descriptor
5892            .get("inputSchema")
5893            .or_else(|| descriptor.get("input_schema"))
5894            .cloned();
5895        spec.output_schema = descriptor
5896            .get("outputSchema")
5897            .or_else(|| descriptor.get("output_schema"))
5898            .cloned();
5899        spec
5900    }
5901}
5902
5903pub struct MCPContextAdapter {
5904    pub resource_read: MCPResourceRead,
5905}
5906
5907impl MCPContextAdapter {
5908    pub fn read_tool_spec(&self, name: &str, risk_level: &str) -> ToolSpec {
5909        let tool_name = if name.is_empty() {
5910            "mcp.context.read"
5911        } else {
5912            name
5913        };
5914        let risk = if risk_level.is_empty() {
5915            "low"
5916        } else {
5917            risk_level
5918        };
5919        let reader = self.resource_read;
5920        let mut spec = ToolSpec::new(
5921            tool_name,
5922            Box::new(move |args| match args.get("uri") {
5923                Some(Value::String(uri)) => reader(uri),
5924                _ => Err(RuntimeError("uri is required".to_string())),
5925            }),
5926        );
5927        spec.risk_level = risk.to_string();
5928        spec.side_effect = "none".to_string();
5929        spec.input_schema = Some(Value::Object(state(&[("type", "object".into())])));
5930        spec.output_schema = Some(Value::Object(state(&[("type", "object".into())])));
5931        spec
5932    }
5933}
5934
5935fn string_field(state: &State, key: &str, fallback: &str) -> String {
5936    match state.get(key) {
5937        Some(Value::String(value)) => value.clone(),
5938        _ => fallback.to_string(),
5939    }
5940}
5941
5942pub type FrameworkAgentFunc = fn(&mut AgentContext, State) -> Result<Option<Value>>;
5943
5944pub struct FunctionAdapter {
5945    pub func: FrameworkAgentFunc,
5946    pub role: String,
5947    pub name: String,
5948}
5949
5950impl FunctionAdapter {
5951    pub fn new(func: FrameworkAgentFunc, role: &str) -> Self {
5952        Self {
5953            func,
5954            role: if role.is_empty() {
5955                "Agent".to_string()
5956            } else {
5957                role.to_string()
5958            },
5959            name: "function".to_string(),
5960        }
5961    }
5962    pub fn map_run_spec(&self) -> State {
5963        state(&[
5964            ("adapter", self.name.clone().into()),
5965            ("role", self.role.clone().into()),
5966        ])
5967    }
5968    pub fn run(&self, ctx: &mut AgentContext, state_value: State, output_key: &str) -> Result<()> {
5969        if let Some(result) = (self.func)(ctx, state_value)? {
5970            if !output_key.is_empty() {
5971                ctx.write_state(output_key, result);
5972            }
5973        }
5974        Ok(())
5975    }
5976}
5977
5978pub type MethodHandler = fn(State) -> Result<Value>;
5979
5980pub struct MethodFrameworkAdapter {
5981    pub target_name: String,
5982    pub role: String,
5983    pub method_candidates: Vec<String>,
5984    pub methods: HashMap<String, MethodHandler>,
5985    pub output_key: String,
5986}
5987
5988impl MethodFrameworkAdapter {
5989    pub fn new(
5990        target_name: &str,
5991        role: &str,
5992        method_candidates: Vec<String>,
5993        methods: HashMap<String, MethodHandler>,
5994        output_key: &str,
5995    ) -> Self {
5996        Self {
5997            target_name: target_name.to_string(),
5998            role: if role.is_empty() {
5999                "FrameworkAgent".to_string()
6000            } else {
6001                role.to_string()
6002            },
6003            method_candidates,
6004            methods,
6005            output_key: if output_key.is_empty() {
6006                "output".to_string()
6007            } else {
6008                output_key.to_string()
6009            },
6010        }
6011    }
6012    pub fn map_run_spec(&self) -> State {
6013        state(&[
6014            ("adapter", "method-framework".into()),
6015            ("role", self.role.clone().into()),
6016            ("target", self.target_name.clone().into()),
6017            (
6018                "methods",
6019                Value::Array(
6020                    self.method_candidates
6021                        .iter()
6022                        .map(|item| Value::String(item.clone()))
6023                        .collect(),
6024                ),
6025            ),
6026        ])
6027    }
6028    pub fn run(&self, ctx: &mut AgentContext, state_value: State) -> Result<()> {
6029        for name in &self.method_candidates {
6030            if let Some(handler) = self.methods.get(name) {
6031                let result = handler(state_value)?;
6032                if !self.output_key.is_empty() {
6033                    ctx.write_state(&self.output_key, result);
6034                }
6035                return Ok(());
6036            }
6037        }
6038        Err(RuntimeError(
6039            "target does not expose any candidate method".to_string(),
6040        ))
6041    }
6042}
6043
6044#[derive(Clone, Debug)]
6045pub struct BoundaryLintRule {
6046    pub rule_id: String,
6047    pub pattern: String,
6048    pub category: String,
6049    pub message: String,
6050    pub suggestion: String,
6051    pub prefix: bool,
6052}
6053
6054#[derive(Clone, Debug)]
6055pub struct BoundaryLintFinding {
6056    pub path: String,
6057    pub line: usize,
6058    pub column: usize,
6059    pub rule_id: String,
6060    pub severity: String,
6061    pub callee: String,
6062    pub category: String,
6063    pub message: String,
6064    pub suggestion: String,
6065}
6066
6067#[derive(Clone, Debug)]
6068pub struct BoundaryLintReport {
6069    pub passed: bool,
6070    pub scanned_files: Vec<String>,
6071    pub finding_count: usize,
6072    pub findings: Vec<BoundaryLintFinding>,
6073}
6074
6075pub fn default_boundary_rules() -> Vec<BoundaryLintRule> {
6076    vec![
6077        BoundaryLintRule { rule_id: "direct-shell-os-system".into(), pattern: "os.system".into(), category: "shell".into(), message: "direct shell execution bypasses ToolGateway, policy, ledger, sandbox, and audit".into(), suggestion: "wrap shell execution as a runtime-managed tool".into(), prefix: false },
6078        BoundaryLintRule { rule_id: "direct-shell-subprocess".into(), pattern: "subprocess.".into(), category: "shell".into(), message: "direct subprocess execution bypasses ToolGateway, policy, ledger, sandbox, and audit".into(), suggestion: "wrap command execution as a runtime-managed tool".into(), prefix: true },
6079        BoundaryLintRule { rule_id: "direct-http-requests".into(), pattern: "requests.".into(), category: "network".into(), message: "direct HTTP calls bypass ToolGateway, policy, ledger, budget, replay, and audit".into(), suggestion: "register the HTTP/API call as a runtime-managed tool".into(), prefix: true },
6080        BoundaryLintRule { rule_id: "direct-http-httpx".into(), pattern: "httpx.".into(), category: "network".into(), message: "direct HTTP calls bypass ToolGateway, policy, ledger, budget, replay, and audit".into(), suggestion: "register the HTTP/API call as a runtime-managed tool".into(), prefix: true },
6081        BoundaryLintRule { rule_id: "direct-openai-sdk".into(), pattern: "openai.".into(), category: "model".into(), message: "direct model SDK usage bypasses model provider archives, replay, budget, and attribution".into(), suggestion: "call models through the runtime model boundary".into(), prefix: true },
6082        BoundaryLintRule { rule_id: "direct-anthropic-sdk".into(), pattern: "anthropic.".into(), category: "model".into(), message: "direct model SDK usage bypasses model provider archives, replay, budget, and attribution".into(), suggestion: "call models through the runtime model boundary".into(), prefix: true },
6083    ]
6084}
6085
6086pub fn scan_boundary_source(
6087    path: &str,
6088    source: &str,
6089    rules: Option<Vec<BoundaryLintRule>>,
6090) -> BoundaryLintReport {
6091    let rules = rules.unwrap_or_else(default_boundary_rules);
6092    let lines: Vec<&str> = source.split('\n').collect();
6093    let mut findings = Vec::new();
6094    for (i, line) in lines.iter().enumerate() {
6095        let previous = if i > 0 { lines[i - 1] } else { "" };
6096        if line.contains("agentledger: ignore-boundary")
6097            || previous.contains("agentledger: ignore-next-line")
6098        {
6099            continue;
6100        }
6101        for rule in &rules {
6102            if let Some(index) = line.find(&rule.pattern) {
6103                let mut callee = rule.pattern.clone();
6104                if rule.prefix {
6105                    let mut end = index + rule.pattern.len();
6106                    while end < line.len() {
6107                        let ch = line.as_bytes()[end] as char;
6108                        if ch.is_ascii_alphanumeric() || ch == '_' || ch == '.' {
6109                            end += 1;
6110                        } else {
6111                            break;
6112                        }
6113                    }
6114                    callee = line[index..end].to_string();
6115                }
6116                findings.push(BoundaryLintFinding {
6117                    path: path.into(),
6118                    line: i + 1,
6119                    column: index + 1,
6120                    rule_id: rule.rule_id.clone(),
6121                    severity: "error".into(),
6122                    callee,
6123                    category: rule.category.clone(),
6124                    message: rule.message.clone(),
6125                    suggestion: rule.suggestion.clone(),
6126                });
6127                break;
6128            }
6129        }
6130    }
6131    BoundaryLintReport {
6132        passed: findings.is_empty(),
6133        scanned_files: vec![path.into()],
6134        finding_count: findings.len(),
6135        findings,
6136    }
6137}
6138
6139#[derive(Clone, Debug)]
6140pub struct RecoverySummary {
6141    pub recovered_steps: usize,
6142}
6143
6144#[derive(Clone, Debug)]
6145pub struct SchedulerStepStatus {
6146    pub step_id: String,
6147    pub status: String,
6148    pub owner: Option<String>,
6149    pub attempt: u64,
6150    pub lease_until: Option<f64>,
6151    pub last_error_type: Option<String>,
6152}
6153
6154#[derive(Clone, Debug)]
6155pub struct SchedulerStatus {
6156    pub run_id: String,
6157    pub run_status: String,
6158    pub state_version: u64,
6159    pub steps: Vec<SchedulerStepStatus>,
6160    pub cost_summary: CostSummary,
6161}
6162
6163pub struct RuntimeScheduler;
6164
6165impl RuntimeScheduler {
6166    pub fn recover_expired_leases(store: &mut MemoryStore) -> RecoverySummary {
6167        RecoverySummary {
6168            recovered_steps: store.recover_expired_leases(),
6169        }
6170    }
6171
6172    pub fn cancel_run(store: &mut MemoryStore, run_id: &str, reason: &str) -> Result<usize> {
6173        store.cancel_run(run_id, reason)
6174    }
6175
6176    pub fn status(store: &MemoryStore, run_id: &str) -> Result<SchedulerStatus> {
6177        let run = store.run(run_id)?;
6178        let steps = store
6179            .steps(run_id)
6180            .into_iter()
6181            .map(|step| SchedulerStepStatus {
6182                step_id: step.step_id,
6183                status: step.status,
6184                owner: step.owner,
6185                attempt: step.attempt,
6186                lease_until: step.lease_until,
6187                last_error_type: step.last_error_type,
6188            })
6189            .collect();
6190        Ok(SchedulerStatus {
6191            run_id: run_id.to_string(),
6192            run_status: run.status,
6193            state_version: run.state_version,
6194            steps,
6195            cost_summary: store.cost_summary(run_id),
6196        })
6197    }
6198}
6199
6200#[derive(Clone, Debug)]
6201pub struct ReviewCheck {
6202    pub name: String,
6203    pub passed: bool,
6204    pub severity: String,
6205    pub detail: String,
6206}
6207
6208#[derive(Clone, Debug)]
6209pub struct AdversarialReviewReport {
6210    pub passed: bool,
6211    pub run_id: Option<String>,
6212    pub checks: Vec<ReviewCheck>,
6213    pub metadata: State,
6214}
6215
6216pub fn adversarial_review(
6217    bundle: &EvidenceBundle,
6218    max_total_usd: Option<f64>,
6219) -> AdversarialReviewReport {
6220    let mut checks = vec![
6221        review_check(
6222            "no_failed_steps",
6223            !bundle
6224                .events
6225                .iter()
6226                .any(|event| event.event_type == "step_failed"),
6227            "blocker",
6228            "no step is in failed status",
6229        ),
6230        review_check(
6231            "no_pending_verification",
6232            !bundle
6233                .tool_ledger
6234                .iter()
6235                .any(|row| row.status == "PENDING_VERIFICATION"),
6236            "blocker",
6237            "no side effect is pending verification",
6238        ),
6239        review_check(
6240            "no_pending_approvals",
6241            !bundle.approvals.iter().any(|row| row.status == "PENDING"),
6242            "blocker",
6243            "no approval request is still pending",
6244        ),
6245        review_check(
6246            "completed_steps_have_completion_events",
6247            completed_steps_have_events_rust(&bundle.steps, &bundle.events),
6248            "blocker",
6249            "completed steps have step_completed events",
6250        ),
6251        review_check(
6252            "ledger_statuses_known",
6253            ledger_statuses_known_rust(&bundle.tool_ledger),
6254            "blocker",
6255            "Tool Ledger rows use known statuses",
6256        ),
6257        review_check(
6258            "event_sequence_contiguous",
6259            event_sequence_contiguous_rust(&bundle.events),
6260            "blocker",
6261            "event sequence has no gaps",
6262        ),
6263        review_check(
6264            "artifacts_have_blob_refs",
6265            bundle
6266                .artifacts
6267                .iter()
6268                .all(|row| !row.blob_ref.is_empty() && !row.blob_hash.is_empty()),
6269            "warning",
6270            "artifacts have blob refs and hashes",
6271        ),
6272        review_check(
6273            "media_artifacts_have_refs",
6274            bundle
6275                .media_artifacts
6276                .iter()
6277                .all(media_artifact_has_ref_rust),
6278            "blocker",
6279            "media artifacts have kind and durable refs",
6280        ),
6281        review_check(
6282            "stream_checkpoints_have_offsets",
6283            bundle
6284                .stream_checkpoints
6285                .iter()
6286                .all(stream_checkpoint_has_offset_rust),
6287            "blocker",
6288            "stream checkpoints have stream, consumer, and offset",
6289        ),
6290        review_check(
6291            "high_risk_approvals_decided",
6292            high_risk_approvals_decided_rust(&bundle.approvals),
6293            "blocker",
6294            "high-risk approval requests are decided",
6295        ),
6296        review_check(
6297            "no_blocking_failure_events",
6298            !bundle.events.iter().any(|event| {
6299                matches!(
6300                    event.event_type.as_str(),
6301                    "error_raised" | "step_failed" | "tool_call_failed" | "tool_call_blocked"
6302                )
6303            }),
6304            "warning",
6305            "no blocking failure events are present",
6306        ),
6307    ];
6308    if let Some(limit) = max_total_usd {
6309        checks.push(review_check(
6310            "max_total_usd",
6311            bundle.cost_summary.total_usd <= limit,
6312            "blocker",
6313            "cost limit check",
6314        ));
6315    }
6316    let passed = checks
6317        .iter()
6318        .all(|check| check.severity != "blocker" || check.passed);
6319    let mut metadata = State::new();
6320    metadata.insert(
6321        "event_count".into(),
6322        Value::Number(bundle.events.len() as f64),
6323    );
6324    metadata.insert(
6325        "tool_ledger_count".into(),
6326        Value::Number(bundle.tool_ledger.len() as f64),
6327    );
6328    metadata.insert(
6329        "approval_count".into(),
6330        Value::Number(bundle.approvals.len() as f64),
6331    );
6332    metadata.insert(
6333        "artifact_count".into(),
6334        Value::Number(bundle.artifacts.len() as f64),
6335    );
6336    metadata.insert(
6337        "media_artifact_count".into(),
6338        Value::Number(bundle.media_artifacts.len() as f64),
6339    );
6340    metadata.insert(
6341        "stream_checkpoint_count".into(),
6342        Value::Number(bundle.stream_checkpoints.len() as f64),
6343    );
6344    AdversarialReviewReport {
6345        passed,
6346        run_id: Some(bundle.run.run_id.clone()),
6347        checks,
6348        metadata,
6349    }
6350}
6351
6352fn review_check(name: &str, passed: bool, severity: &str, detail: &str) -> ReviewCheck {
6353    ReviewCheck {
6354        name: name.into(),
6355        passed,
6356        severity: severity.into(),
6357        detail: detail.into(),
6358    }
6359}
6360
6361fn completed_steps_have_events_rust(steps: &[Step], events: &[Event]) -> bool {
6362    steps.iter().all(|step| {
6363        step.status != "completed"
6364            || events.iter().any(|event| {
6365                event.event_type == "step_completed"
6366                    && event.step_id.as_deref() == Some(step.step_id.as_str())
6367            })
6368    })
6369}
6370
6371fn ledger_statuses_known_rust(rows: &[ToolLedgerEntry]) -> bool {
6372    rows.iter().all(|row| {
6373        matches!(
6374            row.status.as_str(),
6375            "SUCCEEDED"
6376                | "FAILED_NO_EFFECT"
6377                | "PENDING_VERIFICATION"
6378                | "COMPENSATED"
6379                | "RUNNING"
6380                | "RESERVED"
6381        )
6382    })
6383}
6384fn event_sequence_contiguous_rust(events: &[Event]) -> bool {
6385    events
6386        .iter()
6387        .enumerate()
6388        .all(|(index, event)| event.seq == (index as u64) + 1)
6389}
6390fn state_has_key(row: &State, key: &str) -> bool {
6391    !matches!(row.get(key), None | Some(Value::Null))
6392}
6393fn media_artifact_has_ref_rust(row: &State) -> bool {
6394    state_has_key(row, "kind")
6395        && (state_has_key(row, "uri")
6396            || state_has_key(row, "content_ref")
6397            || state_has_key(row, "blob_ref"))
6398}
6399fn stream_checkpoint_has_offset_rust(row: &State) -> bool {
6400    state_has_key(row, "stream_id")
6401        && state_has_key(row, "consumer_id")
6402        && state_has_key(row, "offset")
6403}
6404fn high_risk_approvals_decided_rust(rows: &[ApprovalRequest]) -> bool {
6405    rows.iter().all(|row| {
6406        !matches!(
6407            row.risk_level.as_str(),
6408            "high" | "destructive" | "sensitive"
6409        ) || matches!(row.status.as_str(), "APPROVED" | "DENIED")
6410    })
6411}
6412
6413#[derive(Clone, Debug)]
6414pub struct EvidenceCheck {
6415    pub name: String,
6416    pub passed: bool,
6417    pub detail: String,
6418}
6419
6420#[derive(Clone, Debug)]
6421pub struct EvidenceCheckReport {
6422    pub passed: bool,
6423    pub checks: Vec<EvidenceCheck>,
6424    pub metadata: State,
6425}
6426
6427pub fn evaluate_evidence(
6428    bundle: &EvidenceBundle,
6429    max_total_usd: Option<f64>,
6430) -> EvidenceCheckReport {
6431    let mut checks = vec![
6432        evidence_check(
6433            "no_failed_steps",
6434            !bundle
6435                .events
6436                .iter()
6437                .any(|event| event.event_type == "step_failed"),
6438            "all steps completed or remain non-failed",
6439        ),
6440        evidence_check(
6441            "no_pending_verification",
6442            !bundle
6443                .tool_ledger
6444                .iter()
6445                .any(|row| row.status == "PENDING_VERIFICATION"),
6446            "no side effect is waiting for human/external verification",
6447        ),
6448        evidence_check(
6449            "completed_steps_have_events",
6450            completed_steps_have_events_rust(&bundle.steps, &bundle.events),
6451            "each completed step has a step_completed event",
6452        ),
6453        evidence_check(
6454            "managed_side_effects_are_ledgered",
6455            ledger_statuses_known_rust(&bundle.tool_ledger),
6456            "every ledger row has a known status",
6457        ),
6458        evidence_check(
6459            "media_artifacts_have_refs",
6460            bundle
6461                .media_artifacts
6462                .iter()
6463                .all(media_artifact_has_ref_rust),
6464            "media artifacts have kind and durable refs",
6465        ),
6466        evidence_check(
6467            "stream_checkpoints_have_offsets",
6468            bundle
6469                .stream_checkpoints
6470                .iter()
6471                .all(stream_checkpoint_has_offset_rust),
6472            "stream checkpoints have stream, consumer, and offset",
6473        ),
6474    ];
6475    if let Some(limit) = max_total_usd {
6476        checks.push(evidence_check(
6477            "max_total_usd",
6478            bundle.cost_summary.total_usd <= limit,
6479            "cost limit check",
6480        ));
6481    }
6482    EvidenceCheckReport {
6483        passed: checks.iter().all(|check| check.passed),
6484        checks,
6485        metadata: State::new(),
6486    }
6487}
6488
6489pub fn evaluate_evidence_regression(
6490    golden: &EvidenceBundle,
6491    current: &EvidenceBundle,
6492    max_total_usd_delta: Option<f64>,
6493) -> EvidenceCheckReport {
6494    let diff = diff_evidence(golden, current);
6495    let mut checks = vec![
6496        evidence_check(
6497            "final_state_regression",
6498            diff.final_state_changed_count == 0,
6499            "final state regression check",
6500        ),
6501        evidence_check(
6502            "event_type_regression",
6503            diff.event_types_changed_count == 0,
6504            "event type regression check",
6505        ),
6506        evidence_check(
6507            "tool_ledger_status_regression",
6508            true,
6509            "tool ledger status regression check",
6510        ),
6511        evidence_check(
6512            "media_artifact_regression",
6513            diff.media_artifacts_changed_count == 0,
6514            "media artifact regression check",
6515        ),
6516        evidence_check(
6517            "stream_checkpoint_regression",
6518            diff.stream_checkpoints_changed_count == 0,
6519            "stream checkpoint regression check",
6520        ),
6521    ];
6522    if let Some(limit) = max_total_usd_delta {
6523        let delta = current.cost_summary.total_usd - golden.cost_summary.total_usd;
6524        checks.push(evidence_check(
6525            "max_total_usd_delta",
6526            delta <= limit,
6527            "cost delta limit check",
6528        ));
6529    }
6530    EvidenceCheckReport {
6531        passed: checks.iter().all(|check| check.passed),
6532        checks,
6533        metadata: State::new(),
6534    }
6535}
6536
6537fn evidence_check(name: &str, passed: bool, detail: &str) -> EvidenceCheck {
6538    EvidenceCheck {
6539        name: name.into(),
6540        passed,
6541        detail: detail.into(),
6542    }
6543}
6544
6545#[derive(Clone, Debug)]
6546pub struct FailureInjectionCheck {
6547    pub name: String,
6548    pub passed: bool,
6549    pub detail: String,
6550    pub run_id: Option<String>,
6551}
6552
6553#[derive(Clone, Debug)]
6554pub struct FailureInjectionReport {
6555    pub passed: bool,
6556    pub checks: Vec<FailureInjectionCheck>,
6557}
6558
6559pub fn run_failure_injection_suite() -> FailureInjectionReport {
6560    let checks = vec![
6561        failure_retry_exhaustion(),
6562        failure_lease_fencing(),
6563        failure_cancellation_fencing(),
6564        failure_side_effect_idempotency(),
6565    ];
6566    FailureInjectionReport {
6567        passed: checks.iter().all(|check| check.passed),
6568        checks,
6569    }
6570}
6571fn failure_check(
6572    name: &str,
6573    passed: bool,
6574    detail: String,
6575    run_id: String,
6576) -> FailureInjectionCheck {
6577    FailureInjectionCheck {
6578        name: name.into(),
6579        passed,
6580        detail,
6581        run_id: Some(run_id),
6582    }
6583}
6584fn failure_retry_exhaustion() -> FailureInjectionCheck {
6585    let mut runtime = Runtime::new();
6586    let (run_id, _) = runtime.create_run(State::new());
6587    let _ = runtime.run_once(
6588        &run_id,
6589        "retry-1",
6590        "FailureInjector",
6591        60.0,
6592        |_ctx, _state| Err(RuntimeError("retryable".into())),
6593    );
6594    let _ = runtime.run_once(
6595        &run_id,
6596        "retry-2",
6597        "FailureInjector",
6598        60.0,
6599        |_ctx, _state| Err(RuntimeError("final failure".into())),
6600    );
6601    let status = runtime
6602        .store
6603        .run(&run_id)
6604        .map(|run| run.status)
6605        .unwrap_or_else(|_| "missing".into());
6606    failure_check(
6607        "retry_exhaustion",
6608        status == "failed",
6609        format!("run_status={status}"),
6610        run_id,
6611    )
6612}
6613fn failure_lease_fencing() -> FailureInjectionCheck {
6614    let mut store = MemoryStore::new();
6615    let (run_id, step_id) = store.create_run(State::new());
6616    let claim = store.claim_step("stale-worker", &run_id, 0.0).unwrap();
6617    let recovered = store.recover_expired_leases();
6618    let stale_rejected = store
6619        .commit_state_patch(&run_id, &step_id, &claim.lease_token, 0, State::new())
6620        .is_err();
6621    let fresh = store.claim_step("fresh-worker", &run_id, 60.0).unwrap();
6622    let passed = recovered == 1 && stale_rejected && fresh.attempt == 2;
6623    failure_check(
6624        "lease_fencing",
6625        passed,
6626        format!("recovered_steps={recovered} stale_rejected={stale_rejected}"),
6627        run_id,
6628    )
6629}
6630fn failure_cancellation_fencing() -> FailureInjectionCheck {
6631    let mut store = MemoryStore::new();
6632    let (run_id, step_id) = store.create_run(State::new());
6633    let claim = store.claim_step("stale-worker", &run_id, 60.0).unwrap();
6634    let cancelled = store.cancel_run(&run_id, "failure injection").unwrap();
6635    let stale_rejected = store
6636        .commit_state_patch(&run_id, &step_id, &claim.lease_token, 0, State::new())
6637        .is_err();
6638    let fresh = store.claim_step("fresh-worker", &run_id, 60.0).is_err();
6639    let status = store.run(&run_id).map(|run| run.status).unwrap_or_default();
6640    let passed = cancelled == 1 && stale_rejected && fresh && status == "cancelled";
6641    failure_check(
6642        "cancellation_fencing",
6643        passed,
6644        format!("cancelled_steps={cancelled} stale_rejected={stale_rejected}"),
6645        run_id,
6646    )
6647}
6648fn failure_side_effect_idempotency() -> FailureInjectionCheck {
6649    use std::sync::{Arc, Mutex};
6650    let calls = Arc::new(Mutex::new(0usize));
6651    let calls_for_tool = Arc::clone(&calls);
6652    let mut runtime = Runtime::new();
6653    runtime.register_tool(
6654        ToolSpec::new(
6655            "external.create",
6656            Box::new(move |_args| {
6657                let mut guard = calls_for_tool.lock().unwrap();
6658                *guard += 1;
6659                Ok(Value::Object(state(&[(
6660                    "id",
6661                    Value::String("EXT-1".into()),
6662                )])))
6663            }),
6664        )
6665        .side_effect("external")
6666        .idempotency_required(true),
6667    );
6668    let (run_id, _) = runtime.create_run(State::new());
6669    let ctx = failure_claim_context(&mut runtime, &run_id, "worker-1", "FailureInjector");
6670    let _ = runtime.call_tool(
6671        &ctx,
6672        "external.create",
6673        state(&[("title", Value::String("once".into()))]),
6674    );
6675    runtime
6676        .store
6677        .mark_retry(&run_id, &ctx.step_id, "RetryableAgentError", "retryable");
6678    let ctx2 = failure_claim_context(&mut runtime, &run_id, "worker-2", "FailureInjector");
6679    let _ = runtime.call_tool(
6680        &ctx2,
6681        "external.create",
6682        state(&[("title", Value::String("once".into()))]),
6683    );
6684    let count = *calls.lock().unwrap();
6685    failure_check(
6686        "side_effect_idempotency",
6687        count == 1,
6688        format!("external_call_count={count}"),
6689        run_id,
6690    )
6691}
6692
6693fn failure_claim_context(
6694    runtime: &mut Runtime,
6695    run_id: &str,
6696    worker_id: &str,
6697    agent_role: &str,
6698) -> AgentContext {
6699    let claim = runtime.store.claim_step(worker_id, run_id, 60.0).unwrap();
6700    AgentContext {
6701        run_id: claim.run_id,
6702        session_id: claim.session_id,
6703        step_id: claim.step_id,
6704        agent_role: agent_role.to_string(),
6705        lease_token: claim.lease_token,
6706        attempt: claim.attempt,
6707        state_version: claim.state_version,
6708        pending_patch: State::new(),
6709    }
6710}
6711
6712#[derive(Clone, Debug)]
6713pub struct ShadowReport {
6714    pub source_run_id: String,
6715    pub shadow_run_id: String,
6716    pub ok: bool,
6717    pub state_diff: State,
6718}
6719
6720pub fn diff_states(source: &State, shadow: &State) -> State {
6721    let mut changed = State::new();
6722    for key in source.keys().chain(shadow.keys()) {
6723        if changed.contains_key(key) {
6724            continue;
6725        }
6726        if source.get(key) != shadow.get(key) {
6727            changed.insert(
6728                key.clone(),
6729                Value::Object(state(&[
6730                    ("source", source.get(key).cloned().unwrap_or_default()),
6731                    ("shadow", shadow.get(key).cloned().unwrap_or_default()),
6732                ])),
6733            );
6734        }
6735    }
6736    state(&[
6737        ("changed", Value::Object(changed.clone())),
6738        ("changed_count", Value::Number(changed.len() as f64)),
6739    ])
6740}
6741
6742pub fn shadow_report(
6743    source_run_id: &str,
6744    shadow_run_id: &str,
6745    ok: bool,
6746    source_state: &State,
6747    shadow_state: &State,
6748) -> ShadowReport {
6749    ShadowReport {
6750        source_run_id: source_run_id.into(),
6751        shadow_run_id: shadow_run_id.into(),
6752        ok,
6753        state_diff: diff_states(source_state, shadow_state),
6754    }
6755}
6756
6757pub fn builtin_golden_names() -> Vec<String> {
6758    vec![
6759        "media-stream-checkpoint".into(),
6760        "minimal-success".into(),
6761        "tool-ledger-success".into(),
6762    ]
6763}
6764
6765pub fn builtin_golden_evidence(name: &str) -> Result<EvidenceBundle> {
6766    match name {
6767        "minimal-success" => golden_minimal_success(),
6768        "tool-ledger-success" => golden_tool_ledger_success(),
6769        "media-stream-checkpoint" => golden_media_stream_checkpoint(),
6770        _ => Err(RuntimeError(format!(
6771            "unknown built-in golden case: {name}"
6772        ))),
6773    }
6774}
6775
6776pub fn golden_regression(golden: &EvidenceBundle, current: &EvidenceBundle) -> EvidenceCheckReport {
6777    evaluate_evidence_regression(golden, current, None)
6778}
6779
6780fn golden_minimal_success() -> Result<EvidenceBundle> {
6781    let mut runtime = Runtime::new();
6782    let (run_id, _) = runtime.create_run(State::new());
6783    runtime.run_once(
6784        &run_id,
6785        "golden-worker",
6786        "GoldenAgent",
6787        60.0,
6788        |ctx, _state| {
6789            ctx.write_state("answer", Value::String("ok".into()));
6790            Ok(())
6791        },
6792    )?;
6793    export_evidence(&runtime.store, &run_id)
6794}
6795fn golden_tool_ledger_success() -> Result<EvidenceBundle> {
6796    let mut runtime = Runtime::new();
6797    runtime.register_tool(
6798        ToolSpec::new(
6799            "github.create_issue",
6800            Box::new(|_args| {
6801                Ok(Value::Object(state(&[(
6802                    "issue_id",
6803                    Value::String("ISSUE-1".into()),
6804                )])))
6805            }),
6806        )
6807        .side_effect("external"),
6808    );
6809    let (run_id, _) = runtime.create_run(State::new());
6810    runtime.run_once(
6811        &run_id,
6812        "golden-worker",
6813        "ExecutorAgent",
6814        60.0,
6815        |ctx, _state| {
6816            ctx.write_state("issue_id", Value::String("ISSUE-1".into()));
6817            Ok(())
6818        },
6819    )?;
6820    export_evidence(&runtime.store, &run_id)
6821}
6822fn golden_media_stream_checkpoint() -> Result<EvidenceBundle> {
6823    let mut runtime = Runtime::new();
6824    let (run_id, _) = runtime.create_run(State::new());
6825    runtime.run_once(
6826        &run_id,
6827        "golden-worker",
6828        "MediaAgent",
6829        60.0,
6830        |ctx, _state| {
6831            ctx.write_state("processed_offset", Value::Number(42.0));
6832            Ok(())
6833        },
6834    )?;
6835    runtime.store.create_artifact(
6836        &run_id,
6837        None,
6838        "golden-video-frame",
6839        State::new(),
6840        state(&[(
6841            "agentledger_media",
6842            Value::Object(state(&[
6843                ("kind", Value::String("frame".into())),
6844                ("uri", Value::String("file://golden-frame.jpg".into())),
6845            ])),
6846        )]),
6847    );
6848    runtime.store.create_artifact(
6849        &run_id,
6850        None,
6851        "golden-stream-checkpoint",
6852        State::new(),
6853        state(&[(
6854            "agentledger_stream",
6855            Value::Object(state(&[
6856                ("stream_id", Value::String("stream-golden".into())),
6857                ("consumer_id", Value::String("consumer-golden".into())),
6858                ("offset", Value::Number(42.0)),
6859            ])),
6860        )]),
6861    );
6862    export_evidence(&runtime.store, &run_id)
6863}
6864
6865#[derive(Clone, Debug)]
6866pub struct TimeTravelFrame {
6867    pub seq: u64,
6868    pub event_id: String,
6869    pub event_type: String,
6870    pub step_id: Option<String>,
6871    pub agent_role: Option<String>,
6872    pub state_version: Option<u64>,
6873    pub timestamp: f64,
6874    pub state_changed: bool,
6875    pub changed_keys: Vec<String>,
6876    pub patch: Option<State>,
6877    pub state_after: Option<State>,
6878}
6879
6880#[derive(Clone, Debug)]
6881pub struct TimeTravelReport {
6882    pub run_id: String,
6883    pub at_seq: Option<u64>,
6884    pub event_count: usize,
6885    pub timeline: Vec<TimeTravelFrame>,
6886    pub state_at_seq: State,
6887    pub selected_event: Option<TimeTravelFrame>,
6888}
6889
6890pub fn time_travel(
6891    bundle: &EvidenceBundle,
6892    at_seq: Option<u64>,
6893    include_states: bool,
6894) -> TimeTravelReport {
6895    let mut current = State::new();
6896    let mut state_at_seq = State::new();
6897    let mut selected_event = None;
6898    let mut timeline = Vec::new();
6899    for event in &bundle.events {
6900        let before = current.clone();
6901        let patch = patch_for_time_travel_event(event);
6902        if let Some(patch_value) = &patch {
6903            for (key, value) in patch_value {
6904                current.insert(key.clone(), value.clone());
6905            }
6906        }
6907        let diff = diff_states(&before, &current);
6908        let changed_keys = match diff.get("changed") {
6909            Some(Value::Object(obj)) => obj.keys().cloned().collect(),
6910            _ => Vec::new(),
6911        };
6912        let frame = TimeTravelFrame {
6913            seq: event.seq,
6914            event_id: event.event_id.clone(),
6915            event_type: event.event_type.clone(),
6916            step_id: event.step_id.clone(),
6917            agent_role: event.agent_role.clone(),
6918            state_version: event.state_version,
6919            timestamp: event.timestamp,
6920            state_changed: diff.get("changed_count") != Some(&Value::Number(0.0)),
6921            changed_keys,
6922            patch,
6923            state_after: if include_states {
6924                Some(current.clone())
6925            } else {
6926                None
6927            },
6928        };
6929        if at_seq.is_some_and(|seq| event.seq <= seq) {
6930            state_at_seq = current.clone();
6931            selected_event = Some(frame.clone());
6932        }
6933        timeline.push(frame);
6934    }
6935    if at_seq.is_none() {
6936        state_at_seq = current.clone();
6937    }
6938    TimeTravelReport {
6939        run_id: bundle.run.run_id.clone(),
6940        at_seq,
6941        event_count: timeline.len(),
6942        timeline,
6943        state_at_seq,
6944        selected_event,
6945    }
6946}
6947
6948fn patch_for_time_travel_event(event: &Event) -> Option<State> {
6949    if event.event_type == "run_created" {
6950        if let Some(Value::Object(obj)) = event.payload.get("initial_state") {
6951            return Some(obj.clone());
6952        }
6953        return Some(State::new());
6954    }
6955    if event.event_type == "state_committed"
6956        || event.event_type == "state_patch_committed"
6957        || event.event_type == "system_state_patch_applied"
6958    {
6959        if let Some(Value::Object(obj)) = event.payload.get("patch") {
6960            return Some(obj.clone());
6961        }
6962        return Some(State::new());
6963    }
6964    None
6965}
6966
6967pub fn time_travel_html(report: &TimeTravelReport) -> String {
6968    let rows = report
6969        .timeline
6970        .iter()
6971        .map(|frame| {
6972            format!(
6973                "<tr><td>{}</td><td>{}</td><td>{}</td></tr>",
6974                frame.seq,
6975                frame.event_type,
6976                frame.changed_keys.join(", ")
6977            )
6978        })
6979        .collect::<Vec<_>>()
6980        .join("\n");
6981    format!("<!doctype html><html><head><meta charset=\"utf-8\"><title>AgentLedger Time Travel Report</title></head><body><h1>AgentLedger Time Travel Report</h1><p>Run <code>{}</code></p><table>{}</table><h2>State At Selected Point</h2><pre>{:?}</pre><h2>Selected Event</h2><pre>{:?}</pre></body></html>", report.run_id, rows, report.state_at_seq, report.selected_event)
6982}
6983
6984#[derive(Debug, Clone, PartialEq, Eq)]
6985pub struct OptionalAdapterCapability {
6986    pub name: String,
6987    pub category: String,
6988    pub core_imports_heavy_sdks: bool,
6989    pub adapter_is_optional: bool,
6990    pub fail_closed_without_adapter: bool,
6991    pub contract_surface: Vec<String>,
6992}
6993
6994pub fn optional_adapter_capabilities() -> Vec<OptionalAdapterCapability> {
6995    fn item(name: &str, category: &str, surface: &[&str]) -> OptionalAdapterCapability {
6996        OptionalAdapterCapability {
6997            name: name.to_string(),
6998            category: category.to_string(),
6999            core_imports_heavy_sdks: false,
7000            adapter_is_optional: true,
7001            fail_closed_without_adapter: true,
7002            contract_surface: surface.iter().map(|s| s.to_string()).collect(),
7003        }
7004    }
7005    vec![
7006        item("postgres", "storage", &["ddl_for", "migrations_for", "state_store"]),
7007        item("mysql", "storage", &["ddl_for", "migrations_for", "state_store"]),
7008        item("s3", "blobstore", &["put_json", "get_json", "content_address"]),
7009        item("docker", "sandbox", &["sandbox_policy", "sandbox_result", "tool_gateway"]),
7010        item("e2b", "sandbox", &["sandbox_policy", "sandbox_result", "tool_gateway"]),
7011        item("bubblewrap", "sandbox", &["sandbox_policy", "sandbox_result", "tool_gateway"]),
7012        item("kubernetes", "sandbox", &["sandbox_policy", "sandbox_result", "tool_gateway"]),
7013        item("gvisor", "sandbox", &["sandbox_policy", "sandbox_result", "tool_gateway"]),
7014        item("firecracker", "sandbox", &["sandbox_policy", "sandbox_result", "tool_gateway"]),
7015        item("langgraph", "framework", &["framework_adapter", "checkpoint_contract"]),
7016        item("langchain", "framework", &["framework_adapter"]),
7017        item("crewai", "framework", &["framework_adapter"]),
7018        item("autogen", "framework", &["framework_adapter"]),
7019        item("openai-agents-sdk", "framework", &["framework_adapter"]),
7020        item("llamaindex", "framework", &["framework_adapter"]),
7021        item("semantic-kernel", "framework", &["framework_adapter"]),
7022        item("mcp-transport", "mcp", &["mcp_tool_descriptor", "mcp_resource_descriptor"]),
7023        item("langfuse", "observability", &["evidence_bundle", "trace_payload", "correlation_ids"]),
7024        item("shadow-runner", "shadow", &["evidence_bundle", "tool_ledger", "state_diff"]),
7025    ]
7026}
7027
7028pub trait SqlExecutor {
7029    fn exec(&mut self, sql: &str, params: &[Value]) -> Result<()>;
7030}
7031
7032pub struct PostgresAdapter<C: SqlExecutor> {
7033    pub schema: String,
7034    pub client: C,
7035}
7036
7037impl<C: SqlExecutor> PostgresAdapter<C> {
7038    pub fn new(client: C, schema: &str) -> Self {
7039        Self { schema: if schema.is_empty() { "agentledger".to_string() } else { schema.to_string() }, client }
7040    }
7041    pub fn migration_plan(&self) -> Result<Vec<Migration>> { migrations_for("postgres") }
7042    pub fn apply_migrations(&mut self) -> Result<()> {
7043        self.client.exec(&ddl_for("postgres")?, &[])?;
7044        for migration in self.migration_plan()? {
7045            self.client.exec(
7046                "INSERT INTO schema_migrations(version, name, checksum) VALUES ($1, $2, $3) ON CONFLICT (version) DO NOTHING",
7047                &[Value::String(migration.version), Value::String(migration.name), Value::String(stable_hash(&migration.sql))],
7048            )?;
7049        }
7050        Ok(())
7051    }
7052}
7053
7054pub struct MySQLAdapter<C: SqlExecutor> {
7055    pub database: String,
7056    pub client: C,
7057}
7058
7059impl<C: SqlExecutor> MySQLAdapter<C> {
7060    pub fn new(client: C, database: &str) -> Self {
7061        Self { database: if database.is_empty() { "agentledger".to_string() } else { database.to_string() }, client }
7062    }
7063    pub fn migration_plan(&self) -> Result<Vec<Migration>> { migrations_for("mysql") }
7064    pub fn apply_migrations(&mut self) -> Result<()> {
7065        self.client.exec(&ddl_for("mysql")?, &[])?;
7066        for migration in self.migration_plan()? {
7067            self.client.exec(
7068                "INSERT INTO schema_migrations(version, name, checksum, applied_at) VALUES (?, ?, ?, UNIX_TIMESTAMP()) ON DUPLICATE KEY UPDATE version=version",
7069                &[Value::String(migration.version), Value::String(migration.name), Value::String(stable_hash(&migration.sql))],
7070            )?;
7071        }
7072        Ok(())
7073    }
7074}
7075
7076pub trait ObjectClient {
7077    fn put_object(&mut self, bucket: &str, key: &str, body: &[u8], content_type: &str, metadata: State) -> Result<()>;
7078    fn get_object(&mut self, bucket: &str, key: &str) -> Result<Vec<u8>>;
7079}
7080
7081pub struct S3BlobStoreAdapter<C: ObjectClient> {
7082    pub bucket: String,
7083    pub prefix: String,
7084    pub client: C,
7085}
7086
7087impl<C: ObjectClient> S3BlobStoreAdapter<C> {
7088    pub fn new(client: C, bucket: &str, prefix: &str) -> Self {
7089        Self { bucket: bucket.to_string(), prefix: if prefix.is_empty() { "agentledger/blobs".to_string() } else { prefix.trim_matches('/').to_string() }, client }
7090    }
7091    pub fn put_json(&mut self, value: &Value) -> Result<(String, String)> {
7092        let encoded = encode_value(value);
7093        let digest = stable_hash(&encoded);
7094        let key = format!("{}/sha256/{}.json", self.prefix, digest);
7095        let mut metadata = State::new();
7096        metadata.insert("agentledger-digest".to_string(), Value::String(format!("sha256:{digest}")));
7097        self.client.put_object(&self.bucket, &key, encoded.as_bytes(), "application/json", metadata)?;
7098        Ok((format!("sha256:{digest}"), format!("s3://{}/{}", self.bucket, key)))
7099    }
7100    pub fn get_json(&mut self, reference: &str) -> Result<Value> {
7101        let prefix = format!("s3://{}/", self.bucket);
7102        if !reference.starts_with(&prefix) || reference.contains("..") {
7103            return Err(RuntimeError(format!("unsupported s3 blob ref: {reference}")));
7104        }
7105        let key = &reference[prefix.len()..];
7106        let body = self.client.get_object(&self.bucket, key)?;
7107        let text = String::from_utf8(body).map_err(|err| RuntimeError(err.to_string()))?;
7108        decode_value(&text)
7109    }
7110}
7111
7112pub trait OtlpClient { fn post_json(&mut self, endpoint: &str, payload: &str, content_type: &str) -> Result<()>; }
7113pub struct OtlpTransport<C: OtlpClient> { pub endpoint: String, pub client: C }
7114impl<C: OtlpClient> OtlpTransport<C> { pub fn export(&mut self, payload: &str) -> Result<()> { self.client.post_json(&self.endpoint, payload, "application/json") } }
7115
7116pub struct DockerSandboxAdapter { pub image: String }
7117impl DockerSandboxAdapter {
7118    pub fn manifest(&self, policy: &State, command: Vec<String>) -> State {
7119        let mut out = State::new();
7120        out.insert("backend".to_string(), Value::String("docker".to_string()));
7121        out.insert("image".to_string(), Value::String(if self.image.is_empty() { "python:3.11-slim".to_string() } else { self.image.clone() }));
7122        let network = match policy.get("network") { Some(Value::String(value)) if value != "deny" => value.clone(), _ => "none".to_string() };
7123        out.insert("network".to_string(), Value::String(network));
7124        out.insert("read_only_root".to_string(), Value::Bool(true));
7125        out.insert("requires_explicit_execution".to_string(), Value::Bool(true));
7126        out.insert("command".to_string(), Value::Array(command.into_iter().map(Value::String).collect()));
7127        out
7128    }
7129}
7130
7131pub struct DockerSandboxExecutor {
7132    pub image: String,
7133    pub binary: String,
7134    pub allow_command_execution: bool,
7135    pub allow_shell: bool,
7136    pub shell: String,
7137    pub memory: String,
7138    pub cpus: String,
7139}
7140
7141impl DockerSandboxExecutor {
7142    pub fn new(image: &str, allow_command_execution: bool) -> Self {
7143        Self {
7144            image: image.to_string(),
7145            binary: "docker".to_string(),
7146            allow_command_execution,
7147            allow_shell: false,
7148            shell: "/bin/sh".to_string(),
7149            memory: String::new(),
7150            cpus: String::new(),
7151        }
7152    }
7153
7154    pub fn with_binary(mut self, binary: &str) -> Self {
7155        self.binary = binary.to_string();
7156        self
7157    }
7158
7159    fn extract_command(&self, args: &State) -> std::result::Result<Vec<String>, String> {
7160        let raw = args.get("_sandbox_command").or_else(|| args.get("command"));
7161        match raw {
7162            Some(Value::String(command)) => {
7163                if !self.allow_shell {
7164                    Err("string commands require allow_shell=true; pass argv list in `_sandbox_command` instead".to_string())
7165                } else {
7166                    let shell = if self.shell.is_empty() { "/bin/sh" } else { &self.shell };
7167                    Ok(vec![shell.to_string(), "-lc".to_string(), command.clone()])
7168                }
7169            }
7170            Some(Value::Array(items)) => {
7171                let mut command = Vec::new();
7172                for item in items {
7173                    match item {
7174                        Value::String(value) if !value.is_empty() => command.push(value.clone()),
7175                        _ => return Err("_sandbox_command must be a non-empty string array".to_string()),
7176                    }
7177                }
7178                if command.is_empty() {
7179                    Err("_sandbox_command must be a non-empty string array".to_string())
7180                } else {
7181                    Ok(command)
7182                }
7183            }
7184            _ => Err("external sandbox tools require a command-style `_sandbox_command` arg".to_string()),
7185        }
7186    }
7187
7188    fn docker_argv(&self, policy: &SandboxPolicy, command: &[String]) -> Vec<String> {
7189        let image = if self.image.is_empty() { "python:3.11-slim" } else { &self.image };
7190        let network = if policy.network == "deny" || policy.network.is_empty() { "none" } else { &policy.network };
7191        let mut argv = vec![
7192            self.binary.clone(),
7193            "run".to_string(),
7194            "--rm".to_string(),
7195            "--network".to_string(),
7196            network.to_string(),
7197            "--read-only".to_string(),
7198        ];
7199        if !self.memory.is_empty() {
7200            argv.extend(["--memory".to_string(), self.memory.clone()]);
7201        }
7202        if !self.cpus.is_empty() {
7203            argv.extend(["--cpus".to_string(), self.cpus.clone()]);
7204        }
7205        argv.push(image.to_string());
7206        argv.extend(command.iter().cloned());
7207        argv
7208    }
7209
7210    fn result_error(policy: &SandboxPolicy, manifest: State, error_type: &str, error: String) -> SandboxResult {
7211        let mut metadata = State::new();
7212        metadata.insert("executor".to_string(), Value::String(policy.executor.clone()));
7213        metadata.insert("isolation_level".to_string(), Value::String("container".to_string()));
7214        metadata.insert("manifest".to_string(), Value::Object(manifest));
7215        metadata.insert("error_type".to_string(), Value::String(error_type.to_string()));
7216        SandboxResult { ok: false, output: Value::Null, error: Some(error), metadata }
7217    }
7218}
7219
7220impl SandboxExecutor for DockerSandboxExecutor {
7221    fn run_tool(&self, args: State, policy: &SandboxPolicy) -> SandboxResult {
7222        let command = match self.extract_command(&args) {
7223            Ok(command) => command,
7224            Err(error) => return Self::result_error(policy, State::new(), "InvalidSandboxCommand", error),
7225        };
7226        let mut policy_state = State::new();
7227        policy_state.insert("network".to_string(), Value::String(policy.network.clone()));
7228        let manifest = (DockerSandboxAdapter { image: self.image.clone() }).manifest(&policy_state, command.clone());
7229        if !self.allow_command_execution {
7230            return Self::result_error(policy, manifest, "SandboxAdapterNotInstalled", "command execution is not enabled for this executor".to_string());
7231        }
7232        let argv = self.docker_argv(policy, &command);
7233        let mut cmd = Command::new(&argv[0]);
7234        cmd.args(&argv[1..]).stdout(Stdio::piped()).stderr(Stdio::piped());
7235        let output = match cmd.output() {
7236            Ok(output) => output,
7237            Err(error) => return Self::result_error(policy, manifest, "SandboxBinaryMissing", error.to_string()),
7238        };
7239        let mut value = State::new();
7240        value.insert("stdout".to_string(), Value::String(String::from_utf8_lossy(&output.stdout).to_string()));
7241        value.insert("stderr".to_string(), Value::String(String::from_utf8_lossy(&output.stderr).to_string()));
7242        value.insert("returncode".to_string(), Value::Number(output.status.code().unwrap_or(-1) as f64));
7243        let mut metadata = State::new();
7244        metadata.insert("executor".to_string(), Value::String(policy.executor.clone()));
7245        metadata.insert("isolation_level".to_string(), Value::String("container".to_string()));
7246        metadata.insert("manifest".to_string(), Value::Object(manifest));
7247        metadata.insert("executed".to_string(), Value::Bool(true));
7248        if !output.status.success() {
7249            metadata.insert("error_type".to_string(), Value::String("SandboxCommandFailed".to_string()));
7250            return SandboxResult {
7251                ok: false,
7252                output: Value::Object(value),
7253                error: Some(format!("sandbox command exited with {}", output.status.code().unwrap_or(-1))),
7254                metadata,
7255            };
7256        }
7257        SandboxResult { ok: true, output: Value::Object(value), error: None, metadata }
7258    }
7259}
7260
7261pub mod adapters {
7262    pub mod postgres {
7263        pub const PACKAGE_NAME: &str = "agentledger-postgres";
7264        pub const FEATURE: &str = "adapter-postgres";
7265        pub use crate::{migrations_for, Migration, PostgresAdapter, SqlExecutor};
7266    }
7267
7268    pub mod mysql {
7269        pub const PACKAGE_NAME: &str = "agentledger-mysql";
7270        pub const FEATURE: &str = "adapter-mysql";
7271        pub use crate::{migrations_for, Migration, MySQLAdapter, SqlExecutor};
7272    }
7273
7274    pub mod s3 {
7275        pub const PACKAGE_NAME: &str = "agentledger-s3";
7276        pub const FEATURE: &str = "adapter-s3";
7277        pub use crate::{ObjectClient, S3BlobStoreAdapter};
7278    }
7279
7280    pub mod mcp {
7281        pub const PACKAGE_NAME: &str = "agentledger-mcp";
7282        pub const FEATURE: &str = "adapter-mcp";
7283        pub use crate::{
7284            InMemoryMCPContextServer, InMemoryMCPToolServer, MCPCall, MCPContextAdapter,
7285            MCPResourceDescriptor, MCPResourceRead, MCPToolAdapter,
7286        };
7287    }
7288
7289    pub mod otel {
7290        pub const PACKAGE_NAME: &str = "agentledger-otel";
7291        pub const FEATURE: &str = "adapter-otel";
7292        pub use crate::{OtlpClient, OtlpTransport};
7293    }
7294
7295    pub mod langfuse {
7296        pub const PACKAGE_NAME: &str = "agentledger-langfuse";
7297        pub const FEATURE: &str = "adapter-langfuse";
7298        pub const CATEGORY: &str = "observability";
7299    }
7300
7301    pub mod docker {
7302        pub const PACKAGE_NAME: &str = "agentledger-sandbox-docker";
7303        pub const FEATURE: &str = "adapter-docker";
7304        pub use crate::{DockerSandboxAdapter, DockerSandboxExecutor, State, Value};
7305    }
7306
7307    pub mod framework {
7308        pub const PACKAGE_NAME: &str = "agentledger-framework";
7309        pub const FEATURE: &str = "adapter-framework";
7310        pub use crate::{FunctionAdapter, MethodFrameworkAdapter};
7311    }
7312}