basalt-ui 1.12.0 → 1.13.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 +17 -0
- package/README.md +38 -35
- package/agent/rules/basalt-agent.md +14 -0
- package/configs/oxlint-plugin.js +7 -3
- package/configs/oxlint-plugin.test.ts +34 -5
- package/configs/oxlint.json +4 -4
- package/dist/agent/adapter.d.ts +5 -1
- package/dist/agent/adapter.d.ts.map +1 -1
- package/dist/agent/adapter.js +22 -13
- package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
- package/dist/agent/ai-sdk-transport.js +6 -2
- package/dist/agent/id.d.ts +67 -19
- package/dist/agent/id.d.ts.map +1 -1
- package/dist/agent/id.js +21 -7
- package/dist/agent/thread.d.ts +4 -0
- package/dist/agent/thread.d.ts.map +1 -1
- package/dist/agent/use-agent-stream.d.ts.map +1 -1
- package/dist/agent/use-agent-stream.js +2 -2
- package/dist/agent/use-agent-thread-runs.d.ts +24 -3
- package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
- package/dist/agent/use-agent-thread-runs.js +55 -15
- package/dist/agent-chat/index.d.ts +4 -0
- package/dist/agent-chat/index.d.ts.map +1 -1
- package/dist/agent-chat/index.js +2 -0
- package/dist/agent-chat/message-affordances.d.ts +59 -0
- package/dist/agent-chat/message-affordances.d.ts.map +1 -0
- package/dist/agent-chat/message-affordances.js +7 -0
- package/dist/agent-chat/relative-time.d.ts +27 -0
- package/dist/agent-chat/relative-time.d.ts.map +1 -0
- package/dist/agent-chat/relative-time.js +27 -0
- package/dist/agent-chat/relative-time.test.d.ts +2 -0
- package/dist/agent-chat/relative-time.test.d.ts.map +1 -0
- package/dist/agent-chat/thread-feed-row.d.ts +65 -0
- package/dist/agent-chat/thread-feed-row.d.ts.map +1 -0
- package/dist/agent-chat/thread-feed-row.js +156 -0
- package/dist/agent-chat/thread-feed-row.test.d.ts +2 -0
- package/dist/agent-chat/thread-feed-row.test.d.ts.map +1 -0
- package/dist/agent-chat/thread-feed.d.ts +34 -5
- package/dist/agent-chat/thread-feed.d.ts.map +1 -1
- package/dist/agent-chat/thread-feed.js +52 -19
- package/dist/agent-chat/thread-feed.test.d.ts +2 -0
- package/dist/agent-chat/thread-feed.test.d.ts.map +1 -0
- package/dist/agent-chat/thread-message.d.ts +183 -2
- package/dist/agent-chat/thread-message.d.ts.map +1 -1
- package/dist/agent-chat/thread-message.js +315 -34
- package/dist/agent-chat/thread-outcome-card.d.ts.map +1 -1
- package/dist/agent-chat/thread-outcome-card.js +1 -16
- package/dist/agent-chat/thread-workspace.d.ts +14 -2
- package/dist/agent-chat/thread-workspace.d.ts.map +1 -1
- package/dist/agent-chat/thread-workspace.js +29 -3
- package/dist/agent-chat/thread-workspace.test.d.ts +2 -0
- package/dist/agent-chat/thread-workspace.test.d.ts.map +1 -0
- package/dist/agent-chat/virtualize.d.ts +123 -0
- package/dist/agent-chat/virtualize.d.ts.map +1 -0
- package/dist/agent-chat/virtualize.js +9 -0
- package/dist/agent-chat/virtualize.type-guard.test.d.ts +2 -0
- package/dist/agent-chat/virtualize.type-guard.test.d.ts.map +1 -0
- package/dist/data/index.d.ts +5 -1
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/virtual-list.d.ts.map +1 -1
- package/dist/data/virtual.d.ts +2 -1
- package/dist/data/virtual.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/surfaces.d.ts +2 -2
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +3 -2
- package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
- package/llms.txt +6 -6
- package/package.json +2 -2
- package/src/agent/adapter.test.ts +237 -0
- package/src/agent/adapter.ts +72 -14
- package/src/agent/ai-sdk-transport.test.ts +64 -0
- package/src/agent/ai-sdk-transport.ts +8 -2
- package/src/agent/id.test.ts +95 -1
- package/src/agent/id.ts +98 -28
- package/src/agent/thread.ts +4 -0
- package/src/agent/use-agent-stream.test.tsx +22 -0
- package/src/agent/use-agent-stream.ts +6 -3
- package/src/agent/use-agent-thread-runs.test.tsx +326 -0
- package/src/agent/use-agent-thread-runs.ts +159 -23
- package/src/agent/use-agent-thread-runs.wedge.test.tsx +80 -0
- package/src/agent-chat/index.ts +12 -0
- package/src/agent-chat/message-affordances.ts +61 -0
- package/src/agent-chat/relative-time.test.ts +95 -0
- package/src/agent-chat/relative-time.ts +55 -0
- package/src/agent-chat/thread-feed-row.test.tsx +510 -0
- package/src/agent-chat/thread-feed-row.tsx +274 -0
- package/src/agent-chat/thread-feed.test.tsx +241 -0
- package/src/agent-chat/thread-feed.tsx +119 -18
- package/src/agent-chat/thread-message.test.tsx +937 -3
- package/src/agent-chat/thread-message.tsx +766 -38
- package/src/agent-chat/thread-outcome-card.tsx +1 -25
- package/src/agent-chat/thread-workspace.test.tsx +169 -0
- package/src/agent-chat/thread-workspace.tsx +69 -5
- package/src/agent-chat/virtualize.ts +132 -0
- package/src/agent-chat/virtualize.type-guard.test.ts +49 -0
- package/src/data/index.ts +5 -1
- package/src/data/virtual-list.tsx +2 -1
- package/src/data/virtual.ts +2 -1
- package/src/index.ts +5 -0
- package/src/surfaces.ts +2 -1
- package/src/theme/shadow-surfaces.test.ts +9 -0
package/dist/agent/adapter.js
CHANGED
|
@@ -113,6 +113,7 @@ function createAdapterThreadsStore(adapter, opts) {
|
|
|
113
113
|
return seq;
|
|
114
114
|
}
|
|
115
115
|
const chains = /* @__PURE__ */ new Map();
|
|
116
|
+
const failedCreateIds = /* @__PURE__ */ new Set();
|
|
116
117
|
function enqueue(threadIds, task) {
|
|
117
118
|
const ids = [...new Set(threadIds)];
|
|
118
119
|
const prior = ids.map((id) => chains.get(id)).filter((p) => p !== void 0);
|
|
@@ -137,9 +138,16 @@ function createAdapterThreadsStore(adapter, opts) {
|
|
|
137
138
|
const tokenAtIssue = errorToken;
|
|
138
139
|
recompute();
|
|
139
140
|
enqueue(i.threadIds, async () => {
|
|
141
|
+
const [soleThreadId] = i.threadIds;
|
|
142
|
+
if (!i.isCreate && i.threadIds.length === 1 && soleThreadId !== void 0 && failedCreateIds.has(soleThreadId)) {
|
|
143
|
+
drop(patch);
|
|
144
|
+
recompute();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
140
147
|
try {
|
|
141
148
|
await i.commit();
|
|
142
149
|
} catch (cause) {
|
|
150
|
+
if (i.isCreate) for (const threadId of i.threadIds) failedCreateIds.add(threadId);
|
|
143
151
|
drop(patch);
|
|
144
152
|
error = cause;
|
|
145
153
|
errorToken += 1;
|
|
@@ -201,6 +209,7 @@ function createAdapterThreadsStore(adapter, opts) {
|
|
|
201
209
|
mutate({
|
|
202
210
|
threadIds: [id],
|
|
203
211
|
apply: (threads) => threads.some((t) => t.id === id) ? [...threads] : [thread, ...threads],
|
|
212
|
+
isCreate: true,
|
|
204
213
|
commit: () => adapter.createThread({
|
|
205
214
|
id,
|
|
206
215
|
...createOpts?.meta !== void 0 ? { meta: createOpts.meta } : {}
|
|
@@ -337,7 +346,7 @@ function threadsStoreAdapterContract(makeAdapter) {
|
|
|
337
346
|
}
|
|
338
347
|
return [
|
|
339
348
|
define("createThread then listThreads returns the thread with empty defaults", async (a) => {
|
|
340
|
-
const id =
|
|
349
|
+
const id = mintThreadId();
|
|
341
350
|
await a.createThread({ id });
|
|
342
351
|
const threads = await a.listThreads();
|
|
343
352
|
const found = threads.filter((t) => t.id === id);
|
|
@@ -352,17 +361,17 @@ function threadsStoreAdapterContract(makeAdapter) {
|
|
|
352
361
|
assert(typeof thread.updatedAt === "number", "updatedAt must be a number");
|
|
353
362
|
}),
|
|
354
363
|
define("createThread persists meta", async (a) => {
|
|
355
|
-
const id =
|
|
364
|
+
const id = mintThreadId();
|
|
356
365
|
await a.createThread({ id, meta: { source: "contract" } });
|
|
357
366
|
const thread = await loadOrThrow(a, id);
|
|
358
367
|
assert(thread.meta?.["source"] === "contract", "meta did not round-trip");
|
|
359
368
|
}),
|
|
360
369
|
define("loadThread returns null for an unknown id", async (a) => {
|
|
361
|
-
const thread = await a.loadThread(
|
|
370
|
+
const thread = await a.loadThread(mintThreadId());
|
|
362
371
|
assert(thread === null, "loadThread must return null (not throw, not undefined) when absent");
|
|
363
372
|
}),
|
|
364
373
|
define("appendMessage is idempotent on message.id", async (a) => {
|
|
365
|
-
const id =
|
|
374
|
+
const id = mintThreadId();
|
|
366
375
|
await a.createThread({ id });
|
|
367
376
|
const msg = message("contract-msg-1");
|
|
368
377
|
await a.appendMessage({ threadId: id, message: msg });
|
|
@@ -375,7 +384,7 @@ function threadsStoreAdapterContract(makeAdapter) {
|
|
|
375
384
|
assert(thread.messages[0]?.id === "contract-msg-1", "the retained message has the wrong id");
|
|
376
385
|
}),
|
|
377
386
|
define("appendMessage preserves order for distinct ids", async (a) => {
|
|
378
|
-
const id =
|
|
387
|
+
const id = mintThreadId();
|
|
379
388
|
await a.createThread({ id });
|
|
380
389
|
await a.appendMessage({ threadId: id, message: message("contract-msg-1") });
|
|
381
390
|
await a.appendMessage({ threadId: id, message: message("contract-msg-2") });
|
|
@@ -386,7 +395,7 @@ function threadsStoreAdapterContract(makeAdapter) {
|
|
|
386
395
|
);
|
|
387
396
|
}),
|
|
388
397
|
define("setStatus round-trips", async (a) => {
|
|
389
|
-
const id =
|
|
398
|
+
const id = mintThreadId();
|
|
390
399
|
await a.createThread({ id });
|
|
391
400
|
await a.setStatus({ threadId: id, status: "streaming" });
|
|
392
401
|
assert((await loadOrThrow(a, id)).status === "streaming", "status did not become 'streaming'");
|
|
@@ -394,7 +403,7 @@ function threadsStoreAdapterContract(makeAdapter) {
|
|
|
394
403
|
assert((await loadOrThrow(a, id)).status === "error", "status did not become 'error'");
|
|
395
404
|
}),
|
|
396
405
|
define("setOutcome round-trips", async (a) => {
|
|
397
|
-
const id =
|
|
406
|
+
const id = mintThreadId();
|
|
398
407
|
await a.createThread({ id });
|
|
399
408
|
const outcome = { title: "Title", summary: "Summary", status: "done" };
|
|
400
409
|
await a.setOutcome({ threadId: id, outcome });
|
|
@@ -404,7 +413,7 @@ function threadsStoreAdapterContract(makeAdapter) {
|
|
|
404
413
|
assert(thread.outcome?.status === "done", "outcome.status did not round-trip");
|
|
405
414
|
}),
|
|
406
415
|
define("setResumeToken round-trips and clears with undefined", async (a) => {
|
|
407
|
-
const id =
|
|
416
|
+
const id = mintThreadId();
|
|
408
417
|
await a.createThread({ id });
|
|
409
418
|
await a.setResumeToken({ threadId: id, token: "tok-1" });
|
|
410
419
|
assert((await loadOrThrow(a, id)).resumeToken === "tok-1", "resumeToken did not round-trip");
|
|
@@ -415,14 +424,14 @@ function threadsStoreAdapterContract(makeAdapter) {
|
|
|
415
424
|
);
|
|
416
425
|
}),
|
|
417
426
|
define("markRead marks the thread read", async (a) => {
|
|
418
|
-
const id =
|
|
427
|
+
const id = mintThreadId();
|
|
419
428
|
await a.createThread({ id });
|
|
420
429
|
assert((await loadOrThrow(a, id)).read === false, "a new thread must start unread");
|
|
421
430
|
await a.markRead(id);
|
|
422
431
|
assert((await loadOrThrow(a, id)).read === true, "markRead did not set read");
|
|
423
432
|
}),
|
|
424
433
|
define("listThreads reflects a write that has already resolved", async (a) => {
|
|
425
|
-
const id =
|
|
434
|
+
const id = mintThreadId();
|
|
426
435
|
await a.createThread({ id });
|
|
427
436
|
await a.appendMessage({ threadId: id, message: message("contract-msg-raw") });
|
|
428
437
|
await a.setStatus({ threadId: id, status: "streaming" });
|
|
@@ -440,7 +449,7 @@ function threadsStoreAdapterContract(makeAdapter) {
|
|
|
440
449
|
define(
|
|
441
450
|
"dependent writes issued straight after createThread resolves are honoured",
|
|
442
451
|
async (a) => {
|
|
443
|
-
const id =
|
|
452
|
+
const id = mintThreadId();
|
|
444
453
|
await a.createThread({ id });
|
|
445
454
|
await a.markRead(id);
|
|
446
455
|
await a.appendMessage({ threadId: id, message: message("contract-msg-dep") });
|
|
@@ -455,7 +464,7 @@ function threadsStoreAdapterContract(makeAdapter) {
|
|
|
455
464
|
}
|
|
456
465
|
),
|
|
457
466
|
define("removeThread removes the thread", async (a) => {
|
|
458
|
-
const id =
|
|
467
|
+
const id = mintThreadId();
|
|
459
468
|
await a.createThread({ id });
|
|
460
469
|
await a.removeThread(id);
|
|
461
470
|
assert(await a.loadThread(id).then((t) => t === null), "loadThread must be null after remove");
|
|
@@ -463,7 +472,7 @@ function threadsStoreAdapterContract(makeAdapter) {
|
|
|
463
472
|
assert(!threads.some((t) => t.id === id), "listThreads still contains the removed thread");
|
|
464
473
|
}),
|
|
465
474
|
define("removeThread of an unknown id does not throw", async (a) => {
|
|
466
|
-
await a.removeThread(
|
|
475
|
+
await a.removeThread(mintThreadId());
|
|
467
476
|
})
|
|
468
477
|
];
|
|
469
478
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-sdk-transport.d.ts","sourceRoot":"","sources":["../../src/agent/ai-sdk-transport.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ai-sdk-transport.d.ts","sourceRoot":"","sources":["../../src/agent/ai-sdk-transport.ts"],"names":[],"mappings":"AAoDA,OAAO,KAAK,EAAE,SAAS,EAAkB,MAAM,SAAS,CAAA;AACxD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA;AAmB1D,MAAM,MAAM,qBAAqB,GAAG;IAClC,kFAAkF;IAClF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,+CAA+C;IAC/C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAA;IACnD,oFAAoF;IACpF,QAAQ,CAAC,WAAW,CAAC,EAAE,kBAAkB,CAAA;IACzC;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;CACzC,CAAA;AAID,MAAM,MAAM,cAAc,CAAC,KAAK,GAAG,SAAS,IAAI,uBAAuB,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG;IACvF;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,uBAAuB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;CAC/E,CAAA;AAwRD;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,KAAK,GAAG,SAAS,EAC9C,OAAO,EAAE,qBAAqB,GAC7B,cAAc,CAAC,KAAK,CAAC,CAkDvB"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { assertNever } from "../register.js";
|
|
2
|
+
import { mintMessageId, mintThreadId } from "./id.js";
|
|
2
3
|
import { TERMINAL_TOOL_STATES } from "./parts.js";
|
|
3
4
|
function createAiSdkResolver(options) {
|
|
4
5
|
let resolved;
|
|
@@ -161,7 +162,10 @@ function aiSdkTransport(options) {
|
|
|
161
162
|
yield { id: `${chatId}#start`, type: "start", runId: chatId, resumeToken: chatId };
|
|
162
163
|
const { httpTransport, readUIMessageStream: readStream } = await resolveAiSdk();
|
|
163
164
|
const userMessage = {
|
|
164
|
-
id
|
|
165
|
+
// This id is what AI SDK's own backend persistence/dedup keys turns hang off — the same
|
|
166
|
+
// idempotency-key cost as basalt's own ChatMessage.id (see mintMessageId's doc), so
|
|
167
|
+
// mintMessageId, not mintThreadId, is the right helper here.
|
|
168
|
+
id: mintMessageId(),
|
|
165
169
|
role: "user",
|
|
166
170
|
parts: [{ type: "text", text: input }]
|
|
167
171
|
};
|
|
@@ -182,7 +186,7 @@ function aiSdkTransport(options) {
|
|
|
182
186
|
}
|
|
183
187
|
};
|
|
184
188
|
}
|
|
185
|
-
const fixedChatId =
|
|
189
|
+
const fixedChatId = mintThreadId();
|
|
186
190
|
return {
|
|
187
191
|
...makeTransport(fixedChatId),
|
|
188
192
|
forThread: (chatId) => makeTransport(chatId)
|
package/dist/agent/id.d.ts
CHANGED
|
@@ -1,33 +1,81 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Id-minting helpers shared
|
|
3
|
-
*
|
|
4
|
-
* `
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* Id-minting helpers shared across the agent layer: `mintThreadId` (a low-stakes client-side id —
|
|
3
|
+
* threads, run/chat namespaces — used by `./thread`'s `createThreadsStore`, `./adapter`'s
|
|
4
|
+
* `createAdapterThreadsStore`/`threadsStoreAdapterContract`, `./use-agent-stream`'s per-send run
|
|
5
|
+
* id, and `aiSdkTransport`'s fixed chat id), `mintMessageId` (the higher-stakes id `appendMessage`
|
|
6
|
+
* treats as its only idempotency key), and `withPartIds` (stamps sequence ids onto streamed
|
|
7
|
+
* parts). Grouped here because they're the id-minting leaves every store/hook/transport in this
|
|
8
|
+
* layer sits above — not because they share a mechanism (`mintMessageId` deliberately diverges
|
|
9
|
+
* from `mintThreadId` on rung 3; see its own doc).
|
|
7
10
|
*/
|
|
8
11
|
/**
|
|
9
|
-
* Mints a client-side
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* Mints a client-side id for a LOW-COLLISION-COST use: a thread id, a per-send run-id namespace, a
|
|
13
|
+
* fixed chat id — anything minted client-side that is NOT the idempotency key some write depends
|
|
14
|
+
* on. `crypto.randomUUID()` exists only in SECURE CONTEXTS (HTTPS or localhost) — a consumer served
|
|
15
|
+
* over plain HTTP on a LAN hostname (a homelab dashboard on a bare IP, a staging box) would
|
|
16
|
+
* otherwise have `create()`, about the most basic user action a thread store has, throw a plain
|
|
17
|
+
* TypeError. A store used from a browser must not throw on a user gesture — the same principle
|
|
18
|
+
* already applied twice this release (`spliceText` clamps rather than throws, `coalesceParts`
|
|
19
|
+
* degrades rather than throws).
|
|
15
20
|
*
|
|
16
21
|
* Fallback chain, each rung reached only when the one above it is unavailable:
|
|
17
22
|
* 1. `crypto.randomUUID()` — cryptographically random, RFC 4122 UUID. The normal path.
|
|
18
23
|
* 2. `crypto.getRandomValues()` — still cryptographically random; hand-assembled into a UUIDv4
|
|
19
|
-
*
|
|
20
|
-
* formatted UUID).
|
|
24
|
+
* via `assembleUuidV4`.
|
|
21
25
|
* 3. No usable `crypto` at all (missing entirely, or missing both methods above — an old WebView,
|
|
22
26
|
* an SSR shim). NOT cryptographically random and NOT collision-resistant in general. This rung
|
|
23
|
-
* exists solely so
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* not this
|
|
27
|
-
*
|
|
28
|
-
* collision cost than that
|
|
27
|
+
* exists solely so a caller never throws on such a host. It is acceptable ONLY because an id
|
|
28
|
+
* minted here is minted a handful of times per client session and is not the idempotency key
|
|
29
|
+
* any write depends on — `ThreadsStoreAdapter.appendMessage`'s idempotency key is the MESSAGE
|
|
30
|
+
* id (see `mintMessageId`), not this one, so a collision on this rung costs a locally
|
|
31
|
+
* merged/duplicated thread (or two runs sharing a part-id namespace), not silent data loss on
|
|
32
|
+
* the append path. Do not reuse this rung for anything with a higher collision cost than that
|
|
33
|
+
* — see `mintMessageId` for the id that needs one.
|
|
29
34
|
*/
|
|
30
35
|
export declare function mintThreadId(): string;
|
|
36
|
+
/**
|
|
37
|
+
* Mints a message id — the value `ThreadsStoreAdapter.appendMessage` treats as its ONLY
|
|
38
|
+
* idempotency key (see that method's contract doc in `./adapter`). Shares `mintThreadId`'s rungs
|
|
39
|
+
* 1-2 (`crypto.randomUUID()`, then `crypto.getRandomValues()` assembled into a UUIDv4) — both are
|
|
40
|
+
* cryptographically random and collision-resistant, and rung 2 is the fix that actually matters in
|
|
41
|
+
* practice (a plain-HTTP/LAN host still has `getRandomValues`; only `randomUUID` is secure-context-
|
|
42
|
+
* gated).
|
|
43
|
+
*
|
|
44
|
+
* Rung 3 deliberately DIVERGES from `mintThreadId`: it THROWS instead of degrading to a
|
|
45
|
+
* non-random fallback. A thread id's rung-3 collision costs a locally merged/duplicated thread —
|
|
46
|
+
* annoying, recoverable, visible. A message id's collision cost is different IN KIND, not degree:
|
|
47
|
+
* `appendMessage` is idempotent on this id, so two messages that collide are not two rows, they
|
|
48
|
+
* are ONE — the second write silently no-ops and its content is gone, with nothing downstream able
|
|
49
|
+
* to tell. Degrading here would trade a loud, debuggable failure for silent, permanent data loss.
|
|
50
|
+
*
|
|
51
|
+
* This deliberately diverges from this layer's other standing rule — render-path code degrades,
|
|
52
|
+
* never throws (`spliceText` clamps, `coalesceParts` degrades, a consumer fence renderer must not
|
|
53
|
+
* take the message down) — because minting a message id is NOT render-path code. It runs once, in
|
|
54
|
+
* the write path that constructs a message before handing it to a store (`start()`,
|
|
55
|
+
* `consumeAndFinalize`'s finalize step, `finalizeStop`, `aiSdkTransport`'s per-turn user message),
|
|
56
|
+
* never inside the per-chunk accumulation/render loop those functions guard. A host with no usable
|
|
57
|
+
* `crypto` at all (missing both `randomUUID` and `getRandomValues`) is vanishingly rare — no
|
|
58
|
+
* runtime with a DOM/fetch surface worth targeting ships without a Crypto object — and an uncaught
|
|
59
|
+
* throw here is a loud, debuggable failure rather than a message silently vanishing with no trace,
|
|
60
|
+
* but WHERE it surfaces differs by call site (audited, not assumed — this paragraph previously
|
|
61
|
+
* claimed a single shared catch handled all of them, which was false):
|
|
62
|
+
* - `consumeAndFinalize`'s own assistantMessage mint runs inside that function's try/catch, so a
|
|
63
|
+
* throw there is correctly turned into `onFailureStatus` ('error' from `start()`, 'interrupted'
|
|
64
|
+
* from the mount-time resume) — a visibly stuck-then-settled run, as originally claimed.
|
|
65
|
+
* - `useAgentThreadRuns.start()`'s userMessage mint runs BEFORE any state is touched
|
|
66
|
+
* (`appendMessage`/`setStatus`/the controller registration all come after it), so a throw there
|
|
67
|
+
* is a true no-op that propagates synchronously out of `start()` itself — not a stuck run at
|
|
68
|
+
* all, since nothing ever started. See `UseAgentThreadRunsReturn.start`'s `@throws` doc.
|
|
69
|
+
* - `finalizeStop`'s stoppedMessage mint is wrapped in its own try/catch precisely so a throw
|
|
70
|
+
* here cannot wedge the thread at 'streaming' forever (stop() has already torn down this run's
|
|
71
|
+
* bookkeeping by the time finalizeStop runs, so a second stop() is always a no-op) — see
|
|
72
|
+
* `finalizeStop`'s doc for the guarded behavior and the resulting 'error' status.
|
|
73
|
+
* A stuck spinner is debuggable; silently discarded chat history is not — that half of the
|
|
74
|
+
* original claim stands regardless of which of the three paths above catches it.
|
|
75
|
+
*
|
|
76
|
+
* @throws {Error} when neither `crypto.randomUUID` nor `crypto.getRandomValues` is available.
|
|
77
|
+
*/
|
|
78
|
+
export declare function mintMessageId(): string;
|
|
31
79
|
/**
|
|
32
80
|
* withPartIds — stamps a stable id onto every draft part arriving without one.
|
|
33
81
|
*
|
package/dist/agent/id.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"id.d.ts","sourceRoot":"","sources":["../../src/agent/id.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"id.d.ts","sourceRoot":"","sources":["../../src/agent/id.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAmBH;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAQrC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAYtC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAuB,WAAW,CAAC,KAAK,SAAS;IAAE,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,EAC9D,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,cAAc,CAAC,KAAK,CAAC,GAC5B,cAAc,CAAC,KAAK,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC,CAUxC"}
|
package/dist/agent/id.js
CHANGED
|
@@ -1,18 +1,31 @@
|
|
|
1
|
+
function assembleUuidV4(bytes) {
|
|
2
|
+
const hex = Array.from({ length: 16 }, (_, i) => {
|
|
3
|
+
const raw = bytes.at(i) ?? 0;
|
|
4
|
+
const byte = i === 6 ? raw & 15 | 64 : i === 8 ? raw & 63 | 128 : raw;
|
|
5
|
+
return byte.toString(16).padStart(2, "0");
|
|
6
|
+
}).join("");
|
|
7
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
8
|
+
}
|
|
1
9
|
function mintThreadId() {
|
|
2
10
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
3
11
|
return crypto.randomUUID();
|
|
4
12
|
}
|
|
5
13
|
if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
|
|
6
|
-
|
|
7
|
-
const hex = Array.from({ length: 16 }, (_, i) => {
|
|
8
|
-
const raw = bytes.at(i) ?? 0;
|
|
9
|
-
const byte = i === 6 ? raw & 15 | 64 : i === 8 ? raw & 63 | 128 : raw;
|
|
10
|
-
return byte.toString(16).padStart(2, "0");
|
|
11
|
-
}).join("");
|
|
12
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
14
|
+
return assembleUuidV4(crypto.getRandomValues(new Uint8Array(16)));
|
|
13
15
|
}
|
|
14
16
|
return `id-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
15
17
|
}
|
|
18
|
+
function mintMessageId() {
|
|
19
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
20
|
+
return crypto.randomUUID();
|
|
21
|
+
}
|
|
22
|
+
if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
|
|
23
|
+
return assembleUuidV4(crypto.getRandomValues(new Uint8Array(16)));
|
|
24
|
+
}
|
|
25
|
+
throw new Error(
|
|
26
|
+
"mintMessageId: no usable crypto (both randomUUID and getRandomValues are unavailable) \u2014 refusing to mint a non-random message id, since ThreadsStoreAdapter.appendMessage treats this id as its only idempotency key and a collision would silently drop a message."
|
|
27
|
+
);
|
|
28
|
+
}
|
|
16
29
|
async function* withPartIds(runId, source) {
|
|
17
30
|
let n = 0;
|
|
18
31
|
for await (const part of source) {
|
|
@@ -25,6 +38,7 @@ async function* withPartIds(runId, source) {
|
|
|
25
38
|
}
|
|
26
39
|
}
|
|
27
40
|
export {
|
|
41
|
+
mintMessageId,
|
|
28
42
|
mintThreadId,
|
|
29
43
|
withPartIds
|
|
30
44
|
};
|
package/dist/agent/thread.d.ts
CHANGED
|
@@ -109,6 +109,10 @@ export type ThreadsStore<TPart = AgentPart> = {
|
|
|
109
109
|
* store built with `createAdapterThreadsStore` (./adapter), where it stays `false` until the
|
|
110
110
|
* first `listThreads` SUCCEEDS. Pair it with `error` — `!hydrated && error !== undefined` is a
|
|
111
111
|
* failed load, `!hydrated && error === undefined` is still loading.
|
|
112
|
+
*
|
|
113
|
+
* Gating an empty state on this is the CALLER's job when hand-assembling `ThreadFeed` /
|
|
114
|
+
* `ThreadDetailPanel`; the shipped `ThreadWorkspace` already does it internally, so an
|
|
115
|
+
* async store never flashes "no threads yet" through it before it actually knows.
|
|
112
116
|
*/
|
|
113
117
|
readonly hydrated: boolean;
|
|
114
118
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"thread.d.ts","sourceRoot":"","sources":["../../src/agent/thread.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"thread.d.ts","sourceRoot":"","sources":["../../src/agent/thread.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-agent-stream.d.ts","sourceRoot":"","sources":["../../src/agent/use-agent-stream.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACxC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAIjD,iDAAiD;AACjD,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,WAAW,GAAG,MAAM,GAAG,OAAO,CAAA;AAIlE,MAAM,MAAM,oBAAoB,CAAC,KAAK,EAAE,MAAM,IAAI;IAChD,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,CAAA;IACvB,gCAAgC;IAChC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAA;IAC7B,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/C,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,EAAE,MAAM,IAAI,CAAA;IACzB,kEAAkE;IAClE,QAAQ,CAAC,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CACzC,CAAA;AAID;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,KAAK,SAAS,QAAQ,GAAG,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,EAClF,SAAS,GACV,EAAE;IACD,SAAS,EAAE,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;CACzC,GAAG,oBAAoB,CAAC,KAAK,EAAE,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"use-agent-stream.d.ts","sourceRoot":"","sources":["../../src/agent/use-agent-stream.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACxC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAIjD,iDAAiD;AACjD,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,WAAW,GAAG,MAAM,GAAG,OAAO,CAAA;AAIlE,MAAM,MAAM,oBAAoB,CAAC,KAAK,EAAE,MAAM,IAAI;IAChD,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,CAAA;IACvB,gCAAgC;IAChC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAA;IAC7B,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/C,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,EAAE,MAAM,IAAI,CAAA;IACzB,kEAAkE;IAClE,QAAQ,CAAC,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CACzC,CAAA;AAID;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,KAAK,SAAS,QAAQ,GAAG,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,EAClF,SAAS,GACV,EAAE;IACD,SAAS,EAAE,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;CACzC,GAAG,oBAAoB,CAAC,KAAK,EAAE,MAAM,CAAC,CA6EtC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
-
import { withPartIds } from "./id.js";
|
|
2
|
+
import { mintThreadId, withPartIds } from "./id.js";
|
|
3
3
|
import { isStartPart } from "./parts.js";
|
|
4
4
|
import { mergePart } from "./merge.js";
|
|
5
5
|
function useAgentStream({
|
|
@@ -25,7 +25,7 @@ function useAgentStream({
|
|
|
25
25
|
setParts([]);
|
|
26
26
|
setError(void 0);
|
|
27
27
|
setStatus("streaming");
|
|
28
|
-
const runId =
|
|
28
|
+
const runId = mintThreadId();
|
|
29
29
|
try {
|
|
30
30
|
for await (const part of withPartIds(runId, transport.stream(input, controller.signal))) {
|
|
31
31
|
if (controllerRef.current !== controller) return;
|
|
@@ -46,18 +46,39 @@ export type UseAgentThreadRunsArgs<TPart = AgentPart> = {
|
|
|
46
46
|
export type UseAgentThreadRunsReturn<TPart = AgentPart> = {
|
|
47
47
|
/** Live stream state per thread id, for threads with a run in progress or just completed. */
|
|
48
48
|
readonly runs: ReadonlyMap<string, ThreadRunState<TPart>>;
|
|
49
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Start a new turn on `threadId`. No-op if that thread already has a stream in flight.
|
|
51
|
+
*
|
|
52
|
+
* @throws {Error} synchronously, before any state is touched, on a host with no usable
|
|
53
|
+
* `crypto` at all (see `mintMessageId` in `./id.ts` — the id it mints here is the new user
|
|
54
|
+
* ChatMessage's idempotency key, so it degrades to a throw rather than a silent collision).
|
|
55
|
+
* This throw is a true no-op: it happens before `appendMessage`/`setStatus`/the run's
|
|
56
|
+
* controller are registered, so nothing is left half-started — the thread is exactly as it was
|
|
57
|
+
* before the call. A caller invoking `start` from an event handler on a target that must
|
|
58
|
+
* tolerate this (very old WebViews, non-DOM SSR shims) should wrap the call in its own
|
|
59
|
+
* try/catch; this rung is vanishingly rare in practice (see `mintMessageId`'s own doc).
|
|
60
|
+
*/
|
|
50
61
|
readonly start: (threadId: string, input: string) => void;
|
|
51
62
|
/**
|
|
52
63
|
* Replay the last user input sent on `threadId` (same code path as `start`). No-op if
|
|
53
64
|
* `threadId` has never had a turn started, or already has one in flight.
|
|
65
|
+
*
|
|
66
|
+
* @throws {Error} same condition and same true-no-op guarantee as `start` — see its doc.
|
|
54
67
|
*/
|
|
55
68
|
readonly retry: (threadId: string) => void;
|
|
56
69
|
/**
|
|
57
|
-
* Abort the in-flight stream for `threadId` (no-op if
|
|
58
|
-
*
|
|
70
|
+
* Abort the in-flight stream for `threadId` (true no-op only if BOTH `controllersRef` and
|
|
71
|
+
* `runs` have nothing for it — i.e. the thread genuinely isn't live). Preserves whatever content
|
|
72
|
+
* had already arrived: if the run had accumulated any parts, they're persisted as an assistant
|
|
59
73
|
* ChatMessage with `finish: 'stopped'` (unless that message already landed — see finalizeStop),
|
|
60
74
|
* then distilled into an outcome and the thread is settled to 'done'.
|
|
75
|
+
*
|
|
76
|
+
* Defense-in-depth: if `runs` reports `threadId` as `'streaming'` but no controller is
|
|
77
|
+
* registered for it (a phantom entry — not reachable via any path in this hook today, since the
|
|
78
|
+
* unmount-cleanup effect and `finalizeStop` both tear down `controllersRef` and `runs` together,
|
|
79
|
+
* but guarded against here so nothing in this file can EVER wedge a thread the UI shows as live
|
|
80
|
+
* with no way to reach a terminal state), `stop()` still settles it instead of silently
|
|
81
|
+
* no-opping forever.
|
|
61
82
|
*/
|
|
62
83
|
readonly stop: (threadId: string) => void;
|
|
63
84
|
/** Abort every in-flight stream across all threads, settling each the same way stop() would. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-agent-thread-runs.d.ts","sourceRoot":"","sources":["../../src/agent/use-agent-thread-runs.ts"],"names":[],"mappings":"AA4CA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AACvC,OAAO,KAAK,EAAgB,eAAe,EAAE,MAAM,WAAW,CAAA;AAE9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACxC,OAAO,KAAK,EAA6B,YAAY,EAAE,MAAM,UAAU,CAAA;AAEvE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAIjD;;;;;;;;;;GAUG;AACH,MAAM,MAAM,cAAc,CAAC,KAAK,GAAG,SAAS,IAAI;IAC9C,yFAAyF;IACzF,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;IAC5B,iEAAiE;IACjE,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,CAAA;CACxB,CAAA;AAID,MAAM,MAAM,sBAAsB,CAAC,KAAK,GAAG,SAAS,IAAI;IACtD;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,SAAS,EACd,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,GAC7B,CAAC,CAAC,QAAQ,EAAE,MAAM,KAAK,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAA;IACzD,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC,CAAA;IACnC,uFAAuF;IACvF,QAAQ,CAAC,cAAc,EAAE,eAAe,CAAC,KAAK,CAAC,CAAA;IAC/C;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,KAAK,EAAE,CAAA;CAClD,CAAA;AAID,MAAM,MAAM,wBAAwB,CAAC,KAAK,GAAG,SAAS,IAAI;IACxD,6FAA6F;IAC7F,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC,CAAA;IACzD
|
|
1
|
+
{"version":3,"file":"use-agent-thread-runs.d.ts","sourceRoot":"","sources":["../../src/agent/use-agent-thread-runs.ts"],"names":[],"mappings":"AA4CA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AACvC,OAAO,KAAK,EAAgB,eAAe,EAAE,MAAM,WAAW,CAAA;AAE9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACxC,OAAO,KAAK,EAA6B,YAAY,EAAE,MAAM,UAAU,CAAA;AAEvE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAIjD;;;;;;;;;;GAUG;AACH,MAAM,MAAM,cAAc,CAAC,KAAK,GAAG,SAAS,IAAI;IAC9C,yFAAyF;IACzF,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;IAC5B,iEAAiE;IACjE,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,CAAA;CACxB,CAAA;AAID,MAAM,MAAM,sBAAsB,CAAC,KAAK,GAAG,SAAS,IAAI;IACtD;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,SAAS,EACd,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,GAC7B,CAAC,CAAC,QAAQ,EAAE,MAAM,KAAK,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAA;IACzD,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC,CAAA;IACnC,uFAAuF;IACvF,QAAQ,CAAC,cAAc,EAAE,eAAe,CAAC,KAAK,CAAC,CAAA;IAC/C;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,KAAK,EAAE,CAAA;CAClD,CAAA;AAID,MAAM,MAAM,wBAAwB,CAAC,KAAK,GAAG,SAAS,IAAI;IACxD,6FAA6F;IAC7F,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC,CAAA;IACzD;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACzD;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IAC1C;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IACzC,gGAAgG;IAChG,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAC7B,CAAA;AAuRD;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,SAAS,QAAQ,GAAG,SAAS,EAAE,EACrE,SAAS,EACT,KAAK,EACL,cAAc,EACd,WAAW,GACZ,EAAE,sBAAsB,CAAC,KAAK,CAAC,GAAG,wBAAwB,CAAC,KAAK,CAAC,CAgSjE"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
-
import { withPartIds } from "./id.js";
|
|
2
|
+
import { mintMessageId, mintThreadId, withPartIds } from "./id.js";
|
|
3
3
|
import { mergePart } from "./merge.js";
|
|
4
4
|
import { isStartPart } from "./parts.js";
|
|
5
5
|
import { isResumable } from "./transport.js";
|
|
6
6
|
function defaultToUserParts(input) {
|
|
7
|
-
return [{ id:
|
|
7
|
+
return [{ id: mintThreadId(), type: "text", text: input }];
|
|
8
8
|
}
|
|
9
9
|
function buildDoneSnapshot(priorThread, threadId, userMessage, assistantMessage) {
|
|
10
10
|
const updatedAt = Date.now();
|
|
@@ -59,7 +59,9 @@ async function consumeAndFinalize(args) {
|
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
61
61
|
const assistantMessage = {
|
|
62
|
-
|
|
62
|
+
// appendMessage's idempotency key — mintMessageId, never mintThreadId (see that helper's
|
|
63
|
+
// doc): a collided id here would silently drop this message, not merge a duplicate thread.
|
|
64
|
+
id: mintMessageId(),
|
|
63
65
|
role: "assistant",
|
|
64
66
|
parts,
|
|
65
67
|
createdAt: Date.now(),
|
|
@@ -101,24 +103,43 @@ async function consumeAndFinalize(args) {
|
|
|
101
103
|
}
|
|
102
104
|
async function finalizeStop(args) {
|
|
103
105
|
const { threadId, parts, alreadyAppended, storeRef, resolveOutcome, setRuns } = args;
|
|
106
|
+
let appendFailed = false;
|
|
104
107
|
if (!alreadyAppended && parts.length > 0) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
try {
|
|
109
|
+
const stoppedMessage = {
|
|
110
|
+
// Same idempotency-key reasoning as consumeAndFinalize's assistantMessage — mintMessageId.
|
|
111
|
+
// This can throw on rung 3 (no usable crypto — see ./id.ts) just like appendMessage
|
|
112
|
+
// (consumer code) can throw for its own reasons; both are caught below so neither can
|
|
113
|
+
// wedge the thread — see this function's doc.
|
|
114
|
+
id: mintMessageId(),
|
|
115
|
+
role: "assistant",
|
|
116
|
+
parts,
|
|
117
|
+
createdAt: Date.now(),
|
|
118
|
+
finish: "stopped"
|
|
119
|
+
};
|
|
120
|
+
storeRef.current.appendMessage(threadId, stoppedMessage);
|
|
121
|
+
} catch {
|
|
122
|
+
appendFailed = true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
let settleFailed = appendFailed;
|
|
126
|
+
try {
|
|
127
|
+
storeRef.current.setStatus(threadId, appendFailed ? "error" : "done");
|
|
128
|
+
} catch {
|
|
129
|
+
settleFailed = true;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
storeRef.current.setResumeToken(threadId, void 0);
|
|
133
|
+
} catch {
|
|
134
|
+
settleFailed = true;
|
|
113
135
|
}
|
|
114
|
-
storeRef.current.setStatus(threadId, "done");
|
|
115
|
-
storeRef.current.setResumeToken(threadId, void 0);
|
|
116
136
|
setRuns((prev) => {
|
|
117
137
|
if (!prev.has(threadId)) return prev;
|
|
118
138
|
const next = new Map(prev);
|
|
119
139
|
next.delete(threadId);
|
|
120
140
|
return next;
|
|
121
141
|
});
|
|
142
|
+
if (settleFailed) return;
|
|
122
143
|
const snapshot = storeRef.current.threads.find((thread) => thread.id === threadId);
|
|
123
144
|
if (snapshot === void 0) return;
|
|
124
145
|
const outcome = await resolveOutcome(snapshot);
|
|
@@ -155,9 +176,20 @@ function useAgentThreadRuns({
|
|
|
155
176
|
};
|
|
156
177
|
useEffect(
|
|
157
178
|
() => () => {
|
|
179
|
+
const abortedThreadIds = Array.from(controllersRef.current.keys());
|
|
158
180
|
controllersRef.current.forEach((controller) => controller.abort());
|
|
159
181
|
controllersRef.current.clear();
|
|
160
182
|
appendedRef.current.clear();
|
|
183
|
+
if (abortedThreadIds.length === 0) return;
|
|
184
|
+
setRuns((prev) => {
|
|
185
|
+
let next;
|
|
186
|
+
for (const threadId of abortedThreadIds) {
|
|
187
|
+
if (!prev.has(threadId)) continue;
|
|
188
|
+
next ??= new Map(prev);
|
|
189
|
+
next.delete(threadId);
|
|
190
|
+
}
|
|
191
|
+
return next ?? prev;
|
|
192
|
+
});
|
|
161
193
|
},
|
|
162
194
|
[]
|
|
163
195
|
);
|
|
@@ -206,7 +238,8 @@ function useAgentThreadRuns({
|
|
|
206
238
|
if (controllersRef.current.has(threadId)) return;
|
|
207
239
|
lastInputRef.current.set(threadId, input);
|
|
208
240
|
const userMessage = {
|
|
209
|
-
|
|
241
|
+
// Same idempotency-key reasoning as consumeAndFinalize's assistantMessage — mintMessageId.
|
|
242
|
+
id: mintMessageId(),
|
|
210
243
|
role: "user",
|
|
211
244
|
parts: toUserPartsRef.current(input),
|
|
212
245
|
createdAt: Date.now()
|
|
@@ -248,7 +281,14 @@ function useAgentThreadRuns({
|
|
|
248
281
|
const stop = useCallback(
|
|
249
282
|
(threadId) => {
|
|
250
283
|
const controller = controllersRef.current.get(threadId);
|
|
251
|
-
if (controller === void 0)
|
|
284
|
+
if (controller === void 0) {
|
|
285
|
+
if (!runsRef.current.has(threadId)) return;
|
|
286
|
+
const parts2 = runsRef.current.get(threadId)?.parts ?? [];
|
|
287
|
+
const alreadyAppended2 = appendedRef.current.has(threadId);
|
|
288
|
+
appendedRef.current.delete(threadId);
|
|
289
|
+
void finalizeStop({ threadId, parts: parts2, alreadyAppended: alreadyAppended2, storeRef, resolveOutcome, setRuns });
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
252
292
|
controllersRef.current.delete(threadId);
|
|
253
293
|
const parts = runsRef.current.get(threadId)?.parts ?? [];
|
|
254
294
|
controller.abort();
|
|
@@ -14,6 +14,10 @@ export { ThreadWorkspace } from './thread-workspace.js';
|
|
|
14
14
|
export type { ThreadWorkspaceProps } from './thread-workspace.js';
|
|
15
15
|
export { ThreadFeed } from './thread-feed.js';
|
|
16
16
|
export type { ThreadFeedProps } from './thread-feed.js';
|
|
17
|
+
export { ThreadFeedRow } from './thread-feed-row.js';
|
|
18
|
+
export type { ThreadFeedRowProps } from './thread-feed-row.js';
|
|
19
|
+
export type { MessageAffordances } from './message-affordances.js';
|
|
20
|
+
export type { VirtualizeOptions, VirtualizeProps } from './virtualize.js';
|
|
17
21
|
export { ThreadOutcomeCard } from './thread-outcome-card.js';
|
|
18
22
|
export type { ThreadOutcomeCardProps } from './thread-outcome-card.js';
|
|
19
23
|
export { ThreadDetailPanel } from './thread-detail-panel.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent-chat/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAG9D,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC1C,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAGpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AACzD,YAAY,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAGnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AACzD,YAAY,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAGnE,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AACrC,YAAY,EAAE,kBAAkB,EAAE,cAAc,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAGnG,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACxE,YAAY,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AAG7D,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACtC,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent-chat/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAG9D,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC1C,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAGpD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACjD,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAO3D,YAAY,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAC/D,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAGtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AACzD,YAAY,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAGnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AACzD,YAAY,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAGnE,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AACrC,YAAY,EAAE,kBAAkB,EAAE,cAAc,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAGnG,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACxE,YAAY,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AAG7D,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACtC,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA"}
|
package/dist/agent-chat/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ThreadWorkspace } from "./thread-workspace.js";
|
|
2
2
|
import { ThreadFeed } from "./thread-feed.js";
|
|
3
|
+
import { ThreadFeedRow } from "./thread-feed-row.js";
|
|
3
4
|
import { ThreadOutcomeCard } from "./thread-outcome-card.js";
|
|
4
5
|
import { ThreadDetailPanel } from "./thread-detail-panel.js";
|
|
5
6
|
import { Composer } from "./composer.js";
|
|
@@ -9,6 +10,7 @@ export {
|
|
|
9
10
|
Composer,
|
|
10
11
|
ThreadDetailPanel,
|
|
11
12
|
ThreadFeed,
|
|
13
|
+
ThreadFeedRow,
|
|
12
14
|
ThreadOutcomeCard,
|
|
13
15
|
ThreadTranscript,
|
|
14
16
|
ThreadWorkspace,
|