ToolContext
Java · package io.github.muthuishere:toolnexus · SPEC §1 · ToolContext.java
public final class ToolContext { public ToolContext(); public ToolContext(Long timeoutMs); public ToolContext(Long timeoutMs, AtomicBoolean cancelled); public ToolContext(Long timeoutMs, AtomicBoolean cancelled, Answer answer);
public Long timeoutMs(); public boolean isCancelled(); public void cancel(); public AtomicBoolean cancellationToken(); public Answer answer();}The second argument to execute. It carries the three things a tool needs to be a good citizen: a
cancellation token, a deadline, and — on a resumed call — the answer to a question it previously
asked.
When to use it
Section titled “When to use it”Read ctx when your tool does anything slow or interactive:
isCancelled()— long work that should stop when the run is cancelled.timeoutMs()— the caller’s deadline for this specific call, in milliseconds.answer()— you returned a suspension on a previous attempt and the host has now resolved it.
A pure, fast, local computation can ignore ctx entirely.
Why a class with a mutable token
Section titled “Why a class with a mutable token”The token is an AtomicBoolean rather than a Future or an interrupt because a tool may spawn work
that outlives a single thread, and cooperative checking is the one mechanism that works everywhere.
cancel() is on the context so a tool can also cancel its own subtree.
Examples
Section titled “Examples”1. Respecting cancellation
Section titled “1. Respecting cancellation”Check before starting work and between steps. A cancelled tool returns an error result promptly rather than throwing.
import io.github.muthuishere.toolnexus.*;import java.util.Map;import java.util.concurrent.atomic.AtomicBoolean;
public class Example { static ToolResult crunch(int steps, ToolContext ctx) { int done = 0; for (int i = 0; i < steps; i++) { // Null-guard: ctx is absent on a direct call. if (ctx != null && ctx.isCancelled()) { return ToolResult.error("cancelled after " + done + " step(s)"); } done++; } return ToolResult.ok("completed " + done + " step(s)"); }
public static void main(String[] args) { // No context at all — the tool still runs. ToolResult plain = crunch(3, null); if (!plain.output().equals("completed 3 step(s)")) throw new AssertionError(plain.output());
// Cancelled before it starts. AtomicBoolean token = new AtomicBoolean(true); ToolResult stopped = crunch(3, new ToolContext(null, token)); if (!stopped.isError() || !stopped.output().equals("cancelled after 0 step(s)")) { throw new AssertionError(stopped.output()); }
// A tool can cancel its own subtree through the context. ToolContext live = new ToolContext(); if (live.isCancelled()) throw new AssertionError("should start live"); live.cancel(); if (!live.isCancelled()) throw new AssertionError("cancel() should flip it");
System.out.println("ok: " + plain.output() + " | " + stopped.output()); }}2. Honouring the caller’s timeout
Section titled “2. Honouring the caller’s timeout”timeoutMs() is a Long — it may be null, meaning “not set”. Fall back to your own default
rather than unboxing blindly.
import io.github.muthuishere.toolnexus.*;
public class Example { static ToolResult fetchish(String url, ToolContext ctx) { // null means unset — use your own default. Unboxing a null Long throws. long budget = (ctx != null && ctx.timeoutMs() != null) ? ctx.timeoutMs() : 30_000L;
if (budget < 100) { return ToolResult.error("budget " + budget + "ms is too small to try"); } return ToolResult.ok("fetched " + url + " within " + budget + "ms"); }
public static void main(String[] args) { ToolResult generous = fetchish("/a", new ToolContext(5000L)); if (!generous.output().equals("fetched /a within 5000ms")) throw new AssertionError(generous.output());
ToolResult stingy = fetchish("/a", new ToolContext(10L)); if (!stingy.isError()) throw new AssertionError("expected the tiny budget to be refused");
ToolResult defaulted = fetchish("/a", new ToolContext()); if (!defaulted.output().contains("30000ms")) throw new AssertionError(defaulted.output());
ToolResult noCtx = fetchish("/a", null); if (!noCtx.output().contains("30000ms")) throw new AssertionError(noCtx.output());
System.out.println("ok: " + generous.output() + " | " + stingy.output()); }}3. answer() — the second half of a suspension
Section titled “3. answer() — the second half of a suspension”This is the field that makes the human-in-the-loop contract work. On the first call the tool
returns a pending. The host resolves it, then calls the same tool again with the answer set.
import io.github.muthuishere.toolnexus.*;import java.util.Map;
public class Example { static ToolResult deploy(ToolContext ctx) { Answer answer = ctx == null ? null : ctx.answer();
// Second pass: the host resolved the question and handed the answer back. if (answer != null) { if (!answer.ok()) { String reason = answer.reason() == null ? "no reason" : answer.reason(); return ToolResult.error("declined: " + reason); } Object env = answer.data() == null ? null : answer.data().get("env"); return ToolResult.ok("deployed to " + (env == null ? "unknown" : env)); }
// First pass: park the run and ask. return ToolResult.pending(new Request("", "input", "Which environment?")); }
public static void main(String[] args) { // First pass — a suspension, not an answer. ToolResult first = deploy(null); Request req = ToolResult.pendingOf(first); if (req == null || !req.kind().equals("input")) throw new AssertionError("expected a suspension");
// Second pass — the host supplies the resolution, echoing the request id. ToolContext resumed = new ToolContext(null, null, new Answer(req.id(), true, Map.of("env", "staging"))); ToolResult second = deploy(resumed); if (second.isError() || !second.output().equals("deployed to staging")) { throw new AssertionError(second.output()); }
// A refusal is a normal outcome, not a crash. ToolContext declined = new ToolContext(null, null, new Answer(req.id(), false, null, "declined")); ToolResult refused = deploy(declined); if (!refused.isError()) throw new AssertionError("expected an error result");
System.out.println("ok: " + second.output() + " | " + refused.output()); }}Members
Section titled “Members”| Member | Returns | What it is |
|---|---|---|
timeoutMs() |
Long |
This call’s budget in milliseconds. May be null — do not unbox blindly. |
isCancelled() |
boolean |
Whether the run has been cancelled. Safe when no token was supplied. |
cancel() |
void |
Cancels from inside a tool. |
cancellationToken() |
AtomicBoolean |
The raw token, to hand to work you spawn. |
answer() |
Answer |
Present only on a post-waitFor retry. |
See also
Section titled “See also”Tool— what receives thisToolResult— whatexecutereturnsRequest.pending— ask a question mid-call