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
@@ -1,32 +1,29 @@
1
- import { ReadableSpan, SpanExporter, SpanProcessor } from "@opentelemetry/sdk-trace-base";
2
- //#region src/telemetry/index.d.ts
3
- type AssistantCloudTraceExportOptions = {
4
- apiKey: string;
5
- baseUrl?: string;
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
- filter?: (span: ReadableSpan) => boolean;
7
+ export type AssistantCloudSpanProcessorOptions = {
8
+ filter?: (span: ReadableSpan) => boolean;
10
9
  };
11
- declare function assistantCloudTraceExportOptions({ apiKey, baseUrl, headers }: AssistantCloudTraceExportOptions): {
12
- url: string;
13
- headers: Record<string, string>;
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
- traceId?: string;
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
- type: string;
20
+ export declare function withAssistantCloudTraceMetadata<Part extends {
21
+ type: string;
23
22
  } = {
24
- type: string;
23
+ type: string;
25
24
  }>(messageMetadata?: (options: {
26
- part: Part;
25
+ part: Part;
27
26
  }) => Record<string, unknown> | undefined): (options: {
28
- part: Part;
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","names":[],"sources":["../../src/telemetry/index.ts"],"mappings":";;KAYY;EACV;EACA;EACA,UAAU;;KAGA;EACV,UAAU,MAAM;;iBAGF,mCACd,QACA,SACA,WACC;EACD;EACA,SAAS;;iBAmBK,kCACd,SAAS,mCACR;iBAIa,qBAAqB,MAAM;iBAO3B,kCACd,UAAU,cACV,UAAS,qCACR;iBAca;EAAiC;;iBAOjC,gCACd;EAAe;;EAAmB;GAElC,mBAAmB;EACjB,MAAM;MACF,uCACJ;EAAW,MAAM;MAAW"}
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
@@ -1,5 +1,2 @@
1
- //#region src/version.d.ts
2
- declare const ASSISTANT_CLOUD_VERSION: string;
3
- //#endregion
4
- export { ASSISTANT_CLOUD_VERSION };
1
+ export declare const ASSISTANT_CLOUD_VERSION: string;
5
2
  //# sourceMappingURL=version.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"version.d.ts","names":[],"sources":["../src/version.ts"],"mappings":";cAAa"}
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
@@ -1,5 +1,5 @@
1
1
  //#region src/version.ts
2
- const ASSISTANT_CLOUD_VERSION = "0.2.1";
2
+ const ASSISTANT_CLOUD_VERSION = "0.2.3";
3
3
  //#endregion
4
4
  export { ASSISTANT_CLOUD_VERSION };
5
5
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assistant-cloud",
3
- "version": "0.2.1",
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.43"
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.27",
62
- "@opentelemetry/api": "^1.9.0",
63
- "@opentelemetry/exporter-trace-otlp-http": "^0.205.0",
64
- "@opentelemetry/sdk-trace-base": "^2.1.0",
65
- "@types/node": "^26.4.1",
66
- "ai": "^7.0.93",
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.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",
@@ -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 { AssistantCloudEvents } from "./AssistantCloudEvents";
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
- this.telemetry =
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);
@@ -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.flush);
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.flush);
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.flush();
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.flush();
111
+ void this.flushBestEffort();
91
112
  }
92
113
  };
93
114
 
94
- private flush = async (): Promise<void> => {
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.buffer = [];
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
- try {
128
- await this.cloud.makeRequest("/events", {
129
- method: "POST",
130
- body: { events },
131
- keepalive: true,
132
- });
133
- } catch {}
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?: "aborted" | "disconnected" | "length" | "content_filter";
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
- assistant_id: assistantId,
76
- response_format: "vercel-ai-data-stream/v1",
77
- thread_id: "unstable_todo",
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
  }