Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- `APIRetryMessage` system message type with `attempt`, `max_retries`, `retry_delay_ms`, `error_status`, and `error` fields (TypeScript SDK v0.2.77 parity)
- `title` and `display_name` fields on `ToolPermissionContext` for richer permission prompt context (TypeScript SDK v0.2.77 parity)
- `allow_read` and `allow_managed_read_paths_only` fields on `SandboxFilesystemConfig` for re-allowing reads within deny regions (TypeScript SDK v0.2.77 parity)

## [0.7.16] - 2026-03-15

## [0.7.15] - 2026-03-15
Expand Down
50 changes: 34 additions & 16 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.76 (npm package)
- Python SDK: from GitHub (commit 302ceb6)
- TypeScript SDK: v0.2.77 (npm package)
- Python SDK: from GitHub (commit 971994c)
- Ruby SDK: This repository

**Last Updated:** 2026-03-15
**Last Updated:** 2026-03-17

---

Expand Down Expand Up @@ -116,9 +116,20 @@ Messages exchanged between SDK and CLI.
| `FilesPersistedEvent` | ✅ | ❌ | ✅ | File persistence confirmation |
| `ElicitationCompleteMessage` | ✅ | ❌ | ✅ | MCP elicitation completed |
| `LocalCommandOutputMessage` | ✅ | ❌ | ✅ | Local command output |
| `APIRetryMessage` | ✅ | ❌ | ✅ | API retry notification (v0.2.77) |

### Message Fields

#### APIRetryMessage

| Field | TypeScript | Python | Ruby | Notes |
|-------------------|:----------:|:------:|:----:|------------------------------------------|
| `attempt` | ✅ | ❌ | ✅ | Current retry attempt number |
| `max_retries` | ✅ | ❌ | ✅ | Maximum retry count |
| `retry_delay_ms` | ✅ | ❌ | ✅ | Delay before retry in milliseconds |
| `error_status` | ✅ | ❌ | ✅ | HTTP status code (null for conn errors) |
| `error` | ✅ | ❌ | ✅ | Error type (AssistantMessageError) |

#### ResultMessage

| Field | TypeScript | Python | Ruby | Notes |
Expand Down Expand Up @@ -666,6 +677,8 @@ Permission handling and updates.
| `toolUseID` | ✅ | ❌ | ✅ | Tool call ID |
| `agentID` | ✅ | ❌ | ✅ | Subagent ID if applicable |
| `description` | ✅ | ❌ | ✅ | Human-readable tool description (v0.2.75) |
| `title` | ✅ | ❌ | ✅ | Full permission prompt sentence (v0.2.77) |
| `displayName` | ✅ | ❌ | ✅ | Short noun phrase for tool action (v0.2.77) |

---

Expand Down Expand Up @@ -799,15 +812,15 @@ Session management and resumption.

| Field | TypeScript | Python | Ruby | Notes |
|------------------|:----------:|:------:|:----:|------------------------------------------------|
| `dir` | ✅ | ❌ | | Project directory |
| `upToMessageId` | ✅ | ❌ | | Slice transcript up to this UUID (inclusive) |
| `title` | ✅ | ❌ | | Custom title for the fork |
| `dir` | ✅ | ❌ | | Project directory |
| `upToMessageId` | ✅ | ❌ | | Slice transcript up to this UUID (inclusive) |
| `title` | ✅ | ❌ | | Custom title for the fork |

#### ForkSessionResult

| Field | TypeScript | Python | Ruby | Notes |
|-------------|:----------:|:------:|:----:|--------------------------------|
| `sessionId` | ✅ | ❌ | | New forked session UUID |
| `sessionId` | ✅ | ❌ | | New forked session UUID |

### V2 Session API (Unstable)

Expand All @@ -833,9 +846,10 @@ Custom subagent definitions.
| `tools` | ✅ | ✅ | ✅ | Allowed tools |
| `disallowedTools` | ✅ | ❌ | ✅ | Blocked tools |
| `model` | ✅ | ✅ | ✅ | Model override (sonnet/opus/haiku/inherit) |
| `mcpServers` | ✅ | | ✅ | Agent-specific MCP servers |
| `mcpServers` | ✅ | | ✅ | Agent-specific MCP servers |
| `criticalSystemReminder_EXPERIMENTAL` | ✅ | ❌ | ✅ | Critical reminder (experimental) |
| `skills` | ✅ | ❌ | ✅ | Skills to preload into agent context |
| `skills` | ✅ | ✅ | ✅ | Skills to preload into agent context |
| `memory` | ❌ | ✅ | ❌ | Memory scope for agent (Python-only) |
| `maxTurns` | ✅ | ❌ | ✅ | Max agentic turns before stopping |

