claude-agent-sdk 0.13.1 → 0.14.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: 4c6c249367872221d23d090bf162658963a287f88abb9f62a5037e1b85977949
4
- data.tar.gz: a09ae1f2ecd82344afa972c3e8c74d9032ae85b5084bf96d72b0d834d45fa839
3
+ metadata.gz: 49a855001019149f2348998742683b3589966a6b97464e815b1fd44abb01ab51
4
+ data.tar.gz: af1826c40f4e8cb6c1910b6ca81b2fa3e01dc83442b4373f5e912b0a1d4af871
5
5
  SHA512:
6
- metadata.gz: c608d05dfe74cbd90ec64836f7c6b3808357db831debbd268e35ef30699867c7f5409ecb101c01dbf899229a14f17bea10ad8d8ed0f359778ae04ad6b87a2c3f
7
- data.tar.gz: 2f1bf0d44b541104d7fba0433b32260c8cda247d79f7f36f46d1aedfed93afea6fb55bbc26657210686602703ea0727e27d7056670f5eda66f23886164f28033
6
+ metadata.gz: b5322fb7d911240d064985742c6817d8f9ccb0918c30ec7b27aca012c657ee0b5c1a648f6714cc471ca32b15acf8d7f72d6bf2f938d8b4811d9a0ba497a3f662
7
+ data.tar.gz: 3ea02aa3ef4da8a98e876c18605f2bffcd5edca9e6ca719bfa70de925463030e8cb1793916bf1681f7fcb432f044398dfdbb5480fb295a11673736977c772347
data/CHANGELOG.md CHANGED
@@ -5,6 +5,35 @@ 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.14.0] - 2026-04-08 — Python SDK v0.1.51–0.1.56 Parity
9
+
10
+ ### Added
11
+
12
+ #### Type Completeness
13
+ - `AssistantMessage`: `message_id`, `stop_reason`, `session_id`, `uuid` fields (populated from CLI message data)
14
+ - `AgentDefinition`: `disallowed_tools`, `max_turns`, `initial_prompt`, `background`, `effort`, `permission_mode` fields (serialized to CLI via initialize request)
15
+ - `ToolPermissionContext`: `tool_use_id`, `agent_id` fields for distinguishing parallel permission requests and sub-agent context
16
+ - `PERMISSION_MODES`: added `dontAsk` and `auto` values
17
+
18
+ #### New Types and Options
19
+ - `SystemPromptFile` class — loads system prompt from a file path via `--system-prompt-file` CLI flag
20
+ - `TaskBudget` class — API-side token budget, passed as `--task-budget` CLI flag
21
+ - `ForkSessionResult` class — returned by `fork_session()` with the new session ID
22
+ - `session_id` option on `ClaudeAgentOptions` — specify a custom session ID via `--session-id` CLI flag
23
+ - `task_budget` option on `ClaudeAgentOptions`
24
+
25
+ #### Session Management
26
+ - `ClaudeAgentSDK.delete_session(session_id:, directory:)` — hard-deletes a session JSONL file
27
+ - `ClaudeAgentSDK.fork_session(session_id:, directory:, up_to_message_id:, title:)` — filesystem-level fork with UUID remapping, sidechain filtering, content-replacement forwarding, and auto-generated titles
28
+ - `offset` parameter on `ClaudeAgentSDK.list_sessions` for cursor-based pagination
29
+
30
+ #### Client Introspection
31
+ - `Client#get_context_usage` / `Query#get_context_usage` — sends `get_context_usage` control request for context window breakdown (tokens by category, model, MCP tools, memory files, etc.)
32
+
33
+ #### MCP Robustness
34
+ - `SdkMcpTool#meta` field and `_meta` forwarding in `tools/list` responses — prevents silent truncation of large tool results (>50K chars) by forwarding `anthropic/maxResultSizeChars` through the MCP `_meta` field
35
+ - `create_tool` auto-populates `_meta` from `annotations[:maxResultSizeChars]` when present
36
+
8
37
  ## [0.13.1] - 2026-04-05
9
38
 
10
39
  ### Fixed
data/README.md CHANGED
@@ -106,7 +106,7 @@ Add this line to your application's Gemfile:
106
106
  gem 'claude-agent-sdk', github: 'ya-luotao/claude-agent-sdk-ruby'
107
107
 
108
108
  # Or use a stable version from RubyGems
109
- gem 'claude-agent-sdk', '~> 0.13.0'
109
+ gem 'claude-agent-sdk', '~> 0.13.1'
110
110
  ```
111
111
 
112
112
  And then execute:
@@ -130,6 +130,20 @@ gem install claude-agent-sdk
130
130
 
131
131
  If you're using [Claude Code](https://claude.ai/claude-code) or another agentic coding tool that supports [skills](https://skills.sh), you can install the SDK skill:
132
132
 
133
+ **Option 1: Via Plugin Marketplace (recommended)**
134
+
135
+ This repo is a Claude Code plugin marketplace. Add it once, then install the skill:
136
+
137
+ ```bash
138
+ # Add the marketplace
139
+ /plugin marketplace add ya-luotao/claude-agent-sdk-ruby
140
+
141
+ # Install the plugin
142
+ /plugin install claude-agent-ruby@claude-agent-sdk-ruby
143
+ ```
144
+
145
+ **Option 2: Via skills.sh**
146
+
133
147
  ```bash
