assistant-cloud 0.1.40 → 0.1.42

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 (57) hide show
  1. package/README.md +3 -3
  2. package/dist/AssistantCloud.js +2 -2
  3. package/dist/AssistantCloud.js.map +1 -1
  4. package/dist/AssistantCloudAPI.d.ts.map +1 -1
  5. package/dist/AssistantCloudAPI.js +5 -5
  6. package/dist/AssistantCloudAPI.js.map +1 -1
  7. package/dist/AssistantCloudAuthStrategy.d.ts.map +1 -1
  8. package/dist/AssistantCloudAuthStrategy.js +35 -12
  9. package/dist/AssistantCloudAuthStrategy.js.map +1 -1
  10. package/dist/AssistantCloudAuthTokens.d.ts.map +1 -1
  11. package/dist/AssistantCloudAuthTokens.js +3 -1
  12. package/dist/AssistantCloudAuthTokens.js.map +1 -1
  13. package/dist/AssistantCloudFiles.d.ts.map +1 -1
  14. package/dist/AssistantCloudFiles.js +16 -4
  15. package/dist/AssistantCloudFiles.js.map +1 -1
  16. package/dist/AssistantCloudRuns.d.ts +3 -13
  17. package/dist/AssistantCloudRuns.d.ts.map +1 -1
  18. package/dist/AssistantCloudRuns.js +10 -2
  19. package/dist/AssistantCloudRuns.js.map +1 -1
  20. package/dist/AssistantCloudThreadMessages.d.ts.map +1 -1
  21. package/dist/AssistantCloudThreadMessages.js +3 -2
  22. package/dist/AssistantCloudThreadMessages.js.map +1 -1
  23. package/dist/AssistantCloudThreads.d.ts.map +1 -1
  24. package/dist/AssistantCloudThreads.js +3 -2
  25. package/dist/AssistantCloudThreads.js.map +1 -1
  26. package/dist/generateThreadTitle.d.ts +15 -0
  27. package/dist/generateThreadTitle.d.ts.map +1 -0
  28. package/dist/generateThreadTitle.js +25 -0
  29. package/dist/generateThreadTitle.js.map +1 -0
  30. package/dist/index.d.ts +3 -1
  31. package/dist/index.js +3 -1
  32. package/dist/runTelemetry.d.ts +54 -0
  33. package/dist/runTelemetry.d.ts.map +1 -0
  34. package/dist/runTelemetry.js +79 -0
  35. package/dist/runTelemetry.js.map +1 -0
  36. package/package.json +5 -5
  37. package/src/AssistantCloud.ts +1 -1
  38. package/src/AssistantCloudAPI.ts +9 -8
  39. package/src/AssistantCloudAuthStrategy.ts +49 -12
  40. package/src/AssistantCloudAuthTokens.test.ts +30 -0
  41. package/src/AssistantCloudAuthTokens.ts +7 -1
  42. package/src/AssistantCloudFiles.test.ts +71 -0
  43. package/src/AssistantCloudFiles.ts +56 -10
  44. package/src/AssistantCloudRuns.ts +37 -15
  45. package/src/AssistantCloudThreadMessages.test.ts +20 -0
  46. package/src/AssistantCloudThreadMessages.ts +10 -3
  47. package/src/AssistantCloudThreads.test.ts +17 -0
  48. package/src/AssistantCloudThreads.ts +6 -1
  49. package/src/generateThreadTitle.test.ts +71 -0
  50. package/src/generateThreadTitle.ts +38 -0
  51. package/src/index.ts +10 -0
  52. package/src/runTelemetry.test.ts +130 -0
  53. package/src/runTelemetry.ts +140 -0
  54. package/src/tests/AssistantCloud.test.ts +39 -0
  55. package/src/tests/AssistantCloudAPI.test.ts +25 -0
  56. package/src/tests/AssistantCloudAuthStrategy.test.ts +278 -5
  57. package/src/tests/AssistantCloudRuns.test.ts +115 -0
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { McpSamplingHandler, SamplingCallData, createSamplingCollector, wrapSamplingHandler } from "./instrumentMcpSampling.js";
2
+ import { AssistantCloudRunReportToolCall, RunTelemetryToolCallInit, RunTelemetryUsage, RunTelemetryUsageInit, createRunTelemetryToolCall, normalizeRunTelemetryUsage, truncateRunTelemetryText } from "./runTelemetry.js";
2
3
  import { AssistantCloudRunReport } from "./AssistantCloudRuns.js";
3
4
  import { AssistantCloudTelemetryConfig, CloudAPIError } from "./AssistantCloudAPI.js";
4
5
  import { CloudMessage } from "./AssistantCloudThreadMessages.js";
@@ -6,4 +7,5 @@ import { AssistantCloud } from "./AssistantCloud.js";
6
7
  import { CloudMessagePersistence } from "./CloudMessagePersistence.js";
7
8
  import { MessageFormatAdapter, createFormattedPersistence } from "./FormattedCloudPersistence.js";
8
9
  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 };
