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
67 changes: 41 additions & 26 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
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.12 (npm package)
- Python SDK: v0.1.20 from GitHub (commit 05d2eb4)
- TypeScript SDK: v0.2.19 (npm package)
- Python SDK: v0.1.22 from GitHub (commit 6a0140a)
- Ruby SDK: This repository

---
Expand Down Expand Up @@ -58,6 +58,7 @@ Configuration options for SDK queries and clients.
| `strictMcpConfig` | ✅ | ❌ | ✅ | Strict validation of MCP config |
| `hooks` | ✅ | ✅ | ✅ | Hook callbacks |
| `agents` | ✅ | ✅ | ✅ | Custom subagent definitions |
| `agent` | ✅ | ❌ | ✅ | Agent name for main thread |
| `cwd` | ✅ | ✅ | ✅ | Working directory |
| `additionalDirectories` | ✅ | ✅ | ✅ | Extra allowed directories |
| `env` | ✅ | ✅ | ✅ | Environment variables |
Expand All @@ -66,7 +67,6 @@ Configuration options for SDK queries and clients.
| `settingSources` | ✅ | ✅ | ✅ | Which settings to load |
| `plugins` | ✅ | ✅ | ✅ | Plugin configurations |
| `betas` | ✅ | ✅ | ✅ | Beta features (e.g., context-1m-2025-08-07) |
| `agent` | ✅ | ❌ | ✅ | Agent name for main thread |
| `abortController` | ✅ | ❌ | ✅ | Cancellation controller |
| `stderr` | ✅ | ✅ | ✅ | Stderr callback |
| `spawnClaudeCodeProcess` | ✅ | ❌ | ✅ | Custom spawn function |
Expand All @@ -85,20 +85,23 @@ Configuration options for SDK queries and clients.

Messages exchanged between SDK and CLI.

| Message Type | TypeScript | Python | Ruby | Notes |
|--------------------------|:----------:|:------:|:----:|---------------------------------|
| `UserMessage` | ✅ | ✅ | ✅ | User input |
| `UserMessageReplay` | ✅ | ❌ | ✅ | Replayed user message on resume |
| `AssistantMessage` | ✅ | ✅ | ✅ | Claude response |
| `SystemMessage` | ✅ | ✅ | ✅ | System/init messages |
| `ResultMessage` | ✅ | ✅ | ✅ | Final result with usage |
| `StreamEvent` | ✅ | ✅ | ✅ | Partial streaming events |
| `CompactBoundaryMessage` | ✅ | ❌ | ✅ | Conversation compaction marker |
| `StatusMessage` | ✅ | ❌ | ✅ | Status updates (compacting) |
| `ToolProgressMessage` | ✅ | ❌ | ✅ | Long-running tool progress |
| `HookResponseMessage` | ✅ | ❌ | ✅ | Hook execution output |
| `AuthStatusMessage` | ✅ | ❌ | ✅ | Authentication status |
| `TaskNotificationMessage`| ✅ | ❌ | ✅ | Background task completion |
| Message Type | TypeScript | Python | Ruby | Notes |
|---------------------------|:----------:|:------:|:----:|------------------------------------|
| `UserMessage` | ✅ | ✅ | ✅ | User input |
| `UserMessageReplay` | ✅ | ❌ | ✅ | Replayed user message on resume |
| `AssistantMessage` | ✅ | ✅ | ✅ | Claude response |
| `SystemMessage` | ✅ | ✅ | ✅ | System/init messages |
| `ResultMessage` | ✅ | ✅ | ✅ | Final result with usage |
| `StreamEvent` | ✅ | ✅ | ✅ | Partial streaming events |
| `CompactBoundaryMessage` | ✅ | ❌ | ✅ | Conversation compaction marker |
| `StatusMessage` | ✅ | ❌ | ✅ | Status updates (compacting) |
| `ToolProgressMessage` | ✅ | ❌ | ✅ | Long-running tool progress |
| `HookStartedMessage` | ✅ | ❌ | ✅ | Hook execution started |
| `HookProgressMessage` | ✅ | ❌ | ✅ | Hook progress during execution |
| `HookResponseMessage` | ✅ | ❌ | ✅ | Hook execution output |
| `AuthStatusMessage` | ✅ | ❌ | ✅ | Authentication status |
| `TaskNotificationMessage` | ✅ | ❌ | ✅ | Background task completion |
| `ToolUseSummaryMessage` | ✅ | ❌ | ✅ | Summary of tool use (collapsed) |

### Message Fields

Expand Down Expand Up @@ -185,6 +188,8 @@ Bidirectional control protocol for SDK-CLI communication.
| `mcp_message` | ✅ | ✅ | ✅ | Route MCP message |
| `mcp_set_servers` | ✅ | ❌ | ✅ | Dynamically set MCP servers |
| `mcp_status` | ✅ | ❌ | ✅ | Get MCP server status |
| `mcp_reconnect` | ✅ | ❌ | ✅ | Reconnect to MCP server |
| `mcp_toggle` | ✅ | ❌ | ✅ | Enable/disable MCP server |
| `supported_commands` | ✅ | ❌ | ✅ | Get available slash commands |
| `supported_models` | ✅ | ❌ | ✅ | Get available models |
| `account_info` | ✅ | ❌ | ✅ | Get account information |
Expand Down Expand Up @@ -264,7 +269,7 @@ Event-specific fields returned via `hookSpecificOutput`:

