@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
@@ -4,6 +4,10 @@ import {
4
4
  normalizeToolResult,
5
5
  } from '../../utilities/tool-result'
6
6
  import { tanstackMetadata } from '../../utilities/merge-metadata'
7
+ import {
8
+ splitSubagentWire,
9
+ subagentWireText,
10
+ } from '../../utilities/subagent-wire'
7
11
  import type { Message as AGUIMessage } from '@ag-ui/core'
8
12
  import type {
9
13
  ContentPart,
@@ -14,6 +18,7 @@ import type {
14
18
  TextPart,
15
19
  ToolCall,
16
20
  ToolCallPart,
21
+ SubagentPart,
17
22
  UIMessage,
18
23
  UIResourcePart,
19
24
  } from '../../types'
@@ -124,6 +129,37 @@ function getTextContent(
124
129
  .join('')
125
130
  }
126
131
 
132
+ function historyTextFromParts(parts: ReadonlyArray<MessagePart>): string {
133
+ const blocks: Array<string> = []
134
+ for (const part of parts) {
135
+ if (part.type === 'text' && part.content !== '') {
136
+ blocks.push(part.content)
137
+ } else if (
138
+ part.type === 'structured-output' &&
139
+ part.status === 'complete' &&
140
+ part.raw !== ''
141
+ ) {
142
+ blocks.push(part.raw)
143
+ } else if (part.type === 'subagent') {
144
+ const nested = subagentHistoryText(part)
145
+ if (nested !== '') blocks.push(nested)
146
+ }
147
+ }
148
+ return blocks.join('\n\n')
149
+ }
150
+
151
+ /** Child text for a later turn. The name stays so the next agent can tell the notes apart. */
152
+ export function subagentHistoryText(part: SubagentPart): string {
153
+ const blocks: Array<string> = []
154
+ for (const message of part.subagent.messages) {
155
+ if (!('parts' in message)) continue
156
+ const text = historyTextFromParts(message.parts).trim()
157
+ if (text !== '') blocks.push(text)
158
+ }
159
+ if (blocks.length === 0) return ''
160
+ return `${part.subagent.name}:\n${blocks.join('\n\n')}`
161
+ }
162
+
127
163
  function toolResultContent(
128
164
  content: string | null | undefined | Array<ContentPart>,
129
165
  ): string | Array<ContentPart> {
@@ -135,6 +171,57 @@ function toolResultContent(
135
171
  */
136
172
  export function convertMessagesToModelMessages(
137
173
  messages: Array<UIMessage | ModelMessage>,
174
+ ): Array<ModelMessage> {
175
+ const { top, groups } = splitSubagentWire(messages)
176
+ if (groups.length === 0) return convertOwnMessages(messages)
177
+
178
+ // Child wire messages leave the parent history. The parent model reads each
179
+ // child's text on the assistant message before it, the same as for a UI
180
+ // subagent part. A child that a tool call started reports through the tool
181
+ // result instead.
182
+ const blocks = new Map<string | undefined, Array<string>>()
183
+ for (const group of groups) {
184
+ if (group.info.parentToolCallId !== undefined) continue
185
+ const text = subagentWireText(group.messages)
186
+ if (text === '') continue
187
+ const host = top
188
+ .slice(0, group.hostIndex + 1)
189
+ .findLast((message) => message.role === 'assistant')
190
+ const hostId = host && 'id' in host ? host.id : undefined
191
+ blocks.set(hostId, [
192
+ ...(blocks.get(hostId) ?? []),
193
+ `${group.info.name}:\n${text}`,
194
+ ])
195
+ }
196
+ const converted = convertOwnMessages(top)
197
+ for (const [hostId, texts] of blocks) {
198
+ const block = texts.join('\n\n')
199
+ const index =
200
+ hostId === undefined
201
+ ? -1
202
+ : converted.findIndex(
203
+ (message) => message.role === 'assistant' && message.id === hostId,
204
+ )
205
+ const host = converted[index]
206
+ if (!host) {
207
+ converted.push({ role: 'assistant', content: block })
208
+ continue
209
+ }
210
+ converted[index] = {
211
+ ...host,
212
+ content:
213
+ typeof host.content === 'string' && host.content !== ''
214
+ ? `${host.content}\n\n${block}`
215
+ : Array.isArray(host.content)
216
+ ? [...host.content, { type: 'text', content: block }]
217
+ : block,
218
+ }
219
+ }
220
+ return converted
221
+ }
222
+
223
+ function convertOwnMessages(
224
+ messages: Array<UIMessage | ModelMessage>,
138
225
  ): Array<ModelMessage> {
139
226
  // Pre-pass: collect toolCallIds already represented in anchor UIMessage parts.
140
227
  // Fan-out tool messages whose toolCallId matches an anchored ToolResultPart
@@ -455,6 +542,7 @@ function assistantMetadata(
455
542
  const previous = tanstackMetadata(uiMessage)
456
543
  const tanstack: TanStackMessageMetadata = {}
457
544
  if (previous?.model !== undefined) tanstack.model = previous.model
545
+ if (previous?.runId !== undefined) tanstack.runId = previous.runId
458
546
  if (previous?.signature !== undefined) tanstack.signature = previous.signature
459
547
  if (fromParts.length > 0) tanstack.uiResources = fromParts
460
548
  const result = { ...current }
@@ -690,6 +778,22 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
690
778
  // model input, so it is intentionally dropped from the model message.
691
779
  break
692
780
 
781
+ case 'subagent': {
782
+ // A child that a tool call started reports through the tool result.
783
+ const block =
784
+ part.subagent.parentToolCallId === undefined
785
+ ? subagentHistoryText(part)
786
+ : ''
787
+ if (block !== '') {
788
+ const prefix = current.contentParts.length > 0 ? '\n\n' : ''
789
+ current.contentParts.push({
790
+ type: 'text',
791
+ content: `${prefix}${block}`,
792
+ })
793
+ }
794
+ break
795
+ }
796
+
693
797
  default:
694
798
  break
695
799
  }
@@ -947,7 +1051,9 @@ export function aguiSnapshotMessageToUIMessage(
947
1051
  modelMessageToUIMessage(
948
1052
  {
949
1053
  role: 'tool',
950
- content: message.content,
1054
+ content: isContentPartArray(message.content)
1055
+ ? message.content
1056
+ : aguiContentToContentParts(message.content),
951
1057
  toolCallId: message.toolCallId,
952
1058
  ...('name' in message && typeof message.name === 'string'
953
1059
  ? { name: message.name }
@@ -1071,25 +1177,29 @@ function snapshotStructuredOutput(
1071
1177
  * AG-UI user content is either a plain string or a multimodal array whose text
1072
1178
  * entries use `{ type: 'text', text }` (vs. TanStack's `{ type: 'text', content }`).
1073
1179
  * Text entries are rewritten to the TanStack shape; image/audio/video/document
1074
- * entries already match `ContentPart` and pass through. `binary` entries have no
1075
- * TanStack equivalent and are dropped.
1180
+ * entries already match `ContentPart` and pass through.
1076
1181
  */
1077
1182
  function aguiUserContentToParts(
1078
1183
  content: Extract<AGUIMessage, { role: 'user' }>['content'],
1079
1184
  ): Array<MessagePart> {
1080
- if (typeof content === 'string') {
1081
- return content ? [{ type: 'text', content }] : []
1082
- }
1185
+ const converted = aguiContentToContentParts(content)
1186
+ return typeof converted === 'string'
1187
+ ? converted
1188
+ ? [{ type: 'text', content: converted }]
1189
+ : []
1190
+ : converted
1191
+ }
1083
1192
 
1084
- const parts: Array<MessagePart> = []
1085
- for (const part of content) {
1086
- if (part.type === 'text') {
1087
- parts.push({ type: 'text', content: part.text })
1088
- } else if (part.type !== 'binary') {
1089
- parts.push(part)
1090
- }
1091
- }
1092
- return parts
1193
+ /** Convert wire content parts. Data, url, and file sources pass through. */
1194
+ export function aguiContentToContentParts(
1195
+ content: Extract<AGUIMessage, { role: 'user' }>['content'],
1196
+ ): string | Array<ContentPart> {
1197
+ if (typeof content === 'string') return content
1198
+ return content.map((part) => {
1199
+ if (part.type !== 'text') return part
1200
+ const { text, ...rest } = part
1201
+ return { ...rest, content: text }
1202
+ })
1093
1203
  }
1094
1204
 
1095
1205
  /**
@@ -112,8 +112,25 @@ export interface RunRecord {
112
112
  * reuse this record by faking `threadId = requestId`; they need a separate
113
113
  * job store. `withGenerationPersistence` currently does exactly that and
114
114
  * labels itself a stopgap — do not copy it.
115
+ *
116
+ * A subagent child record stores `subagent:<subagentRunId>` here, the key of
117
+ * its own transcript, so `findActiveRun` and `listByThread` on the
118
+ * conversation never return children. Use `listByParentRun`.
115
119
  */
116
120
  threadId: string
121
+ /**
122
+ * Parent chat run that started this child, when this record is a subagent.
123
+ * Absent on the parent run itself.
124
+ */
125
+ parentRunId?: string
126
+ /**
127
+ * The child's AG-UI subagentRunId, the id on its `SUBAGENT_*` chunks and on
128
+ * every chunk it streams. On a child record this equals `runId`. Absent on
129
+ * the parent run.
130
+ */
131
+ subagentRunId?: string
132
+ /** Agent name (`researcher`, `writer`) when this record is a subagent. */
133
+ name?: string
117
134
  status: RunStatus
118
135
  startedAt: number
119
136
  finishedAt?: number
@@ -172,9 +189,10 @@ export interface RunRecord {
172
189
  * instead of failing at build time. It was optional for exactly one release
173
190
  * cycle and cost precisely that.
174
191
  *
175
- * OPTIONAL: `listByThread`, `listReclaimable`. Each serves one higher-level
176
- * feature (thread history, reclaim reaping) and callers feature-detect them,
177
- * degrading gracefully when a backend omits them.
192
+ * OPTIONAL: `listByThread`, `listByParentRun`, `listReclaimable`. Each serves
193
+ * one higher-level feature (thread history, subagent card reload, reclaim
194
+ * reaping) and callers feature-detect them, degrading when a backend omits
195
+ * them.
178
196
  */
179
197
  export interface RunStore {
180
198
  /**
@@ -182,12 +200,17 @@ export interface RunStore {
182
200
  * already present.
183
201
  *
184
202
  * INVARIANT (idempotency): an existing record is returned **unchanged** and
185
- * the passed `threadId`/`startedAt`/`status` are ignored. This is what makes
186
- * resuming a run safe. `status` defaults to `'running'` on first creation.
203
+ * the passed `threadId`, `startedAt`, `status`, `parentRunId`,
204
+ * `subagentRunId`, and `name` are ignored. This is what makes resuming a
205
+ * run safe. `status` defaults to `'running'` on first creation. The three
206
+ * link fields are copied only on the first insert.
187
207
  */
188
208
  createOrResume: (
189
209
  input: Pick<RunRecord, 'runId' | 'threadId' | 'startedAt'> & {
190
210
  status?: RunStatus
211
+ parentRunId?: string
212
+ subagentRunId?: string
213
+ name?: string
191
214
  },
192
215
  ) => Promise<RunRecord>
193
216
  /**
@@ -215,10 +238,19 @@ export interface RunStore {
215
238
  /** Current record, or null when unknown. */
216
239
  get: (runId: string) => Promise<RunRecord | null>
217
240
  /**
218
- * Every run in a conversation, ascending by `startedAt`. OPTIONAL: only
219
- * needed to render a thread's past agent activity. Consumers feature-detect.
241
+ * Every run in a conversation, ascending by `startedAt`. OPTIONAL.
242
+ * `reconstructChat` calls it to find the parent runs of children that a
243
+ * tool call started. Without it those cards stay absent on reload.
244
+ * Consumers feature-detect.
220
245
  */
221
246
  listByThread?: (threadId: string) => Promise<Array<RunRecord>>
247
+ /**
248
+ * Child runs started by `parentRunId`, ascending by `startedAt`.
249
+ * OPTIONAL. `reconstructChat` uses this to put subagent cards back
250
+ * on the parent assistant message. A store that omits it reloads the
251
+ * text and not the cards.
252
+ */
253
+ listByParentRun?: (parentRunId: string) => Promise<Array<RunRecord>>
222
254
  /**
223
255
  * Runs that may be reclaimed: ALL THREE of `status === 'running'`,
224
256
  * `detachedSince` is set, and `detachedSince <= now - ttlMs`. The cutoff is
@@ -341,6 +373,9 @@ export class InMemoryRunStore implements RunStore {
341
373
  createOrResume(
342
374
  input: Pick<RunRecord, 'runId' | 'threadId' | 'startedAt'> & {
343
375
  status?: RunStatus
376
+ parentRunId?: string
377
+ subagentRunId?: string
378
+ name?: string
344
379
  },
345
380
  ): Promise<RunRecord> {
346
381
  const existing = this.runs.get(input.runId)
@@ -350,6 +385,13 @@ export class InMemoryRunStore implements RunStore {
350
385
  threadId: input.threadId,
351
386
  status: input.status ?? 'running',
352
387
  startedAt: input.startedAt,
388
+ ...(input.parentRunId !== undefined
389
+ ? { parentRunId: input.parentRunId }
390
+ : {}),
391
+ ...(input.subagentRunId !== undefined
392
+ ? { subagentRunId: input.subagentRunId }
393
+ : {}),
394
+ ...(input.name !== undefined ? { name: input.name } : {}),
353
395
  }
354
396
  this.runs.set(record.runId, record)
355
397
  return Promise.resolve(record)
@@ -387,6 +429,13 @@ export class InMemoryRunStore implements RunStore {
387
429
  return Promise.resolve(matching)
388
430
  }
389
431
 
432
+ listByParentRun(parentRunId: string): Promise<Array<RunRecord>> {
433
+ const matching = [...this.runs.values()]
434
+ .filter((run) => run.parentRunId === parentRunId)
435
+ .sort((a, b) => a.startedAt - b.startedAt)
436
+ return Promise.resolve(matching)
437
+ }
438
+
390
439
  listReclaimable(opts: {
391
440
  now: number
392
441
  ttlMs: number
@@ -4,8 +4,11 @@ import type {
4
4
  } from '@standard-schema/spec'
5
5
  import type {
6
6
  AgentLoopState,
7
+ EmitCustomEventOptions,
8
+ Interrupt,
7
9
  JSONSchema,
8
10
  ModelMessage,
11
+ UIMessage,
9
12
  RunAgentResumeItem,
10
13
  StreamChunk,
11
14
  TokenUsage,
@@ -190,6 +193,11 @@ export interface ChatMiddlewareContext<TContext = unknown> {
190
193
  runId: string
191
194
  /** Interrupted or parent run correlated with this continuation. */
192
195
  parentRunId?: string
196
+ /**
197
+ * Set when this run is a subagent. The id on the child's `SUBAGENT_STARTED`
198
+ * and on every chunk it streams. Absent on a top-level run.
199
+ */
200
+ subagentRunId?: string
193
201
  /**
194
202
  * AG-UI thread identifier — a stable per-conversation ID used to
195
203
  * correlate client and server devtools events. Resolves to the
@@ -216,9 +224,14 @@ export interface ChatMiddlewareContext<TContext = unknown> {
216
224
  /**
217
225
  * Push a `CUSTOM` chunk onto the chat stream immediately.
218
226
  * The engine yields it as soon as it can (including while `onConfig`
219
- * is still awaiting work such as a summarize call).
227
+ * is still awaiting work such as a summarize call). Durability then
228
+ * flushes the event on its own, unless you pass `{ batch: true }`.
220
229
  */
221
- emitCustomEvent: (name: string, value: Record<string, any>) => void
230
+ emitCustomEvent: (
231
+ name: string,
232
+ value: Record<string, any>,
233
+ options?: EmitCustomEventOptions,
234
+ ) => void
222
235
  /** Runtime context provided by chat() options */
223
236
  context: TContext
224
237
  /**
@@ -538,6 +551,40 @@ export interface ErrorInfo {
538
551
  duration: number
539
552
  }
540
553
 
554
+ /**
555
+ * Saves subagent runs while a router owns the turn.
556
+ * `withPersistence` sets this. `chat()` calls it. Apps do not.
557
+ */
558
+ export interface RoutedSubagentPersistence {
559
+ start: (input: {
560
+ threadId: string
561
+ runId: string
562
+ messages: ReadonlyArray<UIMessage | ModelMessage>
563
+ /**
564
+ * The run's resume entries: answers to earlier child interrupts, plus any
565
+ * the parent answers itself.
566
+ */
567
+ resume?: ReadonlyArray<RunAgentResumeItem>
568
+ }) => Promise<void>
569
+ chunk: (input: {
570
+ threadId: string
571
+ runId: string
572
+ chunk: StreamChunk
573
+ }) => Promise<void>
574
+ finish: (input: { threadId: string; runId: string }) => Promise<void>
575
+ /** The run stopped because a child waits for outside input. */
576
+ suspend?: (input: {
577
+ threadId: string
578
+ runId: string
579
+ interrupts: ReadonlyArray<Interrupt>
580
+ }) => Promise<void>
581
+ abort: (input: {
582
+ threadId: string
583
+ runId: string
584
+ error?: unknown
585
+ }) => Promise<void>
586
+ }
587
+
541
588
  // ===========================
542
589
  // Middleware Interface
543
590
  // ===========================
@@ -578,6 +625,12 @@ export interface ChatMiddleware<
578
625
  /** Optional name for debugging and identification */
579
626
  name?: string
580
627
 
628
+ /**
629
+ * Present when this middleware stores subagent runs.
630
+ * The router calls it. An app does not set it.
631
+ */
632
+ routedSubagentPersistence?: RoutedSubagentPersistence
633
+
581
634
  /**
582
635
  * Called at a lifecycle boundary. Return interrupt requests to pause the run.
583
636
  * Requests from every middleware in the same boundary form one batch.