pub struct Runtime {
pub store: MemoryStore,
pub registry: ToolRegistry,
pub budget: BudgetLimits,
pub sandbox: Box<dyn SandboxExecutor>,
}Fields§
§store: MemoryStore§registry: ToolRegistry§budget: BudgetLimits§sandbox: Box<dyn SandboxExecutor>Implementations§
Source§impl Runtime
impl Runtime
Sourcepub fn new() -> Self
pub fn new() -> Self
Examples found in repository?
examples/three_minute_demo.rs (line 21)
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 34)
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 14)
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}examples/travel_assistant.rs (line 505)
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 register_tool(&mut self, spec: ToolSpec)
pub fn register_tool(&mut self, spec: ToolSpec)
Examples found in repository?
examples/three_minute_demo.rs (lines 22-41)
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 82)
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 513-524)
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 set_budget(&mut self, budget: BudgetLimits)
pub fn set_budget(&mut self, budget: BudgetLimits)
Examples found in repository?
examples/travel_assistant.rs (lines 506-510)
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 set_sandbox(&mut self, sandbox: Box<dyn SandboxExecutor>)
pub fn set_sandbox(&mut self, sandbox: Box<dyn SandboxExecutor>)
Examples found in repository?
examples/mcp_governance.rs (line 35)
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 create_run(&mut self, initial_state: State) -> (String, String)
pub fn create_run(&mut self, initial_state: State) -> (String, String)
Examples found in repository?
examples/three_minute_demo.rs (line 42)
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 85)
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 597-600)
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 run_once( &mut self, run_id: &str, worker_id: &str, agent_role: &str, lease_seconds: f64, agent: AgentFunc, ) -> Result<bool>
Sourcepub fn call_tool(
&mut self,
ctx: &AgentContext,
tool_name: &str,
args: State,
) -> Result<Value>
pub fn call_tool( &mut self, ctx: &AgentContext, tool_name: &str, args: State, ) -> Result<Value>
Examples found in repository?
examples/travel_assistant.rs (lines 232-235)
226fn travel_planner(
227 runtime: &mut Runtime,
228 ctx: &mut AgentContext,
229 attempt: u64,
230) -> agentledger::Result<()> {
231 // Phase 1: Research
232 let flights = runtime.call_tool(ctx, "travel.search_flights", make_state(&[
233 ("from", "Beijing".into()),
234 ("to", "Tokyo".into()),
235 ]))?;
236 let hotels = runtime.call_tool(ctx, "travel.search_hotels", make_state(&[
237 ("city", "Tokyo".into()),
238 ]))?;
239 let weather = runtime.call_tool(ctx, "travel.check_weather", make_state(&[
240 ("city", "Tokyo".into()),
241 ]))?;
242
243 let fc = get_number(&flights, "count") as i64;
244 let hc = get_number(&hotels, "count") as i64;
245 let wt = get_number(&weather, "temp_c");
246 ctx.write_state("research", Value::Object(make_state(&[
247 ("flights", Value::Number(fc as f64)),
248 ("hotels", Value::Number(hc as f64)),
249 ("weather", Value::Number(wt)),
250 ])));
251
252 // Phase 2: Book flight (approval required)
253 let flight = runtime.call_tool(ctx, "travel.book_flight", make_state(&[
254 ("flight_id", "FL-002".into()),
255 ("passenger", "Demo User".into()),
256 ("_logical_operation", "book-demo-flight".into()),
257 ]))?;
258
259 // Phase 3: Simulated crash on attempt 2
260 if attempt == 2 {
261 return Err(RuntimeError("retryable".to_string()));
262 }
263
264 // Phase 4: Book hotel (approval required)
265 let hotel = runtime.call_tool(ctx, "travel.book_hotel", make_state(&[
266 ("hotel_id", "HT-002".into()),
267 ("check_in", "2025-06-15".into()),
268 ("check_out", "2025-06-20".into()),
269 ("guest", "Demo User".into()),
270 ("_logical_operation", "book-demo-hotel".into()),
271 ]))?;
272
273 let flight_ref = get_string(&flight, "booking_ref");
274 let hotel_ref = get_string(&hotel, "booking_ref");
275 ctx.write_state("bookings", Value::Object(make_state(&[
276 ("flight", Value::String(flight_ref)),
277 ("hotel", Value::String(hotel_ref)),
278 ])));
279 ctx.write_state("trip_status", Value::String("confirmed".into()));
280 Ok(())
281}More examples
examples/three_minute_demo.rs (lines 55-62)
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 (lines 97-104)
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 create_artifact( &mut self, ctx: &AgentContext, name: &str, content: State, metadata: State, ) -> Result<String>
pub fn create_media_artifact( &mut self, ctx: &AgentContext, name: &str, kind: &str, options: MediaArtifactOptions, ) -> Result<String>
pub fn create_stream_checkpoint( &mut self, ctx: &AgentContext, name: &str, options: StreamCheckpointOptions, ) -> Result<String>
pub fn record_model_call( &mut self, ctx: &AgentContext, model: &str, input_tokens: f64, output_tokens: f64, total_usd: f64, ) -> Result<()>
pub fn record_model_call_evidence( &mut self, ctx: &AgentContext, provider: &str, model: &str, request: State, response: State, usage: State, total_usd: f64, metadata: State, ) -> Result<()>
pub fn record_model_failure( &mut self, ctx: &AgentContext, provider: &str, model: &str, error_type: &str, message: &str, retryable: Option<bool>, request: State, usage: State, total_usd: f64, metadata: State, ) -> Result<()>
pub fn record_tool_call_proposal( &mut self, ctx: &AgentContext, tool_name: &str, arguments: State, provider: Option<&str>, model: Option<&str>, model_call_ref: Option<&str>, confidence: Option<f64>, reason: Option<&str>, metadata: State, )
Trait Implementations§
Auto Trait Implementations§
impl !RefUnwindSafe for Runtime
impl !Send for Runtime
impl !Sync for Runtime
impl !UnwindSafe for Runtime
impl Freeze for Runtime
impl Unpin for Runtime
impl UnsafeUnpin for Runtime
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