@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
@@ -1,1178 +1,1028 @@
1
- import { emitAIDevtoolsEvent, aiEventClient, createAIDevtoolsEventEnvelope } from "@tanstack/ai-event-client";
2
- import { convertSchemaToJsonSchema } from "@tanstack/ai/client";
3
1
  import { DefaultChatClientEventEmitter } from "./events.js";
2
+ import { convertSchemaToJsonSchema } from "@tanstack/ai/client";
3
+ import { aiEventClient, createAIDevtoolsEventEnvelope, emitAIDevtoolsEvent } from "@tanstack/ai-event-client";
4
+ //#region src/devtools.ts
4
5
  function createAIDevtoolsGenerationPreview(input) {
5
- if (input.outputKind === "image") {
6
- return imagePreviewFromResult(input.result);
7
- }
8
- if (input.outputKind === "audio") {
9
- return audioPreviewFromResult(input.result);
10
- }
11
- if (input.outputKind === "video") {
12
- return videoPreviewFromResult(input.result, input.videoStatus);
13
- }
14
- if (input.outputKind === "text") {
15
- return textPreviewFromResult(input.result);
16
- }
17
- if (input.result === null || input.result === void 0) {
18
- return { kind: "empty" };
19
- }
20
- return {
21
- kind: "structured",
22
- value: input.result
23
- };
6
+ if (input.outputKind === "image") return imagePreviewFromResult(input.result);
7
+ if (input.outputKind === "audio") return audioPreviewFromResult(input.result);
8
+ if (input.outputKind === "video") return videoPreviewFromResult(input.result, input.videoStatus);
9
+ if (input.outputKind === "text") return textPreviewFromResult(input.result);
10
+ if (input.result === null || input.result === void 0) return { kind: "empty" };
11
+ return {
12
+ kind: "structured",
13
+ value: input.result
14
+ };
24
15
  }
25
16
  function imagePreviewFromResult(result) {
26
- const record = asRecord(result);
27
- const images = Array.isArray(record?.images) ? record.images : [];
28
- const items = images.map((image) => mediaItemFromSource(image, "image/png")).filter(isGenerationMediaItem);
29
- if (items.length === 0 && result !== null && result !== void 0) {
30
- const directItem = mediaItemFromSource(result, "image/png");
31
- if (directItem) {
32
- items.push(directItem);
33
- }
34
- }
35
- return { kind: "image", items };
17
+ const record = asRecord(result);
18
+ const items = (Array.isArray(record?.images) ? record.images : []).map((image) => mediaItemFromSource(image, "image/png")).filter(isGenerationMediaItem);
19
+ if (items.length === 0 && result !== null && result !== void 0) {
20
+ const directItem = mediaItemFromSource(result, "image/png");
21
+ if (directItem) items.push(directItem);
22
+ }
23
+ return {
24
+ kind: "image",
25
+ items
26
+ };
36
27
  }
37
28
  function audioPreviewFromResult(result) {
38
- const record = asRecord(result);
39
- const audio = record?.audio;
40
- const resultContentType = stringField(record, "contentType");
41
- const format = stringField(record, "format");
42
- const mimeType = resultContentType ?? mimeTypeFromAudioFormat(format);
43
- const items = [];
44
- const directItem = typeof audio === "string" ? base64MediaItem(audio, mimeType, {
45
- format,
46
- duration: numberField(record, "duration")
47
- }) : mediaItemFromSource(audio, mimeType, {
48
- format
49
- });
50
- if (directItem) {
51
- items.push(directItem);
52
- }
53
- return { kind: "audio", items };
29
+ const record = asRecord(result);
30
+ const audio = record?.audio;
31
+ const resultContentType = stringField(record, "contentType");
32
+ const format = stringField(record, "format");
33
+ const mimeType = resultContentType ?? mimeTypeFromAudioFormat(format);
34
+ const items = [];
35
+ const directItem = typeof audio === "string" ? base64MediaItem(audio, mimeType, {
36
+ format,
37
+ duration: numberField(record, "duration")
38
+ }) : mediaItemFromSource(audio, mimeType, { format });
39
+ if (directItem) items.push(directItem);
40
+ return {
41
+ kind: "audio",
42
+ items
43
+ };
54
44
  }
55
45
  function videoPreviewFromResult(result, videoStatus) {
56
- const resultRecord = asRecord(result);
57
- const statusRecord = asRecord(videoStatus);
58
- const item = mediaItemFromSource(result, "video/mp4") ?? mediaItemFromSource(videoStatus, "video/mp4");
59
- const items = item ? [item] : [];
60
- const job = videoJobFromStatus(statusRecord ?? resultRecord);
61
- return {
62
- kind: "video",
63
- items,
64
- ...job ? { job } : {}
65
- };
46
+ const resultRecord = asRecord(result);
47
+ const statusRecord = asRecord(videoStatus);
48
+ const item = mediaItemFromSource(result, "video/mp4") ?? mediaItemFromSource(videoStatus, "video/mp4");
49
+ const items = item ? [item] : [];
50
+ const job = videoJobFromStatus(statusRecord ?? resultRecord);
51
+ return {
52
+ kind: "video",
53
+ items,
54
+ ...job ? { job } : {}
55
+ };
66
56
  }
67
57
  function textPreviewFromResult(result) {
68
- const record = asRecord(result);
69
- const text = stringField(record, "text") ?? stringField(record, "summary") ?? stringField(record, "content") ?? (typeof result === "string" ? result : void 0);
70
- if (text !== void 0) {
71
- return { kind: "text", text };
72
- }
73
- if (result === null || result === void 0) {
74
- return { kind: "empty" };
75
- }
76
- return {
77
- kind: "structured",
78
- value: result
79
- };
58
+ const record = asRecord(result);
59
+ const text = stringField(record, "text") ?? stringField(record, "summary") ?? stringField(record, "content") ?? (typeof result === "string" ? result : void 0);
60
+ if (text !== void 0) return {
61
+ kind: "text",
62
+ text
63
+ };
64
+ if (result === null || result === void 0) return { kind: "empty" };
65
+ return {
66
+ kind: "structured",
67
+ value: result
68
+ };
80
69
  }
81
70
  function videoJobFromStatus(record) {
82
- const jobId = stringField(record, "jobId");
83
- if (!jobId) return void 0;
84
- return {
85
- jobId,
86
- ...stringField(record, "status") ? { status: stringField(record, "status") } : {},
87
- ...numberField(record, "progress") !== void 0 ? { progress: numberField(record, "progress") } : {},
88
- ...stringField(record, "error") ? { error: stringField(record, "error") } : {}
89
- };
71
+ const jobId = stringField(record, "jobId");
72
+ if (!jobId) return void 0;
73
+ return {
74
+ jobId,
75
+ ...stringField(record, "status") ? { status: stringField(record, "status") } : {},
76
+ ...numberField(record, "progress") !== void 0 ? { progress: numberField(record, "progress") } : {},
77
+ ...stringField(record, "error") ? { error: stringField(record, "error") } : {}
78
+ };
90
79
  }
91
80
  function mediaItemFromSource(value, defaultMimeType, extras = {}) {
92
- const record = asRecord(value);
93
- if (!record) return void 0;
94
- const explicitContentType = stringField(record, "contentType") ?? stringField(record, "mimeType");
95
- const duration = numberField(record, "duration") ?? extras.duration;
96
- const format = stringField(record, "format") ?? extras.format;
97
- const url = stringField(record, "url");
98
- if (url) {
99
- return {
100
- src: url,
101
- sourceType: "url",
102
- ...explicitContentType ? { mimeType: explicitContentType } : {},
103
- ...format ? { format } : {},
104
- ...duration !== void 0 ? { duration } : {}
105
- };
106
- }
107
- const b64Json = stringField(record, "b64Json");
108
- if (!b64Json) return void 0;
109
- return base64MediaItem(b64Json, explicitContentType ?? defaultMimeType, {
110
- format,
111
- duration
112
- });
81
+ const record = asRecord(value);
82
+ if (!record) return void 0;
83
+ const explicitContentType = stringField(record, "contentType") ?? stringField(record, "mimeType");
84
+ const duration = numberField(record, "duration") ?? extras.duration;
85
+ const format = stringField(record, "format") ?? extras.format;
86
+ const url = stringField(record, "url");
87
+ if (url) return {
88
+ src: url,
89
+ sourceType: "url",
90
+ ...explicitContentType ? { mimeType: explicitContentType } : {},
91
+ ...format ? { format } : {},
92
+ ...duration !== void 0 ? { duration } : {}
93
+ };
94
+ const b64Json = stringField(record, "b64Json");
95
+ if (!b64Json) return void 0;
96
+ return base64MediaItem(b64Json, explicitContentType ?? defaultMimeType, {
97
+ format,
98
+ duration
99
+ });
113
100
  }
114
101
  function base64MediaItem(value, mimeType, extras = {}) {
115
- const src = value.startsWith("data:") ? value : `data:${mimeType ?? "application/octet-stream"};base64,${value}`;
116
- return {
117
- src,
118
- sourceType: "base64",
119
- ...mimeType ? { mimeType } : {},
120
- ...extras.format ? { format: extras.format } : {},
121
- ...extras.duration !== void 0 ? { duration: extras.duration } : {}
122
- };
102
+ return {
103
+ src: value.startsWith("data:") ? value : `data:${mimeType ?? "application/octet-stream"};base64,${value}`,
104
+ sourceType: "base64",
105
+ ...mimeType ? { mimeType } : {},
106
+ ...extras.format ? { format: extras.format } : {},
107
+ ...extras.duration !== void 0 ? { duration: extras.duration } : {}
108
+ };
123
109
  }
124
110
  function mimeTypeFromAudioFormat(format) {
125
- if (!format) return "audio/mpeg";
126
- if (format === "mp3") return "audio/mpeg";
127
- return `audio/${format}`;
111
+ if (!format) return "audio/mpeg";
112
+ if (format === "mp3") return "audio/mpeg";
113
+ return `audio/${format}`;
128
114
  }
129
115
  function asRecord(value) {
130
- if (!value || typeof value !== "object" || Array.isArray(value)) {
131
- return void 0;
132
- }
133
- return value;
116
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
117
+ return value;
134
118
  }
135
119
  function stringField(record, field) {
136
- const value = record?.[field];
137
- return typeof value === "string" && value.length > 0 ? value : void 0;
120
+ const value = record?.[field];
121
+ return typeof value === "string" && value.length > 0 ? value : void 0;
138
122
  }
139
123
  function numberField(record, field) {
140
- const value = record?.[field];
141
- return typeof value === "number" ? value : void 0;
124
+ const value = record?.[field];
125
+ return typeof value === "number" ? value : void 0;
142
126
  }
143
127
  function isGenerationMediaItem(value) {
144
- return Boolean(value);
128
+ return Boolean(value);
145
129
  }
