@tanstack/ai-client 0.22.1 → 0.23.1

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 (72) hide show
  1. package/README.md +15 -1
  2. package/dist/esm/audio-recorder.js +190 -213
  3. package/dist/esm/audio-recorder.js.map +1 -1
  4. package/dist/esm/chat-client.d.ts +172 -3
  5. package/dist/esm/chat-client.js +1656 -1386
  6. package/dist/esm/chat-client.js.map +1 -1
  7. package/dist/esm/cleared-stream-tracker.d.ts +23 -0
  8. package/dist/esm/cleared-stream-tracker.js +97 -0
  9. package/dist/esm/cleared-stream-tracker.js.map +1 -0
  10. package/dist/esm/client-persistor.d.ts +25 -12
  11. package/dist/esm/client-persistor.js +260 -235
  12. package/dist/esm/client-persistor.js.map +1 -1
  13. package/dist/esm/connection-adapters.d.ts +231 -10
  14. package/dist/esm/connection-adapters.js +989 -574
  15. package/dist/esm/connection-adapters.js.map +1 -1
  16. package/dist/esm/devtools-noop.d.ts +1 -0
  17. package/dist/esm/devtools-noop.js +79 -139
  18. package/dist/esm/devtools-noop.js.map +1 -1
  19. package/dist/esm/devtools.d.ts +31 -1
  20. package/dist/esm/devtools.js +977 -1127
  21. package/dist/esm/devtools.js.map +1 -1
  22. package/dist/esm/events.js +224 -226
  23. package/dist/esm/events.js.map +1 -1
  24. package/dist/esm/generation-client.d.ts +145 -2
  25. package/dist/esm/generation-client.js +659 -321
  26. package/dist/esm/generation-client.js.map +1 -1
  27. package/dist/esm/generation-reconstruct.d.ts +21 -0
  28. package/dist/esm/generation-reconstruct.js +85 -0
  29. package/dist/esm/generation-reconstruct.js.map +1 -0
  30. package/dist/esm/generation-types.d.ts +289 -3
  31. package/dist/esm/generation-types.js +356 -13
  32. package/dist/esm/generation-types.js.map +1 -1
  33. package/dist/esm/index.d.ts +9 -4
  34. package/dist/esm/index.js +7 -39
  35. package/dist/esm/interrupt-manager.d.ts +77 -0
  36. package/dist/esm/interrupt-manager.js +787 -0
  37. package/dist/esm/interrupt-manager.js.map +1 -0
  38. package/dist/esm/mcp-app-bridge.js +56 -64
  39. package/dist/esm/mcp-app-bridge.js.map +1 -1
  40. package/dist/esm/realtime-client.js +366 -440
  41. package/dist/esm/realtime-client.js.map +1 -1
  42. package/dist/esm/response-stream.js +19 -26
  43. package/dist/esm/response-stream.js.map +1 -1
  44. package/dist/esm/sse-parser.js +44 -47
  45. package/dist/esm/sse-parser.js.map +1 -1
  46. package/dist/esm/sse-utils.js +8 -9
  47. package/dist/esm/sse-utils.js.map +1 -1
  48. package/dist/esm/storage-adapters.d.ts +62 -0
  49. package/dist/esm/storage-adapters.js +174 -0
  50. package/dist/esm/storage-adapters.js.map +1 -0
  51. package/dist/esm/types.d.ts +212 -10
  52. package/dist/esm/types.js +38 -7
  53. package/dist/esm/types.js.map +1 -1
  54. package/dist/esm/video-generation-client.d.ts +113 -2
  55. package/dist/esm/video-generation-client.js +665 -379
  56. package/dist/esm/video-generation-client.js.map +1 -1
  57. package/package.json +7 -7
  58. package/src/chat-client.ts +1079 -61
  59. package/src/cleared-stream-tracker.ts +151 -0
  60. package/src/client-persistor.ts +102 -33
  61. package/src/connection-adapters.ts +1185 -142
  62. package/src/devtools-noop.ts +4 -3
  63. package/src/devtools.ts +121 -3
  64. package/src/generation-client.ts +563 -13
  65. package/src/generation-reconstruct.ts +121 -0
  66. package/src/generation-types.ts +727 -3
  67. package/src/index.ts +56 -1
  68. package/src/interrupt-manager.ts +1440 -0
  69. package/src/storage-adapters.ts +242 -0
  70. package/src/types.ts +301 -9
  71. package/src/video-generation-client.ts +479 -13
  72. package/dist/esm/index.js.map +0 -1
