Fix: propagate max_tokens stop reason instead of throwing internal error (#365)

## Summary

- When the Claude SDK reports `stop_reason: "max_tokens"`, the ACP
adapter now returns `{ stopReason: "max_tokens" }` instead of throwing a
`RequestError.internalError` (JSON-RPC `-32603`)
- Scoped to `success` and `error_during_execution` result subtypes only
— `error_max_turns` / `error_max_budget_usd` still correctly map to
`max_turn_requests`

## Problem

When tokens are exhausted, the SDK sends a `result` message with
`stop_reason: "max_tokens"` and `is_error: true`. The adapter hit the
`is_error` check first and threw an internal error, so ACP clients never
received the `stopReason` field. Downstream tools like
[toad](https://github.com/batrachianai/toad/discussions/176#discussioncomment-15445958)
saw a JSON-RPC exception instead of a clean stop reason.

## Fix

Check `stop_reason === "max_tokens"` before the `is_error` throw in the
`success` and `error_during_execution` branches. Token exhaustion is a
normal stop condition, not an internal error.

## Test plan

- [x] Added 5 unit tests covering:
  - `success` + `max_tokens` + `is_error: false` → returns `max_tokens`
- `success` + `max_tokens` + `is_error: true` → returns `max_tokens`
(main bug)
- `error_during_execution` + `max_tokens` + `is_error: true` → returns
`max_tokens`
- `success` + `null` stop_reason → still returns `end_turn` (no
regression)
- `success` + `is_error: true` + non-max_tokens → still throws (no
regression)
- [x] All 100 tests pass
- [x] Build passes

Fixes #235

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Luis
2026-03-03 06:58:40 -05:00
committed by GitHub
parent 6253b1611e
commit 5c1863d56e
2 changed files with 158 additions and 1 deletions

View File

@@ -543,12 +543,18 @@ export class ClaudeAcpAgent implements Agent {
if (message.result.includes("Please run /login")) {
throw RequestError.authRequired();
}
if (message.stop_reason === "max_tokens") {
return { stopReason: "max_tokens", usage };
}
if (message.is_error) {
throw RequestError.internalError(undefined, message.result);
}
return { stopReason: "end_turn", usage };
}
case "error_during_execution":
if (message.stop_reason === "max_tokens") {
return { stopReason: "max_tokens", usage };
}
if (message.is_error) {
throw RequestError.internalError(
undefined,