claude-agent-sdk 0.33.1 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +93 -0
  3. data/README.md +54 -19
  4. data/docs/cli-installer.md +40 -9
  5. data/docs/client.md +27 -18
  6. data/docs/configuration.md +5 -5
  7. data/docs/errors.md +4 -3
  8. data/docs/mcp-servers.md +22 -0
  9. data/docs/observability.md +6 -0
  10. data/docs/rails.md +92 -51
  11. data/docs/sessions.md +66 -15
  12. data/lib/claude_agent_sdk/cli_installer.rb +34 -18
  13. data/lib/claude_agent_sdk/command_builder.rb +11 -3
  14. data/lib/claude_agent_sdk/configuration.rb +54 -2
  15. data/lib/claude_agent_sdk/errors.rb +11 -3
  16. data/lib/claude_agent_sdk/fiber_boundary.rb +42 -3
  17. data/lib/claude_agent_sdk/instrumentation/otel.rb +21 -2
  18. data/lib/claude_agent_sdk/query.rb +140 -59
  19. data/lib/claude_agent_sdk/railtie.rb +94 -0
  20. data/lib/claude_agent_sdk/sdk_mcp_server.rb +46 -4
  21. data/lib/claude_agent_sdk/session_mutations.rb +39 -12
  22. data/lib/claude_agent_sdk/session_resume.rb +112 -39
  23. data/lib/claude_agent_sdk/session_store.rb +19 -3
  24. data/lib/claude_agent_sdk/session_summary.rb +5 -5
  25. data/lib/claude_agent_sdk/sessions.rb +123 -55
  26. data/lib/claude_agent_sdk/streaming.rb +0 -8
  27. data/lib/claude_agent_sdk/subprocess_cli_transport.rb +319 -54
  28. data/lib/claude_agent_sdk/tasks/claude_agent_sdk.rake +30 -0
  29. data/lib/claude_agent_sdk/tasks.rb +13 -0
  30. data/lib/claude_agent_sdk/testing/session_store_conformance.rb +13 -3
  31. data/lib/claude_agent_sdk/transcript_mirror_batcher.rb +77 -18
  32. data/lib/claude_agent_sdk/types.rb +349 -39
  33. data/lib/claude_agent_sdk/version.rb +1 -1
  34. data/lib/claude_agent_sdk.rb +53 -44
  35. data/lib/generators/claude_agent_sdk/install/install_generator.rb +63 -0
  36. data/lib/generators/claude_agent_sdk/install/templates/claude_agent_sdk.rb.tt +35 -0
  37. metadata +29 -13
@@ -18,6 +18,12 @@ module ClaudeAgentSDK
18
18
  # background flush). This keeps adapter latency off the hot path during
19
19
  # model streaming.
20
20
  #
21
+ # At most ONE background drain task is live at a time. Frames arriving
22
+ # while it is busy stay in the pending buffer, and the drainer picks them up
23
+ # as one coalesced append when its current one completes — so a store slower
24
+ # than the frame rate costs buffered entries, never a task (plus detached
25
+ # batch) per frame, and #enqueue never suspends the read loop.
26
+ #
21
27
  # Adapter failures are retried (MIRROR_APPEND_MAX_ATTEMPTS total) with short
22
28
  # backoff; timeouts are not retried since the in-flight call may still land.
23
29
  # Failures never raise — the local-disk transcript is already durable, so the
@@ -25,17 +31,19 @@ module ClaudeAgentSDK
25
31
  # (which surfaces them as a MirrorErrorMessage). Adapters should dedupe by
26
32
  # entry["uuid"] when present, since a retried batch may overlap a prior write.
27
33
  #
28
- # The semaphore serializes appends, but a #send that exceeds send_timeout is
29
- # abandoned (its worker thread keeps running) and the next drain proceeds, so
30
- # two #append calls for the SAME key can briefly overlap. SessionStore#append
31
- # must be thread-safe per key (see that method's contract). For adapters
32
- # declaring `callback_scheduling -> :inline` the timed-out call is instead
33
- # cancelled cooperatively (interrupted at its next suspension point, ensure
34
- # runs) — but only on the adapter's own fiber; work the adapter offloaded
35
- # may still land, so timeouts are not retried in either mode and the
36
- # cancelled append may remain permanently HALF-applied in the store. The
37
- # drop is surfaced (MirrorErrorMessage, batches_dropped?); the local
38
- # transcript remains the source of truth.
34
+ # The semaphore serializes appends (FIFO, and every drain detaches its batch
35
+ # immediately before queueing on it, so append order matches enqueue order),
36
+ # but a #send that exceeds send_timeout is abandoned (its worker thread keeps
37
+ # running) and the next drain proceeds, so two #append calls for the SAME
38
+ # key can briefly overlap. SessionStore#append must be thread-safe per key
39
+ # (see that method's contract). For adapters declaring
40
+ # `callback_scheduling -> :inline` the timed-out call is instead cancelled
41
+ # cooperatively (interrupted at its next suspension point, ensure runs) —
42
+ # but only on the adapter's own fiber; work the adapter offloaded may still
43
+ # land, so timeouts are not retried in either mode and the cancelled append
44
+ # may remain permanently HALF-applied in the store. The drop is surfaced
45
+ # (MirrorErrorMessage, batches_dropped?); the local transcript remains the
46
+ # source of truth.
39
47
  class TranscriptMirrorBatcher
40
48
  # Eager-flush thresholds (exposed for tests).