@@ -0,0 +1,97 @@
1
+ import { getChunkRunId } from "./connection-adapters.js";
2
+ //#region src/cleared-stream-tracker.ts
3
+ function getChunkToolCallId(chunk) {
4
+ return "toolCallId" in chunk && typeof chunk.toolCallId === "string" ? chunk.toolCallId : void 0;
5
+ }
6
+ function getChunkMessageId(chunk) {
7
+ return "messageId" in chunk && typeof chunk.messageId === "string" ? chunk.messageId : void 0;
8
+ }
9
+ function getChunkParentMessageId(chunk) {
10
+ return "parentMessageId" in chunk && typeof chunk.parentMessageId === "string" ? chunk.parentMessageId : void 0;
11
+ }
12
+ /** Tracks stream chunks that must be ignored after the owning chat is cleared. */
13
+ var ClearedStreamTracker = class {
14
+ clearedMessageIds = /* @__PURE__ */ new Set();
15
+ clearedRunIds = /* @__PURE__ */ new Set();
16
+ ignoredActiveRunIds = /* @__PURE__ */ new Set();
17
+ clearedToolCallIds = /* @__PURE__ */ new Set();
18
+ currentRunlessRunId = null;
19
+ snapshotClear(context) {
20
+ for (const message of context.messages) this.clearedMessageIds.add(message.id);
21
+ for (const runId of context.activeRunIds) {
22
+ this.clearedRunIds.add(runId);
23
+ this.ignoredActiveRunIds.add(runId);
24
+ }
25
+ if (context.currentRunId) {
26
+ this.clearedRunIds.add(context.currentRunId);
27
+ this.ignoredActiveRunIds.add(context.currentRunId);
28
+ }
29
+ }
30
+ shouldIgnoreChunk(chunk) {
31
+ const runId = getChunkRunId(chunk);
32
+ if (runId && this.clearedRunIds.has(runId)) {
33
+ if (chunk.type === "RUN_STARTED") {
34
+ this.ignoredActiveRunIds.add(runId);
35
+ this.currentRunlessRunId = runId;
36
+ }
37
+ this.markIgnoredChunkIds(chunk);
38
+ return true;
39
+ }
40
+ if (runId && this.ignoredActiveRunIds.has(runId)) {
41
+ this.markIgnoredChunkIds(chunk);
42
+ return true;
43
+ }
44
+ if (this.isRunlessChunkFromIgnoredRun(chunk)) {
45
+ this.markIgnoredChunkIds(chunk);
46
+ return true;
47
+ }
48
+ const toolCallId = getChunkToolCallId(chunk);
49
+ if (toolCallId && this.clearedToolCallIds.has(toolCallId)) return true;
50
+ const parentMessageId = getChunkParentMessageId(chunk);
51
+ if (parentMessageId && this.clearedMessageIds.has(parentMessageId)) {
52
+ if (toolCallId) this.clearedToolCallIds.add(toolCallId);
53
+ return true;
54
+ }
55
+ const messageId = getChunkMessageId(chunk);
56
+ return Boolean(messageId && this.clearedMessageIds.has(messageId));
57
+ }
58
+ onRunStarted(runId) {
59
+ this.currentRunlessRunId = runId;
60
+ }
61
+ onRunSettled(runId) {
62
+ this.ignoredActiveRunIds.delete(runId);
63
+ this.clearedRunIds.delete(runId);
64
+ if (this.currentRunlessRunId === runId) this.currentRunlessRunId = this.ignoredActiveRunIds.values().next().value ?? null;
65
+ }
66
+ onSessionRunError() {
67
+ this.ignoredActiveRunIds.clear();
68
+ this.currentRunlessRunId = null;
69
+ }
70
+ resetActiveRuns() {
71
+ this.ignoredActiveRunIds.clear();
72
+ this.currentRunlessRunId = null;
73
+ }
74
+ takeRunlessRunId() {
75
+ const runId = this.currentRunlessRunId;
76
+ if (!runId) return null;
77
+ this.ignoredActiveRunIds.delete(runId);
78
+ this.clearedRunIds.delete(runId);
79
+ this.currentRunlessRunId = this.ignoredActiveRunIds.values().next().value ?? null;
80
+ return runId;
81
+ }
82
+ markIgnoredChunkIds(chunk) {
83
+ const messageId = getChunkMessageId(chunk);
84
+ if (messageId) this.clearedMessageIds.add(messageId);
85
+ const toolCallId = getChunkToolCallId(chunk);
86
+ if (toolCallId) this.clearedToolCallIds.add(toolCallId);
87
+ }
88
+ isRunlessChunkFromIgnoredRun(chunk) {
89
+ if (getChunkRunId(chunk) || !this.currentRunlessRunId) return false;
90
+ if (!this.ignoredActiveRunIds.has(this.currentRunlessRunId) && !this.clearedRunIds.has(this.currentRunlessRunId)) return false;
91
+ return chunk.type === "TEXT_MESSAGE_START" || chunk.type === "TEXT_MESSAGE_CONTENT" || chunk.type === "TOOL_CALL_START" || chunk.type === "TOOL_CALL_ARGS" || chunk.type === "TOOL_CALL_END" || chunk.type === "TOOL_CALL_RESULT" || chunk.type === "MESSAGES_SNAPSHOT" || chunk.type === "RUN_ERROR";
92
+ }
93
+ };
94
+ //#endregion
95
+ export { ClearedStreamTracker };
96
+
97
+ //# sourceMappingURL=cleared-stream-tracker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cleared-stream-tracker.js","names":[],"sources":["../../src/cleared-stream-tracker.ts"],"sourcesContent":["import { getChunkRunId } from './connection-adapters'\nimport type { StreamChunk } from '@tanstack/ai/client'\nimport type { UIMessage } from './types'\n\nfunction getChunkToolCallId(chunk: StreamChunk): string | undefined {\n return 'toolCallId' in chunk && typeof chunk.toolCallId === 'string'\n ? chunk.toolCallId\n : undefined\n}\n\nfunction getChunkMessageId(chunk: StreamChunk): string | undefined {\n return 'messageId' in chunk && typeof chunk.messageId === 'string'\n ? chunk.messageId\n : undefined\n}\n\nfunction getChunkParentMessageId(chunk: StreamChunk): string | undefined {\n return 'parentMessageId' in chunk && typeof chunk.parentMessageId === 'string'\n ? chunk.parentMessageId\n : undefined\n}\n\n/** Tracks stream chunks that must be ignored after the owning chat is cleared. */\nexport class ClearedStreamTracker {\n private readonly clearedMessageIds = new Set<string>()\n private readonly clearedRunIds = new Set<string>()\n private readonly ignoredActiveRunIds = new Set<string>()\n private readonly clearedToolCallIds = new Set<string>()\n private currentRunlessRunId: string | null = null\n\n snapshotClear(context: {\n messages: Array<UIMessage>\n activeRunIds: Set<string>\n currentRunId: string | null\n }): void {\n for (const message of context.messages) {\n this.clearedMessageIds.add(message.id)\n }\n for (const runId of context.activeRunIds) {\n this.clearedRunIds.add(runId)\n this.ignoredActiveRunIds.add(runId)\n }\n if (context.currentRunId) {\n this.clearedRunIds.add(context.currentRunId)\n this.ignoredActiveRunIds.add(context.currentRunId)\n }\n }\n\n shouldIgnoreChunk(chunk: StreamChunk): boolean {\n const runId = getChunkRunId(chunk)\n if (runId && this.clearedRunIds.has(runId)) {\n if (chunk.type === 'RUN_STARTED') {\n this.ignoredActiveRunIds.add(runId)\n this.currentRunlessRunId = runId\n }\n this.markIgnoredChunkIds(chunk)\n return true\n }\n\n if (runId && this.ignoredActiveRunIds.has(runId)) {\n this.markIgnoredChunkIds(chunk)\n return true\n }\n\n if (this.isRunlessChunkFromIgnoredRun(chunk)) {\n this.markIgnoredChunkIds(chunk)\n return true\n }\n\n const toolCallId = getChunkToolCallId(chunk)\n if (toolCallId && this.clearedToolCallIds.has(toolCallId)) {\n return true\n }\n\n const parentMessageId = getChunkParentMessageId(chunk)\n if (parentMessageId && this.clearedMessageIds.has(parentMessageId)) {\n if (toolCallId) {\n this.clearedToolCallIds.add(toolCallId)\n }\n return true\n }\n\n const messageId = getChunkMessageId(chunk)\n return Boolean(messageId && this.clearedMessageIds.has(messageId))\n }\n\n onRunStarted(runId: string): void {\n this.currentRunlessRunId = runId\n }\n\n onRunSettled(runId: string): void {\n this.ignoredActiveRunIds.delete(runId)\n this.clearedRunIds.delete(runId)\n if (this.currentRunlessRunId === runId) {\n this.currentRunlessRunId =\n this.ignoredActiveRunIds.values().next().value ?? null\n }\n }\n\n onSessionRunError(): void {\n this.ignoredActiveRunIds.clear()\n this.currentRunlessRunId = null\n }\n\n resetActiveRuns(): void {\n this.ignoredActiveRunIds.clear()\n this.currentRunlessRunId = null\n }\n\n takeRunlessRunId(): string | null {\n const runId = this.currentRunlessRunId\n if (!runId) return null\n this.ignoredActiveRunIds.delete(runId)\n this.clearedRunIds.delete(runId)\n this.currentRunlessRunId =\n this.ignoredActiveRunIds.values().next().value ?? null\n return runId\n }\n\n private markIgnoredChunkIds(chunk: StreamChunk): void {\n const messageId = getChunkMessageId(chunk)\n if (messageId) {\n this.clearedMessageIds.add(messageId)\n }\n const toolCallId = getChunkToolCallId(chunk)\n if (toolCallId) {\n this.clearedToolCallIds.add(toolCallId)\n }\n }\n\n private isRunlessChunkFromIgnoredRun(chunk: StreamChunk): boolean {\n const runId = getChunkRunId(chunk)\n if (runId || !this.currentRunlessRunId) return false\n if (\n !this.ignoredActiveRunIds.has(this.currentRunlessRunId) &&\n !this.clearedRunIds.has(this.currentRunlessRunId)\n ) {\n return false\n }\n return (\n chunk.type === 'TEXT_MESSAGE_START' ||\n chunk.type === 'TEXT_MESSAGE_CONTENT' ||\n chunk.type === 'TOOL_CALL_START' ||\n chunk.type === 'TOOL_CALL_ARGS' ||\n chunk.type === 'TOOL_CALL_END' ||\n chunk.type === 'TOOL_CALL_RESULT' ||\n chunk.type === 'MESSAGES_SNAPSHOT' ||\n chunk.type === 'RUN_ERROR'\n )\n }\n}\n"],"mappings":"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"}
@@ -1,15 +1,17 @@
1
1
  import { StreamChunk } from '@tanstack/ai/client';
