claude-agent-sdk 0.35.0 → 0.36.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.
@@ -20,6 +20,7 @@ require_relative 'claude_agent_sdk/session_resume'
20
20
  require_relative 'claude_agent_sdk/session_mutations'
21
21
  require_relative 'claude_agent_sdk/fiber_boundary'
22
22
  require_relative 'claude_agent_sdk/option_warnings'
23
+ require_relative 'claude_agent_sdk/deprecation'
23
24
  # Rails apps only: Bundler.require runs after `require 'rails'`, so the
24
25
  # Railtie (rake tasks; the generator lives under lib/generators) is picked up
25
26
  # there and nowhere else.
@@ -40,6 +41,7 @@ module ClaudeAgentSDK
40
41
  # skipped — most commonly a Class passed instead of an instance, which
41
42
  # previously produced silent zero instrumentation (every notify raised
42
43
  # NoMethodError, swallowed by notify_observers' error containment).
44
+ # @api private
43
45
  def self.resolve_observers(observers)
44
46
  Array(observers).filter_map do |obs|
45
47
  resolved = obs.respond_to?(:call) ? obs.call : obs
@@ -60,6 +62,7 @@ module ClaudeAgentSDK
60
62
  # configs may use String or Symbol keys (and a Symbol :sdk type) — the
61
63
  # recognition rule must match CommandBuilder#append_mcp_servers, which
62
64
  # strips the instance from exactly these entries.
65
+ # @api private
63
66
  def self.extract_sdk_mcp_servers(mcp_servers)
64
67
  return {} unless mcp_servers.is_a?(Hash)
65
68
 
@@ -75,6 +78,7 @@ module ClaudeAgentSDK
75
78
 
76
79
  # Internal: normalize hook lists for the control protocol. An absent or
77
80
  # disabled event must not become an empty registration in initialize.
81
+ # @api private
78
82
  def self.convert_hooks_to_internal_format(hooks)
79
83
  return nil unless hooks
80
84
 
@@ -108,6 +112,7 @@ module ClaudeAgentSDK
108
112
  # guarantees for can_use_tool — the permission round-trip works. The old
109
113
  # "requires streaming mode" ArgumentError was a needless restriction
110
114
  # (Python #1204).
115
+ # @api private
111
116
  def self.configure_can_use_tool(options)
112
117
  return options unless options.can_use_tool
113
118
 
@@ -124,6 +129,7 @@ module ClaudeAgentSDK
124
129
  # Internal: pull exclude_dynamic_sections out of a preset system prompt for
125
130
  # the initialize request (older CLIs ignore unknown initialize fields).
126
131
  # Shared by Client#connect and the one-shot query() path.
132
+ # @api private
127
133
  def self.extract_exclude_dynamic_sections(system_prompt)
128
134
  if system_prompt.is_a?(SystemPromptPreset)
129
135
  eds = system_prompt.exclude_dynamic_sections
@@ -143,6 +149,7 @@ module ClaudeAgentSDK
143
149
  # String or file prompt has no snapshot, and only a genuine true/false is
144
150
  # forwarded — `snapshot: false` is the primary use case, so the Hash lookup
145
151
  # must not collapse it to nil. Shared by Client#connect and query().
152
+ # @api private
146
153
  def self.extract_system_prompt_snapshot(system_prompt)
147
154
  case system_prompt
148
155
  when SystemPromptPreset, SystemPromptCustom
@@ -162,6 +169,7 @@ module ClaudeAgentSDK
162
169
  # Each observer is invoked through FiberBoundary so that user code runs
163
170
  # on a plain thread (no Fiber scheduler) even when called from inside
164
171
  # the SDK's Async reactor — or in place when scheduling is :inline.
172
+ # @api private
165
173
  def self.notify_observers(observers, method, *args, scheduling: :thread, wrapper: nil)
166
174
  observers.each do |obs|
167
175
  FiberBoundary.invoke(scheduling: scheduling, wrapper: wrapper) { obs.send(method, *args) }
@@ -188,8 +196,8 @@ module ClaudeAgentSDK
188
196
  #
189
197
  # @example Inside an inline-mode tool handler
190
198
  # ClaudeAgentSDK.offload { blocking_db_call }
191
- def self.offload(&block)
192
- FiberBoundary.invoke(&block)
199
+ def self.offload(&)
200
+ FiberBoundary.invoke(&)
193
201
  end
