Skip to content

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.

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.

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);
}
}
}
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.
  • 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 via RunWithAnswer/AskWithAnswer.