basalt-ui 1.9.0 → 1.11.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.
Files changed (104) hide show
  1. package/AGENTS.md +2 -1
  2. package/CHANGELOG.md +18 -0
  3. package/README.md +64 -34
  4. package/agent/rules/basalt-agent.md +49 -6
  5. package/configs/oxlint-plugin.js +279 -0
  6. package/configs/oxlint-plugin.test.ts +216 -1
  7. package/configs/oxlint.json +5 -2
  8. package/dist/agent/ai-sdk-transport.d.ts +3 -52
  9. package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
  10. package/dist/agent/ai-sdk-transport.js +108 -24
  11. package/dist/agent/coalesce.d.ts +26 -0
  12. package/dist/agent/coalesce.d.ts.map +1 -0
  13. package/dist/agent/coalesce.js +69 -0
  14. package/dist/agent/foreign.d.ts +97 -0
  15. package/dist/agent/foreign.d.ts.map +1 -0
  16. package/dist/agent/foreign.js +18 -0
  17. package/dist/agent/history.d.ts +2 -0
  18. package/dist/agent/history.d.ts.map +1 -1
  19. package/dist/agent/id.d.ts +35 -0
  20. package/dist/agent/id.d.ts.map +1 -0
  21. package/dist/agent/id.js +14 -0
  22. package/dist/agent/index.d.ts +9 -4
  23. package/dist/agent/index.d.ts.map +1 -1
  24. package/dist/agent/index.js +15 -3
  25. package/dist/agent/merge.d.ts +8 -0
  26. package/dist/agent/merge.d.ts.map +1 -0
  27. package/dist/agent/merge.js +23 -0
  28. package/dist/agent/part-list.d.ts.map +1 -1
  29. package/dist/agent/part-list.js +16 -6
  30. package/dist/agent/parts.d.ts +100 -21
  31. package/dist/agent/parts.d.ts.map +1 -1
  32. package/dist/agent/parts.js +128 -11
  33. package/dist/agent/transport.d.ts +39 -3
  34. package/dist/agent/transport.d.ts.map +1 -1
  35. package/dist/agent/transport.js +20 -17
  36. package/dist/agent/use-agent-stream.d.ts +2 -1
  37. package/dist/agent/use-agent-stream.d.ts.map +1 -1
  38. package/dist/agent/use-agent-stream.js +5 -2
  39. package/dist/agent/use-agent-thread-runs.d.ts +9 -3
  40. package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
  41. package/dist/agent/use-agent-thread-runs.js +76 -23
  42. package/dist/agent-chat/index.d.ts +6 -3
  43. package/dist/agent-chat/index.d.ts.map +1 -1
  44. package/dist/agent-chat/index.js +2 -0
  45. package/dist/agent-chat/thread-message.d.ts +24 -7
  46. package/dist/agent-chat/thread-message.d.ts.map +1 -1
  47. package/dist/agent-chat/thread-message.js +102 -43
  48. package/dist/agent-chat/tool-chip.d.ts +21 -0
  49. package/dist/agent-chat/tool-chip.d.ts.map +1 -0
  50. package/dist/agent-chat/tool-chip.js +120 -0
  51. package/dist/cli/index.d.ts +33 -0
  52. package/dist/cli/index.d.ts.map +1 -1
  53. package/dist/cli/index.js +131 -0
  54. package/dist/guard/index.d.ts.map +1 -1
  55. package/dist/guard/index.js +1 -3
  56. package/dist/index.d.ts +2 -2
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +3 -1
  59. package/dist/surfaces.d.ts +11 -1
  60. package/dist/surfaces.d.ts.map +1 -1
  61. package/dist/surfaces.js +22 -1
  62. package/llms.txt +10 -3
  63. package/package.json +6 -1
  64. package/src/agent/ai-sdk-transport.test.ts +567 -0
  65. package/src/agent/ai-sdk-transport.ts +206 -53
  66. package/src/agent/coalesce.test.ts +194 -0
  67. package/src/agent/coalesce.ts +118 -0
  68. package/src/agent/foreign.test.ts +47 -0
  69. package/src/agent/foreign.ts +140 -0
  70. package/src/agent/history.ts +2 -0
  71. package/src/agent/id.test.ts +71 -0
  72. package/src/agent/id.ts +43 -0
  73. package/src/agent/index.ts +26 -3
  74. package/src/agent/merge.test.ts +119 -0
  75. package/src/agent/merge.ts +67 -0
  76. package/src/agent/part-list.test.tsx +82 -0
  77. package/src/agent/part-list.tsx +31 -6
  78. package/src/agent/parts.test.ts +603 -0
  79. package/src/agent/parts.ts +262 -32
  80. package/src/agent/thread.test.ts +147 -0
  81. package/src/agent/transport.test.ts +70 -0
  82. package/src/agent/transport.ts +72 -20
  83. package/src/agent/use-agent-stream.test.tsx +261 -0
  84. package/src/agent/use-agent-stream.ts +13 -3
  85. package/src/agent/use-agent-thread-runs.foreign.test.tsx +133 -0
  86. package/src/agent/use-agent-thread-runs.resume.test.tsx +289 -0
  87. package/src/agent/use-agent-thread-runs.test.tsx +781 -0
  88. package/src/agent/use-agent-thread-runs.ts +177 -27
  89. package/src/agent/use-agent-thread-runs.wedge.test.tsx +301 -0
  90. package/src/agent-chat/index.ts +8 -3
  91. package/src/agent-chat/thread-message.test.tsx +211 -0
  92. package/src/agent-chat/thread-message.tsx +235 -62
  93. package/src/agent-chat/tool-chip.test.tsx +139 -0
  94. package/src/agent-chat/tool-chip.tsx +219 -0
  95. package/src/charts/primitives/ChartFrame.test.tsx +8 -5
  96. package/src/cli/index.ts +285 -0
  97. package/src/dashboard/stat-card.test.tsx +5 -3
  98. package/src/guard/check-source.test.ts +8 -4
  99. package/src/guard/index.ts +1 -3
  100. package/src/index.ts +2 -0
  101. package/src/provider/index.test.tsx +6 -5
  102. package/src/surfaces.ts +23 -1
  103. package/src/theme/use-basalt-spacing.test.tsx +7 -4
  104. package/src/tokens/build-fonts-css.test.ts +3 -2