194
202
 
195
203
  # Guards the once-per-process flag below: two sessions connecting
@@ -202,6 +210,7 @@ module ClaudeAgentSDK
202
210
  # almost certainly violates inline mode's fiber-isolation precondition
203
211
  # (solid_queue fiber workers require isolation_level = :fiber).
204
212
  # defined? probing only; the SDK never loads ActiveSupport itself.
213
+ # @api private
205
214
  def self.check_inline_isolation(scheduling)
206
215
  return unless scheduling == :inline
207
216
  return unless defined?(ActiveSupport::IsolatedExecutionState)
@@ -224,6 +233,7 @@ module ClaudeAgentSDK
224
233
  # when there is none (non-user messages, tool_result-only content, …).
225
234
  # Only Hash and JSON-string items are inspected; arbitrary objects written
226
235
  # via to_s are never notified.
236
+ # @api private
227
237
  def self.extract_user_prompt_text(message)
228
238
  data = case message
229
239
  when Hash then message
@@ -254,6 +264,7 @@ module ClaudeAgentSDK
254
264
  # when there is no extractable text — on_user_prompt('') would latch
255
265
  # OTelObserver's first-prompt buffer while never setting the attribute,
256
266
  # permanently suppressing later real prompts.
267
+ # @api private
257
268
  def self.prompt_text_from_content(content)
258
269
  case content
259
270
  when String
@@ -273,6 +284,7 @@ module ClaudeAgentSDK
273
284
  # Wrap a streaming-input enumerable so observers get on_user_prompt for
274
285
  # each user message before it is written to stdin. Identity when no
275
286
  # observers are configured.
287
+ # @api private
276
288
  def self.observing_prompt_stream(prompt, observers, scheduling: :thread, wrapper: nil)
277
289
  return prompt if observers.empty?
278
290
 
@@ -287,6 +299,7 @@ module ClaudeAgentSDK
287
299
 
288
300
  # Look up a value in a hash that may use symbol or string keys in camelCase or snake_case.
289
301
  # Returns the first non-nil value found, preserving false as a meaningful value.
302
+ # @api private
290
303
  def self.flexible_fetch(hash, camel_key, snake_key)
291
304
  val = hash[camel_key.to_sym]
292
305
  val = hash[camel_key.to_s] if val.nil?
@@ -295,93 +308,211 @@ module ClaudeAgentSDK
295
308
  val
296
309
  end
297
310
 
298
- # List sessions for a directory (or all sessions)
299
- # @param directory [String, nil] Working directory to list sessions for
311
+ # ---- Session browsing & mutation ----
312
+ #
313
+ # One function per operation. By default each reads or writes the local-disk
314
+ # transcripts under CLAUDE_CONFIG_DIR; pass +session_store:+ to operate on a
315
+ # SessionStore instead. The two paths differ in a few documented ways:
316
+ #
317
+ # - +directory: nil+ searches every project directory on disk, but means the
318
+ # current working directory with a store (a SessionStore is keyed by
319
+ # project_key and cannot enumerate projects — parity with the Python SDK).
320
+ # - +include_worktrees:+ filters only on disk (list_sessions); with a
321
+ # +session_store:+, anything but the default +true+ raises ArgumentError.
322
+
323
+ # List sessions for a directory (or all sessions), newest first.
324
+ # @param directory [String, nil] Working directory to list sessions for. On
325
+ # disk, nil lists every project; with a session_store, nil means the
326
+ # current working directory.
300
327
  # @param limit [Integer, nil] Maximum number of sessions to return
301
328
  # @param offset [Integer] Number of sessions to skip (for pagination)
302
- # @param include_worktrees [Boolean] Whether to include git worktree sessions
329
+ # @param include_worktrees [Boolean] Disk only: also list the project's git
330
+ # worktree sessions. A store has no worktrees, so with a session_store only
331
+ # the default true is accepted; false or nil raises ArgumentError (the
332
+ # store path cannot apply the filter the caller asked for).
333
+ # @param session_store [SessionStore, nil] List from this store instead of
334
+ # local disk. Uses the store's list_session_summaries when implemented,
335
+ # else list_sessions + one load per listed session.
303
336
  # @return [Array<SDKSessionInfo>] Sessions sorted by last_modified descending
