Skip to content

answer_declined

Python · package toolnexus · SPEC §10 · python/src/toolnexus/types.py

def answer_declined(id: str, reason: str = "declined") -> Answer

Wraps 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.

  • A human explicitly refuses a pending(kind="approval", ...) — “not OK to deploy to prod” — and you want that refusal to reach the tool as ctx.answer.ok is False, distinct from a successful reply.
  • A Request goes stale before anyone answers it — resolve it with answer_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”.

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 False
assert 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())
  • 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 same Answer shape.
  • 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.