claude-agent-sdk 0.35.0 → 0.37.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 +74 -0
- data/README.md +17 -8
- data/docs/cli-installer.md +16 -2
- data/docs/client.md +44 -4
- data/docs/errors.md +15 -1
- data/docs/hooks-and-permissions.md +27 -3
- data/docs/mcp-servers.md +36 -7
- data/docs/rails.md +3 -4
- data/docs/sessions.md +149 -34
- data/docs/types.md +106 -4
- data/lib/claude_agent_sdk/cancellation_signal.rb +2 -1
- data/lib/claude_agent_sdk/cli_installer.rb +68 -11
- data/lib/claude_agent_sdk/command_builder.rb +109 -98
- data/lib/claude_agent_sdk/deprecation.rb +90 -0
- data/lib/claude_agent_sdk/errors.rb +8 -0
- data/lib/claude_agent_sdk/fiber_boundary.rb +113 -2
- data/lib/claude_agent_sdk/instrumentation/otel.rb +15 -7
- data/lib/claude_agent_sdk/message_parser.rb +23 -9
- data/lib/claude_agent_sdk/observer.rb +2 -1
- data/lib/claude_agent_sdk/option_warnings.rb +2 -2
- data/lib/claude_agent_sdk/query.rb +99 -51
- data/lib/claude_agent_sdk/railtie.rb +14 -3
- data/lib/claude_agent_sdk/sdk_mcp_server.rb +58 -38
- data/lib/claude_agent_sdk/session_mutations.rb +28 -16
- data/lib/claude_agent_sdk/session_resume.rb +39 -35
- data/lib/claude_agent_sdk/session_store.rb +35 -21
- data/lib/claude_agent_sdk/session_summary.rb +12 -5
- data/lib/claude_agent_sdk/sessions.rb +112 -24
- data/lib/claude_agent_sdk/streaming.rb +1 -1
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +75 -52
- data/lib/claude_agent_sdk/tasks/claude_agent_sdk.rake +12 -5
- data/lib/claude_agent_sdk/testing/session_store_conformance.rb +15 -11
- data/lib/claude_agent_sdk/transcript_mirror_batcher.rb +19 -1
- data/lib/claude_agent_sdk/types/attributes.rb +271 -0
- data/lib/claude_agent_sdk/types/base.rb +320 -0
- data/lib/claude_agent_sdk/types/content_blocks.rb +57 -0
- data/lib/claude_agent_sdk/types/hooks.rb +640 -0
- data/lib/claude_agent_sdk/types/mcp.rb +232 -0
- data/lib/claude_agent_sdk/types/messages.rb +614 -0
- data/lib/claude_agent_sdk/types/option_values.rb +302 -0
- data/lib/claude_agent_sdk/types/options.rb +352 -0
- data/lib/claude_agent_sdk/types/permissions.rb +107 -0
- data/lib/claude_agent_sdk/types/sessions.rb +10 -0
- data/lib/claude_agent_sdk/types.rb +13 -2534
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +308 -73
- data/lib/generators/claude_agent_sdk/install/templates/claude_agent_sdk.rb.tt +3 -3
- metadata +12 -1
data/docs/sessions.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Session Browsing & Mutations
|
|
2
2
|
|
|
3
|
-
Browse, read, mutate, fork, and resume Claude Code sessions directly from Ruby — no CLI subprocess required.
|
|
3
|
+
Browse, read, mutate, fork, and resume Claude Code sessions directly from Ruby — no CLI subprocess required. By default these APIs read and write `~/.claude/projects/` JSONL files directly, respecting the `CLAUDE_CONFIG_DIR` environment variable (an empty value is treated as unset, falling back to `~/.claude`) and auto-detecting git worktrees. On a host with no usable home directory (`HOME` unset with no passwd entry — e.g. `docker --user` in a minimal image — or an empty/relative `HOME`) and no `CLAUDE_CONFIG_DIR`, the local-disk path raises `ClaudeAgentSDK::ConfigDirError`; set `CLAUDE_CONFIG_DIR` there. Every one of them also takes an optional `session_store:` to operate on a [`SessionStore`](#mirroring-to-a-sessionstore) instead (see [Store-backed sessions](#store-backed-sessions)).
|
|
4
4
|
|
|
5
|
-
Not-found semantics: the read APIs return `[]`/`nil` for unknown sessions and for directories that do not exist or have no recorded sessions. An explicit `directory:` strictly scopes the search to that project and its git worktrees — there is no cross-project fallback (pass `directory: nil` to search all projects). 0-byte transcript stubs are skipped during session-file resolution. Ids are validated at the boundary: a `session_id` that is not a UUID String, or an `agent_id` that is not a String of `[A-Za-z0-9._-]` characters (or is `.`/`..`), gets the same `[]`/`nil` as an unknown session (`import_session_to_store` raises `ArgumentError`), on the disk and store readers alike.
|
|
5
|
+
Not-found semantics: the read APIs return `[]`/`nil` for unknown sessions and for directories that do not exist or have no recorded sessions. An explicit `directory:` strictly scopes the search to that project and its git worktrees — there is no cross-project fallback (pass `directory: nil` to search all projects). 0-byte transcript stubs are skipped during session-file resolution. Ids are validated at the boundary: a `session_id` that is not a UUID String, or an `agent_id` that is not a String of `[A-Za-z0-9._-]` characters (or is `.`/`..`), gets the same `[]`/`nil` as an unknown session (`import_session_to_store` raises `ArgumentError`), on the disk and store readers alike. The mutations (`rename_session`, `tag_session`, `delete_session`, `fork_session`, with or without `session_store:`) apply the same check to `session_id` and `up_to_message_id` and raise `ArgumentError` (`Invalid session_id: ...`) for anything that is not a UUID String.
|
|
6
6
|
|
|
7
7
|
## Listing Sessions
|
|
8
8
|
|
|
@@ -25,7 +25,9 @@ ClaudeAgentSDK.list_sessions(directory: '.', include_worktrees: true)
|
|
|
25
25
|
|
|
26
26
|
Each `SDKSessionInfo` includes: `session_id`, `summary`, `last_modified`, `file_size`, `custom_title`, `first_prompt`, `git_branch`, `cwd`, `tag`, `created_at`.
|
|
27
27
|
|
|
28
|
-
Listings are newest first; sessions with the same `last_modified` are ordered by `session_id`, so `offset:`/`limit:` pages are stable across calls and the disk and store listings order identically. Blank (empty or whitespace-only) custom/AI titles, last-prompt and summary entries, `git_branch`, `cwd`, and `tag` values read as absent on both paths
|
|
28
|
+
Listings are newest first; sessions with the same `last_modified` are ordered by `session_id`, so `offset:`/`limit:` pages are stable across calls and the disk and store listings order identically. Blank (empty or whitespace-only) custom/AI titles, last-prompt and summary entries, `git_branch`, `cwd`, and `tag` values read as absent on both paths. `cwd` is the first non-blank top-level `cwd` in the transcript (a key nested in a tool input doesn't count), falling back to the project path; `first_prompt` is `nil` when the session has no usable prompt. `last_modified` is always Integer epoch milliseconds — the file mtime on disk, the adapter's `mtime` coerced as described under [Implementing an adapter](#implementing-an-adapter) on the store paths.
|
|
29
|
+
|
|
30
|
+
When `list_sessions` finds the same session in several project directories (copied config dirs, worktrees), it keeps one copy: the newest `last_modified`; on equal mtimes the larger file (the more complete copy); then the copy in the project directory whose name sorts first (worktree listings: the worktree `git worktree list` reports first, i.e. the main worktree).
|
|
29
31
|
|
|
30
32
|
## Reading Session Messages
|
|
31
33
|
|
|
@@ -38,7 +40,7 @@ messages.each { |msg| puts "[#{msg.type}] #{msg.message}" }
|
|
|
38
40
|
ClaudeAgentSDK.get_session_messages(session_id: 'abc-123-...', offset: 10, limit: 20)
|
|
39
41
|
```
|
|
40
42
|
|
|
41
|
-
Each `SessionMessage` includes `type` (`"user"` or `"assistant"`), `uuid`, `session_id`, and `message` (raw API hash).
|
|
43
|
+
Each `SessionMessage` includes `type` (`"user"` or `"assistant"`), `uuid`, `session_id`, and `message` (the raw API message Hash, read from the transcript, so its keys are Strings: `msg.message['content']`; see [Hash keys](types.md#hash-keys)).
|
|
42
44
|
|
|
43
45
|
## Reading Subagent Transcripts
|
|
44
46
|
|
|
@@ -49,7 +51,7 @@ ids = ClaudeAgentSDK.list_subagents(session_id: "uuid-here", directory: "/path/t
|
|
|
49
51
|
messages = ClaudeAgentSDK.get_subagent_messages(session_id: "uuid-here", agent_id: ids.first, limit: 50)
|
|
50
52
|
```
|
|
51
53
|
|
|
52
|
-
With `directory:` given, only that project and its git worktrees are searched (no global fallback).
|
|
54
|
+
With `directory:` given, only that project and its git worktrees are searched (no global fallback). Pass `session_store:` to read the subagents mirrored into a store instead.
|
|
53
55
|
|
|
54
56
|
> Each returned `SessionMessage` carries `parent_tool_use_id` — the id of the Agent `tool_use` block in the parent session that spawned this subagent — and `parent_agent_id`, the spawning subagent's id for nested subagents. Both are read from the `agent-<id>.meta.json` sidecar beside the transcript (or the `agent_metadata` entry in a `SessionStore`), and are `nil` when it is missing or unusable.
|
|
55
57
|
|
|
@@ -57,8 +59,8 @@ With `directory:` given, only that project and its git worktrees are searched (n
|
|
|
57
59
|
|
|
58
60
|
```ruby
|
|
59
61
|
meta = ClaudeAgentSDK.get_subagent_metadata(session_id: session_id, agent_id: agent_id, directory: project)
|
|
60
|
-
meta = ClaudeAgentSDK.
|
|
61
|
-
|
|
62
|
+
meta = ClaudeAgentSDK.get_subagent_metadata(
|
|
63
|
+
session_id: session_id, agent_id: agent_id, directory: project, session_store: store
|
|
62
64
|
)
|
|
63
65
|
meta&.dig('toolUseId') # spawning Agent tool call; not task_id
|
|
64
66
|
meta&.dig('parentAgentId')
|
|
@@ -211,6 +213,14 @@ ClaudeAgentSDK.query(
|
|
|
211
213
|
) { |message| }
|
|
212
214
|
```
|
|
213
215
|
|
|
216
|
+
The mirror maps each transcript file the CLI reports to a store key relative to
|
|
217
|
+
the subprocess's projects dir: `CLAUDE_CONFIG_DIR` from `options.env` (else
|
|
218
|
+
`ENV`), else `~/.claude` under the `HOME` the subprocess sees (`options.env`'s
|
|
219
|
+
`HOME` when it sets one). When neither exists — no `CLAUDE_CONFIG_DIR` and no
|
|
220
|
+
usable home — the session still runs, but nothing is mirrored: each unmappable
|
|
221
|
+
batch is reported as a `MirrorErrorMessage` (with a `nil` key) telling you to
|
|
222
|
+
set `CLAUDE_CONFIG_DIR`.
|
|
223
|
+
|
|
214
224
|
Relevant options: `session_store`, `session_store_flush` (`"batched"` default, or
|
|
215
225
|
`"eager"` to flush each frame as soon as the store is free — frames arriving
|
|
216
226
|
while an append is in flight are coalesced into the next append, so a slow
|
|
@@ -228,7 +238,8 @@ normal spawn path and `continue_conversation` moves on to the next candidate.
|
|
|
228
238
|
> **Store-backed resume runs against a temp `CLAUDE_CONFIG_DIR`.** The SDK
|
|
229
239
|
> materializes the session transcript (plus subagent transcripts, when the
|
|
230
240
|
> store implements `#list_subkeys`) into it and seeds it from your real config
|
|
231
|
-
> dir (`CLAUDE_CONFIG_DIR` from `options.env`/`ENV`, else `~/.claude`
|
|
241
|
+
> dir (`CLAUDE_CONFIG_DIR` from `options.env`/`ENV`, else `~/.claude` under the
|
|
242
|
+
> `HOME` the subprocess will see — `options.env`'s `HOME` when it sets one):
|
|
232
243
|
>
|
|
233
244
|
> - `.credentials.json`, with the OAuth `refreshToken` removed so the resumed
|
|
234
245
|
> subprocess can't consume it. On macOS with the default config dir and no
|
|
@@ -264,8 +275,11 @@ normal spawn path and `continue_conversation` moves on to the next candidate.
|
|
|
264
275
|
Subclass `ClaudeAgentSDK::SessionStore` (or duck-type it). Only `#append` and
|
|
265
276
|
`#load` are required; `#list_sessions`, `#delete`, `#list_subkeys`, and
|
|
266
277
|
`#list_session_summaries` are optional and probed via `SessionStore.implements?`.
|
|
267
|
-
Report `mtime` as epoch milliseconds; the SDK also
|
|
268
|
-
ISO-8601-string
|
|
278
|
+
Report `mtime` as epoch milliseconds; the SDK also accepts numeric-string,
|
|
279
|
+
ISO-8601-string and `Time` mtimes (ordering them correctly and reporting them
|
|
280
|
+
as Integer epoch ms in `last_modified`), but anything else sorts as oldest and
|
|
281
|
+
reads as `0`. `continue_conversation` picks the newest candidate by the same
|
|
282
|
+
rule as the listings (equal mtimes: lowest `session_id`). Subagent
|
|
269
283
|
transcripts arrive under a `subpath` key such as `subagents/agent-<agent_id>`
|
|
270
284
|
(or nested `subagents/workflows/<runId>/agent-<agent_id>`); on a store without
|
|
271
285
|
`#list_subkeys` the subagent readers build `subagents/agent-<agent_id>` from the
|
|
@@ -278,6 +292,65 @@ require 'claude_agent_sdk/testing/session_store_conformance'
|
|
|
278
292
|
ClaudeAgentSDK::Testing.run_session_store_conformance(-> { MyStore.new(...) })
|
|
279
293
|
```
|
|
280
294
|
|
|
295
|
+
Keys and entries cross the adapter boundary with **String** keys (see
|
|
296
|
+
[Hash keys](types.md#hash-keys)): a key is `{ 'project_key' => ..., 'session_id' => ... }`,
|
|
297
|
+
plus `'subpath'` for a subagent transcript, and entries are the raw JSONL
|
|
298
|
+
objects. Persist entries verbatim and treat `entry['uuid']` as an idempotency
|
|
299
|
+
key, since a retried or re-imported batch can repeat earlier writes.
|
|
300
|
+
|
|
301
|
+
#### Helpers for adapter authors
|
|
302
|
+
|
|
303
|
+
`ClaudeAgentSDK.project_key_for_directory(directory = nil)` returns the
|
|
304
|
+
`project_key` the SDK uses for a directory (a String or Pathname; `nil` means
|
|
305
|
+
the current working directory). It applies the CLI's project-directory naming
|
|
306
|
+
(realpath, Unicode NFC, then the CLI's sanitization), so a key you build
|
|
307
|
+
matches the keys of live-mirrored transcripts:
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
key = { 'project_key' => ClaudeAgentSDK.project_key_for_directory('/path/to/project'),
|
|
311
|
+
'session_id' => '550e8400-e29b-41d4-a716-446655440000' }
|
|
312
|
+
store.load(key)
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
`ClaudeAgentSDK.fold_session_summary(prev, key, entries)` maintains a
|
|
316
|
+
per-session summary incrementally, so an adapter can implement
|
|
317
|
+
`#list_session_summaries` and `list_sessions(session_store:)` can read every
|
|
318
|
+
session's metadata in one call instead of one `#load` per session. Call it
|
|
319
|
+
from `#append`:
|
|
320
|
+
|
|
321
|
+
```ruby
|
|
322
|
+
def append(key, entries)
|
|
323
|
+
return if entries.nil? || entries.empty?
|
|
324
|
+
|
|
325
|
+
write_entries(key, entries)
|
|
326
|
+
return unless key['subpath'].nil? # subagent transcripts never feed the summary
|
|
327
|
+
|
|
328
|
+
summary = ClaudeAgentSDK.fold_session_summary(read_summary(key), key, entries)
|
|
329
|
+
summary['mtime'] = write_time_ms # the clock #list_sessions reports
|
|
330
|
+
write_summary(key, summary)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def list_session_summaries(project_key)
|
|
334
|
+
read_summaries(project_key) # => [{ 'session_id' => ..., 'mtime' => ..., 'data' => {...} }, ...]
|
|
335
|
+
end
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
- `prev` is the summary you stored for the same key on the previous append,
|
|
339
|
+
or `nil` on the first one. `entries` are the entries being appended.
|
|
340
|
+
- It returns a new `{ 'session_id', 'mtime', 'data' }` Hash and leaves `prev`
|
|
341
|
+
unchanged. Every derived field is set-once or last-wins, so the fold never
|
|
342
|
+
needs earlier entries again.
|
|
343
|
+
- Only call it for main-transcript keys (no `'subpath'`).
|
|
344
|
+
- It does not set `mtime`: it carries `prev`'s value, or `0` for a new
|
|
345
|
+
session. Stamp it after persisting, from the same clock as the `mtime`
|
|
346
|
+
your `#list_sessions` returns. When the store also implements
|
|
347
|
+
`#list_sessions`, a summary whose `mtime` is older than the listed one is
|
|
348
|
+
treated as stale and the SDK re-derives it from the transcript.
|
|
349
|
+
- `data` is opaque. Store it as returned; its String keys survive a JSON
|
|
350
|
+
round-trip (JSONB, Redis).
|
|
351
|
+
|
|
352
|
+
`InMemorySessionStore#append` is a working reference.
|
|
353
|
+
|
|
281
354
|
Copy-in reference adapters for **S3, Redis, and Postgres** live in
|
|
282
355
|
[`examples/session_stores/`](https://github.com/ya-luotao/claude-agent-sdk-ruby/blob/main/examples/session_stores/README.md), each with a
|
|
283
356
|
production checklist.
|
|
@@ -340,30 +413,72 @@ thread for default adapters, inside the cooperative timeout for inline
|
|
|
340
413
|
declarers (the cancellation passes through the wrapper un-swallowed and the
|
|
341
414
|
wrapper's `ensure` runs at cancellation).
|
|
342
415
|
|
|
343
|
-
### Store-backed
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
`
|
|
347
|
-
|
|
348
|
-
- Reads: `list_sessions_from_store`, `get_session_info_from_store`,
|
|
349
|
-
`get_session_messages_from_store`, `list_subagents_from_store`,
|
|
350
|
-
`get_subagent_messages_from_store`. Unlike the disk readers (where a nil
|
|
351
|
-
`directory:` searches every project directory), the store helpers key every
|
|
352
|
-
read by `project_key` and a nil `directory:` defaults to the **current
|
|
353
|
-
working directory** — the `SessionStore` interface has no way to enumerate
|
|
354
|
-
project keys (parity with the Python SDK).
|
|
355
|
-
- Mutations: `rename_session_via_store`, `tag_session_via_store`,
|
|
356
|
-
`delete_session_via_store` (a no-op on append-only stores without `#delete`),
|
|
357
|
-
`fork_session_via_store`. Like their disk counterparts, rename, tag, and fork
|
|
358
|
-
raise `Errno::ENOENT` for a session the store has never seen (`#load`
|
|
359
|
-
returns nil or `[]`) instead of appending to — and so creating — a phantom
|
|
360
|
-
session. Rename/tag probe with one `#load` before appending; the probe is
|
|
361
|
-
check-then-act, so a session deleted concurrently between the probe and the
|
|
362
|
-
append can still be recreated by that append.
|
|
363
|
-
- Migration: `import_session_to_store` replays a local on-disk session (and its
|
|
364
|
-
subagents) into a store.
|
|
416
|
+
### Store-backed sessions
|
|
417
|
+
|
|
418
|
+
Every browsing/mutation function above takes an optional `session_store:`.
|
|
419
|
+
Omitted (or `nil`), it works on local disk as described above; given a store,
|
|
420
|
+
it operates on the store instead, with the same arguments:
|
|
365
421
|
|
|
366
422
|
```ruby
|
|
367
|
-
ClaudeAgentSDK.
|
|
368
|
-
|
|
423
|
+
ClaudeAgentSDK.list_sessions(session_store: store, limit: 10)
|
|
424
|
+
ClaudeAgentSDK.get_session_messages(session_id: '550e8400-...', session_store: store)
|
|
425
|
+
ClaudeAgentSDK.rename_session(session_id: '550e8400-...', title: 'Renamed', session_store: store)
|
|
426
|
+
forked = ClaudeAgentSDK.fork_session(session_id: '550e8400-...', session_store: store)
|
|
369
427
|
```
|
|
428
|
+
|
|
429
|
+
Where the store path differs from the disk path:
|
|
430
|
+
|
|
431
|
+
- **`directory: nil` means the current working directory.** The disk readers
|
|
432
|
+
search every project directory when `directory:` is nil; a store keys every
|
|
433
|
+
read and write by `project_key` and has no way to enumerate project keys
|
|
434
|
+
(parity with the Python SDK).
|
|
435
|
+
- **`include_worktrees:` is disk-only.** A store has no worktrees, so with
|
|
436
|
+
`session_store:` only the default `true` is accepted; `false` or `nil`
|
|
437
|
+
raises `ArgumentError` rather than being silently ignored.
|
|
438
|
+
- `list_sessions` uses the store's `#list_session_summaries` when implemented,
|
|
439
|
+
else `#list_sessions` plus one `#load` per listed session; a store with
|
|
440
|
+
neither raises `ArgumentError`. `list_subagents` requires `#list_subkeys`.
|
|
441
|
+
- Rename, tag, and fork raise `Errno::ENOENT` for a session the store has
|
|
442
|
+
never seen (`#load` returns nil or `[]`) instead of appending to — and so
|
|
443
|
+
creating — a phantom session, like their disk counterparts. Rename/tag probe
|
|
444
|
+
with one `#load` before appending; the probe is check-then-act, so a session
|
|
445
|
+
deleted concurrently between the probe and the append can still be
|
|
446
|
+
recreated by that append. The appended entries carry a fresh `uuid` and
|
|
447
|
+
`timestamp`, so adapters that dedupe by `uuid` treat them correctly.
|
|
448
|
+
- `delete_session` is a no-op on append-only stores without `#delete` (the
|
|
449
|
+
disk path raises `Errno::ENOENT` for an unknown session); whether subagent
|
|
450
|
+
entries are removed too depends on the store's delete cascade.
|
|
451
|
+
|
|
452
|
+
To migrate, `import_session_to_store` replays a local on-disk session (and its
|
|
453
|
+
subagents) into a store:
|
|
454
|
+
|
|
455
|
+
```ruby
|
|
456
|
+
ClaudeAgentSDK.import_session_to_store(
|
|
457
|
+
session_id: '550e8400-...',
|
|
458
|
+
session_store: store,
|
|
459
|
+
directory: '/path/to/project', # optional; nil searches every project
|
|
460
|
+
include_subagents: true, # default
|
|
461
|
+
batch_size: 500 # default
|
|
462
|
+
)
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
It streams the transcript and calls `store.append` once per batch. A batch ends
|
|
466
|
+
at `batch_size` entries (default **500**; `nil` or a non-positive value also
|
|
467
|
+
means 500) or at about 1 MiB of JSONL, whichever comes first. Entries are
|
|
468
|
+
keyed under the on-disk project directory name, so the imported session can
|
|
469
|
+
be resumed with `session_store:` + `resume:` from the original directory.
|
|
470
|
+
Re-importing appends the entries again, so adapters should dedupe by
|
|
471
|
+
`entry['uuid']`. It raises `ArgumentError` for an invalid `session_id` and
|
|
472
|
+
`Errno::ENOENT` when the transcript cannot be found; an unparseable line is
|
|
473
|
+
skipped with a warning.
|
|
474
|
+
|
|
475
|
+
> **Deprecated:** the separate store functions (`list_sessions_from_store`,
|
|
476
|
+
> `get_session_info_from_store`, `get_session_messages_from_store`,
|
|
477
|
+
> `list_subagents_from_store`, `get_subagent_metadata_from_store`,
|
|
478
|
+
> `get_subagent_messages_from_store`, `rename_session_via_store`,
|
|
479
|
+
> `tag_session_via_store`, `delete_session_via_store`,
|
|
480
|
+
> `fork_session_via_store`) still work unchanged but print a one-time
|
|
481
|
+
> deprecation warning and will be removed in 1.0. Replace
|
|
482
|
+
> `ClaudeAgentSDK.x_from_store(session_store: store, ...)` or
|
|
483
|
+
> `x_via_store(session_store: store, ...)` with
|
|
484
|
+
> `ClaudeAgentSDK.x(..., session_store: store)`.
|
data/docs/types.md
CHANGED
|
@@ -1,6 +1,81 @@
|
|
|
1
1
|
# Types Reference
|
|
2
2
|
|
|
3
|
-
See [lib/claude_agent_sdk/types
|
|
3
|
+
See [lib/claude_agent_sdk/types/](https://github.com/ya-luotao/claude-agent-sdk-ruby/tree/main/lib/claude_agent_sdk/types) for complete type definitions (one file per area; `types.rb` loads them all).
|
|
4
|
+
|
|
5
|
+
## Hash keys
|
|
6
|
+
|
|
7
|
+
Where the SDK hands you a plain Hash rather than a typed object, its key form
|
|
8
|
+
depends on where the data came from. One rule covers every case:
|
|
9
|
+
|
|
10
|
+
| Source | Key form | Examples |
|
|
11
|
+
|--------|----------|----------|
|
|
12
|
+
| The CLI's live stream-JSON, passed through as-is | **Symbols**, spelled exactly as on the wire | `UserMessage#origin`, `ResultMessage#origin`, `AssistantMessage#usage`, `ResultMessage#usage`, `ResultMessage#model_usage`, `ResultMessage#structured_output`, `UserMessage#tool_use_result`, `SystemMessage#data`, hook `tool_input`, the `can_use_tool` `input`, SDK MCP tool and prompt `args`, `Client#mcp_status`, `Client#context_usage` |
|
|
13
|
+
| Transcripts read from disk or a `SessionStore` | **Strings**, spelled as in the JSONL | `SessionMessage#message`, `get_subagent_metadata`, `SessionStore` keys and entries, `fold_session_summary` input and output |
|
|
14
|
+
|
|
15
|
+
"As on the wire" means the SDK does not rewrite key names. Structures the CLI
|
|
16
|
+
generates use camelCase (`origin[:fromSession]`, `model_usage` values'
|
|
17
|
+
`:inputTokens` / `:costUSD`, `client.mcp_status[:mcpServers]`), while objects
|
|
18
|
+
the CLI relays from the API keep their snake_case (`usage[:input_tokens]`).
|
|
19
|
+
Nesting follows the same rule all the way down, including keys that are data
|
|
20
|
+
rather than field names: `model_usage` is keyed by model-name Symbols
|
|
21
|
+
(`result.model_usage.each { |model, u| puts "#{model}: $#{u[:costUSD]}" }`).
|
|
22
|
+
|
|
23
|
+
The wrong key form reads as `nil` rather than raising, so look up the source
|
|
24
|
+
before indexing: `tool_input['command']` on a hook input, or
|
|
25
|
+
`meta[:toolUseId]` on subagent metadata, silently returns `nil`. The Python
|
|
26
|
+
SDK uses String keys everywhere; do not port its lookups literally.
|
|
27
|
+
|
|
28
|
+
The Symbol side of the rule relies on the transport parsing each line with
|
|
29
|
+
`JSON.parse(line, symbolize_names: true)`. The built-in
|
|
30
|
+
`SubprocessCLITransport` does; a [custom transport](client.md#custom-transport)
|
|
31
|
+
must too.
|
|
32
|
+
|
|
33
|
+
## Reading and writing attributes
|
|
34
|
+
|
|
35
|
+
The SDK's typed objects (messages, content blocks, hook inputs and outputs,
|
|
36
|
+
option objects: everything built on `ClaudeAgentSDK::Type`) accept the same
|
|
37
|
+
attribute name in several spellings. These accessors are public API:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
msg.session_id # the attr_accessor
|
|
41
|
+
msg[:session_id] # Symbol or String, snake_case or camelCase:
|
|
42
|
+
msg['session_id'] # all four read the same attribute
|
|
43
|
+
msg[:sessionId]
|
|
44
|
+
msg['sessionId']
|
|
45
|
+
msg.sessionId # camelCase reader (also answers respond_to?)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`#[]` returns `nil` for a name the type does not define, while a misspelled
|
|
49
|
+
method call such as `msg.nope` raises `NoMethodError`. These accessors reach a type's
|
|
50
|
+
**attributes** only; any other method reached this way warns in 0.37 and stops
|
|
51
|
+
working in 1.0 — see [Attributes Only](#attributes-only).
|
|
52
|
+
|
|
53
|
+
`#[]=` assigns through the attribute's setter, with the same name
|
|
54
|
+
normalization, and returns the assigned value:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
msg[:result] = 'edited' # same as msg.result = 'edited'
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- It **changes the object you received**. Messages are not frozen or copied
|
|
61
|
+
on delivery, so a change is visible to anything else holding the same
|
|
62
|
+
object (for example an observer that received it before your block did).
|
|
63
|
+
Copy first if you need the original.
|
|
64
|
+
- A name the type does not define is ignored on the types the SDK parses from
|
|
65
|
+
CLI output. `ClaudeAgentOptions` raises `ArgumentError` for an unknown key (as
|
|
66
|
+
its constructor and `dup_with` do), and the value types you build and pass in
|
|
67
|
+
warn once and will raise in 1.0 — see [Unknown Keys](#unknown-keys).
|
|
68
|
+
- Discriminator fields (`type` on the MCP server and system-prompt configs,
|
|
69
|
+
`behavior` on `PermissionResultAllow` / `PermissionResultDeny`,
|
|
70
|
+
`hook_event_name` on hook inputs and outputs) are read-only, so
|
|
71
|
+
assigning them has no effect.
|
|
72
|
+
|
|
73
|
+
Constructors accept the same spellings: `ResultMessage.new('sessionId' => 'abc')`
|
|
74
|
+
is equivalent to `ResultMessage.new(session_id: 'abc')`.
|
|
75
|
+
|
|
76
|
+
`SDKSessionInfo` and `SessionMessage` (returned by the session functions) are
|
|
77
|
+
currently plain classes, not `Type`s: use their snake_case accessors
|
|
78
|
+
(`info.session_id`); they have no `#[]`, `#[]=` or camelCase readers.
|
|
4
79
|
|
|
5
80
|
## Message Types
|
|
6
81
|
|
|
@@ -134,8 +209,9 @@ turn was cancelled via `Client#interrupt`. `nil` when the CLI did not report
|
|
|
134
209
|
one (older CLIs, or a result that bypassed the query loop such as a local
|
|
135
210
|
slash command).
|
|
136
211
|
|
|
137
|
-
`model_usage`
|
|
138
|
-
|
|
212
|
+
`model_usage` is passed through verbatim from the CLI (see [Hash keys](#hash-keys)):
|
|
213
|
+
it is keyed by model-name Symbols (`:"claude-sonnet-4-5"`), and each value's
|
|
214
|
+
keys are camelCase Symbols (the TypeScript/Python SDKs' `ModelUsage` shape): `inputTokens`,
|
|
139
215
|
`outputTokens`, `cacheReadInputTokens`, `cacheCreationInputTokens`,
|
|
140
216
|
`webSearchRequests`, `costUSD`, `contextWindow`, `maxOutputTokens`, plus
|
|
141
217
|
optional `canonicalModel` (canonical id used for the pricing lookup, which can
|
|
@@ -287,7 +363,7 @@ end
|
|
|
287
363
|
| `McpHttpServerConfig` | MCP server config for HTTP transport |
|
|
288
364
|
| `SdkPluginConfig` | SDK plugin configuration |
|
|
289
365
|
| `McpServerStatus` | Status of a single MCP server connection (with `.parse`) |
|
|
290
|
-
| `McpStatusResponse` |
|
|
366
|
+
| `McpStatusResponse` | Typed view of the `Client#mcp_status` / `#get_mcp_status` Hash: `McpStatusResponse.parse(client.mcp_status).mcp_servers` is an Array of `McpServerStatus`. The client itself returns the raw Hash (see [client.md](client.md#mcp-status-and-context-usage-return-hashes)) |
|
|
291
367
|
| `McpServerInfo` | MCP server name and version |
|
|
292
368
|
| `McpToolInfo` | MCP tool name, description, and annotations |
|
|
293
369
|
| `McpToolAnnotations` | MCP tool annotation hints (`read_only`, `destructive`, `open_world`) |
|
|
@@ -302,6 +378,32 @@ end
|
|
|
302
378
|
| `SystemPromptFile` | System prompt loaded from a file path |
|
|
303
379
|
| `ToolsPreset` | Tools preset configuration for base tools selection |
|
|
304
380
|
|
|
381
|
+
### Unknown Keys
|
|
382
|
+
|
|
383
|
+
`ClaudeAgentOptions` raises `ArgumentError` on an unknown key. The value types you build and pass *in* used to drop a misspelled key silently; they now print a warning, once per class and key, pointing at your call:
|
|
384
|
+
|
|
385
|
+
```
|
|
386
|
+
app/agents/reviewer.rb:12: warning: ClaudeAgentSDK::HookMatcher: unknown attribute :matchr ignored; this will raise ArgumentError in 1.0 (known: hooks, matcher, timeout)
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
**In 1.0 the same call raises `ArgumentError`.** This covers `.new` and `#[]=` on:
|
|
390
|
+
|
|
391
|
+
- option values: `AgentDefinition`, `SandboxSettings`, `SandboxNetworkConfig`, `SandboxFilesystemConfig`, `ThinkingConfigAdaptive` / `Enabled` / `Disabled`, `TaskBudget`, `SystemPromptPreset` / `Custom` / `File`, `ToolsPreset`, `SdkPluginConfig`, `McpStdioServerConfig`, `McpSSEServerConfig`, `McpHttpServerConfig`, `McpSdkServerConfig`
|
|
392
|
+
- `HookMatcher` and hook outputs: `SyncHookJSONOutput`, `AsyncHookJSONOutput`, every `*HookSpecificOutput`
|
|
393
|
+
- `PermissionResultAllow`, `PermissionResultDeny`, `PermissionUpdate`, `PermissionRuleValue`
|
|
394
|
+
|
|
395
|
+
Accepted without a warning: Symbol or String keys, snake_case or camelCase spellings, and the fixed discriminator a type sets itself (`type`, `hook_event_name`, `behavior`), so `klass.new(value.to_h)` round-trips. Types the SDK parses from CLI output (messages, content blocks, hook inputs, `ToolPermissionContext`, the MCP status types) stay lenient, so a field added by a newer CLI never warns, and so does every construction through `.from_hash` or `.wrap`. The warning goes through `Kernel#warn`, so `-W0` or `$VERBOSE = nil` silences it.
|
|
396
|
+
|
|
397
|
+
### Attributes Only
|
|
398
|
+
|
|
399
|
+
`#[]`, `#[]=` and the camelCase readers (`msg[:session_id]`, `msg['sessionId']`, `msg.sessionId`) are public API for a type's **attributes**: the fields it declares, plus predicates such as `options.forkSession?`. Methods your own code adds to a subclass (an `attr_accessor`, a hand-written reader or setter, a mixin's accessors, a singleton method) count as attributes too. Until now they reached any public method, so `msg[:to_h]` returned a Hash, `msg['freeze']` froze the message and `msg.toH` worked. Such a call still works in 0.37 but warns once per class and name:
|
|
400
|
+
|
|
401
|
+
```
|
|
402
|
+
app/jobs/sync.rb:8: warning: ClaudeAgentSDK::ResultMessage#[]: :to_h is not an attribute; Type#[] will only read attributes in 1.0
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
**In 1.0 a name that is not an attribute behaves like an undefined one:** `#[]` returns `nil`, `#[]=` ignores it (on the strict types above it raises `ArgumentError`), and a camelCase call raises `NoMethodError`. Call the method directly instead (`msg.to_h`). Undefined names already behave that way today and do not warn. `UserMessage#text` and `AssistantMessage#text` are convenience methods, not attributes.
|
|
406
|
+
|
|
305
407
|
## Constants
|
|
306
408
|
|
|
307
409
|
| Constant | Description |
|
|
@@ -30,16 +30,22 @@ module ClaudeAgentSDK
|
|
|
30
30
|
# ClaudeAgentSDK::CLIInstaller.install_pinned
|
|
31
31
|
# @example Pin a version of your own
|
|
32
32
|
# ClaudeAgentSDK::CLIInstaller.install(version: '2.1.220')
|
|
33
|
-
module CLIInstaller
|
|
33
|
+
module CLIInstaller # rubocop:disable Metrics/ModuleLength -- Http/Platform/Release/Metadata submodules in one file
|
|
34
|
+
# @api private
|
|
34
35
|
BASE_URL = 'https://downloads.claude.ai/claude-code-releases'
|
|
35
36
|
# Dist-tags resolved through a GET to BASE_URL/<tag>.
|
|
37
|
+
#
|
|
38
|
+
# @api private
|
|
36
39
|
DIST_TAGS = %w[stable latest].freeze
|
|
37
40
|
# Concrete version, optionally with a pre-release suffix (e.g. 2.1.220-rc1).
|
|
38
41
|
# The suffix is restricted to the semver pre-release character set: every
|
|
39
42
|
# accepted version is interpolated straight into a download URL, and a
|
|
40
43
|
# laxer `\S+` would let "2.1.220-x/../2.1.221" traverse out of the release
|
|
41
44
|
# path — silently installing something other than the pinned version.
|
|
45
|
+
#
|
|
46
|
+
# @api private
|
|
42
47
|
VERSION_PATTERN = /\A\d+\.\d+\.\d+(-[A-Za-z0-9.-]+)?\z/
|
|
48
|
+
# @api private
|
|
43
49
|
CHECKSUM_PATTERN = /\A[0-9a-f]{64}\z/
|
|
44
50
|
# The CLI version this gem release is developed and tested against — the
|
|
45
51
|
# Ruby equivalent of the Python SDK's bundled-CLI pin (_cli_version.py),
|
|
@@ -48,23 +54,35 @@ module ClaudeAgentSDK
|
|
|
48
54
|
# .github/workflows/cli-pin-bump.yml or a Python-sync release, so a
|
|
49
55
|
# Dependabot bump of the gem carries the CLI forward with it.
|
|
50
56
|
PINNED_CLI_VERSION = '2.1.280'
|
|
57
|
+
# @api private
|
|
51
58
|
BINARY_NAME = 'claude'
|
|
59
|
+
# @api private
|
|
52
60
|
VERSION_FILE = 'VERSION'
|
|
61
|
+
# @api private
|
|
53
62
|
LOCK_FILE = '.install.lock'
|
|
54
|
-
# Relative to Dir.pwd, resolved at CALL time by
|
|
55
|
-
# constant would freeze the working directory
|
|
56
|
-
# wrong for anything that chdirs (Rake
|
|
63
|
+
# Relative to .root (Dir.pwd when unset), resolved at CALL time by
|
|
64
|
+
# .default_dir — an absolute constant would freeze the working directory
|
|
65
|
+
# as of require time, which is wrong for anything that chdirs (Rake
|
|
66
|
+
# tasks, bin/setup, test suites).
|
|
67
|
+
#
|
|
68
|
+
# @api private
|
|
57
69
|
DEFAULT_DIR = File.join('vendor', 'claude')
|
|
58
70
|
# Response caps. The dist-tag endpoints return a bare version string and
|
|
59
71
|
# manifests are a few KB; anything larger is a misrouted response, not
|
|
60
72
|
# something to buffer in memory. (The binary itself streams to disk.)
|
|
73
|
+
#
|
|
74
|
+
# @api private
|
|
61
75
|
VERSION_RESPONSE_LIMIT = 1024
|
|
76
|
+
# @api private
|
|
62
77
|
MANIFEST_RESPONSE_LIMIT = 5 * 1024 * 1024
|
|
78
|
+
# @api private
|
|
63
79
|
METADATA_READ_LIMIT = 4096
|
|
64
80
|
|
|
65
81
|
# Maps the running Ruby to a release-manifest platform key
|
|
66
82
|
# (darwin-arm64, darwin-x64, linux-x64, linux-arm64, and the -musl
|
|
67
83
|
# variants). Windows is not supported by this gem.
|
|
84
|
+
#
|
|
85
|
+
# @api private
|
|
68
86
|
module Platform
|
|
69
87
|
class << self
|
|
70
88
|
def detect
|
|
@@ -122,6 +140,8 @@ module ClaudeAgentSDK
|
|
|
122
140
|
# and chunked streaming for the binary. Knows nothing about releases; the
|
|
123
141
|
# specs stub .fetch_text / .download_to wholesale so no HTTP stubbing
|
|
124
142
|
# library is needed.
|
|
143
|
+
#
|
|
144
|
+
# @api private
|
|
125
145
|
module Http
|
|
126
146
|
MAX_REDIRECTS = 5
|
|
127
147
|
OPEN_TIMEOUT_SECONDS = 10
|
|
@@ -154,7 +174,9 @@ module ClaudeAgentSDK
|
|
|
154
174
|
File.open(path, File::WRONLY | File::CREAT | File::EXCL | File::BINARY, 0o600) do |file|
|
|
155
175
|
response.read_body do |chunk|
|
|
156
176
|
written += chunk.bytesize
|
|
157
|
-
|
|
177
|
+
if over?(written, max_bytes)
|
|
178
|
+
raise CLIInstallError, "Download from #{url} exceeds the expected #{max_bytes} bytes"
|
|
179
|
+
end
|
|
158
180
|
|
|
159
181
|
file.write(chunk)
|
|
160
182
|
end
|
|
@@ -191,19 +213,21 @@ module ClaudeAgentSDK
|
|
|
191
213
|
raise CLIInstallError, "Failed to fetch #{url}: #{e.class}: #{e.message}"
|
|
192
214
|
end
|
|
193
215
|
|
|
194
|
-
def follow_redirect(uri, response, redirects_left, &
|
|
216
|
+
def follow_redirect(uri, response, redirects_left, &)
|
|
195
217
|
raise CLIInstallError, "Too many redirects while fetching #{uri}" if redirects_left <= 0
|
|
196
218
|
|
|
197
219
|
location = response['location'].to_s
|
|
198
220
|
raise CLIInstallError, "Redirect from #{uri} is missing a Location header" if location.empty?
|
|
199
221
|
|
|
200
|
-
with_response(URI.join(uri.to_s, location), redirects_left - 1, &
|
|
222
|
+
with_response(URI.join(uri.to_s, location), redirects_left - 1, &)
|
|
201
223
|
end
|
|
202
224
|
end
|
|
203
225
|
end
|
|
204
226
|
|
|
205
227
|
# Talks to the release service: dist-tag resolution, manifest lookup and
|
|
206
228
|
# URL construction. Pure remote reads — no filesystem, no state.
|
|
229
|
+
#
|
|
230
|
+
# @api private
|
|
207
231
|
module Release
|
|
208
232
|
class << self
|
|
209
233
|
# Local, network-free check of what the caller asked for. Returns the
|
|
@@ -238,7 +262,9 @@ module ClaudeAgentSDK
|
|
|
238
262
|
end
|
|
239
263
|
|
|
240
264
|
checksum = entry['checksum'].to_s.downcase
|
|
241
|
-
|
|
265
|
+
unless checksum.match?(CHECKSUM_PATTERN)
|
|
266
|
+
raise CLIInstallError, "#{url} has no valid sha256 checksum for #{platform}"
|
|
267
|
+
end
|
|
242
268
|
|
|
243
269
|
size = entry['size']
|
|
244
270
|
{ checksum: checksum, size: size.is_a?(Integer) && size.positive? ? size : nil }
|
|
@@ -276,6 +302,8 @@ module ClaudeAgentSDK
|
|
|
276
302
|
# per line. Both platform and checksum must match before trusting a cached
|
|
277
303
|
# binary offline: a cache copied between OS/CPU/libc targets is not usable.
|
|
278
304
|
# Older one- or two-line files lack that proof and trigger a clean reinstall.
|
|
305
|
+
#
|
|
306
|
+
# @api private
|
|
279
307
|
module Metadata
|
|
280
308
|
class << self
|
|
281
309
|
def read(dir)
|
|
@@ -309,10 +337,39 @@ module ClaudeAgentSDK
|
|
|
309
337
|
end
|
|
310
338
|
|
|
311
339
|
class << self
|
|
312
|
-
#
|
|
313
|
-
# current working directory
|
|
340
|
+
# The directory DEFAULT_DIR is resolved against, or nil (the default)
|
|
341
|
+
# for the current working directory at call time.
|
|
342
|
+
#
|
|
343
|
+
# Set it when the process cwd is not the project root — a daemonized
|
|
344
|
+
# worker, a job runner started from /, a systemd unit without
|
|
345
|
+
# WorkingDirectory — so .default_dir, and with it .installed_path and
|
|
346
|
+
# SubprocessCLITransport's discovery of the vendored binary, still
|
|
347
|
+
# point at <root>/vendor/claude. The Rails Railtie sets it to
|
|
348
|
+
# Rails.root unless something already has.
|
|
349
|
+
#
|
|
350
|
+
# Safe to read from any thread without a lock: the value is a single
|
|
351
|
+
# frozen String reference (or nil), replaced whole by .root=, so a
|
|
352
|
+
# reader sees either the old root or the new one, never a partial one.
|
|
353
|
+
#
|
|
354
|
+
# @return [String, nil] an absolute path, or nil
|
|
355
|
+
attr_reader :root
|
|
356
|
+
|
|
357
|
+
# @param path [String, Pathname, nil] the project root. A relative path
|
|
358
|
+
# is absolutized against the working directory NOW, once, so a later
|
|
359
|
+
# chdir cannot move it. nil restores the Dir.pwd default.
|
|
360
|
+
# @raise [ArgumentError] for an empty path (which would silently pin
|
|
361
|
+
# the current working directory)
|
|
362
|
+
def root=(path)
|
|
363
|
+
raise ArgumentError, 'CLIInstaller.root must be a non-empty path or nil' if path&.to_s&.empty?
|
|
364
|
+
|
|
365
|
+
@root = path && File.expand_path(path).freeze
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Absolute path of the default install directory: vendor/claude under
|
|
369
|
+
# .root, or under the current working directory (resolved each time it
|
|
370
|
+
# is asked for) while .root is unset.
|
|
314
371
|
def default_dir
|
|
315
|
-
File.expand_path(DEFAULT_DIR, Dir.pwd)
|
|
372
|
+
File.expand_path(DEFAULT_DIR, root || Dir.pwd)
|
|
316
373
|
end
|
|
317
374
|
|
|
318
375
|
# Install the CLI into +dir+ and return the absolute path of the binary.
|