@tanstack/ai 0.57.0 → 0.59.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 (95) hide show
  1. package/dist/esm/activities/chat/agents/define-agent.d.ts +81 -0
  2. package/dist/esm/activities/chat/agents/define-agent.js +34 -0
  3. package/dist/esm/activities/chat/agents/define-agent.js.map +1 -0
  4. package/dist/esm/activities/chat/agents/route.d.ts +53 -0
  5. package/dist/esm/activities/chat/agents/route.js +59 -0
  6. package/dist/esm/activities/chat/agents/route.js.map +1 -0
  7. package/dist/esm/activities/chat/agents/spawn.d.ts +124 -0
  8. package/dist/esm/activities/chat/agents/spawn.js +490 -0
  9. package/dist/esm/activities/chat/agents/spawn.js.map +1 -0
  10. package/dist/esm/activities/chat/agents/turn.d.ts +36 -0
  11. package/dist/esm/activities/chat/agents/turn.js +78 -0
  12. package/dist/esm/activities/chat/agents/turn.js.map +1 -0
  13. package/dist/esm/activities/chat/index.d.ts +13 -3
  14. package/dist/esm/activities/chat/index.js +345 -19
  15. package/dist/esm/activities/chat/index.js.map +1 -1
  16. package/dist/esm/activities/chat/messages.d.ts +7 -1
  17. package/dist/esm/activities/chat/messages.js +94 -18
  18. package/dist/esm/activities/chat/messages.js.map +1 -1
  19. package/dist/esm/activities/chat/middleware/run-store.d.ts +43 -7
  20. package/dist/esm/activities/chat/middleware/run-store.js +8 -1
  21. package/dist/esm/activities/chat/middleware/run-store.js.map +1 -1
  22. package/dist/esm/activities/chat/middleware/types.d.ts +50 -3
  23. package/dist/esm/activities/chat/middleware/types.js.map +1 -1
  24. package/dist/esm/activities/chat/stream/processor.d.ts +46 -0
  25. package/dist/esm/activities/chat/stream/processor.js +280 -7
  26. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  27. package/dist/esm/activities/chat/tools/tool-calls.d.ts +17 -3
  28. package/dist/esm/activities/chat/tools/tool-calls.js +56 -7
  29. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  30. package/dist/esm/activities/generateAudio/index.js +1 -1
  31. package/dist/esm/activities/generateImage/index.js +1 -1
  32. package/dist/esm/activities/generateLiveVideo/index.js +1 -1
  33. package/dist/esm/activities/generateSpeech/index.js +1 -1
  34. package/dist/esm/activities/generateTranscription/index.js +1 -1
  35. package/dist/esm/activities/generateVoice/index.js +1 -1
  36. package/dist/esm/activities/generateWorld/index.js +1 -1
  37. package/dist/esm/activities/index.d.ts +3 -0
  38. package/dist/esm/activities/index.js +5 -3
  39. package/dist/esm/activities/summarize/index.js +1 -1
  40. package/dist/esm/client.d.ts +5 -36
  41. package/dist/esm/client.js +4 -37
  42. package/dist/esm/client.js.map +1 -1
  43. package/dist/esm/index.d.ts +5 -0
  44. package/dist/esm/index.js +7 -4
  45. package/dist/esm/middlewares/content-guard.js.map +1 -1
  46. package/dist/esm/stream-to-response.js +12 -6
  47. package/dist/esm/stream-to-response.js.map +1 -1
  48. package/dist/esm/strip-to-spec-middleware.js +2 -1
  49. package/dist/esm/strip-to-spec-middleware.js.map +1 -1
  50. package/dist/esm/types.d.ts +128 -89
  51. package/dist/esm/utilities/adapter-yield-chunk.d.ts +5 -1
  52. package/dist/esm/utilities/ag-ui-usage.d.ts +9 -9
  53. package/dist/esm/utilities/ag-ui-usage.js +66 -3
  54. package/dist/esm/utilities/ag-ui-usage.js.map +1 -1
  55. package/dist/esm/utilities/ag-ui-wire.js +56 -0
  56. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  57. package/dist/esm/utilities/durability-batch.d.ts +8 -0
  58. package/dist/esm/utilities/durability-batch.js +45 -0
  59. package/dist/esm/utilities/durability-batch.js.map +1 -0
  60. package/dist/esm/utilities/normalize-stream-chunk.js +7 -2
  61. package/dist/esm/utilities/normalize-stream-chunk.js.map +1 -1
  62. package/dist/esm/utilities/spec-event-keys.js +13 -8
  63. package/dist/esm/utilities/spec-event-keys.js.map +1 -1
  64. package/dist/esm/utilities/subagent-wire.d.ts +36 -0
  65. package/dist/esm/utilities/subagent-wire.js +131 -0
  66. package/dist/esm/utilities/subagent-wire.js.map +1 -0
  67. package/package.json +4 -4
  68. package/skills/ai-core/adapter-configuration/references/grok-adapter.md +1 -1
  69. package/skills/ai-core/media-generation/SKILL.md +2 -2
  70. package/skills/ai-core/middleware/SKILL.md +7 -4
  71. package/src/activities/chat/agents/define-agent.ts +121 -0
  72. package/src/activities/chat/agents/route.ts +115 -0
  73. package/src/activities/chat/agents/spawn.ts +806 -0
  74. package/src/activities/chat/agents/turn.ts +151 -0
  75. package/src/activities/chat/index.ts +540 -16
  76. package/src/activities/chat/messages.ts +125 -15
  77. package/src/activities/chat/middleware/run-store.ts +56 -7
  78. package/src/activities/chat/middleware/types.ts +55 -2
  79. package/src/activities/chat/stream/processor.ts +428 -8
  80. package/src/activities/chat/tools/tool-calls.ts +92 -16
  81. package/src/activities/index.ts +15 -0
  82. package/src/client.ts +22 -35
  83. package/src/index.ts +23 -0
  84. package/src/middlewares/content-guard.ts +7 -5
  85. package/src/stream-to-response.ts +16 -6
  86. package/src/strip-to-spec-middleware.ts +2 -1
  87. package/src/types.ts +177 -95
  88. package/src/utilities/adapter-yield-chunk.ts +10 -2
  89. package/src/utilities/ag-ui-usage.test.ts +38 -0
  90. package/src/utilities/ag-ui-usage.ts +98 -11
  91. package/src/utilities/ag-ui-wire.ts +74 -0
  92. package/src/utilities/durability-batch.ts +48 -0
  93. package/src/utilities/normalize-stream-chunk.ts +10 -2
  94. package/src/utilities/spec-event-keys.ts +34 -7
  95. package/src/utilities/subagent-wire.ts +184 -0
