anthropics / anthropics/claude-agent-sdk-typescript

Regression: 0.3.152+ emits signature-only empty thinking blocks in successful tool-use turns

Đang mở
#337 1 bình luận 1 reaction 0 người được giao Xem trên GitHub
bug
Ngôn ngữ chính
Shell
Star
1.8k
Fork
226
Chỉ số merge pull request
Không có pull request nào được merge trong 30 ngày

Mô tả

## Summary

`@anthropic-ai/claude-agent-sdk` appears to have regressed after `0.3.150`: successful tool-use turns can emit a completed assistant `thinking` block whose `thinking` text is empty, while the raw stream only contains a `signature_delta` and no `thinking_delta` text.

This reproduces with AWS Bedrock and Claude Code runtimes packaged by the Agent SDK. It affects SDK consumers that render or persist partial assistant events/transcripts because the SDK output contains a signed, completed `thinking` block with no displayable content.

## Expected behavior

For a successful turn with no displayable thinking text, one of these contracts should hold:

1. No assistant `thinking` block is emitted at all, or
2. A non-displayable/redacted metadata block is emitted in a way consumers can distinguish from visible thinking, or
3. The SDK documentation explicitly states that signature-only `thinking` blocks with `thinking: ""` are valid and must be suppressed by renderers/transcript consumers.

If `thinking: { type: "adaptive", display: "summarized" }` is requested and a summarized thinking block is emitted, I would expect non-empty `thinking_delta` text or no visible thinking block.

## Actual behavior

In `0.3.152+`, successful tool-use turns can produce this raw-event shape:

```json
{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":"","signature":""}}
{"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"..."}}
{"type":"content_block_stop","index":0}
```

The completed assistant message then contains:

```json
{"type":"thinking","thinking":"","signature":"..."}
```

There are zero `thinking_delta` events and zero displayable thinking characters.

## Environment

- SDK package: `@anthropic-ai/claude-agent-sdk`
- Provider: AWS Bedrock
- Region: `us-east-1`
- Models tested:
- `us.anthropic.claude-opus-4-7[1m]`
- `us.anthropic.claude-opus-4-8[1m]`
- Node: `v22.22.3`
- npm: `10.9.8`
- Local OS used for repro: macOS arm64
- Repro options:
- `thinking: { type: "adaptive", display: "summarized" }`
- `includePartialMessages: true`
- `permissionMode: "bypassPermissions"`
- `allowedTools: ["Bash"]`

## Reproduction script shape

This is the probe shape used for the matrices below. The important part is counting raw `SDKPartialAssistantMessage.event` records before any application-level rendering or transcript processing.

```js
import { query } from "@anthropic-ai/claude-agent-sdk";

const prompt = "Use the Bash tool exactly once to run `printf eval-alpha`, then reply with only DONE.";

const stats = {
rawThinkingStarts: 0,
rawThinkingDeltaEvents: 0,
rawThinkingDeltaChars: 0,
rawSignatureDeltas: 0,
finalThinkingBlocks: 0,
emptyFinalThinkingBlocks: 0,
finalThinkingChars: 0,
reportedModels: [],
resultSubtype: null,
};

for await (const msg of query({
prompt,
options: {
model: "us.anthropic.claude-opus-4-7[1m]",
thinking: { type: "adaptive", display: "summarized" },
includePartialMessages: true,
maxTurns: 3,
allowedTools: ["Bash"],
permissionMode: "bypassPermissions",
allowDangerouslySkipPermissions: true,
},
})) {
if (msg.type === "stream_event") {
const event = msg.event;
if (event?.type === "content_block_start" && event.content_block?.type === "thinking") {
stats.rawThinkingStarts += 1;
}
if (event?.type === "content_block_delta") {
const delta = event.delta;
if (delta?.type === "thinking_delta") {
stats.rawThinkingDeltaEvents += 1;
stats.rawThinkingDeltaChars += typeof delta.thinking === "string" ? delta.thinking.length : 0;
}
if (delta?.type === "signature_delta") stats.rawSignatureDeltas += 1;
}
if (event?.message?.model && !stats.reportedModels.includes(event.message.model)) {
stats.reportedModels.push(event.message.model);
}
}

if (msg.type === "assistant") {
for (const block of msg.message?.content ?? []) {
if (block?.type === "thinking") {
stats.finalThinkingBlocks += 1;
const text = typeof block.thinking === "string" ? block.thinking : "";
stats.finalThinkingChars += text.length;
if (!text.trim()) stats.emptyFinalThinkingBlocks += 1;
}
}
}

if (msg.type === "result") stats.resultSubtype = msg.subtype;
}

console.log(stats);
```

## Formal proof 1: this is a runtime-version regression

Definitions:

- A displayable thinking block exists iff either raw stream output contains at least one `thinking_delta` with non-empty `delta.thinking`, or the completed assistant message contains a `thinking` block whose `thinking.trim().length > 0`.
- A signature-only empty thinking block exists iff the raw stream emits a `thinking` block start and a `signature_delta`, but emits zero `thinking_delta` characters, and the completed assistant message contains `thinking: ""`.
- The application consumer cannot display text that is absent from `SDKPartialAssistantMessage.event` and absent from the completed assistant message.

