claude_agent 0.7.12 → 0.7.14

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 (59) 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 +53 -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 -206
  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 +116 -0
  23. data/lib/claude_agent/control_protocol/messaging.rb +163 -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 +50 -882
  27. data/lib/claude_agent/conversation.rb +8 -1
  28. data/lib/claude_agent/event_handler.rb +1 -0
  29. data/lib/claude_agent/get_session_info.rb +86 -0
  30. data/lib/claude_agent/hooks.rb +23 -2
  31. data/lib/claude_agent/list_sessions.rb +22 -13
  32. data/lib/claude_agent/message_parser.rb +26 -4
  33. data/lib/claude_agent/messages/conversation.rb +138 -0
  34. data/lib/claude_agent/messages/generic.rb +39 -0
  35. data/lib/claude_agent/messages/hook_lifecycle.rb +158 -0
  36. data/lib/claude_agent/messages/result.rb +80 -0
  37. data/lib/claude_agent/messages/streaming.rb +84 -0
  38. data/lib/claude_agent/messages/system.rb +67 -0
  39. data/lib/claude_agent/messages/task_lifecycle.rb +240 -0
  40. data/lib/claude_agent/messages/tool_lifecycle.rb +95 -0
  41. data/lib/claude_agent/messages.rb +11 -829
  42. data/lib/claude_agent/options/serializer.rb +194 -0
  43. data/lib/claude_agent/options.rb +11 -176
  44. data/lib/claude_agent/query.rb +0 -2
  45. data/lib/claude_agent/sandbox_settings.rb +3 -0
  46. data/lib/claude_agent/session.rb +0 -204
  47. data/lib/claude_agent/session_mutations.rb +148 -0
  48. data/lib/claude_agent/transport/subprocess.rb +2 -2
  49. data/lib/claude_agent/types/mcp.rb +30 -0
  50. data/lib/claude_agent/types/models.rb +146 -0
  51. data/lib/claude_agent/types/operations.rb +38 -0
  52. data/lib/claude_agent/types/sessions.rb +50 -0
  53. data/lib/claude_agent/types/tools.rb +32 -0
  54. data/lib/claude_agent/types.rb +6 -264
  55. data/lib/claude_agent/v2_session.rb +207 -0
  56. data/lib/claude_agent/version.rb +1 -1
  57. data/lib/claude_agent.rb +37 -3
  58. data/sig/claude_agent.rbs +144 -13
  59. metadata +33 -1
@@ -1,5 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "types/tools"
4
+ require_relative "types/models"
5
+ require_relative "types/mcp"
6
+ require_relative "types/sessions"
7
+ require_relative "types/operations"
8
+
3
9
  module ClaudeAgent
4
10
  # Assistant message error types (TypeScript SDK parity)
5
11
  # Used to categorize errors returned by the assistant
@@ -16,268 +22,4 @@ module ClaudeAgent
16
22
  # API key source types (TypeScript SDK parity)
17
23
  # Indicates where the API key was sourced from
18
24
  API_KEY_SOURCES = %w[user project org temporary oauth].freeze
