claude_agent 0.7.12 → 0.7.13

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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/rules/testing.md +51 -10
  3. data/.claude/settings.json +1 -0
  4. data/ARCHITECTURE.md +237 -0
  5. data/CHANGELOG.md +45 -0
  6. data/CLAUDE.md +2 -0
  7. data/README.md +46 -1
  8. data/Rakefile +17 -0
  9. data/SPEC.md +214 -125
  10. data/lib/claude_agent/client/commands.rb +225 -0
  11. data/lib/claude_agent/client.rb +4 -204
  12. data/lib/claude_agent/content_blocks/generic_block.rb +39 -0
  13. data/lib/claude_agent/content_blocks/image_content_block.rb +54 -0
  14. data/lib/claude_agent/content_blocks/server_tool_result_block.rb +22 -0
  15. data/lib/claude_agent/content_blocks/server_tool_use_block.rb +48 -0
  16. data/lib/claude_agent/content_blocks/text_block.rb +19 -0
  17. data/lib/claude_agent/content_blocks/thinking_block.rb +19 -0
  18. data/lib/claude_agent/content_blocks/tool_result_block.rb +25 -0
  19. data/lib/claude_agent/content_blocks/tool_use_block.rb +134 -0
  20. data/lib/claude_agent/content_blocks.rb +8 -335
  21. data/lib/claude_agent/control_protocol/commands.rb +304 -0
  22. data/lib/claude_agent/control_protocol/lifecycle.rb +113 -0
  23. data/lib/claude_agent/control_protocol/messaging.rb +166 -0
  24. data/lib/claude_agent/control_protocol/primitives.rb +168 -0
  25. data/lib/claude_agent/control_protocol/request_handling.rb +231 -0
  26. data/lib/claude_agent/control_protocol.rb +27 -882
  27. data/lib/claude_agent/event_handler.rb +1 -0
  28. data/lib/claude_agent/get_session_info.rb +86 -0
  29. data/lib/claude_agent/hooks.rb +23 -2
  30. data/lib/claude_agent/list_sessions.rb +22 -13
  31. data/lib/claude_agent/message_parser.rb +26 -4
  32. data/lib/claude_agent/messages/conversation.rb +138 -0
  33. data/lib/claude_agent/messages/generic.rb +39 -0
  34. data/lib/claude_agent/messages/hook_lifecycle.rb +158 -0
  35. data/lib/claude_agent/messages/result.rb +80 -0
  36. data/lib/claude_agent/messages/streaming.rb +84 -0
  37. data/lib/claude_agent/messages/system.rb +67 -0
  38. data/lib/claude_agent/messages/task_lifecycle.rb +240 -0
  39. data/lib/claude_agent/messages/tool_lifecycle.rb +95 -0
  40. data/lib/claude_agent/messages.rb +11 -829
  41. data/lib/claude_agent/options/serializer.rb +194 -0
  42. data/lib/claude_agent/options.rb +11 -176
  43. data/lib/claude_agent/sandbox_settings.rb +3 -0
  44. data/lib/claude_agent/session.rb +0 -204
  45. data/lib/claude_agent/session_mutations.rb +148 -0
  46. data/lib/claude_agent/types/mcp.rb +30 -0
  47. data/lib/claude_agent/types/models.rb +146 -0
  48. data/lib/claude_agent/types/operations.rb +38 -0
  49. data/lib/claude_agent/types/sessions.rb +50 -0
  50. data/lib/claude_agent/types/tools.rb +32 -0
  51. data/lib/claude_agent/types.rb +6 -264
  52. data/lib/claude_agent/v2_session.rb +207 -0
  53. data/lib/claude_agent/version.rb +1 -1
  54. data/lib/claude_agent.rb +37 -3
  55. data/sig/claude_agent.rbs +144 -13
  56. metadata +33 -1
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module ClaudeAgent
6
+ class Options
7
+ # Converts Options into CLI arguments and environment variables.
8
+ #
9
+ # Separated from Options to keep configuration concerns (what to store)
10
+ # distinct from serialization concerns (how to render for the CLI).
11
+ module Serializer
12
+ # Build CLI arguments from options
13
+ # @return [Array<String>] CLI arguments
14
+ def to_cli_args
15
+ [].tap do |args|
16
+ args.concat(system_prompt_args)
17
+ args.concat(model_args)
18
+ args.concat(tools_args)
19
+ args.concat(permission_args)
20
+ args.concat(conversation_args)
21
+ args.concat(limits_args)
22
+ args.concat(mcp_args)
23
+ args.concat(sandbox_args)
24
+ args.concat(environment_args)
25
+ args.concat(output_args)
26
+ args.concat(debug_args)
27
+ args.concat(extra_cli_args)
28
+ end
29
+ end
30
+
31
+ # Build environment variables for CLI process
32
+ # @return [Hash] Environment variables
33
+ def to_env
34
+ env.dup.tap do |process_env|
35
+ process_env["CLAUDE_CODE_ENTRYPOINT"] = "sdk-rb"
36
+ process_env["CLAUDE_AGENT_SDK_VERSION"] = ClaudeAgent::VERSION
37
+ process_env["CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING"] = "true" if enable_file_checkpointing
38
+ process_env["PWD"] = cwd.to_s if cwd
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ # --- CLI Argument Builders ---
45
+
46
+ def system_prompt_args
47
+ [].tap do |args|
48
+ if system_prompt
49
+ case system_prompt
50
+ when String then args.push("--system-prompt", system_prompt)
51
+ when Hash then args.push("--system-prompt", JSON.generate(system_prompt))
52
+ end
53
+ end
54
+ args.push("--append-system-prompt", append_system_prompt) if append_system_prompt
55
+ end
56
+ end
57
+
58
+ def model_args
59
+ [].tap do |args|
60
+ args.push("--model", model) if model
61
+ args.push("--fallback-model", fallback_model) if fallback_model
62
+ end
63
+ end
64
+
65
+ def tools_args
66
+ [].tap do |args|
67
+ if tools
68
+ case tools
69
+ when Array then args.push("--tools", tools.join(","))
70
+ when ToolsPreset then args.push("--tools", JSON.generate(tools.to_h))
71
+ when Hash then args.push("--tools", JSON.generate(tools))
72
+ else args.push("--tools", tools.to_s)
73
+ end
74
+ end
75
+ args.push("--allowedTools", allowed_tools.join(",")) if allowed_tools.any?
76
+ args.push("--disallowedTools", disallowed_tools.join(",")) if disallowed_tools.any?
77
+ end
78
+ end
79
+
80
+ def permission_args
81
+ [].tap do |args|
82
+ args.push("--permission-mode", permission_mode) if permission_mode
83
+ args.push("--permission-prompt-tool", permission_prompt_tool_name) if permission_prompt_tool_name
84
+ args.push("--dangerously-skip-permissions") if allow_dangerously_skip_permissions
85
+ end
86
+ end
87
+
88
+ def conversation_args
89
+ [].tap do |args|
90
+ args.push("--continue") if continue_conversation
91
+ args.push("--resume", resume) if resume
92
+ args.push("--fork-session") if fork_session
93
+ args.push("--resume-session-at", resume_session_at) if resume_session_at
94
+ args.push("--session-id", session_id) if session_id
95
+ end
96
+ end
97
+
98
+ def limits_args
99
+ [].tap do |args|
100
+ args.push("--max-turns", max_turns.to_s) if max_turns
101
+ args.push("--max-budget-usd", max_budget_usd.to_s) if max_budget_usd
102
+ args.concat(thinking_args)
103
+ args.push("--max-thinking-tokens", max_thinking_tokens.to_s) if !thinking && max_thinking_tokens
104
+ args.push("--effort", effort) if effort
105
+ args.push("--strict-mcp-config") if strict_mcp_config
106
+ end
107
+ end
108
+
109
+ def thinking_args
110
+ return [] unless thinking.is_a?(Hash)
111
+
112
+ type = thinking[:type] || thinking["type"]
113
+ case type
114
+ when "disabled"
115
+ [ "--max-thinking-tokens", "0" ]
116
+ when "enabled"
117
+ budget = thinking[:budgetTokens] || thinking[:budget_tokens] ||
118
+ thinking["budgetTokens"] || thinking["budget_tokens"]
119
+ budget ? [ "--max-thinking-tokens", budget.to_s ] : []
120
+ else # "adaptive" or unrecognized — omit flag, let CLI use default
121
+ []
122
+ end
123
+ end
124
+
125
+ def mcp_args
126
+ [].tap do |args|
127
+ if mcp_servers.is_a?(Hash) && mcp_servers.any?
128
+ external_servers = mcp_servers.reject { |_, v| v.is_a?(Hash) && v[:type] == "sdk" }
129
+ args.push("--mcp-config", JSON.generate(external_servers)) if external_servers.any?
130
+ elsif mcp_servers.is_a?(String)
131
+ args.push("--mcp-config", mcp_servers)
132
+ end
133
+ end
134
+ end
135
+
136
+ def sandbox_args
137
+ [].tap do |args|
138
+ if sandbox
139
+ args.push("--sandbox", JSON.generate(sandbox.to_h))
140
+ end
141
+ end
142
+ end
143
+
144
+ def environment_args
145
+ [].tap do |args|
146
+ args.push("--agent", agent) if agent
147
+ add_dirs.each { |dir| args.push("--add-dir", dir.to_s) }
148
+ args.push("--setting-sources", setting_sources.join(",")) if setting_sources&.any?
149
+ if settings
150
+ case settings
151
+ when String then args.push("--settings", settings)
152
+ when Hash then args.push("--settings", JSON.generate(settings))
153
+ end
154
+ end
155
+ plugins.each do |plugin|
156
+ dir = plugin.is_a?(Hash) ? plugin[:dir] : plugin
157
+ args.push("--plugin-dir", dir.to_s)
158
+ end
159
+ args.push("--betas", betas.join(",")) if betas.any?
160
+ end
161
+ end
162
+
163
+ def output_args
164
+ [].tap do |args|
165
+ args.push("--enable-file-checkpointing") if enable_file_checkpointing
166
+ args.push("--no-persist-session") if persist_session == false
167
+ args.push("--json-schema", JSON.generate(output_format)) if output_format
168
+ args.push("--include-partial-messages") if include_partial_messages
169
+ args.push("--prompt-suggestions") if prompt_suggestions
170
+ if agents
171
+ agents_hash = agents.transform_values(&:to_h)
172
+ args.push("--agents", JSON.generate(agents_hash))
173
+ end
174
+ end
175
+ end
176
+
177
+ def debug_args
178
+ [].tap do |args|
179
+ args.push("--debug") if debug
180
+ args.push("--debug-file", debug_file) if debug_file
181
+ end
182
+ end
183
+
184
+ def extra_cli_args
185
+ [].tap do |args|
186
+ extra_args.each do |key, value|
187
+ flag = key.to_s.start_with?("--") ? key.to_s : "--#{key}"
188
+ value.nil? ? args.push(flag) : args.push(flag, value.to_s)
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
3
+ require_relative "options/serializer"
4
4
 