| Field | TypeScript | Python | Ruby | Notes |
|----------------------------|:----------:|:------:|:----:|------------------------------------|
| `permissionDecision` | ✅ | | ✅ | `allow`, `deny`, or `ask` |
| `permissionDecision` | ✅ | | ✅ | `allow`, `deny`, or `ask` |
| `permissionDecisionReason` | ✅ | ❌ | ✅ | Reason for permission decision |
| `updatedInput` | ✅ | ✅ | ✅ | Modified tool input |
| `additionalContext` | ✅ | ❌ | ✅ | Context string returned to model |
Expand All @@ -273,7 +278,7 @@ Event-specific fields returned via `hookSpecificOutput`:

| Field | TypeScript | Python | Ruby | Notes |
|------------------------|:----------:|:------:|:----:|----------------------------------|
| `additionalContext` | ✅ | | ✅ | Context string returned to model |
| `additionalContext` | ✅ | | ✅ | Context string returned to model |
| `updatedMCPToolOutput` | ✅ | ❌ | ✅ | Modified MCP tool output |

#### PostToolUseFailureHookSpecificOutput
Expand Down Expand Up @@ -304,14 +309,20 @@ Event-specific fields returned via `hookSpecificOutput`:

| Field | TypeScript | Python | Ruby | Notes |
|---------------------|:----------:|:------:|:----:|----------------------------------|
| `additionalContext` | ✅ | | ✅ | Context string returned to model |
| `additionalContext` | ✅ | | ✅ | Context string returned to model |

#### PermissionRequestHookSpecificOutput

| Field | TypeScript | Python | Ruby | Notes |
|------------|:----------:|:------:|:----:|------------------------------------------|
| `decision` | ✅ | ❌ | ✅ | `{ behavior: 'allow'/'deny', ... }` obj |

#### NotificationHookSpecificOutput

| Field | TypeScript | Python | Ruby | Notes |
|---------------------|:----------:|:------:|:----:|----------------------------------|
| `additionalContext` | ✅ | ❌ | ✅ | Context string returned to model |

### Hook Matcher

| Field | TypeScript | Python | Ruby |
Expand Down Expand Up @@ -569,6 +580,7 @@ Public API surface for SDK clients.
| `rewindFiles()` | ✅ | ✅ | ✅ | Rewind file changes |
| `setMcpServers()` | ✅ | ❌ | ✅ | Dynamic MCP servers |
| `streamInput()` | ✅ | ❌ | ✅ | Stream user input |
| `close()` | ✅ | ❌ | ✅ | Close query/session |

### Client Class

Expand Down Expand Up @@ -611,21 +623,24 @@ Public API surface for SDK clients.
- Adds `deno` as supported executable option
- Includes experimental `criticalSystemReminder_EXPERIMENTAL` for agent definitions
- `SessionStartHookInput` includes `model` field
- v0.2.12 adds `Setup` hook event for init/maintenance
- v0.2.12 adds `skills` and `maxTurns` to AgentDefinition
- v0.2.12 adds `TaskNotificationMessage` for background task completion
- v0.2.12 adds `user` option to SDKSessionOptions
- v0.2.12+ adds `Setup` hook event for init/maintenance
- v0.2.12+ adds `skills` and `maxTurns` to AgentDefinition
- v0.2.12+ adds `TaskNotificationMessage` for background task completion
- v0.2.12+ adds `user` option to SDKSessionOptions
- v0.2.19 adds `mcp_reconnect` and `mcp_toggle` control requests
- v0.2.19 adds `HookStartedMessage`, `HookProgressMessage`, and `ToolUseSummaryMessage`

### Python SDK
- Full source available (v0.1.20)
- Full source available (v0.1.22)
- Fewer control protocol features than TypeScript
- Does not support SessionStart/SessionEnd/Notification hooks due to setup limitations
- Missing several permission modes (delegate, dontAsk)
- `excludedCommands` in sandbox now supported
- `tool_use_id` now included in PreToolUseHookInput
- `additionalContext` now supported in UserPromptSubmitHookSpecificOutput

### Ruby SDK (This Repository)
- Full TypeScript SDK feature parity achieved
- Complete TypeScript SDK feature parity
- Ruby-idiomatic patterns (Data.define, snake_case)
- Complete control protocol support
- Dedicated Client class for multi-turn conversations
Expand Down
36 changes: 36 additions & 0 deletions lib/claude_agent/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,42 @@ def set_mcp_servers(servers)
@protocol.set_mcp_servers(servers)
end

# Reconnect to an MCP server (TypeScript SDK parity)
#
# Attempts to reconnect to a disconnected or errored MCP server.
#
# @param server_name [String] Name of the MCP server to reconnect
# @return [Hash] Response from the CLI
#
# @example
# client.mcp_reconnect("my-server")
#
def mcp_reconnect(server_name)
require_connection!

