Repository navigation
fix(claude-agents): fail the run when the CLI reports an API error as success - #138
Merged
Merged
Conversation
… success The Claude Code CLI does not fail a run when an Anthropic API call fails (bad key, no credits, rate limit after retries). Measured against Agent SDK 0.3.220, it emits a synthetic assistant message (model "<synthetic>", the error text as content, zero usage) and then a result with subtype "success", is_error true, api_error_status set, and the error text in result. The handler only checked subtype, so it returned "Credit balance is too low" as the model output, invoke tracked generation success, and the trace showed an OK "chat <synthetic>" span. A result now fails the run when its subtype is not "success" or is_error is true, the same test the agent SDK applies itself. A synthetic assistant message produces no chat span, joins no conversation, and adds no usage. The native graph adapter applies the same check to every node, which also stops it from returning "" for an error-subtype result. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
XieX
marked this pull request as ready for review
October 15, 2026 20:51
jeffdupont
approved these changes
Oct 15, 2026
jeffdupont
left a comment
Contributor
There was a problem hiding this comment.
Matches ai-sdks-monorepo#62. assertRunSucceeded writes usage to the root before it throws and sets output attributes only after the check passes, so a failed span carries no gen_ai.output.messages. Streaming sends text only from stream_event deltas, so the synthetic message never reaches callers as a chunk. The native graph root goes to the existing invocation_failure catch, and a failed sub-agent throws from its tool executor. Tests cover every spec bullet; CI green.
XieX
added a commit
to launchdarkly/python-ai-sdk
that referenced
this pull request
Oct 19, 2026
… success (#174) ## Problem With an Anthropic key that had no credits, a Claude Agents run returned `{ output: 'Credit balance is too low' }` as a successful run. `config().invoke` tracked `$ld:ai:generation:success`, a caller would show the error text to a user as model output, and the trace showed an OK `chat <synthetic>` span. The Claude Code CLI does not fail a run when an API call fails. Captured live from Agent SDK 0.3.220 with a rejected key (no credits takes the same path): - an assistant message with `model: "<synthetic>"`, `error: "authentication_failed"`, zero usage, and the error text as content - then a result with `subtype: "success"`, **`is_error: true`**, `api_error_status: 401`, `terminal_reason: "api_error"`, and the error text in `result` The handlers checked only `subtype`. The agent SDK itself treats `is_error` as the failure signal and raises after the result, but the handler stops reading at the result and never sees that. ## Fix - `_raise_if_run_failed` fails a run when `subtype != "success"` or `is_error` is true. Both handler paths and `to_claude_agents` use it. - `to_claude_agents` previously read `message.result` with no check at all, so it returned the error text as the answer and also ignored error subtypes. A failed root now raises and tracks `$ld:ai:graph:invocation_failure`; a failed sub-agent fails its tool call. - An `AssistantMessage` whose `model` is `<synthetic>` gets no `chat` span, joins no conversation, and adds no usage. It keys on `model`, not the `error` field, because the CLI also emits a synthetic message after a response stops at `max_output_tokens`, and that run can still succeed. - `agents.md` documents the rule. ## Tests Six new tests, each failing on `main` for the intended reason: an API-error result on the blocking and streaming paths, a synthetic message that shares a real response's id in a run that then succeeds, and the native graph root (API error, and `error_max_turns`) and sub-agent cases. A seventh pins that `is_error: false` stays a success. Changed existing fixtures: `_make_result_msg` in `test_native_graph.py` now sets `subtype`, `is_error`, `errors` and `api_error_status` explicitly. Unset `MagicMock` attributes are truthy, so every existing mock result would otherwise read as an error. Also checked end to end against the real CLI with a fake key and an empty `CLAUDE_CONFIG_DIR`: both paths reject with `Claude agent run ended with an error result (HTTP 401): Invalid API key · Fix external API key`, and the only span is `invoke_agent` with status ERROR. Spec: TESTING.md §2.2 "Anthropic agents handler — run outcome from the `result` message" (launchdarkly/ai-sdks-monorepo#62). Counterpart: launchdarkly/js-ai-sdk#138. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Overview** > Fixes a case where Anthropic API failures (bad key, no credits, etc.) were treated as successful runs because the Claude Code CLI returns `ResultMessage` with `subtype: "success"` and `is_error: true`, with the error text in `result`. > > **`_raise_if_run_failed`** replaces the old subtype-only check: it raises on non-`success` subtypes (e.g. `error_max_turns`) and on `success` with `is_error`, including optional `api_error_status` in the error. Blocking invoke, streaming, and **`to_claude_agents`** all call it so callers get `RuntimeError` and generation/graph failure tracking instead of surfacing API error strings as model output. > > **Telemetry:** assistant messages with `model: "<synthetic>"` (CLI-written errors or max-token notices) are ignored in `InferenceSpans`—no `chat` span, no conversation/usage merge—so error text cannot pollute a real response that shares the same `message_id`. > > Docs in `agents.md` and expanded handler/native-graph tests cover API-error paths, streaming without a `done` event, and sub-agent failures. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit a453733. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
With an Anthropic key that had no credits, a Claude Agents run returned
{ output: 'Credit balance is too low' }as a successful run.config().invoketracked$ld:ai:generation:success, a caller would show the error text to a user as model output, and the trace showed an OKchat <synthetic>span.The Claude Code CLI does not fail a run when an API call fails. Captured live from Agent SDK 0.3.220 with a rejected key (no credits takes the same path):
model: "<synthetic>",error: "authentication_failed", zero usage, and the error text as contentsubtype: "success",is_error: true,api_error_status: 401,terminal_reason: "api_error", and the error text inresultThe handlers checked only
subtype. The agent SDK itself treatsis_erroras the failure signal and raises after the result, but the handler stops reading at the result and never sees that.Fix
assertRunSucceeded(exported fromhandler.ts, not from the package root) fails a run whensubtype !== 'success'oris_error === true, and narrows the message toSDKResultSuccessotherwise. Both handler paths andtoClaudeAgentsuse it.toClaudeAgentsnow gates onmessage.type === 'result'instead of'result' in message. The old gate also let an error-subtype result through as an empty answer. A failed root now rejects and tracks$ld:ai:graph:invocation_failure; a failed sub-agent fails its tool call.modelis<synthetic>gets nochatspan, joins no conversation, and adds no usage. It keys onmodel, not theerrorfield, because the CLI also emits a synthetic message after a response stops atmax_output_tokens, and that run can still succeed.agents.mddocuments the rule and drops the stale'result' in messageexample.Tests
Six new tests, each failing on
mainfor the intended reason: an API-error result on the blocking and streaming paths, a synthetic message that shares a real response's id in a run that then succeeds, and the native graph root (API error, anderror_max_turns) and sub-agent cases. A seventh pins thatis_error: falsestays a success.Changed existing fixtures: two
native-graph.test.tsresults yielded{ result, usage }with notype, which only worked through the old gate; they now carrytype: 'result', subtype: 'success'.The
clientpackage-exports snapshot test fails onmainindependently of this change; #137 fixes it.Also checked end to end against the real CLI with a fake key and an empty
CLAUDE_CONFIG_DIR: both paths reject withClaude agent run ended with an error result (HTTP 401): Invalid API key · Fix external API key, and the only span isinvoke_agentwith status ERROR.Spec: TESTING.md §2.2 "Anthropic agents handler — run outcome from the
resultmessage" (launchdarkly/ai-sdks-monorepo#62).Counterpart: launchdarkly/python-ai-sdk#174.
🤖 Generated with Claude Code
Note
Overview
Fixes a case where Claude Agent SDK runs could succeed while returning API error text (e.g. “Credit balance is too low”) because the CLI emits
subtype: 'success'withis_error: true.Run outcome: Adds exported
assertRunSucceededand uses it on blocking, streaming, andtoClaudeAgentspaths. Result handling now gates onmessage.type === 'result'instead of'result' in message, so error subtypes without aresultfield and API-error “success” results both throw (generation/graph failure tracking instead of treating error text as model output).Telemetry: Assistant messages with
model: '<synthetic>'are ignored forchatspans, conversation history, and usage so CLI-injected error notices do not pollute traces or span content when a run later succeeds.Docs in
agents.mdand new/updated tests cover API errors, streaming, synthetic messages, and native-graph root/sub-agent failures.Reviewed by Cursor Bugbot for commit f5258bb. Bugbot is set up for automated code reviews on this repo. Configure here.