@@ -1,49 +1,166 @@
1
+ const TERMINAL_TOOL_STATES = ["output-available", "output-error", "output-denied"];
2
+ function isToolCallSettled(part) {
3
+ return TERMINAL_TOOL_STATES.includes(part.state);
4
+ }
1
5
  function parseAgentPart(raw) {
2
6
  if (raw === null || typeof raw !== "object") return null;
3
7
  const obj = raw;
4
8
  if (typeof obj["type"] !== "string") return null;
9
+ if (typeof obj["id"] !== "string") return null;
10
+ const id = obj["id"];
5
11
  switch (obj["type"]) {
6
12
  case "start":
7
13
  if (typeof obj["runId"] !== "string") return null;
8
14
  return {
15
+ id,
9
16
  type: "start",
10
17
  runId: obj["runId"],
11
18
  ...typeof obj["resumeToken"] === "string" ? { resumeToken: obj["resumeToken"] } : {}
12
19
  };
13
20
  case "text":
14
21
  if (typeof obj["text"] !== "string") return null;
15
- return { type: "text", text: obj["text"] };
22
+ return {
23
+ id,
24
+ type: "text",
25
+ text: obj["text"],
26
+ ...typeof obj["offset"] === "number" ? { offset: obj["offset"] } : {}
27
+ };
16
28
  case "reasoning":
17
29
  if (typeof obj["text"] !== "string") return null;
18
- return { type: "reasoning", text: obj["text"] };
19
- case "tool":
20
- if (typeof obj["toolName"] !== "string") return null;
21
- if (!("input" in obj)) return null;
22
30
  return {
23
- type: "tool",
24
- toolName: obj["toolName"],
25
- input: obj["input"],
26
- ...obj["output"] !== void 0 ? { output: obj["output"] } : {},
27
- ...typeof obj["toolCallId"] === "string" ? { toolCallId: obj["toolCallId"] } : {}
31
+ id,
32
+ type: "reasoning",
33
+ text: obj["text"],
34
+ ...typeof obj["offset"] === "number" ? { offset: obj["offset"] } : {}
28
35
  };
36
+ case "tool":
37
+ return parseToolCallPart(id, obj);
29
38
  case "source":
30
39
  if (typeof obj["url"] !== "string") return null;
31
40
  return {
41
+ id,
32
42
  type: "source",
33
43
  url: obj["url"],
34
44
  ...typeof obj["title"] === "string" ? { title: obj["title"] } : {}
35
45
  };
36
46
  case "error":
37
47
  if (typeof obj["message"] !== "string") return null;
38
- return { type: "error", message: obj["message"] };
48
+ return { id, type: "error", message: obj["message"] };
39
49
  default:
40
50
  return null;
41
51
  }
42
52
  }