146
- const activeBridgeRegistryKey = /* @__PURE__ */ Symbol.for(
147
- "tanstack.ai.devtools.activeBridgeByHookId"
148
- );
130
+ var activeBridgeRegistryKey = Symbol.for("tanstack.ai.devtools.activeBridgeByHookId");
149
131
  function getActiveBridgeRegistry() {
150
- const global = globalThis;
151
- const existing = global[activeBridgeRegistryKey];
152
- if (existing) return existing;
153
- const registry = /* @__PURE__ */ new Map();
154
- global[activeBridgeRegistryKey] = registry;
155
- return registry;
132
+ const global = globalThis;
133
+ const existing = global[activeBridgeRegistryKey];
134
+ if (existing) return existing;
135
+ const registry = /* @__PURE__ */ new Map();
136
+ global[activeBridgeRegistryKey] = registry;
137
+ return registry;
156
138
  }
157
- class ClientDevtoolsBridge {
158
- options;
159
- bridgeId;
160
- unsubscribers = [];
161
- disposed = false;
162
- superseded = false;
163
- registered = false;
164
- constructor(options) {
165
- this.options = options;
166
- this.bridgeId = createBridgeId(options.hookId);
167
- }
168
- emitRegistered() {
169
- if (!this.prepareForMountEmit()) {
170
- return;
171
- }
172
- this.registered = true;
173
- emitAIDevtoolsEvent("hook:registered", {
174
- ...this.createEnvelope("hook:registered"),
175
- ...this.createMetadataPayload(),
176
- lifecycle: "mounted"
177
- });
178
- }
179
- emitUpdated() {
180
- if (!this.prepareForEmit()) {
181
- return;
182
- }
183
- emitAIDevtoolsEvent("hook:updated", {
184
- ...this.createEnvelope("hook:updated"),
185
- ...this.createMetadataPayload(),
186
- lifecycle: "active"
187
- });
188
- }
189
- emitSnapshot() {
190
- if (!this.prepareForEmit()) {
191
- return;
192
- }
193
- emitAIDevtoolsEvent("hook:state-snapshot", {
194
- ...this.createEnvelope("hook:state-snapshot"),
195
- ...this.createMetadataPayload(),
196
- // Wire envelope uses Record<string, unknown>; widen the typed snapshot
197
- // here so the typed-snapshot constraint above can stay narrow.
198
- // eslint-disable-next-line no-restricted-syntax -- TSnapshot extends object is structurally compatible but TS can't see the missing index signature
199
- state: this.options.getSnapshot()
200
- });
201
- }
202
- emitToolsRegistered() {
203
- if (!this.prepareForEmit()) {
204
- return;
205
- }
206
- const tools = this.options.getTools ? Array.from(this.options.getTools()).map((tool) => ({
207
- name: tool.name,
208
- description: tool.description,
209
- inputSchema: tool.inputSchema ? convertSchemaToJsonSchema(tool.inputSchema) : { type: "object" },
210
- outputSchema: tool.outputSchema ? convertSchemaToJsonSchema(tool.outputSchema) : void 0,
211
- needsApproval: tool.needsApproval,
212
- metadata: tool.metadata
213
- })) : [];
214
- emitAIDevtoolsEvent("tools:registered", {
215
- ...this.createEnvelope("tools:registered"),
216
- ...this.createMetadataPayload(),
217
- tools
218
- });
219
- }
220
- emitRunLifecycle(eventType, runId, status, options = {}) {
221
- if (!this.prepareForEmit()) {
222
- return;
223
- }
224
- emitAIDevtoolsEvent(eventType, {
225
- ...this.createEnvelope(eventType, "client-state", { runId }),
226
- runId,
227
- status,
228
- ...options.error ? { error: options.error } : {}
229
- });
230
- }
231
- deactivate() {
232
- const activeBridgeByHookId = getActiveBridgeRegistry();
233
- if (activeBridgeByHookId.get(this.options.hookId) === this) {
234
- activeBridgeByHookId.delete(this.options.hookId);
235
- }
236
- for (const unsubscribe of this.unsubscribers.splice(0)) {
237
- unsubscribe();
238
- }
239
- }
240
- supersede() {
241
- if (this.disposed) {
242
- return;
243
- }
244
- this.superseded = true;
245
- this.disposed = true;
246
- this.deactivate();
247
- }
248
- dispose() {
249
- if (this.disposed) {
250
- return;
251
- }
252
- this.disposed = true;
253
- if (!this.registered) {
254
- this.deactivate();
255
- return;
256
- }
257
- const payload = {
258
- ...this.createEnvelope("hook:unregistered"),
259
- ...this.createMetadataPayload(),
260
- reason: "disposed"
261
- };
262
- emitAIDevtoolsEvent("hook:unregistered", payload);
263
- this.deactivate();
264
- }
265
- prepareForEmit() {
266
- if (this.disposed || this.superseded) {
267
- return false;
268
- }
269
- this.activate();
270
- return true;
271
- }
272
- prepareForMountEmit() {
273
- if (this.superseded) {
274
- return false;
275
- }
276
- if (this.disposed) {
277
- this.disposed = false;
278
- this.registered = false;
279
- }
280
- this.activate();
281
- return true;
282
- }
283
- activate() {
284
- if (this.disposed) {
285
- return;
286
- }
287
- const activeBridgeByHookId = getActiveBridgeRegistry();
288
- const activeBridge = activeBridgeByHookId.get(this.options.hookId);
289
- if (activeBridge && activeBridge !== this) {
290
- if (typeof activeBridge.supersede === "function") {
291
- activeBridge.supersede();
292
- } else {
293
- activeBridge.deactivate();
294
- }
295
- }
296
- activeBridgeByHookId.set(this.options.hookId, this);
297
- if (this.unsubscribers.length > 0) {
298
- return;
299
- }
300
- this.unsubscribers.push(
301
- aiEventClient.on("devtools:request-state", (event) => {
302
- this.handleRequestState(event);
303
- })
304
- );
305
- if (this.options.applyToolFixture) {
306
- this.unsubscribers.push(
307
- aiEventClient.on("devtools:tool-fixture:apply", (event) => {
308
- void this.handleToolFixtureApply(event);
309
- })
310
- );
311
- }
312
- }
313
- handleRequestState(event) {
314
- if (this.disposed || this.superseded) {
315
- return;
316
- }
317
- const targetHookId = event.payload.targetHookId;
318
- if (targetHookId && targetHookId !== this.options.hookId) {
319
- return;
320
- }
321
- this.emitRegistered();
322
- this.emitToolsRegistered();
323
- this.emitSnapshot();
324
- }
325
- async handleToolFixtureApply(event) {
326
- const fixture = event.payload;
327
- if (!this.matchesFixtureTarget(fixture)) {
328
- return;
329
- }
330
- await this.options.applyToolFixture?.(fixture);
331
- }
332
- matchesFixtureTarget(fixture) {
333
- if (!fixture.hookId && !fixture.threadId) {
334
- return false;
335
- }
336
- if (fixture.hookId) {
337
- return fixture.hookId === this.options.hookId;
338
- }
339
- if (fixture.threadId && (!this.options.threadId || fixture.threadId !== this.options.threadId)) {
340
- return false;
341
- }
342
- return true;
343
- }
344
- createEnvelope(eventType, visibility = "client-state", context = {}) {
345
- return createAIDevtoolsEventEnvelope({
346
- eventType,
347
- source: "client",
348
- visibility,
349
- clientId: this.options.clientId,
350
- hookId: this.options.hookId,
351
- correlationId: this.bridgeId,
352
- ...this.options.threadId ? { threadId: this.options.threadId } : {},
353
- ...context.runId ? { runId: context.runId } : {},
354
- timestamp: Date.now()
355
- });
356
- }
357
- createMetadataPayload() {
358
- return {
359
- hookId: this.options.hookId,
360
- hookName: this.options.metadata.hookName,
361
- ...this.options.metadata.name ? { displayName: this.options.metadata.name } : {},
362
- ...this.options.metadata.outputKind ? { outputKind: this.options.metadata.outputKind } : {},
363
- ...this.options.metadata.framework ? { framework: this.options.metadata.framework } : {}
364
- };
365
- }
366
- }
367
- let bridgeIdSequence = 0;
139
+ var ClientDevtoolsBridge = class {
140
+ options;
141
+ bridgeId;
142
+ unsubscribers = [];
143
+ disposed = false;
144
+ superseded = false;
145
+ registered = false;
146
+ constructor(options) {
147
+ this.options = options;
148
+ this.bridgeId = createBridgeId(options.hookId);
149
+ }
150
+ emitRegistered() {
151
+ if (!this.prepareForMountEmit()) return;
152
+ this.registered = true;
153
+ emitAIDevtoolsEvent("hook:registered", {
154
+ ...this.createEnvelope("hook:registered"),
155
+ ...this.createMetadataPayload(),
156
+ lifecycle: "mounted"
157
+ });
158
+ }
159
+ emitUpdated() {
160
+ if (!this.prepareForEmit()) return;
161
+ emitAIDevtoolsEvent("hook:updated", {
162
+ ...this.createEnvelope("hook:updated"),
163
+ ...this.createMetadataPayload(),
164
+ lifecycle: "active"
165
+ });
166
+ }
167
+ emitSnapshot() {
168
+ if (!this.prepareForEmit()) return;
169
+ emitAIDevtoolsEvent("hook:state-snapshot", {
170
+ ...this.createEnvelope("hook:state-snapshot"),
171
+ ...this.createMetadataPayload(),
172
+ state: this.options.getSnapshot()
173
+ });
174
+ }
175
+ emitToolsRegistered() {
176
+ if (!this.prepareForEmit()) return;
177
+ const tools = this.options.getTools ? Array.from(this.options.getTools()).map((tool) => ({
178
+ name: tool.name,
179
+ description: tool.description,
180
+ inputSchema: tool.inputSchema ? convertSchemaToJsonSchema(tool.inputSchema) : { type: "object" },
181
+ outputSchema: tool.outputSchema ? convertSchemaToJsonSchema(tool.outputSchema) : void 0,
182
+ needsApproval: tool.needsApproval,
183
+ metadata: tool.metadata
184
+ })) : [];
185
+ emitAIDevtoolsEvent("tools:registered", {
186
+ ...this.createEnvelope("tools:registered"),
187
+ ...this.createMetadataPayload(),
188
+ tools
189
+ });
190
+ }
191
+ emitRunLifecycle(eventType, runId, status, options = {}) {
192
+ if (!this.prepareForEmit()) return;
193
+ emitAIDevtoolsEvent(eventType, {
194
+ ...this.createEnvelope(eventType, "client-state", { runId }),
195
+ runId,
196
+ status,
197
+ ...options.error ? { error: options.error } : {}
198
+ });
199
+ }
200
+ deactivate() {
201
+ const activeBridgeByHookId = getActiveBridgeRegistry();
202
+ if (activeBridgeByHookId.get(this.options.hookId) === this) activeBridgeByHookId.delete(this.options.hookId);
203
+ for (const unsubscribe of this.unsubscribers.splice(0)) unsubscribe();
204
+ }
205
+ supersede() {
206
+ if (this.disposed) return;
207
+ this.superseded = true;
208
+ this.disposed = true;
209
+ this.deactivate();
210
+ }
211
+ dispose() {
212
+ if (this.disposed) return;
213
+ this.disposed = true;
214
+ if (!this.registered) {
215
+ this.deactivate();
216
+ return;
217
+ }
218
+ emitAIDevtoolsEvent("hook:unregistered", {
219
+ ...this.createEnvelope("hook:unregistered"),
220
+ ...this.createMetadataPayload(),
221
+ reason: "disposed"
222
+ });
223
+ this.deactivate();
224
+ }
225
+ prepareForEmit() {
226
+ if (this.disposed || this.superseded) return false;
227
+ this.activate();
228
+ return true;
229
+ }
230
+ prepareForMountEmit() {
231
+ if (this.superseded) return false;
232
+ if (this.disposed) {
233
+ this.disposed = false;
234
+ this.registered = false;
235
+ }
236
+ this.activate();
237
+ return true;
238
+ }
239
+ activate() {
240
+ if (this.disposed) return;
241
+ const activeBridgeByHookId = getActiveBridgeRegistry();
242
+ const activeBridge = activeBridgeByHookId.get(this.options.hookId);
243
+ if (activeBridge && activeBridge !== this) if (typeof activeBridge.supersede === "function") activeBridge.supersede();
244
+ else activeBridge.deactivate();
245
+ activeBridgeByHookId.set(this.options.hookId, this);
246
+ if (this.unsubscribers.length > 0) return;
247
+ this.unsubscribers.push(aiEventClient.on("devtools:request-state", (event) => {
248
+ this.handleRequestState(event);
249
+ }));
250
+ if (this.options.applyToolFixture) this.unsubscribers.push(aiEventClient.on("devtools:tool-fixture:apply", (event) => {
251
+ this.handleToolFixtureApply(event);
252
+ }));
253
+ }
254
+ handleRequestState(event) {
255
+ if (this.disposed || this.superseded) return;
256
+ const targetHookId = event.payload.targetHookId;
257
+ if (targetHookId && targetHookId !== this.options.hookId) return;
258
+ this.emitRegistered();
259
+ this.emitToolsRegistered();
260
+ this.emitSnapshot();
261
+ this.onReplayState();
262
+ }
263
+ /**
264
+ * Extension hook for subclasses to replay any additional cached state on a
265
+ * `devtools:request-state` (i.e. when a panel opens). Called only after the
266
+ * base guards (disposed/superseded/targetHookId) pass. No-op by default.
267
+ */
268
+ onReplayState() {}
269
+ async handleToolFixtureApply(event) {
270
+ const fixture = event.payload;
271
+ if (!this.matchesFixtureTarget(fixture)) return;
272
+ await this.options.applyToolFixture?.(fixture);
273
+ }
274
+ matchesFixtureTarget(fixture) {
275
+ if (!fixture.hookId && !fixture.threadId) return false;
276
+ if (fixture.hookId) return fixture.hookId === this.options.hookId;
277
+ if (fixture.threadId && (!this.options.threadId || fixture.threadId !== this.options.threadId)) return false;
278
+ return true;
279
+ }
280
+ createEnvelope(eventType, visibility = "client-state", context = {}) {
281
+ return createAIDevtoolsEventEnvelope({
282
+ eventType,
283
+ source: "client",
284
+ visibility,
285
+ clientId: this.options.clientId,
286
+ hookId: this.options.hookId,
287
+ correlationId: this.bridgeId,
288
+ ...this.options.threadId ? { threadId: this.options.threadId } : {},
289
+ ...context.runId ? { runId: context.runId } : {},
290
+ timestamp: Date.now()
291
+ });
292
+ }
293
+ createMetadataPayload() {
294
+ return {
295
+ hookId: this.options.hookId,
296
+ hookName: this.options.metadata.hookName,
297
+ ...this.options.metadata.name ? { displayName: this.options.metadata.name } : {},
298
+ ...this.options.metadata.outputKind ? { outputKind: this.options.metadata.outputKind } : {},
299
+ ...this.options.metadata.framework ? { framework: this.options.metadata.framework } : {}
300
+ };
301
+ }
302
+ };
303
+ var bridgeIdSequence = 0;
368
304
  function createBridgeId(hookId) {
369
- const cryptoLike = globalThis.crypto;
370
- if (cryptoLike?.randomUUID) {
371
- return `bridge:${hookId}:${cryptoLike.randomUUID()}`;
372
- }
373
- bridgeIdSequence += 1;
374
- return `bridge:${hookId}:${bridgeIdSequence}`;
375
- }
376
- class ChatDevtoolsBridge extends ClientDevtoolsBridge {
377
- events;
378
- chatOptions;
379
- currentRunId = null;
380
- currentRunThreadId = null;
381
- currentStreamId = null;
382
- lastStreamId = null;
383
- lastRunEventContext;
384
- constructor(options) {
385
- super({
386
- ...options,
387
- // Thunk defers `this.applyFixture` lookup until after `super` returns.
388
- applyToolFixture: (fixture) => this.applyFixture(fixture)
389
- });
390
- this.chatOptions = options;
391
- this.events = new ChatDevtoolsAwareEventEmitter(options.clientId, this);
392
- }
393
- // --- Stream / run context API -------------------------------------------
394
- setCurrentStreamId(streamId) {
395
- this.currentStreamId = streamId;
396
- if (streamId) {
397
- this.lastStreamId = streamId;
398
- }
399
- }
400
- /**
401
- * Called by the auto-attaching emitter every time it sees a non-empty
402
- * streamId pass through. Lets devtools track the latest stream id
403
- * without the chat client wiring it up explicitly.
404
- */
405
- recordStreamId(streamId) {
406
- if (streamId) this.lastStreamId = streamId;
407
- }
408
- mountWithTools(initialMessageCount) {
409
- this.events.clientCreated(initialMessageCount);
410
- this.emitRegistered();
411
- this.emitToolsRegistered();
412
- this.emitSnapshot();
413
- }
414
- notifyToolsChanged() {
415
- this.emitToolsRegistered();
416
- this.emitSnapshot();
417
- }
418
- getCurrentStreamId() {
419
- return this.currentStreamId;
420
- }
421
- getLastStreamId() {
422
- return this.lastStreamId;
423
- }
424
- resolveStreamId() {
425
- return this.currentStreamId ?? this.lastStreamId ?? this.chatOptions.generateId("stream");
426
- }
427
- // Called when the chat client has just generated a runId for outbound emits;
428
- // the matching RUN_STARTED chunk from the adapter lands later and
429
- // observeChunk keeps the same context.
430
- beginRun(runId, threadId) {
431
- this.currentRunId = runId;
432
- this.currentRunThreadId = threadId;
433
- this.lastRunEventContext = { runId, threadId };
434
- }
435
- observeChunk(chunk) {
436
- if (chunk.type === "RUN_STARTED") {
437
- this.beginRun(chunk.runId, chunk.threadId);
438
- return;
439
- }
440
- if (chunk.type === "RUN_FINISHED" || chunk.type === "RUN_ERROR") {
441
- const runId = chunk.type === "RUN_FINISHED" ? chunk.runId : chunk.runId;
442
- if (!runId || runId === this.currentRunId) {
443
- const context = this.getCurrentRunEventContext();
444
- if (context) {
445
- this.lastRunEventContext = context;
446
- }
447
- this.currentRunId = null;
448
- this.currentRunThreadId = null;
449
- }
450
- }
451
- }
452
- getCurrentRunEventContext() {
453
- if (!this.currentRunId) return void 0;
454
- return {
455
- threadId: this.currentRunThreadId ?? this.chatOptions.threadId ?? "",
456
- runId: this.currentRunId
457
- };
458
- }
459
- getCurrentOrLastRunEventContext() {
460
- return this.getCurrentRunEventContext() ?? this.lastRunEventContext;
461
- }
462
- findToolCallContext(toolCallId) {
463
- const base = { toolCallId };
464
- const runContext = this.getCurrentRunEventContext();
465
- if (runContext) {
466
- return {
467
- threadId: runContext.threadId,
468
- runId: runContext.runId,
469
- toolCallId
470
- };
471
- }
472
- if (this.chatOptions.threadId) {
473
- return { threadId: this.chatOptions.threadId, toolCallId };
474
- }
475
- return base;
476
- }
477
- // --- Fixture replay ------------------------------------------------------
478
- /**
479
- * Entry point invoked when the devtools panel emits
480
- * `devtools:tool-fixture:apply`. The chat client never calls this
481
- * directly; it is wired through the base bridge's fixture subscription.
482
- */
483
- async applyFixture(fixture) {
484
- const messages = this.chatOptions.getMessages();
485
- const threadId = fixture.threadId ?? this.chatOptions.threadId ?? "";
486
- if (fixture.execute) {
487
- await this.executeFixture(fixture, messages, threadId);
488
- return;
489
- }
490
- const replay = this.createReplayMessageFromFixture(fixture, messages);
491
- const { message, toolCallId } = replay;
492
- const messageId = message.id;
493
- this.events.messageAppended(message, void 0, {
494
- threadId,
495
- toolCallId,
496
- ...fixture.runId ? { runId: fixture.runId } : {}
497
- });
498
- this.chatOptions.setMessages([...messages, message]);
499
- this.events.toolFixtureApplied({
500
- hookId: this.chatOptions.hookId,
501
- threadId,
502
- ...fixture.runId ? { runId: fixture.runId } : {},
503
- toolName: fixture.toolName,
504
- input: fixture.input,
505
- output: fixture.output,
506
- messageId,
507
- toolCallId,
508
- ...fixture.execute !== void 0 ? { execute: fixture.execute } : {},
509
- ...fixture.message ? { message: fixture.message } : {},
510
- ...fixture.errorText ? { errorText: fixture.errorText } : {}
511
- });
512
- this.emitSnapshot();
513
- }
514
- async executeFixture(fixture, messages, threadId) {
515
- const toolCallId = this.resolveFixtureToolCallId(
516
- fixture.toolCallId,
517
- messages
518
- );
519
- const messageId = this.resolveFixtureMessageId(fixture.messageId, messages);
520
- const message = {
521
- id: messageId,
522
- role: "assistant",
523
- parts: [
524
- {
525
- type: "tool-call",
526
- id: toolCallId,
527
- name: fixture.toolName,
528
- arguments: stringifyFixtureValue(fixture.input),
529
- input: fixture.input,
530
- state: "input-complete"
531
- }
532
- ],
533
- createdAt: /* @__PURE__ */ new Date()
534
- };
535
- this.events.messageAppended(message, void 0, {
536
- threadId,
537
- toolCallId,
538
- ...fixture.runId ? { runId: fixture.runId } : {}
539
- });
540
- this.chatOptions.setMessages([...messages, message]);
541
- this.emitSnapshot();
542
- const clientTool = this.findClientTool(fixture.toolName);
543
- const executeFunc = clientTool?.execute;
544
- if (!executeFunc) {
545
- console.warn(
546
- `[ai-devtools] tool fixture "${fixture.toolName}" requested execute=true but no client tool implementation is registered; replaying saved output instead.`
547
- );
548
- this.addToolResultForFixture({
549
- fixture,
550
- messageId,
551
- toolCallId,
552
- threadId,
553
- output: fixture.output,
554
- errorText: fixture.errorText
555
- });
556
- return;
557
- }
558
- let output;
559
- try {
560
- output = await executeFunc(fixture.input);
561
- } catch (error) {
562
- console.error(
563
- `[ai-devtools] tool fixture "${fixture.toolName}" execute threw`,
564
- error
565
- );
566
- this.addToolResultForFixture({
567
- fixture,
568
- messageId,
569
- toolCallId,
570
- threadId,
571
- output: null,
572
- errorText: error instanceof Error ? `${error.name}: ${error.message}` : `Tool execution failed: ${String(error)}`
573
- });
574
- return;
575
- }
576
- this.addToolResultForFixture({
577
- fixture,
578
- messageId,
579
- toolCallId,
580
- threadId,
581
- output
582
- });
583
- }
584
- addToolResultForFixture(input) {
585
- const state = input.errorText ? "output-error" : "output-available";
586
- this.events.toolResultAdded(
587
- input.toolCallId,
588
- input.fixture.toolName,
589
- input.output,
590
- state,
591
- {
592
- threadId: input.threadId,
593
- ...input.fixture.runId ? { runId: input.fixture.runId } : {},
594
- toolCallId: input.toolCallId
595
- }
596
- );
597
- this.chatOptions.addToolResult(
598
- input.toolCallId,
599
- input.output,
600
- input.errorText
601
- );
602
- this.events.toolFixtureApplied({
603
- hookId: this.chatOptions.hookId,
604
- threadId: input.threadId,
605
- ...input.fixture.runId ? { runId: input.fixture.runId } : {},
606
- toolName: input.fixture.toolName,
607
- input: input.fixture.input,
608
- output: input.output,
609
- execute: true,
610
- messageId: input.messageId,
611
- toolCallId: input.toolCallId,
612
- ...input.errorText ? { errorText: input.errorText } : {}
613
- });
614
- this.emitSnapshot();
615
- }
616
- createReplayMessageFromFixture(fixture, messages) {
617
- const cloned = this.cloneFixtureSourceMessage(fixture, messages);
618
- if (cloned) return cloned;
619
- const toolCallId = this.resolveFixtureToolCallId(
620
- fixture.toolCallId,
621
- messages
622
- );
623
- const messageId = this.resolveFixtureMessageId(fixture.messageId, messages);
624
- const state = fixture.errorText ? "error" : "complete";
625
- return {
626
- toolCallId,
627
- message: {
628
- id: messageId,
629
- role: "assistant",
630
- parts: [
631
- {
632
- type: "tool-call",
633
- id: toolCallId,
634
- name: fixture.toolName,
635
- arguments: stringifyFixtureValue(fixture.input),
636
- input: fixture.input,
637
- state: "input-complete",
638
- output: fixture.output
639
- },
640
- {
641
- type: "tool-result",
642
- toolCallId,
643
- content: stringifyFixtureValue(fixture.output),
644
- state,
645
- ...fixture.errorText ? { error: fixture.errorText } : {}
646
- }
647
- ],
648
- createdAt: /* @__PURE__ */ new Date()
649
- }
650
- };
651
- }
652
- cloneFixtureSourceMessage(fixture, messages) {
653
- const sourceMessage = fixture.message;
654
- if (!sourceMessage || !Array.isArray(sourceMessage.parts)) {
655
- return void 0;
656
- }
657
- const toolCallIds = this.createFixtureToolCallIdMap(
658
- sourceMessage.parts,
659
- messages
660
- );
661
- const parts = sourceMessage.parts.map((part) => cloneFixtureMessagePart(part, toolCallIds)).filter((part) => Boolean(part));
662
- const mappedFixtureToolCallId = fixture.toolCallId ? toolCallIds.get(fixture.toolCallId) : void 0;
663
- hydrateToolCallOutputs(parts, {
664
- ...mappedFixtureToolCallId ? { mappedToolCallId: mappedFixtureToolCallId } : {},
665
- output: fixture.output
666
- });
667
- if (parts.length === 0) return void 0;
668
- const toolCallId = (fixture.toolCallId ? toolCallIds.get(fixture.toolCallId) : void 0) ?? firstToolCallId(parts);
669
- if (!toolCallId) return void 0;
670
- return {
671
- toolCallId,
672
- message: {
673
- id: this.resolveFixtureMessageId(sourceMessage.id, messages),
674
- role: sourceMessage.role,
675
- parts,
676
- createdAt: /* @__PURE__ */ new Date()
677
- }
678
- };
679
- }
680
- createFixtureToolCallIdMap(parts, messages) {
681
- const ids = /* @__PURE__ */ new Map();
682
- for (const part of parts) {
683
- if (!isRecord(part) || part.type !== "tool-call") continue;
684
- if (typeof part.id !== "string") continue;
685
- ids.set(part.id, this.resolveFixtureToolCallId(part.id, messages));
686
- }
687
- return ids;
688
- }
689
- resolveFixtureMessageId(messageId, messages) {
690
- if (messageId && !messages.some((message) => message.id === messageId)) {
691
- return messageId;
692
- }
693
- return this.chatOptions.generateId("fixture-msg");
694
- }
695
- resolveFixtureToolCallId(toolCallId, messages) {
696
- if (toolCallId && !hasToolCallId(messages, toolCallId)) {
697
- return toolCallId;
698
- }
699
- return this.chatOptions.generateId("fixture-tool-call");
700
- }
701
- findClientTool(name) {
702
- const tools = this.chatOptions.getTools?.();
703
- if (!tools) return void 0;
704
- for (const tool of tools) {
705
- if (tool.name === name) return tool;
706
- }
707
- return void 0;
708
- }
305
+ const cryptoLike = globalThis.crypto;
306
+ if (cryptoLike?.randomUUID) return `bridge:${hookId}:${cryptoLike.randomUUID()}`;
307
+ bridgeIdSequence += 1;
308
+ return `bridge:${hookId}:${bridgeIdSequence}`;
709
309
  }
