claude-agent-sdk 0.23.0 → 0.25.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 +20 -0
- data/README.md +2 -2
- data/docs/hooks-and-permissions.md +22 -1
- data/docs/rails.md +46 -1
- data/docs/types.md +32 -11
- data/lib/claude_agent_sdk/command_builder.rb +14 -3
- data/lib/claude_agent_sdk/fiber_boundary.rb +84 -8
- data/lib/claude_agent_sdk/option_warnings.rb +113 -0
- data/lib/claude_agent_sdk/query.rb +150 -18
- data/lib/claude_agent_sdk/sdk_mcp_server.rb +38 -7
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +88 -6
- data/lib/claude_agent_sdk/types.rb +52 -3
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +82 -24
- metadata +3 -2
data/lib/claude_agent_sdk.rb
CHANGED
|
@@ -18,6 +18,7 @@ require_relative 'claude_agent_sdk/transcript_mirror_batcher'
|
|
|
18
18
|
require_relative 'claude_agent_sdk/session_resume'
|
|
19
19
|
require_relative 'claude_agent_sdk/session_mutations'
|
|
20
20
|
require_relative 'claude_agent_sdk/fiber_boundary'
|
|
21
|
+
require_relative 'claude_agent_sdk/option_warnings'
|
|
21
22
|
require 'async'
|
|
22
23
|
require 'securerandom'
|
|
23
24
|
|
|
@@ -82,10 +83,10 @@ module ClaudeAgentSDK
|
|
|
82
83
|
# Safely call a method on each observer, suppressing any errors.
|
|
83
84
|
# Each observer is invoked through FiberBoundary so that user code runs
|
|
84
85
|
# on a plain thread (no Fiber scheduler) even when called from inside
|
|
85
|
-
# the SDK's Async reactor.
|
|
86
|
-
def self.notify_observers(observers, method, *args)
|
|
86
|
+
# the SDK's Async reactor — or in place when scheduling is :inline.
|
|
87
|
+
def self.notify_observers(observers, method, *args, scheduling: :thread)
|
|
87
88
|
observers.each do |obs|
|
|
88
|
-
FiberBoundary.invoke { obs.send(method, *args) }
|
|
89
|
+
FiberBoundary.invoke(scheduling: scheduling) { obs.send(method, *args) }
|
|
89
90
|
rescue StandardError, ScriptError
|
|
90
91
|
# ScriptError too: NotImplementedError < ScriptError (not
|
|
91
92
|
# StandardError), and a stubbed observer must never mask the original
|
|
@@ -94,6 +95,45 @@ module ClaudeAgentSDK
|
|
|
94
95
|
end
|
|
95
96
|
end
|
|
96
97
|
|
|
98
|
+
# Public escape hatch for hosts running with callback_scheduling: :inline:
|
|
99
|
+
# run a heavy piece of a callback on a plain thread instead of the shared
|
|
100
|
+
# reactor fiber. What that buys, precisely:
|
|
101
|
+
# - scheduler-opaque BLOCKING that releases the GVL (native DB drivers,
|
|
102
|
+
# file/socket calls the scheduler can't see): the reactor keeps running.
|
|
103
|
+
# - pure-Ruby CPU-bound work: degrades a hard reactor stall into GVL
|
|
104
|
+
# time-slicing — added latency for other fibers, not starvation.
|
|
105
|
+
# - a C extension that HOLDS the GVL for the whole computation: no help;
|
|
106
|
+
# nothing in-process can protect the reactor from that — move such work
|
|
107
|
+
# to a subprocess.
|
|
108
|
+
# No-op outside a Fiber scheduler, so it is safe to call unconditionally.
|
|
109
|
+
# Returns the block's value; exceptions propagate.
|
|
110
|
+
#
|
|
111
|
+
# @example Inside an inline-mode tool handler
|
|
112
|
+
# ClaudeAgentSDK.offload { blocking_db_call }
|
|
113
|
+
def self.offload(&block)
|
|
114
|
+
FiberBoundary.invoke(&block)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Internal: warn once per process when :inline callback scheduling is
|
|
118
|
+
# enabled while ActiveSupport reports thread isolation — the host then
|
|
119
|
+
# almost certainly violates inline mode's fiber-isolation precondition
|
|
120
|
+
# (solid_queue fiber workers require isolation_level = :fiber).
|
|
121
|
+
# defined? probing only; the SDK never loads ActiveSupport itself.
|
|
122
|
+
def self.check_inline_isolation(scheduling)
|
|
123
|
+
return unless scheduling == :inline
|
|
124
|
+
return if @inline_isolation_warned
|
|
125
|
+
return unless defined?(ActiveSupport::IsolatedExecutionState)
|
|
126
|
+
return unless ActiveSupport::IsolatedExecutionState.isolation_level == :thread
|
|
127
|
+
|
|
128
|
+
@inline_isolation_warned = true
|
|
129
|
+
warn 'ClaudeAgentSDK: callback_scheduling: :inline is enabled but ' \
|
|
130
|
+
'ActiveSupport::IsolatedExecutionState.isolation_level is :thread. ' \
|
|
131
|
+
'Inline callbacks run on reactor fibers that share one thread, so ' \
|
|
132
|
+
'thread-keyed Rails state will leak across fibers. Set ' \
|
|
133
|
+
'isolation_level = :fiber (as solid_queue fiber workers require) ' \
|
|
134
|
+
'or use the default callback_scheduling: :thread.'
|
|
135
|
+
end
|
|
136
|
+
|
|
97
137
|
# Extract the user-visible prompt text from a streamed input item, or nil
|
|
98
138
|
# when there is none (non-user messages, tool_result-only content, …).
|
|
99
139
|
# Only Hash and JSON-string items are inspected; arbitrary objects written
|
|
@@ -147,13 +187,13 @@ module ClaudeAgentSDK
|
|
|
147
187
|
# Wrap a streaming-input enumerable so observers get on_user_prompt for
|
|
148
188
|
# each user message before it is written to stdin. Identity when no
|
|
149
189
|
# observers are configured.
|
|
150
|
-
def self.observing_prompt_stream(prompt, observers)
|
|
190
|
+
def self.observing_prompt_stream(prompt, observers, scheduling: :thread)
|
|
151
191
|
return prompt if observers.empty?
|
|
152
192
|
|
|
153
193
|
Enumerator.new do |yielder|
|
|
154
194
|
prompt.each do |message|
|
|
155
195
|
text = extract_user_prompt_text(message)
|
|
156
|
-
notify_observers(observers, :on_user_prompt, text) if text
|
|
196
|
+
notify_observers(observers, :on_user_prompt, text, scheduling: scheduling) if text
|
|
157
197
|
yielder << message
|
|
158
198
|
end
|
|
159
199
|
end
|
|
@@ -411,12 +451,20 @@ module ClaudeAgentSDK
|
|
|
411
451
|
configured_options = options.dup_with(permission_prompt_tool_name: 'stdio')
|
|
412
452
|
end
|
|
413
453
|
|
|
454
|
+
# Advisory: warn if other options shadow the can_use_tool callback.
|
|
455
|
+
# After the ArgumentError validations so invalid configs raise, not warn.
|
|
456
|
+
OptionWarnings.warn_if_can_use_tool_shadowed(options)
|
|
457
|
+
|
|
414
458
|
# Fail fast on invalid session_store combinations before spawning the CLI.
|
|
415
459
|
SessionStores.validate_session_store_options(configured_options)
|
|
416
460
|
|
|
417
461
|
# Resolve callable observers into fresh instances (thread-safe for global defaults)
|
|
418
462
|
resolved_observers = ClaudeAgentSDK.resolve_observers(configured_options.observers)
|
|
419
463
|
|
|
464
|
+
# Where user callbacks run (see ClaudeAgentOptions#callback_scheduling).
|
|
465
|
+
callback_scheduling = configured_options.callback_scheduling || :thread
|
|
466
|
+
ClaudeAgentSDK.check_inline_isolation(callback_scheduling)
|
|
467
|
+
|
|
420
468
|
raise ArgumentError, 'transport must respond to #connect (see ClaudeAgentSDK::Transport)' if transport && !transport.respond_to?(:connect)
|
|
421
469
|
|
|
422
470
|
Async do
|
|
@@ -476,7 +524,8 @@ module ClaudeAgentSDK
|
|
|
476
524
|
agents: configured_options.agents,
|
|
477
525
|
sdk_mcp_servers: sdk_mcp_servers,
|
|
478
526
|
exclude_dynamic_sections: ClaudeAgentSDK.extract_exclude_dynamic_sections(configured_options.system_prompt),
|
|
479
|
-
skills: configured_options.skills
|
|
527
|
+
skills: configured_options.skills,
|
|
528
|
+
callback_scheduling: callback_scheduling
|
|
480
529
|
)
|
|
481
530
|
|
|
482
531
|
# Mirror transcripts to the session_store, if configured. Installed
|
|
@@ -500,7 +549,7 @@ module ClaudeAgentSDK
|
|
|
500
549
|
|
|
501
550
|
# Send prompt(s) as user messages, then close stdin
|
|
502
551
|
if prompt.is_a?(String)
|
|
503
|
-
ClaudeAgentSDK.notify_observers(resolved_observers, :on_user_prompt, prompt)
|
|
552
|
+
ClaudeAgentSDK.notify_observers(resolved_observers, :on_user_prompt, prompt, scheduling: callback_scheduling)
|
|
504
553
|
message = {
|
|
505
554
|
type: 'user',
|
|
506
555
|
message: { role: 'user', content: prompt },
|
|
@@ -518,19 +567,20 @@ module ClaudeAgentSDK
|
|
|
518
567
|
# here kept the root reactor alive forever when the read loop died
|
|
519
568
|
# while the user enumerator was still blocked (matches Python's
|
|
520
569
|
# query.spawn_task(query.stream_input(prompt))).
|
|
521
|
-
observed_prompt = ClaudeAgentSDK.observing_prompt_stream(prompt, resolved_observers)
|
|
570
|
+
observed_prompt = ClaudeAgentSDK.observing_prompt_stream(prompt, resolved_observers, scheduling: callback_scheduling)
|
|
522
571
|
query_handler.spawn_task { query_handler.stream_input(observed_prompt) }
|
|
523
572
|
end
|
|
524
573
|
|
|
525
574
|
# Read and yield messages from the query handler (filters out control messages).
|
|
526
575
|
# User block is invoked through FiberBoundary so ActiveRecord / PG calls
|
|
527
|
-
# inside it don't see the async gem's Fiber scheduler
|
|
576
|
+
# inside it don't see the async gem's Fiber scheduler (default :thread
|
|
577
|
+
# mode; :inline runs it in place on the reactor fiber).
|
|
528
578
|
query_handler.receive_messages do |data|
|
|
529
579
|
message = MessageParser.parse(data)
|
|
530
580
|
next unless message
|
|
531
581
|
|
|
532
|
-
ClaudeAgentSDK.notify_observers(resolved_observers, :on_message, message)
|
|
533
|
-
signal = FiberBoundary.invoke_iteration(block, message)
|
|
582
|
+
ClaudeAgentSDK.notify_observers(resolved_observers, :on_message, message, scheduling: callback_scheduling)
|
|
583
|
+
signal = FiberBoundary.invoke_iteration(block, message, scheduling: callback_scheduling)
|
|
534
584
|
break signal.value if signal.is_a?(FiberBoundary::Break)
|
|
535
585
|
end
|
|
536
586
|
rescue StandardError => e
|
|
@@ -539,10 +589,10 @@ module ClaudeAgentSDK
|
|
|
539
589
|
# parse errors, and user-block errors. StandardError only: Async::Stop
|
|
540
590
|
# is cancellation, not an error. Bare raise preserves the backtrace;
|
|
541
591
|
# the ensure below still fires on_close after on_error.
|
|
542
|
-
ClaudeAgentSDK.notify_observers(resolved_observers, :on_error, e)
|
|
592
|
+
ClaudeAgentSDK.notify_observers(resolved_observers, :on_error, e, scheduling: callback_scheduling)
|
|
543
593
|
raise
|
|
544
594
|
ensure
|
|
545
|
-
ClaudeAgentSDK.notify_observers(resolved_observers, :on_close)
|
|
595
|
+
ClaudeAgentSDK.notify_observers(resolved_observers, :on_close, scheduling: callback_scheduling)
|
|
546
596
|
# query_handler.close stops the background read task and closes the
|
|
547
597
|
# transport (flushing the mirror batcher first). Fall back to a bare
|
|
548
598
|
# transport close when the handler was never built.
|
|
@@ -612,6 +662,7 @@ module ClaudeAgentSDK
|
|
|
612
662
|
# @param transport_args [Hash] Additional keyword arguments passed to transport_class.new(options, **transport_args)
|
|
613
663
|
def initialize(options: nil, transport_class: SubprocessCLITransport, transport_args: {})
|
|
614
664
|
@options = options || ClaudeAgentOptions.new
|
|
665
|
+
@callback_scheduling = @options.callback_scheduling || :thread
|
|
615
666
|
@transport_class = transport_class
|
|
616
667
|
@transport_args = transport_args
|
|
617
668
|
@transport = nil
|
|
@@ -680,6 +731,10 @@ module ClaudeAgentSDK
|
|
|
680
731
|
configured_options = @options.dup_with(permission_prompt_tool_name: 'stdio')
|
|
681
732
|
end
|
|
682
733
|
|
|
734
|
+
# Advisory: warn if other options shadow the can_use_tool callback.
|
|
735
|
+
# After the ArgumentError validations so invalid configs raise, not warn.
|
|
736
|
+
OptionWarnings.warn_if_can_use_tool_shadowed(@options)
|
|
737
|
+
|
|
683
738
|
# Fail fast on invalid session_store combinations before spawning the CLI.
|
|
684
739
|
# Configuration validation is a usage error, like the ArgumentErrors
|
|
685
740
|
# above — deliberately outside the on_error notify scope.
|
|
@@ -690,6 +745,8 @@ module ClaudeAgentSDK
|
|
|
690
745
|
# notified via on_error.
|
|
691
746
|
@resolved_observers = ClaudeAgentSDK.resolve_observers(@options.observers)
|
|
692
747
|
|
|
748
|
+
ClaudeAgentSDK.check_inline_isolation(@callback_scheduling)
|
|
749
|
+
|
|
693
750
|
# If anything from materialization onward fails, tear down (closes the
|
|
694
751
|
# subprocess and removes the materialized temp config dir) before
|
|
695
752
|
# surfacing the error, so a partial connect never leaks a temp dir
|
|
@@ -742,7 +799,7 @@ module ClaudeAgentSDK
|
|
|
742
799
|
|
|
743
800
|
begin
|
|
744
801
|
if prompt.is_a?(String)
|
|
745
|
-
ClaudeAgentSDK.notify_observers(@resolved_observers, :on_user_prompt, prompt)
|
|
802
|
+
ClaudeAgentSDK.notify_observers(@resolved_observers, :on_user_prompt, prompt, scheduling: @callback_scheduling)
|
|
746
803
|
message = {
|
|
747
804
|
type: 'user',
|
|
748
805
|
message: { role: 'user', content: prompt },
|
|
@@ -783,8 +840,8 @@ module ClaudeAgentSDK
|
|
|
783
840
|
message = MessageParser.parse(data)
|
|
784
841
|
next unless message
|
|
785
842
|
|
|
786
|
-
ClaudeAgentSDK.notify_observers(@resolved_observers, :on_message, message)
|
|
787
|
-
signal = FiberBoundary.invoke_iteration(block, message)
|
|
843
|
+
ClaudeAgentSDK.notify_observers(@resolved_observers, :on_message, message, scheduling: @callback_scheduling)
|
|
844
|
+
signal = FiberBoundary.invoke_iteration(block, message, scheduling: @callback_scheduling)
|
|
788
845
|
break signal.value if signal.is_a?(FiberBoundary::Break)
|
|
789
846
|
end
|
|
790
847
|
rescue StandardError => e
|
|
@@ -809,8 +866,8 @@ module ClaudeAgentSDK
|
|
|
809
866
|
message = MessageParser.parse(data)
|
|
810
867
|
next unless message
|
|
811
868
|
|
|
812
|
-
ClaudeAgentSDK.notify_observers(@resolved_observers, :on_message, message)
|
|
813
|
-
signal = FiberBoundary.invoke_iteration(block, message)
|
|
869
|
+
ClaudeAgentSDK.notify_observers(@resolved_observers, :on_message, message, scheduling: @callback_scheduling)
|
|
870
|
+
signal = FiberBoundary.invoke_iteration(block, message, scheduling: @callback_scheduling)
|
|
814
871
|
break signal.value if signal.is_a?(FiberBoundary::Break)
|
|
815
872
|
break if message.is_a?(ResultMessage)
|
|
816
873
|
end
|
|
@@ -902,7 +959,7 @@ module ClaudeAgentSDK
|
|
|
902
959
|
|
|
903
960
|
# Disconnect from Claude
|
|
904
961
|
def disconnect
|
|
905
|
-
ClaudeAgentSDK.notify_observers(@resolved_observers || [], :on_close) if @connected
|
|
962
|
+
ClaudeAgentSDK.notify_observers(@resolved_observers || [], :on_close, scheduling: @callback_scheduling) if @connected
|
|
906
963
|
# Tear down whatever exists — robust to a partial/failed connect, where
|
|
907
964
|
# @connected is still false but a transport and/or materialized temp dir
|
|
908
965
|
# were already created. #close on the query handler also closes the
|
|
@@ -988,7 +1045,8 @@ module ClaudeAgentSDK
|
|
|
988
1045
|
sdk_mcp_servers: sdk_mcp_servers,
|
|
989
1046
|
agents: configured_options.agents,
|
|
990
1047
|
exclude_dynamic_sections: exclude_dynamic_sections,
|
|
991
|
-
skills: configured_options.skills
|
|
1048
|
+
skills: configured_options.skills,
|
|
1049
|
+
callback_scheduling: @callback_scheduling
|
|
992
1050
|
)
|
|
993
1051
|
|
|
994
1052
|
# Mirror transcripts to the session_store, if configured.
|
|
@@ -1019,7 +1077,7 @@ module ClaudeAgentSDK
|
|
|
1019
1077
|
# Observer#on_error contract; notifying a swallowed error would mark
|
|
1020
1078
|
# a still-live OTel trace as failed). Same behavior as query()'s
|
|
1021
1079
|
# streaming path.
|
|
1022
|
-
observed = ClaudeAgentSDK.observing_prompt_stream(prompt, @resolved_observers)
|
|
1080
|
+
observed = ClaudeAgentSDK.observing_prompt_stream(prompt, @resolved_observers, scheduling: @callback_scheduling)
|
|
1023
1081
|
@query_handler.spawn_task { @query_handler.stream_input(observed) }
|
|
1024
1082
|
end
|
|
1025
1083
|
end
|
|
@@ -1036,12 +1094,12 @@ module ClaudeAgentSDK
|
|
|
1036
1094
|
when Hash
|
|
1037
1095
|
msg = msg.merge(session_id: session_id) unless msg.key?(:session_id) || msg.key?('session_id')
|
|
1038
1096
|
if (text = ClaudeAgentSDK.extract_user_prompt_text(msg))
|
|
1039
|
-
ClaudeAgentSDK.notify_observers(@resolved_observers, :on_user_prompt, text)
|
|
1097
|
+
ClaudeAgentSDK.notify_observers(@resolved_observers, :on_user_prompt, text, scheduling: @callback_scheduling)
|
|
1040
1098
|
end
|
|
1041
1099
|
writeln(JSON.generate(msg))
|
|
1042
1100
|
when String
|
|
1043
1101
|
if (text = ClaudeAgentSDK.extract_user_prompt_text(msg))
|
|
1044
|
-
ClaudeAgentSDK.notify_observers(@resolved_observers, :on_user_prompt, text)
|
|
1102
|
+
ClaudeAgentSDK.notify_observers(@resolved_observers, :on_user_prompt, text, scheduling: @callback_scheduling)
|
|
1045
1103
|
end
|
|
1046
1104
|
writeln(msg)
|
|
1047
1105
|
else
|
|
@@ -1055,7 +1113,7 @@ module ClaudeAgentSDK
|
|
|
1055
1113
|
# Notify observers of an error surfacing to the consumer. `|| []` keeps a
|
|
1056
1114
|
# mis-scoped call before connect harmless instead of NoMethodError on nil.
|
|
1057
1115
|
def notify_error(error)
|
|
1058
|
-
ClaudeAgentSDK.notify_observers(@resolved_observers || [], :on_error, error)
|
|
1116
|
+
ClaudeAgentSDK.notify_observers(@resolved_observers || [], :on_error, error, scheduling: @callback_scheduling)
|
|
1059
1117
|
end
|
|
1060
1118
|
|
|
1061
1119
|
# Build and install the transcript-mirror batcher on the query handler when
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: claude-agent-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.25.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Community Contributors
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-31 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: async
|
|
@@ -129,6 +129,7 @@ files:
|
|
|
129
129
|
- lib/claude_agent_sdk/instrumentation/otel.rb
|
|
130
130
|
- lib/claude_agent_sdk/message_parser.rb
|
|
131
131
|
- lib/claude_agent_sdk/observer.rb
|
|
132
|
+
- lib/claude_agent_sdk/option_warnings.rb
|
|
132
133
|
- lib/claude_agent_sdk/query.rb
|
|
133
134
|
- lib/claude_agent_sdk/sdk_mcp_server.rb
|
|
134
135
|
- lib/claude_agent_sdk/session_mutations.rb
|