134
148
  npx skills add https://github.com/ya-luotao/claude-agent-sdk-ruby --skill claude-agent-sdk-ruby
135
149
  ```
@@ -949,6 +963,9 @@ end
949
963
  # List sessions for a specific directory
950
964
  sessions = ClaudeAgentSDK.list_sessions(directory: '/path/to/project', limit: 10)
951
965
 
966
+ # Paginate with offset
967
+ page2 = ClaudeAgentSDK.list_sessions(directory: '.', limit: 10, offset: 10)
968
+
952
969
  # Include git worktree sessions
953
970
  sessions = ClaudeAgentSDK.list_sessions(directory: '.', include_worktrees: true)
954
971
  ```
@@ -1005,7 +1022,34 @@ ClaudeAgentSDK.tag_session(
1005
1022
  )
1006
1023
  ```
1007
1024
 
1008
- > **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.
1025
+ ### Deleting a Session
1026
+
1027
+ ```ruby
1028
+ # Hard-delete a session (removes the JSONL file permanently)
1029
+ ClaudeAgentSDK.delete_session(
1030
+ session_id: '550e8400-e29b-41d4-a716-446655440000',
1031
+ directory: '/path/to/project' # optional
1032
+ )
1033
+ ```
1034
+
1035
+ ### Forking a Session
1036
+
1037
+ ```ruby
1038
+ # Fork a session into a new branch with fresh UUIDs
1039
+ result = ClaudeAgentSDK.fork_session(
1040
+ session_id: '550e8400-e29b-41d4-a716-446655440000',
1041
+ title: 'Experiment branch' # optional, auto-generated if omitted
1042
+ )
1043
+ puts result.session_id # UUID of the new forked session
1044
+
1045
+ # Fork up to a specific message (partial fork)
1046
+ result = ClaudeAgentSDK.fork_session(
1047
+ session_id: '550e8400-e29b-41d4-a716-446655440000',
1048
+ up_to_message_id: 'message-uuid-here'
1049
+ )
1050
+ ```
1051
+
1052
+ > **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. `fork_session` uses `O_CREAT | O_EXCL` to prevent race conditions.
1009
1053
 
1010
1054
  ## Observability (OpenTelemetry / Langfuse)
1011
1055
 
@@ -70,7 +70,11 @@ module ClaudeAgentSDK
70
70
  model: data.dig(:message, :model),
71
71
  parent_tool_use_id: data[:parent_tool_use_id],
72
72
  error: data[:error], # authentication_failed, billing_error, rate_limit, invalid_request, server_error, unknown
73
- usage: data.dig(:message, :usage)
73
+ usage: data.dig(:message, :usage),
74
+ message_id: data.dig(:message, :id),
75
+ stop_reason: data.dig(:message, :stop_reason),
76
+ session_id: data[:session_id],
77
+ uuid: data[:uuid]
74
78
  )
75
79
  end
76
80
 
@@ -89,10 +89,16 @@ module ClaudeAgentSDK
89
89
  description: agent_def.description,
90
90
  prompt: agent_def.prompt,
91
91
  tools: agent_def.tools,
92
+ disallowedTools: agent_def.disallowed_tools,
92
93
  model: agent_def.model,
93
94
  skills: agent_def.skills,
94
95
  memory: agent_def.memory,
95
- mcpServers: agent_def.mcp_servers
96
+ mcpServers: agent_def.mcp_servers,
97
+ initialPrompt: agent_def.initial_prompt,
98
+ maxTurns: agent_def.max_turns,
99
+ background: agent_def.background,
100
+ effort: agent_def.effort,
101
+ permissionMode: agent_def.permission_mode
96
102
  }.compact
97
103
  end
98
104
  end
@@ -283,7 +289,9 @@ module ClaudeAgentSDK
283
289
 
284
290
  context = ToolPermissionContext.new(
285
291
  signal: nil,
286
- suggestions: request_data[:permission_suggestions] || []
292
+ suggestions: request_data[:permission_suggestions] || [],
293
+ tool_use_id: request_data[:tool_use_id],
294
+ agent_id: request_data[:agent_id]
287
295
  )
288
296
 
289
297
  response = @can_use_tool.call(
@@ -805,6 +813,12 @@ module ClaudeAgentSDK
805
813
 
806
814
  public
807
815
 
816
+ # Get a breakdown of current context window usage by category.
817
+ # @return [Hash] Context usage response with categories, totalTokens, maxTokens, etc.
818
+ def get_context_usage
819
+ send_control_request({ subtype: 'get_context_usage' })
820
+ end
821
+
808
822
  # Get current MCP server connection status (only works with streaming mode)
809
823
  # @return [Hash] MCP status information, including mcpServers list
810
824
  def get_mcp_status
@@ -66,6 +66,7 @@ module ClaudeAgentSDK
66
66
  inputSchema: convert_input_schema(tool.input_schema)
67
67
  }
68
68
  entry[:annotations] = tool.annotations if tool.annotations
69
+ entry[:_meta] = tool.meta if tool.meta
69
70
  entry
70
71
  end
71
72
  end
@@ -400,15 +401,23 @@ module ClaudeAgentSDK
400
401
  # { content: [{ type: 'text', text: "Result: #{result}" }] }
401
402
  # end
402
403
  # end
403
- def self.create_tool(name, description, input_schema, annotations: nil, &handler)
404
+ def self.create_tool(name, description, input_schema, annotations: nil, meta: nil, &handler)
404
405
  raise ArgumentError, 'Block required for tool handler' unless handler
405
406
 
407
+ # Auto-populate _meta with maxResultSizeChars from annotations if present
408
+ resolved_meta = meta
409
+ if resolved_meta.nil? && annotations
410
+ max_chars = annotations[:maxResultSizeChars] || annotations['maxResultSizeChars']
411
+ resolved_meta = { 'anthropic/maxResultSizeChars' => max_chars } if max_chars
412
+ end
413
+
406
414
  SdkMcpTool.new(
407
415
  name: name,
408
416
  description: description,
409
417
  input_schema: input_schema,
410
418
  handler: handler,
411
- annotations: annotations
419
+ annotations: annotations,
420
+ meta: resolved_meta
412
421
  )
413
422
  end
414
423
 
@@ -1,15 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
+ require 'securerandom'
4
5
  require_relative 'sessions'
5
6
 
6
7
  module ClaudeAgentSDK
7
- # Session mutation functions: rename and tag sessions.
8
+ # Session mutation functions: rename, tag, delete, and fork sessions.
8
9
  #
9
10
  # Ported from Python SDK's _internal/session_mutations.py.
10
11
  # Appends typed metadata entries to the session's JSONL file,
11
12
  # matching the CLI pattern. Safe to call from any SDK host process.
12
- module SessionMutations
13
+ module SessionMutations # rubocop:disable Metrics/ModuleLength
13
14
  module_function
14
15
 
15
16
  # Rename a session by appending a custom-title entry.
@@ -60,8 +61,245 @@ module ClaudeAgentSDK
60
61
  append_to_session(session_id, data, directory)
61
62
  end
62
63
 
64
+ # Delete a session by removing its JSONL file.
65
+ #
66
+ # This is a hard delete — the file is removed permanently. For soft-delete
67
+ # semantics, use tag_session(id, '__hidden') and filter on listing instead.
68
+ #
69
+ # @param session_id [String] UUID of the session to delete
70
+ # @param directory [String, nil] Project directory path
71
+ # @raise [ArgumentError] if session_id is invalid
72
+ # @raise [Errno::ENOENT] if the session file cannot be found
73
+ def delete_session(session_id:, directory: nil)
74
+ raise ArgumentError, "Invalid session_id: #{session_id}" unless session_id.match?(Sessions::UUID_RE)
75
+
76
+ result = find_session_file_with_dir(session_id, directory)
77
+ raise Errno::ENOENT, "Session #{session_id} not found#{" in project directory for #{directory}" if directory}" unless result
78
+
79
+ path = result[0]
80
+
81
+ begin
82
+ File.delete(path)
83
+ rescue Errno::ENOENT
84
+ raise Errno::ENOENT, "Session #{session_id} not found"
85
+ end
86
+ end
87
+
88
+ # Fork a session into a new branch with fresh UUIDs.
89
+ #
90
+ # Creates a copy of the session transcript (or a prefix up to up_to_message_id)
91
+ # with remapped UUIDs and a new session ID. Sidechains are filtered out,
92
+ # progress entries are excluded from the written output but used for
93
+ # parentUuid chain walking.
94
+ #
95
+ # @param session_id [String] UUID of the session to fork
96
+ # @param directory [String, nil] Project directory path
97
+ # @param up_to_message_id [String, nil] Truncate the fork at this message UUID
98
+ # @param title [String, nil] Custom title for the fork (auto-generated if omitted)
99
+ # @return [ForkSessionResult] Result containing the new session ID
100
+ # @raise [ArgumentError] if session_id or up_to_message_id is invalid
101
+ # @raise [Errno::ENOENT] if the session file cannot be found
102
+ def fork_session(session_id:, directory: nil, up_to_message_id: nil, title: nil) # rubocop:disable Metrics/MethodLength
103
+ raise ArgumentError, "Invalid session_id: #{session_id}" unless session_id.match?(Sessions::UUID_RE)
104
+
105
+ raise ArgumentError, "Invalid up_to_message_id: #{up_to_message_id}" if up_to_message_id && !up_to_message_id.match?(Sessions::UUID_RE)
106
+
107
+ result = find_session_file_with_dir(session_id, directory)
108
+ raise Errno::ENOENT, "Session #{session_id} not found#{" in project directory for #{directory}" if directory}" unless result
109
+
110
+ file_path, project_dir = result
111
+ content = File.read(file_path)
112
+ raise ArgumentError, "Session #{session_id} has no messages to fork" if content.empty?
113
+
114
+ transcript, content_replacements = parse_fork_transcript(content, session_id)
115
+ transcript.reject! { |e| e['isSidechain'] }
116
+ raise ArgumentError, "Session #{session_id} has no messages to fork" if transcript.empty?
117
+
118
+ if up_to_message_id
119
+ cutoff = transcript.index { |e| e['uuid'] == up_to_message_id }
120
+ raise ArgumentError, "Message #{up_to_message_id} not found in session #{session_id}" unless cutoff
121
+
122
+ transcript = transcript[0..cutoff]
123
+ end
124
+
125
+ # Build UUID mapping (including progress entries for parentUuid chain walk)
126
+ uuid_mapping = {}
127
+ transcript.each { |e| uuid_mapping[e['uuid']] = SecureRandom.uuid }
128
+
129
+ by_uuid = transcript.to_h { |e| [e['uuid'], e] }
130
+
131
+ # Filter out progress messages from written output
132
+ writable = transcript.reject { |e| e['type'] == 'progress' }
133
+ raise ArgumentError, "Session #{session_id} has no messages to fork" if writable.empty?
134
+
135
+ forked_session_id = SecureRandom.uuid
136
+ now = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S.%3NZ')
137
+
138
+ lines = writable.each_with_index.map do |original, i|
139
+ build_forked_entry(original, i, writable.size, uuid_mapping, by_uuid,
140
+ forked_session_id, session_id, now)
141
+ end
142
+
143
+ # Append content-replacement entry if any
144
+ if content_replacements && !content_replacements.empty?
145
+ lines << JSON.generate({
146
+ 'type' => 'content-replacement',
147
+ 'sessionId' => forked_session_id,
148
+ 'replacements' => content_replacements
149
+ })
150
+ end
151
+
152
+ # Derive title
153
+ fork_title = title&.strip
154
+ if fork_title.nil? || fork_title.empty?
155
+ head = content[0, Sessions::LITE_READ_BUF_SIZE] || ''
156
+ tail = content.length > Sessions::LITE_READ_BUF_SIZE ? content[-Sessions::LITE_READ_BUF_SIZE..] : head
157
+ base = Sessions.extract_json_string_field(tail, 'customTitle', last: true) ||
158
+ Sessions.extract_json_string_field(head, 'customTitle', last: true) ||
159
+ Sessions.extract_json_string_field(tail, 'aiTitle', last: true) ||
160
+ Sessions.extract_json_string_field(head, 'aiTitle', last: true) ||
161
+ Sessions.extract_first_prompt_from_head(head) ||
162
+ 'Forked session'
163
+ fork_title = "#{base} (fork)"
164
+ end
165
+
166
+ lines << JSON.generate({
167
+ 'type' => 'custom-title',
168
+ 'sessionId' => forked_session_id,
169
+ 'customTitle' => fork_title
170
+ })
171
+
172
+ fork_path = File.join(project_dir, "#{forked_session_id}.jsonl")
173
+ io = nil
174
+ fd = IO.sysopen(fork_path, File::WRONLY | File::CREAT | File::EXCL, 0o600)
175
+ begin
176
+ io = IO.new(fd)
177
+ io.write("#{lines.join("\n")}\n")
178
+ ensure
179
+ if io
180
+ io.close
181
+ else
182
+ IO.for_fd(fd).close rescue nil # rubocop:disable Style/RescueModifier
183
+ end
184
+ end
185
+
186
+ ForkSessionResult.new(session_id: forked_session_id)
187
+ end
188
+
63
189
  # -- Private helpers --
64
190
 
191
+ # Locate the JSONL file for a session and return [file_path, project_dir].
192
+ def find_session_file_with_dir(session_id, directory)
193
+ file_name = "#{session_id}.jsonl"
194
+ return find_in_directory(file_name, directory) if directory
195
+
196
+ find_in_all_projects(file_name)
197
+ end
198
+
199
+ def find_in_directory(file_name, directory)
200
+ path = File.realpath(directory).unicode_normalize(:nfc)
201
+ result = try_project_dir(file_name, Sessions.find_project_dir(path))
202
+ return result if result
203
+
204
+ worktree_paths = begin
205
+ Sessions.detect_worktrees(path)
206
+ rescue Errno::ENOENT, Errno::EACCES
207
+ []
208
+ end
209
+ worktree_paths.each do |wt_path|
210
+ next if wt_path == path
211
+
212
+ result = try_project_dir(file_name, Sessions.find_project_dir(wt_path))
213
+ return result if result
214
+ end
215
+ nil
216
+ end
217
+
218
+ def try_project_dir(file_name, project_dir)
219
+ return nil unless project_dir
220
+
221
+ candidate = File.join(project_dir, file_name)
222
+ File.exist?(candidate) ? [candidate, project_dir] : nil
223
+ end
224
+
225
+ def find_in_all_projects(file_name)
226
+ projects_dir = File.join(Sessions.config_dir, 'projects')
227
+ return nil unless File.directory?(projects_dir)
228
+
229
+ Dir.children(projects_dir).each do |child|
230
+ pd = File.join(projects_dir, child)
231
+ next unless File.directory?(pd)
232
+
233
+ candidate = File.join(pd, file_name)
234
+ return [candidate, pd] if File.exist?(candidate)
235
+ end
236
+ nil
237
+ end
238
+
239
+ # Parse a fork transcript, extracting entries and content-replacement data.
240
+ def parse_fork_transcript(content, _session_id)
241
+ transcript = []
242
+ content_replacements = nil
243
+
244
+ content.each_line do |line|
245
+ entry = JSON.parse(line.strip)
246
+ next unless entry.is_a?(Hash) && entry['uuid']
247
+
248
+ if entry['type'] == 'content-replacement'
249
+ content_replacements = entry['replacements']
250
+ next
251
+ end
252
+ transcript << entry
253
+ rescue JSON::ParserError
254
+ next
255
+ end
256
+
257
+ [transcript, content_replacements]
258
+ end
259
+
260
+ # Build a single forked entry with remapped UUIDs.
261
+ def build_forked_entry(original, index, total, uuid_mapping, by_uuid,
262
+ forked_session_id, source_session_id, now)
263
+ new_uuid = uuid_mapping[original['uuid']]
264
+
265
+ # Resolve parentUuid, skipping progress ancestors
266
+ new_parent_uuid = resolve_parent_uuid(original['parentUuid'], by_uuid, uuid_mapping)
267
+
268
+ # Only update timestamp on the last message
269
+ timestamp = index == total - 1 ? now : (original['timestamp'] || now)
270
+
271
+ # Remap logicalParentUuid — unlike parentUuid (which walks the chain and nils on miss),
272
+ # logicalParentUuid preserves the original UUID when unmapped because it may reference
273
+ # a message outside the forked range (e.g., a prior conversation branch).
274
+ logical_parent = original['logicalParentUuid']
275
+ new_logical_parent = logical_parent ? (uuid_mapping[logical_parent] || logical_parent) : logical_parent
276
+
277
+ forked = original.merge(
278
+ 'uuid' => new_uuid,
279
+ 'parentUuid' => new_parent_uuid,
280
+ 'logicalParentUuid' => new_logical_parent,
281
+ 'sessionId' => forked_session_id,
282
+ 'timestamp' => timestamp,
283
+ 'isSidechain' => false,
284
+ 'forkedFrom' => { 'sessionId' => source_session_id, 'messageUuid' => original['uuid'] }
285
+ )
286
+ %w[teamName agentName slug sourceToolAssistantUUID].each { |k| forked.delete(k) }
287
+
288
+ JSON.generate(forked)
289
+ end
290
+
291
+ # Walk up parentUuid chain skipping progress entries.
292
+ def resolve_parent_uuid(parent_id, by_uuid, uuid_mapping)
293
+ while parent_id
294
+ parent = by_uuid[parent_id]
295
+ break unless parent
296
+ return uuid_mapping[parent_id] if parent['type'] != 'progress'
297
+
298
+ parent_id = parent['parentUuid']
299
+ end
300
+ nil
301
+ end
302
+
65
303
  def append_to_session(session_id, data, directory)
66
304
  file_name = "#{session_id}.jsonl"
67
305
 
@@ -156,7 +394,10 @@ module ClaudeAgentSDK
156
394
  'Other'
157
395
  end
158
396
 
159
- private_class_method :append_to_session, :append_to_session_in_directory,
397
+ private_class_method :find_session_file_with_dir,
398
+ :find_in_directory, :try_project_dir, :find_in_all_projects,
399
+ :parse_fork_transcript, :build_forked_entry, :resolve_parent_uuid,
400
+ :append_to_session, :append_to_session_in_directory,
160
401
  :append_to_session_global, :try_append, :sanitize_unicode, :unicode_category
161
402
  end
162
403
  end
@@ -302,17 +302,19 @@ module ClaudeAgentSDK
302
302
  # List sessions for a directory (or all sessions)
303
303
  # @param directory [String, nil] Working directory to list sessions for
304
304
  # @param limit [Integer, nil] Maximum number of sessions to return
305
+ # @param offset [Integer] Number of sessions to skip (for pagination)
305
306
  # @param include_worktrees [Boolean] Whether to include git worktree sessions
306
307
  # @return [Array<SDKSessionInfo>] Sessions sorted by last_modified descending
307
- def list_sessions(directory: nil, limit: nil, include_worktrees: true)
308
+ def list_sessions(directory: nil, limit: nil, offset: 0, include_worktrees: true)
308
309
  sessions = if directory
309
310
  list_sessions_for_directory(directory, include_worktrees)
310
311
  else
311
312
  list_all_sessions
312
313
  end
313
314
 
314
- # Sort by last_modified descending
315
+ # Sort by last_modified descending, then apply offset and limit
315
316
  sessions.sort_by! { |s| -s.last_modified }
317
+ sessions = sessions[offset..] || [] if offset.positive?
316
318
  sessions = sessions.first(limit) if limit
317
319
  sessions
318
320
  end
@@ -74,13 +74,18 @@ module ClaudeAgentSDK
74
74
  cmd.concat(['--system-prompt', ''])
75
75
  elsif @options.system_prompt.is_a?(String)
76
76
  cmd.concat(['--system-prompt', @options.system_prompt])
77
+ elsif @options.system_prompt.is_a?(SystemPromptFile)
78
+ cmd.concat(['--system-prompt-file', @options.system_prompt.path])
77
79
  elsif @options.system_prompt.is_a?(SystemPromptPreset)
78
80
  # Preset activates the default Claude Code system prompt by not passing --system-prompt ""
79
81
  # Only --append-system-prompt is passed if append text is provided
80
82
  cmd.concat(['--append-system-prompt', @options.system_prompt.append]) if @options.system_prompt.append
81
83
  elsif @options.system_prompt.is_a?(Hash)
82
84
  prompt_type = @options.system_prompt[:type] || @options.system_prompt['type']
83
- if prompt_type == 'preset'
85
+ if prompt_type == 'file'
86
+ prompt_path = @options.system_prompt[:path] || @options.system_prompt['path']
87
+ cmd.concat(['--system-prompt-file', prompt_path]) if prompt_path
88
+ elsif prompt_type == 'preset'
84
89
  append = @options.system_prompt[:append] || @options.system_prompt['append']
85
90
  # Preset activates the default Claude Code system prompt by not passing --system-prompt ""
86
91
  cmd.concat(['--append-system-prompt', append]) if append
@@ -96,6 +101,7 @@ module ClaudeAgentSDK
96
101
  cmd.concat(['--permission-mode', @options.permission_mode]) if @options.permission_mode
97
102
  cmd << '--continue' if @options.continue_conversation
98
103
  cmd.concat(['--resume', @options.resume]) if @options.resume
104
+ cmd.concat(['--session-id', @options.session_id]) if @options.session_id
99
105
 
100
106
  # Settings handling with sandbox merge
101
107
  build_settings_args(cmd)
@@ -103,6 +109,16 @@ module ClaudeAgentSDK
103
109
  # Budget limit option
104
110
  cmd.concat(['--max-budget-usd', @options.max_budget_usd.to_s]) if @options.max_budget_usd
105
111
 
112
+ # Task budget (API-side token budget)
113
+ if @options.task_budget
114
+ total = if @options.task_budget.is_a?(TaskBudget)
115
+ @options.task_budget.total
116
+ else
117
+ @options.task_budget[:total] || @options.task_budget['total']
118
+ end
119
+ cmd.concat(['--task-budget', total.to_s]) if total
120
+ end
121
+
106
122
  # Thinking configuration (takes precedence over deprecated max_thinking_tokens)
107
123
  thinking_tokens = resolve_thinking_tokens
108
124
  cmd.concat(['--max-thinking-tokens', thinking_tokens.to_s]) unless thinking_tokens.nil?
@@ -2,7 +2,7 @@
2
2
 
3
3
  module ClaudeAgentSDK
4
4
  # Type constants for permission modes
5
- PERMISSION_MODES = %w[default acceptEdits plan bypassPermissions].freeze
5
+ PERMISSION_MODES = %w[default acceptEdits plan bypassPermissions dontAsk auto].freeze
6
6
 
7
7
  # Type constants for setting sources
8
8
  SETTING_SOURCES = %w[user project local].freeze
@@ -121,14 +121,20 @@ module ClaudeAgentSDK
121
121
 
122
122
  # Assistant message with content blocks
123
123
  class AssistantMessage
124
- attr_accessor :content, :model, :parent_tool_use_id, :error, :usage
124
+ attr_accessor :content, :model, :parent_tool_use_id, :error, :usage,
125
+ :message_id, :stop_reason, :session_id, :uuid
125
126
 
126
- def initialize(content:, model:, parent_tool_use_id: nil, error: nil, usage: nil)
127
+ def initialize(content:, model:, parent_tool_use_id: nil, error: nil, usage: nil,
128
+ message_id: nil, stop_reason: nil, session_id: nil, uuid: nil)
127
129
  @content = content
128
130
  @model = model
129
131
  @parent_tool_use_id = parent_tool_use_id
130
132
  @error = error # One of: authentication_failed, billing_error, rate_limit, invalid_request, server_error, unknown
131
133
  @usage = usage # Token usage info from the API response
134
+ @message_id = message_id # Unique message identifier from the API (message.id)
135
+ @stop_reason = stop_reason # Why the assistant stopped (e.g., "end_turn", "max_tokens")
136
+ @session_id = session_id # Session the message belongs to
137
+ @uuid = uuid # Unique message UUID in the session transcript
132
138
  end
133
139
  end
134
140
 
@@ -597,16 +603,25 @@ module ClaudeAgentSDK
597
603
 
598
604
  # Agent definition configuration
599
605
  class AgentDefinition
600
- attr_accessor :description, :prompt, :tools, :model, :skills, :memory, :mcp_servers
606
+ attr_accessor :description, :prompt, :tools, :disallowed_tools, :model, :skills, :memory, :mcp_servers,
607
+ :initial_prompt, :max_turns, :background, :effort, :permission_mode
601
608
 
602
- def initialize(description:, prompt:, tools: nil, model: nil, skills: nil, memory: nil, mcp_servers: nil)
609
+ def initialize(description:, prompt:, tools: nil, disallowed_tools: nil, model: nil, skills: nil,
610
+ memory: nil, mcp_servers: nil, initial_prompt: nil, max_turns: nil,
611
+ background: nil, effort: nil, permission_mode: nil)
603
612
  @description = description
604
613
  @prompt = prompt
605
614
  @tools = tools
615
+ @disallowed_tools = disallowed_tools # Array of tool names to disallow
606
616
  @model = model
607
617
  @skills = skills # Array of skill names
608
618
  @memory = memory # One of: 'user', 'project', 'local'
609
619
  @mcp_servers = mcp_servers # Array of server names or config hashes
620
+ @initial_prompt = initial_prompt # Initial prompt sent when agent starts
621
+ @max_turns = max_turns # Maximum conversation turns for the agent
622
+ @background = background # Whether this agent runs in background
623
+ @effort = effort # "low", "medium", "high", "max", or Integer
624
+ @permission_mode = permission_mode # Permission mode for the agent
610
625
  end
611
626
  end
612
627
 
@@ -660,11 +675,13 @@ module ClaudeAgentSDK
660
675
 
661
676
  # Tool permission context
662
677
  class ToolPermissionContext
663
- attr_accessor :signal, :suggestions
678
+ attr_accessor :signal, :suggestions, :tool_use_id, :agent_id
664
679
 
665
- def initialize(signal: nil, suggestions: [])
680
+ def initialize(signal: nil, suggestions: [], tool_use_id: nil, agent_id: nil)
666
681
  @signal = signal
667
682
  @suggestions = suggestions
683
+ @tool_use_id = tool_use_id # Unique ID for this tool call within the assistant message
684
+ @agent_id = agent_id # Sub-agent ID if running within an agent context
668
685
  end
669
686
  end
670
687
 
@@ -1678,6 +1695,44 @@ module ClaudeAgentSDK
1678
1695
  end
1679
1696
  end
1680
1697
 
1698
+ # Result of a session fork operation
1699
+ class ForkSessionResult
1700
+ attr_accessor :session_id
1701
+
1702
+ def initialize(session_id:)
1703
+ @session_id = session_id
1704
+ end
1705
+ end
1706
+
1707
+ # API-side task budget in tokens.
1708
+ # When set, the model is made aware of its remaining token budget so it can
1709
+ # pace tool use and wrap up before the limit.
1710
+ class TaskBudget
1711
+ attr_accessor :total
1712
+
1713
+ def initialize(total:)
1714
+ @total = total
1715
+ end
1716
+
1717
+ def to_h
1718
+ { total: @total }
1719
+ end
1720
+ end
1721
+
1722
+ # System prompt file configuration — loads system prompt from a file path
1723
+ class SystemPromptFile
1724
+ attr_accessor :type, :path
1725
+
1726
+ def initialize(path:)
1727
+ @type = 'file'
1728
+ @path = path
1729
+ end
1730
+
1731
+ def to_h
1732
+ { type: @type, path: @path }
1733
+ end
1734
+ end
1735
+
1681
1736
  # System prompt preset configuration
1682
1737
  class SystemPromptPreset
1683
1738
  attr_accessor :type, :preset, :append
@@ -1712,7 +1767,7 @@ module ClaudeAgentSDK
1712
1767
  # Claude Agent Options for configuring queries
1713
1768
  class ClaudeAgentOptions
1714
1769
  attr_accessor :allowed_tools, :system_prompt, :mcp_servers, :permission_mode,
1715
- :continue_conversation, :resume, :max_turns, :disallowed_tools,
1770
+ :continue_conversation, :resume, :session_id, :max_turns, :disallowed_tools,
1716
1771
  :model, :permission_prompt_tool_name, :cwd, :cli_path, :settings,
1717
1772
  :add_dirs, :env, :extra_args, :max_buffer_size, :stderr,
1718
1773
  :can_use_tool, :hooks, :user, :include_partial_messages,
@@ -1720,7 +1775,7 @@ module ClaudeAgentSDK
1720
1775
  :output_format, :max_budget_usd, :max_thinking_tokens,
1721
1776
  :fallback_model, :plugins, :debug_stderr,
1722
1777
  :betas, :tools, :sandbox, :enable_file_checkpointing, :append_allowed_tools,
1723
- :thinking, :effort, :bare, :observers
1778
+ :thinking, :effort, :bare, :observers, :task_budget
1724
1779
 
1725
1780
  # Non-nil defaults for options that need them.
1726
1781
  # Keys absent from here default to nil.
@@ -1783,14 +1838,15 @@ module ClaudeAgentSDK
1783
1838
 
1784
1839
  # SDK MCP Tool definition
1785
1840
  class SdkMcpTool
1786
- attr_accessor :name, :description, :input_schema, :handler, :annotations
1841
+ attr_accessor :name, :description, :input_schema, :handler, :annotations, :meta
1787
1842
 
1788
- def initialize(name:, description:, input_schema:, handler:, annotations: nil)
1843
+ def initialize(name:, description:, input_schema:, handler:, annotations: nil, meta: nil)
1789
1844
  @name = name
1790
1845
  @description = description
1791
1846
  @input_schema = input_schema
1792
1847
  @handler = handler
1793
1848
  @annotations = annotations # MCP tool annotations (e.g., { title: '...', readOnlyHint: true })
1849
+ @meta = meta # MCP _meta field (e.g., { 'anthropic/maxResultSizeChars' => 100000 })
1794
1850
  end
1795
1851
  end
1796
1852
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeAgentSDK
4
- VERSION = '0.13.1'
4
+ VERSION = '0.14.0'
5
5
  end
@@ -82,10 +82,11 @@ module ClaudeAgentSDK
82
82
  # List sessions for a directory (or all sessions)
83
83
  # @param directory [String, nil] Working directory to list sessions for
84
84
  # @param limit [Integer, nil] Maximum number of sessions to return
85
+ # @param offset [Integer] Number of sessions to skip (for pagination)
85
86
  # @param include_worktrees [Boolean] Whether to include git worktree sessions
86
87
  # @return [Array<SDKSessionInfo>] Sessions sorted by last_modified descending
87
- def self.list_sessions(directory: nil, limit: nil, include_worktrees: true)
88
- Sessions.list_sessions(directory: directory, limit: limit, include_worktrees: include_worktrees)
88
+ def self.list_sessions(directory: nil, limit: nil, offset: 0, include_worktrees: true)
89
+ Sessions.list_sessions(directory: directory, limit: limit, offset: offset, include_worktrees: include_worktrees)
89
90
  end
90
91
 
91
92
  # Read metadata for a single session by ID (no full directory scan)
@@ -122,6 +123,24 @@ module ClaudeAgentSDK
122
123
  SessionMutations.tag_session(session_id: session_id, tag: tag, directory: directory)
123
124
  end
124
125
 
126
+ # Delete a session by removing its JSONL file (hard delete).
127
+ # @param session_id [String] UUID of the session to delete
128
+ # @param directory [String, nil] Project directory path
129
+ def self.delete_session(session_id:, directory: nil)
130
+ SessionMutations.delete_session(session_id: session_id, directory: directory)
131
+ end
132
+
133
+ # Fork a session into a new branch with fresh UUIDs.
134
+ # @param session_id [String] UUID of the session to fork
135
+ # @param directory [String, nil] Project directory path
136
+ # @param up_to_message_id [String, nil] Truncate the fork at this message UUID
137
+ # @param title [String, nil] Custom title for the fork
138
+ # @return [ForkSessionResult] Result containing the new session ID
139
+ def self.fork_session(session_id:, directory: nil, up_to_message_id: nil, title: nil)
140
+ SessionMutations.fork_session(session_id: session_id, directory: directory,
141
+ up_to_message_id: up_to_message_id, title: title)
142
+ end
143
+
125
144
  def self.query(prompt:, options: nil, &block)
126
145
  return enum_for(:query, prompt: prompt, options: options) unless block
127
146
 
@@ -455,6 +474,15 @@ module ClaudeAgentSDK
455
474
  @query_handler&.instance_variable_get(:@initialization_result)
456
475
  end
457
476
 
477
+ # Get a breakdown of current context window usage by category.
478
+ # Returns token counts per category (system prompt, tools, messages, etc.),
479
+ # total/max tokens, model info, MCP tools, memory files, and more.
480
+ # @return [Hash] Context usage response
481
+ def get_context_usage
482
+ raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
483
+ @query_handler.get_context_usage
484
+ end
485
+
458
486
  # Get current MCP server connection status (only works with streaming mode)
459
487
  # @return [Hash] MCP status information, including mcpServers list
460
488
  def get_mcp_status
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.13.1
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Community Contributors
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-04-05 00:00:00.000000000 Z
10
+ date: 2026-04-07 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: async