310
+ var ChatDevtoolsBridge = class extends ClientDevtoolsBridge {
311
+ events;
312
+ chatOptions;
313
+ currentRunId = null;
314
+ currentRunThreadId = null;
315
+ currentStreamId = null;
316
+ lastStreamId = null;
317
+ lastRunEventContext;
318
+ /** Last transported `memory:state` value, replayed when a panel opens. */
319
+ lastMemoryStateValue = null;
320
+ constructor(options) {
321
+ super({
322
+ ...options,
323
+ applyToolFixture: (fixture) => this.applyFixture(fixture)
324
+ });
325
+ this.chatOptions = options;
326
+ this.events = new ChatDevtoolsAwareEventEmitter(options.clientId, this);
327
+ }
328
+ setCurrentStreamId(streamId) {
329
+ this.currentStreamId = streamId;
330
+ if (streamId) this.lastStreamId = streamId;
331
+ }
332
+ /**
333
+ * Called by the auto-attaching emitter every time it sees a non-empty
334
+ * streamId pass through. Lets devtools track the latest stream id
335
+ * without the chat client wiring it up explicitly.
336
+ */
337
+ recordStreamId(streamId) {
338
+ if (streamId) this.lastStreamId = streamId;
339
+ }
340
+ mountWithTools(initialMessageCount) {
341
+ this.events.clientCreated(initialMessageCount);
342
+ this.emitRegistered();
343
+ this.emitToolsRegistered();
344
+ this.emitSnapshot();
345
+ }
346
+ notifyToolsChanged() {
347
+ this.emitToolsRegistered();
348
+ this.emitSnapshot();
349
+ }
350
+ getCurrentStreamId() {
351
+ return this.currentStreamId;
352
+ }
353
+ getLastStreamId() {
354
+ return this.lastStreamId;
355
+ }
356
+ resolveStreamId() {
357
+ return this.currentStreamId ?? this.lastStreamId ?? this.chatOptions.generateId("stream");
358
+ }
359
+ beginRun(runId, threadId) {
360
+ this.currentRunId = runId;
361
+ this.currentRunThreadId = threadId;
362
+ this.lastRunEventContext = {
363
+ runId,
364
+ threadId
365
+ };
366
+ }
367
+ observeChunk(chunk) {
368
+ if (chunk.type === "RUN_STARTED") {
369
+ this.beginRun(chunk.runId, chunk.threadId);
370
+ return;
371
+ }
372
+ if (chunk.type === "RUN_FINISHED" || chunk.type === "RUN_ERROR") {
373
+ const runId = chunk.type === "RUN_FINISHED" ? chunk.runId : chunk.runId;
374
+ if (!runId || runId === this.currentRunId) {
375
+ const context = this.getCurrentRunEventContext();
376
+ if (context) this.lastRunEventContext = context;
377
+ this.currentRunId = null;
378
+ this.currentRunThreadId = null;
379
+ }
380
+ }
381
+ }
382
+ /**
383
+ * Record a transported `memory:state` value (the server memory middleware's
384
+ * per-turn recall metrics + store snapshot). Called from the chat client's
385
+ * `onCustomEvent` handler — the designated path for CUSTOM stream events —
386
+ * NOT from `observeChunk`. Re-emits the browser `memory:*` events the devtools
387
+ * store consumes, and caches the value so a panel opened later can replay it
388
+ * (see {@link onReplayState}). Symmetric with the generation bridge's
389
+ * `recordResultChange` / `recordProgressChange`.
390
+ */
391
+ recordMemoryState(rawValue) {
392
+ this.lastMemoryStateValue = rawValue;
393
+ this.emitMemoryState(rawValue);
394
+ }
395
+ /**
396
+ * Re-emit the browser-side `memory:*` events from a transported
397
+ * `memory:state` value. The devtools store consumes these to render the
398
+ * Memory tab (operations timeline + live contents). The recall pair mirrors
399
+ * the server middleware's own emits; the snapshot is included only when the
400
+ * adapter supports introspection.
401
+ */
402
+ emitMemoryState(rawValue) {
403
+ if (!rawValue || typeof rawValue !== "object") return;
404
+ const value = rawValue;
405
+ const scope = value.scope;
406
+ const adapter = value.adapter;
407
+ if (!scope || typeof adapter !== "string") return;
408
+ const runContext = this.currentRunId ? { runId: this.currentRunId } : {};
409
+ emitAIDevtoolsEvent("memory:retrieve:started", {
410
+ ...this.createEnvelope("memory:retrieve:started", "client-state", runContext),
411
+ scope,
412
+ adapter,
413
+ query: value.query ?? ""
414
+ });
415
+ if (value.recall) emitAIDevtoolsEvent("memory:retrieve:completed", {
416
+ ...this.createEnvelope("memory:retrieve:completed", "client-state", runContext),
417
+ scope,
418
+ adapter,
419
+ fragmentCount: value.recall.fragmentCount ?? 0,
420
+ hasTools: Boolean(value.recall.hasTools),
421
+ systemPromptChars: value.recall.systemPromptChars ?? 0,
422
+ durationMs: value.recall.durationMs ?? 0
423
+ });
424
+ if (value.snapshot) emitAIDevtoolsEvent("memory:snapshot", {
425
+ ...this.createEnvelope("memory:snapshot", "client-state", runContext),
426
+ scope,
427
+ adapter,
428
+ takenAt: value.snapshot.takenAt,
429
+ data: value.snapshot.data,
430
+ facts: value.snapshot.facts ?? []
431
+ });
432
+ }
433
+ onReplayState() {
434
+ if (this.lastMemoryStateValue != null) this.emitMemoryState(this.lastMemoryStateValue);
435
+ }
436
+ getCurrentRunEventContext() {
437
+ if (!this.currentRunId) return void 0;
438
+ return {
439
+ threadId: this.currentRunThreadId ?? this.chatOptions.threadId ?? "",
440
+ runId: this.currentRunId
441
+ };
442
+ }
443
+ getCurrentOrLastRunEventContext() {
444
+ return this.getCurrentRunEventContext() ?? this.lastRunEventContext;
445
+ }
446
+ findToolCallContext(toolCallId) {
447
+ const base = { toolCallId };
448
+ const runContext = this.getCurrentRunEventContext();
449
+ if (runContext) return {
450
+ threadId: runContext.threadId,
451
+ runId: runContext.runId,
452
+ toolCallId
453
+ };
454
+ if (this.chatOptions.threadId) return {
455
+ threadId: this.chatOptions.threadId,
456
+ toolCallId
457
+ };
458
+ return base;
459
+ }
460
+ /**
461
+ * Entry point invoked when the devtools panel emits
462
+ * `devtools:tool-fixture:apply`. The chat client never calls this
463
+ * directly; it is wired through the base bridge's fixture subscription.
464
+ */
465
+ async applyFixture(fixture) {
466
+ const messages = this.chatOptions.getMessages();
467
+ const threadId = fixture.threadId ?? this.chatOptions.threadId ?? "";
468
+ if (fixture.execute) {
469
+ await this.executeFixture(fixture, messages, threadId);
470
+ return;
471
+ }
472
+ const { message, toolCallId } = this.createReplayMessageFromFixture(fixture, messages);
473
+ const messageId = message.id;
474
+ this.events.messageAppended(message, void 0, {
475
+ threadId,
476
+ toolCallId,
477
+ ...fixture.runId ? { runId: fixture.runId } : {}
478
+ });
479
+ this.chatOptions.setMessages([...messages, message]);
480
+ this.events.toolFixtureApplied({
481
+ hookId: this.chatOptions.hookId,
482
+ threadId,
483
+ ...fixture.runId ? { runId: fixture.runId } : {},
484
+ toolName: fixture.toolName,
485
+ input: fixture.input,
486
+ output: fixture.output,
487
+ messageId,
488
+ toolCallId,
489
+ ...fixture.execute !== void 0 ? { execute: fixture.execute } : {},
490
+ ...fixture.message ? { message: fixture.message } : {},
491
+ ...fixture.errorText ? { errorText: fixture.errorText } : {}
492
+ });
493
+ this.emitSnapshot();
494
+ }
495
+ async executeFixture(fixture, messages, threadId) {
496
+ const toolCallId = this.resolveFixtureToolCallId(fixture.toolCallId, messages);
497
+ const messageId = this.resolveFixtureMessageId(fixture.messageId, messages);
498
+ const message = {
499
+ id: messageId,
500
+ role: "assistant",
501
+ parts: [{
502
+ type: "tool-call",
503
+ id: toolCallId,
504
+ name: fixture.toolName,
505
+ arguments: stringifyFixtureValue(fixture.input),
506
+ input: fixture.input,
507
+ state: "input-complete"
508
+ }],
509
+ createdAt: /* @__PURE__ */ new Date()
510
+ };
511
+ this.events.messageAppended(message, void 0, {
512
+ threadId,
513
+ toolCallId,
514
+ ...fixture.runId ? { runId: fixture.runId } : {}
515
+ });
516
+ this.chatOptions.setMessages([...messages, message]);
517
+ this.emitSnapshot();
518
+ const executeFunc = this.findClientTool(fixture.toolName)?.execute;
519
+ if (!executeFunc) {
520
+ console.warn(`[ai-devtools] tool fixture "${fixture.toolName}" requested execute=true but no client tool implementation is registered; replaying saved output instead.`);
521
+ this.addToolResultForFixture({
522
+ fixture,
523
+ messageId,
524
+ toolCallId,
525
+ threadId,
526
+ output: fixture.output,
527
+ errorText: fixture.errorText
528
+ });
529
+ return;
530
+ }
531
+ let output;
532
+ try {
533
+ output = await executeFunc(fixture.input);
534
+ } catch (error) {
535
+ console.error(`[ai-devtools] tool fixture "${fixture.toolName}" execute threw`, error);
536
+ this.addToolResultForFixture({
537
+ fixture,
538
+ messageId,
539
+ toolCallId,
540
+ threadId,
541
+ output: null,
542
+ errorText: error instanceof Error ? `${error.name}: ${error.message}` : `Tool execution failed: ${String(error)}`
543
+ });
544
+ return;
545
+ }
546
+ this.addToolResultForFixture({
547
+ fixture,
548
+ messageId,
549
+ toolCallId,
550
+ threadId,
551
+ output
552
+ });
553
+ }
554
+ addToolResultForFixture(input) {
555
+ const state = input.errorText ? "output-error" : "output-available";
556
+ this.events.toolResultAdded(input.toolCallId, input.fixture.toolName, input.output, state, {
557
+ threadId: input.threadId,
558
+ ...input.fixture.runId ? { runId: input.fixture.runId } : {},
559
+ toolCallId: input.toolCallId
560
+ });
561
+ this.chatOptions.addToolResult(input.toolCallId, input.output, input.errorText);
562
+ this.events.toolFixtureApplied({
563
+ hookId: this.chatOptions.hookId,
564
+ threadId: input.threadId,
565
+ ...input.fixture.runId ? { runId: input.fixture.runId } : {},
566
+ toolName: input.fixture.toolName,
567
+ input: input.fixture.input,
568
+ output: input.output,
569
+ execute: true,
570
+ messageId: input.messageId,
571
+ toolCallId: input.toolCallId,
572
+ ...input.errorText ? { errorText: input.errorText } : {}
573
+ });
574
+ this.emitSnapshot();
575
+ }
576
+ createReplayMessageFromFixture(fixture, messages) {
577
+ const cloned = this.cloneFixtureSourceMessage(fixture, messages);
578
+ if (cloned) return cloned;
579
+ const toolCallId = this.resolveFixtureToolCallId(fixture.toolCallId, messages);
580
+ const messageId = this.resolveFixtureMessageId(fixture.messageId, messages);
581
+ const state = fixture.errorText ? "error" : "complete";
582
+ return {
583
+ toolCallId,
584
+ message: {
585
+ id: messageId,
586
+ role: "assistant",
587
+ parts: [{
588
+ type: "tool-call",
589
+ id: toolCallId,
590
+ name: fixture.toolName,
591
+ arguments: stringifyFixtureValue(fixture.input),
592
+ input: fixture.input,
593
+ state: "input-complete",
594
+ output: fixture.output
595
+ }, {
596
+ type: "tool-result",
597
+ toolCallId,
598
+ content: stringifyFixtureValue(fixture.output),
599
+ state,
600
+ ...fixture.errorText ? { error: fixture.errorText } : {}
601
+ }],
602
+ createdAt: /* @__PURE__ */ new Date()
603
+ }
604
+ };
605
+ }
606
+ cloneFixtureSourceMessage(fixture, messages) {
607
+ const sourceMessage = fixture.message;
608
+ if (!sourceMessage || !Array.isArray(sourceMessage.parts)) return;
609
+ const toolCallIds = this.createFixtureToolCallIdMap(sourceMessage.parts, messages);
610
+ const parts = sourceMessage.parts.map((part) => cloneFixtureMessagePart(part, toolCallIds)).filter((part) => Boolean(part));
611
+ const mappedFixtureToolCallId = fixture.toolCallId ? toolCallIds.get(fixture.toolCallId) : void 0;
612
+ hydrateToolCallOutputs(parts, {
613
+ ...mappedFixtureToolCallId ? { mappedToolCallId: mappedFixtureToolCallId } : {},
614
+ output: fixture.output
615
+ });
616
+ if (parts.length === 0) return void 0;
617
+ const toolCallId = (fixture.toolCallId ? toolCallIds.get(fixture.toolCallId) : void 0) ?? firstToolCallId(parts);
618
+ if (!toolCallId) return void 0;
619
+ return {
620
+ toolCallId,
621
+ message: {
622
+ id: this.resolveFixtureMessageId(sourceMessage.id, messages),
623
+ role: sourceMessage.role,
624
+ parts,
625
+ createdAt: /* @__PURE__ */ new Date()
626
+ }
627
+ };
628
+ }
629
+ createFixtureToolCallIdMap(parts, messages) {
630
+ const ids = /* @__PURE__ */ new Map();
631
+ for (const part of parts) {
632
+ if (!isRecord(part) || part.type !== "tool-call") continue;
633
+ if (typeof part.id !== "string") continue;
634
+ ids.set(part.id, this.resolveFixtureToolCallId(part.id, messages));
635
+ }
636
+ return ids;
637
+ }
638
+ resolveFixtureMessageId(messageId, messages) {
639
+ if (messageId && !messages.some((message) => message.id === messageId)) return messageId;
640
+ return this.chatOptions.generateId("fixture-msg");
641
+ }
642
+ resolveFixtureToolCallId(toolCallId, messages) {
643
+ if (toolCallId && !hasToolCallId(messages, toolCallId)) return toolCallId;
644
+ return this.chatOptions.generateId("fixture-tool-call");
645
+ }
646
+ findClientTool(name) {
647
+ const tools = this.chatOptions.getTools?.();
648
+ if (!tools) return void 0;
649
+ for (const tool of tools) if (tool.name === name) return tool;
650
+ }
651
+ };
710
652
  function isRecord(value) {
711
- return typeof value === "object" && value !== null;
653
+ return typeof value === "object" && value !== null;
712
654
  }