5
5
  module ClaudeAgent
6
6
  # Permission modes for tool execution (TypeScript SDK parity)
@@ -22,6 +22,8 @@ module ClaudeAgent
22
22
  # )
23
23
  #
24
24
  class Options
25
+ include Serializer
26
+
25
27
  # Default values for options that have non-nil defaults
26
28
  DEFAULTS = {
27
29
  allowed_tools: [],
@@ -52,17 +54,19 @@ module ClaudeAgent
52
54
  tools allowed_tools disallowed_tools
53
55
  system_prompt append_system_prompt
54
56
  model fallback_model
55
- permission_mode permission_prompt_tool_name can_use_tool allow_dangerously_skip_permissions
57
+ permission_mode permission_prompt_tool_name can_use_tool on_elicitation allow_dangerously_skip_permissions
56
58
  permission_queue
57
59
  continue_conversation resume fork_session resume_session_at session_id
58
60
  max_turns max_budget_usd thinking effort max_thinking_tokens
59
61
  strict_mcp_config mcp_servers hooks
60
62
  sandbox cwd add_dirs env agent
61
- cli_path extra_args agents setting_sources plugins
63
+ cli_path extra_args agents setting_sources settings plugins
62
64
  include_partial_messages output_format enable_file_checkpointing
63
65
  persist_session prompt_suggestions betas max_buffer_size stderr_callback
64
66
  abort_controller spawn_claude_code_process
65
67
  debug debug_file
68
+ tool_config
69
+ agent_progress_summaries
66
70
  logger
67
71
  ].freeze
