claude-agent-sdk 0.32.0 → 0.33.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 +35 -0
- data/README.md +3 -2
- data/docs/client.md +4 -0
- data/docs/configuration.md +33 -0
- data/docs/hooks-and-permissions.md +66 -2
- data/docs/sessions.md +28 -0
- data/docs/subagents.md +198 -0
- data/docs/types.md +34 -4
- data/lib/claude_agent_sdk/cancellation_signal.rb +28 -0
- data/lib/claude_agent_sdk/message_parser.rb +3 -1
- data/lib/claude_agent_sdk/query.rb +113 -24
- data/lib/claude_agent_sdk/sdk_mcp_server.rb +10 -0
- data/lib/claude_agent_sdk/session_mutations.rb +2 -4
- data/lib/claude_agent_sdk/sessions.rb +37 -0
- data/lib/claude_agent_sdk/types.rb +224 -12
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +43 -0
- metadata +8 -6
|
@@ -8,6 +8,7 @@ require 'async/condition'
|
|
|
8
8
|
require 'securerandom'
|
|
9
9
|
require_relative 'transport'
|
|
10
10
|
require_relative 'errors'
|
|
11
|
+
require_relative 'cancellation_signal'
|
|
11
12
|
|
|
12
13
|
module ClaudeAgentSDK
|
|
13
14
|
# Handles bidirectional control protocol on top of Transport
|
|
@@ -64,7 +65,8 @@ module ClaudeAgentSDK
|
|
|
64
65
|
|
|
65
66
|
def initialize(transport:, is_streaming_mode:, can_use_tool: nil, hooks: nil, sdk_mcp_servers: nil, agents: nil,
|
|
66
67
|
exclude_dynamic_sections: nil, system_prompt_snapshot: nil, skills: nil,
|
|
67
|
-
forward_subagent_text: false,
|
|
68
|
+
forward_subagent_text: false, agent_progress_summaries: nil,
|
|
69
|
+
callback_scheduling: :thread, callback_wrapper: nil)
|
|
68
70
|
@transport = transport
|
|
69
71
|
@is_streaming_mode = is_streaming_mode
|
|
70
72
|
@can_use_tool = can_use_tool
|
|
@@ -77,6 +79,7 @@ module ClaudeAgentSDK
|
|
|
77
79
|
@system_prompt_snapshot = system_prompt_snapshot
|
|
78
80
|
@skills = skills
|
|
79
81
|
@forward_subagent_text = forward_subagent_text
|
|
82
|
+
@agent_progress_summaries = agent_progress_summaries
|
|
80
83
|
|
|
81
84
|
# Control protocol state
|
|
82
85
|
@pending_control_responses = {}
|
|
@@ -87,6 +90,7 @@ module ClaudeAgentSDK
|
|
|
87
90
|
@request_counter = 0
|
|
88
91
|
@request_counter_mutex = Mutex.new
|
|
89
92
|
@inflight_control_request_tasks = {}
|
|
93
|
+
@callback_request_signals = {}
|
|
90
94
|
|
|
91
95
|
# Message stream
|
|
92
96
|
@message_queue = Async::Queue.new
|
|
@@ -199,6 +203,10 @@ module ClaudeAgentSDK
|
|
|
199
203
|
# Off is the CLI default, so only send the field when enabled — an
|
|
200
204
|
# older CLI then never sees an unknown key on the common path.
|
|
201
205
|
request[:forwardSubagentText] = true if @forward_subagent_text
|
|
206
|
+
# Unset (nil) omits the key; true/false are forwarded verbatim. Not a live toggle: CLI
|
|
207
|
+
# 2.1.278 only acts on a truthy value, so false is schema-valid but
|
|
208
|
+
# equivalent to omitting the key.
|
|
209
|
+
request[:agentProgressSummaries] = @agent_progress_summaries unless @agent_progress_summaries.nil?
|
|
202
210
|
|
|
203
211
|
response = send_control_request(request)
|
|
204
212
|
@initialized = true
|
|
@@ -331,7 +339,11 @@ module ClaudeAgentSDK
|
|
|
331
339
|
begin
|
|
332
340
|
handle_control_request(message)
|
|
333
341
|
ensure
|
|
334
|
-
|
|
342
|
+
# Identity-guarded: if the CLI ever reused an in-flight request
|
|
343
|
+
# id, the later handler owns the slot and must stay cancellable.
|
|
344
|
+
if request_id && @inflight_control_request_tasks[request_id].equal?(Async::Task.current)
|
|
345
|
+
@inflight_control_request_tasks.delete(request_id)
|
|
346
|
+
end
|
|
335
347
|
end
|
|
336
348
|
end
|
|
337
349
|
# A handler that never suspends (MCP metadata, unsupported-subtype
|
|
@@ -341,6 +353,7 @@ module ClaudeAgentSDK
|
|
|
341
353
|
@inflight_control_request_tasks[request_id] = handler_task if request_id && !handler_task.finished?
|
|
342
354
|
when 'control_cancel_request'
|
|
343
355
|
request_id = message[:request_id] || message[:requestId]
|
|
356
|
+
@callback_request_signals[request_id]&.cancel
|
|
344
357
|
task = request_id ? @inflight_control_request_tasks[request_id] : nil
|
|
345
358
|
task&.stop
|
|
346
359
|
next
|
|
@@ -418,6 +431,9 @@ module ClaudeAgentSDK
|
|
|
418
431
|
# Put error in queue so iterators can handle it
|
|
419
432
|
@message_queue.enqueue({ type: 'error', error: error })
|
|
420
433
|
ensure
|
|
434
|
+
# A callback can no longer be answered after EOF, transport failure,
|
|
435
|
+
# or reactor cancellation. Wake cooperative worker-thread callbacks too.
|
|
436
|
+
@callback_request_signals.dup.each_value(&:cancel)
|
|
421
437
|
# Catch entries from a turn that ended without a `result` (early EOF /
|
|
422
438
|
# transport error) so they aren't dropped. The flush can suspend (lock
|
|
423
439
|
# acquire / thread join), so Async::Stop delivered mid-flush would skip
|
|
@@ -539,9 +555,9 @@ module ClaudeAgentSDK
|
|
|
539
555
|
|
|
540
556
|
case subtype
|
|
541
557
|
when 'can_use_tool'
|
|
542
|
-
response_data = handle_permission_request(request_data)
|
|
558
|
+
response_data = handle_permission_request(request_data, request_id: request_id)
|
|
543
559
|
when 'hook_callback'
|
|
544
|
-
response_data = handle_hook_callback(request_data)
|
|
560
|
+
response_data = handle_hook_callback(request_data, request_id: request_id)
|
|
545
561
|
when 'mcp_message'
|
|
546
562
|
response_data = handle_mcp_message(request_data)
|
|
547
563
|
else
|
|
@@ -561,33 +577,34 @@ module ClaudeAgentSDK
|
|
|
561
577
|
writeln(JSON.generate(success_response))
|
|
562
578
|
rescue Async::Stop
|
|
563
579
|
# Cancellation requested; respond with an error so the CLI can unblock.
|
|
564
|
-
|
|
565
|
-
type: 'control_response',
|
|
566
|
-
response: {
|
|
567
|
-
subtype: 'error',
|
|
568
|
-
request_id: request_id,
|
|
569
|
-
requestId: request_id,
|
|
570
|
-
error: 'Cancelled'
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
writeln(JSON.generate(cancelled_response))
|
|
580
|
+
send_control_error(request_id, 'Cancelled')
|
|
574
581
|
rescue StandardError => e
|
|
575
|
-
|
|
582
|
+
send_control_error(request_id, e.message)
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
def send_control_error(request_id, message)
|
|
576
586
|
error_response = {
|
|
577
587
|
type: 'control_response',
|
|
578
588
|
response: {
|
|
579
589
|
subtype: 'error',
|
|
580
590
|
request_id: request_id,
|
|
581
591
|
requestId: request_id,
|
|
582
|
-
error:
|
|
592
|
+
error: message
|
|
583
593
|
}
|
|
584
594
|
}
|
|
585
595
|
writeln(JSON.generate(error_response))
|
|
596
|
+
rescue CLIConnectionError
|
|
597
|
+
# EOF/close can invalidate a callback after the peer has gone away.
|
|
598
|
+
# Only this best-effort reply is discarded; read errors still reach
|
|
599
|
+
# the message queue through read_messages.
|
|
600
|
+
nil
|
|
586
601
|
end
|
|
587
602
|
|
|
588
|
-
def handle_permission_request(request_data)
|
|
603
|
+
def handle_permission_request(request_data, request_id: nil)
|
|
589
604
|
raise 'canUseTool callback is not provided' unless @can_use_tool
|
|
590
605
|
|
|
606
|
+
signal = CancellationSignal.new
|
|
607
|
+
@callback_request_signals[request_id] = signal if request_id
|
|
591
608
|
original_input = request_data[:input]
|
|
592
609
|
|
|
593
610
|
# Field order mirrors Python _internal/query.py's can_use_tool branch.
|
|
@@ -595,7 +612,8 @@ module ClaudeAgentSDK
|
|
|
595
612
|
# malformed entry raises here, on the reactor, and becomes an error
|
|
596
613
|
# control_response — same observable behavior as Python.
|
|
597
614
|
context = ToolPermissionContext.new(
|
|
598
|
-
signal:
|
|
615
|
+
signal: signal,
|
|
616
|
+
request_id: request_id,
|
|
599
617
|
suggestions: (request_data[:permission_suggestions] || []).map { |s| PermissionUpdate.new(s) },
|
|
600
618
|
tool_use_id: request_data[:tool_use_id],
|
|
601
619
|
agent_id: request_data[:agent_id],
|
|
@@ -614,6 +632,9 @@ module ClaudeAgentSDK
|
|
|
614
632
|
response = FiberBoundary.invoke(scheduling: @callback_scheduling, wrapper: @callback_wrapper) do
|
|
615
633
|
@can_use_tool.call(request_data[:tool_name], request_data[:input], context)
|
|
616
634
|
end
|
|
635
|
+
# A worker may return a decision after the read loop invalidated the
|
|
636
|
+
# request. Never turn that late decision into an allow response.
|
|
637
|
+
raise Async::Stop if signal.cancelled?
|
|
617
638
|
|
|
618
639
|
# Convert PermissionResult to expected format
|
|
619
640
|
case response
|
|
@@ -633,19 +654,27 @@ module ClaudeAgentSDK
|
|
|
633
654
|
else
|
|
634
655
|
raise "Tool permission callback must return PermissionResult, got #{response.class}"
|
|
635
656
|
end
|
|
657
|
+
completed = true
|
|
658
|
+
result
|
|
659
|
+
ensure
|
|
660
|
+
signal&.cancel unless completed
|
|
661
|
+
untrack_callback_signal(request_id, signal)
|
|
636
662
|
end
|
|
637
663
|
|
|
638
|
-
def handle_hook_callback(request_data)
|
|
664
|
+
def handle_hook_callback(request_data, request_id: nil)
|
|
639
665
|
callback_id = request_data[:callback_id]
|
|
640
666
|
callback = @hook_callbacks[callback_id]
|
|
641
667
|
raise "No hook callback found for ID: #{callback_id}" unless callback
|
|
642
668
|
|
|
669
|
+
signal = CancellationSignal.new
|
|
670
|
+
@callback_request_signals[request_id] = signal if request_id
|
|
671
|
+
|
|
643
672
|
# Parse input data into typed HookInput object
|
|
644
673
|
input_data = request_data[:input] || {}
|
|
645
674
|
hook_input = parse_hook_input(input_data)
|
|
646
675
|
|
|
647
676
|
# Create typed HookContext
|
|
648
|
-
context = HookContext.new(signal:
|
|
677
|
+
context = HookContext.new(signal: signal, request_id: request_id)
|
|
649
678
|
|
|
650
679
|
# Hop off the Fiber scheduler before invoking user hook code (default
|
|
651
680
|
# :thread mode). With a timeout, the Async-side with_timeout wraps the
|
|
@@ -693,8 +722,25 @@ module ClaudeAgentSDK
|
|
|
693
722
|
end
|
|
694
723
|
end
|
|
695
724
|
|
|
725
|
+
# A thread callback may finish after EOF/close invalidated its request.
|
|
726
|
+
raise Async::Stop if signal.cancelled?
|
|
727
|
+
|
|
696
728
|
# Convert Ruby-safe field names to CLI-expected names
|
|
697
|
-
convert_hook_output_for_cli(hook_output)
|
|
729
|
+
result = convert_hook_output_for_cli(hook_output)
|
|
730
|
+
completed = true
|
|
731
|
+
result
|
|
732
|
+
ensure
|
|
733
|
+
signal&.cancel unless completed
|
|
734
|
+
untrack_callback_signal(request_id, signal)
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
# Identity-guarded for the same reason as the in-flight task map: a handler
|
|
738
|
+
# only untracks its own signal, never a later request that reused its id —
|
|
739
|
+
# otherwise EOF/close could no longer invalidate that later request.
|
|
740
|
+
def untrack_callback_signal(request_id, signal)
|
|
741
|
+
return unless request_id && @callback_request_signals[request_id].equal?(signal)
|
|
742
|
+
|
|
743
|
+
@callback_request_signals.delete(request_id)
|
|
698
744
|
end
|
|
699
745
|
|
|
700
746
|
def parse_hook_input(input_data)
|
|
@@ -753,6 +799,8 @@ module ClaudeAgentSDK
|
|
|
753
799
|
StopHookInput.new(
|
|
754
800
|
stop_hook_active: fetch.call(:stop_hook_active),
|
|
755
801
|
last_assistant_message: fetch.call(:last_assistant_message),
|
|
802
|
+
background_tasks: fetch.call(:background_tasks),
|
|
803
|
+
session_crons: fetch.call(:session_crons),
|
|
756
804
|
**base_args
|
|
757
805
|
)
|
|
758
806
|
when 'SubagentStop'
|
|
@@ -762,6 +810,8 @@ module ClaudeAgentSDK
|
|
|
762
810
|
agent_transcript_path: fetch.call(:agent_transcript_path),
|
|
763
811
|
agent_type: fetch.call(:agent_type),
|
|
764
812
|
last_assistant_message: fetch.call(:last_assistant_message),
|
|
813
|
+
background_tasks: fetch.call(:background_tasks),
|
|
814
|
+
session_crons: fetch.call(:session_crons),
|
|
765
815
|
**base_args
|
|
766
816
|
)
|
|
767
817
|
when 'Notification'
|
|
@@ -1145,9 +1195,12 @@ module ClaudeAgentSDK
|
|
|
1145
1195
|
# server validates arguments against the tool's inputSchema BEFORE the
|
|
1146
1196
|
# handler runs and reports validation failures, unknown tools, and
|
|
1147
1197
|
# handler exceptions as in-band isError results). tools/list,
|
|
1148
|
-
# initialize, resources/* and prompts/* stay on the SDK paths
|
|
1149
|
-
# gem
|
|
1150
|
-
#
|
|
1198
|
+
# initialize, resources/* and prompts/* stay on the SDK paths: the
|
|
1199
|
+
# gem's tools/list injects "$schema" and drops `required: []` (and
|
|
1200
|
+
# would advertise the empty fallback schema where the SDK advertises
|
|
1201
|
+
# the user's own), and its initialize negotiates newer protocol
|
|
1202
|
+
# versions and advertises prompts/resources/logging even for a
|
|
1203
|
+
# tools-only server. Annotations/_meta do survive the gem path.
|
|
1151
1204
|
server.handle_message(message)
|
|
1152
1205
|
end
|
|
1153
1206
|
|
|
@@ -1267,6 +1320,21 @@ module ClaudeAgentSDK
|
|
|
1267
1320
|
})
|
|
1268
1321
|
end
|
|
1269
1322
|
|
|
1323
|
+
# Background in-flight foreground tasks (Bash commands and subagents) — the
|
|
1324
|
+
# control-request equivalent of pressing Ctrl+B in the terminal.
|
|
1325
|
+
# @param tool_use_id [String, nil] The spawning tool_use block's id (not a
|
|
1326
|
+
# task_id or agent_id). nil is the explicit all-tasks form: it backgrounds
|
|
1327
|
+
# every foreground task
|
|
1328
|
+
# @return [Hash] Targeted: `{ backgrounded: true }`, or `{ backgrounded:
|
|
1329
|
+
# false }` — a definitive miss (no matching foreground task). All-tasks:
|
|
1330
|
+
# `{}`, which says nothing about whether any task existed
|
|
1331
|
+
# @raise [ArgumentError] if tool_use_id is neither nil nor a non-empty String
|
|
1332
|
+
def background_tasks(tool_use_id: nil)
|
|
1333
|
+
request = { subtype: 'background_tasks' }
|
|
1334
|
+
request[:tool_use_id] = background_selector(tool_use_id) unless tool_use_id.nil?
|
|
1335
|
+
send_control_request(request)
|
|
1336
|
+
end
|
|
1337
|
+
|
|
1270
1338
|
# Rewind files to a previous checkpoint (v0.1.15+)
|
|
1271
1339
|
# Restores file state to what it was at the given user message
|
|
1272
1340
|
# Requires enable_file_checkpointing to be true in options
|
|
@@ -1392,6 +1460,25 @@ module ClaudeAgentSDK
|
|
|
1392
1460
|
|
|
1393
1461
|
private
|
|
1394
1462
|
|
|
1463
|
+
# The selector actually sent for a targeted background_tasks request.
|
|
1464
|
+
#
|
|
1465
|
+
# The CLI normalizes "" to "background ALL foreground tasks", so a selector
|
|
1466
|
+
# built from a missing id (`id.to_s`) would release every blocking call.
|
|
1467
|
+
# Validate the value that goes on the wire, not the caller's object: a
|
|
1468
|
+
# private plain-String copy cannot be emptied by another thread between this
|
|
1469
|
+
# check and serialization (send_control_request can park on a mutex first),
|
|
1470
|
+
# and a String subclass cannot answer `empty?` or `to_json` for it. Never
|
|
1471
|
+
# normalize a bad selector to nil, and never strip — a whitespace-only id is
|
|
1472
|
+
# still a targeted selector CLI-side, so stripping would widen the request.
|
|
1473
|
+
def background_selector(tool_use_id)
|
|
1474
|
+
selector = String.new(tool_use_id) if String === tool_use_id # rubocop:disable Style/CaseEquality
|
|
1475
|
+
return selector unless selector.nil? || selector.empty?
|
|
1476
|
+
|
|
1477
|
+
raise ArgumentError,
|
|
1478
|
+
"tool_use_id must be a non-empty String (got #{tool_use_id.inspect}); " \
|
|
1479
|
+
'pass nil explicitly to background all foreground tasks'
|
|
1480
|
+
end
|
|
1481
|
+
|
|
1395
1482
|
def close_now
|
|
1396
1483
|
# First caller wins: a reactor-side close racing a watcher-served
|
|
1397
1484
|
# foreign close (or a repeated disconnect) must not re-run teardown
|
|
@@ -1410,6 +1497,8 @@ module ClaudeAgentSDK
|
|
|
1410
1497
|
return unless first_caller
|
|
1411
1498
|
|
|
1412
1499
|
@closed = true
|
|
1500
|
+
# Snapshot for the off-reactor fallback, like the response waiters below.
|
|
1501
|
+
@callback_request_signals.dup.each_value(&:cancel)
|
|
1413
1502
|
# Wake pending control-request waiters (same shape as the read-loop
|
|
1414
1503
|
# rescue broadcast): close stops the read task with Async::Stop, which
|
|
1415
1504
|
# bypasses that broadcast — a worker-thread caller parked in
|
|
@@ -453,6 +453,16 @@ module ClaudeAgentSDK
|
|
|
453
453
|
error: !!is_error,
|
|
454
454
|
structured_content: structured_content
|
|
455
455
|
)
|
|
456
|
+
rescue StandardError => e
|
|
457
|
+
# Report handler failures in-band HERE rather than letting them
|
|
458
|
+
# reach the gem: mcp >= 1.2 deliberately drops e.message from
|
|
459
|
+
# its "Internal error calling tool X" wrapper (CWE-209), which
|
|
460
|
+
# would hide the text the model needs to self-correct. Bare
|
|
461
|
+
# e.message like Python's str(e) and #call_tool — no prefix.
|
|
462
|
+
# Nothing gem-internal can be swallowed here today: handlers get
|
|
463
|
+
# no server_context, so MCP::CancelledError never originates
|
|
464
|
+
# inside this method. Revisit if cancellation is ever plumbed in.
|
|
465
|
+
MCP::Tool::Response.new([{ type: 'text', text: e.message }], error: true)
|
|
456
466
|
end
|
|
457
467
|
end
|
|
458
468
|
end
|
|
@@ -38,8 +38,7 @@ module ClaudeAgentSDK
|
|
|
38
38
|
stripped = title.strip
|
|
39
39
|
raise ArgumentError, 'title must be non-empty' if stripped.empty?
|
|
40
40
|
|
|
41
|
-
data = "#{JSON.generate({ type: 'custom-title', customTitle: stripped, sessionId: session_id }
|
|
42
|
-
space_size: 0)}\n"
|
|
41
|
+
data = "#{JSON.generate({ type: 'custom-title', customTitle: stripped, sessionId: session_id })}\n"
|
|
43
42
|
|
|
44
43
|
append_to_session(session_id, data, directory)
|
|
45
44
|
end
|
|
@@ -64,8 +63,7 @@ module ClaudeAgentSDK
|
|
|
64
63
|
tag = sanitized
|
|
65
64
|
end
|
|
66
65
|
|
|
67
|
-
data = "#{JSON.generate({ type: 'tag', tag: tag || '', sessionId: session_id }
|
|
68
|
-
space_size: 0)}\n"
|
|
66
|
+
data = "#{JSON.generate({ type: 'tag', tag: tag || '', sessionId: session_id })}\n"
|
|
69
67
|
|
|
70
68
|
append_to_session(session_id, data, directory)
|
|
71
69
|
end
|
|
@@ -596,6 +596,25 @@ module ClaudeAgentSDK
|
|
|
596
596
|
collect_agent_files(subagents_dir).map(&:first)
|
|
597
597
|
end
|
|
598
598
|
|
|
599
|
+
# Read the optional subagent metadata sidecar without reading its transcript.
|
|
600
|
+
# Uses the same project scoping and sorted first-match rule as the message
|
|
601
|
+
# reader. This is historical metadata, not a live status query.
|
|
602
|
+
# @return [Hash{String => Object}, nil] Original CLI fields, or nil if unavailable
|
|
603
|
+
def get_subagent_metadata(session_id:, agent_id:, directory: nil)
|
|
604
|
+
return nil unless session_id.match?(UUID_RE)
|
|
605
|
+
return nil if agent_id.nil? || agent_id.empty?
|
|
606
|
+
|
|
607
|
+
subagents_dir = resolve_subagents_dir(session_id, directory)
|
|
608
|
+
return nil if subagents_dir.nil?
|
|
609
|
+
|
|
610
|
+
_id, path = collect_agent_files(subagents_dir).find { |id, _path| id == agent_id }
|
|
611
|
+
return nil if path.nil?
|
|
612
|
+
|
|
613
|
+
read_agent_metadata_sidecar(path)
|
|
614
|
+
rescue SystemCallError
|
|
615
|
+
nil
|
|
616
|
+
end
|
|
617
|
+
|
|
599
618
|
# Read a subagent's conversation messages from local disk (counterpart to
|
|
600
619
|
# get_subagent_messages_from_store). First match in sorted walk order wins
|
|
601
620
|
# when the same agent id exists at multiple depths (mirrors Python).
|
|
@@ -760,6 +779,24 @@ module ClaudeAgentSDK
|
|
|
760
779
|
end
|
|
761
780
|
end
|
|
762
781
|
|
|
782
|
+
# Store counterpart to get_subagent_metadata. The last agent_metadata entry
|
|
783
|
+
# wins, including when no conversation messages have been mirrored yet.
|
|
784
|
+
# The synthetic `type` marker is omitted; all other string-keyed fields
|
|
785
|
+
# remain unchanged. Adapter failures propagate, like other store readers.
|
|
786
|
+
# @return [Hash{String => Object}, nil]
|
|
787
|
+
def get_subagent_metadata_from_store(session_store:, session_id:, agent_id:, directory: nil)
|
|
788
|
+
return nil unless session_id.match?(UUID_RE)
|
|
789
|
+
return nil if agent_id.nil? || agent_id.empty?
|
|
790
|
+
|
|
791
|
+
project_key = project_key_for_directory(directory)
|
|
792
|
+
subpath = resolve_subagent_subpath(session_store, project_key, session_id, agent_id)
|
|
793
|
+
return nil if subpath.nil?
|
|
794
|
+
|
|
795
|
+
entries = session_store.load('project_key' => project_key, 'session_id' => session_id, 'subpath' => subpath)
|
|
796
|
+
metadata, = split_agent_metadata(entries || [])
|
|
797
|
+
metadata&.except('type')
|
|
798
|
+
end
|
|
799
|
+
|
|
763
800
|
# Read a subagent's conversation messages from a SessionStore. Subagents may
|
|
764
801
|
# live at subagents/agent-<id> or nested under
|
|
765
802
|
# subagents/workflows/<runId>/agent-<id>; scans subkeys to resolve the path
|