assistant-cloud 0.2.2 → 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 (87) hide show
  1. package/README.md +2 -2
  2. package/dist/AssistantCloud.d.ts +15 -17
  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 +69 -66
  7. package/dist/AssistantCloudAPI.d.ts.map +1 -1
  8. package/dist/AssistantCloudAPI.js.map +1 -1
  9. package/dist/AssistantCloudAuthStrategy.d.ts +23 -25
  10. package/dist/AssistantCloudAuthStrategy.d.ts.map +1 -1
  11. package/dist/AssistantCloudAuthTokens.d.ts +6 -7
  12. package/dist/AssistantCloudAuthTokens.d.ts.map +1 -1
  13. package/dist/AssistantCloudEvents.d.ts +32 -25
  14. package/dist/AssistantCloudEvents.d.ts.map +1 -1
  15. package/dist/AssistantCloudEvents.js +65 -16
  16. package/dist/AssistantCloudEvents.js.map +1 -1
  17. package/dist/AssistantCloudFiles.d.ts +25 -26
  18. package/dist/AssistantCloudFiles.d.ts.map +1 -1
  19. package/dist/AssistantCloudProjectThreadMessages.d.ts +10 -11
  20. package/dist/AssistantCloudProjectThreadMessages.d.ts.map +1 -1
  21. package/dist/AssistantCloudProjectThreads.d.ts +11 -12
  22. package/dist/AssistantCloudProjectThreads.d.ts.map +1 -1
  23. package/dist/AssistantCloudProjects.d.ts +3 -5
  24. package/dist/AssistantCloudProjects.d.ts.map +1 -1
  25. package/dist/AssistantCloudRuns.d.ts +65 -53
  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 +17 -19
  30. package/dist/AssistantCloudScores.d.ts.map +1 -1
  31. package/dist/AssistantCloudThreadMessages.d.ts +33 -31
  32. package/dist/AssistantCloudThreadMessages.d.ts.map +1 -1
  33. package/dist/AssistantCloudThreadMessages.js +9 -2
  34. package/dist/AssistantCloudThreadMessages.js.map +1 -1
  35. package/dist/AssistantCloudThreads.d.ts +37 -38
  36. package/dist/AssistantCloudThreads.d.ts.map +1 -1
  37. package/dist/CloudEngagementReporter.d.ts +53 -54
  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 +55 -56
  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 +6 -8
  46. package/dist/CloudRunReporter.d.ts.map +1 -1
  47. package/dist/FormattedCloudPersistence.d.ts +34 -36
  48. package/dist/FormattedCloudPersistence.d.ts.map +1 -1
  49. package/dist/ai-sdk/index.d.ts +10 -12
  50. package/dist/ai-sdk/index.d.ts.map +1 -1
  51. package/dist/cloudResponse.d.ts +2 -4
  52. package/dist/cloudResponse.d.ts.map +1 -1
  53. package/dist/generateThreadTitle.d.ts +8 -10
  54. package/dist/generateThreadTitle.d.ts.map +1 -1
  55. package/dist/index.d.ts +18 -17
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/instrumentMcpSampling.d.ts +30 -32
  58. package/dist/instrumentMcpSampling.d.ts.map +1 -1
  59. package/dist/runTelemetry.d.ts +86 -78
  60. package/dist/runTelemetry.d.ts.map +1 -1
  61. package/dist/runTelemetry.js +57 -15
  62. package/dist/runTelemetry.js.map +1 -1
  63. package/dist/telemetry/index.d.ts +13 -15
  64. package/dist/telemetry/index.d.ts.map +1 -1
  65. package/dist/version.d.ts +0 -2
  66. package/dist/version.d.ts.map +1 -1
  67. package/dist/version.js +1 -1
  68. package/package.json +3 -3
  69. package/src/AssistantCloud.ts +17 -2
  70. package/src/AssistantCloudAPI.ts +4 -0
  71. package/src/AssistantCloudEvents.test.ts +116 -0
  72. package/src/AssistantCloudEvents.ts +81 -20
  73. package/src/AssistantCloudRuns.ts +35 -5
  74. package/src/AssistantCloudThreadMessages.ts +21 -1
  75. package/src/BackendTranscriptCopy.test.ts +79 -0
  76. package/src/CloudEngagementReporter.ts +9 -3
  77. package/src/CloudMessagePersistence.ts +4 -0
  78. package/src/ai-sdk/index.test.ts +2 -2
  79. package/src/runTelemetry.test.ts +226 -2
  80. package/src/runTelemetry.ts +121 -17
  81. package/src/tests/AssistantCloud.test.ts +22 -0
  82. package/src/tests/AssistantCloudRuns.test.ts +28 -0
  83. package/src/tests/CloudEngagementReporter.test.ts +45 -0
  84. package/src/tests/CloudRunReporter.test.ts +14 -2
  85. package/dist/tests/setup.d.ts +0 -1
  86. package/dist/tests/setup.js +0 -7
  87. package/dist/tests/setup.js.map +0 -1
