@assistant-ui/eve 0.0.21 → 0.0.22

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.
@@ -1,6 +1,7 @@
1
1
  import { type AppendMessage, type RespondToToolApprovalOptions, type ThreadMessage } from "@assistant-ui/core";
2
2
  import type { EveAuthorizationOutcome, EveAuthorizationPart, EveMessage, EveMessageData, EveMessageInputRequest } from "eve/react";
3
3
  import type { InputResponse } from "eve/client";
4
+ import type { MessageStreamEvent } from "eve/client";
4
5
  export type ConvertEveMessagesOptions = {
5
6
  /**
6
7
  * Marks the last assistant message as running while Eve is submitting or
@@ -14,6 +15,12 @@ export type ConvertEveMessagesOptions = {
14
15
  */
15
16
  readonly error?: unknown;
16
17
  readonly getCreatedAt?: ((message: EveMessage) => Date) | undefined;
18
+ /**
19
+ * The session's stream events. A `turn.failed` or `turn.cancelled` event
20
+ * marks its turn's assistant message as incomplete, with a failure's code
21
+ * and message as its error.
22
+ */
23
+ readonly events?: readonly MessageStreamEvent[] | undefined;
17
24
  };
18
25
  /**
19
26
  * Payload of the `authorization` data part the Eve runtime emits for a
@@ -42,6 +49,16 @@ export type EveAuthorizationData = {
42
49
  * Converts a single Eve message into an assistant-ui thread message.
43
50
  */
44
51
  export declare const convertEveMessage: (message: EveMessage, index: number, messages: readonly EveMessage[], options?: ConvertEveMessagesOptions) => ThreadMessage;
52
+ export type InterruptedTurnEventCache = {
53
+ lastEvents: readonly MessageStreamEvent[];
54
+ interruptions: readonly MessageStreamEvent[];
55
+ };
56
+ /**
57
+ * Collects the `turn.failed` and `turn.cancelled` events of an append-only
58
+ * event log, scanning only the events appended since the cached call. The
59
+ * returned array keeps its identity until a new interruption arrives.
60
+ */
61
+ export declare const collectInterruptedTurnEvents: (events: readonly MessageStreamEvent[], cache: InterruptedTurnEventCache) => readonly MessageStreamEvent[];
45
62
  /**
46
63
  * Converts the full Eve message data object into assistant-ui thread messages.
47
64
  */
@@ -1 +1 @@
1
- {"version":3,"file":"convertEveMessages.d.ts","sourceRoot":"","sources":["../src/convertEveMessages.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,aAAa,EAOlB,KAAK,4BAA4B,EAEjC,KAAK,aAAa,EAMnB,MAAM,oBAAoB,CAAC;AAK5B,OAAO,KAAK,EACV,uBAAuB,EACvB,oBAAoB,EAEpB,UAAU,EACV,cAAc,EACd,sBAAsB,EAEvB,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAqBhD,MAAM,MAAM,yBAAyB,GAAG;IACtC;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACzC;;OAEG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,UAAU,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;CACrE,CAAC;AA8OF;;;;;;;;;;GAUG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,KAAK,EAAE,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,uBAAuB,CAAC;IAC3C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAyIF;;GAEG;AACH,eAAO,MAAM,iBAAiB,GAC5B,SAAS,UAAU,EACnB,OAAO,MAAM,EACb,UAAU,SAAS,UAAU,EAAE,EAC/B,UAAS,yBAA8B,KACtC,aAkCF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAC7B,MAAM,cAAc,EACpB,UAAS,yBAA8B,KACtC,aAAa,EAGb,CAAC;AAEJ;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GACzB,MAAM,GACN,CACI;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAChD;IACE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B,CACJ,EAAE,CAAC;AAER;;;GAGG;AACH,eAAO,MAAM,oBAAoB,GAC/B,SAAS,aAAa,KACrB,iBAiEF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mBAAmB,GAC9B,MAAM,cAAc,EACpB,YAAY,MAAM,KACjB,sBAAsB,GAAG,SAS3B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,kBAAkB,GAC7B,UAAU,4BAA4B,EACtC,eAAe,sBAAsB,KACpC,aAyDF,CAAC"}
1
+ {"version":3,"file":"convertEveMessages.d.ts","sourceRoot":"","sources":["../src/convertEveMessages.ts"],"names":[],"mappings":"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"}
@@ -27,8 +27,10 @@ const stringifyArgs = (value) => {
27
27
  return "";
28
28
  }
29
29
  };
