@assistant-ui/core 0.2.6 → 0.2.8

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.
Files changed (192) hide show
  1. package/dist/adapters/attachment.d.ts.map +1 -1
  2. package/dist/adapters/speech.d.ts.map +1 -1
  3. package/dist/adapters/speech.js.map +1 -1
  4. package/dist/index.d.ts +4 -1
  5. package/dist/index.js +8 -1
  6. package/dist/index.js.map +1 -0
  7. package/dist/internal/duplicate-detection.d.ts +5 -0
  8. package/dist/internal/duplicate-detection.d.ts.map +1 -0
  9. package/dist/internal/duplicate-detection.js +11 -0
  10. package/dist/internal/duplicate-detection.js.map +1 -0
  11. package/dist/internal.d.ts +2 -2
  12. package/dist/internal.js +2 -2
  13. package/dist/model-context/frame/host.d.ts.map +1 -1
  14. package/dist/model-context/frame/host.js.map +1 -1
  15. package/dist/model-context/frame/provider.d.ts.map +1 -1
  16. package/dist/model-context/frame/provider.js.map +1 -1
  17. package/dist/model-context/registry.d.ts.map +1 -1
  18. package/dist/model-context/tool.d.ts.map +1 -1
  19. package/dist/react/AssistantProvider.d.ts.map +1 -1
  20. package/dist/react/AssistantProvider.js.map +1 -1
  21. package/dist/react/client/Interactables.js.map +1 -1
  22. package/dist/react/client/Tools.d.ts.map +1 -1
  23. package/dist/react/client/Tools.js +26 -15
  24. package/dist/react/client/Tools.js.map +1 -1
  25. package/dist/react/index.d.ts +5 -4
  26. package/dist/react/index.js +2 -2
  27. package/dist/react/model-context/toolbox.d.ts +29 -2
  28. package/dist/react/model-context/toolbox.d.ts.map +1 -1
  29. package/dist/react/model-context/toolbox.js +18 -0
  30. package/dist/react/model-context/toolbox.js.map +1 -0
  31. package/dist/react/model-context/useAssistantTool.d.ts.map +1 -1
  32. package/dist/react/model-context/useAssistantTool.js +6 -3
  33. package/dist/react/model-context/useAssistantTool.js.map +1 -1
  34. package/dist/react/model-context/useAssistantToolUI.d.ts +6 -0
  35. package/dist/react/model-context/useAssistantToolUI.d.ts.map +1 -1
  36. package/dist/react/model-context/useAssistantToolUI.js +4 -2
  37. package/dist/react/model-context/useAssistantToolUI.js.map +1 -1
  38. package/dist/react/model-context/useInlineRender.js.map +1 -1
  39. package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js.map +1 -1
  40. package/dist/react/primitives/message/MessageGroupedParts.d.ts +49 -7
  41. package/dist/react/primitives/message/MessageGroupedParts.d.ts.map +1 -1
  42. package/dist/react/primitives/message/MessageGroupedParts.js +28 -3
  43. package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
  44. package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
  45. package/dist/react/primitives/message/MessageParts.js +2 -7
  46. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  47. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  48. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  49. package/dist/react/runtimes/RuntimeAdapterProvider.d.ts.map +1 -1
  50. package/dist/react/runtimes/RuntimeAdapterProvider.js +6 -5
  51. package/dist/react/runtimes/RuntimeAdapterProvider.js.map +1 -1
  52. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts.map +1 -1
  53. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts.map +1 -1
  54. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
  55. package/dist/react/runtimes/external-message-converter.d.ts +1 -1
  56. package/dist/react/runtimes/external-message-converter.d.ts.map +1 -1
  57. package/dist/react/runtimes/external-message-converter.js +1 -0
  58. package/dist/react/runtimes/external-message-converter.js.map +1 -1
  59. package/dist/react/runtimes/useExternalStoreSharedOptions.d.ts +7 -0
  60. package/dist/react/runtimes/useExternalStoreSharedOptions.d.ts.map +1 -0
  61. package/dist/react/runtimes/useExternalStoreSharedOptions.js +21 -0
  62. package/dist/react/runtimes/useExternalStoreSharedOptions.js.map +1 -0
  63. package/dist/react/runtimes/useLocalRuntime.d.ts.map +1 -1
  64. package/dist/react/runtimes/useLocalRuntime.js.map +1 -1
  65. package/dist/react/runtimes/useRemoteThreadListRuntime.d.ts.map +1 -1
  66. package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
  67. package/dist/react/types/scopes/tools.d.ts +19 -2
  68. package/dist/react/types/scopes/tools.d.ts.map +1 -1
  69. package/dist/react/utils/groupParts.d.ts +32 -11
  70. package/dist/react/utils/groupParts.d.ts.map +1 -1
  71. package/dist/react/utils/groupParts.js +13 -6
  72. package/dist/react/utils/groupParts.js.map +1 -1
  73. package/dist/runtime/api/assistant-runtime.d.ts.map +1 -1
  74. package/dist/runtime/api/attachment-runtime.d.ts.map +1 -1
  75. package/dist/runtime/api/attachment-runtime.js.map +1 -1
  76. package/dist/runtime/api/composer-runtime.d.ts.map +1 -1
  77. package/dist/runtime/api/message-part-runtime.d.ts.map +1 -1
  78. package/dist/runtime/api/message-runtime.d.ts.map +1 -1
  79. package/dist/runtime/api/thread-list-item-runtime.d.ts.map +1 -1
  80. package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
  81. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  82. package/dist/runtime/base/base-assistant-runtime-core.d.ts.map +1 -1
  83. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  84. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  85. package/dist/runtime/base/default-edit-composer-runtime-core.d.ts.map +1 -1
  86. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
  87. package/dist/runtime/interfaces/thread-runtime-core.d.ts +8 -0
  88. package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
  89. package/dist/runtime/utils/message-repository.d.ts +9 -1
  90. package/dist/runtime/utils/message-repository.d.ts.map +1 -1
  91. package/dist/runtime/utils/message-repository.js +34 -14
  92. package/dist/runtime/utils/message-repository.js.map +1 -1
  93. package/dist/runtime/utils/thread-message-like.d.ts +1 -0
  94. package/dist/runtime/utils/thread-message-like.d.ts.map +1 -1
  95. package/dist/runtime/utils/thread-message-like.js +2 -1
  96. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  97. package/dist/runtimes/external-store/external-store-adapter.d.ts +31 -0
  98. package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
  99. package/dist/runtimes/external-store/external-store-shared-options.d.ts +8 -0
  100. package/dist/runtimes/external-store/external-store-shared-options.d.ts.map +1 -0
  101. package/dist/runtimes/external-store/external-store-shared-options.js +11 -0
  102. package/dist/runtimes/external-store/external-store-shared-options.js.map +1 -0
  103. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  104. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
  105. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +25 -2
  106. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  107. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +106 -26
  108. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  109. package/dist/runtimes/external-store/thread-message-converter.d.ts.map +1 -1
  110. package/dist/runtimes/local/local-thread-list-runtime-core.d.ts.map +1 -1
  111. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  112. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
  113. package/dist/runtimes/remote-thread-list/adapter/in-memory.d.ts.map +1 -1
  114. package/dist/runtimes/remote-thread-list/optimistic-state.d.ts.map +1 -1
  115. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts +168 -0
  116. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -0
  117. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +449 -0
  118. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -0
  119. package/dist/subscribable/subscribable.d.ts.map +1 -1
  120. package/dist/subscribable/subscribable.js.map +1 -1
  121. package/dist/tests/remote-thread-list-test-helpers.d.ts.map +1 -1
  122. package/dist/types/message.d.ts +6 -0
  123. package/dist/types/message.d.ts.map +1 -1
  124. package/dist/types/message.js.map +1 -1
  125. package/dist/utils/composite-context-provider.d.ts.map +1 -1
  126. package/dist/utils/id.d.ts +1 -3
  127. package/dist/utils/id.d.ts.map +1 -1
  128. package/dist/utils/id.js +1 -4
  129. package/dist/utils/id.js.map +1 -1
  130. package/package.json +10 -10
  131. package/src/adapters/index.ts +1 -4
  132. package/src/adapters/speech.ts +0 -1
  133. package/src/index.ts +12 -0
  134. package/src/internal/duplicate-detection.ts +26 -0
  135. package/src/internal.ts +0 -2
  136. package/src/model-context/frame/host.ts +0 -1
  137. package/src/model-context/frame/provider.ts +0 -1
  138. package/src/react/AssistantProvider.tsx +2 -3
  139. package/src/react/client/Interactables.ts +0 -1
  140. package/src/react/client/Tools.ts +50 -25
  141. package/src/react/index.ts +9 -8
  142. package/src/react/model-context/toolbox.ts +46 -1
  143. package/src/react/model-context/useAssistantTool.ts +8 -3
  144. package/src/react/model-context/useAssistantToolUI.ts +9 -2
  145. package/src/react/model-context/useInlineRender.ts +0 -1
  146. package/src/react/primitives/chainOfThought/ChainOfThoughtParts.tsx +1 -2
  147. package/src/react/primitives/message/MessageAttachments.test.tsx +1 -1
  148. package/src/react/primitives/message/MessageGroupedParts.tsx +102 -13
  149. package/src/react/primitives/message/MessageParts.tsx +4 -7
  150. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +0 -3
  151. package/src/react/runtimes/RuntimeAdapterProvider.tsx +12 -7
  152. package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +0 -3
  153. package/src/react/runtimes/external-message-converter.ts +5 -1
  154. package/src/react/runtimes/useExternalStoreSharedOptions.ts +23 -0
  155. package/src/react/runtimes/useLocalRuntime.ts +0 -10
  156. package/src/react/runtimes/useRemoteThreadListRuntime.ts +0 -6
  157. package/src/react/types/scopes/tools.ts +20 -1
  158. package/src/react/utils/groupParts.ts +49 -18
  159. package/src/runtime/api/attachment-runtime.ts +1 -2
  160. package/src/runtime/interfaces/thread-runtime-core.ts +8 -0
  161. package/src/runtime/internal.ts +1 -4
  162. package/src/runtime/utils/message-repository.ts +57 -16
  163. package/src/runtime/utils/thread-message-like.ts +2 -0
  164. package/src/runtimes/external-store/external-store-adapter.ts +33 -0
  165. package/src/runtimes/external-store/external-store-shared-options.ts +18 -0
  166. package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +1 -3
  167. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +179 -37
  168. package/src/runtimes/tool-invocations/EDGE_CASES.md +194 -0
  169. package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +1054 -0
  170. package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +782 -0
  171. package/src/subscribable/subscribable.ts +3 -3
  172. package/src/tests/MessageRepository.test.ts +83 -52
  173. package/src/tests/OptimisticState-delete-crash.test.ts +2 -0
  174. package/src/tests/OptimisticState-list-race.test.ts +2 -4
  175. package/src/tests/RemoteThreadListThreadListRuntimeCore-loadMore.test.ts +5 -5
  176. package/src/tests/auiV0Encode.test.ts +1 -1
  177. package/src/tests/composer-can-send.test.ts +8 -4
  178. package/src/tests/duplicate-detection.test.ts +34 -0
  179. package/src/tests/external-store-thread-list-runtime-core.test.ts +1 -1
  180. package/src/tests/external-store-thread-runtime-core.test.ts +112 -79
  181. package/src/tests/groupParts.test.ts +70 -0
  182. package/src/tests/no-unsafe-process-env.test.ts +1 -0
  183. package/src/tests/remote-thread-list-isLoading.test.ts +2 -5
  184. package/src/tests/thread-message-like.test.ts +4 -1
  185. package/src/types/index.ts +1 -4
  186. package/src/types/message.ts +6 -0
  187. package/src/utils/id.ts +0 -4
  188. package/dist/react/runtimes/useToolInvocations.d.ts +0 -53
  189. package/dist/react/runtimes/useToolInvocations.d.ts.map +0 -1
  190. package/dist/react/runtimes/useToolInvocations.js +0 -380
  191. package/dist/react/runtimes/useToolInvocations.js.map +0 -1
  192. package/src/react/runtimes/useToolInvocations.ts +0 -694