713
655
  function stringifyFixtureValue(value) {
714
- if (typeof value === "string") return value;
715
- if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
716
- return String(value);
717
- }
718
- try {
719
- return JSON.stringify(value);
720
- } catch (error) {
721
- console.error(
722
- "[ai-devtools] failed to JSON.stringify fixture value; falling back to String(). Tool call arguments may be malformed.",
723
- { error, value }
724
- );
725
- return String(value);
726
- }
656
+ if (typeof value === "string") return value;
657
+ if (value === void 0 || typeof value === "function" || typeof value === "symbol") return String(value);
658
+ try {
659
+ return JSON.stringify(value);
660
+ } catch (error) {
661
+ console.error("[ai-devtools] failed to JSON.stringify fixture value; falling back to String(). Tool call arguments may be malformed.", {
662
+ error,
663
+ value
664
+ });
665
+ return String(value);
666
+ }
727
667
  }
728
668
  function parseFixtureResultContent(content) {
729
- try {
730
- return JSON.parse(content);
731
- } catch (error) {
732
- console.error(
733
- "[ai-devtools] failed to JSON.parse fixture result content; replaying as raw string. Fixture payload may be corrupted.",
734
- { error, content }
735
- );
736
- return content;
737
- }
669
+ try {
670
+ return JSON.parse(content);
671
+ } catch (error) {
672
+ console.error("[ai-devtools] failed to JSON.parse fixture result content; replaying as raw string. Fixture payload may be corrupted.", {
673
+ error,
674
+ content
675
+ });
676
+ return content;
677
+ }
738
678
  }