@@ -7,8 +7,11 @@ import type {
7
7
  AnyTool,
8
8
  ContentPart,
9
9
  CustomEvent,
10
+ EmitCustomEventOptions,
11
+ Interrupt,
10
12
  ModelMessage,
11
13
  RunFinishedEvent,
14
+ StreamChunk,
12
15
  Tool,
13
16
  ToolCall,
14
17
  ToolCallArgsEvent,
@@ -37,6 +40,25 @@ function safeJsonParse(value: string): unknown {
37
40
  }
38
41
  }
39
42
 
43
+ /** Marks the synthetic tool that runs a subagent. */
44
+ export const SUBAGENT_TOOL = Symbol.for('tanstack.ai.subagentTool')
45
+
46
+ /** Set on the tool context of a subagent tool. Streams a child chunk live. */
47
+ export const EMIT_STREAM_CHUNK = Symbol.for('tanstack.ai.emitStreamChunk')
48
+
49
+ /** What a subagent tool's `execute` returns. */
50
+ export interface SubagentToolOutcome {
51
+ subagentRunId: string
52
+ text: string
53
+ error?: string
54
+ /** Set when the child stopped for outside input. The tool call stays open. */
55
+ interrupts?: Array<Interrupt>
56
+ }
57
+
58
+ function isSubagentTool(tool: AnyTool): boolean {
59
+ return (tool as { [SUBAGENT_TOOL]?: true })[SUBAGENT_TOOL] === true
60
+ }
61
+
40
62
  /**
41
63
  * MCP Apps metadata attached to a server tool at discovery (see
42
64
  * `@tanstack/ai-mcp` discovery + `MCPManager.discover()`).
@@ -504,6 +526,8 @@ interface ExecuteToolCallsResult {
504
526
  needsApproval: Array<ApprovalRequest>
505
527
  /** Tools that need client-side execution */
