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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0830ea985ad6a281cbdf19d50a13e4309b45d900c0024bd4e1b28949afeb0587'
4
- data.tar.gz: fa9946c463c428d9c3128a1e8acd010eccbda18760a1993e9e12ced8da2d56a9
3
+ metadata.gz: a02840b42d191cb45136765380af25f17cc9c5a561da2dcb958ddd27a2552a67
4
+ data.tar.gz: 85a0b37471518912e94ea4c84285a334abc44bac507c6121fa355fdacd8f7911
5
5
  SHA512:
6
- metadata.gz: 36cb49cbd769299019cf24098fb7ad4d81a4fe78a750e77b7b17ea90c4f57e66b6b6f344b7bde4ad04cfe048cb55d6420f3a004200ba0029063da1225240cb74
7
- data.tar.gz: 84c081d5d2639e395e87927b28a3187584d9cd9323282d49dc2744fa530a579426f5393f883cc763a5b8772679eed2152608e8ad0508641d8b9145291f1e0a93
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
- case @options.system_prompt
73
- when nil
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 String
77
- cmd.push('--system-prompt', @options.system_prompt)
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(@options.system_prompt.prompt))
84
- when SystemPromptPreset
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', @options.system_prompt.append) if @options.system_prompt.append
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] = sandbox_section(@options.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
- return unless @options.task_budget
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
- type, budget, display = thinking_fields(@options.thinking)
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 budget.nil?
357
+ raise ArgumentError, "thinking type 'enabled' requires budget_tokens" if thinking.budget_tokens.nil?
399
358
 
400
- cmd.push('--max-thinking-tokens', budget.to_s)
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
- case @options.tools
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 = @options.tools.empty? ? '' : @options.tools.join(',')
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', @options.tools)
458
- when ToolsPreset
459
- cmd.push('--tools', 'default')
407
+ cmd.push('--tools', tools)
460
408
  when Hash
461
- if hash_type(@options.tools) == 'preset'
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
- if @options.mcp_servers.is_a?(Hash)
506
- servers_for_cli = {}
507
- @options.mcp_servers.each do |name, config|
508
- # Typed Mcp*ServerConfig objects serialize via their wire hash —
509
- # without this they'd JSON-stringify as "#<...>" via to_s.
510
- config = config.to_h if config.is_a?(Type)
511
- # Same recognition rule as ClaudeAgentSDK.extract_sdk_mcp_servers:
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', @options.mcp_servers.to_s)
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 |plugin|
543
- plugin_config = plugin.is_a?(SdkPluginConfig) ? plugin.to_h : plugin
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?(hash_type(plugin_config))
547
- plugin_type = plugin_config[:type] || plugin_config['type']
548
- raise ArgumentError, "Unsupported plugin type: #{plugin_type.inspect}"
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 plugin_path
468
+ next unless plugin.path
551
469
 
552
- cmd.push('--plugin-dir', path_string(plugin_path))
470
+ cmd.push('--plugin-dir', path_string(plugin.path))
553
471
  end
554
472
  end
555
473