claude-agent-sdk 0.16.7 → 0.16.8
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 +28 -0
- data/README.md +84 -1656
- data/docs/client.md +157 -0
- data/docs/configuration.md +215 -0
- data/docs/errors.md +95 -0
- data/docs/hooks-and-permissions.md +110 -0
- data/docs/mcp-servers.md +153 -0
- data/docs/observability.md +126 -0
- data/docs/rails.md +199 -0
- data/docs/sessions.md +101 -0
- data/docs/types.md +187 -0
- data/lib/claude_agent_sdk/command_builder.rb +5 -0
- data/lib/claude_agent_sdk/message_parser.rb +8 -0
- data/lib/claude_agent_sdk/query.rb +46 -17
- data/lib/claude_agent_sdk/sdk_mcp_server.rb +12 -6
- data/lib/claude_agent_sdk/session_mutations.rb +46 -12
- data/lib/claude_agent_sdk/sessions.rb +43 -3
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +78 -23
- data/lib/claude_agent_sdk/types.rb +46 -5
- data/lib/claude_agent_sdk/version.rb +1 -1
- metadata +10 -1
data/README.md
CHANGED
|
@@ -6,12 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
An **unofficial, community-maintained** Ruby SDK for the [Claude Code](https://docs.claude.com/en/docs/claude-code-overview) agent runtime. Not affiliated with or supported by Anthropic.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Official SDKs: [TypeScript](https://github.com/anthropics/claude-agent-sdk-typescript) · [Python](https://github.com/anthropics/claude-agent-sdk-python).
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
- **Python** (official): [anthropics/claude-agent-sdk-python](https://github.com/anthropics/claude-agent-sdk-python)
|
|
13
|
-
|
|
14
|
-
### Why a Ruby SDK?
|
|
11
|
+
## Why a Ruby SDK?
|
|
15
12
|
|
|
16
13
|
Ruby powers a massive ecosystem — Rails, Sidekiq, Kamal, countless production web apps — but has no official Claude Agent SDK. This gem fills that gap so Ruby and Rails developers can build AI agents, automate coding workflows, and integrate Claude into existing applications without switching languages or shelling out to Python/Node.
|
|
17
14
|
|
|
@@ -39,16 +36,14 @@ All three SDKs share the same underlying mechanism: they spawn the `claude` CLI
|
|
|
39
36
|
| Custom transport (pluggable I/O) | — | — | ✅ |
|
|
40
37
|
| Rails integration | — | — | ✅ |
|
|
41
38
|
|
|
42
|
-
**Where Ruby goes further:** Built-in OpenTelemetry observer with Langfuse flow diagram support — no third-party instrumentation library needed. Custom transport support lets you swap the subprocess for any I/O layer (e.g., connect to a remote Claude Code instance over SSH or a container). Rails integration provides a `configure` block for initializers with thread-safe observer factories, and plays well with ActionCable for real-time streaming. Full typed coverage for all 24 CLI message types and all 27 hook events
|
|
39
|
+
**Where Ruby goes further:** Built-in OpenTelemetry observer with Langfuse flow diagram support — no third-party instrumentation library needed. Custom transport support lets you swap the subprocess for any I/O layer (e.g., connect to a remote Claude Code instance over SSH or a container). Rails integration provides a `configure` block for initializers with thread-safe observer factories, and plays well with ActionCable for real-time streaming. Full typed coverage for all 24 CLI message types and all 27 hook events.
|
|
43
40
|
|
|
44
41
|
**What's missing:** The Ruby gem does not bundle the `claude` CLI binary (`npm install -g @anthropic-ai/claude-code`).
|
|
45
42
|
|
|
46
43
|
<details>
|
|
47
44
|
<summary><strong>Implementation differences from the official SDKs</strong></summary>
|
|
48
45
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
TypeScript uses native `async`/`await`. Python uses `async`/`await` with `anyio`. Ruby uses the [`async`](https://github.com/socketry/async) gem with fibers — no `await` keyword needed, blocking calls yield automatically inside an `Async` block.
|
|
46
|
+
TypeScript uses native `async`/`await`. Python uses `async`/`await` with `anyio`. Ruby uses the [`async`](https://github.com/socketry/async) gem with fibers — no `await` keyword needed; blocking calls yield automatically inside an `Async` block:
|
|
52
47
|
|
|
53
48
|
```ruby
|
|
54
49
|
Async do
|
|
@@ -60,68 +55,23 @@ Async do
|
|
|
60
55
|
end.wait
|
|
61
56
|
```
|
|
62
57
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
TypeScript has Zod schemas with inferred types. Python uses `dataclass` with type annotations. Ruby uses plain classes with `attr_accessor` and keyword args — no runtime type checking, but the same structure and field names.
|
|
66
|
-
|
|
67
|
-
#### Subprocess transport
|
|
68
|
-
|
|
69
|
-
All three SDKs spawn `claude` CLI as a subprocess with stream-JSON over stdin/stdout. TypeScript uses Node `child_process`, Python uses `anyio.open_process`, Ruby uses `Open3.popen3`. The wire protocol is identical.
|
|
58
|
+
Types use plain Ruby classes with `attr_accessor` and keyword args — no runtime type checking, but the same structure and field names as the TS Zod schemas / Python dataclasses. Subprocess transport uses `Open3.popen3`; wire protocol is identical.
|
|
70
59
|
|
|
71
60
|
</details>
|
|
72
61
|
|
|
73
|
-
## Table of Contents
|
|
74
|
-
|
|
75
|
-
- [Installation](#installation)
|
|
76
|
-
- [Quick Start](#quick-start)
|
|
77
|
-
- [Basic Usage: query()](#basic-usage-query)
|
|
78
|
-
- [Client](#client)
|
|
79
|
-
- [Custom Transport](#custom-transport)
|
|
80
|
-
- [Custom Tools (SDK MCP Servers)](#custom-tools-sdk-mcp-servers)
|
|
81
|
-
- [Hooks](#hooks)
|
|
82
|
-
- [Permission Callbacks](#permission-callbacks)
|
|
83
|
-
- [Structured Output](#structured-output)
|
|
84
|
-
- [Thinking Configuration](#thinking-configuration)
|
|
85
|
-
- [Budget Control](#budget-control)
|
|
86
|
-
- [Fallback Model](#fallback-model)
|
|
87
|
-
- [Beta Features](#beta-features)
|
|
88
|
-
- [Tools Configuration](#tools-configuration)
|
|
89
|
-
- [Sandbox Settings](#sandbox-settings)
|
|
90
|
-
- [Bare Mode](#bare-mode)
|
|
91
|
-
- [File Checkpointing & Rewind](#file-checkpointing--rewind)
|
|
92
|
-
- [Session Browsing](#session-browsing)
|
|
93
|
-
- [Session Mutations](#session-mutations)
|
|
94
|
-
- [Observability (OpenTelemetry / Langfuse)](#observability-opentelemetry--langfuse)
|
|
95
|
-
- [Rails Integration](#rails-integration)
|
|
96
|
-
- [Types](#types)
|
|
97
|
-
- [Error Handling](#error-handling)
|
|
98
|
-
- [Examples](#examples)
|
|
99
|
-
- [Development](#development)
|
|
100
|
-
- [License](#license)
|
|
101
|
-
|
|
102
62
|
## Installation
|
|
103
63
|
|
|
104
64
|
Add this line to your application's Gemfile:
|
|
105
65
|
|
|
106
66
|
```ruby
|
|
107
|
-
# Recommended:
|
|
67
|
+
# Recommended: use the latest from GitHub for newest features
|
|
108
68
|
gem 'claude-agent-sdk', github: 'ya-luotao/claude-agent-sdk-ruby'
|
|
109
69
|
|
|
110
70
|
# Or use a stable version from RubyGems
|
|
111
|
-
gem 'claude-agent-sdk', '~> 0.16.
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
And then execute:
|
|
115
|
-
|
|
116
|
-
```bash
|
|
117
|
-
bundle install
|
|
71
|
+
gem 'claude-agent-sdk', '~> 0.16.8'
|
|
118
72
|
```
|
|
119
73
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
```bash
|
|
123
|
-
gem install claude-agent-sdk
|
|
124
|
-
```
|
|
74
|
+
Then `bundle install`, or install directly: `gem install claude-agent-sdk`.
|
|
125
75
|
|
|
126
76
|
**Prerequisites:**
|
|
127
77
|
- Ruby 3.2+
|
|
@@ -133,14 +83,11 @@ gem install claude-agent-sdk
|
|
|
133
83
|
If you're using [Claude Code](https://claude.ai/claude-code), this repo is a Claude Code plugin marketplace. Add it once, then install the skill:
|
|
134
84
|
|
|
135
85
|
```bash
|
|
136
|
-
# Add the marketplace
|
|
137
86
|
/plugin marketplace add ya-luotao/claude-agent-sdk-ruby
|
|
138
|
-
|
|
139
|
-
# Install the plugin
|
|
140
87
|
/plugin install claude-agent-ruby@claude-agent-sdk-ruby
|
|
141
88
|
```
|
|
142
89
|
|
|
143
|
-
This skill teaches your AI coding assistant about the SDK's APIs, patterns, and best practices
|
|
90
|
+
This skill teaches your AI coding assistant about the SDK's APIs, patterns, and best practices.
|
|
144
91
|
|
|
145
92
|
## Quick Start
|
|
146
93
|
|
|
@@ -152,7 +99,7 @@ ClaudeAgentSDK.query(prompt: "What is 2 + 2?") do |message|
|
|
|
152
99
|
end
|
|
153
100
|
```
|
|
154
101
|
|
|
155
|
-
## Basic Usage: query()
|
|
102
|
+
## Basic Usage: `query()`
|
|
156
103
|
|
|
157
104
|
`query()` is a function for querying Claude Code. It yields response messages to a block.
|
|
158
105
|
|
|
@@ -175,72 +122,33 @@ ClaudeAgentSDK.query(prompt: "Tell me a joke", options: options) do |message|
|
|
|
175
122
|
end
|
|
176
123
|
```
|
|
177
124
|
|
|
178
|
-
|
|
125
|
+
**Using tools:**
|
|
179
126
|
|
|
180
127
|
```ruby
|
|
181
128
|
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
182
129
|
allowed_tools: ['Read', 'Write', 'Bash'],
|
|
183
|
-
permission_mode: 'acceptEdits'
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
ClaudeAgentSDK.query(
|
|
187
|
-
prompt: "Create a hello.rb file",
|
|
188
|
-
options: options
|
|
189
|
-
) do |message|
|
|
190
|
-
# Process tool use and results
|
|
191
|
-
end
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
### Working Directory
|
|
195
|
-
|
|
196
|
-
```ruby
|
|
197
|
-
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
130
|
+
permission_mode: 'acceptEdits',
|
|
198
131
|
cwd: "/path/to/project"
|
|
199
132
|
)
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
### Streaming Input
|
|
203
133
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
```ruby
|
|
207
|
-
require 'claude_agent_sdk'
|
|
208
|
-
|
|
209
|
-
# Create a stream of messages
|
|
210
|
-
messages = ['Hello!', 'What is 2+2?', 'Thanks!']
|
|
211
|
-
stream = ClaudeAgentSDK::Streaming.from_array(messages)
|
|
212
|
-
|
|
213
|
-
# Query with streaming input
|
|
214
|
-
ClaudeAgentSDK.query(prompt: stream) do |message|
|
|
215
|
-
puts message if message.is_a?(ClaudeAgentSDK::AssistantMessage)
|
|
216
|
-
end
|
|
134
|
+
ClaudeAgentSDK.query(prompt: "Create a hello.rb file", options: options) { |message| }
|
|
217
135
|
```
|
|
218
136
|
|
|
219
|
-
|
|
137
|
+
**Streaming input** — send multiple messages dynamically instead of a single prompt string:
|
|
220
138
|
|
|
221
139
|
```ruby
|
|
222
|
-
|
|
223
|
-
stream = Enumerator.new do |yielder|
|
|
224
|
-
yielder << ClaudeAgentSDK::Streaming.user_message("First message")
|
|
225
|
-
# Do some processing...
|
|
226
|
-
yielder << ClaudeAgentSDK::Streaming.user_message("Second message")
|
|
227
|
-
yielder << ClaudeAgentSDK::Streaming.user_message("Third message")
|
|
228
|
-
end
|
|
140
|
+
stream = ClaudeAgentSDK::Streaming.from_array(['Hello!', 'What is 2+2?', 'Thanks!'])
|
|
229
141
|
|
|
230
142
|
ClaudeAgentSDK.query(prompt: stream) do |message|
|
|
231
|
-
|
|
143
|
+
puts message if message.is_a?(ClaudeAgentSDK::AssistantMessage)
|
|
232
144
|
end
|
|
233
145
|
```
|
|
234
146
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
## Client
|
|
238
|
-
|
|
239
|
-
`ClaudeAgentSDK::Client` supports bidirectional, interactive conversations with Claude Code. Unlike `query()`, `Client` enables **custom tools**, **hooks**, and **permission callbacks**, all of which can be defined as Ruby procs/lambdas.
|
|
147
|
+
See [examples/streaming_input_example.rb](examples/streaming_input_example.rb) and [examples/quick_start.rb](examples/quick_start.rb).
|
|
240
148
|
|
|
241
|
-
|
|
149
|
+
## `Client` — Bidirectional Sessions
|
|
242
150
|
|
|
243
|
-
|
|
151
|
+
`Client` supports interactive conversations with hooks, permission callbacks, and custom tools. It uses streaming mode automatically.
|
|
244
152
|
|
|
245
153
|
```ruby
|
|
246
154
|
require 'claude_agent_sdk'
|
|
@@ -250,1602 +158,122 @@ Async do
|
|
|
250
158
|
client = ClaudeAgentSDK::Client.new
|
|
251
159
|
|
|
252
160
|
begin
|
|
253
|
-
# Connect automatically uses streaming mode for bidirectional communication
|
|
254
161
|
client.connect
|
|
255
|
-
|
|
256
|
-
# Send a query
|
|
257
162
|
client.query("What is the capital of France?")
|
|
258
|
-
|
|
259
|
-
# Receive the response
|
|
260
|
-
client.receive_response do |msg|
|
|
261
|
-
case msg
|
|
262
|
-
when ClaudeAgentSDK::AssistantMessage
|
|
263
|
-
puts msg.text
|
|
264
|
-
when ClaudeAgentSDK::ResultMessage
|
|
265
|
-
puts "Cost: $#{msg.total_cost_usd}" if msg.total_cost_usd
|
|
266
|
-
end
|
|
267
|
-
end
|
|
268
|
-
|
|
163
|
+
client.receive_response { |msg| puts msg }
|
|
269
164
|
ensure
|
|
270
165
|
client.disconnect
|
|
271
166
|
end
|
|
272
167
|
end.wait
|
|
273
168
|
```
|
|
274
169
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
```ruby
|
|
278
|
-
Async do
|
|
279
|
-
client = ClaudeAgentSDK::Client.new
|
|
280
|
-
client.connect
|
|
281
|
-
|
|
282
|
-
# Send interrupt signal
|
|
283
|
-
client.interrupt
|
|
284
|
-
|
|
285
|
-
# Change permission mode during conversation
|
|
286
|
-
client.set_permission_mode('acceptEdits')
|
|
287
|
-
|
|
288
|
-
# Change AI model during conversation
|
|
289
|
-
client.set_model('claude-sonnet-4-5')
|
|
290
|
-
|
|
291
|
-
# Get MCP server connection status
|
|
292
|
-
status = client.get_mcp_status
|
|
293
|
-
puts "MCP status: #{status}"
|
|
294
|
-
|
|
295
|
-
# Get server initialization info
|
|
296
|
-
info = client.get_server_info
|
|
297
|
-
puts "Available commands: #{info}"
|
|
298
|
-
|
|
299
|
-
# Reconnect a failed MCP server
|
|
300
|
-
client.reconnect_mcp_server('my-server')
|
|
301
|
-
|
|
302
|
-
# Enable or disable an MCP server
|
|
303
|
-
client.toggle_mcp_server('my-server', false)
|
|
304
|
-
|
|
305
|
-
# Stop a running background task
|
|
306
|
-
client.stop_task('task_abc123')
|
|
307
|
-
|
|
308
|
-
client.disconnect
|
|
309
|
-
end.wait
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
### Custom Transport
|
|
313
|
-
|
|
314
|
-
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., remote SSH, WebSocket, or a sandbox VM).
|
|
315
|
-
|
|
316
|
-
A transport must implement six methods:
|
|
317
|
-
|
|
318
|
-
| Method | Purpose |
|
|
319
|
-
|---|---|
|
|
320
|
-
| `connect` | Establish the connection / spawn the remote CLI |
|
|
321
|
-
| `write(data)` | Send raw JSON-line bytes to stdin |
|
|
322
|
-
| `read_messages { \|hash\| ... }` | Yield parsed JSON messages from stdout; block until the stream closes |
|
|
323
|
-
| `end_input` | Signal EOF on stdin |
|
|
324
|
-
| `close` | Terminate and clean up |
|
|
325
|
-
| `ready?` | Report whether the transport can accept I/O |
|
|
326
|
-
|
|
327
|
-
Then plug it into `Client` via `transport_class:` / `transport_args:`. All connect orchestration (option transforms, MCP extraction, hook conversion, Query lifecycle) is handled for you.
|
|
328
|
-
|
|
329
|
-
```ruby
|
|
330
|
-
client = ClaudeAgentSDK::Client.new(
|
|
331
|
-
options: options,
|
|
332
|
-
transport_class: MyTransport,
|
|
333
|
-
transport_args: { foo: 'bar' } # forwarded to MyTransport.new(options, **transport_args)
|
|
334
|
-
)
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
#### Reference: running `claude` inside an E2B sandbox
|
|
338
|
-
|
|
339
|
-
[`examples/e2b_transport_example.rb`](examples/e2b_transport_example.rb) is a working transport that runs the Claude Code CLI inside an [E2B](https://e2b.dev) Firecracker microVM instead of on your host. The wire protocol stays identical — only the I/O layer changes:
|
|
340
|
-
|
|
341
|
-
```
|
|
342
|
-
ClaudeAgentSDK::Client (host)
|
|
343
|
-
│ JSON-lines
|
|
344
|
-
▼
|
|
345
|
-
E2BCliTransport (host)
|
|
346
|
-
│ send_stdin / commands.run(background:) / CommandHandle#each
|
|
347
|
-
▼
|
|
348
|
-
E2B envd RPC (HTTP/2)
|
|
349
|
-
│
|
|
350
|
-
▼
|
|
351
|
-
/usr/local/bin/claude (in-VM subprocess)
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
The example reuses the SDK's `CommandBuilder` to produce the exact same argv that `SubprocessCLITransport` would build (including SDK MCP server `:instance` field stripping), shell-escapes it for E2B's `/bin/bash -l -c` execution path, and streams stdout/stderr back through `CommandHandle#each`.
|
|
355
|
-
|
|
356
|
-
Sketch (full file is ~250 lines):
|
|
357
|
-
|
|
358
|
-
```ruby
|
|
359
|
-
require 'claude_agent_sdk'
|
|
360
|
-
require 'e2b'
|
|
361
|
-
|
|
362
|
-
class E2BCliTransport < ClaudeAgentSDK::Transport
|
|
363
|
-
def initialize(options, sandbox:, cli_path: '/usr/local/bin/claude')
|
|
364
|
-
@options, @sandbox, @cli_path = options, sandbox, cli_path
|
|
365
|
-
end
|
|
366
|
-
|
|
367
|
-
def connect
|
|
368
|
-
argv = ClaudeAgentSDK::CommandBuilder.new(@cli_path, @options).build
|
|
369
|
-
cmd = argv.map { |a| Shellwords.shellescape(a.to_s) }.join(' ')
|
|
370
|
-
@handle = @sandbox.commands.run(cmd, background: true, stdin: true,
|
|
371
|
-
cwd: @options.cwd&.to_s, envs: build_env)
|
|
372
|
-
@pid = @handle.pid
|
|
373
|
-
@ready = true
|
|
374
|
-
end
|
|
375
|
-
|
|
376
|
-
def write(data) = @sandbox.commands.send_stdin(@pid, data)
|
|
377
|
-
def end_input = @sandbox.commands.close_stdin(@pid)
|
|
378
|
-
def close = @handle&.kill
|
|
379
|
-
def ready? = @ready
|
|
380
|
-
|
|
381
|
-
def read_messages(&block)
|
|
382
|
-
buf = +''
|
|
383
|
-
@handle.each do |stdout, stderr, _pty|
|
|
384
|
-
next if stderr && !stderr.empty?
|
|
385
|
-
stdout.each_line do |line|
|
|
386
|
-
buf << line.strip
|
|
387
|
-
begin
|
|
388
|
-
yield JSON.parse(buf, symbolize_names: true)
|
|
389
|
-
buf.clear
|
|
390
|
-
rescue JSON::ParserError
|
|
391
|
-
# JSON line split across reads — keep buffering
|
|
392
|
-
end
|
|
393
|
-
end
|
|
394
|
-
end
|
|
395
|
-
@handle.wait # raises E2B::CommandExitError on non-zero exit
|
|
396
|
-
end
|
|
397
|
-
end
|
|
398
|
-
|
|
399
|
-
# Use it
|
|
400
|
-
sandbox = E2B::Sandbox.create(template: 'base', timeout: 600)
|
|
401
|
-
Async do
|
|
402
|
-
client = ClaudeAgentSDK::Client.new(
|
|
403
|
-
options: options,
|
|
404
|
-
transport_class: E2BCliTransport,
|
|
405
|
-
transport_args: { sandbox: sandbox }
|
|
406
|
-
)
|
|
407
|
-
client.connect
|
|
408
|
-
client.query('Hello from the sandbox!')
|
|
409
|
-
client.receive_response { |msg| puts msg }
|
|
410
|
-
client.disconnect
|
|
411
|
-
ensure
|
|
412
|
-
sandbox.kill
|
|
413
|
-
end.wait
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
**Why use a remote transport?** Untrusted code execution, multi-tenant agent runs that can't share a host, environments without local Node.js, or simply isolating filesystem/network blast radius. The Firecracker VM gives you a fresh `/home/user` per session and is killable without touching the host.
|
|
417
|
-
|
|
418
|
-
**Production hardening** (intentionally omitted from the example for clarity): inactivity watchdog, keepalive heartbeat, stream reconnect on transient SSL/EOF errors, host env-var blocklist, MCP server filtering for sandbox compatibility. See the example file's header comments for what to add and why.
|
|
170
|
+
Advanced features (`interrupt`, mid-session model/permission switching, MCP status, custom transports for E2B/SSH/etc.) → see [docs/client.md](docs/client.md).
|
|
419
171
|
|
|
420
172
|
## Custom Tools (SDK MCP Servers)
|
|
421
173
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
Custom tools are implemented as in-process MCP servers that run directly within your Ruby application, eliminating the need for separate processes that regular MCP servers require.
|
|
425
|
-
|
|
426
|
-
**Implementation**: This SDK uses the [official Ruby MCP SDK](https://github.com/modelcontextprotocol/ruby-sdk) (`mcp` gem) internally, providing full protocol compliance while offering a simpler block-based API for tool definition.
|
|
427
|
-
|
|
428
|
-
### Creating a Simple Tool
|
|
174
|
+
Define tools as Ruby procs/lambdas that run in-process — no subprocess, no IPC, direct access to your app state.
|
|
429
175
|
|
|
430
176
|
```ruby
|
|
431
|
-
|
|
432
|
-
require 'async'
|
|
433
|
-
|
|
434
|
-
# Define a tool using create_tool (with optional annotations)
|
|
435
|
-
greet_tool = ClaudeAgentSDK.create_tool(
|
|
436
|
-
'greet', 'Greet a user', { name: :string },
|
|
437
|
-
annotations: { title: 'Greeter', readOnlyHint: true }
|
|
438
|
-
) do |args|
|
|
177
|
+
greet = ClaudeAgentSDK.create_tool('greet', 'Greet a user', { name: :string }) do |args|
|
|
439
178
|
{ content: [{ type: 'text', text: "Hello, #{args[:name]}!" }] }
|
|
440
179
|
end
|
|
441
180
|
|
|
442
|
-
|
|
443
|
-
server = ClaudeAgentSDK.create_sdk_mcp_server(
|
|
444
|
-
name: 'my-tools',
|
|
445
|
-
version: '1.0.0',
|
|
446
|
-
tools: [greet_tool]
|
|
447
|
-
)
|
|
181
|
+
server = ClaudeAgentSDK.create_sdk_mcp_server(name: 'my-tools', tools: [greet])
|
|
448
182
|
|
|
449
|
-
# Use it with Claude
|
|
450
183
|
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
451
184
|
mcp_servers: { tools: server },
|
|
452
185
|
allowed_tools: ['mcp__tools__greet']
|
|
453
186
|
)
|
|
454
|
-
|
|
455
|
-
Async do
|
|
456
|
-
client = ClaudeAgentSDK::Client.new(options: options)
|
|
457
|
-
client.connect
|
|
458
|
-
|
|
459
|
-
client.query("Greet Alice")
|
|
460
|
-
client.receive_response { |msg| puts msg }
|
|
461
|
-
|
|
462
|
-
client.disconnect
|
|
463
|
-
end.wait
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
### Pre-built JSON Schemas
|
|
467
|
-
|
|
468
|
-
If your schemas come from another library (e.g., [RubyLLM](https://github.com/crmne/ruby_llm)) that deep-stringifies keys, the SDK handles them transparently — both symbol-keyed and string-keyed schemas are accepted and normalized:
|
|
469
|
-
|
|
470
|
-
```ruby
|
|
471
|
-
# Symbol keys (standard Ruby)
|
|
472
|
-
tool = ClaudeAgentSDK.create_tool('save', 'Save a fact', {
|
|
473
|
-
type: 'object',
|
|
474
|
-
properties: { fact: { type: 'string' } },
|
|
475
|
-
required: ['fact']
|
|
476
|
-
}) { |args| { content: [{ type: 'text', text: "Saved: #{args[:fact]}" }] } }
|
|
477
|
-
|
|
478
|
-
# String keys (e.g., from RubyLLM or JSON.parse)
|
|
479
|
-
tool = ClaudeAgentSDK.create_tool('save', 'Save a fact', {
|
|
480
|
-
'type' => 'object',
|
|
481
|
-
'properties' => { 'fact' => { 'type' => 'string' } },
|
|
482
|
-
'required' => ['fact']
|
|
483
|
-
}) { |args| { content: [{ type: 'text', text: "Saved: #{args[:fact]}" }] } }
|
|
484
|
-
```
|
|
485
|
-
|
|
486
|
-
### Benefits Over External MCP Servers
|
|
487
|
-
|
|
488
|
-
- **No subprocess management** - Runs in the same process as your application
|
|
489
|
-
- **Better performance** - No IPC overhead for tool calls
|
|
490
|
-
- **Simpler deployment** - Single Ruby process instead of multiple
|
|
491
|
-
- **Easier debugging** - All code runs in the same process
|
|
492
|
-
- **Direct access** - Tools can directly access your application's state
|
|
493
|
-
|
|
494
|
-
### Calculator Example
|
|
495
|
-
|
|
496
|
-
```ruby
|
|
497
|
-
# Define calculator tools
|
|
498
|
-
add_tool = ClaudeAgentSDK.create_tool('add', 'Add two numbers', { a: :number, b: :number }) do |args|
|
|
499
|
-
result = args[:a] + args[:b]
|
|
500
|
-
{ content: [{ type: 'text', text: "#{args[:a]} + #{args[:b]} = #{result}" }] }
|
|
501
|
-
end
|
|
502
|
-
|
|
503
|
-
divide_tool = ClaudeAgentSDK.create_tool('divide', 'Divide numbers', { a: :number, b: :number }) do |args|
|
|
504
|
-
if args[:b] == 0
|
|
505
|
-
{ content: [{ type: 'text', text: 'Error: Division by zero' }], is_error: true }
|
|
506
|
-
else
|
|
507
|
-
result = args[:a] / args[:b]
|
|
508
|
-
{ content: [{ type: 'text', text: "Result: #{result}" }] }
|
|
509
|
-
end
|
|
510
|
-
end
|
|
511
|
-
|
|
512
|
-
# Create server
|
|
513
|
-
calculator = ClaudeAgentSDK.create_sdk_mcp_server(
|
|
514
|
-
name: 'calculator',
|
|
515
|
-
tools: [add_tool, divide_tool]
|
|
516
|
-
)
|
|
517
|
-
|
|
518
|
-
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
519
|
-
mcp_servers: { calc: calculator },
|
|
520
|
-
allowed_tools: ['mcp__calc__add', 'mcp__calc__divide']
|
|
521
|
-
)
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
### Mixed Server Support
|
|
525
|
-
|
|
526
|
-
You can use both SDK and external MCP servers together:
|
|
527
|
-
|
|
528
|
-
```ruby
|
|
529
|
-
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
530
|
-
mcp_servers: {
|
|
531
|
-
internal: sdk_server, # In-process SDK server
|
|
532
|
-
external: { # External subprocess server
|
|
533
|
-
type: 'stdio',
|
|
534
|
-
command: 'external-server'
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
)
|
|
538
|
-
```
|
|
539
|
-
|
|
540
|
-
### MCP Resources and Prompts
|
|
541
|
-
|
|
542
|
-
SDK MCP servers can also expose **resources** (data sources) and **prompts** (reusable templates):
|
|
543
|
-
|
|
544
|
-
```ruby
|
|
545
|
-
# Create a resource (data source Claude can read)
|
|
546
|
-
config_resource = ClaudeAgentSDK.create_resource(
|
|
547
|
-
uri: 'config://app/settings',
|
|
548
|
-
name: 'Application Settings',
|
|
549
|
-
description: 'Current app configuration',
|
|
550
|
-
mime_type: 'application/json'
|
|
551
|
-
) do
|
|
552
|
-
config_data = { app_name: 'MyApp', version: '1.0.0' }
|
|
553
|
-
{
|
|
554
|
-
contents: [{
|
|
555
|
-
uri: 'config://app/settings',
|
|
556
|
-
mimeType: 'application/json',
|
|
557
|
-
text: JSON.pretty_generate(config_data)
|
|
558
|
-
}]
|
|
559
|
-
}
|
|
560
|
-
end
|
|
561
|
-
|
|
562
|
-
# Create a prompt template
|
|
563
|
-
review_prompt = ClaudeAgentSDK.create_prompt(
|
|
564
|
-
name: 'code_review',
|
|
565
|
-
description: 'Review code for best practices',
|
|
566
|
-
arguments: [
|
|
567
|
-
{ name: 'code', description: 'Code to review', required: true }
|
|
568
|
-
]
|
|
569
|
-
) do |args|
|
|
570
|
-
{
|
|
571
|
-
messages: [{
|
|
572
|
-
role: 'user',
|
|
573
|
-
content: {
|
|
574
|
-
type: 'text',
|
|
575
|
-
text: "Review this code: #{args[:code]}"
|
|
576
|
-
}
|
|
577
|
-
}]
|
|
578
|
-
}
|
|
579
|
-
end
|
|
580
|
-
|
|
581
|
-
# Create server with tools, resources, and prompts
|
|
582
|
-
server = ClaudeAgentSDK.create_sdk_mcp_server(
|
|
583
|
-
name: 'dev-tools',
|
|
584
|
-
tools: [my_tool],
|
|
585
|
-
resources: [config_resource],
|
|
586
|
-
prompts: [review_prompt]
|
|
587
|
-
)
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
For complete examples, see [examples/mcp_calculator.rb](examples/mcp_calculator.rb) and [examples/mcp_resources_prompts_example.rb](examples/mcp_resources_prompts_example.rb).
|
|
591
|
-
|
|
592
|
-
## Hooks
|
|
593
|
-
|
|
594
|
-
A **hook** is a Ruby proc/lambda that the Claude Code *application* (*not* Claude) invokes at specific points of the Claude agent loop. Hooks can provide deterministic processing and automated feedback for Claude. Read more in [Claude Code Hooks Reference](https://docs.anthropic.com/en/docs/claude-code/hooks).
|
|
595
|
-
|
|
596
|
-
### Supported Events
|
|
597
|
-
|
|
598
|
-
All hook input objects include common fields like `session_id`, `transcript_path`, `cwd`, and `permission_mode`.
|
|
599
|
-
|
|
600
|
-
- `PreToolUse` → `PreToolUseHookInput` (`tool_name`, `tool_input`, `tool_use_id`)
|
|
601
|
-
- `PostToolUse` → `PostToolUseHookInput` (`tool_name`, `tool_input`, `tool_response`, `tool_use_id`)
|
|
602
|
-
- `PostToolUseFailure` → `PostToolUseFailureHookInput` (`tool_name`, `tool_input`, `tool_use_id`, `error`, `is_interrupt`)
|
|
603
|
-
- `UserPromptSubmit` → `UserPromptSubmitHookInput` (`prompt`)
|
|
604
|
-
- `Stop` → `StopHookInput` (`stop_hook_active`)
|
|
605
|
-
- `SubagentStop` → `SubagentStopHookInput` (`stop_hook_active`, `agent_id`, `agent_transcript_path`, `agent_type`)
|
|
606
|
-
- `PreCompact` → `PreCompactHookInput` (`trigger`, `custom_instructions`)
|
|
607
|
-
- `Notification` → `NotificationHookInput` (`message`, `title`, `notification_type`)
|
|
608
|
-
- `SubagentStart` → `SubagentStartHookInput` (`agent_id`, `agent_type`)
|
|
609
|
-
- `PermissionRequest` → `PermissionRequestHookInput` (`tool_name`, `tool_input`, `permission_suggestions`)
|
|
610
|
-
|
|
611
|
-
### Example
|
|
612
|
-
|
|
613
|
-
```ruby
|
|
614
|
-
require 'claude_agent_sdk'
|
|
615
|
-
require 'async'
|
|
616
|
-
|
|
617
|
-
Async do
|
|
618
|
-
# Define a hook that blocks dangerous bash commands
|
|
619
|
-
bash_hook = lambda do |input, _tool_use_id, _context|
|
|
620
|
-
# Hook inputs are typed objects (e.g., PreToolUseHookInput) with Ruby-style accessors
|
|
621
|
-
return {} unless input.respond_to?(:tool_name) && input.tool_name == 'Bash'
|
|
622
|
-
|
|
623
|
-
tool_input = input.tool_input || {}
|
|
624
|
-
command = tool_input[:command] || tool_input['command'] || ''
|
|
625
|
-
block_patterns = ['rm -rf', 'foo.sh']
|
|
626
|
-
|
|
627
|
-
block_patterns.each do |pattern|
|
|
628
|
-
if command.include?(pattern)
|
|
629
|
-
return {
|
|
630
|
-
hookSpecificOutput: {
|
|
631
|
-
hookEventName: 'PreToolUse',
|
|
632
|
-
permissionDecision: 'deny',
|
|
633
|
-
permissionDecisionReason: "Command contains forbidden pattern: #{pattern}"
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
end
|
|
637
|
-
end
|
|
638
|
-
|
|
639
|
-
{} # Allow if no patterns match
|
|
640
|
-
end
|
|
641
|
-
|
|
642
|
-
# Create options with hook
|
|
643
|
-
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
644
|
-
allowed_tools: ['Bash'],
|
|
645
|
-
hooks: {
|
|
646
|
-
'PreToolUse' => [
|
|
647
|
-
ClaudeAgentSDK::HookMatcher.new(
|
|
648
|
-
matcher: 'Bash',
|
|
649
|
-
hooks: [bash_hook]
|
|
650
|
-
)
|
|
651
|
-
]
|
|
652
|
-
}
|
|
653
|
-
)
|
|
654
|
-
|
|
655
|
-
client = ClaudeAgentSDK::Client.new(options: options)
|
|
656
|
-
client.connect
|
|
657
|
-
|
|
658
|
-
# Test: Command with forbidden pattern (will be blocked)
|
|
659
|
-
client.query("Run the bash command: ./foo.sh --help")
|
|
660
|
-
client.receive_response { |msg| puts msg }
|
|
661
|
-
|
|
662
|
-
client.disconnect
|
|
663
|
-
end.wait
|
|
664
|
-
```
|
|
665
|
-
|
|
666
|
-
For more examples, see [examples/hooks_example.rb](examples/hooks_example.rb).
|
|
667
|
-
|
|
668
|
-
## Permission Callbacks
|
|
669
|
-
|
|
670
|
-
A **permission callback** is a Ruby proc/lambda that allows you to programmatically control tool execution. This gives you fine-grained control over what tools Claude can use and with what inputs.
|
|
671
|
-
|
|
672
|
-
### Example
|
|
673
|
-
|
|
674
|
-
```ruby
|
|
675
|
-
require 'claude_agent_sdk'
|
|
676
|
-
require 'async'
|
|
677
|
-
|
|
678
|
-
Async do
|
|
679
|
-
# Define a permission callback
|
|
680
|
-
permission_callback = lambda do |tool_name, input, context|
|
|
681
|
-
# Allow Read operations
|
|
682
|
-
if tool_name == 'Read'
|
|
683
|
-
return ClaudeAgentSDK::PermissionResultAllow.new
|
|
684
|
-
end
|
|
685
|
-
|
|
686
|
-
# Block Write to sensitive files
|
|
687
|
-
if tool_name == 'Write'
|
|
688
|
-
file_path = input[:file_path] || input['file_path']
|
|
689
|
-
if file_path && file_path.include?('/etc/')
|
|
690
|
-
return ClaudeAgentSDK::PermissionResultDeny.new(
|
|
691
|
-
message: 'Cannot write to sensitive system files',
|
|
692
|
-
interrupt: false
|
|
693
|
-
)
|
|
694
|
-
end
|
|
695
|
-
return ClaudeAgentSDK::PermissionResultAllow.new
|
|
696
|
-
end
|
|
697
|
-
|
|
698
|
-
# Default: allow
|
|
699
|
-
ClaudeAgentSDK::PermissionResultAllow.new
|
|
700
|
-
end
|
|
701
|
-
|
|
702
|
-
# Create options with permission callback
|
|
703
|
-
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
704
|
-
allowed_tools: ['Read', 'Write', 'Bash'],
|
|
705
|
-
can_use_tool: permission_callback
|
|
706
|
-
)
|
|
707
|
-
|
|
708
|
-
client = ClaudeAgentSDK::Client.new(options: options)
|
|
709
|
-
client.connect
|
|
710
|
-
|
|
711
|
-
# This will be allowed
|
|
712
|
-
client.query("Create a file called test.txt with content 'Hello'")
|
|
713
|
-
client.receive_response { |msg| puts msg }
|
|
714
|
-
|
|
715
|
-
# This will be blocked
|
|
716
|
-
client.query("Write to /etc/passwd")
|
|
717
|
-
client.receive_response { |msg| puts msg }
|
|
718
|
-
|
|
719
|
-
client.disconnect
|
|
720
|
-
end.wait
|
|
721
|
-
```
|
|
722
|
-
|
|
723
|
-
For more examples, see [examples/permission_callback_example.rb](examples/permission_callback_example.rb).
|
|
724
|
-
|
|
725
|
-
## Structured Output
|
|
726
|
-
|
|
727
|
-
Use `output_format` to get validated JSON responses matching a schema. The Claude CLI returns structured output via a `StructuredOutput` tool use block.
|
|
728
|
-
|
|
729
|
-
```ruby
|
|
730
|
-
require 'claude_agent_sdk'
|
|
731
|
-
require 'json'
|
|
732
|
-
|
|
733
|
-
# Define a JSON schema
|
|
734
|
-
schema = {
|
|
735
|
-
type: 'object',
|
|
736
|
-
properties: {
|
|
737
|
-
name: { type: 'string' },
|
|
738
|
-
age: { type: 'integer' },
|
|
739
|
-
skills: { type: 'array', items: { type: 'string' } }
|
|
740
|
-
},
|
|
741
|
-
required: %w[name age skills]
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
745
|
-
output_format: { type: 'json_schema', schema: schema },
|
|
746
|
-
max_turns: 3
|
|
747
|
-
)
|
|
748
|
-
|
|
749
|
-
structured_data = nil
|
|
750
|
-
|
|
751
|
-
ClaudeAgentSDK.query(
|
|
752
|
-
prompt: "Create a profile for a software engineer",
|
|
753
|
-
options: options
|
|
754
|
-
) do |message|
|
|
755
|
-
if message.is_a?(ClaudeAgentSDK::AssistantMessage)
|
|
756
|
-
message.content.each do |block|
|
|
757
|
-
# Structured output comes via StructuredOutput tool use
|
|
758
|
-
if block.is_a?(ClaudeAgentSDK::ToolUseBlock) && block.name == 'StructuredOutput'
|
|
759
|
-
structured_data = block.input
|
|
760
|
-
end
|
|
761
|
-
end
|
|
762
|
-
end
|
|
763
|
-
end
|
|
764
|
-
|
|
765
|
-
if structured_data
|
|
766
|
-
puts "Name: #{structured_data[:name]}"
|
|
767
|
-
puts "Age: #{structured_data[:age]}"
|
|
768
|
-
puts "Skills: #{structured_data[:skills].join(', ')}"
|
|
769
|
-
end
|
|
770
|
-
```
|
|
771
|
-
|
|
772
|
-
For complete examples, see [examples/structured_output_example.rb](examples/structured_output_example.rb).
|
|
773
|
-
|
|
774
|
-
## Thinking Configuration
|
|
775
|
-
|
|
776
|
-
Control extended thinking behavior with typed configuration objects. The `thinking` option takes precedence over the deprecated `max_thinking_tokens`.
|
|
777
|
-
|
|
778
|
-
```ruby
|
|
779
|
-
# Adaptive thinking — CLI dynamically adjusts budget based on task complexity
|
|
780
|
-
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
781
|
-
thinking: ClaudeAgentSDK::ThinkingConfigAdaptive.new
|
|
782
|
-
)
|
|
783
|
-
|
|
784
|
-
# Enabled thinking with explicit token budget
|
|
785
|
-
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
786
|
-
thinking: ClaudeAgentSDK::ThinkingConfigEnabled.new(budget_tokens: 50_000)
|
|
787
|
-
)
|
|
788
|
-
|
|
789
|
-
# Explicitly disabled thinking
|
|
790
|
-
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
791
|
-
thinking: ClaudeAgentSDK::ThinkingConfigDisabled.new
|
|
792
|
-
)
|
|
793
|
-
```
|
|
794
|
-
|
|
795
|
-
Use the `effort` option to control the model's effort level:
|
|
796
|
-
|
|
797
|
-
```ruby
|
|
798
|
-
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
799
|
-
effort: 'xhigh' # see ClaudeAgentSDK::EFFORT_LEVELS
|
|
800
|
-
)
|
|
801
187
|
```
|
|
802
188
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
> **Note:** When `system_prompt` is `nil` (the default), the SDK passes `--system-prompt ""` to the CLI, which suppresses the default Claude Code system prompt. To use the default system prompt, use a `SystemPromptPreset`.
|
|
189
|
+
Resources, prompts, mixed (SDK + external) servers, RubyLLM schema compatibility → see [docs/mcp-servers.md](docs/mcp-servers.md).
|
|
806
190
|
|
|
807
|
-
|
|
191
|
+
## Hooks & Permission Callbacks
|
|
808
192
|
|
|
809
|
-
|
|
193
|
+
**Hooks** let the Claude Code application invoke your Ruby code at all 27 lifecycle points (`PreToolUse`, `PostToolUse`, `UserPromptSubmit`, `Stop`, `PreCompact`, etc.) with typed input objects. **Permission callbacks** give you programmatic control over tool execution.
|
|
810
194
|
|
|
811
195
|
```ruby
|
|
812
196
|
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
append: '...your shared domain instructions...',
|
|
816
|
-
exclude_dynamic_sections: true
|
|
817
|
-
)
|
|
197
|
+
hooks: { 'PreToolUse' => [ClaudeAgentSDK::HookMatcher.new(matcher: 'Bash', hooks: [my_hook])] },
|
|
198
|
+
can_use_tool: my_permission_callback
|
|
818
199
|
)
|
|
819
200
|
```
|
|
820
201
|
|
|
821
|
-
|
|
202
|
+
→ Full event list, typed inputs, and worked examples in [docs/hooks-and-permissions.md](docs/hooks-and-permissions.md).
|
|
822
203
|
|
|
823
|
-
##
|
|
204
|
+
## Advanced Topics
|
|
824
205
|
|
|
825
|
-
|
|
206
|
+
| Topic | Reference |
|
|
207
|
+
|-------|-----------|
|
|
208
|
+
| Structured output, thinking config, budget, fallback model, beta features, sandbox, bare mode, file checkpointing | [docs/configuration.md](docs/configuration.md) |
|
|
209
|
+
| Session listing, reading, renaming, tagging, deleting, forking, resume-at-message | [docs/sessions.md](docs/sessions.md) |
|
|
210
|
+
| OpenTelemetry tracing, Langfuse setup, custom observers | [docs/observability.md](docs/observability.md) |
|
|
211
|
+
| Rails integration (fiber safety, ActionCable, sessions, jobs, HTTP MCP, observability initializer) | [docs/rails.md](docs/rails.md) |
|
|
212
|
+
| Message, content block, and configuration type reference | [docs/types.md](docs/types.md) |
|
|
213
|
+
| Error handling, exception hierarchy, timeout configuration | [docs/errors.md](docs/errors.md) |
|
|
826
214
|
|
|
827
|
-
|
|
828
|
-
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
829
|
-
max_budget_usd: 0.10, # Cap at $0.10
|
|
830
|
-
max_turns: 3
|
|
831
|
-
)
|
|
215
|
+
## Examples
|
|
832
216
|
|
|
833
|
-
|
|
834
|
-
if message.is_a?(ClaudeAgentSDK::ResultMessage)
|
|
835
|
-
puts "Cost: $#{message.total_cost_usd}"
|
|
836
|
-
end
|
|
837
|
-
end
|
|
838
|
-
```
|
|
217
|
+
### Core
|
|
839
218
|
|
|
840
|
-
|
|
219
|
+
| Example | Description |
|
|
220
|
+
|---------|-------------|
|
|
221
|
+
| [quick_start.rb](examples/quick_start.rb) | Basic `query()` usage with options |
|
|
222
|
+
| [client_example.rb](examples/client_example.rb) | Interactive Client usage |
|
|
223
|
+
| [message_types_example.rb](examples/message_types_example.rb) | Handling all 24 SDK message types |
|
|
224
|
+
| [streaming_input_example.rb](examples/streaming_input_example.rb) | Streaming input for multi-turn conversations |
|
|
225
|
+
| [session_resumption_example.rb](examples/session_resumption_example.rb) | Multi-turn conversations with session persistence |
|
|
226
|
+
| [structured_output_example.rb](examples/structured_output_example.rb) | JSON schema structured output |
|
|
227
|
+
| [error_handling_example.rb](examples/error_handling_example.rb) | Error handling with `AssistantMessage.error` |
|
|
228
|
+
| [bare_mode_example.rb](examples/bare_mode_example.rb) | Minimal startup with `bare: true` |
|
|
229
|
+
| [sandbox_example.rb](examples/sandbox_example.rb) | Full sandbox settings (network, filesystem, violations) |
|
|
230
|
+
|
|
231
|
+
### MCP Servers
|
|
841
232
|
|
|
842
|
-
|
|
233
|
+
| Example | Description |
|
|
234
|
+
|---------|-------------|
|
|
235
|
+
| [mcp_calculator.rb](examples/mcp_calculator.rb) | Custom tools with SDK MCP servers |
|
|
236
|
+
| [mcp_resources_prompts_example.rb](examples/mcp_resources_prompts_example.rb) | MCP resources and prompts |
|
|
237
|
+
| [http_mcp_server_example.rb](examples/http_mcp_server_example.rb) | HTTP/SSE MCP server configuration |
|
|
843
238
|
|
|
844
|
-
|
|
239
|
+
### Hooks & Permissions
|
|
845
240
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
)
|
|
241
|
+
| Example | Description |
|
|
242
|
+
|---------|-------------|
|
|
243
|
+
| [hooks_example.rb](examples/hooks_example.rb) | Using hooks to control tool execution |
|
|
244
|
+
| [advanced_hooks_example.rb](examples/advanced_hooks_example.rb) | Typed hook inputs/outputs |
|
|
245
|
+
| [lifecycle_hooks_example.rb](examples/lifecycle_hooks_example.rb) | All 27 hook events |
|
|
246
|
+
| [permission_callback_example.rb](examples/permission_callback_example.rb) | Dynamic tool permission control |
|
|
851
247
|
|
|
852
|
-
|
|
853
|
-
if message.is_a?(ClaudeAgentSDK::AssistantMessage)
|
|
854
|
-
puts "Model used: #{message.model}"
|
|
855
|
-
end
|
|
856
|
-
end
|
|
857
|
-
```
|
|
248
|
+
### Advanced
|
|
858
249
|
|
|
859
|
-
|
|
250
|
+
| Example | Description |
|
|
251
|
+
|---------|-------------|
|
|
252
|
+
| [budget_control_example.rb](examples/budget_control_example.rb) | Budget control with `max_budget_usd` |
|
|
253
|
+
| [fallback_model_example.rb](examples/fallback_model_example.rb) | Fallback model configuration |
|
|
254
|
+
| [extended_thinking_example.rb](examples/extended_thinking_example.rb) | Extended thinking |
|
|
255
|
+
| [e2b_transport_example.rb](examples/e2b_transport_example.rb) | Custom transport running CLI in an E2B microVM |
|
|
860
256
|
|
|
861
|
-
|
|
257
|
+
### Observability & Rails
|
|
862
258
|
|
|
863
|
-
|
|
259
|
+
| Example | Description |
|
|
260
|
+
|---------|-------------|
|
|
261
|
+
| [otel_langfuse_example.rb](examples/otel_langfuse_example.rb) | OpenTelemetry tracing with Langfuse backend |
|
|
262
|
+
| [rails_actioncable_example.rb](examples/rails_actioncable_example.rb) | ActionCable streaming to frontend |
|
|
263
|
+
| [rails_background_job_example.rb](examples/rails_background_job_example.rb) | Background jobs with session resumption |
|
|
864
264
|
|
|
865
|
-
|
|
866
|
-
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
867
|
-
betas: ['context-1m-2025-08-07'] # Extended context window
|
|
868
|
-
)
|
|
265
|
+
## Available Tools
|
|
869
266
|
|
|
870
|
-
|
|
871
|
-
puts message
|
|
872
|
-
end
|
|
873
|
-
```
|
|
267
|
+
See the [Claude Code documentation](https://docs.anthropic.com/en/docs/claude-code/settings#tools-available-to-claude) for a complete list of available tools.
|
|
874
268
|
|
|
875
|
-
|
|
269
|
+
## Development
|
|
876
270
|
|
|
877
|
-
|
|
271
|
+
After checking out the repo, run `bundle install` to install dependencies. Then `bundle exec rspec` to run the tests.
|
|
878
272
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
```ruby
|
|
882
|
-
# Using an array of tool names
|
|
883
|
-
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
884
|
-
tools: ['Read', 'Edit', 'Bash'] # Base tools available
|
|
885
|
-
)
|
|
886
|
-
|
|
887
|
-
# Using a preset
|
|
888
|
-
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
889
|
-
tools: ClaudeAgentSDK::ToolsPreset.new(preset: 'claude_code')
|
|
890
|
-
)
|
|
891
|
-
|
|
892
|
-
# Appending to allowed tools
|
|
893
|
-
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
894
|
-
append_allowed_tools: ['Write', 'Bash']
|
|
895
|
-
)
|
|
896
|
-
```
|
|
897
|
-
|
|
898
|
-
## Sandbox Settings
|
|
899
|
-
|
|
900
|
-
Configure [sandbox-runtime](https://github.com/anthropic-experimental/sandbox-runtime) restrictions (network policy, filesystem access) via the CLI's `--sandbox` flag. The CLI handles OS-level process isolation using `srt`.
|
|
901
|
-
|
|
902
|
-
```ruby
|
|
903
|
-
sandbox = ClaudeAgentSDK::SandboxSettings.new(
|
|
904
|
-
enabled: true,
|
|
905
|
-
auto_allow_bash_if_sandboxed: true,
|
|
906
|
-
network: ClaudeAgentSDK::SandboxNetworkConfig.new(
|
|
907
|
-
allow_local_binding: true
|
|
908
|
-
)
|
|
909
|
-
)
|
|
910
|
-
|
|
911
|
-
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
912
|
-
sandbox: sandbox,
|
|
913
|
-
permission_mode: 'acceptEdits'
|
|
914
|
-
)
|
|
915
|
-
|
|
916
|
-
ClaudeAgentSDK.query(prompt: "Run some commands", options: options) do |message|
|
|
917
|
-
puts message
|
|
918
|
-
end
|
|
919
|
-
```
|
|
920
|
-
|
|
921
|
-
## Bare Mode
|
|
922
|
-
|
|
923
|
-
Bare mode (`--bare`) is a minimal startup mode that skips hooks, LSP, plugin sync, attribution, auto-memory, background prefetches, keychain reads, and CLAUDE.md auto-discovery. It sets `CLAUDE_CODE_SIMPLE=1` internally. This is useful for scripted/programmatic usage where you want fast startup and full control over what's loaded.
|
|
924
|
-
|
|
925
|
-
```ruby
|
|
926
|
-
# Sugar option
|
|
927
|
-
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
928
|
-
bare: true,
|
|
929
|
-
system_prompt: 'You are a code reviewer.',
|
|
930
|
-
permission_mode: 'bypassPermissions'
|
|
931
|
-
)
|
|
932
|
-
|
|
933
|
-
ClaudeAgentSDK.query(prompt: "Review this function", options: options) do |message|
|
|
934
|
-
# ...
|
|
935
|
-
end
|
|
936
|
-
```
|
|
937
|
-
|
|
938
|
-
In bare mode, explicitly provide any context you need:
|
|
939
|
-
|
|
940
|
-
```ruby
|
|
941
|
-
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
942
|
-
bare: true,
|
|
943
|
-
system_prompt: 'You are a helpful assistant.',
|
|
944
|
-
add_dirs: ['/path/to/project'], # CLAUDE.md directories (auto-discovery is off)
|
|
945
|
-
setting_sources: ['project'], # load .claude/settings.json
|
|
946
|
-
allowed_tools: ['Read', 'Grep', 'Glob'],
|
|
947
|
-
permission_mode: 'bypassPermissions'
|
|
948
|
-
)
|
|
949
|
-
```
|
|
950
|
-
|
|
951
|
-
**What bare mode skips:** hooks, LSP, plugin sync, attribution, auto-memory, background prefetches, keychain reads, CLAUDE.md auto-discovery, teammate snapshots, release notes.
|
|
952
|
-
|
|
953
|
-
**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.
|
|
954
|
-
|
|
955
|
-
## File Checkpointing & Rewind
|
|
956
|
-
|
|
957
|
-
Enable file checkpointing to revert file changes to a previous state:
|
|
958
|
-
|
|
959
|
-
```ruby
|
|
960
|
-
require 'async'
|
|
961
|
-
|
|
962
|
-
Async do
|
|
963
|
-
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
964
|
-
enable_file_checkpointing: true,
|
|
965
|
-
permission_mode: 'acceptEdits'
|
|
966
|
-
)
|
|
967
|
-
|
|
968
|
-
client = ClaudeAgentSDK::Client.new(options: options)
|
|
969
|
-
client.connect
|
|
970
|
-
|
|
971
|
-
# Track user message UUIDs for potential rewind
|
|
972
|
-
user_message_uuids = []
|
|
973
|
-
|
|
974
|
-
# First query - create a file
|
|
975
|
-
client.query("Create a test.rb file with some code")
|
|
976
|
-
client.receive_response do |message|
|
|
977
|
-
# Process all message types as needed
|
|
978
|
-
case message
|
|
979
|
-
when ClaudeAgentSDK::UserMessage
|
|
980
|
-
# Capture UUID for rewind capability
|
|
981
|
-
user_message_uuids << message.uuid if message.uuid
|
|
982
|
-
when ClaudeAgentSDK::AssistantMessage
|
|
983
|
-
puts message.text
|
|
984
|
-
when ClaudeAgentSDK::ResultMessage
|
|
985
|
-
puts "Query completed (cost: $#{message.total_cost_usd})"
|
|
986
|
-
end
|
|
987
|
-
end
|
|
988
|
-
|
|
989
|
-
# Second query - modify the file
|
|
990
|
-
client.query("Modify the test.rb file to add error handling")
|
|
991
|
-
client.receive_response do |message|
|
|
992
|
-
user_message_uuids << message.uuid if message.is_a?(ClaudeAgentSDK::UserMessage) && message.uuid
|
|
993
|
-
end
|
|
994
|
-
|
|
995
|
-
# Rewind to the first checkpoint (undoes the second query's changes)
|
|
996
|
-
if user_message_uuids.first
|
|
997
|
-
puts "Rewinding to checkpoint: #{user_message_uuids.first}"
|
|
998
|
-
client.rewind_files(user_message_uuids.first)
|
|
999
|
-
end
|
|
1000
|
-
|
|
1001
|
-
client.disconnect
|
|
1002
|
-
end.wait
|
|
1003
|
-
```
|
|
1004
|
-
|
|
1005
|
-
> **Note:** The `uuid` field on `UserMessage` is populated by the CLI and represents checkpoint identifiers. Rewinding to a UUID restores file state to what it was at that point in the conversation.
|
|
1006
|
-
|
|
1007
|
-
## Session Browsing
|
|
1008
|
-
|
|
1009
|
-
Browse and inspect previous Claude Code sessions directly from Ruby — no CLI subprocess required.
|
|
1010
|
-
|
|
1011
|
-
### Listing Sessions
|
|
1012
|
-
|
|
1013
|
-
```ruby
|
|
1014
|
-
# List all sessions (sorted by most recent first)
|
|
1015
|
-
sessions = ClaudeAgentSDK.list_sessions
|
|
1016
|
-
sessions.each do |session|
|
|
1017
|
-
puts "#{session.session_id}: #{session.summary} (#{session.git_branch})"
|
|
1018
|
-
end
|
|
1019
|
-
|
|
1020
|
-
# List sessions for a specific directory
|
|
1021
|
-
sessions = ClaudeAgentSDK.list_sessions(directory: '/path/to/project', limit: 10)
|
|
1022
|
-
|
|
1023
|
-
# Paginate with offset
|
|
1024
|
-
page2 = ClaudeAgentSDK.list_sessions(directory: '.', limit: 10, offset: 10)
|
|
1025
|
-
|
|
1026
|
-
# Include git worktree sessions
|
|
1027
|
-
sessions = ClaudeAgentSDK.list_sessions(directory: '.', include_worktrees: true)
|
|
1028
|
-
```
|
|
1029
|
-
|
|
1030
|
-
Each `SDKSessionInfo` includes:
|
|
1031
|
-
- `session_id`, `summary`, `last_modified`, `file_size`
|
|
1032
|
-
- `custom_title`, `first_prompt`, `git_branch`, `cwd`
|
|
1033
|
-
|
|
1034
|
-
### Reading Session Messages
|
|
1035
|
-
|
|
1036
|
-
```ruby
|
|
1037
|
-
# Get the full conversation from a session
|
|
1038
|
-
messages = ClaudeAgentSDK.get_session_messages(session_id: 'abc-123-...')
|
|
1039
|
-
messages.each do |msg|
|
|
1040
|
-
puts "[#{msg.type}] #{msg.message}"
|
|
1041
|
-
end
|
|
1042
|
-
|
|
1043
|
-
# Paginate through messages
|
|
1044
|
-
page = ClaudeAgentSDK.get_session_messages(session_id: 'abc-123-...', offset: 10, limit: 20)
|
|
1045
|
-
```
|
|
1046
|
-
|
|
1047
|
-
Each `SessionMessage` includes `type` (`"user"` or `"assistant"`), `uuid`, `session_id`, and `message` (raw API dict).
|
|
1048
|
-
|
|
1049
|
-
> **Note:** Session browsing reads `~/.claude/projects/` JSONL files directly. It respects the `CLAUDE_CONFIG_DIR` environment variable and automatically detects git worktrees.
|
|
1050
|
-
|
|
1051
|
-
## Session Mutations
|
|
1052
|
-
|
|
1053
|
-
Rename or tag sessions programmatically — no CLI subprocess required.
|
|
1054
|
-
|
|
1055
|
-
### Renaming a Session
|
|
1056
|
-
|
|
1057
|
-
```ruby
|
|
1058
|
-
# Rename a session (appends a custom-title JSONL entry)
|
|
1059
|
-
ClaudeAgentSDK.rename_session(
|
|
1060
|
-
session_id: '550e8400-e29b-41d4-a716-446655440000',
|
|
1061
|
-
title: 'My refactoring session',
|
|
1062
|
-
directory: '/path/to/project' # optional
|
|
1063
|
-
)
|
|
1064
|
-
```
|
|
1065
|
-
|
|
1066
|
-
### Tagging a Session
|
|
1067
|
-
|
|
1068
|
-
```ruby
|
|
1069
|
-
# Tag a session (Unicode-sanitized before storing)
|
|
1070
|
-
ClaudeAgentSDK.tag_session(
|
|
1071
|
-
session_id: '550e8400-e29b-41d4-a716-446655440000',
|
|
1072
|
-
tag: 'experiment'
|
|
1073
|
-
)
|
|
1074
|
-
|
|
1075
|
-
# Clear a tag
|
|
1076
|
-
ClaudeAgentSDK.tag_session(
|
|
1077
|
-
session_id: '550e8400-e29b-41d4-a716-446655440000',
|
|
1078
|
-
tag: nil
|
|
1079
|
-
)
|
|
1080
|
-
```
|
|
1081
|
-
|
|
1082
|
-
### Deleting a Session
|
|
1083
|
-
|
|
1084
|
-
```ruby
|
|
1085
|
-
# Hard-delete a session (removes the JSONL file permanently)
|
|
1086
|
-
ClaudeAgentSDK.delete_session(
|
|
1087
|
-
session_id: '550e8400-e29b-41d4-a716-446655440000',
|
|
1088
|
-
directory: '/path/to/project' # optional
|
|
1089
|
-
)
|
|
1090
|
-
```
|
|
1091
|
-
|
|
1092
|
-
### Forking a Session
|
|
1093
|
-
|
|
1094
|
-
```ruby
|
|
1095
|
-
# Fork a session into a new branch with fresh UUIDs
|
|
1096
|
-
result = ClaudeAgentSDK.fork_session(
|
|
1097
|
-
session_id: '550e8400-e29b-41d4-a716-446655440000',
|
|
1098
|
-
title: 'Experiment branch' # optional, auto-generated if omitted
|
|
1099
|
-
)
|
|
1100
|
-
puts result.session_id # UUID of the new forked session
|
|
1101
|
-
|
|
1102
|
-
# Fork up to a specific message (partial fork)
|
|
1103
|
-
result = ClaudeAgentSDK.fork_session(
|
|
1104
|
-
session_id: '550e8400-e29b-41d4-a716-446655440000',
|
|
1105
|
-
up_to_message_id: 'message-uuid-here'
|
|
1106
|
-
)
|
|
1107
|
-
```
|
|
1108
|
-
|
|
1109
|
-
> **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. `fork_session` uses `O_CREAT | O_EXCL` to prevent race conditions.
|
|
1110
|
-
|
|
1111
|
-
### Resuming at a Specific Message
|
|
1112
|
-
|
|
1113
|
-
`resume_session_at` truncates the resumed conversation to messages up to **and including** the assistant message with the given UUID — useful for rewriting history from a known point or branching exploration without forking the session file. The flag rides on top of `resume`, so the original session ID is preserved; only the in-memory history loaded for the new turn is shortened.
|
|
1114
|
-
|
|
1115
|
-
```ruby
|
|
1116
|
-
ClaudeAgentSDK.query(
|
|
1117
|
-
prompt: 'Try a different approach',
|
|
1118
|
-
options: ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
1119
|
-
resume: '550e8400-e29b-41d4-a716-446655440000',
|
|
1120
|
-
resume_session_at: 'assistant-message-uuid-from-history'
|
|
1121
|
-
)
|
|
1122
|
-
) do |message|
|
|
1123
|
-
# ...
|
|
1124
|
-
end
|
|
1125
|
-
```
|
|
1126
|
-
|
|
1127
|
-
`resume_session_at` requires `resume`; the SDK raises `ArgumentError` from `CommandBuilder` when this constraint is violated, matching the underlying CLI's validation but surfacing it synchronously in the caller's stack.
|
|
1128
|
-
|
|
1129
|
-
## Observability (OpenTelemetry / Langfuse)
|
|
1130
|
-
|
|
1131
|
-
The SDK includes a built-in **observer interface** and an **OpenTelemetry observer** for tracing agent sessions. Traces are emitted using standard `gen_ai.*` semantic conventions, compatible with Langfuse, Jaeger, Datadog, and any OTel backend.
|
|
1132
|
-
|
|
1133
|
-
### How It Works
|
|
1134
|
-
|
|
1135
|
-
Register observers via `ClaudeAgentOptions`. The SDK calls `on_message` for every parsed message in both `query()` and `Client`, and `on_close` when the session ends. Observer errors are silently rescued so they never crash your application.
|
|
1136
|
-
|
|
1137
|
-
```
|
|
1138
|
-
claude_agent.session (root span — one per query/session)
|
|
1139
|
-
├── claude_agent.generation (per AssistantMessage, with model + token usage)
|
|
1140
|
-
├── claude_agent.tool.Bash (per tool call, open on ToolUseBlock, close on ToolResultBlock)
|
|
1141
|
-
├── claude_agent.tool.Read
|
|
1142
|
-
├── claude_agent.generation
|
|
1143
|
-
└── ...
|
|
1144
|
-
```
|
|
1145
|
-
|
|
1146
|
-
### Setup with Langfuse
|
|
1147
|
-
|
|
1148
|
-
**1. Install the OTel gems** (not bundled with the SDK — you choose your exporter):
|
|
1149
|
-
|
|
1150
|
-
```bash
|
|
1151
|
-
gem install opentelemetry-sdk opentelemetry-exporter-otlp
|
|
1152
|
-
```
|
|
1153
|
-
|
|
1154
|
-
Or add to your Gemfile:
|
|
1155
|
-
|
|
1156
|
-
```ruby
|
|
1157
|
-
gem 'opentelemetry-sdk', '~> 1.4'
|
|
1158
|
-
gem 'opentelemetry-exporter-otlp', '~> 0.28'
|
|
1159
|
-
```
|
|
1160
|
-
|
|
1161
|
-
**2. Configure the OTel SDK** to export to your Langfuse instance:
|
|
1162
|
-
|
|
1163
|
-
```ruby
|
|
1164
|
-
require 'base64'
|
|
1165
|
-
require 'opentelemetry/sdk'
|
|
1166
|
-
require 'opentelemetry/exporter/otlp'
|
|
1167
|
-
|
|
1168
|
-
# Langfuse authenticates via Basic Auth over OTLP
|
|
1169
|
-
public_key = ENV['LANGFUSE_PUBLIC_KEY']
|
|
1170
|
-
secret_key = ENV['LANGFUSE_SECRET_KEY']
|
|
1171
|
-
auth = Base64.strict_encode64("#{public_key}:#{secret_key}")
|
|
1172
|
-
|
|
1173
|
-
# Self-hosted or cloud: https://cloud.langfuse.com (EU) / https://us.cloud.langfuse.com (US)
|
|
1174
|
-
langfuse_host = ENV.fetch('LANGFUSE_HOST', 'https://cloud.langfuse.com')
|
|
1175
|
-
|
|
1176
|
-
OpenTelemetry::SDK.configure do |c|
|
|
1177
|
-
c.service_name = 'my-agent-app'
|
|
1178
|
-
c.add_span_processor(
|
|
1179
|
-
OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
|
|
1180
|
-
OpenTelemetry::Exporter::OTLP::Exporter.new(
|
|
1181
|
-
endpoint: "#{langfuse_host}/api/public/otel/v1/traces",
|
|
1182
|
-
headers: {
|
|
1183
|
-
'Authorization' => "Basic #{auth}",
|
|
1184
|
-
'x-langfuse-ingestion-version' => '4'
|
|
1185
|
-
}
|
|
1186
|
-
)
|
|
1187
|
-
)
|
|
1188
|
-
)
|
|
1189
|
-
end
|
|
1190
|
-
```
|
|
1191
|
-
|
|
1192
|
-
**3. Create the observer and run a query:**
|
|
1193
|
-
|
|
1194
|
-
```ruby
|
|
1195
|
-
require 'claude_agent_sdk'
|
|
1196
|
-
require 'claude_agent_sdk/instrumentation'
|
|
1197
|
-
|
|
1198
|
-
observer = ClaudeAgentSDK::Instrumentation::OTelObserver.new(
|
|
1199
|
-
'langfuse.session.id' => 'my-session-123', # optional: group traces by session
|
|
1200
|
-
'user.id' => 'user-42' # optional: tag with user ID
|
|
1201
|
-
)
|
|
1202
|
-
|
|
1203
|
-
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
1204
|
-
observers: [observer],
|
|
1205
|
-
allowed_tools: ['Bash', 'Read'],
|
|
1206
|
-
permission_mode: 'bypassPermissions'
|
|
1207
|
-
)
|
|
1208
|
-
|
|
1209
|
-
ClaudeAgentSDK.query(prompt: "List files in /tmp", options: options) do |msg|
|
|
1210
|
-
puts msg.text if msg.is_a?(ClaudeAgentSDK::AssistantMessage)
|
|
1211
|
-
end
|
|
1212
|
-
|
|
1213
|
-
# For long-running apps, flush before exit:
|
|
1214
|
-
# OpenTelemetry.tracer_provider.shutdown
|
|
1215
|
-
```
|
|
1216
|
-
|
|
1217
|
-
### Span Attributes
|
|
1218
|
-
|
|
1219
|
-
The OTel observer sets attributes using both `gen_ai.*` (OTel GenAI) and OpenInference conventions for maximum backend compatibility:
|
|
1220
|
-
|
|
1221
|
-
| Span | Type | Key Attributes |
|
|
1222
|
-
|------|------|----------------|
|
|
1223
|
-
| `claude_agent.session` | `agent` | `gen_ai.system`, `gen_ai.request.model`, `session.id`, `input.value`, `output.value`, `gen_ai.usage.cost`, `llm.cost.total` |
|
|
1224
|
-
| `claude_agent.generation` | `generation` | `gen_ai.response.model`, `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens`, `output.value` |
|
|
1225
|
-
| `claude_agent.tool.*` | `tool` | `tool.name`, `input.value`, `output.value` |
|
|
1226
|
-
|
|
1227
|
-
Events (`api_retry`, `rate_limit`, `tool_progress`) are recorded on the root span.
|
|
1228
|
-
|
|
1229
|
-
The `langfuse.observation.type` attribute is set on each span (`agent`/`generation`/`tool`) to enable Langfuse's **trace flow diagram** (DAG graph visualization).
|
|
1230
|
-
|
|
1231
|
-
### Custom Observers
|
|
1232
|
-
|
|
1233
|
-
Implement the `Observer` module to build your own instrumentation:
|
|
1234
|
-
|
|
1235
|
-
```ruby
|
|
1236
|
-
class MyObserver
|
|
1237
|
-
include ClaudeAgentSDK::Observer
|
|
1238
|
-
|
|
1239
|
-
def on_message(message)
|
|
1240
|
-
case message
|
|
1241
|
-
when ClaudeAgentSDK::ResultMessage
|
|
1242
|
-
puts "Cost: $#{message.total_cost_usd}, Tokens: #{message.usage}"
|
|
1243
|
-
end
|
|
1244
|
-
end
|
|
1245
|
-
|
|
1246
|
-
def on_close
|
|
1247
|
-
puts "Session ended"
|
|
1248
|
-
end
|
|
1249
|
-
end
|
|
1250
|
-
|
|
1251
|
-
options = ClaudeAgentSDK::ClaudeAgentOptions.new(observers: [MyObserver.new])
|
|
1252
|
-
```
|
|
1253
|
-
|
|
1254
|
-
For a complete multi-tool example, see [examples/otel_langfuse_example.rb](examples/otel_langfuse_example.rb).
|
|
1255
|
-
|
|
1256
|
-
## Rails Integration
|
|
1257
|
-
|
|
1258
|
-
The SDK integrates well with Rails applications. Here are common patterns:
|
|
1259
|
-
|
|
1260
|
-
### Thread-keyed libraries are safe inside SDK callbacks
|
|
1261
|
-
|
|
1262
|
-
The SDK depends on [`async`](https://github.com/socketry/async), which installs
|
|
1263
|
-
a Fiber scheduler that multiplexes fibers onto a single OS thread and
|
|
1264
|
-
intercepts IO so blocking calls yield to siblings. Most mature Ruby libraries
|
|
1265
|
-
are thread-safe but not fiber-safe — they key state (checked-out DB
|
|
1266
|
-
connections, per-thread caches, request stores) on `Thread.current`. When the
|
|
1267
|
-
scheduler interleaves two fibers on one thread, those fibers share the same
|
|
1268
|
-
state slot, and interleaved IO on a shared connection silently corrupts wire
|
|
1269
|
-
protocols. This affects every DB driver keyed by thread (`pg`, `mysql2`,
|
|
1270
|
-
`sqlite3`), ActiveRecord's connection pool, and HTTP/cache clients pooled per
|
|
1271
|
-
thread.
|
|
1272
|
-
|
|
1273
|
-
You do **not** need to think about this. The SDK hops to a plain thread at
|
|
1274
|
-
every user-callback boundary — message blocks given to `query` / `Client`, SDK
|
|
1275
|
-
MCP tool handlers, hooks, permission callbacks, and observer methods — so
|
|
1276
|
-
your code runs with no Fiber scheduler active and inherits the ordinary
|
|
1277
|
-
thread-keyed assumptions every Rails / Sidekiq / Kamal app already makes:
|
|
1278
|
-
|
|
1279
|
-
```ruby
|
|
1280
|
-
tool = ClaudeAgentSDK.create_tool('lookup_user', 'Look up a user', { id: Integer }) do |args|
|
|
1281
|
-
user = User.find(args[:id]) # just works
|
|
1282
|
-
{ content: [{ type: 'text', text: user.name }] }
|
|
1283
|
-
end
|
|
1284
|
-
|
|
1285
|
-
ClaudeAgentSDK.query(prompt: '...') do |message|
|
|
1286
|
-
Message.create!(role: 'assistant', body: message.to_s) # just works
|
|
1287
|
-
end
|
|
1288
|
-
```
|
|
1289
|
-
|
|
1290
|
-
The trade-off: because callbacks run on a plain thread rather than inside
|
|
1291
|
-
an `Async::Task`, fiber-specific primitives aren't available to them —
|
|
1292
|
-
`Async::Task.current` will raise "No async task available". If a callback
|
|
1293
|
-
wants cooperative concurrency it should open its own `Async { }` block. In
|
|
1294
|
-
practice, callbacks typically do some Ruby work, call external services, and
|
|
1295
|
-
return — so this rarely matters. If you wrap your own call site in an outer
|
|
1296
|
-
`Async { }` block, the scheduler is visible to your code again; you've opted
|
|
1297
|
-
in, and whatever fiber-safety rules your app uses apply there.
|
|
1298
|
-
|
|
1299
|
-
### ActionCable Streaming
|
|
1300
|
-
|
|
1301
|
-
Stream Claude responses to the frontend in real-time:
|
|
1302
|
-
|
|
1303
|
-
```ruby
|
|
1304
|
-
# app/jobs/chat_agent_job.rb
|
|
1305
|
-
class ChatAgentJob < ApplicationJob
|
|
1306
|
-
queue_as :claude_agents
|
|
1307
|
-
|
|
1308
|
-
def perform(chat_id, message_content)
|
|
1309
|
-
Async do
|
|
1310
|
-
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
1311
|
-
system_prompt: { type: 'preset', preset: 'claude_code' },
|
|
1312
|
-
permission_mode: 'bypassPermissions'
|
|
1313
|
-
)
|
|
1314
|
-
|
|
1315
|
-
client = ClaudeAgentSDK::Client.new(options: options)
|
|
1316
|
-
|
|
1317
|
-
begin
|
|
1318
|
-
client.connect
|
|
1319
|
-
client.query(message_content)
|
|
1320
|
-
|
|
1321
|
-
client.receive_response do |message|
|
|
1322
|
-
case message
|
|
1323
|
-
when ClaudeAgentSDK::AssistantMessage
|
|
1324
|
-
ChatChannel.broadcast_to(chat_id, { type: 'chunk', content: message.text })
|
|
1325
|
-
|
|
1326
|
-
when ClaudeAgentSDK::ResultMessage
|
|
1327
|
-
ChatChannel.broadcast_to(chat_id, {
|
|
1328
|
-
type: 'complete',
|
|
1329
|
-
content: message.result,
|
|
1330
|
-
cost: message.total_cost_usd
|
|
1331
|
-
})
|
|
1332
|
-
end
|
|
1333
|
-
end
|
|
1334
|
-
ensure
|
|
1335
|
-
client.disconnect
|
|
1336
|
-
end
|
|
1337
|
-
end.wait
|
|
1338
|
-
end
|
|
1339
|
-
end
|
|
1340
|
-
```
|
|
1341
|
-
|
|
1342
|
-
### Session Resumption
|
|
1343
|
-
|
|
1344
|
-
Persist Claude sessions for multi-turn conversations:
|
|
1345
|
-
|
|
1346
|
-
```ruby
|
|
1347
|
-
# app/models/chat_session.rb
|
|
1348
|
-
class ChatSession < ApplicationRecord
|
|
1349
|
-
# Columns: id, claude_session_id, user_id, created_at, updated_at
|
|
1350
|
-
|
|
1351
|
-
def send_message(content)
|
|
1352
|
-
options = build_options
|
|
1353
|
-
client = ClaudeAgentSDK::Client.new(options: options)
|
|
1354
|
-
|
|
1355
|
-
Async do
|
|
1356
|
-
client.connect
|
|
1357
|
-
client.query(content, session_id: claude_session_id ? nil : generate_session_id)
|
|
1358
|
-
|
|
1359
|
-
client.receive_response do |message|
|
|
1360
|
-
if message.is_a?(ClaudeAgentSDK::ResultMessage)
|
|
1361
|
-
# Save session ID for next message
|
|
1362
|
-
update!(claude_session_id: message.session_id)
|
|
1363
|
-
end
|
|
1364
|
-
end
|
|
1365
|
-
ensure
|
|
1366
|
-
client.disconnect
|
|
1367
|
-
end.wait
|
|
1368
|
-
end
|
|
1369
|
-
|
|
1370
|
-
private
|
|
1371
|
-
|
|
1372
|
-
def build_options
|
|
1373
|
-
opts = {
|
|
1374
|
-
permission_mode: 'bypassPermissions',
|
|
1375
|
-
setting_sources: []
|
|
1376
|
-
}
|
|
1377
|
-
opts[:resume] = claude_session_id if claude_session_id.present?
|
|
1378
|
-
ClaudeAgentSDK::ClaudeAgentOptions.new(**opts)
|
|
1379
|
-
end
|
|
1380
|
-
|
|
1381
|
-
def generate_session_id
|
|
1382
|
-
"chat_#{id}_#{Time.current.to_i}"
|
|
1383
|
-
end
|
|
1384
|
-
end
|
|
1385
|
-
```
|
|
1386
|
-
|
|
1387
|
-
### Background Jobs with Error Handling
|
|
1388
|
-
|
|
1389
|
-
```ruby
|
|
1390
|
-
class ClaudeAgentJob < ApplicationJob
|
|
1391
|
-
queue_as :claude_agents
|
|
1392
|
-
retry_on ClaudeAgentSDK::ProcessError, wait: :polynomially_longer, attempts: 3
|
|
1393
|
-
|
|
1394
|
-
def perform(task_id)
|
|
1395
|
-
task = Task.find(task_id)
|
|
1396
|
-
|
|
1397
|
-
Async do
|
|
1398
|
-
execute_agent(task)
|
|
1399
|
-
end.wait
|
|
1400
|
-
|
|
1401
|
-
rescue ClaudeAgentSDK::CLINotFoundError => e
|
|
1402
|
-
task.update!(status: 'failed', error: 'Claude CLI not installed')
|
|
1403
|
-
raise
|
|
1404
|
-
end
|
|
1405
|
-
|
|
1406
|
-
private
|
|
1407
|
-
|
|
1408
|
-
def execute_agent(task)
|
|
1409
|
-
# ... agent execution
|
|
1410
|
-
end
|
|
1411
|
-
end
|
|
1412
|
-
```
|
|
1413
|
-
|
|
1414
|
-
### HTTP MCP Servers
|
|
1415
|
-
|
|
1416
|
-
Connect to remote tool services:
|
|
1417
|
-
|
|
1418
|
-
```ruby
|
|
1419
|
-
mcp_servers = {
|
|
1420
|
-
'api_tools' => ClaudeAgentSDK::McpHttpServerConfig.new(
|
|
1421
|
-
url: ENV['MCP_SERVER_URL'],
|
|
1422
|
-
headers: { 'Authorization' => "Bearer #{ENV['MCP_TOKEN']}" }
|
|
1423
|
-
).to_h
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
1427
|
-
mcp_servers: mcp_servers,
|
|
1428
|
-
permission_mode: 'bypassPermissions'
|
|
1429
|
-
)
|
|
1430
|
-
```
|
|
1431
|
-
|
|
1432
|
-
### Observability in Rails
|
|
1433
|
-
|
|
1434
|
-
Add OpenTelemetry tracing to your Rails app with a single initializer:
|
|
1435
|
-
|
|
1436
|
-
```ruby
|
|
1437
|
-
# config/initializers/opentelemetry.rb
|
|
1438
|
-
require 'base64'
|
|
1439
|
-
require 'opentelemetry/sdk'
|
|
1440
|
-
require 'opentelemetry/exporter/otlp'
|
|
1441
|
-
|
|
1442
|
-
if ENV['LANGFUSE_PUBLIC_KEY'].present?
|
|
1443
|
-
auth = Base64.strict_encode64("#{ENV['LANGFUSE_PUBLIC_KEY']}:#{ENV['LANGFUSE_SECRET_KEY']}")
|
|
1444
|
-
langfuse_host = ENV.fetch('LANGFUSE_HOST', 'https://cloud.langfuse.com')
|
|
1445
|
-
|
|
1446
|
-
OpenTelemetry::SDK.configure do |c|
|
|
1447
|
-
c.service_name = Rails.application.class.module_parent_name.underscore
|
|
1448
|
-
c.add_span_processor(
|
|
1449
|
-
OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
|
|
1450
|
-
OpenTelemetry::Exporter::OTLP::Exporter.new(
|
|
1451
|
-
endpoint: "#{langfuse_host}/api/public/otel/v1/traces",
|
|
1452
|
-
headers: {
|
|
1453
|
-
'Authorization' => "Basic #{auth}",
|
|
1454
|
-
'x-langfuse-ingestion-version' => '4'
|
|
1455
|
-
}
|
|
1456
|
-
)
|
|
1457
|
-
)
|
|
1458
|
-
)
|
|
1459
|
-
end
|
|
1460
|
-
end
|
|
1461
|
-
```
|
|
1462
|
-
|
|
1463
|
-
```ruby
|
|
1464
|
-
# config/initializers/claude_agent_sdk.rb
|
|
1465
|
-
require 'claude_agent_sdk/instrumentation'
|
|
1466
|
-
|
|
1467
|
-
ClaudeAgentSDK.configure do |config|
|
|
1468
|
-
config.default_options = {
|
|
1469
|
-
permission_mode: 'bypassPermissions',
|
|
1470
|
-
observers: ENV['LANGFUSE_PUBLIC_KEY'].present? ? [
|
|
1471
|
-
# Use a lambda so each query gets a fresh observer instance (thread-safe).
|
|
1472
|
-
# A single shared instance would have its span state clobbered by concurrent requests.
|
|
1473
|
-
-> { ClaudeAgentSDK::Instrumentation::OTelObserver.new }
|
|
1474
|
-
] : []
|
|
1475
|
-
}
|
|
1476
|
-
end
|
|
1477
|
-
```
|
|
1478
|
-
|
|
1479
|
-
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.
|
|
1480
|
-
|
|
1481
|
-
For complete examples, see:
|
|
1482
|
-
- [examples/rails_actioncable_example.rb](examples/rails_actioncable_example.rb)
|
|
1483
|
-
- [examples/rails_background_job_example.rb](examples/rails_background_job_example.rb)
|
|
1484
|
-
- [examples/session_resumption_example.rb](examples/session_resumption_example.rb)
|
|
1485
|
-
- [examples/http_mcp_server_example.rb](examples/http_mcp_server_example.rb)
|
|
1486
|
-
|
|
1487
|
-
## Types
|
|
1488
|
-
|
|
1489
|
-
See [lib/claude_agent_sdk/types.rb](lib/claude_agent_sdk/types.rb) for complete type definitions.
|
|
1490
|
-
|
|
1491
|
-
### Message Types
|
|
1492
|
-
|
|
1493
|
-
```ruby
|
|
1494
|
-
# Union type of all possible messages
|
|
1495
|
-
Message = UserMessage | AssistantMessage | SystemMessage | ResultMessage
|
|
1496
|
-
```
|
|
1497
|
-
|
|
1498
|
-
#### UserMessage
|
|
1499
|
-
|
|
1500
|
-
User input message.
|
|
1501
|
-
|
|
1502
|
-
```ruby
|
|
1503
|
-
class UserMessage
|
|
1504
|
-
attr_accessor :content, # String | Array<ContentBlock>
|
|
1505
|
-
:uuid, # String | nil - Unique ID for rewind support
|
|
1506
|
-
:parent_tool_use_id, # String | nil
|
|
1507
|
-
:tool_use_result # Hash | nil - Tool result data when message is a tool response
|
|
1508
|
-
end
|
|
1509
|
-
```
|
|
1510
|
-
|
|
1511
|
-
#### AssistantMessage
|
|
1512
|
-
|
|
1513
|
-
Assistant response message with content blocks.
|
|
1514
|
-
|
|
1515
|
-
```ruby
|
|
1516
|
-
class AssistantMessage
|
|
1517
|
-
attr_accessor :content, # Array<ContentBlock>
|
|
1518
|
-
:model, # String
|
|
1519
|
-
:parent_tool_use_id,# String | nil
|
|
1520
|
-
:error, # String | nil ('authentication_failed', 'billing_error', 'rate_limit', 'invalid_request', 'server_error', 'unknown')
|
|
1521
|
-
:usage # Hash | nil - Token usage info from the API response
|
|
1522
|
-
end
|
|
1523
|
-
```
|
|
1524
|
-
|
|
1525
|
-
#### SystemMessage
|
|
1526
|
-
|
|
1527
|
-
System message with metadata. Task lifecycle events are typed subclasses.
|
|
1528
|
-
|
|
1529
|
-
```ruby
|
|
1530
|
-
class SystemMessage
|
|
1531
|
-
attr_accessor :subtype, # String ('init', 'task_started', 'task_progress', 'task_notification', etc.)
|
|
1532
|
-
:data # Hash
|
|
1533
|
-
end
|
|
1534
|
-
|
|
1535
|
-
# Typed subclasses (all inherit from SystemMessage, so is_a?(SystemMessage) still works)
|
|
1536
|
-
class TaskStartedMessage < SystemMessage
|
|
1537
|
-
attr_accessor :task_id, :description, :uuid, :session_id, :tool_use_id, :task_type
|
|
1538
|
-
end
|
|
1539
|
-
|
|
1540
|
-
class TaskProgressMessage < SystemMessage
|
|
1541
|
-
attr_accessor :task_id, :description, :usage, :uuid, :session_id, :tool_use_id, :last_tool_name
|
|
1542
|
-
end
|
|
1543
|
-
|
|
1544
|
-
class TaskNotificationMessage < SystemMessage
|
|
1545
|
-
attr_accessor :task_id, :status, :output_file, :summary, :uuid, :session_id, :tool_use_id, :usage
|
|
1546
|
-
end
|
|
1547
|
-
```
|
|
1548
|
-
|
|
1549
|
-
#### ResultMessage
|
|
1550
|
-
|
|
1551
|
-
Final result message with cost and usage information.
|
|
1552
|
-
|
|
1553
|
-
```ruby
|
|
1554
|
-
class ResultMessage
|
|
1555
|
-
attr_accessor :subtype, # String
|
|
1556
|
-
:duration_ms, # Integer
|
|
1557
|
-
:duration_api_ms, # Integer
|
|
1558
|
-
:is_error, # Boolean
|
|
1559
|
-
:num_turns, # Integer
|
|
1560
|
-
:session_id, # String
|
|
1561
|
-
:stop_reason, # String | nil ('end_turn', 'max_tokens', 'stop_sequence')
|
|
1562
|
-
:total_cost_usd, # Float | nil
|
|
1563
|
-
:usage, # Hash | nil
|
|
1564
|
-
:result, # String | nil (final text result)
|
|
1565
|
-
:structured_output # Hash | nil (when using output_format)
|
|
1566
|
-
end
|
|
1567
|
-
```
|
|
1568
|
-
|
|
1569
|
-
### Content Block Types
|
|
1570
|
-
|
|
1571
|
-
```ruby
|
|
1572
|
-
# Union type of all content blocks
|
|
1573
|
-
ContentBlock = TextBlock | ThinkingBlock | ToolUseBlock | ToolResultBlock | UnknownBlock
|
|
1574
|
-
```
|
|
1575
|
-
|
|
1576
|
-
#### TextBlock
|
|
1577
|
-
|
|
1578
|
-
Text content block.
|
|
1579
|
-
|
|
1580
|
-
```ruby
|
|
1581
|
-
class TextBlock
|
|
1582
|
-
attr_accessor :text # String
|
|
1583
|
-
end
|
|
1584
|
-
```
|
|
1585
|
-
|
|
1586
|
-
#### ThinkingBlock
|
|
1587
|
-
|
|
1588
|
-
Thinking content block (for models with extended thinking capability).
|
|
1589
|
-
|
|
1590
|
-
```ruby
|
|
1591
|
-
class ThinkingBlock
|
|
1592
|
-
attr_accessor :thinking, # String
|
|
1593
|
-
:signature # String
|
|
1594
|
-
end
|
|
1595
|
-
```
|
|
1596
|
-
|
|
1597
|
-
#### ToolUseBlock
|
|
1598
|
-
|
|
1599
|
-
Tool use request block.
|
|
1600
|
-
|
|
1601
|
-
```ruby
|
|
1602
|
-
class ToolUseBlock
|
|
1603
|
-
attr_accessor :id, # String
|
|
1604
|
-
:name, # String
|
|
1605
|
-
:input # Hash
|
|
1606
|
-
end
|
|
1607
|
-
```
|
|
1608
|
-
|
|
1609
|
-
#### ToolResultBlock
|
|
1610
|
-
|
|
1611
|
-
Tool execution result block.
|
|
1612
|
-
|
|
1613
|
-
```ruby
|
|
1614
|
-
class ToolResultBlock
|
|
1615
|
-
attr_accessor :tool_use_id, # String
|
|
1616
|
-
:content, # String | Array<Hash> | nil
|
|
1617
|
-
:is_error # Boolean | nil
|
|
1618
|
-
end
|
|
1619
|
-
```
|
|
1620
|
-
|
|
1621
|
-
#### UnknownBlock
|
|
1622
|
-
|
|
1623
|
-
Generic content block for types the SDK doesn't explicitly handle (e.g., `document` for PDFs, `image` for inline images). Preserves the raw data for forward compatibility with newer CLI versions.
|
|
1624
|
-
|
|
1625
|
-
```ruby
|
|
1626
|
-
class UnknownBlock
|
|
1627
|
-
attr_accessor :type, # String — the original block type (e.g., "document")
|
|
1628
|
-
:data # Hash — the full raw block hash
|
|
1629
|
-
end
|
|
1630
|
-
```
|
|
1631
|
-
|
|
1632
|
-
### Error Types
|
|
1633
|
-
|
|
1634
|
-
```ruby
|
|
1635
|
-
# Base exception class for all SDK errors
|
|
1636
|
-
class ClaudeSDKError < StandardError; end
|
|
1637
|
-
|
|
1638
|
-
# Raised when connection to Claude Code fails
|
|
1639
|
-
class CLIConnectionError < ClaudeSDKError; end
|
|
1640
|
-
|
|
1641
|
-
# Raised when the control protocol does not respond in time
|
|
1642
|
-
class ControlRequestTimeoutError < CLIConnectionError; end
|
|
1643
|
-
|
|
1644
|
-
# Raised when Claude Code CLI is not found
|
|
1645
|
-
class CLINotFoundError < CLIConnectionError
|
|
1646
|
-
# @param message [String] Error message (default: "Claude Code not found")
|
|
1647
|
-
# @param cli_path [String, nil] Optional path to the CLI that was not found
|
|
1648
|
-
end
|
|
1649
|
-
|
|
1650
|
-
# Raised when the Claude Code process fails
|
|
1651
|
-
class ProcessError < ClaudeSDKError
|
|
1652
|
-
attr_reader :exit_code, # Integer | nil
|
|
1653
|
-
:stderr # String | nil
|
|
1654
|
-
end
|
|
1655
|
-
|
|
1656
|
-
# Raised when JSON parsing fails
|
|
1657
|
-
class CLIJSONDecodeError < ClaudeSDKError
|
|
1658
|
-
attr_reader :line, # String - The line that failed to parse
|
|
1659
|
-
:original_error # Exception - The original JSON decode exception
|
|
1660
|
-
end
|
|
1661
|
-
|
|
1662
|
-
# Raised when message parsing fails
|
|
1663
|
-
class MessageParseError < ClaudeSDKError
|
|
1664
|
-
attr_reader :data # Hash | nil
|
|
1665
|
-
end
|
|
1666
|
-
```
|
|
1667
|
-
|
|
1668
|
-
### Configuration Types
|
|
1669
|
-
|
|
1670
|
-
| Type | Description |
|
|
1671
|
-
|------|-------------|
|
|
1672
|
-
| `Configuration` | Global defaults via `ClaudeAgentSDK.configure` block |
|
|
1673
|
-
| `ClaudeAgentOptions` | Main configuration for queries and clients |
|
|
1674
|
-
| `HookMatcher` | Hook configuration with matcher pattern and timeout |
|
|
1675
|
-
| `PermissionResultAllow` | Permission callback result to allow tool use |
|
|
1676
|
-
| `PermissionResultDeny` | Permission callback result to deny tool use |
|
|
1677
|
-
| `AgentDefinition` | Agent definition with description, prompt, tools, model, skills, memory, mcp_servers |
|
|
1678
|
-
| `ThinkingConfigAdaptive` | Adaptive thinking mode (CLI dynamically adjusts budget) |
|
|
1679
|
-
| `ThinkingConfigEnabled` | Enabled thinking with explicit `budget_tokens` |
|
|
1680
|
-
| `ThinkingConfigDisabled` | Disabled thinking |
|
|
1681
|
-
| `SdkMcpTool` | SDK MCP tool definition with name, description, input_schema, handler, annotations |
|
|
1682
|
-
| `McpStdioServerConfig` | MCP server config for stdio transport |
|
|
1683
|
-
| `McpSSEServerConfig` | MCP server config for SSE transport |
|
|
1684
|
-
| `McpHttpServerConfig` | MCP server config for HTTP transport |
|
|
1685
|
-
| `SdkPluginConfig` | SDK plugin configuration |
|
|
1686
|
-
| `McpServerStatus` | Status of a single MCP server connection (with `.parse`) |
|
|
1687
|
-
| `McpStatusResponse` | Response from `get_mcp_status` containing all server statuses (with `.parse`) |
|
|
1688
|
-
| `McpServerInfo` | MCP server name and version |
|
|
1689
|
-
| `McpToolInfo` | MCP tool name, description, and annotations |
|
|
1690
|
-
| `McpToolAnnotations` | MCP tool annotation hints (`read_only`, `destructive`, `open_world`) |
|
|
1691
|
-
| `TaskUsage` | Typed usage data (`total_tokens`, `tool_uses`, `duration_ms`) with `from_hash` factory |
|
|
1692
|
-
| `SDKSessionInfo` | Session metadata from `list_sessions` |
|
|
1693
|
-
| `SessionMessage` | Single message from `get_session_messages` |
|
|
1694
|
-
| `SandboxSettings` | Sandbox settings for isolated command execution |
|
|
1695
|
-
| `SandboxNetworkConfig` | Network configuration for sandbox |
|
|
1696
|
-
| `SandboxIgnoreViolations` | Configure which sandbox violations to ignore |
|
|
1697
|
-
| `SystemPromptPreset` | System prompt preset configuration |
|
|
1698
|
-
| `ToolsPreset` | Tools preset configuration for base tools selection |
|
|
1699
|
-
|
|
1700
|
-
### Constants
|
|
1701
|
-
|
|
1702
|
-
| Constant | Description |
|
|
1703
|
-
|----------|-------------|
|
|
1704
|
-
| `SDK_BETAS` | Available beta features (e.g., `"context-1m-2025-08-07"`) |
|
|
1705
|
-
| `PERMISSION_MODES` | Available permission modes |
|
|
1706
|
-
| `SETTING_SOURCES` | Available setting sources |
|
|
1707
|
-
| `HOOK_EVENTS` | Available hook events |
|
|
1708
|
-
| `ASSISTANT_MESSAGE_ERRORS` | Possible error types in AssistantMessage |
|
|
1709
|
-
| `TASK_NOTIFICATION_STATUSES` | Task lifecycle notification statuses (`completed`, `failed`, `stopped`) |
|
|
1710
|
-
| `MCP_SERVER_CONNECTION_STATUSES` | MCP server connection states (`connected`, `failed`, `needs-auth`, `pending`, `disabled`) |
|
|
1711
|
-
|
|
1712
|
-
## Error Handling
|
|
1713
|
-
|
|
1714
|
-
### AssistantMessage Errors
|
|
1715
|
-
|
|
1716
|
-
`AssistantMessage` includes an `error` field for API-level errors:
|
|
1717
|
-
|
|
1718
|
-
```ruby
|
|
1719
|
-
ClaudeAgentSDK.query(prompt: "Hello") do |message|
|
|
1720
|
-
if message.is_a?(ClaudeAgentSDK::AssistantMessage) && message.error
|
|
1721
|
-
case message.error
|
|
1722
|
-
when 'rate_limit'
|
|
1723
|
-
puts "Rate limited - retry after delay"
|
|
1724
|
-
when 'authentication_failed'
|
|
1725
|
-
puts "Check your API key"
|
|
1726
|
-
when 'billing_error'
|
|
1727
|
-
puts "Check your billing status"
|
|
1728
|
-
when 'invalid_request'
|
|
1729
|
-
puts "Invalid request format"
|
|
1730
|
-
when 'server_error'
|
|
1731
|
-
puts "Server error - retry later"
|
|
1732
|
-
end
|
|
1733
|
-
end
|
|
1734
|
-
end
|
|
1735
|
-
```
|
|
1736
|
-
|
|
1737
|
-
For complete examples, see [examples/error_handling_example.rb](examples/error_handling_example.rb).
|
|
1738
|
-
|
|
1739
|
-
### Exception Handling
|
|
1740
|
-
|
|
1741
|
-
```ruby
|
|
1742
|
-
require 'claude_agent_sdk'
|
|
1743
|
-
|
|
1744
|
-
begin
|
|
1745
|
-
ClaudeAgentSDK.query(prompt: "Hello") do |message|
|
|
1746
|
-
puts message
|
|
1747
|
-
end
|
|
1748
|
-
rescue ClaudeAgentSDK::ControlRequestTimeoutError
|
|
1749
|
-
puts "Control protocol timed out — consider increasing the timeout"
|
|
1750
|
-
rescue ClaudeAgentSDK::CLINotFoundError
|
|
1751
|
-
puts "Please install Claude Code"
|
|
1752
|
-
rescue ClaudeAgentSDK::ProcessError => e
|
|
1753
|
-
puts "Process failed with exit code: #{e.exit_code}"
|
|
1754
|
-
rescue ClaudeAgentSDK::CLIJSONDecodeError => e
|
|
1755
|
-
puts "Failed to parse response: #{e}"
|
|
1756
|
-
end
|
|
1757
|
-
```
|
|
1758
|
-
|
|
1759
|
-
#### Configuring Timeout
|
|
1760
|
-
|
|
1761
|
-
The control request timeout defaults to **1200 seconds** (20 minutes) to accommodate long-running agent sessions. Override it via environment variable:
|
|
1762
|
-
|
|
1763
|
-
```bash
|
|
1764
|
-
# Set a custom timeout (in seconds)
|
|
1765
|
-
export CLAUDE_AGENT_SDK_CONTROL_REQUEST_TIMEOUT_SECONDS=300 # 5 minutes
|
|
1766
|
-
```
|
|
1767
|
-
|
|
1768
|
-
### Error Types
|
|
1769
|
-
|
|
1770
|
-
| Error | Description |
|
|
1771
|
-
|-------|-------------|
|
|
1772
|
-
| `ClaudeSDKError` | Base error for all SDK errors |
|
|
1773
|
-
| `CLIConnectionError` | Connection issues |
|
|
1774
|
-
| `ControlRequestTimeoutError` | Control protocol timeout (configurable via env var) |
|
|
1775
|
-
| `CLINotFoundError` | Claude Code not installed |
|
|
1776
|
-
| `ProcessError` | Process failed (includes `exit_code` and `stderr`) |
|
|
1777
|
-
| `CLIJSONDecodeError` | JSON parsing issues |
|
|
1778
|
-
| `MessageParseError` | Message parsing issues |
|
|
1779
|
-
|
|
1780
|
-
See [lib/claude_agent_sdk/errors.rb](lib/claude_agent_sdk/errors.rb) for all error types.
|
|
1781
|
-
|
|
1782
|
-
## Available Tools
|
|
1783
|
-
|
|
1784
|
-
See the [Claude Code documentation](https://docs.anthropic.com/en/docs/claude-code/settings#tools-available-to-claude) for a complete list of available tools.
|
|
1785
|
-
|
|
1786
|
-
## Examples
|
|
1787
|
-
|
|
1788
|
-
### Core Examples
|
|
1789
|
-
|
|
1790
|
-
| Example | Description |
|
|
1791
|
-
|---------|-------------|
|
|
1792
|
-
| [examples/quick_start.rb](examples/quick_start.rb) | Basic `query()` usage with options |
|
|
1793
|
-
| [examples/client_example.rb](examples/client_example.rb) | Interactive Client usage |
|
|
1794
|
-
| [examples/message_types_example.rb](examples/message_types_example.rb) | Handling all 24 SDK message types |
|
|
1795
|
-
| [examples/streaming_input_example.rb](examples/streaming_input_example.rb) | Streaming input for multi-turn conversations |
|
|
1796
|
-
| [examples/session_resumption_example.rb](examples/session_resumption_example.rb) | Multi-turn conversations with session persistence |
|
|
1797
|
-
| [examples/structured_output_example.rb](examples/structured_output_example.rb) | JSON schema structured output |
|
|
1798
|
-
| [examples/error_handling_example.rb](examples/error_handling_example.rb) | Error handling with `AssistantMessage.error` |
|
|
1799
|
-
| [examples/bare_mode_example.rb](examples/bare_mode_example.rb) | Minimal startup with `bare: true` |
|
|
1800
|
-
| [examples/sandbox_example.rb](examples/sandbox_example.rb) | Full sandbox settings (network, filesystem, violations) |
|
|
1801
|
-
|
|
1802
|
-
### MCP Server Examples
|
|
1803
|
-
|
|
1804
|
-
| Example | Description |
|
|
1805
|
-
|---------|-------------|
|
|
1806
|
-
| [examples/mcp_calculator.rb](examples/mcp_calculator.rb) | Custom tools with SDK MCP servers |
|
|
1807
|
-
| [examples/mcp_resources_prompts_example.rb](examples/mcp_resources_prompts_example.rb) | MCP resources and prompts |
|
|
1808
|
-
| [examples/http_mcp_server_example.rb](examples/http_mcp_server_example.rb) | HTTP/SSE MCP server configuration |
|
|
1809
|
-
|
|
1810
|
-
### Hooks & Permissions
|
|
1811
|
-
|
|
1812
|
-
| Example | Description |
|
|
1813
|
-
|---------|-------------|
|
|
1814
|
-
| [examples/hooks_example.rb](examples/hooks_example.rb) | Using hooks to control tool execution |
|
|
1815
|
-
| [examples/advanced_hooks_example.rb](examples/advanced_hooks_example.rb) | Typed hook inputs/outputs (PreToolUse, PostToolUse) |
|
|
1816
|
-
| [examples/lifecycle_hooks_example.rb](examples/lifecycle_hooks_example.rb) | All 27 hook events (SessionStart, Stop, PostCompact, etc.) |
|
|
1817
|
-
| [examples/permission_callback_example.rb](examples/permission_callback_example.rb) | Dynamic tool permission control |
|
|
1818
|
-
|
|
1819
|
-
### Advanced Features
|
|
1820
|
-
|
|
1821
|
-
| Example | Description |
|
|
1822
|
-
|---------|-------------|
|
|
1823
|
-
| [examples/budget_control_example.rb](examples/budget_control_example.rb) | Budget control with `max_budget_usd` |
|
|
1824
|
-
| [examples/fallback_model_example.rb](examples/fallback_model_example.rb) | Fallback model configuration |
|
|
1825
|
-
| [examples/extended_thinking_example.rb](examples/extended_thinking_example.rb) | Extended thinking (API parity) |
|
|
1826
|
-
|
|
1827
|
-
### Observability
|
|
1828
|
-
|
|
1829
|
-
| Example | Description |
|
|
1830
|
-
|---------|-------------|
|
|
1831
|
-
| [examples/otel_langfuse_example.rb](examples/otel_langfuse_example.rb) | OpenTelemetry tracing with Langfuse backend |
|
|
1832
|
-
|
|
1833
|
-
### Rails Integration
|
|
1834
|
-
|
|
1835
|
-
| Example | Description |
|
|
1836
|
-
|---------|-------------|
|
|
1837
|
-
| [examples/rails_actioncable_example.rb](examples/rails_actioncable_example.rb) | ActionCable streaming to frontend |
|
|
1838
|
-
| [examples/rails_background_job_example.rb](examples/rails_background_job_example.rb) | Background jobs with session resumption |
|
|
1839
|
-
|
|
1840
|
-
## Development
|
|
1841
|
-
|
|
1842
|
-
After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec rspec` to run the tests.
|
|
1843
|
-
|
|
1844
|
-
## License
|
|
273
|
+
## License
|
|
1845
274
|
|
|
1846
275
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
1847
276
|
|
|
1848
|
-
|
|
1849
277
|
## Star History
|
|
1850
278
|
|
|
1851
279
|
<a href="https://www.star-history.com/?repos=ya-luotao%2Fclaude-agent-sdk-ruby&type=date&legend=top-left">
|