68
72
 
@@ -78,36 +82,6 @@ module ClaudeAgent
78
82
  validate!
79
83
  end
80
84
 
81
- # Build CLI arguments from options
82
- # @return [Array<String>] CLI arguments
83
- def to_cli_args
84
- [].tap do |args|
85
- args.concat(system_prompt_args)
86
- args.concat(model_args)
87
- args.concat(tools_args)
88
- args.concat(permission_args)
89
- args.concat(conversation_args)
90
- args.concat(limits_args)
91
- args.concat(mcp_args)
92
- args.concat(sandbox_args)
93
- args.concat(environment_args)
94
- args.concat(output_args)
95
- args.concat(debug_args)
96
- args.concat(extra_cli_args)
97
- end
98
- end
99
-
100
- # Build environment variables for CLI process
101
- # @return [Hash] Environment variables
102
- def to_env
103
- env.dup.tap do |process_env|
104
- process_env["CLAUDE_CODE_ENTRYPOINT"] = "sdk-rb"
105
- process_env["CLAUDE_AGENT_SDK_VERSION"] = ClaudeAgent::VERSION
106
- process_env["CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING"] = "true" if enable_file_checkpointing
107
- process_env["PWD"] = cwd.to_s if cwd
108
- end
109
- end
110
-
111
85
  # Check if SDK MCP servers are configured
