claude-agent-sdk 0.17.0 → 0.19.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.
@@ -333,6 +333,19 @@ module ClaudeAgentSDK
333
333
  # Task lifecycle notification statuses
334
334
  TASK_NOTIFICATION_STATUSES = %w[completed failed stopped].freeze
335
335
 
336
+ # Possible status values reported inside a `task_updated` patch.
337
+ # pending/running/paused are non-terminal; completed/failed/killed are
338
+ # terminal. Note: task_updated reports the raw "killed"; the CLI maps that to
339
+ # "stopped" only when it emits a task_notification.
340
+ TASK_UPDATED_STATUSES = %w[pending running paused completed failed killed].freeze
341
+
342
+ # Task statuses that mean the task has finished and should be cleared from any
343
+ # "active task" tracking. Spans both lifecycle vocabularies: task_notification
344
+ # reports "stopped" (the CLI's mapped form of a killed task) while task_updated
345
+ # reports the raw "killed". Treat the status of a TaskNotificationMessage and a
346
+ # TaskUpdatedMessage the same way.
347
+ TERMINAL_TASK_STATUSES = %w[completed failed stopped killed].freeze
348
+
336
349
  # Typed usage data for task progress and notifications
337
350
  class TaskUsage < Type
338
351
  attr_accessor :total_tokens, :tool_uses, :duration_ms
@@ -356,11 +369,44 @@ module ClaudeAgentSDK
356
369
  attr_accessor :task_id, :description, :usage, :uuid, :session_id, :tool_use_id, :last_tool_name, :summary
357
370
  end
358
371
 
359
- # Task notification system message (task completed/failed/stopped)
372
+ # Task notification system message (task completed/failed/stopped).
373
+ #
374
+ # Note: not every terminal task emits this message. Background tasks may
375
+ # instead report completion only via a TaskUpdatedMessage whose patch["status"]
376
+ # is terminal (see TERMINAL_TASK_STATUSES). Consumers tracking active task IDs
377
+ # should clear them on a terminal status from *either* message.
360
378
  class TaskNotificationMessage < SystemMessage
361
379
  attr_accessor :task_id, :status, :output_file, :summary, :uuid, :session_id, :tool_use_id, :usage
362
380
  end
363
381
 
382
+ # Task updated system message (background task lifecycle state change).
383
+ #
384
+ # The CLI emits system/task_updated events as a task moves through its
385
+ # lifecycle. `patch` carries the changed fields (e.g. status, end_time); when
386
+ # patch["status"] is terminal (see TERMINAL_TASK_STATUSES) the task has
387
+ # finished. A background task's terminal state can arrive *only* as a
388
+ # TaskUpdatedMessage with no accompanying TaskNotificationMessage — e.g. a task
389
+ # stopped via TaskStop reports status "killed" here and the matching
390
+ # notification is sometimes suppressed. Consumers tracking active task IDs
391
+ # should clear them on a terminal status from *either* message.
392
+ #
393
+ # Parsed defensively in the constructor — a lifecycle event must never raise:
394
+ # `status` is derived from patch["status"] (not a top-level field); a non-Hash
395
+ # or absent patch falls back to {}; and `task_id` defaults to "" (never nil,
396
+ # matching the Python SDK) so consumers can rely on it always being a String.
397
+ # The full patch is preserved on `#patch` for callers that need more than the
398
+ # status.
399
+ class TaskUpdatedMessage < SystemMessage
400
+ attr_accessor :task_id, :patch, :status, :uuid, :session_id
401
+
402
+ def initialize(attributes = {})
403
+ super
404
+ @task_id ||= ''
405
+ @patch = {} unless @patch.is_a?(Hash)
406
+ @status = @patch[:status]
407
+ end
408
+ end
409
+
364
410
  # Result message with cost and usage information
365
411
  class ResultMessage < Type
366
412
  attr_accessor :subtype, :duration_ms, :duration_api_ms, :is_error,
@@ -525,7 +571,16 @@ module ClaudeAgentSDK
525
571
 
526
572
  # Permission update configuration
527
573
  class PermissionUpdate < Type
