Repository navigation
Agent runs: listener, limits, cancellation, token usage, refusal handling and current models - #12
Merged
Merged
Conversation
…n a failing tool A result built with LLMMessageContents::fromErrorString() reached Claude as a success: the encoder read the error flag of the wrapping user message, which the agent loop always creates without one. And a tool that threw (or whose promise was rejected) aborted the whole run, so one failing lookup killed a two-minute agent turn. The model can only recover from an error it is told about. Encode is_error from each tool result's own content, and turn exceptions, rejected promises and wrongly typed results of ToolDefinition::handle() into error results carrying the exception class and message. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The agent framework logs tokens and cost per model call and per turn, but a response only exposed cumulative uncached input/output: cache writes and reads were folded into the price and lost, so "input tokens" hid most of the real prompt, and 1-hour cache writes (2x base input) were billed at the 5-minute rate. Add a TokenUsage value object (uncached input, output, 5m/1h cache writes, cache reads, reasoning), accumulate it on the request like the cost, and expose getUsage() (run), getStepUsage() (this call) and getTotalCostUsd() on the response. Anthropic models get explicit cache prices and a rate card per prompt size; OpenAI and Gemini report their cached and reasoning tokens per step. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Valid API responses threw in the decoder: Opus 5.5 / Sonnet 5.5 safety classifiers answer with stop_reason "refusal" (HTTP 200), server tools pause with "pause_turn", and redacted_thinking or server-tool blocks raised "Unsupported message type" - in a background agent job that is a crashed turn. Thinking and server blocks must also go back unchanged, or the next request is rejected. Add StopReason::REFUSAL (with getStopDetails()) and PAUSE_TURN, map model_context_window_exceeded to LENGTH, decode redacted_thinking into LLMMessageRedactedReasoning and any other block into LLMMessageOpaque that is re-sent as received (empty inputs restored as JSON objects), in both the plain and the streaming path; both types survive the conversation JSON round trip. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Agent turns run as background jobs with a progress UI: the job must show each model step and tool call, stop a looping model before it burns the budget or outlives the cron route, honour a "stop" button, and still store a transcript that the next turn can replay. A refusal must end the run instead of being returned as if it were an answer, and a paused server-tool turn must just go on. Add an optional AgentRunOptions argument to run()/runAsync(): an AgentListener (model response with step usage, tool start, tool finish with error flag and duration), maxIterations, maxCostUsd, deadline and a cancel callback. Hitting one throws AgentRunInterruptedException with the reason, usage, cost and a conversation whose open tool uses are closed with error results; refusals end the run the same way without the refused turn; pause_turn is resumed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…that reject it Claude Opus 4.7+, Sonnet 5 and Fable reject sampling parameters with a 400, yet the encoder always sent temperature (0.0 by default), so callers had to guess a value the model happens to accept (the eshop passes 1.0 as a workaround). Allow temperature: null to omit it for every provider, and let Anthropic models declare supportsSamplingParameters(); the encoder drops temperature for those whatever the caller passed. The 0.0 default stays for older models. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…effort Claude's effort scale is low/medium/high/xhigh/max, but the library had no xhigh and only reached max through EXTRA_HIGH. NONE simply omitted thinking, which on Opus 5.5 and the other adaptive-by-default models still runs thinking at the default effort (medium on Opus 5.5) - the opposite of what was asked. Add ReasoningEffort::XHIGH and MAX (EXTRA_HIGH keeps mapping to max), let models declare hasAdaptiveThinkingByDefault(), and send the lowest effort for NONE there; OpenAI maps the top levels to xhigh, Gemini to high. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Opus 5.5 and Sonnet 5.5 return their between-tool progress notes only with thinking.display "updates" (beta thinking-display-updates-2026-08-18); without it a long agent turn looks silent for minutes. Beta headers could only be set for the whole client, so a request-level option had no way to switch its beta. Add LLMRequest's thinkingDisplay (summarized / updates / omitted), encode it on the thinking object (or alone on models that think by default), and let the encoder report the betas a request needs; the client merges them with its own for messages, streaming and batches. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Agent conversations grow with every screenshot and tool result. Pruning old tool results client-side edits earlier turns, which on Opus 5.5 / Sonnet 5.5 invalidates the replayed thinking blocks (a 400 for accounts created after 2026-08-31) and the prompt cache; server-side context editing never does. Add LLMRequest's contextManagement with ClearToolUses (trigger, keep, clear inputs, excluded tools, minimum cleared) encoded as context_management.edits with the context-management beta header. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tween turns Agent turns repeat a large, stable system prompt and need to add per-turn operator notes (the editor's current state) without editing earlier turns: editing the history breaks the prompt cache and, on Opus 5.5 / Sonnet 5.5, the replayed thinking blocks. The encoder sent the system prompt uncached and rejected any second system message. A system message created with cached: true now gets a cache breakpoint (with the conversation cache TTL). Later system messages go out as mid-conversation role "system" messages where the model and position allow it, and are otherwise appended to the neighbouring user message, never between a tool use and its result. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…mini 4 Argon The agent framework runs on Claude Sonnet 5.5, which the library did not know; Haiku 5.5 is the cheap sibling for sub-tasks. Cost logging must match the provider's rate cards, including Sonnet 5.5's 0.05x cache reads and Haiku 5.5's higher prices for prompts over 100,000 tokens. Add both Claude classes with prices from the Anthropic pricing page, their capability flags, and Haiku's prompt-length rate card; gpt-6.1-sol and gemini-4-argon are added from their pricing pages. A capability matrix test covers every current Claude class. Sources, all fetched 2026-10-20 and matching the plan values: - https://platform.claude.com/docs/en/about-claude/pricing.md (Sonnet 5.5 and Haiku 5.5 rate cards, Haiku long-prompt rule counting cache tokens) - https://platform.claude.com/docs/en/models/overview (IDs claude-sonnet-5-5, claude-haiku-5-5) - https://developers.openai.com/api/docs/pricing (gpt-6.1-sol 2.00 / 0.10 / 10.00 for inputs up to 272K tokens) - https://ai.google.dev/gemini-api/docs/pricing (gemini-4-argon 2.00 / 10.00 until 2026-11-30, 4.00 / 20.00 from 2026-12-01) - https://ai.google.dev/gemini-api/docs/models/gemini-4-argon lists thinking as "low, medium, high" without naming the API field, so thinking level is kept (thinking level unverified by field name). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The brand agent reads uploaded brand manuals through a tool, so a PDF must be encoded as an Anthropic document block inside tool_result content, survive the transcript JSON round trip and travel through the agent loop. Nothing tested that path, and a stored PDF without the cached key failed to load. Cover the three paths with tests and default the missing cached flag to false. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Callers of the library learn its options from these guides; without them the listener, run limits, nullable temperature, thinking display, context editing, system-prompt caching and the new effort levels would only be discoverable by reading the encoder. Add an "Agent Runs" guide and update the configuration, reasoning, caching, multimodal and tools guides and the Readme. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The final review found guides promising behaviour the code lacks: Gemini never fills cacheRead, only Claude receives is_error, and the 'behaves as before' line hid the refusal, pause_turn and tool-error changes that apply even without options. Wrong docs make callers rely on things that are not there, so each statement now matches the encoder it describes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The API answers a 400 to display 'updates' (and its beta) on models that do not support it, but the encoder sent it to any Claude model, so one request option could break a run when the model was swapped. A model capability, in the pattern of supportsSamplingParameters(), now decides; other models fall back to the default display and no beta is added. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each block type had its own round-trip test, but nothing covered the composition a stored cancelled run depends on: thinking, redacted thinking, opaque server blocks and an unrun tool call must come back from JSON and encode into a request where every tool_use has a tool_result. The final review verified it only with a scratch probe. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Makes
LLMAgentClientusable as the engine of long-running, observable agent turns (background jobs with progress, limits and cost logging) and fixes several bugs found on the way. Existing callers stay source-compatible: every new parameter is optional and appended.Fixes
is_error(the encoder read the flag from the wrapping message, so errors arrived as successes).refusalandpause_turnstop reasons,redacted_thinkingand unknown content blocks (kept opaque and re-sent unchanged).temperatureis optional (nullomits it) and never sent to models that reject sampling parameters (Claude 5 family, Opus 4.7/4.8).XHIGHandMAXlevels.New
AgentRunOptions(listener, max iterations, max cost, deadline, cancel callback) as an optional 4th argument ofrun()/runAsync();AgentListenerevents (model step with step usage, tool start/finish);AgentRunInterruptedExceptionwith a conversation that is valid to persist and replay (open tool calls closed with error results, refused turn dropped).TokenUsageper step and cumulative (LLMResponse::getUsage(),getStepUsage()).summarized/updates/omitted) with per-request beta headers, sent only to models that support updates.ContextManagement/ClearToolUses).docs/guides/agent-runs.mdand updates to the configuration, reasoning, caching, multimodal and tools guides.Tests
vendor/bin/phpunit --testsuite default: 431 tests OK; PHPStan level 5 clean. Every change has tests with a scripted client — no live API calls.Used by the Simplia e-shop's new agent framework (newsletter AI).
🤖 Generated with Claude Code