10
+ import { generateThreadTitle } from "./generateThreadTitle.js";
11
+ export { AssistantCloud, type AssistantCloudRunReport, type AssistantCloudRunReportToolCall, type AssistantCloudTelemetryConfig, CloudAPIError, type CloudMessage, CloudMessagePersistence, CloudResponseError, type McpSamplingHandler, type MessageFormatAdapter, type RunTelemetryToolCallInit, type RunTelemetryUsage, type RunTelemetryUsageInit, type SamplingCallData, createFormattedPersistence, createRunTelemetryToolCall, createSamplingCollector, generateThreadTitle, normalizeRunTelemetryUsage, truncateRunTelemetryText, wrapSamplingHandler };
package/dist/index.js CHANGED
@@ -3,5 +3,7 @@ import { CloudAPIError } from "./AssistantCloudAPI.js";
3
3
  import { AssistantCloud } from "./AssistantCloud.js";
4
4
  import { CloudMessagePersistence } from "./CloudMessagePersistence.js";
5
5
  import { createFormattedPersistence } from "./FormattedCloudPersistence.js";
6
+ import { generateThreadTitle } from "./generateThreadTitle.js";
7
+ import { createRunTelemetryToolCall, normalizeRunTelemetryUsage, truncateRunTelemetryText } from "./runTelemetry.js";
6
8
  import { createSamplingCollector, wrapSamplingHandler } from "./instrumentMcpSampling.js";
