Skip to content

ToolResult.pendingOf

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

public static Request pendingOf(ToolResult result)

Reads the suspension back off a ToolResult: returns the Request when result.metadata() carries a pending entry of that type, null otherwise (including when result itself is null). This is the reader half of pending/ authRequired — the one place code should ask “is this a suspension?” rather than inspecting metadata by hand. Because a suspension also sets isError true, pendingOf is what distinguishes a parked call from a genuine failure — don’t read isError() alone as “the tool broke”.

Anywhere you’re inspecting a ToolResult (or a halted RunResult, whose transcript carries the first suspension’s placeholder) and need to tell a suspension apart from an ordinary error — a beforeTool/afterTool hook classifying results, a durable host deciding whether to persist a RunResult.pending request, or a test asserting a tool suspended for the right reason.

1. The smallest useful call — null on an ordinary result, non-null on a suspension

Section titled “1. The smallest useful call — null on an ordinary result, non-null on a suspension”
import io.github.muthuishere.toolnexus.Request;
import io.github.muthuishere.toolnexus.ToolResult;
public class Example {
public static void main(String[] args) {
ToolResult ordinary = ToolResult.ok("done");
if (ToolResult.pendingOf(ordinary) != null) throw new AssertionError("an ordinary result is not pending");
ToolResult failed = ToolResult.error("not found");
if (ToolResult.pendingOf(failed) != null) throw new AssertionError("a real error is not pending either");
ToolResult suspended = ToolResult.pending(new Request("", "input", "Which environment?"));
Request req = ToolResult.pendingOf(suspended);
if (req == null) throw new AssertionError("expected the Request back");
if (!req.kind().equals("input")) throw new AssertionError(req.kind());
System.out.println("ok: ordinary=null, failed=null, suspended=" + req.kind());
}
}

2. The realistic case — distinguishing a suspension from a real error in a hook

Section titled “2. The realistic case — distinguishing a suspension from a real error in a hook”
import io.github.muthuishere.toolnexus.*;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
public class Example {
public static void main(String[] args) {
List<String> classifications = new CopyOnWriteArrayList<>();
LlmClient.Hooks hooks = new LlmClient.Hooks().afterTool(ev -> {
ToolResult r = ev.result();
if (ToolResult.pendingOf(r) != null) {
classifications.add("suspended");
} else if (r.isError()) {
classifications.add("failed");
} else {
classifications.add("ok");
}
return null;
});
// Exercise the classification logic directly against three representative results —
// afterTool would see the same three shapes in a real run.
List<ToolResult> results = List.of(
ToolResult.ok("42"),
ToolResult.error("boom"),
ToolResult.pending(new Request("", "approval", "Approve?")));
for (ToolResult r : results) {
hooks.afterTool.apply(new LlmClient.AfterToolEvent("t", Map.of(), r, "c1", 0));
}
if (!classifications.equals(List.of("ok", "failed", "suspended"))) {
throw new AssertionError(classifications);
}
System.out.println("ok: " + classifications);
}
}

3. The full surface — reading the halted request back off a durable RunResult

Section titled “3. The full surface — reading the halted request back off a durable RunResult”
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 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);
// RunResult.pending IS the Request pendingOf would have read off the halted tool call.
if (res.pending == null || !res.pending.kind().equals("approval")) throw new AssertionError(res.pending);
System.out.println("ok: " + res.status + " kind=" + res.pending.kind());
} finally {
server.stop(0);
}
}
}
Member Type What it is
pendingOf(result) Request The suspension’s Request, or null when result is null/not a suspension.
  • ToolResult.pending — Return a Pending from a tool to park the run until someone answers.
  • 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.