53
+ function parseToolCallPart(id, obj) {
54
+ if (typeof obj["toolCallId"] !== "string") return null;
55
+ if (typeof obj["toolName"] !== "string") return null;
56
+ if (typeof obj["state"] !== "string") return null;
57
+ const { toolCallId, toolName } = obj;
58
+ const common = {
59
+ id,
60
+ type: "tool",
61
+ toolCallId,
62
+ toolName,
63
+ ...typeof obj["durationMs"] === "number" ? { durationMs: obj["durationMs"] } : {},
64
+ ...typeof obj["providerExecuted"] === "boolean" ? { providerExecuted: obj["providerExecuted"] } : {}
65
+ };
66
+ switch (obj["state"]) {
67
+ case "input-streaming":
68
+ return {
69
+ ...common,
70
+ state: "input-streaming",
71
+ ..."input" in obj ? { input: obj["input"] } : {}
72
+ };
73
+ case "input-available":
74
+ if (!("input" in obj)) return null;
75
+ return { ...common, state: "input-available", input: obj["input"] };
76
+ case "approval-requested": {
77
+ if (!("input" in obj)) return null;
78
+ const approval = parseApproval(obj["approval"]);
79
+ if (approval === null || approval.approved !== void 0 || approval.reason !== void 0) {
80
+ return null;
81
+ }
82
+ return {
83
+ ...common,
84
+ state: "approval-requested",
85
+ input: obj["input"],
86
+ approval
87
+ };
88
+ }
89
+ case "approval-responded": {
90
+ if (!("input" in obj)) return null;
91
+ const approval = parseApproval(obj["approval"]);
92
+ if (approval === null || typeof approval.approved !== "boolean") return null;
93
+ return {
94
+ ...common,
95
+ state: "approval-responded",
96
+ input: obj["input"],
97
+ approval
98
+ };
99
+ }
100
+ case "output-denied": {
101
+ if (!("input" in obj)) return null;
102
+ const approval = parseApproval(obj["approval"]);
103
+ if (approval === null || approval.approved !== false) return null;
104
+ return {
105
+ ...common,
106
+ state: "output-denied",
107
+ input: obj["input"],
108
+ approval
109
+ };
110
+ }
111
+ case "output-available": {
112
+ if (!("input" in obj)) return null;
113
+ if (!("output" in obj)) return null;
114
+ const approval = obj["approval"] === void 0 ? null : parseApproval(obj["approval"]);
115
+ if (obj["approval"] !== void 0 && (approval === null || approval.approved === false)) {
116
+ return null;
117
+ }
118
+ return {
119
+ ...common,
120
+ state: "output-available",
121
+ input: obj["input"],
122
+ output: obj["output"],
123
+ ...typeof obj["preliminary"] === "boolean" ? { preliminary: obj["preliminary"] } : {},
124
+ ...approval !== null ? { approval } : {}
125
+ };
126
+ }
127
+ case "output-error": {
128
+ if (typeof obj["errorText"] !== "string") return null;
129
+ const approval = obj["approval"] === void 0 ? null : parseApproval(obj["approval"]);
130
+ if (obj["approval"] !== void 0 && (approval === null || approval.approved === false)) {
131
+ return null;
132
+ }
133
+ return {
134
+ ...common,
135
+ state: "output-error",
136
+ errorText: obj["errorText"],
137
+ ..."input" in obj ? { input: obj["input"] } : {},
138
+ ..."rawInput" in obj ? { rawInput: obj["rawInput"] } : {},
139
+ ...approval !== null ? { approval } : {}
140
+ };
141
+ }
142
+ default:
143
+ return null;
144
+ }
145
+ }
146
+ function parseApproval(raw) {
147
+ if (raw === null || typeof raw !== "object") return null;
148
+ const obj = raw;
149
+ if (typeof obj["id"] !== "string") return null;
150
+ return {
151
+ id: obj["id"],
152
+ ...typeof obj["approved"] === "boolean" ? { approved: obj["approved"] } : {},
153
+ ...typeof obj["reason"] === "string" ? { reason: obj["reason"] } : {},
154
+ ...typeof obj["isAutomatic"] === "boolean" ? { isAutomatic: obj["isAutomatic"] } : {},
155
+ ...typeof obj["signature"] === "string" ? { signature: obj["signature"] } : {}
156
+ };
157
+ }
43
158
  function isStartPart(part) {
44
159
  return typeof part === "object" && part !== null && part.type === "start";
45
160
  }
46
161
  export {
162
+ TERMINAL_TOOL_STATES,
47
163
  isStartPart,
164
+ isToolCallSettled,
48
165
  parseAgentPart
49
166
  };
@@ -29,11 +29,16 @@
29
29
  * api.chat.post({ body: { message: input }, fetch: { signal } }),
30
30
  * )
31
31
  */
32
- import type { AgentPart } from './parts.js';
32
+ import type { AgentPart, AgentPartDraft } from './parts.js';
33
33
  /**
34
34
  * The injected transport seam. `stream(input, signal)` returns an async generator of parts.
35
35
  * Implement this interface for any backend — Eden, fetch-based SSE, AI SDK, or a mock.
36
36
  *
37
+ * The default `TPart` is `AgentPartDraft` (not `AgentPart`): transports yield DRAFTS — `id`
38
+ * optional — and the consuming hooks (or `withPartIds`) normalize a draft into a fully-identified
39
+ * part before it is ever accumulated. Pass an explicit `AgentPart` generic when a transport mints
40
+ * its own ids directly (as `aiSdkTransport` does).
41
+ *
37
42
  * @example
38
43
  * // Minimal mock transport for tests / playground:
39
44
  * const mockTransport: AgentTransport = {
@@ -42,11 +47,33 @@ import type { AgentPart } from './parts.js';
42
47
  * },
43
48
  * }
44
49
  */
