assistant-cloud 0.1.41 → 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.
package/README.md CHANGED
@@ -9,16 +9,16 @@ Server- and client-side SDK for [Assistant Cloud](https://cloud.assistant-ui.com
9
9
  ## Installation
10
10
 
11
11
  ```bash
12
- npm install @assistant-ui/react @assistant-ui/react-ai-sdk assistant-cloud
12
+ npm install @assistant-ui/react @assistant-ui/ai-sdk assistant-cloud
13
13
  ```
14
14
 
15
15
  ## Usage
16
16
 
17
- Pass an `AssistantCloud` instance to your runtime hook (typically `useChatRuntime` from `@assistant-ui/react-ai-sdk`):
17
+ Pass an `AssistantCloud` instance to your runtime hook (typically `useChatRuntime` from `@assistant-ui/ai-sdk`):
18
18
 
19
19
  ```tsx
20
20
  import { AssistantCloud, AssistantRuntimeProvider } from "@assistant-ui/react";
21
- import { useChatRuntime } from "@assistant-ui/react-ai-sdk";
21
+ import { useChatRuntime } from "@assistant-ui/ai-sdk";
22
22
 
23
23
  const cloud = new AssistantCloud({
24
24
  baseUrl: process.env.NEXT_PUBLIC_ASSISTANT_BASE_URL!,
@@ -21,8 +21,8 @@ var AssistantCloud = class {
21
21
  this.files = new AssistantCloudFiles(api);
22
22
  const t = config.telemetry;
23
23
  this.telemetry = t === false ? { enabled: false } : t === true || t === void 0 ? { enabled: true } : {
24
- enabled: t.enabled !== false,
25
- ...t
24
+ ...t,
25
+ enabled: t.enabled !== false
26
26
  };
27
27
  }
28
28
  };
@@ -1 +1 @@
1
- {"version":3,"file":"AssistantCloud.js","names":[],"sources":["../src/AssistantCloud.ts"],"sourcesContent":["import {\n AssistantCloudAPI,\n type AssistantCloudConfig,\n type AssistantCloudTelemetryConfig,\n} from \"./AssistantCloudAPI\";\nimport { AssistantCloudAuthTokens } from \"./AssistantCloudAuthTokens\";\nimport { AssistantCloudProjects } from \"./AssistantCloudProjects\";\nimport { AssistantCloudRuns } from \"./AssistantCloudRuns\";\nimport { AssistantCloudThreads } from \"./AssistantCloudThreads\";\nimport { AssistantCloudFiles } from \"./AssistantCloudFiles\";\n\nexport class AssistantCloud {\n public readonly threads;\n public readonly projects;\n public readonly auth;\n public readonly runs;\n public readonly files;\n public readonly telemetry: AssistantCloudTelemetryConfig;\n\n constructor(config: AssistantCloudConfig) {\n const api = new AssistantCloudAPI(config);\n this.threads = new AssistantCloudThreads(api);\n this.projects = new AssistantCloudProjects(api);\n this.auth = {\n tokens: new AssistantCloudAuthTokens(api),\n };\n this.runs = new AssistantCloudRuns(api);\n this.files = new AssistantCloudFiles(api);\n\n const t = config.telemetry;\n this.telemetry =\n t === false\n ? { enabled: false }\n : t === true || t === undefined\n ? { enabled: true }\n : { enabled: t.enabled !== false, ...t };\n }\n}\n"],"mappings":";;;;;;;AAWA,IAAa,iBAAb,MAA4B;CAC1B;CACA;CACA;CACA;CACA;CACA;CAEA,YAAY,QAA8B;EACxC,MAAM,MAAM,IAAI,kBAAkB,MAAM;EACxC,KAAK,UAAU,IAAI,sBAAsB,GAAG;EAC5C,KAAK,WAAW,IAAI,uBAAuB,GAAG;EAC9C,KAAK,OAAO,EACV,QAAQ,IAAI,yBAAyB,GAAG,EAC1C;EACA,KAAK,OAAO,IAAI,mBAAmB,GAAG;EACtC,KAAK,QAAQ,IAAI,oBAAoB,GAAG;EAExC,MAAM,IAAI,OAAO;EACjB,KAAK,YACH,MAAM,QACF,EAAE,SAAS,MAAM,IACjB,MAAM,QAAQ,MAAM,KAAA,IAClB,EAAE,SAAS,KAAK,IAChB;GAAE,SAAS,EAAE,YAAY;GAAO,GAAG;EAAE;CAC/C;AACF"}
1
+ {"version":3,"file":"AssistantCloud.js","names":[],"sources":["../src/AssistantCloud.ts"],"sourcesContent":["import {\n AssistantCloudAPI,\n type AssistantCloudConfig,\n type AssistantCloudTelemetryConfig,\n} from \"./AssistantCloudAPI\";\nimport { AssistantCloudAuthTokens } from \"./AssistantCloudAuthTokens\";\nimport { AssistantCloudProjects } from \"./AssistantCloudProjects\";\nimport { AssistantCloudRuns } from \"./AssistantCloudRuns\";\nimport { AssistantCloudThreads } from \"./AssistantCloudThreads\";\nimport { AssistantCloudFiles } from \"./AssistantCloudFiles\";\n\nexport class AssistantCloud {\n public readonly threads;\n public readonly projects;\n public readonly auth;\n public readonly runs;\n public readonly files;\n public readonly telemetry: AssistantCloudTelemetryConfig;\n\n constructor(config: AssistantCloudConfig) {\n const api = new AssistantCloudAPI(config);\n this.threads = new AssistantCloudThreads(api);\n this.projects = new AssistantCloudProjects(api);\n this.auth = {\n tokens: new AssistantCloudAuthTokens(api),\n };\n this.runs = new AssistantCloudRuns(api);\n this.files = new AssistantCloudFiles(api);\n\n const t = config.telemetry;\n this.telemetry =\n t === false\n ? { enabled: false }\n : t === true || t === undefined\n ? { enabled: true }\n : { ...t, enabled: t.enabled !== false };\n }\n}\n"],"mappings":";;;;;;;AAWA,IAAa,iBAAb,MAA4B;CAC1B;CACA;CACA;CACA;CACA;CACA;CAEA,YAAY,QAA8B;EACxC,MAAM,MAAM,IAAI,kBAAkB,MAAM;EACxC,KAAK,UAAU,IAAI,sBAAsB,GAAG;EAC5C,KAAK,WAAW,IAAI,uBAAuB,GAAG;EAC9C,KAAK,OAAO,EACV,QAAQ,IAAI,yBAAyB,GAAG,EAC1C;EACA,KAAK,OAAO,IAAI,mBAAmB,GAAG;EACtC,KAAK,QAAQ,IAAI,oBAAoB,GAAG;EAExC,MAAM,IAAI,OAAO;EACjB,KAAK,YACH,MAAM,QACF,EAAE,SAAS,MAAM,IACjB,MAAM,QAAQ,MAAM,KAAA,IAClB,EAAE,SAAS,KAAK,IAChB;GAAE,GAAG;GAAG,SAAS,EAAE,YAAY;EAAM;CAC/C;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"AssistantCloudAPI.d.ts","names":[],"sources":["../src/AssistantCloudAPI.ts"],"mappings":";;;KAQY;EACV;;;;;;EAMA,gBACE,QAAQ,4BACL;;KAGK;EAEN;EACA,iBAAiB;;EAGjB;EACA;EACA;EACA;;EAGA;EACA;;;;;;;;;;;;;EAcJ,sBAAsB;;cAGX,sBAAsB;WACjB;EAEJ,YAAA,iBAAiB;;KAO1B;EACH;EACA,UAAU;EACV,QAAQ;EACR;;cAQW;EACJ,OAAO;EACP;EAEK,YAAA,QAAQ;EAuBP,kBAAc;EAId,eACX,kBACA,UAAS,qBAAuB,QAAA;EAmDrB,YAAY,kBAAkB,UAAS,qBAAuB"}
1
+ {"version":3,"file":"AssistantCloudAPI.d.ts","names":[],"sources":["../src/AssistantCloudAPI.ts"],"mappings":";;;KAQY;EACV;;;;;;EAMA,gBACE,QAAQ,4BACL;;KAGK;EAEN;EACA,iBAAiB;;EAGjB;EACA;EACA;EACA;;EAGA;EACA;;;;;;;;;;;;;EAcJ,sBAAsB;;cAGX,sBAAsB;WACjB;EAEJ,YAAA,iBAAiB;;KAO1B;EACH;EACA,UAAU;EACV,QAAQ;EACR;;cAQW;EACJ,OAAO;EACP;EAEK,YAAA,QAAQ;EAuBP,kBAAc;EAId,eACX,kBACA,UAAS,qBAAuB,QAAA;EAoDrB,YAAY,kBAAkB,UAAS,qBAAuB"}
@@ -54,12 +54,12 @@ var AssistantCloudAPI = class {
54
54
  this._auth.readAuthHeaders(response.headers);
55
55
  if (!response.ok) {
56
56
  const text = await response.text();
57
+ let message;
57
58
  try {
58
- throw new CloudAPIError(JSON.parse(text).message, response.status);
59
- } catch (error) {
60
- if (error instanceof CloudAPIError) throw error;
61
- throw new CloudAPIError(`Request failed with status ${response.status}, ${text}`, response.status);
62
- }
59
+ const body = JSON.parse(text);
60
+ if (typeof body?.message === "string" && body.message.length > 0) message = body.message;
61
+ } catch {}
62
+ throw new CloudAPIError(message ?? `Request failed with status ${response.status}, ${text}`, response.status);
63
63
  }
64
64
  return response;
65
65
  }
@@ -1 +1 @@
1
- {"version":3,"file":"AssistantCloudAPI.js","names":[],"sources":["../src/AssistantCloudAPI.ts"],"sourcesContent":["import {\n type AssistantCloudAuthStrategy,\n AssistantCloudJWTAuthStrategy,\n AssistantCloudAPIKeyAuthStrategy,\n AssistantCloudAnonymousAuthStrategy,\n} from \"./AssistantCloudAuthStrategy\";\nimport type { AssistantCloudRunReport } from \"./AssistantCloudRuns\";\n\nexport type AssistantCloudTelemetryConfig = {\n enabled?: boolean;\n /**\n * Called before each telemetry report is sent.\n * Return a modified report to enrich it (e.g. add `model_id`),\n * or return `null` to skip the report.\n */\n beforeReport?: (\n report: AssistantCloudRunReport,\n ) => AssistantCloudRunReport | null;\n};\n\nexport type AssistantCloudConfig = (\n | {\n baseUrl: string;\n authToken: () => Promise<string | null>;\n }\n | {\n baseUrl?: string;\n apiKey: string;\n userId: string;\n workspaceId: string;\n }\n | {\n baseUrl: string;\n anonymous: true;\n }\n) & {\n /**\n * Client-side run telemetry reporting. Default: `true`.\n *\n * When enabled, the SDK automatically reports run metadata (status, step\n * count, tool calls, and token usage) to Assistant Cloud after each\n * assistant message is saved. No message content is sent.\n *\n * - `true` / `undefined` — enabled with defaults\n * - `false` — disabled\n * - `{ beforeReport }` — enabled with a hook to enrich or filter reports\n */\n telemetry?: boolean | AssistantCloudTelemetryConfig;\n};\n\nexport class CloudAPIError extends Error {\n public readonly status: number;\n\n constructor(message: string, status: number) {\n super(message);\n this.status = status;\n this.name = \"CloudAPIError\";\n }\n}\n\ntype MakeRequestOptions = {\n method?: \"POST\" | \"PUT\" | \"DELETE\" | undefined;\n headers?: Record<string, string> | undefined;\n query?: Record<string, string | number | boolean> | undefined;\n body?: object | undefined;\n};\n\nconst normalizeBaseUrl = (baseUrl: string) => {\n if (!baseUrl || !baseUrl.endsWith(\"/\")) return baseUrl;\n return baseUrl.slice(0, -1);\n};\n\nexport class AssistantCloudAPI {\n public _auth: AssistantCloudAuthStrategy;\n public _baseUrl;\n\n constructor(config: AssistantCloudConfig) {\n if (\"authToken\" in config) {\n this._baseUrl = normalizeBaseUrl(config.baseUrl);\n this._auth = new AssistantCloudJWTAuthStrategy(config.authToken);\n } else if (\"apiKey\" in config) {\n this._baseUrl = normalizeBaseUrl(\n config.baseUrl ?? \"https://backend.assistant-api.com\",\n );\n this._auth = new AssistantCloudAPIKeyAuthStrategy(\n config.apiKey,\n config.userId,\n config.workspaceId,\n );\n } else if (\"anonymous\" in config) {\n this._baseUrl = normalizeBaseUrl(config.baseUrl);\n this._auth = new AssistantCloudAnonymousAuthStrategy(this._baseUrl);\n } else {\n throw new Error(\n \"Invalid configuration: Must provide authToken, apiKey, or anonymous configuration\",\n );\n }\n }\n\n public async initializeAuth() {\n return !!(await this._auth.getAuthHeaders());\n }\n\n public async makeRawRequest(\n endpoint: string,\n options: MakeRequestOptions = {},\n ) {\n const authHeaders = await this._auth.getAuthHeaders();\n if (!authHeaders) throw new Error(\"Authorization failed\");\n\n const headers = {\n ...authHeaders,\n ...options.headers,\n \"Content-Type\": \"application/json\",\n };\n\n const queryParams = new URLSearchParams();\n if (options.query) {\n for (const [key, value] of Object.entries(options.query)) {\n if (value === false) continue;\n if (value === true) {\n queryParams.set(key, \"true\");\n } else {\n queryParams.set(key, value.toString());\n }\n }\n }\n\n const url = new URL(`${this._baseUrl}/v1${endpoint}`);\n url.search = queryParams.toString();\n\n const response = await fetch(url, {\n method: options.method ?? \"GET\",\n headers,\n body: options.body ? JSON.stringify(options.body) : null,\n });\n\n this._auth.readAuthHeaders(response.headers);\n\n if (!response.ok) {\n const text = await response.text();\n try {\n const body = JSON.parse(text);\n throw new CloudAPIError(body.message, response.status);\n } catch (error) {\n if (error instanceof CloudAPIError) throw error;\n throw new CloudAPIError(\n `Request failed with status ${response.status}, ${text}`,\n response.status,\n );\n }\n }\n\n return response;\n }\n\n public async makeRequest(endpoint: string, options: MakeRequestOptions = {}) {\n const response = await this.makeRawRequest(endpoint, options);\n if (\n response.status === 204 ||\n response.headers.get(\"content-length\") === \"0\"\n )\n return undefined;\n\n const text = await response.text();\n if (text.trim() === \"\") return undefined;\n\n return JSON.parse(text);\n }\n}\n"],"mappings":";;AAkDA,IAAa,gBAAb,cAAmC,MAAM;CACvC;CAEA,YAAY,SAAiB,QAAgB;EAC3C,MAAM,OAAO;EACb,KAAK,SAAS;EACd,KAAK,OAAO;CACd;AACF;AASA,MAAM,oBAAoB,YAAoB;CAC5C,IAAI,CAAC,WAAW,CAAC,QAAQ,SAAS,GAAG,GAAG,OAAO;CAC/C,OAAO,QAAQ,MAAM,GAAG,EAAE;AAC5B;AAEA,IAAa,oBAAb,MAA+B;CAC7B;CACA;CAEA,YAAY,QAA8B;EACxC,IAAI,eAAe,QAAQ;GACzB,KAAK,WAAW,iBAAiB,OAAO,OAAO;GAC/C,KAAK,QAAQ,IAAI,8BAA8B,OAAO,SAAS;EACjE,OAAO,IAAI,YAAY,QAAQ;GAC7B,KAAK,WAAW,iBACd,OAAO,WAAW,mCACpB;GACA,KAAK,QAAQ,IAAI,iCACf,OAAO,QACP,OAAO,QACP,OAAO,WACT;EACF,OAAO,IAAI,eAAe,QAAQ;GAChC,KAAK,WAAW,iBAAiB,OAAO,OAAO;GAC/C,KAAK,QAAQ,IAAI,oCAAoC,KAAK,QAAQ;EACpE,OACE,MAAM,IAAI,MACR,mFACF;CAEJ;CAEA,MAAa,iBAAiB;EAC5B,OAAO,CAAC,CAAE,MAAM,KAAK,MAAM,eAAe;CAC5C;CAEA,MAAa,eACX,UACA,UAA8B,CAAC,GAC/B;EACA,MAAM,cAAc,MAAM,KAAK,MAAM,eAAe;EACpD,IAAI,CAAC,aAAa,MAAM,IAAI,MAAM,sBAAsB;EAExD,MAAM,UAAU;GACd,GAAG;GACH,GAAG,QAAQ;GACX,gBAAgB;EAClB;EAEA,MAAM,cAAc,IAAI,gBAAgB;EACxC,IAAI,QAAQ,OACV,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,QAAQ,KAAK,GAAG;GACxD,IAAI,UAAU,OAAO;GACrB,IAAI,UAAU,MACZ,YAAY,IAAI,KAAK,MAAM;QAE3B,YAAY,IAAI,KAAK,MAAM,SAAS,CAAC;EAEzC;EAGF,MAAM,MAAM,IAAI,IAAI,GAAG,KAAK,SAAS,KAAK,UAAU;EACpD,IAAI,SAAS,YAAY,SAAS;EAElC,MAAM,WAAW,MAAM,MAAM,KAAK;GAChC,QAAQ,QAAQ,UAAU;GAC1B;GACA,MAAM,QAAQ,OAAO,KAAK,UAAU,QAAQ,IAAI,IAAI;EACtD,CAAC;EAED,KAAK,MAAM,gBAAgB,SAAS,OAAO;EAE3C,IAAI,CAAC,SAAS,IAAI;GAChB,MAAM,OAAO,MAAM,SAAS,KAAK;GACjC,IAAI;IAEF,MAAM,IAAI,cADG,KAAK,MAAM,IACA,CAAA,CAAK,SAAS,SAAS,MAAM;GACvD,SAAS,OAAO;IACd,IAAI,iBAAiB,eAAe,MAAM;IAC1C,MAAM,IAAI,cACR,8BAA8B,SAAS,OAAO,IAAI,QAClD,SAAS,MACX;GACF;EACF;EAEA,OAAO;CACT;CAEA,MAAa,YAAY,UAAkB,UAA8B,CAAC,GAAG;EAC3E,MAAM,WAAW,MAAM,KAAK,eAAe,UAAU,OAAO;EAC5D,IACE,SAAS,WAAW,OACpB,SAAS,QAAQ,IAAI,gBAAgB,MAAM,KAE3C,OAAO,KAAA;EAET,MAAM,OAAO,MAAM,SAAS,KAAK;EACjC,IAAI,KAAK,KAAK,MAAM,IAAI,OAAO,KAAA;EAE/B,OAAO,KAAK,MAAM,IAAI;CACxB;AACF"}
1
+ {"version":3,"file":"AssistantCloudAPI.js","names":[],"sources":["../src/AssistantCloudAPI.ts"],"sourcesContent":["import {\n type AssistantCloudAuthStrategy,\n AssistantCloudJWTAuthStrategy,\n AssistantCloudAPIKeyAuthStrategy,\n AssistantCloudAnonymousAuthStrategy,\n} from \"./AssistantCloudAuthStrategy\";\nimport type { AssistantCloudRunReport } from \"./AssistantCloudRuns\";\n\nexport type AssistantCloudTelemetryConfig = {\n enabled?: boolean;\n /**\n * Called before each telemetry report is sent.\n * Return a modified report to enrich it (e.g. add `model_id`),\n * or return `null` to skip the report.\n */\n beforeReport?: (\n report: AssistantCloudRunReport,\n ) => AssistantCloudRunReport | null;\n};\n\nexport type AssistantCloudConfig = (\n | {\n baseUrl: string;\n authToken: () => Promise<string | null>;\n }\n | {\n baseUrl?: string;\n apiKey: string;\n userId: string;\n workspaceId: string;\n }\n | {\n baseUrl: string;\n anonymous: true;\n }\n) & {\n /**\n * Client-side run telemetry reporting. Default: `true`.\n *\n * When enabled, the SDK automatically reports run metadata (status, step\n * count, tool calls, and token usage) to Assistant Cloud after each\n * assistant message is saved. No message content is sent.\n *\n * - `true` / `undefined` — enabled with defaults\n * - `false` — disabled\n * - `{ beforeReport }` — enabled with a hook to enrich or filter reports\n */\n telemetry?: boolean | AssistantCloudTelemetryConfig;\n};\n\nexport class CloudAPIError extends Error {\n public readonly status: number;\n\n constructor(message: string, status: number) {\n super(message);\n this.status = status;\n this.name = \"CloudAPIError\";\n }\n}\n\ntype MakeRequestOptions = {\n method?: \"POST\" | \"PUT\" | \"DELETE\" | undefined;\n headers?: Record<string, string> | undefined;\n query?: Record<string, string | number | boolean> | undefined;\n body?: object | undefined;\n};\n\nconst normalizeBaseUrl = (baseUrl: string) => {\n if (!baseUrl || !baseUrl.endsWith(\"/\")) return baseUrl;\n return baseUrl.slice(0, -1);\n};\n\nexport class AssistantCloudAPI {\n public _auth: AssistantCloudAuthStrategy;\n public _baseUrl;\n\n constructor(config: AssistantCloudConfig) {\n if (\"authToken\" in config) {\n this._baseUrl = normalizeBaseUrl(config.baseUrl);\n this._auth = new AssistantCloudJWTAuthStrategy(config.authToken);\n } else if (\"apiKey\" in config) {\n this._baseUrl = normalizeBaseUrl(\n config.baseUrl ?? \"https://backend.assistant-api.com\",\n );\n this._auth = new AssistantCloudAPIKeyAuthStrategy(\n config.apiKey,\n config.userId,\n config.workspaceId,\n );\n } else if (\"anonymous\" in config) {\n this._baseUrl = normalizeBaseUrl(config.baseUrl);\n this._auth = new AssistantCloudAnonymousAuthStrategy(this._baseUrl);\n } else {\n throw new Error(\n \"Invalid configuration: Must provide authToken, apiKey, or anonymous configuration\",\n );\n }\n }\n\n public async initializeAuth() {\n return !!(await this._auth.getAuthHeaders());\n }\n\n public async makeRawRequest(\n endpoint: string,\n options: MakeRequestOptions = {},\n ) {\n const authHeaders = await this._auth.getAuthHeaders();\n if (!authHeaders) throw new Error(\"Authorization failed\");\n\n const headers = {\n ...authHeaders,\n ...options.headers,\n \"Content-Type\": \"application/json\",\n };\n\n const queryParams = new URLSearchParams();\n if (options.query) {\n for (const [key, value] of Object.entries(options.query)) {\n if (value === false) continue;\n if (value === true) {\n queryParams.set(key, \"true\");\n } else {\n queryParams.set(key, value.toString());\n }\n }\n }\n\n const url = new URL(`${this._baseUrl}/v1${endpoint}`);\n url.search = queryParams.toString();\n\n const response = await fetch(url, {\n method: options.method ?? \"GET\",\n headers,\n body: options.body ? JSON.stringify(options.body) : null,\n });\n\n this._auth.readAuthHeaders(response.headers);\n\n if (!response.ok) {\n const text = await response.text();\n let message: string | undefined;\n try {\n const body = JSON.parse(text);\n if (typeof body?.message === \"string\" && body.message.length > 0) {\n message = body.message;\n }\n } catch {}\n throw new CloudAPIError(\n message ?? `Request failed with status ${response.status}, ${text}`,\n response.status,\n );\n }\n\n return response;\n }\n\n public async makeRequest(endpoint: string, options: MakeRequestOptions = {}) {\n const response = await this.makeRawRequest(endpoint, options);\n if (\n response.status === 204 ||\n response.headers.get(\"content-length\") === \"0\"\n )\n return undefined;\n\n const text = await response.text();\n if (text.trim() === \"\") return undefined;\n\n return JSON.parse(text);\n }\n}\n"],"mappings":";;AAkDA,IAAa,gBAAb,cAAmC,MAAM;CACvC;CAEA,YAAY,SAAiB,QAAgB;EAC3C,MAAM,OAAO;EACb,KAAK,SAAS;EACd,KAAK,OAAO;CACd;AACF;AASA,MAAM,oBAAoB,YAAoB;CAC5C,IAAI,CAAC,WAAW,CAAC,QAAQ,SAAS,GAAG,GAAG,OAAO;CAC/C,OAAO,QAAQ,MAAM,GAAG,EAAE;AAC5B;AAEA,IAAa,oBAAb,MAA+B;CAC7B;CACA;CAEA,YAAY,QAA8B;EACxC,IAAI,eAAe,QAAQ;GACzB,KAAK,WAAW,iBAAiB,OAAO,OAAO;GAC/C,KAAK,QAAQ,IAAI,8BAA8B,OAAO,SAAS;EACjE,OAAO,IAAI,YAAY,QAAQ;GAC7B,KAAK,WAAW,iBACd,OAAO,WAAW,mCACpB;GACA,KAAK,QAAQ,IAAI,iCACf,OAAO,QACP,OAAO,QACP,OAAO,WACT;EACF,OAAO,IAAI,eAAe,QAAQ;GAChC,KAAK,WAAW,iBAAiB,OAAO,OAAO;GAC/C,KAAK,QAAQ,IAAI,oCAAoC,KAAK,QAAQ;EACpE,OACE,MAAM,IAAI,MACR,mFACF;CAEJ;CAEA,MAAa,iBAAiB;EAC5B,OAAO,CAAC,CAAE,MAAM,KAAK,MAAM,eAAe;CAC5C;CAEA,MAAa,eACX,UACA,UAA8B,CAAC,GAC/B;EACA,MAAM,cAAc,MAAM,KAAK,MAAM,eAAe;EACpD,IAAI,CAAC,aAAa,MAAM,IAAI,MAAM,sBAAsB;EAExD,MAAM,UAAU;GACd,GAAG;GACH,GAAG,QAAQ;GACX,gBAAgB;EAClB;EAEA,MAAM,cAAc,IAAI,gBAAgB;EACxC,IAAI,QAAQ,OACV,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,QAAQ,KAAK,GAAG;GACxD,IAAI,UAAU,OAAO;GACrB,IAAI,UAAU,MACZ,YAAY,IAAI,KAAK,MAAM;QAE3B,YAAY,IAAI,KAAK,MAAM,SAAS,CAAC;EAEzC;EAGF,MAAM,MAAM,IAAI,IAAI,GAAG,KAAK,SAAS,KAAK,UAAU;EACpD,IAAI,SAAS,YAAY,SAAS;EAElC,MAAM,WAAW,MAAM,MAAM,KAAK;GAChC,QAAQ,QAAQ,UAAU;GAC1B;GACA,MAAM,QAAQ,OAAO,KAAK,UAAU,QAAQ,IAAI,IAAI;EACtD,CAAC;EAED,KAAK,MAAM,gBAAgB,SAAS,OAAO;EAE3C,IAAI,CAAC,SAAS,IAAI;GAChB,MAAM,OAAO,MAAM,SAAS,KAAK;GACjC,IAAI;GACJ,IAAI;IACF,MAAM,OAAO,KAAK,MAAM,IAAI;IAC5B,IAAI,OAAO,MAAM,YAAY,YAAY,KAAK,QAAQ,SAAS,GAC7D,UAAU,KAAK;GAEnB,QAAQ,CAAC;GACT,MAAM,IAAI,cACR,WAAW,8BAA8B,SAAS,OAAO,IAAI,QAC7D,SAAS,MACX;EACF;EAEA,OAAO;CACT;CAEA,MAAa,YAAY,UAAkB,UAA8B,CAAC,GAAG;EAC3E,MAAM,WAAW,MAAM,KAAK,eAAe,UAAU,OAAO;EAC5D,IACE,SAAS,WAAW,OACpB,SAAS,QAAQ,IAAI,gBAAgB,MAAM,KAE3C,OAAO,KAAA;EAET,MAAM,OAAO,MAAM,SAAS,KAAK;EACjC,IAAI,KAAK,KAAK,MAAM,IAAI,OAAO,KAAA;EAE/B,OAAO,KAAK,MAAM,IAAI;CACxB;AACF"}
@@ -1,4 +1,4 @@
1
- import { SamplingCallData } from "./instrumentMcpSampling.js";
1
+ import { AssistantCloudRunReportToolCall } from "./runTelemetry.js";
2
2
  import { AssistantCloudAPI } from "./AssistantCloudAPI.js";
3
3
  import { AssistantStream } from "assistant-stream";
4
4
  //#region src/AssistantCloudRuns.d.ts
@@ -7,27 +7,17 @@ type AssistantCloudRunsStreamBody = {
7
7
  assistant_id: "system/thread_title";
8
8
  messages: readonly unknown[];
9
9
  };
10
- type ReportToolCall = {
11
- tool_name: string;
12
- tool_call_id: string;
13
- tool_args?: string;
14
- tool_result?: string;
15
- tool_source?: "mcp" | "frontend" | "backend";
16
- start_ms?: number;
17
- end_ms?: number;
18
- sampling_calls?: SamplingCallData[];
19
- };
20
10
  type AssistantCloudRunReport = {
21
11
  thread_id: string;
22
12
  status: "completed" | "incomplete" | "error";
23
13
  total_steps?: number;
24
- tool_calls?: ReportToolCall[];
14
+ tool_calls?: AssistantCloudRunReportToolCall[];
25
15
  steps?: {
26
16
  input_tokens?: number;
27
17
  output_tokens?: number;
28
18
  reasoning_tokens?: number;
29
19
  cached_input_tokens?: number;
30
- tool_calls?: ReportToolCall[];
20
+ tool_calls?: AssistantCloudRunReportToolCall[];
31
21
  start_ms?: number;
32
22
  end_ms?: number;
33
23
  }[];
@@ -1 +1 @@
1
- {"version":3,"file":"AssistantCloudRuns.d.ts","names":[],"sources":["../src/AssistantCloudRuns.ts"],"mappings":";;;;KASK;EACH;EACA;EACA;;KAGG;EACH;EACA;EACA;EACA;EACA;EACA;EACA;EACA,iBAAiB;;KAMP;EACV;EACA;EACA;EACA,aAAa;EACb;IACE;IACA;IACA;IACA;IACA,aAAa;IACb;IACA;;EAEF;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,WAAW;;cAGA;UACH;EAEI,YAAA,OAAO;EAIZ,+BAA+B;;;;;;;;;;;EAmBzB,OACX,MAAM,+BACL,QAAQ;EAkCE,OACX,MAAM,0BACL;IAAU"}
1
+ {"version":3,"file":"AssistantCloudRuns.d.ts","names":[],"sources":["../src/AssistantCloudRuns.ts"],"mappings":";;;;KASK;EACH;EACA;EACA;;KAMU;EACV;EACA;EACA;EACA,aAAa;EACb;IACE;IACA;IACA;IACA;IACA,aAAa;IACb;IACA;;EAEF;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,WAAW;;cAGA;UACH;EAEI,YAAA,OAAO;EAIZ,+BAA+B;;;;;;;;;;;EAmBzB,OACX,MAAM,+BACL,QAAQ;EAkCE,OACX,MAAM,0BACL;IAAU"}
@@ -1 +1 @@
1
- {"version":3,"file":"AssistantCloudRuns.js","names":[],"sources":["../src/AssistantCloudRuns.ts"],"sourcesContent":["import type { AssistantCloudAPI } from \"./AssistantCloudAPI\";\nimport type { SamplingCallData } from \"./instrumentMcpSampling\";\nimport { AssistantStream, PlainTextDecoder } from \"assistant-stream\";\nimport {\n CloudResponseError,\n readCloudRecord,\n readCloudString,\n} from \"./cloudResponse\";\n\ntype AssistantCloudRunsStreamBody = {\n thread_id: string;\n assistant_id: \"system/thread_title\";\n messages: readonly unknown[]; // TODO type\n};\n\ntype ReportToolCall = {\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// NOTE: Keep this payload shape aligned with the strict runtime validator in\n// assistant-cloud: apps/aui-cloud-api/src/endpoints/runs/create.ts\n// (createRunSchema). New telemetry fields must be added in both repos together.\nexport type AssistantCloudRunReport = {\n thread_id: string;\n status: \"completed\" | \"incomplete\" | \"error\";\n total_steps?: number;\n tool_calls?: ReportToolCall[];\n steps?: {\n input_tokens?: number;\n output_tokens?: number;\n reasoning_tokens?: number;\n cached_input_tokens?: number;\n tool_calls?: ReportToolCall[];\n start_ms?: number;\n end_ms?: number;\n }[];\n input_tokens?: number;\n output_tokens?: number;\n reasoning_tokens?: number;\n cached_input_tokens?: number;\n model_id?: string;\n provider_type?: string;\n duration_ms?: number;\n output_text?: string;\n metadata?: Record<string, unknown>;\n};\n\nexport class AssistantCloudRuns {\n private cloud: AssistantCloudAPI;\n\n constructor(cloud: AssistantCloudAPI) {\n this.cloud = cloud;\n }\n\n public __internal_getAssistantOptions(assistantId: string) {\n return {\n api: `${this.cloud._baseUrl}/v1/runs/stream`,\n headers: async () => {\n const headers = await this.cloud._auth.getAuthHeaders();\n if (!headers) throw new Error(\"Authorization failed\");\n return {\n ...headers,\n Accept: \"text/plain\",\n };\n },\n body: {\n assistant_id: assistantId,\n response_format: \"vercel-ai-data-stream/v1\",\n thread_id: \"unstable_todo\",\n },\n };\n }\n\n public async stream(\n body: AssistantCloudRunsStreamBody,\n ): Promise<AssistantStream> {\n const response = await this.cloud.makeRawRequest(\"/runs/stream\", {\n method: \"POST\",\n headers: {\n Accept: \"text/plain\",\n },\n body,\n });\n\n if (!response.body) {\n throw new CloudResponseError(\n 'Invalid Assistant Cloud response for \"run stream\": expected a response body',\n );\n }\n\n const receivedContentType = response.headers.get(\"content-type\");\n const contentType = receivedContentType\n ?.split(\";\", 1)[0]\n ?.trim()\n .toLowerCase();\n if (contentType !== \"text/plain\") {\n await response.body.cancel().catch(() => undefined);\n throw new CloudResponseError(\n `Invalid Assistant Cloud response for \"run stream\": expected a \"text/plain\" content type, received ${\n receivedContentType\n ? `\"${receivedContentType}\"`\n : \"no Content-Type header\"\n }`,\n );\n }\n\n return AssistantStream.fromResponse(response, new PlainTextDecoder());\n }\n\n public async report(\n body: AssistantCloudRunReport,\n ): Promise<{ run_id: string }> {\n const response = readCloudRecord(\n await this.cloud.makeRequest(\"/runs\", { method: \"POST\", body }),\n \"run report response\",\n );\n\n return { run_id: readCloudString(response.run_id, \"run_id\") };\n }\n}\n"],"mappings":";;;AAsDA,IAAa,qBAAb,MAAgC;CAC9B;CAEA,YAAY,OAA0B;EACpC,KAAK,QAAQ;CACf;CAEA,+BAAsC,aAAqB;EACzD,OAAO;GACL,KAAK,GAAG,KAAK,MAAM,SAAS;GAC5B,SAAS,YAAY;IACnB,MAAM,UAAU,MAAM,KAAK,MAAM,MAAM,eAAe;IACtD,IAAI,CAAC,SAAS,MAAM,IAAI,MAAM,sBAAsB;IACpD,OAAO;KACL,GAAG;KACH,QAAQ;IACV;GACF;GACA,MAAM;IACJ,cAAc;IACd,iBAAiB;IACjB,WAAW;GACb;EACF;CACF;CAEA,MAAa,OACX,MAC0B;EAC1B,MAAM,WAAW,MAAM,KAAK,MAAM,eAAe,gBAAgB;GAC/D,QAAQ;GACR,SAAS,EACP,QAAQ,aACV;GACA;EACF,CAAC;EAED,IAAI,CAAC,SAAS,MACZ,MAAM,IAAI,mBACR,+EACF;EAGF,MAAM,sBAAsB,SAAS,QAAQ,IAAI,cAAc;EAK/D,IAJoB,qBAChB,MAAM,KAAK,CAAC,CAAC,CAAC,EAAE,EAChB,KAAK,CAAC,CACP,YAAY,MACK,cAAc;GAChC,MAAM,SAAS,KAAK,OAAO,CAAC,CAAC,YAAY,KAAA,CAAS;GAClD,MAAM,IAAI,mBACR,qGACE,sBACI,IAAI,oBAAoB,KACxB,0BAER;EACF;EAEA,OAAO,gBAAgB,aAAa,UAAU,IAAI,iBAAiB,CAAC;CACtE;CAEA,MAAa,OACX,MAC6B;EAC7B,MAAM,WAAW,gBACf,MAAM,KAAK,MAAM,YAAY,SAAS;GAAE,QAAQ;GAAQ;EAAK,CAAC,GAC9D,qBACF;EAEA,OAAO,EAAE,QAAQ,gBAAgB,SAAS,QAAQ,QAAQ,EAAE;CAC9D;AACF"}
1
+ {"version":3,"file":"AssistantCloudRuns.js","names":[],"sources":["../src/AssistantCloudRuns.ts"],"sourcesContent":["import type { AssistantCloudAPI } from \"./AssistantCloudAPI\";\nimport type { AssistantCloudRunReportToolCall } from \"./runTelemetry\";\nimport { AssistantStream, PlainTextDecoder } from \"assistant-stream\";\nimport {\n CloudResponseError,\n readCloudRecord,\n readCloudString,\n} from \"./cloudResponse\";\n\ntype AssistantCloudRunsStreamBody = {\n thread_id: string;\n assistant_id: \"system/thread_title\";\n messages: readonly unknown[]; // TODO type\n};\n\n// NOTE: Keep this payload shape aligned with the strict runtime validator in\n// assistant-cloud: apps/aui-cloud-api/src/endpoints/runs/create.ts\n// (createRunSchema). New telemetry fields must be added in both repos together.\nexport type AssistantCloudRunReport = {\n thread_id: string;\n status: \"completed\" | \"incomplete\" | \"error\";\n total_steps?: number;\n tool_calls?: AssistantCloudRunReportToolCall[];\n steps?: {\n input_tokens?: number;\n output_tokens?: number;\n reasoning_tokens?: number;\n cached_input_tokens?: number;\n tool_calls?: AssistantCloudRunReportToolCall[];\n start_ms?: number;\n end_ms?: number;\n }[];\n input_tokens?: number;\n output_tokens?: number;\n reasoning_tokens?: number;\n cached_input_tokens?: number;\n model_id?: string;\n provider_type?: string;\n duration_ms?: number;\n output_text?: string;\n metadata?: Record<string, unknown>;\n};\n\nexport class AssistantCloudRuns {\n private cloud: AssistantCloudAPI;\n\n constructor(cloud: AssistantCloudAPI) {\n this.cloud = cloud;\n }\n\n public __internal_getAssistantOptions(assistantId: string) {\n return {\n api: `${this.cloud._baseUrl}/v1/runs/stream`,\n headers: async () => {\n const headers = await this.cloud._auth.getAuthHeaders();\n if (!headers) throw new Error(\"Authorization failed\");\n return {\n ...headers,\n Accept: \"text/plain\",\n };\n },\n body: {\n assistant_id: assistantId,\n response_format: \"vercel-ai-data-stream/v1\",\n thread_id: \"unstable_todo\",\n },\n };\n }\n\n public async stream(\n body: AssistantCloudRunsStreamBody,\n ): Promise<AssistantStream> {\n const response = await this.cloud.makeRawRequest(\"/runs/stream\", {\n method: \"POST\",\n headers: {\n Accept: \"text/plain\",\n },\n body,\n });\n\n if (!response.body) {\n throw new CloudResponseError(\n 'Invalid Assistant Cloud response for \"run stream\": expected a response body',\n );\n }\n\n const receivedContentType = response.headers.get(\"content-type\");\n const contentType = receivedContentType\n ?.split(\";\", 1)[0]\n ?.trim()\n .toLowerCase();\n if (contentType !== \"text/plain\") {\n await response.body.cancel().catch(() => undefined);\n throw new CloudResponseError(\n `Invalid Assistant Cloud response for \"run stream\": expected a \"text/plain\" content type, received ${\n receivedContentType\n ? `\"${receivedContentType}\"`\n : \"no Content-Type header\"\n }`,\n );\n }\n\n return AssistantStream.fromResponse(response, new PlainTextDecoder());\n }\n\n public async report(\n body: AssistantCloudRunReport,\n ): Promise<{ run_id: string }> {\n const response = readCloudRecord(\n await this.cloud.makeRequest(\"/runs\", { method: \"POST\", body }),\n \"run report response\",\n );\n\n return { run_id: readCloudString(response.run_id, \"run_id\") };\n }\n}\n"],"mappings":";;;AA2CA,IAAa,qBAAb,MAAgC;CAC9B;CAEA,YAAY,OAA0B;EACpC,KAAK,QAAQ;CACf;CAEA,+BAAsC,aAAqB;EACzD,OAAO;GACL,KAAK,GAAG,KAAK,MAAM,SAAS;GAC5B,SAAS,YAAY;IACnB,MAAM,UAAU,MAAM,KAAK,MAAM,MAAM,eAAe;IACtD,IAAI,CAAC,SAAS,MAAM,IAAI,MAAM,sBAAsB;IACpD,OAAO;KACL,GAAG;KACH,QAAQ;IACV;GACF;GACA,MAAM;IACJ,cAAc;IACd,iBAAiB;IACjB,WAAW;GACb;EACF;CACF;CAEA,MAAa,OACX,MAC0B;EAC1B,MAAM,WAAW,MAAM,KAAK,MAAM,eAAe,gBAAgB;GAC/D,QAAQ;GACR,SAAS,EACP,QAAQ,aACV;GACA;EACF,CAAC;EAED,IAAI,CAAC,SAAS,MACZ,MAAM,IAAI,mBACR,+EACF;EAGF,MAAM,sBAAsB,SAAS,QAAQ,IAAI,cAAc;EAK/D,IAJoB,qBAChB,MAAM,KAAK,CAAC,CAAC,CAAC,EAAE,EAChB,KAAK,CAAC,CACP,YAAY,MACK,cAAc;GAChC,MAAM,SAAS,KAAK,OAAO,CAAC,CAAC,YAAY,KAAA,CAAS;GAClD,MAAM,IAAI,mBACR,qGACE,sBACI,IAAI,oBAAoB,KACxB,0BAER;EACF;EAEA,OAAO,gBAAgB,aAAa,UAAU,IAAI,iBAAiB,CAAC;CACtE;CAEA,MAAa,OACX,MAC6B;EAC7B,MAAM,WAAW,gBACf,MAAM,KAAK,MAAM,YAAY,SAAS;GAAE,QAAQ;GAAQ;EAAK,CAAC,GAC9D,qBACF;EAEA,OAAO,EAAE,QAAQ,gBAAgB,SAAS,QAAQ,QAAQ,EAAE;CAC9D;AACF"}
@@ -0,0 +1,15 @@
1
+ import { AssistantCloud } from "./AssistantCloud.js";
2
+ //#region src/generateThreadTitle.d.ts
3
+ declare function generateThreadTitle(cloud: AssistantCloud, options: {
4
+ threadId: string;
5
+ messages: readonly {
6
+ role: string;
7
+ content: readonly {
8
+ type: "text";
9
+ text: string;
10
+ }[];
11
+ }[];
12
+ }): Promise<string | null>;
13
+ //#endregion
14
+ export { generateThreadTitle };
15
+ //# sourceMappingURL=generateThreadTitle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generateThreadTitle.d.ts","names":[],"sources":["../src/generateThreadTitle.ts"],"mappings":";;iBAEsB,oBACpB,OAAO,gBACP;EACE;EACA;IACE;IACA;MAAoB;MAAc;;;IAGrC"}
@@ -0,0 +1,25 @@
1
+ //#region src/generateThreadTitle.ts
2
+ async function generateThreadTitle(cloud, options) {
3
+ const stream = await cloud.runs.stream({
4
+ thread_id: options.threadId,
5
+ assistant_id: "system/thread_title",
6
+ messages: options.messages
7
+ });
8
+ let title = "";
9
+ const reader = stream.getReader();
10
+ try {
11
+ while (true) {
12
+ const { done, value: chunk } = await reader.read();
13
+ if (done) break;
14
+ if (chunk.type === "text-delta") title += chunk.textDelta;
15
+ }
16
+ } finally {
17
+ reader.releaseLock();
18
+ }
19
+ if (title) await cloud.threads.update(options.threadId, { title });
20
+ return title || null;
21
+ }
22
+ //#endregion
23
+ export { generateThreadTitle };
24
+
25
+ //# sourceMappingURL=generateThreadTitle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generateThreadTitle.js","names":[],"sources":["../src/generateThreadTitle.ts"],"sourcesContent":["import type { AssistantCloud } from \"./AssistantCloud\";\n\nexport async function generateThreadTitle(\n cloud: AssistantCloud,\n options: {\n threadId: string;\n messages: readonly {\n role: string;\n content: readonly { type: \"text\"; text: string }[];\n }[];\n },\n): Promise<string | null> {\n const stream = await cloud.runs.stream({\n thread_id: options.threadId,\n assistant_id: \"system/thread_title\",\n messages: options.messages,\n });\n\n let title = \"\";\n const reader = stream.getReader();\n try {\n while (true) {\n const { done, value: chunk } = await reader.read();\n if (done) break;\n if (chunk.type === \"text-delta\") {\n title += chunk.textDelta;\n }\n }\n } finally {\n reader.releaseLock();\n }\n\n if (title) {\n await cloud.threads.update(options.threadId, { title });\n }\n\n return title || null;\n}\n"],"mappings":";AAEA,eAAsB,oBACpB,OACA,SAOwB;CACxB,MAAM,SAAS,MAAM,MAAM,KAAK,OAAO;EACrC,WAAW,QAAQ;EACnB,cAAc;EACd,UAAU,QAAQ;CACpB,CAAC;CAED,IAAI,QAAQ;CACZ,MAAM,SAAS,OAAO,UAAU;CAChC,IAAI;EACF,OAAO,MAAM;GACX,MAAM,EAAE,MAAM,OAAO,UAAU,MAAM,OAAO,KAAK;GACjD,IAAI,MAAM;GACV,IAAI,MAAM,SAAS,cACjB,SAAS,MAAM;EAEnB;CACF,UAAU;EACR,OAAO,YAAY;CACrB;CAEA,IAAI,OACF,MAAM,MAAM,QAAQ,OAAO,QAAQ,UAAU,EAAE,MAAM,CAAC;CAGxD,OAAO,SAAS;AAClB"}
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.41",
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.38"
30
+ "assistant-stream": "^0.3.40"
31
31
  },
