assistant-cloud 0.2.1 → 0.2.3

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 (91) hide show
  1. package/README.md +24 -8
  2. package/dist/AssistantCloud.d.ts +16 -19
  3. package/dist/AssistantCloud.d.ts.map +1 -1
  4. package/dist/AssistantCloud.js +7 -2
  5. package/dist/AssistantCloud.js.map +1 -1
  6. package/dist/AssistantCloudAPI.d.ts +74 -72
  7. package/dist/AssistantCloudAPI.d.ts.map +1 -1
  8. package/dist/AssistantCloudAPI.js.map +1 -1
  9. package/dist/AssistantCloudAuthStrategy.d.ts +29 -32
  10. package/dist/AssistantCloudAuthStrategy.d.ts.map +1 -1
  11. package/dist/AssistantCloudAuthTokens.d.ts +7 -9
  12. package/dist/AssistantCloudAuthTokens.d.ts.map +1 -1
  13. package/dist/AssistantCloudEvents.d.ts +35 -29
  14. package/dist/AssistantCloudEvents.d.ts.map +1 -1
  15. package/dist/AssistantCloudEvents.js +66 -17
  16. package/dist/AssistantCloudEvents.js.map +1 -1
  17. package/dist/AssistantCloudFiles.d.ts +27 -29
  18. package/dist/AssistantCloudFiles.d.ts.map +1 -1
  19. package/dist/AssistantCloudProjectThreadMessages.d.ts +11 -13
  20. package/dist/AssistantCloudProjectThreadMessages.d.ts.map +1 -1
  21. package/dist/AssistantCloudProjectThreads.d.ts +12 -14
  22. package/dist/AssistantCloudProjectThreads.d.ts.map +1 -1
  23. package/dist/AssistantCloudProjects.d.ts +4 -7
  24. package/dist/AssistantCloudProjects.d.ts.map +1 -1
  25. package/dist/AssistantCloudRuns.d.ts +67 -56
  26. package/dist/AssistantCloudRuns.d.ts.map +1 -1
  27. package/dist/AssistantCloudRuns.js +9 -4
  28. package/dist/AssistantCloudRuns.js.map +1 -1
  29. package/dist/AssistantCloudScores.d.ts +20 -23
  30. package/dist/AssistantCloudScores.d.ts.map +1 -1
  31. package/dist/AssistantCloudThreadMessages.d.ts +41 -38
  32. package/dist/AssistantCloudThreadMessages.d.ts.map +1 -1
  33. package/dist/AssistantCloudThreadMessages.js +16 -4
  34. package/dist/AssistantCloudThreadMessages.js.map +1 -1
  35. package/dist/AssistantCloudThreads.d.ts +42 -44
  36. package/dist/AssistantCloudThreads.d.ts.map +1 -1
  37. package/dist/CloudEngagementReporter.d.ts +56 -58
  38. package/dist/CloudEngagementReporter.d.ts.map +1 -1
  39. package/dist/CloudEngagementReporter.js +4 -1
  40. package/dist/CloudEngagementReporter.js.map +1 -1
  41. package/dist/CloudMessagePersistence.d.ts +56 -58
  42. package/dist/CloudMessagePersistence.d.ts.map +1 -1
  43. package/dist/CloudMessagePersistence.js +3 -0
  44. package/dist/CloudMessagePersistence.js.map +1 -1
  45. package/dist/CloudRunReporter.d.ts +10 -13
  46. package/dist/CloudRunReporter.d.ts.map +1 -1
  47. package/dist/CloudRunReporter.js +13 -4
  48. package/dist/CloudRunReporter.js.map +1 -1
  49. package/dist/FormattedCloudPersistence.d.ts +36 -39
  50. package/dist/FormattedCloudPersistence.d.ts.map +1 -1
  51. package/dist/ai-sdk/index.d.ts +14 -17
  52. package/dist/ai-sdk/index.d.ts.map +1 -1
  53. package/dist/cloudResponse.d.ts +14 -17
  54. package/dist/cloudResponse.d.ts.map +1 -1
  55. package/dist/generateThreadTitle.d.ts +9 -12
  56. package/dist/generateThreadTitle.d.ts.map +1 -1
  57. package/dist/index.d.ts +18 -17
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/instrumentMcpSampling.d.ts +36 -39
  60. package/dist/instrumentMcpSampling.d.ts.map +1 -1
  61. package/dist/runTelemetry.d.ts +101 -94
  62. package/dist/runTelemetry.d.ts.map +1 -1
  63. package/dist/runTelemetry.js +57 -15
  64. package/dist/runTelemetry.js.map +1 -1
  65. package/dist/telemetry/index.d.ts +20 -23
  66. package/dist/telemetry/index.d.ts.map +1 -1
  67. package/dist/version.d.ts +1 -4
  68. package/dist/version.d.ts.map +1 -1
  69. package/dist/version.js +1 -1
  70. package/package.json +10 -9
  71. package/src/AssistantCloud.ts +17 -2
  72. package/src/AssistantCloudAPI.ts +4 -0
  73. package/src/AssistantCloudEvents.test.ts +157 -0
  74. package/src/AssistantCloudEvents.ts +82 -20
  75. package/src/AssistantCloudRuns.ts +35 -5
  76. package/src/AssistantCloudThreadMessages.test.ts +53 -2
  77. package/src/AssistantCloudThreadMessages.ts +34 -2
  78. package/src/BackendTranscriptCopy.test.ts +79 -0
  79. package/src/CloudEngagementReporter.ts +9 -3
  80. package/src/CloudMessagePersistence.ts +4 -0
  81. package/src/CloudRunReporter.ts +16 -4
  82. package/src/ai-sdk/index.test.ts +2 -2
  83. package/src/runTelemetry.test.ts +226 -2
  84. package/src/runTelemetry.ts +121 -17
  85. package/src/tests/AssistantCloud.test.ts +22 -0
  86. package/src/tests/AssistantCloudRuns.test.ts +28 -0
  87. package/src/tests/CloudEngagementReporter.test.ts +45 -0
  88. package/src/tests/CloudRunReporter.test.ts +49 -2
  89. package/dist/tests/setup.d.ts +0 -1
  90. package/dist/tests/setup.js +0 -7
  91. package/dist/tests/setup.js.map +0 -1