45
- export type AgentTransport<TPart = AgentPart, TInput = string> = {
50
+ export type AgentTransport<TPart = AgentPartDraft, TInput = string> = {
46
51
  stream: (input: TInput, signal?: AbortSignal) => AsyncGenerator<TPart>;
47
52
  /** Optional: resume a previously-started run from its resumeToken (see StartPart). Transports that don't support resumption simply omit this. */
48
53
  resume?: (resumeToken: string, signal?: AbortSignal) => AsyncGenerator<TPart>;
49
54
  };
55
+ /**
56
+ * A transport whose replay is safe to run over an already-rendered turn. Implementing `resume`
57
+ * alone is no longer enough: `idempotentReplay: true` is a literal-typed assertion that every
58
+ * text/reasoning part this transport emits carries an authoritative `offset`, so mergePart
59
+ * rewrites instead of appending.
60
+ */
61
+ export type ResumableAgentTransport<TPart = AgentPartDraft, TInput = string> = AgentTransport<TPart, TInput> & {
62
+ readonly resume: (resumeToken: string, signal?: AbortSignal) => AsyncGenerator<TPart>;
63
+ readonly idempotentReplay: true;
64
+ };
65
+ /**
66
+ * Type guard: true when `t` both implements `resume` AND has explicitly asserted
67
+ * `idempotentReplay: true`. A transport with `resume` but no `idempotentReplay` is NOT resumable
68
+ * — the mount-time reconcile in `useAgentThreadRuns` falls back to `'interrupted'` for it rather
69
+ * than risk duplicating already-rendered content.
70
+ *
71
+ * @example
72
+ * if (isResumable(transport)) {
73
+ * for await (const part of transport.resume(resumeToken)) { ... }
74
+ * }
75
+ */
76
+ export declare function isResumable<TPart, TInput>(t: AgentTransport<TPart, TInput>): t is ResumableAgentTransport<TPart, TInput>;
50
77
  /**
51
78
  * Wraps a consumer's Eden async-generator call into an AgentTransport.
52
79
  *
@@ -64,6 +91,12 @@ export type AgentTransport<TPart = AgentPart, TInput = string> = {
64
91
  * object has NO `resume` key at all (not `resume: undefined`) — consumers that don't wire a
65
92
  * resumable backend get a transport that behaves exactly as before.
66
93
  *
94
+ * `resumeCall` alone does NOT make the transport resumable per `isResumable` — pass
95
+ * `{ idempotentReplay: true }` as the third argument to opt in EXPLICITLY once the server-side
96
+ * `resumeCall` route is verified to re-emit every text/reasoning delta with an authoritative
97
+ * `offset` (never a bare append). This is an opt-in, not an inference: a `resumeCall` whose deltas
98
+ * aren't offset-accurate would double-render on replay if treated as resumable.
99
+ *
67
100
  * @example
68
101
  * import { edenTransport, type AgentPart } from 'basalt-ui/agent'
69
102
  *
@@ -72,6 +105,7 @@ export type AgentTransport<TPart = AgentPart, TInput = string> = {
72
105
  * const transport = edenTransport<AgentPart>(
73
106
  * (input, signal) => api.chat.post({ body: { message: input }, fetch: { signal } }),
74
107
  * (resumeToken, signal) => api.chat.resume.post({ body: { resumeToken }, fetch: { signal } }),
108
+ * { idempotentReplay: true },
75
109
  * )
76
110
  *
77
111
  * // Wire into useAgentStream:
@@ -83,5 +117,7 @@ export declare function edenTransport<TPart = AgentPart, TInput = string>(call:
83
117
  }>, resumeCall?: (resumeToken: string, signal?: AbortSignal) => Promise<{
84
118
  data: AsyncGenerator<TPart> | null;
85
119
  error: unknown;
86
- }>): AgentTransport<TPart, TInput>;
120
+ }>, options?: {
121
+ readonly idempotentReplay?: true;
122
+ }): AgentTransport<TPart, TInput> | ResumableAgentTransport<TPart, TInput>;
87
123
  //# sourceMappingURL=transport.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/agent/transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAIxC;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,cAAc,CAAC,KAAK,GAAG,SAAS,EAAE,MAAM,GAAG,MAAM,IAAI;IAC/D,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,cAAc,CAAC,KAAK,CAAC,CAAA;IACtE,iJAAiJ;IACjJ,MAAM,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,cAAc,CAAC,KAAK,CAAC,CAAA;CAC9E,CAAA;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,aAAa,CAAC,KAAK,GAAG,SAAS,EAAE,MAAM,GAAG,MAAM,EAC9D,IAAI,EAAE,CACJ,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,WAAW,KACjB,OAAO,CAAC;IAAE,IAAI,EAAE,cAAc,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC,EACpE,UAAU,CAAC,EAAE,CACX,WAAW,EAAE,MAAM,EACnB,MAAM,CAAC,EAAE,WAAW,KACjB,OAAO,CAAC;IAAE,IAAI,EAAE,cAAc,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC,GACnE,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,CAmB/B"}
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/agent/transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAIxD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,cAAc,CAAC,KAAK,GAAG,cAAc,EAAE,MAAM,GAAG,MAAM,IAAI;IACpE,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,cAAc,CAAC,KAAK,CAAC,CAAA;IACtE,iJAAiJ;IACjJ,MAAM,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,cAAc,CAAC,KAAK,CAAC,CAAA;CAC9E,CAAA;AAID;;;;;GAKG;AACH,MAAM,MAAM,uBAAuB,CAAC,KAAK,GAAG,cAAc,EAAE,MAAM,GAAG,MAAM,IAAI,cAAc,CAC3F,KAAK,EACL,MAAM,CACP,GAAG;IACF,QAAQ,CAAC,MAAM,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,cAAc,CAAC,KAAK,CAAC,CAAA;IACrF,QAAQ,CAAC,gBAAgB,EAAE,IAAI,CAAA;CAChC,CAAA;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EACvC,CAAC,EAAE,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,GAC/B,CAAC,IAAI,uBAAuB,CAAC,KAAK,EAAE,MAAM,CAAC,CAK7C;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,aAAa,CAAC,KAAK,GAAG,SAAS,EAAE,MAAM,GAAG,MAAM,EAC9D,IAAI,EAAE,CACJ,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,WAAW,KACjB,OAAO,CAAC;IAAE,IAAI,EAAE,cAAc,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC,EACpE,UAAU,CAAC,EAAE,CACX,WAAW,EAAE,MAAM,EACnB,MAAM,CAAC,EAAE,WAAW,KACjB,OAAO,CAAC;IAAE,IAAI,EAAE,cAAc,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC,EACpE,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,IAAI,CAAA;CAAE,GAC7C,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,uBAAuB,CAAC,KAAK,EAAE,MAAM,CAAC,CAsBxE"}
@@ -1,21 +1,24 @@
1
- function edenTransport(call, resumeCall) {
2
- return {
3
- async *stream(input, signal) {
4
- const { data, error } = await call(input, signal);
5
- if (error) throw error;
6
- if (data === null) return;
7
- yield* data;
8
- },
9
- ...resumeCall !== void 0 ? {
10
- async *resume(resumeToken, signal) {
11
- const { data, error } = await resumeCall(resumeToken, signal);
12
- if (error) throw error;
13
- if (data === null) return;
14
- yield* data;
15
- }
16
- } : {}
1
+ function isResumable(t) {
2
+ return typeof t.resume === "function" && t.idempotentReplay === true;
3
+ }
4
+ function edenTransport(call, resumeCall, options) {
5
+ const stream = async function* (input, signal) {
6
+ const { data, error } = await call(input, signal);
7
+ if (error) throw error;
8
+ if (data === null) return;
9
+ yield* data;
10
+ };
11
+ if (resumeCall === void 0) return { stream };
12
+ const resume = async function* (resumeToken, signal) {
13
+ const { data, error } = await resumeCall(resumeToken, signal);
14
+ if (error) throw error;
15
+ if (data === null) return;
16
+ yield* data;
17
17
  };
18
+ if (options?.idempotentReplay !== true) return { stream, resume };
19
+ return { stream, resume, idempotentReplay: true };
18
20
  }
19
21
  export {
20
- edenTransport
22
+ edenTransport,
23
+ isResumable
21
24
  };
@@ -1,3 +1,4 @@
1
+ import type { PartLike } from './merge.js';
1
2
  import type { AgentPart } from './parts.js';
2
3
  import type { AgentTransport } from './transport.js';
3
4
  /** The lifecycle state of the current stream. */
@@ -28,7 +29,7 @@ export type UseAgentStreamReturn<TPart, TInput> = {
28
29
  * // parts accumulates as the stream arrives
29
30
  * // status: 'idle' → 'streaming' → 'done' (or 'error')
30
31
  */
31
- export declare function useAgentStream<TPart = AgentPart, TInput = string>({ transport, }: {
32
+ export declare function useAgentStream<TPart extends PartLike = AgentPart, TInput = string>({ transport, }: {
32
33
  transport: AgentTransport<TPart, TInput>;
33
34
  }): UseAgentStreamReturn<TPart, TInput>;
34
35
  //# sourceMappingURL=use-agent-stream.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-agent-stream.d.ts","sourceRoot":"","sources":["../../src/agent/use-agent-stream.ts"],"names":[],"mappings":"AA8BA,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,GAAG,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,EACjE,SAAS,GACV,EAAE;IACD,SAAS,EAAE,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;CACzC,GAAG,oBAAoB,CAAC,KAAK,EAAE,MAAM,CAAC,CAmEtC"}
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,CA0EtC"}
@@ -1,5 +1,7 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { withPartIds } from "./id.js";
2
3
  import { isStartPart } from "./parts.js";
4
+ import { mergePart } from "./merge.js";
3
5
  function useAgentStream({
4
6
  transport
5
7
  }) {
@@ -23,12 +25,13 @@ function useAgentStream({
23
25
  setParts([]);
24
26
  setError(void 0);
25
27
  setStatus("streaming");
28
+ const runId = crypto.randomUUID();
26
29
  try {
27
- for await (const part of transport.stream(input, controller.signal)) {
30
+ for await (const part of withPartIds(runId, transport.stream(input, controller.signal))) {
28
31
  if (controllerRef.current !== controller) return;
29
32
  if (controller.signal.aborted) break;
30
33
  if (isStartPart(part)) continue;
31
- setParts((prev) => [...prev, part]);
34
+ setParts((prev) => mergePart(prev, part));
32
35
  }
33
36
  if (controllerRef.current === controller && !controller.signal.aborted) setStatus("done");
34
37
  } catch (err) {
@@ -1,3 +1,4 @@
1
+ import type { PartLike } from './merge.js';
1
2
  import type { OutcomeResolver } from './outcome.js';
2
3
  import type { AgentPart } from './parts.js';
3
4
  import type { ThreadsStore } from './thread.js';
@@ -52,9 +53,14 @@ export type UseAgentThreadRunsReturn<TPart = AgentPart> = {
52
53
  * `threadId` has never had a turn started, or already has one in flight.
53
54
  */
54
55
  readonly retry: (threadId: string) => void;
55
- /** Abort the in-flight stream for `threadId` (no-op if idle). */
56
+ /**
57
+ * Abort the in-flight stream for `threadId` (no-op if idle). Preserves whatever content had
58
+ * already arrived: if the run had accumulated any parts, they're persisted as an assistant
59
+ * ChatMessage with `finish: 'stopped'` (unless that message already landed — see finalizeStop),
60
+ * then distilled into an outcome and the thread is settled to 'done'.
61
+ */
56
62
  readonly stop: (threadId: string) => void;
57
- /** Abort every in-flight stream across all threads. */
63
+ /** Abort every in-flight stream across all threads, settling each the same way stop() would. */
58
64
  readonly stopAll: () => void;
59
65
  };
60
66
  /**
@@ -68,5 +74,5 @@ export type UseAgentThreadRunsReturn<TPart = AgentPart> = {
68
74
  * start(threadB, 'What changed in the last release?')
69
75
  * // both stream concurrently; runs.get(threadA) / runs.get(threadB) update independently
70
76
  */
71
- export declare function useAgentThreadRuns<TPart = AgentPart>({ transport, store, resolveOutcome, toUserParts, }: UseAgentThreadRunsArgs<TPart>): UseAgentThreadRunsReturn<TPart>;
77
+ export declare function useAgentThreadRuns<TPart extends PartLike = AgentPart>({ transport, store, resolveOutcome, toUserParts, }: UseAgentThreadRunsArgs<TPart>): UseAgentThreadRunsReturn<TPart>;
72
78
  //# sourceMappingURL=use-agent-thread-runs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-agent-thread-runs.d.ts","sourceRoot":"","sources":["../../src/agent/use-agent-thread-runs.ts"],"names":[],"mappings":"AA0CA,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;AACvE,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,2FAA2F;IAC3F,QAAQ,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACzD;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IAC1C,iEAAiE;IACjE,QAAQ,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IACzC,uDAAuD;IACvD,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAC7B,CAAA;AA0ID;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,GAAG,SAAS,EAAE,EACpD,SAAS,EACT,KAAK,EACL,cAAc,EACd,WAAW,GACZ,EAAE,sBAAsB,CAAC,KAAK,CAAC,GAAG,wBAAwB,CAAC,KAAK,CAAC,CA6KjE"}
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,2FAA2F;IAC3F,QAAQ,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACzD;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IAC1C;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IACzC,gGAAgG;IAChG,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAC7B,CAAA;AAkND;;;;;;;;;;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,CAkPjE"}
@@ -1,7 +1,10 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { withPartIds } from "./id.js";
3
+ import { mergePart } from "./merge.js";
2
4
  import { isStartPart } from "./parts.js";
5
+ import { isResumable } from "./transport.js";
3
6
  function defaultToUserParts(input) {
4
- return [{ type: "text", text: input }];
7
+ return [{ id: crypto.randomUUID(), type: "text", text: input }];
5
8
  }
6
9
  function buildDoneSnapshot(priorThread, threadId, userMessage, assistantMessage) {
7
10
  const updatedAt = Date.now();
@@ -30,12 +33,13 @@ async function consumeAndFinalize(args) {
30
33
  generator,
31
34
  userMessage,
32
35
  controllersRef,
36
+ appendedRef,
33
37
  storeRef,
34
38
  resolveOutcome,
35
39
  setRuns,
36
40
  onFailureStatus
37
41
  } = args;
38
- const parts = [];
42
+ let parts = [];
39
43
  try {
40
44
  for await (const part of generator) {
41
45
  if (controllersRef.current.get(threadId) !== controller) return;
@@ -44,10 +48,10 @@ async function consumeAndFinalize(args) {
44
48
  storeRef.current.setResumeToken(threadId, part.resumeToken);
45
49
  continue;
46
50
  }
47
- parts.push(part);
51
+ parts = mergePart(parts, part);
48
52
  setRuns((prev) => {
49
53
  const next = new Map(prev);
50
- next.set(threadId, { status: "streaming", parts: [...parts] });
54
+ next.set(threadId, { status: "streaming", parts });
51
55
  return next;
52
56
  });
53
57
  }
@@ -58,16 +62,22 @@ async function consumeAndFinalize(args) {
58
62
  id: crypto.randomUUID(),
59
63
  role: "assistant",
60
64
  parts,
61
- createdAt: Date.now()
65
+ createdAt: Date.now(),
66
+ finish: "complete"
62
67
  };
63
68
  storeRef.current.appendMessage(threadId, assistantMessage);
69
+ appendedRef.current.set(threadId, controller);
64
70
  const priorThread = storeRef.current.threads.find((thread) => thread.id === threadId);
65
71
  const snapshot = buildDoneSnapshot(priorThread, threadId, userMessage, assistantMessage);
66
72
  const outcome = await resolveOutcome(snapshot);
73
+ if (controllersRef.current.get(threadId) !== controller || controller.signal.aborted) {
74
+ return;
75
+ }
67
76
  storeRef.current.setOutcome(threadId, outcome);
68
77
  storeRef.current.setStatus(threadId, outcome.status);
69
78
  storeRef.current.setResumeToken(threadId, void 0);
70
79
  controllersRef.current.delete(threadId);
80
+ appendedRef.current.delete(threadId);
71
81
  setRuns((prev) => {
72
82
  if (!prev.has(threadId)) return prev;
73
83
  const next = new Map(prev);
@@ -80,6 +90,7 @@ async function consumeAndFinalize(args) {
80
90
  if (controller.signal.aborted) return;
81
91
  storeRef.current.setStatus(threadId, onFailureStatus);
82
92
  controllersRef.current.delete(threadId);
93
+ appendedRef.current.delete(threadId);
83
94
  setRuns((prev) => {
84
95
  if (!prev.has(threadId)) return prev;
85
96
  const next = new Map(prev);
@@ -88,6 +99,31 @@ async function consumeAndFinalize(args) {
88
99
  });
89
100
  }
90
101
  }
102
+ async function finalizeStop(args) {
103
+ const { threadId, parts, alreadyAppended, storeRef, resolveOutcome, setRuns } = args;
104
+ if (!alreadyAppended && parts.length > 0) {
105
+ const stoppedMessage = {
106
+ id: crypto.randomUUID(),
107
+ role: "assistant",
108
+ parts,
109
+ createdAt: Date.now(),
110
+ finish: "stopped"
111
+ };
112
+ storeRef.current.appendMessage(threadId, stoppedMessage);
113
+ }
114
+ storeRef.current.setStatus(threadId, "done");
115
+ storeRef.current.setResumeToken(threadId, void 0);
116
+ setRuns((prev) => {
117
+ if (!prev.has(threadId)) return prev;
118
+ const next = new Map(prev);
119
+ next.delete(threadId);
120
+ return next;
121
+ });
122
+ const snapshot = storeRef.current.threads.find((thread) => thread.id === threadId);
123
+ if (snapshot === void 0) return;
124
+ const outcome = await resolveOutcome(snapshot);
125
+ storeRef.current.setOutcome(threadId, outcome);
126
+ }
91
127
  function useAgentThreadRuns({
92
128
  transport,
93
129
  store,
@@ -95,7 +131,10 @@ function useAgentThreadRuns({
95
131
  toUserParts
96
132
  }) {
97
133
  const [runs, setRuns] = useState(/* @__PURE__ */ new Map());
134
+ const runsRef = useRef(runs);
135
+ runsRef.current = runs;
98
136
  const controllersRef = useRef(/* @__PURE__ */ new Map());
137
+ const appendedRef = useRef(/* @__PURE__ */ new Map());
99
138
  const lastInputRef = useRef(/* @__PURE__ */ new Map());
100
139
  const storeRef = useRef(store);
101
140
  storeRef.current = store;
@@ -117,6 +156,8 @@ function useAgentThreadRuns({
117
156
  useEffect(
118
157
  () => () => {
119
158
  controllersRef.current.forEach((controller) => controller.abort());
159
+ controllersRef.current.clear();
160
+ appendedRef.current.clear();
120
161
  },
121
162
  []
122
163
  );
@@ -127,7 +168,7 @@ function useAgentThreadRuns({
127
168
  const resumeToken = thread.resumeToken;
128
169
  const lastUserMessage = thread.messages.filter((message) => message.role === "user").at(-1);
129
170
  const resolvedTransport = resolveTransport(thread.id);
130
- if (resolvedTransport.resume === void 0 || resumeToken === void 0 || lastUserMessage === void 0) {
171
+ if (!isResumable(resolvedTransport) || resumeToken === void 0 || lastUserMessage === void 0) {
131
172
  storeRef.current.setStatus(thread.id, "interrupted");
132
173
  continue;
133
174
  }
@@ -142,9 +183,17 @@ function useAgentThreadRuns({
142
183
  void consumeAndFinalize({
143
184
  threadId: thread.id,
144
185
  controller,
145
- generator: resume(resumeToken, controller.signal),
186
+ // Namespaced by thread.id (stable across a thread's whole lifetime, including this
187
+ // resume) rather than per-call — see withPartIds' own doc for why this is NOT a replay-
188
+ // convergence guarantee on its own: it only protects against two id-less drafts within
189
+ // ONE generator colliding (the F1 bug). A transport whose resume() needs genuine replay
190
+ // safety must assert `idempotentReplay: true` AND mint its own content-stable ids (as
191
+ // aiSdkTransport does) — withPartIds is then a no-op passthrough for it, not the mechanism
192
+ // providing that safety.
193
+ generator: withPartIds(thread.id, resume(resumeToken, controller.signal)),
146
194
  userMessage: lastUserMessage,
147
195
  controllersRef,
196
+ appendedRef,
148
197
  storeRef,
149
198
  resolveOutcome,
150
199
  setRuns,
@@ -175,9 +224,11 @@ function useAgentThreadRuns({
175
224
  void consumeAndFinalize({
176
225
  threadId,
177
226
  controller,
178
- generator: resolvedTransport.stream(input, controller.signal),
227
+ // Namespaced by threadId — same choice, same caveat, as the mount-time resume call above.
228
+ generator: withPartIds(threadId, resolvedTransport.stream(input, controller.signal)),
179
229
  userMessage,
180
230
  controllersRef,
231
+ appendedRef,
181
232
  storeRef,
182
233
  resolveOutcome,
183
234
  setRuns,
@@ -194,22 +245,24 @@ function useAgentThreadRuns({
194
245
  },
195
246
  [start]
196
247
  );
197
- const stop = useCallback((threadId) => {
198
- controllersRef.current.get(threadId)?.abort();
199
- controllersRef.current.delete(threadId);
200
- storeRef.current.setStatus(threadId, "done");
201
- setRuns((prev) => {
202
- if (!prev.has(threadId)) return prev;
203
- const next = new Map(prev);
204
- next.delete(threadId);
205
- return next;
206
- });
207
- }, []);
248
+ const stop = useCallback(
249
+ (threadId) => {
250
+ const controller = controllersRef.current.get(threadId);
251
+ if (controller === void 0) return;
252
+ controllersRef.current.delete(threadId);
253
+ const parts = runsRef.current.get(threadId)?.parts ?? [];
254
+ controller.abort();
255
+ const alreadyAppended = appendedRef.current.get(threadId) === controller;
256
+ appendedRef.current.delete(threadId);
257
+ void finalizeStop({ threadId, parts, alreadyAppended, storeRef, resolveOutcome, setRuns });
258
+ },
259
+ [resolveOutcome]
260
+ );
208
261
  const stopAll = useCallback(() => {
209
- controllersRef.current.forEach((controller) => controller.abort());
210
- controllersRef.current.clear();
211
- setRuns(/* @__PURE__ */ new Map());
212
- }, []);
262
+ for (const threadId of Array.from(controllersRef.current.keys())) {
263
+ stop(threadId);
264
+ }
265
+ }, [stop]);
213
266
  return { runs, start, retry, stop, stopAll };
214
267
  }
215
268
  export {
@@ -2,11 +2,12 @@
2
2
  * Mantine-styled thread-chat components over the headless `basalt-ui/agent` layer, capped by the
3
3
  * flagship `ThreadWorkspace` composite.
4
4
  *
5
- * This module is Mantine-coupled by design and lives under the root entry (unlike `basalt-ui/agent`,
6
- * which stays headless so it can be styled by any consumer).
5
+ * This module is Mantine-coupled by design and ships its own `basalt-ui/agent-chat` subpath
6
+ * (also re-exported from the root entry, unchanged for existing consumers) — unlike
7
+ * `basalt-ui/agent`, which stays headless so it can be styled by any consumer.
7
8
  *
8
9
  * @example
9
- * import { ThreadWorkspace } from 'basalt-ui'
10
+ * import { ThreadWorkspace } from 'basalt-ui/agent-chat'
10
11
  * import { createThreadsStore, edenTransport, heuristicOutcome } from 'basalt-ui/agent'
11
12
  */
12
13
  export { ThreadWorkspace } from './thread-workspace.js';
@@ -21,4 +22,6 @@ export { Composer } from './composer.js';
21
22
  export type { ComposerProps } from './composer.js';
22
23
  export { threadPartRenderers, ThreadTranscript } from './thread-message.js';
23
24
  export type { ThreadTranscriptProps } from './thread-message.js';
25
+ export { ToolChip } from './tool-chip.js';
26
+ export type { ToolChipProps } from './tool-chip.js';
24
27
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent-chat/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;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,aAAa,EAAE,MAAM,YAAY,CAAA;AAG/C,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACxE,YAAY,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,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,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,aAAa,EAAE,MAAM,YAAY,CAAA;AAG/C,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"}
@@ -4,6 +4,7 @@ import { ThreadOutcomeCard } from "./thread-outcome-card.js";
4
4
  import { ThreadDetailPanel } from "./thread-detail-panel.js";
5
5
  import { Composer } from "./composer.js";
6
6
  import { threadPartRenderers, ThreadTranscript } from "./thread-message.js";
7
+ import { ToolChip } from "./tool-chip.js";
7
8
  export {
8
9
  Composer,
9
10
  ThreadDetailPanel,
@@ -11,5 +12,6 @@ export {
11
12
  ThreadOutcomeCard,
12
13
  ThreadTranscript,
13
14
  ThreadWorkspace,
15
+ ToolChip,
14
16
  threadPartRenderers
15
17
  };