@tanstack/ai 0.17.0 → 0.19.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.
- package/README.md +0 -4
- package/dist/esm/activities/chat/index.d.ts +12 -3
- package/dist/esm/activities/chat/index.js +75 -7
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.js +41 -2
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/middleware/compose.js +1 -1
- package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
- package/dist/esm/activities/chat/middleware/types.d.ts +12 -1
- package/dist/esm/activities/chat/stream/message-updaters.d.ts +35 -0
- package/dist/esm/activities/chat/stream/message-updaters.js +95 -0
- package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +1 -0
- package/dist/esm/activities/chat/stream/processor.js +90 -2
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/schema-converter.js +5 -0
- package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
- package/dist/esm/adapter-internals.d.ts +1 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +8 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/types.d.ts +86 -16
- package/dist/esm/utilities/ag-ui-wire.d.ts +44 -0
- package/dist/esm/utilities/ag-ui-wire.js +104 -0
- package/dist/esm/utilities/ag-ui-wire.js.map +1 -0
- package/dist/esm/utilities/chat-params.d.ts +80 -0
- package/dist/esm/utilities/chat-params.js +96 -0
- package/dist/esm/utilities/chat-params.js.map +1 -0
- package/package.json +3 -3
- package/skills/ai-core/ag-ui-protocol/SKILL.md +46 -3
- package/skills/ai-core/structured-outputs/SKILL.md +240 -47
- package/src/activities/chat/index.ts +144 -10
- package/src/activities/chat/messages.ts +70 -4
- package/src/activities/chat/middleware/compose.ts +1 -1
- package/src/activities/chat/middleware/types.ts +12 -1
- package/src/activities/chat/stream/message-updaters.ts +171 -0
- package/src/activities/chat/stream/processor.ts +137 -2
- package/src/activities/chat/tools/schema-converter.ts +14 -0
- package/src/adapter-internals.ts +1 -0
- package/src/index.ts +11 -0
- package/src/types.ts +104 -15
- package/src/utilities/ag-ui-wire.ts +201 -0
- package/src/utilities/chat-params.ts +199 -0
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
import { generateMessageId, uiMessageToModelMessages } from '../messages.js'
|
|
21
21
|
import { defaultJSONParser } from './json-parser'
|
|
22
22
|
import {
|
|
23
|
+
appendStructuredOutputDelta,
|
|
24
|
+
completeStructuredOutputPart,
|
|
25
|
+
errorStructuredOutputPart,
|
|
23
26
|
updateTextPart,
|
|
24
27
|
updateThinkingPart,
|
|
25
28
|
updateToolCallApproval,
|
|
@@ -145,6 +148,8 @@ export class StreamProcessor {
|
|
|
145
148
|
private pendingManualMessageId: string | null = null
|
|
146
149
|
private pendingThinkingStepId: string | null = null
|
|
147
150
|
|
|
151
|
+
private structuredMessageIds: Set<string> = new Set()
|
|
152
|
+
|
|
148
153
|
// Run tracking (for concurrent run safety)
|
|
149
154
|
private activeRuns = new Set<string>()
|
|
150
155
|
|
|
@@ -383,6 +388,27 @@ export class StreamProcessor {
|
|
|
383
388
|
* Remove messages after a certain index (for reload/retry)
|
|
384
389
|
*/
|
|
385
390
|
removeMessagesAfter(index: number): void {
|
|
391
|
+
const keptIds = new Set(this.messages.slice(0, index + 1).map((m) => m.id))
|
|
392
|
+
// Drop routing state for messages that no longer exist; otherwise a
|
|
393
|
+
// resumed stream (`reload()` or a server that reuses messageIds across
|
|
394
|
+
// runs) could land deltas / tool args on stale map entries and corrupt
|
|
395
|
+
// the new assistant message's parts. Mirror the four routing maps that
|
|
396
|
+
// key on messageId: structuredMessageIds (custom-event routing),
|
|
397
|
+
// messageStates (per-message stream state), toolCallToMessage (tool
|
|
398
|
+
// args → message), and activeMessageIds (finalize / completeAllToolCalls
|
|
399
|
+
// iteration targets — must not include phantoms).
|
|
400
|
+
for (const id of this.structuredMessageIds) {
|
|
401
|
+
if (!keptIds.has(id)) this.structuredMessageIds.delete(id)
|
|
402
|
+
}
|
|
403
|
+
for (const id of this.messageStates.keys()) {
|
|
404
|
+
if (!keptIds.has(id)) this.messageStates.delete(id)
|
|
405
|
+
}
|
|
406
|
+
for (const [toolCallId, msgId] of this.toolCallToMessage) {
|
|
407
|
+
if (!keptIds.has(msgId)) this.toolCallToMessage.delete(toolCallId)
|
|
408
|
+
}
|
|
409
|
+
for (const id of this.activeMessageIds) {
|
|
410
|
+
if (!keptIds.has(id)) this.activeMessageIds.delete(id)
|
|
411
|
+
}
|
|
386
412
|
this.messages = this.messages.slice(0, index + 1)
|
|
387
413
|
this.emitMessagesChange()
|
|
388
414
|
}
|
|
@@ -395,6 +421,7 @@ export class StreamProcessor {
|
|
|
395
421
|
this.messageStates.clear()
|
|
396
422
|
this.activeMessageIds.clear()
|
|
397
423
|
this.toolCallToMessage.clear()
|
|
424
|
+
this.structuredMessageIds.clear()
|
|
398
425
|
this.pendingManualMessageId = null
|
|
399
426
|
this.emitMessagesChange()
|
|
400
427
|
}
|
|
@@ -841,6 +868,41 @@ export class StreamProcessor {
|
|
|
841
868
|
// Content arriving means all current tool calls for this message are complete
|
|
842
869
|
this.completeAllToolCallsForMessage(messageId)
|
|
843
870
|
|
|
871
|
+
if (this.structuredMessageIds.has(messageId)) {
|
|
872
|
+
// `chunk.delta` is incremental; `chunk.content` is sometimes cumulative
|
|
873
|
+
// (mirrors what the plain-text branch handles below). Reconcile against
|
|
874
|
+
// the existing raw buffer so adapters that emit cumulative content
|
|
875
|
+
// don't duplicate the JSON.
|
|
876
|
+
let delta = chunk.delta || ''
|
|
877
|
+
if (delta === '' && chunk.content !== undefined && chunk.content !== '') {
|
|
878
|
+
const existingRaw = (
|
|
879
|
+
this.messages
|
|
880
|
+
.find((m) => m.id === messageId)
|
|
881
|
+
?.parts.find(
|
|
882
|
+
(p): p is Extract<MessagePart, { type: 'structured-output' }> =>
|
|
883
|
+
p.type === 'structured-output',
|
|
884
|
+
) ?? { raw: '' }
|
|
885
|
+
).raw
|
|
886
|
+
if (chunk.content.startsWith(existingRaw)) {
|
|
887
|
+
delta = chunk.content.slice(existingRaw.length)
|
|
888
|
+
} else if (existingRaw.startsWith(chunk.content)) {
|
|
889
|
+
delta = ''
|
|
890
|
+
} else {
|
|
891
|
+
delta = chunk.content
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
if (delta !== '') {
|
|
895
|
+
this.messages = appendStructuredOutputDelta(
|
|
896
|
+
this.messages,
|
|
897
|
+
messageId,
|
|
898
|
+
delta,
|
|
899
|
+
)
|
|
900
|
+
state.totalTextContent += delta
|
|
901
|
+
this.emitMessagesChange()
|
|
902
|
+
}
|
|
903
|
+
return
|
|
904
|
+
}
|
|
905
|
+
|
|
844
906
|
const previousSegment = state.currentSegmentText
|
|
845
907
|
|
|
846
908
|
// Detect if this is a NEW text segment (after tool calls) vs continuation
|
|
@@ -1192,10 +1254,29 @@ export class StreamProcessor {
|
|
|
1192
1254
|
} else {
|
|
1193
1255
|
this.activeRuns.clear()
|
|
1194
1256
|
}
|
|
1195
|
-
this.ensureAssistantMessage()
|
|
1196
|
-
// Prefer spec field `message`; fall back to deprecated `error.message
|
|
1257
|
+
const { messageId } = this.ensureAssistantMessage()
|
|
1258
|
+
// Prefer spec field `message`; fall back to deprecated `error.message`.
|
|
1259
|
+
// If neither is set, the chunk still carries debug context (provider
|
|
1260
|
+
// error codes, request ids, etc.) — log it so the failure isn't silent.
|
|
1197
1261
|
const errorMessage =
|
|
1198
1262
|
chunk.message || chunk.error?.message || 'An error occurred'
|
|
1263
|
+
if (!chunk.message && !chunk.error?.message) {
|
|
1264
|
+
console.error(
|
|
1265
|
+
'[StreamProcessor] RUN_ERROR with no message; original chunk:',
|
|
1266
|
+
chunk,
|
|
1267
|
+
)
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
if (this.structuredMessageIds.has(messageId)) {
|
|
1271
|
+
this.messages = errorStructuredOutputPart(
|
|
1272
|
+
this.messages,
|
|
1273
|
+
messageId,
|
|
1274
|
+
errorMessage,
|
|
1275
|
+
)
|
|
1276
|
+
this.structuredMessageIds.delete(messageId)
|
|
1277
|
+
this.emitMessagesChange()
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1199
1280
|
this.events.onError?.(new Error(errorMessage))
|
|
1200
1281
|
}
|
|
1201
1282
|
|
|
@@ -1375,6 +1456,38 @@ export class StreamProcessor {
|
|
|
1375
1456
|
): void {
|
|
1376
1457
|
const messageId = this.getActiveAssistantMessageId()
|
|
1377
1458
|
|
|
1459
|
+
if (chunk.name === 'structured-output.start' && chunk.value) {
|
|
1460
|
+
const v = chunk.value as { messageId?: string }
|
|
1461
|
+
const targetId = v.messageId ?? messageId
|
|
1462
|
+
if (targetId) {
|
|
1463
|
+
this.ensureAssistantMessage(targetId)
|
|
1464
|
+
this.structuredMessageIds.add(targetId)
|
|
1465
|
+
}
|
|
1466
|
+
return
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
if (chunk.name === 'structured-output.complete' && chunk.value) {
|
|
1470
|
+
const v = chunk.value as {
|
|
1471
|
+
object: unknown
|
|
1472
|
+
raw?: string
|
|
1473
|
+
reasoning?: string
|
|
1474
|
+
messageId?: string
|
|
1475
|
+
}
|
|
1476
|
+
const targetId = v.messageId ?? messageId
|
|
1477
|
+
if (targetId) {
|
|
1478
|
+
this.messages = completeStructuredOutputPart(
|
|
1479
|
+
this.messages,
|
|
1480
|
+
targetId,
|
|
1481
|
+
v.object,
|
|
1482
|
+
v.raw ?? '',
|
|
1483
|
+
v.reasoning,
|
|
1484
|
+
)
|
|
1485
|
+
this.structuredMessageIds.delete(targetId)
|
|
1486
|
+
this.emitMessagesChange()
|
|
1487
|
+
}
|
|
1488
|
+
// Fall through so user `onCustomEvent` callbacks still observe the event.
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1378
1491
|
// Handle client tool input availability - trigger client-side execution
|
|
1379
1492
|
if (chunk.name === 'tool-input-available' && chunk.value) {
|
|
1380
1493
|
const { toolCallId, toolName, input } = chunk.value as {
|
|
@@ -1591,6 +1704,27 @@ export class StreamProcessor {
|
|
|
1591
1704
|
}
|
|
1592
1705
|
}
|
|
1593
1706
|
|
|
1707
|
+
// The stream closed but one or more structured-output runs never sent
|
|
1708
|
+
// their terminal `structured-output.complete`. Snap each lingering
|
|
1709
|
+
// streaming part to error so the UI doesn't appear to stream forever,
|
|
1710
|
+
// and drop the routing entries so a subsequent run on the same
|
|
1711
|
+
// processor instance (long-lived `subscribe()` mode) doesn't reuse
|
|
1712
|
+
// the stale ids.
|
|
1713
|
+
//
|
|
1714
|
+
// The iteration is unconditional w.r.t. `this.hasError` — RUN_ERROR
|
|
1715
|
+
// already removed its target messageId from `structuredMessageIds`
|
|
1716
|
+
// before reaching finalize, so anything still in the set is by
|
|
1717
|
+
// definition a non-errored, never-completed run (the multi-run case:
|
|
1718
|
+
// run-A errors, run-B is still streaming when finalize fires).
|
|
1719
|
+
for (const messageId of this.structuredMessageIds) {
|
|
1720
|
+
this.messages = errorStructuredOutputPart(
|
|
1721
|
+
this.messages,
|
|
1722
|
+
messageId,
|
|
1723
|
+
'Stream ended without structured-output.complete',
|
|
1724
|
+
)
|
|
1725
|
+
}
|
|
1726
|
+
this.structuredMessageIds.clear()
|
|
1727
|
+
|
|
1594
1728
|
this.activeMessageIds.clear()
|
|
1595
1729
|
|
|
1596
1730
|
// Remove whitespace-only assistant messages (handles models like Gemini
|
|
@@ -1719,6 +1853,7 @@ export class StreamProcessor {
|
|
|
1719
1853
|
this.activeMessageIds.clear()
|
|
1720
1854
|
this.activeRuns.clear()
|
|
1721
1855
|
this.toolCallToMessage.clear()
|
|
1856
|
+
this.structuredMessageIds.clear()
|
|
1722
1857
|
this.pendingManualMessageId = null
|
|
1723
1858
|
this.pendingThinkingStepId = null
|
|
1724
1859
|
this.finishReason = null
|
|
@@ -254,6 +254,20 @@ export function convertSchemaToJsonSchema(
|
|
|
254
254
|
return result as JSONSchema
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
// Detect Standard Schema validators (Zod, ArkType, Valibot, …) that don't
|
|
258
|
+
// expose a `~standard.jsonSchema` converter. These would otherwise fall
|
|
259
|
+
// through to the JSONSchema pass-through below and ship `{ '~standard': … }`
|
|
260
|
+
// straight to the LLM provider, producing an opaque downstream error. Fail
|
|
261
|
+
// fast with actionable guidance instead.
|
|
262
|
+
if (isStandardSchema(schema)) {
|
|
263
|
+
throw new Error(
|
|
264
|
+
'Schema is a Standard Schema validator but does not expose a JSON Schema ' +
|
|
265
|
+
'converter on `~standard.jsonSchema`. Use Zod v4.2+, ArkType v2.1.28+, ' +
|
|
266
|
+
'or wrap a Valibot schema with `toStandardJsonSchema()` from ' +
|
|
267
|
+
'`@valibot/to-json-schema` before passing it as `outputSchema`.',
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
257
271
|
// If it's not a Standard JSON Schema, assume it's already a JSONSchema and pass through
|
|
258
272
|
// Still apply structured output transformation if requested
|
|
259
273
|
|
package/src/adapter-internals.ts
CHANGED
|
@@ -4,5 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
export type { ResolvedCategories } from './logger/internal-logger'
|
|
6
6
|
export { InternalLogger } from './logger/internal-logger'
|
|
7
|
+
export type { Logger } from './logger/types'
|
|
7
8
|
export { resolveDebugOption } from './logger/resolve'
|
|
8
9
|
export { toRunErrorPayload } from './activities/error-payload'
|
package/src/index.ts
CHANGED
|
@@ -168,6 +168,17 @@ export type {
|
|
|
168
168
|
JSONParser,
|
|
169
169
|
} from './activities/chat/stream/index'
|
|
170
170
|
|
|
171
|
+
// Chat utilities
|
|
172
|
+
export {
|
|
173
|
+
chatParamsFromRequest,
|
|
174
|
+
chatParamsFromRequestBody,
|
|
175
|
+
mergeAgentTools,
|
|
176
|
+
} from './utilities/chat-params'
|
|
177
|
+
|
|
178
|
+
// AG-UI wire serialization (used internally by @tanstack/ai-client)
|
|
179
|
+
export { uiMessagesToWire } from './utilities/ag-ui-wire'
|
|
180
|
+
export type { WireMessage } from './utilities/ag-ui-wire'
|
|
181
|
+
|
|
171
182
|
// Adapter extension utilities
|
|
172
183
|
export { createModel, extendAdapter } from './extend-adapter'
|
|
173
184
|
export type { ExtendedModelDef } from './extend-adapter'
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
StandardJSONSchemaV1,
|
|
3
|
+
StandardSchemaV1,
|
|
4
|
+
} from '@standard-schema/spec'
|
|
2
5
|
import type { InternalLogger } from './logger/internal-logger'
|
|
3
6
|
import type {
|
|
4
7
|
BaseEvent as AGUIBaseEvent,
|
|
@@ -91,25 +94,42 @@ export interface JSONSchema {
|
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
/**
|
|
94
|
-
* Union type for schema input - can be any Standard
|
|
97
|
+
* Union type for schema input - can be any Standard Schema compliant validator,
|
|
98
|
+
* any Standard JSON Schema compliant schema, or a plain JSONSchema object.
|
|
95
99
|
*
|
|
96
|
-
* Standard JSON Schema compliant libraries
|
|
100
|
+
* Standard JSON Schema compliant libraries (carry the JSON-schema converter):
|
|
97
101
|
* - Zod v4.2+ (natively supports StandardJSONSchemaV1)
|
|
98
102
|
* - ArkType v2.1.28+ (natively supports StandardJSONSchemaV1)
|
|
99
103
|
* - Valibot v1.2+ (via `toStandardJsonSchema()` from `@valibot/to-json-schema`)
|
|
100
104
|
*
|
|
105
|
+
* StandardSchemaV1 covers libraries whose published types only expose the
|
|
106
|
+
* validator surface — Zod's core `$ZodType['~standard']` is currently typed
|
|
107
|
+
* as `StandardSchemaV1.Props` even though the runtime attaches the
|
|
108
|
+
* `jsonSchema` converter, so this branch is what makes `InferSchemaType`
|
|
109
|
+
* recover the inferred type for callers using `z.ZodType<T>`.
|
|
110
|
+
*
|
|
101
111
|
* @see https://standardschema.dev/json-schema
|
|
102
112
|
*/
|
|
103
113
|
|
|
104
|
-
export type SchemaInput =
|
|
114
|
+
export type SchemaInput =
|
|
115
|
+
| StandardJSONSchemaV1<any, any>
|
|
116
|
+
| StandardSchemaV1<any, any>
|
|
117
|
+
| JSONSchema
|
|
105
118
|
|
|
106
119
|
/**
|
|
107
120
|
* Infer the TypeScript type from a schema.
|
|
108
121
|
* For Standard JSON Schema compliant schemas, extracts the input type.
|
|
109
|
-
* For
|
|
122
|
+
* For Standard Schema validators (e.g. Zod's `~standard` surface), extracts
|
|
123
|
+
* the input type from the `StandardSchemaV1` shape.
|
|
124
|
+
* For plain JSONSchema, returns `unknown` since we can't infer types from
|
|
125
|
+
* JSON Schema at compile time.
|
|
110
126
|
*/
|
|
111
127
|
export type InferSchemaType<T> =
|
|
112
|
-
T extends StandardJSONSchemaV1<infer TInput, unknown>
|
|
128
|
+
T extends StandardJSONSchemaV1<infer TInput, unknown>
|
|
129
|
+
? TInput
|
|
130
|
+
: T extends StandardSchemaV1<infer TInput, unknown>
|
|
131
|
+
? TInput
|
|
132
|
+
: unknown
|
|
113
133
|
|
|
114
134
|
export interface ToolCall<TMetadata = unknown> {
|
|
115
135
|
id: string
|
|
@@ -345,7 +365,43 @@ export interface ThinkingPart {
|
|
|
345
365
|
signature?: string
|
|
346
366
|
}
|
|
347
367
|
|
|
348
|
-
|
|
368
|
+
/**
|
|
369
|
+
* Recursive `Partial` — every nested field becomes optional. Used as the
|
|
370
|
+
* `partial` type on a streaming structured-output part since the progressive
|
|
371
|
+
* JSON parse hands back objects whose fields are only filled in as bytes
|
|
372
|
+
* arrive. Defaulted in `DeepPartial<unknown>` → `unknown` so untyped parts
|
|
373
|
+
* keep their existing shape.
|
|
374
|
+
*/
|
|
375
|
+
export type DeepPartial<T> =
|
|
376
|
+
T extends ReadonlyArray<infer U>
|
|
377
|
+
? Array<DeepPartial<U>>
|
|
378
|
+
: T extends object
|
|
379
|
+
? { [K in keyof T]?: DeepPartial<T[K]> }
|
|
380
|
+
: T
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* StructuredOutputPart — a typed structured response attached to the assistant
|
|
384
|
+
* message that produced it. Generic over the schema-inferred data type so
|
|
385
|
+
* consumers can thread `useChat({ outputSchema })`'s schema all the way down
|
|
386
|
+
* to `messages[i].parts[j].data`. Defaults to `unknown` so untyped consumers
|
|
387
|
+
* (e.g. internal codepaths that don't know about TSchema) keep working.
|
|
388
|
+
*/
|
|
389
|
+
export interface StructuredOutputPart<TData = unknown> {
|
|
390
|
+
type: 'structured-output'
|
|
391
|
+
status: 'streaming' | 'complete' | 'error'
|
|
392
|
+
/** Progressive parse of `raw` via parsePartialJSON — populated while streaming and after complete. */
|
|
393
|
+
partial?: DeepPartial<TData>
|
|
394
|
+
/** Validated final object — only set when `status === 'complete'`. */
|
|
395
|
+
data?: TData
|
|
396
|
+
/** Accumulating JSON buffer. Source of truth for wire round-trip. */
|
|
397
|
+
raw: string
|
|
398
|
+
/** Optional chain-of-thought surfaced by reasoning models alongside the structured output. */
|
|
399
|
+
reasoning?: string
|
|
400
|
+
/** Populated when `status === 'error'`. */
|
|
401
|
+
errorMessage?: string
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export type MessagePart<TData = unknown> =
|
|
349
405
|
| TextPart
|
|
350
406
|
| ImagePart
|
|
351
407
|
| AudioPart
|
|
@@ -354,15 +410,19 @@ export type MessagePart =
|
|
|
354
410
|
| ToolCallPart
|
|
355
411
|
| ToolResultPart
|
|
356
412
|
| ThinkingPart
|
|
413
|
+
| StructuredOutputPart<TData>
|
|
357
414
|
|
|
358
415
|
/**
|
|
359
416
|
* UIMessage - Domain-specific message format optimized for building chat UIs
|
|
360
|
-
* Contains parts that can be text, tool calls, or tool results
|
|
417
|
+
* Contains parts that can be text, tool calls, or tool results. Generic over
|
|
418
|
+
* the structured-output data type so `useChat({ outputSchema })`'s schema
|
|
419
|
+
* narrows `parts.find(p => p.type === 'structured-output').data` on the
|
|
420
|
+
* consumer side without manual casts.
|
|
361
421
|
*/
|
|
362
|
-
export interface UIMessage {
|
|
422
|
+
export interface UIMessage<TData = unknown> {
|
|
363
423
|
id: string
|
|
364
424
|
role: 'system' | 'user' | 'assistant'
|
|
365
|
-
parts: Array<MessagePart
|
|
425
|
+
parts: Array<MessagePart<TData>>
|
|
366
426
|
createdAt?: Date
|
|
367
427
|
}
|
|
368
428
|
|
|
@@ -729,8 +789,14 @@ export interface TextOptions<
|
|
|
729
789
|
*/
|
|
730
790
|
outputSchema?: SchemaInput
|
|
731
791
|
/**
|
|
732
|
-
*
|
|
733
|
-
*
|
|
792
|
+
* @deprecated Use `threadId` instead. `conversationId` is the legacy
|
|
793
|
+
* pre-AG-UI name for the same concept (a stable per-conversation
|
|
794
|
+
* identifier used to correlate client/server devtools events). When
|
|
795
|
+
* `conversationId` is omitted, the runtime falls back to `threadId`
|
|
796
|
+
* automatically, so most callers can simply pass `threadId` (or rely
|
|
797
|
+
* on `chatParamsFromRequest`, which surfaces it on `params`).
|
|
798
|
+
*
|
|
799
|
+
* Will be removed in a future major release.
|
|
734
800
|
*/
|
|
735
801
|
conversationId?: string
|
|
736
802
|
/**
|
|
@@ -766,6 +832,11 @@ export interface TextOptions<
|
|
|
766
832
|
* If not provided, a unique ID will be generated.
|
|
767
833
|
*/
|
|
768
834
|
runId?: string
|
|
835
|
+
/**
|
|
836
|
+
* Parent run ID for AG-UI protocol nested run correlation.
|
|
837
|
+
* Surfaced for observability/middleware; not consumed by the LLM call.
|
|
838
|
+
*/
|
|
839
|
+
parentRunId?: string
|
|
769
840
|
}
|
|
770
841
|
|
|
771
842
|
// ============================================================================
|
|
@@ -1083,11 +1154,28 @@ export interface StructuredOutputCompleteEvent<T = unknown> extends Omit<
|
|
|
1083
1154
|
value: { object: T; raw: string; reasoning?: string }
|
|
1084
1155
|
}
|
|
1085
1156
|
|
|
1157
|
+
/**
|
|
1158
|
+
* Emitted at the start of a streaming structured-output run, before the JSON
|
|
1159
|
+
* deltas. Tells consumers that the upcoming `TEXT_MESSAGE_CONTENT` deltas
|
|
1160
|
+
* belong to a structured response so they can route those bytes into a
|
|
1161
|
+
* `StructuredOutputPart` instead of building a `TextPart`. Carries the
|
|
1162
|
+
* `messageId` the deltas will be tagged with so the routing decision can be
|
|
1163
|
+
* made per-message rather than globally.
|
|
1164
|
+
*/
|
|
1165
|
+
export interface StructuredOutputStartEvent extends Omit<
|
|
1166
|
+
CustomEvent,
|
|
1167
|
+
'name' | 'value'
|
|
1168
|
+
> {
|
|
1169
|
+
name: 'structured-output.start'
|
|
1170
|
+
value: { messageId: string }
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1086
1173
|
/**
|
|
1087
1174
|
* Emitted when a server tool requires approval before execution. The agent
|
|
1088
1175
|
* loop yields this and pauses — `structured-output.complete` will not fire
|
|
1089
1176
|
* for that run. The shape is fixed by the orchestrator's tool-approval flow
|
|
1090
|
-
* (
|
|
1177
|
+
* (the agent-loop branch of `runStreamingStructuredOutputImpl` in
|
|
1178
|
+
* `activities/chat/index.ts` forwards CUSTOM events from `TextEngine.run()`).
|
|
1091
1179
|
*/
|
|
1092
1180
|
export interface ApprovalRequestedEvent extends Omit<
|
|
1093
1181
|
CustomEvent,
|
|
@@ -1105,8 +1193,8 @@ export interface ApprovalRequestedEvent extends Omit<
|
|
|
1105
1193
|
/**
|
|
1106
1194
|
* Emitted when a client tool is invoked. The agent loop yields this and
|
|
1107
1195
|
* pauses to let the caller run the tool client-side — `structured-output.complete`
|
|
1108
|
-
* will not fire for that run. Shape fixed by
|
|
1109
|
-
* `activities/chat/index.ts`.
|
|
1196
|
+
* will not fire for that run. Shape fixed by the agent-loop forwarding in
|
|
1197
|
+
* `runStreamingStructuredOutputImpl` in `activities/chat/index.ts`.
|
|
1110
1198
|
*/
|
|
1111
1199
|
export interface ToolInputAvailableEvent extends Omit<
|
|
1112
1200
|
CustomEvent,
|
|
@@ -1152,6 +1240,7 @@ export interface ToolInputAvailableEvent extends Omit<
|
|
|
1152
1240
|
*/
|
|
1153
1241
|
export type StructuredOutputStream<T = unknown> = AsyncIterable<
|
|
1154
1242
|
| Exclude<StreamChunk, CustomEvent>
|
|
1243
|
+
| StructuredOutputStartEvent
|
|
1155
1244
|
| StructuredOutputCompleteEvent<T>
|
|
1156
1245
|
| ApprovalRequestedEvent
|
|
1157
1246
|
| ToolInputAvailableEvent
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type { ContentPart, MessagePart, UIMessage } from '../types'
|
|
2
|
+
|
|
3
|
+
type AGUITextInputContent = { type: 'text'; text: string }
|
|
4
|
+
type AGUIInputContent =
|
|
5
|
+
| AGUITextInputContent
|
|
6
|
+
| (ContentPart & { type: 'image' | 'audio' | 'video' | 'document' })
|
|
7
|
+
|
|
8
|
+
type AGUIToolCallMirror = {
|
|
9
|
+
id: string
|
|
10
|
+
type: 'function'
|
|
11
|
+
function: { name: string; arguments: string }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type AGUIToolMessage = {
|
|
15
|
+
role: 'tool'
|
|
16
|
+
id: string
|
|
17
|
+
toolCallId: string
|
|
18
|
+
content: string
|
|
19
|
+
error?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type AGUIReasoningMessage = {
|
|
23
|
+
role: 'reasoning'
|
|
24
|
+
id: string
|
|
25
|
+
content: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type WireAnchorMessage = UIMessage & {
|
|
29
|
+
content?: string | Array<AGUIInputContent>
|
|
30
|
+
toolCalls?: Array<AGUIToolCallMirror>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type WireMessage =
|
|
34
|
+
| WireAnchorMessage
|
|
35
|
+
| AGUIToolMessage
|
|
36
|
+
| AGUIReasoningMessage
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Serialize TanStack `UIMessage`s into the AG-UI `RunAgentInput.messages`
|
|
40
|
+
* wire shape. Each anchor (system/user/assistant) carries the canonical
|
|
41
|
+
* `parts` array verbatim plus AG-UI mirror fields (`content`, `toolCalls`)
|
|
42
|
+
* so AG-UI Zod parsing succeeds. Tool results and thinking parts on
|
|
43
|
+
* assistant messages are additionally emitted as fan-out
|
|
44
|
+
* `{role:'tool',...}` and `{role:'reasoning',...}` entries for strict
|
|
45
|
+
* AG-UI server consumers.
|
|
46
|
+
*/
|
|
47
|
+
export function uiMessagesToWire(
|
|
48
|
+
messages: Array<UIMessage>,
|
|
49
|
+
): Array<WireMessage> {
|
|
50
|
+
const wire: Array<WireMessage> = []
|
|
51
|
+
|
|
52
|
+
for (const msg of messages) {
|
|
53
|
+
// Defensive: if parts is missing (ModelMessage-shaped input), pass through as-is.
|
|
54
|
+
// UIMessage always has parts; ModelMessage uses content directly.
|
|
55
|
+
const parts: ReadonlyArray<MessagePart> =
|
|
56
|
+
(msg.parts as ReadonlyArray<MessagePart> | undefined) ?? []
|
|
57
|
+
|
|
58
|
+
if (msg.role === 'system') {
|
|
59
|
+
wire.push({
|
|
60
|
+
...msg,
|
|
61
|
+
content:
|
|
62
|
+
parts.length > 0
|
|
63
|
+
? collectText(parts)
|
|
64
|
+
: ((msg as unknown as { content?: string }).content ?? ''),
|
|
65
|
+
})
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (msg.role === 'user') {
|
|
70
|
+
wire.push({
|
|
71
|
+
...msg,
|
|
72
|
+
content:
|
|
73
|
+
parts.length > 0
|
|
74
|
+
? collectUserContent(parts)
|
|
75
|
+
: ((msg as unknown as { content?: string }).content ?? ''),
|
|
76
|
+
})
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// assistant: emit reasoning fan-outs first, then anchor, then tool fan-outs
|
|
81
|
+
for (const part of parts) {
|
|
82
|
+
if (part.type === 'thinking') {
|
|
83
|
+
wire.push({
|
|
84
|
+
role: 'reasoning',
|
|
85
|
+
id: deriveReasoningId(msg.id, part),
|
|
86
|
+
content: part.content,
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const text = collectText(parts)
|
|
92
|
+
const toolCalls = collectToolCalls(parts)
|
|
93
|
+
wire.push({
|
|
94
|
+
...msg,
|
|
95
|
+
...(text !== '' && { content: text }),
|
|
96
|
+
...(toolCalls && { toolCalls }),
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
for (const part of parts) {
|
|
100
|
+
if (part.type === 'tool-result') {
|
|
101
|
+
wire.push({
|
|
102
|
+
role: 'tool',
|
|
103
|
+
id: deriveToolMessageId(part.toolCallId),
|
|
104
|
+
toolCallId: part.toolCallId,
|
|
105
|
+
content: part.content,
|
|
106
|
+
...(part.error !== undefined && { error: part.error }),
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return wire
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function collectText(parts: ReadonlyArray<MessagePart>): string {
|
|
116
|
+
// The streamed JSON of a completed structured-output part is the source of
|
|
117
|
+
// truth for multi-turn coherence — emitting it back as assistant content
|
|
118
|
+
// lets the LLM see its own prior structured response. Streaming/errored
|
|
119
|
+
// parts are skipped: they'd ship malformed JSON fragments and confuse the
|
|
120
|
+
// model. `completeStructuredOutputPart` tries hard to populate `raw`
|
|
121
|
+
// (caller → existing buffer → `JSON.stringify(data)`), but the stringify
|
|
122
|
+
// fallback can leave it empty when `data` is unserializable (BigInt,
|
|
123
|
+
// circular). The `p.raw !== ''` guard below is what enforces "no malformed
|
|
124
|
+
// round-trip" in that case — without it we'd ship `''` and the model would
|
|
125
|
+
// see an empty assistant turn.
|
|
126
|
+
const out: Array<string> = []
|
|
127
|
+
for (const p of parts) {
|
|
128
|
+
if (p.type === 'text') {
|
|
129
|
+
out.push(p.content)
|
|
130
|
+
} else if (
|
|
131
|
+
p.type === 'structured-output' &&
|
|
132
|
+
p.status === 'complete' &&
|
|
133
|
+
p.raw !== ''
|
|
134
|
+
) {
|
|
135
|
+
out.push(p.raw)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return out.join('')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function collectUserContent(
|
|
142
|
+
parts: ReadonlyArray<MessagePart>,
|
|
143
|
+
): string | Array<AGUIInputContent> {
|
|
144
|
+
const hasMultimodal = parts.some(
|
|
145
|
+
(p) =>
|
|
146
|
+
p.type === 'image' ||
|
|
147
|
+
p.type === 'audio' ||
|
|
148
|
+
p.type === 'video' ||
|
|
149
|
+
p.type === 'document',
|
|
150
|
+
)
|
|
151
|
+
if (!hasMultimodal) {
|
|
152
|
+
return collectText(parts)
|
|
153
|
+
}
|
|
154
|
+
const out: Array<AGUIInputContent> = []
|
|
155
|
+
for (const p of parts) {
|
|
156
|
+
if (p.type === 'text') {
|
|
157
|
+
out.push({ type: 'text', text: p.content })
|
|
158
|
+
} else if (
|
|
159
|
+
p.type === 'image' ||
|
|
160
|
+
p.type === 'audio' ||
|
|
161
|
+
p.type === 'video' ||
|
|
162
|
+
p.type === 'document'
|
|
163
|
+
) {
|
|
164
|
+
out.push(p as AGUIInputContent)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return out
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function collectToolCalls(
|
|
171
|
+
parts: ReadonlyArray<MessagePart>,
|
|
172
|
+
): Array<AGUIToolCallMirror> | undefined {
|
|
173
|
+
const calls: Array<AGUIToolCallMirror> = []
|
|
174
|
+
for (const p of parts) {
|
|
175
|
+
if (p.type === 'tool-call') {
|
|
176
|
+
calls.push({
|
|
177
|
+
id: p.id,
|
|
178
|
+
type: 'function',
|
|
179
|
+
function: { name: p.name, arguments: p.arguments },
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return calls.length > 0 ? calls : undefined
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function deriveReasoningId(messageId: string, part: MessagePart): string {
|
|
187
|
+
return `${messageId}-reasoning-${(part as { id?: string }).id ?? hashContent((part as { content: string }).content)}`
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function deriveToolMessageId(toolCallId: string): string {
|
|
191
|
+
return `tool-${toolCallId}`
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function hashContent(s: string): string {
|
|
195
|
+
// Cheap deterministic id suffix; collisions are tolerable since
|
|
196
|
+
// reasoning ids only matter for AG-UI server consumers, not for our
|
|
197
|
+
// own server's dedup logic (which keys on toolCallId, not reasoning id).
|
|
198
|
+
let h = 0
|
|
199
|
+
for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0
|
|
200
|
+
return Math.abs(h).toString(36)
|
|
201
|
+
}
|