claude-agent-sdk 1.1.0 → 1.2.1
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/.yardopts +10 -0
- data/CHANGELOG.md +99 -0
- data/README.md +43 -31
- data/docs/cli-installer.md +26 -4
- data/docs/client.md +29 -11
- data/docs/configuration.md +164 -1
- data/docs/errors.md +32 -2
- data/docs/hooks-and-permissions.md +30 -10
- data/docs/mcp-servers.md +30 -9
- data/docs/observability.md +61 -10
- data/docs/options.md +232 -0
- data/docs/rails.md +263 -18
- data/docs/sessions.md +40 -12
- data/docs/subagents.md +1 -1
- data/docs/types.md +100 -11
- data/lib/claude_agent_sdk/cli_installer.rb +140 -19
- data/lib/claude_agent_sdk/command_builder.rb +83 -108
- data/lib/claude_agent_sdk/fiber_boundary.rb +45 -2
- data/lib/claude_agent_sdk/instrumentation/otel.rb +90 -28
- data/lib/claude_agent_sdk/option_forms.rb +379 -0
- data/lib/claude_agent_sdk/query/run_lifecycle.rb +373 -0
- data/lib/claude_agent_sdk/query.rb +266 -364
- data/lib/claude_agent_sdk/railtie.rb +27 -2
- data/lib/claude_agent_sdk/sdk_mcp_server.rb +78 -26
- data/lib/claude_agent_sdk/session_assembly.rb +311 -0
- data/lib/claude_agent_sdk/session_mutations.rb +112 -92
- data/lib/claude_agent_sdk/session_resume.rb +360 -43
- data/lib/claude_agent_sdk/session_store.rb +31 -2
- data/lib/claude_agent_sdk/sessions.rb +720 -138
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +228 -29
- data/lib/claude_agent_sdk/testing/session_store_conformance.rb +18 -7
- data/lib/claude_agent_sdk/transcript_mirror_batcher.rb +45 -37
- data/lib/claude_agent_sdk/transport.rb +28 -12
- data/lib/claude_agent_sdk/types/attributes.rb +9 -0
- data/lib/claude_agent_sdk/types/base.rb +85 -15
- data/lib/claude_agent_sdk/types/hooks.rb +73 -0
- data/lib/claude_agent_sdk/types/mcp.rb +37 -1
- data/lib/claude_agent_sdk/types/option_values.rb +186 -4
- data/lib/claude_agent_sdk/types/options.rb +35 -5
- data/lib/claude_agent_sdk/types/permissions.rb +18 -9
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +223 -418
- data/lib/generators/claude_agent_sdk/install/templates/claude_agent_sdk.rb.tt +6 -0
- data/sig/claude_agent_sdk/types/hooks.rbs +6 -3
- data/sig/claude_agent_sdk/types/option_values.rbs +23 -6
- data/sig/claude_agent_sdk/types/options.rbs +11 -7
- data/sig/claude_agent_sdk/types/permissions.rbs +4 -2
- metadata +9 -4
data/lib/claude_agent_sdk.rb
CHANGED
|
@@ -19,6 +19,8 @@ require_relative 'claude_agent_sdk/transcript_mirror_batcher'
|
|
|
19
19
|
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
|
+
require_relative 'claude_agent_sdk/session_assembly'
|
|
23
|
+
require_relative 'claude_agent_sdk/option_forms'
|
|
22
24
|
require_relative 'claude_agent_sdk/option_warnings'
|
|
23
25
|
require_relative 'claude_agent_sdk/deprecation'
|
|
24
26
|
# Rails apps only: Bundler.require runs after `require 'rails'`, so the
|
|
@@ -66,20 +68,14 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
66
68
|
# strips the instance from exactly these entries.
|
|
67
69
|
# @api private
|
|
68
70
|
def self.extract_sdk_mcp_servers(mcp_servers)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
servers = {}
|
|
72
|
-
mcp_servers.each do |name, config|
|
|
73
|
-
config = config.to_h if config.is_a?(Type)
|
|
74
|
-
next unless config.is_a?(Hash) && (config[:type] || config['type']).to_s == 'sdk'
|
|
75
|
-
|
|
76
|
-
servers[name] = config.key?(:instance) ? config[:instance] : config['instance']
|
|
77
|
-
end
|
|
78
|
-
servers
|
|
71
|
+
OptionForms.sdk_mcp_servers(mcp_servers)
|
|
79
72
|
end
|
|
80
73
|
|
|
81
74
|
# Internal: normalize hook lists for the control protocol. An absent or
|
|
82
|
-
# disabled event must not become an empty registration in initialize.
|
|
75
|
+
# disabled event must not become an empty registration in initialize. An
|
|
76
|
+
# event written as a String under one key and as a Symbol under another
|
|
77
|
+
# ('PreToolUse' and :PreToolUse) is one event: its matcher lists are
|
|
78
|
+
# joined in the order they were written, never replaced.
|
|
83
79
|
# @api private
|
|
84
80
|
def self.convert_hooks_to_internal_format(hooks)
|
|
85
81
|
return nil unless hooks
|
|
@@ -94,7 +90,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
94
90
|
config[:timeout] = matcher.timeout if matcher.timeout
|
|
95
91
|
entries << config
|
|
96
92
|
end
|
|
97
|
-
internal_hooks[event.to_s]
|
|
93
|
+
(internal_hooks[event.to_s] ||= []).concat(entries) unless entries.empty?
|
|
98
94
|
end
|
|
99
95
|
internal_hooks.empty? ? nil : internal_hooks
|
|
100
96
|
end
|
|
@@ -135,17 +131,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
135
131
|
# Shared by Client#connect and the one-shot query() path.
|
|
136
132
|
# @api private
|
|
137
133
|
def self.extract_exclude_dynamic_sections(system_prompt)
|
|
138
|
-
|
|
139
|
-
eds = system_prompt.exclude_dynamic_sections
|
|
140
|
-
return eds if [true, false].include?(eds)
|
|
141
|
-
elsif system_prompt.is_a?(Hash)
|
|
142
|
-
type = system_prompt[:type] || system_prompt['type']
|
|
143
|
-
if type == 'preset'
|
|
144
|
-
eds = system_prompt.fetch(:exclude_dynamic_sections) { system_prompt['exclude_dynamic_sections'] }
|
|
145
|
-
return eds if [true, false].include?(eds)
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
nil
|
|
134
|
+
OptionForms.exclude_dynamic_sections(system_prompt)
|
|
149
135
|
end
|
|
150
136
|
|
|
151
137
|
# Internal: pull snapshot out of a preset or custom system prompt for the
|
|
@@ -155,32 +141,46 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
155
141
|
# must not collapse it to nil. Shared by Client#connect and query().
|
|
156
142
|
# @api private
|
|
157
143
|
def self.extract_system_prompt_snapshot(system_prompt)
|
|
158
|
-
|
|
159
|
-
when SystemPromptPreset, SystemPromptCustom
|
|
160
|
-
snapshot = system_prompt.snapshot
|
|
161
|
-
return snapshot if [true, false].include?(snapshot)
|
|
162
|
-
when Hash
|
|
163
|
-
type = system_prompt[:type] || system_prompt['type']
|
|
164
|
-
if %w[preset custom].include?(type)
|
|
165
|
-
snapshot = system_prompt.fetch(:snapshot) { system_prompt['snapshot'] }
|
|
166
|
-
return snapshot if [true, false].include?(snapshot)
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
nil
|
|
144
|
+
OptionForms.system_prompt_snapshot(system_prompt)
|
|
170
145
|
end
|
|
171
146
|
|
|
172
147
|
# Safely call a method on each observer, suppressing any errors.
|
|
173
148
|
# Each observer is invoked through FiberBoundary so that user code runs
|
|
174
149
|
# on a plain thread (no Fiber scheduler) even when called from inside
|
|
175
150
|
# the SDK's Async reactor — or in place when scheduling is :inline.
|
|
151
|
+
#
|
|
152
|
+
# What the observer raises, and what its callback_wrapper raises, is
|
|
153
|
+
# contained where it happens: inside the hop, on the execution context
|
|
154
|
+
# the observer runs on. The rescue must not sit around the hop, because
|
|
155
|
+
# the calling fiber WAITS there, and an exception raised into a waiting
|
|
156
|
+
# fiber is not the observer's — a caller's `task.with_timeout` /
|
|
157
|
+
# `Timeout.timeout` deadline is delivered exactly that way. Rescued out
|
|
158
|
+
# here, an expired deadline was swallowed as if the observer had failed,
|
|
159
|
+
# and the turn carried on. So the wrapper is composed inside the rescued
|
|
160
|
+
# body rather than handed to FiberBoundary.invoke, which would run it
|
|
161
|
+
# outside that rescue.
|
|
162
|
+
#
|
|
163
|
+
# Known residue with `scheduling: :inline` (and with no scheduler at all):
|
|
164
|
+
# the observer runs on the calling fiber itself, so a deadline that lands
|
|
165
|
+
# while the observer is suspended is raised inside the observer's own
|
|
166
|
+
# frames, cannot be told from the observer's own timeout, and is still
|
|
167
|
+
# swallowed here.
|
|
176
168
|
# @api private
|
|
177
169
|
def self.notify_observers(observers, method, *args, scheduling: :thread, wrapper: nil)
|
|
178
170
|
observers.each do |obs|
|
|
179
|
-
FiberBoundary.invoke(scheduling: scheduling
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
171
|
+
FiberBoundary.invoke(scheduling: scheduling) do
|
|
172
|
+
invocation = proc { obs.send(method, *args) }
|
|
173
|
+
wrapper ? wrapper.call(invocation) : invocation.call
|
|
174
|
+
rescue StandardError, ScriptError
|
|
175
|
+
# ScriptError too: NotImplementedError < ScriptError (not
|
|
176
|
+
# StandardError), and a stubbed observer must never mask the original
|
|
177
|
+
# error being notified or abort connect/teardown cleanup.
|
|
178
|
+
nil
|
|
179
|
+
end
|
|
180
|
+
rescue ScriptError
|
|
181
|
+
# A ScriptError raised around the body rather than in it (the hop's own
|
|
182
|
+
# plumbing). No deadline is a ScriptError, so nothing a caller injects
|
|
183
|
+
# into the waiting fiber is caught here.
|
|
184
184
|
nil
|
|
185
185
|
end
|
|
186
186
|
end
|
|
@@ -690,164 +690,85 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
690
690
|
# Fail fast on invalid session_store combinations before spawning the CLI.
|
|
691
691
|
SessionStores.validate_session_store_options(configured_options)
|
|
692
692
|
|
|
693
|
-
# Resolve callable observers into fresh instances (thread-safe for global
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
#
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
ClaudeAgentSDK.check_inline_isolation(
|
|
693
|
+
# Resolve callable observers into fresh instances (thread-safe for global
|
|
694
|
+
# defaults), bound to where user callbacks run (see
|
|
695
|
+
# ClaudeAgentOptions#callback_scheduling) and the middleware wrapped
|
|
696
|
+
# around them (#callback_wrapper).
|
|
697
|
+
dispatch = Dispatch.new(ClaudeAgentSDK.resolve_observers(configured_options.observers),
|
|
698
|
+
scheduling: configured_options.callback_scheduling || :thread,
|
|
699
|
+
wrapper: configured_options.callback_wrapper)
|
|
700
|
+
ClaudeAgentSDK.check_inline_isolation(dispatch.scheduling)
|
|
701
701
|
|
|
702
702
|
if transport && !transport.respond_to?(:connect)
|
|
703
703
|
raise ArgumentError, 'transport must respond to #connect (see ClaudeAgentSDK::Transport)'
|
|
704
704
|
end
|
|
705
705
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
#
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
#
|
|
739
|
-
query_handler =
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
system_prompt_snapshot: ClaudeAgentSDK.extract_system_prompt_snapshot(configured_options.system_prompt),
|
|
748
|
-
skills: configured_options.skills,
|
|
749
|
-
forward_subagent_text: configured_options.forward_subagent_text?,
|
|
750
|
-
agent_progress_summaries: configured_options.agent_progress_summaries,
|
|
751
|
-
callback_scheduling: callback_scheduling,
|
|
752
|
-
callback_wrapper: callback_wrapper,
|
|
753
|
-
verbatim_prompts: configured_options.verbatim_prompts?,
|
|
754
|
-
run_end_ceiling_ms: Query.run_end_ceiling_ms(configured_options.env)
|
|
755
|
-
)
|
|
756
|
-
|
|
757
|
-
# Mirror transcripts to the session_store, if configured. Installed
|
|
758
|
-
# before #start so the read loop captures transcript_mirror frames.
|
|
759
|
-
if configured_options.session_store
|
|
760
|
-
query_handler.set_transcript_mirror_batcher(
|
|
761
|
-
SessionResume.build_mirror_batcher(
|
|
762
|
-
store: configured_options.session_store,
|
|
763
|
-
env: configured_options.env,
|
|
764
|
-
on_error: ->(key, message) { query_handler.report_mirror_error(key, message) },
|
|
765
|
-
eager: configured_options.session_store_flush.to_s == 'eager',
|
|
766
|
-
callback_wrapper: callback_wrapper
|
|
767
|
-
)
|
|
768
|
-
)
|
|
769
|
-
end
|
|
770
|
-
|
|
771
|
-
# Start reading messages in background
|
|
772
|
-
query_handler.start
|
|
773
|
-
|
|
774
|
-
# Initialize the control protocol (sends agents)
|
|
775
|
-
query_handler.initialize_protocol
|
|
776
|
-
|
|
777
|
-
# Send prompt(s) as user messages, then close stdin
|
|
778
|
-
if prompt.is_a?(String)
|
|
779
|
-
ClaudeAgentSDK.notify_observers(resolved_observers, :on_user_prompt, prompt,
|
|
780
|
-
scheduling: callback_scheduling, wrapper: callback_wrapper)
|
|
781
|
-
message = {
|
|
782
|
-
type: 'user',
|
|
783
|
-
message: { role: 'user', content: prompt },
|
|
784
|
-
parent_tool_use_id: nil,
|
|
785
|
-
session_id: ''
|
|
786
|
-
}
|
|
787
|
-
transport.write("#{Query.serialize_user_message(message, configured_options.verbatim_prompts?)}\n")
|
|
788
|
-
# Background-spawn so messages stream to the user block while stdin
|
|
789
|
-
# close waits (without timeout) for the first result; a synchronous
|
|
790
|
-
# call would defer all delivery until the turn completes (mirrors
|
|
791
|
-
# Python's query.spawn_task(query.wait_for_result_and_end_input())).
|
|
792
|
-
query_handler.spawn_task { query_handler.wait_for_result_and_end_input }
|
|
793
|
-
elsif prompt.is_a?(Enumerator) || prompt.respond_to?(:each)
|
|
794
|
-
# Tracked on the Query so close() stops it; an untracked Async task
|
|
795
|
-
# here kept the root reactor alive forever when the read loop died
|
|
796
|
-
# while the user enumerator was still blocked (matches Python's
|
|
797
|
-
# query.spawn_task(query.stream_input(prompt))).
|
|
798
|
-
observed_prompt = ClaudeAgentSDK.observing_prompt_stream(
|
|
799
|
-
prompt, resolved_observers, scheduling: callback_scheduling, wrapper: callback_wrapper
|
|
800
|
-
)
|
|
801
|
-
query_handler.spawn_task { query_handler.stream_input(observed_prompt) }
|
|
802
|
-
end
|
|
803
|
-
|
|
804
|
-
# Read and yield messages from the query handler (filters out control messages).
|
|
805
|
-
# User block is invoked through FiberBoundary so ActiveRecord / PG calls
|
|
806
|
-
# inside it don't see the async gem's Fiber scheduler (default :thread
|
|
807
|
-
# mode; :inline runs it in place on the reactor fiber).
|
|
808
|
-
query_handler.receive_messages do |data|
|
|
809
|
-
message = MessageParser.parse(data)
|
|
810
|
-
next unless message
|
|
811
|
-
|
|
812
|
-
ClaudeAgentSDK.notify_observers(resolved_observers, :on_message, message,
|
|
813
|
-
scheduling: callback_scheduling, wrapper: callback_wrapper)
|
|
814
|
-
signal = FiberBoundary.invoke_iteration(block, message, scheduling: callback_scheduling,
|
|
815
|
-
wrapper: callback_wrapper)
|
|
816
|
-
break signal.value if signal.is_a?(FiberBoundary::Break)
|
|
817
|
-
end
|
|
818
|
-
rescue StandardError => e
|
|
819
|
-
# One notify point for every error surfacing from query() — transport
|
|
820
|
-
# connect, initialize, stream errors re-raised from the message queue,
|
|
821
|
-
# parse errors, and user-block errors. StandardError only: Async::Stop
|
|
822
|
-
# is cancellation, not an error. Bare raise preserves the backtrace;
|
|
823
|
-
# the ensure below still fires on_close after on_error.
|
|
824
|
-
ClaudeAgentSDK.notify_observers(resolved_observers, :on_error, e,
|
|
825
|
-
scheduling: callback_scheduling, wrapper: callback_wrapper)
|
|
826
|
-
raise
|
|
827
|
-
ensure
|
|
828
|
-
ClaudeAgentSDK.notify_observers(resolved_observers, :on_close,
|
|
829
|
-
scheduling: callback_scheduling, wrapper: callback_wrapper)
|
|
830
|
-
# query_handler.close stops the background read task and closes the
|
|
831
|
-
# transport (flushing the mirror batcher first). Fall back to a bare
|
|
832
|
-
# transport close when the handler was never built.
|
|
833
|
-
begin
|
|
834
|
-
if query_handler
|
|
835
|
-
query_handler.close
|
|
836
|
-
elsif transport
|
|
837
|
-
transport.close
|
|
838
|
-
end
|
|
839
|
-
ensure
|
|
840
|
-
# Remove the materialized resume temp dir (which holds a redacted
|
|
841
|
-
# .credentials.json copy) AFTER the subprocess has exited, even when
|
|
842
|
-
# close itself raises — unless the mirror dropped batches: the store
|
|
843
|
-
# copy is then incomplete and the temp dir holds the only copy of
|
|
844
|
-
# the dropped turns, so it is preserved (scrubbed of credentials)
|
|
845
|
-
# with a warning instead of deleted.
|
|
846
|
-
if materialized
|
|
847
|
-
query_handler&.mirror_batches_dropped? ? materialized.preserve_transcripts : materialized.cleanup
|
|
848
|
-
end
|
|
849
|
-
end
|
|
706
|
+
# Acquires nothing yet. An injected transport is used as it is: it was
|
|
707
|
+
# built before a store-backed resume could be materialized for it, so
|
|
708
|
+
# none is (Python parity: client.py skips materialization when a
|
|
709
|
+
# transport is supplied). Without one the session constructs the
|
|
710
|
+
# subprocess transport itself, after materialization.
|
|
711
|
+
session = SessionAssembly.new(
|
|
712
|
+
configured_options,
|
|
713
|
+
dispatch: dispatch,
|
|
714
|
+
transport_source: transport ? { instance: transport } : { class: SubprocessCLITransport, args: {} }
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
# finished: false tells Async that a waiter handles this task's failure
|
|
718
|
+
# (the .wait at the end re-raises it), as Kernel#Sync does for its own
|
|
719
|
+
# task. Without it a task that fails before anyone waits for it — always
|
|
720
|
+
# the case outside a reactor, where Async runs the whole task before it
|
|
721
|
+
# returns — is also logged as "Task may have ended with unhandled
|
|
722
|
+
# exception", message and backtrace included, although the caller gets
|
|
723
|
+
# the same error raised and may well rescue it.
|
|
724
|
+
Async(finished: false, &FiberBoundary.capture_otel_context do
|
|
725
|
+
# Resume materialization, the transport's connect and the handshake
|
|
726
|
+
# all happen in here: inside the notified region.
|
|
727
|
+
session.connect
|
|
728
|
+
|
|
729
|
+
# Send prompt(s) as user messages, then close stdin
|
|
730
|
+
if prompt.is_a?(String)
|
|
731
|
+
dispatch.notify(:on_user_prompt, prompt)
|
|
732
|
+
# verbatim_prompts is read now, after the notification, from the
|
|
733
|
+
# options the session runs on.
|
|
734
|
+
session.write_prompt(prompt, session_id: '', verbatim: :current)
|
|
735
|
+
# Background-spawn so messages stream to the user block while stdin
|
|
736
|
+
# close waits (without timeout) for the first result; a synchronous
|
|
737
|
+
# call would defer all delivery until the turn completes (mirrors
|
|
738
|
+
# Python's query.spawn_task(query.wait_for_result_and_end_input())).
|
|
739
|
+
query_handler = session.query_handler
|
|
740
|
+
query_handler.spawn_task { query_handler.wait_for_result_and_end_input }
|
|
741
|
+
elsif prompt.is_a?(Enumerator) || prompt.respond_to?(:each)
|
|
742
|
+
# Tracked on the Query so close() stops it; an untracked Async task
|
|
743
|
+
# here kept the root reactor alive forever when the read loop died
|
|
744
|
+
# while the user enumerator was still blocked (matches Python's
|
|
745
|
+
# query.spawn_task(query.stream_input(prompt))).
|
|
746
|
+
session.stream_prompt_in_background(prompt)
|
|
850
747
|
end
|
|
748
|
+
|
|
749
|
+
# Read and yield messages from the query handler (filters out control messages).
|
|
750
|
+
# User block is invoked through FiberBoundary so ActiveRecord / PG calls
|
|
751
|
+
# inside it don't see the async gem's Fiber scheduler (default :thread
|
|
752
|
+
# mode; :inline runs it in place on the reactor fiber).
|
|
753
|
+
session.deliver(block)
|
|
754
|
+
rescue StandardError => e
|
|
755
|
+
# One notify point for every error surfacing from query() — transport
|
|
756
|
+
# connect, initialize, stream errors re-raised from the message queue,
|
|
757
|
+
# parse errors, and user-block errors. StandardError only: Async::Stop
|
|
758
|
+
# is cancellation, not an error. Bare raise preserves the backtrace;
|
|
759
|
+
# the ensure below still fires on_close after on_error.
|
|
760
|
+
dispatch.notify(:on_error, e)
|
|
761
|
+
raise
|
|
762
|
+
ensure
|
|
763
|
+
dispatch.notify(:on_close)
|
|
764
|
+
# Closing the query handler stops the background read task and closes
|
|
765
|
+
# the transport (flushing the mirror batcher first); a transport
|
|
766
|
+
# without a handler — its #connect raised, or the handshake never got
|
|
767
|
+
# that far — is closed on its own. That includes an injected one: a
|
|
768
|
+
# deliberate deviation from Python, which leaves it unclosed
|
|
769
|
+
# (Transport#close must be idempotent). The materialized resume temp
|
|
770
|
+
# dir is dealt with after that, even when the close raised.
|
|
771
|
+
session.close_resources(always_close_transport: false)
|
|
851
772
|
end).wait
|
|
852
773
|
end
|
|
853
774
|
|
|
@@ -903,16 +824,13 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
903
824
|
# The Client class always uses streaming mode for bidirectional communication.
|
|
904
825
|
#
|
|
905
826
|
# @example Basic usage
|
|
906
|
-
#
|
|
907
|
-
#
|
|
908
|
-
#
|
|
909
|
-
#
|
|
827
|
+
# # Client.open connects, yields the client and always disconnects,
|
|
828
|
+
# # also when the block raises.
|
|
829
|
+
# ClaudeAgentSDK::Client.open do |client|
|
|
910
830
|
# client.query("What is the capital of France?")
|
|
911
831
|
# client.receive_response do |msg|
|
|
912
832
|
# puts msg if msg.is_a?(ClaudeAgentSDK::AssistantMessage)
|
|
913
833
|
# end
|
|
914
|
-
#
|
|
915
|
-
# client.disconnect
|
|
916
834
|
# end
|
|
917
835
|
#
|
|
918
836
|
# @example With hooks
|
|
@@ -932,11 +850,13 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
932
850
|
# }
|
|
933
851
|
# )
|
|
934
852
|
# client = ClaudeAgentSDK::Client.new(options: options)
|
|
935
|
-
class Client
|
|
853
|
+
class Client
|
|
936
854
|
# The session's control-protocol handler (nil until #connect).
|
|
937
855
|
#
|
|
938
856
|
# @api private
|
|
939
|
-
|
|
857
|
+
def query_handler
|
|
858
|
+
@session&.query_handler
|
|
859
|
+
end
|
|
940
860
|
|
|
941
861
|
# @param options [ClaudeAgentOptions, nil] Configuration options
|
|
942
862
|
# @param transport_class [Class] Transport class to use (must implement Transport interface).
|
|
@@ -944,14 +864,17 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
944
864
|
# @param transport_args [Hash] Additional keyword arguments passed to transport_class.new(options, **transport_args)
|
|
945
865
|
def initialize(options: nil, transport_class: SubprocessCLITransport, transport_args: {})
|
|
946
866
|
@options = options || ClaudeAgentOptions.new
|
|
947
|
-
|
|
948
|
-
|
|
867
|
+
# One for the life of the client. Scheduling and wrapper are captured
|
|
868
|
+
# here; the observers are resolved, and put in, on each #connect.
|
|
869
|
+
@dispatch = Dispatch.new([], scheduling: @options.callback_scheduling || :thread,
|
|
870
|
+
wrapper: @options.callback_wrapper)
|
|
949
871
|
@transport_class = transport_class
|
|
950
872
|
@transport_args = transport_args
|
|
951
|
-
|
|
952
|
-
|
|
873
|
+
# Owns the transport, the query handler and the materialized resume dir
|
|
874
|
+
# of the current connection; nil while there is none. Kept after a
|
|
875
|
+
# disconnect that raised, until one completes (see #disconnect).
|
|
876
|
+
@session = nil
|
|
953
877
|
@connected = false
|
|
954
|
-
@materialized = nil
|
|
955
878
|
end
|
|
956
879
|
|
|
957
880
|
# Block-scoped Client lifecycle, mirroring Python's
|
|
@@ -1020,22 +943,29 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1020
943
|
# Resolve observers before the first failable runtime step so
|
|
1021
944
|
# connect-phase failures (including resume materialization) can be
|
|
1022
945
|
# notified via on_error.
|
|
1023
|
-
@
|
|
946
|
+
@dispatch.observers = ClaudeAgentSDK.resolve_observers(@options.observers)
|
|
947
|
+
|
|
948
|
+
ClaudeAgentSDK.check_inline_isolation(@dispatch.scheduling)
|
|
1024
949
|
|
|
1025
|
-
|
|
950
|
+
# Acquires nothing yet. Assigned before the begin, so the disconnect in
|
|
951
|
+
# the rescue below finds whatever the session went on to acquire. The
|
|
952
|
+
# transport is constructed by the session, after materialization.
|
|
953
|
+
@session = SessionAssembly.new(configured_options, dispatch: @dispatch,
|
|
954
|
+
transport_source: { class: @transport_class,
|
|
955
|
+
args: @transport_args })
|
|
1026
956
|
|
|
1027
957
|
# If anything from materialization onward fails, tear down (closes the
|
|
1028
958
|
# subprocess and removes the materialized temp config dir) before
|
|
1029
959
|
# surfacing the error, so a partial connect never leaks a temp dir
|
|
1030
960
|
# holding a credential copy.
|
|
1031
961
|
begin
|
|
1032
|
-
# Resume
|
|
1033
|
-
#
|
|
1034
|
-
#
|
|
1035
|
-
|
|
1036
|
-
|
|
962
|
+
# Resume materialization (store IO) happens in here too: inside the
|
|
963
|
+
# instrumented begin, so its failures fire on_error (matching the
|
|
964
|
+
# one-shot query() path) and disconnect cleans up.
|
|
965
|
+
@session.connect
|
|
966
|
+
@connected = true
|
|
1037
967
|
|
|
1038
|
-
|
|
968
|
+
send_initial_prompt(prompt)
|
|
1039
969
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
1040
970
|
# Pre-handshake failures (@connected still false) are notified here;
|
|
1041
971
|
# post-handshake String-prompt send failures were already notified by
|
|
@@ -1043,17 +973,22 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1043
973
|
# (The enumerator branch streams in the background and cannot raise
|
|
1044
974
|
# out of connect.) No on_close follows for pre-handshake failures
|
|
1045
975
|
# (disconnect gates it on @connected): the session never opened.
|
|
1046
|
-
notify_error(e) if e.is_a?(StandardError) && !@connected
|
|
1047
|
-
# Tear down the partial connect, but never let a cleanup failure (e.g. a
|
|
1048
|
-
# custom transport whose #close raises) mask the original connect error.
|
|
1049
|
-
# Rescue Exception (not StandardError) so reactor cancellation
|
|
1050
|
-
# (Async::Stop < Exception) after materialize_resume set @materialized
|
|
1051
|
-
# still runs disconnect -> @materialized.cleanup, never leaking the temp
|
|
1052
|
-
# CLAUDE_CONFIG_DIR that holds the redacted .credentials.json copy.
|
|
1053
976
|
begin
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
977
|
+
@dispatch.notify(:on_error, e) if e.is_a?(StandardError) && !@connected
|
|
978
|
+
ensure
|
|
979
|
+
# Tear down the partial connect, but never let a cleanup failure (e.g. a
|
|
980
|
+
# custom transport whose #close raises) mask the original connect error.
|
|
981
|
+
# Rescue Exception (not StandardError) so reactor cancellation
|
|
982
|
+
# (Async::Stop < Exception) after the resume was materialized still
|
|
983
|
+
# runs disconnect, which removes the temp CLAUDE_CONFIG_DIR holding
|
|
984
|
+
# the redacted .credentials.json copy.
|
|
985
|
+
# In an ensure: an exception raised into this fiber while it waits
|
|
986
|
+
# for the on_error observer (a caller's deadline) must not skip it.
|
|
987
|
+
begin
|
|
988
|
+
disconnect
|
|
989
|
+
rescue StandardError => cleanup_error
|
|
990
|
+
warn "Claude SDK: cleanup after failed connect raised: #{cleanup_error.message}"
|
|
991
|
+
end
|
|
1057
992
|
end
|
|
1058
993
|
raise
|
|
1059
994
|
end
|
|
@@ -1078,15 +1013,8 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1078
1013
|
|
|
1079
1014
|
begin
|
|
1080
1015
|
if prompt.is_a?(String)
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
message = {
|
|
1084
|
-
type: 'user',
|
|
1085
|
-
message: { role: 'user', content: prompt },
|
|
1086
|
-
parent_tool_use_id: nil,
|
|
1087
|
-
session_id: session_id
|
|
1088
|
-
}
|
|
1089
|
-
writeln(Query.serialize_user_message(message, @verbatim_prompts))
|
|
1016
|
+
@dispatch.notify(:on_user_prompt, prompt)
|
|
1017
|
+
@session.write_prompt(prompt, session_id: session_id)
|
|
1090
1018
|
elsif prompt.respond_to?(:each)
|
|
1091
1019
|
# Inline iteration on the caller, Python client.py parity — NOT
|
|
1092
1020
|
# Query#stream_input, whose ensure always ends input after
|
|
@@ -1098,7 +1026,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1098
1026
|
raise ArgumentError, "prompt must be a String or respond to #each (got #{prompt.class})"
|
|
1099
1027
|
end
|
|
1100
1028
|
rescue StandardError => e
|
|
1101
|
-
|
|
1029
|
+
@dispatch.notify(:on_error, e)
|
|
1102
1030
|
raise
|
|
1103
1031
|
end
|
|
1104
1032
|
end
|
|
@@ -1116,18 +1044,9 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1116
1044
|
raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
|
|
1117
1045
|
|
|
1118
1046
|
begin
|
|
1119
|
-
@
|
|
1120
|
-
message = MessageParser.parse(data)
|
|
1121
|
-
next unless message
|
|
1122
|
-
|
|
1123
|
-
ClaudeAgentSDK.notify_observers(@resolved_observers, :on_message, message,
|
|
1124
|
-
scheduling: @callback_scheduling, wrapper: @callback_wrapper)
|
|
1125
|
-
signal = FiberBoundary.invoke_iteration(block, message, scheduling: @callback_scheduling,
|
|
1126
|
-
wrapper: @callback_wrapper)
|
|
1127
|
-
break signal.value if signal.is_a?(FiberBoundary::Break)
|
|
1128
|
-
end
|
|
1047
|
+
@session.deliver(block)
|
|
1129
1048
|
rescue StandardError => e
|
|
1130
|
-
|
|
1049
|
+
@dispatch.notify(:on_error, e)
|
|
1131
1050
|
raise
|
|
1132
1051
|
end
|
|
1133
1052
|
end
|
|
@@ -1139,24 +1058,10 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1139
1058
|
|
|
1140
1059
|
raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
|
|
1141
1060
|
|
|
1142
|
-
# Keep loop control on the same fiber as the underlying dequeue: both
|
|
1143
|
-
# the SDK's ResultMessage break and the user's translated break happen
|
|
1144
|
-
# here, never inside the FiberBoundary hop (break in a proc on a
|
|
1145
|
-
# foreign thread raises LocalJumpError).
|
|
1146
1061
|
begin
|
|
1147
|
-
@
|
|
1148
|
-
message = MessageParser.parse(data)
|
|
1149
|
-
next unless message
|
|
1150
|
-
|
|
1151
|
-
ClaudeAgentSDK.notify_observers(@resolved_observers, :on_message, message,
|
|
1152
|
-
scheduling: @callback_scheduling, wrapper: @callback_wrapper)
|
|
1153
|
-
signal = FiberBoundary.invoke_iteration(block, message, scheduling: @callback_scheduling,
|
|
1154
|
-
wrapper: @callback_wrapper)
|
|
1155
|
-
break signal.value if signal.is_a?(FiberBoundary::Break)
|
|
1156
|
-
break if message.is_a?(ResultMessage)
|
|
1157
|
-
end
|
|
1062
|
+
@session.deliver(block, until_result: true)
|
|
1158
1063
|
rescue StandardError => e
|
|
1159
|
-
|
|
1064
|
+
@dispatch.notify(:on_error, e)
|
|
1160
1065
|
raise
|
|
1161
1066
|
end
|
|
1162
1067
|
end
|
|
@@ -1165,7 +1070,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1165
1070
|
def interrupt
|
|
1166
1071
|
raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
|
|
1167
1072
|
|
|
1168
|
-
|
|
1073
|
+
query_handler.interrupt
|
|
1169
1074
|
end
|
|
1170
1075
|
|
|
1171
1076
|
# Change permission mode during conversation
|
|
@@ -1173,7 +1078,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1173
1078
|
def set_permission_mode(mode)
|
|
1174
1079
|
raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
|
|
1175
1080
|
|
|
1176
|
-
|
|
1081
|
+
query_handler.set_permission_mode(mode)
|
|
1177
1082
|
end
|
|
1178
1083
|
|
|
1179
1084
|
# Ruby-style spelling of #set_permission_mode: `client.permission_mode = 'plan'`.
|
|
@@ -1187,7 +1092,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1187
1092
|
def set_model(model)
|
|
1188
1093
|
raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
|
|
1189
1094
|
|
|
1190
|
-
|
|
1095
|
+
query_handler.set_model(model)
|
|
1191
1096
|
end
|
|
1192
1097
|
|
|
1193
1098
|
# Ruby-style spelling of #set_model: `client.model = 'claude-opus-5'`.
|
|
@@ -1200,7 +1105,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1200
1105
|
def reconnect_mcp_server(server_name)
|
|
1201
1106
|
raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
|
|
1202
1107
|
|
|
1203
|
-
|
|
1108
|
+
query_handler.reconnect_mcp_server(server_name)
|
|
1204
1109
|
end
|
|
1205
1110
|
|
|
1206
1111
|
# Enable or disable an MCP server
|
|
@@ -1209,7 +1114,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1209
1114
|
def toggle_mcp_server(server_name, enabled)
|
|
1210
1115
|
raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
|
|
1211
1116
|
|
|
1212
|
-
|
|
1117
|
+
query_handler.toggle_mcp_server(server_name, enabled)
|
|
1213
1118
|
end
|
|
1214
1119
|
|
|
1215
1120
|
# Stop a running background task
|
|
@@ -1217,7 +1122,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1217
1122
|
def stop_task(task_id)
|
|
1218
1123
|
raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
|
|
1219
1124
|
|
|
1220
|
-
|
|
1125
|
+
query_handler.stop_task(task_id)
|
|
1221
1126
|
end
|
|
1222
1127
|
|
|
1223
1128
|
# Background in-flight foreground tasks (Bash commands and subagents) — the
|
|
@@ -1243,7 +1148,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1243
1148
|
def background_tasks(tool_use_id: nil)
|
|
1244
1149
|
raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
|
|
1245
1150
|
|
|
1246
|
-
|
|
1151
|
+
query_handler.background_tasks(tool_use_id: tool_use_id)
|
|
1247
1152
|
end
|
|
1248
1153
|
|
|
1249
1154
|
# Rewind files to a previous checkpoint (v0.1.15+)
|
|
@@ -1253,13 +1158,13 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1253
1158
|
def rewind_files(user_message_uuid)
|
|
1254
1159
|
raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
|
|
1255
1160
|
|
|
1256
|
-
|
|
1161
|
+
query_handler.rewind_files(user_message_uuid)
|
|
1257
1162
|
end
|
|
1258
1163
|
|
|
1259
1164
|
# Get server initialization info
|
|
1260
1165
|
# @return [Hash, nil] Server info or nil
|
|
1261
1166
|
def server_info
|
|
1262
|
-
|
|
1167
|
+
query_handler&.initialization_result
|
|
1263
1168
|
end
|
|
1264
1169
|
|
|
1265
1170
|
# Get a breakdown of current context window usage by category.
|
|
@@ -1269,7 +1174,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1269
1174
|
def get_context_usage
|
|
1270
1175
|
raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
|
|
1271
1176
|
|
|
1272
|
-
|
|
1177
|
+
query_handler.get_context_usage
|
|
1273
1178
|
end
|
|
1274
1179
|
|
|
1275
1180
|
# Ruby-style spelling of #get_context_usage.
|
|
@@ -1283,7 +1188,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1283
1188
|
def get_mcp_status
|
|
1284
1189
|
raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
|
|
1285
1190
|
|
|
1286
|
-
|
|
1191
|
+
query_handler.get_mcp_status
|
|
1287
1192
|
end
|
|
1288
1193
|
|
|
1289
1194
|
# Ruby-style spelling of #get_mcp_status.
|
|
@@ -1314,121 +1219,54 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1314
1219
|
# holds, in every scheduling mode, for a streaming-input enumerator that
|
|
1315
1220
|
# calls disconnect: it is iterated on the reactor inside a task the close
|
|
1316
1221
|
# stops, so it unwinds with Async::Stop once the teardown has completed.
|
|
1222
|
+
#
|
|
1223
|
+
# Interrupted while an observer's on_close runs — the caller's deadline
|
|
1224
|
+
# expires, or its task is stopped — disconnect still completes the
|
|
1225
|
+
# teardown, then lets the interruption propagate.
|
|
1317
1226
|
def disconnect
|
|
1318
|
-
if @connected
|
|
1319
|
-
ClaudeAgentSDK.notify_observers(@resolved_observers || [], :on_close,
|
|
1320
|
-
scheduling: @callback_scheduling, wrapper: @callback_wrapper)
|
|
1321
|
-
end
|
|
1322
1227
|
# Tear down whatever exists — robust to a partial/failed connect, where
|
|
1323
|
-
# @connected is still false but
|
|
1324
|
-
#
|
|
1325
|
-
# transport (flushing the mirror batcher first); the extra @transport
|
|
1326
|
-
# close covers a failure before the query handler was built (idempotent).
|
|
1228
|
+
# @connected is still false but the session already holds a transport
|
|
1229
|
+
# and/or a materialized temp dir.
|
|
1327
1230
|
#
|
|
1328
|
-
#
|
|
1329
|
-
#
|
|
1330
|
-
#
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
#
|
|
1334
|
-
#
|
|
1335
|
-
#
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1231
|
+
# Notified inside the method body, with the teardown in its ensure: an
|
|
1232
|
+
# exception raised into this fiber while it waits for an observer (a
|
|
1233
|
+
# caller's deadline) propagates, and must not skip the teardown.
|
|
1234
|
+
@dispatch.notify(:on_close) if @connected
|
|
1235
|
+
ensure
|
|
1236
|
+
# SessionAssembly#close_resources closes the query handler (which
|
|
1237
|
+
# closes the transport, flushing the mirror batcher first), closes the
|
|
1238
|
+
# transport in an ensure of its own (it covers a failure before the
|
|
1239
|
+
# handler was built, and a handler whose #close raised) and decides
|
|
1240
|
+
# what happens to the materialized temp dir, which holds a redacted
|
|
1241
|
+
# .credentials.json copy. Whatever one of those raises, the others
|
|
1242
|
+
# still run — so disconnect can never leave the client half-open. The
|
|
1243
|
+
# original error still propagates.
|
|
1244
|
+
#
|
|
1245
|
+
# The client is disconnected as soon as both closes are behind, before
|
|
1246
|
+
# the temp dir is dealt with: its removal can take a while and lets
|
|
1247
|
+
# other tasks run, and what they call meanwhile has to be refused as
|
|
1248
|
+
# "Not connected" (and a disconnect of theirs must not notify on_close
|
|
1249
|
+
# again) rather than reach a session whose resources are gone.
|
|
1250
|
+
#
|
|
1251
|
+
# The session is let go of only once all of it was disposed of. When
|
|
1252
|
+
# the teardown raises — a close failed, or the removal of the temp dir
|
|
1253
|
+
# was cut short by the caller's deadline or a stop — the session stays,
|
|
1254
|
+
# holding whatever it could not dispose of, so that the next disconnect
|
|
1255
|
+
# finishes the job (what was already closed is not closed again). It
|
|
1256
|
+
# is not the client's session any more once a reconnect replaced it.
|
|
1257
|
+
session = @session
|
|
1258
|
+
if session
|
|
1259
|
+
session.close_resources(always_close_transport: true) { @connected = false }
|
|
1260
|
+
@session = nil if @session.equal?(session)
|
|
1261
|
+
else
|
|
1262
|
+
@connected = false
|
|
1360
1263
|
end
|
|
1361
1264
|
end
|
|
1362
1265
|
|
|
1363
1266
|
private
|
|
1364
1267
|
|
|
1365
|
-
#
|
|
1366
|
-
|
|
1367
|
-
# return options repointed at it (env + --resume). Returns the options
|
|
1368
|
-
# unchanged when no materialization applies. Skipped for non-subprocess
|
|
1369
|
-
# transports — the materialized env/--resume only affect the CLI subprocess.
|
|
1370
|
-
# Ancestry (<=), not identity: a SubprocessCLITransport subclass spawns the
|
|
1371
|
-
# CLI with the same env/--resume semantics, and the transport is constructed
|
|
1372
|
-
# AFTER materialization, so the repointed options do reach it.
|
|
1373
|
-
def materialize_resume(options)
|
|
1374
|
-
subprocess_transport = @transport_class.is_a?(Class) && @transport_class <= SubprocessCLITransport
|
|
1375
|
-
return options unless options.session_store && subprocess_transport
|
|
1376
|
-
|
|
1377
|
-
@materialized = SessionResume.materialize_resume_session(options)
|
|
1378
|
-
@materialized ? SessionResume.apply_materialized_options(options, @materialized) : options
|
|
1379
|
-
end
|
|
1380
|
-
|
|
1381
|
-
# The connect body, wrapped by #connect so a failure triggers cleanup.
|
|
1382
|
-
def connect_inner(configured_options, prompt) # rubocop:disable Metrics/MethodLength -- connect sequence kept in order; #connect wraps it for cleanup
|
|
1383
|
-
# Client always uses streaming mode; keep stdin open for bidirectional
|
|
1384
|
-
# communication. Observers were already resolved by #connect.
|
|
1385
|
-
@transport = @transport_class.new(configured_options, **@transport_args)
|
|
1386
|
-
@transport.connect
|
|
1387
|
-
|
|
1388
|
-
# Extract SDK MCP servers
|
|
1389
|
-
sdk_mcp_servers = ClaudeAgentSDK.extract_sdk_mcp_servers(configured_options.mcp_servers)
|
|
1390
|
-
|
|
1391
|
-
# Convert hooks to internal format
|
|
1392
|
-
hooks = ClaudeAgentSDK.convert_hooks_to_internal_format(configured_options.hooks)
|
|
1393
|
-
|
|
1394
|
-
# Extract exclude_dynamic_sections and snapshot from the system prompt
|
|
1395
|
-
# for the initialize request (older CLIs ignore unknown initialize fields)
|
|
1396
|
-
exclude_dynamic_sections = ClaudeAgentSDK.extract_exclude_dynamic_sections(configured_options.system_prompt)
|
|
1397
|
-
system_prompt_snapshot = ClaudeAgentSDK.extract_system_prompt_snapshot(configured_options.system_prompt)
|
|
1398
|
-
|
|
1399
|
-
# Captured once, so String and streamed prompts in one session are
|
|
1400
|
-
# stamped alike (the Query stamps the streamed ones with this value).
|
|
1401
|
-
@verbatim_prompts = configured_options.verbatim_prompts?
|
|
1402
|
-
|
|
1403
|
-
# Create Query handler
|
|
1404
|
-
@query_handler = Query.new(
|
|
1405
|
-
transport: @transport,
|
|
1406
|
-
is_streaming_mode: true,
|
|
1407
|
-
can_use_tool: configured_options.can_use_tool,
|
|
1408
|
-
hooks: hooks,
|
|
1409
|
-
sdk_mcp_servers: sdk_mcp_servers,
|
|
1410
|
-
agents: configured_options.agents,
|
|
1411
|
-
exclude_dynamic_sections: exclude_dynamic_sections,
|
|
1412
|
-
system_prompt_snapshot: system_prompt_snapshot,
|
|
1413
|
-
skills: configured_options.skills,
|
|
1414
|
-
forward_subagent_text: configured_options.forward_subagent_text?,
|
|
1415
|
-
agent_progress_summaries: configured_options.agent_progress_summaries,
|
|
1416
|
-
callback_scheduling: @callback_scheduling,
|
|
1417
|
-
callback_wrapper: @callback_wrapper,
|
|
1418
|
-
verbatim_prompts: @verbatim_prompts,
|
|
1419
|
-
run_end_ceiling_ms: Query.run_end_ceiling_ms(configured_options.env)
|
|
1420
|
-
)
|
|
1421
|
-
|
|
1422
|
-
# Mirror transcripts to the session_store, if configured.
|
|
1423
|
-
install_transcript_mirror(configured_options)
|
|
1424
|
-
|
|
1425
|
-
# Start query handler and initialize
|
|
1426
|
-
@query_handler.start
|
|
1427
|
-
@query_handler.initialize_protocol
|
|
1428
|
-
|
|
1429
|
-
@connected = true
|
|
1430
|
-
|
|
1431
|
-
# Optionally send initial prompt/messages after connection is ready.
|
|
1268
|
+
# Optionally send the initial prompt/messages once the connection is ready.
|
|
1269
|
+
def send_initial_prompt(prompt)
|
|
1432
1270
|
case prompt
|
|
1433
1271
|
when nil
|
|
1434
1272
|
nil
|
|
@@ -1447,9 +1285,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1447
1285
|
# Observer#on_error contract; notifying a swallowed error would mark
|
|
1448
1286
|
# a still-live OTel trace as failed). Same behavior as query()'s
|
|
1449
1287
|
# streaming path.
|
|
1450
|
-
|
|
1451
|
-
scheduling: @callback_scheduling, wrapper: @callback_wrapper)
|
|
1452
|
-
@query_handler.spawn_task { @query_handler.stream_input(observed) }
|
|
1288
|
+
@session.stream_prompt_in_background(prompt)
|
|
1453
1289
|
end
|
|
1454
1290
|
end
|
|
1455
1291
|
|
|
@@ -1461,22 +1297,22 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1461
1297
|
# parse-stamp-regenerate, which would block the reactor on huge frames),
|
|
1462
1298
|
# except that verbatim_prompts must mark them `client_composed`, so with
|
|
1463
1299
|
# that option on they are parsed and re-serialized (Query.stamp_user_message).
|
|
1300
|
+
# Every message is stamped with the value captured at connect, like the
|
|
1301
|
+
# String prompts of the same session.
|
|
1464
1302
|
def stream_query_messages(prompt, session_id)
|
|
1465
1303
|
prompt.each do |msg|
|
|
1466
1304
|
case msg
|
|
1467
1305
|
when Hash
|
|
1468
1306
|
msg = msg.merge(session_id: session_id) unless msg.key?(:session_id) || msg.key?('session_id')
|
|
1469
1307
|
if (text = ClaudeAgentSDK.extract_user_prompt_text(msg))
|
|
1470
|
-
|
|
1471
|
-
scheduling: @callback_scheduling, wrapper: @callback_wrapper)
|
|
1308
|
+
@dispatch.notify(:on_user_prompt, text)
|
|
1472
1309
|
end
|
|
1473
|
-
|
|
1310
|
+
@session.write_message(msg)
|
|
1474
1311
|
when String
|
|
1475
1312
|
if (text = ClaudeAgentSDK.extract_user_prompt_text(msg))
|
|
1476
|
-
|
|
1477
|
-
scheduling: @callback_scheduling, wrapper: @callback_wrapper)
|
|
1313
|
+
@dispatch.notify(:on_user_prompt, text)
|
|
1478
1314
|
end
|
|
1479
|
-
|
|
1315
|
+
@session.write_message(msg)
|
|
1480
1316
|
else
|
|
1481
1317
|
# No to_s fallback — silently serializing arbitrary objects is the
|
|
1482
1318
|
# exact inspect-garbage bug class this method exists to prevent.
|
|
@@ -1484,36 +1320,5 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1484
1320
|
end
|
|
1485
1321
|
end
|
|
1486
1322
|
end
|
|
1487
|
-
|
|
1488
|
-
# Notify observers of an error surfacing to the consumer. `|| []` keeps a
|
|
1489
|
-
# mis-scoped call before connect harmless instead of NoMethodError on nil.
|
|
1490
|
-
def notify_error(error)
|
|
1491
|
-
ClaudeAgentSDK.notify_observers(@resolved_observers || [], :on_error, error,
|
|
1492
|
-
scheduling: @callback_scheduling, wrapper: @callback_wrapper)
|
|
1493
|
-
end
|
|
1494
|
-
|
|
1495
|
-
# Build and install the transcript-mirror batcher on the query handler when
|
|
1496
|
-
# a session_store is configured, via the shared SessionResume helper (also
|
|
1497
|
-
# used by the one-shot query() path).
|
|
1498
|
-
def install_transcript_mirror(options)
|
|
1499
|
-
return unless options.session_store
|
|
1500
|
-
|
|
1501
|
-
batcher = SessionResume.build_mirror_batcher(
|
|
1502
|
-
store: options.session_store,
|
|
1503
|
-
env: options.env,
|
|
1504
|
-
on_error: ->(key, message) { @query_handler.report_mirror_error(key, message) },
|
|
1505
|
-
eager: options.session_store_flush.to_s == 'eager',
|
|
1506
|
-
callback_wrapper: @callback_wrapper
|
|
1507
|
-
)
|
|
1508
|
-
@query_handler.set_transcript_mirror_batcher(batcher)
|
|
1509
|
-
end
|
|
1510
|
-
|
|
1511
|
-
def writeln(string)
|
|
1512
|
-
write string.end_with?("\n") ? string : "#{string}\n"
|
|
1513
|
-
end
|
|
1514
|
-
|
|
1515
|
-
def write(string)
|
|
1516
|
-
@transport.write(string)
|
|
1517
|
-
end
|
|
1518
1323
|
end
|
|
1519
1324
|
end
|