claude-agent-sdk 0.18.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +1 -1
- data/docs/types.md +13 -3
- data/lib/claude-agent-sdk.rb +8 -0
- data/lib/claude_agent_sdk/command_builder.rb +3 -0
- data/lib/claude_agent_sdk/instrumentation.rb +6 -0
- data/lib/claude_agent_sdk/message_parser.rb +23 -3
- data/lib/claude_agent_sdk/query.rb +5 -1
- data/lib/claude_agent_sdk/sdk_mcp_server.rb +5 -0
- data/lib/claude_agent_sdk/session_mutations.rb +15 -5
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +7 -0
- data/lib/claude_agent_sdk/types.rb +47 -1
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +34 -14
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fc9c6b243b2a7ac982141db36c55d510d9a08cb18caeffb5dd2d9ed59fefc42d
|
|
4
|
+
data.tar.gz: 7ce8124f1cebd923857fda75532caa8332a89d930564ff9c474e847e57fcb1d8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7c548159588586dd908b4a51173bdf89abac417ceab753d8eb8b313095463770dbf5fc44e7af9c90dce26df651f11ae5b25eca28d755a17e2c5b68984eff1ccb
|
|
7
|
+
data.tar.gz: 0a46ae5342425411af912c98ab5f96607bdf4d8ec76f027a7388b5519c3dc7d425a79e03a4b82c7317e9f983dcd232c7375d91f23feb33ecb758289f57dcce3b
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.19.1] - 2026-07-03
|
|
11
|
+
|
|
12
|
+
Zero-risk fix batch from the 2026-07-03 full-codebase audit (`AUDIT-2026-07-03.md`, PR #41).
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- `lib/claude-agent-sdk.rb` require shim: `Bundler.require` now loads the SDK in default Rails/Bundler apps. Previously Bundler tried `claude-agent-sdk` and `claude/agent/sdk`, silently swallowed both LoadErrors, and left `ClaudeAgentSDK` undefined until a confusing `NameError` at first use.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- `require 'claude_agent_sdk/instrumentation'` now loads the SDK core, so the documented Rails observability initializer (and `OTelObserver`'s own `@example`) works as the only require.
|
|
19
|
+
- Typed `McpStdioServerConfig` / `McpSSEServerConfig` / `McpHttpServerConfig` / `McpSdkServerConfig` objects passed directly in `mcp_servers` (without `.to_h`) now serialize to their wire hash instead of a `#<...>` string the CLI cannot parse — and `McpSdkServerConfig` instances now actually register their in-process server, so its tools connect.
|
|
20
|
+
- `create_tool` with a Symbol name produced a tool that was advertised in `tools/list` but failed every invocation with 'Tool not found'; names are now coerced to String.
|
|
21
|
+
- One stdout line carrying invalid UTF-8 bytes no longer aborts the whole message stream (which dropped all buffered valid frames, including a trailing `result`); the bad line is scrubbed, matching the version-probe path.
|
|
22
|
+
- `fork_session` no longer hangs forever on a transcript with a `parentUuid` cycle among progress entries.
|
|
23
|
+
- `fork_session` / `delete_session` no longer stop at a 0-byte transcript stub: they fall through to the worktree project dir holding the real transcript, mirroring the read path. A session whose *only* copy is a 0-byte stub now raises `Errno::ENOENT`, consistent with the read paths that already hide it.
|
|
24
|
+
- Long-lived `Client`s using SDK MCP servers no longer leak one finished internal task per synchronously-handled control request (MCP metadata requests, unsupported subtypes).
|
|
25
|
+
- An observer implementing none of the observer interface — most commonly a Class passed instead of an instance (`observers: [OTelObserver]`) — is now warned about and skipped instead of producing silent zero instrumentation.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- The gem packages git-tracked files only (previously a working-tree glob, which shipped stray untracked files under `lib/`/`docs/` in locally built gems). `gem build` from a tree with tracked-but-deleted files now fails fast instead of silently packaging whatever was on disk.
|
|
29
|
+
|
|
30
|
+
## [0.19.0] - 2026-06-29
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
- `TaskUpdatedMessage` — typed `system`/`task_updated` lifecycle events (Python SDK 0.2.101 / #1016 parity). A background task's terminal state can arrive *only* as a `task_updated` patch with no accompanying `TaskNotificationMessage` (e.g. a `TaskStop`-killed task reports `status: 'killed'` here), so consumers tracking active task IDs no longer hang waiting for a notification that never comes. `status` is derived from `patch['status']` (parsed defensively — a non-Hash/absent patch falls back to `{}`, `task_id` defaults to `''` so it is never nil, and parsing never raises). New `TASK_UPDATED_STATUSES` and `TERMINAL_TASK_STATUSES` constants; the latter spans both lifecycle vocabularies (`task_notification` reports `stopped`, `task_updated` reports the raw `killed`) so a terminal status from *either* message clears active-task tracking.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
- Malformed CLI message content now raises a descriptive `MessageParseError` instead of an opaque `TypeError`/`NoMethodError` (Python SDK #1058 parity): a non-Hash content block (e.g. a bare String) and an assistant `content` that is not an Array are both caught with a clear message carrying the full payload, rather than crashing deep inside block parsing on `block[:type]`.
|
|
37
|
+
|
|
10
38
|
## [0.18.0] - 2026-06-12
|
|
11
39
|
|
|
12
40
|
### Added
|
data/README.md
CHANGED
|
@@ -68,7 +68,7 @@ Add this line to your application's Gemfile:
|
|
|
68
68
|
gem 'claude-agent-sdk', github: 'ya-luotao/claude-agent-sdk-ruby'
|
|
69
69
|
|
|
70
70
|
# Or use a stable version from RubyGems
|
|
71
|
-
gem 'claude-agent-sdk', '~> 0.
|
|
71
|
+
gem 'claude-agent-sdk', '~> 0.19.1'
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
Then `bundle install`, or install directly: `gem install claude-agent-sdk`.
|
data/docs/types.md
CHANGED
|
@@ -42,22 +42,30 @@ System message with metadata. Task lifecycle events are typed subclasses.
|
|
|
42
42
|
|
|
43
43
|
```ruby
|
|
44
44
|
class SystemMessage
|
|
45
|
-
attr_accessor :subtype, # String ('init', 'task_started', 'task_progress', 'task_notification', etc.)
|
|
45
|
+
attr_accessor :subtype, # String ('init', 'task_started', 'task_progress', 'task_notification', 'task_updated', etc.)
|
|
46
46
|
:data # Hash
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
# Typed subclasses (all inherit from SystemMessage, so is_a?(SystemMessage) still works)
|
|
50
50
|
class TaskStartedMessage < SystemMessage
|
|
51
|
-
attr_accessor :task_id, :description, :uuid, :session_id, :tool_use_id, :task_type
|
|
51
|
+
attr_accessor :task_id, :description, :uuid, :session_id, :tool_use_id, :task_type, :workflow_name, :prompt
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
class TaskProgressMessage < SystemMessage
|
|
55
|
-
attr_accessor :task_id, :description, :usage, :uuid, :session_id, :tool_use_id, :last_tool_name
|
|
55
|
+
attr_accessor :task_id, :description, :usage, :uuid, :session_id, :tool_use_id, :last_tool_name, :summary
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
class TaskNotificationMessage < SystemMessage
|
|
59
59
|
attr_accessor :task_id, :status, :output_file, :summary, :uuid, :session_id, :tool_use_id, :usage
|
|
60
60
|
end
|
|
61
|
+
|
|
62
|
+
# Background task lifecycle state change. `status` is derived from patch["status"].
|
|
63
|
+
# A terminal task can arrive *only* as a TaskUpdatedMessage (no TaskNotificationMessage) —
|
|
64
|
+
# e.g. a TaskStop-killed task reports status "killed" here. Clear tracked task IDs on a
|
|
65
|
+
# terminal status (see TERMINAL_TASK_STATUSES) from *either* message.
|
|
66
|
+
class TaskUpdatedMessage < SystemMessage
|
|
67
|
+
attr_accessor :task_id, :patch, :status, :uuid, :session_id
|
|
68
|
+
end
|
|
61
69
|
```
|
|
62
70
|
|
|
63
71
|
### ResultMessage
|
|
@@ -183,5 +191,7 @@ end
|
|
|
183
191
|
| `HOOK_EVENTS` | Available hook events |
|
|
184
192
|
| `ASSISTANT_MESSAGE_ERRORS` | Possible error types in AssistantMessage |
|
|
185
193
|
| `TASK_NOTIFICATION_STATUSES` | Task lifecycle notification statuses (`completed`, `failed`, `stopped`) |
|
|
194
|
+
| `TASK_UPDATED_STATUSES` | `task_updated` patch statuses (`pending`, `running`, `paused`, `completed`, `failed`, `killed`) |
|
|
195
|
+
| `TERMINAL_TASK_STATUSES` | Statuses meaning a task has finished — spans both vocabularies (`completed`, `failed`, `stopped`, `killed`); clear active-task tracking on any of these |
|
|
186
196
|
| `MCP_SERVER_CONNECTION_STATUSES` | MCP server connection states (`connected`, `failed`, `needs-auth`, `pending`, `disabled`) |
|
|
187
197
|
| `EFFORT_LEVELS` | Effort levels (`low`, `medium`, `high`, `xhigh`, `max`) |
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Bundler autorequire shim. The gem is named `claude-agent-sdk`, so
|
|
4
|
+
# `Bundler.require` tries `require 'claude-agent-sdk'` and then
|
|
5
|
+
# `require 'claude/agent/sdk'` — and silently swallows both LoadErrors when
|
|
6
|
+
# neither file exists, leaving the SDK unloaded in default Rails/Bundler
|
|
7
|
+
# apps. This file makes the gem-named require resolve to the real entry point.
|
|
8
|
+
require_relative 'claude_agent_sdk'
|
|
@@ -306,6 +306,9 @@ module ClaudeAgentSDK
|
|
|
306
306
|
if @options.mcp_servers.is_a?(Hash)
|
|
307
307
|
servers_for_cli = {}
|
|
308
308
|
@options.mcp_servers.each do |name, config|
|
|
309
|
+
# Typed Mcp*ServerConfig objects serialize via their wire hash —
|
|
310
|
+
# without this they'd JSON-stringify as "#<...>" via to_s.
|
|
311
|
+
config = config.to_h if config.is_a?(Type)
|
|
309
312
|
servers_for_cli[name] = if config.is_a?(Hash) && config[:type] == "sdk"
|
|
310
313
|
config.except(:instance)
|
|
311
314
|
else
|
|
@@ -3,4 +3,10 @@
|
|
|
3
3
|
# Instrumentation adapters for ClaudeAgentSDK.
|
|
4
4
|
# Each adapter lazy-requires its external gem, so loading this file has zero cost
|
|
5
5
|
# unless you instantiate a specific observer.
|
|
6
|
+
#
|
|
7
|
+
# This entry point is documented as a standalone require (docs/rails.md's
|
|
8
|
+
# initializer, OTelObserver's @example), so it must load the SDK core too —
|
|
9
|
+
# otherwise ClaudeAgentSDK exists (observer.rb reopens the module) but has
|
|
10
|
+
# no .configure/.query/ClaudeAgentOptions.
|
|
11
|
+
require_relative '../claude_agent_sdk'
|
|
6
12
|
require_relative 'instrumentation/otel'
|
|
@@ -51,7 +51,7 @@ module ClaudeAgentSDK
|
|
|
51
51
|
raise MessageParseError.new("Missing content in user message", data: data) unless content
|
|
52
52
|
|
|
53
53
|
if content.is_a?(Array)
|
|
54
|
-
content_blocks = content
|
|
54
|
+
content_blocks = parse_content_blocks(content, data)
|
|
55
55
|
UserMessage.new(content: content_blocks, uuid: uuid, parent_tool_use_id: parent_tool_use_id,
|
|
56
56
|
tool_use_result: tool_use_result)
|
|
57
57
|
else
|
|
@@ -63,8 +63,9 @@ module ClaudeAgentSDK
|
|
|
63
63
|
def self.parse_assistant_message(data)
|
|
64
64
|
content = data.dig(:message, :content)
|
|
65
65
|
raise MessageParseError.new("Missing content in assistant message", data: data) unless content
|
|
66
|
+
raise MessageParseError.new("Invalid assistant content (expected Array, got #{content.class})", data: data) unless content.is_a?(Array)
|
|
66
67
|
|
|
67
|
-
content_blocks = content
|
|
68
|
+
content_blocks = parse_content_blocks(content, data)
|
|
68
69
|
AssistantMessage.new(
|
|
69
70
|
content: content_blocks,
|
|
70
71
|
model: data.dig(:message, :model),
|
|
@@ -96,7 +97,14 @@ module ClaudeAgentSDK
|
|
|
96
97
|
'elicitation_complete' => ElicitationCompleteMessage,
|
|
97
98
|
'task_started' => TaskStartedMessage,
|
|
98
99
|
'task_progress' => TaskProgressMessage,
|
|
99
|
-
'task_notification' => TaskNotificationMessage
|
|
100
|
+
'task_notification' => TaskNotificationMessage,
|
|
101
|
+
# task_updated carries `status` inside `patch` (not at the top level) and
|
|
102
|
+
# defaults task_id to "" — it derives those defensively in its own
|
|
103
|
+
# constructor (see TaskUpdatedMessage), so it dispatches through the table
|
|
104
|
+
# like every other system subtype. `data` is always symbol-keyed here:
|
|
105
|
+
# `parse` rejects any message lacking a `:type` symbol key, so a
|
|
106
|
+
# string-keyed hash never reaches these classes.
|
|
107
|
+
'task_updated' => TaskUpdatedMessage
|
|
100
108
|
}.freeze
|
|
101
109
|
|
|
102
110
|
def self.parse_system_message(data)
|
|
@@ -132,6 +140,18 @@ module ClaudeAgentSDK
|
|
|
132
140
|
PromptSuggestionMessage.new(data)
|
|
133
141
|
end
|
|
134
142
|
|
|
143
|
+
# Maps a content Array to typed blocks, guarding each element. A non-Hash
|
|
144
|
+
# block (e.g. a bare String or nil from a malformed CLI message) raises a
|
|
145
|
+
# descriptive MessageParseError carrying the full message rather than an
|
|
146
|
+
# opaque TypeError/NoMethodError from `block[:type]` deep in parsing.
|
|
147
|
+
def self.parse_content_blocks(content, data)
|
|
148
|
+
content.map do |block|
|
|
149
|
+
raise MessageParseError.new("Invalid content block (expected Hash, got #{block.class})", data: data) unless block.is_a?(Hash)
|
|
150
|
+
|
|
151
|
+
parse_content_block(block)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
135
155
|
# Accepts blocks with either symbol or string keys — live CLI messages
|
|
136
156
|
# arrive symbol-keyed (parsed via `symbolize_names: true`), session
|
|
137
157
|
# transcripts arrive string-keyed (parsed via `symbolize_names: false`).
|
|
@@ -256,7 +256,11 @@ module ClaudeAgentSDK
|
|
|
256
256
|
@inflight_control_request_tasks.delete(request_id) if request_id
|
|
257
257
|
end
|
|
258
258
|
end
|
|
259
|
-
|
|
259
|
+
# A handler that never suspends (MCP metadata, unsupported-subtype
|
|
260
|
+
# error path) already ran to completion inside the async{} above —
|
|
261
|
+
# its ensure-delete fired before this insert, so registering it here
|
|
262
|
+
# would leak a finished task in the map forever.
|
|
263
|
+
@inflight_control_request_tasks[request_id] = handler_task if request_id && !handler_task.finished?
|
|
260
264
|
when 'control_cancel_request'
|
|
261
265
|
request_id = message[:request_id] || message[:requestId]
|
|
262
266
|
task = request_id ? @inflight_control_request_tasks[request_id] : nil
|
|
@@ -470,6 +470,11 @@ module ClaudeAgentSDK
|
|
|
470
470
|
resolved_meta = { 'anthropic/maxResultSizeChars' => max_chars } if max_chars
|
|
471
471
|
end
|
|
472
472
|
|
|
473
|
+
# tools/call arrives from the CLI with the name as a JSON String; the mcp
|
|
474
|
+
# gem keys its lookup on the value stored here, so a Symbol name would be
|
|
475
|
+
# advertised in tools/list yet miss on every invocation ('Tool not found').
|
|
476
|
+
name = name.to_s if name.is_a?(Symbol)
|
|
477
|
+
|
|
473
478
|
SdkMcpTool.new(
|
|
474
479
|
name: name,
|
|
475
480
|
description: description,
|
|
@@ -280,11 +280,17 @@ module ClaudeAgentSDK
|
|
|
280
280
|
nil
|
|
281
281
|
end
|
|
282
282
|
|
|
283
|
+
# A candidate counts only when it exists AND is non-empty — a 0-byte stub
|
|
284
|
+
# in one project dir must not stop the search when the real transcript
|
|
285
|
+
# lives under another (worktree) project dir. Mirrors the read path
|
|
286
|
+
# (Sessions.stat_candidate) and the append path (try_append).
|
|
283
287
|
def try_project_dir(file_name, project_dir)
|
|
284
288
|
return nil unless project_dir
|
|
285
289
|
|
|
286
290
|
candidate = File.join(project_dir, file_name)
|
|
287
|
-
File.
|
|
291
|
+
File.size(candidate).positive? ? [candidate, project_dir] : nil
|
|
292
|
+
rescue SystemCallError
|
|
293
|
+
nil
|
|
288
294
|
end
|
|
289
295
|
|
|
290
296
|
def find_in_all_projects(file_name)
|
|
@@ -295,8 +301,8 @@ module ClaudeAgentSDK
|
|
|
295
301
|
pd = File.join(projects_dir, child)
|
|
296
302
|
next unless File.directory?(pd)
|
|
297
303
|
|
|
298
|
-
|
|
299
|
-
return
|
|
304
|
+
result = try_project_dir(file_name, pd)
|
|
305
|
+
return result if result
|
|
300
306
|
end
|
|
301
307
|
nil
|
|
302
308
|
end
|
|
@@ -523,9 +529,13 @@ module ClaudeAgentSDK
|
|
|
523
529
|
JSON.generate(forked)
|
|
524
530
|
end
|
|
525
531
|
|
|
526
|
-
# Walk up parentUuid chain skipping progress entries.
|
|
532
|
+
# Walk up parentUuid chain skipping progress entries. The visited set
|
|
533
|
+
# guards against parentUuid cycles among progress entries (corrupt
|
|
534
|
+
# transcripts) like every other chain walker — an unguarded walk spun
|
|
535
|
+
# forever and hung both fork paths.
|
|
527
536
|
def resolve_parent_uuid(parent_id, by_uuid, uuid_mapping)
|
|
528
|
-
|
|
537
|
+
visited = Set.new
|
|
538
|
+
while parent_id && visited.add?(parent_id)
|
|
529
539
|
parent = by_uuid[parent_id]
|
|
530
540
|
break unless parent
|
|
531
541
|
return uuid_mapping[parent_id] if parent['type'] != 'progress'
|
|
@@ -494,6 +494,13 @@ module ClaudeAgentSDK
|
|
|
494
494
|
# an oversized line was fully allocated BEFORE the 1MB cap could
|
|
495
495
|
# fire — unbounded memory on hostile/buggy stdout.
|
|
496
496
|
@stdout.each_line("\n", @max_buffer_size + 1) do |line|
|
|
497
|
+
# stdout is UTF-8-tagged but the CLI can emit invalid bytes (echoed
|
|
498
|
+
# binary/latin-1 output). strip/lstrip below raise on invalid
|
|
499
|
+
# encoding, which would abort the whole stream and drop buffered
|
|
500
|
+
# valid frames — scrub the one bad line instead (the version-probe
|
|
501
|
+
# path guards the same way).
|
|
502
|
+
line = line.scrub unless line.valid_encoding?
|
|
503
|
+
|
|
497
504
|
# Position-aware whitespace handling: a chunk of an over-limit line
|
|
498
505
|
# must keep its interior whitespace — a blanket per-chunk strip
|
|
499
506
|
# deleted spaces inside JSON strings straddling the chunk boundary
|
|
@@ -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,
|
data/lib/claude_agent_sdk.rb
CHANGED
|
@@ -23,13 +23,43 @@ require 'securerandom'
|
|
|
23
23
|
|
|
24
24
|
# Claude Agent SDK for Ruby
|
|
25
25
|
module ClaudeAgentSDK
|
|
26
|
+
# The duck-typed observer surface probed by resolve_observers — implementing
|
|
27
|
+
# any one of these counts as an observer (see Observer's no-op defaults).
|
|
28
|
+
OBSERVER_INTERFACE = %i[on_user_prompt on_message on_error on_close].freeze
|
|
29
|
+
|
|
26
30
|
# Resolve observers array: callables (Proc/lambda) are invoked to produce
|
|
27
31
|
# a fresh instance per query/session (thread-safe); plain objects are used as-is.
|
|
28
32
|
# Array() guards against nil (e.g., when observers: nil is passed explicitly).
|
|
33
|
+
# Anything implementing none of the observer methods is warned about and
|
|
34
|
+
# skipped — most commonly a Class passed instead of an instance, which
|
|
35
|
+
# previously produced silent zero instrumentation (every notify raised
|
|
36
|
+
# NoMethodError, swallowed by notify_observers' error containment).
|
|
29
37
|
def self.resolve_observers(observers)
|
|
30
|
-
Array(observers).
|
|
31
|
-
obs.respond_to?(:call) ? obs.call : obs
|
|
38
|
+
Array(observers).filter_map do |obs|
|
|
39
|
+
resolved = obs.respond_to?(:call) ? obs.call : obs
|
|
40
|
+
if OBSERVER_INTERFACE.none? { |m| resolved.respond_to?(m) }
|
|
41
|
+
label = resolved.is_a?(Module) ? resolved : resolved.class
|
|
42
|
+
hint = resolved.is_a?(Module) ? " — pass an instance (#{resolved}.new) or a factory lambda" : ''
|
|
43
|
+
warn "ClaudeAgentSDK: ignoring observer #{label}: it implements none of #{OBSERVER_INTERFACE.join('/')}#{hint}"
|
|
44
|
+
next nil
|
|
45
|
+
end
|
|
46
|
+
resolved
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Internal: pull live SDK MCP server instances out of an mcp_servers Hash.
|
|
51
|
+
# Accepts both raw Hash configs and typed Mcp*ServerConfig objects — a
|
|
52
|
+
# McpSdkServerConfig passed without .to_h previously failed the Hash-only
|
|
53
|
+
# guard, so its in-process server was silently never registered.
|
|
54
|
+
def self.extract_sdk_mcp_servers(mcp_servers)
|
|
55
|
+
return {} unless mcp_servers.is_a?(Hash)
|
|
56
|
+
|
|
57
|
+
servers = {}
|
|
58
|
+
mcp_servers.each do |name, config|
|
|
59
|
+
config = config.to_h if config.is_a?(Type)
|
|
60
|
+
servers[name] = config[:instance] if config.is_a?(Hash) && config[:type] == 'sdk'
|
|
32
61
|
end
|
|
62
|
+
servers
|
|
33
63
|
end
|
|
34
64
|
|
|
35
65
|
# Safely call a method on each observer, suppressing any errors.
|
|
@@ -391,12 +421,7 @@ module ClaudeAgentSDK
|
|
|
391
421
|
transport.connect
|
|
392
422
|
|
|
393
423
|
# Extract SDK MCP servers
|
|
394
|
-
sdk_mcp_servers =
|
|
395
|
-
if configured_options.mcp_servers.is_a?(Hash)
|
|
396
|
-
configured_options.mcp_servers.each do |name, config|
|
|
397
|
-
sdk_mcp_servers[name] = config[:instance] if config.is_a?(Hash) && config[:type] == 'sdk'
|
|
398
|
-
end
|
|
399
|
-
end
|
|
424
|
+
sdk_mcp_servers = extract_sdk_mcp_servers(configured_options.mcp_servers)
|
|
400
425
|
|
|
401
426
|
hooks = nil
|
|
402
427
|
if configured_options.hooks
|
|
@@ -903,12 +928,7 @@ module ClaudeAgentSDK
|
|
|
903
928
|
@transport.connect
|
|
904
929
|
|
|
905
930
|
# Extract SDK MCP servers
|
|
906
|
-
sdk_mcp_servers =
|
|
907
|
-
if configured_options.mcp_servers.is_a?(Hash)
|
|
908
|
-
configured_options.mcp_servers.each do |name, config|
|
|
909
|
-
sdk_mcp_servers[name] = config[:instance] if config.is_a?(Hash) && config[:type] == 'sdk'
|
|
910
|
-
end
|
|
911
|
-
end
|
|
931
|
+
sdk_mcp_servers = ClaudeAgentSDK.extract_sdk_mcp_servers(configured_options.mcp_servers)
|
|
912
932
|
|
|
913
933
|
# Convert hooks to internal format
|
|
914
934
|
hooks = convert_hooks_to_internal_format(configured_options.hooks) if configured_options.hooks
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: claude-agent-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.19.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Community Contributors
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: async
|
|
@@ -119,6 +119,7 @@ files:
|
|
|
119
119
|
- docs/rails.md
|
|
120
120
|
- docs/sessions.md
|
|
121
121
|
- docs/types.md
|
|
122
|
+
- lib/claude-agent-sdk.rb
|
|
122
123
|
- lib/claude_agent_sdk.rb
|
|
123
124
|
- lib/claude_agent_sdk/command_builder.rb
|
|
124
125
|
- lib/claude_agent_sdk/configuration.rb
|