@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.
Files changed (43) hide show
  1. package/README.md +0 -4
  2. package/dist/esm/activities/chat/index.d.ts +12 -3
  3. package/dist/esm/activities/chat/index.js +75 -7
  4. package/dist/esm/activities/chat/index.js.map +1 -1
  5. package/dist/esm/activities/chat/messages.js +41 -2
  6. package/dist/esm/activities/chat/messages.js.map +1 -1
  7. package/dist/esm/activities/chat/middleware/compose.js +1 -1
  8. package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
  9. package/dist/esm/activities/chat/middleware/types.d.ts +12 -1
  10. package/dist/esm/activities/chat/stream/message-updaters.d.ts +35 -0
  11. package/dist/esm/activities/chat/stream/message-updaters.js +95 -0
  12. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  13. package/dist/esm/activities/chat/stream/processor.d.ts +1 -0
  14. package/dist/esm/activities/chat/stream/processor.js +90 -2
  15. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  16. package/dist/esm/activities/chat/tools/schema-converter.js +5 -0
  17. package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
  18. package/dist/esm/adapter-internals.d.ts +1 -0
  19. package/dist/esm/index.d.ts +3 -0
  20. package/dist/esm/index.js +8 -2
  21. package/dist/esm/index.js.map +1 -1
  22. package/dist/esm/types.d.ts +86 -16
  23. package/dist/esm/utilities/ag-ui-wire.d.ts +44 -0
  24. package/dist/esm/utilities/ag-ui-wire.js +104 -0
  25. package/dist/esm/utilities/ag-ui-wire.js.map +1 -0
  26. package/dist/esm/utilities/chat-params.d.ts +80 -0
  27. package/dist/esm/utilities/chat-params.js +96 -0
  28. package/dist/esm/utilities/chat-params.js.map +1 -0
  29. package/package.json +3 -3
  30. package/skills/ai-core/ag-ui-protocol/SKILL.md +46 -3
  31. package/skills/ai-core/structured-outputs/SKILL.md +240 -47
  32. package/src/activities/chat/index.ts +144 -10
  33. package/src/activities/chat/messages.ts +70 -4
  34. package/src/activities/chat/middleware/compose.ts +1 -1
  35. package/src/activities/chat/middleware/types.ts +12 -1
  36. package/src/activities/chat/stream/message-updaters.ts +171 -0
  37. package/src/activities/chat/stream/processor.ts +137 -2
  38. package/src/activities/chat/tools/schema-converter.ts +14 -0
  39. package/src/adapter-internals.ts +1 -0
  40. package/src/index.ts +11 -0
  41. package/src/types.ts +104 -15
  42. package/src/utilities/ag-ui-wire.ts +201 -0
  43. package/src/utilities/chat-params.ts +199 -0
@@ -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
+ }