2
- import { ChatClientPersistence, UIMessage } from './types.js';
2
+ import { ChatClientPersistence, ChatPersistedState, ChatResumeSnapshot, UIMessage } from './types.js';
3
3
  /**
4
4
  * Encapsulates everything persistence-related for `ChatClient` so the client
5
5
  * itself stays focused on streaming and message state.
6
6
  *
7
7
  * Two responsibilities live here:
8
8
  *
9
- * 1. **Storage orchestration** — hydrate from `getItem(id)` on creation, save to
10
- * `setItem(id, messages)` on every change through an ordered write queue, and
11
- * `removeItem(id)` on clear. A generation counter discards stale writes when a
12
- * removal or a newer conversation supersedes an in-flight async operation.
9
+ * 1. **Storage orchestration** — hydrate from `getItem(id)` on creation, save a
10
+ * combined `{ messages, resume? }` record via `setItem` on every change
11
+ * through an ordered write queue, and `removeItem(id)` on clear (or when
12
+ * both the transcript and resume pointer are empty). A generation counter
13
+ * discards stale writes when a removal or a newer conversation supersedes
14
+ * an in-flight async operation.
13
15
  * 2. **Clear-during-stream suppression** — when a conversation is cleared while a
14
16
  * stream is still producing, late chunks for the cleared run(s) must not
15
17
  * repopulate the now-empty state. The persistor tracks the cleared ids and
@@ -22,33 +24,44 @@ export declare class ChatPersistor {
22
24
  private readonly adapter;
23
25
  private readonly id;
24
26
  private readonly applyMessages;
27
+ private readonly applyResume?;
25
28
  private skipNextPersist;
26
29
  private generation;
27
30
  private queue;
28
31
  private queuePending;
29
32
  private messagesGeneration;
33
+ private lastMessages;
34
+ private lastResume;
30
35
  private readonly clearedMessageIds;
31
36
  private readonly clearedRunIds;
32
37
  private readonly ignoredActiveRunIds;
33
38
  private readonly clearedToolCallIds;
34
39
  private currentRunlessRunId;
35
- constructor(adapter: ChatClientPersistence, id: string, applyMessages: (messages: Array<UIMessage>) => void);
40
+ constructor(adapter: ChatClientPersistence, id: string, applyMessages: (messages: Array<UIMessage>) => void, applyResume?: ((snapshot: ChatResumeSnapshot) => void) | undefined);
41
+ /** Persist the current state as one combined `{ messages, resume? }` record. */
42
+ private writeState;
36
43
  /**
37
- * Synchronously read the persisted messages for constructor-time hydration.
38
- * Returns the raw `getItem` result (which may be a promise for async stores).
44
+ * Synchronously read the persisted state for constructor-time hydration.
45
+ * Returns the normalized combined record, or a promise of it for async stores.
39
46
  */
