claude-agent-sdk 0.36.0 → 0.37.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 +17 -0
- data/README.md +2 -2
- data/docs/client.md +26 -1
- data/docs/hooks-and-permissions.md +5 -3
- data/docs/mcp-servers.md +1 -2
- data/docs/sessions.md +81 -2
- data/docs/types.md +106 -4
- data/lib/claude_agent_sdk/cli_installer.rb +30 -3
- data/lib/claude_agent_sdk/command_builder.rb +109 -98
- data/lib/claude_agent_sdk/deprecation.rb +39 -0
- data/lib/claude_agent_sdk/fiber_boundary.rb +2 -0
- data/lib/claude_agent_sdk/instrumentation/otel.rb +15 -7
- data/lib/claude_agent_sdk/message_parser.rb +23 -9
- data/lib/claude_agent_sdk/observer.rb +2 -1
- data/lib/claude_agent_sdk/option_warnings.rb +2 -0
- data/lib/claude_agent_sdk/query.rb +50 -43
- data/lib/claude_agent_sdk/sdk_mcp_server.rb +22 -13
- data/lib/claude_agent_sdk/session_mutations.rb +20 -8
- data/lib/claude_agent_sdk/session_resume.rb +20 -11
- data/lib/claude_agent_sdk/session_store.rb +7 -3
- data/lib/claude_agent_sdk/session_summary.rb +4 -2
- data/lib/claude_agent_sdk/sessions.rb +8 -6
- data/lib/claude_agent_sdk/streaming.rb +1 -1
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +72 -40
- data/lib/claude_agent_sdk/testing/session_store_conformance.rb +14 -10
- data/lib/claude_agent_sdk/transcript_mirror_batcher.rb +2 -0
- data/lib/claude_agent_sdk/types/attributes.rb +271 -0
- data/lib/claude_agent_sdk/types/base.rb +320 -0
- data/lib/claude_agent_sdk/types/content_blocks.rb +57 -0
- data/lib/claude_agent_sdk/types/hooks.rb +640 -0
- data/lib/claude_agent_sdk/types/mcp.rb +232 -0
- data/lib/claude_agent_sdk/types/messages.rb +614 -0
- data/lib/claude_agent_sdk/types/option_values.rb +302 -0
- data/lib/claude_agent_sdk/types/options.rb +352 -0
- data/lib/claude_agent_sdk/types/permissions.rb +107 -0
- data/lib/claude_agent_sdk/types/sessions.rb +10 -0
- data/lib/claude_agent_sdk/types.rb +13 -2534
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +51 -17
- metadata +11 -1
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require_relative
|
|
5
|
-
require_relative
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative 'errors'
|
|
5
|
+
require_relative 'types'
|
|
6
6
|
|
|
7
7
|
module ClaudeAgentSDK
|
|
8
8
|
# Builds the CLI argv array from a ClaudeAgentOptions instance.
|
|
9
|
-
class CommandBuilder
|
|
9
|
+
class CommandBuilder # rubocop:disable Metrics/ClassLength -- one append_* method per CLI flag group
|
|
10
|
+
# @api private
|
|
10
11
|
EXTRA_ARG_FLAG_REGEXP = /\A[a-z0-9][a-z0-9-]*\z/
|
|
11
12
|
|
|
12
13
|
# Parentheses and commas are delimiters to the --allowedTools tokenizer;
|
|
13
14
|
# control characters (C0, DEL, C1) never appear in a skill directory name.
|
|
14
15
|
# U+FEFF is here rather than in the whitespace checks below because the
|
|
15
16
|
# CLI trims it as whitespace and [[:space:]] does not match it.
|
|
17
|
+
#
|
|
18
|
+
# @api private
|
|
16
19
|
SKILL_NAME_INVALID_CHARS = /[(),\u0000-\u001F\u007F-\u009F\uFEFF]/
|
|
17
20
|
|
|
18
21
|
# Unicode-aware edge whitespace: the CLI and Skill tool trim Unicode
|
|
19
22
|
# whitespace, and Ruby's String#strip is ASCII-only.
|
|
23
|
+
#
|
|
24
|
+
# @api private
|
|
20
25
|
SKILL_NAME_EDGE_WHITESPACE = /\A[[:space:]]+|[[:space:]]+\z/
|
|
21
26
|
|
|
22
27
|
def initialize(cli_path, options)
|
|
@@ -25,7 +30,7 @@ module ClaudeAgentSDK
|
|
|
25
30
|
end
|
|
26
31
|
|
|
27
32
|
def build
|
|
28
|
-
cmd = [@cli_path,
|
|
33
|
+
cmd = [@cli_path, '--output-format', 'stream-json', '--verbose']
|
|
29
34
|
|
|
30
35
|
# skills auto-wires the Skill tool into --allowedTools and defaults
|
|
31
36
|
# --setting-sources; compute both once so the two flags cannot diverge
|
|
@@ -56,7 +61,7 @@ module ClaudeAgentSDK
|
|
|
56
61
|
# Always use streaming mode for bidirectional control protocol.
|
|
57
62
|
# Prompts and agents are sent via stdin (initialize + user messages),
|
|
58
63
|
# which avoids OS ARG_MAX limits for large prompts and agent configurations.
|
|
59
|
-
cmd.push(
|
|
64
|
+
cmd.push('--input-format', 'stream-json')
|
|
60
65
|
|
|
61
66
|
cmd
|
|
62
67
|
end
|
|
@@ -67,37 +72,37 @@ module ClaudeAgentSDK
|
|
|
67
72
|
case @options.system_prompt
|
|
68
73
|
when nil
|
|
69
74
|
# When nil, pass empty string to ensure predictable behavior without default Claude Code system prompt
|
|
70
|
-
cmd.push(
|
|
75
|
+
cmd.push('--system-prompt', '')
|
|
71
76
|
when String
|
|
72
|
-
cmd.push(
|
|
77
|
+
cmd.push('--system-prompt', @options.system_prompt)
|
|
73
78
|
when SystemPromptFile
|
|
74
|
-
cmd.push(
|
|
79
|
+
cmd.push('--system-prompt-file', @options.system_prompt.path)
|
|
75
80
|
when SystemPromptCustom
|
|
76
81
|
# The object form of a String prompt; snapshot travels on the
|
|
77
82
|
# initialize request, not as a CLI flag.
|
|
78
|
-
cmd.push(
|
|
83
|
+
cmd.push('--system-prompt', custom_prompt_text(@options.system_prompt.prompt))
|
|
79
84
|
when SystemPromptPreset
|
|
80
85
|
# Preset activates the default Claude Code system prompt by not passing --system-prompt ""
|
|
81
86
|
# Only --append-system-prompt is passed if append text is provided
|
|
82
|
-
cmd.push(
|
|
87
|
+
cmd.push('--append-system-prompt', @options.system_prompt.append) if @options.system_prompt.append
|
|
83
88
|
when Hash
|
|
84
89
|
append_hash_system_prompt(cmd, @options.system_prompt)
|
|
85
90
|
end
|
|
86
91
|
end
|
|
87
92
|
|
|
88
93
|
def append_hash_system_prompt(cmd, prompt_hash)
|
|
89
|
-
prompt_type = prompt_hash[:type] || prompt_hash[
|
|
94
|
+
prompt_type = prompt_hash[:type] || prompt_hash['type']
|
|
90
95
|
case prompt_type
|
|
91
|
-
when
|
|
92
|
-
prompt_path = prompt_hash[:path] || prompt_hash[
|
|
93
|
-
cmd.push(
|
|
94
|
-
when
|
|
95
|
-
prompt = prompt_hash.fetch(:prompt) { prompt_hash[
|
|
96
|
-
cmd.push(
|
|
97
|
-
when
|
|
98
|
-
append = prompt_hash[:append] || prompt_hash[
|
|
96
|
+
when 'file'
|
|
97
|
+
prompt_path = prompt_hash[:path] || prompt_hash['path']
|
|
98
|
+
cmd.push('--system-prompt-file', prompt_path) if prompt_path
|
|
99
|
+
when 'custom'
|
|
100
|
+
prompt = prompt_hash.fetch(:prompt) { prompt_hash['prompt'] }
|
|
101
|
+
cmd.push('--system-prompt', custom_prompt_text(prompt))
|
|
102
|
+
when 'preset'
|
|
103
|
+
append = prompt_hash[:append] || prompt_hash['append']
|
|
99
104
|
# Preset activates the default Claude Code system prompt by not passing --system-prompt ""
|
|
100
|
-
cmd.push(
|
|
105
|
+
cmd.push('--append-system-prompt', append) if append
|
|
101
106
|
end
|
|
102
107
|
end
|
|
103
108
|
|
|
@@ -113,7 +118,7 @@ module ClaudeAgentSDK
|
|
|
113
118
|
end
|
|
114
119
|
|
|
115
120
|
def append_allowed_tools(cmd, allowed_tools)
|
|
116
|
-
cmd.push(
|
|
121
|
+
cmd.push('--allowedTools', allowed_tools.join(',')) unless allowed_tools.empty?
|
|
117
122
|
end
|
|
118
123
|
|
|
119
124
|
# Mirror of Python's _apply_skills_defaults: when skills are requested,
|
|
@@ -130,10 +135,10 @@ module ClaudeAgentSDK
|
|
|
130
135
|
skills = @options.skills
|
|
131
136
|
return [allowed_tools, setting_sources] if skills.nil?
|
|
132
137
|
|
|
133
|
-
valid = skills ==
|
|
138
|
+
valid = skills == 'all' || skills.is_a?(Array)
|
|
134
139
|
raise ArgumentError, "skills must be 'all' or an Array of skill names (got #{skills.inspect})" unless valid
|
|
135
140
|
|
|
136
|
-
entries = skills ==
|
|
141
|
+
entries = skills == 'all' ? ['Skill'] : skills.map { |name| "Skill(#{validate_skill_name(name)})" }
|
|
137
142
|
entries.each { |entry| allowed_tools << entry unless allowed_tools.include?(entry) }
|
|
138
143
|
setting_sources = %w[user project] if setting_sources.nil?
|
|
139
144
|
[allowed_tools, setting_sources]
|
|
@@ -151,7 +156,7 @@ module ClaudeAgentSDK
|
|
|
151
156
|
# rejected too, so a dead rule fails loudly here instead of silently
|
|
152
157
|
# granting nothing. Returns the name as a UTF-8 String so later argv
|
|
153
158
|
# joins cannot raise Encoding::CompatibilityError.
|
|
154
|
-
def validate_skill_name(name) # rubocop:disable Metrics/MethodLength
|
|
159
|
+
def validate_skill_name(name) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity -- one guard per rejected input shape, each with its own message
|
|
155
160
|
raise TypeError, "Skill names must be strings, got #{name.class}: #{name.inspect}" unless name.is_a?(String)
|
|
156
161
|
|
|
157
162
|
# Ruby's analogue of Python's surrogate check: a lone surrogate (or any
|
|
@@ -164,75 +169,77 @@ module ClaudeAgentSDK
|
|
|
164
169
|
end
|
|
165
170
|
if utf8.nil? || !utf8.valid_encoding?
|
|
166
171
|
raise ArgumentError, "Invalid skill name #{name.inspect}: contains bytes that cannot form " \
|
|
167
|
-
|
|
168
|
-
|
|
172
|
+
'valid UTF-8 (such as a surrogate code point), which can never match ' \
|
|
173
|
+
'a skill the CLI discovered.'
|
|
169
174
|
end
|
|
170
175
|
|
|
171
|
-
stripped = utf8.gsub(SKILL_NAME_EDGE_WHITESPACE,
|
|
172
|
-
raise ArgumentError,
|
|
176
|
+
stripped = utf8.gsub(SKILL_NAME_EDGE_WHITESPACE, '')
|
|
177
|
+
raise ArgumentError, 'Skill names must be non-empty strings' if stripped.empty?
|
|
173
178
|
|
|
174
179
|
if utf8 != stripped
|
|
175
180
|
raise ArgumentError, "Invalid skill name #{name.inspect}: leading or trailing whitespace " \
|
|
176
|
-
|
|
181
|
+
'can never match -- the Skill tool trims the invoked name.'
|
|
177
182
|
end
|
|
178
183
|
if SKILL_NAME_INVALID_CHARS.match?(utf8)
|
|
179
184
|
raise ArgumentError, "Invalid skill name #{name.inspect}: parentheses, commas, control " \
|
|
180
|
-
|
|
185
|
+
'characters, and byte-order marks are not allowed. Names match the ' \
|
|
181
186
|
"skill's directory name, or 'plugin:skill' for plugin-qualified skills."
|
|
182
187
|
end
|
|
183
|
-
raise ArgumentError, "Invalid skill name '*': use skills: 'all' to enable every skill." if utf8 ==
|
|
188
|
+
raise ArgumentError, "Invalid skill name '*': use skills: 'all' to enable every skill." if utf8 == '*'
|
|
184
189
|
|
|
185
|
-
if utf8.end_with?(
|
|
190
|
+
if utf8.end_with?(':*', ' *')
|
|
186
191
|
raise ArgumentError, "Invalid skill name #{name.inspect}: wildcard-suffix names are not " \
|
|
187
|
-
|
|
192
|
+
'allowed; list each skill by its exact name.'
|
|
188
193
|
end
|
|
189
|
-
if utf8.start_with?(
|
|
194
|
+
if utf8.start_with?('/')
|
|
190
195
|
raise ArgumentError, "Invalid skill name #{name.inspect}: skill names may not start with " \
|
|
191
196
|
"'/'. The skills option takes the canonical name, not the " \
|
|
192
|
-
|
|
197
|
+
'slash-command form.'
|
|
193
198
|
end
|
|
194
|
-
if utf8.include?(
|
|
199
|
+
if utf8.include?('\\\\')
|
|
195
200
|
raise ArgumentError, "Invalid skill name #{name.inspect}: consecutive backslashes are not " \
|
|
196
|
-
|
|
197
|
-
|
|
201
|
+
'allowed -- the per-rule parser collapses them, so the rule would ' \
|
|
202
|
+
'name a different skill.'
|
|
198
203
|
end
|
|
199
|
-
if utf8.end_with?(
|
|
204
|
+
if utf8.end_with?('\\')
|
|
200
205
|
raise ArgumentError, "Invalid skill name #{name.inspect}: names may not end with an " \
|
|
201
|
-
|
|
206
|
+
'unpaired backslash.'
|
|
202
207
|
end
|
|
203
208
|
|
|
204
209
|
utf8
|
|
205
210
|
end
|
|
206
211
|
|
|
207
212
|
def append_disallowed_tools(cmd)
|
|
208
|
-
cmd.push(
|
|
213
|
+
cmd.push('--disallowedTools', @options.disallowed_tools.join(',')) unless @options.disallowed_tools.empty?
|
|
209
214
|
end
|
|
210
215
|
|
|
211
216
|
def append_max_turns(cmd)
|
|
212
|
-
cmd.push(
|
|
217
|
+
cmd.push('--max-turns', @options.max_turns.to_s) if @options.max_turns
|
|
213
218
|
end
|
|
214
219
|
|
|
215
220
|
def append_model(cmd)
|
|
216
|
-
cmd.push(
|
|
217
|
-
cmd.push(
|
|
221
|
+
cmd.push('--model', @options.model) if @options.model
|
|
222
|
+
cmd.push('--fallback-model', @options.fallback_model) if @options.fallback_model
|
|
218
223
|
# Server-side advisor tool (experimental, Anthropic API only). The CLI
|
|
219
224
|
# validates the main-model/advisor pairing; pairing rules are
|
|
220
225
|
# CLI-version-dependent, so the SDK passes the value through verbatim.
|
|
221
|
-
cmd.push(
|
|
226
|
+
cmd.push('--advisor', @options.advisor_model) if @options.advisor_model
|
|
222
227
|
end
|
|
223
228
|
|
|
224
229
|
def append_permission(cmd)
|
|
225
|
-
cmd.push(
|
|
226
|
-
cmd.push(
|
|
230
|
+
cmd.push('--permission-prompt-tool', @options.permission_prompt_tool_name) if @options.permission_prompt_tool_name
|
|
231
|
+
cmd.push('--permission-mode', @options.permission_mode) if @options.permission_mode
|
|
227
232
|
end
|
|
228
233
|
|
|
229
234
|
def append_session(cmd)
|
|
230
235
|
# `--continue` and `--resume <id>` are mutually exclusive session-restore
|
|
231
236
|
# modes. Passing both surfaces as a generic non-zero CLI exit, which is
|
|
232
237
|
# painful to debug at the caller; raise early in the SDK stack instead.
|
|
233
|
-
|
|
238
|
+
if @options.continue_conversation && @options.resume
|
|
239
|
+
raise ArgumentError, 'continue_conversation and resume are mutually exclusive'
|
|
240
|
+
end
|
|
234
241
|
|
|
235
|
-
cmd.push(
|
|
242
|
+
cmd.push('--continue') if @options.continue_conversation
|
|
236
243
|
# =-joined single tokens: the CLI declares `--resume [value]` with an
|
|
237
244
|
# OPTIONAL value, so in the two-token form a dash-leading value is not
|
|
238
245
|
# bound to the flag and parses as an independent CLI flag — letting an
|
|
@@ -252,7 +259,7 @@ module ClaudeAgentSDK
|
|
|
252
259
|
def append_resume_session_at(cmd)
|
|
253
260
|
return unless @options.resume_session_at
|
|
254
261
|
|
|
255
|
-
raise ArgumentError,
|
|
262
|
+
raise ArgumentError, 'resume_session_at requires resume to be set' unless @options.resume
|
|
256
263
|
|
|
257
264
|
# Equals form for the same reason as --resume above: never let a
|
|
258
265
|
# dash-leading value parse as a separate flag.
|
|
@@ -287,7 +294,7 @@ module ClaudeAgentSDK
|
|
|
287
294
|
# must reach the CLI so it can override a sandbox enabled in the
|
|
288
295
|
# settings JSON (`sandbox: true`, the boolean toggle, used to crash on
|
|
289
296
|
# true.empty?; false and {} were silently dropped).
|
|
290
|
-
def append_settings(cmd)
|
|
297
|
+
def append_settings(cmd) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- folds settings, sandbox and flag overrides into one --settings value
|
|
291
298
|
return unless @options.settings || !@options.sandbox.nil?
|
|
292
299
|
|
|
293
300
|
settings_hash = {}
|
|
@@ -298,9 +305,9 @@ module ClaudeAgentSDK
|
|
|
298
305
|
begin
|
|
299
306
|
settings_hash = JSON.parse(@options.settings)
|
|
300
307
|
rescue JSON::ParserError
|
|
301
|
-
if @options.sandbox.nil?
|
|
308
|
+
if @options.sandbox.nil? # rubocop:disable Metrics/BlockNesting -- settings-is-a-path fallback inside the JSON parse rescue
|
|
302
309
|
settings_is_path = true
|
|
303
|
-
cmd.push(
|
|
310
|
+
cmd.push('--settings', @options.settings)
|
|
304
311
|
else
|
|
305
312
|
settings_hash = load_settings_file(@options.settings)
|
|
306
313
|
end
|
|
@@ -314,20 +321,20 @@ module ClaudeAgentSDK
|
|
|
314
321
|
settings_hash[:sandbox] = @options.sandbox.is_a?(SandboxSettings) ? @options.sandbox.to_h : @options.sandbox
|
|
315
322
|
end
|
|
316
323
|
|
|
317
|
-
cmd.push(
|
|
324
|
+
cmd.push('--settings', JSON.generate(settings_hash)) if !settings_is_path && !settings_hash.empty?
|
|
318
325
|
end
|
|
319
326
|
|
|
320
327
|
def append_budget(cmd)
|
|
321
|
-
cmd.push(
|
|
328
|
+
cmd.push('--max-budget-usd', @options.max_budget_usd.to_s) if @options.max_budget_usd
|
|
322
329
|
|
|
323
330
|
return unless @options.task_budget
|
|
324
331
|
|
|
325
332
|
total = if @options.task_budget.is_a?(TaskBudget)
|
|
326
333
|
@options.task_budget.total
|
|
327
334
|
else
|
|
328
|
-
@options.task_budget[:total] || @options.task_budget[
|
|
335
|
+
@options.task_budget[:total] || @options.task_budget['total']
|
|
329
336
|
end
|
|
330
|
-
cmd.push(
|
|
337
|
+
cmd.push('--task-budget', total.to_s) if total
|
|
331
338
|
end
|
|
332
339
|
|
|
333
340
|
# Thinking configuration takes precedence over deprecated
|
|
@@ -340,21 +347,21 @@ module ClaudeAgentSDK
|
|
|
340
347
|
if @options.thinking
|
|
341
348
|
type, budget, display = thinking_fields(@options.thinking)
|
|
342
349
|
case type
|
|
343
|
-
when
|
|
344
|
-
cmd.push(
|
|
350
|
+
when 'adaptive'
|
|
351
|
+
cmd.push('--thinking', 'adaptive')
|
|
345
352
|
append_thinking_display(cmd, display)
|
|
346
|
-
when
|
|
353
|
+
when 'enabled'
|
|
347
354
|
raise ArgumentError, "thinking type 'enabled' requires budget_tokens" if budget.nil?
|
|
348
355
|
|
|
349
|
-
cmd.push(
|
|
356
|
+
cmd.push('--max-thinking-tokens', budget.to_s)
|
|
350
357
|
append_thinking_display(cmd, display)
|
|
351
|
-
when
|
|
352
|
-
cmd.push(
|
|
358
|
+
when 'disabled'
|
|
359
|
+
cmd.push('--thinking', 'disabled')
|
|
353
360
|
else
|
|
354
361
|
raise ArgumentError, "unsupported thinking config: #{@options.thinking.inspect}"
|
|
355
362
|
end
|
|
356
363
|
elsif @options.max_thinking_tokens
|
|
357
|
-
cmd.push(
|
|
364
|
+
cmd.push('--max-thinking-tokens', @options.max_thinking_tokens.to_s)
|
|
358
365
|
end
|
|
359
366
|
end
|
|
360
367
|
|
|
@@ -363,8 +370,8 @@ module ClaudeAgentSDK
|
|
|
363
370
|
def thinking_fields(thinking)
|
|
364
371
|
case thinking
|
|
365
372
|
when Hash
|
|
366
|
-
type = (thinking[:type] || thinking[
|
|
367
|
-
[type, thinking[:budget_tokens] || thinking[
|
|
373
|
+
type = (thinking[:type] || thinking['type'])&.to_s
|
|
374
|
+
[type, thinking[:budget_tokens] || thinking['budget_tokens'], thinking[:display] || thinking['display']]
|
|
368
375
|
when ThinkingConfigAdaptive then [thinking.type, nil, thinking.display]
|
|
369
376
|
when ThinkingConfigEnabled then [thinking.type, thinking.budget_tokens, thinking.display]
|
|
370
377
|
when ThinkingConfigDisabled then [thinking.type, nil, nil]
|
|
@@ -378,20 +385,20 @@ module ClaudeAgentSDK
|
|
|
378
385
|
def append_thinking_display(cmd, display)
|
|
379
386
|
return if display.nil?
|
|
380
387
|
|
|
381
|
-
cmd.push(
|
|
388
|
+
cmd.push('--thinking-display', display.to_s)
|
|
382
389
|
end
|
|
383
390
|
|
|
384
391
|
# The set of supported levels is model-dependent; the CLI falls back to
|
|
385
392
|
# the highest supported level at or below the one requested
|
|
386
393
|
# (e.g. `xhigh` → `high` on Opus 4.6).
|
|
387
394
|
def append_effort(cmd)
|
|
388
|
-
cmd.push(
|
|
395
|
+
cmd.push('--effort', @options.effort.to_s) if @options.effort
|
|
389
396
|
end
|
|
390
397
|
|
|
391
398
|
def append_betas(cmd)
|
|
392
399
|
return unless @options.betas && !@options.betas.empty?
|
|
393
400
|
|
|
394
|
-
cmd.push(
|
|
401
|
+
cmd.push('--betas', @options.betas.join(','))
|
|
395
402
|
end
|
|
396
403
|
|
|
397
404
|
def append_tools(cmd)
|
|
@@ -399,15 +406,15 @@ module ClaudeAgentSDK
|
|
|
399
406
|
|
|
400
407
|
case @options.tools
|
|
401
408
|
when Array
|
|
402
|
-
tools_value = @options.tools.empty? ?
|
|
403
|
-
cmd.push(
|
|
409
|
+
tools_value = @options.tools.empty? ? '' : @options.tools.join(',')
|
|
410
|
+
cmd.push('--tools', tools_value)
|
|
404
411
|
when ToolsPreset
|
|
405
|
-
cmd.push(
|
|
412
|
+
cmd.push('--tools', 'default')
|
|
406
413
|
when Hash
|
|
407
|
-
if (@options.tools[:type] || @options.tools[
|
|
408
|
-
cmd.push(
|
|
414
|
+
if (@options.tools[:type] || @options.tools['type']) == 'preset'
|
|
415
|
+
cmd.push('--tools', 'default')
|
|
409
416
|
else
|
|
410
|
-
cmd.push(
|
|
417
|
+
cmd.push('--tools', JSON.generate(@options.tools))
|
|
411
418
|
end
|
|
412
419
|
end
|
|
413
420
|
end
|
|
@@ -415,10 +422,10 @@ module ClaudeAgentSDK
|
|
|
415
422
|
def append_output_format(cmd)
|
|
416
423
|
return unless @options.output_format
|
|
417
424
|
|
|
418
|
-
schema = if @options.output_format.is_a?(Hash) && @options.output_format[:type] ==
|
|
425
|
+
schema = if @options.output_format.is_a?(Hash) && @options.output_format[:type] == 'json_schema'
|
|
419
426
|
@options.output_format[:schema]
|
|
420
|
-
elsif @options.output_format.is_a?(Hash) && @options.output_format[
|
|
421
|
-
@options.output_format[
|
|
427
|
+
elsif @options.output_format.is_a?(Hash) && @options.output_format['type'] == 'json_schema'
|
|
428
|
+
@options.output_format['schema']
|
|
422
429
|
else
|
|
423
430
|
@options.output_format
|
|
424
431
|
end
|
|
@@ -428,11 +435,11 @@ module ClaudeAgentSDK
|
|
|
428
435
|
return if schema.nil?
|
|
429
436
|
|
|
430
437
|
schema_json = schema.is_a?(String) ? schema : JSON.generate(schema)
|
|
431
|
-
cmd.push(
|
|
438
|
+
cmd.push('--json-schema', schema_json)
|
|
432
439
|
end
|
|
433
440
|
|
|
434
441
|
def append_additional_dirs(cmd)
|
|
435
|
-
@options.add_dirs.each { |dir| cmd.push(
|
|
442
|
+
@options.add_dirs.each { |dir| cmd.push('--add-dir', dir.to_s) }
|
|
436
443
|
end
|
|
437
444
|
|
|
438
445
|
def append_mcp_servers(cmd)
|
|
@@ -448,28 +455,28 @@ module ClaudeAgentSDK
|
|
|
448
455
|
# either key style (and a Symbol :sdk type). The live instance is
|
|
449
456
|
# never serialized — JSON.generate would raise on it or leak its
|
|
450
457
|
# #to_s onto the command line.
|
|
451
|
-
servers_for_cli[name] = if config.is_a?(Hash) && (config[:type] || config[
|
|
452
|
-
config.except(:instance,
|
|
458
|
+
servers_for_cli[name] = if config.is_a?(Hash) && (config[:type] || config['type']).to_s == 'sdk'
|
|
459
|
+
config.except(:instance, 'instance')
|
|
453
460
|
else
|
|
454
461
|
config
|
|
455
462
|
end
|
|
456
463
|
end
|
|
457
|
-
cmd.push(
|
|
464
|
+
cmd.push('--mcp-config', JSON.generate({ mcpServers: servers_for_cli })) unless servers_for_cli.empty?
|
|
458
465
|
else
|
|
459
|
-
cmd.push(
|
|
466
|
+
cmd.push('--mcp-config', @options.mcp_servers.to_s)
|
|
460
467
|
end
|
|
461
468
|
end
|
|
462
469
|
|
|
463
470
|
# NOTE: agents are sent via the initialize control request (not CLI args)
|
|
464
471
|
# to avoid OS ARG_MAX limits with large agent configurations.
|
|
465
472
|
def append_boolean_flags(cmd)
|
|
466
|
-
cmd.push(
|
|
467
|
-
cmd.push(
|
|
468
|
-
cmd.push(
|
|
469
|
-
cmd.push(
|
|
470
|
-
cmd.push(
|
|
473
|
+
cmd.push('--include-partial-messages') if @options.include_partial_messages
|
|
474
|
+
cmd.push('--fork-session') if @options.fork_session
|
|
475
|
+
cmd.push('--bare') if @options.bare
|
|
476
|
+
cmd.push('--include-hook-events') if @options.include_hook_events
|
|
477
|
+
cmd.push('--strict-mcp-config') if @options.strict_mcp_config
|
|
471
478
|
# When a session_store is set, ask the CLI to emit transcript_mirror frames.
|
|
472
|
-
cmd.push(
|
|
479
|
+
cmd.push('--session-mirror') if @options.session_store
|
|
473
480
|
end
|
|
474
481
|
|
|
475
482
|
def append_plugins(cmd)
|
|
@@ -477,29 +484,33 @@ module ClaudeAgentSDK
|
|
|
477
484
|
|
|
478
485
|
@options.plugins.each do |plugin|
|
|
479
486
|
plugin_config = plugin.is_a?(SdkPluginConfig) ? plugin.to_h : plugin
|
|
480
|
-
plugin_type = plugin_config[:type] || plugin_config[
|
|
481
|
-
plugin_path = plugin_config[:path] || plugin_config[
|
|
487
|
+
plugin_type = plugin_config[:type] || plugin_config['type']
|
|
488
|
+
plugin_path = plugin_config[:path] || plugin_config['path']
|
|
482
489
|
|
|
483
|
-
|
|
490
|
+
unless %w[local plugin].include?(plugin_type)
|
|
491
|
+
raise ArgumentError, "Unsupported plugin type: #{plugin_type.inspect}"
|
|
492
|
+
end
|
|
484
493
|
next unless plugin_path
|
|
485
494
|
|
|
486
|
-
cmd.push(
|
|
495
|
+
cmd.push('--plugin-dir', plugin_path)
|
|
487
496
|
end
|
|
488
497
|
end
|
|
489
498
|
|
|
490
499
|
def append_setting_sources(cmd, setting_sources)
|
|
491
500
|
return if setting_sources.nil?
|
|
492
501
|
|
|
493
|
-
cmd.push(
|
|
502
|
+
cmd.push('--setting-sources', setting_sources.join(','))
|
|
494
503
|
end
|
|
495
504
|
|
|
496
505
|
def append_extra_args(cmd)
|
|
497
506
|
@options.extra_args.each do |flag, value|
|
|
498
|
-
|
|
507
|
+
unless EXTRA_ARG_FLAG_REGEXP.match?(flag)
|
|
508
|
+
raise ArgumentError, "Invalid extra_args flag name: #{flag.inspect} (expected lowercase kebab-case)"
|
|
509
|
+
end
|
|
499
510
|
|
|
500
511
|
if value.nil?
|
|
501
512
|
cmd.push("--#{flag}")
|
|
502
|
-
elsif value.to_s.start_with?(
|
|
513
|
+
elsif value.to_s.start_with?('-')
|
|
503
514
|
# A dash-leading value must bind via `=` or the CLI parses it as a
|
|
504
515
|
# separate flag — same injection class as --resume above.
|
|
505
516
|
cmd.push("--#{flag}=#{value}")
|
|
@@ -42,10 +42,49 @@ module ClaudeAgentSDK
|
|
|
42
42
|
end
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
# Warn +message+ once per process per +key+, attributed to the first
|
|
46
|
+
# caller frame outside the SDK's lib/ directory: the user's call site,
|
|
47
|
+
# however deep inside the SDK the deprecated behaviour is detected
|
|
48
|
+
# (e.g. an unknown attribute found by Type#assign_attribute during
|
|
49
|
+
# HookMatcher.new). Best-effort like #warn_once.
|
|
50
|
+
#
|
|
51
|
+
# @param key [Object] once-guard key; any value usable in a Set
|
|
52
|
+
# @param message [String]
|
|
53
|
+
# @return [void]
|
|
54
|
+
def warn_once_at_caller(key, message)
|
|
55
|
+
first = @mutex.synchronize { @warned.add?(key) }
|
|
56
|
+
return unless first
|
|
57
|
+
|
|
58
|
+
begin
|
|
59
|
+
locations = caller_locations(1)
|
|
60
|
+
index = locations.index { |location| !sdk_frame?(location) }
|
|
61
|
+
index ? warn(message, uplevel: index + 1) : warn(message)
|
|
62
|
+
rescue StandardError
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
45
67
|
# Test hook: forget which deprecations were already reported.
|
|
46
68
|
def reset!
|
|
47
69
|
@mutex.synchronize { @warned.clear }
|
|
48
70
|
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# A frame inside the gem's lib/ (both the loaded and the real path, in
|
|
75
|
+
# case lib/ is reached through a symlink), or a Ruby-internal one
|
|
76
|
+
# (<internal:...>, e.g. Array#each on 3.4). A C frame such as Class#new
|
|
77
|
+
# reports its caller's path, so it counts as the caller's.
|
|
78
|
+
def sdk_frame?(location)
|
|
79
|
+
path = location.absolute_path || location.path
|
|
80
|
+
return true if path.nil? || path.start_with?('<internal:')
|
|
81
|
+
|
|
82
|
+
SDK_LIB_DIRS.any? { |dir| path.start_with?(dir) }
|
|
83
|
+
end
|
|
49
84
|
end
|
|
85
|
+
|
|
86
|
+
SDK_LIB_DIRS = [File.expand_path('..', File.dirname(__FILE__)), File.expand_path('..', __dir__)]
|
|
87
|
+
.uniq.map { |dir| "#{dir}/" }.freeze
|
|
88
|
+
private_constant :SDK_LIB_DIRS
|
|
50
89
|
end
|
|
51
90
|
end
|
|
@@ -71,6 +71,8 @@ module ClaudeAgentSDK
|
|
|
71
71
|
# scheduler-aware and park only the stream task; CPU-bound or
|
|
72
72
|
# scheduler-opaque work must be moved by the user (a producer Thread
|
|
73
73
|
# feeding a Thread::Queue, or FiberBoundary.invoke inside the enumerator).
|
|
74
|
+
#
|
|
75
|
+
# @api private
|
|
74
76
|
module FiberBoundary
|
|
75
77
|
# Raised by .invoke when a timeout-bounded call exceeds its allotted time.
|
|
76
78
|
# The worker thread is abandoned (cancellation is best-effort; the
|
|
@@ -34,7 +34,7 @@ module ClaudeAgentSDK
|
|
|
34
34
|
# observer = ClaudeAgentSDK::Instrumentation::OTelObserver.new
|
|
35
35
|
# options = ClaudeAgentSDK::ClaudeAgentOptions.new(observers: [observer])
|
|
36
36
|
# ClaudeAgentSDK.query(prompt: "Hello", options: options) { |msg| ... }
|
|
37
|
-
class OTelObserver
|
|
37
|
+
class OTelObserver # rubocop:disable Metrics/ClassLength -- one observer mapping every message type to spans
|
|
38
38
|
include ClaudeAgentSDK::Observer
|
|
39
39
|
|
|
40
40
|
TRACER_NAME = 'claude_agent_sdk'
|
|
@@ -124,7 +124,7 @@ module ClaudeAgentSDK
|
|
|
124
124
|
|
|
125
125
|
private
|
|
126
126
|
|
|
127
|
-
def start_trace(message)
|
|
127
|
+
def start_trace(message) # rubocop:disable Metrics/AbcSize -- flat mapping of init-message fields to root span attributes
|
|
128
128
|
# A new init without an intervening ResultMessage (e.g. /clear or an
|
|
129
129
|
# interrupted turn) supersedes the current trace; finish it so it is
|
|
130
130
|
# exported instead of leaking as a never-ended span, and reset the
|
|
@@ -157,15 +157,21 @@ module ClaudeAgentSDK
|
|
|
157
157
|
'session.id' => message.session_id
|
|
158
158
|
}.merge(@default_attributes)
|
|
159
159
|
|
|
160
|
-
|
|
160
|
+
if message.respond_to?(:claude_code_version) && message.claude_code_version
|
|
161
|
+
attrs['claude_code.version'] = message.claude_code_version
|
|
162
|
+
end
|
|
161
163
|
attrs['claude_code.cwd'] = message.cwd if message.respond_to?(:cwd) && message.cwd
|
|
162
|
-
|
|
164
|
+
if message.respond_to?(:permission_mode) && message.permission_mode
|
|
165
|
+
attrs['claude_code.permission_mode'] = message.permission_mode
|
|
166
|
+
end
|
|
163
167
|
|
|
164
168
|
@root_span = @tracer.start_span('claude_agent.session', attributes: compact_attrs(attrs))
|
|
165
169
|
@root_context = OpenTelemetry::Trace.context_with_span(@root_span)
|
|
166
170
|
|
|
167
171
|
# Apply buffered prompt if on_user_prompt was called before InitMessage arrived
|
|
168
|
-
|
|
172
|
+
return unless @first_user_input && !@first_user_input.empty?
|
|
173
|
+
|
|
174
|
+
@root_span.set_attribute('input.value', truncate(@first_user_input))
|
|
169
175
|
end
|
|
170
176
|
|
|
171
177
|
def handle_assistant(message)
|
|
@@ -222,7 +228,7 @@ module ClaudeAgentSDK
|
|
|
222
228
|
end
|
|
223
229
|
end
|
|
224
230
|
|
|
225
|
-
def end_trace(message)
|
|
231
|
+
def end_trace(message) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- flat mapping of optional usage/cost fields to span attributes
|
|
226
232
|
return unless @root_span
|
|
227
233
|
|
|
228
234
|
usage = message.usage || {}
|
|
@@ -235,7 +241,9 @@ module ClaudeAgentSDK
|
|
|
235
241
|
# tokens (Anthropic's input_tokens excludes them; OpenInference's own
|
|
236
242
|
# Anthropic instrumentation sums them in). gen_ai.usage.* keys keep
|
|
237
243
|
# the raw exclusive values — Langfuse prices those additively.
|
|
238
|
-
|
|
244
|
+
if input_tokens || cache_creation_tokens || cache_read_tokens
|
|
245
|
+
prompt_tokens = (input_tokens || 0) + (cache_creation_tokens || 0) + (cache_read_tokens || 0)
|
|
246
|
+
end
|
|
239
247
|
total_tokens = (prompt_tokens || 0) + (output_tokens || 0) if prompt_tokens || output_tokens
|
|
240
248
|
|
|
241
249
|
# Set trace output (last assistant response — shown in Langfuse UI)
|