@tanstack/ai 0.45.1 → 0.47.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/index.d.ts +36 -11
- package/dist/esm/activities/chat/index.js +462 -66
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.d.ts +1 -0
- package/dist/esm/activities/chat/messages.js +12 -7
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/middleware/builder.d.ts +7 -2
- package/dist/esm/activities/chat/middleware/builder.js.map +1 -1
- package/dist/esm/activities/chat/middleware/compose.d.ts +10 -3
- package/dist/esm/activities/chat/middleware/compose.js +55 -0
- package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
- package/dist/esm/activities/chat/middleware/define.d.ts +6 -3
- package/dist/esm/activities/chat/middleware/define.js.map +1 -1
- package/dist/esm/activities/chat/middleware/generic-interrupts.d.ts +13 -0
- package/dist/esm/activities/chat/middleware/generic-interrupts.js +8 -0
- package/dist/esm/activities/chat/middleware/generic-interrupts.js.map +1 -0
- package/dist/esm/activities/chat/middleware/index.d.ts +4 -1
- package/dist/esm/activities/chat/middleware/types.d.ts +54 -3
- package/dist/esm/activities/chat/middleware/types.js +16 -0
- package/dist/esm/activities/chat/middleware/types.js.map +1 -0
- package/dist/esm/activities/chat/stream/processor.js +18 -5
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/unique-tool-names.d.ts +20 -0
- package/dist/esm/activities/chat/tools/unique-tool-names.js +57 -0
- package/dist/esm/activities/chat/tools/unique-tool-names.js.map +1 -0
- package/dist/esm/adapter-internals.d.ts +7 -0
- package/dist/esm/adapter-internals.js +5 -1
- package/dist/esm/client.d.ts +4 -0
- package/dist/esm/client.js +3 -1
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/generic-interrupt-continuation.d.ts +45 -0
- package/dist/esm/generic-interrupt-continuation.js +80 -0
- package/dist/esm/generic-interrupt-continuation.js.map +1 -0
- package/dist/esm/index.d.ts +9 -1
- package/dist/esm/index.js +7 -2
- package/dist/esm/interrupt-definition.d.ts +113 -0
- package/dist/esm/interrupt-definition.js +169 -0
- package/dist/esm/interrupt-definition.js.map +1 -0
- package/dist/esm/interrupt-resume.d.ts +3 -0
- package/dist/esm/interrupt-resume.js +77 -16
- package/dist/esm/interrupt-resume.js.map +1 -1
- package/dist/esm/interrupts.d.ts +12 -3
- package/dist/esm/interrupts.js.map +1 -1
- package/dist/esm/middlewares/usage-attributes.d.ts +2 -2
- package/dist/esm/middlewares/usage-attributes.js +9 -2
- package/dist/esm/middlewares/usage-attributes.js.map +1 -1
- package/dist/esm/stream-to-response.d.ts +26 -0
- package/dist/esm/stream-to-response.js +1 -1
- package/dist/esm/stream-to-response.js.map +1 -1
- package/dist/esm/stream-to-websocket.d.ts +123 -0
- package/dist/esm/stream-to-websocket.js +249 -0
- package/dist/esm/stream-to-websocket.js.map +1 -0
- package/dist/esm/types.d.ts +19 -10
- package/dist/esm/utilities/chat-params.js +10 -1
- package/dist/esm/utilities/chat-params.js.map +1 -1
- package/package.json +2 -2
- package/skills/ai-core/media-generation/SKILL.md +13 -9
- package/skills/ai-core/middleware/SKILL.md +53 -44
- package/skills/ai-core/structured-outputs/SKILL.md +59 -55
- package/skills/ai-core/tool-calling/SKILL.md +54 -1
- package/src/activities/chat/index.ts +1076 -194
- package/src/activities/chat/messages.ts +11 -3
- package/src/activities/chat/middleware/builder.ts +29 -4
- package/src/activities/chat/middleware/compose.ts +95 -5
- package/src/activities/chat/middleware/define.ts +13 -3
- package/src/activities/chat/middleware/generic-interrupts.ts +26 -0
- package/src/activities/chat/middleware/index.ts +15 -0
- package/src/activities/chat/middleware/types.ts +127 -2
- package/src/activities/chat/stream/processor.ts +21 -0
- package/src/activities/chat/tools/unique-tool-names.ts +73 -0
- package/src/adapter-internals.ts +24 -0
- package/src/client.ts +20 -0
- package/src/generic-interrupt-continuation.ts +162 -0
- package/src/index.ts +51 -0
- package/src/interrupt-definition.ts +581 -0
- package/src/interrupt-resume.ts +156 -25
- package/src/interrupts.ts +13 -3
- package/src/middlewares/usage-attributes.ts +12 -2
- package/src/stream-to-response.ts +2 -2
- package/src/stream-to-websocket.ts +418 -0
- package/src/types.ts +21 -8
- package/src/utilities/chat-params.ts +16 -3
|
@@ -14,10 +14,21 @@ import { EventType } from '../../types'
|
|
|
14
14
|
import {
|
|
15
15
|
INTERRUPT_BINDING_METADATA_KEY,
|
|
16
16
|
InterruptResumeValidationError,
|
|
17
|
+
readInterruptBinding,
|
|
17
18
|
readUnopenedInterruptBinding,
|
|
18
19
|
validateInterruptResumeBatch,
|
|
19
20
|
} from '../../interrupt-resume'
|
|
20
21
|
import { INTERRUPT_BINDING_VERSION } from '../../interrupts'
|
|
22
|
+
import {
|
|
23
|
+
INTERRUPT_PAYLOAD_METADATA_KEY,
|
|
24
|
+
createInterruptBinding,
|
|
25
|
+
rehydrateInterruptRequest,
|
|
26
|
+
} from '../../interrupt-definition'
|
|
27
|
+
import { readGenericInterruptContinuation } from '../../generic-interrupt-continuation'
|
|
28
|
+
import type {
|
|
29
|
+
GenericInterruptRequest,
|
|
30
|
+
InterruptDefinition,
|
|
31
|
+
} from '../../interrupt-definition'
|
|
21
32
|
import {
|
|
22
33
|
canonicalInterruptJson,
|
|
23
34
|
digestInterruptJson,
|
|
@@ -25,6 +36,7 @@ import {
|
|
|
25
36
|
import { normalizeToolResult } from '../../utilities/tool-result'
|
|
26
37
|
import { isProviderExecutedToolCall } from '../../utilities/provider-executed'
|
|
27
38
|
import { LazyToolManager } from './tools/lazy-tool-manager'
|
|
39
|
+
import { assertUniqueToolNames } from './tools/unique-tool-names'
|
|
28
40
|
import {
|
|
29
41
|
MiddlewareAbortError,
|
|
30
42
|
ToolCallManager,
|
|
@@ -42,7 +54,12 @@ import {
|
|
|
42
54
|
} from './tools/approval-schema'
|
|
43
55
|
import { maxIterations as maxIterationsStrategy } from './agent-loop-strategies'
|
|
44
56
|
import { isCancelRequestedReason } from './cancel'
|
|
45
|
-
import {
|
|
57
|
+
import {
|
|
58
|
+
convertMessagesToModelMessages,
|
|
59
|
+
generateMessageId,
|
|
60
|
+
modelMessageToUIMessage,
|
|
61
|
+
safeJsonStringify,
|
|
62
|
+
} from './messages'
|
|
46
63
|
import { MiddlewareRunner } from './middleware/compose'
|
|
47
64
|
import { getRunDetached } from './middleware/run-store'
|
|
48
65
|
import { publishRunDetachedSignal } from '../../delivery-detach'
|
|
@@ -84,6 +101,7 @@ import type {
|
|
|
84
101
|
SchemaInput,
|
|
85
102
|
StreamChunk,
|
|
86
103
|
StructuredOutputCompleteEvent,
|
|
104
|
+
StructuredOutputPart,
|
|
87
105
|
StructuredOutputStream,
|
|
88
106
|
TextMessageContentEvent,
|
|
89
107
|
TextOptions,
|
|
@@ -99,10 +117,13 @@ import type {
|
|
|
99
117
|
ChatMiddleware,
|
|
100
118
|
ChatMiddlewareConfig,
|
|
101
119
|
ChatMiddlewareContext,
|
|
120
|
+
ChatResumeGenericResolution,
|
|
102
121
|
ChatResumeToolState,
|
|
122
|
+
InterruptResolutionCollection,
|
|
103
123
|
SandboxFileHookEvent,
|
|
104
124
|
StructuredOutputMiddlewareConfig,
|
|
105
125
|
} from './middleware/types'
|
|
126
|
+
import { provideGenericInterruptDefinitionRegistry } from './middleware/generic-interrupts'
|
|
106
127
|
import type { CheckCoverage } from './middleware/builder'
|
|
107
128
|
import type { SystemPrompt } from '../../system-prompts'
|
|
108
129
|
import type { InternalLogger } from '../../logger/internal-logger'
|
|
@@ -199,74 +220,11 @@ function normalizePublicInterruptBinding(
|
|
|
199
220
|
value: unknown,
|
|
200
221
|
expectedInterruptId: string,
|
|
201
222
|
): InterruptBinding | undefined {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
)
|
|
208
|
-
if (
|
|
209
|
-
binding.interruptId !== expectedInterruptId ||
|
|
210
|
-
// A binding version we don't recognise belongs to another producer. Drop
|
|
211
|
-
// it rather than reading our fields out of it.
|
|
212
|
-
(binding.v !== undefined && binding.v !== INTERRUPT_BINDING_VERSION) ||
|
|
213
|
-
typeof binding.interruptedRunId !== 'string' ||
|
|
214
|
-
typeof binding.generation !== 'number' ||
|
|
215
|
-
!Number.isInteger(binding.generation) ||
|
|
216
|
-
binding.generation < 0 ||
|
|
217
|
-
typeof binding.responseSchemaHash !== 'string' ||
|
|
218
|
-
(binding.expiresAt !== undefined && typeof binding.expiresAt !== 'string')
|
|
219
|
-
) {
|
|
220
|
-
return undefined
|
|
221
|
-
}
|
|
222
|
-
const base = {
|
|
223
|
-
v: INTERRUPT_BINDING_VERSION,
|
|
224
|
-
interruptId: binding.interruptId,
|
|
225
|
-
interruptedRunId: binding.interruptedRunId,
|
|
226
|
-
generation: binding.generation,
|
|
227
|
-
responseSchemaHash: binding.responseSchemaHash,
|
|
228
|
-
...(typeof binding.expiresAt === 'string'
|
|
229
|
-
? { expiresAt: binding.expiresAt }
|
|
230
|
-
: {}),
|
|
231
|
-
}
|
|
232
|
-
if (binding.kind === 'generic') {
|
|
233
|
-
return { kind: binding.kind, ...base }
|
|
234
|
-
}
|
|
235
|
-
if (
|
|
236
|
-
typeof binding.toolName !== 'string' ||
|
|
237
|
-
typeof binding.toolCallId !== 'string'
|
|
238
|
-
) {
|
|
239
|
-
return undefined
|
|
240
|
-
}
|
|
241
|
-
if (
|
|
242
|
-
binding.kind === 'client-tool-execution' &&
|
|
243
|
-
typeof binding.outputSchemaHash === 'string'
|
|
244
|
-
) {
|
|
245
|
-
return {
|
|
246
|
-
kind: binding.kind,
|
|
247
|
-
...base,
|
|
248
|
-
toolName: binding.toolName,
|
|
249
|
-
toolCallId: binding.toolCallId,
|
|
250
|
-
outputSchemaHash: binding.outputSchemaHash,
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
if (
|
|
254
|
-
binding.kind === 'tool-approval' &&
|
|
255
|
-
Object.prototype.hasOwnProperty.call(binding, 'originalArgs') &&
|
|
256
|
-
typeof binding.inputSchemaHash === 'string' &&
|
|
257
|
-
typeof binding.approvalSchemaHash === 'string'
|
|
258
|
-
) {
|
|
259
|
-
return {
|
|
260
|
-
kind: binding.kind,
|
|
261
|
-
...base,
|
|
262
|
-
toolName: binding.toolName,
|
|
263
|
-
toolCallId: binding.toolCallId,
|
|
264
|
-
originalArgs: binding.originalArgs,
|
|
265
|
-
inputSchemaHash: binding.inputSchemaHash,
|
|
266
|
-
approvalSchemaHash: binding.approvalSchemaHash,
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
return undefined
|
|
223
|
+
return readInterruptBinding({
|
|
224
|
+
id: expectedInterruptId,
|
|
225
|
+
reason: '',
|
|
226
|
+
metadata: { [INTERRUPT_BINDING_METADATA_KEY]: value },
|
|
227
|
+
})
|
|
270
228
|
}
|
|
271
229
|
|
|
272
230
|
// The leaf context-inference primitives (KnownContext, MergeContext,
|
|
@@ -305,33 +263,133 @@ type InferredContext<TTools, TMiddleware> = [
|
|
|
305
263
|
? unknown
|
|
306
264
|
: ContextFromInputs<TTools, TMiddleware>
|
|
307
265
|
|
|
308
|
-
type
|
|
309
|
-
|
|
266
|
+
type RegistryInterrupt<
|
|
267
|
+
TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>,
|
|
268
|
+
> = [TInterrupts[number]] extends [never] ? never : TInterrupts[number]
|
|
269
|
+
|
|
270
|
+
type DuplicateInterruptDefinitionId<
|
|
271
|
+
TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>,
|
|
272
|
+
TSeenIds extends string = never,
|
|
273
|
+
> = TInterrupts extends readonly [infer THead, ...infer TTail]
|
|
274
|
+
? THead extends InterruptDefinition<infer TId, any, any, any>
|
|
275
|
+
? string extends TId
|
|
276
|
+
? TTail extends ReadonlyArray<InterruptDefinition<any, any, any, any>>
|
|
277
|
+
? DuplicateInterruptDefinitionId<TTail, TSeenIds>
|
|
278
|
+
: never
|
|
279
|
+
: TId extends TSeenIds
|
|
280
|
+
? TId
|
|
281
|
+
: TTail extends ReadonlyArray<InterruptDefinition<any, any, any, any>>
|
|
282
|
+
? DuplicateInterruptDefinitionId<TTail, TSeenIds | TId>
|
|
283
|
+
: never
|
|
284
|
+
: never
|
|
285
|
+
: never
|
|
286
|
+
|
|
287
|
+
type CheckUniqueInterruptDefinitions<
|
|
288
|
+
TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>,
|
|
289
|
+
> = [DuplicateInterruptDefinitionId<TInterrupts>] extends [never]
|
|
290
|
+
? unknown
|
|
291
|
+
: {
|
|
292
|
+
readonly '✖ Duplicate interrupt definition id in chat({ interrupts }).': never
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
type InlineChatContext<TTools, TContext> = MergeContext<
|
|
296
|
+
ContextFromArray<NonNullable<TTools>>,
|
|
297
|
+
TContext
|
|
298
|
+
>
|
|
299
|
+
|
|
300
|
+
type RegistryChatMiddleware<
|
|
301
|
+
TContext,
|
|
302
|
+
TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>,
|
|
303
|
+
> = ChatMiddleware<TContext, RegistryInterrupt<TInterrupts>>
|
|
304
|
+
|
|
305
|
+
type MiddlewareInterruptDefinitions<TMiddleware> =
|
|
306
|
+
TMiddleware extends ReadonlyArray<infer TMiddlewareItem>
|
|
307
|
+
? TMiddlewareItem extends ChatMiddleware<any, infer TDefinitions>
|
|
308
|
+
? TDefinitions
|
|
309
|
+
: never
|
|
310
|
+
: never
|
|
311
|
+
|
|
312
|
+
type IsAny<TValue> = 0 extends 1 & TValue ? true : false
|
|
313
|
+
|
|
314
|
+
type CheckInterruptRegistry<
|
|
315
|
+
TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>,
|
|
316
|
+
TMiddleware,
|
|
317
|
+
> =
|
|
318
|
+
IsAny<MiddlewareInterruptDefinitions<TMiddleware>> extends true
|
|
319
|
+
? unknown
|
|
320
|
+
: [MiddlewareInterruptDefinitions<TMiddleware>] extends [never]
|
|
321
|
+
? unknown
|
|
322
|
+
: [
|
|
323
|
+
Exclude<
|
|
324
|
+
MiddlewareInterruptDefinitions<TMiddleware>,
|
|
325
|
+
RegistryInterrupt<TInterrupts>
|
|
326
|
+
>,
|
|
327
|
+
] extends [never]
|
|
328
|
+
? unknown
|
|
329
|
+
: {
|
|
330
|
+
readonly '✖ Middleware emits an interrupt definition that is not registered in chat({ interrupts }).': never
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
type RuntimeContextOption<TTools, TMiddleware, TContext> = [
|
|
334
|
+
MergeContext<ContextFromInputs<TTools, TMiddleware>, TContext>,
|
|
310
335
|
] extends [never]
|
|
311
|
-
? { context?:
|
|
312
|
-
: undefined extends
|
|
313
|
-
|
|
314
|
-
|
|
336
|
+
? { context?: TContext }
|
|
337
|
+
: undefined extends MergeContext<
|
|
338
|
+
ContextFromInputs<TTools, TMiddleware>,
|
|
339
|
+
TContext
|
|
340
|
+
>
|
|
341
|
+
? {
|
|
342
|
+
context?: MergeContext<ContextFromInputs<TTools, TMiddleware>, TContext>
|
|
343
|
+
}
|
|
344
|
+
: {
|
|
345
|
+
context: MergeContext<ContextFromInputs<TTools, TMiddleware>, TContext>
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
type ExactMiddlewareOption<
|
|
349
|
+
TTools,
|
|
350
|
+
TContext,
|
|
351
|
+
TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>,
|
|
352
|
+
TMiddleware extends Array<unknown> | undefined,
|
|
353
|
+
> = [TMiddleware] extends [undefined]
|
|
354
|
+
? Array<
|
|
355
|
+
RegistryChatMiddleware<
|
|
356
|
+
InlineChatContext<TTools, TContext>,
|
|
357
|
+
NoInfer<TInterrupts>
|
|
358
|
+
>
|
|
359
|
+
>
|
|
360
|
+
: TMiddleware &
|
|
361
|
+
(TMiddleware extends Array<
|
|
362
|
+
RegistryChatMiddleware<
|
|
363
|
+
InlineChatContext<TTools, NoInfer<TContext>>,
|
|
364
|
+
NoInfer<TInterrupts>
|
|
365
|
+
>
|
|
366
|
+
>
|
|
367
|
+
? Array<
|
|
368
|
+
RegistryChatMiddleware<
|
|
369
|
+
InlineChatContext<TTools, TContext>,
|
|
370
|
+
NoInfer<TInterrupts>
|
|
371
|
+
>
|
|
372
|
+
>
|
|
373
|
+
: CheckInterruptRegistry<TInterrupts, TMiddleware>) &
|
|
374
|
+
CheckCoverage<Extract<TMiddleware, ReadonlyArray<AnyChatMiddleware>>>
|
|
315
375
|
|
|
316
376
|
type TextActivityOptionsWithContext<
|
|
317
377
|
TAdapter extends AnyTextAdapter,
|
|
318
378
|
TSchema extends SchemaInput | undefined,
|
|
319
379
|
TStream extends boolean,
|
|
320
380
|
TTools extends TextActivityOptions<TAdapter, TSchema, TStream, any>['tools'],
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
any
|
|
326
|
-
>['middleware'],
|
|
381
|
+
TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> =
|
|
382
|
+
[],
|
|
383
|
+
TContext = unknown,
|
|
384
|
+
TMiddleware extends Array<unknown> | undefined = undefined,
|
|
327
385
|
> = Omit<
|
|
328
386
|
TextActivityOptions<TAdapter, TSchema, TStream, any>,
|
|
329
|
-
'tools' | 'middleware' | 'context'
|
|
387
|
+
'tools' | 'middleware' | 'context' | 'interrupts'
|
|
330
388
|
> & {
|
|
331
389
|
tools?: TTools
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
} &
|
|
390
|
+
interrupts?: TInterrupts & CheckUniqueInterruptDefinitions<TInterrupts>
|
|
391
|
+
middleware?: ExactMiddlewareOption<TTools, TContext, TInterrupts, TMiddleware>
|
|
392
|
+
} & RuntimeContextOption<TTools, TMiddleware, TContext>
|
|
335
393
|
|
|
336
394
|
// ===========================
|
|
337
395
|
// Activity Options Type
|
|
@@ -489,6 +547,11 @@ export interface TextActivityOptions<
|
|
|
489
547
|
* ```
|
|
490
548
|
*/
|
|
491
549
|
middleware?: Array<ChatMiddleware<TContext>>
|
|
550
|
+
/**
|
|
551
|
+
* First-party generic interrupt definitions for this chat call.
|
|
552
|
+
* Register the same definitions on the client to type payloads and answers.
|
|
553
|
+
*/
|
|
554
|
+
interrupts?: ReadonlyArray<InterruptDefinition<any, any, any, any>>
|
|
492
555
|
/**
|
|
493
556
|
* Runtime context value passed to middleware hooks and server tools.
|
|
494
557
|
*/
|
|
@@ -529,28 +592,21 @@ export function createChatOptions<
|
|
|
529
592
|
TStream,
|
|
530
593
|
any
|
|
531
594
|
>['tools'] = TextActivityOptions<TAdapter, TSchema, TStream, any>['tools'],
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
>['middleware'] = TextActivityOptions<
|
|
538
|
-
TAdapter,
|
|
539
|
-
TSchema,
|
|
540
|
-
TStream,
|
|
541
|
-
any
|
|
542
|
-
>['middleware'],
|
|
595
|
+
const TInterrupts extends ReadonlyArray<
|
|
596
|
+
InterruptDefinition<any, any, any, any>
|
|
597
|
+
> = [],
|
|
598
|
+
TContext = unknown,
|
|
599
|
+
const TMiddleware extends Array<unknown> | undefined = undefined,
|
|
543
600
|
>(
|
|
544
601
|
options: TextActivityOptionsWithContext<
|
|
545
602
|
TAdapter,
|
|
546
603
|
TSchema,
|
|
547
604
|
TStream,
|
|
548
605
|
TTools,
|
|
606
|
+
TInterrupts,
|
|
607
|
+
TContext,
|
|
549
608
|
TMiddleware
|
|
550
609
|
>,
|
|
551
|
-
// Preserve the concrete `tools` tuple on the returned options (so a later
|
|
552
|
-
// `chat({ ...opts })` still narrows tool-call events to the tool names)
|
|
553
|
-
// while threading the inferred runtime context like the bare options type.
|
|
554
610
|
): Omit<
|
|
555
611
|
TextActivityOptions<
|
|
556
612
|
TAdapter,
|
|
@@ -558,8 +614,12 @@ export function createChatOptions<
|
|
|
558
614
|
TStream,
|
|
559
615
|
InferredContext<TTools, TMiddleware>
|
|
560
616
|
>,
|
|
561
|
-
'tools'
|
|
562
|
-
> & {
|
|
617
|
+
'tools' | 'middleware' | 'interrupts'
|
|
618
|
+
> & {
|
|
619
|
+
tools?: TTools
|
|
620
|
+
interrupts?: TInterrupts
|
|
621
|
+
middleware?: ExactMiddlewareOption<TTools, TContext, TInterrupts, TMiddleware>
|
|
622
|
+
} {
|
|
563
623
|
return options
|
|
564
624
|
}
|
|
565
625
|
|
|
@@ -623,7 +683,7 @@ interface TextEngineConfig<
|
|
|
623
683
|
adapter: TAdapter
|
|
624
684
|
systemPrompts?: Array<SystemPrompt>
|
|
625
685
|
params: TParams
|
|
626
|
-
middleware?: Array<
|
|
686
|
+
middleware?: Array<AnyChatMiddleware>
|
|
627
687
|
context?: TContext
|
|
628
688
|
/**
|
|
629
689
|
* If set, after the agent loop finishes the engine runs a
|
|
@@ -707,12 +767,18 @@ class TextEngine<
|
|
|
707
767
|
>,
|
|
708
768
|
> {
|
|
709
769
|
private readonly adapter: TAdapter
|
|
770
|
+
private readonly interruptDefinitions: ReadonlyMap<
|
|
771
|
+
string,
|
|
772
|
+
InterruptDefinition<any, any, any, any>
|
|
773
|
+
>
|
|
710
774
|
private params: TParams
|
|
711
775
|
private systemPrompts: Array<SystemPrompt>
|
|
712
776
|
private tools: Array<AnyRuntimeTool>
|
|
713
777
|
private readonly loopStrategy: AgentLoopStrategy
|
|
714
778
|
private toolCallManager: ToolCallManager<ReadonlyArray<AnyTool>, TContext>
|
|
715
779
|
private readonly lazyToolManager: LazyToolManager
|
|
780
|
+
/** A public interruption terminal must always have this run's start event. */
|
|
781
|
+
private hasPublicRunStarted = false
|
|
716
782
|
private readonly initialMessageCount: number
|
|
717
783
|
private readonly requestId: string
|
|
718
784
|
private readonly streamId: string
|
|
@@ -738,11 +804,15 @@ class TextEngine<
|
|
|
738
804
|
[]
|
|
739
805
|
private currentThinkingContent = ''
|
|
740
806
|
private currentThinkingSignature = ''
|
|
807
|
+
private hasSeenReasoningEvents = false
|
|
741
808
|
private eventOptions?: Record<string, unknown> | undefined
|
|
742
809
|
private eventToolNames?: Array<string>
|
|
743
810
|
private finishedEvent: RunFinishedEvent | null = null
|
|
744
811
|
private readonly streamedToolErrorResults = new Map<string, ToolResult>()
|
|
745
812
|
private deferredToolCallRunFinishedChunks: Array<StreamChunk> = []
|
|
813
|
+
/** The model terminal is held until afterModel can choose an interrupt. */
|
|
814
|
+
private deferredModelRunFinishedChunks: Array<StreamChunk> = []
|
|
815
|
+
|
|
746
816
|
private earlyTermination = false
|
|
747
817
|
private toolPhase: ToolPhaseResult = 'continue'
|
|
748
818
|
private cyclePhase: CyclePhase = 'processText'
|
|
@@ -753,6 +823,14 @@ class TextEngine<
|
|
|
753
823
|
private readonly resumeClientToolResults = new Map<string, any>()
|
|
754
824
|
private readonly resumeDeniedToolResults = new Map<string, unknown>()
|
|
755
825
|
private readonly resumeCancelledToolCallIds = new Set<string>()
|
|
826
|
+
private readonly resumeGenericInterrupts = new Map<
|
|
827
|
+
string,
|
|
828
|
+
ChatResumeGenericResolution
|
|
829
|
+
>()
|
|
830
|
+
private readonly resumeGenericInterruptRequests = new Map<
|
|
831
|
+
string,
|
|
832
|
+
GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
|
|
833
|
+
>()
|
|
756
834
|
|
|
757
835
|
// AG-UI protocol IDs
|
|
758
836
|
private readonly threadId: string
|
|
@@ -760,7 +838,10 @@ class TextEngine<
|
|
|
760
838
|
private readonly parentRunIdOverride?: string
|
|
761
839
|
|
|
762
840
|
// Middleware support
|
|
763
|
-
private readonly middlewareRunner: MiddlewareRunner<
|
|
841
|
+
private readonly middlewareRunner: MiddlewareRunner<
|
|
842
|
+
TContext,
|
|
843
|
+
InterruptDefinition<any, any, any, any>
|
|
844
|
+
>
|
|
764
845
|
private readonly middlewareCtx: ChatMiddlewareContext<TContext>
|
|
765
846
|
private readonly sandboxFileQueue: Array<StreamChunk> = []
|
|
766
847
|
private readonly deferredPromises: Array<Promise<unknown>> = []
|
|
@@ -782,8 +863,13 @@ class TextEngine<
|
|
|
782
863
|
private readonly logger: InternalLogger
|
|
783
864
|
|
|
784
865
|
// Structured-output finalization state (populated by runStructuredFinalization)
|
|
785
|
-
private structuredOutputResult: {
|
|
786
|
-
|
|
866
|
+
private structuredOutputResult: {
|
|
867
|
+
data: unknown
|
|
868
|
+
rawText: string
|
|
869
|
+
reasoning?: string
|
|
870
|
+
} | null = null
|
|
871
|
+
private structuredOutputMessageId: string | null = null
|
|
872
|
+
private structuredOutputMessageCreatedAt: Date | null = null
|
|
787
873
|
// Native combined mode: tracks whether we've already emitted the synthetic
|
|
788
874
|
// `structured-output.start` event before the schema-constrained final-turn
|
|
789
875
|
// text begins streaming. The event must precede the first
|
|
@@ -820,6 +906,15 @@ class TextEngine<
|
|
|
820
906
|
) {
|
|
821
907
|
this.logger = logger
|
|
822
908
|
this.adapter = config.adapter
|
|
909
|
+
this.interruptDefinitions = new Map(
|
|
910
|
+
(
|
|
911
|
+
(
|
|
912
|
+
config.params as TParams & {
|
|
913
|
+
interrupts?: ReadonlyArray<InterruptDefinition<any, any, any, any>>
|
|
914
|
+
}
|
|
915
|
+
).interrupts ?? []
|
|
916
|
+
).map((definition) => [definition.id, definition]),
|
|
917
|
+
)
|
|
823
918
|
this.finalStructuredOutput = config.finalStructuredOutput
|
|
824
919
|
this.params = config.params
|
|
825
920
|
this.systemPrompts = config.params.systemPrompts || []
|
|
@@ -841,6 +936,7 @@ class TextEngine<
|
|
|
841
936
|
this.messages = convertMessagesToModelMessages(config.params.messages)
|
|
842
937
|
|
|
843
938
|
// Initialize lazy tool manager after messages are converted (needs message history for scanning)
|
|
939
|
+
assertUniqueToolNames(config.params.tools || [])
|
|
844
940
|
this.lazyToolManager = new LazyToolManager(
|
|
845
941
|
config.params.tools || [],
|
|
846
942
|
this.messages,
|
|
@@ -871,7 +967,9 @@ class TextEngine<
|
|
|
871
967
|
// handleStreamChunk processes raw chunks BEFORE middleware, so internal
|
|
872
968
|
// state management sees extended fields (finishReason, delta, toolCallName, etc.).
|
|
873
969
|
// The strip middleware ensures the yielded public stream is AG-UI spec-compliant.
|
|
874
|
-
const allMiddleware: Array<
|
|
970
|
+
const allMiddleware: Array<
|
|
971
|
+
ChatMiddleware<TContext, InterruptDefinition<any, any, any, any>>
|
|
972
|
+
> = [
|
|
875
973
|
devtoolsMiddleware(),
|
|
876
974
|
...(config.middleware || []),
|
|
877
975
|
stripToSpecMiddleware(),
|
|
@@ -950,6 +1048,10 @@ class TextEngine<
|
|
|
950
1048
|
},
|
|
951
1049
|
})
|
|
952
1050
|
|
|
1051
|
+
provideGenericInterruptDefinitionRegistry(this.middlewareCtx, {
|
|
1052
|
+
definitions: this.interruptDefinitions,
|
|
1053
|
+
})
|
|
1054
|
+
|
|
953
1055
|
// Provide the internal SandboxRuntime capability so harness adapters and
|
|
954
1056
|
// sandbox middleware can emit file events. The sink logs, fans the event
|
|
955
1057
|
// out through the middleware `onFile*` hooks (fire-and-forget), and queues
|
|
@@ -1041,10 +1143,25 @@ class TextEngine<
|
|
|
1041
1143
|
)
|
|
1042
1144
|
this.applyMiddlewareConfig(transformedConfig)
|
|
1043
1145
|
await this.applyEphemeralInterruptResume(transformedConfig)
|
|
1146
|
+
await this.applyDurableGenericInterruptResolution()
|
|
1044
1147
|
|
|
1045
1148
|
// Run onStart (devtools middleware emits text:request:started and initial messages here)
|
|
1046
1149
|
await this.middlewareRunner.runOnStart(this.middlewareCtx)
|
|
1047
1150
|
|
|
1151
|
+
if (this.earlyTermination) {
|
|
1152
|
+
yield* this.emitSuccessfulEarlyTermination()
|
|
1153
|
+
if (!this.terminalHookCalled) {
|
|
1154
|
+
this.terminalHookCalled = true
|
|
1155
|
+
await this.middlewareRunner.runOnFinish(this.middlewareCtx, {
|
|
1156
|
+
finishReason: this.lastFinishReason,
|
|
1157
|
+
duration: Date.now() - this.streamStartTime,
|
|
1158
|
+
content: this.accumulatedContent,
|
|
1159
|
+
usage: this.finishedEvent?.usage,
|
|
1160
|
+
})
|
|
1161
|
+
}
|
|
1162
|
+
return
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1048
1165
|
const pendingPhase = yield* this.checkForPendingToolCalls()
|
|
1049
1166
|
if (pendingPhase === 'wait') {
|
|
1050
1167
|
return
|
|
@@ -1088,7 +1205,35 @@ class TextEngine<
|
|
|
1088
1205
|
)
|
|
1089
1206
|
this.applyMiddlewareConfig(iterTransformedConfig)
|
|
1090
1207
|
|
|
1208
|
+
if (
|
|
1209
|
+
yield* this.emitBoundaryInterrupts(
|
|
1210
|
+
'beforeModel',
|
|
1211
|
+
this.createSyntheticFinishedEvent(),
|
|
1212
|
+
)
|
|
1213
|
+
) {
|
|
1214
|
+
this.setToolPhase('wait')
|
|
1215
|
+
return
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1091
1218
|
yield* this.streamModelResponse()
|
|
1219
|
+
|
|
1220
|
+
if (
|
|
1221
|
+
yield* this.emitBoundaryInterrupts(
|
|
1222
|
+
'afterModel',
|
|
1223
|
+
this.finishedEvent ?? this.createSyntheticFinishedEvent(),
|
|
1224
|
+
)
|
|
1225
|
+
) {
|
|
1226
|
+
this.setToolPhase('wait')
|
|
1227
|
+
return
|
|
1228
|
+
}
|
|
1229
|
+
if (this.shouldExecuteToolPhase()) {
|
|
1230
|
+
this.deferredToolCallRunFinishedChunks.push(
|
|
1231
|
+
...this.deferredModelRunFinishedChunks,
|
|
1232
|
+
)
|
|
1233
|
+
this.deferredModelRunFinishedChunks = []
|
|
1234
|
+
} else {
|
|
1235
|
+
yield* this.flushDeferredModelRunFinishedChunks()
|
|
1236
|
+
}
|
|
1092
1237
|
} else {
|
|
1093
1238
|
yield* this.processToolCalls()
|
|
1094
1239
|
}
|
|
@@ -1152,6 +1297,7 @@ class TextEngine<
|
|
|
1152
1297
|
duration: Date.now() - this.streamStartTime,
|
|
1153
1298
|
})
|
|
1154
1299
|
} else {
|
|
1300
|
+
this.addTerminalAssistantMessages()
|
|
1155
1301
|
this.terminalHookCalled = true
|
|
1156
1302
|
await this.middlewareRunner.runOnFinish(this.middlewareCtx, {
|
|
1157
1303
|
finishReason: this.lastFinishReason,
|
|
@@ -1282,6 +1428,7 @@ class TextEngine<
|
|
|
1282
1428
|
this.accumulatedThinking = []
|
|
1283
1429
|
this.currentThinkingContent = ''
|
|
1284
1430
|
this.currentThinkingSignature = ''
|
|
1431
|
+
this.hasSeenReasoningEvents = false
|
|
1285
1432
|
this.finishedEvent = null
|
|
1286
1433
|
this.streamedToolErrorResults.clear()
|
|
1287
1434
|
|
|
@@ -1394,6 +1541,7 @@ class TextEngine<
|
|
|
1394
1541
|
typeof startValue.messageId === 'string'
|
|
1395
1542
|
) {
|
|
1396
1543
|
this.combinedStructuredMessageId = startValue.messageId
|
|
1544
|
+
this.captureStructuredOutputMessageIdentity(startValue.messageId)
|
|
1397
1545
|
}
|
|
1398
1546
|
}
|
|
1399
1547
|
|
|
@@ -1411,6 +1559,11 @@ class TextEngine<
|
|
|
1411
1559
|
this.structuredOutputResult = { data: object, rawText: parsed.raw }
|
|
1412
1560
|
this.combinedCompleteEmitted = true
|
|
1413
1561
|
const value = chunk.value
|
|
1562
|
+
const completeMessageId = readCustomEventMessageId(value)
|
|
1563
|
+
if (completeMessageId) {
|
|
1564
|
+
this.combinedStructuredMessageId = completeMessageId
|
|
1565
|
+
this.captureStructuredOutputMessageIdentity(completeMessageId)
|
|
1566
|
+
}
|
|
1414
1567
|
if (object !== parsed.object && value && typeof value === 'object') {
|
|
1415
1568
|
outboundChunk = { ...chunk, value: { ...value, object } }
|
|
1416
1569
|
}
|
|
@@ -1473,10 +1626,17 @@ class TextEngine<
|
|
|
1473
1626
|
) {
|
|
1474
1627
|
continue
|
|
1475
1628
|
}
|
|
1629
|
+
if (outputChunk.type === EventType.RUN_FINISHED) {
|
|
1630
|
+
this.deferredModelRunFinishedChunks.push(outputChunk)
|
|
1631
|
+
continue
|
|
1632
|
+
}
|
|
1476
1633
|
if (this.shouldDeferToolCallRunFinished(outputChunk)) {
|
|
1477
1634
|
this.deferredToolCallRunFinishedChunks.push(outputChunk)
|
|
1478
1635
|
continue
|
|
1479
1636
|
}
|
|
1637
|
+
if (outputChunk.type === EventType.RUN_STARTED) {
|
|
1638
|
+
this.hasPublicRunStarted = true
|
|
1639
|
+
}
|
|
1480
1640
|
this.logger.output(`type=${outputChunk.type}`, { chunk: outputChunk })
|
|
1481
1641
|
yield outputChunk
|
|
1482
1642
|
this.middlewareCtx.chunkIndex++
|
|
@@ -1533,16 +1693,19 @@ class TextEngine<
|
|
|
1533
1693
|
this.handleStepFinishedEvent(chunk)
|
|
1534
1694
|
break
|
|
1535
1695
|
|
|
1696
|
+
case 'REASONING_MESSAGE_CONTENT':
|
|
1697
|
+
this.handleReasoningMessageContentEvent(chunk)
|
|
1698
|
+
break
|
|
1699
|
+
|
|
1536
1700
|
case 'TOOL_CALL_RESULT':
|
|
1537
1701
|
// Tool result is already added to messages in buildToolResultChunks
|
|
1538
1702
|
break
|
|
1539
1703
|
|
|
1540
1704
|
case 'REASONING_START':
|
|
1541
1705
|
case 'REASONING_MESSAGE_START':
|
|
1542
|
-
case 'REASONING_MESSAGE_CONTENT':
|
|
1543
1706
|
case 'REASONING_MESSAGE_END':
|
|
1544
1707
|
case 'REASONING_END':
|
|
1545
|
-
//
|
|
1708
|
+
// No special handling needed
|
|
1546
1709
|
break
|
|
1547
1710
|
|
|
1548
1711
|
default:
|
|
@@ -1574,6 +1737,11 @@ class TextEngine<
|
|
|
1574
1737
|
}
|
|
1575
1738
|
}
|
|
1576
1739
|
|
|
1740
|
+
private captureStructuredOutputMessageIdentity(messageId: string): void {
|
|
1741
|
+
this.structuredOutputMessageId = messageId
|
|
1742
|
+
this.structuredOutputMessageCreatedAt ??= new Date()
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1577
1745
|
private handleToolCallStartEvent(chunk: ToolCallStartEvent): void {
|
|
1578
1746
|
if (
|
|
1579
1747
|
typeof chunk.parentMessageId === 'string' &&
|
|
@@ -1651,14 +1819,29 @@ class TextEngine<
|
|
|
1651
1819
|
private handleStepFinishedEvent(
|
|
1652
1820
|
chunk: Extract<StreamChunk, { type: 'STEP_FINISHED' }>,
|
|
1653
1821
|
): void {
|
|
1654
|
-
if (
|
|
1655
|
-
|
|
1822
|
+
if (!this.hasSeenReasoningEvents) {
|
|
1823
|
+
if (chunk.delta) {
|
|
1824
|
+
this.currentThinkingContent += chunk.delta
|
|
1825
|
+
} else if (chunk.content) {
|
|
1826
|
+
if (chunk.content.startsWith(this.currentThinkingContent)) {
|
|
1827
|
+
this.currentThinkingContent = chunk.content
|
|
1828
|
+
} else if (!this.currentThinkingContent.startsWith(chunk.content)) {
|
|
1829
|
+
this.currentThinkingContent += chunk.content
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1656
1832
|
}
|
|
1657
1833
|
if (chunk.signature) {
|
|
1658
1834
|
this.currentThinkingSignature = chunk.signature
|
|
1659
1835
|
}
|
|
1660
1836
|
}
|
|
1661
1837
|
|
|
1838
|
+
private handleReasoningMessageContentEvent(
|
|
1839
|
+
chunk: Extract<StreamChunk, { type: 'REASONING_MESSAGE_CONTENT' }>,
|
|
1840
|
+
): void {
|
|
1841
|
+
this.hasSeenReasoningEvents = true
|
|
1842
|
+
this.currentThinkingContent += chunk.delta
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1662
1845
|
/**
|
|
1663
1846
|
* Tools available for execution this turn. The discovery tool is dropped
|
|
1664
1847
|
* from the advertised set (`this.tools`) once every lazy tool is discovered,
|
|
@@ -1736,6 +1919,18 @@ class TextEngine<
|
|
|
1736
1919
|
return 'continue'
|
|
1737
1920
|
}
|
|
1738
1921
|
|
|
1922
|
+
this.middlewareCtx.phase = 'beforeTools'
|
|
1923
|
+
if (
|
|
1924
|
+
yield* this.emitBoundaryInterrupts(
|
|
1925
|
+
'beforeTools',
|
|
1926
|
+
finishEvent,
|
|
1927
|
+
executablePendingCalls,
|
|
1928
|
+
)
|
|
1929
|
+
) {
|
|
1930
|
+
this.setToolPhase('wait')
|
|
1931
|
+
return 'wait'
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1739
1934
|
const { approvals, clientToolResults } = this.collectClientState()
|
|
1740
1935
|
|
|
1741
1936
|
const generator = executeToolCalls(
|
|
@@ -1903,6 +2098,17 @@ class TextEngine<
|
|
|
1903
2098
|
}
|
|
1904
2099
|
this.middlewareCtx.phase = 'beforeTools'
|
|
1905
2100
|
|
|
2101
|
+
if (
|
|
2102
|
+
yield* this.emitBoundaryInterrupts(
|
|
2103
|
+
'beforeTools',
|
|
2104
|
+
finishEvent,
|
|
2105
|
+
executableToolCalls,
|
|
2106
|
+
)
|
|
2107
|
+
) {
|
|
2108
|
+
this.setToolPhase('wait')
|
|
2109
|
+
return
|
|
2110
|
+
}
|
|
2111
|
+
|
|
1906
2112
|
const { approvals, clientToolResults } = this.collectClientState()
|
|
1907
2113
|
|
|
1908
2114
|
const generator = executeToolCalls(
|
|
@@ -1970,15 +2176,36 @@ class TextEngine<
|
|
|
1970
2176
|
needsClientExecution: executionResult.needsClientExecution,
|
|
1971
2177
|
})
|
|
1972
2178
|
|
|
2179
|
+
const afterToolBoundaryChunks = this.buildToolResultChunks(
|
|
2180
|
+
allResults,
|
|
2181
|
+
finishEvent,
|
|
2182
|
+
)
|
|
2183
|
+
const afterToolRequests =
|
|
2184
|
+
await this.middlewareRunner.runOnInterruptBoundary(
|
|
2185
|
+
this.middlewareCtx as ChatMiddlewareContext<TContext> & {
|
|
2186
|
+
phase: 'afterTools'
|
|
2187
|
+
},
|
|
2188
|
+
)
|
|
2189
|
+
if (afterToolRequests.length > 0) {
|
|
2190
|
+
for (const chunk of afterToolBoundaryChunks) {
|
|
2191
|
+
yield* this.pipeThroughMiddleware(chunk)
|
|
2192
|
+
}
|
|
2193
|
+
yield* this.emitBoundaryInterrupts(
|
|
2194
|
+
'afterTools',
|
|
2195
|
+
finishEvent,
|
|
2196
|
+
toolCalls,
|
|
2197
|
+
afterToolRequests,
|
|
2198
|
+
)
|
|
2199
|
+
this.setToolPhase('wait')
|
|
2200
|
+
return
|
|
2201
|
+
}
|
|
2202
|
+
|
|
1973
2203
|
if (
|
|
1974
2204
|
executionResult.needsApproval.length > 0 ||
|
|
1975
2205
|
executionResult.needsClientExecution.length > 0
|
|
1976
2206
|
) {
|
|
1977
2207
|
if (allResults.length > 0) {
|
|
1978
|
-
for (const chunk of
|
|
1979
|
-
allResults,
|
|
1980
|
-
finishEvent,
|
|
1981
|
-
)) {
|
|
2208
|
+
for (const chunk of afterToolBoundaryChunks) {
|
|
1982
2209
|
yield* this.pipeThroughMiddleware(chunk)
|
|
1983
2210
|
}
|
|
1984
2211
|
}
|
|
@@ -1994,7 +2221,7 @@ class TextEngine<
|
|
|
1994
2221
|
|
|
1995
2222
|
yield* this.flushDeferredToolCallRunFinishedChunks()
|
|
1996
2223
|
|
|
1997
|
-
const toolResultChunks =
|
|
2224
|
+
const toolResultChunks = afterToolBoundaryChunks
|
|
1998
2225
|
|
|
1999
2226
|
for (const chunk of toolResultChunks) {
|
|
2000
2227
|
yield* this.pipeThroughMiddleware(chunk)
|
|
@@ -2034,6 +2261,48 @@ class TextEngine<
|
|
|
2034
2261
|
this.deferredToolCallRunFinishedChunks = []
|
|
2035
2262
|
}
|
|
2036
2263
|
|
|
2264
|
+
private *flushDeferredModelRunFinishedChunks(): Generator<StreamChunk> {
|
|
2265
|
+
for (const chunk of this.deferredModelRunFinishedChunks) {
|
|
2266
|
+
this.logger.output(`type=${chunk.type}`, { chunk })
|
|
2267
|
+
yield chunk
|
|
2268
|
+
this.middlewareCtx.chunkIndex++
|
|
2269
|
+
}
|
|
2270
|
+
this.deferredModelRunFinishedChunks = []
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
private async *emitSyntheticRunStarted(
|
|
2274
|
+
finishEvent: RunFinishedEvent,
|
|
2275
|
+
): AsyncGenerator<StreamChunk, void, void> {
|
|
2276
|
+
if (this.hasPublicRunStarted) return
|
|
2277
|
+
yield* this.pipeThroughMiddleware({
|
|
2278
|
+
type: EventType.RUN_STARTED,
|
|
2279
|
+
runId: finishEvent.runId,
|
|
2280
|
+
threadId: finishEvent.threadId,
|
|
2281
|
+
model: finishEvent.model,
|
|
2282
|
+
timestamp: Date.now(),
|
|
2283
|
+
})
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
private async *emitSuccessfulEarlyTermination(): AsyncGenerator<
|
|
2287
|
+
StreamChunk,
|
|
2288
|
+
void,
|
|
2289
|
+
void
|
|
2290
|
+
> {
|
|
2291
|
+
// `stop` is a finished run, not another tool cycle. `tool_calls` here
|
|
2292
|
+
// makes the client auto-send after afterTools, so reject looks stuck.
|
|
2293
|
+
this.lastFinishReason = 'stop'
|
|
2294
|
+
const finishEvent = {
|
|
2295
|
+
...this.createSyntheticFinishedEvent(),
|
|
2296
|
+
finishReason: 'stop' as const,
|
|
2297
|
+
}
|
|
2298
|
+
yield* this.emitSyntheticRunStarted(finishEvent)
|
|
2299
|
+
yield* this.pipeThroughMiddleware({
|
|
2300
|
+
...finishEvent,
|
|
2301
|
+
timestamp: Date.now(),
|
|
2302
|
+
outcome: { type: 'success' },
|
|
2303
|
+
})
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2037
2306
|
private discardDeferredToolCallRunFinishedChunks(): void {
|
|
2038
2307
|
this.deferredToolCallRunFinishedChunks = []
|
|
2039
2308
|
}
|
|
@@ -2065,6 +2334,107 @@ class TextEngine<
|
|
|
2065
2334
|
this.middlewareCtx.messages = this.messages
|
|
2066
2335
|
}
|
|
2067
2336
|
|
|
2337
|
+
private addTerminalAssistantMessages(): void {
|
|
2338
|
+
this.finalizeCurrentThinkingStep()
|
|
2339
|
+
|
|
2340
|
+
const structuredResult = this.structuredOutputResult
|
|
2341
|
+
const raw = structuredResult
|
|
2342
|
+
? structuredResult.rawText || safeJsonStringify(structuredResult.data)
|
|
2343
|
+
: ''
|
|
2344
|
+
const structuredOutput: StructuredOutputPart | undefined = structuredResult
|
|
2345
|
+
? {
|
|
2346
|
+
type: 'structured-output',
|
|
2347
|
+
status: 'complete',
|
|
2348
|
+
data: structuredResult.data,
|
|
2349
|
+
partial: structuredResult.data,
|
|
2350
|
+
raw,
|
|
2351
|
+
...(structuredResult.reasoning !== undefined
|
|
2352
|
+
? { reasoning: structuredResult.reasoning }
|
|
2353
|
+
: {}),
|
|
2354
|
+
}
|
|
2355
|
+
: undefined
|
|
2356
|
+
const nativeCombined = this.finalStructuredOutput?.nativeCombined === true
|
|
2357
|
+
const eventSourced = this.finalStructuredOutput?.source === 'event'
|
|
2358
|
+
const structuredId =
|
|
2359
|
+
this.structuredOutputMessageId ??
|
|
2360
|
+
this.combinedStructuredMessageId ??
|
|
2361
|
+
this.currentMessageId ??
|
|
2362
|
+
this.createId('msg')
|
|
2363
|
+
// Codex, OpenCode, ACP, and grok-build reuse the last text messageId
|
|
2364
|
+
// on structured-output.complete. Split only when the event uses a
|
|
2365
|
+
// different id (Claude Code). Same-id output stays on one message.
|
|
2366
|
+
const splitStructuredMessage =
|
|
2367
|
+
Boolean(structuredOutput) &&
|
|
2368
|
+
(!nativeCombined || eventSourced) &&
|
|
2369
|
+
this.currentMessageId != null &&
|
|
2370
|
+
structuredId !== this.currentMessageId
|
|
2371
|
+
const messages = [...this.middlewareCtx.messages]
|
|
2372
|
+
const existingStructuredIndex = messages.findIndex(
|
|
2373
|
+
(message) => message.role === 'assistant' && message.id === structuredId,
|
|
2374
|
+
)
|
|
2375
|
+
const currentTurnAlreadyRecorded = messages.some(
|
|
2376
|
+
(message) =>
|
|
2377
|
+
message.role === 'assistant' && message.id === this.currentMessageId,
|
|
2378
|
+
)
|
|
2379
|
+
const thinking =
|
|
2380
|
+
this.accumulatedThinking.length > 0 ? this.accumulatedThinking : undefined
|
|
2381
|
+
const startedLength = messages.length
|
|
2382
|
+
|
|
2383
|
+
if (structuredOutput && existingStructuredIndex >= 0) {
|
|
2384
|
+
const existing = messages[existingStructuredIndex]
|
|
2385
|
+
if (existing) {
|
|
2386
|
+
messages[existingStructuredIndex] = {
|
|
2387
|
+
...existing,
|
|
2388
|
+
content: raw || existing.content,
|
|
2389
|
+
structuredOutput,
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
} else if (structuredOutput && !splitStructuredMessage) {
|
|
2393
|
+
if (!currentTurnAlreadyRecorded) {
|
|
2394
|
+
messages.push({
|
|
2395
|
+
role: 'assistant',
|
|
2396
|
+
content: this.accumulatedContent || raw || null,
|
|
2397
|
+
id: structuredId,
|
|
2398
|
+
createdAt:
|
|
2399
|
+
this.currentMessageCreatedAt ??
|
|
2400
|
+
this.structuredOutputMessageCreatedAt ??
|
|
2401
|
+
new Date(),
|
|
2402
|
+
structuredOutput,
|
|
2403
|
+
...(thinking ? { thinking } : {}),
|
|
2404
|
+
})
|
|
2405
|
+
}
|
|
2406
|
+
} else {
|
|
2407
|
+
if (
|
|
2408
|
+
!currentTurnAlreadyRecorded &&
|
|
2409
|
+
(this.accumulatedContent !== '' || thinking)
|
|
2410
|
+
) {
|
|
2411
|
+
messages.push({
|
|
2412
|
+
role: 'assistant',
|
|
2413
|
+
content: this.accumulatedContent || null,
|
|
2414
|
+
id: this.currentMessageId ?? this.createId('msg'),
|
|
2415
|
+
createdAt: this.currentMessageCreatedAt ?? new Date(),
|
|
2416
|
+
...(thinking ? { thinking } : {}),
|
|
2417
|
+
})
|
|
2418
|
+
}
|
|
2419
|
+
if (structuredOutput) {
|
|
2420
|
+
messages.push({
|
|
2421
|
+
role: 'assistant',
|
|
2422
|
+
content: raw || null,
|
|
2423
|
+
id: structuredId,
|
|
2424
|
+
createdAt: this.structuredOutputMessageCreatedAt ?? new Date(),
|
|
2425
|
+
structuredOutput,
|
|
2426
|
+
})
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
if (messages.length === startedLength && existingStructuredIndex < 0) {
|
|
2431
|
+
return
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
this.messages = messages
|
|
2435
|
+
this.middlewareCtx.messages = this.messages
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2068
2438
|
/**
|
|
2069
2439
|
* Extract client state (approvals and client tool results) from original messages.
|
|
2070
2440
|
* This is called in the constructor BEFORE converting to ModelMessage format,
|
|
@@ -2155,9 +2525,17 @@ class TextEngine<
|
|
|
2155
2525
|
return { approvals, clientToolResults }
|
|
2156
2526
|
}
|
|
2157
2527
|
|
|
2528
|
+
private genericInterruptId(): string {
|
|
2529
|
+
return this.createId('interrupt')
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2158
2532
|
private buildActionableInterrupts(
|
|
2159
2533
|
approvals: Array<ApprovalRequest>,
|
|
2160
2534
|
clientRequests: Array<ClientToolRequest>,
|
|
2535
|
+
genericRequests: ReadonlyArray<
|
|
2536
|
+
GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
|
|
2537
|
+
> = [],
|
|
2538
|
+
genericInterruptIds: ReadonlyArray<string> = [],
|
|
2161
2539
|
): Array<Interrupt> {
|
|
2162
2540
|
const interrupts: Array<Interrupt> = []
|
|
2163
2541
|
|
|
@@ -2227,6 +2605,64 @@ class TextEngine<
|
|
|
2227
2605
|
})
|
|
2228
2606
|
}
|
|
2229
2607
|
|
|
2608
|
+
for (const [index, request] of genericRequests.entries()) {
|
|
2609
|
+
const batchIndex = interrupts.length
|
|
2610
|
+
const id = genericInterruptIds[index]
|
|
2611
|
+
if (!id) throw new Error('Generic interrupt id is unavailable.')
|
|
2612
|
+
const preEmission = createInterruptBinding(request, { batchIndex })
|
|
2613
|
+
interrupts.push({
|
|
2614
|
+
id,
|
|
2615
|
+
reason: request.reason,
|
|
2616
|
+
message: request.message,
|
|
2617
|
+
...(preEmission.descriptor.responseSchemaCanonicalJson !== undefined
|
|
2618
|
+
? {
|
|
2619
|
+
responseSchema: JSON.parse(
|
|
2620
|
+
preEmission.descriptor.responseSchemaCanonicalJson,
|
|
2621
|
+
),
|
|
2622
|
+
}
|
|
2623
|
+
: {}),
|
|
2624
|
+
...(request.expiresAt !== undefined
|
|
2625
|
+
? { expiresAt: request.expiresAt }
|
|
2626
|
+
: {}),
|
|
2627
|
+
metadata: {
|
|
2628
|
+
[interruptBindingMetadataKey]: {
|
|
2629
|
+
v: INTERRUPT_BINDING_VERSION,
|
|
2630
|
+
kind: 'generic',
|
|
2631
|
+
interruptId: id,
|
|
2632
|
+
definitionId: preEmission.descriptor.definitionId,
|
|
2633
|
+
key: preEmission.descriptor.key,
|
|
2634
|
+
batchIndex,
|
|
2635
|
+
...(request.expiresAt !== undefined
|
|
2636
|
+
? { expiresAt: request.expiresAt }
|
|
2637
|
+
: {}),
|
|
2638
|
+
...(preEmission.descriptor.payloadSchemaHash
|
|
2639
|
+
? {
|
|
2640
|
+
payloadSchemaHash: preEmission.descriptor.payloadSchemaHash,
|
|
2641
|
+
}
|
|
2642
|
+
: {}),
|
|
2643
|
+
...(preEmission.descriptor.responseSchemaHash !== undefined
|
|
2644
|
+
? {
|
|
2645
|
+
responseSchemaHash: preEmission.descriptor.responseSchemaHash,
|
|
2646
|
+
}
|
|
2647
|
+
: {}),
|
|
2648
|
+
},
|
|
2649
|
+
...(preEmission.payload !== undefined
|
|
2650
|
+
? { [INTERRUPT_PAYLOAD_METADATA_KEY]: preEmission.payload }
|
|
2651
|
+
: {}),
|
|
2652
|
+
},
|
|
2653
|
+
})
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
const ids = new Set<string>()
|
|
2657
|
+
for (const interrupt of interrupts) {
|
|
2658
|
+
if (ids.has(interrupt.id)) {
|
|
2659
|
+
throw new Error(
|
|
2660
|
+
`Duplicate interrupt id in final batch: ${interrupt.id}`,
|
|
2661
|
+
)
|
|
2662
|
+
}
|
|
2663
|
+
ids.add(interrupt.id)
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2230
2666
|
return interrupts
|
|
2231
2667
|
}
|
|
2232
2668
|
|
|
@@ -2234,13 +2670,22 @@ class TextEngine<
|
|
|
2234
2670
|
finishEvent: RunFinishedEvent,
|
|
2235
2671
|
approvals: Array<ApprovalRequest>,
|
|
2236
2672
|
clientRequests: Array<ClientToolRequest>,
|
|
2673
|
+
genericRequests: ReadonlyArray<
|
|
2674
|
+
GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
|
|
2675
|
+
> = [],
|
|
2676
|
+
genericInterruptIds?: ReadonlyArray<string>,
|
|
2237
2677
|
): StreamChunk {
|
|
2238
2678
|
return {
|
|
2239
2679
|
...finishEvent,
|
|
2240
2680
|
timestamp: Date.now(),
|
|
2241
2681
|
outcome: {
|
|
2242
2682
|
type: 'interrupt',
|
|
2243
|
-
interrupts: this.buildActionableInterrupts(
|
|
2683
|
+
interrupts: this.buildActionableInterrupts(
|
|
2684
|
+
approvals,
|
|
2685
|
+
clientRequests,
|
|
2686
|
+
genericRequests,
|
|
2687
|
+
genericInterruptIds,
|
|
2688
|
+
),
|
|
2244
2689
|
},
|
|
2245
2690
|
}
|
|
2246
2691
|
}
|
|
@@ -2254,12 +2699,19 @@ class TextEngine<
|
|
|
2254
2699
|
: message.content === null
|
|
2255
2700
|
? undefined
|
|
2256
2701
|
: JSON.stringify(message.content)
|
|
2702
|
+
const id =
|
|
2703
|
+
message.id ||
|
|
2704
|
+
`snapshot_${this.runIdOverride ?? this.requestId}_${index}`
|
|
2705
|
+
const parts =
|
|
2706
|
+
message.role === 'assistant' &&
|
|
2707
|
+
(message.thinking?.length || message.structuredOutput)
|
|
2708
|
+
? modelMessageToUIMessage(message, id).parts
|
|
2709
|
+
: undefined
|
|
2257
2710
|
return {
|
|
2258
|
-
id
|
|
2259
|
-
message.id ||
|
|
2260
|
-
`snapshot_${this.runIdOverride ?? this.requestId}_${index}`,
|
|
2711
|
+
id,
|
|
2261
2712
|
role: message.role,
|
|
2262
2713
|
...(content !== undefined ? { content } : {}),
|
|
2714
|
+
...(parts ? { parts } : {}),
|
|
2263
2715
|
...('toolCalls' in message && message.toolCalls
|
|
2264
2716
|
? { toolCalls: message.toolCalls }
|
|
2265
2717
|
: {}),
|
|
@@ -2383,9 +2835,22 @@ class TextEngine<
|
|
|
2383
2835
|
finishEvent: RunFinishedEvent,
|
|
2384
2836
|
approvals: Array<ApprovalRequest>,
|
|
2385
2837
|
clientRequests: Array<ClientToolRequest>,
|
|
2838
|
+
genericRequests: ReadonlyArray<
|
|
2839
|
+
GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
|
|
2840
|
+
> = [],
|
|
2386
2841
|
): AsyncGenerator<StreamChunk, boolean, void> {
|
|
2842
|
+
yield* this.emitSyntheticRunStarted(finishEvent)
|
|
2843
|
+
const genericInterruptIds = genericRequests.map(() =>
|
|
2844
|
+
this.genericInterruptId(),
|
|
2845
|
+
)
|
|
2387
2846
|
const terminal = this.completeEphemeralInterruptBindings(
|
|
2388
|
-
this.buildInterruptFinishedChunk(
|
|
2847
|
+
this.buildInterruptFinishedChunk(
|
|
2848
|
+
finishEvent,
|
|
2849
|
+
approvals,
|
|
2850
|
+
clientRequests,
|
|
2851
|
+
genericRequests,
|
|
2852
|
+
genericInterruptIds,
|
|
2853
|
+
),
|
|
2389
2854
|
)
|
|
2390
2855
|
let terminalOutputs: Array<StreamChunk>
|
|
2391
2856
|
try {
|
|
@@ -2414,6 +2879,103 @@ class TextEngine<
|
|
|
2414
2879
|
return true
|
|
2415
2880
|
}
|
|
2416
2881
|
|
|
2882
|
+
private async *emitBoundaryInterrupts(
|
|
2883
|
+
phase: 'beforeModel' | 'afterModel' | 'beforeTools' | 'afterTools',
|
|
2884
|
+
finishEvent: RunFinishedEvent,
|
|
2885
|
+
toolCalls: ReadonlyArray<ToolCall> = [],
|
|
2886
|
+
requests?: ReadonlyArray<
|
|
2887
|
+
GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
|
|
2888
|
+
>,
|
|
2889
|
+
): AsyncGenerator<StreamChunk, boolean, void> {
|
|
2890
|
+
this.middlewareCtx.phase = phase
|
|
2891
|
+
const boundaryRequests =
|
|
2892
|
+
requests ??
|
|
2893
|
+
(await this.middlewareRunner.runOnInterruptBoundary(
|
|
2894
|
+
this.middlewareCtx as ChatMiddlewareContext<TContext> & {
|
|
2895
|
+
phase: typeof phase
|
|
2896
|
+
},
|
|
2897
|
+
))
|
|
2898
|
+
if (boundaryRequests.length === 0) return false
|
|
2899
|
+
for (const request of boundaryRequests) {
|
|
2900
|
+
if (
|
|
2901
|
+
this.interruptDefinitions.get(request.definition.id) !==
|
|
2902
|
+
request.definition
|
|
2903
|
+
) {
|
|
2904
|
+
throw new Error(
|
|
2905
|
+
`Generic interrupt definition ${request.definition.id} is not registered on this chat.`,
|
|
2906
|
+
)
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
if (phase === 'afterModel') {
|
|
2910
|
+
if (this.toolCallManager.hasToolCalls()) {
|
|
2911
|
+
this.addAssistantToolCallMessage(this.toolCallManager.getToolCalls())
|
|
2912
|
+
} else {
|
|
2913
|
+
this.addAssistantTextMessageForInterrupt()
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
const actionable = this.getBoundaryActionableToolRequests(toolCalls)
|
|
2917
|
+
yield* this.emitActionableInterruptBoundary(
|
|
2918
|
+
finishEvent,
|
|
2919
|
+
actionable.approvals,
|
|
2920
|
+
actionable.clientRequests,
|
|
2921
|
+
boundaryRequests,
|
|
2922
|
+
)
|
|
2923
|
+
return true
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
private addAssistantTextMessageForInterrupt(): void {
|
|
2927
|
+
if (this.accumulatedContent.length === 0) return
|
|
2928
|
+
this.messages = [
|
|
2929
|
+
...this.messages,
|
|
2930
|
+
{ role: 'assistant', content: this.accumulatedContent },
|
|
2931
|
+
]
|
|
2932
|
+
this.middlewareCtx.messages = this.messages
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
private getBoundaryActionableToolRequests(
|
|
2936
|
+
toolCalls: ReadonlyArray<ToolCall>,
|
|
2937
|
+
): {
|
|
2938
|
+
approvals: Array<ApprovalRequest>
|
|
2939
|
+
clientRequests: Array<ClientToolRequest>
|
|
2940
|
+
} {
|
|
2941
|
+
const { approvals, clientToolResults } = this.collectClientState()
|
|
2942
|
+
const approvalRequests: Array<ApprovalRequest> = []
|
|
2943
|
+
const clientRequests: Array<ClientToolRequest> = []
|
|
2944
|
+
for (const toolCall of toolCalls) {
|
|
2945
|
+
const tool = this.resolveExecutableTools([toolCall]).find(
|
|
2946
|
+
(candidate) => candidate.name === toolCall.function.name,
|
|
2947
|
+
) as RuntimeToolWithApproval | undefined
|
|
2948
|
+
if (!tool) continue
|
|
2949
|
+
let input: unknown = {}
|
|
2950
|
+
try {
|
|
2951
|
+
const parsed = JSON.parse(toolCall.function.arguments.trim() || '{}')
|
|
2952
|
+
input = parsed && typeof parsed === 'object' ? parsed : {}
|
|
2953
|
+
} catch {
|
|
2954
|
+
input = {}
|
|
2955
|
+
}
|
|
2956
|
+
const approvalId = `approval_${toolCall.id}`
|
|
2957
|
+
if (tool.needsApproval && !approvals.has(approvalId)) {
|
|
2958
|
+
approvalRequests.push({
|
|
2959
|
+
toolCallId: toolCall.id,
|
|
2960
|
+
toolName: toolCall.function.name,
|
|
2961
|
+
input,
|
|
2962
|
+
approvalId,
|
|
2963
|
+
})
|
|
2964
|
+
} else if (
|
|
2965
|
+
!tool.execute &&
|
|
2966
|
+
!clientToolResults.has(toolCall.id) &&
|
|
2967
|
+
!this.resumeCancelledToolCallIds.has(toolCall.id)
|
|
2968
|
+
) {
|
|
2969
|
+
clientRequests.push({
|
|
2970
|
+
toolCallId: toolCall.id,
|
|
2971
|
+
toolName: toolCall.function.name,
|
|
2972
|
+
input,
|
|
2973
|
+
})
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
return { approvals: approvalRequests, clientRequests }
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2417
2979
|
private completeEphemeralInterruptBindings(chunk: StreamChunk): StreamChunk {
|
|
2418
2980
|
if (
|
|
2419
2981
|
chunk.type !== EventType.RUN_FINISHED ||
|
|
@@ -2661,7 +3223,7 @@ class TextEngine<
|
|
|
2661
3223
|
private createSyntheticFinishedEvent(): RunFinishedEvent {
|
|
2662
3224
|
return {
|
|
2663
3225
|
type: 'RUN_FINISHED',
|
|
2664
|
-
runId: this.
|
|
3226
|
+
runId: this.runIdOverride ?? this.requestId,
|
|
2665
3227
|
threadId: this.threadId,
|
|
2666
3228
|
model: this.params.model,
|
|
2667
3229
|
timestamp: Date.now(),
|
|
@@ -2940,6 +3502,7 @@ class TextEngine<
|
|
|
2940
3502
|
const buildSynthesizedStart = (timestamp = Date.now()): StreamChunk => {
|
|
2941
3503
|
const idForStart = structuredMessageId ?? generateMessageId()
|
|
2942
3504
|
structuredMessageId = idForStart
|
|
3505
|
+
this.captureStructuredOutputMessageIdentity(idForStart)
|
|
2943
3506
|
return {
|
|
2944
3507
|
type: EventType.CUSTOM,
|
|
2945
3508
|
name: 'structured-output.start',
|
|
@@ -2980,7 +3543,10 @@ class TextEngine<
|
|
|
2980
3543
|
// synthesized start (when needed) uses the SAME id the deltas carry
|
|
2981
3544
|
if (!structuredMessageId) {
|
|
2982
3545
|
const extracted = extractMessageId(chunk)
|
|
2983
|
-
if (extracted)
|
|
3546
|
+
if (extracted) {
|
|
3547
|
+
structuredMessageId = extracted
|
|
3548
|
+
this.captureStructuredOutputMessageIdentity(extracted)
|
|
3549
|
+
}
|
|
2984
3550
|
}
|
|
2985
3551
|
|
|
2986
3552
|
// Synthesis only matters for the streaming client path — the agentic
|
|
@@ -3047,7 +3613,13 @@ class TextEngine<
|
|
|
3047
3613
|
const object = this.finalStructuredOutput.normalize
|
|
3048
3614
|
? this.finalStructuredOutput.normalize(parsed.object)
|
|
3049
3615
|
: parsed.object
|
|
3050
|
-
this.structuredOutputResult = {
|
|
3616
|
+
this.structuredOutputResult = {
|
|
3617
|
+
data: object,
|
|
3618
|
+
rawText: parsed.raw,
|
|
3619
|
+
...(parsed.reasoning !== undefined
|
|
3620
|
+
? { reasoning: parsed.reasoning }
|
|
3621
|
+
: {}),
|
|
3622
|
+
}
|
|
3051
3623
|
// Rewrite the outbound event so the yielded chunk carries the
|
|
3052
3624
|
// normalized object (the original `chunk.value` still holds the
|
|
3053
3625
|
// widened one). Preserve every other field — `raw`, `reasoning` —
|
|
@@ -3498,7 +4070,15 @@ class TextEngine<
|
|
|
3498
4070
|
}
|
|
3499
4071
|
}
|
|
3500
4072
|
|
|
3501
|
-
const
|
|
4073
|
+
const genericPending = this.getGenericContinuationPending(interruptedRunId)
|
|
4074
|
+
const pending: Array<{
|
|
4075
|
+
interruptId: string
|
|
4076
|
+
payload: unknown
|
|
4077
|
+
binding: InterruptBinding
|
|
4078
|
+
genericRequest?: GenericInterruptRequest<
|
|
4079
|
+
InterruptDefinition<any, any, any, any>
|
|
4080
|
+
>
|
|
4081
|
+
}> = this.buildActionableInterrupts(
|
|
3502
4082
|
approvalRequests,
|
|
3503
4083
|
clientRequests,
|
|
3504
4084
|
).flatMap((descriptor) => {
|
|
@@ -3517,6 +4097,7 @@ class TextEngine<
|
|
|
3517
4097
|
]
|
|
3518
4098
|
: []
|
|
3519
4099
|
})
|
|
4100
|
+
pending.push(...genericPending)
|
|
3520
4101
|
const validated = await validateInterruptResumeBatch({
|
|
3521
4102
|
threadId: this.threadId,
|
|
3522
4103
|
interruptedRunId,
|
|
@@ -3544,6 +4125,185 @@ class TextEngine<
|
|
|
3544
4125
|
...validated.resumeToolState,
|
|
3545
4126
|
approvals,
|
|
3546
4127
|
})
|
|
4128
|
+
|
|
4129
|
+
const genericResolutions = validated.resumeToolState.genericInterrupts
|
|
4130
|
+
if (genericPending.length > 0 && genericResolutions) {
|
|
4131
|
+
const resolutions = genericPending
|
|
4132
|
+
.sort((left, right) => {
|
|
4133
|
+
const leftIndex =
|
|
4134
|
+
left.binding.kind === 'generic' ? (left.binding.batchIndex ?? 0) : 0
|
|
4135
|
+
const rightIndex =
|
|
4136
|
+
right.binding.kind === 'generic'
|
|
4137
|
+
? (right.binding.batchIndex ?? 0)
|
|
4138
|
+
: 0
|
|
4139
|
+
return leftIndex - rightIndex
|
|
4140
|
+
})
|
|
4141
|
+
.flatMap((record) => {
|
|
4142
|
+
const resolution = genericResolutions.get(record.interruptId)
|
|
4143
|
+
if (!resolution || !record.genericRequest) return []
|
|
4144
|
+
return [
|
|
4145
|
+
resolution.status === 'resolved'
|
|
4146
|
+
? {
|
|
4147
|
+
request: record.genericRequest,
|
|
4148
|
+
status: 'resolved' as const,
|
|
4149
|
+
response: resolution.payload,
|
|
4150
|
+
}
|
|
4151
|
+
: {
|
|
4152
|
+
request: record.genericRequest,
|
|
4153
|
+
status: 'cancelled' as const,
|
|
4154
|
+
},
|
|
4155
|
+
]
|
|
4156
|
+
})
|
|
4157
|
+
const collection: InterruptResolutionCollection = {
|
|
4158
|
+
for: (definition) =>
|
|
4159
|
+
resolutions.filter(
|
|
4160
|
+
(resolution) => resolution.request.definition === definition,
|
|
4161
|
+
) as never,
|
|
4162
|
+
all: (
|
|
4163
|
+
...definitions: Array<InterruptDefinition<any, any, any, any>>
|
|
4164
|
+
) =>
|
|
4165
|
+
definitions.length === 0
|
|
4166
|
+
? resolutions
|
|
4167
|
+
: resolutions.filter((resolution) =>
|
|
4168
|
+
definitions.includes(resolution.request.definition),
|
|
4169
|
+
),
|
|
4170
|
+
}
|
|
4171
|
+
const policy = await this.middlewareRunner.runOnInterruptResolution(
|
|
4172
|
+
this.middlewareCtx,
|
|
4173
|
+
collection,
|
|
4174
|
+
)
|
|
4175
|
+
if (policy.toolResume === 'stop') {
|
|
4176
|
+
this.earlyTermination = true
|
|
4177
|
+
} else if (policy.toolResume === 'cancel') {
|
|
4178
|
+
for (const request of pendingToolCalls) {
|
|
4179
|
+
this.resumeCancelledToolCallIds.add(request.id)
|
|
4180
|
+
}
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
}
|
|
4184
|
+
|
|
4185
|
+
private getGenericContinuationPending(interruptedRunId: string): Array<{
|
|
4186
|
+
interruptId: string
|
|
4187
|
+
payload: unknown
|
|
4188
|
+
binding: InterruptBinding
|
|
4189
|
+
genericRequest: GenericInterruptRequest<
|
|
4190
|
+
InterruptDefinition<any, any, any, any>
|
|
4191
|
+
>
|
|
4192
|
+
}> {
|
|
4193
|
+
const fail = (message: string): never => {
|
|
4194
|
+
throw new InterruptResumeValidationError([
|
|
4195
|
+
{
|
|
4196
|
+
scope: 'batch',
|
|
4197
|
+
threadId: this.threadId,
|
|
4198
|
+
interruptedRunId,
|
|
4199
|
+
generation: 0,
|
|
4200
|
+
interruptIds: [],
|
|
4201
|
+
code: 'stale',
|
|
4202
|
+
message,
|
|
4203
|
+
source: 'server',
|
|
4204
|
+
retryable: false,
|
|
4205
|
+
},
|
|
4206
|
+
])
|
|
4207
|
+
}
|
|
4208
|
+
const pending: Array<{
|
|
4209
|
+
interruptId: string
|
|
4210
|
+
payload: unknown
|
|
4211
|
+
binding: InterruptBinding
|
|
4212
|
+
genericRequest: GenericInterruptRequest<
|
|
4213
|
+
InterruptDefinition<any, any, any, any>
|
|
4214
|
+
>
|
|
4215
|
+
}> = []
|
|
4216
|
+
const ids = new Set<string>()
|
|
4217
|
+
const batchIndexes = new Set<number>()
|
|
4218
|
+
for (const resumeItem of this.params.resume ?? []) {
|
|
4219
|
+
const parsed = readGenericInterruptContinuation(resumeItem.metadata)
|
|
4220
|
+
if (parsed.status === 'absent') continue
|
|
4221
|
+
if (parsed.status === 'invalid') {
|
|
4222
|
+
return fail(parsed.message)
|
|
4223
|
+
}
|
|
4224
|
+
const entry = parsed.value
|
|
4225
|
+
const id = resumeItem.interruptId
|
|
4226
|
+
const definition = this.interruptDefinitions.get(entry.definitionId)
|
|
4227
|
+
if (!definition) {
|
|
4228
|
+
return fail(
|
|
4229
|
+
`Generic interrupt definition ${entry.definitionId} is unavailable.`,
|
|
4230
|
+
)
|
|
4231
|
+
}
|
|
4232
|
+
if (ids.has(id) || batchIndexes.has(entry.batchIndex)) {
|
|
4233
|
+
return fail(
|
|
4234
|
+
'Generic interrupt continuation contains duplicate entries.',
|
|
4235
|
+
)
|
|
4236
|
+
}
|
|
4237
|
+
ids.add(id)
|
|
4238
|
+
batchIndexes.add(entry.batchIndex)
|
|
4239
|
+
let request: GenericInterruptRequest<
|
|
4240
|
+
InterruptDefinition<any, any, any, any>
|
|
4241
|
+
>
|
|
4242
|
+
try {
|
|
4243
|
+
request = rehydrateInterruptRequest(definition, {
|
|
4244
|
+
key: entry.key,
|
|
4245
|
+
reason: entry.reason,
|
|
4246
|
+
message: entry.message,
|
|
4247
|
+
...(typeof entry.expiresAt === 'string'
|
|
4248
|
+
? { expiresAt: entry.expiresAt }
|
|
4249
|
+
: {}),
|
|
4250
|
+
...(Object.prototype.hasOwnProperty.call(entry, 'payload')
|
|
4251
|
+
? { payload: entry.payload }
|
|
4252
|
+
: {}),
|
|
4253
|
+
})
|
|
4254
|
+
} catch (error) {
|
|
4255
|
+
return fail(
|
|
4256
|
+
`Generic interrupt continuation ${id} is invalid: ${
|
|
4257
|
+
error instanceof Error ? error.message : String(error)
|
|
4258
|
+
}`,
|
|
4259
|
+
)
|
|
4260
|
+
}
|
|
4261
|
+
const emitted = createInterruptBinding(request, {
|
|
4262
|
+
batchIndex: entry.batchIndex,
|
|
4263
|
+
})
|
|
4264
|
+
if (
|
|
4265
|
+
entry.responseSchemaHash !== emitted.descriptor.responseSchemaHash ||
|
|
4266
|
+
entry.payloadSchemaHash !== emitted.descriptor.payloadSchemaHash
|
|
4267
|
+
) {
|
|
4268
|
+
return fail(
|
|
4269
|
+
`Generic interrupt continuation ${id} does not match its definition.`,
|
|
4270
|
+
)
|
|
4271
|
+
}
|
|
4272
|
+
pending.push({
|
|
4273
|
+
interruptId: id,
|
|
4274
|
+
payload: {
|
|
4275
|
+
id,
|
|
4276
|
+
...(emitted.descriptor.responseSchemaCanonicalJson !== undefined
|
|
4277
|
+
? {
|
|
4278
|
+
responseSchema: JSON.parse(
|
|
4279
|
+
emitted.descriptor.responseSchemaCanonicalJson,
|
|
4280
|
+
),
|
|
4281
|
+
}
|
|
4282
|
+
: {}),
|
|
4283
|
+
},
|
|
4284
|
+
binding: {
|
|
4285
|
+
v: INTERRUPT_BINDING_VERSION,
|
|
4286
|
+
kind: 'generic',
|
|
4287
|
+
interruptId: id,
|
|
4288
|
+
interruptedRunId,
|
|
4289
|
+
generation: 0,
|
|
4290
|
+
definitionId: entry.definitionId,
|
|
4291
|
+
key: entry.key,
|
|
4292
|
+
batchIndex: entry.batchIndex,
|
|
4293
|
+
...(typeof entry.expiresAt === 'string'
|
|
4294
|
+
? { expiresAt: entry.expiresAt }
|
|
4295
|
+
: {}),
|
|
4296
|
+
...(emitted.descriptor.payloadSchemaHash
|
|
4297
|
+
? { payloadSchemaHash: emitted.descriptor.payloadSchemaHash }
|
|
4298
|
+
: {}),
|
|
4299
|
+
...(entry.responseSchemaHash !== undefined
|
|
4300
|
+
? { responseSchemaHash: entry.responseSchemaHash }
|
|
4301
|
+
: {}),
|
|
4302
|
+
},
|
|
4303
|
+
genericRequest: request,
|
|
4304
|
+
})
|
|
4305
|
+
}
|
|
4306
|
+
return pending
|
|
3547
4307
|
}
|
|
3548
4308
|
|
|
3549
4309
|
private applyResumeToolState(state: ChatResumeToolState | undefined): void {
|
|
@@ -3567,12 +4327,65 @@ class TextEngine<
|
|
|
3567
4327
|
this.resumeCancelledToolCallIds.add(toolCallId)
|
|
3568
4328
|
}
|
|
3569
4329
|
}
|
|
4330
|
+
if (state?.genericInterrupts) {
|
|
4331
|
+
for (const [interruptId, resolution] of state.genericInterrupts) {
|
|
4332
|
+
this.resumeGenericInterrupts.set(interruptId, resolution)
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4335
|
+
if (state?.genericInterruptRequests) {
|
|
4336
|
+
for (const [interruptId, request] of state.genericInterruptRequests) {
|
|
4337
|
+
this.resumeGenericInterruptRequests.set(interruptId, request)
|
|
4338
|
+
}
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4341
|
+
|
|
4342
|
+
private async applyDurableGenericInterruptResolution(): Promise<void> {
|
|
4343
|
+
if (this.resumeGenericInterruptRequests.size === 0) return
|
|
4344
|
+
const resolutions = [
|
|
4345
|
+
...this.resumeGenericInterruptRequests.entries(),
|
|
4346
|
+
].flatMap(([interruptId, request]) => {
|
|
4347
|
+
const resolution = this.resumeGenericInterrupts.get(interruptId)
|
|
4348
|
+
if (!resolution) return []
|
|
4349
|
+
return [
|
|
4350
|
+
resolution.status === 'resolved'
|
|
4351
|
+
? {
|
|
4352
|
+
request,
|
|
4353
|
+
status: 'resolved' as const,
|
|
4354
|
+
response: resolution.payload,
|
|
4355
|
+
}
|
|
4356
|
+
: { request, status: 'cancelled' as const },
|
|
4357
|
+
]
|
|
4358
|
+
})
|
|
4359
|
+
const collection: InterruptResolutionCollection = {
|
|
4360
|
+
for: (definition) =>
|
|
4361
|
+
resolutions.filter(
|
|
4362
|
+
(resolution) => resolution.request.definition === definition,
|
|
4363
|
+
) as never,
|
|
4364
|
+
all: (...definitions: Array<InterruptDefinition<any, any, any, any>>) =>
|
|
4365
|
+
definitions.length === 0
|
|
4366
|
+
? resolutions
|
|
4367
|
+
: resolutions.filter((resolution) =>
|
|
4368
|
+
definitions.includes(resolution.request.definition),
|
|
4369
|
+
),
|
|
4370
|
+
}
|
|
4371
|
+
const policy = await this.middlewareRunner.runOnInterruptResolution(
|
|
4372
|
+
this.middlewareCtx,
|
|
4373
|
+
collection,
|
|
4374
|
+
)
|
|
4375
|
+
if (policy.toolResume === 'stop') {
|
|
4376
|
+
this.earlyTermination = true
|
|
4377
|
+
} else if (policy.toolResume === 'cancel') {
|
|
4378
|
+
for (const toolCall of this.getPendingToolCallsFromMessages()) {
|
|
4379
|
+
this.resumeCancelledToolCallIds.add(toolCall.id)
|
|
4380
|
+
}
|
|
4381
|
+
}
|
|
3570
4382
|
}
|
|
3571
4383
|
|
|
3572
4384
|
private applyMiddlewareConfig(config: ChatMiddlewareConfig): void {
|
|
3573
4385
|
this.applyResumeToolState(config.resumeToolState)
|
|
3574
4386
|
this.messages = config.messages
|
|
3575
4387
|
this.systemPrompts = config.systemPrompts
|
|
4388
|
+
assertUniqueToolNames(config.tools)
|
|
3576
4389
|
this.tools = config.tools
|
|
3577
4390
|
this.params = {
|
|
3578
4391
|
...this.params,
|
|
@@ -3604,6 +4417,9 @@ class TextEngine<
|
|
|
3604
4417
|
chunk,
|
|
3605
4418
|
)
|
|
3606
4419
|
for (const outputChunk of outputChunks) {
|
|
4420
|
+
if (outputChunk.type === EventType.RUN_STARTED) {
|
|
4421
|
+
this.hasPublicRunStarted = true
|
|
4422
|
+
}
|
|
3607
4423
|
yield outputChunk
|
|
3608
4424
|
this.middlewareCtx.chunkIndex++
|
|
3609
4425
|
}
|
|
@@ -3744,64 +4560,136 @@ export function chat<
|
|
|
3744
4560
|
TStream,
|
|
3745
4561
|
any
|
|
3746
4562
|
>['tools'] = TextActivityOptions<TAdapter, TSchema, TStream, any>['tools'],
|
|
3747
|
-
const
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
>['middleware'] = TextActivityOptions<
|
|
3753
|
-
TAdapter,
|
|
3754
|
-
TSchema,
|
|
3755
|
-
TStream,
|
|
3756
|
-
any
|
|
3757
|
-
>['middleware'],
|
|
4563
|
+
const TInterrupts extends ReadonlyArray<
|
|
4564
|
+
InterruptDefinition<any, any, any, any>
|
|
4565
|
+
> = [],
|
|
4566
|
+
TContext = unknown,
|
|
4567
|
+
const TMiddleware extends Array<unknown> | undefined = undefined,
|
|
3758
4568
|
>(
|
|
3759
4569
|
options: TextActivityOptionsWithContext<
|
|
3760
4570
|
TAdapter,
|
|
3761
4571
|
TSchema,
|
|
3762
4572
|
TStream,
|
|
3763
4573
|
TTools,
|
|
4574
|
+
TInterrupts,
|
|
4575
|
+
TContext,
|
|
3764
4576
|
TMiddleware
|
|
3765
4577
|
>,
|
|
3766
4578
|
): TextActivityResult<TSchema, TStream, TTools> {
|
|
3767
|
-
|
|
4579
|
+
validateInterruptDefinitions(options.interrupts)
|
|
4580
|
+
validateCapabilities(
|
|
4581
|
+
readRuntimeMiddleware(options.middleware) ?? [],
|
|
4582
|
+
options.adapter,
|
|
4583
|
+
)
|
|
4584
|
+
if (options.tools) {
|
|
4585
|
+
assertUniqueToolNames(options.tools)
|
|
4586
|
+
}
|
|
3768
4587
|
|
|
3769
4588
|
const { outputSchema, stream } = options
|
|
3770
4589
|
|
|
3771
|
-
// outputSchema + stream:true is the only branch that streams structured
|
|
3772
|
-
// output. Without an explicit `stream: true`, schema-bearing calls run the
|
|
3773
|
-
// agent loop and resolve to a typed Promise<InferSchemaType<TSchema>>.
|
|
3774
4590
|
if (outputSchema && stream === true) {
|
|
3775
|
-
return runStreamingStructuredOutput(
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
4591
|
+
return runStreamingStructuredOutput(
|
|
4592
|
+
toRuntimeTextActivityOptions(options, {
|
|
4593
|
+
outputSchema,
|
|
4594
|
+
stream: true,
|
|
4595
|
+
}),
|
|
4596
|
+
) as TextActivityResult<TSchema, TStream, TTools>
|
|
3780
4597
|
}
|
|
3781
4598
|
|
|
3782
|
-
// If outputSchema is provided, run agentic structured output (Promise<T>)
|
|
3783
4599
|
if (outputSchema) {
|
|
3784
|
-
return runAgenticStructuredOutput(
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
4600
|
+
return runAgenticStructuredOutput(
|
|
4601
|
+
toRuntimeTextActivityOptions(options, {
|
|
4602
|
+
outputSchema,
|
|
4603
|
+
stream: false,
|
|
4604
|
+
}),
|
|
4605
|
+
) as TextActivityResult<TSchema, TStream, TTools>
|
|
3788
4606
|
}
|
|
3789
4607
|
|
|
3790
|
-
// If stream is explicitly false, run non-streaming text
|
|
3791
4608
|
if (stream === false) {
|
|
3792
|
-
return runNonStreamingText(
|
|
3793
|
-
|
|
4609
|
+
return runNonStreamingText(
|
|
4610
|
+
toRuntimeTextActivityOptions(options, {
|
|
4611
|
+
outputSchema: undefined,
|
|
4612
|
+
stream: false,
|
|
4613
|
+
}),
|
|
4614
|
+
) as TextActivityResult<TSchema, TStream, TTools>
|
|
4615
|
+
}
|
|
4616
|
+
|
|
4617
|
+
return runStreamingText(
|
|
4618
|
+
toRuntimeTextActivityOptions(options, {
|
|
3794
4619
|
outputSchema: undefined,
|
|
3795
|
-
stream,
|
|
3796
|
-
})
|
|
4620
|
+
stream: true,
|
|
4621
|
+
}),
|
|
4622
|
+
) as TextActivityResult<TSchema, TStream, TTools>
|
|
4623
|
+
}
|
|
4624
|
+
|
|
4625
|
+
type RuntimeTextActivityOptions<
|
|
4626
|
+
TAdapter extends AnyTextAdapter,
|
|
4627
|
+
TSchema extends SchemaInput | undefined,
|
|
4628
|
+
TStream extends boolean,
|
|
4629
|
+
> = Omit<TextActivityOptions<TAdapter, TSchema, TStream, any>, 'middleware'> & {
|
|
4630
|
+
middleware?: Array<AnyChatMiddleware>
|
|
4631
|
+
}
|
|
4632
|
+
|
|
4633
|
+
function readRuntimeMiddleware(
|
|
4634
|
+
middleware: unknown,
|
|
4635
|
+
): Array<AnyChatMiddleware> | undefined {
|
|
4636
|
+
if (middleware === undefined) return undefined
|
|
4637
|
+
if (!Array.isArray(middleware)) {
|
|
4638
|
+
throw new TypeError('Chat middleware must be an array.')
|
|
3797
4639
|
}
|
|
4640
|
+
return middleware
|
|
4641
|
+
}
|
|
3798
4642
|
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
4643
|
+
function toRuntimeTextActivityOptions<
|
|
4644
|
+
TAdapter extends AnyTextAdapter,
|
|
4645
|
+
TInputSchema extends SchemaInput | undefined,
|
|
4646
|
+
TInputStream extends boolean,
|
|
4647
|
+
TOutputSchema extends SchemaInput | undefined,
|
|
4648
|
+
TOutputStream extends boolean,
|
|
4649
|
+
TTools extends TextActivityOptions<
|
|
4650
|
+
TAdapter,
|
|
4651
|
+
TInputSchema,
|
|
4652
|
+
TInputStream,
|
|
4653
|
+
any
|
|
4654
|
+
>['tools'],
|
|
4655
|
+
TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>,
|
|
4656
|
+
TContext,
|
|
4657
|
+
TMiddleware extends Array<unknown> | undefined,
|
|
4658
|
+
>(
|
|
4659
|
+
options: TextActivityOptionsWithContext<
|
|
4660
|
+
TAdapter,
|
|
4661
|
+
TInputSchema,
|
|
4662
|
+
TInputStream,
|
|
4663
|
+
TTools,
|
|
4664
|
+
TInterrupts,
|
|
4665
|
+
TContext,
|
|
4666
|
+
TMiddleware
|
|
4667
|
+
>,
|
|
4668
|
+
overrides: { outputSchema: TOutputSchema; stream: TOutputStream },
|
|
4669
|
+
): RuntimeTextActivityOptions<TAdapter, TOutputSchema, TOutputStream> {
|
|
4670
|
+
const { middleware, ...rest } = options
|
|
4671
|
+
return {
|
|
4672
|
+
...rest,
|
|
4673
|
+
...overrides,
|
|
4674
|
+
...(middleware === undefined
|
|
4675
|
+
? {}
|
|
4676
|
+
: { middleware: readRuntimeMiddleware(middleware) }),
|
|
4677
|
+
}
|
|
4678
|
+
}
|
|
4679
|
+
|
|
4680
|
+
function validateInterruptDefinitions(
|
|
4681
|
+
definitions:
|
|
4682
|
+
| ReadonlyArray<InterruptDefinition<any, any, any, any>>
|
|
4683
|
+
| undefined,
|
|
4684
|
+
): void {
|
|
4685
|
+
if (!definitions) return
|
|
4686
|
+
const seen = new Set<string>()
|
|
4687
|
+
for (const definition of definitions) {
|
|
4688
|
+
if (seen.has(definition.id)) {
|
|
4689
|
+
throw new Error(`Duplicate interrupt definition id: ${definition.id}`)
|
|
4690
|
+
}
|
|
4691
|
+
seen.add(definition.id)
|
|
4692
|
+
}
|
|
3805
4693
|
}
|
|
3806
4694
|
|
|
3807
4695
|
/**
|
|
@@ -3853,8 +4741,8 @@ function publishDeliverySeams(
|
|
|
3853
4741
|
* returns, so the identity has to be minted out here and the engine reached back
|
|
3854
4742
|
* through `engineRef`, which the body fills as soon as its engine exists.
|
|
3855
4743
|
*/
|
|
3856
|
-
function runStreamingText
|
|
3857
|
-
options:
|
|
4744
|
+
function runStreamingText(
|
|
4745
|
+
options: RuntimeTextActivityOptions<AnyTextAdapter, undefined, boolean>,
|
|
3858
4746
|
): AsyncIterable<StreamChunk> {
|
|
3859
4747
|
const engineRef: DeliveryEngineRef = {}
|
|
3860
4748
|
const stream = streamTextChunks(options, engineRef)
|
|
@@ -3862,8 +4750,8 @@ function runStreamingText<TContext = unknown>(
|
|
|
3862
4750
|
return stream
|
|
3863
4751
|
}
|
|
3864
4752
|
|
|
3865
|
-
async function* streamTextChunks
|
|
3866
|
-
options:
|
|
4753
|
+
async function* streamTextChunks(
|
|
4754
|
+
options: RuntimeTextActivityOptions<AnyTextAdapter, undefined, boolean>,
|
|
3867
4755
|
engineRef: DeliveryEngineRef,
|
|
3868
4756
|
): AsyncIterable<StreamChunk> {
|
|
3869
4757
|
const { adapter, middleware, context, debug, mcp, ...textOptions } = options
|
|
@@ -3882,7 +4770,7 @@ async function* streamTextChunks<TContext = unknown>(
|
|
|
3882
4770
|
params: { ...textOptions, model, logger } as TextOptions<
|
|
3883
4771
|
Record<string, any>,
|
|
3884
4772
|
Record<string, any>,
|
|
3885
|
-
|
|
4773
|
+
any
|
|
3886
4774
|
>,
|
|
3887
4775
|
middleware,
|
|
3888
4776
|
context,
|
|
@@ -3904,19 +4792,13 @@ async function* streamTextChunks<TContext = unknown>(
|
|
|
3904
4792
|
* Run non-streaming text - collects all content and returns as a string.
|
|
3905
4793
|
* Runs the full agentic loop (if tools are provided) but returns collected text.
|
|
3906
4794
|
*/
|
|
3907
|
-
function runNonStreamingText
|
|
3908
|
-
options:
|
|
4795
|
+
function runNonStreamingText(
|
|
4796
|
+
options: RuntimeTextActivityOptions<AnyTextAdapter, undefined, false>,
|
|
3909
4797
|
): Promise<string> {
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
AnyTextAdapter,
|
|
3915
|
-
undefined,
|
|
3916
|
-
true,
|
|
3917
|
-
TContext
|
|
3918
|
-
>,
|
|
3919
|
-
)
|
|
4798
|
+
const stream = runStreamingText({
|
|
4799
|
+
...options,
|
|
4800
|
+
stream: true,
|
|
4801
|
+
})
|
|
3920
4802
|
|
|
3921
4803
|
return streamToText(stream)
|
|
3922
4804
|
}
|
|
@@ -3927,11 +4809,8 @@ function runNonStreamingText<TContext = unknown>(
|
|
|
3927
4809
|
* 2. Once complete, call adapter.structuredOutput with the conversation context
|
|
3928
4810
|
* 3. Validate and return the structured result
|
|
3929
4811
|
*/
|
|
3930
|
-
async function runAgenticStructuredOutput<
|
|
3931
|
-
|
|
3932
|
-
TContext = unknown,
|
|
3933
|
-
>(
|
|
3934
|
-
options: TextActivityOptions<AnyTextAdapter, TSchema, boolean, TContext>,
|
|
4812
|
+
async function runAgenticStructuredOutput<TSchema extends SchemaInput>(
|
|
4813
|
+
options: RuntimeTextActivityOptions<AnyTextAdapter, TSchema, boolean>,
|
|
3935
4814
|
): Promise<InferSchemaType<TSchema>> {
|
|
3936
4815
|
const {
|
|
3937
4816
|
adapter,
|
|
@@ -4002,7 +4881,7 @@ async function runAgenticStructuredOutput<
|
|
|
4002
4881
|
params: { ...textOptions, model, logger } as TextOptions<
|
|
4003
4882
|
Record<string, unknown>,
|
|
4004
4883
|
Record<string, unknown>,
|
|
4005
|
-
|
|
4884
|
+
any
|
|
4006
4885
|
>,
|
|
4007
4886
|
middleware,
|
|
4008
4887
|
context,
|
|
@@ -4065,6 +4944,15 @@ async function runAgenticStructuredOutput<
|
|
|
4065
4944
|
* Uses an `unknown`-input runtime check rather than `as` casts so the engine
|
|
4066
4945
|
* stays cast-free in its hot path.
|
|
4067
4946
|
*/
|
|
4947
|
+
function readCustomEventMessageId(value: unknown): string | undefined {
|
|
4948
|
+
if (typeof value !== 'object' || value === null) return undefined
|
|
4949
|
+
if (!('messageId' in value)) return undefined
|
|
4950
|
+
const messageId = value.messageId
|
|
4951
|
+
return typeof messageId === 'string' && messageId !== ''
|
|
4952
|
+
? messageId
|
|
4953
|
+
: undefined
|
|
4954
|
+
}
|
|
4955
|
+
|
|
4068
4956
|
function readStructuredOutputCompleteValue(
|
|
4069
4957
|
value: unknown,
|
|
4070
4958
|
): { object: unknown; raw: string; reasoning?: string } | null {
|
|
@@ -4210,11 +5098,8 @@ async function* fallbackStructuredOutputStream(
|
|
|
4210
5098
|
* synchronously at call time rather than as a yielded RUN_ERROR mid-stream —
|
|
4211
5099
|
* those are programmer errors, not runtime conditions.
|
|
4212
5100
|
*/
|
|
4213
|
-
function runStreamingStructuredOutput<
|
|
4214
|
-
|
|
4215
|
-
TContext = unknown,
|
|
4216
|
-
>(
|
|
4217
|
-
options: TextActivityOptions<AnyTextAdapter, TSchema, true, TContext>,
|
|
5101
|
+
function runStreamingStructuredOutput<TSchema extends SchemaInput>(
|
|
5102
|
+
options: RuntimeTextActivityOptions<AnyTextAdapter, TSchema, true>,
|
|
4218
5103
|
): StructuredOutputStream<InferSchemaType<TSchema>> {
|
|
4219
5104
|
const { outputSchema } = options
|
|
4220
5105
|
|
|
@@ -4275,11 +5160,8 @@ type StructuredOutputStreamInternal<T> = AsyncIterable<
|
|
|
4275
5160
|
StreamChunk | StructuredOutputCompleteEvent<T>
|
|
4276
5161
|
>
|
|
4277
5162
|
|
|
4278
|
-
async function* runStreamingStructuredOutputImpl<
|
|
4279
|
-
|
|
4280
|
-
TContext = unknown,
|
|
4281
|
-
>(
|
|
4282
|
-
options: TextActivityOptions<AnyTextAdapter, TSchema, true, TContext>,
|
|
5163
|
+
async function* runStreamingStructuredOutputImpl<TSchema extends SchemaInput>(
|
|
5164
|
+
options: RuntimeTextActivityOptions<AnyTextAdapter, TSchema, true>,
|
|
4283
5165
|
jsonSchema: NonNullable<ReturnType<typeof convertSchemaToJsonSchema>>,
|
|
4284
5166
|
normalize: (data: unknown) => unknown,
|
|
4285
5167
|
engineRef: DeliveryEngineRef,
|
|
@@ -4322,7 +5204,7 @@ async function* runStreamingStructuredOutputImpl<
|
|
|
4322
5204
|
params: { ...textOptions, model, logger } as TextOptions<
|
|
4323
5205
|
Record<string, unknown>,
|
|
4324
5206
|
Record<string, unknown>,
|
|
4325
|
-
|
|
5207
|
+
any
|
|
4326
5208
|
>,
|
|
4327
5209
|
middleware,
|
|
4328
5210
|
context,
|