41
49
  MAX_PENDING_ENTRIES = 500
@@ -72,6 +80,12 @@ module ClaudeAgentSDK
72
80
  @pending = []
73
81
  @pending_entries = 0
74
82
  @pending_bytes = 0
83
+ # True while the background drain task is live; #enqueue spawns another
84
+ # only once it has exited (see #schedule_background_drain).
85
+ @background_drain_live = false
86
+ # Set when #close starts; from then on anything still in @pending counts
87
+ # as dropped (see #batches_dropped?).
88
+ @closed = false
75
89
  # Batches that exhausted retries (or could not be keyed) and never
76
90
  # reached the store. Written only under @lock; read cross-thread by
77
91
  # #batches_dropped? (a plain Integer read is safe under the GVL).
@@ -86,12 +100,24 @@ module ClaudeAgentSDK
86
100
  # mirror copy is incomplete. Consulted at teardown by the resume-from-store
87
101
  # cleanup so the materialized temp dir (which then holds the only copy of
88
102
  # the dropped turns) is preserved instead of deleted.
103
+ #
104
+ # Frames still buffered once #close has started count too. Query#close
105
+ # stops the read task (the background drainer's parent) right after
106
+ # #close returns, so a frame the read loop enqueued during the close
107
+ # window — or any below-threshold frame after it — is never appended.
108
+ # The old per-frame drain detached it into a parked task whose
109
+ # cancellation counted it; with coalescing it stays in @pending, so it is
110
+ # accounted here. Evaluated lazily: frames the drainer still manages to
111
+ # deliver before the check are not drops, and nothing past #close has to
112
+ # run for a stranded frame to be counted. (A plain ivar/Array#empty? read
113
+ # is safe cross-thread under the GVL, like @dropped_batches.)
89
114
  def batches_dropped?
90
- @dropped_batches.positive?
115
+ @dropped_batches.positive? || (@closed && !@pending.empty?)
91
116
  end
92
117
 
93
118
  # Buffer a frame; schedule an eager background flush if thresholds are
94
- # exceeded. Synchronous and fire-and-forget.
119
+ # exceeded and no background drain is already live (a live one will pick
120
+ # this frame up). Synchronous and fire-and-forget.
95
121
  #
96
122
  # +entries+ are deep-stringified because the transport parses CLI output
97
123
  # with symbolized keys, but SessionStore entries are opaque JSON blobs that
@@ -109,12 +135,10 @@ module ClaudeAgentSDK
109
135
  @pending << { file_path: file_path, entries: entries }
110
136
  @pending_entries += entries.length
111
137
  @pending_bytes += size
112
- return unless @pending_entries > @max_pending_entries || @pending_bytes > @max_pending_bytes
138
+ return unless over_threshold?
113
139
 
114
140
  task = Async::Task.current?
115
- # Fire-and-forget on the reactor; @lock in #drain serializes against any
116
- # in-flight flush so append ordering holds. #drain never raises.
117
- task ? task.async { drain } : drain
141
+ task ? schedule_background_drain(task) : drain
118
142
  end
119
143
 
120
144
  # Flush all pending entries, serialized after any in-flight eager flush.
@@ -122,8 +146,11 @@ module ClaudeAgentSDK
122
146
  drain
123
147
  end
124
148
 
125
- # Final flush before teardown. Never raises.
149
+ # Final flush before teardown. Never raises. Bounded: it waits behind at
150
+ # most the in-flight append, and frames arriving after it detached are
151
+ # not chased (they count as dropped unless delivered; #batches_dropped?).
126
152
  def close
153
+ @closed = true
127
154
  flush
128
155
  rescue StandardError => e
129
156
  warn "Claude SDK: TranscriptMirrorBatcher close flush failed: #{e.message}"
@@ -131,6 +158,38 @@ module ClaudeAgentSDK
131
158
 
132
159
  private
133
160
 
161
+ def over_threshold?
162
+ @pending_entries > @max_pending_entries || @pending_bytes > @max_pending_bytes
163
+ end
164
+
165
+ # Fire-and-forget on the reactor. The drainer loops until the buffer is
166
+ # back under the thresholds, so while it is live every later frame is
167
+ # simply buffered and coalesced into its next #drain — live background
168
+ # tasks <= 1 and detached batches <= 1 + one per #flush / #close caller
169
+ # parked on @lock, independent of frame rate and store latency.
170
+ #
171
+ # No lost wakeup: the loop's final over_threshold? check and the ensure
172
+ # clearing the flag run with no suspension point between them, so an
173
+ # #enqueue that saw the flag set is always observed by that check.
174
+ # Every drainer (this one, #flush, #close) keeps detach-then-acquire, so
175
+ # detach order == @lock FIFO order and append ordering holds. #drain
176
+ # never raises.
177
+ def schedule_background_drain(task)
178
+ return if @background_drain_live
179
+
180
+ @background_drain_live = true
181
+ task.async do
182
+ drain while over_threshold?
183
+ ensure
184
+ @background_drain_live = false
185
+ end
186
+ rescue StandardError
187
+ # Task#async raises synchronously (e.g. FinishedError) before the block
188
+ # runs; don't let a stuck flag disable background drains for good.
189
+ @background_drain_live = false
190
+ raise
191
+ end
192
+
134
193
  # Detach the pending buffer, await any prior flush, then send. Detaching
135
194
  # before acquiring the lock lets #enqueue keep accumulating into a fresh
136
195
  # buffer while a prior flush is in flight. Never raises.