assistant-cloud 0.1.42 → 0.2.0

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 (81) hide show
  1. package/README.md +40 -0
  2. package/dist/AssistantCloud.d.ts +4 -0
  3. package/dist/AssistantCloud.d.ts.map +1 -1
  4. package/dist/AssistantCloud.js +11 -5
  5. package/dist/AssistantCloud.js.map +1 -1
  6. package/dist/AssistantCloudAPI.d.ts +17 -1
  7. package/dist/AssistantCloudAPI.d.ts.map +1 -1
  8. package/dist/AssistantCloudAPI.js +20 -9
  9. package/dist/AssistantCloudAPI.js.map +1 -1
  10. package/dist/AssistantCloudAuthStrategy.d.ts +4 -1
  11. package/dist/AssistantCloudAuthStrategy.d.ts.map +1 -1
  12. package/dist/AssistantCloudAuthStrategy.js +79 -18
  13. package/dist/AssistantCloudAuthStrategy.js.map +1 -1
  14. package/dist/AssistantCloudEvents.d.ts +32 -0
  15. package/dist/AssistantCloudEvents.d.ts.map +1 -0
  16. package/dist/AssistantCloudEvents.js +115 -0
  17. package/dist/AssistantCloudEvents.js.map +1 -0
  18. package/dist/AssistantCloudFiles.d.ts +12 -1
  19. package/dist/AssistantCloudFiles.d.ts.map +1 -1
  20. package/dist/AssistantCloudFiles.js +13 -1
  21. package/dist/AssistantCloudFiles.js.map +1 -1
  22. package/dist/AssistantCloudRuns.d.ts +11 -0
  23. package/dist/AssistantCloudRuns.d.ts.map +1 -1
  24. package/dist/AssistantCloudRuns.js.map +1 -1
  25. package/dist/AssistantCloudScores.d.ts +27 -0
  26. package/dist/AssistantCloudScores.d.ts.map +1 -0
  27. package/dist/AssistantCloudScores.js +30 -0
  28. package/dist/AssistantCloudScores.js.map +1 -0
  29. package/dist/AssistantCloudThreadMessages.d.ts +11 -1
  30. package/dist/AssistantCloudThreadMessages.d.ts.map +1 -1
  31. package/dist/AssistantCloudThreadMessages.js +12 -1
  32. package/dist/AssistantCloudThreadMessages.js.map +1 -1
  33. package/dist/AssistantCloudThreads.d.ts +8 -0
  34. package/dist/AssistantCloudThreads.d.ts.map +1 -1
  35. package/dist/AssistantCloudThreads.js +9 -1
  36. package/dist/AssistantCloudThreads.js.map +1 -1
  37. package/dist/CloudMessagePersistence.d.ts +4 -0
  38. package/dist/CloudMessagePersistence.d.ts.map +1 -1
  39. package/dist/CloudMessagePersistence.js +53 -18
  40. package/dist/CloudMessagePersistence.js.map +1 -1
  41. package/dist/cloudResponse.d.ts +4 -1
  42. package/dist/cloudResponse.d.ts.map +1 -1
  43. package/dist/cloudResponse.js +14 -1
  44. package/dist/cloudResponse.js.map +1 -1
  45. package/dist/index.d.ts +7 -3
  46. package/dist/index.js +5 -2
  47. package/dist/runTelemetry.d.ts +79 -9
  48. package/dist/runTelemetry.d.ts.map +1 -1
  49. package/dist/runTelemetry.js +152 -6
  50. package/dist/runTelemetry.js.map +1 -1
  51. package/dist/telemetry/index.d.ts +32 -0
  52. package/dist/telemetry/index.d.ts.map +1 -0
  53. package/dist/telemetry/index.js +62 -0
  54. package/dist/telemetry/index.js.map +1 -0
  55. package/package.json +28 -4
  56. package/src/AssistantCloud.ts +20 -8
  57. package/src/AssistantCloudAPI.ts +42 -10
  58. package/src/AssistantCloudAuthStrategy.ts +153 -46
  59. package/src/AssistantCloudEvents.test.ts +124 -0
  60. package/src/AssistantCloudEvents.ts +185 -0
  61. package/src/AssistantCloudFiles.test.ts +59 -0
  62. package/src/AssistantCloudFiles.ts +39 -0
  63. package/src/AssistantCloudRuns.ts +12 -1
  64. package/src/AssistantCloudScores.test.ts +113 -0
  65. package/src/AssistantCloudScores.ts +61 -0
  66. package/src/AssistantCloudThreadMessages.test.ts +51 -0
  67. package/src/AssistantCloudThreadMessages.ts +33 -0
  68. package/src/AssistantCloudThreads.test.ts +21 -0
  69. package/src/AssistantCloudThreads.ts +21 -0
  70. package/src/CloudMessagePersistence.ts +65 -29
  71. package/src/cloudResponse.test.ts +34 -1
  72. package/src/cloudResponse.ts +30 -0
  73. package/src/index.ts +24 -1
  74. package/src/runTelemetry.test.ts +366 -0
  75. package/src/runTelemetry.ts +271 -16
  76. package/src/telemetry/index.test.ts +252 -0
  77. package/src/telemetry/index.ts +98 -0
  78. package/src/tests/AssistantCloud.test.ts +22 -0
  79. package/src/tests/AssistantCloudAPI.test.ts +44 -4
  80. package/src/tests/AssistantCloudAuthStrategy.test.ts +354 -11
  81. package/src/tests/CloudMessagePersistence.test.ts +208 -0