112
86
  # @return [Boolean]
113
87
  def has_sdk_mcp_servers?
@@ -136,149 +110,6 @@ module ClaudeAgent
136
110
 
137
111
  private
138
112
 
139
- # --- CLI Argument Builders ---
140
-
141
- def system_prompt_args
142
- [].tap do |args|
143
- if system_prompt
144
- case system_prompt
145
- when String then args.push("--system-prompt", system_prompt)
146
- when Hash then args.push("--system-prompt", JSON.generate(system_prompt))
147
- end
148
- end
149
- args.push("--append-system-prompt", append_system_prompt) if append_system_prompt
150
- end
151
- end
152
-
153
- def model_args
154
- [].tap do |args|
155
- args.push("--model", model) if model
156
- args.push("--fallback-model", fallback_model) if fallback_model
157
- end
158
- end
159
-
160
- def tools_args
161
- [].tap do |args|
162
- if tools
163
- case tools
164
- when Array then args.push("--tools", tools.join(","))
165
- when ToolsPreset then args.push("--tools", JSON.generate(tools.to_h))
166
- when Hash then args.push("--tools", JSON.generate(tools))
167
- else args.push("--tools", tools.to_s)
168
- end
169
- end
170
- args.push("--allowedTools", allowed_tools.join(",")) if allowed_tools.any?
171
- args.push("--disallowedTools", disallowed_tools.join(",")) if disallowed_tools.any?
172
- end
173
- end
174
-
175
- def permission_args
176
- [].tap do |args|
177
- args.push("--permission-mode", permission_mode) if permission_mode
178
- args.push("--permission-prompt-tool", permission_prompt_tool_name) if permission_prompt_tool_name
179
- args.push("--dangerously-skip-permissions") if allow_dangerously_skip_permissions
180
- end
181
- end
182
-
183
- def conversation_args
184
- [].tap do |args|
185
- args.push("--continue") if continue_conversation
186
- args.push("--resume", resume) if resume
187
- args.push("--fork-session") if fork_session
188
- args.push("--resume-session-at", resume_session_at) if resume_session_at
189
- args.push("--session-id", session_id) if session_id
190
- end
191
- end
192
-
193
- def limits_args
194
- [].tap do |args|
195
- args.push("--max-turns", max_turns.to_s) if max_turns
196
- args.push("--max-budget-usd", max_budget_usd.to_s) if max_budget_usd
197
- args.concat(thinking_args)
198
- args.push("--max-thinking-tokens", max_thinking_tokens.to_s) if !thinking && max_thinking_tokens
199
- args.push("--effort", effort) if effort
200
- args.push("--strict-mcp-config") if strict_mcp_config
201
- end
202
- end
203
-
204
- def thinking_args
205
- return [] unless thinking.is_a?(Hash)
206
-
207
- type = thinking[:type] || thinking["type"]
208
- case type
209
- when "disabled"
210
- [ "--max-thinking-tokens", "0" ]
211
- when "enabled"
212
- budget = thinking[:budgetTokens] || thinking[:budget_tokens] ||
213
- thinking["budgetTokens"] || thinking["budget_tokens"]
214
- budget ? [ "--max-thinking-tokens", budget.to_s ] : []
215
- else # "adaptive" or unrecognized — omit flag, let CLI use default
216
- []
217
- end
218
- end
219
-
220
- def mcp_args
221
- [].tap do |args|
222
- if mcp_servers.is_a?(Hash) && mcp_servers.any?
223
- external_servers = mcp_servers.reject { |_, v| v.is_a?(Hash) && v[:type] == "sdk" }
224
- args.push("--mcp-config", JSON.generate(external_servers)) if external_servers.any?
225
- elsif mcp_servers.is_a?(String)
226
- args.push("--mcp-config", mcp_servers)
227
- end
228
- end
229
- end
230
-
231
- def sandbox_args
232
- [].tap do |args|
233
- if sandbox
234
- args.push("--sandbox", JSON.generate(sandbox.to_h))
235
- end
236
- end
237
- end
238
-
239
- def environment_args
240
- [].tap do |args|
241
- args.push("--agent", agent) if agent
242
- add_dirs.each { |dir| args.push("--add-dir", dir.to_s) }
243
- args.push("--setting-sources", setting_sources.join(",")) if setting_sources&.any?
244
- plugins.each do |plugin|
245
- dir = plugin.is_a?(Hash) ? plugin[:dir] : plugin
246
- args.push("--plugin-dir", dir.to_s)
247
- end
248
- args.push("--betas", betas.join(",")) if betas.any?
249
- end
250
- end
251
-
252
- def output_args
253
- [].tap do |args|
254
- args.push("--enable-file-checkpointing") if enable_file_checkpointing
255
- args.push("--no-persist-session") if persist_session == false
256
- args.push("--json-schema", JSON.generate(output_format)) if output_format
257
- args.push("--include-partial-messages") if include_partial_messages
258
- args.push("--prompt-suggestions") if prompt_suggestions
259
- if agents
260
- agents_hash = agents.transform_values(&:to_h)
261
- args.push("--agents", JSON.generate(agents_hash))
262
- end
263
- end
264
- end
265
-
266
- def debug_args
267
- [].tap do |args|
268
- args.push("--debug") if debug
269
- args.push("--debug-file", debug_file) if debug_file
270
- end
271
- end
272
-
273
- def extra_cli_args
274
- [].tap do |args|
275
- extra_args.each do |key, value|
276
- flag = key.to_s.start_with?("--") ? key.to_s : "--#{key}"
277
- value.nil? ? args.push(flag) : args.push(flag, value.to_s)
278
- end
279
- end
280
- end
281
-
282
113
  # --- Validation ---
