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.
- package/README.md +24 -8
- package/dist/AssistantCloud.d.ts +16 -19
- package/dist/AssistantCloud.d.ts.map +1 -1
- package/dist/AssistantCloud.js +7 -2
- package/dist/AssistantCloud.js.map +1 -1
- package/dist/AssistantCloudAPI.d.ts +74 -72
- package/dist/AssistantCloudAPI.d.ts.map +1 -1
- package/dist/AssistantCloudAPI.js.map +1 -1
- package/dist/AssistantCloudAuthStrategy.d.ts +29 -32
- package/dist/AssistantCloudAuthStrategy.d.ts.map +1 -1
- package/dist/AssistantCloudAuthTokens.d.ts +7 -9
- package/dist/AssistantCloudAuthTokens.d.ts.map +1 -1
- package/dist/AssistantCloudEvents.d.ts +35 -29
- package/dist/AssistantCloudEvents.d.ts.map +1 -1
- package/dist/AssistantCloudEvents.js +66 -17
- package/dist/AssistantCloudEvents.js.map +1 -1
- package/dist/AssistantCloudFiles.d.ts +27 -29
- package/dist/AssistantCloudFiles.d.ts.map +1 -1
- package/dist/AssistantCloudProjectThreadMessages.d.ts +11 -13
- package/dist/AssistantCloudProjectThreadMessages.d.ts.map +1 -1
- package/dist/AssistantCloudProjectThreads.d.ts +12 -14
- package/dist/AssistantCloudProjectThreads.d.ts.map +1 -1
- package/dist/AssistantCloudProjects.d.ts +4 -7
- package/dist/AssistantCloudProjects.d.ts.map +1 -1
- package/dist/AssistantCloudRuns.d.ts +67 -56
- package/dist/AssistantCloudRuns.d.ts.map +1 -1
- package/dist/AssistantCloudRuns.js +9 -4
- package/dist/AssistantCloudRuns.js.map +1 -1
- package/dist/AssistantCloudScores.d.ts +20 -23
- package/dist/AssistantCloudScores.d.ts.map +1 -1
- package/dist/AssistantCloudThreadMessages.d.ts +41 -38
- package/dist/AssistantCloudThreadMessages.d.ts.map +1 -1
- package/dist/AssistantCloudThreadMessages.js +16 -4
- package/dist/AssistantCloudThreadMessages.js.map +1 -1
- package/dist/AssistantCloudThreads.d.ts +42 -44
- package/dist/AssistantCloudThreads.d.ts.map +1 -1
- package/dist/CloudEngagementReporter.d.ts +56 -58
- package/dist/CloudEngagementReporter.d.ts.map +1 -1
- package/dist/CloudEngagementReporter.js +4 -1
- package/dist/CloudEngagementReporter.js.map +1 -1
- package/dist/CloudMessagePersistence.d.ts +56 -58
- package/dist/CloudMessagePersistence.d.ts.map +1 -1
- package/dist/CloudMessagePersistence.js +3 -0
- package/dist/CloudMessagePersistence.js.map +1 -1
- package/dist/CloudRunReporter.d.ts +10 -13
- package/dist/CloudRunReporter.d.ts.map +1 -1
- package/dist/CloudRunReporter.js +13 -4
- package/dist/CloudRunReporter.js.map +1 -1
- package/dist/FormattedCloudPersistence.d.ts +36 -39
- package/dist/FormattedCloudPersistence.d.ts.map +1 -1
- package/dist/ai-sdk/index.d.ts +14 -17
- package/dist/ai-sdk/index.d.ts.map +1 -1
- package/dist/cloudResponse.d.ts +14 -17
- package/dist/cloudResponse.d.ts.map +1 -1
- package/dist/generateThreadTitle.d.ts +9 -12
- package/dist/generateThreadTitle.d.ts.map +1 -1
- package/dist/index.d.ts +18 -17
- package/dist/index.d.ts.map +1 -0
- package/dist/instrumentMcpSampling.d.ts +36 -39
- package/dist/instrumentMcpSampling.d.ts.map +1 -1
- package/dist/runTelemetry.d.ts +101 -94
- package/dist/runTelemetry.d.ts.map +1 -1
- package/dist/runTelemetry.js +57 -15
- package/dist/runTelemetry.js.map +1 -1
- package/dist/telemetry/index.d.ts +20 -23
- package/dist/telemetry/index.d.ts.map +1 -1
- package/dist/version.d.ts +1 -4
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +10 -9
- package/src/AssistantCloud.ts +17 -2
- package/src/AssistantCloudAPI.ts +4 -0
- package/src/AssistantCloudEvents.test.ts +157 -0
- package/src/AssistantCloudEvents.ts +82 -20
- package/src/AssistantCloudRuns.ts +35 -5
- package/src/AssistantCloudThreadMessages.test.ts +53 -2
- package/src/AssistantCloudThreadMessages.ts +34 -2
- package/src/BackendTranscriptCopy.test.ts +79 -0
- package/src/CloudEngagementReporter.ts +9 -3
- package/src/CloudMessagePersistence.ts +4 -0
- package/src/CloudRunReporter.ts +16 -4
- package/src/ai-sdk/index.test.ts +2 -2
- package/src/runTelemetry.test.ts +226 -2
- package/src/runTelemetry.ts +121 -17
- package/src/tests/AssistantCloud.test.ts +22 -0
- package/src/tests/AssistantCloudRuns.test.ts +28 -0
- package/src/tests/CloudEngagementReporter.test.ts +45 -0
- package/src/tests/CloudRunReporter.test.ts +49 -2
- package/dist/tests/setup.d.ts +0 -1
- package/dist/tests/setup.js +0 -7
- package/dist/tests/setup.js.map +0 -1
package/src/runTelemetry.ts
CHANGED
|
@@ -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+/]{
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
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?:
|
|
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);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
import type { AssistantCloud } from "../AssistantCloud";
|
|
3
|
+
import { CloudAPIError } from "../AssistantCloudAPI";
|
|
3
4
|
import { CloudRunReporter } from "../CloudRunReporter";
|
|
5
|
+
import { CloudResponseError } from "../cloudResponse";
|
|
4
6
|
|
|
5
7
|
const createCloud = (telemetry: AssistantCloud["telemetry"]) => {
|
|
6
8
|
const report = vi.fn().mockResolvedValue({ run_id: "run_1" });
|
|
@@ -19,7 +21,13 @@ describe("CloudRunReporter", () => {
|
|
|
19
21
|
});
|
|
20
22
|
|
|
21
23
|
it("stamps the cloud's environment, release and tags and applies beforeReport last", async () => {
|
|
22
|
-
const beforeReport = vi.fn((report) => ({
|
|
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
|
+
}));
|
|
23
31
|
const { cloud, report } = createCloud({
|
|
24
32
|
enabled: true,
|
|
25
33
|
environment: "production",
|
|
@@ -41,7 +49,13 @@ describe("CloudRunReporter", () => {
|
|
|
41
49
|
}),
|
|
42
50
|
);
|
|
43
51
|
expect(report).toHaveBeenCalledWith(
|
|
44
|
-
expect.objectContaining({
|
|
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
|
+
}),
|
|
45
59
|
);
|
|
46
60
|
});
|
|
47
61
|
|
|
@@ -69,6 +83,39 @@ describe("CloudRunReporter", () => {
|
|
|
69
83
|
expect(report).toHaveBeenCalledTimes(3);
|
|
70
84
|
});
|
|
71
85
|
|
|
86
|
+
it("allows a keyed run to retry after rate limiting", async () => {
|
|
87
|
+
const { cloud, report } = createCloud({ enabled: true });
|
|
88
|
+
report.mockRejectedValueOnce(new CloudAPIError("rate limited", 429));
|
|
89
|
+
const reporter = new CloudRunReporter(cloud);
|
|
90
|
+
|
|
91
|
+
await reporter.report({ threadId: "t", status: "completed" }, "t:m");
|
|
92
|
+
await reporter.report({ threadId: "t", status: "completed" }, "t:m");
|
|
93
|
+
await reporter.report({ threadId: "t", status: "completed" }, "t:m");
|
|
94
|
+
|
|
95
|
+
expect(report).toHaveBeenCalledTimes(2);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it.each([
|
|
99
|
+
["a transport failure", new Error("offline")],
|
|
100
|
+
["a request timeout", new CloudAPIError("timeout", 408)],
|
|
101
|
+
["a server error", new CloudAPIError("unavailable", 503)],
|
|
102
|
+
[
|
|
103
|
+
"a successful response with an invalid body",
|
|
104
|
+
new CloudResponseError("invalid response"),
|
|
105
|
+
],
|
|
106
|
+
["a successful response with invalid JSON", new SyntaxError("invalid")],
|
|
107
|
+
["a permanent client error", new CloudAPIError("invalid report", 400)],
|
|
108
|
+
])("does not retry after %s", async (_name, error) => {
|
|
109
|
+
const { cloud, report } = createCloud({ enabled: true });
|
|
110
|
+
report.mockRejectedValueOnce(error);
|
|
111
|
+
const reporter = new CloudRunReporter(cloud);
|
|
112
|
+
|
|
113
|
+
await reporter.report({ threadId: "t", status: "completed" }, "t:m");
|
|
114
|
+
await reporter.report({ threadId: "t", status: "completed" }, "t:m");
|
|
115
|
+
|
|
116
|
+
expect(report).toHaveBeenCalledOnce();
|
|
117
|
+
});
|
|
118
|
+
|
|
72
119
|
it("swallows a failed send and reads the cloud through a getter", async () => {
|
|
73
120
|
const { cloud } = createCloud({ enabled: true });
|
|
74
121
|
(cloud.runs.report as ReturnType<typeof vi.fn>).mockRejectedValue(
|
package/dist/tests/setup.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {}
|
package/dist/tests/setup.js
DELETED
package/dist/tests/setup.js.map
DELETED
|
@@ -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"}
|