assistant-cloud 0.1.43 → 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 +11 -1
  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 +38 -11
  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 +70 -7
  48. package/dist/runTelemetry.d.ts.map +1 -1
  49. package/dist/runTelemetry.js +144 -1
  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 +13 -0
  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 +48 -16
  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 +325 -0
  75. package/src/runTelemetry.ts +257 -6
  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 +71 -2
  81. package/src/tests/CloudMessagePersistence.test.ts +115 -0
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
  import type { AssistantCloudAPI } from "./AssistantCloudAPI";
3
3
  import { AssistantCloudThreadMessages } from "./AssistantCloudThreadMessages";
4
+ import { CloudResponseError } from "./cloudResponse";
4
5
 
5
6
  const createCloudThreadMessages = () => {
6
7
  const makeRequest = vi.fn();
@@ -32,6 +33,41 @@ describe("AssistantCloudThreadMessages responses", () => {
32
33
  );
33
34
  });
34
35
 
36
+ it("submits and validates message feedback", async () => {
37
+ const { messages, makeRequest } = createCloudThreadMessages();
38
+ const body = { type: "positive" as const };
39
+ makeRequest.mockResolvedValueOnce({
40
+ feedback_id: "feedback-1",
41
+ type: "positive",
42
+ });
43
+
44
+ await expect(
45
+ messages.feedback("thread/1", "message/1", body),
46
+ ).resolves.toEqual({
47
+ feedback_id: "feedback-1",
48
+ type: "positive",
49
+ });
50
+ expect(makeRequest).toHaveBeenCalledWith(
51
+ "/threads/thread%2F1/messages/message%2F1/feedback",
52
+ { method: "POST", body },
53
+ );
54
+
55
+ makeRequest.mockResolvedValueOnce({ type: "positive" });
56
+
57
+ await expect(
58
+ messages.feedback("thread-1", "message-1", body),
59
+ ).rejects.toBeInstanceOf(CloudResponseError);
60
+
61
+ makeRequest.mockResolvedValueOnce({
62
+ feedback_id: "feedback-1",
63
+ type: "neutral",
64
+ });
65
+
66
+ await expect(
67
+ messages.feedback("thread-1", "message-1", body),
68
+ ).rejects.toThrow('expected one of "positive", "negative"');
69
+ });
70
+
35
71
  it("decodes canonical message responses without changing content", async () => {
36
72
  const { messages, makeRequest } = createCloudThreadMessages();
37
73
  makeRequest.mockResolvedValue({
@@ -58,4 +94,19 @@ describe("AssistantCloudThreadMessages responses", () => {
58
94
  created_at: "leave-this-string-untouched",
59
95
  });
60
96
  });
97
+
98
+ it("forwards the pagination query to the list endpoint", async () => {
99
+ const { messages, makeRequest } = createCloudThreadMessages();
100
+ makeRequest.mockResolvedValueOnce({ messages: [] });
101
+
102
+ await messages.list("thread/1", {
103
+ format: "aui/v0",
104
+ limit: 200,
105
+ after: "message-200",
106
+ });
107
+
108
+ expect(makeRequest).toHaveBeenCalledWith("/threads/thread%2F1/messages", {
109
+ query: { format: "aui/v0", limit: 200, after: "message-200" },
110
+ });
111
+ });
61
112
  });
@@ -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,15 +42,16 @@ export class CloudMessagePersistence {
39
42
  content: ReadonlyJSONObject,
40
43
  ): Promise<void> {
41
44
  const cloud = this.getCloud();
42
- const existing = this.idMapping[messageId];
45
+ const existing = this.idMapping.get(messageId);
43
46
  if (existing instanceof Promise) {
44
47
  await existing;
45
48
  return;
46
49
  }
47
50
 
48
51
  const task = (async () => {
52
+ const parentEntry = parentId ? this.idMapping.get(parentId) : undefined;
49
53
  const resolvedParentId = parentId
50
- ? ((await this.idMapping[parentId]) ?? parentId)
54
+ ? ((await parentEntry) ?? parentId)
51
55
  : null;
52
56
  const { message_id } = await cloud.threads.messages.create(threadId, {
53
57
  parent_id: resolvedParentId,
@@ -57,15 +61,15 @@ export class CloudMessagePersistence {
57
61
  return message_id;
58
62
  })();
59
63
 
60
- this.idMapping[messageId] = task;
64
+ this.idMapping.set(messageId, task);
61
65
  try {
62
66
  const remoteId = await task;
63
- if (this.idMapping[messageId] === task) {
64
- this.idMapping[messageId] = remoteId;
67
+ if (this.idMapping.get(messageId) === task) {
68
+ this.idMapping.set(messageId, remoteId);
65
69
  }
66
70
  } catch (err) {
67
- if (this.idMapping[messageId] === task) {
68
- delete this.idMapping[messageId];
71
+ if (this.idMapping.get(messageId) === task) {
72
+ this.idMapping.delete(messageId);
69
73
  }
70
74
  throw err;
71
75
  }
@@ -95,7 +99,7 @@ export class CloudMessagePersistence {
95
99
  * Check if a message has been persisted (or is currently being persisted).
96
100
  */
97
101
  isPersisted(messageId: string): boolean {
98
- return messageId in this.idMapping;
102
+ return this.idMapping.has(messageId);
99
103
  }
100
104
 
101
105
  /**
@@ -103,14 +107,22 @@ export class CloudMessagePersistence {
103
107
  * Returns undefined if not persisted.
104
108
  */
105
109
  async getRemoteId(messageId: string): Promise<string | undefined> {
106
- const entry = this.idMapping[messageId];
110
+ const entry = this.idMapping.get(messageId);
107
111
  if (!entry) return undefined;
108
112
  return entry;
109
113
  }
110
114
 
115
+ getResolvedRemoteId(messageId: string): string | undefined {
116
+ const entry = this.idMapping.get(messageId);
117
+ return typeof entry === "string" ? entry : undefined;
118
+ }
119
+
111
120
  /**
112
121
  * Load messages from the cloud and populate the ID mapping.
113
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
+ *
114
126
  * The ID mapping is populated so that `isPersisted()` returns true for
115
127
  * loaded messages, preventing re-persistence of already-stored messages.
116
128
  *
@@ -120,13 +132,33 @@ export class CloudMessagePersistence {
120
132
  */
121
133
  async load(threadId: string, format?: string) {
122
134
  const cloud = this.getCloud();
123
- const { messages } = await cloud.threads.messages.list(
124
- threadId,
125
- format ? { format } : undefined,
126
- );
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
+
127
159
  // Populate ID mapping so isPersisted() recognizes loaded messages
128
160
  for (const m of messages) {
129
- this.idMapping[m.id] = m.id;
161
+ this.idMapping.set(m.id, m.id);
130
162
  }
131
163
  return messages;
132
164
  }
@@ -135,6 +167,6 @@ export class CloudMessagePersistence {
135
167
  * Reset the ID mapping (call when switching threads).
136
168
  */
137
169
  reset() {
138
- this.idMapping = {};
170
+ this.idMapping.clear();
139
171
  }
140
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,