Skip to content

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.

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.

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);
}
}
}
Member Type What it is
authRequired(url) ToolResult kind:"authorization", prompt "Authorization required to continue".
authRequired(url, prompt) ToolResult Same, with a custom prompt.
  • 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.