528
- attr_accessor :type, :rules, :behavior, :mode, :directories, :destination
574
+ attr_accessor :type, :behavior, :mode, :directories, :destination
575
+ attr_reader :rules
576
+
577
+ # Wire-format parity with Python PermissionUpdate.from_dict (#920): the CLI
578
+ # sends rules as camelCase hashes ({toolName:, ruleContent:}); hydrate them
579
+ # into PermissionRuleValue (Type#assign_attribute normalizes the camelCase
580
+ # keys). Already-typed PermissionRuleValue entries pass through unchanged.
581
+ def rules=(value)
582
+ @rules = value&.map { |rule| rule.is_a?(Hash) ? PermissionRuleValue.new(rule) : rule }
583
+ end
529
584
 
530
585
  def to_h
531
586
  result = { type: @type }
@@ -888,6 +943,22 @@ module ClaudeAgentSDK
888
943
  end
889
944
  end
890
945
 
946
+ # Fallback for hook events the SDK does not yet model. Carries the wire
947
+ # event name and the complete raw payload so no fields are lost (Python
948
+ # passes hook input through as a raw dict, so unknown events lose
949
+ # nothing there).
950
+ class UnknownHookInput < BaseHookInput
951
+ attr_accessor :raw_input
952
+
953
+ def initialize(attributes = {})
954
+ super
955
+ # Direct assignment: BaseHookInput exposes hook_event_name as
956
+ # attr_reader only, and Type#assign_attribute silently drops keys
957
+ # without public setters.
958
+ @hook_event_name = attributes[:hook_event_name] || attributes['hook_event_name']
959
+ end
960
+ end
961
+
891
962
  # Setup hook specific output
892
963
  class SetupHookSpecificOutput < Type
893
964
  attr_accessor :additional_context
@@ -1480,10 +1551,10 @@ module ClaudeAgentSDK
1480
1551
  :model, :permission_prompt_tool_name, :cwd, :cli_path, :settings,
1481
1552
  :add_dirs, :env, :extra_args, :max_buffer_size, :stderr,
1482
1553
  :can_use_tool, :hooks, :user,
1483
- :agents, :setting_sources,
1554
+ :agents, :setting_sources, :skills,
1484
1555
  :output_format, :max_budget_usd, :max_thinking_tokens,
1485
1556
  :fallback_model, :plugins, :debug_stderr,
1486
- :betas, :tools, :sandbox, :append_allowed_tools,
1557
+ :betas, :tools, :sandbox,
1487
1558
  :thinking, :effort, :observers, :task_budget,
1488
1559
  :session_store, :session_store_flush, :load_timeout_ms
1489
1560
  attr_reader :bare, :fork_session, :enable_file_checkpointing,
@@ -1596,8 +1667,11 @@ module ClaudeAgentSDK
1596
1667
  defaults = ClaudeAgentSDK.default_options
1597
1668
  return attributes unless defaults.any?
1598
1669
 
1599
- # Start from configured defaults (deep dup hashes to prevent mutation)
1600
- result = defaults.transform_values { |v| v.is_a?(Hash) ? v.dup : v }
1670
+ # Start from configured defaults. Container values are recursively
1671
+ # duped so per-instance mutation (options.allowed_tools << 'Bash')
1672
+ # can never corrupt the global defaults; non-container leaves
1673
+ # (Strings, Procs, SdkMcpServer instances) intentionally keep identity.
1674
+ result = deep_dup_containers(defaults)
1601
1675
  attributes.each do |key, value|
1602
1676
  default_val = result[key]
1603
1677
  result[key] = if value.nil?
@@ -1610,6 +1684,16 @@ module ClaudeAgentSDK
1610
1684
  end
1611
1685
  result
1612
1686
  end
1687
+
1688
+ # Recurse ONLY into Hash/Array; leaves keep object identity (observer
1689
+ # factories, callbacks, SDK MCP server instances must not be duped).
1690
+ def deep_dup_containers(value)
1691
+ case value
1692
+ when Hash then value.to_h { |k, v| [k, deep_dup_containers(v)] }
1693
+ when Array then value.map { |v| deep_dup_containers(v) }
1694
+ else value
1695
+ end
1696
+ end
1613
1697
  end
1614
1698
 
1615
1699
  # SDK MCP Tool definition
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeAgentSDK
4
- VERSION = '0.17.0'
4
+ VERSION = '0.19.0'
5
5
  end