32
32
  "devDependencies": {
33
+ "@assistant-ui/x-buildutils": "0.0.24",
33
34
  "@types/node": "^26.2.0",
34
- "vitest": "^4.1.10",
35
- "@assistant-ui/x-buildutils": "0.0.23"
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;
@@ -1,5 +1,5 @@
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
4
  import {
5
5
  CloudResponseError,
@@ -13,17 +13,6 @@ type AssistantCloudRunsStreamBody = {
13
13
  messages: readonly unknown[]; // TODO type
14
14
  };
15
15
 
16
- type ReportToolCall = {
17
- tool_name: string;
18
- tool_call_id: string;
19
- tool_args?: string;
20
- tool_result?: string;
21
- tool_source?: "mcp" | "frontend" | "backend";
22
- start_ms?: number;
23
- end_ms?: number;
24
- sampling_calls?: SamplingCallData[];
25
- };
26
-
27
16
  // NOTE: Keep this payload shape aligned with the strict runtime validator in
28
17
  // assistant-cloud: apps/aui-cloud-api/src/endpoints/runs/create.ts
29
18
  // (createRunSchema). New telemetry fields must be added in both repos together.
@@ -31,13 +20,13 @@ export type AssistantCloudRunReport = {
31
20
  thread_id: string;
32
21
  status: "completed" | "incomplete" | "error";
33
22
  total_steps?: number;
34
- tool_calls?: ReportToolCall[];
23
+ tool_calls?: AssistantCloudRunReportToolCall[];
35
24
  steps?: {
36
25
  input_tokens?: number;
37
26
  output_tokens?: number;
38
27
  reasoning_tokens?: number;
39
28
  cached_input_tokens?: number;
40
- tool_calls?: ReportToolCall[];
29
+ tool_calls?: AssistantCloudRunReportToolCall[];
41
30
  start_ms?: number;
42
31
  end_ms?: number;
43
32
  }[];
@@ -0,0 +1,71 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type { AssistantCloud } from "./AssistantCloud";
3
+ import { generateThreadTitle } from "./generateThreadTitle";
4
+
5
+ const titleStream = (...chunks: { type: string; textDelta?: string }[]) =>
6
+ new ReadableStream({
7
+ start(controller) {
8
+ for (const chunk of chunks) controller.enqueue(chunk);
9
+ controller.close();
10
+ },
11
+ });
12
+
13
+ const createCloud = (stream: ReadableStream<unknown>) => {
14
+ const update = vi.fn().mockResolvedValue(undefined);
15
+ const run = vi.fn().mockResolvedValue(stream);
16
+ const cloud = {
17
+ threads: { update },
18
+ runs: { stream: run },
19
+ } as unknown as AssistantCloud;
20
+ return { cloud, update, run };
21
+ };
22
+
23
+ describe("generateThreadTitle", () => {
24
+ it("accumulates text deltas and updates the thread title", async () => {
25
+ const { cloud, run, update } = createCloud(
26
+ titleStream(
27
+ { type: "text-delta", textDelta: "Weather " },
28
+ { type: "text-delta", textDelta: "chat" },
29
+ ),
30
+ );
31
+
32
+ await expect(
33
+ generateThreadTitle(cloud, {
34
+ threadId: "thread-1",
35
+ messages: [
36
+ {
37
+ role: "user",
38
+ content: [{ type: "text", text: "What is the weather today?" }],
39
+ },
40
+ ],
41
+ }),
42
+ ).resolves.toBe("Weather chat");
43
+
44
+ expect(run).toHaveBeenCalledExactlyOnceWith({
45
+ thread_id: "thread-1",
46
+ assistant_id: "system/thread_title",
47
+ messages: [
48
+ {
49
+ role: "user",
50
+ content: [{ type: "text", text: "What is the weather today?" }],
51
+ },
52
+ ],
53
+ });
54
+ expect(update).toHaveBeenCalledExactlyOnceWith("thread-1", {
55
+ title: "Weather chat",
56
+ });
57
+ });
58
+
59
+ it("returns null without updating when the stream has no text", async () => {
60
+ const { cloud, update } = createCloud(titleStream());
61
+
62
+ await expect(
63
+ generateThreadTitle(cloud, {
64
+ threadId: "thread-1",
65
+ messages: [],
66
+ }),
67
+ ).resolves.toBeNull();
68
+
69
+ expect(update).not.toHaveBeenCalled();
70
+ });
71
+ });
@@ -0,0 +1,38 @@
1
+ import type { AssistantCloud } from "./AssistantCloud";
2
+
3
+ export async function generateThreadTitle(
4
+ cloud: AssistantCloud,
5
+ options: {
6
+ threadId: string;
7
+ messages: readonly {
8
+ role: string;
9
+ content: readonly { type: "text"; text: string }[];
10
+ }[];
11
+ },
12
+ ): Promise<string | null> {
13
+ const stream = await cloud.runs.stream({
14
+ thread_id: options.threadId,
15
+ assistant_id: "system/thread_title",
16
+ messages: options.messages,
17
+ });
18
+
19
+ let title = "";
20
+ const reader = stream.getReader();
21
+ try {
22
+ while (true) {
23
+ const { done, value: chunk } = await reader.read();
24
+ if (done) break;
25
+ if (chunk.type === "text-delta") {
26
+ title += chunk.textDelta;
27
+ }
28
+ }
29
+ } finally {
30
+ reader.releaseLock();
31
+ }
32
+
33
+ if (title) {
34
+ await cloud.threads.update(options.threadId, { title });
35
+ }
36
+
37
+ return title || null;
38
+ }
package/src/index.ts CHANGED
@@ -2,7 +2,17 @@ export type { CloudMessage } from "./AssistantCloudThreadMessages";
2
2
  export type { AssistantCloudTelemetryConfig } from "./AssistantCloudAPI";
3
3
  export { CloudAPIError } from "./AssistantCloudAPI";
4
4
  export { CloudResponseError } from "./cloudResponse";
5
+ export { generateThreadTitle } from "./generateThreadTitle";
5
6
  export type { AssistantCloudRunReport } from "./AssistantCloudRuns";
7
+ export {
8
+ createRunTelemetryToolCall,
9
+ normalizeRunTelemetryUsage,
10
+ truncateRunTelemetryText,
11
+ type AssistantCloudRunReportToolCall,
12
+ type RunTelemetryToolCallInit,
13
+ type RunTelemetryUsage,
14
+ type RunTelemetryUsageInit,
15
+ } from "./runTelemetry";
6
16
  export { AssistantCloud } from "./AssistantCloud";
7
17
  export { CloudMessagePersistence } from "./CloudMessagePersistence";
8
18
  export {
@@ -0,0 +1,130 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ createRunTelemetryToolCall,
4
+ normalizeRunTelemetryUsage,
5
+ truncateRunTelemetryText,
6
+ } from "./runTelemetry";
7
+
8
+ const MAX = 50_000;
9
+
10
+ describe("truncateRunTelemetryText", () => {
11
+ it("passes text at or under the cap through unchanged", () => {
12
+ expect(truncateRunTelemetryText("hello")).toBe("hello");
13
+ const exact = "a".repeat(MAX);
14
+ expect(truncateRunTelemetryText(exact)).toBe(exact);
15
+ });
16
+
17
+ it("clamps text over the cap", () => {
18
+ expect(truncateRunTelemetryText("a".repeat(MAX + 1))).toHaveLength(MAX);
19
+ });
20
+ });
21
+
22
+ describe("createRunTelemetryToolCall", () => {
23
+ it("serializes args and omits tool_source when the caller gives none", () => {
24
+ expect(
25
+ createRunTelemetryToolCall({
26
+ toolName: "calculator",
27
+ toolCallId: "call-1",
28
+ args: { a: 1 },
29
+ result: { sum: 1 },
30
+ }),
31
+ ).toEqual({
32
+ tool_name: "calculator",
33
+ tool_call_id: "call-1",
34
+ tool_args: '{"a":1}',
35
+ tool_result: '{"sum":1}',
36
+ });
37
+ });
38
+
39
+ it("clamps serialized args and results", () => {
40
+ const call = createRunTelemetryToolCall({
41
+ toolName: "t",
42
+ toolCallId: "call-1",
43
+ args: { blob: "a".repeat(MAX) },
44
+ result: { blob: "a".repeat(MAX) },
45
+ });
46
+ expect(call.tool_args).toHaveLength(MAX);
47
+ expect(call.tool_result).toHaveLength(MAX);
48
+ });
49
+
50
+ it("clamps pre-serialized argsText to the cap", () => {
51
+ const argsText = "a".repeat(MAX + 10);
52
+ const call = createRunTelemetryToolCall({
53
+ toolName: "t",
54
+ toolCallId: "call-1",
55
+ argsText,
56
+ args: { ignored: true },
57
+ });
58
+ expect(call.tool_args).toBe(argsText.slice(0, MAX));
59
+ });
60
+
61
+ it("omits fields whose value cannot be serialized", () => {
62
+ const circular: Record<string, unknown> = {};
63
+ circular.self = circular;
64
+ expect(
65
+ createRunTelemetryToolCall({
66
+ toolName: "t",
67
+ toolCallId: "call-1",
68
+ args: circular,
69
+ result: undefined,
70
+ }),
71
+ ).toEqual({ tool_name: "t", tool_call_id: "call-1" });
72
+ });
73
+
74
+ it("summarizes base64 image and audio blocks in an mcp result", () => {
75
+ const call = createRunTelemetryToolCall({
76
+ toolName: "t",
77
+ toolCallId: "call-1",
78
+ toolSource: "mcp",
79
+ result: [
80
+ { type: "text", text: "keep me" },
81
+ { type: "image", data: "A".repeat(4096) },
82
+ ],
83
+ });
84
+ expect(call.tool_source).toBe("mcp");
85
+ expect(call.tool_result).toContain("keep me");
86
+ expect(call.tool_result).toContain("[image: 3.0KB]");
87
+ expect(call.tool_result).not.toContain("A".repeat(200));
88
+ });
89
+
90
+ it("leaves a non-mcp result unsummarized", () => {
91
+ const result = [{ type: "image", data: "A".repeat(4096) }];
92
+ const call = createRunTelemetryToolCall({
93
+ toolName: "t",
94
+ toolCallId: "call-1",
95
+ toolSource: "frontend",
96
+ result,
97
+ });
98
+ expect(call.tool_source).toBe("frontend");
99
+ expect(call.tool_result).toBe(JSON.stringify(result));
100
+ });
101
+ });
102
+
103
+ describe("normalizeRunTelemetryUsage", () => {
104
+ it("prefers the current names over the legacy ones", () => {
105
+ expect(
106
+ normalizeRunTelemetryUsage({
107
+ inputTokens: 1,
108
+ outputTokens: 2,
109
+ promptTokens: 90,
110
+ completionTokens: 90,
111
+ }),
112
+ ).toEqual({ inputTokens: 1, outputTokens: 2 });
113
+ });
114
+
115
+ it("falls back to the legacy prompt and completion names", () => {
116
+ expect(
117
+ normalizeRunTelemetryUsage({ promptTokens: 3, completionTokens: 4 }),
118
+ ).toEqual({ inputTokens: 3, outputTokens: 4 });
119
+ });
120
+
121
+ it("keeps a zero count and omits an absent one", () => {
122
+ expect(
123
+ normalizeRunTelemetryUsage({ inputTokens: 0, cachedInputTokens: 5 }),
124
+ ).toEqual({ inputTokens: 0, cachedInputTokens: 5 });
125
+ });
126
+
127
+ it("returns undefined when no count is present", () => {
128
+ expect(normalizeRunTelemetryUsage({})).toBeUndefined();
129
+ });
130
+ });
@@ -0,0 +1,140 @@
1
+ import type { SamplingCallData } from "./instrumentMcpSampling";
2
+
3
+ const MAX_TELEMETRY_TEXT_LENGTH = 50_000;
4
+
5
+ const BASE64_PATTERN = /^[A-Za-z0-9+/]{100,}={0,2}$/;
6
+
7
+ export type AssistantCloudRunReportToolCall = {
8
+ tool_name: string;
9
+ tool_call_id: string;
10
+ tool_args?: string;
11
+ tool_result?: string;
12
+ tool_source?: "mcp" | "frontend" | "backend";
13
+ start_ms?: number;
14
+ end_ms?: number;
15
+ sampling_calls?: SamplingCallData[];
16
+ };
17
+
18
+ /**
19
+ * Clamps a string to the size the runs endpoint accepts for a single span
20
+ * field.
21
+ */
22
+ export function truncateRunTelemetryText(value: string): string {
23
+ if (value.length <= MAX_TELEMETRY_TEXT_LENGTH) return value;
24
+ return value.slice(0, MAX_TELEMETRY_TEXT_LENGTH);
25
+ }
26
+
27
+ function safeStringify(value: unknown): string | undefined {
28
+ if (value == null) return undefined;
29
+ try {
30
+ return truncateRunTelemetryText(JSON.stringify(value));
31
+ } catch {
32
+ return undefined;
33
+ }
34
+ }
35
+
36
+ function summarizeMcpResult(value: unknown): string | undefined {
37
+ if (value == null) return undefined;
38
+ try {
39
+ const parsed = typeof value === "string" ? JSON.parse(value) : value;
40
+ if (Array.isArray(parsed)) {
41
+ const summarized = parsed.map((item) => {
42
+ if (item && typeof item === "object" && item.type) {
43
+ if (
44
+ (item.type === "image" || item.type === "audio") &&
45
+ typeof item.data === "string" &&
46
+ BASE64_PATTERN.test(item.data.slice(0, 200))
47
+ ) {
48
+ const sizeKB = ((item.data.length * 3) / 4 / 1024).toFixed(1);
49
+ return { ...item, data: `[${item.type}: ${sizeKB}KB]` };
50
+ }
51
+ }
52
+ return item;
53
+ });
54
+ return truncateRunTelemetryText(JSON.stringify(summarized));
55
+ }
56
+ } catch {
57
+ // not JSON array, fall through
58
+ }
59
+ return safeStringify(value);
60
+ }
61
+
62
+ export type RunTelemetryToolCallInit = {
63
+ toolName: string;
64
+ toolCallId: string;
65
+ args?: unknown;
66
+ /**
67
+ * Pre-serialized arguments, used in place of serializing `args`. Values over
68
+ * the span size are clamped before they are included in the report.
69
+ */
70
+ argsText?: string | undefined;
71
+ result?: unknown;
72
+ toolSource?: "mcp" | "frontend" | "backend" | undefined;
73
+ };
74
+
75
+ /**
76
+ * Serializes one tool call into the shape the runs endpoint accepts. An `mcp`
77
+ * source has its result summarized, because MCP content blocks carry inline
78
+ * base64 image and audio payloads that would otherwise dominate the report.
79
+ */
80
+ export function createRunTelemetryToolCall(
81
+ init: RunTelemetryToolCallInit,
82
+ ): AssistantCloudRunReportToolCall {
83
+ const { toolName, toolCallId, args, argsText, result, toolSource } = init;
84
+ const call: AssistantCloudRunReportToolCall = {
85
+ tool_name: toolName,
86
+ tool_call_id: toolCallId,
87
+ };
88
+ const toolArgs =
89
+ argsText != null ? truncateRunTelemetryText(argsText) : safeStringify(args);
90
+ if (toolArgs !== undefined) call.tool_args = toolArgs;
91
+ const toolResult =
92
+ toolSource === "mcp" ? summarizeMcpResult(result) : safeStringify(result);
93
+ if (toolResult !== undefined) call.tool_result = toolResult;
94
+ if (toolSource) call.tool_source = toolSource;
95
+ return call;
96
+ }
97
+
98
+ export type RunTelemetryUsage = {
99
+ inputTokens?: number;
100
+ outputTokens?: number;
101
+ reasoningTokens?: number;
102
+ cachedInputTokens?: number;
103
+ };
104
+
105
+ export type RunTelemetryUsageInit = RunTelemetryUsage & {
106
+ promptTokens?: number;
107
+ completionTokens?: number;
108
+ };
109
+
110
+ /**
111
+ * Resolves the token counts a provider reports under either the current or the
112
+ * legacy prompt/completion names. Returns undefined when no count is present,
113
+ * so callers can tell an empty usage object from a zeroed one.
114
+ */
115
+ export function normalizeRunTelemetryUsage(
116
+ usage: RunTelemetryUsageInit,
117
+ ): RunTelemetryUsage | undefined {
118
+ const inputTokens = usage.inputTokens ?? usage.promptTokens;
119
+ const outputTokens = usage.outputTokens ?? usage.completionTokens;
120
+
121
+ if (
122
+ inputTokens == null &&
123
+ outputTokens == null &&
124
+ usage.reasoningTokens == null &&
125
+ usage.cachedInputTokens == null
126
+ ) {
127
+ return undefined;
128
+ }
129
+
130
+ return {
131
+ ...(inputTokens != null ? { inputTokens } : undefined),
132
+ ...(outputTokens != null ? { outputTokens } : undefined),
133
+ ...(usage.reasoningTokens != null
134
+ ? { reasoningTokens: usage.reasoningTokens }
135
+ : undefined),
136
+ ...(usage.cachedInputTokens != null
137
+ ? { cachedInputTokens: usage.cachedInputTokens }
138
+ : undefined),
139
+ };
140
+ }
@@ -0,0 +1,39 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { AssistantCloud } from "../AssistantCloud";
3
+ import type { AssistantCloudTelemetryConfig } from "../AssistantCloudAPI";
4
+
5
+ const createCloud = (
6
+ telemetry?: ConstructorParameters<typeof AssistantCloud>[0]["telemetry"],
7
+ ) =>
8
+ new AssistantCloud({
9
+ apiKey: "test-key",
10
+ userId: "user-id",
11
+ workspaceId: "workspace-id",
12
+ ...(telemetry !== undefined ? { telemetry } : {}),
13
+ });
14
+
15
+ describe("AssistantCloud telemetry config", () => {
16
+ it("defaults to enabled", () => {
17
+ expect(createCloud().telemetry.enabled).toBe(true);
18
+ expect(createCloud(true).telemetry.enabled).toBe(true);
19
+ });
20
+
21
+ it("disables when configured off", () => {
22
+ expect(createCloud(false).telemetry.enabled).toBe(false);
23
+ expect(createCloud({ enabled: false }).telemetry.enabled).toBe(false);
24
+ });
25
+
26
+ it("stays enabled when the config object carries an undefined enabled", () => {
27
+ const beforeReport: NonNullable<
28
+ AssistantCloudTelemetryConfig["beforeReport"]
29
+ > = (report) => report;
30
+ // JS consumers (and TS apps without exactOptionalPropertyTypes) can pass
31
+ // an explicitly-undefined enabled, e.g. { enabled: cfg.enabled }.
32
+ const telemetry = createCloud({
33
+ enabled: undefined,
34
+ beforeReport,
35
+ } as unknown as AssistantCloudTelemetryConfig).telemetry;
36
+ expect(telemetry.enabled).toBe(true);
37
+ expect(telemetry.beforeReport).toBe(beforeReport);
38
+ });
39
+ });
@@ -169,6 +169,31 @@ describe("AssistantCloudAPI", () => {
169
169
  expect(error.status).toBe(400);
170
170
  });
171
171
 
172
+ it("falls back to the response text when the JSON error body has no message", async () => {
173
+ const fetchMock = vi.fn().mockResolvedValue({
174
+ ok: false,
175
+ status: 429,
176
+ headers: new Headers(),
177
+ text: vi
178
+ .fn()
179
+ .mockResolvedValue(JSON.stringify({ error: "rate limited" })),
180
+ });
181
+ vi.stubGlobal("fetch", fetchMock);
182
+
183
+ const api = new AssistantCloudAPI({
184
+ apiKey: "test-key",
185
+ userId: "u-1",
186
+ workspaceId: "w-1",
187
+ });
188
+
189
+ const error = await api.makeRawRequest("/threads").catch((e) => e);
190
+ expect(error).toBeInstanceOf(CloudAPIError);
191
+ expect(error.message).toBe(
192
+ 'Request failed with status 429, {"error":"rate limited"}',
193
+ );
194
+ expect(error.status).toBe(429);
195
+ });
196
+
172
197
  it("throws generic error with status for non-JSON error responses", async () => {
173
198
  const fetchMock = vi.fn().mockResolvedValue({
174
199
  ok: false,