@assistant-ui/core 0.2.6 → 0.2.7
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 +3 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/duplicate-detection.d.ts +5 -0
- package/dist/internal/duplicate-detection.d.ts.map +1 -0
- package/dist/internal/duplicate-detection.js +11 -0
- package/dist/internal/duplicate-detection.js.map +1 -0
- package/dist/react/AssistantProvider.d.ts.map +1 -1
- package/dist/react/AssistantProvider.js.map +1 -1
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +1 -2
- package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js.map +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
- package/dist/react/runtimes/external-message-converter.d.ts +1 -1
- package/dist/react/runtimes/external-message-converter.js.map +1 -1
- package/dist/runtime/api/attachment-runtime.d.ts.map +1 -1
- package/dist/runtime/api/attachment-runtime.js.map +1 -1
- package/dist/runtime/interfaces/thread-runtime-core.d.ts +8 -0
- package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-adapter.d.ts +31 -0
- package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +25 -0
- 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 +94 -3
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts +168 -0
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -0
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +449 -0
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -0
- package/dist/subscribable/subscribable.d.ts.map +1 -1
- package/dist/subscribable/subscribable.js.map +1 -1
- package/package.json +3 -3
- package/src/adapters/index.ts +1 -4
- package/src/index.ts +10 -0
- package/src/internal/duplicate-detection.ts +26 -0
- package/src/react/AssistantProvider.tsx +2 -3
- package/src/react/index.ts +1 -6
- package/src/react/primitives/chainOfThought/ChainOfThoughtParts.tsx +1 -2
- package/src/react/primitives/message/MessageAttachments.test.tsx +1 -1
- package/src/react/primitives/message/MessageGroupedParts.tsx +1 -1
- package/src/react/runtimes/external-message-converter.ts +1 -1
- package/src/runtime/api/attachment-runtime.ts +1 -2
- package/src/runtime/interfaces/thread-runtime-core.ts +8 -0
- package/src/runtime/internal.ts +1 -4
- package/src/runtimes/external-store/external-store-adapter.ts +33 -0
- package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +1 -3
- package/src/runtimes/external-store/external-store-thread-runtime-core.ts +161 -4
- package/src/runtimes/tool-invocations/EDGE_CASES.md +194 -0
- package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +1054 -0
- package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +783 -0
- package/src/subscribable/subscribable.ts +3 -3
- package/src/tests/OptimisticState-delete-crash.test.ts +2 -0
- package/src/tests/OptimisticState-list-race.test.ts +2 -0
- package/src/tests/RemoteThreadListThreadListRuntimeCore-loadMore.test.ts +5 -5
- package/src/tests/auiV0Encode.test.ts +1 -1
- package/src/tests/composer-can-send.test.ts +8 -4
- package/src/tests/duplicate-detection.test.ts +34 -0
- package/src/tests/external-store-thread-list-runtime-core.test.ts +1 -1
- package/src/tests/external-store-thread-runtime-core.test.ts +7 -6
- package/src/tests/no-unsafe-process-env.test.ts +1 -0
- package/src/tests/remote-thread-list-isLoading.test.ts +2 -0
- package/src/tests/thread-message-like.test.ts +4 -1
- package/src/types/index.ts +1 -4
- package/dist/react/runtimes/useToolInvocations.d.ts +0 -53
- package/dist/react/runtimes/useToolInvocations.d.ts.map +0 -1
- package/dist/react/runtimes/useToolInvocations.js +0 -380
- package/dist/react/runtimes/useToolInvocations.js.map +0 -1
- package/src/react/runtimes/useToolInvocations.ts +0 -694
|
@@ -207,9 +207,9 @@ export class LazyMemoizeSubject<TState extends object, TPath>
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
export class NestedSubscriptionSubject<
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
210
|
+
TState extends Subscribable | undefined,
|
|
211
|
+
TPath,
|
|
212
|
+
>
|
|
213
213
|
extends BaseSubject
|
|
214
214
|
implements
|
|
215
215
|
SubscribableWithState<TState, TPath>,
|
|
@@ -24,6 +24,8 @@ const createThreadState = (
|
|
|
24
24
|
const mappingId = createThreadMappingId(threadId);
|
|
25
25
|
return {
|
|
26
26
|
isLoading: false,
|
|
27
|
+
isLoadingMore: false,
|
|
28
|
+
cursor: undefined,
|
|
27
29
|
newThreadId: undefined,
|
|
28
30
|
threadIds: status === "regular" ? [threadId] : [],
|
|
29
31
|
archivedThreadIds: status === "archived" ? [threadId] : [],
|
|
@@ -20,7 +20,7 @@ describe("RemoteThreadListThreadListRuntimeCore.loadMore", () => {
|
|
|
20
20
|
|
|
21
21
|
it("initial list response with nextCursor sets hasMore=true", async () => {
|
|
22
22
|
const adapter = makeAdapter({
|
|
23
|
-
list: vi.fn(async () => ({
|
|
23
|
+
list: vi.fn<ListFn>(async () => ({
|
|
24
24
|
threads: [
|
|
25
25
|
{
|
|
26
26
|
status: "regular",
|
|
@@ -41,7 +41,7 @@ describe("RemoteThreadListThreadListRuntimeCore.loadMore", () => {
|
|
|
41
41
|
|
|
42
42
|
it("absent nextCursor leaves hasMore=false", async () => {
|
|
43
43
|
const adapter = makeAdapter({
|
|
44
|
-
list: vi.fn(async () => ({
|
|
44
|
+
list: vi.fn<ListFn>(async () => ({
|
|
45
45
|
threads: [
|
|
46
46
|
{
|
|
47
47
|
status: "regular",
|
|
@@ -243,7 +243,7 @@ describe("RemoteThreadListThreadListRuntimeCore.loadMore", () => {
|
|
|
243
243
|
|
|
244
244
|
it("__internal_setOptions clears cursor and dedup handles on adapter swap, then refetches via the new adapter", async () => {
|
|
245
245
|
const firstAdapter = makeAdapter({
|
|
246
|
-
list: vi.fn(async () => ({
|
|
246
|
+
list: vi.fn<ListFn>(async () => ({
|
|
247
247
|
threads: [{ status: "regular", remoteId: "old", externalId: "old" }],
|
|
248
248
|
nextCursor: "old-cursor",
|
|
249
249
|
})),
|
|
@@ -253,7 +253,7 @@ describe("RemoteThreadListThreadListRuntimeCore.loadMore", () => {
|
|
|
253
253
|
expect(core.threadIds).toEqual(["old"]);
|
|
254
254
|
expect(core.hasMore).toBe(true);
|
|
255
255
|
|
|
256
|
-
const secondList = vi.fn(async () => ({
|
|
256
|
+
const secondList = vi.fn<ListFn>(async () => ({
|
|
257
257
|
threads: [{ status: "regular", remoteId: "new", externalId: "new" }],
|
|
258
258
|
}));
|
|
259
259
|
const secondAdapter = makeAdapter({ list: secondList });
|
|
@@ -290,7 +290,7 @@ describe("RemoteThreadListThreadListRuntimeCore.loadMore", () => {
|
|
|
290
290
|
const stale = core.loadMore();
|
|
291
291
|
|
|
292
292
|
const secondAdapter = makeAdapter({
|
|
293
|
-
list: vi.fn(async () => ({
|
|
293
|
+
list: vi.fn<ListFn>(async () => ({
|
|
294
294
|
threads: [{ status: "regular", remoteId: "ignored", externalId: "x" }],
|
|
295
295
|
})),
|
|
296
296
|
});
|
|
@@ -102,10 +102,14 @@ describe("BaseComposerRuntimeCore.send", () => {
|
|
|
102
102
|
describe("DefaultEditComposerRuntimeCore.canSend", () => {
|
|
103
103
|
it("ignores runtime.isSendDisabled (thread-scoped flag does not block edits)", () => {
|
|
104
104
|
const stub = makeRuntimeStub({ isSendDisabled: true });
|
|
105
|
-
const composer = new DefaultEditComposerRuntimeCore(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
const composer = new DefaultEditComposerRuntimeCore(
|
|
106
|
+
stub as unknown as ThreadRuntimeCore,
|
|
107
|
+
() => {},
|
|
108
|
+
{
|
|
109
|
+
parentId: null,
|
|
110
|
+
message: makeUserMessage("seed"),
|
|
111
|
+
},
|
|
112
|
+
);
|
|
109
113
|
|
|
110
114
|
expect(composer.canSend).toBe(true);
|
|
111
115
|
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import { checkDuplicateCore } from "../internal/duplicate-detection";
|
|
3
|
+
|
|
4
|
+
const KEY = Symbol.for("@assistant-ui/core.loaded");
|
|
5
|
+
|
|
6
|
+
function reset(): void {
|
|
7
|
+
delete (globalThis as unknown as Record<symbol, unknown>)[KEY];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe("checkDuplicateCore", () => {
|
|
11
|
+
let warn: ReturnType<typeof vi.spyOn>;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
reset();
|
|
15
|
+
warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
warn.mockRestore();
|
|
20
|
+
reset();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("does not warn on the first load", () => {
|
|
24
|
+
checkDuplicateCore();
|
|
25
|
+
expect(warn).not.toHaveBeenCalled();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("warns when a second copy registers in the same runtime", () => {
|
|
29
|
+
checkDuplicateCore();
|
|
30
|
+
checkDuplicateCore();
|
|
31
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
32
|
+
expect(warn.mock.calls[0]![0]).toMatch(/npx assistant-ui doctor/);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -184,7 +184,7 @@ describe("ExternalStoreThreadListRuntimeCore - isMain via ThreadListRuntimeImpl"
|
|
|
184
184
|
const core = new ExternalStoreThreadListRuntimeCore(adapter, makeFactory());
|
|
185
185
|
return new ThreadListRuntimeImpl(
|
|
186
186
|
core,
|
|
187
|
-
NoopThreadRuntime as unknown as
|
|
187
|
+
NoopThreadRuntime as unknown as ConstructorParameters<
|
|
188
188
|
typeof ThreadListRuntimeImpl
|
|
189
189
|
>[1],
|
|
190
190
|
);
|
|
@@ -8,12 +8,13 @@ const mockContextProvider: ModelContextProvider = {
|
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
const makeStore = (
|
|
11
|
-
overrides?: Partial<ExternalStoreAdapter>,
|
|
12
|
-
): ExternalStoreAdapter =>
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
overrides?: Partial<ExternalStoreAdapter> | Record<string, unknown>,
|
|
12
|
+
): ExternalStoreAdapter =>
|
|
13
|
+
({
|
|
14
|
+
messages: [],
|
|
15
|
+
onNew: vi.fn(),
|
|
16
|
+
...overrides,
|
|
17
|
+
}) as ExternalStoreAdapter;
|
|
17
18
|
|
|
18
19
|
describe("ExternalStoreThreadRuntimeCore - state reference stability", () => {
|
|
19
20
|
describe("capabilities", () => {
|
|
@@ -2,7 +2,10 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
import { fromThreadMessageLike } from "../runtime/utils/thread-message-like";
|
|
3
3
|
|
|
4
4
|
const fallbackId = "test-id";
|
|
5
|
-
const fallbackStatus = {
|
|
5
|
+
const fallbackStatus = {
|
|
6
|
+
type: "complete" as const,
|
|
7
|
+
reason: "stop" as const,
|
|
8
|
+
};
|
|
6
9
|
|
|
7
10
|
describe("fromThreadMessageLike", () => {
|
|
8
11
|
describe("data-* prefixed types", () => {
|
package/src/types/index.ts
CHANGED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { ThreadMessage } from "../../types/message.js";
|
|
2
|
-
import { Tool, ToolModelContentPart } from "assistant-stream";
|
|
3
|
-
import { ReadonlyJSONValue } from "assistant-stream/utils";
|
|
4
|
-
|
|
5
|
-
//#region src/react/runtimes/useToolInvocations.d.ts
|
|
6
|
-
type AssistantTransportState = {
|
|
7
|
-
readonly messages: readonly ThreadMessage[];
|
|
8
|
-
readonly state?: ReadonlyJSONValue;
|
|
9
|
-
readonly isRunning: boolean;
|
|
10
|
-
};
|
|
11
|
-
type AddToolResultCommand = {
|
|
12
|
-
readonly type: "add-tool-result";
|
|
13
|
-
readonly toolCallId: string;
|
|
14
|
-
readonly toolName: string;
|
|
15
|
-
readonly result: ReadonlyJSONValue;
|
|
16
|
-
readonly isError: boolean;
|
|
17
|
-
readonly artifact?: ReadonlyJSONValue;
|
|
18
|
-
readonly modelContent?: readonly ToolModelContentPart[];
|
|
19
|
-
};
|
|
20
|
-
type UseToolInvocationsParams = {
|
|
21
|
-
state: AssistantTransportState;
|
|
22
|
-
getTools: () => Record<string, Tool> | undefined;
|
|
23
|
-
onResult: (command: AddToolResultCommand) => void;
|
|
24
|
-
setToolStatuses: (updater: Record<string, ToolExecutionStatus> | ((prev: Record<string, ToolExecutionStatus>) => Record<string, ToolExecutionStatus>)) => void;
|
|
25
|
-
};
|
|
26
|
-
/**
|
|
27
|
-
* Streaming execution state for a frontend tool.
|
|
28
|
-
*
|
|
29
|
-
* Custom runtime integrations use this to mirror in-flight tool calls while
|
|
30
|
-
* `useToolInvocations` executes tools in the browser.
|
|
31
|
-
*/
|
|
32
|
-
type ToolExecutionStatus = {
|
|
33
|
-
/** The tool's execute function is currently running. */type: "executing";
|
|
34
|
-
} | {
|
|
35
|
-
/** The tool is waiting for a human input payload before continuing. */type: "interrupt"; /** Human input request emitted by the tool execution context. */
|
|
36
|
-
payload: {
|
|
37
|
-
type: "human";
|
|
38
|
-
payload: unknown;
|
|
39
|
-
};
|
|
40
|
-
};
|
|
41
|
-
declare function useToolInvocations({
|
|
42
|
-
state,
|
|
43
|
-
getTools,
|
|
44
|
-
onResult,
|
|
45
|
-
setToolStatuses
|
|
46
|
-
}: UseToolInvocationsParams): {
|
|
47
|
-
reset: () => void;
|
|
48
|
-
abort: () => Promise<void>;
|
|
49
|
-
resume: (toolCallId: string, payload: unknown) => void;
|
|
50
|
-
};
|
|
51
|
-
//#endregion
|
|
52
|
-
export { AddToolResultCommand, AssistantTransportState, ToolExecutionStatus, useToolInvocations };
|
|
53
|
-
//# sourceMappingURL=useToolInvocations.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"useToolInvocations.d.ts","names":[],"sources":["../../../src/react/runtimes/useToolInvocations.ts"],"mappings":";;;;;KAkBY,uBAAA;EAAA,SACD,QAAA,WAAmB,aAAA;EAAA,SACnB,KAAA,GAAQ,iBAAiB;EAAA,SACzB,SAAA;AAAA;AAAA,KAGC,oBAAA;EAAA,SACD,IAAA;EAAA,SACA,UAAA;EAAA,SACA,QAAA;EAAA,SACA,MAAA,EAAQ,iBAAA;EAAA,SACR,OAAA;EAAA,SACA,QAAA,GAAW,iBAAA;EAAA,SACX,YAAA,YAAwB,oBAAA;AAAA;AAAA,KA2B9B,wBAAA;EACH,KAAA,EAAO,uBAAA;EACP,QAAA,QAAgB,MAAA,SAAe,IAAA;EAC/B,QAAA,GAAW,OAAA,EAAS,oBAAA;EACpB,eAAA,GACE,OAAA,EACI,MAAA,SAAe,mBAAA,MAEb,IAAA,EAAM,MAAA,SAAe,mBAAA,MAClB,MAAA,SAAe,mBAAA;AAAA;;;;;;;KAUhB,mBAAA;EA/CD,wDAkDL,IAAA;AAAA;EAjD6B,uEAqD7B,IAAA,eArDiD;EAuDjD,OAAA;IAAW,IAAA;IAAe,OAAA;EAAA;AAAA;AAAA,iBAyChB,kBAAA,CAAA;EACd,KAAA;EACA,QAAA;EACA,QAAA;EACA;AAAA,GACC,wBAAA;;eA+fiB,OAAA;+BAkCW,OAAA;AAAA"}
|
|
@@ -1,380 +0,0 @@
|
|
|
1
|
-
import { isJSONValueEqual } from "../../utils/json/is-json-equal.js";
|
|
2
|
-
import { ToolResponse, createAssistantStreamController, unstable_toolResultStream } from "assistant-stream";
|
|
3
|
-
import { AssistantMetaTransformStream } from "assistant-stream/utils";
|
|
4
|
-
import { useEffect, useRef, useState } from "react";
|
|
5
|
-
//#region src/react/runtimes/useToolInvocations.ts
|
|
6
|
-
const isArgsTextComplete = (argsText) => {
|
|
7
|
-
try {
|
|
8
|
-
JSON.parse(argsText);
|
|
9
|
-
return true;
|
|
10
|
-
} catch {
|
|
11
|
-
return false;
|
|
12
|
-
}
|
|
13
|
-
};
|
|
14
|
-
const parseArgsText = (argsText) => {
|
|
15
|
-
try {
|
|
16
|
-
return JSON.parse(argsText);
|
|
17
|
-
} catch {
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
const isEquivalentCompleteArgsText = (previous, next) => {
|
|
22
|
-
const previousValue = parseArgsText(previous);
|
|
23
|
-
const nextValue = parseArgsText(next);
|
|
24
|
-
if (previousValue === void 0 || nextValue === void 0) return false;
|
|
25
|
-
return isJSONValueEqual(previousValue, nextValue);
|
|
26
|
-
};
|
|
27
|
-
function useToolInvocations({ state, getTools, onResult, setToolStatuses }) {
|
|
28
|
-
/**
|
|
29
|
-
* Single source of truth for per-tool-call lifecycle. Keyed by *logical*
|
|
30
|
-
* toolCallId (the id the host knows). Restored entries have no controller;
|
|
31
|
-
* active entries carry their stream id and rewrite/execution bookkeeping.
|
|
32
|
-
*/
|
|
33
|
-
const entriesRef = useRef(/* @__PURE__ */ new Map());
|
|
34
|
-
/**
|
|
35
|
-
* Reverse alias map populated only when a rewrite assigns a synthetic stream
|
|
36
|
-
* id to an entry. Identity mappings are implicit via the fallback in
|
|
37
|
-
* `getLogicalToolCallId`.
|
|
38
|
-
*/
|
|
39
|
-
const streamToLogicalRef = useRef(/* @__PURE__ */ new Map());
|
|
40
|
-
/**
|
|
41
|
-
* Stream ids whose `result` chunks must be dropped before reaching `onResult`.
|
|
42
|
-
* Populated when:
|
|
43
|
-
* - an argsText rewrite supersedes a stream (the old stream's result, if
|
|
44
|
-
* any, is no longer authoritative)
|
|
45
|
-
* - `reset()` is called while a pre-resolved tool call has a never-settling
|
|
46
|
-
* Promise pending in the executor — the eventual cancellation chunk
|
|
47
|
-
* would otherwise be forwarded to a host that has already moved on.
|
|
48
|
-
*/
|
|
49
|
-
const abandonedStreamIdsRef = useRef(/* @__PURE__ */ new Set());
|
|
50
|
-
/**
|
|
51
|
-
* Stream ids whose `execute` should be short-circuited in the tool wrapper.
|
|
52
|
-
* Tracked by physical stream id (not logical id) so cleanup is keyed off
|
|
53
|
-
* the same id the wrapper sees in its context.
|
|
54
|
-
*/
|
|
55
|
-
const skipExecuteStreamIdsRef = useRef(/* @__PURE__ */ new Set());
|
|
56
|
-
const humanInputRef = useRef(/* @__PURE__ */ new Map());
|
|
57
|
-
/**
|
|
58
|
-
* In-flight `execute` invocations keyed by physical stream id. Lives outside
|
|
59
|
-
* `entriesRef` so `reset()` can drop tool-call state without orphaning the
|
|
60
|
-
* cleanup the cancellation `onExecutionEnd` still needs.
|
|
61
|
-
*/
|
|
62
|
-
const executingRef = useRef(/* @__PURE__ */ new Map());
|
|
63
|
-
const acRef = useRef(new AbortController());
|
|
64
|
-
const executingCountRef = useRef(0);
|
|
65
|
-
const settledResolversRef = useRef([]);
|
|
66
|
-
const rewriteCounterRef = useRef(0);
|
|
67
|
-
/**
|
|
68
|
-
* `true` until the first snapshot has been processed; `reset()` flips it
|
|
69
|
-
* back to `true`. Snapshots observed while this is `true` are treated as
|
|
70
|
-
* historical: their tool calls are recorded in `entriesRef` as restored
|
|
71
|
-
* but no streamCall/execute fires. The next snapshot is processed as live.
|
|
72
|
-
*/
|
|
73
|
-
const pendingRestoreRef = useRef(true);
|
|
74
|
-
const getLogicalToolCallId = (streamId) => streamToLogicalRef.current.get(streamId) ?? streamId;
|
|
75
|
-
const getWrappedTools = () => {
|
|
76
|
-
const tools = getTools();
|
|
77
|
-
if (!tools) return void 0;
|
|
78
|
-
return Object.fromEntries(Object.entries(tools).map(([name, tool]) => {
|
|
79
|
-
const execute = tool.execute;
|
|
80
|
-
const streamCall = tool.streamCall;
|
|
81
|
-
const toModelOutput = tool.toModelOutput;
|
|
82
|
-
return [name, {
|
|
83
|
-
...tool,
|
|
84
|
-
...execute !== void 0 && { execute: (...[args, context]) => {
|
|
85
|
-
if (skipExecuteStreamIdsRef.current.has(context.toolCallId)) return new Promise(() => {});
|
|
86
|
-
return execute(args, {
|
|
87
|
-
...context,
|
|
88
|
-
toolCallId: getLogicalToolCallId(context.toolCallId)
|
|
89
|
-
});
|
|
90
|
-
} },
|
|
91
|
-
...streamCall !== void 0 && { streamCall: (...[reader, context]) => streamCall(reader, {
|
|
92
|
-
...context,
|
|
93
|
-
toolCallId: getLogicalToolCallId(context.toolCallId)
|
|
94
|
-
}) },
|
|
95
|
-
...toModelOutput !== void 0 && { toModelOutput: (options) => toModelOutput({
|
|
96
|
-
...options,
|
|
97
|
-
toolCallId: getLogicalToolCallId(options.toolCallId)
|
|
98
|
-
}) }
|
|
99
|
-
}];
|
|
100
|
-
}));
|
|
101
|
-
};
|
|
102
|
-
const resolveAllSettledResolvers = () => {
|
|
103
|
-
const resolvers = settledResolversRef.current;
|
|
104
|
-
settledResolversRef.current = [];
|
|
105
|
-
resolvers.forEach((resolve) => resolve());
|
|
106
|
-
};
|
|
107
|
-
const [controller] = useState(() => {
|
|
108
|
-
const [stream, controller] = createAssistantStreamController();
|
|
109
|
-
const transform = unstable_toolResultStream(getWrappedTools, () => acRef.current?.signal ?? new AbortController().signal, (toolCallId, payload) => {
|
|
110
|
-
const logicalToolCallId = getLogicalToolCallId(toolCallId);
|
|
111
|
-
return new Promise((resolve, reject) => {
|
|
112
|
-
const previous = humanInputRef.current.get(logicalToolCallId);
|
|
113
|
-
if (previous) previous.reject(/* @__PURE__ */ new Error("Human input request was superseded by a new request"));
|
|
114
|
-
humanInputRef.current.set(logicalToolCallId, {
|
|
115
|
-
resolve,
|
|
116
|
-
reject
|
|
117
|
-
});
|
|
118
|
-
setToolStatuses((prev) => ({
|
|
119
|
-
...prev,
|
|
120
|
-
[logicalToolCallId]: {
|
|
121
|
-
type: "interrupt",
|
|
122
|
-
payload: {
|
|
123
|
-
type: "human",
|
|
124
|
-
payload
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}));
|
|
128
|
-
});
|
|
129
|
-
}, {
|
|
130
|
-
onExecutionStart: (streamId) => {
|
|
131
|
-
if (skipExecuteStreamIdsRef.current.has(streamId)) return;
|
|
132
|
-
const logicalToolCallId = getLogicalToolCallId(streamId);
|
|
133
|
-
const abandoned = abandonedStreamIdsRef.current.has(streamId);
|
|
134
|
-
executingRef.current.set(streamId, {
|
|
135
|
-
logicalToolCallId,
|
|
136
|
-
abandoned
|
|
137
|
-
});
|
|
138
|
-
executingCountRef.current++;
|
|
139
|
-
if (!abandoned) setToolStatuses((prev) => ({
|
|
140
|
-
...prev,
|
|
141
|
-
[logicalToolCallId]: { type: "executing" }
|
|
142
|
-
}));
|
|
143
|
-
},
|
|
144
|
-
onExecutionEnd: (streamId) => {
|
|
145
|
-
const info = executingRef.current.get(streamId);
|
|
146
|
-
if (!info) return;
|
|
147
|
-
executingRef.current.delete(streamId);
|
|
148
|
-
executingCountRef.current--;
|
|
149
|
-
if (!info.abandoned) setToolStatuses((prev) => {
|
|
150
|
-
const next = { ...prev };
|
|
151
|
-
delete next[info.logicalToolCallId];
|
|
152
|
-
return next;
|
|
153
|
-
});
|
|
154
|
-
if (executingCountRef.current === 0) resolveAllSettledResolvers();
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
stream.pipeThrough(transform).pipeThrough(new AssistantMetaTransformStream()).pipeTo(new WritableStream({ write(chunk) {
|
|
158
|
-
if (chunk.type !== "result") return;
|
|
159
|
-
const streamId = chunk.meta.toolCallId;
|
|
160
|
-
const logicalToolCallId = getLogicalToolCallId(streamId);
|
|
161
|
-
const entry = entriesRef.current.get(logicalToolCallId);
|
|
162
|
-
if (abandonedStreamIdsRef.current.delete(streamId)) {
|
|
163
|
-
streamToLogicalRef.current.delete(streamId);
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
if (!entry && skipExecuteStreamIdsRef.current.has(streamId)) return;
|
|
167
|
-
if (entry?.hasResult) return;
|
|
168
|
-
if (streamId !== logicalToolCallId) streamToLogicalRef.current.delete(streamId);
|
|
169
|
-
onResult({
|
|
170
|
-
type: "add-tool-result",
|
|
171
|
-
toolCallId: logicalToolCallId,
|
|
172
|
-
toolName: chunk.meta.toolName,
|
|
173
|
-
result: chunk.result,
|
|
174
|
-
isError: chunk.isError,
|
|
175
|
-
...chunk.artifact !== void 0 && { artifact: chunk.artifact },
|
|
176
|
-
...chunk.modelContent !== void 0 && { modelContent: chunk.modelContent }
|
|
177
|
-
});
|
|
178
|
-
} }));
|
|
179
|
-
return controller;
|
|
180
|
-
});
|
|
181
|
-
useEffect(() => {
|
|
182
|
-
const hasExecutableTool = (toolName) => {
|
|
183
|
-
const tool = getTools()?.[toolName];
|
|
184
|
-
return tool?.execute !== void 0 || tool?.streamCall !== void 0;
|
|
185
|
-
};
|
|
186
|
-
const shouldCloseArgsStream = ({ toolName, argsText, hasResult }) => {
|
|
187
|
-
if (hasResult) return true;
|
|
188
|
-
if (!hasExecutableTool(toolName)) return !state.isRunning && isArgsTextComplete(argsText);
|
|
189
|
-
return isArgsTextComplete(argsText);
|
|
190
|
-
};
|
|
191
|
-
const startActiveEntry = (toolCallId, toolName, skipExecute) => {
|
|
192
|
-
const toolCallController = controller.addToolCallPart({
|
|
193
|
-
toolName,
|
|
194
|
-
toolCallId
|
|
195
|
-
});
|
|
196
|
-
if (skipExecute) skipExecuteStreamIdsRef.current.add(toolCallId);
|
|
197
|
-
const entry = {
|
|
198
|
-
toolName,
|
|
199
|
-
controller: toolCallController,
|
|
200
|
-
streamId: toolCallId,
|
|
201
|
-
argsText: "",
|
|
202
|
-
hasResult: false,
|
|
203
|
-
argsComplete: false
|
|
204
|
-
};
|
|
205
|
-
entriesRef.current.set(toolCallId, entry);
|
|
206
|
-
return entry;
|
|
207
|
-
};
|
|
208
|
-
const restartArgsStream = (entry, toolCallId) => {
|
|
209
|
-
if (!entry.controller) return;
|
|
210
|
-
abandonedStreamIdsRef.current.add(entry.streamId);
|
|
211
|
-
const wasSkipExecute = skipExecuteStreamIdsRef.current.has(entry.streamId);
|
|
212
|
-
entry.controller.argsText.close();
|
|
213
|
-
const newStreamId = `${toolCallId}:rewrite:${rewriteCounterRef.current++}`;
|
|
214
|
-
streamToLogicalRef.current.set(newStreamId, toolCallId);
|
|
215
|
-
const newController = controller.addToolCallPart({
|
|
216
|
-
toolName: entry.toolName,
|
|
217
|
-
toolCallId: newStreamId
|
|
218
|
-
});
|
|
219
|
-
if (wasSkipExecute) skipExecuteStreamIdsRef.current.add(newStreamId);
|
|
220
|
-
if (process.env.NODE_ENV !== "production") console.warn("started replacement stream tool call", {
|
|
221
|
-
toolCallId,
|
|
222
|
-
streamToolCallId: newStreamId
|
|
223
|
-
});
|
|
224
|
-
entry.controller = newController;
|
|
225
|
-
entry.streamId = newStreamId;
|
|
226
|
-
entry.argsText = "";
|
|
227
|
-
entry.argsComplete = false;
|
|
228
|
-
};
|
|
229
|
-
const processArgsText = (entry, content) => {
|
|
230
|
-
if (!entry.controller) return;
|
|
231
|
-
const hasResult = content.result !== void 0;
|
|
232
|
-
if (content.argsText !== entry.argsText) {
|
|
233
|
-
let shouldWriteArgsText = true;
|
|
234
|
-
if (entry.argsComplete) if (isEquivalentCompleteArgsText(entry.argsText, content.argsText)) {
|
|
235
|
-
entry.argsText = content.argsText;
|
|
236
|
-
shouldWriteArgsText = false;
|
|
237
|
-
} else {
|
|
238
|
-
const canRestart = !entry.hasResult && !executingRef.current.has(entry.streamId);
|
|
239
|
-
if (process.env.NODE_ENV !== "production") console.warn(canRestart ? "argsText updated after controller was closed, restarting tool args stream:" : "argsText updated after controller was closed:", {
|
|
240
|
-
previous: entry.argsText,
|
|
241
|
-
next: content.argsText
|
|
242
|
-
});
|
|
243
|
-
if (!canRestart) {
|
|
244
|
-
entry.argsText = content.argsText;
|
|
245
|
-
shouldWriteArgsText = false;
|
|
246
|
-
} else restartArgsStream(entry, content.toolCallId);
|
|
247
|
-
}
|
|
248
|
-
else if (!content.argsText.startsWith(entry.argsText)) if (isArgsTextComplete(entry.argsText) && isArgsTextComplete(content.argsText) && isEquivalentCompleteArgsText(entry.argsText, content.argsText)) {
|
|
249
|
-
const shouldClose = shouldCloseArgsStream({
|
|
250
|
-
toolName: content.toolName,
|
|
251
|
-
argsText: content.argsText,
|
|
252
|
-
hasResult
|
|
253
|
-
});
|
|
254
|
-
if (shouldClose) entry.controller.argsText.close();
|
|
255
|
-
entry.argsText = content.argsText;
|
|
256
|
-
entry.argsComplete = shouldClose;
|
|
257
|
-
shouldWriteArgsText = false;
|
|
258
|
-
} else {
|
|
259
|
-
if (process.env.NODE_ENV !== "production") console.warn("argsText rewrote previous snapshot, restarting tool args stream:", {
|
|
260
|
-
previous: entry.argsText,
|
|
261
|
-
next: content.argsText,
|
|
262
|
-
toolCallId: content.toolCallId
|
|
263
|
-
});
|
|
264
|
-
restartArgsStream(entry, content.toolCallId);
|
|
265
|
-
}
|
|
266
|
-
if (shouldWriteArgsText) {
|
|
267
|
-
const delta = content.argsText.slice(entry.argsText.length);
|
|
268
|
-
entry.controller.argsText.append(delta);
|
|
269
|
-
const shouldClose = shouldCloseArgsStream({
|
|
270
|
-
toolName: content.toolName,
|
|
271
|
-
argsText: content.argsText,
|
|
272
|
-
hasResult
|
|
273
|
-
});
|
|
274
|
-
if (shouldClose) entry.controller.argsText.close();
|
|
275
|
-
entry.argsText = content.argsText;
|
|
276
|
-
entry.argsComplete = shouldClose;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
if (!entry.argsComplete) {
|
|
280
|
-
if (shouldCloseArgsStream({
|
|
281
|
-
toolName: content.toolName,
|
|
282
|
-
argsText: content.argsText,
|
|
283
|
-
hasResult
|
|
284
|
-
})) {
|
|
285
|
-
entry.controller.argsText.close();
|
|
286
|
-
entry.argsText = content.argsText;
|
|
287
|
-
entry.argsComplete = true;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
};
|
|
291
|
-
const processMessages = (messages) => {
|
|
292
|
-
const isRestore = pendingRestoreRef.current;
|
|
293
|
-
messages.forEach((message) => {
|
|
294
|
-
message.content.forEach((content) => {
|
|
295
|
-
if (content.type !== "tool-call") return;
|
|
296
|
-
const existing = entriesRef.current.get(content.toolCallId);
|
|
297
|
-
if (isRestore) {
|
|
298
|
-
if (!existing?.controller) entriesRef.current.set(content.toolCallId, {
|
|
299
|
-
toolName: content.toolName,
|
|
300
|
-
argsText: content.argsText,
|
|
301
|
-
hasResult: content.result !== void 0
|
|
302
|
-
});
|
|
303
|
-
if (content.messages) processMessages(content.messages);
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
let entry = existing;
|
|
307
|
-
if (entry && !entry.controller) {
|
|
308
|
-
if (!(content.argsText !== entry.argsText || content.result !== void 0 !== entry.hasResult)) {
|
|
309
|
-
if (content.messages) processMessages(content.messages);
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
entriesRef.current.delete(content.toolCallId);
|
|
313
|
-
entry = void 0;
|
|
314
|
-
}
|
|
315
|
-
if (!entry) entry = startActiveEntry(content.toolCallId, content.toolName, content.result !== void 0);
|
|
316
|
-
processArgsText(entry, content);
|
|
317
|
-
if (content.result !== void 0 && !entry.hasResult) {
|
|
318
|
-
const { controller: activeController } = entry;
|
|
319
|
-
if (!activeController) return;
|
|
320
|
-
entry.hasResult = true;
|
|
321
|
-
entry.argsComplete = true;
|
|
322
|
-
activeController.setResponse(new ToolResponse({
|
|
323
|
-
result: content.result,
|
|
324
|
-
artifact: content.artifact,
|
|
325
|
-
isError: content.isError,
|
|
326
|
-
...content.modelContent !== void 0 ? { modelContent: content.modelContent } : {}
|
|
327
|
-
}));
|
|
328
|
-
activeController.close();
|
|
329
|
-
}
|
|
330
|
-
if (content.messages) processMessages(content.messages);
|
|
331
|
-
});
|
|
332
|
-
});
|
|
333
|
-
};
|
|
334
|
-
processMessages(state.messages);
|
|
335
|
-
pendingRestoreRef.current = false;
|
|
336
|
-
}, [
|
|
337
|
-
state,
|
|
338
|
-
controller,
|
|
339
|
-
getTools
|
|
340
|
-
]);
|
|
341
|
-
const abort = () => {
|
|
342
|
-
humanInputRef.current.forEach(({ reject }) => {
|
|
343
|
-
reject(/* @__PURE__ */ new Error("Tool execution aborted"));
|
|
344
|
-
});
|
|
345
|
-
humanInputRef.current.clear();
|
|
346
|
-
acRef.current.abort();
|
|
347
|
-
acRef.current = new AbortController();
|
|
348
|
-
if (executingCountRef.current === 0) return Promise.resolve();
|
|
349
|
-
return new Promise((resolve) => {
|
|
350
|
-
settledResolversRef.current.push(resolve);
|
|
351
|
-
});
|
|
352
|
-
};
|
|
353
|
-
return {
|
|
354
|
-
reset: () => {
|
|
355
|
-
pendingRestoreRef.current = true;
|
|
356
|
-
entriesRef.current.clear();
|
|
357
|
-
abort().finally(() => {
|
|
358
|
-
executingRef.current.clear();
|
|
359
|
-
streamToLogicalRef.current.clear();
|
|
360
|
-
rewriteCounterRef.current = 0;
|
|
361
|
-
});
|
|
362
|
-
},
|
|
363
|
-
abort,
|
|
364
|
-
resume: (toolCallId, payload) => {
|
|
365
|
-
const handlers = humanInputRef.current.get(toolCallId);
|
|
366
|
-
if (handlers) {
|
|
367
|
-
humanInputRef.current.delete(toolCallId);
|
|
368
|
-
setToolStatuses((prev) => ({
|
|
369
|
-
...prev,
|
|
370
|
-
[toolCallId]: { type: "executing" }
|
|
371
|
-
}));
|
|
372
|
-
handlers.resolve(payload);
|
|
373
|
-
} else throw new Error(`Tool call ${toolCallId} is not waiting for human input`);
|
|
374
|
-
}
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
//#endregion
|
|
378
|
-
export { useToolInvocations };
|
|
379
|
-
|
|
380
|
-
//# sourceMappingURL=useToolInvocations.js.map
|