739
679
  function cloneFixtureMessagePart(part, toolCallIds) {
740
- if (!isRecord(part) || typeof part.type !== "string") return void 0;
741
- const cloned = { ...part };
742
- if (part.type === "tool-call" && typeof part.id === "string") {
743
- cloned.id = toolCallIds.get(part.id) ?? part.id;
744
- }
745
- if (part.type === "tool-result" && typeof part.toolCallId === "string") {
746
- cloned.toolCallId = toolCallIds.get(part.toolCallId) ?? part.toolCallId;
747
- }
748
- return cloned;
680
+ if (!isRecord(part) || typeof part.type !== "string") return void 0;
681
+ const cloned = { ...part };
682
+ if (part.type === "tool-call" && typeof part.id === "string") cloned.id = toolCallIds.get(part.id) ?? part.id;
683
+ if (part.type === "tool-result" && typeof part.toolCallId === "string") cloned.toolCallId = toolCallIds.get(part.toolCallId) ?? part.toolCallId;
684
+ return cloned;
749
685
  }
750
686
  function firstToolCallId(parts) {
751
- const toolCall = parts.find((part) => part.type === "tool-call");
752
- return toolCall?.type === "tool-call" ? toolCall.id : void 0;
687
+ const toolCall = parts.find((part) => part.type === "tool-call");
688
+ return toolCall?.type === "tool-call" ? toolCall.id : void 0;
753
689
  }