283
114
 
284
115
  def validate!
@@ -295,6 +126,10 @@ module ClaudeAgent
295
126
  raise ConfigurationError, "can_use_tool must be callable (Proc, Lambda, or object responding to #call)"
296
127
  end
297
128
 
129
+ if on_elicitation && !on_elicitation.respond_to?(:call)
130
+ raise ConfigurationError, "on_elicitation must be callable (Proc, Lambda, or object responding to #call)"
131
+ end
132
+
298
133
  # Auto-set permission_prompt_tool_name to "stdio" when can_use_tool or
299
134
  # permission_queue is configured, so the CLI routes permission prompts
300
135
  # through the control protocol instead of interactive terminal prompts
@@ -135,6 +135,7 @@ module ClaudeAgent
135
135
  :network,
136
136
  :ignore_violations,
137
137
  :enable_weaker_nested_sandbox,
138
+ :enable_weaker_network_isolation,
138
139
  :ripgrep,
139
140
  :filesystem
140
141
  ) do
@@ -146,6 +147,7 @@ module ClaudeAgent
146
147
  network: nil,
147
148
  ignore_violations: nil,
148
149
  enable_weaker_nested_sandbox: false,
150
+ enable_weaker_network_isolation: false,
149
151
  ripgrep: nil,
150
152
  filesystem: nil