@@ -2,6 +2,7 @@ import type { ReadonlyJSONObject } from "assistant-stream/utils";
2
2
  import type { AssistantCloudAPI } from "./AssistantCloudAPI";
3
3
  import {
4
4
  readCloudArray,
5
+ readCloudEnum,
5
6
  readCloudInteger,
6
7
  readCloudJSONObject,
7
8
  readCloudNullableString,
@@ -22,6 +23,8 @@ export type CloudMessage = {
22
23
 
23
24
  type AssistantCloudThreadMessageListQuery = {
24
25
  format?: string;
26
+ limit?: number;
27
+ after?: string;
25
28
  };
26
29
 
27
30
  type AssistantCloudThreadMessageListResponse = {
@@ -42,6 +45,17 @@ type AssistantCloudThreadMessageUpdateBody = {
42
45
  content: ReadonlyJSONObject;
43
46
  };
44
47
 
48
+ const MESSAGE_FEEDBACK_TYPES = ["positive", "negative"] as const;
49
+
50
+ export type AssistantCloudThreadMessageFeedbackBody = {
51
+ type: "positive" | "negative";
52
+ };
53
+
54
+ export type AssistantCloudThreadMessageFeedbackResponse = {
55
+ feedback_id: string;
56
+ type: "positive" | "negative";
57
+ };
58
+
45
59
  export const decodeCloudMessage = (
46
60
  value: unknown,
47
61
  field: string,
@@ -112,4 +126,23 @@ export class AssistantCloudThreadMessages {
112
126
  { method: "PUT", body },
113
127
  );
114
128
  }
129
+
130
+ public async feedback(
131
+ threadId: string,
132
+ messageId: string,
133
+ body: AssistantCloudThreadMessageFeedbackBody,
134
+ ): Promise<AssistantCloudThreadMessageFeedbackResponse> {
135
+ const response = readCloudRecord(
136
+ await this.cloud.makeRequest(
137
+ `/threads/${encodeURIComponent(threadId)}/messages/${encodeURIComponent(messageId)}/feedback`,
138
+ { method: "POST", body },
139
+ ),
140
+ "thread message feedback response",
141
+ );
142
+
143
+ return {
144
+ feedback_id: readCloudString(response.feedback_id, "feedback_id"),
145
+ type: readCloudEnum(response.type, "type", MESSAGE_FEEDBACK_TYPES),
146
+ };
147
+ }
115
148
  }
@@ -39,6 +39,27 @@ describe("AssistantCloudThreads responses", () => {
39
39
  );
40
40
  });
41
41
 
42
+ it("claims anonymous threads and validates the moved count", async () => {
43
+ const { threads, makeRequest } = createCloudThreads();
44
+ makeRequest.mockResolvedValueOnce({ moved: 2 });
45
+
46
+ await expect(
47
+ threads.claim({ refresh_token: "anonymous-refresh" }),
48
+ ).resolves.toEqual({ moved: 2 });
49
+ expect(makeRequest).toHaveBeenLastCalledWith("/threads/claim", {
50
+ method: "POST",
51
+ body: { refresh_token: "anonymous-refresh" },
52
+ });
53
+
54
+ makeRequest.mockResolvedValueOnce({ moved: 1.5 });
55
+
56
+ await expect(
57
+ threads.claim({ refresh_token: "anonymous-refresh" }),
58
+ ).rejects.toThrow(
59
+ 'Invalid Assistant Cloud response for "moved": expected an integer',
60
+ );
61
+ });
62
+
42
63
  it("forwards both archive filter values", async () => {
43
64
  const { threads, makeRequest } = createCloudThreads();
44
65
  makeRequest.mockResolvedValue({ threads: [] });
@@ -3,6 +3,7 @@ import { AssistantCloudThreadMessages } from "./AssistantCloudThreadMessages";
3
3
  import {
4
4
  readCloudArray,
5
5
  readCloudBoolean,
6
+ readCloudInteger,
6
7
  readCloudNullableString,
7
8
  readCloudRecord,
8
9
  readCloudString,
@@ -43,6 +44,14 @@ type AssistantCloudThreadsCreateResponse = {
43
44
  thread_id: string;
44
45
  };
45
46
 
47
+ type AssistantCloudThreadsClaimBody = {
48
+ refresh_token: string;
49
+ };
50
+
51
+ type AssistantCloudThreadsClaimResponse = {
52
+ moved: number;
53
+ };
54
+
46
55
  type AssistantCloudThreadsUpdateBody = {
47
56
  title?: string | undefined;
48
57
  last_message_at?: Date | undefined;
@@ -135,6 +144,18 @@ export class AssistantCloudThreads {
135
144
  });
136
145
  }
137
146
 
147
+ /** Moves every thread of the anonymous identity behind `refresh_token` into the caller's workspace. */
148
+ public async claim(
149
+ body: AssistantCloudThreadsClaimBody,
150
+ ): Promise<AssistantCloudThreadsClaimResponse> {
151
+ const response = readCloudRecord(
152
+ await this.cloud.makeRequest("/threads/claim", { method: "POST", body }),
153
+ "thread claim response",
154
+ );
155
+
156
+ return { moved: readCloudInteger(response.moved, "moved") };
157
+ }
158
+
138
159
  public async delete(threadId: string): Promise<void> {
139
160
  return this.cloud.makeRequest(`/threads/${encodeURIComponent(threadId)}`, {
140
161
  method: "DELETE",
@@ -1,5 +1,8 @@
1
1
  import type { ReadonlyJSONObject } from "assistant-stream/utils";
2
2
  import type { AssistantCloud } from "./AssistantCloud";
3
+ import type { CloudMessage } from "./AssistantCloudThreadMessages";
4
+
5
+ const CLOUD_MESSAGE_PAGE_SIZE = 200;
3
6
 
4
7
  /**
5
8
  * Shared persistence logic for cloud message storage.
@@ -13,7 +16,7 @@ import type { AssistantCloud } from "./AssistantCloud";
13
16
  * to get its remote ID before creating B.
14
17
  */
15
18
  export class CloudMessagePersistence {
16
- private idMapping: Record<string, string | Promise<string>> = {};
19
+ private idMapping = new Map<string, string | Promise<string>>();
17
20
  private getCloud: () => AssistantCloud;
18
21
 
19
22
  constructor(cloud: AssistantCloud);
@@ -39,32 +42,37 @@ export class CloudMessagePersistence {
39
42
  content: ReadonlyJSONObject,
40
43
  ): Promise<void> {
41
44
  const cloud = this.getCloud();
42
- // Resolve parent's remote ID if it exists (may be a promise if concurrent)
43
- const resolvedParentId = parentId
44
- ? ((await this.idMapping[parentId]) ?? parentId)
45
- : null;
45
+ const existing = this.idMapping.get(messageId);
46
+ if (existing instanceof Promise) {
47
+ await existing;
48
+ return;
49
+ }
46
50
 
47
- const task = cloud.threads.messages
48
- .create(threadId, {
51
+ const task = (async () => {
52
+ const parentEntry = parentId ? this.idMapping.get(parentId) : undefined;
53
+ const resolvedParentId = parentId
54
+ ? ((await parentEntry) ?? parentId)
55
+ : null;
56
+ const { message_id } = await cloud.threads.messages.create(threadId, {
49
57
  parent_id: resolvedParentId,
50
58
  format,
51
59
  content,
52
- })
53
- .then(({ message_id }) => {
54
- this.idMapping[messageId] = message_id;
55
- return message_id;
56
- })
57
- .catch((err) => {
58
- // Only delete if we're still the active task (avoids clobbering a retry)
59
- if (this.idMapping[messageId] === task) {
60
- delete this.idMapping[messageId];
61
- }
62
- throw err;
63
60
  });
61
+ return message_id;
62
+ })();
64
63
 
65
- // Store the promise immediately so concurrent appends can await it
66
- this.idMapping[messageId] = task;
67
- return task.then(() => {});
64
+ this.idMapping.set(messageId, task);
65
+ try {
66
+ const remoteId = await task;
67
+ if (this.idMapping.get(messageId) === task) {
68
+ this.idMapping.set(messageId, remoteId);
69
+ }
70
+ } catch (err) {
71
+ if (this.idMapping.get(messageId) === task) {
72
+ this.idMapping.delete(messageId);
73
+ }
74
+ throw err;
75
+ }
68
76
  }
69
77
 
70
78
  /**
@@ -91,7 +99,7 @@ export class CloudMessagePersistence {
91
99
  * Check if a message has been persisted (or is currently being persisted).
92
100
  */
93
101
  isPersisted(messageId: string): boolean {
94
- return messageId in this.idMapping;
102
+ return this.idMapping.has(messageId);
95
103
  }
96
104
 
97
105
  /**
@@ -99,14 +107,22 @@ export class CloudMessagePersistence {
99
107
  * Returns undefined if not persisted.
100
108
  */
101
109
  async getRemoteId(messageId: string): Promise<string | undefined> {
102
- const entry = this.idMapping[messageId];
110
+ const entry = this.idMapping.get(messageId);
103
111
  if (!entry) return undefined;
104
112
  return entry;
105
113
  }
106
114
 
115
+ getResolvedRemoteId(messageId: string): string | undefined {
116
+ const entry = this.idMapping.get(messageId);
117
+ return typeof entry === "string" ? entry : undefined;
118
+ }
119
+
107
120
  /**
108
121
  * Load messages from the cloud and populate the ID mapping.
109
122
  *
123
+ * The list endpoint caps a response at 200 rows, so pages are followed by
124
+ * message ID cursor until a short page and concatenated in server order.
125
+ *
110
126
  * The ID mapping is populated so that `isPersisted()` returns true for
111
127
  * loaded messages, preventing re-persistence of already-stored messages.
112
128
  *
@@ -116,13 +132,33 @@ export class CloudMessagePersistence {
116
132
  */
117
133
  async load(threadId: string, format?: string) {
118
134
  const cloud = this.getCloud();
119
- const { messages } = await cloud.threads.messages.list(
120
- threadId,
121
- format ? { format } : undefined,
122
- );
135
+ const messages: CloudMessage[] = [];
136
+ const seen = new Set<string>();
137
+ let after: string | undefined;
138
+
139
+ while (true) {
140
+ const page = await cloud.threads.messages.list(threadId, {
141
+ ...(format ? { format } : undefined),
142
+ limit: CLOUD_MESSAGE_PAGE_SIZE,
143
+ ...(after ? { after } : undefined),
144
+ });
145
+ const last = page.messages.at(-1);
146
+ if (!last) break;
147
+
148
+ // A cursor the server cannot resolve drops the keyset filter and replays
149
+ // an earlier page, so already-seen rows end the walk instead of repeating.
150
+ const fresh = page.messages.filter((m) => !seen.has(m.id));
151
+ if (fresh.length === 0) break;
152
+ for (const m of fresh) seen.add(m.id);
153
+
154
+ messages.push(...fresh);
155
+ if (page.messages.length < CLOUD_MESSAGE_PAGE_SIZE) break;
156
+ after = last.id;
157
+ }
158
+
123
159
  // Populate ID mapping so isPersisted() recognizes loaded messages
124
160
  for (const m of messages) {
125
- this.idMapping[m.id] = m.id;
161
+ this.idMapping.set(m.id, m.id);
126
162
  }
127
163
  return messages;
128
164
  }
@@ -131,6 +167,6 @@ export class CloudMessagePersistence {
131
167
  * Reset the ID mapping (call when switching threads).
132
168
  */
133
169
  reset() {
134
- this.idMapping = {};
170
+ this.idMapping.clear();
135
171
  }
136
172
  }
@@ -1,5 +1,10 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { CloudResponseError, readCloudTimestamp } from "./cloudResponse";
2
+ import {
3
+ CloudResponseError,
4
+ readCloudNullableNumber,
5
+ readCloudNumber,
6
+ readCloudTimestamp,
7
+ } from "./cloudResponse";
3
8
 
4
9
  describe("readCloudTimestamp", () => {
5
10
  it("decodes a canonical Cloud timestamp", () => {
@@ -26,3 +31,31 @@ describe("readCloudTimestamp", () => {
26
31
  );
27
32
  });
28
33
  });
34
+
35
+ describe("readCloudNumber", () => {
36
+ it("returns a finite number", () => {
37
+ expect(readCloudNumber(0.5, "score.value")).toBe(0.5);
38
+ expect(readCloudNumber(0, "score.value")).toBe(0);
39
+ });
40
+
41
+ it.each([null, undefined, "1", true, Number.NaN, Number.POSITIVE_INFINITY])(
42
+ "rejects %s",
43
+ (value) => {
44
+ const decode = () => readCloudNumber(value, "score.value");
45
+ expect(decode).toThrow(CloudResponseError);
46
+ expect(decode).toThrow(
47
+ 'Invalid Assistant Cloud response for "score.value": expected a number',
48
+ );
49
+ },
50
+ );
51
+ });
52
+
53
+ describe("readCloudNullableNumber", () => {
54
+ it("passes null through and validates everything else", () => {
55
+ expect(readCloudNullableNumber(null, "score.value")).toBeNull();
56
+ expect(readCloudNullableNumber(1, "score.value")).toBe(1);
57
+ expect(() => readCloudNullableNumber("1", "score.value")).toThrow(
58
+ CloudResponseError,
59
+ );
60
+ });
61
+ });
@@ -32,6 +32,36 @@ export const readCloudString = (value: unknown, field: string): string => {
32
32
  return value;
33
33
  };
34
34
 
35
+ export const readCloudNumber = (value: unknown, field: string): number => {
36
+ if (typeof value !== "number" || !Number.isFinite(value)) {
37
+ throw invalidCloudResponse(field, "a number");
38
+ }
39
+ return value;
40
+ };
41
+
42
+ export const readCloudNullableNumber = (
43
+ value: unknown,
44
+ field: string,
45
+ ): number | null => {
46
+ if (value === null) return null;
47
+ return readCloudNumber(value, field);
48
+ };
49
+
50
+ export const readCloudEnum = <const T extends readonly string[]>(
51
+ value: unknown,
52
+ field: string,
53
+ allowed: T,
54
+ ): T[number] => {
55
+ const text = readCloudString(value, field);
56
+ if (!allowed.includes(text)) {
57
+ throw invalidCloudResponse(
58
+ field,
59
+ `one of ${allowed.map((entry) => `"${entry}"`).join(", ")}`,
60
+ );
61
+ }
62
+ return text;
63
+ };
64
+
35
65
  export const readCloudNullableString = (
36
66
  value: unknown,
37
67
  field: string,
package/src/index.ts CHANGED
@@ -1,19 +1,42 @@
1
- export type { CloudMessage } from "./AssistantCloudThreadMessages";
1
+ export type {
2
+ CloudMessage,
3
+ AssistantCloudThreadMessageFeedbackBody,
4
+ AssistantCloudThreadMessageFeedbackResponse,
5
+ } from "./AssistantCloudThreadMessages";
2
6
  export type { AssistantCloudTelemetryConfig } from "./AssistantCloudAPI";
7
+ export {
8
+ AssistantCloudEvents,
9
+ type AssistantCloudEvent,
10
+ type AssistantCloudEventKind,
11
+ } from "./AssistantCloudEvents";
12
+ export {
13
+ AssistantCloudScores,
14
+ type AssistantCloudScoreBody,
15
+ type AssistantCloudScoreResponse,
16
+ } from "./AssistantCloudScores";
17
+ export type { GeneratePresignedDownloadUrlResponse } from "./AssistantCloudFiles";
3
18
  export { CloudAPIError } from "./AssistantCloudAPI";
4
19
  export { CloudResponseError } from "./cloudResponse";
5
20
  export { generateThreadTitle } from "./generateThreadTitle";
6
21
  export type { AssistantCloudRunReport } from "./AssistantCloudRuns";
7
22
  export {
23
+ createRunReport,
8
24
  createRunTelemetryToolCall,
25
+ deriveRunOutcome,
26
+ describeRunError,
27
+ extractRunTelemetryModelId,
9
28
  normalizeRunTelemetryUsage,
10
29
  truncateRunTelemetryText,
11
30
  type AssistantCloudRunReportToolCall,
31
+ type RunReportInit,
32
+ type RunReportOutcome,
33
+ type RunReportStepInit,
12
34
  type RunTelemetryToolCallInit,
13
35
  type RunTelemetryUsage,
14
36
  type RunTelemetryUsageInit,
15
37
  } from "./runTelemetry";
16
38
  export { AssistantCloud } from "./AssistantCloud";
39
+ export { readAnonymousRefreshToken } from "./AssistantCloudAuthStrategy";
17
40
  export { CloudMessagePersistence } from "./CloudMessagePersistence";
18
41
  export {
19
42
  createFormattedPersistence,