@assistant-ui/core 0.3.16 → 0.3.17
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/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +2 -1
- package/dist/internal.js +2 -1
- package/dist/model-context/frame/provider.d.ts +3 -0
- package/dist/model-context/frame/provider.d.ts.map +1 -1
- package/dist/model-context/frame/provider.js +71 -21
- package/dist/model-context/frame/provider.js.map +1 -1
- package/dist/model-context/tool-call-text.d.ts +26 -0
- package/dist/model-context/tool-call-text.d.ts.map +1 -0
- package/dist/model-context/tool-call-text.js +18 -0
- package/dist/model-context/tool-call-text.js.map +1 -0
- package/dist/react/RuntimeAdapter.js +1 -1
- package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
- package/dist/react/adapters/LocalStorageThreadListAdapter.js +1 -1
- package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
- package/dist/react/client/AdaptedRemoteThread.js +1 -1
- package/dist/react/client/InMemoryThreadList.js +1 -1
- package/dist/react/client/Interactables.js +1 -1
- package/dist/react/client/RemoteThreadList.d.ts +15 -0
- package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
- package/dist/react/client/RemoteThreadList.js +240 -138
- package/dist/react/client/RemoteThreadList.js.map +1 -1
- package/dist/react/client/Tools.d.ts.map +1 -1
- package/dist/react/client/Tools.js +6 -2
- package/dist/react/client/Tools.js.map +1 -1
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.js +2 -1
- package/dist/react/interactables-legacy/Interactables.js +4 -4
- package/dist/react/interactables-legacy/Interactables.js.map +1 -1
- package/dist/react/model-context/toolbox.d.ts +2 -14
- package/dist/react/model-context/toolbox.d.ts.map +1 -1
- package/dist/react/model-context/toolbox.js +1 -13
- package/dist/react/model-context/toolbox.js.map +1 -1
- package/dist/react/model-context/useAssistantContext.js +37 -7
- package/dist/react/model-context/useAssistantContext.js.map +1 -1
- package/dist/react/model-context/useAuiToolOverrides.js +31 -5
- package/dist/react/model-context/useAuiToolOverrides.js.map +1 -1
- package/dist/react/model-context/useInlineRender.d.ts.map +1 -1
- package/dist/react/model-context/useInlineRender.js +10 -36
- package/dist/react/model-context/useInlineRender.js.map +1 -1
- package/dist/react/primitive-hooks/useMessageError.d.ts.map +1 -1
- package/dist/react/primitive-hooks/useMessageError.js +2 -8
- package/dist/react/primitive-hooks/useMessageError.js.map +1 -1
- package/dist/react/primitive-hooks/useThreadListLoadMore.d.ts.map +1 -1
- package/dist/react/primitive-hooks/useThreadListLoadMore.js +2 -4
- package/dist/react/primitive-hooks/useThreadListLoadMore.js.map +1 -1
- package/dist/react/primitives/composer/ComposerAttachments.d.ts.map +1 -1
- package/dist/react/primitives/composer/ComposerAttachments.js +4 -3
- package/dist/react/primitives/composer/ComposerAttachments.js.map +1 -1
- package/dist/react/primitives/composer/ComposerQueue.d.ts.map +1 -1
- package/dist/react/primitives/composer/ComposerQueue.js +4 -3
- package/dist/react/primitives/composer/ComposerQueue.js.map +1 -1
- package/dist/react/primitives/message/MessageAttachments.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageAttachments.js +7 -6
- package/dist/react/primitives/message/MessageAttachments.js.map +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.js +2 -2
- package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
- package/dist/react/primitives/message/MessageParts.js +3 -3
- package/dist/react/primitives/message/MessageParts.js.map +1 -1
- package/dist/react/primitives/threadList/ThreadListItems.d.ts.map +1 -1
- package/dist/react/primitives/threadList/ThreadListItems.js +5 -4
- package/dist/react/primitives/threadList/ThreadListItems.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +6 -4
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +44 -22
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +6 -5
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +79 -62
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadResource.d.ts +1 -6
- package/dist/react/runtimes/RemoteThreadResource.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadResource.js +3 -3
- package/dist/react/runtimes/RemoteThreadResource.js.map +1 -1
- package/dist/react/runtimes/assistant-transport/useLatestRef.js +2 -2
- package/dist/react/runtimes/assistant-transport/useLatestRef.js.map +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +23 -18
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
- package/dist/react/runtimes/cloud/auiV0.d.ts +5 -1
- package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
- package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts +29 -0
- package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts.map +1 -0
- package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js +159 -0
- package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js.map +1 -0
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts +1 -9
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +16 -153
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
- package/dist/react/runtimes/external-message-converter.js +1 -1
- package/dist/react/runtimes/external-message-converter.js.map +1 -1
- package/dist/react/runtimes/useRemoteThreadListRuntime.d.ts.map +1 -1
- package/dist/react/runtimes/useRemoteThreadListRuntime.js +14 -6
- package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
- package/dist/react/types/MessagePartComponentTypes.d.ts +7 -3
- package/dist/react/types/MessagePartComponentTypes.d.ts.map +1 -1
- package/dist/react/types/scopes/tools.d.ts +2 -0
- package/dist/react/types/scopes/tools.d.ts.map +1 -1
- package/dist/runtime/api/message-part-runtime.d.ts +2 -2
- package/dist/runtime/api/message-part-runtime.d.ts.map +1 -1
- package/dist/runtime/api/message-part-runtime.js +1 -1
- package/dist/runtime/api/message-part-runtime.js.map +1 -1
- package/dist/runtime/api/thread-list-runtime.d.ts +9 -1
- package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-list-runtime.js +5 -0
- package/dist/runtime/api/thread-list-runtime.js.map +1 -1
- package/dist/runtime/api/thread-runtime.d.ts +1 -1
- package/dist/runtime/base/base-thread-runtime-core.d.ts +2 -2
- package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.js +2 -2
- package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
- package/dist/runtime/interfaces/thread-list-runtime-core.d.ts +14 -2
- package/dist/runtime/interfaces/thread-list-runtime-core.d.ts.map +1 -1
- package/dist/runtime/interfaces/thread-runtime-core.d.ts +15 -1
- package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/utils/auto-status.d.ts +1 -1
- package/dist/runtime/utils/auto-status.d.ts.map +1 -1
- package/dist/runtime/utils/auto-status.js +6 -2
- package/dist/runtime/utils/auto-status.js.map +1 -1
- package/dist/runtime/utils/external-message-conversion.d.ts +3 -2
- package/dist/runtime/utils/external-message-conversion.d.ts.map +1 -1
- package/dist/runtime/utils/external-message-conversion.js +4 -3
- package/dist/runtime/utils/external-message-conversion.js.map +1 -1
- package/dist/runtime/utils/resolveToolApprovalResponse.d.ts +6 -3
- package/dist/runtime/utils/resolveToolApprovalResponse.d.ts.map +1 -1
- package/dist/runtime/utils/resolveToolApprovalResponse.js +12 -3
- package/dist/runtime/utils/resolveToolApprovalResponse.js.map +1 -1
- package/dist/runtime/utils/thread-message-like.d.ts +2 -10
- package/dist/runtime/utils/thread-message-like.d.ts.map +1 -1
- package/dist/runtime/utils/thread-message-like.js.map +1 -1
- package/dist/runtime/utils/tool-call-tree.d.ts +39 -0
- package/dist/runtime/utils/tool-call-tree.d.ts.map +1 -0
- package/dist/runtime/utils/tool-call-tree.js +85 -0
- package/dist/runtime/utils/tool-call-tree.js.map +1 -0
- package/dist/runtimes/external-store/external-store-adapter.d.ts +29 -2
- package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +2 -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 +34 -28
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/inert/InertThreadRuntimeCore.d.ts +1 -1
- package/dist/runtimes/inert/InertThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtimes/inert/InertThreadRuntimeCore.js.map +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 +3 -1
- package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts +1 -1
- package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/remote-thread-state.js +30 -13
- package/dist/runtimes/remote-thread-list/remote-thread-state.js.map +1 -1
- package/dist/runtimes/remote-thread-list/title.d.ts +9 -0
- package/dist/runtimes/remote-thread-list/title.d.ts.map +1 -0
- package/dist/runtimes/remote-thread-list/title.js +6 -0
- package/dist/runtimes/remote-thread-list/title.js.map +1 -0
- package/dist/runtimes/remote-thread-list/types.d.ts +6 -5
- package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts +31 -8
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +97 -60
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
- package/dist/store/clients/external-thread.d.ts +1 -1
- package/dist/store/clients/external-thread.d.ts.map +1 -1
- package/dist/store/clients/external-thread.js +11 -4
- package/dist/store/clients/external-thread.js.map +1 -1
- package/dist/store/clients/single-thread-list.js +1 -1
- package/dist/store/clients/suggestions.d.ts.map +1 -1
- package/dist/store/clients/suggestions.js +18 -6
- package/dist/store/clients/suggestions.js.map +1 -1
- package/dist/store/clients/thread-message-client.js +1 -1
- package/dist/store/clients/thread-selection-events.js +1 -1
- package/dist/store/index.d.ts +2 -1
- package/dist/store/index.js +2 -1
- package/dist/store/internal.d.ts +2 -2
- package/dist/store/internal.js +2 -2
- package/dist/store/primitive-predicates.d.ts +4 -1
- package/dist/store/primitive-predicates.d.ts.map +1 -1
- package/dist/store/primitive-predicates.js +9 -1
- package/dist/store/primitive-predicates.js.map +1 -1
- package/dist/store/runtime-clients/composer-runtime-client.js +1 -1
- package/dist/store/runtime-clients/message-runtime-client.js +1 -1
- package/dist/store/runtime-clients/thread-list-item-runtime-client.js +1 -1
- package/dist/store/runtime-clients/thread-list-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-list-runtime-client.js +124 -105
- package/dist/store/runtime-clients/thread-list-runtime-client.js.map +1 -1
- package/dist/store/runtime-clients/thread-runtime-client.js +1 -1
- package/dist/store/runtime-clients/useSubscribable.d.ts +3 -1
- package/dist/store/runtime-clients/useSubscribable.d.ts.map +1 -1
- package/dist/store/runtime-clients/useSubscribable.js +1 -1
- package/dist/store/runtime-clients/useSubscribable.js.map +1 -1
- package/dist/store/scopes/part.d.ts +6 -3
- package/dist/store/scopes/part.d.ts.map +1 -1
- package/dist/subscribable/subscribable.d.ts +8 -1
- package/dist/subscribable/subscribable.d.ts.map +1 -1
- package/dist/subscribable/subscribable.js +24 -16
- package/dist/subscribable/subscribable.js.map +1 -1
- package/dist/types/message.d.ts +40 -1
- package/dist/types/message.d.ts.map +1 -1
- package/dist/types/message.js +7 -1
- package/dist/types/message.js.map +1 -1
- package/package.json +12 -17
- package/src/index.ts +6 -1
- package/src/internal.ts +8 -0
- package/src/model-context/frame/provider.test.ts +217 -1
- package/src/model-context/frame/provider.ts +111 -23
- package/src/model-context/tool-call-text.test.ts +67 -0
- package/src/model-context/tool-call-text.ts +67 -0
- package/src/react/AssistantRuntimeProvider.test.tsx +66 -2
- package/src/react/adapters/LocalStorageThreadListAdapter.tsx +1 -3
- package/src/react/client/RemoteThreadList.background.test.ts +384 -0
- package/src/react/client/RemoteThreadList.concurrent.test.tsx +274 -0
- package/src/react/client/RemoteThreadList.test.ts +273 -1
- package/src/react/client/RemoteThreadList.ts +293 -83
- package/src/react/client/Tools.ts +7 -1
- package/src/react/index.ts +4 -0
- package/src/react/model-context/modelContextHooks.test.tsx +131 -0
- package/src/react/model-context/toolbox.test.ts +9 -9
- package/src/react/model-context/toolbox.ts +9 -36
- package/src/react/model-context/useAssistantContext.ts +4 -2
- package/src/react/model-context/useAuiToolOverrides.ts +4 -2
- package/src/react/model-context/useInlineRender.test.tsx +48 -0
- package/src/react/model-context/useInlineRender.ts +14 -10
- package/src/react/primitive-hooks/useMessageError.ts +2 -19
- package/src/react/primitive-hooks/useThreadListLoadMore.ts +2 -3
- package/src/react/primitives/composer/ComposerAttachments.test.tsx +76 -0
- package/src/react/primitives/composer/ComposerAttachments.tsx +9 -4
- package/src/react/primitives/composer/ComposerQueue.test.tsx +72 -0
- package/src/react/primitives/composer/ComposerQueue.tsx +7 -4
- package/src/react/primitives/message/MessageAttachments.test.tsx +107 -37
- package/src/react/primitives/message/MessageAttachments.tsx +10 -7
- package/src/react/primitives/message/MessageGroupedParts.tsx +2 -2
- package/src/react/primitives/message/MessageParts.tsx +3 -3
- package/src/react/primitives/threadList/ThreadListItems.test.tsx +99 -0
- package/src/react/primitives/threadList/ThreadListItems.tsx +8 -5
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.running.test.tsx +94 -1
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +72 -31
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.switch-delete.test.ts +169 -0
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +114 -68
- package/src/react/runtimes/RemoteThreadResource.ts +1 -4
- package/src/react/runtimes/assistant-transport/transport-scheduling.test.ts +51 -2
- package/src/react/runtimes/assistant-transport/useLatestRef.ts +2 -2
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.ts +45 -0
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +65 -1
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +42 -35
- package/src/react/runtimes/cloud/auiV0.ts +5 -0
- package/src/react/runtimes/cloud/createCloudThreadListAdapter.test.ts +88 -0
- package/src/react/runtimes/cloud/createCloudThreadListAdapter.ts +236 -0
- package/src/react/runtimes/cloud/useCloudThreadListAdapter.test.tsx +90 -1
- package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +33 -238
- package/src/react/runtimes/external-message-converter.ts +1 -0
- package/src/react/runtimes/useRemoteThreadListRuntime.concurrent.test.tsx +216 -0
- package/src/react/runtimes/useRemoteThreadListRuntime.ts +24 -6
- package/src/react/types/MessagePartComponentTypes.ts +7 -3
- package/src/react/types/scopes/tools.ts +2 -0
- package/src/runtime/api/message-part-runtime.ts +3 -3
- package/src/runtime/api/thread-list-runtime.ts +25 -1
- package/src/runtime/base/base-thread-runtime-core.test.ts +94 -3
- package/src/runtime/base/base-thread-runtime-core.ts +10 -4
- package/src/runtime/interfaces/thread-list-runtime-core.ts +20 -1
- package/src/runtime/interfaces/thread-runtime-core.ts +17 -1
- package/src/runtime/utils/auto-status.test.ts +57 -0
- package/src/runtime/utils/auto-status.ts +13 -1
- package/src/runtime/utils/external-message-conversion.test.ts +97 -0
- package/src/runtime/utils/external-message-conversion.ts +17 -1
- package/src/runtime/utils/resolveToolApprovalResponse.test.ts +73 -0
- package/src/runtime/utils/resolveToolApprovalResponse.ts +27 -6
- package/src/runtime/utils/thread-message-like.ts +2 -10
- package/src/runtime/utils/tool-call-tree.test.ts +191 -0
- package/src/runtime/utils/tool-call-tree.ts +97 -0
- package/src/runtimes/external-store/external-store-adapter.ts +38 -2
- package/src/runtimes/external-store/external-store-thread-runtime-core.ts +56 -33
- package/src/runtimes/inert/InertThreadRuntimeCore.ts +1 -1
- package/src/runtimes/local/local-thread-runtime-core.test.ts +29 -3
- package/src/runtimes/local/local-thread-runtime-core.ts +5 -1
- package/src/runtimes/remote-thread-list/remote-thread-state.test.ts +112 -0
- package/src/runtimes/remote-thread-list/remote-thread-state.ts +55 -16
- package/src/runtimes/remote-thread-list/title.ts +3 -0
- package/src/runtimes/remote-thread-list/types.ts +6 -5
- package/src/runtimes/tool-invocations/EDGE_CASES.md +110 -5
- package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +750 -136
- package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +144 -91
- package/src/store/clients/external-thread.ts +14 -8
- package/src/store/clients/suggestions.test.ts +155 -1
- package/src/store/clients/suggestions.ts +42 -34
- package/src/store/index.ts +4 -0
- package/src/store/internal.ts +2 -0
- package/src/store/primitive-predicates.ts +35 -0
- package/src/store/runtime-clients/thread-list-runtime-client.ts +16 -2
- package/src/store/runtime-clients/useSubscribable.ts +8 -2
- package/src/store/scopes/part.ts +6 -3
- package/src/subscribable/subscribable.test.ts +50 -0
- package/src/subscribable/subscribable.ts +33 -21
- package/src/tests/RemoteThreadListThreadListRuntimeCore-load-race.test.ts +111 -0
- package/src/tests/RemoteThreadListThreadListRuntimeCore-loadMore.test.ts +78 -0
- package/src/tests/RemoteThreadListThreadListRuntimeCore-reload.test.ts +52 -0
- package/src/tests/external-store-optimistic-id.test.ts +79 -0
- package/src/tests/external-store-repository-instance.test.ts +166 -0
- package/src/tests/external-store-thread-runtime-core-adapter.test.ts +22 -16
- package/src/tests/external-thread-attachments.test.tsx +85 -1
- package/src/tests/remote-thread-list-adapter-switch.test.ts +66 -1
- package/src/tests/thread-switch-events.test.tsx +397 -2
- package/src/types/message.ts +50 -2
|
@@ -14,7 +14,8 @@ import {
|
|
|
14
14
|
type ReadonlyJSONValue,
|
|
15
15
|
} from "assistant-stream/utils";
|
|
16
16
|
import { isJSONValueEqual } from "../../utils/json/is-json-equal";
|
|
17
|
-
import type { ThreadMessage } from "../../types/message";
|
|
17
|
+
import type { ThreadMessage, ToolCallMessagePart } from "../../types/message";
|
|
18
|
+
import { walkToolCallTree } from "../../runtime/utils/tool-call-tree";
|
|
18
19
|
|
|
19
20
|
const TOOL_EXECUTION_ID = Symbol.for("assistant-stream.tool-execution-id");
|
|
20
21
|
|
|
@@ -54,6 +55,7 @@ type ToolCallEntry = {
|
|
|
54
55
|
/** Active phase — chunks are flowing through `controller`. */
|
|
55
56
|
controller: ToolCallStreamController;
|
|
56
57
|
argsComplete: boolean;
|
|
58
|
+
clientOwned: boolean;
|
|
57
59
|
}
|
|
58
60
|
);
|
|
59
61
|
|
|
@@ -116,6 +118,9 @@ const getToolExecutionId = (value: object): symbol | undefined =>
|
|
|
116
118
|
export class ToolInvocationTracker {
|
|
117
119
|
private readonly _getTools: () => Record<string, Tool> | undefined;
|
|
118
120
|
private readonly _callbacks: ToolInvocationTracker.Callbacks;
|
|
121
|
+
private readonly _isClientToolCall:
|
|
122
|
+
| ((toolCall: ToolCallMessagePart) => boolean | undefined)
|
|
123
|
+
| undefined;
|
|
119
124
|
|
|
120
125
|
private readonly _entries = new Map<string, ToolCallEntry>();
|
|
121
126
|
private readonly _humanInput = new Map<
|
|
@@ -127,6 +132,13 @@ export class ToolInvocationTracker {
|
|
|
127
132
|
}
|
|
128
133
|
>();
|
|
129
134
|
private readonly _executing = new Set<symbol>();
|
|
135
|
+
/**
|
|
136
|
+
* Tool calls whose turn ended before they reached the executor. Held here
|
|
137
|
+
* rather than on the entry because an entry is rebuilt whenever a snapshot
|
|
138
|
+
* re-creates the call, and this is the one reason to skip that no later
|
|
139
|
+
* snapshot carries.
|
|
140
|
+
*/
|
|
141
|
+
private readonly _discardedToolCallIds = new Set<string>();
|
|
130
142
|
private readonly _settledResolvers: SettledResolver[] = [];
|
|
131
143
|
|
|
132
144
|
private _statuses = new Map<string, ToolExecutionStatus>();
|
|
@@ -143,10 +155,12 @@ export class ToolInvocationTracker {
|
|
|
143
155
|
/**
|
|
144
156
|
* Set when the assistant-stream pipeline has died (errored out via
|
|
145
157
|
* `.pipeTo(...).catch(...)`). The next `setState` re-initializes the
|
|
146
|
-
* pipeline and demotes
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
158
|
+
* pipeline and demotes each active entry that reached the executor to
|
|
159
|
+
* restored, so it survives the restart without re-firing `streamCall`.
|
|
160
|
+
* A restart is an execution boundary like `reset()`: an entry that had
|
|
161
|
+
* not reached it starts over and fires once there (F.4). Capped at a
|
|
162
|
+
* single auto-restart per session — repeated failures keep the tracker
|
|
163
|
+
* dead with a more visible error.
|
|
150
164
|
*/
|
|
151
165
|
private _pipelineDead = false;
|
|
152
166
|
private _pipelineRestartUsed = false;
|
|
@@ -154,9 +168,11 @@ export class ToolInvocationTracker {
|
|
|
154
168
|
constructor(
|
|
155
169
|
getTools: () => Record<string, Tool> | undefined,
|
|
156
170
|
callbacks: ToolInvocationTracker.Callbacks,
|
|
171
|
+
isClientToolCall?: (toolCall: ToolCallMessagePart) => boolean | undefined,
|
|
157
172
|
) {
|
|
158
173
|
this._getTools = getTools;
|
|
159
174
|
this._callbacks = callbacks;
|
|
175
|
+
this._isClientToolCall = isClientToolCall;
|
|
160
176
|
|
|
161
177
|
this._initPipeline();
|
|
162
178
|
}
|
|
@@ -222,10 +238,10 @@ export class ToolInvocationTracker {
|
|
|
222
238
|
*/
|
|
223
239
|
public setState(snapshot: ToolInvocationTracker.Snapshot): void {
|
|
224
240
|
try {
|
|
225
|
-
// Recover from a dead pipeline before processing anything.
|
|
226
|
-
//
|
|
227
|
-
// re-fire `streamCall` for
|
|
228
|
-
//
|
|
241
|
+
// Recover from a dead pipeline before processing anything. Entries
|
|
242
|
+
// that reached the executor are demoted to "restored" so the rebuilt
|
|
243
|
+
// pipeline does not re-fire `streamCall` for them; the rest start over
|
|
244
|
+
// across the boundary the restart opens (F.4).
|
|
229
245
|
if (this._pipelineDead) {
|
|
230
246
|
if (this._pipelineRestartUsed) {
|
|
231
247
|
// Already retried once and failed again. Stay dead.
|
|
@@ -291,6 +307,7 @@ export class ToolInvocationTracker {
|
|
|
291
307
|
try {
|
|
292
308
|
this._pendingRestore = true;
|
|
293
309
|
this._entries.clear();
|
|
310
|
+
this._discardedToolCallIds.clear();
|
|
294
311
|
this._lastSnapshot = null;
|
|
295
312
|
void this.abort();
|
|
296
313
|
// Statuses are cleared synchronously: discarded executions may never
|
|
@@ -310,8 +327,13 @@ export class ToolInvocationTracker {
|
|
|
310
327
|
/**
|
|
311
328
|
* Abort any in-flight `execute()` invocations. Resolves once all of them
|
|
312
329
|
* have settled (or immediately if none are running).
|
|
330
|
+
*
|
|
331
|
+
* `discardPending` additionally kills the calls that never reached the
|
|
332
|
+
* executor, for a caller ending the turn rather than interrupting it. The
|
|
333
|
+
* signal cannot reach those: they are waiting on the run to settle (A.10),
|
|
334
|
+
* and the settled snapshot arrives after this installs a fresh controller.
|
|
313
335
|
*/
|
|
314
|
-
public abort(): Promise<void> {
|
|
336
|
+
public abort(options?: { discardPending?: boolean }): Promise<void> {
|
|
315
337
|
try {
|
|
316
338
|
this._humanInput.forEach(({ reject }) => {
|
|
317
339
|
try {
|
|
@@ -323,6 +345,15 @@ export class ToolInvocationTracker {
|
|
|
323
345
|
});
|
|
324
346
|
this._humanInput.clear();
|
|
325
347
|
|
|
348
|
+
if (options?.discardPending) {
|
|
349
|
+
for (const [toolCallId, entry] of this._entries) {
|
|
350
|
+
if (!entry.controller) continue;
|
|
351
|
+
if (entry.argsComplete || entry.hasResult) continue;
|
|
352
|
+
this._discardedToolCallIds.add(toolCallId);
|
|
353
|
+
entry.skipExecute = true;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
326
357
|
this._ac.abort();
|
|
327
358
|
this._ac = new AbortController();
|
|
328
359
|
|
|
@@ -473,6 +504,8 @@ export class ToolInvocationTracker {
|
|
|
473
504
|
if (entry.skipExecute) return;
|
|
474
505
|
|
|
475
506
|
this._executing.add(executionId!);
|
|
507
|
+
// execute can park human() before onExecutionStart; preserve this execution's interrupt.
|
|
508
|
+
if (this._humanInput.get(toolCallId)?.executionId === executionId) return;
|
|
476
509
|
this._setStatus(toolCallId, { type: "executing" });
|
|
477
510
|
}
|
|
478
511
|
|
|
@@ -577,31 +610,40 @@ export class ToolInvocationTracker {
|
|
|
577
610
|
|
|
578
611
|
// ──────────────── internal: snapshot processing ────────────────
|
|
579
612
|
|
|
580
|
-
private
|
|
581
|
-
|
|
582
|
-
|
|
613
|
+
private _warnProviderOwnedSkip(toolName: string, toolCallId: string): void {
|
|
614
|
+
if (process.env.NODE_ENV === "production") return;
|
|
615
|
+
if (this._getTools()?.[toolName]?.execute === undefined) return;
|
|
616
|
+
console.warn(
|
|
617
|
+
"[ToolInvocationTracker] the runtime reports this tool call as provider-owned, so the registered execute is skipped; the provider has to hand the call to the client for it to run (see EDGE_CASES.md A.9)",
|
|
618
|
+
{ toolCallId, toolName },
|
|
619
|
+
);
|
|
583
620
|
}
|
|
584
621
|
|
|
622
|
+
/**
|
|
623
|
+
* Closing the args stream hands the call to the client executor, so it may
|
|
624
|
+
* only happen once the provider can no longer speak about that call. The run
|
|
625
|
+
* ending is the only universal signal for that; an adapter that reports the
|
|
626
|
+
* call as client-owned has said it earlier, per call.
|
|
627
|
+
*/
|
|
585
628
|
private _shouldCloseArgsStream({
|
|
586
|
-
toolName,
|
|
587
629
|
argsText,
|
|
588
630
|
hasResult,
|
|
631
|
+
clientOwned,
|
|
589
632
|
}: {
|
|
590
|
-
toolName: string;
|
|
591
633
|
argsText: string;
|
|
592
634
|
hasResult: boolean;
|
|
635
|
+
clientOwned: boolean;
|
|
593
636
|
}): boolean {
|
|
594
637
|
if (hasResult) return true;
|
|
595
|
-
if (!
|
|
596
|
-
|
|
597
|
-
}
|
|
598
|
-
return isArgsTextComplete(argsText);
|
|
638
|
+
if (!isArgsTextComplete(argsText)) return false;
|
|
639
|
+
return clientOwned || !this._isRunning;
|
|
599
640
|
}
|
|
600
641
|
|
|
601
642
|
private _startActiveEntry(
|
|
602
643
|
toolCallId: string,
|
|
603
644
|
toolName: string,
|
|
604
645
|
skipExecute: boolean,
|
|
646
|
+
clientOwned: boolean,
|
|
605
647
|
): ToolCallEntry {
|
|
606
648
|
const toolCallController = this._controller.addToolCallPart({
|
|
607
649
|
toolName,
|
|
@@ -614,6 +656,7 @@ export class ToolInvocationTracker {
|
|
|
614
656
|
hasResult: false,
|
|
615
657
|
skipExecute,
|
|
616
658
|
argsComplete: false,
|
|
659
|
+
clientOwned,
|
|
617
660
|
};
|
|
618
661
|
this._entries.set(toolCallId, entry);
|
|
619
662
|
return entry;
|
|
@@ -629,6 +672,16 @@ export class ToolInvocationTracker {
|
|
|
629
672
|
private _demoteEntriesToRestored(): void {
|
|
630
673
|
for (const [toolCallId, entry] of this._entries) {
|
|
631
674
|
if (!entry.controller) continue;
|
|
675
|
+
if (!entry.argsComplete && !entry.hasResult) {
|
|
676
|
+
// The call never reached the executor. A restored entry is promoted
|
|
677
|
+
// only when its signature changes, and a call waiting on the run to
|
|
678
|
+
// settle already holds its final args, so demoting it would strand it
|
|
679
|
+
// unexecuted. Dropping it lets the next snapshot start it over; a call
|
|
680
|
+
// whose turn was discarded is held by `_discardedToolCallIds`, not by
|
|
681
|
+
// the entry, so starting over does not revive it.
|
|
682
|
+
this._entries.delete(toolCallId);
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
632
685
|
this._entries.set(toolCallId, {
|
|
633
686
|
toolName: entry.toolName,
|
|
634
687
|
argsText: entry.argsText,
|
|
@@ -683,9 +736,9 @@ export class ToolInvocationTracker {
|
|
|
683
736
|
isEquivalentCompleteArgsText(entry.argsText, content.argsText)
|
|
684
737
|
) {
|
|
685
738
|
const shouldClose = this._shouldCloseArgsStream({
|
|
686
|
-
toolName: content.toolName,
|
|
687
739
|
argsText: content.argsText,
|
|
688
740
|
hasResult,
|
|
741
|
+
clientOwned: entry.clientOwned,
|
|
689
742
|
});
|
|
690
743
|
if (shouldClose) entry.controller.argsText.close();
|
|
691
744
|
entry.argsText = content.argsText;
|
|
@@ -717,9 +770,9 @@ export class ToolInvocationTracker {
|
|
|
717
770
|
const delta = content.argsText.slice(entry.argsText.length);
|
|
718
771
|
entry.controller.argsText.append(delta);
|
|
719
772
|
const shouldClose = this._shouldCloseArgsStream({
|
|
720
|
-
toolName: content.toolName,
|
|
721
773
|
argsText: content.argsText,
|
|
722
774
|
hasResult,
|
|
775
|
+
clientOwned: entry.clientOwned,
|
|
723
776
|
});
|
|
724
777
|
if (shouldClose) entry.controller.argsText.close();
|
|
725
778
|
entry.argsText = content.argsText;
|
|
@@ -732,9 +785,9 @@ export class ToolInvocationTracker {
|
|
|
732
785
|
// gates on the streamed content; a divergent snapshot (A.2) can be
|
|
733
786
|
// complete while the controller still holds an incomplete stale prefix.
|
|
734
787
|
const shouldClose = this._shouldCloseArgsStream({
|
|
735
|
-
toolName: content.toolName,
|
|
736
788
|
argsText: entry.argsText,
|
|
737
789
|
hasResult,
|
|
790
|
+
clientOwned: entry.clientOwned,
|
|
738
791
|
});
|
|
739
792
|
if (shouldClose) {
|
|
740
793
|
entry.controller.argsText.close();
|
|
@@ -746,81 +799,81 @@ export class ToolInvocationTracker {
|
|
|
746
799
|
private _processMessages(messages: readonly ThreadMessage[]): void {
|
|
747
800
|
const isRestore = this._pendingRestore;
|
|
748
801
|
|
|
749
|
-
for (const
|
|
750
|
-
|
|
802
|
+
for (const { part: content } of walkToolCallTree(messages)) {
|
|
803
|
+
const existing = this._entries.get(content.toolCallId);
|
|
804
|
+
|
|
805
|
+
if (isRestore) {
|
|
806
|
+
// Don't overwrite an already-active entry (e.g. live tool-call
|
|
807
|
+
// observed before this restore snapshot landed). Restore can
|
|
808
|
+
// only seed entries the runtime has never seen.
|
|
809
|
+
if (!existing?.controller) {
|
|
810
|
+
this._entries.set(content.toolCallId, {
|
|
811
|
+
toolName: content.toolName,
|
|
812
|
+
argsText: content.argsText,
|
|
813
|
+
hasResult: content.result !== undefined,
|
|
814
|
+
});
|
|
815
|
+
}
|
|
751
816
|
continue;
|
|
752
817
|
}
|
|
753
|
-
for (const content of message.content as readonly ThreadMessage["content"][number][]) {
|
|
754
|
-
if (!content || content.type !== "tool-call") continue;
|
|
755
|
-
|
|
756
|
-
const existing = this._entries.get(content.toolCallId);
|
|
757
|
-
|
|
758
|
-
if (isRestore) {
|
|
759
|
-
// Don't overwrite an already-active entry (e.g. live tool-call
|
|
760
|
-
// observed before this restore snapshot landed). Restore can
|
|
761
|
-
// only seed entries the runtime has never seen.
|
|
762
|
-
if (!existing?.controller) {
|
|
763
|
-
this._entries.set(content.toolCallId, {
|
|
764
|
-
toolName: content.toolName,
|
|
765
|
-
argsText: content.argsText,
|
|
766
|
-
hasResult: content.result !== undefined,
|
|
767
|
-
});
|
|
768
|
-
}
|
|
769
|
-
if (content.messages) this._processMessages(content.messages);
|
|
770
|
-
continue;
|
|
771
|
-
}
|
|
772
818
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
819
|
+
// Live snapshot.
|
|
820
|
+
let entry = existing;
|
|
821
|
+
|
|
822
|
+
// A discarded id is remembered only until the call is answered, which
|
|
823
|
+
// bounds the set to the open calls of a discarded turn.
|
|
824
|
+
if (content.result !== undefined)
|
|
825
|
+
this._discardedToolCallIds.delete(content.toolCallId);
|
|
826
|
+
|
|
827
|
+
if (entry && !entry.controller) {
|
|
828
|
+
// Restored entry observed in a live snapshot. Promote if its
|
|
829
|
+
// signature has changed; otherwise treat as still-historical.
|
|
830
|
+
const signatureChanged =
|
|
831
|
+
content.argsText !== entry.argsText ||
|
|
832
|
+
(content.result !== undefined) !== entry.hasResult;
|
|
833
|
+
if (!signatureChanged) continue;
|
|
834
|
+
this._entries.delete(content.toolCallId);
|
|
835
|
+
entry = undefined;
|
|
836
|
+
}
|
|
789
837
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
);
|
|
796
|
-
|
|
838
|
+
if (!entry) {
|
|
839
|
+
const ownership = this._isClientToolCall?.(content);
|
|
840
|
+
const providerOwned =
|
|
841
|
+
content.result === undefined && ownership === false;
|
|
842
|
+
if (providerOwned)
|
|
843
|
+
this._warnProviderOwnedSkip(content.toolName, content.toolCallId);
|
|
844
|
+
entry = this._startActiveEntry(
|
|
845
|
+
content.toolCallId,
|
|
846
|
+
content.toolName,
|
|
847
|
+
content.result !== undefined ||
|
|
848
|
+
providerOwned ||
|
|
849
|
+
this._discardedToolCallIds.has(content.toolCallId),
|
|
850
|
+
ownership === true,
|
|
851
|
+
);
|
|
852
|
+
}
|
|
797
853
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
this._processArgsText(entry, content);
|
|
801
|
-
|
|
802
|
-
if (content.result !== undefined && !entry.hasResult) {
|
|
803
|
-
// `entry` is in active phase from this point — either just
|
|
804
|
-
// created by `_startActiveEntry`, or pre-existing with a live
|
|
805
|
-
// controller. Narrow once instead of asserting at every use.
|
|
806
|
-
const { controller: activeController } = entry;
|
|
807
|
-
if (!activeController) continue;
|
|
808
|
-
entry.hasResult = true;
|
|
809
|
-
entry.argsComplete = true;
|
|
810
|
-
activeController.setResponse(
|
|
811
|
-
new ToolResponse({
|
|
812
|
-
result: content.result as ReadonlyJSONValue,
|
|
813
|
-
artifact: content.artifact as ReadonlyJSONValue | undefined,
|
|
814
|
-
isError: content.isError,
|
|
815
|
-
...(content.modelContent !== undefined
|
|
816
|
-
? { modelContent: content.modelContent }
|
|
817
|
-
: {}),
|
|
818
|
-
}),
|
|
819
|
-
);
|
|
820
|
-
activeController.close();
|
|
821
|
-
}
|
|
854
|
+
if (content.approval !== undefined) entry.skipExecute = true;
|
|
822
855
|
|
|
823
|
-
|
|
856
|
+
this._processArgsText(entry, content);
|
|
857
|
+
|
|
858
|
+
if (content.result !== undefined && !entry.hasResult) {
|
|
859
|
+
// `entry` is in active phase from this point — either just
|
|
860
|
+
// created by `_startActiveEntry`, or pre-existing with a live
|
|
861
|
+
// controller. Narrow once instead of asserting at every use.
|
|
862
|
+
const { controller: activeController } = entry;
|
|
863
|
+
if (!activeController) continue;
|
|
864
|
+
entry.hasResult = true;
|
|
865
|
+
entry.argsComplete = true;
|
|
866
|
+
activeController.setResponse(
|
|
867
|
+
new ToolResponse({
|
|
868
|
+
result: content.result as ReadonlyJSONValue,
|
|
869
|
+
artifact: content.artifact as ReadonlyJSONValue | undefined,
|
|
870
|
+
isError: content.isError,
|
|
871
|
+
...(content.modelContent !== undefined
|
|
872
|
+
? { modelContent: content.modelContent }
|
|
873
|
+
: {}),
|
|
874
|
+
}),
|
|
875
|
+
);
|
|
876
|
+
activeController.close();
|
|
824
877
|
}
|
|
825
878
|
}
|
|
826
879
|
}
|
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
drainAttachmentAdd,
|
|
50
50
|
} from "../../runtime/utils/attachment-add-operations";
|
|
51
51
|
import { toMessagePartStatus } from "../../utils/normalizePartStatus";
|
|
52
|
+
import { generateId } from "../../utils/id";
|
|
52
53
|
import { ModelContext } from "./model-context-client";
|
|
53
54
|
import { ThreadSuggestions } from "./suggestions";
|
|
54
55
|
import { Tools } from "../../react/client/Tools";
|
|
@@ -118,7 +119,9 @@ export type ExternalThreadProps = {
|
|
|
118
119
|
/** Branch adapter for runtimes that track sibling variants of messages. */
|
|
119
120
|
branches?: ExternalThreadBranchAdapter;
|
|
120
121
|
/** Callback for tool approval decisions. Absent: responding to an approval throws a capability error. */
|
|
121
|
-
onRespondToToolApproval?: (
|
|
122
|
+
onRespondToToolApproval?: (
|
|
123
|
+
options: RespondToToolApprovalOptions,
|
|
124
|
+
) => void | Promise<void>;
|
|
122
125
|
};
|
|
123
126
|
|
|
124
127
|
type MessageClientProps = {
|
|
@@ -130,7 +133,7 @@ type MessageClientProps = {
|
|
|
130
133
|
queue?: ExternalThreadQueueAdapter | undefined;
|
|
131
134
|
branches?: ExternalThreadBranchAdapter | undefined;
|
|
132
135
|
onRespondToToolApproval?:
|
|
133
|
-
| ((options: RespondToToolApprovalOptions) => void)
|
|
136
|
+
| ((options: RespondToToolApprovalOptions) => void | Promise<void>)
|
|
134
137
|
| undefined;
|
|
135
138
|
onAddToolResult?: ((options: AddToolResultOptions) => void) | undefined;
|
|
136
139
|
onResumeToolCall?: ((options: ResumeToolCallOptions) => void) | undefined;
|
|
@@ -313,7 +316,7 @@ type PartResourceProps = {
|
|
|
313
316
|
status: ToolCallMessagePartStatus;
|
|
314
317
|
messageId: string;
|
|
315
318
|
onRespondToToolApproval?:
|
|
316
|
-
| ((options: RespondToToolApprovalOptions) => void)
|
|
319
|
+
| ((options: RespondToToolApprovalOptions) => void | Promise<void>)
|
|
317
320
|
| undefined;
|
|
318
321
|
onAddToolResult?: ((options: AddToolResultOptions) => void) | undefined;
|
|
319
322
|
onResumeToolCall?: ((options: ResumeToolCallOptions) => void) | undefined;
|
|
@@ -382,9 +385,12 @@ const usePartResource = ({
|
|
|
382
385
|
)
|
|
383
386
|
throw new Error("Tool call has no pending approval");
|
|
384
387
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
+
const options = resolveToolApprovalResponse(part.approval, response);
|
|
389
|
+
try {
|
|
390
|
+
return Promise.resolve(onRespondToToolApproval(options));
|
|
391
|
+
} catch (error) {
|
|
392
|
+
return Promise.reject(error);
|
|
393
|
+
}
|
|
388
394
|
},
|
|
389
395
|
};
|
|
390
396
|
};
|
|
@@ -632,7 +638,7 @@ const useComposerClientResource = ({
|
|
|
632
638
|
}
|
|
633
639
|
} else if (!isCreateAttachment(fileOrAttachment)) {
|
|
634
640
|
const newAttachment: Attachment = {
|
|
635
|
-
id:
|
|
641
|
+
id: generateId(),
|
|
636
642
|
type: "file",
|
|
637
643
|
name: fileOrAttachment.name,
|
|
638
644
|
contentType: fileOrAttachment.type,
|
|
@@ -643,7 +649,7 @@ const useComposerClientResource = ({
|
|
|
643
649
|
setAttachments((prev) => [...prev, newAttachment]);
|
|
644
650
|
} else {
|
|
645
651
|
const newAttachment: Attachment = {
|
|
646
|
-
id: fileOrAttachment.id ??
|
|
652
|
+
id: fileOrAttachment.id ?? generateId(),
|
|
647
653
|
type: fileOrAttachment.type ?? "document",
|
|
648
654
|
name: fileOrAttachment.name,
|
|
649
655
|
contentType: fileOrAttachment.contentType,
|
|
@@ -1,9 +1,68 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import { createTapRoot, flushTapSync, useResource } from "@assistant-ui/tap";
|
|
3
3
|
import { describe, expect, it } from "vitest";
|
|
4
|
-
import {
|
|
4
|
+
import type { ThreadSuggestion } from "../../runtime/interfaces/thread-runtime-core";
|
|
5
|
+
import {
|
|
6
|
+
Suggestions,
|
|
7
|
+
ThreadSuggestions,
|
|
8
|
+
type SuggestionConfig,
|
|
9
|
+
} from "./suggestions";
|
|
5
10
|
|
|
6
11
|
describe("Suggestions", () => {
|
|
12
|
+
it("keeps equal inline string suggestions stable across owner renders", () => {
|
|
13
|
+
let rerender!: () => void;
|
|
14
|
+
const root = createTapRoot(function SuggestionsRoot() {
|
|
15
|
+
const [, setTick] = useState(0);
|
|
16
|
+
rerender = () => setTick((tick) => tick + 1);
|
|
17
|
+
return useResource(Suggestions(["account-a", "account-b"]));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const firstState = root.getValue().getState();
|
|
22
|
+
const firstSuggestion = root
|
|
23
|
+
.getValue()
|
|
24
|
+
.suggestion({ index: 0 })
|
|
25
|
+
.getState();
|
|
26
|
+
|
|
27
|
+
flushTapSync(rerender);
|
|
28
|
+
|
|
29
|
+
expect(root.getValue().getState()).toBe(firstState);
|
|
30
|
+
expect(root.getValue().suggestion({ index: 0 }).getState()).toBe(
|
|
31
|
+
firstSuggestion,
|
|
32
|
+
);
|
|
33
|
+
} finally {
|
|
34
|
+
root.unmount();
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("keeps equal inline object suggestions stable across owner renders", () => {
|
|
39
|
+
let rerender!: () => void;
|
|
40
|
+
const root = createTapRoot(function SuggestionsRoot() {
|
|
41
|
+
const [, setTick] = useState(0);
|
|
42
|
+
rerender = () => setTick((tick) => tick + 1);
|
|
43
|
+
return useResource(
|
|
44
|
+
Suggestions([{ title: "Title", label: "Label", prompt: "Prompt" }]),
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const firstState = root.getValue().getState();
|
|
50
|
+
const firstSuggestion = root
|
|
51
|
+
.getValue()
|
|
52
|
+
.suggestion({ index: 0 })
|
|
53
|
+
.getState();
|
|
54
|
+
|
|
55
|
+
flushTapSync(rerender);
|
|
56
|
+
|
|
57
|
+
expect(root.getValue().getState()).toBe(firstState);
|
|
58
|
+
expect(root.getValue().suggestion({ index: 0 }).getState()).toBe(
|
|
59
|
+
firstSuggestion,
|
|
60
|
+
);
|
|
61
|
+
} finally {
|
|
62
|
+
root.unmount();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
7
66
|
it("updates when the configured suggestions change", () => {
|
|
8
67
|
let setSuggestions!: (suggestions: SuggestionConfig[]) => void;
|
|
9
68
|
const root = createTapRoot(function SuggestionsRoot() {
|
|
@@ -28,4 +87,99 @@ describe("Suggestions", () => {
|
|
|
28
87
|
root.unmount();
|
|
29
88
|
}
|
|
30
89
|
});
|
|
90
|
+
|
|
91
|
+
it("reuses unchanged suggestion state when one entry changes", () => {
|
|
92
|
+
let setSuggestions!: (suggestions: SuggestionConfig[]) => void;
|
|
93
|
+
const root = createTapRoot(function SuggestionsRoot() {
|
|
94
|
+
const [suggestions, setValue] = useState<SuggestionConfig[]>([
|
|
95
|
+
{ title: "Title A", label: "Label A", prompt: "Prompt A" },
|
|
96
|
+
{ title: "Title B", label: "Label B", prompt: "Prompt B" },
|
|
97
|
+
]);
|
|
98
|
+
setSuggestions = setValue;
|
|
99
|
+
return useResource(Suggestions(suggestions));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const firstState = root.getValue().getState();
|
|
104
|
+
const unchangedSuggestion = root
|
|
105
|
+
.getValue()
|
|
106
|
+
.suggestion({ index: 1 })
|
|
107
|
+
.getState();
|
|
108
|
+
|
|
109
|
+
flushTapSync(() =>
|
|
110
|
+
setSuggestions([
|
|
111
|
+
{ title: "Changed", label: "Label A", prompt: "Prompt A" },
|
|
112
|
+
{ title: "Title B", label: "Label B", prompt: "Prompt B" },
|
|
113
|
+
]),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(root.getValue().getState()).not.toBe(firstState);
|
|
117
|
+
expect(root.getValue().getState().suggestions[0]?.title).toBe("Changed");
|
|
118
|
+
expect(root.getValue().suggestion({ index: 1 }).getState()).toBe(
|
|
119
|
+
unchangedSuggestion,
|
|
120
|
+
);
|
|
121
|
+
} finally {
|
|
122
|
+
root.unmount();
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("ThreadSuggestions", () => {
|
|
128
|
+
it("keeps equal runtime suggestions stable across owner renders", () => {
|
|
129
|
+
let rerender!: () => void;
|
|
130
|
+
const root = createTapRoot(function ThreadSuggestionsRoot() {
|
|
131
|
+
const [, setTick] = useState(0);
|
|
132
|
+
rerender = () => setTick((tick) => tick + 1);
|
|
133
|
+
return useResource(ThreadSuggestions([{ prompt: "Prompt" }]));
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const firstState = root.getValue().getState();
|
|
138
|
+
const firstSuggestion = root
|
|
139
|
+
.getValue()
|
|
140
|
+
.suggestion({ index: 0 })
|
|
141
|
+
.getState();
|
|
142
|
+
|
|
143
|
+
flushTapSync(rerender);
|
|
144
|
+
|
|
145
|
+
expect(root.getValue().getState()).toBe(firstState);
|
|
146
|
+
expect(root.getValue().suggestion({ index: 0 }).getState()).toBe(
|
|
147
|
+
firstSuggestion,
|
|
148
|
+
);
|
|
149
|
+
} finally {
|
|
150
|
+
root.unmount();
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("reuses unchanged suggestion state when one entry changes", () => {
|
|
155
|
+
let setSuggestions!: (suggestions: ThreadSuggestion[]) => void;
|
|
156
|
+
const root = createTapRoot(function ThreadSuggestionsRoot() {
|
|
157
|
+
const [suggestions, setValue] = useState<ThreadSuggestion[]>([
|
|
158
|
+
{ prompt: "Prompt A" },
|
|
159
|
+
{ prompt: "Prompt B" },
|
|
160
|
+
]);
|
|
161
|
+
setSuggestions = setValue;
|
|
162
|
+
return useResource(ThreadSuggestions(suggestions));
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const firstState = root.getValue().getState();
|
|
167
|
+
const unchangedSuggestion = root
|
|
168
|
+
.getValue()
|
|
169
|
+
.suggestion({ index: 1 })
|
|
170
|
+
.getState();
|
|
171
|
+
|
|
172
|
+
flushTapSync(() =>
|
|
173
|
+
setSuggestions([{ prompt: "Changed" }, { prompt: "Prompt B" }]),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
expect(root.getValue().getState()).not.toBe(firstState);
|
|
177
|
+
expect(root.getValue().getState().suggestions[0]?.prompt).toBe("Changed");
|
|
178
|
+
expect(root.getValue().suggestion({ index: 1 }).getState()).toBe(
|
|
179
|
+
unchangedSuggestion,
|
|
180
|
+
);
|
|
181
|
+
} finally {
|
|
182
|
+
root.unmount();
|
|
183
|
+
}
|
|
184
|
+
});
|
|
31
185
|
});
|