@tanstack/ai 0.57.0 → 0.58.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.
@@ -9,6 +9,10 @@ import { notifyRunDisconnected } from './delivery-disconnect'
9
9
  import { resolveResumeRunId } from './stream-durability'
10
10
  import { EventType } from './types'
11
11
  import { toWireChunk } from './strip-to-spec-middleware'
12
+ import {
13
+ isDurabilityBatchedCustom,
14
+ stripDurabilityBatchHint,
15
+ } from './utilities/durability-batch'
12
16
  import { resolveDebugOption } from './logger/resolve'
13
17
  import { runErrorEventToError } from './utilities/errors'
14
18
  import type { LockStore } from './activities/chat/middleware/locks'
@@ -319,23 +323,29 @@ function resolveBatchSize(batch: number | undefined): number {
319
323
 
320
324
  /**
321
325
  * Boundaries at which the batching producer flushes early, regardless of the
322
- * batch size — the run-start marker, terminal events, and tool-call ends.
323
- * Flushing here keeps the durability log promptly consistent at semantically
324
- * meaningful points.
326
+ * batch size: run-start, terminals, tool-call ends, and CUSTOM events that
327
+ * are not high-volume adapter output.
325
328
  *
326
329
  * `RUN_STARTED` matters especially for one-shot activities (image, speech,
327
330
  * transcription, summarize): they emit `RUN_STARTED`, then await the provider
328
331
  * for seconds, then a terminal. Without flushing `RUN_STARTED` the log stays
329
332
  * empty for the whole run, so a mount-time `joinRun` finds nothing and its
330
- * empty-log deadline fast-fails as "run gone" — even though the run is alive.
333
+ * empty-log deadline fast-fails as "run gone" even though the run is alive.
331
334
  * Flushing it immediately makes the run resumable from the instant it starts.
335
+ *
336
+ * CUSTOM progress events (compaction, tool progress, middleware) flush at
337
+ * emit time so a live indicator can render. `process.stdout`,
338
+ * `process.stderr`, `sandbox.file`, and `sandbox.file.diff` stay batched.
339
+ * `emitCustomEvent(name, value, { batch: true })` opts a single event into
340
+ * that same batch.
332
341
  */
333
342
  function isDurabilityFlushBoundary(chunk: StreamChunk): boolean {
334
343
  return (
335
344
  chunk.type === 'RUN_STARTED' ||
336
345
  chunk.type === 'RUN_FINISHED' ||
337
346
  chunk.type === 'RUN_ERROR' ||
338
- chunk.type === 'TOOL_CALL_END'
347
+ chunk.type === 'TOOL_CALL_END' ||
348
+ (chunk.type === 'CUSTOM' && !isDurabilityBatchedCustom(chunk))
339
349
  )
340
350
  }
341
351
 
@@ -446,7 +456,7 @@ export function durableStreamSource<TOffset extends string>(
446
456
 
447
457
  async function* flush(): AsyncIterable<StreamChunk> {
448
458
  if (batch.length === 0) return
449
- const toForward = batch
459
+ const toForward = batch.map(stripDurabilityBatchHint)
450
460
  batch = []
451
461
  // Tag each chunk with the exact backend offset. Requiring one opaque
452
462
  // token per chunk preserves exact-once resume at any batch size.
@@ -6,6 +6,7 @@ import {
6
6
  tanstackMetadata,
7
7
  withTanstackMetadata,
8
8
  } from './utilities/merge-metadata'
9
+ import { stripDurabilityBatchHint } from './utilities/durability-batch'
9
10
  import { normalizeStreamChunk } from './utilities/normalize-stream-chunk'
10
11
  import { isSpecTopLevelKey } from './utilities/spec-event-keys'
11
12
 
@@ -54,5 +55,5 @@ export function toWireChunk(
54
55
  chunk: StreamChunk | AdapterYieldChunk,
55
56
  ): StreamChunk {
56
57
  const [normalized] = normalizeStreamChunk(chunk)
57
- return stripToSpec(normalized ?? chunk)
58
+ return stripDurabilityBatchHint(stripToSpec(normalized ?? chunk))
58
59
  }
package/src/types.ts CHANGED
@@ -629,6 +629,22 @@ type RuntimeContextField<TContext> =
629
629
  context: TContext
630
630
  }
631
631
 
632
+ /**
633
+ * Options for a single `emitCustomEvent` call, on both the tool-execution and
634
+ * middleware contexts.
635
+ */
636
+ export interface EmitCustomEventOptions {
637
+ /**
638
+ * Keep this event in the durability batch with later chunks.
639
+ * CUSTOM events flush as soon as they are emitted, so a progress
640
+ * indicator can render at emit time. Pass `{ batch: true }` for a
641
+ * high-volume stream that should share appends with later output.
642
+ * `process.stdout`, `process.stderr`, `sandbox.file`, and
643
+ * `sandbox.file.diff` already batch.
644
+ */
645
+ batch?: boolean
646
+ }
647
+
632
648
  /**
633
649
  * Context passed to tool execute functions, providing capabilities like
634
650
  * emitting custom events during execution.
@@ -649,6 +665,8 @@ export type ToolExecutionContext<TContext = unknown> =
649
665
  *
650
666
  * @param eventName - Name of the custom event
651
667
  * @param value - Event payload value
668
+ * @param options - Pass `{ batch: true }` to keep this event in the
669
+ * durability batch instead of flushing it immediately
652
670
  *
653
671
  * @example
654
672
  * ```ts
@@ -661,7 +679,11 @@ export type ToolExecutionContext<TContext = unknown> =
661
679
  * })
662
680
  * ```
663
681
  */
664
- emitCustomEvent: (eventName: string, value: Record<string, any>) => void
682
+ emitCustomEvent: (
683
+ eventName: string,
684
+ value: Record<string, any>,
685
+ options?: EmitCustomEventOptions,
686
+ ) => void
665
687
  }
666
688
 
667
689
  export type ToolExecuteFunction<
@@ -0,0 +1,48 @@
1
+ import { CUSTOM_EVENT } from '../custom-events'
2
+ import type { CustomEvent, StreamChunk } from '../types'
3
+ import { tanstackMetadata, withTanstackMetadata } from './merge-metadata'
4
+
5
+ /**
6
+ * High-volume CUSTOM names that stay in the durability batch.
7
+ * Everything else flushes as soon as it is emitted.
8
+ */
9
+ const BATCHED_CUSTOM_EVENT_NAMES = new Set<string>([
10
+ CUSTOM_EVENT.PROCESS_STDOUT,
11
+ CUSTOM_EVENT.PROCESS_STDERR,
12
+ 'sandbox.file',
13
+ 'sandbox.file.diff',
14
+ ])
15
+
16
+ function hasBatchHint(chunk: StreamChunk): boolean {
17
+ const tanstack = tanstackMetadata(chunk)
18
+ if (tanstack == null) return false
19
+ return Reflect.get(tanstack, 'batch') === true
20
+ }
21
+
22
+ /** Mark a CUSTOM chunk so the durability producer keeps it in the batch. */
23
+ export function withDurabilityBatchHint(chunk: CustomEvent): CustomEvent {
24
+ return withTanstackMetadata(chunk, { batch: true })
25
+ }
26
+
27
+ export function isDurabilityBatchedCustom(chunk: StreamChunk): boolean {
28
+ if (chunk.type !== 'CUSTOM') return false
29
+ if (BATCHED_CUSTOM_EVENT_NAMES.has(chunk.name)) return true
30
+ return hasBatchHint(chunk)
31
+ }
32
+
33
+ /**
34
+ * Drop the in-process batch hint so it does not sit in the log or on the wire.
35
+ */
36
+ export function stripDurabilityBatchHint(chunk: StreamChunk): StreamChunk {
37
+ const tanstack = tanstackMetadata(chunk)
38
+ if (tanstack == null || Reflect.get(tanstack, 'batch') !== true) return chunk
39
+ Reflect.deleteProperty(tanstack, 'batch')
40
+ const metadata = chunk.metadata
41
+ if (metadata != null && Object.keys(tanstack).length === 0) {
42
+ Reflect.deleteProperty(metadata, 'tanstack')
43
+ if (Object.keys(metadata).length === 0) {
44
+ Reflect.deleteProperty(chunk, 'metadata')
45
+ }
46
+ }
47
+ return chunk
48
+ }