Skip to main content

replay

Function replay 

Source
pub fn replay(store: &MemoryStore, run_id: &str) -> Result<ReplaySummary>
Examples found in repository?
examples/three_minute_demo.rs (line 99)
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/travel_assistant.rs (line 882)
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}