Skip to main content

Runtime

Struct Runtime 

Source
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

Source

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
Hide additional 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}
Source

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
Hide additional 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}
Source

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}
Source

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}
Source

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
Hide additional 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}
Source

pub fn run_once( &mut self, run_id: &str, worker_id: &str, agent_role: &str, lease_seconds: f64, agent: AgentFunc, ) -> Result<bool>

Source

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
Hide additional 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}
Source

pub fn create_artifact( &mut self, ctx: &AgentContext, name: &str, content: State, metadata: State, ) -> Result<String>

Source

pub fn create_media_artifact( &mut self, ctx: &AgentContext, name: &str, kind: &str, options: MediaArtifactOptions, ) -> Result<String>

Source

pub fn create_stream_checkpoint( &mut self, ctx: &AgentContext, name: &str, options: StreamCheckpointOptions, ) -> Result<String>

Source

pub fn record_model_call( &mut self, ctx: &AgentContext, model: &str, input_tokens: f64, output_tokens: f64, total_usd: f64, ) -> Result<()>

Source

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<()>

Source

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<()>

Source

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§

Source§

impl Default for Runtime

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.