40
- readInitial(): Array<UIMessage> | null | undefined | Promise<Array<UIMessage> | null | undefined>;
47
+ readInitial(): ChatPersistedState | undefined | Promise<ChatPersistedState | undefined>;
41
48
  /**
42
- * Apply messages from an async `getItem` once it resolves, unless the message
49
+ * Apply state from an async `getItem` once it resolves, unless the message
43
50
  * list has already changed since hydration began.
44
51
  */
45
- hydrateAsync(persistedMessages: Array<UIMessage> | null | undefined | Promise<Array<UIMessage> | null | undefined>): void;
52
+ hydrateAsync(persistedState: ChatPersistedState | undefined | Promise<ChatPersistedState | undefined>): void;
46
53
  /**
47
- * Record a message-list change and queue a `setItem` write for it. Skips a
54
+ * Record a message-list change and queue a combined write for it. Skips a
48
55
  * single write after {@link beginClear} so the clear's empty snapshot isn't
49
56
  * persisted between `clearMessages()` and {@link remove}.
50
57
  */
51
58
  notifyMessagesChanged(messages: Array<UIMessage>): void;
59
+ /**
60
+ * Record the current resume snapshot (which run to rejoin / which interrupts
61
+ * are pending) and persist it alongside the messages. Pass `null` to clear it
62
+ * once the run reaches a non-interrupt terminal.
63
+ */
64
+ persistResumeSnapshot(snapshot: ChatResumeSnapshot | null): void;
52
65
  /** Remove the persisted conversation. Invalidates any queued writes. */
53
66
  remove(): void;
54
67
  private runOperation;
@@ -1,243 +1,268 @@
1
1
  import { getChunkRunId } from "./connection-adapters.js";
2
+ //#region src/client-persistor.ts
3
+ /** Normalize a raw `getItem` result (legacy bare array or combined record). */
4
+ function normalizePersistedState(raw) {
5
+ if (Array.isArray(raw)) return { messages: raw };
6
+ if (raw && Array.isArray(raw.messages)) return raw;
7
+ }
2
8
  function getChunkToolCallId(chunk) {
3
- return "toolCallId" in chunk && typeof chunk.toolCallId === "string" ? chunk.toolCallId : void 0;
9
+ return "toolCallId" in chunk && typeof chunk.toolCallId === "string" ? chunk.toolCallId : void 0;
4
10
  }
5
11
  function getChunkMessageId(chunk) {
6
- return "messageId" in chunk && typeof chunk.messageId === "string" ? chunk.messageId : void 0;
12
+ return "messageId" in chunk && typeof chunk.messageId === "string" ? chunk.messageId : void 0;
7
13
  }