304
- def self.list_sessions(directory: nil, limit: nil, offset: 0, include_worktrees: true)
337
+ # @raise [ArgumentError] if include_worktrees is not true with a session_store,
338
+ # or the store implements neither list_session_summaries nor list_sessions
339
+ def self.list_sessions(directory: nil, limit: nil, offset: 0, include_worktrees: true, session_store: nil)
340
+ unless session_store.nil?
341
+ unless include_worktrees == true
342
+ raise ArgumentError, "include_worktrees: #{include_worktrees.inspect} applies only to local-disk " \
343
+ 'listing; a session_store is keyed by project and has no worktrees to exclude'
344
+ end
345
+
346
+ return Sessions.list_sessions_from_store(session_store: session_store, directory: directory,
347
+ limit: limit, offset: offset)
348
+ end
349
+
305
350
  Sessions.list_sessions(directory: directory, limit: limit, offset: offset, include_worktrees: include_worktrees)
306
351
  end
307
352
 
308
353
  # Read metadata for a single session by ID (no full directory scan)
309
354
  # @param session_id [String] UUID of the session to look up
310
- # @param directory [String, nil] Project directory path
311
- # @return [SDKSessionInfo, nil] Session info, or nil if not found
312
- def self.get_session_info(session_id:, directory: nil)
355
+ # @param directory [String, nil] Project directory path. On disk, nil
356
+ # searches every project; with a session_store, nil means the current
357
+ # working directory.
358
+ # @param session_store [SessionStore, nil] Read from this store instead of local disk
359
+ # @return [SDKSessionInfo, nil] Session info, or nil if not found / sidechain / no summary
360
+ def self.get_session_info(session_id:, directory: nil, session_store: nil)
361
+ unless session_store.nil?
362
+ return Sessions.get_session_info_from_store(session_store: session_store, session_id: session_id,
363
+ directory: directory)
364
+ end
365
+
313
366
  Sessions.get_session_info(session_id: session_id, directory: directory)
314
367
  end
315
368
 
316
369
  # Get messages from a session transcript
317
370
  # @param session_id [String] The session UUID
318
- # @param directory [String, nil] Working directory to search in
371
+ # @param directory [String, nil] Working directory to search in. On disk,
372
+ # nil searches every project; with a session_store, nil means the current
373
+ # working directory.
319
374
  # @param limit [Integer, nil] Maximum number of messages
320
375
  # @param offset [Integer] Number of messages to skip
376
+ # @param session_store [SessionStore, nil] Read from this store instead of local disk
321
377
  # @return [Array<SessionMessage>] Ordered messages from the session
322
- def self.get_session_messages(session_id:, directory: nil, limit: nil, offset: 0)
378
+ def self.get_session_messages(session_id:, directory: nil, limit: nil, offset: 0, session_store: nil)
379
+ unless session_store.nil?
380
+ return Sessions.get_session_messages_from_store(session_store: session_store, session_id: session_id,
381
+ directory: directory, limit: limit, offset: offset)
382
+ end
383
+
323
384
  Sessions.get_session_messages(session_id: session_id, directory: directory, limit: limit, offset: offset)
324
385
  end
325
386
 
326
- # List subagent IDs recorded for a session on local disk
387
+ # List subagent IDs recorded for a session
327
388
  # @param session_id [String] The session UUID
328
- # @param directory [String, nil] Working directory to search in
389
+ # @param directory [String, nil] Working directory to search in. On disk,
390
+ # nil searches every project; with a session_store, nil means the current
391
+ # working directory.
392
+ # @param session_store [SessionStore, nil] Read from this store instead of
393
+ # local disk (the store must implement list_subkeys)
329
394
  # @return [Array<String>] Subagent IDs
330
- def self.list_subagents(session_id:, directory: nil)
395
+ # @raise [ArgumentError] if the session_store does not implement list_subkeys
396
+ def self.list_subagents(session_id:, directory: nil, session_store: nil)
397
+ unless session_store.nil?
398
+ return Sessions.list_subagents_from_store(session_store: session_store, session_id: session_id,
399
+ directory: directory)
400
+ end
401
+
331
402
  Sessions.list_subagents(session_id: session_id, directory: directory)
332
403
  end
333
404
 
