claude-agent-sdk 1.2.0 → 1.3.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 +95 -71
- data/README.md +40 -40
- data/UPGRADING-1.0.md +1 -1
- data/docs/client.md +1 -1
- data/docs/configuration.md +6 -6
- data/docs/errors.md +2 -2
- data/docs/hooks-and-permissions.md +36 -7
- data/docs/mcp-servers.md +1 -1
- data/docs/observability.md +1 -1
- data/docs/rails.md +5 -5
- data/docs/sessions.md +1 -1
- data/docs/subagents.md +1 -1
- data/docs/types.md +2 -2
- data/lib/claude_agent_sdk/cli_installer.rb +1 -1
- data/lib/claude_agent_sdk/command_builder.rb +48 -130
- data/lib/claude_agent_sdk/option_forms.rb +379 -0
- data/lib/claude_agent_sdk/query/run_lifecycle.rb +373 -0
- data/lib/claude_agent_sdk/query.rb +95 -516
- data/lib/claude_agent_sdk/session_assembly.rb +311 -0
- data/lib/claude_agent_sdk/session_resume.rb +4 -4
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +10 -9
- data/lib/claude_agent_sdk/types/hooks.rb +378 -243
- data/lib/claude_agent_sdk/types/messages.rb +14 -3
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +163 -406
- data/lib/generators/claude_agent_sdk/install/install_generator.rb +1 -1
- data/lib/generators/claude_agent_sdk/install/templates/claude_agent_sdk.rb.tt +1 -1
- data/sig/claude_agent_sdk/types/hooks.rbs +286 -3
- data/sig/claude_agent_sdk/types/messages.rbs +15 -0
- metadata +9 -6
|
@@ -8,8 +8,10 @@ module ClaudeAgentSDK
|
|
|
8
8
|
PreToolUse
|
|
9
9
|
PostToolUse
|
|
10
10
|
PostToolUseFailure
|
|
11
|
+
PostToolBatch
|
|
11
12
|
Notification
|
|
12
13
|
UserPromptSubmit
|
|
14
|
+
UserPromptExpansion
|
|
13
15
|
SessionStart
|
|
14
16
|
SessionEnd
|
|
15
17
|
Stop
|
|
@@ -18,6 +20,8 @@ module ClaudeAgentSDK
|
|
|
18
20
|
SubagentStop
|
|
19
21
|
PreCompact
|
|
20
22
|
PostCompact
|
|
23
|
+
PreModelSwitch
|
|
24
|
+
PostModelSwitch
|
|
21
25
|
PermissionRequest
|
|
22
26
|
PermissionDenied
|
|
23
27
|
Setup
|
|
@@ -32,6 +36,8 @@ module ClaudeAgentSDK
|
|
|
32
36
|
InstructionsLoaded
|
|
33
37
|
CwdChanged
|
|
34
38
|
FileChanged
|
|
39
|
+
DirectoryAdded
|
|
40
|
+
MessageDisplay
|
|
35
41
|
].freeze
|
|
36
42
|
|
|
37
43
|
# Hook matcher configuration
|
|
@@ -54,10 +60,29 @@ module ClaudeAgentSDK
|
|
|
54
60
|
attr_accessor :signal, :request_id
|
|
55
61
|
end
|
|
56
62
|
|
|
57
|
-
# Base hook input with common fields
|
|
63
|
+
# Base hook input with common fields.
|
|
64
|
+
#
|
|
65
|
+
# `prompt_id` is the UUID of the user prompt being processed (the
|
|
66
|
+
# `prompt.id` of the CLI's OpenTelemetry events; absent before the first
|
|
67
|
+
# prompt). `effort` is the reasoning effort in effect for the turn, a Hash
|
|
68
|
+
# such as `{ level: "high" }`, sent on models that support effort.
|
|
69
|
+
# `raw_input` is the payload exactly as the CLI sent it (Symbol keys), so a
|
|
70
|
+
# field this SDK does not model yet can still be read.
|
|
58
71
|
class BaseHookInput < Type
|
|
59
|
-
attr_accessor :session_id, :transcript_path, :cwd, :permission_mode
|
|
72
|
+
attr_accessor :session_id, :transcript_path, :cwd, :permission_mode,
|
|
73
|
+
:prompt_id, :effort, :raw_input
|
|
60
74
|
attr_reader :hook_event_name
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# A typed input already exposes the fields of its payload as attributes;
|
|
79
|
+
# repeating @raw_input would double the output. An UnknownHookInput keeps
|
|
80
|
+
# it, since @raw_input is the only place its payload lives.
|
|
81
|
+
def inspect_attributes
|
|
82
|
+
return super if is_a?(UnknownHookInput)
|
|
83
|
+
|
|
84
|
+
super.reject { |pair| pair.first == 'raw_input' }
|
|
85
|
+
end
|
|
61
86
|
end
|
|
62
87
|
|
|
63
88
|
# PreToolUse hook input
|
|
@@ -70,9 +95,11 @@ module ClaudeAgentSDK
|
|
|
70
95
|
end
|
|
71
96
|
end
|
|
72
97
|
|
|
73
|
-
# PostToolUse hook input
|
|
98
|
+
# PostToolUse hook input. `duration_ms` is the tool's execution time,
|
|
99
|
+
# without permission prompts and hooks.
|
|
74
100
|
class PostToolUseHookInput < BaseHookInput
|
|
75
|
-
attr_accessor :tool_name, :tool_input, :tool_response, :tool_use_id, :agent_id, :agent_type
|
|
101
|
+
attr_accessor :tool_name, :tool_input, :tool_response, :tool_use_id, :agent_id, :agent_type,
|
|
102
|
+
:duration_ms
|
|
76
103
|
|
|
77
104
|
def initialize(attributes = {})
|
|
78
105
|
super
|
|
@@ -82,7 +109,7 @@ module ClaudeAgentSDK
|
|
|
82
109
|
|
|
83
110
|
# UserPromptSubmit hook input
|
|
84
111
|
class UserPromptSubmitHookInput < BaseHookInput
|
|
85
|
-
attr_accessor :prompt
|
|
112
|
+
attr_accessor :prompt, :session_title, :source
|
|
86
113
|
|
|
87
114
|
def initialize(attributes = {})
|
|
88
115
|
super
|
|
@@ -116,10 +143,10 @@ module ClaudeAgentSDK
|
|
|
116
143
|
end
|
|
117
144
|
end
|
|
118
145
|
|
|
119
|
-
# PostToolUseFailure hook input
|
|
146
|
+
# PostToolUseFailure hook input. `duration_ms` as on PostToolUseHookInput.
|
|
120
147
|
class PostToolUseFailureHookInput < BaseHookInput
|
|
121
148
|
attr_accessor :tool_name, :tool_input, :tool_use_id, :error, :is_interrupt,
|
|
122
|
-
:agent_id, :agent_type
|
|
149
|
+
:agent_id, :agent_type, :duration_ms
|
|
123
150
|
|
|
124
151
|
def initialize(attributes = {})
|
|
125
152
|
super
|
|
@@ -167,9 +194,14 @@ module ClaudeAgentSDK
|
|
|
167
194
|
end
|
|
168
195
|
end
|
|
169
196
|
|
|
170
|
-
# SessionStart hook input
|
|
197
|
+
# SessionStart hook input. On "resume" and "fork",
|
|
198
|
+
# `seconds_since_last_response` is the time since the transcript's last
|
|
199
|
+
# assistant response and `prompt_cache_likely_expired` says whether that
|
|
200
|
+
# exceeds the prompt-cache TTL; `context_tokens` and
|
|
201
|
+
# `estimated_cache_write_usd` size the prompt the next request re-sends.
|
|
171
202
|
class SessionStartHookInput < BaseHookInput
|
|
172
|
-
attr_accessor :source, :agent_type, :model
|
|
203
|
+
attr_accessor :source, :agent_type, :model, :session_title, :seconds_since_last_response,
|
|
204
|
+
:prompt_cache_likely_expired, :context_tokens, :estimated_cache_write_usd
|
|
173
205
|
|
|
174
206
|
def initialize(attributes = {})
|
|
175
207
|
super
|
|
@@ -310,7 +342,7 @@ module ClaudeAgentSDK
|
|
|
310
342
|
|
|
311
343
|
# InstructionsLoaded hook input
|
|
312
344
|
class InstructionsLoadedHookInput < BaseHookInput
|
|
313
|
-
attr_accessor :file_path, :memory_type, :load_reason, :globs, :trigger_file_path
|
|
345
|
+
attr_accessor :file_path, :memory_type, :load_reason, :globs, :trigger_file_path, :parent_file_path
|
|
314
346
|
|
|
315
347
|
def initialize(attributes = {})
|
|
316
348
|
super
|
|
@@ -338,64 +370,231 @@ module ClaudeAgentSDK
|
|
|
338
370
|
end
|
|
339
371
|
end
|
|
340
372
|
|
|
341
|
-
#
|
|
342
|
-
#
|
|
343
|
-
#
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
attr_accessor :raw_input
|
|
373
|
+
# PostToolBatch hook input: fired once every tool call of a batch has
|
|
374
|
+
# resolved, before the next model request. `tool_calls` is an Array of
|
|
375
|
+
# `{ tool_name:, tool_input:, tool_use_id:, tool_response: }` Hashes.
|
|
376
|
+
class PostToolBatchHookInput < BaseHookInput
|
|
377
|
+
attr_accessor :tool_calls
|
|
347
378
|
|
|
348
379
|
def initialize(attributes = {})
|
|
349
380
|
super
|
|
350
|
-
|
|
351
|
-
# attr_reader only, and Type#assign_attribute silently drops keys
|
|
352
|
-
# without public setters.
|
|
353
|
-
@hook_event_name = attributes[:hook_event_name] || attributes['hook_event_name']
|
|
381
|
+
@hook_event_name = 'PostToolBatch'
|
|
354
382
|
end
|
|
355
383
|
end
|
|
356
384
|
|
|
357
|
-
#
|
|
358
|
-
|
|
359
|
-
|
|
385
|
+
# UserPromptExpansion hook input: a slash command or MCP prompt is about
|
|
386
|
+
# to be expanded. `expansion_type` is "slash_command" or "mcp_prompt".
|
|
387
|
+
class UserPromptExpansionHookInput < BaseHookInput
|
|
388
|
+
attr_accessor :expansion_type, :command_name, :command_args, :command_source, :prompt
|
|
360
389
|
|
|
361
|
-
|
|
362
|
-
|
|
390
|
+
def initialize(attributes = {})
|
|
391
|
+
super
|
|
392
|
+
@hook_event_name = 'UserPromptExpansion'
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# PreModelSwitch hook input. `requested_model` is what was asked for (an
|
|
397
|
+
# alias, a full id, or nil for the default); `from_model` and `to_model`
|
|
398
|
+
# are resolved ids. `context_tokens`, `prompt_cache_warm`, `cache_ttl`
|
|
399
|
+
# ("5m" or "1h") and `estimated_cache_write_usd` describe what switching
|
|
400
|
+
# costs; `pricing` says which rate the estimate used ("configured",
|
|
401
|
+
# "catalog" or "default").
|
|
402
|
+
class PreModelSwitchHookInput < BaseHookInput
|
|
403
|
+
attr_accessor :from_model, :to_model, :requested_model, :source,
|
|
404
|
+
:context_tokens, :prompt_cache_warm, :cache_ttl, :estimated_cache_write_usd, :pricing
|
|
363
405
|
|
|
364
406
|
def initialize(attributes = {})
|
|
365
407
|
super
|
|
366
|
-
@hook_event_name = '
|
|
408
|
+
@hook_event_name = 'PreModelSwitch'
|
|
367
409
|
end
|
|
410
|
+
end
|
|
368
411
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
412
|
+
# PostModelSwitch hook input (the fields of PreModelSwitchHookInput)
|
|
413
|
+
class PostModelSwitchHookInput < BaseHookInput
|
|
414
|
+
attr_accessor :from_model, :to_model, :requested_model, :source,
|
|
415
|
+
:context_tokens, :prompt_cache_warm, :cache_ttl, :estimated_cache_write_usd, :pricing
|
|
416
|
+
|
|
417
|
+
def initialize(attributes = {})
|
|
418
|
+
super
|
|
419
|
+
@hook_event_name = 'PostModelSwitch'
|
|
373
420
|
end
|
|
374
421
|
end
|
|
375
422
|
|
|
376
|
-
#
|
|
377
|
-
|
|
378
|
-
|
|
423
|
+
# DirectoryAdded hook input. `directory` is absolute; `source` is
|
|
424
|
+
# "slash_command" (/add-dir) or "register_repo_root".
|
|
425
|
+
class DirectoryAddedHookInput < BaseHookInput
|
|
426
|
+
attr_accessor :directory, :source
|
|
379
427
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
428
|
+
def initialize(attributes = {})
|
|
429
|
+
super
|
|
430
|
+
@hook_event_name = 'DirectoryAdded'
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# MessageDisplay hook input: one flush of an assistant message on its way
|
|
435
|
+
# to the screen. `delta` holds the lines completed since the previous
|
|
436
|
+
# flush; `index` counts flushes from 0 and `final` marks the last one.
|
|
437
|
+
class MessageDisplayHookInput < BaseHookInput
|
|
438
|
+
attr_accessor :turn_id, :message_id, :index, :final, :delta
|
|
383
439
|
|
|
384
440
|
def initialize(attributes = {})
|
|
385
441
|
super
|
|
386
|
-
@hook_event_name = '
|
|
442
|
+
@hook_event_name = 'MessageDisplay'
|
|
387
443
|
end
|
|
444
|
+
end
|
|
388
445
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
446
|
+
# Fallback for hook events the SDK does not yet model. Carries the wire
|
|
447
|
+
# event name; the complete payload is in #raw_input, as on every hook
|
|
448
|
+
# input, so no field is lost (Python passes hook input through as a raw
|
|
449
|
+
# dict, so unknown events lose nothing there).
|
|
450
|
+
class UnknownHookInput < BaseHookInput
|
|
451
|
+
def initialize(attributes = {})
|
|
452
|
+
super
|
|
453
|
+
# Direct assignment: BaseHookInput exposes hook_event_name as
|
|
454
|
+
# attr_reader only, and Type#assign_attribute silently drops keys
|
|
455
|
+
# without public setters.
|
|
456
|
+
@hook_event_name = attributes[:hook_event_name] || attributes['hook_event_name']
|
|
396
457
|
end
|
|
397
458
|
end
|
|
398
459
|
|
|
460
|
+
# The spellings a hook callback's return value may use for a field of the
|
|
461
|
+
# typed output classes below, mapped to the key the CLI reads. A Hash a
|
|
462
|
+
# callback returns stands for the typed output with the same fields: its
|
|
463
|
+
# keys may be Symbols or Strings, the attribute names (snake_case) or what
|
|
464
|
+
# #to_h emits (camelCase), at the top level and inside hook_specific_output.
|
|
465
|
+
#
|
|
466
|
+
# Only names that differ from their wire key are listed. A key that is not
|
|
467
|
+
# listed is sent as written, so a field of a newer CLI that the typed
|
|
468
|
+
# classes do not model still gets through, in the CLI's own spelling.
|
|
469
|
+
# spec/unit/hook_output_normalization_spec.rb walks every typed output
|
|
470
|
+
# class and fails when an attribute and these tables disagree.
|
|
471
|
+
#
|
|
472
|
+
# @api private
|
|
473
|
+
module HookOutputKeys
|
|
474
|
+
# SyncHookJSONOutput and AsyncHookJSONOutput attributes, plus the
|
|
475
|
+
# Ruby-safe spellings of the two keywords.
|
|
476
|
+
TOP_LEVEL = {
|
|
477
|
+
'continue_' => 'continue',
|
|
478
|
+
'async_' => 'async',
|
|
479
|
+
'suppress_output' => 'suppressOutput',
|
|
480
|
+
'stop_reason' => 'stopReason',
|
|
481
|
+
'system_message' => 'systemMessage',
|
|
482
|
+
'hook_specific_output' => 'hookSpecificOutput',
|
|
483
|
+
'async_timeout' => 'asyncTimeout'
|
|
484
|
+
}.freeze
|
|
485
|
+
|
|
486
|
+
# Attributes of the *HookSpecificOutput classes, which take their wire
|
|
487
|
+
# keys from this table (HookSpecificOutputFields).
|
|
488
|
+
HOOK_SPECIFIC = {
|
|
489
|
+
'hook_event_name' => 'hookEventName',
|
|
490
|
+
'permission_decision' => 'permissionDecision',
|
|
491
|
+
'permission_decision_reason' => 'permissionDecisionReason',
|
|
492
|
+
'updated_input' => 'updatedInput',
|
|
493
|
+
'additional_context' => 'additionalContext',
|
|
494
|
+
'updated_tool_output' => 'updatedToolOutput',
|
|
495
|
+
'updated_mcp_tool_output' => 'updatedMCPToolOutput',
|
|
496
|
+
'watch_paths' => 'watchPaths',
|
|
497
|
+
'session_title' => 'sessionTitle',
|
|
498
|
+
'suppress_original_prompt' => 'suppressOriginalPrompt',
|
|
499
|
+
'initial_user_message' => 'initialUserMessage',
|
|
500
|
+
'reload_skills' => 'reloadSkills',
|
|
501
|
+
'worktree_path' => 'worktreePath',
|
|
502
|
+
'display_content' => 'displayContent'
|
|
503
|
+
}.freeze
|
|
504
|
+
|
|
505
|
+
# The hook output Hash as the CLI reads it: String keys in wire
|
|
506
|
+
# spelling, at the top level and one level down, inside
|
|
507
|
+
# hookSpecificOutput. Values are never rewritten: updatedInput and the
|
|
508
|
+
# tool outputs are the tool's own payloads, and a PermissionRequest
|
|
509
|
+
# decision goes out as the caller wrote it.
|
|
510
|
+
def self.normalize(output)
|
|
511
|
+
normalized = rename(output, TOP_LEVEL)
|
|
512
|
+
specific = normalized['hookSpecificOutput']
|
|
513
|
+
normalized['hookSpecificOutput'] = rename(specific, HOOK_SPECIFIC) if specific.is_a?(Hash)
|
|
514
|
+
normalized
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
# Every key ends up as one String, so a Symbol and a String spelling the
|
|
518
|
+
# same field cannot both reach JSON.generate (json 3.x raises on that;
|
|
519
|
+
# 2.x emits the key twice). When a Hash carries both spellings of one
|
|
520
|
+
# field the wire spelling wins, whichever comes first; between two keys
|
|
521
|
+
# in the same spelling the later one does.
|
|
522
|
+
def self.rename(hash, table)
|
|
523
|
+
renamed = {}
|
|
524
|
+
wire_spelled = {}
|
|
525
|
+
hash.each do |key, value|
|
|
526
|
+
name = key.to_s
|
|
527
|
+
wire = table.fetch(name, name)
|
|
528
|
+
if wire == name
|
|
529
|
+
wire_spelled[wire] = true
|
|
530
|
+
elsif wire_spelled.key?(wire)
|
|
531
|
+
next
|
|
532
|
+
end
|
|
533
|
+
renamed[wire] = value
|
|
534
|
+
end
|
|
535
|
+
renamed
|
|
536
|
+
end
|
|
537
|
+
private_class_method :rename
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
# Completes a *HookSpecificOutput class, which declares its fields with
|
|
541
|
+
# attr_accessor and then names the event it answers. #to_h writes
|
|
542
|
+
# hookEventName and every one of those fields that is not nil, under the
|
|
543
|
+
# key HookOutputKeys::HOOK_SPECIFIC gives it (the attribute name when the
|
|
544
|
+
# table has no entry).
|
|
545
|
+
#
|
|
546
|
+
# @api private
|
|
547
|
+
module HookSpecificOutputFields
|
|
548
|
+
# The fields are the attributes the class has declared when this is
|
|
549
|
+
# called, so it goes after attr_accessor.
|
|
550
|
+
#
|
|
551
|
+
# @!macro [attach] hook_specific_output
|
|
552
|
+
# @!attribute [r] hook_event_name
|
|
553
|
+
# @return [String] "$1"
|
|
554
|
+
# @!method initialize(attributes = {})
|
|
555
|
+
# Takes the fields by attribute name; an unknown one raises ArgumentError.
|
|
556
|
+
# @!method to_h
|
|
557
|
+
# The output as the CLI reads it: `hookEventName` and every field
|
|
558
|
+
# that is not nil, under its wire key.
|
|
559
|
+
# @return [Hash{Symbol => Object}]
|
|
560
|
+
def hook_specific_output(event)
|
|
561
|
+
strict_attributes
|
|
562
|
+
wire_keys = own_attribute_names.to_h do |name|
|
|
563
|
+
[:"@#{name}", HookOutputKeys::HOOK_SPECIFIC.fetch(name, name).to_sym]
|
|
564
|
+
end
|
|
565
|
+
attr_reader :hook_event_name
|
|
566
|
+
|
|
567
|
+
define_method(:initialize) do |attributes = {}|
|
|
568
|
+
super(attributes)
|
|
569
|
+
@hook_event_name = event
|
|
570
|
+
end
|
|
571
|
+
define_method(:to_h) do
|
|
572
|
+
wire_keys.each_with_object({ hookEventName: @hook_event_name }) do |(ivar, key), result|
|
|
573
|
+
value = instance_variable_get(ivar)
|
|
574
|
+
result[key] = value unless value.nil?
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
# Setup hook specific output
|
|
581
|
+
class SetupHookSpecificOutput < Type
|
|
582
|
+
extend HookSpecificOutputFields
|
|
583
|
+
|
|
584
|
+
attr_accessor :additional_context
|
|
585
|
+
|
|
586
|
+
hook_specific_output 'Setup'
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
# PreToolUse hook specific output
|
|
590
|
+
class PreToolUseHookSpecificOutput < Type
|
|
591
|
+
extend HookSpecificOutputFields
|
|
592
|
+
|
|
593
|
+
attr_accessor :permission_decision, :permission_decision_reason, :updated_input, :additional_context
|
|
594
|
+
|
|
595
|
+
hook_specific_output 'PreToolUse'
|
|
596
|
+
end
|
|
597
|
+
|
|
399
598
|
# PostToolUse hook specific output.
|
|
400
599
|
#
|
|
401
600
|
# `updated_tool_output` (CLI 2.1.110+) replaces the tool's output entirely
|
|
@@ -404,195 +603,204 @@ module ClaudeAgentSDK
|
|
|
404
603
|
# honors it, so both are emitted when set. Mirrors Python's
|
|
405
604
|
# `PostToolUseHookSpecificOutput`.
|
|
406
605
|
class PostToolUseHookSpecificOutput < Type
|
|
407
|
-
|
|
606
|
+
extend HookSpecificOutputFields
|
|
408
607
|
|
|
409
|
-
attr_accessor :additional_context, :
|
|
410
|
-
attr_reader :hook_event_name
|
|
411
|
-
|
|
412
|
-
def initialize(attributes = {})
|
|
413
|
-
super
|
|
414
|
-
@hook_event_name = 'PostToolUse'
|
|
415
|
-
end
|
|
608
|
+
attr_accessor :additional_context, :updated_tool_output, :updated_mcp_tool_output
|
|
416
609
|
|
|
417
|
-
|
|
418
|
-
result = { hookEventName: @hook_event_name }
|
|
419
|
-
result[:additionalContext] = @additional_context if @additional_context
|
|
420
|
-
result[:updatedToolOutput] = @updated_tool_output unless @updated_tool_output.nil?
|
|
421
|
-
result[:updatedMCPToolOutput] = @updated_mcp_tool_output if @updated_mcp_tool_output
|
|
422
|
-
result
|
|
423
|
-
end
|
|
610
|
+
hook_specific_output 'PostToolUse'
|
|
424
611
|
end
|
|
425
612
|
|
|
426
613
|
# PostToolUseFailure hook specific output
|
|
427
614
|
class PostToolUseFailureHookSpecificOutput < Type
|
|
428
|
-
|
|
615
|
+
extend HookSpecificOutputFields
|
|
429
616
|
|
|
430
617
|
attr_accessor :additional_context
|
|
431
|
-
attr_reader :hook_event_name
|
|
432
618
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
@hook_event_name = 'PostToolUseFailure'
|
|
436
|
-
end
|
|
619
|
+
hook_specific_output 'PostToolUseFailure'
|
|
620
|
+
end
|
|
437
621
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
622
|
+
# PostToolBatch hook specific output
|
|
623
|
+
class PostToolBatchHookSpecificOutput < Type
|
|
624
|
+
extend HookSpecificOutputFields
|
|
625
|
+
|
|
626
|
+
attr_accessor :additional_context
|
|
627
|
+
|
|
628
|
+
hook_specific_output 'PostToolBatch'
|
|
443
629
|
end
|
|
444
630
|
|
|
445
|
-
# UserPromptSubmit hook specific output
|
|
631
|
+
# UserPromptSubmit hook specific output. `session_title` sets the session
|
|
632
|
+
# title; `suppress_original_prompt` leaves the prompt out of the block
|
|
633
|
+
# message when the hook's decision is "block".
|
|
446
634
|
class UserPromptSubmitHookSpecificOutput < Type
|
|
447
|
-
|
|
635
|
+
extend HookSpecificOutputFields
|
|
448
636
|
|
|
449
|
-
attr_accessor :additional_context
|
|
450
|
-
attr_reader :hook_event_name
|
|
637
|
+
attr_accessor :additional_context, :session_title, :suppress_original_prompt
|
|
451
638
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
@hook_event_name = 'UserPromptSubmit'
|
|
455
|
-
end
|
|
639
|
+
hook_specific_output 'UserPromptSubmit'
|
|
640
|
+
end
|
|
456
641
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
642
|
+
# UserPromptExpansion hook specific output (`suppress_original_prompt` as
|
|
643
|
+
# on UserPromptSubmitHookSpecificOutput)
|
|
644
|
+
class UserPromptExpansionHookSpecificOutput < Type
|
|
645
|
+
extend HookSpecificOutputFields
|
|
646
|
+
|
|
647
|
+
attr_accessor :additional_context, :suppress_original_prompt
|
|
648
|
+
|
|
649
|
+
hook_specific_output 'UserPromptExpansion'
|
|
462
650
|
end
|
|
463
651
|
|
|
464
652
|
# Notification hook specific output
|
|
465
653
|
class NotificationHookSpecificOutput < Type
|
|
466
|
-
|
|
654
|
+
extend HookSpecificOutputFields
|
|
467
655
|
|
|
468
656
|
attr_accessor :additional_context
|
|
469
|
-
attr_reader :hook_event_name
|
|
470
657
|
|
|
471
|
-
|
|
472
|
-
super
|
|
473
|
-
@hook_event_name = 'Notification'
|
|
474
|
-
end
|
|
475
|
-
|
|
476
|
-
def to_h
|
|
477
|
-
result = { hookEventName: @hook_event_name }
|
|
478
|
-
result[:additionalContext] = @additional_context if @additional_context
|
|
479
|
-
result
|
|
480
|
-
end
|
|
658
|
+
hook_specific_output 'Notification'
|
|
481
659
|
end
|
|
482
660
|
|
|
483
661
|
# SubagentStart hook specific output
|
|
484
662
|
class SubagentStartHookSpecificOutput < Type
|
|
485
|
-
|
|
663
|
+
extend HookSpecificOutputFields
|
|
486
664
|
|
|
487
665
|
attr_accessor :additional_context
|
|
488
|
-
attr_reader :hook_event_name
|
|
489
666
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
@hook_event_name = 'SubagentStart'
|
|
493
|
-
end
|
|
667
|
+
hook_specific_output 'SubagentStart'
|
|
668
|
+
end
|
|
494
669
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
670
|
+
# Stop hook specific output. `additional_context` is feedback for the
|
|
671
|
+
# model, not an error: the conversation continues so it can act on it.
|
|
672
|
+
class StopHookSpecificOutput < Type
|
|
673
|
+
extend HookSpecificOutputFields
|
|
674
|
+
|
|
675
|
+
attr_accessor :additional_context
|
|
676
|
+
|
|
677
|
+
hook_specific_output 'Stop'
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
# SubagentStop hook specific output (`additional_context` as on
|
|
681
|
+
# StopHookSpecificOutput, delivered to the subagent)
|
|
682
|
+
class SubagentStopHookSpecificOutput < Type
|
|
683
|
+
extend HookSpecificOutputFields
|
|
684
|
+
|
|
685
|
+
attr_accessor :additional_context
|
|
686
|
+
|
|
687
|
+
hook_specific_output 'SubagentStop'
|
|
500
688
|
end
|
|
501
689
|
|
|
502
690
|
# PermissionRequest hook specific output
|
|
503
691
|
class PermissionRequestHookSpecificOutput < Type
|
|
504
|
-
|
|
692
|
+
extend HookSpecificOutputFields
|
|
505
693
|
|
|
506
694
|
attr_accessor :decision
|
|
507
|
-
attr_reader :hook_event_name
|
|
508
695
|
|
|
509
|
-
|
|
510
|
-
super
|
|
511
|
-
@hook_event_name = 'PermissionRequest'
|
|
512
|
-
end
|
|
513
|
-
|
|
514
|
-
def to_h
|
|
515
|
-
result = { hookEventName: @hook_event_name }
|
|
516
|
-
result[:decision] = @decision if @decision
|
|
517
|
-
result
|
|
518
|
-
end
|
|
696
|
+
hook_specific_output 'PermissionRequest'
|
|
519
697
|
end
|
|
520
698
|
|
|
521
|
-
# SessionStart hook specific output
|
|
699
|
+
# SessionStart hook specific output. `initial_user_message` becomes the
|
|
700
|
+
# session's first user message; `session_title` sets its title (ignored
|
|
701
|
+
# when the session starts from "clear" or "compact"); `watch_paths` are
|
|
702
|
+
# absolute paths to watch for FileChanged; `reload_skills` re-scans skill
|
|
703
|
+
# and command directories once SessionStart hooks finish.
|
|
522
704
|
class SessionStartHookSpecificOutput < Type
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
attr_accessor :additional_context
|
|
526
|
-
attr_reader :hook_event_name
|
|
705
|
+
extend HookSpecificOutputFields
|
|
527
706
|
|
|
528
|
-
|
|
529
|
-
super
|
|
530
|
-
@hook_event_name = 'SessionStart'
|
|
531
|
-
end
|
|
707
|
+
attr_accessor :additional_context, :initial_user_message, :session_title, :watch_paths, :reload_skills
|
|
532
708
|
|
|
533
|
-
|
|
534
|
-
result = { hookEventName: @hook_event_name }
|
|
535
|
-
result[:additionalContext] = @additional_context if @additional_context
|
|
536
|
-
result
|
|
537
|
-
end
|
|
709
|
+
hook_specific_output 'SessionStart'
|
|
538
710
|
end
|
|
539
711
|
|
|
540
|
-
# PermissionDenied hook specific output
|
|
712
|
+
# PermissionDenied hook specific output. `retry: true` tells the model it
|
|
713
|
+
# may retry the denied call (ignored for denials without a classifier
|
|
714
|
+
# verdict); left nil, the key is not sent, which the CLI reads as false.
|
|
541
715
|
class PermissionDeniedHookSpecificOutput < Type
|
|
542
|
-
|
|
716
|
+
extend HookSpecificOutputFields
|
|
543
717
|
|
|
544
718
|
attr_accessor :retry
|
|
545
|
-
attr_reader :hook_event_name
|
|
546
719
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
@hook_event_name = 'PermissionDenied'
|
|
550
|
-
@retry = false if @retry.nil?
|
|
551
|
-
end
|
|
720
|
+
hook_specific_output 'PermissionDenied'
|
|
721
|
+
end
|
|
552
722
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
723
|
+
# PreModelSwitch hook specific output. `permission_decision` "allow"
|
|
724
|
+
# proceeds and "deny" cancels the switch; "ask" is a refusal outside an
|
|
725
|
+
# interactive session.
|
|
726
|
+
class PreModelSwitchHookSpecificOutput < Type
|
|
727
|
+
extend HookSpecificOutputFields
|
|
728
|
+
|
|
729
|
+
attr_accessor :permission_decision, :permission_decision_reason
|
|
730
|
+
|
|
731
|
+
hook_specific_output 'PreModelSwitch'
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
# PostModelSwitch hook specific output (`additional_context` reaches the
|
|
735
|
+
# model with the next request the new model serves)
|
|
736
|
+
class PostModelSwitchHookSpecificOutput < Type
|
|
737
|
+
extend HookSpecificOutputFields
|
|
738
|
+
|
|
739
|
+
attr_accessor :additional_context
|
|
740
|
+
|
|
741
|
+
hook_specific_output 'PostModelSwitch'
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
# Elicitation hook specific output: answers an MCP elicitation request.
|
|
745
|
+
# `action` is "accept", "decline" or "cancel"; `content` holds the form
|
|
746
|
+
# values to submit with "accept".
|
|
747
|
+
class ElicitationHookSpecificOutput < Type
|
|
748
|
+
extend HookSpecificOutputFields
|
|
749
|
+
|
|
750
|
+
attr_accessor :action, :content
|
|
751
|
+
|
|
752
|
+
hook_specific_output 'Elicitation'
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
# ElicitationResult hook specific output: overrides the action or content
|
|
756
|
+
# before the response reaches the MCP server.
|
|
757
|
+
class ElicitationResultHookSpecificOutput < Type
|
|
758
|
+
extend HookSpecificOutputFields
|
|
759
|
+
|
|
760
|
+
attr_accessor :action, :content
|
|
761
|
+
|
|
762
|
+
hook_specific_output 'ElicitationResult'
|
|
558
763
|
end
|
|
559
764
|
|
|
560
|
-
# CwdChanged hook specific output
|
|
765
|
+
# CwdChanged hook specific output (`watch_paths` replaces the dynamic
|
|
766
|
+
# FileChanged watch list)
|
|
561
767
|
class CwdChangedHookSpecificOutput < Type
|
|
562
|
-
|
|
768
|
+
extend HookSpecificOutputFields
|
|
563
769
|
|
|
564
770
|
attr_accessor :watch_paths
|
|
565
|
-
attr_reader :hook_event_name
|
|
566
771
|
|
|
567
|
-
|
|
568
|
-
super
|
|
569
|
-
@hook_event_name = 'CwdChanged'
|
|
570
|
-
end
|
|
571
|
-
|
|
572
|
-
def to_h
|
|
573
|
-
result = { hookEventName: @hook_event_name }
|
|
574
|
-
result[:watchPaths] = @watch_paths if @watch_paths
|
|
575
|
-
result
|
|
576
|
-
end
|
|
772
|
+
hook_specific_output 'CwdChanged'
|
|
577
773
|
end
|
|
578
774
|
|
|
579
|
-
# FileChanged hook specific output
|
|
775
|
+
# FileChanged hook specific output (`watch_paths` as on
|
|
776
|
+
# CwdChangedHookSpecificOutput)
|
|
580
777
|
class FileChangedHookSpecificOutput < Type
|
|
581
|
-
|
|
778
|
+
extend HookSpecificOutputFields
|
|
582
779
|
|
|
583
780
|
attr_accessor :watch_paths
|
|
584
|
-
attr_reader :hook_event_name
|
|
585
781
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
@hook_event_name = 'FileChanged'
|
|
589
|
-
end
|
|
782
|
+
hook_specific_output 'FileChanged'
|
|
783
|
+
end
|
|
590
784
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
785
|
+
# WorktreeCreate hook specific output (`worktree_path`: the absolute path
|
|
786
|
+
# of the created worktree)
|
|
787
|
+
class WorktreeCreateHookSpecificOutput < Type
|
|
788
|
+
extend HookSpecificOutputFields
|
|
789
|
+
|
|
790
|
+
attr_accessor :worktree_path
|
|
791
|
+
|
|
792
|
+
hook_specific_output 'WorktreeCreate'
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
# MessageDisplay hook specific output. `display_content` replaces the text
|
|
796
|
+
# on screen only: the stored message and what the model sees are
|
|
797
|
+
# unchanged. Left nil, the original is displayed.
|
|
798
|
+
class MessageDisplayHookSpecificOutput < Type
|
|
799
|
+
extend HookSpecificOutputFields
|
|
800
|
+
|
|
801
|
+
attr_accessor :display_content
|
|
802
|
+
|
|
803
|
+
hook_specific_output 'MessageDisplay'
|
|
596
804
|
end
|
|
597
805
|
|
|
598
806
|
# Async hook JSON output
|
|
@@ -637,77 +845,4 @@ module ClaudeAgentSDK
|
|
|
637
845
|
result
|
|
638
846
|
end
|
|
639
847
|
end
|
|
640
|
-
|
|
641
|
-
# The spellings a hook callback's return value may use for a field of the
|
|
642
|
-
# typed output classes above, mapped to the key the CLI reads. A Hash a
|
|
643
|
-
# callback returns stands for the typed output with the same fields: its
|
|
644
|
-
# keys may be Symbols or Strings, the attribute names (snake_case) or what
|
|
645
|
-
# #to_h emits (camelCase), at the top level and inside hook_specific_output.
|
|
646
|
-
#
|
|
647
|
-
# Only names that differ from their wire key are listed. A key that is not
|
|
648
|
-
# listed is sent as written, so a field of a newer CLI that the typed
|
|
649
|
-
# classes do not model still gets through, in the CLI's own spelling.
|
|
650
|
-
# spec/unit/hook_output_normalization_spec.rb walks every typed output
|
|
651
|
-
# class and fails when an attribute and these tables disagree.
|
|
652
|
-
#
|
|
653
|
-
# @api private
|
|
654
|
-
module HookOutputKeys
|
|
655
|
-
# SyncHookJSONOutput and AsyncHookJSONOutput attributes, plus the
|
|
656
|
-
# Ruby-safe spellings of the two keywords.
|
|
657
|
-
TOP_LEVEL = {
|
|
658
|
-
'continue_' => 'continue',
|
|
659
|
-
'async_' => 'async',
|
|
660
|
-
'suppress_output' => 'suppressOutput',
|
|
661
|
-
'stop_reason' => 'stopReason',
|
|
662
|
-
'system_message' => 'systemMessage',
|
|
663
|
-
'hook_specific_output' => 'hookSpecificOutput',
|
|
664
|
-
'async_timeout' => 'asyncTimeout'
|
|
665
|
-
}.freeze
|
|
666
|
-
|
|
667
|
-
# Attributes of the *HookSpecificOutput classes.
|
|
668
|
-
HOOK_SPECIFIC = {
|
|
669
|
-
'hook_event_name' => 'hookEventName',
|
|
670
|
-
'permission_decision' => 'permissionDecision',
|
|
671
|
-
'permission_decision_reason' => 'permissionDecisionReason',
|
|
672
|
-
'updated_input' => 'updatedInput',
|
|
673
|
-
'additional_context' => 'additionalContext',
|
|
674
|
-
'updated_tool_output' => 'updatedToolOutput',
|
|
675
|
-
'updated_mcp_tool_output' => 'updatedMCPToolOutput',
|
|
676
|
-
'watch_paths' => 'watchPaths'
|
|
677
|
-
}.freeze
|
|
678
|
-
|
|
679
|
-
# The hook output Hash as the CLI reads it: String keys in wire
|
|
680
|
-
# spelling, at the top level and one level down, inside
|
|
681
|
-
# hookSpecificOutput. Values are never rewritten: updatedInput and the
|
|
682
|
-
# tool outputs are the tool's own payloads, and a PermissionRequest
|
|
683
|
-
# decision goes out as the caller wrote it.
|
|
684
|
-
def self.normalize(output)
|
|
685
|
-
normalized = rename(output, TOP_LEVEL)
|
|
686
|
-
specific = normalized['hookSpecificOutput']
|
|
687
|
-
normalized['hookSpecificOutput'] = rename(specific, HOOK_SPECIFIC) if specific.is_a?(Hash)
|
|
688
|
-
normalized
|
|
689
|
-
end
|
|
690
|
-
|
|
691
|
-
# Every key ends up as one String, so a Symbol and a String spelling the
|
|
692
|
-
# same field cannot both reach JSON.generate (json 3.x raises on that;
|
|
693
|
-
# 2.x emits the key twice). When a Hash carries both spellings of one
|
|
694
|
-
# field the wire spelling wins, whichever comes first; between two keys
|
|
695
|
-
# in the same spelling the later one does.
|
|
696
|
-
def self.rename(hash, table)
|
|
697
|
-
renamed = {}
|
|
698
|
-
wire_spelled = {}
|
|
699
|
-
hash.each do |key, value|
|
|
700
|
-
name = key.to_s
|
|
701
|
-
wire = table.fetch(name, name)
|
|
702
|
-
if wire == name
|
|
703
|
-
wire_spelled[wire] = true
|
|
704
|
-
elsif wire_spelled.key?(wire)
|
|
705
|
-
next
|
|
706
|
-
end
|
|
707
|
-
renamed[wire] = value
|
|
708
|
-
end
|
|
709
|
-
renamed
|
|
710
|
-
end
|
|
711
|
-
private_class_method :rename
|
|
712
|
-
end
|
|
713
848
|
end
|