@assistant-ui/core 0.2.12 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/thread-history.d.ts +3 -1
- package/dist/adapters/thread-history.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js.map +1 -1
- package/dist/react/AssistantProvider.js +6 -1
- package/dist/react/AssistantProvider.js.map +1 -1
- package/dist/react/RuntimeAdapter.d.ts +1 -1
- package/dist/react/RuntimeAdapter.d.ts.map +1 -1
- package/dist/react/RuntimeAdapter.js +16 -6
- package/dist/react/RuntimeAdapter.js.map +1 -1
- package/dist/react/client/DataRenderers.d.ts +1 -8
- package/dist/react/client/DataRenderers.d.ts.map +1 -1
- package/dist/react/client/DataRenderers.js +3 -2
- package/dist/react/client/DataRenderers.js.map +1 -1
- package/dist/react/client/Interactables.d.ts +1 -1
- package/dist/react/client/Interactables.d.ts.map +1 -1
- package/dist/react/client/Interactables.js +4 -3
- package/dist/react/client/Interactables.js.map +1 -1
- package/dist/react/client/Tools.d.ts +2 -13
- package/dist/react/client/Tools.d.ts.map +1 -1
- package/dist/react/client/Tools.js +4 -3
- package/dist/react/client/Tools.js.map +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.d.ts +3 -2
- package/dist/react/primitives/message/MessageGroupedParts.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.js +4 -4
- package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
- package/dist/react/primitives/message/MessageParts.d.ts +28 -1
- package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageParts.js +43 -9
- package/dist/react/primitives/message/MessageParts.js.map +1 -1
- package/dist/react/providers/TextMessagePartProvider.d.ts.map +1 -1
- package/dist/react/providers/TextMessagePartProvider.js +3 -2
- package/dist/react/providers/TextMessagePartProvider.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +2 -0
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +2 -0
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +1 -0
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +6 -0
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +2 -0
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
- package/dist/react/utils/groupParts.d.ts +13 -1
- package/dist/react/utils/groupParts.d.ts.map +1 -1
- package/dist/react/utils/groupParts.js +17 -5
- package/dist/react/utils/groupParts.js.map +1 -1
- package/dist/runtime/api/bindings.d.ts +1 -0
- package/dist/runtime/api/bindings.d.ts.map +1 -1
- package/dist/runtime/api/message-runtime.d.ts +2 -0
- package/dist/runtime/api/message-runtime.d.ts.map +1 -1
- package/dist/runtime/api/message-runtime.js +5 -0
- package/dist/runtime/api/message-runtime.js.map +1 -1
- package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-list-runtime.js +1 -0
- package/dist/runtime/api/thread-list-runtime.js.map +1 -1
- package/dist/runtime/api/thread-runtime.d.ts +3 -0
- package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-runtime.js +4 -0
- package/dist/runtime/api/thread-runtime.js.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.d.ts +1 -0
- package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
- package/dist/runtime/branch/external-thread-branch-adapter.d.ts +30 -0
- package/dist/runtime/branch/external-thread-branch-adapter.d.ts.map +1 -0
- package/dist/runtime/branch/external-thread-branch-adapter.js +0 -0
- package/dist/runtime/interfaces/thread-list-runtime-core.d.ts +1 -0
- package/dist/runtime/interfaces/thread-list-runtime-core.d.ts.map +1 -1
- package/dist/runtime/interfaces/thread-runtime-core.d.ts +2 -0
- package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-adapter.d.ts +1 -0
- 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 +1 -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 +13 -0
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/local/local-runtime-options.d.ts +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.d.ts +8 -1
- package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.js +63 -5
- package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/local/should-continue.js +4 -2
- package/dist/runtimes/local/should-continue.js.map +1 -1
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts +2 -0
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js +4 -0
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js.map +1 -1
- package/dist/runtimes/remote-thread-list/empty-thread-core.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/empty-thread-core.js +4 -0
- package/dist/runtimes/remote-thread-list/empty-thread-core.js.map +1 -1
- package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts +1 -0
- package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/remote-thread-state.js +1 -0
- package/dist/runtimes/remote-thread-list/remote-thread-state.js.map +1 -1
- package/dist/runtimes/remote-thread-list/types.d.ts +1 -0
- package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
- package/dist/store/clients/chain-of-thought-client.d.ts +2 -7
- package/dist/store/clients/chain-of-thought-client.d.ts.map +1 -1
- package/dist/store/clients/chain-of-thought-client.js +3 -2
- package/dist/store/clients/chain-of-thought-client.js.map +1 -1
- package/dist/store/clients/model-context-client.d.ts +1 -1
- package/dist/store/clients/model-context-client.d.ts.map +1 -1
- package/dist/store/clients/model-context-client.js +3 -2
- package/dist/store/clients/model-context-client.js.map +1 -1
- package/dist/store/clients/no-op-composer-client.d.ts +2 -4
- package/dist/store/clients/no-op-composer-client.d.ts.map +1 -1
- package/dist/store/clients/no-op-composer-client.js +3 -2
- package/dist/store/clients/no-op-composer-client.js.map +1 -1
- package/dist/store/clients/runtime-adapter.d.ts +1 -3
- package/dist/store/clients/runtime-adapter.d.ts.map +1 -1
- package/dist/store/clients/runtime-adapter.js +2 -15
- package/dist/store/clients/runtime-adapter.js.map +1 -1
- package/dist/store/clients/suggestions.d.ts +1 -4
- package/dist/store/clients/suggestions.d.ts.map +1 -1
- package/dist/store/clients/suggestions.js +6 -4
- package/dist/store/clients/suggestions.js.map +1 -1
- package/dist/store/clients/thread-message-client.d.ts +1 -1
- package/dist/store/clients/thread-message-client.d.ts.map +1 -1
- package/dist/store/clients/thread-message-client.js +14 -10
- package/dist/store/clients/thread-message-client.js.map +1 -1
- package/dist/store/internal.d.ts +2 -2
- package/dist/store/internal.js +2 -2
- package/dist/store/runtime-clients/attachment-runtime-client.d.ts +2 -4
- package/dist/store/runtime-clients/attachment-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/attachment-runtime-client.js +3 -2
- package/dist/store/runtime-clients/attachment-runtime-client.js.map +1 -1
- package/dist/store/runtime-clients/composer-runtime-client.d.ts +2 -10
- package/dist/store/runtime-clients/composer-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/composer-runtime-client.js +9 -6
- package/dist/store/runtime-clients/composer-runtime-client.js.map +1 -1
- package/dist/store/runtime-clients/message-part-runtime-client.d.ts +2 -4
- package/dist/store/runtime-clients/message-part-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/message-part-runtime-client.js +3 -2
- package/dist/store/runtime-clients/message-part-runtime-client.js.map +1 -1
- package/dist/store/runtime-clients/message-runtime-client.d.ts +2 -7
- package/dist/store/runtime-clients/message-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/message-runtime-client.js +10 -6
- package/dist/store/runtime-clients/message-runtime-client.js.map +1 -1
- package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts +2 -4
- package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-list-item-runtime-client.js +3 -2
- package/dist/store/runtime-clients/thread-list-item-runtime-client.js.map +1 -1
- package/dist/store/runtime-clients/thread-list-runtime-client.d.ts +2 -5
- package/dist/store/runtime-clients/thread-list-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-list-runtime-client.js +6 -4
- package/dist/store/runtime-clients/thread-list-runtime-client.js.map +1 -1
- package/dist/store/runtime-clients/thread-runtime-client.d.ts +2 -4
- package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-runtime-client.js +7 -4
- package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
- package/dist/store/scopes/message.d.ts +1 -0
- package/dist/store/scopes/message.d.ts.map +1 -1
- package/dist/store/scopes/thread-list-item.d.ts +1 -0
- package/dist/store/scopes/thread-list-item.d.ts.map +1 -1
- package/dist/store/scopes/thread.d.ts +1 -0
- package/dist/store/scopes/thread.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/adapters/thread-history.ts +2 -0
- package/src/index.ts +1 -0
- package/src/react/AssistantProvider.tsx +3 -1
- package/src/react/RuntimeAdapter.ts +25 -8
- package/src/react/client/DataRenderers.ts +42 -45
- package/src/react/client/Interactables.ts +261 -261
- package/src/react/client/Tools.ts +6 -4
- package/src/react/primitives/message/MessageGroupedParts.tsx +19 -7
- package/src/react/primitives/message/MessageParts.tsx +64 -13
- package/src/react/providers/TextMessagePartProvider.tsx +5 -3
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +1 -0
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +11 -0
- package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +6 -0
- package/src/react/utils/groupParts.ts +27 -0
- package/src/runtime/api/bindings.ts +1 -0
- package/src/runtime/api/message-runtime.ts +7 -0
- package/src/runtime/api/thread-list-runtime.ts +1 -0
- package/src/runtime/api/thread-runtime.ts +7 -0
- package/src/runtime/base/base-thread-runtime-core.ts +1 -0
- package/src/runtime/branch/external-thread-branch-adapter.ts +26 -0
- package/src/runtime/interfaces/thread-list-runtime-core.ts +1 -0
- package/src/runtime/interfaces/thread-runtime-core.ts +2 -0
- package/src/runtimes/external-store/external-store-adapter.ts +1 -0
- package/src/runtimes/external-store/external-store-thread-runtime-core.ts +24 -0
- package/src/runtimes/local/local-runtime-options.ts +1 -1
- package/src/runtimes/local/local-thread-runtime-core.test.ts +311 -0
- package/src/runtimes/local/local-thread-runtime-core.ts +104 -7
- package/src/runtimes/local/should-continue.ts +23 -13
- package/src/runtimes/readonly/ReadonlyThreadRuntimeCore.ts +5 -0
- package/src/runtimes/remote-thread-list/empty-thread-core.ts +5 -0
- package/src/runtimes/remote-thread-list/remote-thread-state.ts +2 -0
- package/src/runtimes/remote-thread-list/types.ts +1 -0
- package/src/store/clients/chain-of-thought-client.ts +5 -3
- package/src/store/clients/model-context-client.test.ts +5 -4
- package/src/store/clients/model-context-client.ts +21 -21
- package/src/store/clients/no-op-composer-client.ts +5 -3
- package/src/store/clients/runtime-adapter.ts +0 -24
- package/src/store/clients/suggestions.ts +9 -18
- package/src/store/clients/thread-message-client.ts +29 -26
- package/src/store/internal.ts +1 -4
- package/src/store/runtime-clients/attachment-runtime-client.ts +14 -14
- package/src/store/runtime-clients/composer-runtime-client.ts +30 -24
- package/src/store/runtime-clients/message-part-runtime-client.ts +5 -3
- package/src/store/runtime-clients/message-runtime-client.ts +26 -19
- package/src/store/runtime-clients/thread-list-item-runtime-client.ts +5 -3
- package/src/store/runtime-clients/thread-list-runtime-client.ts +10 -6
- package/src/store/runtime-clients/thread-runtime-client.ts +11 -6
- package/src/store/scopes/message.ts +1 -0
- package/src/store/scopes/thread-list-item.ts +1 -0
- package/src/store/scopes/thread.ts +1 -0
- package/src/tests/external-store-thread-runtime-core.test.ts +57 -0
- package/src/tests/groupMessageParts.test.ts +84 -0
- package/src/tests/groupParts.test.ts +55 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { LocalRuntimeCore } from "./local-runtime-core";
|
|
3
|
+
import type {
|
|
4
|
+
ChatModelAdapter,
|
|
5
|
+
ChatModelRunOptions,
|
|
6
|
+
ChatModelRunResult,
|
|
7
|
+
} from "../../runtime/utils/chat-model-adapter";
|
|
8
|
+
import type { AppendMessage } from "../../types/message";
|
|
9
|
+
|
|
10
|
+
const flush = () => new Promise((resolve) => setTimeout(resolve, 10));
|
|
11
|
+
|
|
12
|
+
const createThread = (adapter: ChatModelAdapter) => {
|
|
13
|
+
const core = new LocalRuntimeCore(
|
|
14
|
+
{
|
|
15
|
+
adapters: { chatModel: adapter },
|
|
16
|
+
unstable_humanToolNames: ["send_email"],
|
|
17
|
+
},
|
|
18
|
+
undefined,
|
|
19
|
+
);
|
|
20
|
+
return core.threads.getMainThreadRuntimeCore();
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const userMessage = (text: string): AppendMessage => ({
|
|
24
|
+
parentId: null,
|
|
25
|
+
sourceId: null,
|
|
26
|
+
runConfig: {},
|
|
27
|
+
role: "user",
|
|
28
|
+
content: [{ type: "text", text }],
|
|
29
|
+
attachments: [],
|
|
30
|
+
metadata: { custom: {} },
|
|
31
|
+
createdAt: new Date(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const toolCallPart = (toolName: string, approval?: { id: string }) => ({
|
|
35
|
+
type: "tool-call" as const,
|
|
36
|
+
toolCallId: `call-${toolName}`,
|
|
37
|
+
toolName,
|
|
38
|
+
args: {},
|
|
39
|
+
argsText: "{}",
|
|
40
|
+
...(approval !== undefined ? { approval } : {}),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const toolCallResult = (
|
|
44
|
+
toolName: string,
|
|
45
|
+
approval?: { id: string },
|
|
46
|
+
): ChatModelRunResult => ({
|
|
47
|
+
content: [toolCallPart(toolName, approval)],
|
|
48
|
+
status: { type: "requires-action", reason: "tool-calls" },
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const createApprovalThread = (firstResult: ChatModelRunResult) => {
|
|
52
|
+
const runs: ChatModelRunOptions[] = [];
|
|
53
|
+
const thread = createThread({
|
|
54
|
+
async run(options) {
|
|
55
|
+
runs.push(options);
|
|
56
|
+
if (runs.length === 1) return firstResult;
|
|
57
|
+
return { content: [{ type: "text", text: "done" }] };
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
return { thread, runs };
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
describe("LocalThreadRuntimeCore human-in-the-loop tools", () => {
|
|
64
|
+
it("pauses on requires-action while a listed tool call has no result", async () => {
|
|
65
|
+
const { thread, runs } = createApprovalThread(toolCallResult("send_email"));
|
|
66
|
+
|
|
67
|
+
await thread.append(userMessage("send an email"));
|
|
68
|
+
await flush();
|
|
69
|
+
|
|
70
|
+
expect(runs).toHaveLength(1);
|
|
71
|
+
expect(thread.messages.at(-1)?.status?.type).toBe("requires-action");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("does not hold the run for unlisted tool calls", async () => {
|
|
75
|
+
const { thread, runs } = createApprovalThread(
|
|
76
|
+
toolCallResult("lookup_weather"),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
await thread.append(userMessage("what is the weather"));
|
|
80
|
+
await flush();
|
|
81
|
+
|
|
82
|
+
expect(runs).toHaveLength(2);
|
|
83
|
+
expect(thread.messages.at(-1)?.status?.type).toBe("complete");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("resumes via addToolResult and exposes the result to the adapter", async () => {
|
|
87
|
+
const { thread, runs } = createApprovalThread(toolCallResult("send_email"));
|
|
88
|
+
|
|
89
|
+
await thread.append(userMessage("send an email"));
|
|
90
|
+
await flush();
|
|
91
|
+
|
|
92
|
+
const assistantMessage = thread.messages.at(-1)!;
|
|
93
|
+
thread.addToolResult({
|
|
94
|
+
messageId: assistantMessage.id,
|
|
95
|
+
toolCallId: "call-send_email",
|
|
96
|
+
toolName: "send_email",
|
|
97
|
+
result: { approved: true },
|
|
98
|
+
isError: false,
|
|
99
|
+
});
|
|
100
|
+
await flush();
|
|
101
|
+
|
|
102
|
+
expect(runs).toHaveLength(2);
|
|
103
|
+
const resumed = runs[1]!;
|
|
104
|
+
expect(resumed.messages.at(-1)?.role).toBe("user");
|
|
105
|
+
const toolCall = resumed
|
|
106
|
+
.unstable_getMessage()
|
|
107
|
+
.content.find((part) => part.type === "tool-call");
|
|
108
|
+
expect(toolCall?.result).toEqual({ approved: true });
|
|
109
|
+
|
|
110
|
+
const finalMessage = thread.messages.at(-1)!;
|
|
111
|
+
expect(finalMessage.status?.type).toBe("complete");
|
|
112
|
+
expect(finalMessage.content.map((part) => part.type)).toEqual([
|
|
113
|
+
"tool-call",
|
|
114
|
+
"text",
|
|
115
|
+
]);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("LocalThreadRuntimeCore tool approvals", () => {
|
|
120
|
+
it("pauses the run while an approval is pending, even for unlisted tools", async () => {
|
|
121
|
+
const { thread, runs } = createApprovalThread(
|
|
122
|
+
toolCallResult("deploy", { id: "a1" }),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
await thread.append(userMessage("deploy the app"));
|
|
126
|
+
await flush();
|
|
127
|
+
|
|
128
|
+
expect(runs).toHaveLength(1);
|
|
129
|
+
expect(thread.messages.at(-1)?.status?.type).toBe("requires-action");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("records an approval and resumes, exempting the gated tool from the human tool result requirement", async () => {
|
|
133
|
+
const { thread, runs } = createApprovalThread(
|
|
134
|
+
toolCallResult("send_email", { id: "a1" }),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
await thread.append(userMessage("send an email"));
|
|
138
|
+
await flush();
|
|
139
|
+
|
|
140
|
+
thread.respondToToolApproval({ approvalId: "a1", approved: true });
|
|
141
|
+
await flush();
|
|
142
|
+
|
|
143
|
+
expect(runs).toHaveLength(2);
|
|
144
|
+
const toolCall = runs[1]!
|
|
145
|
+
.unstable_getMessage()
|
|
146
|
+
.content.find((part) => part.type === "tool-call");
|
|
147
|
+
expect(toolCall?.approval).toEqual({ id: "a1", approved: true });
|
|
148
|
+
expect(toolCall?.result).toBeUndefined();
|
|
149
|
+
expect(thread.messages.at(-1)?.status?.type).toBe("complete");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("continues multi-step turns after an approval resume", async () => {
|
|
153
|
+
const runs: ChatModelRunOptions[] = [];
|
|
154
|
+
const thread = createThread({
|
|
155
|
+
async run(options) {
|
|
156
|
+
runs.push(options);
|
|
157
|
+
if (runs.length === 1) return toolCallResult("deploy", { id: "a1" });
|
|
158
|
+
if (runs.length === 2) return toolCallResult("lookup_weather");
|
|
159
|
+
return { content: [{ type: "text", text: "done" }] };
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await thread.append(userMessage("deploy the app"));
|
|
164
|
+
await flush();
|
|
165
|
+
|
|
166
|
+
thread.respondToToolApproval({ approvalId: "a1", approved: true });
|
|
167
|
+
await flush();
|
|
168
|
+
|
|
169
|
+
expect(runs).toHaveLength(3);
|
|
170
|
+
expect(thread.messages.at(-1)?.status?.type).toBe("complete");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("resumes when a result is added to an approval-gated tool call", async () => {
|
|
174
|
+
const { thread, runs } = createApprovalThread(
|
|
175
|
+
toolCallResult("deploy", { id: "a1" }),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
await thread.append(userMessage("deploy the app"));
|
|
179
|
+
await flush();
|
|
180
|
+
|
|
181
|
+
thread.addToolResult({
|
|
182
|
+
messageId: thread.messages.at(-1)!.id,
|
|
183
|
+
toolCallId: "call-deploy",
|
|
184
|
+
toolName: "deploy",
|
|
185
|
+
result: "done manually",
|
|
186
|
+
isError: false,
|
|
187
|
+
});
|
|
188
|
+
await flush();
|
|
189
|
+
|
|
190
|
+
expect(runs).toHaveLength(2);
|
|
191
|
+
expect(thread.messages.at(-1)?.status?.type).toBe("complete");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("records a denial and synthesizes an error result", async () => {
|
|
195
|
+
const { thread, runs } = createApprovalThread(
|
|
196
|
+
toolCallResult("deploy", { id: "a1" }),
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
await thread.append(userMessage("deploy the app"));
|
|
200
|
+
await flush();
|
|
201
|
+
|
|
202
|
+
thread.respondToToolApproval({
|
|
203
|
+
approvalId: "a1",
|
|
204
|
+
approved: false,
|
|
205
|
+
reason: "not today",
|
|
206
|
+
});
|
|
207
|
+
await flush();
|
|
208
|
+
|
|
209
|
+
expect(runs).toHaveLength(2);
|
|
210
|
+
const toolCall = thread.messages
|
|
211
|
+
.at(-1)!
|
|
212
|
+
.content.find((part) => part.type === "tool-call");
|
|
213
|
+
expect(toolCall?.approval).toEqual({
|
|
214
|
+
id: "a1",
|
|
215
|
+
approved: false,
|
|
216
|
+
reason: "not today",
|
|
217
|
+
});
|
|
218
|
+
expect(toolCall?.result).toEqual({ error: "not today" });
|
|
219
|
+
expect(toolCall?.isError).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("synthesizes a default denial reason", async () => {
|
|
223
|
+
const { thread } = createApprovalThread(
|
|
224
|
+
toolCallResult("deploy", { id: "a1" }),
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
await thread.append(userMessage("deploy the app"));
|
|
228
|
+
await flush();
|
|
229
|
+
|
|
230
|
+
thread.respondToToolApproval({ approvalId: "a1", approved: false });
|
|
231
|
+
await flush();
|
|
232
|
+
|
|
233
|
+
const toolCall = thread.messages
|
|
234
|
+
.at(-1)!
|
|
235
|
+
.content.find((part) => part.type === "tool-call");
|
|
236
|
+
expect(toolCall?.result).toEqual({ error: "Tool approval denied" });
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("waits until every pending approval is decided before resuming", async () => {
|
|
240
|
+
const { thread, runs } = createApprovalThread({
|
|
241
|
+
content: [
|
|
242
|
+
toolCallPart("deploy", { id: "a1" }),
|
|
243
|
+
toolCallPart("send_invoice", { id: "a2" }),
|
|
244
|
+
],
|
|
245
|
+
status: { type: "requires-action", reason: "tool-calls" },
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
await thread.append(userMessage("deploy and bill"));
|
|
249
|
+
await flush();
|
|
250
|
+
|
|
251
|
+
thread.respondToToolApproval({ approvalId: "a1", approved: true });
|
|
252
|
+
await flush();
|
|
253
|
+
expect(runs).toHaveLength(1);
|
|
254
|
+
|
|
255
|
+
expect(() =>
|
|
256
|
+
thread.respondToToolApproval({ approvalId: "a1", approved: false }),
|
|
257
|
+
).toThrowError(/already decided/);
|
|
258
|
+
|
|
259
|
+
thread.respondToToolApproval({ approvalId: "a2", approved: false });
|
|
260
|
+
await flush();
|
|
261
|
+
expect(runs).toHaveLength(2);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("throws while the run is still in flight, even if the message already reads requires-action", async () => {
|
|
265
|
+
let release!: () => void;
|
|
266
|
+
const gate = new Promise<void>((resolve) => (release = resolve));
|
|
267
|
+
const runs: ChatModelRunOptions[] = [];
|
|
268
|
+
const thread = createThread({
|
|
269
|
+
async *run(options) {
|
|
270
|
+
runs.push(options);
|
|
271
|
+
if (runs.length === 1) {
|
|
272
|
+
yield toolCallResult("deploy", { id: "a1" });
|
|
273
|
+
await gate;
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
yield { content: [{ type: "text", text: "done" }] };
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const appendPromise = thread.append(userMessage("deploy the app"));
|
|
281
|
+
await flush();
|
|
282
|
+
|
|
283
|
+
expect(thread.messages.at(-1)?.status?.type).toBe("requires-action");
|
|
284
|
+
expect(() =>
|
|
285
|
+
thread.respondToToolApproval({ approvalId: "a1", approved: true }),
|
|
286
|
+
).toThrowError(/run is in progress/);
|
|
287
|
+
|
|
288
|
+
release();
|
|
289
|
+
await appendPromise;
|
|
290
|
+
|
|
291
|
+
thread.respondToToolApproval({ approvalId: "a1", approved: true });
|
|
292
|
+
await flush();
|
|
293
|
+
|
|
294
|
+
expect(runs).toHaveLength(2);
|
|
295
|
+
expect(thread.messages.at(-1)?.status?.type).toBe("complete");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("throws for unknown approvals and unsupported tool call resumption", async () => {
|
|
299
|
+
const { thread } = createApprovalThread(toolCallResult("send_email"));
|
|
300
|
+
|
|
301
|
+
await thread.append(userMessage("send an email"));
|
|
302
|
+
await flush();
|
|
303
|
+
|
|
304
|
+
expect(() =>
|
|
305
|
+
thread.respondToToolApproval({ approvalId: "nope", approved: true }),
|
|
306
|
+
).toThrowError(/non-existing tool approval/);
|
|
307
|
+
expect(() =>
|
|
308
|
+
thread.resumeToolCall({ toolCallId: "call-send_email", payload: {} }),
|
|
309
|
+
).toThrowError(/unstable_humanToolNames/);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
@@ -49,6 +49,7 @@ export class LocalThreadRuntimeCore
|
|
|
49
49
|
switchToBranch: true,
|
|
50
50
|
switchBranchDuringRun: true,
|
|
51
51
|
edit: true,
|
|
52
|
+
delete: false,
|
|
52
53
|
reload: true,
|
|
53
54
|
cancel: true,
|
|
54
55
|
unstable_copy: true,
|
|
@@ -150,6 +151,12 @@ export class LocalThreadRuntimeCore
|
|
|
150
151
|
hasUpdates = true;
|
|
151
152
|
}
|
|
152
153
|
|
|
154
|
+
const canDelete = options.adapters?.history?.delete !== undefined;
|
|
155
|
+
if (this.capabilities.delete !== canDelete) {
|
|
156
|
+
this.capabilities.delete = canDelete;
|
|
157
|
+
hasUpdates = true;
|
|
158
|
+
}
|
|
159
|
+
|
|
153
160
|
const canQueue = options.unstable_enableMessageQueue === true;
|
|
154
161
|
if (canQueue && !this._queue) {
|
|
155
162
|
this._queue = createMessageQueue({
|
|
@@ -275,6 +282,25 @@ export class LocalThreadRuntimeCore
|
|
|
275
282
|
}
|
|
276
283
|
}
|
|
277
284
|
|
|
285
|
+
public async deleteMessage(messageId: string): Promise<void> {
|
|
286
|
+
const adapter = this._options.adapters.history;
|
|
287
|
+
if (!adapter?.delete)
|
|
288
|
+
throw new Error("Runtime does not support deleting messages.");
|
|
289
|
+
|
|
290
|
+
const messages = this.repository.getMessages();
|
|
291
|
+
const messageIndex = messages.findIndex((m) => m.id === messageId);
|
|
292
|
+
if (messageIndex === -1) throw new Error("Message not found.");
|
|
293
|
+
|
|
294
|
+
const message = messages[messageIndex]!;
|
|
295
|
+
const parentId = messages[messageIndex - 1]?.id ?? null;
|
|
296
|
+
const items = [{ parentId, message }];
|
|
297
|
+
|
|
298
|
+
await adapter.delete(items);
|
|
299
|
+
|
|
300
|
+
this.repository.deleteMessage(messageId);
|
|
301
|
+
this._notifySubscribers();
|
|
302
|
+
}
|
|
303
|
+
|
|
278
304
|
public resumeRun({ stream, ...startConfig }: ResumeRunConfig): Promise<void> {
|
|
279
305
|
if (!stream)
|
|
280
306
|
throw new Error("You must pass a stream parameter to resume runs.");
|
|
@@ -297,7 +323,7 @@ export class LocalThreadRuntimeCore
|
|
|
297
323
|
|
|
298
324
|
// add assistant message
|
|
299
325
|
const id = generateId();
|
|
300
|
-
|
|
326
|
+
const message: ThreadAssistantMessage = {
|
|
301
327
|
id,
|
|
302
328
|
role: "assistant",
|
|
303
329
|
status: { type: "running" },
|
|
@@ -312,6 +338,15 @@ export class LocalThreadRuntimeCore
|
|
|
312
338
|
createdAt: new Date(),
|
|
313
339
|
};
|
|
314
340
|
|
|
341
|
+
return this._runLoop(parentId, message, runConfig, runCallback);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private async _runLoop(
|
|
345
|
+
parentId: string | null,
|
|
346
|
+
message: ThreadAssistantMessage,
|
|
347
|
+
runConfig: RunConfig | undefined,
|
|
348
|
+
runCallback?: ChatModelAdapter["run"],
|
|
349
|
+
): Promise<void> {
|
|
315
350
|
this._notifyEventSubscribers("runStart", {});
|
|
316
351
|
|
|
317
352
|
try {
|
|
@@ -592,22 +627,84 @@ export class LocalThreadRuntimeCore
|
|
|
592
627
|
content: newContent,
|
|
593
628
|
};
|
|
594
629
|
this.repository.addOrUpdateMessage(parentId, message);
|
|
630
|
+
this._notifySubscribers();
|
|
595
631
|
|
|
632
|
+
// a result may arrive mid-run or on a non-head message; the resume
|
|
633
|
+
// intentionally aborts any in-flight run, unlike respondToToolApproval
|
|
596
634
|
if (
|
|
597
635
|
added &&
|
|
598
636
|
shouldContinue(message, this._options.unstable_humanToolNames)
|
|
599
637
|
) {
|
|
600
|
-
this.
|
|
601
|
-
() => {},
|
|
602
|
-
);
|
|
638
|
+
this._runLoop(parentId, message, this._lastRunConfig).catch(() => {});
|
|
603
639
|
}
|
|
604
640
|
}
|
|
605
641
|
|
|
606
642
|
public resumeToolCall(_options: ResumeToolCallOptions) {
|
|
607
|
-
throw new Error(
|
|
643
|
+
throw new Error(
|
|
644
|
+
"Local runtime does not support resuming tool calls. For human-in-the-loop tools, list the tool in unstable_humanToolNames and complete the call with addToolResult.",
|
|
645
|
+
);
|
|
608
646
|
}
|
|
609
647
|
|
|
610
|
-
public respondToToolApproval(
|
|
611
|
-
|
|
648
|
+
public respondToToolApproval({
|
|
649
|
+
approvalId,
|
|
650
|
+
approved,
|
|
651
|
+
reason,
|
|
652
|
+
}: RespondToToolApprovalOptions) {
|
|
653
|
+
let message = this.repository
|
|
654
|
+
.getMessages()
|
|
655
|
+
.findLast(
|
|
656
|
+
(m): m is ThreadAssistantMessage =>
|
|
657
|
+
m.role === "assistant" &&
|
|
658
|
+
m.content.some(
|
|
659
|
+
(c) => c.type === "tool-call" && c.approval?.id === approvalId,
|
|
660
|
+
),
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
if (!message)
|
|
664
|
+
throw new Error("Tried to respond to a non-existing tool approval");
|
|
665
|
+
|
|
666
|
+
if (this.abortController !== null)
|
|
667
|
+
throw new Error(
|
|
668
|
+
"Tried to respond to a tool approval while a run is in progress",
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
if (message.status?.type !== "requires-action")
|
|
672
|
+
throw new Error(
|
|
673
|
+
"Tried to respond to a tool approval on a message whose status is not requires-action",
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
let recorded = false;
|
|
677
|
+
const newContent = message.content.map((c) => {
|
|
678
|
+
if (c.type !== "tool-call" || c.approval?.id !== approvalId) return c;
|
|
679
|
+
if (c.approval.approved !== undefined) return c;
|
|
680
|
+
recorded = true;
|
|
681
|
+
const approval = {
|
|
682
|
+
...c.approval,
|
|
683
|
+
approved,
|
|
684
|
+
...(reason != null && { reason }),
|
|
685
|
+
};
|
|
686
|
+
if (approved) return { ...c, approval };
|
|
687
|
+
return {
|
|
688
|
+
...c,
|
|
689
|
+
approval,
|
|
690
|
+
result: { error: reason || "Tool approval denied" },
|
|
691
|
+
isError: true,
|
|
692
|
+
};
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
if (!recorded)
|
|
696
|
+
throw new Error("Tried to respond to an already decided tool approval");
|
|
697
|
+
|
|
698
|
+
message = { ...message, content: newContent };
|
|
699
|
+
const { parentId } = this.repository.getMessage(message.id);
|
|
700
|
+
this.repository.addOrUpdateMessage(parentId, message);
|
|
701
|
+
this._notifySubscribers();
|
|
702
|
+
|
|
703
|
+
if (
|
|
704
|
+
this.repository.headId === message.id &&
|
|
705
|
+
shouldContinue(message, this._options.unstable_humanToolNames)
|
|
706
|
+
) {
|
|
707
|
+
this._runLoop(parentId, message, this._lastRunConfig).catch(() => {});
|
|
708
|
+
}
|
|
612
709
|
}
|
|
613
710
|
}
|
|
@@ -4,23 +4,33 @@ export const shouldContinue = (
|
|
|
4
4
|
result: ThreadAssistantMessage,
|
|
5
5
|
humanToolNames: string[] | undefined,
|
|
6
6
|
) => {
|
|
7
|
+
if (
|
|
8
|
+
result.status?.type !== "requires-action" ||
|
|
9
|
+
result.status.reason !== "tool-calls"
|
|
10
|
+
)
|
|
11
|
+
return false;
|
|
12
|
+
|
|
13
|
+
const hasPendingApproval = result.content.some(
|
|
14
|
+
(c) =>
|
|
15
|
+
c.type === "tool-call" &&
|
|
16
|
+
c.result === undefined &&
|
|
17
|
+
c.approval !== undefined &&
|
|
18
|
+
c.approval.approved === undefined,
|
|
19
|
+
);
|
|
20
|
+
if (hasPendingApproval) return false;
|
|
21
|
+
|
|
7
22
|
// TODO legacy behavior -- make specifying human tool names required
|
|
8
23
|
if (humanToolNames === undefined) {
|
|
9
|
-
return (
|
|
10
|
-
|
|
11
|
-
result.status.reason === "tool-calls" &&
|
|
12
|
-
result.content.every((c) => c.type !== "tool-call" || !!c.result)
|
|
24
|
+
return result.content.every(
|
|
25
|
+
(c) => c.type !== "tool-call" || !!c.result || c.approval !== undefined,
|
|
13
26
|
);
|
|
14
27
|
}
|
|
15
28
|
|
|
16
|
-
return (
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
!!c.result ||
|
|
23
|
-
!humanToolNames.includes(c.toolName),
|
|
24
|
-
)
|
|
29
|
+
return result.content.every(
|
|
30
|
+
(c) =>
|
|
31
|
+
c.type !== "tool-call" ||
|
|
32
|
+
!!c.result ||
|
|
33
|
+
c.approval !== undefined ||
|
|
34
|
+
!humanToolNames.includes(c.toolName),
|
|
25
35
|
);
|
|
26
36
|
};
|
|
@@ -47,6 +47,10 @@ export class ReadonlyThreadRuntimeCore
|
|
|
47
47
|
throw READONLY_THREAD_ERROR;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
deleteMessage(): void {
|
|
51
|
+
throw READONLY_THREAD_ERROR;
|
|
52
|
+
}
|
|
53
|
+
|
|
50
54
|
startRun(): void {
|
|
51
55
|
throw READONLY_THREAD_ERROR;
|
|
52
56
|
}
|
|
@@ -202,6 +206,7 @@ export class ReadonlyThreadRuntimeCore
|
|
|
202
206
|
switchToBranch: false,
|
|
203
207
|
switchBranchDuringRun: false,
|
|
204
208
|
edit: false,
|
|
209
|
+
delete: false,
|
|
205
210
|
reload: false,
|
|
206
211
|
cancel: false,
|
|
207
212
|
unstable_copy: false,
|
|
@@ -20,6 +20,10 @@ export const EMPTY_THREAD_CORE: ThreadRuntimeCore = {
|
|
|
20
20
|
throw EMPTY_THREAD_ERROR;
|
|
21
21
|
},
|
|
22
22
|
|
|
23
|
+
deleteMessage() {
|
|
24
|
+
throw EMPTY_THREAD_ERROR;
|
|
25
|
+
},
|
|
26
|
+
|
|
23
27
|
startRun() {
|
|
24
28
|
throw EMPTY_THREAD_ERROR;
|
|
25
29
|
},
|
|
@@ -183,6 +187,7 @@ export const EMPTY_THREAD_CORE: ThreadRuntimeCore = {
|
|
|
183
187
|
switchToBranch: false,
|
|
184
188
|
switchBranchDuringRun: false,
|
|
185
189
|
edit: false,
|
|
190
|
+
delete: false,
|
|
186
191
|
reload: false,
|
|
187
192
|
cancel: false,
|
|
188
193
|
unstable_copy: false,
|
|
@@ -28,6 +28,7 @@ export type RemoteThreadData =
|
|
|
28
28
|
readonly externalId: string | undefined;
|
|
29
29
|
readonly status: "regular" | "archived";
|
|
30
30
|
readonly title?: string | undefined;
|
|
31
|
+
readonly lastMessageAt?: Date | undefined;
|
|
31
32
|
readonly custom?: Record<string, unknown> | undefined;
|
|
32
33
|
};
|
|
33
34
|
|
|
@@ -75,6 +76,7 @@ export const classifyThreads = (
|
|
|
75
76
|
externalId: thread.externalId,
|
|
76
77
|
status: thread.status,
|
|
77
78
|
title: thread.title,
|
|
79
|
+
lastMessageAt: thread.lastMessageAt,
|
|
78
80
|
custom: thread.custom,
|
|
79
81
|
initializeTask: Promise.resolve({
|
|
80
82
|
remoteId: thread.remoteId,
|
|
@@ -13,6 +13,7 @@ export type RemoteThreadMetadata = {
|
|
|
13
13
|
readonly remoteId: string;
|
|
14
14
|
readonly externalId?: string | undefined;
|
|
15
15
|
readonly title?: string | undefined;
|
|
16
|
+
readonly lastMessageAt?: Date | undefined;
|
|
16
17
|
readonly custom?: Record<string, unknown> | undefined;
|
|
17
18
|
};
|
|
18
19
|
|
|
@@ -12,13 +12,13 @@ const COMPLETE_STATUS: MessagePartStatus = Object.freeze({
|
|
|
12
12
|
type: "complete",
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
const useChainOfThoughtClient = ({
|
|
16
16
|
parts,
|
|
17
17
|
getMessagePart,
|
|
18
18
|
}: {
|
|
19
19
|
parts: readonly ChainOfThoughtPart[];
|
|
20
20
|
getMessagePart: (selector: { index: number }) => PartMethods;
|
|
21
|
-
}): ClientOutput<"chainOfThought"> {
|
|
21
|
+
}): ClientOutput<"chainOfThought"> => {
|
|
22
22
|
const [collapsed, setCollapsed] = useState(true);
|
|
23
23
|
|
|
24
24
|
const status = useMemo(() => {
|
|
@@ -36,4 +36,6 @@ export const ChainOfThoughtClient = resource(function ChainOfThoughtClient({
|
|
|
36
36
|
setCollapsed,
|
|
37
37
|
part: getMessagePart,
|
|
38
38
|
};
|
|
39
|
-
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const ChainOfThoughtClient = resource(useChainOfThoughtClient);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import { createTapRoot, useResource } from "@assistant-ui/tap";
|
|
3
3
|
import type { Tool } from "assistant-stream";
|
|
4
4
|
import { ModelContext } from "./model-context-client";
|
|
5
5
|
import type {
|
|
@@ -18,9 +18,10 @@ const toolFixture = (): Tool<any, any> =>
|
|
|
18
18
|
({ description: "", parameters: {} as any }) as unknown as Tool<any, any>;
|
|
19
19
|
|
|
20
20
|
const render = () => {
|
|
21
|
-
const root =
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
const root = createTapRoot(function Root() {
|
|
22
|
+
return useResource(ModelContext());
|
|
23
|
+
});
|
|
24
|
+
return { sub: root, unmount: () => root.unmount() };
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
describe("ModelContext", () => {
|
|
@@ -29,25 +29,25 @@ const deriveState = (
|
|
|
29
29
|
return { modelName, toolNames };
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
const useModelContext = (): ClientOutput<"modelContext"> => {
|
|
33
|
+
const composite = useMemo(() => new CompositeContextProvider(), []);
|
|
34
|
+
const [state, setState] = useState<ModelContextState>(() =>
|
|
35
|
+
deriveState(composite, INITIAL_STATE),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
setState((prev) => deriveState(composite, prev));
|
|
40
|
+
return composite.subscribe(() => {
|
|
40
41
|
setState((prev) => deriveState(composite, prev));
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
);
|
|
42
|
+
});
|
|
43
|
+
}, [composite]);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
getState: () => deriveState(composite, state),
|
|
47
|
+
getModelContext: () => composite.getModelContext(),
|
|
48
|
+
subscribe: (callback) => composite.subscribe(callback),
|
|
49
|
+
register: (provider) => composite.registerModelContextProvider(provider),
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const ModelContext = resource(useModelContext);
|