19
-
20
- # Tools preset configuration (TypeScript SDK parity)
21
- #
22
- # @example
23
- # preset = ToolsPreset.new(preset: "claude_code")
24
- # options = ClaudeAgent::Options.new(tools: preset)
25
- #
26
- ToolsPreset = Data.define(:type, :preset) do
27
- def initialize(type: "preset", preset: "claude_code")
28
- super
29
- end
30
-
31
- def to_h
32
- { type: type, preset: preset }
33
- end
34
- end
35
- # Return type for supported_commands() (TypeScript SDK parity)
36
- #
37
- # @example
38
- # cmd = SlashCommand.new(name: "commit", description: "Create a commit", argument_hint: "[message]")
39
- # cmd.name # => "commit"
40
- # cmd.description # => "Create a commit"
41
- #
42
- SlashCommand = Data.define(:name, :description, :argument_hint) do
43
- def initialize(name:, description: nil, argument_hint: nil)
44
- super
45
- end
46
- end
47
-
48
- # Return type for supported_models() (TypeScript SDK parity)
49
- #
50
- # @example
51
- # model = ModelInfo.new(value: "claude-3-opus", display_name: "Claude 3 Opus", description: "Most capable")
52
- # model.value # => "claude-3-opus"
53
- # model.display_name # => "Claude 3 Opus"
54
- #
55
- ModelInfo = Data.define(:value, :display_name, :description, :supports_effort, :supported_effort_levels, :supports_adaptive_thinking) do
56
- def initialize(value:, display_name: nil, description: nil, supports_effort: nil, supported_effort_levels: nil, supports_adaptive_thinking: nil)
57
- super
58
- end
59
- end
60
-
61
- # Return type for mcp_server_status() (TypeScript SDK parity)
62
- # Status values: "connected", "failed", "needs-auth", "pending"
63
- #
64
- # @example
65
- # status = McpServerStatus.new(name: "filesystem", status: "connected", server_info: {name: "fs", version: "1.0"})
66
- #
67
- McpServerStatus = Data.define(:name, :status, :server_info, :error, :config, :scope, :tools) do
68
- def initialize(name:, status:, server_info: nil, error: nil, config: nil, scope: nil, tools: nil)
69
- super
70
- end
71
- end
72
-
73
- # Task usage statistics for TaskNotificationMessage (TypeScript SDK parity)
74
- #
75
- # @example
76
- # usage = TaskUsage.new(total_tokens: 5000, tool_uses: 3, duration_ms: 2500)
77
- #
78
- TaskUsage = Data.define(:total_tokens, :tool_uses, :duration_ms) do
79
- def initialize(total_tokens: 0, tool_uses: 0, duration_ms: 0)
80
- super
81
- end
82
- end
83
-
84
- # Return type for account_info() (TypeScript SDK parity)
85
- #
86
- # @example
87
- # info = AccountInfo.new(email: "user@example.com", organization: "Acme Corp")
88
- #
89
- AccountInfo = Data.define(:email, :organization, :subscription_type, :token_source, :api_key_source) do
90
- def initialize(email: nil, organization: nil, subscription_type: nil, token_source: nil, api_key_source: nil)
91
- super
92
- end
93
- end
94
-
95
- # Composite initialization result from supported_commands request (TypeScript SDK parity)
96
- #
97
- # @example
98
- # result = InitializationResult.new(
99
- # commands: [SlashCommand.new(name: "commit")],
100
- # output_style: "default",
101
- # available_output_styles: ["default", "concise"],
102
- # models: [ModelInfo.new(value: "claude-sonnet")],
103
- # account: AccountInfo.new(email: "user@example.com")
104
- # )
105
- #
106
- InitializationResult = Data.define(:commands, :output_style, :available_output_styles, :models, :account) do
107
- def initialize(commands: [], output_style: nil, available_output_styles: [], models: [], account: nil)
108
- super
109
- end
110
- end
111
-
112
- # Per-model usage statistics returned in result messages (TypeScript SDK parity)
113
- #
114
- # @example
115
- # usage = ModelUsage.new(input_tokens: 100, output_tokens: 50, cost_usd: 0.01, max_output_tokens: 4096)
116
- #
117
- ModelUsage = Data.define(
118
- :input_tokens,
119
- :output_tokens,
120
- :cache_read_input_tokens,
121
- :cache_creation_input_tokens,
122
- :web_search_requests,
123
- :cost_usd,
124
- :context_window,
125
- :max_output_tokens
126
- ) do
127
- def initialize(
128
- input_tokens: 0,
129
- output_tokens: 0,
130
- cache_read_input_tokens: 0,
131
- cache_creation_input_tokens: 0,
132
- web_search_requests: 0,
133
- cost_usd: 0.0,
134
- context_window: nil,
135
- max_output_tokens: nil
136
- )
137
- super
138
- end
139
- end
140
-
141
- # Permission denial information in result messages (TypeScript SDK parity)
142
- #
143
- SDKPermissionDenial = Data.define(:tool_name, :tool_use_id, :tool_input) do
144
- def initialize(tool_name:, tool_use_id:, tool_input:)
145
- super
146
- end
147
- end
148
-
149
- # Result of set_mcp_servers() control method (TypeScript SDK parity)
150
- #
151
- # @example
152
- # result = McpSetServersResult.new(
153
- # added: ["server1"],
154
- # removed: ["old-server"],
155
- # errors: {"server2" => "Connection failed"}
156
- # )
157
- #
158
- McpSetServersResult = Data.define(:added, :removed, :errors) do
159
- def initialize(added: [], removed: [], errors: {})
160
- super
161
- end
162
- end
163
-
164
- # Result of rewind_files() control method (TypeScript SDK parity)
165
- #
166
- # @example
167
- # result = RewindFilesResult.new(
168
- # can_rewind: true,
169
- # files_changed: ["src/foo.rb", "src/bar.rb"],
170
- # insertions: 10,
171
- # deletions: 5
172
- # )
173
- #
174
- RewindFilesResult = Data.define(:can_rewind, :error, :files_changed, :insertions, :deletions) do
175
- def initialize(can_rewind:, error: nil, files_changed: nil, insertions: nil, deletions: nil)
176
- super
177
- end
178
- end
179
-
180
- # Agent definition for custom subagents (TypeScript SDK parity)
181
- #
182
- # @example Basic agent
183
- # agent = AgentDefinition.new(
184
- # description: "Runs tests and reports results",
185
- # prompt: "You are a test runner...",
186
- # tools: ["Read", "Grep", "Glob", "Bash"],
187
- # model: "haiku"
188
- # )
189
- #
190
- # @example Agent with skills and max_turns
191
- # agent = AgentDefinition.new(
192
- # description: "Research agent with specialized skills",
193
- # prompt: "You are a research expert...",
194
- # skills: ["web-search", "summarization"],
195
- # max_turns: 10
196
- # )
197
- #
198
- # Session metadata returned by list_sessions (TypeScript SDK parity: SDKSessionInfo)
199
- #
200
- # @example
201
- # session = SessionInfo.new(
202
- # session_id: "abc-123",
203
- # summary: "Fix login bug",
204
- # last_modified: 1706000000000,
205
- # file_size: 4096,
206
- # custom_title: "Login fix",
207
- # first_prompt: "Help me fix the login page",
208
- # git_branch: "fix/login",
209
- # cwd: "/Users/dev/myapp"
210
- # )
211
- #
212
- SessionInfo = Data.define(
213
- :session_id,
214
- :summary,
215
- :last_modified,
216
- :file_size,
217
- :custom_title,
218
- :first_prompt,
219
- :git_branch,
220
- :cwd
221
- ) do
222
- def initialize(session_id:, summary:, last_modified:, file_size:, custom_title: nil, first_prompt: nil, git_branch: nil, cwd: nil)
223
- super
224
- end
225
- end
226
-
227
- # Message from a session transcript returned by get_session_messages (TypeScript SDK v0.2.59 parity)
228
- #
229
- # @example
230
- # msg = SessionMessage.new(
231
- # type: "user",
232
- # uuid: "abc-123",
233
- # session_id: "def-456",
234
- # message: { "role" => "user", "content" => [{ "type" => "text", "text" => "Hello" }] }
235
- # )
236
- #
237
- SessionMessage = Data.define(:type, :uuid, :session_id, :message, :parent_tool_use_id) do
238
- def initialize(type:, uuid:, session_id:, message:, parent_tool_use_id: nil)
239
- super
240
- end
241
- end
242
-
243
- AgentDefinition = Data.define(
244
- :description,
245
- :prompt,
246
- :tools,
247
- :disallowed_tools,
248
- :model,
249
- :mcp_servers,
250
- :critical_system_reminder,
251
- :skills,
252
- :max_turns
253
- ) do
254
- def initialize(
255
- description:,
256
- prompt:,
257
- tools: nil,
258
- disallowed_tools: nil,
259
- model: nil,
260
- mcp_servers: nil,
261
- critical_system_reminder: nil,
262
- skills: nil,
263
- max_turns: nil
264
- )
265
- super
266
- end
267
-
268
- def to_h
269
- result = {
270
- description: description,
271
- prompt: prompt
272
- }
273
- result[:tools] = tools if tools
274
- result[:disallowedTools] = disallowed_tools if disallowed_tools
275
- result[:model] = model if model
276
- result[:mcpServers] = mcp_servers if mcp_servers
277
- result[:criticalSystemReminder_EXPERIMENTAL] = critical_system_reminder if critical_system_reminder
278
- result[:skills] = skills if skills
279
- result[:maxTurns] = max_turns if max_turns
280
- result
281
- end
282
- end
283
25
  end
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
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
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeAgent
4
- VERSION = "0.7.12"
4
+ VERSION = "0.7.14"
5
5
  end
