@tanstack/ai 0.46.0 → 0.47.2
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 +440 -79
- 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/adapter-internals.d.ts +6 -0
- package/dist/esm/adapter-internals.js +4 -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 +6 -1
- package/dist/esm/index.js +4 -1
- 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/types.d.ts +11 -3
- package/dist/esm/utilities/chat-params.js +10 -1
- package/dist/esm/utilities/chat-params.js.map +1 -1
- package/package.json +3 -3
- package/skills/ai-core/media-generation/SKILL.md +4 -1
- 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 +1030 -211
- 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/adapter-internals.ts +20 -0
- package/src/client.ts +20 -0
- package/src/generic-interrupt-continuation.ts +162 -0
- package/src/index.ts +34 -0
- package/src/interrupt-definition.ts +581 -0
- package/src/interrupt-resume.ts +156 -25
- package/src/interrupts.ts +13 -3
- package/src/types.ts +11 -3
- package/src/utilities/chat-params.ts +16 -3
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
ContentPart,
|
|
5
5
|
MessagePart,
|
|
6
6
|
ModelMessage,
|
|
7
|
+
StructuredOutputPart,
|
|
7
8
|
TextPart,
|
|
8
9
|
ToolCallPart,
|
|
9
10
|
UIMessage,
|
|
@@ -26,9 +27,9 @@ function isContentPart(part: MessagePart): part is ContentPart {
|
|
|
26
27
|
)
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
function safeJsonStringify(value: unknown): string {
|
|
30
|
+
export function safeJsonStringify(value: unknown): string {
|
|
30
31
|
try {
|
|
31
|
-
return JSON.stringify(value)
|
|
32
|
+
return JSON.stringify(value) ?? ''
|
|
32
33
|
} catch {
|
|
33
34
|
return ''
|
|
34
35
|
}
|
|
@@ -196,6 +197,7 @@ function buildUserOrToolMessage(uiMessage: UIMessage): ModelMessage {
|
|
|
196
197
|
// Accumulator for building an assistant segment (content + tool calls)
|
|
197
198
|
interface AssistantSegment {
|
|
198
199
|
contentParts: Array<ContentPart>
|
|
200
|
+
structuredOutput?: StructuredOutputPart
|
|
199
201
|
toolCalls: Array<{
|
|
200
202
|
id: string
|
|
201
203
|
type: 'function'
|
|
@@ -255,6 +257,9 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
|
|
|
255
257
|
content,
|
|
256
258
|
...(hasToolCalls && { toolCalls: current.toolCalls }),
|
|
257
259
|
...(pendingThinking.length > 0 && { thinking: pendingThinking }),
|
|
260
|
+
...(current.structuredOutput && {
|
|
261
|
+
structuredOutput: current.structuredOutput,
|
|
262
|
+
}),
|
|
258
263
|
...(uiMessage.createdAt !== undefined && {
|
|
259
264
|
createdAt: uiMessage.createdAt,
|
|
260
265
|
}),
|
|
@@ -333,6 +338,7 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
|
|
|
333
338
|
: ''
|
|
334
339
|
if (serialized !== '') {
|
|
335
340
|
current.contentParts.push({ type: 'text', content: serialized })
|
|
341
|
+
current.structuredOutput = part
|
|
336
342
|
}
|
|
337
343
|
}
|
|
338
344
|
break
|
|
@@ -445,7 +451,9 @@ export function modelMessageToUIMessage(
|
|
|
445
451
|
|
|
446
452
|
// Handle tool results (when role is "tool") - only produce tool-result part,
|
|
447
453
|
// not a text part (the content IS the tool result, not display text)
|
|
448
|
-
if (modelMessage.role === '
|
|
454
|
+
if (modelMessage.role === 'assistant' && modelMessage.structuredOutput) {
|
|
455
|
+
parts.push(modelMessage.structuredOutput)
|
|
456
|
+
} else if (modelMessage.role === 'tool' && modelMessage.toolCallId) {
|
|
449
457
|
parts.push({
|
|
450
458
|
type: 'tool-result',
|
|
451
459
|
toolCallId: modelMessage.toolCallId,
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { CapabilityHandle } from './capabilities'
|
|
2
2
|
import type { AnyChatMiddleware, ChatMiddleware } from './types'
|
|
3
3
|
import type { DefinedChatMiddleware } from './define'
|
|
4
|
+
import type { InterruptDefinition } from '../../../interrupt-definition'
|
|
5
|
+
|
|
6
|
+
type AnyInterruptDefinition = InterruptDefinition<any, any, any, any>
|
|
4
7
|
|
|
5
8
|
/** Union of capability NAME literals from a tuple of handles. */
|
|
6
9
|
export type NamesOf<T extends ReadonlyArray<CapabilityHandle>> =
|
|
@@ -67,19 +70,41 @@ export type CheckCoverage<TList extends ReadonlyArray<AnyChatMiddleware>> = [
|
|
|
67
70
|
export interface ChatMiddlewareBuilder<
|
|
68
71
|
TList extends ReadonlyArray<AnyChatMiddleware>,
|
|
69
72
|
TProvided extends string,
|
|
73
|
+
TInterruptDefinitions extends AnyInterruptDefinition = never,
|
|
70
74
|
> {
|
|
71
75
|
use: <
|
|
72
76
|
TRequires extends ReadonlyArray<CapabilityHandle>,
|
|
73
77
|
TProvides extends ReadonlyArray<CapabilityHandle>,
|
|
74
78
|
TContext = unknown,
|
|
79
|
+
TMiddlewareInterruptDefinitions extends AnyInterruptDefinition =
|
|
80
|
+
TInterruptDefinitions,
|
|
75
81
|
>(
|
|
76
82
|
middleware: [NamesOf<TRequires>] extends [TProvided]
|
|
77
|
-
? DefinedChatMiddleware<
|
|
78
|
-
|
|
83
|
+
? DefinedChatMiddleware<
|
|
84
|
+
TContext,
|
|
85
|
+
TRequires,
|
|
86
|
+
TProvides,
|
|
87
|
+
TMiddlewareInterruptDefinitions
|
|
88
|
+
>
|
|
89
|
+
: DefinedChatMiddleware<
|
|
90
|
+
TContext,
|
|
91
|
+
TRequires,
|
|
92
|
+
TProvides,
|
|
93
|
+
TMiddlewareInterruptDefinitions
|
|
94
|
+
> &
|
|
79
95
|
MissingCapabilities<Exclude<NamesOf<TRequires>, TProvided>>,
|
|
80
96
|
) => ChatMiddlewareBuilder<
|
|
81
|
-
readonly [
|
|
82
|
-
|
|
97
|
+
readonly [
|
|
98
|
+
...TList,
|
|
99
|
+
DefinedChatMiddleware<
|
|
100
|
+
TContext,
|
|
101
|
+
TRequires,
|
|
102
|
+
TProvides,
|
|
103
|
+
TMiddlewareInterruptDefinitions
|
|
104
|
+
>,
|
|
105
|
+
],
|
|
106
|
+
TProvided | NamesOf<TProvides>,
|
|
107
|
+
TInterruptDefinitions | TMiddlewareInterruptDefinitions
|
|
83
108
|
>
|
|
84
109
|
|
|
85
110
|
build: () => [...TList]
|
|
@@ -10,6 +10,9 @@ import type {
|
|
|
10
10
|
ChatMiddlewareContext,
|
|
11
11
|
ErrorInfo,
|
|
12
12
|
FinishInfo,
|
|
13
|
+
InterruptBoundaryPhase,
|
|
14
|
+
InterruptResolutionCollection,
|
|
15
|
+
InterruptToolResume,
|
|
13
16
|
IterationInfo,
|
|
14
17
|
SandboxFileHookEvent,
|
|
15
18
|
StructuredOutputMiddlewareConfig,
|
|
@@ -17,6 +20,10 @@ import type {
|
|
|
17
20
|
ToolPhaseCompleteInfo,
|
|
18
21
|
UsageInfo,
|
|
19
22
|
} from './types'
|
|
23
|
+
import type {
|
|
24
|
+
GenericInterruptRequest,
|
|
25
|
+
InterruptDefinition,
|
|
26
|
+
} from '../../../interrupt-definition'
|
|
20
27
|
|
|
21
28
|
/** One middleware's terminal-hook throw, captured instead of propagated. */
|
|
22
29
|
interface HookFailure {
|
|
@@ -25,7 +32,7 @@ interface HookFailure {
|
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
/** Check if a middleware should be skipped for instrumentation events. */
|
|
28
|
-
function shouldSkipInstrumentation(mw: ChatMiddleware<any>): boolean {
|
|
35
|
+
function shouldSkipInstrumentation(mw: ChatMiddleware<any, any>): boolean {
|
|
29
36
|
return mw.name === 'devtools' || mw.name === 'strip-to-spec'
|
|
30
37
|
}
|
|
31
38
|
|
|
@@ -43,12 +50,18 @@ function instrumentCtx(ctx: ChatMiddlewareContext<any>) {
|
|
|
43
50
|
* Internal middleware runner that manages composed execution of middleware hooks.
|
|
44
51
|
* Created once per chat() invocation.
|
|
45
52
|
*/
|
|
46
|
-
export class MiddlewareRunner<
|
|
47
|
-
|
|
53
|
+
export class MiddlewareRunner<
|
|
54
|
+
TContext = unknown,
|
|
55
|
+
TInterruptDefinitions extends InterruptDefinition<any, any, any, any> =
|
|
56
|
+
InterruptDefinition<any, any, any, any>,
|
|
57
|
+
> {
|
|
58
|
+
private readonly middlewares: ReadonlyArray<
|
|
59
|
+
ChatMiddleware<TContext, TInterruptDefinitions>
|
|
60
|
+
>
|
|
48
61
|
private readonly logger: InternalLogger
|
|
49
62
|
|
|
50
63
|
constructor(
|
|
51
|
-
middlewares: ReadonlyArray<ChatMiddleware<TContext>>,
|
|
64
|
+
middlewares: ReadonlyArray<ChatMiddleware<TContext, TInterruptDefinitions>>,
|
|
52
65
|
logger: InternalLogger,
|
|
53
66
|
) {
|
|
54
67
|
this.middlewares = middlewares
|
|
@@ -59,6 +72,83 @@ export class MiddlewareRunner<TContext = unknown> {
|
|
|
59
72
|
return this.middlewares.length > 0
|
|
60
73
|
}
|
|
61
74
|
|
|
75
|
+
async runOnInterruptBoundary(
|
|
76
|
+
ctx: ChatMiddlewareContext<TContext> & { phase: InterruptBoundaryPhase },
|
|
77
|
+
): Promise<ReadonlyArray<GenericInterruptRequest<TInterruptDefinitions>>> {
|
|
78
|
+
const requests: Array<GenericInterruptRequest<TInterruptDefinitions>> = []
|
|
79
|
+
for (const mw of this.middlewares) {
|
|
80
|
+
if (mw.onInterruptBoundary) {
|
|
81
|
+
const skip = shouldSkipInstrumentation(mw)
|
|
82
|
+
const start = Date.now()
|
|
83
|
+
const result = await mw.onInterruptBoundary(ctx)
|
|
84
|
+
if (result?.interrupts) requests.push(...result.interrupts)
|
|
85
|
+
if (!skip) {
|
|
86
|
+
this.logger.middleware(
|
|
87
|
+
`hook=onInterruptBoundary middleware=${mw.name ?? 'unnamed'}`,
|
|
88
|
+
{
|
|
89
|
+
middleware: mw.name ?? 'unnamed',
|
|
90
|
+
hook: 'onInterruptBoundary',
|
|
91
|
+
},
|
|
92
|
+
)
|
|
93
|
+
aiEventClient.emit('middleware:hook:executed', {
|
|
94
|
+
...instrumentCtx(ctx),
|
|
95
|
+
middlewareName: mw.name || 'unnamed',
|
|
96
|
+
hookName: 'onInterruptBoundary',
|
|
97
|
+
iteration: ctx.iteration,
|
|
98
|
+
duration: Date.now() - start,
|
|
99
|
+
hasTransform: result?.interrupts !== undefined,
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return requests
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async runOnInterruptResolution(
|
|
108
|
+
ctx: ChatMiddlewareContext<TContext>,
|
|
109
|
+
resolutions: InterruptResolutionCollection<TInterruptDefinitions>,
|
|
110
|
+
): Promise<{ toolResume?: InterruptToolResume }> {
|
|
111
|
+
let toolResume: InterruptToolResume | undefined
|
|
112
|
+
for (const mw of this.middlewares) {
|
|
113
|
+
if (mw.onInterruptResolution) {
|
|
114
|
+
const skip = shouldSkipInstrumentation(mw)
|
|
115
|
+
const start = Date.now()
|
|
116
|
+
const next = await mw.onInterruptResolution(ctx, resolutions)
|
|
117
|
+
if (next?.toolResume !== undefined) {
|
|
118
|
+
const priority: Record<InterruptToolResume, number> = {
|
|
119
|
+
continue: 0,
|
|
120
|
+
cancel: 1,
|
|
121
|
+
stop: 2,
|
|
122
|
+
}
|
|
123
|
+
if (
|
|
124
|
+
toolResume === undefined ||
|
|
125
|
+
priority[next.toolResume] > priority[toolResume]
|
|
126
|
+
) {
|
|
127
|
+
toolResume = next.toolResume
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (!skip) {
|
|
131
|
+
this.logger.middleware(
|
|
132
|
+
`hook=onInterruptResolution middleware=${mw.name ?? 'unnamed'}`,
|
|
133
|
+
{
|
|
134
|
+
middleware: mw.name ?? 'unnamed',
|
|
135
|
+
hook: 'onInterruptResolution',
|
|
136
|
+
},
|
|
137
|
+
)
|
|
138
|
+
aiEventClient.emit('middleware:hook:executed', {
|
|
139
|
+
...instrumentCtx(ctx),
|
|
140
|
+
middlewareName: mw.name || 'unnamed',
|
|
141
|
+
hookName: 'onInterruptResolution',
|
|
142
|
+
iteration: ctx.iteration,
|
|
143
|
+
duration: Date.now() - start,
|
|
144
|
+
hasTransform: next !== undefined,
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return toolResume === undefined ? {} : { toolResume }
|
|
150
|
+
}
|
|
151
|
+
|
|
62
152
|
/**
|
|
63
153
|
* Pipe config through all middleware onConfig hooks in order.
|
|
64
154
|
* Each middleware receives the merged config from previous middleware.
|
|
@@ -492,7 +582,7 @@ export class MiddlewareRunner<TContext = unknown> {
|
|
|
492
582
|
* {@link runOnError}.
|
|
493
583
|
*/
|
|
494
584
|
private async captureTerminalHook(
|
|
495
|
-
mw: ChatMiddleware<TContext>,
|
|
585
|
+
mw: ChatMiddleware<TContext, TInterruptDefinitions>,
|
|
496
586
|
hookName: 'onFinish' | 'onAbort' | 'onError',
|
|
497
587
|
invoke: () => void | Promise<void>,
|
|
498
588
|
): Promise<HookFailure | undefined> {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { CapabilityHandle } from './capabilities'
|
|
2
2
|
import type { ChatMiddleware } from './types'
|
|
3
|
+
import type { InterruptDefinition } from '../../../interrupt-definition'
|
|
4
|
+
|
|
5
|
+
type AnyInterruptDefinition = InterruptDefinition<any, any, any, any>
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* A middleware whose `requires`/`provides` tuple types are captured precisely
|
|
@@ -9,7 +12,8 @@ export interface DefinedChatMiddleware<
|
|
|
9
12
|
TContext,
|
|
10
13
|
TRequires extends ReadonlyArray<CapabilityHandle>,
|
|
11
14
|
TProvides extends ReadonlyArray<CapabilityHandle>,
|
|
12
|
-
|
|
15
|
+
TInterruptDefinitions extends AnyInterruptDefinition = never,
|
|
16
|
+
> extends ChatMiddleware<TContext, TInterruptDefinitions> {
|
|
13
17
|
requires?: TRequires
|
|
14
18
|
provides?: TProvides
|
|
15
19
|
}
|
|
@@ -24,11 +28,17 @@ export function defineChatMiddleware<
|
|
|
24
28
|
TContext = unknown,
|
|
25
29
|
const TRequires extends ReadonlyArray<CapabilityHandle> = readonly [],
|
|
26
30
|
const TProvides extends ReadonlyArray<CapabilityHandle> = readonly [],
|
|
31
|
+
TInterruptDefinitions extends AnyInterruptDefinition = never,
|
|
27
32
|
>(
|
|
28
|
-
middleware: ChatMiddleware<TContext> & {
|
|
33
|
+
middleware: ChatMiddleware<TContext, TInterruptDefinitions> & {
|
|
29
34
|
requires?: TRequires
|
|
30
35
|
provides?: TProvides
|
|
31
36
|
},
|
|
32
|
-
): DefinedChatMiddleware<
|
|
37
|
+
): DefinedChatMiddleware<
|
|
38
|
+
TContext,
|
|
39
|
+
TRequires,
|
|
40
|
+
TProvides,
|
|
41
|
+
TInterruptDefinitions
|
|
42
|
+
> {
|
|
33
43
|
return middleware
|
|
34
44
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createCapability } from './capabilities'
|
|
2
|
+
import type { InterruptDefinition } from '../../../interrupt-definition'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Internal per-run registry of first-party generic interrupt definitions.
|
|
6
|
+
*
|
|
7
|
+
* Persistence uses this bridge while restoring a durable interrupt. It never
|
|
8
|
+
* receives a definition from the stored record; it can only look up one that
|
|
9
|
+
* the current chat call registered.
|
|
10
|
+
*/
|
|
11
|
+
export interface GenericInterruptDefinitionRegistry {
|
|
12
|
+
readonly definitions: ReadonlyMap<
|
|
13
|
+
string,
|
|
14
|
+
InterruptDefinition<any, any, any, any>
|
|
15
|
+
>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const GenericInterruptDefinitionRegistryCapability =
|
|
19
|
+
createCapability<GenericInterruptDefinitionRegistry>()(
|
|
20
|
+
'generic-interrupt-definition-registry',
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
export const [
|
|
24
|
+
getGenericInterruptDefinitionRegistry,
|
|
25
|
+
provideGenericInterruptDefinitionRegistry,
|
|
26
|
+
] = GenericInterruptDefinitionRegistryCapability
|
|
@@ -18,8 +18,23 @@ export type {
|
|
|
18
18
|
SandboxFileEvent,
|
|
19
19
|
SandboxFileHookEvent,
|
|
20
20
|
ChatSandboxHooks,
|
|
21
|
+
InterruptBoundaryPhase,
|
|
22
|
+
InterruptToolResume,
|
|
23
|
+
InterruptResolutionCollection,
|
|
24
|
+
GenericInterruptResolution,
|
|
25
|
+
InterruptBoundaryResult,
|
|
26
|
+
InterruptResolutionResult,
|
|
21
27
|
} from './types'
|
|
22
28
|
|
|
29
|
+
export { INTERRUPT_BOUNDARY_PHASES, INTERRUPT_TOOL_RESUMES } from './types'
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
GenericInterruptDefinitionRegistryCapability,
|
|
33
|
+
getGenericInterruptDefinitionRegistry,
|
|
34
|
+
provideGenericInterruptDefinitionRegistry,
|
|
35
|
+
} from './generic-interrupts'
|
|
36
|
+
export type { GenericInterruptDefinitionRegistry } from './generic-interrupts'
|
|
37
|
+
|
|
23
38
|
export { MiddlewareRunner } from './compose'
|
|
24
39
|
|
|
25
40
|
export { createCapability, CapabilityRegistry } from './capabilities'
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
StandardJSONSchemaV1,
|
|
3
|
+
StandardSchemaV1,
|
|
4
|
+
} from '@standard-schema/spec'
|
|
1
5
|
import type {
|
|
2
6
|
AgentLoopState,
|
|
3
7
|
JSONSchema,
|
|
@@ -10,6 +14,10 @@ import type {
|
|
|
10
14
|
} from '../../../types'
|
|
11
15
|
import type { SystemPrompt } from '../../../system-prompts'
|
|
12
16
|
import type { ToolApprovalResolution } from '../../../interrupts'
|
|
17
|
+
import type {
|
|
18
|
+
GenericInterruptRequest,
|
|
19
|
+
InterruptDefinition,
|
|
20
|
+
} from '../../../interrupt-definition'
|
|
13
21
|
import type {
|
|
14
22
|
Capability,
|
|
15
23
|
CapabilityHandle,
|
|
@@ -68,6 +76,7 @@ export interface ChatSandboxHooks<TContext = unknown> {
|
|
|
68
76
|
* Phase of the chat middleware lifecycle.
|
|
69
77
|
* - 'init': Initial config transform before the chat engine starts
|
|
70
78
|
* - 'beforeModel': Before each adapter chatStream call (per agent iteration)
|
|
79
|
+
* - 'afterModel': After each adapter chatStream call (per agent iteration)
|
|
71
80
|
* - 'modelStream': During model streaming
|
|
72
81
|
* - 'beforeTools': Before tool execution phase
|
|
73
82
|
* - 'afterTools': After tool execution phase
|
|
@@ -77,11 +86,97 @@ export interface ChatSandboxHooks<TContext = unknown> {
|
|
|
77
86
|
export type ChatMiddlewarePhase =
|
|
78
87
|
| 'init'
|
|
79
88
|
| 'beforeModel'
|
|
89
|
+
| 'afterModel'
|
|
80
90
|
| 'modelStream'
|
|
81
91
|
| 'beforeTools'
|
|
82
92
|
| 'afterTools'
|
|
83
93
|
| 'structuredOutput'
|
|
84
94
|
|
|
95
|
+
export const INTERRUPT_BOUNDARY_PHASES = [
|
|
96
|
+
'beforeModel',
|
|
97
|
+
'afterModel',
|
|
98
|
+
'beforeTools',
|
|
99
|
+
'afterTools',
|
|
100
|
+
] as const
|
|
101
|
+
|
|
102
|
+
export type InterruptBoundaryPhase = (typeof INTERRUPT_BOUNDARY_PHASES)[number]
|
|
103
|
+
|
|
104
|
+
export const INTERRUPT_TOOL_RESUMES = ['continue', 'cancel', 'stop'] as const
|
|
105
|
+
|
|
106
|
+
export type InterruptToolResume = (typeof INTERRUPT_TOOL_RESUMES)[number]
|
|
107
|
+
|
|
108
|
+
type AnyInterruptDefinition = InterruptDefinition<any, any, any, any>
|
|
109
|
+
|
|
110
|
+
type InterruptResponse<TDefinition> =
|
|
111
|
+
TDefinition extends InterruptDefinition<any, any, infer TResponseSchema, any>
|
|
112
|
+
? TResponseSchema extends StandardSchemaV1<any, infer TResponse>
|
|
113
|
+
? TResponse
|
|
114
|
+
: TResponseSchema extends StandardJSONSchemaV1<any, infer TResponse>
|
|
115
|
+
? TResponse
|
|
116
|
+
: unknown
|
|
117
|
+
: unknown
|
|
118
|
+
|
|
119
|
+
export type GenericInterruptResolution<
|
|
120
|
+
TDefinition extends AnyInterruptDefinition,
|
|
121
|
+
> = TDefinition extends AnyInterruptDefinition
|
|
122
|
+
?
|
|
123
|
+
| {
|
|
124
|
+
readonly request: GenericInterruptRequest<TDefinition>
|
|
125
|
+
readonly status: 'resolved'
|
|
126
|
+
readonly response: InterruptResponse<TDefinition>
|
|
127
|
+
}
|
|
128
|
+
| {
|
|
129
|
+
readonly request: GenericInterruptRequest<TDefinition>
|
|
130
|
+
readonly status: 'cancelled'
|
|
131
|
+
readonly response?: never
|
|
132
|
+
}
|
|
133
|
+
: never
|
|
134
|
+
|
|
135
|
+
export interface InterruptResolutionCollection<
|
|
136
|
+
TDefinitions extends AnyInterruptDefinition = AnyInterruptDefinition,
|
|
137
|
+
> {
|
|
138
|
+
for: <
|
|
139
|
+
TDefinition extends ([TDefinitions] extends [never]
|
|
140
|
+
? AnyInterruptDefinition
|
|
141
|
+
: TDefinitions),
|
|
142
|
+
>(
|
|
143
|
+
definition: TDefinition,
|
|
144
|
+
) => ReadonlyArray<GenericInterruptResolution<TDefinition>>
|
|
145
|
+
all: {
|
|
146
|
+
(): ReadonlyArray<GenericInterruptResolution<TDefinitions>>
|
|
147
|
+
<const TSelected extends ReadonlyArray<TDefinitions>>(
|
|
148
|
+
...definitions: TSelected
|
|
149
|
+
): ReadonlyArray<GenericInterruptResolution<TSelected[number]>>
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
type BivariantInterruptResolutionHook<
|
|
154
|
+
TContext,
|
|
155
|
+
TDefinitions extends AnyInterruptDefinition,
|
|
156
|
+
> = InterruptResolutionHookSignature<TContext, TDefinitions>['call']
|
|
157
|
+
|
|
158
|
+
declare abstract class InterruptResolutionHookSignature<
|
|
159
|
+
TContext,
|
|
160
|
+
TDefinitions extends AnyInterruptDefinition,
|
|
161
|
+
> {
|
|
162
|
+
abstract call(
|
|
163
|
+
ctx: ChatMiddlewareContext<TContext>,
|
|
164
|
+
resolutions: InterruptResolutionCollection<TDefinitions>,
|
|
165
|
+
): InterruptResolutionResult | Promise<InterruptResolutionResult>
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export type InterruptBoundaryResult<
|
|
169
|
+
TDefinitions extends AnyInterruptDefinition = AnyInterruptDefinition,
|
|
170
|
+
> =
|
|
171
|
+
| undefined
|
|
172
|
+
| {
|
|
173
|
+
readonly interrupts: ReadonlyArray<GenericInterruptRequest<TDefinitions>>
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export type InterruptResolutionResult = void | {
|
|
177
|
+
readonly toolResume: InterruptToolResume
|
|
178
|
+
}
|
|
179
|
+
|
|
85
180
|
/**
|
|
86
181
|
* Stable context object passed to all middleware hooks.
|
|
87
182
|
* Created once per chat() invocation and shared across all hooks.
|
|
@@ -230,6 +325,13 @@ export interface ChatResumeToolState {
|
|
|
230
325
|
genericInterrupts?:
|
|
231
326
|
| ReadonlyMap<string, ChatResumeGenericResolution>
|
|
232
327
|
| undefined
|
|
328
|
+
/** Durable generic requests reconstructed by server middleware. */
|
|
329
|
+
genericInterruptRequests?:
|
|
330
|
+
| ReadonlyMap<
|
|
331
|
+
string,
|
|
332
|
+
GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
|
|
333
|
+
>
|
|
334
|
+
| undefined
|
|
233
335
|
deniedToolResults?: ReadonlyMap<string, unknown> | undefined
|
|
234
336
|
cancelledToolCallIds?: ReadonlySet<string> | undefined
|
|
235
337
|
}
|
|
@@ -460,10 +562,32 @@ export interface ErrorInfo {
|
|
|
460
562
|
* }
|
|
461
563
|
* ```
|
|
462
564
|
*/
|
|
463
|
-
export interface ChatMiddleware<
|
|
565
|
+
export interface ChatMiddleware<
|
|
566
|
+
TContext = unknown,
|
|
567
|
+
TInterruptDefinitions extends AnyInterruptDefinition = never,
|
|
568
|
+
> {
|
|
464
569
|
/** Optional name for debugging and identification */
|
|
465
570
|
name?: string
|
|
466
571
|
|
|
572
|
+
/**
|
|
573
|
+
* Called at a lifecycle boundary. Return interrupt requests to pause the run.
|
|
574
|
+
* Requests from every middleware in the same boundary form one batch.
|
|
575
|
+
*/
|
|
576
|
+
onInterruptBoundary?: (
|
|
577
|
+
ctx: ChatMiddlewareContext<TContext> & { phase: InterruptBoundaryPhase },
|
|
578
|
+
) =>
|
|
579
|
+
| InterruptBoundaryResult<TInterruptDefinitions>
|
|
580
|
+
| Promise<InterruptBoundaryResult<TInterruptDefinitions>>
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Called on a continuation run after the client answers registered interrupts.
|
|
584
|
+
* Return `toolResume` to decide whether pending tools continue, cancel, or stop.
|
|
585
|
+
*/
|
|
586
|
+
onInterruptResolution?: BivariantInterruptResolutionHook<
|
|
587
|
+
TContext,
|
|
588
|
+
TInterruptDefinitions
|
|
589
|
+
>
|
|
590
|
+
|
|
467
591
|
/**
|
|
468
592
|
* Capabilities this middleware requires. `chat()` validates that some
|
|
469
593
|
* middleware (or the adapter) provides each one; unsatisfied requirements are
|
|
@@ -653,4 +777,5 @@ export interface ChatMiddleware<TContext = unknown> {
|
|
|
653
777
|
}
|
|
654
778
|
|
|
655
779
|
/** A `ChatMiddleware` with a permissive context — for use as a constraint. */
|
|
656
|
-
|
|
780
|
+
/** A permissive middleware constraint that retains the definition parameter. */
|
|
781
|
+
export type AnyChatMiddleware = ChatMiddleware<any, any>
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
updateToolResultPart,
|
|
39
39
|
} from './message-updaters'
|
|
40
40
|
import { ImmediateStrategy } from './strategies'
|
|
41
|
+
import { INTERRUPT_BINDING_METADATA_KEY } from '../../../interrupt-resume'
|
|
41
42
|
import type {
|
|
42
43
|
ChunkRecording,
|
|
43
44
|
ChunkStrategy,
|
|
@@ -138,6 +139,22 @@ export interface StreamProcessorOptions {
|
|
|
138
139
|
|
|
139
140
|
const STRUCTURED_OUTPUT_UPDATE_BATCH_SIZE = 12
|
|
140
141
|
|
|
142
|
+
function interruptBatchHasGeneric(interrupts: Array<Interrupt>): boolean {
|
|
143
|
+
return interrupts.some((interrupt) => {
|
|
144
|
+
const metadata = interrupt.metadata
|
|
145
|
+
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
|
|
146
|
+
return false
|
|
147
|
+
}
|
|
148
|
+
const binding = metadata[INTERRUPT_BINDING_METADATA_KEY]
|
|
149
|
+
return (
|
|
150
|
+
binding !== null &&
|
|
151
|
+
typeof binding === 'object' &&
|
|
152
|
+
!Array.isArray(binding) &&
|
|
153
|
+
binding.kind === 'generic'
|
|
154
|
+
)
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
141
158
|
/**
|
|
142
159
|
* StreamProcessor - State machine for processing AI response streams
|
|
143
160
|
*
|
|
@@ -1548,6 +1565,7 @@ export class StreamProcessor {
|
|
|
1548
1565
|
}
|
|
1549
1566
|
|
|
1550
1567
|
private handleInterrupts(interrupts: Array<Interrupt>): void {
|
|
1568
|
+
const hasGeneric = interruptBatchHasGeneric(interrupts)
|
|
1551
1569
|
for (const interrupt of interrupts) {
|
|
1552
1570
|
const metadata =
|
|
1553
1571
|
interrupt.metadata && typeof interrupt.metadata === 'object'
|
|
@@ -1598,6 +1616,9 @@ export class StreamProcessor {
|
|
|
1598
1616
|
}
|
|
1599
1617
|
|
|
1600
1618
|
if (kind === 'client_tool' || interrupt.reason === 'client_tool_input') {
|
|
1619
|
+
// Generic interrupts in the same batch decide `toolResume`. Do not
|
|
1620
|
+
// run client tools until that policy is `continue`.
|
|
1621
|
+
if (hasGeneric) continue
|
|
1601
1622
|
this.events.onToolCall?.({
|
|
1602
1623
|
toolCallId,
|
|
1603
1624
|
toolName,
|
package/src/adapter-internals.ts
CHANGED
|
@@ -25,6 +25,26 @@ export {
|
|
|
25
25
|
PendingTurnCapability,
|
|
26
26
|
providePendingTurn,
|
|
27
27
|
} from './activities/chat/middleware/pending-turn'
|
|
28
|
+
export {
|
|
29
|
+
getGenericInterruptDefinitionRegistry,
|
|
30
|
+
GenericInterruptDefinitionRegistryCapability,
|
|
31
|
+
provideGenericInterruptDefinitionRegistry,
|
|
32
|
+
} from './activities/chat/middleware/generic-interrupts'
|
|
33
|
+
export type { GenericInterruptDefinitionRegistry } from './activities/chat/middleware/generic-interrupts'
|
|
34
|
+
export {
|
|
35
|
+
createInterruptBinding,
|
|
36
|
+
getInterruptRequestInput,
|
|
37
|
+
rehydrateInterruptRequest,
|
|
38
|
+
} from './interrupt-definition'
|
|
39
|
+
export type {
|
|
40
|
+
GenericInterruptRequest,
|
|
41
|
+
InterruptDefinition,
|
|
42
|
+
} from './interrupt-definition'
|
|
43
|
+
export {
|
|
44
|
+
readInterruptBinding,
|
|
45
|
+
validateInterruptResumeBatch,
|
|
46
|
+
} from './interrupt-resume'
|
|
47
|
+
export type { PendingInterruptResumeRecord } from './interrupt-resume'
|
|
28
48
|
export {
|
|
29
49
|
assertUniqueToolNames,
|
|
30
50
|
DuplicateToolNameError,
|
package/src/client.ts
CHANGED
|
@@ -343,6 +343,26 @@ export {
|
|
|
343
343
|
INTERRUPT_BINDING_VERSION,
|
|
344
344
|
canonicalizeInterruptResolutions,
|
|
345
345
|
} from './interrupts'
|
|
346
|
+
export {
|
|
347
|
+
defineInterrupt,
|
|
348
|
+
hashInterruptDefinitionSchema,
|
|
349
|
+
INTERRUPT_PAYLOAD_METADATA_KEY,
|
|
350
|
+
} from './interrupt-definition'
|
|
351
|
+
export {
|
|
352
|
+
INTERRUPT_CONTINUATION_METADATA_KEY,
|
|
353
|
+
INTERRUPT_CONTINUATION_VERSION,
|
|
354
|
+
genericInterruptContinuationFromDescriptor,
|
|
355
|
+
readGenericInterruptContinuation,
|
|
356
|
+
wrapGenericInterruptContinuation,
|
|
357
|
+
} from './generic-interrupt-continuation'
|
|
358
|
+
export type {
|
|
359
|
+
GenericInterruptContinuation,
|
|
360
|
+
GenericInterruptContinuationReadResult,
|
|
361
|
+
} from './generic-interrupt-continuation'
|
|
362
|
+
export type {
|
|
363
|
+
GenericInterruptRequest,
|
|
364
|
+
InterruptDefinition,
|
|
365
|
+
} from './interrupt-definition'
|
|
346
366
|
export type {
|
|
347
367
|
BatchInterruptError,
|
|
348
368
|
BatchInterruptErrorCode,
|