claude_agent 0.7.14 → 0.7.16

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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/rules/conventions.md +66 -16
  3. data/CHANGELOG.md +20 -0
  4. data/CLAUDE.md +24 -4
  5. data/README.md +52 -1529
  6. data/SPEC.md +56 -29
  7. data/docs/architecture.md +339 -0
  8. data/docs/client.md +526 -0
  9. data/docs/configuration.md +571 -0
  10. data/docs/conversations.md +461 -0
  11. data/docs/errors.md +127 -0
  12. data/docs/events.md +225 -0
  13. data/docs/getting-started.md +310 -0
  14. data/docs/hooks.md +380 -0
  15. data/docs/logging.md +96 -0
  16. data/docs/mcp.md +308 -0
  17. data/docs/messages.md +871 -0
  18. data/docs/permissions.md +611 -0
  19. data/docs/queries.md +227 -0
  20. data/docs/sessions.md +335 -0
  21. data/lib/claude_agent/abort_controller.rb +24 -0
  22. data/lib/claude_agent/client/commands.rb +32 -0
  23. data/lib/claude_agent/client.rb +10 -4
  24. data/lib/claude_agent/configuration.rb +129 -0
  25. data/lib/claude_agent/control_protocol/commands.rb +28 -0
  26. data/lib/claude_agent/conversation.rb +37 -4
  27. data/lib/claude_agent/errors.rb +21 -4
  28. data/lib/claude_agent/event_handler.rb +14 -0
  29. data/lib/claude_agent/fork_session.rb +117 -0
  30. data/lib/claude_agent/hook_registry.rb +110 -0
  31. data/lib/claude_agent/hooks.rb +4 -0
  32. data/lib/claude_agent/mcp/server.rb +22 -0
  33. data/lib/claude_agent/mcp/tool.rb +24 -3
  34. data/lib/claude_agent/message.rb +93 -0
  35. data/lib/claude_agent/messages/streaming.rb +37 -0
  36. data/lib/claude_agent/options.rb +10 -0
  37. data/lib/claude_agent/permission_policy.rb +174 -0
  38. data/lib/claude_agent/permission_request.rb +17 -0
  39. data/lib/claude_agent/session.rb +100 -11
  40. data/lib/claude_agent/session_paths.rb +5 -2
  41. data/lib/claude_agent/turn_result.rb +20 -2
  42. data/lib/claude_agent/types/sessions.rb +8 -0
  43. data/lib/claude_agent/version.rb +1 -1
  44. data/lib/claude_agent.rb +187 -0
  45. data/sig/claude_agent.rbs +38 -1
  46. metadata +20 -1
data/SPEC.md CHANGED
@@ -3,11 +3,11 @@
3
3
  This document provides a comprehensive specification of the Claude Agent SDK, comparing feature parity across the official TypeScript and Python SDKs with this Ruby implementation.
4
4
 
5
5
  **Reference Versions:**
6
- - TypeScript SDK: v0.2.75 (npm package)
7
- - Python SDK: from GitHub (commit 9880677)
6
+ - TypeScript SDK: v0.2.76 (npm package)
7
+ - Python SDK: from GitHub (commit 302ceb6)
8
8
  - Ruby SDK: This repository
9
9
 
10
- **Last Updated:** 2026-03-13
10
+ **Last Updated:** 2026-03-15
11
11
 
12
12
  ---
13
13
 
@@ -305,29 +305,31 @@ Bidirectional control protocol for SDK-CLI communication.
305
305
 
306
306
  ### Control Request Types
307
307
 
