assistant-cloud 0.1.35 → 0.1.37

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 (76) hide show
  1. package/dist/AssistantCloud.d.ts +2 -0
  2. package/dist/AssistantCloud.d.ts.map +1 -1
  3. package/dist/AssistantCloud.js +4 -1
  4. package/dist/AssistantCloud.js.map +1 -1
  5. package/dist/AssistantCloudAPI.d.ts.map +1 -1
  6. package/dist/AssistantCloudAPI.js +8 -4
  7. package/dist/AssistantCloudAPI.js.map +1 -1
  8. package/dist/AssistantCloudAuthStrategy.d.ts +2 -0
  9. package/dist/AssistantCloudAuthStrategy.d.ts.map +1 -1
  10. package/dist/AssistantCloudAuthStrategy.js +15 -5
  11. package/dist/AssistantCloudAuthStrategy.js.map +1 -1
  12. package/dist/AssistantCloudAuthTokens.d.ts.map +1 -1
  13. package/dist/AssistantCloudAuthTokens.js.map +1 -1
  14. package/dist/AssistantCloudFiles.d.ts.map +1 -1
  15. package/dist/AssistantCloudFiles.js.map +1 -1
  16. package/dist/AssistantCloudProjectThreadMessages.d.ts +19 -0
  17. package/dist/AssistantCloudProjectThreadMessages.d.ts.map +1 -0
  18. package/dist/AssistantCloudProjectThreadMessages.js +16 -0
  19. package/dist/AssistantCloudProjectThreadMessages.js.map +1 -0
  20. package/dist/AssistantCloudProjectThreads.d.ts +21 -0
  21. package/dist/AssistantCloudProjectThreads.d.ts.map +1 -0
  22. package/dist/AssistantCloudProjectThreads.js +22 -0
  23. package/dist/AssistantCloudProjectThreads.js.map +1 -0
  24. package/dist/AssistantCloudProjects.d.ts +10 -0
  25. package/dist/AssistantCloudProjects.d.ts.map +1 -0
  26. package/dist/AssistantCloudProjects.js +12 -0
  27. package/dist/AssistantCloudProjects.js.map +1 -0
  28. package/dist/AssistantCloudRuns.d.ts.map +1 -1
  29. package/dist/AssistantCloudRuns.js.map +1 -1
  30. package/dist/AssistantCloudThreadMessages.d.ts +2 -1
  31. package/dist/AssistantCloudThreadMessages.d.ts.map +1 -1
  32. package/dist/AssistantCloudThreadMessages.js +15 -2
  33. package/dist/AssistantCloudThreadMessages.js.map +1 -1
  34. package/dist/AssistantCloudThreads.d.ts +3 -2
  35. package/dist/AssistantCloudThreads.d.ts.map +1 -1
  36. package/dist/AssistantCloudThreads.js +21 -4
  37. package/dist/AssistantCloudThreads.js.map +1 -1
  38. package/dist/CloudMessagePersistence.d.ts +2 -1
  39. package/dist/CloudMessagePersistence.d.ts.map +1 -1
  40. package/dist/CloudMessagePersistence.js +11 -6
  41. package/dist/CloudMessagePersistence.js.map +1 -1
  42. package/dist/FormattedCloudPersistence.d.ts.map +1 -1
  43. package/dist/cloudResponse.d.ts +16 -0
  44. package/dist/cloudResponse.d.ts.map +1 -0
  45. package/dist/cloudResponse.js +43 -0
  46. package/dist/cloudResponse.js.map +1 -0
  47. package/dist/index.d.ts +2 -1
  48. package/dist/index.js +2 -1
  49. package/dist/instrumentMcpSampling.d.ts.map +1 -1
  50. package/dist/tests/setup.d.ts +1 -1
  51. package/dist/tests/setup.js +2 -4
  52. package/dist/tests/setup.js.map +1 -1
  53. package/package.json +3 -3
  54. package/src/AssistantCloud.ts +3 -0
  55. package/src/AssistantCloudAPI.ts +15 -10
  56. package/src/AssistantCloudAuthStrategy.ts +21 -6
  57. package/src/AssistantCloudAuthTokens.ts +5 -1
  58. package/src/AssistantCloudFiles.ts +5 -1
  59. package/src/AssistantCloudProjectThreadMessages.ts +44 -0
  60. package/src/AssistantCloudProjectThreads.ts +50 -0
  61. package/src/AssistantCloudProjects.ts +10 -0
  62. package/src/AssistantCloudRuns.ts +5 -1
  63. package/src/AssistantCloudThreadMessages.test.ts +41 -0
  64. package/src/AssistantCloudThreadMessages.ts +43 -4
  65. package/src/AssistantCloudThreads.test.ts +74 -0
  66. package/src/AssistantCloudThreads.ts +55 -4
  67. package/src/CloudMessagePersistence.ts +18 -5
  68. package/src/cloudResponse.test.ts +28 -0
  69. package/src/cloudResponse.ts +72 -0
  70. package/src/index.ts +1 -0
  71. package/src/tests/AssistantCloudAPI.test.ts +5 -19
  72. package/src/tests/AssistantCloudAuthStrategy.test.ts +50 -22
  73. package/src/tests/AssistantCloudProjects.test.ts +121 -0
  74. package/src/tests/CloudMessagePersistence.test.ts +40 -0
  75. package/src/tests/setup.ts +2 -9
  76. package/src/tests/AssistantCloudFiles.test.ts +0 -562
@@ -14,9 +14,10 @@ import { ReadonlyJSONObject } from "assistant-stream/utils";
14
14
  * to get its remote ID before creating B.
15
15
  */
