@tanstack/ai 0.16.0 → 0.18.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/dist/esm/activities/chat/adapter.d.ts +14 -0
- package/dist/esm/activities/chat/adapter.js.map +1 -1
- package/dist/esm/activities/chat/index.d.ts +27 -8
- package/dist/esm/activities/chat/index.js +245 -14
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.js +26 -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/tools/schema-converter.js +5 -0
- package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
- package/dist/esm/activities/error-payload.d.ts +0 -8
- package/dist/esm/activities/error-payload.js +20 -2
- package/dist/esm/activities/error-payload.js.map +1 -1
- package/dist/esm/activities/generateImage/adapter.d.ts +2 -2
- package/dist/esm/activities/generateImage/adapter.js.map +1 -1
- package/dist/esm/activities/generateVideo/adapter.d.ts +2 -2
- package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
- package/dist/esm/activities/index.d.ts +1 -0
- package/dist/esm/activities/index.js +2 -0
- package/dist/esm/activities/index.js.map +1 -1
- package/dist/esm/activities/stream-generation-result.js +0 -2
- package/dist/esm/activities/stream-generation-result.js.map +1 -1
- package/dist/esm/activities/summarize/adapter.d.ts +4 -4
- package/dist/esm/activities/summarize/adapter.js.map +1 -1
- package/dist/esm/activities/summarize/chat-stream-summarize.d.ts +45 -0
- package/dist/esm/activities/summarize/chat-stream-summarize.js +148 -0
- package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -0
- package/dist/esm/activities/summarize/index.d.ts +1 -0
- package/dist/esm/activities/summarize/index.js +4 -2
- package/dist/esm/activities/summarize/index.js.map +1 -1
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/types.d.ts +123 -11
- package/dist/esm/utilities/ag-ui-wire.d.ts +44 -0
- package/dist/esm/utilities/ag-ui-wire.js +96 -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 +92 -1
- package/src/activities/chat/adapter.ts +17 -0
- package/src/activities/chat/index.ts +401 -35
- package/src/activities/chat/messages.ts +44 -4
- package/src/activities/chat/middleware/compose.ts +1 -1
- package/src/activities/chat/middleware/types.ts +12 -1
- package/src/activities/chat/tools/schema-converter.ts +14 -0
- package/src/activities/error-payload.ts +31 -2
- package/src/activities/generateImage/adapter.ts +8 -2
- package/src/activities/generateVideo/adapter.ts +8 -2
- package/src/activities/index.ts +5 -0
- package/src/activities/stream-generation-result.ts +4 -6
- package/src/activities/summarize/adapter.ts +8 -4
- package/src/activities/summarize/chat-stream-summarize.ts +238 -0
- package/src/activities/summarize/index.ts +12 -9
- package/src/index.ts +11 -0
- package/src/types.ts +146 -11
- package/src/utilities/ag-ui-wire.ts +182 -0
- package/src/utilities/chat-params.ts +199 -0
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
|
|
@@ -729,8 +749,14 @@ export interface TextOptions<
|
|
|
729
749
|
*/
|
|
730
750
|
outputSchema?: SchemaInput
|
|
731
751
|
/**
|
|
732
|
-
*
|
|
733
|
-
*
|
|
752
|
+
* @deprecated Use `threadId` instead. `conversationId` is the legacy
|
|
753
|
+
* pre-AG-UI name for the same concept (a stable per-conversation
|
|
754
|
+
* identifier used to correlate client/server devtools events). When
|
|
755
|
+
* `conversationId` is omitted, the runtime falls back to `threadId`
|
|
756
|
+
* automatically, so most callers can simply pass `threadId` (or rely
|
|
757
|
+
* on `chatParamsFromRequest`, which surfaces it on `params`).
|
|
758
|
+
*
|
|
759
|
+
* Will be removed in a future major release.
|
|
734
760
|
*/
|
|
735
761
|
conversationId?: string
|
|
736
762
|
/**
|
|
@@ -766,6 +792,11 @@ export interface TextOptions<
|
|
|
766
792
|
* If not provided, a unique ID will be generated.
|
|
767
793
|
*/
|
|
768
794
|
runId?: string
|
|
795
|
+
/**
|
|
796
|
+
* Parent run ID for AG-UI protocol nested run correlation.
|
|
797
|
+
* Surfaced for observability/middleware; not consumed by the LLM call.
|
|
798
|
+
*/
|
|
799
|
+
parentRunId?: string
|
|
769
800
|
}
|
|
770
801
|
|
|
771
802
|
// ============================================================================
|
|
@@ -1057,6 +1088,106 @@ export interface CustomEvent extends AGUICustomEvent {
|
|
|
1057
1088
|
model?: string
|
|
1058
1089
|
}
|
|
1059
1090
|
|
|
1091
|
+
/**
|
|
1092
|
+
* Final event of a streaming structured-output run. Carries the validated
|
|
1093
|
+
* `object` (typed as `T` after the orchestrator runs Standard Schema parsing),
|
|
1094
|
+
* the `raw` JSON text that produced it, and — for thinking/reasoning models —
|
|
1095
|
+
* the accumulated reasoning text. Adapters emit this with `T = unknown`; the
|
|
1096
|
+
* chat orchestrator narrows to the schema's inferred type after validation.
|
|
1097
|
+
*
|
|
1098
|
+
* `reasoning` is `undefined` when the model produced none (most non-thinking
|
|
1099
|
+
* models) and when the underlying adapter doesn't expose reasoning streams.
|
|
1100
|
+
*
|
|
1101
|
+
* `name` is a string literal so consumers can narrow directly:
|
|
1102
|
+
*
|
|
1103
|
+
* ```ts
|
|
1104
|
+
* if (chunk.type === 'CUSTOM' && chunk.name === 'structured-output.complete') {
|
|
1105
|
+
* chunk.value.object // typed as T
|
|
1106
|
+
* }
|
|
1107
|
+
* ```
|
|
1108
|
+
*/
|
|
1109
|
+
export interface StructuredOutputCompleteEvent<T = unknown> extends Omit<
|
|
1110
|
+
CustomEvent,
|
|
1111
|
+
'name' | 'value'
|
|
1112
|
+
> {
|
|
1113
|
+
name: 'structured-output.complete'
|
|
1114
|
+
value: { object: T; raw: string; reasoning?: string }
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* Emitted when a server tool requires approval before execution. The agent
|
|
1119
|
+
* loop yields this and pauses — `structured-output.complete` will not fire
|
|
1120
|
+
* for that run. The shape is fixed by the orchestrator's tool-approval flow
|
|
1121
|
+
* (see `buildApprovalChunks` in `activities/chat/index.ts`).
|
|
1122
|
+
*/
|
|
1123
|
+
export interface ApprovalRequestedEvent extends Omit<
|
|
1124
|
+
CustomEvent,
|
|
1125
|
+
'name' | 'value'
|
|
1126
|
+
> {
|
|
1127
|
+
name: 'approval-requested'
|
|
1128
|
+
value: {
|
|
1129
|
+
toolCallId: string
|
|
1130
|
+
toolName: string
|
|
1131
|
+
input: unknown
|
|
1132
|
+
approval: { id: string; needsApproval: true }
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
/**
|
|
1137
|
+
* Emitted when a client tool is invoked. The agent loop yields this and
|
|
1138
|
+
* pauses to let the caller run the tool client-side — `structured-output.complete`
|
|
1139
|
+
* will not fire for that run. Shape fixed by `buildClientToolChunks` in
|
|
1140
|
+
* `activities/chat/index.ts`.
|
|
1141
|
+
*/
|
|
1142
|
+
export interface ToolInputAvailableEvent extends Omit<
|
|
1143
|
+
CustomEvent,
|
|
1144
|
+
'name' | 'value'
|
|
1145
|
+
> {
|
|
1146
|
+
name: 'tool-input-available'
|
|
1147
|
+
value: {
|
|
1148
|
+
toolCallId: string
|
|
1149
|
+
toolName: string
|
|
1150
|
+
input: unknown
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* Public type for streams returned by `chat({ outputSchema, stream: true })`.
|
|
1156
|
+
*
|
|
1157
|
+
* Yields all standard `StreamChunk` lifecycle events plus the three tagged
|
|
1158
|
+
* `CUSTOM` events the orchestrator can emit through this path:
|
|
1159
|
+
* - `structured-output.complete` — terminal event with typed `value.object: T`
|
|
1160
|
+
* - `approval-requested` — server tool needs approval (pauses the run)
|
|
1161
|
+
* - `tool-input-available` — client tool invocation (pauses the run)
|
|
1162
|
+
*
|
|
1163
|
+
* Each variant has a literal `name`, so a single discriminated narrow gives
|
|
1164
|
+
* you a typed `value` with no helper or cast:
|
|
1165
|
+
*
|
|
1166
|
+
* ```ts
|
|
1167
|
+
* for await (const chunk of stream) {
|
|
1168
|
+
* if (chunk.type === 'CUSTOM' && chunk.name === 'structured-output.complete') {
|
|
1169
|
+
* chunk.value.object // typed as T
|
|
1170
|
+
* } else if (chunk.type === 'CUSTOM' && chunk.name === 'approval-requested') {
|
|
1171
|
+
* chunk.value.toolCallId // typed as string
|
|
1172
|
+
* }
|
|
1173
|
+
* }
|
|
1174
|
+
* ```
|
|
1175
|
+
*
|
|
1176
|
+
* Caveat: tools can emit arbitrary user-defined custom events via the
|
|
1177
|
+
* `emitCustomEvent(name, value)` context API. Those flow through this stream
|
|
1178
|
+
* at runtime but are intentionally absent from this type — including a bare
|
|
1179
|
+
* `CustomEvent` (whose `value: any` would poison the union) would collapse
|
|
1180
|
+
* `chunk.value` back to `any` after the narrow. If you rely on
|
|
1181
|
+
* `emitCustomEvent` plus `outputSchema + stream: true`, branch on `CUSTOM`
|
|
1182
|
+
* outside the literal-`name` narrows or cast explicitly.
|
|
1183
|
+
*/
|
|
1184
|
+
export type StructuredOutputStream<T = unknown> = AsyncIterable<
|
|
1185
|
+
| Exclude<StreamChunk, CustomEvent>
|
|
1186
|
+
| StructuredOutputCompleteEvent<T>
|
|
1187
|
+
| ApprovalRequestedEvent
|
|
1188
|
+
| ToolInputAvailableEvent
|
|
1189
|
+
>
|
|
1190
|
+
|
|
1060
1191
|
// ============================================================================
|
|
1061
1192
|
// AG-UI Reasoning Event Interfaces
|
|
1062
1193
|
// ============================================================================
|
|
@@ -1179,12 +1310,16 @@ export interface TextCompletionChunk {
|
|
|
1179
1310
|
}
|
|
1180
1311
|
}
|
|
1181
1312
|
|
|
1182
|
-
export interface SummarizationOptions
|
|
1313
|
+
export interface SummarizationOptions<
|
|
1314
|
+
TProviderOptions extends object = Record<string, unknown>,
|
|
1315
|
+
> {
|
|
1183
1316
|
model: string
|
|
1184
1317
|
text: string
|
|
1185
1318
|
maxLength?: number
|
|
1186
1319
|
style?: 'bullet-points' | 'paragraph' | 'concise'
|
|
1187
1320
|
focus?: Array<string>
|
|
1321
|
+
/** Provider-specific options forwarded by the summarize() activity. */
|
|
1322
|
+
modelOptions?: TProviderOptions
|
|
1188
1323
|
/**
|
|
1189
1324
|
* Internal logger threaded from the summarize() entry point. Adapters must
|
|
1190
1325
|
* call logger.request() before the SDK call and logger.errors() in catch blocks.
|
|
@@ -1213,7 +1348,7 @@ export interface SummarizationResult {
|
|
|
1213
1348
|
*/
|
|
1214
1349
|
export interface ImageGenerationOptions<
|
|
1215
1350
|
TProviderOptions extends object = object,
|
|
1216
|
-
TSize extends string = string,
|
|
1351
|
+
TSize extends string | undefined = string,
|
|
1217
1352
|
> {
|
|
1218
1353
|
/** The model to use for image generation */
|
|
1219
1354
|
model: string
|
|
@@ -1343,7 +1478,7 @@ export interface AudioGenerationResult {
|
|
|
1343
1478
|
*/
|
|
1344
1479
|
export interface VideoGenerationOptions<
|
|
1345
1480
|
TProviderOptions extends object = object,
|
|
1346
|
-
TSize extends string = string,
|
|
1481
|
+
TSize extends string | undefined = string,
|
|
1347
1482
|
> {
|
|
1348
1483
|
/** The model to use for video generation */
|
|
1349
1484
|
model: string
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { ContentPart, MessagePart, TextPart, 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
|
+
return parts
|
|
117
|
+
.filter((p): p is TextPart => p.type === 'text')
|
|
118
|
+
.map((p) => p.content)
|
|
119
|
+
.join('')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function collectUserContent(
|
|
123
|
+
parts: ReadonlyArray<MessagePart>,
|
|
124
|
+
): string | Array<AGUIInputContent> {
|
|
125
|
+
const hasMultimodal = parts.some(
|
|
126
|
+
(p) =>
|
|
127
|
+
p.type === 'image' ||
|
|
128
|
+
p.type === 'audio' ||
|
|
129
|
+
p.type === 'video' ||
|
|
130
|
+
p.type === 'document',
|
|
131
|
+
)
|
|
132
|
+
if (!hasMultimodal) {
|
|
133
|
+
return collectText(parts)
|
|
134
|
+
}
|
|
135
|
+
const out: Array<AGUIInputContent> = []
|
|
136
|
+
for (const p of parts) {
|
|
137
|
+
if (p.type === 'text') {
|
|
138
|
+
out.push({ type: 'text', text: p.content })
|
|
139
|
+
} else if (
|
|
140
|
+
p.type === 'image' ||
|
|
141
|
+
p.type === 'audio' ||
|
|
142
|
+
p.type === 'video' ||
|
|
143
|
+
p.type === 'document'
|
|
144
|
+
) {
|
|
145
|
+
out.push(p as AGUIInputContent)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return out
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function collectToolCalls(
|
|
152
|
+
parts: ReadonlyArray<MessagePart>,
|
|
153
|
+
): Array<AGUIToolCallMirror> | undefined {
|
|
154
|
+
const calls: Array<AGUIToolCallMirror> = []
|
|
155
|
+
for (const p of parts) {
|
|
156
|
+
if (p.type === 'tool-call') {
|
|
157
|
+
calls.push({
|
|
158
|
+
id: p.id,
|
|
159
|
+
type: 'function',
|
|
160
|
+
function: { name: p.name, arguments: p.arguments },
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return calls.length > 0 ? calls : undefined
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function deriveReasoningId(messageId: string, part: MessagePart): string {
|
|
168
|
+
return `${messageId}-reasoning-${(part as { id?: string }).id ?? hashContent((part as { content: string }).content)}`
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function deriveToolMessageId(toolCallId: string): string {
|
|
172
|
+
return `tool-${toolCallId}`
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function hashContent(s: string): string {
|
|
176
|
+
// Cheap deterministic id suffix; collisions are tolerable since
|
|
177
|
+
// reasoning ids only matter for AG-UI server consumers, not for our
|
|
178
|
+
// own server's dedup logic (which keys on toolCallId, not reasoning id).
|
|
179
|
+
let h = 0
|
|
180
|
+
for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0
|
|
181
|
+
return Math.abs(h).toString(36)
|
|
182
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { AGUIError, RunAgentInputSchema } from '@ag-ui/core'
|
|
2
|
+
import type { Context as AGUIContext } from '@ag-ui/core'
|
|
3
|
+
import type { JSONSchema, ModelMessage, Tool, UIMessage } from '../types'
|
|
4
|
+
|
|
5
|
+
const KNOWN_PART_TYPES = new Set([
|
|
6
|
+
'text',
|
|
7
|
+
'image',
|
|
8
|
+
'audio',
|
|
9
|
+
'video',
|
|
10
|
+
'document',
|
|
11
|
+
'tool-call',
|
|
12
|
+
'tool-result',
|
|
13
|
+
'thinking',
|
|
14
|
+
])
|
|
15
|
+
|
|
16
|
+
function isValidParts(value: unknown): value is Array<{ type: string }> {
|
|
17
|
+
if (!Array.isArray(value)) return false
|
|
18
|
+
for (const p of value) {
|
|
19
|
+
if (!p || typeof p !== 'object') return false
|
|
20
|
+
const type = (p as { type?: unknown }).type
|
|
21
|
+
if (typeof type !== 'string' || !KNOWN_PART_TYPES.has(type)) return false
|
|
22
|
+
}
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse and validate an HTTP request body as an AG-UI `RunAgentInput`.
|
|
28
|
+
*
|
|
29
|
+
* Returns a spread-friendly object whose `messages` field is suitable for
|
|
30
|
+
* passing directly to `chat({ messages })`. The existing
|
|
31
|
+
* `convertMessagesToModelMessages` handles AG-UI fan-out dedup and
|
|
32
|
+
* reasoning/activity/developer-role normalization internally.
|
|
33
|
+
*
|
|
34
|
+
* @throws An error with a migration-pointing message when the body does
|
|
35
|
+
* not conform to AG-UI 0.0.52 `RunAgentInputSchema`. Surface this as a
|
|
36
|
+
* 400 Bad Request to the client.
|
|
37
|
+
*/
|
|
38
|
+
export function chatParamsFromRequestBody(body: unknown): Promise<{
|
|
39
|
+
messages: Array<UIMessage | ModelMessage>
|
|
40
|
+
threadId: string
|
|
41
|
+
runId: string
|
|
42
|
+
parentRunId?: string
|
|
43
|
+
tools: Array<{ name: string; description: string; parameters: JSONSchema }>
|
|
44
|
+
forwardedProps: Record<string, unknown>
|
|
45
|
+
state: unknown
|
|
46
|
+
context: Array<AGUIContext>
|
|
47
|
+
}> {
|
|
48
|
+
const parseResult = RunAgentInputSchema.safeParse(body)
|
|
49
|
+
if (!parseResult.success) {
|
|
50
|
+
return Promise.reject(
|
|
51
|
+
new AGUIError(
|
|
52
|
+
`Request body is not a valid AG-UI RunAgentInput. ` +
|
|
53
|
+
`If you're upgrading from a previous @tanstack/ai-client release, ` +
|
|
54
|
+
`see docs/migration/ag-ui-compliance.md. ` +
|
|
55
|
+
`Validation errors: ${parseResult.error.message}`,
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const parsed = parseResult.data
|
|
61
|
+
|
|
62
|
+
// AG-UI Zod uses `.strip()` so extra fields like `parts` on messages are
|
|
63
|
+
// dropped during parse. We re-attach them from the original body so the
|
|
64
|
+
// existing UIMessage path inside `chat()` can use them directly.
|
|
65
|
+
const rawMessages =
|
|
66
|
+
(body as { messages?: Array<Record<string, unknown>> }).messages ?? []
|
|
67
|
+
const messages = parsed.messages.map((m, i) => {
|
|
68
|
+
const raw = rawMessages[i]
|
|
69
|
+
if (
|
|
70
|
+
raw &&
|
|
71
|
+
typeof raw === 'object' &&
|
|
72
|
+
'parts' in raw &&
|
|
73
|
+
isValidParts(raw.parts)
|
|
74
|
+
) {
|
|
75
|
+
return { ...m, parts: raw.parts } as UIMessage | ModelMessage
|
|
76
|
+
}
|
|
77
|
+
return m as ModelMessage
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
return Promise.resolve({
|
|
81
|
+
messages,
|
|
82
|
+
threadId: parsed.threadId,
|
|
83
|
+
runId: parsed.runId,
|
|
84
|
+
parentRunId: parsed.parentRunId,
|
|
85
|
+
tools: parsed.tools as Array<{
|
|
86
|
+
name: string
|
|
87
|
+
description: string
|
|
88
|
+
parameters: JSONSchema
|
|
89
|
+
}>,
|
|
90
|
+
forwardedProps: (parsed.forwardedProps ?? {}) as Record<string, unknown>,
|
|
91
|
+
state: parsed.state,
|
|
92
|
+
context: parsed.context,
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Read an HTTP `Request`, parse its JSON body, and validate it as an
|
|
98
|
+
* AG-UI `RunAgentInput` — collapsing the standard `req.json()` +
|
|
99
|
+
* `chatParamsFromRequestBody(...)` pair into a single call.
|
|
100
|
+
*
|
|
101
|
+
* On a malformed body or invalid AG-UI shape, this **throws a
|
|
102
|
+
* `Response`** with status 400 and a migration-pointing message in the
|
|
103
|
+
* body. Frameworks that natively handle thrown `Response` objects
|
|
104
|
+
* (TanStack Start, SolidStart, Remix, React Router 7) will return the
|
|
105
|
+
* 400 to the client automatically, so the handler reduces to:
|
|
106
|
+
*
|
|
107
|
+
* ```ts
|
|
108
|
+
* export async function POST(req: Request) {
|
|
109
|
+
* const params = await chatParamsFromRequest(req)
|
|
110
|
+
* // ...use params
|
|
111
|
+
* }
|
|
112
|
+
* ```
|
|
113
|
+
*
|
|
114
|
+
* In frameworks that do not auto-handle thrown `Response` objects
|
|
115
|
+
* (Next.js Route Handlers, SvelteKit, Hono, raw Node), wrap the call
|
|
116
|
+
* with try/catch and return the caught Response yourself, or use
|
|
117
|
+
* `chatParamsFromRequestBody` directly with your own JSON-parsing.
|
|
118
|
+
*
|
|
119
|
+
* @throws {Response} 400 on malformed JSON or invalid AG-UI shape.
|
|
120
|
+
*/
|
|
121
|
+
export async function chatParamsFromRequest(
|
|
122
|
+
req: Request,
|
|
123
|
+
): Promise<Awaited<ReturnType<typeof chatParamsFromRequestBody>>> {
|
|
124
|
+
let body: unknown
|
|
125
|
+
try {
|
|
126
|
+
body = await req.json()
|
|
127
|
+
} catch (cause) {
|
|
128
|
+
// Preserve the underlying error on the thrown Response for
|
|
129
|
+
// server-side observability without leaking it to the client.
|
|
130
|
+
const res = new Response(
|
|
131
|
+
'Invalid AG-UI request body. See docs/migration/ag-ui-compliance.md.',
|
|
132
|
+
{ status: 400 },
|
|
133
|
+
)
|
|
134
|
+
;(res as unknown as { cause?: unknown }).cause = cause
|
|
135
|
+
throw res
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
return await chatParamsFromRequestBody(body)
|
|
139
|
+
} catch (cause) {
|
|
140
|
+
// Generic public message — avoid echoing Zod paths (which can contain
|
|
141
|
+
// user payload fragments) or internal validator strings to the client.
|
|
142
|
+
// The original AGUIError is attached as `cause` so server logs can
|
|
143
|
+
// surface it without exposing it to remote callers.
|
|
144
|
+
const res = new Response(
|
|
145
|
+
'Invalid AG-UI request body. See docs/migration/ag-ui-compliance.md.',
|
|
146
|
+
{ status: 400 },
|
|
147
|
+
)
|
|
148
|
+
;(res as unknown as { cause?: unknown }).cause = cause
|
|
149
|
+
throw res
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Merge a server-side tool array with the AG-UI client-declared tools
|
|
155
|
+
* received in the request body.
|
|
156
|
+
*
|
|
157
|
+
* Rules:
|
|
158
|
+
* - Server tools win on name collision. The client's declaration is
|
|
159
|
+
* ignored if the server already has a tool with that name. The client's
|
|
160
|
+
* UI-side handler still fires when the streamed tool-result event comes
|
|
161
|
+
* through (see `chat-client.ts` `onToolCall`), giving the
|
|
162
|
+
* "after server execution the client also handles" semantic for free.
|
|
163
|
+
* - Client-only tools (name not in `serverTools`) become no-execute
|
|
164
|
+
* entries: the runtime's existing `ClientToolRequest` path handles
|
|
165
|
+
* them — server emits a tool-call request, client executes via its
|
|
166
|
+
* registered handler, client posts back the result.
|
|
167
|
+
*
|
|
168
|
+
* @param serverTools - The server's tool array (e.g. from
|
|
169
|
+
* `[myToolDef.server(...)]`). Pass directly to `chat({ tools })`.
|
|
170
|
+
* @param clientTools - The `tools` array received from
|
|
171
|
+
* `chatParamsFromRequest(...)` / `chatParamsFromRequestBody(...)`.
|
|
172
|
+
* @returns A merged array suitable for `chat({ tools })`.
|
|
173
|
+
*/
|
|
174
|
+
export function mergeAgentTools(
|
|
175
|
+
serverTools: ReadonlyArray<Tool>,
|
|
176
|
+
clientTools: ReadonlyArray<{
|
|
177
|
+
name: string
|
|
178
|
+
description: string
|
|
179
|
+
parameters: JSONSchema
|
|
180
|
+
}>,
|
|
181
|
+
): Array<Tool> {
|
|
182
|
+
const seen = new Set(serverTools.map((t) => t.name))
|
|
183
|
+
const merged: Array<Tool> = [...serverTools]
|
|
184
|
+
for (const ct of clientTools) {
|
|
185
|
+
if (seen.has(ct.name)) {
|
|
186
|
+
// Server wins on name collision.
|
|
187
|
+
continue
|
|
188
|
+
}
|
|
189
|
+
seen.add(ct.name)
|
|
190
|
+
merged.push({
|
|
191
|
+
name: ct.name,
|
|
192
|
+
description: ct.description,
|
|
193
|
+
inputSchema: ct.parameters,
|
|
194
|
+
// No `execute` — runtime treats this as a client-side tool and
|
|
195
|
+
// emits ClientToolRequest events.
|
|
196
|
+
} as Tool)
|
|
197
|
+
}
|
|
198
|
+
return merged
|
|
199
|
+
}
|