8
14
  function getChunkParentMessageId(chunk) {
9
- return "parentMessageId" in chunk && typeof chunk.parentMessageId === "string" ? chunk.parentMessageId : void 0;
10
- }
11
- class ChatPersistor {
12
- constructor(adapter, id, applyMessages) {
13
- this.adapter = adapter;
14
- this.id = id;
15
- this.applyMessages = applyMessages;
16
- }
17
- adapter;
18
- id;
19
- applyMessages;
20
- // --- storage queue state ---
21
- skipNextPersist = false;
22
- generation = 0;
23
- queue = Promise.resolve();
24
- queuePending = false;
25
- // Bumped on every message change; lets an in-flight async hydration detect
26
- // that the message list moved on and avoid clobbering it.
27
- messagesGeneration = 0;
28
- // --- clear-during-stream suppression state ---
29
- clearedMessageIds = /* @__PURE__ */ new Set();
30
- clearedRunIds = /* @__PURE__ */ new Set();
31
- ignoredActiveRunIds = /* @__PURE__ */ new Set();
32
- clearedToolCallIds = /* @__PURE__ */ new Set();
33
- currentRunlessRunId = null;
34
- // ---------------------------------------------------------------------------
35
- // Storage orchestration
36
- // ---------------------------------------------------------------------------
37
- /**
38
- * Synchronously read the persisted messages for constructor-time hydration.
39
- * Returns the raw `getItem` result (which may be a promise for async stores).
40
- */
41
- readInitial() {
42
- try {
43
- return this.adapter.getItem(this.id);
44
- } catch {
45
- return void 0;
46
- }
47
- }
48
- /**
49
- * Apply messages from an async `getItem` once it resolves, unless the message
50
- * list has already changed since hydration began.
51
- */
52
- hydrateAsync(persistedMessages) {
53
- if (!(persistedMessages instanceof Promise)) {
54
- return;
55
- }
56
- const hydrationGeneration = this.messagesGeneration;
57
- persistedMessages.then((messages) => {
58
- if (Array.isArray(messages) && this.messagesGeneration === hydrationGeneration) {
59
- this.applyMessages(messages);
60
- }
61
- }).catch(() => {
62
- });
63
- }
64
- /**
65
- * Record a message-list change and queue a `setItem` write for it. Skips a
66
- * single write after {@link beginClear} so the clear's empty snapshot isn't
67
- * persisted between `clearMessages()` and {@link remove}.
68
- */
69
- notifyMessagesChanged(messages) {
70
- this.messagesGeneration++;
71
- if (this.skipNextPersist) {
72
- this.skipNextPersist = false;
73
- return;
74
- }
75
- const generation = this.generation;
76
- const messagesSnapshot = [...messages];
77
- this.runOperation(() => {
78
- if (generation !== this.generation) {
79
- return;
80
- }
81
- return this.adapter.setItem(this.id, messagesSnapshot);
82
- });
83
- }
84
- /** Remove the persisted conversation. Invalidates any queued writes. */
85
- remove() {
86
- const generation = ++this.generation;
87
- this.runOperation(() => {
88
- if (generation !== this.generation) {
89
- return;
90
- }
91
- return this.adapter.removeItem(this.id);
92
- });
93
- }
94
- runOperation(operation) {
95
- if (this.queuePending) {
96
- const queued = this.queue.then(operation).catch(() => {
97
- });
98
- this.queue = queued;
99
- void queued.finally(() => {
100
- if (this.queue === queued) {
101
- this.queuePending = false;
102
- }
103
- });
104
- return;
105
- }
106
- try {
107
- const result = operation();
108
- if (result instanceof Promise) {
109
- this.queuePending = true;
110
- const queued = result.catch(() => {
111
- });
112
- this.queue = queued;
113
- void queued.finally(() => {
114
- if (this.queue === queued) {
115
- this.queuePending = false;
116
- }
117
- });
118
- }
119
- } catch {
120
- }
121
- }
122
- // ---------------------------------------------------------------------------
123
- // Clear-during-stream suppression
124
- // ---------------------------------------------------------------------------
125
- /**
126
- * Capture the message/run ids that exist at the moment of a clear so chunks
127
- * still arriving for them can be ignored.
128
- */
129
- snapshotClear(context) {
130
- for (const message of context.messages) {
131
- this.clearedMessageIds.add(message.id);
132
- }
133
- for (const runId of context.activeRunIds) {
134
- this.clearedRunIds.add(runId);
135
- this.ignoredActiveRunIds.add(runId);
136
- }
137
- if (context.currentRunId) {
138
- this.clearedRunIds.add(context.currentRunId);
139
- this.ignoredActiveRunIds.add(context.currentRunId);
140
- }
141
- }
142
- /** Mark that the next persisted message change (the clear itself) is skipped. */
143
- beginClear() {
144
- this.skipNextPersist = true;
145
- }
146
- /** Whether a chunk belongs to cleared state and should not be processed. */
147
- shouldIgnoreChunk(chunk) {
148
- const runId = getChunkRunId(chunk);
149
- if (runId && this.clearedRunIds.has(runId)) {
150
- if (chunk.type === "RUN_STARTED") {
151
- this.ignoredActiveRunIds.add(runId);
152
- this.currentRunlessRunId = runId;
153
- }
154
- this.markIgnoredChunkIds(chunk);
155
- return true;
156
- }
157
- if (runId && this.ignoredActiveRunIds.has(runId)) {
158
- this.markIgnoredChunkIds(chunk);
159
- return true;
160
- }
161
- if (this.isRunlessChunkFromIgnoredRun(chunk)) {
162
- this.markIgnoredChunkIds(chunk);
163
- return true;
164
- }
165
- const toolCallId = getChunkToolCallId(chunk);
166
- if (toolCallId && this.clearedToolCallIds.has(toolCallId)) {
167
- return true;
168
- }
169
- const parentMessageId = getChunkParentMessageId(chunk);
170
- if (parentMessageId && this.clearedMessageIds.has(parentMessageId)) {
171
- if (toolCallId) {
172
- this.clearedToolCallIds.add(toolCallId);
173
- }
174
- return true;
175
- }
176
- const messageId = getChunkMessageId(chunk);
177
- if (!messageId) {
178
- return false;
179
- }
180
- if (this.clearedMessageIds.has(messageId)) {
181
- return true;
182
- }
183
- return false;
184
- }
185
- /**
186
- * The owning client calls this when a run starts so runless content chunks
187
- * (adapters that omit `runId` on content events) can be attributed to it.
188
- */
189
- onRunStarted(runId) {
190
- this.currentRunlessRunId = runId;
191
- }
192
- /** Forget a settled run, advancing the runless pointer to another ignored run. */
193
- onRunSettled(runId) {
194
- this.ignoredActiveRunIds.delete(runId);
195
- this.clearedRunIds.delete(runId);
196
- if (this.currentRunlessRunId === runId) {
197
- this.currentRunlessRunId = this.ignoredActiveRunIds.values().next().value ?? null;
198
- }
199
- }
200
- /** A session-level (runId-less) RUN_ERROR clears all ignored-run tracking. */
201
- onSessionRunError() {
202
- this.ignoredActiveRunIds.clear();
203
- this.currentRunlessRunId = null;
204
- }
205
- /** Clear the ignored-active-run markers (mirrors a session-generating reset). */
206
- resetIgnored() {
207
- this.ignoredActiveRunIds.clear();
208
- }
209
- /**
210
- * Consume the current runless run id (if any), forgetting it. Used when an
211
- * ignored, runId-less RUN_ERROR drains the run the client is still tracking.
212
- */
213
- takeRunlessRunId() {
214
- const runId = this.currentRunlessRunId;
215
- if (!runId) return null;
216
- this.ignoredActiveRunIds.delete(runId);
217
- this.clearedRunIds.delete(runId);
218
- this.currentRunlessRunId = this.ignoredActiveRunIds.values().next().value ?? null;
219
- return runId;
220
- }
221
- markIgnoredChunkIds(chunk) {
222
- const messageId = getChunkMessageId(chunk);
223
- if (messageId) {
224
- this.clearedMessageIds.add(messageId);
225
- }
226
- const toolCallId = getChunkToolCallId(chunk);
227
- if (toolCallId) {
228
- this.clearedToolCallIds.add(toolCallId);
229
- }
230
- }
231
- isRunlessChunkFromIgnoredRun(chunk) {
232
- const runId = getChunkRunId(chunk);
233
- if (runId || !this.currentRunlessRunId) return false;
234
- if (!this.ignoredActiveRunIds.has(this.currentRunlessRunId) && !this.clearedRunIds.has(this.currentRunlessRunId)) {
235
- return false;
236
- }
237
- return chunk.type === "TEXT_MESSAGE_START" || chunk.type === "TEXT_MESSAGE_CONTENT" || chunk.type === "TOOL_CALL_START" || chunk.type === "TOOL_CALL_ARGS" || chunk.type === "TOOL_CALL_END" || chunk.type === "TOOL_CALL_RESULT" || chunk.type === "MESSAGES_SNAPSHOT" || chunk.type === "RUN_ERROR";
238
- }
15
+ return "parentMessageId" in chunk && typeof chunk.parentMessageId === "string" ? chunk.parentMessageId : void 0;
239
16
  }
