@tanstack/ai 0.20.0 → 0.21.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.js +3 -1
- package/dist/esm/activities/chat/adapter.js.map +1 -1
- package/dist/esm/activities/chat/index.d.ts +4 -4
- package/dist/esm/activities/chat/index.js +403 -276
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.js +1 -1
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/middleware/compose.d.ts +10 -1
- package/dist/esm/activities/chat/middleware/compose.js +57 -0
- package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
- package/dist/esm/activities/chat/middleware/index.d.ts +1 -1
- package/dist/esm/activities/chat/middleware/types.d.ts +40 -8
- package/dist/esm/activities/chat/stream/processor.d.ts +8 -8
- package/dist/esm/activities/chat/stream/processor.js +29 -21
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/stream/strategies.d.ts +3 -3
- package/dist/esm/activities/chat/stream/strategies.js +4 -4
- package/dist/esm/activities/chat/stream/strategies.js.map +1 -1
- package/dist/esm/activities/chat/tools/lazy-tool-manager.js +5 -0
- package/dist/esm/activities/chat/tools/lazy-tool-manager.js.map +1 -1
- package/dist/esm/activities/chat/tools/schema-converter.d.ts +13 -2
- package/dist/esm/activities/chat/tools/schema-converter.js +47 -37
- package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.d.ts +2 -2
- package/dist/esm/activities/chat/tools/tool-calls.js +17 -9
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-definition.js +1 -1
- package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
- package/dist/esm/activities/generateAudio/adapter.js +3 -1
- package/dist/esm/activities/generateAudio/adapter.js.map +1 -1
- package/dist/esm/activities/generateAudio/index.d.ts +5 -1
- package/dist/esm/activities/generateAudio/index.js.map +1 -1
- package/dist/esm/activities/generateImage/adapter.js +3 -1
- package/dist/esm/activities/generateImage/adapter.js.map +1 -1
- package/dist/esm/activities/generateImage/index.js +5 -0
- package/dist/esm/activities/generateImage/index.js.map +1 -1
- package/dist/esm/activities/generateSpeech/adapter.js +3 -1
- package/dist/esm/activities/generateSpeech/adapter.js.map +1 -1
- package/dist/esm/activities/generateTranscription/adapter.js +3 -1
- package/dist/esm/activities/generateTranscription/adapter.js.map +1 -1
- package/dist/esm/activities/generateVideo/adapter.js +3 -1
- package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
- package/dist/esm/activities/stream-generation-result.js +6 -2
- package/dist/esm/activities/stream-generation-result.js.map +1 -1
- package/dist/esm/activities/summarize/adapter.js +3 -1
- package/dist/esm/activities/summarize/adapter.js.map +1 -1
- package/dist/esm/activities/summarize/chat-stream-summarize.d.ts +1 -1
- package/dist/esm/activities/summarize/chat-stream-summarize.js +5 -0
- package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
- package/dist/esm/activities/summarize/index.js.map +1 -1
- package/dist/esm/extend-adapter.js.map +1 -1
- package/dist/esm/index.d.ts +3 -2
- package/dist/esm/index.js +4 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/logger/internal-logger.js +2 -0
- package/dist/esm/logger/internal-logger.js.map +1 -1
- package/dist/esm/middlewares/content-guard.js +5 -4
- package/dist/esm/middlewares/content-guard.js.map +1 -1
- package/dist/esm/middlewares/otel.js +25 -18
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/dist/esm/realtime/index.d.ts +1 -1
- package/dist/esm/realtime/index.js.map +1 -1
- package/dist/esm/strip-to-spec-middleware.js.map +1 -1
- package/dist/esm/tools/provider-tool.d.ts +9 -0
- package/dist/esm/tools/provider-tool.js +7 -0
- package/dist/esm/tools/provider-tool.js.map +1 -0
- package/dist/esm/types.d.ts +6 -6
- package/dist/esm/utilities/ag-ui-wire.js +4 -1
- package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
- package/dist/esm/utilities/chat-params.js.map +1 -1
- package/package.json +2 -2
- package/skills/ai-core/middleware/SKILL.md +124 -18
- package/skills/ai-core/structured-outputs/SKILL.md +13 -0
- package/src/activities/chat/index.ts +685 -394
- package/src/activities/chat/messages.ts +3 -2
- package/src/activities/chat/middleware/compose.ts +65 -5
- package/src/activities/chat/middleware/index.ts +1 -0
- package/src/activities/chat/middleware/types.ts +64 -11
- package/src/activities/chat/stream/processor.ts +21 -21
- package/src/activities/chat/stream/strategies.ts +3 -3
- package/src/activities/chat/tools/schema-converter.ts +98 -58
- package/src/activities/chat/tools/tool-calls.ts +18 -11
- package/src/activities/chat/tools/tool-definition.ts +6 -1
- package/src/activities/generateAudio/index.ts +5 -4
- package/src/activities/generateImage/index.ts +8 -3
- package/src/activities/stream-generation-result.ts +11 -2
- package/src/activities/summarize/chat-stream-summarize.ts +5 -2
- package/src/activities/summarize/index.ts +2 -2
- package/src/extend-adapter.ts +1 -1
- package/src/index.ts +6 -1
- package/src/middlewares/content-guard.ts +12 -4
- package/src/middlewares/otel.ts +32 -24
- package/src/realtime/index.ts +1 -1
- package/src/strip-to-spec-middleware.ts +1 -4
- package/src/tools/provider-tool.ts +14 -0
- package/src/types.ts +12 -8
- package/src/utilities/ag-ui-wire.ts +4 -3
- package/src/utilities/chat-params.ts +2 -2
|
@@ -114,7 +114,7 @@ export function convertMessagesToModelMessages(
|
|
|
114
114
|
modelMessages.push({
|
|
115
115
|
role: 'system' as ModelMessage['role'],
|
|
116
116
|
content: (msg as { content: string }).content,
|
|
117
|
-
}
|
|
117
|
+
})
|
|
118
118
|
continue
|
|
119
119
|
}
|
|
120
120
|
|
|
@@ -463,12 +463,13 @@ export function modelMessagesToUIMessages(
|
|
|
463
463
|
if (msg.role === 'tool') {
|
|
464
464
|
// Tool result - merge into the last assistant message if possible
|
|
465
465
|
if (
|
|
466
|
+
msg.toolCallId !== undefined &&
|
|
466
467
|
currentAssistantMessage &&
|
|
467
468
|
currentAssistantMessage.role === 'assistant'
|
|
468
469
|
) {
|
|
469
470
|
currentAssistantMessage.parts.push({
|
|
470
471
|
type: 'tool-result',
|
|
471
|
-
toolCallId: msg.toolCallId
|
|
472
|
+
toolCallId: msg.toolCallId,
|
|
472
473
|
content: getTextContent(msg.content),
|
|
473
474
|
state: 'complete',
|
|
474
475
|
})
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
ErrorInfo,
|
|
12
12
|
FinishInfo,
|
|
13
13
|
IterationInfo,
|
|
14
|
+
StructuredOutputMiddlewareConfig,
|
|
14
15
|
ToolCallHookContext,
|
|
15
16
|
ToolPhaseCompleteInfo,
|
|
16
17
|
UsageInfo,
|
|
@@ -71,7 +72,7 @@ export class MiddlewareRunner {
|
|
|
71
72
|
current = { ...current, ...result }
|
|
72
73
|
if (!skip) {
|
|
73
74
|
this.logger.config(
|
|
74
|
-
`middleware=${mw.name ?? 'unnamed'} keys=${Object.keys(result
|
|
75
|
+
`middleware=${mw.name ?? 'unnamed'} keys=${Object.keys(result).join(',')}`,
|
|
75
76
|
{
|
|
76
77
|
middleware: mw.name ?? 'unnamed',
|
|
77
78
|
changes: result,
|
|
@@ -94,7 +95,66 @@ export class MiddlewareRunner {
|
|
|
94
95
|
...base,
|
|
95
96
|
middlewareName: mw.name || 'unnamed',
|
|
96
97
|
iteration: ctx.iteration,
|
|
97
|
-
changes: result
|
|
98
|
+
changes: result,
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return current
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Pipe config through all middleware onStructuredOutputConfig hooks in order.
|
|
109
|
+
* Each middleware receives the merged config from previous middleware.
|
|
110
|
+
* Partial returns are shallow-merged with the current config.
|
|
111
|
+
*
|
|
112
|
+
* Called once at the structured-output boundary, before runOnConfig at the
|
|
113
|
+
* same boundary (which receives a ChatMiddlewareConfig view, no outputSchema).
|
|
114
|
+
*/
|
|
115
|
+
async runOnStructuredOutputConfig(
|
|
116
|
+
ctx: ChatMiddlewareContext,
|
|
117
|
+
config: StructuredOutputMiddlewareConfig,
|
|
118
|
+
): Promise<StructuredOutputMiddlewareConfig> {
|
|
119
|
+
let current = config
|
|
120
|
+
for (const mw of this.middlewares) {
|
|
121
|
+
if (mw.onStructuredOutputConfig) {
|
|
122
|
+
const skip = shouldSkipInstrumentation(mw)
|
|
123
|
+
const start = Date.now()
|
|
124
|
+
const result = await mw.onStructuredOutputConfig(ctx, current)
|
|
125
|
+
const hasTransform = result !== undefined && result !== null
|
|
126
|
+
if (hasTransform) {
|
|
127
|
+
current = { ...current, ...result }
|
|
128
|
+
if (!skip) {
|
|
129
|
+
this.logger.config(
|
|
130
|
+
`middleware=${mw.name ?? 'unnamed'} keys=${Object.keys(result).join(',')}`,
|
|
131
|
+
{
|
|
132
|
+
middleware: mw.name ?? 'unnamed',
|
|
133
|
+
changes: result,
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (!skip) {
|
|
139
|
+
const base = instrumentCtx(ctx)
|
|
140
|
+
aiEventClient.emit('middleware:hook:executed', {
|
|
141
|
+
...base,
|
|
142
|
+
middlewareName: mw.name || 'unnamed',
|
|
143
|
+
hookName: 'onStructuredOutputConfig',
|
|
144
|
+
iteration: ctx.iteration,
|
|
145
|
+
duration: Date.now() - start,
|
|
146
|
+
hasTransform,
|
|
147
|
+
})
|
|
148
|
+
if (hasTransform) {
|
|
149
|
+
aiEventClient.emit('middleware:config:transformed', {
|
|
150
|
+
...base,
|
|
151
|
+
middlewareName: mw.name || 'unnamed',
|
|
152
|
+
iteration: ctx.iteration,
|
|
153
|
+
// `result` is `Partial<StructuredOutputMiddlewareConfig>` —
|
|
154
|
+
// Object.fromEntries(Object.entries(result)) yields the
|
|
155
|
+
// structural `Record<string, unknown>` the event emitter wants
|
|
156
|
+
// without an `as` cast.
|
|
157
|
+
changes: Object.fromEntries(Object.entries(result)),
|
|
98
158
|
})
|
|
99
159
|
}
|
|
100
160
|
}
|
|
@@ -152,7 +212,7 @@ export class MiddlewareRunner {
|
|
|
152
212
|
const nextChunks: Array<StreamChunk> = []
|
|
153
213
|
for (const c of chunks) {
|
|
154
214
|
// Cast: @ag-ui/core Zod passthrough types prevent direct `.type` access
|
|
155
|
-
const chunkType =
|
|
215
|
+
const chunkType = c.type
|
|
156
216
|
if (!skip) {
|
|
157
217
|
this.logger.middleware(
|
|
158
218
|
`hook=onChunk middleware=${mw.name ?? 'unnamed'} in=${chunkType}`,
|
|
@@ -188,7 +248,7 @@ export class MiddlewareRunner {
|
|
|
188
248
|
nextChunks.push(...result)
|
|
189
249
|
if (!skip) {
|
|
190
250
|
this.logger.middleware(
|
|
191
|
-
`hook=onChunk middleware=${mw.name ?? 'unnamed'} in=${chunkType} out=[${result.map((r: StreamChunk) =>
|
|
251
|
+
`hook=onChunk middleware=${mw.name ?? 'unnamed'} in=${chunkType} out=[${result.map((r: StreamChunk) => r.type).join(',')}]`,
|
|
192
252
|
{
|
|
193
253
|
middleware: mw.name ?? 'unnamed',
|
|
194
254
|
hook: 'onChunk',
|
|
@@ -209,7 +269,7 @@ export class MiddlewareRunner {
|
|
|
209
269
|
nextChunks.push(result)
|
|
210
270
|
if (!skip) {
|
|
211
271
|
this.logger.middleware(
|
|
212
|
-
`hook=onChunk middleware=${mw.name ?? 'unnamed'} in=${chunkType} out=${
|
|
272
|
+
`hook=onChunk middleware=${mw.name ?? 'unnamed'} in=${chunkType} out=${result.type}`,
|
|
213
273
|
{
|
|
214
274
|
middleware: mw.name ?? 'unnamed',
|
|
215
275
|
hook: 'onChunk',
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
JSONSchema,
|
|
3
|
+
ModelMessage,
|
|
4
|
+
StreamChunk,
|
|
5
|
+
Tool,
|
|
6
|
+
ToolCall,
|
|
7
|
+
} from '../../../types'
|
|
2
8
|
import type { SystemPrompt } from '../../../system-prompts'
|
|
3
9
|
|
|
4
10
|
// ===========================
|
|
@@ -12,6 +18,8 @@ import type { SystemPrompt } from '../../../system-prompts'
|
|
|
12
18
|
* - 'modelStream': During model streaming
|
|
13
19
|
* - 'beforeTools': Before tool execution phase
|
|
14
20
|
* - 'afterTools': After tool execution phase
|
|
21
|
+
* - 'structuredOutput': During the final structured-output adapter call (set
|
|
22
|
+
* for chunks from adapter.structuredOutputStream or the synthesized fallback)
|
|
15
23
|
*/
|
|
16
24
|
export type ChatMiddlewarePhase =
|
|
17
25
|
| 'init'
|
|
@@ -19,6 +27,7 @@ export type ChatMiddlewarePhase =
|
|
|
19
27
|
| 'modelStream'
|
|
20
28
|
| 'beforeTools'
|
|
21
29
|
| 'afterTools'
|
|
30
|
+
| 'structuredOutput'
|
|
22
31
|
|
|
23
32
|
/**
|
|
24
33
|
* Stable context object passed to all middleware hooks.
|
|
@@ -79,9 +88,9 @@ export interface ChatMiddlewareContext {
|
|
|
79
88
|
/** Names of configured tools, if any */
|
|
80
89
|
toolNames?: Array<string>
|
|
81
90
|
/** Flattened generation options (temperature, topP, maxTokens, metadata) */
|
|
82
|
-
options?: Record<string, unknown>
|
|
91
|
+
options?: Record<string, unknown> | undefined
|
|
83
92
|
/** Provider-specific model options */
|
|
84
|
-
modelOptions?: Record<string, unknown>
|
|
93
|
+
modelOptions?: Record<string, unknown> | undefined
|
|
85
94
|
|
|
86
95
|
// --- Computed info ---
|
|
87
96
|
|
|
@@ -121,8 +130,26 @@ export interface ChatMiddlewareConfig {
|
|
|
121
130
|
temperature?: number
|
|
122
131
|
topP?: number
|
|
123
132
|
maxTokens?: number
|
|
124
|
-
metadata?: Record<string, unknown>
|
|
125
|
-
modelOptions?: Record<string, unknown>
|
|
133
|
+
metadata?: Record<string, unknown> | undefined
|
|
134
|
+
modelOptions?: Record<string, unknown> | undefined
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Config passed to onStructuredOutputConfig.
|
|
139
|
+
*
|
|
140
|
+
* Mirrors ChatMiddlewareConfig minus `tools` (the final structured-output call
|
|
141
|
+
* is a single typed-response request, not an agentic loop — tools cannot be
|
|
142
|
+
* forwarded to it), plus the `outputSchema` being sent to the provider.
|
|
143
|
+
* Middleware may transform the schema (e.g., inject $defs, strip
|
|
144
|
+
* vendor-incompatible keywords) by returning a partial that includes
|
|
145
|
+
* `outputSchema`.
|
|
146
|
+
*/
|
|
147
|
+
export interface StructuredOutputMiddlewareConfig extends Omit<
|
|
148
|
+
ChatMiddlewareConfig,
|
|
149
|
+
'tools'
|
|
150
|
+
> {
|
|
151
|
+
/** JSON Schema being sent to the provider for structured output. */
|
|
152
|
+
outputSchema: JSONSchema
|
|
126
153
|
}
|
|
127
154
|
|
|
128
155
|
// ===========================
|
|
@@ -257,11 +284,13 @@ export interface FinishInfo {
|
|
|
257
284
|
/** Final accumulated text content */
|
|
258
285
|
content: string
|
|
259
286
|
/** Final usage totals, if available */
|
|
260
|
-
usage?:
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
287
|
+
usage?:
|
|
288
|
+
| {
|
|
289
|
+
promptTokens: number
|
|
290
|
+
completionTokens: number
|
|
291
|
+
totalTokens: number
|
|
292
|
+
}
|
|
293
|
+
| undefined
|
|
265
294
|
}
|
|
266
295
|
|
|
267
296
|
/**
|
|
@@ -335,7 +364,31 @@ export interface ChatMiddleware {
|
|
|
335
364
|
| void
|
|
336
365
|
| null
|
|
337
366
|
| Partial<ChatMiddlewareConfig>
|
|
338
|
-
| Promise<void | Partial<ChatMiddlewareConfig>>
|
|
367
|
+
| Promise<void | null | Partial<ChatMiddlewareConfig>>
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Called at the start of the final structured-output call (when the chat
|
|
371
|
+
* was invoked with outputSchema). Pipes through middleware in order, like
|
|
372
|
+
* onConfig, but with access to the JSON Schema being sent to the provider.
|
|
373
|
+
*
|
|
374
|
+
* Return a partial to shallow-merge into the current config, or void to
|
|
375
|
+
* pass through.
|
|
376
|
+
*
|
|
377
|
+
* Fires BEFORE onConfig at the structured-output boundary. onConfig also
|
|
378
|
+
* re-fires at the same boundary with ctx.phase === 'structuredOutput',
|
|
379
|
+
* receiving the post-onStructuredOutputConfig view of the config (minus
|
|
380
|
+
* outputSchema). Use onConfig for general-purpose transforms that apply
|
|
381
|
+
* to every adapter call; use this hook when you need to transform the
|
|
382
|
+
* outputSchema or apply structured-output-specific behavior.
|
|
383
|
+
*/
|
|
384
|
+
onStructuredOutputConfig?: (
|
|
385
|
+
ctx: ChatMiddlewareContext,
|
|
386
|
+
config: StructuredOutputMiddlewareConfig,
|
|
387
|
+
) =>
|
|
388
|
+
| void
|
|
389
|
+
| null
|
|
390
|
+
| Partial<StructuredOutputMiddlewareConfig>
|
|
391
|
+
| Promise<void | null | Partial<StructuredOutputMiddlewareConfig>>
|
|
339
392
|
|
|
340
393
|
/**
|
|
341
394
|
* Called when the chat run starts (after initial onConfig).
|
|
@@ -133,25 +133,25 @@ export interface StreamProcessorOptions {
|
|
|
133
133
|
* @see docs/chat-architecture.md#adapter-contract — What this class expects from adapters
|
|
134
134
|
*/
|
|
135
135
|
export class StreamProcessor {
|
|
136
|
-
private chunkStrategy: ChunkStrategy
|
|
137
|
-
private events: StreamProcessorEvents
|
|
138
|
-
private jsonParser: { parse: (jsonString: string) => any }
|
|
136
|
+
private readonly chunkStrategy: ChunkStrategy
|
|
137
|
+
private readonly events: StreamProcessorEvents
|
|
138
|
+
private readonly jsonParser: { parse: (jsonString: string) => any }
|
|
139
139
|
private recordingEnabled: boolean
|
|
140
140
|
|
|
141
141
|
// Message state
|
|
142
142
|
private messages: Array<UIMessage> = []
|
|
143
143
|
|
|
144
144
|
// Per-message stream state
|
|
145
|
-
private messageStates: Map<string, MessageStreamState> = new Map()
|
|
146
|
-
private activeMessageIds: Set<string> = new Set()
|
|
147
|
-
private toolCallToMessage: Map<string, string> = new Map()
|
|
145
|
+
private readonly messageStates: Map<string, MessageStreamState> = new Map()
|
|
146
|
+
private readonly activeMessageIds: Set<string> = new Set()
|
|
147
|
+
private readonly toolCallToMessage: Map<string, string> = new Map()
|
|
148
148
|
private pendingManualMessageId: string | null = null
|
|
149
149
|
private pendingThinkingStepId: string | null = null
|
|
150
150
|
|
|
151
|
-
private structuredMessageIds: Set<string> = new Set()
|
|
151
|
+
private readonly structuredMessageIds: Set<string> = new Set()
|
|
152
152
|
|
|
153
153
|
// Run tracking (for concurrent run safety)
|
|
154
|
-
private activeRuns = new Set<string>()
|
|
154
|
+
private readonly activeRuns = new Set<string>()
|
|
155
155
|
|
|
156
156
|
// Shared stream state
|
|
157
157
|
private finishReason: string | null = null
|
|
@@ -216,7 +216,7 @@ export class StreamProcessor {
|
|
|
216
216
|
? [{ type: 'text', content }]
|
|
217
217
|
: content.map((part) => {
|
|
218
218
|
// ContentPart types (text, image, audio, video, document) are compatible with MessagePart
|
|
219
|
-
return part
|
|
219
|
+
return part
|
|
220
220
|
})
|
|
221
221
|
|
|
222
222
|
const userMessage: UIMessage = {
|
|
@@ -479,7 +479,8 @@ export class StreamProcessor {
|
|
|
479
479
|
|
|
480
480
|
// Cast needed: @ag-ui/core Zod passthrough types add `& { [k: string]: unknown }`
|
|
481
481
|
// which prevents TypeScript from narrowing the `type` discriminant in switch.
|
|
482
|
-
const c = chunk
|
|
482
|
+
const c = chunk
|
|
483
|
+
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check -- AG-UI EventType enum members vs string-literal case labels; default branch handles untraced events.
|
|
483
484
|
switch (c.type) {
|
|
484
485
|
// AG-UI Events
|
|
485
486
|
case 'TEXT_MESSAGE_START':
|
|
@@ -645,10 +646,9 @@ export class StreamProcessor {
|
|
|
645
646
|
* Used as fallback for events that don't include a messageId.
|
|
646
647
|
*/
|
|
647
648
|
private getActiveAssistantMessageId(): string | null {
|
|
648
|
-
// Set iteration is insertion-order;
|
|
649
|
-
const ids = Array.from(this.activeMessageIds)
|
|
650
|
-
for (
|
|
651
|
-
const id = ids[i]!
|
|
649
|
+
// Set iteration is insertion-order; reverse-iterate to search from the end
|
|
650
|
+
const ids = Array.from(this.activeMessageIds).reverse()
|
|
651
|
+
for (const id of ids) {
|
|
652
652
|
const state = this.messageStates.get(id)
|
|
653
653
|
if (state && state.role === 'assistant') {
|
|
654
654
|
return id
|
|
@@ -679,8 +679,8 @@ export class StreamProcessor {
|
|
|
679
679
|
// Try active assistant message
|
|
680
680
|
const activeId = this.getActiveAssistantMessageId()
|
|
681
681
|
if (activeId) {
|
|
682
|
-
const state = this.getMessageState(activeId)
|
|
683
|
-
return { messageId: activeId, state }
|
|
682
|
+
const state = this.getMessageState(activeId)
|
|
683
|
+
if (state) return { messageId: activeId, state }
|
|
684
684
|
}
|
|
685
685
|
|
|
686
686
|
// Check if a message with preferredId already exists (reconnect/resume case).
|
|
@@ -781,10 +781,10 @@ export class StreamProcessor {
|
|
|
781
781
|
const existingMsg = this.messages.find((m) => m.id === messageId)
|
|
782
782
|
if (existingMsg) {
|
|
783
783
|
this.activeMessageIds.add(messageId)
|
|
784
|
-
|
|
784
|
+
const existingState = this.messageStates.get(messageId)
|
|
785
|
+
if (!existingState) {
|
|
785
786
|
this.createMessageState(messageId, uiRole)
|
|
786
787
|
} else {
|
|
787
|
-
const existingState = this.messageStates.get(messageId)!
|
|
788
788
|
// If tool calls happened since last text, this TEXT_MESSAGE_START
|
|
789
789
|
// signals a new text segment — reset segment accumulation
|
|
790
790
|
if (existingState.hasToolCallsSinceTextStart) {
|
|
@@ -845,7 +845,7 @@ export class StreamProcessor {
|
|
|
845
845
|
): void {
|
|
846
846
|
this.resetStreamState()
|
|
847
847
|
// AG-UI Message[] is compatible with UIMessage[] at runtime
|
|
848
|
-
this.messages = [...chunk.messages] as
|
|
848
|
+
this.messages = [...chunk.messages] as Array<UIMessage>
|
|
849
849
|
this.emitMessagesChange()
|
|
850
850
|
}
|
|
851
851
|
|
|
@@ -1365,7 +1365,7 @@ export class StreamProcessor {
|
|
|
1365
1365
|
state.currentThinkingStepId = stepId
|
|
1366
1366
|
}
|
|
1367
1367
|
|
|
1368
|
-
const previous = state.thinkingSteps.get(stepId)
|
|
1368
|
+
const previous = state.thinkingSteps.get(stepId) ?? ''
|
|
1369
1369
|
let nextThinking = previous
|
|
1370
1370
|
|
|
1371
1371
|
// Prefer delta over content
|
|
@@ -1900,7 +1900,7 @@ export function createReplayStream(
|
|
|
1900
1900
|
recording: ChunkRecording,
|
|
1901
1901
|
): AsyncIterable<StreamChunk> {
|
|
1902
1902
|
return {
|
|
1903
|
-
// eslint-disable-next-line @typescript-eslint/require-await
|
|
1903
|
+
// eslint-disable-next-line @typescript-eslint/require-await -- async generator required by AsyncIterable contract; body has no await
|
|
1904
1904
|
async *[Symbol.asyncIterator]() {
|
|
1905
1905
|
for (const { chunk } of recording.chunks) {
|
|
1906
1906
|
yield chunk
|
|
@@ -20,7 +20,7 @@ export class ImmediateStrategy implements ChunkStrategy {
|
|
|
20
20
|
* Useful for natural text flow in UI
|
|
21
21
|
*/
|
|
22
22
|
export class PunctuationStrategy implements ChunkStrategy {
|
|
23
|
-
private punctuation = /[.,!?;:\n]/
|
|
23
|
+
private readonly punctuation = /[.,!?;:\n]/
|
|
24
24
|
|
|
25
25
|
shouldEmit(chunk: string, _accumulated: string): boolean {
|
|
26
26
|
return this.punctuation.test(chunk)
|
|
@@ -34,7 +34,7 @@ export class PunctuationStrategy implements ChunkStrategy {
|
|
|
34
34
|
export class BatchStrategy implements ChunkStrategy {
|
|
35
35
|
private chunkCount = 0
|
|
36
36
|
|
|
37
|
-
constructor(private batchSize: number = 5) {}
|
|
37
|
+
constructor(private readonly batchSize: number = 5) {}
|
|
38
38
|
|
|
39
39
|
shouldEmit(_chunk: string, _accumulated: string): boolean {
|
|
40
40
|
this.chunkCount++
|
|
@@ -66,7 +66,7 @@ export class WordBoundaryStrategy implements ChunkStrategy {
|
|
|
66
66
|
* Emits if ANY strategy says to emit
|
|
67
67
|
*/
|
|
68
68
|
export class CompositeStrategy implements ChunkStrategy {
|
|
69
|
-
constructor(private strategies: Array<ChunkStrategy>) {}
|
|
69
|
+
constructor(private readonly strategies: Array<ChunkStrategy>) {}
|
|
70
70
|
|
|
71
71
|
shouldEmit(chunk: string, accumulated: string): boolean {
|
|
72
72
|
return this.strategies.some((s) => s.shouldEmit(chunk, accumulated))
|
|
@@ -1,11 +1,29 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
|
2
|
-
|
|
3
1
|
import type {
|
|
4
2
|
StandardJSONSchemaV1,
|
|
5
3
|
StandardSchemaV1,
|
|
6
4
|
} from '@standard-schema/spec'
|
|
7
5
|
import type { JSONSchema, SchemaInput } from '../../../types'
|
|
8
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Build a JSONSchema object from any plain key/value source. The `JSONSchema`
|
|
9
|
+
* interface's `[key: string]: any` index signature makes every property
|
|
10
|
+
* assignable through bracket access without a type cast — copying keys here
|
|
11
|
+
* lets us narrow either `Record<string, unknown>` (returned by
|
|
12
|
+
* `~standard.jsonSchema.input()`) or a `JSONSchema` (from the SchemaInput
|
|
13
|
+
* pass-through arm) into the typed view used by the rest of this module.
|
|
14
|
+
*
|
|
15
|
+
* Accepts `object` so callers don't need a cast when narrowing from union
|
|
16
|
+
* types like `SchemaInput`.
|
|
17
|
+
*/
|
|
18
|
+
function toJsonSchema(obj: object): JSONSchema {
|
|
19
|
+
const result: JSONSchema = {}
|
|
20
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
21
|
+
if (key === '$schema') continue // not needed by LLM providers
|
|
22
|
+
result[key] = value
|
|
23
|
+
}
|
|
24
|
+
return result
|
|
25
|
+
}
|
|
26
|
+
|
|
9
27
|
/**
|
|
10
28
|
* Check if a value is a Standard JSON Schema compliant schema.
|
|
11
29
|
* Standard JSON Schema compliant libraries (Zod v4+, ArkType, Valibot with toStandardJsonSchema, etc.)
|
|
@@ -19,6 +37,7 @@ export function isStandardJSONSchema(
|
|
|
19
37
|
schema !== null &&
|
|
20
38
|
'~standard' in schema &&
|
|
21
39
|
typeof (schema as StandardJSONSchemaV1)['~standard'] === 'object' &&
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime guard for caller-provided unknown; type assertion narrows but doesn't validate the wire payload
|
|
22
41
|
(schema as StandardJSONSchemaV1)['~standard'].version === 1 &&
|
|
23
42
|
typeof (schema as StandardJSONSchemaV1)['~standard'].jsonSchema ===
|
|
24
43
|
'object' &&
|
|
@@ -37,7 +56,6 @@ export function isStandardSchema(schema: unknown): schema is StandardSchemaV1 {
|
|
|
37
56
|
schema !== null &&
|
|
38
57
|
'~standard' in schema &&
|
|
39
58
|
typeof schema['~standard'] === 'object' &&
|
|
40
|
-
schema !== null &&
|
|
41
59
|
schema['~standard'] !== null &&
|
|
42
60
|
'version' in schema['~standard'] &&
|
|
43
61
|
schema['~standard'].version === 1 &&
|
|
@@ -58,19 +76,20 @@ export function isStandardSchema(schema: unknown): schema is StandardSchemaV1 {
|
|
|
58
76
|
* @returns Transformed schema compatible with OpenAI structured output
|
|
59
77
|
*/
|
|
60
78
|
function makeStructuredOutputCompatible(
|
|
61
|
-
schema:
|
|
79
|
+
schema: JSONSchema,
|
|
62
80
|
originalRequired: Array<string> = [],
|
|
63
|
-
):
|
|
64
|
-
const result = { ...schema }
|
|
81
|
+
): JSONSchema {
|
|
82
|
+
const result: JSONSchema = { ...schema }
|
|
65
83
|
|
|
66
84
|
// Handle object types
|
|
67
85
|
if (result.type === 'object' && result.properties) {
|
|
68
|
-
const properties = { ...result.properties }
|
|
86
|
+
const properties: Record<string, JSONSchema> = { ...result.properties }
|
|
69
87
|
const allPropertyNames = Object.keys(properties)
|
|
70
88
|
|
|
71
89
|
// Transform each property
|
|
72
90
|
for (const propName of allPropertyNames) {
|
|
73
91
|
const prop = properties[propName]
|
|
92
|
+
if (!prop) continue
|
|
74
93
|
const wasOptional = !originalRequired.includes(propName)
|
|
75
94
|
|
|
76
95
|
// Recursively transform nested objects/arrays
|
|
@@ -83,12 +102,12 @@ function makeStructuredOutputCompatible(
|
|
|
83
102
|
? { ...transformed, type: ['object', 'null'] }
|
|
84
103
|
: transformed
|
|
85
104
|
} else if (prop.type === 'array' && prop.items) {
|
|
86
|
-
const
|
|
105
|
+
const items = Array.isArray(prop.items) ? prop.items[0] : prop.items
|
|
106
|
+
const transformed: JSONSchema = {
|
|
87
107
|
...prop,
|
|
88
|
-
items:
|
|
89
|
-
|
|
90
|
-
prop.items
|
|
91
|
-
),
|
|
108
|
+
items: items
|
|
109
|
+
? makeStructuredOutputCompatible(items, items.required || [])
|
|
110
|
+
: prop.items,
|
|
92
111
|
}
|
|
93
112
|
properties[propName] = wasOptional
|
|
94
113
|
? { ...transformed, type: ['array', 'null'] }
|
|
@@ -118,10 +137,10 @@ function makeStructuredOutputCompatible(
|
|
|
118
137
|
|
|
119
138
|
// Handle array types with object items
|
|
120
139
|
if (result.type === 'array' && result.items) {
|
|
121
|
-
result.items
|
|
122
|
-
|
|
123
|
-
result.items.required || []
|
|
124
|
-
|
|
140
|
+
const items = Array.isArray(result.items) ? result.items[0] : result.items
|
|
141
|
+
if (items) {
|
|
142
|
+
result.items = makeStructuredOutputCompatible(items, items.required || [])
|
|
143
|
+
}
|
|
125
144
|
}
|
|
126
145
|
|
|
127
146
|
return result
|
|
@@ -216,42 +235,33 @@ export function convertSchemaToJsonSchema(
|
|
|
216
235
|
target: 'draft-07',
|
|
217
236
|
})
|
|
218
237
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const { $schema, ...rest } = result
|
|
224
|
-
result = rest
|
|
225
|
-
}
|
|
238
|
+
// Rebuild structurally so the typed JSONSchema view is acquired without
|
|
239
|
+
// a `Record<string, unknown> as JSONSchema` cast; `toJsonSchema()` also
|
|
240
|
+
// drops the `$schema` key which LLM providers don't need.
|
|
241
|
+
let result: JSONSchema = toJsonSchema(jsonSchema)
|
|
226
242
|
|
|
227
243
|
// Ensure object schemas always have type: "object"
|
|
244
|
+
// If it has properties (even empty), it should be an object type
|
|
245
|
+
if ('properties' in result && !result.type) {
|
|
246
|
+
result.type = 'object'
|
|
247
|
+
}
|
|
228
248
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Ensure properties exists for object types (even if empty)
|
|
236
|
-
if (result.type === 'object' && !('properties' in result)) {
|
|
237
|
-
result.properties = {}
|
|
238
|
-
}
|
|
249
|
+
// Ensure properties exists for object types (even if empty)
|
|
250
|
+
if (result.type === 'object' && !('properties' in result)) {
|
|
251
|
+
result.properties = {}
|
|
252
|
+
}
|
|
239
253
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
254
|
+
// Ensure required exists for object types (even if empty array)
|
|
255
|
+
if (result.type === 'object' && !('required' in result)) {
|
|
256
|
+
result.required = []
|
|
257
|
+
}
|
|
244
258
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
result,
|
|
249
|
-
(result.required as Array<string>) || [],
|
|
250
|
-
)
|
|
251
|
-
}
|
|
259
|
+
// Apply structured output transformation if requested
|
|
260
|
+
if (forStructuredOutput) {
|
|
261
|
+
result = makeStructuredOutputCompatible(result, result.required || [])
|
|
252
262
|
}
|
|
253
263
|
|
|
254
|
-
return result
|
|
264
|
+
return result
|
|
255
265
|
}
|
|
256
266
|
|
|
257
267
|
// Detect Standard Schema validators (Zod, ArkType, Valibot, …) that don't
|
|
@@ -271,14 +281,25 @@ export function convertSchemaToJsonSchema(
|
|
|
271
281
|
// If it's not a Standard JSON Schema, assume it's already a JSONSchema and pass through
|
|
272
282
|
// Still apply structured output transformation if requested
|
|
273
283
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
284
|
+
// At this branch, `schema` is the plain `JSONSchema` arm of `SchemaInput`
|
|
285
|
+
// (the two `~standard` arms were handled above). When no transformation
|
|
286
|
+
// is requested we pass the schema through by reference to preserve
|
|
287
|
+
// identity for callers that compare via `===`.
|
|
288
|
+
if (typeof schema !== 'object') {
|
|
289
|
+
// The SchemaInput union is object-shaped on every arm; if we ever hit a
|
|
290
|
+
// non-object here, propagate it untouched and let the downstream
|
|
291
|
+
// provider error loudly rather than silently widen.
|
|
292
|
+
return schema
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (forStructuredOutput) {
|
|
296
|
+
// Build a typed view structurally so we don't need a SchemaInput→JSONSchema
|
|
297
|
+
// cast on the transformation path.
|
|
298
|
+
const typedView = toJsonSchema(schema)
|
|
299
|
+
return makeStructuredOutputCompatible(typedView, typedView.required || [])
|
|
279
300
|
}
|
|
280
301
|
|
|
281
|
-
return schema
|
|
302
|
+
return schema
|
|
282
303
|
}
|
|
283
304
|
|
|
284
305
|
/**
|
|
@@ -293,7 +314,10 @@ export async function validateWithStandardSchema<T>(
|
|
|
293
314
|
data: unknown,
|
|
294
315
|
): Promise<
|
|
295
316
|
| { success: true; data: T }
|
|
296
|
-
| {
|
|
317
|
+
| {
|
|
318
|
+
success: false
|
|
319
|
+
issues: Array<{ message: string; path?: Array<string> | undefined }>
|
|
320
|
+
}
|
|
297
321
|
> {
|
|
298
322
|
if (!isStandardSchema(schema)) {
|
|
299
323
|
// If it's not a Standard Schema, just return the data as-is
|
|
@@ -315,6 +339,25 @@ export async function validateWithStandardSchema<T>(
|
|
|
315
339
|
}
|
|
316
340
|
}
|
|
317
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Error thrown when Standard Schema validation fails. Carries the original
|
|
344
|
+
* `issues` array so consumers (middleware `onError`, callers catching from
|
|
345
|
+
* `chat({ outputSchema })`) can programmatically inspect each failure.
|
|
346
|
+
*/
|
|
347
|
+
export class StandardSchemaValidationError extends Error {
|
|
348
|
+
override readonly name = 'StandardSchemaValidationError'
|
|
349
|
+
readonly issues: ReadonlyArray<StandardSchemaV1.Issue>
|
|
350
|
+
|
|
351
|
+
constructor(issues: ReadonlyArray<StandardSchemaV1.Issue>) {
|
|
352
|
+
super(
|
|
353
|
+
`Validation failed: ${issues
|
|
354
|
+
.map((i) => i.message || 'Validation failed')
|
|
355
|
+
.join(', ')}`,
|
|
356
|
+
)
|
|
357
|
+
this.issues = issues
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
318
361
|
/**
|
|
319
362
|
* Synchronously validates data against a Standard Schema compliant schema.
|
|
320
363
|
* Note: Some Standard Schema implementations may only support async validation.
|
|
@@ -323,7 +366,8 @@ export async function validateWithStandardSchema<T>(
|
|
|
323
366
|
* @param schema - Standard Schema compliant schema
|
|
324
367
|
* @param data - Data to validate
|
|
325
368
|
* @returns Parsed/validated data
|
|
326
|
-
* @throws
|
|
369
|
+
* @throws StandardSchemaValidationError if validation fails; Error if the
|
|
370
|
+
* schema only supports async validation.
|
|
327
371
|
*/
|
|
328
372
|
export function parseWithStandardSchema<T>(schema: unknown, data: unknown): T {
|
|
329
373
|
if (!isStandardSchema(schema)) {
|
|
@@ -344,9 +388,5 @@ export function parseWithStandardSchema<T>(schema: unknown, data: unknown): T {
|
|
|
344
388
|
return result.value as T
|
|
345
389
|
}
|
|
346
390
|
|
|
347
|
-
|
|
348
|
-
const errorMessages = result.issues
|
|
349
|
-
.map((issue) => issue.message || 'Validation failed')
|
|
350
|
-
.join(', ')
|
|
351
|
-
throw new Error(`Validation failed: ${errorMessages}`)
|
|
391
|
+
throw new StandardSchemaValidationError(result.issues)
|
|
352
392
|
}
|