@assistant-ui/eve 0.0.21 → 0.0.23

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 +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":"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"}
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 { invokeUserCallback } from \"@assistant-ui/core/internal\";\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\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 void invokeUserCallback(\n \"assistant-ui/eve\",\n \"onError\",\n onError,\n error,\n ),\n }\n : {}),\n ...(onEvent\n ? {\n onEvent: (event) =>\n void invokeUserCallback(\n \"assistant-ui/eve\",\n \"onEvent\",\n onEvent,\n event,\n ),\n }\n : {}),\n onFinish: (snapshot) => {\n lastFinishStatusRef.current = snapshot.status;\n void invokeUserCallback(\n \"assistant-ui/eve\",\n \"onFinish\",\n onFinish,\n snapshot,\n );\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 void invokeUserCallback(\n \"assistant-ui/eve\",\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.23",
4
4
  "description": "Eve runtime adapter for assistant-ui",
5
5
  "keywords": [
6
6
  "eve",
@@ -28,8 +28,8 @@
28
28
  ],
29
29
  "sideEffects": false,
30
30
  "dependencies": {
31
- "@assistant-ui/core": "^0.3.23",
32
- "@assistant-ui/store": "^0.3.17",
31
+ "@assistant-ui/core": "^0.3.25",
32
+ "@assistant-ui/store": "^0.3.18",
33
33
  "assistant-cloud": "*"
34
34
  },
35
35
  "peerDependencies": {
@@ -48,8 +48,8 @@
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",
52
- "jsdom": "^30.0.1",
51
+ "eve": "^0.74.0",
52
+ "jsdom": "^30.1.2",
53
53
  "react": "^19.3.0",
54
54
  "react-dom": "^19.3.0",
55
55
  "vitest": "^5.0.3"
@@ -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,
@@ -207,6 +213,61 @@ describe("convertEveMessages", () => {
207
213
  });
208
214
  });
209
215
 