data/lib/claude_agent.rb CHANGED
@@ -36,7 +36,10 @@ require_relative "claude_agent/session_paths" # Shared session path infra
36
36
  require_relative "claude_agent/list_sessions" # Session discovery (TypeScript SDK v0.2.53 parity)
37
37
  require_relative "claude_agent/get_session_messages" # Session transcript reading (TypeScript SDK v0.2.59 parity)
38
38
  require_relative "claude_agent/session_message_relation" # Chainable message query object
39
- require_relative "claude_agent/session" # Session finder + V2 Session API (unstable)
39
+ require_relative "claude_agent/session_mutations" # Session rename/tag mutations
40
+ require_relative "claude_agent/get_session_info" # Single session lookup
41
+ require_relative "claude_agent/v2_session" # V2 Session API (unstable)
42
+ require_relative "claude_agent/session" # Session finder
40
43
 
41
44
  module ClaudeAgent
42
45
  class << self
@@ -56,9 +59,11 @@ module ClaudeAgent
56
59
  # @param dir [String, nil] Directory to scope sessions to (includes git worktrees).
57
60
  # When nil, returns sessions from all projects.
58
61
  # @param limit [Integer, nil] Maximum number of sessions to return.
62
+ # @param include_worktrees [Boolean] When dir is in a git repo, include sessions
63
+ # from all git worktree paths. Defaults to true.
59
64
  # @return [Array<SessionInfo>]