240
- export {
241
- ChatPersistor
17
+ /**
18
+ * Encapsulates everything persistence-related for `ChatClient` so the client
19
+ * itself stays focused on streaming and message state.
20
+ *
21
+ * Two responsibilities live here:
22
+ *
23
+ * 1. **Storage orchestration** — hydrate from `getItem(id)` on creation, save a
24
+ * combined `{ messages, resume? }` record via `setItem` on every change
25
+ * through an ordered write queue, and `removeItem(id)` on clear (or when
26
+ * both the transcript and resume pointer are empty). A generation counter
27
+ * discards stale writes when a removal or a newer conversation supersedes
28
+ * an in-flight async operation.
29
+ * 2. **Clear-during-stream suppression** — when a conversation is cleared while a
30
+ * stream is still producing, late chunks for the cleared run(s) must not
31
+ * repopulate the now-empty state. The persistor tracks the cleared ids and
32
+ * decides, per chunk, whether the client should ignore it.
33
+ *
34
+ * All adapter calls are best-effort: a throwing or rejecting adapter is swallowed
35
+ * so storage problems never break the chat.
36
+ */
37
+ var ChatPersistor = class {
38
+ adapter;
39
+ id;
40
+ applyMessages;
41
+ applyResume;
42
+ skipNextPersist = false;
43
+ generation = 0;
44
+ queue = Promise.resolve();
45
+ queuePending = false;
46
+ messagesGeneration = 0;
47
+ lastMessages = [];
48
+ lastResume = null;
49
+ clearedMessageIds = /* @__PURE__ */ new Set();
50
+ clearedRunIds = /* @__PURE__ */ new Set();
51
+ ignoredActiveRunIds = /* @__PURE__ */ new Set();
52
+ clearedToolCallIds = /* @__PURE__ */ new Set();
53
+ currentRunlessRunId = null;
54
+ constructor(adapter, id, applyMessages, applyResume) {
55
+ this.adapter = adapter;
56
+ this.id = id;
57
+ this.applyMessages = applyMessages;
58
+ this.applyResume = applyResume;
59
+ }
60
+ /** Persist the current state as one combined `{ messages, resume? }` record. */
61
+ writeState() {
62
+ const messages = [...this.lastMessages];
63
+ if (messages.length === 0 && !this.lastResume) {
64
+ const generation = this.generation;
65
+ this.runOperation(() => {
66
+ if (generation !== this.generation) return;
67
+ return this.adapter.removeItem(this.id);
68
+ });
69
+ return;
70
+ }
71
+ const generation = this.generation;
72
+ const state = {
73
+ messages,
74
+ ...this.lastResume ? { resume: this.lastResume } : {}
75
+ };
76
+ this.runOperation(() => {
77
+ if (generation !== this.generation) return;
78
+ return this.adapter.setItem(this.id, state);
79
+ });
80
+ }
81
+ /**
82
+ * Synchronously read the persisted state for constructor-time hydration.
83
+ * Returns the normalized combined record, or a promise of it for async stores.
84
+ */
85
+ readInitial() {
86
+ try {
87
+ const raw = this.adapter.getItem(this.id);
88
+ if (raw instanceof Promise) return raw.then(normalizePersistedState).catch(() => void 0);
89
+ const state = normalizePersistedState(raw);
90
+ if (state) {
91
+ this.lastMessages = state.messages;
92
+ this.lastResume = state.resume ?? null;
93
+ }
94
+ return state;
95
+ } catch {
96
+ return;
97
+ }
98
+ }
99
+ /**
100
+ * Apply state from an async `getItem` once it resolves, unless the message
101
+ * list has already changed since hydration began.
102
+ */
103
+ hydrateAsync(persistedState) {
104
+ if (!(persistedState instanceof Promise)) return;
105
+ const hydrationGeneration = this.messagesGeneration;
106
+ persistedState.then((state) => {
107
+ if (!state || this.messagesGeneration !== hydrationGeneration) return;
108
+ this.lastResume = state.resume ?? null;
109
+ this.lastMessages = state.messages;
110
+ this.applyMessages(state.messages);
111
+ if (state.resume && this.applyResume) this.applyResume(state.resume);
112
+ }).catch(() => {});
113
+ }
114
+ /**
115
+ * Record a message-list change and queue a combined write for it. Skips a
116
+ * single write after {@link beginClear} so the clear's empty snapshot isn't
117
+ * persisted between `clearMessages()` and {@link remove}.
118
+ */
119
+ notifyMessagesChanged(messages) {
120
+ this.messagesGeneration++;
121
+ this.lastMessages = [...messages];
122
+ if (this.skipNextPersist) {
123
+ this.skipNextPersist = false;
124
+ return;
125
+ }
126
+ this.writeState();
127
+ }
128
+ /**
129
+ * Record the current resume snapshot (which run to rejoin / which interrupts
130
+ * are pending) and persist it alongside the messages. Pass `null` to clear it
131
+ * once the run reaches a non-interrupt terminal.
132
+ */
133
+ persistResumeSnapshot(snapshot) {
134
+ this.lastResume = snapshot;
135
+ if (this.skipNextPersist) return;
136
+ this.writeState();
137
+ }
138
+ /** Remove the persisted conversation. Invalidates any queued writes. */
139
+ remove() {
140
+ this.lastMessages = [];
141
+ this.lastResume = null;
142
+ const generation = ++this.generation;
143
+ this.runOperation(() => {
144
+ if (generation !== this.generation) return;
145
+ return this.adapter.removeItem(this.id);
146
+ });
147
+ }
148
+ runOperation(operation) {
149
+ if (this.queuePending) {
150
+ const queued = this.queue.then(operation).catch(() => {});
151
+ this.queue = queued;
152
+ queued.finally(() => {
153
+ if (this.queue === queued) this.queuePending = false;
154
+ });
155
+ return;
156
+ }
157
+ try {
158
+ const result = operation();
159
+ if (result instanceof Promise) {
160
+ this.queuePending = true;
161
+ const queued = result.catch(() => {});
162
+ this.queue = queued;
163
+ queued.finally(() => {
164
+ if (this.queue === queued) this.queuePending = false;
165
+ });
166
+ }
167
+ } catch {}
168
+ }
169
+ /**
170
+ * Capture the message/run ids that exist at the moment of a clear so chunks
171
+ * still arriving for them can be ignored.
172
+ */
173
+ snapshotClear(context) {
174
+ for (const message of context.messages) this.clearedMessageIds.add(message.id);
175
+ for (const runId of context.activeRunIds) {
176
+ this.clearedRunIds.add(runId);
177
+ this.ignoredActiveRunIds.add(runId);
178
+ }
179
+ if (context.currentRunId) {
180
+ this.clearedRunIds.add(context.currentRunId);
181
+ this.ignoredActiveRunIds.add(context.currentRunId);
182
+ }
183
+ }
184
+ /** Mark that the next persisted message change (the clear itself) is skipped. */
185
+ beginClear() {
186
+ this.skipNextPersist = true;
187
+ }
188
+ /** Whether a chunk belongs to cleared state and should not be processed. */
189
+ shouldIgnoreChunk(chunk) {
190
+ const runId = getChunkRunId(chunk);
191
+ if (runId && this.clearedRunIds.has(runId)) {
192
+ if (chunk.type === "RUN_STARTED") {
193
+ this.ignoredActiveRunIds.add(runId);
194
+ this.currentRunlessRunId = runId;
195
+ }
196
+ this.markIgnoredChunkIds(chunk);
197
+ return true;
198
+ }
199
+ if (runId && this.ignoredActiveRunIds.has(runId)) {
200
+ this.markIgnoredChunkIds(chunk);
201
+ return true;
202
+ }
203
+ if (this.isRunlessChunkFromIgnoredRun(chunk)) {
204
+ this.markIgnoredChunkIds(chunk);
205
+ return true;
206
+ }
207
+ const toolCallId = getChunkToolCallId(chunk);
208
+ if (toolCallId && this.clearedToolCallIds.has(toolCallId)) return true;
209
+ const parentMessageId = getChunkParentMessageId(chunk);
210
+ if (parentMessageId && this.clearedMessageIds.has(parentMessageId)) {
211
+ if (toolCallId) this.clearedToolCallIds.add(toolCallId);
212
+ return true;
213
+ }
214
+ const messageId = getChunkMessageId(chunk);
215
+ if (!messageId) return false;
216
+ if (this.clearedMessageIds.has(messageId)) return true;
217
+ return false;
218
+ }
219
+ /**
220
+ * The owning client calls this when a run starts so runless content chunks
221
+ * (adapters that omit `runId` on content events) can be attributed to it.
222
+ */
223
+ onRunStarted(runId) {
224
+ this.currentRunlessRunId = runId;
225
+ }
226
+ /** Forget a settled run, advancing the runless pointer to another ignored run. */
227
+ onRunSettled(runId) {
228
+ this.ignoredActiveRunIds.delete(runId);
229
+ this.clearedRunIds.delete(runId);
230
+ if (this.currentRunlessRunId === runId) this.currentRunlessRunId = this.ignoredActiveRunIds.values().next().value ?? null;
231
+ }
232
+ /** A session-level (runId-less) RUN_ERROR clears all ignored-run tracking. */
233
+ onSessionRunError() {
234
+ this.ignoredActiveRunIds.clear();
235
+ this.currentRunlessRunId = null;
236
+ }
237
+ /** Clear the ignored-active-run markers (mirrors a session-generating reset). */
238
+ resetIgnored() {
239
+ this.ignoredActiveRunIds.clear();
240
+ }
241
+ /**
242
+ * Consume the current runless run id (if any), forgetting it. Used when an
243
+ * ignored, runId-less RUN_ERROR drains the run the client is still tracking.
244
+ */
245
+ takeRunlessRunId() {
246
+ const runId = this.currentRunlessRunId;
247
+ if (!runId) return null;
248
+ this.ignoredActiveRunIds.delete(runId);
249
+ this.clearedRunIds.delete(runId);
250
+ this.currentRunlessRunId = this.ignoredActiveRunIds.values().next().value ?? null;
251
+ return runId;
252
+ }
253
+ markIgnoredChunkIds(chunk) {
254
+ const messageId = getChunkMessageId(chunk);
255
+ if (messageId) this.clearedMessageIds.add(messageId);
256
+ const toolCallId = getChunkToolCallId(chunk);
257
+ if (toolCallId) this.clearedToolCallIds.add(toolCallId);
258
+ }
259
+ isRunlessChunkFromIgnoredRun(chunk) {
260
+ if (getChunkRunId(chunk) || !this.currentRunlessRunId) return false;
261
+ if (!this.ignoredActiveRunIds.has(this.currentRunlessRunId) && !this.clearedRunIds.has(this.currentRunlessRunId)) return false;
262
+ return chunk.type === "TEXT_MESSAGE_START" || chunk.type === "TEXT_MESSAGE_CONTENT" || chunk.type === "TOOL_CALL_START" || chunk.type === "TOOL_CALL_ARGS" || chunk.type === "TOOL_CALL_END" || chunk.type === "TOOL_CALL_RESULT" || chunk.type === "MESSAGES_SNAPSHOT" || chunk.type === "RUN_ERROR";
263
+ }
242
264
  };
243
- //# sourceMappingURL=client-persistor.js.map
265
+ //#endregion
266
+ export { ChatPersistor };
267
+
268
+ //# sourceMappingURL=client-persistor.js.map