308
- | Request Subtype | TypeScript | Python | Ruby | Notes |
309
- |---------------------------|:----------:|:------:|:----:|--------------------------------------------|
310
- | `initialize` | ✅ | ✅ | ✅ | Initialize session with hooks/MCP |
311
- | `interrupt` | ✅ | ✅ | ✅ | Interrupt current operation |
312
- | `can_use_tool` | ✅ | ✅ | ✅ | Permission callback |
313
- | `hook_callback` | ✅ | ✅ | ✅ | Execute hook callback |
314
- | `set_permission_mode` | ✅ | ✅ | ✅ | Change permission mode |
315
- | `set_model` | ✅ | ✅ | ✅ | Change model |
316
- | `set_max_thinking_tokens` | ✅ | ❌ | ✅ | Change thinking tokens limit |
317
- | `rewind_files` | ✅ | ✅ | ✅ | Rewind file checkpoints (supports dry_run) |
318
- | `mcp_message` | ✅ | ✅ | ✅ | Route MCP message |
319
- | `mcp_set_servers` | ✅ | ❌ | ✅ | Dynamically set MCP servers |
320
- | `mcp_status` | ✅ | ✅ | ✅ | Get MCP server status |
321
- | `mcp_reconnect` | ✅ | ✅ | ✅ | Reconnect to MCP server |
322
- | `mcp_toggle` | ✅ | ✅ | ✅ | Enable/disable MCP server |
323
- | `stop_task` | ✅ | ✅ | ✅ | Stop a running background task |
324
- | `mcp_authenticate` | ✅ | ❌ | ✅ | Authenticate MCP server (v0.2.52) |
325
- | `mcp_clear_auth` | ✅ | ❌ | ✅ | Clear MCP server auth (v0.2.52) |
326
- | `supported_commands` | ✅ | ❌ | ✅ | Get available slash commands |
327
- | `supported_models` | ✅ | ❌ | ✅ | Get available models |
328
- | `account_info` | ✅ | ❌ | ✅ | Get account information |
329
- | `apply_flag_settings` | ✅ | ❌ | ✅ | Merge settings into flag layer |
330
- | `supported_agents` | ✅ | ❌ | ✅ | Get available subagents (v0.2.63) |
308
+ | Request Subtype | TypeScript | Python | Ruby | Notes |
309
+ |---------------------------|:----------:|:------:|:----:|----------------------------------------------|
310
+ | `initialize` | ✅ | ✅ | ✅ | Initialize session with hooks/MCP |
311
+ | `interrupt` | ✅ | ✅ | ✅ | Interrupt current operation |
312
+ | `can_use_tool` | ✅ | ✅ | ✅ | Permission callback |
313
+ | `hook_callback` | ✅ | ✅ | ✅ | Execute hook callback |
314
+ | `set_permission_mode` | ✅ | ✅ | ✅ | Change permission mode |
315
+ | `set_model` | ✅ | ✅ | ✅ | Change model |
316
+ | `set_max_thinking_tokens` | ✅ | ❌ | ✅ | Change thinking tokens limit |
317
+ | `rewind_files` | ✅ | ✅ | ✅ | Rewind file checkpoints (supports dry_run) |
318
+ | `mcp_message` | ✅ | ✅ | ✅ | Route MCP message |
319
+ | `mcp_set_servers` | ✅ | ❌ | ✅ | Dynamically set MCP servers |
320
+ | `mcp_status` | ✅ | ✅ | ✅ | Get MCP server status |
321
+ | `mcp_reconnect` | ✅ | ✅ | ✅ | Reconnect to MCP server |
322
+ | `mcp_toggle` | ✅ | ✅ | ✅ | Enable/disable MCP server |
323
+ | `stop_task` | ✅ | ✅ | ✅ | Stop a running background task |
324
+ | `mcp_authenticate` | ✅ | ❌ | ✅ | Authenticate MCP server (v0.2.52) |
325
+ | `mcp_clear_auth` | ✅ | ❌ | ✅ | Clear MCP server auth (v0.2.52) |
326
+ | `supported_commands` | ✅ | ❌ | ✅ | Get available slash commands |
327
+ | `supported_models` | ✅ | ❌ | ✅ | Get available models |
328
+ | `account_info` | ✅ | ❌ | ✅ | Get account information |
329
+ | `apply_flag_settings` | ✅ | ❌ | ✅ | Merge settings into flag layer |
330
+ | `supported_agents` | ✅ | ❌ | ✅ | Get available subagents (v0.2.63) |
331
+ | `cancel_async_message` | ✅ | ❌ | ✅ | Cancel queued user message by UUID (v0.2.76) |
332
+ | `get_settings` | ✅ | ❌ | ✅ | Get effective merged settings (v0.2.72) |
331
333
 
332
334
  ### Return Types
333
335
 
@@ -426,6 +428,7 @@ Event hooks for intercepting and modifying SDK behavior.
426
428
  | `SubagentStart` | ✅ | ✅ | ✅ | Subagent starts (Py v0.1.29) |
427
429
  | `SubagentStop` | ✅ | ✅ | ✅ | Subagent stops |
428
430
  | `PreCompact` | ✅ | ✅ | ✅ | Before compaction |
431
+ | `PostCompact` | ✅ | ❌ | ✅ | After compaction (v0.2.76) |
429
432
  | `PermissionRequest` | ✅ | ✅ | ✅ | Permission requested (Py v0.1.29) |
430
433
  | `Setup` | ✅ | ❌ | ✅ | Initial setup/maintenance |
431
434
  | `TeammateIdle` | ✅ | ❌ | ✅ | Teammate idle (v0.2.33) |
@@ -452,6 +455,7 @@ Event hooks for intercepting and modifying SDK behavior.
452
455
  | `SubagentStartHookInput` | ✅ | ✅ | ✅ |
453
456
  | `SubagentStopHookInput` | ✅ | ✅ | ✅ |
454
457
  | `PreCompactHookInput` | ✅ | ✅ | ✅ |
458
+ | `PostCompactHookInput` | ✅ | ❌ | ✅ |
455
459
  | `PermissionRequestHookInput` | ✅ | ✅ | ✅ |
456
460
  | `SetupHookInput` | ✅ | ❌ | ✅ |
457
461
  | `TeammateIdleHookInput` | ✅ | ❌ | ✅ |
@@ -537,6 +541,13 @@ Event-specific fields returned via `hookSpecificOutput`:
537
541
  |---------------------|:----------:|:------:|:----:|----------------------------------|