---
Expand All @@ -861,11 +875,13 @@ Sandbox configuration for command execution isolation.

### SandboxFilesystemConfig

| Field | TypeScript | Python | Ruby |
|--------------|:----------:|:------:|:----:|
| `allowWrite` | ✅ | ❌ | ✅ |
| `denyWrite` | ✅ | ❌ | ✅ |
| `denyRead` | ✅ | ❌ | ✅ |
| Field | TypeScript | Python | Ruby |
|-----------------------------|:----------:|:------:|:----:|
| `allowWrite` | ✅ | ❌ | ✅ |
| `denyWrite` | ✅ | ❌ | ✅ |
| `denyRead` | ✅ | ❌ | ✅ |
| `allowRead` | ✅ | ❌ | ✅ |
| `allowManagedReadPathsOnly` | ✅ | ❌ | ✅ |

### SandboxNetworkConfig

Expand Down Expand Up @@ -1017,6 +1033,7 @@ Public API surface for SDK clients.
- 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)

### Python SDK
Expand Down Expand Up @@ -1045,10 +1062,11 @@ Public API surface for SDK clients.
- v0.1.48: Fixed fine-grained tool streaming regression
- Added `RateLimitEvent` message type with `RateLimitInfo`
- Added `rename_session()` and `tag_session()` session management functions
- Missing: `onElicitation`, `Elicitation`/`ElicitationResult` hooks, `ElicitationCompleteMessage`, `LocalCommandOutputMessage`, `FastModeState`, `InstructionsLoaded` hook, `agentProgressSummaries`, `getSessionInfo()`, `forkSession()`, `PostCompact` hook, `cancel_async_message`, `get_settings`
- `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`

### Ruby SDK (This Repository)
- Feature parity with TypeScript SDK v0.2.76
- Feature parity with TypeScript SDK v0.2.77
- Ruby-idiomatic patterns (Data.define, snake_case)
- Complete control protocol, hook, and V2 Session API support
- Dedicated Client class for multi-turn conversations
Expand Down
2 changes: 2 additions & 0 deletions lib/claude_agent/control_protocol/request_handling.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ def handle_can_use_tool(request)
tool_use_id: request["tool_use_id"],
agent_id: request["agent_id"],
description: request["description"],
title: request["title"],
display_name: request["display_name"],
signal: @abort_signal,
request: perm_request
)
Expand Down
13 changes: 13 additions & 0 deletions lib/claude_agent/message_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ def parse(raw)
register "system:task_progress", :parse_task_progress_message
register "system:elicitation_complete", :parse_elicitation_complete_message
register "system:local_command_output", :parse_local_command_output_message
register "system:api_retry", :parse_api_retry_message

private

Expand Down Expand Up @@ -421,5 +422,17 @@ def parse_prompt_suggestion_message(raw)
suggestion: raw[:suggestion] || ""
)
end

def parse_api_retry_message(raw)
APIRetryMessage.new(
uuid: raw[:uuid] || "",
session_id: raw[:session_id] || "",
attempt: raw[:attempt] || 0,
max_retries: raw[:max_retries] || 0,
retry_delay_ms: raw[:retry_delay_ms] || 0,
error_status: raw[:error_status],
error: raw[:error]
)
end
end
end
1 change: 1 addition & 0 deletions lib/claude_agent/messages.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ module ClaudeAgent
PromptSuggestionMessage,
ElicitationCompleteMessage,
LocalCommandOutputMessage,
APIRetryMessage,
GenericMessage
].freeze
end
27 changes: 27 additions & 0 deletions lib/claude_agent/messages/system.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,33 @@ def pre_tokens
# status: "compacting"
# )
#
# API retry message (TypeScript SDK v0.2.77 parity)
#
# Emitted when an API request fails with a retryable error and will be
# retried after a delay. Exposes attempt count, max retries, delay, and
# error status for observability.
#
# @example
# msg = APIRetryMessage.new(
# uuid: "msg-123",
# session_id: "session-abc",
# attempt: 1,
# max_retries: 3,
# retry_delay_ms: 5000,
# error_status: 529,
# error: "rate_limit"
# )
#
APIRetryMessage = Data.define(:uuid, :session_id, :attempt, :max_retries, :retry_delay_ms, :error_status, :error) do
def initialize(uuid: "", session_id: "", attempt: 0, max_retries: 0, retry_delay_ms: 0, error_status: nil, error: nil)
super
end