506
528
  needsClientExecution: Array<ClientToolRequest>
529
+ /** Interrupts raised by subagents that run as tools */
530
+ subagentInterrupts: Array<Interrupt>
507
531
  }
508
532
 
509
533
  /**
@@ -513,8 +537,8 @@ interface ExecuteToolCallsResult {
513
537
  */
514
538
  async function* executeWithEventPolling<T>(
515
539
  executionPromise: Promise<T>,
516
- pendingEvents: Array<CustomEvent>,
517
- ): AsyncGenerator<CustomEvent, T, void> {
540
+ pendingEvents: Array<CustomEvent | StreamChunk>,
541
+ ): AsyncGenerator<CustomEvent | StreamChunk, T, void> {
518
542
  // Use an object to track mutable state across the async boundary
519
543
  const state = { done: false, result: undefined as T }
520
544
  const executionWithFlag = executionPromise.then((r) => {
@@ -531,14 +555,14 @@ async function* executeWithEventPolling<T>(
531
555
  ])
532
556
 
533
557
  // Flush any pending events
534
- let event: CustomEvent | undefined
558
+ let event: CustomEvent | StreamChunk | undefined
535
559
  while ((event = pendingEvents.shift()) !== undefined) {
536
560
  yield event
537
561
  }
538
562
  }
539
563
 
540
564
  // Final flush in case events were emitted right at completion
541
- let event: CustomEvent | undefined
565
+ let event: CustomEvent | StreamChunk | undefined
542
566
  while ((event = pendingEvents.shift()) !== undefined) {
543
567
  yield event
544
568
  }
@@ -611,19 +635,59 @@ export async function* executeServerTool<TContext = unknown>(
611
635
  toolName: string,
612
636
  input: unknown,
613
637
  context: ToolExecutionContext<TContext>,
614
- pendingEvents: Array<CustomEvent>,
638
+ pendingEvents: Array<CustomEvent | StreamChunk>,
615
639
  results: Array<ToolResult>,
616
640
  middlewareHooks?: ToolExecutionMiddlewareHooks,
617
- ): AsyncGenerator<CustomEvent, void, void> {
641
+ subagentInterrupts?: Array<Interrupt>,
642
+ ): AsyncGenerator<CustomEvent | StreamChunk, void, void> {
618
643
  const startTime = Date.now()
619
644
  try {
620
645
  if (!tool.execute) {
621
646
  throw new Error(`Tool ${toolName} has no execute() implementation`)
622
647
  }
648
+ const subagent = isSubagentTool(tool)
649
+ if (subagent) {
650
+ Object.assign(context, {
651
+ [EMIT_STREAM_CHUNK]: (chunk: StreamChunk) => pendingEvents.push(chunk),
652
+ })
653
+ }
623
654
  const executionPromise = Promise.resolve(tool.execute(input, context))
624
655
  let result = yield* executeWithEventPolling(executionPromise, pendingEvents)
625
656
  const duration = Date.now() - startTime
626
657
 
658
+ if (subagent) {
659
+ const outcome = result as SubagentToolOutcome
660
+ if (outcome.interrupts?.length) {
661
+ // The child waits for outside input. Leave the call open so the
662
+ // resume run executes it again and continues the same child.
663
+ subagentInterrupts?.push(...outcome.interrupts)
664
+ return
665
+ }
666
+ const modelResult = outcome.error
667
+ ? { subagentRunId: outcome.subagentRunId, error: outcome.error }
668
+ : { subagentRunId: outcome.subagentRunId, result: outcome.text }
669
+ results.push({
670
+ toolCallId: toolCall.id,
671
+ toolName,
672
+ result: modelResult,
673
+ input,
674
+ output: modelResult,
675
+ duration,
676
+ ...(outcome.error ? { state: 'output-error' as const } : {}),
677
+ })
678
+ await middlewareHooks?.onAfterToolCall?.({
679
+ toolCall,
680
+ tool,
681
+ toolName,
682
+ toolCallId: toolCall.id,
683
+ duration,
684
+ ...(outcome.error
685
+ ? { ok: false as const, error: new Error(outcome.error) }
686
+ : { ok: true as const, result: modelResult }),
687
+ })
688
+ return
689
+ }
690
+
627
691
  // MCP Apps: if this tool links a ui:// resource, eagerly read it and queue
628
692
  // a `ui-resource` CUSTOM event. The MCP source stays live until the run
629
693
  // drains (MCPManager's `connection:'close'` policy disposes on completion),
@@ -632,7 +696,7 @@ export async function* executeServerTool<TContext = unknown>(
632
696
  await emitUiResourceIfLinked(tool, context)
633
697
 
634
698
  // Flush remaining events (including any queued ui-resource event)
635
- let pendingEvent: CustomEvent | undefined
699
+ let pendingEvent: CustomEvent | StreamChunk | undefined
636
700
  while ((pendingEvent = pendingEvents.shift()) !== undefined) {
637
701
  yield pendingEvent
638
702
  }
@@ -670,7 +734,7 @@ export async function* executeServerTool<TContext = unknown>(
670
734
  const duration = Date.now() - startTime
671
735
 
672
736
  // Flush remaining events
673
- let pendingEvent: CustomEvent | undefined
737
+ let pendingEvent: CustomEvent | StreamChunk | undefined
674
738
  while ((pendingEvent = pendingEvents.shift()) !== undefined) {
675
739
  yield pendingEvent
676
740
  }
@@ -760,15 +824,17 @@ export async function* executeToolCalls<TContext = unknown>(
760
824
  createCustomEventChunk?: (
761
825
  eventName: string,
762
826
  value: Record<string, any>,
827
+ options?: EmitCustomEventOptions,
763
828
  ) => CustomEvent,
764
829
  middlewareHooks?: ToolExecutionMiddlewareHooks,
765
830
  userContext?: TContext,
766
831
  abortSignal?: AbortSignal,
767
832
  resumeState?: ToolResumeExecutionState,
768
- ): AsyncGenerator<CustomEvent, ExecuteToolCallsResult, void> {
833
+ ): AsyncGenerator<CustomEvent | StreamChunk, ExecuteToolCallsResult, void> {
769
834
  const results: Array<ToolResult> = []
770
835
  const needsApproval: Array<ApprovalRequest> = []
771
836
  const needsClientExecution: Array<ClientToolRequest> = []
837
+ const subagentInterrupts: Array<Interrupt> = []
772
838
 
773
839
  // Create tool lookup map
774
840
  const toolMap = new Map<string, AnyTool>()
@@ -869,18 +935,26 @@ export async function* executeToolCalls<TContext = unknown>(
869
935
  }
870
936
 
871
937
  // Create a ToolExecutionContext for this tool call with event emission
872
- const pendingEvents: Array<CustomEvent> = []
938
+ const pendingEvents: Array<CustomEvent | StreamChunk> = []
873
939
  const context = {
874
940
  toolCallId: toolCall.id,
875
941
  context: userContext,
876
942
  abortSignal,
877
- emitCustomEvent: (eventName: string, value: Record<string, any>) => {
943
+ emitCustomEvent: (
944
+ eventName: string,
945
+ value: Record<string, any>,
946
+ options?: EmitCustomEventOptions,
947
+ ) => {
878
948
  if (createCustomEventChunk) {
879
949
  pendingEvents.push(
880
- createCustomEventChunk(eventName, {
881
- ...value,
882
- toolCallId: toolCall.id,
883
- }),
950
+ createCustomEventChunk(
951
+ eventName,
952
+ {
953
+ ...value,
954
+ toolCallId: toolCall.id,
955
+ },
956
+ options,
957
+ ),
884
958
  )
885
959
  }
886
960
  },
@@ -997,6 +1071,7 @@ export async function* executeToolCalls<TContext = unknown>(
997
1071
  pendingEvents,
998
1072
  results,
999
1073
  middlewareHooks,
1074
+ subagentInterrupts,
1000
1075
  )
1001
1076
  } else {
1002
1077
  // User declined
@@ -1045,8 +1120,9 @@ export async function* executeToolCalls<TContext = unknown>(
1045
1120
  pendingEvents,
1046
1121
  results,
1047
1122
  middlewareHooks,
1123
+ subagentInterrupts,
1048
1124
  )
1049
1125
  }
1050
1126
 
1051
- return { results, needsApproval, needsClientExecution }
1127
+ return { results, needsApproval, needsClientExecution, subagentInterrupts }
1052
1128
  }
@@ -39,6 +39,21 @@ export {
39
39
  type TextActivityResult,
40
40
  } from './chat/index'
41
41
 
42
+ export {
43
+ defineAgent,
44
+ type DefinedAgent,
45
+ type SubagentChoiceOptions,
46
+ type SubagentRunContext,
47
+ } from './chat/agents/define-agent'
48
+ export { subagentRoute, type SubagentRouteOptions } from './chat/agents/route'
49
+ export type {
50
+ SubagentOrder,
51
+ SubagentRouterPick,
52
+ SubagentRouterPlan,
53
+ SubagentStep,
54
+ SubagentStepsPlan,
55
+ } from './chat/agents/spawn'
56
+
42
57
  export {
43
58
  BaseTextAdapter,
44
59
  type AnyTextAdapter,
package/src/client.ts CHANGED
@@ -224,41 +224,25 @@ export async function generationParamsFromRequest<TKind extends GenerationKind>(
224
224
  return generationParamsFromBody(kind, body)
225
225
  }
226
226
 
227
- export enum EventType {
228
- TEXT_MESSAGE_START = 'TEXT_MESSAGE_START',
229
- TEXT_MESSAGE_CONTENT = 'TEXT_MESSAGE_CONTENT',
230
- TEXT_MESSAGE_END = 'TEXT_MESSAGE_END',
231
- TEXT_MESSAGE_CHUNK = 'TEXT_MESSAGE_CHUNK',
232
- TOOL_CALL_START = 'TOOL_CALL_START',
233
- TOOL_CALL_ARGS = 'TOOL_CALL_ARGS',
234
- TOOL_CALL_END = 'TOOL_CALL_END',
235
- TOOL_CALL_CHUNK = 'TOOL_CALL_CHUNK',
236
- TOOL_CALL_RESULT = 'TOOL_CALL_RESULT',
237
- THINKING_START = 'THINKING_START',
238
- THINKING_END = 'THINKING_END',
239
- THINKING_TEXT_MESSAGE_START = 'THINKING_TEXT_MESSAGE_START',
240
- THINKING_TEXT_MESSAGE_CONTENT = 'THINKING_TEXT_MESSAGE_CONTENT',
241
- THINKING_TEXT_MESSAGE_END = 'THINKING_TEXT_MESSAGE_END',
242
- STATE_SNAPSHOT = 'STATE_SNAPSHOT',
243
- STATE_DELTA = 'STATE_DELTA',
244
- MESSAGES_SNAPSHOT = 'MESSAGES_SNAPSHOT',
245
- ACTIVITY_SNAPSHOT = 'ACTIVITY_SNAPSHOT',
246
- ACTIVITY_DELTA = 'ACTIVITY_DELTA',
247
- RAW = 'RAW',
248
- CUSTOM = 'CUSTOM',
249
- RUN_STARTED = 'RUN_STARTED',
250
- RUN_FINISHED = 'RUN_FINISHED',
251
- RUN_ERROR = 'RUN_ERROR',
252
- STEP_STARTED = 'STEP_STARTED',
253
- STEP_FINISHED = 'STEP_FINISHED',
254
- REASONING_START = 'REASONING_START',
255
- REASONING_MESSAGE_START = 'REASONING_MESSAGE_START',
256
- REASONING_MESSAGE_CONTENT = 'REASONING_MESSAGE_CONTENT',
257
- REASONING_MESSAGE_END = 'REASONING_MESSAGE_END',
258
- REASONING_MESSAGE_CHUNK = 'REASONING_MESSAGE_CHUNK',
259
- REASONING_END = 'REASONING_END',
260
- REASONING_ENCRYPTED_VALUE = 'REASONING_ENCRYPTED_VALUE',
261
- }
227
+ export { EventType } from '@ag-ui/core'
228
+
229
+ export {
230
+ defineAgent,
231
+ type DefinedAgent,
232
+ type SubagentChoiceOptions,
233
+ type SubagentRunContext,
234
+ } from './activities/chat/agents/define-agent'
235
+ export {
236
+ subagentRoute,
237
+ type SubagentRouteOptions,
238
+ } from './activities/chat/agents/route'
239
+ export type {
240
+ SubagentOrder,
241
+ SubagentRouterPick,
242
+ SubagentRouterPlan,
243
+ SubagentStep,
244
+ SubagentStepsPlan,
245
+ } from './activities/chat/agents/spawn'
262
246
 
263
247
  export {
264
248
  toolDefinition,
@@ -349,6 +333,9 @@ export type {
349
333
  AudioPart,
350
334
  ContentPart,
351
335
  ContentPartDataSource,
336
+ ContentPartFileSource,
337
+ SubagentHandleData,
338
+ SubagentStatus,
352
339
  ContentPartSource,
353
340
  ContentPartUrlSource,
354
341
  CustomEvent,
package/src/index.ts CHANGED
@@ -78,6 +78,24 @@ export { BaseRerankAdapter } from './activities/rerank/adapter'
78
78
  // Evaluate adapter base + types
79
79
  export { BaseEvaluateAdapter } from './activities/evaluate/adapter'
80
80
 
81
+ export {
82
+ defineAgent,
83
+ type DefinedAgent,
84
+ type SubagentChoiceOptions,
85
+ type SubagentRunContext,
86
+ } from './activities/chat/agents/define-agent'
87
+ export {
88
+ subagentRoute,
89
+ type SubagentRouteOptions,
90
+ } from './activities/chat/agents/route'
91
+ export type {
92
+ SubagentOrder,
93
+ SubagentRouterPick,
94
+ SubagentRouterPlan,
95
+ SubagentStep,
96
+ SubagentStepsPlan,
97
+ } from './activities/chat/agents/spawn'
98
+
81
99
  // Tool definition
82
100
  export {
83
101
  toolDefinition,
@@ -494,6 +512,11 @@ export { generationParamsFromBody, generationParamsFromRequest } from './client'
494
512
 
495
513
  // AG-UI wire serialization (used internally by @tanstack/ai-client)
496
514
  export { uiMessagesToWire } from './utilities/ag-ui-wire'
515
+ export type { SubagentWireInfo } from './utilities/subagent-wire'
516
+ export {
517
+ subagentHostMessageId,
518
+ wireSubagentInfo,
519
+ } from './utilities/subagent-wire'
497
520
  export { mergeMetadata, withTanstackMetadata } from './utilities/merge-metadata'
498
521
  export { fromSpecTokenUsage, toSpecTokenUsage } from './utilities/ag-ui-usage'
499
522
  export type { SpecTokenUsage } from './utilities/ag-ui-usage'
@@ -3,6 +3,7 @@ import type {
3
3
  ChatMiddlewareContext,
4
4
  } from '../activities/chat/middleware/types'
5
5
  import type { StreamChunk } from '../types'
6
+ import type { AdapterYieldChunk } from '../utilities/adapter-yield-chunk'
6
7
 
7
8
  /**
8
9
  * A content guard rule — either a regex pattern with replacement, or a transform function.
@@ -142,11 +143,12 @@ function createDeltaStrategy(
142
143
 
143
144
  if (blockOnMatch) return null // drop chunk
144
145
 
145
- // Strip out the previous `content` field by destructuring it away — with
146
- // `exactOptionalPropertyTypes` we can't assign `content: undefined`
147
- // against `content?: string`. The replacement event carries only the
148
- // filtered delta.
149
- const { content: _strippedContent, ...rest } = chunk
146
+ // Drop the adapter-era content field; the replacement event carries only
147
+ // the filtered delta.
148
+ const { content: _strippedContent, ...rest } = chunk as Extract<
149
+ AdapterYieldChunk,
150
+ { type: 'TEXT_MESSAGE_CONTENT' }
151
+ >
150
152
  void _strippedContent
151
153
  return {
152
154
  ...rest,
@@ -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
  }