334
- # Read a subagent's optional metadata (not live status) from local disk.
405
+ # Read a subagent's optional metadata (not live status). With a
406
+ # session_store, the last agent_metadata entry wins and its synthetic type
407
+ # marker is omitted.
335
408
  # @param session_id [String] The parent session UUID
336
409
  # @param agent_id [String] The subagent ID, without the agent- prefix
337
- # @param directory [String, nil] Project directory to search in
410
+ # @param directory [String, nil] Project directory to search in. On disk,
411
+ # nil searches every project; with a session_store, nil means the current
412
+ # working directory.
413
+ # @param session_store [SessionStore, nil] Read from this store instead of local disk
338
414
  # @return [Hash{String => Object}, nil] CLI metadata, or nil if unavailable
339
- def self.get_subagent_metadata(session_id:, agent_id:, directory: nil)
415
+ def self.get_subagent_metadata(session_id:, agent_id:, directory: nil, session_store: nil)
416
+ unless session_store.nil?
417
+ return Sessions.get_subagent_metadata_from_store(session_store: session_store, session_id: session_id,
418
+ agent_id: agent_id, directory: directory)
419
+ end
420
+
340
421
  Sessions.get_subagent_metadata(session_id: session_id, agent_id: agent_id, directory: directory)
341
422
  end
342
423
 
343
- # Read a subagent's conversation messages from local disk
424
+ # Read a subagent's conversation messages
344
425
  # @param session_id [String] The session UUID
345
426
  # @param agent_id [String] The subagent ID (without the agent- prefix)
346
- # @param directory [String, nil] Working directory to search in
427
+ # @param directory [String, nil] Working directory to search in. On disk,
428
+ # nil searches every project; with a session_store, nil means the current
429
+ # working directory.
347
430
  # @param limit [Integer, nil] Maximum number of messages
348
431
  # @param offset [Integer] Number of messages to skip
432
+ # @param session_store [SessionStore, nil] Read from this store instead of local disk
349
433
  # @return [Array<SessionMessage>] Ordered messages from the subagent
350
- def self.get_subagent_messages(session_id:, agent_id:, directory: nil, limit: nil, offset: 0)
434
+ def self.get_subagent_messages(session_id:, agent_id:, directory: nil, limit: nil, offset: 0, session_store: nil)
435
+ unless session_store.nil?
436
+ return Sessions.get_subagent_messages_from_store(session_store: session_store, session_id: session_id,
437
+ agent_id: agent_id, directory: directory,
438
+ limit: limit, offset: offset)
439
+ end
440
+
351
441
  Sessions.get_subagent_messages(session_id: session_id, agent_id: agent_id,
352
442
  directory: directory, limit: limit, offset: offset)
353
443
  end
354
444
 
355
- # Rename a session by appending a custom-title entry
445
+ # Rename a session by appending a custom-title entry. With a session_store
446
+ # the entry is appended via SessionStore#append and carries a fresh uuid +
447
+ # timestamp (so uuid-deduping adapters treat it correctly).
356
448
  # @param session_id [String] UUID of the session to rename
357
449
  # @param title [String] New session title
358
- # @param directory [String, nil] Project directory path
359
- def self.rename_session(session_id:, title:, directory: nil)
450
+ # @param directory [String, nil] Project directory path (nil = cwd with a session_store)
451
+ # @param session_store [SessionStore, nil] Rename in this store instead of on local disk
452
+ # @raise [ArgumentError] if session_id is invalid or title is empty
453
+ # @raise [Errno::ENOENT] if the session is not found (nothing is written)
454
+ def self.rename_session(session_id:, title:, directory: nil, session_store: nil)
455
+ unless session_store.nil?
456
+ return SessionMutations.rename_session_via_store(session_store: session_store, session_id: session_id,
457
+ title: title, directory: directory)
458
+ end
459
+
360
460
  SessionMutations.rename_session(session_id: session_id, title: title, directory: directory)
361
461
  end
362
462
 
363
463
  # Tag a session. Pass nil to clear the tag.
364
464
  # @param session_id [String] UUID of the session to tag
365
465
  # @param tag [String, nil] Tag string, or nil to clear
