claude-agent-sdk 0.21.0 → 0.22.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 408fe2ed3f2f1f00866e3cef0b1795e67dc50dafd3292fc016985f62c10f1b6e
4
- data.tar.gz: 045b04fee0681072fe7e99c81ab7a99d51d2eec362baf282a45c8d825cc38135
3
+ metadata.gz: b739ecaa08f1ddc9bc97ffdf639deca97a69da0251ec7b4ec975c7994c76c4b7
4
+ data.tar.gz: c85fe94b25b9b18c7de7d842cdeef7d0059c40cf829f33a963a4c6a215b4f999
5
5
  SHA512:
6
- metadata.gz: 91b04e46d137103527e3af91d089acc01be96b69a8b0e6902eecb603654567ee826c8284ac0f9b797f180af64220ecfe0d71c3ab6f5e755e03525d596fcff85e
7
- data.tar.gz: fb7e3d0a535522e98ed3c7a0180f1ac35ca5057a29dfdb3429b07cd896375672c0c94e114d92809325680f599ca39d8dee2f7fc3a658a73fabd744cf4a987946
6
+ metadata.gz: 7c1e37466bb844a9bcb4be7206881051e097e45181b64554f1e1299602a00f0ad88f1ffcefd73796e118715595bcef83cfd8e63138e2f537e2b6312428767e2c
7
+ data.tar.gz: daa2ba1268fdf355b426fec710b28254eaa4db61ff45017fe444f05b1499986f13aa2b1273997a52f64c061ce84a6cd7efda897d977c5d753323511c05184399
data/CHANGELOG.md CHANGED
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.22.0] - 2026-07-03
11
+
12
+ Final batch (Batch D) from the 2026-07-03 full-codebase audit, closing it out: tests/docs/examples plus the three recorded split-verdict findings (P1-P3). Minor because the conformance suite gained a contract (an adapter that passed 0.21.0 can now fail contract 16) and new API surface (`check_uuid_dedupe:`).
13
+
14
+ ### Fixed
15
+ - OTel observer: the next turn's prompt is no longer dropped from the new trace after an interrupted turn or `/clear` (a superseding init with no ResultMessage) — prompts arriving while the current trace already has its input are buffered for the next trace instead of being latched out; a prompt queued mid-turn now labels its own trace too.
16
+ - Disk session listings no longer misclassify a session as a sidechain from a nested `"isSidechain":true` inside a structured field: the first line is parsed and the top-level key checked (same as the store fold), with the substring heuristic kept only for window-truncated first lines.
17
+ - SDK MCP servers: propertyless object schemas (`{ type: 'object', additionalProperties: ... }`, `oneOf`, accept-anything forms) now pass through intact instead of being mangled into nonsense parameter lists ("additionalProperties" advertised as a required string param).
18
+ - `import_session_to_store` skips an unparseable trailing line with a warning (an ordinary interrupted-CLI artifact every read path already tolerates) instead of aborting mid-import with a raw `JSON::ParserError` and leaving a partial store import behind.
19
+ - Redis reference adapter: the delete cascade now WATCHes the subkey set, so a concurrent eager-mode append can no longer orphan a freshly-created subagent list forever; both the Redis and Postgres reference adapters stamp mtimes through a monotonic guard (like S3) so a backward clock step can't misdirect `--continue`.
20
+ - Redis example spec no longer FLUSHDBs whatever `SESSION_STORE_REDIS_URL` points at — it uses a random key prefix per run with prefix-scoped cleanup, like the Postgres spec.
21
+
22
+ ### Added
23
+ - Conformance suite contract 16: `list_sessions` must return exactly one row per session under multiple appends (the naive one-row-per-append implementation previously passed every contract, then showed N duplicate sessions in pickers). Contract 4 now also asserts `append([])` cannot create a phantom key. New opt-in `check_uuid_dedupe:` flag asserts the advisory retried-batch uuid-dedupe recommendation.
24
+ - `--continue` with a store that implements `list_session_summaries` skips sidechain candidates via the summary sidecar instead of downloading every candidate's full transcript.
25
+
26
+ ### Changed
27
+ - `RUN_INTEGRATION=1` now actually runs the real-CLI integration suite (the old tautological placeholder suite is gone; `RUN_REAL_INTEGRATION` remains as an alias). The real suite still self-skips without a `claude` CLI or `ANTHROPIC_API_KEY`.
28
+ - Packaged docs/README now link to examples and repo files via absolute GitHub URLs — the relative links were dead in installed gems and on rubydoc.info (examples/ and assets/ are not packaged).
29
+
10
30
  ## [0.21.0] - 2026-07-03
11
31
 
