claude-agent-sdk 0.34.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +80 -0
- data/README.md +68 -24
- data/docs/cli-installer.md +38 -1
- data/docs/client.md +44 -20
- data/docs/errors.md +15 -1
- data/docs/hooks-and-permissions.md +22 -0
- data/docs/mcp-servers.md +37 -7
- data/docs/rails.md +92 -54
- data/docs/sessions.md +69 -33
- data/lib/claude_agent_sdk/cancellation_signal.rb +2 -1
- data/lib/claude_agent_sdk/cli_installer.rb +38 -8
- data/lib/claude_agent_sdk/deprecation.rb +51 -0
- data/lib/claude_agent_sdk/errors.rb +8 -0
- data/lib/claude_agent_sdk/fiber_boundary.rb +111 -2
- data/lib/claude_agent_sdk/option_warnings.rb +0 -2
- data/lib/claude_agent_sdk/query.rb +49 -8
- data/lib/claude_agent_sdk/railtie.rb +105 -0
- data/lib/claude_agent_sdk/sdk_mcp_server.rb +36 -25
- data/lib/claude_agent_sdk/session_mutations.rb +10 -10
- data/lib/claude_agent_sdk/session_resume.rb +19 -24
- data/lib/claude_agent_sdk/session_store.rb +28 -18
- data/lib/claude_agent_sdk/session_summary.rb +8 -3
- data/lib/claude_agent_sdk/sessions.rb +104 -18
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +4 -13
- data/lib/claude_agent_sdk/tasks/claude_agent_sdk.rake +37 -0
- data/lib/claude_agent_sdk/tasks.rb +13 -0
- data/lib/claude_agent_sdk/testing/session_store_conformance.rb +1 -1
- data/lib/claude_agent_sdk/transcript_mirror_batcher.rb +17 -1
- data/lib/claude_agent_sdk/types.rb +219 -3
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +261 -56
- data/lib/generators/claude_agent_sdk/install/install_generator.rb +63 -0
- data/lib/generators/claude_agent_sdk/install/templates/claude_agent_sdk.rb.tt +35 -0
- metadata +17 -6
data/lib/claude_agent_sdk.rb
CHANGED
|
@@ -20,6 +20,11 @@ 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'
|
|
24
|
+
# Rails apps only: Bundler.require runs after `require 'rails'`, so the
|
|
25
|
+
# Railtie (rake tasks; the generator lives under lib/generators) is picked up
|
|
26
|
+
# there and nowhere else.
|
|
27
|
+
require_relative 'claude_agent_sdk/railtie' if defined?(Rails::Railtie)
|
|
23
28
|
require 'async'
|
|
24
29
|
require 'securerandom'
|
|
25
30
|
|
|
@@ -36,6 +41,7 @@ module ClaudeAgentSDK
|
|
|
36
41
|
# skipped — most commonly a Class passed instead of an instance, which
|
|
37
42
|
# previously produced silent zero instrumentation (every notify raised
|
|
38
43
|
# NoMethodError, swallowed by notify_observers' error containment).
|
|
44
|
+
# @api private
|
|
39
45
|
def self.resolve_observers(observers)
|
|
40
46
|
Array(observers).filter_map do |obs|
|
|
41
47
|
resolved = obs.respond_to?(:call) ? obs.call : obs
|
|
@@ -56,6 +62,7 @@ module ClaudeAgentSDK
|
|
|
56
62
|
# configs may use String or Symbol keys (and a Symbol :sdk type) — the
|
|
57
63
|
# recognition rule must match CommandBuilder#append_mcp_servers, which
|
|
58
64
|
# strips the instance from exactly these entries.
|
|
65
|
+
# @api private
|
|
59
66
|
def self.extract_sdk_mcp_servers(mcp_servers)
|
|
60
67
|
return {} unless mcp_servers.is_a?(Hash)
|
|
61
68
|
|
|
@@ -71,6 +78,7 @@ module ClaudeAgentSDK
|
|
|
71
78
|
|
|
72
79
|
# Internal: normalize hook lists for the control protocol. An absent or
|
|
73
80
|
# disabled event must not become an empty registration in initialize.
|
|
81
|
+
# @api private
|
|
74
82
|
def self.convert_hooks_to_internal_format(hooks)
|
|
75
83
|
return nil unless hooks
|
|
76
84
|
|
|
@@ -104,6 +112,7 @@ module ClaudeAgentSDK
|
|
|
104
112
|
# guarantees for can_use_tool — the permission round-trip works. The old
|
|
105
113
|
# "requires streaming mode" ArgumentError was a needless restriction
|
|
106
114
|
# (Python #1204).
|
|
115
|
+
# @api private
|
|
107
116
|
def self.configure_can_use_tool(options)
|
|
108
117
|
return options unless options.can_use_tool
|
|
109
118
|
|
|
@@ -120,6 +129,7 @@ module ClaudeAgentSDK
|
|
|
120
129
|
# Internal: pull exclude_dynamic_sections out of a preset system prompt for
|
|
121
130
|
# the initialize request (older CLIs ignore unknown initialize fields).
|
|
122
131
|
# Shared by Client#connect and the one-shot query() path.
|
|
132
|
+
# @api private
|
|
123
133
|
def self.extract_exclude_dynamic_sections(system_prompt)
|
|
124
134
|
if system_prompt.is_a?(SystemPromptPreset)
|
|
125
135
|
eds = system_prompt.exclude_dynamic_sections
|
|
@@ -139,6 +149,7 @@ module ClaudeAgentSDK
|
|
|
139
149
|
# String or file prompt has no snapshot, and only a genuine true/false is
|
|
140
150
|
# forwarded — `snapshot: false` is the primary use case, so the Hash lookup
|
|
141
151
|
# must not collapse it to nil. Shared by Client#connect and query().
|
|
152
|
+
# @api private
|
|
142
153
|
def self.extract_system_prompt_snapshot(system_prompt)
|
|
143
154
|
case system_prompt
|
|
144
155
|
when SystemPromptPreset, SystemPromptCustom
|
|
@@ -158,6 +169,7 @@ module ClaudeAgentSDK
|
|
|
158
169
|
# Each observer is invoked through FiberBoundary so that user code runs
|
|
159
170
|
# on a plain thread (no Fiber scheduler) even when called from inside
|
|
160
171
|
# the SDK's Async reactor — or in place when scheduling is :inline.
|
|
172
|
+
# @api private
|
|
161
173
|
def self.notify_observers(observers, method, *args, scheduling: :thread, wrapper: nil)
|
|
162
174
|
observers.each do |obs|
|
|
163
175
|
FiberBoundary.invoke(scheduling: scheduling, wrapper: wrapper) { obs.send(method, *args) }
|
|
@@ -184,8 +196,8 @@ module ClaudeAgentSDK
|
|
|
184
196
|
#
|
|
185
197
|
# @example Inside an inline-mode tool handler
|
|
186
198
|
# ClaudeAgentSDK.offload { blocking_db_call }
|
|
187
|
-
def self.offload(&
|
|
188
|
-
FiberBoundary.invoke(&
|
|
199
|
+
def self.offload(&)
|
|
200
|
+
FiberBoundary.invoke(&)
|
|
189
201
|
end
|
|
190
202
|
|
|
191
203
|
# Guards the once-per-process flag below: two sessions connecting
|
|
@@ -198,6 +210,7 @@ module ClaudeAgentSDK
|
|
|
198
210
|
# almost certainly violates inline mode's fiber-isolation precondition
|
|
199
211
|
# (solid_queue fiber workers require isolation_level = :fiber).
|
|
200
212
|
# defined? probing only; the SDK never loads ActiveSupport itself.
|
|
213
|
+
# @api private
|
|
201
214
|
def self.check_inline_isolation(scheduling)
|
|
202
215
|
return unless scheduling == :inline
|
|
203
216
|
return unless defined?(ActiveSupport::IsolatedExecutionState)
|
|
@@ -220,6 +233,7 @@ module ClaudeAgentSDK
|
|
|
220
233
|
# when there is none (non-user messages, tool_result-only content, …).
|
|
221
234
|
# Only Hash and JSON-string items are inspected; arbitrary objects written
|
|
222
235
|
# via to_s are never notified.
|
|
236
|
+
# @api private
|
|
223
237
|
def self.extract_user_prompt_text(message)
|
|
224
238
|
data = case message
|
|
225
239
|
when Hash then message
|
|
@@ -250,6 +264,7 @@ module ClaudeAgentSDK
|
|
|
250
264
|
# when there is no extractable text — on_user_prompt('') would latch
|
|
251
265
|
# OTelObserver's first-prompt buffer while never setting the attribute,
|
|
252
266
|
# permanently suppressing later real prompts.
|
|
267
|
+
# @api private
|
|
253
268
|
def self.prompt_text_from_content(content)
|
|
254
269
|
case content
|
|
255
270
|
when String
|
|
@@ -269,6 +284,7 @@ module ClaudeAgentSDK
|
|
|
269
284
|
# Wrap a streaming-input enumerable so observers get on_user_prompt for
|
|
270
285
|
# each user message before it is written to stdin. Identity when no
|
|
271
286
|
# observers are configured.
|
|
287
|
+
# @api private
|
|
272
288
|
def self.observing_prompt_stream(prompt, observers, scheduling: :thread, wrapper: nil)
|
|
273
289
|
return prompt if observers.empty?
|
|
274
290
|
|
|
@@ -283,6 +299,7 @@ module ClaudeAgentSDK
|
|
|
283
299
|
|
|
284
300
|
# Look up a value in a hash that may use symbol or string keys in camelCase or snake_case.
|
|
285
301
|
# Returns the first non-nil value found, preserving false as a meaningful value.
|
|
302
|
+
# @api private
|
|
286
303
|
def self.flexible_fetch(hash, camel_key, snake_key)
|
|
287
304
|
val = hash[camel_key.to_sym]
|
|
288
305
|
val = hash[camel_key.to_s] if val.nil?
|
|
@@ -291,93 +308,211 @@ module ClaudeAgentSDK
|
|
|
291
308
|
val
|
|
292
309
|
end
|
|
293
310
|
|
|
294
|
-
#
|
|
295
|
-
#
|
|
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.
|
|
296
327
|
# @param limit [Integer, nil] Maximum number of sessions to return
|
|
297
328
|
# @param offset [Integer] Number of sessions to skip (for pagination)
|
|
298
|
-
# @param include_worktrees [Boolean]
|
|
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.
|
|
299
336
|
# @return [Array<SDKSessionInfo>] Sessions sorted by last_modified descending
|
|
300
|
-
|
|
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
|
+
|
|
301
350
|
Sessions.list_sessions(directory: directory, limit: limit, offset: offset, include_worktrees: include_worktrees)
|
|
302
351
|
end
|
|
303
352
|
|
|
304
353
|
# Read metadata for a single session by ID (no full directory scan)
|
|
305
354
|
# @param session_id [String] UUID of the session to look up
|
|
306
|
-
# @param directory [String, nil] Project directory path
|
|
307
|
-
#
|
|
308
|
-
|
|
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
|
+
|
|
309
366
|
Sessions.get_session_info(session_id: session_id, directory: directory)
|
|
310
367
|
end
|
|
311
368
|
|
|
312
369
|
# Get messages from a session transcript
|
|
313
370
|
# @param session_id [String] The session UUID
|
|
314
|
-
# @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.
|
|
315
374
|
# @param limit [Integer, nil] Maximum number of messages
|
|
316
375
|
# @param offset [Integer] Number of messages to skip
|
|
376
|
+
# @param session_store [SessionStore, nil] Read from this store instead of local disk
|
|
317
377
|
# @return [Array<SessionMessage>] Ordered messages from the session
|
|
318
|
-
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
|
+
|
|
319
384
|
Sessions.get_session_messages(session_id: session_id, directory: directory, limit: limit, offset: offset)
|
|
320
385
|
end
|
|
321
386
|
|
|
322
|
-
# List subagent IDs recorded for a session
|
|
387
|
+
# List subagent IDs recorded for a session
|
|
323
388
|
# @param session_id [String] The session UUID
|
|
324
|
-
# @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)
|
|
325
394
|
# @return [Array<String>] Subagent IDs
|
|
326
|
-
|
|
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
|
+
|
|
327
402
|
Sessions.list_subagents(session_id: session_id, directory: directory)
|
|
328
403
|
end
|
|
329
404
|
|
|
330
|
-
# Read a subagent's optional metadata (not live status)
|
|
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.
|
|
331
408
|
# @param session_id [String] The parent session UUID
|
|
332
409
|
# @param agent_id [String] The subagent ID, without the agent- prefix
|
|
333
|
-
# @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
|
|
334
414
|
# @return [Hash{String => Object}, nil] CLI metadata, or nil if unavailable
|
|
335
|
-
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
|
+
|
|
336
421
|
Sessions.get_subagent_metadata(session_id: session_id, agent_id: agent_id, directory: directory)
|
|
337
422
|
end
|
|
338
423
|
|
|
339
|
-
# Read a subagent's conversation messages
|
|
424
|
+
# Read a subagent's conversation messages
|
|
340
425
|
# @param session_id [String] The session UUID
|
|
341
426
|
# @param agent_id [String] The subagent ID (without the agent- prefix)
|
|
342
|
-
# @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.
|
|
343
430
|
# @param limit [Integer, nil] Maximum number of messages
|
|
344
431
|
# @param offset [Integer] Number of messages to skip
|
|
432
|
+
# @param session_store [SessionStore, nil] Read from this store instead of local disk
|
|
345
433
|
# @return [Array<SessionMessage>] Ordered messages from the subagent
|
|
346
|
-
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
|
+
|
|
347
441
|
Sessions.get_subagent_messages(session_id: session_id, agent_id: agent_id,
|
|
348
442
|
directory: directory, limit: limit, offset: offset)
|
|
349
443
|
end
|
|
350
444
|
|
|
351
|
-
# 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).
|
|
352
448
|
# @param session_id [String] UUID of the session to rename
|
|
353
449
|
# @param title [String] New session title
|
|
354
|
-
# @param directory [String, nil] Project directory path
|
|
355
|
-
|
|
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
|
+
|
|
356
460
|
SessionMutations.rename_session(session_id: session_id, title: title, directory: directory)
|
|
357
461
|
end
|
|
358
462
|
|
|
359
463
|
# Tag a session. Pass nil to clear the tag.
|
|
360
464
|
# @param session_id [String] UUID of the session to tag
|
|
361
465
|
# @param tag [String, nil] Tag string, or nil to clear
|
|
362
|
-
# @param directory [String, nil] Project directory path
|
|
363
|
-
|
|
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
|
+
|
|
364
476
|
SessionMutations.tag_session(session_id: session_id, tag: tag, directory: directory)
|
|
365
477
|
end
|
|
366
478
|
|
|
367
|
-
# Delete a session
|
|
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.
|
|
368
484
|
# @param session_id [String] UUID of the session to delete
|
|
369
|
-
# @param directory [String, nil] Project directory path
|
|
370
|
-
|
|
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
|
+
|
|
371
495
|
SessionMutations.delete_session(session_id: session_id, directory: directory)
|
|
372
496
|
end
|
|
373
497
|
|
|
374
|
-
# 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.
|
|
375
501
|
# @param session_id [String] UUID of the session to fork
|
|
376
|
-
# @param directory [String, nil] Project directory path
|
|
502
|
+
# @param directory [String, nil] Project directory path (nil = cwd with a session_store)
|
|
377
503
|
# @param up_to_message_id [String, nil] Truncate the fork at this message UUID
|
|
378
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
|
|
379
506
|
# @return [ForkSessionResult] Result containing the new session ID
|
|
380
|
-
|
|
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
|
+
|
|
381
516
|
SessionMutations.fork_session(session_id: session_id, directory: directory,
|
|
382
517
|
up_to_message_id: up_to_message_id, title: title)
|
|
383
518
|
end
|
|
@@ -402,81 +537,83 @@ module ClaudeAgentSDK
|
|
|
402
537
|
SessionSummary.fold_session_summary(prev, key, entries)
|
|
403
538
|
end
|
|
404
539
|
|
|
405
|
-
#
|
|
406
|
-
#
|
|
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.
|
|
407
547
|
# @return [Array<SDKSessionInfo>] sorted by last_modified descending
|
|
408
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)')
|
|
409
550
|
Sessions.list_sessions_from_store(session_store: session_store, directory: directory, limit: limit, offset: offset)
|
|
410
551
|
end
|
|
411
552
|
|
|
412
|
-
#
|
|
553
|
+
# @deprecated Use {.get_session_info} with +session_store:+. Removed in 1.0.
|
|
413
554
|
# @return [SDKSessionInfo, nil]
|
|
414
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, ...)')
|
|
415
557
|
Sessions.get_session_info_from_store(session_store: session_store, session_id: session_id, directory: directory)
|
|
416
558
|
end
|
|
417
559
|
|
|
418
|
-
#
|
|
560
|
+
# @deprecated Use {.get_session_messages} with +session_store:+. Removed in 1.0.
|
|
419
561
|
# @return [Array<SessionMessage>]
|
|
420
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, ...)')
|
|
421
564
|
Sessions.get_session_messages_from_store(session_store: session_store, session_id: session_id,
|
|
422
565
|
directory: directory, limit: limit, offset: offset)
|
|
423
566
|
end
|
|
424
567
|
|
|
425
|
-
#
|
|
568
|
+
# @deprecated Use {.list_subagents} with +session_store:+. Removed in 1.0.
|
|
426
569
|
# @return [Array<String>]
|
|
427
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, ...)')
|
|
428
572
|
Sessions.list_subagents_from_store(session_store: session_store, session_id: session_id, directory: directory)
|
|
429
573
|
end
|
|
430
574
|
|
|
431
|
-
#
|
|
575
|
+
# @deprecated Use {.get_subagent_metadata} with +session_store:+. Removed in 1.0.
|
|
432
576
|
# @return [Hash{String => Object}, nil]
|
|
433
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, ...)')
|
|
434
579
|
Sessions.get_subagent_metadata_from_store(session_store: session_store, session_id: session_id,
|
|
435
580
|
agent_id: agent_id, directory: directory)
|
|
436
581
|
end
|
|
437
582
|
|
|
438
|
-
#
|
|
583
|
+
# @deprecated Use {.get_subagent_messages} with +session_store:+. Removed in 1.0.
|
|
439
584
|
# @return [Array<SessionMessage>]
|
|
440
585
|
def self.get_subagent_messages_from_store(session_store:, session_id:, agent_id:, directory: nil, limit: nil,
|
|
441
586
|
offset: 0)
|
|
587
|
+
Deprecation.warn_once(:get_subagent_messages_from_store, 'get_subagent_messages(session_store: store, ...)')
|
|
442
588
|
Sessions.get_subagent_messages_from_store(session_store: session_store, session_id: session_id,
|
|
443
589
|
agent_id: agent_id, directory: directory, limit: limit, offset: offset)
|
|
444
590
|
end
|
|
445
591
|
|
|
446
|
-
#
|
|
447
|
-
# rename_session). Appends a custom-title entry carrying a fresh uuid +
|
|
448
|
-
# timestamp via SessionStore#append.
|
|
449
|
-
# @raise [ArgumentError] if session_id is invalid or title is empty
|
|
450
|
-
# @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.
|
|
451
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, ...)')
|
|
452
595
|
SessionMutations.rename_session_via_store(session_store: session_store, session_id: session_id,
|
|
453
596
|
title: title, directory: directory)
|
|
454
597
|
end
|
|
455
598
|
|
|
456
|
-
#
|
|
457
|
-
# Pass nil to clear the tag.
|
|
458
|
-
# @raise [ArgumentError] if session_id is invalid or tag is empty after sanitization
|
|
459
|
-
# @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.
|
|
460
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, ...)')
|
|
461
602
|
SessionMutations.tag_session_via_store(session_store: session_store, session_id: session_id,
|
|
462
603
|
tag: tag, directory: directory)
|
|
463
604
|
end
|
|
464
605
|
|
|
465
|
-
#
|
|
466
|
-
# delete_session). No-op when the store does not implement #delete
|
|
467
|
-
# (WORM/append-only backends).
|
|
468
|
-
# @raise [ArgumentError] if session_id is invalid
|
|
606
|
+
# @deprecated Use {.delete_session} with +session_store:+. Removed in 1.0.
|
|
469
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, ...)')
|
|
470
609
|
SessionMutations.delete_session_via_store(session_store: session_store, session_id: session_id,
|
|
471
610
|
directory: directory)
|
|
472
611
|
end
|
|
473
612
|
|
|
474
|
-
#
|
|
475
|
-
#
|
|
476
|
-
# @return [ForkSessionResult] result containing the new session ID
|
|
477
|
-
# @raise [ArgumentError] if session_id/up_to_message_id is invalid or there are no messages
|
|
478
|
-
# @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]
|
|
479
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, ...)')
|
|
480
617
|
SessionMutations.fork_session_via_store(session_store: session_store, session_id: session_id,
|
|
481
618
|
directory: directory, up_to_message_id: up_to_message_id, title: title)
|
|
482
619
|
end
|
|
@@ -698,6 +835,51 @@ module ClaudeAgentSDK
|
|
|
698
835
|
end).wait
|
|
699
836
|
end
|
|
700
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
|
+
|
|
701
883
|
# Client for bidirectional, interactive conversations with Claude Code
|
|
702
884
|
#
|
|
703
885
|
# This client provides full control over the conversation flow with support
|
|
@@ -967,6 +1149,12 @@ module ClaudeAgentSDK
|
|
|
967
1149
|
@query_handler.set_permission_mode(mode)
|
|
968
1150
|
end
|
|
969
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
|
+
|
|
970
1158
|
# Change the AI model during conversation
|
|
971
1159
|
# @param model [String, nil] Model name or nil for default
|
|
972
1160
|
def set_model(model)
|
|
@@ -974,6 +1162,11 @@ module ClaudeAgentSDK
|
|
|
974
1162
|
@query_handler.set_model(model)
|
|
975
1163
|
end
|
|
976
1164
|
|
|
1165
|
+
# Ruby-style spelling of #set_model: `client.model = 'claude-opus-5'`.
|
|
1166
|
+
def model=(model)
|
|
1167
|
+
set_model(model)
|
|
1168
|
+
end
|
|
1169
|
+
|
|
977
1170
|
# Reconnect a failed MCP server
|
|
978
1171
|
# @param server_name [String] Name of the MCP server to reconnect
|
|
979
1172
|
def reconnect_mcp_server(server_name)
|
|
@@ -1045,6 +1238,12 @@ module ClaudeAgentSDK
|
|
|
1045
1238
|
@query_handler.get_context_usage
|
|
1046
1239
|
end
|
|
1047
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
|
+
|
|
1048
1247
|
# Get current MCP server connection status (only works with streaming mode)
|
|
1049
1248
|
# @return [Hash] MCP status information, including mcpServers list
|
|
1050
1249
|
def get_mcp_status
|
|
@@ -1052,6 +1251,12 @@ module ClaudeAgentSDK
|
|
|
1052
1251
|
@query_handler.get_mcp_status
|
|
1053
1252
|
end
|
|
1054
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
|
+
|
|
1055
1260
|
# Get server initialization info including available commands and output styles
|
|
1056
1261
|
# @return [Hash] Server info
|
|
1057
1262
|
def get_server_info
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
|
|
5
|
+
module ClaudeAgentSDK
|
|
6
|
+
module Generators
|
|
7
|
+
# `bin/rails generate claude_agent_sdk:install` — writes the initializer,
|
|
8
|
+
# ignores the vendored CLI, and prints the next steps. Lives under
|
|
9
|
+
# lib/generators/ so Rails' generator lookup finds it by namespace.
|
|
10
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
11
|
+
# Explicit: Thor derives the namespace by snake-casing the class path,
|
|
12
|
+
# which turns ClaudeAgentSDK into "claude_agent_s_d_k".
|
|
13
|
+
namespace 'claude_agent_sdk:install'
|
|
14
|
+
source_root File.expand_path('templates', __dir__)
|
|
15
|
+
|
|
16
|
+
desc 'Creates config/initializers/claude_agent_sdk.rb and git-ignores the vendored Claude Code CLI.'
|
|
17
|
+
|
|
18
|
+
GITIGNORE_ENTRY = '/vendor/claude/'
|
|
19
|
+
# Spellings that already ignore the vendored CLI directory.
|
|
20
|
+
GITIGNORE_PATTERN = %r{\A/?vendor/claude/?\z}
|
|
21
|
+
|
|
22
|
+
def create_initializer
|
|
23
|
+
template 'claude_agent_sdk.rb.tt', 'config/initializers/claude_agent_sdk.rb'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def ignore_vendored_cli
|
|
27
|
+
path = File.join(destination_root, '.gitignore')
|
|
28
|
+
entry = "# Claude Code CLI vendored by `bin/rails claude_agent_sdk:install_cli`\n#{GITIGNORE_ENTRY}\n"
|
|
29
|
+
return create_file('.gitignore', entry) unless File.exist?(path)
|
|
30
|
+
|
|
31
|
+
content = File.read(path)
|
|
32
|
+
if content.each_line.any? { |line| line.strip.match?(GITIGNORE_PATTERN) }
|
|
33
|
+
say_status :identical, '.gitignore (already ignores vendor/claude)', :blue
|
|
34
|
+
else
|
|
35
|
+
append_to_file '.gitignore', gitignore_separator(content) + entry
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def show_next_steps
|
|
40
|
+
say <<~MSG
|
|
41
|
+
|
|
42
|
+
Next steps:
|
|
43
|
+
1. Install the Claude Code CLI this gem is tested with into vendor/claude
|
|
44
|
+
(run it in your Dockerfile / bin/setup as well):
|
|
45
|
+
bin/rails claude_agent_sdk:install_cli
|
|
46
|
+
2. Provide credentials to the CLI, e.g. ANTHROPIC_API_KEY in the environment.
|
|
47
|
+
3. Review config/initializers/claude_agent_sdk.rb.
|
|
48
|
+
|
|
49
|
+
Rails guide: https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/docs/rails.md
|
|
50
|
+
MSG
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Start the appended block on its own line, after a blank one.
|
|
56
|
+
def gitignore_separator(content)
|
|
57
|
+
return '' if content.empty?
|
|
58
|
+
|
|
59
|
+
content.end_with?("\n") ? "\n" : "\n\n"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Claude Agent SDK defaults, merged into every ClaudeAgentSDK.query and
|
|
4
|
+
# ClaudeAgentSDK::Client session; per-call ClaudeAgentOptions override them.
|
|
5
|
+
# Guide: https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/docs/rails.md
|
|
6
|
+
|
|
7
|
+
# Uncomment together with `observers:` below (needs the opentelemetry-sdk gem).
|
|
8
|
+
# require 'claude_agent_sdk/instrumentation'
|
|
9
|
+
|
|
10
|
+
ClaudeAgentSDK.configure do |config|
|
|
11
|
+
config.default_options = {
|
|
12
|
+
# Model: full ID or alias ('opus', 'sonnet', 'haiku'). Unset, the CLI picks.
|
|
13
|
+
# model: 'claude-sonnet-5',
|
|
14
|
+
# model: 'claude-opus-5',
|
|
15
|
+
# model: 'claude-haiku-4-5',
|
|
16
|
+
|
|
17
|
+
# Tool permissions: 'default', 'acceptEdits', 'plan', 'dontAsk',
|
|
18
|
+
# 'bypassPermissions' (unattended jobs in a sandbox only), or 'auto'.
|
|
19
|
+
# permission_mode: 'default',
|
|
20
|
+
|
|
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
|
+
|
|
25
|
+
# OpenTelemetry tracing (Langfuse, Honeycomb, ...). A factory lambda gives
|
|
26
|
+
# every query/session its own observer — safe under Puma and job threads.
|
|
27
|
+
# observers: [-> { ClaudeAgentSDK::Instrumentation::OTelObserver.new }],
|
|
28
|
+
|
|
29
|
+
# Wraps every SDK callback (tool handlers, hooks, permission callbacks,
|
|
30
|
+
# message blocks, observers) so ActiveRecord connections go back to the
|
|
31
|
+
# pool. Not a bare `Rails.application.executor.wrap`: that deadlocks with
|
|
32
|
+
# development code reloading. See docs/rails.md ("callback_wrapper").
|
|
33
|
+
callback_wrapper: ClaudeAgentSDK::Railtie.callback_wrapper
|
|
34
|
+
}
|
|
35
|
+
end
|