366
- # @param directory [String, nil] Project directory path
367
- def self.tag_session(session_id:, tag:, directory: nil)
466
+ # @param directory [String, nil] Project directory path (nil = cwd with a session_store)
467
+ # @param session_store [SessionStore, nil] Tag in this store instead of on local disk
468
+ # @raise [ArgumentError] if session_id is invalid or tag is empty after sanitization
469
+ # @raise [Errno::ENOENT] if the session is not found (nothing is written)
470
+ def self.tag_session(session_id:, tag:, directory: nil, session_store: nil)
471
+ unless session_store.nil?
472
+ return SessionMutations.tag_session_via_store(session_store: session_store, session_id: session_id,
473
+ tag: tag, directory: directory)
474
+ end
475
+
368
476
  SessionMutations.tag_session(session_id: session_id, tag: tag, directory: directory)
369
477
  end
370
478
 
371
- # Delete a session by removing its JSONL file (hard delete).
479
+ # Delete a session (hard delete). On disk, removes its JSONL file and
480
+ # subagent directory, raising Errno::ENOENT if the session is not found.
481
+ # With a session_store, calls SessionStore#delete — a no-op when the store
482
+ # does not implement #delete (WORM/append-only backends); whether subagent
483
+ # subkeys are removed too depends on the store's cascade semantics.
372
484
  # @param session_id [String] UUID of the session to delete
373
- # @param directory [String, nil] Project directory path
374
- def self.delete_session(session_id:, directory: nil)
485
+ # @param directory [String, nil] Project directory path (nil = cwd with a session_store)
486
+ # @param session_store [SessionStore, nil] Delete from this store instead of local disk
487
+ # @raise [ArgumentError] if session_id is invalid
488
+ # @raise [Errno::ENOENT] on disk, if the session file cannot be found
489
+ def self.delete_session(session_id:, directory: nil, session_store: nil)
490
+ unless session_store.nil?
491
+ return SessionMutations.delete_session_via_store(session_store: session_store, session_id: session_id,
492
+ directory: directory)
493
+ end
494
+
375
495
  SessionMutations.delete_session(session_id: session_id, directory: directory)
376
496
  end
377
497
 
378
- # Fork a session into a new branch with fresh UUIDs.
498
+ # Fork a session into a new branch with fresh UUIDs. With a session_store
499
+ # the fork transform runs over the store's entries and the fork is appended
500
+ # to the same store.
379
501
  # @param session_id [String] UUID of the session to fork
380
- # @param directory [String, nil] Project directory path
502
+ # @param directory [String, nil] Project directory path (nil = cwd with a session_store)
381
503
  # @param up_to_message_id [String, nil] Truncate the fork at this message UUID
382
504
  # @param title [String, nil] Custom title for the fork
505
+ # @param session_store [SessionStore, nil] Fork within this store instead of on local disk
383
506
  # @return [ForkSessionResult] Result containing the new session ID
384
- def self.fork_session(session_id:, directory: nil, up_to_message_id: nil, title: nil)
507
+ # @raise [ArgumentError] if session_id/up_to_message_id is invalid or there are no messages
508
+ # @raise [Errno::ENOENT] if the source session is not found
509
+ def self.fork_session(session_id:, directory: nil, up_to_message_id: nil, title: nil, session_store: nil)
510
+ unless session_store.nil?
511
+ return SessionMutations.fork_session_via_store(session_store: session_store, session_id: session_id,
512
+ directory: directory, up_to_message_id: up_to_message_id,
513
+ title: title)
514
+ end
515
+
385
516
  SessionMutations.fork_session(session_id: session_id, directory: directory,
386
517
  up_to_message_id: up_to_message_id, title: title)
387
518
  end
@@ -406,81 +537,83 @@ module ClaudeAgentSDK
406
537
  SessionSummary.fold_session_summary(prev, key, entries)
407
538
  end
408
539
 
409
- # List sessions from a SessionStore (store-backed counterpart to list_sessions).
410
- # @param session_store [SessionStore] the store to read from
540
+ # ---- Deprecated store twins (removed in 1.0) ----
541
+ #
542
+ # Each forwards to the same implementation as before — not to the merged
543
+ # function, so a nil session_store keeps failing as it always did instead
544
+ # of silently reading local disk — after one warning per method per process.
545
+
546
+ # @deprecated Use {.list_sessions} with +session_store:+. Removed in 1.0.
411
547
  # @return [Array<SDKSessionInfo>] sorted by last_modified descending
412
548
  def self.list_sessions_from_store(session_store:, directory: nil, limit: nil, offset: 0)
