@tanstack/ai 0.20.0 → 0.20.1
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 +3 -3
- package/dist/esm/activities/chat/index.js +46 -18
- 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.js +2 -0
- package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
- package/dist/esm/activities/chat/middleware/types.d.ts +5 -5
- 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 +1 -1
- package/dist/esm/activities/chat/tools/schema-converter.js +36 -35
- 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/index.d.ts +1 -0
- package/dist/esm/index.js +2 -0
- 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 +24 -14
- 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/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.map +1 -1
- package/dist/esm/utilities/chat-params.js.map +1 -1
- package/package.json +2 -2
- package/src/activities/chat/index.ts +34 -26
- package/src/activities/chat/messages.ts +2 -1
- package/src/activities/chat/middleware/types.ts +11 -9
- package/src/activities/chat/stream/processor.ts +19 -19
- package/src/activities/chat/stream/strategies.ts +3 -3
- package/src/activities/chat/tools/schema-converter.ts +77 -52
- 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 +5 -0
- 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/index.ts +1 -0
- package/src/middlewares/content-guard.ts +11 -3
- package/src/middlewares/otel.ts +25 -14
- package/src/realtime/index.ts +1 -1
- package/src/tools/provider-tool.ts +14 -0
- package/src/types.ts +12 -8
- package/src/utilities/ag-ui-wire.ts +2 -2
- package/src/utilities/chat-params.ts +2 -2
|
@@ -95,14 +95,16 @@ export interface TextActivityOptions<
|
|
|
95
95
|
*
|
|
96
96
|
* The three shapes can be mixed in a single array (e.g., when forwarding a wire payload that includes both anchor UIMessages and AG-UI fan-out ModelMessages).
|
|
97
97
|
*/
|
|
98
|
-
messages?:
|
|
99
|
-
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
98
|
+
messages?:
|
|
99
|
+
| Array<
|
|
100
|
+
| UIMessage
|
|
101
|
+
| ModelMessage
|
|
102
|
+
| ConstrainedModelMessage<{
|
|
103
|
+
inputModalities: TAdapter['~types']['inputModalities']
|
|
104
|
+
messageMetadataByModality: TAdapter['~types']['messageMetadataByModality']
|
|
105
|
+
}>
|
|
106
|
+
>
|
|
107
|
+
| undefined
|
|
106
108
|
/**
|
|
107
109
|
* System prompts to prepend to the conversation.
|
|
108
110
|
*
|
|
@@ -112,9 +114,9 @@ export interface TextActivityOptions<
|
|
|
112
114
|
* caching), providers without per-prompt metadata reject the field
|
|
113
115
|
* entirely.
|
|
114
116
|
*/
|
|
115
|
-
systemPrompts?:
|
|
116
|
-
SystemPrompt<TAdapter['~types']['systemPromptMetadata']
|
|
117
|
-
|
|
117
|
+
systemPrompts?:
|
|
118
|
+
| Array<SystemPrompt<TAdapter['~types']['systemPromptMetadata']>>
|
|
119
|
+
| undefined
|
|
118
120
|
/**
|
|
119
121
|
* Tools for function calling (auto-executed when called).
|
|
120
122
|
*
|
|
@@ -125,10 +127,12 @@ export interface TextActivityOptions<
|
|
|
125
127
|
* `supports.tools` list. Passing an unsupported tool produces a
|
|
126
128
|
* compile-time error on the array element.
|
|
127
129
|
*/
|
|
128
|
-
tools?:
|
|
129
|
-
|
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
tools?:
|
|
131
|
+
| Array<
|
|
132
|
+
| (Tool & { readonly '~toolKind'?: never })
|
|
133
|
+
| ProviderTool<string, TAdapter['~types']['toolCapabilities'][number]>
|
|
134
|
+
>
|
|
135
|
+
| undefined
|
|
132
136
|
/** Controls the randomness of the output. Higher values make output more random. Range: [0.0, 2.0] */
|
|
133
137
|
temperature?: TextOptions['temperature']
|
|
134
138
|
/** Nucleus sampling parameter. The model considers tokens with topP probability mass. */
|
|
@@ -323,7 +327,7 @@ class TextEngine<
|
|
|
323
327
|
[]
|
|
324
328
|
private currentThinkingContent = ''
|
|
325
329
|
private currentThinkingSignature = ''
|
|
326
|
-
private eventOptions?: Record<string, unknown>
|
|
330
|
+
private eventOptions?: Record<string, unknown> | undefined
|
|
327
331
|
private eventToolNames?: Array<string>
|
|
328
332
|
private finishedEvent: RunFinishedEvent | null = null
|
|
329
333
|
private earlyTermination = false
|
|
@@ -334,16 +338,16 @@ class TextEngine<
|
|
|
334
338
|
private readonly initialClientToolResults: Map<string, any>
|
|
335
339
|
|
|
336
340
|
// AG-UI protocol IDs
|
|
337
|
-
private threadId: string
|
|
338
|
-
private runIdOverride?: string
|
|
339
|
-
private parentRunIdOverride?: string
|
|
341
|
+
private readonly threadId: string
|
|
342
|
+
private readonly runIdOverride?: string
|
|
343
|
+
private readonly parentRunIdOverride?: string
|
|
340
344
|
|
|
341
345
|
// Middleware support
|
|
342
346
|
private readonly middlewareRunner: MiddlewareRunner
|
|
343
347
|
private readonly middlewareCtx: ChatMiddlewareContext
|
|
344
348
|
private readonly deferredPromises: Array<Promise<unknown>> = []
|
|
345
349
|
private abortReason?: string
|
|
346
|
-
private middlewareAbortController?: AbortController
|
|
350
|
+
private readonly middlewareAbortController?: AbortController
|
|
347
351
|
private terminalHookCalled = false
|
|
348
352
|
|
|
349
353
|
private readonly logger: InternalLogger
|
|
@@ -402,8 +406,11 @@ class TextEngine<
|
|
|
402
406
|
// handleStreamChunk processes raw chunks BEFORE middleware, so internal
|
|
403
407
|
// state management sees extended fields (finishReason, delta, toolCallName, etc.).
|
|
404
408
|
// The strip middleware ensures the yielded public stream is AG-UI spec-compliant.
|
|
405
|
-
|
|
406
|
-
|
|
409
|
+
// `devtoolsMiddleware()` returns a structurally compatible
|
|
410
|
+
// `DevtoolsChatMiddleware` (defined in `@tanstack/ai-event-client` to
|
|
411
|
+
// avoid a circular dep). Cast it to `ChatMiddleware` for the runner.
|
|
412
|
+
const allMiddleware: Array<ChatMiddleware> = [
|
|
413
|
+
devtoolsMiddleware() as ChatMiddleware,
|
|
407
414
|
...(config.middleware || []),
|
|
408
415
|
stripToSpecMiddleware(),
|
|
409
416
|
]
|
|
@@ -703,6 +710,7 @@ class TextEngine<
|
|
|
703
710
|
}
|
|
704
711
|
|
|
705
712
|
private handleStreamChunk(chunk: StreamChunk): void {
|
|
713
|
+
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check -- AG-UI EventType enum members vs string-literal case labels; default branch handles untraced events.
|
|
706
714
|
switch (chunk.type) {
|
|
707
715
|
// AG-UI Events
|
|
708
716
|
case 'TEXT_MESSAGE_CONTENT':
|
|
@@ -1001,11 +1009,10 @@ class TextEngine<
|
|
|
1001
1009
|
return true
|
|
1002
1010
|
})
|
|
1003
1011
|
|
|
1004
|
-
if (undiscoveredLazyResults.length > 0) {
|
|
1005
|
-
const finishEvt = this.finishedEvent!
|
|
1012
|
+
if (undiscoveredLazyResults.length > 0 && this.finishedEvent) {
|
|
1006
1013
|
for (const chunk of this.buildToolResultChunks(
|
|
1007
1014
|
undiscoveredLazyResults,
|
|
1008
|
-
|
|
1015
|
+
this.finishedEvent,
|
|
1009
1016
|
)) {
|
|
1010
1017
|
yield* this.pipeThroughMiddleware(chunk)
|
|
1011
1018
|
}
|
|
@@ -1702,8 +1709,9 @@ async function* runStreamingText(
|
|
|
1702
1709
|
function runNonStreamingText(
|
|
1703
1710
|
options: TextActivityOptions<AnyTextAdapter, undefined, false>,
|
|
1704
1711
|
): Promise<string> {
|
|
1705
|
-
// Run the streaming text and collect all text using streamToText
|
|
1712
|
+
// Run the streaming text and collect all text using streamToText.
|
|
1706
1713
|
const stream = runStreamingText(
|
|
1714
|
+
// eslint-disable-next-line no-restricted-syntax -- generic-stream remap: caller is non-streaming (false), but runStreamingText is invoked internally to collect text; concrete `false`→`true` literals don't structurally overlap.
|
|
1707
1715
|
options as unknown as TextActivityOptions<AnyTextAdapter, undefined, true>,
|
|
1708
1716
|
)
|
|
1709
1717
|
|
|
@@ -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
|
})
|
|
@@ -79,9 +79,9 @@ export interface ChatMiddlewareContext {
|
|
|
79
79
|
/** Names of configured tools, if any */
|
|
80
80
|
toolNames?: Array<string>
|
|
81
81
|
/** Flattened generation options (temperature, topP, maxTokens, metadata) */
|
|
82
|
-
options?: Record<string, unknown>
|
|
82
|
+
options?: Record<string, unknown> | undefined
|
|
83
83
|
/** Provider-specific model options */
|
|
84
|
-
modelOptions?: Record<string, unknown>
|
|
84
|
+
modelOptions?: Record<string, unknown> | undefined
|
|
85
85
|
|
|
86
86
|
// --- Computed info ---
|
|
87
87
|
|
|
@@ -121,8 +121,8 @@ export interface ChatMiddlewareConfig {
|
|
|
121
121
|
temperature?: number
|
|
122
122
|
topP?: number
|
|
123
123
|
maxTokens?: number
|
|
124
|
-
metadata?: Record<string, unknown>
|
|
125
|
-
modelOptions?: Record<string, unknown>
|
|
124
|
+
metadata?: Record<string, unknown> | undefined
|
|
125
|
+
modelOptions?: Record<string, unknown> | undefined
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
// ===========================
|
|
@@ -257,11 +257,13 @@ export interface FinishInfo {
|
|
|
257
257
|
/** Final accumulated text content */
|
|
258
258
|
content: string
|
|
259
259
|
/** Final usage totals, if available */
|
|
260
|
-
usage?:
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
260
|
+
usage?:
|
|
261
|
+
| {
|
|
262
|
+
promptTokens: number
|
|
263
|
+
completionTokens: number
|
|
264
|
+
totalTokens: number
|
|
265
|
+
}
|
|
266
|
+
| undefined
|
|
265
267
|
}
|
|
266
268
|
|
|
267
269
|
/**
|
|
@@ -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
|
|
@@ -480,6 +480,7 @@ export class StreamProcessor {
|
|
|
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
482
|
const c = chunk as StreamChunk & { type: string }
|
|
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,9 +56,9 @@ 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'] &&
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime guard for caller-provided unknown; in-operator narrows but doesn't validate the wire payload
|
|
43
62
|
schema['~standard'].version === 1 &&
|
|
44
63
|
'validate' in schema['~standard'] &&
|
|
45
64
|
typeof schema['~standard'].validate === 'function'
|
|
@@ -58,19 +77,20 @@ export function isStandardSchema(schema: unknown): schema is StandardSchemaV1 {
|
|
|
58
77
|
* @returns Transformed schema compatible with OpenAI structured output
|
|
59
78
|
*/
|
|
60
79
|
function makeStructuredOutputCompatible(
|
|
61
|
-
schema:
|
|
80
|
+
schema: JSONSchema,
|
|
62
81
|
originalRequired: Array<string> = [],
|
|
63
|
-
):
|
|
64
|
-
const result = { ...schema }
|
|
82
|
+
): JSONSchema {
|
|
83
|
+
const result: JSONSchema = { ...schema }
|
|
65
84
|
|
|
66
85
|
// Handle object types
|
|
67
86
|
if (result.type === 'object' && result.properties) {
|
|
68
|
-
const properties = { ...result.properties }
|
|
87
|
+
const properties: Record<string, JSONSchema> = { ...result.properties }
|
|
69
88
|
const allPropertyNames = Object.keys(properties)
|
|
70
89
|
|
|
71
90
|
// Transform each property
|
|
72
91
|
for (const propName of allPropertyNames) {
|
|
73
92
|
const prop = properties[propName]
|
|
93
|
+
if (!prop) continue
|
|
74
94
|
const wasOptional = !originalRequired.includes(propName)
|
|
75
95
|
|
|
76
96
|
// Recursively transform nested objects/arrays
|
|
@@ -83,12 +103,12 @@ function makeStructuredOutputCompatible(
|
|
|
83
103
|
? { ...transformed, type: ['object', 'null'] }
|
|
84
104
|
: transformed
|
|
85
105
|
} else if (prop.type === 'array' && prop.items) {
|
|
86
|
-
const
|
|
106
|
+
const items = Array.isArray(prop.items) ? prop.items[0] : prop.items
|
|
107
|
+
const transformed: JSONSchema = {
|
|
87
108
|
...prop,
|
|
88
|
-
items:
|
|
89
|
-
|
|
90
|
-
prop.items
|
|
91
|
-
),
|
|
109
|
+
items: items
|
|
110
|
+
? makeStructuredOutputCompatible(items, items.required || [])
|
|
111
|
+
: prop.items,
|
|
92
112
|
}
|
|
93
113
|
properties[propName] = wasOptional
|
|
94
114
|
? { ...transformed, type: ['array', 'null'] }
|
|
@@ -118,10 +138,10 @@ function makeStructuredOutputCompatible(
|
|
|
118
138
|
|
|
119
139
|
// Handle array types with object items
|
|
120
140
|
if (result.type === 'array' && result.items) {
|
|
121
|
-
result.items
|
|
122
|
-
|
|
123
|
-
result.items.required || []
|
|
124
|
-
|
|
141
|
+
const items = Array.isArray(result.items) ? result.items[0] : result.items
|
|
142
|
+
if (items) {
|
|
143
|
+
result.items = makeStructuredOutputCompatible(items, items.required || [])
|
|
144
|
+
}
|
|
125
145
|
}
|
|
126
146
|
|
|
127
147
|
return result
|
|
@@ -216,42 +236,33 @@ export function convertSchemaToJsonSchema(
|
|
|
216
236
|
target: 'draft-07',
|
|
217
237
|
})
|
|
218
238
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const { $schema, ...rest } = result
|
|
224
|
-
result = rest
|
|
225
|
-
}
|
|
239
|
+
// Rebuild structurally so the typed JSONSchema view is acquired without
|
|
240
|
+
// a `Record<string, unknown> as JSONSchema` cast; `toJsonSchema()` also
|
|
241
|
+
// drops the `$schema` key which LLM providers don't need.
|
|
242
|
+
let result: JSONSchema = toJsonSchema(jsonSchema)
|
|
226
243
|
|
|
227
244
|
// Ensure object schemas always have type: "object"
|
|
245
|
+
// If it has properties (even empty), it should be an object type
|
|
246
|
+
if ('properties' in result && !result.type) {
|
|
247
|
+
result.type = 'object'
|
|
248
|
+
}
|
|
228
249
|
|
|
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
|
-
}
|
|
250
|
+
// Ensure properties exists for object types (even if empty)
|
|
251
|
+
if (result.type === 'object' && !('properties' in result)) {
|
|
252
|
+
result.properties = {}
|
|
253
|
+
}
|
|
239
254
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
255
|
+
// Ensure required exists for object types (even if empty array)
|
|
256
|
+
if (result.type === 'object' && !('required' in result)) {
|
|
257
|
+
result.required = []
|
|
258
|
+
}
|
|
244
259
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
result,
|
|
249
|
-
(result.required as Array<string>) || [],
|
|
250
|
-
)
|
|
251
|
-
}
|
|
260
|
+
// Apply structured output transformation if requested
|
|
261
|
+
if (forStructuredOutput) {
|
|
262
|
+
result = makeStructuredOutputCompatible(result, result.required || [])
|
|
252
263
|
}
|
|
253
264
|
|
|
254
|
-
return result
|
|
265
|
+
return result
|
|
255
266
|
}
|
|
256
267
|
|
|
257
268
|
// Detect Standard Schema validators (Zod, ArkType, Valibot, …) that don't
|
|
@@ -271,14 +282,25 @@ export function convertSchemaToJsonSchema(
|
|
|
271
282
|
// If it's not a Standard JSON Schema, assume it's already a JSONSchema and pass through
|
|
272
283
|
// Still apply structured output transformation if requested
|
|
273
284
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
285
|
+
// At this branch, `schema` is the plain `JSONSchema` arm of `SchemaInput`
|
|
286
|
+
// (the two `~standard` arms were handled above). When no transformation
|
|
287
|
+
// is requested we pass the schema through by reference to preserve
|
|
288
|
+
// identity for callers that compare via `===`.
|
|
289
|
+
if (typeof schema !== 'object') {
|
|
290
|
+
// The SchemaInput union is object-shaped on every arm; if we ever hit a
|
|
291
|
+
// non-object here, propagate it untouched and let the downstream
|
|
292
|
+
// provider error loudly rather than silently widen.
|
|
293
|
+
return schema
|
|
279
294
|
}
|
|
280
295
|
|
|
281
|
-
|
|
296
|
+
if (forStructuredOutput) {
|
|
297
|
+
// Build a typed view structurally so we don't need a SchemaInput→JSONSchema
|
|
298
|
+
// cast on the transformation path.
|
|
299
|
+
const typedView = toJsonSchema(schema)
|
|
300
|
+
return makeStructuredOutputCompatible(typedView, typedView.required || [])
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return schema
|
|
282
304
|
}
|
|
283
305
|
|
|
284
306
|
/**
|
|
@@ -293,7 +315,10 @@ export async function validateWithStandardSchema<T>(
|
|
|
293
315
|
data: unknown,
|
|
294
316
|
): Promise<
|
|
295
317
|
| { success: true; data: T }
|
|
296
|
-
| {
|
|
318
|
+
| {
|
|
319
|
+
success: false
|
|
320
|
+
issues: Array<{ message: string; path?: Array<string> | undefined }>
|
|
321
|
+
}
|
|
297
322
|
> {
|
|
298
323
|
if (!isStandardSchema(schema)) {
|
|
299
324
|
// If it's not a Standard Schema, just return the data as-is
|
|
@@ -81,8 +81,8 @@ export class MiddlewareAbortError extends Error {
|
|
|
81
81
|
* ```
|
|
82
82
|
*/
|
|
83
83
|
export class ToolCallManager {
|
|
84
|
-
private toolCallsMap = new Map<number, ToolCall>()
|
|
85
|
-
private tools: ReadonlyArray<Tool>
|
|
84
|
+
private readonly toolCallsMap = new Map<number, ToolCall>()
|
|
85
|
+
private readonly tools: ReadonlyArray<Tool>
|
|
86
86
|
|
|
87
87
|
constructor(tools: ReadonlyArray<Tool>) {
|
|
88
88
|
this.tools = tools
|
|
@@ -320,14 +320,16 @@ async function* executeWithEventPolling<T>(
|
|
|
320
320
|
])
|
|
321
321
|
|
|
322
322
|
// Flush any pending events
|
|
323
|
-
|
|
324
|
-
|
|
323
|
+
let event: CustomEvent | undefined
|
|
324
|
+
while ((event = pendingEvents.shift()) !== undefined) {
|
|
325
|
+
yield event
|
|
325
326
|
}
|
|
326
327
|
}
|
|
327
328
|
|
|
328
329
|
// Final flush in case events were emitted right at completion
|
|
329
|
-
|
|
330
|
-
|
|
330
|
+
let event: CustomEvent | undefined
|
|
331
|
+
while ((event = pendingEvents.shift()) !== undefined) {
|
|
332
|
+
yield event
|
|
331
333
|
}
|
|
332
334
|
|
|
333
335
|
return state.result
|
|
@@ -404,13 +406,17 @@ async function* executeServerTool(
|
|
|
404
406
|
): AsyncGenerator<CustomEvent, void, void> {
|
|
405
407
|
const startTime = Date.now()
|
|
406
408
|
try {
|
|
407
|
-
|
|
409
|
+
if (!tool.execute) {
|
|
410
|
+
throw new Error(`Tool ${toolName} has no execute() implementation`)
|
|
411
|
+
}
|
|
412
|
+
const executionPromise = Promise.resolve(tool.execute(input, context))
|
|
408
413
|
let result = yield* executeWithEventPolling(executionPromise, pendingEvents)
|
|
409
414
|
const duration = Date.now() - startTime
|
|
410
415
|
|
|
411
416
|
// Flush remaining events
|
|
412
|
-
|
|
413
|
-
|
|
417
|
+
let pendingEvent: CustomEvent | undefined
|
|
418
|
+
while ((pendingEvent = pendingEvents.shift()) !== undefined) {
|
|
419
|
+
yield pendingEvent
|
|
414
420
|
}
|
|
415
421
|
|
|
416
422
|
// Validate output against outputSchema if provided
|
|
@@ -448,8 +454,9 @@ async function* executeServerTool(
|
|
|
448
454
|
const duration = Date.now() - startTime
|
|
449
455
|
|
|
450
456
|
// Flush remaining events
|
|
451
|
-
|
|
452
|
-
|
|
457
|
+
let pendingEvent: CustomEvent | undefined
|
|
458
|
+
while ((pendingEvent = pendingEvents.shift()) !== undefined) {
|
|
459
|
+
yield pendingEvent
|
|
453
460
|
}
|
|
454
461
|
|
|
455
462
|
if (error instanceof MiddlewareAbortError) {
|
|
@@ -29,6 +29,11 @@ export interface ClientTool<
|
|
|
29
29
|
__toolSide: 'client'
|
|
30
30
|
name: TName
|
|
31
31
|
description: string
|
|
32
|
+
// Note: `inputSchema` / `outputSchema` stay as bare optionals (not
|
|
33
|
+
// widened to `| undefined`). They participate in inference via
|
|
34
|
+
// `InferToolInput` / `InferToolOutput` — widening with `| undefined`
|
|
35
|
+
// breaks the `infer TInput extends StandardJSONSchemaV1<...>` chain
|
|
36
|
+
// because `undefined` doesn't extend the schema constraint.
|
|
32
37
|
inputSchema?: TInput
|
|
33
38
|
outputSchema?: TOutput
|
|
34
39
|
needsApproval?: boolean
|
|
@@ -215,7 +220,7 @@ export function toolDefinition<
|
|
|
215
220
|
return {
|
|
216
221
|
__toolSide: 'client',
|
|
217
222
|
...config,
|
|
218
|
-
execute,
|
|
223
|
+
...(execute !== undefined && { execute }),
|
|
219
224
|
}
|
|
220
225
|
},
|
|
221
226
|
}
|
|
@@ -27,10 +27,11 @@ export const kind = 'audio' as const
|
|
|
27
27
|
/**
|
|
28
28
|
* Extract provider options from an AudioAdapter via ~types.
|
|
29
29
|
*/
|
|
30
|
-
export type AudioProviderOptions<TAdapter> =
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
export type AudioProviderOptions<TAdapter> = TAdapter extends {
|
|
31
|
+
'~types': { providerOptions: infer P extends object }
|
|
32
|
+
}
|
|
33
|
+
? P
|
|
34
|
+
: object
|
|
34
35
|
|
|
35
36
|
// ===========================
|
|
36
37
|
// Activity Options Type
|
|
@@ -227,6 +227,11 @@ async function runGenerateImage<
|
|
|
227
227
|
requestId,
|
|
228
228
|
provider: adapter.name,
|
|
229
229
|
model,
|
|
230
|
+
// GeneratedImage is a discriminated `{ url } | { b64Json }` union, but the
|
|
231
|
+
// wire shape on the devtools event is a plain optional pair. Use
|
|
232
|
+
// conditional spreads so the emitted record only sets the field actually
|
|
233
|
+
// present — `exactOptionalPropertyTypes` rejects `field: undefined`
|
|
234
|
+
// against `field?: string` targets.
|
|
230
235
|
images: result.images.map((image) => ({
|
|
231
236
|
url: image.url,
|
|
232
237
|
b64Json: image.b64Json,
|