Skip to content

fix(claude-agents): fail the run when the CLI reports an API error as success - #174

Merged
XieX merged 2 commits into
mainfrom
xie/claude-agents-synthetic-error
Oct 19, 2026
Merged

XieX merged 2 commits into
mainfrom
xie/claude-agents-synthetic-error

Conversation

@XieX

@XieX XieX commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

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


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.

Reviewed by Cursor Bugbot for commit a453733. Bugbot is set up for automated code reviews on this repo. Configure here.

… 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
XieX requested review from apucacao and jeffdupont October 15, 2026 20:57
@XieX
XieX marked this pull request as ready for review October 15, 2026 20:57

@jeffdupont jeffdupont 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.

Matches ai-sdks-monorepo#62 and js#138. _raise_if_run_failed writes usage to the root before it raises, on both paths and in _run_query. A streamed failure closes open AG-UI tool calls and re-raises with no done event. _is_synthetic keys on model, and the chat-span grouping uses message_id, which matches the SDK's AssistantMessage. The 7 new tests mirror the JS set; CI green.

XieX added a commit to launchdarkly/js-ai-sdk that referenced this pull request Oct 19, 2026
… success (#138)

## 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

- `assertRunSucceeded` (exported from `handler.ts`, not from the package
root) fails a run when `subtype !== 'success'` or `is_error === true`,
and narrows the message to `SDKResultSuccess` otherwise. Both handler
paths and `toClaudeAgents` use it.
- `toClaudeAgents` now gates on `message.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.
- An assistant message 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 and drops the stale `'result' in
message` example.

## 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: two `native-graph.test.ts` results yielded `{
result, usage }` with no `type`, which only worked through the old gate;
they now carry `type: 'result', subtype: 'success'`.

The `client` package-exports snapshot test fails on `main` independently
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 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/python-ai-sdk#174.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!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'` with **`is_error: true`**.
> 
> **Run outcome:** Adds exported **`assertRunSucceeded`** and uses it on
blocking, streaming, and **`toClaudeAgents`** paths. Result handling now
gates on **`message.type === 'result'`** instead of `'result' in
message`, so error subtypes without a `result` field 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 for `chat` spans, conversation history, and usage so
CLI-injected error notices do not pollute traces or span content when a
run later succeeds.
> 
> Docs in **`agents.md`** and new/updated tests cover API errors,
streaming, synthetic messages, and native-graph root/sub-agent failures.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
f5258bb. 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>
@XieX
XieX merged commit 3d1a768 into main Oct 19, 2026
7 checks passed
@XieX
XieX deleted the xie/claude-agents-synthetic-error branch October 19, 2026 15:16
@github-actions github-actions Bot mentioned this pull request Oct 16, 2026
jeffdupont pushed a commit that referenced this pull request Oct 23, 2026
🤖 I have created a release *beep* *boop*
---


<details><summary>launchdarkly-ai-server: 0.4.0</summary>

##
[0.4.0](launchdarkly-ai-server-0.3.0...launchdarkly-ai-server-0.4.0)
(2026-10-23)


### ⚠ BREAKING CHANGES

* **evaluations:** keep the management API client internal
([#202](#202))
* **evaluations:** rename base uri to match spec
([#199](#199))
* **evaluations:** add test proving harness edge case matches spec
([#200](#200))
* **evaluations:** route handlers by provider and mode
* **evaluations:** mark evaluations from code as experimental
([#157](#157))

### Experimental

* **evaluations:** add test proving harness edge case matches spec
([#200](#200))
([fa7162e](fa7162e))
* **evaluations:** keep the management API client internal
([#202](#202))
([17270ed](17270ed))
* **evaluations:** mark evaluations from code as experimental
([#157](#157))
([1389765](1389765))
* **evaluations:** rename base uri to match spec
([#199](#199))
([7d93f9c](7d93f9c))
* **evaluations:** route handlers by provider and mode
([f62591f](f62591f))
* **evaluations:** Set JSON as standard for output to pass to judge
([#203](#203))
([ffc728b](ffc728b))
* **skills:** Correct the HTTP 403 advice now that server-side FDv2 is
available to every account
([#178](#178))
([b17e322](b17e322))


### Features

* **agui:** add a shared reader for handler streams
([#164](#164))
([fc08dd5](fc08dd5))
* **AIC-3534:** add a TypeSafe Jev judge handler
([#149](#149))
([1b41ea4](1b41ea4))
* **client:** add configure for SDK defaults
([#158](#158))
([3736767](3736767))
* **langchain:** emit AG-UI text and tool events
([#163](#163))
([ab5e9e1](ab5e9e1))
* **openai:** emit AG-UI text and tool events
([#161](#161))
([2b1b2d4](2b1b2d4))


### Bug Fixes

* **AIC-3570:** re-resolve graph flags on every invoke and stream
([#187](#187))
([8fdc4d4](8fdc4d4))
* **AIC-3571:** visit every child before its parent in reverse_traverse
([#189](#189))
([87c73d9](87c73d9))
* **AIC-3572:** treat a variation without _ldMeta.enabled as disabled
([#184](#184))
([7a0d260](7a0d260))
* **AIC-3574:** build the LaunchDarkly client off the event loop
([#185](#185))
([bf03b6c](bf03b6c))
* **AIC-3576:** record deferred judge scores under the evaluated config
([#190](#190))
([354d3f1](354d3f1))
* **AIC-3577:** send object tool results to the model as JSON
([#188](#188))
([dead4bd](dead4bd))
* **AIC-3596:** name the [otel] extra in the missing-OTel warning
([#186](#186))
([90ce7bf](90ce7bf))
* **client:** match a handler's provides_for after normalizing its mode
([f62591f](f62591f))
* give native graph spans their LaunchDarkly identity
([#121](#121))
([e863b6d](e863b6d))
</details>

<details><summary>launchdarkly-ai-python: 0.3.0</summary>

##
[0.3.0](launchdarkly-ai-python-0.2.0...launchdarkly-ai-python-0.3.0)
(2026-10-23)


### ⚠ BREAKING CHANGES

* **evaluations:** mark evaluations from code as experimental
([#157](#157))

### Experimental

* **evaluations:** mark evaluations from code as experimental
([#157](#157))
([1389765](1389765))
</details>

<details><summary>launchdarkly-ai-claude-agents: 0.2.6</summary>

##
[0.2.6](launchdarkly-ai-claude-agents-0.2.5...launchdarkly-ai-claude-agents-0.2.6)
(2026-10-23)


### Features

* **claude:** emit AG-UI text and tool events
([#165](#165))
([3f3a0ce](3f3a0ce))
* **client:** add configure for SDK defaults
([#158](#158))
([3736767](3736767))


### Bug Fixes

* **AIC-3571:** visit every child before its parent in reverse_traverse
([#189](#189))
([87c73d9](87c73d9))
* **AIC-3577:** send object tool results to the model as JSON
([#188](#188))
([dead4bd](dead4bd))
* **claude-agents:** fail the run when the CLI reports an API error as
success
([#174](#174))
([3d1a768](3d1a768))
* give native graph spans their LaunchDarkly identity
([#121](#121))
([e863b6d](e863b6d))
</details>

<details><summary>launchdarkly-ai-claude-messages: 0.2.5</summary>

##
[0.2.5](launchdarkly-ai-claude-messages-0.2.4...launchdarkly-ai-claude-messages-0.2.5)
(2026-10-23)


### Features

* **claude:** emit AG-UI text and tool events
([#165](#165))
([3f3a0ce](3f3a0ce))
* **client:** add configure for SDK defaults
([#158](#158))
([3736767](3736767))


### Bug Fixes

* **AIC-3577:** send object tool results to the model as JSON
([#188](#188))
([dead4bd](dead4bd))
</details>

<details><summary>launchdarkly-ai-openai-agents: 0.2.6</summary>

##
[0.2.6](launchdarkly-ai-openai-agents-0.2.5...launchdarkly-ai-openai-agents-0.2.6)
(2026-10-23)


### Features

* **client:** add configure for SDK defaults
([#158](#158))
([3736767](3736767))
* **openai:** emit AG-UI text and tool events
([#161](#161))
([2b1b2d4](2b1b2d4))


### Bug Fixes

* **AIC-3571:** visit every child before its parent in reverse_traverse
([#189](#189))
([87c73d9](87c73d9))
* **AIC-3577:** send object tool results to the model as JSON
([#188](#188))
([dead4bd](dead4bd))
* give native graph spans their LaunchDarkly identity
([#121](#121))
([e863b6d](e863b6d))
</details>

<details><summary>launchdarkly-ai-vercel-agents: 0.1.1</summary>

##
[0.1.1](launchdarkly-ai-vercel-agents-0.1.0...launchdarkly-ai-vercel-agents-0.1.1)
(2026-10-23)


### Features

* **AIC-3408:** add Vercel AI SDK messages and agents adapters
([#103](#103))
([64d679e](64d679e))
</details>

<details><summary>launchdarkly-ai-litellm-agents: 0.1.1</summary>

##
[0.1.1](launchdarkly-ai-litellm-agents-0.1.0...launchdarkly-ai-litellm-agents-0.1.1)
(2026-10-23)


### Features

* add LiteLLM messages and agents integrations
([#102](#102))
([f2e9886](f2e9886))
</details>

<details><summary>launchdarkly-ai-openai-messages: 0.2.5</summary>

##
[0.2.5](launchdarkly-ai-openai-messages-0.2.4...launchdarkly-ai-openai-messages-0.2.5)
(2026-10-23)


### Features

* **client:** add configure for SDK defaults
([#158](#158))
([3736767](3736767))
* **openai:** emit AG-UI text and tool events
([#161](#161))
([2b1b2d4](2b1b2d4))


### Bug Fixes

* **AIC-3577:** send object tool results to the model as JSON
([#188](#188))
([dead4bd](dead4bd))
</details>

<details><summary>launchdarkly-ai-vercel-messages: 0.1.1</summary>

##
[0.1.1](launchdarkly-ai-vercel-messages-0.1.0...launchdarkly-ai-vercel-messages-0.1.1)
(2026-10-23)


### Features

* **AIC-3408:** add Vercel AI SDK messages and agents adapters
([#103](#103))
([64d679e](64d679e))
</details>

<details><summary>launchdarkly-ai-litellm-messages: 0.1.1</summary>

##
[0.1.1](launchdarkly-ai-litellm-messages-0.1.0...launchdarkly-ai-litellm-messages-0.1.1)
(2026-10-23)


### Features

* add LiteLLM messages and agents integrations
([#102](#102))
([f2e9886](f2e9886))
</details>

<details><summary>launchdarkly-ai-langchain-agents: 0.2.6</summary>

##
[0.2.6](launchdarkly-ai-langchain-agents-0.2.5...launchdarkly-ai-langchain-agents-0.2.6)
(2026-10-23)


### Features

* **client:** add configure for SDK defaults
([#158](#158))
([3736767](3736767))
* **langchain:** emit AG-UI text and tool events
([#163](#163))
([ab5e9e1](ab5e9e1))


### Bug Fixes

* **AIC-3577:** send object tool results to the model as JSON
([#188](#188))
([dead4bd](dead4bd))
* give native graph spans their LaunchDarkly identity
([#121](#121))
([e863b6d](e863b6d))
</details>

<details><summary>launchdarkly-ai-langchain-messages: 0.2.5</summary>

##
[0.2.5](launchdarkly-ai-langchain-messages-0.2.4...launchdarkly-ai-langchain-messages-0.2.5)
(2026-10-23)


### Features

* **client:** add configure for SDK defaults
([#158](#158))
([3736767](3736767))
* **langchain:** emit AG-UI text and tool events
([#163](#163))
([ab5e9e1](ab5e9e1))


### Bug Fixes

* **AIC-3577:** send object tool results to the model as JSON
([#188](#188))
([dead4bd](dead4bd))
</details>

<details><summary>launchdarkly-ai-opencode-agents: 0.2.0</summary>

##
[0.2.0](launchdarkly-ai-opencode-agents-0.1.0...launchdarkly-ai-opencode-agents-0.2.0)
(2026-10-23)


### ⚠ BREAKING CHANGES

* **opencode:** start and stop opencode serve in the handler
([#198](#198))

### Features

* **opencode-agents:** add an OpenCode handler
([#159](#159))
([fc08e8c](fc08e8c))
* **opencode-agents:** emit the AG-UI text and tool subset
([#162](#162))
([7677621](7677621))
* **opencode-agents:** open one OpenCode session per call
([#179](#179))
([b225113](b225113))
* **opencode-agents:** render agent instruction templates
([#182](#182))
([83175aa](83175aa))
* **opencode:** start and stop opencode serve in the handler
([#198](#198))
([0748dcf](0748dcf))


### Bug Fixes

* **opencode-agents:** own the HTTP client
([#194](#194))
([ed2d419](ed2d419))
* **opencode-agents:** skip judges and record structured history
([#177](#177))
([6fadf1d](6fadf1d))
</details>

<details><summary>launchdarkly-ai-typesafe: 0.1.1</summary>

##
[0.1.1](launchdarkly-ai-typesafe-0.1.0...launchdarkly-ai-typesafe-0.1.1)
(2026-10-23)


### Features

* **AIC-3534:** add a TypeSafe Jev judge handler
([#149](#149))
([1b41ea4](1b41ea4))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
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