basalt-ui 1.11.0 → 1.12.0
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/AGENTS.md +21 -21
- package/CHANGELOG.md +15 -0
- package/README.md +36 -27
- package/agent/rules/basalt-content.md +16 -3
- package/dist/agent/adapter.d.ts +146 -0
- package/dist/agent/adapter.d.ts.map +1 -0
- package/dist/agent/adapter.js +473 -0
- package/dist/agent/adapter.test.d.ts +2 -0
- package/dist/agent/adapter.test.d.ts.map +1 -0
- package/dist/agent/ai-sdk-transport.test.d.ts +2 -0
- package/dist/agent/ai-sdk-transport.test.d.ts.map +1 -0
- package/dist/agent/coalesce.d.ts +20 -4
- package/dist/agent/coalesce.d.ts.map +1 -1
- package/dist/agent/coalesce.js +76 -8
- package/dist/agent/coalesce.test.d.ts +2 -0
- package/dist/agent/coalesce.test.d.ts.map +1 -0
- package/dist/agent/foreign.test.d.ts +2 -0
- package/dist/agent/foreign.test.d.ts.map +1 -0
- package/dist/agent/id.d.ts +30 -0
- package/dist/agent/id.d.ts.map +1 -1
- package/dist/agent/id.js +16 -0
- package/dist/agent/id.test.d.ts +2 -0
- package/dist/agent/id.test.d.ts.map +1 -0
- package/dist/agent/index.d.ts +2 -0
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +3 -0
- package/dist/agent/merge.d.ts.map +1 -1
- package/dist/agent/merge.js +19 -7
- package/dist/agent/merge.test.d.ts +2 -0
- package/dist/agent/merge.test.d.ts.map +1 -0
- package/dist/agent/part-list.d.ts +49 -34
- package/dist/agent/part-list.d.ts.map +1 -1
- package/dist/agent/part-list.js +42 -6
- package/dist/agent/part-list.test.d.ts +2 -0
- package/dist/agent/part-list.test.d.ts.map +1 -0
- package/dist/agent/parts.d.ts +46 -10
- package/dist/agent/parts.d.ts.map +1 -1
- package/dist/agent/parts.js +16 -6
- package/dist/agent/parts.test.d.ts +2 -0
- package/dist/agent/parts.test.d.ts.map +1 -0
- package/dist/agent/thread.d.ts +14 -0
- package/dist/agent/thread.d.ts.map +1 -1
- package/dist/agent/thread.js +8 -2
- package/dist/agent/thread.test.d.ts +2 -0
- package/dist/agent/thread.test.d.ts.map +1 -0
- package/dist/agent/transport.test.d.ts +2 -0
- package/dist/agent/transport.test.d.ts.map +1 -0
- package/dist/agent/use-agent-stream.test.d.ts +2 -0
- package/dist/agent/use-agent-stream.test.d.ts.map +1 -0
- package/dist/agent/use-agent-thread-runs.foreign.test.d.ts +2 -0
- package/dist/agent/use-agent-thread-runs.foreign.test.d.ts.map +1 -0
- package/dist/agent/use-agent-thread-runs.resume.test.d.ts +2 -0
- package/dist/agent/use-agent-thread-runs.resume.test.d.ts.map +1 -0
- package/dist/agent/use-agent-thread-runs.test.d.ts +2 -0
- package/dist/agent/use-agent-thread-runs.test.d.ts.map +1 -0
- package/dist/agent/use-agent-thread-runs.wedge.test.d.ts +2 -0
- package/dist/agent/use-agent-thread-runs.wedge.test.d.ts.map +1 -0
- package/dist/agent-chat/composer.d.ts +130 -8
- package/dist/agent-chat/composer.d.ts.map +1 -1
- package/dist/agent-chat/composer.js +189 -23
- package/dist/agent-chat/composer.test.d.ts +2 -0
- package/dist/agent-chat/composer.test.d.ts.map +1 -0
- package/dist/agent-chat/index.d.ts +1 -1
- package/dist/agent-chat/index.d.ts.map +1 -1
- package/dist/agent-chat/thread-detail-panel.d.ts.map +1 -1
- package/dist/agent-chat/thread-detail-panel.js +1 -1
- package/dist/agent-chat/thread-message.d.ts.map +1 -1
- package/dist/agent-chat/thread-message.js +14 -3
- package/dist/agent-chat/thread-message.test.d.ts +2 -0
- package/dist/agent-chat/thread-message.test.d.ts.map +1 -0
- package/dist/agent-chat/thread-workspace.js +1 -1
- package/dist/agent-chat/tool-chip.test.d.ts +2 -0
- package/dist/agent-chat/tool-chip.test.d.ts.map +1 -0
- package/dist/charts/primitives/ChartFrame.test.d.ts +2 -0
- package/dist/charts/primitives/ChartFrame.test.d.ts.map +1 -0
- package/dist/charts/utils/domain.test.d.ts +2 -0
- package/dist/charts/utils/domain.test.d.ts.map +1 -0
- package/dist/cli/check-theme.test.d.ts +2 -0
- package/dist/cli/check-theme.test.d.ts.map +1 -0
- package/dist/cli/help.test.d.ts +2 -0
- package/dist/cli/help.test.d.ts.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/init-sync.test.d.ts +2 -0
- package/dist/cli/init-sync.test.d.ts.map +1 -0
- package/dist/cli/placement-engine.test.d.ts +2 -0
- package/dist/cli/placement-engine.test.d.ts.map +1 -0
- package/dist/cli/tarball-parity.test.d.ts +2 -0
- package/dist/cli/tarball-parity.test.d.ts.map +1 -0
- package/dist/cli/template-format.test.d.ts +2 -0
- package/dist/cli/template-format.test.d.ts.map +1 -0
- package/dist/cli/template-ledger.test.d.ts +2 -0
- package/dist/cli/template-ledger.test.d.ts.map +1 -0
- package/dist/cli/tokens-css.test.d.ts +2 -0
- package/dist/cli/tokens-css.test.d.ts.map +1 -0
- package/dist/content/article-actions.test.d.ts +2 -0
- package/dist/content/article-actions.test.d.ts.map +1 -0
- package/dist/content/article-model.test.d.ts +2 -0
- package/dist/content/article-model.test.d.ts.map +1 -0
- package/dist/content/block-split.test.d.ts +2 -0
- package/dist/content/block-split.test.d.ts.map +1 -0
- package/dist/content/fence-block.d.ts +37 -10
- package/dist/content/fence-block.d.ts.map +1 -1
- package/dist/content/fence-block.js +80 -14
- package/dist/content/heading-components.d.ts.map +1 -1
- package/dist/content/heading-components.js +7 -4
- package/dist/content/heading-components.test.d.ts +2 -0
- package/dist/content/heading-components.test.d.ts.map +1 -0
- package/dist/content/index.d.ts +6 -3
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +5 -1
- package/dist/content/markdown.d.ts +101 -14
- package/dist/content/markdown.d.ts.map +1 -1
- package/dist/content/markdown.js +141 -28
- package/dist/content/markdown.test.d.ts +2 -0
- package/dist/content/markdown.test.d.ts.map +1 -0
- package/dist/content/prose.module.css.test.d.ts +2 -0
- package/dist/content/prose.module.css.test.d.ts.map +1 -0
- package/dist/content/sanitize.d.ts +170 -0
- package/dist/content/sanitize.d.ts.map +1 -0
- package/dist/content/sanitize.js +178 -0
- package/dist/content/sanitize.test.d.ts +2 -0
- package/dist/content/sanitize.test.d.ts.map +1 -0
- package/dist/content/slug.test.d.ts +2 -0
- package/dist/content/slug.test.d.ts.map +1 -0
- package/dist/dashboard/stat-card.test.d.ts +2 -0
- package/dist/dashboard/stat-card.test.d.ts.map +1 -0
- package/dist/guard/check-source.test.d.ts +2 -0
- package/dist/guard/check-source.test.d.ts.map +1 -0
- package/dist/guard/guard-hook.test.d.ts +2 -0
- package/dist/guard/guard-hook.test.d.ts.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/provider/index.test.d.ts +2 -0
- package/dist/provider/index.test.d.ts.map +1 -0
- package/dist/provider/lab-theme.test.d.ts +2 -0
- package/dist/provider/lab-theme.test.d.ts.map +1 -0
- package/dist/router-tanstack/multi-search-param-store.test.d.ts +2 -0
- package/dist/router-tanstack/multi-search-param-store.test.d.ts.map +1 -0
- package/dist/styles.floor.test.d.ts +2 -0
- package/dist/styles.floor.test.d.ts.map +1 -0
- package/dist/surfaces.d.ts +4 -4
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +12 -3
- package/dist/theme/border-coverage.test.d.ts +18 -0
- package/dist/theme/border-coverage.test.d.ts.map +1 -0
- package/dist/theme/contrast.test.d.ts +2 -0
- package/dist/theme/contrast.test.d.ts.map +1 -0
- package/dist/theme/density-option.test.d.ts +2 -0
- package/dist/theme/density-option.test.d.ts.map +1 -0
- package/dist/theme/density-relations.test.d.ts +2 -0
- package/dist/theme/density-relations.test.d.ts.map +1 -0
- package/dist/theme/derive-theme.test.d.ts +2 -0
- package/dist/theme/derive-theme.test.d.ts.map +1 -0
- package/dist/theme/fonts-option.test.d.ts +2 -0
- package/dist/theme/fonts-option.test.d.ts.map +1 -0
- package/dist/theme/radius-option.test.d.ts +2 -0
- package/dist/theme/radius-option.test.d.ts.map +1 -0
- package/dist/theme/radius.test.d.ts +2 -0
- package/dist/theme/radius.test.d.ts.map +1 -0
- package/dist/theme/shadow-surfaces.test.d.ts +26 -0
- package/dist/theme/shadow-surfaces.test.d.ts.map +1 -0
- package/dist/theme/spacing.test.d.ts +2 -0
- package/dist/theme/spacing.test.d.ts.map +1 -0
- package/dist/theme/use-basalt-spacing.test.d.ts +2 -0
- package/dist/theme/use-basalt-spacing.test.d.ts.map +1 -0
- package/dist/theme-lab/derive-controls.test.d.ts +2 -0
- package/dist/theme-lab/derive-controls.test.d.ts.map +1 -0
- package/dist/theme-lab/derive-state.test.d.ts +2 -0
- package/dist/theme-lab/derive-state.test.d.ts.map +1 -0
- package/dist/theme-lab/theme-lab.test.d.ts +2 -0
- package/dist/theme-lab/theme-lab.test.d.ts.map +1 -0
- package/dist/tokens/build-fonts-css.test.d.ts +2 -0
- package/dist/tokens/build-fonts-css.test.d.ts.map +1 -0
- package/dist/tokens/build-radius-css.test.d.ts +2 -0
- package/dist/tokens/build-radius-css.test.d.ts.map +1 -0
- package/dist/tokens/density.test.d.ts +2 -0
- package/dist/tokens/density.test.d.ts.map +1 -0
- package/dist/tokens/derive-radius.test.d.ts +2 -0
- package/dist/tokens/derive-radius.test.d.ts.map +1 -0
- package/dist/tokens/derive.test.d.ts +2 -0
- package/dist/tokens/derive.test.d.ts.map +1 -0
- package/dist/tokens/palette.test.d.ts +2 -0
- package/dist/tokens/palette.test.d.ts.map +1 -0
- package/dist/vite-color-mix.test.d.ts +2 -0
- package/dist/vite-color-mix.test.d.ts.map +1 -0
- package/dist/vite.test.d.ts +2 -0
- package/dist/vite.test.d.ts.map +1 -0
- package/llms.txt +6 -6
- package/package.json +6 -1
- package/src/agent/adapter.test.ts +1403 -0
- package/src/agent/adapter.ts +887 -0
- package/src/agent/ai-sdk-transport.test.ts +53 -26
- package/src/agent/coalesce.test.ts +202 -9
- package/src/agent/coalesce.ts +126 -15
- package/src/agent/foreign.test.ts +5 -1
- package/src/agent/id.test.ts +6 -2
- package/src/agent/id.ts +48 -0
- package/src/agent/index.ts +8 -0
- package/src/agent/merge.test.ts +87 -0
- package/src/agent/merge.ts +57 -10
- package/src/agent/part-list.test.tsx +31 -1
- package/src/agent/part-list.tsx +77 -29
- package/src/agent/parts.test.ts +121 -4
- package/src/agent/parts.ts +78 -17
- package/src/agent/thread.test.ts +75 -2
- package/src/agent/thread.ts +21 -1
- package/src/agent/use-agent-stream.test.tsx +9 -5
- package/src/agent/use-agent-thread-runs.foreign.test.tsx +5 -0
- package/src/agent/use-agent-thread-runs.resume.test.tsx +16 -11
- package/src/agent/use-agent-thread-runs.test.tsx +30 -17
- package/src/agent/use-agent-thread-runs.wedge.test.tsx +13 -7
- package/src/agent-chat/composer.test.tsx +924 -0
- package/src/agent-chat/composer.tsx +521 -52
- package/src/agent-chat/index.ts +1 -1
- package/src/agent-chat/thread-detail-panel.tsx +3 -1
- package/src/agent-chat/thread-message.test.tsx +141 -1
- package/src/agent-chat/thread-message.tsx +29 -4
- package/src/agent-chat/thread-workspace.tsx +1 -1
- package/src/cli/index.ts +0 -1
- package/src/cli/template-format.test.ts +2 -2
- package/src/content/fence-block.tsx +242 -15
- package/src/content/heading-components.test.tsx +65 -0
- package/src/content/heading-components.tsx +26 -5
- package/src/content/index.ts +19 -3
- package/src/content/markdown.test.tsx +603 -0
- package/src/content/markdown.tsx +355 -35
- package/src/content/sanitize.test.ts +659 -0
- package/src/content/sanitize.ts +510 -0
- package/src/dashboard/stat-card.test.tsx +6 -1
- package/src/index.ts +13 -1
- package/src/surfaces.ts +12 -3
- package/src/theme/contrast.test.ts +1 -4
- package/src/theme/density-relations.test.ts +4 -2
- package/src/theme/fonts-option.test.ts +7 -1
- package/src/theme/shadow-surfaces.test.ts +12 -6
- package/src/theme/use-basalt-spacing.test.tsx +2 -1
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import { useEffect, useSyncExternalStore } from "react";
|
|
2
|
+
import { mintThreadId } from "./id.js";
|
|
3
|
+
function createAdapterThreadsStore(adapter, opts) {
|
|
4
|
+
const revalidateOnFocus = opts?.revalidateOnFocus ?? false;
|
|
5
|
+
const EMPTY = [];
|
|
6
|
+
const SERVER_SNAPSHOT = {
|
|
7
|
+
threads: EMPTY,
|
|
8
|
+
activeId: null,
|
|
9
|
+
hydrated: false,
|
|
10
|
+
error: void 0
|
|
11
|
+
};
|
|
12
|
+
let base = EMPTY;
|
|
13
|
+
let baseSeq = 0;
|
|
14
|
+
let activeId = null;
|
|
15
|
+
let hydrated = false;
|
|
16
|
+
let error;
|
|
17
|
+
let errorToken = 0;
|
|
18
|
+
let errorFromWrite = false;
|
|
19
|
+
const pending = [];
|
|
20
|
+
let snapshot = SERVER_SNAPSHOT;
|
|
21
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
22
|
+
function recompute() {
|
|
23
|
+
let threads = base;
|
|
24
|
+
let active = activeId;
|
|
25
|
+
for (const patch of pending) {
|
|
26
|
+
threads = patch.apply(threads);
|
|
27
|
+
if (patch.applyActive !== void 0) active = patch.applyActive(active);
|
|
28
|
+
}
|
|
29
|
+
snapshot = { threads, activeId: active, hydrated, error };
|
|
30
|
+
for (const listener of listeners) listener();
|
|
31
|
+
}
|
|
32
|
+
function memoizeApply(apply) {
|
|
33
|
+
let lastIn;
|
|
34
|
+
let lastOut;
|
|
35
|
+
return (threads) => {
|
|
36
|
+
if (lastOut !== void 0 && threads === lastIn) return lastOut;
|
|
37
|
+
lastIn = threads;
|
|
38
|
+
lastOut = apply(threads);
|
|
39
|
+
return lastOut;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function subscribe(listener) {
|
|
43
|
+
listeners.add(listener);
|
|
44
|
+
return () => {
|
|
45
|
+
listeners.delete(listener);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function getSnapshot() {
|
|
49
|
+
return snapshot;
|
|
50
|
+
}
|
|
51
|
+
function getServerSnapshot() {
|
|
52
|
+
return SERVER_SNAPSHOT;
|
|
53
|
+
}
|
|
54
|
+
let loadController = null;
|
|
55
|
+
let loadSeq = 0;
|
|
56
|
+
let queuedSeq = null;
|
|
57
|
+
function prune() {
|
|
58
|
+
const kept = [];
|
|
59
|
+
for (const patch of pending) {
|
|
60
|
+
if (patch.confirmSeq !== null && patch.confirmSeq <= baseSeq) {
|
|
61
|
+
if (patch.applyActive !== void 0) activeId = patch.applyActive(activeId);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
kept.push(patch);
|
|
65
|
+
}
|
|
66
|
+
if (kept.length !== pending.length) pending.splice(0, pending.length, ...kept);
|
|
67
|
+
}
|
|
68
|
+
function startLoad(seq) {
|
|
69
|
+
const controller = new AbortController();
|
|
70
|
+
loadController = controller;
|
|
71
|
+
void (async () => {
|
|
72
|
+
try {
|
|
73
|
+
const next = await adapter.listThreads(controller.signal);
|
|
74
|
+
if (loadController !== controller) return;
|
|
75
|
+
base = [...next];
|
|
76
|
+
baseSeq = seq;
|
|
77
|
+
hydrated = true;
|
|
78
|
+
if (!errorFromWrite) error = void 0;
|
|
79
|
+
prune();
|
|
80
|
+
recompute();
|
|
81
|
+
} catch (cause) {
|
|
82
|
+
if (controller.signal.aborted || loadController !== controller) return;
|
|
83
|
+
error = cause;
|
|
84
|
+
errorToken += 1;
|
|
85
|
+
errorFromWrite = false;
|
|
86
|
+
recompute();
|
|
87
|
+
} finally {
|
|
88
|
+
if (loadController === controller) {
|
|
89
|
+
loadController = null;
|
|
90
|
+
if (queuedSeq !== null) {
|
|
91
|
+
const next = queuedSeq;
|
|
92
|
+
queuedSeq = null;
|
|
93
|
+
startLoad(next);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
})();
|
|
98
|
+
}
|
|
99
|
+
function revalidate() {
|
|
100
|
+
if (loadController !== null) {
|
|
101
|
+
queuedSeq ??= ++loadSeq;
|
|
102
|
+
return queuedSeq;
|
|
103
|
+
}
|
|
104
|
+
const seq = ++loadSeq;
|
|
105
|
+
startLoad(seq);
|
|
106
|
+
return seq;
|
|
107
|
+
}
|
|
108
|
+
function forceRevalidate() {
|
|
109
|
+
loadController?.abort();
|
|
110
|
+
queuedSeq = null;
|
|
111
|
+
const seq = ++loadSeq;
|
|
112
|
+
startLoad(seq);
|
|
113
|
+
return seq;
|
|
114
|
+
}
|
|
115
|
+
const chains = /* @__PURE__ */ new Map();
|
|
116
|
+
function enqueue(threadIds, task) {
|
|
117
|
+
const ids = [...new Set(threadIds)];
|
|
118
|
+
const prior = ids.map((id) => chains.get(id)).filter((p) => p !== void 0);
|
|
119
|
+
const after = prior.length === 0 ? Promise.resolve() : Promise.all(prior).then(() => void 0);
|
|
120
|
+
const run = after.then(task);
|
|
121
|
+
for (const id of ids) chains.set(id, run);
|
|
122
|
+
void run.finally(() => {
|
|
123
|
+
for (const id of ids) if (chains.get(id) === run) chains.delete(id);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function drop(patch) {
|
|
127
|
+
const index = pending.indexOf(patch);
|
|
128
|
+
if (index !== -1) pending.splice(index, 1);
|
|
129
|
+
}
|
|
130
|
+
function mutate(i) {
|
|
131
|
+
const patch = {
|
|
132
|
+
apply: memoizeApply(i.apply),
|
|
133
|
+
...i.applyActive !== void 0 ? { applyActive: i.applyActive } : {},
|
|
134
|
+
confirmSeq: null
|
|
135
|
+
};
|
|
136
|
+
pending.push(patch);
|
|
137
|
+
const tokenAtIssue = errorToken;
|
|
138
|
+
recompute();
|
|
139
|
+
enqueue(i.threadIds, async () => {
|
|
140
|
+
try {
|
|
141
|
+
await i.commit();
|
|
142
|
+
} catch (cause) {
|
|
143
|
+
drop(patch);
|
|
144
|
+
error = cause;
|
|
145
|
+
errorToken += 1;
|
|
146
|
+
errorFromWrite = true;
|
|
147
|
+
recompute();
|
|
148
|
+
revalidate();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (errorFromWrite && errorToken === tokenAtIssue) {
|
|
152
|
+
error = void 0;
|
|
153
|
+
errorFromWrite = false;
|
|
154
|
+
recompute();
|
|
155
|
+
}
|
|
156
|
+
patch.confirmSeq = revalidate();
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
function patchThread(id, update) {
|
|
160
|
+
return (threads) => threads.map((thread) => thread.id === id ? update(thread) : thread);
|
|
161
|
+
}
|
|
162
|
+
let mounted = 0;
|
|
163
|
+
function onFocus() {
|
|
164
|
+
forceRevalidate();
|
|
165
|
+
}
|
|
166
|
+
function retain() {
|
|
167
|
+
mounted += 1;
|
|
168
|
+
if (mounted !== 1) return;
|
|
169
|
+
revalidate();
|
|
170
|
+
if (revalidateOnFocus && typeof window !== "undefined") {
|
|
171
|
+
window.addEventListener("focus", onFocus);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function release() {
|
|
175
|
+
mounted -= 1;
|
|
176
|
+
if (mounted !== 0) return;
|
|
177
|
+
loadController?.abort();
|
|
178
|
+
loadController = null;
|
|
179
|
+
queuedSeq = null;
|
|
180
|
+
if (revalidateOnFocus && typeof window !== "undefined") {
|
|
181
|
+
window.removeEventListener("focus", onFocus);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function select(id) {
|
|
185
|
+
activeId = id;
|
|
186
|
+
recompute();
|
|
187
|
+
}
|
|
188
|
+
function create(createOpts) {
|
|
189
|
+
const id = mintThreadId();
|
|
190
|
+
const now = Date.now();
|
|
191
|
+
const thread = {
|
|
192
|
+
id,
|
|
193
|
+
messages: [],
|
|
194
|
+
outcome: null,
|
|
195
|
+
status: "pending",
|
|
196
|
+
read: false,
|
|
197
|
+
createdAt: now,
|
|
198
|
+
updatedAt: now,
|
|
199
|
+
...createOpts?.meta !== void 0 ? { meta: createOpts.meta } : {}
|
|
200
|
+
};
|
|
201
|
+
mutate({
|
|
202
|
+
threadIds: [id],
|
|
203
|
+
apply: (threads) => threads.some((t) => t.id === id) ? [...threads] : [thread, ...threads],
|
|
204
|
+
commit: () => adapter.createThread({
|
|
205
|
+
id,
|
|
206
|
+
...createOpts?.meta !== void 0 ? { meta: createOpts.meta } : {}
|
|
207
|
+
})
|
|
208
|
+
});
|
|
209
|
+
return id;
|
|
210
|
+
}
|
|
211
|
+
function appendMessage(id, message2) {
|
|
212
|
+
const now = Date.now();
|
|
213
|
+
mutate({
|
|
214
|
+
threadIds: [id],
|
|
215
|
+
apply: patchThread(
|
|
216
|
+
id,
|
|
217
|
+
(thread) => thread.messages.some((m) => m.id === message2.id) ? thread : { ...thread, messages: [...thread.messages, message2], updatedAt: now }
|
|
218
|
+
),
|
|
219
|
+
commit: () => adapter.appendMessage({ threadId: id, message: message2 })
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
function setOutcome(id, outcome) {
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
mutate({
|
|
225
|
+
threadIds: [id],
|
|
226
|
+
apply: patchThread(id, (thread) => ({ ...thread, outcome, updatedAt: now })),
|
|
227
|
+
commit: () => adapter.setOutcome({ threadId: id, outcome })
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
function setStatus(id, status) {
|
|
231
|
+
const now = Date.now();
|
|
232
|
+
mutate({
|
|
233
|
+
threadIds: [id],
|
|
234
|
+
apply: patchThread(id, (thread) => ({ ...thread, status, updatedAt: now })),
|
|
235
|
+
commit: () => adapter.setStatus({ threadId: id, status })
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
function setResumeToken(id, token) {
|
|
239
|
+
const now = Date.now();
|
|
240
|
+
mutate({
|
|
241
|
+
threadIds: [id],
|
|
242
|
+
apply: patchThread(id, (thread) => {
|
|
243
|
+
const { resumeToken: _resumeToken, ...rest } = thread;
|
|
244
|
+
return {
|
|
245
|
+
...rest,
|
|
246
|
+
...token !== void 0 ? { resumeToken: token } : {},
|
|
247
|
+
updatedAt: now
|
|
248
|
+
};
|
|
249
|
+
}),
|
|
250
|
+
commit: () => adapter.setResumeToken({ threadId: id, token })
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
function markRead(id) {
|
|
254
|
+
mutate({
|
|
255
|
+
threadIds: [id],
|
|
256
|
+
// Identity-preserving when the server already agrees — markRead does not bump updatedAt.
|
|
257
|
+
apply: patchThread(id, (thread) => thread.read ? thread : { ...thread, read: true }),
|
|
258
|
+
commit: () => adapter.markRead(id)
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
function remove(id) {
|
|
262
|
+
mutate({
|
|
263
|
+
threadIds: [id],
|
|
264
|
+
apply: (threads) => threads.filter((thread) => thread.id !== id),
|
|
265
|
+
// Inside the patch, so a rejected delete restores the SELECTION along with the row. Written
|
|
266
|
+
// as a conditional clear rather than a flat `null` so a `select()` issued while the delete
|
|
267
|
+
// is in flight survives the patch being re-applied on top of it.
|
|
268
|
+
applyActive: (active) => active === id ? null : active,
|
|
269
|
+
commit: () => adapter.removeThread(id)
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
function clear() {
|
|
273
|
+
const ids = snapshot.threads.map((thread) => thread.id);
|
|
274
|
+
const clearedIds = new Set(ids);
|
|
275
|
+
mutate({
|
|
276
|
+
threadIds: ids,
|
|
277
|
+
apply: () => [],
|
|
278
|
+
// Same idiom as remove(): only null a selection pointing at one of the threads THIS clear
|
|
279
|
+
// captured, so a select() issued while the clear is in flight — e.g. into a thread created
|
|
280
|
+
// concurrently by create() — survives the patch being re-applied on top of it.
|
|
281
|
+
applyActive: (active) => active !== null && clearedIds.has(active) ? null : active,
|
|
282
|
+
commit: async () => {
|
|
283
|
+
const results = await Promise.allSettled(ids.map((id) => adapter.removeThread(id)));
|
|
284
|
+
const failures = results.filter((r) => r.status === "rejected");
|
|
285
|
+
if (failures.length === 0) return;
|
|
286
|
+
throw new AggregateError(
|
|
287
|
+
failures.map((f) => f.reason),
|
|
288
|
+
`removeThread rejected for ${failures.length} of ${ids.length} threads`
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return function useAdapterThreadsStore() {
|
|
294
|
+
const current = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
295
|
+
useEffect(() => {
|
|
296
|
+
retain();
|
|
297
|
+
return () => {
|
|
298
|
+
release();
|
|
299
|
+
};
|
|
300
|
+
}, []);
|
|
301
|
+
return {
|
|
302
|
+
threads: current.threads,
|
|
303
|
+
activeId: current.activeId,
|
|
304
|
+
hydrated: current.hydrated,
|
|
305
|
+
error: current.error,
|
|
306
|
+
select,
|
|
307
|
+
create,
|
|
308
|
+
appendMessage,
|
|
309
|
+
setOutcome,
|
|
310
|
+
setStatus,
|
|
311
|
+
setResumeToken,
|
|
312
|
+
markRead,
|
|
313
|
+
remove,
|
|
314
|
+
clear
|
|
315
|
+
};
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function assert(condition, message2) {
|
|
319
|
+
if (!condition) throw new Error(`ThreadsStoreAdapter contract: ${message2}`);
|
|
320
|
+
}
|
|
321
|
+
function message(id) {
|
|
322
|
+
return { id, role: "user", parts: [], createdAt: Date.now() };
|
|
323
|
+
}
|
|
324
|
+
function threadsStoreAdapterContract(makeAdapter) {
|
|
325
|
+
function define(name, run) {
|
|
326
|
+
return {
|
|
327
|
+
name,
|
|
328
|
+
run: async () => {
|
|
329
|
+
await run(await makeAdapter());
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
async function loadOrThrow(adapter, id) {
|
|
334
|
+
const thread = await adapter.loadThread(id);
|
|
335
|
+
assert(thread !== null, `loadThread('${id}') returned null for a thread that was created`);
|
|
336
|
+
return thread;
|
|
337
|
+
}
|
|
338
|
+
return [
|
|
339
|
+
define("createThread then listThreads returns the thread with empty defaults", async (a) => {
|
|
340
|
+
const id = crypto.randomUUID();
|
|
341
|
+
await a.createThread({ id });
|
|
342
|
+
const threads = await a.listThreads();
|
|
343
|
+
const found = threads.filter((t) => t.id === id);
|
|
344
|
+
assert(found.length === 1, `listThreads returned ${found.length} threads for one create`);
|
|
345
|
+
const thread = found[0];
|
|
346
|
+
assert(thread !== void 0, "listThreads entry was undefined");
|
|
347
|
+
assert(thread.messages.length === 0, "a new thread must start with no messages");
|
|
348
|
+
assert(thread.outcome === null, "a new thread's outcome must be null");
|
|
349
|
+
assert(thread.status === "pending", `a new thread's status must be 'pending'`);
|
|
350
|
+
assert(thread.read === false, "a new thread must be unread");
|
|
351
|
+
assert(typeof thread.createdAt === "number", "createdAt must be a number");
|
|
352
|
+
assert(typeof thread.updatedAt === "number", "updatedAt must be a number");
|
|
353
|
+
}),
|
|
354
|
+
define("createThread persists meta", async (a) => {
|
|
355
|
+
const id = crypto.randomUUID();
|
|
356
|
+
await a.createThread({ id, meta: { source: "contract" } });
|
|
357
|
+
const thread = await loadOrThrow(a, id);
|
|
358
|
+
assert(thread.meta?.["source"] === "contract", "meta did not round-trip");
|
|
359
|
+
}),
|
|
360
|
+
define("loadThread returns null for an unknown id", async (a) => {
|
|
361
|
+
const thread = await a.loadThread(crypto.randomUUID());
|
|
362
|
+
assert(thread === null, "loadThread must return null (not throw, not undefined) when absent");
|
|
363
|
+
}),
|
|
364
|
+
define("appendMessage is idempotent on message.id", async (a) => {
|
|
365
|
+
const id = crypto.randomUUID();
|
|
366
|
+
await a.createThread({ id });
|
|
367
|
+
const msg = message("contract-msg-1");
|
|
368
|
+
await a.appendMessage({ threadId: id, message: msg });
|
|
369
|
+
await a.appendMessage({ threadId: id, message: msg });
|
|
370
|
+
const thread = await loadOrThrow(a, id);
|
|
371
|
+
assert(
|
|
372
|
+
thread.messages.length === 1,
|
|
373
|
+
`appending the same message.id twice produced ${thread.messages.length} rows \u2014 the id is the only idempotency key`
|
|
374
|
+
);
|
|
375
|
+
assert(thread.messages[0]?.id === "contract-msg-1", "the retained message has the wrong id");
|
|
376
|
+
}),
|
|
377
|
+
define("appendMessage preserves order for distinct ids", async (a) => {
|
|
378
|
+
const id = crypto.randomUUID();
|
|
379
|
+
await a.createThread({ id });
|
|
380
|
+
await a.appendMessage({ threadId: id, message: message("contract-msg-1") });
|
|
381
|
+
await a.appendMessage({ threadId: id, message: message("contract-msg-2") });
|
|
382
|
+
const thread = await loadOrThrow(a, id);
|
|
383
|
+
assert(
|
|
384
|
+
thread.messages.map((m) => m.id).join(",") === "contract-msg-1,contract-msg-2",
|
|
385
|
+
"messages must read back oldest-first in append order"
|
|
386
|
+
);
|
|
387
|
+
}),
|
|
388
|
+
define("setStatus round-trips", async (a) => {
|
|
389
|
+
const id = crypto.randomUUID();
|
|
390
|
+
await a.createThread({ id });
|
|
391
|
+
await a.setStatus({ threadId: id, status: "streaming" });
|
|
392
|
+
assert((await loadOrThrow(a, id)).status === "streaming", "status did not become 'streaming'");
|
|
393
|
+
await a.setStatus({ threadId: id, status: "error" });
|
|
394
|
+
assert((await loadOrThrow(a, id)).status === "error", "status did not become 'error'");
|
|
395
|
+
}),
|
|
396
|
+
define("setOutcome round-trips", async (a) => {
|
|
397
|
+
const id = crypto.randomUUID();
|
|
398
|
+
await a.createThread({ id });
|
|
399
|
+
const outcome = { title: "Title", summary: "Summary", status: "done" };
|
|
400
|
+
await a.setOutcome({ threadId: id, outcome });
|
|
401
|
+
const thread = await loadOrThrow(a, id);
|
|
402
|
+
assert(thread.outcome?.title === "Title", "outcome.title did not round-trip");
|
|
403
|
+
assert(thread.outcome?.summary === "Summary", "outcome.summary did not round-trip");
|
|
404
|
+
assert(thread.outcome?.status === "done", "outcome.status did not round-trip");
|
|
405
|
+
}),
|
|
406
|
+
define("setResumeToken round-trips and clears with undefined", async (a) => {
|
|
407
|
+
const id = crypto.randomUUID();
|
|
408
|
+
await a.createThread({ id });
|
|
409
|
+
await a.setResumeToken({ threadId: id, token: "tok-1" });
|
|
410
|
+
assert((await loadOrThrow(a, id)).resumeToken === "tok-1", "resumeToken did not round-trip");
|
|
411
|
+
await a.setResumeToken({ threadId: id, token: void 0 });
|
|
412
|
+
assert(
|
|
413
|
+
(await loadOrThrow(a, id)).resumeToken === void 0,
|
|
414
|
+
"setResumeToken(undefined) must CLEAR the token"
|
|
415
|
+
);
|
|
416
|
+
}),
|
|
417
|
+
define("markRead marks the thread read", async (a) => {
|
|
418
|
+
const id = crypto.randomUUID();
|
|
419
|
+
await a.createThread({ id });
|
|
420
|
+
assert((await loadOrThrow(a, id)).read === false, "a new thread must start unread");
|
|
421
|
+
await a.markRead(id);
|
|
422
|
+
assert((await loadOrThrow(a, id)).read === true, "markRead did not set read");
|
|
423
|
+
}),
|
|
424
|
+
define("listThreads reflects a write that has already resolved", async (a) => {
|
|
425
|
+
const id = crypto.randomUUID();
|
|
426
|
+
await a.createThread({ id });
|
|
427
|
+
await a.appendMessage({ threadId: id, message: message("contract-msg-raw") });
|
|
428
|
+
await a.setStatus({ threadId: id, status: "streaming" });
|
|
429
|
+
const found = (await a.listThreads()).find((t) => t.id === id);
|
|
430
|
+
assert(found !== void 0, "listThreads omitted a thread created before the call");
|
|
431
|
+
assert(
|
|
432
|
+
found.status === "streaming",
|
|
433
|
+
"listThreads returned a STALE status \u2014 a list started after a write resolves must reflect it"
|
|
434
|
+
);
|
|
435
|
+
assert(
|
|
436
|
+
found.messages.length === 1,
|
|
437
|
+
`listThreads returned a STALE message list (${found.messages.length} rows) \u2014 a list started after a write resolves must reflect it`
|
|
438
|
+
);
|
|
439
|
+
}),
|
|
440
|
+
define(
|
|
441
|
+
"dependent writes issued straight after createThread resolves are honoured",
|
|
442
|
+
async (a) => {
|
|
443
|
+
const id = crypto.randomUUID();
|
|
444
|
+
await a.createThread({ id });
|
|
445
|
+
await a.markRead(id);
|
|
446
|
+
await a.appendMessage({ threadId: id, message: message("contract-msg-dep") });
|
|
447
|
+
await a.setStatus({ threadId: id, status: "streaming" });
|
|
448
|
+
const thread = await loadOrThrow(a, id);
|
|
449
|
+
assert(thread.read === true, "markRead did not apply to a just-created thread");
|
|
450
|
+
assert(
|
|
451
|
+
thread.messages.length === 1,
|
|
452
|
+
`appendMessage did not apply to a just-created thread (${thread.messages.length} rows)`
|
|
453
|
+
);
|
|
454
|
+
assert(thread.status === "streaming", "setStatus did not apply to a just-created thread");
|
|
455
|
+
}
|
|
456
|
+
),
|
|
457
|
+
define("removeThread removes the thread", async (a) => {
|
|
458
|
+
const id = crypto.randomUUID();
|
|
459
|
+
await a.createThread({ id });
|
|
460
|
+
await a.removeThread(id);
|
|
461
|
+
assert(await a.loadThread(id).then((t) => t === null), "loadThread must be null after remove");
|
|
462
|
+
const threads = await a.listThreads();
|
|
463
|
+
assert(!threads.some((t) => t.id === id), "listThreads still contains the removed thread");
|
|
464
|
+
}),
|
|
465
|
+
define("removeThread of an unknown id does not throw", async (a) => {
|
|
466
|
+
await a.removeThread(crypto.randomUUID());
|
|
467
|
+
})
|
|
468
|
+
];
|
|
469
|
+
}
|
|
470
|
+
export {
|
|
471
|
+
createAdapterThreadsStore,
|
|
472
|
+
threadsStoreAdapterContract
|
|
473
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.test.d.ts","sourceRoot":"","sources":["../../src/agent/adapter.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-sdk-transport.test.d.ts","sourceRoot":"","sources":["../../src/agent/ai-sdk-transport.test.ts"],"names":[],"mappings":""}
|
package/dist/agent/coalesce.d.ts
CHANGED
|
@@ -12,10 +12,26 @@
|
|
|
12
12
|
* wire, the SDK re-supplies `input` by reading it back off the stored invocation, and a merge that
|
|
13
13
|
* replaces wholesale rather than accumulating would lose it outright.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* A settled tool part can arrive without a `toolCallId` at all — `approval-responded` plus the
|
|
16
|
+
* three terminal states all carry it as optional (see `MaybeIdentified` in `parts.ts`).
|
|
17
|
+
* `approval-responded` is where the WIRE itself omits it: the SDK's `tool-approval-response` chunk
|
|
18
|
+
* carries no `toolCallId`, only `approval.id`, so it is resolved by reverse lookup on `approval.id`
|
|
19
|
+
* instead — this maintains an `approvalId -> toolCallId` side index while folding and routes such a
|
|
20
|
+
* part through it. A part resolvable by neither key is kept as-is rather than dropped; never
|
|
21
|
+
* silently discard a part.
|
|
22
|
+
*
|
|
23
|
+
* The fold is strictly LEFT-TO-RIGHT with no backtracking: the side index only holds ids seen so
|
|
24
|
+
* far, so an unidentified `approval-responded` arriving BEFORE its `approval-requested` stays its
|
|
25
|
+
* own block and the later request opens a second one. That is deliberate, not an oversight —
|
|
26
|
+
* reconciling backwards would let the request (the later arrival) win under the "later state wins"
|
|
27
|
+
* rule and render an already-answered call as still pending, which is worse than two blocks in an
|
|
28
|
+
* ordering the wire never produces.
|
|
29
|
+
*
|
|
30
|
+
* **This function never throws.** It is public, it runs on the render path, and it folds data that
|
|
31
|
+
* came off a wire — so every anomaly (an unrecognized tool state, an unresolvable id) degrades to
|
|
32
|
+
* "keep the part as it arrived", announced in dev, rather than aborting the render. A throw here
|
|
33
|
+
* would turn one malformed chunk into a permanently dead transcript. Exhaustiveness over the tool
|
|
34
|
+
* states is still enforced, but at COMPILE time only (see `mergeToolPart`'s default branch).
|
|
19
35
|
*
|
|
20
36
|
* @example
|
|
21
37
|
* import { coalesceParts } from 'basalt-ui/agent'
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coalesce.d.ts","sourceRoot":"","sources":["../../src/agent/coalesce.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"coalesce.d.ts","sourceRoot":"","sources":["../../src/agent/coalesce.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,OAAO,KAAK,EAAE,SAAS,EAAyC,MAAM,SAAS,CAAA;AA8I/E,wBAAgB,aAAa,CAAC,KAAK,SAAS,SAAS,EAAE,KAAK,EAAE,SAAS,KAAK,EAAE,GAAG,KAAK,EAAE,CA+CvF"}
|
package/dist/agent/coalesce.js
CHANGED
|
@@ -2,27 +2,95 @@ function isTextLikeType(type) {
|
|
|
2
2
|
return type === "text" || type === "reasoning";
|
|
3
3
|
}
|
|
4
4
|
function resolveToolCallId(part, toolCallIdByApprovalId) {
|
|
5
|
-
if (
|
|
5
|
+
if (part.toolCallId !== void 0) return part.toolCallId;
|
|
6
6
|
const approval = "approval" in part ? part.approval : void 0;
|
|
7
7
|
return approval !== void 0 ? toolCallIdByApprovalId.get(approval.id) : void 0;
|
|
8
8
|
}
|
|
9
9
|
function registerApprovalIndex(part, toolCallIdByApprovalId) {
|
|
10
10
|
const approval = "approval" in part ? part.approval : void 0;
|
|
11
11
|
if (approval === void 0) return;
|
|
12
|
-
if (
|
|
12
|
+
if (part.toolCallId === void 0) return;
|
|
13
13
|
toolCallIdByApprovalId.set(approval.id, part.toolCallId);
|
|
14
14
|
}
|
|
15
15
|
function mergeToolPart(existing, next) {
|
|
16
16
|
const existingInput = "input" in existing ? existing.input : void 0;
|
|
17
17
|
const nextInput = "input" in next ? next.input : void 0;
|
|
18
18
|
const input = nextInput !== void 0 ? nextInput : existingInput;
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
const durationMs = next.durationMs ?? existing.durationMs;
|
|
20
|
+
const providerExecuted = next.providerExecuted ?? existing.providerExecuted;
|
|
21
|
+
const settledToolCallId = next.toolCallId ?? existing.toolCallId;
|
|
22
|
+
const common = {
|
|
23
|
+
id: next.id,
|
|
24
|
+
type: "tool",
|
|
25
|
+
toolName: next.toolName.length > 0 ? next.toolName : existing.toolName,
|
|
26
|
+
...durationMs !== void 0 ? { durationMs } : {},
|
|
27
|
+
...providerExecuted !== void 0 ? { providerExecuted } : {}
|
|
25
28
|
};
|
|
29
|
+
switch (next.state) {
|
|
30
|
+
case "input-streaming":
|
|
31
|
+
return {
|
|
32
|
+
...common,
|
|
33
|
+
toolCallId: next.toolCallId,
|
|
34
|
+
state: "input-streaming",
|
|
35
|
+
...input !== void 0 ? { input } : {}
|
|
36
|
+
};
|
|
37
|
+
case "input-available":
|
|
38
|
+
return { ...common, toolCallId: next.toolCallId, state: "input-available", input };
|
|
39
|
+
case "approval-requested":
|
|
40
|
+
return {
|
|
41
|
+
...common,
|
|
42
|
+
toolCallId: next.toolCallId,
|
|
43
|
+
state: "approval-requested",
|
|
44
|
+
input,
|
|
45
|
+
approval: next.approval
|
|
46
|
+
};
|
|
47
|
+
case "approval-responded":
|
|
48
|
+
return {
|
|
49
|
+
...common,
|
|
50
|
+
...settledToolCallId !== void 0 ? { toolCallId: settledToolCallId } : {},
|
|
51
|
+
state: "approval-responded",
|
|
52
|
+
input,
|
|
53
|
+
approval: next.approval
|
|
54
|
+
};
|
|
55
|
+
case "output-available":
|
|
56
|
+
return {
|
|
57
|
+
...common,
|
|
58
|
+
...settledToolCallId !== void 0 ? { toolCallId: settledToolCallId } : {},
|
|
59
|
+
state: "output-available",
|
|
60
|
+
input,
|
|
61
|
+
output: next.output,
|
|
62
|
+
...next.preliminary !== void 0 ? { preliminary: next.preliminary } : {},
|
|
63
|
+
...next.approval !== void 0 ? { approval: next.approval } : {}
|
|
64
|
+
};
|
|
65
|
+
case "output-error":
|
|
66
|
+
return {
|
|
67
|
+
...common,
|
|
68
|
+
...settledToolCallId !== void 0 ? { toolCallId: settledToolCallId } : {},
|
|
69
|
+
state: "output-error",
|
|
70
|
+
errorText: next.errorText,
|
|
71
|
+
...input !== void 0 ? { input } : {},
|
|
72
|
+
...next.rawInput !== void 0 ? { rawInput: next.rawInput } : {},
|
|
73
|
+
...next.approval !== void 0 ? { approval: next.approval } : {}
|
|
74
|
+
};
|
|
75
|
+
case "output-denied":
|
|
76
|
+
return {
|
|
77
|
+
...common,
|
|
78
|
+
...settledToolCallId !== void 0 ? { toolCallId: settledToolCallId } : {},
|
|
79
|
+
state: "output-denied",
|
|
80
|
+
input,
|
|
81
|
+
approval: next.approval
|
|
82
|
+
};
|
|
83
|
+
default: {
|
|
84
|
+
const unhandled = next;
|
|
85
|
+
const fallback = unhandled;
|
|
86
|
+
if (process.env["NODE_ENV"] !== "production") {
|
|
87
|
+
console.warn(
|
|
88
|
+
`[basalt] coalesceParts: unrecognized tool state ${JSON.stringify(fallback.state)} \u2014 kept, merged best-effort`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return { ...fallback, ...common };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
26
94
|
}
|
|
27
95
|
function coalesceParts(parts) {
|
|
28
96
|
const out = [];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coalesce.test.d.ts","sourceRoot":"","sources":["../../src/agent/coalesce.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"foreign.test.d.ts","sourceRoot":"","sources":["../../src/agent/foreign.test.ts"],"names":[],"mappings":""}
|
package/dist/agent/id.d.ts
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Id-minting helpers shared by the thread stores: `mintThreadId` (a client-side thread id, used by
|
|
3
|
+
* both `./thread`'s `createThreadsStore` and `./adapter`'s `createAdapterThreadsStore`) and
|
|
4
|
+
* `withPartIds` (stamps sequence ids onto streamed parts). Related in theme — both mint an id a
|
|
5
|
+
* store needs — unrelated in mechanism; grouped here because they're the two id-minting leaves the
|
|
6
|
+
* two thread stores both sit above.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Mints a client-side thread id. `crypto.randomUUID()` exists only in SECURE CONTEXTS (HTTPS or
|
|
10
|
+
* localhost) — a consumer served over plain HTTP on a LAN hostname (a homelab dashboard on a bare
|
|
11
|
+
* IP, a staging box) would otherwise have `create()`, about the most basic user action a thread
|
|
12
|
+
* store has, throw a plain TypeError. A store used from a browser must not throw on a user gesture
|
|
13
|
+
* — the same principle already applied twice this release (`spliceText` clamps rather than throws,
|
|
14
|
+
* `coalesceParts` degrades rather than throws).
|
|
15
|
+
*
|
|
16
|
+
* Fallback chain, each rung reached only when the one above it is unavailable:
|
|
17
|
+
* 1. `crypto.randomUUID()` — cryptographically random, RFC 4122 UUID. The normal path.
|
|
18
|
+
* 2. `crypto.getRandomValues()` — still cryptographically random; hand-assembled into a UUIDv4
|
|
19
|
+
* (the version/variant bits are set per RFC 4122, since this rung hands back raw bytes, not a
|
|
20
|
+
* formatted UUID).
|
|
21
|
+
* 3. No usable `crypto` at all (missing entirely, or missing both methods above — an old WebView,
|
|
22
|
+
* an SSR shim). NOT cryptographically random and NOT collision-resistant in general. This rung
|
|
23
|
+
* exists solely so `create()` never throws on such a host. It is acceptable ONLY because this
|
|
24
|
+
* id is minted a handful of times per client session and is not the idempotency key any write
|
|
25
|
+
* here depends on — `ThreadsStoreAdapter.appendMessage`'s idempotency key is the MESSAGE id,
|
|
26
|
+
* not this thread id, so a collision on this rung costs a locally merged/duplicated thread, not
|
|
27
|
+
* silent data loss on the append path. Do not reuse this rung for anything with a higher
|
|
28
|
+
* collision cost than that.
|
|
29
|
+
*/
|
|
30
|
+
export declare function mintThreadId(): string;
|
|
1
31
|
/**
|
|
2
32
|
* withPartIds — stamps a stable id onto every draft part arriving without one.
|
|
3
33
|
*
|