claude-agent-sdk 1.4.2 → 1.4.3

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: e818247c70aeb869293f08c2ae18750d4fdc03ac3caa77c3f40be464df2ed8d6
4
- data.tar.gz: 3e29b5ea072ad2514ec3949efd1fa5dd339085d5859ac597fd2b4e1c44548ac2
3
+ metadata.gz: bb80b64b04c7a08624103b5ebf09504a42f20bd42d8587e23a10761dffa54d7e
4
+ data.tar.gz: 80dbdc35d394a22bbe20f5db952044cf82668e5c12c3eb98de2cb454779f6741
5
5
  SHA512:
6
- metadata.gz: 166b250491fc219607048dfe81a349002c2c206bb2f3838b566b6b7431215ffd2ad83f634baa94ea757c0b719559dd8c0d3f488b5152479f6c1ebec5013f6f0e
7
- data.tar.gz: 69abc6fbb88df26a6d74a7f348a07f83fa7010df39f0f031d7d3ba39d5fdf083706b305f5561a072d84fbfe9e09eec6f2eb6e7e80b4f20b3600a6357707c8d1b
6
+ metadata.gz: 355f4fd5d1163e46b922be4c8320b6d3ede6a916d7dd2b732aa70dc955d924411e0cb19b87b86b76c1a2cd4cdd3fc0d4fc1599605b60b6b88b7cbb70518c3f1d
7
+ data.tar.gz: a9e98c5026f9b04ac4a50b39c665cebc6e6b29058a8db8050a6293fc3aa49044c20cb5a48bddf3b6ccd0636031a79a86377b95fca8f684b2af2bb909cd9c558b
data/CHANGELOG.md CHANGED
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.4.3] - 2026-10-28
11
+
12
+ Syncs with Python SDK 0.2.174 (previously 0.2.162). Python #1331 and #1332 were already in 1.2.0, and `SandboxSettings#ignore_violations` already took the command-pattern Hash that Python #1380 fixed its type to; every other release in the range only bumps the CLI Python bundles.
13
+
14
+ ### Fixed
15
+ - **`get_session_messages` and `get_subagent_messages` (disk and `session_store:`) no longer drop the tool calls of a parallel batch** (Python SDK #1422). The conversation goes on from the tool result written last. When that result answers an earlier call of the batch than the last one, the `tool_use` entries written after that call were off the path the readers walk, so 1.2.0's fix (#157) never saw them: the readers returned neither those calls nor their results. Of the parallel batches in one developer's local transcripts, about 7% have this shape. The readers now put back every entry of an API message on the conversation, right after its last entry there (in timestamp order), then the results as before. A result that the CLI parented on a hook attachment instead of its call's entry (its `sourceToolAssistantUUID` still names the call) is put back as well, when the same agent wrote both. The guards from #157 hold: the chain's own result wins, at most one result comes back per call, and the result of a branch a rewind dropped stays out. That last guard was also too wide on real transcripts, and missed results #157 meant to return: going on from a result no longer counts as a dropped branch when what goes on is a later call of the same batch, another result of the batch (the CLI sometimes parents one under an earlier result's hook attachment), or a meta entry (the `[Image: ...]` note the CLI writes under a screenshot result). A branch that goes on through a meta entry or through another result of the batch is still recognized. Affected sessions return more messages than before, so `offset:`/`limit:` pages over them shift. Python fixes the main-session readers only; here both readers are fixed.
16
+
17
+ ### Changed
18
+ - `docs/options.md`: `include_hook_events` does not turn off the `SessionStart` and `Setup` hook events, which the CLI sends whether or not it is set (Python SDK #1336).
19
+ - `CLIInstaller::PINNED_CLI_VERSION` moves from 2.1.295 to **2.1.311**, following the CLI the Python SDK bundles. `CLIInstaller.install_pinned` installs it. See the [Claude Code changelog](https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md).
20
+
10
21
  ## [1.4.2] - 2026-10-27
11
22
 
12
23
  ### Fixed
data/docs/options.md CHANGED
@@ -158,7 +158,7 @@ Asks the CLI for partial message chunks while the model is still producing a mes
158
158
 
159
159
  ### `include_hook_events`
160
160
 
161
- Asks the CLI to put all hook lifecycle events into the message stream, as `HookStartedMessage`, `HookProgressMessage` and `HookResponseMessage` (see [System and progress messages](types.md#system-and-progress-messages)). It only adds messages: the callbacks you register with `hooks` are called whether or not it is set.
161
+ Asks the CLI to put all hook lifecycle events into the message stream, as `HookStartedMessage`, `HookProgressMessage` and `HookResponseMessage` (see [System and progress messages](types.md#system-and-progress-messages)). It only adds messages: the callbacks you register with `hooks` are called whether or not it is set. It does not turn any off either: the CLI sends the events of `SessionStart` and `Setup` hooks whether or not it is set.
162
162
 
163
163
  ## The CLI process
164
164
 
@@ -53,7 +53,7 @@ module ClaudeAgentSDK
53
53
  # Single source of truth: bumped here (and only here) by
54
54
  # .github/workflows/cli-pin-bump.yml or a Python-sync release, so a
55
55
  # Dependabot bump of the gem carries the CLI forward with it.
56
- PINNED_CLI_VERSION = '2.1.295'
56
+ PINNED_CLI_VERSION = '2.1.311'
57
57
  # @api private
58
58
  BINARY_NAME = 'claude'
59
59
  # @api private
@@ -2037,25 +2037,29 @@ module ClaudeAgentSDK
2037
2037
  %w[user assistant].include?(entry['type']) && !off_main_conversation?(entry)
2038
2038
  end
2039
2039
 
2040
- # Put the results of parallel tool calls back on a leaf-to-root chain.
2040
+ # Put the tool calls and results of parallel tool calls back on a
2041
+ # leaf-to-root chain.
2041
2042
  #
2042
- # The CLI writes one assistant entry per tool_use block (chained through
2043
- # parentUuid) and parents every tool_result on the entry that holds ITS
2044
- # tool_use. With two or more calls in one API message, only the result
2045
- # the conversation continued from is an ancestor of the leaf; the others
2046
- # are siblings of the next tool_use entry, and a single-path walk returns
2047
- # their tool_use without them.
2043
+ # The CLI writes one assistant entry per content block (they share
2044
+ # message.id) and parents every tool_result on the entry that holds ITS
2045
+ # tool_use. With two or more calls in one API message the transcript
2046
+ # forks: the conversation goes on from the result written last, and a
2047
+ # single-path walk keeps only that branch. When that result answers an
2048
+ # earlier call, the tool_use entries written after it are off the path
2049
+ # too, results and all (Python #1422).
2048
2050
  #
2049
- # For each assistant entry on the chain, take its user children that are
2050
- # not on the chain and carry a tool_result for a tool_use on the chain,
2051
- # and insert them — in file order — before the next user entry of the
2052
- # chain (the batch's own on-chain result), or at the end when the chain
2053
- # has none. The anchor, not the raw file position, decides the place: a
2054
- # result that arrived after the conversation had already moved on to a
2055
- # further tool_use of the same message still lands with its batch, so
2056
- # every result follows the assistant turn that asked for it.
2051
+ # So first the off-chain entries of every API message on the chain go
2052
+ # back in, right after that message's last entry on the chain
2053
+ # (insert_parallel_siblings). Then, for each assistant entry, the results
2054
+ # off the chain that answer one of its calls are inserted — in file order
2055
+ # — before the next user entry of the chain (the batch's own on-chain
2056
+ # result), or at the end when the chain has none. The anchor, not the raw
2057
+ # file position, decides the place: a result that arrived after the
2058
+ # conversation had already moved on to a further tool_use of the same
2059
+ # message still lands with its batch, so every result follows the
2060
+ # assistant turn that asked for it.
2057
2061
  #
2058
- # The tool_use_id match is what keeps other user siblings out: a prompt
2062
+ # The tool_use_id match is what keeps other user entries out: a prompt
2059
2063
  # abandoned by a rewind is a second child of a chain entry too, and
2060
2064
  # starts a branch that was dropped — and so is the old result of a call
2061
2065
  # the conversation was rewound to and answered again: the chain's own
@@ -2067,6 +2071,7 @@ module ClaudeAgentSDK
2067
2071
  # rejects sidechain / meta / team entries (main transcripts; a subagent
2068
2072
  # transcript is sidechain throughout).
2069
2073
  def reattach_parallel_tool_results(chain, entries, skip_flagged:)
2074
+ chain = insert_parallel_siblings(chain, entries, skip_flagged)
2070
2075
  off_chain = off_chain_tool_results(chain, entries, skip_flagged)
2071
2076
  return chain if off_chain.empty?
2072
2077
 
@@ -2083,29 +2088,79 @@ module ClaudeAgentSDK
2083
2088
  placed.concat(pending.sort_by(&:first).map(&:last))
2084
2089
  end
2085
2090
 
2086
- # { uuid of an assistant entry on the chain => [[file position, entry], ...] }
2087
- # for the off-chain user children reattach_parallel_tool_results places.
2088
- def off_chain_tool_results(chain, entries, skip_flagged) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- one filter per condition of the re-attachment rule
2091
+ # The chain with the off-chain assistant entries of each API message on
2092
+ # it inserted after that message's last entry on the chain, in timestamp
2093
+ # order (file order on a tie, as Python's stable sort). Only a message
2094
+ # the chain holds is completed: an earlier attempt at the same turn is a
2095
+ # different API message and stays out.
2096
+ def insert_parallel_siblings(chain, entries, skip_flagged)
2097
+ recovered = off_chain_siblings(chain, entries, skip_flagged)
2098
+ return chain if recovered.empty?
2099
+
2100
+ chain.flat_map do |entry|
2101
+ siblings = recovered.fetch(entry['uuid'], {}).values
2102
+ [entry, *siblings.sort_by { |timestamp, position, _| [timestamp, position] }.map(&:last)]
2103
+ end
2104
+ end
2105
+
2106
+ # { uuid of an API message's last entry on the chain => { uuid => [timestamp, file position, entry] } }
2107
+ # for that message's entries off the chain.
2108
+ def off_chain_siblings(chain, entries, skip_flagged)
2109
+ anchors = {} # message.id => uuid of its last entry on the chain
2110
+ chain.each do |entry|
2111
+ message_id = assistant_message_id(entry)
2112
+ anchors[message_id] = entry['uuid'] if message_id
2113
+ end
2114
+ return {} if anchors.empty?
2115
+
2116
+ on_chain = chain.to_set { |entry| entry['uuid'] }
2117
+ recovered = {}
2118
+ entries.each_with_index do |entry, position|
2119
+ anchor = anchors[assistant_message_id(entry)]
2120
+ next if anchor.nil? || on_chain.include?(entry['uuid']) || (skip_flagged && off_main_conversation?(entry))
2121
+
2122
+ # A store can hold a second copy of an entry (a retried mirror batch).
2123
+ (recovered[anchor] ||= {})[entry['uuid']] ||= [entry['timestamp'].to_s, position, entry]
2124
+ end
2125
+ recovered
2126
+ end
2127
+
2128
+ # { uuid of the assistant entry a result belongs to => [[file position, entry], ...] }
2129
+ # for the off-chain results reattach_parallel_tool_results places. A
2130
+ # result belongs to its parent when that is an assistant entry of the
2131
+ # chain, else to the entry holding the call it answers, if the same agent
2132
+ # wrote both: the CLI sometimes parents a result on a hook attachment
2133
+ # (its sourceToolAssistantUUID still names the call).
2134
+ def off_chain_tool_results(chain, entries, skip_flagged) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity -- one filter per condition of the re-attachment rule
2089
2135
  on_chain = Set.new
2090
2136
  assistants = Set.new
2091
- tool_use_ids = Set.new
2137
+ owners = {} # tool_use id => the chain's assistant entry holding it (nil when two API messages claim it)
2092
2138
  answered = Set.new # tool_use ids the chain's own results answer
2093
2139
  chain.each do |entry|
2094
2140
  on_chain << entry['uuid']
2095
2141
  case entry['type']
2096
2142
  when 'assistant'
2097
2143
  assistants << entry['uuid']
2098
- tool_use_ids.merge(content_block_values(entry, 'tool_use', 'id'))
2144
+ content_block_values(entry, 'tool_use', 'id').each do |id|
2145
+ owner = owners.fetch(id, entry)
2146
+ owners[id] = owner && assistant_message_id(owner) == assistant_message_id(entry) ? entry : nil
2147
+ end
2099
2148
  when 'user' then answered.merge(content_block_values(entry, 'tool_result', 'tool_use_id'))
2100
2149
  end
2101
2150
  end
2102
- return {} if (tool_use_ids - answered).empty?
2151
+ return {} if (owners.keys - answered.to_a).empty?
2103
2152
 
2104
- continued = continued_from(entries)
2153
+ # An entry of the chain going on from a result (the next tool_use of
2154
+ # its batch) is not a branch that went on. Nor is another result for a
2155
+ # call of the chain (the CLI sometimes parents one under an earlier
2156
+ # result's hook attachment) — unless something went on from it in turn.
2157
+ batch = entries.filter_map do |entry|
2158
+ entry['uuid'] if entry['type'] == 'user' && answers_chain_call?(entry, owners)
2159
+ end
2160
+ continued = continued_from(entries, except: on_chain, relay: batch.to_set)
2105
2161
  found = {}
2106
2162
  entries.each_with_index do |entry, position|
2107
- next unless entry['type'] == 'user' && assistants.include?(entry['parentUuid'])
2108
- next if on_chain.include?(entry['uuid'])
2163
+ next unless entry['type'] == 'user' && !on_chain.include?(entry['uuid'])
2109
2164
  next if skip_flagged && off_main_conversation?(entry)
2110
2165
  # A result something went on from started a branch that a rewind
2111
2166
  # dropped (the conversation was rewound to the call and went on
@@ -2114,13 +2169,17 @@ module ClaudeAgentSDK
2114
2169
  next if continued.include?(entry['uuid'])
2115
2170
 
2116
2171
  ids = content_block_values(entry, 'tool_result', 'tool_use_id')
2117
- next if ids.empty? || !ids.all? { |id| tool_use_ids.include?(id) && !answered.include?(id) }
2172
+ next if !answers_chain_call?(entry, owners) || ids.any? { |id| answered.include?(id) }
2173
+
2174
+ anchor = entry['parentUuid'] if assistants.include?(entry['parentUuid'])
2175
+ anchor ||= (owner = owners[ids.first]) && same_writer?(entry, owner) && owner['uuid']
2176
+ next unless anchor
2118
2177
 
2119
2178
  # Claimed: a later result for the same call is not added — nor a
2120
2179
  # second copy of this entry, which a store can hold (a retried mirror
2121
2180
  # batch overlaps the write it retries).
2122
2181
  answered.merge(ids)
2123
- (found[entry['parentUuid']] ||= []) << [position, entry]
2182
+ (found[anchor] ||= []) << [position, entry]
2124
2183
  end
2125
2184
  found
2126
2185
  end
@@ -2128,21 +2187,64 @@ module ClaudeAgentSDK
2128
2187
  # uuids of the entries a user or assistant entry continues from: its
2129
2188
  # nearest user / assistant ancestor, reached through entries that are
2130
2189
  # neither (hook attachments, system entries) — so a hook attachment
2131
- # written after an entry does not count as going on from it.
2132
- def continued_from(entries)
2190
+ # written after an entry does not count as going on from it. Meta
2191
+ # entries count as neither: the CLI writes one under a result itself
2192
+ # (the "[Image: ...]" note of a screenshot), so it does not go on from
2193
+ # the result, and a branch that goes on through one is still found.
2194
+ # Entries whose uuid is in +except+ are not counted as going on; those in
2195
+ # +relay+ only once something goes on from them.
2196
+ def continued_from(entries, except: Set.new, relay: Set.new)
2197
+ parents = goes_on_from(entries, except)
2198
+ continued = Set.new
2199
+ pending = parents.keys.reject { |uuid| relay.include?(uuid) }
2200
+ while (uuid = pending.pop)
2201
+ from = parents[uuid]
2202
+ pending << from if from && continued.add?(from) && relay.include?(from)
2203
+ end
2204
+ continued
2205
+ end
2206
+
2207
+ # { uuid => uuid of the entry it goes on from } for the user and
2208
+ # assistant entries not in +except+ (see continued_from).
2209
+ def goes_on_from(entries, except)
2133
2210
  by_uuid = {}
2134
2211
  entries.each { |entry| by_uuid[entry['uuid']] = entry if entry['uuid'] }
2135
- continued = Set.new
2136
- entries.each do |entry|
2137
- next unless %w[user assistant].include?(entry['type'])
2212
+ entries.each_with_object({}) do |entry, found|
2213
+ next if !goes_on?(entry) || except.include?(entry['uuid'])
2138
2214
 
2139
2215
  seen = Set.new
2140
2216
  parent = by_uuid[entry['parentUuid']]
2141
- parent = by_uuid[parent['parentUuid']] while parent && !%w[user assistant].include?(parent['type']) &&
2142
- seen.add?(parent['uuid'])
2143
- continued << parent['uuid'] if parent && %w[user assistant].include?(parent['type'])
2217
+ parent = by_uuid[parent['parentUuid']] while parent && !goes_on?(parent) && seen.add?(parent['uuid'])
2218
+ found[entry['uuid']] = parent['uuid'] if parent && goes_on?(parent)
2144
2219
  end
2145
- continued
2220
+ end
2221
+
2222
+ # A user or assistant entry that is not a meta injection: what
2223
+ # continued_from counts as going on.
2224
+ def goes_on?(entry)
2225
+ %w[user assistant].include?(entry['type']) && !entry['isMeta']
2226
+ end
2227
+
2228
+ # Whether an entry carries tool_results, every one for a call +owners+
2229
+ # (tool_use id => the chain's entry holding it) places.
2230
+ def answers_chain_call?(entry, owners)
2231
+ ids = content_block_values(entry, 'tool_result', 'tool_use_id')
2232
+ !ids.empty? && ids.all? { |id| owners[id] }
2233
+ end
2234
+
2235
+ # The API message.id of an assistant entry (nil for any other entry).
2236
+ def assistant_message_id(entry)
2237
+ return unless entry['type'] == 'assistant'
2238
+
2239
+ message = entry['message']
2240
+ message_id = message['id'] if message.is_a?(Hash)
2241
+ message_id if message_id.is_a?(String) && !message_id.empty?
2242
+ end
2243
+
2244
+ # Whether two entries were written by the same agent (the main session,
2245
+ # or one subagent).
2246
+ def same_writer?(entry, other)
2247
+ !entry['isSidechain'] == !other['isSidechain'] && entry['agentId'] == other['agentId']
2146
2248
  end
2147
2249
 
2148
2250
  # Values of +key+ over the +type+ content blocks of an entry's message
@@ -2208,7 +2310,10 @@ module ClaudeAgentSDK
2208
2310
  :collect_agent_files, :parse_jsonl_entries, :utf8_transcript_text,
2209
2311
  :build_conversation_chain, :walk_to_leaf, :walk_to_root,
2210
2312
  :pick_leaf, :visible_ancestor?, :off_main_conversation?, :visible_message?,
2211
- :reattach_parallel_tool_results, :off_chain_tool_results,
2313
+ :reattach_parallel_tool_results, :insert_parallel_siblings, :off_chain_siblings,
2314
+ :off_chain_tool_results,
2315
+ :continued_from, :goes_on_from, :goes_on?,
2316
+ :answers_chain_call?, :assistant_message_id, :same_writer?,
2212
2317
  :content_block_values,
2213
2318
  :filter_visible_messages, :build_session_info, :created_at_from_file, :user_entry_texts,
2214
2319
  :each_line_past_head,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeAgentSDK
4
- VERSION = '1.4.2'
4
+ VERSION = '1.4.3'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: claude-agent-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.2
4
+ version: 1.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - ya-luotao
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-27 00:00:00.000000000 Z
11
+ date: 2026-10-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async