pub struct MemoryStore { /* private fields */ }Implementations§
Source§impl MemoryStore
impl MemoryStore
pub fn new() -> Self
pub fn save_to_path(&self, path: impl AsRef<Path>) -> Result<()>
pub fn load_from_path(path: impl AsRef<Path>) -> Result<Self>
pub fn create_run(&mut self, initial_state: State) -> (String, String)
pub fn create_external_step(&mut self, run_id: &str) -> Result<Step>
Sourcepub fn claim_step(
&mut self,
worker_id: &str,
run_id: &str,
lease_seconds: f64,
) -> Result<StepClaim>
pub fn claim_step( &mut self, worker_id: &str, run_id: &str, lease_seconds: f64, ) -> Result<StepClaim>
Examples found in repository?
examples/travel_assistant.rs (line 293)
287fn claim_context(
288 runtime: &mut Runtime,
289 run_id: &str,
290 worker: &str,
291 role: &str,
292) -> AgentContext {
293 let claim = runtime.store.claim_step(worker, run_id, 60.0)
294 .expect("claim step failed");
295 let mut payload = State::new();
296 payload.insert("agent_role".to_string(), Value::String(role.to_string()));
297 payload.insert("attempt".to_string(), Value::Number(claim.attempt as f64));
298 runtime.store.append_event(
299 run_id,
300 Some(&claim.session_id),
301 Some(&claim.step_id),
302 "agent_started",
303 payload,
304 Some(role),
305 Some(claim.state_version),
306 None,
307 );
308 AgentContext {
309 run_id: claim.run_id.clone(),
310 session_id: claim.session_id,
311 step_id: claim.step_id.clone(),
312 agent_role: role.to_string(),
313 lease_token: claim.lease_token.clone(),
314 attempt: claim.attempt,
315 state_version: claim.state_version,
316 pending_patch: State::new(),
317 }
318}More examples
examples/three_minute_demo.rs (line 44)
20fn main() -> agentledger::Result<()> {
21 let mut runtime = Runtime::new();
22 runtime.register_tool(
23 ToolSpec::new(
24 "ticket.create",
25 Box::new(|args| {
26 Ok(Value::Object(state(&[
27 ("external_id", "TICKET-1".into()),
28 ("title", args["title"].clone()),
29 ])))
30 }),
31 )
32 .side_effect("external_write")
33 .idempotency_required(true)
34 .input_schema(Value::Object(state(&[
35 ("type", "object".into()),
36 (
37 "required",
38 Value::Array(vec![Value::String("title".to_string())]),
39 ),
40 ]))),
41 );
42 let (run_id, _) = runtime.create_run(State::new());
43
44 let first_claim = runtime.store.claim_step("worker-before-crash", &run_id, 60.0)?;
45 let first_ctx = AgentContext {
46 run_id: run_id.clone(),
47 session_id: first_claim.session_id.clone(),
48 step_id: first_claim.step_id.clone(),
49 agent_role: "SupportAgent".to_string(),
50 lease_token: first_claim.lease_token.clone(),
51 attempt: first_claim.attempt,
52 state_version: first_claim.state_version,
53 pending_patch: State::new(),
54 };
55 let ticket = runtime.call_tool(
56 &first_ctx,
57 "ticket.create",
58 state(&[
59 ("title", "Investigate failed payment".into()),
60 ("_logical_operation", "open-payment-ticket".into()),
61 ]),
62 )?;
63 runtime.store.mark_retry(
64 &run_id,
65 &first_ctx.step_id,
66 "RetryableAgentError",
67 "after external ticket create, before state commit",
68 );
69
70 let second_claim = runtime.store.claim_step("worker-after-restart", &run_id, 60.0)?;
71 let mut second_ctx = AgentContext {
72 run_id: run_id.clone(),
73 session_id: second_claim.session_id.clone(),
74 step_id: second_claim.step_id.clone(),
75 agent_role: "SupportAgent".to_string(),
76 lease_token: second_claim.lease_token.clone(),
77 attempt: second_claim.attempt,
78 state_version: second_claim.state_version,
79 pending_patch: State::new(),
80 };
81 let replayed_ticket = runtime.call_tool(
82 &second_ctx,
83 "ticket.create",
84 state(&[
85 ("title", "Investigate failed payment".into()),
86 ("_logical_operation", "open-payment-ticket".into()),
87 ]),
88 )?;
89 second_ctx.write_state("ticket", replayed_ticket);
90 second_ctx.write_state("recovered", true.into());
91 runtime.store.commit_state_patch(
92 &run_id,
93 &second_ctx.step_id,
94 &second_ctx.lease_token,
95 second_ctx.state_version,
96 second_ctx.pending_patch,
97 )?;
98
99 let summary = replay(&runtime.store, &run_id)?;
100 let ledger = runtime.store.ledger(&run_id);
101 let final_state = runtime.store.final_state(&run_id)?;
102 println!(
103 "{{\n \"run_id\": \"{}\",\n \"first_attempt_ok\": false,\n \"second_attempt_ok\": true,\n \"external_ticket_count\": 1,\n \"actual_tool_executions\": 1,\n \"tool_ledger_count\": {},\n \"tool_ledger_status\": \"{}\",\n \"first_ticket_external_id\": \"{}\",\n \"final_state_recovered\": {},\n \"replay\": {{\n \"safe\": {},\n \"event_count\": {},\n \"tool_call_count\": {}\n }}\n}}",
104 run_id,
105 ledger.len(),
106 ledger.first().map(|row| row.status.as_str()).unwrap_or(""),
107 get_string(&ticket, "external_id"),
108 final_state.get("recovered") == Some(&Value::Bool(true)),
109 summary.replay_safe,
110 summary.event_count,
111 summary.tool_call_count
112 );
113 Ok(())
114}examples/mcp_governance.rs (line 86)
33fn main() -> agentledger::Result<()> {
34 let mut runtime = Runtime::new();
35 runtime.set_sandbox(Box::new(DemoSandboxExecutor));
36 let mut server = InMemoryMCPToolServer::new();
37 server.add_tool(
38 state(&[
39 ("name", "mcp.github.create_pr".into()),
40 (
41 "inputSchema",
42 Value::Object(state(&[
43 ("type", "object".into()),
44 (
45 "required",
46 Value::Array(vec![Value::String("title".to_string())]),
47 ),
48 (
49 "properties",
50 Value::Object(state(&[(
51 "title",
52 Value::Object(state(&[
53 ("type", "string".into()),
54 ("minLength", 1_i64.into()),
55 ])),
56 )])),
57 ),
58 ])),
59 ),
60 (
61 "annotations",
62 Value::Object(state(&[
63 ("side_effect", "external_write".into()),
64 ("risk_level", "high".into()),
65 ("idempotency_required", true.into()),
66 ("approval_required", true.into()),
67 ("sandbox_required", true.into()),
68 (
69 "sandbox_policy",
70 Value::Object(state(&[
71 ("network", "deny".into()),
72 ("filesystem", "read-only".into()),
73 ])),
74 ),
75 ])),
76 ),
77 ]),
78 create_pr,
79 );
80 let adapter = MCPToolAdapter { client_call: create_pr };
81 for descriptor in server.list_tools() {
82 runtime.register_tool(adapter.tool_spec_from_descriptor(&descriptor));
83 }
84
85 let (run_id, _) = runtime.create_run(State::new());
86 let first_claim = runtime.store.claim_step("worker-before-approval", &run_id, 60.0)?;
87 let first_ctx = AgentContext {
88 run_id: run_id.clone(),
89 session_id: first_claim.session_id.clone(),
90 step_id: first_claim.step_id.clone(),
91 agent_role: "MCPAgent".to_string(),
92 lease_token: first_claim.lease_token.clone(),
93 attempt: first_claim.attempt,
94 state_version: first_claim.state_version,
95 pending_patch: State::new(),
96 };
97 let first = runtime.call_tool(
98 &first_ctx,
99 "mcp.github.create_pr",
100 state(&[
101 ("title", "Update runtime docs".into()),
102 ("_logical_operation", "docs-pr".into()),
103 ]),
104 );
105 let approval_id = first
106 .unwrap_err()
107 .0
108 .trim_start_matches("approval required:")
109 .to_string();
110 runtime.store.mark_waiting_human(
111 &run_id,
112 &first_ctx.step_id,
113 "approval required for tool mcp.github.create_pr",
114 &approval_id,
115 );
116 runtime
117 .store
118 .approve_request(&approval_id, "maintainer", "demo approval")?;
119
120 let second_claim = runtime.store.claim_step("worker-after-approval", &run_id, 60.0)?;
121 let mut second_ctx = AgentContext {
122 run_id: run_id.clone(),
123 session_id: second_claim.session_id.clone(),
124 step_id: second_claim.step_id.clone(),
125 agent_role: "MCPAgent".to_string(),
126 lease_token: second_claim.lease_token.clone(),
127 attempt: second_claim.attempt,
128 state_version: second_claim.state_version,
129 pending_patch: State::new(),
130 };
131 let result = runtime.call_tool(
132 &second_ctx,
133 "mcp.github.create_pr",
134 state(&[
135 ("title", "Update runtime docs".into()),
136 ("_logical_operation", "docs-pr".into()),
137 ]),
138 )?;
139 second_ctx.write_state("pull_request", result);
140 runtime.store.commit_state_patch(
141 &run_id,
142 &second_ctx.step_id,
143 &second_ctx.lease_token,
144 second_ctx.state_version,
145 second_ctx.pending_patch,
146 )?;
147
148 let approvals = runtime.store.approval_requests(&run_id);
149 println!(
150 "{{\n \"run_id\": \"{}\",\n \"first_attempt_waited_for_approval\": true,\n \"second_attempt_ok\": true,\n \"approval_count\": {},\n \"approval_status\": \"{}\",\n \"external_action_count\": 1,\n \"final_state_has_pull_request\": {}\n}}",
151 run_id,
152 approvals.len(),
153 approvals.first().map(|row| row.status.as_str()).unwrap_or(""),
154 runtime
155 .store
156 .final_state(&run_id)?
157 .contains_key("pull_request")
158 );
159 Ok(())
160}pub fn load_state(&self, run_id: &str) -> Result<(State, u64, String)>
pub fn recover_expired_leases(&mut self) -> usize
pub fn cancel_run(&mut self, run_id: &str, reason: &str) -> Result<usize>
Sourcepub fn commit_state_patch(
&mut self,
run_id: &str,
step_id: &str,
lease_token: &str,
base_version: u64,
patch: State,
) -> Result<u64>
pub fn commit_state_patch( &mut self, run_id: &str, step_id: &str, lease_token: &str, base_version: u64, patch: State, ) -> Result<u64>
Examples found in repository?
examples/three_minute_demo.rs (lines 91-97)
20fn main() -> agentledger::Result<()> {
21 let mut runtime = Runtime::new();
22 runtime.register_tool(
23 ToolSpec::new(
24 "ticket.create",
25 Box::new(|args| {
26 Ok(Value::Object(state(&[
27 ("external_id", "TICKET-1".into()),
28 ("title", args["title"].clone()),
29 ])))
30 }),
31 )
32 .side_effect("external_write")
33 .idempotency_required(true)
34 .input_schema(Value::Object(state(&[
35 ("type", "object".into()),
36 (
37 "required",
38 Value::Array(vec![Value::String("title".to_string())]),
39 ),
40 ]))),
41 );
42 let (run_id, _) = runtime.create_run(State::new());
43
44 let first_claim = runtime.store.claim_step("worker-before-crash", &run_id, 60.0)?;
45 let first_ctx = AgentContext {
46 run_id: run_id.clone(),
47 session_id: first_claim.session_id.clone(),
48 step_id: first_claim.step_id.clone(),
49 agent_role: "SupportAgent".to_string(),
50 lease_token: first_claim.lease_token.clone(),
51 attempt: first_claim.attempt,
52 state_version: first_claim.state_version,
53 pending_patch: State::new(),
54 };
55 let ticket = runtime.call_tool(
56 &first_ctx,
57 "ticket.create",
58 state(&[
59 ("title", "Investigate failed payment".into()),
60 ("_logical_operation", "open-payment-ticket".into()),
61 ]),
62 )?;
63 runtime.store.mark_retry(
64 &run_id,
65 &first_ctx.step_id,
66 "RetryableAgentError",
67 "after external ticket create, before state commit",
68 );
69
70 let second_claim = runtime.store.claim_step("worker-after-restart", &run_id, 60.0)?;
71 let mut second_ctx = AgentContext {
72 run_id: run_id.clone(),
73 session_id: second_claim.session_id.clone(),
74 step_id: second_claim.step_id.clone(),
75 agent_role: "SupportAgent".to_string(),
76 lease_token: second_claim.lease_token.clone(),
77 attempt: second_claim.attempt,
78 state_version: second_claim.state_version,
79 pending_patch: State::new(),
80 };
81 let replayed_ticket = runtime.call_tool(
82 &second_ctx,
83 "ticket.create",
84 state(&[
85 ("title", "Investigate failed payment".into()),
86 ("_logical_operation", "open-payment-ticket".into()),
87 ]),
88 )?;
89 second_ctx.write_state("ticket", replayed_ticket);
90 second_ctx.write_state("recovered", true.into());
91 runtime.store.commit_state_patch(
92 &run_id,
93 &second_ctx.step_id,
94 &second_ctx.lease_token,
95 second_ctx.state_version,
96 second_ctx.pending_patch,
97 )?;
98
99 let summary = replay(&runtime.store, &run_id)?;
100 let ledger = runtime.store.ledger(&run_id);
101 let final_state = runtime.store.final_state(&run_id)?;
102 println!(
103 "{{\n \"run_id\": \"{}\",\n \"first_attempt_ok\": false,\n \"second_attempt_ok\": true,\n \"external_ticket_count\": 1,\n \"actual_tool_executions\": 1,\n \"tool_ledger_count\": {},\n \"tool_ledger_status\": \"{}\",\n \"first_ticket_external_id\": \"{}\",\n \"final_state_recovered\": {},\n \"replay\": {{\n \"safe\": {},\n \"event_count\": {},\n \"tool_call_count\": {}\n }}\n}}",
104 run_id,
105 ledger.len(),
106 ledger.first().map(|row| row.status.as_str()).unwrap_or(""),
107 get_string(&ticket, "external_id"),
108 final_state.get("recovered") == Some(&Value::Bool(true)),
109 summary.replay_safe,
110 summary.event_count,
111 summary.tool_call_count
112 );
113 Ok(())
114}More examples
examples/mcp_governance.rs (lines 140-146)
33fn main() -> agentledger::Result<()> {
34 let mut runtime = Runtime::new();
35 runtime.set_sandbox(Box::new(DemoSandboxExecutor));
36 let mut server = InMemoryMCPToolServer::new();
37 server.add_tool(
38 state(&[
39 ("name", "mcp.github.create_pr".into()),
40 (
41 "inputSchema",
42 Value::Object(state(&[
43 ("type", "object".into()),
44 (
45 "required",
46 Value::Array(vec![Value::String("title".to_string())]),
47 ),
48 (
49 "properties",
50 Value::Object(state(&[(
51 "title",
52 Value::Object(state(&[
53 ("type", "string".into()),
54 ("minLength", 1_i64.into()),
55 ])),
56 )])),
57 ),
58 ])),
59 ),
60 (
61 "annotations",
62 Value::Object(state(&[
63 ("side_effect", "external_write".into()),
64 ("risk_level", "high".into()),
65 ("idempotency_required", true.into()),
66 ("approval_required", true.into()),
67 ("sandbox_required", true.into()),
68 (
69 "sandbox_policy",
70 Value::Object(state(&[
71 ("network", "deny".into()),
72 ("filesystem", "read-only".into()),
73 ])),
74 ),
75 ])),
76 ),
77 ]),
78 create_pr,
79 );
80 let adapter = MCPToolAdapter { client_call: create_pr };
81 for descriptor in server.list_tools() {
82 runtime.register_tool(adapter.tool_spec_from_descriptor(&descriptor));
83 }
84
85 let (run_id, _) = runtime.create_run(State::new());
86 let first_claim = runtime.store.claim_step("worker-before-approval", &run_id, 60.0)?;
87 let first_ctx = AgentContext {
88 run_id: run_id.clone(),
89 session_id: first_claim.session_id.clone(),
90 step_id: first_claim.step_id.clone(),
91 agent_role: "MCPAgent".to_string(),
92 lease_token: first_claim.lease_token.clone(),
93 attempt: first_claim.attempt,
94 state_version: first_claim.state_version,
95 pending_patch: State::new(),
96 };
97 let first = runtime.call_tool(
98 &first_ctx,
99 "mcp.github.create_pr",
100 state(&[
101 ("title", "Update runtime docs".into()),
102 ("_logical_operation", "docs-pr".into()),
103 ]),
104 );
105 let approval_id = first
106 .unwrap_err()
107 .0
108 .trim_start_matches("approval required:")
109 .to_string();
110 runtime.store.mark_waiting_human(
111 &run_id,
112 &first_ctx.step_id,
113 "approval required for tool mcp.github.create_pr",
114 &approval_id,
115 );
116 runtime
117 .store
118 .approve_request(&approval_id, "maintainer", "demo approval")?;
119
120 let second_claim = runtime.store.claim_step("worker-after-approval", &run_id, 60.0)?;
121 let mut second_ctx = AgentContext {
122 run_id: run_id.clone(),
123 session_id: second_claim.session_id.clone(),
124 step_id: second_claim.step_id.clone(),
125 agent_role: "MCPAgent".to_string(),
126 lease_token: second_claim.lease_token.clone(),
127 attempt: second_claim.attempt,
128 state_version: second_claim.state_version,
129 pending_patch: State::new(),
130 };
131 let result = runtime.call_tool(
132 &second_ctx,
133 "mcp.github.create_pr",
134 state(&[
135 ("title", "Update runtime docs".into()),
136 ("_logical_operation", "docs-pr".into()),
137 ]),
138 )?;
139 second_ctx.write_state("pull_request", result);
140 runtime.store.commit_state_patch(
141 &run_id,
142 &second_ctx.step_id,
143 &second_ctx.lease_token,
144 second_ctx.state_version,
145 second_ctx.pending_patch,
146 )?;
147
148 let approvals = runtime.store.approval_requests(&run_id);
149 println!(
150 "{{\n \"run_id\": \"{}\",\n \"first_attempt_waited_for_approval\": true,\n \"second_attempt_ok\": true,\n \"approval_count\": {},\n \"approval_status\": \"{}\",\n \"external_action_count\": 1,\n \"final_state_has_pull_request\": {}\n}}",
151 run_id,
152 approvals.len(),
153 approvals.first().map(|row| row.status.as_str()).unwrap_or(""),
154 runtime
155 .store
156 .final_state(&run_id)?
157 .contains_key("pull_request")
158 );
159 Ok(())
160}examples/travel_assistant.rs (lines 824-830)
425fn main() -> Result<(), Box<dyn std::error::Error>> {
426 let args: Vec<String> = std::env::args().collect();
427 let root = if args.len() > 1 {
428 PathBuf::from(&args[1])
429 } else {
430 std::env::temp_dir().join(format!("agentledger-rust-{}", std::process::id()))
431 };
432 let _ = fs::create_dir_all(&root);
433
434 // Intro
435 println!(
436 "\n{}{} ╔════════════════════════════════════════════════════╗{}",
437 C_BOLD, C_C, C_RST
438 );
439 println!(
440 "{}{} ║ AgentLedger Travel Assistant (Rust) — Interactive Demo ║{}",
441 C_BOLD, C_C, C_RST
442 );
443 println!(
444 "{}{} ║ See real database state at every step ║{}",
445 C_BOLD, C_C, C_RST
446 );
447 println!(
448 "{}{} ╚════════════════════════════════════════════════════╝{}",
449 C_BOLD, C_C, C_RST
450 );
451
452 println!(
453 "\n {}AgentLedger — Durable Execution Runtime for AI Agents{}",
454 C_DIM, C_RST
455 );
456 println!(" ┌────────────────────────────────────────────────────┐");
457 println!(
458 " │ {}✓{} Durable execution — crash recovery │",
459 C_G, C_RST
460 );
461 println!(
462 " │ {}✓{} Tool Ledger — idempotent replay │",
463 C_G, C_RST
464 );
465 println!(
466 " │ {}✓{} Approval gates — human-in-the-loop │",
467 C_G, C_RST
468 );
469 println!(
470 " │ {}✓{} Policy engine — risk-based access │",
471 C_G, C_RST
472 );
473 println!(
474 " │ {}✓{} Budget control — tool call limits │",
475 C_G, C_RST
476 );
477 println!(
478 " │ {}✓{} Evidence export — full audit trail │",
479 C_G, C_RST
480 );
481 println!(" └────────────────────────────────────────────────────┘");
482
483 wait("Press Enter to start / 按 Enter 开始");
484
485 // ════════════════════════════════════════════════════════
486 // Step 1: Setup
487 // ════════════════════════════════════════════════════════
488 println!(
489 "\n{}{}{}",
490 C_BOLD,
491 C_B,
492 "═".repeat(60)
493 );
494 println!(
495 "{}{} Step 1: Initialize — Register tools, configure policy{}",
496 C_BOLD, C_B, C_RST
497 );
498 println!(
499 "{}{}{}",
500 C_BOLD,
501 C_B,
502 "═".repeat(60)
503 );
504
505 let mut runtime = Runtime::new();
506 runtime.set_budget(BudgetLimits {
507 max_tool_calls: Some(25.0),
508 max_model_tokens: None,
509 max_total_usd: None,
510 });
511
512 // Register tools
513 runtime.register_tool(
514 ToolSpec::new("travel.search_flights", Box::new(search_flights))
515 .side_effect("none")
516 .risk_level("low")
517 .input_schema(Value::Object(make_state(&[
518 ("type", "object".into()),
519 (
520 "required",
521 Value::Array(vec!["from".into(), "to".into()]),
522 ),
523 ]))),
524 );
525 runtime.register_tool(
526 ToolSpec::new("travel.search_hotels", Box::new(search_hotels))
527 .side_effect("none")
528 .risk_level("low")
529 .input_schema(Value::Object(make_state(&[
530 ("type", "object".into()),
531 ("required", Value::Array(vec!["city".into()])),
532 ]))),
533 );
534 runtime.register_tool(
535 ToolSpec::new("travel.check_weather", Box::new(check_weather))
536 .side_effect("none")
537 .risk_level("low")
538 .input_schema(Value::Object(make_state(&[
539 ("type", "object".into()),
540 ("required", Value::Array(vec!["city".into()])),
541 ]))),
542 );
543 runtime.register_tool(
544 ToolSpec::new("travel.book_flight", Box::new(book_flight))
545 .side_effect("external_write")
546 .risk_level("high")
547 .idempotency_required(true)
548 .approval_required(true)
549 .input_schema(Value::Object(make_state(&[
550 ("type", "object".into()),
551 (
552 "required",
553 Value::Array(vec!["flight_id".into(), "passenger".into()]),
554 ),
555 ]))),
556 );
557 runtime.register_tool(
558 ToolSpec::new("travel.book_hotel", Box::new(book_hotel))
559 .side_effect("external_write")
560 .risk_level("high")
561 .idempotency_required(true)
562 .approval_required(true)
563 .input_schema(Value::Object(make_state(&[
564 ("type", "object".into()),
565 (
566 "required",
567 Value::Array(vec!["hotel_id".into(), "guest".into()]),
568 ),
569 ]))),
570 );
571
572 println!("\n {}Registered 5 tools:{}", C_C, C_RST);
573 for (name, risk, approval) in [
574 ("travel.search_flights", "low", false),
575 ("travel.search_hotels", "low", false),
576 ("travel.check_weather", "low", false),
577 ("travel.book_flight", "high", true),
578 ("travel.book_hotel", "high", true),
579 ] {
580 let rc = if risk == "low" { C_G } else { C_R };
581 let ac = if approval {
582 format!("{}needs approval{}", C_R, C_RST)
583 } else {
584 format!("{}no approval{}", C_G, C_RST)
585 };
586 println!(
587 " {}•{} {} [{}{}{}] [{}]",
588 C_DIM, C_RST, name, rc, risk, C_RST, ac
589 );
590 }
591 println!(
592 " {}Policy: Rust uses risk-based policy (low=allow, high=deny+approval){}",
593 C_DIM, C_RST
594 );
595 println!(" {}Budget: max 25 tool calls{}", C_DIM, C_RST);
596
597 let (run_id, _step_id) = runtime.create_run(make_state(&[
598 ("trip", "Tokyo".into()),
599 ("budget_usd", Value::Number(3000.0)),
600 ]));
601 println!(
602 "\n {}Run created: {}{}{}",
603 C_B, C_BOLD, run_id, C_RST
604 );
605 show_db(&runtime.store, &run_id);
606 wait("Press Enter to continue");
607
608 // ════════════════════════════════════════════════════════
609 // Step 2: Attempt 1 — Approval interception
610 // ════════════════════════════════════════════════════════
611 println!(
612 "\n{}{}{}",
613 C_BOLD,
614 C_R,
615 "═".repeat(60)
616 );
617 println!(
618 "{}{} Step 2: Attempt 1 — Agent runs → Approval triggered{}",
619 C_BOLD, C_R, C_RST
620 );
621 println!(
622 "{}{}{}",
623 C_BOLD,
624 C_R,
625 "═".repeat(60)
626 );
627 println!(
628 "\n {}Agent executing: search flights → search hotels → check weather → book flight...{}",
629 C_DIM, C_RST
630 );
631
632 {
633 let mut ctx = claim_context(&mut runtime, &run_id, "worker-rust", "TravelPlanner");
634 let result = travel_planner(&mut runtime, &mut ctx, 1);
635 match result {
636 Err(err) if err.0.starts_with("approval required:") => {
637 let approval_id = err.0.trim_start_matches("approval required:");
638 runtime
639 .store
640 .mark_waiting_human(&run_id, &ctx.step_id, &err.0, approval_id);
641 }
642 Err(err) => return Err(format!("unexpected error at step 2: {}", err.0).into()),
643 Ok(()) => {}
644 }
645 }
646
647 println!(
648 "\n {}book_flight triggered approval! Runtime paused, waiting for human.{}",
649 C_R, C_RST
650 );
651 show_db(&runtime.store, &run_id);
652 println!(
653 " {}Note: Tool Ledger has RESERVED entry, approval status is PENDING{}",
654 C_R, C_RST
655 );
656 wait("Press Enter to approve / 按 Enter 审批");
657
658 for req in runtime.store.approval_requests(&run_id) {
659 if req.status == "PENDING" {
660 runtime
661 .store
662 .approve_request(&req.approval_id, "traveler", "Within budget, approved")?;
663 println!(
664 "\n {}✅ Approved: {} — by traveler{}",
665 C_G, req.tool_name, C_RST
666 );
667 }
668 }
669 show_db(&runtime.store, &run_id);
670 wait("Press Enter to continue");
671
672 // ════════════════════════════════════════════════════════
673 // Step 3: Attempt 2 — Execute + Crash
674 // ════════════════════════════════════════════════════════
675 println!(
676 "\n{}{}{}",
677 C_BOLD,
678 C_Y,
679 "═".repeat(60)
680 );
681 println!(
682 "{}{} Step 3: Attempt 2 — Approved → Execute booking → Simulated crash{}",
683 C_BOLD, C_Y, C_RST
684 );
685 println!(
686 "{}{}{}",
687 C_BOLD,
688 C_Y,
689 "═".repeat(60)
690 );
691 println!(
692 "\n {}Re-running agent (approval passed, book_flight will execute)...{}",
693 C_DIM, C_RST
694 );
695
696 {
697 let mut ctx = claim_context(&mut runtime, &run_id, "worker-rust", "TravelPlanner");
698 let result = travel_planner(&mut runtime, &mut ctx, 2);
699 match result {
700 Err(err) if err.0 == "retryable" => {
701 runtime.store.mark_retry(
702 &run_id,
703 &ctx.step_id,
704 "RetryableAgentError",
705 "after flight booking",
706 );
707 }
708 Err(err) if err.0.starts_with("approval required:") => {
709 let approval_id = err.0.trim_start_matches("approval required:");
710 runtime
711 .store
712 .mark_waiting_human(&run_id, &ctx.step_id, &err.0, approval_id);
713 }
714 Err(err) => return Err(format!("unexpected error at step 3: {}", err.0).into()),
715 Ok(()) => {}
716 }
717 }
718
719 println!(
720 "\n {}Agent booked flight, then crashed before committing state!{}",
721 C_Y, C_RST
722 );
723 println!(
724 " {}Flight is booked in external system, but agent state was NOT persisted.{}",
725 C_Y, C_RST
726 );
727 show_db(&runtime.store, &run_id);
728 println!(
729 " {}Key: Tool Ledger book_flight status = {}SUCCEEDED{} (external side effect executed){}",
730 C_Y, C_G, C_Y, C_RST
731 );
732 println!(
733 " {} Step status = retry_scheduled (state not committed, waiting for retry){}",
734 C_Y, C_RST
735 );
736 wait("Press Enter to continue");
737
738 // ════════════════════════════════════════════════════════
739 // Step 4: Attempt 3 — Recovery + Hotel approval
740 // ════════════════════════════════════════════════════════
741 println!(
742 "\n{}{}{}",
743 C_BOLD,
744 C_G,
745 "═".repeat(60)
746 );
747 println!(
748 "{}{} Step 4: Attempt 3 — Crash recovery → Tool Ledger idempotent replay{}",
749 C_BOLD, C_G, C_RST
750 );
751 println!(
752 "{}{}{}",
753 C_BOLD,
754 C_G,
755 "═".repeat(60)
756 );
757 println!(
758 "\n {}Agent re-executes. book_flight: Tool Ledger sees SUCCEEDED record...{}",
759 C_DIM, C_RST
760 );
761 println!(
762 " {}{}→ Returns cached result, no duplicate API call, no double charge!{}",
763 C_DIM, C_G, C_RST
764 );
765
766 {
767 let mut ctx = claim_context(&mut runtime, &run_id, "worker-rust", "TravelPlanner");
768 let result = travel_planner(&mut runtime, &mut ctx, 3);
769 match result {
770 Err(err) if err.0.starts_with("approval required:") => {
771 let approval_id = err.0.trim_start_matches("approval required:");
772 runtime
773 .store
774 .mark_waiting_human(&run_id, &ctx.step_id, &err.0, approval_id);
775 }
776 Err(err) => return Err(format!("unexpected error at step 4: {}", err.0).into()),
777 Ok(()) => {}
778 }
779 }
780
781 println!(
782 "\n {}✅ Flight idempotent replay successful! (no duplicate _book_flight call){}",
783 C_G, C_RST
784 );
785 println!(" {}Hotel booking → triggers approval again{}", C_R, C_RST);
786
787 for req in runtime.store.approval_requests(&run_id) {
788 if req.status == "PENDING" {
789 runtime
790 .store
791 .approve_request(&req.approval_id, "traveler", "Hotel within budget, approved")?;
792 println!(
793 "\n {}✅ Approved: {} — by traveler{}",
794 C_G, req.tool_name, C_RST
795 );
796 }
797 }
798 show_db(&runtime.store, &run_id);
799 wait("Press Enter to continue");
800
801 // ════════════════════════════════════════════════════════
802 // Step 5: Attempt 4 — Complete
803 // ════════════════════════════════════════════════════════
804 println!(
805 "\n{}{}{}",
806 C_BOLD,
807 C_G,
808 "═".repeat(60)
809 );
810 println!(
811 "{}{} Step 5: Attempt 4 — Hotel approved → Full execution → State committed{}",
812 C_BOLD, C_G, C_RST
813 );
814 println!(
815 "{}{}{}",
816 C_BOLD,
817 C_G,
818 "═".repeat(60)
819 );
820
821 {
822 let mut ctx = claim_context(&mut runtime, &run_id, "worker-rust", "TravelPlanner");
823 travel_planner(&mut runtime, &mut ctx, 4)?;
824 runtime.store.commit_state_patch(
825 &run_id,
826 &ctx.step_id,
827 &ctx.lease_token,
828 ctx.state_version,
829 ctx.pending_patch,
830 )?;
831 }
832
833 {
834 let db = BOOKING_DB.lock().unwrap();
835 if db.len() != 2 {
836 return Err(format!("Expected 2 bookings, got {}", db.len()).into());
837 }
838 }
839
840 println!(
841 "\n {}✅ Travel planning complete! State persisted to database.{}",
842 C_G, C_RST
843 );
844 show_db(&runtime.store, &run_id);
845 println!(
846 " {}Step status = completed, State has bookings + trip_status{}",
847 C_G, C_RST
848 );
849 let db = BOOKING_DB.lock().unwrap();
850 let keys: Vec<String> = db.keys().cloned().collect();
851 println!(
852 " {}External bookings: {:?} ({} total, no duplicates){}",
853 C_G,
854 keys,
855 keys.len(),
856 C_RST
857 );
858 drop(db);
859 wait("Press Enter to continue");
860
861 // ════════════════════════════════════════════════════════
862 // Step 6: Evidence + Cost + Replay
863 // ════════════════════════════════════════════════════════
864 println!(
865 "\n{}{}{}",
866 C_BOLD,
867 C_M,
868 "═".repeat(60)
869 );
870 println!(
871 "{}{} Step 6: Evidence export + Cost attribution + Replay verification{}",
872 C_BOLD, C_M, C_RST
873 );
874 println!(
875 "{}{}{}",
876 C_BOLD,
877 C_M,
878 "═".repeat(60)
879 );
880
881 let bundle = export_evidence(&runtime.store, &run_id)?;
882 let replay_result = replay(&runtime.store, &run_id)?;
883 let cost = agentledger::cost_attribution(&runtime.store, &run_id);
884
885 println!(
886 "\n {}Cost attribution: {} tool calls{}",
887 C_M, cost.total.tool_calls, C_RST
888 );
889 println!(
890 " {}Replay: {} events, safe={}{}{}",
891 C_M, replay_result.event_count, C_G, replay_result.replay_safe, C_RST
892 );
893 println!(
894 " {}Evidence bundle: {} events total{}",
895 C_M,
896 bundle.events.len(),
897 C_RST
898 );
899
900 // Write evidence HTML
901 let html_path = root.join("evidence.html");
902 let html = debug_html(&bundle);
903 fs::write(&html_path, html)?;
904 let html_abs = html_path.canonicalize().unwrap_or_else(|_| html_path.clone());
905
906 // Final summary
907 println!(
908 "\n{}{}{}",
909 C_BOLD,
910 C_G,
911 "═".repeat(60)
912 );
913 println!(
914 "{}{} Summary: What AgentLedger (Rust) did in this demo{}",
915 C_BOLD, C_G, C_RST
916 );
917 println!(
918 "{}{}{}",
919 C_BOLD,
920 C_G,
921 "═".repeat(60)
922 );
923 println!(
924 "
925 ┌──────────────────────────────────────────────────────────┐
926 │ │
927 │ {g}✓ Durable execution{rst} Crash → auto retry, state preserved │
928 │ Step: retry_scheduled → completed │
929 │ │
930 │ {g}✓ Tool Ledger{rst} Idempotent replay, flight booked {bold}1x{rst} only │
931 │ SUCCEEDED → cached result on retry │
932 │ │
933 │ {g}✓ Approval gates{rst} Flight + hotel each trigger approval │
934 │ approval_requests records in store │
935 │ │
936 │ {g}✓ Policy engine{rst} Risk-based policy (high → deny+approval) │
937 │ low-risk tools auto-allowed │
938 │ │
939 │ {g}✓ Budget control{rst} Tracked {tc} tool calls │
940 │ BudgetController.before_tool_call() │
941 │ │
942 │ {g}✓ Evidence export{rst} {ec} events recorded │
943 │ events stored in memory store │
944 │ │
945 │ {g}✓ Cost attribution{rst} Auto-recorded per run │
946 │ CostAttribution by agent │
947 │ │
948 │ {g}✓ Replay engine{rst} Event hash verification passed │
949 │ Verify history without re-running │
950 │ │
951 └──────────────────────────────────────────────────────────┘
952",
953 g = C_G,
954 rst = C_RST,
955 bold = C_BOLD,
956 tc = cost.total.tool_calls,
957 ec = bundle.events.len()
958 );
959
960 println!(" {}Storage: in-memory (MemoryStore){}", C_DIM, C_RST);
961 println!(" {}Evidence HTML: {:?}{}", C_DIM, html_abs, C_RST);
962 println!();
963
964 Ok(())
965}pub fn apply_system_state_patch( &mut self, run_id: &str, patch: State, reason: &str, ) -> Result<u64>
Sourcepub fn mark_retry(
&mut self,
run_id: &str,
step_id: &str,
error_type: &str,
message: &str,
)
pub fn mark_retry( &mut self, run_id: &str, step_id: &str, error_type: &str, message: &str, )
Examples found in repository?
examples/three_minute_demo.rs (lines 63-68)
20fn main() -> agentledger::Result<()> {
21 let mut runtime = Runtime::new();
22 runtime.register_tool(
23 ToolSpec::new(
24 "ticket.create",
25 Box::new(|args| {
26 Ok(Value::Object(state(&[
27 ("external_id", "TICKET-1".into()),
28 ("title", args["title"].clone()),
29 ])))
30 }),
31 )
32 .side_effect("external_write")
33 .idempotency_required(true)
34 .input_schema(Value::Object(state(&[
35 ("type", "object".into()),
36 (
37 "required",
38 Value::Array(vec![Value::String("title".to_string())]),
39 ),
40 ]))),
41 );
42 let (run_id, _) = runtime.create_run(State::new());
43
44 let first_claim = runtime.store.claim_step("worker-before-crash", &run_id, 60.0)?;
45 let first_ctx = AgentContext {
46 run_id: run_id.clone(),
47 session_id: first_claim.session_id.clone(),
48 step_id: first_claim.step_id.clone(),
49 agent_role: "SupportAgent".to_string(),
50 lease_token: first_claim.lease_token.clone(),
51 attempt: first_claim.attempt,
52 state_version: first_claim.state_version,
53 pending_patch: State::new(),
54 };
55 let ticket = runtime.call_tool(
56 &first_ctx,
57 "ticket.create",
58 state(&[
59 ("title", "Investigate failed payment".into()),
60 ("_logical_operation", "open-payment-ticket".into()),
61 ]),
62 )?;
63 runtime.store.mark_retry(
64 &run_id,
65 &first_ctx.step_id,
66 "RetryableAgentError",
67 "after external ticket create, before state commit",
68 );
69
70 let second_claim = runtime.store.claim_step("worker-after-restart", &run_id, 60.0)?;
71 let mut second_ctx = AgentContext {
72 run_id: run_id.clone(),
73 session_id: second_claim.session_id.clone(),
74 step_id: second_claim.step_id.clone(),
75 agent_role: "SupportAgent".to_string(),
76 lease_token: second_claim.lease_token.clone(),
77 attempt: second_claim.attempt,
78 state_version: second_claim.state_version,
79 pending_patch: State::new(),
80 };
81 let replayed_ticket = runtime.call_tool(
82 &second_ctx,
83 "ticket.create",
84 state(&[
85 ("title", "Investigate failed payment".into()),
86 ("_logical_operation", "open-payment-ticket".into()),
87 ]),
88 )?;
89 second_ctx.write_state("ticket", replayed_ticket);
90 second_ctx.write_state("recovered", true.into());
91 runtime.store.commit_state_patch(
92 &run_id,
93 &second_ctx.step_id,
94 &second_ctx.lease_token,
95 second_ctx.state_version,
96 second_ctx.pending_patch,
97 )?;
98
99 let summary = replay(&runtime.store, &run_id)?;
100 let ledger = runtime.store.ledger(&run_id);
101 let final_state = runtime.store.final_state(&run_id)?;
102 println!(
103 "{{\n \"run_id\": \"{}\",\n \"first_attempt_ok\": false,\n \"second_attempt_ok\": true,\n \"external_ticket_count\": 1,\n \"actual_tool_executions\": 1,\n \"tool_ledger_count\": {},\n \"tool_ledger_status\": \"{}\",\n \"first_ticket_external_id\": \"{}\",\n \"final_state_recovered\": {},\n \"replay\": {{\n \"safe\": {},\n \"event_count\": {},\n \"tool_call_count\": {}\n }}\n}}",
104 run_id,
105 ledger.len(),
106 ledger.first().map(|row| row.status.as_str()).unwrap_or(""),
107 get_string(&ticket, "external_id"),
108 final_state.get("recovered") == Some(&Value::Bool(true)),
109 summary.replay_safe,
110 summary.event_count,
111 summary.tool_call_count
112 );
113 Ok(())
114}More examples
examples/travel_assistant.rs (lines 701-706)
425fn main() -> Result<(), Box<dyn std::error::Error>> {
426 let args: Vec<String> = std::env::args().collect();
427 let root = if args.len() > 1 {
428 PathBuf::from(&args[1])
429 } else {
430 std::env::temp_dir().join(format!("agentledger-rust-{}", std::process::id()))
431 };
432 let _ = fs::create_dir_all(&root);
433
434 // Intro
435 println!(
436 "\n{}{} ╔════════════════════════════════════════════════════╗{}",
437 C_BOLD, C_C, C_RST
438 );
439 println!(
440 "{}{} ║ AgentLedger Travel Assistant (Rust) — Interactive Demo ║{}",
441 C_BOLD, C_C, C_RST
442 );
443 println!(
444 "{}{} ║ See real database state at every step ║{}",
445 C_BOLD, C_C, C_RST
446 );
447 println!(
448 "{}{} ╚════════════════════════════════════════════════════╝{}",
449 C_BOLD, C_C, C_RST
450 );
451
452 println!(
453 "\n {}AgentLedger — Durable Execution Runtime for AI Agents{}",
454 C_DIM, C_RST
455 );
456 println!(" ┌────────────────────────────────────────────────────┐");
457 println!(
458 " │ {}✓{} Durable execution — crash recovery │",
459 C_G, C_RST
460 );
461 println!(
462 " │ {}✓{} Tool Ledger — idempotent replay │",
463 C_G, C_RST
464 );
465 println!(
466 " │ {}✓{} Approval gates — human-in-the-loop │",
467 C_G, C_RST
468 );
469 println!(
470 " │ {}✓{} Policy engine — risk-based access │",
471 C_G, C_RST
472 );
473 println!(
474 " │ {}✓{} Budget control — tool call limits │",
475 C_G, C_RST
476 );
477 println!(
478 " │ {}✓{} Evidence export — full audit trail │",
479 C_G, C_RST
480 );
481 println!(" └────────────────────────────────────────────────────┘");
482
483 wait("Press Enter to start / 按 Enter 开始");
484
485 // ════════════════════════════════════════════════════════
486 // Step 1: Setup
487 // ════════════════════════════════════════════════════════
488 println!(
489 "\n{}{}{}",
490 C_BOLD,
491 C_B,
492 "═".repeat(60)
493 );
494 println!(
495 "{}{} Step 1: Initialize — Register tools, configure policy{}",
496 C_BOLD, C_B, C_RST
497 );
498 println!(
499 "{}{}{}",
500 C_BOLD,
501 C_B,
502 "═".repeat(60)
503 );
504
505 let mut runtime = Runtime::new();
506 runtime.set_budget(BudgetLimits {
507 max_tool_calls: Some(25.0),
508 max_model_tokens: None,
509 max_total_usd: None,
510 });
511
512 // Register tools
513 runtime.register_tool(
514 ToolSpec::new("travel.search_flights", Box::new(search_flights))
515 .side_effect("none")
516 .risk_level("low")
517 .input_schema(Value::Object(make_state(&[
518 ("type", "object".into()),
519 (
520 "required",
521 Value::Array(vec!["from".into(), "to".into()]),
522 ),
523 ]))),
524 );
525 runtime.register_tool(
526 ToolSpec::new("travel.search_hotels", Box::new(search_hotels))
527 .side_effect("none")
528 .risk_level("low")
529 .input_schema(Value::Object(make_state(&[
530 ("type", "object".into()),
531 ("required", Value::Array(vec!["city".into()])),
532 ]))),
533 );
534 runtime.register_tool(
535 ToolSpec::new("travel.check_weather", Box::new(check_weather))
536 .side_effect("none")
537 .risk_level("low")
538 .input_schema(Value::Object(make_state(&[
539 ("type", "object".into()),
540 ("required", Value::Array(vec!["city".into()])),
541 ]))),
542 );
543 runtime.register_tool(
544 ToolSpec::new("travel.book_flight", Box::new(book_flight))
545 .side_effect("external_write")
546 .risk_level("high")
547 .idempotency_required(true)
548 .approval_required(true)
549 .input_schema(Value::Object(make_state(&[
550 ("type", "object".into()),
551 (
552 "required",
553 Value::Array(vec!["flight_id".into(), "passenger".into()]),
554 ),
555 ]))),
556 );
557 runtime.register_tool(
558 ToolSpec::new("travel.book_hotel", Box::new(book_hotel))
559 .side_effect("external_write")
560 .risk_level("high")
561 .idempotency_required(true)
562 .approval_required(true)
563 .input_schema(Value::Object(make_state(&[
564 ("type", "object".into()),
565 (
566 "required",
567 Value::Array(vec!["hotel_id".into(), "guest".into()]),
568 ),
569 ]))),
570 );
571
572 println!("\n {}Registered 5 tools:{}", C_C, C_RST);
573 for (name, risk, approval) in [
574 ("travel.search_flights", "low", false),
575 ("travel.search_hotels", "low", false),
576 ("travel.check_weather", "low", false),
577 ("travel.book_flight", "high", true),
578 ("travel.book_hotel", "high", true),
579 ] {
580 let rc = if risk == "low" { C_G } else { C_R };
581 let ac = if approval {
582 format!("{}needs approval{}", C_R, C_RST)
583 } else {
584 format!("{}no approval{}", C_G, C_RST)
585 };
586 println!(
587 " {}•{} {} [{}{}{}] [{}]",
588 C_DIM, C_RST, name, rc, risk, C_RST, ac
589 );
590 }
591 println!(
592 " {}Policy: Rust uses risk-based policy (low=allow, high=deny+approval){}",
593 C_DIM, C_RST
594 );
595 println!(" {}Budget: max 25 tool calls{}", C_DIM, C_RST);
596
597 let (run_id, _step_id) = runtime.create_run(make_state(&[
598 ("trip", "Tokyo".into()),
599 ("budget_usd", Value::Number(3000.0)),
600 ]));
601 println!(
602 "\n {}Run created: {}{}{}",
603 C_B, C_BOLD, run_id, C_RST
604 );
605 show_db(&runtime.store, &run_id);
606 wait("Press Enter to continue");
607
608 // ════════════════════════════════════════════════════════
609 // Step 2: Attempt 1 — Approval interception
610 // ════════════════════════════════════════════════════════
611 println!(
612 "\n{}{}{}",
613 C_BOLD,
614 C_R,
615 "═".repeat(60)
616 );
617 println!(
618 "{}{} Step 2: Attempt 1 — Agent runs → Approval triggered{}",
619 C_BOLD, C_R, C_RST
620 );
621 println!(
622 "{}{}{}",
623 C_BOLD,
624 C_R,
625 "═".repeat(60)
626 );
627 println!(
628 "\n {}Agent executing: search flights → search hotels → check weather → book flight...{}",
629 C_DIM, C_RST
630 );
631
632 {
633 let mut ctx = claim_context(&mut runtime, &run_id, "worker-rust", "TravelPlanner");
634 let result = travel_planner(&mut runtime, &mut ctx, 1);
635 match result {
636 Err(err) if err.0.starts_with("approval required:") => {
637 let approval_id = err.0.trim_start_matches("approval required:");
638 runtime
639 .store
640 .mark_waiting_human(&run_id, &ctx.step_id, &err.0, approval_id);
641 }
642 Err(err) => return Err(format!("unexpected error at step 2: {}", err.0).into()),
643 Ok(()) => {}
644 }
645 }
646
647 println!(
648 "\n {}book_flight triggered approval! Runtime paused, waiting for human.{}",
649 C_R, C_RST
650 );
651 show_db(&runtime.store, &run_id);
652 println!(
653 " {}Note: Tool Ledger has RESERVED entry, approval status is PENDING{}",
654 C_R, C_RST
655 );
656 wait("Press Enter to approve / 按 Enter 审批");
657
658 for req in runtime.store.approval_requests(&run_id) {
659 if req.status == "PENDING" {
660 runtime
661 .store
662 .approve_request(&req.approval_id, "traveler", "Within budget, approved")?;
663 println!(
664 "\n {}✅ Approved: {} — by traveler{}",
665 C_G, req.tool_name, C_RST
666 );
667 }
668 }
669 show_db(&runtime.store, &run_id);
670 wait("Press Enter to continue");
671
672 // ════════════════════════════════════════════════════════
673 // Step 3: Attempt 2 — Execute + Crash
674 // ════════════════════════════════════════════════════════
675 println!(
676 "\n{}{}{}",
677 C_BOLD,
678 C_Y,
679 "═".repeat(60)
680 );
681 println!(
682 "{}{} Step 3: Attempt 2 — Approved → Execute booking → Simulated crash{}",
683 C_BOLD, C_Y, C_RST
684 );
685 println!(
686 "{}{}{}",
687 C_BOLD,
688 C_Y,
689 "═".repeat(60)
690 );
691 println!(
692 "\n {}Re-running agent (approval passed, book_flight will execute)...{}",
693 C_DIM, C_RST
694 );
695
696 {
697 let mut ctx = claim_context(&mut runtime, &run_id, "worker-rust", "TravelPlanner");
698 let result = travel_planner(&mut runtime, &mut ctx, 2);
699 match result {
700 Err(err) if err.0 == "retryable" => {
701 runtime.store.mark_retry(
702 &run_id,
703 &ctx.step_id,
704 "RetryableAgentError",
705 "after flight booking",
706 );
707 }
708 Err(err) if err.0.starts_with("approval required:") => {
709 let approval_id = err.0.trim_start_matches("approval required:");
710 runtime
711 .store
712 .mark_waiting_human(&run_id, &ctx.step_id, &err.0, approval_id);
713 }
714 Err(err) => return Err(format!("unexpected error at step 3: {}", err.0).into()),
715 Ok(()) => {}
716 }
717 }
718
719 println!(
720 "\n {}Agent booked flight, then crashed before committing state!{}",
721 C_Y, C_RST
722 );
723 println!(
724 " {}Flight is booked in external system, but agent state was NOT persisted.{}",
725 C_Y, C_RST
726 );
727 show_db(&runtime.store, &run_id);
728 println!(
729 " {}Key: Tool Ledger book_flight status = {}SUCCEEDED{} (external side effect executed){}",
730 C_Y, C_G, C_Y, C_RST
731 );
732 println!(
733 " {} Step status = retry_scheduled (state not committed, waiting for retry){}",
734 C_Y, C_RST
735 );
736 wait("Press Enter to continue");
737
738 // ════════════════════════════════════════════════════════
739 // Step 4: Attempt 3 — Recovery + Hotel approval
740 // ════════════════════════════════════════════════════════
741 println!(
742 "\n{}{}{}",
743 C_BOLD,
744 C_G,
745 "═".repeat(60)
746 );
747 println!(
748 "{}{} Step 4: Attempt 3 — Crash recovery → Tool Ledger idempotent replay{}",
749 C_BOLD, C_G, C_RST
750 );
751 println!(
752 "{}{}{}",
753 C_BOLD,
754 C_G,
755 "═".repeat(60)
756 );
757 println!(
758 "\n {}Agent re-executes. book_flight: Tool Ledger sees SUCCEEDED record...{}",
759 C_DIM, C_RST
760 );
761 println!(
762 " {}{}→ Returns cached result, no duplicate API call, no double charge!{}",
763 C_DIM, C_G, C_RST
764 );
765
766 {
767 let mut ctx = claim_context(&mut runtime, &run_id, "worker-rust", "TravelPlanner");
768 let result = travel_planner(&mut runtime, &mut ctx, 3);
769 match result {
770 Err(err) if err.0.starts_with("approval required:") => {
771 let approval_id = err.0.trim_start_matches("approval required:");
772 runtime
773 .store
774 .mark_waiting_human(&run_id, &ctx.step_id, &err.0, approval_id);
775 }
776 Err(err) => return Err(format!("unexpected error at step 4: {}", err.0).into()),
777 Ok(()) => {}
778 }
779 }
780
781 println!(
782 "\n {}✅ Flight idempotent replay successful! (no duplicate _book_flight call){}",
783 C_G, C_RST
784 );
785 println!(" {}Hotel booking → triggers approval again{}", C_R, C_RST);
786
787 for req in runtime.store.approval_requests(&run_id) {
788 if req.status == "PENDING" {
789 runtime
790 .store
791 .approve_request(&req.approval_id, "traveler", "Hotel within budget, approved")?;
792 println!(
793 "\n {}✅ Approved: {} — by traveler{}",
794 C_G, req.tool_name, C_RST
795 );
796 }
797 }
798 show_db(&runtime.store, &run_id);
799 wait("Press Enter to continue");
800
801 // ════════════════════════════════════════════════════════
802 // Step 5: Attempt 4 — Complete
803 // ════════════════════════════════════════════════════════
804 println!(
805 "\n{}{}{}",
806 C_BOLD,
807 C_G,
808 "═".repeat(60)
809 );
810 println!(
811 "{}{} Step 5: Attempt 4 — Hotel approved → Full execution → State committed{}",
812 C_BOLD, C_G, C_RST
813 );
814 println!(
815 "{}{}{}",
816 C_BOLD,
817 C_G,
818 "═".repeat(60)
819 );
820
821 {
822 let mut ctx = claim_context(&mut runtime, &run_id, "worker-rust", "TravelPlanner");
823 travel_planner(&mut runtime, &mut ctx, 4)?;
824 runtime.store.commit_state_patch(
825 &run_id,
826 &ctx.step_id,
827 &ctx.lease_token,
828 ctx.state_version,
829 ctx.pending_patch,
830 )?;
831 }
832
833 {
834 let db = BOOKING_DB.lock().unwrap();
835 if db.len() != 2 {
836 return Err(format!("Expected 2 bookings, got {}", db.len()).into());
837 }
838 }
839
840 println!(
841 "\n {}✅ Travel planning complete! State persisted to database.{}",
842 C_G, C_RST
843 );
844 show_db(&runtime.store, &run_id);
845 println!(
846 " {}Step status = completed, State has bookings + trip_status{}",
847 C_G, C_RST
848 );
849 let db = BOOKING_DB.lock().unwrap();
850 let keys: Vec<String> = db.keys().cloned().collect();
851 println!(
852 " {}External bookings: {:?} ({} total, no duplicates){}",
853 C_G,
854 keys,
855 keys.len(),
856 C_RST
857 );
858 drop(db);
859 wait("Press Enter to continue");
860
861 // ════════════════════════════════════════════════════════
862 // Step 6: Evidence + Cost + Replay
863 // ════════════════════════════════════════════════════════
864 println!(
865 "\n{}{}{}",
866 C_BOLD,
867 C_M,
868 "═".repeat(60)
869 );
870 println!(
871 "{}{} Step 6: Evidence export + Cost attribution + Replay verification{}",
872 C_BOLD, C_M, C_RST
873 );
874 println!(
875 "{}{}{}",
876 C_BOLD,
877 C_M,
878 "═".repeat(60)
879 );
880
881 let bundle = export_evidence(&runtime.store, &run_id)?;
882 let replay_result = replay(&runtime.store, &run_id)?;
883 let cost = agentledger::cost_attribution(&runtime.store, &run_id);
884
885 println!(
886 "\n {}Cost attribution: {} tool calls{}",
887 C_M, cost.total.tool_calls, C_RST
888 );
889 println!(
890 " {}Replay: {} events, safe={}{}{}",
891 C_M, replay_result.event_count, C_G, replay_result.replay_safe, C_RST
892 );
893 println!(
894 " {}Evidence bundle: {} events total{}",
895 C_M,
896 bundle.events.len(),
897 C_RST
898 );
899
900 // Write evidence HTML
901 let html_path = root.join("evidence.html");
902 let html = debug_html(&bundle);
903 fs::write(&html_path, html)?;
904 let html_abs = html_path.canonicalize().unwrap_or_else(|_| html_path.clone());
905
906 // Final summary
907 println!(
908 "\n{}{}{}",
909 C_BOLD,
910 C_G,
911 "═".repeat(60)
912 );
913 println!(
914 "{}{} Summary: What AgentLedger (Rust) did in this demo{}",
915 C_BOLD, C_G, C_RST
916 );
917 println!(
918 "{}{}{}",
919 C_BOLD,
920 C_G,
921 "═".repeat(60)
922 );
923 println!(
924 "
925 ┌──────────────────────────────────────────────────────────┐
926 │ │
927 │ {g}✓ Durable execution{rst} Crash → auto retry, state preserved │
928 │ Step: retry_scheduled → completed │
929 │ │
930 │ {g}✓ Tool Ledger{rst} Idempotent replay, flight booked {bold}1x{rst} only │
931 │ SUCCEEDED → cached result on retry │
932 │ │
933 │ {g}✓ Approval gates{rst} Flight + hotel each trigger approval │
934 │ approval_requests records in store │
935 │ │
936 │ {g}✓ Policy engine{rst} Risk-based policy (high → deny+approval) │
937 │ low-risk tools auto-allowed │
938 │ │
939 │ {g}✓ Budget control{rst} Tracked {tc} tool calls │
940 │ BudgetController.before_tool_call() │
941 │ │
942 │ {g}✓ Evidence export{rst} {ec} events recorded │
943 │ events stored in memory store │
944 │ │
945 │ {g}✓ Cost attribution{rst} Auto-recorded per run │
946 │ CostAttribution by agent │
947 │ │
948 │ {g}✓ Replay engine{rst} Event hash verification passed │
949 │ Verify history without re-running │
950 │ │
951 └──────────────────────────────────────────────────────────┘
952",
953 g = C_G,
954 rst = C_RST,
955 bold = C_BOLD,
956 tc = cost.total.tool_calls,
957 ec = bundle.events.len()
958 );
959
960 println!(" {}Storage: in-memory (MemoryStore){}", C_DIM, C_RST);
961 println!(" {}Evidence HTML: {:?}{}", C_DIM, html_abs, C_RST);
962 println!();
963
964 Ok(())
965}Sourcepub fn mark_waiting_human(
&mut self,
run_id: &str,
step_id: &str,
reason: &str,
approval_id: &str,
)
pub fn mark_waiting_human( &mut self, run_id: &str, step_id: &str, reason: &str, approval_id: &str, )
Examples found in repository?
examples/mcp_governance.rs (lines 110-115)
33fn main() -> agentledger::Result<()> {
34 let mut runtime = Runtime::new();
35 runtime.set_sandbox(Box::new(DemoSandboxExecutor));
36 let mut server = InMemoryMCPToolServer::new();
37 server.add_tool(
38 state(&[
39 ("name", "mcp.github.create_pr".into()),
40 (
41 "inputSchema",
42 Value::Object(state(&[
43 ("type", "object".into()),
44 (
45 "required",
46 Value::Array(vec![Value::String("title".to_string())]),
47 ),
48 (
49 "properties",
50 Value::Object(state(&[(
51 "title",
52 Value::Object(state(&[
53 ("type", "string".into()),
54 ("minLength", 1_i64.into()),
55 ])),
56 )])),
57 ),
58 ])),
59 ),
60 (
61 "annotations",
62 Value::Object(state(&[
63 ("side_effect", "external_write".into()),
64 ("risk_level", "high".into()),
65 ("idempotency_required", true.into()),
66 ("approval_required", true.into()),
67 ("sandbox_required", true.into()),
68 (
69 "sandbox_policy",
70 Value::Object(state(&[
71 ("network", "deny".into()),
72 ("filesystem", "read-only".into()),
73 ])),
74 ),
75 ])),
76 ),
77 ]),
78 create_pr,
79 );
80 let adapter = MCPToolAdapter { client_call: create_pr };
81 for descriptor in server.list_tools() {
82 runtime.register_tool(adapter.tool_spec_from_descriptor(&descriptor));
83 }
84
85 let (run_id, _) = runtime.create_run(State::new());
86 let first_claim = runtime.store.claim_step("worker-before-approval", &run_id, 60.0)?;
87 let first_ctx = AgentContext {
88 run_id: run_id.clone(),
89 session_id: first_claim.session_id.clone(),
90 step_id: first_claim.step_id.clone(),
91 agent_role: "MCPAgent".to_string(),
92 lease_token: first_claim.lease_token.clone(),
93 attempt: first_claim.attempt,
94 state_version: first_claim.state_version,
95 pending_patch: State::new(),
96 };
97 let first = runtime.call_tool(
98 &first_ctx,
99 "mcp.github.create_pr",
100 state(&[
101 ("title", "Update runtime docs".into()),
102 ("_logical_operation", "docs-pr".into()),
103 ]),
104 );
105 let approval_id = first
106 .unwrap_err()
107 .0
108 .trim_start_matches("approval required:")
109 .to_string();
110 runtime.store.mark_waiting_human(
111 &run_id,
112 &first_ctx.step_id,
113 "approval required for tool mcp.github.create_pr",
114 &approval_id,
115 );
116 runtime
117 .store
118 .approve_request(&approval_id, "maintainer", "demo approval")?;
119
120 let second_claim = runtime.store.claim_step("worker-after-approval", &run_id, 60.0)?;
121 let mut second_ctx = AgentContext {
122 run_id: run_id.clone(),
123 session_id: second_claim.session_id.clone(),
124 step_id: second_claim.step_id.clone(),
125 agent_role: "MCPAgent".to_string(),
126 lease_token: second_claim.lease_token.clone(),
127 attempt: second_claim.attempt,
128 state_version: second_claim.state_version,
129 pending_patch: State::new(),
130 };
131 let result = runtime.call_tool(
132 &second_ctx,
133 "mcp.github.create_pr",
134 state(&[
135 ("title", "Update runtime docs".into()),
136 ("_logical_operation", "docs-pr".into()),
137 ]),
138 )?;
139 second_ctx.write_state("pull_request", result);
140 runtime.store.commit_state_patch(
141 &run_id,
142 &second_ctx.step_id,
143 &second_ctx.lease_token,
144 second_ctx.state_version,
145 second_ctx.pending_patch,
146 )?;
147
148 let approvals = runtime.store.approval_requests(&run_id);
149 println!(
150 "{{\n \"run_id\": \"{}\",\n \"first_attempt_waited_for_approval\": true,\n \"second_attempt_ok\": true,\n \"approval_count\": {},\n \"approval_status\": \"{}\",\n \"external_action_count\": 1,\n \"final_state_has_pull_request\": {}\n}}",
151 run_id,
152 approvals.len(),
153 approvals.first().map(|row| row.status.as_str()).unwrap_or(""),
154 runtime
155 .store
156 .final_state(&run_id)?
157 .contains_key("pull_request")
158 );
159 Ok(())
160}More examples
examples/travel_assistant.rs (line 640)
425fn main() -> Result<(), Box<dyn std::error::Error>> {
426 let args: Vec<String> = std::env::args().collect();
427 let root = if args.len() > 1 {
428 PathBuf::from(&args[1])
429 } else {
430 std::env::temp_dir().join(format!("agentledger-rust-{}", std::process::id()))
431 };
432 let _ = fs::create_dir_all(&root);
433
434 // Intro
435 println!(
436 "\n{}{} ╔════════════════════════════════════════════════════╗{}",
437 C_BOLD, C_C, C_RST
438 );
439 println!(
440 "{}{} ║ AgentLedger Travel Assistant (Rust) — Interactive Demo ║{}",
441 C_BOLD, C_C, C_RST
442 );
443 println!(
444 "{}{} ║ See real database state at every step ║{}",
445 C_BOLD, C_C, C_RST
446 );
447 println!(
448 "{}{} ╚════════════════════════════════════════════════════╝{}",
449 C_BOLD, C_C, C_RST
450 );
451
452 println!(
453 "\n {}AgentLedger — Durable Execution Runtime for AI Agents{}",
454 C_DIM, C_RST
455 );
456 println!(" ┌────────────────────────────────────────────────────┐");
457 println!(
458 " │ {}✓{} Durable execution — crash recovery │",
459 C_G, C_RST
460 );
461 println!(
462 " │ {}✓{} Tool Ledger — idempotent replay │",
463 C_G, C_RST
464 );
465 println!(
466 " │ {}✓{} Approval gates — human-in-the-loop │",
467 C_G, C_RST
468 );
469 println!(
470 " │ {}✓{} Policy engine — risk-based access │",
471 C_G, C_RST
472 );
473 println!(
474 " │ {}✓{} Budget control — tool call limits │",
475 C_G, C_RST
476 );
477 println!(
478 " │ {}✓{} Evidence export — full audit trail │",
479 C_G, C_RST
480 );
481 println!(" └────────────────────────────────────────────────────┘");
482
483 wait("Press Enter to start / 按 Enter 开始");
484
485 // ════════════════════════════════════════════════════════
486 // Step 1: Setup
487 // ════════════════════════════════════════════════════════
488 println!(
489 "\n{}{}{}",
490 C_BOLD,
491 C_B,
492 "═".repeat(60)
493 );
494 println!(
495 "{}{} Step 1: Initialize — Register tools, configure policy{}",
496 C_BOLD, C_B, C_RST
497 );
498 println!(
499 "{}{}{}",
500 C_BOLD,
501 C_B,
502 "═".repeat(60)
503 );
504
505 let mut runtime = Runtime::new();
506 runtime.set_budget(BudgetLimits {
507 max_tool_calls: Some(25.0),
508 max_model_tokens: None,
509 max_total_usd: None,
510 });
511
512 // Register tools
513 runtime.register_tool(
514 ToolSpec::new("travel.search_flights", Box::new(search_flights))
515 .side_effect("none")
516 .risk_level("low")
517 .input_schema(Value::Object(make_state(&[
518 ("type", "object".into()),
519 (
520 "required",
521 Value::Array(vec!["from".into(), "to".into()]),
522 ),
523 ]))),
524 );
525 runtime.register_tool(
526 ToolSpec::new("travel.search_hotels", Box::new(search_hotels))
527 .side_effect("none")
528 .risk_level("low")
529 .input_schema(Value::Object(make_state(&[
530 ("type", "object".into()),
531 ("required", Value::Array(vec!["city".into()])),
532 ]))),
533 );
534 runtime.register_tool(
535 ToolSpec::new("travel.check_weather", Box::new(check_weather))
536 .side_effect("none")
537 .risk_level("low")
538 .input_schema(Value::Object(make_state(&[
539 ("type", "object".into()),
540 ("required", Value::Array(vec!["city".into()])),
541 ]))),
542 );
543 runtime.register_tool(
544 ToolSpec::new("travel.book_flight", Box::new(book_flight))
545 .side_effect("external_write")
546 .risk_level("high")
547 .idempotency_required(true)
548 .approval_required(true)
549 .input_schema(Value::Object(make_state(&[
550 ("type", "object".into()),
551 (
552 "required",
553 Value::Array(vec!["flight_id".into(), "passenger".into()]),
554 ),
555 ]))),
556 );
557 runtime.register_tool(
558 ToolSpec::new("travel.book_hotel", Box::new(book_hotel))
559 .side_effect("external_write")
560 .risk_level("high")
561 .idempotency_required(true)
562 .approval_required(true)
563 .input_schema(Value::Object(make_state(&[
564 ("type", "object".into()),
565 (
566 "required",
567 Value::Array(vec!["hotel_id".into(), "guest".into()]),
568 ),
569 ]))),
570 );
571
572 println!("\n {}Registered 5 tools:{}", C_C, C_RST);
573 for (name, risk, approval) in [
574 ("travel.search_flights", "low", false),
575 ("travel.search_hotels", "low", false),
576 ("travel.check_weather", "low", false),
577 ("travel.book_flight", "high", true),
578 ("travel.book_hotel", "high", true),
579 ] {
580 let rc = if risk == "low" { C_G } else { C_R };
581 let ac = if approval {
582 format!("{}needs approval{}", C_R, C_RST)
583 } else {
584 format!("{}no approval{}", C_G, C_RST)
585 };
586 println!(
587 " {}•{} {} [{}{}{}] [{}]",
588 C_DIM, C_RST, name, rc, risk, C_RST, ac
589 );
590 }
591 println!(
592 " {}Policy: Rust uses risk-based policy (low=allow, high=deny+approval){}",
593 C_DIM, C_RST
594 );
595 println!(" {}Budget: max 25 tool calls{}", C_DIM, C_RST);
596
597 let (run_id, _step_id) = runtime.create_run(make_state(&[
598 ("trip", "Tokyo".into()),
599 ("budget_usd", Value::Number(3000.0)),
600 ]));
601 println!(
602 "\n {}Run created: {}{}{}",
603 C_B, C_BOLD, run_id, C_RST
604 );
605 show_db(&runtime.store, &run_id);
606 wait("Press Enter to continue");
607
608 // ════════════════════════════════════════════════════════
609 // Step 2: Attempt 1 — Approval interception
610 // ════════════════════════════════════════════════════════
611 println!(
612 "\n{}{}{}",
613 C_BOLD,
614 C_R,
615 "═".repeat(60)
616 );
617 println!(
618 "{}{} Step 2: Attempt 1 — Agent runs → Approval triggered{}",
619 C_BOLD, C_R, C_RST
620 );
621 println!(
622 "{}{}{}",
623 C_BOLD,
624 C_R,
625 "═".repeat(60)
626 );
627 println!(
628 "\n {}Agent executing: search flights → search hotels → check weather → book flight...{}",
629 C_DIM, C_RST
630 );
631
632 {
633 let mut ctx = claim_context(&mut runtime, &run_id, "worker-rust", "TravelPlanner");
634 let result = travel_planner(&mut runtime, &mut ctx, 1);
635 match result {
636 Err(err) if err.0.starts_with("approval required:") => {
637 let approval_id = err.0.trim_start_matches("approval required:");
638 runtime
639 .store
640 .mark_waiting_human(&run_id, &ctx.step_id, &err.0, approval_id);
641 }
642 Err(err) => return Err(format!("unexpected error at step 2: {}", err.0).into()),
643 Ok(()) => {}
644 }
645 }
646
647 println!(
648 "\n {}book_flight triggered approval! Runtime paused, waiting for human.{}",
649 C_R, C_RST
650 );
651 show_db(&runtime.store, &run_id);
652 println!(
653 " {}Note: Tool Ledger has RESERVED entry, approval status is PENDING{}",
654 C_R, C_RST
655 );
656 wait("Press Enter to approve / 按 Enter 审批");
657
658 for req in runtime.store.approval_requests(&run_id) {
659 if req.status == "PENDING" {
660 runtime
661 .store
662 .approve_request(&req.approval_id, "traveler", "Within budget, approved")?;
663 println!(
664 "\n {}✅ Approved: {} — by traveler{}",
665 C_G, req.tool_name, C_RST
666 );
667 }
668 }
669 show_db(&runtime.store, &run_id);
670 wait("Press Enter to continue");
671
672 // ════════════════════════════════════════════════════════
673 // Step 3: Attempt 2 — Execute + Crash
674 // ════════════════════════════════════════════════════════
675 println!(
676 "\n{}{}{}",
677 C_BOLD,
678 C_Y,
679 "═".repeat(60)
680 );
681 println!(
682 "{}{} Step 3: Attempt 2 — Approved → Execute booking → Simulated crash{}",
683 C_BOLD, C_Y, C_RST
684 );
685 println!(
686 "{}{}{}",
687 C_BOLD,
688 C_Y,
689 "═".repeat(60)
690 );
691 println!(
692 "\n {}Re-running agent (approval passed, book_flight will execute)...{}",
693 C_DIM, C_RST
694 );
695
696 {
697 let mut ctx = claim_context(&mut runtime, &run_id, "worker-rust", "TravelPlanner");
698 let result = travel_planner(&mut runtime, &mut ctx, 2);
699 match result {
700 Err(err) if err.0 == "retryable" => {
701 runtime.store.mark_retry(
702 &run_id,
703 &ctx.step_id,
704 "RetryableAgentError",
705 "after flight booking",
706 );
707 }
708 Err(err) if err.0.starts_with("approval required:") => {
709 let approval_id = err.0.trim_start_matches("approval required:");
710 runtime
711 .store
712 .mark_waiting_human(&run_id, &ctx.step_id, &err.0, approval_id);
713 }
714 Err(err) => return Err(format!("unexpected error at step 3: {}", err.0).into()),
715 Ok(()) => {}
716 }
717 }
718
719 println!(
720 "\n {}Agent booked flight, then crashed before committing state!{}",
721 C_Y, C_RST
722 );
723 println!(
724 " {}Flight is booked in external system, but agent state was NOT persisted.{}",
725 C_Y, C_RST
726 );
727 show_db(&runtime.store, &run_id);
728 println!(
729 " {}Key: Tool Ledger book_flight status = {}SUCCEEDED{} (external side effect executed){}",
730 C_Y, C_G, C_Y, C_RST
731 );
732 println!(
733 " {} Step status = retry_scheduled (state not committed, waiting for retry){}",
734 C_Y, C_RST
735 );
736 wait("Press Enter to continue");
737
738 // ════════════════════════════════════════════════════════
739 // Step 4: Attempt 3 — Recovery + Hotel approval
740 // ════════════════════════════════════════════════════════
741 println!(
742 "\n{}{}{}",
743 C_BOLD,
744 C_G,
745 "═".repeat(60)
746 );
747 println!(
748 "{}{} Step 4: Attempt 3 — Crash recovery → Tool Ledger idempotent replay{}",
749 C_BOLD, C_G, C_RST
750 );
751 println!(
752 "{}{}{}",
753 C_BOLD,
754 C_G,
755 "═".repeat(60)
756 );
757 println!(
758 "\n {}Agent re-executes. book_flight: Tool Ledger sees SUCCEEDED record...{}",
759 C_DIM, C_RST
760 );
761 println!(
762 " {}{}→ Returns cached result, no duplicate API call, no double charge!{}",
763 C_DIM, C_G, C_RST
764 );
765
766 {
767 let mut ctx = claim_context(&mut runtime, &run_id, "worker-rust", "TravelPlanner");
768 let result = travel_planner(&mut runtime, &mut ctx, 3);
769 match result {
770 Err(err) if err.0.starts_with("approval required:") => {
771 let approval_id = err.0.trim_start_matches("approval required:");
772 runtime
773 .store
774 .mark_waiting_human(&run_id, &ctx.step_id, &err.0, approval_id);
775 }
776 Err(err) => return Err(format!("unexpected error at step 4: {}", err.0).into()),
777 Ok(()) => {}
778 }
779 }
780
781 println!(
782 "\n {}✅ Flight idempotent replay successful! (no duplicate _book_flight call){}",
783 C_G, C_RST
784 );
785 println!(" {}Hotel booking → triggers approval again{}", C_R, C_RST);
786
787 for req in runtime.store.approval_requests(&run_id) {
788 if req.status == "PENDING" {
789 runtime
790 .store
791 .approve_request(&req.approval_id, "traveler", "Hotel within budget, approved")?;
792 println!(
793 "\n {}✅ Approved: {} — by traveler{}",
794 C_G, req.tool_name, C_RST
795 );
796 }
797 }
798 show_db(&runtime.store, &run_id);
799 wait("Press Enter to continue");
800
801 // ════════════════════════════════════════════════════════
802 // Step 5: Attempt 4 — Complete
803 // ════════════════════════════════════════════════════════
804 println!(
805 "\n{}{}{}",
806 C_BOLD,
807 C_G,
808 "═".repeat(60)
809 );
810 println!(
811 "{}{} Step 5: Attempt 4 — Hotel approved → Full execution → State committed{}",
812 C_BOLD, C_G, C_RST
813 );
814 println!(
815 "{}{}{}",
816 C_BOLD,
817 C_G,
818 "═".repeat(60)
819 );
820
821 {
822 let mut ctx = claim_context(&mut runtime, &run_id, "worker-rust", "TravelPlanner");
823 travel_planner(&mut runtime, &mut ctx, 4)?;
824 runtime.store.commit_state_patch(
825 &run_id,
826 &ctx.step_id,
827 &ctx.lease_token,
828 ctx.state_version,
829 ctx.pending_patch,
830 )?;
831 }
832
833 {
834 let db = BOOKING_DB.lock().unwrap();
835 if db.len() != 2 {
836 return Err(format!("Expected 2 bookings, got {}", db.len()).into());
837 }
838 }
839
840 println!(
841 "\n {}✅ Travel planning complete! State persisted to database.{}",
842 C_G, C_RST
843 );
844 show_db(&runtime.store, &run_id);
845 println!(
846 " {}Step status = completed, State has bookings + trip_status{}",
847 C_G, C_RST
848 );
849 let db = BOOKING_DB.lock().unwrap();
850 let keys: Vec<String> = db.keys().cloned().collect();
851 println!(
852 " {}External bookings: {:?} ({} total, no duplicates){}",
853 C_G,
854 keys,
855 keys.len(),
856 C_RST
857 );
858 drop(db);
859 wait("Press Enter to continue");
860
861 // ════════════════════════════════════════════════════════
862 // Step 6: Evidence + Cost + Replay
863 // ════════════════════════════════════════════════════════
864 println!(
865 "\n{}{}{}",
866 C_BOLD,
867 C_M,
868 "═".repeat(60)
869 );
870 println!(
871 "{}{} Step 6: Evidence export + Cost attribution + Replay verification{}",
872 C_BOLD, C_M, C_RST
873 );
874 println!(
875 "{}{}{}",
876 C_BOLD,
877 C_M,
878 "═".repeat(60)
879 );
880
881 let bundle = export_evidence(&runtime.store, &run_id)?;
882 let replay_result = replay(&runtime.store, &run_id)?;
883 let cost = agentledger::cost_attribution(&runtime.store, &run_id);
884
885 println!(
886 "\n {}Cost attribution: {} tool calls{}",
887 C_M, cost.total.tool_calls, C_RST
888 );
889 println!(
890 " {}Replay: {} events, safe={}{}{}",
891 C_M, replay_result.event_count, C_G, replay_result.replay_safe, C_RST
892 );
893 println!(
894 " {}Evidence bundle: {} events total{}",
895 C_M,
896 bundle.events.len(),
897 C_RST
898 );
899
900 // Write evidence HTML
901 let html_path = root.join("evidence.html");
902 let html = debug_html(&bundle);
903 fs::write(&html_path, html)?;
904 let html_abs = html_path.canonicalize().unwrap_or_else(|_| html_path.clone());
905
906 // Final summary
907 println!(
908 "\n{}{}{}",
909 C_BOLD,
910 C_G,
911 "═".repeat(60)
912 );
913 println!(
914 "{}{} Summary: What AgentLedger (Rust) did in this demo{}",
915 C_BOLD, C_G, C_RST
916 );
917 println!(
918 "{}{}{}",
919 C_BOLD,
920 C_G,
921 "═".repeat(60)
922 );
923 println!(
924 "
925 ┌──────────────────────────────────────────────────────────┐
926 │ │
927 │ {g}✓ Durable execution{rst} Crash → auto retry, state preserved │
928 │ Step: retry_scheduled → completed │
929 │ │
930 │ {g}✓ Tool Ledger{rst} Idempotent replay, flight booked {bold}1x{rst} only │
931 │ SUCCEEDED → cached result on retry │
932 │ │
933 │ {g}✓ Approval gates{rst} Flight + hotel each trigger approval │
934 │ approval_requests records in store │
935 │ │
936 │ {g}✓ Policy engine{rst} Risk-based policy (high → deny+approval) │
937 │ low-risk tools auto-allowed │
938 │ │
939 │ {g}✓ Budget control{rst} Tracked {tc} tool calls │
940 │ BudgetController.before_tool_call() │
941 │ │
942 │ {g}✓ Evidence export{rst} {ec} events recorded │
943 │ events stored in memory store │
944 │ │
945 │ {g}✓ Cost attribution{rst} Auto-recorded per run │
946 │ CostAttribution by agent │
947 │ │
948 │ {g}✓ Replay engine{rst} Event hash verification passed │
949 │ Verify history without re-running │
950 │ │
951 └──────────────────────────────────────────────────────────┘
952",
953 g = C_G,
954 rst = C_RST,
955 bold = C_BOLD,
956 tc = cost.total.tool_calls,
957 ec = bundle.events.len()
958 );
959
960 println!(" {}Storage: in-memory (MemoryStore){}", C_DIM, C_RST);
961 println!(" {}Evidence HTML: {:?}{}", C_DIM, html_abs, C_RST);
962 println!();
963
964 Ok(())
965}pub fn mark_failed( &mut self, run_id: &str, step_id: &str, error_type: &str, message: &str, )
Sourcepub fn append_event(
&mut self,
run_id: &str,
session_id: Option<&str>,
step_id: Option<&str>,
event_type: &str,
payload: State,
agent_role: Option<&str>,
state_version: Option<u64>,
causal_token: Option<&str>,
) -> Event
pub fn append_event( &mut self, run_id: &str, session_id: Option<&str>, step_id: Option<&str>, event_type: &str, payload: State, agent_role: Option<&str>, state_version: Option<u64>, causal_token: Option<&str>, ) -> Event
Examples found in repository?
examples/travel_assistant.rs (lines 298-307)
287fn claim_context(
288 runtime: &mut Runtime,
289 run_id: &str,
290 worker: &str,
291 role: &str,
292) -> AgentContext {
293 let claim = runtime.store.claim_step(worker, run_id, 60.0)
294 .expect("claim step failed");
295 let mut payload = State::new();
296 payload.insert("agent_role".to_string(), Value::String(role.to_string()));
297 payload.insert("attempt".to_string(), Value::Number(claim.attempt as f64));
298 runtime.store.append_event(
299 run_id,
300 Some(&claim.session_id),
301 Some(&claim.step_id),
302 "agent_started",
303 payload,
304 Some(role),
305 Some(claim.state_version),
306 None,
307 );
308 AgentContext {
309 run_id: claim.run_id.clone(),
310 session_id: claim.session_id,
311 step_id: claim.step_id.clone(),
312 agent_role: role.to_string(),
313 lease_token: claim.lease_token.clone(),
314 attempt: claim.attempt,
315 state_version: claim.state_version,
316 pending_patch: State::new(),
317 }
318}pub fn reserve_ledger( &mut self, key: &str, entry: ToolLedgerEntry, ) -> Option<ToolLedgerEntry>
pub fn update_ledger( &mut self, key: &str, status: &str, external_id: Option<String>, response: Option<Value>, error_type: Option<String>, )
pub fn request_approval( &mut self, approval_key: &str, run_id: &str, session_id: &str, step_id: &str, tool_name: &str, risk_level: &str, reason: &str, request_hash: &str, request_ref: &str, requested_by: &str, ) -> ApprovalRequest
pub fn approval_for_key(&self, approval_key: &str) -> Option<ApprovalRequest>
Sourcepub fn approval_requests(&self, run_id: &str) -> Vec<ApprovalRequest>
pub fn approval_requests(&self, run_id: &str) -> Vec<ApprovalRequest>
Examples found in repository?
examples/travel_assistant.rs (line 404)
347fn show_db(store: &MemoryStore, run_id: &str) {
348 // Runs
349 let mut run_rows = Vec::new();
350 if let Ok(run) = store.run(run_id) {
351 let short_id = if run.run_id.len() > 24 {
352 format!("{}...", &run.run_id[..24])
353 } else {
354 run.run_id.clone()
355 };
356 run_rows.push(vec![short_id, run.status, run.state_version.to_string()]);
357 }
358 show_rows("Runs", &["run_id", "status", "state_version"], &run_rows, C_B);
359
360 // Steps
361 let step_rows: Vec<Vec<String>> = store
362 .steps(run_id)
363 .into_iter()
364 .map(|s| {
365 let short_id = if s.step_id.len() > 24 {
366 format!("{}...", &s.step_id[..24])
367 } else {
368 s.step_id.clone()
369 };
370 vec![short_id, s.status, s.attempt.to_string()]
371 })
372 .collect();
373 show_rows("Steps", &["step_id", "status", "attempt"], &step_rows, C_B);
374
375 // Tool Ledger
376 let ledger_rows: Vec<Vec<String>> = store
377 .ledger(run_id)
378 .into_iter()
379 .map(|tl| {
380 let key = &tl.idempotency_key;
381 let short_key = match key.rfind(':') {
382 Some(idx) => {
383 let prev = key[..idx].rfind(':').unwrap_or(0);
384 if prev > 0 {
385 format!("{}:{}", &key[prev + 1..idx], &key[idx + 1..])
386 } else {
387 format!("{}", &key[idx + 1..])
388 }
389 }
390 None => key[..key.len().min(25)].to_string(),
391 };
392 vec![tl.tool_name, tl.status, short_key]
393 })
394 .collect();
395 show_rows(
396 "Tool Ledger",
397 &["tool", "status", "idemp_key"],
398 &ledger_rows,
399 C_Y,
400 );
401
402 // Approval requests
403 let approval_rows: Vec<Vec<String>> = store
404 .approval_requests(run_id)
405 .into_iter()
406 .map(|a| {
407 let approved_by = a.approved_by.unwrap_or_else(|| "-".to_string());
408 vec![a.tool_name, a.status, approved_by]
409 })
410 .collect();
411 show_rows(
412 "Approval Requests",
413 &["tool", "status", "approved_by"],
414 &approval_rows,
415 C_R,
416 );
417
418 println!();
419}
420
421// ════════════════════════════════════════════════════════════
422// Main
423// ════════════════════════════════════════════════════════════
424
425fn main() -> Result<(), Box<dyn std::error::Error>> {
426 let args: Vec<String> = std::env::args().collect();
427 let root = if args.len() > 1 {
428 PathBuf::from(&args[1])
429 } else {
430 std::env::temp_dir().join(format!("agentledger-rust-{}", std::process::id()))
431 };
432 let _ = fs::create_dir_all(&root);
433
434 // Intro
435 println!(
436 "\n{}{} ╔════════════════════════════════════════════════════╗{}",
437 C_BOLD, C_C, C_RST
438 );
439 println!(
440 "{}{} ║ AgentLedger Travel Assistant (Rust) — Interactive Demo ║{}",
441 C_BOLD, C_C, C_RST
442 );
443 println!(
444 "{}{} ║ See real database state at every step ║{}",
445 C_BOLD, C_C, C_RST
446 );
447 println!(
448 "{}{} ╚════════════════════════════════════════════════════╝{}",
449 C_BOLD, C_C, C_RST
450 );
451
452 println!(
453 "\n {}AgentLedger — Durable Execution Runtime for AI Agents{}",
454 C_DIM, C_RST
455 );
456 println!(" ┌────────────────────────────────────────────────────┐");
457 println!(
458 " │ {}✓{} Durable execution — crash recovery │",
459 C_G, C_RST
460 );
461 println!(
462 " │ {}✓{} Tool Ledger — idempotent replay │",
463 C_G, C_RST
464 );
465 println!(
466 " │ {}✓{} Approval gates — human-in-the-loop │",
467 C_G, C_RST
468 );
469 println!(
470 " │ {}✓{} Policy engine — risk-based access │",
471 C_G, C_RST
472 );
473 println!(
474 " │ {}✓{} Budget control — tool call limits │",
475 C_G, C_RST
476 );
477 println!(
478 " │ {}✓{} Evidence export — full audit trail │",
479 C_G, C_RST
480 );
481 println!(" └────────────────────────────────────────────────────┘");
482
483 wait("Press Enter to start / 按 Enter 开始");
484
485 // ════════════════════════════════════════════════════════
486 // Step 1: Setup
487 // ════════════════════════════════════════════════════════
488 println!(
489 "\n{}{}{}",
490 C_BOLD,
491 C_B,
492 "═".repeat(60)
493 );
494 println!(
495 "{}{} Step 1: Initialize — Register tools, configure policy{}",
496 C_BOLD, C_B, C_RST
497 );
498 println!(
499 "{}{}{}",
500 C_BOLD,
501 C_B,
502 "═".repeat(60)
503 );
504
505 let mut runtime = Runtime::new();
506 runtime.set_budget(BudgetLimits {
507 max_tool_calls: Some(25.0),
508 max_model_tokens: None,
509 max_total_usd: None,
510 });
511
512 // Register tools
513 runtime.register_tool(
514 ToolSpec::new("travel.search_flights", Box::new(search_flights))
515 .side_effect("none")
516 .risk_level("low")
517 .input_schema(Value::Object(make_state(&[
518 ("type", "object".into()),
519 (
520 "required",
521 Value::Array(vec!["from".into(), "to".into()]),
522 ),
523 ]))),
524 );
525 runtime.register_tool(
526 ToolSpec::new("travel.search_hotels", Box::new(search_hotels))
527 .side_effect("none")
528 .risk_level("low")
529 .input_schema(Value::Object(make_state(&[
530 ("type", "object".into()),
531 ("required", Value::Array(vec!["city".into()])),
532 ]))),
533 );
534 runtime.register_tool(
535 ToolSpec::new("travel.check_weather", Box::new(check_weather))
536 .side_effect("none")
537 .risk_level("low")
538 .input_schema(Value::Object(make_state(&[
539 ("type", "object".into()),
540 ("required", Value::Array(vec!["city".into()])),
541 ]))),
542 );
543 runtime.register_tool(
544 ToolSpec::new("travel.book_flight", Box::new(book_flight))
545 .side_effect("external_write")
546 .risk_level("high")
547 .idempotency_required(true)
548 .approval_required(true)
549 .input_schema(Value::Object(make_state(&[
550 ("type", "object".into()),
551 (
552 "required",
553 Value::Array(vec!["flight_id".into(), "passenger".into()]),
554 ),
555 ]))),
556 );
557 runtime.register_tool(
558 ToolSpec::new("travel.book_hotel", Box::new(book_hotel))
559 .side_effect("external_write")
560 .risk_level("high")
561 .idempotency_required(true)
562 .approval_required(true)
563 .input_schema(Value::Object(make_state(&[
564 ("type", "object".into()),
565 (
566 "required",
567 Value::Array(vec!["hotel_id".into(), "guest".into()]),
568 ),
569 ]))),
570 );
571
572 println!("\n {}Registered 5 tools:{}", C_C, C_RST);
573 for (name, risk, approval) in [
574 ("travel.search_flights", "low", false),
575 ("travel.search_hotels", "low", false),
576 ("travel.check_weather", "low", false),
577 ("travel.book_flight", "high", true),
578 ("travel.book_hotel", "high", true),
579 ] {
580 let rc = if risk == "low" { C_G } else { C_R };
581 let ac = if approval {
582 format!("{}needs approval{}", C_R, C_RST)
583 } else {
584 format!("{}no approval{}", C_G, C_RST)
585 };
586 println!(
587 " {}•{} {} [{}{}{}] [{}]",
588 C_DIM, C_RST, name, rc, risk, C_RST, ac
589 );
590 }
591 println!(
592 " {}Policy: Rust uses risk-based policy (low=allow, high=deny+approval){}",
593 C_DIM, C_RST
594 );
595 println!(" {}Budget: max 25 tool calls{}", C_DIM, C_RST);
596
597 let (run_id, _step_id) = runtime.create_run(make_state(&[
598 ("trip", "Tokyo".into()),
599 ("budget_usd", Value::Number(3000.0)),
600 ]));
601 println!(
602 "\n {}Run created: {}{}{}",
603 C_B, C_BOLD, run_id, C_RST
604 );
605 show_db(&runtime.store, &run_id);
606 wait("Press Enter to continue");
607
608 // ════════════════════════════════════════════════════════
609 // Step 2: Attempt 1 — Approval interception
610 // ════════════════════════════════════════════════════════
611 println!(
612 "\n{}{}{}",
613 C_BOLD,
614 C_R,
615 "═".repeat(60)
616 );
617 println!(
618 "{}{} Step 2: Attempt 1 — Agent runs → Approval triggered{}",
619 C_BOLD, C_R, C_RST
620 );
621 println!(
622 "{}{}{}",
623 C_BOLD,
624 C_R,
625 "═".repeat(60)
626 );
627 println!(
628 "\n {}Agent executing: search flights → search hotels → check weather → book flight...{}",
629 C_DIM, C_RST
630 );
631
632 {
633 let mut ctx = claim_context(&mut runtime, &run_id, "worker-rust", "TravelPlanner");
634 let result = travel_planner(&mut runtime, &mut ctx, 1);
635 match result {
636 Err(err) if err.0.starts_with("approval required:") => {
637 let approval_id = err.0.trim_start_matches("approval required:");
638 runtime
639 .store
640 .mark_waiting_human(&run_id, &ctx.step_id, &err.0, approval_id);
641 }
642 Err(err) => return Err(format!("unexpected error at step 2: {}", err.0).into()),
643 Ok(()) => {}
644 }
645 }
646
647 println!(
648 "\n {}book_flight triggered approval! Runtime paused, waiting for human.{}",
649 C_R, C_RST
650 );
651 show_db(&runtime.store, &run_id);
652 println!(
653 " {}Note: Tool Ledger has RESERVED entry, approval status is PENDING{}",
654 C_R, C_RST
655 );
656 wait("Press Enter to approve / 按 Enter 审批");
657
658 for req in runtime.store.approval_requests(&run_id) {
659 if req.status == "PENDING" {
660 runtime
661 .store
662 .approve_request(&req.approval_id, "traveler", "Within budget, approved")?;
663 println!(
664 "\n {}✅ Approved: {} — by traveler{}",
665 C_G, req.tool_name, C_RST
666 );
667 }
668 }
669 show_db(&runtime.store, &run_id);
670 wait("Press Enter to continue");
671
672 // ════════════════════════════════════════════════════════
673 // Step 3: Attempt 2 — Execute + Crash
674 // ════════════════════════════════════════════════════════
675 println!(
676 "\n{}{}{}",
677 C_BOLD,
678 C_Y,
679 "═".repeat(60)
680 );
681 println!(
682 "{}{} Step 3: Attempt 2 — Approved → Execute booking → Simulated crash{}",
683 C_BOLD, C_Y, C_RST
684 );
685 println!(
686 "{}{}{}",
687 C_BOLD,
688 C_Y,
689 "═".repeat(60)
690 );
691 println!(
692 "\n {}Re-running agent (approval passed, book_flight will execute)...{}",
693 C_DIM, C_RST
694 );
695
696 {
697 let mut ctx = claim_context(&mut runtime, &run_id, "worker-rust", "TravelPlanner");
698 let result = travel_planner(&mut runtime, &mut ctx, 2);
699 match result {
700 Err(err) if err.0 == "retryable" => {
701 runtime.store.mark_retry(
702 &run_id,
703 &ctx.step_id,
704 "RetryableAgentError",
705 "after flight booking",
706 );
707 }
708 Err(err) if err.0.starts_with("approval required:") => {
709 let approval_id = err.0.trim_start_matches("approval required:");
710 runtime
711 .store
712 .mark_waiting_human(&run_id, &ctx.step_id, &err.0, approval_id);
713 }
714 Err(err) => return Err(format!("unexpected error at step 3: {}", err.0).into()),
715 Ok(()) => {}
716 }
717 }
718
719 println!(
720 "\n {}Agent booked flight, then crashed before committing state!{}",
721 C_Y, C_RST
722 );
723 println!(
724 " {}Flight is booked in external system, but agent state was NOT persisted.{}",
725 C_Y, C_RST
726 );
727 show_db(&runtime.store, &run_id);
728 println!(
729 " {}Key: Tool Ledger book_flight status = {}SUCCEEDED{} (external side effect executed){}",
730 C_Y, C_G, C_Y, C_RST
731 );
732 println!(
733 " {} Step status = retry_scheduled (state not committed, waiting for retry){}",
734 C_Y, C_RST
735 );
736 wait("Press Enter to continue");
737
738 // ════════════════════════════════════════════════════════
739 // Step 4: Attempt 3 — Recovery + Hotel approval
740 // ════════════════════════════════════════════════════════
741 println!(
742 "\n{}{}{}",
743 C_BOLD,
744 C_G,
745 "═".repeat(60)
746 );
747 println!(
748 "{}{} Step 4: Attempt 3 — Crash recovery → Tool Ledger idempotent replay{}",
749 C_BOLD, C_G, C_RST
750 );
751 println!(
752 "{}{}{}",
753 C_BOLD,
754 C_G,
755 "═".repeat(60)
756 );
757 println!(
758 "\n {}Agent re-executes. book_flight: Tool Ledger sees SUCCEEDED record...{}",
759 C_DIM, C_RST
760 );
761 println!(
762 " {}{}→ Returns cached result, no duplicate API call, no double charge!{}",
763 C_DIM, C_G, C_RST
764 );
765
766 {
767 let mut ctx = claim_context(&mut runtime, &run_id, "worker-rust", "TravelPlanner");
768 let result = travel_planner(&mut runtime, &mut ctx, 3);
769 match result {
770 Err(err) if err.0.starts_with("approval required:") => {
771 let approval_id = err.0.trim_start_matches("approval required:");
772 runtime
773 .store
774 .mark_waiting_human(&run_id, &ctx.step_id, &err.0, approval_id);
775 }
776 Err(err) => return Err(format!("unexpected error at step 4: {}", err.0).into()),
777 Ok(()) => {}
778 }
779 }
780
781 println!(
782 "\n {}✅ Flight idempotent replay successful! (no duplicate _book_flight call){}",
783 C_G, C_RST
784 );
785 println!(" {}Hotel booking → triggers approval again{}", C_R, C_RST);
786
787 for req in runtime.store.approval_requests(&run_id) {
788 if req.status == "PENDING" {
789 runtime
790 .store
791 .approve_request(&req.approval_id, "traveler", "Hotel within budget, approved")?;
792 println!(
793 "\n {}✅ Approved: {} — by traveler{}",
794 C_G, req.tool_name, C_RST
795 );
796 }
797 }
798 show_db(&runtime.store, &run_id);
799 wait("Press Enter to continue");
800
801 // ════════════════════════════════════════════════════════
802 // Step 5: Attempt 4 — Complete
803 // ════════════════════════════════════════════════════════
804 println!(
805 "\n{}{}{}",
806 C_BOLD,
807 C_G,
808 "═".repeat(60)
809 );
810 println!(
811 "{}{} Step 5: Attempt 4 — Hotel approved → Full execution → State committed{}",
812 C_BOLD, C_G, C_RST
813 );
814 println!(
815 "{}{}{}",
816 C_BOLD,
817 C_G,
818 "═".repeat(60)
819 );
820
821 {
822 let mut ctx = claim_context(&mut runtime, &run_id, "worker-rust", "TravelPlanner");
823 travel_planner(&mut runtime, &mut ctx, 4)?;
824 runtime.store.commit_state_patch(
825 &run_id,
826 &ctx.step_id,
827 &ctx.lease_token,
828 ctx.state_version,
829 ctx.pending_patch,
830 )?;
831 }
832
833 {
834 let db = BOOKING_DB.lock().unwrap();
835 if db.len() != 2 {
836 return Err(format!("Expected 2 bookings, got {}", db.len()).into());
837 }
838 }
839
840 println!(
841 "\n {}✅ Travel planning complete! State persisted to database.{}",
842 C_G, C_RST
843 );
844 show_db(&runtime.store, &run_id);
845 println!(
846 " {}Step status = completed, State has bookings + trip_status{}",
847 C_G, C_RST
848 );
849 let db = BOOKING_DB.lock().unwrap();
850 let keys: Vec<String> = db.keys().cloned().collect();
851 println!(
852 " {}External bookings: {:?} ({} total, no duplicates){}",
853 C_G,
854 keys,
855 keys.len(),
856 C_RST
857 );
858 drop(db);
859 wait("Press Enter to continue");
860
861 // ════════════════════════════════════════════════════════
862 // Step 6: Evidence + Cost + Replay
863 // ════════════════════════════════════════════════════════
864 println!(
865 "\n{}{}{}",
866 C_BOLD,
867 C_M,
868 "═".repeat(60)
869 );
870 println!(
871 "{}{} Step 6: Evidence export + Cost attribution + Replay verification{}",
872 C_BOLD, C_M, C_RST
873 );
874 println!(
875 "{}{}{}",
876 C_BOLD,
877 C_M,
878 "═".repeat(60)
879 );
880
881 let bundle = export_evidence(&runtime.store, &run_id)?;
882 let replay_result = replay(&runtime.store, &run_id)?;
883 let cost = agentledger::cost_attribution(&runtime.store, &run_id);
884
885 println!(
886 "\n {}Cost attribution: {} tool calls{}",
887 C_M, cost.total.tool_calls, C_RST
888 );
889 println!(
890 " {}Replay: {} events, safe={}{}{}",
891 C_M, replay_result.event_count, C_G, replay_result.replay_safe, C_RST
892 );
893 println!(
894 " {}Evidence bundle: {} events total{}",
895 C_M,
896 bundle.events.len(),
897 C_RST
898 );
899
900 // Write evidence HTML
901 let html_path = root.join("evidence.html");
902 let html = debug_html(&bundle);
903 fs::write(&html_path, html)?;
904 let html_abs = html_path.canonicalize().unwrap_or_else(|_| html_path.clone());
905
906 // Final summary
907 println!(
908 "\n{}{}{}",
909 C_BOLD,
910 C_G,
911 "═".repeat(60)
912 );
913 println!(
914 "{}{} Summary: What AgentLedger (Rust) did in this demo{}",
915 C_BOLD, C_G, C_RST
916 );
917 println!(
918 "{}{}{}",
919 C_BOLD,
920 C_G,
921 "═".repeat(60)
922 );
923 println!(
924 "
925 ┌──────────────────────────────────────────────────────────┐
926 │ │
927 │ {g}✓ Durable execution{rst} Crash → auto retry, state preserved │
928 │ Step: retry_scheduled → completed │
929 │ │
930 │ {g}✓ Tool Ledger{rst} Idempotent replay, flight booked {bold}1x{rst} only │
931 │ SUCCEEDED → cached result on retry │
932 │ │
933 │ {g}✓ Approval gates{rst} Flight + hotel each trigger approval │
934 │ approval_requests records in store │
935 │ │
936 │ {g}✓ Policy engine{rst} Risk-based policy (high → deny+approval) │
937 │ low-risk tools auto-allowed │
938 │ │
939 │ {g}✓ Budget control{rst} Tracked {tc} tool calls │
940 │ BudgetController.before_tool_call() │
941 │ │
942 │ {g}✓ Evidence export{rst} {ec} events recorded │
943 │ events stored in memory store │
944 │ │
945 │ {g}✓ Cost attribution{rst} Auto-recorded per run │
946 │ CostAttribution by agent │
947 │ │
948 │ {g}✓ Replay engine{rst} Event hash verification passed │
949 │ Verify history without re-running │
950 │ │
951 └──────────────────────────────────────────────────────────┘
952",
953 g = C_G,
954 rst = C_RST,
955 bold = C_BOLD,
956 tc = cost.total.tool_calls,
957 ec = bundle.events.len()
958 );
959
960 println!(" {}Storage: in-memory (MemoryStore){}", C_DIM, C_RST);
961 println!(" {}Evidence HTML: {:?}{}", C_DIM, html_abs, C_RST);
962 println!();
963
964 Ok(())
965}More examples
examples/mcp_governance.rs (line 148)
33fn main() -> agentledger::Result<()> {
34 let mut runtime = Runtime::new();
35 runtime.set_sandbox(Box::new(DemoSandboxExecutor));
36 let mut server = InMemoryMCPToolServer::new();
37 server.add_tool(
38 state(&[
39 ("name", "mcp.github.create_pr".into()),
40 (
41 "inputSchema",
42 Value::Object(state(&[
43 ("type", "object".into()),
44 (
45 "required",
46 Value::Array(vec![Value::String("title".to_string())]),
47 ),
48 (
49 "properties",
50 Value::Object(state(&[(
51 "title",
52 Value::Object(state(&[
53 ("type", "string".into()),
54 ("minLength", 1_i64.into()),
55 ])),
56 )])),
57 ),
58 ])),
59 ),
60 (
61 "annotations",
62 Value::Object(state(&[
63 ("side_effect", "external_write".into()),
64 ("risk_level", "high".into()),
65 ("idempotency_required", true.into()),
66 ("approval_required", true.into()),
67 ("sandbox_required", true.into()),
68 (
69 "sandbox_policy",
70 Value::Object(state(&[
71 ("network", "deny".into()),
72 ("filesystem", "read-only".into()),
73 ])),
74 ),
75 ])),
76 ),
77 ]),
78 create_pr,
79 );
80 let adapter = MCPToolAdapter { client_call: create_pr };
81 for descriptor in server.list_tools() {
82 runtime.register_tool(adapter.tool_spec_from_descriptor(&descriptor));
83 }
84
85 let (run_id, _) = runtime.create_run(State::new());
86 let first_claim = runtime.store.claim_step("worker-before-approval", &run_id, 60.0)?;
87 let first_ctx = AgentContext {
88 run_id: run_id.clone(),
89 session_id: first_claim.session_id.clone(),
90 step_id: first_claim.step_id.clone(),
91 agent_role: "MCPAgent".to_string(),
92 lease_token: first_claim.lease_token.clone(),
93 attempt: first_claim.attempt,
94 state_version: first_claim.state_version,
95 pending_patch: State::new(),
96 };
97 let first = runtime.call_tool(
98 &first_ctx,
99 "mcp.github.create_pr",
100 state(&[
101 ("title", "Update runtime docs".into()),
102 ("_logical_operation", "docs-pr".into()),
103 ]),
104 );
105 let approval_id = first
106 .unwrap_err()
107 .0
108 .trim_start_matches("approval required:")
109 .to_string();
110 runtime.store.mark_waiting_human(
111 &run_id,
112 &first_ctx.step_id,
113 "approval required for tool mcp.github.create_pr",
114 &approval_id,
115 );
116 runtime
117 .store
118 .approve_request(&approval_id, "maintainer", "demo approval")?;
119
120 let second_claim = runtime.store.claim_step("worker-after-approval", &run_id, 60.0)?;
121 let mut second_ctx = AgentContext {
122 run_id: run_id.clone(),
123 session_id: second_claim.session_id.clone(),
124 step_id: second_claim.step_id.clone(),
125 agent_role: "MCPAgent".to_string(),
126 lease_token: second_claim.lease_token.clone(),
127 attempt: second_claim.attempt,
128 state_version: second_claim.state_version,
129 pending_patch: State::new(),
130 };
131 let result = runtime.call_tool(
132 &second_ctx,
133 "mcp.github.create_pr",
134 state(&[
135 ("title", "Update runtime docs".into()),
136 ("_logical_operation", "docs-pr".into()),
137 ]),
138 )?;
139 second_ctx.write_state("pull_request", result);
140 runtime.store.commit_state_patch(
141 &run_id,
142 &second_ctx.step_id,
143 &second_ctx.lease_token,
144 second_ctx.state_version,
145 second_ctx.pending_patch,
146 )?;
147
148 let approvals = runtime.store.approval_requests(&run_id);
149 println!(
150 "{{\n \"run_id\": \"{}\",\n \"first_attempt_waited_for_approval\": true,\n \"second_attempt_ok\": true,\n \"approval_count\": {},\n \"approval_status\": \"{}\",\n \"external_action_count\": 1,\n \"final_state_has_pull_request\": {}\n}}",
151 run_id,
152 approvals.len(),
153 approvals.first().map(|row| row.status.as_str()).unwrap_or(""),
154 runtime
155 .store
156 .final_state(&run_id)?
157 .contains_key("pull_request")
158 );
159 Ok(())
160}Sourcepub fn approve_request(
&mut self,
approval_id: &str,
approver: &str,
reason: &str,
) -> Result<ApprovalRequest>
pub fn approve_request( &mut self, approval_id: &str, approver: &str, reason: &str, ) -> Result<ApprovalRequest>
Examples found in repository?
examples/mcp_governance.rs (line 118)
33fn main() -> agentledger::Result<()> {
34 let mut runtime = Runtime::new();
35 runtime.set_sandbox(Box::new(DemoSandboxExecutor));
36 let mut server = InMemoryMCPToolServer::new();
37 server.add_tool(
38 state(&[
39 ("name", "mcp.github.create_pr".into()),
40 (
41 "inputSchema",
42 Value::Object(state(&[
43 ("type", "object".into()),
44 (
45 "required",
46 Value::Array(vec![Value::String("title".to_string())]),
47 ),
48 (
49 "properties",
50 Value::Object(state(&[(
51 "title",
52 Value::Object(state(&[
53 ("type", "string".into()),
54 ("minLength", 1_i64.into()),
55 ])),
56 )])),
57 ),
58 ])),
59 ),
60 (
61 "annotations",
62 Value::Object(state(&[
63 ("side_effect", "external_write".into()),
64 ("risk_level", "high".into()),
65 ("idempotency_required", true.into()),
66 ("approval_required", true.into()),
67 ("sandbox_required", true.into()),
68 (
69 "sandbox_policy",
70 Value::Object(state(&[
71 ("network", "deny".into()),
72 ("filesystem", "read-only".into()),
73 ])),
74 ),
75 ])),
76 ),
77 ]),
78 create_pr,
79 );
80 let adapter = MCPToolAdapter { client_call: create_pr };
81 for descriptor in server.list_tools() {
82 runtime.register_tool(adapter.tool_spec_from_descriptor(&descriptor));
83 }
84
85 let (run_id, _) = runtime.create_run(State::new());
86 let first_claim = runtime.store.claim_step("worker-before-approval", &run_id, 60.0)?;
87 let first_ctx = AgentContext {
88 run_id: run_id.clone(),
89 session_id: first_claim.session_id.clone(),
90 step_id: first_claim.step_id.clone(),
91 agent_role: "MCPAgent".to_string(),
92 lease_token: first_claim.lease_token.clone(),
93 attempt: first_claim.attempt,
94 state_version: first_claim.state_version,
95 pending_patch: State::new(),
96 };
97 let first = runtime.call_tool(
98 &first_ctx,
99 "mcp.github.create_pr",
100 state(&[
101 ("title", "Update runtime docs".into()),
102 ("_logical_operation", "docs-pr".into()),
103 ]),
104 );
105 let approval_id = first
106 .unwrap_err()
107 .0
108 .trim_start_matches("approval required:")
109 .to_string();
110 runtime.store.mark_waiting_human(
111 &run_id,
112 &first_ctx.step_id,
113 "approval required for tool mcp.github.create_pr",
114 &approval_id,
115 );
116 runtime
117 .store
118 .approve_request(&approval_id, "maintainer", "demo approval")?;
119
120 let second_claim = runtime.store.claim_step("worker-after-approval", &run_id, 60.0)?;
121 let mut second_ctx = AgentContext {
122 run_id: run_id.clone(),
123 session_id: second_claim.session_id.clone(),
124 step_id: second_claim.step_id.clone(),
125 agent_role: "MCPAgent".to_string(),
126 lease_token: second_claim.lease_token.clone(),
127 attempt: second_claim.attempt,
128 state_version: second_claim.state_version,
129 pending_patch: State::new(),
130 };
131 let result = runtime.call_tool(
132 &second_ctx,
133 "mcp.github.create_pr",
134 state(&[
135 ("title", "Update runtime docs".into()),
136 ("_logical_operation", "docs-pr".into()),
137 ]),
138 )?;
139 second_ctx.write_state("pull_request", result);
140 runtime.store.commit_state_patch(
141 &run_id,
142 &second_ctx.step_id,
143 &second_ctx.lease_token,
144 second_ctx.state_version,
145 second_ctx.pending_patch,
146 )?;
147
148 let approvals = runtime.store.approval_requests(&run_id);
149 println!(
150 "{{\n \"run_id\": \"{}\",\n \"first_attempt_waited_for_approval\": true,\n \"second_attempt_ok\": true,\n \"approval_count\": {},\n \"approval_status\": \"{}\",\n \"external_action_count\": 1,\n \"final_state_has_pull_request\": {}\n}}",
151 run_id,
152 approvals.len(),
153 approvals.first().map(|row| row.status.as_str()).unwrap_or(""),
154 runtime
155 .store
156 .final_state(&run_id)?
157 .contains_key("pull_request")
158 );
159 Ok(())
160}More examples
examples/travel_assistant.rs (line 662)
425fn main() -> Result<(), Box<dyn std::error::Error>> {
426 let args: Vec<String> = std::env::args().collect();
427 let root = if args.len() > 1 {
428 PathBuf::from(&args[1])
429 } else {
430 std::env::temp_dir().join(format!("agentledger-rust-{}", std::process::id()))
431 };
432 let _ = fs::create_dir_all(&root);
433
434 // Intro
435 println!(
436 "\n{}{} ╔════════════════════════════════════════════════════╗{}",
437 C_BOLD, C_C, C_RST
438 );
439 println!(
440 "{}{} ║ AgentLedger Travel Assistant (Rust) — Interactive Demo ║{}",
441 C_BOLD, C_C, C_RST
442 );
443 println!(
444 "{}{} ║ See real database state at every step ║{}",
445 C_BOLD, C_C, C_RST
446 );
447 println!(
448 "{}{} ╚════════════════════════════════════════════════════╝{}",
449 C_BOLD, C_C, C_RST
450 );
451
452 println!(
453 "\n {}AgentLedger — Durable Execution Runtime for AI Agents{}",
454 C_DIM, C_RST
455 );
456 println!(" ┌────────────────────────────────────────────────────┐");
457 println!(
458 " │ {}✓{} Durable execution — crash recovery │",
459 C_G, C_RST
460 );
461 println!(
462 " │ {}✓{} Tool Ledger — idempotent replay │",
463 C_G, C_RST
464 );
465 println!(
466 " │ {}✓{} Approval gates — human-in-the-loop │",
467 C_G, C_RST
468 );
469 println!(
470 " │ {}✓{} Policy engine — risk-based access │",
471 C_G, C_RST
472 );
473 println!(
474 " │ {}✓{} Budget control — tool call limits │",
475 C_G, C_RST
476 );
477 println!(
478 " │ {}✓{} Evidence export — full audit trail │",
479 C_G, C_RST
480 );
481 println!(" └────────────────────────────────────────────────────┘");
482
483 wait("Press Enter to start / 按 Enter 开始");
484
485 // ════════════════════════════════════════════════════════
486 // Step 1: Setup
487 // ════════════════════════════════════════════════════════
488 println!(
489 "\n{}{}{}",
490 C_BOLD,
491 C_B,
492 "═".repeat(60)
493 );
494 println!(
495 "{}{} Step 1: Initialize — Register tools, configure policy{}",
496 C_BOLD, C_B, C_RST
497 );
498 println!(
499 "{}{}{}",
500 C_BOLD,
501 C_B,
502 "═".repeat(60)
503 );
504
505 let mut runtime = Runtime::new();
506 runtime.set_budget(BudgetLimits {
507 max_tool_calls: Some(25.0),
508 max_model_tokens: None,
509 max_total_usd: None,
510 });
511
512 // Register tools
513 runtime.register_tool(
514 ToolSpec::new("travel.search_flights", Box::new(search_flights))
515 .side_effect("none")
516 .risk_level("low")
517 .input_schema(Value::Object(make_state(&[
518 ("type", "object".into()),
519 (
520 "required",
521 Value::Array(vec!["from".into(), "to".into()]),
522 ),
523 ]))),
524 );
525 runtime.register_tool(
526 ToolSpec::new("travel.search_hotels", Box::new(search_hotels))
527 .side_effect("none")
528 .risk_level("low")
529 .input_schema(Value::Object(make_state(&[
530 ("type", "object".into()),
531 ("required", Value::Array(vec!["city".into()])),
532 ]))),
533 );
534 runtime.register_tool(
535 ToolSpec::new("travel.check_weather", Box::new(check_weather))
536 .side_effect("none")
537 .risk_level("low")
538 .input_schema(Value::Object(make_state(&[
539 ("type", "object".into()),
540 ("required", Value::Array(vec!["city".into()])),
541 ]))),
542 );
543 runtime.register_tool(
544 ToolSpec::new("travel.book_flight", Box::new(book_flight))
545 .side_effect("external_write")
546 .risk_level("high")
547 .idempotency_required(true)
548 .approval_required(true)
549 .input_schema(Value::Object(make_state(&[
550 ("type", "object".into()),
551 (
552 "required",
553 Value::Array(vec!["flight_id".into(), "passenger".into()]),
554 ),
555 ]))),
556 );
557 runtime.register_tool(
558 ToolSpec::new("travel.book_hotel", Box::new(book_hotel))
559 .side_effect("external_write")
560 .risk_level("high")
561 .idempotency_required(true)
562 .approval_required(true)
563 .input_schema(Value::Object(make_state(&[
564 ("type", "object".into()),
565 (
566 "required",
567 Value::Array(vec!["hotel_id".into(), "guest".into()]),
568 ),
569 ]))),
570 );
571
572 println!("\n {}Registered 5 tools:{}", C_C, C_RST);
573 for (name, risk, approval) in [
574 ("travel.search_flights", "low", false),
575 ("travel.search_hotels", "low", false),
576 ("travel.check_weather", "low", false),
577 ("travel.book_flight", "high", true),
578 ("travel.book_hotel", "high", true),
579 ] {
580 let rc = if risk == "low" { C_G } else { C_R };
581 let ac = if approval {
582 format!("{}needs approval{}", C_R, C_RST)
583 } else {
584 format!("{}no approval{}", C_G, C_RST)
585 };
586 println!(
587 " {}•{} {} [{}{}{}] [{}]",
588 C_DIM, C_RST, name, rc, risk, C_RST, ac
589 );
590 }
591 println!(
592 " {}Policy: Rust uses risk-based policy (low=allow, high=deny+approval){}",
593 C_DIM, C_RST
594 );
595 println!(" {}Budget: max 25 tool calls{}", C_DIM, C_RST);
596
597 let (run_id, _step_id) = runtime.create_run(make_state(&[
598 ("trip", "Tokyo".into()),
599 ("budget_usd", Value::Number(3000.0)),
600 ]));
601 println!(
602 "\n {}Run created: {}{}{}",
603 C_B, C_BOLD, run_id, C_RST
604 );
605 show_db(&runtime.store, &run_id);
606 wait("Press Enter to continue");
607
608 // ════════════════════════════════════════════════════════
609 // Step 2: Attempt 1 — Approval interception
610 // ════════════════════════════════════════════════════════
611 println!(
612 "\n{}{}{}",
613 C_BOLD,
614 C_R,
615 "═".repeat(60)
616 );
617 println!(
618 "{}{} Step 2: Attempt 1 — Agent runs → Approval triggered{}",
619 C_BOLD, C_R, C_RST
620 );
621 println!(
622 "{}{}{}",
623 C_BOLD,
624 C_R,
625 "═".repeat(60)
626 );
627 println!(
628 "\n {}Agent executing: search flights → search hotels → check weather → book flight...{}",
629 C_DIM, C_RST
630 );
631
632 {
633 let mut ctx = claim_context(&mut runtime, &run_id, "worker-rust", "TravelPlanner");
634 let result = travel_planner(&mut runtime, &mut ctx, 1);
635 match result {
636 Err(err) if err.0.starts_with("approval required:") => {
637 let approval_id = err.0.trim_start_matches("approval required:");
638 runtime
639 .store
640 .mark_waiting_human(&run_id, &ctx.step_id, &err.0, approval_id);
641 }
642 Err(err) => return Err(format!("unexpected error at step 2: {}", err.0).into()),
643 Ok(()) => {}
644 }
645 }
646
647 println!(
648 "\n {}book_flight triggered approval! Runtime paused, waiting for human.{}",
649 C_R, C_RST
650 );
651 show_db(&runtime.store, &run_id);
652 println!(
653 " {}Note: Tool Ledger has RESERVED entry, approval status is PENDING{}",
654 C_R, C_RST
655 );
656 wait("Press Enter to approve / 按 Enter 审批");
657
658 for req in runtime.store.approval_requests(&run_id) {
659 if req.status == "PENDING" {
660 runtime
661 .store
662 .approve_request(&req.approval_id, "traveler", "Within budget, approved")?;
663 println!(
664 "\n {}✅ Approved: {} — by traveler{}",
665 C_G, req.tool_name, C_RST
666 );
667 }
668 }
669 show_db(&runtime.store, &run_id);
670 wait("Press Enter to continue");
671
672 // ════════════════════════════════════════════════════════
673 // Step 3: Attempt 2 — Execute + Crash
674 // ════════════════════════════════════════════════════════
675 println!(
676 "\n{}{}{}",
677 C_BOLD,
678 C_Y,
679 "═".repeat(60)
680 );
681 println!(
682 "{}{} Step 3: Attempt 2 — Approved → Execute booking → Simulated crash{}",
683 C_BOLD, C_Y, C_RST
684 );
685 println!(
686 "{}{}{}",
687 C_BOLD,
688 C_Y,
689 "═".repeat(60)
690 );
691 println!(
692 "\n {}Re-running agent (approval passed, book_flight will execute)...{}",
693 C_DIM, C_RST
694 );
695
696 {
697 let mut ctx = claim_context(&mut runtime, &run_id, "worker-rust", "TravelPlanner");
698 let result = travel_planner(&mut runtime, &mut ctx, 2);
699 match result {
700 Err(err) if err.0 == "retryable" => {
701 runtime.store.mark_retry(
702 &run_id,
703 &ctx.step_id,
704 "RetryableAgentError",
705 "after flight booking",
706 );
707 }
708 Err(err) if err.0.starts_with("approval required:") => {
709 let approval_id = err.0.trim_start_matches("approval required:");
710 runtime
711 .store
712 .mark_waiting_human(&run_id, &ctx.step_id, &err.0, approval_id);
713 }
714 Err(err) => return Err(format!("unexpected error at step 3: {}", err.0).into()),
715 Ok(()) => {}
716 }
717 }
718
719 println!(
720 "\n {}Agent booked flight, then crashed before committing state!{}",
721 C_Y, C_RST
722 );
723 println!(
724 " {}Flight is booked in external system, but agent state was NOT persisted.{}",
725 C_Y, C_RST
726 );
727 show_db(&runtime.store, &run_id);
728 println!(
729 " {}Key: Tool Ledger book_flight status = {}SUCCEEDED{} (external side effect executed){}",
730 C_Y, C_G, C_Y, C_RST
731 );
732 println!(
733 " {} Step status = retry_scheduled (state not committed, waiting for retry){}",
734 C_Y, C_RST
735 );
736 wait("Press Enter to continue");
737
738 // ════════════════════════════════════════════════════════
739 // Step 4: Attempt 3 — Recovery + Hotel approval
740 // ════════════════════════════════════════════════════════
741 println!(
742 "\n{}{}{}",
743 C_BOLD,
744 C_G,
745 "═".repeat(60)
746 );
747 println!(
748 "{}{} Step 4: Attempt 3 — Crash recovery → Tool Ledger idempotent replay{}",
749 C_BOLD, C_G, C_RST
750 );
751 println!(
752 "{}{}{}",
753 C_BOLD,
754 C_G,
755 "═".repeat(60)
756 );
757 println!(
758 "\n {}Agent re-executes. book_flight: Tool Ledger sees SUCCEEDED record...{}",
759 C_DIM, C_RST
760 );
761 println!(
762 " {}{}→ Returns cached result, no duplicate API call, no double charge!{}",
763 C_DIM, C_G, C_RST
764 );
765
766 {
767 let mut ctx = claim_context(&mut runtime, &run_id, "worker-rust", "TravelPlanner");
768 let result = travel_planner(&mut runtime, &mut ctx, 3);
769 match result {
770 Err(err) if err.0.starts_with("approval required:") => {
771 let approval_id = err.0.trim_start_matches("approval required:");
772 runtime
773 .store
774 .mark_waiting_human(&run_id, &ctx.step_id, &err.0, approval_id);
775 }
776 Err(err) => return Err(format!("unexpected error at step 4: {}", err.0).into()),
777 Ok(()) => {}
778 }
779 }
780
781 println!(
782 "\n {}✅ Flight idempotent replay successful! (no duplicate _book_flight call){}",
783 C_G, C_RST
784 );
785 println!(" {}Hotel booking → triggers approval again{}", C_R, C_RST);
786
787 for req in runtime.store.approval_requests(&run_id) {
788 if req.status == "PENDING" {
789 runtime
790 .store
791 .approve_request(&req.approval_id, "traveler", "Hotel within budget, approved")?;
792 println!(
793 "\n {}✅ Approved: {} — by traveler{}",
794 C_G, req.tool_name, C_RST
795 );
796 }
797 }
798 show_db(&runtime.store, &run_id);
799 wait("Press Enter to continue");
800
801 // ════════════════════════════════════════════════════════
802 // Step 5: Attempt 4 — Complete
803 // ════════════════════════════════════════════════════════
804 println!(
805 "\n{}{}{}",
806 C_BOLD,
807 C_G,
808 "═".repeat(60)
809 );
810 println!(
811 "{}{} Step 5: Attempt 4 — Hotel approved → Full execution → State committed{}",
812 C_BOLD, C_G, C_RST
813 );
814 println!(
815 "{}{}{}",
816 C_BOLD,
817 C_G,
818 "═".repeat(60)
819 );
820
821 {
822 let mut ctx = claim_context(&mut runtime, &run_id, "worker-rust", "TravelPlanner");
823 travel_planner(&mut runtime, &mut ctx, 4)?;
824 runtime.store.commit_state_patch(
825 &run_id,
826 &ctx.step_id,
827 &ctx.lease_token,
828 ctx.state_version,
829 ctx.pending_patch,
830 )?;
831 }
832
833 {
834 let db = BOOKING_DB.lock().unwrap();
835 if db.len() != 2 {
836 return Err(format!("Expected 2 bookings, got {}", db.len()).into());
837 }
838 }
839
840 println!(
841 "\n {}✅ Travel planning complete! State persisted to database.{}",
842 C_G, C_RST
843 );
844 show_db(&runtime.store, &run_id);
845 println!(
846 " {}Step status = completed, State has bookings + trip_status{}",
847 C_G, C_RST
848 );
849 let db = BOOKING_DB.lock().unwrap();
850 let keys: Vec<String> = db.keys().cloned().collect();
851 println!(
852 " {}External bookings: {:?} ({} total, no duplicates){}",
853 C_G,
854 keys,
855 keys.len(),
856 C_RST
857 );
858 drop(db);
859 wait("Press Enter to continue");
860
861 // ════════════════════════════════════════════════════════
862 // Step 6: Evidence + Cost + Replay
863 // ════════════════════════════════════════════════════════
864 println!(
865 "\n{}{}{}",
866 C_BOLD,
867 C_M,
868 "═".repeat(60)
869 );
870 println!(
871 "{}{} Step 6: Evidence export + Cost attribution + Replay verification{}",
872 C_BOLD, C_M, C_RST
873 );
874 println!(
875 "{}{}{}",
876 C_BOLD,
877 C_M,
878 "═".repeat(60)
879 );
880
881 let bundle = export_evidence(&runtime.store, &run_id)?;
882 let replay_result = replay(&runtime.store, &run_id)?;
883 let cost = agentledger::cost_attribution(&runtime.store, &run_id);
884
885 println!(
886 "\n {}Cost attribution: {} tool calls{}",
887 C_M, cost.total.tool_calls, C_RST
888 );
889 println!(
890 " {}Replay: {} events, safe={}{}{}",
891 C_M, replay_result.event_count, C_G, replay_result.replay_safe, C_RST
892 );
893 println!(
894 " {}Evidence bundle: {} events total{}",
895 C_M,
896 bundle.events.len(),
897 C_RST
898 );
899
900 // Write evidence HTML
901 let html_path = root.join("evidence.html");
902 let html = debug_html(&bundle);
903 fs::write(&html_path, html)?;
904 let html_abs = html_path.canonicalize().unwrap_or_else(|_| html_path.clone());
905
906 // Final summary
907 println!(
908 "\n{}{}{}",
909 C_BOLD,
910 C_G,
911 "═".repeat(60)
912 );
913 println!(
914 "{}{} Summary: What AgentLedger (Rust) did in this demo{}",
915 C_BOLD, C_G, C_RST
916 );
917 println!(
918 "{}{}{}",
919 C_BOLD,
920 C_G,
921 "═".repeat(60)
922 );
923 println!(
924 "
925 ┌──────────────────────────────────────────────────────────┐
926 │ │
927 │ {g}✓ Durable execution{rst} Crash → auto retry, state preserved │
928 │ Step: retry_scheduled → completed │
929 │ │
930 │ {g}✓ Tool Ledger{rst} Idempotent replay, flight booked {bold}1x{rst} only │
931 │ SUCCEEDED → cached result on retry │
932 │ │
933 │ {g}✓ Approval gates{rst} Flight + hotel each trigger approval │
934 │ approval_requests records in store │
935 │ │
936 │ {g}✓ Policy engine{rst} Risk-based policy (high → deny+approval) │
937 │ low-risk tools auto-allowed │
938 │ │
939 │ {g}✓ Budget control{rst} Tracked {tc} tool calls │
940 │ BudgetController.before_tool_call() │
941 │ │
942 │ {g}✓ Evidence export{rst} {ec} events recorded │
943 │ events stored in memory store │
944 │ │
945 │ {g}✓ Cost attribution{rst} Auto-recorded per run │
946 │ CostAttribution by agent │
947 │ │
948 │ {g}✓ Replay engine{rst} Event hash verification passed │
949 │ Verify history without re-running │
950 │ │
951 └──────────────────────────────────────────────────────────┘
952",
953 g = C_G,
954 rst = C_RST,
955 bold = C_BOLD,
956 tc = cost.total.tool_calls,
957 ec = bundle.events.len()
958 );
959
960 println!(" {}Storage: in-memory (MemoryStore){}", C_DIM, C_RST);
961 println!(" {}Evidence HTML: {:?}{}", C_DIM, html_abs, C_RST);
962 println!();
963
964 Ok(())
965}pub fn deny_request( &mut self, approval_id: &str, approver: &str, reason: &str, ) -> Result<ApprovalRequest>
pub fn record_cost( &mut self, run_id: &str, session_id: &str, step_id: &str, category: &str, name: &str, amount: f64, unit: &str, metadata: State, ) -> String
pub fn cost_records(&self, run_id: &str) -> Vec<CostRecord>
pub fn cost_summary(&self, run_id: &str) -> CostSummary
pub fn create_artifact( &mut self, run_id: &str, step_id: Option<&str>, name: &str, content: State, metadata: State, ) -> Artifact
pub fn artifacts(&self, run_id: &str) -> Vec<Artifact>
pub fn validate_lease(&self, step_id: &str, lease_token: &str) -> Result<()>
Sourcepub fn final_state(&self, run_id: &str) -> Result<State>
pub fn final_state(&self, run_id: &str) -> Result<State>
Examples found in repository?
examples/three_minute_demo.rs (line 101)
20fn main() -> agentledger::Result<()> {
21 let mut runtime = Runtime::new();
22 runtime.register_tool(
23 ToolSpec::new(
24 "ticket.create",
25 Box::new(|args| {
26 Ok(Value::Object(state(&[
27 ("external_id", "TICKET-1".into()),
28 ("title", args["title"].clone()),
29 ])))
30 }),
31 )
32 .side_effect("external_write")
33 .idempotency_required(true)
34 .input_schema(Value::Object(state(&[
35 ("type", "object".into()),
36 (
37 "required",
38 Value::Array(vec![Value::String("title".to_string())]),
39 ),
40 ]))),
41 );
42 let (run_id, _) = runtime.create_run(State::new());
43
44 let first_claim = runtime.store.claim_step("worker-before-crash", &run_id, 60.0)?;
45 let first_ctx = AgentContext {
46 run_id: run_id.clone(),
47 session_id: first_claim.session_id.clone(),
48 step_id: first_claim.step_id.clone(),
49 agent_role: "SupportAgent".to_string(),
50 lease_token: first_claim.lease_token.clone(),
51 attempt: first_claim.attempt,
52 state_version: first_claim.state_version,
53 pending_patch: State::new(),
54 };
55 let ticket = runtime.call_tool(
56 &first_ctx,
57 "ticket.create",
58 state(&[
59 ("title", "Investigate failed payment".into()),
60 ("_logical_operation", "open-payment-ticket".into()),
61 ]),
62 )?;
63 runtime.store.mark_retry(
64 &run_id,
65 &first_ctx.step_id,
66 "RetryableAgentError",
67 "after external ticket create, before state commit",
68 );
69
70 let second_claim = runtime.store.claim_step("worker-after-restart", &run_id, 60.0)?;
71 let mut second_ctx = AgentContext {
72 run_id: run_id.clone(),
73 session_id: second_claim.session_id.clone(),
74 step_id: second_claim.step_id.clone(),
75 agent_role: "SupportAgent".to_string(),
76 lease_token: second_claim.lease_token.clone(),
77 attempt: second_claim.attempt,
78 state_version: second_claim.state_version,
79 pending_patch: State::new(),
80 };
81 let replayed_ticket = runtime.call_tool(
82 &second_ctx,
83 "ticket.create",
84 state(&[
85 ("title", "Investigate failed payment".into()),
86 ("_logical_operation", "open-payment-ticket".into()),
87 ]),
88 )?;
89 second_ctx.write_state("ticket", replayed_ticket);
90 second_ctx.write_state("recovered", true.into());
91 runtime.store.commit_state_patch(
92 &run_id,
93 &second_ctx.step_id,
94 &second_ctx.lease_token,
95 second_ctx.state_version,
96 second_ctx.pending_patch,
97 )?;
98
99 let summary = replay(&runtime.store, &run_id)?;
100 let ledger = runtime.store.ledger(&run_id);
101 let final_state = runtime.store.final_state(&run_id)?;
102 println!(
103 "{{\n \"run_id\": \"{}\",\n \"first_attempt_ok\": false,\n \"second_attempt_ok\": true,\n \"external_ticket_count\": 1,\n \"actual_tool_executions\": 1,\n \"tool_ledger_count\": {},\n \"tool_ledger_status\": \"{}\",\n \"first_ticket_external_id\": \"{}\",\n \"final_state_recovered\": {},\n \"replay\": {{\n \"safe\": {},\n \"event_count\": {},\n \"tool_call_count\": {}\n }}\n}}",
104 run_id,
105 ledger.len(),
106 ledger.first().map(|row| row.status.as_str()).unwrap_or(""),
107 get_string(&ticket, "external_id"),
108 final_state.get("recovered") == Some(&Value::Bool(true)),
109 summary.replay_safe,
110 summary.event_count,
111 summary.tool_call_count
112 );
113 Ok(())
114}More examples
examples/mcp_governance.rs (line 156)
33fn main() -> agentledger::Result<()> {
34 let mut runtime = Runtime::new();
35 runtime.set_sandbox(Box::new(DemoSandboxExecutor));
36 let mut server = InMemoryMCPToolServer::new();
37 server.add_tool(
38 state(&[
39 ("name", "mcp.github.create_pr".into()),
40 (
41 "inputSchema",
42 Value::Object(state(&[
43 ("type", "object".into()),
44 (
45 "required",
46 Value::Array(vec![Value::String("title".to_string())]),
47 ),
48 (
49 "properties",
50 Value::Object(state(&[(
51 "title",
52 Value::Object(state(&[
53 ("type", "string".into()),
54 ("minLength", 1_i64.into()),
55 ])),
56 )])),
57 ),
58 ])),
59 ),
60 (
61 "annotations",
62 Value::Object(state(&[
63 ("side_effect", "external_write".into()),
64 ("risk_level", "high".into()),
65 ("idempotency_required", true.into()),
66 ("approval_required", true.into()),
67 ("sandbox_required", true.into()),
68 (
69 "sandbox_policy",
70 Value::Object(state(&[
71 ("network", "deny".into()),
72 ("filesystem", "read-only".into()),
73 ])),
74 ),
75 ])),
76 ),
77 ]),
78 create_pr,
79 );
80 let adapter = MCPToolAdapter { client_call: create_pr };
81 for descriptor in server.list_tools() {
82 runtime.register_tool(adapter.tool_spec_from_descriptor(&descriptor));
83 }
84
85 let (run_id, _) = runtime.create_run(State::new());
86 let first_claim = runtime.store.claim_step("worker-before-approval", &run_id, 60.0)?;
87 let first_ctx = AgentContext {
88 run_id: run_id.clone(),
89 session_id: first_claim.session_id.clone(),
90 step_id: first_claim.step_id.clone(),
91 agent_role: "MCPAgent".to_string(),
92 lease_token: first_claim.lease_token.clone(),
93 attempt: first_claim.attempt,
94 state_version: first_claim.state_version,
95 pending_patch: State::new(),
96 };
97 let first = runtime.call_tool(
98 &first_ctx,
99 "mcp.github.create_pr",
100 state(&[
101 ("title", "Update runtime docs".into()),
102 ("_logical_operation", "docs-pr".into()),
103 ]),
104 );
105 let approval_id = first
106 .unwrap_err()
107 .0
108 .trim_start_matches("approval required:")
109 .to_string();
110 runtime.store.mark_waiting_human(
111 &run_id,
112 &first_ctx.step_id,
113 "approval required for tool mcp.github.create_pr",
114 &approval_id,
115 );
116 runtime
117 .store
118 .approve_request(&approval_id, "maintainer", "demo approval")?;
119
120 let second_claim = runtime.store.claim_step("worker-after-approval", &run_id, 60.0)?;
121 let mut second_ctx = AgentContext {
122 run_id: run_id.clone(),
123 session_id: second_claim.session_id.clone(),
124 step_id: second_claim.step_id.clone(),
125 agent_role: "MCPAgent".to_string(),
126 lease_token: second_claim.lease_token.clone(),
127 attempt: second_claim.attempt,
128 state_version: second_claim.state_version,
129 pending_patch: State::new(),
130 };
131 let result = runtime.call_tool(
132 &second_ctx,
133 "mcp.github.create_pr",
134 state(&[
135 ("title", "Update runtime docs".into()),
136 ("_logical_operation", "docs-pr".into()),
137 ]),
138 )?;
139 second_ctx.write_state("pull_request", result);
140 runtime.store.commit_state_patch(
141 &run_id,
142 &second_ctx.step_id,
143 &second_ctx.lease_token,
144 second_ctx.state_version,
145 second_ctx.pending_patch,
146 )?;
147
148 let approvals = runtime.store.approval_requests(&run_id);
149 println!(
150 "{{\n \"run_id\": \"{}\",\n \"first_attempt_waited_for_approval\": true,\n \"second_attempt_ok\": true,\n \"approval_count\": {},\n \"approval_status\": \"{}\",\n \"external_action_count\": 1,\n \"final_state_has_pull_request\": {}\n}}",
151 run_id,
152 approvals.len(),
153 approvals.first().map(|row| row.status.as_str()).unwrap_or(""),
154 runtime
155 .store
156 .final_state(&run_id)?
157 .contains_key("pull_request")
158 );
159 Ok(())
160}examples/omp_bridge.rs (line 108)
13fn main() -> agentledger::Result<()> {
14 let runtime = Runtime::new();
15 let mut bridge = OmpLedgerBridge::new(runtime, "omp-demo");
16 let run_id = bridge.record_session_started(OmpSession {
17 session_id: "omp-session-1".to_string(),
18 initial_state: state(&[("task", "review contract".into())]),
19 metadata: state(&[("runtime", "synthetic-omp".into())]),
20 ..Default::default()
21 })?;
22 bridge.record_turn_started(OmpTurn {
23 session_id: "omp-session-1".to_string(),
24 turn_id: "turn-1".to_string(),
25 agent_role: "OMPPlanner".to_string(),
26 metadata: state(&[("phase", "planning".into())]),
27 ..Default::default()
28 })?;
29 bridge.record_model_call(OmpModelCall {
30 session_id: "omp-session-1".to_string(),
31 turn_id: "turn-1".to_string(),
32 provider: "openai-compatible-gateway".to_string(),
33 model: "legal-router".to_string(),
34 request: state(&[(
35 "messages",
36 Value::Array(vec![Value::Object(state(&[
37 ("role", "user".into()),
38 ("content", "find payment clause".into()),
39 ]))]),
40 )]),
41 response: state(&[(
42 "tool_calls",
43 Value::Array(vec![Value::Object(state(&[
44 ("name", "contract.search".into()),
45 (
46 "arguments",
47 Value::Object(state(&[("clause", "payment".into())])),
48 ),
49 ]))]),
50 )]),
51 usage: state(&[
52 ("input_tokens", Value::Number(12.0)),
53 ("output_tokens", Value::Number(7.0)),
54 ("total_tokens", Value::Number(19.0)),
55 ]),
56 total_usd: 0.003,
57 ..Default::default()
58 })?;
59 bridge.record_tool_proposal(OmpToolProposal {
60 session_id: "omp-session-1".to_string(),
61 turn_id: "turn-1".to_string(),
62 tool_name: "contract.search".to_string(),
63 arguments: state(&[("clause", "payment".into())]),
64 provider: Some("openai-compatible-gateway".to_string()),
65 model: Some("legal-router".to_string()),
66 reason: Some("model proposed a contract search".to_string()),
67 ..Default::default()
68 })?;
69 bridge.record_tool_execution(OmpToolExecution {
70 session_id: "omp-session-1".to_string(),
71 turn_id: "turn-1".to_string(),
72 tool_name: "contract.search".to_string(),
73 arguments: state(&[("clause", "payment".into())]),
74 result: Some(Value::Object(state(&[
75 ("matches", Value::Array(vec!["Section 9.2".into()])),
76 ("external_id", "search-001".into()),
77 ]))),
78 ledger_status: Some("SUCCEEDED".to_string()),
79 ..Default::default()
80 })?;
81 bridge.record_state_change(OmpStateChange {
82 session_id: "omp-session-1".to_string(),
83 turn_id: Some("turn-1".to_string()),
84 reason: "persist normalized runtime-adjacent state".to_string(),
85 patch: state(&[("memory_version", Value::Number(1.0))]),
86 before_snapshot: Some(Value::Object(state(&[("memory_version", Value::Number(0.0))]))),
87 after_snapshot: Some(Value::Object(state(&[("memory_version", Value::Number(1.0))]))),
88 diff: Some(Value::Object(state(&[(
89 "memory_version",
90 Value::Array(vec![Value::Number(0.0), Value::Number(1.0)]),
91 )]))),
92 ..Default::default()
93 })?;
94 bridge.record_turn_completed(OmpTurn {
95 session_id: "omp-session-1".to_string(),
96 turn_id: "turn-1".to_string(),
97 state_patch: state(&[("last_tool", "contract.search".into())]),
98 ..Default::default()
99 })?;
100
101 let events = bridge
102 .runtime
103 .store
104 .events(&run_id)
105 .iter()
106 .map(|event| event.event_type.clone())
107 .collect::<Vec<_>>();
108 let final_state = bridge.runtime.store.final_state(&run_id)?;
109 let rendered_events = format!("{:?}", events);
110 let memory_version = match final_state.get("memory_version") {
111 Some(Value::Number(value)) => *value,
112 _ => 0.0,
113 };
114 let last_tool = match final_state.get("last_tool") {
115 Some(Value::String(value)) => value.as_str(),
116 _ => "",
117 };
118 println!(
119 concat!(
120 "{{\n",
121 " \"run_id\": \"{}\",\n",
122 " \"events\": {},\n",
123 " \"tool_ledger_count\": {},\n",
124 " \"final_state_memory_version\": {},\n",
125 " \"final_state_last_tool\": \"{}\"\n",
126 "}}"
127 ),
128 run_id,
129 rendered_events,
130 bridge.runtime.store.ledger(&run_id).len(),
131 memory_version,
132 last_tool
133 );
134 Ok(())
135}Sourcepub fn run(&self, run_id: &str) -> Result<Run>
pub fn run(&self, run_id: &str) -> Result<Run>
Examples found in repository?
examples/travel_assistant.rs (line 350)
347fn show_db(store: &MemoryStore, run_id: &str) {
348 // Runs
349 let mut run_rows = Vec::new();
350 if let Ok(run) = store.run(run_id) {
351 let short_id = if run.run_id.len() > 24 {
352 format!("{}...", &run.run_id[..24])
353 } else {
354 run.run_id.clone()
355 };
356 run_rows.push(vec![short_id, run.status, run.state_version.to_string()]);
357 }
358 show_rows("Runs", &["run_id", "status", "state_version"], &run_rows, C_B);
359
360 // Steps
361 let step_rows: Vec<Vec<String>> = store
362 .steps(run_id)
363 .into_iter()
364 .map(|s| {
365 let short_id = if s.step_id.len() > 24 {
366 format!("{}...", &s.step_id[..24])
367 } else {
368 s.step_id.clone()
369 };
370 vec![short_id, s.status, s.attempt.to_string()]
371 })
372 .collect();
373 show_rows("Steps", &["step_id", "status", "attempt"], &step_rows, C_B);
374
375 // Tool Ledger
376 let ledger_rows: Vec<Vec<String>> = store
377 .ledger(run_id)
378 .into_iter()
379 .map(|tl| {
380 let key = &tl.idempotency_key;
381 let short_key = match key.rfind(':') {
382 Some(idx) => {
383 let prev = key[..idx].rfind(':').unwrap_or(0);
384 if prev > 0 {
385 format!("{}:{}", &key[prev + 1..idx], &key[idx + 1..])
386 } else {
387 format!("{}", &key[idx + 1..])
388 }
389 }
390 None => key[..key.len().min(25)].to_string(),
391 };
392 vec![tl.tool_name, tl.status, short_key]
393 })
394 .collect();
395 show_rows(
396 "Tool Ledger",
397 &["tool", "status", "idemp_key"],
398 &ledger_rows,
399 C_Y,
400 );
401
402 // Approval requests
403 let approval_rows: Vec<Vec<String>> = store
404 .approval_requests(run_id)
405 .into_iter()
406 .map(|a| {
407 let approved_by = a.approved_by.unwrap_or_else(|| "-".to_string());
408 vec![a.tool_name, a.status, approved_by]
409 })
410 .collect();
411 show_rows(
412 "Approval Requests",
413 &["tool", "status", "approved_by"],
414 &approval_rows,
415 C_R,
416 );
417
418 println!();
419}Sourcepub fn steps(&self, run_id: &str) -> Vec<Step>
pub fn steps(&self, run_id: &str) -> Vec<Step>
Examples found in repository?
examples/travel_assistant.rs (line 362)
347fn show_db(store: &MemoryStore, run_id: &str) {
348 // Runs
349 let mut run_rows = Vec::new();
350 if let Ok(run) = store.run(run_id) {
351 let short_id = if run.run_id.len() > 24 {
352 format!("{}...", &run.run_id[..24])
353 } else {
354 run.run_id.clone()
355 };
356 run_rows.push(vec![short_id, run.status, run.state_version.to_string()]);
357 }
358 show_rows("Runs", &["run_id", "status", "state_version"], &run_rows, C_B);
359
360 // Steps
361 let step_rows: Vec<Vec<String>> = store
362 .steps(run_id)
363 .into_iter()
364 .map(|s| {
365 let short_id = if s.step_id.len() > 24 {
366 format!("{}...", &s.step_id[..24])
367 } else {
368 s.step_id.clone()
369 };
370 vec![short_id, s.status, s.attempt.to_string()]
371 })
372 .collect();
373 show_rows("Steps", &["step_id", "status", "attempt"], &step_rows, C_B);
374
375 // Tool Ledger
376 let ledger_rows: Vec<Vec<String>> = store
377 .ledger(run_id)
378 .into_iter()
379 .map(|tl| {
380 let key = &tl.idempotency_key;
381 let short_key = match key.rfind(':') {
382 Some(idx) => {
383 let prev = key[..idx].rfind(':').unwrap_or(0);
384 if prev > 0 {
385 format!("{}:{}", &key[prev + 1..idx], &key[idx + 1..])
386 } else {
387 format!("{}", &key[idx + 1..])
388 }
389 }
390 None => key[..key.len().min(25)].to_string(),
391 };
392 vec![tl.tool_name, tl.status, short_key]
393 })
394 .collect();
395 show_rows(
396 "Tool Ledger",
397 &["tool", "status", "idemp_key"],
398 &ledger_rows,
399 C_Y,
400 );
401
402 // Approval requests
403 let approval_rows: Vec<Vec<String>> = store
404 .approval_requests(run_id)
405 .into_iter()
406 .map(|a| {
407 let approved_by = a.approved_by.unwrap_or_else(|| "-".to_string());
408 vec![a.tool_name, a.status, approved_by]
409 })
410 .collect();
411 show_rows(
412 "Approval Requests",
413 &["tool", "status", "approved_by"],
414 &approval_rows,
415 C_R,
416 );
417
418 println!();
419}Sourcepub fn events(&self, run_id: &str) -> Vec<Event>
pub fn events(&self, run_id: &str) -> Vec<Event>
Examples found in repository?
examples/omp_bridge.rs (line 104)
13fn main() -> agentledger::Result<()> {
14 let runtime = Runtime::new();
15 let mut bridge = OmpLedgerBridge::new(runtime, "omp-demo");
16 let run_id = bridge.record_session_started(OmpSession {
17 session_id: "omp-session-1".to_string(),
18 initial_state: state(&[("task", "review contract".into())]),
19 metadata: state(&[("runtime", "synthetic-omp".into())]),
20 ..Default::default()
21 })?;
22 bridge.record_turn_started(OmpTurn {
23 session_id: "omp-session-1".to_string(),
24 turn_id: "turn-1".to_string(),
25 agent_role: "OMPPlanner".to_string(),
26 metadata: state(&[("phase", "planning".into())]),
27 ..Default::default()
28 })?;
29 bridge.record_model_call(OmpModelCall {
30 session_id: "omp-session-1".to_string(),
31 turn_id: "turn-1".to_string(),
32 provider: "openai-compatible-gateway".to_string(),
33 model: "legal-router".to_string(),
34 request: state(&[(
35 "messages",
36 Value::Array(vec![Value::Object(state(&[
37 ("role", "user".into()),
38 ("content", "find payment clause".into()),
39 ]))]),
40 )]),
41 response: state(&[(
42 "tool_calls",
43 Value::Array(vec![Value::Object(state(&[
44 ("name", "contract.search".into()),
45 (
46 "arguments",
47 Value::Object(state(&[("clause", "payment".into())])),
48 ),
49 ]))]),
50 )]),
51 usage: state(&[
52 ("input_tokens", Value::Number(12.0)),
53 ("output_tokens", Value::Number(7.0)),
54 ("total_tokens", Value::Number(19.0)),
55 ]),
56 total_usd: 0.003,
57 ..Default::default()
58 })?;
59 bridge.record_tool_proposal(OmpToolProposal {
60 session_id: "omp-session-1".to_string(),
61 turn_id: "turn-1".to_string(),
62 tool_name: "contract.search".to_string(),
63 arguments: state(&[("clause", "payment".into())]),
64 provider: Some("openai-compatible-gateway".to_string()),
65 model: Some("legal-router".to_string()),
66 reason: Some("model proposed a contract search".to_string()),
67 ..Default::default()
68 })?;
69 bridge.record_tool_execution(OmpToolExecution {
70 session_id: "omp-session-1".to_string(),
71 turn_id: "turn-1".to_string(),
72 tool_name: "contract.search".to_string(),
73 arguments: state(&[("clause", "payment".into())]),
74 result: Some(Value::Object(state(&[
75 ("matches", Value::Array(vec!["Section 9.2".into()])),
76 ("external_id", "search-001".into()),
77 ]))),
78 ledger_status: Some("SUCCEEDED".to_string()),
79 ..Default::default()
80 })?;
81 bridge.record_state_change(OmpStateChange {
82 session_id: "omp-session-1".to_string(),
83 turn_id: Some("turn-1".to_string()),
84 reason: "persist normalized runtime-adjacent state".to_string(),
85 patch: state(&[("memory_version", Value::Number(1.0))]),
86 before_snapshot: Some(Value::Object(state(&[("memory_version", Value::Number(0.0))]))),
87 after_snapshot: Some(Value::Object(state(&[("memory_version", Value::Number(1.0))]))),
88 diff: Some(Value::Object(state(&[(
89 "memory_version",
90 Value::Array(vec![Value::Number(0.0), Value::Number(1.0)]),
91 )]))),
92 ..Default::default()
93 })?;
94 bridge.record_turn_completed(OmpTurn {
95 session_id: "omp-session-1".to_string(),
96 turn_id: "turn-1".to_string(),
97 state_patch: state(&[("last_tool", "contract.search".into())]),
98 ..Default::default()
99 })?;
100
101 let events = bridge
102 .runtime
103 .store
104 .events(&run_id)
105 .iter()
106 .map(|event| event.event_type.clone())
107 .collect::<Vec<_>>();
108 let final_state = bridge.runtime.store.final_state(&run_id)?;
109 let rendered_events = format!("{:?}", events);
110 let memory_version = match final_state.get("memory_version") {
111 Some(Value::Number(value)) => *value,
112 _ => 0.0,
113 };
114 let last_tool = match final_state.get("last_tool") {
115 Some(Value::String(value)) => value.as_str(),
116 _ => "",
117 };
118 println!(
119 concat!(
120 "{{\n",
121 " \"run_id\": \"{}\",\n",
122 " \"events\": {},\n",
123 " \"tool_ledger_count\": {},\n",
124 " \"final_state_memory_version\": {},\n",
125 " \"final_state_last_tool\": \"{}\"\n",
126 "}}"
127 ),
128 run_id,
129 rendered_events,
130 bridge.runtime.store.ledger(&run_id).len(),
131 memory_version,
132 last_tool
133 );
134 Ok(())
135}Sourcepub fn ledger(&self, run_id: &str) -> Vec<ToolLedgerEntry>
pub fn ledger(&self, run_id: &str) -> Vec<ToolLedgerEntry>
Examples found in repository?
examples/travel_assistant.rs (line 377)
347fn show_db(store: &MemoryStore, run_id: &str) {
348 // Runs
349 let mut run_rows = Vec::new();
350 if let Ok(run) = store.run(run_id) {
351 let short_id = if run.run_id.len() > 24 {
352 format!("{}...", &run.run_id[..24])
353 } else {
354 run.run_id.clone()
355 };
356 run_rows.push(vec![short_id, run.status, run.state_version.to_string()]);
357 }
358 show_rows("Runs", &["run_id", "status", "state_version"], &run_rows, C_B);
359
360 // Steps
361 let step_rows: Vec<Vec<String>> = store
362 .steps(run_id)
363 .into_iter()
364 .map(|s| {
365 let short_id = if s.step_id.len() > 24 {
366 format!("{}...", &s.step_id[..24])
367 } else {
368 s.step_id.clone()
369 };
370 vec![short_id, s.status, s.attempt.to_string()]
371 })
372 .collect();
373 show_rows("Steps", &["step_id", "status", "attempt"], &step_rows, C_B);
374
375 // Tool Ledger
376 let ledger_rows: Vec<Vec<String>> = store
377 .ledger(run_id)
378 .into_iter()
379 .map(|tl| {
380 let key = &tl.idempotency_key;
381 let short_key = match key.rfind(':') {
382 Some(idx) => {
383 let prev = key[..idx].rfind(':').unwrap_or(0);
384 if prev > 0 {
385 format!("{}:{}", &key[prev + 1..idx], &key[idx + 1..])
386 } else {
387 format!("{}", &key[idx + 1..])
388 }
389 }
390 None => key[..key.len().min(25)].to_string(),
391 };
392 vec![tl.tool_name, tl.status, short_key]
393 })
394 .collect();
395 show_rows(
396 "Tool Ledger",
397 &["tool", "status", "idemp_key"],
398 &ledger_rows,
399 C_Y,
400 );
401
402 // Approval requests
403 let approval_rows: Vec<Vec<String>> = store
404 .approval_requests(run_id)
405 .into_iter()
406 .map(|a| {
407 let approved_by = a.approved_by.unwrap_or_else(|| "-".to_string());
408 vec![a.tool_name, a.status, approved_by]
409 })
410 .collect();
411 show_rows(
412 "Approval Requests",
413 &["tool", "status", "approved_by"],
414 &approval_rows,
415 C_R,
416 );
417
418 println!();
419}More examples
examples/three_minute_demo.rs (line 100)
20fn main() -> agentledger::Result<()> {
21 let mut runtime = Runtime::new();
22 runtime.register_tool(
23 ToolSpec::new(
24 "ticket.create",
25 Box::new(|args| {
26 Ok(Value::Object(state(&[
27 ("external_id", "TICKET-1".into()),
28 ("title", args["title"].clone()),
29 ])))
30 }),
31 )
32 .side_effect("external_write")
33 .idempotency_required(true)
34 .input_schema(Value::Object(state(&[
35 ("type", "object".into()),
36 (
37 "required",
38 Value::Array(vec![Value::String("title".to_string())]),
39 ),
40 ]))),
41 );
42 let (run_id, _) = runtime.create_run(State::new());
43
44 let first_claim = runtime.store.claim_step("worker-before-crash", &run_id, 60.0)?;
45 let first_ctx = AgentContext {
46 run_id: run_id.clone(),
47 session_id: first_claim.session_id.clone(),
48 step_id: first_claim.step_id.clone(),
49 agent_role: "SupportAgent".to_string(),
50 lease_token: first_claim.lease_token.clone(),
51 attempt: first_claim.attempt,
52 state_version: first_claim.state_version,
53 pending_patch: State::new(),
54 };
55 let ticket = runtime.call_tool(
56 &first_ctx,
57 "ticket.create",
58 state(&[
59 ("title", "Investigate failed payment".into()),
60 ("_logical_operation", "open-payment-ticket".into()),
61 ]),
62 )?;
63 runtime.store.mark_retry(
64 &run_id,
65 &first_ctx.step_id,
66 "RetryableAgentError",
67 "after external ticket create, before state commit",
68 );
69
70 let second_claim = runtime.store.claim_step("worker-after-restart", &run_id, 60.0)?;
71 let mut second_ctx = AgentContext {
72 run_id: run_id.clone(),
73 session_id: second_claim.session_id.clone(),
74 step_id: second_claim.step_id.clone(),
75 agent_role: "SupportAgent".to_string(),
76 lease_token: second_claim.lease_token.clone(),
77 attempt: second_claim.attempt,
78 state_version: second_claim.state_version,
79 pending_patch: State::new(),
80 };
81 let replayed_ticket = runtime.call_tool(
82 &second_ctx,
83 "ticket.create",
84 state(&[
85 ("title", "Investigate failed payment".into()),
86 ("_logical_operation", "open-payment-ticket".into()),
87 ]),
88 )?;
89 second_ctx.write_state("ticket", replayed_ticket);
90 second_ctx.write_state("recovered", true.into());
91 runtime.store.commit_state_patch(
92 &run_id,
93 &second_ctx.step_id,
94 &second_ctx.lease_token,
95 second_ctx.state_version,
96 second_ctx.pending_patch,
97 )?;
98
99 let summary = replay(&runtime.store, &run_id)?;
100 let ledger = runtime.store.ledger(&run_id);
101 let final_state = runtime.store.final_state(&run_id)?;
102 println!(
103 "{{\n \"run_id\": \"{}\",\n \"first_attempt_ok\": false,\n \"second_attempt_ok\": true,\n \"external_ticket_count\": 1,\n \"actual_tool_executions\": 1,\n \"tool_ledger_count\": {},\n \"tool_ledger_status\": \"{}\",\n \"first_ticket_external_id\": \"{}\",\n \"final_state_recovered\": {},\n \"replay\": {{\n \"safe\": {},\n \"event_count\": {},\n \"tool_call_count\": {}\n }}\n}}",
104 run_id,
105 ledger.len(),
106 ledger.first().map(|row| row.status.as_str()).unwrap_or(""),
107 get_string(&ticket, "external_id"),
108 final_state.get("recovered") == Some(&Value::Bool(true)),
109 summary.replay_safe,
110 summary.event_count,
111 summary.tool_call_count
112 );
113 Ok(())
114}examples/omp_bridge.rs (line 130)
13fn main() -> agentledger::Result<()> {
14 let runtime = Runtime::new();
15 let mut bridge = OmpLedgerBridge::new(runtime, "omp-demo");
16 let run_id = bridge.record_session_started(OmpSession {
17 session_id: "omp-session-1".to_string(),
18 initial_state: state(&[("task", "review contract".into())]),
19 metadata: state(&[("runtime", "synthetic-omp".into())]),
20 ..Default::default()
21 })?;
22 bridge.record_turn_started(OmpTurn {
23 session_id: "omp-session-1".to_string(),
24 turn_id: "turn-1".to_string(),
25 agent_role: "OMPPlanner".to_string(),
26 metadata: state(&[("phase", "planning".into())]),
27 ..Default::default()
28 })?;
29 bridge.record_model_call(OmpModelCall {
30 session_id: "omp-session-1".to_string(),
31 turn_id: "turn-1".to_string(),
32 provider: "openai-compatible-gateway".to_string(),
33 model: "legal-router".to_string(),
34 request: state(&[(
35 "messages",
36 Value::Array(vec![Value::Object(state(&[
37 ("role", "user".into()),
38 ("content", "find payment clause".into()),
39 ]))]),
40 )]),
41 response: state(&[(
42 "tool_calls",
43 Value::Array(vec![Value::Object(state(&[
44 ("name", "contract.search".into()),
45 (
46 "arguments",
47 Value::Object(state(&[("clause", "payment".into())])),
48 ),
49 ]))]),
50 )]),
51 usage: state(&[
52 ("input_tokens", Value::Number(12.0)),
53 ("output_tokens", Value::Number(7.0)),
54 ("total_tokens", Value::Number(19.0)),
55 ]),
56 total_usd: 0.003,
57 ..Default::default()
58 })?;
59 bridge.record_tool_proposal(OmpToolProposal {
60 session_id: "omp-session-1".to_string(),
61 turn_id: "turn-1".to_string(),
62 tool_name: "contract.search".to_string(),
63 arguments: state(&[("clause", "payment".into())]),
64 provider: Some("openai-compatible-gateway".to_string()),
65 model: Some("legal-router".to_string()),
66 reason: Some("model proposed a contract search".to_string()),
67 ..Default::default()
68 })?;
69 bridge.record_tool_execution(OmpToolExecution {
70 session_id: "omp-session-1".to_string(),
71 turn_id: "turn-1".to_string(),
72 tool_name: "contract.search".to_string(),
73 arguments: state(&[("clause", "payment".into())]),
74 result: Some(Value::Object(state(&[
75 ("matches", Value::Array(vec!["Section 9.2".into()])),
76 ("external_id", "search-001".into()),
77 ]))),
78 ledger_status: Some("SUCCEEDED".to_string()),
79 ..Default::default()
80 })?;
81 bridge.record_state_change(OmpStateChange {
82 session_id: "omp-session-1".to_string(),
83 turn_id: Some("turn-1".to_string()),
84 reason: "persist normalized runtime-adjacent state".to_string(),
85 patch: state(&[("memory_version", Value::Number(1.0))]),
86 before_snapshot: Some(Value::Object(state(&[("memory_version", Value::Number(0.0))]))),
87 after_snapshot: Some(Value::Object(state(&[("memory_version", Value::Number(1.0))]))),
88 diff: Some(Value::Object(state(&[(
89 "memory_version",
90 Value::Array(vec![Value::Number(0.0), Value::Number(1.0)]),
91 )]))),
92 ..Default::default()
93 })?;
94 bridge.record_turn_completed(OmpTurn {
95 session_id: "omp-session-1".to_string(),
96 turn_id: "turn-1".to_string(),
97 state_patch: state(&[("last_tool", "contract.search".into())]),
98 ..Default::default()
99 })?;
100
101 let events = bridge
102 .runtime
103 .store
104 .events(&run_id)
105 .iter()
106 .map(|event| event.event_type.clone())
107 .collect::<Vec<_>>();
108 let final_state = bridge.runtime.store.final_state(&run_id)?;
109 let rendered_events = format!("{:?}", events);
110 let memory_version = match final_state.get("memory_version") {
111 Some(Value::Number(value)) => *value,
112 _ => 0.0,
113 };
114 let last_tool = match final_state.get("last_tool") {
115 Some(Value::String(value)) => value.as_str(),
116 _ => "",
117 };
118 println!(
119 concat!(
120 "{{\n",
121 " \"run_id\": \"{}\",\n",
122 " \"events\": {},\n",
123 " \"tool_ledger_count\": {},\n",
124 " \"final_state_memory_version\": {},\n",
125 " \"final_state_last_tool\": \"{}\"\n",
126 "}}"
127 ),
128 run_id,
129 rendered_events,
130 bridge.runtime.store.ledger(&run_id).len(),
131 memory_version,
132 last_tool
133 );
134 Ok(())
135}Trait Implementations§
Source§impl Default for MemoryStore
impl Default for MemoryStore
Source§fn default() -> MemoryStore
fn default() -> MemoryStore
Returns the “default value” for a type. Read more
Auto Trait Implementations§
impl Freeze for MemoryStore
impl RefUnwindSafe for MemoryStore
impl Send for MemoryStore
impl Sync for MemoryStore
impl Unpin for MemoryStore
impl UnsafeUnpin for MemoryStore
impl UnwindSafe for MemoryStore
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more