claude-agent-sdk 0.9.0 → 0.11.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 +40 -0
- data/README.md +97 -4
- data/lib/claude_agent_sdk/message_parser.rb +2 -1
- data/lib/claude_agent_sdk/query.rb +4 -1
- data/lib/claude_agent_sdk/session_mutations.rb +165 -0
- data/lib/claude_agent_sdk/sessions.rb +4 -1
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +0 -3
- data/lib/claude_agent_sdk/types.rb +29 -4
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +25 -2
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ca3c257ced832f2e2120e7600aa346ef7e8487567309ac1ff579e8672743819e
|
|
4
|
+
data.tar.gz: f3598c2b3abe03b67042b5f04b5a0fd4b6cc12f80ba73d36bb9708e6b080d94e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d88cfa73d92aa49aaf83d94ad51ba6f0e65761c51b7f4703477e606765a4cee06f65725cf295f30e8605c694cafc40f389f96e131f0db668db145ec31409da02
|
|
7
|
+
data.tar.gz: fa86ac725b72ee1896525cccaebc119b0d1c8138c931e375e95e8549275ff0b89426cc9dbaa2f352ff389e9b7614d4cb7164a1c290ed92c0b03634246f17db4b
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,46 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.11.0] - 2026-03-20
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
#### Custom Transport Support
|
|
13
|
+
- `Client.new` accepts `transport_class:` and `transport_args:` keyword arguments, allowing consumers to plug in custom transports (e.g., E2B sandbox, remote SSH) without duplicating `Client#connect` internals
|
|
14
|
+
- Default remains `SubprocessCLITransport` — zero behavior change for existing callers
|
|
15
|
+
- Custom transport class must implement the `Transport` interface (`connect`, `write`, `read_messages`, `end_input`, `close`, `ready?`)
|
|
16
|
+
- `transport_args` are passed as keyword arguments to `transport_class.new(options, **transport_args)`
|
|
17
|
+
- All option transformations, MCP extraction, hook conversion, and Query lifecycle stay in `Client#connect` — the transport only handles I/O
|
|
18
|
+
|
|
19
|
+
## [0.10.0] - 2026-03-20
|
|
20
|
+
|
|
21
|
+
Port of Python SDK v0.1.48 features for feature parity.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
#### Session Mutations
|
|
26
|
+
- `ClaudeAgentSDK.rename_session(session_id:, title:, directory:)` — rename a session by appending a custom-title JSONL entry
|
|
27
|
+
- `ClaudeAgentSDK.tag_session(session_id:, tag:, directory:)` — tag a session (pass `nil` to clear); tags are Unicode-sanitized
|
|
28
|
+
- `SessionMutations` module with `rename_session`, `tag_session`, and internal Unicode sanitization helpers
|
|
29
|
+
- Ported from Python SDK's `_internal/session_mutations.py` with TOCTOU-safe `O_WRONLY | O_APPEND` file operations
|
|
30
|
+
|
|
31
|
+
#### AssistantMessage Usage
|
|
32
|
+
- `usage` attribute on `AssistantMessage` — token usage data from the API response
|
|
33
|
+
- `MessageParser` populates `usage` from `data.dig(:message, :usage)`
|
|
34
|
+
|
|
35
|
+
#### AgentDefinition Fields
|
|
36
|
+
- `skills`, `memory`, `mcp_servers` attributes on `AgentDefinition`
|
|
37
|
+
- Serialized as `skills`, `memory`, `mcpServers` (camelCase) in the CLI wire protocol initialize request
|
|
38
|
+
|
|
39
|
+
#### TaskUsage Typed Class
|
|
40
|
+
- `TaskUsage` class with `total_tokens`, `tool_uses`, `duration_ms` attributes
|
|
41
|
+
- `TaskUsage.from_hash` factory supporting symbol, string, camelCase, and snake_case keys
|
|
42
|
+
|
|
43
|
+
### Removed
|
|
44
|
+
|
|
45
|
+
#### FGTS Environment Variable
|
|
46
|
+
- Removed auto-setting of `CLAUDE_CODE_ENABLE_FINE_GRAINED_TOOL_STREAMING` environment variable when `include_partial_messages` is enabled — Python SDK v0.1.48 reverted this because it causes HTTP 400 errors on LiteLLM proxies, Bedrock, and Vertex with Claude 4.5 models. The `--include-partial-messages` CLI flag remains the correct mechanism.
|
|
47
|
+
|
|
8
48
|
## [0.9.0] - 2026-03-12
|
|
9
49
|
|
|
10
50
|
Port of Python SDK v0.1.48 parity improvements.
|
data/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
[](https://badge.fury.io/rb/claude-agent-sdk)
|
|
8
8
|
|
|
9
|
-
### Feature Parity with Python SDK (v0.1.
|
|
9
|
+
### Feature Parity with Python SDK (v0.1.48) + Ruby Extras
|
|
10
10
|
|
|
11
11
|
| Feature | Python | Ruby |
|
|
12
12
|
|---------|:------:|:----:|
|
|
@@ -25,13 +25,16 @@
|
|
|
25
25
|
| Beta features (1M context) | ✅ | ✅ |
|
|
26
26
|
| File checkpointing & rewind | ✅ | ✅ |
|
|
27
27
|
| Session browsing (`list_sessions`, `get_session_messages`) | ✅ | ✅ |
|
|
28
|
+
| Session mutations (`rename_session`, `tag_session`) | ✅ | ✅ |
|
|
28
29
|
| Task message types (started/progress/notification) | ✅ | ✅ |
|
|
29
30
|
| MCP server control (reconnect/toggle/stop) | ✅ | ✅ |
|
|
30
31
|
| Subagent context on hook inputs | ✅ | ✅ |
|
|
31
32
|
| Typed MCP status response | ✅ | ✅ |
|
|
32
33
|
| `stop_reason` on `ResultMessage` | ✅ | ✅ |
|
|
34
|
+
| `usage` on `AssistantMessage` | ✅ | ✅ |
|
|
33
35
|
| Fallback model | ✅ | ✅ |
|
|
34
36
|
| Plugin support | ✅ | ✅ |
|
|
37
|
+
| Custom transport (pluggable I/O layer) | — | ✅ |
|
|
35
38
|
| Rails integration (configure block, ActionCable) | — | ✅ |
|
|
36
39
|
| Bundled CLI binary | ✅ | — |
|
|
37
40
|
|
|
@@ -125,6 +128,7 @@ Both SDKs spawn `claude` CLI as a subprocess with stream-JSON over stdin/stdout.
|
|
|
125
128
|
- [Quick Start](#quick-start)
|
|
126
129
|
- [Basic Usage: query()](#basic-usage-query)
|
|
127
130
|
- [Client](#client)
|
|
131
|
+
- [Custom Transport](#custom-transport)
|
|
128
132
|
- [Custom Tools (SDK MCP Servers)](#custom-tools-sdk-mcp-servers)
|
|
129
133
|
- [Hooks](#hooks)
|
|
130
134
|
- [Permission Callbacks](#permission-callbacks)
|
|
@@ -137,6 +141,7 @@ Both SDKs spawn `claude` CLI as a subprocess with stream-JSON over stdin/stdout.
|
|
|
137
141
|
- [Sandbox Settings](#sandbox-settings)
|
|
138
142
|
- [File Checkpointing & Rewind](#file-checkpointing--rewind)
|
|
139
143
|
- [Session Browsing](#session-browsing)
|
|
144
|
+
- [Session Mutations](#session-mutations)
|
|
140
145
|
- [Rails Integration](#rails-integration)
|
|
141
146
|
- [Types](#types)
|
|
142
147
|
- [Error Handling](#error-handling)
|
|
@@ -153,7 +158,7 @@ Add this line to your application's Gemfile:
|
|
|
153
158
|
gem 'claude-agent-sdk', github: 'ya-luotao/claude-agent-sdk-ruby'
|
|
154
159
|
|
|
155
160
|
# Or use a stable version from RubyGems
|
|
156
|
-
gem 'claude-agent-sdk', '~> 0.
|
|
161
|
+
gem 'claude-agent-sdk', '~> 0.11.0'
|
|
157
162
|
```
|
|
158
163
|
|
|
159
164
|
And then execute:
|
|
@@ -355,6 +360,59 @@ Async do
|
|
|
355
360
|
end.wait
|
|
356
361
|
```
|
|
357
362
|
|
|
363
|
+
### Custom Transport
|
|
364
|
+
|
|
365
|
+
By default, `Client` uses `SubprocessCLITransport` to spawn the Claude Code CLI locally. You can provide a custom transport class to connect via other channels (e.g., E2B sandbox, remote SSH, WebSocket):
|
|
366
|
+
|
|
367
|
+
```ruby
|
|
368
|
+
# Custom transport must implement the Transport interface:
|
|
369
|
+
# connect, write, read_messages, end_input, close, ready?
|
|
370
|
+
class E2BSandboxTransport < ClaudeAgentSDK::Transport
|
|
371
|
+
def initialize(options, sandbox:)
|
|
372
|
+
@options = options
|
|
373
|
+
@sandbox = sandbox
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def connect
|
|
377
|
+
@sandbox.connect
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def write(data)
|
|
381
|
+
@sandbox.stdin_write(data)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def read_messages(&block)
|
|
385
|
+
@sandbox.stdout_read_lines { |line| yield JSON.parse(line, symbolize_names: true) }
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def end_input
|
|
389
|
+
@sandbox.close_stdin
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def close
|
|
393
|
+
@sandbox.disconnect
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def ready?
|
|
397
|
+
@sandbox.connected?
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# Use it with Client — all connect orchestration (option transforms,
|
|
402
|
+
# MCP extraction, hook conversion, Query lifecycle) is handled for you
|
|
403
|
+
Async do
|
|
404
|
+
client = ClaudeAgentSDK::Client.new(
|
|
405
|
+
options: options,
|
|
406
|
+
transport_class: E2BSandboxTransport,
|
|
407
|
+
transport_args: { sandbox: my_sandbox }
|
|
408
|
+
)
|
|
409
|
+
client.connect
|
|
410
|
+
client.query("Hello from the sandbox!")
|
|
411
|
+
client.receive_response { |msg| puts msg }
|
|
412
|
+
client.disconnect
|
|
413
|
+
end.wait
|
|
414
|
+
```
|
|
415
|
+
|
|
358
416
|
## Custom Tools (SDK MCP Servers)
|
|
359
417
|
|
|
360
418
|
A **custom tool** is a Ruby proc/lambda that you can offer to Claude, for Claude to invoke as needed.
|
|
@@ -934,6 +992,39 @@ Each `SessionMessage` includes `type` (`"user"` or `"assistant"`), `uuid`, `sess
|
|
|
934
992
|
|
|
935
993
|
> **Note:** Session browsing reads `~/.claude/projects/` JSONL files directly. It respects the `CLAUDE_CONFIG_DIR` environment variable and automatically detects git worktrees.
|
|
936
994
|
|
|
995
|
+
## Session Mutations
|
|
996
|
+
|
|
997
|
+
Rename or tag sessions programmatically — no CLI subprocess required.
|
|
998
|
+
|
|
999
|
+
### Renaming a Session
|
|
1000
|
+
|
|
1001
|
+
```ruby
|
|
1002
|
+
# Rename a session (appends a custom-title JSONL entry)
|
|
1003
|
+
ClaudeAgentSDK.rename_session(
|
|
1004
|
+
session_id: '550e8400-e29b-41d4-a716-446655440000',
|
|
1005
|
+
title: 'My refactoring session',
|
|
1006
|
+
directory: '/path/to/project' # optional
|
|
1007
|
+
)
|
|
1008
|
+
```
|
|
1009
|
+
|
|
1010
|
+
### Tagging a Session
|
|
1011
|
+
|
|
1012
|
+
```ruby
|
|
1013
|
+
# Tag a session (Unicode-sanitized before storing)
|
|
1014
|
+
ClaudeAgentSDK.tag_session(
|
|
1015
|
+
session_id: '550e8400-e29b-41d4-a716-446655440000',
|
|
1016
|
+
tag: 'experiment'
|
|
1017
|
+
)
|
|
1018
|
+
|
|
1019
|
+
# Clear a tag
|
|
1020
|
+
ClaudeAgentSDK.tag_session(
|
|
1021
|
+
session_id: '550e8400-e29b-41d4-a716-446655440000',
|
|
1022
|
+
tag: nil
|
|
1023
|
+
)
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
> **Note:** Session mutations use append-only JSONL writes with `O_WRONLY | O_APPEND` (no `O_CREAT`) for TOCTOU safety. They are safe to call while the session is open in a CLI process.
|
|
1027
|
+
|
|
937
1028
|
## Rails Integration
|
|
938
1029
|
|
|
939
1030
|
The SDK integrates well with Rails applications. Here are common patterns:
|
|
@@ -1120,7 +1211,8 @@ class AssistantMessage
|
|
|
1120
1211
|
attr_accessor :content, # Array<ContentBlock>
|
|
1121
1212
|
:model, # String
|
|
1122
1213
|
:parent_tool_use_id,# String | nil
|
|
1123
|
-
:error
|
|
1214
|
+
:error, # String | nil ('authentication_failed', 'billing_error', 'rate_limit', 'invalid_request', 'server_error', 'unknown')
|
|
1215
|
+
:usage # Hash | nil - Token usage info from the API response
|
|
1124
1216
|
end
|
|
1125
1217
|
```
|
|
1126
1218
|
|
|
@@ -1276,7 +1368,7 @@ end
|
|
|
1276
1368
|
| `HookMatcher` | Hook configuration with matcher pattern and timeout |
|
|
1277
1369
|
| `PermissionResultAllow` | Permission callback result to allow tool use |
|
|
1278
1370
|
| `PermissionResultDeny` | Permission callback result to deny tool use |
|
|
1279
|
-
| `AgentDefinition` | Agent definition with description, prompt, tools, model |
|
|
1371
|
+
| `AgentDefinition` | Agent definition with description, prompt, tools, model, skills, memory, mcp_servers |
|
|
1280
1372
|
| `ThinkingConfigAdaptive` | Adaptive thinking mode (32,000 token default budget) |
|
|
1281
1373
|
| `ThinkingConfigEnabled` | Enabled thinking with explicit `budget_tokens` |
|
|
1282
1374
|
| `ThinkingConfigDisabled` | Disabled thinking (0 tokens) |
|
|
@@ -1290,6 +1382,7 @@ end
|
|
|
1290
1382
|
| `McpServerInfo` | MCP server name and version |
|
|
1291
1383
|
| `McpToolInfo` | MCP tool name, description, and annotations |
|
|
1292
1384
|
| `McpToolAnnotations` | MCP tool annotation hints (`read_only`, `destructive`, `open_world`) |
|
|
1385
|
+
| `TaskUsage` | Typed usage data (`total_tokens`, `tool_uses`, `duration_ms`) with `from_hash` factory |
|
|
1293
1386
|
| `SDKSessionInfo` | Session metadata from `list_sessions` |
|
|
1294
1387
|
| `SessionMessage` | Single message from `get_session_messages` |
|
|
1295
1388
|
| `SandboxSettings` | Sandbox settings for isolated command execution |
|
|
@@ -61,7 +61,8 @@ module ClaudeAgentSDK
|
|
|
61
61
|
content: content_blocks,
|
|
62
62
|
model: data.dig(:message, :model),
|
|
63
63
|
parent_tool_use_id: data[:parent_tool_use_id],
|
|
64
|
-
error: data[:error] # authentication_failed, billing_error, rate_limit, invalid_request, server_error, unknown
|
|
64
|
+
error: data[:error], # authentication_failed, billing_error, rate_limit, invalid_request, server_error, unknown
|
|
65
|
+
usage: data.dig(:message, :usage)
|
|
65
66
|
)
|
|
66
67
|
end
|
|
67
68
|
|
|
@@ -89,7 +89,10 @@ module ClaudeAgentSDK
|
|
|
89
89
|
description: agent_def.description,
|
|
90
90
|
prompt: agent_def.prompt,
|
|
91
91
|
tools: agent_def.tools,
|
|
92
|
-
model: agent_def.model
|
|
92
|
+
model: agent_def.model,
|
|
93
|
+
skills: agent_def.skills,
|
|
94
|
+
memory: agent_def.memory,
|
|
95
|
+
mcpServers: agent_def.mcp_servers
|
|
93
96
|
}.compact
|
|
94
97
|
end
|
|
95
98
|
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative 'sessions'
|
|
5
|
+
|
|
6
|
+
module ClaudeAgentSDK
|
|
7
|
+
# Session mutation functions: rename and tag sessions.
|
|
8
|
+
#
|
|
9
|
+
# Ported from Python SDK's _internal/session_mutations.py.
|
|
10
|
+
# Appends typed metadata entries to the session's JSONL file,
|
|
11
|
+
# matching the CLI pattern. Safe to call from any SDK host process.
|
|
12
|
+
module SessionMutations
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# Rename a session by appending a custom-title entry.
|
|
16
|
+
#
|
|
17
|
+
# list_sessions reads the LAST custom-title from the file tail, so
|
|
18
|
+
# repeated calls are safe — the most recent wins.
|
|
19
|
+
#
|
|
20
|
+
# @param session_id [String] UUID of the session to rename
|
|
21
|
+
# @param title [String] New session title (whitespace stripped)
|
|
22
|
+
# @param directory [String, nil] Project directory path
|
|
23
|
+
# @raise [ArgumentError] if session_id is invalid or title is empty
|
|
24
|
+
# @raise [Errno::ENOENT] if the session file cannot be found
|
|
25
|
+
def rename_session(session_id:, title:, directory: nil)
|
|
26
|
+
raise ArgumentError, "Invalid session_id: #{session_id}" unless session_id.match?(Sessions::UUID_RE)
|
|
27
|
+
|
|
28
|
+
stripped = title.strip
|
|
29
|
+
raise ArgumentError, 'title must be non-empty' if stripped.empty?
|
|
30
|
+
|
|
31
|
+
data = "#{JSON.generate({ type: 'custom-title', customTitle: stripped, sessionId: session_id },
|
|
32
|
+
space_size: 0)}\n"
|
|
33
|
+
|
|
34
|
+
append_to_session(session_id, data, directory)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Tag a session. Pass nil to clear the tag.
|
|
38
|
+
#
|
|
39
|
+
# Appends a {type:'tag',tag:<tag>,sessionId:<id>} JSONL entry.
|
|
40
|
+
# Tags are Unicode-sanitized before storing.
|
|
41
|
+
#
|
|
42
|
+
# @param session_id [String] UUID of the session to tag
|
|
43
|
+
# @param tag [String, nil] Tag string, or nil to clear
|
|
44
|
+
# @param directory [String, nil] Project directory path
|
|
45
|
+
# @raise [ArgumentError] if session_id is invalid or tag is empty after sanitization
|
|
46
|
+
# @raise [Errno::ENOENT] if the session file cannot be found
|
|
47
|
+
def tag_session(session_id:, tag:, directory: nil)
|
|
48
|
+
raise ArgumentError, "Invalid session_id: #{session_id}" unless session_id.match?(Sessions::UUID_RE)
|
|
49
|
+
|
|
50
|
+
if tag
|
|
51
|
+
sanitized = sanitize_unicode(tag).strip
|
|
52
|
+
raise ArgumentError, 'tag must be non-empty (use nil to clear)' if sanitized.empty?
|
|
53
|
+
|
|
54
|
+
tag = sanitized
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
data = "#{JSON.generate({ type: 'tag', tag: tag || '', sessionId: session_id },
|
|
58
|
+
space_size: 0)}\n"
|
|
59
|
+
|
|
60
|
+
append_to_session(session_id, data, directory)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# -- Private helpers --
|
|
64
|
+
|
|
65
|
+
def append_to_session(session_id, data, directory)
|
|
66
|
+
file_name = "#{session_id}.jsonl"
|
|
67
|
+
|
|
68
|
+
if directory
|
|
69
|
+
append_to_session_in_directory(session_id, data, file_name, directory)
|
|
70
|
+
else
|
|
71
|
+
append_to_session_global(session_id, data, file_name)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def append_to_session_in_directory(session_id, data, file_name, directory)
|
|
76
|
+
path = File.realpath(directory).unicode_normalize(:nfc)
|
|
77
|
+
|
|
78
|
+
# Try the exact/prefix-matched project directory first.
|
|
79
|
+
project_dir = Sessions.find_project_dir(path)
|
|
80
|
+
return if project_dir && try_append(File.join(project_dir, file_name), data)
|
|
81
|
+
|
|
82
|
+
# Worktree fallback
|
|
83
|
+
begin
|
|
84
|
+
worktree_paths = Sessions.detect_worktrees(path)
|
|
85
|
+
rescue StandardError
|
|
86
|
+
worktree_paths = []
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
found = worktree_paths.any? do |wt_path|
|
|
90
|
+
next false if wt_path == path
|
|
91
|
+
|
|
92
|
+
wt_project_dir = Sessions.find_project_dir(wt_path)
|
|
93
|
+
wt_project_dir && try_append(File.join(wt_project_dir, file_name), data)
|
|
94
|
+
end
|
|
95
|
+
return if found
|
|
96
|
+
|
|
97
|
+
raise Errno::ENOENT, "Session #{session_id} not found in project directory for #{directory}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def append_to_session_global(session_id, data, file_name)
|
|
101
|
+
projects_dir = File.join(Sessions.config_dir, 'projects')
|
|
102
|
+
raise Errno::ENOENT, "Session #{session_id} not found (no projects directory)" unless File.directory?(projects_dir)
|
|
103
|
+
|
|
104
|
+
found = Dir.children(projects_dir).any? do |child|
|
|
105
|
+
candidate = File.join(projects_dir, child, file_name)
|
|
106
|
+
try_append(candidate, data)
|
|
107
|
+
end
|
|
108
|
+
return if found
|
|
109
|
+
|
|
110
|
+
raise Errno::ENOENT, "Session #{session_id} not found in any project directory"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Try appending to a path.
|
|
114
|
+
#
|
|
115
|
+
# Opens with WRONLY | APPEND (no CREAT) so the open fails with
|
|
116
|
+
# ENOENT if the file does not exist. Returns false for missing
|
|
117
|
+
# files or zero-byte files; true on successful write.
|
|
118
|
+
def try_append(path, data)
|
|
119
|
+
file = File.open(path, File::WRONLY | File::APPEND)
|
|
120
|
+
begin
|
|
121
|
+
return false if file.stat.size.zero? # rubocop:disable Style/ZeroLengthPredicate
|
|
122
|
+
|
|
123
|
+
file.write(data)
|
|
124
|
+
true
|
|
125
|
+
ensure
|
|
126
|
+
file.close
|
|
127
|
+
end
|
|
128
|
+
rescue Errno::ENOENT, Errno::ENOTDIR
|
|
129
|
+
false
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Unicode sanitization — ported from Python SDK / TS sanitization.ts
|
|
133
|
+
#
|
|
134
|
+
# Iteratively applies NFKC normalization and strips format/private-use/
|
|
135
|
+
# unassigned characters until stable (max 10 iterations).
|
|
136
|
+
UNICODE_STRIP_RE = /[\u200b-\u200f\u202a-\u202e\u2066-\u2069\ufeff\ue000-\uf8ff]/
|
|
137
|
+
FORMAT_CATEGORIES = %w[Cf Co Cn].freeze
|
|
138
|
+
|
|
139
|
+
def sanitize_unicode(value)
|
|
140
|
+
current = value
|
|
141
|
+
10.times do
|
|
142
|
+
previous = current
|
|
143
|
+
current = current.unicode_normalize(:nfkc)
|
|
144
|
+
current = current.each_char.reject { |c| FORMAT_CATEGORIES.include?(unicode_category(c)) }.join
|
|
145
|
+
current = current.gsub(UNICODE_STRIP_RE, '')
|
|
146
|
+
break if current == previous
|
|
147
|
+
end
|
|
148
|
+
current
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Returns the Unicode general category for a character (e.g., 'Cf', 'Lu', 'Ll').
|
|
152
|
+
def unicode_category(char)
|
|
153
|
+
# Ruby doesn't have a built-in unicodedata.category(), but we can
|
|
154
|
+
# check the specific categories we care about using regex properties.
|
|
155
|
+
return 'Cf' if char.match?(/\p{Cf}/)
|
|
156
|
+
return 'Co' if char.match?(/\p{Co}/)
|
|
157
|
+
return 'Cn' if char.match?(/\p{Cn}/)
|
|
158
|
+
|
|
159
|
+
'Other'
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
private_class_method :append_to_session, :append_to_session_in_directory,
|
|
163
|
+
:append_to_session_global, :try_append, :sanitize_unicode, :unicode_category
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -491,9 +491,12 @@ module ClaudeAgentSDK
|
|
|
491
491
|
end
|
|
492
492
|
|
|
493
493
|
private_class_method :list_sessions_for_directory, :list_all_sessions,
|
|
494
|
-
:deduplicate_sessions,
|
|
494
|
+
:deduplicate_sessions,
|
|
495
495
|
:find_session_file, :parse_jsonl_entries,
|
|
496
496
|
:build_conversation_chain, :walk_to_leaf, :walk_to_root,
|
|
497
497
|
:filter_visible_messages, :read_head_tail, :build_session_info
|
|
498
|
+
|
|
499
|
+
# These remain accessible for SessionMutations:
|
|
500
|
+
# config_dir, sanitize_path, find_project_dir, detect_worktrees
|
|
498
501
|
end
|
|
499
502
|
end
|
|
@@ -181,9 +181,6 @@ module ClaudeAgentSDK
|
|
|
181
181
|
process_env = ENV.to_h.merge('CLAUDECODE' => nil, 'CLAUDE_AGENT_SDK_VERSION' => VERSION).merge(custom_env)
|
|
182
182
|
process_env['CLAUDE_CODE_ENTRYPOINT'] ||= 'sdk-rb'
|
|
183
183
|
process_env['CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING'] = 'true' if @options.enable_file_checkpointing
|
|
184
|
-
if @options.include_partial_messages
|
|
185
|
-
process_env['CLAUDE_CODE_ENABLE_FINE_GRAINED_TOOL_STREAMING'] ||= '1'
|
|
186
|
-
end
|
|
187
184
|
process_env['PWD'] = @cwd.to_s if @cwd
|
|
188
185
|
|
|
189
186
|
# Determine stderr handling
|
|
@@ -104,13 +104,14 @@ module ClaudeAgentSDK
|
|
|
104
104
|
|
|
105
105
|
# Assistant message with content blocks
|
|
106
106
|
class AssistantMessage
|
|
107
|
-
attr_accessor :content, :model, :parent_tool_use_id, :error
|
|
107
|
+
attr_accessor :content, :model, :parent_tool_use_id, :error, :usage
|
|
108
108
|
|
|
109
|
-
def initialize(content:, model:, parent_tool_use_id: nil, error: nil)
|
|
109
|
+
def initialize(content:, model:, parent_tool_use_id: nil, error: nil, usage: nil)
|
|
110
110
|
@content = content
|
|
111
111
|
@model = model
|
|
112
112
|
@parent_tool_use_id = parent_tool_use_id
|
|
113
113
|
@error = error # One of: authentication_failed, billing_error, rate_limit, invalid_request, server_error, unknown
|
|
114
|
+
@usage = usage # Token usage info from the API response
|
|
114
115
|
end
|
|
115
116
|
end
|
|
116
117
|
|
|
@@ -127,6 +128,27 @@ module ClaudeAgentSDK
|
|
|
127
128
|
# Task lifecycle notification statuses
|
|
128
129
|
TASK_NOTIFICATION_STATUSES = %w[completed failed stopped].freeze
|
|
129
130
|
|
|
131
|
+
# Typed usage data for task progress and notifications
|
|
132
|
+
class TaskUsage
|
|
133
|
+
attr_accessor :total_tokens, :tool_uses, :duration_ms
|
|
134
|
+
|
|
135
|
+
def initialize(total_tokens: 0, tool_uses: 0, duration_ms: 0)
|
|
136
|
+
@total_tokens = total_tokens
|
|
137
|
+
@tool_uses = tool_uses
|
|
138
|
+
@duration_ms = duration_ms
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def self.from_hash(hash)
|
|
142
|
+
return nil unless hash.is_a?(Hash)
|
|
143
|
+
|
|
144
|
+
new(
|
|
145
|
+
total_tokens: hash[:total_tokens] || hash['total_tokens'] || hash[:totalTokens] || hash['totalTokens'] || 0,
|
|
146
|
+
tool_uses: hash[:tool_uses] || hash['tool_uses'] || hash[:toolUses] || hash['toolUses'] || 0,
|
|
147
|
+
duration_ms: hash[:duration_ms] || hash['duration_ms'] || hash[:durationMs] || hash['durationMs'] || 0
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
130
152
|
# Task started system message (subagent/background task started)
|
|
131
153
|
class TaskStartedMessage < SystemMessage
|
|
132
154
|
attr_accessor :task_id, :description, :uuid, :session_id, :tool_use_id, :task_type
|
|
@@ -285,13 +307,16 @@ module ClaudeAgentSDK
|
|
|
285
307
|
|
|
286
308
|
# Agent definition configuration
|
|
287
309
|
class AgentDefinition
|
|
288
|
-
attr_accessor :description, :prompt, :tools, :model
|
|
310
|
+
attr_accessor :description, :prompt, :tools, :model, :skills, :memory, :mcp_servers
|
|
289
311
|
|
|
290
|
-
def initialize(description:, prompt:, tools: nil, model: nil)
|
|
312
|
+
def initialize(description:, prompt:, tools: nil, model: nil, skills: nil, memory: nil, mcp_servers: nil)
|
|
291
313
|
@description = description
|
|
292
314
|
@prompt = prompt
|
|
293
315
|
@tools = tools
|
|
294
316
|
@model = model
|
|
317
|
+
@skills = skills # Array of skill names
|
|
318
|
+
@memory = memory # One of: 'user', 'project', 'local'
|
|
319
|
+
@mcp_servers = mcp_servers # Array of server names or config hashes
|
|
295
320
|
end
|
|
296
321
|
end
|
|
297
322
|
|
data/lib/claude_agent_sdk.rb
CHANGED
|
@@ -11,6 +11,7 @@ require_relative 'claude_agent_sdk/query'
|
|
|
11
11
|
require_relative 'claude_agent_sdk/sdk_mcp_server'
|
|
12
12
|
require_relative 'claude_agent_sdk/streaming'
|
|
13
13
|
require_relative 'claude_agent_sdk/sessions'
|
|
14
|
+
require_relative 'claude_agent_sdk/session_mutations'
|
|
14
15
|
require 'async'
|
|
15
16
|
require 'securerandom'
|
|
16
17
|
|
|
@@ -78,6 +79,22 @@ module ClaudeAgentSDK
|
|
|
78
79
|
Sessions.get_session_messages(session_id: session_id, directory: directory, limit: limit, offset: offset)
|
|
79
80
|
end
|
|
80
81
|
|
|
82
|
+
# Rename a session by appending a custom-title entry
|
|
83
|
+
# @param session_id [String] UUID of the session to rename
|
|
84
|
+
# @param title [String] New session title
|
|
85
|
+
# @param directory [String, nil] Project directory path
|
|
86
|
+
def self.rename_session(session_id:, title:, directory: nil)
|
|
87
|
+
SessionMutations.rename_session(session_id: session_id, title: title, directory: directory)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Tag a session. Pass nil to clear the tag.
|
|
91
|
+
# @param session_id [String] UUID of the session to tag
|
|
92
|
+
# @param tag [String, nil] Tag string, or nil to clear
|
|
93
|
+
# @param directory [String, nil] Project directory path
|
|
94
|
+
def self.tag_session(session_id:, tag:, directory: nil)
|
|
95
|
+
SessionMutations.tag_session(session_id: session_id, tag: tag, directory: directory)
|
|
96
|
+
end
|
|
97
|
+
|
|
81
98
|
def self.query(prompt:, options: nil, &block)
|
|
82
99
|
return enum_for(:query, prompt: prompt, options: options) unless block
|
|
83
100
|
|
|
@@ -222,8 +239,14 @@ module ClaudeAgentSDK
|
|
|
222
239
|
class Client
|
|
223
240
|
attr_reader :query_handler
|
|
224
241
|
|
|
225
|
-
|
|
242
|
+
# @param options [ClaudeAgentOptions, nil] Configuration options
|
|
243
|
+
# @param transport_class [Class] Transport class to use (must implement Transport interface).
|
|
244
|
+
# Defaults to SubprocessCLITransport.
|
|
245
|
+
# @param transport_args [Hash] Additional keyword arguments passed to transport_class.new(options, **transport_args)
|
|
246
|
+
def initialize(options: nil, transport_class: SubprocessCLITransport, transport_args: {})
|
|
226
247
|
@options = options || ClaudeAgentOptions.new
|
|
248
|
+
@transport_class = transport_class
|
|
249
|
+
@transport_args = transport_args
|
|
227
250
|
@transport = nil
|
|
228
251
|
@query_handler = nil
|
|
229
252
|
@connected = false
|
|
@@ -257,7 +280,7 @@ module ClaudeAgentSDK
|
|
|
257
280
|
)
|
|
258
281
|
|
|
259
282
|
# Client always uses streaming mode; keep stdin open for bidirectional communication.
|
|
260
|
-
@transport =
|
|
283
|
+
@transport = @transport_class.new(configured_options, **@transport_args)
|
|
261
284
|
@transport.connect
|
|
262
285
|
|
|
263
286
|
# Extract SDK MCP servers
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: claude-agent-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Community Contributors
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-03-
|
|
10
|
+
date: 2026-03-20 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: async
|
|
@@ -109,6 +109,7 @@ files:
|
|
|
109
109
|
- lib/claude_agent_sdk/message_parser.rb
|
|
110
110
|
- lib/claude_agent_sdk/query.rb
|
|
111
111
|
- lib/claude_agent_sdk/sdk_mcp_server.rb
|
|
112
|
+
- lib/claude_agent_sdk/session_mutations.rb
|
|
112
113
|
- lib/claude_agent_sdk/sessions.rb
|
|
113
114
|
- lib/claude_agent_sdk/streaming.rb
|
|
114
115
|
- lib/claude_agent_sdk/subprocess_cli_transport.rb
|