@@ -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({
@@ -3,7 +3,7 @@ import type { AssistantCloudRunReport } from "./AssistantCloudRuns";
3
3
 
4
4
  const MAX_TELEMETRY_TEXT_LENGTH = 50_000;
5
5
 
6
- const BASE64_PATTERN = /^[A-Za-z0-9+/]{100,}={0,2}$/;
6
+ const BASE64_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/;
7
7
 
8
8
  export type AssistantCloudRunReportToolCall = {
9
9
  tool_name: string;
@@ -114,28 +114,81 @@ function safeStringify(value: unknown): string | undefined {
114
114
  }
115
115
  }
116
116
 
117
+ const base64SizeKB = (value: string) =>
118
+ ((value.length * 3) / 4 / 1024).toFixed(1);
119
+
120
+ // Both call sites read a field the MCP content grammar already defines as
121
+ // base64, so the test only has to recognise the encoding's own shape: its
122
+ // alphabet, and a length that is a multiple of four. A size floor would leave
123
+ // a short payload — `aGk=` is a whole image in this repo's own fixtures —
124
+ // serialized raw.
125
+ const isInlineBase64 = (value: unknown): value is string => {
126
+ if (typeof value !== "string") return false;
127
+ const head = value.slice(0, 200);
128
+ return head.length > 0 && head.length % 4 === 0 && BASE64_PATTERN.test(head);
129
+ };
130
+
131
+ function summarizeMcpContentBlock(item: unknown): unknown {
132
+ if (!item || typeof item !== "object") return item;
133
+ const block = item as {
134
+ type?: unknown;
135
+ data?: unknown;
136
+ resource?: unknown;
137
+ };
138
+ if (
139
+ (block.type === "image" || block.type === "audio") &&
140
+ isInlineBase64(block.data)
141
+ ) {
142
+ return {
143
+ ...block,
144
+ data: `[${String(block.type)}: ${base64SizeKB(block.data)}KB]`,
145
+ };
146
+ }
147
+ // An EmbeddedResource is the third inline carrier: a binary resource arrives
148
+ // as base64 under `resource.blob` rather than as a top-level `data` field.
149
+ if (
150
+ block.type === "resource" &&
151
+ block.resource &&
152
+ typeof block.resource === "object"
153
+ ) {
154
+ const resource = block.resource as { blob?: unknown };
155
+ if (isInlineBase64(resource.blob)) {
156
+ return {
157
+ ...block,
158
+ resource: {
159
+ ...resource,
160
+ blob: `[resource: ${base64SizeKB(resource.blob)}KB]`,
161
+ },
162
+ };
163
+ }
164
+ }
165
+ return item;
166
+ }
167
+
117
168
  function summarizeMcpResult(value: unknown): string | undefined {
118
169
  if (value == null) return undefined;
119
170
  try {
120
171
  const parsed = typeof value === "string" ? JSON.parse(value) : value;
121
172
  if (Array.isArray(parsed)) {
122
- const summarized = parsed.map((item) => {
123
- if (item && typeof item === "object" && item.type) {
124
- if (
125
- (item.type === "image" || item.type === "audio") &&
126
- typeof item.data === "string" &&
127
- BASE64_PATTERN.test(item.data.slice(0, 200))
128
- ) {
129
- const sizeKB = ((item.data.length * 3) / 4 / 1024).toFixed(1);
130
- return { ...item, data: `[${item.type}: ${sizeKB}KB]` };
131
- }
132
- }
133
- return item;
134
- });
135
- return truncateRunTelemetryText(JSON.stringify(summarized));
173
+ return truncateRunTelemetryText(
174
+ JSON.stringify(parsed.map(summarizeMcpContentBlock)),
175
+ );
176
+ }
177
+ // `callTool` resolves to a CallToolResult, so the blocks carrying base64
178
+ // arrive under `content` rather than as the result itself.
179
+ if (typeof parsed === "object") {
180
+ const content = (parsed as { content?: unknown }).content;
181
+ if (Array.isArray(content)) {
182
+ return truncateRunTelemetryText(
183
+ JSON.stringify({
184
+ ...(parsed as Record<string, unknown>),
185
+ content: content.map(summarizeMcpContentBlock),
186
+ }),
187
+ );
188
+ }
136
189
  }
137
190
  } catch {
138
- // not JSON array, fall through
191
+ // not a shape with summarizable content, fall through
139
192
  }
140
193
  return safeStringify(value);
141
194
  }
@@ -223,6 +276,7 @@ export type RunReportStepInit = {
223
276
  startMs?: number | undefined;
224
277
  endMs?: number | undefined;
225
278
  finishReason?: string | undefined;
279
+ input?: string | undefined;
226
280
  };
227
281
 
228
282
  /**
@@ -245,11 +299,12 @@ export type RunMessageTelemetry = {
245
299
  export type RunReportInit = {
246
300
  threadId: string;
247
301
  status: AssistantCloudRunReport["status"];
248
- outcome?: RunReportOutcome | undefined;
302
+ outcome?: AssistantCloudRunReport["outcome_type"] | undefined;
249
303
  errorCode?: string | undefined;
250
304
  error?: string | undefined;
251
305
  messageId?: string | undefined;
252
306
  traceId?: string | undefined;
307
+ rootSpanId?: string | undefined;
253
308
  modelId?: string | undefined;
254
309
  provider?: string | undefined;
255
310
  usage?: RunTelemetryUsageInit | undefined;
@@ -258,7 +313,17 @@ export type RunReportInit = {
258
313
  toolCalls?: AssistantCloudRunReportToolCall[] | undefined;
259
314
  durationMs?: number | undefined;
260
315
  firstTokenMs?: number | undefined;
316
+ costUsd?: number | undefined;
317
+ costDetails?:
318
+ | {
319
+ input?: number | undefined;
320
+ inputCachedTokens?: number | undefined;
321
+ output?: number | undefined;
322
+ total?: number | undefined;
323
+ }
324
+ | undefined;
261
325
  outputText?: string | undefined;
326
+ attributes?: Record<string, unknown> | undefined;
262
327
  metadata?: Record<string, unknown> | undefined;
263
328
  telemetry?: {
264
329
  environment?: string | undefined;
@@ -308,6 +373,9 @@ function createRunReportStep(
308
373
  if (init.finishReason !== undefined) {
309
374
  step.finish_reason = init.finishReason.slice(0, 32);
310
375
  }
376
+ if (init.input !== undefined) {
377
+ step.input = truncateRunTelemetryText(init.input);
378
+ }
311
379
  return step;
312
380
  }
313
381
 
@@ -334,6 +402,34 @@ function normalizeRunReportMilliseconds(
334
402
  return Math.max(0, Math.round(value));
335
403
  }
336
404
 
405
+ function normalizeRunReportCost(value: number | undefined): number | undefined {
406
+ if (value === undefined || !Number.isFinite(value) || value < 0) {
407
+ return undefined;
408
+ }
409
+ return value;
410
+ }
411
+
412
+ function assignCostDetails(
413
+ report: AssistantCloudRunReport,
414
+ costDetails: RunReportInit["costDetails"],
415
+ ): void {
416
+ if (!costDetails) return;
417
+ const normalized: NonNullable<AssistantCloudRunReport["cost_details"]> = {};
418
+ const input = normalizeRunReportCost(costDetails.input);
419
+ if (input !== undefined) normalized.input = input;
420
+ const inputCachedTokens = normalizeRunReportCost(
421
+ costDetails.inputCachedTokens,
422
+ );
423
+ if (inputCachedTokens !== undefined) {
424
+ normalized.input_cached_tokens = inputCachedTokens;
425
+ }
426
+ const output = normalizeRunReportCost(costDetails.output);
427
+ if (output !== undefined) normalized.output = output;
428
+ const total = normalizeRunReportCost(costDetails.total);
429
+ if (total !== undefined) normalized.total = total;
430
+ if (Object.keys(normalized).length > 0) report.cost_details = normalized;
431
+ }
432
+
337
433
  export function createRunReport(init: RunReportInit): AssistantCloudRunReport {
338
434
  const report: AssistantCloudRunReport = {
339
435
  thread_id: init.threadId,
@@ -341,6 +437,10 @@ export function createRunReport(init: RunReportInit): AssistantCloudRunReport {
341
437
  };
342
438
  const traceId = init.traceId?.toLowerCase();
343
439
  if (traceId && /^[0-9a-f]{32}$/.test(traceId)) report.trace_id = traceId;
440
+ const rootSpanId = init.rootSpanId?.toLowerCase();
441
+ if (rootSpanId && /^[0-9a-f]{16}$/.test(rootSpanId)) {
442
+ report.root_span_id = rootSpanId;
443
+ }
344
444
  if (init.outcome !== undefined) report.outcome_type = init.outcome;
345
445
  if (init.errorCode !== undefined) report.error_code = init.errorCode;
346
446
  if (init.error !== undefined) report.error = init.error;
@@ -362,9 +462,13 @@ export function createRunReport(init: RunReportInit): AssistantCloudRunReport {
362
462
  if (durationMs !== undefined) report.duration_ms = durationMs;
363
463
  const firstTokenMs = normalizeRunReportMilliseconds(init.firstTokenMs);
364
464
  if (firstTokenMs !== undefined) report.first_token_ms = firstTokenMs;
465
+ const costUsd = normalizeRunReportCost(init.costUsd);
466
+ if (costUsd !== undefined) report.cost_usd = costUsd;
467
+ assignCostDetails(report, init.costDetails);
365
468
  if (init.outputText !== undefined) {
366
469
  report.output_text = truncateRunTelemetryText(init.outputText);
367
470
  }
471
+ if (init.attributes !== undefined) report.attributes = init.attributes;
368
472
  if (init.metadata !== undefined) report.metadata = init.metadata;
369
473
  if (init.telemetry?.environment !== undefined) {
370
474
  report.environment = init.telemetry.environment;
@@ -14,6 +14,7 @@ const createCloud = (
14
14
 
15
15
  describe("AssistantCloud telemetry config", () => {
16
16
  afterEach(() => {
17
+ vi.useRealTimers();
17
18
  vi.unstubAllGlobals();
18
19
  });
19
20
 
@@ -34,6 +35,27 @@ describe("AssistantCloud telemetry config", () => {
34
35
  });
35
36
  });
36
37
 
38
+ it.each(["enabled", "events"] as const)(
39
+ "does not resume pending events after telemetry %s is disabled",
40
+ async (property) => {
41
+ vi.useFakeTimers();
42
+ const fetchMock = vi.fn().mockRejectedValueOnce(new Error("offline"));
43
+ vi.stubGlobal("fetch", fetchMock);
44
+ const cloud = createCloud();
45
+
46
+ for (let index = 0; index < 20; index++) {
47
+ cloud.events.track({ kind: "message_sent" });
48
+ }
49
+ await vi.waitFor(() => expect(fetchMock).toHaveBeenCalledOnce());
50
+
51
+ cloud.telemetry[property] = false;
52
+ cloud.telemetry[property] = true;
53
+ await vi.runAllTimersAsync();
54
+
55
+ expect(fetchMock).toHaveBeenCalledOnce();
56
+ },
57
+ );
58
+
37
59
  it("stays enabled when the config object carries an undefined enabled", () => {
38
60
  const beforeReport: NonNullable<
39
61
  AssistantCloudTelemetryConfig["beforeReport"]
@@ -90,6 +90,34 @@ describe("AssistantCloudRuns", () => {
90
90
  );
91
91
  expect(cancel).toHaveBeenCalledOnce();
92
92
  });
93
+
94
+ it("pins the UI message stream protocol in assistant options", () => {
95
+ const { protocol } =
96
+ createCloud().runs.__internal_getAssistantOptions("assistant-id");
97
+
98
+ expect(protocol).toBe("ui-message-stream");
99
+ });
100
+
101
+ it("uses the requested thread ID in assistant options", async () => {
102
+ const { body } =
103
+ createCloud().runs.__internal_getAssistantOptions("assistant-id");
104
+
105
+ await expect(body({ threadId: "remote-thread" })).resolves.toEqual({
106
+ assistant_id: "assistant-id",
107
+ response_format: "vercel-ai-data-stream/v1",
108
+ thread_id: "remote-thread",
109
+ });
110
+ });
111
+
112
+ it("rejects assistant options without a thread ID", async () => {
113
+ const { body } =
114
+ createCloud().runs.__internal_getAssistantOptions("assistant-id");
115
+
116
+ await expect(body({})).rejects.toThrow(
117
+ "Assistant Cloud runs need a thread",
118
+ );
119
+ await expect(body()).rejects.toThrow("Assistant Cloud runs need a thread");
120
+ });
93
121
  });
94
122
 
95
123
  const createCloudRuns = () => {
@@ -47,6 +47,24 @@ describe("CloudEngagementReporter", () => {
47
47
  });
48
48
  });
49
49
 
50
+ it("drops an event its resolver declines", async () => {
51
+ const { cloud, track } = createCloud();
52
+ const reporter = new CloudEngagementReporter(cloud, (threadId) =>
53
+ threadId === "known" ? { thread_id: "remote-known" } : undefined,
54
+ );
55
+
56
+ reporter.messageSent("unknown", { chars: 3, attachments: 0 });
57
+ reporter.messageSent("known", { chars: 5, attachments: 0 });
58
+ await flush();
59
+
60
+ expect(track).toHaveBeenCalledOnce();
61
+ expect(track).toHaveBeenCalledWith({
62
+ kind: "message_sent",
63
+ thread_id: "remote-known",
64
+ props: { chars: 5, attachments: 0 },
65
+ });
66
+ });
67
+
50
68
  it("reports tool approval decisions with their resolved message IDs", async () => {
51
69
  const { cloud, track } = createCloud();
52
70
  const reporter = new CloudEngagementReporter(
@@ -113,6 +131,33 @@ describe("CloudEngagementReporter", () => {
113
131
  );
114
132
  });
115
133
 
134
+ it("measures the next send from a stopped run, not the last completed one", async () => {
135
+ const { cloud, track } = createCloud();
136
+ const reporter = new CloudEngagementReporter(cloud);
137
+
138
+ vi.setSystemTime(new Date("2023-01-01T00:00:00.000Z"));
139
+ reporter.runStarted("t1");
140
+ vi.setSystemTime(new Date("2023-01-01T00:01:40.000Z"));
141
+ reporter.runEnded("t1");
142
+
143
+ // a second run the user stops rather than lets finish
144
+ vi.setSystemTime(new Date("2023-01-01T00:03:20.000Z"));
145
+ reporter.runStarted("t1");
146
+ vi.setSystemTime(new Date("2023-01-01T00:05:00.000Z"));
147
+ reporter.runStopped("t1");
148
+
149
+ vi.setSystemTime(new Date("2023-01-01T00:05:10.000Z"));
150
+ reporter.messageSent("t1", { chars: 3, attachments: 0 });
151
+ await flush();
152
+
153
+ // a stopped run is still a run that ended, so the idle gap is the 10s
154
+ // since the stop, not the 210s since the last completed run
155
+ const sent = track.mock.calls
156
+ .map(([event]) => event as { kind: string; value?: number })
157
+ .find((event) => event.kind === "message_sent");
158
+ expect(sent?.value).toBe(10_000);
159
+ });
160
+
116
161
  it("shows one error per run and one suggestion list per thread", async () => {
117
162
  const { cloud, track } = createCloud();
118
163
  const reporter = new CloudEngagementReporter(cloud);
@@ -21,7 +21,13 @@ describe("CloudRunReporter", () => {
21
21
  });
22
22
 
23
23
  it("stamps the cloud's environment, release and tags and applies beforeReport last", async () => {
24
- const beforeReport = vi.fn((report) => ({ ...report, model_id: "gpt" }));
24
+ const beforeReport = vi.fn((report) => ({
25
+ ...report,
26
+ model_id: "gpt",
27
+ cost_usd: 0.012,
28
+ attributes: { tenant: "acme" },
29
+ root_span_id: "0011223344556677",
30
+ }));
25
31
  const { cloud, report } = createCloud({
26
32
  enabled: true,
27
33
  environment: "production",
@@ -43,7 +49,13 @@ describe("CloudRunReporter", () => {
43
49
  }),
44
50
  );
45
51
  expect(report).toHaveBeenCalledWith(
46
- expect.objectContaining({ thread_id: "thread_1", model_id: "gpt" }),
52
+ expect.objectContaining({
53
+ thread_id: "thread_1",
54
+ model_id: "gpt",
55
+ cost_usd: 0.012,
56
+ attributes: { tenant: "acme" },
57
+ root_span_id: "0011223344556677",
58
+ }),
47
59
  );
48
60
  });
49
61
 
@@ -1 +0,0 @@
1
- export {}
@@ -1,7 +0,0 @@
1
- import { vi } from "vitest";
2
- //#region src/tests/setup.ts
3
- vi.useFakeTimers({ toFake: ["Date"] });
4
- vi.setSystemTime(/* @__PURE__ */ new Date("2023-01-01T00:00:00.000Z"));
5
- //#endregion
6
-
7
- //# sourceMappingURL=setup.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"setup.js","names":[],"sources":["../../src/tests/setup.ts"],"sourcesContent":["import { vi } from \"vitest\";\n\nvi.useFakeTimers({ toFake: [\"Date\"] });\nvi.setSystemTime(new Date(\"2023-01-01T00:00:00.000Z\"));\n"],"mappings":";;AAEA,GAAG,cAAc,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC;AACrC,GAAG,8BAAc,IAAI,KAAK,0BAA0B,CAAC"}