@@ -43,14 +43,65 @@ describe("AssistantCloudThreadMessages responses", () => {
43
43
 
44
44
  await expect(
45
45
  messages.feedback("thread/1", "message/1", body),
46
- ).resolves.toEqual({
46
+ ).resolves.toStrictEqual({
47
47
  feedback_id: "feedback-1",
48
48
  type: "positive",
49
49
  });
50
50
  expect(makeRequest).toHaveBeenCalledWith(
51
51
  "/threads/thread%2F1/messages/message%2F1/feedback",
52
- { method: "POST", body },
52
+ { method: "POST", body: { type: "positive" } },
53
53
  );
54
+ expect(makeRequest.mock.calls[0]![1].body).toStrictEqual({
55
+ type: "positive",
56
+ });
57
+
58
+ const bodyWithComment = {
59
+ type: "negative" as const,
60
+ comment: "The response missed a detail",
61
+ };
62
+ makeRequest.mockResolvedValueOnce({
63
+ feedback_id: "feedback-2",
64
+ type: "negative",
65
+ comment: "The response missed a detail",
66
+ });
67
+
68
+ await expect(
69
+ messages.feedback("thread-1", "message-1", bodyWithComment),
70
+ ).resolves.toEqual({
71
+ feedback_id: "feedback-2",
72
+ type: "negative",
73
+ comment: "The response missed a detail",
74
+ });
75
+ expect(makeRequest).toHaveBeenLastCalledWith(
76
+ "/threads/thread-1/messages/message-1/feedback",
77
+ { method: "POST", body: bodyWithComment },
78
+ );
79
+
80
+ makeRequest.mockResolvedValueOnce({
81
+ feedback_id: "feedback-1",
82
+ type: "positive",
83
+ });
84
+ await messages.feedback("thread-1", "message-1", {
85
+ type: "positive",
86
+ comment: " ",
87
+ });
88
+ expect(makeRequest.mock.lastCall![1].body).toStrictEqual({
89
+ type: "positive",
90
+ });
91
+
92
+ makeRequest.mockResolvedValueOnce({
93
+ feedback_id: "feedback-3",
94
+ type: "positive",
95
+ comment: null,
96
+ });
97
+
98
+ await expect(
99
+ messages.feedback("thread-1", "message-1", body),
100
+ ).resolves.toStrictEqual({
101
+ feedback_id: "feedback-3",
102
+ type: "positive",
103
+ comment: null,
104
+ });
54
105
 
55
106
  makeRequest.mockResolvedValueOnce({ type: "positive" });
56
107
 
@@ -19,6 +19,7 @@ export type CloudMessage = {
19
19
  updated_at: Date;
20
20
  format: "aui/v0" | string;
21
21
  content: ReadonlyJSONObject;
22
+ external_id?: string | null | undefined;
22
23
  };
23
24
 
24
25
  type AssistantCloudThreadMessageListQuery = {
@@ -35,6 +36,8 @@ type AssistantCloudThreadMessageCreateBody = {
35
36
  parent_id: string | null;
36
37
  format: "aui/v0" | string;
37
38
  content: ReadonlyJSONObject;
39
+ external_id?: string | undefined;
40
+ parent_external_id?: string | undefined;
38
41
  };
39
42
 
40
43
  type AssistantCloudMessageCreateResponse = {
@@ -49,11 +52,13 @@ const MESSAGE_FEEDBACK_TYPES = ["positive", "negative"] as const;
49
52
 
50
53
  export type AssistantCloudThreadMessageFeedbackBody = {
51
54
  type: "positive" | "negative";
55
+ comment?: string;
52
56
  };
53
57
 
54
58
  export type AssistantCloudThreadMessageFeedbackResponse = {
55
59
  feedback_id: string;
56
60
  type: "positive" | "negative";
61
+ comment?: string | null;
57
62
  };
58
63
 
59
64
  export const decodeCloudMessage = (
@@ -69,6 +74,10 @@ export const decodeCloudMessage = (
69
74
  updated_at: readCloudTimestamp(message.updated_at, `${field}.updated_at`),
70
75
  format: readCloudString(message.format, `${field}.format`),
71
76
  content: readCloudJSONObject(message.content, `${field}.content`),
77
+ external_id: readCloudNullableString(
78
+ message.external_id ?? null,
79
+ `${field}.external_id`,
80
+ ),
72
81
  };
73
82
  };
74
83
 
@@ -106,7 +115,20 @@ export class AssistantCloudThreadMessages {
106
115
  const response = readCloudRecord(
107
116
  await this.cloud.makeRequest(
108
117
  `/threads/${encodeURIComponent(threadId)}/messages`,
109
- { method: "POST", body },
118
+ {
119
+ method: "POST",
120
+ body: {
121
+ parent_id: body.parent_id,
122
+ format: body.format,
123
+ content: body.content,
124
+ ...(body.external_id !== undefined
125
+ ? { external_id: body.external_id }
126
+ : undefined),
127
+ ...(body.parent_external_id !== undefined
128
+ ? { parent_external_id: body.parent_external_id }
129
+ : undefined),
130
+ },
131
+ },
110
132
  ),
111
133
  "thread message create response",
112
134
  );
@@ -132,10 +154,17 @@ export class AssistantCloudThreadMessages {
132
154
  messageId: string,
133
155
  body: AssistantCloudThreadMessageFeedbackBody,
134
156
  ): Promise<AssistantCloudThreadMessageFeedbackResponse> {
157
+ const comment = body.comment?.trim();
135
158
  const response = readCloudRecord(
136
159
  await this.cloud.makeRequest(
137
160
  `/threads/${encodeURIComponent(threadId)}/messages/${encodeURIComponent(messageId)}/feedback`,
138
- { method: "POST", body },
161
+ {
162
+ method: "POST",
163
+ body: {
164
+ type: body.type,
165
+ ...(comment ? { comment } : undefined),
166
+ },
167
+ },
139
168
  ),
140
169
  "thread message feedback response",
141
170
  );
@@ -143,6 +172,9 @@ export class AssistantCloudThreadMessages {
143
172
  return {
144
173
  feedback_id: readCloudString(response.feedback_id, "feedback_id"),
145
174
  type: readCloudEnum(response.type, "type", MESSAGE_FEEDBACK_TYPES),
175
+ ...("comment" in response
176
+ ? { comment: readCloudNullableString(response.comment, "comment") }
177
+ : undefined),
146
178
  };
147
179
  }
148
180
  }
@@ -0,0 +1,79 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type { AssistantCloud } from "./AssistantCloud";
3
+ import type { AssistantCloudAPI } from "./AssistantCloudAPI";
4
+ import { AssistantCloudThreadMessages } from "./AssistantCloudThreadMessages";
5
+ import { CloudMessagePersistence } from "./CloudMessagePersistence";
6
+
7
+ describe("backend transcript cloud client", () => {
8
+ it("sends external ids only when supplied", async () => {
9
+ const makeRequest = vi.fn().mockResolvedValue({ message_id: "cloud-1" });
10
+ const messages = new AssistantCloudThreadMessages({
11
+ makeRequest,
12
+ } as unknown as AssistantCloudAPI);
13
+ const body = { parent_id: null, format: "aui/v0", content: {} };
14
+
15
+ await messages.create("thread-1", body);
16
+ expect(makeRequest).toHaveBeenLastCalledWith("/threads/thread-1/messages", {
17
+ method: "POST",
18
+ body,
19
+ });
20
+
21
+ await messages.create("thread-1", {
22
+ ...body,
23
+ external_id: "backend-1",
24
+ parent_external_id: "backend-0",
25
+ });
26
+ expect(makeRequest).toHaveBeenLastCalledWith("/threads/thread-1/messages", {
27
+ method: "POST",
28
+ body: {
29
+ ...body,
30
+ external_id: "backend-1",
31
+ parent_external_id: "backend-0",
32
+ },
33
+ });
34
+ });
35
+
36
+ it("decodes external ids and treats an absent field as null", async () => {
37
+ const makeRequest = vi.fn().mockResolvedValue({
38
+ messages: [
39
+ {
40
+ id: "cloud-1",
41
+ parent_id: null,
42
+ height: 0,
43
+ created_at: "2026-01-01T00:00:00.000Z",
44
+ updated_at: "2026-01-01T00:00:00.000Z",
45
+ format: "aui/v0",
46
+ content: {},
47
+ external_id: "backend-1",
48
+ },
49
+ {
50
+ id: "cloud-2",
51
+ parent_id: "cloud-1",
52
+ height: 1,
53
+ created_at: "2026-01-01T00:00:00.000Z",
54
+ updated_at: "2026-01-01T00:00:00.000Z",
55
+ format: "aui/v0",
56
+ content: {},
57
+ },
58
+ ],
59
+ });
60
+ const messages = new AssistantCloudThreadMessages({
61
+ makeRequest,
62
+ } as unknown as AssistantCloudAPI);
63
+
64
+ const result = await messages.list("thread-1");
65
+ expect(result.messages.map((message) => message.external_id)).toEqual([
66
+ "backend-1",
67
+ null,
68
+ ]);
69
+ });
70
+
71
+ it("records a resolved local to cloud message mapping", async () => {
72
+ const persistence = new CloudMessagePersistence({} as AssistantCloud);
73
+ persistence.record("backend-1", "cloud-1");
74
+
75
+ expect(persistence.isPersisted("backend-1")).toBe(true);
76
+ expect(await persistence.getRemoteId("backend-1")).toBe("cloud-1");
77
+ expect(persistence.getResolvedRemoteId("backend-1")).toBe("cloud-1");
78
+ });
79
+ });
@@ -13,13 +13,14 @@ export type EngagementEventIds = Pick<
13
13
  * Turns the ids an integration knows (its own thread and message ids) into the
14
14
  * ids the cloud stores. A send is the one event that may create the remote
15
15
  * thread, so it asks for `awaitThread`; every other event reads the ids that
16
- * already exist.
16
+ * already exist. Returning nothing declines the event: the thread is not one
17
+ * the integration knows, so there is nothing to attribute it to.
17
18
  */
18
19
  export type EngagementIdResolver = (
19
20
  threadId: string,
20
21
  messageId: string | undefined,
21
22
  options: { awaitThread: boolean },
22
- ) => EngagementEventIds | Promise<EngagementEventIds>;
23
+ ) => EngagementEventIds | undefined | Promise<EngagementEventIds | undefined>;
23
24
 
24
25
  type EngagementEventInit = Pick<AssistantCloudEvent, "value" | "props">;
25
26
 
@@ -86,9 +87,13 @@ export class CloudEngagementReporter {
86
87
  public runStopped(threadId: string): void {
87
88
  const startedAt = this.runStartedAt.get(threadId);
88
89
  if (startedAt === undefined) return;
90
+ const stoppedAt = Date.now();
89
91
  this.runStartedAt.delete(threadId);
92
+ // A stopped run is a run that ended, so the next send measures its idle
93
+ // time from here rather than from the last run allowed to finish.
94
+ remember(this.runEndedAt, threadId, stoppedAt);
90
95
  this.track("run_stopped", threadId, undefined, {
91
- value: Math.max(0, Date.now() - startedAt),
96
+ value: Math.max(0, stoppedAt - startedAt),
92
97
  });
93
98
  }
94
99
 
@@ -223,6 +228,7 @@ export class CloudEngagementReporter {
223
228
  void Promise.resolve()
224
229
  .then(() => this.resolveIds(threadId, messageId, options))
225
230
  .then((ids) => {
231
+ if (!ids) return;
226
232
  if (kind === "thread_switched" && !ids.thread_id) return;
227
233
  this.getCloud().events.track({ kind, ...init, ...ids });
228
234
  })
@@ -111,6 +111,10 @@ export class CloudMessagePersistence {
111
111
  return typeof entry === "string" ? entry : undefined;
112
112
  }
113
113
 
114
+ record(localId: string, remoteId: string): void {
115
+ this.idMapping.set(localId, remoteId);
116
+ }
117
+
114
118
  /**
115
119
  * Load messages from the cloud and populate the ID mapping.
116
120
  *
@@ -1,4 +1,5 @@
1
1
  import type { AssistantCloud } from "./AssistantCloud";
2
+ import { CloudAPIError } from "./AssistantCloudAPI";
2
3
  import { createRunReport, type RunReportInit } from "./runTelemetry";
3
4
 
4
5
  export type CloudRunReportInit = Omit<RunReportInit, "telemetry">;
@@ -7,8 +8,8 @@ export type CloudRunReportInit = Omit<RunReportInit, "telemetry">;
7
8
  * Sends run reports the way every client integration has to: nothing while
8
9
  * telemetry is off, the cloud's environment, release and tags on every report,
9
10
  * the `beforeReport` hook applied last, and a failed send that never surfaces.
10
- * A report given a key is sent once per key, so an integration that observes
11
- * the same finished run twice reports it once.
11
+ * A keyed report is deduplicated while in flight and after an attempt. A
12
+ * rate-limited attempt releases the key so a later observation can try again.
12
13
  */
13
14
  export class CloudRunReporter {
14
15
  private readonly reported = new Set<string>();
@@ -19,6 +20,7 @@ export class CloudRunReporter {
19
20
  }
20
21
 
21
22
  public async report(init: CloudRunReportInit, key?: string): Promise<void> {
23
+ let claimedKey: string | undefined;
22
24
  try {
23
25
  const cloud = this.getCloud();
24
26
  if (!cloud.telemetry.enabled) return;
@@ -29,10 +31,20 @@ export class CloudRunReporter {
29
31
  const report = beforeReport ? beforeReport(initial) : initial;
30
32
  if (!report) return;
31
33
 
32
- if (key !== undefined) this.reported.add(key);
34
+ if (key !== undefined) {
35
+ this.reported.add(key);
36
+ claimedKey = key;
37
+ }
33
38
  await cloud.runs.report(report);
34
- } catch {
39
+ } catch (error) {
40
+ if (claimedKey !== undefined && isRetryableReportError(error)) {
41
+ this.reported.delete(claimedKey);
42
+ }
35
43
  return;
36
44
  }
37
45
  }
38
46
  }
47
+
48
+ const isRetryableReportError = (error: unknown): boolean => {
49
+ return error instanceof CloudAPIError && error.status === 429;
50
+ };
@@ -79,7 +79,7 @@ describe("extractAISDKRunTelemetry", () => {
79
79
  {},
80
80
  ],
81
81
  samplingCalls: { call_2: [{ duration_ms: 20 }] },
82
- modelId: "gpt-5.6-luna",
82
+ modelId: "gpt-6-luna",
83
83
  },
84
84
  );
85
85
 
@@ -119,7 +119,7 @@ describe("extractAISDKRunTelemetry", () => {
119
119
  totalSteps: 3,
120
120
  outputText: "Cold.",
121
121
  usage: { inputTokens: 10, outputTokens: 5 },
122
- modelId: "gpt-5.6-luna",
122
+ modelId: "gpt-6-luna",
123
123
  metadata: message.metadata,
124
124
  });
125
125
  });
@@ -187,7 +187,7 @@ describe("createRunReport", () => {
187
187
  threadId: "ai-sdk-thread",
188
188
  status: "completed",
189
189
  traceId: "00112233445566778899aabbccddeeff",
190
- modelId: "gpt-5.6-terra",
190
+ modelId: "gpt-6-sol",
191
191
  provider: "gateway",
192
192
  usage: {
193
193
  inputTokens: 100,
@@ -215,7 +215,7 @@ describe("createRunReport", () => {
215
215
  thread_id: "ai-sdk-thread",
216
216
  status: "completed",
217
217
  trace_id: "00112233445566778899aabbccddeeff",
218
- model_id: "gpt-5.6-terra",
218
+ model_id: "gpt-6-sol",
219
219
  provider: "gateway",
220
220
  provider_type: "gateway",
221
221
  input_tokens: 100,
@@ -289,6 +289,90 @@ describe("createRunReport", () => {
289
289
  }),
290
290
  ).toEqual({ thread_id: "thread", status: "completed" });
291
291
  });
292
+
293
+ it("normalizes caller-supplied run cost, attributes, and root span", () => {
294
+ expect(
295
+ createRunReport({
296
+ threadId: "thread",
297
+ status: "completed",
298
+ rootSpanId: "AABBCCDDEEFF0011",
299
+ costUsd: 0.012,
300
+ costDetails: {
301
+ input: 0.002,
302
+ inputCachedTokens: 0.001,
303
+ output: 0.009,
304
+ total: 0.012,
305
+ },
306
+ attributes: { tenant: "acme" },
307
+ }),
308
+ ).toEqual({
309
+ thread_id: "thread",
310
+ status: "completed",
311
+ root_span_id: "aabbccddeeff0011",
312
+ cost_usd: 0.012,
313
+ cost_details: {
314
+ input: 0.002,
315
+ input_cached_tokens: 0.001,
316
+ output: 0.009,
317
+ total: 0.012,
318
+ },
319
+ attributes: { tenant: "acme" },
320
+ });
321
+ });
322
+
323
+ it("keeps valid cost members beside invalid ones", () => {
324
+ expect(
325
+ createRunReport({
326
+ threadId: "thread",
327
+ status: "completed",
328
+ costUsd: 0,
329
+ costDetails: {
330
+ input: 0.002,
331
+ inputCachedTokens: Number.NaN,
332
+ output: -1,
333
+ total: 0,
334
+ },
335
+ }),
336
+ ).toEqual({
337
+ thread_id: "thread",
338
+ status: "completed",
339
+ cost_usd: 0,
340
+ cost_details: { input: 0.002, total: 0 },
341
+ });
342
+ });
343
+
344
+ it("omits invalid run cost and root span values", () => {
345
+ expect(
346
+ createRunReport({
347
+ threadId: "thread",
348
+ status: "completed",
349
+ rootSpanId: "not-a-span-id",
350
+ costUsd: Number.POSITIVE_INFINITY,
351
+ costDetails: {
352
+ input: -1,
353
+ inputCachedTokens: Number.NaN,
354
+ output: Number.NEGATIVE_INFINITY,
355
+ },
356
+ }),
357
+ ).toEqual({ thread_id: "thread", status: "completed" });
358
+ });
359
+
360
+ it("keeps a server outcome type and truncates the step input", () => {
361
+ expect(
362
+ createRunReport({
363
+ threadId: "thread",
364
+ status: "error",
365
+ outcome: "timeout",
366
+ steps: [{ input: "a".repeat(MAX + 1) }],
367
+ }),
368
+ ).toEqual({
369
+ thread_id: "thread",
370
+ status: "error",
371
+ outcome_type: "timeout",
372
+ steps: [{ input: "a".repeat(MAX) }],
373
+ total_steps: 1,
374
+ });
375
+ });
292
376
  });
293
377
 
294
378
  describe("truncateRunTelemetryText", () => {
@@ -371,6 +455,146 @@ describe("createRunTelemetryToolCall", () => {
371
455
  expect(call.tool_result).not.toContain("A".repeat(200));
372
456
  });
373
457
 
458
+ it("summarizes a short base64 payload the same as a long one", () => {
459
+ // `aGk=` is how this repo's own MCP fixtures spell a whole image.
460
+ const call = createRunTelemetryToolCall({
461
+ toolName: "t",
462
+ toolCallId: "call-1",
463
+ toolSource: "mcp",
464
+ result: {
465
+ content: [
466
+ { type: "image", data: "aGk=", mimeType: "image/png" },
467
+ { type: "audio", data: "aGk=", mimeType: "audio/wav" },
468
+ {
469
+ type: "resource",
470
+ resource: { uri: "file:///a.pdf", blob: "aGk=" },
471
+ },
472
+ ],
473
+ },
474
+ });
475
+ expect(call.tool_result).not.toContain("aGk=");
476
+ expect(call.tool_result).toContain("[image:");
477
+ expect(call.tool_result).toContain("[audio:");
478
+ expect(call.tool_result).toContain("[resource:");
479
+ // a field the grammar does not define as base64 is left alone
480
+ expect(call.tool_result).toContain("image/png");
481
+ });
482
+
483
+ it("leaves plain text in an mcp result untouched", () => {
484
+ const call = createRunTelemetryToolCall({
485
+ toolName: "t",
486
+ toolCallId: "call-1",
487
+ toolSource: "mcp",
488
+ result: { content: [{ type: "text", text: "test" }] },
489
+ });
490
+ expect(call.tool_result).toContain("test");
491
+ });
492
+
493
+ it("summarizes base64 blocks inside an mcp CallToolResult envelope", () => {
494
+ // what @modelcontextprotocol/sdk callTool actually returns
495
+ const call = createRunTelemetryToolCall({
496
+ toolName: "t",
497
+ toolCallId: "call-1",
498
+ toolSource: "mcp",
499
+ result: {
500
+ content: [
501
+ { type: "text", text: "keep me" },
502
+ { type: "image", data: "A".repeat(4096) },
503
+ ],
504
+ isError: false,
505
+ },
506
+ });
507
+
508
+ expect(call.tool_result).toContain("keep me");
509
+ expect(call.tool_result).toContain("[image: 3.0KB]");
510
+ expect(call.tool_result).not.toContain("A".repeat(200));
511
+ });
512
+
513
+ it("keeps the envelope's sibling fields when summarizing its content", () => {
514
+ const call = createRunTelemetryToolCall({
515
+ toolName: "t",
516
+ toolCallId: "call-1",
517
+ toolSource: "mcp",
518
+ result: {
519
+ content: [{ type: "image", data: "A".repeat(4096) }],
520
+ isError: true,
521
+ structuredContent: { ok: false },
522
+ },
523
+ });
524
+
525
+ expect(call.tool_result).toContain("[image: 3.0KB]");
526
+ expect(call.tool_result).toContain('"isError":true');
527
+ expect(call.tool_result).toContain('"structuredContent":{"ok":false}');
528
+ });
529
+
530
+ it("summarizes a CallToolResult that arrived as a JSON string", () => {
531
+ const call = createRunTelemetryToolCall({
532
+ toolName: "t",
533
+ toolCallId: "call-1",
534
+ toolSource: "mcp",
535
+ result: JSON.stringify({
536
+ content: [{ type: "audio", data: "A".repeat(4096) }],
537
+ }),
538
+ });
539
+
540
+ expect(call.tool_result).toContain("[audio: 3.0KB]");
541
+ expect(call.tool_result).not.toContain("A".repeat(200));
542
+ });
543
+
544
+ it("leaves an object without content blocks alone", () => {
545
+ const call = createRunTelemetryToolCall({
546
+ toolName: "t",
547
+ toolCallId: "call-1",
548
+ toolSource: "mcp",
549
+ result: { content: "not blocks", other: 1 },
550
+ });
551
+
552
+ expect(call.tool_result).toContain('"content":"not blocks"');
553
+ expect(call.tool_result).toContain('"other":1');
554
+ });
555
+
556
+ it("summarizes the base64 blob of an embedded resource", () => {
557
+ const call = createRunTelemetryToolCall({
558
+ toolName: "t",
559
+ toolCallId: "call-1",
560
+ toolSource: "mcp",
561
+ result: {
562
+ content: [
563
+ {
564
+ type: "resource",
565
+ resource: {
566
+ uri: "file:///report.pdf",
567
+ mimeType: "application/pdf",
568
+ blob: "A".repeat(4096),
569
+ },
570
+ },
571
+ ],
572
+ },
573
+ });
574
+
575
+ expect(call.tool_result).toContain("[resource: 3.0KB]");
576
+ expect(call.tool_result).toContain("file:///report.pdf");
577
+ expect(call.tool_result).not.toContain("A".repeat(200));
578
+ });
579
+
580
+ it("leaves a text-bearing embedded resource alone", () => {
581
+ const call = createRunTelemetryToolCall({
582
+ toolName: "t",
583
+ toolCallId: "call-1",
584
+ toolSource: "mcp",
585
+ result: {
586
+ content: [
587
+ {
588
+ type: "resource",
589
+ resource: { uri: "file:///a.txt", text: "plain text" },
590
+ },
591
+ ],
592
+ },
593
+ });
594
+
595
+ expect(call.tool_result).toContain("plain text");
596
+ });
597
+
374
598
  it("leaves a non-mcp result unsummarized", () => {
375
599
  const result = [{ type: "image", data: "A".repeat(4096) }];
376
600
  const call = createRunTelemetryToolCall({