claude_agent 0.7.13 → 0.7.15
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 +26 -0
- data/SPEC.md +56 -29
- data/lib/claude_agent/abort_controller.rb +24 -0
- data/lib/claude_agent/client/commands.rb +32 -0
- data/lib/claude_agent/client.rb +10 -6
- data/lib/claude_agent/control_protocol/commands.rb +28 -0
- data/lib/claude_agent/control_protocol/lifecycle.rb +4 -1
- data/lib/claude_agent/control_protocol/messaging.rb +4 -7
- data/lib/claude_agent/control_protocol.rb +23 -0
- data/lib/claude_agent/conversation.rb +17 -2
- data/lib/claude_agent/errors.rb +18 -4
- data/lib/claude_agent/fork_session.rb +117 -0
- data/lib/claude_agent/hooks.rb +4 -0
- data/lib/claude_agent/messages/streaming.rb +37 -0
- data/lib/claude_agent/permission_request.rb +17 -0
- data/lib/claude_agent/query.rb +0 -2
- data/lib/claude_agent/transport/subprocess.rb +2 -2
- data/lib/claude_agent/turn_result.rb +20 -2
- data/lib/claude_agent/types/sessions.rb +8 -0
- data/lib/claude_agent/version.rb +1 -1
- data/lib/claude_agent.rb +12 -0
- data/sig/claude_agent.rbs +38 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 332a35645bfd711fc84e4fe7b537df19113bc30cf7b94879057c1d784e1bea2d
|
|
4
|
+
data.tar.gz: 12eefc64eaed4f54078eb9d27ed8dd891968e11980350a777dcfa4d7281cc7b7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 809c6be65d3327939436ea2e64cd4e531584ffd3b955e504ffdfbb086991588fbec449ab123b4c3daeb1895406bbe963692e811cb51c0fa49aabf3bc7971d217
|
|
7
|
+
data.tar.gz: f12b331660e1835e3b1ae0e2f48e0e16c91ce8be459cd72a0dbdd3908846fcc252a78d36febe34caeb0bd24733f31b3ecba2e2287a49801355fed11048fd0fdb
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.15] - 2026-03-15
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `AbortError#partial_turn` carries the in-progress `TurnResult` when a turn is aborted, eliminating the need for separate extraction paths on cancellation
|
|
14
|
+
- `StreamEvent` typed accessors: `delta_text`, `delta_type`, `thinking_text`, `content_index` for convenient access without hash-digging into raw event data
|
|
15
|
+
- `PermissionRequest#display_label` and `#summary` delegate to `ToolUseBlock` formatting, removing the need to construct dummy blocks for display
|
|
16
|
+
- `AbortController#reset!` and `AbortSignal#reset!` allow reuse across turns; `Conversation#say` auto-resets at turn start
|
|
17
|
+
- `PostCompact` hook event with `PostCompactInput` type (`trigger` and `compact_summary` fields) (TypeScript SDK v0.2.76 parity)
|
|
18
|
+
- `cancel_async_message` control request to drop queued user messages by UUID (TypeScript SDK v0.2.76 parity)
|
|
19
|
+
- `get_settings` control request to read effective merged settings (TypeScript SDK v0.2.76 parity)
|
|
20
|
+
- `fork_session` standalone function to branch a session from a specific point with UUID remapping (TypeScript SDK v0.2.76 parity)
|
|
21
|
+
- `ForkSessionResult` data type returned by `fork_session`
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- `TurnResult#text` now falls back to accumulated streaming deltas when no `AssistantMessage` text is available (e.g., on abort mid-stream)
|
|
25
|
+
- `Conversation#say` resets the tool tracker at the **start** of each turn instead of the end, so tracker data survives abort and remains accessible until the next `say()` call
|
|
26
|
+
- `Client#receive_turn` catches `AbortError` and re-raises with the partial `TurnResult` attached
|
|
27
|
+
|
|
28
|
+
## [0.7.14] - 2026-03-14
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- Replace busy-wait polling (`Queue#pop(true)` + `sleep 0.01`) in `ControlProtocol::Messaging#each_message` with blocking `Queue#pop` and `:done` sentinel, eliminating CPU waste and up-to-10ms per-message latency
|
|
32
|
+
- Fix `Conversation#partition_kwargs` misrouting `on_elicitation` (and any future `on_*` Options attributes) as event callbacks instead of forwarding to Options; now uses explicit allowlist derived from `EventHandler::EVENTS`
|
|
33
|
+
- Remove global `ENV["CLAUDE_CODE_ENTRYPOINT"]` mutation from `Client#connect` and `ClaudeAgent.query`; the subprocess already receives this via `Options#to_env`
|
|
34
|
+
- Log stderr callback errors instead of silently swallowing them in `Transport::Subprocess`
|
|
35
|
+
|
|
10
36
|
## [0.7.13] - 2026-03-14
|
|
11
37
|
|
|
12
38
|
### Added
|
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.
|
|
7
|
-
- Python SDK: from GitHub (commit
|
|
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-
|
|
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.
|
|
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`
|
|
@@ -34,6 +34,17 @@ module ClaudeAgent
|
|
|
34
34
|
def abort(reason = nil)
|
|
35
35
|
@signal.abort!(reason)
|
|
36
36
|
end
|
|
37
|
+
|
|
38
|
+
# Reset the controller so it can be reused for another turn.
|
|
39
|
+
#
|
|
40
|
+
# After calling {#abort}, the controller is in an aborted state
|
|
41
|
+
# and cannot be reused without resetting. Call this before the
|
|
42
|
+
# next operation, or use {Conversation} which auto-resets.
|
|
43
|
+
#
|
|
44
|
+
# @return [void]
|
|
45
|
+
def reset!
|
|
46
|
+
@signal.reset!
|
|
47
|
+
end
|
|
37
48
|
end
|
|
38
49
|
|
|
39
50
|
# Signal object that tracks abort state (TypeScript SDK parity)
|
|
@@ -93,6 +104,19 @@ module ClaudeAgent
|
|
|
93
104
|
raise AbortError, reason if aborted?
|
|
94
105
|
end
|
|
95
106
|
|
|
107
|
+
# Reset the signal so the controller can be reused.
|
|
108
|
+
#
|
|
109
|
+
# No-op if the signal has not been aborted.
|
|
110
|
+
#
|
|
111
|
+
# @return [void]
|
|
112
|
+
def reset!
|
|
113
|
+
@mutex.synchronize do
|
|
114
|
+
return unless @aborted
|
|
115
|
+
@aborted = false
|
|
116
|
+
@reason = nil
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
96
120
|
# @api private
|
|
97
121
|
# Trigger the abort
|
|
98
122
|
# @param reason [String, nil] Reason for aborting
|
|
@@ -220,6 +220,38 @@ module ClaudeAgent
|
|
|
220
220
|
|
|
221
221
|
@protocol.mcp_clear_auth(server_name)
|
|
222
222
|
end
|
|
223
|
+
|
|
224
|
+
# Cancel a queued async user message (TypeScript SDK v0.2.76 parity)
|
|
225
|
+
#
|
|
226
|
+
# Drops a previously queued user message before it is processed.
|
|
227
|
+
#
|
|
228
|
+
# @param message_uuid [String] UUID of the message to cancel
|
|
229
|
+
# @return [Hash] Response from the CLI
|
|
230
|
+
#
|
|
231
|
+
# @example
|
|
232
|
+
# client.cancel_async_message("msg-uuid-123")
|
|
233
|
+
#
|
|
234
|
+
def cancel_async_message(message_uuid)
|
|
235
|
+
require_connection!
|
|
236
|
+
|
|
237
|
+
@protocol.cancel_async_message(message_uuid)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Get effective merged settings (TypeScript SDK v0.2.76 parity)
|
|
241
|
+
#
|
|
242
|
+
# Returns the current effective settings after merging all layers.
|
|
243
|
+
#
|
|
244
|
+
# @return [Hash] Merged settings
|
|
245
|
+
#
|
|
246
|
+
# @example
|
|
247
|
+
# settings = client.get_settings
|
|
248
|
+
# puts settings["model"]
|
|
249
|
+
#
|
|
250
|
+
def get_settings
|
|
251
|
+
require_connection!
|
|
252
|
+
|
|
253
|
+
@protocol.get_settings
|
|
254
|
+
end
|
|
223
255
|
end
|
|
224
256
|
end
|
|
225
257
|
end
|
data/lib/claude_agent/client.rb
CHANGED
|
@@ -77,8 +77,6 @@ module ClaudeAgent
|
|
|
77
77
|
def connect(prompt: nil)
|
|
78
78
|
raise CLIConnectionError, "Already connected" if @connected
|
|
79
79
|
|
|
80
|
-
ENV["CLAUDE_CODE_ENTRYPOINT"] = "sdk-rb-client"
|
|
81
|
-
|
|
82
80
|
logger.info("client") { "Connecting" }
|
|
83
81
|
@protocol = ControlProtocol.new(transport: @transport, options: @options)
|
|
84
82
|
@protocol.permission_queue = @permission_queue
|
|
@@ -191,17 +189,23 @@ module ClaudeAgent
|
|
|
191
189
|
# Receive messages until a ResultMessage, accumulating into a TurnResult
|
|
192
190
|
#
|
|
193
191
|
# Dispatches events to registered handlers (see {#on}).
|
|
192
|
+
# On abort, raises {AbortError} with the partial {TurnResult} attached.
|
|
194
193
|
#
|
|
195
194
|
# @yield [Message] Each message as it arrives (optional)
|
|
196
195
|
# @return [TurnResult] The completed turn
|
|
196
|
+
# @raise [AbortError] If abort signal is triggered (with partial_turn attached)
|
|
197
197
|
def receive_turn
|
|
198
198
|
require_connection!
|
|
199
199
|
|
|
200
200
|
turn = TurnResult.new
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
201
|
+
begin
|
|
202
|
+
receive_response do |message|
|
|
203
|
+
turn << message
|
|
204
|
+
@event_handler.handle(message)
|
|
205
|
+
yield message if block_given?
|
|
206
|
+
end
|
|
207
|
+
rescue AbortError => e
|
|
208
|
+
raise AbortError.new(e.message, partial_turn: turn)
|
|
205
209
|
end
|
|
206
210
|
@event_handler.reset!
|
|
207
211
|
turn
|
|
@@ -299,6 +299,34 @@ module ClaudeAgent
|
|
|
299
299
|
errors: response["errors"] || {}
|
|
300
300
|
)
|
|
301
301
|
end
|
|
302
|
+
# Cancel a queued async user message (TypeScript SDK v0.2.76 parity)
|
|
303
|
+
#
|
|
304
|
+
# Drops a previously queued user message before it is processed.
|
|
305
|
+
#
|
|
306
|
+
# @param message_uuid [String] UUID of the message to cancel
|
|
307
|
+
# @return [Hash] Response from the CLI
|
|
308
|
+
#
|
|
309
|
+
# @example
|
|
310
|
+
# protocol.cancel_async_message("msg-uuid-123")
|
|
311
|
+
#
|
|
312
|
+
def cancel_async_message(message_uuid)
|
|
313
|
+
send_control_request(subtype: "cancel_async_message", message_uuid: message_uuid)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Get effective merged settings (TypeScript SDK v0.2.76 parity)
|
|
317
|
+
#
|
|
318
|
+
# Returns the current effective settings after merging all layers
|
|
319
|
+
# (user, project, local, flag, etc.).
|
|
320
|
+
#
|
|
321
|
+
# @return [Hash] Merged settings
|
|
322
|
+
#
|
|
323
|
+
# @example
|
|
324
|
+
# settings = protocol.get_settings
|
|
325
|
+
# puts settings["model"]
|
|
326
|
+
#
|
|
327
|
+
def get_settings
|
|
328
|
+
send_control_request(subtype: "get_settings")
|
|
329
|
+
end
|
|
302
330
|
end
|
|
303
331
|
end
|
|
304
332
|
end
|
|
@@ -72,7 +72,8 @@ module ClaudeAgent
|
|
|
72
72
|
@condition.broadcast
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
-
#
|
|
75
|
+
# Unblock the consumer and terminate the transport
|
|
76
|
+
@message_queue.push(:done)
|
|
76
77
|
@transport.terminate if @transport.respond_to?(:terminate)
|
|
77
78
|
end
|
|
78
79
|
|
|
@@ -107,6 +108,8 @@ module ClaudeAgent
|
|
|
107
108
|
rescue AbortError
|
|
108
109
|
logger.debug("protocol") { "Reader thread exiting: abort signal" }
|
|
109
110
|
@running = false
|
|
111
|
+
ensure
|
|
112
|
+
@message_queue.push(:done)
|
|
110
113
|
end
|
|
111
114
|
end
|
|
112
115
|
end
|
|
@@ -26,19 +26,16 @@ module ClaudeAgent
|
|
|
26
26
|
def each_message
|
|
27
27
|
return enum_for(:each_message) unless block_given?
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
# Check abort signal
|
|
29
|
+
loop do
|
|
31
30
|
@abort_signal&.check!
|
|
32
31
|
|
|
32
|
+
raw = @message_queue.pop # blocks until data available
|
|
33
|
+
break if raw == :done # sentinel from reader_loop
|
|
34
|
+
|
|
33
35
|
begin
|
|
34
|
-
raw = @message_queue.pop(true)
|
|
35
36
|
message = @parser.parse(raw)
|
|
36
37
|
yield message
|
|
37
|
-
rescue ThreadError
|
|
38
|
-
# Queue empty, wait a bit
|
|
39
|
-
sleep 0.01
|
|
40
38
|
rescue AbortError
|
|
41
|
-
# Re-raise abort errors
|
|
42
39
|
raise
|
|
43
40
|
rescue => e
|
|
44
41
|
logger.warn("protocol") { "Message parse error: #{e.message}" }
|
|
@@ -25,6 +25,28 @@ module ClaudeAgent
|
|
|
25
25
|
# protocol.start
|
|
26
26
|
# protocol.each_message { |msg| process(msg) }
|
|
27
27
|
#
|
|
28
|
+
# State ownership across modules:
|
|
29
|
+
#
|
|
30
|
+
# Owned by Lifecycle:
|
|
31
|
+
# @running, @reader_thread
|
|
32
|
+
#
|
|
33
|
+
# Owned by RequestHandling:
|
|
34
|
+
# @hook_callbacks
|
|
35
|
+
#
|
|
36
|
+
# Owned by Primitives:
|
|
37
|
+
# write_message, read helpers (stateless)
|
|
38
|
+
#
|
|
39
|
+
# Owned by Commands:
|
|
40
|
+
# interrupt, rewind (stateless, uses @transport)
|
|
41
|
+
#
|
|
42
|
+
# Shared (initialized here, used by multiple modules):
|
|
43
|
+
# @transport, @options, @parser, @server_info
|
|
44
|
+
# @request_counter, @pending_requests, @pending_results (Primitives + RequestHandling)
|
|
45
|
+
# @mutex, @condition (Primitives + Lifecycle + RequestHandling)
|
|
46
|
+
# @message_queue (Lifecycle + Messaging)
|
|
47
|
+
# @abort_signal (Lifecycle + Messaging)
|
|
48
|
+
# @permission_queue (RequestHandling, set externally by Client)
|
|
49
|
+
#
|
|
28
50
|
class ControlProtocol
|
|
29
51
|
DEFAULT_TIMEOUT = 60
|
|
30
52
|
REQUEST_ID_PREFIX = "req"
|
|
@@ -78,6 +100,7 @@ module ClaudeAgent
|
|
|
78
100
|
|
|
79
101
|
# Abort signal from options
|
|
80
102
|
@abort_signal = options&.abort_signal
|
|
103
|
+
@abort_signal&.on_abort { @message_queue.push(:done) }
|
|
81
104
|
end
|
|
82
105
|
|
|
83
106
|
private
|
|
@@ -100,12 +100,21 @@ module ClaudeAgent
|
|
|
100
100
|
# Send a message and receive the complete turn result.
|
|
101
101
|
#
|
|
102
102
|
# Auto-connects on first call. Appends to conversation history.
|
|
103
|
+
# Resets the tool tracker and abort signal at the start of each turn
|
|
104
|
+
# so they are fresh for the new operation.
|
|
105
|
+
#
|
|
106
|
+
# On abort, raises {AbortError} with the partial {TurnResult} attached.
|
|
107
|
+
# The tool tracker retains its state from the aborted turn until the
|
|
108
|
+
# next call to {#say}.
|
|
103
109
|
#
|
|
104
110
|
# @param prompt [String, Array] The message content
|
|
105
111
|
# @yield [Message] Each message as it streams in (optional)
|
|
106
112
|
# @return [TurnResult] The completed turn
|
|
113
|
+
# @raise [AbortError] If abort signal is triggered (with partial_turn attached)
|
|
107
114
|
def say(prompt, &block)
|
|
108
115
|
ensure_connected!
|
|
116
|
+
@tool_tracker&.reset!
|
|
117
|
+
@options.abort_signal&.reset!
|
|
109
118
|
|
|
110
119
|
logger.debug("conversation") { "Turn #{@turns.size}: sending message" }
|
|
111
120
|
|
|
@@ -116,7 +125,6 @@ module ClaudeAgent
|
|
|
116
125
|
|
|
117
126
|
@turns << turn
|
|
118
127
|
build_tool_activities(turn, @turns.size - 1)
|
|
119
|
-
@tool_tracker&.reset!
|
|
120
128
|
|
|
121
129
|
logger.info("conversation") { "Turn #{@turns.size - 1} complete (#{turn.tool_uses.size} tools, cost=$#{total_cost})" }
|
|
122
130
|
|
|
@@ -193,6 +201,13 @@ module ClaudeAgent
|
|
|
193
201
|
|
|
194
202
|
private
|
|
195
203
|
|
|
204
|
+
# Callback keys derived from EventHandler events + aliases + on_permission
|
|
205
|
+
CALLBACK_KEYS = (
|
|
206
|
+
EventHandler::EVENTS.map { |e| :"on_#{e}" } +
|
|
207
|
+
CALLBACK_ALIASES.keys +
|
|
208
|
+
[ :on_permission ]
|
|
209
|
+
).freeze
|
|
210
|
+
|
|
196
211
|
def partition_kwargs(kwargs)
|
|
197
212
|
callbacks = {}
|
|
198
213
|
conversation_kwargs = {}
|
|
@@ -201,7 +216,7 @@ module ClaudeAgent
|
|
|
201
216
|
kwargs.each do |key, value|
|
|
202
217
|
if CONVERSATION_KEYS.include?(key)
|
|
203
218
|
conversation_kwargs[key] = value
|
|
204
|
-
elsif
|
|
219
|
+
elsif CALLBACK_KEYS.include?(key)
|
|
205
220
|
callbacks[key] = value
|
|
206
221
|
else
|
|
207
222
|
options_kwargs[key] = value
|
data/lib/claude_agent/errors.rb
CHANGED
|
@@ -92,12 +92,26 @@ module ClaudeAgent
|
|
|
92
92
|
# This error is raised when an operation is explicitly cancelled,
|
|
93
93
|
# such as through a user interrupt or abort signal.
|
|
94
94
|
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
95
|
+
# When raised from {Client#receive_turn} or {Conversation#say},
|
|
96
|
+
# carries a partial {TurnResult} with whatever was accumulated
|
|
97
|
+
# before cancellation (text, tool executions, usage).
|
|
98
|
+
#
|
|
99
|
+
# @example Accessing partial results
|
|
100
|
+
# begin
|
|
101
|
+
# turn = conversation.say("Fix the bug")
|
|
102
|
+
# rescue ClaudeAgent::AbortError => e
|
|
103
|
+
# turn = e.partial_turn
|
|
104
|
+
# puts turn.text # accumulated text (from stream events if needed)
|
|
105
|
+
# puts turn.tool_uses # tools that were called before abort
|
|
106
|
+
# end
|
|
97
107
|
#
|
|
98
108
|
class AbortError < Error
|
|
99
|
-
|
|
100
|
-
|
|
109
|
+
# @return [TurnResult, nil] Partial turn accumulated before abort
|
|
110
|
+
attr_reader :partial_turn
|
|
111
|
+
|
|
112
|
+
def initialize(message = "Operation was aborted", partial_turn: nil)
|
|
113
|
+
@partial_turn = partial_turn
|
|
114
|
+
super(message)
|
|
101
115
|
end
|
|
102
116
|
end
|
|
103
117
|
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module ClaudeAgent
|
|
7
|
+
# Fork a session by creating a new session file with remapped UUIDs.
|
|
8
|
+
#
|
|
9
|
+
# Reads all JSONL entries from the source session, optionally truncates
|
|
10
|
+
# at a given message UUID, remaps all UUIDs to fresh values, and writes
|
|
11
|
+
# a new session file in the same project directory.
|
|
12
|
+
#
|
|
13
|
+
# @example Basic fork
|
|
14
|
+
# result = ClaudeAgent.fork_session("abc-123-...")
|
|
15
|
+
# puts result.session_id
|
|
16
|
+
#
|
|
17
|
+
# @example Fork with truncation and title
|
|
18
|
+
# result = ClaudeAgent.fork_session(
|
|
19
|
+
# "abc-123-...",
|
|
20
|
+
# up_to_message_id: "msg-456-...",
|
|
21
|
+
# title: "Forked conversation"
|
|
22
|
+
# )
|
|
23
|
+
#
|
|
24
|
+
module ForkSession
|
|
25
|
+
UUID_FIELDS = %w[uuid parentUuid].freeze
|
|
26
|
+
SESSION_ID_FIELDS = %w[session_id sessionId].freeze
|
|
27
|
+
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
# Fork a session, creating a new session file with remapped UUIDs.
|
|
31
|
+
#
|
|
32
|
+
# @param session_id [String] UUID of the source session
|
|
33
|
+
# @param up_to_message_id [String, nil] If provided, truncate at this message UUID (inclusive)
|
|
34
|
+
# @param title [String, nil] If provided, append a custom-title entry
|
|
35
|
+
# @param dir [String, nil] Project directory to find the session in
|
|
36
|
+
# @return [ForkSessionResult] Result with the new session ID
|
|
37
|
+
# @raise [ArgumentError] If session_id is not a valid UUID
|
|
38
|
+
# @raise [Error] If the session file is not found
|
|
39
|
+
def call(session_id, up_to_message_id: nil, title: nil, dir: nil)
|
|
40
|
+
SessionMutations.send(:validate_session_id!, session_id)
|
|
41
|
+
|
|
42
|
+
source_path = SessionMutations.send(:find_session_file, session_id, dir: dir)
|
|
43
|
+
raise Error, "Session not found: #{session_id}" unless source_path
|
|
44
|
+
|
|
45
|
+
lines = File.readlines(source_path, chomp: true)
|
|
46
|
+
entries = lines.filter_map do |line|
|
|
47
|
+
next if line.strip.empty?
|
|
48
|
+
JSON.parse(line)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Truncate at up_to_message_id (inclusive)
|
|
52
|
+
if up_to_message_id
|
|
53
|
+
cut_index = entries.index { |e| e["uuid"] == up_to_message_id }
|
|
54
|
+
raise ArgumentError, "Message not found: #{up_to_message_id}" unless cut_index
|
|
55
|
+
entries = entries[0..cut_index]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Generate new session ID and UUID map
|
|
59
|
+
new_session_id = SecureRandom.uuid
|
|
60
|
+
uuid_map = build_uuid_map(entries)
|
|
61
|
+
|
|
62
|
+
# Remap all entries
|
|
63
|
+
remapped = entries.map { |entry| remap_entry(entry, uuid_map, new_session_id) }
|
|
64
|
+
|
|
65
|
+
# Append custom-title entry if requested
|
|
66
|
+
if title
|
|
67
|
+
remapped << {
|
|
68
|
+
"type" => "custom-title",
|
|
69
|
+
"customTitle" => title.to_s,
|
|
70
|
+
"sessionId" => new_session_id
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Write new session file in the same directory as the source
|
|
75
|
+
dest_path = File.join(File.dirname(source_path), "#{new_session_id}.jsonl")
|
|
76
|
+
File.open(dest_path, "w") do |f|
|
|
77
|
+
remapped.each { |entry| f.write("#{JSON.generate(entry)}\n") }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
ForkSessionResult.new(session_id: new_session_id)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Build a map of old UUIDs to new UUIDs from all entries.
|
|
84
|
+
# @param entries [Array<Hash>]
|
|
85
|
+
# @return [Hash<String, String>]
|
|
86
|
+
def build_uuid_map(entries)
|
|
87
|
+
map = {}
|
|
88
|
+
entries.each do |entry|
|
|
89
|
+
UUID_FIELDS.each do |field|
|
|
90
|
+
value = entry[field]
|
|
91
|
+
next unless value.is_a?(String) && !value.empty?
|
|
92
|
+
map[value] ||= SecureRandom.uuid
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
map
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Remap UUID and session_id fields in an entry.
|
|
99
|
+
# @param entry [Hash] Original entry
|
|
100
|
+
# @param uuid_map [Hash<String, String>] Old UUID to new UUID mapping
|
|
101
|
+
# @param new_session_id [String] New session ID
|
|
102
|
+
# @return [Hash] Remapped entry
|
|
103
|
+
def remap_entry(entry, uuid_map, new_session_id)
|
|
104
|
+
result = entry.dup
|
|
105
|
+
|
|
106
|
+
UUID_FIELDS.each do |field|
|
|
107
|
+
result[field] = uuid_map[result[field]] if result.key?(field) && uuid_map.key?(result[field])
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
SESSION_ID_FIELDS.each do |field|
|
|
111
|
+
result[field] = new_session_id if result.key?(field)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
result
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
data/lib/claude_agent/hooks.rb
CHANGED
|
@@ -14,6 +14,7 @@ module ClaudeAgent
|
|
|
14
14
|
SubagentStart
|
|
15
15
|
SubagentStop
|
|
16
16
|
PreCompact
|
|
17
|
+
PostCompact
|
|
17
18
|
PermissionRequest
|
|
18
19
|
Setup
|
|
19
20
|
TeammateIdle
|
|
@@ -184,6 +185,9 @@ module ClaudeAgent
|
|
|
184
185
|
required: [ :trigger ],
|
|
185
186
|
optional: { custom_instructions: nil }
|
|
186
187
|
|
|
188
|
+
BaseHookInput.define_input "PostCompact",
|
|
189
|
+
required: [ :trigger, :compact_summary ]
|
|
190
|
+
|
|
187
191
|
BaseHookInput.define_input "PermissionRequest",
|
|
188
192
|
required: [ :tool_name, :tool_input ],
|
|
189
193
|
optional: { permission_suggestions: nil }
|
|
@@ -24,6 +24,43 @@ module ClaudeAgent
|
|
|
24
24
|
def event_type
|
|
25
25
|
event[:type]
|
|
26
26
|
end
|
|
27
|
+
|
|
28
|
+
# Text delta content, or nil if this is not a text_delta event.
|
|
29
|
+
# @return [String, nil]
|
|
30
|
+
def delta_text
|
|
31
|
+
return nil unless event_type == "content_block_delta"
|
|
32
|
+
delta = event[:delta] || event["delta"]
|
|
33
|
+
return nil unless delta
|
|
34
|
+
type = delta[:type] || delta["type"]
|
|
35
|
+
return nil unless type == "text_delta"
|
|
36
|
+
delta[:text] || delta["text"]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The delta type (e.g. "text_delta", "thinking_delta", "input_json_delta").
|
|
40
|
+
# @return [String, nil]
|
|
41
|
+
def delta_type
|
|
42
|
+
return nil unless event_type == "content_block_delta"
|
|
43
|
+
delta = event[:delta] || event["delta"]
|
|
44
|
+
return nil unless delta
|
|
45
|
+
delta[:type] || delta["type"]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Thinking delta text, or nil if this is not a thinking_delta event.
|
|
49
|
+
# @return [String, nil]
|
|
50
|
+
def thinking_text
|
|
51
|
+
return nil unless event_type == "content_block_delta"
|
|
52
|
+
delta = event[:delta] || event["delta"]
|
|
53
|
+
return nil unless delta
|
|
54
|
+
type = delta[:type] || delta["type"]
|
|
55
|
+
return nil unless type == "thinking_delta"
|
|
56
|
+
delta[:thinking] || delta["thinking"]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Content block index within the message.
|
|
60
|
+
# @return [Integer, nil]
|
|
61
|
+
def content_index
|
|
62
|
+
event[:index] || event["index"]
|
|
63
|
+
end
|
|
27
64
|
end
|
|
28
65
|
|
|
29
66
|
# Rate limit event (TypeScript SDK v0.2.45 parity)
|
|
@@ -132,6 +132,23 @@ module ClaudeAgent
|
|
|
132
132
|
end
|
|
133
133
|
end
|
|
134
134
|
|
|
135
|
+
# Human-readable label for the permission request.
|
|
136
|
+
#
|
|
137
|
+
# Delegates to {ToolUseBlock#display_label} formatting.
|
|
138
|
+
#
|
|
139
|
+
# @return [String]
|
|
140
|
+
def display_label
|
|
141
|
+
ToolUseBlock.new(id: "", name: tool_name, input: input || {}).display_label
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Detailed summary of the tool call.
|
|
145
|
+
#
|
|
146
|
+
# @param max [Integer] Maximum length before truncation
|
|
147
|
+
# @return [String]
|
|
148
|
+
def summary(max: 60)
|
|
149
|
+
ToolUseBlock.new(id: "", name: tool_name, input: input || {}).summary(max: max)
|
|
150
|
+
end
|
|
151
|
+
|
|
135
152
|
def inspect
|
|
136
153
|
status = resolved? ? "resolved(#{@result&.behavior})" : "pending"
|
|
137
154
|
"#<#{self.class} tool=#{tool_name} status=#{status} age=#{(Time.now - created_at).round(1)}s>"
|
data/lib/claude_agent/query.rb
CHANGED
|
@@ -41,8 +41,6 @@ module ClaudeAgent
|
|
|
41
41
|
transport ||= Transport::Subprocess.new(options: options)
|
|
42
42
|
|
|
43
43
|
Enumerator.new do |yielder|
|
|
44
|
-
# Set entrypoint environment variable
|
|
45
|
-
ENV["CLAUDE_CODE_ENTRYPOINT"] = "sdk-rb"
|
|
46
44
|
query_logger = options.effective_logger
|
|
47
45
|
query_logger.info("query") { "Starting query" }
|
|
48
46
|
query_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
@@ -387,8 +387,8 @@ module ClaudeAgent
|
|
|
387
387
|
@stderr.each_line do |line|
|
|
388
388
|
# Call callback if provided, otherwise just drain
|
|
389
389
|
@options.stderr_callback&.call(line.chomp)
|
|
390
|
-
rescue
|
|
391
|
-
|
|
390
|
+
rescue => e
|
|
391
|
+
logger.debug("transport") { "stderr callback error: #{e.message}" }
|
|
392
392
|
end
|
|
393
393
|
rescue IOError
|
|
394
394
|
# Stream closed, exit thread
|
|
@@ -35,6 +35,7 @@ module ClaudeAgent
|
|
|
35
35
|
def initialize
|
|
36
36
|
@messages = []
|
|
37
37
|
@result = nil
|
|
38
|
+
@streamed_text = +""
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
# Append a message to this turn
|
|
@@ -44,6 +45,13 @@ module ClaudeAgent
|
|
|
44
45
|
def <<(message)
|
|
45
46
|
@messages << message
|
|
46
47
|
@result = message if message.is_a?(ResultMessage)
|
|
48
|
+
|
|
49
|
+
# Accumulate streaming text deltas for reliable text access on abort
|
|
50
|
+
if message.is_a?(StreamEvent)
|
|
51
|
+
delta = message.delta_text
|
|
52
|
+
@streamed_text << delta if delta
|
|
53
|
+
end
|
|
54
|
+
|
|
47
55
|
self
|
|
48
56
|
end
|
|
49
57
|
|
|
@@ -61,10 +69,20 @@ module ClaudeAgent
|
|
|
61
69
|
|
|
62
70
|
# --- Text & Thinking ---
|
|
63
71
|
|
|
64
|
-
# All text content
|
|
72
|
+
# All text content from the turn.
|
|
73
|
+
#
|
|
74
|
+
# Returns text from AssistantMessages when available (canonical source).
|
|
75
|
+
# Falls back to accumulated streaming deltas when assistant messages
|
|
76
|
+
# have no text (e.g., turn was aborted before AssistantMessage arrived).
|
|
77
|
+
#
|
|
65
78
|
# @return [String]
|
|
66
79
|
def text
|
|
67
|
-
assistant_messages.map(&:text).join
|
|
80
|
+
t = assistant_messages.map(&:text).join
|
|
81
|
+
if t.empty? && !@streamed_text.empty?
|
|
82
|
+
@streamed_text.dup.freeze
|
|
83
|
+
else
|
|
84
|
+
t
|
|
85
|
+
end
|
|
68
86
|
end
|
|
69
87
|
|
|
70
88
|
# All thinking content concatenated across assistant messages
|
|
@@ -47,4 +47,12 @@ module ClaudeAgent
|
|
|
47
47
|
super
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
|
+
|
|
51
|
+
# Result of forking a session (TypeScript SDK v0.2.76 parity)
|
|
52
|
+
#
|
|
53
|
+
# @example
|
|
54
|
+
# result = ClaudeAgent.fork_session("abc-123")
|
|
55
|
+
# puts result.session_id # => new UUID
|
|
56
|
+
#
|
|
57
|
+
ForkSessionResult = Data.define(:session_id)
|
|
50
58
|
end
|
data/lib/claude_agent/version.rb
CHANGED
data/lib/claude_agent.rb
CHANGED
|
@@ -38,6 +38,7 @@ require_relative "claude_agent/get_session_messages" # Session transcript rea
|
|
|
38
38
|
require_relative "claude_agent/session_message_relation" # Chainable message query object
|
|
39
39
|
require_relative "claude_agent/session_mutations" # Session rename/tag mutations
|
|
40
40
|
require_relative "claude_agent/get_session_info" # Single session lookup
|
|
41
|
+
require_relative "claude_agent/fork_session" # Session forking (TypeScript SDK v0.2.76 parity)
|
|
41
42
|
require_relative "claude_agent/v2_session" # V2 Session API (unstable)
|
|
42
43
|
require_relative "claude_agent/session" # Session finder
|
|
43
44
|
|
|
@@ -110,6 +111,17 @@ module ClaudeAgent
|
|
|
110
111
|
GetSessionInfo.call(session_id, dir: dir)
|
|
111
112
|
end
|
|
112
113
|
|
|
114
|
+
# Fork a session by creating a new session file with remapped UUIDs.
|
|
115
|
+
#
|
|
116
|
+
# @param session_id [String] UUID of the source session
|
|
117
|
+
# @param up_to_message_id [String, nil] Truncate at this message UUID (inclusive)
|
|
118
|
+
# @param title [String, nil] Title for the forked session
|
|
119
|
+
# @param dir [String, nil] Project directory to find the session in
|
|
120
|
+
# @return [ForkSessionResult]
|
|
121
|
+
def fork_session(session_id, up_to_message_id: nil, title: nil, dir: nil)
|
|
122
|
+
ForkSession.call(session_id, up_to_message_id: up_to_message_id, title: title, dir: dir)
|
|
123
|
+
end
|
|
124
|
+
|
|
113
125
|
# Resume a previous Conversation by session ID
|
|
114
126
|
#
|
|
115
127
|
# @param session_id [String] Session ID to resume
|
data/sig/claude_agent.rbs
CHANGED
|
@@ -91,7 +91,9 @@ module ClaudeAgent
|
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
class AbortError < Error
|
|
94
|
-
|
|
94
|
+
attr_reader partial_turn: TurnResult?
|
|
95
|
+
|
|
96
|
+
def initialize: (?String message, ?partial_turn: TurnResult?) -> void
|
|
95
97
|
end
|
|
96
98
|
|
|
97
99
|
# Abort controller for cancelling operations (TypeScript SDK parity)
|
|
@@ -100,6 +102,7 @@ module ClaudeAgent
|
|
|
100
102
|
|
|
101
103
|
def initialize: () -> void
|
|
102
104
|
def abort: (?String? reason) -> void
|
|
105
|
+
def reset!: () -> void
|
|
103
106
|
end
|
|
104
107
|
|
|
105
108
|
# Abort signal for tracking abort state (TypeScript SDK parity)
|
|
@@ -110,6 +113,7 @@ module ClaudeAgent
|
|
|
110
113
|
def on_abort: () { (String?) -> void } -> void
|
|
111
114
|
def wait: (?timeout: Numeric?) -> bool
|
|
112
115
|
def check!: () -> void
|
|
116
|
+
def reset!: () -> void
|
|
113
117
|
def abort!: (?String? reason) -> void
|
|
114
118
|
end
|
|
115
119
|
|
|
@@ -255,6 +259,13 @@ module ClaudeAgent
|
|
|
255
259
|
def initialize: (type: String, uuid: String, session_id: String, message: untyped, ?parent_tool_use_id: nil) -> void
|
|
256
260
|
end
|
|
257
261
|
|
|
262
|
+
# Fork session result (TypeScript SDK v0.2.76 parity)
|
|
263
|
+
class ForkSessionResult
|
|
264
|
+
attr_reader session_id: String
|
|
265
|
+
|
|
266
|
+
def initialize: (session_id: String) -> void
|
|
267
|
+
end
|
|
268
|
+
|
|
258
269
|
# Agent definition for custom subagents (TypeScript SDK parity)
|
|
259
270
|
class AgentDefinition
|
|
260
271
|
attr_reader description: String
|
|
@@ -680,6 +691,10 @@ module ClaudeAgent
|
|
|
680
691
|
def initialize: (uuid: String, session_id: String, event: Hash[String, untyped], ?parent_tool_use_id: String?) -> void
|
|
681
692
|
def type: () -> :stream_event
|
|
682
693
|
def event_type: () -> String?
|
|
694
|
+
def delta_text: () -> String?
|
|
695
|
+
def delta_type: () -> String?
|
|
696
|
+
def thinking_text: () -> String?
|
|
697
|
+
def content_index: () -> Integer?
|
|
683
698
|
end
|
|
684
699
|
|
|
685
700
|
class CompactBoundaryMessage
|
|
@@ -996,6 +1011,13 @@ module ClaudeAgent
|
|
|
996
1011
|
def initialize: (trigger: String, ?custom_instructions: String?, **untyped) -> void
|
|
997
1012
|
end
|
|
998
1013
|
|
|
1014
|
+
class PostCompactInput < BaseHookInput
|
|
1015
|
+
attr_reader trigger: String
|
|
1016
|
+
attr_reader compact_summary: String
|
|
1017
|
+
|
|
1018
|
+
def initialize: (trigger: String, compact_summary: String, **untyped) -> void
|
|
1019
|
+
end
|
|
1020
|
+
|
|
999
1021
|
class PermissionRequestInput < BaseHookInput
|
|
1000
1022
|
attr_reader tool_name: String
|
|
1001
1023
|
attr_reader tool_input: Hash[String, untyped]
|
|
@@ -1161,6 +1183,8 @@ module ClaudeAgent
|
|
|
1161
1183
|
def pending?: () -> bool
|
|
1162
1184
|
def result: () -> permission_result?
|
|
1163
1185
|
def wait: (?timeout: Numeric?) -> permission_result
|
|
1186
|
+
def display_label: () -> String
|
|
1187
|
+
def summary: (?max: Integer) -> String
|
|
1164
1188
|
def inspect: () -> String
|
|
1165
1189
|
end
|
|
1166
1190
|
|
|
@@ -1384,6 +1408,7 @@ module ClaudeAgent
|
|
|
1384
1408
|
def self.rename_session: (String session_id, String title, ?dir: String?) -> void
|
|
1385
1409
|
def self.tag_session: (String session_id, String? tag, ?dir: String?) -> void
|
|
1386
1410
|
def self.get_session_info: (String session_id, ?dir: String?) -> SessionInfo?
|
|
1411
|
+
def self.fork_session: (String session_id, ?up_to_message_id: String?, ?title: String?, ?dir: String?) -> ForkSessionResult
|
|
1387
1412
|
|
|
1388
1413
|
# Shared session path infrastructure
|
|
1389
1414
|
module SessionPaths
|
|
@@ -1427,6 +1452,14 @@ module ClaudeAgent
|
|
|
1427
1452
|
def self.call: (String session_id, ?dir: String?) -> SessionInfo?
|
|
1428
1453
|
end
|
|
1429
1454
|
|
|
1455
|
+
# Session forking (TypeScript SDK v0.2.76 parity)
|
|
1456
|
+
module ForkSession
|
|
1457
|
+
UUID_FIELDS: Array[String]
|
|
1458
|
+
SESSION_ID_FIELDS: Array[String]
|
|
1459
|
+
|
|
1460
|
+
def self.call: (String session_id, ?up_to_message_id: String?, ?title: String?, ?dir: String?) -> ForkSessionResult
|
|
1461
|
+
end
|
|
1462
|
+
|
|
1430
1463
|
# Convenience methods
|
|
1431
1464
|
def self.conversation: (**untyped) -> Conversation
|
|
1432
1465
|
def self.resume_conversation: (String session_id, **untyped) -> Conversation
|
|
@@ -1514,6 +1547,8 @@ module ClaudeAgent
|
|
|
1514
1547
|
def mcp_server_status: () -> Array[McpServerStatus]
|
|
1515
1548
|
def account_info: () -> AccountInfo
|
|
1516
1549
|
def initialization_result: () -> InitializationResult
|
|
1550
|
+
def cancel_async_message: (String message_uuid) -> Hash[String, untyped]
|
|
1551
|
+
def get_settings: () -> Hash[String, untyped]
|
|
1517
1552
|
def pending_permission: () -> PermissionRequest?
|
|
1518
1553
|
def pending_permissions?: () -> bool
|
|
1519
1554
|
end
|
|
@@ -1580,6 +1615,8 @@ module ClaudeAgent
|
|
|
1580
1615
|
def mcp_server_status: () -> Array[McpServerStatus]
|
|
1581
1616
|
def account_info: () -> AccountInfo
|
|
1582
1617
|
def initialization_result: () -> InitializationResult
|
|
1618
|
+
def cancel_async_message: (String message_uuid) -> Hash[String, untyped]
|
|
1619
|
+
def get_settings: () -> Hash[String, untyped]
|
|
1583
1620
|
end
|
|
1584
1621
|
|
|
1585
1622
|
# Transport layer
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: claude_agent
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.7.
|
|
4
|
+
version: 0.7.15
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Thomas Carr
|
|
@@ -74,6 +74,7 @@ files:
|
|
|
74
74
|
- lib/claude_agent/cumulative_usage.rb
|
|
75
75
|
- lib/claude_agent/errors.rb
|
|
76
76
|
- lib/claude_agent/event_handler.rb
|
|
77
|
+
- lib/claude_agent/fork_session.rb
|
|
77
78
|
- lib/claude_agent/get_session_info.rb
|
|
78
79
|
- lib/claude_agent/get_session_messages.rb
|
|
79
80
|
- lib/claude_agent/hooks.rb
|