def type
:api_retry
end
end

StatusMessage = Data.define(:uuid, :session_id, :status, :permission_mode) do
def initialize(uuid:, session_id:, status:, permission_mode: nil)
super
Expand Down
6 changes: 6 additions & 0 deletions lib/claude_agent/permissions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ def to_h
# decision_reason: "Path outside allowed directories",
# tool_use_id: "tool_123",
# agent_id: "agent_456",
# title: "Claude wants to read /etc/passwd",
# display_name: "Read file",
# signal: abort_signal
# )
#
Expand All @@ -158,6 +160,8 @@ def to_h
:agent_id,
:signal,
:description,
:title,
:display_name,
:request
) do
def initialize(
Expand All @@ -168,6 +172,8 @@ def initialize(
agent_id: nil,
signal: nil,
description: nil,
title: nil,
display_name: nil,
request: nil
)
super
Expand Down
12 changes: 8 additions & 4 deletions lib/claude_agent/sandbox_settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,17 +85,19 @@ def to_h
end
end

# Filesystem-specific configuration for sandbox mode (TypeScript SDK v0.2.49 parity)
# Filesystem-specific configuration for sandbox mode (TypeScript SDK v0.2.77 parity)
#
# @example
# filesystem = SandboxFilesystemConfig.new(
# allow_write: ["/tmp/*"],
# deny_write: ["/etc/*"],
# deny_read: ["/secrets/*"]
# deny_read: ["/secrets/*"],
# allow_read: ["/secrets/public/*"],
# allow_managed_read_paths_only: false
# )
#
SandboxFilesystemConfig = Data.define(:allow_write, :deny_write, :deny_read) do
def initialize(allow_write: [], deny_write: [], deny_read: [])
SandboxFilesystemConfig = Data.define(:allow_write, :deny_write, :deny_read, :allow_read, :allow_managed_read_paths_only) do
def initialize(allow_write: [], deny_write: [], deny_read: [], allow_read: [], allow_managed_read_paths_only: false)
super
end

Expand All @@ -104,6 +106,8 @@ def to_h
result[:allowWrite] = allow_write unless allow_write.empty?
result[:denyWrite] = deny_write unless deny_write.empty?
result[:denyRead] = deny_read unless deny_read.empty?
result[:allowRead] = allow_read unless allow_read.empty?
result[:allowManagedReadPathsOnly] = allow_managed_read_paths_only if allow_managed_read_paths_only
result
end
end
Expand Down
24 changes: 21 additions & 3 deletions sig/claude_agent.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -314,8 +314,10 @@ module ClaudeAgent
attr_reader allow_write: Array[String]
attr_reader deny_write: Array[String]
attr_reader deny_read: Array[String]
attr_reader allow_read: Array[String]
attr_reader allow_managed_read_paths_only: bool

def initialize: (?allow_write: Array[String], ?deny_write: Array[String], ?deny_read: Array[String]) -> void
def initialize: (?allow_write: Array[String], ?deny_write: Array[String], ?deny_read: Array[String], ?allow_read: Array[String], ?allow_managed_read_paths_only: bool) -> void
def to_h: () -> Hash[Symbol, untyped]
end

Expand Down Expand Up @@ -573,6 +575,20 @@ module ClaudeAgent
def type: () -> :local_command_output
end

# API retry message (TypeScript SDK v0.2.77 parity)
class APIRetryMessage
attr_reader uuid: String
attr_reader session_id: String
attr_reader attempt: Integer
attr_reader max_retries: Integer
attr_reader retry_delay_ms: Integer
attr_reader error_status: Integer?
attr_reader error: String?

def initialize: (?uuid: String, ?session_id: String, ?attempt: Integer, ?max_retries: Integer, ?retry_delay_ms: Integer, ?error_status: Integer?, ?error: String?) -> void
def type: () -> :api_retry
end

class GenericMessage
attr_reader message_type: String?
attr_reader raw: Hash[Symbol, untyped]
Expand All @@ -584,7 +600,7 @@ module ClaudeAgent
end

# Message types
type message = UserMessage | UserMessageReplay | AssistantMessage | SystemMessage | ResultMessage | StreamEvent | CompactBoundaryMessage | StatusMessage | ToolProgressMessage | HookResponseMessage | AuthStatusMessage | TaskNotificationMessage | HookStartedMessage | HookProgressMessage | ToolUseSummaryMessage | FilesPersistedEvent | TaskStartedMessage | TaskProgressMessage | RateLimitEvent | PromptSuggestionMessage | ElicitationCompleteMessage | LocalCommandOutputMessage | GenericMessage
type message = UserMessage | UserMessageReplay | AssistantMessage | SystemMessage | ResultMessage | StreamEvent | CompactBoundaryMessage | StatusMessage | ToolProgressMessage | HookResponseMessage | AuthStatusMessage | TaskNotificationMessage | HookStartedMessage | HookProgressMessage | ToolUseSummaryMessage | FilesPersistedEvent | TaskStartedMessage | TaskProgressMessage | RateLimitEvent | PromptSuggestionMessage | ElicitationCompleteMessage | LocalCommandOutputMessage | APIRetryMessage | GenericMessage

MESSAGE_TYPES: Array[Class]

Expand Down Expand Up @@ -1161,9 +1177,11 @@ module ClaudeAgent
attr_reader agent_id: String?
attr_reader signal: AbortSignal?
attr_reader description: String?
attr_reader title: String?
attr_reader display_name: String?
attr_reader request: PermissionRequest?

def initialize: (?permission_suggestions: untyped, ?blocked_path: String?, ?decision_reason: String?, ?tool_use_id: String?, ?agent_id: String?, ?signal: AbortSignal?, ?description: String?, ?request: PermissionRequest?) -> void
def initialize: (?permission_suggestions: untyped, ?blocked_path: String?, ?decision_reason: String?, ?tool_use_id: String?, ?agent_id: String?, ?signal: AbortSignal?, ?description: String?, ?title: String?, ?display_name: String?, ?request: PermissionRequest?) -> void
end

# Deferred permission request resolved from any thread
Expand Down
63 changes: 62 additions & 1 deletion test/claude_agent/test_message_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1418,6 +1418,67 @@ class TestClaudeAgentMessageParser < ActiveSupport::TestCase
assert_equal "", msg.suggestion
end

# --- APIRetryMessage parsing ---

test "parse_api_retry_message" do
raw = {
"type" => "system",
"subtype" => "api_retry",
"uuid" => "msg-123",
"session_id" => "sess-abc",
"attempt" => 2,
"max_retries" => 5,
"retry_delay_ms" => 3000,
"error_status" => 529,
"error" => "rate_limit"
}
msg = @parser.parse(raw)

assert_instance_of ClaudeAgent::APIRetryMessage, msg
assert_equal "msg-123", msg.uuid
assert_equal "sess-abc", msg.session_id
assert_equal 2, msg.attempt
assert_equal 5, msg.max_retries
assert_equal 3000, msg.retry_delay_ms
assert_equal 529, msg.error_status
assert_equal "rate_limit", msg.error
assert_equal :api_retry, msg.type
end

test "parse_api_retry_message_connection_error" do
raw = {
"type" => "system",
"subtype" => "api_retry",
"uuid" => "msg-456",
"session_id" => "sess-xyz",
"attempt" => 1,
"max_retries" => 3,
"retry_delay_ms" => 1000,
"error_status" => nil,
"error" => "unknown"
}
msg = @parser.parse(raw)

assert_nil msg.error_status
assert_equal "unknown", msg.error
end

test "parse_api_retry_message_defaults" do
raw = {
"type" => "system",
"subtype" => "api_retry"
}
msg = @parser.parse(raw)

assert_equal "", msg.uuid
assert_equal "", msg.session_id
assert_equal 0, msg.attempt
assert_equal 0, msg.max_retries
assert_equal 0, msg.retry_delay_ms
assert_nil msg.error_status
assert_nil msg.error
end

# --- Parser Registry ---

test "registry contains all top-level message types" do
Expand All @@ -1434,7 +1495,7 @@ class TestClaudeAgentMessageParser < ActiveSupport::TestCase

%w[compact_boundary status hook_response task_notification hook_started
hook_progress files_persisted task_started task_progress
elicitation_complete local_command_output].each do |subtype|
elicitation_complete local_command_output api_retry].each do |subtype|
assert registry.key?("system:#{subtype}"), "Registry should contain 'system:#{subtype}'"
end
end
Expand Down
Loading
Loading