basalt-ui 1.10.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 (88) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +10 -0
  3. package/dist/agent/ai-sdk-transport.d.ts +3 -52
  4. package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
  5. package/dist/agent/ai-sdk-transport.js +108 -24
  6. package/dist/agent/coalesce.d.ts +26 -0
  7. package/dist/agent/coalesce.d.ts.map +1 -0
  8. package/dist/agent/coalesce.js +69 -0
  9. package/dist/agent/foreign.d.ts +97 -0
  10. package/dist/agent/foreign.d.ts.map +1 -0
  11. package/dist/agent/foreign.js +18 -0
  12. package/dist/agent/history.d.ts +2 -0
  13. package/dist/agent/history.d.ts.map +1 -1
  14. package/dist/agent/id.d.ts +35 -0
  15. package/dist/agent/id.d.ts.map +1 -0
  16. package/dist/agent/id.js +14 -0
  17. package/dist/agent/index.d.ts +9 -4
  18. package/dist/agent/index.d.ts.map +1 -1
  19. package/dist/agent/index.js +15 -3
  20. package/dist/agent/merge.d.ts +8 -0
  21. package/dist/agent/merge.d.ts.map +1 -0
  22. package/dist/agent/merge.js +23 -0
  23. package/dist/agent/part-list.d.ts.map +1 -1
  24. package/dist/agent/part-list.js +16 -6
  25. package/dist/agent/parts.d.ts +100 -21
  26. package/dist/agent/parts.d.ts.map +1 -1
  27. package/dist/agent/parts.js +128 -11
  28. package/dist/agent/transport.d.ts +39 -3
  29. package/dist/agent/transport.d.ts.map +1 -1
  30. package/dist/agent/transport.js +20 -17
  31. package/dist/agent/use-agent-stream.d.ts +2 -1
  32. package/dist/agent/use-agent-stream.d.ts.map +1 -1
  33. package/dist/agent/use-agent-stream.js +5 -2
  34. package/dist/agent/use-agent-thread-runs.d.ts +9 -3
  35. package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
  36. package/dist/agent/use-agent-thread-runs.js +75 -23
  37. package/dist/agent-chat/index.d.ts +2 -0
  38. package/dist/agent-chat/index.d.ts.map +1 -1
  39. package/dist/agent-chat/index.js +2 -0
  40. package/dist/agent-chat/thread-message.d.ts +24 -7
  41. package/dist/agent-chat/thread-message.d.ts.map +1 -1
  42. package/dist/agent-chat/thread-message.js +102 -43
  43. package/dist/agent-chat/tool-chip.d.ts +21 -0
  44. package/dist/agent-chat/tool-chip.d.ts.map +1 -0
  45. package/dist/agent-chat/tool-chip.js +120 -0
  46. package/dist/guard/index.d.ts.map +1 -1
  47. package/dist/guard/index.js +1 -3
  48. package/dist/index.d.ts +2 -2
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +3 -1
  51. package/dist/surfaces.d.ts +2 -2
  52. package/dist/surfaces.js +2 -2
  53. package/llms.txt +4 -4
  54. package/package.json +1 -1
  55. package/src/agent/ai-sdk-transport.test.ts +338 -40
  56. package/src/agent/ai-sdk-transport.ts +206 -53
  57. package/src/agent/coalesce.test.ts +194 -0
  58. package/src/agent/coalesce.ts +118 -0
  59. package/src/agent/foreign.test.ts +47 -0
  60. package/src/agent/foreign.ts +140 -0
  61. package/src/agent/history.ts +2 -0
  62. package/src/agent/id.test.ts +71 -0
  63. package/src/agent/id.ts +43 -0
  64. package/src/agent/index.ts +26 -3
  65. package/src/agent/merge.test.ts +119 -0
  66. package/src/agent/merge.ts +67 -0
  67. package/src/agent/part-list.test.tsx +82 -0
  68. package/src/agent/part-list.tsx +31 -6
  69. package/src/agent/parts.test.ts +401 -59
  70. package/src/agent/parts.ts +262 -32
  71. package/src/agent/transport.test.ts +70 -0
  72. package/src/agent/transport.ts +72 -20
  73. package/src/agent/use-agent-stream.test.tsx +47 -17
  74. package/src/agent/use-agent-stream.ts +13 -3
  75. package/src/agent/use-agent-thread-runs.foreign.test.tsx +133 -0
  76. package/src/agent/use-agent-thread-runs.resume.test.tsx +34 -6
  77. package/src/agent/use-agent-thread-runs.test.tsx +397 -14
  78. package/src/agent/use-agent-thread-runs.ts +170 -26
  79. package/src/agent/use-agent-thread-runs.wedge.test.tsx +4 -2
  80. package/src/agent-chat/index.ts +4 -0
  81. package/src/agent-chat/thread-message.test.tsx +211 -0
  82. package/src/agent-chat/thread-message.tsx +235 -62
  83. package/src/agent-chat/tool-chip.test.tsx +139 -0
  84. package/src/agent-chat/tool-chip.tsx +219 -0
  85. package/src/guard/check-source.test.ts +8 -4
  86. package/src/guard/index.ts +1 -3
  87. package/src/index.ts +2 -0
  88. package/src/surfaces.ts +2 -2