7
- export { AssistantCloud, CloudAPIError, CloudMessagePersistence, CloudResponseError, createFormattedPersistence, createSamplingCollector, wrapSamplingHandler };
9
+ export { AssistantCloud, CloudAPIError, CloudMessagePersistence, CloudResponseError, createFormattedPersistence, createRunTelemetryToolCall, createSamplingCollector, generateThreadTitle, normalizeRunTelemetryUsage, truncateRunTelemetryText, wrapSamplingHandler };
@@ -0,0 +1,54 @@
1
+ import { SamplingCallData } from "./instrumentMcpSampling.js";
2
+ //#region src/runTelemetry.d.ts
3
+ type AssistantCloudRunReportToolCall = {
4
+ tool_name: string;
5
+ tool_call_id: string;
6
+ tool_args?: string;
7
+ tool_result?: string;
8
+ tool_source?: "mcp" | "frontend" | "backend";
9
+ start_ms?: number;
10
+ end_ms?: number;
11
+ sampling_calls?: SamplingCallData[];
12
+ };
13
+ /**
14
+ * Clamps a string to the size the runs endpoint accepts for a single span
15
+ * field.
16
+ */
17
+ declare function truncateRunTelemetryText(value: string): string;
18
+ type RunTelemetryToolCallInit = {
19
+ toolName: string;
20
+ toolCallId: string;
21
+ args?: unknown;
22
+ /**
23
+ * Pre-serialized arguments, used in place of serializing `args`. Values over
24
+ * the span size are clamped before they are included in the report.
25
+ */
26
+ argsText?: string | undefined;
27
+ result?: unknown;
28
+ toolSource?: "mcp" | "frontend" | "backend" | undefined;
29
+ };
30
+ /**
31
+ * Serializes one tool call into the shape the runs endpoint accepts. An `mcp`
32
+ * source has its result summarized, because MCP content blocks carry inline
33
+ * base64 image and audio payloads that would otherwise dominate the report.
34
+ */
35
+ declare function createRunTelemetryToolCall(init: RunTelemetryToolCallInit): AssistantCloudRunReportToolCall;
36
+ type RunTelemetryUsage = {
37
+ inputTokens?: number;
38
+ outputTokens?: number;
39
+ reasoningTokens?: number;
40
+ cachedInputTokens?: number;
41
+ };
42
+ type RunTelemetryUsageInit = RunTelemetryUsage & {
43
+ promptTokens?: number;
44
+ completionTokens?: number;
45
+ };
46
+ /**
47
+ * Resolves the token counts a provider reports under either the current or the
48
+ * legacy prompt/completion names. Returns undefined when no count is present,
49
+ * so callers can tell an empty usage object from a zeroed one.
50
+ */
51
+ declare function normalizeRunTelemetryUsage(usage: RunTelemetryUsageInit): RunTelemetryUsage | undefined;
52
+ //#endregion
53
+ export { AssistantCloudRunReportToolCall, RunTelemetryToolCallInit, RunTelemetryUsage, RunTelemetryUsageInit, createRunTelemetryToolCall, normalizeRunTelemetryUsage, truncateRunTelemetryText };
54
+ //# sourceMappingURL=runTelemetry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runTelemetry.d.ts","names":[],"sources":["../src/runTelemetry.ts"],"mappings":";;KAMY;EACV;EACA;EACA;EACA;EACA;EACA;EACA;EACA,iBAAiB;;;;;;iBAOH,yBAAyB;KAwC7B;EACV;EACA;EACA;;;;;EAKA;EACA;EACA;;;;;;;iBAQc,2BACd,MAAM,2BACL;KAgBS;EACV;EACA;EACA;EACA;;KAGU,wBAAwB;EAClC;EACA;;;;;;;iBAQc,2BACd,OAAO,wBACN"}
@@ -0,0 +1,79 @@
1
+ //#region src/runTelemetry.ts
2
+ const MAX_TELEMETRY_TEXT_LENGTH = 5e4;
3
+ const BASE64_PATTERN = /^[A-Za-z0-9+/]{100,}={0,2}$/;
4
+ /**
5
+ * Clamps a string to the size the runs endpoint accepts for a single span
6
+ * field.
7
+ */
8
+ function truncateRunTelemetryText(value) {
9
+ if (value.length <= MAX_TELEMETRY_TEXT_LENGTH) return value;
10
+ return value.slice(0, MAX_TELEMETRY_TEXT_LENGTH);
11
+ }
12
+ function safeStringify(value) {
13
+ if (value == null) return void 0;
14
+ try {
15
+ return truncateRunTelemetryText(JSON.stringify(value));
16
+ } catch {
17
+ return;
18
+ }
19
+ }
20
+ function summarizeMcpResult(value) {
21
+ if (value == null) return void 0;
22
+ try {
23
+ const parsed = typeof value === "string" ? JSON.parse(value) : value;
24
+ if (Array.isArray(parsed)) {
25
+ const summarized = parsed.map((item) => {
26
+ if (item && typeof item === "object" && item.type) {
27
+ if ((item.type === "image" || item.type === "audio") && typeof item.data === "string" && BASE64_PATTERN.test(item.data.slice(0, 200))) {
28
+ const sizeKB = (item.data.length * 3 / 4 / 1024).toFixed(1);
29
+ return {
30
+ ...item,
31
+ data: `[${item.type}: ${sizeKB}KB]`
32
+ };
33
+ }
34
+ }
35
+ return item;
36
+ });
37
+ return truncateRunTelemetryText(JSON.stringify(summarized));
38
+ }
39
+ } catch {}
40
+ return safeStringify(value);
41
+ }
42
+ /**
43
+ * Serializes one tool call into the shape the runs endpoint accepts. An `mcp`
44
+ * source has its result summarized, because MCP content blocks carry inline
45
+ * base64 image and audio payloads that would otherwise dominate the report.
46
+ */
47
+ function createRunTelemetryToolCall(init) {
48
+ const { toolName, toolCallId, args, argsText, result, toolSource } = init;
49
+ const call = {
50
+ tool_name: toolName,
51
+ tool_call_id: toolCallId
52
+ };
53
+ const toolArgs = argsText != null ? truncateRunTelemetryText(argsText) : safeStringify(args);
54
+ if (toolArgs !== void 0) call.tool_args = toolArgs;
55
+ const toolResult = toolSource === "mcp" ? summarizeMcpResult(result) : safeStringify(result);
56
+ if (toolResult !== void 0) call.tool_result = toolResult;
57
+ if (toolSource) call.tool_source = toolSource;
58
+ return call;
59
+ }
60
+ /**
61
+ * Resolves the token counts a provider reports under either the current or the
62
+ * legacy prompt/completion names. Returns undefined when no count is present,
63
+ * so callers can tell an empty usage object from a zeroed one.
64
+ */
65
+ function normalizeRunTelemetryUsage(usage) {
66
+ const inputTokens = usage.inputTokens ?? usage.promptTokens;
67
+ const outputTokens = usage.outputTokens ?? usage.completionTokens;
68
+ if (inputTokens == null && outputTokens == null && usage.reasoningTokens == null && usage.cachedInputTokens == null) return;
69
+ return {
70
+ ...inputTokens != null ? { inputTokens } : void 0,
71
+ ...outputTokens != null ? { outputTokens } : void 0,
72
+ ...usage.reasoningTokens != null ? { reasoningTokens: usage.reasoningTokens } : void 0,
73
+ ...usage.cachedInputTokens != null ? { cachedInputTokens: usage.cachedInputTokens } : void 0
74
+ };
75
+ }
76
+ //#endregion
77
+ export { createRunTelemetryToolCall, normalizeRunTelemetryUsage, truncateRunTelemetryText };
78
+
79
+ //# sourceMappingURL=runTelemetry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runTelemetry.js","names":[],"sources":["../src/runTelemetry.ts"],"sourcesContent":["import type { SamplingCallData } from \"./instrumentMcpSampling\";\n\nconst MAX_TELEMETRY_TEXT_LENGTH = 50_000;\n\nconst BASE64_PATTERN = /^[A-Za-z0-9+/]{100,}={0,2}$/;\n\nexport type AssistantCloudRunReportToolCall = {\n tool_name: string;\n tool_call_id: string;\n tool_args?: string;\n tool_result?: string;\n tool_source?: \"mcp\" | \"frontend\" | \"backend\";\n start_ms?: number;\n end_ms?: number;\n sampling_calls?: SamplingCallData[];\n};\n\n/**\n * Clamps a string to the size the runs endpoint accepts for a single span\n * field.\n */\nexport function truncateRunTelemetryText(value: string): string {\n if (value.length <= MAX_TELEMETRY_TEXT_LENGTH) return value;\n return value.slice(0, MAX_TELEMETRY_TEXT_LENGTH);\n}\n\nfunction safeStringify(value: unknown): string | undefined {\n if (value == null) return undefined;\n try {\n return truncateRunTelemetryText(JSON.stringify(value));\n } catch {\n return undefined;\n }\n}\n\nfunction summarizeMcpResult(value: unknown): string | undefined {\n if (value == null) return undefined;\n try {\n const parsed = typeof value === \"string\" ? JSON.parse(value) : value;\n if (Array.isArray(parsed)) {\n const summarized = parsed.map((item) => {\n if (item && typeof item === \"object\" && item.type) {\n if (\n (item.type === \"image\" || item.type === \"audio\") &&\n typeof item.data === \"string\" &&\n BASE64_PATTERN.test(item.data.slice(0, 200))\n ) {\n const sizeKB = ((item.data.length * 3) / 4 / 1024).toFixed(1);\n return { ...item, data: `[${item.type}: ${sizeKB}KB]` };\n }\n }\n return item;\n });\n return truncateRunTelemetryText(JSON.stringify(summarized));\n }\n } catch {\n // not JSON array, fall through\n }\n return safeStringify(value);\n}\n\nexport type RunTelemetryToolCallInit = {\n toolName: string;\n toolCallId: string;\n args?: unknown;\n /**\n * Pre-serialized arguments, used in place of serializing `args`. Values over\n * the span size are clamped before they are included in the report.\n */\n argsText?: string | undefined;\n result?: unknown;\n toolSource?: \"mcp\" | \"frontend\" | \"backend\" | undefined;\n};\n\n/**\n * Serializes one tool call into the shape the runs endpoint accepts. An `mcp`\n * source has its result summarized, because MCP content blocks carry inline\n * base64 image and audio payloads that would otherwise dominate the report.\n */\nexport function createRunTelemetryToolCall(\n init: RunTelemetryToolCallInit,\n): AssistantCloudRunReportToolCall {\n const { toolName, toolCallId, args, argsText, result, toolSource } = init;\n const call: AssistantCloudRunReportToolCall = {\n tool_name: toolName,\n tool_call_id: toolCallId,\n };\n const toolArgs =\n argsText != null ? truncateRunTelemetryText(argsText) : safeStringify(args);\n if (toolArgs !== undefined) call.tool_args = toolArgs;\n const toolResult =\n toolSource === \"mcp\" ? summarizeMcpResult(result) : safeStringify(result);\n if (toolResult !== undefined) call.tool_result = toolResult;\n if (toolSource) call.tool_source = toolSource;\n return call;\n}\n\nexport type RunTelemetryUsage = {\n inputTokens?: number;\n outputTokens?: number;\n reasoningTokens?: number;\n cachedInputTokens?: number;\n};\n\nexport type RunTelemetryUsageInit = RunTelemetryUsage & {\n promptTokens?: number;\n completionTokens?: number;\n};\n\n/**\n * Resolves the token counts a provider reports under either the current or the\n * legacy prompt/completion names. Returns undefined when no count is present,\n * so callers can tell an empty usage object from a zeroed one.\n */\nexport function normalizeRunTelemetryUsage(\n usage: RunTelemetryUsageInit,\n): RunTelemetryUsage | undefined {\n const inputTokens = usage.inputTokens ?? usage.promptTokens;\n const outputTokens = usage.outputTokens ?? usage.completionTokens;\n\n if (\n inputTokens == null &&\n outputTokens == null &&\n usage.reasoningTokens == null &&\n usage.cachedInputTokens == null\n ) {\n return undefined;\n }\n\n return {\n ...(inputTokens != null ? { inputTokens } : undefined),\n ...(outputTokens != null ? { outputTokens } : undefined),\n ...(usage.reasoningTokens != null\n ? { reasoningTokens: usage.reasoningTokens }\n : undefined),\n ...(usage.cachedInputTokens != null\n ? { cachedInputTokens: usage.cachedInputTokens }\n : undefined),\n };\n}\n"],"mappings":";AAEA,MAAM,4BAA4B;AAElC,MAAM,iBAAiB;;;;;AAiBvB,SAAgB,yBAAyB,OAAuB;CAC9D,IAAI,MAAM,UAAU,2BAA2B,OAAO;CACtD,OAAO,MAAM,MAAM,GAAG,yBAAyB;AACjD;AAEA,SAAS,cAAc,OAAoC;CACzD,IAAI,SAAS,MAAM,OAAO,KAAA;CAC1B,IAAI;EACF,OAAO,yBAAyB,KAAK,UAAU,KAAK,CAAC;CACvD,QAAQ;EACN;CACF;AACF;AAEA,SAAS,mBAAmB,OAAoC;CAC9D,IAAI,SAAS,MAAM,OAAO,KAAA;CAC1B,IAAI;EACF,MAAM,SAAS,OAAO,UAAU,WAAW,KAAK,MAAM,KAAK,IAAI;EAC/D,IAAI,MAAM,QAAQ,MAAM,GAAG;GACzB,MAAM,aAAa,OAAO,KAAK,SAAS;IACtC,IAAI,QAAQ,OAAO,SAAS,YAAY,KAAK,MAExC;UAAA,KAAK,SAAS,WAAW,KAAK,SAAS,YACxC,OAAO,KAAK,SAAS,YACrB,eAAe,KAAK,KAAK,KAAK,MAAM,GAAG,GAAG,CAAC,GAC3C;MACA,MAAM,UAAW,KAAK,KAAK,SAAS,IAAK,IAAI,KAAA,CAAM,QAAQ,CAAC;MAC5D,OAAO;OAAE,GAAG;OAAM,MAAM,IAAI,KAAK,KAAK,IAAI,OAAO;MAAK;KACxD;;IAEF,OAAO;GACT,CAAC;GACD,OAAO,yBAAyB,KAAK,UAAU,UAAU,CAAC;EAC5D;CACF,QAAQ,CAER;CACA,OAAO,cAAc,KAAK;AAC5B;;;;;;AAoBA,SAAgB,2BACd,MACiC;CACjC,MAAM,EAAE,UAAU,YAAY,MAAM,UAAU,QAAQ,eAAe;CACrE,MAAM,OAAwC;EAC5C,WAAW;EACX,cAAc;CAChB;CACA,MAAM,WACJ,YAAY,OAAO,yBAAyB,QAAQ,IAAI,cAAc,IAAI;CAC5E,IAAI,aAAa,KAAA,GAAW,KAAK,YAAY;CAC7C,MAAM,aACJ,eAAe,QAAQ,mBAAmB,MAAM,IAAI,cAAc,MAAM;CAC1E,IAAI,eAAe,KAAA,GAAW,KAAK,cAAc;CACjD,IAAI,YAAY,KAAK,cAAc;CACnC,OAAO;AACT;;;;;;AAmBA,SAAgB,2BACd,OAC+B;CAC/B,MAAM,cAAc,MAAM,eAAe,MAAM;CAC/C,MAAM,eAAe,MAAM,gBAAgB,MAAM;CAEjD,IACE,eAAe,QACf,gBAAgB,QAChB,MAAM,mBAAmB,QACzB,MAAM,qBAAqB,MAE3B;CAGF,OAAO;EACL,GAAI,eAAe,OAAO,EAAE,YAAY,IAAI,KAAA;EAC5C,GAAI,gBAAgB,OAAO,EAAE,aAAa,IAAI,KAAA;EAC9C,GAAI,MAAM,mBAAmB,OACzB,EAAE,iBAAiB,MAAM,gBAAgB,IACzC,KAAA;EACJ,GAAI,MAAM,qBAAqB,OAC3B,EAAE,mBAAmB,MAAM,kBAAkB,IAC7C,KAAA;CACN;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assistant-cloud",
3
- "version": "0.1.40",
3
+ "version": "0.1.42",
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.36"
30
+ "assistant-stream": "^0.3.40"
31
31
  },