@@ -1 +1 @@
1
- {"version":3,"file":"message.js","names":[],"sources":["../../src/types/message.ts"],"sourcesContent":["import type {\n ReadonlyJSONObject,\n ReadonlyJSONValue,\n} from \"assistant-stream/utils\";\nimport type { ToolModelContentPart } from \"assistant-stream\";\nimport type { CompleteAttachment } from \"./attachment\";\n\nexport type { ToolModelContentPart };\n\nexport type TextMessagePart = {\n readonly type: \"text\";\n readonly text: string;\n readonly parentId?: string;\n};\n\nexport type ReasoningMessagePart = {\n readonly type: \"reasoning\";\n readonly text: string;\n readonly parentId?: string;\n};\n\nexport type SourceProviderMetadata = {\n readonly [providerName: string]: ReadonlyJSONObject;\n};\n\nexport type SourceMessagePart =\n | {\n readonly type: \"source\";\n readonly sourceType: \"url\";\n readonly id: string;\n readonly url: string;\n readonly title?: string;\n readonly providerMetadata?: SourceProviderMetadata;\n readonly parentId?: string;\n }\n | {\n readonly type: \"source\";\n readonly sourceType: \"document\";\n readonly id: string;\n readonly url?: undefined;\n readonly title: string;\n readonly mediaType: string;\n readonly filename?: string;\n readonly providerMetadata?: SourceProviderMetadata;\n readonly parentId?: string;\n };\n\nexport type ImageMessagePart = {\n readonly type: \"image\";\n readonly image: string;\n readonly filename?: string;\n};\n\nexport type FileMessagePart = {\n readonly type: \"file\";\n readonly filename?: string;\n readonly data: string;\n readonly mimeType: string;\n readonly parentId?: string;\n};\n\nexport type Unstable_AudioMessagePart = {\n readonly type: \"audio\";\n readonly audio: {\n readonly data: string;\n readonly format: \"mp3\" | \"wav\";\n };\n};\n\nexport type DataMessagePart<T = any> = {\n readonly type: \"data\";\n readonly name: string;\n readonly data: T;\n};\n\n/**\n * A JSON spec describing a tree of UI components to render.\n *\n * The agent emits a {@link GenerativeUIMessagePart} containing this spec, and\n * the consumer-provided component allowlist is used to resolve `component`\n * names. Any component referenced that is not present in the allowlist is\n * rejected with a typed error — the allowlist is the security boundary in the\n * default same-realm rendering path.\n */\nexport type GenerativeUINode =\n | string\n | {\n /** Allowlisted component name (resolved against the consumer registry). */\n readonly component: string;\n /** Props passed to the resolved component (must be JSON-serializable). */\n readonly props?: Record<string, unknown>;\n /** Optional children — strings render as text, objects recurse. */\n readonly children?: readonly GenerativeUINode[];\n /** Optional stable key for React reconciliation. */\n readonly key?: string;\n };\n\n/**\n * The root spec for a generative UI tree.\n */\nexport type GenerativeUISpec = {\n /** Root node(s) to render. */\n readonly root: GenerativeUINode | readonly GenerativeUINode[];\n};\n\n/**\n * A message part that carries a JSON spec describing UI to render.\n *\n * Render with `<MessagePrimitive.GenerativeUI components={...} />`. The\n * primitive resolves component names against the consumer-provided allowlist\n * — any unknown name throws a typed error rather than rendering. Stream-\n * friendly: a partially-streamed spec renders progressively.\n */\nexport type GenerativeUIMessagePart = {\n readonly type: \"generative-ui\";\n /** The JSON spec describing the UI tree. */\n readonly spec: GenerativeUISpec;\n /** Optional id (useful for replays / stable keys). */\n readonly id?: string;\n readonly parentId?: string;\n};\n\nexport type McpAppMetadata = {\n readonly resourceUri: string;\n readonly mimeType?: string;\n readonly visibility?: readonly (\"model\" | \"app\")[];\n};\n\nexport const MCP_APP_URI_SCHEME = \"ui://\";\n\nexport const isMcpAppUri = (uri: string | undefined): boolean =>\n !!uri?.startsWith(MCP_APP_URI_SCHEME);\n\nexport type ToolCallMessagePartMcpMetadata = {\n readonly app?: McpAppMetadata;\n};\n\nexport type ToolCallMessagePart<\n TArgs = ReadonlyJSONObject,\n TResult = unknown,\n> = {\n /** Identifies this part as a tool call. */\n readonly type: \"tool-call\";\n /** Stable identifier for this invocation of the tool. */\n readonly toolCallId: string;\n /** Name of the tool requested by the model. */\n readonly toolName: string;\n /**\n * Arguments supplied by the model. During streaming this is a partial parse:\n * fields may be missing or incomplete. From a tool-call renderer, use\n * `useToolArgsStatus` to detect which fields are still arriving.\n */\n readonly args: TArgs;\n /** Result returned by the tool, if it has completed. */\n readonly result?: TResult | undefined;\n /** Whether the result represents a tool execution error. */\n readonly isError?: boolean | undefined;\n /** Raw JSON argument text streamed by the model. */\n readonly argsText: string;\n /** UI-only artifact associated with the tool result. */\n readonly artifact?: unknown;\n /** MCP app metadata associated with this tool call, when present. */\n readonly mcp?: ToolCallMessagePartMcpMetadata;\n /** Content returned to the model for this tool result. */\n readonly modelContent?: readonly ToolModelContentPart[] | undefined;\n /** Human-input request that must be resolved before the run can continue. */\n readonly interrupt?: { type: \"human\"; payload: unknown };\n /** Server-side approval gate. `approved === undefined` is the only state in which `respondToApproval` may be called. */\n readonly approval?: {\n readonly id: string;\n readonly approved?: boolean;\n readonly reason?: string;\n readonly isAutomatic?: boolean;\n };\n /** Parent message-part ID when this part belongs to a nested structure. */\n readonly parentId?: string;\n /**\n * Nested thread messages produced by this tool call, for example a sub-agent\n * conversation.\n */\n readonly messages?: readonly ThreadMessage[];\n};\n\nexport type ThreadUserMessagePart =\n | TextMessagePart\n | ImageMessagePart\n | FileMessagePart\n | DataMessagePart\n | Unstable_AudioMessagePart;\n\nexport type ThreadAssistantMessagePart =\n | TextMessagePart\n | ReasoningMessagePart\n | ToolCallMessagePart\n | SourceMessagePart\n | FileMessagePart\n | ImageMessagePart\n | DataMessagePart\n | GenerativeUIMessagePart;\n\nexport type MessagePartStatus =\n | {\n readonly type: \"running\";\n }\n | {\n readonly type: \"complete\";\n }\n | {\n readonly type: \"incomplete\";\n readonly reason:\n | \"cancelled\"\n | \"length\"\n | \"content-filter\"\n | \"other\"\n | \"error\";\n readonly error?: unknown;\n };\n\nexport type ToolCallMessagePartStatus =\n | {\n /** The tool call is waiting for UI or human input before continuing. */\n readonly type: \"requires-action\";\n /** Reason the tool call requires action. */\n readonly reason: \"interrupt\";\n }\n | MessagePartStatus;\n\nexport type MessageStatus =\n | {\n readonly type: \"running\";\n }\n | {\n readonly type: \"requires-action\";\n readonly reason: \"tool-calls\" | \"interrupt\";\n }\n | {\n readonly type: \"complete\";\n readonly reason: \"stop\" | \"unknown\";\n }\n | {\n readonly type: \"incomplete\";\n readonly reason:\n | \"cancelled\"\n | \"tool-calls\"\n | \"length\"\n | \"content-filter\"\n | \"other\"\n | \"error\";\n readonly error?: ReadonlyJSONValue;\n };\n\nexport type MessageTiming = {\n readonly streamStartTime: number;\n readonly firstTokenTime?: number;\n readonly totalStreamTime?: number;\n readonly tokenCount?: number;\n readonly tokensPerSecond?: number;\n readonly totalChunks: number;\n readonly toolCallCount: number;\n};\n\nexport type ThreadStep = {\n readonly messageId?: string;\n readonly usage?:\n | {\n readonly inputTokens: number;\n readonly outputTokens: number;\n }\n | undefined;\n};\n\ntype MessageCommonProps = {\n readonly id: string;\n readonly createdAt: Date;\n};\n\nexport type ThreadSystemMessage = MessageCommonProps & {\n readonly role: \"system\";\n readonly content: readonly [TextMessagePart];\n readonly metadata: {\n readonly unstable_state?: undefined;\n readonly unstable_annotations?: undefined;\n readonly unstable_data?: undefined;\n readonly steps?: undefined;\n readonly submittedFeedback?: undefined;\n readonly timing?: undefined;\n readonly custom: Record<string, unknown>;\n };\n};\n\nexport type ThreadUserMessage = MessageCommonProps & {\n readonly role: \"user\";\n readonly content: readonly ThreadUserMessagePart[];\n readonly attachments: readonly CompleteAttachment[];\n readonly metadata: {\n readonly unstable_state?: undefined;\n readonly unstable_annotations?: undefined;\n readonly unstable_data?: undefined;\n readonly steps?: undefined;\n readonly submittedFeedback?: undefined;\n readonly timing?: undefined;\n readonly custom: Record<string, unknown>;\n };\n};\n\nexport type ThreadAssistantMessage = MessageCommonProps & {\n readonly role: \"assistant\";\n readonly content: readonly ThreadAssistantMessagePart[];\n readonly status: MessageStatus;\n readonly metadata: {\n readonly unstable_state: ReadonlyJSONValue;\n readonly unstable_annotations: readonly ReadonlyJSONValue[];\n readonly unstable_data: readonly ReadonlyJSONValue[];\n readonly steps: readonly ThreadStep[];\n readonly submittedFeedback?: { readonly type: \"positive\" | \"negative\" };\n readonly timing?: MessageTiming;\n readonly custom: Record<string, unknown>;\n };\n};\n\ntype BaseThreadMessage = {\n readonly status?: ThreadAssistantMessage[\"status\"];\n readonly metadata: {\n readonly unstable_state?: ReadonlyJSONValue;\n readonly unstable_annotations?: readonly ReadonlyJSONValue[];\n readonly unstable_data?: readonly ReadonlyJSONValue[];\n readonly steps?: readonly ThreadStep[];\n readonly submittedFeedback?: { readonly type: \"positive\" | \"negative\" };\n readonly timing?: MessageTiming;\n readonly custom: Record<string, unknown>;\n };\n readonly attachments?: ThreadUserMessage[\"attachments\"];\n};\n\nexport type ThreadMessage = BaseThreadMessage &\n (ThreadSystemMessage | ThreadUserMessage | ThreadAssistantMessage);\n\nexport type MessageRole = ThreadMessage[\"role\"];\n\nexport type RunConfig = {\n readonly custom?: Record<string, unknown>;\n};\n\nexport type AppendMessage = Omit<ThreadMessage, \"id\"> & {\n parentId: string | null;\n\n /** The ID of the message that was edited or undefined. */\n sourceId: string | null;\n runConfig: RunConfig | undefined;\n startRun?: boolean | undefined;\n};\n"],"mappings":";AAgIA,MAAa,qBAAqB;AAElC,MAAa,eAAe,QAC1B,CAAC,CAAC,KAAK,WAAW,kBAAkB"}
1
+ {"version":3,"file":"message.js","names":[],"sources":["../../src/types/message.ts"],"sourcesContent":["import type {\n ReadonlyJSONObject,\n ReadonlyJSONValue,\n} from \"assistant-stream/utils\";\nimport type { ToolModelContentPart } from \"assistant-stream\";\nimport type { CompleteAttachment } from \"./attachment\";\n\nexport type { ToolModelContentPart };\n\nexport type TextMessagePart = {\n readonly type: \"text\";\n readonly text: string;\n readonly parentId?: string;\n};\n\nexport type ReasoningMessagePart = {\n readonly type: \"reasoning\";\n readonly text: string;\n readonly parentId?: string;\n};\n\nexport type SourceProviderMetadata = {\n readonly [providerName: string]: ReadonlyJSONObject;\n};\n\nexport type SourceMessagePart =\n | {\n readonly type: \"source\";\n readonly sourceType: \"url\";\n readonly id: string;\n readonly url: string;\n readonly title?: string;\n readonly providerMetadata?: SourceProviderMetadata;\n readonly parentId?: string;\n }\n | {\n readonly type: \"source\";\n readonly sourceType: \"document\";\n readonly id: string;\n readonly url?: undefined;\n readonly title: string;\n readonly mediaType: string;\n readonly filename?: string;\n readonly providerMetadata?: SourceProviderMetadata;\n readonly parentId?: string;\n };\n\nexport type ImageMessagePart = {\n readonly type: \"image\";\n readonly image: string;\n readonly filename?: string;\n};\n\nexport type FileMessagePart = {\n readonly type: \"file\";\n readonly filename?: string;\n readonly data: string;\n readonly mimeType: string;\n readonly parentId?: string;\n};\n\nexport type Unstable_AudioMessagePart = {\n readonly type: \"audio\";\n readonly audio: {\n readonly data: string;\n readonly format: \"mp3\" | \"wav\";\n };\n};\n\nexport type DataMessagePart<T = any> = {\n readonly type: \"data\";\n readonly name: string;\n readonly data: T;\n};\n\n/**\n * A JSON spec describing a tree of UI components to render.\n *\n * The agent emits a {@link GenerativeUIMessagePart} containing this spec, and\n * the consumer-provided component allowlist is used to resolve `component`\n * names. Any component referenced that is not present in the allowlist is\n * rejected with a typed error — the allowlist is the security boundary in the\n * default same-realm rendering path.\n */\nexport type GenerativeUINode =\n | string\n | {\n /** Allowlisted component name (resolved against the consumer registry). */\n readonly component: string;\n /** Props passed to the resolved component (must be JSON-serializable). */\n readonly props?: Record<string, unknown>;\n /** Optional children — strings render as text, objects recurse. */\n readonly children?: readonly GenerativeUINode[];\n /** Optional stable key for React reconciliation. */\n readonly key?: string;\n };\n\n/**\n * The root spec for a generative UI tree.\n */\nexport type GenerativeUISpec = {\n /** Root node(s) to render. */\n readonly root: GenerativeUINode | readonly GenerativeUINode[];\n};\n\n/**\n * A message part that carries a JSON spec describing UI to render.\n *\n * Render with `<MessagePrimitive.GenerativeUI components={...} />`. The\n * primitive resolves component names against the consumer-provided allowlist\n * — any unknown name throws a typed error rather than rendering. Stream-\n * friendly: a partially-streamed spec renders progressively.\n */\nexport type GenerativeUIMessagePart = {\n readonly type: \"generative-ui\";\n /** The JSON spec describing the UI tree. */\n readonly spec: GenerativeUISpec;\n /** Optional id (useful for replays / stable keys). */\n readonly id?: string;\n readonly parentId?: string;\n};\n\nexport type McpAppMetadata = {\n readonly resourceUri: string;\n readonly mimeType?: string;\n readonly visibility?: readonly (\"model\" | \"app\")[];\n};\n\nexport const MCP_APP_URI_SCHEME = \"ui://\";\n\nexport const isMcpAppUri = (uri: string | undefined): boolean =>\n !!uri?.startsWith(MCP_APP_URI_SCHEME);\n\nexport type ToolCallMessagePartMcpMetadata = {\n readonly app?: McpAppMetadata;\n};\n\nexport type ToolCallMessagePart<\n TArgs = ReadonlyJSONObject,\n TResult = unknown,\n> = {\n /** Identifies this part as a tool call. */\n readonly type: \"tool-call\";\n /** Stable identifier for this invocation of the tool. */\n readonly toolCallId: string;\n /** Name of the tool requested by the model. */\n readonly toolName: string;\n /**\n * Arguments supplied by the model. During streaming this is a partial parse:\n * fields may be missing or incomplete. From a tool-call renderer, use\n * `useToolArgsStatus` to detect which fields are still arriving.\n */\n readonly args: TArgs;\n /** Result returned by the tool, if it has completed. */\n readonly result?: TResult | undefined;\n /** Whether the result represents a tool execution error. */\n readonly isError?: boolean | undefined;\n /** Raw JSON argument text streamed by the model. */\n readonly argsText: string;\n /** UI-only artifact associated with the tool result. */\n readonly artifact?: unknown;\n /** MCP app metadata associated with this tool call, when present. */\n readonly mcp?: ToolCallMessagePartMcpMetadata;\n /** Content returned to the model for this tool result. */\n readonly modelContent?: readonly ToolModelContentPart[] | undefined;\n /** Human-input request that must be resolved before the run can continue. */\n readonly interrupt?: { type: \"human\"; payload: unknown };\n /** Server-side approval gate. `approved === undefined` is the only state in which `respondToApproval` may be called. */\n readonly approval?: {\n readonly id: string;\n readonly approved?: boolean;\n readonly reason?: string;\n readonly isAutomatic?: boolean;\n };\n /** Parent message-part ID when this part belongs to a nested structure. */\n readonly parentId?: string;\n /**\n * Nested thread messages produced by this tool call, for example a sub-agent\n * conversation.\n */\n readonly messages?: readonly ThreadMessage[];\n};\n\nexport type ThreadUserMessagePart =\n | TextMessagePart\n | ImageMessagePart\n | FileMessagePart\n | DataMessagePart\n | Unstable_AudioMessagePart;\n\nexport type ThreadAssistantMessagePart =\n | TextMessagePart\n | ReasoningMessagePart\n | ToolCallMessagePart\n | SourceMessagePart\n | FileMessagePart\n | ImageMessagePart\n | DataMessagePart\n | GenerativeUIMessagePart;\n\nexport type MessagePartStatus =\n | {\n readonly type: \"running\";\n }\n | {\n readonly type: \"complete\";\n }\n | {\n readonly type: \"incomplete\";\n readonly reason:\n | \"cancelled\"\n | \"length\"\n | \"content-filter\"\n | \"other\"\n | \"error\";\n readonly error?: unknown;\n };\n\nexport type ToolCallMessagePartStatus =\n | {\n /** The tool call is waiting for UI or human input before continuing. */\n readonly type: \"requires-action\";\n /** Reason the tool call requires action. */\n readonly reason: \"interrupt\";\n }\n | MessagePartStatus;\n\nexport type MessageStatus =\n | {\n readonly type: \"running\";\n }\n | {\n readonly type: \"requires-action\";\n readonly reason: \"tool-calls\" | \"interrupt\";\n }\n | {\n readonly type: \"complete\";\n readonly reason: \"stop\" | \"unknown\";\n }\n | {\n readonly type: \"incomplete\";\n readonly reason:\n | \"cancelled\"\n | \"tool-calls\"\n | \"length\"\n | \"content-filter\"\n | \"other\"\n | \"error\";\n readonly error?: ReadonlyJSONValue;\n };\n\nexport type MessageTiming = {\n readonly streamStartTime: number;\n readonly firstTokenTime?: number;\n readonly totalStreamTime?: number;\n readonly tokenCount?: number;\n readonly tokensPerSecond?: number;\n readonly totalChunks: number;\n readonly toolCallCount: number;\n};\n\nexport type ThreadStep = {\n readonly messageId?: string;\n readonly usage?:\n | {\n readonly inputTokens: number;\n readonly outputTokens: number;\n }\n | undefined;\n};\n\ntype MessageCommonProps = {\n readonly id: string;\n readonly createdAt: Date;\n};\n\nexport type ThreadSystemMessage = MessageCommonProps & {\n readonly role: \"system\";\n readonly content: readonly [TextMessagePart];\n readonly metadata: {\n readonly unstable_state?: undefined;\n readonly unstable_annotations?: undefined;\n readonly unstable_data?: undefined;\n readonly steps?: undefined;\n readonly submittedFeedback?: undefined;\n readonly timing?: undefined;\n readonly custom: Record<string, unknown>;\n };\n};\n\nexport type ThreadUserMessage = MessageCommonProps & {\n readonly role: \"user\";\n readonly content: readonly ThreadUserMessagePart[];\n readonly attachments: readonly CompleteAttachment[];\n readonly metadata: {\n readonly unstable_state?: undefined;\n readonly unstable_annotations?: undefined;\n readonly unstable_data?: undefined;\n readonly steps?: undefined;\n readonly submittedFeedback?: undefined;\n readonly timing?: undefined;\n readonly custom: Record<string, unknown>;\n };\n};\n\nexport type ThreadAssistantMessage = MessageCommonProps & {\n readonly role: \"assistant\";\n readonly content: readonly ThreadAssistantMessagePart[];\n readonly status: MessageStatus;\n readonly metadata: {\n readonly unstable_state: ReadonlyJSONValue;\n readonly unstable_annotations: readonly ReadonlyJSONValue[];\n readonly unstable_data: readonly ReadonlyJSONValue[];\n readonly steps: readonly ThreadStep[];\n readonly submittedFeedback?: { readonly type: \"positive\" | \"negative\" };\n readonly timing?: MessageTiming;\n /**\n * Marks a client-side optimistic placeholder. Such messages are evicted\n * once off the head branch and are never persisted.\n */\n readonly isOptimistic?: boolean;\n readonly custom: Record<string, unknown>;\n };\n};\n\ntype BaseThreadMessage = {\n readonly status?: ThreadAssistantMessage[\"status\"];\n readonly metadata: {\n readonly unstable_state?: ReadonlyJSONValue;\n readonly unstable_annotations?: readonly ReadonlyJSONValue[];\n readonly unstable_data?: readonly ReadonlyJSONValue[];\n readonly steps?: readonly ThreadStep[];\n readonly submittedFeedback?: { readonly type: \"positive\" | \"negative\" };\n readonly timing?: MessageTiming;\n readonly isOptimistic?: boolean;\n readonly custom: Record<string, unknown>;\n };\n readonly attachments?: ThreadUserMessage[\"attachments\"];\n};\n\nexport type ThreadMessage = BaseThreadMessage &\n (ThreadSystemMessage | ThreadUserMessage | ThreadAssistantMessage);\n\nexport type MessageRole = ThreadMessage[\"role\"];\n\nexport type RunConfig = {\n readonly custom?: Record<string, unknown>;\n};\n\nexport type AppendMessage = Omit<ThreadMessage, \"id\"> & {\n parentId: string | null;\n\n /** The ID of the message that was edited or undefined. */\n sourceId: string | null;\n runConfig: RunConfig | undefined;\n startRun?: boolean | undefined;\n};\n"],"mappings":";AAgIA,MAAa,qBAAqB;AAElC,MAAa,eAAe,QAC1B,CAAC,CAAC,KAAK,WAAW,kBAAkB"}
@@ -1 +1 @@
1
- {"version":3,"file":"composite-context-provider.d.ts","names":[],"sources":["../../src/utils/composite-context-provider.ts"],"mappings":";;cAKa,wBAAA,YAAoC,oBAAA;EAAA,QACvC,UAAA;EAER,eAAA,CAAA,GAHoC,YAAA;EAOpC,4BAAA,CAA6B,QAAA,EAAU,oBAAA;EAAA,QAa/B,YAAA;EAER,iBAAA,CAAA;EAIA,SAAA,CAAU,QAAA;AAAA"}
1
+ {"version":3,"file":"composite-context-provider.d.ts","names":[],"sources":["../../src/utils/composite-context-provider.ts"],"mappings":";;cAKa,wBAAA,YAAoC,oBAAA;EAAA,QACvC,UAAA;EAER,eAAA,IAHoC,YAAA;EAOpC,4BAAA,CAA6B,QAAA,EAAU,oBAAA;EAAA,QAa/B,YAAA;EAER,iBAAA;EAIA,SAAA,CAAU,QAAA;AAAA"}
@@ -1,9 +1,7 @@
1
1
  //#region src/utils/id.d.ts