151
153
  )
@@ -160,6 +162,7 @@ module ClaudeAgent
160
162
  result[:network] = network.to_h if network && !network.to_h.empty?
161
163
  result[:ignoreViolations] = ignore_violations.to_h if ignore_violations && !ignore_violations.to_h.empty?
162
164
  result[:enableWeakerNestedSandbox] = enable_weaker_nested_sandbox if enable_weaker_nested_sandbox
165
+ result[:enableWeakerNetworkIsolation] = enable_weaker_network_isolation if enable_weaker_network_isolation
163
166
  result[:ripgrep] = ripgrep.to_h if ripgrep
164
167
  result[:filesystem] = filesystem.to_h if filesystem && !filesystem.to_h.empty?
165
168
  result
@@ -1,210 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeAgent
4
- # V2 Session options (subset of full Options)
5
- # V2 API - UNSTABLE
6
- # @alpha
7
- #
8
- # @example
9
- # options = SessionOptions.new(
10
- # model: "claude-sonnet-4-5-20250929",
11
- # permission_mode: "acceptEdits"
12
- # )
13
- #
14
- SessionOptions = Data.define(
15
- :model,
16
- :path_to_claude_code_executable,
17
- :env,
18
- :allowed_tools,
19
- :disallowed_tools,
20
- :can_use_tool,
21
- :hooks,
22
- :permission_mode
23
- ) do
24
- def initialize(
25
- model:,
26
- path_to_claude_code_executable: nil,
27
- env: nil,
28
- allowed_tools: nil,
29
- disallowed_tools: nil,
30
- can_use_tool: nil,
31
- hooks: nil,
32
- permission_mode: nil
33
- )
34
- super
35
- end
36
- end
37
-
38
- # V2 API - UNSTABLE
39
- # Multi-turn session interface for persistent conversations.
40
- #
41
- # This provides a simpler interface than the full Client class,
42
- # matching the TypeScript SDK's SDKSession interface.
43
- #
44
- # @alpha
45
- #
46
- # @example Create a session and send messages
47
- # session = ClaudeAgent.unstable_v2_create_session(model: "claude-sonnet-4-5-20250929")
48
- # session.send("Hello!")
49
- # session.stream.each { |msg| puts msg.inspect }
50
- # session.close
51
- #
52
- class V2Session
53
- attr_reader :session_id, :options
54
-
55
- def initialize(options)
56
- @options = options.is_a?(SessionOptions) ? options : SessionOptions.new(**options)
57
- @client = nil
58
- @session_id = nil
59
- @closed = false
60
- end
61
-
62
- # Send a message to the agent
63
- #
64
- # @param message [String, UserMessage] The message to send
65
- # @return [void]
66
- def send(message)
67
- ensure_connected!
68
- content = message.is_a?(String) ? message : message
69
- @client.send_message(content)
70
- end
71
-
72
- # Stream messages from the agent
73
- #
74
- # @return [Enumerator<message>] An enumerator of messages
75
- # @yield [message] Each message received from the agent
76
- def stream(&block)
77
- ensure_connected!
78
- if block_given?
79
- @client.receive_response(&block)
80
- else
81
- @client.receive_response
82
- end
83
- end
84
-
85
- # Close the session
86
- #
87
- # @return [void]
88
- def close
89
- return if @closed
90
- @client&.disconnect
91
- @closed = true
92
- end
93
-
94
- # Check if session is closed
95
- #
96
- # @return [Boolean]
97
- def closed?
98
- @closed
99
- end
100
-
101
- private
102
-
103
- def ensure_connected!
104
- raise AbortError, "Session is closed" if @closed
105
- return if @client&.connected?
106
-
107
- @client = Client.new(options: build_client_options)
108
- @client.connect
109
- update_session_id
110
- end
111
-
112
- def build_client_options
113
- Options.new(
114
- model: @options.model,
115
- cli_path: @options.path_to_claude_code_executable,
116
- env: @options.env,
117
- allowed_tools: @options.allowed_tools,
118
- disallowed_tools: @options.disallowed_tools,
119
- can_use_tool: @options.can_use_tool,
120
- hooks: @options.hooks,
121
- permission_mode: @options.permission_mode
122
- )
123
- end
124
-
125
- def update_session_id
126
- # Session ID is typically extracted from the first system message
127
- # but since we don't have it immediately, we leave it nil until available
128
- @session_id = @client.server_info&.dig("session_id")
129
- end
130
- end
131
-
132
- class << self
133
- # V2 API - UNSTABLE
134
- # Create a persistent session for multi-turn conversations.
135
- #
136
- # @param options [Hash, SessionOptions] Session configuration
137
- # @return [Session] A new session instance
138
- # @alpha
139
- #
140
- # @example
141
- # session = ClaudeAgent.unstable_v2_create_session(model: "claude-sonnet-4-5-20250929")
142
- #
143
- def unstable_v2_create_session(options)
144
- V2Session.new(options)
145
- end
146
-
147
- # V2 API - UNSTABLE
148
- # Resume an existing session by ID.
149
- #
150
- # @param session_id [String] The session ID to resume
151
- # @param options [Hash, SessionOptions] Session configuration
152
- # @return [Session] A session configured to resume the specified session
153
- # @alpha
154
- #
155
- # @example
156
- # session = ClaudeAgent.unstable_v2_resume_session("session-abc123", model: "claude-sonnet-4-5-20250929")
157
- #
158
- def unstable_v2_resume_session(session_id, options)
159
- # For resumption, we need to pass the resume option through
160
- # Since SessionOptions doesn't have resume, we handle it in the Client options
161
- session = V2Session.new(options)
162
- session.instance_variable_set(:@resume_session_id, session_id)
163
-
164
- # Override build_client_options to include resume
165
- session.define_singleton_method(:build_client_options) do
166
- Options.new(
167
- model: @options.model,
168
- cli_path: @options.path_to_claude_code_executable,
169
- env: @options.env,
170
- allowed_tools: @options.allowed_tools,
171
- disallowed_tools: @options.disallowed_tools,
172
- can_use_tool: @options.can_use_tool,
173
- hooks: @options.hooks,
174
- permission_mode: @options.permission_mode,
175
- resume: @resume_session_id
176
- )
177
- end
178
-
179
- session
180
- end
181
-
182
- # V2 API - UNSTABLE
183
- # One-shot convenience function for single prompts.
184
- #
185
- # @param message [String] The prompt message
186
- # @param options [Hash, SessionOptions] Session configuration
187
- # @return [ResultMessage] The result of the query
188
- # @alpha
189
- #
190
- # @example
191
- # result = ClaudeAgent.unstable_v2_prompt("What files are here?", model: "claude-sonnet-4-5-20250929")
192
- #
193
- def unstable_v2_prompt(message, options)
194
- session = unstable_v2_create_session(options)
195
- begin
196
- session.send(message)
197
- result = nil
198
- session.stream.each do |msg|
199
- result = msg if msg.is_a?(ResultMessage)
200
- end
201
- result
202
- ensure
203
- session.close
204
- end
205
- end
206
- end
207
-
208
4
  # Historical session finder with Rails-like API.
209
5
  #
210
6
  # Wraps SessionInfo with a rich interface for finding sessions