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
@@ -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
 
@@ -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
  }
@@ -19,6 +19,7 @@ export type CloudMessage = {
19
19
  updated_at: Date;
20
20
  format: "aui/v0" | string;
21
21
  content: ReadonlyJSONObject;
22
+ external_id?: string | null | undefined;
22
23
  };
23
24
 
24
25
  type AssistantCloudThreadMessageListQuery = {
@@ -35,6 +36,8 @@ type AssistantCloudThreadMessageCreateBody = {
35
36
  parent_id: string | null;
36
37
  format: "aui/v0" | string;
37
38
  content: ReadonlyJSONObject;
39
+ external_id?: string | undefined;
40
+ parent_external_id?: string | undefined;
38
41
  };
39
42
 
40
43
  type AssistantCloudMessageCreateResponse = {
@@ -71,6 +74,10 @@ export const decodeCloudMessage = (
71
74
  updated_at: readCloudTimestamp(message.updated_at, `${field}.updated_at`),
72
75
  format: readCloudString(message.format, `${field}.format`),
73
76
  content: readCloudJSONObject(message.content, `${field}.content`),
77
+ external_id: readCloudNullableString(
78
+ message.external_id ?? null,
79
+ `${field}.external_id`,
80
+ ),
74
81
  };
75
82
  };
76
83
 
@@ -108,7 +115,20 @@ export class AssistantCloudThreadMessages {
108
115
  const response = readCloudRecord(
109
116
  await this.cloud.makeRequest(
110
117
  `/threads/${encodeURIComponent(threadId)}/messages`,
111
- { method: "POST", body },
118
+ {
119
+ method: "POST",
120
+ body: {
121
+ parent_id: body.parent_id,
122
+ format: body.format,
123
+ content: body.content,
124
+ ...(body.external_id !== undefined
125
+ ? { external_id: body.external_id }
126
+ : undefined),
127
+ ...(body.parent_external_id !== undefined
128
+ ? { parent_external_id: body.parent_external_id }
129
+ : undefined),
130
+ },
131
+ },
112
132
  ),
113
133
  "thread message create response",
114
134
  );
@@ -0,0 +1,79 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type { AssistantCloud } from "./AssistantCloud";
3
+ import type { AssistantCloudAPI } from "./AssistantCloudAPI";
4
+ import { AssistantCloudThreadMessages } from "./AssistantCloudThreadMessages";
5
+ import { CloudMessagePersistence } from "./CloudMessagePersistence";
6
+
7
+ describe("backend transcript cloud client", () => {
8
+ it("sends external ids only when supplied", async () => {
9
+ const makeRequest = vi.fn().mockResolvedValue({ message_id: "cloud-1" });
10
+ const messages = new AssistantCloudThreadMessages({
11
+ makeRequest,
12
+ } as unknown as AssistantCloudAPI);
13
+ const body = { parent_id: null, format: "aui/v0", content: {} };
14
+
15
+ await messages.create("thread-1", body);
16
+ expect(makeRequest).toHaveBeenLastCalledWith("/threads/thread-1/messages", {
17
+ method: "POST",
18
+ body,
19
+ });
20
+
21
+ await messages.create("thread-1", {
22
+ ...body,
23
+ external_id: "backend-1",
24
+ parent_external_id: "backend-0",
25
+ });
26
+ expect(makeRequest).toHaveBeenLastCalledWith("/threads/thread-1/messages", {
27
+ method: "POST",
28
+ body: {
29
+ ...body,
30
+ external_id: "backend-1",
31
+ parent_external_id: "backend-0",
32
+ },
33
+ });
34
+ });
35
+
36
+ it("decodes external ids and treats an absent field as null", async () => {
37
+ const makeRequest = vi.fn().mockResolvedValue({
38
+ messages: [
39
+ {
40
+ id: "cloud-1",
41
+ parent_id: null,
42
+ height: 0,
43
+ created_at: "2026-01-01T00:00:00.000Z",
44
+ updated_at: "2026-01-01T00:00:00.000Z",
45
+ format: "aui/v0",
46
+ content: {},
47
+ external_id: "backend-1",
48
+ },
49
+ {
50
+ id: "cloud-2",
51
+ parent_id: "cloud-1",
52
+ height: 1,
53
+ created_at: "2026-01-01T00:00:00.000Z",
54
+ updated_at: "2026-01-01T00:00:00.000Z",
55
+ format: "aui/v0",
56
+ content: {},
57
+ },
58
+ ],
59
+ });
60
+ const messages = new AssistantCloudThreadMessages({
61
+ makeRequest,
62
+ } as unknown as AssistantCloudAPI);
63
+
64
+ const result = await messages.list("thread-1");
65
+ expect(result.messages.map((message) => message.external_id)).toEqual([
66
+ "backend-1",
67
+ null,
68
+ ]);
69
+ });
70
+
71
+ it("records a resolved local to cloud message mapping", async () => {
72
+ const persistence = new CloudMessagePersistence({} as AssistantCloud);
73
+ persistence.record("backend-1", "cloud-1");
74
+
75
+ expect(persistence.isPersisted("backend-1")).toBe(true);
76
+ expect(await persistence.getRemoteId("backend-1")).toBe("cloud-1");
77
+ expect(persistence.getResolvedRemoteId("backend-1")).toBe("cloud-1");
78
+ });
79
+ });
@@ -13,13 +13,14 @@ export type EngagementEventIds = Pick<
13
13
  * Turns the ids an integration knows (its own thread and message ids) into the
14
14
  * ids the cloud stores. A send is the one event that may create the remote
15
15
  * thread, so it asks for `awaitThread`; every other event reads the ids that
16
- * already exist.
16
+ * already exist. Returning nothing declines the event: the thread is not one
17
+ * the integration knows, so there is nothing to attribute it to.
17
18
  */
18
19
  export type EngagementIdResolver = (
19
20
  threadId: string,
20
21
  messageId: string | undefined,
21
22
  options: { awaitThread: boolean },
22
- ) => EngagementEventIds | Promise<EngagementEventIds>;
23
+ ) => EngagementEventIds | undefined | Promise<EngagementEventIds | undefined>;
23
24
 
24
25
  type EngagementEventInit = Pick<AssistantCloudEvent, "value" | "props">;
25
26
 
@@ -86,9 +87,13 @@ export class CloudEngagementReporter {
86
87
  public runStopped(threadId: string): void {
87
88
  const startedAt = this.runStartedAt.get(threadId);
88
89
  if (startedAt === undefined) return;
90
+ const stoppedAt = Date.now();
89
91
  this.runStartedAt.delete(threadId);
92
+ // A stopped run is a run that ended, so the next send measures its idle
93
+ // time from here rather than from the last run allowed to finish.
94
+ remember(this.runEndedAt, threadId, stoppedAt);
90
95
  this.track("run_stopped", threadId, undefined, {
91
- value: Math.max(0, Date.now() - startedAt),
96
+ value: Math.max(0, stoppedAt - startedAt),
92
97
  });
93
98
  }
94
99
 
@@ -223,6 +228,7 @@ export class CloudEngagementReporter {
223
228
  void Promise.resolve()
224
229
  .then(() => this.resolveIds(threadId, messageId, options))
225
230
  .then((ids) => {
231
+ if (!ids) return;
226
232
  if (kind === "thread_switched" && !ids.thread_id) return;
227
233
  this.getCloud().events.track({ kind, ...init, ...ids });
228
234
  })
@@ -111,6 +111,10 @@ export class CloudMessagePersistence {
111
111
  return typeof entry === "string" ? entry : undefined;
112
112
  }
113
113
 
114
+ record(localId: string, remoteId: string): void {
115
+ this.idMapping.set(localId, remoteId);
116
+ }
117
+
114
118
  /**
115
119
  * Load messages from the cloud and populate the ID mapping.
116
120
  *
@@ -79,7 +79,7 @@ describe("extractAISDKRunTelemetry", () => {
79
79
  {},
80
80
  ],
81
81
  samplingCalls: { call_2: [{ duration_ms: 20 }] },
82
- modelId: "gpt-5.6-luna",
82
+ modelId: "gpt-6-luna",
83
83
  },
84
84
  );
85
85
 
@@ -119,7 +119,7 @@ describe("extractAISDKRunTelemetry", () => {
119
119
  totalSteps: 3,
120
120
  outputText: "Cold.",
121
121
  usage: { inputTokens: 10, outputTokens: 5 },
122
- modelId: "gpt-5.6-luna",
122
+ modelId: "gpt-6-luna",
123
123
  metadata: message.metadata,
124
124
  });
125
125
  });