12
32
  Design-decision fix batch (Batch C) from the 2026-07-03 full-codebase audit (`AUDIT-2026-07-03.md`, PR #43), plus three teardown-race hardenings from its adversarial review. Minor (not patch) because three fixes change observable behavior for previously-broken flows: a missing settings file with sandbox no longer raises, a raising initial prompt stream no longer notifies observers, and teardown can now preserve (instead of delete) the materialized resume dir.
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Claude Agent SDK for Ruby
2
2
 
3
- ![Claude Agent SDK for Ruby banner](assets/claude-agent-sdk-ruby-banner.png)
3
+ ![Claude Agent SDK for Ruby banner](https://raw.githubusercontent.com/ya-luotao/claude-agent-sdk-ruby/main/assets/claude-agent-sdk-ruby-banner.png)
4
4
 
5
5
  [![Gem Version](https://badge.fury.io/rb/claude-agent-sdk.svg?icon=si%3Arubygems)](https://badge.fury.io/rb/claude-agent-sdk)
6
6
 
@@ -68,7 +68,7 @@ Add this line to your application's Gemfile:
68
68
  gem 'claude-agent-sdk', github: 'ya-luotao/claude-agent-sdk-ruby'
69
69
 
70
70
  # Or use a stable version from RubyGems
71
- gem 'claude-agent-sdk', '~> 0.21.0'
71
+ gem 'claude-agent-sdk', '~> 0.22.0'
72
72
  ```
73
73
 
74
74
  Then `bundle install`, or install directly: `gem install claude-agent-sdk`.
@@ -144,7 +144,7 @@ ClaudeAgentSDK.query(prompt: stream) do |message|
144
144
  end
145
145
  ```
146
146
 
147
- See [examples/streaming_input_example.rb](examples/streaming_input_example.rb) and [examples/quick_start.rb](examples/quick_start.rb).
147
+ See [examples/streaming_input_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/streaming_input_example.rb) and [examples/quick_start.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/quick_start.rb).
148
148
 
149
149
  ## `Client` — Bidirectional Sessions
150
150
 
@@ -220,49 +220,49 @@ options = ClaudeAgentSDK::ClaudeAgentOptions.new(
220
220
 
221
221
  | Example | Description |
222
222
  |---------|-------------|
223
- | [quick_start.rb](examples/quick_start.rb) | Basic `query()` usage with options |
224
- | [client_example.rb](examples/client_example.rb) | Interactive Client usage |
225
- | [message_types_example.rb](examples/message_types_example.rb) | Handling all 24 SDK message types |
226
- | [streaming_input_example.rb](examples/streaming_input_example.rb) | Streaming input for multi-turn conversations |
227
- | [session_resumption_example.rb](examples/session_resumption_example.rb) | Multi-turn conversations with session persistence |
228
- | [structured_output_example.rb](examples/structured_output_example.rb) | JSON schema structured output |
229
- | [error_handling_example.rb](examples/error_handling_example.rb) | Error handling with `AssistantMessage.error` |
230
- | [bare_mode_example.rb](examples/bare_mode_example.rb) | Minimal startup with `bare: true` |
231
- | [sandbox_example.rb](examples/sandbox_example.rb) | Full sandbox settings (network, filesystem, violations) |
223
+ | [quick_start.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/quick_start.rb) | Basic `query()` usage with options |
224
+ | [client_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/client_example.rb) | Interactive Client usage |
225
+ | [message_types_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/message_types_example.rb) | Handling all 24 SDK message types |
226
+ | [streaming_input_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/streaming_input_example.rb) | Streaming input for multi-turn conversations |
227
+ | [session_resumption_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/session_resumption_example.rb) | Multi-turn conversations with session persistence |
228
+ | [structured_output_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/structured_output_example.rb) | JSON schema structured output |
229
+ | [error_handling_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/error_handling_example.rb) | Error handling with `AssistantMessage.error` |
230
+ | [bare_mode_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/bare_mode_example.rb) | Minimal startup with `bare: true` |
231
+ | [sandbox_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/sandbox_example.rb) | Full sandbox settings (network, filesystem, violations) |
232
232
 
233
233
  ### MCP Servers
234
234
 
235
235
  | Example | Description |
236
236
  |---------|-------------|
237
- | [mcp_calculator.rb](examples/mcp_calculator.rb) | Custom tools with SDK MCP servers |
238
- | [mcp_resources_prompts_example.rb](examples/mcp_resources_prompts_example.rb) | MCP resources and prompts |
239
- | [http_mcp_server_example.rb](examples/http_mcp_server_example.rb) | HTTP/SSE MCP server configuration |
237
+ | [mcp_calculator.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/mcp_calculator.rb) | Custom tools with SDK MCP servers |
238
+ | [mcp_resources_prompts_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/mcp_resources_prompts_example.rb) | MCP resources and prompts |
239
+ | [http_mcp_server_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/http_mcp_server_example.rb) | HTTP/SSE MCP server configuration |
240
240
 
241
241
  ### Hooks & Permissions
242
242
 
243
243
  | Example | Description |
244
244
  |---------|-------------|
245
- | [hooks_example.rb](examples/hooks_example.rb) | Using hooks to control tool execution |
246
- | [advanced_hooks_example.rb](examples/advanced_hooks_example.rb) | Typed hook inputs/outputs |
247
- | [lifecycle_hooks_example.rb](examples/lifecycle_hooks_example.rb) | All 27 hook events |
248
- | [permission_callback_example.rb](examples/permission_callback_example.rb) | Dynamic tool permission control |
245
+ | [hooks_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/hooks_example.rb) | Using hooks to control tool execution |
246
+ | [advanced_hooks_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/advanced_hooks_example.rb) | Typed hook inputs/outputs |
247
+ | [lifecycle_hooks_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/lifecycle_hooks_example.rb) | All 27 hook events |
248
+ | [permission_callback_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/permission_callback_example.rb) | Dynamic tool permission control |
249
249
 
250
250
  ### Advanced
251
251
 
252
252
  | Example | Description |
253
253
  |---------|-------------|
254
- | [budget_control_example.rb](examples/budget_control_example.rb) | Budget control with `max_budget_usd` |
255
- | [fallback_model_example.rb](examples/fallback_model_example.rb) | Fallback model configuration |
256
- | [extended_thinking_example.rb](examples/extended_thinking_example.rb) | Extended thinking |
257
- | [e2b_transport_example.rb](examples/e2b_transport_example.rb) | Custom transport running CLI in an E2B microVM |
254
+ | [budget_control_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/budget_control_example.rb) | Budget control with `max_budget_usd` |
255
+ | [fallback_model_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/fallback_model_example.rb) | Fallback model configuration |
256
+ | [extended_thinking_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/extended_thinking_example.rb) | Extended thinking |
257
+ | [e2b_transport_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/e2b_transport_example.rb) | Custom transport running CLI in an E2B microVM |
258
258
 
259
259
  ### Observability & Rails
260
260
 
261
261
  | Example | Description |
262
262
  |---------|-------------|
263
- | [otel_langfuse_example.rb](examples/otel_langfuse_example.rb) | OpenTelemetry tracing with Langfuse backend |
264
- | [rails_actioncable_example.rb](examples/rails_actioncable_example.rb) | ActionCable streaming to frontend |
265
- | [rails_background_job_example.rb](examples/rails_background_job_example.rb) | Background jobs with session resumption |
263
+ | [otel_langfuse_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/otel_langfuse_example.rb) | OpenTelemetry tracing with Langfuse backend |
264
+ | [rails_actioncable_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/rails_actioncable_example.rb) | ActionCable streaming to frontend |
265
+ | [rails_background_job_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/rails_background_job_example.rb) | Background jobs with session resumption |
266
266
 
267
267
  ## Available Tools
268
268
 
data/docs/client.md CHANGED
@@ -76,7 +76,7 @@ client = ClaudeAgentSDK::Client.new(
76
76
 
77
77
  ### Reference: running `claude` inside an E2B sandbox
78
78
 
79
- [`examples/e2b_transport_example.rb`](../examples/e2b_transport_example.rb) is a working transport that runs the Claude Code CLI inside an [E2B](https://e2b.dev) Firecracker microVM instead of on your host. The wire protocol stays identical — only the I/O layer changes:
79
+ [`examples/e2b_transport_example.rb`](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/e2b_transport_example.rb) is a working transport that runs the Claude Code CLI inside an [E2B](https://e2b.dev) Firecracker microVM instead of on your host. The wire protocol stays identical — only the I/O layer changes:
80
80
 
81
81
  ```
82
82
  ClaudeAgentSDK::Client (host)
@@ -35,7 +35,7 @@ ClaudeAgentSDK.query(prompt: "Create a profile for a software engineer", options
35
35
  end
36
36
  ```
37
37
 
38
- See [examples/structured_output_example.rb](../examples/structured_output_example.rb).
38
+ See [examples/structured_output_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/structured_output_example.rb).
39
39
 
40
40
  ## Thinking Configuration
41
41
 
@@ -91,7 +91,7 @@ ClaudeAgentSDK.query(prompt: "Explain recursion", options: options) do |message|
91
91
  end
92
92
  ```
93
93
 
94
- See [examples/budget_control_example.rb](../examples/budget_control_example.rb).
94
+ See [examples/budget_control_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/budget_control_example.rb).
95
95
 
96
96
  ## Fallback Model
97
97
 
@@ -102,7 +102,7 @@ options = ClaudeAgentSDK::ClaudeAgentOptions.new(
102
102
  )
103
103
  ```
104
104
 
105
- See [examples/fallback_model_example.rb](../examples/fallback_model_example.rb).
105
+ See [examples/fallback_model_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/fallback_model_example.rb).
106
106
 
107
107
  ## Beta Features
108
108
 
@@ -155,7 +155,7 @@ options = ClaudeAgentSDK::ClaudeAgentOptions.new(
155
155
  )
156
156
  ```
157
157
 
158
- See [examples/sandbox_example.rb](../examples/sandbox_example.rb).
158
+ See [examples/sandbox_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/sandbox_example.rb).
159
159
 
160
160
  ## Bare Mode
161
161
 
@@ -186,7 +186,7 @@ options = ClaudeAgentSDK::ClaudeAgentOptions.new(
186
186
 
187
187
  **What still works:** skills (via `/skill-name`), explicit `--add-dir` CLAUDE.md, `--settings`, `--mcp-config`, `--agents`, `--plugin-dir`, API key from `ANTHROPIC_API_KEY` env var.
188
188
 
189
- See [examples/bare_mode_example.rb](../examples/bare_mode_example.rb).
189
+ See [examples/bare_mode_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/bare_mode_example.rb).
190
190
 
191
191
  ## File Checkpointing & Rewind
192
192
 
data/docs/errors.md CHANGED
@@ -18,7 +18,7 @@ ClaudeAgentSDK.query(prompt: "Hello") do |message|
18
18
  end
19
19
  ```
20
20
 
21
- See [examples/error_handling_example.rb](../examples/error_handling_example.rb).
21
+ See [examples/error_handling_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/error_handling_example.rb).
22
22
 
23
23
  ## Exception Handling
24
24
 
@@ -92,4 +92,4 @@ end
92
92
  | `CLIJSONDecodeError` | JSON parsing issues |
93
93
  | `MessageParseError` | Message parsing issues |
94
94
 
95
- See [lib/claude_agent_sdk/errors.rb](../lib/claude_agent_sdk/errors.rb) for all error types.
95
+ See [lib/claude_agent_sdk/errors.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/lib/claude_agent_sdk/errors.rb) for all error types.
@@ -19,7 +19,7 @@ All hook input objects include common fields like `session_id`, `transcript_path
19
19
  - `SubagentStart` → `SubagentStartHookInput` (`agent_id`, `agent_type`)
20
20
  - `PermissionRequest` → `PermissionRequestHookInput` (`tool_name`, `tool_input`, `permission_suggestions`)
21
21
 
22
- All 27 hook events have typed input classes. See [`ClaudeAgentSDK::HOOK_EVENTS`](../lib/claude_agent_sdk/types.rb) and [examples/lifecycle_hooks_example.rb](../examples/lifecycle_hooks_example.rb).
22
+ All 27 hook events have typed input classes. See [`ClaudeAgentSDK::HOOK_EVENTS`](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/lib/claude_agent_sdk/types.rb) and [examples/lifecycle_hooks_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/lifecycle_hooks_example.rb).
23
23
 
24
24
  ### Example: Blocking Dangerous Commands
25
25
 
@@ -67,7 +67,7 @@ Async do
67
67
  end.wait
68
68
  ```
69
69
 
70
- See [examples/hooks_example.rb](../examples/hooks_example.rb), [examples/advanced_hooks_example.rb](../examples/advanced_hooks_example.rb), and [examples/lifecycle_hooks_example.rb](../examples/lifecycle_hooks_example.rb).
70
+ See [examples/hooks_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/hooks_example.rb), [examples/advanced_hooks_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/advanced_hooks_example.rb), and [examples/lifecycle_hooks_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/lifecycle_hooks_example.rb).
71
71
 
72
72
  ## Permission Callbacks
73
73
 
@@ -107,4 +107,4 @@ Async do
107
107
  end.wait
108
108
  ```
109
109
 
110
- See [examples/permission_callback_example.rb](../examples/permission_callback_example.rb).
110
+ See [examples/permission_callback_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/permission_callback_example.rb).
data/docs/mcp-servers.md CHANGED
@@ -150,4 +150,4 @@ server = ClaudeAgentSDK.create_sdk_mcp_server(
150
150
  )
151
151
  ```
152
152
 
153
- See [examples/mcp_calculator.rb](../examples/mcp_calculator.rb) and [examples/mcp_resources_prompts_example.rb](../examples/mcp_resources_prompts_example.rb) for complete examples.
153
+ See [examples/mcp_calculator.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/mcp_calculator.rb) and [examples/mcp_resources_prompts_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/mcp_resources_prompts_example.rb) for complete examples.
@@ -147,4 +147,4 @@ end
147
147
  options = ClaudeAgentSDK::ClaudeAgentOptions.new(observers: [MyObserver.new])
148
148
  ```
149
149
 
150
- See [examples/otel_langfuse_example.rb](../examples/otel_langfuse_example.rb) for a complete multi-tool example.
150
+ See [examples/otel_langfuse_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/otel_langfuse_example.rb) for a complete multi-tool example.
data/docs/rails.md CHANGED
@@ -193,7 +193,7 @@ end
193
193
  Then every `ClaudeAgentSDK.query` and `Client` session automatically gets traced — no per-call wiring needed. The lambda factory ensures each request gets its own observer with isolated span state, safe for concurrent Puma/Sidekiq workers.
194
194
 
195
195
  See:
196
- - [examples/rails_actioncable_example.rb](../examples/rails_actioncable_example.rb)
197
- - [examples/rails_background_job_example.rb](../examples/rails_background_job_example.rb)
198
- - [examples/session_resumption_example.rb](../examples/session_resumption_example.rb)
199
- - [examples/http_mcp_server_example.rb](../examples/http_mcp_server_example.rb)
196
+ - [examples/rails_actioncable_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/rails_actioncable_example.rb)
197
+ - [examples/rails_background_job_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/rails_background_job_example.rb)
198
+ - [examples/session_resumption_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/session_resumption_example.rb)
199
+ - [examples/http_mcp_server_example.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/http_mcp_server_example.rb)
data/docs/sessions.md CHANGED
@@ -172,7 +172,7 @@ ClaudeAgentSDK::Testing.run_session_store_conformance(-> { MyStore.new(...) })
172
172
  ```
173
173
 
174
174
  Copy-in reference adapters for **S3, Redis, and Postgres** live in
175
- [`examples/session_stores/`](../examples/session_stores/README.md), each with a
175
+ [`examples/session_stores/`](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/session_stores/README.md), each with a
176
176
  production checklist.
177
177
 
178
178
  ### Store-backed helpers
data/docs/types.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Types Reference
2
2
 
3
- See [lib/claude_agent_sdk/types.rb](../lib/claude_agent_sdk/types.rb) for complete type definitions.
3
+ See [lib/claude_agent_sdk/types.rb](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/lib/claude_agent_sdk/types.rb) for complete type definitions.
4
4
 
5
5
  ## Message Types
6
6
 
@@ -50,16 +50,32 @@ module ClaudeAgentSDK
50
50
  @root_span = nil
51
51
  @root_context = nil
52
52
  @tool_spans = {} # tool_use_id => span
53
- @first_user_input = nil # capture first user prompt for trace input
53
+ @first_user_input = nil # first user prompt of the current trace
54
+ @pending_prompt = nil # prompt that belongs to the NEXT trace (see on_user_prompt)
54
55
  @last_assistant_text = nil # capture last assistant text for trace output
55
56
  end
56
57
 
57
58
  def on_user_prompt(prompt)
58
- return if @first_user_input # only capture the first prompt
59
-
60
- @first_user_input = prompt.to_s
61
- # If root span already exists, set immediately; otherwise start_trace will apply it
62
- @root_span&.set_attribute('input.value', truncate(@first_user_input)) unless @first_user_input.empty?
59
+ text = prompt.to_s
60
+ return if text.empty?
61
+
62
+ if @root_span.nil?
63
+ # Pre-init: buffered for the trace start_trace is about to open.
64
+ # First prompt wins as the trace input.
65
+ @first_user_input ||= text
66
+ elsif @first_user_input.nil?
67
+ # Open trace with no input yet (init arrived before the prompt).
68
+ @first_user_input = text
69
+ @root_span.set_attribute('input.value', truncate(text))
70
+ else
71
+ # The open trace already has its input, so this prompt belongs to
72
+ # the NEXT trace: either the current turn was interrupted (/clear —
73
+ # a superseding init follows with no ResultMessage) or the user
74
+ # queued the next turn mid-stream (its init follows the current
75
+ # ResultMessage). A first-prompt-forever latch here dropped these
76
+ # prompts from the new trace entirely.
77
+ @pending_prompt ||= text
78
+ end
63
79
  end
64
80
 
65
81
  def on_message(message)
@@ -96,6 +112,10 @@ module ClaudeAgentSDK
96
112
  def on_close
97
113
  finish_open_spans
98
114
  reset_session_buffers
115
+ # Unlike the per-trace buffers, the pending next-trace prompt survives
116
+ # end_trace/supersede resets by design — but the session is over now,
117
+ # and a reused observer must not leak it into the next session.
118
+ @pending_prompt = nil
99
119
  end
100
120
 
101
121
  private
@@ -112,6 +132,11 @@ module ClaudeAgentSDK
112
132
  superseding = !@root_span.nil?
113
133
  finish_open_spans
114
134
  reset_session_buffers if superseding
135
+ # A prompt that arrived while the previous trace already had its input
136
+ # was buffered for THIS trace; it wins over a younger post-end prompt
137
+ # because it came first chronologically.
138
+ @first_user_input = @pending_prompt if @pending_prompt
139
+ @pending_prompt = nil
115
140
 
116
141
  attrs = {
117
142
  # gen_ai semantic conventions (recognized by Langfuse, Datadog, etc.)
@@ -25,16 +25,23 @@ module ClaudeAgentSDK
25
25
  end
26
26
 
27
27
  # A prebuilt JSON Schema is detected by type == 'object' (String or Symbol)
28
- # AND a Hash properties value. Deliberately stricter than Python's rule:
29
- # Ruby's simple-schema idiom uses Symbols as type VALUES, so
28
+ # with properties either ABSENT or a Hash. Properties-present-but-not-Hash
29
+ # falls through to the simple idiom, which uses Symbols as type VALUES
30
30
  # { type: :string, properties: :string } is a legal simple schema with
31
- # params literally named type/properties.
31
+ # params literally named type/properties. Propertyless object schemas
32
+ # ({ type: 'object', additionalProperties: ... } / oneOf / empty-object
33
+ # accept-anything forms) previously fell into the simple branch and were
34
+ # mangled into nonsense parameter lists ("additionalProperties" as a
35
+ # required string param). A $ref-only schema without type: 'object' remains
36
+ # indistinguishable from a params hash — declare the type alongside $ref.
32
37
  def self.prebuilt_json_schema?(schema)
33
38
  return false unless schema.is_a?(Hash)
34
39
 
35
40
  type_val = schema[:type] || schema['type']
36
- props_val = schema[:properties] || schema['properties']
37
- (type_val.is_a?(String) || type_val.is_a?(Symbol)) && type_val.to_s == 'object' && props_val.is_a?(Hash)
41
+ return false unless (type_val.is_a?(String) || type_val.is_a?(Symbol)) && type_val.to_s == 'object'
42
+ return true unless schema.key?(:properties) || schema.key?('properties')
43
+
44
+ (schema[:properties] || schema['properties']).is_a?(Hash)
38
45
  end
39
46
 
40
47
  # Single source of truth for tool input schemas: prebuilt schemas are
@@ -168,9 +168,15 @@ module ClaudeAgentSDK
168
168
  end
169
169
  return nil if sessions.nil? || sessions.empty?
170
170
 
171
+ sidechain_flags = sidechain_flags_from_summaries(store, project_key, timeout_s)
172
+
171
173
  sessions.sort_by { |s| -sortable_mtime(s['mtime']) }.each do |cand|
172
174
  sid = cand['session_id']
173
175
  next unless sid.is_a?(String) && sid.match?(Sessions::UUID_RE)
176
+ # Skip known sidechains without downloading their transcript: the
177
+ # newest keys are often sidechains, and full-loading each one made
178
+ # --continue O(sum of transcript sizes) instead of O(candidates).
179
+ next if sidechain_flags&.fetch(sid, false)
174
180
 
175
181
  loaded = load_candidate(store, project_key, sid, timeout_s)
176
182
  next if loaded.nil?
@@ -183,6 +189,26 @@ module ClaudeAgentSDK
183
189
  nil
184
190
  end
185
191
 
192
+ # session_id => true for sessions the summary sidecar marks as sidechains;
193
+ # nil when the store doesn't implement list_session_summaries or the call
194
+ # fails (callers then fall back to checking each full load). The per-load
195
+ # isSidechain check above stays even on the summary path: a missing or
196
+ # stale sidecar row costs one extra load, never a wrong resume.
197
+ def sidechain_flags_from_summaries(store, project_key, timeout_s)
198
+ return nil unless SessionStore.implements?(store, :list_session_summaries)
199
+
200
+ rows = with_timeout(timeout_s, 'SessionStore#list_session_summaries') do
201
+ store.list_session_summaries(project_key)
202
+ end
203
+ Array(rows).each_with_object({}) do |row, acc|
204
+ sid = row.is_a?(Hash) ? row['session_id'] : nil
205
+ acc[sid] = row.dig('data', 'is_sidechain') == true if sid
206
+ end
207
+ # NotImplementedError is a ScriptError that with_timeout does not wrap.
208
+ rescue StandardError, NotImplementedError
209
+ nil
210
+ end
211
+
186
212
  # Adapters contractually report mtime as an epoch-ms Numeric (the
187
213
  # conformance suite asserts it), but SQL timestamps naturally arrive as
188
214
  # ISO-8601 Strings through JSON. Unary minus on a String is String#-@
@@ -373,14 +373,26 @@ module ClaudeAgentSDK
373
373
  head, tail = read_head_tail(file_path, stat.size)
374
374
 
375
375
  # Check first line for sidechain
376
- first_line = head.lines.first || ''
377
- return nil if first_line.include?('"isSidechain":true') || first_line.include?('"isSidechain": true')
376
+ return nil if sidechain_first_line?(head.lines.first || '')
378
377
 
379
378
  build_session_info(file_path, head, tail, stat, project_path)
380
379
  rescue StandardError
381
380
  nil
382
381
  end
383
382
 
383
+ # Sidechain classification parses the first line and reads the top-level
384
+ # key — the same key the store fold reads (`entry['isSidechain'] == true`).
385
+ # The old raw substring scan also matched "isSidechain":true nested inside
386
+ # a structured field, hiding the session from disk listings only. A first
387
+ # line truncated by the read window can't be shape-checked and keeps the
388
+ # substring heuristic.
389
+ def sidechain_first_line?(line)
390
+ entry = JSON.parse(line)
391
+ entry.is_a?(Hash) && entry['isSidechain'] == true
392
+ rescue StandardError
393
+ line.include?('"isSidechain":true') || line.include?('"isSidechain": true')
394
+ end
395
+
384
396
  def read_head_tail(file_path, size)
385
397
  head = tail = nil
386
398
  File.open(file_path, 'rb') do |f|
@@ -974,11 +986,23 @@ module ClaudeAgentSDK
974
986
  # encoding: transcripts are UTF-8 regardless of locale; without it a
975
987
  # LANG=C process raises Encoding::InvalidByteSequenceError on the first
976
988
  # multibyte line, aborting the import mid-way (Python pins utf-8 here).
977
- File.foreach(file_path, encoding: 'UTF-8') do |line|
989
+ File.foreach(file_path, encoding: 'UTF-8').with_index(1) do |line, lineno|
978
990
  line = line.chomp
979
991
  next if line.empty?
980
992
 
981
- batch << JSON.parse(line)
993
+ begin
994
+ entry = JSON.parse(line)
995
+ rescue JSON::ParserError
996
+ # A truncated trailing line is an ordinary interrupted-CLI artifact;
997
+ # every read path tolerates it (parse_jsonl_entries skips bad
998
+ # lines), and raising here aborted mid-import, leaving a partial
999
+ # store import behind. Skip the line, but say so — import is an
1000
+ # explicit user operation.
1001
+ warn "Claude SDK: import_session_to_store skipped unparseable line #{lineno} of #{file_path}"
1002
+ next
1003
+ end
1004
+
1005
+ batch << entry
982
1006
  nbytes += line.bytesize
983
1007
  next unless batch.length >= batch_size || nbytes >= TranscriptMirrorBatcher::MAX_PENDING_BYTES
984
1008
 
@@ -13,10 +13,13 @@ module ClaudeAgentSDK
13
13
 
14
14
  module_function
15
15
 
16
- # Assert the 15 SessionStore behavioral contracts against an adapter.
16
+ # Assert the 16 SessionStore behavioral contracts against an adapter.
17
17
  #
18
18
  # Contracts 1-14 mirror the Python SDK's run_session_store_conformance.
19
- # Contract 15 is a Ruby SDK extension locking empty-subpath delete
19
+ # Contract 16 (Ruby extension) locks one-row-per-session `list_sessions`
20
+ # under multiple appends — the naive one-row-per-append implementation
21
+ # passed every other contract and then showed N duplicate sessions in
22
+ # pickers. Contract 15 is a Ruby SDK extension locking empty-subpath delete
20
23
  # coherence ('' == no subpath, the same addressing append/load already
21
24
  # use in every implementation in both SDKs); it runs only for stores
22
25
  # implementing #delete, and skip_optional: %w[delete] excludes the
@@ -38,7 +41,15 @@ module ClaudeAgentSDK
38
41
  # @param skip_optional [Array<String>] optional method names to skip.
39
42
  # Contracts for an optional method are also skipped automatically when the
40
43
  # store does not override it.
41
- def run_session_store_conformance(make_store, skip_optional: [])
44
+ # @param check_uuid_dedupe [Boolean] additionally assert the ADVISORY
45
+ # uuid-dedupe recommendation: re-appending a batch that overlaps a prior
46
+ # write (exactly what the mirror batcher's retry can produce) must not
47
+ # duplicate entries sharing a uuid. The SessionStore contract phrases
48
+ # this as a "should" and the shipped reference adapters deliberately
49
+ # skip it (duplicates largely self-heal through last-wins summary folds,
50
+ # though resumed transcripts can still show repeated turns), so this
51
+ # defaults to off. Turn it on if your adapter dedupes.
52
+ def run_session_store_conformance(make_store, skip_optional: [], check_uuid_dedupe: false)
42
53
  skip_optional = skip_optional.map(&:to_s)
43
54
  invalid = skip_optional - OPTIONAL_METHODS
44
55
  raise ConformanceError, "unknown optional methods in skip_optional: #{invalid}" unless invalid.empty?
@@ -56,6 +67,7 @@ module ClaudeAgentSDK
56
67
  check_list_session_summaries(fresh, has_list_sessions, has_delete) if has_list_summaries
57
68
  check_delete(fresh, has_list_subkeys, has_list_sessions) if has_delete
58
69
  check_list_subkeys(fresh) if has_list_subkeys
70
+ check_uuid_dedupe_contract(fresh) if check_uuid_dedupe
59
71
  nil
60
72
  end
61
73
 
@@ -86,11 +98,21 @@ module ClaudeAgentSDK
86
98
  entry('uuid' => 'm', 'n' => 3), entry('uuid' => 'b', 'n' => 4)],
87
99
  'multiple appends must preserve call order')
88
100
 
89
- # 4. append([]) is a no-op.
101
+ # 4. append([]) is a no-op — including on a never-written key, which
102
+ # must NOT come into existence (a phantom key surfaces as an empty
103
+ # session in listings; documented on InMemorySessionStore, previously
104
+ # never asserted).
90
105
  store = fresh.call
91
106
  store.append(key, [entry('uuid' => 'a', 'n' => 1)])
92
107
  store.append(key, [])
93
108
  assert_eq(store.load(key), [entry('uuid' => 'a', 'n' => 1)], 'append([]) must be a no-op')
109
+ phantom = { 'project_key' => key['project_key'], 'session_id' => 'phantom' }
110
+ store.append(phantom, [])
111
+ assert(store.load(phantom).nil?, 'append([]) to an unwritten key must not create it')
112
+ if has_list_sessions
113
+ listed = store.list_sessions(key['project_key']).map { |s| s['session_id'] }
114
+ assert(!listed.include?('phantom'), 'append([]) must not surface a phantom session in list_sessions')
115
+ end
94
116
 
95
117
  # 5. subpath keys are stored independently of main.
96
118
  store = fresh.call
@@ -134,6 +156,16 @@ module ClaudeAgentSDK
134
156
  [entry('n' => 1)])
135
157
  assert_eq(store.list_sessions('proj').map { |s| s['session_id'] }, ['main'],
136
158
  'list_sessions must exclude subagent subpaths')
159
+
160
+ # 16. one row per session regardless of how many appends built it. A
161
+ # one-row-per-append implementation passed every other contract (the
162
+ # multi-append guard existed only for list_session_summaries) and then
163
+ # showed N duplicate sessions in pickers.
164
+ store = fresh.call
165
+ multi = { 'project_key' => 'proj', 'session_id' => 'multi' }
166
+ 3.times { |i| store.append(multi, [entry('uuid' => "u#{i}", 'n' => i)]) }
167
+ assert_eq(store.list_sessions('proj').map { |s| s['session_id'] }, ['multi'],
168
+ 'list_sessions must return exactly one row per session after multiple appends')
137
169
  end
138
170
 
139
171
  # -- Optional: list_session_summaries ----------------------------------
@@ -259,6 +291,19 @@ module ClaudeAgentSDK
259
291
  'list_subkeys of an unknown session must be empty')
260
292
  end
261
293
 
294
+ # -- Opt-in: uuid dedupe (advisory) --------------------------------------
295
+
296
+ # A retried mirror batch can overlap a prior write (timeout abandons the
297
+ # in-flight append, which may still land). Adapters that opt in promise
298
+ # load reflects each uuid once, in first-seen order.
299
+ def check_uuid_dedupe_contract(fresh)
300
+ store = fresh.call
301
+ store.append(key, [entry('uuid' => 'r1', 'n' => 1), entry('uuid' => 'r2', 'n' => 2)])
302
+ store.append(key, [entry('uuid' => 'r2', 'n' => 2), entry('uuid' => 'r3', 'n' => 3)]) # retry overlap
303
+ assert_eq(store.load(key).map { |e| e['uuid'] }, %w[r1 r2 r3],
304
+ 'a retried batch overlapping a prior write must dedupe by entry uuid')
305
+ end
306
+
262
307
  # -- helpers -----------------------------------------------------------
263
308
 
264
309
  def key
@@ -303,7 +348,7 @@ module ClaudeAgentSDK
303
348
  end
304
349
 
305
350
  private_class_method :check_append_and_load, :check_list_sessions, :check_list_session_summaries,
306
- :check_delete, :check_list_subkeys, :key, :entry, :epoch_ms?, :optional?,
307
- :summaries_by_id, :assert, :assert_eq
351
+ :check_delete, :check_list_subkeys, :check_uuid_dedupe_contract, :key, :entry,
352
+ :epoch_ms?, :optional?, :summaries_by_id, :assert, :assert_eq
308
353
  end
309
354
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeAgentSDK
4
- VERSION = '0.21.0'
4
+ VERSION = '0.22.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: claude-agent-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.21.0
4
+ version: 0.22.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Community Contributors