60
- def list_sessions(dir: nil, limit: nil)
61
- ListSessions.call(dir: dir, limit: limit)
65
+ def list_sessions(dir: nil, limit: nil, offset: nil, include_worktrees: true)
66
+ ListSessions.call(dir: dir, limit: limit, offset: offset, include_worktrees: include_worktrees)
62
67
  end
63
68
 
64
69
  # Read messages from a past session's transcript
@@ -76,6 +81,35 @@ module ClaudeAgent
76
81
  GetSessionMessages.call(session_id, dir: dir, limit: limit, offset: offset)
77
82
  end
78
83
 
84
+ # Rename a session by appending a custom-title entry to its file.
85
+ #
86
+ # @param session_id [String] UUID of the session to rename
87
+ # @param title [String] New title
88
+ # @param dir [String, nil] Project directory to scope the search
89
+ # @return [void]
90
+ def rename_session(session_id, title, dir: nil)
91
+ SessionMutations.rename_session(session_id, title, dir: dir)
92
+ end
93
+
94
+ # Tag a session by appending a tag entry to its file.
95
+ #
96
+ # @param session_id [String] UUID of the session to tag
97
+ # @param tag [String, nil] Tag value. Pass nil to clear.
98
+ # @param dir [String, nil] Project directory to scope the search
99
+ # @return [void]
100
+ def tag_session(session_id, tag, dir: nil)
101
+ SessionMutations.tag_session(session_id, tag, dir: dir)
102
+ end
103
+
104
+ # Look up a single session by ID.
105
+ #
106
+ # @param session_id [String] UUID of the session
107
+ # @param dir [String, nil] Project directory to scope the search
108
+ # @return [SessionInfo, nil]
109
+ def get_session_info(session_id, dir: nil)
110
+ GetSessionInfo.call(session_id, dir: dir)
111
+ end
112
+
79
113
  # Resume a previous Conversation by session ID
80
114
  #
81
115
  # @param session_id [String] Session ID to resume