549
+ Deprecation.warn_once(:list_sessions_from_store, 'list_sessions(session_store: store)')
413
550
  Sessions.list_sessions_from_store(session_store: session_store, directory: directory, limit: limit, offset: offset)
414
551
  end
415
552
 
416
- # Read metadata for a single session from a SessionStore.
553
+ # @deprecated Use {.get_session_info} with +session_store:+. Removed in 1.0.
417
554
  # @return [SDKSessionInfo, nil]
418
555
  def self.get_session_info_from_store(session_store:, session_id:, directory: nil)
556
+ Deprecation.warn_once(:get_session_info_from_store, 'get_session_info(session_store: store, ...)')
419
557
  Sessions.get_session_info_from_store(session_store: session_store, session_id: session_id, directory: directory)
420
558
  end
421
559
 
422
- # Read a session's conversation messages from a SessionStore.
560
+ # @deprecated Use {.get_session_messages} with +session_store:+. Removed in 1.0.
423
561
  # @return [Array<SessionMessage>]
424
562
  def self.get_session_messages_from_store(session_store:, session_id:, directory: nil, limit: nil, offset: 0)
563
+ Deprecation.warn_once(:get_session_messages_from_store, 'get_session_messages(session_store: store, ...)')
425
564
  Sessions.get_session_messages_from_store(session_store: session_store, session_id: session_id,
426
565
  directory: directory, limit: limit, offset: offset)
427
566
  end
428
567
 
429
- # List subagent IDs for a session from a SessionStore (requires list_subkeys).
568
+ # @deprecated Use {.list_subagents} with +session_store:+. Removed in 1.0.
430
569
  # @return [Array<String>]
431
570
  def self.list_subagents_from_store(session_store:, session_id:, directory: nil)
571
+ Deprecation.warn_once(:list_subagents_from_store, 'list_subagents(session_store: store, ...)')
432
572
  Sessions.list_subagents_from_store(session_store: session_store, session_id: session_id, directory: directory)
433
573
  end
434
574
 
435
- # Read the latest subagent metadata from a SessionStore, without its synthetic type marker.
575
+ # @deprecated Use {.get_subagent_metadata} with +session_store:+. Removed in 1.0.
436
576
  # @return [Hash{String => Object}, nil]
437
577
  def self.get_subagent_metadata_from_store(session_store:, session_id:, agent_id:, directory: nil)
578
+ Deprecation.warn_once(:get_subagent_metadata_from_store, 'get_subagent_metadata(session_store: store, ...)')
438
579
  Sessions.get_subagent_metadata_from_store(session_store: session_store, session_id: session_id,
439
580
  agent_id: agent_id, directory: directory)
440
581
  end
441
582
 
442
- # Read a subagent's conversation messages from a SessionStore.
583
+ # @deprecated Use {.get_subagent_messages} with +session_store:+. Removed in 1.0.
443
584
  # @return [Array<SessionMessage>]
444
585
  def self.get_subagent_messages_from_store(session_store:, session_id:, agent_id:, directory: nil, limit: nil,
445
586
  offset: 0)
587
+ Deprecation.warn_once(:get_subagent_messages_from_store, 'get_subagent_messages(session_store: store, ...)')
446
588
  Sessions.get_subagent_messages_from_store(session_store: session_store, session_id: session_id,
447
589
  agent_id: agent_id, directory: directory, limit: limit, offset: offset)
448
590
  end
449
591
 
450
- # Rename a session in a SessionStore (store-backed counterpart to
451
- # rename_session). Appends a custom-title entry carrying a fresh uuid +
452
- # timestamp via SessionStore#append.
453
- # @raise [ArgumentError] if session_id is invalid or title is empty
454
- # @raise [Errno::ENOENT] if the session is not found in the store (nothing is appended)
592
+ # @deprecated Use {.rename_session} with +session_store:+. Removed in 1.0.
455
593
  def self.rename_session_via_store(session_store:, session_id:, title:, directory: nil)
594
+ Deprecation.warn_once(:rename_session_via_store, 'rename_session(session_store: store, ...)')
456
595
  SessionMutations.rename_session_via_store(session_store: session_store, session_id: session_id,
457
596
  title: title, directory: directory)
458
597
  end
459
598
 
