claude_agent 0.7.14 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b6188550282e29001c6bf2996842b5c11cd013de37aeeded995c982cef67399
4
- data.tar.gz: 33e7d2e6b000d5f085093f236fd531c136e3711be170c3af4c11715fbdedda77
3
+ metadata.gz: 332a35645bfd711fc84e4fe7b537df19113bc30cf7b94879057c1d784e1bea2d
4
+ data.tar.gz: 12eefc64eaed4f54078eb9d27ed8dd891968e11980350a777dcfa4d7281cc7b7
5
5
  SHA512:
6
- metadata.gz: b5ec2de8caa5ef32681e671645dd6d6540d58f86221c3d99186cc7ab7e9a076b892fe41c16e2472b94399c296439b5ceba72f573eb0b218b8b2ff507661d162f
7
- data.tar.gz: c00a1312f295f1f15949cac076155750b2259f5412cfb0cbe6acffa8bcb789927887bb58ebacdc94fd92fa492851f985b975a14bb31abe68b78e23ce4fda8c5d
6
+ metadata.gz: 809c6be65d3327939436ea2e64cd4e531584ffd3b955e504ffdfbb086991588fbec449ab123b4c3daeb1895406bbe963692e811cb51c0fa49aabf3bc7971d217
7
+ data.tar.gz: f12b331660e1835e3b1ae0e2f48e0e16c91ce8be459cd72a0dbdd3908846fcc252a78d36febe34caeb0bd24733f31b3ecba2e2287a49801355fed11048fd0fdb
data/CHANGELOG.md CHANGED
@@ -7,6 +7,24 @@ 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
+
10
28
  ## [0.7.14] - 2026-03-14
11
29
 
12
30
  ### Fixed
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`
@@ -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
@@ -189,17 +189,23 @@ module ClaudeAgent
189
189
  # Receive messages until a ResultMessage, accumulating into a TurnResult
190
190
  #
191
191
  # Dispatches events to registered handlers (see {#on}).
192
+ # On abort, raises {AbortError} with the partial {TurnResult} attached.
192
193
  #
193
194
  # @yield [Message] Each message as it arrives (optional)
194
195
  # @return [TurnResult] The completed turn
196
+ # @raise [AbortError] If abort signal is triggered (with partial_turn attached)
195
197
  def receive_turn
196
198
  require_connection!
197
199
 
198
200
  turn = TurnResult.new
199
- receive_response do |message|
200
- turn << message
201
- @event_handler.handle(message)
202
- yield message if block_given?
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)
203
209
  end
204
210
  @event_handler.reset!
205
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
@@ -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
 
@@ -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
- # @example
96
- # raise ClaudeAgent::AbortError, "Operation cancelled by user"
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
- def initialize(message = "Operation was aborted")
100
- super
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
@@ -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>"
@@ -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 concatenated across assistant messages
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeAgent
4
- VERSION = "0.7.14"
4
+ VERSION = "0.7.15"
5
5
  end
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
- def initialize: (?String message) -> void
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.14
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