Observed with the same prompt, provider, model family, and SDK options:

| SDK / Claude Code | Requested model | Result | Raw thinking_delta chars | Empty final thinking blocks | Conclusion |
| --- | --- | --- | ---: | ---: | --- |
| `0.3.150` / `2.1.150` | `us.anthropic.claude-opus-4-7[1m]` | success | 96 | 0 | Displayable thinking exists. |
| `0.3.152` / `2.1.152` | `us.anthropic.claude-opus-4-7[1m]` | success | 0 | 1 | Signature-only empty thinking block. |
| `0.3.153` / `2.1.153` | `us.anthropic.claude-opus-4-7[1m]` | success | 0 | 1 | Signature-only empty thinking block. |
| `0.3.156` / `2.1.156` | `us.anthropic.claude-opus-4-7[1m]` | success | 0 | 1 | Signature-only empty thinking block. |
| `0.3.157` / `2.1.157` | `us.anthropic.claude-opus-4-7[1m]` | success | 0 | 1 | Signature-only empty thinking block. |

Therefore, between `0.3.150` and `0.3.152`, successful tool-use turns started producing signed empty `thinking` blocks instead of displayable summarized thinking for the same observable SDK contract.

Note: `0.3.151` is not published in the npm registry, so the first published post-`0.3.150` candidate I could test was `0.3.152`.

## Formal proof 2: the issue follows runtime version, not AWS credentials/account

I also crossed two AWS Bedrock credential sets with old/new SDK runtimes using the same five safe Bash tool-use prompts: `echo_alpha`, `math_node`, `pwd_check`, `two_commands`, `list_count`.

Each run used:

- `us.anthropic.claude-opus-4-7[1m]`
- `thinking: { type: "adaptive", display: "summarized" }`
- `includePartialMessages: true`
- `permissionMode: "bypassPermissions"`

| Runtime | Credential set | Successes | Thinking blocks | Empty thinking blocks | Non-empty thinking chars | Signature-only thinking deltas |
| --- | --- | ---: | ---: | ---: | ---: | ---: |
| `0.3.153` / `2.1.153` | A | 5/5 | 5 | 5 | 0 | 5 |
| `0.3.150` / `2.1.150` | B | 5/5 | 4 | 0 | 846 | 4 |
| `0.3.150` / `2.1.150` | A | 5/5 | 5 | 0 | 2,362 | 5 |
| `0.3.153` / `2.1.153` | B | 5/5 | 5 | 5 | 0 | 5 |

Since `0.3.150` produced non-empty thinking on both credential sets, and `0.3.153` produced empty thinking on both credential sets, the observed behavior follows SDK/Claude Code runtime version rather than the AWS credential set.

## Formal proof 3: Opus 4.8 does not eliminate the issue on newer runtimes

I tested `us.anthropic.claude-opus-4-8[1m]`, which the stream reported as `claude-opus-4-8`.

| SDK / Claude Code | Requested model | Result | Raw thinking_delta chars | Empty final thinking blocks | Notes |
| --- | --- | --- | ---: | ---: | --- |
| `0.3.150` / `2.1.150` | `us.anthropic.claude-opus-4-8[1m]` | 5/5 successes | 67 | 0 | Can call 4.8; when thinking was emitted, it was non-empty. |
| `0.3.154` / `2.1.154` | `us.anthropic.claude-opus-4-8[1m]` | 5/5 successes | 0 | 5 | Latest produced empty thinking blocks. |
| `0.3.156` / `2.1.156` | `us.anthropic.claude-opus-4-8[1m]` | success | 0 | 1 | `next` still reproduced the issue. |
| `0.3.157` / `2.1.157` | `us.anthropic.claude-opus-4-8[1m]` | success | 0 | 1 | May 29 `latest`/`next` still reproduced the issue in a local Bedrock raw-stream probe. |

A single-prompt variant sweep on `0.3.154` + `4.8[1m]` also reproduced the empty signature-only shape for:

- `thinking: { type: "adaptive" }`
- `thinking: { type: "adaptive", display: "summarized" }`
- `thinking: { type: "adaptive", display: "omitted" }`
- `thinking: { type: "enabled", budgetTokens: 1024, display: "summarized" }`

`thinking: { type: "disabled" }` produced no thinking block, as expected.

Five-prompt repeat on May 29, 2026 with `us.anthropic.claude-opus-4-8[1m]`:

| Provider | SDK / Claude Code | Successes | Thinking blocks | Empty thinking blocks | Raw thinking_delta chars | Raw signature deltas |
| --- | --- | ---: | ---: | ---: | ---: | ---: |
| AWS Bedrock | `0.3.156` / `2.1.156` | 5/5 | 5 | 5 | 0 | 5 |
| AWS Bedrock | `0.3.157` / `2.1.157` | 5/5 | 5 | 5 | 0 | 5 |

