claude-agent-sdk 1.2.0 → 1.2.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 +9 -0
- 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 +47 -296
- 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/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +163 -406
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a02840b42d191cb45136765380af25f17cc9c5a561da2dcb958ddd27a2552a67
|
|
4
|
+
data.tar.gz: 85a0b37471518912e94ea4c84285a334abc44bac507c6121fa355fdacd8f7911
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 37b772dfad73d47a161abc14490eb4a8dbf0e95f30c69db4fa20d04fdee289ea534a74252fe054ebda39729b117e9a6898ef71f8078083372f6ad65108b0e9e8
|
|
7
|
+
data.tar.gz: f618e4e8a3e94b2b87a0b878c9a0587bc0348643a37a124ec34764735babf08a37189738a8b318e53d91d6ccab48a125ab906b7d435926b768ea6ea785cbae53
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.2.1] - 2026-10-06
|
|
11
|
+
|
|
12
|
+
An internal release: three refactors that move duplicated or scattered logic into one private module each. No public API, signature (`sig/`) or documented behavior changes; one cleanup difference is noted below.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- `ClaudeAgentSDK.query` and `Client` share one implementation of session setup, prompt writes, message delivery and teardown (`SessionAssembly`, `@api private`); a session's observers, `callback_scheduling` and `callback_wrapper` travel in one object (`Dispatch`, `@api private`) instead of being passed at every call site. The documented differences between the two entry points (what observers are told when a session cannot be opened, the `session_id` on the wire, when stdin is closed, which error wins when a close raises) are preserved and now pinned by specs that run both side by side. One cleanup difference: when a `Client` is reconnected while its previous `disconnect` is still removing the materialized directory of a store-backed resume, the finishing `disconnect` no longer clears the new session, so the new session's resume directory is removed by its own `disconnect` (it used to be left behind). (#169)
|
|
16
|
+
- The decision of when `query()` may close stdin lives in one private class, `Query::RunLifecycle` (the per-run end, the task ledger, the session state and the ceiling timer, with the orderings between them); `Query` feeds it events and supplies the timer. Its specs go through that interface instead of `Query`'s instance variables. (#168)
|
|
17
|
+
- Hash-form option interpretation (`system_prompt`, `thinking`, `tools`, `output_format`, `task_budget`, `plugins`, `sandbox`, `mcp_servers`, `agents`) lives in one private module, `OptionForms`; every existing per-option spelling and key policy is preserved as it was. (#167)
|
|
18
|
+
|
|
10
19
|
## [1.2.0] - 2026-10-03
|
|
11
20
|
|
|
12
21
|
Fixes from a review of the whole SDK, a reference for every option (`docs/options.md`), and Claude Code 2.1.288 as the pinned CLI. Settings the SDK used to drop without an error now take effect — hook outputs and `sandbox:` Hashes in Ruby spelling, a `tools:` String, option Hashes with a Symbol `type` — so read the entries in bold before upgrading: they say what changes for code that relied on the old behavior.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require_relative 'errors'
|
|
5
5
|
require_relative 'types'
|
|
6
|
+
require_relative 'option_forms'
|
|
6
7
|
|
|
7
8
|
module ClaudeAgentSDK
|
|
8
9
|
# Builds the CLI argv array from a ClaudeAgentOptions instance.
|
|
@@ -68,57 +69,34 @@ module ClaudeAgentSDK
|
|
|
68
69
|
|
|
69
70
|
private
|
|
70
71
|
|
|
72
|
+
# What the option stands for, typed or as a Hash, is OptionForms' to say;
|
|
73
|
+
# a kind that is none of these (a preset without an append, a Hash the
|
|
74
|
+
# SDK does not recognize, ...) sends no prompt flag at all.
|
|
71
75
|
def append_system_prompt(cmd)
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
prompt = OptionForms.system_prompt(@options.system_prompt)
|
|
77
|
+
case prompt.kind
|
|
78
|
+
when :empty
|
|
74
79
|
# When nil, pass empty string to ensure predictable behavior without default Claude Code system prompt
|
|
75
80
|
cmd.push('--system-prompt', '')
|
|
76
|
-
when
|
|
77
|
-
|
|
78
|
-
when SystemPromptFile
|
|
79
|
-
cmd.push('--system-prompt-file', path_string(@options.system_prompt.path))
|
|
80
|
-
when SystemPromptCustom
|
|
81
|
-
# The object form of a String prompt; snapshot travels on the
|
|
81
|
+
when :text
|
|
82
|
+
# A String, or its object form (custom); snapshot travels on the
|
|
82
83
|
# initialize request, not as a CLI flag.
|
|
83
|
-
cmd.push('--system-prompt', custom_prompt_text(
|
|
84
|
-
when
|
|
84
|
+
cmd.push('--system-prompt', custom_prompt_text(prompt.value))
|
|
85
|
+
when :file
|
|
86
|
+
cmd.push('--system-prompt-file', path_string(prompt.value))
|
|
87
|
+
when :append
|
|
85
88
|
# Preset activates the default Claude Code system prompt by not passing --system-prompt ""
|
|
86
89
|
# Only --append-system-prompt is passed if append text is provided
|
|
87
|
-
cmd.push('--append-system-prompt',
|
|
88
|
-
when Hash
|
|
89
|
-
append_hash_system_prompt(cmd, @options.system_prompt)
|
|
90
|
+
cmd.push('--append-system-prompt', prompt.value)
|
|
90
91
|
end
|
|
91
92
|
end
|
|
92
93
|
|
|
93
|
-
# The type tag of a Hash option, as a String: `type: :preset` is the
|
|
94
|
-
# natural Ruby spelling of `type: 'preset'`, and thinking and the MCP
|
|
95
|
-
# server configs already read it that way. A missing tag is '', which
|
|
96
|
-
# matches no branch, like any other tag the SDK does not know.
|
|
97
|
-
def hash_type(hash)
|
|
98
|
-
(hash[:type] || hash['type']).to_s
|
|
99
|
-
end
|
|
100
|
-
|
|
101
94
|
# A path as the String the command line takes. A Pathname (anything that
|
|
102
95
|
# answers #to_path) is converted; every other value is returned as it is.
|
|
103
96
|
def path_string(path)
|
|
104
97
|
path.respond_to?(:to_path) ? path.to_path : path
|
|
105
98
|
end
|
|
106
99
|
|
|
107
|
-
def append_hash_system_prompt(cmd, prompt_hash)
|
|
108
|
-
case hash_type(prompt_hash)
|
|
109
|
-
when 'file'
|
|
110
|
-
prompt_path = prompt_hash[:path] || prompt_hash['path']
|
|
111
|
-
cmd.push('--system-prompt-file', path_string(prompt_path)) if prompt_path
|
|
112
|
-
when 'custom'
|
|
113
|
-
prompt = prompt_hash.fetch(:prompt) { prompt_hash['prompt'] }
|
|
114
|
-
cmd.push('--system-prompt', custom_prompt_text(prompt))
|
|
115
|
-
when 'preset'
|
|
116
|
-
append = prompt_hash[:append] || prompt_hash['append']
|
|
117
|
-
# Preset activates the default Claude Code system prompt by not passing --system-prompt ""
|
|
118
|
-
cmd.push('--append-system-prompt', append) if append
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
|
|
122
100
|
# A custom prompt is always forwarded, even when empty (an empty String
|
|
123
101
|
# suppresses the default Claude Code prompt, exactly like a nil
|
|
124
102
|
# system_prompt). A missing prompt is rejected loudly rather than
|
|
@@ -338,7 +316,7 @@ module ClaudeAgentSDK
|
|
|
338
316
|
# read from JSON spell that key as a String; left next to the Symbol
|
|
339
317
|
# key below it would be written twice (json 3.x raises on that).
|
|
340
318
|
settings_hash = settings_hash.reject { |key, _| key.to_s == 'sandbox' }
|
|
341
|
-
settings_hash[:sandbox] =
|
|
319
|
+
settings_hash[:sandbox] = OptionForms.sandbox(@options.sandbox)
|
|
342
320
|
end
|
|
343
321
|
|
|
344
322
|
cmd.push('--settings', JSON.generate(settings_hash)) if !settings_is_path && !settings_hash.empty?
|
|
@@ -355,29 +333,10 @@ module ClaudeAgentSDK
|
|
|
355
333
|
[{}, true]
|
|
356
334
|
end
|
|
357
335
|
|
|
358
|
-
# The sandbox section as the CLI reads it. A Hash stands for the
|
|
359
|
-
# SandboxSettings with the same fields: the CLI only knows the camelCase
|
|
360
|
-
# keys that class writes, and it ignores the others without an error, so
|
|
361
|
-
# a Hash in Ruby spelling (deny_read, denied_domains) is renamed like the
|
|
362
|
-
# typed value would be (SandboxKeys). Booleans go out as they are.
|
|
363
|
-
def sandbox_section(sandbox)
|
|
364
|
-
case sandbox
|
|
365
|
-
when SandboxSettings then sandbox.to_h
|
|
366
|
-
when Hash then SandboxKeys.normalize(sandbox)
|
|
367
|
-
else sandbox
|
|
368
|
-
end
|
|
369
|
-
end
|
|
370
|
-
|
|
371
336
|
def append_budget(cmd)
|
|
372
337
|
cmd.push('--max-budget-usd', @options.max_budget_usd.to_s) if @options.max_budget_usd
|
|
373
338
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
total = if @options.task_budget.is_a?(TaskBudget)
|
|
377
|
-
@options.task_budget.total
|
|
378
|
-
else
|
|
379
|
-
@options.task_budget[:total] || @options.task_budget['total']
|
|
380
|
-
end
|
|
339
|
+
total = OptionForms.task_budget_total(@options.task_budget)
|
|
381
340
|
cmd.push('--task-budget', total.to_s) if total
|
|
382
341
|
end
|
|
383
342
|
|
|
@@ -389,16 +348,16 @@ module ClaudeAgentSDK
|
|
|
389
348
|
# max_thinking_tokens fallback.
|
|
390
349
|
def append_thinking(cmd)
|
|
391
350
|
if @options.thinking
|
|
392
|
-
|
|
393
|
-
case type
|
|
351
|
+
thinking = OptionForms.thinking(@options.thinking)
|
|
352
|
+
case thinking.type
|
|
394
353
|
when 'adaptive'
|
|
395
354
|
cmd.push('--thinking', 'adaptive')
|
|
396
|
-
append_thinking_display(cmd, display)
|
|
355
|
+
append_thinking_display(cmd, thinking.display)
|
|
397
356
|
when 'enabled'
|
|
398
|
-
raise ArgumentError, "thinking type 'enabled' requires budget_tokens" if
|
|
357
|
+
raise ArgumentError, "thinking type 'enabled' requires budget_tokens" if thinking.budget_tokens.nil?
|
|
399
358
|
|
|
400
|
-
cmd.push('--max-thinking-tokens',
|
|
401
|
-
append_thinking_display(cmd, display)
|
|
359
|
+
cmd.push('--max-thinking-tokens', thinking.budget_tokens.to_s)
|
|
360
|
+
append_thinking_display(cmd, thinking.display)
|
|
402
361
|
when 'disabled'
|
|
403
362
|
cmd.push('--thinking', 'disabled')
|
|
404
363
|
else
|
|
@@ -409,20 +368,6 @@ module ClaudeAgentSDK
|
|
|
409
368
|
end
|
|
410
369
|
end
|
|
411
370
|
|
|
412
|
-
# Explicit class dispatch — never respond_to? probes (Kernel#display
|
|
413
|
-
# exists on every object and PRINTS the receiver to $stdout).
|
|
414
|
-
def thinking_fields(thinking)
|
|
415
|
-
case thinking
|
|
416
|
-
when Hash
|
|
417
|
-
type = (thinking[:type] || thinking['type'])&.to_s
|
|
418
|
-
[type, thinking[:budget_tokens] || thinking['budget_tokens'], thinking[:display] || thinking['display']]
|
|
419
|
-
when ThinkingConfigAdaptive then [thinking.type, nil, thinking.display]
|
|
420
|
-
when ThinkingConfigEnabled then [thinking.type, thinking.budget_tokens, thinking.display]
|
|
421
|
-
when ThinkingConfigDisabled then [thinking.type, nil, nil]
|
|
422
|
-
else [nil, nil, nil] # falls into append_thinking's else -> ArgumentError
|
|
423
|
-
end
|
|
424
|
-
end
|
|
425
|
-
|
|
426
371
|
# `--thinking-display` toggles between `"summarized"` (visible thinking
|
|
427
372
|
# text) and `"omitted"` (empty thinking, signature only). Current models default
|
|
428
373
|
# to `"omitted"`, so pass `display: "summarized"` to see reasoning.
|
|
@@ -448,28 +393,27 @@ module ClaudeAgentSDK
|
|
|
448
393
|
def append_tools(cmd)
|
|
449
394
|
return if @options.tools.nil?
|
|
450
395
|
|
|
451
|
-
|
|
396
|
+
# The preset, typed or as a Hash, is OptionForms' to recognize; every
|
|
397
|
+
# other value comes back as it was given and is only encoded here.
|
|
398
|
+
tools = OptionForms.tools(@options.tools)
|
|
399
|
+
case tools
|
|
400
|
+
when OptionForms::DEFAULT_TOOLS
|
|
401
|
+
cmd.push('--tools', 'default')
|
|
452
402
|
when Array
|
|
453
|
-
tools_value =
|
|
403
|
+
tools_value = tools.empty? ? '' : tools.join(',')
|
|
454
404
|
cmd.push('--tools', tools_value)
|
|
455
405
|
when String
|
|
456
406
|
# The CLI's own syntax ("Read,Grep", "default", ""): passed as written.
|
|
457
|
-
cmd.push('--tools',
|
|
458
|
-
when ToolsPreset
|
|
459
|
-
cmd.push('--tools', 'default')
|
|
407
|
+
cmd.push('--tools', tools)
|
|
460
408
|
when Hash
|
|
461
|
-
|
|
462
|
-
cmd.push('--tools', 'default')
|
|
463
|
-
else
|
|
464
|
-
cmd.push('--tools', JSON.generate(@options.tools))
|
|
465
|
-
end
|
|
409
|
+
cmd.push('--tools', JSON.generate(tools))
|
|
466
410
|
end
|
|
467
411
|
end
|
|
468
412
|
|
|
469
413
|
def append_output_format(cmd)
|
|
470
414
|
return unless @options.output_format
|
|
471
415
|
|
|
472
|
-
schema = output_schema(@options.output_format)
|
|
416
|
+
schema = OptionForms.output_schema(@options.output_format)
|
|
473
417
|
# A json_schema output_format with a nil/absent schema must skip the
|
|
474
418
|
# flag — `--json-schema null` is rejected by the CLI (Python guards
|
|
475
419
|
# `schema is not None`).
|
|
@@ -479,22 +423,6 @@ module ClaudeAgentSDK
|
|
|
479
423
|
cmd.push('--json-schema', schema_json)
|
|
480
424
|
end
|
|
481
425
|
|
|
482
|
-
# The schema of a { type: 'json_schema', schema: ... } output format; any
|
|
483
|
-
# other value is the schema itself. The tag may be a Symbol, and `schema`
|
|
484
|
-
# is read under the key style `type` was written in, or under the other
|
|
485
|
-
# one when that key is absent ({ 'type' => 'json_schema', schema: {...} }).
|
|
486
|
-
def output_schema(format)
|
|
487
|
-
return format unless format.is_a?(Hash)
|
|
488
|
-
|
|
489
|
-
if format[:type].to_s == 'json_schema'
|
|
490
|
-
format.fetch(:schema) { format['schema'] }
|
|
491
|
-
elsif format['type'].to_s == 'json_schema'
|
|
492
|
-
format.fetch('schema') { format[:schema] }
|
|
493
|
-
else
|
|
494
|
-
format
|
|
495
|
-
end
|
|
496
|
-
end
|
|
497
|
-
|
|
498
426
|
def append_additional_dirs(cmd)
|
|
499
427
|
(@options.add_dirs || []).each { |dir| cmd.push('--add-dir', dir.to_s) }
|
|
500
428
|
end
|
|
@@ -502,25 +430,15 @@ module ClaudeAgentSDK
|
|
|
502
430
|
def append_mcp_servers(cmd)
|
|
503
431
|
return unless @options.mcp_servers && !@options.mcp_servers.empty?
|
|
504
432
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
# either key style (and a Symbol :sdk type). The live instance is
|
|
513
|
-
# never serialized — JSON.generate would raise on it or leak its
|
|
514
|
-
# #to_s onto the command line.
|
|
515
|
-
servers_for_cli[name] = if config.is_a?(Hash) && (config[:type] || config['type']).to_s == 'sdk'
|
|
516
|
-
config.except(:instance, 'instance')
|
|
517
|
-
else
|
|
518
|
-
config
|
|
519
|
-
end
|
|
520
|
-
end
|
|
521
|
-
cmd.push('--mcp-config', JSON.generate({ mcpServers: servers_for_cli })) unless servers_for_cli.empty?
|
|
433
|
+
# A Hash of servers comes back with typed configs as their wire Hashes
|
|
434
|
+
# and without the live instance of an SDK server, which is never
|
|
435
|
+
# serialized: JSON.generate would raise on it or leak its #to_s onto
|
|
436
|
+
# the command line. Any other value is a path or JSON text.
|
|
437
|
+
servers = OptionForms.mcp_servers(@options.mcp_servers)
|
|
438
|
+
if servers.is_a?(Hash)
|
|
439
|
+
cmd.push('--mcp-config', JSON.generate({ mcpServers: servers })) unless servers.empty?
|
|
522
440
|
else
|
|
523
|
-
cmd.push('--mcp-config',
|
|
441
|
+
cmd.push('--mcp-config', servers.to_s)
|
|
524
442
|
end
|
|
525
443
|
end
|
|
526
444
|
|
|
@@ -539,17 +457,17 @@ module ClaudeAgentSDK
|
|
|
539
457
|
def append_plugins(cmd)
|
|
540
458
|
return unless @options.plugins && !@options.plugins.empty?
|
|
541
459
|
|
|
542
|
-
@options.plugins.each do |
|
|
543
|
-
|
|
544
|
-
plugin_path = plugin_config[:path] || plugin_config['path']
|
|
460
|
+
@options.plugins.each do |entry|
|
|
461
|
+
plugin = OptionForms.plugin(entry)
|
|
545
462
|
|
|
546
|
-
unless %w[local plugin].include?(
|
|
547
|
-
|
|
548
|
-
|
|
463
|
+
unless %w[local plugin].include?(plugin.type_tag)
|
|
464
|
+
# raw_type looks the type up again, as it was written; asked for
|
|
465
|
+
# only here, so an accepted plugin has its type read once.
|
|
466
|
+
raise ArgumentError, "Unsupported plugin type: #{plugin.raw_type.inspect}"
|
|
549
467
|
end
|
|
550
|
-
next unless
|
|
468
|
+
next unless plugin.path
|
|
551
469
|
|
|
552
|
-
cmd.push('--plugin-dir', path_string(
|
|
470
|
+
cmd.push('--plugin-dir', path_string(plugin.path))
|
|
553
471
|
end
|
|
554
472
|
end
|
|
555
473
|
|