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
|
@@ -1,32 +1,29 @@
|
|
|
1
|
-
import { ReadableSpan, SpanExporter, SpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
headers?: Record<string, string>;
|
|
1
|
+
import { type ReadableSpan, type SpanExporter, type SpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
2
|
+
export type AssistantCloudTraceExportOptions = {
|
|
3
|
+
apiKey: string;
|
|
4
|
+
baseUrl?: string;
|
|
5
|
+
headers?: Record<string, string>;
|
|
7
6
|
};
|
|
8
|
-
type AssistantCloudSpanProcessorOptions = {
|
|
9
|
-
|
|
7
|
+
export type AssistantCloudSpanProcessorOptions = {
|
|
8
|
+
filter?: (span: ReadableSpan) => boolean;
|
|
10
9
|
};
|
|
11
|
-
declare function assistantCloudTraceExportOptions({ apiKey, baseUrl, headers }: AssistantCloudTraceExportOptions): {
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
export declare function assistantCloudTraceExportOptions({ apiKey, baseUrl, headers, }: AssistantCloudTraceExportOptions): {
|
|
11
|
+
url: string;
|
|
12
|
+
headers: Record<string, string>;
|
|
14
13
|
};
|
|
15
|
-
declare function createAssistantCloudTraceExporter(options: AssistantCloudTraceExportOptions): SpanExporter;
|
|
16
|
-
declare function isAssistantCloudSpan(span: ReadableSpan): boolean;
|
|
17
|
-
declare function createAssistantCloudSpanProcessor(exporter: SpanExporter, options?: AssistantCloudSpanProcessorOptions): SpanProcessor;
|
|
18
|
-
declare function assistantCloudTraceMetadata(): {
|
|
19
|
-
|
|
14
|
+
export declare function createAssistantCloudTraceExporter(options: AssistantCloudTraceExportOptions): SpanExporter;
|
|
15
|
+
export declare function isAssistantCloudSpan(span: ReadableSpan): boolean;
|
|
16
|
+
export declare function createAssistantCloudSpanProcessor(exporter: SpanExporter, options?: AssistantCloudSpanProcessorOptions): SpanProcessor;
|
|
17
|
+
export declare function assistantCloudTraceMetadata(): {
|
|
18
|
+
traceId?: string;
|
|
20
19
|
};
|
|
21
|
-
declare function withAssistantCloudTraceMetadata<Part extends {
|
|
22
|
-
|
|
20
|
+
export declare function withAssistantCloudTraceMetadata<Part extends {
|
|
21
|
+
type: string;
|
|
23
22
|
} = {
|
|
24
|
-
|
|
23
|
+
type: string;
|
|
25
24
|
}>(messageMetadata?: (options: {
|
|
26
|
-
|
|
25
|
+
part: Part;
|
|
27
26
|
}) => Record<string, unknown> | undefined): (options: {
|
|
28
|
-
|
|
27
|
+
part: Part;
|
|
29
28
|
}) => Record<string, unknown> | undefined;
|
|
30
|
-
//#endregion
|
|
31
|
-
export { AssistantCloudSpanProcessorOptions, AssistantCloudTraceExportOptions, assistantCloudTraceExportOptions, assistantCloudTraceMetadata, createAssistantCloudSpanProcessor, createAssistantCloudTraceExporter, isAssistantCloudSpan, withAssistantCloudTraceMetadata };
|
|
32
29
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/telemetry/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,aAAa,EACnB,MAAM,+BAA+B,CAAC;AAKvC,MAAM,MAAM,gCAAgC,GAAG;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,kCAAkC,GAAG;IAC/C,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,OAAO,CAAC;CAC1C,CAAC;AAEF,wBAAgB,gCAAgC,CAAC,EAC/C,MAAM,EACN,OAAkC,EAClC,OAAO,GACR,EAAE,gCAAgC,GAAG;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC,CAUA;AAQD,wBAAgB,iCAAiC,CAC/C,OAAO,EAAE,gCAAgC,GACxC,YAAY,CAEd;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAKhE;AAED,wBAAgB,iCAAiC,CAC/C,QAAQ,EAAE,YAAY,EACtB,OAAO,GAAE,kCAAuC,GAC/C,aAAa,CAYf;AAED,wBAAgB,2BAA2B,IAAI;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAKlE;AAED,wBAAgB,+BAA+B,CAC7C,IAAI,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,EAEhD,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE;IAC1B,IAAI,EAAE,IAAI,CAAC;CACZ,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GACxC,CAAC,OAAO,EAAE;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAOlE"}
|
package/dist/version.d.ts
CHANGED
package/dist/version.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.d.ts","
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,uBAAuB,EAAE,MAGzB,CAAC"}
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "assistant-cloud",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Cloud integration for assistant-ui",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"assistant",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
],
|
|
36
36
|
"sideEffects": false,
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"assistant-stream": "^0.3.
|
|
38
|
+
"assistant-stream": "^0.3.45"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@opentelemetry/api": "^1.9.0",
|
|
@@ -58,15 +58,15 @@
|
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@assistant-ui/x-buildutils": "0.0.
|
|
62
|
-
"@opentelemetry/api": "^1.9.
|
|
63
|
-
"@opentelemetry/exporter-trace-otlp-http": "^0.
|
|
64
|
-
"@opentelemetry/sdk-trace-base": "^2.
|
|
65
|
-
"@types/node": "^26.
|
|
66
|
-
"ai": "^7.0.
|
|
61
|
+
"@assistant-ui/x-buildutils": "0.0.29",
|
|
62
|
+
"@opentelemetry/api": "^1.9.1",
|
|
63
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.222.0",
|
|
64
|
+
"@opentelemetry/sdk-trace-base": "^2.11.0",
|
|
65
|
+
"@types/node": "^26.5.1",
|
|
66
|
+
"ai": "^7.0.101",
|
|
67
67
|
"ai-v6": "npm:ai@^6.0.277",
|
|
68
68
|
"jsdom": "^30.0.1",
|
|
69
|
-
"vitest": "^5.0.
|
|
69
|
+
"vitest": "^5.0.1"
|
|
70
70
|
},
|
|
71
71
|
"publishConfig": {
|
|
72
72
|
"access": "public",
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
"scripts": {
|
|
85
85
|
"build": "aui-build",
|
|
86
86
|
"test": "pnpm run test:v7 && pnpm run test:peer-v6 && pnpm run test:types:peer-v6",
|
|
87
|
+
"typecheck": "tsc --noEmit",
|
|
87
88
|
"test:v7": "vitest run",
|
|
88
89
|
"test:peer-v6": "vitest run --config vitest.peer-v6.config.ts",
|
|
89
90
|
"test:types:peer-v6": "tsc --noEmit --project tsconfig.peer-v6.json",
|
package/src/AssistantCloud.ts
CHANGED
|
@@ -9,7 +9,10 @@ import { AssistantCloudProjects } from "./AssistantCloudProjects";
|
|
|
9
9
|
import { AssistantCloudRuns } from "./AssistantCloudRuns";
|
|
10
10
|
import { AssistantCloudThreads } from "./AssistantCloudThreads";
|
|
11
11
|
import { AssistantCloudFiles } from "./AssistantCloudFiles";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
AssistantCloudEvents,
|
|
14
|
+
clearPendingAssistantCloudEvents,
|
|
15
|
+
} from "./AssistantCloudEvents";
|
|
13
16
|
import { AssistantCloudScores } from "./AssistantCloudScores";
|
|
14
17
|
|
|
15
18
|
export class AssistantCloud {
|
|
@@ -27,7 +30,7 @@ export class AssistantCloud {
|
|
|
27
30
|
const api = new AssistantCloudAPI(config);
|
|
28
31
|
this.registerSdk = api.registerSdk;
|
|
29
32
|
const t = config.telemetry;
|
|
30
|
-
|
|
33
|
+
const telemetry =
|
|
31
34
|
t === false
|
|
32
35
|
? { enabled: false }
|
|
33
36
|
: t === true || t === undefined
|
|
@@ -36,6 +39,18 @@ export class AssistantCloud {
|
|
|
36
39
|
...t,
|
|
37
40
|
enabled: t.enabled !== false,
|
|
38
41
|
};
|
|
42
|
+
this.telemetry = new Proxy(telemetry, {
|
|
43
|
+
set: (target, property, value) => {
|
|
44
|
+
const updated = Reflect.set(target, property, value);
|
|
45
|
+
if (
|
|
46
|
+
(property === "enabled" || property === "events") &&
|
|
47
|
+
(target.enabled === false || target.events === false)
|
|
48
|
+
) {
|
|
49
|
+
clearPendingAssistantCloudEvents(this.events);
|
|
50
|
+
}
|
|
51
|
+
return updated;
|
|
52
|
+
},
|
|
53
|
+
});
|
|
39
54
|
|
|
40
55
|
this.threads = new AssistantCloudThreads(api);
|
|
41
56
|
this.projects = new AssistantCloudProjects(api);
|
package/src/AssistantCloudAPI.ts
CHANGED
|
@@ -25,6 +25,10 @@ export type AssistantCloudTelemetryConfig = {
|
|
|
25
25
|
* engagement events.
|
|
26
26
|
*/
|
|
27
27
|
events?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Stores the messages of runtimes whose backend keeps the transcript (LangGraph, LangChain, Google ADK, custom external stores), so the dashboard can show them. Defaults to `true` when telemetry is enabled. Set to `false` to keep run reports and events without storing those messages.
|
|
30
|
+
*/
|
|
31
|
+
messages?: boolean;
|
|
28
32
|
release?: string;
|
|
29
33
|
environment?: string;
|
|
30
34
|
tags?: string[];
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
import {
|
|
5
5
|
AssistantCloudEvents,
|
|
6
|
+
clearPendingAssistantCloudEvents,
|
|
6
7
|
type AssistantCloudEvent,
|
|
7
8
|
} from "./AssistantCloudEvents";
|
|
8
9
|
import type { AssistantCloudAPI } from "./AssistantCloudAPI";
|
|
@@ -84,6 +85,121 @@ describe("AssistantCloudEvents", () => {
|
|
|
84
85
|
expect(makeRequest).toHaveBeenCalledOnce();
|
|
85
86
|
});
|
|
86
87
|
|
|
88
|
+
it("retries a failed event batch with bounded backoff", async () => {
|
|
89
|
+
vi.useFakeTimers();
|
|
90
|
+
const { events, makeRequest } = createEvents();
|
|
91
|
+
makeRequest.mockRejectedValueOnce(new Error("temporary failure"));
|
|
92
|
+
|
|
93
|
+
for (let index = 0; index < 20; index++) events.track(event(index));
|
|
94
|
+
await vi.waitFor(() => expect(makeRequest).toHaveBeenCalledOnce());
|
|
95
|
+
|
|
96
|
+
await vi.advanceTimersByTimeAsync(249);
|
|
97
|
+
expect(makeRequest).toHaveBeenCalledOnce();
|
|
98
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
99
|
+
|
|
100
|
+
expect(makeRequest).toHaveBeenCalledTimes(2);
|
|
101
|
+
expect(makeRequest.mock.calls[1]).toEqual(makeRequest.mock.calls[0]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("does not retry a cleared batch", async () => {
|
|
105
|
+
vi.useFakeTimers();
|
|
106
|
+
const { events, makeRequest } = createEvents();
|
|
107
|
+
makeRequest.mockRejectedValueOnce(new Error("temporary failure"));
|
|
108
|
+
|
|
109
|
+
for (let index = 0; index < 20; index++) events.track(event(index));
|
|
110
|
+
await vi.waitFor(() => expect(makeRequest).toHaveBeenCalledOnce());
|
|
111
|
+
|
|
112
|
+
clearPendingAssistantCloudEvents(events);
|
|
113
|
+
await vi.runAllTimersAsync();
|
|
114
|
+
|
|
115
|
+
expect(makeRequest).toHaveBeenCalledOnce();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("does not let an invalidated flush consume new events", async () => {
|
|
119
|
+
vi.useFakeTimers();
|
|
120
|
+
let resolveFirstRequest!: (value: { accepted: number }) => void;
|
|
121
|
+
const { events, makeRequest } = createEvents();
|
|
122
|
+
makeRequest
|
|
123
|
+
.mockImplementationOnce(
|
|
124
|
+
() =>
|
|
125
|
+
new Promise((resolve) => {
|
|
126
|
+
resolveFirstRequest = resolve;
|
|
127
|
+
}),
|
|
128
|
+
)
|
|
129
|
+
.mockRejectedValueOnce(new Error("temporary failure"));
|
|
130
|
+
|
|
131
|
+
for (let index = 0; index < 20; index++) events.track(event(index));
|
|
132
|
+
await vi.waitFor(() => expect(makeRequest).toHaveBeenCalledOnce());
|
|
133
|
+
|
|
134
|
+
window.dispatchEvent(new Event("pagehide"));
|
|
135
|
+
clearPendingAssistantCloudEvents(events);
|
|
136
|
+
events.track(event(20));
|
|
137
|
+
resolveFirstRequest({ accepted: 20 });
|
|
138
|
+
|
|
139
|
+
await vi.waitFor(() => expect(makeRequest).toHaveBeenCalledTimes(2));
|
|
140
|
+
await vi.advanceTimersByTimeAsync(250);
|
|
141
|
+
|
|
142
|
+
expect(makeRequest).toHaveBeenCalledTimes(3);
|
|
143
|
+
expect(makeRequest.mock.calls[1]?.[1].body.events).toEqual([event(20)]);
|
|
144
|
+
expect(makeRequest.mock.calls[2]).toEqual(makeRequest.mock.calls[1]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("stops retrying an event batch after three attempts", async () => {
|
|
148
|
+
vi.useFakeTimers();
|
|
149
|
+
const { events, makeRequest } = createEvents();
|
|
150
|
+
makeRequest.mockRejectedValue(new Error("persistent failure"));
|
|
151
|
+
|
|
152
|
+
for (let index = 0; index < 20; index++) events.track(event(index));
|
|
153
|
+
await vi.waitFor(() => expect(makeRequest).toHaveBeenCalledOnce());
|
|
154
|
+
|
|
155
|
+
await vi.advanceTimersByTimeAsync(250);
|
|
156
|
+
expect(makeRequest).toHaveBeenCalledTimes(2);
|
|
157
|
+
await vi.advanceTimersByTimeAsync(999);
|
|
158
|
+
expect(makeRequest).toHaveBeenCalledTimes(2);
|
|
159
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
160
|
+
expect(makeRequest).toHaveBeenCalledTimes(3);
|
|
161
|
+
await vi.runAllTimersAsync();
|
|
162
|
+
|
|
163
|
+
expect(makeRequest).toHaveBeenCalledTimes(3);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("keeps dispose delivery best effort", async () => {
|
|
167
|
+
vi.useFakeTimers();
|
|
168
|
+
const { events, makeRequest } = createEvents();
|
|
169
|
+
makeRequest.mockRejectedValue(new Error("offline"));
|
|
170
|
+
|
|
171
|
+
events.track(event(1));
|
|
172
|
+
events.dispose();
|
|
173
|
+
await vi.waitFor(() => expect(makeRequest).toHaveBeenCalledOnce());
|
|
174
|
+
await vi.runAllTimersAsync();
|
|
175
|
+
|
|
176
|
+
expect(makeRequest).toHaveBeenCalledOnce();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it.each(["dispose", "pagehide"] as const)(
|
|
180
|
+
"interrupts retry backoff on %s and drains buffered events once",
|
|
181
|
+
async (trigger) => {
|
|
182
|
+
vi.useFakeTimers();
|
|
183
|
+
const { events, makeRequest } = createEvents();
|
|
184
|
+
makeRequest.mockRejectedValue(new Error("offline"));
|
|
185
|
+
|
|
186
|
+
for (let index = 0; index < 20; index++) events.track(event(index));
|
|
187
|
+
await vi.waitFor(() => expect(makeRequest).toHaveBeenCalledOnce());
|
|
188
|
+
events.track(event(20));
|
|
189
|
+
|
|
190
|
+
if (trigger === "dispose") {
|
|
191
|
+
events.dispose();
|
|
192
|
+
} else {
|
|
193
|
+
window.dispatchEvent(new Event("pagehide"));
|
|
194
|
+
}
|
|
195
|
+
await vi.waitFor(() => expect(makeRequest).toHaveBeenCalledTimes(2));
|
|
196
|
+
await vi.runAllTimersAsync();
|
|
197
|
+
|
|
198
|
+
expect(makeRequest).toHaveBeenCalledTimes(2);
|
|
199
|
+
expect(makeRequest.mock.calls[1]?.[1].body.events).toEqual([event(20)]);
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
|
|
87
203
|
it("flushes when the document becomes hidden", async () => {
|
|
88
204
|
const { events, makeRequest } = createEvents();
|
|
89
205
|
events.track(event(1));
|
|
@@ -146,4 +262,45 @@ describe("AssistantCloudEvents", () => {
|
|
|
146
262
|
expect(options.body.events.length).toBeLessThanOrEqual(50);
|
|
147
263
|
}
|
|
148
264
|
});
|
|
265
|
+
|
|
266
|
+
it.each([Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY])(
|
|
267
|
+
"drops props containing the non-finite number %s",
|
|
268
|
+
async (value) => {
|
|
269
|
+
const { events, makeRequest } = createEvents();
|
|
270
|
+
|
|
271
|
+
events.track({ kind: "message_sent", props: { value } });
|
|
272
|
+
events.dispose();
|
|
273
|
+
|
|
274
|
+
await vi.waitFor(() => expect(makeRequest).toHaveBeenCalledOnce());
|
|
275
|
+
expect(makeRequest).toHaveBeenCalledWith("/events", {
|
|
276
|
+
method: "POST",
|
|
277
|
+
body: { events: [{ kind: "message_sent" }] },
|
|
278
|
+
keepalive: true,
|
|
279
|
+
});
|
|
280
|
+
},
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
it("keeps finite numeric props", async () => {
|
|
284
|
+
const { events, makeRequest } = createEvents();
|
|
285
|
+
|
|
286
|
+
events.track({
|
|
287
|
+
kind: "message_sent",
|
|
288
|
+
props: { negative: -1.5, zero: 0, positive: 2.5 },
|
|
289
|
+
});
|
|
290
|
+
events.dispose();
|
|
291
|
+
|
|
292
|
+
await vi.waitFor(() => expect(makeRequest).toHaveBeenCalledOnce());
|
|
293
|
+
expect(makeRequest).toHaveBeenCalledWith("/events", {
|
|
294
|
+
method: "POST",
|
|
295
|
+
body: {
|
|
296
|
+
events: [
|
|
297
|
+
{
|
|
298
|
+
kind: "message_sent",
|
|
299
|
+
props: { negative: -1.5, zero: 0, positive: 2.5 },
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
},
|
|
303
|
+
keepalive: true,
|
|
304
|
+
});
|
|
305
|
+
});
|
|
149
306
|
});
|
|
@@ -30,11 +30,23 @@ export type AssistantCloudEvent = {
|
|
|
30
30
|
const FLUSH_SIZE = 20;
|
|
31
31
|
const MAX_BATCH_SIZE = 50;
|
|
32
32
|
const FLUSH_DELAY_MS = 2_000;
|
|
33
|
+
const RETRY_DELAYS_MS = [250, 1_000] as const;
|
|
34
|
+
const pendingClearers = new WeakMap<AssistantCloudEvents, () => void>();
|
|
35
|
+
|
|
36
|
+
export const clearPendingAssistantCloudEvents = (
|
|
37
|
+
events: AssistantCloudEvents,
|
|
38
|
+
): void => {
|
|
39
|
+
pendingClearers.get(events)?.();
|
|
40
|
+
};
|
|
33
41
|
|
|
34
42
|
export class AssistantCloudEvents {
|
|
35
43
|
private buffer: AssistantCloudEvent[] = [];
|
|
36
44
|
private timer: ReturnType<typeof setTimeout> | undefined;
|
|
37
45
|
private flushing: Promise<void> | undefined;
|
|
46
|
+
private retryTimer: ReturnType<typeof setTimeout> | undefined;
|
|
47
|
+
private resolveRetryDelay: (() => void) | undefined;
|
|
48
|
+
private bestEffortRequested = false;
|
|
49
|
+
private generation = 0;
|
|
38
50
|
|
|
39
51
|
private readonly cloud: AssistantCloudAPI;
|
|
40
52
|
private readonly isEnabled: () => boolean;
|
|
@@ -44,6 +56,7 @@ export class AssistantCloudEvents {
|
|
|
44
56
|
constructor(cloud: AssistantCloudAPI, isEnabled: () => boolean) {
|
|
45
57
|
this.cloud = cloud;
|
|
46
58
|
this.isEnabled = isEnabled;
|
|
59
|
+
pendingClearers.set(this, () => this.clearPending());
|
|
47
60
|
}
|
|
48
61
|
|
|
49
62
|
public track(event: AssistantCloudEvent): void {
|
|
@@ -52,7 +65,7 @@ export class AssistantCloudEvents {
|
|
|
52
65
|
this.listen();
|
|
53
66
|
this.buffer.push(normalizeEvent(event));
|
|
54
67
|
if (this.buffer.length >= FLUSH_SIZE) {
|
|
55
|
-
void this.flush();
|
|
68
|
+
void this.flush(true);
|
|
56
69
|
return;
|
|
57
70
|
}
|
|
58
71
|
|
|
@@ -68,55 +81,70 @@ export class AssistantCloudEvents {
|
|
|
68
81
|
return;
|
|
69
82
|
}
|
|
70
83
|
this.listening = true;
|
|
71
|
-
window.addEventListener("pagehide", this.
|
|
84
|
+
window.addEventListener("pagehide", this.flushBestEffort);
|
|
72
85
|
document.addEventListener("visibilitychange", this.onVisibilityChange);
|
|
73
86
|
}
|
|
74
87
|
|
|
75
88
|
private unlisten(): void {
|
|
76
89
|
if (!this.listening) return;
|
|
77
90
|
this.listening = false;
|
|
78
|
-
window.removeEventListener("pagehide", this.
|
|
91
|
+
window.removeEventListener("pagehide", this.flushBestEffort);
|
|
79
92
|
document.removeEventListener("visibilitychange", this.onVisibilityChange);
|
|
80
93
|
}
|
|
81
94
|
|
|
82
95
|
public dispose(): void {
|
|
83
96
|
this.unlisten();
|
|
84
97
|
this.clearFlushTimer();
|
|
85
|
-
void this.
|
|
98
|
+
void this.flushBestEffort();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private clearPending(): void {
|
|
102
|
+
this.generation++;
|
|
103
|
+
this.buffer = [];
|
|
104
|
+
this.clearFlushTimer();
|
|
105
|
+
this.interruptRetryDelay();
|
|
106
|
+
this.unlisten();
|
|
86
107
|
}
|
|
87
108
|
|
|
88
109
|
private onVisibilityChange = () => {
|
|
89
110
|
if (document.visibilityState === "hidden") {
|
|
90
|
-
void this.
|
|
111
|
+
void this.flushBestEffort();
|
|
91
112
|
}
|
|
92
113
|
};
|
|
93
114
|
|
|
94
|
-
private
|
|
115
|
+
private flushBestEffort = () => {
|
|
116
|
+
if (this.flushing) {
|
|
117
|
+
this.bestEffortRequested = true;
|
|
118
|
+
this.interruptRetryDelay();
|
|
119
|
+
}
|
|
120
|
+
return this.flush(false);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
private flush = async (retryFailures: boolean): Promise<void> => {
|
|
95
124
|
if (!this.isEnabled()) {
|
|
96
|
-
this.
|
|
97
|
-
this.clearFlushTimer();
|
|
98
|
-
this.unlisten();
|
|
125
|
+
this.clearPending();
|
|
99
126
|
return;
|
|
100
127
|
}
|
|
101
128
|
if (this.flushing) return this.flushing;
|
|
102
129
|
|
|
103
130
|
this.clearFlushTimer();
|
|
104
|
-
const task = this.flushPending();
|
|
131
|
+
const task = this.flushPending(retryFailures);
|
|
105
132
|
this.flushing = task;
|
|
106
133
|
void task.then(() => {
|
|
107
134
|
if (this.flushing !== task) return;
|
|
108
135
|
this.flushing = undefined;
|
|
136
|
+
this.bestEffortRequested = false;
|
|
109
137
|
if (this.buffer.length === 0) {
|
|
110
138
|
this.clearFlushTimer();
|
|
111
139
|
this.unlisten();
|
|
112
140
|
} else {
|
|
113
|
-
void this.flush();
|
|
141
|
+
void this.flush(true);
|
|
114
142
|
}
|
|
115
143
|
});
|
|
116
144
|
return task;
|
|
117
145
|
};
|
|
118
146
|
|
|
119
|
-
private async flushPending(): Promise<void> {
|
|
147
|
+
private async flushPending(retryFailures: boolean): Promise<void> {
|
|
120
148
|
while (this.buffer.length > 0) {
|
|
121
149
|
if (!this.isEnabled()) {
|
|
122
150
|
this.buffer = [];
|
|
@@ -124,21 +152,54 @@ export class AssistantCloudEvents {
|
|
|
124
152
|
}
|
|
125
153
|
|
|
126
154
|
const events = this.buffer.splice(0, MAX_BATCH_SIZE);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
155
|
+
const generation = this.generation;
|
|
156
|
+
for (let attempt = 0; ; attempt++) {
|
|
157
|
+
try {
|
|
158
|
+
await this.cloud.makeRequest("/events", {
|
|
159
|
+
method: "POST",
|
|
160
|
+
body: { events },
|
|
161
|
+
keepalive: true,
|
|
162
|
+
});
|
|
163
|
+
if (generation !== this.generation) return;
|
|
164
|
+
break;
|
|
165
|
+
} catch {
|
|
166
|
+
if (generation !== this.generation) return;
|
|
167
|
+
const delay =
|
|
168
|
+
retryFailures && !this.bestEffortRequested
|
|
169
|
+
? RETRY_DELAYS_MS[attempt]
|
|
170
|
+
: undefined;
|
|
171
|
+
if (delay === undefined) break;
|
|
172
|
+
await this.waitForRetry(delay);
|
|
173
|
+
if (generation !== this.generation) return;
|
|
174
|
+
if (!this.isEnabled()) return this.clearPending();
|
|
175
|
+
if (this.bestEffortRequested) break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
134
178
|
}
|
|
135
179
|
}
|
|
136
180
|
|
|
181
|
+
private waitForRetry(delay: number): Promise<void> {
|
|
182
|
+
return new Promise((resolve) => {
|
|
183
|
+
const finish = () => {
|
|
184
|
+
this.retryTimer = undefined;
|
|
185
|
+
this.resolveRetryDelay = undefined;
|
|
186
|
+
resolve();
|
|
187
|
+
};
|
|
188
|
+
this.resolveRetryDelay = finish;
|
|
189
|
+
this.retryTimer = setTimeout(finish, delay);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private interruptRetryDelay(): void {
|
|
194
|
+
if (this.retryTimer !== undefined) clearTimeout(this.retryTimer);
|
|
195
|
+
this.resolveRetryDelay?.();
|
|
196
|
+
}
|
|
197
|
+
|
|
137
198
|
private scheduleFlush() {
|
|
138
199
|
if (this.timer !== undefined) return;
|
|
139
200
|
this.timer = setTimeout(() => {
|
|
140
201
|
this.timer = undefined;
|
|
141
|
-
void this.flush();
|
|
202
|
+
void this.flush(true);
|
|
142
203
|
}, FLUSH_DELAY_MS);
|
|
143
204
|
}
|
|
144
205
|
|
|
@@ -176,6 +237,7 @@ const normalizeProps = (
|
|
|
176
237
|
entries.some(
|
|
177
238
|
([, value]) =>
|
|
178
239
|
(typeof value === "string" && value.length > 256) ||
|
|
240
|
+
(typeof value === "number" && !Number.isFinite(value)) ||
|
|
179
241
|
(typeof value !== "string" &&
|
|
180
242
|
typeof value !== "number" &&
|
|
181
243
|
typeof value !== "boolean"),
|
|
@@ -19,7 +19,18 @@ type AssistantCloudRunsStreamBody = {
|
|
|
19
19
|
export type AssistantCloudRunReport = {
|
|
20
20
|
thread_id: string;
|
|
21
21
|
status: "completed" | "incomplete" | "error";
|
|
22
|
-
outcome_type?:
|
|
22
|
+
outcome_type?:
|
|
23
|
+
| "rate_limited"
|
|
24
|
+
| "validation_failed"
|
|
25
|
+
| "provider_error"
|
|
26
|
+
| "server_error"
|
|
27
|
+
| "budget_denied"
|
|
28
|
+
| "persistence_error"
|
|
29
|
+
| "aborted"
|
|
30
|
+
| "timeout"
|
|
31
|
+
| "disconnected"
|
|
32
|
+
| "length"
|
|
33
|
+
| "content_filter";
|
|
23
34
|
message_id?: string;
|
|
24
35
|
first_token_ms?: number;
|
|
25
36
|
release?: string;
|
|
@@ -27,6 +38,7 @@ export type AssistantCloudRunReport = {
|
|
|
27
38
|
tags?: string[];
|
|
28
39
|
provider?: string;
|
|
29
40
|
trace_id?: string;
|
|
41
|
+
root_span_id?: string;
|
|
30
42
|
error_code?: string;
|
|
31
43
|
error?: string;
|
|
32
44
|
total_steps?: number;
|
|
@@ -40,15 +52,24 @@ export type AssistantCloudRunReport = {
|
|
|
40
52
|
start_ms?: number;
|
|
41
53
|
end_ms?: number;
|
|
42
54
|
finish_reason?: string;
|
|
55
|
+
input?: string;
|
|
43
56
|
}[];
|
|
44
57
|
input_tokens?: number;
|
|
45
58
|
output_tokens?: number;
|
|
46
59
|
reasoning_tokens?: number;
|
|
47
60
|
cached_input_tokens?: number;
|
|
61
|
+
cost_usd?: number;
|
|
62
|
+
cost_details?: {
|
|
63
|
+
input?: number;
|
|
64
|
+
input_cached_tokens?: number;
|
|
65
|
+
output?: number;
|
|
66
|
+
total?: number;
|
|
67
|
+
};
|
|
48
68
|
model_id?: string;
|
|
49
69
|
provider_type?: string;
|
|
50
70
|
duration_ms?: number;
|
|
51
71
|
output_text?: string;
|
|
72
|
+
attributes?: Record<string, unknown>;
|
|
52
73
|
metadata?: Record<string, unknown>;
|
|
53
74
|
};
|
|
54
75
|
|
|
@@ -62,6 +83,7 @@ export class AssistantCloudRuns {
|
|
|
62
83
|
public __internal_getAssistantOptions(assistantId: string) {
|
|
63
84
|
return {
|
|
64
85
|
api: `${this.cloud._baseUrl}/v1/runs/stream`,
|
|
86
|
+
protocol: "ui-message-stream" as const,
|
|
65
87
|
headers: async () => {
|
|
66
88
|
const headers = await this.cloud._auth.getAuthHeaders();
|
|
67
89
|
if (!headers) throw new Error("Authorization failed");
|
|
@@ -71,10 +93,18 @@ export class AssistantCloudRuns {
|
|
|
71
93
|
"Aui-Sdk": this.cloud.sdkHeader(),
|
|
72
94
|
};
|
|
73
95
|
},
|
|
74
|
-
body: {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
96
|
+
body: async (options?: { threadId?: string }) => {
|
|
97
|
+
const threadId = options?.threadId;
|
|
98
|
+
if (threadId === undefined) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
"Assistant Cloud runs need a thread; the thread list adapter has not assigned a remote id to this thread yet.",
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
assistant_id: assistantId,
|
|
105
|
+
response_format: "vercel-ai-data-stream/v1",
|
|
106
|
+
thread_id: threadId,
|
|
107
|
+
};
|
|
78
108
|
},
|
|
79
109
|
};
|
|
80
110
|
}
|