1use agentledger::{AgentContext, InMemoryMCPToolServer, MCPToolAdapter, Runtime, SandboxExecutor, SandboxPolicy, SandboxResult, State, Value};
2
3fn state(items: &[(&str, Value)]) -> State {
4 items
5 .iter()
6 .map(|(key, value)| ((*key).to_string(), value.clone()))
7 .collect()
8}
9
10fn create_pr(_name: &str, args: State) -> agentledger::Result<Value> {
11 Ok(Value::Object(state(&[
12 ("external_id", "PR-1".into()),
13 ("title", args["title"].clone()),
14 ])))
15}
16
17struct DemoSandboxExecutor;
18
19impl SandboxExecutor for DemoSandboxExecutor {
20 fn run_tool(&self, args: State, _policy: &SandboxPolicy) -> SandboxResult {
21 SandboxResult {
22 ok: true,
23 output: create_pr("mcp.github.create_pr", args).unwrap_or(Value::Null),
24 error: None,
25 metadata: state(&[
26 ("executor", "demo-local".into()),
27 ("isolation_level", "none".into()),
28 ]),
29 }
30 }
31}
32
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}