460
- # Tag a session in a SessionStore (store-backed counterpart to tag_session).
461
- # Pass nil to clear the tag.
462
- # @raise [ArgumentError] if session_id is invalid or tag is empty after sanitization
463
- # @raise [Errno::ENOENT] if the session is not found in the store (nothing is appended)
599
+ # @deprecated Use {.tag_session} with +session_store:+. Removed in 1.0.
464
600
  def self.tag_session_via_store(session_store:, session_id:, tag:, directory: nil)
601
+ Deprecation.warn_once(:tag_session_via_store, 'tag_session(session_store: store, ...)')
465
602
  SessionMutations.tag_session_via_store(session_store: session_store, session_id: session_id,
466
603
  tag: tag, directory: directory)
467
604
  end
468
605
 
469
- # Delete a session from a SessionStore (store-backed counterpart to
470
- # delete_session). No-op when the store does not implement #delete
471
- # (WORM/append-only backends).
472
- # @raise [ArgumentError] if session_id is invalid
606
+ # @deprecated Use {.delete_session} with +session_store:+. Removed in 1.0.
473
607
  def self.delete_session_via_store(session_store:, session_id:, directory: nil)
608
+ Deprecation.warn_once(:delete_session_via_store, 'delete_session(session_store: store, ...)')
474
609
  SessionMutations.delete_session_via_store(session_store: session_store, session_id: session_id,
475
610
  directory: directory)
476
611
  end
477
612
 
478
- # Fork a session in a SessionStore into a new branch with fresh UUIDs
479
- # (store-backed counterpart to fork_session).
480
- # @return [ForkSessionResult] result containing the new session ID
481
- # @raise [ArgumentError] if session_id/up_to_message_id is invalid or there are no messages
482
- # @raise [Errno::ENOENT] if the source session is not found in the store
613
+ # @deprecated Use {.fork_session} with +session_store:+. Removed in 1.0.
614
+ # @return [ForkSessionResult]
483
615
  def self.fork_session_via_store(session_store:, session_id:, directory: nil, up_to_message_id: nil, title: nil)
616
+ Deprecation.warn_once(:fork_session_via_store, 'fork_session(session_store: store, ...)')
484
617
  SessionMutations.fork_session_via_store(session_store: session_store, session_id: session_id,
485
618
  directory: directory, up_to_message_id: up_to_message_id, title: title)
486
619
  end
@@ -702,6 +835,51 @@ module ClaudeAgentSDK
702
835
  end).wait
703
836
  end
704
837
 
838
+ # Run a query to completion and return its final ResultMessage.
839
+ #
840
+ # The one-call form of {.query} for when you want the answer rather than
841
+ # the stream: +ask(prompt).result+ is the final text, and the returned
842
+ # ResultMessage also carries cost, usage, duration, session_id and
843
+ # structured_output. It is {.query} underneath — same prompt types, same
844
+ # options, same errors — and it consumes the whole stream before
845
+ # returning. With an Enumerable prompt that produces several turns, the
846
+ # last ResultMessage is returned.
847
+ #
848
+ # An error result is returned like any other (check #is_error / #subtype);
849
+ # when the CLI then exits non-zero, {.query} raises ResultError, which
850
+ # propagates from here unchanged.
851
+ #
852
+ # @param prompt [String, Enumerable] The prompt, as for {.query}
853
+ # @param options [ClaudeAgentOptions, nil] Optional configuration
854
+ # @param transport [Transport, nil] Optional transport, as for {.query}
855
+ # @yield [Message] Optionally, every message as it arrives (including the
856
+ # final ResultMessage), so you can stream progress and still get the
857
+ # result back. Runs where {.query}'s block runs. The block observes the
858
+ # stream; it cannot end it early — use {.query} for that.
859
+ # @return [ResultMessage]
860
+ # @raise [CLIConnectionError] if the stream ends without a ResultMessage
861
+ #
862
+ # @example
863
+ # puts ClaudeAgentSDK.ask('What is 2 + 2?').result
864
+ #
865
+ # @example Stream progress, keep the result
866
+ # result = ClaudeAgentSDK.ask('Refactor lib/foo.rb', options: options) do |message|
867
+ # puts message.text if message.is_a?(ClaudeAgentSDK::AssistantMessage)
868
+ # end
869
+ # puts result # => [result: success, 3 turns, 12.4s, $0.0421]
870
+ def self.ask(prompt, options: nil, transport: nil, &block)
871
+ result = nil
872
+ query(prompt: prompt, options: options, transport: transport) do |message|
873
+ result = message if message.is_a?(ResultMessage)
874
+ block&.call(message)
875
+ end
876
+ # The same class Query raises to a caller still waiting on the stream
877
+ # when it ends ("Control stream ended").
878
+ raise CLIConnectionError, 'Claude Code ended the conversation without a result message' unless result
879
+
880
+ result
881
+ end
882
+
705
883
  # Client for bidirectional, interactive conversations with Claude Code
