From d58ea02d422f7ebcf605922324713f1b9e7eb53c Mon Sep 17 00:00:00 2001 From: Thomas Carr <9591402+htcarr3@users.noreply.github.com> Date: Fri, 20 Mar 2026 18:22:04 -0400 Subject: [PATCH] feat: TypeScript SDK v0.2.80 feature parity (#72) Add StopFailure hook event with StopFailureInput type (error, error_details, last_assistant_message) and on_stop_failure DSL method. Update SPEC.md for TypeScript SDK v0.2.80 and Python SDK v0.1.49, trimming notes to notable items only. Add APIRetryMessage to docs/messages.md and StopFailure to docs/hooks.md. --- CHANGELOG.md | 4 + SPEC.md | 112 +++++++++--------------- docs/hooks.md | 14 +-- docs/messages.md | 24 ++++- lib/claude_agent/hook_registry.rb | 1 + lib/claude_agent/hooks.rb | 5 ++ sig/claude_agent.rbs | 8 ++ test/claude_agent/test_hook_registry.rb | 4 +- test/claude_agent/test_hooks.rb | 27 +++++- 9 files changed, 116 insertions(+), 83 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40db213..26db278 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- `StopFailure` hook event with `StopFailureInput` type (`error`, `error_details`, `last_assistant_message` fields) for handling API error-triggered stops (TypeScript SDK v0.2.80 parity) +- `on_stop_failure` DSL method on `HookRegistry` for the new event + ## [0.7.17] - 2026-03-17 ### Added diff --git a/SPEC.md b/SPEC.md index 405e261..1d5cc85 100644 --- a/SPEC.md +++ b/SPEC.md @@ -3,11 +3,11 @@ This document provides a comprehensive specification of the Claude Agent SDK, comparing feature parity across the official TypeScript and Python SDKs with this Ruby implementation. **Reference Versions:** -- TypeScript SDK: v0.2.77 (npm package) -- Python SDK: from GitHub (commit 971994c) +- TypeScript SDK: v0.2.80 (npm package) +- Python SDK: from GitHub (commit 13e119a) - Ruby SDK: This repository -**Last Updated:** 2026-03-17 +**Last Updated:** 2026-03-20 --- @@ -436,6 +436,7 @@ Event hooks for intercepting and modifying SDK behavior. | `SessionStart` | ✅ | ✅ | ✅ | Session starts | | `SessionEnd` | ✅ | ❌ | ✅ | Session ends | | `Stop` | ✅ | ✅ | ✅ | Agent stops | +| `StopFailure` | ✅ | ❌ | ✅ | Agent stops due to API error | | `SubagentStart` | ✅ | ✅ | ✅ | Subagent starts (Py v0.1.29) | | `SubagentStop` | ✅ | ✅ | ✅ | Subagent stops | | `PreCompact` | ✅ | ✅ | ✅ | Before compaction | @@ -463,6 +464,7 @@ Event hooks for intercepting and modifying SDK behavior. | `SessionStartHookInput` | ✅ | ❌ | ✅ | | `SessionEndHookInput` | ✅ | ❌ | ✅ | | `StopHookInput` | ✅ | ✅ | ✅ | +| `StopFailureHookInput` | ✅ | ❌ | ✅ | | `SubagentStartHookInput` | ✅ | ✅ | ✅ | | `SubagentStopHookInput` | ✅ | ✅ | ✅ | | `PreCompactHookInput` | ✅ | ✅ | ✅ | @@ -496,6 +498,14 @@ Event hooks for intercepting and modifying SDK behavior. | `stop_hook_active` | ✅ | ✅ | ✅ | Whether stop hook is active | | `last_assistant_message` | ✅ | ❌ | ✅ | Last assistant message text (v0.2.51+) | +#### StopFailureHookInput Fields + +| Field | TypeScript | Python | Ruby | Notes | +|--------------------------|:----------:|:------:|:----:|----------------------------------------| +| `error` | ✅ | ❌ | ✅ | API error type (AssistantMessageError) | +| `error_details` | ✅ | ❌ | ✅ | Additional error details | +| `last_assistant_message` | ✅ | ❌ | ✅ | Last assistant message text | + #### SubagentStopHookInput Fields | Field | TypeScript | Python | Ruby | Notes | @@ -761,7 +771,7 @@ Session management and resumption. |------------------------|:----------:|:------:|:----:|--------------------------------------------------| | `listSessions()` | ✅ | ✅ | ✅ | List past sessions with metadata (v0.2.53) | | `getSessionMessages()` | ✅ | ✅ | ✅ | Read session transcript messages (v0.2.59) | -| `getSessionInfo()` | ✅ | ❌ | ✅ | Get single session metadata (v0.2.75) | +| `getSessionInfo()` | ✅ | ✅ | ✅ | Get single session metadata (v0.2.75) | | `renameSession()` | ✅ | ✅ | ✅ | Rename a session (v0.2.74) | | `tagSession()` | ✅ | ✅ | ✅ | Tag a session (v0.2.75) | | `forkSession()` | ✅ | ❌ | ✅ | Fork/branch a session (v0.2.76) | @@ -805,8 +815,8 @@ Session management and resumption. | `firstPrompt` | ✅ | ✅ | ✅ | First meaningful user prompt | | `gitBranch` | ✅ | ✅ | ✅ | Git branch at end of session | | `cwd` | ✅ | ✅ | ✅ | Working directory for session | -| `tag` | ✅ | ❌ | ✅ | User-set tag (v0.2.75) | -| `createdAt` | ✅ | ❌ | ✅ | Creation time in ms (v0.2.75) | +| `tag` | ✅ | ✅ | ✅ | User-set tag (v0.2.75) | +| `createdAt` | ✅ | ✅ | ✅ | Creation time in ms (v0.2.75) | #### ForkSessionOptions @@ -839,18 +849,18 @@ Custom subagent definitions. ### AgentDefinition -| Field | TypeScript | Python | Ruby | Notes | -|---------------------------------------|:----------:|:------:|:----:|--------------------------------------------| -| `description` | ✅ | ✅ | ✅ | When to use agent | -| `prompt` | ✅ | ✅ | ✅ | Agent system prompt | -| `tools` | ✅ | ✅ | ✅ | Allowed tools | -| `disallowedTools` | ✅ | ❌ | ✅ | Blocked tools | -| `model` | ✅ | ✅ | ✅ | Model override (sonnet/opus/haiku/inherit) | -| `mcpServers` | ✅ | ✅ | ✅ | Agent-specific MCP servers | -| `criticalSystemReminder_EXPERIMENTAL` | ✅ | ❌ | ✅ | Critical reminder (experimental) | -| `skills` | ✅ | ✅ | ✅ | Skills to preload into agent context | -| `memory` | ❌ | ✅ | ❌ | Memory scope for agent (Python-only) | -| `maxTurns` | ✅ | ❌ | ✅ | Max agentic turns before stopping | +| Field | TypeScript | Python | Ruby | Notes | +|---------------------------------------|:----------:|:------:|:----:|----------------------------------------------| +| `description` | ✅ | ✅ | ✅ | When to use agent | +| `prompt` | ✅ | ✅ | ✅ | Agent system prompt | +| `tools` | ✅ | ✅ | ✅ | Allowed tools | +| `disallowedTools` | ✅ | ❌ | ✅ | Blocked tools | +| `model` | ✅ | ✅ | ✅ | Model override (sonnet/opus/haiku/inherit) | +| `mcpServers` | ✅ | ✅ | ✅ | Agent-specific MCP servers | +| `criticalSystemReminder_EXPERIMENTAL` | ✅ | ❌ | ✅ | Critical reminder (experimental) | +| `skills` | ✅ | ✅ | ✅ | Skills to preload into agent context | +| `memory` | ❌ | ✅ | ❌ | Memory scope for agent (Python-only v0.1.49) | +| `maxTurns` | ✅ | ❌ | ✅ | Max agentic turns before stopping | --- @@ -938,7 +948,7 @@ Public API surface for SDK clients. |------------------------|:----------:|:------:|:----:|--------------------------------------------| | `listSessions()` | ✅ | ✅ | ✅ | List past sessions with metadata (v0.2.53) | | `getSessionMessages()` | ✅ | ✅ | ✅ | Read session transcript (v0.2.59) | -| `getSessionInfo()` | ✅ | ❌ | ✅ | Get single session metadata (v0.2.75) | +| `getSessionInfo()` | ✅ | ✅ | ✅ | Get single session metadata (v0.2.75) | | `renameSession()` | ✅ | ✅ | ✅ | Rename a session (v0.2.74) | | `tagSession()` | ✅ | ✅ | ✅ | Tag a session (v0.2.75) | | `forkSession()` | ✅ | ❌ | ✅ | Fork/branch a session (v0.2.76) | @@ -1007,66 +1017,22 @@ Public API surface for SDK clients. ## Notes ### TypeScript SDK -- Primary reference for API surface (most comprehensive) -- Source is bundled/minified, but `sdk.d.ts` provides complete type definitions -- Includes unstable V2 session API +- Primary reference for API surface — `sdk.d.ts` provides complete type definitions - `executable`/`executableArgs` are JS-specific (`node`/`bun`/`deno`) - Does NOT have `user`, `init`/`initOnly`/`maintenance` as typed Options (use `extraArgs` or `settingSources`) -- `ApiKeySource` includes `'oauth'` -- v0.2.45: Added `TaskStartedMessage`, `RateLimitEvent` message types -- v0.2.47: Added `promptSuggestions` option and `PromptSuggestionMessage` -- v0.2.49: Added `ConfigChange` hook event, `SandboxFilesystemConfig`, ModelInfo capability fields -- v0.2.50: Added `WorktreeCreate`/`WorktreeRemove` hook events, `apply_flag_settings` control request -- v0.2.51: Added `TaskProgressMessage`, `StopHookInput.last_assistant_message`, `SubagentStopHookInput.last_assistant_message` -- v0.2.52: Added `mcp_authenticate`/`mcp_clear_auth` control requests for MCP server authentication -- v0.2.53: Added `listSessions()` for discovering and listing past sessions with `SDKSessionInfo` metadata -- v0.2.54 – v0.2.58: CLI parity updates (no new SDK-facing features) -- v0.2.59: Added `getSessionMessages()` for reading session transcript history with pagination (limit/offset) -- v0.2.61 – v0.2.62: CLI parity updates (no new SDK-facing features) -- v0.2.63: Added `supportedAgents()` method on Query, fixed `pathToClaudeCodeExecutable` PATH resolution -- v0.2.64 – v0.2.68: CLI parity updates (no new SDK-facing features) -- v0.2.69: Added `toolConfig` option (askUserQuestion preview format), `supportsFastMode` in ModelInfo, `agent_id`/`agent_type` on BaseHookInput, `InstructionsLoaded` hook event -- v0.2.70: Made `AgentToolInput.subagent_type` optional (defaults to general-purpose), fixed HTTP MCP servers -- v0.2.71: CLI parity update; `settings` now a typed Option (string path or `Settings` object) -- v0.2.72: Added `agentProgressSummaries` option for periodic AI-generated progress summaries -- v0.2.73: Fixed `options.env` being overridden by `~/.claude/settings.json` -- v0.2.74: Added `renameSession()` for renaming session files -- v0.2.75: Added `tag`/`createdAt` fields on `SDKSessionInfo`; `getSessionInfo()` for single-session lookup; `offset` on `listSessions` for pagination; `tagSession()` for tagging sessions; `supportsAutoMode` in `ModelInfo`; `description` on `SDKControlPermissionRequest`; `prompt` on `SDKTaskStartedMessage`; `fast_mode_state` on `SDKControlInitializeResponse`; `queued_to_running` status on `AgentToolOutput` -- v0.2.76: Added `forkSession(sessionId, opts?)` for branching conversations from a point; `cancel_async_message` control subtype to drop queued user messages; `PostCompact` hook event with `compact_summary` field; `get_settings` control request for reading effective merged settings; `planFilePath` field on `ExitPlanMode` tool input -- v0.2.77: Added `SDKAPIRetryMessage` (system subtype `api_retry`) exposing attempt count, max retries, delay, and error status for transient API error retries; added `title` and `displayName` fields on `SDKControlPermissionRequest`/`CanUseTool` options; added `allowRead` and `allowManagedReadPathsOnly` on `SandboxFilesystemConfig` -- Includes `Elicitation`/`ElicitationResult` hook events, `onElicitation` option, `ElicitationCompleteMessage`, `LocalCommandOutputMessage`, `FastModeState` (undocumented in changelog, present in types) +- Some features present in types but undocumented in changelog: `StopFailure` hook, `Elicitation`/`ElicitationResult` hooks, `onElicitation` option, `ElicitationCompleteMessage`, `LocalCommandOutputMessage`, `FastModeState` ### Python SDK -- Full source available with `Transport` abstract class -- Partial control protocol: query and client support interrupt, setPermissionMode, setModel, rewindFiles, mcpStatus, reconnectMcpServer, toggleMcpServer, stopTask -- Has `CLINotFoundError`, `CLIConnectionError`, `ProcessError`, `CLIJSONDecodeError`, `MessageParseError` error types -- Has `TaskStartedMessage`, `TaskProgressMessage`, `TaskNotificationMessage` typed message classes (v0.1.46+) -- Has `stop_reason` field on ResultMessage (v0.1.46+) -- Has `SDKSessionInfo`, `SessionMessage` types with `list_sessions()`/`get_session_messages()` functions (v0.1.46+) -- Has `McpServerStatus` with all fields (name, status, serverInfo, error, config, scope, tools) (v0.1.46+) -- Has `agent_id`/`agent_type` on tool-lifecycle hook inputs (v0.1.46+) -- Has `add_mcp_server()`/`remove_mcp_server()` client methods for runtime MCP management (v0.1.46+) -- Has `include_worktrees` parameter on `list_sessions()` (v0.1.46+) -- Missing hooks: SessionEnd, Setup, TeammateIdle, TaskCompleted, Elicitation, ElicitationResult, ConfigChange, WorktreeCreate, WorktreeRemove, InstructionsLoaded -- Missing permission modes: `dontAsk` -- Missing options: `allowDangerouslySkipPermissions`, `persistSession`, `resumeSessionAt`, `sessionId`, `strictMcpConfig`, `init`/`initOnly`/`maintenance`, `debug`/`debugFile`, `promptSuggestions`, `onElicitation`, `toolConfig`, `agentProgressSummaries`, `agent` (main thread agent) -- `ToolPermissionContext` missing `blockedPath`, `decisionReason`, `toolUseID`, `agentID`, `description` -- Has `rename_session()`/`tag_session()` for session mutation -- Has `RateLimitEvent`/`RateLimitInfo` types with full field coverage -- Has SDK MCP server support with `tool()` helper and annotations -- Missing `getSessionInfo()`, `offset` on `list_sessions`, `tag`/`createdAt` on `SDKSessionInfo` -- Added `thinking` config and `effort` option in v0.1.36 -- Handles `rate_limit_event` and unknown message types gracefully (v0.1.40) -- Client has `get_server_info()` for accessing the initialization result (v0.1.31+) -- v0.1.45 – v0.1.48: Major catch-up with TypeScript SDK (task messages, session APIs, MCP control methods, stop_reason) -- v0.1.48: Fixed fine-grained tool streaming regression -- Added `RateLimitEvent` message type with `RateLimitInfo` -- Added `rename_session()` and `tag_session()` session management functions -- `AgentDefinition` now has `skills`, `mcpServers`, and `memory` (Python-only) fields -- Missing: `onElicitation`, `Elicitation`/`ElicitationResult` hooks, `ElicitationCompleteMessage`, `LocalCommandOutputMessage`, `FastModeState`, `InstructionsLoaded` hook, `agentProgressSummaries`, `getSessionInfo()`, `forkSession()`, `PostCompact` hook, `cancel_async_message`, `get_settings`, `APIRetryMessage` +- Partial control protocol: supports interrupt, setPermissionMode, setModel, rewindFiles, mcpStatus, reconnectMcp, toggleMcp, stopTask +- Has `add_mcp_server()`/`remove_mcp_server()` client methods (not in TypeScript) +- `AgentDefinition.memory` is Python-only (not in TypeScript or Ruby) +- Missing hooks: SessionEnd, StopFailure, Setup, TeammateIdle, TaskCompleted, Elicitation, ElicitationResult, ConfigChange, WorktreeCreate, WorktreeRemove, InstructionsLoaded, PostCompact +- Missing options: `allowDangerouslySkipPermissions`, `persistSession`, `resumeSessionAt`, `sessionId`, `strictMcpConfig`, `debug`/`debugFile`, `promptSuggestions`, `onElicitation`, `toolConfig`, `agentProgressSummaries`, `agent` (main thread agent), `dontAsk` permission mode +- `ToolPermissionContext` missing `blockedPath`, `decisionReason`, `toolUseID`, `agentID`, `description`, `title`, `displayName` +- Missing: `forkSession()`, `offset` on `list_sessions`, `cancel_async_message`, `get_settings`, `APIRetryMessage` ### Ruby SDK (This Repository) -- Feature parity with TypeScript SDK v0.2.77 +- Feature parity with TypeScript SDK v0.2.80 - Ruby-idiomatic patterns (Data.define, snake_case) - Complete control protocol, hook, and V2 Session API support - Dedicated Client class for multi-turn conversations diff --git a/docs/hooks.md b/docs/hooks.md index 9239e3b..de0eacf 100644 --- a/docs/hooks.md +++ b/docs/hooks.md @@ -64,11 +64,11 @@ When both global and per-conversation hooks are set, they are merged additively Each hook method accepts an optional first argument that filters which tool names trigger the callback. The matcher is passed as the first positional argument, before any keyword arguments. -| Matcher type | Behavior | Example | -|---|---|---| -| `nil` (omitted) | Catch-all, fires for every tool | `h.before_tool_use { \|i, c\| ... }` | -| `String` | Treated as a regex pattern | `h.before_tool_use("Bash") { \|i, c\| ... }` | -| `Regexp` | Normalized to its `source` string | `h.before_tool_use(/Bash\|Write/) { \|i, c\| ... }` | +| Matcher type | Behavior | Example | +|-----------------|-----------------------------------|-----------------------------------------------------| +| `nil` (omitted) | Catch-all, fires for every tool | `h.before_tool_use { \|i, c\| ... }` | +| `String` | Treated as a regex pattern | `h.before_tool_use("Bash") { \|i, c\| ... }` | +| `Regexp` | Normalized to its `source` string | `h.before_tool_use(/Bash\|Write/) { \|i, c\| ... }` | A `Regexp` is converted to its `.source` string internally so it can be serialized over the control protocol. This means flags like `Regexp::IGNORECASE` are not preserved. @@ -133,7 +133,7 @@ end ## Event Mapping Table -All 22 hook events with their Ruby DSL method, CLI event name, and description: +All 23 hook events with their Ruby DSL method, CLI event name, and description: | Ruby method | CLI event | Description | |--------------------------|----------------------|-------------------------------------------------| @@ -145,6 +145,7 @@ All 22 hook events with their Ruby DSL method, CLI event name, and description: | `on_session_start` | `SessionStart` | When a session begins. | | `on_session_end` | `SessionEnd` | When a session ends. | | `on_stop` | `Stop` | When the agent stops. | +| `on_stop_failure` | `StopFailure` | When the agent stops due to an API error. | | `on_subagent_start` | `SubagentStart` | When a subagent is spawned. | | `on_subagent_stop` | `SubagentStop` | When a subagent stops. | | `before_compact` | `PreCompact` | Before context compaction. | @@ -190,6 +191,7 @@ All input types inherit these fields from `BaseHookInput`: | `SessionStart` | `SessionStartInput` | `source`, `agent_type`, `model` | | `SessionEnd` | `SessionEndInput` | `reason` | | `Stop` | `StopInput` | `stop_hook_active`, `last_assistant_message` | +| `StopFailure` | `StopFailureInput` | `error`, `error_details`, `last_assistant_message` | | `SubagentStart` | `SubagentStartInput` | `agent_id`, `agent_type` | | `SubagentStop` | `SubagentStopInput` | `stop_hook_active`, `agent_id`, `agent_transcript_path`, `agent_type`, `last_assistant_message` | | `PreCompact` | `PreCompactInput` | `trigger`, `custom_instructions` | diff --git a/docs/messages.md b/docs/messages.md index 4c54caa..01ba319 100644 --- a/docs/messages.md +++ b/docs/messages.md @@ -61,7 +61,7 @@ end ## Message Types -22 message types, grouped by category. +24 message types, grouped by category. ### Conversation Messages @@ -223,6 +223,28 @@ Methods: - `trigger` -- compaction trigger type (`"manual"` or `"auto"`) - `pre_tokens` -- token count before compaction +#### APIRetryMessage + +Emitted when an API request fails with a retryable error and will be retried. + +```ruby +APIRetryMessage = Data.define(:uuid, :session_id, :attempt, :max_retries, :retry_delay_ms, :error_status, :error) +``` + +| Field | Type | Default | +|------------------|----------------|---------| +| `uuid` | `String` | `""` | +| `session_id` | `String` | `""` | +| `attempt` | `Integer` | `0` | +| `max_retries` | `Integer` | `0` | +| `retry_delay_ms` | `Integer` | `0` | +| `error_status` | `Integer, nil` | `nil` | +| `error` | `String, nil` | `nil` | + +Methods: + +- `type` -- `:api_retry` + #### StatusMessage Session status report (e.g., `"compacting"`). diff --git a/lib/claude_agent/hook_registry.rb b/lib/claude_agent/hook_registry.rb index 6e166dc..3849640 100644 --- a/lib/claude_agent/hook_registry.rb +++ b/lib/claude_agent/hook_registry.rb @@ -28,6 +28,7 @@ class HookRegistry on_session_start: "SessionStart", on_session_end: "SessionEnd", on_stop: "Stop", + on_stop_failure: "StopFailure", on_subagent_start: "SubagentStart", on_subagent_stop: "SubagentStop", before_compact: "PreCompact", diff --git a/lib/claude_agent/hooks.rb b/lib/claude_agent/hooks.rb index 6948fee..fb26093 100644 --- a/lib/claude_agent/hooks.rb +++ b/lib/claude_agent/hooks.rb @@ -11,6 +11,7 @@ module ClaudeAgent SessionStart SessionEnd Stop + StopFailure SubagentStart SubagentStop PreCompact @@ -175,6 +176,10 @@ def initialize(#{params.join(", ")}) BaseHookInput.define_input "Stop", optional: { stop_hook_active: false, last_assistant_message: nil } + BaseHookInput.define_input "StopFailure", + required: [ :error ], + optional: { error_details: nil, last_assistant_message: nil } + BaseHookInput.define_input "SubagentStart", required: [ :agent_id, :agent_type ] diff --git a/sig/claude_agent.rbs b/sig/claude_agent.rbs index 714e0ff..dc7870d 100644 --- a/sig/claude_agent.rbs +++ b/sig/claude_agent.rbs @@ -1003,6 +1003,14 @@ module ClaudeAgent def initialize: (?stop_hook_active: bool, ?last_assistant_message: String?, **untyped) -> void end + class StopFailureInput < BaseHookInput + attr_reader error: String + attr_reader error_details: String? + attr_reader last_assistant_message: String? + + def initialize: (error: String, ?error_details: String?, ?last_assistant_message: String?, **untyped) -> void + end + class SubagentStartInput < BaseHookInput attr_reader agent_id: String attr_reader agent_type: String diff --git a/test/claude_agent/test_hook_registry.rb b/test/claude_agent/test_hook_registry.rb index de89df7..0630a1c 100644 --- a/test/claude_agent/test_hook_registry.rb +++ b/test/claude_agent/test_hook_registry.rb @@ -54,8 +54,8 @@ class TestClaudeAgentHookRegistry < ActiveSupport::TestCase assert hooks.key?("Stop") end - test "all 22 events are mapped" do - assert_equal 22, ClaudeAgent::HookRegistry::EVENT_MAP.size + test "all 23 events are mapped" do + assert_equal 23, ClaudeAgent::HookRegistry::EVENT_MAP.size end # --- Matcher normalization --- diff --git a/test/claude_agent/test_hooks.rb b/test/claude_agent/test_hooks.rb index 1fe1a3e..b5ae90f 100644 --- a/test/claude_agent/test_hooks.rb +++ b/test/claude_agent/test_hooks.rb @@ -247,6 +247,31 @@ class TestClaudeAgentHooks < ActiveSupport::TestCase assert_equal "I've completed the task.", input.last_assistant_message end + # --- StopFailureInput --- + + test "stop_failure_input" do + input = ClaudeAgent::StopFailureInput.new(error: "rate_limit") + assert_equal "StopFailure", input.hook_event_name + assert_equal "rate_limit", input.error + end + + test "stop_failure_input_defaults" do + input = ClaudeAgent::StopFailureInput.new(error: "server_error") + assert_nil input.error_details + assert_nil input.last_assistant_message + end + + test "stop_failure_input_with_all_fields" do + input = ClaudeAgent::StopFailureInput.new( + error: "rate_limit", + error_details: "Rate limit exceeded for model", + last_assistant_message: "I was working on..." + ) + assert_equal "rate_limit", input.error + assert_equal "Rate limit exceeded for model", input.error_details + assert_equal "I was working on...", input.last_assistant_message + end + # --- SubagentStartInput --- test "subagent_start_input" do @@ -675,7 +700,7 @@ class TestClaudeAgentHooks < ActiveSupport::TestCase assert ClaudeAgent::SetupInput < ClaudeAgent::BaseHookInput end - test "define_input generates all 21 input classes" do + test "define_input generates all 22 input classes" do ClaudeAgent::HOOK_EVENTS.each do |event| klass = ClaudeAgent.const_get("#{event}Input") assert klass < ClaudeAgent::BaseHookInput, "#{event}Input should inherit from BaseHookInput"