@assistant-ui/core 0.3.18 → 0.3.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/speech.d.ts.map +1 -1
- package/dist/adapters/speech.js +13 -3
- package/dist/adapters/speech.js.map +1 -1
- package/dist/adapters/thread-history.d.ts +4 -0
- package/dist/adapters/thread-history.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/model-context/frame/host.d.ts.map +1 -1
- package/dist/model-context/frame/host.js +26 -11
- package/dist/model-context/frame/host.js.map +1 -1
- package/dist/model-context/interactable-composer-metadata.js +11 -9
- package/dist/model-context/interactable-composer-metadata.js.map +1 -1
- package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
- package/dist/react/adapters/LocalStorageThreadListAdapter.js +45 -7
- package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
- package/dist/react/client/DataRenderers.d.ts.map +1 -1
- package/dist/react/client/DataRenderers.js +20 -15
- package/dist/react/client/DataRenderers.js.map +1 -1
- package/dist/react/client/Interactables.d.ts.map +1 -1
- package/dist/react/client/Interactables.js +11 -5
- package/dist/react/client/Interactables.js.map +1 -1
- package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
- package/dist/react/client/RemoteThreadList.js +3 -2
- package/dist/react/client/RemoteThreadList.js.map +1 -1
- package/dist/react/client/interactable-model-context.d.ts.map +1 -1
- package/dist/react/client/interactable-model-context.js +3 -2
- package/dist/react/client/interactable-model-context.js.map +1 -1
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +2 -2
- package/dist/react/interactables-legacy/useAssistantInteractable.d.ts.map +1 -1
- package/dist/react/interactables-legacy/useAssistantInteractable.js +3 -0
- package/dist/react/interactables-legacy/useAssistantInteractable.js.map +1 -1
- package/dist/react/model-context/useInteractable.d.ts.map +1 -1
- package/dist/react/model-context/useInteractable.js +4 -1
- package/dist/react/model-context/useInteractable.js.map +1 -1
- package/dist/react/model-context/useInteractableVersions.js +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.d.ts +11 -2
- package/dist/react/primitives/message/MessageGroupedParts.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.js +3 -2
- package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
- package/dist/react/primitives/part/PartMessages.d.ts +1 -0
- package/dist/react/primitives/part/PartMessages.d.ts.map +1 -1
- package/dist/react/primitives/part/PartMessages.js +1 -0
- package/dist/react/primitives/part/PartMessages.js.map +1 -1
- package/dist/react/primitives/thread/ThreadMessages.d.ts.map +1 -1
- package/dist/react/primitives/thread/ThreadMessages.js +6 -5
- package/dist/react/primitives/thread/ThreadMessages.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +2 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +2 -24
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +135 -374
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts +1 -0
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js +22 -2
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js.map +1 -1
- package/dist/react/runtimes/cloud/auiV0.d.ts +40 -15
- package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/auiV0.js +62 -15
- package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
- package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts +2 -1
- package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js +4 -0
- package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js.map +1 -1
- package/dist/react/runtimes/cloud/sdkIdentity.d.ts +6 -0
- package/dist/react/runtimes/cloud/sdkIdentity.d.ts.map +1 -0
- package/dist/react/runtimes/cloud/sdkIdentity.js +9 -0
- package/dist/react/runtimes/cloud/sdkIdentity.js.map +1 -0
- package/dist/react/runtimes/external-message-converter.d.ts +12 -2
- package/dist/react/runtimes/external-message-converter.d.ts.map +1 -1
- package/dist/react/runtimes/external-message-converter.js +26 -61
- package/dist/react/runtimes/external-message-converter.js.map +1 -1
- package/dist/react/runtimes/useRemoteThreadListRuntime.js +1 -1
- package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
- package/dist/react/utils/groupParts.d.ts +6 -1
- package/dist/react/utils/groupParts.d.ts.map +1 -1
- package/dist/react/utils/groupParts.js +6 -0
- package/dist/react/utils/groupParts.js.map +1 -1
- package/dist/react/utils/useJSONEqual.d.ts +3 -1
- package/dist/react/utils/useJSONEqual.d.ts.map +1 -1
- package/dist/react/utils/useJSONEqual.js +6 -1
- package/dist/react/utils/useJSONEqual.js.map +1 -1
- package/dist/react/utils/useJSONSchemaDependency.d.ts +7 -0
- package/dist/react/utils/useJSONSchemaDependency.d.ts.map +1 -0
- package/dist/react/utils/useJSONSchemaDependency.js +20 -0
- package/dist/react/utils/useJSONSchemaDependency.js.map +1 -0
- package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-runtime.js +2 -5
- package/dist/runtime/api/thread-runtime.js.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.d.ts +1 -0
- package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.js +46 -10
- package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.d.ts +2 -0
- package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.js +155 -64
- package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
- package/dist/runtime/interfaces/thread-runtime-core.d.ts +6 -0
- package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/queue/message-queue.d.ts +5 -0
- package/dist/runtime/queue/message-queue.d.ts.map +1 -1
- package/dist/runtime/queue/message-queue.js +61 -6
- package/dist/runtime/queue/message-queue.js.map +1 -1
- package/dist/runtime/utils/auto-status.d.ts +6 -2
- package/dist/runtime/utils/auto-status.d.ts.map +1 -1
- package/dist/runtime/utils/auto-status.js +12 -4
- package/dist/runtime/utils/auto-status.js.map +1 -1
- package/dist/runtime/utils/external-message-conversion.d.ts +16 -4
- package/dist/runtime/utils/external-message-conversion.d.ts.map +1 -1
- package/dist/runtime/utils/external-message-conversion.js +48 -12
- package/dist/runtime/utils/external-message-conversion.js.map +1 -1
- package/dist/runtime/utils/message-repository.js +3 -3
- package/dist/runtime/utils/message-repository.js.map +1 -1
- package/dist/runtime/utils/thread-message-like.d.ts +2 -1
- package/dist/runtime/utils/thread-message-like.d.ts.map +1 -1
- package/dist/runtime/utils/thread-message-like.js +4 -2
- package/dist/runtime/utils/thread-message-like.js.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js +8 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/internal.d.ts +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.d.ts +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.js +4 -1
- package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +2 -1
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
- package/dist/store/clients/external-thread.d.ts.map +1 -1
- package/dist/store/clients/external-thread.js +241 -200
- package/dist/store/clients/external-thread.js.map +1 -1
- package/dist/store/clients/thread-tasks.d.ts +13 -0
- package/dist/store/clients/thread-tasks.d.ts.map +1 -0
- package/dist/store/clients/thread-tasks.js +69 -0
- package/dist/store/clients/thread-tasks.js.map +1 -0
- package/dist/store/index.d.ts +3 -2
- package/dist/store/index.js +3 -3
- package/dist/store/primitive-predicates.d.ts.map +1 -1
- package/dist/store/primitive-predicates.js +4 -4
- package/dist/store/primitive-predicates.js.map +1 -1
- package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-runtime-client.js +176 -128
- package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
- package/dist/store/scope-registration.d.ts +2 -0
- package/dist/store/scope-registration.d.ts.map +1 -1
- package/dist/store/scopes/task.d.ts +36 -0
- package/dist/store/scopes/task.d.ts.map +1 -0
- package/dist/store/scopes/task.js +0 -0
- package/dist/store/scopes/thread.d.ts +20 -0
- package/dist/store/scopes/thread.d.ts.map +1 -1
- package/dist/subscribable/subscribable.d.ts +2 -1
- package/dist/subscribable/subscribable.d.ts.map +1 -1
- package/dist/subscribable/subscribable.js +23 -15
- package/dist/subscribable/subscribable.js.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/message.d.ts +8 -1
- package/dist/types/message.d.ts.map +1 -1
- package/dist/types/message.js.map +1 -1
- package/dist/utils/abortable-iterable.d.ts.map +1 -1
- package/dist/utils/abortable-iterable.js +30 -14
- package/dist/utils/abortable-iterable.js.map +1 -1
- package/dist/utils/getGroupStatus.d.ts +18 -2
- package/dist/utils/getGroupStatus.d.ts.map +1 -1
- package/dist/utils/getGroupStatus.js +32 -7
- package/dist/utils/getGroupStatus.js.map +1 -1
- package/dist/utils/wire-media.d.ts.map +1 -1
- package/dist/utils/wire-media.js +2 -2
- package/dist/utils/wire-media.js.map +1 -1
- package/package.json +6 -6
- package/src/adapters/speech.test.ts +236 -0
- package/src/adapters/speech.ts +17 -10
- package/src/adapters/thread-history.ts +3 -0
- package/src/index.ts +1 -0
- package/src/model-context/frame/host.test.ts +113 -1
- package/src/model-context/frame/host.ts +39 -12
- package/src/model-context/interactable-composer-metadata.test.ts +30 -0
- package/src/model-context/interactable-composer-metadata.ts +10 -10
- package/src/react/adapters/LocalStorageThreadListAdapter.test.ts +244 -0
- package/src/react/adapters/LocalStorageThreadListAdapter.tsx +104 -7
- package/src/react/client/DataRenderers.test.tsx +45 -0
- package/src/react/client/DataRenderers.ts +13 -9
- package/src/react/client/Interactables.test.ts +44 -1
- package/src/react/client/Interactables.ts +7 -1
- package/src/react/client/RemoteThreadList.test.ts +41 -1
- package/src/react/client/RemoteThreadList.ts +15 -2
- package/src/react/client/interactable-model-context.test.ts +44 -0
- package/src/react/client/interactable-model-context.ts +3 -2
- package/src/react/index.ts +5 -1
- package/src/react/interactables-legacy/useAssistantInteractable.test.tsx +132 -0
- package/src/react/interactables-legacy/useAssistantInteractable.ts +10 -1
- package/src/react/model-context/useInteractable.test.tsx +177 -0
- package/src/react/model-context/useInteractable.ts +3 -0
- package/src/react/primitive-hooks/useActionBarCopy.test.ts +0 -1
- package/src/react/primitive-hooks/useSuggestionTrigger.test.ts +0 -1
- package/src/react/primitives/message/MessageGroupedParts.test.tsx +96 -0
- package/src/react/primitives/message/MessageGroupedParts.tsx +15 -4
- package/src/react/primitives/part/PartMessages.test.tsx +104 -1
- package/src/react/primitives/part/PartMessages.tsx +1 -0
- package/src/react/primitives/thread/ThreadMessages.test.tsx +142 -0
- package/src/react/primitives/thread/ThreadMessages.tsx +8 -5
- package/src/react/providers/ReadonlyThreadProvider.test.tsx +56 -0
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +7 -1
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.ts +20 -14
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +221 -1
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +166 -512
- package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.test.ts +77 -0
- package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.ts +27 -1
- package/src/react/runtimes/cloud/auiV0.ts +160 -25
- package/src/react/runtimes/cloud/createCloudThreadListAdapter.test.ts +21 -0
- package/src/react/runtimes/cloud/createCloudThreadListAdapter.ts +7 -1
- package/src/react/runtimes/cloud/sdkIdentity.ts +9 -0
- package/src/react/runtimes/cloud/useCloudThreadListAdapter.test.tsx +18 -4
- package/src/react/runtimes/external-message-converter.test.ts +136 -1
- package/src/react/runtimes/external-message-converter.ts +39 -93
- package/src/react/runtimes/useRemoteThreadListRuntime.ts +1 -1
- package/src/react/utils/groupParts.ts +12 -1
- package/src/react/utils/useJSONEqual.ts +9 -0
- package/src/react/utils/useJSONSchemaDependency.ts +22 -0
- package/src/runtime/api/thread-runtime.test.ts +58 -0
- package/src/runtime/api/thread-runtime.ts +2 -4
- package/src/runtime/base/base-composer-runtime-core.ts +69 -16
- package/src/runtime/base/base-thread-runtime-core.test.ts +1008 -1
- package/src/runtime/base/base-thread-runtime-core.ts +222 -83
- package/src/runtime/interfaces/thread-runtime-core.ts +6 -0
- package/src/runtime/queue/message-queue.ts +68 -6
- package/src/runtime/utils/auto-status.test.ts +112 -1
- package/src/runtime/utils/auto-status.ts +31 -5
- package/src/runtime/utils/external-message-conversion.test.ts +134 -0
- package/src/runtime/utils/external-message-conversion.ts +122 -31
- package/src/runtime/utils/message-repository.test.ts +49 -0
- package/src/runtime/utils/message-repository.ts +3 -3
- package/src/runtime/utils/thread-message-like.test.ts +52 -0
- package/src/runtime/utils/thread-message-like.ts +4 -0
- package/src/runtimes/external-store/external-store-thread-runtime-core.ts +31 -1
- package/src/runtimes/local/local-thread-runtime-core.test.ts +21 -0
- package/src/runtimes/local/local-thread-runtime-core.ts +11 -1
- package/src/runtimes/tool-invocations/EDGE_CASES.md +2 -6
- package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +133 -0
- package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +10 -6
- package/src/store/clients/external-thread.ts +19 -1
- package/src/store/clients/thread-tasks.test.ts +274 -0
- package/src/store/clients/thread-tasks.ts +138 -0
- package/src/store/index.ts +8 -0
- package/src/store/primitive-predicates.test.ts +48 -5
- package/src/store/primitive-predicates.ts +7 -2
- package/src/store/runtime-clients/thread-runtime-client.ts +31 -1
- package/src/store/scope-registration.ts +2 -0
- package/src/store/scopes/task.ts +34 -0
- package/src/store/scopes/thread.ts +16 -0
- package/src/store/thread-tasks.test.tsx +209 -0
- package/src/subscribable/subscribable.test.ts +156 -1
- package/src/subscribable/subscribable.ts +25 -15
- package/src/tests/MessageRepository.test.ts +1 -5
- package/src/tests/auiV0Encode.test.ts +300 -0
- package/src/tests/base-composer-runtime-core-send.test.ts +51 -0
- package/src/tests/base-composer-runtime-core.test.ts +314 -2
- package/src/tests/external-store-thread-runtime-core-adapter.test.ts +151 -0
- package/src/tests/external-thread-attachments.test.tsx +69 -0
- package/src/tests/external-thread-parity.test.tsx +34 -0
- package/src/tests/groupParts.test.ts +71 -0
- package/src/tests/message-queue.test.ts +253 -0
- package/src/tests/remote-thread-list-reactive-threadId.test.ts +51 -0
- package/src/tests/useRemoteThreadListRuntime-controlled.test.tsx +43 -0
- package/src/types/index.ts +1 -0
- package/src/types/message.ts +8 -0
- package/src/utils/abortable-iterable.test.ts +115 -0
- package/src/utils/abortable-iterable.ts +34 -23
- package/src/utils/getGroupStatus.test.ts +70 -4
- package/src/utils/getGroupStatus.ts +35 -12
- package/src/utils/wire-media.test.ts +10 -0
- package/src/utils/wire-media.ts +6 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wire-media.js","names":["dataUrlMediaType","isParsableUrl","parseDataUrl","detectImageMediaType","resolveImageMediaType","image","contentType","startsWith","declared","parsed","payload","data","undefined","sniffed","resolveFileMediaType","mimeType","toMediaWireUrl","mediaType"],"sources":["../../src/utils/wire-media.ts"],"sourcesContent":["import { dataUrlMediaType, isParsableUrl, parseDataUrl } from \"./data-url\";\nimport { detectImageMediaType } from \"./image-media-type\";\n\n/**\n * The media type to declare for an `ImageMessagePart`, which carries none of\n * its own: an attachment's content type, then the payload's data URL\n * declaration, then its leading bytes, then `image/png`.\n *\n * The floor cannot be a wildcard. `image/*` is rejected outright by the AI SDK\n * for a url source and whenever inline bytes fail to sniff, so it fails exactly\n * where a fallback is needed.\n */\nexport function resolveImageMediaType(\n image: string,\n contentType?: string | undefined,\n): string {\n if (contentType?.startsWith(\"image/\")) return contentType;\n\n const declared = dataUrlMediaType(image);\n if (declared?.startsWith(\"image/\")) return declared;\n\n // Read through a data URL envelope too, so a generic one such as\n // `application/octet-stream` does not mask the format. A url of any other\n // scheme has no local bytes to read.\n const parsed = parseDataUrl(image);\n const payload = parsed?.data ?? (isParsableUrl(image) ? undefined : image);\n if (payload !== undefined) {\n const sniffed = detectImageMediaType(payload);\n if (sniffed) return sniffed;\n }\n\n return \"image/png\";\n}\n\n/**\n * The media type to declare for a `FileMessagePart`: its own `mimeType`, then\n * the payload's data URL declaration, then `application/octet-stream`.\n *\n * `mimeType` is a plain string, and an adapter reading `file.type` on a file\n * the OS cannot type yields `\"\"`, so the declared value is not always present.\n */\nexport function resolveFileMediaType(\n data: string,\n mimeType?: string | undefined,\n): string {\n return mimeType || dataUrlMediaType(data) || \"application/octet-stream\";\n}\n\n/**\n * A payload placed into a wire field that is contractually a url.\n *\n * Two hazards drive this. A consumer may hand the value to an unguarded\n * `new URL()`, so a payload that is not a url has to be wrapped; and a data\n * URL's own media type wins over a separately declared one downstream, so a\n * base64 envelope that disagrees with `mediaType` is rebuilt around the same\n * bytes. Everything else passes through byte for byte: an envelope that agrees,\n * a url of any other scheme, and a percent-encoded data URL, whose declaration\n * cannot be corrected without transcoding the payload and is authoritative for\n * the bytes it carries anyway.\n */\nexport function toMediaWireUrl(payload: string, mediaType: string): string {\n const parsed = parseDataUrl(payload);\n if (parsed) {\n return parsed.mimeType === mediaType\n ? payload\n : `data:${mediaType};base64,${parsed.data}`;\n }\n if (isParsableUrl(payload)) return payload;\n return `data:${mediaType};base64,${payload}`;\n}\n"],"mappings":";;;;;;;;;;;;AAYA,SAAgBI,sBACdC,OACAC,aACQ;CACR,IAAIA,aAAaC,WAAW,QAAQ,GAAG,
|
|
1
|
+
{"version":3,"file":"wire-media.js","names":["dataUrlMediaType","isParsableUrl","parseDataUrl","detectImageMediaType","resolveImageMediaType","image","contentType","startsWith","includes","declared","parsed","payload","data","undefined","sniffed","resolveFileMediaType","mimeType","toMediaWireUrl","mediaType"],"sources":["../../src/utils/wire-media.ts"],"sourcesContent":["import { dataUrlMediaType, isParsableUrl, parseDataUrl } from \"./data-url\";\nimport { detectImageMediaType } from \"./image-media-type\";\n\n/**\n * The media type to declare for an `ImageMessagePart`, which carries none of\n * its own: an attachment's content type, then the payload's data URL\n * declaration, then its leading bytes, then `image/png`.\n *\n * The floor cannot be a wildcard. `image/*` is rejected outright by the AI SDK\n * for a url source and whenever inline bytes fail to sniff, so it fails exactly\n * where a fallback is needed.\n */\nexport function resolveImageMediaType(\n image: string,\n contentType?: string | undefined,\n): string {\n if (contentType?.startsWith(\"image/\") && !contentType.includes(\"*\")) {\n return contentType;\n }\n\n const declared = dataUrlMediaType(image);\n if (declared?.startsWith(\"image/\") && !declared.includes(\"*\")) {\n return declared;\n }\n\n // Read through a data URL envelope too, so a generic one such as\n // `application/octet-stream` does not mask the format. A url of any other\n // scheme has no local bytes to read.\n const parsed = parseDataUrl(image);\n const payload = parsed?.data ?? (isParsableUrl(image) ? undefined : image);\n if (payload !== undefined) {\n const sniffed = detectImageMediaType(payload);\n if (sniffed) return sniffed;\n }\n\n return \"image/png\";\n}\n\n/**\n * The media type to declare for a `FileMessagePart`: its own `mimeType`, then\n * the payload's data URL declaration, then `application/octet-stream`.\n *\n * `mimeType` is a plain string, and an adapter reading `file.type` on a file\n * the OS cannot type yields `\"\"`, so the declared value is not always present.\n */\nexport function resolveFileMediaType(\n data: string,\n mimeType?: string | undefined,\n): string {\n return mimeType || dataUrlMediaType(data) || \"application/octet-stream\";\n}\n\n/**\n * A payload placed into a wire field that is contractually a url.\n *\n * Two hazards drive this. A consumer may hand the value to an unguarded\n * `new URL()`, so a payload that is not a url has to be wrapped; and a data\n * URL's own media type wins over a separately declared one downstream, so a\n * base64 envelope that disagrees with `mediaType` is rebuilt around the same\n * bytes. Everything else passes through byte for byte: an envelope that agrees,\n * a url of any other scheme, and a percent-encoded data URL, whose declaration\n * cannot be corrected without transcoding the payload and is authoritative for\n * the bytes it carries anyway.\n */\nexport function toMediaWireUrl(payload: string, mediaType: string): string {\n const parsed = parseDataUrl(payload);\n if (parsed) {\n return parsed.mimeType === mediaType\n ? payload\n : `data:${mediaType};base64,${parsed.data}`;\n }\n if (isParsableUrl(payload)) return payload;\n return `data:${mediaType};base64,${payload}`;\n}\n"],"mappings":";;;;;;;;;;;;AAYA,SAAgBI,sBACdC,OACAC,aACQ;CACR,IAAIA,aAAaC,WAAW,QAAQ,KAAK,CAACD,YAAYE,SAAS,GAAG,GAChE,OAAOF;CAGT,MAAMG,WAAWT,iBAAiBK,KAAK;CACvC,IAAII,UAAUF,WAAW,QAAQ,KAAK,CAACE,SAASD,SAAS,GAAG,GAC1D,OAAOC;CAOT,MAAME,UADST,aAAaG,KACZK,CAAM,EAAEE,SAASX,cAAcI,KAAK,IAAIQ,KAAAA,IAAYR;CACpE,IAAIM,YAAYE,KAAAA,GAAW;EACzB,MAAMC,UAAUX,qBAAqBQ,OAAO;EAC5C,IAAIG,SAAS,OAAOA;CACtB;CAEA,OAAO;AACT;;;;;;;;AASA,SAAgBC,qBACdH,MACAI,UACQ;CACR,OAAOA,YAAYhB,iBAAiBY,IAAI,KAAK;AAC/C;;;;;;;;;;;;;AAcA,SAAgBK,eAAeN,SAAiBO,WAA2B;CACzE,MAAMR,SAASR,aAAaS,OAAO;CACnC,IAAID,QACF,OAAOA,OAAOM,aAAaE,YACvBP,UACA,QAAQO,UAAS,UAAWR,OAAOE;CAEzC,IAAIX,cAAcU,OAAO,GAAG,OAAOA;CACnC,OAAO,QAAQO,UAAS,UAAWP;AACrC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.19",
|
|
4
4
|
"description": "Framework-agnostic core runtime for assistant-ui",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"assistant",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
],
|
|
60
60
|
"sideEffects": false,
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"assistant-stream": "^0.3.
|
|
62
|
+
"assistant-stream": "^0.3.43",
|
|
63
63
|
"nanoid": "^6.0.1"
|
|
64
64
|
},
|
|
65
65
|
"optionalDevDependencies": {
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"@assistant-ui/tap": "^0.9.17",
|
|
71
71
|
"@types/react": "*",
|
|
72
72
|
"react": "^18 || ^19",
|
|
73
|
-
"assistant-cloud": "^0.2.
|
|
73
|
+
"assistant-cloud": "^0.2.1"
|
|
74
74
|
},
|
|
75
75
|
"peerDependenciesMeta": {
|
|
76
76
|
"@types/react": {
|
|
@@ -87,14 +87,14 @@
|
|
|
87
87
|
"@assistant-ui/store": "0.3.13",
|
|
88
88
|
"@assistant-ui/tap": "0.9.17",
|
|
89
89
|
"@assistant-ui/vite": "0.0.17",
|
|
90
|
-
"@assistant-ui/x-buildutils": "0.0.
|
|
90
|
+
"@assistant-ui/x-buildutils": "0.0.27",
|
|
91
91
|
"@testing-library/react": "^16.3.3",
|
|
92
92
|
"@types/react": "^19.2.18",
|
|
93
|
-
"assistant-cloud": "0.2.
|
|
93
|
+
"assistant-cloud": "0.2.1",
|
|
94
94
|
"jsdom": "^30.0.1",
|
|
95
95
|
"react": "^19.2.8",
|
|
96
96
|
"react-dom": "^19.2.8",
|
|
97
|
-
"vitest": "^
|
|
97
|
+
"vitest": "^5.0.0"
|
|
98
98
|
},
|
|
99
99
|
"publishConfig": {
|
|
100
100
|
"access": "public",
|
|
@@ -7,6 +7,62 @@ afterEach(() => {
|
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
describe("WebSpeechSynthesisAdapter", () => {
|
|
10
|
+
const stubSpeechSynthesis = () => {
|
|
11
|
+
const utterances: EventTarget[] = [];
|
|
12
|
+
class MockSpeechSynthesisUtterance extends EventTarget {
|
|
13
|
+
constructor() {
|
|
14
|
+
super();
|
|
15
|
+
utterances.push(this);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const cancel = vi.fn();
|
|
19
|
+
vi.stubGlobal("SpeechSynthesisUtterance", MockSpeechSynthesisUtterance);
|
|
20
|
+
vi.stubGlobal("window", { speechSynthesis: { speak: vi.fn(), cancel } });
|
|
21
|
+
return { utterances, cancel };
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
it.each(["end", "error"])(
|
|
25
|
+
"does not cancel newer playback through a handle that received %s",
|
|
26
|
+
(event) => {
|
|
27
|
+
const { utterances, cancel } = stubSpeechSynthesis();
|
|
28
|
+
const adapter = new WebSpeechSynthesisAdapter();
|
|
29
|
+
const old = adapter.speak("old");
|
|
30
|
+
utterances[0]!.dispatchEvent(new Event(event));
|
|
31
|
+
const endedStatus = old.status;
|
|
32
|
+
const current = adapter.speak("current");
|
|
33
|
+
|
|
34
|
+
old.cancel();
|
|
35
|
+
old.cancel();
|
|
36
|
+
|
|
37
|
+
expect(cancel).not.toHaveBeenCalled();
|
|
38
|
+
expect(old.status).toBe(endedStatus);
|
|
39
|
+
expect(current.status).toEqual({ type: "running" });
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
it("cancels active playback once and leaves later playback alone", () => {
|
|
44
|
+
const { cancel } = stubSpeechSynthesis();
|
|
45
|
+
const adapter = new WebSpeechSynthesisAdapter();
|
|
46
|
+
const old = adapter.speak("old");
|
|
47
|
+
const onChange = vi.fn();
|
|
48
|
+
old.subscribe(onChange);
|
|
49
|
+
|
|
50
|
+
old.cancel();
|
|
51
|
+
|
|
52
|
+
expect(cancel).toHaveBeenCalledOnce();
|
|
53
|
+
expect(old.status).toMatchObject({ type: "ended", reason: "cancelled" });
|
|
54
|
+
expect(onChange).toHaveBeenCalledOnce();
|
|
55
|
+
|
|
56
|
+
const current = adapter.speak("current");
|
|
57
|
+
old.cancel();
|
|
58
|
+
|
|
59
|
+
expect(cancel).toHaveBeenCalledOnce();
|
|
60
|
+
expect(onChange).toHaveBeenCalledOnce();
|
|
61
|
+
expect(current.status).toEqual({ type: "running" });
|
|
62
|
+
current.cancel();
|
|
63
|
+
expect(cancel).toHaveBeenCalledTimes(2);
|
|
64
|
+
});
|
|
65
|
+
|
|
10
66
|
it("isolates a late subscriber that throws after the utterance ended", async () => {
|
|
11
67
|
const listeners = new Map<string, EventListener>();
|
|
12
68
|
class MockSpeechSynthesisUtterance {
|
|
@@ -109,6 +165,186 @@ describe("WebSpeechDictationAdapter", () => {
|
|
|
109
165
|
return listeners;
|
|
110
166
|
};
|
|
111
167
|
|
|
168
|
+
const emitResults = (
|
|
169
|
+
listeners: Map<string, EventListener>,
|
|
170
|
+
results: [transcript: string, isFinal: boolean][],
|
|
171
|
+
resultIndex = 0,
|
|
172
|
+
) => {
|
|
173
|
+
listeners.get("result")!({
|
|
174
|
+
resultIndex,
|
|
175
|
+
results: results.map(([transcript, isFinal]) => ({
|
|
176
|
+
0: { transcript },
|
|
177
|
+
isFinal,
|
|
178
|
+
})),
|
|
179
|
+
} as unknown as Event);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
it("publishes the entire interim suffix when only its last result changes", () => {
|
|
183
|
+
const listeners = stubSpeechRecognition();
|
|
184
|
+
const session = new WebSpeechDictationAdapter().listen();
|
|
185
|
+
const onSpeech = vi.fn();
|
|
186
|
+
session.onSpeech(onSpeech);
|
|
187
|
+
|
|
188
|
+
emitResults(listeners, [
|
|
189
|
+
["hello ", false],
|
|
190
|
+
["world", false],
|
|
191
|
+
]);
|
|
192
|
+
emitResults(
|
|
193
|
+
listeners,
|
|
194
|
+
[
|
|
195
|
+
["hello ", false],
|
|
196
|
+
["there", false],
|
|
197
|
+
],
|
|
198
|
+
1,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
expect(onSpeech.mock.calls).toEqual([
|
|
202
|
+
[{ transcript: "hello world", isFinal: false }],
|
|
203
|
+
[{ transcript: "hello there", isFinal: false }],
|
|
204
|
+
]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("publishes partial and complete retractions with no changed results", () => {
|
|
208
|
+
const listeners = stubSpeechRecognition();
|
|
209
|
+
const session = new WebSpeechDictationAdapter().listen();
|
|
210
|
+
const onSpeech = vi.fn();
|
|
211
|
+
session.onSpeech(onSpeech);
|
|
212
|
+
|
|
213
|
+
emitResults(listeners, [
|
|
214
|
+
["hello ", false],
|
|
215
|
+
["world", false],
|
|
216
|
+
]);
|
|
217
|
+
emitResults(listeners, [["hello ", false]], 1);
|
|
218
|
+
emitResults(listeners, []);
|
|
219
|
+
emitResults(listeners, []);
|
|
220
|
+
|
|
221
|
+
expect(onSpeech.mock.calls).toEqual([
|
|
222
|
+
[{ transcript: "hello world", isFinal: false }],
|
|
223
|
+
[{ transcript: "hello ", isFinal: false }],
|
|
224
|
+
[{ transcript: "", isFinal: false }],
|
|
225
|
+
]);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("delivers changed final results once before the remaining interim suffix", () => {
|
|
229
|
+
const listeners = stubSpeechRecognition();
|
|
230
|
+
const session = new WebSpeechDictationAdapter().listen();
|
|
231
|
+
const onSpeech = vi.fn();
|
|
232
|
+
const onEnd = vi.fn();
|
|
233
|
+
session.onSpeech(onSpeech);
|
|
234
|
+
session.onSpeechEnd(onEnd);
|
|
235
|
+
|
|
236
|
+
emitResults(listeners, [
|
|
237
|
+
["hello ", true],
|
|
238
|
+
["wor", false],
|
|
239
|
+
["ld", false],
|
|
240
|
+
]);
|
|
241
|
+
emitResults(
|
|
242
|
+
listeners,
|
|
243
|
+
[
|
|
244
|
+
["hello ", true],
|
|
245
|
+
["world", true],
|
|
246
|
+
],
|
|
247
|
+
1,
|
|
248
|
+
);
|
|
249
|
+
listeners.get("end")!(new Event("end"));
|
|
250
|
+
|
|
251
|
+
expect(onSpeech.mock.calls).toEqual([
|
|
252
|
+
[{ transcript: "hello ", isFinal: true }],
|
|
253
|
+
[{ transcript: "world", isFinal: false }],
|
|
254
|
+
[{ transcript: "world", isFinal: true }],
|
|
255
|
+
]);
|
|
256
|
+
expect(onEnd).toHaveBeenCalledExactlyOnceWith({
|
|
257
|
+
transcript: "hello world",
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("clears an interim suffix while retaining earlier final results", () => {
|
|
262
|
+
const listeners = stubSpeechRecognition();
|
|
263
|
+
const session = new WebSpeechDictationAdapter().listen();
|
|
264
|
+
const onSpeech = vi.fn();
|
|
265
|
+
session.onSpeech(onSpeech);
|
|
266
|
+
|
|
267
|
+
emitResults(listeners, [
|
|
268
|
+
["hello ", true],
|
|
269
|
+
["world", false],
|
|
270
|
+
]);
|
|
271
|
+
emitResults(listeners, [["hello ", true]], 1);
|
|
272
|
+
|
|
273
|
+
expect(onSpeech.mock.calls).toEqual([
|
|
274
|
+
[{ transcript: "hello ", isFinal: true }],
|
|
275
|
+
[{ transcript: "world", isFinal: false }],
|
|
276
|
+
[{ transcript: "", isFinal: false }],
|
|
277
|
+
]);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("keeps final-only delivery free of empty interim updates", () => {
|
|
281
|
+
const listeners = stubSpeechRecognition();
|
|
282
|
+
const session = new WebSpeechDictationAdapter().listen();
|
|
283
|
+
const onSpeech = vi.fn();
|
|
284
|
+
session.onSpeech(onSpeech);
|
|
285
|
+
|
|
286
|
+
emitResults(listeners, [["hello", true]]);
|
|
287
|
+
emitResults(listeners, [["hello", true]], 1);
|
|
288
|
+
|
|
289
|
+
expect(onSpeech).toHaveBeenCalledExactlyOnceWith({
|
|
290
|
+
transcript: "hello",
|
|
291
|
+
isFinal: true,
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("does not drop an unconsumed final when resultIndex skips it", () => {
|
|
296
|
+
const listeners = stubSpeechRecognition();
|
|
297
|
+
const session = new WebSpeechDictationAdapter().listen();
|
|
298
|
+
const onSpeech = vi.fn();
|
|
299
|
+
const onEnd = vi.fn();
|
|
300
|
+
session.onSpeech(onSpeech);
|
|
301
|
+
session.onSpeechEnd(onEnd);
|
|
302
|
+
|
|
303
|
+
emitResults(listeners, [["hello ", false]]);
|
|
304
|
+
emitResults(
|
|
305
|
+
listeners,
|
|
306
|
+
[
|
|
307
|
+
["hello ", true],
|
|
308
|
+
["world", false],
|
|
309
|
+
],
|
|
310
|
+
1,
|
|
311
|
+
);
|
|
312
|
+
listeners.get("end")!(new Event("end"));
|
|
313
|
+
|
|
314
|
+
expect(onSpeech.mock.calls).toEqual([
|
|
315
|
+
[{ transcript: "hello ", isFinal: false }],
|
|
316
|
+
[{ transcript: "hello ", isFinal: true }],
|
|
317
|
+
[{ transcript: "world", isFinal: false }],
|
|
318
|
+
]);
|
|
319
|
+
expect(onEnd).toHaveBeenCalledExactlyOnceWith({ transcript: "hello " });
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("does not reread historical finalized results during interim updates", () => {
|
|
323
|
+
const listeners = stubSpeechRecognition();
|
|
324
|
+
new WebSpeechDictationAdapter().listen();
|
|
325
|
+
const readFinal = vi.fn(() => ({ transcript: "word " }));
|
|
326
|
+
const finals = Array.from({ length: 1_000 }, () => ({
|
|
327
|
+
get 0() {
|
|
328
|
+
return readFinal();
|
|
329
|
+
},
|
|
330
|
+
isFinal: true,
|
|
331
|
+
}));
|
|
332
|
+
listeners.get("result")!({
|
|
333
|
+
resultIndex: 0,
|
|
334
|
+
results: finals,
|
|
335
|
+
} as unknown as Event);
|
|
336
|
+
expect(readFinal).toHaveBeenCalledTimes(1_000);
|
|
337
|
+
|
|
338
|
+
for (const transcript of ["hel", "hello", "hello world"]) {
|
|
339
|
+
listeners.get("result")!({
|
|
340
|
+
resultIndex: finals.length,
|
|
341
|
+
results: [...finals, { 0: { transcript }, isFinal: false }],
|
|
342
|
+
} as unknown as Event);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
expect(readFinal).toHaveBeenCalledTimes(1_000);
|
|
346
|
+
});
|
|
347
|
+
|
|
112
348
|
it("continues notifying dictation listeners when one throws", () => {
|
|
113
349
|
const listeners = stubSpeechRecognition();
|
|
114
350
|
const listenerError = new Error("listener failed");
|
package/src/adapters/speech.ts
CHANGED
|
@@ -76,6 +76,7 @@ export class WebSpeechSynthesisAdapter implements SpeechSynthesisAdapter {
|
|
|
76
76
|
const res: SpeechSynthesisAdapter.Utterance = {
|
|
77
77
|
status: { type: "running" },
|
|
78
78
|
cancel: () => {
|
|
79
|
+
if (res.status.type === "ended") return;
|
|
79
80
|
window.speechSynthesis.cancel();
|
|
80
81
|
handleEnd("cancelled");
|
|
81
82
|
},
|
|
@@ -205,6 +206,8 @@ export class WebSpeechDictationAdapter implements DictationAdapter {
|
|
|
205
206
|
>();
|
|
206
207
|
|
|
207
208
|
let finalTranscript = "";
|
|
209
|
+
let hasInterimTranscript = false;
|
|
210
|
+
let firstInterimIndex = 0;
|
|
208
211
|
|
|
209
212
|
const session: DictationAdapter.Session = {
|
|
210
213
|
status: { type: "starting" },
|
|
@@ -267,32 +270,36 @@ export class WebSpeechDictationAdapter implements DictationAdapter {
|
|
|
267
270
|
|
|
268
271
|
recognition.addEventListener("result", (event) => {
|
|
269
272
|
const speechEvent = event as unknown as SpeechRecognitionEvent;
|
|
273
|
+
let interimTranscript = "";
|
|
270
274
|
|
|
271
|
-
for (
|
|
272
|
-
let i = speechEvent.resultIndex;
|
|
273
|
-
i < speechEvent.results.length;
|
|
274
|
-
i++
|
|
275
|
-
) {
|
|
275
|
+
for (let i = firstInterimIndex; i < speechEvent.results.length; i++) {
|
|
276
276
|
const result = speechEvent.results[i];
|
|
277
277
|
if (!result) continue;
|
|
278
278
|
|
|
279
279
|
const transcript = result[0]?.transcript ?? "";
|
|
280
280
|
|
|
281
281
|
if (result.isFinal) {
|
|
282
|
+
firstInterimIndex = i + 1;
|
|
282
283
|
finalTranscript += transcript;
|
|
284
|
+
hasInterimTranscript = false;
|
|
283
285
|
notifyEventListeners(
|
|
284
286
|
speechCallbacks,
|
|
285
287
|
() => ({ transcript, isFinal: true }),
|
|
286
288
|
"Dictation",
|
|
287
289
|
);
|
|
288
290
|
} else {
|
|
289
|
-
|
|
290
|
-
speechCallbacks,
|
|
291
|
-
() => ({ transcript, isFinal: false }),
|
|
292
|
-
"Dictation",
|
|
293
|
-
);
|
|
291
|
+
interimTranscript += transcript;
|
|
294
292
|
}
|
|
295
293
|
}
|
|
294
|
+
|
|
295
|
+
if (interimTranscript || hasInterimTranscript) {
|
|
296
|
+
hasInterimTranscript = interimTranscript.length > 0;
|
|
297
|
+
notifyEventListeners(
|
|
298
|
+
speechCallbacks,
|
|
299
|
+
() => ({ transcript: interimTranscript, isFinal: false }),
|
|
300
|
+
"Dictation",
|
|
301
|
+
);
|
|
302
|
+
}
|
|
296
303
|
});
|
|
297
304
|
|
|
298
305
|
recognition.addEventListener("speechend", () => {
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
ExportedMessageRepositoryItem,
|
|
8
8
|
} from "../runtime/utils/message-repository";
|
|
9
9
|
import type { ReadonlyJSONValue } from "assistant-stream/utils";
|
|
10
|
+
import type { ThreadMessage } from "../types";
|
|
10
11
|
|
|
11
12
|
export interface MessageStorageEntry<TPayload> {
|
|
12
13
|
id: string;
|
|
@@ -52,6 +53,8 @@ export type GenericThreadHistoryAdapter<TMessage> = {
|
|
|
52
53
|
options?: {
|
|
53
54
|
durationMs?: number;
|
|
54
55
|
stepTimestamps?: { start_ms: number; end_ms: number }[];
|
|
56
|
+
/** The thread message the items were persisted from; its status and timing complete a report the stored format cannot carry. */
|
|
57
|
+
message?: ThreadMessage;
|
|
55
58
|
},
|
|
56
59
|
): void;
|
|
57
60
|
};
|
package/src/index.ts
CHANGED
|
@@ -74,9 +74,31 @@ beforeEach(() => {
|
|
|
74
74
|
afterEach(() => {
|
|
75
75
|
vi.useRealTimers();
|
|
76
76
|
vi.unstubAllGlobals();
|
|
77
|
+
vi.restoreAllMocks();
|
|
77
78
|
});
|
|
78
79
|
|
|
79
80
|
describe("AssistantFrameHost", () => {
|
|
81
|
+
it("does not install its message listener when initialization fails", () => {
|
|
82
|
+
const addEventListener = vi.fn();
|
|
83
|
+
const removeEventListener = vi.fn();
|
|
84
|
+
vi.stubGlobal("window", {
|
|
85
|
+
addEventListener,
|
|
86
|
+
removeEventListener,
|
|
87
|
+
location: { origin: DEFAULT_ORIGIN },
|
|
88
|
+
});
|
|
89
|
+
const error = new Error("postMessage failed");
|
|
90
|
+
const iframeWindow = {
|
|
91
|
+
postMessage: vi.fn(() => {
|
|
92
|
+
throw error;
|
|
93
|
+
}),
|
|
94
|
+
} as unknown as Window;
|
|
95
|
+
|
|
96
|
+
expect(() => new AssistantFrameHost(iframeWindow)).toThrow(error);
|
|
97
|
+
|
|
98
|
+
expect(addEventListener).not.toHaveBeenCalled();
|
|
99
|
+
expect(removeEventListener).not.toHaveBeenCalled();
|
|
100
|
+
});
|
|
101
|
+
|
|
80
102
|
it("defaults to the current origin", () => {
|
|
81
103
|
const { host, postMessage } = createHost();
|
|
82
104
|
|
|
@@ -212,8 +234,83 @@ describe("AssistantFrameHost", () => {
|
|
|
212
234
|
expect(vi.getTimerCount()).toBe(0);
|
|
213
235
|
});
|
|
214
236
|
|
|
237
|
+
it("settles every pending tool call when cancellation posting fails", async () => {
|
|
238
|
+
const { execute, host, postMessage } = createHost();
|
|
239
|
+
const consoleError = vi
|
|
240
|
+
.spyOn(console, "error")
|
|
241
|
+
.mockImplementation(() => undefined);
|
|
242
|
+
const firstAbortController = new AbortController();
|
|
243
|
+
const secondAbortController = new AbortController();
|
|
244
|
+
const firstRemoveEventListener = vi.spyOn(
|
|
245
|
+
firstAbortController.signal,
|
|
246
|
+
"removeEventListener",
|
|
247
|
+
);
|
|
248
|
+
const secondRemoveEventListener = vi.spyOn(
|
|
249
|
+
secondAbortController.signal,
|
|
250
|
+
"removeEventListener",
|
|
251
|
+
);
|
|
252
|
+
const firstRejected = vi.fn();
|
|
253
|
+
const secondRejected = vi.fn();
|
|
254
|
+
|
|
255
|
+
void execute(
|
|
256
|
+
{},
|
|
257
|
+
{ ...executionContext, abortSignal: firstAbortController.signal },
|
|
258
|
+
).catch(firstRejected);
|
|
259
|
+
void execute(
|
|
260
|
+
{},
|
|
261
|
+
{ ...executionContext, abortSignal: secondAbortController.signal },
|
|
262
|
+
).catch(secondRejected);
|
|
263
|
+
|
|
264
|
+
const firstTransportError = new Error("first tool cancellation failed");
|
|
265
|
+
const secondTransportError = new Error("second tool cancellation failed");
|
|
266
|
+
let cancellationCount = 0;
|
|
267
|
+
postMessage.mockImplementation((data) => {
|
|
268
|
+
if (data.message.type !== "tool-cancel") return;
|
|
269
|
+
cancellationCount += 1;
|
|
270
|
+
throw cancellationCount === 1
|
|
271
|
+
? firstTransportError
|
|
272
|
+
: secondTransportError;
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
expect(() => host.dispose()).toThrow(firstTransportError);
|
|
276
|
+
await Promise.resolve();
|
|
277
|
+
|
|
278
|
+
expect(firstRejected).toHaveBeenCalledWith(
|
|
279
|
+
expect.objectContaining({
|
|
280
|
+
message: "AssistantFrameHost has been disposed",
|
|
281
|
+
}),
|
|
282
|
+
);
|
|
283
|
+
expect(secondRejected).toHaveBeenCalledWith(
|
|
284
|
+
expect.objectContaining({
|
|
285
|
+
message: "AssistantFrameHost has been disposed",
|
|
286
|
+
}),
|
|
287
|
+
);
|
|
288
|
+
expect(
|
|
289
|
+
postMessage.mock.calls.filter(
|
|
290
|
+
([data]) => data.message.type === "tool-cancel",
|
|
291
|
+
),
|
|
292
|
+
).toHaveLength(2);
|
|
293
|
+
expect(firstRemoveEventListener).toHaveBeenCalledWith(
|
|
294
|
+
"abort",
|
|
295
|
+
expect.any(Function),
|
|
296
|
+
);
|
|
297
|
+
expect(secondRemoveEventListener).toHaveBeenCalledWith(
|
|
298
|
+
"abort",
|
|
299
|
+
expect.any(Function),
|
|
300
|
+
);
|
|
301
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
302
|
+
expect(consoleError).toHaveBeenCalledWith(
|
|
303
|
+
"[assistant-ui] AssistantFrameHost tool cancellation could not be sent.",
|
|
304
|
+
secondTransportError,
|
|
305
|
+
);
|
|
306
|
+
expect(() => host.dispose()).not.toThrow();
|
|
307
|
+
});
|
|
308
|
+
|
|
215
309
|
it("rejects pending tool calls when execution is aborted", async () => {
|
|
216
310
|
const { execute, getToolCallId, host, postMessage } = createHost();
|
|
311
|
+
const consoleError = vi
|
|
312
|
+
.spyOn(console, "error")
|
|
313
|
+
.mockImplementation(() => undefined);
|
|
217
314
|
const abortController = new AbortController();
|
|
218
315
|
const abortError = new Error("Run cancelled");
|
|
219
316
|
abortError.name = "AbortError";
|
|
@@ -229,8 +326,13 @@ describe("AssistantFrameHost", () => {
|
|
|
229
326
|
const onRejected = vi.fn();
|
|
230
327
|
void result.catch(onRejected);
|
|
231
328
|
const toolCallId = getToolCallId();
|
|
329
|
+
postMessage.mockImplementation((data) => {
|
|
330
|
+
if (data.message.type === "tool-cancel") {
|
|
331
|
+
throw new Error("tool cancellation failed");
|
|
332
|
+
}
|
|
333
|
+
});
|
|
232
334
|
|
|
233
|
-
abortController.abort(abortError);
|
|
335
|
+
expect(() => abortController.abort(abortError)).not.toThrow();
|
|
234
336
|
await Promise.resolve();
|
|
235
337
|
|
|
236
338
|
expect(onRejected).toHaveBeenCalledWith(abortError);
|
|
@@ -241,17 +343,26 @@ describe("AssistantFrameHost", () => {
|
|
|
241
343
|
},
|
|
242
344
|
DEFAULT_ORIGIN,
|
|
243
345
|
);
|
|
346
|
+
expect(consoleError).toHaveBeenCalledOnce();
|
|
244
347
|
expect(vi.getTimerCount()).toBe(0);
|
|
245
348
|
host.dispose();
|
|
246
349
|
});
|
|
247
350
|
|
|
248
351
|
it("cancels tool calls when they time out", async () => {
|
|
249
352
|
const { execute, getToolCallId, host, postMessage } = createHost();
|
|
353
|
+
const consoleError = vi
|
|
354
|
+
.spyOn(console, "error")
|
|
355
|
+
.mockImplementation(() => undefined);
|
|
250
356
|
const result = Promise.resolve(execute({}, executionContext));
|
|
251
357
|
const toolCallId = getToolCallId();
|
|
252
358
|
const rejection = expect(result).rejects.toThrow(
|
|
253
359
|
'Tool call "search" timed out',
|
|
254
360
|
);
|
|
361
|
+
postMessage.mockImplementation((data) => {
|
|
362
|
+
if (data.message.type === "tool-cancel") {
|
|
363
|
+
throw new Error("tool cancellation failed");
|
|
364
|
+
}
|
|
365
|
+
});
|
|
255
366
|
|
|
256
367
|
await vi.advanceTimersByTimeAsync(30000);
|
|
257
368
|
|
|
@@ -263,6 +374,7 @@ describe("AssistantFrameHost", () => {
|
|
|
263
374
|
},
|
|
264
375
|
DEFAULT_ORIGIN,
|
|
265
376
|
);
|
|
377
|
+
expect(consoleError).toHaveBeenCalledOnce();
|
|
266
378
|
expect(vi.getTimerCount()).toBe(0);
|
|
267
379
|
host.dispose();
|
|
268
380
|
});
|
|
@@ -12,6 +12,13 @@ import {
|
|
|
12
12
|
|
|
13
13
|
const getDefaultTargetOrigin = () => window.location.origin;
|
|
14
14
|
|
|
15
|
+
const logCancellationFailure = (error: unknown) => {
|
|
16
|
+
console.error(
|
|
17
|
+
"[assistant-ui] AssistantFrameHost tool cancellation could not be sent.",
|
|
18
|
+
error,
|
|
19
|
+
);
|
|
20
|
+
};
|
|
21
|
+
|
|
15
22
|
/**
|
|
16
23
|
* Deserializes tools from JSON Schema format back to Tool objects
|
|
17
24
|
*/
|
|
@@ -73,9 +80,10 @@ export class AssistantFrameHost implements ModelContextProvider {
|
|
|
73
80
|
this._targetOrigin = targetOrigin;
|
|
74
81
|
|
|
75
82
|
this.handleMessage = this.handleMessage.bind(this);
|
|
76
|
-
|
|
77
|
-
|
|
83
|
+
// A posted message cannot be delivered before this constructor returns, so
|
|
84
|
+
// requesting first means a failed request leaves no listener to clean up.
|
|
78
85
|
this.requestContext();
|
|
86
|
+
window.addEventListener("message", this.handleMessage);
|
|
79
87
|
}
|
|
80
88
|
|
|
81
89
|
private handleMessage(event: MessageEvent) {
|
|
@@ -208,14 +216,21 @@ export class AssistantFrameHost implements ModelContextProvider {
|
|
|
208
216
|
});
|
|
209
217
|
}
|
|
210
218
|
|
|
211
|
-
private cancelToolCall(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
+
private cancelToolCall(
|
|
220
|
+
id: string,
|
|
221
|
+
onError: (error: unknown) => void = logCancellationFailure,
|
|
222
|
+
) {
|
|
223
|
+
try {
|
|
224
|
+
this._iframeWindow.postMessage(
|
|
225
|
+
{
|
|
226
|
+
channel: FRAME_MESSAGE_CHANNEL,
|
|
227
|
+
message: { type: "tool-cancel", id } satisfies FrameMessage,
|
|
228
|
+
},
|
|
229
|
+
this._targetOrigin,
|
|
230
|
+
);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
onError(error);
|
|
233
|
+
}
|
|
219
234
|
}
|
|
220
235
|
|
|
221
236
|
private requestContext() {
|
|
@@ -248,10 +263,22 @@ export class AssistantFrameHost implements ModelContextProvider {
|
|
|
248
263
|
window.removeEventListener("message", this.handleMessage);
|
|
249
264
|
this._subscribers.clear();
|
|
250
265
|
const error = new Error("AssistantFrameHost has been disposed");
|
|
266
|
+
let cancellationFailed = false;
|
|
267
|
+
let cancellationError: unknown;
|
|
268
|
+
|
|
251
269
|
for (const [id, pending] of this._pendingRequests) {
|
|
252
|
-
this.
|
|
270
|
+
this._pendingRequests.delete(id);
|
|
271
|
+
this.cancelToolCall(id, (error) => {
|
|
272
|
+
if (!cancellationFailed) {
|
|
273
|
+
cancellationFailed = true;
|
|
274
|
+
cancellationError = error;
|
|
275
|
+
} else {
|
|
276
|
+
logCancellationFailure(error);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
253
279
|
pending.reject(error);
|
|
254
280
|
}
|
|
255
|
-
|
|
281
|
+
|
|
282
|
+
if (cancellationFailed) throw cancellationError;
|
|
256
283
|
}
|
|
257
284
|
}
|