AgentRuntime.resume
Java · package io.github.muthuishere:toolnexus · SPEC §7D · agents/AgentRuntime.java
public void resume(Answer answer)Routes an Answer to the deepest suspended handle in the
runtime’s tree, resumes it from its checkpoint (a retry-with-answer of the halted tool — turns and
token usage keep accumulating, never reset), then cascades upward: each suspended ancestor
replays too, and its re-invoked task delegation call reattaches to the already-resumed child
by task key rather than spawning a duplicate. resume itself returns nothing — call
rt.waitOn(handle) afterward for the finished TaskResult.
When to use it
Section titled “When to use it”Reach for rt.resume(answer) any time a spawned agent’s handle transitions to
Handle.State.SUSPENDED (handle.pendingReq set) and you have — or have just obtained — the
Answer to that suspension: a human approved a payment, a login completed, a form was filled in.
There is no separate resume path per handle; one call resolves whichever handle is currently the
deepest suspended one in the tree.
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”1. The smallest useful call — suspend, resume, get the final answer
Section titled “1. The smallest useful call — suspend, resume, get the final answer”import com.sun.net.httpserver.HttpServer;import io.github.muthuishere.toolnexus.*;import io.github.muthuishere.toolnexus.agents.*;import java.io.OutputStream;import java.net.InetSocketAddress;import java.nio.charset.StandardCharsets;import java.util.List;import java.util.Map;
public class Example { public static void main(String[] args) throws Exception { HttpServer llm = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); llm.createContext("/", ex -> { try { String body = new String(ex.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); Map<String, Object> req = Json.toMap(body); List<Object> msgs = (List<Object>) req.get("messages"); boolean answered = msgs.stream().anyMatch(m -> "tool".equals(((Map<?, ?>) m).get("role"))); String message = answered ? "{\"content\":\"Done — charged.\"}" : "{\"content\":null,\"tool_calls\":[{\"id\":\"c1\",\"type\":\"function\"," + "\"function\":{\"name\":\"charge_card\",\"arguments\":\"{}\"}}]}"; byte[] b = ("{\"choices\":[{\"message\":" + message + "}]}").getBytes(StandardCharsets.UTF_8); ex.getResponseHeaders().add("Content-Type", "application/json"); ex.sendResponseHeaders(200, b.length); try (OutputStream os = ex.getResponseBody()) { os.write(b); } } catch (Exception ignored) { } }); llm.start();
Tool chargeCard = new Tool() { @Override public String name() { return "charge_card"; } @Override public String description() { return "charge the card"; } @Override public Map<String, Object> inputSchema() { return Map.of("type", "object", "properties", Map.of()); } @Override public String source() { return "custom"; } @Override public ToolResult execute(Map<String, Object> args, ToolContext ctx) { if (ctx != null && ctx.answer() != null) return ToolResult.ok("charged (ok=" + ctx.answer().ok() + ")"); return ToolResult.pending(new Request(null, "approval", "Approve $500 charge?")); } };
try { AgentDef def = new AgentDef("approve", "approves a payment", "", "test-model").tools(List.of(chargeCard)); RuntimeOptions rtOpts = new RuntimeOptions() .baseUrl("http://127.0.0.1:" + llm.getAddress().getPort()) .apiKey("test-key") .registry(Map.of("approve", def)); AgentRuntime rt = new AgentRuntime(rtOpts); Handle h = rt.spawn(rt.root, "approve").handle();
var fut = rt.futureResult(h); rt.wake(h, "Charge the card."); TaskResult r1 = fut.join();
if (!"pending".equals(r1.status())) throw new AssertionError(r1.status()); if (h.state != Handle.State.SUSPENDED) throw new AssertionError(h.state);
rt.resume(new Answer(r1.pending().id(), true));
if (h.state != Handle.State.IDLE) throw new AssertionError("resumed back to idle: " + h.state); if (h.lastResult == null || !h.lastResult.text().equals("Done — charged.")) { throw new AssertionError(h.lastResult); }
System.out.println("ok: suspended -> resumed -> " + h.lastResult.text()); } finally { llm.stop(0); } }}2. A declined answer — the run finishes, it just doesn’t do the thing
Section titled “2. A declined answer — the run finishes, it just doesn’t do the thing”resume’s loop rule branches only on Answer.ok() — a decline is data, not a thrown error.
import com.sun.net.httpserver.HttpServer;import io.github.muthuishere.toolnexus.*;import io.github.muthuishere.toolnexus.agents.*;import java.io.OutputStream;import java.net.InetSocketAddress;import java.nio.charset.StandardCharsets;import java.util.List;import java.util.Map;
public class Example { public static void main(String[] args) throws Exception { HttpServer llm = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); llm.createContext("/", ex -> { try { String body = new String(ex.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); Map<String, Object> req = Json.toMap(body); List<Object> msgs = (List<Object>) req.get("messages"); boolean answered = msgs.stream().anyMatch(m -> "tool".equals(((Map<?, ?>) m).get("role"))); String message = answered ? "{\"content\":\"Understood — the charge was cancelled.\"}" : "{\"content\":null,\"tool_calls\":[{\"id\":\"c1\",\"type\":\"function\"," + "\"function\":{\"name\":\"charge_card\",\"arguments\":\"{}\"}}]}"; byte[] b = ("{\"choices\":[{\"message\":" + message + "}]}").getBytes(StandardCharsets.UTF_8); ex.getResponseHeaders().add("Content-Type", "application/json"); ex.sendResponseHeaders(200, b.length); try (OutputStream os = ex.getResponseBody()) { os.write(b); } } catch (Exception ignored) { } }); llm.start();
Tool chargeCard = new Tool() { @Override public String name() { return "charge_card"; } @Override public String description() { return "charge the card"; } @Override public Map<String, Object> inputSchema() { return Map.of("type", "object", "properties", Map.of()); } @Override public String source() { return "custom"; } @Override public ToolResult execute(Map<String, Object> args, ToolContext ctx) { if (ctx != null && ctx.answer() != null) return ToolResult.ok("charge outcome ok=" + ctx.answer().ok()); return ToolResult.pending(new Request(null, "approval", "Approve $500 charge?")); } };
try { AgentDef def = new AgentDef("approve", "approves a payment", "", "test-model").tools(List.of(chargeCard)); RuntimeOptions rtOpts = new RuntimeOptions() .baseUrl("http://127.0.0.1:" + llm.getAddress().getPort()) .apiKey("test-key") .registry(Map.of("approve", def)); AgentRuntime rt = new AgentRuntime(rtOpts); Handle h = rt.spawn(rt.root, "approve").handle();
var fut = rt.futureResult(h); rt.wake(h, "Charge the card."); TaskResult r1 = fut.join(); if (h.state != Handle.State.SUSPENDED) throw new AssertionError(h.state);
rt.resume(new Answer(r1.pending().id(), false, null, "declined"));
// The RUN still completes — it just knows the charge was declined. if (h.lastResult == null || !h.lastResult.text().contains("cancelled")) { throw new AssertionError(h.lastResult); }
System.out.println("ok: " + h.lastResult.text()); } finally { llm.stop(0); } }}3. Inspecting the parked handle before resuming — list()
Section titled “3. Inspecting the parked handle before resuming — list()”A host that stores the answer separately from the runtime (a queue, a database row) still needs a
live handle to resume — list() gives a read-only view of what’s parked, including the pending
request’s kind, without walking handle.children by hand.
import com.sun.net.httpserver.HttpServer;import io.github.muthuishere.toolnexus.*;import io.github.muthuishere.toolnexus.agents.*;import java.io.OutputStream;import java.net.InetSocketAddress;import java.nio.charset.StandardCharsets;import java.util.List;import java.util.Map;
public class Example { public static void main(String[] args) throws Exception { HttpServer llm = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); llm.createContext("/", ex -> { try { String body = new String(ex.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); Map<String, Object> req = Json.toMap(body); List<Object> msgs = (List<Object>) req.get("messages"); boolean answered = msgs.stream().anyMatch(m -> "tool".equals(((Map<?, ?>) m).get("role"))); String message = answered ? "{\"content\":\"Charged.\"}" : "{\"content\":null,\"tool_calls\":[{\"id\":\"c1\",\"type\":\"function\"," + "\"function\":{\"name\":\"charge_card\",\"arguments\":\"{}\"}}]}"; byte[] b = ("{\"choices\":[{\"message\":" + message + "}]}").getBytes(StandardCharsets.UTF_8); ex.getResponseHeaders().add("Content-Type", "application/json"); ex.sendResponseHeaders(200, b.length); try (OutputStream os = ex.getResponseBody()) { os.write(b); } } catch (Exception ignored) { } }); llm.start();
Tool chargeCard = new Tool() { @Override public String name() { return "charge_card"; } @Override public String description() { return "charge the card"; } @Override public Map<String, Object> inputSchema() { return Map.of("type", "object", "properties", Map.of()); } @Override public String source() { return "custom"; } @Override public ToolResult execute(Map<String, Object> args, ToolContext ctx) { if (ctx != null && ctx.answer() != null) return ToolResult.ok("charged"); return ToolResult.pending(new Request(null, "approval", "Approve $500 charge?")); } };
try { AgentDef def = new AgentDef("approve", "approves a payment", "", "test-model").tools(List.of(chargeCard)); RuntimeOptions rtOpts = new RuntimeOptions() .baseUrl("http://127.0.0.1:" + llm.getAddress().getPort()) .apiKey("test-key") .registry(Map.of("approve", def)); AgentRuntime rt = new AgentRuntime(rtOpts); Handle h = rt.spawn(rt.root, "approve").handle();
var fut = rt.futureResult(h); rt.wake(h, "Charge the card."); TaskResult r1 = fut.join();
// The read-only view: same handle state, reachable from list() alone. var view = rt.list().stream().filter(v -> v.id().equals(h.id)).findFirst().orElseThrow(); if (view.state() != Handle.State.SUSPENDED) throw new AssertionError(view.state());
rt.resume(new Answer(r1.pending().id(), true)); if (h.lastResult == null || !"done".equals(h.lastResult.status())) throw new AssertionError(h.lastResult);
System.out.println("ok: " + view.state() + " -> " + h.lastResult.status()); } finally { llm.stop(0); } }}Signature
Section titled “Signature”| Parameter | Type | What it is |
|---|---|---|
answer |
Answer |
Must echo the id of the pending Request — routed to the deepest suspended handle. |
| returns | void |
Call rt.waitOn(handle) or rt.futureResult(handle) afterward for the finished TaskResult. |
See also
Section titled “See also”agents.Handle— The state machine for one spawned agent: pending, running, suspended, done.agents.Runtime— The six verbs that create and transition handles.ToolResult.pending— Return a Pending from a tool to park the run until someone answers.suspension/relay— The golang-only §10 preview that typically resumes viaRunWithAnswer/AskWithAnswer.