package/AGENTS.md CHANGED
@@ -22,8 +22,8 @@ Toolchain: Bun runtime, oxlint + oxfmt, conventional commits (empty scope), `mas
22
22
  | `basalt-ui/data` | mantine-coupled | Convenience barrel pulling both TanStack Table + Virtual peer groups: BasaltDataTable, BasaltVirtualList (Mantine-rendered) — prefer ./data/table or ./data/virtual for per-feature opt-in |
23
23
  | `basalt-ui/data/table` | mantine-coupled | BasaltDataTable: a sortable data table over TanStack Table, rendered with Mantine (Mantine-rendered) |
24
24
  | `basalt-ui/data/virtual` | mantine-coupled | BasaltVirtualList: a windowed virtual list over TanStack Virtual, rendered with Mantine (Mantine-rendered) |
25
- | `basalt-ui/agent` | headless | Headless streaming-chat layer: useAgentStream, aiSdkTransport (recommended default) + edenTransport, PartList, plus the multi-thread createThreadsStore + useAgentThreadRuns + outcome-resolver seam (Mantine-free) |
26
- | `basalt-ui/agent-chat` | mantine-coupled | Mantine-styled thread-chat components over basalt-ui/agent: ThreadWorkspace, ThreadFeed, ThreadOutcomeCard, ThreadDetailPanel, Composer, ThreadTranscript, threadPartRenderers (Mantine-coupled). remend and motion are required, not optional — ThreadTranscript imports content/markdown.tsx (remend eagerly), and ThreadFeed/ThreadDetailPanel import motion/react eagerly, so this subpath fails to resolve without both installed even though peerDependenciesMeta marks them optional (npm has no per-subpath optionality). |
25
+ | `basalt-ui/agent` | headless | Headless streaming-chat layer: useAgentStream, aiSdkTransport (recommended default) + edenTransport, isResumable/ResumableAgentTransport (stream-resumption seam), PartList, coalesceParts, the ForeignPart/definePartRenderers/narrowAgentPart open part-registry seam, plus the multi-thread createThreadsStore + useAgentThreadRuns + outcome-resolver seam (Mantine-free) |
26
+ | `basalt-ui/agent-chat` | mantine-coupled | Mantine-styled thread-chat components over basalt-ui/agent: ThreadWorkspace, ThreadFeed, ThreadOutcomeCard, ThreadDetailPanel, Composer, ThreadTranscript (open part-renderer registry via its renderers/fallbackRenderer props), threadPartRenderers, ToolChip (Mantine-coupled). remend and motion are required, not optional — ThreadTranscript imports content/markdown.tsx (remend eagerly), and ThreadFeed/ThreadDetailPanel import motion/react eagerly, so this subpath fails to resolve without both installed even though peerDependenciesMeta marks them optional (npm has no per-subpath optionality). |
27
27
  | `basalt-ui/content` | mantine-coupled | Prose (article/chat typography), CodeBlock (shiki, optional peer), Callout, TableOfContents, ReadingProgress, Markdown (react-markdown + remark-gfm, optional peers, streaming-aware), MermaidDiagram (beautiful-mermaid, optional peer), mdxComponents/createMdxComponents, ArticleLayout (docs-page frame), ArticleCard/ArticleGrid (overview cards), Article model (sortArticles/filterArticles/formatArticleDate), ArticleFilterBar (category/tags filter UI), toArticleActions (Spotlight projector, @mantine/spotlight type-only), GuideLink/GuideDrawer (contextual-help drawer) — the content/prose surface |
28
28
  | `basalt-ui/state` | headless | createPersistedState (versioned localStorage) + useOnlineStatus — Mantine-free state primitives |
29
29
  | `basalt-ui/connectivity` | mantine-coupled | ConnectivityProvider (aggregates browser online/offline, React Query onlineManager, SSE, and health-check pings into one status), useConnectivity, and ConnectivityIndicator — auto-mounted by BasaltProvider |
package/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ # [1.11.0](https://github.com/jkrumm/basalt-ui/compare/v1.10.0...v1.11.0) (2026-08-03)
2
+
3
+
4
+ ### Features
5
+
6
+ * give every part a stable id and mirror v7's real tool lifecycle ([d63aac3](https://github.com/jkrumm/basalt-ui/commit/d63aac3267d2322dfd027ba9b78e730eeec95d30))
7
+ * make the transcript addressable, open, and honest about how a turn ended ([f153107](https://github.com/jkrumm/basalt-ui/commit/f1531077b411517226d2f349970677951d95bd59))
8
+ * promote the shade-index guard out of its grace period ([e6a7bf9](https://github.com/jkrumm/basalt-ui/commit/e6a7bf919a1c28246157f918b5462311b37bc192))
9
+ * prove the transcript layer from outside the package boundary ([52bd6d2](https://github.com/jkrumm/basalt-ui/commit/52bd6d2965e780d9d11008c6c449120285983c76))
10
+
1
11
  # [1.10.0](https://github.com/jkrumm/basalt-ui/compare/v1.9.0...v1.10.0) (2026-08-02)
2
12
 
3
13
 
@@ -1,54 +1,5 @@
1
- /**
2
- * aiSdkTransport — an AgentTransport backed by Vercel AI SDK's native streaming + resumption
3
- * primitives (the `ai` npm package). This is the RECOMMENDED DEFAULT transport for LLM chat use
4
- * cases — edenTransport remains the zero-extra-dependency alternative (see agent/rules/basalt-agent.md).
5
- *
6
- * OPTIONAL PEER: `ai` is not a required dependency. Importing 'basalt-ui/agent' (and calling this
7
- * factory) does NOT eagerly resolve it — the package is loaded via a memoized dynamic import()
8
- * only when the first stream()/resume() call actually runs, mirroring the lazy-optional-peer
9
- * contract used by BasaltStickToBottom. Unlike that, there is no meaningful "plain text" fallback
10
- * for a missing transport: if 'ai' is not installed, the dynamic import
11
- * rejects and the error propagates through the async generator to the consumer's EXISTING error
12
- * handling (useAgentStream sets status: 'error'; useAgentThreadRuns falls back to its
13
- * onFailureStatus) — no separate crash path is introduced.
14
- *
15
- * Install the optional peer:
16
- * bun add ai
17
- *
18
- * WHY DIFFING IS NEEDED: AI SDK's `readUIMessageStream` yields the FULL accumulated `UIMessage`
19
- * snapshot (a growing `parts` array, with existing parts' content growing in place) on every
20
- * update — it is not itself a delta stream. basalt-ui's whole part-accumulation model
21
- * (useAgentStream / useAgentThreadRuns, and every consumer's own coalescing) is delta-based: many
22
- * small parts merged at render time. This module diffs consecutive snapshots and yields only the
23
- * new deltas as AgentParts, so nothing is double-rendered.
24
- *
25
- * CHAT ID BINDING: AI SDK's convention is that the chat id is known client-side before the
26
- * request (unlike a river/server-minted id) — `aiSdkTransport(options)` mints ONE stable chat id
27
- * at construction time, so the returned object is immediately usable with useAgentStream (a single
28
- * ongoing conversation). `.forThread(threadId)` returns a transport bound to an arbitrary
29
- * caller-supplied id instead — pass a basalt-ui thread id here to plug into
30
- * useAgentThreadRuns's per-thread transport-factory form, so a thread's turns (and its mount-time
31
- * resume) share one continuous AI SDK chat session:
32
- *
33
- * @example
34
- * import { useAgentStream, aiSdkTransport } from 'basalt-ui/agent'
35
- *
36
- * const transport = aiSdkTransport({ api: '/api/chat' })
37
- * const { parts, status, send } = useAgentStream({ transport })
38
- *
39
- * @example
40
- * import { useAgentThreadRuns, aiSdkTransport, createThreadsStore, heuristicOutcome } from 'basalt-ui/agent'
41
- *
42
- * const useThreads = createThreadsStore({ key: 'main-threads', version: 1 })
43
- * const transport = aiSdkTransport({ api: '/api/chat' })
44
- * const { runs, start } = useAgentThreadRuns({
45
- * transport: (threadId) => transport.forThread(threadId),
46
- * store: useThreads(),
47
- * resolveOutcome: heuristicOutcome,
48
- * })
49
- */
50
1
  import type { AgentPart } from './parts.js';
51
- import type { AgentTransport } from './transport.js';
2
+ import type { ResumableAgentTransport } from './transport.js';
52
3
  export type AiSdkTransportOptions = {
53
4
  /** The API URL AI SDK's transport POSTs new turns to and GETs reconnects from. */
54
5
  readonly api: string;
@@ -62,13 +13,13 @@ export type AiSdkTransportOptions = {
62
13
  */
63
14
  readonly fetch?: typeof globalThis.fetch;
64
15
  };
65
- export type AiSdkTransport<TPart = AgentPart> = AgentTransport<TPart, string> & {
16
+ export type AiSdkTransport<TPart = AgentPart> = ResumableAgentTransport<TPart, string> & {
66
17
  /**
67
18
  * Returns an AgentTransport bound to a stable chat id (e.g. a basalt-ui thread id) instead of
68
19
  * this transport's own fixed one. Use this for useAgentThreadRuns's per-thread transport-factory
69
20
  * form so each thread's turns share one continuous AI SDK chat session.
70
21
  */
71
- readonly forThread: (chatId: string) => AgentTransport<TPart, string>;
22
+ readonly forThread: (chatId: string) => ResumableAgentTransport<TPart, string>;
72
23
  };
73
24
  /**
74
25
  * Wraps AI SDK's `DefaultChatTransport` + `readUIMessageStream` into an AgentTransport.
@@ -1 +1 @@
1
- {"version":3,"file":"ai-sdk-transport.d.ts","sourceRoot":"","sources":["../../src/agent/ai-sdk-transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,OAAO,KAAK,EAAE,SAAS,EAA4B,MAAM,SAAS,CAAA;AAClE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAYjD,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,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG;IAC9E;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;CACtE,CAAA;AA8ID;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,KAAK,GAAG,SAAS,EAC9C,OAAO,EAAE,qBAAqB,GAC7B,cAAc,CAAC,KAAK,CAAC,CAuCvB"}
1
+ {"version":3,"file":"ai-sdk-transport.d.ts","sourceRoot":"","sources":["../../src/agent/ai-sdk-transport.ts"],"names":[],"mappings":"AAmDA,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,CA6CvB"}
@@ -1,3 +1,5 @@
1
+ import { assertNever } from "../register.js";
2
+ import { TERMINAL_TOOL_STATES } from "./parts.js";
1
3
  function createAiSdkResolver(options) {
2
4
  let resolved;
3
5
  return function resolveAiSdk() {
@@ -15,55 +17,133 @@ function createAiSdkResolver(options) {
15
17
  return resolved;
16
18
  };
17
19
  }
18
- function diffPart(prevPart, currPart) {
20
+ function diffPart(chatId, index, prevPart, currPart, toolStartTimes) {
19
21
  switch (currPart.type) {
20
22
  case "text": {
21
23
  const prevText = prevPart?.type === "text" ? prevPart.text : "";
22
24
  const delta = currPart.text.slice(prevText.length);
23
- return delta.length === 0 ? [] : [{ type: "text", text: delta }];
25
+ return delta.length === 0 ? [] : [{ id: `${chatId}#${index}`, type: "text", text: delta, offset: prevText.length }];
24
26
  }