## Formal proof 4: the data is absent at the SDK raw-event boundary

The SDK type for partial streaming messages is:

```ts
type SDKPartialAssistantMessage = {
type: "stream_event";
event: BetaRawMessageStreamEvent;
// ...
};
```

For affected runs, `SDKPartialAssistantMessage.event` contains a `thinking` block start and a `signature_delta`, but no `thinking_delta` text. The final assistant message also contains a `thinking` block with `thinking: ""`.

Therefore the missing displayable text is already absent at the earliest SDK event boundary available to consumers. A renderer/transcript consumer downstream of the SDK cannot recover the thinking text.

This does not prove whether the native Claude Code binary is faithfully forwarding a provider response or applying an internal transform before surfacing SDK events. It does prove that SDK consumers receive a signed empty visible-thinking block in successful turns.

## Formal proof 5: direct Anthropic provider did not reproduce the issue

I also tested the same raw SDK event boundary with Bedrock disabled and the direct Anthropic provider. This used local Claude Code auth rather than `ANTHROPIC_API_KEY` in the shell environment.

Single-prompt matrix, same safe Bash tool-use prompt and `thinking: { type: "adaptive", display: "summarized" }`:

| Provider | SDK / Claude Code | Requested model | Result | Raw thinking_delta chars | Empty final thinking blocks |
| --- | --- | --- | --- | ---: | ---: |
| Direct Anthropic | `0.3.150` / `2.1.150` | `claude-opus-4-7` | success | 103 | 0 |
| Direct Anthropic | `0.3.150` / `2.1.150` | `claude-opus-4-8` | success | 100 | 0 |
| Direct Anthropic | `0.3.152` / `2.1.152` | `claude-opus-4-7` | success | 103 | 0 |
| Direct Anthropic | `0.3.152` / `2.1.152` | `claude-opus-4-8` | success | 100 | 0 |
| Direct Anthropic | `0.3.153` / `2.1.153` | `claude-opus-4-7` | success | 156 | 0 |
| Direct Anthropic | `0.3.153` / `2.1.153` | `claude-opus-4-8` | success | 100 | 0 |
| Direct Anthropic | `0.3.154` / `2.1.154` | `claude-opus-4-7` | success | 108 | 0 |
| Direct Anthropic | `0.3.154` / `2.1.154` | `claude-opus-4-8` | success | 100 | 0 |
| Direct Anthropic | `0.3.156` / `2.1.156` | `claude-opus-4-7` | success | 172 | 0 |
| Direct Anthropic | `0.3.156` / `2.1.156` | `claude-opus-4-8` | success | 100 | 0 |
| Direct Anthropic | `0.3.157` / `2.1.157` | `claude-opus-4-7` | success | 108 | 0 |
| Direct Anthropic | `0.3.157` / `2.1.157` | `claude-opus-4-8` | success | 108 | 0 |

Five-prompt direct-provider controls against the Bedrock failure cases:

| Provider | SDK / Claude Code | Requested model | Successes | Thinking blocks | Empty thinking blocks | Raw thinking_delta chars | Raw signature deltas |
| --- | --- | --- | ---: | ---: | ---: | ---: | ---: |
| Direct Anthropic | `0.3.153` / `2.1.153` | `claude-opus-4-7` | 5/5 | 5 | 0 | 512 | 5 |
| Direct Anthropic | `0.3.154` / `2.1.154` | `claude-opus-4-8` | 5/5 | 5 | 0 | 546 | 5 |
| Direct Anthropic | `0.3.156` / `2.1.156` | `claude-opus-4-8` | 5/5 | 5 | 0 | 618 | 5 |
| Direct Anthropic | `0.3.157` / `2.1.157` | `claude-opus-4-8` | 5/5 | 5 | 0 | 530 | 5 |

Conclusion: the empty signature-only thinking issue did not reproduce on the direct Anthropic provider in this matrix. The currently observed failure appears specific to the Bedrock provider path in Claude Code/Agent SDK `0.3.152+`, including `0.3.156` and `0.3.157` in local raw-stream probes; the same SDK versions and prompts produced non-empty thinking on direct Anthropic.

## Impact

SDK consumers that render or persist assistant thinking blocks need to special-case signed empty thinking blocks. Without that special case, UIs can show completed empty reasoning sections and transcripts can contain completed assistant `thinking` blocks with no displayable content.

## Request

Can you clarify whether signature-only `thinking` blocks with `thinking: ""` are intentional SDK output?

If intentional, please document the contract and recommended consumer behavior. If unintentional, please either restore non-empty summarized thinking output for `display: "summarized"` or suppress visible `thinking` blocks when no `thinking_delta` text is available.

Hướng dẫn đóng góp

Chưa lập chỉ mục được hướng dẫn đóng góp cho kho mã nguồn này

Đánh giá

Issue này chưa được đánh giá.

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.