From 3a8f2d929103cab1305bca91d471edb2b92ff72a Mon Sep 17 00:00:00 2001 From: Thomas Carr <9591402+htcarr3@users.noreply.github.com> Date: Sat, 21 Mar 2026 08:18:55 -0400 Subject: [PATCH] refactor: replace Data.define with ImmutableRecord base class Introduces a plain-inheritance ImmutableRecord base class with an `attribute` DSL, replacing all 63 Data.define types. This eliminates the prepend hack for the Message module, gives real supertype inheritance for RBS signatures, and centralizes freeze/equality/ pattern-matching in one transparent ~50-line class. --- .claude/rules/conventions.md | 44 ++- .claude/rules/testing.md | 4 +- CHANGELOG.md | 5 + CLAUDE.md | 4 +- README.md | 2 +- SPEC.md | 1 - docs/architecture.md | 4 +- docs/configuration.md | 2 +- docs/conversations.md | 2 +- docs/getting-started.md | 2 +- docs/hooks.md | 2 +- docs/messages.md | 278 ++++++++++++++---- docs/sessions.md | 2 +- lib/claude_agent.rb | 1 + .../content_blocks/generic_block.rb | 5 +- .../content_blocks/image_content_block.rb | 4 +- .../server_tool_result_block.rb | 9 +- .../content_blocks/server_tool_use_block.rb | 7 +- lib/claude_agent/content_blocks/text_block.rb | 4 +- .../content_blocks/thinking_block.rb | 5 +- .../content_blocks/tool_result_block.rb | 8 +- .../content_blocks/tool_use_block.rb | 6 +- lib/claude_agent/hooks.rb | 14 +- lib/claude_agent/immutable_record.rb | 103 +++++++ lib/claude_agent/message.rb | 15 +- lib/claude_agent/messages/conversation.rb | 48 ++- lib/claude_agent/messages/generic.rb | 5 +- lib/claude_agent/messages/hook_lifecycle.rb | 90 ++---- lib/claude_agent/messages/result.rb | 55 ++-- lib/claude_agent/messages/streaming.rb | 25 +- lib/claude_agent/messages/system.rb | 32 +- lib/claude_agent/messages/task_lifecycle.rb | 137 +++------ lib/claude_agent/messages/tool_lifecycle.rb | 55 ++-- lib/claude_agent/permissions.rb | 85 ++---- lib/claude_agent/sandbox_settings.rb | 89 ++---- lib/claude_agent/spawn.rb | 10 +- lib/claude_agent/tool_activity.rb | 16 +- lib/claude_agent/types/mcp.rb | 20 +- lib/claude_agent/types/models.rb | 108 +++---- lib/claude_agent/types/operations.rb | 26 +- lib/claude_agent/types/sessions.rb | 40 +-- lib/claude_agent/types/tools.rb | 15 +- lib/claude_agent/v2_session.rb | 31 +- sig/claude_agent.rbs | 138 +++++---- 44 files changed, 825 insertions(+), 733 deletions(-) create mode 100644 lib/claude_agent/immutable_record.rb diff --git a/.claude/rules/conventions.md b/.claude/rules/conventions.md index af57635..eac6886 100644 --- a/.claude/rules/conventions.md +++ b/.claude/rules/conventions.md @@ -30,11 +30,11 @@ lib/ Separate concerns into distinct layers: -| Layer | Purpose | Example | -|-------|---------|---------| -| CLI | Orchestrates operations, user interaction | `Cli::App#deploy` | -| Commands | Builds command arrays, no execution | `Commands::App#run` | -| Configuration | Loads, validates, provides config | `Configuration::Role` | +| Layer | Purpose | Example | +|---------------|-------------------------------------------|-----------------------| +| CLI | Orchestrates operations, user interaction | `Cli::App#deploy` | +| Commands | Builds command arrays, no execution | `Commands::App#run` | +| Configuration | Loads, validates, provides config | `Configuration::Role` | ### Entry Point Pattern @@ -646,27 +646,23 @@ Key conventions: - `to_*` method compiles to the internal format - `empty?` predicate for skipping when unconfigured -### Prepending Modules into Data.define Types +### ImmutableRecord Base Class -To add shared behavior (like `deconstruct_keys` overrides) to `Data.define` classes, -use `prepend` not `include`. Data.define generates methods on the class itself, so -`include` would be shadowed. When overriding `deconstruct_keys`, filter virtual keys -out before calling `super` — Data's implementation stops early on unknown member keys. +All immutable value types (messages, content blocks, permissions, sandbox config, etc.) +inherit from `ImmutableRecord`. It provides an `attribute` DSL, freeze-on-initialize, +structural equality, and `deconstruct_keys` for pattern matching. ```ruby -module Message - def deconstruct_keys(keys) - if keys.nil? - { type: type }.merge(super) - elsif keys.include?(:type) - member_keys = keys - [ :type ] - base = member_keys.empty? ? {} : super(member_keys) - { type: type }.merge(base) - else - super - end - end -end +class TextBlock < ImmutableRecord + attribute :text -MESSAGE_TYPES.each { |klass| klass.prepend(Message) } + def type = :text + def to_h = { type: "text", text: text } +end ``` + +Key conventions: +- Required attributes: `attribute :name` +- Optional attributes: `attribute :name, default: nil` +- No need to override `initialize` — the base class handles keyword args and freeze +- The `Message` module is `include`d (not prepended) into message/content block types diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md index d0ebd5d..8fbea59 100644 --- a/.claude/rules/testing.md +++ b/.claude/rules/testing.md @@ -90,9 +90,9 @@ require "mocha/minitest" Dir[File.join(__dir__, "support", "**", "*.rb")].each { |f| require f } ``` -## Testing Data.define Types +## Testing ImmutableRecord Types -This SDK uses `Data.define` for immutable message types: +This SDK uses `ImmutableRecord` for immutable message types: ```ruby test "data type attributes" do diff --git a/CHANGELOG.md b/CHANGELOG.md index d796ce1..225485c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed +- Replaced all 63 `Data.define` types with plain class inheritance from `ImmutableRecord` base class +- `Message` module is now `include`d instead of `prepend`ed into message and content block types +- RBS signatures now use real supertype inheritance (`< ImmutableRecord`) + ## [0.7.18] - 2026-03-20 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 90f9877..06f3c00 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,7 +21,7 @@ Ruby SDK for building autonomous AI agents that interact with Claude Code CLI. S ## Stack -- **Ruby** 3.2+ (uses `Data.define` for immutable types) +- **Ruby** 3.2+ - **Minitest** for testing - **RuboCop** with `rubocop-rails-omakase` for linting - **RBS** for type signatures (in `sig/`) @@ -53,7 +53,7 @@ bin/release VERSION # Release gem (e.g., bin/release 1.2.0) ## Conventions -- **Immutable data types**: All messages and content blocks use `Data.define`, frozen at construction +- **Immutable data types**: All messages and content blocks inherit from `ImmutableRecord`, frozen at construction - **Frozen string literals**: Every file starts with `# frozen_string_literal: true` - **Message module**: All message/block types include `ClaudeAgent::Message` (text_content, pattern matching) - **Stripe-style config**: `ClaudeAgent.model = "opus"`, `ClaudeAgent.configure { |c| ... }`, `Configuration#to_options` diff --git a/README.md b/README.md index 69590db..57ac773 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Ruby SDK for building AI-powered applications with the [Claude Agent SDK](https: ## Requirements -- Ruby 3.2+ (uses `Data.define`) +- Ruby 3.2+ - [Claude Code CLI](https://code.claude.com/docs/en/getting-started) v2.0.0+ ## Installation diff --git a/SPEC.md b/SPEC.md index 1d5cc85..cf87b43 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1033,7 +1033,6 @@ Public API surface for SDK clients. ### Ruby SDK (This Repository) - 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 - `executable`/`executableArgs` marked N/A (JS runtime options) diff --git a/docs/architecture.md b/docs/architecture.md index 92803dd..edde88c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -129,7 +129,7 @@ Internal architecture of the ClaudeAgent Ruby SDK. | Module | Role | |-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `ToolActivity` | Immutable (`Data.define`) record of a completed tool execution. Pairs `ToolUseBlock` + `ToolResultBlock` with turn index and wall-clock timestamps. | +| `ToolActivity` | Immutable (`ImmutableRecord`) record of a completed tool execution. Pairs `ToolUseBlock` + `ToolResultBlock` with turn index and wall-clock timestamps. | | `LiveToolActivity` | Mutable wrapper for real-time status tracking. States: `:running`, `:done`, `:error`. Updated by progress messages. Suitable for live UIs. | | `ToolActivityTracker` | Enumerable collection with auto-wiring. Attaches to `EventHandler` or `Client`. Callbacks: `on_start`, `on_complete`, `on_progress`, `on_change`. Query methods: `running`, `done`, `errored`, `find_by_id`. | @@ -236,7 +236,7 @@ CLI sends control_request { subtype: "can_use_tool" } ## Immutable Types -All message types and content blocks use `Data.define`, frozen at construction: +All message types and content blocks inherit from `ImmutableRecord`, frozen at construction: **Messages**: `UserMessage`, `UserMessageReplay`, `AssistantMessage`, `SystemMessage`, `ResultMessage`, `StreamEvent`, `CompactBoundaryMessage`, `StatusMessage`, `ToolProgressMessage`, `HookResponseMessage`, `AuthStatusMessage`, `TaskNotificationMessage`, `HookStartedMessage`, `HookProgressMessage`, `ToolUseSummaryMessage`, `FilesPersistedEvent`, `TaskStartedMessage`, `TaskProgressMessage`, `RateLimitEvent`, `PromptSuggestionMessage`, `ElicitationCompleteMessage`, `LocalCommandOutputMessage`, `GenericMessage` diff --git a/docs/configuration.md b/docs/configuration.md index 79ddbbb..ffd36f1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -407,7 +407,7 @@ options = ClaudeAgent::Options.new( ## Sandbox Settings -`SandboxSettings` configures execution sandboxing for the CLI process. It is an immutable `Data.define` type. +`SandboxSettings` configures execution sandboxing for the CLI process. It is an immutable `ImmutableRecord` type. ### Basic sandbox diff --git a/docs/conversations.md b/docs/conversations.md index baef7db..f017262 100644 --- a/docs/conversations.md +++ b/docs/conversations.md @@ -269,7 +269,7 @@ end ### ToolActivity Accessors -Each `ToolActivity` is an immutable `Data.define` object built after a turn completes. +Each `ToolActivity` is an immutable `ImmutableRecord` object built after a turn completes. | Method | Return Type | Description | |-----------------|------------------------|---------------------------------------------------------------| diff --git a/docs/getting-started.md b/docs/getting-started.md index 02ad047..e972cfb 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -4,7 +4,7 @@ This guide walks you through the ClaudeAgent Ruby SDK from first install to mult ## Requirements -- **Ruby 3.2+** (the SDK uses `Data.define` for immutable types) +- **Ruby 3.2+** - **Claude Code CLI v2.0.0+** ([install guide](https://code.claude.com/docs/en/getting-started)) Verify both are available: diff --git a/docs/hooks.md b/docs/hooks.md index de0eacf..12d9c3d 100644 --- a/docs/hooks.md +++ b/docs/hooks.md @@ -296,7 +296,7 @@ opts = ClaudeAgent::Options.new(hooks: hooks) turn = ClaudeAgent.ask("Hello", options: opts) ``` -Each key is a CLI event name string (e.g., `"PreToolUse"`). Each value is an array of `HookMatcher` instances. A `HookMatcher` is a `Data.define` with three fields: +Each key is a CLI event name string (e.g., `"PreToolUse"`). Each value is an array of `HookMatcher` instances. A `HookMatcher` is an `ImmutableRecord` with three fields: | Field | Type | Description | |-------------|------------------|--------------------------------------------------------------| diff --git a/docs/messages.md b/docs/messages.md index 01ba319..f320933 100644 --- a/docs/messages.md +++ b/docs/messages.md @@ -2,7 +2,7 @@ Complete reference for all message types and content blocks in the ClaudeAgent Ruby SDK. -All types are immutable (`Data.define`, frozen at construction). All types include the `ClaudeAgent::Message` module. +All types are immutable (`ImmutableRecord`, frozen at construction). All types include the `ClaudeAgent::Message` module. ## Message Module @@ -70,7 +70,12 @@ end User message sent to Claude. ```ruby -UserMessage = Data.define(:content, :uuid, :session_id, :parent_tool_use_id) +class UserMessage < ImmutableRecord + attribute :content + attribute :uuid, default: nil + attribute :session_id, default: nil + attribute :parent_tool_use_id, default: nil +end ``` | Field | Type | Default | @@ -91,10 +96,15 @@ Methods: Replayed user message from a resumed session. ```ruby -UserMessageReplay = Data.define( - :content, :uuid, :session_id, :parent_tool_use_id, - :is_replay, :is_synthetic, :tool_use_result -) +class UserMessageReplay < ImmutableRecord + attribute :content + attribute :uuid, default: nil + attribute :session_id, default: nil + attribute :parent_tool_use_id, default: nil + attribute :is_replay, default: true + attribute :is_synthetic, default: nil + attribute :tool_use_result, default: nil +end ``` | Field | Type | Default | @@ -119,7 +129,14 @@ Methods: Response from Claude containing content blocks. ```ruby -AssistantMessage = Data.define(:content, :model, :uuid, :session_id, :error, :parent_tool_use_id) +class AssistantMessage < ImmutableRecord + attribute :content + attribute :model + attribute :uuid, default: nil + attribute :session_id, default: nil + attribute :error, default: nil + attribute :parent_tool_use_id, default: nil +end ``` | Field | Type | Default | @@ -152,11 +169,24 @@ msg.has_tool_use? # => true Final message with cost, usage, and outcome info. ```ruby -ResultMessage = Data.define( - :subtype, :duration_ms, :duration_api_ms, :is_error, :num_turns, - :session_id, :uuid, :total_cost_usd, :usage, :result, :structured_output, - :errors, :permission_denials, :model_usage, :stop_reason, :fast_mode_state -) +class ResultMessage < ImmutableRecord + attribute :subtype + attribute :duration_ms + attribute :duration_api_ms + attribute :is_error + attribute :num_turns + attribute :session_id + attribute :uuid, default: nil + attribute :total_cost_usd, default: nil + attribute :usage, default: nil + attribute :result, default: nil + attribute :structured_output, default: nil + attribute :errors, default: nil + attribute :permission_denials, default: nil + attribute :model_usage, default: nil + attribute :stop_reason, default: nil + attribute :fast_mode_state, default: nil +end ``` | Field | Type | Default | @@ -191,7 +221,10 @@ Methods: Internal system event (e.g., session init). ```ruby -SystemMessage = Data.define(:subtype, :data) +class SystemMessage < ImmutableRecord + attribute :subtype + attribute :data +end ``` | Field | Type | Default | @@ -208,7 +241,11 @@ Methods: Conversation compaction marker. ```ruby -CompactBoundaryMessage = Data.define(:uuid, :session_id, :compact_metadata) +class CompactBoundaryMessage < ImmutableRecord + attribute :uuid + attribute :session_id + attribute :compact_metadata +end ``` | Field | Type | Default | @@ -228,7 +265,15 @@ Methods: 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) +class APIRetryMessage < ImmutableRecord + attribute :uuid, default: "" + attribute :session_id, default: "" + attribute :attempt, default: 0 + attribute :max_retries, default: 0 + attribute :retry_delay_ms, default: 0 + attribute :error_status, default: nil + attribute :error, default: nil +end ``` | Field | Type | Default | @@ -250,7 +295,12 @@ Methods: Session status report (e.g., `"compacting"`). ```ruby -StatusMessage = Data.define(:uuid, :session_id, :status, :permission_mode) +class StatusMessage < ImmutableRecord + attribute :uuid + attribute :session_id + attribute :status + attribute :permission_mode, default: nil +end ``` | Field | Type | Default | @@ -271,7 +321,12 @@ Methods: Partial message during streaming. ```ruby -StreamEvent = Data.define(:uuid, :session_id, :event, :parent_tool_use_id) +class StreamEvent < ImmutableRecord + attribute :uuid + attribute :session_id + attribute :event + attribute :parent_tool_use_id, default: nil +end ``` | Field | Type | Default | @@ -301,7 +356,11 @@ event.thinking_text # => nil (only set for thinking deltas) Rate limit status and utilization info. ```ruby -RateLimitEvent = Data.define(:rate_limit_info, :uuid, :session_id) +class RateLimitEvent < ImmutableRecord + attribute :rate_limit_info + attribute :uuid, default: nil + attribute :session_id, default: nil +end ``` | Field | Type | Default | @@ -320,7 +379,11 @@ Methods: Suggested prompt for the user. ```ruby -PromptSuggestionMessage = Data.define(:uuid, :session_id, :suggestion) +class PromptSuggestionMessage < ImmutableRecord + attribute :uuid, default: nil + attribute :session_id, default: nil + attribute :suggestion +end ``` | Field | Type | Default | @@ -340,10 +403,15 @@ Methods: Progress during long-running tool execution. ```ruby -ToolProgressMessage = Data.define( - :uuid, :session_id, :tool_use_id, :tool_name, - :parent_tool_use_id, :elapsed_time_seconds, :task_id -) +class ToolProgressMessage < ImmutableRecord + attribute :uuid + attribute :session_id + attribute :tool_use_id + attribute :tool_name + attribute :elapsed_time_seconds + attribute :parent_tool_use_id, default: nil + attribute :task_id, default: nil +end ``` | Field | Type | Default | @@ -365,7 +433,12 @@ Methods: Summary of tool use for collapsed display. ```ruby -ToolUseSummaryMessage = Data.define(:uuid, :session_id, :summary, :preceding_tool_use_ids) +class ToolUseSummaryMessage < ImmutableRecord + attribute :uuid + attribute :session_id + attribute :summary + attribute :preceding_tool_use_ids, default: [] +end ``` | Field | Type | Default | @@ -384,7 +457,11 @@ Methods: Output from a local command execution. ```ruby -LocalCommandOutputMessage = Data.define(:uuid, :session_id, :content) +class LocalCommandOutputMessage < ImmutableRecord + attribute :uuid, default: "" + attribute :session_id, default: "" + attribute :content, default: "" +end ``` | Field | Type | Default | @@ -404,7 +481,13 @@ Methods: Sent when a hook execution starts. ```ruby -HookStartedMessage = Data.define(:uuid, :session_id, :hook_id, :hook_name, :hook_event) +class HookStartedMessage < ImmutableRecord + attribute :uuid + attribute :session_id + attribute :hook_id + attribute :hook_name + attribute :hook_event +end ``` | Field | Type | Default | @@ -424,10 +507,16 @@ Methods: Progress during hook execution. ```ruby -HookProgressMessage = Data.define( - :uuid, :session_id, :hook_id, :hook_name, :hook_event, - :stdout, :stderr, :output -) +class HookProgressMessage < ImmutableRecord + attribute :uuid + attribute :session_id + attribute :hook_id + attribute :hook_name + attribute :hook_event + attribute :stdout, default: "" + attribute :stderr, default: "" + attribute :output, default: "" +end ``` | Field | Type | Default | @@ -450,10 +539,18 @@ Methods: Final result of a hook execution. ```ruby -HookResponseMessage = Data.define( - :uuid, :session_id, :hook_id, :hook_name, :hook_event, - :stdout, :stderr, :output, :exit_code, :outcome -) +class HookResponseMessage < ImmutableRecord + attribute :uuid + attribute :session_id + attribute :hook_id, default: nil + attribute :hook_name + attribute :hook_event + attribute :stdout, default: "" + attribute :stderr, default: "" + attribute :output, default: "" + attribute :exit_code, default: nil + attribute :outcome, default: nil +end ``` | Field | Type | Default | @@ -483,10 +580,15 @@ Methods: Sent when a new task (subagent) starts. ```ruby -TaskStartedMessage = Data.define( - :uuid, :session_id, :task_id, :tool_use_id, - :description, :task_type, :prompt -) +class TaskStartedMessage < ImmutableRecord + attribute :uuid + attribute :session_id + attribute :task_id + attribute :tool_use_id, default: nil + attribute :description, default: nil + attribute :task_type, default: nil + attribute :prompt, default: nil +end ``` | Field | Type | Default | @@ -508,10 +610,16 @@ Methods: Progress during background task (subagent) execution. ```ruby -TaskProgressMessage = Data.define( - :uuid, :session_id, :task_id, :tool_use_id, - :description, :usage, :last_tool_name, :summary -) +class TaskProgressMessage < ImmutableRecord + attribute :uuid + attribute :session_id + attribute :task_id + attribute :description + attribute :tool_use_id, default: nil + attribute :usage, default: nil + attribute :last_tool_name, default: nil + attribute :summary, default: nil +end ``` | Field | Type | Default | @@ -534,10 +642,16 @@ Methods: Sent when a background task completes, fails, or is stopped. ```ruby -TaskNotificationMessage = Data.define( - :uuid, :session_id, :task_id, :status, - :output_file, :summary, :tool_use_id, :usage -) +class TaskNotificationMessage < ImmutableRecord + attribute :uuid + attribute :session_id + attribute :task_id + attribute :status + attribute :output_file + attribute :summary + attribute :tool_use_id, default: nil + attribute :usage, default: nil +end ``` | Field | Type | Default | @@ -565,7 +679,13 @@ Methods: Sent when files are persisted to storage. ```ruby -FilesPersistedEvent = Data.define(:uuid, :session_id, :files, :failed, :processed_at) +class FilesPersistedEvent < ImmutableRecord + attribute :uuid + attribute :session_id + attribute :files, default: [] + attribute :failed, default: [] + attribute :processed_at, default: nil +end ``` | Field | Type | Default | @@ -585,7 +705,12 @@ Methods: Sent when an MCP server elicitation request completes. ```ruby -ElicitationCompleteMessage = Data.define(:uuid, :session_id, :mcp_server_name, :elicitation_id) +class ElicitationCompleteMessage < ImmutableRecord + attribute :uuid, default: "" + attribute :session_id, default: "" + attribute :mcp_server_name, default: "" + attribute :elicitation_id, default: "" +end ``` | Field | Type | Default | @@ -604,7 +729,13 @@ Methods: Authentication status during login flows. ```ruby -AuthStatusMessage = Data.define(:uuid, :session_id, :is_authenticating, :output, :error) +class AuthStatusMessage < ImmutableRecord + attribute :uuid + attribute :session_id + attribute :is_authenticating + attribute :output, default: [] + attribute :error, default: nil +end ``` | Field | Type | Default | @@ -624,7 +755,10 @@ Methods: Catch-all for unknown/future protocol message types. Supports dynamic field access. ```ruby -GenericMessage = Data.define(:message_type, :raw) +class GenericMessage < ImmutableRecord + attribute :message_type + attribute :raw +end ``` | Field | Type | Default | @@ -657,7 +791,9 @@ msg.data # => "hello" Plain text content. ```ruby -TextBlock = Data.define(:text) +class TextBlock < ImmutableRecord + attribute :text +end ``` | Field | Type | Default | @@ -674,7 +810,10 @@ Methods: Extended thinking content. ```ruby -ThinkingBlock = Data.define(:thinking, :signature) +class ThinkingBlock < ImmutableRecord + attribute :thinking + attribute :signature +end ``` | Field | Type | Default | @@ -692,7 +831,11 @@ Methods: Tool use request from Claude. ```ruby -ToolUseBlock = Data.define(:id, :name, :input) +class ToolUseBlock < ImmutableRecord + attribute :id + attribute :name + attribute :input +end ``` | Field | Type | Default | @@ -721,7 +864,11 @@ block.summary # => "Read: /tmp/file.rb" Result returned from a tool execution. ```ruby -ToolResultBlock = Data.define(:tool_use_id, :content, :is_error) +class ToolResultBlock < ImmutableRecord + attribute :tool_use_id + attribute :content, default: nil + attribute :is_error, default: nil +end ``` | Field | Type | Default | @@ -740,7 +887,12 @@ Methods: Tool use request for an MCP server tool. ```ruby -ServerToolUseBlock = Data.define(:id, :name, :input, :server_name) +class ServerToolUseBlock < ImmutableRecord + attribute :id + attribute :name + attribute :input + attribute :server_name +end ``` | Field | Type | Default | @@ -763,7 +915,12 @@ Methods: Result from an MCP server tool execution. ```ruby -ServerToolResultBlock = Data.define(:tool_use_id, :content, :is_error, :server_name) +class ServerToolResultBlock < ImmutableRecord + attribute :tool_use_id + attribute :server_name + attribute :content, default: nil + attribute :is_error, default: nil +end ``` | Field | Type | Default | @@ -783,7 +940,9 @@ Methods: Image content (base64-encoded or URL-sourced). ```ruby -ImageContentBlock = Data.define(:source) +class ImageContentBlock < ImmutableRecord + attribute :source +end ``` | Field | Type | Default | @@ -810,7 +969,10 @@ block.media_type # => "image/png" Catch-all for unknown/future content block types. Supports dynamic field access. ```ruby -GenericBlock = Data.define(:block_type, :raw) +class GenericBlock < ImmutableRecord + attribute :block_type + attribute :raw +end ``` | Field | Type | Default | diff --git a/docs/sessions.md b/docs/sessions.md index 03d77ac..e7024c2 100644 --- a/docs/sessions.md +++ b/docs/sessions.md @@ -305,7 +305,7 @@ puts result.text ### SessionOptions -`SessionOptions` is a `Data.define` type with the following fields: +`SessionOptions` is an `ImmutableRecord` type with the following fields: | Field | Type | Description | |----------------------------------|-----------------|----------------------------------------------------| diff --git a/lib/claude_agent.rb b/lib/claude_agent.rb index 9e34784..f85b361 100644 --- a/lib/claude_agent.rb +++ b/lib/claude_agent.rb @@ -6,6 +6,7 @@ require_relative "claude_agent/version" require_relative "claude_agent/logging" require_relative "claude_agent/errors" +require_relative "claude_agent/immutable_record" # Base class for all immutable value types require_relative "claude_agent/types" # TypeScript SDK parity types require_relative "claude_agent/sandbox_settings" # Sandbox configuration types require_relative "claude_agent/abort_controller" # Abort/cancel support (TypeScript SDK parity) diff --git a/lib/claude_agent/content_blocks/generic_block.rb b/lib/claude_agent/content_blocks/generic_block.rb index dbead3a..94c98fa 100644 --- a/lib/claude_agent/content_blocks/generic_block.rb +++ b/lib/claude_agent/content_blocks/generic_block.rb @@ -14,7 +14,10 @@ module ClaudeAgent # block.url # => "https://example.com" # block.to_h # => { text: "ref", url: "https://example.com" } # - GenericBlock = Data.define(:block_type, :raw) do + class GenericBlock < ImmutableRecord + attribute :block_type + attribute :raw + def type block_type&.to_sym || :unknown end diff --git a/lib/claude_agent/content_blocks/image_content_block.rb b/lib/claude_agent/content_blocks/image_content_block.rb index e635701..d562b74 100644 --- a/lib/claude_agent/content_blocks/image_content_block.rb +++ b/lib/claude_agent/content_blocks/image_content_block.rb @@ -18,7 +18,9 @@ module ClaudeAgent # ) # block.url # => "https://example.com/image.png" # - ImageContentBlock = Data.define(:source) do + class ImageContentBlock < ImmutableRecord + attribute :source + def type :image end diff --git a/lib/claude_agent/content_blocks/server_tool_result_block.rb b/lib/claude_agent/content_blocks/server_tool_result_block.rb index 6b2c760..08ae57d 100644 --- a/lib/claude_agent/content_blocks/server_tool_result_block.rb +++ b/lib/claude_agent/content_blocks/server_tool_result_block.rb @@ -3,10 +3,11 @@ module ClaudeAgent # Server tool result block # - ServerToolResultBlock = Data.define(:tool_use_id, :content, :is_error, :server_name) do - def initialize(tool_use_id:, server_name:, content: nil, is_error: nil) - super - end + class ServerToolResultBlock < ImmutableRecord + attribute :tool_use_id + attribute :server_name + attribute :content, default: nil + attribute :is_error, default: nil def type :server_tool_result diff --git a/lib/claude_agent/content_blocks/server_tool_use_block.rb b/lib/claude_agent/content_blocks/server_tool_use_block.rb index 53cc19a..dd3c9f3 100644 --- a/lib/claude_agent/content_blocks/server_tool_use_block.rb +++ b/lib/claude_agent/content_blocks/server_tool_use_block.rb @@ -3,7 +3,12 @@ module ClaudeAgent # Server tool use block (for MCP servers) # - ServerToolUseBlock = Data.define(:id, :name, :input, :server_name) do + class ServerToolUseBlock < ImmutableRecord + attribute :id + attribute :name + attribute :input + attribute :server_name + def type :server_tool_use end diff --git a/lib/claude_agent/content_blocks/text_block.rb b/lib/claude_agent/content_blocks/text_block.rb index e508c1b..4b19dda 100644 --- a/lib/claude_agent/content_blocks/text_block.rb +++ b/lib/claude_agent/content_blocks/text_block.rb @@ -7,7 +7,9 @@ module ClaudeAgent # block = TextBlock.new(text: "Hello, world!") # block.text # => "Hello, world!" # - TextBlock = Data.define(:text) do + class TextBlock < ImmutableRecord + attribute :text + def type :text end diff --git a/lib/claude_agent/content_blocks/thinking_block.rb b/lib/claude_agent/content_blocks/thinking_block.rb index 6a66294..fa4c3ae 100644 --- a/lib/claude_agent/content_blocks/thinking_block.rb +++ b/lib/claude_agent/content_blocks/thinking_block.rb @@ -7,7 +7,10 @@ module ClaudeAgent # block = ThinkingBlock.new(thinking: "Let me consider...", signature: "abc123") # block.thinking # => "Let me consider..." # - ThinkingBlock = Data.define(:thinking, :signature) do + class ThinkingBlock < ImmutableRecord + attribute :thinking + attribute :signature + def type :thinking end diff --git a/lib/claude_agent/content_blocks/tool_result_block.rb b/lib/claude_agent/content_blocks/tool_result_block.rb index 6deb92c..e5574d5 100644 --- a/lib/claude_agent/content_blocks/tool_result_block.rb +++ b/lib/claude_agent/content_blocks/tool_result_block.rb @@ -6,10 +6,10 @@ module ClaudeAgent # @example # block = ToolResultBlock.new(tool_use_id: "tool_123", content: "file contents", is_error: false) # - ToolResultBlock = Data.define(:tool_use_id, :content, :is_error) do - def initialize(tool_use_id:, content: nil, is_error: nil) - super - end + class ToolResultBlock < ImmutableRecord + attribute :tool_use_id + attribute :content, default: nil + attribute :is_error, default: nil def type :tool_result diff --git a/lib/claude_agent/content_blocks/tool_use_block.rb b/lib/claude_agent/content_blocks/tool_use_block.rb index d3a6a74..c1f7916 100644 --- a/lib/claude_agent/content_blocks/tool_use_block.rb +++ b/lib/claude_agent/content_blocks/tool_use_block.rb @@ -10,7 +10,11 @@ module ClaudeAgent # block.input[:file_path] # => "/tmp/file" # block.name # => "Read" # - ToolUseBlock = Data.define(:id, :name, :input) do + class ToolUseBlock < ImmutableRecord + attribute :id + attribute :name + attribute :input + def type :tool_use end diff --git a/lib/claude_agent/hooks.rb b/lib/claude_agent/hooks.rb index fb26093..a3c11c7 100644 --- a/lib/claude_agent/hooks.rb +++ b/lib/claude_agent/hooks.rb @@ -37,10 +37,10 @@ module ClaudeAgent # timeout: 30 # ) # - HookMatcher = Data.define(:matcher, :callbacks, :timeout) do - def initialize(matcher:, callbacks:, timeout: nil) - super - end + class HookMatcher < ImmutableRecord + attribute :matcher + attribute :callbacks + attribute :timeout, default: nil # Check if this matcher matches a tool name # @param tool_name [String] Tool name to check @@ -63,10 +63,8 @@ def matches?(tool_name) # Context passed to hook callbacks # - HookContext = Data.define(:tool_use_id) do - def initialize(tool_use_id: nil) - super - end + class HookContext < ImmutableRecord + attribute :tool_use_id, default: nil end # Base class for hook input types (TypeScript SDK parity) diff --git a/lib/claude_agent/immutable_record.rb b/lib/claude_agent/immutable_record.rb new file mode 100644 index 0000000..5752e49 --- /dev/null +++ b/lib/claude_agent/immutable_record.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +module ClaudeAgent + # Base class for immutable value types. + # + # Provides the same guarantees as Data.define (frozen at construction, + # structural equality, pattern matching) with plain inheritance so + # modules can be included normally and RBS signatures use real supertypes. + # + # @example + # class TextBlock < ImmutableRecord + # attribute :text + # + # def type = :text + # def to_h = { type: "text", text: text } + # end + # + # block = TextBlock.new(text: "hello") + # block.text # => "hello" + # block.frozen? # => true + # + class ImmutableRecord + class << self + # Copy parent attributes to subclass so multi-level inheritance works. + def inherited(subclass) + super + subclass.instance_variable_set(:@attributes, (_attributes || []).dup) + end + + # Declare a named attribute on this type. + # + # @param name [Symbol] attribute name + # @param default [Object] default value; omit for required attributes + def attribute(name, default: :__required__) + @attributes ||= [] + @attributes << { name: name, default: default } + attr_reader name + end + + # @return [Array] attribute names + def members + (_attributes || []).map { |a| a[:name] }.freeze + end + + private + + def _attributes + @attributes + end + end + + # Build an instance from keyword arguments and freeze it. + # + # @raise [ArgumentError] if a required attribute is missing + def initialize(**kwargs) + self.class.send(:_attributes).each do |attr| + name = attr[:name] + if kwargs.key?(name) + instance_variable_set(:"@#{name}", kwargs[name]) + elsif attr[:default] != :__required__ + value = attr[:default] + value = value.dup if value.is_a?(Array) || value.is_a?(Hash) + instance_variable_set(:"@#{name}", value) + else + raise ArgumentError, "missing keyword: #{name}" + end + end + freeze + end + + # Structural equality. + def ==(other) + other.is_a?(self.class) && _attribute_values == other._attribute_values + end + alias eql? == + + # Hash code consistent with structural equality. + def hash + [ self.class, _attribute_values ].hash + end + + # Pattern matching support. Unknown keys are silently ignored (unlike + # Data.define which aborts on unknown members), allowing modules to + # add virtual keys via super. + # + # @param keys [Array, nil] requested keys, or nil for all + # @return [Hash] + def deconstruct_keys(keys) + members = self.class.members + if keys.nil? + members.each_with_object({}) { |n, h| h[n] = instance_variable_get(:"@#{n}") } + else + keys.each_with_object({}) { |n, h| h[n] = instance_variable_get(:"@#{n}") if members.include?(n) } + end + end + + protected + + def _attribute_values + self.class.members.map { |n| instance_variable_get(:"@#{n}") } + end + end +end diff --git a/lib/claude_agent/message.rb b/lib/claude_agent/message.rb index 63a9ba4..d92953d 100644 --- a/lib/claude_agent/message.rb +++ b/lib/claude_agent/message.rb @@ -4,7 +4,8 @@ module ClaudeAgent # Shared interface for all message and content block types. # # Provides a consistent API across the 22+ message types and 8 content - # block types without interfering with `Data.define` inheritance. + # block types. Included (not prepended) into all ImmutableRecord subclasses + # that represent messages or content blocks. # # @example Universal text extraction # message.text_content # works on AssistantMessage, UserMessage, TextBlock, etc. @@ -66,9 +67,7 @@ def identifiable? # Override deconstruct_keys to include :type for pattern matching. # # Allows `case msg; in { type: :assistant }` to work naturally. - # - # Data#deconstruct_keys stops early if it encounters a non-member key, - # so we filter out :type (a virtual key) before delegating to super. + # The virtual :type key is added to ImmutableRecord's attribute hash. # # @param keys [Array, nil] # @return [Hash] @@ -85,9 +84,9 @@ def deconstruct_keys(keys) end end - # Prepend Message in all message types (prepend needed to override Data#deconstruct_keys) - MESSAGE_TYPES.each { |klass| klass.prepend(Message) } + # Include Message in all message types + MESSAGE_TYPES.each { |klass| klass.include(Message) } - # Prepend Message in all content block types - CONTENT_BLOCK_TYPES.each { |klass| klass.prepend(Message) } + # Include Message in all content block types + CONTENT_BLOCK_TYPES.each { |klass| klass.include(Message) } end diff --git a/lib/claude_agent/messages/conversation.rb b/lib/claude_agent/messages/conversation.rb index 90b9a3b..e7599ae 100644 --- a/lib/claude_agent/messages/conversation.rb +++ b/lib/claude_agent/messages/conversation.rb @@ -6,10 +6,11 @@ module ClaudeAgent # @example # msg = UserMessage.new(content: "Hello!", uuid: "abc-123", session_id: "session-abc") # - UserMessage = Data.define(:content, :uuid, :session_id, :parent_tool_use_id) do - def initialize(content:, uuid: nil, session_id: nil, parent_tool_use_id: nil) - super - end + class UserMessage < ImmutableRecord + attribute :content + attribute :uuid, default: nil + attribute :session_id, default: nil + attribute :parent_tool_use_id, default: nil def type :user @@ -42,26 +43,14 @@ def replay? # ) # msg.replay? # => true # - UserMessageReplay = Data.define( - :content, - :uuid, - :session_id, - :parent_tool_use_id, - :is_replay, - :is_synthetic, - :tool_use_result - ) do - def initialize( - content:, - uuid: nil, - session_id: nil, - parent_tool_use_id: nil, - is_replay: true, - is_synthetic: nil, - tool_use_result: nil - ) - super - end + class UserMessageReplay < ImmutableRecord + attribute :content + attribute :uuid, default: nil + attribute :session_id, default: nil + attribute :parent_tool_use_id, default: nil + attribute :is_replay, default: true + attribute :is_synthetic, default: nil + attribute :tool_use_result, default: nil def type :user @@ -96,10 +85,13 @@ def synthetic? # session_id: "session-abc" # ) # - AssistantMessage = Data.define(:content, :model, :uuid, :session_id, :error, :parent_tool_use_id) do - def initialize(content:, model:, uuid: nil, session_id: nil, error: nil, parent_tool_use_id: nil) - super - end + class AssistantMessage < ImmutableRecord + attribute :content + attribute :model + attribute :uuid, default: nil + attribute :session_id, default: nil + attribute :error, default: nil + attribute :parent_tool_use_id, default: nil def type :assistant diff --git a/lib/claude_agent/messages/generic.rb b/lib/claude_agent/messages/generic.rb index a44b036..320b7e5 100644 --- a/lib/claude_agent/messages/generic.rb +++ b/lib/claude_agent/messages/generic.rb @@ -14,7 +14,10 @@ module ClaudeAgent # msg.data # => "hello" # msg.to_h # => { data: "hello" } # - GenericMessage = Data.define(:message_type, :raw) do + class GenericMessage < ImmutableRecord + attribute :message_type + attribute :raw + def type message_type&.to_sym || :unknown end diff --git a/lib/claude_agent/messages/hook_lifecycle.rb b/lib/claude_agent/messages/hook_lifecycle.rb index 0b6ea59..c68f448 100644 --- a/lib/claude_agent/messages/hook_lifecycle.rb +++ b/lib/claude_agent/messages/hook_lifecycle.rb @@ -14,22 +14,12 @@ module ClaudeAgent # hook_event: "PreToolUse" # ) # - HookStartedMessage = Data.define( - :uuid, - :session_id, - :hook_id, - :hook_name, - :hook_event - ) do - def initialize( - uuid:, - session_id:, - hook_id:, - hook_name:, - hook_event: - ) - super - end + class HookStartedMessage < ImmutableRecord + attribute :uuid + attribute :session_id + attribute :hook_id + attribute :hook_name + attribute :hook_event def type :hook_started @@ -52,28 +42,15 @@ def type # output: "Combined output" # ) # - HookProgressMessage = Data.define( - :uuid, - :session_id, - :hook_id, - :hook_name, - :hook_event, - :stdout, - :stderr, - :output - ) do - def initialize( - uuid:, - session_id:, - hook_id:, - hook_name:, - hook_event:, - stdout: "", - stderr: "", - output: "" - ) - super - end + class HookProgressMessage < ImmutableRecord + attribute :uuid + attribute :session_id + attribute :hook_id + attribute :hook_name + attribute :hook_event + attribute :stdout, default: "" + attribute :stderr, default: "" + attribute :output, default: "" def type :hook_progress @@ -106,32 +83,17 @@ def type # - "error" - Hook encountered an error # - "cancelled" - Hook was cancelled # - HookResponseMessage = Data.define( - :uuid, - :session_id, - :hook_id, - :hook_name, - :hook_event, - :stdout, - :stderr, - :output, - :exit_code, - :outcome - ) do - def initialize( - uuid:, - session_id:, - hook_id: nil, - hook_name:, - hook_event:, - stdout: "", - stderr: "", - output: "", - exit_code: nil, - outcome: nil - ) - super - end + class HookResponseMessage < ImmutableRecord + attribute :uuid + attribute :session_id + attribute :hook_name + attribute :hook_event + attribute :hook_id, default: nil + attribute :stdout, default: "" + attribute :stderr, default: "" + attribute :output, default: "" + attribute :exit_code, default: nil + attribute :outcome, default: nil def type :hook_response diff --git a/lib/claude_agent/messages/result.rb b/lib/claude_agent/messages/result.rb index e51c85a..0b38053 100644 --- a/lib/claude_agent/messages/result.rb +++ b/lib/claude_agent/messages/result.rb @@ -22,44 +22,23 @@ module ClaudeAgent # ... # ) # - ResultMessage = Data.define( - :subtype, - :duration_ms, - :duration_api_ms, - :is_error, - :num_turns, - :session_id, - :uuid, - :total_cost_usd, - :usage, - :result, - :structured_output, - :errors, # Array for error subtypes - :permission_denials, # Array - :model_usage, # Hash with per-model usage breakdown - :stop_reason, # Why the model stopped generating (TypeScript SDK parity) - :fast_mode_state # Fast mode state (TypeScript SDK v0.2.63 parity) - ) do - def initialize( - subtype:, - duration_ms:, - duration_api_ms:, - is_error:, - num_turns:, - session_id:, - uuid: nil, - total_cost_usd: nil, - usage: nil, - result: nil, - structured_output: nil, - errors: nil, - permission_denials: nil, - model_usage: nil, - stop_reason: nil, - fast_mode_state: nil - ) - super - end + class ResultMessage < ImmutableRecord + attribute :subtype + attribute :duration_ms + attribute :duration_api_ms + attribute :is_error + attribute :num_turns + attribute :session_id + attribute :uuid, default: nil + attribute :total_cost_usd, default: nil + attribute :usage, default: nil + attribute :result, default: nil + attribute :structured_output, default: nil + attribute :errors, default: nil # Array for error subtypes + attribute :permission_denials, default: nil # Array + attribute :model_usage, default: nil # Hash with per-model usage breakdown + attribute :stop_reason, default: nil # Why the model stopped generating (TypeScript SDK parity) + attribute :fast_mode_state, default: nil # Fast mode state (TypeScript SDK v0.2.63 parity) def type :result diff --git a/lib/claude_agent/messages/streaming.rb b/lib/claude_agent/messages/streaming.rb index 0e2bf72..51ce9be 100644 --- a/lib/claude_agent/messages/streaming.rb +++ b/lib/claude_agent/messages/streaming.rb @@ -10,10 +10,11 @@ module ClaudeAgent # event: {type: "content_block_delta", delta: {type: "text_delta", text: "Hello"}} # ) # - StreamEvent = Data.define(:uuid, :session_id, :event, :parent_tool_use_id) do - def initialize(uuid:, session_id:, event:, parent_tool_use_id: nil) - super - end + class StreamEvent < ImmutableRecord + attribute :uuid + attribute :session_id + attribute :event + attribute :parent_tool_use_id, default: nil def type :stream_event @@ -82,10 +83,10 @@ def content_index # ) # msg.status # => "allowed_warning" # - RateLimitEvent = Data.define(:rate_limit_info, :uuid, :session_id) do - def initialize(rate_limit_info:, uuid: nil, session_id: nil) - super - end + class RateLimitEvent < ImmutableRecord + attribute :rate_limit_info + attribute :uuid, default: nil + attribute :session_id, default: nil def type :rate_limit_event @@ -109,10 +110,10 @@ def status # suggestion: "Tell me about this project" # ) # - PromptSuggestionMessage = Data.define(:uuid, :session_id, :suggestion) do - def initialize(uuid: nil, session_id: nil, suggestion:) - super - end + class PromptSuggestionMessage < ImmutableRecord + attribute :suggestion + attribute :uuid, default: nil + attribute :session_id, default: nil def type :prompt_suggestion diff --git a/lib/claude_agent/messages/system.rb b/lib/claude_agent/messages/system.rb index a129af5..2ee7275 100644 --- a/lib/claude_agent/messages/system.rb +++ b/lib/claude_agent/messages/system.rb @@ -6,7 +6,10 @@ module ClaudeAgent # @example # msg = SystemMessage.new(subtype: "init", data: {version: "2.0.0"}) # - SystemMessage = Data.define(:subtype, :data) do + class SystemMessage < ImmutableRecord + attribute :subtype + attribute :data + def type :system end @@ -26,7 +29,11 @@ def type # msg.trigger # => "auto" # msg.pre_tokens # => 50000 # - CompactBoundaryMessage = Data.define(:uuid, :session_id, :compact_metadata) do + class CompactBoundaryMessage < ImmutableRecord + attribute :uuid + attribute :session_id + attribute :compact_metadata + def type :compact_boundary end @@ -72,20 +79,25 @@ def pre_tokens # 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 + class APIRetryMessage < ImmutableRecord + attribute :uuid, default: "" + attribute :session_id, default: "" + attribute :attempt, default: 0 + attribute :max_retries, default: 0 + attribute :retry_delay_ms, default: 0 + attribute :error_status, default: nil + attribute :error, default: nil 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 - end + class StatusMessage < ImmutableRecord + attribute :uuid + attribute :session_id + attribute :status + attribute :permission_mode, default: nil def type :status diff --git a/lib/claude_agent/messages/task_lifecycle.rb b/lib/claude_agent/messages/task_lifecycle.rb index 98de6a6..649f662 100644 --- a/lib/claude_agent/messages/task_lifecycle.rb +++ b/lib/claude_agent/messages/task_lifecycle.rb @@ -15,26 +15,14 @@ module ClaudeAgent # task_type: "bash" # ) # - TaskStartedMessage = Data.define( - :uuid, - :session_id, - :task_id, - :tool_use_id, - :description, - :task_type, - :prompt - ) do - def initialize( - uuid:, - session_id:, - task_id:, - tool_use_id: nil, - description: nil, - task_type: nil, - prompt: nil - ) - super - end + class TaskStartedMessage < ImmutableRecord + attribute :uuid + attribute :session_id + attribute :task_id + attribute :tool_use_id, default: nil + attribute :description, default: nil + attribute :task_type, default: nil + attribute :prompt, default: nil def type :task_started @@ -55,22 +43,15 @@ def type # usage: { total_tokens: 5000, tool_uses: 3, duration_ms: 2500 } # ) # - TaskProgressMessage = Data.define( - :uuid, :session_id, :task_id, :tool_use_id, - :description, :usage, :last_tool_name, :summary - ) do - def initialize( - uuid:, - session_id:, - task_id:, - description:, - usage: nil, - tool_use_id: nil, - last_tool_name: nil, - summary: nil - ) - super - end + class TaskProgressMessage < ImmutableRecord + attribute :uuid + attribute :session_id + attribute :task_id + attribute :description + attribute :tool_use_id, default: nil + attribute :usage, default: nil + attribute :last_tool_name, default: nil + attribute :summary, default: nil def type :task_progress @@ -99,28 +80,15 @@ def type # - "failed" - Task encountered an error # - "stopped" - Task was manually stopped # - TaskNotificationMessage = Data.define( - :uuid, - :session_id, - :task_id, - :status, - :output_file, - :summary, - :tool_use_id, - :usage - ) do - def initialize( - uuid:, - session_id:, - task_id:, - status:, - output_file:, - summary:, - tool_use_id: nil, - usage: nil - ) - super - end + class TaskNotificationMessage < ImmutableRecord + attribute :uuid + attribute :session_id + attribute :task_id + attribute :status + attribute :output_file + attribute :summary + attribute :tool_use_id, default: nil + attribute :usage, default: nil def type :task_notification @@ -160,22 +128,12 @@ def stopped? # ) # msg.files.first[:filename] # => "test.rb" # - FilesPersistedEvent = Data.define( - :uuid, - :session_id, - :files, - :failed, - :processed_at - ) do - def initialize( - uuid:, - session_id:, - files: [], - failed: [], - processed_at: nil - ) - super - end + class FilesPersistedEvent < ImmutableRecord + attribute :uuid + attribute :session_id + attribute :files, default: [] + attribute :failed, default: [] + attribute :processed_at, default: nil def type :files_persisted @@ -194,10 +152,11 @@ def type # elicitation_id: "elic-456" # ) # - ElicitationCompleteMessage = Data.define(:uuid, :session_id, :mcp_server_name, :elicitation_id) do - def initialize(uuid: "", session_id: "", mcp_server_name: "", elicitation_id: "") - super - end + class ElicitationCompleteMessage < ImmutableRecord + attribute :uuid, default: "" + attribute :session_id, default: "" + attribute :mcp_server_name, default: "" + attribute :elicitation_id, default: "" def type :elicitation_complete @@ -216,22 +175,12 @@ def type # output: ["Waiting for browser..."] # ) # - AuthStatusMessage = Data.define( - :uuid, - :session_id, - :is_authenticating, - :output, - :error - ) do - def initialize( - uuid:, - session_id:, - is_authenticating:, - output: [], - error: nil - ) - super - end + class AuthStatusMessage < ImmutableRecord + attribute :uuid + attribute :session_id + attribute :is_authenticating + attribute :output, default: [] + attribute :error, default: nil def type :auth_status diff --git a/lib/claude_agent/messages/tool_lifecycle.rb b/lib/claude_agent/messages/tool_lifecycle.rb index 75e764b..35bed04 100644 --- a/lib/claude_agent/messages/tool_lifecycle.rb +++ b/lib/claude_agent/messages/tool_lifecycle.rb @@ -14,26 +14,14 @@ module ClaudeAgent # elapsed_time_seconds: 5.2 # ) # - ToolProgressMessage = Data.define( - :uuid, - :session_id, - :tool_use_id, - :tool_name, - :parent_tool_use_id, - :elapsed_time_seconds, - :task_id - ) do - def initialize( - uuid:, - session_id:, - tool_use_id:, - tool_name:, - elapsed_time_seconds:, - parent_tool_use_id: nil, - task_id: nil - ) - super - end + class ToolProgressMessage < ImmutableRecord + attribute :uuid + attribute :session_id + attribute :tool_use_id + attribute :tool_name + attribute :elapsed_time_seconds + attribute :parent_tool_use_id, default: nil + attribute :task_id, default: nil def type :tool_progress @@ -52,20 +40,11 @@ def type # preceding_tool_use_ids: ["tool-1", "tool-2", "tool-3"] # ) # - ToolUseSummaryMessage = Data.define( - :uuid, - :session_id, - :summary, - :preceding_tool_use_ids - ) do - def initialize( - uuid:, - session_id:, - summary:, - preceding_tool_use_ids: [] - ) - super - end + class ToolUseSummaryMessage < ImmutableRecord + attribute :uuid + attribute :session_id + attribute :summary + attribute :preceding_tool_use_ids, default: [] def type :tool_use_summary @@ -83,10 +62,10 @@ def type # content: "command output here" # ) # - LocalCommandOutputMessage = Data.define(:uuid, :session_id, :content) do - def initialize(uuid: "", session_id: "", content: "") - super - end + class LocalCommandOutputMessage < ImmutableRecord + attribute :uuid, default: "" + attribute :session_id, default: "" + attribute :content, default: "" def type :local_command_output diff --git a/lib/claude_agent/permissions.rb b/lib/claude_agent/permissions.rb index 09e0f07..719ed9d 100644 --- a/lib/claude_agent/permissions.rb +++ b/lib/claude_agent/permissions.rb @@ -9,10 +9,10 @@ module ClaudeAgent # tool_use_id: "tool_123" # ) # - PermissionResultAllow = Data.define(:updated_input, :updated_permissions, :tool_use_id) do - def initialize(updated_input: nil, updated_permissions: nil, tool_use_id: nil) - super - end + class PermissionResultAllow < ImmutableRecord + attribute :updated_input, default: nil + attribute :updated_permissions, default: nil + attribute :tool_use_id, default: nil def behavior "allow" @@ -36,10 +36,10 @@ def to_h # tool_use_id: "tool_123" # ) # - PermissionResultDeny = Data.define(:message, :interrupt, :tool_use_id) do - def initialize(message: "", interrupt: false, tool_use_id: nil) - super - end + class PermissionResultDeny < ImmutableRecord + attribute :message, default: "" + attribute :interrupt, default: false + attribute :tool_use_id, default: nil def behavior "deny" @@ -70,24 +70,13 @@ def to_h # rules: [{tool_name: "Read", behavior: "allow"}] # ) # - PermissionUpdate = Data.define( - :type, - :rules, - :behavior, - :mode, - :directories, - :destination - ) do - def initialize( - type:, - rules: nil, - behavior: nil, - mode: nil, - directories: nil, - destination: nil - ) - super - end + class PermissionUpdate < ImmutableRecord + attribute :type + attribute :rules, default: nil + attribute :behavior, default: nil + attribute :mode, default: nil + attribute :directories, default: nil + attribute :destination, default: nil def to_h h = { type: type } @@ -116,10 +105,9 @@ def normalize_rule(rule) # Permission rule value (TypeScript SDK parity) # Note: behavior is on PermissionUpdate, not on individual rules # - PermissionRuleValue = Data.define(:tool_name, :rule_content) do - def initialize(tool_name: nil, rule_content: nil) - super - end + class PermissionRuleValue < ImmutableRecord + attribute :tool_name, default: nil + attribute :rule_content, default: nil def to_h { @@ -152,31 +140,16 @@ def to_h # signal: abort_signal # ) # - ToolPermissionContext = Data.define( - :permission_suggestions, - :blocked_path, - :decision_reason, - :tool_use_id, - :agent_id, - :signal, - :description, - :title, - :display_name, - :request - ) do - def initialize( - permission_suggestions: nil, - blocked_path: nil, - decision_reason: nil, - tool_use_id: nil, - agent_id: nil, - signal: nil, - description: nil, - title: nil, - display_name: nil, - request: nil - ) - super - end + class ToolPermissionContext < ImmutableRecord + attribute :permission_suggestions, default: nil + attribute :blocked_path, default: nil + attribute :decision_reason, default: nil + attribute :tool_use_id, default: nil + attribute :agent_id, default: nil + attribute :signal, default: nil + attribute :description, default: nil + attribute :title, default: nil + attribute :display_name, default: nil + attribute :request, default: nil end end diff --git a/lib/claude_agent/sandbox_settings.rb b/lib/claude_agent/sandbox_settings.rb index d7c15e4..8be50eb 100644 --- a/lib/claude_agent/sandbox_settings.rb +++ b/lib/claude_agent/sandbox_settings.rb @@ -10,26 +10,14 @@ module ClaudeAgent # allowed_domains: ["api.example.com"] # ) # - SandboxNetworkConfig = Data.define( - :allowed_domains, - :allow_local_binding, - :allow_unix_sockets, - :allow_all_unix_sockets, - :allow_managed_domains_only, - :http_proxy_port, - :socks_proxy_port - ) do - def initialize( - allowed_domains: [], - allow_local_binding: false, - allow_unix_sockets: [], - allow_all_unix_sockets: false, - allow_managed_domains_only: false, - http_proxy_port: nil, - socks_proxy_port: nil - ) - super - end + class SandboxNetworkConfig < ImmutableRecord + attribute :allowed_domains, default: [] + attribute :allow_local_binding, default: false + attribute :allow_unix_sockets, default: [] + attribute :allow_all_unix_sockets, default: false + attribute :allow_managed_domains_only, default: false + attribute :http_proxy_port, default: nil + attribute :socks_proxy_port, default: nil def to_h result = {} @@ -52,10 +40,9 @@ def to_h # network: ["localhost:*"] # ) # - SandboxIgnoreViolations = Data.define(:file, :network) do - def initialize(file: [], network: []) - super - end + class SandboxIgnoreViolations < ImmutableRecord + attribute :file, default: [] + attribute :network, default: [] def to_h result = {} @@ -73,10 +60,9 @@ def to_h # args: ["--hidden"] # ) # - SandboxRipgrepConfig = Data.define(:command, :args) do - def initialize(command:, args: nil) - super - end + class SandboxRipgrepConfig < ImmutableRecord + attribute :command + attribute :args, default: nil def to_h result = { command: command } @@ -96,10 +82,12 @@ def to_h # allow_managed_read_paths_only: false # ) # - 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 + class SandboxFilesystemConfig < ImmutableRecord + attribute :allow_write, default: [] + attribute :deny_write, default: [] + attribute :deny_read, default: [] + attribute :allow_read, default: [] + attribute :allow_managed_read_paths_only, default: false def to_h result = {} @@ -131,32 +119,17 @@ def to_h # ripgrep: SandboxRipgrepConfig.new(command: "/usr/local/bin/rg") # ) # - SandboxSettings = Data.define( - :enabled, - :auto_allow_bash_if_sandboxed, - :excluded_commands, - :allow_unsandboxed_commands, - :network, - :ignore_violations, - :enable_weaker_nested_sandbox, - :enable_weaker_network_isolation, - :ripgrep, - :filesystem - ) do - def initialize( - enabled: false, - auto_allow_bash_if_sandboxed: false, - excluded_commands: [], - allow_unsandboxed_commands: false, - network: nil, - ignore_violations: nil, - enable_weaker_nested_sandbox: false, - enable_weaker_network_isolation: false, - ripgrep: nil, - filesystem: nil - ) - super - end + class SandboxSettings < ImmutableRecord + attribute :enabled, default: false + attribute :auto_allow_bash_if_sandboxed, default: false + attribute :excluded_commands, default: [] + attribute :allow_unsandboxed_commands, default: false + attribute :network, default: nil + attribute :ignore_violations, default: nil + attribute :enable_weaker_nested_sandbox, default: false + attribute :enable_weaker_network_isolation, default: false + attribute :ripgrep, default: nil + attribute :filesystem, default: nil def to_h result = { enabled: enabled } diff --git a/lib/claude_agent/spawn.rb b/lib/claude_agent/spawn.rb index 8b23635..329b9be 100644 --- a/lib/claude_agent/spawn.rb +++ b/lib/claude_agent/spawn.rb @@ -15,10 +15,12 @@ module ClaudeAgent # env: { "CLAUDE_CODE_ENTRYPOINT" => "sdk-rb" } # ) # - SpawnOptions = Data.define(:command, :args, :cwd, :env, :abort_signal) do - def initialize(command:, args: [], cwd: nil, env: {}, abort_signal: nil) - super - end + class SpawnOptions < ImmutableRecord + attribute :command + attribute :args, default: [] + attribute :cwd, default: nil + attribute :env, default: {} + attribute :abort_signal, default: nil # Get the full command line as an array # @return [Array] diff --git a/lib/claude_agent/tool_activity.rb b/lib/claude_agent/tool_activity.rb index fbcebb8..37a5781 100644 --- a/lib/claude_agent/tool_activity.rb +++ b/lib/claude_agent/tool_activity.rb @@ -14,16 +14,12 @@ module ClaudeAgent # puts " Error!" if activity.error? # end # - ToolActivity = Data.define( - :tool_use, - :tool_result, - :turn_index, - :started_at, - :completed_at - ) do - def initialize(tool_use:, tool_result: nil, turn_index:, started_at: nil, completed_at: nil) - super - end + class ToolActivity < ImmutableRecord + attribute :tool_use + attribute :turn_index + attribute :tool_result, default: nil + attribute :started_at, default: nil + attribute :completed_at, default: nil # Tool name # @return [String] diff --git a/lib/claude_agent/types/mcp.rb b/lib/claude_agent/types/mcp.rb index 7b9fc35..2c716d3 100644 --- a/lib/claude_agent/types/mcp.rb +++ b/lib/claude_agent/types/mcp.rb @@ -7,10 +7,14 @@ module ClaudeAgent # @example # status = McpServerStatus.new(name: "filesystem", status: "connected", server_info: {name: "fs", version: "1.0"}) # - McpServerStatus = Data.define(:name, :status, :server_info, :error, :config, :scope, :tools) do - def initialize(name:, status:, server_info: nil, error: nil, config: nil, scope: nil, tools: nil) - super - end + class McpServerStatus < ImmutableRecord + attribute :name + attribute :status + attribute :server_info, default: nil + attribute :error, default: nil + attribute :config, default: nil + attribute :scope, default: nil + attribute :tools, default: nil end # Result of set_mcp_servers() control method (TypeScript SDK parity) @@ -22,9 +26,9 @@ def initialize(name:, status:, server_info: nil, error: nil, config: nil, scope: # errors: {"server2" => "Connection failed"} # ) # - McpSetServersResult = Data.define(:added, :removed, :errors) do - def initialize(added: [], removed: [], errors: {}) - super - end + class McpSetServersResult < ImmutableRecord + attribute :added, default: [] + attribute :removed, default: [] + attribute :errors, default: {} end end diff --git a/lib/claude_agent/types/models.rb b/lib/claude_agent/types/models.rb index c9b1f1b..95b5dff 100644 --- a/lib/claude_agent/types/models.rb +++ b/lib/claude_agent/types/models.rb @@ -8,10 +8,15 @@ module ClaudeAgent # model.value # => "claude-3-opus" # model.display_name # => "Claude 3 Opus" # - ModelInfo = Data.define(:value, :display_name, :description, :supports_effort, :supported_effort_levels, :supports_adaptive_thinking, :supports_fast_mode, :supports_auto_mode) do - def initialize(value:, display_name: nil, description: nil, supports_effort: nil, supported_effort_levels: nil, supports_adaptive_thinking: nil, supports_fast_mode: nil, supports_auto_mode: nil) - super - end + class ModelInfo < ImmutableRecord + attribute :value + attribute :display_name, default: nil + attribute :description, default: nil + attribute :supports_effort, default: nil + attribute :supported_effort_levels, default: nil + attribute :supports_adaptive_thinking, default: nil + attribute :supports_fast_mode, default: nil + attribute :supports_auto_mode, default: nil end # Per-model usage statistics returned in result messages (TypeScript SDK parity) @@ -19,28 +24,15 @@ def initialize(value:, display_name: nil, description: nil, supports_effort: nil # @example # usage = ModelUsage.new(input_tokens: 100, output_tokens: 50, cost_usd: 0.01, max_output_tokens: 4096) # - ModelUsage = Data.define( - :input_tokens, - :output_tokens, - :cache_read_input_tokens, - :cache_creation_input_tokens, - :web_search_requests, - :cost_usd, - :context_window, - :max_output_tokens - ) do - def initialize( - input_tokens: 0, - output_tokens: 0, - cache_read_input_tokens: 0, - cache_creation_input_tokens: 0, - web_search_requests: 0, - cost_usd: 0.0, - context_window: nil, - max_output_tokens: nil - ) - super - end + class ModelUsage < ImmutableRecord + attribute :input_tokens, default: 0 + attribute :output_tokens, default: 0 + attribute :cache_read_input_tokens, default: 0 + attribute :cache_creation_input_tokens, default: 0 + attribute :web_search_requests, default: 0 + attribute :cost_usd, default: 0.0 + attribute :context_window, default: nil + attribute :max_output_tokens, default: nil end # Return type for account_info() (TypeScript SDK parity) @@ -48,10 +40,12 @@ def initialize( # @example # info = AccountInfo.new(email: "user@example.com", organization: "Acme Corp") # - AccountInfo = Data.define(:email, :organization, :subscription_type, :token_source, :api_key_source) do - def initialize(email: nil, organization: nil, subscription_type: nil, token_source: nil, api_key_source: nil) - super - end + class AccountInfo < ImmutableRecord + attribute :email, default: nil + attribute :organization, default: nil + attribute :subscription_type, default: nil + attribute :token_source, default: nil + attribute :api_key_source, default: nil end # Return type for supported_agents() (TypeScript SDK v0.2.63 parity) @@ -61,10 +55,10 @@ def initialize(email: nil, organization: nil, subscription_type: nil, token_sour # agent.name # => "Explore" # agent.description # => "Search agent" # - AgentInfo = Data.define(:name, :description, :model) do - def initialize(name:, description: nil, model: nil) - super - end + class AgentInfo < ImmutableRecord + attribute :name + attribute :description, default: nil + attribute :model, default: nil end # Agent definition for custom subagents (TypeScript SDK parity) @@ -85,30 +79,16 @@ def initialize(name:, description: nil, model: nil) # max_turns: 10 # ) # - AgentDefinition = Data.define( - :description, - :prompt, - :tools, - :disallowed_tools, - :model, - :mcp_servers, - :critical_system_reminder, - :skills, - :max_turns - ) do - def initialize( - description:, - prompt:, - tools: nil, - disallowed_tools: nil, - model: nil, - mcp_servers: nil, - critical_system_reminder: nil, - skills: nil, - max_turns: nil - ) - super - end + class AgentDefinition < ImmutableRecord + attribute :description + attribute :prompt + attribute :tools, default: nil + attribute :disallowed_tools, default: nil + attribute :model, default: nil + attribute :mcp_servers, default: nil + attribute :critical_system_reminder, default: nil + attribute :skills, default: nil + attribute :max_turns, default: nil def to_h result = { @@ -138,9 +118,13 @@ def to_h # agents: [AgentInfo.new(name: "Explore")] # ) # - InitializationResult = Data.define(:commands, :output_style, :available_output_styles, :models, :account, :agents, :fast_mode_state) do - def initialize(commands: [], output_style: nil, available_output_styles: [], models: [], account: nil, agents: [], fast_mode_state: nil) - super - end + class InitializationResult < ImmutableRecord + attribute :commands, default: [] + attribute :output_style, default: nil + attribute :available_output_styles, default: [] + attribute :models, default: [] + attribute :account, default: nil + attribute :agents, default: [] + attribute :fast_mode_state, default: nil end end diff --git a/lib/claude_agent/types/operations.rb b/lib/claude_agent/types/operations.rb index 2587bf7..816c04a 100644 --- a/lib/claude_agent/types/operations.rb +++ b/lib/claude_agent/types/operations.rb @@ -6,18 +6,18 @@ module ClaudeAgent # @example # usage = TaskUsage.new(total_tokens: 5000, tool_uses: 3, duration_ms: 2500) # - TaskUsage = Data.define(:total_tokens, :tool_uses, :duration_ms) do - def initialize(total_tokens: 0, tool_uses: 0, duration_ms: 0) - super - end + class TaskUsage < ImmutableRecord + attribute :total_tokens, default: 0 + attribute :tool_uses, default: 0 + attribute :duration_ms, default: 0 end # Permission denial information in result messages (TypeScript SDK parity) # - SDKPermissionDenial = Data.define(:tool_name, :tool_use_id, :tool_input) do - def initialize(tool_name:, tool_use_id:, tool_input:) - super - end + class SDKPermissionDenial < ImmutableRecord + attribute :tool_name + attribute :tool_use_id + attribute :tool_input end # Result of rewind_files() control method (TypeScript SDK parity) @@ -30,9 +30,11 @@ def initialize(tool_name:, tool_use_id:, tool_input:) # deletions: 5 # ) # - RewindFilesResult = Data.define(:can_rewind, :error, :files_changed, :insertions, :deletions) do - def initialize(can_rewind:, error: nil, files_changed: nil, insertions: nil, deletions: nil) - super - end + class RewindFilesResult < ImmutableRecord + attribute :can_rewind + attribute :error, default: nil + attribute :files_changed, default: nil + attribute :insertions, default: nil + attribute :deletions, default: nil end end diff --git a/lib/claude_agent/types/sessions.rb b/lib/claude_agent/types/sessions.rb index 414db49..7307731 100644 --- a/lib/claude_agent/types/sessions.rb +++ b/lib/claude_agent/types/sessions.rb @@ -15,21 +15,17 @@ module ClaudeAgent # cwd: "/Users/dev/myapp" # ) # - SessionInfo = Data.define( - :session_id, - :summary, - :last_modified, - :file_size, - :custom_title, - :first_prompt, - :git_branch, - :cwd, - :tag, - :created_at - ) do - def initialize(session_id:, summary:, last_modified:, file_size:, custom_title: nil, first_prompt: nil, git_branch: nil, cwd: nil, tag: nil, created_at: nil) - super - end + class SessionInfo < ImmutableRecord + attribute :session_id + attribute :summary + attribute :last_modified + attribute :file_size + attribute :custom_title, default: nil + attribute :first_prompt, default: nil + attribute :git_branch, default: nil + attribute :cwd, default: nil + attribute :tag, default: nil + attribute :created_at, default: nil end # Message from a session transcript returned by get_session_messages (TypeScript SDK v0.2.59 parity) @@ -42,10 +38,12 @@ def initialize(session_id:, summary:, last_modified:, file_size:, custom_title: # message: { "role" => "user", "content" => [{ "type" => "text", "text" => "Hello" }] } # ) # - SessionMessage = Data.define(:type, :uuid, :session_id, :message, :parent_tool_use_id) do - def initialize(type:, uuid:, session_id:, message:, parent_tool_use_id: nil) - super - end + class SessionMessage < ImmutableRecord + attribute :type + attribute :uuid + attribute :session_id + attribute :message + attribute :parent_tool_use_id, default: nil end # Result of forking a session (TypeScript SDK v0.2.76 parity) @@ -54,5 +52,7 @@ def initialize(type:, uuid:, session_id:, message:, parent_tool_use_id: nil) # result = ClaudeAgent.fork_session("abc-123") # puts result.session_id # => new UUID # - ForkSessionResult = Data.define(:session_id) + class ForkSessionResult < ImmutableRecord + attribute :session_id + end end diff --git a/lib/claude_agent/types/tools.rb b/lib/claude_agent/types/tools.rb index b2631d2..d6256db 100644 --- a/lib/claude_agent/types/tools.rb +++ b/lib/claude_agent/types/tools.rb @@ -7,10 +7,9 @@ module ClaudeAgent # preset = ToolsPreset.new(preset: "claude_code") # options = ClaudeAgent::Options.new(tools: preset) # - ToolsPreset = Data.define(:type, :preset) do - def initialize(type: "preset", preset: "claude_code") - super - end + class ToolsPreset < ImmutableRecord + attribute :type, default: "preset" + attribute :preset, default: "claude_code" def to_h { type: type, preset: preset } @@ -24,9 +23,9 @@ def to_h # cmd.name # => "commit" # cmd.description # => "Create a commit" # - SlashCommand = Data.define(:name, :description, :argument_hint) do - def initialize(name:, description: nil, argument_hint: nil) - super - end + class SlashCommand < ImmutableRecord + attribute :name + attribute :description, default: nil + attribute :argument_hint, default: nil end end diff --git a/lib/claude_agent/v2_session.rb b/lib/claude_agent/v2_session.rb index 7c7ba05..660bae0 100644 --- a/lib/claude_agent/v2_session.rb +++ b/lib/claude_agent/v2_session.rb @@ -11,28 +11,15 @@ module ClaudeAgent # permission_mode: "acceptEdits" # ) # - SessionOptions = Data.define( - :model, - :path_to_claude_code_executable, - :env, - :allowed_tools, - :disallowed_tools, - :can_use_tool, - :hooks, - :permission_mode - ) do - def initialize( - model:, - path_to_claude_code_executable: nil, - env: nil, - allowed_tools: nil, - disallowed_tools: nil, - can_use_tool: nil, - hooks: nil, - permission_mode: nil - ) - super - end + class SessionOptions < ImmutableRecord + attribute :model + attribute :path_to_claude_code_executable, default: nil + attribute :env, default: nil + attribute :allowed_tools, default: nil + attribute :disallowed_tools, default: nil + attribute :can_use_tool, default: nil + attribute :hooks, default: nil + attribute :permission_mode, default: nil end # V2 API - UNSTABLE diff --git a/sig/claude_agent.rbs b/sig/claude_agent.rbs index dc7870d..6f290fe 100644 --- a/sig/claude_agent.rbs +++ b/sig/claude_agent.rbs @@ -117,10 +117,22 @@ module ClaudeAgent def abort!: (?String? reason) -> void end + # Base class for immutable record types (replaces Data.define) + class ImmutableRecord + def self.attribute: (Symbol name, ?default: untyped) -> void + def self.members: () -> Array[Symbol] + def self.inherited: (Class subclass) -> void + def initialize: (**untyped) -> void + def ==: (untyped other) -> bool + def eql?: (untyped other) -> bool + def hash: () -> Integer + def deconstruct_keys: (Array[Symbol]? keys) -> Hash[Symbol, untyped] + end + # TypeScript SDK parity types ASSISTANT_MESSAGE_ERROR_TYPES: Array[String] API_KEY_SOURCES: Array[String] - class SlashCommand + class SlashCommand < ImmutableRecord attr_reader name: String attr_reader description: String? attr_reader argument_hint: String? @@ -128,7 +140,7 @@ module ClaudeAgent def initialize: (name: String, ?description: String?, ?argument_hint: String?) -> void end - class ModelInfo + class ModelInfo < ImmutableRecord attr_reader value: String attr_reader display_name: String? attr_reader description: String? @@ -141,7 +153,7 @@ module ClaudeAgent def initialize: (value: String, ?display_name: String?, ?description: String?, ?supports_effort: bool?, ?supported_effort_levels: Array[String]?, ?supports_adaptive_thinking: bool?, ?supports_fast_mode: bool?, ?supports_auto_mode: bool?) -> void end - class McpServerStatus + class McpServerStatus < ImmutableRecord attr_reader name: String attr_reader status: String attr_reader server_info: Hash[String, untyped]? @@ -153,7 +165,7 @@ module ClaudeAgent def initialize: (name: String, status: String, ?server_info: Hash[String, untyped]?, ?error: String?, ?config: Hash[String, untyped]?, ?scope: String?, ?tools: Array[Hash[String, untyped]]?) -> void end - class TaskUsage + class TaskUsage < ImmutableRecord attr_reader total_tokens: Integer attr_reader tool_uses: Integer attr_reader duration_ms: Integer @@ -161,7 +173,7 @@ module ClaudeAgent def initialize: (?total_tokens: Integer, ?tool_uses: Integer, ?duration_ms: Integer) -> void end - class AccountInfo + class AccountInfo < ImmutableRecord attr_reader email: String? attr_reader organization: String? attr_reader subscription_type: String? @@ -171,7 +183,7 @@ module ClaudeAgent def initialize: (?email: String?, ?organization: String?, ?subscription_type: String?, ?token_source: String?, ?api_key_source: String?) -> void end - class AgentInfo + class AgentInfo < ImmutableRecord attr_reader name: String attr_reader description: String? attr_reader model: String? @@ -179,7 +191,7 @@ module ClaudeAgent def initialize: (name: String, ?description: String?, ?model: String?) -> void end - class InitializationResult + class InitializationResult < ImmutableRecord attr_reader commands: Array[SlashCommand] attr_reader output_style: String? attr_reader available_output_styles: Array[String] @@ -191,7 +203,7 @@ module ClaudeAgent def initialize: (?commands: Array[SlashCommand], ?output_style: String?, ?available_output_styles: Array[String], ?models: Array[ModelInfo], ?account: AccountInfo?, ?agents: Array[AgentInfo], ?fast_mode_state: String?) -> void end - class ModelUsage + class ModelUsage < ImmutableRecord attr_reader input_tokens: Integer attr_reader output_tokens: Integer attr_reader cache_read_input_tokens: Integer @@ -204,7 +216,7 @@ module ClaudeAgent def initialize: (?input_tokens: Integer, ?output_tokens: Integer, ?cache_read_input_tokens: Integer, ?cache_creation_input_tokens: Integer, ?web_search_requests: Integer, ?cost_usd: Float, ?context_window: Integer?, ?max_output_tokens: Integer?) -> void end - class SDKPermissionDenial + class SDKPermissionDenial < ImmutableRecord attr_reader tool_name: String attr_reader tool_use_id: String attr_reader tool_input: Hash[String, untyped] @@ -213,7 +225,7 @@ module ClaudeAgent end # Result of set_mcp_servers() control method (TypeScript SDK parity) - class McpSetServersResult + class McpSetServersResult < ImmutableRecord attr_reader added: Array[String] attr_reader removed: Array[String] attr_reader errors: Hash[String, String] @@ -222,7 +234,7 @@ module ClaudeAgent end # Result of rewind_files() control method (TypeScript SDK parity) - class RewindFilesResult + class RewindFilesResult < ImmutableRecord attr_reader can_rewind: bool attr_reader error: String? attr_reader files_changed: Array[String]? @@ -233,7 +245,7 @@ module ClaudeAgent end # Session metadata returned by list_sessions (TypeScript SDK parity: SDKSessionInfo) - class SessionInfo + class SessionInfo < ImmutableRecord attr_reader session_id: String attr_reader summary: String attr_reader last_modified: Integer @@ -249,7 +261,7 @@ module ClaudeAgent end # Session transcript message returned by get_session_messages (TypeScript SDK v0.2.59 parity) - class SessionMessage + class SessionMessage < ImmutableRecord attr_reader type: String attr_reader uuid: String attr_reader session_id: String @@ -260,14 +272,14 @@ module ClaudeAgent end # Fork session result (TypeScript SDK v0.2.76 parity) - class ForkSessionResult + class ForkSessionResult < ImmutableRecord attr_reader session_id: String def initialize: (session_id: String) -> void end # Agent definition for custom subagents (TypeScript SDK parity) - class AgentDefinition + class AgentDefinition < ImmutableRecord attr_reader description: String attr_reader prompt: String attr_reader tools: Array[String]? @@ -281,7 +293,7 @@ module ClaudeAgent end # Sandbox configuration types - class SandboxNetworkConfig + class SandboxNetworkConfig < ImmutableRecord attr_reader allowed_domains: Array[String] attr_reader allow_local_binding: bool attr_reader allow_unix_sockets: Array[String] @@ -294,7 +306,7 @@ module ClaudeAgent def to_h: () -> Hash[Symbol, untyped] end - class SandboxRipgrepConfig + class SandboxRipgrepConfig < ImmutableRecord attr_reader command: String attr_reader args: Array[String]? @@ -302,7 +314,7 @@ module ClaudeAgent def to_h: () -> Hash[Symbol, untyped] end - class SandboxIgnoreViolations + class SandboxIgnoreViolations < ImmutableRecord attr_reader file: Array[String] attr_reader network: Array[String] @@ -310,7 +322,7 @@ module ClaudeAgent def to_h: () -> Hash[Symbol, untyped] end - class SandboxFilesystemConfig + class SandboxFilesystemConfig < ImmutableRecord attr_reader allow_write: Array[String] attr_reader deny_write: Array[String] attr_reader deny_read: Array[String] @@ -321,7 +333,7 @@ module ClaudeAgent def to_h: () -> Hash[Symbol, untyped] end - class SandboxSettings + class SandboxSettings < ImmutableRecord attr_reader enabled: bool attr_reader auto_allow_bash_if_sandboxed: bool attr_reader excluded_commands: Array[String] @@ -338,7 +350,7 @@ module ClaudeAgent end # Tools preset configuration - class ToolsPreset + class ToolsPreset < ImmutableRecord attr_reader type: String attr_reader preset: String @@ -463,7 +475,7 @@ module ClaudeAgent end # Content block types - class TextBlock + class TextBlock < ImmutableRecord attr_reader text: String def initialize: (text: String) -> void @@ -471,7 +483,7 @@ module ClaudeAgent def to_h: () -> Hash[Symbol, untyped] end - class ThinkingBlock + class ThinkingBlock < ImmutableRecord attr_reader thinking: String attr_reader signature: String? @@ -480,7 +492,7 @@ module ClaudeAgent def to_h: () -> Hash[Symbol, untyped] end - class ToolUseBlock + class ToolUseBlock < ImmutableRecord attr_reader id: String attr_reader name: String attr_reader input: Hash[String, untyped] @@ -493,7 +505,7 @@ module ClaudeAgent def summary: (?max: Integer) -> String end - class ToolResultBlock + class ToolResultBlock < ImmutableRecord attr_reader tool_use_id: String attr_reader content: String? attr_reader is_error: bool? @@ -503,7 +515,7 @@ module ClaudeAgent def to_h: () -> Hash[Symbol, untyped] end - class ServerToolUseBlock + class ServerToolUseBlock < ImmutableRecord attr_reader id: String attr_reader name: String attr_reader input: Hash[String, untyped] @@ -517,7 +529,7 @@ module ClaudeAgent def summary: (?max: Integer) -> String end - class ServerToolResultBlock + class ServerToolResultBlock < ImmutableRecord attr_reader tool_use_id: String attr_reader server_name: String attr_reader content: String? @@ -528,7 +540,7 @@ module ClaudeAgent def to_h: () -> Hash[Symbol, untyped] end - class ImageContentBlock + class ImageContentBlock < ImmutableRecord attr_reader source: Hash[String | Symbol, untyped] def initialize: (source: Hash[String | Symbol, untyped]) -> void @@ -540,7 +552,7 @@ module ClaudeAgent def to_h: () -> Hash[Symbol, untyped] end - class GenericBlock + class GenericBlock < ImmutableRecord attr_reader block_type: String? attr_reader raw: Hash[Symbol, untyped] @@ -555,7 +567,7 @@ module ClaudeAgent CONTENT_BLOCK_TYPES: Array[Class] # Elicitation complete message (TypeScript SDK v0.2.63 parity) - class ElicitationCompleteMessage + class ElicitationCompleteMessage < ImmutableRecord attr_reader uuid: String attr_reader session_id: String attr_reader mcp_server_name: String @@ -566,7 +578,7 @@ module ClaudeAgent end # Local command output message (TypeScript SDK v0.2.63 parity) - class LocalCommandOutputMessage + class LocalCommandOutputMessage < ImmutableRecord attr_reader uuid: String attr_reader session_id: String attr_reader content: String @@ -576,7 +588,7 @@ module ClaudeAgent end # API retry message (TypeScript SDK v0.2.77 parity) - class APIRetryMessage + class APIRetryMessage < ImmutableRecord attr_reader uuid: String attr_reader session_id: String attr_reader attempt: Integer @@ -589,7 +601,7 @@ module ClaudeAgent def type: () -> :api_retry end - class GenericMessage + class GenericMessage < ImmutableRecord attr_reader message_type: String? attr_reader raw: Hash[Symbol, untyped] @@ -604,7 +616,7 @@ module ClaudeAgent MESSAGE_TYPES: Array[Class] - class UserMessage + class UserMessage < ImmutableRecord attr_reader content: String | Array[content_block] attr_reader uuid: String? attr_reader session_id: String? @@ -616,7 +628,7 @@ module ClaudeAgent def replay?: () -> bool end - class UserMessageReplay + class UserMessageReplay < ImmutableRecord attr_reader content: String | Array[content_block] attr_reader uuid: String? attr_reader session_id: String? @@ -632,7 +644,7 @@ module ClaudeAgent def synthetic?: () -> bool end - class AssistantMessage + class AssistantMessage < ImmutableRecord attr_reader content: Array[content_block] attr_reader model: String attr_reader uuid: String? @@ -648,7 +660,7 @@ module ClaudeAgent def has_tool_use?: () -> bool end - class SystemMessage + class SystemMessage < ImmutableRecord attr_reader subtype: String attr_reader data: Hash[String, untyped] @@ -656,7 +668,7 @@ module ClaudeAgent def type: () -> :system end - class ResultMessage + class ResultMessage < ImmutableRecord attr_reader subtype: String attr_reader duration_ms: Integer attr_reader duration_api_ms: Integer @@ -698,7 +710,7 @@ module ClaudeAgent def error?: () -> bool end - class StreamEvent + class StreamEvent < ImmutableRecord attr_reader uuid: String attr_reader session_id: String attr_reader event: Hash[String, untyped] @@ -713,7 +725,7 @@ module ClaudeAgent def content_index: () -> Integer? end - class CompactBoundaryMessage + class CompactBoundaryMessage < ImmutableRecord attr_reader uuid: String attr_reader session_id: String attr_reader compact_metadata: Hash[String | Symbol, untyped] @@ -725,7 +737,7 @@ module ClaudeAgent end # Status message (TypeScript SDK parity) - class StatusMessage + class StatusMessage < ImmutableRecord attr_reader uuid: String attr_reader session_id: String attr_reader status: String @@ -736,7 +748,7 @@ module ClaudeAgent end # Tool progress message (TypeScript SDK parity) - class ToolProgressMessage + class ToolProgressMessage < ImmutableRecord attr_reader uuid: String attr_reader session_id: String attr_reader tool_use_id: String @@ -750,7 +762,7 @@ module ClaudeAgent end # Hook response message (TypeScript SDK parity) - class HookResponseMessage + class HookResponseMessage < ImmutableRecord attr_reader uuid: String attr_reader session_id: String attr_reader hook_id: String? @@ -770,7 +782,7 @@ module ClaudeAgent end # Auth status message (TypeScript SDK parity) - class AuthStatusMessage + class AuthStatusMessage < ImmutableRecord attr_reader uuid: String attr_reader session_id: String attr_reader is_authenticating: bool @@ -782,7 +794,7 @@ module ClaudeAgent end # Task notification message (TypeScript SDK parity) - class TaskNotificationMessage + class TaskNotificationMessage < ImmutableRecord attr_reader uuid: String attr_reader session_id: String attr_reader task_id: String @@ -800,7 +812,7 @@ module ClaudeAgent end # Hook started message (TypeScript SDK parity) - class HookStartedMessage + class HookStartedMessage < ImmutableRecord attr_reader uuid: String attr_reader session_id: String attr_reader hook_id: String @@ -812,7 +824,7 @@ module ClaudeAgent end # Hook progress message (TypeScript SDK parity) - class HookProgressMessage + class HookProgressMessage < ImmutableRecord attr_reader uuid: String attr_reader session_id: String attr_reader hook_id: String @@ -827,7 +839,7 @@ module ClaudeAgent end # Tool use summary message (TypeScript SDK parity) - class ToolUseSummaryMessage + class ToolUseSummaryMessage < ImmutableRecord attr_reader uuid: String attr_reader session_id: String attr_reader summary: String @@ -838,7 +850,7 @@ module ClaudeAgent end # Files persisted event (TypeScript SDK v0.2.25 parity) - class FilesPersistedEvent + class FilesPersistedEvent < ImmutableRecord attr_reader uuid: String attr_reader session_id: String attr_reader files: Array[Hash[String, String]] @@ -850,7 +862,7 @@ module ClaudeAgent end # Task started message (TypeScript SDK v0.2.75 parity) - class TaskStartedMessage + class TaskStartedMessage < ImmutableRecord attr_reader uuid: String attr_reader session_id: String attr_reader task_id: String @@ -864,7 +876,7 @@ module ClaudeAgent end # Task progress message (TypeScript SDK v0.2.75 parity) - class TaskProgressMessage + class TaskProgressMessage < ImmutableRecord attr_reader uuid: String attr_reader session_id: String attr_reader task_id: String @@ -879,7 +891,7 @@ module ClaudeAgent end # Rate limit event (TypeScript SDK v0.2.45 parity) - class RateLimitEvent + class RateLimitEvent < ImmutableRecord attr_reader rate_limit_info: Hash[String, untyped] attr_reader uuid: String? attr_reader session_id: String? @@ -890,7 +902,7 @@ module ClaudeAgent end # Prompt suggestion message (TypeScript SDK v0.2.47 parity) - class PromptSuggestionMessage + class PromptSuggestionMessage < ImmutableRecord attr_reader uuid: String? attr_reader session_id: String? attr_reader suggestion: String @@ -911,7 +923,7 @@ module ClaudeAgent # Hook types HOOK_EVENTS: Array[String] - class HookMatcher + class HookMatcher < ImmutableRecord attr_reader matcher: String | Regexp attr_reader callbacks: Array[^(BaseHookInput, HookContext) -> Hash[Symbol, untyped]] attr_reader timeout: Integer? @@ -920,7 +932,7 @@ module ClaudeAgent def matches?: (String tool_name) -> bool end - class HookContext + class HookContext < ImmutableRecord attr_reader tool_use_id: String? def initialize: (?tool_use_id: String?) -> void @@ -1137,7 +1149,7 @@ module ClaudeAgent PERMISSION_UPDATE_TYPES: Array[String] PERMISSION_UPDATE_DESTINATIONS: Array[String] - class PermissionResultAllow + class PermissionResultAllow < ImmutableRecord attr_reader updated_input: Hash[String, untyped]? attr_reader updated_permissions: Array[PermissionUpdate]? attr_reader tool_use_id: String? @@ -1147,7 +1159,7 @@ module ClaudeAgent def to_h: () -> Hash[Symbol, untyped] end - class PermissionResultDeny + class PermissionResultDeny < ImmutableRecord attr_reader message: String attr_reader interrupt: bool attr_reader tool_use_id: String? @@ -1157,7 +1169,7 @@ module ClaudeAgent def to_h: () -> Hash[Symbol, untyped] end - class PermissionUpdate + class PermissionUpdate < ImmutableRecord attr_reader type: String attr_reader rules: Array[PermissionRuleValue]? attr_reader behavior: String? @@ -1169,7 +1181,7 @@ module ClaudeAgent def to_h: () -> Hash[Symbol, untyped] end - class PermissionRuleValue + class PermissionRuleValue < ImmutableRecord attr_reader tool_name: String? attr_reader rule_content: String? @@ -1177,7 +1189,7 @@ module ClaudeAgent def to_h: () -> Hash[Symbol, untyped] end - class ToolPermissionContext + class ToolPermissionContext < ImmutableRecord attr_reader permission_suggestions: untyped attr_reader blocked_path: String? attr_reader decision_reason: String? @@ -1334,7 +1346,7 @@ module ClaudeAgent end # A single tool execution in the conversation timeline - class ToolActivity + class ToolActivity < ImmutableRecord attr_reader tool_use: ToolUseBlock | ServerToolUseBlock attr_reader tool_result: ToolResultBlock | ServerToolResultBlock | nil attr_reader turn_index: Integer @@ -1675,7 +1687,7 @@ module ClaudeAgent end # Spawn options for custom process creation (TypeScript SDK parity) - class SpawnOptions + class SpawnOptions < ImmutableRecord attr_reader command: String attr_reader args: Array[String] attr_reader cwd: String? @@ -1762,7 +1774,7 @@ module ClaudeAgent # @alpha # V2 Session options (subset of full Options) - class SessionOptions + class SessionOptions < ImmutableRecord attr_reader model: String attr_reader path_to_claude_code_executable: String? attr_reader env: Hash[String, String]?