claude-agent-sdk 1.2.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/CHANGELOG.md +9 -0
- data/lib/claude_agent_sdk/command_builder.rb +48 -130
- 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 +47 -296
- data/lib/claude_agent_sdk/session_assembly.rb +311 -0
- data/lib/claude_agent_sdk/session_resume.rb +4 -4
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +10 -9
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +163 -406
- metadata +5 -2
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,16 +68,7 @@ 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
|
|
@@ -138,18 +131,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
138
131
|
# Shared by Client#connect and the one-shot query() path.
|
|
139
132
|
# @api private
|
|
140
133
|
def self.extract_exclude_dynamic_sections(system_prompt)
|
|
141
|
-
|
|
142
|
-
eds = system_prompt.exclude_dynamic_sections
|
|
143
|
-
return eds if [true, false].include?(eds)
|
|
144
|
-
elsif system_prompt.is_a?(Hash)
|
|
145
|
-
# The tag may be a Symbol (type: :preset), as CommandBuilder reads it.
|
|
146
|
-
type = (system_prompt[:type] || system_prompt['type']).to_s
|
|
147
|
-
if type == 'preset'
|
|
148
|
-
eds = system_prompt.fetch(:exclude_dynamic_sections) { system_prompt['exclude_dynamic_sections'] }
|
|
149
|
-
return eds if [true, false].include?(eds)
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
nil
|
|
134
|
+
OptionForms.exclude_dynamic_sections(system_prompt)
|
|
153
135
|
end
|
|
154
136
|
|
|
155
137
|
# Internal: pull snapshot out of a preset or custom system prompt for the
|
|
@@ -159,18 +141,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
159
141
|
# must not collapse it to nil. Shared by Client#connect and query().
|
|
160
142
|
# @api private
|
|
161
143
|
def self.extract_system_prompt_snapshot(system_prompt)
|
|
162
|
-
|
|
163
|
-
when SystemPromptPreset, SystemPromptCustom
|
|
164
|
-
snapshot = system_prompt.snapshot
|
|
165
|
-
return snapshot if [true, false].include?(snapshot)
|
|
166
|
-
when Hash
|
|
167
|
-
type = (system_prompt[:type] || system_prompt['type']).to_s
|
|
168
|
-
if %w[preset custom].include?(type)
|
|
169
|
-
snapshot = system_prompt.fetch(:snapshot) { system_prompt['snapshot'] }
|
|
170
|
-
return snapshot if [true, false].include?(snapshot)
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
nil
|
|
144
|
+
OptionForms.system_prompt_snapshot(system_prompt)
|
|
174
145
|
end
|
|
175
146
|
|
|
176
147
|
# Safely call a method on each observer, suppressing any errors.
|
|
@@ -719,19 +690,30 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
719
690
|
# Fail fast on invalid session_store combinations before spawning the CLI.
|
|
720
691
|
SessionStores.validate_session_store_options(configured_options)
|
|
721
692
|
|
|
722
|
-
# Resolve callable observers into fresh instances (thread-safe for global
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
#
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
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)
|
|
730
701
|
|
|
731
702
|
if transport && !transport.respond_to?(:connect)
|
|
732
703
|
raise ArgumentError, 'transport must respond to #connect (see ClaudeAgentSDK::Transport)'
|
|
733
704
|
end
|
|
734
705
|
|
|
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
|
+
|
|
735
717
|
# finished: false tells Async that a waiter handles this task's failure
|
|
736
718
|
# (the .wait at the end re-raises it), as Kernel#Sync does for its own
|
|
737
719
|
# task. Without it a task that fails before anyone waits for it — always
|
|
@@ -739,151 +721,54 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
739
721
|
# returns — is also logged as "Task may have ended with unhandled
|
|
740
722
|
# exception", message and backtrace included, although the caller gets
|
|
741
723
|
# the same error raised and may well rescue it.
|
|
742
|
-
Async(finished: false, &FiberBoundary.capture_otel_context do
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
# injected transport whose #connect raised (Python leaves it
|
|
766
|
-
# unclosed); Transport#close must be idempotent.
|
|
767
|
-
transport.connect
|
|
768
|
-
|
|
769
|
-
# Extract SDK MCP servers
|
|
770
|
-
sdk_mcp_servers = extract_sdk_mcp_servers(configured_options.mcp_servers)
|
|
771
|
-
|
|
772
|
-
hooks = convert_hooks_to_internal_format(configured_options.hooks)
|
|
773
|
-
|
|
774
|
-
# Create Query handler for control protocol
|
|
775
|
-
query_handler = Query.new(
|
|
776
|
-
transport: transport,
|
|
777
|
-
is_streaming_mode: true,
|
|
778
|
-
can_use_tool: configured_options.can_use_tool,
|
|
779
|
-
hooks: hooks,
|
|
780
|
-
agents: configured_options.agents,
|
|
781
|
-
sdk_mcp_servers: sdk_mcp_servers,
|
|
782
|
-
exclude_dynamic_sections: ClaudeAgentSDK.extract_exclude_dynamic_sections(configured_options.system_prompt),
|
|
783
|
-
system_prompt_snapshot: ClaudeAgentSDK.extract_system_prompt_snapshot(configured_options.system_prompt),
|
|
784
|
-
skills: configured_options.skills,
|
|
785
|
-
forward_subagent_text: configured_options.forward_subagent_text?,
|
|
786
|
-
agent_progress_summaries: configured_options.agent_progress_summaries,
|
|
787
|
-
callback_scheduling: callback_scheduling,
|
|
788
|
-
callback_wrapper: callback_wrapper,
|
|
789
|
-
verbatim_prompts: configured_options.verbatim_prompts?,
|
|
790
|
-
run_end_ceiling_ms: Query.run_end_ceiling_ms(configured_options.env)
|
|
791
|
-
)
|
|
792
|
-
|
|
793
|
-
# Mirror transcripts to the session_store, if configured. Installed
|
|
794
|
-
# before #start so the read loop captures transcript_mirror frames.
|
|
795
|
-
if configured_options.session_store
|
|
796
|
-
query_handler.set_transcript_mirror_batcher(
|
|
797
|
-
SessionResume.build_mirror_batcher(
|
|
798
|
-
store: configured_options.session_store,
|
|
799
|
-
env: configured_options.env,
|
|
800
|
-
on_error: ->(key, message) { query_handler.report_mirror_error(key, message) },
|
|
801
|
-
eager: configured_options.session_store_flush.to_s == 'eager',
|
|
802
|
-
callback_wrapper: callback_wrapper
|
|
803
|
-
)
|
|
804
|
-
)
|
|
805
|
-
end
|
|
806
|
-
|
|
807
|
-
# Start reading messages in background
|
|
808
|
-
query_handler.start
|
|
809
|
-
|
|
810
|
-
# Initialize the control protocol (sends agents)
|
|
811
|
-
query_handler.initialize_protocol
|
|
812
|
-
|
|
813
|
-
# Send prompt(s) as user messages, then close stdin
|
|
814
|
-
if prompt.is_a?(String)
|
|
815
|
-
ClaudeAgentSDK.notify_observers(resolved_observers, :on_user_prompt, prompt,
|
|
816
|
-
scheduling: callback_scheduling, wrapper: callback_wrapper)
|
|
817
|
-
message = {
|
|
818
|
-
type: 'user',
|
|
819
|
-
message: { role: 'user', content: prompt },
|
|
820
|
-
parent_tool_use_id: nil,
|
|
821
|
-
session_id: ''
|
|
822
|
-
}
|
|
823
|
-
transport.write("#{Query.serialize_user_message(message, configured_options.verbatim_prompts?)}\n")
|
|
824
|
-
# Background-spawn so messages stream to the user block while stdin
|
|
825
|
-
# close waits (without timeout) for the first result; a synchronous
|
|
826
|
-
# call would defer all delivery until the turn completes (mirrors
|
|
827
|
-
# Python's query.spawn_task(query.wait_for_result_and_end_input())).
|
|
828
|
-
query_handler.spawn_task { query_handler.wait_for_result_and_end_input }
|
|
829
|
-
elsif prompt.is_a?(Enumerator) || prompt.respond_to?(:each)
|
|
830
|
-
# Tracked on the Query so close() stops it; an untracked Async task
|
|
831
|
-
# here kept the root reactor alive forever when the read loop died
|
|
832
|
-
# while the user enumerator was still blocked (matches Python's
|
|
833
|
-
# query.spawn_task(query.stream_input(prompt))).
|
|
834
|
-
observed_prompt = ClaudeAgentSDK.observing_prompt_stream(
|
|
835
|
-
prompt, resolved_observers, scheduling: callback_scheduling, wrapper: callback_wrapper
|
|
836
|
-
)
|
|
837
|
-
query_handler.spawn_task { query_handler.stream_input(observed_prompt) }
|
|
838
|
-
end
|
|
839
|
-
|
|
840
|
-
# Read and yield messages from the query handler (filters out control messages).
|
|
841
|
-
# User block is invoked through FiberBoundary so ActiveRecord / PG calls
|
|
842
|
-
# inside it don't see the async gem's Fiber scheduler (default :thread
|
|
843
|
-
# mode; :inline runs it in place on the reactor fiber).
|
|
844
|
-
query_handler.receive_messages do |data|
|
|
845
|
-
message = MessageParser.parse(data)
|
|
846
|
-
next unless message
|
|
847
|
-
|
|
848
|
-
ClaudeAgentSDK.notify_observers(resolved_observers, :on_message, message,
|
|
849
|
-
scheduling: callback_scheduling, wrapper: callback_wrapper)
|
|
850
|
-
signal = FiberBoundary.invoke_iteration(block, message, scheduling: callback_scheduling,
|
|
851
|
-
wrapper: callback_wrapper)
|
|
852
|
-
break signal.value if signal.is_a?(FiberBoundary::Break)
|
|
853
|
-
end
|
|
854
|
-
rescue StandardError => e
|
|
855
|
-
# One notify point for every error surfacing from query() — transport
|
|
856
|
-
# connect, initialize, stream errors re-raised from the message queue,
|
|
857
|
-
# parse errors, and user-block errors. StandardError only: Async::Stop
|
|
858
|
-
# is cancellation, not an error. Bare raise preserves the backtrace;
|
|
859
|
-
# the ensure below still fires on_close after on_error.
|
|
860
|
-
ClaudeAgentSDK.notify_observers(resolved_observers, :on_error, e,
|
|
861
|
-
scheduling: callback_scheduling, wrapper: callback_wrapper)
|
|
862
|
-
raise
|
|
863
|
-
ensure
|
|
864
|
-
ClaudeAgentSDK.notify_observers(resolved_observers, :on_close,
|
|
865
|
-
scheduling: callback_scheduling, wrapper: callback_wrapper)
|
|
866
|
-
# query_handler.close stops the background read task and closes the
|
|
867
|
-
# transport (flushing the mirror batcher first). Fall back to a bare
|
|
868
|
-
# transport close when the handler was never built.
|
|
869
|
-
begin
|
|
870
|
-
if query_handler
|
|
871
|
-
query_handler.close
|
|
872
|
-
elsif transport
|
|
873
|
-
transport.close
|
|
874
|
-
end
|
|
875
|
-
ensure
|
|
876
|
-
# Remove the materialized resume temp dir (which holds a redacted
|
|
877
|
-
# .credentials.json copy) AFTER the subprocess has exited, even when
|
|
878
|
-
# close itself raises — unless the mirror dropped batches: the store
|
|
879
|
-
# copy is then incomplete and the temp dir holds the only copy of
|
|
880
|
-
# the dropped turns, so it is preserved (scrubbed of credentials)
|
|
881
|
-
# with a warning instead of deleted.
|
|
882
|
-
if materialized
|
|
883
|
-
query_handler&.mirror_batches_dropped? ? materialized.preserve_transcripts : materialized.cleanup
|
|
884
|
-
end
|
|
885
|
-
end
|
|
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)
|
|
886
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)
|
|
887
772
|
end).wait
|
|
888
773
|
end
|
|
889
774
|
|
|
@@ -965,11 +850,13 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
965
850
|
# }
|
|
966
851
|
# )
|
|
967
852
|
# client = ClaudeAgentSDK::Client.new(options: options)
|
|
968
|
-
class Client
|
|
853
|
+
class Client
|
|
969
854
|
# The session's control-protocol handler (nil until #connect).
|
|
970
855
|
#
|
|
971
856
|
# @api private
|
|
972
|
-
|
|
857
|
+
def query_handler
|
|
858
|
+
@session&.query_handler
|
|
859
|
+
end
|
|
973
860
|
|
|
974
861
|
# @param options [ClaudeAgentOptions, nil] Configuration options
|
|
975
862
|
# @param transport_class [Class] Transport class to use (must implement Transport interface).
|
|
@@ -977,14 +864,17 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
977
864
|
# @param transport_args [Hash] Additional keyword arguments passed to transport_class.new(options, **transport_args)
|
|
978
865
|
def initialize(options: nil, transport_class: SubprocessCLITransport, transport_args: {})
|
|
979
866
|
@options = options || ClaudeAgentOptions.new
|
|
980
|
-
|
|
981
|
-
|
|
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)
|
|
982
871
|
@transport_class = transport_class
|
|
983
872
|
@transport_args = transport_args
|
|
984
|
-
|
|
985
|
-
|
|
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
|
|
986
877
|
@connected = false
|
|
987
|
-
@materialized = nil
|
|
988
878
|
end
|
|
989
879
|
|
|
990
880
|
# Block-scoped Client lifecycle, mirroring Python's
|
|
@@ -1053,22 +943,29 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1053
943
|
# Resolve observers before the first failable runtime step so
|
|
1054
944
|
# connect-phase failures (including resume materialization) can be
|
|
1055
945
|
# notified via on_error.
|
|
1056
|
-
@
|
|
946
|
+
@dispatch.observers = ClaudeAgentSDK.resolve_observers(@options.observers)
|
|
947
|
+
|
|
948
|
+
ClaudeAgentSDK.check_inline_isolation(@dispatch.scheduling)
|
|
1057
949
|
|
|
1058
|
-
|
|
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 })
|
|
1059
956
|
|
|
1060
957
|
# If anything from materialization onward fails, tear down (closes the
|
|
1061
958
|
# subprocess and removes the materialized temp config dir) before
|
|
1062
959
|
# surfacing the error, so a partial connect never leaks a temp dir
|
|
1063
960
|
# holding a credential copy.
|
|
1064
961
|
begin
|
|
1065
|
-
# Resume
|
|
1066
|
-
#
|
|
1067
|
-
#
|
|
1068
|
-
|
|
1069
|
-
|
|
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
|
|
1070
967
|
|
|
1071
|
-
|
|
968
|
+
send_initial_prompt(prompt)
|
|
1072
969
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
1073
970
|
# Pre-handshake failures (@connected still false) are notified here;
|
|
1074
971
|
# post-handshake String-prompt send failures were already notified by
|
|
@@ -1077,14 +974,14 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1077
974
|
# out of connect.) No on_close follows for pre-handshake failures
|
|
1078
975
|
# (disconnect gates it on @connected): the session never opened.
|
|
1079
976
|
begin
|
|
1080
|
-
|
|
977
|
+
@dispatch.notify(:on_error, e) if e.is_a?(StandardError) && !@connected
|
|
1081
978
|
ensure
|
|
1082
979
|
# Tear down the partial connect, but never let a cleanup failure (e.g. a
|
|
1083
980
|
# custom transport whose #close raises) mask the original connect error.
|
|
1084
981
|
# Rescue Exception (not StandardError) so reactor cancellation
|
|
1085
|
-
# (Async::Stop < Exception) after
|
|
1086
|
-
#
|
|
1087
|
-
#
|
|
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.
|
|
1088
985
|
# In an ensure: an exception raised into this fiber while it waits
|
|
1089
986
|
# for the on_error observer (a caller's deadline) must not skip it.
|
|
1090
987
|
begin
|
|
@@ -1116,15 +1013,8 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1116
1013
|
|
|
1117
1014
|
begin
|
|
1118
1015
|
if prompt.is_a?(String)
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
message = {
|
|
1122
|
-
type: 'user',
|
|
1123
|
-
message: { role: 'user', content: prompt },
|
|
1124
|
-
parent_tool_use_id: nil,
|
|
1125
|
-
session_id: session_id
|
|
1126
|
-
}
|
|
1127
|
-
writeln(Query.serialize_user_message(message, @verbatim_prompts))
|
|
1016
|
+
@dispatch.notify(:on_user_prompt, prompt)
|
|
1017
|
+
@session.write_prompt(prompt, session_id: session_id)
|
|
1128
1018
|
elsif prompt.respond_to?(:each)
|
|
1129
1019
|
# Inline iteration on the caller, Python client.py parity — NOT
|
|
1130
1020
|
# Query#stream_input, whose ensure always ends input after
|
|
@@ -1136,7 +1026,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1136
1026
|
raise ArgumentError, "prompt must be a String or respond to #each (got #{prompt.class})"
|
|
1137
1027
|
end
|
|
1138
1028
|
rescue StandardError => e
|
|
1139
|
-
|
|
1029
|
+
@dispatch.notify(:on_error, e)
|
|
1140
1030
|
raise
|
|
1141
1031
|
end
|
|
1142
1032
|
end
|
|
@@ -1154,18 +1044,9 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1154
1044
|
raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
|
|
1155
1045
|
|
|
1156
1046
|
begin
|
|
1157
|
-
@
|
|
1158
|
-
message = MessageParser.parse(data)
|
|
1159
|
-
next unless message
|
|
1160
|
-
|
|
1161
|
-
ClaudeAgentSDK.notify_observers(@resolved_observers, :on_message, message,
|
|
1162
|
-
scheduling: @callback_scheduling, wrapper: @callback_wrapper)
|
|
1163
|
-
signal = FiberBoundary.invoke_iteration(block, message, scheduling: @callback_scheduling,
|
|
1164
|
-
wrapper: @callback_wrapper)
|
|
1165
|
-
break signal.value if signal.is_a?(FiberBoundary::Break)
|
|
1166
|
-
end
|
|
1047
|
+
@session.deliver(block)
|
|
1167
1048
|
rescue StandardError => e
|
|
1168
|
-
|
|
1049
|
+
@dispatch.notify(:on_error, e)
|
|
1169
1050
|
raise
|
|
1170
1051
|
end
|
|
1171
1052
|
end
|
|
@@ -1177,24 +1058,10 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1177
1058
|
|
|
1178
1059
|
raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
|
|
1179
1060
|
|
|
1180
|
-
# Keep loop control on the same fiber as the underlying dequeue: both
|
|
1181
|
-
# the SDK's ResultMessage break and the user's translated break happen
|
|
1182
|
-
# here, never inside the FiberBoundary hop (break in a proc on a
|
|
1183
|
-
# foreign thread raises LocalJumpError).
|
|
1184
1061
|
begin
|
|
1185
|
-
@
|
|
1186
|
-
message = MessageParser.parse(data)
|
|
1187
|
-
next unless message
|
|
1188
|
-
|
|
1189
|
-
ClaudeAgentSDK.notify_observers(@resolved_observers, :on_message, message,
|
|
1190
|
-
scheduling: @callback_scheduling, wrapper: @callback_wrapper)
|
|
1191
|
-
signal = FiberBoundary.invoke_iteration(block, message, scheduling: @callback_scheduling,
|
|
1192
|
-
wrapper: @callback_wrapper)
|
|
1193
|
-
break signal.value if signal.is_a?(FiberBoundary::Break)
|
|
1194
|
-
break if message.is_a?(ResultMessage)
|
|
1195
|
-
end
|
|
1062
|
+
@session.deliver(block, until_result: true)
|
|
1196
1063
|
rescue StandardError => e
|
|
1197
|
-
|
|
1064
|
+
@dispatch.notify(:on_error, e)
|
|
1198
1065
|
raise
|
|
1199
1066
|
end
|
|
1200
1067
|
end
|
|
@@ -1203,7 +1070,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1203
1070
|
def interrupt
|
|
1204
1071
|
raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
|
|
1205
1072
|
|
|
1206
|
-
|
|
1073
|
+
query_handler.interrupt
|
|
1207
1074
|
end
|
|
1208
1075
|
|
|
1209
1076
|
# Change permission mode during conversation
|
|
@@ -1211,7 +1078,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1211
1078
|
def set_permission_mode(mode)
|
|
1212
1079
|
raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
|
|
1213
1080
|
|
|
1214
|
-
|
|
1081
|
+
query_handler.set_permission_mode(mode)
|
|
1215
1082
|
end
|
|
1216
1083
|
|
|
1217
1084
|
# Ruby-style spelling of #set_permission_mode: `client.permission_mode = 'plan'`.
|
|
@@ -1225,7 +1092,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1225
1092
|
def set_model(model)
|
|
1226
1093
|
raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
|
|
1227
1094
|
|
|
1228
|
-
|
|
1095
|
+
query_handler.set_model(model)
|
|
1229
1096
|
end
|
|
1230
1097
|
|
|
1231
1098
|
# Ruby-style spelling of #set_model: `client.model = 'claude-opus-5'`.
|
|
@@ -1238,7 +1105,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1238
1105
|
def reconnect_mcp_server(server_name)
|
|
1239
1106
|
raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
|
|
1240
1107
|
|
|
1241
|
-
|
|
1108
|
+
query_handler.reconnect_mcp_server(server_name)
|
|
1242
1109
|
end
|
|
1243
1110
|
|
|
1244
1111
|
# Enable or disable an MCP server
|
|
@@ -1247,7 +1114,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1247
1114
|
def toggle_mcp_server(server_name, enabled)
|
|
1248
1115
|
raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
|
|
1249
1116
|
|
|
1250
|
-
|
|
1117
|
+
query_handler.toggle_mcp_server(server_name, enabled)
|
|
1251
1118
|
end
|
|
1252
1119
|
|
|
1253
1120
|
# Stop a running background task
|
|
@@ -1255,7 +1122,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1255
1122
|
def stop_task(task_id)
|
|
1256
1123
|
raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
|
|
1257
1124
|
|
|
1258
|
-
|
|
1125
|
+
query_handler.stop_task(task_id)
|
|
1259
1126
|
end
|
|
1260
1127
|
|
|
1261
1128
|
# Background in-flight foreground tasks (Bash commands and subagents) — the
|
|
@@ -1281,7 +1148,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1281
1148
|
def background_tasks(tool_use_id: nil)
|
|
1282
1149
|
raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
|
|
1283
1150
|
|
|
1284
|
-
|
|
1151
|
+
query_handler.background_tasks(tool_use_id: tool_use_id)
|
|
1285
1152
|
end
|
|
1286
1153
|
|
|
1287
1154
|
# Rewind files to a previous checkpoint (v0.1.15+)
|
|
@@ -1291,13 +1158,13 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1291
1158
|
def rewind_files(user_message_uuid)
|
|
1292
1159
|
raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
|
|
1293
1160
|
|
|
1294
|
-
|
|
1161
|
+
query_handler.rewind_files(user_message_uuid)
|
|
1295
1162
|
end
|
|
1296
1163
|
|
|
1297
1164
|
# Get server initialization info
|
|
1298
1165
|
# @return [Hash, nil] Server info or nil
|
|
1299
1166
|
def server_info
|
|
1300
|
-
|
|
1167
|
+
query_handler&.initialization_result
|
|
1301
1168
|
end
|
|
1302
1169
|
|
|
1303
1170
|
# Get a breakdown of current context window usage by category.
|
|
@@ -1307,7 +1174,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1307
1174
|
def get_context_usage
|
|
1308
1175
|
raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
|
|
1309
1176
|
|
|
1310
|
-
|
|
1177
|
+
query_handler.get_context_usage
|
|
1311
1178
|
end
|
|
1312
1179
|
|
|
1313
1180
|
# Ruby-style spelling of #get_context_usage.
|
|
@@ -1321,7 +1188,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1321
1188
|
def get_mcp_status
|
|
1322
1189
|
raise CLIConnectionError, 'Not connected. Call connect() first' unless @connected
|
|
1323
1190
|
|
|
1324
|
-
|
|
1191
|
+
query_handler.get_mcp_status
|
|
1325
1192
|
end
|
|
1326
1193
|
|
|
1327
1194
|
# Ruby-style spelling of #get_mcp_status.
|
|
@@ -1358,125 +1225,48 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1358
1225
|
# teardown, then lets the interruption propagate.
|
|
1359
1226
|
def disconnect
|
|
1360
1227
|
# Tear down whatever exists — robust to a partial/failed connect, where
|
|
1361
|
-
# @connected is still false but
|
|
1362
|
-
#
|
|
1363
|
-
# transport (flushing the mirror batcher first); the extra @transport
|
|
1364
|
-
# 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.
|
|
1365
1230
|
#
|
|
1366
|
-
#
|
|
1367
|
-
#
|
|
1368
|
-
#
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
#
|
|
1372
|
-
#
|
|
1373
|
-
#
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
if @materialized
|
|
1399
|
-
if query_handler&.mirror_batches_dropped?
|
|
1400
|
-
@materialized.preserve_transcripts
|
|
1401
|
-
else
|
|
1402
|
-
@materialized.cleanup
|
|
1403
|
-
end
|
|
1404
|
-
@materialized = nil
|
|
1405
|
-
end
|
|
1406
|
-
end
|
|
1407
|
-
end
|
|
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
|
|
1408
1263
|
end
|
|
1409
1264
|
end
|
|
1410
1265
|
|
|
1411
1266
|
private
|
|
1412
1267
|
|
|
1413
|
-
#
|
|
1414
|
-
|
|
1415
|
-
# return options repointed at it (env + --resume). Returns the options
|
|
1416
|
-
# unchanged when no materialization applies. Skipped for non-subprocess
|
|
1417
|
-
# transports — the materialized env/--resume only affect the CLI subprocess.
|
|
1418
|
-
# Ancestry (<=), not identity: a SubprocessCLITransport subclass spawns the
|
|
1419
|
-
# CLI with the same env/--resume semantics, and the transport is constructed
|
|
1420
|
-
# AFTER materialization, so the repointed options do reach it.
|
|
1421
|
-
def materialize_resume(options)
|
|
1422
|
-
subprocess_transport = @transport_class.is_a?(Class) && @transport_class <= SubprocessCLITransport
|
|
1423
|
-
return options unless options.session_store && subprocess_transport
|
|
1424
|
-
|
|
1425
|
-
@materialized = SessionResume.materialize_resume_session(options)
|
|
1426
|
-
@materialized ? SessionResume.apply_materialized_options(options, @materialized) : options
|
|
1427
|
-
end
|
|
1428
|
-
|
|
1429
|
-
# The connect body, wrapped by #connect so a failure triggers cleanup.
|
|
1430
|
-
def connect_inner(configured_options, prompt) # rubocop:disable Metrics/MethodLength -- connect sequence kept in order; #connect wraps it for cleanup
|
|
1431
|
-
# Client always uses streaming mode; keep stdin open for bidirectional
|
|
1432
|
-
# communication. Observers were already resolved by #connect.
|
|
1433
|
-
@transport = @transport_class.new(configured_options, **@transport_args)
|
|
1434
|
-
@transport.connect
|
|
1435
|
-
|
|
1436
|
-
# Extract SDK MCP servers
|
|
1437
|
-
sdk_mcp_servers = ClaudeAgentSDK.extract_sdk_mcp_servers(configured_options.mcp_servers)
|
|
1438
|
-
|
|
1439
|
-
# Convert hooks to internal format
|
|
1440
|
-
hooks = ClaudeAgentSDK.convert_hooks_to_internal_format(configured_options.hooks)
|
|
1441
|
-
|
|
1442
|
-
# Extract exclude_dynamic_sections and snapshot from the system prompt
|
|
1443
|
-
# for the initialize request (older CLIs ignore unknown initialize fields)
|
|
1444
|
-
exclude_dynamic_sections = ClaudeAgentSDK.extract_exclude_dynamic_sections(configured_options.system_prompt)
|
|
1445
|
-
system_prompt_snapshot = ClaudeAgentSDK.extract_system_prompt_snapshot(configured_options.system_prompt)
|
|
1446
|
-
|
|
1447
|
-
# Captured once, so String and streamed prompts in one session are
|
|
1448
|
-
# stamped alike (the Query stamps the streamed ones with this value).
|
|
1449
|
-
@verbatim_prompts = configured_options.verbatim_prompts?
|
|
1450
|
-
|
|
1451
|
-
# Create Query handler
|
|
1452
|
-
@query_handler = Query.new(
|
|
1453
|
-
transport: @transport,
|
|
1454
|
-
is_streaming_mode: true,
|
|
1455
|
-
can_use_tool: configured_options.can_use_tool,
|
|
1456
|
-
hooks: hooks,
|
|
1457
|
-
sdk_mcp_servers: sdk_mcp_servers,
|
|
1458
|
-
agents: configured_options.agents,
|
|
1459
|
-
exclude_dynamic_sections: exclude_dynamic_sections,
|
|
1460
|
-
system_prompt_snapshot: system_prompt_snapshot,
|
|
1461
|
-
skills: configured_options.skills,
|
|
1462
|
-
forward_subagent_text: configured_options.forward_subagent_text?,
|
|
1463
|
-
agent_progress_summaries: configured_options.agent_progress_summaries,
|
|
1464
|
-
callback_scheduling: @callback_scheduling,
|
|
1465
|
-
callback_wrapper: @callback_wrapper,
|
|
1466
|
-
verbatim_prompts: @verbatim_prompts,
|
|
1467
|
-
run_end_ceiling_ms: Query.run_end_ceiling_ms(configured_options.env)
|
|
1468
|
-
)
|
|
1469
|
-
|
|
1470
|
-
# Mirror transcripts to the session_store, if configured.
|
|
1471
|
-
install_transcript_mirror(configured_options)
|
|
1472
|
-
|
|
1473
|
-
# Start query handler and initialize
|
|
1474
|
-
@query_handler.start
|
|
1475
|
-
@query_handler.initialize_protocol
|
|
1476
|
-
|
|
1477
|
-
@connected = true
|
|
1478
|
-
|
|
1479
|
-
# 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)
|
|
1480
1270
|
case prompt
|
|
1481
1271
|
when nil
|
|
1482
1272
|
nil
|
|
@@ -1495,9 +1285,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1495
1285
|
# Observer#on_error contract; notifying a swallowed error would mark
|
|
1496
1286
|
# a still-live OTel trace as failed). Same behavior as query()'s
|
|
1497
1287
|
# streaming path.
|
|
1498
|
-
|
|
1499
|
-
scheduling: @callback_scheduling, wrapper: @callback_wrapper)
|
|
1500
|
-
@query_handler.spawn_task { @query_handler.stream_input(observed) }
|
|
1288
|
+
@session.stream_prompt_in_background(prompt)
|
|
1501
1289
|
end
|
|
1502
1290
|
end
|
|
1503
1291
|
|
|
@@ -1509,22 +1297,22 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1509
1297
|
# parse-stamp-regenerate, which would block the reactor on huge frames),
|
|
1510
1298
|
# except that verbatim_prompts must mark them `client_composed`, so with
|
|
1511
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.
|
|
1512
1302
|
def stream_query_messages(prompt, session_id)
|
|
1513
1303
|
prompt.each do |msg|
|
|
1514
1304
|
case msg
|
|
1515
1305
|
when Hash
|
|
1516
1306
|
msg = msg.merge(session_id: session_id) unless msg.key?(:session_id) || msg.key?('session_id')
|
|
1517
1307
|
if (text = ClaudeAgentSDK.extract_user_prompt_text(msg))
|
|
1518
|
-
|
|
1519
|
-
scheduling: @callback_scheduling, wrapper: @callback_wrapper)
|
|
1308
|
+
@dispatch.notify(:on_user_prompt, text)
|
|
1520
1309
|
end
|
|
1521
|
-
|
|
1310
|
+
@session.write_message(msg)
|
|
1522
1311
|
when String
|
|
1523
1312
|
if (text = ClaudeAgentSDK.extract_user_prompt_text(msg))
|
|
1524
|
-
|
|
1525
|
-
scheduling: @callback_scheduling, wrapper: @callback_wrapper)
|
|
1313
|
+
@dispatch.notify(:on_user_prompt, text)
|
|
1526
1314
|
end
|
|
1527
|
-
|
|
1315
|
+
@session.write_message(msg)
|
|
1528
1316
|
else
|
|
1529
1317
|
# No to_s fallback — silently serializing arbitrary objects is the
|
|
1530
1318
|
# exact inspect-garbage bug class this method exists to prevent.
|
|
@@ -1532,36 +1320,5 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
|
|
|
1532
1320
|
end
|
|
1533
1321
|
end
|
|
1534
1322
|
end
|
|
1535
|
-
|
|
1536
|
-
# Notify observers of an error surfacing to the consumer. `|| []` keeps a
|
|
1537
|
-
# mis-scoped call before connect harmless instead of NoMethodError on nil.
|
|
1538
|
-
def notify_error(error)
|
|
1539
|
-
ClaudeAgentSDK.notify_observers(@resolved_observers || [], :on_error, error,
|
|
1540
|
-
scheduling: @callback_scheduling, wrapper: @callback_wrapper)
|
|
1541
|
-
end
|
|
1542
|
-
|
|
1543
|
-
# Build and install the transcript-mirror batcher on the query handler when
|
|
1544
|
-
# a session_store is configured, via the shared SessionResume helper (also
|
|
1545
|
-
# used by the one-shot query() path).
|
|
1546
|
-
def install_transcript_mirror(options)
|
|
1547
|
-
return unless options.session_store
|
|
1548
|
-
|
|
1549
|
-
batcher = SessionResume.build_mirror_batcher(
|
|
1550
|
-
store: options.session_store,
|
|
1551
|
-
env: options.env,
|
|
1552
|
-
on_error: ->(key, message) { @query_handler.report_mirror_error(key, message) },
|
|
1553
|
-
eager: options.session_store_flush.to_s == 'eager',
|
|
1554
|
-
callback_wrapper: @callback_wrapper
|
|
1555
|
-
)
|
|
1556
|
-
@query_handler.set_transcript_mirror_batcher(batcher)
|
|
1557
|
-
end
|
|
1558
|
-
|
|
1559
|
-
def writeln(string)
|
|
1560
|
-
write string.end_with?("\n") ? string : "#{string}\n"
|
|
1561
|
-
end
|
|
1562
|
-
|
|
1563
|
-
def write(string)
|
|
1564
|
-
@transport.write(string)
|
|
1565
|
-
end
|
|
1566
1323
|
end
|
|
1567
1324
|
end
|