@protocol.mcp_reconnect(server_name)
end

# Enable or disable an MCP server (TypeScript SDK parity)
#
# Toggles an MCP server on or off without removing its configuration.
#
# @param server_name [String] Name of the MCP server to toggle
# @param enabled [Boolean] Whether to enable (true) or disable (false) the server
# @return [Hash] Response from the CLI
#
# @example Enable a server
# client.mcp_toggle("my-server", enabled: true)
#
# @example Disable a server
# client.mcp_toggle("my-server", enabled: false)
#
def mcp_toggle(server_name, enabled:)
require_connection!

@protocol.mcp_toggle(server_name, enabled: enabled)
end

private

def require_connection!
Expand Down
32 changes: 32 additions & 0 deletions lib/claude_agent/control_protocol.rb
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,38 @@ def account_info
)
end

# Reconnect to an MCP server (TypeScript SDK parity)
#
# Attempts to reconnect to a disconnected or errored MCP server.
#
# @param server_name [String] Name of the MCP server to reconnect
# @return [Hash] Response from the CLI
#
# @example
# protocol.mcp_reconnect("my-server")
#
def mcp_reconnect(server_name)
send_control_request(subtype: "mcp_reconnect", serverName: server_name)
end

# Enable or disable an MCP server (TypeScript SDK parity)
#
# Toggles an MCP server on or off without removing its configuration.
#
# @param server_name [String] Name of the MCP server to toggle
# @param enabled [Boolean] Whether to enable (true) or disable (false) the server
# @return [Hash] Response from the CLI
#
# @example Enable a server
# protocol.mcp_toggle("my-server", enabled: true)
#
# @example Disable a server
# protocol.mcp_toggle("my-server", enabled: false)
#
def mcp_toggle(server_name, enabled:)
send_control_request(subtype: "mcp_toggle", serverName: server_name, enabled: enabled)
end

# Dynamically set MCP servers for this session (TypeScript SDK parity)
#
# This replaces the current set of dynamically-added MCP servers.
Expand Down
40 changes: 39 additions & 1 deletion lib/claude_agent/message_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class MessageParser
# Parse a raw message hash into a typed message object
#
# @param raw [Hash] Raw message from CLI
# @return [UserMessage, UserMessageReplay, AssistantMessage, SystemMessage, ResultMessage, StreamEvent, CompactBoundaryMessage, StatusMessage, ToolProgressMessage, HookResponseMessage, AuthStatusMessage, TaskNotificationMessage]
# @return [UserMessage, UserMessageReplay, AssistantMessage, SystemMessage, ResultMessage, StreamEvent, CompactBoundaryMessage, StatusMessage, ToolProgressMessage, HookResponseMessage, AuthStatusMessage, TaskNotificationMessage, HookStartedMessage, HookProgressMessage, ToolUseSummaryMessage]
# @raise [MessageParseError] If message cannot be parsed
def parse(raw)
type = raw["type"]
Expand All @@ -32,6 +32,10 @@ def parse(raw)
parse_hook_response_message(raw)
when "task_notification"
parse_task_notification_message(raw)
when "hook_started"
parse_hook_started_message(raw)
when "hook_progress"
parse_hook_progress_message(raw)
else
parse_system_message(raw)
end
Expand All @@ -43,6 +47,8 @@ def parse(raw)
parse_tool_progress_message(raw)
when "auth_status"
parse_auth_status_message(raw)
when "tool_use_summary"
parse_tool_use_summary_message(raw)
else
raise MessageParseError.new("Unknown message type: #{type}", raw_message: raw)
end
Expand Down Expand Up @@ -271,5 +277,37 @@ def parse_task_notification_message(raw)
summary: raw["summary"] || ""
)
end

def parse_hook_started_message(raw)
HookStartedMessage.new(
uuid: raw["uuid"] || "",
session_id: fetch_dual(raw, :session_id, ""),
hook_id: fetch_dual(raw, :hook_id, ""),
hook_name: fetch_dual(raw, :hook_name, ""),
hook_event: fetch_dual(raw, :hook_event, "")
)
end

def parse_hook_progress_message(raw)
HookProgressMessage.new(
uuid: raw["uuid"] || "",
session_id: fetch_dual(raw, :session_id, ""),
hook_id: fetch_dual(raw, :hook_id, ""),
hook_name: fetch_dual(raw, :hook_name, ""),
hook_event: fetch_dual(raw, :hook_event, ""),
stdout: raw["stdout"] || "",
stderr: raw["stderr"] || "",
output: raw["output"] || ""
)
end

def parse_tool_use_summary_message(raw)
ToolUseSummaryMessage.new(
uuid: raw["uuid"] || "",
session_id: fetch_dual(raw, :session_id, ""),
summary: raw["summary"] || "",
preceding_tool_use_ids: fetch_dual(raw, :preceding_tool_use_ids, [])
)
end
end
end
Loading