706
884
  #
707
885
  # This client provides full control over the conversation flow with support
@@ -971,6 +1149,12 @@ module ClaudeAgentSDK
971
1149
  @query_handler.set_permission_mode(mode)
972
1150
  end
973
1151
 
1152
+ # Ruby-style spelling of #set_permission_mode: `client.permission_mode = 'plan'`.
1153
+ # Delegates (rather than aliasing) so an override of #set_permission_mode applies to both.
1154
+ def permission_mode=(mode)
1155
+ set_permission_mode(mode)
1156
+ end
1157
+
974
1158
  # Change the AI model during conversation
975
1159
  # @param model [String, nil] Model name or nil for default
976
1160
  def set_model(model)
@@ -978,6 +1162,11 @@ module ClaudeAgentSDK
978
1162
  @query_handler.set_model(model)
979
1163
  end
980
1164
 
1165
+ # Ruby-style spelling of #set_model: `client.model = 'claude-opus-5'`.
1166
+ def model=(model)
1167
+ set_model(model)
1168
+ end
1169
+
981
1170
  # Reconnect a failed MCP server
982
1171
  # @param server_name [String] Name of the MCP server to reconnect
983
1172
  def reconnect_mcp_server(server_name)
@@ -1049,6 +1238,12 @@ module ClaudeAgentSDK
1049
1238
  @query_handler.get_context_usage
1050
1239
  end
1051
1240
 
1241
+ # Ruby-style spelling of #get_context_usage.
1242
+ # @return [Hash] Context usage response
1243
+ def context_usage
1244
+ get_context_usage
1245
+ end
1246
+
1052
1247
  # Get current MCP server connection status (only works with streaming mode)
1053
1248
  # @return [Hash] MCP status information, including mcpServers list
1054
1249
  def get_mcp_status
@@ -1056,6 +1251,12 @@ module ClaudeAgentSDK
1056
1251
  @query_handler.get_mcp_status
1057
1252
  end
1058
1253
 
1254
+ # Ruby-style spelling of #get_mcp_status.
1255
+ # @return [Hash] MCP status information, including mcpServers list
1256
+ def mcp_status
1257
+ get_mcp_status
1258
+ end
1259
+
1059
1260
  # Get server initialization info including available commands and output styles
1060
1261
  # @return [Hash] Server info
1061
1262
  def get_server_info
@@ -18,9 +18,9 @@ ClaudeAgentSDK.configure do |config|
18
18
  # 'bypassPermissions' (unattended jobs in a sandbox only), or 'auto'.
19
19
  # permission_mode: 'default',
20
20
 
21
- # The CLI is found in vendor/claude relative to the process cwd (Rails.root
22
- # under bin/rails, Puma and most job runners). Pin it for other launchers:
23
- # cli_path: Rails.root.join('vendor', 'claude', 'claude').to_s,
21
+ # The CLI is found in Rails.root/vendor/claude (bin/rails claude_agent_sdk:install_cli),
22
+ # whatever the process cwd. To run a binary from somewhere else instead:
23
+ # cli_path: '/usr/local/bin/claude',
24
24
 
25
25
  # OpenTelemetry tracing (Langfuse, Honeycomb, ...). A factory lambda gives
26
26
  # every query/session its own observer — safe under Puma and job threads.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: claude-agent-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.35.0
4
+ version: 0.36.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ya-luotao
@@ -138,6 +138,7 @@ files:
138
138
  - lib/claude_agent_sdk/cli_installer.rb
139
139
  - lib/claude_agent_sdk/command_builder.rb
140
140
  - lib/claude_agent_sdk/configuration.rb
141
+ - lib/claude_agent_sdk/deprecation.rb
141
142
  - lib/claude_agent_sdk/errors.rb
142
143
  - lib/claude_agent_sdk/fiber_boundary.rb
143
144
  - lib/claude_agent_sdk/instrumentation.rb