Skip to content

feat(anthropic): handle pause_turn and refusal stop reasons#1003

Open
adamthehutt wants to merge 1 commit intoprism-php:mainfrom
adamthehutt:feat/anthropic-pause-turn-handling
Open

feat(anthropic): handle pause_turn and refusal stop reasons#1003
adamthehutt wants to merge 1 commit intoprism-php:mainfrom
adamthehutt:feat/anthropic-pause-turn-handling

Conversation

@adamthehutt
Copy link
Copy Markdown

Summary

Anthropic returns stop_reason="pause_turn" when a long-running server-side tool (web_search, web_fetch, code_execution, MCP connectors, etc.) needs the client to continue the turn, and stop_reason="refusal" when the model declines to respond. Today both fall through the Text handler's match and surface as a generic:

Anthropic: unknown finish reason

…with no information about which stop reason actually occurred. For requests using web_search / web_fetch provider tools this can fail an entire job after several minutes of work, with no way to recover or even diagnose what happened.

Changes

  • Add FinishReason::Pause and FinishReason::Refusal cases.
  • Map pause_turn and refusal in Anthropic\Maps\FinishReasonMap.
  • Implement pause_turn resume in Anthropic\Handlers\Text per Anthropic's documented protocol: append the assistant message to the conversation and re-send the request so the model can continue. Bounded by the existing maxSteps() guard, identical in shape to how handleToolCalls() already loops.
  • Throw a descriptive PrismException for refusal that names the stop reason.
  • Surface the raw stop_reason string in the fallback "unhandled finish reason" exception, so any future stop reason that Anthropic introduces is debuggable without patching the library.
  • Add fixture-backed tests covering:
    • The pause_turn resume flow producing a final response with two steps.
    • The pause_turn resume flow respecting maxSteps() and stopping early.
    • The refusal exception path.

Out of scope

The Stream handler is not modified. Streaming pause_turn resume is more involved (the partial assistant message needs to be reassembled from deltas before being re-sent) and is best handled in a follow-up PR. Today, streaming pause_turn will still fall through to the existing FinishReason::Stop default — which is no worse than current behaviour.

Test plan

  • vendor/bin/pest tests/Providers/Anthropic/ — 155 tests pass (3 new, 152 existing).
  • New tests:
    • pause_turn → it resumes the turn when Anthropic returns stop_reason="pause_turn"
    • pause_turn → it stops resuming once maxSteps is reached
    • exceptions → it throws a descriptive exception when Anthropic returns a refusal stop_reason

References

Anthropic returns stop_reason="pause_turn" when long-running server-side
tools (web_search, web_fetch, code_execution, MCP connectors, etc.) need
the client to continue the turn, and stop_reason="refusal" when the model
declines to respond. Both currently fall through the Text handler's match
and surface as a generic "Anthropic: unknown finish reason" exception
with no information about what actually happened.

This change:

- Adds FinishReason::Pause and FinishReason::Refusal cases.
- Maps "pause_turn" and "refusal" in Anthropic's FinishReasonMap.
- Implements pause_turn resume in the Text handler per Anthropic's
  documented protocol: append the assistant message to the conversation
  and re-send the request so the model can continue. Bounded by the
  existing maxSteps() guard.
- Throws a descriptive exception for refusal that names the stop reason.
- Surfaces the raw stop_reason string in the fallback "unhandled finish
  reason" exception so debugging future stop_reason additions doesn't
  require patching the library.
- Adds fixture-backed tests for the pause_turn resume flow (including
  the maxSteps boundary) and for the refusal exception path.

Stream handler is left unchanged in this PR; pause_turn resume in
streaming mode is more involved and can be addressed separately.
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.

1 participant