754
690
  function hydrateToolCallOutputs(parts, fixtureOutput) {
755
- for (const part of parts) {
756
- if (part.type !== "tool-result") continue;
757
- const toolCall = parts.find(
758
- (candidate) => candidate.type === "tool-call" && candidate.id === part.toolCallId && candidate.output === void 0
759
- );
760
- if (toolCall) {
761
- toolCall.output = Array.isArray(part.content) ? part.content : parseFixtureResultContent(part.content);
762
- }
763
- }
764
- if (fixtureOutput.mappedToolCallId && fixtureOutput.output !== void 0) {
765
- const toolCall = parts.find(
766
- (candidate) => candidate.type === "tool-call" && candidate.id === fixtureOutput.mappedToolCallId && candidate.output === void 0
767
- );
768
- if (toolCall) {
769
- toolCall.output = fixtureOutput.output;
770
- }
771
- }
691
+ for (const part of parts) {
692
+ if (part.type !== "tool-result") continue;
693
+ const toolCall = parts.find((candidate) => candidate.type === "tool-call" && candidate.id === part.toolCallId && candidate.output === void 0);
694
+ if (toolCall) toolCall.output = Array.isArray(part.content) ? part.content : parseFixtureResultContent(part.content);
695
+ }
696
+ if (fixtureOutput.mappedToolCallId && fixtureOutput.output !== void 0) {
697
+ const toolCall = parts.find((candidate) => candidate.type === "tool-call" && candidate.id === fixtureOutput.mappedToolCallId && candidate.output === void 0);
698
+ if (toolCall) toolCall.output = fixtureOutput.output;
699
+ }
772
700
  }
