Skip to content

ToolResult.pending

Java · package io.github.muthuishere:toolnexus · SPEC §10 · ToolResult.java

public static ToolResult pending(Request request)

Return this from a Tool.execute when the call can’t finish in one shot and needs an out-of-band, asynchronous resolution — a human approves something, uploads a file, or answers a question. pending wraps the given Request into a ToolResult whose metadata.pending carries it: isError is true (a parked call did not produce a usable answer) and output is a human-readable fallback (request.prompt() plus the URL, if present). If request.id() is empty, pending generates one — the correlation key a later Answer must echo. The signature of execute never changes; a suspension rides the ordinary return type.

Any time a tool’s real work can’t complete synchronously inside one execute call — it needs a human in the loop, a login, an approval, a piece of missing information. Return ToolResult.pending(new Request(...)) instead of blocking the thread or throwing; the client loop either resolves it transparently (a waitFor is configured) or halts the run with status:"pending" so a durable host can resolve it later, possibly in another process.

1. The smallest useful call — park a tool call with a generated id

Section titled “1. The smallest useful call — park a tool call with a generated id”
import io.github.muthuishere.toolnexus.Request;
import io.github.muthuishere.toolnexus.ToolResult;
public class Example {
public static void main(String[] args) {
ToolResult res = ToolResult.pending(new Request("", "input", "Which environment?"));
if (!res.isError()) throw new AssertionError("a parked call is not a success");
if (!res.output().equals("Which environment?")) throw new AssertionError(res.output());
Request req = ToolResult.pendingOf(res);
if (req == null) throw new AssertionError("expected a Request back");
if (!req.kind().equals("input")) throw new AssertionError(req.kind());
if (req.id() == null || req.id().isEmpty()) throw new AssertionError("an id must be generated");
System.out.println("ok: " + req.kind() + " id=" + req.id());
}
}

2. The realistic case — a tool that suspends on the first call, then succeeds

Section titled “2. The realistic case — a tool that suspends on the first call, then succeeds”
import io.github.muthuishere.toolnexus.*;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
public class Example {
public static void main(String[] args) {
AtomicBoolean approved = new AtomicBoolean(false);
Tool payTool = new Tool() {
@Override public String name() { return "make_payment"; }
@Override public String description() { return "Send a payment. Requires approval first."; }
@Override public Map<String, Object> inputSchema() {
return Map.of("type", "object", "properties", Map.of("amount", Map.of("type", "number")),
"required", java.util.List.of("amount"));
}
@Override public String source() { return "custom"; }
@Override public ToolResult execute(Map<String, Object> args, ToolContext ctx) {
if (!approved.get()) {
return ToolResult.pending(new Request("", "approval",
"Approve payment of " + args.get("amount") + "?"));
}
return ToolResult.ok("paid " + args.get("amount"));
}
};
ToolResult first = payTool.execute(Map.of("amount", 500), null);
Request req = ToolResult.pendingOf(first);
if (req == null || !req.kind().equals("approval")) throw new AssertionError(first);
// out-of-band: someone approves it
approved.set(true);
ToolResult second = payTool.execute(Map.of("amount", 500), null);
if (second.isError() || !second.output().equals("paid 500")) throw new AssertionError(second.output());
System.out.println("ok: " + first.output() + " -> " + second.output());
}
}

3. The full surface — a suspension driven through the real client loop

Section titled “3. The full surface — a suspension driven through the real client loop”

Without a waitFor, LlmClient.run doesn’t hang — it halts with status:"pending" and hands back the Request, so a durable host can resolve it later. See waitFor for the in-process resolution path.

import com.sun.net.httpserver.HttpServer;
import io.github.muthuishere.toolnexus.*;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
public class Example {
public static void main(String[] args) throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
server.createContext("/", ex -> {
ex.getRequestBody().readAllBytes();
byte[] b = ("{\"choices\":[{\"message\":{\"content\":null,\"tool_calls\":[{\"id\":\"c1\","
+ "\"type\":\"function\",\"function\":{\"name\":\"approve_refund\",\"arguments\":\"{}\"}}]}}]}")
.getBytes(StandardCharsets.UTF_8);
ex.getResponseHeaders().add("Content-Type", "application/json");
ex.sendResponseHeaders(200, b.length);
try (OutputStream os = ex.getResponseBody()) { os.write(b); }
});
server.start();
Tool refund = new Tool() {
@Override public String name() { return "approve_refund"; }
@Override public String description() { return "Refund a customer. Needs manager approval."; }
@Override public java.util.Map<String, Object> inputSchema() {
return java.util.Map.of("type", "object", "properties", java.util.Map.of());
}
@Override public String source() { return "custom"; }
@Override public ToolResult execute(java.util.Map<String, Object> args, ToolContext ctx) {
return ToolResult.pending(new Request("", "approval", "Approve the refund?"));
}
};
try (Toolkit tk = Toolkit.create(new Toolkit.Options().builtins(false).extraTools(refund))) {
LlmClient client = LlmClient.create(new LlmClient.Options()
.baseUrl("http://127.0.0.1:" + server.getAddress().getPort())
.style("openai").model("test-model").apiKey("test-key")); // no waitFor
LlmClient.RunResult res = client.run("please refund order 42", tk);
if (!"pending".equals(res.status)) throw new AssertionError(res.status);
if (res.pending == null || !res.pending.kind().equals("approval")) throw new AssertionError(res.pending);
System.out.println("ok: status=" + res.status + " kind=" + res.pending.kind());
} finally {
server.stop(0);
}
}
}
Member Type What it is
pending(request) ToolResult A suspension: isError:true, metadata.pending = request (id filled in if empty).
output() String request.prompt() plus \n + url, if present.
  • ToolResult.authRequired — The auth-shaped suspension: hand back a URL, resume once the user has granted access.
  • LlmClient.waitFor — The single hook where the host resolves a suspension — in-process prompt or durable queue, same contract.
  • ToolResult.pendingOf — Detect that a RunResult is parked rather than finished, and get the Request that parked it.
  • ToolResult — The result envelope a suspension rides on.