answerOutput
JavaScript · package toolnexus · SPEC §10
function answerOutput(id: string, output: string): Answer// => { id, ok: true, data: { output } }Wraps a human’s typed string reply into the Answer a suspended run resumes with — the success
counterpart to answerDeclined. output must
be a string; a non-string is a thrown TypeError, never a silent degrade to "" — a fabricated
empty result would be indistinguishable from an answer the human never actually gave (ADR 0026
D2). id is required and must match the Request.id the suspension carried.
When to use it
Section titled “When to use it”Use answerOutput wherever a host resumes a run that a tool suspended with
pending() — a human approved something, typed a
value, or provided a token — and you now have that string in hand. It exists specifically
because hand-building { id, ok: true, data: { output } } is where hosts get the key wrong
(output vs value vs answer); answerOutput removes the key from the host’s hands entirely.
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”1. The smallest useful call — build the Answer for a resume
Section titled “1. The smallest useful call — build the Answer for a resume”import assert from "node:assert"import { answerOutput } from "toolnexus"
const answer = answerOutput("req-1", "staging")assert.deepEqual(answer, { id: "req-1", ok: true, data: { output: "staging" } })console.log("ok:", JSON.stringify(answer))2. The realistic case — resuming a suspended sub-agent runtime
Section titled “2. The realistic case — resuming a suspended sub-agent runtime”A tool suspends with pending(), the run comes back status: "pending", and the host resumes it
once it has the human’s answer in hand.
import assert from "node:assert"import { agents, defineTool, pending, answerOutput } from "toolnexus"
const { AgentRuntime } = agents as any
let asked = 0const suspendFetch: any = async (_u: string, init: any) => { const body = JSON.parse(String(init.body)) const toolMsgs = body.messages.filter((m: any) => m.role === "tool") const approved = toolMsgs.some((m: any) => String(m.content).includes("secret-token")) const reply = approved ? { role: "assistant", content: "final: secret-token" } : { role: "assistant", tool_calls: [{ id: "a1", type: "function", function: { name: "check_secret", arguments: "{}" } }] } return new Response( JSON.stringify({ choices: [{ index: 0, message: reply, finish_reason: reply.tool_calls ? "tool_calls" : "stop" }], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, }), { status: 200, headers: { "content-type": "application/json" } }, )}
const checkSecret = defineTool({ name: "check_secret", description: "needs approval", inputSchema: { type: "object", properties: {} }, run: async (_a: any, ctx: any) => (ctx?.answer?.ok ? "secret-token" : pending({ kind: "approval", prompt: "approve?" })),})
const rt = new AgentRuntime({ fetch: suspendFetch, registry: { asker: { name: "asker", does: "asks", model: "m", tools: [checkSecret] } },})const h = rt.spawn(rt.root, "asker")rt.wake(h, "go")const halted = await rt.wait(h)assert.equal(halted.status, "pending")
// The human approved — resume with the typed constructor, never a hand-built object.const resumed = await rt.resume(answerOutput(halted.pending.id, "approved"))assert.equal(resumed.status, "done")assert.equal(resumed.text, "final: secret-token")await rt.close(rt.root)console.log("ok:", resumed.status, resumed.text)3. The full surface — the guard that keeps a fabricated empty answer impossible
Section titled “3. The full surface — the guard that keeps a fabricated empty answer impossible”import assert from "node:assert"import { answerOutput } from "toolnexus"
// A non-string output is an ERROR, never a silent "" — a fabricated empty answer is// indistinguishable from an answer the human never gave.assert.throws(() => answerOutput("req-1", { value: "staging" } as any), /output must be a string, got object/)assert.throws(() => answerOutput("req-1", 42 as any), /output must be a string, got number/)assert.throws(() => answerOutput("req-1", null as any), /output must be a string, got null/)
// id is required — it must match the Request's id the suspension carried.assert.throws(() => answerOutput("", "x"), /id is required/)
console.log("ok: guards hold")See also
Section titled “See also”pending— Return a Pending from a tool to park the run until someone answers.authRequired— The auth-shaped suspension: hand back a URL, resume once the user has granted access.waitFor— The single hook where the host resolves a suspension — in-process prompt or durable queue, same contract.pendingOf— Detect that a RunResult is parked rather than finished, and get the Request that parked it.