@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
|
@@ -36,6 +36,15 @@ const AUTO_STATUS_COMPLETE = Object.freeze(
|
|
|
36
36
|
{ [symbolAutoStatus]: true },
|
|
37
37
|
),
|
|
38
38
|
);
|
|
39
|
+
const AUTO_STATUS_CANCELLED = Object.freeze(
|
|
40
|
+
Object.assign(
|
|
41
|
+
{
|
|
42
|
+
type: "incomplete" as const,
|
|
43
|
+
reason: "cancelled" as const,
|
|
44
|
+
},
|
|
45
|
+
{ [symbolAutoStatus]: true },
|
|
46
|
+
),
|
|
47
|
+
);
|
|
39
48
|
|
|
40
49
|
const AUTO_STATUS_PENDING = Object.freeze(
|
|
41
50
|
Object.assign(
|
|
@@ -66,6 +75,7 @@ export const getAutoStatus = (
|
|
|
66
75
|
hasInterruptedToolCalls: boolean,
|
|
67
76
|
hasPendingToolCalls: boolean,
|
|
68
77
|
error?: ReadonlyJSONValue,
|
|
78
|
+
isCancelled?: boolean,
|
|
69
79
|
): MessageStatus => {
|
|
70
80
|
if (isLast && error) {
|
|
71
81
|
return Object.assign(
|
|
@@ -84,7 +94,9 @@ export const getAutoStatus = (
|
|
|
84
94
|
? AUTO_STATUS_INTERRUPT
|
|
85
95
|
: hasPendingToolCalls
|
|
86
96
|
? AUTO_STATUS_PENDING
|
|
87
|
-
:
|
|
97
|
+
: isCancelled
|
|
98
|
+
? AUTO_STATUS_CANCELLED
|
|
99
|
+
: AUTO_STATUS_COMPLETE;
|
|
88
100
|
};
|
|
89
101
|
|
|
90
102
|
export const getContentAutoStatus = (
|
|
@@ -110,4 +110,101 @@ describe("convertExternalMessageChunk", () => {
|
|
|
110
110
|
|
|
111
111
|
expect(second).toBe(first);
|
|
112
112
|
});
|
|
113
|
+
|
|
114
|
+
it("replaces a cached complete status after cancellation", () => {
|
|
115
|
+
const input = {};
|
|
116
|
+
const chunk = {
|
|
117
|
+
inputs: [input],
|
|
118
|
+
outputs: [{ id: "m1", role: "assistant" as const, content: "partial" }],
|
|
119
|
+
};
|
|
120
|
+
const cancelled = new Set(["m1"]);
|
|
121
|
+
const generatedFallbackMessages = new WeakSet<object>();
|
|
122
|
+
const first = convertExternalMessageChunk(chunk, 0, 1, false, undefined, {
|
|
123
|
+
message: undefined,
|
|
124
|
+
generatedFallbackMessages,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const second = convertExternalMessageChunk(
|
|
128
|
+
chunk,
|
|
129
|
+
0,
|
|
130
|
+
1,
|
|
131
|
+
false,
|
|
132
|
+
undefined,
|
|
133
|
+
{ message: first, generatedFallbackMessages },
|
|
134
|
+
cancelled,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
expect(first.status).toMatchObject({ type: "complete", reason: "unknown" });
|
|
138
|
+
expect(second).not.toBe(first);
|
|
139
|
+
expect(second.status).toMatchObject({
|
|
140
|
+
type: "incomplete",
|
|
141
|
+
reason: "cancelled",
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const failed = convertExternalMessageChunk(
|
|
145
|
+
chunk,
|
|
146
|
+
0,
|
|
147
|
+
1,
|
|
148
|
+
false,
|
|
149
|
+
"failed",
|
|
150
|
+
{ message: second, generatedFallbackMessages },
|
|
151
|
+
cancelled,
|
|
152
|
+
);
|
|
153
|
+
expect(failed.status).toMatchObject({
|
|
154
|
+
type: "incomplete",
|
|
155
|
+
reason: "error",
|
|
156
|
+
error: "failed",
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("cancels a joined chunk when any joined message was stopped", () => {
|
|
161
|
+
const generatedFallbackMessages = new WeakSet<object>();
|
|
162
|
+
const result = convertExternalMessageChunk(
|
|
163
|
+
{
|
|
164
|
+
inputs: [{}, {}],
|
|
165
|
+
outputs: [
|
|
166
|
+
{ id: "m1", role: "assistant" as const, content: "first" },
|
|
167
|
+
{ id: "m2", role: "assistant" as const, content: "second" },
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
0,
|
|
171
|
+
1,
|
|
172
|
+
false,
|
|
173
|
+
undefined,
|
|
174
|
+
{ message: undefined, generatedFallbackMessages },
|
|
175
|
+
new Set(["m2"]),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
expect(result.status).toMatchObject({
|
|
179
|
+
type: "incomplete",
|
|
180
|
+
reason: "cancelled",
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("cancels an earlier message that is no longer the last one", () => {
|
|
185
|
+
const cancelled = new Set(["m1"]);
|
|
186
|
+
const generatedFallbackMessages = new WeakSet<object>();
|
|
187
|
+
const build = (id: string, idx: number) =>
|
|
188
|
+
convertExternalMessageChunk(
|
|
189
|
+
{
|
|
190
|
+
inputs: [{}],
|
|
191
|
+
outputs: [{ id, role: "assistant" as const, content: "text" }],
|
|
192
|
+
},
|
|
193
|
+
idx,
|
|
194
|
+
2,
|
|
195
|
+
false,
|
|
196
|
+
undefined,
|
|
197
|
+
{ message: undefined, generatedFallbackMessages },
|
|
198
|
+
cancelled,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
expect(build("m1", 0).status).toMatchObject({
|
|
202
|
+
type: "incomplete",
|
|
203
|
+
reason: "cancelled",
|
|
204
|
+
});
|
|
205
|
+
expect(build("m2", 1).status).toMatchObject({
|
|
206
|
+
type: "complete",
|
|
207
|
+
reason: "unknown",
|
|
208
|
+
});
|
|
209
|
+
});
|
|
113
210
|
});
|
|
@@ -46,6 +46,7 @@ export type ExternalMessageConverterMessage =
|
|
|
46
46
|
export type ExternalMessageConverterMetadata = {
|
|
47
47
|
readonly toolStatuses?: Record<string, ToolExecutionStatus>;
|
|
48
48
|
readonly error?: ReadonlyJSONValue;
|
|
49
|
+
readonly cancelledMessageIds?: ReadonlySet<string>;
|
|
49
50
|
readonly messageTiming?: Record<string, MessageTiming>;
|
|
50
51
|
};
|
|
51
52
|
|
|
@@ -358,7 +359,10 @@ export const chunkExternalMessages = <T>(
|
|
|
358
359
|
return results;
|
|
359
360
|
};
|
|
360
361
|
|
|
361
|
-
export const shallowArrayEqual = (
|
|
362
|
+
export const shallowArrayEqual = (
|
|
363
|
+
a: readonly unknown[],
|
|
364
|
+
b: readonly unknown[],
|
|
365
|
+
) => {
|
|
362
366
|
if (a.length !== b.length) return false;
|
|
363
367
|
for (let i = 0; i < a.length; i++) {
|
|
364
368
|
if (a[i] !== b[i]) return false;
|
|
@@ -378,9 +382,18 @@ export const convertExternalMessageChunk = <T>(
|
|
|
378
382
|
isRunning: boolean,
|
|
379
383
|
error: ReadonlyJSONValue | undefined,
|
|
380
384
|
cache?: ExternalMessageConversionCache,
|
|
385
|
+
cancelledMessageIds?: ReadonlySet<string>,
|
|
381
386
|
) => {
|
|
382
387
|
const isLast = idx === chunkCount - 1;
|
|
383
388
|
const joined = joinExternalMessages(message.outputs);
|
|
389
|
+
const isCancelled =
|
|
390
|
+
cancelledMessageIds !== undefined &&
|
|
391
|
+
message.outputs.some(
|
|
392
|
+
(output) =>
|
|
393
|
+
output.role !== "tool" &&
|
|
394
|
+
output.id != null &&
|
|
395
|
+
cancelledMessageIds.has(output.id),
|
|
396
|
+
);
|
|
384
397
|
const hasInterruptedToolCalls =
|
|
385
398
|
typeof joined.content === "object" &&
|
|
386
399
|
joined.content.some(isInterruptedToolCall);
|
|
@@ -393,6 +406,7 @@ export const convertExternalMessageChunk = <T>(
|
|
|
393
406
|
hasInterruptedToolCalls,
|
|
394
407
|
hasPendingToolCalls,
|
|
395
408
|
isLast ? error : undefined,
|
|
409
|
+
isCancelled,
|
|
396
410
|
);
|
|
397
411
|
const fallbackId = `${FALLBACK_ID_PREFIX}${idx}`;
|
|
398
412
|
|
|
@@ -488,6 +502,8 @@ export const convertExternalMessages = <T extends WeakKey>(
|
|
|
488
502
|
chunks.length,
|
|
489
503
|
isRunning,
|
|
490
504
|
metadata.error,
|
|
505
|
+
undefined,
|
|
506
|
+
metadata.cancelledMessageIds,
|
|
491
507
|
),
|
|
492
508
|
);
|
|
493
509
|
return completeExternalMessageConversion(result, metadata.error);
|
|
@@ -88,4 +88,77 @@ describe("resolveToolApprovalResponse", () => {
|
|
|
88
88
|
resolveToolApprovalResponse({ id: "a2" }, { optionId: "once" }),
|
|
89
89
|
).toThrow('no option with id "once"');
|
|
90
90
|
});
|
|
91
|
+
|
|
92
|
+
it("resolves a free-form answer as approved and carries the text", () => {
|
|
93
|
+
expect(
|
|
94
|
+
resolveToolApprovalResponse(
|
|
95
|
+
{ id: "q1", display: "text" },
|
|
96
|
+
{ text: "staging" },
|
|
97
|
+
),
|
|
98
|
+
).toEqual({ approvalId: "q1", approved: true, text: "staging" });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("carries the text alongside a chosen option", () => {
|
|
102
|
+
expect(
|
|
103
|
+
resolveToolApprovalResponse(
|
|
104
|
+
{
|
|
105
|
+
id: "q2",
|
|
106
|
+
display: "select",
|
|
107
|
+
allowFreeform: true,
|
|
108
|
+
options: [{ id: "other", kind: "_other" }],
|
|
109
|
+
},
|
|
110
|
+
{ optionId: "other", approved: true, text: "somewhere else" },
|
|
111
|
+
),
|
|
112
|
+
).toEqual({
|
|
113
|
+
approvalId: "q2",
|
|
114
|
+
approved: true,
|
|
115
|
+
optionId: "other",
|
|
116
|
+
text: "somewhere else",
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("throws when the request does not accept a free-form answer", () => {
|
|
121
|
+
expect(() =>
|
|
122
|
+
resolveToolApprovalResponse(approval, { text: "anything" }),
|
|
123
|
+
).toThrow("does not accept a free-form answer");
|
|
124
|
+
expect(() =>
|
|
125
|
+
resolveToolApprovalResponse(
|
|
126
|
+
{ id: "a4", display: "select" },
|
|
127
|
+
{ approved: true, text: "anything" },
|
|
128
|
+
),
|
|
129
|
+
).toThrow("does not accept a free-form answer");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("refuses to infer approval from a bare answer on a decision", () => {
|
|
133
|
+
expect(() =>
|
|
134
|
+
resolveToolApprovalResponse(
|
|
135
|
+
{ id: "a5", display: "decision", allowFreeform: true },
|
|
136
|
+
{ text: "not this one" },
|
|
137
|
+
),
|
|
138
|
+
).toThrow("is a decision, not a question");
|
|
139
|
+
expect(() =>
|
|
140
|
+
resolveToolApprovalResponse(
|
|
141
|
+
{ id: "a6", allowFreeform: true },
|
|
142
|
+
{ text: "not this one" },
|
|
143
|
+
),
|
|
144
|
+
).toThrow("is a decision, not a question");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("records the answer alongside an explicit decision on a gate", () => {
|
|
148
|
+
expect(
|
|
149
|
+
resolveToolApprovalResponse(
|
|
150
|
+
{ id: "a7", display: "decision", allowFreeform: true },
|
|
151
|
+
{ approved: false, text: "not this one" },
|
|
152
|
+
),
|
|
153
|
+
).toEqual({ approvalId: "a7", approved: false, text: "not this one" });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("keeps a refusal a refusal when the request accepts text", () => {
|
|
157
|
+
expect(
|
|
158
|
+
resolveToolApprovalResponse(
|
|
159
|
+
{ id: "q3", allowFreeform: true },
|
|
160
|
+
{ approved: false, reason: "not answering" },
|
|
161
|
+
),
|
|
162
|
+
).toEqual({ approvalId: "q3", approved: false, reason: "not answering" });
|
|
163
|
+
});
|
|
91
164
|
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
toolApprovalAcceptsText,
|
|
3
|
+
type ToolApprovalDisplay,
|
|
4
|
+
type ToolApprovalOption,
|
|
5
|
+
type ToolApprovalResponse,
|
|
4
6
|
} from "../../types/message";
|
|
5
7
|
import type { RespondToToolApprovalOptions } from "../interfaces/thread-runtime-core";
|
|
6
8
|
|
|
@@ -12,16 +14,25 @@ const APPROVED_BY_KIND: Record<string, boolean> = {
|
|
|
12
14
|
};
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
|
-
* Resolves a renderer-facing approval response (boolean
|
|
16
|
-
* against the approval's
|
|
17
|
+
* Resolves a renderer-facing approval response (boolean, optionId, or
|
|
18
|
+
* free-form answer) against the approval's request shape into the
|
|
19
|
+
* runtime-facing decision shape.
|
|
17
20
|
*/
|
|
18
21
|
export const resolveToolApprovalResponse = (
|
|
19
22
|
approval: {
|
|
20
23
|
readonly id: string;
|
|
24
|
+
readonly display?: ToolApprovalDisplay;
|
|
25
|
+
readonly allowFreeform?: boolean;
|
|
21
26
|
readonly options?: readonly ToolApprovalOption[];
|
|
22
27
|
},
|
|
23
28
|
response: ToolApprovalResponse,
|
|
24
29
|
): RespondToToolApprovalOptions => {
|
|
30
|
+
const text = response.text;
|
|
31
|
+
if (text !== undefined && !toolApprovalAcceptsText(approval))
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Tool approval "${approval.id}" does not accept a free-form answer; the request must declare display "text" or allowFreeform`,
|
|
34
|
+
);
|
|
35
|
+
|
|
25
36
|
let approved: boolean;
|
|
26
37
|
let optionId: string | undefined;
|
|
27
38
|
|
|
@@ -42,14 +53,24 @@ export const resolveToolApprovalResponse = (
|
|
|
42
53
|
approved = APPROVED_BY_KIND[option.kind]!;
|
|
43
54
|
}
|
|
44
55
|
optionId = option.id;
|
|
45
|
-
} else {
|
|
56
|
+
} else if ("approved" in response) {
|
|
46
57
|
approved = response.approved;
|
|
58
|
+
} else {
|
|
59
|
+
// A bare answer resolves a question, where answering is not refusing. On a
|
|
60
|
+
// decision the approval itself is the authorization, so inferring one from
|
|
61
|
+
// a typed note would let the note authorize the call.
|
|
62
|
+
if (approval.display !== "text" && approval.display !== "select")
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Tool approval "${approval.id}" is a decision, not a question; respond with an explicit approved value, optionally alongside the answer`,
|
|
65
|
+
);
|
|
66
|
+
approved = true;
|
|
47
67
|
}
|
|
48
68
|
|
|
49
69
|
return {
|
|
50
70
|
approvalId: approval.id,
|
|
51
71
|
approved,
|
|
52
72
|
...(optionId !== undefined && { optionId }),
|
|
73
|
+
...(text !== undefined && { text }),
|
|
53
74
|
...(response.reason != null && { reason: response.reason }),
|
|
54
75
|
};
|
|
55
76
|
};
|
|
@@ -23,8 +23,8 @@ import type {
|
|
|
23
23
|
MessageTiming,
|
|
24
24
|
PartProviderMetadata,
|
|
25
25
|
TextMessagePart,
|
|
26
|
-
ToolApprovalOption,
|
|
27
26
|
ToolCallTiming,
|
|
27
|
+
ToolCallMessagePart,
|
|
28
28
|
ToolCallMessagePartMcpMetadata,
|
|
29
29
|
} from "../../types/message";
|
|
30
30
|
import type {
|
|
@@ -66,15 +66,7 @@ export type ThreadMessageLike = {
|
|
|
66
66
|
readonly timing?: ToolCallTiming;
|
|
67
67
|
readonly mcp?: ToolCallMessagePartMcpMetadata;
|
|
68
68
|
readonly providerMetadata?: PartProviderMetadata;
|
|
69
|
-
readonly approval?:
|
|
70
|
-
readonly id: string;
|
|
71
|
-
readonly approved?: boolean;
|
|
72
|
-
readonly reason?: string;
|
|
73
|
-
readonly isAutomatic?: boolean;
|
|
74
|
-
readonly options?: readonly ToolApprovalOption[];
|
|
75
|
-
readonly optionId?: string;
|
|
76
|
-
readonly resolution?: "cancelled" | "expired";
|
|
77
|
-
};
|
|
69
|
+
readonly approval?: NonNullable<ToolCallMessagePart["approval"]>;
|
|
78
70
|
}
|
|
79
71
|
)[];
|
|
80
72
|
readonly id?: string | undefined;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type {
|
|
3
|
+
ThreadAssistantMessage,
|
|
4
|
+
ThreadAssistantMessagePart,
|
|
5
|
+
ThreadMessage,
|
|
6
|
+
ToolCallMessagePart,
|
|
7
|
+
} from "../../types/message";
|
|
8
|
+
import {
|
|
9
|
+
iterateToolCallParts,
|
|
10
|
+
mapToolCallPartsDeep,
|
|
11
|
+
walkToolCallTree,
|
|
12
|
+
} from "./tool-call-tree";
|
|
13
|
+
|
|
14
|
+
const toolCall = (
|
|
15
|
+
toolCallId: string,
|
|
16
|
+
messages?: readonly ThreadMessage[],
|
|
17
|
+
): ToolCallMessagePart => ({
|
|
18
|
+
type: "tool-call",
|
|
19
|
+
toolCallId,
|
|
20
|
+
toolName: "t",
|
|
21
|
+
argsText: "{}",
|
|
22
|
+
args: {},
|
|
23
|
+
...(messages !== undefined && { messages }),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const assistant = (
|
|
27
|
+
id: string,
|
|
28
|
+
content: readonly ThreadAssistantMessagePart[],
|
|
29
|
+
): ThreadAssistantMessage =>
|
|
30
|
+
({
|
|
31
|
+
id,
|
|
32
|
+
role: "assistant",
|
|
33
|
+
content,
|
|
34
|
+
status: { type: "complete", reason: "unknown" },
|
|
35
|
+
metadata: {
|
|
36
|
+
unstable_state: null,
|
|
37
|
+
unstable_annotations: [],
|
|
38
|
+
unstable_data: [],
|
|
39
|
+
steps: [],
|
|
40
|
+
custom: {},
|
|
41
|
+
},
|
|
42
|
+
createdAt: new Date(0),
|
|
43
|
+
attachments: [],
|
|
44
|
+
}) as unknown as ThreadAssistantMessage;
|
|
45
|
+
|
|
46
|
+
const user = (id: string): ThreadMessage =>
|
|
47
|
+
({
|
|
48
|
+
id,
|
|
49
|
+
role: "user",
|
|
50
|
+
content: [{ type: "text", text: "hi" }],
|
|
51
|
+
metadata: { custom: {} },
|
|
52
|
+
createdAt: new Date(0),
|
|
53
|
+
attachments: [],
|
|
54
|
+
}) as unknown as ThreadMessage;
|
|
55
|
+
|
|
56
|
+
// root
|
|
57
|
+
// └─ a ─ nested message "m2"
|
|
58
|
+
// ├─ b ─ nested message "m3"
|
|
59
|
+
// │ └─ c
|
|
60
|
+
// └─ d
|
|
61
|
+
const tree = () => {
|
|
62
|
+
const c = toolCall("c");
|
|
63
|
+
const b = toolCall("b", [assistant("m3", [c])]);
|
|
64
|
+
const d = toolCall("d");
|
|
65
|
+
const a = toolCall("a", [assistant("m2", [b, d])]);
|
|
66
|
+
return { a, b, c, d };
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
describe("walkToolCallTree", () => {
|
|
70
|
+
it("yields every part in document order, each ahead of its descendants", () => {
|
|
71
|
+
const { a } = tree();
|
|
72
|
+
const entries = [
|
|
73
|
+
...walkToolCallTree([user("m0"), assistant("m1", [a, toolCall("e")])]),
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
expect(entries.map((entry) => entry.part.toolCallId)).toEqual([
|
|
77
|
+
"a",
|
|
78
|
+
"b",
|
|
79
|
+
"c",
|
|
80
|
+
"d",
|
|
81
|
+
"e",
|
|
82
|
+
]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("reports the message that directly holds each part", () => {
|
|
86
|
+
const { a } = tree();
|
|
87
|
+
const entries = [...walkToolCallTree([assistant("m1", [a])])];
|
|
88
|
+
|
|
89
|
+
expect(
|
|
90
|
+
Object.fromEntries(
|
|
91
|
+
entries.map((entry) => [entry.part.toolCallId, entry.messageId]),
|
|
92
|
+
),
|
|
93
|
+
).toEqual({ a: "m1", b: "m2", c: "m3", d: "m2" });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("skips messages whose content is not an array", () => {
|
|
97
|
+
const malformed = {
|
|
98
|
+
id: "m1",
|
|
99
|
+
role: "assistant",
|
|
100
|
+
} as unknown as ThreadMessage;
|
|
101
|
+
expect([...walkToolCallTree([malformed])]).toEqual([]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("skips a non-assistant message carrying a tool-call part", () => {
|
|
105
|
+
const malformed = {
|
|
106
|
+
...user("m2"),
|
|
107
|
+
content: [toolCall("b")],
|
|
108
|
+
} as unknown as ThreadMessage;
|
|
109
|
+
const a = toolCall("a", [malformed]);
|
|
110
|
+
|
|
111
|
+
expect(
|
|
112
|
+
[...walkToolCallTree([assistant("m1", [a])])].map(
|
|
113
|
+
(entry) => entry.part.toolCallId,
|
|
114
|
+
),
|
|
115
|
+
).toEqual(["a"]);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("iterateToolCallParts", () => {
|
|
120
|
+
it("matches walkToolCallTree over the same content", () => {
|
|
121
|
+
const { a } = tree();
|
|
122
|
+
const content = [a, toolCall("e")];
|
|
123
|
+
|
|
124
|
+
expect([...iterateToolCallParts(content)].map((p) => p.toolCallId)).toEqual(
|
|
125
|
+
[...walkToolCallTree([assistant("m1", content)])].map(
|
|
126
|
+
(entry) => entry.part.toolCallId,
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("ignores non tool-call parts", () => {
|
|
132
|
+
expect([
|
|
133
|
+
...iterateToolCallParts([
|
|
134
|
+
{ type: "text", text: "hi" } as ThreadAssistantMessagePart,
|
|
135
|
+
toolCall("a"),
|
|
136
|
+
]),
|
|
137
|
+
]).toHaveLength(1);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("mapToolCallPartsDeep", () => {
|
|
142
|
+
it("rewrites nested parts and reports the change", () => {
|
|
143
|
+
const { a } = tree();
|
|
144
|
+
const content = [a];
|
|
145
|
+
const { content: next, changed } = mapToolCallPartsDeep(content, (part) =>
|
|
146
|
+
part.toolCallId === "c" ? { ...part, isError: true } : part,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
expect(changed).toBe(true);
|
|
150
|
+
const parts = [...iterateToolCallParts(next)];
|
|
151
|
+
expect(parts.find((part) => part.toolCallId === "c")?.isError).toBe(true);
|
|
152
|
+
expect(
|
|
153
|
+
parts.find((part) => part.toolCallId === "d")?.isError,
|
|
154
|
+
).toBeUndefined();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("skips a nested message whose content is not an array", () => {
|
|
158
|
+
const malformed = {
|
|
159
|
+
id: "m2",
|
|
160
|
+
role: "assistant",
|
|
161
|
+
} as unknown as ThreadMessage;
|
|
162
|
+
const a = toolCall("a", [malformed]);
|
|
163
|
+
|
|
164
|
+
expect(() =>
|
|
165
|
+
mapToolCallPartsDeep([a], (part) => ({ ...part, isError: true })),
|
|
166
|
+
).not.toThrow();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("keeps the original content identity when nothing changed", () => {
|
|
170
|
+
const { a } = tree();
|
|
171
|
+
const content = [a, toolCall("e")];
|
|
172
|
+
const result = mapToolCallPartsDeep(content, (part) => part);
|
|
173
|
+
|
|
174
|
+
expect(result.changed).toBe(false);
|
|
175
|
+
expect(result.content).toBe(content);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("preserves untouched sibling messages by identity", () => {
|
|
179
|
+
const nested = assistant("m2", [toolCall("b")]);
|
|
180
|
+
const sibling = user("m3");
|
|
181
|
+
const a = toolCall("a", [nested, sibling]);
|
|
182
|
+
const { content: next } = mapToolCallPartsDeep([a], (part) =>
|
|
183
|
+
part.toolCallId === "b" ? { ...part, isError: true } : part,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const mapped = next[0] as ToolCallMessagePart;
|
|
187
|
+
expect(mapped).not.toBe(a);
|
|
188
|
+
expect(mapped.messages?.[1]).toBe(sibling);
|
|
189
|
+
expect(mapped.messages?.[0]).not.toBe(nested);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ThreadAssistantMessage,
|
|
3
|
+
ThreadAssistantMessagePart,
|
|
4
|
+
ThreadMessage,
|
|
5
|
+
ToolCallMessagePart,
|
|
6
|
+
} from "../../types/message";
|
|
7
|
+
|
|
8
|
+
export type ToolCallTreeEntry = {
|
|
9
|
+
readonly part: ToolCallMessagePart;
|
|
10
|
+
/** Id of the message whose content directly holds `part`. */
|
|
11
|
+
readonly messageId: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Walk every tool-call part reachable from `messages`. A tool call projects a
|
|
16
|
+
* child run as nested messages on `ToolCallMessagePart.messages`, so a
|
|
17
|
+
* subagent's own calls live below the top-level content and a seam that only
|
|
18
|
+
* scans the top level cannot reach them.
|
|
19
|
+
*
|
|
20
|
+
* Parts arrive in document order, each one ahead of its own descendants.
|
|
21
|
+
* `messageId` names the message that directly holds the part, which for a
|
|
22
|
+
* nested call is the child run's message rather than the top-level message the
|
|
23
|
+
* tree hangs from. Only assistant messages are descended, the same rule
|
|
24
|
+
* {@link mapToolCallPartsDeep} rewrites under, so a part this reports is always
|
|
25
|
+
* a part that can be written back.
|
|
26
|
+
*/
|
|
27
|
+
export function* walkToolCallTree(
|
|
28
|
+
messages: readonly ThreadMessage[],
|
|
29
|
+
): Generator<ToolCallTreeEntry> {
|
|
30
|
+
for (const message of messages) {
|
|
31
|
+
if (message?.role !== "assistant" || !Array.isArray(message.content)) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
for (const part of message.content) {
|
|
35
|
+
if (!part || part.type !== "tool-call") continue;
|
|
36
|
+
yield { part, messageId: message.id };
|
|
37
|
+
if (part.messages?.length) yield* walkToolCallTree(part.messages);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Content-level form of {@link walkToolCallTree}, for a caller that already
|
|
44
|
+
* holds one assistant message's content and does not need the owning id.
|
|
45
|
+
*/
|
|
46
|
+
export function* iterateToolCallParts(
|
|
47
|
+
content: readonly ThreadAssistantMessagePart[],
|
|
48
|
+
): Generator<ToolCallMessagePart> {
|
|
49
|
+
for (const part of content) {
|
|
50
|
+
if (!part || part.type !== "tool-call") continue;
|
|
51
|
+
yield part;
|
|
52
|
+
if (part.messages?.length) {
|
|
53
|
+
for (const entry of walkToolCallTree(part.messages)) {
|
|
54
|
+
yield entry.part;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Rebuild `content` with `fn` applied to every tool-call part in the tree.
|
|
62
|
+
* Arrays and nested messages are reused wherever `fn` returned the part it was
|
|
63
|
+
* given, so an unchanged subtree keeps its identity and `changed` reports
|
|
64
|
+
* whether anything moved.
|
|
65
|
+
*/
|
|
66
|
+
export function mapToolCallPartsDeep(
|
|
67
|
+
content: readonly ThreadAssistantMessagePart[],
|
|
68
|
+
fn: (part: ToolCallMessagePart) => ToolCallMessagePart,
|
|
69
|
+
): { content: readonly ThreadAssistantMessagePart[]; changed: boolean } {
|
|
70
|
+
let changed = false;
|
|
71
|
+
const next = content.map((part): ThreadAssistantMessagePart => {
|
|
72
|
+
if (part.type !== "tool-call") return part;
|
|
73
|
+
let mapped = fn(part);
|
|
74
|
+
if (mapped.messages !== undefined) {
|
|
75
|
+
let nestedChanged = false;
|
|
76
|
+
const nestedMessages = mapped.messages.map((nested) => {
|
|
77
|
+
if (nested.role !== "assistant" || !Array.isArray(nested.content)) {
|
|
78
|
+
return nested;
|
|
79
|
+
}
|
|
80
|
+
const assistant = nested as ThreadAssistantMessage;
|
|
81
|
+
const result = mapToolCallPartsDeep(assistant.content, fn);
|
|
82
|
+
if (!result.changed) return nested;
|
|
83
|
+
nestedChanged = true;
|
|
84
|
+
return { ...assistant, content: result.content };
|
|
85
|
+
});
|
|
86
|
+
if (nestedChanged) {
|
|
87
|
+
mapped =
|
|
88
|
+
mapped === part
|
|
89
|
+
? { ...part, messages: nestedMessages }
|
|
90
|
+
: { ...mapped, messages: nestedMessages };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (mapped !== part) changed = true;
|
|
94
|
+
return mapped;
|
|
95
|
+
});
|
|
96
|
+
return changed ? { content: next, changed } : { content, changed };
|
|
97
|
+
}
|