claude-agent-sdk 0.16.7 → 0.16.9
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 +33 -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 +47 -6
- data/lib/claude_agent_sdk/version.rb +1 -1
- metadata +11 -2
data/docs/mcp-servers.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Custom Tools (SDK MCP Servers)
|
|
2
|
+
|
|
3
|
+
A **custom tool** is a Ruby proc/lambda that you can offer to Claude, for Claude to invoke as needed. 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.
|
|
4
|
+
|
|
5
|
+
**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.
|
|
6
|
+
|
|
7
|
+
## Creating a Simple Tool
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
require 'claude_agent_sdk'
|
|
11
|
+
require 'async'
|
|
12
|
+
|
|
13
|
+
greet_tool = ClaudeAgentSDK.create_tool(
|
|
14
|
+
'greet', 'Greet a user', { name: :string },
|
|
15
|
+
annotations: { title: 'Greeter', readOnlyHint: true }
|
|
16
|
+
) do |args|
|
|
17
|
+
{ content: [{ type: 'text', text: "Hello, #{args[:name]}!" }] }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
server = ClaudeAgentSDK.create_sdk_mcp_server(
|
|
21
|
+
name: 'my-tools',
|
|
22
|
+
version: '1.0.0',
|
|
23
|
+
tools: [greet_tool]
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
27
|
+
mcp_servers: { tools: server },
|
|
28
|
+
allowed_tools: ['mcp__tools__greet']
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
Async do
|
|
32
|
+
client = ClaudeAgentSDK::Client.new(options: options)
|
|
33
|
+
client.connect
|
|
34
|
+
client.query("Greet Alice")
|
|
35
|
+
client.receive_response { |msg| puts msg }
|
|
36
|
+
client.disconnect
|
|
37
|
+
end.wait
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Pre-built JSON Schemas
|
|
41
|
+
|
|
42
|
+
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:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
# Symbol keys (standard Ruby)
|
|
46
|
+
ClaudeAgentSDK.create_tool('save', 'Save a fact', {
|
|
47
|
+
type: 'object',
|
|
48
|
+
properties: { fact: { type: 'string' } },
|
|
49
|
+
required: ['fact']
|
|
50
|
+
}) { |args| { content: [{ type: 'text', text: "Saved: #{args[:fact]}" }] } }
|
|
51
|
+
|
|
52
|
+
# String keys (e.g., from RubyLLM or JSON.parse)
|
|
53
|
+
ClaudeAgentSDK.create_tool('save', 'Save a fact', {
|
|
54
|
+
'type' => 'object',
|
|
55
|
+
'properties' => { 'fact' => { 'type' => 'string' } },
|
|
56
|
+
'required' => ['fact']
|
|
57
|
+
}) { |args| { content: [{ type: 'text', text: "Saved: #{args[:fact]}" }] } }
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Benefits Over External MCP Servers
|
|
61
|
+
|
|
62
|
+
- **No subprocess management** — runs in the same process as your application
|
|
63
|
+
- **Better performance** — no IPC overhead for tool calls
|
|
64
|
+
- **Simpler deployment** — single Ruby process instead of multiple
|
|
65
|
+
- **Easier debugging** — all code runs in the same process
|
|
66
|
+
- **Direct access** — tools can directly access your application's state
|
|
67
|
+
|
|
68
|
+
## Calculator Example
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
add_tool = ClaudeAgentSDK.create_tool('add', 'Add two numbers', { a: :number, b: :number }) do |args|
|
|
72
|
+
result = args[:a] + args[:b]
|
|
73
|
+
{ content: [{ type: 'text', text: "#{args[:a]} + #{args[:b]} = #{result}" }] }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
divide_tool = ClaudeAgentSDK.create_tool('divide', 'Divide numbers', { a: :number, b: :number }) do |args|
|
|
77
|
+
if args[:b] == 0
|
|
78
|
+
{ content: [{ type: 'text', text: 'Error: Division by zero' }], is_error: true }
|
|
79
|
+
else
|
|
80
|
+
{ content: [{ type: 'text', text: "Result: #{args[:a] / args[:b]}" }] }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
calculator = ClaudeAgentSDK.create_sdk_mcp_server(
|
|
85
|
+
name: 'calculator',
|
|
86
|
+
tools: [add_tool, divide_tool]
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
90
|
+
mcp_servers: { calc: calculator },
|
|
91
|
+
allowed_tools: ['mcp__calc__add', 'mcp__calc__divide']
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Mixed Server Support
|
|
96
|
+
|
|
97
|
+
You can use both SDK and external MCP servers together:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
101
|
+
mcp_servers: {
|
|
102
|
+
internal: sdk_server, # In-process SDK server
|
|
103
|
+
external: { # External subprocess server
|
|
104
|
+
type: 'stdio',
|
|
105
|
+
command: 'external-server'
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## MCP Resources and Prompts
|
|
112
|
+
|
|
113
|
+
SDK MCP servers can also expose **resources** (data sources) and **prompts** (reusable templates):
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
config_resource = ClaudeAgentSDK.create_resource(
|
|
117
|
+
uri: 'config://app/settings',
|
|
118
|
+
name: 'Application Settings',
|
|
119
|
+
description: 'Current app configuration',
|
|
120
|
+
mime_type: 'application/json'
|
|
121
|
+
) do
|
|
122
|
+
config_data = { app_name: 'MyApp', version: '1.0.0' }
|
|
123
|
+
{
|
|
124
|
+
contents: [{
|
|
125
|
+
uri: 'config://app/settings',
|
|
126
|
+
mimeType: 'application/json',
|
|
127
|
+
text: JSON.pretty_generate(config_data)
|
|
128
|
+
}]
|
|
129
|
+
}
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
review_prompt = ClaudeAgentSDK.create_prompt(
|
|
133
|
+
name: 'code_review',
|
|
134
|
+
description: 'Review code for best practices',
|
|
135
|
+
arguments: [{ name: 'code', description: 'Code to review', required: true }]
|
|
136
|
+
) do |args|
|
|
137
|
+
{
|
|
138
|
+
messages: [{
|
|
139
|
+
role: 'user',
|
|
140
|
+
content: { type: 'text', text: "Review this code: #{args[:code]}" }
|
|
141
|
+
}]
|
|
142
|
+
}
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
server = ClaudeAgentSDK.create_sdk_mcp_server(
|
|
146
|
+
name: 'dev-tools',
|
|
147
|
+
tools: [my_tool],
|
|
148
|
+
resources: [config_resource],
|
|
149
|
+
prompts: [review_prompt]
|
|
150
|
+
)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
See [examples/mcp_calculator.rb](../examples/mcp_calculator.rb) and [examples/mcp_resources_prompts_example.rb](../examples/mcp_resources_prompts_example.rb) for complete examples.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Observability (OpenTelemetry / Langfuse)
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
## How It Works
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
claude_agent.session (root span — one per query/session)
|
|
11
|
+
├── claude_agent.generation (per AssistantMessage, with model + token usage)
|
|
12
|
+
├── claude_agent.tool.Bash (per tool call, open on ToolUseBlock, close on ToolResultBlock)
|
|
13
|
+
├── claude_agent.tool.Read
|
|
14
|
+
├── claude_agent.generation
|
|
15
|
+
└── ...
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Setup with Langfuse
|
|
19
|
+
|
|
20
|
+
**1. Install the OTel gems** (not bundled with the SDK — you choose your exporter):
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
gem install opentelemetry-sdk opentelemetry-exporter-otlp
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or add to your Gemfile:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
gem 'opentelemetry-sdk', '~> 1.4'
|
|
30
|
+
gem 'opentelemetry-exporter-otlp', '~> 0.28'
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**2. Configure the OTel SDK** to export to your Langfuse instance:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
require 'base64'
|
|
37
|
+
require 'opentelemetry/sdk'
|
|
38
|
+
require 'opentelemetry/exporter/otlp'
|
|
39
|
+
|
|
40
|
+
# Langfuse authenticates via Basic Auth over OTLP
|
|
41
|
+
public_key = ENV['LANGFUSE_PUBLIC_KEY']
|
|
42
|
+
secret_key = ENV['LANGFUSE_SECRET_KEY']
|
|
43
|
+
auth = Base64.strict_encode64("#{public_key}:#{secret_key}")
|
|
44
|
+
|
|
45
|
+
# Self-hosted or cloud: https://cloud.langfuse.com (EU) / https://us.cloud.langfuse.com (US)
|
|
46
|
+
langfuse_host = ENV.fetch('LANGFUSE_HOST', 'https://cloud.langfuse.com')
|
|
47
|
+
|
|
48
|
+
OpenTelemetry::SDK.configure do |c|
|
|
49
|
+
c.service_name = 'my-agent-app'
|
|
50
|
+
c.add_span_processor(
|
|
51
|
+
OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
|
|
52
|
+
OpenTelemetry::Exporter::OTLP::Exporter.new(
|
|
53
|
+
endpoint: "#{langfuse_host}/api/public/otel/v1/traces",
|
|
54
|
+
headers: {
|
|
55
|
+
'Authorization' => "Basic #{auth}",
|
|
56
|
+
'x-langfuse-ingestion-version' => '4'
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**3. Create the observer and run a query:**
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
require 'claude_agent_sdk'
|
|
68
|
+
require 'claude_agent_sdk/instrumentation'
|
|
69
|
+
|
|
70
|
+
observer = ClaudeAgentSDK::Instrumentation::OTelObserver.new(
|
|
71
|
+
'langfuse.session.id' => 'my-session-123', # optional: group traces by session
|
|
72
|
+
'user.id' => 'user-42' # optional: tag with user ID
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
76
|
+
observers: [observer],
|
|
77
|
+
allowed_tools: ['Bash', 'Read'],
|
|
78
|
+
permission_mode: 'bypassPermissions'
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
ClaudeAgentSDK.query(prompt: "List files in /tmp", options: options) do |msg|
|
|
82
|
+
puts msg.text if msg.is_a?(ClaudeAgentSDK::AssistantMessage)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# For long-running apps, flush before exit:
|
|
86
|
+
# OpenTelemetry.tracer_provider.shutdown
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Span Attributes
|
|
90
|
+
|
|
91
|
+
The OTel observer sets attributes using both `gen_ai.*` (OTel GenAI) and OpenInference conventions for maximum backend compatibility:
|
|
92
|
+
|
|
93
|
+
| Span | Type | Key Attributes |
|
|
94
|
+
|------|------|----------------|
|
|
95
|
+
| `claude_agent.session` | `agent` | `gen_ai.system`, `gen_ai.request.model`, `session.id`, `input.value`, `output.value`, `gen_ai.usage.cost`, `llm.cost.total` |
|
|
96
|
+
| `claude_agent.generation` | `generation` | `gen_ai.response.model`, `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens`, `output.value` |
|
|
97
|
+
| `claude_agent.tool.*` | `tool` | `tool.name`, `input.value`, `output.value` |
|
|
98
|
+
|
|
99
|
+
Events (`api_retry`, `rate_limit`, `tool_progress`) are recorded on the root span.
|
|
100
|
+
|
|
101
|
+
The `langfuse.observation.type` attribute is set on each span (`agent`/`generation`/`tool`) to enable Langfuse's **trace flow diagram** (DAG graph visualization).
|
|
102
|
+
|
|
103
|
+
## Custom Observers
|
|
104
|
+
|
|
105
|
+
Implement the `Observer` module to build your own instrumentation:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
class MyObserver
|
|
109
|
+
include ClaudeAgentSDK::Observer
|
|
110
|
+
|
|
111
|
+
def on_message(message)
|
|
112
|
+
case message
|
|
113
|
+
when ClaudeAgentSDK::ResultMessage
|
|
114
|
+
puts "Cost: $#{message.total_cost_usd}, Tokens: #{message.usage}"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def on_close
|
|
119
|
+
puts "Session ended"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(observers: [MyObserver.new])
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
See [examples/otel_langfuse_example.rb](../examples/otel_langfuse_example.rb) for a complete multi-tool example.
|
data/docs/rails.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Rails Integration
|
|
2
|
+
|
|
3
|
+
The SDK integrates well with Rails applications. Below are the common patterns.
|
|
4
|
+
|
|
5
|
+
## Thread-keyed libraries are safe inside SDK callbacks
|
|
6
|
+
|
|
7
|
+
The SDK depends on [`async`](https://github.com/socketry/async), which installs a Fiber scheduler that multiplexes fibers onto a single OS thread and intercepts IO so blocking calls yield to siblings. Most mature Ruby libraries are thread-safe but not fiber-safe — they key state (checked-out DB connections, per-thread caches, request stores) on `Thread.current`. When the scheduler interleaves two fibers on one thread, those fibers share the same state slot, and interleaved IO on a shared connection silently corrupts wire protocols. This affects every DB driver keyed by thread (`pg`, `mysql2`, `sqlite3`), ActiveRecord's connection pool, and HTTP/cache clients pooled per thread.
|
|
8
|
+
|
|
9
|
+
You do **not** need to think about this. The SDK hops to a plain thread at every user-callback boundary — message blocks given to `query` / `Client`, SDK MCP tool handlers, hooks, permission callbacks, and observer methods — so your code runs with no Fiber scheduler active and inherits the ordinary thread-keyed assumptions every Rails / Sidekiq / Kamal app already makes:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
tool = ClaudeAgentSDK.create_tool('lookup_user', 'Look up a user', { id: Integer }) do |args|
|
|
13
|
+
user = User.find(args[:id]) # just works
|
|
14
|
+
{ content: [{ type: 'text', text: user.name }] }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
ClaudeAgentSDK.query(prompt: '...') do |message|
|
|
18
|
+
Message.create!(role: 'assistant', body: message.to_s) # just works
|
|
19
|
+
end
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The trade-off: because callbacks run on a plain thread rather than inside an `Async::Task`, fiber-specific primitives aren't available to them — `Async::Task.current` will raise "No async task available". If a callback wants cooperative concurrency it should open its own `Async { }` block. In practice, callbacks typically do some Ruby work, call external services, and return — so this rarely matters. If you wrap your own call site in an outer `Async { }` block, the scheduler is visible to your code again; you've opted in, and whatever fiber-safety rules your app uses apply there.
|
|
23
|
+
|
|
24
|
+
## ActionCable Streaming
|
|
25
|
+
|
|
26
|
+
Stream Claude responses to the frontend in real-time:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
# app/jobs/chat_agent_job.rb
|
|
30
|
+
class ChatAgentJob < ApplicationJob
|
|
31
|
+
queue_as :claude_agents
|
|
32
|
+
|
|
33
|
+
def perform(chat_id, message_content)
|
|
34
|
+
Async do
|
|
35
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
36
|
+
system_prompt: { type: 'preset', preset: 'claude_code' },
|
|
37
|
+
permission_mode: 'bypassPermissions'
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
client = ClaudeAgentSDK::Client.new(options: options)
|
|
41
|
+
|
|
42
|
+
begin
|
|
43
|
+
client.connect
|
|
44
|
+
client.query(message_content)
|
|
45
|
+
|
|
46
|
+
client.receive_response do |message|
|
|
47
|
+
case message
|
|
48
|
+
when ClaudeAgentSDK::AssistantMessage
|
|
49
|
+
ChatChannel.broadcast_to(chat_id, { type: 'chunk', content: message.text })
|
|
50
|
+
when ClaudeAgentSDK::ResultMessage
|
|
51
|
+
ChatChannel.broadcast_to(chat_id, {
|
|
52
|
+
type: 'complete',
|
|
53
|
+
content: message.result,
|
|
54
|
+
cost: message.total_cost_usd
|
|
55
|
+
})
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
ensure
|
|
59
|
+
client.disconnect
|
|
60
|
+
end
|
|
61
|
+
end.wait
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Session Resumption
|
|
67
|
+
|
|
68
|
+
Persist Claude sessions for multi-turn conversations:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
# app/models/chat_session.rb
|
|
72
|
+
class ChatSession < ApplicationRecord
|
|
73
|
+
# Columns: id, claude_session_id, user_id, created_at, updated_at
|
|
74
|
+
|
|
75
|
+
def send_message(content)
|
|
76
|
+
options = build_options
|
|
77
|
+
client = ClaudeAgentSDK::Client.new(options: options)
|
|
78
|
+
|
|
79
|
+
Async do
|
|
80
|
+
client.connect
|
|
81
|
+
client.query(content, session_id: claude_session_id ? nil : generate_session_id)
|
|
82
|
+
|
|
83
|
+
client.receive_response do |message|
|
|
84
|
+
update!(claude_session_id: message.session_id) if message.is_a?(ClaudeAgentSDK::ResultMessage)
|
|
85
|
+
end
|
|
86
|
+
ensure
|
|
87
|
+
client.disconnect
|
|
88
|
+
end.wait
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def build_options
|
|
94
|
+
opts = { permission_mode: 'bypassPermissions', setting_sources: [] }
|
|
95
|
+
opts[:resume] = claude_session_id if claude_session_id.present?
|
|
96
|
+
ClaudeAgentSDK::ClaudeAgentOptions.new(**opts)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def generate_session_id
|
|
100
|
+
"chat_#{id}_#{Time.current.to_i}"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Background Jobs with Error Handling
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
class ClaudeAgentJob < ApplicationJob
|
|
109
|
+
queue_as :claude_agents
|
|
110
|
+
retry_on ClaudeAgentSDK::ProcessError, wait: :polynomially_longer, attempts: 3
|
|
111
|
+
|
|
112
|
+
def perform(task_id)
|
|
113
|
+
task = Task.find(task_id)
|
|
114
|
+
Async { execute_agent(task) }.wait
|
|
115
|
+
rescue ClaudeAgentSDK::CLINotFoundError
|
|
116
|
+
task.update!(status: 'failed', error: 'Claude CLI not installed')
|
|
117
|
+
raise
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def execute_agent(task)
|
|
123
|
+
# ... agent execution
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## HTTP MCP Servers
|
|
129
|
+
|
|
130
|
+
Connect to remote tool services:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
mcp_servers = {
|
|
134
|
+
'api_tools' => ClaudeAgentSDK::McpHttpServerConfig.new(
|
|
135
|
+
url: ENV['MCP_SERVER_URL'],
|
|
136
|
+
headers: { 'Authorization' => "Bearer #{ENV['MCP_TOKEN']}" }
|
|
137
|
+
).to_h
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
141
|
+
mcp_servers: mcp_servers,
|
|
142
|
+
permission_mode: 'bypassPermissions'
|
|
143
|
+
)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Observability in Rails
|
|
147
|
+
|
|
148
|
+
Add OpenTelemetry tracing to your Rails app with a single initializer:
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
# config/initializers/opentelemetry.rb
|
|
152
|
+
require 'base64'
|
|
153
|
+
require 'opentelemetry/sdk'
|
|
154
|
+
require 'opentelemetry/exporter/otlp'
|
|
155
|
+
|
|
156
|
+
if ENV['LANGFUSE_PUBLIC_KEY'].present?
|
|
157
|
+
auth = Base64.strict_encode64("#{ENV['LANGFUSE_PUBLIC_KEY']}:#{ENV['LANGFUSE_SECRET_KEY']}")
|
|
158
|
+
langfuse_host = ENV.fetch('LANGFUSE_HOST', 'https://cloud.langfuse.com')
|
|
159
|
+
|
|
160
|
+
OpenTelemetry::SDK.configure do |c|
|
|
161
|
+
c.service_name = Rails.application.class.module_parent_name.underscore
|
|
162
|
+
c.add_span_processor(
|
|
163
|
+
OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
|
|
164
|
+
OpenTelemetry::Exporter::OTLP::Exporter.new(
|
|
165
|
+
endpoint: "#{langfuse_host}/api/public/otel/v1/traces",
|
|
166
|
+
headers: {
|
|
167
|
+
'Authorization' => "Basic #{auth}",
|
|
168
|
+
'x-langfuse-ingestion-version' => '4'
|
|
169
|
+
}
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
# config/initializers/claude_agent_sdk.rb
|
|
179
|
+
require 'claude_agent_sdk/instrumentation'
|
|
180
|
+
|
|
181
|
+
ClaudeAgentSDK.configure do |config|
|
|
182
|
+
config.default_options = {
|
|
183
|
+
permission_mode: 'bypassPermissions',
|
|
184
|
+
observers: ENV['LANGFUSE_PUBLIC_KEY'].present? ? [
|
|
185
|
+
# Use a lambda so each query gets a fresh observer instance (thread-safe).
|
|
186
|
+
# A single shared instance would have its span state clobbered by concurrent requests.
|
|
187
|
+
-> { ClaudeAgentSDK::Instrumentation::OTelObserver.new }
|
|
188
|
+
] : []
|
|
189
|
+
}
|
|
190
|
+
end
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Then every `ClaudeAgentSDK.query` and `Client` session automatically gets traced — no per-call wiring needed. The lambda factory ensures each request gets its own observer with isolated span state, safe for concurrent Puma/Sidekiq workers.
|
|
194
|
+
|
|
195
|
+
See:
|
|
196
|
+
- [examples/rails_actioncable_example.rb](../examples/rails_actioncable_example.rb)
|
|
197
|
+
- [examples/rails_background_job_example.rb](../examples/rails_background_job_example.rb)
|
|
198
|
+
- [examples/session_resumption_example.rb](../examples/session_resumption_example.rb)
|
|
199
|
+
- [examples/http_mcp_server_example.rb](../examples/http_mcp_server_example.rb)
|
data/docs/sessions.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Session Browsing & Mutations
|
|
2
|
+
|
|
3
|
+
Browse, read, mutate, fork, and resume Claude Code sessions directly from Ruby — no CLI subprocess required. These APIs read and write `~/.claude/projects/` JSONL files directly, respecting the `CLAUDE_CONFIG_DIR` environment variable and auto-detecting git worktrees.
|
|
4
|
+
|
|
5
|
+
## Listing Sessions
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# All sessions (sorted by most recent first)
|
|
9
|
+
sessions = ClaudeAgentSDK.list_sessions
|
|
10
|
+
sessions.each do |session|
|
|
11
|
+
puts "#{session.session_id}: #{session.summary} (#{session.git_branch})"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# For a specific directory
|
|
15
|
+
ClaudeAgentSDK.list_sessions(directory: '/path/to/project', limit: 10)
|
|
16
|
+
|
|
17
|
+
# Paginate with offset
|
|
18
|
+
ClaudeAgentSDK.list_sessions(directory: '.', limit: 10, offset: 10)
|
|
19
|
+
|
|
20
|
+
# Include git worktree sessions
|
|
21
|
+
ClaudeAgentSDK.list_sessions(directory: '.', include_worktrees: true)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Each `SDKSessionInfo` includes: `session_id`, `summary`, `last_modified`, `file_size`, `custom_title`, `first_prompt`, `git_branch`, `cwd`.
|
|
25
|
+
|
|
26
|
+
## Reading Session Messages
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
# Full conversation
|
|
30
|
+
messages = ClaudeAgentSDK.get_session_messages(session_id: 'abc-123-...')
|
|
31
|
+
messages.each { |msg| puts "[#{msg.type}] #{msg.message}" }
|
|
32
|
+
|
|
33
|
+
# Paginate
|
|
34
|
+
ClaudeAgentSDK.get_session_messages(session_id: 'abc-123-...', offset: 10, limit: 20)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Each `SessionMessage` includes `type` (`"user"` or `"assistant"`), `uuid`, `session_id`, and `message` (raw API hash).
|
|
38
|
+
|
|
39
|
+
## Renaming a Session
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
ClaudeAgentSDK.rename_session(
|
|
43
|
+
session_id: '550e8400-e29b-41d4-a716-446655440000',
|
|
44
|
+
title: 'My refactoring session',
|
|
45
|
+
directory: '/path/to/project' # optional
|
|
46
|
+
)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Tagging a Session
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
ClaudeAgentSDK.tag_session(session_id: '550e8400-...', tag: 'experiment')
|
|
53
|
+
ClaudeAgentSDK.tag_session(session_id: '550e8400-...', tag: nil) # clear
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Tags are Unicode-sanitized before storing.
|
|
57
|
+
|
|
58
|
+
## Deleting a Session
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
# Hard-delete (removes the JSONL file permanently)
|
|
62
|
+
ClaudeAgentSDK.delete_session(
|
|
63
|
+
session_id: '550e8400-...',
|
|
64
|
+
directory: '/path/to/project' # optional
|
|
65
|
+
)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Forking a Session
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
# Fork into a new branch with fresh UUIDs
|
|
72
|
+
result = ClaudeAgentSDK.fork_session(
|
|
73
|
+
session_id: '550e8400-...',
|
|
74
|
+
title: 'Experiment branch' # optional, auto-generated if omitted
|
|
75
|
+
)
|
|
76
|
+
puts result.session_id # UUID of the new forked session
|
|
77
|
+
|
|
78
|
+
# Partial fork — fork up to a specific message
|
|
79
|
+
ClaudeAgentSDK.fork_session(
|
|
80
|
+
session_id: '550e8400-...',
|
|
81
|
+
up_to_message_id: 'message-uuid-here'
|
|
82
|
+
)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
> 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.
|
|
86
|
+
|
|
87
|
+
## Resuming at a Specific Message
|
|
88
|
+
|
|
89
|
+
`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.
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
ClaudeAgentSDK.query(
|
|
93
|
+
prompt: 'Try a different approach',
|
|
94
|
+
options: ClaudeAgentSDK::ClaudeAgentOptions.new(
|
|
95
|
+
resume: '550e8400-...',
|
|
96
|
+
resume_session_at: 'assistant-message-uuid-from-history'
|
|
97
|
+
)
|
|
98
|
+
) { |message| }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`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.
|