32
32
  "devDependencies": {
33
- "@types/node": "^26.1.2",
34
- "vitest": "^4.1.10",
35
- "@assistant-ui/x-buildutils": "0.0.22"
33
+ "@assistant-ui/x-buildutils": "0.0.24",
34
+ "@types/node": "^26.2.0",
35
+ "vitest": "^4.1.11"
36
36
  },
37
37
  "publishConfig": {
38
38
  "access": "public",
@@ -33,6 +33,6 @@ export class AssistantCloud {
33
33
  ? { enabled: false }
34
34
  : t === true || t === undefined
35
35
  ? { enabled: true }
36
- : { enabled: t.enabled !== false, ...t };
36
+ : { ...t, enabled: t.enabled !== false };
37
37
  }
38
38
  }
@@ -139,16 +139,17 @@ export class AssistantCloudAPI {
139
139
 
140
140
  if (!response.ok) {
141
141
  const text = await response.text();
142
+ let message: string | undefined;
142
143
  try {
143
144
  const body = JSON.parse(text);
144
- throw new CloudAPIError(body.message, response.status);
145
- } catch (error) {
146
- if (error instanceof CloudAPIError) throw error;
147
- throw new CloudAPIError(
148
- `Request failed with status ${response.status}, ${text}`,
149
- response.status,
150
- );
151
- }
145
+ if (typeof body?.message === "string" && body.message.length > 0) {
146
+ message = body.message;
147
+ }
148
+ } catch {}
149
+ throw new CloudAPIError(
150
+ message ?? `Request failed with status ${response.status}, ${text}`,
151
+ response.status,
152
+ );
152
153
  }