538
542
  | `additionalContext` | ✅ | ✅ | ✅ | Context string returned to model |
539
543
 
544
+ #### PostCompactHookInput Fields
545
+
546
+ | Field | TypeScript | Python | Ruby | Notes |
547
+ |-------------------|:----------:|:------:|:----:|--------------------------------|
548
+ | `trigger` | ✅ | ❌ | ✅ | 'manual' or 'auto' |
549
+ | `compact_summary` | ✅ | ❌ | ✅ | Summary produced by compaction |
550
+
540
551
  #### SetupHookSpecificOutput
541
552
 
542
553
  | Field | TypeScript | Python | Ruby | Notes |
@@ -740,6 +751,7 @@ Session management and resumption.
740
751
  | `getSessionInfo()` | ✅ | ❌ | ✅ | Get single session metadata (v0.2.75) |
741
752
  | `renameSession()` | ✅ | ✅ | ✅ | Rename a session (v0.2.74) |
742
753
  | `tagSession()` | ✅ | ✅ | ✅ | Tag a session (v0.2.75) |
754
+ | `forkSession()` | ✅ | ❌ | ✅ | Fork/branch a session (v0.2.76) |
743
755
 
744
756
  #### ListSessionsOptions
745
757
 
@@ -783,6 +795,20 @@ Session management and resumption.
783
795
  | `tag` | ✅ | ❌ | ✅ | User-set tag (v0.2.75) |
784
796
  | `createdAt` | ✅ | ❌ | ✅ | Creation time in ms (v0.2.75) |
785
797
 
798
+ #### ForkSessionOptions
799
+
800
+ | Field | TypeScript | Python | Ruby | Notes |
801
+ |------------------|:----------:|:------:|:----:|------------------------------------------------|
802
+ | `dir` | ✅ | ❌ | ❌ | Project directory |
803
+ | `upToMessageId` | ✅ | ❌ | ❌ | Slice transcript up to this UUID (inclusive) |
804
+ | `title` | ✅ | ❌ | ❌ | Custom title for the fork |
805
+
806
+ #### ForkSessionResult
807
+
808
+ | Field | TypeScript | Python | Ruby | Notes |
809
+ |-------------|:----------:|:------:|:----:|--------------------------------|
810
+ | `sessionId` | ✅ | ❌ | ❌ | New forked session UUID |
811
+
786
812
  ### V2 Session API (Unstable)
787
813
 
788
814
  | Feature | TypeScript | Python | Ruby | Notes |
@@ -899,6 +925,7 @@ Public API surface for SDK clients.
899
925
  | `getSessionInfo()` | ✅ | ❌ | ✅ | Get single session metadata (v0.2.75) |
900
926
  | `renameSession()` | ✅ | ✅ | ✅ | Rename a session (v0.2.74) |
901
927
  | `tagSession()` | ✅ | ✅ | ✅ | Tag a session (v0.2.75) |
928
+ | `forkSession()` | ✅ | ❌ | ✅ | Fork/branch a session (v0.2.76) |
902
929
 
903
930
  ### Query Interface
904
931
 
@@ -989,6 +1016,7 @@ Public API surface for SDK clients.
989
1016
  - v0.2.73: Fixed `options.env` being overridden by `~/.claude/settings.json`
990
1017
  - v0.2.74: Added `renameSession()` for renaming session files
991
1018
  - v0.2.75: Added `tag`/`createdAt` fields on `SDKSessionInfo`; `getSessionInfo()` for single-session lookup; `offset` on `listSessions` for pagination; `tagSession()` for tagging sessions; `supportsAutoMode` in `ModelInfo`; `description` on `SDKControlPermissionRequest`; `prompt` on `SDKTaskStartedMessage`; `fast_mode_state` on `SDKControlInitializeResponse`; `queued_to_running` status on `AgentToolOutput`
1019
+ - v0.2.76: Added `forkSession(sessionId, opts?)` for branching conversations from a point; `cancel_async_message` control subtype to drop queued user messages; `PostCompact` hook event with `compact_summary` field; `get_settings` control request for reading effective merged settings; `planFilePath` field on `ExitPlanMode` tool input
992
1020
  - Includes `Elicitation`/`ElicitationResult` hook events, `onElicitation` option, `ElicitationCompleteMessage`, `LocalCommandOutputMessage`, `FastModeState` (undocumented in changelog, present in types)
993
1021
 
994
1022
  ### Python SDK
@@ -1017,12 +1045,11 @@ Public API surface for SDK clients.
1017
1045
  - v0.1.48: Fixed fine-grained tool streaming regression
1018
1046
  - Added `RateLimitEvent` message type with `RateLimitInfo`
1019
1047
  - Added `rename_session()` and `tag_session()` session management functions
1020
- - Missing: `onElicitation`, `Elicitation`/`ElicitationResult` hooks, `ElicitationCompleteMessage`, `LocalCommandOutputMessage`, `FastModeState`, `InstructionsLoaded` hook, `agentProgressSummaries`, `getSessionInfo()`
1048
+ - Missing: `onElicitation`, `Elicitation`/`ElicitationResult` hooks, `ElicitationCompleteMessage`, `LocalCommandOutputMessage`, `FastModeState`, `InstructionsLoaded` hook, `agentProgressSummaries`, `getSessionInfo()`, `forkSession()`, `PostCompact` hook, `cancel_async_message`, `get_settings`
1021
1049
 
