@tanstack/ai-client 0.24.0 → 0.25.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/chat-client.d.ts +14 -11
- package/dist/esm/chat-client.js +25 -10
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/connection-adapters.js +3 -1
- package/dist/esm/connection-adapters.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/interrupt-manager.d.ts +15 -10
- package/dist/esm/interrupt-manager.js +176 -83
- package/dist/esm/interrupt-manager.js.map +1 -1
- package/dist/esm/types.d.ts +40 -13
- package/dist/esm/types.js.map +1 -1
- package/package.json +2 -2
- package/src/chat-client.ts +115 -42
- package/src/connection-adapters.ts +14 -2
- package/src/index.ts +3 -0
- package/src/interrupt-manager.ts +386 -145
- package/src/types.ts +99 -11
package/src/interrupt-manager.ts
CHANGED
|
@@ -5,15 +5,20 @@ import {
|
|
|
5
5
|
canonicalizeInterruptResolutions,
|
|
6
6
|
cloneAndDeepFreezeJson,
|
|
7
7
|
digestInterruptJson,
|
|
8
|
+
genericInterruptContinuationFromDescriptor,
|
|
9
|
+
hashInterruptDefinitionSchema,
|
|
8
10
|
hashSchemaInput,
|
|
9
11
|
isStandardSchema,
|
|
10
12
|
normalizeApprovalSchema,
|
|
13
|
+
readInterruptBinding,
|
|
14
|
+
wrapGenericInterruptContinuation,
|
|
11
15
|
} from '@tanstack/ai/client'
|
|
12
16
|
import type {
|
|
13
17
|
AnyClientTool,
|
|
14
18
|
BatchInterruptError,
|
|
15
19
|
Interrupt,
|
|
16
20
|
InterruptBinding,
|
|
21
|
+
InterruptDefinition,
|
|
17
22
|
InterruptSubmissionError,
|
|
18
23
|
ItemInterruptError,
|
|
19
24
|
RunAgentResumeItem,
|
|
@@ -25,6 +30,7 @@ import type {
|
|
|
25
30
|
ChatInterruptState,
|
|
26
31
|
GenericAGUIInterrupt,
|
|
27
32
|
InterruptItemStatus,
|
|
33
|
+
ResolvableChatInterrupt,
|
|
28
34
|
UnboundInterrupt,
|
|
29
35
|
} from './types'
|
|
30
36
|
|
|
@@ -44,12 +50,17 @@ export interface InterruptManagerSubmission {
|
|
|
44
50
|
fingerprint: string
|
|
45
51
|
}
|
|
46
52
|
|
|
53
|
+
export type InterruptManagerChangeSource = 'hydrate' | 'live'
|
|
54
|
+
|
|
47
55
|
export interface InterruptManagerOptions<
|
|
48
56
|
TTools extends ReadonlyArray<AnyClientTool>,
|
|
57
|
+
TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> =
|
|
58
|
+
readonly [],
|
|
49
59
|
> {
|
|
50
60
|
tools?: TTools
|
|
61
|
+
interrupts?: TInterrupts
|
|
51
62
|
submit: (submission: InterruptManagerSubmission) => Promise<void>
|
|
52
|
-
onChange?: () => void
|
|
63
|
+
onChange?: (source: InterruptManagerChangeSource) => void
|
|
53
64
|
}
|
|
54
65
|
|
|
55
66
|
type UnknownObject = { [key: string]: unknown }
|
|
@@ -71,6 +82,11 @@ interface RuntimeInterrupt {
|
|
|
71
82
|
error?: ItemInterruptError
|
|
72
83
|
resolution?: RunAgentResumeItem
|
|
73
84
|
tool?: AnyClientTool
|
|
85
|
+
definition?: InterruptDefinition<any, any, any, any>
|
|
86
|
+
/** Validated display payload for a registered first-party generic item. */
|
|
87
|
+
payload?: unknown
|
|
88
|
+
/** This binding is valid and can participate in this chat resume batch. */
|
|
89
|
+
resumable: boolean
|
|
74
90
|
validationGeneration: number
|
|
75
91
|
}
|
|
76
92
|
|
|
@@ -93,6 +109,33 @@ interface RuntimeInterruptCheckpoint {
|
|
|
93
109
|
validationGeneration: number
|
|
94
110
|
}
|
|
95
111
|
|
|
112
|
+
function isClientOwnedInterrupt(item: RuntimeInterrupt): boolean {
|
|
113
|
+
return item.resumable
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function resolutionWithContinuation(
|
|
117
|
+
item: RuntimeInterrupt,
|
|
118
|
+
resolution: RunAgentResumeItem,
|
|
119
|
+
): RunAgentResumeItem {
|
|
120
|
+
const continuation = genericInterruptContinuationFromDescriptor(
|
|
121
|
+
item.descriptor,
|
|
122
|
+
)
|
|
123
|
+
if (!continuation) return resolution
|
|
124
|
+
return {
|
|
125
|
+
...resolution,
|
|
126
|
+
metadata: wrapGenericInterruptContinuation(continuation),
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isRootResolvableInterrupt<
|
|
131
|
+
TTools extends ReadonlyArray<AnyClientTool>,
|
|
132
|
+
TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>,
|
|
133
|
+
>(
|
|
134
|
+
interrupt: ChatInterrupt<TTools, TInterrupts>,
|
|
135
|
+
): interrupt is ResolvableChatInterrupt<TTools, TInterrupts> {
|
|
136
|
+
return 'cancel' in interrupt && 'clearResolution' in interrupt
|
|
137
|
+
}
|
|
138
|
+
|
|
96
139
|
const itemErrorCodes = new Set<ItemInterruptError['code']>([
|
|
97
140
|
'invalid-payload',
|
|
98
141
|
'invalid-edited-args',
|
|
@@ -157,91 +200,47 @@ function isLegacyInterruptMetadata(interrupt: Interrupt): boolean {
|
|
|
157
200
|
)
|
|
158
201
|
}
|
|
159
202
|
|
|
160
|
-
function
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
// the current version so pre-versioning bindings still resume.
|
|
165
|
-
(value['v'] === undefined || value['v'] === INTERRUPT_BINDING_VERSION) &&
|
|
166
|
-
typeof value['kind'] === 'string' &&
|
|
167
|
-
typeof value['interruptId'] === 'string' &&
|
|
168
|
-
typeof value['interruptedRunId'] === 'string' &&
|
|
169
|
-
typeof value['generation'] === 'number' &&
|
|
170
|
-
Number.isInteger(value['generation']) &&
|
|
171
|
-
value['generation'] >= 0 &&
|
|
172
|
-
typeof value['responseSchemaHash'] === 'string' &&
|
|
173
|
-
(value['expiresAt'] === undefined ||
|
|
174
|
-
(typeof value['expiresAt'] === 'string' &&
|
|
175
|
-
Number.isFinite(Date.parse(value['expiresAt']))))
|
|
176
|
-
)
|
|
203
|
+
function getDescriptorBinding(
|
|
204
|
+
interrupt: Interrupt,
|
|
205
|
+
): InterruptBinding | undefined {
|
|
206
|
+
return readInterruptBinding(interrupt)
|
|
177
207
|
}
|
|
178
208
|
|
|
179
|
-
function
|
|
180
|
-
if (!isUnknownObject(
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
if (value['kind'] === 'generic') {
|
|
184
|
-
return {
|
|
185
|
-
v: INTERRUPT_BINDING_VERSION,
|
|
186
|
-
kind: 'generic',
|
|
187
|
-
interruptId: String(value['interruptId']),
|
|
188
|
-
interruptedRunId: String(value['interruptedRunId']),
|
|
189
|
-
generation: Number(value['generation']),
|
|
190
|
-
responseSchemaHash: String(value['responseSchemaHash']),
|
|
191
|
-
...(expiresAt !== undefined ? { expiresAt } : {}),
|
|
192
|
-
}
|
|
193
|
-
}
|
|
209
|
+
function hasReservedFirstPartyBindingMarker(interrupt: Interrupt): boolean {
|
|
210
|
+
if (!isUnknownObject(interrupt.metadata)) return false
|
|
211
|
+
const binding = interrupt.metadata[INTERRUPT_BINDING_METADATA_KEY]
|
|
212
|
+
if (!isUnknownObject(binding)) return false
|
|
194
213
|
if (
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
typeof value['toolCallId'] === 'string' &&
|
|
198
|
-
typeof value['outputSchemaHash'] === 'string'
|
|
214
|
+
binding['v'] !== undefined &&
|
|
215
|
+
binding['v'] !== INTERRUPT_BINDING_VERSION
|
|
199
216
|
) {
|
|
200
|
-
return
|
|
201
|
-
v: INTERRUPT_BINDING_VERSION,
|
|
202
|
-
kind: 'client-tool-execution',
|
|
203
|
-
interruptId: String(value['interruptId']),
|
|
204
|
-
interruptedRunId: String(value['interruptedRunId']),
|
|
205
|
-
generation: Number(value['generation']),
|
|
206
|
-
toolName: value['toolName'],
|
|
207
|
-
toolCallId: value['toolCallId'],
|
|
208
|
-
outputSchemaHash: value['outputSchemaHash'],
|
|
209
|
-
responseSchemaHash: String(value['responseSchemaHash']),
|
|
210
|
-
...(expiresAt !== undefined ? { expiresAt } : {}),
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
if (
|
|
214
|
-
value['kind'] === 'tool-approval' &&
|
|
215
|
-
typeof value['toolName'] === 'string' &&
|
|
216
|
-
typeof value['toolCallId'] === 'string' &&
|
|
217
|
-
typeof value['inputSchemaHash'] === 'string' &&
|
|
218
|
-
typeof value['approvalSchemaHash'] === 'string' &&
|
|
219
|
-
'originalArgs' in value
|
|
220
|
-
) {
|
|
221
|
-
return {
|
|
222
|
-
v: INTERRUPT_BINDING_VERSION,
|
|
223
|
-
kind: 'tool-approval',
|
|
224
|
-
interruptId: String(value['interruptId']),
|
|
225
|
-
interruptedRunId: String(value['interruptedRunId']),
|
|
226
|
-
generation: Number(value['generation']),
|
|
227
|
-
toolName: value['toolName'],
|
|
228
|
-
toolCallId: value['toolCallId'],
|
|
229
|
-
originalArgs: value['originalArgs'],
|
|
230
|
-
inputSchemaHash: value['inputSchemaHash'],
|
|
231
|
-
approvalSchemaHash: value['approvalSchemaHash'],
|
|
232
|
-
responseSchemaHash: String(value['responseSchemaHash']),
|
|
233
|
-
...(expiresAt !== undefined ? { expiresAt } : {}),
|
|
234
|
-
}
|
|
217
|
+
return false
|
|
235
218
|
}
|
|
236
|
-
return
|
|
219
|
+
return (
|
|
220
|
+
binding['kind'] === 'generic' ||
|
|
221
|
+
binding['kind'] === 'tool-approval' ||
|
|
222
|
+
binding['kind'] === 'client-tool-execution' ||
|
|
223
|
+
'definitionId' in binding ||
|
|
224
|
+
'key' in binding ||
|
|
225
|
+
'batchIndex' in binding ||
|
|
226
|
+
'payloadSchemaHash' in binding
|
|
227
|
+
)
|
|
237
228
|
}
|
|
238
229
|
|
|
239
|
-
function
|
|
240
|
-
interrupt
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
230
|
+
function hasFirstPartyGenericMarker(interrupt: Interrupt): boolean {
|
|
231
|
+
if (!isUnknownObject(interrupt.metadata)) return false
|
|
232
|
+
const binding = interrupt.metadata[INTERRUPT_BINDING_METADATA_KEY]
|
|
233
|
+
if (!isUnknownObject(binding) || binding['kind'] !== 'generic') return false
|
|
234
|
+
return (
|
|
235
|
+
'definitionId' in binding ||
|
|
236
|
+
'key' in binding ||
|
|
237
|
+
'batchIndex' in binding ||
|
|
238
|
+
'payloadSchemaHash' in binding
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function getInterruptPayload(interrupt: Interrupt): unknown {
|
|
243
|
+
return interrupt.metadata?.['tanstack:interruptPayload']
|
|
245
244
|
}
|
|
246
245
|
|
|
247
246
|
/**
|
|
@@ -264,6 +263,17 @@ function responseSchemaHash(interrupt: Interrupt): string | undefined {
|
|
|
264
263
|
}
|
|
265
264
|
}
|
|
266
265
|
|
|
266
|
+
function definitionSchemaHash(
|
|
267
|
+
schema: InterruptDefinition<any, any, any, any>['responseSchema'] | undefined,
|
|
268
|
+
): string | undefined {
|
|
269
|
+
if (schema === undefined) return undefined
|
|
270
|
+
try {
|
|
271
|
+
return hashInterruptDefinitionSchema(schema)
|
|
272
|
+
} catch {
|
|
273
|
+
return undefined
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
267
277
|
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
|
|
268
278
|
return (
|
|
269
279
|
value !== null &&
|
|
@@ -431,16 +441,15 @@ function genericBinding(
|
|
|
431
441
|
hydration: InterruptManagerHydration,
|
|
432
442
|
candidate: InterruptBinding | undefined,
|
|
433
443
|
): InterruptBinding {
|
|
444
|
+
const schemaHash =
|
|
445
|
+
responseSchemaHash(interrupt) ?? candidate?.responseSchemaHash
|
|
434
446
|
return cloneAndDeepFreezeJson({
|
|
435
447
|
v: INTERRUPT_BINDING_VERSION,
|
|
436
448
|
kind: 'generic',
|
|
437
449
|
interruptId: interrupt.id,
|
|
438
450
|
interruptedRunId: hydration.interruptedRunId,
|
|
439
451
|
generation: hydration.generation,
|
|
440
|
-
responseSchemaHash:
|
|
441
|
-
responseSchemaHash(interrupt) ??
|
|
442
|
-
candidate?.responseSchemaHash ??
|
|
443
|
-
'invalid',
|
|
452
|
+
...(schemaHash !== undefined ? { responseSchemaHash: schemaHash } : {}),
|
|
444
453
|
...(interrupt.expiresAt !== undefined
|
|
445
454
|
? { expiresAt: interrupt.expiresAt }
|
|
446
455
|
: {}),
|
|
@@ -450,9 +459,7 @@ function genericBinding(
|
|
|
450
459
|
function baseSnapshot(
|
|
451
460
|
item: RuntimeInterrupt,
|
|
452
461
|
hydration: InterruptManagerHydration,
|
|
453
|
-
|
|
454
|
-
clearResolution: () => void,
|
|
455
|
-
): BoundInterruptBase {
|
|
462
|
+
): Omit<BoundInterruptBase, 'cancel' | 'clearResolution'> {
|
|
456
463
|
const descriptor = cloneAndDeepFreezeJson(item.descriptor)
|
|
457
464
|
const errors: ReadonlyArray<ItemInterruptError> =
|
|
458
465
|
item.error === undefined
|
|
@@ -482,21 +489,22 @@ function baseSnapshot(
|
|
|
482
489
|
errors,
|
|
483
490
|
...(error !== undefined ? { error } : {}),
|
|
484
491
|
canResolve: item.canResolve,
|
|
485
|
-
cancel,
|
|
486
|
-
clearResolution,
|
|
487
492
|
}
|
|
488
493
|
}
|
|
489
494
|
|
|
490
495
|
export class InterruptManager<
|
|
491
496
|
TTools extends ReadonlyArray<AnyClientTool> = ReadonlyArray<AnyClientTool>,
|
|
497
|
+
TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> =
|
|
498
|
+
readonly [],
|
|
492
499
|
> {
|
|
493
500
|
private hydration: InterruptManagerHydration | undefined
|
|
494
501
|
private items: Array<RuntimeInterrupt> = []
|
|
495
|
-
private snapshot: ReadonlyArray<ChatInterrupt<TTools>> =
|
|
502
|
+
private snapshot: ReadonlyArray<ChatInterrupt<TTools, TInterrupts>> =
|
|
503
|
+
Object.freeze([])
|
|
496
504
|
private rootErrors: ReadonlyArray<BatchInterruptError> = Object.freeze([])
|
|
497
505
|
private submissionRootErrors: ReadonlyArray<BatchInterruptError> =
|
|
498
506
|
Object.freeze([])
|
|
499
|
-
private state: ChatInterruptState<TTools> = Object.freeze({
|
|
507
|
+
private state: ChatInterruptState<TTools, TInterrupts> = Object.freeze({
|
|
500
508
|
interrupts: this.snapshot,
|
|
501
509
|
pendingInterrupts: this.snapshot,
|
|
502
510
|
interruptErrors: this.rootErrors,
|
|
@@ -506,37 +514,72 @@ export class InterruptManager<
|
|
|
506
514
|
private retrySubmission: InterruptManagerSubmission | undefined
|
|
507
515
|
private resuming = false
|
|
508
516
|
private tools: TTools | undefined
|
|
517
|
+
private readonly interruptDefinitions: ReadonlyMap<
|
|
518
|
+
string,
|
|
519
|
+
InterruptDefinition<any, any, any, any>
|
|
520
|
+
>
|
|
509
521
|
|
|
510
|
-
constructor(
|
|
522
|
+
constructor(
|
|
523
|
+
private readonly options: InterruptManagerOptions<TTools, TInterrupts>,
|
|
524
|
+
) {
|
|
511
525
|
this.tools = options.tools
|
|
526
|
+
const definitions = new Map<
|
|
527
|
+
string,
|
|
528
|
+
InterruptDefinition<any, any, any, any>
|
|
529
|
+
>()
|
|
530
|
+
for (const definition of options.interrupts ?? []) {
|
|
531
|
+
if (definitions.has(definition.id)) {
|
|
532
|
+
throw new Error(`Duplicate interrupt definition id: ${definition.id}`)
|
|
533
|
+
}
|
|
534
|
+
definitions.set(definition.id, definition)
|
|
535
|
+
}
|
|
536
|
+
this.interruptDefinitions = definitions
|
|
512
537
|
}
|
|
513
538
|
|
|
514
539
|
updateTools(tools: TTools): void {
|
|
515
540
|
this.tools = tools
|
|
516
541
|
}
|
|
517
542
|
|
|
518
|
-
hydrate(
|
|
543
|
+
hydrate(
|
|
544
|
+
hydration: InterruptManagerHydration,
|
|
545
|
+
source: InterruptManagerChangeSource = 'live',
|
|
546
|
+
): void {
|
|
519
547
|
this.hydration = {
|
|
520
548
|
threadId: hydration.threadId,
|
|
521
549
|
interruptedRunId: hydration.interruptedRunId,
|
|
522
550
|
generation: hydration.generation,
|
|
523
551
|
interrupts: cloneAndDeepFreezeJson(hydration.interrupts),
|
|
524
552
|
}
|
|
553
|
+
const firstPartyIndexes = new Map<number, number>()
|
|
554
|
+
for (const interrupt of hydration.interrupts) {
|
|
555
|
+
const binding = getDescriptorBinding(interrupt)
|
|
556
|
+
if (
|
|
557
|
+
binding?.kind === 'generic' &&
|
|
558
|
+
binding.definitionId !== undefined &&
|
|
559
|
+
binding.key !== undefined &&
|
|
560
|
+
binding.batchIndex !== undefined
|
|
561
|
+
) {
|
|
562
|
+
firstPartyIndexes.set(
|
|
563
|
+
binding.batchIndex,
|
|
564
|
+
(firstPartyIndexes.get(binding.batchIndex) ?? 0) + 1,
|
|
565
|
+
)
|
|
566
|
+
}
|
|
567
|
+
}
|
|
525
568
|
this.items = hydration.interrupts.map((interrupt) =>
|
|
526
|
-
this.hydrateInterrupt(interrupt, hydration),
|
|
569
|
+
this.hydrateInterrupt(interrupt, hydration, firstPartyIndexes),
|
|
527
570
|
)
|
|
528
571
|
this.rootErrors = Object.freeze([])
|
|
529
572
|
this.submissionRootErrors = Object.freeze([])
|
|
530
573
|
this.retrySubmission = undefined
|
|
531
574
|
this.resuming = false
|
|
532
|
-
this.publish()
|
|
575
|
+
this.publish(source)
|
|
533
576
|
}
|
|
534
577
|
|
|
535
|
-
getInterrupts(): BoundInterrupts<TTools> {
|
|
578
|
+
getInterrupts(): BoundInterrupts<TTools, TInterrupts> {
|
|
536
579
|
return this.snapshot
|
|
537
580
|
}
|
|
538
581
|
|
|
539
|
-
getState(): ChatInterruptState<TTools> {
|
|
582
|
+
getState(): ChatInterruptState<TTools, TInterrupts> {
|
|
540
583
|
return this.state
|
|
541
584
|
}
|
|
542
585
|
|
|
@@ -544,7 +587,16 @@ export class InterruptManager<
|
|
|
544
587
|
return this.hydration?.interrupts ?? Object.freeze([])
|
|
545
588
|
}
|
|
546
589
|
|
|
547
|
-
|
|
590
|
+
hasValidatedFirstPartyGenericBatch(): boolean {
|
|
591
|
+
return this.items.some(
|
|
592
|
+
(item) => item.kind === 'generic' && item.definition !== undefined,
|
|
593
|
+
)
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
reset(options?: {
|
|
597
|
+
preserveRootErrors?: boolean
|
|
598
|
+
source?: InterruptManagerChangeSource
|
|
599
|
+
}): void {
|
|
548
600
|
this.hydration = undefined
|
|
549
601
|
this.items = []
|
|
550
602
|
this.snapshot = Object.freeze([])
|
|
@@ -560,7 +612,7 @@ export class InterruptManager<
|
|
|
560
612
|
interruptErrors: this.rootErrors,
|
|
561
613
|
resuming: false,
|
|
562
614
|
})
|
|
563
|
-
this.options.onChange?.()
|
|
615
|
+
this.options.onChange?.(options?.source ?? 'live')
|
|
564
616
|
}
|
|
565
617
|
|
|
566
618
|
getInterruptErrors(): ReadonlyArray<BatchInterruptError> {
|
|
@@ -572,9 +624,15 @@ export class InterruptManager<
|
|
|
572
624
|
}
|
|
573
625
|
|
|
574
626
|
resolve(approved: boolean): void
|
|
575
|
-
resolve(resolver: (interrupt: ChatInterrupt<TTools>) => undefined): void
|
|
576
627
|
resolve(
|
|
577
|
-
|
|
628
|
+
resolver: (
|
|
629
|
+
interrupt: ResolvableChatInterrupt<TTools, TInterrupts>,
|
|
630
|
+
) => undefined,
|
|
631
|
+
): void
|
|
632
|
+
resolve(
|
|
633
|
+
resolution:
|
|
634
|
+
| boolean
|
|
635
|
+
| ((interrupt: ResolvableChatInterrupt<TTools, TInterrupts>) => unknown),
|
|
578
636
|
): void {
|
|
579
637
|
this.assertRootMutable()
|
|
580
638
|
if (typeof resolution === 'boolean') {
|
|
@@ -588,11 +646,14 @@ export class InterruptManager<
|
|
|
588
646
|
this.assertRootMutable()
|
|
589
647
|
this.invalidateRetry()
|
|
590
648
|
for (const item of this.items) {
|
|
649
|
+
if (!isClientOwnedInterrupt(item)) continue
|
|
591
650
|
item.validationGeneration++
|
|
592
|
-
item.resolution = Object.freeze(
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
651
|
+
item.resolution = Object.freeze(
|
|
652
|
+
resolutionWithContinuation(item, {
|
|
653
|
+
interruptId: item.descriptor.id,
|
|
654
|
+
status: 'cancelled',
|
|
655
|
+
}),
|
|
656
|
+
)
|
|
596
657
|
item.status = 'staged'
|
|
597
658
|
item.error = undefined
|
|
598
659
|
}
|
|
@@ -648,9 +709,12 @@ export class InterruptManager<
|
|
|
648
709
|
private hydrateInterrupt(
|
|
649
710
|
descriptor: Interrupt,
|
|
650
711
|
hydration: InterruptManagerHydration,
|
|
712
|
+
firstPartyIndexes: ReadonlyMap<number, number>,
|
|
651
713
|
): RuntimeInterrupt {
|
|
652
714
|
const interrupt = cloneAndDeepFreezeJson(descriptor)
|
|
653
715
|
const candidate = getDescriptorBinding(interrupt)
|
|
716
|
+
const legacyResumable =
|
|
717
|
+
candidate === undefined && isLegacyInterruptMetadata(interrupt)
|
|
654
718
|
|
|
655
719
|
// No binding we understand, and nothing else identifying the descriptor as
|
|
656
720
|
// ours, means this interrupt was not produced by this package's resume
|
|
@@ -668,18 +732,42 @@ export class InterruptManager<
|
|
|
668
732
|
// Pre-binding TanStack descriptors are still ours: they carry the legacy
|
|
669
733
|
// `metadata.kind` marker, so they keep hydrating through the generic path
|
|
670
734
|
// below.
|
|
671
|
-
if (candidate === undefined && !
|
|
735
|
+
if (candidate === undefined && !legacyResumable) {
|
|
736
|
+
if (hasReservedFirstPartyBindingMarker(interrupt)) {
|
|
737
|
+
return {
|
|
738
|
+
descriptor: interrupt,
|
|
739
|
+
binding: genericBinding(interrupt, hydration, undefined),
|
|
740
|
+
kind: 'generic',
|
|
741
|
+
status: 'error',
|
|
742
|
+
canResolve: false,
|
|
743
|
+
resumable: false,
|
|
744
|
+
error: this.itemError(
|
|
745
|
+
interrupt.id,
|
|
746
|
+
'stale',
|
|
747
|
+
'The interrupt binding is invalid or incomplete.',
|
|
748
|
+
),
|
|
749
|
+
validationGeneration: 0,
|
|
750
|
+
}
|
|
751
|
+
}
|
|
672
752
|
return {
|
|
673
753
|
descriptor: interrupt,
|
|
674
754
|
binding: undefined,
|
|
675
755
|
kind: 'unbound',
|
|
676
756
|
status: 'pending',
|
|
677
757
|
canResolve: false,
|
|
758
|
+
resumable: false,
|
|
678
759
|
validationGeneration: 0,
|
|
679
760
|
}
|
|
680
761
|
}
|
|
681
762
|
|
|
682
763
|
const correlated =
|
|
764
|
+
candidate !== undefined &&
|
|
765
|
+
candidate.interruptId === interrupt.id &&
|
|
766
|
+
candidate.interruptedRunId === hydration.interruptedRunId &&
|
|
767
|
+
candidate.generation === hydration.generation &&
|
|
768
|
+
responseSchemaHash(interrupt) === candidate.responseSchemaHash
|
|
769
|
+
|
|
770
|
+
const structurallyCorrelated =
|
|
683
771
|
candidate !== undefined &&
|
|
684
772
|
candidate.interruptId === interrupt.id &&
|
|
685
773
|
candidate.interruptedRunId === hydration.interruptedRunId &&
|
|
@@ -687,7 +775,32 @@ export class InterruptManager<
|
|
|
687
775
|
candidate.responseSchemaHash ===
|
|
688
776
|
(responseSchemaHash(interrupt) ?? candidate.responseSchemaHash)
|
|
689
777
|
|
|
690
|
-
if (
|
|
778
|
+
if (
|
|
779
|
+
candidate !== undefined &&
|
|
780
|
+
hasFirstPartyGenericMarker(interrupt) &&
|
|
781
|
+
(!structurallyCorrelated ||
|
|
782
|
+
candidate.kind !== 'generic' ||
|
|
783
|
+
candidate.definitionId === undefined ||
|
|
784
|
+
candidate.key === undefined ||
|
|
785
|
+
candidate.batchIndex === undefined)
|
|
786
|
+
) {
|
|
787
|
+
return {
|
|
788
|
+
descriptor: interrupt,
|
|
789
|
+
binding: genericBinding(interrupt, hydration, candidate),
|
|
790
|
+
kind: 'generic',
|
|
791
|
+
status: 'error',
|
|
792
|
+
canResolve: false,
|
|
793
|
+
resumable: false,
|
|
794
|
+
error: this.itemError(
|
|
795
|
+
interrupt.id,
|
|
796
|
+
'stale',
|
|
797
|
+
'The interrupt binding does not match this interrupted run.',
|
|
798
|
+
),
|
|
799
|
+
validationGeneration: 0,
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (structurallyCorrelated && candidate.kind === 'tool-approval') {
|
|
691
804
|
const tool = this.tools?.find(
|
|
692
805
|
(configured) => configured.name === candidate.toolName,
|
|
693
806
|
)
|
|
@@ -714,6 +827,7 @@ export class InterruptManager<
|
|
|
714
827
|
kind: 'tool-approval',
|
|
715
828
|
status: 'pending',
|
|
716
829
|
canResolve: true,
|
|
830
|
+
resumable: true,
|
|
717
831
|
tool,
|
|
718
832
|
validationGeneration: 0,
|
|
719
833
|
}
|
|
@@ -724,7 +838,7 @@ export class InterruptManager<
|
|
|
724
838
|
}
|
|
725
839
|
}
|
|
726
840
|
|
|
727
|
-
if (
|
|
841
|
+
if (structurallyCorrelated && candidate.kind === 'client-tool-execution') {
|
|
728
842
|
const tool = this.tools?.find(
|
|
729
843
|
(configured) => configured.name === candidate.toolName,
|
|
730
844
|
)
|
|
@@ -740,31 +854,112 @@ export class InterruptManager<
|
|
|
740
854
|
kind: 'client-tool-execution',
|
|
741
855
|
status: 'pending',
|
|
742
856
|
canResolve: true,
|
|
857
|
+
resumable: true,
|
|
743
858
|
tool,
|
|
744
859
|
validationGeneration: 0,
|
|
745
860
|
}
|
|
746
861
|
}
|
|
747
862
|
}
|
|
748
863
|
|
|
864
|
+
if (
|
|
865
|
+
candidate !== undefined &&
|
|
866
|
+
candidate.kind === 'generic' &&
|
|
867
|
+
candidate.definitionId !== undefined &&
|
|
868
|
+
candidate.key !== undefined &&
|
|
869
|
+
candidate.batchIndex !== undefined &&
|
|
870
|
+
candidate.key.length > 0 &&
|
|
871
|
+
firstPartyIndexes.get(candidate.batchIndex) !== 1
|
|
872
|
+
) {
|
|
873
|
+
return {
|
|
874
|
+
descriptor: interrupt,
|
|
875
|
+
binding: cloneAndDeepFreezeJson(candidate),
|
|
876
|
+
kind: 'generic',
|
|
877
|
+
status: 'error',
|
|
878
|
+
canResolve: false,
|
|
879
|
+
resumable: false,
|
|
880
|
+
error: this.itemError(
|
|
881
|
+
interrupt.id,
|
|
882
|
+
'stale',
|
|
883
|
+
'Generic interrupt batch contains a duplicate batchIndex.',
|
|
884
|
+
),
|
|
885
|
+
validationGeneration: 0,
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (
|
|
890
|
+
correlated &&
|
|
891
|
+
candidate.kind === 'generic' &&
|
|
892
|
+
candidate.definitionId !== undefined &&
|
|
893
|
+
candidate.key !== undefined &&
|
|
894
|
+
candidate.batchIndex !== undefined &&
|
|
895
|
+
candidate.key.length > 0 &&
|
|
896
|
+
firstPartyIndexes.get(candidate.batchIndex) === 1
|
|
897
|
+
) {
|
|
898
|
+
const definition = this.interruptDefinitions.get(candidate.definitionId)
|
|
899
|
+
if (
|
|
900
|
+
definition !== undefined &&
|
|
901
|
+
definitionSchemaHash(definition.responseSchema) ===
|
|
902
|
+
candidate.responseSchemaHash &&
|
|
903
|
+
(definition.payloadSchema === undefined
|
|
904
|
+
? candidate.payloadSchemaHash === undefined
|
|
905
|
+
: candidate.payloadSchemaHash ===
|
|
906
|
+
definitionSchemaHash(definition.payloadSchema))
|
|
907
|
+
) {
|
|
908
|
+
const rawPayload = getInterruptPayload(interrupt)
|
|
909
|
+
// First-party display payloads are parsed by definition.interrupt()
|
|
910
|
+
// before the server emits them. Re-validating here would feed schema
|
|
911
|
+
// output back through an input schema and reject transforms such as
|
|
912
|
+
// z.string().transform(Number). The checks above still bind this value
|
|
913
|
+
// to the exact descriptor, run, generation, definition, and schemas.
|
|
914
|
+
return {
|
|
915
|
+
descriptor: interrupt,
|
|
916
|
+
binding: cloneAndDeepFreezeJson(candidate),
|
|
917
|
+
definition,
|
|
918
|
+
kind: 'generic',
|
|
919
|
+
status: 'pending',
|
|
920
|
+
canResolve: true,
|
|
921
|
+
resumable: true,
|
|
922
|
+
...(rawPayload === undefined
|
|
923
|
+
? {}
|
|
924
|
+
: { payload: cloneAndDeepFreezeJson(rawPayload) }),
|
|
925
|
+
validationGeneration: 0,
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const resumable =
|
|
931
|
+
legacyResumable ||
|
|
932
|
+
(candidate !== undefined &&
|
|
933
|
+
candidate.interruptId === interrupt.id &&
|
|
934
|
+
candidate.interruptedRunId === hydration.interruptedRunId &&
|
|
935
|
+
candidate.generation === hydration.generation &&
|
|
936
|
+
(candidate.kind !== 'generic' ||
|
|
937
|
+
responseSchemaHash(interrupt) === undefined ||
|
|
938
|
+
candidate.responseSchemaHash === responseSchemaHash(interrupt)))
|
|
749
939
|
return {
|
|
750
940
|
descriptor: interrupt,
|
|
751
941
|
binding: genericBinding(interrupt, hydration, candidate),
|
|
752
942
|
kind: 'generic',
|
|
753
943
|
status: 'pending',
|
|
754
|
-
//
|
|
755
|
-
//
|
|
756
|
-
|
|
944
|
+
// A valid raw binding is an explicit request to use this resume path,
|
|
945
|
+
// even when this client has no registered first-party definition. Keep
|
|
946
|
+
// it untyped, but preserve its existing generic controls. Missing,
|
|
947
|
+
// malformed, and unsupported bindings remain display-only.
|
|
948
|
+
canResolve: resumable,
|
|
949
|
+
resumable,
|
|
757
950
|
validationGeneration: 0,
|
|
758
951
|
}
|
|
759
952
|
}
|
|
760
953
|
|
|
761
954
|
private buildSnapshot(
|
|
762
955
|
transaction?: TransactionToken,
|
|
763
|
-
): BoundInterrupts<TTools> {
|
|
956
|
+
): BoundInterrupts<TTools, TInterrupts> {
|
|
764
957
|
const hydration = this.requireHydration()
|
|
765
|
-
// `client-tool-execution` items stay in `this.items` (they gate
|
|
766
|
-
// submission and are resolved internally via auto-execution /
|
|
767
|
-
// but they are never surfaced as public bound interrupts.
|
|
958
|
+
// `client-tool-execution` items stay in `this.items` (they usually gate
|
|
959
|
+
// batch submission and are resolved internally via auto-execution /
|
|
960
|
+
// addToolResult), but they are never surfaced as public bound interrupts.
|
|
961
|
+
// A mixed generic batch is the exception: those client tools wait for
|
|
962
|
+
// `toolResume` and must not block submit.
|
|
768
963
|
//
|
|
769
964
|
// Items with status `submitting` are also omitted: the resume stream is
|
|
770
965
|
// already in flight, so Approve/Deny is not actionable. Keeping them in
|
|
@@ -776,12 +971,7 @@ export class InterruptManager<
|
|
|
776
971
|
item.kind !== 'client-tool-execution' && item.status !== 'submitting',
|
|
777
972
|
)
|
|
778
973
|
.map((item) => {
|
|
779
|
-
const base = baseSnapshot(
|
|
780
|
-
item,
|
|
781
|
-
hydration,
|
|
782
|
-
() => this.cancelItem(item.descriptor.id, transaction),
|
|
783
|
-
() => this.clearItem(item.descriptor.id, transaction),
|
|
784
|
-
)
|
|
974
|
+
const base = baseSnapshot(item, hydration)
|
|
785
975
|
// Not ours to resume: expose the descriptor so a UI can show the run
|
|
786
976
|
// is paused, with no `resolveInterrupt` to call.
|
|
787
977
|
if (item.kind === 'unbound' || item.binding === undefined) {
|
|
@@ -804,6 +994,9 @@ export class InterruptManager<
|
|
|
804
994
|
toolName: item.binding.toolName,
|
|
805
995
|
toolCallId: item.binding.toolCallId,
|
|
806
996
|
originalArgs: cloneAndDeepFreezeJson(item.binding.originalArgs),
|
|
997
|
+
cancel: () => this.cancelItem(item.descriptor.id, transaction),
|
|
998
|
+
clearResolution: () =>
|
|
999
|
+
this.clearItem(item.descriptor.id, transaction),
|
|
807
1000
|
resolveInterrupt: (approved: boolean, options?: unknown) => {
|
|
808
1001
|
const details = isUnknownObject(options) ? options : undefined
|
|
809
1002
|
this.resolveItem(
|
|
@@ -832,15 +1025,33 @@ export class InterruptManager<
|
|
|
832
1025
|
interruptId: item.descriptor.id,
|
|
833
1026
|
interruptedRunId: hydration.interruptedRunId,
|
|
834
1027
|
generation: hydration.generation,
|
|
835
|
-
responseSchemaHash
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
: 'none',
|
|
1028
|
+
...(typeof item.binding.responseSchemaHash === 'string'
|
|
1029
|
+
? { responseSchemaHash: item.binding.responseSchemaHash }
|
|
1030
|
+
: {}),
|
|
839
1031
|
})
|
|
1032
|
+
if (item.definition !== undefined && item.binding.kind === 'generic') {
|
|
1033
|
+
const snapshot = {
|
|
1034
|
+
...base,
|
|
1035
|
+
kind: 'generic',
|
|
1036
|
+
definitionId: item.definition.id,
|
|
1037
|
+
key: item.binding.key ?? '',
|
|
1038
|
+
payload: item.payload,
|
|
1039
|
+
binding: boundGeneric,
|
|
1040
|
+
cancel: () => this.cancelItem(item.descriptor.id, transaction),
|
|
1041
|
+
clearResolution: () =>
|
|
1042
|
+
this.clearItem(item.descriptor.id, transaction),
|
|
1043
|
+
resolveInterrupt: (response: unknown) =>
|
|
1044
|
+
this.resolveItem(item.descriptor.id, response, transaction),
|
|
1045
|
+
}
|
|
1046
|
+
return Object.freeze(snapshot)
|
|
1047
|
+
}
|
|
840
1048
|
const snapshot: GenericAGUIInterrupt = {
|
|
841
1049
|
...base,
|
|
842
1050
|
kind: 'generic',
|
|
843
1051
|
binding: boundGeneric,
|
|
1052
|
+
cancel: () => this.cancelItem(item.descriptor.id, transaction),
|
|
1053
|
+
clearResolution: () =>
|
|
1054
|
+
this.clearItem(item.descriptor.id, transaction),
|
|
844
1055
|
resolveInterrupt: (payload) =>
|
|
845
1056
|
this.resolveItem(item.descriptor.id, payload, transaction),
|
|
846
1057
|
}
|
|
@@ -851,10 +1062,12 @@ export class InterruptManager<
|
|
|
851
1062
|
// selected by name. TypeScript cannot preserve that per-element lookup
|
|
852
1063
|
// through Array.map, so this generic return boundary restores the proven
|
|
853
1064
|
// distributive public union.
|
|
854
|
-
return Object.freeze(next) as BoundInterrupts<TTools>
|
|
1065
|
+
return Object.freeze(next) as BoundInterrupts<TTools, TInterrupts>
|
|
855
1066
|
}
|
|
856
1067
|
|
|
857
|
-
|
|
1068
|
+
// Provenance belongs to each publication because `onChange` may synchronously
|
|
1069
|
+
// mutate the manager and publish again before an outer callback returns.
|
|
1070
|
+
private publish(source: InterruptManagerChangeSource = 'live'): void {
|
|
858
1071
|
if (!this.hydration) {
|
|
859
1072
|
this.snapshot = Object.freeze([])
|
|
860
1073
|
this.state = Object.freeze({
|
|
@@ -863,7 +1076,7 @@ export class InterruptManager<
|
|
|
863
1076
|
interruptErrors: this.rootErrors,
|
|
864
1077
|
resuming: this.resuming,
|
|
865
1078
|
})
|
|
866
|
-
this.options.onChange?.()
|
|
1079
|
+
this.options.onChange?.(source)
|
|
867
1080
|
return
|
|
868
1081
|
}
|
|
869
1082
|
this.snapshot = this.buildSnapshot()
|
|
@@ -873,7 +1086,7 @@ export class InterruptManager<
|
|
|
873
1086
|
interruptErrors: this.rootErrors,
|
|
874
1087
|
resuming: this.resuming,
|
|
875
1088
|
})
|
|
876
|
-
this.options.onChange?.()
|
|
1089
|
+
this.options.onChange?.(source)
|
|
877
1090
|
}
|
|
878
1091
|
|
|
879
1092
|
private resolveItem(
|
|
@@ -929,7 +1142,9 @@ export class InterruptManager<
|
|
|
929
1142
|
const item = this.findItem(interruptId)
|
|
930
1143
|
this.invalidateRetry()
|
|
931
1144
|
item.validationGeneration++
|
|
932
|
-
item.resolution = Object.freeze(
|
|
1145
|
+
item.resolution = Object.freeze(
|
|
1146
|
+
resolutionWithContinuation(item, { interruptId, status: 'cancelled' }),
|
|
1147
|
+
)
|
|
933
1148
|
item.status = 'staged'
|
|
934
1149
|
item.error = undefined
|
|
935
1150
|
if (!transaction) {
|
|
@@ -954,7 +1169,12 @@ export class InterruptManager<
|
|
|
954
1169
|
// owns them. Including them in the completeness gate would deadlock the
|
|
955
1170
|
// batch, so the run's own interrupts could never be answered once a
|
|
956
1171
|
// foreign one shared the stream.
|
|
957
|
-
const
|
|
1172
|
+
const hasGeneric = this.items.some((item) => item.kind === 'generic')
|
|
1173
|
+
const ours = this.items.filter(
|
|
1174
|
+
(item) =>
|
|
1175
|
+
isClientOwnedInterrupt(item) &&
|
|
1176
|
+
!(hasGeneric && item.kind === 'client-tool-execution'),
|
|
1177
|
+
)
|
|
958
1178
|
if (
|
|
959
1179
|
ours.length === 0 ||
|
|
960
1180
|
ours.some(
|
|
@@ -1019,11 +1239,13 @@ export class InterruptManager<
|
|
|
1019
1239
|
if (!transaction) this.publish()
|
|
1020
1240
|
return
|
|
1021
1241
|
}
|
|
1022
|
-
item.resolution = cloneAndDeepFreezeJson(
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1242
|
+
item.resolution = cloneAndDeepFreezeJson(
|
|
1243
|
+
resolutionWithContinuation(item, {
|
|
1244
|
+
interruptId: item.descriptor.id,
|
|
1245
|
+
status: 'resolved',
|
|
1246
|
+
payload: result.payload,
|
|
1247
|
+
}),
|
|
1248
|
+
)
|
|
1027
1249
|
item.status = 'staged'
|
|
1028
1250
|
item.error = undefined
|
|
1029
1251
|
if (!transaction) {
|
|
@@ -1037,11 +1259,17 @@ export class InterruptManager<
|
|
|
1037
1259
|
payload: unknown,
|
|
1038
1260
|
): ValidationResult | Promise<ValidationResult> {
|
|
1039
1261
|
if (item.kind === 'generic') {
|
|
1040
|
-
|
|
1041
|
-
item.descriptor.responseSchema,
|
|
1262
|
+
const validation = validateWithSchema(
|
|
1263
|
+
item.definition?.responseSchema ?? item.descriptor.responseSchema,
|
|
1042
1264
|
payload,
|
|
1043
1265
|
'invalid-payload',
|
|
1044
1266
|
)
|
|
1267
|
+
if (item.definition === undefined) return validation
|
|
1268
|
+
const preserveInput = (result: ValidationResult): ValidationResult =>
|
|
1269
|
+
'valid' in result ? { valid: true, payload } : result
|
|
1270
|
+
return isPromiseLike(validation)
|
|
1271
|
+
? Promise.resolve(validation).then(preserveInput)
|
|
1272
|
+
: preserveInput(validation)
|
|
1045
1273
|
}
|
|
1046
1274
|
if (item.kind === 'client-tool-execution') {
|
|
1047
1275
|
return validateWithSchema(
|
|
@@ -1160,7 +1388,8 @@ export class InterruptManager<
|
|
|
1160
1388
|
// addToolResult); they are transparent to the boolean shorthand. Eligibility
|
|
1161
1389
|
// and resolution consider only the publicly resolvable items.
|
|
1162
1390
|
const resolvable = this.items.filter(
|
|
1163
|
-
(item) =>
|
|
1391
|
+
(item) =>
|
|
1392
|
+
isClientOwnedInterrupt(item) && item.kind !== 'client-tool-execution',
|
|
1164
1393
|
)
|
|
1165
1394
|
const eligible = resolvable.every(
|
|
1166
1395
|
(item) =>
|
|
@@ -1191,7 +1420,9 @@ export class InterruptManager<
|
|
|
1191
1420
|
}
|
|
1192
1421
|
|
|
1193
1422
|
private resolveTransaction(
|
|
1194
|
-
resolver: (
|
|
1423
|
+
resolver: (
|
|
1424
|
+
interrupt: ResolvableChatInterrupt<TTools, TInterrupts>,
|
|
1425
|
+
) => unknown,
|
|
1195
1426
|
): void {
|
|
1196
1427
|
const checkpoints = this.items.map<RuntimeInterruptCheckpoint>((item) => ({
|
|
1197
1428
|
status: item.status,
|
|
@@ -1201,7 +1432,7 @@ export class InterruptManager<
|
|
|
1201
1432
|
}))
|
|
1202
1433
|
const token: TransactionToken = { active: true }
|
|
1203
1434
|
this.activeTransaction = token
|
|
1204
|
-
const stable = this.buildSnapshot(token)
|
|
1435
|
+
const stable = this.buildSnapshot(token).filter(isRootResolvableInterrupt)
|
|
1205
1436
|
let failure:
|
|
1206
1437
|
| { code: BatchInterruptError['code']; message: string }
|
|
1207
1438
|
| undefined
|
|
@@ -1227,7 +1458,8 @@ export class InterruptManager<
|
|
|
1227
1458
|
// `client-tool-execution` items are resolved out-of-band (auto
|
|
1228
1459
|
// execution / addToolResult), not by this synchronous resolver, so
|
|
1229
1460
|
// they don't count against transaction completeness. `maybeSubmit`
|
|
1230
|
-
// still
|
|
1461
|
+
// still waits for them unless a generic interrupt shares the batch.
|
|
1462
|
+
isClientOwnedInterrupt(item) &&
|
|
1231
1463
|
item.kind !== 'client-tool-execution' &&
|
|
1232
1464
|
(item.resolution === undefined || item.status !== 'staged'),
|
|
1233
1465
|
)
|
|
@@ -1298,7 +1530,9 @@ export class InterruptManager<
|
|
|
1298
1530
|
private submitBatch(submission: InterruptManagerSubmission): void {
|
|
1299
1531
|
this.resuming = true
|
|
1300
1532
|
this.retrySubmission = undefined
|
|
1301
|
-
for (const item of this.items)
|
|
1533
|
+
for (const item of this.items) {
|
|
1534
|
+
if (isClientOwnedInterrupt(item)) item.status = 'submitting'
|
|
1535
|
+
}
|
|
1302
1536
|
this.publish()
|
|
1303
1537
|
void this.performSubmission(submission)
|
|
1304
1538
|
}
|
|
@@ -1325,7 +1559,9 @@ export class InterruptManager<
|
|
|
1325
1559
|
const message = error instanceof Error ? error.message : String(error)
|
|
1326
1560
|
this.addRootError('transport', message, true, 'transport')
|
|
1327
1561
|
this.retrySubmission = submission
|
|
1328
|
-
for (const item of this.items)
|
|
1562
|
+
for (const item of this.items) {
|
|
1563
|
+
if (isClientOwnedInterrupt(item)) item.status = 'error'
|
|
1564
|
+
}
|
|
1329
1565
|
return
|
|
1330
1566
|
}
|
|
1331
1567
|
|
|
@@ -1355,6 +1591,7 @@ export class InterruptManager<
|
|
|
1355
1591
|
if (submissionError.scope === 'item') {
|
|
1356
1592
|
const item = this.items.find(
|
|
1357
1593
|
(candidate) =>
|
|
1594
|
+
isClientOwnedInterrupt(candidate) &&
|
|
1358
1595
|
candidate.descriptor.id === submissionError.interruptId,
|
|
1359
1596
|
)
|
|
1360
1597
|
if (item) {
|
|
@@ -1373,7 +1610,9 @@ export class InterruptManager<
|
|
|
1373
1610
|
this.rootErrors = mergedBatchErrors.rootErrors
|
|
1374
1611
|
this.submissionRootErrors = mergedBatchErrors.submissionRootErrors
|
|
1375
1612
|
for (const item of this.items) {
|
|
1376
|
-
if (item.status === 'submitting')
|
|
1613
|
+
if (isClientOwnedInterrupt(item) && item.status === 'submitting') {
|
|
1614
|
+
item.status = 'error'
|
|
1615
|
+
}
|
|
1377
1616
|
}
|
|
1378
1617
|
this.retrySubmission = retryable && !nonRetryable ? submission : undefined
|
|
1379
1618
|
}
|
|
@@ -1394,7 +1633,9 @@ export class InterruptManager<
|
|
|
1394
1633
|
source,
|
|
1395
1634
|
retryable,
|
|
1396
1635
|
interruptIds: Object.freeze(
|
|
1397
|
-
this.items
|
|
1636
|
+
this.items
|
|
1637
|
+
.filter(isClientOwnedInterrupt)
|
|
1638
|
+
.map((item) => item.descriptor.id),
|
|
1398
1639
|
),
|
|
1399
1640
|
threadId: hydration.threadId,
|
|
1400
1641
|
interruptedRunId: hydration.interruptedRunId,
|