claude-agent-sdk 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 52b6649f2a72ef33e746d168e4016dcc72c1439157fbe5b971edbd55b0a0d404
4
- data.tar.gz: a344cc38d96ef7185630c1e1518a5cf6d08a855f40b39092728236d5a48df50d
3
+ metadata.gz: ca3c257ced832f2e2120e7600aa346ef7e8487567309ac1ff579e8672743819e
4
+ data.tar.gz: f3598c2b3abe03b67042b5f04b5a0fd4b6cc12f80ba73d36bb9708e6b080d94e
5
5
  SHA512:
6
- metadata.gz: 18a32d856585a36aa39a9aa586829a305c099cd4686a338e6f1df5310490abb81808821940a6be5d6c830a1fc86cb1cd95dab5ca8e2c5737574e3e91da513f5f
7
- data.tar.gz: c6dadb9b32d373e1fac9246b5bafc8456f3779cf0187abceb7bd85ecdd96b823c95c49c751db9bcd4b384ceb34141057b9e48d4540366d1e3d7d951a69e0aa7f
6
+ metadata.gz: d88cfa73d92aa49aaf83d94ad51ba6f0e65761c51b7f4703477e606765a4cee06f65725cf295f30e8605c694cafc40f389f96e131f0db668db145ec31409da02
7
+ data.tar.gz: fa86ac725b72ee1896525cccaebc119b0d1c8138c931e375e95e8549275ff0b89426cc9dbaa2f352ff389e9b7614d4cb7164a1c290ed92c0b03634246f17db4b
data/CHANGELOG.md CHANGED
@@ -5,6 +5,46 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.11.0] - 2026-03-20
9
+
10
+ ### Added
11
+
12
+ #### Custom Transport Support
13
+ - `Client.new` accepts `transport_class:` and `transport_args:` keyword arguments, allowing consumers to plug in custom transports (e.g., E2B sandbox, remote SSH) without duplicating `Client#connect` internals
14
+ - Default remains `SubprocessCLITransport` — zero behavior change for existing callers
15
+ - Custom transport class must implement the `Transport` interface (`connect`, `write`, `read_messages`, `end_input`, `close`, `ready?`)
16
+ - `transport_args` are passed as keyword arguments to `transport_class.new(options, **transport_args)`
17
+ - All option transformations, MCP extraction, hook conversion, and Query lifecycle stay in `Client#connect` — the transport only handles I/O
18
+
19
+ ## [0.10.0] - 2026-03-20
20
+
21
+ Port of Python SDK v0.1.48 features for feature parity.
22
+
23
+ ### Added
24
+
25
+ #### Session Mutations
26
+ - `ClaudeAgentSDK.rename_session(session_id:, title:, directory:)` — rename a session by appending a custom-title JSONL entry
27
+ - `ClaudeAgentSDK.tag_session(session_id:, tag:, directory:)` — tag a session (pass `nil` to clear); tags are Unicode-sanitized
28
+ - `SessionMutations` module with `rename_session`, `tag_session`, and internal Unicode sanitization helpers
29
+ - Ported from Python SDK's `_internal/session_mutations.py` with TOCTOU-safe `O_WRONLY | O_APPEND` file operations
30
+
31
+ #### AssistantMessage Usage
32
+ - `usage` attribute on `AssistantMessage` — token usage data from the API response
33
+ - `MessageParser` populates `usage` from `data.dig(:message, :usage)`
34
+
35
+ #### AgentDefinition Fields
36
+ - `skills`, `memory`, `mcp_servers` attributes on `AgentDefinition`
37
+ - Serialized as `skills`, `memory`, `mcpServers` (camelCase) in the CLI wire protocol initialize request
38
+
39
+ #### TaskUsage Typed Class
40
+ - `TaskUsage` class with `total_tokens`, `tool_uses`, `duration_ms` attributes
41
+ - `TaskUsage.from_hash` factory supporting symbol, string, camelCase, and snake_case keys
42
+
43
+ ### Removed
44
+
45
+ #### FGTS Environment Variable
46
+ - Removed auto-setting of `CLAUDE_CODE_ENABLE_FINE_GRAINED_TOOL_STREAMING` environment variable when `include_partial_messages` is enabled — Python SDK v0.1.48 reverted this because it causes HTTP 400 errors on LiteLLM proxies, Bedrock, and Vertex with Claude 4.5 models. The `--include-partial-messages` CLI flag remains the correct mechanism.
47
+
8
48
  ## [0.9.0] - 2026-03-12
