ToolResult.authRequired
Java · package io.github.muthuishere:toolnexus · SPEC §10 · ToolResult.java
public static ToolResult authRequired(String url)public static ToolResult authRequired(String url, String prompt)Sugar over ToolResult.pending for the single most common
suspension: a login. Builds a Request with kind:"authorization", the given url, and a
generated id; the 1-arg overload defaults prompt to "Authorization required to continue".
By SPEC §10 convention, kind:"authorization" follows OAuth2/OIDC authorization-code semantics —
url is the authorize endpoint, and the host’s waitFor performs the redirect → consent →
callback out-of-band. toolnexus stays OIDC-agnostic: there is no auth subsystem in the kernel,
no token logic, no OIDC library — authRequired just shapes the data; every credential flow lives
entirely inside the host’s waitFor at the edge.
When to use it
Section titled “When to use it”A tool needs the caller to be logged in somewhere before it can do real work — an API session that
expired, a service the agent has never authenticated with, a scope it doesn’t yet hold. Return
authRequired(loginUrl) the moment you detect the missing session; don’t build your own
“not authenticated” error string and hope the model retries correctly — the suspension mechanism
makes the retry automatic once the host resolves it.
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”1. The smallest useful call — the 1-arg overload’s default prompt
Section titled “1. The smallest useful call — the 1-arg overload’s default prompt”import io.github.muthuishere.toolnexus.Request;import io.github.muthuishere.toolnexus.ToolResult;
public class Example { public static void main(String[] args) { ToolResult res = ToolResult.authRequired("https://example.com/login?token=abc");
if (!res.isError()) throw new AssertionError("a parked call is not a success");
Request req = ToolResult.pendingOf(res); if (req == null) throw new AssertionError("expected a Request back"); if (!req.kind().equals("authorization")) throw new AssertionError(req.kind()); if (!req.url().equals("https://example.com/login?token=abc")) throw new AssertionError(req.url()); if (!req.prompt().equals("Authorization required to continue")) throw new AssertionError(req.prompt());
System.out.println("ok: " + req.kind() + " " + req.url()); }}2. The realistic case — a custom prompt, and a tool that flips once authed
Section titled “2. The realistic case — a custom prompt, and a tool that flips once authed”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 authed = new AtomicBoolean(false);
Tool balance = new Tool() { @Override public String name() { return "get_balance"; } @Override public String description() { return "Return the account balance. Requires login first."; } @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 (!authed.get()) { return ToolResult.authRequired("https://example.com/login?token=abc", "Log in to view your balance"); } return ToolResult.ok("balance: 67417"); } };
ToolResult first = balance.execute(Map.of(), null); Request req = ToolResult.pendingOf(first); if (req == null || !req.prompt().equals("Log in to view your balance")) throw new AssertionError(first);
authed.set(true); // the world changed out-of-band (the human logged in) ToolResult second = balance.execute(Map.of(), null); if (second.isError() || !second.output().equals("balance: 67417")) throw new AssertionError(second.output());
System.out.println("ok: " + req.prompt() + " -> " + second.output()); }}3. The full surface — resolved transparently through the real client loop
Section titled “3. The full surface — resolved transparently through the real client loop”With a waitFor configured, the engine resolves the login and retries the tool automatically —
the model never sees the suspension, only the final answer.
import com.sun.net.httpserver.HttpServer;import io.github.muthuishere.toolnexus.*;import java.io.IOException;import java.io.OutputStream;import java.net.InetSocketAddress;import java.nio.charset.StandardCharsets;import java.util.List;import java.util.Map;import java.util.concurrent.atomic.AtomicBoolean;import java.util.concurrent.atomic.AtomicReference;
public class Example { @SuppressWarnings("unchecked") public static void main(String[] args) throws Exception { HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); server.createContext("/", exchange -> { String body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); Map<String, Object> req = Json.toMap(body); List<Object> messages = (List<Object>) req.get("messages"); boolean sawToolResult = messages.stream() .anyMatch(m -> m instanceof Map && "tool".equals(((Map<String, Object>) m).get("role"))); String message = sawToolResult ? "{\"content\":\"Your balance is 67417.\"}" : "{\"content\":null,\"tool_calls\":[{\"id\":\"c1\",\"type\":\"function\"," + "\"function\":{\"name\":\"get_balance\",\"arguments\":\"{}\"}}]}"; byte[] b = ("{\"choices\":[{\"message\":" + message + "}]}").getBytes(StandardCharsets.UTF_8); exchange.getResponseHeaders().set("Content-Type", "application/json"); exchange.sendResponseHeaders(200, b.length); try (OutputStream os = exchange.getResponseBody()) { os.write(b); } }); server.start();
AtomicBoolean authed = new AtomicBoolean(false); Tool balance = new Tool() { @Override public String name() { return "get_balance"; } @Override public String description() { return "Return the account balance. Requires login."; } @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 (!authed.get()) return ToolResult.authRequired("https://example.com/login?token=abc"); return ToolResult.ok("67417"); } };
AtomicReference<String> linkSeen = new AtomicReference<>(); try (Toolkit tk = Toolkit.create(new Toolkit.Options().builtins(false).extraTools(balance))) { LlmClient client = LlmClient.create(new LlmClient.Options() .baseUrl("http://127.0.0.1:" + server.getAddress().getPort()) .style("openai").model("test-model").apiKey("test-key") .waitFor(request -> { linkSeen.set(request.url()); authed.set(true); // simulate the human completing login return new Answer(request.id(), true); }));
LlmClient.RunResult res = client.run("what is my balance?", tk);
if (!"done".equals(res.status)) throw new AssertionError(res.status); if (!res.text.contains("67417")) throw new AssertionError(res.text); if (linkSeen.get() == null || !linkSeen.get().contains("example.com/login")) { throw new AssertionError("the login link must reach the host: " + linkSeen.get()); }
System.out.println("ok: " + res.text); } finally { server.stop(0); } }}Members
Section titled “Members”| Member | Type | What it is |
|---|---|---|
authRequired(url) |
ToolResult |
kind:"authorization", prompt "Authorization required to continue". |
authRequired(url, prompt) |
ToolResult |
Same, with a custom prompt. |
See also
Section titled “See also”ToolResult.pending— Return a Pending from a tool to park the run until someone answers.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.