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 +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +26 -26
- data/docs/client.md +1 -1
- data/docs/configuration.md +5 -5
- data/docs/errors.md +2 -2
- data/docs/hooks-and-permissions.md +3 -3
- data/docs/mcp-servers.md +1 -1
- data/docs/observability.md +1 -1
- data/docs/rails.md +4 -4
- data/docs/sessions.md +1 -1
- data/docs/types.md +1 -1
- data/lib/claude_agent_sdk/instrumentation/otel.rb +31 -6
- data/lib/claude_agent_sdk/sdk_mcp_server.rb +12 -5
- data/lib/claude_agent_sdk/session_resume.rb +26 -0
- data/lib/claude_agent_sdk/sessions.rb +28 -4
- data/lib/claude_agent_sdk/testing/session_store_conformance.rb +51 -6
- data/lib/claude_agent_sdk/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b739ecaa08f1ddc9bc97ffdf639deca97a69da0251ec7b4ec975c7994c76c4b7
|
|
4
|
+
data.tar.gz: c85fe94b25b9b18c7de7d842cdeef7d0059c40cf829f33a963a4c6a215b4f999
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-

|
|
3
|
+

|
|
4
4
|
|
|
5
5
|
[](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.
|
|
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`](
|
|
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)
|
data/docs/configuration.md
CHANGED
|
@@ -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](
|
|
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](
|
|
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](
|
|
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](
|
|
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](
|
|
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](
|
|
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](
|
|
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`](
|
|
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](
|
|
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](
|
|
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](
|
|
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.
|
data/docs/observability.md
CHANGED
|
@@ -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](
|
|
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](
|
|
197
|
-
- [examples/rails_background_job_example.rb](
|
|
198
|
-
- [examples/session_resumption_example.rb](
|
|
199
|
-
- [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/`](
|
|
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](
|
|
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 #
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
#
|
|
29
|
-
#
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|