@tanstack/ai-client 0.23.3 → 0.25.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.
@@ -8,7 +8,7 @@ export type { McpAppBridge, CreateMcpAppBridgeOptions } from './mcp-app-bridge.j
8
8
  export { RealtimeClient } from './realtime-client.js';
9
9
  export { GenerationClient } from './generation-client.js';
10
10
  export { VideoGenerationClient } from './video-generation-client.js';
11
- export type { UIMessage, MessagePart, TextPart, ToolCallPart, ToolResultPart, ThinkingPart, StructuredOutputPart, ChatClientPersistence, ChatPersistedState, ChatPersistenceOption, ChatStorageAdapter, ChatClientOptions, ChatPendingInterrupt, BoundInterruptBase, BoundInterrupts, ChatInterrupt, ChatInterruptState, GenericAGUIInterrupt, UnboundInterrupt, InterruptItemStatus, ToolApprovalInterrupt, ClientContextOptionFromTools, ChatResumeState, ChatRequestBody, InferChatMessages, InferredClientContext, ChatClientState, ConnectionStatus, ChatFetcher, ChatFetcherInput, ChatFetcherOptions, ChatTransport, DistributedOmit, MultimodalContent, QueuedMessage, WhenBusy, QueueBusyReason, QueueConfig, QueueStrategy, QueueOption, SendMessageOptions, } from './types.js';
11
+ export type { UIMessage, MessagePart, TextPart, ToolCallPart, ToolResultPart, ThinkingPart, StructuredOutputPart, ChatClientPersistence, ChatPersistedState, ChatPersistenceOption, ChatPersistenceOptions, ChatStorageAdapter, ChatClientOptions, ChatPendingInterrupt, BoundInterruptBase, BoundInterrupts, ChatInterrupt, ChatInterruptState, GenericAGUIInterrupt, GenericInterrupt, RegisteredGenericInterrupt, ResolvableChatInterrupt, UnboundInterrupt, InterruptItemStatus, ToolApprovalInterrupt, ClientContextOptionFromTools, ChatResumeState, ChatRequestBody, InferChatMessages, InferredClientContext, ChatClientState, ConnectionStatus, ChatFetcher, ChatFetcherInput, ChatFetcherOptions, ChatTransport, DistributedOmit, MultimodalContent, QueuedMessage, WhenBusy, QueueBusyReason, QueueConfig, QueueStrategy, QueueOption, SendMessageOptions, } from './types.js';
12
12
  export type { InferGenerationOutput, InferGenerationOutputFromReturn, GenerationClientState, GenerationPersistenceOptions, GenerationClientOptions, GenerationFetcher, GenerationFetcherOptions, GenerationTransport, VideoGenerationClientOptions, VideoStatusInfo, VideoGenerateResult, ImageGenerateInput, AudioGenerateInput, SpeechGenerateInput, TranscriptionGenerateInput, SummarizeGenerateInput, VideoGenerateInput, GenerationRestoredResult, } from './generation-types.js';
13
13
  export { GENERATION_EVENTS } from './generation-types.js';
14
14
  export { reconstructImageResult, reconstructAudioResult, reconstructSpeechResult, reconstructTranscriptionResult, reconstructSummarizeResult, } from './generation-reconstruct.js';