9
49
 
10
50
  Port of Python SDK v0.1.48 parity improvements.
data/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  [![Gem Version](https://badge.fury.io/rb/claude-agent-sdk.svg?icon=si%3Arubygems)](https://badge.fury.io/rb/claude-agent-sdk)
8
8
 
9
- ### Feature Parity with Python SDK (v0.1.46)
9
+ ### Feature Parity with Python SDK (v0.1.48) + Ruby Extras
10
10
 
11
11
  | Feature | Python | Ruby |
12
12
  |---------|:------:|:----:|
@@ -25,13 +25,16 @@
25
25
  | Beta features (1M context) | ✅ | ✅ |
26
26
  | File checkpointing & rewind | ✅ | ✅ |
27
27
  | Session browsing (`list_sessions`, `get_session_messages`) | ✅ | ✅ |
28
+ | Session mutations (`rename_session`, `tag_session`) | ✅ | ✅ |
28
29
  | Task message types (started/progress/notification) | ✅ | ✅ |
29
30
  | MCP server control (reconnect/toggle/stop) | ✅ | ✅ |
30
31
  | Subagent context on hook inputs | ✅ | ✅ |
31
32
  | Typed MCP status response | ✅ | ✅ |
32
33
  | `stop_reason` on `ResultMessage` | ✅ | ✅ |
34
+ | `usage` on `AssistantMessage` | ✅ | ✅ |
33
35
  | Fallback model | ✅ | ✅ |
34
36
  | Plugin support | ✅ | ✅ |
37
+ | Custom transport (pluggable I/O layer) | — | ✅ |
35
38
  | Rails integration (configure block, ActionCable) | — | ✅ |
36
39
  | Bundled CLI binary | ✅ | — |
37
40
 
@@ -125,6 +128,7 @@ Both SDKs spawn `claude` CLI as a subprocess with stream-JSON over stdin/stdout.
125
128
  - [Quick Start](#quick-start)
126
129
  - [Basic Usage: query()](#basic-usage-query)
127
130
  - [Client](#client)
131
+ - [Custom Transport](#custom-transport)
128
132
  - [Custom Tools (SDK MCP Servers)](#custom-tools-sdk-mcp-servers)
129
133
  - [Hooks](#hooks)
130
134
  - [Permission Callbacks](#permission-callbacks)
@@ -137,6 +141,7 @@ Both SDKs spawn `claude` CLI as a subprocess with stream-JSON over stdin/stdout.
137
141
  - [Sandbox Settings](#sandbox-settings)
138
142
  - [File Checkpointing & Rewind](#file-checkpointing--rewind)
139
143
  - [Session Browsing](#session-browsing)
144
+ - [Session Mutations](#session-mutations)
140
145
  - [Rails Integration](#rails-integration)
141
146
  - [Types](#types)
142
147
  - [Error Handling](#error-handling)
@@ -153,7 +158,7 @@ Add this line to your application's Gemfile:
153
158
  gem 'claude-agent-sdk', github: 'ya-luotao/claude-agent-sdk-ruby'
154
159
 
155
160
  # Or use a stable version from RubyGems
156
- gem 'claude-agent-sdk', '~> 0.8.0'
161
+ gem 'claude-agent-sdk', '~> 0.11.0'
157
162
  ```
158
163
 
159
164
  And then execute:
@@ -355,6 +360,59 @@ Async do
355
360
  end.wait
356
361
  ```
357
362
 
363
+ ### Custom Transport
364
+
365
+ By default, `Client` uses `SubprocessCLITransport` to spawn the Claude Code CLI locally. You can provide a custom transport class to connect via other channels (e.g., E2B sandbox, remote SSH, WebSocket):
366
+
367
+ ```ruby
368
+ # Custom transport must implement the Transport interface:
369
+ # connect, write, read_messages, end_input, close, ready?
370
+ class E2BSandboxTransport < ClaudeAgentSDK::Transport
371
+ def initialize(options, sandbox:)
372
+ @options = options
373
+ @sandbox = sandbox
374
+ end
375
+
376
+ def connect
377
+ @sandbox.connect
378
+ end
379
+
380
+ def write(data)
381
+ @sandbox.stdin_write(data)
382
+ end
383
+
384
+ def read_messages(&block)
385
+ @sandbox.stdout_read_lines { |line| yield JSON.parse(line, symbolize_names: true) }
386
+ end
387
+
388
+ def end_input
389
+ @sandbox.close_stdin
390
+ end
391
+
392
+ def close
393
+ @sandbox.disconnect
394
+ end
395
+
396
+ def ready?
397
+ @sandbox.connected?
398
+ end
399
+ end
400
+
401
+ # Use it with Client — all connect orchestration (option transforms,
402
+ # MCP extraction, hook conversion, Query lifecycle) is handled for you
403
+ Async do
404
+ client = ClaudeAgentSDK::Client.new(
405
+ options: options,
406
+ transport_class: E2BSandboxTransport,
407
+ transport_args: { sandbox: my_sandbox }
408
+ )
409
+ client.connect
410
+ client.query("Hello from the sandbox!")
411
+ client.receive_response { |msg| puts msg }
412
+ client.disconnect
413
+ end.wait
414
+ ```
415
+
358
416
  ## Custom Tools (SDK MCP Servers)
359
417
 
360
418
  A **custom tool** is a Ruby proc/lambda that you can offer to Claude, for Claude to invoke as needed.
@@ -934,6 +992,39 @@ Each `SessionMessage` includes `type` (`"user"` or `"assistant"`), `uuid`, `sess
934
992
 
935
993
  > **Note:** Session browsing reads `~/.claude/projects/` JSONL files directly. It respects the `CLAUDE_CONFIG_DIR` environment variable and automatically detects git worktrees.
936
994
 
995
+ ## Session Mutations
996
+
997
+ Rename or tag sessions programmatically — no CLI subprocess required.
998
+
999
+ ### Renaming a Session
1000
+
1001
+ ```ruby
1002
+ # Rename a session (appends a custom-title JSONL entry)
1003
+ ClaudeAgentSDK.rename_session(
1004
+ session_id: '550e8400-e29b-41d4-a716-446655440000',
1005
+ title: 'My refactoring session',
1006
+ directory: '/path/to/project' # optional
1007
+ )
1008
+ ```
1009
+
1010
+ ### Tagging a Session
1011
+
1012
+ ```ruby
1013
+ # Tag a session (Unicode-sanitized before storing)
1014
+ ClaudeAgentSDK.tag_session(
1015
+ session_id: '550e8400-e29b-41d4-a716-446655440000',
1016
+ tag: 'experiment'
1017
+ )
1018
+
1019
+ # Clear a tag
1020
+ ClaudeAgentSDK.tag_session(
1021
+ session_id: '550e8400-e29b-41d4-a716-446655440000',
1022
+ tag: nil
1023
+ )
1024
+ ```
1025
+
1026
+ > **Note:** Session mutations use append-only JSONL writes with `O_WRONLY | O_APPEND` (no `O_CREAT`) for TOCTOU safety. They are safe to call while the session is open in a CLI process.
1027
+
937
1028
  ## Rails Integration
938
1029
 
939
1030
  The SDK integrates well with Rails applications. Here are common patterns:
@@ -1120,7 +1211,8 @@ class AssistantMessage
1120
1211
  attr_accessor :content, # Array<ContentBlock>
1121
1212
  :model, # String
1122
1213
  :parent_tool_use_id,# String | nil
1123
- :error # String | nil ('authentication_failed', 'billing_error', 'rate_limit', 'invalid_request', 'server_error', 'unknown')
1214
+ :error, # String | nil ('authentication_failed', 'billing_error', 'rate_limit', 'invalid_request', 'server_error', 'unknown')
1215
+ :usage # Hash | nil - Token usage info from the API response
1124
1216
  end
1125
1217
  ```
1126
1218
 
@@ -1276,7 +1368,7 @@ end
1276
1368
  | `HookMatcher` | Hook configuration with matcher pattern and timeout |
1277
1369
  | `PermissionResultAllow` | Permission callback result to allow tool use |
1278
1370
  | `PermissionResultDeny` | Permission callback result to deny tool use |
1279
- | `AgentDefinition` | Agent definition with description, prompt, tools, model |
1371
+ | `AgentDefinition` | Agent definition with description, prompt, tools, model, skills, memory, mcp_servers |
1280
1372
  | `ThinkingConfigAdaptive` | Adaptive thinking mode (32,000 token default budget) |
1281
1373
  | `ThinkingConfigEnabled` | Enabled thinking with explicit `budget_tokens` |
1282
1374
  | `ThinkingConfigDisabled` | Disabled thinking (0 tokens) |
@@ -1290,6 +1382,7 @@ end
1290
1382
  | `McpServerInfo` | MCP server name and version |
1291
1383
  | `McpToolInfo` | MCP tool name, description, and annotations |
1292
1384
  | `McpToolAnnotations` | MCP tool annotation hints (`read_only`, `destructive`, `open_world`) |
1385
+ | `TaskUsage` | Typed usage data (`total_tokens`, `tool_uses`, `duration_ms`) with `from_hash` factory |
1293
1386
  | `SDKSessionInfo` | Session metadata from `list_sessions` |
1294
1387
  | `SessionMessage` | Single message from `get_session_messages` |
1295
1388
  | `SandboxSettings` | Sandbox settings for isolated command execution |
@@ -61,7 +61,8 @@ module ClaudeAgentSDK
61
61
  content: content_blocks,
62
62
  model: data.dig(:message, :model),
63
63
  parent_tool_use_id: data[:parent_tool_use_id],
64
- error: data[:error] # authentication_failed, billing_error, rate_limit, invalid_request, server_error, unknown
64
+ error: data[:error], # authentication_failed, billing_error, rate_limit, invalid_request, server_error, unknown
65
+ usage: data.dig(:message, :usage)
65
66
  )
66
67
  end
67
68
 
@@ -89,7 +89,10 @@ module ClaudeAgentSDK
89
89
  description: agent_def.description,
90
90
  prompt: agent_def.prompt,
91
91
  tools: agent_def.tools,
92
- model: agent_def.model
92
+ model: agent_def.model,
93
+ skills: agent_def.skills,
94
+ memory: agent_def.memory,
95
+ mcpServers: agent_def.mcp_servers
93
96
  }.compact
94
97
  end
95
98
  end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative 'sessions'
5
+
6
+ module ClaudeAgentSDK
7
+ # Session mutation functions: rename and tag sessions.
8
+ #
9
+ # Ported from Python SDK's _internal/session_mutations.py.
10
+ # Appends typed metadata entries to the session's JSONL file,
11
+ # matching the CLI pattern. Safe to call from any SDK host process.
12
+ module SessionMutations
13
+ module_function
14
+
15
+ # Rename a session by appending a custom-title entry.
16
+ #
17
+ # list_sessions reads the LAST custom-title from the file tail, so
18
+ # repeated calls are safe — the most recent wins.
19
+ #
20
+ # @param session_id [String] UUID of the session to rename
21
+ # @param title [String] New session title (whitespace stripped)
22
+ # @param directory [String, nil] Project directory path
23
+ # @raise [ArgumentError] if session_id is invalid or title is empty
24
+ # @raise [Errno::ENOENT] if the session file cannot be found
25
+ def rename_session(session_id:, title:, directory: nil)
26
+ raise ArgumentError, "Invalid session_id: #{session_id}" unless session_id.match?(Sessions::UUID_RE)
27
+
28
+ stripped = title.strip
29
+ raise ArgumentError, 'title must be non-empty' if stripped.empty?
30
+
31
+ data = "#{JSON.generate({ type: 'custom-title', customTitle: stripped, sessionId: session_id },
32
+ space_size: 0)}\n"
33
+
34
+ append_to_session(session_id, data, directory)
35
+ end
36
+
37
+ # Tag a session. Pass nil to clear the tag.
38
+ #
39
+ # Appends a {type:'tag',tag:<tag>,sessionId:<id>} JSONL entry.
40
+ # Tags are Unicode-sanitized before storing.
41
+ #
42
+ # @param session_id [String] UUID of the session to tag
43
+ # @param tag [String, nil] Tag string, or nil to clear
44
+ # @param directory [String, nil] Project directory path
45
+ # @raise [ArgumentError] if session_id is invalid or tag is empty after sanitization
46
+ # @raise [Errno::ENOENT] if the session file cannot be found
47
+ def tag_session(session_id:, tag:, directory: nil)
48
+ raise ArgumentError, "Invalid session_id: #{session_id}" unless session_id.match?(Sessions::UUID_RE)
49
+
50
+ if tag
51
+ sanitized = sanitize_unicode(tag).strip
52
+ raise ArgumentError, 'tag must be non-empty (use nil to clear)' if sanitized.empty?
53
+
54
+ tag = sanitized
55
+ end
56
+
57
+ data = "#{JSON.generate({ type: 'tag', tag: tag || '', sessionId: session_id },
58
+ space_size: 0)}\n"
59
+
60
+ append_to_session(session_id, data, directory)
61
+ end
62
+
63
+ # -- Private helpers --
64
+
65
+ def append_to_session(session_id, data, directory)
66
+ file_name = "#{session_id}.jsonl"
67
+
68
+ if directory
69
+ append_to_session_in_directory(session_id, data, file_name, directory)
70
+ else
71
+ append_to_session_global(session_id, data, file_name)
72
+ end
73
+ end
74
+
75
+ def append_to_session_in_directory(session_id, data, file_name, directory)
76
+ path = File.realpath(directory).unicode_normalize(:nfc)
77
+
78
+ # Try the exact/prefix-matched project directory first.
79
+ project_dir = Sessions.find_project_dir(path)
80
+ return if project_dir && try_append(File.join(project_dir, file_name), data)
81
+
82
+ # Worktree fallback
83
+ begin
84
+ worktree_paths = Sessions.detect_worktrees(path)
85
+ rescue StandardError
86
+ worktree_paths = []
87
+ end
88
+
89
+ found = worktree_paths.any? do |wt_path|
90
+ next false if wt_path == path
91
+
92
+ wt_project_dir = Sessions.find_project_dir(wt_path)
93
+ wt_project_dir && try_append(File.join(wt_project_dir, file_name), data)
94
+ end
95
+ return if found
96
+
97
+ raise Errno::ENOENT, "Session #{session_id} not found in project directory for #{directory}"
98
+ end
99
+
100
+ def append_to_session_global(session_id, data, file_name)
101
+ projects_dir = File.join(Sessions.config_dir, 'projects')
102
+ raise Errno::ENOENT, "Session #{session_id} not found (no projects directory)" unless File.directory?(projects_dir)
103
+
104
+ found = Dir.children(projects_dir).any? do |child|
105
+ candidate = File.join(projects_dir, child, file_name)
106
+ try_append(candidate, data)
107
+ end
108
+ return if found
109
+
110
+ raise Errno::ENOENT, "Session #{session_id} not found in any project directory"
111
+ end
112
+
113
+ # Try appending to a path.
114
+ #
115
+ # Opens with WRONLY | APPEND (no CREAT) so the open fails with
116
+ # ENOENT if the file does not exist. Returns false for missing
117
+ # files or zero-byte files; true on successful write.
118
+ def try_append(path, data)
119
+ file = File.open(path, File::WRONLY | File::APPEND)
120
+ begin
121
+ return false if file.stat.size.zero? # rubocop:disable Style/ZeroLengthPredicate
122
+
123
+ file.write(data)
124
+ true
125
+ ensure
126
+ file.close
127
+ end
128
+ rescue Errno::ENOENT, Errno::ENOTDIR
129
+ false
130
+ end
131
+
132
+ # Unicode sanitization — ported from Python SDK / TS sanitization.ts
133
+ #
134
+ # Iteratively applies NFKC normalization and strips format/private-use/
135
+ # unassigned characters until stable (max 10 iterations).
136
+ UNICODE_STRIP_RE = /[\u200b-\u200f\u202a-\u202e\u2066-\u2069\ufeff\ue000-\uf8ff]/
137
+ FORMAT_CATEGORIES = %w[Cf Co Cn].freeze
138
+
139
+ def sanitize_unicode(value)
140
+ current = value
141
+ 10.times do
142
+ previous = current
143
+ current = current.unicode_normalize(:nfkc)
144
+ current = current.each_char.reject { |c| FORMAT_CATEGORIES.include?(unicode_category(c)) }.join
145
+ current = current.gsub(UNICODE_STRIP_RE, '')
146
+ break if current == previous
147
+ end
148
+ current
149
+ end
150
+
151
+ # Returns the Unicode general category for a character (e.g., 'Cf', 'Lu', 'Ll').
152
+ def unicode_category(char)
153
+ # Ruby doesn't have a built-in unicodedata.category(), but we can
154
+ # check the specific categories we care about using regex properties.
155
+ return 'Cf' if char.match?(/\p{Cf}/)
156
+ return 'Co' if char.match?(/\p{Co}/)
157
+ return 'Cn' if char.match?(/\p{Cn}/)
158
+
159
+ 'Other'
160
+ end
161
+
162
+ private_class_method :append_to_session, :append_to_session_in_directory,
163
+ :append_to_session_global, :try_append, :sanitize_unicode, :unicode_category
164
+ end
165
+ end
@@ -491,9 +491,12 @@ module ClaudeAgentSDK
491
491
  end
492
492
 
493
493
  private_class_method :list_sessions_for_directory, :list_all_sessions,
494
- :deduplicate_sessions, :detect_worktrees,
494
+ :deduplicate_sessions,
495
495
  :find_session_file, :parse_jsonl_entries,
496
496
  :build_conversation_chain, :walk_to_leaf, :walk_to_root,
497
497
  :filter_visible_messages, :read_head_tail, :build_session_info
498
+
499
+ # These remain accessible for SessionMutations:
500
+ # config_dir, sanitize_path, find_project_dir, detect_worktrees
498
501
  end
499
502
  end
@@ -181,9 +181,6 @@ module ClaudeAgentSDK
181
181
  process_env = ENV.to_h.merge('CLAUDECODE' => nil, 'CLAUDE_AGENT_SDK_VERSION' => VERSION).merge(custom_env)
182
182
  process_env['CLAUDE_CODE_ENTRYPOINT'] ||= 'sdk-rb'
183
183
  process_env['CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING'] = 'true' if @options.enable_file_checkpointing
184
- if @options.include_partial_messages
185
- process_env['CLAUDE_CODE_ENABLE_FINE_GRAINED_TOOL_STREAMING'] ||= '1'
186
- end
187
184
  process_env['PWD'] = @cwd.to_s if @cwd
188
185
 
189
186
  # Determine stderr handling
@@ -104,13 +104,14 @@ module ClaudeAgentSDK
104
104
 
105
105
  # Assistant message with content blocks
106
106
  class AssistantMessage
107
- attr_accessor :content, :model, :parent_tool_use_id, :error
107
+ attr_accessor :content, :model, :parent_tool_use_id, :error, :usage
108
108
 
109
- def initialize(content:, model:, parent_tool_use_id: nil, error: nil)
109
+ def initialize(content:, model:, parent_tool_use_id: nil, error: nil, usage: nil)
110
110
  @content = content
111
111
  @model = model
112
112
  @parent_tool_use_id = parent_tool_use_id
113
113
  @error = error # One of: authentication_failed, billing_error, rate_limit, invalid_request, server_error, unknown
114
+ @usage = usage # Token usage info from the API response
114
115
  end
115
116
  end
116
117
 
@@ -127,6 +128,27 @@ module ClaudeAgentSDK
127
128
  # Task lifecycle notification statuses
128
129
  TASK_NOTIFICATION_STATUSES = %w[completed failed stopped].freeze
129
130
 
131
+ # Typed usage data for task progress and notifications
132
+ class TaskUsage
133
+ attr_accessor :total_tokens, :tool_uses, :duration_ms
134
+
135
+ def initialize(total_tokens: 0, tool_uses: 0, duration_ms: 0)
136
+ @total_tokens = total_tokens
137
+ @tool_uses = tool_uses
138
+ @duration_ms = duration_ms
139
+ end
140
+
141
+ def self.from_hash(hash)
142
+ return nil unless hash.is_a?(Hash)
143
+
144
+ new(
145
+ total_tokens: hash[:total_tokens] || hash['total_tokens'] || hash[:totalTokens] || hash['totalTokens'] || 0,
146
+ tool_uses: hash[:tool_uses] || hash['tool_uses'] || hash[:toolUses] || hash['toolUses'] || 0,
147
+ duration_ms: hash[:duration_ms] || hash['duration_ms'] || hash[:durationMs] || hash['durationMs'] || 0
148
+ )
149
+ end
150
+ end
151
+
130
152
  # Task started system message (subagent/background task started)
131
153
  class TaskStartedMessage < SystemMessage
132
154
  attr_accessor :task_id, :description, :uuid, :session_id, :tool_use_id, :task_type
@@ -285,13 +307,16 @@ module ClaudeAgentSDK
285
307
 
286
308
  # Agent definition configuration
287
309
  class AgentDefinition
288
- attr_accessor :description, :prompt, :tools, :model
310
+ attr_accessor :description, :prompt, :tools, :model, :skills, :memory, :mcp_servers
289
311
 
290
- def initialize(description:, prompt:, tools: nil, model: nil)
312
+ def initialize(description:, prompt:, tools: nil, model: nil, skills: nil, memory: nil, mcp_servers: nil)
291
313
  @description = description
292
314
  @prompt = prompt
293
315
  @tools = tools
294
316
  @model = model
317
+ @skills = skills # Array of skill names
318
+ @memory = memory # One of: 'user', 'project', 'local'
319
+ @mcp_servers = mcp_servers # Array of server names or config hashes
295
320
  end
296
321
  end
297
322
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeAgentSDK
4
- VERSION = '0.9.0'
4
+ VERSION = '0.11.0'
5
5
  end
@@ -11,6 +11,7 @@ require_relative 'claude_agent_sdk/query'
11
11
  require_relative 'claude_agent_sdk/sdk_mcp_server'
12
12
  require_relative 'claude_agent_sdk/streaming'
13
13
  require_relative 'claude_agent_sdk/sessions'
14
+ require_relative 'claude_agent_sdk/session_mutations'
14
15
  require 'async'
15
16
  require 'securerandom'
16
17
 
@@ -78,6 +79,22 @@ module ClaudeAgentSDK
78
79
  Sessions.get_session_messages(session_id: session_id, directory: directory, limit: limit, offset: offset)
79
80
  end
80
81
 
82
+ # Rename a session by appending a custom-title entry
83
+ # @param session_id [String] UUID of the session to rename
84
+ # @param title [String] New session title
85
+ # @param directory [String, nil] Project directory path
86
+ def self.rename_session(session_id:, title:, directory: nil)
87
+ SessionMutations.rename_session(session_id: session_id, title: title, directory: directory)
88
+ end
89
+
90
+ # Tag a session. Pass nil to clear the tag.
91
+ # @param session_id [String] UUID of the session to tag
92
+ # @param tag [String, nil] Tag string, or nil to clear
93
+ # @param directory [String, nil] Project directory path
94
+ def self.tag_session(session_id:, tag:, directory: nil)
95
+ SessionMutations.tag_session(session_id: session_id, tag: tag, directory: directory)
96
+ end
97
+
81
98
  def self.query(prompt:, options: nil, &block)
82
99
  return enum_for(:query, prompt: prompt, options: options) unless block
83
100
 
@@ -222,8 +239,14 @@ module ClaudeAgentSDK
222
239
  class Client
223
240
  attr_reader :query_handler
224
241
 
225
- def initialize(options: nil)
242
+ # @param options [ClaudeAgentOptions, nil] Configuration options
243
+ # @param transport_class [Class] Transport class to use (must implement Transport interface).
244
+ # Defaults to SubprocessCLITransport.
245
+ # @param transport_args [Hash] Additional keyword arguments passed to transport_class.new(options, **transport_args)
246
+ def initialize(options: nil, transport_class: SubprocessCLITransport, transport_args: {})
226
247
  @options = options || ClaudeAgentOptions.new
248
+ @transport_class = transport_class
249
+ @transport_args = transport_args
227
250
  @transport = nil
228
251
  @query_handler = nil
229
252
  @connected = false
@@ -257,7 +280,7 @@ module ClaudeAgentSDK
257
280
  )
258
281
 
259
282
  # Client always uses streaming mode; keep stdin open for bidirectional communication.
260
- @transport = SubprocessCLITransport.new(configured_options)
283
+ @transport = @transport_class.new(configured_options, **@transport_args)
261
284
  @transport.connect
262
285
 
263
286
  # Extract SDK MCP servers
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: claude-agent-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Community Contributors
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-03-12 00:00:00.000000000 Z
10
+ date: 2026-03-20 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: async
@@ -109,6 +109,7 @@ files:
109
109
  - lib/claude_agent_sdk/message_parser.rb
110
110
  - lib/claude_agent_sdk/query.rb
111
111
  - lib/claude_agent_sdk/sdk_mcp_server.rb
112
+ - lib/claude_agent_sdk/session_mutations.rb
112
113
  - lib/claude_agent_sdk/sessions.rb
113
114
  - lib/claude_agent_sdk/streaming.rb
114
115
  - lib/claude_agent_sdk/subprocess_cli_transport.rb