2
2
  declare const generateId: (size?: number) => string;
3
- declare const generateOptimisticId: () => string;
4
- declare const isOptimisticId: (id: string) => boolean;
5
3
  declare const generateErrorMessageId: () => string;
6
4
  declare const isErrorMessageId: (id: string) => boolean;
7
5
  //#endregion
8
- export { generateErrorMessageId, generateId, generateOptimisticId, isErrorMessageId, isOptimisticId };
6
+ export { generateErrorMessageId, generateId, isErrorMessageId };
9
7
  //# sourceMappingURL=id.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"id.d.ts","names":[],"sources":["../../src/utils/id.ts"],"mappings":";cAEa,UAAA,GAAU,IAAA;AAAA,cAMV,oBAAA;AAAA,cACA,cAAA,GAAkB,EAAU;AAAA,cAG5B,sBAAA;AAAA,cACA,gBAAA,GAAoB,EAAU"}
1
+ {"version":3,"file":"id.d.ts","names":[],"sources":["../../src/utils/id.ts"],"mappings":";cAEa,UAAA,GAAU,IAAA;AAAA,cAMV,sBAAA;AAAA,cACA,gBAAA,GAAoB,EAAU"}
package/dist/utils/id.js CHANGED
@@ -1,13 +1,10 @@
1
1
  import { customAlphabet } from "nanoid/non-secure";