153
154
 
154
155
  return response;
@@ -178,7 +178,16 @@ export class AssistantCloudAPIKeyAuthStrategy implements AssistantCloudAuthStrat
178
178
  }
179
179
  }
180
180
 
181
- const AUI_REFRESH_TOKEN_NAME = "aui:refresh_token";
181
+ const LEGACY_AUI_REFRESH_TOKEN_NAME = "aui:refresh_token";
182
+
183
+ const getRefreshTokenName = (baseUrl: string): string =>
184
+ `${LEGACY_AUI_REFRESH_TOKEN_NAME}:${baseUrl}`;
185
+
186
+ const removeLegacyRefreshToken = (storage: Storage): void => {
187
+ try {
188
+ storage.removeItem(LEGACY_AUI_REFRESH_TOKEN_NAME);
189
+ } catch {}
190
+ };
182
191
 
183
192
  const getLocalStorage = (): Storage | null => {
184
193
  if (!("localStorage" in globalThis)) return null;
@@ -189,32 +198,52 @@ const getLocalStorage = (): Storage | null => {
189
198
  }
190
199
  };
191
200
 
192
- const readRefreshToken = (): RefreshToken | undefined => {
201
+ const readRefreshToken = (baseUrl: string): RefreshToken | undefined => {
193
202
  const storage = getLocalStorage();
194
203
  if (!storage) return undefined;
195
204
  try {
196
- const value = storage.getItem(AUI_REFRESH_TOKEN_NAME);
197
- return value
198
- ? (JSON.parse(value) as { token: string; expires_at: string })
199
- : undefined;
205
+ const name = getRefreshTokenName(baseUrl);
206
+ const value = storage.getItem(name);
207
+ if (value) {
208
+ removeLegacyRefreshToken(storage);
209
+ return JSON.parse(value) as RefreshToken;
210
+ }
211
+
212
+ const legacyValue = storage.getItem(LEGACY_AUI_REFRESH_TOKEN_NAME);
213
+ if (!legacyValue) return undefined;
214
+
215
+ let refreshToken: RefreshToken;
216
+ try {
217
+ refreshToken = JSON.parse(legacyValue) as RefreshToken;
218
+ } catch {
219
+ removeLegacyRefreshToken(storage);
220
+ return undefined;
221
+ }
222
+
223
+ storage.setItem(name, legacyValue);
224
+ removeLegacyRefreshToken(storage);
225
+ return refreshToken;
200
226
  } catch {
201
227
  return undefined;
202
228
  }
203
229
  };
204
230
 
205
- const writeRefreshToken = (refreshToken: RefreshToken): void => {
231
+ const writeRefreshToken = (
232
+ baseUrl: string,
233
+ refreshToken: RefreshToken,
234
+ ): void => {
206
235
  const storage = getLocalStorage();
207
236
  if (!storage) return;
208
237
  try {
209
- storage.setItem(AUI_REFRESH_TOKEN_NAME, JSON.stringify(refreshToken));
238
+ storage.setItem(getRefreshTokenName(baseUrl), JSON.stringify(refreshToken));
210
239
  } catch {}
211
240
  };
212
241
 
213
- const removeRefreshToken = (): void => {
242
+ const removeRefreshToken = (baseUrl: string): void => {
214
243
  const storage = getLocalStorage();
215
244
  if (!storage) return;
216
245
  try {
217
- storage.removeItem(AUI_REFRESH_TOKEN_NAME);
246
+ storage.removeItem(getRefreshTokenName(baseUrl));
218
247
  } catch {}
219
248
  };
220
249
 
@@ -228,7 +257,7 @@ export class AssistantCloudAnonymousAuthStrategy implements AssistantCloudAuthSt
228
257
  this.baseUrl = baseUrl;
229
258
  this.jwtStrategy = new AssistantCloudJWTAuthStrategy(async () => {
230
259
  const currentTime = Date.now();
231
- const storedRefreshToken = readRefreshToken();
260
+ const storedRefreshToken = readRefreshToken(this.baseUrl);
232
261
 
233
262
  if (storedRefreshToken) {
234
263
  const refreshExpiry = new Date(storedRefreshToken.expires_at).getTime();
@@ -249,6 +278,7 @@ export class AssistantCloudAnonymousAuthStrategy implements AssistantCloudAuthSt
249
278
  );
250
279
  if (data.refresh_token != null) {
251
280
  writeRefreshToken(
281
+ this.baseUrl,
252
282
  readRefreshTokenResponse(
253
283
  data.refresh_token,
254
284
  "refresh auth token response.refresh_token",
@@ -257,8 +287,14 @@ export class AssistantCloudAnonymousAuthStrategy implements AssistantCloudAuthSt
257
287
  }
258
288
  return accessToken;
259
289
  }
290
+
291
+ if (response.status === 429 || response.status >= 500) {
292
+ throw new Error(
293
+ `Assistant Cloud token refresh failed with status ${response.status}`,
294
+ );
295
+ }
260
296
  } else {
261
- removeRefreshToken();
297
+ removeRefreshToken(this.baseUrl);
262
298
  }
263
299
  }
264
300
 
@@ -275,6 +311,7 @@ export class AssistantCloudAnonymousAuthStrategy implements AssistantCloudAuthSt
275
311
  );
276
312
 
277
313
  writeRefreshToken(
314
+ this.baseUrl,
278
315
  readRefreshTokenResponse(
279
316
  data.refresh_token,
280
317
  "anonymous auth token response.refresh_token",
@@ -0,0 +1,30 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type { AssistantCloudAPI } from "./AssistantCloudAPI";
3
+ import { AssistantCloudAuthTokens } from "./AssistantCloudAuthTokens";
4
+
5
+ const createCloudAuthTokens = () => {
6
+ const makeRequest = vi.fn();
7
+ const api = { makeRequest } as unknown as AssistantCloudAPI;
8
+ return { tokens: new AssistantCloudAuthTokens(api), makeRequest };
9
+ };
10
+
11
+ describe("AssistantCloudAuthTokens responses", () => {
12
+ it("decodes a valid token response", async () => {
13
+ const { tokens, makeRequest } = createCloudAuthTokens();
14
+ makeRequest.mockResolvedValue({ token: "token-1" });
15
+
16
+ await expect(tokens.create()).resolves.toEqual({ token: "token-1" });
17
+ expect(makeRequest).toHaveBeenCalledWith("/auth/tokens", {
18
+ method: "POST",
19
+ });
20
+ });
21
+
22
+ it("rejects a response that is missing the token", async () => {
23
+ const { tokens, makeRequest } = createCloudAuthTokens();
24
+ makeRequest.mockResolvedValue({});
25
+
26
+ await expect(tokens.create()).rejects.toThrow(
27
+ 'Invalid Assistant Cloud response for "token": expected a string',
28
+ );
29
+ });
30
+ });
@@ -1,4 +1,5 @@
1
1
  import type { AssistantCloudAPI } from "./AssistantCloudAPI";
2
+ import { readCloudRecord, readCloudString } from "./cloudResponse";
2
3
 
3
4
  type AssistantCloudAuthTokensCreateResponse = {
4
5
  token: string;
@@ -12,6 +13,11 @@ export class AssistantCloudAuthTokens {
12
13
  }
13
14
 
14
15
  public async create(): Promise<AssistantCloudAuthTokensCreateResponse> {
15
- return this.cloud.makeRequest("/auth/tokens", { method: "POST" });
16
+ const response = readCloudRecord(
17
+ await this.cloud.makeRequest("/auth/tokens", { method: "POST" }),
18
+ "auth token response",
19
+ );
20
+
21
+ return { token: readCloudString(response.token, "token") };
16
22
  }
17
23
  }
@@ -0,0 +1,71 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type { AssistantCloudAPI } from "./AssistantCloudAPI";
3
+ import { AssistantCloudFiles } from "./AssistantCloudFiles";
4
+
5
+ const createCloudFiles = () => {
6
+ const makeRequest = vi.fn();
7
+ const api = { makeRequest } as unknown as AssistantCloudAPI;
8
+ return { files: new AssistantCloudFiles(api), makeRequest };
9
+ };
10
+
11
+ describe("AssistantCloudFiles responses", () => {
12
+ it("decodes PDF conversion responses", async () => {
13
+ const { files, makeRequest } = createCloudFiles();
14
+ makeRequest.mockResolvedValue({
15
+ success: true,
16
+ urls: ["https://example.com/page-1.png"],
17
+ message: "converted",
18
+ });
19
+
20
+ await expect(
21
+ files.pdfToImages({ file_url: "https://example.com/file.pdf" }),
22
+ ).resolves.toEqual({
23
+ success: true,
24
+ urls: ["https://example.com/page-1.png"],
25
+ message: "converted",
26
+ });
27
+ });
28
+
29
+ it("rejects malformed PDF conversion responses", async () => {
30
+ const { files, makeRequest } = createCloudFiles();
31
+ makeRequest.mockResolvedValue({
32
+ success: true,
33
+ urls: [42],
34
+ message: "converted",
35
+ });
36
+
37
+ await expect(files.pdfToImages({ file_blob: "data" })).rejects.toThrow(
38
+ 'Invalid Assistant Cloud response for "PDF conversion response.urls[0]": expected a string',
39
+ );
40
+ });
41
+
42
+ it("decodes presigned upload responses", async () => {
43
+ const { files, makeRequest } = createCloudFiles();
44
+ makeRequest.mockResolvedValue({
45
+ success: true,
46
+ signedUrl: "https://uploads.example.com/file",
47
+ expiresAt: "2026-08-16T12:00:00.000Z",
48
+ publicUrl: "https://cdn.example.com/file",
49
+ });
50
+
51
+ await expect(
52
+ files.generatePresignedUploadUrl({ filename: "notes.txt" }),
53
+ ).resolves.toEqual({
54
+ success: true,
55
+ signedUrl: "https://uploads.example.com/file",
56
+ expiresAt: "2026-08-16T12:00:00.000Z",
57
+ publicUrl: "https://cdn.example.com/file",
58
+ });
59
+ });
60
+
61
+ it("rejects malformed presigned upload responses", async () => {
62
+ const { files, makeRequest } = createCloudFiles();
63
+ makeRequest.mockResolvedValue({});
64
+
65
+ await expect(
66
+ files.generatePresignedUploadUrl({ filename: "notes.txt" }),
67
+ ).rejects.toThrow(
68
+ 'Invalid Assistant Cloud response for "presigned upload response.success": expected a boolean',
69
+ );
70
+ });
71
+ });
@@ -1,4 +1,10 @@
1
1
  import type { AssistantCloudAPI } from "./AssistantCloudAPI";
2
+ import {
3
+ readCloudArray,
4
+ readCloudBoolean,
5
+ readCloudRecord,
6
+ readCloudString,
7
+ } from "./cloudResponse";
2
8
 
3
9
  type PdfToImagesRequestBody = {
4
10
  file_blob?: string | undefined;
@@ -32,21 +38,61 @@ export class AssistantCloudFiles {
32
38
  public async pdfToImages(
33
39
  body: PdfToImagesRequestBody,
34
40
  ): Promise<PdfToImagesResponse> {
35
- return this.cloud.makeRequest("/files/pdf-to-images", {
36
- method: "POST",
37
- body,
38
- });
41
+ const response = readCloudRecord(
42
+ await this.cloud.makeRequest("/files/pdf-to-images", {
43
+ method: "POST",
44
+ body,
45
+ }),
46
+ "PDF conversion response",
47
+ );
48
+
49
+ return {
50
+ success: readCloudBoolean(
51
+ response.success,
52
+ "PDF conversion response.success",
53
+ ),
54
+ urls: readCloudArray(response.urls, "PDF conversion response.urls").map(
55
+ (url, index) =>
56
+ readCloudString(url, `PDF conversion response.urls[${index}]`),
57
+ ),
58
+ message: readCloudString(
59
+ response.message,
60
+ "PDF conversion response.message",
61
+ ),
62
+ };
39
63
  }
40
64
 
41
65
  public async generatePresignedUploadUrl(
42
66
  body: GeneratePresignedUploadUrlRequestBody,
43
67
  ): Promise<GeneratePresignedUploadUrlResponse> {
44
- return this.cloud.makeRequest(
45
- "/files/attachments/generate-presigned-upload-url",
46
- {
47
- method: "POST",
48
- body,
49
- },
68
+ const response = readCloudRecord(
69
+ await this.cloud.makeRequest(
70
+ "/files/attachments/generate-presigned-upload-url",
71
+ {
72
+ method: "POST",
73
+ body,
74
+ },
75
+ ),
76
+ "presigned upload response",
50
77
  );
78
+
79
+ return {
80
+ success: readCloudBoolean(
81
+ response.success,
82
+ "presigned upload response.success",
83
+ ),
84
+ signedUrl: readCloudString(
85
+ response.signedUrl,
86
+ "presigned upload response.signedUrl",
87
+ ),
88
+ expiresAt: readCloudString(
89
+ response.expiresAt,
90
+ "presigned upload response.expiresAt",
91
+ ),
92
+ publicUrl: readCloudString(
93
+ response.publicUrl,
94
+ "presigned upload response.publicUrl",
95
+ ),
96
+ };
51
97
  }
52
98
  }
@@ -1,6 +1,11 @@
1
1
  import type { AssistantCloudAPI } from "./AssistantCloudAPI";
2
- import type { SamplingCallData } from "./instrumentMcpSampling";
2
+ import type { AssistantCloudRunReportToolCall } from "./runTelemetry";
3
3
  import { AssistantStream, PlainTextDecoder } from "assistant-stream";
4
+ import {
5
+ CloudResponseError,
6
+ readCloudRecord,
7
+ readCloudString,
8
+ } from "./cloudResponse";
4
9
 
5
10
  type AssistantCloudRunsStreamBody = {
6
11
  thread_id: string;
@@ -8,17 +13,6 @@ type AssistantCloudRunsStreamBody = {
8
13
  messages: readonly unknown[]; // TODO type
9
14
  };
10
15
 
11
- type ReportToolCall = {
12
- tool_name: string;
13
- tool_call_id: string;
14
- tool_args?: string;
15
- tool_result?: string;
16
- tool_source?: "mcp" | "frontend" | "backend";
17
- start_ms?: number;
18
- end_ms?: number;
19
- sampling_calls?: SamplingCallData[];
20
- };
21
-
22
16
  // NOTE: Keep this payload shape aligned with the strict runtime validator in
23
17
  // assistant-cloud: apps/aui-cloud-api/src/endpoints/runs/create.ts
24
18
  // (createRunSchema). New telemetry fields must be added in both repos together.
@@ -26,13 +20,13 @@ export type AssistantCloudRunReport = {
26
20
  thread_id: string;
27
21
  status: "completed" | "incomplete" | "error";
28
22
  total_steps?: number;
29
- tool_calls?: ReportToolCall[];
23
+ tool_calls?: AssistantCloudRunReportToolCall[];
30
24
  steps?: {
31
25
  input_tokens?: number;
32
26
  output_tokens?: number;
33
27
  reasoning_tokens?: number;
34
28
  cached_input_tokens?: number;
35
- tool_calls?: ReportToolCall[];
29
+ tool_calls?: AssistantCloudRunReportToolCall[];
36
30
  start_ms?: number;
37
31
  end_ms?: number;
38
32
  }[];
@@ -83,12 +77,40 @@ export class AssistantCloudRuns {
83
77
  },
84
78
  body,
85
79
  });
80
+
81
+ if (!response.body) {
82
+ throw new CloudResponseError(
83
+ 'Invalid Assistant Cloud response for "run stream": expected a response body',
84
+ );
85
+ }
86
+
87
+ const receivedContentType = response.headers.get("content-type");
88
+ const contentType = receivedContentType
89
+ ?.split(";", 1)[0]
90
+ ?.trim()
91
+ .toLowerCase();
92
+ if (contentType !== "text/plain") {
93
+ await response.body.cancel().catch(() => undefined);
94
+ throw new CloudResponseError(
95
+ `Invalid Assistant Cloud response for "run stream": expected a "text/plain" content type, received ${
96
+ receivedContentType
97
+ ? `"${receivedContentType}"`
98
+ : "no Content-Type header"
99
+ }`,
100
+ );
101
+ }
102
+
86
103
  return AssistantStream.fromResponse(response, new PlainTextDecoder());
87
104
  }
88
105
 
89
106
  public async report(
90
107
  body: AssistantCloudRunReport,
91
108
  ): Promise<{ run_id: string }> {
92
- return this.cloud.makeRequest("/runs", { method: "POST", body });
109
+ const response = readCloudRecord(
110
+ await this.cloud.makeRequest("/runs", { method: "POST", body }),
111
+ "run report response",
112
+ );
113
+
114
+ return { run_id: readCloudString(response.run_id, "run_id") };
93
115
  }
94
116
  }