answer_declined
Python · package toolnexus · SPEC §10 · python/src/toolnexus/types.py
def answer_declined(id: str, reason: str = "declined") -> AnswerWraps a human’s refusal into the Answer a suspended run resumes with, carrying a reason
string that defaults to "declined". Builds Answer(id=id, ok=False, reason=reason) — the
refusal counterpart to answer_output. reason is a
closed, advisory vocabulary: "declined", "cancelled", or "expired"; anything else raises
ValueError immediately. The loop itself branches only on Answer.ok, never on reason — the
string is there so a host can tell an explicit “no” from a dismissal or a stale request apart
when it later inspects why a run stopped.
When to use it
Section titled “When to use it”- A human explicitly refuses a
pending(kind="approval", ...)— “not OK to deploy to prod” — and you want that refusal to reach the tool asctx.answer.ok is False, distinct from a successful reply. - A
Requestgoes stale before anyone answers it — resolve it withanswer_declined(request.id, "expired")rather than leaving the run parked forever. - Your UI lets a user dismiss a prompt without answering it —
answer_declined(request.id, "cancelled")distinguishes that from a considered “no”.
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”1. The smallest useful call — the default reason
Section titled “1. The smallest useful call — the default reason”from toolnexus import answer_declined
a = answer_declined("pnd-1")
assert a.ok is Falseassert a.id == "pnd-1"assert a.reason == "declined" # the default when none is given
print("ok:", a.id, "|", a.reason)2. The realistic case — an explicit reason, and an invalid one rejected
Section titled “2. The realistic case — an explicit reason, and an invalid one rejected”from toolnexus import answer_declined
cancelled = answer_declined("pnd-1", "cancelled")assert cancelled.ok is False and cancelled.reason == "cancelled"
expired = answer_declined("pnd-1", "expired")assert expired.reason == "expired"
try: answer_declined("pnd-1", "nope") # not in the closed vocabulary raise AssertionError("expected ValueError")except ValueError: pass
print("ok:", cancelled.reason, "/", expired.reason, "| invalid reason rejected")3. The full surface — a suspended run resumes with a decline, ok decides the branch
Section titled “3. The full surface — a suspended run resumes with a decline, ok decides the branch”import asyncio
from toolnexus import ToolResult, answer_declined, create_client, create_toolkit, define_tool, pending
class RecordingTransport: def __init__(self, responses): self._responses = list(responses)
def post(self, url, headers, payload, timeout): return self._responses.pop(0)
def open(self, url, headers, payload, timeout): # noqa: A003 raise NotImplementedError
calls = {"n": 0}
async def approve_deploy(args=None, ctx=None): calls["n"] += 1 if ctx is not None and ctx.answer is not None: return ToolResult(output="deployed", is_error=False) return pending(kind="approval", prompt="OK to deploy to prod?")
async def main(): transport = RecordingTransport([ { "choices": [{ "message": { "role": "assistant", "content": None, "tool_calls": [{"id": "c1", "type": "function", "function": {"name": "deploy", "arguments": "{}"}}], }, }], "usage": {"total_tokens": 5}, }, {"choices": [{"message": {"role": "assistant", "content": "ok"}}], "usage": {"total_tokens": 5}}, ]) tk = await create_toolkit(builtins=False, extra_tools=[ define_tool(approve_deploy, name="deploy", description="Deploy to prod.") ]) try: client = create_client( base_url="http://mock.local/v1", style="openai", model="mock", api_key="unused", http_transport=transport, wait_for=lambda req: answer_declined(req.id, "declined"), ) result = await client.run("deploy the latest build", tk)
# A decline resolves the pending call directly — it does NOT re-invoke the tool. # `answer_output`'s accepted case re-runs the tool with `ctx.answer` set; a decline # never reaches that second call at all, so `approve_deploy` ran exactly once. assert calls["n"] == 1 assert result.status == "done" assert result.tool_calls[0]["is_error"] is True assert result.tool_calls[0]["output"] == "declined/expired: OK to deploy to prod?"
print("ok:", result.tool_calls[0]["output"]) finally: await tk.close()
asyncio.run(main())See also
Section titled “See also”pending— Return a Pending from a tool to park the run until someone answers.answer_output— The success counterpart: wraps a human’s typed reply into the sameAnswershape.auth_required— 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.pending_of— Detect that a RunResult is parked rather than finished, and get the Request that parked it.