@@ -20,6 +20,6 @@ export { createAIDevtoolsGenerationPreview, type AIDevtoolsClientMetadata, type
20
20
  export type { ExtractToolNames, ExtractToolInput, ExtractToolOutput, } from './tool-types.js';
21
21
  export type { AnyClientTool, Interrupt, PersistedArtifactActivity, PersistedArtifactRef, PersistedArtifactRole, RunAgentResumeItem, RunFinishedOutcome, } from '@tanstack/ai/client';
22
22
  export type { RealtimeAdapter, RealtimeConnection, RealtimeClientOptions, RealtimeClientState, RealtimeStateChangeCallback, } from './realtime-types.js';
23
- export { fetchServerSentEvents, fetchHttpStream, xhrServerSentEvents, xhrHttpStream, stream, rpcStream, StreamTruncatedError, DurableStreamIncompleteError, StreamReconnectLimitError, type ConnectConnectionAdapter, type ConnectionAdapter, type GenerationHydrationResult, type FetchConnectionOptions, type ReconnectOptions, type ResumableConnectConnectionAdapter, type RunAgentInputContext, type StreamConnectionHandlers, type SubscribeConnectionAdapter, type XhrConnectionOptions, } from './connection-adapters.js';
23
+ export { fetchServerSentEvents, fetchHttpStream, xhrServerSentEvents, xhrHttpStream, stream, rpcStream, webSocket, StreamTruncatedError, DurableStreamIncompleteError, StreamReconnectLimitError, type ConnectConnectionAdapter, type ConnectionAdapter, type GenerationHydrationResult, type FetchConnectionOptions, type ReconnectOptions, type ResumableConnectConnectionAdapter, type RunAgentInputContext, type StreamConnectionHandlers, type SubscribeConnectionAdapter, type WebSocketConnectionOptions, type XhrConnectionOptions, } from './connection-adapters.js';
24
24
  export { uiMessageToModelMessages, modelMessageToUIMessage, modelMessagesToUIMessages, convertMessagesToModelMessages, normalizeToUIMessage, generateMessageId, } from '@tanstack/ai/client';
25
25
  export { StreamProcessor, ImmediateStrategy, PunctuationStrategy, BatchStrategy, WordBoundaryStrategy, CompositeStrategy, parsePartialJSON, PartialJSONParser, defaultJSONParser, type ChunkStrategy, type StreamProcessorOptions, type StreamProcessorEvents, type InternalToolCallState, type ToolCallState, type ToolResultState, type JSONParser, type ChunkRecording, type ProcessorResult, type ProcessorState, } from '@tanstack/ai/client';
package/dist/esm/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { AudioRecorder } from "./audio-recorder.js";
2
2
  import { UnsupportedResponseStreamError } from "./response-stream.js";
3
- import { DurableStreamIncompleteError, StreamReconnectLimitError, StreamTruncatedError, fetchHttpStream, fetchServerSentEvents, rpcStream, stream, xhrHttpStream, xhrServerSentEvents } from "./connection-adapters.js";
3
+ import { DurableStreamIncompleteError, StreamReconnectLimitError, StreamTruncatedError, fetchHttpStream, fetchServerSentEvents, rpcStream, stream, webSocket, xhrHttpStream, xhrServerSentEvents } from "./connection-adapters.js";
4
4
  import { InterruptManager } from "./interrupt-manager.js";
5
5
  import { ChatClient } from "./chat-client.js";
6
6
  import { createMcpAppBridge } from "./mcp-app-bridge.js";
@@ -13,4 +13,4 @@ import { clientTools, createChatClientOptions } from "./types.js";
13
13
  import { StorageUnavailableError, indexedDBPersistence, localStoragePersistence, sessionStoragePersistence } from "./storage-adapters.js";
14
14
  import { createAIDevtoolsGenerationPreview } from "./devtools.js";
15
15
  import { BatchStrategy, CompositeStrategy, ImmediateStrategy, PartialJSONParser, PunctuationStrategy, StreamProcessor, WordBoundaryStrategy, convertMessagesToModelMessages, defaultJSONParser, generateMessageId, modelMessageToUIMessage, modelMessagesToUIMessages, normalizeToUIMessage, parsePartialJSON, uiMessageToModelMessages } from "@tanstack/ai/client";
16
- export { AudioRecorder, BatchStrategy, ChatClient, CompositeStrategy, DurableStreamIncompleteError, GENERATION_EVENTS, GenerationClient, ImmediateStrategy, InterruptManager, PartialJSONParser, PunctuationStrategy, RealtimeClient, StorageUnavailableError, StreamProcessor, StreamReconnectLimitError, StreamTruncatedError, UnsupportedResponseStreamError, VideoGenerationClient, WordBoundaryStrategy, clientTools, convertMessagesToModelMessages, createAIDevtoolsGenerationPreview, createChatClientOptions, createMcpAppBridge, defaultJSONParser, fetchHttpStream, fetchServerSentEvents, generateMessageId, indexedDBPersistence, localStoragePersistence, modelMessageToUIMessage, modelMessagesToUIMessages, normalizeToUIMessage, parsePartialJSON, reconstructAudioResult, reconstructImageResult, reconstructSpeechResult, reconstructSummarizeResult, reconstructTranscriptionResult, rpcStream, sessionStoragePersistence, stream, uiMessageToModelMessages, xhrHttpStream, xhrServerSentEvents };
16
+ export { AudioRecorder, BatchStrategy, ChatClient, CompositeStrategy, DurableStreamIncompleteError, GENERATION_EVENTS, GenerationClient, ImmediateStrategy, InterruptManager, PartialJSONParser, PunctuationStrategy, RealtimeClient, StorageUnavailableError, StreamProcessor, StreamReconnectLimitError, StreamTruncatedError, UnsupportedResponseStreamError, VideoGenerationClient, WordBoundaryStrategy, clientTools, convertMessagesToModelMessages, createAIDevtoolsGenerationPreview, createChatClientOptions, createMcpAppBridge, defaultJSONParser, fetchHttpStream, fetchServerSentEvents, generateMessageId, indexedDBPersistence, localStoragePersistence, modelMessageToUIMessage, modelMessagesToUIMessages, normalizeToUIMessage, parsePartialJSON, reconstructAudioResult, reconstructImageResult, reconstructSpeechResult, reconstructSummarizeResult, reconstructTranscriptionResult, rpcStream, sessionStoragePersistence, stream, uiMessageToModelMessages, webSocket, xhrHttpStream, xhrServerSentEvents };
@@ -1,5 +1,5 @@
1
- import { AnyClientTool, BatchInterruptError, Interrupt, RunAgentResumeItem } from '@tanstack/ai/client';
2
- import { BoundInterrupts, ChatInterrupt, ChatInterruptState } from './types.js';
1
+ import { AnyClientTool, BatchInterruptError, Interrupt, InterruptDefinition, RunAgentResumeItem } from '@tanstack/ai/client';
2
+ import { BoundInterrupts, ChatInterruptState, ResolvableChatInterrupt } from './types.js';
3
3
  export interface InterruptManagerHydration {
4
4
  threadId: string;
5
5
  interruptedRunId: string;
@@ -14,12 +14,14 @@ export interface InterruptManagerSubmission {
14
14
  canonicalResolutions: string;
15
15
  fingerprint: string;
16
16
  }
17
- export interface InterruptManagerOptions<TTools extends ReadonlyArray<AnyClientTool>> {
17
+ export type InterruptManagerChangeSource = 'hydrate' | 'live';
18
+ export interface InterruptManagerOptions<TTools extends ReadonlyArray<AnyClientTool>, TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> = readonly []> {
18
19
  tools?: TTools;
20
+ interrupts?: TInterrupts;
19
21
  submit: (submission: InterruptManagerSubmission) => Promise<void>;
20
- onChange?: () => void;
22
+ onChange?: (source: InterruptManagerChangeSource) => void;
21
23
  }
22
- export declare class InterruptManager<TTools extends ReadonlyArray<AnyClientTool> = ReadonlyArray<AnyClientTool>> {
24
+ export declare class InterruptManager<TTools extends ReadonlyArray<AnyClientTool> = ReadonlyArray<AnyClientTool>, TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> = readonly []> {
23
25
  private readonly options;
24
26
  private hydration;
25
27
  private items;
@@ -31,19 +33,22 @@ export declare class InterruptManager<TTools extends ReadonlyArray<AnyClientTool
31
33
  private retrySubmission;
32
34
  private resuming;
33
35
  private tools;
34
- constructor(options: InterruptManagerOptions<TTools>);
36
+ private readonly interruptDefinitions;
37
+ constructor(options: InterruptManagerOptions<TTools, TInterrupts>);
35
38
  updateTools(tools: TTools): void;
36
- hydrate(hydration: InterruptManagerHydration): void;
37
- getInterrupts(): BoundInterrupts<TTools>;
38
- getState(): ChatInterruptState<TTools>;
39
+ hydrate(hydration: InterruptManagerHydration, source?: InterruptManagerChangeSource): void;
40
+ getInterrupts(): BoundInterrupts<TTools, TInterrupts>;
41
+ getState(): ChatInterruptState<TTools, TInterrupts>;
39
42
  getDescriptors(): ReadonlyArray<Interrupt>;
43
+ hasValidatedFirstPartyGenericBatch(): boolean;
40
44
  reset(options?: {
41
45
  preserveRootErrors?: boolean;
46
+ source?: InterruptManagerChangeSource;
42
47
  }): void;
43
48
  getInterruptErrors(): ReadonlyArray<BatchInterruptError>;
44
49
  getResuming(): boolean;
45
50
  resolve(approved: boolean): void;
46
- resolve(resolver: (interrupt: ChatInterrupt<TTools>) => undefined): void;
51
+ resolve(resolver: (interrupt: ResolvableChatInterrupt<TTools, TInterrupts>) => undefined): void;
47
52
  cancel(): void;
48
53
  retry(): void;
49
54
  resolveClientToolOutput(toolCallId: string, output: unknown): boolean;
@@ -1,5 +1,19 @@
1
- import { INTERRUPT_BINDING_METADATA_KEY, INTERRUPT_BINDING_VERSION, canonicalInterruptJson, canonicalizeInterruptResolutions, cloneAndDeepFreezeJson, digestInterruptJson, hashSchemaInput, isStandardSchema, normalizeApprovalSchema } from "@tanstack/ai/client";
1
+ import { INTERRUPT_BINDING_METADATA_KEY, INTERRUPT_BINDING_VERSION, canonicalInterruptJson, canonicalizeInterruptResolutions, cloneAndDeepFreezeJson, digestInterruptJson, genericInterruptContinuationFromDescriptor, hashInterruptDefinitionSchema, hashSchemaInput, isStandardSchema, normalizeApprovalSchema, readInterruptBinding, wrapGenericInterruptContinuation } from "@tanstack/ai/client";
2
2
  //#region src/interrupt-manager.ts
3
+ function isClientOwnedInterrupt(item) {
4
+ return item.resumable;
5
+ }
6
+ function resolutionWithContinuation(item, resolution) {
7
+ const continuation = genericInterruptContinuationFromDescriptor(item.descriptor);
8
+ if (!continuation) return resolution;
9
+ return {
10
+ ...resolution,
11
+ metadata: wrapGenericInterruptContinuation(continuation)
12
+ };
13
+ }
14
+ function isRootResolvableInterrupt(interrupt) {
15
+ return "cancel" in interrupt && "clearResolution" in interrupt;
16
+ }
3
17
  var itemErrorCodes = /* @__PURE__ */ new Set([
4
18
  "invalid-payload",
5
19
  "invalid-edited-args",
@@ -45,51 +59,24 @@ function isLegacyClientToolMetadata(value) {
45
59
  function isLegacyInterruptMetadata(interrupt) {
46
60
  return isLegacyApprovalMetadata(interrupt.metadata) || isLegacyClientToolMetadata(interrupt.metadata);
47
61
  }
48
- function isBindingBase(value) {
49
- return (value["v"] === void 0 || value["v"] === INTERRUPT_BINDING_VERSION) && typeof value["kind"] === "string" && typeof value["interruptId"] === "string" && typeof value["interruptedRunId"] === "string" && typeof value["generation"] === "number" && Number.isInteger(value["generation"]) && value["generation"] >= 0 && typeof value["responseSchemaHash"] === "string" && (value["expiresAt"] === void 0 || typeof value["expiresAt"] === "string" && Number.isFinite(Date.parse(value["expiresAt"])));
62
+ function getDescriptorBinding(interrupt) {
63
+ return readInterruptBinding(interrupt);
50
64
  }
51
- function readBinding(value) {
52
- if (!isUnknownObject(value) || !isBindingBase(value)) return void 0;
53
- const expiresAt = typeof value["expiresAt"] === "string" ? value["expiresAt"] : void 0;
54
- if (value["kind"] === "generic") return {
55
- v: INTERRUPT_BINDING_VERSION,
56
- kind: "generic",
57
- interruptId: String(value["interruptId"]),
58
- interruptedRunId: String(value["interruptedRunId"]),
59
- generation: Number(value["generation"]),
60
- responseSchemaHash: String(value["responseSchemaHash"]),
61
- ...expiresAt !== void 0 ? { expiresAt } : {}
62
- };
63
- if (value["kind"] === "client-tool-execution" && typeof value["toolName"] === "string" && typeof value["toolCallId"] === "string" && typeof value["outputSchemaHash"] === "string") return {
64
- v: INTERRUPT_BINDING_VERSION,
65
- kind: "client-tool-execution",
66
- interruptId: String(value["interruptId"]),
67
- interruptedRunId: String(value["interruptedRunId"]),
68
- generation: Number(value["generation"]),
69
- toolName: value["toolName"],
70
- toolCallId: value["toolCallId"],
71
- outputSchemaHash: value["outputSchemaHash"],
72
- responseSchemaHash: String(value["responseSchemaHash"]),
73
- ...expiresAt !== void 0 ? { expiresAt } : {}
74
- };
75
- if (value["kind"] === "tool-approval" && typeof value["toolName"] === "string" && typeof value["toolCallId"] === "string" && typeof value["inputSchemaHash"] === "string" && typeof value["approvalSchemaHash"] === "string" && "originalArgs" in value) return {
76
- v: INTERRUPT_BINDING_VERSION,
77
- kind: "tool-approval",
78
- interruptId: String(value["interruptId"]),
79
- interruptedRunId: String(value["interruptedRunId"]),
80
- generation: Number(value["generation"]),
81
- toolName: value["toolName"],
82
- toolCallId: value["toolCallId"],
83
- originalArgs: value["originalArgs"],
84
- inputSchemaHash: value["inputSchemaHash"],
85
- approvalSchemaHash: value["approvalSchemaHash"],
86
- responseSchemaHash: String(value["responseSchemaHash"]),
87
- ...expiresAt !== void 0 ? { expiresAt } : {}
88
- };
65
+ function hasReservedFirstPartyBindingMarker(interrupt) {
66
+ if (!isUnknownObject(interrupt.metadata)) return false;
67
+ const binding = interrupt.metadata[INTERRUPT_BINDING_METADATA_KEY];
68
+ if (!isUnknownObject(binding)) return false;
69
+ if (binding["v"] !== void 0 && binding["v"] !== INTERRUPT_BINDING_VERSION) return false;
70
+ return binding["kind"] === "generic" || binding["kind"] === "tool-approval" || binding["kind"] === "client-tool-execution" || "definitionId" in binding || "key" in binding || "batchIndex" in binding || "payloadSchemaHash" in binding;
89
71
  }
90
- function getDescriptorBinding(interrupt) {
91
- const candidate = interrupt.metadata?.[INTERRUPT_BINDING_METADATA_KEY];
92
- return readBinding(candidate);
72
+ function hasFirstPartyGenericMarker(interrupt) {
73
+ if (!isUnknownObject(interrupt.metadata)) return false;
74
+ const binding = interrupt.metadata[INTERRUPT_BINDING_METADATA_KEY];
75
+ if (!isUnknownObject(binding) || binding["kind"] !== "generic") return false;
76
+ return "definitionId" in binding || "key" in binding || "batchIndex" in binding || "payloadSchemaHash" in binding;
77
+ }
78
+ function getInterruptPayload(interrupt) {
79
+ return interrupt.metadata?.["tanstack:interruptPayload"];
93
80
  }
94
81
  /**
95
82
  * Only used to route *legacy* (pre-binding) descriptors, which have no binding
@@ -106,6 +93,14 @@ function responseSchemaHash(interrupt) {
106
93
  return;
107
94
  }
108
95
  }
96
+ function definitionSchemaHash(schema) {
97
+ if (schema === void 0) return void 0;
98
+ try {
99
+ return hashInterruptDefinitionSchema(schema);
100
+ } catch {
101
+ return;
102
+ }
103
+ }
109
104
  function isPromiseLike(value) {
110
105
  return value !== null && (typeof value === "object" || typeof value === "function") && "then" in value && typeof value.then === "function";
111
106
  }
@@ -179,17 +174,18 @@ function submissionErrorMatchesActiveBatch(error, submission) {
179
174
  return error.scope === "item" ? interruptIds.includes(error.interruptId) : haveSameInterruptIds(error.interruptIds, interruptIds);
180
175
  }
181
176
  function genericBinding(interrupt, hydration, candidate) {
177
+ const schemaHash = responseSchemaHash(interrupt) ?? candidate?.responseSchemaHash;
182
178
  return cloneAndDeepFreezeJson({
183
179
  v: INTERRUPT_BINDING_VERSION,
184
180
  kind: "generic",
185
181
  interruptId: interrupt.id,
186
182
  interruptedRunId: hydration.interruptedRunId,
187
183
  generation: hydration.generation,
188
- responseSchemaHash: responseSchemaHash(interrupt) ?? candidate?.responseSchemaHash ?? "invalid",
184
+ ...schemaHash !== void 0 ? { responseSchemaHash: schemaHash } : {},
189
185
  ...interrupt.expiresAt !== void 0 ? { expiresAt: interrupt.expiresAt } : {}
190
186
  });
191
187
  }
192
- function baseSnapshot(item, hydration, cancel, clearResolution) {
188
+ function baseSnapshot(item, hydration) {
193
189
  const descriptor = cloneAndDeepFreezeJson(item.descriptor);
194
190
  const errors = item.error === void 0 ? Object.freeze([]) : Object.freeze([cloneAndDeepFreezeJson(item.error)]);
195
191
  const error = errors[0];
@@ -207,9 +203,7 @@ function baseSnapshot(item, hydration, cancel, clearResolution) {
207
203
  status: item.status,
208
204
  errors,
209
205
  ...error !== void 0 ? { error } : {},
210
- canResolve: item.canResolve,
211
- cancel,
212
- clearResolution
206
+ canResolve: item.canResolve
213
207
  };
214
208
  }
215
209
  var InterruptManager = class {
@@ -229,26 +223,38 @@ var InterruptManager = class {
229
223
  retrySubmission;
230
224
  resuming = false;
231
225
  tools;
226
+ interruptDefinitions;
232
227
  constructor(options) {
233
228
  this.options = options;
234
229
  this.tools = options.tools;
230
+ const definitions = /* @__PURE__ */ new Map();
231
+ for (const definition of options.interrupts ?? []) {
232
+ if (definitions.has(definition.id)) throw new Error(`Duplicate interrupt definition id: ${definition.id}`);
233
+ definitions.set(definition.id, definition);
234
+ }
235
+ this.interruptDefinitions = definitions;
235
236
  }
236
237
  updateTools(tools) {
237
238
  this.tools = tools;
238
239
  }
239
- hydrate(hydration) {
240
+ hydrate(hydration, source = "live") {
240
241
  this.hydration = {
241
242
  threadId: hydration.threadId,
242
243
  interruptedRunId: hydration.interruptedRunId,
243
244
  generation: hydration.generation,
244
245
  interrupts: cloneAndDeepFreezeJson(hydration.interrupts)
245
246
  };
246
- this.items = hydration.interrupts.map((interrupt) => this.hydrateInterrupt(interrupt, hydration));
247
+ const firstPartyIndexes = /* @__PURE__ */ new Map();
248
+ for (const interrupt of hydration.interrupts) {
249
+ const binding = getDescriptorBinding(interrupt);
250
+ if (binding?.kind === "generic" && binding.definitionId !== void 0 && binding.key !== void 0 && binding.batchIndex !== void 0) firstPartyIndexes.set(binding.batchIndex, (firstPartyIndexes.get(binding.batchIndex) ?? 0) + 1);
251
+ }
252
+ this.items = hydration.interrupts.map((interrupt) => this.hydrateInterrupt(interrupt, hydration, firstPartyIndexes));
247
253
  this.rootErrors = Object.freeze([]);
248
254
  this.submissionRootErrors = Object.freeze([]);
249
255
  this.retrySubmission = void 0;
250
256
  this.resuming = false;
251
- this.publish();
257
+ this.publish(source);
252
258
  }
253
259
  getInterrupts() {
254
260
  return this.snapshot;
@@ -259,6 +265,9 @@ var InterruptManager = class {
259
265
  getDescriptors() {
260
266
  return this.hydration?.interrupts ?? Object.freeze([]);
261
267
  }
268
+ hasValidatedFirstPartyGenericBatch() {
269
+ return this.items.some((item) => item.kind === "generic" && item.definition !== void 0);
270
+ }
262
271
  reset(options) {
263
272
  this.hydration = void 0;
264
273
  this.items = [];
@@ -275,7 +284,7 @@ var InterruptManager = class {
275
284
  interruptErrors: this.rootErrors,
276
285
  resuming: false
277
286
  });
278
- this.options.onChange?.();
287
+ this.options.onChange?.(options?.source ?? "live");
279
288
  }
280
289
  getInterruptErrors() {
281
290
  return this.rootErrors;
@@ -295,11 +304,12 @@ var InterruptManager = class {
295
304
  this.assertRootMutable();
296
305
  this.invalidateRetry();
297
306
  for (const item of this.items) {
307
+ if (!isClientOwnedInterrupt(item)) continue;
298
308
  item.validationGeneration++;
299
- item.resolution = Object.freeze({
309
+ item.resolution = Object.freeze(resolutionWithContinuation(item, {
300
310
  interruptId: item.descriptor.id,
301
311
  status: "cancelled"
302
- });
312
+ }));
303
313
  item.status = "staged";
304
314
  item.error = void 0;
305
315
  }
@@ -327,19 +337,44 @@ var InterruptManager = class {
327
337
  this.resolveItem(item.descriptor.id, { approved });
328
338
  return true;
329
339
  }
330
- hydrateInterrupt(descriptor, hydration) {
340
+ hydrateInterrupt(descriptor, hydration, firstPartyIndexes) {
331
341
  const interrupt = cloneAndDeepFreezeJson(descriptor);
332
342
  const candidate = getDescriptorBinding(interrupt);
333
- if (candidate === void 0 && !isLegacyInterruptMetadata(interrupt)) return {
343
+ const legacyResumable = candidate === void 0 && isLegacyInterruptMetadata(interrupt);
344
+ if (candidate === void 0 && !legacyResumable) {
345
+ if (hasReservedFirstPartyBindingMarker(interrupt)) return {
346
+ descriptor: interrupt,
347
+ binding: genericBinding(interrupt, hydration, void 0),
348
+ kind: "generic",
349
+ status: "error",
350
+ canResolve: false,
351
+ resumable: false,
352
+ error: this.itemError(interrupt.id, "stale", "The interrupt binding is invalid or incomplete."),
353
+ validationGeneration: 0
354
+ };
355
+ return {
356
+ descriptor: interrupt,
357
+ binding: void 0,
358
+ kind: "unbound",
359
+ status: "pending",
360
+ canResolve: false,
361
+ resumable: false,
362
+ validationGeneration: 0
363
+ };
364
+ }
365
+ const correlated = candidate !== void 0 && candidate.interruptId === interrupt.id && candidate.interruptedRunId === hydration.interruptedRunId && candidate.generation === hydration.generation && responseSchemaHash(interrupt) === candidate.responseSchemaHash;
366
+ const structurallyCorrelated = candidate !== void 0 && candidate.interruptId === interrupt.id && candidate.interruptedRunId === hydration.interruptedRunId && candidate.generation === hydration.generation && candidate.responseSchemaHash === (responseSchemaHash(interrupt) ?? candidate.responseSchemaHash);
367
+ if (candidate !== void 0 && hasFirstPartyGenericMarker(interrupt) && (!structurallyCorrelated || candidate.kind !== "generic" || candidate.definitionId === void 0 || candidate.key === void 0 || candidate.batchIndex === void 0)) return {
334
368
  descriptor: interrupt,
335
- binding: void 0,
336
- kind: "unbound",
337
- status: "pending",
369
+ binding: genericBinding(interrupt, hydration, candidate),
370
+ kind: "generic",
371
+ status: "error",
338
372
  canResolve: false,
373
+ resumable: false,
374
+ error: this.itemError(interrupt.id, "stale", "The interrupt binding does not match this interrupted run."),
339
375
  validationGeneration: 0
340
376
  };
341
- const correlated = candidate !== void 0 && candidate.interruptId === interrupt.id && candidate.interruptedRunId === hydration.interruptedRunId && candidate.generation === hydration.generation && candidate.responseSchemaHash === (responseSchemaHash(interrupt) ?? candidate.responseSchemaHash);
342
- if (correlated && candidate.kind === "tool-approval") {
377
+ if (structurallyCorrelated && candidate.kind === "tool-approval") {
343
378
  const tool = this.tools?.find((configured) => configured.name === candidate.toolName);
344
379
  if (tool?.needsApproval === true && interrupt.toolCallId === candidate.toolCallId) try {
345
380
  const approval = normalizeApprovalSchema(tool.approvalSchema, tool.inputSchema);
@@ -349,12 +384,13 @@ var InterruptManager = class {
349
384
  kind: "tool-approval",
350
385
  status: "pending",
351
386
  canResolve: true,
387
+ resumable: true,
352
388
  tool,
353
389
  validationGeneration: 0
354
390
  };
355
391
  } catch {}
356
392
  }
357
- if (correlated && candidate.kind === "client-tool-execution") {
393
+ if (structurallyCorrelated && candidate.kind === "client-tool-execution") {
358
394
  const tool = this.tools?.find((configured) => configured.name === candidate.toolName);
359
395
  if (tool !== void 0 && interrupt.toolCallId === candidate.toolCallId && hashSchemaInput(tool.outputSchema) === candidate.outputSchemaHash) return {
360
396
  descriptor: interrupt,
@@ -362,23 +398,53 @@ var InterruptManager = class {
362
398
  kind: "client-tool-execution",
363
399
  status: "pending",
364
400
  canResolve: true,
401
+ resumable: true,
365
402
  tool,
366
403
  validationGeneration: 0
367
404
  };
368
405
  }
406
+ if (candidate !== void 0 && candidate.kind === "generic" && candidate.definitionId !== void 0 && candidate.key !== void 0 && candidate.batchIndex !== void 0 && candidate.key.length > 0 && firstPartyIndexes.get(candidate.batchIndex) !== 1) return {
407
+ descriptor: interrupt,
408
+ binding: cloneAndDeepFreezeJson(candidate),
409
+ kind: "generic",
410
+ status: "error",
411
+ canResolve: false,
412
+ resumable: false,
413
+ error: this.itemError(interrupt.id, "stale", "Generic interrupt batch contains a duplicate batchIndex."),
414
+ validationGeneration: 0
415
+ };
416
+ if (correlated && candidate.kind === "generic" && candidate.definitionId !== void 0 && candidate.key !== void 0 && candidate.batchIndex !== void 0 && candidate.key.length > 0 && firstPartyIndexes.get(candidate.batchIndex) === 1) {
417
+ const definition = this.interruptDefinitions.get(candidate.definitionId);
418
+ if (definition !== void 0 && definitionSchemaHash(definition.responseSchema) === candidate.responseSchemaHash && (definition.payloadSchema === void 0 ? candidate.payloadSchemaHash === void 0 : candidate.payloadSchemaHash === definitionSchemaHash(definition.payloadSchema))) {
419
+ const rawPayload = getInterruptPayload(interrupt);
420
+ return {
421
+ descriptor: interrupt,
422
+ binding: cloneAndDeepFreezeJson(candidate),
423
+ definition,
424
+ kind: "generic",
425
+ status: "pending",
426
+ canResolve: true,
427
+ resumable: true,
428
+ ...rawPayload === void 0 ? {} : { payload: cloneAndDeepFreezeJson(rawPayload) },
429
+ validationGeneration: 0
430
+ };
431
+ }
432
+ }
433
+ const resumable = legacyResumable || candidate !== void 0 && candidate.interruptId === interrupt.id && candidate.interruptedRunId === hydration.interruptedRunId && candidate.generation === hydration.generation && (candidate.kind !== "generic" || responseSchemaHash(interrupt) === void 0 || candidate.responseSchemaHash === responseSchemaHash(interrupt));
369
434
  return {
370
435
  descriptor: interrupt,
371
436
  binding: genericBinding(interrupt, hydration, candidate),
372
437
  kind: "generic",
373
438
  status: "pending",
374
- canResolve: true,
439
+ canResolve: resumable,
440
+ resumable,
375
441
  validationGeneration: 0
376
442
  };
377
443
  }
378
444
  buildSnapshot(transaction) {
379
445
  const hydration = this.requireHydration();
380
446
  const next = this.items.filter((item) => item.kind !== "client-tool-execution" && item.status !== "submitting").map((item) => {
381
- const base = baseSnapshot(item, hydration, () => this.cancelItem(item.descriptor.id, transaction), () => this.clearItem(item.descriptor.id, transaction));
447
+ const base = baseSnapshot(item, hydration);
382
448
  if (item.kind === "unbound" || item.binding === void 0) {
383
449
  const snapshot = {
384
450
  ...base,
@@ -396,6 +462,8 @@ var InterruptManager = class {
396
462
  toolName: item.binding.toolName,
397
463
  toolCallId: item.binding.toolCallId,
398
464
  originalArgs: cloneAndDeepFreezeJson(item.binding.originalArgs),
465
+ cancel: () => this.cancelItem(item.descriptor.id, transaction),
466
+ clearResolution: () => this.clearItem(item.descriptor.id, transaction),
399
467
  resolveInterrupt: (approved, options) => {
400
468
  const details = isUnknownObject(options) ? options : void 0;
401
469
  this.resolveItem(item.descriptor.id, {
@@ -413,19 +481,35 @@ var InterruptManager = class {
413
481
  interruptId: item.descriptor.id,
414
482
  interruptedRunId: hydration.interruptedRunId,
415
483
  generation: hydration.generation,
416
- responseSchemaHash: typeof item.binding.responseSchemaHash === "string" ? item.binding.responseSchemaHash : "none"
484
+ ...typeof item.binding.responseSchemaHash === "string" ? { responseSchemaHash: item.binding.responseSchemaHash } : {}
417
485
  });
486
+ if (item.definition !== void 0 && item.binding.kind === "generic") {
487
+ const snapshot = {
488
+ ...base,
489
+ kind: "generic",
490
+ definitionId: item.definition.id,
491
+ key: item.binding.key ?? "",
492
+ payload: item.payload,
493
+ binding: boundGeneric,
494
+ cancel: () => this.cancelItem(item.descriptor.id, transaction),
495
+ clearResolution: () => this.clearItem(item.descriptor.id, transaction),
496
+ resolveInterrupt: (response) => this.resolveItem(item.descriptor.id, response, transaction)
497
+ };
498
+ return Object.freeze(snapshot);
499
+ }
418
500
  const snapshot = {
419
501
  ...base,
420
502
  kind: "generic",
421
503
  binding: boundGeneric,
504
+ cancel: () => this.cancelItem(item.descriptor.id, transaction),
505
+ clearResolution: () => this.clearItem(item.descriptor.id, transaction),
422
506
  resolveInterrupt: (payload) => this.resolveItem(item.descriptor.id, payload, transaction)
423
507
  };
424
508
  return Object.freeze(snapshot);
425
509
  });
426
510
  return Object.freeze(next);
427
511
  }
428
- publish() {
512
+ publish(source = "live") {
429
513
  if (!this.hydration) {
430
514
  this.snapshot = Object.freeze([]);
431
515
  this.state = Object.freeze({
@@ -434,7 +518,7 @@ var InterruptManager = class {
434
518
  interruptErrors: this.rootErrors,
435
519
  resuming: this.resuming
436
520
  });
437
- this.options.onChange?.();
521
+ this.options.onChange?.(source);
438
522
  return;
439
523
  }
440
524
  this.snapshot = this.buildSnapshot();
@@ -444,7 +528,7 @@ var InterruptManager = class {
444
528
  interruptErrors: this.rootErrors,
445
529
  resuming: this.resuming
446
530
  });
447
- this.options.onChange?.();
531
+ this.options.onChange?.(source);
448
532
  }
449
533
  resolveItem(interruptId, payload, transaction) {
450
534
  this.assertItemMutable(transaction);
@@ -481,10 +565,10 @@ var InterruptManager = class {
481
565
  const item = this.findItem(interruptId);
482
566
  this.invalidateRetry();
483
567
  item.validationGeneration++;
484
- item.resolution = Object.freeze({
568
+ item.resolution = Object.freeze(resolutionWithContinuation(item, {
485
569
  interruptId,
486
570
  status: "cancelled"
487
- });
571
+ }));
488
572
  item.status = "staged";
489
573
  item.error = void 0;
490
574
  if (!transaction) {
@@ -503,7 +587,8 @@ var InterruptManager = class {
503
587
  if (!transaction) this.publish();
504
588
  }
505
589
  maybeSubmit() {
506
- const ours = this.items.filter((item) => item.kind !== "unbound");
590
+ const hasGeneric = this.items.some((item) => item.kind === "generic");
591
+ const ours = this.items.filter((item) => isClientOwnedInterrupt(item) && !(hasGeneric && item.kind === "client-tool-execution"));
507
592
  if (ours.length === 0 || ours.some((item) => item.resolution === void 0 || item.status !== "staged")) return;
508
593
  const hydration = this.requireHydration();
509
594
  const canonical = canonicalizeInterruptResolutions(ours.map((item) => item.resolution).filter((item) => item !== void 0));
@@ -536,11 +621,11 @@ var InterruptManager = class {
536
621
  if (!transaction) this.publish();
537
622
  return;
538
623
  }
539
- item.resolution = cloneAndDeepFreezeJson({
624
+ item.resolution = cloneAndDeepFreezeJson(resolutionWithContinuation(item, {
540
625
  interruptId: item.descriptor.id,
541
626
  status: "resolved",
542
627
  payload: result.payload
543
- });
628
+ }));
544
629
  item.status = "staged";
545
630
  item.error = void 0;
546
631
  if (!transaction) {
@@ -549,7 +634,15 @@ var InterruptManager = class {
549
634
  }
550
635
  }
551
636
  validateCandidate(item, payload) {
552
- if (item.kind === "generic") return validateWithSchema(item.descriptor.responseSchema, payload, "invalid-payload");
637
+ if (item.kind === "generic") {
638
+ const validation = validateWithSchema(item.definition?.responseSchema ?? item.descriptor.responseSchema, payload, "invalid-payload");
639
+ if (item.definition === void 0) return validation;
640
+ const preserveInput = (result) => "valid" in result ? {
641
+ valid: true,
642
+ payload
643
+ } : result;
644
+ return isPromiseLike(validation) ? Promise.resolve(validation).then(preserveInput) : preserveInput(validation);
645
+ }
553
646
  if (item.kind === "client-tool-execution") return validateWithSchema(item.tool?.outputSchema, payload, "invalid-tool-output");
554
647
  return this.validateApprovalCandidate(item, payload);
555
648
  }
@@ -608,7 +701,7 @@ var InterruptManager = class {
608
701
  return item.kind === "client-tool-execution" ? "invalid-tool-output" : "invalid-payload";
609
702
  }
610
703
  resolveBooleanBulk(approved) {
611
- const resolvable = this.items.filter((item) => item.kind !== "client-tool-execution");
704
+ const resolvable = this.items.filter((item) => isClientOwnedInterrupt(item) && item.kind !== "client-tool-execution");
612
705
  if (!resolvable.every((item) => item.kind === "tool-approval" && this.approvalBranchSchema(item.tool, approved) === void 0) || resolvable.length === 0) {
613
706
  this.addRootError("unsupported-bulk-operation", "Boolean bulk resolution requires payloadless tool approvals.", false);
614
707
  return;
@@ -636,7 +729,7 @@ var InterruptManager = class {
636
729
  }));
637
730
  const token = { active: true };
638
731
  this.activeTransaction = token;
639
- const stable = this.buildSnapshot(token);
732
+ const stable = this.buildSnapshot(token).filter(isRootResolvableInterrupt);
640
733
  let failure;
641
734
  try {
642
735
  for (const interrupt of stable) {
@@ -649,7 +742,7 @@ var InterruptManager = class {
649
742
  break;
650
743
  }
651
744
  }
652
- if (failure === void 0 && this.items.some((item) => item.kind !== "client-tool-execution" && (item.resolution === void 0 || item.status !== "staged"))) failure = {
745
+ if (failure === void 0 && this.items.some((item) => isClientOwnedInterrupt(item) && item.kind !== "client-tool-execution" && (item.resolution === void 0 || item.status !== "staged"))) failure = {
653
746
  code: "incomplete-batch",
654
747
  message: "Interrupt transaction did not resolve every item."
655
748
  };
@@ -696,7 +789,7 @@ var InterruptManager = class {
696
789
  submitBatch(submission) {
697
790
  this.resuming = true;
698
791
  this.retrySubmission = void 0;
699
- for (const item of this.items) item.status = "submitting";
792
+ for (const item of this.items) if (isClientOwnedInterrupt(item)) item.status = "submitting";
700
793
  this.publish();
701
794
  this.performSubmission(submission);
702
795
  }
@@ -716,7 +809,7 @@ var InterruptManager = class {
716
809
  const message = error instanceof Error ? error.message : String(error);
717
810
  this.addRootError("transport", message, true, "transport");
718
811
  this.retrySubmission = submission;
719
- for (const item of this.items) item.status = "error";
812
+ for (const item of this.items) if (isClientOwnedInterrupt(item)) item.status = "error";
720
813
  return;
721
814
  }
722
815
  const correlatedErrors = errors.filter((submissionError) => submissionErrorMatchesActiveBatch(submissionError, submission));
@@ -728,7 +821,7 @@ var InterruptManager = class {
728
821
  if (submissionError.code === "stale" || submissionError.code === "expired" || submissionError.code === "conflict") nonRetryable = true;
729
822
  retryable ||= submissionError.retryable;
730
823
  if (submissionError.scope === "item") {
731
- const item = this.items.find((candidate) => candidate.descriptor.id === submissionError.interruptId);
824
+ const item = this.items.find((candidate) => isClientOwnedInterrupt(candidate) && candidate.descriptor.id === submissionError.interruptId);
732
825
  if (item) {
733
826
  item.status = "error";
734
827
  item.error = cloneAndDeepFreezeJson(submissionError);
@@ -738,7 +831,7 @@ var InterruptManager = class {
738
831
  const mergedBatchErrors = mergeSubmissionBatchErrors(this.rootErrors, this.submissionRootErrors, batchErrors);
739
832
  this.rootErrors = mergedBatchErrors.rootErrors;
740
833
  this.submissionRootErrors = mergedBatchErrors.submissionRootErrors;
741
- for (const item of this.items) if (item.status === "submitting") item.status = "error";
834
+ for (const item of this.items) if (isClientOwnedInterrupt(item) && item.status === "submitting") item.status = "error";
742
835
  this.retrySubmission = retryable && !nonRetryable ? submission : void 0;
743
836
  }
744
837
  addRootError(code, message, retryable, source = "client") {
@@ -749,7 +842,7 @@ var InterruptManager = class {
749
842
  message,
750
843
  source,
751
844
  retryable,
752
- interruptIds: Object.freeze(this.items.map((item) => item.descriptor.id)),
845
+ interruptIds: Object.freeze(this.items.filter(isClientOwnedInterrupt).map((item) => item.descriptor.id)),
753
846
  threadId: hydration.threadId,
754
847
  interruptedRunId: hydration.interruptedRunId,
755
848
  generation: hydration.generation