2
2
  //#region src/utils/id.ts
3
3
  const generateId = customAlphabet("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 7);
4
- const optimisticPrefix = "__optimistic__";
5
- const generateOptimisticId = () => `${optimisticPrefix}${generateId()}`;
6
- const isOptimisticId = (id) => id.startsWith(optimisticPrefix);
7
4
  const errorPrefix = "__error__";
8
5
  const generateErrorMessageId = () => `${errorPrefix}${generateId()}`;
9
6
  const isErrorMessageId = (id) => id.startsWith(errorPrefix);
10
7
  //#endregion
11
- export { generateErrorMessageId, generateId, generateOptimisticId, isErrorMessageId, isOptimisticId };
8
+ export { generateErrorMessageId, generateId, isErrorMessageId };
12
9
 
13
10
  //# sourceMappingURL=id.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"id.js","names":[],"sources":["../../src/utils/id.ts"],"sourcesContent":["import { customAlphabet } from \"nanoid/non-secure\";\n\nexport const generateId = customAlphabet(\n \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\",\n 7,\n);\n\nconst optimisticPrefix = \"__optimistic__\";\nexport const generateOptimisticId = () => `${optimisticPrefix}${generateId()}`;\nexport const isOptimisticId = (id: string) => id.startsWith(optimisticPrefix);\n\nconst errorPrefix = \"__error__\";\nexport const generateErrorMessageId = () => `${errorPrefix}${generateId()}`;\nexport const isErrorMessageId = (id: string) => id.startsWith(errorPrefix);\n"],"mappings":";;AAEA,MAAa,aAAa,eACxB,kEACA,CACF;AAEA,MAAM,mBAAmB;AACzB,MAAa,6BAA6B,GAAG,mBAAmB,WAAW;AAC3E,MAAa,kBAAkB,OAAe,GAAG,WAAW,gBAAgB;AAE5E,MAAM,cAAc;AACpB,MAAa,+BAA+B,GAAG,cAAc,WAAW;AACxE,MAAa,oBAAoB,OAAe,GAAG,WAAW,WAAW"}
1
+ {"version":3,"file":"id.js","names":[],"sources":["../../src/utils/id.ts"],"sourcesContent":["import { customAlphabet } from \"nanoid/non-secure\";\n\nexport const generateId = customAlphabet(\n \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\",\n 7,\n);\n\nconst errorPrefix = \"__error__\";\nexport const generateErrorMessageId = () => `${errorPrefix}${generateId()}`;\nexport const isErrorMessageId = (id: string) => id.startsWith(errorPrefix);\n"],"mappings":";;AAEA,MAAa,aAAa,eACxB,kEACA,CACF;AAEA,MAAM,cAAc;AACpB,MAAa,+BAA+B,GAAG,cAAc,WAAW;AACxE,MAAa,oBAAoB,OAAe,GAAG,WAAW,WAAW"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/core",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "Framework-agnostic core runtime for assistant-ui",
5
5
  "keywords": [
6
6
  "assistant",
@@ -59,15 +59,15 @@
59
59
  ],
60
60
  "sideEffects": false,
61
61
  "dependencies": {
62
- "assistant-stream": "^0.3.17",
62
+ "assistant-stream": "^0.3.18",
63
63
  "nanoid": "^5.1.11"
64
64
  },
65
65
  "peerDependencies": {
66
- "@assistant-ui/store": "^0.2.12",
67
- "@assistant-ui/tap": "^0.5.12",
66
+ "@assistant-ui/store": "^0.2.13",
67
+ "@assistant-ui/tap": "^0.5.14",
68
68
  "@types/react": "*",
69
69
  "react": "^18 || ^19",
70
- "assistant-cloud": "^0.1.29",
70
+ "assistant-cloud": "^0.1.30",
71
71
  "zustand": "^5.0.11"
72
72
  },
73
73
  "peerDependenciesMeta": {
@@ -88,11 +88,11 @@
88
88
  "@types/react": "^19.2.15",
89
89
  "react": "^19.2.6",
90
90
  "vitest": "^4.1.7",
91
- "zustand": "^5.0.13",
92
- "@assistant-ui/store": "0.2.12",
93
- "@assistant-ui/tap": "0.5.12",
94
- "@assistant-ui/x-buildutils": "0.0.9",
95
- "assistant-cloud": "0.1.29"
91
+ "zustand": "^5.0.14",
92
+ "@assistant-ui/store": "0.2.13",
93
+ "@assistant-ui/tap": "0.5.14",
94
+ "@assistant-ui/x-buildutils": "0.0.10",
95
+ "assistant-cloud": "0.1.30"
96
96
  },
97
97
  "publishConfig": {
98
98
  "access": "public",
@@ -8,10 +8,7 @@ export {
8
8
 
9
9
  // Speech adapters
10
10
  export type { SpeechSynthesisAdapter, DictationAdapter } from "./speech";
11
- export {
12
- WebSpeechSynthesisAdapter,
13
- WebSpeechDictationAdapter,
14
- } from "./speech";
11
+ export { WebSpeechSynthesisAdapter, WebSpeechDictationAdapter } from "./speech";
15
12
 
16
13
  // Voice adapter
17
14
  export type { RealtimeVoiceAdapter } from "./voice";
@@ -64,7 +64,6 @@ export class WebSpeechSynthesisAdapter implements SpeechSynthesisAdapter {
64
64
  if (res.status.type === "ended") return;
65
65
 
66
66
  res.status = { type: "ended", reason, error };
67
- // biome-ignore lint/suspicious/useIterableCallbackReturn: forEach callback intentionally has no return
68
67
  subscribers.forEach((handler) => handler());
69
68
  };
70
69
 
package/src/index.ts CHANGED
@@ -1,5 +1,13 @@
1
1
  // @assistant-ui/core - Framework-agnostic core runtime (public API)
2
2
 
3
+ /// <reference path="./store/scope-registration.ts" />
4
+
5
+ import { checkDuplicateCore } from "./internal/duplicate-detection";
6
+
7
+ if (process.env.NODE_ENV !== "production") {
8
+ checkDuplicateCore();
9
+ }
10
+
3
11
  export type {
4
12
  // Message parts
5
13
  TextMessagePart,
@@ -80,6 +88,8 @@ export { mergeModelContexts } from "./model-context/types";
80
88
 
81
89
  export { tool } from "./model-context/tool";
82
90
 
91
+ export type { ToolExecutionStatus } from "./runtimes/tool-invocations/ToolInvocationTracker";
92
+
83
93
  export { ModelContextRegistry } from "./model-context/registry";
84
94
  export type {
85
95
  ModelContextRegistryToolHandle,
@@ -272,6 +282,8 @@ export type {
272
282
  ExternalStoreThreadListAdapter,
273
283
  ExternalStoreThreadData,
274
284
  } from "./runtimes/external-store/external-store-adapter";
285
+ export type { ExternalStoreSharedOptions } from "./runtimes/external-store/external-store-shared-options";
286
+ export { pickExternalStoreSharedOptions } from "./runtimes/external-store/external-store-shared-options";
275
287
 
276
288
  // Remote Thread List (user-facing)
277
289
  export type {
@@ -0,0 +1,26 @@
1
+ // Warns once if a second copy of @assistant-ui/core is loaded into the
2
+ // same runtime. Mismatched transitive versions of core silently break
3
+ // runtime behavior — tools registered via `makeAssistantTool` don't reach
4
+ // the active runtime, context lookups resolve to the wrong provider,
5
+ // `instanceof` checks fail (see issue #4101). The actual version diagnosis
6
+ // lives in `npx assistant-ui doctor`.
7
+ //
8
+ // The caller is responsible for gating on `process.env.NODE_ENV` so this
9
+ // module tree-shakes out of production bundles.
10
+
11
+ const KEY = Symbol.for("@assistant-ui/core.loaded");
12
+
13
+ export function checkDuplicateCore(): void {
14
+ const g = globalThis as unknown as Record<symbol, boolean | undefined>;
15
+ if (g[KEY]) {
16
+ // eslint-disable-next-line no-console
17
+ console.warn(
18
+ "[@assistant-ui/core] Multiple copies of @assistant-ui/core are " +
19
+ "loaded into the same runtime. This causes subtle bugs (tools not " +
20
+ "reaching the runtime, context lookups returning the wrong " +
21
+ "provider, instanceof checks failing). Run " +
22
+ "`npx assistant-ui doctor` to diagnose.",
23
+ );
24
+ }
25
+ g[KEY] = true;
26
+ }
package/src/internal.ts CHANGED
@@ -26,8 +26,6 @@ export type {
26
26
  // ID generation
27
27
  export {
28
28
  generateId,
29
- generateOptimisticId,
30
- isOptimisticId,
31
29
  generateErrorMessageId,
32
30
  isErrorMessageId,
33
31
  } from "./utils/id";
@@ -175,7 +175,6 @@ export class AssistantFrameHost implements ModelContextProvider {
175
175
  }
176
176
 
177
177
  private notifySubscribers() {
178
- // biome-ignore lint/suspicious/useIterableCallbackReturn: forEach callback intentionally has no return
179
178
  this._subscribers.forEach((callback) => callback());
180
179
  }
181
180
 
@@ -179,7 +179,6 @@ export class AssistantFrameProvider {
179
179
  const instance = AssistantFrameProvider._instance;
180
180
  window.removeEventListener("message", instance.handleMessage);
181
181
 
182
- // biome-ignore lint/suspicious/useIterableCallbackReturn: forEach callback intentionally has no return
183
182
  instance._providerUnsubscribes.forEach((unsubscribe) => unsubscribe?.());
184
183
  instance._providerUnsubscribes.clear();
185
184
  instance._providers.clear();
@@ -10,9 +10,8 @@ import type { AssistantRuntimeCore } from "../runtime/interfaces/assistant-runti
10
10
  import { RuntimeAdapter } from "./RuntimeAdapter";
11
11
 
12
12
  export const getRenderComponent = (runtime: AssistantRuntime) => {
13
- return (runtime as { _core?: AssistantRuntimeCore })._core?.RenderComponent as
14
- | ComponentType
15
- | undefined;
13
+ return (runtime as { _core?: AssistantRuntimeCore })._core
14
+ ?.RenderComponent as ComponentType | undefined;
16
15
  };
17
16
 
18
17
  export type AssistantProviderBaseProps = PropsWithChildren<{
@@ -253,7 +253,6 @@ export const Interactables = resource((): ClientOutput<"interactables"> => {
253
253
  [setDefState],
254
254
  );
255
255
 
256
- // biome-ignore lint/correctness/useExhaustiveDependencies: state dep triggers notification
257
256
  tapEffect(() => {
258
257
  for (const cb of subscribersRef.current) cb();
259
258
  }, [state]);
@@ -15,7 +15,10 @@ import {
15
15
  } from "@assistant-ui/store";
16
16
  import type { McpAppResourceOutput, ToolsState } from "../types/scopes/tools";
17
17
  import type { Tool } from "assistant-stream";
18
- import type { Toolkit } from "../model-context/toolbox";
18
+ import {
19
+ isStandaloneToolDisplay,
20
+ type Toolkit,
21
+ } from "../model-context/toolbox";
19
22
  import type { ToolCallMessagePartComponent } from "../types/MessagePartComponentTypes";
20
23
  import { ModelContext } from "../../store";
21
24
 
@@ -45,36 +48,53 @@ export const Tools = resource(
45
48
  );
46
49
  const mcpAppOutput = mcpAppOutputs[0];
47
50
 
48
- const [toolsState, setToolsState] = tapState<{
49
- tools: ToolsState["tools"];
50
- }>(() => ({
51
- tools: {},
52
- }));
51
+ const [toolUIs, setToolUIs] = tapState<ToolsState["toolUIs"]>(() => ({}));
53
52
 
54
53
  const state = tapMemo(
55
- (): ToolsState => ({ tools: toolsState.tools, mcpApp: mcpAppOutput }),
56
- [toolsState, mcpAppOutput],
54
+ (): ToolsState => ({
55
+ toolUIs,
56
+ mcpApp: mcpAppOutput,
57
+ // Deprecated component-only view, derived from `toolUIs`. Removed in v0.15.
58
+ tools: Object.fromEntries(
59
+ Object.entries(toolUIs).map(([name, regs]) => [
60
+ name,
61
+ regs.map((r) => r.render),
62
+ ]),
63
+ ),
64
+ }),
65
+ [toolUIs, mcpAppOutput],
57
66
  );
58
67
 
59
68
  const clientRef = tapAssistantClientRef();
60
69
 
61
70
  const setToolUI = tapCallback(
62
- (toolName: string, render: ToolCallMessagePartComponent) => {
63
- setToolsState((prev) => ({
64
- tools: {
65
- ...prev.tools,
66
- [toolName]: [...(prev.tools[toolName] ?? []), render],
67
- },
71
+ (
72
+ toolName: string,
73
+ render: ToolCallMessagePartComponent,
74
+ options?: { standalone?: boolean },
75
+ ) => {
76
+ // One registration object per call; identity is the removal key, so
77
+ // the per-name list stays correctly ref-counted across re-registers.
78
+ const registration = {
79
+ render,
80
+ standalone: options?.standalone ?? false,
81
+ };
82
+
83
+ setToolUIs((prev) => ({
84
+ ...prev,
85
+ [toolName]: [...(prev[toolName] ?? []), registration],
68
86
  }));
69
87
 
70
88
  return () => {
71
- setToolsState((prev) => ({
72
- tools: {
73
- ...prev.tools,
74
- [toolName]:
75
- prev.tools[toolName]?.filter((r) => r !== render) ?? [],
76
- },
77
- }));
89
+ setToolUIs((prev) => {
90
+ const next =
91
+ prev[toolName]?.filter((r) => r !== registration) ?? [];
92
+ if (next.length > 0) return { ...prev, [toolName]: next };
93
+ // Drop the key entirely so repeatedly mounted/unmounted tools
94
+ // don't leave empty arrays accumulating across a long session.
95
+ const { [toolName]: _removed, ...rest } = prev;
96
+ return rest;
97
+ });
78
98
  };
79
99
  },
80
100
  [],
@@ -87,14 +107,20 @@ export const Tools = resource(
87
107
  // Register tool UIs (exclude symbols)
88
108
  for (const [toolName, tool] of Object.entries(toolkit)) {
89
109
  if (tool.render) {
90
- unsubscribes.push(setToolUI(toolName, tool.render));
110
+ unsubscribes.push(
111
+ setToolUI(toolName, tool.render, {
112
+ standalone: isStandaloneToolDisplay(tool),
113
+ }),
114
+ );
91
115
  }
92
116
  }
93
117
 
94
- // Register tools with model context (exclude symbols)
118
+ // Register tools with model context (exclude symbols). `render` and
119
+ // `display` are client-only presentation concerns and never reach the
120
+ // model.
95
121
  const toolsWithoutRender = Object.entries(toolkit).reduce(
96
122
  (acc, [name, tool]) => {
97
- const { render, ...rest } = tool;
123
+ const { render, display, ...rest } = tool;
98
124
  acc[name] = rest;
99
125
  return acc;
100
126
  },
@@ -112,7 +138,6 @@ export const Tools = resource(
112
138
  );
113
139
 
114
140
  return () => {
115
- // biome-ignore lint/suspicious/useIterableCallbackReturn: forEach callback intentionally has no return
116
141
  unsubscribes.forEach((fn) => fn());
117
142
  };
118
143
  }, [toolkit, setToolUI, clientRef]);
@@ -1,3 +1,4 @@
1
+ /// <reference path="../store/scope-registration.ts" />
1
2
  /// <reference path="./types/store-augmentation.ts" />
2
3
 
3
4
  // model-context
@@ -31,7 +32,12 @@ export {
31
32
  type AssistantDataUIProps,
32
33
  } from "./model-context/useAssistantDataUI";
33
34
  export { useInlineRender } from "./model-context/useInlineRender";
34
- export type { Toolkit, ToolDefinition } from "./model-context/toolbox";
35
+ export {
36
+ type Toolkit,
37
+ type ToolDefinition,
38
+ type ToolkitDeclaration,
39
+ type ToolkitDeclarationDefinition,
40
+ } from "./model-context/toolbox";
35
41
  export {
36
42
  useAssistantInteractable,
37
43
  type AssistantInteractableProps,
@@ -129,13 +135,8 @@ export {
129
135
  useRuntimeAdapters,
130
136
  type RuntimeAdapters,
131
137
  } from "./runtimes/RuntimeAdapterProvider";
132
- export {
133
- useToolInvocations,
134
- type ToolExecutionStatus,
135
- type AssistantTransportState,
136
- type AddToolResultCommand,
137
- } from "./runtimes/useToolInvocations";
138
138
  export { useExternalStoreRuntime } from "./runtimes/useExternalStoreRuntime";
139
+ export { useExternalStoreSharedOptions } from "./runtimes/useExternalStoreSharedOptions";
139
140
  export {
140
141
  useExternalMessageConverter,
141
142
  convertExternalMessages,
@@ -178,7 +179,7 @@ export {
178
179
  type PartState,
179
180
  } from "./primitives/message/MessageParts";
180
181
  export { MessagePrimitiveGroupedParts } from "./primitives/message/MessageGroupedParts";
181
- export { groupPartByType } from "./utils/groupParts";
182
+ export { groupPartByType, type GroupByContext } from "./utils/groupParts";
182
183
  export {
183
184
  MessagePrimitiveGenerativeUI,
184
185
  GenerativeUIRender,
@@ -1,6 +1,22 @@
1
- import type { Tool } from "assistant-stream";
1
+ import type { Tool, ToolDeclaration } from "assistant-stream";
2
2
  import type { ToolCallMessagePartComponent } from "../types/MessagePartComponentTypes";
3
3
 
4
+ /**
5
+ * Resolves whether a tool's UI should be presented standalone (outside the
6
+ * chain-of-thought grouping), applying the type-based defaults.
7
+ *
8
+ * An explicit `display` wins. Otherwise `human` tools default to standalone
9
+ * (they prompt the user), and every other tool defaults to inline (a trace of
10
+ * what the model is doing). MCP-app tool calls are detected separately from
11
+ * the part itself and are not resolved here.
12
+ */
13
+ export const isStandaloneToolDisplay = (
14
+ tool: Pick<Tool<any, any>, "type" | "display">,
15
+ ): boolean => {
16
+ if (tool.display !== undefined) return tool.display === "standalone";
17
+ return tool.type === "human";
18
+ };
19
+
4
20
  type WithRender<T, TArgs extends Record<string, unknown>, TResult> = T extends {
5
21
  type: "frontend" | "human";
6
22
  }
@@ -43,6 +59,35 @@ export type ToolDefinition<
43
59
  */
44
60
  export type Toolkit = Record<string, ToolDefinition<any, any>>;
45
61
 
62
+ /**
63
+ * A tool as authored, before the build splits it: like {@link ToolDefinition}
64
+ * but it may declare `description`, `parameters`, and a server-side `execute`
65
+ * alongside its `render`. The `type` field is **not** authored — the
66
+ * `"use generative"` compiler infers it (`execute: hitl()` → human; `execute`
67
+ * with a `"use client"` directive → frontend; otherwise backend) and writes it
68
+ * back — so declaring it here is a type error.
69
+ */
70
+ export type ToolkitDeclarationDefinition<
71
+ TArgs extends Record<string, unknown>,
72
+ TResult,
73
+ > = WithRender<
74
+ Omit<ToolDeclaration<TArgs, TResult>, "type">,
75
+ TArgs,
76
+ TResult
77
+ > & {
78
+ type?: never;
79
+ };
80
+
81
+ /**
82
+ * The permissive, authoring-time counterpart to {@link Toolkit} — the input to
83
+ * {@link defineToolkit}. Backend entries may carry their server `execute` here;
84
+ * the canonical {@link Toolkit} keeps those fields `undefined`.
85
+ */
86
+ export type ToolkitDeclaration = Record<
87
+ string,
88
+ ToolkitDeclarationDefinition<any, any>
89
+ >;
90
+
46
91
  /** Configuration for the {@link Tools} resource. */
47
92
  export type ToolsConfig = {
48
93
  /** Tools to register with model context and, when provided, message renderers. */
@@ -2,6 +2,7 @@ import { useEffect } from "react";
2
2
  import { useAui } from "@assistant-ui/store";
3
3
  import type { ToolCallMessagePartComponent } from "../types/MessagePartComponentTypes";
4
4
  import type { AssistantToolProps as CoreAssistantToolProps } from "../..";
5
+ import { isStandaloneToolDisplay } from "./toolbox";
5
6
 
6
7
  /**
7
8
  * Props used to register a tool from React.
@@ -52,13 +53,17 @@ export const useAssistantTool = <
52
53
  ) => {
53
54
  const aui = useAui();
54
55
 
56
+ const standalone = isStandaloneToolDisplay(tool);
57
+
55
58
  useEffect(() => {
56
59
  if (!tool.render) return undefined;
57
- return aui.tools().setToolUI(tool.toolName, tool.render);
58
- }, [aui, tool.toolName, tool.render]);
60
+ return aui.tools().setToolUI(tool.toolName, tool.render, { standalone });
61
+ }, [aui, tool.toolName, tool.render, standalone]);
59
62
 
60
63
  useEffect(() => {
61
- const { toolName, render, ...rest } = tool;
64
+ // `render` and `display` are client-only presentation concerns and never
65
+ // reach the model.
66
+ const { toolName, render, display, ...rest } = tool;
62
67
  const context = {
63
68
  tools: {
64
69
  [toolName]: rest,
@@ -8,6 +8,12 @@ export type AssistantToolUIProps<TArgs, TResult> = {
8
8
  toolName: string;
9
9
  /** Component rendered for matching tool-call message parts. */
10
10
  render: ToolCallMessagePartComponent<TArgs, TResult>;
11
+ /**
12
+ * How the UI is presented relative to the chain-of-thought trace. Set
13
+ * `"standalone"` to surface it on its own (e.g. human-in-the-loop or
14
+ * generative UI for a backend/MCP tool). Defaults to `"inline"`.
15
+ */
16
+ display?: "standalone" | "inline";
11
17
  };
12
18
 
13
19
  /**
@@ -23,8 +29,9 @@ export const useAssistantToolUI = (
23
29
  tool: AssistantToolUIProps<any, any> | null,
24
30
  ) => {
25
31
  const aui = useAui();
32
+ const standalone = tool?.display === "standalone";
26
33
  useEffect(() => {
27
34
  if (!tool?.toolName || !tool?.render) return undefined;
28
- return aui.tools().setToolUI(tool.toolName, tool.render);
29
- }, [aui, tool?.toolName, tool?.render]);
35
+ return aui.tools().setToolUI(tool.toolName, tool.render, { standalone });
36
+ }, [aui, tool?.toolName, tool?.render, standalone]);
30
37
  };
@@ -17,7 +17,6 @@ export const useInlineRender = <TArgs, TResult>(
17
17
 
18
18
  return useCallback(
19
19
  function ToolUI(args) {
20
- // biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
21
20
  const store = useToolUIStore();
22
21
  return store.toolUI(args);
23
22
  },
@@ -86,8 +86,7 @@ export const ChainOfThoughtPrimitiveParts: FC<
86
86
  );
87
87
  }
88
88
 
89
- // eslint-disable-next-line react-hooks/exhaustive-deps
90
- // biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
89
+ // oxlint-disable-next-line react-hooks/rules-of-hooks -- intentional conditional hook below the early return above
91
90
  const messageComponents = useMemo(
92
91
  () => ({
93
92
  Reasoning: components?.Reasoning,
@@ -4,7 +4,7 @@ import { MessagePrimitiveAttachments } from "./MessageAttachments";
4
4
 
5
5
  const mockUseAuiState = vi.fn();
6
6
  type UseAuiStateSelector = Parameters<
7
- typeof import("@assistant-ui/store")["useAuiState"]
7
+ (typeof import("@assistant-ui/store"))["useAuiState"]
8
8
  >[0];
9
9
  type AttachmentsElement = ReactElement<{ children: () => null }>;
10
10