1022
1050
  ### Ruby SDK (This Repository)
1023
- - Feature parity with TypeScript SDK v0.2.75
1051
+ - Feature parity with TypeScript SDK v0.2.76
1024
1052
  - Ruby-idiomatic patterns (Data.define, snake_case)
1025
1053
  - Complete control protocol, hook, and V2 Session API support
1026
1054
  - Dedicated Client class for multi-turn conversations
1027
1055
  - `executable`/`executableArgs` marked N/A (JS runtime options)
1028
- - Full v0.2.75 parity: `agentProgressSummaries`, `getSessionInfo()`, `renameSession()`, `tagSession()`, `offset` on `listSessions`, `tag`/`createdAt` on `SessionInfo`, `supportsAutoMode` on `ModelInfo`
@@ -0,0 +1,339 @@
1
+ # Architecture
2
+
3
+ Internal architecture of the ClaudeAgent Ruby SDK.
4
+
5
+ ---
6
+
7
+ ## Layer Diagram
8
+
9
+ ```
10
+ ClaudeAgent.ask / .chat / .query
11
+ |
12
+ Configuration (Stripe-style global defaults, Forwardable delegators)
13
+ |
14
+ Options (validation, CLI arg serialization, env vars)
15
+ |
16
+ Conversation / Client (lifecycle, turns, event dispatch)
17
+ |
18
+ EventHandler / TurnResult / CumulativeUsage
19
+ |
20
+ ControlProtocol (request/response routing, hooks, MCP, permissions)
21
+ Primitives | Lifecycle | Messaging | Commands | RequestHandling
22
+ |
23
+ Transport::Subprocess (JSON Lines framing, stdin/stdout, process mgmt)
24
+ |
25
+ Claude Code CLI (spawned as subprocess)
26
+ ```
27
+
28
+ ---
29
+
30
+ ## Module Responsibilities
31
+
32
+ ### Entry Points
33
+
34
+ | Module | Role |
35
+ |--------------------------|-------------------------------------------------------------------------------------------------------|
36
+ | `ClaudeAgent.ask` | One-shot query, returns `TurnResult`. Merges global config, builds `EventHandler` from `on_*` kwargs. |
37
+ | `ClaudeAgent.chat` | Multi-turn conversation. Block form auto-cleans; no block returns `Conversation`. |
38
+ | `ClaudeAgent.query` | Low-level streaming enumerator. Returns `Enumerator<Message>`. |
39
+ | `ClaudeAgent.query_turn` | Like `query` but accumulates into `TurnResult` with optional `EventHandler`. |
40
+
41
+ ### Configuration Layer
42
+
43
+ | Module | Role |
44
+ |-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
45
+ | `Configuration` | Stripe-style global defaults. Holds all configurable fields (Tier 1/2/3), global `PermissionPolicy`, `HookRegistry`, and MCP server registrations. `to_options(**overrides)` merges config + per-request kwargs into an `Options` instance. |
46
+ | `Options` | All configurable attributes with validation and CLI arg serialization. Includes `Serializer` mixin for `to_cli_args` and `to_env`. Auto-compiles `PermissionPolicy` to lambda, `HookRegistry` to hash. Auto-sets `permission_prompt_tool_name = "stdio"` when `can_use_tool` or `permission_queue` is present. |
47
+
48
+ ### Conversation Layer
49
+
50
+ | Module | Role |
51
+ |----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
52
+ | `Conversation` | High-level lifecycle manager. Wraps `Client` with auto-connect on first `say`, multi-turn history, tool activity timeline with timestamps, and cumulative cost tracking. Partitions kwargs into callbacks / conversation keys / options keys. Supports `open` (block), `resume` (session ID), and permission mode mapping (`:queue`, `:accept_edits`, policy, callable). |
53
+ | `Client` | Bidirectional connection to CLI. Composes `Transport`, `ControlProtocol`, `EventHandler`, `CumulativeUsage`, and `PermissionQueue`. Provides `send_message`, `receive_turn`, `send_and_receive`, `stream_input`, `interrupt`, and `abort!`. Includes `Commands` mixin for control operations (permission mode, model changes, file rewind, MCP server management). |
54
+
55
+ ### Event & Accumulation Layer
56
+
57
+ | Module | Role |
58
+ |-------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
59
+ | `EventHandler` | Three-layer event dispatch: (1) `:message` catch-all, (2) type-based (`:assistant`, `:stream_event`, `:status`, etc.), (3) decomposed (`:text`, `:thinking`, `:tool_use`, `:tool_result`). Pairs tool results with their originating tool use blocks. Supports `EventHandler.define` DSL and method chaining. |
60
+ | `TurnResult` | Message accumulator for a single agent turn. Convenience accessors: `text`, `thinking`, `tool_uses`, `tool_results`, `tool_executions`, `cost`, `session_id`, `usage`, `model`, `structured_output`, `permission_denials`. Accumulates streaming text deltas as fallback for aborted turns. |
61
+ | `CumulativeUsage` | Thread-safe (Mutex) token and cost tracker across turns. Sums `input_tokens`, `output_tokens`, cache tokens. Takes session-cumulative `total_cost_usd` and `num_turns` from the most recent `ResultMessage`. |
62
+
63
+ ### Protocol Layer
64
+
65
+ | Module | Role |
66
+ |-------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
67
+ | `ControlProtocol` | Core protocol handler. Composed of five submodules (below). Manages shared state: transport, parser, request counter, pending requests/results, threading primitives (Mutex, ConditionVariable, Queue), abort signal. |
68
+ | `Primitives` | Low-level read/write helpers. `write_message` serializes and sends JSON. Request/response ID generation. Stateless except for shared counters and pending-request maps. |
69
+ | `Lifecycle` | Connection lifecycle: `start` (connect transport, spawn reader thread, send initialize), `stop` (end input, join reader, close transport), `abort!` (cancel pending requests, drain permission queue, terminate transport). Background `reader_loop` routes `control_request`, `control_response`, and SDK messages to appropriate handlers or the message queue. |
70
+ | `Messaging` | Consumer-facing message delivery: `each_message`, `receive_response`, `send_user_message`, `stream_input`, `stream_conversation`. Reads from the internal `Queue`, parses via `MessageParser`, checks abort signal. |
71
+ | `Commands` | Control commands sent to CLI: `change_permission_mode`, `change_model`, `rewind_files`, `mcp_server_status`, `set_mcp_servers`, `interrupt`. Each sends a `control_request` and waits for the response. |
72
+ | `RequestHandling` | Handles incoming control requests from CLI: `can_use_tool` (three modes: synchronous callback, queue-based, default allow), `hook_callback`, `mcp_message` (routes to SDK MCP server instances), `elicitation`. Normalizes Ruby field names to CLI camelCase keys. |
73
+
74
+ ### Transport Layer
75
+
76
+ | Module | Role |
77
+ |-------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
78
+ | `Transport::Base` | Abstract base class defining the transport interface. |
79
+ | `Transport::Subprocess` | Spawns Claude Code CLI via `Open3.popen3` or custom spawn function. Manages stdin/stdout/stderr streams. JSON Lines framing with partial-JSON buffering. Version checking against minimum CLI version. Supports graceful termination (SIGTERM) and force kill (SIGKILL). Custom spawn support via `SpawnOptions` / `SpawnedProcess` for non-standard process management. |
80
+
81
+ ### Parsing Layer
82
+
83
+ | Module | Role |
84
+ |-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
85
+ | `MessageParser` | Registry-based router. Maps raw JSON hashes (string keys, camelCase) to typed message objects. `deep_transform_keys` normalizes to snake_case symbols. Dispatches by `type` (top-level) or `type:subtype` (system messages). Unknown types wrapped in `GenericMessage`. |
86
+
87
+ ### Permission System
88
+
89
+ | Module | Role |
90
+ |---------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
91
+ | `PermissionPolicy` | Declarative DSL for permission rules: `allow`, `deny`, `allow_matching`, `deny_matching`, `allow_all`, `deny_all`, `ask` (custom fallback). Compiles to a `can_use_tool` lambda. Rules evaluated in order; first match wins. |
92
+ | `PermissionQueue` | Thread-safe `Queue` wrapper for deferred permission requests. Non-blocking `poll`, blocking `pop(timeout:)`, and `drain!` for abort cleanup. |
93
+ | `PermissionRequest` | Deferred permission request resolved from any thread. `allow!` / `deny!` unblock the reader thread via Mutex + ConditionVariable. Supports hybrid mode: callback can call `context.request.defer!` to enqueue instead of returning synchronously. |
94
+
95
+ ### Hook System
96
+
97
+ | Module | Role |
98
+ |----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
99
+ | `HookRegistry` | Ruby-friendly DSL mapping idiomatic method names (`before_tool_use`, `after_tool_use`, `on_session_start`, etc.) to CLI event names (`PreToolUse`, `PostToolUse`, `SessionStart`, etc.). Compiles to the `Hash{String => Array<HookMatcher>}` format consumed by `Options#hooks`. Supports regex/string tool matchers and additive merge. |
100
+
101
+ ### MCP Layer
102
+
103
+ | Module | Role |
104
+ |---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
105
+ | `MCP::Server` | In-process MCP server. Handles JSON-RPC messages: `initialize`, `tools/list`, `tools/call`. Registered via `Options#mcp_servers` with `type: "sdk"`. Block DSL for inline tool definition. |
106
+ | `MCP::Tool` | Single tool definition with name, description, JSON Schema (auto-normalized from Ruby types/symbols), optional annotations, and handler block. Formats results as MCP content blocks. |
107
+
108
+ ### Session Layer
109
+
110
+ | Module | Role |
111
+ |--------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
112
+ | `Session` | Rails-like finder with Stripe-style resource methods. `find` / `retrieve` / `all` / `where` class methods. Instance methods: `messages` (returns `SessionMessageRelation`), `rename`, `tag_session`, `fork`, `reload`, `resume`. |
113
+ | `SessionMessageRelation` | Chainable Enumerable query object. Lazy evaluation with `where(limit:, offset:)`. Wraps `GetSessionMessages`. |
114
+ | `ListSessions` | Reads session metadata from disk without spawning CLI. Returns `SessionInfo` sorted by last modified. Supports directory scoping and git worktree inclusion. |
115
+ | `GetSessionMessages` | Reads JSONL session transcript, reconstructs main conversation thread, returns `SessionMessage` array with pagination. |
116
+ | `GetSessionInfo` | Targeted single-session lookup by UUID. |
117
+ | `ForkSession` | Creates a new session file with remapped UUIDs, optional truncation point. |
118
+ | `SessionMutations` | Appends `custom-title` and `tag` entries to session files. |
119
+ | `SessionPaths` | Shared infrastructure for resolving session file paths across projects and worktrees. |
120
+
121
+ ### Abort & Signal
122
+
123
+ | Module | Role |
124
+ |-------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
125
+ | `AbortController` | JavaScript-style abort controller. Owns an `AbortSignal`. `abort(reason)` triggers the signal; `reset!` clears it for reuse. |
126
+ | `AbortSignal` | Thread-safe (Mutex + ConditionVariable) signal. `aborted?`, `reason`, `on_abort` callbacks, `wait(timeout:)`, `check!` (raises `AbortError`). Used by `ControlProtocol` (reader loop check, queue push), `Conversation` (auto-reset per turn). |
127
+
128
+ ### Tool Activity Tracking
129
+
130
+ | Module | Role |
131
+ |-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
132
+ | `ToolActivity` | Immutable (`Data.define`) record of a completed tool execution. Pairs `ToolUseBlock` + `ToolResultBlock` with turn index and wall-clock timestamps. |
133
+ | `LiveToolActivity` | Mutable wrapper for real-time status tracking. States: `:running`, `:done`, `:error`. Updated by progress messages. Suitable for live UIs. |
134
+ | `ToolActivityTracker` | Enumerable collection with auto-wiring. Attaches to `EventHandler` or `Client`. Callbacks: `on_start`, `on_complete`, `on_progress`, `on_change`. Query methods: `running`, `done`, `errored`, `find_by_id`. |
135
+
136
+ ---
137
+
138
+ ## Data Flow
139
+
140
+ ### One-Shot Query (`ask`)
141
+
142
+ ```
143
+ User calls ClaudeAgent.ask(prompt, **kwargs)
144
+ |
145
+ +-- extract_callbacks separates on_* from config overrides
146
+ +-- Configuration.to_options merges global defaults + overrides --> Options
147
+ +-- build_events creates EventHandler from callbacks
148
+ |
149
+ +-- query_turn(prompt, options, events)
150
+ |
151
+ +-- ClaudeAgent.query(prompt, options) returns Enumerator
152
+ | |
153
+ | +-- Transport::Subprocess.new(options)
154
+ | +-- ControlProtocol.new(transport, options)
155
+ | +-- protocol.start(streaming: true)
156
+ | | +-- transport.connect --> spawn CLI subprocess
157
+ | | +-- reader_loop starts in background Thread
158
+ | | +-- send_initialize --> handshake with CLI
159
+ | +-- protocol.send_user_message(prompt)
160
+ | | +-- write JSON to stdin
161
+ | +-- protocol.each_message yields parsed messages
162
+ | +-- reader_loop reads JSON Lines from stdout
163
+ | +-- routes control_request to RequestHandling
164
+ | +-- routes control_response to pending request
165
+ | +-- queues SDK messages for consumer
166
+ | +-- consumer pops from Queue
167
+ | +-- MessageParser.parse(raw) --> typed message
168
+ | +-- yield message to Enumerator
169
+ |
170
+ +-- TurnResult << message (accumulates)
171
+ +-- EventHandler.handle(message) (dispatches events)
172
+ +-- yield message to caller block (if given)
173
+ +-- return TurnResult
174
+ ```
175
+
176
+ ### Multi-Turn Conversation (`chat`)
177
+
178
+ ```
179
+ User calls ClaudeAgent.chat(**kwargs)
180
+ |
181
+ +-- merge_config_into_kwargs applies global defaults
182
+ +-- Conversation.new(**merged)
183
+ |
184
+ +-- partition_kwargs --> callbacks / conversation_kwargs / options_kwargs
185
+ +-- build_options (compiles PermissionPolicy, HookRegistry, permission mode)
186
+ +-- Client.new(options)
187
+ +-- register_callbacks on Client's EventHandler
188
+ +-- register_timing_hooks for tool activity timestamps
189
+ |
190
+ +-- conversation.say(prompt)
191
+ |
192
+ +-- ensure_connected! (auto-connects on first call)
193
+ | +-- Client.connect
194
+ | +-- ControlProtocol.start(streaming: true)
195
+ +-- Client.send_and_receive(prompt)
196
+ | +-- send_message --> protocol.send_user_message
197
+ | +-- receive_turn
198
+ | +-- TurnResult.new
199
+ | +-- receive_response yields messages
200
+ | +-- TurnResult << message
201
+ | +-- EventHandler.handle(message)
202
+ | +-- CumulativeUsage.track(message)
203
+ | +-- stops on ResultMessage
204
+ +-- build_tool_activities (timestamps from hooks)
205
+ +-- return TurnResult
206
+ ```
207
+
208
+ ### Permission Request Flow
209
+
210
+ ```
211
+ CLI sends control_request { subtype: "can_use_tool" }
212
+ |
213
+ +-- reader_loop receives raw message
214
+ +-- handle_control_request dispatches to handle_can_use_tool
215
+ |
216
+ +-- Mode 1: Synchronous callback
217
+ | +-- options.can_use_tool.call(name, input, context)
218
+ | +-- callback returns PermissionResultAllow or PermissionResultDeny
219
+ | +-- (or callback calls context.request.defer! to switch to queue mode)
220
+ |
221
+ +-- Mode 2: Queue-based
222
+ | +-- PermissionRequest created with Mutex + ConditionVariable
223
+ | +-- pushed to PermissionQueue
224
+ | +-- reader thread blocks on perm_request.wait(timeout:)
225
+ | +-- main thread polls client.pending_permission
226
+ | +-- main thread calls request.allow! or request.deny!
227
+ | +-- ConditionVariable.broadcast unblocks reader thread
228
+ |
229
+ +-- Mode 3: Default allow (no callback, no queue)
230
+ |
231
+ +-- normalize_permission_result --> Hash
232
+ +-- send_control_response back to CLI
233
+ ```
234
+
235
+ ---
236
+
237
+ ## Immutable Types
238
+
239
+ All message types and content blocks use `Data.define`, frozen at construction:
240
+
241
+ **Messages**: `UserMessage`, `UserMessageReplay`, `AssistantMessage`, `SystemMessage`, `ResultMessage`, `StreamEvent`, `CompactBoundaryMessage`, `StatusMessage`, `ToolProgressMessage`, `HookResponseMessage`, `AuthStatusMessage`, `TaskNotificationMessage`, `HookStartedMessage`, `HookProgressMessage`, `ToolUseSummaryMessage`, `FilesPersistedEvent`, `TaskStartedMessage`, `TaskProgressMessage`, `RateLimitEvent`, `PromptSuggestionMessage`, `ElicitationCompleteMessage`, `LocalCommandOutputMessage`, `GenericMessage`
242
+
243
+ **Content Blocks**: `TextBlock`, `ThinkingBlock`, `ToolUseBlock`, `ToolResultBlock`, `ServerToolUseBlock`, `ServerToolResultBlock`, `ImageContentBlock`, `GenericBlock`
244
+
245
+ **Data Types**: `SessionInfo`, `SessionMessage`, `ForkSessionResult`, `ToolActivity`, `TaskUsage`, `SDKPermissionDenial`, `RewindFilesResult`, `ToolsPreset`, `SlashCommand`, `McpServerStatus`, `McpSetServersResult`, `PermissionResultAllow`, `PermissionResultDeny`
246
+
247
+ ---
248
+
249
+ ## Thread Safety
250
+
251
+ | Component | Mechanism | Purpose |
252
+ |---------------------------------------------|-------------------------------|----------------------------------------------------------------------------------------------------|
253
+ | `PermissionRequest` | `Mutex` + `ConditionVariable` | Reader thread blocks on `wait`; main thread resolves via `allow!` / `deny!` |
254
+ | `PermissionQueue` | `Queue` (thread-safe stdlib) | Bridges reader thread and consumer thread for permission requests |
255
+ | `AbortSignal` | `Mutex` + `ConditionVariable` | Multiple consumers check `aborted?`; `on_abort` callbacks fire once; `wait` blocks until triggered |
256
+ | `CumulativeUsage` | `Mutex` | All reads and writes synchronized |
257
+ | `ControlProtocol` | `Mutex` + `ConditionVariable` | Shared state for pending requests/results; reader thread signals consumer |
258
+ | `Transport::Subprocess` | `Mutex` | Protects stdin writes and stream close operations |
259
+ | `ControlProtocol.reader_loop` | Background `Thread` | Reads transport, routes control messages, queues SDK messages |
260
+ | `Transport::Subprocess.start_stderr_reader` | Background `Thread` | Drains stderr to prevent pipe buffer fill; forwards to `stderr_callback` |
261
+
262
+ ---
263
+
264
+ ## Types Reference
265
+
266
+ ### Core Return Types
267
+
268
+ | Type | Description |
269
+ |-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
270
+ | `TurnResult` | Accumulator for a single turn. Accessors: `text`, `thinking`, `tool_uses`, `tool_results`, `tool_executions`, `cost`, `session_id`, `usage`, `model`, `stop_reason`, `structured_output`, `permission_denials` |
271
+ | `CumulativeUsage` | Cross-turn token/cost tracker. Fields: `input_tokens`, `output_tokens`, `cache_read_input_tokens`, `cache_creation_input_tokens`, `total_cost_usd`, `num_turns`, `duration_ms`, `duration_api_ms` |
272
+ | `EventHandler` | Event dispatcher. Events: `message`, `user`, `assistant`, `system`, `result`, `stream_event`, `status`, `tool_progress`, `text`, `thinking`, `tool_use`, `tool_result`, and more |
273
+
274
+ ### Tool Activity
275
+
276
+ | Type | Description |
277
+ |-----------------------|-----------------------------------------------------------------------------------------------|
278
+ | `ToolActivity` | Immutable. Post-turn record of a tool execution with timestamps and turn index |
279
+ | `LiveToolActivity` | Mutable. Real-time status (`:running`, `:done`, `:error`) with elapsed time |
280
+ | `ToolActivityTracker` | Enumerable collection with `on_start` / `on_complete` / `on_progress` / `on_change` callbacks |
281
+
282
+ ### Permissions
283
+
284
+ | Type | Description |
285
+ |-------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|
286
+ | `PermissionRequest` | Deferred request with `allow!` / `deny!` / `defer!`. Thread-safe resolution |
287
+ | `PermissionQueue` | Thread-safe queue with `poll` (non-blocking) and `pop` (blocking) |
288
+ | `PermissionPolicy` | Declarative DSL. Compiles to `can_use_tool` lambda |
289
+ | `PermissionResultAllow` | Allow response with optional `updated_input` and `updated_permissions` |
290
+ | `PermissionResultDeny` | Deny response with `message` and `interrupt` flag |
291
+ | `ToolPermissionContext` | Context passed to `can_use_tool`: `permission_suggestions`, `blocked_path`, `decision_reason`, `tool_use_id`, `agent_id`, `description`, `signal`, `request` |
292
+
293
+ ### Sessions
294
+
295
+ | Type | Description |
296
+ |--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|
297
+ | `Session` | Rich finder object. Class methods: `find`, `retrieve`, `all`, `where`. Instance: `messages`, `rename`, `fork`, `resume`, `reload` |
298
+ | `SessionInfo` | Immutable metadata: `session_id`, `summary`, `last_modified`, `file_size`, `custom_title`, `first_prompt`, `git_branch`, `cwd`, `tag`, `created_at` |
299
+ | `SessionMessage` | Immutable transcript entry: `type`, `uuid`, `session_id`, `message`, `parent_tool_use_id` |
300
+ | `SessionMessageRelation` | Chainable Enumerable with `where(limit:, offset:)` |
301
+ | `ForkSessionResult` | Fork result with new `session_id` |
302
+
303
+ ### MCP
304
+
305
+ | Type | Description |
306
+ |-----------------------|-----------------------------------------------------------------------------------------------|
307
+ | `MCP::Server` | In-process MCP server hosting `MCP::Tool` instances |
308
+ | `MCP::Tool` | Tool definition with schema normalization and handler block |
309
+ | `McpServerStatus` | Status of an MCP server: `name`, `status`, `server_info`, `error`, `config`, `scope`, `tools` |
310
+ | `McpSetServersResult` | Result of dynamic server management: `added`, `removed`, `errors` |
311
+
312
+ ### Abort
313
+
314
+ | Type | Description |
315
+ |-------------------|-------------------------------------------------------------------------------------------|
316
+ | `AbortController` | Owns an `AbortSignal`. Methods: `abort(reason)`, `reset!` |
317
+ | `AbortSignal` | Thread-safe signal. Methods: `aborted?`, `reason`, `on_abort`, `wait`, `check!`, `reset!` |
318
+
319
+ ---
320
+
321
+ ## Development
322
+
323
+ ```bash
324
+ bin/setup # Install dependencies
325
+ bundle exec rake # Unit tests + RBS + RuboCop (default task)
326
+ bundle exec rake test # Unit tests only
327
+ bundle exec rake test_integration # Integration tests (requires CLI v2.0.0+)
328
+ bundle exec rake test_smoke # Smoke tests against local LLM (e.g. Ollama)
329
+ bundle exec rake test_all # All tests
330
+ bundle exec rake rbs # Validate RBS type signatures
331
+ bundle exec rubocop # Lint
332
+ bin/console # IRB with gem loaded
333
+ ```
334
+
335
+ **Binstubs**: `bin/test`, `bin/test-integration`, `bin/test-all`, `bin/test-smoke`, `bin/rbs-validate`
336
+
337
+ **Test structure**: Unit tests in `test/claude_agent/` (no CLI required), integration tests in `test/integration/` (require `INTEGRATION=true`), smoke tests in `test/smoke/` (require Ollama + `SMOKE=true`). Support files and mocks in `test/support/`.
338
+
339
+ **Single file**: `bundle exec ruby -Itest test/claude_agent/test_foo.rb`