216
+ it("keeps streaming dynamic tool args incomplete until input is available", () => {
217
+ const streamingData = {
218
+ messages: [
219
+ {
220
+ id: "a1",
221
+ role: "assistant",
222
+ parts: [
223
+ {
224
+ type: "dynamic-tool",
225
+ state: "input-streaming",
226
+ toolCallId: "call_1",
227
+ toolName: "search",
228
+ input: undefined,
229
+ inputText: "",
230
+ },
231
+ ],
232
+ },
233
+ ],
234
+ } satisfies EveMessageData;
235
+
236
+ const [streamingMessage] = convertEveMessages(streamingData);
237
+
238
+ expect(streamingMessage?.content[0]).toMatchObject({
239
+ type: "tool-call",
240
+ args: {},
241
+ argsText: "",
242
+ });
243
+
244
+ const availableData = {
245
+ messages: [
246
+ {
247
+ id: "a1",
248
+ role: "assistant",
249
+ parts: [
250
+ {
251
+ type: "dynamic-tool",
252
+ state: "input-available",
253
+ toolCallId: "call_1",
254
+ toolName: "search",
255
+ input: { query: "badge" },
256
+ },
257
+ ],
258
+ },
259
+ ],
260
+ } satisfies EveMessageData;
261
+
262
+ const [availableMessage] = convertEveMessages(availableData);
263
+
264
+ expect(availableMessage?.content[0]).toMatchObject({
265
+ type: "tool-call",
266
+ args: { query: "badge" },
267
+ argsText: '{"query":"badge"}',
268
+ });
269
+ });
270
+
210
271
  it.each([
211
272
  [
212
273
  "a confirmation request",
@@ -699,7 +760,7 @@ describe("convertEveMessages", () => {
699
760
  ]);
700
761
  });
701
762
 
702
- it("defaults a file part with a missing mediaType to unknown/unknown", () => {
763
+ it("defaults a file part with a missing mediaType to application/octet-stream", () => {
703
764
  const data = {
704
765
  messages: [
705
766
  {
@@ -716,7 +777,7 @@ describe("convertEveMessages", () => {
716
777
  {
717
778
  type: "file",
718
779
  data: "https://example.com/blob",
719
- mimeType: "unknown/unknown",
780
+ mimeType: "application/octet-stream",
720
781
  sourceType: "url",
721
782
  },
722
783
  ]);
@@ -729,16 +790,66 @@ describe("convertEveMessages", () => {
729
790
  {
730
791
  type: "file",
731
792
  data: "https://example.com/blob",
732
- mimeType: "unknown/unknown",
793
+ mimeType: "application/octet-stream",
733
794
  sourceType: "url",
734
795
  },
735
796
  ],
736
- contentType: "unknown/unknown",
797
+ contentType: "application/octet-stream",
737
798
  status: { type: "complete" },
738
799
  },
739
800
  ]);
740
801
  });
741
802
 
803
+ it("reads the media type from a data URL when eve omits mediaType", () => {
804
+ const data = {
805
+ messages: [
806
+ {
807
+ id: "u1",
808
+ role: "user",
809
+ parts: [{ type: "file", url: "data:image/png;base64,iVBORw0KGgo=" }],
810
+ },
811
+ ],
812
+ } as unknown as EveMessageData;
813
+
814
+ const [message] = convertEveMessages(data);
815
+
816
+ expect(message?.content).toEqual([
817
+ {
818
+ type: "file",
819
+ data: "data:image/png;base64,iVBORw0KGgo=",
820
+ mimeType: "image/png",
821
+ },
822
+ ]);
823
+ expect(message?.attachments?.map((a) => [a.type, a.contentType])).toEqual([
824
+ ["image", "image/png"],
825
+ ]);
826
+ });
827
+
828
+ it("prefers an explicit mediaType over the data URL declaration", () => {
829
+ const data = {
830
+ messages: [
831
+ {
832
+ id: "u1",
833
+ role: "user",
834
+ parts: [
835
+ {
836
+ type: "file",
837
+ url: "data:application/octet-stream;base64,JVBERi0=",
838
+ mediaType: "application/pdf",
839
+ },
840
+ ],
841
+ },
842
+ ],
843
+ } satisfies EveMessageData;
844
+
845
+ const [message] = convertEveMessages(data);
846
+
847
+ expect(message?.content[0]).toMatchObject({
848
+ type: "file",
849
+ mimeType: "application/pdf",
850
+ });
851
+ });
852
+
742
853
  it("converts an assistant file part into a file content part", () => {
743
854
  const data = {
744
855
  messages: [
@@ -1742,28 +1853,49 @@ describe("convertEveMessages", () => {
1742
1853
  });
1743
1854
  });
1744
1855
 
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
- },
1856
+ it("a failed turn stays incomplete after Eve settles its message", () => {
1857
+ const failureEvent = {
1858
+ type: "turn.failed",
1859
+ meta: eventMeta(3),
1860
+ data: {
1861
+ turnId: "turn_1",
1862
+ sequence: 3,
1863
+ code: "internal",
1864
+ message: "boom",
1757
1865
  },
1758
- ]);
1866
+ } as const satisfies MessageStreamEvent;
1867
+ const state = replay([...midStreamEvents, failureEvent]);
1759
1868
 
1760
- const converted = convertEveMessages(state, { isRunning: false });
1869
+ const converted = convertEveMessages(state, {
1870
+ isRunning: false,
1871
+ events: [failureEvent],
1872
+ });
1761
1873
  expect(converted.at(-1)?.status).toEqual({
1762
1874
  type: "incomplete",
1763
- reason: "cancelled",
1875
+ reason: "error",
1876
+ error: { code: "internal", message: "boom" },
1764
1877
  });
1765
1878
  });
1766
1879
 
1880
+ it("a cancelled turn stays cancelled after Eve settles its message", () => {
1881
+ const cancelEvent = {
1882
+ type: "turn.cancelled",
1883
+ meta: eventMeta(3),
1884
+ data: { turnId: "turn_1", sequence: 3 },
1885
+ } as const satisfies MessageStreamEvent;
1886
+ const state = replay([...midStreamEvents, cancelEvent]);
1887
+
1888
+ expect(
1889
+ convertEveMessages(state, { isRunning: false }).at(-1)?.status,
1890
+ ).toEqual({ type: "complete", reason: "stop" });
1891
+ expect(
1892
+ convertEveMessages(state, {
1893
+ isRunning: false,
1894
+ events: [cancelEvent],
1895
+ }).at(-1)?.status,
1896
+ ).toEqual({ type: "incomplete", reason: "cancelled" });
1897
+ });
1898
+
1767
1899
  it("a completed turn terminalizes the streaming marker and converts to complete", () => {
1768
1900
  const state = replay([
1769
1901
  ...midStreamEvents,
@@ -1850,6 +1982,174 @@ describe("getEveMessageContent", () => {
1850
1982
  expect(getEveMessageContent(message)).toBe("Hello");
1851
1983
  });
1852
1984
 
1985
+ it("declares the data URL subtype of an image part", () => {
1986
+ const message = {
1987
+ ...baseAppendMessage,
1988
+ content: [{ type: "image", image: "data:image/jpeg;base64,/9j/4AAQ" }],
1989
+ } as unknown as AppendMessage;
1990
+
1991
+ expect(getEveMessageContent(message)).toEqual([
1992
+ {
1993
+ type: "file",
1994
+ data: "data:image/jpeg;base64,/9j/4AAQ",
1995
+ mediaType: "image/jpeg",
1996
+ },
1997
+ ]);
1998
+ });
1999
+
2000
+ it("sniffs an image part behind a generic envelope and rebuilds it", () => {
2001
+ const message = {
2002
+ ...baseAppendMessage,
2003
+ content: [
2004
+ {
2005
+ type: "image",
2006
+ image:
2007
+ "data:application/octet-stream;base64,iVBORw0KGgoAAAANSUhEUgAAAAE=",
2008
+ },
2009
+ ],
2010
+ } as unknown as AppendMessage;
2011
+
2012
+ expect(getEveMessageContent(message)).toEqual([
2013
+ {
2014
+ type: "file",
2015
+ data: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAE=",
2016
+ mediaType: "image/png",
2017
+ },
2018
+ ]);
2019
+ });
2020
+
2021
+ it("floors an http image part to image/png instead of a wildcard", () => {
2022
+ const message = {
2023
+ ...baseAppendMessage,
2024
+ content: [
2025
+ {
2026
+ type: "image",
2027
+ image: "https://example.com/photo",
2028
+ filename: "photo",
2029
+ },
2030
+ ],
2031
+ } as unknown as AppendMessage;
2032
+
2033
+ expect(getEveMessageContent(message)).toEqual([
2034
+ {
2035
+ type: "file",
2036
+ data: "https://example.com/photo",
2037
+ mediaType: "image/png",
2038
+ filename: "photo",
2039
+ },
2040
+ ]);
2041
+ });
2042
+
2043
+ it("declares an image attachment's content type for a url payload", () => {
2044
+ const message = {
2045
+ ...baseAppendMessage,
2046
+ content: [],
2047
+ attachments: [
2048
+ {
2049
+ id: "1",
2050
+ type: "image",
2051
+ name: "photo.jpg",
2052
+ contentType: "image/jpeg",
2053
+ content: [{ type: "image", image: "https://example.com/photo.jpg" }],
2054
+ status: { type: "complete" },
2055
+ },
2056
+ ],
2057
+ } as unknown as AppendMessage;
2058
+
2059
+ expect(getEveMessageContent(message)).toEqual([
2060
+ {
2061
+ type: "file",
2062
+ data: "https://example.com/photo.jpg",
2063
+ mediaType: "image/jpeg",
2064
+ },
2065
+ ]);
2066
+ });
2067
+
2068
+ it.each([
2069
+ {
2070
+ name: "an untyped URL",
2071
+ data: "https://example.com/file",
2072
+ mimeType: "",
2073
+ mediaType: "application/octet-stream",
2074
+ wireData: "https://example.com/file",
2075
+ },
2076
+ {
2077
+ name: "a data URL without an explicit type",
2078
+ data: "data:application/pdf;base64,JVBERi0=",
2079
+ mimeType: "",
2080
+ mediaType: "application/pdf",
2081
+ wireData: "data:application/pdf;base64,JVBERi0=",
2082
+ },
2083
+ {
2084
+ name: "a data URL with a conflicting envelope",
2085
+ data: "data:application/octet-stream;base64,JVBERi0=",
2086
+ mimeType: "application/pdf",
2087
+ mediaType: "application/pdf",
2088
+ wireData: "data:application/pdf;base64,JVBERi0=",
2089
+ },
2090
+ ])(
2091
+ "resolves file media types for $name",
2092
+ ({ data, mimeType, mediaType, wireData }) => {
2093
+ const message = {
2094
+ ...baseAppendMessage,
2095
+ content: [{ type: "file", data, mimeType, filename: "report.pdf" }],
2096
+ } satisfies AppendMessage;
2097
+
2098
+ expect(getEveMessageContent(message)).toEqual([
2099
+ { type: "file", data: wireData, mediaType, filename: "report.pdf" },
2100
+ ]);
2101
+ },
2102
+ );
2103
+
2104
+ it("uses a file attachment's content type when its part has none", () => {
2105
+ const message = {
2106
+ ...baseAppendMessage,
2107
+ content: [],
2108
+ attachments: [
2109
+ {
2110
+ id: "file-1",
2111
+ type: "file",
2112
+ name: "report.pdf",
2113
+ contentType: "application/pdf",
2114
+ content: [
2115
+ {
2116
+ type: "file",
2117
+ data: "https://example.com/report.pdf",
2118
+ mimeType: "",
2119
+ },
2120
+ ],
2121
+ status: { type: "complete" },
2122
+ },
2123
+ ],
2124
+ } satisfies AppendMessage;
2125
+
2126
+ expect(getEveMessageContent(message)).toEqual([
2127
+ {
2128
+ type: "file",
2129
+ data: "https://example.com/report.pdf",
2130
+ mediaType: "application/pdf",
2131
+ },
2132
+ ]);
2133
+ });
2134
+
2135
+ it("preserves opaque file references", () => {
2136
+ const message = {
2137
+ ...baseAppendMessage,
2138
+ content: [
2139
+ {
2140
+ type: "file",
2141
+ data: "file_abc123",
2142
+ mimeType: "application/pdf",
2143
+ sourceType: "id",
2144
+ },
2145
+ ],
2146
+ } satisfies AppendMessage;
2147
+
2148
+ expect(getEveMessageContent(message)).toEqual([
2149
+ { type: "file", data: "file_abc123", mediaType: "application/pdf" },
2150
+ ]);
2151
+ });
2152
+
1853
2153
  it("converts an audio part into a file part with the format-derived media type", () => {
1854
2154
  const message = {
1855
2155
  ...baseAppendMessage,
@@ -1900,6 +2200,26 @@ describe("getEveMessageContent", () => {
1900
2200
  ]);
1901
2201
  });
1902
2202
 
2203
+ it("rebuilds a media-less audio data URL from the typed format", () => {
2204
+ const message = {
2205
+ ...baseAppendMessage,
2206
+ content: [
2207
+ {
2208
+ type: "audio",
2209
+ audio: { data: "data:;base64,QUJD", format: "mp3" },
2210
+ },
2211
+ ],
2212
+ } as unknown as AppendMessage;
2213
+
2214
+ expect(getEveMessageContent(message)).toEqual([
2215
+ {
2216
+ type: "file",
2217
+ data: "data:audio/mp3;base64,QUJD",
2218
+ mediaType: "audio/mp3",
2219
+ },
2220
+ ]);
2221
+ });
2222
+
1903
2223
  it("forwards an http audio source instead of wrapping it in a data URL", () => {
1904
2224
  const message = {
1905
2225
  ...baseAppendMessage,
@@ -2295,3 +2615,49 @@ describe("findEveInputRequest", () => {
2295
2615
  ).toBeUndefined();
2296
2616
  });
2297
2617
  });
2618
+
2619
+ describe("collectInterruptedTurnEvents", () => {
2620
+ const started = (turnId: string, sequence: number) =>
2621
+ ({
2622
+ type: "turn.started",
2623
+ meta: eventMeta(sequence),
2624
+ data: { turnId, sequence },
2625
+ }) as const satisfies MessageStreamEvent;
2626
+ const failed = (turnId: string, sequence: number) =>
2627
+ ({
2628
+ type: "turn.failed",
2629
+ meta: eventMeta(sequence),
2630
+ data: { turnId, sequence, code: "internal", message: "boom" },
2631
+ }) as const satisfies MessageStreamEvent;
2632
+ const cancelled = (turnId: string, sequence: number) =>
2633
+ ({
2634
+ type: "turn.cancelled",
2635
+ meta: eventMeta(sequence),
2636
+ data: { turnId, sequence },
2637
+ }) as const satisfies MessageStreamEvent;
2638
+
2639
+ it("scans only appended events and keeps the array until an interruption arrives", () => {
2640
+ const cache: InterruptedTurnEventCache = {
2641
+ lastEvents: [],
2642
+ interruptions: [],
2643
+ };
2644
+ const first = [started("t1", 0), failed("t1", 1)];
2645
+ const interruptions = collectInterruptedTurnEvents(first, cache);
2646
+ expect(interruptions).toEqual([first[1]]);
2647
+ const quiet = [...cache.lastEvents, started("t2", 2)];
2648
+ expect(collectInterruptedTurnEvents(quiet, cache)).toBe(interruptions);
2649
+ const appended = [...cache.lastEvents, cancelled("t2", 3)];
2650
+ const next = collectInterruptedTurnEvents(appended, cache);
2651
+ expect(next).toEqual([first[1], appended[3]]);
2652
+ expect(next[0]).toBe(interruptions[0]);
2653
+ });
2654
+
2655
+ it("rescans a log that does not extend the scanned one", () => {
2656
+ const cache: InterruptedTurnEventCache = {
2657
+ lastEvents: [],
2658
+ interruptions: [],
2659
+ };
2660
+ collectInterruptedTurnEvents([started("t1", 0), failed("t1", 1)], cache);
2661
+ expect(collectInterruptedTurnEvents([started("t2", 0)], cache)).toEqual([]);
2662
+ });
2663
+ });