16
16
  declare class CloudMessagePersistence {
17
- private cloud;
18
17
  private idMapping;
18
+ private getCloud;
19
19
  constructor(cloud: AssistantCloud);
20
+ constructor(getCloud: () => AssistantCloud);
20
21
  /**
21
22
  * Persist a message to the cloud.
22
23
  *
@@ -1 +1 @@
1
- {"version":3,"file":"CloudMessagePersistence.d.ts","names":[],"sources":["../src/CloudMessagePersistence.ts"],"mappings":";;;;;;;;;;;;;;;cAca;UAGS;UAFZ;cAEY,OAAO;;;;;;;;;;EAWrB,OACJ,kBACA,mBACA,yBACA,gBACA,SAAS,qBACR;;;;EAgCG,OACJ,kBACA,mBACA,iBACA,SAAS,qBACR;;;;EASH,YAAY;;;;;EAQN,YAAY,oBAAoB;;;;;;;;;;;EAgBhC,KAAK,kBAAkB,kBAAe,QAhBN;;;;EA+BtC"}
1
+ {"version":3,"file":"CloudMessagePersistence.d.ts","names":[],"sources":["../src/CloudMessagePersistence.ts"],"mappings":";;;;;;;;;;;;;;;cAca;UACH;UACA;EAEI,YAAA,OAAO;EACP,YAAA,gBAAgB;;;;;;;;;;EActB,OACJ,kBACA,mBACA,yBACA,gBACA,SAAS,qBACR;;;;EAiCG,OACJ,kBACA,mBACA,iBACA,SAAS,qBACR;;;;EAeH,YAAY;;;;;EAQN,YAAY,oBAAoB;;;;;;;;;;;EAgBhC,KAAK,kBAAkB,kBAAe,QAhBN;;;;EAgCtC"}
@@ -11,10 +11,10 @@
11
11
  * to get its remote ID before creating B.
12
12
  */
13
13
  var CloudMessagePersistence = class {
14
- cloud;
15
14
  idMapping = {};
15
+ getCloud;
16
16
  constructor(cloud) {
17
- this.cloud = cloud;
17
+ this.getCloud = typeof cloud === "function" ? cloud : () => cloud;
18
18
  }
19
19
  /**
20
20
  * Persist a message to the cloud.
@@ -26,8 +26,9 @@ var CloudMessagePersistence = class {
26
26
  * @param content - Message content (format-specific)
27
27
  */
28
28
  async append(threadId, messageId, parentId, format, content) {
29
+ const cloud = this.getCloud();
29
30
  const resolvedParentId = parentId ? await this.idMapping[parentId] ?? parentId : null;
30
- const task = this.cloud.threads.messages.create(threadId, {
31
+ const task = cloud.threads.messages.create(threadId, {
31
32
  parent_id: resolvedParentId,
32
33
  format,
33
34
  content
@@ -45,9 +46,13 @@ var CloudMessagePersistence = class {
45
46
  * Update an already-persisted message in the cloud.
46
47
  */
47
48
  async update(threadId, messageId, _format, content) {
49
+ const cloud = this.getCloud();
48
50
  const remoteId = await this.getRemoteId(messageId);
49
- if (!remoteId) return;
50
- await this.cloud.threads.messages.update(threadId, remoteId, { content });
51
+ if (!remoteId) {
52
+ console.warn(`Skipping update for message ${messageId}: no remote id is mapped.`);
53
+ return;
54
+ }
55
+ await cloud.threads.messages.update(threadId, remoteId, { content });
51
56
  }
52
57
  /**
53
58
  * Check if a message has been persisted (or is currently being persisted).
@@ -75,7 +80,7 @@ var CloudMessagePersistence = class {
75
80
  * @returns Array of cloud messages
76
81
  */
77
82
  async load(threadId, format) {
78
- const { messages } = await this.cloud.threads.messages.list(threadId, format ? { format } : void 0);
83
+ const { messages } = await this.getCloud().threads.messages.list(threadId, format ? { format } : void 0);
79
84
  for (const m of messages) this.idMapping[m.id] = m.id;
80
85
  return messages;
81
86
  }
@@ -1 +1 @@
1
- {"version":3,"file":"CloudMessagePersistence.js","names":[],"sources":["../src/CloudMessagePersistence.ts"],"sourcesContent":["import type { ReadonlyJSONObject } from \"assistant-stream/utils\";\nimport type { AssistantCloud } from \"./AssistantCloud\";\n\n/**\n * Shared persistence logic for cloud message storage.\n *\n * Handles ID mapping (local → remote) and parent_id chaining for both:\n * - AssistantCloudThreadHistoryAdapter (assistant-ui runtime)\n * - useCloudChat (standalone AI SDK hook)\n *\n * The promise-based ID resolution handles concurrent appends — if message B's\n * parent is message A, and A is still being created, we await A's promise\n * to get its remote ID before creating B.\n */\nexport class CloudMessagePersistence {\n private idMapping: Record<string, string | Promise<string>> = {};\n\n constructor(private cloud: AssistantCloud) {}\n\n /**\n * Persist a message to the cloud.\n *\n * @param threadId - Remote thread ID\n * @param messageId - Local message ID (used for tracking)\n * @param parentId - Local parent message ID (or null for first message)\n * @param format - Message format (e.g., \"aui/v0\", \"ai-sdk/v6\")\n * @param content - Message content (format-specific)\n */\n async append(\n threadId: string,\n messageId: string,\n parentId: string | null,\n format: string,\n content: ReadonlyJSONObject,\n ): Promise<void> {\n // Resolve parent's remote ID if it exists (may be a promise if concurrent)\n const resolvedParentId = parentId\n ? ((await this.idMapping[parentId]) ?? parentId)\n : null;\n\n const task = this.cloud.threads.messages\n .create(threadId, {\n parent_id: resolvedParentId,\n format,\n content,\n })\n .then(({ message_id }) => {\n this.idMapping[messageId] = message_id;\n return message_id;\n })\n .catch((err) => {\n // Only delete if we're still the active task (avoids clobbering a retry)\n if (this.idMapping[messageId] === task) {\n delete this.idMapping[messageId];\n }\n throw err;\n });\n\n // Store the promise immediately so concurrent appends can await it\n this.idMapping[messageId] = task;\n return task.then(() => {});\n }\n\n /**\n * Update an already-persisted message in the cloud.\n */\n async update(\n threadId: string,\n messageId: string,\n _format: string,\n content: ReadonlyJSONObject,\n ): Promise<void> {\n const remoteId = await this.getRemoteId(messageId);\n if (!remoteId) return; // not persisted yet, skip\n await this.cloud.threads.messages.update(threadId, remoteId, { content });\n }\n\n /**\n * Check if a message has been persisted (or is currently being persisted).\n */\n isPersisted(messageId: string): boolean {\n return messageId in this.idMapping;\n }\n\n /**\n * Get the remote ID for a local message ID (resolved).\n * Returns undefined if not persisted.\n */\n async getRemoteId(messageId: string): Promise<string | undefined> {\n const entry = this.idMapping[messageId];\n if (!entry) return undefined;\n return entry;\n }\n\n /**\n * Load messages from the cloud and populate the ID mapping.\n *\n * The ID mapping is populated so that `isPersisted()` returns true for\n * loaded messages, preventing re-persistence of already-stored messages.\n *\n * @param threadId - Remote thread ID\n * @param format - Optional format filter\n * @returns Array of cloud messages\n */\n async load(threadId: string, format?: string) {\n const { messages } = await this.cloud.threads.messages.list(\n threadId,\n format ? { format } : undefined,\n );\n // Populate ID mapping so isPersisted() recognizes loaded messages\n for (const m of messages) {\n this.idMapping[m.id] = m.id;\n }\n return messages;\n }\n\n /**\n * Reset the ID mapping (call when switching threads).\n */\n reset() {\n this.idMapping = {};\n }\n}\n"],"mappings":";;;;;;;;;;;;AAcA,IAAa,0BAAb,MAAqC;CAGf;CAFpB,YAA8D,CAAC;CAE/D,YAAY,OAA+B;EAAvB,KAAA,QAAA;CAAwB;;;;;;;;;;CAW5C,MAAM,OACJ,UACA,WACA,UACA,QACA,SACe;EAEf,MAAM,mBAAmB,WACnB,MAAM,KAAK,UAAU,aAAc,WACrC;EAEJ,MAAM,OAAO,KAAK,MAAM,QAAQ,SAC7B,OAAO,UAAU;GAChB,WAAW;GACX;GACA;EACF,CAAC,CAAC,CACD,MAAM,EAAE,iBAAiB;GACxB,KAAK,UAAU,aAAa;GAC5B,OAAO;EACT,CAAC,CAAC,CACD,OAAO,QAAQ;GAEd,IAAI,KAAK,UAAU,eAAe,MAChC,OAAO,KAAK,UAAU;GAExB,MAAM;EACR,CAAC;EAGH,KAAK,UAAU,aAAa;EAC5B,OAAO,KAAK,WAAW,CAAC,CAAC;CAC3B;;;;CAKA,MAAM,OACJ,UACA,WACA,SACA,SACe;EACf,MAAM,WAAW,MAAM,KAAK,YAAY,SAAS;EACjD,IAAI,CAAC,UAAU;EACf,MAAM,KAAK,MAAM,QAAQ,SAAS,OAAO,UAAU,UAAU,EAAE,QAAQ,CAAC;CAC1E;;;;CAKA,YAAY,WAA4B;EACtC,OAAO,aAAa,KAAK;CAC3B;;;;;CAMA,MAAM,YAAY,WAAgD;EAChE,MAAM,QAAQ,KAAK,UAAU;EAC7B,IAAI,CAAC,OAAO,OAAO,KAAA;EACnB,OAAO;CACT;;;;;;;;;;;CAYA,MAAM,KAAK,UAAkB,QAAiB;EAC5C,MAAM,EAAE,aAAa,MAAM,KAAK,MAAM,QAAQ,SAAS,KACrD,UACA,SAAS,EAAE,OAAO,IAAI,KAAA,CACxB;EAEA,KAAK,MAAM,KAAK,UACd,KAAK,UAAU,EAAE,MAAM,EAAE;EAE3B,OAAO;CACT;;;;CAKA,QAAQ;EACN,KAAK,YAAY,CAAC;CACpB;AACF"}
1
+ {"version":3,"file":"CloudMessagePersistence.js","names":[],"sources":["../src/CloudMessagePersistence.ts"],"sourcesContent":["import type { ReadonlyJSONObject } from \"assistant-stream/utils\";\nimport type { AssistantCloud } from \"./AssistantCloud\";\n\n/**\n * Shared persistence logic for cloud message storage.\n *\n * Handles ID mapping (local → remote) and parent_id chaining for both:\n * - AssistantCloudThreadHistoryAdapter (assistant-ui runtime)\n * - useCloudChat (standalone AI SDK hook)\n *\n * The promise-based ID resolution handles concurrent appends — if message B's\n * parent is message A, and A is still being created, we await A's promise\n * to get its remote ID before creating B.\n */\nexport class CloudMessagePersistence {\n private idMapping: Record<string, string | Promise<string>> = {};\n private getCloud: () => AssistantCloud;\n\n constructor(cloud: AssistantCloud);\n constructor(getCloud: () => AssistantCloud);\n constructor(cloud: AssistantCloud | (() => AssistantCloud)) {\n this.getCloud = typeof cloud === \"function\" ? cloud : () => cloud;\n }\n\n /**\n * Persist a message to the cloud.\n *\n * @param threadId - Remote thread ID\n * @param messageId - Local message ID (used for tracking)\n * @param parentId - Local parent message ID (or null for first message)\n * @param format - Message format (e.g., \"aui/v0\", \"ai-sdk/v6\")\n * @param content - Message content (format-specific)\n */\n async append(\n threadId: string,\n messageId: string,\n parentId: string | null,\n format: string,\n content: ReadonlyJSONObject,\n ): Promise<void> {\n const cloud = this.getCloud();\n // Resolve parent's remote ID if it exists (may be a promise if concurrent)\n const resolvedParentId = parentId\n ? ((await this.idMapping[parentId]) ?? parentId)\n : null;\n\n const task = cloud.threads.messages\n .create(threadId, {\n parent_id: resolvedParentId,\n format,\n content,\n })\n .then(({ message_id }) => {\n this.idMapping[messageId] = message_id;\n return message_id;\n })\n .catch((err) => {\n // Only delete if we're still the active task (avoids clobbering a retry)\n if (this.idMapping[messageId] === task) {\n delete this.idMapping[messageId];\n }\n throw err;\n });\n\n // Store the promise immediately so concurrent appends can await it\n this.idMapping[messageId] = task;\n return task.then(() => {});\n }\n\n /**\n * Update an already-persisted message in the cloud.\n */\n async update(\n threadId: string,\n messageId: string,\n _format: string,\n content: ReadonlyJSONObject,\n ): Promise<void> {\n const cloud = this.getCloud();\n const remoteId = await this.getRemoteId(messageId);\n if (!remoteId) {\n console.warn(\n `Skipping update for message ${messageId}: no remote id is mapped.`,\n );\n return;\n }\n await cloud.threads.messages.update(threadId, remoteId, { content });\n }\n\n /**\n * Check if a message has been persisted (or is currently being persisted).\n */\n isPersisted(messageId: string): boolean {\n return messageId in this.idMapping;\n }\n\n /**\n * Get the remote ID for a local message ID (resolved).\n * Returns undefined if not persisted.\n */\n async getRemoteId(messageId: string): Promise<string | undefined> {\n const entry = this.idMapping[messageId];\n if (!entry) return undefined;\n return entry;\n }\n\n /**\n * Load messages from the cloud and populate the ID mapping.\n *\n * The ID mapping is populated so that `isPersisted()` returns true for\n * loaded messages, preventing re-persistence of already-stored messages.\n *\n * @param threadId - Remote thread ID\n * @param format - Optional format filter\n * @returns Array of cloud messages\n */\n async load(threadId: string, format?: string) {\n const cloud = this.getCloud();\n const { messages } = await cloud.threads.messages.list(\n threadId,\n format ? { format } : undefined,\n );\n // Populate ID mapping so isPersisted() recognizes loaded messages\n for (const m of messages) {\n this.idMapping[m.id] = m.id;\n }\n return messages;\n }\n\n /**\n * Reset the ID mapping (call when switching threads).\n */\n reset() {\n this.idMapping = {};\n }\n}\n"],"mappings":";;;;;;;;;;;;AAcA,IAAa,0BAAb,MAAqC;CACnC,YAA8D,CAAC;CAC/D;CAIA,YAAY,OAAgD;EAC1D,KAAK,WAAW,OAAO,UAAU,aAAa,cAAc;CAC9D;;;;;;;;;;CAWA,MAAM,OACJ,UACA,WACA,UACA,QACA,SACe;EACf,MAAM,QAAQ,KAAK,SAAS;EAE5B,MAAM,mBAAmB,WACnB,MAAM,KAAK,UAAU,aAAc,WACrC;EAEJ,MAAM,OAAO,MAAM,QAAQ,SACxB,OAAO,UAAU;GAChB,WAAW;GACX;GACA;EACF,CAAC,CAAC,CACD,MAAM,EAAE,iBAAiB;GACxB,KAAK,UAAU,aAAa;GAC5B,OAAO;EACT,CAAC,CAAC,CACD,OAAO,QAAQ;GAEd,IAAI,KAAK,UAAU,eAAe,MAChC,OAAO,KAAK,UAAU;GAExB,MAAM;EACR,CAAC;EAGH,KAAK,UAAU,aAAa;EAC5B,OAAO,KAAK,WAAW,CAAC,CAAC;CAC3B;;;;CAKA,MAAM,OACJ,UACA,WACA,SACA,SACe;EACf,MAAM,QAAQ,KAAK,SAAS;EAC5B,MAAM,WAAW,MAAM,KAAK,YAAY,SAAS;EACjD,IAAI,CAAC,UAAU;GACb,QAAQ,KACN,+BAA+B,UAAU,0BAC3C;GACA;EACF;EACA,MAAM,MAAM,QAAQ,SAAS,OAAO,UAAU,UAAU,EAAE,QAAQ,CAAC;CACrE;;;;CAKA,YAAY,WAA4B;EACtC,OAAO,aAAa,KAAK;CAC3B;;;;;CAMA,MAAM,YAAY,WAAgD;EAChE,MAAM,QAAQ,KAAK,UAAU;EAC7B,IAAI,CAAC,OAAO,OAAO,KAAA;EACnB,OAAO;CACT;;;;;;;;;;;CAYA,MAAM,KAAK,UAAkB,QAAiB;EAE5C,MAAM,EAAE,aAAa,MADP,KAAK,SACY,CAAC,CAAC,QAAQ,SAAS,KAChD,UACA,SAAS,EAAE,OAAO,IAAI,KAAA,CACxB;EAEA,KAAK,MAAM,KAAK,UACd,KAAK,UAAU,EAAE,MAAM,EAAE;EAE3B,OAAO;CACT;;;;CAKA,QAAQ;EACN,KAAK,YAAY,CAAC;CACpB;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"FormattedCloudPersistence.d.ts","names":[],"sources":["../src/FormattedCloudPersistence.ts"],"mappings":";;;;;;;KAOY,qBAAqB,UAAU;EACzC;EACA,OAAO;IAAQ;IAAyB,SAAS;MAAa;EAC9D,OAAO;IACL;IACA;IACA;IACA,SAAS;;IACL;IAAyB,SAAS;;EACxC,MAAM,SAAS;;;;;;;;;;;;cAaJ,6BAA8B,UAAU,gBACnD;EACE,SACE,kBACA,mBACA,yBACA,gBACA,SAAS,uBACN;EACL,OAAO,kBAAkB,oBAAoB;EAC7C,cAAc;EACd,UACE,kBACA,mBACA,gBACA,SAAS,uBACN;GAEP,SAAS,qBAAqB,UAAU;6BAGtB;IACR;IAAyB,SAAS;QACzC;8BAamB;IACR;IAAyB,SAAS;KAAU,sBAEnD;8BAUsB"}
1
+ {"version":3,"file":"FormattedCloudPersistence.d.ts","names":[],"sources":["../src/FormattedCloudPersistence.ts"],"mappings":";;;;;;;KAOY,qBAAqB,UAAU;EACzC;EACA,OAAO;IAAQ;IAAyB,SAAS;MAAa;EAC9D,OAAO;IACL;IACA;IACA;IACA,SAAS;;IACL;IAAyB,SAAS;;EACxC,MAAM,SAAS;;;;;;;;;;;;cAaJ,6BAA8B,UAAU,gBACnD;EACE,SACE,kBACA,mBACA,yBACA,gBACA,SAAS,uBACN;EACL,OAAO,kBAAkB,oBAAoB;EAC7C,cAAc;EACd,UACE,kBACA,mBACA,gBACA,SAAS,uBACN;GAEP,SAAS,qBAAqB,UAAU;EAG5B,SAAA,kBAAM;IACR;IAAyB,SAAS;QACzC;EAaa,UAAA,kBAAM;IACR;IAAyB,SAAS;KAAU,sBAEnD;EAUgB,OAAA,qBAAM;;MA/Db;;;;EA+ES,cAAA"}
@@ -0,0 +1,16 @@
1
+ import { ReadonlyJSONObject } from "assistant-stream/utils";
2
+ //#region src/cloudResponse.d.ts
3
+ declare class CloudResponseError extends Error {
4
+ constructor(message: string);
5
+ }
6
+ declare const readCloudRecord: (value: unknown, field: string) => Record<string, unknown>;
7
+ declare const readCloudArray: (value: unknown, field: string) => unknown[];
8
+ declare const readCloudString: (value: unknown, field: string) => string;
9
+ declare const readCloudNullableString: (value: unknown, field: string) => string | null;
10
+ declare const readCloudBoolean: (value: unknown, field: string) => boolean;
11
+ declare const readCloudInteger: (value: unknown, field: string) => number;
12
+ declare const readCloudTimestamp: (value: unknown, field: string) => Date;
13
+ declare const readCloudJSONObject: (value: unknown, field: string) => ReadonlyJSONObject;
14
+ //#endregion
15
+ export { CloudResponseError, readCloudArray, readCloudBoolean, readCloudInteger, readCloudJSONObject, readCloudNullableString, readCloudRecord, readCloudString, readCloudTimestamp };
16
+ //# sourceMappingURL=cloudResponse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloudResponse.d.ts","names":[],"sources":["../src/cloudResponse.ts"],"mappings":";;cAEa,2BAA2B;EAC1B,YAAA;;cAWD,kBACX,gBACA,kBACC;cAOU,iBAAkB,gBAAgB;cAKlC,kBAAmB,gBAAgB;cAKnC,0BACX,gBACA;cAMW,mBAAoB,gBAAgB;cAOpC,mBAAoB,gBAAgB;cAKpC,qBAAsB,gBAAgB,kBAAgB;cActD,sBACX,gBACA,kBACC"}
@@ -0,0 +1,43 @@
1
+ //#region src/cloudResponse.ts
2
+ var CloudResponseError = class extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "CloudResponseError";
6
+ }
7
+ };
8
+ const invalidCloudResponse = (field, expected) => new CloudResponseError(`Invalid Assistant Cloud response for "${field}": expected ${expected}`);
9
+ const readCloudRecord = (value, field) => {
10
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw invalidCloudResponse(field, "an object");
11
+ return value;
12
+ };
13
+ const readCloudArray = (value, field) => {
14
+ if (!Array.isArray(value)) throw invalidCloudResponse(field, "an array");
15
+ return value;
16
+ };
17
+ const readCloudString = (value, field) => {
18
+ if (typeof value !== "string") throw invalidCloudResponse(field, "a string");
19
+ return value;
20
+ };
21
+ const readCloudNullableString = (value, field) => {
22
+ if (value === null) return null;
23
+ return readCloudString(value, field);
24
+ };
25
+ const readCloudBoolean = (value, field) => {
26
+ if (typeof value !== "boolean") throw invalidCloudResponse(field, "a boolean");
27
+ return value;
28
+ };
29
+ const readCloudInteger = (value, field) => {
30
+ if (!Number.isInteger(value)) throw invalidCloudResponse(field, "an integer");
31
+ return value;
32
+ };
33
+ const readCloudTimestamp = (value, field) => {
34
+ if (typeof value !== "string") throw invalidCloudResponse(field, "a canonical ISO timestamp");
35
+ const date = new Date(value);
36
+ if (Number.isNaN(date.getTime()) || date.toISOString() !== value) throw invalidCloudResponse(field, "a canonical ISO timestamp");
37
+ return date;
38
+ };
39
+ const readCloudJSONObject = (value, field) => readCloudRecord(value, field);
40
+ //#endregion
41
+ export { CloudResponseError, readCloudArray, readCloudBoolean, readCloudInteger, readCloudJSONObject, readCloudNullableString, readCloudRecord, readCloudString, readCloudTimestamp };
42
+
43
+ //# sourceMappingURL=cloudResponse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloudResponse.js","names":[],"sources":["../src/cloudResponse.ts"],"sourcesContent":["import type { ReadonlyJSONObject } from \"assistant-stream/utils\";\n\nexport class CloudResponseError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"CloudResponseError\";\n }\n}\n\nconst invalidCloudResponse = (field: string, expected: string) =>\n new CloudResponseError(\n `Invalid Assistant Cloud response for \"${field}\": expected ${expected}`,\n );\n\nexport const readCloudRecord = (\n value: unknown,\n field: string,\n): Record<string, unknown> => {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw invalidCloudResponse(field, \"an object\");\n }\n return value as Record<string, unknown>;\n};\n\nexport const readCloudArray = (value: unknown, field: string): unknown[] => {\n if (!Array.isArray(value)) throw invalidCloudResponse(field, \"an array\");\n return value;\n};\n\nexport const readCloudString = (value: unknown, field: string): string => {\n if (typeof value !== \"string\") throw invalidCloudResponse(field, \"a string\");\n return value;\n};\n\nexport const readCloudNullableString = (\n value: unknown,\n field: string,\n): string | null => {\n if (value === null) return null;\n return readCloudString(value, field);\n};\n\nexport const readCloudBoolean = (value: unknown, field: string): boolean => {\n if (typeof value !== \"boolean\") {\n throw invalidCloudResponse(field, \"a boolean\");\n }\n return value;\n};\n\nexport const readCloudInteger = (value: unknown, field: string): number => {\n if (!Number.isInteger(value)) throw invalidCloudResponse(field, \"an integer\");\n return value as number;\n};\n\nexport const readCloudTimestamp = (value: unknown, field: string): Date => {\n if (typeof value !== \"string\") {\n throw invalidCloudResponse(field, \"a canonical ISO timestamp\");\n }\n\n const date = new Date(value);\n // The round trip pins the backend DTO's Date#toISOString() wire contract.\n if (Number.isNaN(date.getTime()) || date.toISOString() !== value) {\n throw invalidCloudResponse(field, \"a canonical ISO timestamp\");\n }\n\n return date;\n};\n\nexport const readCloudJSONObject = (\n value: unknown,\n field: string,\n): ReadonlyJSONObject => readCloudRecord(value, field) as ReadonlyJSONObject;\n"],"mappings":";AAEA,IAAa,qBAAb,cAAwC,MAAM;CAC5C,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF;AAEA,MAAM,wBAAwB,OAAe,aAC3C,IAAI,mBACF,yCAAyC,MAAM,cAAc,UAC/D;AAEF,MAAa,mBACX,OACA,UAC4B;CAC5B,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GACpE,MAAM,qBAAqB,OAAO,WAAW;CAE/C,OAAO;AACT;AAEA,MAAa,kBAAkB,OAAgB,UAA6B;CAC1E,IAAI,CAAC,MAAM,QAAQ,KAAK,GAAG,MAAM,qBAAqB,OAAO,UAAU;CACvE,OAAO;AACT;AAEA,MAAa,mBAAmB,OAAgB,UAA0B;CACxE,IAAI,OAAO,UAAU,UAAU,MAAM,qBAAqB,OAAO,UAAU;CAC3E,OAAO;AACT;AAEA,MAAa,2BACX,OACA,UACkB;CAClB,IAAI,UAAU,MAAM,OAAO;CAC3B,OAAO,gBAAgB,OAAO,KAAK;AACrC;AAEA,MAAa,oBAAoB,OAAgB,UAA2B;CAC1E,IAAI,OAAO,UAAU,WACnB,MAAM,qBAAqB,OAAO,WAAW;CAE/C,OAAO;AACT;AAEA,MAAa,oBAAoB,OAAgB,UAA0B;CACzE,IAAI,CAAC,OAAO,UAAU,KAAK,GAAG,MAAM,qBAAqB,OAAO,YAAY;CAC5E,OAAO;AACT;AAEA,MAAa,sBAAsB,OAAgB,UAAwB;CACzE,IAAI,OAAO,UAAU,UACnB,MAAM,qBAAqB,OAAO,2BAA2B;CAG/D,MAAM,OAAO,IAAI,KAAK,KAAK;CAE3B,IAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,KAAK,KAAK,YAAY,MAAM,OACzD,MAAM,qBAAqB,OAAO,2BAA2B;CAG/D,OAAO;AACT;AAEA,MAAa,uBACX,OACA,UACuB,gBAAgB,OAAO,KAAK"}
package/dist/index.d.ts CHANGED
@@ -5,4 +5,5 @@ import { CloudMessage } from "./AssistantCloudThreadMessages.js";
5
5
  import { AssistantCloud } from "./AssistantCloud.js";
6
6
  import { CloudMessagePersistence } from "./CloudMessagePersistence.js";
7
7
  import { MessageFormatAdapter, createFormattedPersistence } from "./FormattedCloudPersistence.js";
8
- export { AssistantCloud, type AssistantCloudRunReport, type AssistantCloudTelemetryConfig, CloudAPIError, type CloudMessage, CloudMessagePersistence, type McpSamplingHandler, type MessageFormatAdapter, type SamplingCallData, createFormattedPersistence, createSamplingCollector, wrapSamplingHandler };
8
+ import { CloudResponseError } from "./cloudResponse.js";
9
+ export { AssistantCloud, type AssistantCloudRunReport, type AssistantCloudTelemetryConfig, CloudAPIError, type CloudMessage, CloudMessagePersistence, CloudResponseError, type McpSamplingHandler, type MessageFormatAdapter, type SamplingCallData, createFormattedPersistence, createSamplingCollector, wrapSamplingHandler };
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { CloudAPIError } from "./AssistantCloudAPI.js";
2
+ import { CloudResponseError } from "./cloudResponse.js";
2
3
  import { AssistantCloud } from "./AssistantCloud.js";
3
4
  import { CloudMessagePersistence } from "./CloudMessagePersistence.js";
4
5
  import { createFormattedPersistence } from "./FormattedCloudPersistence.js";
5
6
  import { createSamplingCollector, wrapSamplingHandler } from "./instrumentMcpSampling.js";
6
- export { AssistantCloud, CloudAPIError, CloudMessagePersistence, createFormattedPersistence, createSamplingCollector, wrapSamplingHandler };
7
+ export { AssistantCloud, CloudAPIError, CloudMessagePersistence, CloudResponseError, createFormattedPersistence, createSamplingCollector, wrapSamplingHandler };
@@ -1 +1 @@
1
- {"version":3,"file":"instrumentMcpSampling.d.ts","names":[],"sources":["../src/instrumentMcpSampling.ts"],"mappings":";;;;;;;;KAQY;EACV;EACA;EACA;EACA;EACA;EACA;;KAGU,sBACV,SAAS,uBACN,QAAQ;KAED;EACV;EACA;IACE;IACA;MAAqB;QAAU;;;IAC/B;KACC;;;KAIO;EACV;EACA;EACA;IACE;IACA;IACA;IACA;IACA;IACA;;GAED;;;;;;;;;;;;;;;;;;;;iBAqBa,oBACd,SAAS,oBACT,iBAAiB,MAAM,4BACtB;;;;;;;;;;;;;iBA6Ca;kBAGI;kBAAA"}
1
+ {"version":3,"file":"instrumentMcpSampling.d.ts","names":[],"sources":["../src/instrumentMcpSampling.ts"],"mappings":";;;;;;;;KAQY;EACV;EACA;EACA;EACA;EACA;EACA;;KAGU,sBACV,SAAS,uBACN,QAAQ;KAED;EACV;EACA;IACE;IACA;MAAqB;QAAU;;;IAC/B;KACC;;;KAIO;EACV;EACA;EACA;IACE;IACA;IACA;IACA;IACA;IACA;;GAED;;;;;;;;;;;;;;;;;;;;iBAqBa,oBACd,SAAS,oBACT,iBAAiB,MAAM,4BACtB;;;;;;;;;;;;;iBA6Ca;EAGI,UAAA,MAAA;kBAAA"}
@@ -1 +1 @@
1
- export { };
1
+ export {}
@@ -1,9 +1,7 @@
1
1
  import { vi } from "vitest";
2
2
  //#region src/tests/setup.ts
3
- const OriginalDate = globalThis.Date;
4
- const fixedDate = new OriginalDate("2023-01-01");
5
- globalThis.Date = vi.fn(() => fixedDate);
6
- globalThis.Date.now = vi.fn(() => fixedDate.getTime());
3
+ vi.useFakeTimers({ toFake: ["Date"] });
4
+ vi.setSystemTime(/* @__PURE__ */ new Date("2023-01-01T00:00:00.000Z"));
7
5
  //#endregion
8
6
 
9
7
  //# sourceMappingURL=setup.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"setup.js","names":[],"sources":["../../src/tests/setup.ts"],"sourcesContent":["// This file contains setup code for tests\nimport { vi } from \"vitest\";\n\n// Set up globalThis mocks if needed\n// Using a fixed date to avoid recursive calls\nconst OriginalDate = globalThis.Date;\nconst fixedDate = new OriginalDate(\"2023-01-01\");\nglobalThis.Date = vi.fn(() => fixedDate) as any;\nglobalThis.Date.now = vi.fn(() => fixedDate.getTime());\n\n// Add any other globalThis setup needed for tests\n"],"mappings":";;AAKA,MAAM,eAAe,WAAW;AAChC,MAAM,YAAY,IAAI,aAAa,YAAY;AAC/C,WAAW,OAAO,GAAG,SAAS,SAAS;AACvC,WAAW,KAAK,MAAM,GAAG,SAAS,UAAU,QAAQ,CAAC"}
1
+ {"version":3,"file":"setup.js","names":[],"sources":["../../src/tests/setup.ts"],"sourcesContent":["import { vi } from \"vitest\";\n\nvi.useFakeTimers({ toFake: [\"Date\"] });\nvi.setSystemTime(new Date(\"2023-01-01T00:00:00.000Z\"));\n"],"mappings":";;AAEA,GAAG,cAAc,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC;AACrC,GAAG,8BAAc,IAAI,KAAK,0BAA0B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assistant-cloud",
3
- "version": "0.1.35",
3
+ "version": "0.1.37",
4
4
  "description": "Cloud integration for assistant-ui",
5
5
  "keywords": [
6
6
  "assistant",
@@ -27,12 +27,12 @@
27
27
  ],
28
28
  "sideEffects": false,
29
29
  "dependencies": {
30
- "assistant-stream": "^0.3.26"
30
+ "assistant-stream": "^0.3.28"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^26.1.1",
34
34
  "vitest": "^4.1.10",
35
- "@assistant-ui/x-buildutils": "0.0.18"
35
+ "@assistant-ui/x-buildutils": "0.0.20"
36
36
  },
37
37
  "publishConfig": {
38
38
  "access": "public",
@@ -4,12 +4,14 @@ import {
4
4
  type AssistantCloudTelemetryConfig,
5
5
  } from "./AssistantCloudAPI";
6
6
  import { AssistantCloudAuthTokens } from "./AssistantCloudAuthTokens";
7
+ import { AssistantCloudProjects } from "./AssistantCloudProjects";
7
8
  import { AssistantCloudRuns } from "./AssistantCloudRuns";
8
9
  import { AssistantCloudThreads } from "./AssistantCloudThreads";
9
10
  import { AssistantCloudFiles } from "./AssistantCloudFiles";
10
11
 
11
12
  export class AssistantCloud {
12
13
  public readonly threads;
14
+ public readonly projects;
13
15
  public readonly auth;
14
16
  public readonly runs;
15
17
  public readonly files;
@@ -18,6 +20,7 @@ export class AssistantCloud {
18
20
  constructor(config: AssistantCloudConfig) {
19
21
  const api = new AssistantCloudAPI(config);
20
22
  this.threads = new AssistantCloudThreads(api);
23
+ this.projects = new AssistantCloudProjects(api);
21
24
  this.auth = {
22
25
  tokens: new AssistantCloudAuthTokens(api),
23
26
  };
@@ -49,11 +49,11 @@ export type AssistantCloudConfig = (
49
49
  };
50
50
 
51
51
  export class CloudAPIError extends Error {
52
- constructor(
53
- message: string,
54
- public readonly status: number,
55
- ) {
52
+ public readonly status: number;
53
+
54
+ constructor(message: string, status: number) {
56
55
  super(message);
56
+ this.status = status;
57
57
  this.name = "CloudAPIError";
58
58
  }
59
59
  }
@@ -65,26 +65,31 @@ type MakeRequestOptions = {
65
65
  body?: object | undefined;
66
66
  };
67
67
 
68
+ const normalizeBaseUrl = (baseUrl: string) => {
69
+ if (!baseUrl || !baseUrl.endsWith("/")) return baseUrl;
70
+ return baseUrl.slice(0, -1);
71
+ };
72
+
68
73
  export class AssistantCloudAPI {
69
74
  public _auth: AssistantCloudAuthStrategy;
70
75
  public _baseUrl;
71
76
 
72
77
  constructor(config: AssistantCloudConfig) {
73
78
  if ("authToken" in config) {
74
- this._baseUrl = config.baseUrl;
79
+ this._baseUrl = normalizeBaseUrl(config.baseUrl);
75
80
  this._auth = new AssistantCloudJWTAuthStrategy(config.authToken);
76
81
  } else if ("apiKey" in config) {
77
- this._baseUrl = (
78
- config.baseUrl ?? "https://backend.assistant-api.com"
79
- ).replace(/\/$/, "");
82
+ this._baseUrl = normalizeBaseUrl(
83
+ config.baseUrl ?? "https://backend.assistant-api.com",
84
+ );
80
85
  this._auth = new AssistantCloudAPIKeyAuthStrategy(
81
86
  config.apiKey,
82
87
  config.userId,
83
88
  config.workspaceId,
84
89
  );
85
90
  } else if ("anonymous" in config) {
86
- this._baseUrl = config.baseUrl;
87
- this._auth = new AssistantCloudAnonymousAuthStrategy(config.baseUrl);
91
+ this._baseUrl = normalizeBaseUrl(config.baseUrl);
92
+ this._auth = new AssistantCloudAnonymousAuthStrategy(this._baseUrl);
88
93
  } else {
89
94
  throw new Error(
90
95
  "Invalid configuration: Must provide authToken, apiKey, or anonymous configuration",
@@ -39,6 +39,7 @@ export class AssistantCloudJWTAuthStrategy implements AssistantCloudAuthStrategy
39
39
 
40
40
  private cachedToken: string | null = null;
41
41
  private tokenExpiry: number | null = null;
42
+ private tokenRequest: Promise<Record<string, string> | false> | null = null;
42
43
  #authTokenCallback: () => Promise<string | null>;
43
44
 
44
45
  constructor(authTokenCallback: () => Promise<string | null>) {
@@ -57,14 +58,28 @@ export class AssistantCloudJWTAuthStrategy implements AssistantCloudAuthStrategy
57
58
  return { Authorization: `Bearer ${this.cachedToken}` };
58
59
  }
59
60
 
60
- // Fetch a new token via the callback
61
- const newToken = await this.#authTokenCallback();
62
- if (!newToken) return false;
61
+ if (!this.tokenRequest) {
62
+ this.tokenRequest = this.fetchAuthHeaders();
63
+ }
64
+
65
+ const tokenRequest = this.tokenRequest;
66
+ try {
67
+ return await tokenRequest;
68
+ } finally {
69
+ if (this.tokenRequest === tokenRequest) {
70
+ this.tokenRequest = null;
71
+ }
72
+ }
73
+ }
74
+
75
+ private async fetchAuthHeaders(): Promise<Record<string, string> | false> {
76
+ const token = await this.#authTokenCallback();
77
+ if (!token) return false;
63
78
 
64
- this.cachedToken = newToken;
65
- this.tokenExpiry = getJwtExpiry(newToken);
79
+ this.cachedToken = token;
80
+ this.tokenExpiry = getJwtExpiry(token);
66
81
 
67
- return { Authorization: `Bearer ${newToken}` };
82
+ return { Authorization: `Bearer ${token}` };
68
83
  }
69
84
 
70
85
  public readAuthHeaders(headers: Headers) {
@@ -5,7 +5,11 @@ type AssistantCloudAuthTokensCreateResponse = {
5
5
  };
6
6
 
7
7
  export class AssistantCloudAuthTokens {
8
- constructor(private cloud: AssistantCloudAPI) {}
8
+ private cloud: AssistantCloudAPI;
9
+
10
+ constructor(cloud: AssistantCloudAPI) {
11
+ this.cloud = cloud;
12
+ }
9
13
 
10
14
  public async create(): Promise<AssistantCloudAuthTokensCreateResponse> {
11
15
  return this.cloud.makeRequest("/auth/tokens", { method: "POST" });
@@ -23,7 +23,11 @@ type GeneratePresignedUploadUrlResponse = {
23
23
  };
24
24
 
25
25
  export class AssistantCloudFiles {
26
- constructor(private cloud: AssistantCloudAPI) {}
26
+ private cloud: AssistantCloudAPI;
27
+
28
+ constructor(cloud: AssistantCloudAPI) {
29
+ this.cloud = cloud;
30
+ }
27
31
 
28
32
  public async pdfToImages(
29
33
  body: PdfToImagesRequestBody,
@@ -0,0 +1,44 @@
1
+ import type { AssistantCloudAPI } from "./AssistantCloudAPI";
2
+ import {
3
+ decodeCloudMessage,
4
+ type CloudMessage,
5
+ } from "./AssistantCloudThreadMessages";
6
+ import { readCloudArray, readCloudRecord } from "./cloudResponse";
7
+
8
+ type AssistantCloudProjectThreadMessageListQuery = {
9
+ format?: string;
10
+ limit?: number;
11
+ after?: string;
12
+ };
13
+
14
+ type AssistantCloudProjectThreadMessageListResponse = {
15
+ messages: CloudMessage[];
16
+ };
17
+
18
+ export class AssistantCloudProjectThreadMessages {
19
+ private cloud: AssistantCloudAPI;
20
+
21
+ constructor(cloud: AssistantCloudAPI) {
22
+ this.cloud = cloud;
23
+ }
24
+
25
+ public async list(
26
+ threadId: string,
27
+ query?: AssistantCloudProjectThreadMessageListQuery,
28
+ ): Promise<AssistantCloudProjectThreadMessageListResponse> {
29
+ const response = readCloudRecord(
30
+ await this.cloud.makeRequest(
31
+ `/projects/threads/${encodeURIComponent(threadId)}/messages`,
32
+ { query },
33
+ ),
34
+ "project thread message list response",
35
+ );
36
+ const messages = readCloudArray(response.messages, "messages");
37
+
38
+ return {
39
+ messages: messages.map((message, index) =>
40
+ decodeCloudMessage(message, `messages[${index}]`),
41
+ ),
42
+ };
43
+ }
44
+ }
@@ -0,0 +1,50 @@
1
+ import type { AssistantCloudAPI } from "./AssistantCloudAPI";
2
+ import { AssistantCloudProjectThreadMessages } from "./AssistantCloudProjectThreadMessages";
3
+ import { decodeCloudThread, type CloudThread } from "./AssistantCloudThreads";
4
+ import { readCloudArray, readCloudRecord } from "./cloudResponse";
5
+
6
+ type AssistantCloudProjectThreadsListQuery = {
7
+ is_archived?: boolean;
8
+ limit?: number;
9
+ after?: string;
10
+ };
11
+
12
+ type AssistantCloudProjectThreadsListResponse = {
13
+ threads: CloudThread[];
14
+ };
15
+
16
+ export class AssistantCloudProjectThreads {
17
+ public readonly messages: AssistantCloudProjectThreadMessages;
18
+
19
+ private cloud: AssistantCloudAPI;
20
+
21
+ constructor(cloud: AssistantCloudAPI) {
22
+ this.cloud = cloud;
23
+ this.messages = new AssistantCloudProjectThreadMessages(cloud);
24
+ }
25
+
26
+ public async list(
27
+ query?: AssistantCloudProjectThreadsListQuery,
28
+ ): Promise<AssistantCloudProjectThreadsListResponse> {
29
+ const response = readCloudRecord(
30
+ await this.cloud.makeRequest("/projects/threads", {
31
+ query: {
32
+ ...query,
33
+ // The shared query serializer drops `false`; the wire accepts the
34
+ // string form, so the archive filter survives explicitly.
35
+ ...(query?.is_archived !== undefined
36
+ ? { is_archived: String(query.is_archived) }
37
+ : {}),
38
+ },
39
+ }),
40
+ "project thread list response",
41
+ );
42
+ const threads = readCloudArray(response.threads, "threads");
43
+
44
+ return {
45
+ threads: threads.map((thread, index) =>
46
+ decodeCloudThread(thread, `threads[${index}]`),
47
+ ),
48
+ };
49
+ }
50
+ }
@@ -0,0 +1,10 @@
1
+ import type { AssistantCloudAPI } from "./AssistantCloudAPI";
2
+ import { AssistantCloudProjectThreads } from "./AssistantCloudProjectThreads";
3
+
4
+ export class AssistantCloudProjects {
5
+ public readonly threads: AssistantCloudProjectThreads;
6
+
7
+ constructor(cloud: AssistantCloudAPI) {
8
+ this.threads = new AssistantCloudProjectThreads(cloud);
9
+ }
10
+ }
@@ -48,7 +48,11 @@ export type AssistantCloudRunReport = {
48
48
  };
49
49
 
50
50
  export class AssistantCloudRuns {
51
- constructor(private cloud: AssistantCloudAPI) {}
51
+ private cloud: AssistantCloudAPI;
52
+
53
+ constructor(cloud: AssistantCloudAPI) {
54
+ this.cloud = cloud;
55
+ }
52
56
 
53
57
  public __internal_getAssistantOptions(assistantId: string) {
54
58
  return {
@@ -0,0 +1,41 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type { AssistantCloudAPI } from "./AssistantCloudAPI";
3
+ import { AssistantCloudThreadMessages } from "./AssistantCloudThreadMessages";
4
+
5
+ const createCloudThreadMessages = () => {
6
+ const makeRequest = vi.fn();
7
+ const api = { makeRequest } as unknown as AssistantCloudAPI;
8
+ return {
9
+ messages: new AssistantCloudThreadMessages(api),
10
+ makeRequest,
11
+ };
12
+ };
13
+
14
+ describe("AssistantCloudThreadMessages responses", () => {
15
+ it("decodes canonical message responses without changing content", async () => {
16
+ const { messages, makeRequest } = createCloudThreadMessages();
17
+ makeRequest.mockResolvedValue({
18
+ messages: [
19
+ {
20
+ id: "message-1",
21
+ parent_id: null,
22
+ height: 0,
23
+ created_at: "2026-07-16T13:00:00.000Z",
24
+ updated_at: "2026-07-16T13:05:00.987Z",
25
+ format: "aui/v0",
26
+ content: { created_at: "leave-this-string-untouched" },
27
+ },
28
+ ],
29
+ });
30
+
31
+ const result = await messages.list("thread-1");
32
+ const message = result.messages[0]!;
33
+
34
+ expect(message.created_at).toBeInstanceOf(Date);
35
+ expect(message.updated_at).toBeInstanceOf(Date);
36
+ expect(message.updated_at.toISOString()).toBe("2026-07-16T13:05:00.987Z");
37
+ expect(message.content).toEqual({
38
+ created_at: "leave-this-string-untouched",
39
+ });
40
+ });
41
+ });
@@ -1,5 +1,14 @@
1
1
  import type { ReadonlyJSONObject } from "assistant-stream/utils";
2
2
  import type { AssistantCloudAPI } from "./AssistantCloudAPI";
3
+ import {
4
+ readCloudArray,
5
+ readCloudInteger,
6
+ readCloudJSONObject,
7
+ readCloudNullableString,
8
+ readCloudRecord,
9
+ readCloudString,
10
+ readCloudTimestamp,
11
+ } from "./cloudResponse";
3
12
 
4
13
  export type CloudMessage = {
5
14
  id: string;
@@ -33,17 +42,47 @@ type AssistantCloudThreadMessageUpdateBody = {
33
42
  content: ReadonlyJSONObject;
34
43
  };
35
44
 
45
+ export const decodeCloudMessage = (
46
+ value: unknown,
47
+ field: string,
48
+ ): CloudMessage => {
49
+ const message = readCloudRecord(value, field);
50
+ return {
51
+ id: readCloudString(message.id, `${field}.id`),
52
+ parent_id: readCloudNullableString(message.parent_id, `${field}.parent_id`),
53
+ height: readCloudInteger(message.height, `${field}.height`),
54
+ created_at: readCloudTimestamp(message.created_at, `${field}.created_at`),
55
+ updated_at: readCloudTimestamp(message.updated_at, `${field}.updated_at`),
56
+ format: readCloudString(message.format, `${field}.format`),
57
+ content: readCloudJSONObject(message.content, `${field}.content`),
58
+ };
59
+ };
60
+
36
61
  export class AssistantCloudThreadMessages {
37
- constructor(private cloud: AssistantCloudAPI) {}
62
+ private cloud: AssistantCloudAPI;
63
+
64
+ constructor(cloud: AssistantCloudAPI) {
65
+ this.cloud = cloud;
66
+ }
38
67
 
39
68
  public async list(
40
69
  threadId: string,
41
70
  query?: AssistantCloudThreadMessageListQuery,
42
71
  ): Promise<AssistantCloudThreadMessageListResponse> {
43
- return this.cloud.makeRequest(
44
- `/threads/${encodeURIComponent(threadId)}/messages`,
45
- { query },
72
+ const response = readCloudRecord(
73
+ await this.cloud.makeRequest(
74
+ `/threads/${encodeURIComponent(threadId)}/messages`,
75
+ { query },
76
+ ),
77
+ "thread message list response",
46
78
  );
79
+ const messages = readCloudArray(response.messages, "messages");
80
+
81
+ return {
82
+ messages: messages.map((message, index) =>
83
+ decodeCloudMessage(message, `messages[${index}]`),
84
+ ),
85
+ };
47
86
  }
48
87
 
49
88
  public async create(