773
701
  function hasToolCallId(messages, toolCallId) {
774
- return messages.some(
775
- (message) => message.parts.some((part) => {
776
- if (part.type === "tool-call") return part.id === toolCallId;
777
- if (part.type === "tool-result") return part.toolCallId === toolCallId;
778
- return false;
779
- })
780
- );
781
- }
782
- class GenerationDevtoolsBridge extends ClientDevtoolsBridge {
783
- activeRunId = null;
784
- activeRunStarted = false;
785
- devtoolsRuns = [];
786
- maxRuns;
787
- getCoreState;
788
- constructor(options) {
789
- super({
790
- ...options,
791
- getSnapshot: () => this.buildSnapshot()
792
- });
793
- this.maxRuns = options.maxRuns ?? 20;
794
- this.getCoreState = options.getCoreState;
795
- }
796
- // --- Run lifecycle (called by GenerationClient) -----------------------
797
- beginRun(input) {
798
- const runId = this.generateRunId();
799
- this.activeRunId = runId;
800
- this.activeRunStarted = false;
801
- this.upsertRun(runId, {
802
- input,
803
- result: null,
804
- preview: this.createPreview(null),
805
- progress: null,
806
- status: "generating",
807
- isLoading: true,
808
- clearError: true
809
- });
810
- return runId;
811
- }
812
- ensureRunStarted(runId) {
813
- if (this.activeRunStarted && this.activeRunId === runId) return;
814
- if (!this.activeRunStarted && this.activeRunId && this.activeRunId !== runId) {
815
- this.renameRun(this.activeRunId, runId);
816
- }
817
- this.activeRunId = runId;
818
- this.activeRunStarted = true;
819
- this.upsertRun(runId, {
820
- status: "generating",
821
- isLoading: true,
822
- clearError: true
823
- });
824
- this.emitRunLifecycle("run:started", runId, "started");
825
- this.emitState();
826
- }
827
- finishRun(runId, eventType, status, error) {
828
- this.ensureRunStarted(runId);
829
- const completedAt = Date.now();
830
- const completedProgress = status === "completed" ? this.completeProgress() : this.getProgress();
831
- const runStatus = status === "completed" ? "success" : status === "errored" ? "error" : "cancelled";
832
- this.upsertRun(runId, {
833
- status: runStatus,
834
- isLoading: false,
835
- progress: completedProgress,
836
- completedAt,
837
- ...error ? { error } : { clearError: true }
838
- });
839
- if (this.activeRunId === runId) {
840
- this.activeRunId = null;
841
- }
842
- this.activeRunStarted = false;
843
- this.emitRunLifecycle(eventType, runId, status, {
844
- ...error ? { error } : {}
845
- });
846
- this.emitState();
847
- }
848
- getActiveRunId() {
849
- return this.activeRunId;
850
- }
851
- /** Clear all per-run history. Called when the client `reset()`s. */
852
- resetRuns() {
853
- this.activeRunId = null;
854
- this.activeRunStarted = false;
855
- this.devtoolsRuns = [];
856
- }
857
- /** Record state changes from the client and emit the matching snapshot. */
858
- recordResultChange() {
859
- this.updateActiveRun({
860
- result: this.getCoreState().result,
861
- preview: this.createPreview(this.getCoreState().result),
862
- clearError: true
863
- });
864
- this.emitState();
865
- }
866
- recordLoadingChange() {
867
- this.updateActiveRun({ isLoading: this.getCoreState().isLoading });
868
- this.emitState();
869
- }
870
- recordErrorChange(error) {
871
- this.updateActiveRun(
872
- error ? { error: error.message } : { clearError: true }
873
- );
874
- this.emitState();
875
- }
876
- recordStatusChange(status) {
877
- this.updateActiveRun({ status });
878
- this.emitState();
879
- }
880
- recordProgressChange() {
881
- this.updateActiveRun({ progress: this.getCoreState().progress });
882
- this.emitState();
883
- }
884
- /** Emit the latest snapshot without touching run state. */
885
- emitState() {
886
- this.emitUpdated();
887
- this.emitSnapshot();
888
- }
889
- // --- Internal ---------------------------------------------------------
890
- buildSnapshot() {
891
- const core = this.getCoreState();
892
- return {
893
- input: core.input,
894
- result: core.result,
895
- preview: this.createPreview(core.result),
896
- progress: core.progress,
897
- status: core.status,
898
- isLoading: core.isLoading,
899
- activeRunId: this.activeRunId,
900
- runs: this.devtoolsRuns,
901
- ...core.error ? { error: core.error } : {}
902
- };
903
- }
904
- updateActiveRun(patch) {
905
- if (!this.activeRunId) return;
906
- this.upsertRun(this.activeRunId, patch);
907
- }
908
- upsertRun(runId, patch) {
909
- const now = Date.now();
910
- const index = this.devtoolsRuns.findIndex((run) => run.id === runId);
911
- const existing = index >= 0 ? this.devtoolsRuns[index] : void 0;
912
- const next = existing ? { ...existing } : {
913
- id: runId,
914
- input: this.getCoreState().input,
915
- result: null,
916
- preview: this.createPreview(null),
917
- progress: null,
918
- status: "idle",
919
- isLoading: false,
920
- startedAt: now,
921
- updatedAt: now
922
- };
923
- if ("input" in patch) next.input = patch.input ?? null;
924
- if ("result" in patch) next.result = patch.result ?? null;
925
- if (patch.preview) next.preview = patch.preview;
926
- if ("progress" in patch) next.progress = patch.progress ?? null;
927
- if (patch.status) next.status = patch.status;
928
- if ("isLoading" in patch) next.isLoading = patch.isLoading === true;
929
- if (patch.completedAt !== void 0) next.completedAt = patch.completedAt;
930
- if (patch.clearError) delete next.error;
931
- if (patch.error !== void 0) next.error = patch.error;
932
- next.updatedAt = now;
933
- if (index >= 0) {
934
- this.devtoolsRuns = this.devtoolsRuns.map(
935
- (run) => run.id === runId ? next : run
936
- );
937
- } else {
938
- this.devtoolsRuns = [...this.devtoolsRuns, next];
939
- }
940
- if (this.devtoolsRuns.length > this.maxRuns) {
941
- this.devtoolsRuns = this.devtoolsRuns.slice(-this.maxRuns);
942
- }
943
- }
944
- renameRun(previousRunId, nextRunId) {
945
- if (previousRunId === nextRunId) return;
946
- const existing = this.devtoolsRuns.find((run) => run.id === previousRunId);
947
- if (!existing) return;
948
- const renamed = { ...existing, id: nextRunId, updatedAt: Date.now() };
949
- this.devtoolsRuns = this.devtoolsRuns.filter((run) => run.id !== nextRunId).map((run) => run.id === previousRunId ? renamed : run);
950
- }
951
- getProgress() {
952
- return this.getCoreState().progress;
953
- }
954
- completeProgress() {
955
- const progress = this.getCoreState().progress;
956
- if (!progress) return null;
957
- return {
958
- value: 100,
959
- ...progress.message ? { message: progress.message } : {}
960
- };
961
- }
962
- createPreview(result) {
963
- return createAIDevtoolsGenerationPreview({
964
- outputKind: this.options.metadata.outputKind,
965
- result
966
- });
967
- }
968
- generateRunId() {
969
- return `run-${Date.now()}-${Math.random().toString(36).substring(7)}`;
970
- }
971
- }
972
- class VideoDevtoolsBridge extends GenerationDevtoolsBridge {
973
- constructor(options) {
974
- super(options);
975
- }
976
- recordJobIdChange() {
977
- this.updateActiveRun({
978
- jobId: this.getCoreState().jobId
979
- });
980
- this.emitState();
981
- }
982
- recordVideoStatusChange() {
983
- const core = this.getCoreState();
984
- this.updateActiveRun({
985
- videoStatus: core.videoStatus,
986
- preview: this.createVideoPreview(core.result, core.videoStatus)
987
- });
988
- this.emitState();
989
- }
990
- buildSnapshot() {
991
- const core = this.getCoreState();
992
- return {
993
- input: core.input,
994
- result: core.result,
995
- preview: this.createVideoPreview(core.result, core.videoStatus),
996
- progress: core.progress,
997
- status: core.status,
998
- isLoading: core.isLoading,
999
- activeRunId: this.activeRunId,
1000
- runs: this.devtoolsRuns,
1001
- jobId: core.jobId,
1002
- videoStatus: core.videoStatus,
1003
- ...core.error ? { error: core.error } : {}
1004
- };
1005
- }
1006
- upsertRun(runId, patch) {
1007
- super.upsertRun(runId, patch);
1008
- if (!("jobId" in patch || "videoStatus" in patch)) return;
1009
- const index = this.devtoolsRuns.findIndex((run) => run.id === runId);
1010
- if (index < 0) return;
1011
- const target = this.devtoolsRuns[index];
1012
- if (!target) return;
1013
- const merged = { ...target };
1014
- if ("jobId" in patch) merged.jobId = patch.jobId ?? null;
1015
- if ("videoStatus" in patch) merged.videoStatus = patch.videoStatus ?? null;
1016
- this.devtoolsRuns = this.devtoolsRuns.map(
1017
- (run) => run.id === runId ? merged : run
1018
- );
1019
- }
1020
- // Override so record* methods inherited from GenerationDevtoolsBridge
1021
- // (e.g. recordResultChange) thread the latest videoStatus into the preview.
1022
- createPreview(result) {
1023
- const core = this.getCoreState();
1024
- return this.createVideoPreview(result, core.videoStatus);
1025
- }
1026
- createVideoPreview(result, videoStatus) {
1027
- return createAIDevtoolsGenerationPreview({
1028
- outputKind: this.options.metadata.outputKind,
1029
- result,
1030
- videoStatus
1031
- });
1032
- }
1033
- }
1034
- class ChatDevtoolsAwareEventEmitter extends DefaultChatClientEventEmitter {
1035
- constructor(clientId, helper) {
1036
- super(clientId);
1037
- this.helper = helper;
1038
- }
1039
- helper;
1040
- afterEmit(streamId) {
1041
- if (streamId) this.helper.recordStreamId(streamId);
1042
- this.helper.emitSnapshot();
1043
- }
1044
- // -- methods with run context --------------------------------------------
1045
- textUpdated(streamId, messageId, content, context) {
1046
- super.textUpdated(
1047
- streamId,
1048
- messageId,
1049
- content,
1050
- context ?? this.helper.getCurrentRunEventContext()
1051
- );
1052
- this.afterEmit(streamId);
1053
- }
1054
- thinkingUpdated(streamId, messageId, content, delta, context) {
1055
- super.thinkingUpdated(
1056
- streamId,
1057
- messageId,
1058
- content,
1059
- delta,
1060
- context ?? this.helper.getCurrentRunEventContext()
1061
- );
1062
- this.afterEmit(streamId);
1063
- }
1064
- messageAppended(uiMessage, streamId, context) {
1065
- super.messageAppended(
1066
- uiMessage,
1067
- streamId,
1068
- context ?? this.helper.getCurrentRunEventContext()
1069
- );
1070
- this.afterEmit(streamId);
1071
- }
1072
- toolCallStateChanged(streamId, messageId, toolCallId, toolName, state, args, context) {
1073
- super.toolCallStateChanged(
1074
- streamId,
1075
- messageId,
1076
- toolCallId,
1077
- toolName,
1078
- state,
1079
- args,
1080
- context ?? this.helper.getCurrentRunEventContext()
1081
- );
1082
- this.afterEmit(streamId);
1083
- }
1084
- structuredOutputChanged(eventName, streamId, messageId, output, context) {
1085
- super.structuredOutputChanged(
1086
- eventName,
1087
- streamId,
1088
- messageId,
1089
- output,
1090
- context ?? this.helper.getCurrentOrLastRunEventContext()
1091
- );
1092
- this.afterEmit(streamId);
1093
- }
1094
- approvalRequested(streamId, messageId, toolCallId, toolName, input, approvalId, context) {
1095
- super.approvalRequested(
1096
- streamId,
1097
- messageId,
1098
- toolCallId,
1099
- toolName,
1100
- input,
1101
- approvalId,
1102
- context ?? this.helper.getCurrentOrLastRunEventContext()
1103
- );
1104
- this.afterEmit(streamId);
1105
- }
1106
- toolResultAdded(toolCallId, toolName, output, state, context) {
1107
- super.toolResultAdded(
1108
- toolCallId,
1109
- toolName,
1110
- output,
1111
- state,
1112
- context ?? this.helper.getCurrentRunEventContext()
1113
- );
1114
- this.afterEmit();
1115
- }
1116
- toolApprovalResponded(approvalId, toolCallId, approved, context) {
1117
- super.toolApprovalResponded(
1118
- approvalId,
1119
- toolCallId,
1120
- approved,
1121
- context ?? this.helper.getCurrentRunEventContext()
1122
- );
1123
- this.afterEmit();
1124
- }
1125
- // -- methods without context (just auto-emit snapshot) -------------------
1126
- clientCreated(initialMessageCount) {
1127
- super.clientCreated(initialMessageCount);
1128
- this.afterEmit();
1129
- }
1130
- loadingChanged(isLoading) {
1131
- super.loadingChanged(isLoading);
1132
- this.afterEmit();
1133
- }
1134
- errorChanged(error) {
1135
- super.errorChanged(error);
1136
- this.afterEmit();
1137
- }
1138
- reloaded(fromMessageIndex) {
1139
- super.reloaded(fromMessageIndex);
1140
- this.afterEmit();
1141
- }
1142
- stopped() {
1143
- super.stopped();
1144
- this.afterEmit();
1145
- }
1146
- messagesCleared() {
1147
- super.messagesCleared();
1148
- this.afterEmit();
1149
- }
1150
- messageSent(messageId, content) {
1151
- super.messageSent(messageId, content);
1152
- this.afterEmit();
1153
- }
1154
- toolFixtureApplied(fixture) {
1155
- super.toolFixtureApplied(fixture);
1156
- this.afterEmit();
1157
- }
702
+ return messages.some((message) => message.parts.some((part) => {
703
+ if (part.type === "tool-call") return part.id === toolCallId;
704
+ if (part.type === "tool-result") return part.toolCallId === toolCallId;
705
+ return false;
706
+ }));
1158
707
  }
