Skip to content

Tell the Python LangGraph Bot's model why a tool call was refused - #524

Merged
davidmckayv merged 2 commits into
CopilotKit:mainfrom
kevin9327:fix/langgraph-agui-refusal-reason
Sep 14, 2026
Merged

davidmckayv merged 2 commits into
CopilotKit:mainfrom
kevin9327:fix/langgraph-agui-refusal-reason

Conversation

@kevin9327

Copy link
Copy Markdown
Contributor

What this changes

When the deployment will not run a tool call from the Python LangGraph Bot (agent-langgraph-agui), /api/agent-tools/call answers with a status and the reason under error: a token it no longer accepts, a token issued to another Bot, or a malformed call (400). The Bot dropped the reason, so its model was told only:

Refused. Tool callback returned HTTP 403.

The model could say a call was refused but not why, and could not correct a call the deployment had named as malformed. The TypeScript LangGraph Bot passes the reason on since #518 (agent-langgraph/src/tool-answer.ts); this is the same reading for the Python one:

Refused. Tool callback returned HTTP 403. That token is not for this Bot.

The reason is read on its own, so a body that is not JSON (a proxy's HTML error page) still reads as Refused. Tool callback returned HTTP 502. rather than falling into the "could not be completed" branch, and a blank error adds nothing. A successful call is untouched.

Where it runs

  • New state that outlives a request? None. One more read of a response body already in hand.
  • What happens on the second replica? Nothing changes; the Bot process holds no state for this.
  • Anything serialised? No.
  • Anything fanned out to a browser? No.
  • New listener, port, or schedule? No.

Boundary and audit

  • Every acting call still goes through the gateway: resolve, decide, audit, then act. The refusal and its mcp.callback_refused row are unchanged; only what the Bot tells its model changes.
  • New refusals and new failures each write a row. No new refusals.
  • Nothing new is trusted from the client that the server can resolve itself. The text comes from this deployment's own refusal.

Changelog

  • A line in CHANGELOG.md under Unreleased.

Proof

New parametrised test in agent-langgraph-agui/tests/test_tool_protocol.py, driven through the real AG-UI endpoint against the file's existing loopback callback server (the fixture can now reply with a chosen body).

Before the fix:

FAILED ...test_http_refusal_tells_the_model_the_deployments_reason[403-...]
FAILED ...test_http_refusal_tells_the_model_the_deployments_reason[400-...]
E       - Refused. Tool callback returned HTTP 400. Tool args must be a JSON object.
E       + Refused. Tool callback returned HTTP 400.
2 failed, 3 passed

(The blank-reason and non-JSON 502 cases already passed and stay as guards.)

After, the whole Bot suite as CI runs it:

python -m pytest agent-langgraph-agui/tests -q
2 failed, 55 passed, 3 skipped

The two failures are test_chatgpt_token_provider_atomic_writer_*, which run docker run and write under /tmp. They fail identically on main on this machine (no Docker), so they are environmental, not this change.

🤖 Generated with Claude Code

When the deployment refuses a callback from the Python LangGraph Bot it
answers with a status and the reason under `error`: a token it no longer
accepts, one issued to another Bot, or a malformed call. The Bot dropped
the reason and told its model only "Refused. Tool callback returned HTTP
403.", so the model could say a call was refused but not why, and could
not correct a call the deployment had named as malformed.

The reason now follows the status, the way the TypeScript LangGraph Bot
passes it on since CopilotKit#518. It is read on its own, so a body that is not
JSON still reads as the refusal it was rather than as a failed callback.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@davidmckayv davidmckayv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep-reviewed clean (validation, no secret leak, fail-closed, agrees with existing layers). CI green.

@davidmckayv
davidmckayv merged commit 3137a35 into CopilotKit:main Sep 14, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants