claude-agent-sdk 1.2.1 → 1.4.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.
data/docs/rails.md CHANGED
@@ -356,7 +356,7 @@ end
356
356
 
357
357
  Preconditions, spelled out: `:inline` is only correct when the process satisfies the same requirements as solid_queue's fiber workers — `isolation_level = :fiber`, Rails 7.2+ for AR, and no thread-keyed libraries used inside callbacks without a fiber-aware wrapper. The SDK warns once if it detects `:inline` under `isolation_level == :thread`. Everything else (Puma request threads, threaded Sidekiq/solid_queue workers) should stay on the default `callback_scheduling: :thread`.
358
358
 
359
- Note that `SessionStore` adapter calls (`#append` / `#load`) stay on threads by default even in `:inline` mode — their timeouts are hard bounds (`Thread#join`) so a wedged store adapter can never stall the reactor. The exception is an adapter that declares itself fiber-native via an optional `callback_scheduling` method returning `:inline` (see "Fiber-native adapters" in [docs/sessions.md](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/docs/sessions.md)); its calls then run on the reactor under a cooperative timeout. In both cases a configured `callback_wrapper` composes around the adapter call inside the bound.
359
+ Note that `SessionStore` adapter calls (`#append` / `#load`) stay on threads by default even in `:inline` mode — their timeouts are hard bounds (`Thread#join`) so a wedged store adapter can never stall the reactor. The exception is an adapter that declares itself fiber-native via an optional `callback_scheduling` method returning `:inline` (see "Fiber-native adapters" in [docs/sessions.md](https://github.com/rubycatco/claude-agent-sdk-ruby/blob/main/docs/sessions.md)); its calls then run on the reactor under a cooperative timeout. In both cases a configured `callback_wrapper` composes around the adapter call inside the bound.
360
360
 
361
361
  ## ActionCable Streaming
362
362
 
@@ -541,7 +541,7 @@ end
541
541
  Then every `ClaudeAgentSDK.query` and `Client` session automatically gets traced — no per-call wiring needed. The lambda factory ensures each request gets its own observer with isolated span state, safe for concurrent Puma/Sidekiq workers.
542
542
 
543
543
  See:
544
- - [examples/rails_actioncable_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/rails_actioncable_example.rb)
545
- - [examples/rails_background_job_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/rails_background_job_example.rb)
546
- - [examples/session_resumption_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/session_resumption_example.rb)
547
- - [examples/http_mcp_server_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/http_mcp_server_example.rb)
544
+ - [examples/rails_actioncable_example.rb](https://github.com/rubycatco/claude-agent-sdk-ruby/blob/main/examples/rails_actioncable_example.rb)
545
+ - [examples/rails_background_job_example.rb](https://github.com/rubycatco/claude-agent-sdk-ruby/blob/main/examples/rails_background_job_example.rb)
546
+ - [examples/session_resumption_example.rb](https://github.com/rubycatco/claude-agent-sdk-ruby/blob/main/examples/session_resumption_example.rb)
547
+ - [examples/http_mcp_server_example.rb](https://github.com/rubycatco/claude-agent-sdk-ruby/blob/main/examples/http_mcp_server_example.rb)
data/docs/sessions.md CHANGED
@@ -398,7 +398,7 @@ end
398
398
  `InMemorySessionStore#append` is a working reference.
399
399
 
400
400
  Copy-in reference adapters for **S3, Redis, and Postgres** live in
401
- [`examples/session_stores/`](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/session_stores/README.md), each with a
401
+ [`examples/session_stores/`](https://github.com/rubycatco/claude-agent-sdk-ruby/blob/main/examples/session_stores/README.md), each with a
402
402
  production checklist.
403
403
 
404
404
  #### Fiber-native adapters
data/docs/subagents.md CHANGED
@@ -158,7 +158,7 @@ See [hook fields and permission cancellation](hooks-and-permissions.md).
158
158
 
159
159
  ## Minimal example
160
160
 
161
- [`examples/subagent_status_example.rb`](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/subagent_status_example.rb)
161
+ [`examples/subagent_status_example.rb`](https://github.com/rubycatco/claude-agent-sdk-ruby/blob/main/examples/subagent_status_example.rb)
162
162
  defines a tool-free reviewer, registers lifecycle hooks, reads metadata, and
163
163
  prints task events and forwarded child text without building a status model:
164
164
 
data/docs/types.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Types Reference
2
2
 
3
- See [lib/claude_agent_sdk/types/](https://github.com/ya-luotao/claude-agent-sdk-ruby/tree/main/lib/claude_agent_sdk/types) for complete type definitions (one file per area; `types.rb` loads them all).
3
+ See [lib/claude_agent_sdk/types/](https://github.com/rubycatco/claude-agent-sdk-ruby/tree/main/lib/claude_agent_sdk/types) for complete type definitions (one file per area; `types.rb` loads them all).
4
4
 
5
5
  ## Hash keys
6
6
 
@@ -214,7 +214,7 @@ message types of their own.
214
214
 
215
215
  | Class | Wire type | Attributes | Notes |
216
216
  |-------|-----------|------------|-------|
217
- | `InitMessage` | `system` / `init` | `uuid`, `session_id`, `model`, `cwd`, `tools`, `mcp_servers`, `agents`, `skills`, `plugins`, `slash_commands`, `permission_mode`, `claude_code_version`, `api_key_source`, `betas`, `output_style`, `fast_mode_state` | Start of every turn, with the session as the CLI sees it (so a multi-query `Client` session receives one per query) |
217
+ | `InitMessage` | `system` / `init` | `uuid`, `session_id`, `model`, `cwd`, `tools`, `mcp_servers`, `agents`, `skills`, `plugins`, `plugin_errors`, `slash_commands`, `terminal_slash_commands`, `permission_mode`, `claude_code_version`, `api_key_source`, `betas`, `output_style`, `fast_mode_state`, `fast_mode_disabled_reason`, `capabilities` (protocol features such as `"interrupt_receipt_v1"`: test it instead of `claude_code_version`) | Start of every turn, with the session as the CLI sees it (so a multi-query `Client` session receives one per query) |
218
218
  | `CompactBoundaryMessage` | `system` / `compact_boundary` | `uuid`, `session_id`, `compact_metadata` (a `CompactMetadata`: `pre_tokens`, `post_tokens`, `trigger`, `preserved_segment`, `custom_instructions`) | Context compaction completed |
219
219
  | `StatusMessage` | `system` / `status` | `uuid`, `session_id`, `status`, `permission_mode` | Compacting status, permission mode changes |
220
220
  | `APIRetryMessage` | `system` / `api_retry` | `uuid`, `session_id`, `attempt`, `max_retries`, `retry_delay_ms`, `error_status`, `error` | The CLI is retrying an API request |
@@ -53,7 +53,7 @@ module ClaudeAgentSDK
53
53
  # Single source of truth: bumped here (and only here) by
54
54
  # .github/workflows/cli-pin-bump.yml or a Python-sync release, so a
55
55
  # Dependabot bump of the gem carries the CLI forward with it.
56
- PINNED_CLI_VERSION = '2.1.288'
56
+ PINNED_CLI_VERSION = '2.1.295'
57
57
  # @api private
58
58
  BINARY_NAME = 'claude'
59
59
  # @api private
@@ -868,227 +868,55 @@ module ClaudeAgentSDK
868
868
  @callback_request_signals.delete(request_id)
869
869
  end
870
870
 
871
- def parse_hook_input(input_data) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength -- one branch per hook event type
871
+ # The typed input built for each hook event the SDK models; any other
872
+ # event becomes an UnknownHookInput.
873
+ HOOK_INPUT_CLASSES = {
874
+ 'PreToolUse' => PreToolUseHookInput,
875
+ 'PostToolUse' => PostToolUseHookInput,
876
+ 'PostToolUseFailure' => PostToolUseFailureHookInput,
877
+ 'UserPromptSubmit' => UserPromptSubmitHookInput,
878
+ 'Stop' => StopHookInput,
879
+ 'SubagentStop' => SubagentStopHookInput,
880
+ 'Notification' => NotificationHookInput,
881
+ 'SubagentStart' => SubagentStartHookInput,
882
+ 'PermissionRequest' => PermissionRequestHookInput,
883
+ 'PreCompact' => PreCompactHookInput,
884
+ 'SessionStart' => SessionStartHookInput,
885
+ 'SessionEnd' => SessionEndHookInput,
886
+ 'Setup' => SetupHookInput,
887
+ 'TeammateIdle' => TeammateIdleHookInput,
888
+ 'TaskCompleted' => TaskCompletedHookInput,
889
+ 'ConfigChange' => ConfigChangeHookInput,
890
+ 'WorktreeCreate' => WorktreeCreateHookInput,
891
+ 'WorktreeRemove' => WorktreeRemoveHookInput,
892
+ 'StopFailure' => StopFailureHookInput,
893
+ 'PostCompact' => PostCompactHookInput,
894
+ 'PermissionDenied' => PermissionDeniedHookInput,
895
+ 'TaskCreated' => TaskCreatedHookInput,
896
+ 'Elicitation' => ElicitationHookInput,
897
+ 'ElicitationResult' => ElicitationResultHookInput,
898
+ 'InstructionsLoaded' => InstructionsLoadedHookInput,
899
+ 'CwdChanged' => CwdChangedHookInput,
900
+ 'FileChanged' => FileChangedHookInput,
901
+ 'PostToolBatch' => PostToolBatchHookInput,
902
+ 'UserPromptExpansion' => UserPromptExpansionHookInput,
903
+ 'PreModelSwitch' => PreModelSwitchHookInput,
904
+ 'PostModelSwitch' => PostModelSwitchHookInput,
905
+ 'DirectoryAdded' => DirectoryAddedHookInput,
906
+ 'MessageDisplay' => MessageDisplayHookInput
907
+ }.freeze
908
+ private_constant :HOOK_INPUT_CLASSES
909
+
910
+ # Every field of the payload that the class declares is assigned (the
911
+ # lenient parse path: other keys are skipped, not rejected), and the
912
+ # whole payload is kept as #raw_input, so a field the CLI added after this
913
+ # SDK version is still readable there.
914
+ def parse_hook_input(input_data)
915
+ input_data = {} unless input_data.is_a?(Hash)
872
916
  event_name = input_data[:hook_event_name] || input_data['hook_event_name']
873
- fetch = lambda do |key|
874
- if input_data.key?(key)
875
- input_data[key]
876
- elsif input_data.key?(key.to_s)
877
- input_data[key.to_s]
878
- end
879
- end
880
- base_args = {
881
- session_id: fetch.call(:session_id),
882
- transcript_path: fetch.call(:transcript_path),
883
- cwd: fetch.call(:cwd),
884
- permission_mode: fetch.call(:permission_mode)
885
- }
886
-
887
- # Subagent context fields shared by tool-lifecycle hooks
888
- subagent_args = {
889
- agent_id: fetch.call(:agent_id),
890
- agent_type: fetch.call(:agent_type)
891
- }
892
-
893
- case event_name
894
- when 'PreToolUse'
895
- PreToolUseHookInput.new(
896
- tool_name: fetch.call(:tool_name),
897
- tool_input: fetch.call(:tool_input),
898
- tool_use_id: fetch.call(:tool_use_id),
899
- **subagent_args, **base_args
900
- )
901
- when 'PostToolUse'
902
- PostToolUseHookInput.new(
903
- tool_name: fetch.call(:tool_name),
904
- tool_input: fetch.call(:tool_input),
905
- tool_response: fetch.call(:tool_response),
906
- tool_use_id: fetch.call(:tool_use_id),
907
- **subagent_args, **base_args
908
- )
909
- when 'PostToolUseFailure'
910
- PostToolUseFailureHookInput.new(
911
- tool_name: fetch.call(:tool_name),
912
- tool_input: fetch.call(:tool_input),
913
- tool_use_id: fetch.call(:tool_use_id),
914
- error: fetch.call(:error),
915
- is_interrupt: fetch.call(:is_interrupt),
916
- **subagent_args, **base_args
917
- )
918
- when 'UserPromptSubmit'
919
- UserPromptSubmitHookInput.new(
920
- prompt: fetch.call(:prompt),
921
- **base_args
922
- )
923
- when 'Stop'
924
- StopHookInput.new(
925
- stop_hook_active: fetch.call(:stop_hook_active),
926
- last_assistant_message: fetch.call(:last_assistant_message),
927
- background_tasks: fetch.call(:background_tasks),
928
- session_crons: fetch.call(:session_crons),
929
- **base_args
930
- )
931
- when 'SubagentStop'
932
- SubagentStopHookInput.new(
933
- stop_hook_active: fetch.call(:stop_hook_active),
934
- agent_id: fetch.call(:agent_id),
935
- agent_transcript_path: fetch.call(:agent_transcript_path),
936
- agent_type: fetch.call(:agent_type),
937
- last_assistant_message: fetch.call(:last_assistant_message),
938
- background_tasks: fetch.call(:background_tasks),
939
- session_crons: fetch.call(:session_crons),
940
- **base_args
941
- )
942
- when 'Notification'
943
- NotificationHookInput.new(
944
- message: fetch.call(:message),
945
- title: fetch.call(:title),
946
- notification_type: fetch.call(:notification_type),
947
- **base_args
948
- )
949
- when 'SubagentStart'
950
- SubagentStartHookInput.new(
951
- agent_id: fetch.call(:agent_id),
952
- agent_type: fetch.call(:agent_type),
953
- **base_args
954
- )
955
- when 'PermissionRequest'
956
- PermissionRequestHookInput.new(
957
- tool_name: fetch.call(:tool_name),
958
- tool_input: fetch.call(:tool_input),
959
- permission_suggestions: fetch.call(:permission_suggestions),
960
- **subagent_args, **base_args
961
- )
962
- when 'PreCompact'
963
- PreCompactHookInput.new(
964
- trigger: fetch.call(:trigger),
965
- custom_instructions: fetch.call(:custom_instructions),
966
- **base_args
967
- )
968
- when 'SessionStart'
969
- SessionStartHookInput.new(
970
- source: fetch.call(:source),
971
- agent_type: fetch.call(:agent_type),
972
- model: fetch.call(:model),
973
- **base_args
974
- )
975
- when 'SessionEnd'
976
- SessionEndHookInput.new(
977
- reason: fetch.call(:reason),
978
- **base_args
979
- )
980
- when 'Setup'
981
- SetupHookInput.new(
982
- trigger: fetch.call(:trigger),
983
- **base_args
984
- )
985
- when 'TeammateIdle'
986
- TeammateIdleHookInput.new(
987
- teammate_name: fetch.call(:teammate_name),
988
- team_name: fetch.call(:team_name),
989
- **base_args
990
- )
991
- when 'TaskCompleted'
992
- TaskCompletedHookInput.new(
993
- task_id: fetch.call(:task_id),
994
- task_subject: fetch.call(:task_subject),
995
- task_description: fetch.call(:task_description),
996
- teammate_name: fetch.call(:teammate_name),
997
- team_name: fetch.call(:team_name),
998
- **base_args
999
- )
1000
- when 'ConfigChange'
1001
- ConfigChangeHookInput.new(
1002
- source: fetch.call(:source),
1003
- file_path: fetch.call(:file_path),
1004
- **base_args
1005
- )
1006
- when 'WorktreeCreate'
1007
- WorktreeCreateHookInput.new(
1008
- name: fetch.call(:name),
1009
- **base_args
1010
- )
1011
- when 'WorktreeRemove'
1012
- WorktreeRemoveHookInput.new(
1013
- worktree_path: fetch.call(:worktree_path),
1014
- **base_args
1015
- )
1016
- when 'StopFailure'
1017
- StopFailureHookInput.new(
1018
- error: fetch.call(:error),
1019
- error_details: fetch.call(:error_details),
1020
- last_assistant_message: fetch.call(:last_assistant_message),
1021
- **base_args
1022
- )
1023
- when 'PostCompact'
1024
- PostCompactHookInput.new(
1025
- trigger: fetch.call(:trigger),
1026
- compact_summary: fetch.call(:compact_summary),
1027
- **base_args
1028
- )
1029
- when 'PermissionDenied'
1030
- PermissionDeniedHookInput.new(
1031
- tool_name: fetch.call(:tool_name),
1032
- tool_input: fetch.call(:tool_input),
1033
- tool_use_id: fetch.call(:tool_use_id),
1034
- reason: fetch.call(:reason),
1035
- **subagent_args, **base_args
1036
- )
1037
- when 'TaskCreated'
1038
- TaskCreatedHookInput.new(
1039
- task_id: fetch.call(:task_id),
1040
- task_subject: fetch.call(:task_subject),
1041
- task_description: fetch.call(:task_description),
1042
- teammate_name: fetch.call(:teammate_name),
1043
- team_name: fetch.call(:team_name),
1044
- **base_args
1045
- )
1046
- when 'Elicitation'
1047
- ElicitationHookInput.new(
1048
- mcp_server_name: fetch.call(:mcp_server_name),
1049
- message: fetch.call(:message),
1050
- mode: fetch.call(:mode),
1051
- url: fetch.call(:url),
1052
- elicitation_id: fetch.call(:elicitation_id),
1053
- requested_schema: fetch.call(:requested_schema),
1054
- **base_args
1055
- )
1056
- when 'ElicitationResult'
1057
- ElicitationResultHookInput.new(
1058
- mcp_server_name: fetch.call(:mcp_server_name),
1059
- elicitation_id: fetch.call(:elicitation_id),
1060
- mode: fetch.call(:mode),
1061
- action: fetch.call(:action),
1062
- content: fetch.call(:content),
1063
- **base_args
1064
- )
1065
- when 'InstructionsLoaded'
1066
- InstructionsLoadedHookInput.new(
1067
- file_path: fetch.call(:file_path),
1068
- memory_type: fetch.call(:memory_type),
1069
- load_reason: fetch.call(:load_reason),
1070
- globs: fetch.call(:globs),
1071
- trigger_file_path: fetch.call(:trigger_file_path),
1072
- **base_args
1073
- )
1074
- when 'CwdChanged'
1075
- CwdChangedHookInput.new(
1076
- old_cwd: fetch.call(:old_cwd),
1077
- new_cwd: fetch.call(:new_cwd),
1078
- **base_args
1079
- )
1080
- when 'FileChanged'
1081
- FileChangedHookInput.new(
1082
- file_path: fetch.call(:file_path),
1083
- event: fetch.call(:event),
1084
- **base_args
1085
- )
1086
- else
1087
- # Unknown event: preserve the wire event name and full raw payload
1088
- # rather than dropping event-specific fields (Python passes the raw
1089
- # dict through, so nothing is lost there).
1090
- UnknownHookInput.new(hook_event_name: event_name, raw_input: input_data, **base_args)
1091
- end
917
+ hook_input = HOOK_INPUT_CLASSES.fetch(event_name, UnknownHookInput).from_hash(input_data)
918
+ hook_input.raw_input = input_data
919
+ hook_input
1092
920
  end
1093
921
 
1094
922
  def handle_mcp_message(request_data)
@@ -373,6 +373,15 @@ module ClaudeAgentSDK
373
373
  # On Windows spawn raises for :uid, wrapped below into
374
374
  # CLIConnectionError — fail-loud instead of the old silent ignore.
375
375
  opts = { chdir: @cwd&.to_s, uid: @options.user }.compact
376
+ # process_group: the terminal's Ctrl-C signals the whole foreground
377
+ # process group, so a CLI left in ours ends the turn on its own
378
+ # before the host can choose to interrupt, ask, or ignore it. Its own
379
+ # group keeps that decision with the host. Teardown still signals
380
+ # the pid, not the group. Windows has no :pgroup; :new_pgroup
381
+ # (CREATE_NEW_PROCESS_GROUP) is its equivalent, and Unix rejects it.
382
+ # Absent unless set, like :uid. The `-v` probe is short-lived and
383
+ # bounded, and stays in our group.
384
+ opts[Gem.win_platform? ? :new_pgroup : :pgroup] = true if @options.process_group?
376
385
 
377
386
  @stdin, @stdout, @stderr, @process = Open3.popen3(process_env, *cmd, opts)
378
387
  # The CLI emits UTF-8 regardless of the parent locale. popen3 pipes