708
+ var GenerationDevtoolsBridge = class extends ClientDevtoolsBridge {
709
+ activeRunId = null;
710
+ activeRunStarted = false;
711
+ devtoolsRuns = [];
712
+ maxRuns;
713
+ getCoreState;
714
+ constructor(options) {
715
+ super({
716
+ ...options,
717
+ getSnapshot: () => this.buildSnapshot()
718
+ });
719
+ this.maxRuns = options.maxRuns ?? 20;
720
+ this.getCoreState = options.getCoreState;
721
+ }
722
+ beginRun(input) {
723
+ const runId = this.generateRunId();
724
+ this.activeRunId = runId;
725
+ this.activeRunStarted = false;
726
+ this.upsertRun(runId, {
727
+ input,
728
+ result: null,
729
+ preview: this.createPreview(null),
730
+ progress: null,
731
+ status: "generating",
732
+ isLoading: true,
733
+ clearError: true
734
+ });
735
+ return runId;
736
+ }
737
+ ensureRunStarted(runId) {
738
+ if (this.activeRunStarted && this.activeRunId === runId) return;
739
+ if (!this.activeRunStarted && this.activeRunId && this.activeRunId !== runId) this.renameRun(this.activeRunId, runId);
740
+ this.activeRunId = runId;
741
+ this.activeRunStarted = true;
742
+ this.upsertRun(runId, {
743
+ status: "generating",
744
+ isLoading: true,
745
+ clearError: true
746
+ });
747
+ this.emitRunLifecycle("run:started", runId, "started");
748
+ this.emitState();
749
+ }
750
+ finishRun(runId, eventType, status, error) {
751
+ this.ensureRunStarted(runId);
752
+ const completedAt = Date.now();
753
+ const completedProgress = status === "completed" ? this.completeProgress() : this.getProgress();
754
+ const runStatus = status === "completed" ? "success" : status === "errored" ? "error" : "cancelled";
755
+ this.upsertRun(runId, {
756
+ status: runStatus,
757
+ isLoading: false,
758
+ progress: completedProgress,
759
+ completedAt,
760
+ ...error ? { error } : { clearError: true }
761
+ });
762
+ if (this.activeRunId === runId) this.activeRunId = null;
763
+ this.activeRunStarted = false;
764
+ this.emitRunLifecycle(eventType, runId, status, { ...error ? { error } : {} });
765
+ this.emitState();
766
+ }
767
+ getActiveRunId() {
768
+ return this.activeRunId;
769
+ }
770
+ /** Clear all per-run history. Called when the client `reset()`s. */
771
+ resetRuns() {
772
+ this.activeRunId = null;
773
+ this.activeRunStarted = false;
774
+ this.devtoolsRuns = [];
775
+ }
776
+ /** Record state changes from the client and emit the matching snapshot. */
777
+ recordResultChange() {
778
+ this.updateActiveRun({
779
+ result: this.getCoreState().result,
780
+ preview: this.createPreview(this.getCoreState().result),
781
+ clearError: true
782
+ });
783
+ this.emitState();
784
+ }
785
+ recordLoadingChange() {
786
+ this.updateActiveRun({ isLoading: this.getCoreState().isLoading });
787
+ this.emitState();
788
+ }
789
+ recordErrorChange(error) {
790
+ this.updateActiveRun(error ? { error: error.message } : { clearError: true });
791
+ this.emitState();
792
+ }
793
+ recordStatusChange(status) {
794
+ this.updateActiveRun({ status });
795
+ this.emitState();
796
+ }
797
+ recordProgressChange() {
798
+ this.updateActiveRun({ progress: this.getCoreState().progress });
799
+ this.emitState();
800
+ }
801
+ /** Emit the latest snapshot without touching run state. */
802
+ emitState() {
803
+ this.emitUpdated();
804
+ this.emitSnapshot();
805
+ }
806
+ buildSnapshot() {
807
+ const core = this.getCoreState();
808
+ return {
809
+ input: core.input,
810
+ result: core.result,
811
+ preview: this.createPreview(core.result),
812
+ progress: core.progress,
813
+ status: core.status,
814
+ isLoading: core.isLoading,
815
+ activeRunId: this.activeRunId,
816
+ runs: this.devtoolsRuns,
817
+ ...core.error ? { error: core.error } : {}
818
+ };
819
+ }
820
+ updateActiveRun(patch) {
821
+ if (!this.activeRunId) return;
822
+ this.upsertRun(this.activeRunId, patch);
823
+ }
824
+ upsertRun(runId, patch) {
825
+ const now = Date.now();
826
+ const index = this.devtoolsRuns.findIndex((run) => run.id === runId);
827
+ const existing = index >= 0 ? this.devtoolsRuns[index] : void 0;
828
+ const next = existing ? { ...existing } : {
829
+ id: runId,
830
+ input: this.getCoreState().input,
831
+ result: null,
832
+ preview: this.createPreview(null),
833
+ progress: null,
834
+ status: "idle",
835
+ isLoading: false,
836
+ startedAt: now,
837
+ updatedAt: now
838
+ };
839
+ if ("input" in patch) next.input = patch.input ?? null;
840
+ if ("result" in patch) next.result = patch.result ?? null;
841
+ if (patch.preview) next.preview = patch.preview;
842
+ if ("progress" in patch) next.progress = patch.progress ?? null;
843
+ if (patch.status) next.status = patch.status;
844
+ if ("isLoading" in patch) next.isLoading = patch.isLoading === true;
845
+ if (patch.completedAt !== void 0) next.completedAt = patch.completedAt;
846
+ if (patch.clearError) delete next.error;
847
+ if (patch.error !== void 0) next.error = patch.error;
848
+ next.updatedAt = now;
849
+ if (index >= 0) this.devtoolsRuns = this.devtoolsRuns.map((run) => run.id === runId ? next : run);
850
+ else this.devtoolsRuns = [...this.devtoolsRuns, next];
851
+ if (this.devtoolsRuns.length > this.maxRuns) this.devtoolsRuns = this.devtoolsRuns.slice(-this.maxRuns);
852
+ }
853
+ renameRun(previousRunId, nextRunId) {
854
+ if (previousRunId === nextRunId) return;
855
+ const existing = this.devtoolsRuns.find((run) => run.id === previousRunId);
856
+ if (!existing) return;
857
+ const renamed = {
858
+ ...existing,
859
+ id: nextRunId,
860
+ updatedAt: Date.now()
861
+ };
862
+ this.devtoolsRuns = this.devtoolsRuns.filter((run) => run.id !== nextRunId).map((run) => run.id === previousRunId ? renamed : run);
863
+ }
864
+ getProgress() {
865
+ return this.getCoreState().progress;
866
+ }
867
+ completeProgress() {
868
+ const progress = this.getCoreState().progress;
869
+ if (!progress) return null;
870
+ return {
871
+ value: 100,
872
+ ...progress.message ? { message: progress.message } : {}
873
+ };
874
+ }
875
+ createPreview(result) {
876
+ return createAIDevtoolsGenerationPreview({
877
+ outputKind: this.options.metadata.outputKind,
878
+ result
879
+ });
880
+ }
881
+ generateRunId() {
882
+ return `run-${Date.now()}-${Math.random().toString(36).substring(7)}`;
883
+ }
884
+ };
885
+ var VideoDevtoolsBridge = class extends GenerationDevtoolsBridge {
886
+ constructor(options) {
887
+ super(options);
888
+ }
889
+ recordJobIdChange() {
890
+ this.updateActiveRun({ jobId: this.getCoreState().jobId });
891
+ this.emitState();
892
+ }
893
+ recordVideoStatusChange() {
894
+ const core = this.getCoreState();
895
+ this.updateActiveRun({
896
+ videoStatus: core.videoStatus,
897
+ preview: this.createVideoPreview(core.result, core.videoStatus)
898
+ });
899
+ this.emitState();
900
+ }
901
+ buildSnapshot() {
902
+ const core = this.getCoreState();
903
+ return {
904
+ input: core.input,
905
+ result: core.result,
906
+ preview: this.createVideoPreview(core.result, core.videoStatus),
907
+ progress: core.progress,
908
+ status: core.status,
909
+ isLoading: core.isLoading,
910
+ activeRunId: this.activeRunId,
911
+ runs: this.devtoolsRuns,
912
+ jobId: core.jobId,
913
+ videoStatus: core.videoStatus,
914
+ ...core.error ? { error: core.error } : {}
915
+ };
916
+ }
917
+ upsertRun(runId, patch) {
918
+ super.upsertRun(runId, patch);
919
+ if (!("jobId" in patch || "videoStatus" in patch)) return;
920
+ const index = this.devtoolsRuns.findIndex((run) => run.id === runId);
921
+ if (index < 0) return;
922
+ const target = this.devtoolsRuns[index];
923
+ if (!target) return;
924
+ const merged = { ...target };
925
+ if ("jobId" in patch) merged.jobId = patch.jobId ?? null;
926
+ if ("videoStatus" in patch) merged.videoStatus = patch.videoStatus ?? null;
927
+ this.devtoolsRuns = this.devtoolsRuns.map((run) => run.id === runId ? merged : run);
928
+ }
929
+ createPreview(result) {
930
+ const core = this.getCoreState();
931
+ return this.createVideoPreview(result, core.videoStatus);
932
+ }
933
+ createVideoPreview(result, videoStatus) {
934
+ return createAIDevtoolsGenerationPreview({
935
+ outputKind: this.options.metadata.outputKind,
936
+ result,
937
+ videoStatus
938
+ });
939
+ }
940
+ };
941
+ var ChatDevtoolsAwareEventEmitter = class extends DefaultChatClientEventEmitter {
942
+ helper;
943
+ constructor(clientId, helper) {
944
+ super(clientId);
945
+ this.helper = helper;
946
+ }
947
+ afterEmit(streamId) {
948
+ if (streamId) this.helper.recordStreamId(streamId);
949
+ this.helper.emitSnapshot();
950
+ }
951
+ textUpdated(streamId, messageId, content, context) {
952
+ super.textUpdated(streamId, messageId, content, context ?? this.helper.getCurrentRunEventContext());
953
+ this.afterEmit(streamId);
954
+ }
955
+ thinkingUpdated(streamId, messageId, content, delta, context) {
956
+ super.thinkingUpdated(streamId, messageId, content, delta, context ?? this.helper.getCurrentRunEventContext());
957
+ this.afterEmit(streamId);
958
+ }
959
+ messageAppended(uiMessage, streamId, context) {
960
+ super.messageAppended(uiMessage, streamId, context ?? this.helper.getCurrentRunEventContext());
961
+ this.afterEmit(streamId);
962
+ }
963
+ toolCallStateChanged(streamId, messageId, toolCallId, toolName, state, args, context) {
964
+ super.toolCallStateChanged(streamId, messageId, toolCallId, toolName, state, args, context ?? this.helper.getCurrentRunEventContext());
965
+ this.afterEmit(streamId);
966
+ }
967
+ structuredOutputChanged(eventName, streamId, messageId, output, context) {
968
+ super.structuredOutputChanged(eventName, streamId, messageId, output, context ?? this.helper.getCurrentOrLastRunEventContext());
969
+ this.afterEmit(streamId);
970
+ }
971
+ approvalRequested(streamId, messageId, toolCallId, toolName, input, approvalId, context) {
972
+ super.approvalRequested(streamId, messageId, toolCallId, toolName, input, approvalId, context ?? this.helper.getCurrentOrLastRunEventContext());
973
+ this.afterEmit(streamId);
974
+ }
975
+ toolResultAdded(toolCallId, toolName, output, state, context) {
976
+ super.toolResultAdded(toolCallId, toolName, output, state, context ?? this.helper.getCurrentRunEventContext());
977
+ this.afterEmit();
978
+ }
979
+ toolApprovalResponded(approvalId, toolCallId, approved, context) {
980
+ super.toolApprovalResponded(approvalId, toolCallId, approved, context ?? this.helper.getCurrentRunEventContext());
981
+ this.afterEmit();
982
+ }
983
+ clientCreated(initialMessageCount) {
984
+ super.clientCreated(initialMessageCount);
985
+ this.afterEmit();
986
+ }
987
+ loadingChanged(isLoading) {
988
+ super.loadingChanged(isLoading);
989
+ this.afterEmit();
990
+ }
991
+ errorChanged(error) {
992
+ super.errorChanged(error);
993
+ this.afterEmit();
994
+ }
995
+ reloaded(fromMessageIndex) {
996
+ super.reloaded(fromMessageIndex);
997
+ this.afterEmit();
998
+ }
999
+ stopped() {
1000
+ super.stopped();
1001
+ this.afterEmit();
1002
+ }
1003
+ messagesCleared() {
1004
+ super.messagesCleared();
1005
+ this.afterEmit();
1006
+ }
1007
+ messageSent(messageId, content) {
1008
+ super.messageSent(messageId, content);
1009
+ this.afterEmit();
1010
+ }
1011
+ toolFixtureApplied(fixture) {
1012
+ super.toolFixtureApplied(fixture);
1013
+ this.afterEmit();
1014
+ }
1015
+ };
1159
1016
  function createChatDevtoolsBridge(options) {
1160
- return new ChatDevtoolsBridge(options);
1017
+ return new ChatDevtoolsBridge(options);
1161
1018
  }
1162
1019
  function createGenerationDevtoolsBridge(options) {
1163
- return new GenerationDevtoolsBridge(options);
1020
+ return new GenerationDevtoolsBridge(options);
1164
1021
  }
1165
1022
  function createVideoDevtoolsBridge(options) {
1166
- return new VideoDevtoolsBridge(options);
1023
+ return new VideoDevtoolsBridge(options);
1167
1024
  }
1168
- export {
1169
- ChatDevtoolsBridge,
1170
- ClientDevtoolsBridge,
1171
- GenerationDevtoolsBridge,
1172
- VideoDevtoolsBridge,
1173
- createAIDevtoolsGenerationPreview,
1174
- createChatDevtoolsBridge,
1175
- createGenerationDevtoolsBridge,
1176
- createVideoDevtoolsBridge
1177
- };
1178
- //# sourceMappingURL=devtools.js.map
1025
+ //#endregion
1026
+ export { ChatDevtoolsBridge, ClientDevtoolsBridge, GenerationDevtoolsBridge, VideoDevtoolsBridge, createAIDevtoolsGenerationPreview, createChatDevtoolsBridge, createGenerationDevtoolsBridge, createVideoDevtoolsBridge };
1027
+
1028
+ //# sourceMappingURL=devtools.js.map