Skip to content

fix(adapters): propagate budget halts out of the LangChain callback - #55

Merged
prashar32 merged 1 commit into
mainfrom
fix/langchain-callback-propagates-halts
Jun 3, 2026
Merged

fix(adapters): propagate budget halts out of the LangChain callback#55
prashar32 merged 1 commit into
mainfrom
fix/langchain-callback-propagates-halts

Conversation

@prashar32

Copy link
Copy Markdown
Owner

The bug

The LangChain RiskKernelCallbackHandler enforces budgets by raising BudgetExceeded (and ApprovalDenied) from its callback hooks. But LangChain swallows exceptions raised inside a callback — it logs them and keeps running — unless the handler opts in with raise_error = True.

The handler never set it. So on a real LangChain agent, a budget halt raised in on_llm_start was silently dropped and the chain kept spending past its budget. Enforcement was a no-op on the exact path the adapter exists for.

It is the kind of bug a hooks-only unit test cannot see (calling on_llm_start() directly does raise) — only driving a real LangChain invoke() surfaces it.

The fix

Set raise_error = True on the handler so the deterministic halt propagates out of llm.invoke() and actually stops the run.

Tests (test_sdk.py)

  • test_langchain_handler_enforces_loop_budget — drives the hooks against the stub daemon; the loop budget halts at the cap, and asserts raise_error stays set so this can't silently regress (runs in CI, no langchain needed).
  • test_langchain_handler_gates_denied_tool — a denied tool raises ApprovalDenied.
  • test_langchain_integration_stops_runaway_loop@skipUnless(langchain installed): a real FakeListLLM in a runaway while True loop, confirmed to stop after exactly the budgeted number of calls. This is the test that actually catches the bug.

Verification

  • With langchain-core installed: all 12 SDK tests pass (the integration test runs and stops the loop after 2 calls).
  • Without it (the CI matrix, py3.9 / py3.12): 11 pass, the integration test skips. The regression guard still runs.

The callback handler raised BudgetExceeded / ApprovalDenied from its hooks, but
LangChain swallows exceptions thrown inside a callback (it logs them and keeps
running) unless the handler sets raise_error=True. So on a real LangChain agent
the halt was silently dropped and the chain kept spending past budget —
enforcement was a no-op.

Set raise_error=True so the deterministic halt actually stops the run.

Tests: a unit test drives the hooks against the stub daemon and asserts the loop
budget halts at the cap (and that raise_error stays set, so this can't silently
regress); a tool-gating test asserts a denied tool raises ApprovalDenied; and a
skip-unless-langchain integration test runs a real FakeListLLM in a runaway loop
and confirms it stops after exactly the budgeted number of calls.
@prashar32
prashar32 merged commit 8b017b6 into main Jun 3, 2026
7 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.

1 participant