claude-agent-sdk 0.32.0 → 0.33.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 +24 -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 +107 -21
- 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 +4 -2
|
@@ -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'
|
|
@@ -1267,6 +1317,21 @@ module ClaudeAgentSDK
|
|
|
1267
1317
|
})
|
|
1268
1318
|
end
|
|
1269
1319
|
|
|
1320
|
+
# Background in-flight foreground tasks (Bash commands and subagents) — the
|
|
1321
|
+
# control-request equivalent of pressing Ctrl+B in the terminal.
|
|
1322
|
+
# @param tool_use_id [String, nil] The spawning tool_use block's id (not a
|
|
1323
|
+
# task_id or agent_id). nil is the explicit all-tasks form: it backgrounds
|
|
1324
|
+
# every foreground task
|
|
1325
|
+
# @return [Hash] Targeted: `{ backgrounded: true }`, or `{ backgrounded:
|
|
1326
|
+
# false }` — a definitive miss (no matching foreground task). All-tasks:
|
|
1327
|
+
# `{}`, which says nothing about whether any task existed
|
|
1328
|
+
# @raise [ArgumentError] if tool_use_id is neither nil nor a non-empty String
|
|
1329
|
+
def background_tasks(tool_use_id: nil)
|
|
1330
|
+
request = { subtype: 'background_tasks' }
|
|
1331
|
+
request[:tool_use_id] = background_selector(tool_use_id) unless tool_use_id.nil?
|
|
1332
|
+
send_control_request(request)
|
|
1333
|
+
end
|
|
1334
|
+
|
|
1270
1335
|
# Rewind files to a previous checkpoint (v0.1.15+)
|
|
1271
1336
|
# Restores file state to what it was at the given user message
|
|
1272
1337
|
# Requires enable_file_checkpointing to be true in options
|
|
@@ -1392,6 +1457,25 @@ module ClaudeAgentSDK
|
|
|
1392
1457
|
|
|
1393
1458
|
private
|
|
1394
1459
|
|
|
1460
|
+
# The selector actually sent for a targeted background_tasks request.
|
|
1461
|
+
#
|
|
1462
|
+
# The CLI normalizes "" to "background ALL foreground tasks", so a selector
|
|
1463
|
+
# built from a missing id (`id.to_s`) would release every blocking call.
|
|
1464
|
+
# Validate the value that goes on the wire, not the caller's object: a
|
|
1465
|
+
# private plain-String copy cannot be emptied by another thread between this
|
|
1466
|
+
# check and serialization (send_control_request can park on a mutex first),
|
|
1467
|
+
# and a String subclass cannot answer `empty?` or `to_json` for it. Never
|
|
1468
|
+
# normalize a bad selector to nil, and never strip — a whitespace-only id is
|
|
1469
|
+
# still a targeted selector CLI-side, so stripping would widen the request.
|
|
1470
|
+
def background_selector(tool_use_id)
|
|
1471
|
+
selector = String.new(tool_use_id) if String === tool_use_id # rubocop:disable Style/CaseEquality
|
|
1472
|
+
return selector unless selector.nil? || selector.empty?
|
|
1473
|
+
|
|
1474
|
+
raise ArgumentError,
|
|
1475
|
+
"tool_use_id must be a non-empty String (got #{tool_use_id.inspect}); " \
|
|
1476
|
+
'pass nil explicitly to background all foreground tasks'
|
|
1477
|
+
end
|
|
1478
|
+
|
|
1395
1479
|
def close_now
|
|
1396
1480
|
# First caller wins: a reactor-side close racing a watcher-served
|
|
1397
1481
|
# foreign close (or a repeated disconnect) must not re-run teardown
|
|
@@ -1410,6 +1494,8 @@ module ClaudeAgentSDK
|
|
|
1410
1494
|
return unless first_caller
|
|
1411
1495
|
|
|
1412
1496
|
@closed = true
|
|
1497
|
+
# Snapshot for the off-reactor fallback, like the response waiters below.
|
|
1498
|
+
@callback_request_signals.dup.each_value(&:cancel)
|
|
1413
1499
|
# Wake pending control-request waiters (same shape as the read-loop
|
|
1414
1500
|
# rescue broadcast): close stops the read task with Async::Stop, which
|
|
1415
1501
|
# bypasses that broadcast — a worker-thread caller parked in
|
|
@@ -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
|
|
@@ -421,12 +421,51 @@ module ClaudeAgentSDK
|
|
|
421
421
|
# Task started system message (subagent/background task started)
|
|
422
422
|
class TaskStartedMessage < SystemMessage
|
|
423
423
|
attr_accessor :task_id, :description, :uuid, :session_id, :tool_use_id, :task_type,
|
|
424
|
-
:workflow_name, :prompt
|
|
424
|
+
:workflow_name, :prompt,
|
|
425
|
+
:subagent_type # Subagent type, for Task/Agent tool subagents; nil otherwise
|
|
426
|
+
|
|
427
|
+
# Whether the task was registered in the background (`true`) or in the
|
|
428
|
+
# foreground with the spawning tool call blocking on it (`false`). `nil`
|
|
429
|
+
# means the CLI did not say (the field is optional, and only set for
|
|
430
|
+
# `local_agent` and `local_bash` tasks) — so test `== false`, never
|
|
431
|
+
# falsiness, to detect a foreground/blocking task. A resumed subagent is
|
|
432
|
+
# always registered in the background. A later move to the background does
|
|
433
|
+
# not re-emit task_started; it arrives as {TaskUpdatedMessage#is_backgrounded}.
|
|
434
|
+
#
|
|
435
|
+
# @return [Boolean, nil]
|
|
436
|
+
attr_accessor :is_backgrounded
|
|
437
|
+
|
|
438
|
+
# Nesting depth of a spawned subagent (`local_agent`) task: 1 for a
|
|
439
|
+
# top-level spawn, N+1 when spawned from inside a depth-N agent. `nil` on
|
|
440
|
+
# other task types and on CLIs that do not report it.
|
|
441
|
+
#
|
|
442
|
+
# @return [Integer, nil]
|
|
443
|
+
attr_accessor :spawn_depth
|
|
444
|
+
|
|
445
|
+
# Display flags, passed through for the host to act on — the SDK never
|
|
446
|
+
# filters frames or computes activity from them. Both are optional
|
|
447
|
+
# Booleans: `nil` when absent, an explicit `false` preserved.
|
|
448
|
+
#
|
|
449
|
+
# - `skip_transcript`: an ambient/housekeeping task. Hide it from the
|
|
450
|
+
# inline transcript; it may still appear in a tasks panel.
|
|
451
|
+
# - `ambient`: true for tasks that are not activity — every
|
|
452
|
+
# `skip_transcript` task, plus every live-update watcher (requested or
|
|
453
|
+
# auto-started). Exclude these from activity indicators.
|
|
454
|
+
#
|
|
455
|
+
# @return [Boolean, nil]
|
|
456
|
+
attr_accessor :skip_transcript, :ambient
|
|
425
457
|
end
|
|
426
458
|
|
|
427
|
-
# Task progress system message (periodic update from a running task)
|
|
459
|
+
# Task progress system message (periodic update from a running task).
|
|
460
|
+
#
|
|
461
|
+
# `summary` is an optional one-line status for the task's row — `nil` on any
|
|
462
|
+
# frame that lacks one. For a `local_agent` task it is the model-generated
|
|
463
|
+
# progress summary, which the CLI produces only while generation is enabled
|
|
464
|
+
# (see {ClaudeAgentOptions#agent_progress_summaries}); for a backgrounded
|
|
465
|
+
# `mcp_task` it is the MCP server's own status message and needs no option.
|
|
428
466
|
class TaskProgressMessage < SystemMessage
|
|
429
|
-
attr_accessor :task_id, :description, :usage, :uuid, :session_id, :tool_use_id, :last_tool_name, :summary
|
|
467
|
+
attr_accessor :task_id, :description, :usage, :uuid, :session_id, :tool_use_id, :last_tool_name, :summary,
|
|
468
|
+
:subagent_type # Subagent type, for Task/Agent tool subagents; nil otherwise
|
|
430
469
|
end
|
|
431
470
|
|
|
432
471
|
# Task notification system message (task completed/failed/stopped).
|
|
@@ -437,6 +476,42 @@ module ClaudeAgentSDK
|
|
|
437
476
|
# should clear them on a terminal status from *either* message.
|
|
438
477
|
class TaskNotificationMessage < SystemMessage
|
|
439
478
|
attr_accessor :task_id, :status, :output_file, :summary, :uuid, :session_id, :tool_use_id, :usage
|
|
479
|
+
|
|
480
|
+
# Machine-readable cause, set only when the task did not end through an
|
|
481
|
+
# ordinary completion, failure, or stop. The one known value is
|
|
482
|
+
# `'worker_restart'` (the worker process restarted and the resumed process
|
|
483
|
+
# found the task orphaned; always with status `'stopped'`). Documentation,
|
|
484
|
+
# not validation: newer CLIs may add values.
|
|
485
|
+
#
|
|
486
|
+
# @return [String, nil]
|
|
487
|
+
attr_accessor :reason
|
|
488
|
+
|
|
489
|
+
# For a backgrounded MCP task (`task_type: 'mcp_task'`) that completed: the
|
|
490
|
+
# `resource_link` content blocks of its final result — the files it
|
|
491
|
+
# returned by reference. A backgrounded task's tool_result is placeholder
|
|
492
|
+
# text, so this is where a host learns which files the call produced; join
|
|
493
|
+
# to the originating call via `tool_use_id`. `nil` when the result had
|
|
494
|
+
# none or the task is any other type.
|
|
495
|
+
#
|
|
496
|
+
# Passed through from the CLI untouched, so each element is a Hash whose
|
|
497
|
+
# keys are **Symbols with the wire spelling preserved**: `:uri` and `:name`
|
|
498
|
+
# (Strings, always present), and optionally `:title`, `:description`,
|
|
499
|
+
# `:mimeType` (camelCase — a `:mime_type` lookup returns nil), `:size` (a
|
|
500
|
+
# Number, not necessarily an Integer), `:annotations` (a Hash of arbitrary
|
|
501
|
+
# values). Elements carry no `type: 'resource_link'` discriminator. The CLI
|
|
502
|
+
# describes its own output as at most 50 links / 64 KiB serialized; that is
|
|
503
|
+
# a producer-side note, and the SDK neither enforces nor truncates.
|
|
504
|
+
#
|
|
505
|
+
# @return [Array<Hash{Symbol => Object}>, nil]
|
|
506
|
+
attr_accessor :resource_links
|
|
507
|
+
|
|
508
|
+
# Display flags with the same meaning as on {TaskStartedMessage}:
|
|
509
|
+
# `skip_transcript` (hide from the inline transcript) and `ambient` (not
|
|
510
|
+
# activity — exclude from activity indicators). Optional Booleans: `nil`
|
|
511
|
+
# when absent, an explicit `false` preserved. The SDK does not act on them.
|
|
512
|
+
#
|
|
513
|
+
# @return [Boolean, nil]
|
|
514
|
+
attr_accessor :skip_transcript, :ambient
|
|
440
515
|
end
|
|
441
516
|
|
|
442
517
|
# Task updated system message (background task lifecycle state change).
|
|
@@ -455,18 +530,105 @@ module ClaudeAgentSDK
|
|
|
455
530
|
# or absent patch falls back to {}; and `task_id` defaults to "" (never nil,
|
|
456
531
|
# matching the Python SDK) so consumers can rely on it always being a String.
|
|
457
532
|
# The full patch is preserved on `#patch` for callers that need more than the
|
|
458
|
-
#
|
|
533
|
+
# derived readers.
|
|
534
|
+
#
|
|
535
|
+
# A patch carries only the fields that changed, so every derived reader is
|
|
536
|
+
# `nil` when its field is absent. That matters most for `is_backgrounded`:
|
|
537
|
+
# `true` means the task just moved to the background (e.g. after
|
|
538
|
+
# {Client#background_tasks}), while `nil` means "this patch does not mention
|
|
539
|
+
# it" — not "foreground". Merge patches into your own task map rather than
|
|
540
|
+
# reading any single one as the task's full state.
|
|
459
541
|
class TaskUpdatedMessage < SystemMessage
|
|
460
|
-
attr_accessor :task_id, :patch, :status, :uuid, :session_id
|
|
542
|
+
attr_accessor :task_id, :patch, :status, :uuid, :session_id,
|
|
543
|
+
:description, # patch[:description] — String, nil when unchanged
|
|
544
|
+
:error, # patch[:error] — String, nil when unchanged
|
|
545
|
+
:end_time, # patch[:end_time] — Integer (epoch ms), nil when unchanged
|
|
546
|
+
:total_paused_ms, # patch[:total_paused_ms] — Integer, nil when unchanged
|
|
547
|
+
:is_backgrounded # patch[:is_backgrounded] — true/false, nil when unchanged
|
|
461
548
|
|
|
462
549
|
def initialize(attributes = {})
|
|
463
550
|
super
|
|
464
551
|
@task_id ||= ''
|
|
465
552
|
@patch = {} unless @patch.is_a?(Hash)
|
|
466
|
-
@status =
|
|
553
|
+
@status = patch_value(:status)
|
|
554
|
+
@description = patch_value(:description)
|
|
555
|
+
@error = patch_value(:error)
|
|
556
|
+
@end_time = patch_value(:end_time)
|
|
557
|
+
@total_paused_ms = patch_value(:total_paused_ms)
|
|
558
|
+
@is_backgrounded = patch_value(:is_backgrounded)
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
private
|
|
562
|
+
|
|
563
|
+
# The parser always hands over a symbol-keyed patch; a hand-built message
|
|
564
|
+
# may use string keys. `fetch` with a block (not `||`) keeps an explicit
|
|
565
|
+
# `false` from falling through to the string-key lookup's nil.
|
|
566
|
+
def patch_value(key)
|
|
567
|
+
@patch.fetch(key) { @patch[key.to_s] }
|
|
467
568
|
end
|
|
468
569
|
end
|
|
469
570
|
|
|
571
|
+
# Background tasks changed system message: the full set of live background
|
|
572
|
+
# tasks, emitted whenever membership changes (start, completion, kill, a
|
|
573
|
+
# foreground agent being backgrounded) or an entry's `ambient` flag flips.
|
|
574
|
+
#
|
|
575
|
+
# A **level** signal with **REPLACE semantics** — `tasks` is every live
|
|
576
|
+
# background task after the change, so swap your set for each payload rather
|
|
577
|
+
# than pairing task_started / task_notification edges; a missed edge then
|
|
578
|
+
# cannot wedge a stale "running" indicator. Per the CLI's contract:
|
|
579
|
+
#
|
|
580
|
+
# - Ordering relative to the edge frames for the same transition is
|
|
581
|
+
# unspecified, and the payload carries ids only — do not correlate it
|
|
582
|
+
# with the edge stream.
|
|
583
|
+
# - The level is per-process: nothing is emitted at startup, so reset to the
|
|
584
|
+
# empty set whenever the session's CLI process (re)starts.
|
|
585
|
+
# - `tasks: []` is an authoritative empty snapshot for that process, not a
|
|
586
|
+
# missing value.
|
|
587
|
+
# - A repeated `initialize` on an already-running process is answered with a
|
|
588
|
+
# snapshot of the current set (even an empty one) right behind its success
|
|
589
|
+
# response; older CLIs send nothing there. This SDK initializes once per
|
|
590
|
+
# connection, so that only matters to custom transports that reconnect.
|
|
591
|
+
# - It covers *background* tasks only. A foreground subagent (the spawning
|
|
592
|
+
# tool call still blocking) is not listed until it is backgrounded.
|
|
593
|
+
#
|
|
594
|
+
# `tasks` is passed through untouched: an Array of symbol-keyed Hashes
|
|
595
|
+
# `{ task_id:, task_type:, description:, ambient: }`. `:ambient` is optional;
|
|
596
|
+
# true marks tasks that are not activity (housekeeping, live-update
|
|
597
|
+
# watchers), which hosts should exclude from activity indicators.
|
|
598
|
+
#
|
|
599
|
+
# The SDK itself deliberately does not consume this frame for its own
|
|
600
|
+
# stdin-close bookkeeping; it is typed purely for consumers.
|
|
601
|
+
class BackgroundTasksChangedMessage < SystemMessage
|
|
602
|
+
attr_accessor :tasks, :uuid, :session_id
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
# Permission denied system message: a tool call was auto-denied without an
|
|
606
|
+
# interactive permission prompt (auto-mode classifier, dontAsk mode,
|
|
607
|
+
# headless-agent auto-deny, a deny rule, or — with no can_use_tool callback —
|
|
608
|
+
# an "ask" decision that nobody can answer). The "ask" path with a callback
|
|
609
|
+
# surfaces through can_use_tool instead.
|
|
610
|
+
#
|
|
611
|
+
# **Best-effort advisory, not a complete denial feed**:
|
|
612
|
+
# {ResultMessage#permission_denials} is the authoritative record. In rare
|
|
613
|
+
# races a booked denial has no frame, or a frame has no booked denial — so do
|
|
614
|
+
# not derive counts or permission state from this stream. Not covered at all:
|
|
615
|
+
# PreToolUse hook denies, deny-rule overrides of a hook's allow/ask decision,
|
|
616
|
+
# Read/Edit/Write calls refused by a path-scoped deny rule (all resolve before
|
|
617
|
+
# the permission check), and the MCP `--permission-prompt-tool` surface.
|
|
618
|
+
#
|
|
619
|
+
# `agent_id` is a subagent id for host-side routing; it is NOT a permission
|
|
620
|
+
# `request_id`, and this message is not a pending permission request.
|
|
621
|
+
# `decision_reason_type` is an open String (the values below are examples,
|
|
622
|
+
# not an enum). The CLI's `decision_reason_code` is marked internal and is
|
|
623
|
+
# left to `#data` with no stability promise.
|
|
624
|
+
class PermissionDeniedMessage < SystemMessage
|
|
625
|
+
attr_accessor :uuid, :session_id, :tool_name, :tool_use_id,
|
|
626
|
+
:agent_id, # Subagent ID when the denied call originated inside a subagent; nil otherwise
|
|
627
|
+
:decision_reason_type, # Open String, e.g. "classifier", "asyncAgent", "mode", "rule"; nil when not reported
|
|
628
|
+
:decision_reason, # Human-readable reason from the deciding component; nil when not reported
|
|
629
|
+
:message # The rejection message returned to the model in the tool_result
|
|
630
|
+
end
|
|
631
|
+
|
|
470
632
|
# Result message with cost and usage information
|
|
471
633
|
class ResultMessage < Type
|
|
472
634
|
# model_usage maps model name => per-model usage Hash, passed through
|
|
@@ -742,8 +904,10 @@ module ClaudeAgentSDK
|
|
|
742
904
|
# `decision_reason`) so the SDK consumer can render the same prompt UI
|
|
743
905
|
# the CLI would have shown. Older fields (`signal`, `suggestions`,
|
|
744
906
|
# `tool_use_id`, `agent_id`) remain unchanged.
|
|
907
|
+
# `signal` is a CancellationSignal on dispatched callbacks; `request_id`
|
|
908
|
+
# identifies this permission request (distinct from the tool invocation).
|
|
745
909
|
class ToolPermissionContext < Type
|
|
746
|
-
attr_accessor :signal, :suggestions, :tool_use_id, :agent_id,
|
|
910
|
+
attr_accessor :signal, :request_id, :suggestions, :tool_use_id, :agent_id,
|
|
747
911
|
:title, :display_name, :description,
|
|
748
912
|
:blocked_path, :decision_reason
|
|
749
913
|
|
|
@@ -786,9 +950,10 @@ module ClaudeAgentSDK
|
|
|
786
950
|
end
|
|
787
951
|
end
|
|
788
952
|
|
|
789
|
-
# Hook context passed to hook callbacks
|
|
953
|
+
# Hook context passed to hook callbacks. Dispatched hooks receive the control
|
|
954
|
+
# request ID and a CancellationSignal (including on HookMatcher timeout).
|
|
790
955
|
class HookContext < Type
|
|
791
|
-
attr_accessor :signal
|
|
956
|
+
attr_accessor :signal, :request_id
|
|
792
957
|
end
|
|
793
958
|
|
|
794
959
|
# Base hook input with common fields
|
|
@@ -828,8 +993,11 @@ module ClaudeAgentSDK
|
|
|
828
993
|
end
|
|
829
994
|
|
|
830
995
|
# Stop hook input
|
|
996
|
+
# Snapshot arrays are passed through unchanged. nil means unavailable;
|
|
997
|
+
# [] means the CLI provided an empty snapshot. They cover the parent
|
|
998
|
+
# session's background work, NOT all foreground and background agents.
|
|
831
999
|
class StopHookInput < BaseHookInput
|
|
832
|
-
attr_accessor :stop_hook_active, :last_assistant_message
|
|
1000
|
+
attr_accessor :stop_hook_active, :last_assistant_message, :background_tasks, :session_crons
|
|
833
1001
|
|
|
834
1002
|
def initialize(attributes = {})
|
|
835
1003
|
super
|
|
@@ -841,7 +1009,7 @@ module ClaudeAgentSDK
|
|
|
841
1009
|
# SubagentStop hook input
|
|
842
1010
|
class SubagentStopHookInput < BaseHookInput
|
|
843
1011
|
attr_accessor :stop_hook_active, :agent_id, :agent_transcript_path, :agent_type,
|
|
844
|
-
:last_assistant_message
|
|
1012
|
+
:last_assistant_message, :background_tasks, :session_crons
|
|
845
1013
|
|
|
846
1014
|
def initialize(attributes = {})
|
|
847
1015
|
super
|
|
@@ -1888,6 +2056,34 @@ module ClaudeAgentSDK
|
|
|
1888
2056
|
@forward_subagent_text = coerce_boolean(value)
|
|
1889
2057
|
end
|
|
1890
2058
|
|
|
2059
|
+
# Request model-generated progress summaries for subagent (`local_agent`)
|
|
2060
|
+
# tasks. `true` *requests* generation: while the CLI has it enabled, a
|
|
2061
|
+
# subagent's {TaskProgressMessage#summary} **may** carry a one-line status.
|
|
2062
|
+
# `summary` stays optional on the wire even then — not every progress
|
|
2063
|
+
# frame has one — so read it nil-safely. `false` / `nil` do not enable
|
|
2064
|
+
# generation; they do not promise that `summary` is absent (a process that
|
|
2065
|
+
# already enabled summaries keeps them, and a backgrounded `mcp_task`
|
|
2066
|
+
# reports its own status there regardless of this option). Matches the
|
|
2067
|
+
# CLI's `agentProgressSummaries` initialize field.
|
|
2068
|
+
#
|
|
2069
|
+
# Defaults to `nil` (unset): the key is omitted from the `initialize`
|
|
2070
|
+
# control request. `true` and `false` are forwarded verbatim. This is an
|
|
2071
|
+
# enable switch, not a live toggle: CLI 2.1.278 only acts on a truthy
|
|
2072
|
+
# value, so `false` is schema-valid but equivalent to leaving the option
|
|
2073
|
+
# unset — it does not switch summaries off on a process that already
|
|
2074
|
+
# enabled them. Both {ClaudeAgentSDK.query} and {Client} run the control
|
|
2075
|
+
# protocol, so the option applies to either entry point.
|
|
2076
|
+
#
|
|
2077
|
+
# Assigning coerces to a Boolean and keeps `nil` as `nil`.
|
|
2078
|
+
#
|
|
2079
|
+
# @return [Boolean, nil]
|
|
2080
|
+
attr_reader :agent_progress_summaries
|
|
2081
|
+
|
|
2082
|
+
# @see #agent_progress_summaries
|
|
2083
|
+
def agent_progress_summaries=(value)
|
|
2084
|
+
@agent_progress_summaries = coerce_boolean(value)
|
|
2085
|
+
end
|
|
2086
|
+
|
|
1891
2087
|
CALLBACK_SCHEDULING_MODES = %i[thread inline].freeze
|
|
1892
2088
|
|
|
1893
2089
|
# Where user callbacks (hooks, can_use_tool, SDK MCP handlers, message
|
|
@@ -1952,6 +2148,12 @@ module ClaudeAgentSDK
|
|
|
1952
2148
|
# Merge caller-provided attributes with configured defaults.
|
|
1953
2149
|
# Only keys the caller explicitly passed are treated as overrides;
|
|
1954
2150
|
# method-signature defaults ([], {}, false) are NOT present unless the caller wrote them.
|
|
2151
|
+
#
|
|
2152
|
+
# Both sides are keyed by the option they name, not by their literal
|
|
2153
|
+
# spelling: Type accepts symbol/string and snake_case/camelCase names, so a
|
|
2154
|
+
# caller's `'permissionMode' => nil` must still inherit a configured
|
|
2155
|
+
# `permission_mode:` (and a Hash must still merge into it) rather than
|
|
2156
|
+
# riding along as a second entry that overwrites the default on assignment.
|
|
1955
2157
|
def merge_with_defaults(attributes)
|
|
1956
2158
|
return attributes unless defined?(ClaudeAgentSDK) && ClaudeAgentSDK.respond_to?(:default_options)
|
|
1957
2159
|
|
|
@@ -1962,8 +2164,10 @@ module ClaudeAgentSDK
|
|
|
1962
2164
|
# duped so per-instance mutation (options.allowed_tools << 'Bash')
|
|
1963
2165
|
# can never corrupt the global defaults; non-container leaves
|
|
1964
2166
|
# (Strings, Procs, SdkMcpServer instances) intentionally keep identity.
|
|
1965
|
-
result =
|
|
2167
|
+
result = {}
|
|
2168
|
+
defaults.each { |key, value| result[option_key(key)] = deep_dup_containers(value) }
|
|
1966
2169
|
attributes.each do |key, value|
|
|
2170
|
+
key = option_key(key)
|
|
1967
2171
|
default_val = result[key]
|
|
1968
2172
|
result[key] = if value.nil?
|
|
1969
2173
|
default_val # nil means "no preference" — keep the configured default
|
|
@@ -1976,6 +2180,14 @@ module ClaudeAgentSDK
|
|
|
1976
2180
|
result
|
|
1977
2181
|
end
|
|
1978
2182
|
|
|
2183
|
+
# The canonical Symbol for a known option, whatever its spelling. An
|
|
2184
|
+
# unknown name is returned untouched so assign_attribute's strict check
|
|
2185
|
+
# reports the typo exactly as the developer wrote it.
|
|
2186
|
+
def option_key(name)
|
|
2187
|
+
normalized = normalize_name(name)
|
|
2188
|
+
respond_to?(:"#{normalized}=") ? normalized.to_sym : name
|
|
2189
|
+
end
|
|
2190
|
+
|
|
1979
2191
|
# Recurse ONLY into Hash/Array; leaves keep object identity (observer
|
|
1980
2192
|
# factories, callbacks, SDK MCP server instances must not be duped).
|
|
1981
2193
|
# Rebuild via dup.clear (never Hash#to_h / Array#map) to preserve
|