25
27
  case "reasoning": {
26
28
  const prevText = prevPart?.type === "reasoning" ? prevPart.text : "";
27
29
  const delta = currPart.text.slice(prevText.length);
28
- return delta.length === 0 ? [] : [{ type: "reasoning", text: delta }];
30
+ return delta.length === 0 ? [] : [{ id: `${chatId}#${index}`, type: "reasoning", text: delta, offset: prevText.length }];
29
31
  }
30
32
  case "source-url": {
31
33
  if (prevPart !== void 0) return [];
32
- const source = { type: "source", url: currPart.url };
34
+ const source = { id: `${chatId}#${index}`, type: "source", url: currPart.url };
33
35
  return [currPart.title !== void 0 ? { ...source, title: currPart.title } : source];
34
36
  }
35
37
  default:
36
- return currPart.type.startsWith("tool-") ? diffToolPart(prevPart, currPart) : [];
38
+ return isToolLikePart(currPart) ? diffToolPart(prevPart, currPart, toolStartTimes) : [];
37
39
  }
38
40
  }
39
- function diffToolPart(prevPart, currPart) {
40
- const curr = currPart;
41
- if (curr.state === "input-streaming") return [];
42
- const prev = prevPart !== void 0 && prevPart.type === currPart.type ? prevPart : void 0;
43
- if (prev?.state === curr.state) return [];
44
- const toolName = curr.type.slice("tool-".length);
41
+ function isToolLikePart(part) {
42
+ return part.type === "dynamic-tool" || part.type.startsWith("tool-");
43
+ }
44
+ function deriveToolName(part) {
45
+ return part.type === "dynamic-tool" ? part.toolName : part.type.split("-").slice(1).join("-");
46
+ }
47
+ function outputSnapshot(part) {
48
+ return part.state === "output-available" ? { output: part.output, preliminary: part.preliminary } : void 0;
49
+ }
50
+ function isSameToolState(prev, curr) {
51
+ if (prev.state !== curr.state) return false;
52
+ const prevOutput = outputSnapshot(prev);
53
+ const currOutput = outputSnapshot(curr);
54
+ if (prevOutput === void 0 || currOutput === void 0) return true;
55
+ return JSON.stringify(prevOutput.output) === JSON.stringify(currOutput.output) && prevOutput.preliminary === currOutput.preliminary;
56
+ }
57
+ function diffToolPart(prevPart, currPart, toolStartTimes) {
58
+ if (!toolStartTimes.has(currPart.toolCallId)) {
59
+ toolStartTimes.set(currPart.toolCallId, Date.now());
60
+ }
61
+ const prev = prevPart !== void 0 && isToolLikePart(prevPart) ? prevPart : void 0;
62
+ if (prev !== void 0 && isSameToolState(prev, currPart)) return [];
63
+ const settled = TERMINAL_TOOL_STATES.includes(currPart.state);
64
+ const durationMs = settled ? Date.now() - (toolStartTimes.get(currPart.toolCallId) ?? Date.now()) : void 0;
45
65
  const base = {
66
+ id: `tool#${currPart.toolCallId}`,
46
67
  type: "tool",
47
- toolName,
48
- toolCallId: curr.toolCallId,
49
- input: curr.input
68
+ toolCallId: currPart.toolCallId,
69
+ toolName: deriveToolName(currPart),
70
+ ...currPart.providerExecuted !== void 0 ? { providerExecuted: currPart.providerExecuted } : {},
71
+ ...durationMs !== void 0 ? { durationMs } : {}
50
72
  };
51
- if (curr.state === "output-available") return [{ ...base, output: curr.output }];
52
- if (curr.state === "output-error") return [{ ...base, output: { error: curr.errorText } }];
53
- return [base];
73
+ switch (currPart.state) {
74
+ case "input-streaming":
75
+ return [
76
+ {
77
+ ...base,
78
+ state: "input-streaming",
79
+ ...currPart.input !== void 0 ? { input: currPart.input } : {}
80
+ }
81
+ ];
82
+ case "input-available":
83
+ return [{ ...base, state: "input-available", input: currPart.input }];
84
+ case "approval-requested":
85
+ return [
86
+ {
87
+ ...base,
88
+ state: "approval-requested",
89
+ input: currPart.input,
90
+ approval: currPart.approval
91
+ }
92
+ ];
93
+ case "approval-responded":
94
+ return [
95
+ {
96
+ ...base,
97
+ state: "approval-responded",
98
+ input: currPart.input,
99
+ approval: currPart.approval
100
+ }
101
+ ];
102
+ case "output-available":
103
+ return [
104
+ {
105
+ ...base,
106
+ state: "output-available",
107
+ input: currPart.input,
108
+ output: currPart.output,
109
+ ...currPart.preliminary !== void 0 ? { preliminary: currPart.preliminary } : {},
110
+ ...currPart.approval !== void 0 ? { approval: currPart.approval } : {}
111
+ }
112
+ ];
113
+ case "output-error": {
114
+ const rawInput = currPart.type === "dynamic-tool" ? void 0 : currPart.rawInput;
115
+ return [
116
+ {
117
+ ...base,
118
+ state: "output-error",
119
+ ...currPart.input !== void 0 ? { input: currPart.input } : {},
120
+ errorText: currPart.errorText,
121
+ ...rawInput !== void 0 ? { rawInput } : {},
122
+ ...currPart.approval !== void 0 ? { approval: currPart.approval } : {}
123
+ }
124
+ ];
125
+ }
126
+ case "output-denied":
127
+ return [
128
+ { ...base, state: "output-denied", input: currPart.input, approval: currPart.approval }
129
+ ];
130
+ default:
131
+ return assertNever(currPart);
132
+ }
54
133
  }
55
- function diffMessage(prev, curr) {
134
+ function diffMessage(chatId, prev, curr, toolStartTimes) {
56
135
  const deltas = [];
57
136
  curr.parts.forEach((part, i) => {
58
- deltas.push(...diffPart(prev?.parts[i], part));
137
+ deltas.push(...diffPart(chatId, i, prev?.parts[i], part, toolStartTimes));
59
138
  });
60
139
  return deltas;
61
140
  }
62
- async function* diffChunkStream(chunkStream, readStream, signal) {
141
+ async function* diffChunkStream(chatId, chunkStream, readStream, signal) {
63
142
  let prev;
143
+ const toolStartTimes = /* @__PURE__ */ new Map();
64
144
  for await (const curr of readStream({ stream: chunkStream })) {
65
145
  if (signal?.aborted) return;
66
- for (const part of diffMessage(prev, curr)) {
146
+ for (const part of diffMessage(chatId, prev, curr, toolStartTimes)) {
67
147
  yield part;
68
148
  }
69
149
  prev = curr;
@@ -73,8 +153,12 @@ function aiSdkTransport(options) {
73
153
  const resolveAiSdk = createAiSdkResolver(options);
74
154
  function makeTransport(chatId) {
75
155
  return {
156
+ // Every text/reasoning delta this transport emits carries an authoritative `offset` (see
157
+ // diffPart), so a replayed run rewrites in place instead of duplicating — the literal
158
+ // condition this assertion stands for.
159
+ idempotentReplay: true,
76
160
  async *stream(input, signal) {
77
- yield { type: "start", runId: chatId, resumeToken: chatId };
161
+ yield { id: `${chatId}#start`, type: "start", runId: chatId, resumeToken: chatId };
78
162
  const { httpTransport, readUIMessageStream: readStream } = await resolveAiSdk();
79
163
  const userMessage = {
80
164
  id: crypto.randomUUID(),
@@ -88,13 +172,13 @@ function aiSdkTransport(options) {
88
172
  messageId: void 0,
89
173
  abortSignal: signal
90
174
  });
91
- yield* diffChunkStream(chunkStream, readStream, signal);
175
+ yield* diffChunkStream(chatId, chunkStream, readStream, signal);
92
176
  },
93
177
  async *resume(resumeToken, signal) {
94
178
  const { httpTransport, readUIMessageStream: readStream } = await resolveAiSdk();
95
179
  const chunkStream = await httpTransport.reconnectToStream({ chatId: resumeToken });
96
180
  if (chunkStream === null) return;
97
- yield* diffChunkStream(chunkStream, readStream, signal);
181
+ yield* diffChunkStream(resumeToken, chunkStream, readStream, signal);
98
182
  }
99
183
  };
100
184
  }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * coalesceParts — merges a rendered-turn's parts array for display.
3
+ *
4
+ * Merges consecutive `text`+`text` and `reasoning`+`reasoning` parts (streamed answers arrive as
5
+ * many small fragments; this renders a streaming reply as one flowing block instead of a stack of
6
+ * fragments). Merges `tool` parts BY `toolCallId` regardless of adjacency — the AI SDK re-emits a
7
+ * tool call's `input-available` then `output-available` as two separate parts, not necessarily
8
+ * adjacent once other parts interleave; without this rule a UI renders two stacked tool blocks for
9
+ * one call. The later state always wins; the merged part keeps the position of its FIRST
10
+ * occurrence (stable ordering); and an `input` (or `toolName`) supplied by an earlier state
11
+ * survives a later state that omits it — `tool-output-available` carries only `output` on the
12
+ * wire, the SDK re-supplies `input` by reading it back off the stored invocation, and a merge that
13
+ * replaces wholesale rather than accumulating would lose it outright.
14
+ *
15
+ * `approval`-bearing responses can arrive without a usable `toolCallId` (the SDK resolves them by
16
+ * reverse lookup on `approval.id` instead) — this maintains an `approvalId -> toolCallId` side
17
+ * index while folding and routes such a part through it. A part resolvable by neither key is kept
18
+ * as-is rather than dropped; never silently discard a part.
19
+ *
20
+ * @example
21
+ * import { coalesceParts } from 'basalt-ui/agent'
22
+ * <PartList parts={coalesceParts(parts)} />
23
+ */
24
+ import type { AgentPart } from './parts.js';
25
+ export declare function coalesceParts<TPart extends AgentPart>(parts: readonly TPart[]): TPart[];
26
+ //# sourceMappingURL=coalesce.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coalesce.d.ts","sourceRoot":"","sources":["../../src/agent/coalesce.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAyC,MAAM,SAAS,CAAA;AA+C/E,wBAAgB,aAAa,CAAC,KAAK,SAAS,SAAS,EAAE,KAAK,EAAE,SAAS,KAAK,EAAE,GAAG,KAAK,EAAE,CA+CvF"}
@@ -0,0 +1,69 @@
1
+ function isTextLikeType(type) {
2
+ return type === "text" || type === "reasoning";
3
+ }
4
+ function resolveToolCallId(part, toolCallIdByApprovalId) {
5
+ if (typeof part.toolCallId === "string" && part.toolCallId.length > 0) return part.toolCallId;
6
+ const approval = "approval" in part ? part.approval : void 0;
7
+ return approval !== void 0 ? toolCallIdByApprovalId.get(approval.id) : void 0;
8
+ }
9
+ function registerApprovalIndex(part, toolCallIdByApprovalId) {
10
+ const approval = "approval" in part ? part.approval : void 0;
11
+ if (approval === void 0) return;
12
+ if (typeof part.toolCallId !== "string" || part.toolCallId.length === 0) return;
13
+ toolCallIdByApprovalId.set(approval.id, part.toolCallId);
14
+ }
15
+ function mergeToolPart(existing, next) {
16
+ const existingInput = "input" in existing ? existing.input : void 0;
17
+ const nextInput = "input" in next ? next.input : void 0;
18
+ const input = nextInput !== void 0 ? nextInput : existingInput;
19
+ const toolName = next.toolName.length > 0 ? next.toolName : existing.toolName;
20
+ return {
21
+ ...existing,
22
+ ...next,
23
+ toolName,
24
+ ...input !== void 0 ? { input } : {}
25
+ };
26
+ }
27
+ function coalesceParts(parts) {
28
+ const out = [];
29
+ const toolIndexByCallId = /* @__PURE__ */ new Map();
30
+ const toolCallIdByApprovalId = /* @__PURE__ */ new Map();
31
+ for (const part of parts) {
32
+ if (isTextLikeType(part.type)) {
33
+ const last = out[out.length - 1];
34
+ if (last !== void 0 && last.type === part.type) {
35
+ const merged = {
36
+ ...part,
37
+ text: last.text + part.text
38
+ };
39
+ out[out.length - 1] = merged;
40
+ continue;
41
+ }
42
+ out.push(part);
43
+ continue;
44
+ }
45
+ if (part.type === "tool") {
46
+ const toolPart = part;
47
+ registerApprovalIndex(toolPart, toolCallIdByApprovalId);
48
+ const resolvedCallId = resolveToolCallId(toolPart, toolCallIdByApprovalId);
49
+ if (resolvedCallId === void 0) {
50
+ out.push(part);
51
+ continue;
52
+ }
53
+ const existingIndex = toolIndexByCallId.get(resolvedCallId);
54
+ if (existingIndex === void 0) {
55
+ toolIndexByCallId.set(resolvedCallId, out.length);
56
+ out.push(part);
57
+ continue;
58
+ }
59
+ const existing = out[existingIndex];
60
+ out[existingIndex] = mergeToolPart(existing, toolPart);
61
+ continue;
62
+ }
63
+ out.push(part);
64
+ }
65
+ return out;
66
+ }
67
+ export {
68
+ coalesceParts
69
+ };
@@ -0,0 +1,97 @@
1
+ /**
2
+ * foreign.ts — the open half of the transcript's part-rendering registry.
3
+ *
4
+ * `AgentPart` (`./parts`) is a closed, exhaustively-switched union — adding a variant without
5
+ * updating every consuming switch is a tsc error (`PartList`'s `assertNever` gate). A transport
6
+ * that streams app-specific structured data (a chart spec, a progress ping) needs a part type
7
+ * `AgentPart` will never carry — closing the union around it would defeat the whole point of the
8
+ * gate. This file is the seam: `ForeignPart` describes what basalt does NOT know, `ConsumerPart`
9
+ * resolves the augmented union a consumer registers via `declare module 'basalt-ui'`, and
10
+ * `definePartRenderers` types an exhaustive renderer map against it. See
11
+ * `agent-chat/thread-message.tsx`'s three-step resolution order for how this plugs into
12
+ * `ThreadTranscript`: a consumer renderer wins first, `narrowAgentPart` routes the built-in six to
13
+ * `PartList`'s own exhaustive switch second, and `fallbackRenderer` catches anything left.
14
+ *
15
+ * @example
16
+ * // app-side: src/features/chat/parts.ts
17
+ * type ChartPart = { type: 'data-chart'; id: string; spec: ChartSpec }
18
+ * declare module 'basalt-ui' {
19
+ * interface BasaltRegister { parts: ChartPart }
20
+ * }
21
+ * export const chatRenderers = definePartRenderers({
22
+ * 'data-chart': ({ part, settled }) => (settled ? <Chart spec={part.spec} /> : <ChartSkeleton />),
23
+ * })
24
+ */
25
+ import type { ReactNode } from 'react';
26
+ import type { BasaltRegister } from '../register.js';
27
+ import type { ChatMessage } from './history.js';
28
+ import type { AgentPart } from './parts.js';
29
+ /** A part basalt does not know. NOT a member of AgentPart — the built-in union stays closed. */
30
+ export type ForeignPart = {
31
+ readonly type: string;
32
+ readonly id: string;
33
+ readonly [k: string]: unknown;
34
+ };
35
+ /** What a transcript actually holds — basalt's own six variants plus anything a consumer registers. */
36
+ export type TranscriptPart = AgentPart | ForeignPart;
37
+ /** The consumer's registered foreign-part union, or `never` when un-augmented. */
38
+ export type ConsumerPart = BasaltRegister extends {
39
+ parts: infer P extends ForeignPart;
40
+ } ? P : never;
41
+ /**
42
+ * Un-augmented: a loose string-keyed map (any renderer, any key — there is nothing to check
43
+ * against). Augmented: a MISSING key is a tsc error (`definePartRenderers` requires the full
44
+ * registered union), and each renderer's `part` is narrowed to exactly its variant. Same
45
+ * bound-check mechanism as `SeriesKey` (`register.ts:26-58`), applied to a union instead of a map.
46
+ *
47
+ * NOT caught: a STALE (extra, unregistered) key. `<const T extends Constraint>` inference does
48
+ * not excess-property-check an object-literal argument the way a direct assignment to a fixed
49
+ * target type would (verified against this repo's tsc — see
50
+ * `apps/playground/src/demo/agent-part-registry.type-guard.ts`'s Fixture 3) — an extra key
51
+ * compiles as dead code rather than erroring. Catching it would need an `Exact<T, Shape>`-style
52
+ * wrapper that departs from the `spec: T` "canonical token-factory contract" every other `defineX`
53
+ * in this codebase follows verbatim; that trade was left as a documented gap rather than a one-off
54
+ * factory shape.
55
+ */
56
+ export type PartRenderers = [ConsumerPart] extends [never] ? Readonly<Record<string, PartRenderer<ForeignPart>>> : {
57
+ readonly [K in ConsumerPart['type']]: PartRenderer<Extract<ConsumerPart, {
58
+ type: K;
59
+ }>>;
60
+ };
61
+ /** Everything a foreign-part renderer needs beyond the part itself. */
62
+ export type PartRenderContext<TPart = ForeignPart> = {
63
+ readonly part: TPart;
64
+ readonly messageId: string;
65
+ readonly partId: string;
66
+ /** false while this part belongs to the in-flight tail of a streaming turn. */
67
+ readonly settled: boolean;
68
+ readonly role: ChatMessage['role'];
69
+ };
70
+ export type PartRenderer<TPart = ForeignPart> = (ctx: PartRenderContext<TPart>) => ReactNode;
71
+ /**
72
+ * Canonical const-generic factory (`packages/basalt-ui/CLAUDE.md`, "Canonical token-factory
73
+ * contract") — identity passthrough, exact-keyed return, no builder, no config bag. Mirrors
74
+ * `defineSeries` (`tokens/index.ts:648-650`) and `defineNotifications`
75
+ * (`notifications/define-notifications.ts:112-115`).
76
+ *
77
+ * @example
78
+ * const chatRenderers = definePartRenderers({
79
+ * 'data-chart': ({ part }) => <Chart spec={part.spec} />,
80
+ * })
81
+ */
82
+ export declare function definePartRenderers<const T extends PartRenderers>(map: T): T;
83
+ /**
84
+ * Returns `part` as an `AgentPart` when its `type` is one of the six built-in variants, else
85
+ * `null`. Deliberately NOT routed through `parseAgentPart`: that function is a VALIDATING parser
86
+ * for untrusted wire data — it checks every field a variant requires (`runId`, `toolCallId`,
87
+ * `approval`, ...) and would reject an already-well-typed in-memory `AgentPart` on shape grounds
88
+ * if it doesn't happen to match the wire representation byte-for-byte. `narrowAgentPart` is the
89
+ * cheap complement: a plain discriminator check on a value already known to be a `TranscriptPart`,
90
+ * used purely to route rendering — it trusts the caller's typing rather than re-validating it.
91
+ *
92
+ * @example
93
+ * const agentPart = narrowAgentPart(part)
94
+ * if (agentPart !== null) return <PartList parts={[agentPart]} />
95
+ */
96
+ export declare function narrowAgentPart(part: TranscriptPart): AgentPart | null;
97
+ //# sourceMappingURL=foreign.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"foreign.d.ts","sourceRoot":"","sources":["../../src/agent/foreign.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAC5C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAIxC,gGAAgG;AAChG,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CAC9B,CAAA;AAED,uGAAuG;AACvG,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,WAAW,CAAA;AAuBpD,kFAAkF;AAClF,MAAM,MAAM,YAAY,GAAG,cAAc,SAAS;IAAE,KAAK,EAAE,MAAM,CAAC,SAAS,WAAW,CAAA;CAAE,GAAG,CAAC,GAAG,KAAK,CAAA;AAIpG;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,YAAY,CAAC,SAAS,CAAC,KAAK,CAAC,GACtD,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,WAAW,CAAC,CAAC,CAAC,GACnD;IAAE,QAAQ,EAAE,CAAC,IAAI,YAAY,CAAC,MAAM,CAAC,GAAG,YAAY,CAAC,OAAO,CAAC,YAAY,EAAE;QAAE,IAAI,EAAE,CAAC,CAAA;KAAE,CAAC,CAAC;CAAE,CAAA;AAE9F,uEAAuE;AACvE,MAAM,MAAM,iBAAiB,CAAC,KAAK,GAAG,WAAW,IAAI;IACnD,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAA;IACpB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CACnC,CAAA;AAED,MAAM,MAAM,YAAY,CAAC,KAAK,GAAG,WAAW,IAAI,CAAC,GAAG,EAAE,iBAAiB,CAAC,KAAK,CAAC,KAAK,SAAS,CAAA;AAE5F;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,CAAC,CAAC,SAAS,aAAa,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAE5E;AAaD;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,cAAc,GAAG,SAAS,GAAG,IAAI,CAEtE"}
@@ -0,0 +1,18 @@
1
+ function definePartRenderers(map) {
2
+ return map;
3
+ }
4
+ const AGENT_PART_TYPES = /* @__PURE__ */ new Set([
5
+ "start",
6
+ "text",
7
+ "reasoning",
8
+ "tool",
9
+ "source",
10
+ "error"
11
+ ]);
12
+ function narrowAgentPart(part) {
13
+ return AGENT_PART_TYPES.has(part.type) ? part : null;
14
+ }
15
+ export {
16
+ definePartRenderers,
17
+ narrowAgentPart
18
+ };
@@ -16,6 +16,8 @@ export type ChatMessage<TPart = AgentPart> = {
16
16
  readonly role: 'user' | 'assistant';
17
17
  readonly parts: TPart[];
18
18
  readonly createdAt: number;
19
+ /** How the turn ended. Absent on user messages and on pre-1.11 persisted assistant messages. */
20
+ readonly finish?: 'complete' | 'stopped' | 'error';
19
21
  };
20
22
  export type ChatHistoryStoreOptions = {
21
23
  /** localStorage key (namespaced as `basalt:<key>`). Stable — changes key lose history. */
@@ -1 +1 @@
1
- {"version":3,"file":"history.d.ts","sourceRoot":"","sources":["../../src/agent/history.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAIxC;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,WAAW,CAAC,KAAK,GAAG,SAAS,IAAI;IAC3C,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAA;IACnC,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,CAAA;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B,CAAA;AAID,MAAM,MAAM,uBAAuB,GAAG;IACpC,0FAA0F;IAC1F,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,wFAAwF;IACxF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB;;;OAGG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAID,MAAM,MAAM,gBAAgB,CAAC,KAAK,GAAG,SAAS,IAAI;IAChD,2CAA2C;IAC3C,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,KAAK,CAAC,EAAE,CAAA;IACvC,8EAA8E;IAC9E,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,WAAW,CAAC,KAAK,CAAC,KAAK,IAAI,CAAA;IACtD,0BAA0B;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,IAAI,CAAA;CAC3B,CAAA;AAID;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,GAAG,SAAS,EACtD,IAAI,EAAE,uBAAuB,GAC5B,MAAM,gBAAgB,CAAC,KAAK,CAAC,CAoC/B"}
1
+ {"version":3,"file":"history.d.ts","sourceRoot":"","sources":["../../src/agent/history.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAIxC;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,WAAW,CAAC,KAAK,GAAG,SAAS,IAAI;IAC3C,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAA;IACnC,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,CAAA;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,gGAAgG;IAChG,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,SAAS,GAAG,OAAO,CAAA;CACnD,CAAA;AAID,MAAM,MAAM,uBAAuB,GAAG;IACpC,0FAA0F;IAC1F,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,wFAAwF;IACxF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB;;;OAGG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAID,MAAM,MAAM,gBAAgB,CAAC,KAAK,GAAG,SAAS,IAAI;IAChD,2CAA2C;IAC3C,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,KAAK,CAAC,EAAE,CAAA;IACvC,8EAA8E;IAC9E,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,WAAW,CAAC,KAAK,CAAC,KAAK,IAAI,CAAA;IACtD,0BAA0B;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,IAAI,CAAA;CAC3B,CAAA;AAID;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,GAAG,SAAS,EACtD,IAAI,EAAE,uBAAuB,GAC5B,MAAM,gBAAgB,CAAC,KAAK,CAAC,CAoC/B"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * withPartIds — stamps a stable id onto every draft part arriving without one.
3
+ *
4
+ * A transport is allowed to yield a draft (`AgentPartDraft` — `id` optional); this wraps such a
5
+ * generator and normalizes it into a fully-identified stream, minting `${runId}#${n}` for any
6
+ * part that arrives without an `id`. A part that already carries an id passes through untouched
7
+ * (idempotent) and does not advance the counter.
8
+ *
9
+ * NOT a replay-convergence mechanism. The minted id is a bare per-call sequence number with no
10
+ * relationship to content or offset — it identifies "the nth id-less part THIS invocation saw",
11
+ * nothing more. Calling `withPartIds` again over a resumed/replayed stream restarts its counter at
12
+ * 0, so the resumed stream's first delta mints the SAME id as the original run's first delta
13
+ * (`${runId}#0`) regardless of whether it carries the same content — if the resumed transport
14
+ * re-sends the full text as one chunk while the original sent it as several deltas, mergePart's
15
+ * tail-splice will corrupt the accumulated text rather than converge it. A transport that needs
16
+ * genuine replay convergence (one asserting `ResumableAgentTransport`'s `idempotentReplay: true`)
17
+ * MUST mint its own content-stable ids — e.g. `aiSdkTransport`'s `${chatId}#${index}` /
18
+ * `tool#${toolCallId}` — and must not rely on this helper for that guarantee. Combining
19
+ * `withPartIds` with a transport that asserts `idempotentReplay: true` is only safe when that
20
+ * transport already mints stable ids itself, in which case every part already carries an `id` and
21
+ * `withPartIds` is a no-op passthrough (see the idempotent behavior above) — it is never the thing
22
+ * providing replay safety.
23
+ *
24
+ * @example
25
+ * const identified = withPartIds(runId, transport.stream(input, signal))
26
+ * for await (const part of identified) {
27
+ * // part.id is always defined here
28
+ * }
29
+ */
30
+ export declare function withPartIds<TPart extends {
31
+ id?: string;
32
+ }>(runId: string, source: AsyncGenerator<TPart>): AsyncGenerator<TPart & {
33
+ id: string;
34
+ }>;
35
+ //# sourceMappingURL=id.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"id.d.ts","sourceRoot":"","sources":["../../src/agent/id.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;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"}
@@ -0,0 +1,14 @@
1
+ async function* withPartIds(runId, source) {
2
+ let n = 0;
3
+ for await (const part of source) {
4
+ if (part.id !== void 0) {
5
+ yield part;
6
+ continue;
7
+ }
8
+ yield { ...part, id: `${runId}#${n}` };
9
+ n += 1;
10
+ }
11
+ }
12
+ export {
13
+ withPartIds
14
+ };
@@ -30,10 +30,15 @@
30
30
  * )
31
31
  * }
32
32
  */
33
- export type { AgentPart, StartPart, TextPart, ReasoningPart, ToolCallPart, SourcePart, ErrorPart, } from './parts.js';
34
- export { parseAgentPart, isStartPart } from './parts.js';
35
- export type { AgentTransport } from './transport.js';
36
- export { edenTransport } from './transport.js';
33
+ export type { AgentPart, AgentPartDraft, Drafted, StartPart, TextPart, ReasoningPart, ToolCallPart, ToolCallState, SourcePart, ErrorPart, } from './parts.js';
34
+ export { parseAgentPart, isStartPart, isToolCallSettled, TERMINAL_TOOL_STATES } from './parts.js';
35
+ export { mergePart } from './merge.js';
36
+ export { withPartIds } from './id.js';
37
+ export { coalesceParts } from './coalesce.js';
38
+ export { definePartRenderers, narrowAgentPart } from './foreign.js';
39
+ export type { ForeignPart, TranscriptPart, ConsumerPart, PartRenderers, PartRenderContext, PartRenderer, } from './foreign.js';
40
+ export type { AgentTransport, ResumableAgentTransport } from './transport.js';
41
+ export { edenTransport, isResumable } from './transport.js';
37
42
  export { aiSdkTransport } from './ai-sdk-transport.js';
38
43
  export type { AiSdkTransport, AiSdkTransportOptions } from './ai-sdk-transport.js';
39
44
  export { useAgentStream } from './use-agent-stream.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAGH,YAAY,EACV,SAAS,EACT,SAAS,EACT,QAAQ,EACR,aAAa,EACb,YAAY,EACZ,UAAU,EACV,SAAS,GACV,MAAM,SAAS,CAAA;AAChB,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAGrD,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAG3C,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACnD,YAAY,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAG/E,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACnD,YAAY,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAG5E,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAC5D,YAAY,EACV,cAAc,EACd,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,yBAAyB,CAAA;AAGhC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACtC,YAAY,EACV,aAAa,EACb,kBAAkB,EAClB,gBAAgB,EAChB,qBAAqB,EACrB,oBAAoB,EACpB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,aAAa,CAAA;AAGpB,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AACvD,YAAY,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAA;AAGjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAA;AAClD,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAA;AAGvF,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAC7C,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAA;AAG5F,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AAC5C,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAGH,YAAY,EACV,SAAS,EACT,cAAc,EACd,OAAO,EACP,SAAS,EACT,QAAQ,EACR,aAAa,EACb,YAAY,EACZ,aAAa,EACb,UAAU,EACV,SAAS,GACV,MAAM,SAAS,CAAA;AAChB,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAG9F,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAGnC,OAAO,EAAE,WAAW,EAAE,MAAM,MAAM,CAAA;AAGlC,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAG1C,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChE,YAAY,EACV,WAAW,EACX,cAAc,EACd,YAAY,EACZ,aAAa,EACb,iBAAiB,EACjB,YAAY,GACb,MAAM,WAAW,CAAA;AAGlB,YAAY,EAAE,cAAc,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA;AAC1E,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAGxD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACnD,YAAY,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAG/E,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACnD,YAAY,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAG5E,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAC5D,YAAY,EACV,cAAc,EACd,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,yBAAyB,CAAA;AAGhC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACtC,YAAY,EACV,aAAa,EACb,kBAAkB,EAClB,gBAAgB,EAChB,qBAAqB,EACrB,oBAAoB,EACpB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,aAAa,CAAA;AAGpB,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AACvD,YAAY,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAA;AAGjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAA;AAClD,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAA;AAGvF,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAC7C,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAA;AAG5F,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AAC5C,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA"}