30
- const toMessageStatus = (message, index, messages, options) => {
30
+ const toMessageStatus = (message, index, messages, options, interruptedTurns) => {
31
31
  if (message.role !== "assistant") return USER_FALLBACK_STATUS;
32
+ const interrupted = message.metadata?.turnId === void 0 ? void 0 : interruptedTurns.get(message.metadata.turnId);
33
+ if (interrupted) return interrupted;
32
34
  const isLast = index === messages.length - 1;
33
35
  const hasPendingApproval = message.parts.some((part) => part.type === "dynamic-tool" && part.state === "approval-requested");
34
36
  const hasPendingAuthorization = message.metadata?.status === "streaming" && (!isLast || options.error === void 0) && message.parts.some((part) => part.type === "authorization" && part.state === "required");
@@ -240,7 +242,8 @@ const toUserAttachments = (parts) => {
240
242
  /**
241
243
  * Converts a single Eve message into an assistant-ui thread message.
242
244
  */
243
- const convertEveMessage = (message, index, messages, options = {}) => {
245
+ const convertEveMessage = (message, index, messages, options = {}) => convertEveMessageWithInterruptions(message, index, messages, options, getInterruptedTurns(options.events));
246
+ const convertEveMessageWithInterruptions = (message, index, messages, options, interruptedTurns) => {
244
247
  const createdAt = options.getCreatedAt?.(message) ?? /* @__PURE__ */ new Date();
245
248
  const metadata = {
246
249
  ...message.metadata?.optimistic && { isOptimistic: true },
@@ -260,12 +263,47 @@ const convertEveMessage = (message, index, messages, options = {}) => {
260
263
  content: message.parts.map(convertAssistantPart).filter((part) => part !== null),
261
264
  metadata
262
265
  };
263
- return fromThreadMessageLike(like, message.id, toMessageStatus(message, index, messages, options));
266
+ return fromThreadMessageLike(like, message.id, toMessageStatus(message, index, messages, options, interruptedTurns));
267
+ };
268
+ const isTurnInterruption = (event) => event.type === "turn.failed" || event.type === "turn.cancelled";
269
+ /**
270
+ * Collects the `turn.failed` and `turn.cancelled` events of an append-only
271
+ * event log, scanning only the events appended since the cached call. The
272
+ * returned array keeps its identity until a new interruption arrives.
273
+ */
274
+ const collectInterruptedTurnEvents = (events, cache) => {
275
+ if (events === cache.lastEvents) return cache.interruptions;
276
+ const scanned = cache.lastEvents;
277
+ const resumesScan = scanned.length === 0 || events[scanned.length - 1] === scanned[scanned.length - 1];
278
+ let interruptions = resumesScan ? cache.interruptions : [];
279
+ for (let i = resumesScan ? scanned.length : 0; i < events.length; i++) {
280
+ const event = events[i];
281
+ if (isTurnInterruption(event)) interruptions = [...interruptions, event];
282
+ }
283
+ cache.lastEvents = events;
284
+ cache.interruptions = interruptions;
285
+ return interruptions;
286
+ };
287
+ const getInterruptedTurns = (events) => {
288
+ const interrupted = /* @__PURE__ */ new Map();
289
+ for (const event of events ?? []) if (event.type === "turn.failed") interrupted.set(event.data.turnId, {
290
+ type: "incomplete",
291
+ reason: "error",
292
+ error: {
293
+ code: event.data.code,
294
+ message: event.data.message
295
+ }
296
+ });
297
+ else if (event.type === "turn.cancelled") interrupted.set(event.data.turnId, ASSISTANT_CANCELLED_STATUS);
298
+ return interrupted;
264
299
  };
265
300
  /**
266
301
  * Converts the full Eve message data object into assistant-ui thread messages.
267
302
  */
268
- const convertEveMessages = (data, options = {}) => data.messages.map((message, index, messages) => convertEveMessage(message, index, messages, options));
303
+ const convertEveMessages = (data, options = {}) => {
304
+ const interruptedTurns = getInterruptedTurns(options.events);
305
+ return data.messages.map((message, index, messages) => convertEveMessageWithInterruptions(message, index, messages, options, interruptedTurns));
306
+ };
269
307
  /**
270
308
  * Converts an assistant-ui append message into the message payload accepted by
271
309
  * Eve's `send` API.
@@ -368,6 +406,6 @@ const toEveInputResponse = (response, inputRequest) => {
368
406
  throw new Error(`Eve input request "${requestId}" (${inputRequest.prompt}) was not answered by this response; ${options?.length ? `respond with one of: ${options.map((option) => option.id).join(", ")}` : !acceptsText ? "the request declares no options to respond with" : response.approved === false ? "a refusal carries no answer for a free-form request; resend with `approved` unset or true and the answer in the response text" : "pass the answer as the response text, because the request takes a free-form answer"}`);
369
407
  };
370
408
  //#endregion
371
- export { convertEveMessage, convertEveMessages, findEveInputRequest, getEveMessageContent, toEveInputResponse };
409
+ export { collectInterruptedTurnEvents, convertEveMessage, convertEveMessages, findEveInputRequest, getEveMessageContent, toEveInputResponse };
372
410
 
373
411
  //# sourceMappingURL=convertEveMessages.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"convertEveMessages.js","names":["_exhaustiveCheck"],"sources":["../src/convertEveMessages.ts"],"sourcesContent":["import {\n fromThreadMessageLike,\n toAssistantError,\n type AppendMessage,\n type CompleteAttachment,\n type DataMessagePart,\n type FileMessagePart,\n type MessagePartStreamStatus,\n type MessageStatus,\n type PartProviderMetadata,\n type RespondToToolApprovalOptions,\n type ThreadAssistantMessagePart,\n type ThreadMessage,\n type ThreadMessageLike,\n type ThreadUserMessagePart,\n type ToolApprovalDisplay,\n type ToolApprovalOption,\n type ToolCallMessagePart,\n} from \"@assistant-ui/core\";\nimport {\n httpUrlPattern,\n resolveFilePartSource,\n} from \"@assistant-ui/core/internal\";\nimport type {\n EveAuthorizationOutcome,\n EveAuthorizationPart,\n EveDynamicToolPart,\n EveMessage,\n EveMessageData,\n EveMessageInputRequest,\n EveMessagePart,\n} from \"eve/react\";\nimport type { InputResponse } from \"eve/client\";\n\nconst ASSISTANT_COMPLETE_STATUS = {\n type: \"complete\",\n reason: \"stop\",\n} satisfies MessageStatus;\n\nconst ASSISTANT_RUNNING_STATUS = {\n type: \"running\",\n} satisfies MessageStatus;\n\nconst ASSISTANT_CANCELLED_STATUS = {\n type: \"incomplete\",\n reason: \"cancelled\",\n} satisfies MessageStatus;\n\nconst USER_FALLBACK_STATUS = {\n type: \"complete\",\n reason: \"unknown\",\n} satisfies MessageStatus;\n\nexport type ConvertEveMessagesOptions = {\n /**\n * Marks the last assistant message as running while Eve is submitting or\n * streaming. When omitted, a message carrying Eve's `\"streaming\"` marker is\n * treated as running; pass `false` to settle interrupted messages to a\n * terminal status.\n */\n readonly isRunning?: boolean | undefined;\n /**\n * The Eve session error, mapped onto the assistant message it interrupted.\n */\n readonly error?: unknown;\n readonly getCreatedAt?: ((message: EveMessage) => Date) | undefined;\n};\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst toJsonObject = (value: unknown): ToolCallMessagePart[\"args\"] => {\n if (isRecord(value)) return value as ToolCallMessagePart[\"args\"];\n if (value === undefined) return {};\n return { value } as ToolCallMessagePart[\"args\"];\n};\n\nconst stringifyArgs = (value: unknown): string => {\n try {\n return JSON.stringify(value ?? {});\n } catch {\n return \"\";\n }\n};\n\nconst toMessageStatus = (\n message: EveMessage,\n index: number,\n messages: readonly EveMessage[],\n options: ConvertEveMessagesOptions,\n): MessageStatus => {\n if (message.role !== \"assistant\") return USER_FALLBACK_STATUS;\n\n const isLast = index === messages.length - 1;\n const hasPendingApproval = message.parts.some(\n (part) =>\n part.type === \"dynamic-tool\" && part.state === \"approval-requested\",\n );\n // Scoped to the stale-marker case so a terminalized or errored turn keeps its\n // own status; the hold only claims the fall-through that reads as cancelled.\n const hasPendingAuthorization =\n message.metadata?.status === \"streaming\" &&\n (!isLast || options.error === undefined) &&\n message.parts.some(\n (part) => part.type === \"authorization\" && part.state === \"required\",\n );\n\n if (hasPendingApproval) {\n return { type: \"requires-action\", reason: \"tool-calls\" };\n }\n\n // A connector authorization is answered outside the thread, so the turn is\n // held on external input rather than on a tool call awaiting a result.\n if (hasPendingAuthorization) {\n return { type: \"requires-action\", reason: \"interrupt\" };\n }\n\n if (message.metadata?.status === \"failed\") {\n return { type: \"incomplete\", reason: \"error\" };\n }\n\n if (isLast && options.isRunning === true) {\n return ASSISTANT_RUNNING_STATUS;\n }\n\n // Eve's default reducer never terminalizes the \"streaming\" marker on\n // cancellation or turn/session failure, so liveness comes from isRunning and\n // a leftover marker means the turn was interrupted.\n if (message.metadata?.status === \"streaming\") {\n if (options.isRunning === undefined) {\n return ASSISTANT_RUNNING_STATUS;\n }\n if (isLast && options.error !== undefined) {\n return {\n type: \"incomplete\",\n reason: \"error\",\n error: toAssistantError(options.error),\n };\n }\n return ASSISTANT_CANCELLED_STATUS;\n }\n\n return ASSISTANT_COMPLETE_STATUS;\n};\n\nconst toolApprovalOptionsFromInputRequest = (\n inputRequest: EveMessageInputRequest | undefined,\n): readonly ToolApprovalOption[] | undefined => {\n const options = inputRequest?.options;\n if (!options || options.length === 0) return undefined;\n\n return options.map((option) => ({\n id: option.id,\n kind:\n option.id === \"approve\"\n ? \"allow-once\"\n : option.id === \"cancel\"\n ? \"reject-once\"\n : `_${option.id}`,\n ...(option.label && { label: option.label }),\n ...(option.description && { description: option.description }),\n }));\n};\n\n/**\n * Eve resolves a request the moment any response for it arrives, so the answer\n * shapes it accepts decide both what the renderer may offer and what\n * {@link toEveInputResponse} may submit. A request with no options and no\n * `display` takes a typed answer, which is why the absence of both is not the\n * same as a confirmation.\n */\nconst isDroppedOptionApproval = (inputRequest: EveMessageInputRequest) =>\n inputRequest.kind === \"tool-approval\" && !inputRequest.options?.length;\n\nconst acceptsFreeformAnswer = (inputRequest: EveMessageInputRequest): boolean =>\n inputRequest.display !== \"confirmation\" &&\n !isDroppedOptionApproval(inputRequest) &&\n (inputRequest.display === \"text\" ||\n inputRequest.allowFreeform === true ||\n !inputRequest.options?.length);\n\nconst toolApprovalDisplay = (\n inputRequest: EveMessageInputRequest,\n): ToolApprovalDisplay => {\n // The mapper answers this shape through its approve/cancel branch whatever\n // the request declares, so the projection has to agree: presenting it as a\n // question would offer an answer the mapper turns into a decision.\n if (isDroppedOptionApproval(inputRequest)) return \"decision\";\n\n switch (inputRequest.display) {\n case \"confirmation\":\n return \"decision\";\n case \"select\":\n return \"select\";\n case \"text\":\n return \"text\";\n default:\n if (inputRequest.kind === \"tool-approval\") return \"decision\";\n return inputRequest.options?.length ? \"select\" : \"text\";\n }\n};\n\nconst toApproval = (\n part: EveDynamicToolPart,\n): ToolCallMessagePart[\"approval\"] | undefined => {\n if (\n part.state !== \"approval-requested\" &&\n part.state !== \"approval-responded\" &&\n part.state !== \"output-available\" &&\n part.state !== \"output-error\" &&\n part.state !== \"output-denied\"\n ) {\n return undefined;\n }\n\n const approval = part.approval;\n if (!approval) return undefined;\n const inputRequest = part.toolMetadata?.eve?.inputRequest;\n const options = toolApprovalOptionsFromInputRequest(inputRequest);\n\n return {\n id: approval.id,\n ...(inputRequest && {\n prompt: inputRequest.prompt,\n display: toolApprovalDisplay(inputRequest),\n ...(acceptsFreeformAnswer(inputRequest) && { allowFreeform: true }),\n }),\n ...(approval.approved !== undefined && { approved: approval.approved }),\n ...(approval.reason && { reason: approval.reason }),\n ...(approval.isAutomatic !== undefined && {\n isAutomatic: approval.isAutomatic,\n }),\n ...(options && { options }),\n };\n};\n\nconst toJsonSafeInputRequest = (\n inputRequest: EveMessageInputRequest,\n): PartProviderMetadata[string] => ({\n requestId: inputRequest.requestId,\n prompt: inputRequest.prompt,\n kind: inputRequest.kind,\n ...(inputRequest.display !== undefined && { display: inputRequest.display }),\n ...(inputRequest.allowFreeform !== undefined && {\n allowFreeform: inputRequest.allowFreeform,\n }),\n ...(inputRequest.options && {\n options: inputRequest.options.map((option) => ({\n id: option.id,\n label: option.label,\n ...(option.description !== undefined && {\n description: option.description,\n }),\n ...(option.style !== undefined && { style: option.style }),\n })),\n }),\n});\n\nconst toToolProviderMetadata = (\n part: EveDynamicToolPart,\n): PartProviderMetadata | undefined => {\n const inputRequest = part.toolMetadata?.eve?.inputRequest;\n if (!inputRequest) return undefined;\n return { eve: { inputRequest: toJsonSafeInputRequest(inputRequest) } };\n};\n\nconst convertDynamicToolPart = (\n part: EveDynamicToolPart,\n): ToolCallMessagePart => {\n const approval = toApproval(part);\n const providerMetadata = toToolProviderMetadata(part);\n const toolCall: ToolCallMessagePart = {\n type: \"tool-call\",\n toolCallId: part.toolCallId,\n toolName: part.toolName,\n args: toJsonObject(part.input),\n argsText: stringifyArgs(part.input),\n ...(approval && { approval }),\n ...(providerMetadata && { providerMetadata }),\n };\n\n switch (part.state) {\n case \"output-available\":\n return { ...toolCall, result: part.output };\n\n case \"output-error\":\n return {\n ...toolCall,\n result: { error: part.errorText },\n isError: true,\n };\n\n case \"output-denied\":\n return {\n ...toolCall,\n result: { error: part.approval?.reason ?? \"Tool approval denied\" },\n isError: true,\n };\n\n default:\n return toolCall;\n }\n};\n\n/**\n * Payload of the `authorization` data part the Eve runtime emits for a\n * connector authorization challenge. `state` discriminates a pending challenge\n * from a settled one; every other field is present only when Eve projected it.\n * This converter drops a `url` whose scheme is not `http(s)`, so a renderer can\n * link to it without checking the scheme itself; Eve itself passes through\n * whatever the connector supplied. Eve's\n * `turnId` and `stepIndex` part identity is omitted: a renderer receives the\n * one part it renders, in Eve's own step order, so display never has to\n * re-identify a challenge among several.\n */\nexport type EveAuthorizationData = {\n readonly state: EveAuthorizationPart[\"state\"];\n readonly name: string;\n readonly displayName?: string;\n readonly description?: string;\n readonly url?: string;\n readonly userCode?: string;\n readonly instructions?: string;\n readonly expiresAt?: string;\n readonly outcome?: EveAuthorizationOutcome;\n readonly reason?: string;\n};\n\nconst convertAuthorizationPart = (\n part: EveAuthorizationPart,\n): DataMessagePart<EveAuthorizationData> => ({\n type: \"data\",\n name: \"authorization\",\n data: {\n state: part.state,\n name: part.name,\n ...(part.displayName !== undefined && { displayName: part.displayName }),\n ...(part.description !== undefined && { description: part.description }),\n ...(part.authorization?.url !== undefined &&\n httpUrlPattern.test(part.authorization.url) && {\n url: part.authorization.url,\n }),\n ...(part.authorization?.userCode !== undefined && {\n userCode: part.authorization.userCode,\n }),\n ...(part.authorization?.instructions !== undefined && {\n instructions: part.authorization.instructions,\n }),\n ...(part.authorization?.expiresAt !== undefined && {\n expiresAt: part.authorization.expiresAt,\n }),\n ...(part.outcome !== undefined && { outcome: part.outcome }),\n ...(part.reason !== undefined && { reason: part.reason }),\n },\n});\n\nconst convertFilePart = (\n part: Extract<EveMessagePart, { type: \"file\" }>,\n): FileMessagePart | null => {\n if (part.url === undefined) return null;\n return {\n type: \"file\",\n data: part.url,\n mimeType: part.mediaType ?? \"unknown/unknown\",\n ...(part.filename && { filename: part.filename }),\n ...(httpUrlPattern.test(part.url) && { sourceType: \"url\" as const }),\n };\n};\n\n// Eve's `state` is a run marker: the reducer settles a part to \"done\" only on\n// `reasoning.completed`, `message.completed`, or `turn.cancelled`.\n// `emitStreamContent` does not flush leftover reasoning on a tool-call, so a\n// non-last reasoning part stays `streaming` until the model stream ends and the\n// leftover flush runs. Mapping `streaming` to running would pin that block\n// open. Only `done` is trustworthy; an unsettled part falls back to core's\n// last-part rule.\nconst partStateToStatus = (\n state: \"done\" | \"streaming\" | undefined,\n): MessagePartStreamStatus | undefined =>\n state === \"done\" ? { type: \"complete\" } : undefined;\n\nconst convertAssistantPart = (\n part: EveMessagePart,\n): ThreadAssistantMessagePart | null => {\n switch (part.type) {\n case \"text\":\n case \"reasoning\": {\n const status = partStateToStatus(part.state);\n return {\n type: part.type,\n text: part.text,\n ...(status && { status }),\n };\n }\n\n case \"step-start\":\n return null;\n\n case \"dynamic-tool\":\n return convertDynamicToolPart(part);\n\n case \"authorization\":\n return convertAuthorizationPart(part);\n\n case \"file\":\n return convertFilePart(part);\n\n default:\n return null;\n }\n};\n\nconst convertUserPart = (\n part: EveMessagePart,\n): ThreadUserMessagePart | null => {\n switch (part.type) {\n case \"text\":\n return { type: \"text\", text: part.text };\n\n case \"file\":\n return convertFilePart(part);\n\n default:\n return null;\n }\n};\n\nconst toUserContent = (\n parts: readonly EveMessagePart[],\n): readonly ThreadUserMessagePart[] => {\n const content = parts.map(convertUserPart).filter((part) => part !== null);\n return content.length > 0 ? content : [{ type: \"text\", text: \"\" }];\n};\n\nconst toUserAttachments = (\n parts: readonly EveMessagePart[],\n): CompleteAttachment[] => {\n const attachments: CompleteAttachment[] = [];\n for (const part of parts) {\n if (part.type !== \"file\") continue;\n const file = convertFilePart(part);\n if (file === null) continue;\n const isImage = file.mimeType.startsWith(\"image/\");\n attachments.push({\n id: String(attachments.length),\n type: isImage ? \"image\" : \"file\",\n name: part.filename ?? \"file\",\n content: [\n isImage\n ? {\n type: \"image\",\n image: file.data,\n ...(part.filename && { filename: part.filename }),\n }\n : file,\n ],\n contentType: file.mimeType,\n status: { type: \"complete\" },\n });\n }\n return attachments;\n};\n\n/**\n * Converts a single Eve message into an assistant-ui thread message.\n */\nexport const convertEveMessage = (\n message: EveMessage,\n index: number,\n messages: readonly EveMessage[],\n options: ConvertEveMessagesOptions = {},\n): ThreadMessage => {\n const createdAt = options.getCreatedAt?.(message) ?? new Date();\n const metadata = {\n ...(message.metadata?.optimistic && { isOptimistic: true }),\n custom: {\n ...(message.metadata ?? {}),\n },\n };\n\n const like: ThreadMessageLike =\n message.role === \"user\"\n ? {\n role: \"user\",\n id: message.id,\n createdAt,\n content: toUserContent(message.parts),\n attachments: toUserAttachments(message.parts),\n metadata,\n }\n : {\n role: \"assistant\",\n id: message.id,\n createdAt,\n content: message.parts\n .map(convertAssistantPart)\n .filter((part) => part !== null),\n metadata,\n };\n\n return fromThreadMessageLike(\n like,\n message.id,\n toMessageStatus(message, index, messages, options),\n );\n};\n\n/**\n * Converts the full Eve message data object into assistant-ui thread messages.\n */\nexport const convertEveMessages = (\n data: EveMessageData,\n options: ConvertEveMessagesOptions = {},\n): ThreadMessage[] =>\n data.messages.map((message, index, messages) =>\n convertEveMessage(message, index, messages, options),\n );\n\n/**\n * Structural subset of the `string | UserContent` message content Eve's `send`\n * API accepts. The helpers declare the shapes they produce and leave\n * assignability to the send call site, checked against the installed version.\n */\nexport type EveMessageContent =\n | string\n | (\n | { readonly type: \"text\"; readonly text: string }\n | {\n readonly type: \"file\";\n readonly data: string;\n readonly mediaType: string;\n readonly filename?: string;\n }\n )[];\n\n/**\n * Converts an assistant-ui append message into the message payload accepted by\n * Eve's `send` API.\n */\nexport const getEveMessageContent = (\n message: AppendMessage,\n): EveMessageContent => {\n const content = [\n ...message.content,\n ...(message.attachments?.flatMap((attachment) => attachment.content) ?? []),\n ];\n\n const parts = content.flatMap((part) => {\n const type = part.type;\n switch (type) {\n case \"text\":\n return { type: \"text\" as const, text: part.text };\n\n case \"file\":\n return {\n type: \"file\" as const,\n data: part.data,\n mediaType: part.mimeType,\n ...(part.filename && { filename: part.filename }),\n };\n\n case \"image\":\n return {\n type: \"file\" as const,\n data: part.image,\n mediaType: \"image/*\",\n ...(part.filename && { filename: part.filename }),\n };\n\n case \"audio\": {\n // A data URL's own media type wins over `mediaType` downstream, so the\n // envelope is rebuilt from the typed format rather than forwarded.\n const mediaType = `audio/${part.audio.format}`;\n const data = part.audio.data;\n const source = resolveFilePartSource({ data, mimeType: mediaType });\n return {\n type: \"file\" as const,\n data:\n source.kind === \"url\"\n ? source.url\n : `data:${mediaType};base64,${source.data}`,\n mediaType,\n };\n }\n\n case \"data\":\n return [];\n\n default: {\n const _exhaustiveCheck:\n | \"generative-ui\"\n | \"reasoning\"\n | \"source\"\n | \"tool-call\" = type;\n throw new Error(\n `Unsupported eve append message part type: ${_exhaustiveCheck}`,\n );\n }\n }\n });\n\n if (parts.length === 1 && parts[0]?.type === \"text\") {\n return parts[0].text;\n }\n\n return parts;\n};\n\n/**\n * Finds the pending input request answered by the given approval id, so the\n * response mapping can honor the request's display mode.\n */\nexport const findEveInputRequest = (\n data: EveMessageData,\n approvalId: string,\n): EveMessageInputRequest | undefined => {\n for (const message of data.messages) {\n for (const part of message.parts) {\n if (part.type === \"dynamic-tool\" && part.approval?.id === approvalId) {\n return part.toolMetadata?.eve?.inputRequest;\n }\n }\n }\n return undefined;\n};\n\n/**\n * Converts an assistant-ui tool approval response into an Eve input response.\n *\n * When `inputRequest` is known, every returned response carries either an\n * option the request declares or a free-form answer. A literal option match\n * wins, then the `\"approve\"` / `\"cancel\"` option the response's boolean decision\n * names, then the response's `text` (or its `reason`, for a caller that\n * predates the first-class answer field) when the request takes a free-form\n * answer (`display: \"text\"`, `allowFreeform`, or no options at all, and never\n * `display: \"confirmation\"`) and the response is not a refusal.\n * A one-argument call, or a part with no request, still forwards a caller\n * `optionId` as it always has, including ids the request never declared.\n *\n * A response the known request cannot record as the answer it asked for throws\n * instead of being submitted, because eve resolves a request the moment any\n * response for it arrives, and an empty one is recorded as an answer with no\n * content. Not sending leaves the request pending, so the caller can retry or\n * the user can answer it as an ordinary message.\n *\n * With no request to map against — a one-argument call, or a part carrying no\n * `toolMetadata.eve.inputRequest` — the response maps as it always has, to the\n * literal `\"approve\"` / `\"cancel\"` option every eve approval declares. Guessing\n * a display mode is what this mapper stopped doing; an unknown request is not\n * a guess about one.\n */\nexport const toEveInputResponse = (\n response: RespondToToolApprovalOptions,\n inputRequest?: EveMessageInputRequest,\n): InputResponse => {\n const requestId = response.approvalId;\n const options = inputRequest?.options;\n const text = response.text ?? response.reason;\n\n if (response.optionId !== undefined) {\n if (\n !inputRequest ||\n options?.some((option) => option.id === response.optionId)\n ) {\n return {\n requestId,\n optionId: response.optionId,\n ...(text && { text }),\n };\n }\n throw new Error(\n `Eve input request \"${requestId}\" has no option with id \"${response.optionId}\"; respond with one of: ${\n options?.length\n ? options.map((option) => option.id).join(\", \")\n : \"(the request carries no options)\"\n }`,\n );\n }\n\n const decisionOptionId = response.approved ? \"approve\" : \"cancel\";\n // Eve's harness always declares `approve`/`cancel` on a `tool-approval`\n // request, so honoring the kind when the options were dropped keeps a\n // non-spec approval payload answerable rather than stuck. A tool-approval\n // that declares its own options still requires one of them.\n if (\n !inputRequest ||\n (inputRequest.kind === \"tool-approval\" && !options?.length) ||\n options?.some((option) => option.id === decisionOptionId)\n ) {\n return { requestId, optionId: decisionOptionId, ...(text && { text }) };\n }\n\n // Eve recognises an approval by its literal two-option `approve`/`cancel`\n // shape, so past this point the request is a question and the boolean\n // carries no decision eve would act on.\n const acceptsText = acceptsFreeformAnswer(inputRequest);\n if (acceptsText && text && response.approved !== false) {\n return { requestId, text };\n }\n\n throw new Error(\n `Eve input request \"${requestId}\" (${inputRequest.prompt}) was not answered by this response; ${\n options?.length\n ? `respond with one of: ${options.map((option) => option.id).join(\", \")}`\n : !acceptsText\n ? \"the request declares no options to respond with\"\n : response.approved === false\n ? \"a refusal carries no answer for a free-form request; resend with `approved` unset or true and the answer in the response text\"\n : \"pass the answer as the response text, because the request takes a free-form answer\"\n }`,\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"convertEveMessages.js","names":["_exhaustiveCheck"],"sources":["../src/convertEveMessages.ts"],"sourcesContent":["import {\n fromThreadMessageLike,\n toAssistantError,\n type AppendMessage,\n type CompleteAttachment,\n type DataMessagePart,\n type FileMessagePart,\n type MessagePartStreamStatus,\n type MessageStatus,\n type PartProviderMetadata,\n type RespondToToolApprovalOptions,\n type ThreadAssistantMessagePart,\n type ThreadMessage,\n type ThreadMessageLike,\n type ThreadUserMessagePart,\n type ToolApprovalDisplay,\n type ToolApprovalOption,\n type ToolCallMessagePart,\n} from \"@assistant-ui/core\";\nimport {\n httpUrlPattern,\n resolveFilePartSource,\n} from \"@assistant-ui/core/internal\";\nimport type {\n EveAuthorizationOutcome,\n EveAuthorizationPart,\n EveDynamicToolPart,\n EveMessage,\n EveMessageData,\n EveMessageInputRequest,\n EveMessagePart,\n} from \"eve/react\";\nimport type { InputResponse } from \"eve/client\";\nimport type { MessageStreamEvent } from \"eve/client\";\n\nconst ASSISTANT_COMPLETE_STATUS = {\n type: \"complete\",\n reason: \"stop\",\n} satisfies MessageStatus;\n\nconst ASSISTANT_RUNNING_STATUS = {\n type: \"running\",\n} satisfies MessageStatus;\n\nconst ASSISTANT_CANCELLED_STATUS = {\n type: \"incomplete\",\n reason: \"cancelled\",\n} satisfies MessageStatus;\n\nconst USER_FALLBACK_STATUS = {\n type: \"complete\",\n reason: \"unknown\",\n} satisfies MessageStatus;\n\nexport type ConvertEveMessagesOptions = {\n /**\n * Marks the last assistant message as running while Eve is submitting or\n * streaming. When omitted, a message carrying Eve's `\"streaming\"` marker is\n * treated as running; pass `false` to settle interrupted messages to a\n * terminal status.\n */\n readonly isRunning?: boolean | undefined;\n /**\n * The Eve session error, mapped onto the assistant message it interrupted.\n */\n readonly error?: unknown;\n readonly getCreatedAt?: ((message: EveMessage) => Date) | undefined;\n /**\n * The session's stream events. A `turn.failed` or `turn.cancelled` event\n * marks its turn's assistant message as incomplete, with a failure's code\n * and message as its error.\n */\n readonly events?: readonly MessageStreamEvent[] | undefined;\n};\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst toJsonObject = (value: unknown): ToolCallMessagePart[\"args\"] => {\n if (isRecord(value)) return value as ToolCallMessagePart[\"args\"];\n if (value === undefined) return {};\n return { value } as ToolCallMessagePart[\"args\"];\n};\n\nconst stringifyArgs = (value: unknown): string => {\n try {\n return JSON.stringify(value ?? {});\n } catch {\n return \"\";\n }\n};\n\nconst toMessageStatus = (\n message: EveMessage,\n index: number,\n messages: readonly EveMessage[],\n options: ConvertEveMessagesOptions,\n interruptedTurns: ReadonlyMap<string, MessageStatus>,\n): MessageStatus => {\n if (message.role !== \"assistant\") return USER_FALLBACK_STATUS;\n\n const interrupted =\n message.metadata?.turnId === undefined\n ? undefined\n : interruptedTurns.get(message.metadata.turnId);\n if (interrupted) return interrupted;\n\n const isLast = index === messages.length - 1;\n const hasPendingApproval = message.parts.some(\n (part) =>\n part.type === \"dynamic-tool\" && part.state === \"approval-requested\",\n );\n // Scoped to the stale-marker case so a terminalized or errored turn keeps its\n // own status; the hold only claims the fall-through that reads as cancelled.\n const hasPendingAuthorization =\n message.metadata?.status === \"streaming\" &&\n (!isLast || options.error === undefined) &&\n message.parts.some(\n (part) => part.type === \"authorization\" && part.state === \"required\",\n );\n\n if (hasPendingApproval) {\n return { type: \"requires-action\", reason: \"tool-calls\" };\n }\n\n // A connector authorization is answered outside the thread, so the turn is\n // held on external input rather than on a tool call awaiting a result.\n if (hasPendingAuthorization) {\n return { type: \"requires-action\", reason: \"interrupt\" };\n }\n\n if (message.metadata?.status === \"failed\") {\n return { type: \"incomplete\", reason: \"error\" };\n }\n\n if (isLast && options.isRunning === true) {\n return ASSISTANT_RUNNING_STATUS;\n }\n\n // Some eve versions leave the streaming marker after cancellation or failure.\n if (message.metadata?.status === \"streaming\") {\n if (options.isRunning === undefined) {\n return ASSISTANT_RUNNING_STATUS;\n }\n if (isLast && options.error !== undefined) {\n return {\n type: \"incomplete\",\n reason: \"error\",\n error: toAssistantError(options.error),\n };\n }\n return ASSISTANT_CANCELLED_STATUS;\n }\n\n return ASSISTANT_COMPLETE_STATUS;\n};\n\nconst toolApprovalOptionsFromInputRequest = (\n inputRequest: EveMessageInputRequest | undefined,\n): readonly ToolApprovalOption[] | undefined => {\n const options = inputRequest?.options;\n if (!options || options.length === 0) return undefined;\n\n return options.map((option) => ({\n id: option.id,\n kind:\n option.id === \"approve\"\n ? \"allow-once\"\n : option.id === \"cancel\"\n ? \"reject-once\"\n : `_${option.id}`,\n ...(option.label && { label: option.label }),\n ...(option.description && { description: option.description }),\n }));\n};\n\n/**\n * Eve resolves a request the moment any response for it arrives, so the answer\n * shapes it accepts decide both what the renderer may offer and what\n * {@link toEveInputResponse} may submit. A request with no options and no\n * `display` takes a typed answer, which is why the absence of both is not the\n * same as a confirmation.\n */\nconst isDroppedOptionApproval = (inputRequest: EveMessageInputRequest) =>\n inputRequest.kind === \"tool-approval\" && !inputRequest.options?.length;\n\nconst acceptsFreeformAnswer = (inputRequest: EveMessageInputRequest): boolean =>\n inputRequest.display !== \"confirmation\" &&\n !isDroppedOptionApproval(inputRequest) &&\n (inputRequest.display === \"text\" ||\n inputRequest.allowFreeform === true ||\n !inputRequest.options?.length);\n\nconst toolApprovalDisplay = (\n inputRequest: EveMessageInputRequest,\n): ToolApprovalDisplay => {\n // The mapper answers this shape through its approve/cancel branch whatever\n // the request declares, so the projection has to agree: presenting it as a\n // question would offer an answer the mapper turns into a decision.\n if (isDroppedOptionApproval(inputRequest)) return \"decision\";\n\n switch (inputRequest.display) {\n case \"confirmation\":\n return \"decision\";\n case \"select\":\n return \"select\";\n case \"text\":\n return \"text\";\n default:\n if (inputRequest.kind === \"tool-approval\") return \"decision\";\n return inputRequest.options?.length ? \"select\" : \"text\";\n }\n};\n\nconst toApproval = (\n part: EveDynamicToolPart,\n): ToolCallMessagePart[\"approval\"] | undefined => {\n if (\n part.state !== \"approval-requested\" &&\n part.state !== \"approval-responded\" &&\n part.state !== \"output-available\" &&\n part.state !== \"output-error\" &&\n part.state !== \"output-denied\"\n ) {\n return undefined;\n }\n\n const approval = part.approval;\n if (!approval) return undefined;\n const inputRequest = part.toolMetadata?.eve?.inputRequest;\n const options = toolApprovalOptionsFromInputRequest(inputRequest);\n\n return {\n id: approval.id,\n ...(inputRequest && {\n prompt: inputRequest.prompt,\n display: toolApprovalDisplay(inputRequest),\n ...(acceptsFreeformAnswer(inputRequest) && { allowFreeform: true }),\n }),\n ...(approval.approved !== undefined && { approved: approval.approved }),\n ...(approval.reason && { reason: approval.reason }),\n ...(approval.isAutomatic !== undefined && {\n isAutomatic: approval.isAutomatic,\n }),\n ...(options && { options }),\n };\n};\n\nconst toJsonSafeInputRequest = (\n inputRequest: EveMessageInputRequest,\n): PartProviderMetadata[string] => ({\n requestId: inputRequest.requestId,\n prompt: inputRequest.prompt,\n kind: inputRequest.kind,\n ...(inputRequest.display !== undefined && { display: inputRequest.display }),\n ...(inputRequest.allowFreeform !== undefined && {\n allowFreeform: inputRequest.allowFreeform,\n }),\n ...(inputRequest.options && {\n options: inputRequest.options.map((option) => ({\n id: option.id,\n label: option.label,\n ...(option.description !== undefined && {\n description: option.description,\n }),\n ...(option.style !== undefined && { style: option.style }),\n })),\n }),\n});\n\nconst toToolProviderMetadata = (\n part: EveDynamicToolPart,\n): PartProviderMetadata | undefined => {\n const inputRequest = part.toolMetadata?.eve?.inputRequest;\n if (!inputRequest) return undefined;\n return { eve: { inputRequest: toJsonSafeInputRequest(inputRequest) } };\n};\n\nconst convertDynamicToolPart = (\n part: EveDynamicToolPart,\n): ToolCallMessagePart => {\n const approval = toApproval(part);\n const providerMetadata = toToolProviderMetadata(part);\n const toolCall: ToolCallMessagePart = {\n type: \"tool-call\",\n toolCallId: part.toolCallId,\n toolName: part.toolName,\n args: toJsonObject(part.input),\n argsText: stringifyArgs(part.input),\n ...(approval && { approval }),\n ...(providerMetadata && { providerMetadata }),\n };\n\n switch (part.state) {\n case \"output-available\":\n return { ...toolCall, result: part.output };\n\n case \"output-error\":\n return {\n ...toolCall,\n result: { error: part.errorText },\n isError: true,\n };\n\n case \"output-denied\":\n return {\n ...toolCall,\n result: { error: part.approval?.reason ?? \"Tool approval denied\" },\n isError: true,\n };\n\n default:\n return toolCall;\n }\n};\n\n/**\n * Payload of the `authorization` data part the Eve runtime emits for a\n * connector authorization challenge. `state` discriminates a pending challenge\n * from a settled one; every other field is present only when Eve projected it.\n * This converter drops a `url` whose scheme is not `http(s)`, so a renderer can\n * link to it without checking the scheme itself; Eve itself passes through\n * whatever the connector supplied. Eve's\n * `turnId` and `stepIndex` part identity is omitted: a renderer receives the\n * one part it renders, in Eve's own step order, so display never has to\n * re-identify a challenge among several.\n */\nexport type EveAuthorizationData = {\n readonly state: EveAuthorizationPart[\"state\"];\n readonly name: string;\n readonly displayName?: string;\n readonly description?: string;\n readonly url?: string;\n readonly userCode?: string;\n readonly instructions?: string;\n readonly expiresAt?: string;\n readonly outcome?: EveAuthorizationOutcome;\n readonly reason?: string;\n};\n\nconst convertAuthorizationPart = (\n part: EveAuthorizationPart,\n): DataMessagePart<EveAuthorizationData> => ({\n type: \"data\",\n name: \"authorization\",\n data: {\n state: part.state,\n name: part.name,\n ...(part.displayName !== undefined && { displayName: part.displayName }),\n ...(part.description !== undefined && { description: part.description }),\n ...(part.authorization?.url !== undefined &&\n httpUrlPattern.test(part.authorization.url) && {\n url: part.authorization.url,\n }),\n ...(part.authorization?.userCode !== undefined && {\n userCode: part.authorization.userCode,\n }),\n ...(part.authorization?.instructions !== undefined && {\n instructions: part.authorization.instructions,\n }),\n ...(part.authorization?.expiresAt !== undefined && {\n expiresAt: part.authorization.expiresAt,\n }),\n ...(part.outcome !== undefined && { outcome: part.outcome }),\n ...(part.reason !== undefined && { reason: part.reason }),\n },\n});\n\nconst convertFilePart = (\n part: Extract<EveMessagePart, { type: \"file\" }>,\n): FileMessagePart | null => {\n if (part.url === undefined) return null;\n return {\n type: \"file\",\n data: part.url,\n mimeType: part.mediaType ?? \"unknown/unknown\",\n ...(part.filename && { filename: part.filename }),\n ...(httpUrlPattern.test(part.url) && { sourceType: \"url\" as const }),\n };\n};\n\n// Eve's `state` is a run marker: the reducer settles a part to \"done\" only on\n// `reasoning.completed`, `message.completed`, or `turn.cancelled`.\n// `emitStreamContent` does not flush leftover reasoning on a tool-call, so a\n// non-last reasoning part stays `streaming` until the model stream ends and the\n// leftover flush runs. Mapping `streaming` to running would pin that block\n// open. Only `done` is trustworthy; an unsettled part falls back to core's\n// last-part rule.\nconst partStateToStatus = (\n state: \"done\" | \"streaming\" | undefined,\n): MessagePartStreamStatus | undefined =>\n state === \"done\" ? { type: \"complete\" } : undefined;\n\nconst convertAssistantPart = (\n part: EveMessagePart,\n): ThreadAssistantMessagePart | null => {\n switch (part.type) {\n case \"text\":\n case \"reasoning\": {\n const status = partStateToStatus(part.state);\n return {\n type: part.type,\n text: part.text,\n ...(status && { status }),\n };\n }\n\n case \"step-start\":\n return null;\n\n case \"dynamic-tool\":\n return convertDynamicToolPart(part);\n\n case \"authorization\":\n return convertAuthorizationPart(part);\n\n case \"file\":\n return convertFilePart(part);\n\n default:\n return null;\n }\n};\n\nconst convertUserPart = (\n part: EveMessagePart,\n): ThreadUserMessagePart | null => {\n switch (part.type) {\n case \"text\":\n return { type: \"text\", text: part.text };\n\n case \"file\":\n return convertFilePart(part);\n\n default:\n return null;\n }\n};\n\nconst toUserContent = (\n parts: readonly EveMessagePart[],\n): readonly ThreadUserMessagePart[] => {\n const content = parts.map(convertUserPart).filter((part) => part !== null);\n return content.length > 0 ? content : [{ type: \"text\", text: \"\" }];\n};\n\nconst toUserAttachments = (\n parts: readonly EveMessagePart[],\n): CompleteAttachment[] => {\n const attachments: CompleteAttachment[] = [];\n for (const part of parts) {\n if (part.type !== \"file\") continue;\n const file = convertFilePart(part);\n if (file === null) continue;\n const isImage = file.mimeType.startsWith(\"image/\");\n attachments.push({\n id: String(attachments.length),\n type: isImage ? \"image\" : \"file\",\n name: part.filename ?? \"file\",\n content: [\n isImage\n ? {\n type: \"image\",\n image: file.data,\n ...(part.filename && { filename: part.filename }),\n }\n : file,\n ],\n contentType: file.mimeType,\n status: { type: \"complete\" },\n });\n }\n return attachments;\n};\n\n/**\n * Converts a single Eve message into an assistant-ui thread message.\n */\nexport const convertEveMessage = (\n message: EveMessage,\n index: number,\n messages: readonly EveMessage[],\n options: ConvertEveMessagesOptions = {},\n): ThreadMessage =>\n convertEveMessageWithInterruptions(\n message,\n index,\n messages,\n options,\n getInterruptedTurns(options.events),\n );\n\nconst convertEveMessageWithInterruptions = (\n message: EveMessage,\n index: number,\n messages: readonly EveMessage[],\n options: ConvertEveMessagesOptions,\n interruptedTurns: ReadonlyMap<string, MessageStatus>,\n): ThreadMessage => {\n const createdAt = options.getCreatedAt?.(message) ?? new Date();\n const metadata = {\n ...(message.metadata?.optimistic && { isOptimistic: true }),\n custom: {\n ...(message.metadata ?? {}),\n },\n };\n\n const like: ThreadMessageLike =\n message.role === \"user\"\n ? {\n role: \"user\",\n id: message.id,\n createdAt,\n content: toUserContent(message.parts),\n attachments: toUserAttachments(message.parts),\n metadata,\n }\n : {\n role: \"assistant\",\n id: message.id,\n createdAt,\n content: message.parts\n .map(convertAssistantPart)\n .filter((part) => part !== null),\n metadata,\n };\n\n return fromThreadMessageLike(\n like,\n message.id,\n toMessageStatus(message, index, messages, options, interruptedTurns),\n );\n};\n\nexport type InterruptedTurnEventCache = {\n lastEvents: readonly MessageStreamEvent[];\n interruptions: readonly MessageStreamEvent[];\n};\n\nconst isTurnInterruption = (event: MessageStreamEvent) =>\n event.type === \"turn.failed\" || event.type === \"turn.cancelled\";\n\n/**\n * Collects the `turn.failed` and `turn.cancelled` events of an append-only\n * event log, scanning only the events appended since the cached call. The\n * returned array keeps its identity until a new interruption arrives.\n */\nexport const collectInterruptedTurnEvents = (\n events: readonly MessageStreamEvent[],\n cache: InterruptedTurnEventCache,\n): readonly MessageStreamEvent[] => {\n if (events === cache.lastEvents) return cache.interruptions;\n\n const scanned = cache.lastEvents;\n const resumesScan =\n scanned.length === 0 ||\n events[scanned.length - 1] === scanned[scanned.length - 1];\n\n let interruptions = resumesScan ? cache.interruptions : [];\n for (let i = resumesScan ? scanned.length : 0; i < events.length; i++) {\n const event = events[i]!;\n if (isTurnInterruption(event)) interruptions = [...interruptions, event];\n }\n\n cache.lastEvents = events;\n cache.interruptions = interruptions;\n return interruptions;\n};\n\nconst getInterruptedTurns = (\n events: readonly MessageStreamEvent[] | undefined,\n) => {\n const interrupted = new Map<string, MessageStatus>();\n for (const event of events ?? []) {\n if (event.type === \"turn.failed\") {\n interrupted.set(event.data.turnId, {\n type: \"incomplete\",\n reason: \"error\",\n error: { code: event.data.code, message: event.data.message },\n });\n } else if (event.type === \"turn.cancelled\") {\n interrupted.set(event.data.turnId, ASSISTANT_CANCELLED_STATUS);\n }\n }\n return interrupted;\n};\n\n/**\n * Converts the full Eve message data object into assistant-ui thread messages.\n */\nexport const convertEveMessages = (\n data: EveMessageData,\n options: ConvertEveMessagesOptions = {},\n): ThreadMessage[] => {\n const interruptedTurns = getInterruptedTurns(options.events);\n return data.messages.map((message, index, messages) =>\n convertEveMessageWithInterruptions(\n message,\n index,\n messages,\n options,\n interruptedTurns,\n ),\n );\n};\n\n/**\n * Structural subset of the `string | UserContent` message content Eve's `send`\n * API accepts. The helpers declare the shapes they produce and leave\n * assignability to the send call site, checked against the installed version.\n */\nexport type EveMessageContent =\n | string\n | (\n | { readonly type: \"text\"; readonly text: string }\n | {\n readonly type: \"file\";\n readonly data: string;\n readonly mediaType: string;\n readonly filename?: string;\n }\n )[];\n\n/**\n * Converts an assistant-ui append message into the message payload accepted by\n * Eve's `send` API.\n */\nexport const getEveMessageContent = (\n message: AppendMessage,\n): EveMessageContent => {\n const content = [\n ...message.content,\n ...(message.attachments?.flatMap((attachment) => attachment.content) ?? []),\n ];\n\n const parts = content.flatMap((part) => {\n const type = part.type;\n switch (type) {\n case \"text\":\n return { type: \"text\" as const, text: part.text };\n\n case \"file\":\n return {\n type: \"file\" as const,\n data: part.data,\n mediaType: part.mimeType,\n ...(part.filename && { filename: part.filename }),\n };\n\n case \"image\":\n return {\n type: \"file\" as const,\n data: part.image,\n mediaType: \"image/*\",\n ...(part.filename && { filename: part.filename }),\n };\n\n case \"audio\": {\n // A data URL's own media type wins over `mediaType` downstream, so the\n // envelope is rebuilt from the typed format rather than forwarded.\n const mediaType = `audio/${part.audio.format}`;\n const data = part.audio.data;\n const source = resolveFilePartSource({ data, mimeType: mediaType });\n return {\n type: \"file\" as const,\n data:\n source.kind === \"url\"\n ? source.url\n : `data:${mediaType};base64,${source.data}`,\n mediaType,\n };\n }\n\n case \"data\":\n return [];\n\n default: {\n const _exhaustiveCheck:\n | \"generative-ui\"\n | \"reasoning\"\n | \"source\"\n | \"tool-call\" = type;\n throw new Error(\n `Unsupported eve append message part type: ${_exhaustiveCheck}`,\n );\n }\n }\n });\n\n if (parts.length === 1 && parts[0]?.type === \"text\") {\n return parts[0].text;\n }\n\n return parts;\n};\n\n/**\n * Finds the pending input request answered by the given approval id, so the\n * response mapping can honor the request's display mode.\n */\nexport const findEveInputRequest = (\n data: EveMessageData,\n approvalId: string,\n): EveMessageInputRequest | undefined => {\n for (const message of data.messages) {\n for (const part of message.parts) {\n if (part.type === \"dynamic-tool\" && part.approval?.id === approvalId) {\n return part.toolMetadata?.eve?.inputRequest;\n }\n }\n }\n return undefined;\n};\n\n/**\n * Converts an assistant-ui tool approval response into an Eve input response.\n *\n * When `inputRequest` is known, every returned response carries either an\n * option the request declares or a free-form answer. A literal option match\n * wins, then the `\"approve\"` / `\"cancel\"` option the response's boolean decision\n * names, then the response's `text` (or its `reason`, for a caller that\n * predates the first-class answer field) when the request takes a free-form\n * answer (`display: \"text\"`, `allowFreeform`, or no options at all, and never\n * `display: \"confirmation\"`) and the response is not a refusal.\n * A one-argument call, or a part with no request, still forwards a caller\n * `optionId` as it always has, including ids the request never declared.\n *\n * A response the known request cannot record as the answer it asked for throws\n * instead of being submitted, because eve resolves a request the moment any\n * response for it arrives, and an empty one is recorded as an answer with no\n * content. Not sending leaves the request pending, so the caller can retry or\n * the user can answer it as an ordinary message.\n *\n * With no request to map against — a one-argument call, or a part carrying no\n * `toolMetadata.eve.inputRequest` — the response maps as it always has, to the\n * literal `\"approve\"` / `\"cancel\"` option every eve approval declares. Guessing\n * a display mode is what this mapper stopped doing; an unknown request is not\n * a guess about one.\n */\nexport const toEveInputResponse = (\n response: RespondToToolApprovalOptions,\n inputRequest?: EveMessageInputRequest,\n): InputResponse => {\n const requestId = response.approvalId;\n const options = inputRequest?.options;\n const text = response.text ?? response.reason;\n\n if (response.optionId !== undefined) {\n if (\n !inputRequest ||\n options?.some((option) => option.id === response.optionId)\n ) {\n return {\n requestId,\n optionId: response.optionId,\n ...(text && { text }),\n };\n }\n throw new Error(\n `Eve input request \"${requestId}\" has no option with id \"${response.optionId}\"; respond with one of: ${\n options?.length\n ? options.map((option) => option.id).join(\", \")\n : \"(the request carries no options)\"\n }`,\n );\n }\n\n const decisionOptionId = response.approved ? \"approve\" : \"cancel\";\n // Eve's harness always declares `approve`/`cancel` on a `tool-approval`\n // request, so honoring the kind when the options were dropped keeps a\n // non-spec approval payload answerable rather than stuck. A tool-approval\n // that declares its own options still requires one of them.\n if (\n !inputRequest ||\n (inputRequest.kind === \"tool-approval\" && !options?.length) ||\n options?.some((option) => option.id === decisionOptionId)\n ) {\n return { requestId, optionId: decisionOptionId, ...(text && { text }) };\n }\n\n // Eve recognises an approval by its literal two-option `approve`/`cancel`\n // shape, so past this point the request is a question and the boolean\n // carries no decision eve would act on.\n const acceptsText = acceptsFreeformAnswer(inputRequest);\n if (acceptsText && text && response.approved !== false) {\n return { requestId, text };\n }\n\n throw new Error(\n `Eve input request \"${requestId}\" (${inputRequest.prompt}) was not answered by this response; ${\n options?.length\n ? `respond with one of: ${options.map((option) => option.id).join(\", \")}`\n : !acceptsText\n ? \"the request declares no options to respond with\"\n : response.approved === false\n ? \"a refusal carries no answer for a free-form request; resend with `approved` unset or true and the answer in the response text\"\n : \"pass the answer as the response text, because the request takes a free-form answer\"\n }`,\n );\n};\n"],"mappings":";;;AAmCA,MAAM,4BAA4B;CAChC,MAAM;CACN,QAAQ;AACV;AAEA,MAAM,2BAA2B,EAC/B,MAAM,UACR;AAEA,MAAM,6BAA6B;CACjC,MAAM;CACN,QAAQ;AACV;AAEA,MAAM,uBAAuB;CAC3B,MAAM;CACN,QAAQ;AACV;AAuBA,MAAM,YAAY,UAChB,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAErE,MAAM,gBAAgB,UAAgD;CACpE,IAAI,SAAS,KAAK,GAAG,OAAO;CAC5B,IAAI,UAAU,KAAA,GAAW,OAAO,CAAC;CACjC,OAAO,EAAE,MAAM;AACjB;AAEA,MAAM,iBAAiB,UAA2B;CAChD,IAAI;EACF,OAAO,KAAK,UAAU,SAAS,CAAC,CAAC;CACnC,QAAQ;EACN,OAAO;CACT;AACF;AAEA,MAAM,mBACJ,SACA,OACA,UACA,SACA,qBACkB;CAClB,IAAI,QAAQ,SAAS,aAAa,OAAO;CAEzC,MAAM,cACJ,QAAQ,UAAU,WAAW,KAAA,IACzB,KAAA,IACA,iBAAiB,IAAI,QAAQ,SAAS,MAAM;CAClD,IAAI,aAAa,OAAO;CAExB,MAAM,SAAS,UAAU,SAAS,SAAS;CAC3C,MAAM,qBAAqB,QAAQ,MAAM,MACtC,SACC,KAAK,SAAS,kBAAkB,KAAK,UAAU,oBACnD;CAGA,MAAM,0BACJ,QAAQ,UAAU,WAAW,gBAC5B,CAAC,UAAU,QAAQ,UAAU,KAAA,MAC9B,QAAQ,MAAM,MACX,SAAS,KAAK,SAAS,mBAAmB,KAAK,UAAU,UAC5D;CAEF,IAAI,oBACF,OAAO;EAAE,MAAM;EAAmB,QAAQ;CAAa;CAKzD,IAAI,yBACF,OAAO;EAAE,MAAM;EAAmB,QAAQ;CAAY;CAGxD,IAAI,QAAQ,UAAU,WAAW,UAC/B,OAAO;EAAE,MAAM;EAAc,QAAQ;CAAQ;CAG/C,IAAI,UAAU,QAAQ,cAAc,MAClC,OAAO;CAIT,IAAI,QAAQ,UAAU,WAAW,aAAa;EAC5C,IAAI,QAAQ,cAAc,KAAA,GACxB,OAAO;EAET,IAAI,UAAU,QAAQ,UAAU,KAAA,GAC9B,OAAO;GACL,MAAM;GACN,QAAQ;GACR,OAAO,iBAAiB,QAAQ,KAAK;EACvC;EAEF,OAAO;CACT;CAEA,OAAO;AACT;AAEA,MAAM,uCACJ,iBAC8C;CAC9C,MAAM,UAAU,cAAc;CAC9B,IAAI,CAAC,WAAW,QAAQ,WAAW,GAAG,OAAO,KAAA;CAE7C,OAAO,QAAQ,KAAK,YAAY;EAC9B,IAAI,OAAO;EACX,MACE,OAAO,OAAO,YACV,eACA,OAAO,OAAO,WACZ,gBACA,IAAI,OAAO;EACnB,GAAI,OAAO,SAAS,EAAE,OAAO,OAAO,MAAM;EAC1C,GAAI,OAAO,eAAe,EAAE,aAAa,OAAO,YAAY;CAC9D,EAAE;AACJ;;;;;;;;AASA,MAAM,2BAA2B,iBAC/B,aAAa,SAAS,mBAAmB,CAAC,aAAa,SAAS;AAElE,MAAM,yBAAyB,iBAC7B,aAAa,YAAY,kBACzB,CAAC,wBAAwB,YAAY,MACpC,aAAa,YAAY,UACxB,aAAa,kBAAkB,QAC/B,CAAC,aAAa,SAAS;AAE3B,MAAM,uBACJ,iBACwB;CAIxB,IAAI,wBAAwB,YAAY,GAAG,OAAO;CAElD,QAAQ,aAAa,SAArB;EACE,KAAK,gBACH,OAAO;EACT,KAAK,UACH,OAAO;EACT,KAAK,QACH,OAAO;EACT;GACE,IAAI,aAAa,SAAS,iBAAiB,OAAO;GAClD,OAAO,aAAa,SAAS,SAAS,WAAW;CACrD;AACF;AAEA,MAAM,cACJ,SACgD;CAChD,IACE,KAAK,UAAU,wBACf,KAAK,UAAU,wBACf,KAAK,UAAU,sBACf,KAAK,UAAU,kBACf,KAAK,UAAU,iBAEf;CAGF,MAAM,WAAW,KAAK;CACtB,IAAI,CAAC,UAAU,OAAO,KAAA;CACtB,MAAM,eAAe,KAAK,cAAc,KAAK;CAC7C,MAAM,UAAU,oCAAoC,YAAY;CAEhE,OAAO;EACL,IAAI,SAAS;EACb,GAAI,gBAAgB;GAClB,QAAQ,aAAa;GACrB,SAAS,oBAAoB,YAAY;GACzC,GAAI,sBAAsB,YAAY,KAAK,EAAE,eAAe,KAAK;EACnE;EACA,GAAI,SAAS,aAAa,KAAA,KAAa,EAAE,UAAU,SAAS,SAAS;EACrE,GAAI,SAAS,UAAU,EAAE,QAAQ,SAAS,OAAO;EACjD,GAAI,SAAS,gBAAgB,KAAA,KAAa,EACxC,aAAa,SAAS,YACxB;EACA,GAAI,WAAW,EAAE,QAAQ;CAC3B;AACF;AAEA,MAAM,0BACJ,kBACkC;CAClC,WAAW,aAAa;CACxB,QAAQ,aAAa;CACrB,MAAM,aAAa;CACnB,GAAI,aAAa,YAAY,KAAA,KAAa,EAAE,SAAS,aAAa,QAAQ;CAC1E,GAAI,aAAa,kBAAkB,KAAA,KAAa,EAC9C,eAAe,aAAa,cAC9B;CACA,GAAI,aAAa,WAAW,EAC1B,SAAS,aAAa,QAAQ,KAAK,YAAY;EAC7C,IAAI,OAAO;EACX,OAAO,OAAO;EACd,GAAI,OAAO,gBAAgB,KAAA,KAAa,EACtC,aAAa,OAAO,YACtB;EACA,GAAI,OAAO,UAAU,KAAA,KAAa,EAAE,OAAO,OAAO,MAAM;CAC1D,EAAE,EACJ;AACF;AAEA,MAAM,0BACJ,SACqC;CACrC,MAAM,eAAe,KAAK,cAAc,KAAK;CAC7C,IAAI,CAAC,cAAc,OAAO,KAAA;CAC1B,OAAO,EAAE,KAAK,EAAE,cAAc,uBAAuB,YAAY,EAAE,EAAE;AACvE;AAEA,MAAM,0BACJ,SACwB;CACxB,MAAM,WAAW,WAAW,IAAI;CAChC,MAAM,mBAAmB,uBAAuB,IAAI;CACpD,MAAM,WAAgC;EACpC,MAAM;EACN,YAAY,KAAK;EACjB,UAAU,KAAK;EACf,MAAM,aAAa,KAAK,KAAK;EAC7B,UAAU,cAAc,KAAK,KAAK;EAClC,GAAI,YAAY,EAAE,SAAS;EAC3B,GAAI,oBAAoB,EAAE,iBAAiB;CAC7C;CAEA,QAAQ,KAAK,OAAb;EACE,KAAK,oBACH,OAAO;GAAE,GAAG;GAAU,QAAQ,KAAK;EAAO;EAE5C,KAAK,gBACH,OAAO;GACL,GAAG;GACH,QAAQ,EAAE,OAAO,KAAK,UAAU;GAChC,SAAS;EACX;EAEF,KAAK,iBACH,OAAO;GACL,GAAG;GACH,QAAQ,EAAE,OAAO,KAAK,UAAU,UAAU,uBAAuB;GACjE,SAAS;EACX;EAEF,SACE,OAAO;CACX;AACF;AA0BA,MAAM,4BACJ,UAC2C;CAC3C,MAAM;CACN,MAAM;CACN,MAAM;EACJ,OAAO,KAAK;EACZ,MAAM,KAAK;EACX,GAAI,KAAK,gBAAgB,KAAA,KAAa,EAAE,aAAa,KAAK,YAAY;EACtE,GAAI,KAAK,gBAAgB,KAAA,KAAa,EAAE,aAAa,KAAK,YAAY;EACtE,GAAI,KAAK,eAAe,QAAQ,KAAA,KAC9B,eAAe,KAAK,KAAK,cAAc,GAAG,KAAK,EAC7C,KAAK,KAAK,cAAc,IAC1B;EACF,GAAI,KAAK,eAAe,aAAa,KAAA,KAAa,EAChD,UAAU,KAAK,cAAc,SAC/B;EACA,GAAI,KAAK,eAAe,iBAAiB,KAAA,KAAa,EACpD,cAAc,KAAK,cAAc,aACnC;EACA,GAAI,KAAK,eAAe,cAAc,KAAA,KAAa,EACjD,WAAW,KAAK,cAAc,UAChC;EACA,GAAI,KAAK,YAAY,KAAA,KAAa,EAAE,SAAS,KAAK,QAAQ;EAC1D,GAAI,KAAK,WAAW,KAAA,KAAa,EAAE,QAAQ,KAAK,OAAO;CACzD;AACF;AAEA,MAAM,mBACJ,SAC2B;CAC3B,IAAI,KAAK,QAAQ,KAAA,GAAW,OAAO;CACnC,OAAO;EACL,MAAM;EACN,MAAM,KAAK;EACX,UAAU,KAAK,aAAa;EAC5B,GAAI,KAAK,YAAY,EAAE,UAAU,KAAK,SAAS;EAC/C,GAAI,eAAe,KAAK,KAAK,GAAG,KAAK,EAAE,YAAY,MAAe;CACpE;AACF;AASA,MAAM,qBACJ,UAEA,UAAU,SAAS,EAAE,MAAM,WAAW,IAAI,KAAA;AAE5C,MAAM,wBACJ,SACsC;CACtC,QAAQ,KAAK,MAAb;EACE,KAAK;EACL,KAAK,aAAa;GAChB,MAAM,SAAS,kBAAkB,KAAK,KAAK;GAC3C,OAAO;IACL,MAAM,KAAK;IACX,MAAM,KAAK;IACX,GAAI,UAAU,EAAE,OAAO;GACzB;EACF;EAEA,KAAK,cACH,OAAO;EAET,KAAK,gBACH,OAAO,uBAAuB,IAAI;EAEpC,KAAK,iBACH,OAAO,yBAAyB,IAAI;EAEtC,KAAK,QACH,OAAO,gBAAgB,IAAI;EAE7B,SACE,OAAO;CACX;AACF;AAEA,MAAM,mBACJ,SACiC;CACjC,QAAQ,KAAK,MAAb;EACE,KAAK,QACH,OAAO;GAAE,MAAM;GAAQ,MAAM,KAAK;EAAK;EAEzC,KAAK,QACH,OAAO,gBAAgB,IAAI;EAE7B,SACE,OAAO;CACX;AACF;AAEA,MAAM,iBACJ,UACqC;CACrC,MAAM,UAAU,MAAM,IAAI,eAAe,CAAC,CAAC,QAAQ,SAAS,SAAS,IAAI;CACzE,OAAO,QAAQ,SAAS,IAAI,UAAU,CAAC;EAAE,MAAM;EAAQ,MAAM;CAAG,CAAC;AACnE;AAEA,MAAM,qBACJ,UACyB;CACzB,MAAM,cAAoC,CAAC;CAC3C,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,KAAK,SAAS,QAAQ;EAC1B,MAAM,OAAO,gBAAgB,IAAI;EACjC,IAAI,SAAS,MAAM;EACnB,MAAM,UAAU,KAAK,SAAS,WAAW,QAAQ;EACjD,YAAY,KAAK;GACf,IAAI,OAAO,YAAY,MAAM;GAC7B,MAAM,UAAU,UAAU;GAC1B,MAAM,KAAK,YAAY;GACvB,SAAS,CACP,UACI;IACE,MAAM;IACN,OAAO,KAAK;IACZ,GAAI,KAAK,YAAY,EAAE,UAAU,KAAK,SAAS;GACjD,IACA,IACN;GACA,aAAa,KAAK;GAClB,QAAQ,EAAE,MAAM,WAAW;EAC7B,CAAC;CACH;CACA,OAAO;AACT;;;;AAKA,MAAa,qBACX,SACA,OACA,UACA,UAAqC,CAAC,MAEtC,mCACE,SACA,OACA,UACA,SACA,oBAAoB,QAAQ,MAAM,CACpC;AAEF,MAAM,sCACJ,SACA,OACA,UACA,SACA,qBACkB;CAClB,MAAM,YAAY,QAAQ,eAAe,OAAO,qBAAK,IAAI,KAAK;CAC9D,MAAM,WAAW;EACf,GAAI,QAAQ,UAAU,cAAc,EAAE,cAAc,KAAK;EACzD,QAAQ,EACN,GAAI,QAAQ,YAAY,CAAC,EAC3B;CACF;CAEA,MAAM,OACJ,QAAQ,SAAS,SACb;EACE,MAAM;EACN,IAAI,QAAQ;EACZ;EACA,SAAS,cAAc,QAAQ,KAAK;EACpC,aAAa,kBAAkB,QAAQ,KAAK;EAC5C;CACF,IACA;EACE,MAAM;EACN,IAAI,QAAQ;EACZ;EACA,SAAS,QAAQ,MACd,IAAI,oBAAoB,CAAC,CACzB,QAAQ,SAAS,SAAS,IAAI;EACjC;CACF;CAEN,OAAO,sBACL,MACA,QAAQ,IACR,gBAAgB,SAAS,OAAO,UAAU,SAAS,gBAAgB,CACrE;AACF;AAOA,MAAM,sBAAsB,UAC1B,MAAM,SAAS,iBAAiB,MAAM,SAAS;;;;;;AAOjD,MAAa,gCACX,QACA,UACkC;CAClC,IAAI,WAAW,MAAM,YAAY,OAAO,MAAM;CAE9C,MAAM,UAAU,MAAM;CACtB,MAAM,cACJ,QAAQ,WAAW,KACnB,OAAO,QAAQ,SAAS,OAAO,QAAQ,QAAQ,SAAS;CAE1D,IAAI,gBAAgB,cAAc,MAAM,gBAAgB,CAAC;CACzD,KAAK,IAAI,IAAI,cAAc,QAAQ,SAAS,GAAG,IAAI,OAAO,QAAQ,KAAK;EACrE,MAAM,QAAQ,OAAO;EACrB,IAAI,mBAAmB,KAAK,GAAG,gBAAgB,CAAC,GAAG,eAAe,KAAK;CACzE;CAEA,MAAM,aAAa;CACnB,MAAM,gBAAgB;CACtB,OAAO;AACT;AAEA,MAAM,uBACJ,WACG;CACH,MAAM,8BAAc,IAAI,IAA2B;CACnD,KAAK,MAAM,SAAS,UAAU,CAAC,GAC7B,IAAI,MAAM,SAAS,eACjB,YAAY,IAAI,MAAM,KAAK,QAAQ;EACjC,MAAM;EACN,QAAQ;EACR,OAAO;GAAE,MAAM,MAAM,KAAK;GAAM,SAAS,MAAM,KAAK;EAAQ;CAC9D,CAAC;MACI,IAAI,MAAM,SAAS,kBACxB,YAAY,IAAI,MAAM,KAAK,QAAQ,0BAA0B;CAGjE,OAAO;AACT;;;;AAKA,MAAa,sBACX,MACA,UAAqC,CAAC,MAClB;CACpB,MAAM,mBAAmB,oBAAoB,QAAQ,MAAM;CAC3D,OAAO,KAAK,SAAS,KAAK,SAAS,OAAO,aACxC,mCACE,SACA,OACA,UACA,SACA,gBACF,CACF;AACF;;;;;AAuBA,MAAa,wBACX,YACsB;CAMtB,MAAM,QAAQ,CAJZ,GAAG,QAAQ,SACX,GAAI,QAAQ,aAAa,SAAS,eAAe,WAAW,OAAO,KAAK,CAAC,CAGvD,CAAC,CAAC,SAAS,SAAS;EACtC,MAAM,OAAO,KAAK;EAClB,QAAQ,MAAR;GACE,KAAK,QACH,OAAO;IAAE,MAAM;IAAiB,MAAM,KAAK;GAAK;GAElD,KAAK,QACH,OAAO;IACL,MAAM;IACN,MAAM,KAAK;IACX,WAAW,KAAK;IAChB,GAAI,KAAK,YAAY,EAAE,UAAU,KAAK,SAAS;GACjD;GAEF,KAAK,SACH,OAAO;IACL,MAAM;IACN,MAAM,KAAK;IACX,WAAW;IACX,GAAI,KAAK,YAAY,EAAE,UAAU,KAAK,SAAS;GACjD;GAEF,KAAK,SAAS;IAGZ,MAAM,YAAY,SAAS,KAAK,MAAM;IACtC,MAAM,OAAO,KAAK,MAAM;IACxB,MAAM,SAAS,sBAAsB;KAAE;KAAM,UAAU;IAAU,CAAC;IAClE,OAAO;KACL,MAAM;KACN,MACE,OAAO,SAAS,QACZ,OAAO,MACP,QAAQ,UAAU,UAAU,OAAO;KACzC;IACF;GACF;GAEA,KAAK,QACH,OAAO,CAAC;GAEV,SAME,MAAM,IAAI,MACR,6CAA6CA,MAC/C;EAEJ;CACF,CAAC;CAED,IAAI,MAAM,WAAW,KAAK,MAAM,EAAE,EAAE,SAAS,QAC3C,OAAO,MAAM,EAAE,CAAC;CAGlB,OAAO;AACT;;;;;AAMA,MAAa,uBACX,MACA,eACuC;CACvC,KAAK,MAAM,WAAW,KAAK,UACzB,KAAK,MAAM,QAAQ,QAAQ,OACzB,IAAI,KAAK,SAAS,kBAAkB,KAAK,UAAU,OAAO,YACxD,OAAO,KAAK,cAAc,KAAK;AAKvC;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,MAAa,sBACX,UACA,iBACkB;CAClB,MAAM,YAAY,SAAS;CAC3B,MAAM,UAAU,cAAc;CAC9B,MAAM,OAAO,SAAS,QAAQ,SAAS;CAEvC,IAAI,SAAS,aAAa,KAAA,GAAW;EACnC,IACE,CAAC,gBACD,SAAS,MAAM,WAAW,OAAO,OAAO,SAAS,QAAQ,GAEzD,OAAO;GACL;GACA,UAAU,SAAS;GACnB,GAAI,QAAQ,EAAE,KAAK;EACrB;EAEF,MAAM,IAAI,MACR,sBAAsB,UAAU,2BAA2B,SAAS,SAAS,0BAC3E,SAAS,SACL,QAAQ,KAAK,WAAW,OAAO,EAAE,CAAC,CAAC,KAAK,IAAI,IAC5C,oCAER;CACF;CAEA,MAAM,mBAAmB,SAAS,WAAW,YAAY;CAKzD,IACE,CAAC,gBACA,aAAa,SAAS,mBAAmB,CAAC,SAAS,UACpD,SAAS,MAAM,WAAW,OAAO,OAAO,gBAAgB,GAExD,OAAO;EAAE;EAAW,UAAU;EAAkB,GAAI,QAAQ,EAAE,KAAK;CAAG;CAMxE,MAAM,cAAc,sBAAsB,YAAY;CACtD,IAAI,eAAe,QAAQ,SAAS,aAAa,OAC/C,OAAO;EAAE;EAAW;CAAK;CAG3B,MAAM,IAAI,MACR,sBAAsB,UAAU,KAAK,aAAa,OAAO,uCACvD,SAAS,SACL,wBAAwB,QAAQ,KAAK,WAAW,OAAO,EAAE,CAAC,CAAC,KAAK,IAAI,MACpE,CAAC,cACC,oDACA,SAAS,aAAa,QACpB,kIACA,sFAEZ;AACF"}
@@ -1,7 +1,7 @@
1
1
  //#region src/sdkIdentity.ts
2
2
  const EVE_SDK = {
3
3
  name: "@assistant-ui/eve",
4
- version: "0.0.21"
4
+ version: "0.0.22"
5
5
  };
6
6
  //#endregion
7
7
  export { EVE_SDK };
@@ -1 +1 @@
1
- {"version":3,"file":"useEveAgentRuntime.d.ts","sourceRoot":"","sources":["../src/useEveAgentRuntime.ts"],"names":[],"mappings":"AAUA,OAAO,EAML,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,0BAA0B,EAC/B,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAG5B,MAAM,oBAAoB,CAAC;AAS5B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,kBAAkB,EAExB,MAAM,WAAW,CAAC;AAkGnB,KAAK,eAAe,GAAG,IAAI,CAAC,kBAAkB,CAAC,cAAc,CAAC,EAAE,SAAS,CAAC,CAAC;AAE3E,MAAM,MAAM,yBAAyB,GAAG,eAAe,GACrD,0BAA0B,GAAG;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EACd;QACE,QAAQ,CAAC,WAAW,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAC;QACrD,QAAQ,CAAC,MAAM,CAAC,EAAE,sBAAsB,GAAG,SAAS,CAAC;QACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAC;QAClD,QAAQ,CAAC,KAAK,CAAC,EAAE,oBAAoB,GAAG,SAAS,CAAC;QAClD,QAAQ,CAAC,QAAQ,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC;KACjD,GACD,SAAS,CAAC;IACd;;OAEG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;CAC7C,CAAC;AAmfJ;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,GAAI,UAAS,yBAA8B,kDAazE,CAAC"}
1
+ {"version":3,"file":"useEveAgentRuntime.d.ts","sourceRoot":"","sources":["../src/useEveAgentRuntime.ts"],"names":[],"mappings":"AAUA,OAAO,EAML,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,0BAA0B,EAC/B,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAG5B,MAAM,oBAAoB,CAAC;AAS5B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,kBAAkB,EAExB,MAAM,WAAW,CAAC;AAoGnB,KAAK,eAAe,GAAG,IAAI,CAAC,kBAAkB,CAAC,cAAc,CAAC,EAAE,SAAS,CAAC,CAAC;AAE3E,MAAM,MAAM,yBAAyB,GAAG,eAAe,GACrD,0BAA0B,GAAG;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EACd;QACE,QAAQ,CAAC,WAAW,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAC;QACrD,QAAQ,CAAC,MAAM,CAAC,EAAE,sBAAsB,GAAG,SAAS,CAAC;QACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAC;QAClD,QAAQ,CAAC,KAAK,CAAC,EAAE,oBAAoB,GAAG,SAAS,CAAC;QAClD,QAAQ,CAAC,QAAQ,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC;KACjD,GACD,SAAS,CAAC;IACd;;OAEG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;CAC7C,CAAC;AA6fJ;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,GAAI,UAAS,yBAA8B,kDAazE,CAAC"}
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { convertEveMessages, findEveInputRequest, getEveMessageContent, toEveInputResponse } from "./convertEveMessages.js";
2
+ import { collectInterruptedTurnEvents, convertEveMessages, findEveInputRequest, getEveMessageContent, toEveInputResponse } from "./convertEveMessages.js";
3
3
  import { collectTurnTimestamps, createTurnTimestampCache } from "./deriveCreatedAt.js";
4
4
  import { createEveCloudSessions } from "./eveCloudSessions.js";
5
5
  import { eveExtras } from "./eveExtras.js";
@@ -92,6 +92,11 @@ const useEveThreadRuntime = (options, cloudSessions) => {
92
92
  const isRunning = providerIsRunning || hasExecutingTools;
93
93
  const turnTimestampCacheRef = useRef(createTurnTimestampCache());
94
94
  const turnTimestamps = useMemo(() => collectTurnTimestamps(agent.events, turnTimestampCacheRef.current), [agent.events]);
95
+ const interruptionCacheRef = useRef({
96
+ lastEvents: [],
97
+ interruptions: []
98
+ });
99
+ const interruptionEvents = useMemo(() => collectInterruptedTurnEvents(agent.events, interruptionCacheRef.current), [agent.events]);
95
100
  const convertedMessages = useMemo(() => {
96
101
  const createdAtByMessageId = createdAtByMessageIdRef.current;
97
102
  const messageIds = new Set(agent.data.messages.map((message) => message.id));
@@ -99,6 +104,7 @@ const useEveThreadRuntime = (options, cloudSessions) => {
99
104
  return convertEveMessages(agent.data, {
100
105
  isRunning,
101
106
  error: agent.error,
107
+ events: interruptionEvents,
102
108
  getCreatedAt: (message) => {
103
109
  const turnId = message.metadata?.turnId;
104
110
  const durable = turnId === void 0 ? void 0 : turnTimestamps.get(turnId)?.[message.role];
@@ -113,6 +119,7 @@ const useEveThreadRuntime = (options, cloudSessions) => {
113
119
  }, [
114
120
  agent.data,
115
121
  agent.error,
122
+ interruptionEvents,
116
123
  isRunning,
117
124
  turnTimestamps
118
125
  ]);
@@ -1 +1 @@
1
- {"version":3,"file":"useEveAgentRuntime.js","names":[],"sources":["../src/useEveAgentRuntime.ts"],"sourcesContent":["\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useInsertionEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport {\n fromThreadMessageLike,\n generateId,\n MessageNotSentError,\n pickExternalStoreSharedOptions,\n type AppendMessage,\n type AttachmentAdapter,\n type DictationAdapter,\n type ExternalStoreSharedOptions,\n type FeedbackAdapter,\n type RealtimeVoiceAdapter,\n type SpeechSynthesisAdapter,\n type ThreadMessage,\n type ToolExecutionStatus,\n} from \"@assistant-ui/core\";\nimport {\n useCloudThreadListAdapter,\n useExternalStoreRuntime,\n useRemoteThreadListRuntime,\n useRuntimeAdapters,\n} from \"@assistant-ui/core/react\";\nimport { useReplaySafeEffect } from \"@assistant-ui/store/internal\";\nimport { useAui } from \"@assistant-ui/store\";\nimport type { AssistantCloud } from \"assistant-cloud\";\nimport {\n useEveAgent,\n type EveMessageData,\n type UseEveAgentOptions,\n type UseEveAgentStatus,\n} from \"eve/react\";\nimport {\n convertEveMessages,\n findEveInputRequest,\n getEveMessageContent,\n toEveInputResponse,\n} from \"./convertEveMessages\";\nimport {\n collectTurnTimestamps,\n createTurnTimestampCache,\n} from \"./deriveCreatedAt\";\nimport {\n createEveCloudSessions,\n type EveCloudSessions,\n} from \"./eveCloudSessions\";\nimport { eveExtras } from \"./eveExtras\";\nimport { EVE_SDK } from \"./sdkIdentity\";\n\nconst USER_STAGED_STATUS = {\n type: \"complete\",\n reason: \"unknown\",\n} as const;\n\nconst sendCancelledError = new MessageNotSentError(\n \"eve send was dropped because the run was cancelled.\",\n);\n\nconst sendAbandonedError = new Error(\n \"eve send was dropped because the runtime unmounted.\",\n);\n\nconst isDroppedSend = (error: unknown) =>\n error === sendCancelledError || error === sendAbandonedError;\n\ntype EveLifecycleCallbackName =\n | \"onError\"\n | \"onEvent\"\n | \"onFinish\"\n | \"onSessionChange\";\n\nconst reportEveLifecycleCallbackError = (\n name: EveLifecycleCallbackName,\n error: unknown,\n) => {\n console.error(`[assistant-ui/eve] ${name} callback threw an error`, error);\n};\n\nconst invokeEveLifecycleCallback = <T>(\n name: EveLifecycleCallbackName,\n callback: ((value: T) => unknown) | undefined,\n value: T,\n) => {\n if (!callback) return;\n\n try {\n void Promise.resolve(callback(value)).catch((error) => {\n reportEveLifecycleCallbackError(name, error);\n });\n } catch (error) {\n reportEveLifecycleCallbackError(name, error);\n }\n};\n\nconst hasRunConfig = (\n runConfig: AppendMessage[\"runConfig\"],\n): runConfig is NonNullable<AppendMessage[\"runConfig\"]> =>\n runConfig?.custom !== undefined && Object.keys(runConfig.custom).length > 0;\n\ntype EveJsonValue =\n | string\n | number\n | boolean\n | null\n | readonly EveJsonValue[]\n | { readonly [key: string]: EveJsonValue };\n\n/**\n * Structural equivalent of eve's `JsonObject`. Eve demoted its public send\n * payload type once already (`SendTurnPayload`, public through 0.30, internal\n * from 0.31), so the helpers declare the shapes they forward instead of\n * re-exporting eve's option types, and assignability is checked at the send\n * call sites against the installed version.\n */\ntype EveClientContext = { readonly [key: string]: EveJsonValue };\n\n/**\n * Only the `custom` bag crosses the wire. Eve reads `clientContext` as its own\n * namespace and serializes it into a model-visible context message, so sending\n * the assistant-ui envelope would surface a literal `\"custom\"` key in the\n * prompt and to every eve-side handler.\n */\nconst toEveSendOptions = (\n runConfig: AppendMessage[\"runConfig\"],\n): { readonly clientContext: EveClientContext } | undefined =>\n hasRunConfig(runConfig)\n ? { clientContext: runConfig.custom as EveClientContext }\n : undefined;\n\ntype EveAgentOptions = Omit<UseEveAgentOptions<EveMessageData>, \"reducer\">;\n\nexport type UseEveAgentRuntimeOptions = EveAgentOptions &\n ExternalStoreSharedOptions & {\n readonly adapters?:\n | {\n readonly attachments?: AttachmentAdapter | undefined;\n readonly speech?: SpeechSynthesisAdapter | undefined;\n readonly dictation?: DictationAdapter | undefined;\n readonly voice?: RealtimeVoiceAdapter | undefined;\n readonly feedback?: FeedbackAdapter | undefined;\n }\n | undefined;\n /**\n * Backs the thread list with Assistant Cloud. Each cloud thread keeps one Eve session as its external id, saved once the thread's first turn creates the session, and opening a thread resumes it, so `session`, `initialSession`, `initialEvents`, and `resume` are not used. Pass it from the first render: adding or removing it later throws, so remount the runtime, for example with a `key`, to switch.\n */\n readonly cloud?: AssistantCloud | undefined;\n };\n\ntype EveCloudThread = {\n readonly sessions: EveCloudSessions;\n readonly id: string;\n readonly isNew: boolean;\n readonly sessionId: string | undefined;\n};\n\nconst withCloudThreadSession = (\n {\n initialEvents: _initialEvents,\n initialSession: _initialSession,\n resume: _resume,\n session: _session,\n ...options\n }: EveAgentOptions,\n sessionId: string | undefined,\n): EveAgentOptions =>\n sessionId === undefined\n ? options\n : {\n ...options,\n initialSession: { sessionId, streamIndex: 0 },\n resume: true,\n };\n\nconst useEveThreadRuntime = (\n options: UseEveAgentRuntimeOptions,\n cloudSessions: EveCloudSessions | undefined,\n) => {\n const {\n adapters,\n cloud: _cloud,\n isDisabled: _isDisabled,\n isSendDisabled: _isSendDisabled,\n suggestions: _suggestions,\n unstable_capabilities: _unstable_capabilities,\n ...agentOptions\n } = options;\n true satisfies keyof typeof agentOptions &\n keyof ExternalStoreSharedOptions extends never\n ? true\n : never;\n\n const aui = useAui();\n const [cloudThread] = useState((): EveCloudThread | undefined => {\n if (!cloudSessions) return undefined;\n const { id, status, externalId } = aui.threadListItem.getState();\n return {\n sessions: cloudSessions,\n id,\n isNew: status === \"new\",\n sessionId: externalId,\n };\n });\n const [resumesOnMount] = useState(() =>\n cloudThread\n ? cloudThread.sessionId !== undefined\n : agentOptions.resume === true,\n );\n const isSessionless =\n cloudThread !== undefined &&\n !cloudThread.isNew &&\n cloudThread.sessionId === undefined;\n\n const { onError, onEvent, onFinish, onSessionChange } = agentOptions;\n const lastFinishStatusRef = useRef<UseEveAgentStatus | null>(null);\n const agent = useEveAgent({\n ...(cloudThread\n ? withCloudThreadSession(agentOptions, cloudThread.sessionId)\n : agentOptions),\n ...(onError\n ? {\n onError: (error) =>\n invokeEveLifecycleCallback(\"onError\", onError, error),\n }\n : {}),\n ...(onEvent\n ? {\n onEvent: (event) =>\n invokeEveLifecycleCallback(\"onEvent\", onEvent, event),\n }\n : {}),\n onFinish: (snapshot) => {\n lastFinishStatusRef.current = snapshot.status;\n invokeEveLifecycleCallback(\"onFinish\", onFinish, snapshot);\n },\n ...(onSessionChange || cloudThread?.isNew\n ? {\n onSessionChange: (session) => {\n if (cloudThread?.isNew && session === undefined) {\n cloudThread.sessions.reject(\n cloudThread.id,\n new Error(\"The eve turn ended before it created a session.\"),\n );\n } else if (cloudThread?.isNew && session !== undefined) {\n cloudThread.sessions.resolve(cloudThread.id, session.sessionId);\n // A first message staged without a run leaves the thread unsaved, so the run that creates the session saves it.\n if (aui.threadListItem.getState().status === \"new\")\n aui.threadListItem.initialize().catch(() => {});\n }\n invokeEveLifecycleCallback(\n \"onSessionChange\",\n onSessionChange,\n session,\n );\n },\n }\n : {}),\n });\n if (cloudThread && !(\"resume\" in agent)) {\n throw new Error(\n \"useEveAgentRuntime needs eve 0.44.1 or later for `cloud`, because opening a cloud thread resumes its session.\",\n );\n }\n const runtimeAdapters = useRuntimeAdapters();\n const [toolStatuses, setToolStatuses] = useState<\n Record<string, ToolExecutionStatus>\n >({});\n const [stagedMessages, setStagedMessages] = useState<ThreadMessage[] | null>(\n null,\n );\n const createdAtByMessageIdRef = useRef(new Map<string, Date>());\n const stagedInputsRef = useRef(\n new Map<\n string,\n { message: AppendMessage; runConfig: AppendMessage[\"runConfig\"] }\n >(),\n );\n\n const hasExecutingTools = Object.values(toolStatuses).some(\n (status) => status?.type === \"executing\",\n );\n const providerIsRunning =\n agent.status === \"submitted\" || agent.status === \"streaming\";\n const isRunning = providerIsRunning || hasExecutingTools;\n\n // Kept apart from the message memo so events that teach no timestamp keep\n // the map identity and skip rebuilding every ThreadMessage.\n const turnTimestampCacheRef = useRef(createTurnTimestampCache());\n const turnTimestamps = useMemo(\n () => collectTurnTimestamps(agent.events, turnTimestampCacheRef.current),\n [agent.events],\n );\n\n const convertedMessages = useMemo(() => {\n const createdAtByMessageId = createdAtByMessageIdRef.current;\n const messageIds = new Set(\n agent.data.messages.map((message) => message.id),\n );\n for (const messageId of createdAtByMessageId.keys()) {\n if (!messageIds.has(messageId)) createdAtByMessageId.delete(messageId);\n }\n\n return convertEveMessages(agent.data, {\n isRunning,\n error: agent.error,\n getCreatedAt: (message) => {\n const turnId = message.metadata?.turnId;\n const durable =\n turnId === undefined\n ? undefined\n : turnTimestamps.get(turnId)?.[message.role];\n if (durable !== undefined) return durable;\n\n const existing = createdAtByMessageId.get(message.id);\n if (existing) return existing;\n\n const createdAt = new Date();\n createdAtByMessageId.set(message.id, createdAt);\n return createdAt;\n },\n });\n }, [agent.data, agent.error, isRunning, turnTimestamps]);\n\n const messages = stagedMessages ?? convertedMessages;\n const messagesRef = useRef(messages);\n const agentRef = useRef(agent);\n // Descendant layout effects can dispatch against these refs synchronously;\n // this matches useA2ARuntime's commit-scoped ref publication.\n useInsertionEffect(() => {\n messagesRef.current = messages;\n agentRef.current = agent;\n }, [agent, messages]);\n\n // Upstream `EveAgentStore` `send` and `respond` reject while a turn is in\n // flight and only resolve once the turn's stream parks, so a pending chain\n // link is exactly an active turn; chaining every dispatch serializes them\n // without watching status.\n const sendChainRef = useRef<Promise<void>>(Promise.resolve());\n const sendEpochRef = useRef(0);\n // A cancel drops the queued send but keeps its draft for a later promotion;\n // only a reset discards the draft with the session, so the two need separate\n // counters. A queued read has no draft and outlives a cancel, so it waits on\n // the reset counter instead.\n const resetEpochRef = useRef(0);\n const isMountedRef = useRef(true);\n const runtimeRef = useRef<ReturnType<typeof useExternalStoreRuntime> | null>(\n null,\n );\n\n const enqueueSend = useCallback(\n (\n dispatch: () => Promise<void>,\n epochRef: { current: number } = sendEpochRef,\n ) => {\n const epoch = epochRef.current;\n const next = sendChainRef.current.then(() => {\n if (!isMountedRef.current) throw sendAbandonedError;\n if (epoch !== epochRef.current) throw sendCancelledError;\n return dispatch();\n });\n sendChainRef.current = next.catch(() => {});\n return next;\n },\n [],\n );\n\n // The store outlives the component (useEveAgent holds it in a ref with no\n // cleanup), so queued sends must not fire server turns after unmount. The\n // flag separates that teardown from a user cancel, and is re-armed in setup\n // for a remounted tree.\n useEffect(() => {\n isMountedRef.current = true;\n return () => {\n isMountedRef.current = false;\n };\n }, []);\n\n // A replay (Fast Refresh, StrictMode) must not cancel the sends queued behind the active turn.\n useReplaySafeEffect(\n () => () => {\n sendEpochRef.current += 1;\n },\n [],\n );\n\n // A replay rides the send chain because `resume()` rejects during a turn and upstream refuses sends while it runs; the reset counter keeps a cancel from dropping a refetch, and the hook's own replay on mount joins this one since upstream shares concurrent `resume()` calls.\n const enqueueResume = useCallback(\n () =>\n enqueueSend(async () => {\n const live = agentRef.current;\n if (live.session === undefined || !(\"resume\" in live)) return;\n await live.resume();\n }, resetEpochRef),\n [enqueueSend],\n );\n\n useEffect(() => {\n if (!resumesOnMount) return;\n // StrictMode's second setup queues the replay again, so only the setup that survives queues it.\n let active = true;\n queueMicrotask(() => {\n if (active) enqueueResume().catch(() => {});\n });\n return () => {\n active = false;\n };\n }, [enqueueResume, resumesOnMount]);\n\n useReplaySafeEffect(() => {\n if (!cloudThread?.isNew) return;\n return () => cloudThread.sessions.release(cloudThread.id);\n }, [cloudThread]);\n\n useEffect(() => {\n if (stagedInputsRef.current.size === 0) return;\n const baseIds = new Set(convertedMessages.map((message) => message.id));\n const remaining = messagesRef.current.filter(\n (message) =>\n stagedInputsRef.current.has(message.id) && !baseIds.has(message.id),\n );\n setStagedMessages(\n remaining.length > 0 ? [...convertedMessages, ...remaining] : null,\n );\n }, [convertedMessages]);\n\n const getStagedRun = (parentId: string | null) => {\n if (!parentId || !stagedInputsRef.current.has(parentId)) return null;\n const staged: {\n message: ThreadMessage;\n input: { message: AppendMessage; runConfig: AppendMessage[\"runConfig\"] };\n }[] = [];\n for (const message of messagesRef.current) {\n const input = stagedInputsRef.current.get(message.id);\n if (input) staged.push({ message, input });\n if (message.id === parentId) break;\n }\n return staged;\n };\n\n const stageUserMessage = (message: AppendMessage) => {\n const threadMessage = fromThreadMessageLike(\n message,\n generateId(),\n USER_STAGED_STATUS,\n );\n stagedInputsRef.current.set(threadMessage.id, {\n message,\n runConfig: message.runConfig,\n });\n const nextMessages = [...messagesRef.current, threadMessage];\n messagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n };\n\n const reset = useCallback(() => {\n // A cloud thread keeps the session its external id names, so a fresh session starts a new thread and leaves this one in the list.\n if (cloudThread) {\n aui.threads.switchToNewThread();\n return;\n }\n runtimeRef.current?.thread.unstable_notifySessionReset();\n // Sends parked behind an active turn captured the pre-reset epoch, so the\n // epoch has to advance before the session is torn down or they dispatch\n // into the new one; `lastFinishStatusRef` is run-scoped for the same\n // reason.\n sendEpochRef.current += 1;\n resetEpochRef.current += 1;\n lastFinishStatusRef.current = null;\n setStagedMessages(null);\n stagedInputsRef.current.clear();\n setToolStatuses({});\n agent.reset();\n }, [agent, aui, cloudThread]);\n\n const extras = useMemo(\n () =>\n eveExtras.provide({\n error: agent.error,\n events: agent.events,\n session: agent.session,\n reset,\n }),\n [agent.error, agent.events, agent.session, reset],\n );\n\n const runtime = useExternalStoreRuntime({\n ...pickExternalStoreSharedOptions(options),\n messages,\n isRunning: providerIsRunning,\n isLoading: agent.status === \"resuming\",\n extras,\n unstable_enableToolInvocations: true,\n setToolStatuses,\n adapters: {\n attachments: adapters?.attachments ?? runtimeAdapters?.attachments,\n speech: adapters?.speech,\n dictation: adapters?.dictation,\n voice: adapters?.voice,\n feedback: adapters?.feedback,\n },\n onNew: async (message) => {\n if (isSessionless)\n throw new MessageNotSentError(\n \"This thread has no eve session to send to.\",\n );\n if (!(message.startRun ?? message.role === \"user\")) {\n // Saving a new cloud thread waits for its first session, which a staged message never creates.\n if (cloudThread?.isNew)\n cloudThread.sessions.reject(\n cloudThread.id,\n new Error(\"The thread's first message was staged without a run.\"),\n );\n stageUserMessage(message);\n return;\n }\n try {\n await enqueueSend(() =>\n agent.send(\n getEveMessageContent(message),\n toEveSendOptions(message.runConfig),\n ),\n );\n } catch (error) {\n // A cancelled send never reached the session, so it rethrows for the\n // composer to take the draft back; an unmounted one has no composer\n // left to restore.\n if (error === sendAbandonedError) return;\n throw error;\n }\n },\n ...(stagedMessages\n ? {\n onReload: async (parentId: string | null, config) => {\n const stagedRun = getStagedRun(parentId);\n if (!stagedRun)\n throw new Error(\"Runtime does not support reloading messages.\");\n const epoch = sendEpochRef.current;\n const resetEpoch = resetEpochRef.current;\n for (const { message: stagedMessage, input } of stagedRun) {\n if (epoch !== sendEpochRef.current) return;\n const previousMessages = messagesRef.current;\n stagedInputsRef.current.delete(stagedMessage.id);\n const nextMessages = previousMessages.filter(\n (message) => message.id !== stagedMessage.id,\n );\n messagesRef.current = nextMessages;\n setStagedMessages(\n stagedInputsRef.current.size > 0 ? nextMessages : null,\n );\n // The reload config belongs to the message being reloaded; the\n // drafts promoted ahead of it keep the config they were staged\n // with, or reloading the tail would rewrite their context too.\n const runConfig =\n stagedMessage.id === parentId && hasRunConfig(config.runConfig)\n ? config.runConfig\n : input.runConfig;\n try {\n await enqueueSend(() =>\n agent.send(\n getEveMessageContent(input.message),\n toEveSendOptions(runConfig),\n ),\n );\n } catch (error) {\n // A reset discarded the draft along with the session, so\n // restoring it here would refloat a message the thread no\n // longer has. A cancel still restores.\n if (resetEpoch !== resetEpochRef.current) return;\n stagedInputsRef.current.set(stagedMessage.id, input);\n messagesRef.current = previousMessages;\n setStagedMessages(previousMessages);\n if (isDroppedSend(error)) return;\n throw error;\n }\n if (lastFinishStatusRef.current === \"error\") return;\n }\n },\n }\n : {}),\n onCancel: async () => {\n sendEpochRef.current += 1;\n // Eve 0.38 replaced the binding's local-abort `stop()` with the durable\n // `cancel()`, so the adapter detects which side of that break the host's\n // eve provides instead of pinning the peer range to one of them.\n const controls = agent as\n | { readonly cancel: () => Promise<unknown> }\n | { readonly stop: () => void };\n if (\"cancel\" in controls) {\n await controls.cancel();\n } else {\n controls.stop();\n }\n },\n // Hosts below eve 0.44.1 expose no `resume`; leaving the capability absent\n // keeps `threads.reloadMainThread()` on core's no-capability no-op.\n ...(\"resume\" in agent\n ? {\n onRefetchThread: () =>\n enqueueResume().catch((error) => {\n if (isDroppedSend(error)) return;\n throw error;\n }),\n }\n : {}),\n onRespondToToolApproval: (response) => {\n // Eve resolves a request the moment any response for it arrives, and an\n // empty one is recorded as an answer with no content. Mapping before the\n // send is enqueued keeps an unmappable response unsent, so the request\n // stays answerable; the mapper's error reaches the caller as the seam's\n // rejection.\n const inputResponse = toEveInputResponse(\n response,\n findEveInputRequest(agent.data, response.approvalId),\n );\n return enqueueSend(() => agent.respond([inputResponse])).catch(\n (error) => {\n if (!isDroppedSend(error)) throw error;\n },\n );\n },\n });\n runtimeRef.current = runtime;\n\n return runtime;\n};\n\nconst useEveCloudRuntime = (\n cloud: AssistantCloud,\n options: UseEveAgentRuntimeOptions,\n) => {\n const [sessions] = useState(createEveCloudSessions);\n const adapter = useCloudThreadListAdapter({\n cloud,\n sdk: EVE_SDK,\n upsert: true,\n create: async (threadId) => ({ externalId: await sessions.wait(threadId) }),\n });\n return useRemoteThreadListRuntime({\n adapter,\n allowNesting: true,\n runtimeHook: function RuntimeHook() {\n return useEveThreadRuntime(options, sessions);\n },\n });\n};\n\n/**\n * Connects Eve's `useEveAgent` hook to assistant-ui's runtime contract.\n *\n * The runtime renders Eve messages, forwards new user messages to the Eve\n * session, supports cancellation, and maps Eve input requests to assistant-ui\n * tool approval UI.\n */\nexport const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {\n const [hasCloud] = useState(options.cloud !== undefined);\n if ((options.cloud !== undefined) !== hasCloud) {\n throw new Error(\n \"useEveAgentRuntime cannot add or remove `cloud` after it mounts; remount it, for example with a `key`, to switch.\",\n );\n }\n if (!options.cloud) {\n // oxlint-disable-next-line react-hooks/rules-of-hooks -- `cloud` stays set or unset for the runtime's lifetime, so every render takes the same branch\n return useEveThreadRuntime(options, undefined);\n }\n // oxlint-disable-next-line react-hooks/rules-of-hooks -- `cloud` stays set or unset for the runtime's lifetime, so every render takes the same branch\n return useEveCloudRuntime(options.cloud, options);\n};\n"],"mappings":";;;;;;;;;;;;;AAyDA,MAAM,qBAAqB;CACzB,MAAM;CACN,QAAQ;AACV;AAEA,MAAM,qBAAqB,IAAI,oBAC7B,qDACF;AAEA,MAAM,qCAAqB,IAAI,MAC7B,qDACF;AAEA,MAAM,iBAAiB,UACrB,UAAU,sBAAsB,UAAU;AAQ5C,MAAM,mCACJ,MACA,UACG;CACH,QAAQ,MAAM,sBAAsB,KAAK,2BAA2B,KAAK;AAC3E;AAEA,MAAM,8BACJ,MACA,UACA,UACG;CACH,IAAI,CAAC,UAAU;CAEf,IAAI;EACF,QAAa,QAAQ,SAAS,KAAK,CAAC,CAAC,CAAC,OAAO,UAAU;GACrD,gCAAgC,MAAM,KAAK;EAC7C,CAAC;CACH,SAAS,OAAO;EACd,gCAAgC,MAAM,KAAK;CAC7C;AACF;AAEA,MAAM,gBACJ,cAEA,WAAW,WAAW,KAAA,KAAa,OAAO,KAAK,UAAU,MAAM,CAAC,CAAC,SAAS;;;;;;;AAyB5E,MAAM,oBACJ,cAEA,aAAa,SAAS,IAClB,EAAE,eAAe,UAAU,OAA2B,IACtD,KAAA;AA4BN,MAAM,0BACJ,EACE,eAAe,gBACf,gBAAgB,iBAChB,QAAQ,SACR,SAAS,UACT,GAAG,WAEL,cAEA,cAAc,KAAA,IACV,UACA;CACE,GAAG;CACH,gBAAgB;EAAE;EAAW,aAAa;CAAE;CAC5C,QAAQ;AACV;AAEN,MAAM,uBACJ,SACA,kBACG;CACH,MAAM,EACJ,UACA,OAAO,QACP,YAAY,aACZ,gBAAgB,iBAChB,aAAa,cACb,uBAAuB,wBACvB,GAAG,iBACD;CAMJ,MAAM,MAAM,OAAO;CACnB,MAAM,CAAC,eAAe,eAA2C;EAC/D,IAAI,CAAC,eAAe,OAAO,KAAA;EAC3B,MAAM,EAAE,IAAI,QAAQ,eAAe,IAAI,eAAe,SAAS;EAC/D,OAAO;GACL,UAAU;GACV;GACA,OAAO,WAAW;GAClB,WAAW;EACb;CACF,CAAC;CACD,MAAM,CAAC,kBAAkB,eACvB,cACI,YAAY,cAAc,KAAA,IAC1B,aAAa,WAAW,IAC9B;CACA,MAAM,gBACJ,gBAAgB,KAAA,KAChB,CAAC,YAAY,SACb,YAAY,cAAc,KAAA;CAE5B,MAAM,EAAE,SAAS,SAAS,UAAU,oBAAoB;CACxD,MAAM,sBAAsB,OAAiC,IAAI;CACjE,MAAM,QAAQ,YAAY;EACxB,GAAI,cACA,uBAAuB,cAAc,YAAY,SAAS,IAC1D;EACJ,GAAI,UACA,EACE,UAAU,UACR,2BAA2B,WAAW,SAAS,KAAK,EACxD,IACA,CAAC;EACL,GAAI,UACA,EACE,UAAU,UACR,2BAA2B,WAAW,SAAS,KAAK,EACxD,IACA,CAAC;EACL,WAAW,aAAa;GACtB,oBAAoB,UAAU,SAAS;GACvC,2BAA2B,YAAY,UAAU,QAAQ;EAC3D;EACA,GAAI,mBAAmB,aAAa,QAChC,EACE,kBAAkB,YAAY;GAC5B,IAAI,aAAa,SAAS,YAAY,KAAA,GACpC,YAAY,SAAS,OACnB,YAAY,oBACZ,IAAI,MAAM,iDAAiD,CAC7D;QACK,IAAI,aAAa,SAAS,YAAY,KAAA,GAAW;IACtD,YAAY,SAAS,QAAQ,YAAY,IAAI,QAAQ,SAAS;IAE9D,IAAI,IAAI,eAAe,SAAS,CAAC,CAAC,WAAW,OAC3C,IAAI,eAAe,WAAW,CAAC,CAAC,YAAY,CAAC,CAAC;GAClD;GACA,2BACE,mBACA,iBACA,OACF;EACF,EACF,IACA,CAAC;CACP,CAAC;CACD,IAAI,eAAe,EAAE,YAAY,QAC/B,MAAM,IAAI,MACR,+GACF;CAEF,MAAM,kBAAkB,mBAAmB;CAC3C,MAAM,CAAC,cAAc,mBAAmB,SAEtC,CAAC,CAAC;CACJ,MAAM,CAAC,gBAAgB,qBAAqB,SAC1C,IACF;CACA,MAAM,0BAA0B,uBAAO,IAAI,IAAkB,CAAC;CAC9D,MAAM,kBAAkB,uBACtB,IAAI,IAGF,CACJ;CAEA,MAAM,oBAAoB,OAAO,OAAO,YAAY,CAAC,CAAC,MACnD,WAAW,QAAQ,SAAS,WAC/B;CACA,MAAM,oBACJ,MAAM,WAAW,eAAe,MAAM,WAAW;CACnD,MAAM,YAAY,qBAAqB;CAIvC,MAAM,wBAAwB,OAAO,yBAAyB,CAAC;CAC/D,MAAM,iBAAiB,cACf,sBAAsB,MAAM,QAAQ,sBAAsB,OAAO,GACvE,CAAC,MAAM,MAAM,CACf;CAEA,MAAM,oBAAoB,cAAc;EACtC,MAAM,uBAAuB,wBAAwB;EACrD,MAAM,aAAa,IAAI,IACrB,MAAM,KAAK,SAAS,KAAK,YAAY,QAAQ,EAAE,CACjD;EACA,KAAK,MAAM,aAAa,qBAAqB,KAAK,GAChD,IAAI,CAAC,WAAW,IAAI,SAAS,GAAG,qBAAqB,OAAO,SAAS;EAGvE,OAAO,mBAAmB,MAAM,MAAM;GACpC;GACA,OAAO,MAAM;GACb,eAAe,YAAY;IACzB,MAAM,SAAS,QAAQ,UAAU;IACjC,MAAM,UACJ,WAAW,KAAA,IACP,KAAA,IACA,eAAe,IAAI,MAAM,CAAC,GAAG,QAAQ;IAC3C,IAAI,YAAY,KAAA,GAAW,OAAO;IAElC,MAAM,WAAW,qBAAqB,IAAI,QAAQ,EAAE;IACpD,IAAI,UAAU,OAAO;IAErB,MAAM,4BAAY,IAAI,KAAK;IAC3B,qBAAqB,IAAI,QAAQ,IAAI,SAAS;IAC9C,OAAO;GACT;EACF,CAAC;CACH,GAAG;EAAC,MAAM;EAAM,MAAM;EAAO;EAAW;CAAc,CAAC;CAEvD,MAAM,WAAW,kBAAkB;CACnC,MAAM,cAAc,OAAO,QAAQ;CACnC,MAAM,WAAW,OAAO,KAAK;CAG7B,yBAAyB;EACvB,YAAY,UAAU;EACtB,SAAS,UAAU;CACrB,GAAG,CAAC,OAAO,QAAQ,CAAC;CAMpB,MAAM,eAAe,OAAsB,QAAQ,QAAQ,CAAC;CAC5D,MAAM,eAAe,OAAO,CAAC;CAK7B,MAAM,gBAAgB,OAAO,CAAC;CAC9B,MAAM,eAAe,OAAO,IAAI;CAChC,MAAM,aAAa,OACjB,IACF;CAEA,MAAM,cAAc,aAEhB,UACA,WAAgC,iBAC7B;EACH,MAAM,QAAQ,SAAS;EACvB,MAAM,OAAO,aAAa,QAAQ,WAAW;GAC3C,IAAI,CAAC,aAAa,SAAS,MAAM;GACjC,IAAI,UAAU,SAAS,SAAS,MAAM;GACtC,OAAO,SAAS;EAClB,CAAC;EACD,aAAa,UAAU,KAAK,YAAY,CAAC,CAAC;EAC1C,OAAO;CACT,GACA,CAAC,CACH;CAMA,gBAAgB;EACd,aAAa,UAAU;EACvB,aAAa;GACX,aAAa,UAAU;EACzB;CACF,GAAG,CAAC,CAAC;CAGL,gCACc;EACV,aAAa,WAAW;CAC1B,GACA,CAAC,CACH;CAGA,MAAM,gBAAgB,kBAElB,YAAY,YAAY;EACtB,MAAM,OAAO,SAAS;EACtB,IAAI,KAAK,YAAY,KAAA,KAAa,EAAE,YAAY,OAAO;EACvD,MAAM,KAAK,OAAO;CACpB,GAAG,aAAa,GAClB,CAAC,WAAW,CACd;CAEA,gBAAgB;EACd,IAAI,CAAC,gBAAgB;EAErB,IAAI,SAAS;EACb,qBAAqB;GACnB,IAAI,QAAQ,cAAc,CAAC,CAAC,YAAY,CAAC,CAAC;EAC5C,CAAC;EACD,aAAa;GACX,SAAS;EACX;CACF,GAAG,CAAC,eAAe,cAAc,CAAC;CAElC,0BAA0B;EACxB,IAAI,CAAC,aAAa,OAAO;EACzB,aAAa,YAAY,SAAS,QAAQ,YAAY,EAAE;CAC1D,GAAG,CAAC,WAAW,CAAC;CAEhB,gBAAgB;EACd,IAAI,gBAAgB,QAAQ,SAAS,GAAG;EACxC,MAAM,UAAU,IAAI,IAAI,kBAAkB,KAAK,YAAY,QAAQ,EAAE,CAAC;EACtE,MAAM,YAAY,YAAY,QAAQ,QACnC,YACC,gBAAgB,QAAQ,IAAI,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,QAAQ,EAAE,CACtE;EACA,kBACE,UAAU,SAAS,IAAI,CAAC,GAAG,mBAAmB,GAAG,SAAS,IAAI,IAChE;CACF,GAAG,CAAC,iBAAiB,CAAC;CAEtB,MAAM,gBAAgB,aAA4B;EAChD,IAAI,CAAC,YAAY,CAAC,gBAAgB,QAAQ,IAAI,QAAQ,GAAG,OAAO;EAChE,MAAM,SAGA,CAAC;EACP,KAAK,MAAM,WAAW,YAAY,SAAS;GACzC,MAAM,QAAQ,gBAAgB,QAAQ,IAAI,QAAQ,EAAE;GACpD,IAAI,OAAO,OAAO,KAAK;IAAE;IAAS;GAAM,CAAC;GACzC,IAAI,QAAQ,OAAO,UAAU;EAC/B;EACA,OAAO;CACT;CAEA,MAAM,oBAAoB,YAA2B;EACnD,MAAM,gBAAgB,sBACpB,SACA,WAAW,GACX,kBACF;EACA,gBAAgB,QAAQ,IAAI,cAAc,IAAI;GAC5C;GACA,WAAW,QAAQ;EACrB,CAAC;EACD,MAAM,eAAe,CAAC,GAAG,YAAY,SAAS,aAAa;EAC3D,YAAY,UAAU;EACtB,kBAAkB,YAAY;CAChC;CAEA,MAAM,QAAQ,kBAAkB;EAE9B,IAAI,aAAa;GACf,IAAI,QAAQ,kBAAkB;GAC9B;EACF;EACA,WAAW,SAAS,OAAO,4BAA4B;EAKvD,aAAa,WAAW;EACxB,cAAc,WAAW;EACzB,oBAAoB,UAAU;EAC9B,kBAAkB,IAAI;EACtB,gBAAgB,QAAQ,MAAM;EAC9B,gBAAgB,CAAC,CAAC;EAClB,MAAM,MAAM;CACd,GAAG;EAAC;EAAO;EAAK;CAAW,CAAC;CAE5B,MAAM,SAAS,cAEX,UAAU,QAAQ;EAChB,OAAO,MAAM;EACb,QAAQ,MAAM;EACd,SAAS,MAAM;EACf;CACF,CAAC,GACH;EAAC,MAAM;EAAO,MAAM;EAAQ,MAAM;EAAS;CAAK,CAClD;CAEA,MAAM,UAAU,wBAAwB;EACtC,GAAG,+BAA+B,OAAO;EACzC;EACA,WAAW;EACX,WAAW,MAAM,WAAW;EAC5B;EACA,gCAAgC;EAChC;EACA,UAAU;GACR,aAAa,UAAU,eAAe,iBAAiB;GACvD,QAAQ,UAAU;GAClB,WAAW,UAAU;GACrB,OAAO,UAAU;GACjB,UAAU,UAAU;EACtB;EACA,OAAO,OAAO,YAAY;GACxB,IAAI,eACF,MAAM,IAAI,oBACR,4CACF;GACF,IAAI,EAAE,QAAQ,YAAY,QAAQ,SAAS,SAAS;IAElD,IAAI,aAAa,OACf,YAAY,SAAS,OACnB,YAAY,oBACZ,IAAI,MAAM,sDAAsD,CAClE;IACF,iBAAiB,OAAO;IACxB;GACF;GACA,IAAI;IACF,MAAM,kBACJ,MAAM,KACJ,qBAAqB,OAAO,GAC5B,iBAAiB,QAAQ,SAAS,CACpC,CACF;GACF,SAAS,OAAO;IAId,IAAI,UAAU,oBAAoB;IAClC,MAAM;GACR;EACF;EACA,GAAI,iBACA,EACE,UAAU,OAAO,UAAyB,WAAW;GACnD,MAAM,YAAY,aAAa,QAAQ;GACvC,IAAI,CAAC,WACH,MAAM,IAAI,MAAM,8CAA8C;GAChE,MAAM,QAAQ,aAAa;GAC3B,MAAM,aAAa,cAAc;GACjC,KAAK,MAAM,EAAE,SAAS,eAAe,WAAW,WAAW;IACzD,IAAI,UAAU,aAAa,SAAS;IACpC,MAAM,mBAAmB,YAAY;IACrC,gBAAgB,QAAQ,OAAO,cAAc,EAAE;IAC/C,MAAM,eAAe,iBAAiB,QACnC,YAAY,QAAQ,OAAO,cAAc,EAC5C;IACA,YAAY,UAAU;IACtB,kBACE,gBAAgB,QAAQ,OAAO,IAAI,eAAe,IACpD;IAIA,MAAM,YACJ,cAAc,OAAO,YAAY,aAAa,OAAO,SAAS,IAC1D,OAAO,YACP,MAAM;IACZ,IAAI;KACF,MAAM,kBACJ,MAAM,KACJ,qBAAqB,MAAM,OAAO,GAClC,iBAAiB,SAAS,CAC5B,CACF;IACF,SAAS,OAAO;KAId,IAAI,eAAe,cAAc,SAAS;KAC1C,gBAAgB,QAAQ,IAAI,cAAc,IAAI,KAAK;KACnD,YAAY,UAAU;KACtB,kBAAkB,gBAAgB;KAClC,IAAI,cAAc,KAAK,GAAG;KAC1B,MAAM;IACR;IACA,IAAI,oBAAoB,YAAY,SAAS;GAC/C;EACF,EACF,IACA,CAAC;EACL,UAAU,YAAY;GACpB,aAAa,WAAW;GAIxB,MAAM,WAAW;GAGjB,IAAI,YAAY,UACd,MAAM,SAAS,OAAO;QAEtB,SAAS,KAAK;EAElB;EAGA,GAAI,YAAY,QACZ,EACE,uBACE,cAAc,CAAC,CAAC,OAAO,UAAU;GAC/B,IAAI,cAAc,KAAK,GAAG;GAC1B,MAAM;EACR,CAAC,EACL,IACA,CAAC;EACL,0BAA0B,aAAa;GAMrC,MAAM,gBAAgB,mBACpB,UACA,oBAAoB,MAAM,MAAM,SAAS,UAAU,CACrD;GACA,OAAO,kBAAkB,MAAM,QAAQ,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,OACtD,UAAU;IACT,IAAI,CAAC,cAAc,KAAK,GAAG,MAAM;GACnC,CACF;EACF;CACF,CAAC;CACD,WAAW,UAAU;CAErB,OAAO;AACT;AAEA,MAAM,sBACJ,OACA,YACG;CACH,MAAM,CAAC,YAAY,SAAS,sBAAsB;CAClD,MAAM,UAAU,0BAA0B;EACxC;EACA,KAAK;EACL,QAAQ;EACR,QAAQ,OAAO,cAAc,EAAE,YAAY,MAAM,SAAS,KAAK,QAAQ,EAAE;CAC3E,CAAC;CACD,OAAO,2BAA2B;EAChC;EACA,cAAc;EACd,aAAa,SAAS,cAAc;GAClC,OAAO,oBAAoB,SAAS,QAAQ;EAC9C;CACF,CAAC;AACH;;;;;;;;AASA,MAAa,sBAAsB,UAAqC,CAAC,MAAM;CAC7E,MAAM,CAAC,YAAY,SAAS,QAAQ,UAAU,KAAA,CAAS;CACvD,IAAK,QAAQ,UAAU,KAAA,MAAe,UACpC,MAAM,IAAI,MACR,mHACF;CAEF,IAAI,CAAC,QAAQ,OAEX,OAAO,oBAAoB,SAAS,KAAA,CAAS;CAG/C,OAAO,mBAAmB,QAAQ,OAAO,OAAO;AAClD"}
1
+ {"version":3,"file":"useEveAgentRuntime.js","names":[],"sources":["../src/useEveAgentRuntime.ts"],"sourcesContent":["\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useInsertionEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport {\n fromThreadMessageLike,\n generateId,\n MessageNotSentError,\n pickExternalStoreSharedOptions,\n type AppendMessage,\n type AttachmentAdapter,\n type DictationAdapter,\n type ExternalStoreSharedOptions,\n type FeedbackAdapter,\n type RealtimeVoiceAdapter,\n type SpeechSynthesisAdapter,\n type ThreadMessage,\n type ToolExecutionStatus,\n} from \"@assistant-ui/core\";\nimport {\n useCloudThreadListAdapter,\n useExternalStoreRuntime,\n useRemoteThreadListRuntime,\n useRuntimeAdapters,\n} from \"@assistant-ui/core/react\";\nimport { useReplaySafeEffect } from \"@assistant-ui/store/internal\";\nimport { useAui } from \"@assistant-ui/store\";\nimport type { AssistantCloud } from \"assistant-cloud\";\nimport {\n useEveAgent,\n type EveMessageData,\n type UseEveAgentOptions,\n type UseEveAgentStatus,\n} from \"eve/react\";\nimport {\n collectInterruptedTurnEvents,\n convertEveMessages,\n findEveInputRequest,\n getEveMessageContent,\n toEveInputResponse,\n type InterruptedTurnEventCache,\n} from \"./convertEveMessages\";\nimport {\n collectTurnTimestamps,\n createTurnTimestampCache,\n} from \"./deriveCreatedAt\";\nimport {\n createEveCloudSessions,\n type EveCloudSessions,\n} from \"./eveCloudSessions\";\nimport { eveExtras } from \"./eveExtras\";\nimport { EVE_SDK } from \"./sdkIdentity\";\n\nconst USER_STAGED_STATUS = {\n type: \"complete\",\n reason: \"unknown\",\n} as const;\n\nconst sendCancelledError = new MessageNotSentError(\n \"eve send was dropped because the run was cancelled.\",\n);\n\nconst sendAbandonedError = new Error(\n \"eve send was dropped because the runtime unmounted.\",\n);\n\nconst isDroppedSend = (error: unknown) =>\n error === sendCancelledError || error === sendAbandonedError;\n\ntype EveLifecycleCallbackName =\n | \"onError\"\n | \"onEvent\"\n | \"onFinish\"\n | \"onSessionChange\";\n\nconst reportEveLifecycleCallbackError = (\n name: EveLifecycleCallbackName,\n error: unknown,\n) => {\n console.error(`[assistant-ui/eve] ${name} callback threw an error`, error);\n};\n\nconst invokeEveLifecycleCallback = <T>(\n name: EveLifecycleCallbackName,\n callback: ((value: T) => unknown) | undefined,\n value: T,\n) => {\n if (!callback) return;\n\n try {\n void Promise.resolve(callback(value)).catch((error) => {\n reportEveLifecycleCallbackError(name, error);\n });\n } catch (error) {\n reportEveLifecycleCallbackError(name, error);\n }\n};\n\nconst hasRunConfig = (\n runConfig: AppendMessage[\"runConfig\"],\n): runConfig is NonNullable<AppendMessage[\"runConfig\"]> =>\n runConfig?.custom !== undefined && Object.keys(runConfig.custom).length > 0;\n\ntype EveJsonValue =\n | string\n | number\n | boolean\n | null\n | readonly EveJsonValue[]\n | { readonly [key: string]: EveJsonValue };\n\n/**\n * Structural equivalent of eve's `JsonObject`. Eve demoted its public send\n * payload type once already (`SendTurnPayload`, public through 0.30, internal\n * from 0.31), so the helpers declare the shapes they forward instead of\n * re-exporting eve's option types, and assignability is checked at the send\n * call sites against the installed version.\n */\ntype EveClientContext = { readonly [key: string]: EveJsonValue };\n\n/**\n * Only the `custom` bag crosses the wire. Eve reads `clientContext` as its own\n * namespace and serializes it into a model-visible context message, so sending\n * the assistant-ui envelope would surface a literal `\"custom\"` key in the\n * prompt and to every eve-side handler.\n */\nconst toEveSendOptions = (\n runConfig: AppendMessage[\"runConfig\"],\n): { readonly clientContext: EveClientContext } | undefined =>\n hasRunConfig(runConfig)\n ? { clientContext: runConfig.custom as EveClientContext }\n : undefined;\n\ntype EveAgentOptions = Omit<UseEveAgentOptions<EveMessageData>, \"reducer\">;\n\nexport type UseEveAgentRuntimeOptions = EveAgentOptions &\n ExternalStoreSharedOptions & {\n readonly adapters?:\n | {\n readonly attachments?: AttachmentAdapter | undefined;\n readonly speech?: SpeechSynthesisAdapter | undefined;\n readonly dictation?: DictationAdapter | undefined;\n readonly voice?: RealtimeVoiceAdapter | undefined;\n readonly feedback?: FeedbackAdapter | undefined;\n }\n | undefined;\n /**\n * Backs the thread list with Assistant Cloud. Each cloud thread keeps one Eve session as its external id, saved once the thread's first turn creates the session, and opening a thread resumes it, so `session`, `initialSession`, `initialEvents`, and `resume` are not used. Pass it from the first render: adding or removing it later throws, so remount the runtime, for example with a `key`, to switch.\n */\n readonly cloud?: AssistantCloud | undefined;\n };\n\ntype EveCloudThread = {\n readonly sessions: EveCloudSessions;\n readonly id: string;\n readonly isNew: boolean;\n readonly sessionId: string | undefined;\n};\n\nconst withCloudThreadSession = (\n {\n initialEvents: _initialEvents,\n initialSession: _initialSession,\n resume: _resume,\n session: _session,\n ...options\n }: EveAgentOptions,\n sessionId: string | undefined,\n): EveAgentOptions =>\n sessionId === undefined\n ? options\n : {\n ...options,\n initialSession: { sessionId, streamIndex: 0 },\n resume: true,\n };\n\nconst useEveThreadRuntime = (\n options: UseEveAgentRuntimeOptions,\n cloudSessions: EveCloudSessions | undefined,\n) => {\n const {\n adapters,\n cloud: _cloud,\n isDisabled: _isDisabled,\n isSendDisabled: _isSendDisabled,\n suggestions: _suggestions,\n unstable_capabilities: _unstable_capabilities,\n ...agentOptions\n } = options;\n true satisfies keyof typeof agentOptions &\n keyof ExternalStoreSharedOptions extends never\n ? true\n : never;\n\n const aui = useAui();\n const [cloudThread] = useState((): EveCloudThread | undefined => {\n if (!cloudSessions) return undefined;\n const { id, status, externalId } = aui.threadListItem.getState();\n return {\n sessions: cloudSessions,\n id,\n isNew: status === \"new\",\n sessionId: externalId,\n };\n });\n const [resumesOnMount] = useState(() =>\n cloudThread\n ? cloudThread.sessionId !== undefined\n : agentOptions.resume === true,\n );\n const isSessionless =\n cloudThread !== undefined &&\n !cloudThread.isNew &&\n cloudThread.sessionId === undefined;\n\n const { onError, onEvent, onFinish, onSessionChange } = agentOptions;\n const lastFinishStatusRef = useRef<UseEveAgentStatus | null>(null);\n const agent = useEveAgent({\n ...(cloudThread\n ? withCloudThreadSession(agentOptions, cloudThread.sessionId)\n : agentOptions),\n ...(onError\n ? {\n onError: (error) =>\n invokeEveLifecycleCallback(\"onError\", onError, error),\n }\n : {}),\n ...(onEvent\n ? {\n onEvent: (event) =>\n invokeEveLifecycleCallback(\"onEvent\", onEvent, event),\n }\n : {}),\n onFinish: (snapshot) => {\n lastFinishStatusRef.current = snapshot.status;\n invokeEveLifecycleCallback(\"onFinish\", onFinish, snapshot);\n },\n ...(onSessionChange || cloudThread?.isNew\n ? {\n onSessionChange: (session) => {\n if (cloudThread?.isNew && session === undefined) {\n cloudThread.sessions.reject(\n cloudThread.id,\n new Error(\"The eve turn ended before it created a session.\"),\n );\n } else if (cloudThread?.isNew && session !== undefined) {\n cloudThread.sessions.resolve(cloudThread.id, session.sessionId);\n // A first message staged without a run leaves the thread unsaved, so the run that creates the session saves it.\n if (aui.threadListItem.getState().status === \"new\")\n aui.threadListItem.initialize().catch(() => {});\n }\n invokeEveLifecycleCallback(\n \"onSessionChange\",\n onSessionChange,\n session,\n );\n },\n }\n : {}),\n });\n if (cloudThread && !(\"resume\" in agent)) {\n throw new Error(\n \"useEveAgentRuntime needs eve 0.44.1 or later for `cloud`, because opening a cloud thread resumes its session.\",\n );\n }\n const runtimeAdapters = useRuntimeAdapters();\n const [toolStatuses, setToolStatuses] = useState<\n Record<string, ToolExecutionStatus>\n >({});\n const [stagedMessages, setStagedMessages] = useState<ThreadMessage[] | null>(\n null,\n );\n const createdAtByMessageIdRef = useRef(new Map<string, Date>());\n const stagedInputsRef = useRef(\n new Map<\n string,\n { message: AppendMessage; runConfig: AppendMessage[\"runConfig\"] }\n >(),\n );\n\n const hasExecutingTools = Object.values(toolStatuses).some(\n (status) => status?.type === \"executing\",\n );\n const providerIsRunning =\n agent.status === \"submitted\" || agent.status === \"streaming\";\n const isRunning = providerIsRunning || hasExecutingTools;\n\n // Kept apart from the message memo so events that teach no timestamp keep\n // the map identity and skip rebuilding every ThreadMessage.\n const turnTimestampCacheRef = useRef(createTurnTimestampCache());\n const turnTimestamps = useMemo(\n () => collectTurnTimestamps(agent.events, turnTimestampCacheRef.current),\n [agent.events],\n );\n const interruptionCacheRef = useRef<InterruptedTurnEventCache>({\n lastEvents: [],\n interruptions: [],\n });\n const interruptionEvents = useMemo(\n () =>\n collectInterruptedTurnEvents(agent.events, interruptionCacheRef.current),\n [agent.events],\n );\n\n const convertedMessages = useMemo(() => {\n const createdAtByMessageId = createdAtByMessageIdRef.current;\n const messageIds = new Set(\n agent.data.messages.map((message) => message.id),\n );\n for (const messageId of createdAtByMessageId.keys()) {\n if (!messageIds.has(messageId)) createdAtByMessageId.delete(messageId);\n }\n\n return convertEveMessages(agent.data, {\n isRunning,\n error: agent.error,\n events: interruptionEvents,\n getCreatedAt: (message) => {\n const turnId = message.metadata?.turnId;\n const durable =\n turnId === undefined\n ? undefined\n : turnTimestamps.get(turnId)?.[message.role];\n if (durable !== undefined) return durable;\n\n const existing = createdAtByMessageId.get(message.id);\n if (existing) return existing;\n\n const createdAt = new Date();\n createdAtByMessageId.set(message.id, createdAt);\n return createdAt;\n },\n });\n }, [agent.data, agent.error, interruptionEvents, isRunning, turnTimestamps]);\n\n const messages = stagedMessages ?? convertedMessages;\n const messagesRef = useRef(messages);\n const agentRef = useRef(agent);\n // Descendant layout effects can dispatch against these refs synchronously;\n // this matches useA2ARuntime's commit-scoped ref publication.\n useInsertionEffect(() => {\n messagesRef.current = messages;\n agentRef.current = agent;\n }, [agent, messages]);\n\n // Upstream `EveAgentStore` `send` and `respond` reject while a turn is in\n // flight and only resolve once the turn's stream parks, so a pending chain\n // link is exactly an active turn; chaining every dispatch serializes them\n // without watching status.\n const sendChainRef = useRef<Promise<void>>(Promise.resolve());\n const sendEpochRef = useRef(0);\n // A cancel drops the queued send but keeps its draft for a later promotion;\n // only a reset discards the draft with the session, so the two need separate\n // counters. A queued read has no draft and outlives a cancel, so it waits on\n // the reset counter instead.\n const resetEpochRef = useRef(0);\n const isMountedRef = useRef(true);\n const runtimeRef = useRef<ReturnType<typeof useExternalStoreRuntime> | null>(\n null,\n );\n\n const enqueueSend = useCallback(\n (\n dispatch: () => Promise<void>,\n epochRef: { current: number } = sendEpochRef,\n ) => {\n const epoch = epochRef.current;\n const next = sendChainRef.current.then(() => {\n if (!isMountedRef.current) throw sendAbandonedError;\n if (epoch !== epochRef.current) throw sendCancelledError;\n return dispatch();\n });\n sendChainRef.current = next.catch(() => {});\n return next;\n },\n [],\n );\n\n // The store outlives the component (useEveAgent holds it in a ref with no\n // cleanup), so queued sends must not fire server turns after unmount. The\n // flag separates that teardown from a user cancel, and is re-armed in setup\n // for a remounted tree.\n useEffect(() => {\n isMountedRef.current = true;\n return () => {\n isMountedRef.current = false;\n };\n }, []);\n\n // A replay (Fast Refresh, StrictMode) must not cancel the sends queued behind the active turn.\n useReplaySafeEffect(\n () => () => {\n sendEpochRef.current += 1;\n },\n [],\n );\n\n // A replay rides the send chain because `resume()` rejects during a turn and upstream refuses sends while it runs; the reset counter keeps a cancel from dropping a refetch, and the hook's own replay on mount joins this one since upstream shares concurrent `resume()` calls.\n const enqueueResume = useCallback(\n () =>\n enqueueSend(async () => {\n const live = agentRef.current;\n if (live.session === undefined || !(\"resume\" in live)) return;\n await live.resume();\n }, resetEpochRef),\n [enqueueSend],\n );\n\n useEffect(() => {\n if (!resumesOnMount) return;\n // StrictMode's second setup queues the replay again, so only the setup that survives queues it.\n let active = true;\n queueMicrotask(() => {\n if (active) enqueueResume().catch(() => {});\n });\n return () => {\n active = false;\n };\n }, [enqueueResume, resumesOnMount]);\n\n useReplaySafeEffect(() => {\n if (!cloudThread?.isNew) return;\n return () => cloudThread.sessions.release(cloudThread.id);\n }, [cloudThread]);\n\n useEffect(() => {\n if (stagedInputsRef.current.size === 0) return;\n const baseIds = new Set(convertedMessages.map((message) => message.id));\n const remaining = messagesRef.current.filter(\n (message) =>\n stagedInputsRef.current.has(message.id) && !baseIds.has(message.id),\n );\n setStagedMessages(\n remaining.length > 0 ? [...convertedMessages, ...remaining] : null,\n );\n }, [convertedMessages]);\n\n const getStagedRun = (parentId: string | null) => {\n if (!parentId || !stagedInputsRef.current.has(parentId)) return null;\n const staged: {\n message: ThreadMessage;\n input: { message: AppendMessage; runConfig: AppendMessage[\"runConfig\"] };\n }[] = [];\n for (const message of messagesRef.current) {\n const input = stagedInputsRef.current.get(message.id);\n if (input) staged.push({ message, input });\n if (message.id === parentId) break;\n }\n return staged;\n };\n\n const stageUserMessage = (message: AppendMessage) => {\n const threadMessage = fromThreadMessageLike(\n message,\n generateId(),\n USER_STAGED_STATUS,\n );\n stagedInputsRef.current.set(threadMessage.id, {\n message,\n runConfig: message.runConfig,\n });\n const nextMessages = [...messagesRef.current, threadMessage];\n messagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n };\n\n const reset = useCallback(() => {\n // A cloud thread keeps the session its external id names, so a fresh session starts a new thread and leaves this one in the list.\n if (cloudThread) {\n aui.threads.switchToNewThread();\n return;\n }\n runtimeRef.current?.thread.unstable_notifySessionReset();\n // Sends parked behind an active turn captured the pre-reset epoch, so the\n // epoch has to advance before the session is torn down or they dispatch\n // into the new one; `lastFinishStatusRef` is run-scoped for the same\n // reason.\n sendEpochRef.current += 1;\n resetEpochRef.current += 1;\n lastFinishStatusRef.current = null;\n setStagedMessages(null);\n stagedInputsRef.current.clear();\n setToolStatuses({});\n agent.reset();\n }, [agent, aui, cloudThread]);\n\n const extras = useMemo(\n () =>\n eveExtras.provide({\n error: agent.error,\n events: agent.events,\n session: agent.session,\n reset,\n }),\n [agent.error, agent.events, agent.session, reset],\n );\n\n const runtime = useExternalStoreRuntime({\n ...pickExternalStoreSharedOptions(options),\n messages,\n isRunning: providerIsRunning,\n isLoading: agent.status === \"resuming\",\n extras,\n unstable_enableToolInvocations: true,\n setToolStatuses,\n adapters: {\n attachments: adapters?.attachments ?? runtimeAdapters?.attachments,\n speech: adapters?.speech,\n dictation: adapters?.dictation,\n voice: adapters?.voice,\n feedback: adapters?.feedback,\n },\n onNew: async (message) => {\n if (isSessionless)\n throw new MessageNotSentError(\n \"This thread has no eve session to send to.\",\n );\n if (!(message.startRun ?? message.role === \"user\")) {\n // Saving a new cloud thread waits for its first session, which a staged message never creates.\n if (cloudThread?.isNew)\n cloudThread.sessions.reject(\n cloudThread.id,\n new Error(\"The thread's first message was staged without a run.\"),\n );\n stageUserMessage(message);\n return;\n }\n try {\n await enqueueSend(() =>\n agent.send(\n getEveMessageContent(message),\n toEveSendOptions(message.runConfig),\n ),\n );\n } catch (error) {\n // A cancelled send never reached the session, so it rethrows for the\n // composer to take the draft back; an unmounted one has no composer\n // left to restore.\n if (error === sendAbandonedError) return;\n throw error;\n }\n },\n ...(stagedMessages\n ? {\n onReload: async (parentId: string | null, config) => {\n const stagedRun = getStagedRun(parentId);\n if (!stagedRun)\n throw new Error(\"Runtime does not support reloading messages.\");\n const epoch = sendEpochRef.current;\n const resetEpoch = resetEpochRef.current;\n for (const { message: stagedMessage, input } of stagedRun) {\n if (epoch !== sendEpochRef.current) return;\n const previousMessages = messagesRef.current;\n stagedInputsRef.current.delete(stagedMessage.id);\n const nextMessages = previousMessages.filter(\n (message) => message.id !== stagedMessage.id,\n );\n messagesRef.current = nextMessages;\n setStagedMessages(\n stagedInputsRef.current.size > 0 ? nextMessages : null,\n );\n // The reload config belongs to the message being reloaded; the\n // drafts promoted ahead of it keep the config they were staged\n // with, or reloading the tail would rewrite their context too.\n const runConfig =\n stagedMessage.id === parentId && hasRunConfig(config.runConfig)\n ? config.runConfig\n : input.runConfig;\n try {\n await enqueueSend(() =>\n agent.send(\n getEveMessageContent(input.message),\n toEveSendOptions(runConfig),\n ),\n );\n } catch (error) {\n // A reset discarded the draft along with the session, so\n // restoring it here would refloat a message the thread no\n // longer has. A cancel still restores.\n if (resetEpoch !== resetEpochRef.current) return;\n stagedInputsRef.current.set(stagedMessage.id, input);\n messagesRef.current = previousMessages;\n setStagedMessages(previousMessages);\n if (isDroppedSend(error)) return;\n throw error;\n }\n if (lastFinishStatusRef.current === \"error\") return;\n }\n },\n }\n : {}),\n onCancel: async () => {\n sendEpochRef.current += 1;\n // Eve 0.38 replaced the binding's local-abort `stop()` with the durable\n // `cancel()`, so the adapter detects which side of that break the host's\n // eve provides instead of pinning the peer range to one of them.\n const controls = agent as\n | { readonly cancel: () => Promise<unknown> }\n | { readonly stop: () => void };\n if (\"cancel\" in controls) {\n await controls.cancel();\n } else {\n controls.stop();\n }\n },\n // Hosts below eve 0.44.1 expose no `resume`; leaving the capability absent\n // keeps `threads.reloadMainThread()` on core's no-capability no-op.\n ...(\"resume\" in agent\n ? {\n onRefetchThread: () =>\n enqueueResume().catch((error) => {\n if (isDroppedSend(error)) return;\n throw error;\n }),\n }\n : {}),\n onRespondToToolApproval: (response) => {\n // Eve resolves a request the moment any response for it arrives, and an\n // empty one is recorded as an answer with no content. Mapping before the\n // send is enqueued keeps an unmappable response unsent, so the request\n // stays answerable; the mapper's error reaches the caller as the seam's\n // rejection.\n const inputResponse = toEveInputResponse(\n response,\n findEveInputRequest(agent.data, response.approvalId),\n );\n return enqueueSend(() => agent.respond([inputResponse])).catch(\n (error) => {\n if (!isDroppedSend(error)) throw error;\n },\n );\n },\n });\n runtimeRef.current = runtime;\n\n return runtime;\n};\n\nconst useEveCloudRuntime = (\n cloud: AssistantCloud,\n options: UseEveAgentRuntimeOptions,\n) => {\n const [sessions] = useState(createEveCloudSessions);\n const adapter = useCloudThreadListAdapter({\n cloud,\n sdk: EVE_SDK,\n upsert: true,\n create: async (threadId) => ({ externalId: await sessions.wait(threadId) }),\n });\n return useRemoteThreadListRuntime({\n adapter,\n allowNesting: true,\n runtimeHook: function RuntimeHook() {\n return useEveThreadRuntime(options, sessions);\n },\n });\n};\n\n/**\n * Connects Eve's `useEveAgent` hook to assistant-ui's runtime contract.\n *\n * The runtime renders Eve messages, forwards new user messages to the Eve\n * session, supports cancellation, and maps Eve input requests to assistant-ui\n * tool approval UI.\n */\nexport const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {\n const [hasCloud] = useState(options.cloud !== undefined);\n if ((options.cloud !== undefined) !== hasCloud) {\n throw new Error(\n \"useEveAgentRuntime cannot add or remove `cloud` after it mounts; remount it, for example with a `key`, to switch.\",\n );\n }\n if (!options.cloud) {\n // oxlint-disable-next-line react-hooks/rules-of-hooks -- `cloud` stays set or unset for the runtime's lifetime, so every render takes the same branch\n return useEveThreadRuntime(options, undefined);\n }\n // oxlint-disable-next-line react-hooks/rules-of-hooks -- `cloud` stays set or unset for the runtime's lifetime, so every render takes the same branch\n return useEveCloudRuntime(options.cloud, options);\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/eve",
3
- "version": "0.0.21",
3
+ "version": "0.0.22",
4
4
  "description": "Eve runtime adapter for assistant-ui",
5
5
  "keywords": [
6
6
  "eve",
@@ -28,7 +28,7 @@
28
28
  ],
29
29
  "sideEffects": false,
30
30
  "dependencies": {
31
- "@assistant-ui/core": "^0.3.23",
31
+ "@assistant-ui/core": "^0.3.24",
32
32
  "@assistant-ui/store": "^0.3.17",
33
33
  "assistant-cloud": "*"
34
34
  },
@@ -48,7 +48,7 @@
48
48
  "@testing-library/dom": "^10.4.2",
49
49
  "@testing-library/react": "^16.3.3",
50
50
  "@types/react": "^19.3.0",
51
- "eve": "^0.56.0",
51
+ "eve": "^0.70.1",
52
52
  "jsdom": "^30.0.1",
53
53
  "react": "^19.3.0",
54
54
  "react-dom": "^19.3.0",
@@ -70,6 +70,7 @@
70
70
  "scripts": {
71
71
  "build": "aui-build",
72
72
  "test": "vitest run",
73
+ "test:coverage": "vitest run --coverage --coverage.reporter=text --coverage.reporter=html --coverage.reporter=json-summary",
73
74
  "typecheck": "tsc --noEmit",
74
75
  "test:peer-react18": "vitest run --config vitest.peer-react18.config.ts",
75
76
  "test:watch": "vitest",
@@ -1,8 +1,14 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import type { EveMessageData, EveMessageInputRequest } from "eve/react";
3
- import { defaultMessageReducer, type EveAgentReducerEvent } from "eve/client";
4
3
  import {
4
+ defaultMessageReducer,
5
+ type EveAgentReducerEvent,
6
+ type MessageStreamEvent,
7
+ } from "eve/client";
8
+ import {
9
+ collectInterruptedTurnEvents,
5
10
  convertEveMessages,
11
+ type InterruptedTurnEventCache,
6
12
  findEveInputRequest,
7
13
  getEveMessageContent,
8
14
  toEveInputResponse,
@@ -1742,28 +1748,49 @@ describe("convertEveMessages", () => {
1742
1748
  });
1743
1749
  });
1744
1750
 
1745
- it("a failed turn converts to cancelled because the store surfaces no error for turn.failed", () => {
1746
- const state = replay([
1747
- ...midStreamEvents,
1748
- {
1749
- type: "turn.failed",
1750
- meta: eventMeta(3),
1751
- data: {
1752
- turnId: "turn_1",
1753
- sequence: 3,
1754
- code: "internal",
1755
- message: "boom",
1756
- },
1751
+ it("a failed turn stays incomplete after Eve settles its message", () => {
1752
+ const failureEvent = {
1753
+ type: "turn.failed",
1754
+ meta: eventMeta(3),
1755
+ data: {
1756
+ turnId: "turn_1",
1757
+ sequence: 3,
1758
+ code: "internal",
1759
+ message: "boom",
1757
1760
  },
1758
- ]);
1761
+ } as const satisfies MessageStreamEvent;
1762
+ const state = replay([...midStreamEvents, failureEvent]);
1759
1763
 
1760
- const converted = convertEveMessages(state, { isRunning: false });
1764
+ const converted = convertEveMessages(state, {
1765
+ isRunning: false,
1766
+ events: [failureEvent],
1767
+ });
1761
1768
  expect(converted.at(-1)?.status).toEqual({
1762
1769
  type: "incomplete",
1763
- reason: "cancelled",
1770
+ reason: "error",
1771
+ error: { code: "internal", message: "boom" },
1764
1772
  });
1765
1773
  });
1766
1774
 
1775
+ it("a cancelled turn stays cancelled after Eve settles its message", () => {
1776
+ const cancelEvent = {
1777
+ type: "turn.cancelled",
1778
+ meta: eventMeta(3),
1779
+ data: { turnId: "turn_1", sequence: 3 },
1780
+ } as const satisfies MessageStreamEvent;
1781
+ const state = replay([...midStreamEvents, cancelEvent]);
1782
+
1783
+ expect(
1784
+ convertEveMessages(state, { isRunning: false }).at(-1)?.status,
1785
+ ).toEqual({ type: "complete", reason: "stop" });
1786
+ expect(
1787
+ convertEveMessages(state, {
1788
+ isRunning: false,
1789
+ events: [cancelEvent],
1790
+ }).at(-1)?.status,
1791
+ ).toEqual({ type: "incomplete", reason: "cancelled" });
1792
+ });
1793
+
1767
1794
  it("a completed turn terminalizes the streaming marker and converts to complete", () => {
1768
1795
  const state = replay([
1769
1796
  ...midStreamEvents,
@@ -2295,3 +2322,49 @@ describe("findEveInputRequest", () => {
2295
2322
  ).toBeUndefined();
2296
2323
  });
2297
2324
  });
2325
+
2326
+ describe("collectInterruptedTurnEvents", () => {
2327
+ const started = (turnId: string, sequence: number) =>
2328
+ ({
2329
+ type: "turn.started",
2330
+ meta: eventMeta(sequence),
2331
+ data: { turnId, sequence },
2332
+ }) as const satisfies MessageStreamEvent;
2333
+ const failed = (turnId: string, sequence: number) =>
2334
+ ({
2335
+ type: "turn.failed",
2336
+ meta: eventMeta(sequence),
2337
+ data: { turnId, sequence, code: "internal", message: "boom" },
2338
+ }) as const satisfies MessageStreamEvent;
2339
+ const cancelled = (turnId: string, sequence: number) =>
2340
+ ({
2341
+ type: "turn.cancelled",
2342
+ meta: eventMeta(sequence),
2343
+ data: { turnId, sequence },
2344
+ }) as const satisfies MessageStreamEvent;
2345
+
2346
+ it("scans only appended events and keeps the array until an interruption arrives", () => {
2347
+ const cache: InterruptedTurnEventCache = {
2348
+ lastEvents: [],
2349
+ interruptions: [],
2350
+ };
2351
+ const first = [started("t1", 0), failed("t1", 1)];
2352
+ const interruptions = collectInterruptedTurnEvents(first, cache);
2353
+ expect(interruptions).toEqual([first[1]]);
2354
+ const quiet = [...cache.lastEvents, started("t2", 2)];
2355
+ expect(collectInterruptedTurnEvents(quiet, cache)).toBe(interruptions);
2356
+ const appended = [...cache.lastEvents, cancelled("t2", 3)];
2357
+ const next = collectInterruptedTurnEvents(appended, cache);
2358
+ expect(next).toEqual([first[1], appended[3]]);
2359
+ expect(next[0]).toBe(interruptions[0]);
2360
+ });
2361
+
2362
+ it("rescans a log that does not extend the scanned one", () => {
2363
+ const cache: InterruptedTurnEventCache = {
2364
+ lastEvents: [],
2365
+ interruptions: [],
2366
+ };
2367
+ collectInterruptedTurnEvents([started("t1", 0), failed("t1", 1)], cache);
2368
+ expect(collectInterruptedTurnEvents([started("t2", 0)], cache)).toEqual([]);
2369
+ });
2370
+ });
@@ -31,6 +31,7 @@ import type {
31
31
  EveMessagePart,
32
32
  } from "eve/react";
33
33
  import type { InputResponse } from "eve/client";
34
+ import type { MessageStreamEvent } from "eve/client";
34
35
 
35
36
  const ASSISTANT_COMPLETE_STATUS = {
36
37
  type: "complete",
@@ -64,6 +65,12 @@ export type ConvertEveMessagesOptions = {
64
65
  */
65
66
  readonly error?: unknown;
66
67
  readonly getCreatedAt?: ((message: EveMessage) => Date) | undefined;
68
+ /**
69
+ * The session's stream events. A `turn.failed` or `turn.cancelled` event
70
+ * marks its turn's assistant message as incomplete, with a failure's code
71
+ * and message as its error.
72
+ */
73
+ readonly events?: readonly MessageStreamEvent[] | undefined;
67
74
  };
68
75
 
69
76
  const isRecord = (value: unknown): value is Record<string, unknown> =>
@@ -88,9 +95,16 @@ const toMessageStatus = (
88
95
  index: number,
89
96
  messages: readonly EveMessage[],
90
97
  options: ConvertEveMessagesOptions,
98
+ interruptedTurns: ReadonlyMap<string, MessageStatus>,
91
99
  ): MessageStatus => {
92
100
  if (message.role !== "assistant") return USER_FALLBACK_STATUS;
93
101
 
102
+ const interrupted =
103
+ message.metadata?.turnId === undefined
104
+ ? undefined
105
+ : interruptedTurns.get(message.metadata.turnId);
106
+ if (interrupted) return interrupted;
107
+
94
108
  const isLast = index === messages.length - 1;
95
109
  const hasPendingApproval = message.parts.some(
96
110
  (part) =>
@@ -123,9 +137,7 @@ const toMessageStatus = (
123
137
  return ASSISTANT_RUNNING_STATUS;
124
138
  }
125
139
 
126
- // Eve's default reducer never terminalizes the "streaming" marker on
127
- // cancellation or turn/session failure, so liveness comes from isRunning and
128
- // a leftover marker means the turn was interrupted.
140
+ // Some eve versions leave the streaming marker after cancellation or failure.
129
141
  if (message.metadata?.status === "streaming") {
130
142
  if (options.isRunning === undefined) {
131
143
  return ASSISTANT_RUNNING_STATUS;
@@ -469,6 +481,21 @@ export const convertEveMessage = (
469
481
  index: number,
470
482
  messages: readonly EveMessage[],
471
483
  options: ConvertEveMessagesOptions = {},
484
+ ): ThreadMessage =>
485
+ convertEveMessageWithInterruptions(
486
+ message,
487
+ index,
488
+ messages,
489
+ options,
490
+ getInterruptedTurns(options.events),
491
+ );
492
+
493
+ const convertEveMessageWithInterruptions = (
494
+ message: EveMessage,
495
+ index: number,
496
+ messages: readonly EveMessage[],
497
+ options: ConvertEveMessagesOptions,
498
+ interruptedTurns: ReadonlyMap<string, MessageStatus>,
472
499
  ): ThreadMessage => {
473
500
  const createdAt = options.getCreatedAt?.(message) ?? new Date();
474
501
  const metadata = {
@@ -501,20 +528,81 @@ export const convertEveMessage = (
501
528
  return fromThreadMessageLike(
502
529
  like,
503
530
  message.id,
504
- toMessageStatus(message, index, messages, options),
531
+ toMessageStatus(message, index, messages, options, interruptedTurns),
505
532
  );
506
533
  };
507
534
 
535
+ export type InterruptedTurnEventCache = {
536
+ lastEvents: readonly MessageStreamEvent[];
537
+ interruptions: readonly MessageStreamEvent[];
538
+ };
539
+
540
+ const isTurnInterruption = (event: MessageStreamEvent) =>
541
+ event.type === "turn.failed" || event.type === "turn.cancelled";
542
+
543
+ /**
544
+ * Collects the `turn.failed` and `turn.cancelled` events of an append-only
545
+ * event log, scanning only the events appended since the cached call. The
546
+ * returned array keeps its identity until a new interruption arrives.
547
+ */
548
+ export const collectInterruptedTurnEvents = (
549
+ events: readonly MessageStreamEvent[],
550
+ cache: InterruptedTurnEventCache,
551
+ ): readonly MessageStreamEvent[] => {
552
+ if (events === cache.lastEvents) return cache.interruptions;
553
+
554
+ const scanned = cache.lastEvents;
555
+ const resumesScan =
556
+ scanned.length === 0 ||
557
+ events[scanned.length - 1] === scanned[scanned.length - 1];
558
+
559
+ let interruptions = resumesScan ? cache.interruptions : [];
560
+ for (let i = resumesScan ? scanned.length : 0; i < events.length; i++) {
561
+ const event = events[i]!;
562
+ if (isTurnInterruption(event)) interruptions = [...interruptions, event];
563
+ }
564
+
565
+ cache.lastEvents = events;
566
+ cache.interruptions = interruptions;
567
+ return interruptions;
568
+ };
569
+
570
+ const getInterruptedTurns = (
571
+ events: readonly MessageStreamEvent[] | undefined,
572
+ ) => {
573
+ const interrupted = new Map<string, MessageStatus>();
574
+ for (const event of events ?? []) {
575
+ if (event.type === "turn.failed") {
576
+ interrupted.set(event.data.turnId, {
577
+ type: "incomplete",
578
+ reason: "error",
579
+ error: { code: event.data.code, message: event.data.message },
580
+ });
581
+ } else if (event.type === "turn.cancelled") {
582
+ interrupted.set(event.data.turnId, ASSISTANT_CANCELLED_STATUS);
583
+ }
584
+ }
585
+ return interrupted;
586
+ };
587
+
508
588
  /**
509
589
  * Converts the full Eve message data object into assistant-ui thread messages.
510
590
  */
511
591
  export const convertEveMessages = (
512
592
  data: EveMessageData,
513
593
  options: ConvertEveMessagesOptions = {},
514
- ): ThreadMessage[] =>
515
- data.messages.map((message, index, messages) =>
516
- convertEveMessage(message, index, messages, options),
594
+ ): ThreadMessage[] => {
595
+ const interruptedTurns = getInterruptedTurns(options.events);
596
+ return data.messages.map((message, index, messages) =>
597
+ convertEveMessageWithInterruptions(
598
+ message,
599
+ index,
600
+ messages,
601
+ options,
602
+ interruptedTurns,
603
+ ),
517
604
  );
605
+ };
518
606
 
519
607
  /**
520
608
  * Structural subset of the `string | UserContent` message content Eve's `send`
@@ -29,6 +29,7 @@ const compactionRequested = (at: string, turnId = TURN) =>
29
29
  modelId: "m",
30
30
  sequence: 3,
31
31
  sessionId: "s",
32
+ stepIndex: 0,
32
33
  turnId,
33
34
  usageInputTokens: null,
34
35
  },
@@ -0,0 +1,57 @@
1
+ import { ClientSession, type MessageStreamEvent } from "eve/client";
2
+ import { vi } from "vitest";
3
+
4
+ export const createEveSessionFixture = ({
5
+ events = [],
6
+ tailIndex = events.length - 1,
7
+ onSend,
8
+ }: {
9
+ events?: readonly MessageStreamEvent[];
10
+ tailIndex?: number;
11
+ onSend?: () => void;
12
+ }) => {
13
+ const encoder = new TextEncoder();
14
+ let controller: ReadableStreamDefaultController<Uint8Array> | undefined;
15
+ const pending = [...events];
16
+ const push = (events: readonly MessageStreamEvent[]) => {
17
+ if (controller === undefined) {
18
+ pending.push(...events);
19
+ return;
20
+ }
21
+ for (const event of events) {
22
+ controller.enqueue(encoder.encode(`${JSON.stringify(event)}\n`));
23
+ }
24
+ };
25
+ const fetch = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
26
+ if (init?.method === "POST") {
27
+ onSend?.();
28
+ return new Response(
29
+ JSON.stringify({
30
+ sessionId: "session_resumed",
31
+ deliveryId: "delivery_1",
32
+ }),
33
+ { status: 202, headers: { "content-type": "application/json" } },
34
+ );
35
+ }
36
+ return new Response(
37
+ new ReadableStream<Uint8Array>({
38
+ start(streamController) {
39
+ controller = streamController;
40
+ push(pending.splice(0));
41
+ },
42
+ }),
43
+ {
44
+ headers: {
45
+ "x-eve-stream-version": "26",
46
+ "x-eve-stream-tail-index": String(tailIndex),
47
+ },
48
+ },
49
+ );
50
+ });
51
+ vi.stubGlobal("fetch", fetch);
52
+ const session = new ClientSession(
53
+ { host: "http://localhost", resolveHeaders: async () => new Headers() },
54
+ { sessionId: "session_resumed", streamIndex: 0 },
55
+ );
56
+ return { fetch, push, session };
57
+ };
@@ -1,14 +1,10 @@
1
1
  // @vitest-environment jsdom
2
2
 
3
- // Deliberately does not mock `eve/react`: the real `useEveAgent` resumes an
4
- // offline session so the durable `meta.at` values travel through eve's own
5
- // store and reducer before the adapter reads them. Supplying `session` keeps
6
- // the store from constructing a network client.
7
-
8
3
  import { act, renderHook, waitFor } from "@testing-library/react";
9
- import { describe, expect, it, vi } from "vitest";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
10
5
  import type { MessageStreamEvent } from "eve/client";
11
6
 
7
+ import { createEveSessionFixture } from "./testUtils";
12
8
  import { useEveAgentRuntime } from "./useEveAgentRuntime";
13
9
 
14
10
  const TURN = "turn_resumed";
@@ -61,7 +57,11 @@ const nextTurnEvents = [
61
57
  {
62
58
  type: "message.received",
63
59
  data: { message: "follow up", sequence: 8, turnId: NEXT_TURN },
64
- meta: { at: "2026-01-02T10:03:01.000Z", id: "evt_202" },
60
+ meta: {
61
+ at: "2026-01-02T10:03:01.000Z",
62
+ id: "evt_202",
63
+ deliveryIds: ["delivery_1"],
64
+ },
65
65
  },
66
66
  {
67
67
  type: "step.started",
@@ -108,23 +108,13 @@ const parkedEvents = [
108
108
  const copyEvents = (events: readonly MessageStreamEvent[]) =>
109
109
  JSON.parse(JSON.stringify(events)) as readonly MessageStreamEvent[];
110
110
 
111
- const offlineSession = {
112
- state: { sessionId: "session_resumed", streamIndex: 0 },
113
- cancel: async () => ({ status: "no_active_turn" }),
114
- send: () => {
115
- throw new Error("the resume test must not reach the network");
116
- },
117
- stream: async function* () {
118
- yield* JSON.parse(
119
- JSON.stringify(resumedEvents),
120
- ) as readonly MessageStreamEvent[];
121
- },
122
- } as never;
111
+ afterEach(() => vi.unstubAllGlobals());
123
112
 
124
113
  describe("useEveAgentRuntime against a resumed eve session", () => {
125
114
  it("renders resumed history at its durable event times, not the current time", async () => {
115
+ const { session } = createEveSessionFixture({ events: resumedEvents });
126
116
  const { result } = renderHook(() =>
127
- useEveAgentRuntime({ resume: true, session: offlineSession }),
117
+ useEveAgentRuntime({ resume: true, session }),
128
118
  );
129
119
 
130
120
  await waitFor(() =>
@@ -137,44 +127,34 @@ describe("useEveAgentRuntime against a resumed eve session", () => {
137
127
  });
138
128
 
139
129
  it("reports the replay as loading and holds a send typed during it until the replay ends", async () => {
140
- let openReplay!: () => void;
141
- const replayOpened = new Promise<void>((resolve) => {
142
- openReplay = resolve;
130
+ const { fetch, push, session } = createEveSessionFixture({
131
+ tailIndex: parkedEvents.length - 1,
132
+ onSend: () => setTimeout(() => push(copyEvents(nextTurnEvents)), 0),
143
133
  });
144
- const stream = vi.fn(async function* () {
145
- await replayOpened;
146
- yield* copyEvents(parkedEvents);
147
- });
148
- const send = vi.fn(async function* () {
149
- yield* copyEvents(nextTurnEvents);
150
- });
151
- const session = {
152
- state: { sessionId: "session_resumed", streamIndex: 0 },
153
- cancel: async () => ({ status: "no_active_turn" }),
154
- send,
155
- stream,
156
- } as never;
157
134
 
158
135
  const { result } = renderHook(() =>
159
136
  useEveAgentRuntime({ resume: true, session }),
160
137
  );
161
138
 
162
- await waitFor(() => expect(stream).toHaveBeenCalledOnce());
139
+ await waitFor(() => expect(fetch).toHaveBeenCalledOnce());
163
140
  expect(result.current.thread.getState().isLoading).toBe(true);
164
141
 
165
142
  act(() => result.current.thread.append("follow up"));
166
143
  await act(async () => {
167
144
  await new Promise((resolve) => setTimeout(resolve, 0));
168
145
  });
169
- expect(send).not.toHaveBeenCalled();
170
- expect(stream).toHaveBeenCalledOnce();
146
+ expect(fetch).toHaveBeenCalledOnce();
171
147
 
172
- await act(async () => openReplay());
148
+ await act(async () => {
149
+ push(copyEvents(parkedEvents));
150
+ });
173
151
 
174
152
  await waitFor(() =>
175
153
  expect(result.current.thread.getState().messages).toHaveLength(4),
176
154
  );
177
- expect(send).toHaveBeenCalledOnce();
155
+ expect(
156
+ fetch.mock.calls.filter(([, init]) => init?.method === "POST"),
157
+ ).toHaveLength(1);
178
158
  expect(result.current.thread.getState().isLoading).toBe(false);
179
159
  });
180
160
  });
@@ -0,0 +1,80 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { renderHook, waitFor } from "@testing-library/react";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+
6
+ import { createEveSessionFixture } from "./testUtils";
7
+ import { useEveAgentRuntime } from "./useEveAgentRuntime";
8
+
9
+ const meta = (second: number) => ({
10
+ at: `2026-01-02T10:00:0${second}.000Z`,
11
+ id: `t1-${second}`,
12
+ });
13
+
14
+ const interruptedTurn = (ending: Record<string, unknown>) => [
15
+ { type: "turn.started", data: { sequence: 1, turnId: "t1" }, meta: meta(0) },
16
+ {
17
+ type: "message.received",
18
+ data: { message: "hi", sequence: 2, turnId: "t1" },
19
+ meta: meta(1),
20
+ },
21
+ {
22
+ type: "step.started",
23
+ data: { modelId: "m", sequence: 3, stepIndex: 0, turnId: "t1" },
24
+ meta: meta(2),
25
+ },
26
+ {
27
+ type: "message.appended",
28
+ data: {
29
+ messageDelta: "Let me th",
30
+ sequence: 4,
31
+ stepIndex: 0,
32
+ turnId: "t1",
33
+ },
34
+ meta: meta(3),
35
+ },
36
+ { ...ending, meta: meta(4) },
37
+ ];
38
+
39
+ const renderInterruptedTurn = (ending: Record<string, unknown>) => {
40
+ const { session } = createEveSessionFixture({});
41
+ return renderHook(() =>
42
+ useEveAgentRuntime({
43
+ initialEvents: interruptedTurn(ending) as never,
44
+ session,
45
+ }),
46
+ );
47
+ };
48
+
49
+ afterEach(() => vi.unstubAllGlobals());
50
+
51
+ describe("useEveAgentRuntime with an interrupted turn", () => {
52
+ it("shows a failed turn's assistant message as an error", async () => {
53
+ const { result } = renderInterruptedTurn({
54
+ type: "turn.failed",
55
+ data: { code: "internal", message: "boom", sequence: 5, turnId: "t1" },
56
+ });
57
+
58
+ await waitFor(() =>
59
+ expect(result.current.thread.getState().messages.at(-1)?.status).toEqual({
60
+ type: "incomplete",
61
+ reason: "error",
62
+ error: { code: "internal", message: "boom" },
63
+ }),
64
+ );
65
+ });
66
+
67
+ it("shows a cancelled turn's assistant message as cancelled", async () => {
68
+ const { result } = renderInterruptedTurn({
69
+ type: "turn.cancelled",
70
+ data: { sequence: 5, turnId: "t1" },
71
+ });
72
+
73
+ await waitFor(() =>
74
+ expect(result.current.thread.getState().messages.at(-1)?.status).toEqual({
75
+ type: "incomplete",
76
+ reason: "cancelled",
77
+ }),
78
+ );
79
+ });
80
+ });
@@ -1,9 +1,10 @@
1
1
  // @vitest-environment jsdom
2
2
 
3
3
  import { act, renderHook } from "@testing-library/react";
4
- import { describe, expect, it, vi } from "vitest";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
5
  import type { HandleMessageStreamEvent } from "eve/client";
6
6
 
7
+ import { createEveSessionFixture } from "./testUtils";
7
8
  import { useEveAgentRuntime } from "./useEveAgentRuntime";
8
9
 
9
10
  const completedTurn = (
@@ -49,18 +50,24 @@ const completedTurn = (
49
50
  const knownTurn = completedTurn("t1", "hi", "hello", 1);
50
51
  const outOfBandTurn = completedTurn("t2", "answered elsewhere", "done", 6);
51
52
 
53
+ afterEach(() => vi.unstubAllGlobals());
54
+
52
55
  describe("useEveAgentRuntime against a durable eve session", () => {
53
56
  it("surfaces a turn the server appended out of band after reloadMainThread", async () => {
54
- const stream = vi.fn(async function* () {
55
- yield* [...knownTurn, ...outOfBandTurn];
57
+ const { fetch, session } = createEveSessionFixture({
58
+ events: [
59
+ ...knownTurn,
60
+ ...outOfBandTurn,
61
+ {
62
+ type: "session.waiting",
63
+ data: {
64
+ continuationToken: "session_resumed",
65
+ wait: "next-user-message",
66
+ },
67
+ meta: { at: "2026-01-02T10:00:05.000Z", id: "waiting" },
68
+ },
69
+ ],
56
70
  });
57
- const session = {
58
- state: { sessionId: "s1" },
59
- stream,
60
- send: () => {
61
- throw new Error("the refetch test must not send");
62
- },
63
- } as never;
64
71
 
65
72
  const { result } = renderHook(() =>
66
73
  useEveAgentRuntime({ initialEvents: knownTurn as never, session }),
@@ -73,7 +80,10 @@ describe("useEveAgentRuntime against a durable eve session", () => {
73
80
  await result.current.threads.reloadMainThread();
74
81
  });
75
82
 
76
- expect(stream).toHaveBeenCalled();
83
+ expect(fetch).toHaveBeenCalled();
84
+ expect(
85
+ fetch.mock.calls.filter(([, init]) => init?.method === "POST"),
86
+ ).toHaveLength(0);
77
87
  const messages = result.current.thread.getState().messages;
78
88
  expect(messages.map((m) => m.role)).toEqual([
79
89
  "user",
@@ -39,10 +39,12 @@ import {
39
39
  type UseEveAgentStatus,
40
40
  } from "eve/react";
41
41
  import {
42
+ collectInterruptedTurnEvents,
42
43
  convertEveMessages,
43
44
  findEveInputRequest,
44
45
  getEveMessageContent,
45
46
  toEveInputResponse,
47
+ type InterruptedTurnEventCache,
46
48
  } from "./convertEveMessages";
47
49
  import {
48
50
  collectTurnTimestamps,
@@ -297,6 +299,15 @@ const useEveThreadRuntime = (
297
299
  () => collectTurnTimestamps(agent.events, turnTimestampCacheRef.current),
298
300
  [agent.events],
299
301
  );
302
+ const interruptionCacheRef = useRef<InterruptedTurnEventCache>({
303
+ lastEvents: [],
304
+ interruptions: [],
305
+ });
306
+ const interruptionEvents = useMemo(
307
+ () =>
308
+ collectInterruptedTurnEvents(agent.events, interruptionCacheRef.current),
309
+ [agent.events],
310
+ );
300
311
 
301
312
  const convertedMessages = useMemo(() => {
302
313
  const createdAtByMessageId = createdAtByMessageIdRef.current;
@@ -310,6 +321,7 @@ const useEveThreadRuntime = (
310
321
  return convertEveMessages(agent.data, {
311
322
  isRunning,
312
323
  error: agent.error,
324
+ events: interruptionEvents,
313
325
  getCreatedAt: (message) => {
314
326
  const turnId = message.metadata?.turnId;
315
327
  const durable =
@@ -326,7 +338,7 @@ const useEveThreadRuntime = (
326
338
  return createdAt;
327
339
  },
328
340
  });
329
- }, [agent.data, agent.error, isRunning, turnTimestamps]);
341
+ }, [agent.data, agent.error, interruptionEvents, isRunning, turnTimestamps]);
330
342
 
331
343
  const messages = stagedMessages ?? convertedMessages;
332
344
  const messagesRef = useRef(messages);