| ... | @@ -434,26 +434,9 @@ function resultErrorsText(result: SDKResultMessage): string { | ... | @@ -434,26 +434,9 @@ function resultErrorsText(result: SDKResultMessage): string { |
| 434 | : ""; | 434 | : ""; |
| 435 | } | 435 | } |
| 436 | | 436 | |
| 437 | /** | 437 | /** Failure text for structured terminal reasons, including success-tagged failures. */ |
| 438 | * First user-facing error from a non-success result. "[ede_diagnostic] ..." | 438 | function terminalResultError(reason: SDKResultMessage["terminal_reason"]): string | undefined { |
| 439 | * entries are CLI-internal telemetry (the CLI hides them from its own UI too), | 439 | switch (reason) { |
| 440 | * so they must never become the error banner. | | |
| 441 | */ | | |
| 442 | function resultUserFacingError(result: SDKResultMessage): string | undefined { | | |
| 443 | const listed = | | |
| 444 | result.subtype === "success" || !Array.isArray(result.errors) | | |
| 445 | ? undefined | | |
| 446 | : result.errors.find((error) => !error.startsWith("[ede_diagnostic]")); | | |
| 447 | if (listed) { | | |
| 448 | return listed; | | |
| 449 | } | | |
| 450 | // Structured failure markers for results whose error list is empty or | | |
| 451 | // diagnostic-only: an overloaded API (529) and the terminal reasons the | | |
| 452 | // CLI stamps when it gives up on a turn. | | |
| 453 | if (isOverloadedResult(result)) { | | |
| 454 | return "Claude API is overloaded (529). Try again shortly."; | | |
| 455 | } | | |
| 456 | switch (result.terminal_reason) { | | |
| 457 | case "api_error": | 440 | case "api_error": |
| 458 | return "Claude gave up after repeated API errors."; | 441 | return "Claude gave up after repeated API errors."; |
| 459 | case "malformed_tool_use_exhausted": | 442 | case "malformed_tool_use_exhausted": |
| ... | @@ -1559,27 +1542,6 @@ const buildUserMessageEffect = Effect.fn("buildUserMessageEffect")(function* ( | ... | @@ -1559,27 +1542,6 @@ const buildUserMessageEffect = Effect.fn("buildUserMessageEffect")(function* ( |
| 1559 | return buildUserMessage({ sdkContent }); | 1542 | return buildUserMessage({ sdkContent }); |
| 1560 | }); | 1543 | }); |
| 1561 | | 1544 | |
| 1562 | /** | | |
| 1563 | * terminal_reason values the CLI classifies as dead turns: the turn died | | |
| 1564 | * rather than finished, even when the result subtype is success and the | | |
| 1565 | * error list is empty. Kept in sync with the messages in | | |
| 1566 | * resultUserFacingError. | | |
| 1567 | */ | | |
| 1568 | const FAILED_TERMINAL_REASONS: ReadonlySet<NonNullable<SDKResultMessage["terminal_reason"]>> = | | |
| 1569 | new Set([ | | |
| 1570 | "api_error", | | |
| 1571 | "malformed_tool_use_exhausted", | | |
| 1572 | "budget_exhausted", | | |
| 1573 | "structured_output_retry_exhausted", | | |
| 1574 | "tool_deferred_unavailable", | | |
| 1575 | "turn_setup_failed", | | |
| 1576 | "blocking_limit", | | |
| 1577 | "rapid_refill_breaker", | | |
| 1578 | "prompt_too_long", | | |
| 1579 | "image_error", | | |
| 1580 | "model_error", | | |
| 1581 | ]); | | |
| 1582 | | | |
| 1583 | /** | 1545 | /** |
| 1584 | * The CLI reports repeated 529 overload failures as a success-subtype result | 1546 | * The CLI reports repeated 529 overload failures as a success-subtype result |
| 1585 | * with api_error_status 529 and an empty error list; the status code is the | 1547 | * with api_error_status 529 and an empty error list; the status code is the |
| ... | @@ -1589,25 +1551,27 @@ function isOverloadedResult(result: SDKResultMessage): boolean { | ... | @@ -1589,25 +1551,27 @@ function isOverloadedResult(result: SDKResultMessage): boolean { |
| 1589 | return result.subtype === "success" && result.api_error_status === 529; | 1551 | return result.subtype === "success" && result.api_error_status === 529; |
| 1590 | } | 1552 | } |
| 1591 | | 1553 | |
| 1592 | function turnStatusFromResult(result: SDKResultMessage): ProviderRuntimeTurnStatus { | 1554 | /** Derives turn status and its error from the same provider result. */ |
| 1593 | if ( | 1555 | function resultOutcome(result: SDKResultMessage): { |
| 1594 | isOverloadedResult(result) || | 1556 | status: ProviderRuntimeTurnStatus; |
| 1595 | (result.terminal_reason !== undefined && FAILED_TERMINAL_REASONS.has(result.terminal_reason)) | 1557 | errorMessage: string | undefined; |
| 1596 | ) { | 1558 | } { |
| 1597 | return "failed"; | 1559 | const structuredError = isOverloadedResult(result) |
| 1598 | } | 1560 | ? "Claude API is overloaded (529). Try again shortly." |
| 1599 | if (result.subtype === "success") { | 1561 | : terminalResultError(result.terminal_reason); |
| 1600 | return "completed"; | 1562 | // CLI diagnostic entries must not become the error banner. |
| 1601 | } | 1563 | const listedError = |
| 1602 | | 1564 | result.subtype === "success" || !Array.isArray(result.errors) |
| 1603 | const errors = resultErrorsText(result); | 1565 | ? undefined |
| 1604 | if (isInterruptedResult(result)) { | 1566 | : result.errors.find((error) => !error.startsWith("[ede_diagnostic]")); |
| 1605 | return "interrupted"; | 1567 | const errorMessage = listedError || structuredError; |
| 1606 | } | 1568 | if (structuredError !== undefined) return { status: "failed", errorMessage }; |
| 1607 | if (errors.includes("cancel")) { | 1569 | if (result.subtype === "success") return { status: "completed", errorMessage }; |
| 1608 | return "cancelled"; | 1570 | if (isInterruptedResult(result)) return { status: "interrupted", errorMessage }; |
| 1609 | } | 1571 | return { |
| 1610 | return "failed"; | 1572 | status: resultErrorsText(result).includes("cancel") ? "cancelled" : "failed", |
| | 1573 | errorMessage, |
| | 1574 | }; |
| 1611 | } | 1575 | } |
| 1612 | | 1576 | |
| 1613 | function streamKindFromDeltaType(deltaType: string): ClaudeTextStreamKind { | 1577 | function streamKindFromDeltaType(deltaType: string): ClaudeTextStreamKind { |
| ... | @@ -3268,8 +3232,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( | ... | @@ -3268,8 +3232,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 3268 | return; | 3232 | return; |
| 3269 | } | 3233 | } |
| 3270 | | 3234 | |
| 3271 | const status = turnStatusFromResult(message); | 3235 | const { status, errorMessage } = resultOutcome(message); |
| 3272 | const errorMessage = resultUserFacingError(message); | | |
| 3273 | | 3236 | |
| 3274 | if (status === "failed") { | 3237 | if (status === "failed") { |
| 3275 | yield* emitRuntimeError(context, errorMessage ?? "Claude turn failed."); | 3238 | yield* emitRuntimeError(context, errorMessage ?? "Claude turn failed."); |