claude-agent-sdk 0.30.0 → 0.32.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 +34 -0
- data/README.md +107 -223
- data/docs/cli-installer.md +57 -0
- data/docs/configuration.md +39 -0
- data/docs/errors.md +53 -0
- data/docs/sessions.md +42 -0
- data/docs/types.md +74 -4
- data/lib/claude_agent_sdk/command_builder.rb +42 -0
- data/lib/claude_agent_sdk/errors.rb +147 -0
- data/lib/claude_agent_sdk/message_parser.rb +38 -3
- data/lib/claude_agent_sdk/query.rb +73 -29
- data/lib/claude_agent_sdk/session_resume.rb +223 -33
- data/lib/claude_agent_sdk/sessions.rb +112 -19
- data/lib/claude_agent_sdk/types.rb +215 -2
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +57 -29
- metadata +3 -2
|
@@ -30,16 +30,24 @@ module ClaudeAgentSDK
|
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
# A single message from a session transcript
|
|
33
|
+
# A single message from a session transcript.
|
|
34
|
+
#
|
|
35
|
+
# +parent_tool_use_id+ / +parent_agent_id+ are only populated for messages
|
|
36
|
+
# returned by get_subagent_messages / get_subagent_messages_from_store:
|
|
37
|
+
# respectively the id of the Agent tool_use block in the parent session that
|
|
38
|
+
# spawned the subagent, and (for nested subagents) the agent id of the
|
|
39
|
+
# subagent that spawned it. Both are nil when the subagent's metadata is
|
|
40
|
+
# unavailable, and always nil for top-level session messages.
|
|
34
41
|
class SessionMessage
|
|
35
|
-
attr_accessor :type, :uuid, :session_id, :message, :parent_tool_use_id
|
|
42
|
+
attr_accessor :type, :uuid, :session_id, :message, :parent_tool_use_id, :parent_agent_id
|
|
36
43
|
|
|
37
|
-
def initialize(type:, uuid:, session_id:, message:, parent_tool_use_id: nil)
|
|
44
|
+
def initialize(type:, uuid:, session_id:, message:, parent_tool_use_id: nil, parent_agent_id: nil)
|
|
38
45
|
@type = type
|
|
39
46
|
@uuid = uuid
|
|
40
47
|
@session_id = session_id
|
|
41
48
|
@message = message
|
|
42
49
|
@parent_tool_use_id = parent_tool_use_id
|
|
50
|
+
@parent_agent_id = parent_agent_id
|
|
43
51
|
end
|
|
44
52
|
|
|
45
53
|
# Concatenated text across every TextBlock in this message.
|
|
@@ -614,7 +622,49 @@ module ClaudeAgentSDK
|
|
|
614
622
|
# `except OSError: return []`).
|
|
615
623
|
return []
|
|
616
624
|
end
|
|
617
|
-
|
|
625
|
+
|
|
626
|
+
# The .meta.json sidecar next to the transcript records which Agent
|
|
627
|
+
# tool_use spawned this subagent (and, for nested subagents, the parent
|
|
628
|
+
# agent id). Like the transcript read above this is best-effort: any
|
|
629
|
+
# failure to read it degrades to "no metadata" rather than raising.
|
|
630
|
+
meta = begin
|
|
631
|
+
read_agent_metadata_sidecar(path)
|
|
632
|
+
rescue SystemCallError
|
|
633
|
+
nil
|
|
634
|
+
end
|
|
635
|
+
parent_tool_use_id, parent_agent_id = parent_ids_from_agent_metadata(meta)
|
|
636
|
+
|
|
637
|
+
entries_to_subagent_messages(entries, limit, offset, parent_tool_use_id, parent_agent_id)
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
# agent-<id>.jsonl -> agent-<id>.meta.json in the same directory. The single
|
|
641
|
+
# definition of the sidecar naming convention, shared by the disk read path,
|
|
642
|
+
# session import, and resume materialization.
|
|
643
|
+
# @param transcript_path [String] Path to the subagent .jsonl transcript
|
|
644
|
+
# @return [String] Path to the sidecar
|
|
645
|
+
def agent_metadata_sidecar_path(transcript_path)
|
|
646
|
+
"#{transcript_path.delete_suffix('.jsonl')}.meta.json"
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
# Separate the synthetic agent_metadata entry from transcript lines.
|
|
650
|
+
#
|
|
651
|
+
# A subagent's SessionStore stream carries its .meta.json sidecar as
|
|
652
|
+
# { 'type' => 'agent_metadata', ... } entries alongside the transcript.
|
|
653
|
+
# Returns [metadata, transcript] where metadata is the LAST such entry (it
|
|
654
|
+
# is rewritten on resume, so last wins) or nil.
|
|
655
|
+
# @param entries [Array] Raw store/transcript entries
|
|
656
|
+
# @return [Array(Hash, Array)] [metadata_or_nil, transcript_entries]
|
|
657
|
+
def split_agent_metadata(entries)
|
|
658
|
+
metadata = nil
|
|
659
|
+
transcript = []
|
|
660
|
+
entries.each do |e|
|
|
661
|
+
if e.is_a?(Hash) && e['type'] == 'agent_metadata'
|
|
662
|
+
metadata = e
|
|
663
|
+
else
|
|
664
|
+
transcript << e
|
|
665
|
+
end
|
|
666
|
+
end
|
|
667
|
+
[metadata, transcript]
|
|
618
668
|
end
|
|
619
669
|
|
|
620
670
|
# ---- SessionStore-backed reads (store counterparts to the disk readers) ----
|
|
@@ -725,12 +775,15 @@ module ClaudeAgentSDK
|
|
|
725
775
|
entries = session_store.load('project_key' => project_key, 'session_id' => session_id, 'subpath' => subpath)
|
|
726
776
|
return [] if entries.nil? || entries.empty?
|
|
727
777
|
|
|
728
|
-
#
|
|
729
|
-
# sidecar
|
|
730
|
-
|
|
778
|
+
# The synthetic agent_metadata entry (the store's copy of the .meta.json
|
|
779
|
+
# sidecar) records which Agent tool_use spawned this subagent. Recover the
|
|
780
|
+
# parent ids from it, then drop it: it is not a transcript line.
|
|
781
|
+
meta_entry, transcript = split_agent_metadata(entries)
|
|
731
782
|
return [] if transcript.empty?
|
|
732
783
|
|
|
733
|
-
|
|
784
|
+
parent_tool_use_id, parent_agent_id = parent_ids_from_agent_metadata(meta_entry)
|
|
785
|
+
entries_to_subagent_messages(filter_transcript_entries(transcript), limit, offset,
|
|
786
|
+
parent_tool_use_id, parent_agent_id)
|
|
734
787
|
end
|
|
735
788
|
|
|
736
789
|
# Replay a local on-disk session transcript into a SessionStore (inverse of
|
|
@@ -908,7 +961,8 @@ module ClaudeAgentSDK
|
|
|
908
961
|
# filter_visible_messages drops sidechain entries) would return [] for
|
|
909
962
|
# every real subagent transcript. Mirrors Python's
|
|
910
963
|
# _entries_to_subagent_messages: type-only filter, no flag rejection.
|
|
911
|
-
|
|
964
|
+
# Every message in one subagent transcript shares the same parent ids.
|
|
965
|
+
def entries_to_subagent_messages(entries, limit, offset, parent_tool_use_id = nil, parent_agent_id = nil)
|
|
912
966
|
offset ||= 0
|
|
913
967
|
messages = build_subagent_chain(entries).filter_map do |entry|
|
|
914
968
|
next unless %w[user assistant].include?(entry['type'])
|
|
@@ -917,7 +971,9 @@ module ClaudeAgentSDK
|
|
|
917
971
|
type: entry['type'],
|
|
918
972
|
uuid: entry['uuid'],
|
|
919
973
|
session_id: entry['sessionId'] || entry['session_id'] || '',
|
|
920
|
-
message: entry['message']
|
|
974
|
+
message: entry['message'],
|
|
975
|
+
parent_tool_use_id: parent_tool_use_id,
|
|
976
|
+
parent_agent_id: parent_agent_id
|
|
921
977
|
)
|
|
922
978
|
end
|
|
923
979
|
messages = messages[offset..] || []
|
|
@@ -966,20 +1022,56 @@ module ClaudeAgentSDK
|
|
|
966
1022
|
sub_key = { 'project_key' => project_key, 'session_id' => session_id, 'subpath' => subpath }
|
|
967
1023
|
append_jsonl_file_in_batches(file_path, sub_key, store, batch_size)
|
|
968
1024
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
end
|
|
974
|
-
next if meta_text.nil?
|
|
1025
|
+
# A missing, corrupt, or non-object sidecar is treated as absent (the
|
|
1026
|
+
# transcript is still imported); other IO errors propagate.
|
|
1027
|
+
meta = read_agent_metadata_sidecar(file_path)
|
|
1028
|
+
next if meta.nil?
|
|
975
1029
|
|
|
976
|
-
meta = JSON.parse(meta_text)
|
|
977
1030
|
# Synthetic 'agent_metadata' marker must always win so a future meta key
|
|
978
1031
|
# named 'type' can't reclassify the sidecar as a transcript line on resume.
|
|
979
|
-
store.append(sub_key, [meta.merge('type' => 'agent_metadata')])
|
|
1032
|
+
store.append(sub_key, [meta.merge('type' => 'agent_metadata')])
|
|
980
1033
|
end
|
|
981
1034
|
end
|
|
982
1035
|
|
|
1036
|
+
# Read the .meta.json sidecar beside a subagent transcript. Returns nil when
|
|
1037
|
+
# the sidecar is missing, is not a regular file, is not valid UTF-8, is not
|
|
1038
|
+
# valid JSON, or is not a JSON object — an unusable optional sidecar
|
|
1039
|
+
# degrades to an absent one. Other IO errors (EACCES, ...) propagate;
|
|
1040
|
+
# callers that need a best-effort read rescue them.
|
|
1041
|
+
def read_agent_metadata_sidecar(transcript_path)
|
|
1042
|
+
path = agent_metadata_sidecar_path(transcript_path)
|
|
1043
|
+
# Check the type on the stat, before any open: opening a FIFO with no
|
|
1044
|
+
# writer blocks forever, and this optional read would hang the caller
|
|
1045
|
+
# with no exception for its best-effort rescue to catch.
|
|
1046
|
+
return nil unless File.stat(path).ftype == 'file'
|
|
1047
|
+
|
|
1048
|
+
text = File.read(path, encoding: 'UTF-8')
|
|
1049
|
+
# JSON.parse is lenient about illegal bytes inside an otherwise
|
|
1050
|
+
# well-formed UTF-8-tagged document: it returns a Hash holding
|
|
1051
|
+
# invalidly-encoded values that only blow up later, at JSON.generate
|
|
1052
|
+
# time. Session import would persist such a Hash as an agent_metadata
|
|
1053
|
+
# entry, and every subsequent resume through that store would then die
|
|
1054
|
+
# re-serializing the sidecar. Treat unusable bytes as an absent sidecar.
|
|
1055
|
+
return nil unless text.valid_encoding?
|
|
1056
|
+
|
|
1057
|
+
meta = JSON.parse(text)
|
|
1058
|
+
meta.is_a?(Hash) ? meta : nil
|
|
1059
|
+
rescue Errno::ENOENT, JSON::ParserError
|
|
1060
|
+
nil
|
|
1061
|
+
end
|
|
1062
|
+
|
|
1063
|
+
# Extract [toolUseId, parentAgentId] from an agent metadata hash, narrowing
|
|
1064
|
+
# both to String. Works for the on-disk .meta.json sidecar and for the
|
|
1065
|
+
# synthetic agent_metadata entry a SessionStore receives in its place.
|
|
1066
|
+
def parent_ids_from_agent_metadata(meta)
|
|
1067
|
+
return [nil, nil] unless meta.is_a?(Hash)
|
|
1068
|
+
|
|
1069
|
+
tool_use_id = meta['toolUseId']
|
|
1070
|
+
parent_agent_id = meta['parentAgentId']
|
|
1071
|
+
[tool_use_id.is_a?(String) ? tool_use_id : nil,
|
|
1072
|
+
parent_agent_id.is_a?(String) ? parent_agent_id : nil]
|
|
1073
|
+
end
|
|
1074
|
+
|
|
983
1075
|
def append_jsonl_file_in_batches(file_path, key, store, batch_size)
|
|
984
1076
|
batch = []
|
|
985
1077
|
nbytes = 0
|
|
@@ -1357,7 +1449,8 @@ module ClaudeAgentSDK
|
|
|
1357
1449
|
:derive_info_from_entries, :mtime_from_entries, :apply_sort_limit_offset,
|
|
1358
1450
|
:filter_transcript_entries, :entries_to_messages,
|
|
1359
1451
|
:entries_to_subagent_messages, :build_subagent_chain, :resolve_subagent_subpath,
|
|
1360
|
-
:import_subagent_files, :append_jsonl_file_in_batches, :collect_jsonl_files
|
|
1452
|
+
:import_subagent_files, :append_jsonl_file_in_batches, :collect_jsonl_files,
|
|
1453
|
+
:read_agent_metadata_sidecar, :parent_ids_from_agent_metadata
|
|
1361
1454
|
|
|
1362
1455
|
# These remain accessible for SessionMutations:
|
|
1363
1456
|
# config_dir, sanitize_path, find_project_dir, detect_worktrees
|
|
@@ -212,6 +212,64 @@ module ClaudeAgentSDK
|
|
|
212
212
|
class UserMessage < Type
|
|
213
213
|
attr_accessor :content, :uuid, :parent_tool_use_id, :tool_use_result
|
|
214
214
|
|
|
215
|
+
# Provenance of this message — where the turn came from.
|
|
216
|
+
#
|
|
217
|
+
# In streaming-input mode a single connection interleaves the turns you
|
|
218
|
+
# send with turns the session injects on its own (background-task
|
|
219
|
+
# notifications, fired scheduled-task prompts, MCP channel messages,
|
|
220
|
+
# messages relayed from peer sessions, ...). `origin` tells them apart —
|
|
221
|
+
# see {ResultMessage#origin} for deciding whether a result answers *your*
|
|
222
|
+
# prompt.
|
|
223
|
+
#
|
|
224
|
+
# **Key form — read this before indexing into it.** A plain Hash, passed
|
|
225
|
+
# through from the CLI untouched: the SDK does not model it, whitelist its
|
|
226
|
+
# keys, or rewrite them, so kinds and fields newer CLI versions add stay
|
|
227
|
+
# visible. Keys therefore follow the transport's JSON parsing, which uses
|
|
228
|
+
# `symbolize_names: true` — they are **Symbols with the wire spelling
|
|
229
|
+
# preserved**, so camelCase keys stay camelCase and you index with
|
|
230
|
+
# `origin[:kind]`, `origin[:fromSession]`, `origin[:senderTaskId]`,
|
|
231
|
+
# `origin[:verifiedPeerPid]`. This is unlike the snake_case attributes
|
|
232
|
+
# elsewhere in this SDK, and unlike the Python SDK's string keys: a
|
|
233
|
+
# `origin["kind"]` or `origin[:from_session]` lookup silently returns nil
|
|
234
|
+
# and makes every attributed turn look unattributed. Only `:kind` is
|
|
235
|
+
# guaranteed present; the rest depend on it.
|
|
236
|
+
#
|
|
237
|
+
# `nil` means the CLI did not attribute the message — that is the normal
|
|
238
|
+
# case for prompts you send through {ClaudeAgentSDK.query} / {Client#query},
|
|
239
|
+
# unless the host stamps `origin: { kind: 'human' }` on the message Hash
|
|
240
|
+
# itself (only the `human` kind is honored from an SDK host). Populated on
|
|
241
|
+
# injected turns (task notifications, channel/peer messages, ...) and on
|
|
242
|
+
# user messages the CLI replays; tool-result messages never carry it.
|
|
243
|
+
#
|
|
244
|
+
# Known `:kind` values — documentation, not validation; treat anything
|
|
245
|
+
# unrecognized as "not human":
|
|
246
|
+
#
|
|
247
|
+
# - `'human'` — a turn submitted by the SDK host
|
|
248
|
+
# - `'channel'` — arrived on an MCP channel; `:server` names the MCP server
|
|
249
|
+
# - `'peer'` — relayed from a peer session. `:from` (sender address,
|
|
250
|
+
# sender-asserted — for reply routing or display, never as proof of
|
|
251
|
+
# identity), `:name` (display name, already normalized by the CLI),
|
|
252
|
+
# `:fromSession` (the sender's host-openable session id, a navigation
|
|
253
|
+
# target only), `:senderTaskId` (task id of the in-process background
|
|
254
|
+
# subagent that sent it; absent for cross-session peers), `:body`
|
|
255
|
+
# (decoded message body with the peer envelope stripped, byte-exact with
|
|
256
|
+
# what the model saw — render this instead of re-parsing the message
|
|
257
|
+
# text), `:verifiedPeerPid` (kernel-verified pid of the process that
|
|
258
|
+
# connected to this session's local messaging socket — the *connecting*
|
|
259
|
+
# process, which for relayed traffic is the relay; absent when
|
|
260
|
+
# unverifiable)
|
|
261
|
+
# - `'task-notification'` — a background task's delivery. `:subkind` is
|
|
262
|
+
# `'scheduled-trigger'` (the fired prompt of a scheduled task) or
|
|
263
|
+
# `'peer-send-message'` (a message sent from another of the user's
|
|
264
|
+
# sessions); absent for ordinary background-task notifications
|
|
265
|
+
# - `'coordinator'`, `'unclassified'`, `'observer'` (`:from` /
|
|
266
|
+
# `:senderTaskId` as for `peer`), `'auto-continuation'`,
|
|
267
|
+
# `'observer-activity'`
|
|
268
|
+
#
|
|
269
|
+
# @return [Hash{Symbol => Object}, nil]
|
|
270
|
+
# @see ResultMessage#origin
|
|
271
|
+
attr_accessor :origin
|
|
272
|
+
|
|
215
273
|
# Concatenated text of this message. Handles both String content
|
|
216
274
|
# (plain-text user prompt) and Array-of-blocks content (typed content).
|
|
217
275
|
# Returns "" when there is no text.
|
|
@@ -442,6 +500,32 @@ module ClaudeAgentSDK
|
|
|
442
500
|
def deferred_tool_use=(value)
|
|
443
501
|
@deferred_tool_use = value.is_a?(Hash) ? DeferredToolUse.from_hash(value) : value
|
|
444
502
|
end
|
|
503
|
+
|
|
504
|
+
# Provenance of the user message that triggered this turn — `nil` when the
|
|
505
|
+
# CLI did not attribute it. Lets a streaming-input consumer distinguish the
|
|
506
|
+
# result of its own prompt from the result of a turn the session injected
|
|
507
|
+
# on its own:
|
|
508
|
+
#
|
|
509
|
+
# if result.origin.nil? || result.origin[:kind] == 'human'
|
|
510
|
+
# # a turn this application submitted
|
|
511
|
+
# elsif result.origin[:kind] == 'task-notification'
|
|
512
|
+
# # follow-up turn driven by a background task
|
|
513
|
+
# end
|
|
514
|
+
#
|
|
515
|
+
# **Key form.** A plain Hash passed through from the CLI untouched, so its
|
|
516
|
+
# keys are **Symbols with the wire spelling preserved** — camelCase stays
|
|
517
|
+
# camelCase (`origin[:kind]`, `origin[:fromSession]`,
|
|
518
|
+
# `origin[:verifiedPeerPid]`), unlike the snake_case attributes elsewhere
|
|
519
|
+
# in this SDK and unlike the Python SDK's string keys. Indexing with
|
|
520
|
+
# `origin["kind"]` silently returns nil and makes every attributed turn
|
|
521
|
+
# look unattributed.
|
|
522
|
+
#
|
|
523
|
+
# See {UserMessage#origin} for the full list of known `:kind` values and
|
|
524
|
+
# their per-kind keys.
|
|
525
|
+
#
|
|
526
|
+
# @return [Hash{Symbol => Object}, nil]
|
|
527
|
+
# @see UserMessage#origin
|
|
528
|
+
attr_accessor :origin
|
|
445
529
|
end
|
|
446
530
|
|
|
447
531
|
# Stream event for partial message updates
|
|
@@ -507,6 +591,33 @@ module ClaudeAgentSDK
|
|
|
507
591
|
end
|
|
508
592
|
end
|
|
509
593
|
|
|
594
|
+
# Emitted when the session's conversation is replaced without ending the
|
|
595
|
+
# connection — e.g. after `/clear` or any other flow that discards the
|
|
596
|
+
# transcript mid-session (type: 'conversation_reset').
|
|
597
|
+
#
|
|
598
|
+
# In streaming-input mode a single connection carries many user turns, and a
|
|
599
|
+
# reset clears the conversation history *and* zeroes the running totals
|
|
600
|
+
# reported on subsequent {ResultMessage} objects (e.g. `total_cost_usd`). If
|
|
601
|
+
# you accumulate those totals across a long-lived session, snapshot them when
|
|
602
|
+
# this message arrives.
|
|
603
|
+
#
|
|
604
|
+
# @!attribute [rw] new_conversation_id
|
|
605
|
+
# Opaque identifier for the fresh conversation, for UIs to key an empty
|
|
606
|
+
# transcript on (and to discard any cached session title). This is *not*
|
|
607
|
+
# the `session_id` of subsequent messages — read that from the next
|
|
608
|
+
# message.
|
|
609
|
+
# @return [String]
|
|
610
|
+
# @!attribute [rw] uuid
|
|
611
|
+
# Unique ID of this message.
|
|
612
|
+
# @return [String]
|
|
613
|
+
# @!attribute [rw] session_id
|
|
614
|
+
# ID of the session that was reset (the outgoing session; messages after
|
|
615
|
+
# the reset carry a new `session_id`).
|
|
616
|
+
# @return [String]
|
|
617
|
+
class ConversationResetMessage < Type
|
|
618
|
+
attr_accessor :new_conversation_id, :uuid, :session_id
|
|
619
|
+
end
|
|
620
|
+
|
|
510
621
|
# Thinking configuration types
|
|
511
622
|
#
|
|
512
623
|
# `display` controls how thinking content appears in responses. Valid values
|
|
@@ -1529,10 +1640,22 @@ module ClaudeAgentSDK
|
|
|
1529
1640
|
end
|
|
1530
1641
|
end
|
|
1531
1642
|
|
|
1532
|
-
# System prompt preset configuration
|
|
1643
|
+
# System prompt preset configuration.
|
|
1644
|
+
#
|
|
1645
|
+
# +snapshot+ controls whether the session keeps the system prompt it
|
|
1646
|
+
# recorded on its first request. When true, every later request (including
|
|
1647
|
+
# after resume) sends the recorded prompt, so a changed +append+ has no
|
|
1648
|
+
# effect until the session is compacted or a new session starts. When
|
|
1649
|
+
# false, the prompt is rebuilt on every request — useful while iterating on
|
|
1650
|
+
# +append+ text across calls that resume the same session. When nil
|
|
1651
|
+
# (omitted), the CLI treats it as true, except in bare mode (+--bare+),
|
|
1652
|
+
# where it acts as false. Sent on the control-protocol +initialize+ request
|
|
1653
|
+
# (never as a CLI flag); requires Claude Code CLI 2.1.257 or later, and
|
|
1654
|
+
# before 2.1.265 a session with an +append+ prompt recorded it only when
|
|
1655
|
+
# +snapshot+ was true. Older CLIs silently ignore it.
|
|
1533
1656
|
class SystemPromptPreset < Type
|
|
1534
1657
|
attr_reader :type
|
|
1535
|
-
attr_accessor :preset, :append, :exclude_dynamic_sections
|
|
1658
|
+
attr_accessor :preset, :append, :exclude_dynamic_sections, :snapshot
|
|
1536
1659
|
|
|
1537
1660
|
def initialize(attributes = {})
|
|
1538
1661
|
super
|
|
@@ -1543,6 +1666,27 @@ module ClaudeAgentSDK
|
|
|
1543
1666
|
result = { type: @type, preset: @preset }
|
|
1544
1667
|
result[:append] = @append if @append
|
|
1545
1668
|
result[:exclude_dynamic_sections] = @exclude_dynamic_sections unless @exclude_dynamic_sections.nil?
|
|
1669
|
+
result[:snapshot] = @snapshot unless @snapshot.nil?
|
|
1670
|
+
result
|
|
1671
|
+
end
|
|
1672
|
+
end
|
|
1673
|
+
|
|
1674
|
+
# Custom system prompt configuration — the object form of passing a String
|
|
1675
|
+
# as +system_prompt+. Reaches the CLI the same way a String does
|
|
1676
|
+
# (+--system-prompt <prompt>+); the object form exists so +snapshot+ can be
|
|
1677
|
+
# set alongside it (see SystemPromptPreset#snapshot for its semantics).
|
|
1678
|
+
class SystemPromptCustom < Type
|
|
1679
|
+
attr_reader :type
|
|
1680
|
+
attr_accessor :prompt, :snapshot
|
|
1681
|
+
|
|
1682
|
+
def initialize(attributes = {})
|
|
1683
|
+
super
|
|
1684
|
+
@type = 'custom'
|
|
1685
|
+
end
|
|
1686
|
+
|
|
1687
|
+
def to_h
|
|
1688
|
+
result = { type: @type, prompt: @prompt }
|
|
1689
|
+
result[:snapshot] = @snapshot unless @snapshot.nil?
|
|
1546
1690
|
result
|
|
1547
1691
|
end
|
|
1548
1692
|
end
|
|
@@ -1580,6 +1724,42 @@ module ClaudeAgentSDK
|
|
|
1580
1724
|
:include_hook_events, :strict_mcp_config,
|
|
1581
1725
|
:callback_scheduling, :callback_wrapper
|
|
1582
1726
|
|
|
1727
|
+
# With {#resume_session_at}: the UUID of the user prompt whose turn this
|
|
1728
|
+
# truncating resume intends to discard.
|
|
1729
|
+
#
|
|
1730
|
+
# When set, the CLI validates at load time that every transcript entry
|
|
1731
|
+
# after the `resume_session_at` point is attributable to that turn, and
|
|
1732
|
+
# refuses the resume otherwise — e.g. when the discarded range contains a
|
|
1733
|
+
# queued user message or task notification the session absorbed mid-turn
|
|
1734
|
+
# that the caller had not yet observed. Leave unset to keep the
|
|
1735
|
+
# unvalidated truncation behavior.
|
|
1736
|
+
#
|
|
1737
|
+
# **Choosing the fork point.** Set `resume_session_at` to the *last*
|
|
1738
|
+
# transcript entry of the turn you are keeping — whatever its type — and
|
|
1739
|
+
# `resume_drops_turn` to the prompt UUID of the turn immediately after it
|
|
1740
|
+
# (e.g. the next `SessionMessage` of `type == "user"` from
|
|
1741
|
+
# {ClaudeAgentSDK.get_session_messages}, or the `uuid` you supplied on a
|
|
1742
|
+
# streamed user message). Note that with structured output
|
|
1743
|
+
# ({#output_format}) or end-turn MCP tools a kept turn ends on entries
|
|
1744
|
+
# *after* its last assistant message, so forking at the assistant UUID is
|
|
1745
|
+
# refused by design.
|
|
1746
|
+
#
|
|
1747
|
+
# **On refusal.** The CLI reports an `error_during_execution` result whose
|
|
1748
|
+
# message starts with `Resume rejected by --resume-drops-turn:` — match on
|
|
1749
|
+
# that text. Treat it as deterministic: clear the pending fork target and
|
|
1750
|
+
# resume plainly rather than retrying the same request.
|
|
1751
|
+
#
|
|
1752
|
+
# Forwarded whenever it is not `nil`. An empty string reaches the CLI and
|
|
1753
|
+
# is rejected there as a malformed declaration rather than being dropped by
|
|
1754
|
+
# the SDK, which would silently disarm the guard you believe is armed. The
|
|
1755
|
+
# SDK does not validate the option combination (`resume` /
|
|
1756
|
+
# `resume_session_at`); like the TypeScript and Python SDKs that is the
|
|
1757
|
+
# CLI's call.
|
|
1758
|
+
#
|
|
1759
|
+
# @return [String, nil]
|
|
1760
|
+
# @see #resume_session_at
|
|
1761
|
+
attr_accessor :resume_drops_turn
|
|
1762
|
+
|
|
1583
1763
|
def initialize(attributes = {})
|
|
1584
1764
|
self.fork_session = false
|
|
1585
1765
|
self.continue_conversation = false
|
|
@@ -1587,6 +1767,7 @@ module ClaudeAgentSDK
|
|
|
1587
1767
|
self.enable_file_checkpointing = false
|
|
1588
1768
|
self.include_hook_events = false
|
|
1589
1769
|
self.strict_mcp_config = false
|
|
1770
|
+
self.forward_subagent_text = false
|
|
1590
1771
|
|
|
1591
1772
|
super(merge_with_defaults(attributes || {}))
|
|
1592
1773
|
|
|
@@ -1675,6 +1856,38 @@ module ClaudeAgentSDK
|
|
|
1675
1856
|
@strict_mcp_config = coerce_boolean(value)
|
|
1676
1857
|
end
|
|
1677
1858
|
|
|
1859
|
+
# Forward subagent text and thinking blocks as messages in the stream.
|
|
1860
|
+
# Defaults to `false`.
|
|
1861
|
+
#
|
|
1862
|
+
# By default only `tool_use` / `tool_result` blocks from subagents
|
|
1863
|
+
# (spawned via the Agent tool) are emitted, as {AssistantMessage} /
|
|
1864
|
+
# {UserMessage} objects whose `parent_tool_use_id` is the spawning Agent
|
|
1865
|
+
# `tool_use` id — enough for a progress heartbeat. When true, the
|
|
1866
|
+
# subagent's text and thinking blocks are forwarded the same way, so
|
|
1867
|
+
# consumers can render the full nested transcript. Matches the TypeScript
|
|
1868
|
+
# SDK's `forwardSubagentText`.
|
|
1869
|
+
#
|
|
1870
|
+
# Sent as the `forwardSubagentText` initialize capability rather than a CLI
|
|
1871
|
+
# flag, and only when enabled, so an older CLI never sees an unknown key on
|
|
1872
|
+
# the common path. Both {ClaudeAgentSDK.query} and {Client} run the control
|
|
1873
|
+
# protocol, so the option applies to either entry point.
|
|
1874
|
+
#
|
|
1875
|
+
# Assigning coerces to a Boolean; {#forward_subagent_text?} is the
|
|
1876
|
+
# predicate form.
|
|
1877
|
+
#
|
|
1878
|
+
# @return [Boolean]
|
|
1879
|
+
attr_reader :forward_subagent_text
|
|
1880
|
+
|
|
1881
|
+
# @return [Boolean] {#forward_subagent_text}, as a strict Boolean.
|
|
1882
|
+
def forward_subagent_text?
|
|
1883
|
+
!!forward_subagent_text
|
|
1884
|
+
end
|
|
1885
|
+
|
|
1886
|
+
# @see #forward_subagent_text
|
|
1887
|
+
def forward_subagent_text=(value)
|
|
1888
|
+
@forward_subagent_text = coerce_boolean(value)
|
|
1889
|
+
end
|
|
1890
|
+
|
|
1678
1891
|
CALLBACK_SCHEDULING_MODES = %i[thread inline].freeze
|
|
1679
1892
|
|
|
1680
1893
|
# Where user callbacks (hooks, can_use_tool, SDK MCP handlers, message
|
data/lib/claude_agent_sdk.rb
CHANGED
|
@@ -64,6 +64,34 @@ module ClaudeAgentSDK
|
|
|
64
64
|
servers
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
+
# Internal: validate can_use_tool and route permission prompts over stdio.
|
|
68
|
+
#
|
|
69
|
+
# Shared by query() and Client#connect so both entry points enforce the
|
|
70
|
+
# same rules. Returns options unchanged when no callback is set; otherwise
|
|
71
|
+
# checks it is not combined with permission_prompt_tool_name, emits the
|
|
72
|
+
# shadowing advisory, and returns a copy with permission_prompt_tool_name
|
|
73
|
+
# set to 'stdio' so the CLI sends permission requests over the control
|
|
74
|
+
# protocol.
|
|
75
|
+
#
|
|
76
|
+
# A String prompt is fine here: the SDK is always streaming internally (a
|
|
77
|
+
# String is written to stdin as a user message like any other), so as long
|
|
78
|
+
# as stdin stays open for the turn — which Query#bidirectional_needs? now
|
|
79
|
+
# guarantees for can_use_tool — the permission round-trip works. The old
|
|
80
|
+
# "requires streaming mode" ArgumentError was a needless restriction
|
|
81
|
+
# (Python #1204).
|
|
82
|
+
def self.configure_can_use_tool(options)
|
|
83
|
+
return options unless options.can_use_tool
|
|
84
|
+
|
|
85
|
+
# can_use_tool and permission_prompt_tool_name are mutually exclusive
|
|
86
|
+
raise ArgumentError, 'can_use_tool callback cannot be used with permission_prompt_tool_name' if options.permission_prompt_tool_name
|
|
87
|
+
|
|
88
|
+
# Advisory: warn if other options shadow the callback. After the
|
|
89
|
+
# ArgumentError above so invalid configs raise, not warn.
|
|
90
|
+
OptionWarnings.warn_if_can_use_tool_shadowed(options)
|
|
91
|
+
|
|
92
|
+
options.dup_with(permission_prompt_tool_name: 'stdio')
|
|
93
|
+
end
|
|
94
|
+
|
|
67
95
|
# Internal: pull exclude_dynamic_sections out of a preset system prompt for
|
|
68
96
|
# the initialize request (older CLIs ignore unknown initialize fields).
|
|
69
97
|
# Shared by Client#connect and the one-shot query() path.
|
|
@@ -81,6 +109,26 @@ module ClaudeAgentSDK
|
|
|
81
109
|
nil
|
|
82
110
|
end
|
|
83
111
|
|
|
112
|
+
# Internal: pull snapshot out of a preset or custom system prompt for the
|
|
113
|
+
# initialize request (older CLIs ignore unknown initialize fields). A
|
|
114
|
+
# String or file prompt has no snapshot, and only a genuine true/false is
|
|
115
|
+
# forwarded — `snapshot: false` is the primary use case, so the Hash lookup
|
|
116
|
+
# must not collapse it to nil. Shared by Client#connect and query().
|
|
117
|
+
def self.extract_system_prompt_snapshot(system_prompt)
|
|
118
|
+
case system_prompt
|
|
119
|
+
when SystemPromptPreset, SystemPromptCustom
|
|
120
|
+
snapshot = system_prompt.snapshot
|
|
121
|
+
return snapshot if [true, false].include?(snapshot)
|
|
122
|
+
when Hash
|
|
123
|
+
type = system_prompt[:type] || system_prompt['type']
|
|
124
|
+
if %w[preset custom].include?(type)
|
|
125
|
+
snapshot = system_prompt.fetch(:snapshot) { system_prompt['snapshot'] }
|
|
126
|
+
return snapshot if [true, false].include?(snapshot)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
131
|
+
|
|
84
132
|
# Safely call a method on each observer, suppressing any errors.
|
|
85
133
|
# Each observer is invoked through FiberBoundary so that user code runs
|
|
86
134
|
# on a plain thread (no Fiber scheduler) even when called from inside
|
|
@@ -448,21 +496,7 @@ module ClaudeAgentSDK
|
|
|
448
496
|
|
|
449
497
|
options ||= ClaudeAgentOptions.new
|
|
450
498
|
|
|
451
|
-
configured_options = options
|
|
452
|
-
if options.can_use_tool
|
|
453
|
-
if prompt.is_a?(String)
|
|
454
|
-
raise ArgumentError,
|
|
455
|
-
'can_use_tool callback requires streaming mode. Please provide prompt as an Enumerator instead of a String.'
|
|
456
|
-
end
|
|
457
|
-
|
|
458
|
-
raise ArgumentError, 'can_use_tool callback cannot be used with permission_prompt_tool_name' if options.permission_prompt_tool_name
|
|
459
|
-
|
|
460
|
-
configured_options = options.dup_with(permission_prompt_tool_name: 'stdio')
|
|
461
|
-
end
|
|
462
|
-
|
|
463
|
-
# Advisory: warn if other options shadow the can_use_tool callback.
|
|
464
|
-
# After the ArgumentError validations so invalid configs raise, not warn.
|
|
465
|
-
OptionWarnings.warn_if_can_use_tool_shadowed(options)
|
|
499
|
+
configured_options = ClaudeAgentSDK.configure_can_use_tool(options)
|
|
466
500
|
|
|
467
501
|
# Fail fast on invalid session_store combinations before spawning the CLI.
|
|
468
502
|
SessionStores.validate_session_store_options(configured_options)
|
|
@@ -535,7 +569,9 @@ module ClaudeAgentSDK
|
|
|
535
569
|
agents: configured_options.agents,
|
|
536
570
|
sdk_mcp_servers: sdk_mcp_servers,
|
|
537
571
|
exclude_dynamic_sections: ClaudeAgentSDK.extract_exclude_dynamic_sections(configured_options.system_prompt),
|
|
572
|
+
system_prompt_snapshot: ClaudeAgentSDK.extract_system_prompt_snapshot(configured_options.system_prompt),
|
|
538
573
|
skills: configured_options.skills,
|
|
574
|
+
forward_subagent_text: configured_options.forward_subagent_text?,
|
|
539
575
|
callback_scheduling: callback_scheduling,
|
|
540
576
|
callback_wrapper: callback_wrapper
|
|
541
577
|
)
|
|
@@ -742,18 +778,7 @@ module ClaudeAgentSDK
|
|
|
742
778
|
raise ArgumentError, "prompt must be a String, an Enumerator, or nil (got #{prompt.class})" unless prompt.nil? || prompt.is_a?(String) || prompt.respond_to?(:each)
|
|
743
779
|
|
|
744
780
|
# Validate and configure permission settings
|
|
745
|
-
configured_options = @options
|
|
746
|
-
if @options.can_use_tool
|
|
747
|
-
# can_use_tool and permission_prompt_tool_name are mutually exclusive
|
|
748
|
-
raise ArgumentError, 'can_use_tool callback cannot be used with permission_prompt_tool_name' if @options.permission_prompt_tool_name
|
|
749
|
-
|
|
750
|
-
# Set permission_prompt_tool_name to stdio for control protocol
|
|
751
|
-
configured_options = @options.dup_with(permission_prompt_tool_name: 'stdio')
|
|
752
|
-
end
|
|
753
|
-
|
|
754
|
-
# Advisory: warn if other options shadow the can_use_tool callback.
|
|
755
|
-
# After the ArgumentError validations so invalid configs raise, not warn.
|
|
756
|
-
OptionWarnings.warn_if_can_use_tool_shadowed(@options)
|
|
781
|
+
configured_options = ClaudeAgentSDK.configure_can_use_tool(@options)
|
|
757
782
|
|
|
758
783
|
# Fail fast on invalid session_store combinations before spawning the CLI.
|
|
759
784
|
# Configuration validation is a usage error, like the ArgumentErrors
|
|
@@ -1060,9 +1085,10 @@ module ClaudeAgentSDK
|
|
|
1060
1085
|
# Convert hooks to internal format
|
|
1061
1086
|
hooks = convert_hooks_to_internal_format(configured_options.hooks) if configured_options.hooks
|
|
1062
1087
|
|
|
1063
|
-
# Extract exclude_dynamic_sections from
|
|
1064
|
-
# initialize request (older CLIs ignore unknown initialize fields)
|
|
1088
|
+
# Extract exclude_dynamic_sections and snapshot from the system prompt
|
|
1089
|
+
# for the initialize request (older CLIs ignore unknown initialize fields)
|
|
1065
1090
|
exclude_dynamic_sections = ClaudeAgentSDK.extract_exclude_dynamic_sections(configured_options.system_prompt)
|
|
1091
|
+
system_prompt_snapshot = ClaudeAgentSDK.extract_system_prompt_snapshot(configured_options.system_prompt)
|
|
1066
1092
|
|
|
1067
1093
|
# Create Query handler
|
|
1068
1094
|
@query_handler = Query.new(
|
|
@@ -1073,7 +1099,9 @@ module ClaudeAgentSDK
|
|
|
1073
1099
|
sdk_mcp_servers: sdk_mcp_servers,
|
|
1074
1100
|
agents: configured_options.agents,
|
|
1075
1101
|
exclude_dynamic_sections: exclude_dynamic_sections,
|
|
1102
|
+
system_prompt_snapshot: system_prompt_snapshot,
|
|
1076
1103
|
skills: configured_options.skills,
|
|
1104
|
+
forward_subagent_text: configured_options.forward_subagent_text?,
|
|
1077
1105
|
callback_scheduling: @callback_scheduling,
|
|
1078
1106
|
callback_wrapper: @callback_wrapper
|
|
1079
1107
|
)
|
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.32.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-
|
|
11
|
+
date: 2026-09-17 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: async
|
|
@@ -110,6 +110,7 @@ files:
|
|
|
110
110
|
- CHANGELOG.md
|
|
111
111
|
- LICENSE
|
|
112
112
|
- README.md
|
|
113
|
+
- docs/cli-installer.md
|
|
113
114
|
- docs/client.md
|
|
114
115
|
- docs/configuration.md
|
|
115
116
|
- docs/errors.md
|