Skip to content

Releases: rubycatco/claude-agent-sdk-ruby

v1.4.3

Choose a tag to compare

@ya-luotao ya-luotao released this 28 Oct 12:41

Syncs with Python SDK 0.2.174 (previously 0.2.162). Python #1331 and #1332 were already in 1.2.0, and SandboxSettings#ignore_violations already took the command-pattern Hash that Python #1380 fixed its type to; every other release in the range only bumps the CLI Python bundles.

Fixed

  • get_session_messages and get_subagent_messages (disk and session_store:) no longer drop the tool calls of a parallel batch (Python SDK #1422). The conversation goes on from the tool result written last. When that result answers an earlier call of the batch than the last one, the tool_use entries written after that call were off the path the readers walk, so 1.2.0's fix (#157) never saw them: the readers returned neither those calls nor their results. Of the parallel batches in one developer's local transcripts, about 7% have this shape. The readers now put back every entry of an API message on the conversation, right after its last entry there (in timestamp order), then the results as before. A result that the CLI parented on a hook attachment instead of its call's entry (its sourceToolAssistantUUID still names the call) is put back as well, when the same agent wrote both. The guards from #157 hold: the chain's own result wins, at most one result comes back per call, and the result of a branch a rewind dropped stays out. That last guard was also too wide on real transcripts, and missed results #157 meant to return: going on from a result no longer counts as a dropped branch when what goes on is a later call of the same batch, another result of the batch (the CLI sometimes parents one under an earlier result's hook attachment), or a meta entry (the [Image: ...] note the CLI writes under a screenshot result). A branch that goes on through a meta entry or through another result of the batch is still recognized. Affected sessions return more messages than before, so offset:/limit: pages over them shift. Python fixes the main-session readers only; here both readers are fixed.

Changed

  • docs/options.md: include_hook_events does not turn off the SessionStart and Setup hook events, which the CLI sends whether or not it is set (Python SDK #1336).
  • CLIInstaller::PINNED_CLI_VERSION moves from 2.1.295 to 2.1.311, following the CLI the Python SDK bundles. CLIInstaller.install_pinned installs it. See the Claude Code changelog.

What's changed

  • #181 Sync with Python SDK 0.2.174 — @ya-luotao
  • #180 Bump PINNED_CLI_VERSION to 2.1.311 — @github-actions

Full Changelog: v1.4.2...v1.4.3


gem install claude-agent-sdk -v 1.4.3

v1.4.2

Choose a tag to compare

@ya-luotao ya-luotao released this 27 Oct 08:14

Fixed

  • bin/rails claude_agent_sdk:install_cli (and CLIInstaller.install whenever Encoding.default_internal is set) no longer fails with Encoding::UndefinedConversionError (issue #177). The download was opened with File::BINARY, which is 0 outside Windows, so the file was in text mode; Rails sets default_internal to UTF-8, and each write then transcoded the binary chunks and raised on the first byte that is not valid UTF-8. The file is now opened with binmode: true.

What's changed

  • #179 CLIInstaller: write the downloaded CLI in binary mode on every platform — @ya-luotao

Full Changelog: v1.4.1...v1.4.2


gem install claude-agent-sdk -v 1.4.2

v1.4.1

Choose a tag to compare

@ya-luotao ya-luotao released this 25 Oct 15:26

Changed

  • process_group: true: a SIGKILL the SDK sends the CLI also goes to the CLI's process group. A wedged CLI that the SDK had to kill could not stop its own children, so one still in its group that ignores stdin EOF (a stdio MCP server, for instance) kept running after the session; before process_group it would at least have got the terminal's Ctrl-C. The group SIGKILL goes out just before the SIGKILL of the CLI itself, whenever the SDK sends one (a normal teardown after its grace periods and SIGTERM, a cancelled teardown after its short grace, or a CLI that closed stdout and hung), and adds no delay. SIGTERM, including the one sent at interpreter exit, still goes to the CLI process only, so the CLI's own cleanup of its children still runs; a CLI that exits on SIGTERM or on its own gets no group signal, and processes that left its group are not reached. Only sessions with process_group: true on Unix are affected. This narrows what 1.4.0 documented ("the SDK still signals the CLI process, not its group"). See docs/options.md.

What's changed

  • #178 process_group: send the SDK's SIGKILL of the CLI to its process group too — @ya-luotao

Full Changelog: v1.4.0...v1.4.1


gem install claude-agent-sdk -v 1.4.1

v1.4.0

Choose a tag to compare

@ya-luotao ya-luotao released this 25 Oct 12:15

Added

  • ClaudeAgentOptions#process_group starts the CLI process in a process group of its own (issue #175). In a terminal, Ctrl-C signals the whole foreground process group, so the CLI used to get the SIGINT along with your process and end the turn on its own before your code could decide what to do. With process_group: true the terminal's Ctrl-C no longer reaches the CLI, and your process owns it: call Client#interrupt or disconnect from ordinary code (not from inside the signal trap, where Ruby raises ThreadError). Off by default, so nothing changes unless you set it. Sends nothing to the CLI; uses pgroup: true on Unix and new_pgroup: true on Windows, where it is untested. Teardown is unchanged: the SDK still signals the CLI process, not its group. Ruby only: the Python SDK's equivalent (PR #617) is unmerged as of 2026-10-25. docs/options.md covers the trap pattern, when the CLI stops, and the background-group caveat for tools that read the terminal.

Changed

  • README: the Claude Code overview link (it led to a "Page not found" page) and the built-in tools link now point at code.claude.com. "Rails in a minute" is a section of its own after the Quick Start, and its job turns off auto-memory (CLAUDE_CODE_DISABLE_AUTO_MEMORY) as the Rails guide's does. The hooks example defines the two callbacks it passes, the custom-tool example runs a prompt, and the Examples table lists subagent_status_example.rb. What a session without credentials, or with a rejected key, returns is now described in a new "Authentication Failures" section of docs/errors.md.

Usage

interrupts = Thread::Queue.new
previous = trap('INT') { interrupts << :int } # nothing else in here: no SDK calls
begin
  options = ClaudeAgentSDK::ClaudeAgentOptions.new(process_group: true)
  ClaudeAgentSDK::Client.open(options: options) do |client|
    watcher = Thread.new do
      client.interrupt if interrupts.pop # on this thread, outside the trap
    end
    client.query('Refactor the parser')
    client.receive_response { |message| puts message }
  ensure
    interrupts << nil # ends the watcher; the queue stays open for the trap
    watcher&.join
  end
ensure
  trap('INT', previous) # Ctrl-C behaves as before once the session is over
end

What's changed

  • #176 Add ClaudeAgentOptions#process_group: start the CLI in its own process group — @ya-luotao

Full Changelog: v1.3.0...v1.4.0


gem install claude-agent-sdk -v 1.4.0

v1.3.0

Choose a tag to compare

@ya-luotao ya-luotao released this 12 Oct 09:42

Added

  • Every hook input carries raw_input, the payload exactly as the CLI sent it. It used to be on UnknownHookInput only; a typed input copied the fields its class names and dropped the rest, so a field a newer CLI added was out of reach until the SDK learned it. Hook inputs are now built from the whole payload (every field the class declares, read from the same key as before). #inspect shows it on an UnknownHookInput only: a typed input already prints those fields as attributes.
  • Hook input fields the CLI already sends (Python SDK #1331, plus the schemas of Claude Code 2.1.295): prompt_id and effort on every hook input; duration_ms on PostToolUseHookInput and PostToolUseFailureHookInput; session_title and source on UserPromptSubmitHookInput; session_title, seconds_since_last_response, prompt_cache_likely_expired, context_tokens and estimated_cache_write_usd on SessionStartHookInput; parent_file_path on InstructionsLoadedHookInput.
  • Typed inputs for the hook events PostToolBatch, UserPromptExpansion, PreModelSwitch, PostModelSwitch, DirectoryAdded and MessageDisplay (PostToolBatchHookInput, ...), which used to arrive as UnknownHookInput. HOOK_EVENTS lists them, so it now has the CLI's 33 events.
  • Hook-specific outputs from Python SDK #1331: PostToolBatchHookSpecificOutput, UserPromptExpansionHookSpecificOutput, StopHookSpecificOutput, SubagentStopHookSpecificOutput, PreModelSwitchHookSpecificOutput, PostModelSwitchHookSpecificOutput, ElicitationHookSpecificOutput, ElicitationResultHookSpecificOutput, WorktreeCreateHookSpecificOutput and MessageDisplayHookSpecificOutput; session_title and suppress_original_prompt on UserPromptSubmitHookSpecificOutput; initial_user_message, session_title, watch_paths and reload_skills on SessionStartHookSpecificOutput. A Hash return value could already send these keys; the typed classes rejected them with ArgumentError. docs/hooks-and-permissions.md now lists every output class and its fields.
  • InitMessage#capabilities, #plugin_errors, #terminal_slash_commands and #fast_mode_disabled_reason, the init fields of Python SDK #1332 (SystemInitData) that InitMessage did not read. Test capabilities (for example for "interrupt_receipt_v1") instead of comparing claude_code_version.

Changed

  • The repository moves to the rubycatco GitHub organization: github.com/rubycatco/claude-agent-sdk-ruby. The old ya-luotao/... URL redirects, and the gem on rubygems.org is unaffected; gem metadata, docs, templates and issue links now point at the new home.
  • PermissionDeniedHookSpecificOutput#retry defaults to nil and is then left out of the output, as in Python; it used to default to false and always send retry: false, which the CLI reads the same way. Code that reads #retry back from a fresh instance gets nil.
  • The *HookSpecificOutput classes are declared from one table of wire keys (HookOutputKeys::HOOK_SPECIFIC) instead of each writing its own #to_h. Each class still sends the same keys, except that a field set to false is now sent (only nil is left out); before, most fields also dropped false.
  • CLIInstaller::PINNED_CLI_VERSION moves from 2.1.288 to 2.1.295, following the CLI the Python SDK bundles. CLIInstaller.install_pinned installs it. See the Claude Code changelog.

Usage

hook = lambda do |input, _tool_use_id, _context|
  input.prompt_id                    # new on every hook input
  input.raw_input[:scratchpad_dir]   # a field this SDK version has no reader for

  ClaudeAgentSDK::SyncHookJSONOutput.new(
    hook_specific_output: ClaudeAgentSDK::UserPromptSubmitHookSpecificOutput.new(session_title: 'Staging rollout')
  )
end

options = ClaudeAgentSDK::ClaudeAgentOptions.new(
  hooks: { 'UserPromptSubmit' => [ClaudeAgentSDK::HookMatcher.new(hooks: [hook])] }
)

What's changed

  • #166 Bump PINNED_CLI_VERSION to 2.1.289 — @github-actions
  • #170 Bump PINNED_CLI_VERSION to 2.1.291 — @github-actions
  • #171 Bump PINNED_CLI_VERSION to 2.1.293 — @github-actions
  • #172 Bump PINNED_CLI_VERSION to 2.1.295 — @github-actions
  • #173 Move repository references to the rubycatco organization — @ya-luotao
  • #174 Follow Python SDK #1331/#1332: hook types, raw hook input, init fields — @ya-luotao

Full Changelog: v1.2.1...v1.3.0


gem install claude-agent-sdk -v 1.3.0

v1.2.1

Choose a tag to compare

@ya-luotao ya-luotao released this 06 Oct 02:54

An internal release: three refactors that move duplicated or scattered logic into one private module each. No public API, signature (sig/) or documented behavior changes; one cleanup difference is noted below.

Changed

  • ClaudeAgentSDK.query and Client share one implementation of session setup, prompt writes, message delivery and teardown (SessionAssembly, @api private); a session's observers, callback_scheduling and callback_wrapper travel in one object (Dispatch, @api private) instead of being passed at every call site. The documented differences between the two entry points are preserved and now pinned by specs that run both side by side. One cleanup difference: when a Client is reconnected while its previous disconnect is still removing the materialized directory of a store-backed resume, the finishing disconnect no longer clears the new session, so the new session's resume directory is removed by its own disconnect (it used to be left behind). (#169)
  • The decision of when query() may close stdin lives in one private class, Query::RunLifecycle; Query feeds it events and supplies the timer. Its specs go through that interface instead of Query's instance variables. (#168)
  • Hash-form option interpretation (system_prompt, thinking, tools, output_format, task_budget, plugins, sandbox, mcp_servers, agents) lives in one private module, OptionForms; every existing per-option spelling and key policy is preserved as it was. (#167)

What's changed

  • #167 Move Hash-form option interpretation into one private module (OptionForms) — @ya-luotao
  • #168 Extract the run-end decision into Query::RunLifecycle — @ya-luotao
  • #169 Share one session assembly between query() and Client — @ya-luotao

Full Changelog: v1.2.0...v1.2.1


gem install claude-agent-sdk -v 1.2.1

v1.2.0

Choose a tag to compare

@ya-luotao ya-luotao released this 03 Oct 09:30

Fixes from a review of the whole SDK, a reference for every option (docs/options.md), and Claude Code 2.1.288 as the pinned CLI. Settings the SDK used to drop without an error now take effect — hook outputs and sandbox: Hashes in Ruby spelling, a tools: String, option Hashes with a Symbol type — so read the entries in bold before upgrading: they say what changes for code that relied on the old behavior.

Added

  • docs/options.md: a reference for every ClaudeAgentOptions attribute — its type, its default, and the CLI flag, initialize field or environment variable it becomes — plus what an option reads when it is left out, passed as nil or given a configured default, and the environment variables the SDK reads. It documents options that had no entry anywhere before (extra_args, include_partial_messages, include_hook_events, task_budget, strict_mcp_config) and the CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS ceiling from 1.1.0. A spec checks every row against the code. docs/client.md documents the transport: argument of query / ask. (#155)
  • Pathname where a path goes: settings: (a settings file; used to send no --settings at all), SystemPromptFile#path, the path of a { type: 'file' } system prompt Hash, and a plugin path, typed or Hash (these failed at connect with no implicit conversion of Pathname into String). tools: is signed String as well; the RBS signatures are widened accordingly. (#160)

Changed

  • docs/cli-installer.md now says when CLIInstaller::PINNED_CLI_VERSION moves: pin-only changes ship with the next gem release rather than one release per CLI bump. It also covers how a gem patch can move your CLI and how to take a newer CLI before a release pins it. The pin-bump workflow keeps a single [Unreleased] entry for the pin.
  • CLIInstaller::PINNED_CLI_VERSION moves from 2.1.285 to 2.1.288, following the CLI the Python SDK bundles. CLIInstaller.install_pinned installs it. See the Claude Code changelog.
  • The Rails guide and both Rails examples turn off the CLI's per-project auto-memory (env: { 'CLAUDE_CODE_DISABLE_AUTO_MEMORY' => '1' }). Every SDK session reads that memory and a claude_code-preset session writes it without a permission check, so in a multi-user app one user's "remember …" would reach every other user's session. The generated initializer only documents the switch: it carries the same line commented out, so a newly generated app is not isolated until you uncomment it (or set it per call). If you copied the examples, add the line; the value must be '1'. (#158)
  • docs/configuration.md has a "Session Isolation" section. Claude Code's per-project auto-memory is on in SDK sessions: every session loads the project's MEMORY.md index into its context (setting_sources: [] does not prevent it), and a session on the claude_code preset also writes memory files, which the CLI allows without any permission setup. Servers and multi-tenant hosts should set env: { 'CLAUDE_CODE_DISABLE_AUTO_MEMORY' => '1' } (or settings: { autoMemoryEnabled: false }); a custom transport has to pass either one through to its CLI itself; Client#context_usage[:memoryFiles] shows what a session loaded. (#155)
  • A requested sandbox that is not active now produces a warning. sandbox: { enabled: true } does not guarantee a sandbox: when the CLI cannot start one (a Linux container without bubblewrap and socat, for example) it prints ⚠ Sandbox disabled: … to its stderr and runs commands unsandboxed, which a host only saw through a stderr: callback. The SDK now repeats that line as a Ruby warning, prefixed [claude-agent-sdk], once per session, when the sandbox: option enabled the sandbox. The default is unchanged; set fail_if_unavailable: true to make the CLI exit instead. (#156)
  • OTelObserver now leaves a span for a session that fails before its first InitMessage. A CLI that could not be found or started, or an initialize that failed or timed out, raised to the caller but exported no span at all (0.18.0's "crashed sessions now produce OTel traces with error status" only held once an InitMessage had arrived). on_error now emits a claude_agent.session span with the observer's default attributes, the prompt if one had been sent, the exception event and error status, and ends it at once. The span has no model or session.id. Nothing changes once a trace has started. If you count claude_agent.session spans, failed starts are now among them. (#148)
  • docs/observability.md no longer promises "standard gen_ai.* semantic conventions … any OTel backend". The attributes follow the Langfuse and OpenInference conventions plus a subset of OTel gen_ai.*; the page now lists every attribute, event and status OTelObserver sets, and how the gen_ai.* names differ from the OTel GenAI semantic conventions — notably that gen_ai.usage.input_tokens leaves cache tokens out, and that the inclusive count is llm.token_count.prompt on the session span. No attribute changed. (#148)
  • docs/rails.md no longer says callbacks inherit the request's state. A callback runs on a thread or fiber Rails never set up: Current attributes, Time.zone, log tags, the error context and the connected_to role / shard read as their defaults there (a write under connected_to(shard:) lands in the default shard), with or without Railtie.callback_wrapper; I18n.locale depends on the i18n version. The guide now has the table, a per-call recipe that carries the state across, the rule for composing your own wrapper with the Rails one, a section on transactions and the connection pool (a callback is outside the caller's transaction and uses its own connection), and a note that in development an agent run holds the reload lock. (#158)
  • docs/configuration.md says what the CLI does with an invalid sandbox value: it discards the whole --settings value, sandbox and permissions rules alike. The SDK does not surface this — connect succeeds and nothing appears on stderr; only the CLI's get_settings control response lists the error. That holds for every form, the typed classes included. (#160)
  • Disk listings (list_sessions, get_session_info) scan each transcript's head and tail windows as bytes; with non-ASCII text in a window this takes a fraction of the CPU time it did. Results are unchanged. (#157)
  • Development only: the gemspec's development dependency on Bundler is >= 2.0 (was ~> 2.0), so bundle install works in a checkout on Ruby 4.0, which ships Bundler 4. CI now runs the suite and RuboCop on Ruby 4.0, and a keyless smoke runs the pinned CLI on PRs that touch the installer, the transport, Query or the message parser; the API-key integration job shows as skipped instead of passed when the key is absent. Runtime dependencies are unchanged. (#153)

Fixed

  • A hook callback's Hash return value now means what the equivalent typed output means, whatever its key style. Only five top-level keys were renamed before, and only as Symbols, so { hook_specific_output: { hook_event_name: 'PreToolUse', permission_decision: 'deny' } } reached the CLI under keys it does not read: the decision was ignored and the tool ran. A String-keyed Hash (e.g. after JSON.parse) and a SyncHookJSONOutput holding a snake_case Hash failed the same way. The fields the typed output classes model are now accepted in snake_case or camelCase, as Symbols or Strings, at the top level and inside hook_specific_output; any other key is sent as written, and the keys inside updated_input, the tool outputs and a PermissionRequest decision are not renamed. If a hook of yours returns snake_case keys, they now take effect. When one Hash spells a field both ways the CLI's spelling is sent, and a Symbol and a String spelling the same key are written once (json 3.x used to fail the hook with detected duplicate key). (#149)
  • A sandbox: Hash written in Ruby spelling is now applied. The typed sandbox classes take snake_case attributes and write the camelCase keys the CLI reads, but a Hash was forwarded as written: sandbox: { enabled: true, network: { denied_domains: ['evil.example'] }, filesystem: { deny_read: ['~/.aws'] } } reached the CLI under keys it does not know, and the CLI ignored them without an error — the session ran with network: {} and filesystem: {}, no deny rule. SandboxSettings.new(network: { denied_domains: [...] }) lost its rules the same way. The fields of SandboxSettings, SandboxNetworkConfig and SandboxFilesystemConfig are now accepted in snake_case or camelCase, as Symbols or Strings, at the top level and inside network / filesystem; any other key is sent as written. If a sandbox Hash of yours has snake_case keys, they now take effect. A snake_case key is renamed only when its value has the shape the CLI accepts for it (true / false, an Array of Strings, a port number from 0 to 65535); with any other value it is sent as written and stays without effect, as before — so the rename can never make the CLI discard your settings. When one Hash spells a field both ways...
Read more

v1.1.0

Choose a tag to compare

@ya-luotao ya-luotao released this 30 Sep 18:47

Syncs with Python SDK 0.2.162. Additive only: one new option and a fix to when query() closes stdin.

Added

  • ClaudeAgentOptions#verbatim_prompts (default false; Python SDK #1269). When true, every user message the SDK writes carries client_composed: true: String prompts and each message of a streamed prompt, through query, Client#connect and Client#query. Claude Code then delivers the text as written, with no @path file expansion and no slash-command dispatch. Use it when a prompt includes text your end user did not type. tools: [] and disallowed_tools do not stop @path expansion, which reads the file before the model runs. While the option is on, a caller's client_composed key is overwritten, and caller Hashes are never mutated. A streamed JSONL String is parsed and re-serialized; one that is not a single JSON object raises ArgumentError rather than going out unmarked. Client reads the option once, at connect. On current CLIs such a turn also skips the turn-start attachment pass (nested CLAUDE.md and rules files, skill and tool listings). Requires Claude Code 2.1.248 or later; the SDK warns when it connects to an older CLI with the option on. See docs/configuration.md.
  • Development only, nothing ships in the gem: TLA+ models in formal/tla/ of the two most concurrency-sensitive designs. CLIInstaller.tla covers CLIInstaller.install's publish order, the in-lock dist-tag resolve and the stale-temp sweep. ControlProtocol.tla covers the outbound control-request waiter protocol in Query. formal/tla/run.sh model-checks both with TLC. It confirms that the shipped design passes and that each alternative the source comments reject, including the historical rename-then-record order, produces a counterexample.

Changed

  • CLIInstaller::PINNED_CLI_VERSION moves from 2.1.280 (the 1.0.0 pin) to 2.1.285, the CLI the Python SDK 0.2.162 bundles. CLIInstaller.install_pinned installs it. It honors both client_composed (verbatim_prompts) and CLAUDE_CODE_SDK_READS_SESSION_STATE (the fix below).

Fixed

  • query() with hooks, can_use_tool or SDK MCP servers no longer closes stdin while a follow-up turn is still owed (Python SDK #1279, fixing Python issue #1190). A background subagent that finished just before the turn's result was already off the in-flight ledger, so stdin closed at that result, and the follow-up turn its completion triggered had every hook, permission and SDK MCP request fail with "Stream closed" (the model reported the tool as refused). The transport now sets CLAUDE_CODE_SDK_READS_SESSION_STATE=1 unless options.env or the environment already names it (in any case; a nil value in options.env unsets it). The CLI then sends session_state_changed frames marked sdk_host_only, which the SDK drops from the message stream for query() and Client alike. query() keeps stdin open until the CLI reports idle after a result:
    • The wait between turns is bounded by CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS, read from options.env and then the environment; the default is 10 minutes and 0 means no limit. The clock restarts at each result and at each running. A main-thread turn, the CLI reporting requires_action, a tracked background agent and a hook, permission or SDK MCP request the SDK is still answering each stop it.
    • With an Enumerable prompt, each message written waits for its own run. Work the CLI takes up after the run ended reopens it.
    • A CLI that sends no state (2.1.282 and earlier) gets the old behavior: stdin closes at the first result with no tracked task in flight.
    • Background agents, bounded monitors and MCP tasks can now keep a one-shot run open, up to the ceiling. Background shells, persistent monitors and remote agents do not, because the CLI leaves them out of its running report.
    • SessionStateChangedMessage reaches your code only if you set CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS=1 yourself; the SDK never sets it.
    • A custom transport must pass CLAUDE_CODE_SDK_READS_SESSION_STATE=1 to the CLI itself to get this behavior (see docs/client.md); the E2B example transport now does.

Usage

# Prompt text your end user did not type can no longer pull local files in via @path
options = ClaudeAgentSDK::ClaudeAgentOptions.new(verbatim_prompts: true)
ClaudeAgentSDK.query(prompt: untrusted_text, options: options) { |message| ... }

What's changed

  • #145 Sync with Python SDK 0.2.162: verbatim_prompts + stdin open until idle — @ya-luotao
  • #144 Bump PINNED_CLI_VERSION to 2.1.285 — @github-actions
  • #143 Bump PINNED_CLI_VERSION to 2.1.284 — @github-actions
  • #142 Bump PINNED_CLI_VERSION to 2.1.283 — @github-actions
  • #141 Add TLA+ models of the CLI installer and control-request protocol — @ya-luotao

Full Changelog: v1.0.0...v1.1.0


gem install claude-agent-sdk -v 1.1.0

v1.0.0

Choose a tag to compare

@ya-luotao ya-luotao released this 23 Sep 15:39

1.0 is a stability commitment. From here on the public API — everything documented in docs/ plus the YARD docs without @api private, now also described by the RBS signatures in sig/ — follows Semantic Versioning: no breaking changes within 1.x. Upgrading from 0.x? Read UPGRADING-1.0.md and run your suite on 0.37 first.

1.0 (#126): three breaking changes, the first two of which 0.37 warns about at the call site. Read UPGRADING-1.0.md before upgrading and run your suite on 0.37 with warnings visible first: an app that runs on 0.37 without SDK warnings is unaffected by those two, except that respond_to? on a camelCase non-attribute (msg.respond_to?(:toH)) silently answered true on 0.37 and answers false now.

Added

  • ClaudeAgentSDK::SessionStoreError (a ClaudeSDKError), raised by query, ask and Client#connect when resuming from session_store: fails: a store call (#load, #list_sessions, #list_subkeys) raised or exceeded load_timeout_ms while the SDK materialized the transcript. The message names the store call, and #cause holds the adapter's exception (or the timeout). Documented in docs/errors.md and docs/sessions.md.
  • RBS signatures for the public API (#126). The gem now ships sig/, so Steep and other RBS tools type-check code that uses it (through rbs collection; sig/manifest.yaml declares the one stdlib dependency, pathname). The signatures cover the whole public surface (the objects documented in docs/ and YARD without @api private): ClaudeAgentSDK.query / .ask / .configure / .offload, the SDK MCP helpers, every session function (including session_store: and the deprecated twins), Client with its Ruby-style aliases, ClaudeAgentOptions (every option as a typed keyword), the message, content-block, hook, permission and MCP types, the error hierarchy, CLIInstaller, Streaming, Railtie.callback_wrapper and the observers. Duck types are interfaces, so any object with the right methods fits: _Transport, _SessionStore (only #append and #load are required), and one interface per user callback (_CanUseTool, _HookCallback, _ToolHandler, _CallbackWrapper, ...). Hashes follow the documented key rule: wire_hash (Hash[Symbol, untyped]) for data from the CLI stream, transcript_hash (Hash[String, untyped]) for transcripts and store data. Internals tagged @api private have no signatures. CI validates the signatures (rake rbs:validate) and runs the suite under RBS's runtime type checker (rake rbs:test), so a signature that disagrees with the code fails the build. CONTRIBUTING.md explains how to keep sig/ in step with API changes.

Changed

  • Breaking: unknown keys on user-constructed types raise ArgumentError. .new and #[]= on the value types you build and pass in (option values such as AgentDefinition, SandboxSettings, the thinking and MCP server configs; HookMatcher; hook outputs; PermissionResultAllow / PermissionResultDeny; PermissionUpdate; PermissionRuleValue) raise instead of warning, with a message naming the class, the key and the known keys: ClaudeAgentSDK::HookMatcher: unknown attribute :matchr (known: hooks, matcher, timeout). camelCase and String keys and a type's own discriminator are still accepted; .from_hash, .wrap and every type parsed from CLI output stay lenient.
  • Breaking: Type#[], #[]= and camelCase methods reach attributes only. A name that is not an attribute behaves like an undefined one: msg[:to_h] is nil, #[]= ignores it (raises on the strict types above), msg.toH raises NoMethodError and respond_to?(:toH) is false. Methods your own code adds to a subclass, mixin or instance still count as attributes.
  • Breaking: store-backed resume failures raise SessionStoreError instead of RuntimeError. The two SDK-raised RuntimeErrors (store call failed, store call timed out) become SessionStoreError, and a RuntimeError raised by the adapter itself, which 0.37 let through unwrapped, is now wrapped too, so rescue ClaudeSDKError catches every resume-materialization failure. Code that rescued RuntimeError there must rescue SessionStoreError. The failure message now includes the adapter exception's class (... failed during resume materialization: IOError: connection reset).
  • UPGRADING-1.0.md ships in the gem and the YARD docs, linked from the README.
  • The deprecation warning printed by the ten *_from_store / *_via_store session functions, and their YARD and docs, now say they will be removed in 2.0. 0.36 and 0.37 said 1.0, but they stay, deprecated, for all of 1.x (#126). Nothing else about them changes: each still works and still warns once per process.

Upgrading from 0.x

  1. Upgrade to ~> 0.37.0 and run your suite with warnings visible — every call that 1.0 rejects prints a warning pointing at your code.
  2. Fix those, check any rescue RuntimeError around store-backed resume (now SessionStoreError).
  3. gem 'claude-agent-sdk', '~> 1.0'.

Full guide: UPGRADING-1.0.md

What's changed

  • #138 1.0: strict attribute keys, attribute-only access, SessionStoreError — @ya-luotao
  • #139 RBS signatures for the public API — @ya-luotao

The road here: #126 (0.35 Rails integration → 0.36 API consolidation → 0.37 deprecations → 1.0).

Full Changelog: v0.37.0...v1.0.0


gem install claude-agent-sdk -v 1.0.0

v0.37.0

Choose a tag to compare

@ya-luotao ya-luotao released this 23 Sep 14:09

The last 0.x release before 1.0 (roadmap). No runtime behaviour changes — only new warnings for things 1.0 will reject. Run your suite on 0.37 with warnings visible before moving to 1.0:

  • A misspelled key on a value type you build (HookMatcher, AgentDefinition, SandboxSettings, MCP server configs, permission results, hook outputs, …) now warns once, pointing at your call; 1.0 raises ArgumentError.
  • msg[:name] / camelCase readers reaching a non-attribute method (e.g. msg[:to_h]) warn once; in 1.0 they only reach attributes.
  • The 1.0 SemVer surface is now visible: SDK internals are tagged @api private and hidden from the API docs, and the documented contracts (Hash-key rule, mcp_status / context_usage return Hashes, Type#[]) are written down.

Changed

  • Docs: pre-1.0 contracts written down (#126). No code changes. Client#mcp_status / #get_mcp_status and #context_usage / #get_context_usage return the CLI's raw Hash with camelCase Symbol keys. docs/types.md had described McpStatusResponse as the response type; it is the optional typed view, via McpStatusResponse.parse(client.mcp_status), and there is no typed context-usage class (docs/client.md). A new "Hash keys" section in docs/types.md states one rule: Hashes passed through from the live CLI stream (origin, usage, model_usage, hook tool_input, can_use_tool input, SDK MCP tool args, control responses) have Symbol keys spelled as on the wire, and Hashes read from transcripts or a SessionStore (SessionMessage#message, get_subagent_metadata, store keys and entries) have String keys. The docs, examples and bundled skill drop their h[:key] || h['key'] fallbacks in favour of the one correct form. Type#[], #[]= and the camelCase readers are documented as public API, including that #[]= changes the object you received. docs/sessions.md documents ClaudeAgentSDK.project_key_for_directory and ClaudeAgentSDK.fold_session_summary for adapter authors, and import_session_to_store's batching (batch_size: defaults to 500 entries, and a batch also ends at about 1 MiB).
  • SDK internals are now tagged @api private and hidden from the YARD docs (#126). The tagged namespaces are Query, MessageParser, TranscriptMirrorBatcher, Sessions, SessionMutations, SessionResume, MaterializedResume, SessionSummary, SessionStores, OptionWarnings and FiberBoundary, including everything nested in them. Some public classes also have internal members, and those are tagged individually: SubprocessCLITransport's methods outside the Transport interface and its constants; CommandBuilder's constants (.new and #build stay public); CLIInstaller::Http, Platform, Release and Metadata and the installer's constants other than PINNED_CLI_VERSION; SdkMcpServer#handle_json, #handle_message and SdkMcpServer::ToolInputSchema; Client#query_handler; and ClaudeAgentSDK::OBSERVER_INTERFACE. Their public faces are unchanged: ClaudeAgentSDK.offload, fold_session_summary, the session functions and SDKSessionInfo / SessionMessage remain public. Nothing changes at runtime. No method or constant was renamed, removed or made private, so every existing call keeps working. From 1.0, SemVer covers docs/ and the YARD API without @api private; objects tagged @api private are not covered and may change in any release. CONTRIBUTING.md has a new "What is public API" section.
  • lib/claude_agent_sdk/types.rb is split into one file per area under lib/claude_agent_sdk/types/ (messages, hooks, options, ...). The code moved without edits and types.rb still loads every part, so require 'claude_agent_sdk/types' and every class and constant are unchanged; no API change (#126).
  • Type's internal machinery is tagged @api private and leaves the rendered docs: Type#dup_for_options, Type.deep_dup_for_options, Type::OptionValue, Type.inspect_filtered, Type.inspect_filtered_attributes, and the new Type.strict_attributes / .strict_attributes? / .declare_attributes / .attribute? / .attribute_names. They are not part of the SemVer surface. Type#[], #[]=, the camelCase readers, #to_h, .wrap and .from_hash stay public (#126).

Deprecated

  • Unknown keys on user-constructed types. The value types you build and pass in (option values such as AgentDefinition, SandboxSettings, the thinking configs and the MCP server configs; HookMatcher; hook outputs; PermissionResultAllow / PermissionResultDeny; PermissionUpdate; PermissionRuleValue) silently dropped a misspelled key, so HookMatcher.new(matchr: 'Bash') quietly built a matcher with no matcher set. .new and #[]= now warn once per class and key, at the caller's line: ClaudeAgentSDK::HookMatcher: unknown attribute :matchr ignored; this will raise ArgumentError in 1.0 (known: hooks, matcher, timeout). In 1.0 they raise ArgumentError, as ClaudeAgentOptions already does. camelCase and String keys and a type's own discriminator (type, hook_event_name, behavior) are accepted, so klass.new(value.to_h) round-trips silently. Types parsed from CLI output (messages, content blocks, hook inputs) and every construction through .from_hash / .wrap stay lenient, so a newer CLI's extra fields never warn; permission suggestions from the CLI are now hydrated through PermissionUpdate.wrap for that reason. Full list in docs/types.md (#126).
  • Type#[], #[]= and camelCase methods reaching something other than an attribute. They are public API for a type's attributes (its attr_* fields, the backward-compatible RateLimitEvent#data, predicates such as options.forkSession?, and any method your own code adds to a subclass, mixin or instance), but they resolved any public method: msg[:to_h] returned a Hash, msg['freeze'] froze the message, msg.toH called #to_h. Such a call still works but warns once per class and name, e.g. ClaudeAgentSDK::ResultMessage#[]: :to_h is not an attribute; Type#[] will only read attributes in 1.0. In 1.0 a name that is not an attribute behaves like an undefined one: #[] returns nil, #[]= ignores it (raises on the types above), a camelCase call raises NoMethodError, and respond_to? says false. Names that are not methods at all keep today's behaviour and do not warn. UserMessage#text / AssistantMessage#text are convenience methods, not attributes (#126).

Check your app before 1.0

# warnings are on by default; make sure your test runner doesn't silence them
bundle exec rspec 2>&1 | grep -E "unknown attribute|is not an attribute|deprecated"

What's changed

  • #133 Split types.rb into lib/claude_agent_sdk/types/ (no API change) — @ya-luotao
  • #136 Warn on unknown keys and non-attribute access on Type (0.37; enforced in 1.0) — @ya-luotao
  • #134 Tag leaked SDK internals @api private — @ya-luotao
  • #135 Write down the pre-1.0 contracts — @ya-luotao
  • #137 Turn Metrics back on everywhere; delete .rubocop_todo.yml — @ya-luotao

Roadmap to 1.0: #126

Full Changelog: v0.36.0...v0.37.0


gem install claude-agent-sdk -v 0.37.0