assistant-cloud 0.1.16 → 0.1.18

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/dist/AssistantCloud.d.ts +2 -1
  2. package/dist/AssistantCloud.d.ts.map +1 -1
  3. package/dist/AssistantCloud.js +9 -1
  4. package/dist/AssistantCloud.js.map +1 -1
  5. package/dist/AssistantCloudAPI.d.ts +24 -1
  6. package/dist/AssistantCloudAPI.d.ts.map +1 -1
  7. package/dist/AssistantCloudAPI.js +3 -1
  8. package/dist/AssistantCloudAPI.js.map +1 -1
  9. package/dist/AssistantCloudAuthStrategy.d.ts.map +1 -1
  10. package/dist/AssistantCloudAuthStrategy.js.map +1 -1
  11. package/dist/AssistantCloudAuthTokens.d.ts.map +1 -1
  12. package/dist/AssistantCloudAuthTokens.js.map +1 -1
  13. package/dist/AssistantCloudFiles.d.ts.map +1 -1
  14. package/dist/AssistantCloudFiles.js.map +1 -1
  15. package/dist/AssistantCloudRuns.d.ts +51 -0
  16. package/dist/AssistantCloudRuns.d.ts.map +1 -1
  17. package/dist/AssistantCloudRuns.js +3 -0
  18. package/dist/AssistantCloudRuns.js.map +1 -1
  19. package/dist/AssistantCloudThreadMessages.d.ts +4 -0
  20. package/dist/AssistantCloudThreadMessages.d.ts.map +1 -1
  21. package/dist/AssistantCloudThreadMessages.js +3 -0
  22. package/dist/AssistantCloudThreadMessages.js.map +1 -1
  23. package/dist/AssistantCloudThreads.d.ts.map +1 -1
  24. package/dist/AssistantCloudThreads.js.map +1 -1
  25. package/dist/CloudMessagePersistence.d.ts +57 -0
  26. package/dist/CloudMessagePersistence.d.ts.map +1 -0
  27. package/dist/CloudMessagePersistence.js +103 -0
  28. package/dist/CloudMessagePersistence.js.map +1 -0
  29. package/dist/FormattedCloudPersistence.d.ts +56 -0
  30. package/dist/FormattedCloudPersistence.d.ts.map +1 -0
  31. package/dist/FormattedCloudPersistence.js +39 -0
  32. package/dist/FormattedCloudPersistence.js.map +1 -0
  33. package/dist/index.d.ts +5 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +3 -0
  36. package/dist/index.js.map +1 -1
  37. package/dist/instrumentMcpSampling.d.ts +75 -0
  38. package/dist/instrumentMcpSampling.d.ts.map +1 -0
  39. package/dist/instrumentMcpSampling.js +65 -0
  40. package/dist/instrumentMcpSampling.js.map +1 -0
  41. package/package.json +3 -3
  42. package/src/{AssistantCloud.tsx → AssistantCloud.ts} +14 -1
  43. package/src/{AssistantCloudAPI.tsx → AssistantCloudAPI.ts} +31 -3
  44. package/src/AssistantCloudRuns.ts +99 -0
  45. package/src/{AssistantCloudThreadMessages.tsx → AssistantCloudThreadMessages.ts} +15 -0
  46. package/src/CloudMessagePersistence.ts +123 -0
  47. package/src/FormattedCloudPersistence.ts +96 -0
  48. package/src/index.ts +13 -0
  49. package/src/instrumentMcpSampling.ts +109 -0
  50. package/src/tests/AssistantCloudAPI.test.ts +129 -0
  51. package/src/tests/CloudMessagePersistence.test.ts +152 -0
  52. package/src/tests/FormattedCloudPersistence.test.ts +134 -0
  53. package/src/AssistantCloudRuns.tsx +0 -44
  54. /package/src/{AssistantCloudAuthStrategy.tsx → AssistantCloudAuthStrategy.ts} +0 -0
  55. /package/src/{AssistantCloudAuthTokens.tsx → AssistantCloudAuthTokens.ts} +0 -0
  56. /package/src/{AssistantCloudFiles.tsx → AssistantCloudFiles.ts} +0 -0
  57. /package/src/{AssistantCloudThreads.tsx → AssistantCloudThreads.ts} +0 -0
package/dist/index.d.ts CHANGED
@@ -1,3 +1,8 @@
1
1
  export type { CloudMessage } from "./AssistantCloudThreadMessages.js";
2
+ export type { AssistantCloudTelemetryConfig } from "./AssistantCloudAPI.js";
3
+ export type { AssistantCloudRunReport } from "./AssistantCloudRuns.js";
2
4
  export { AssistantCloud } from "./AssistantCloud.js";
5
+ export { CloudMessagePersistence } from "./CloudMessagePersistence.js";
6
+ export { createFormattedPersistence, type MessageFormatAdapter, } from "./FormattedCloudPersistence.js";
7
+ export { wrapSamplingHandler, createSamplingCollector, type SamplingCallData, type McpSamplingHandler, } from "./instrumentMcpSampling.js";
3
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,YAAY,EAAE,0CAAuC;AACnE,OAAO,EAAE,cAAc,EAAE,4BAAyB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,YAAY,EAAE,0CAAuC;AACnE,YAAY,EAAE,6BAA6B,EAAE,+BAA4B;AACzE,YAAY,EAAE,uBAAuB,EAAE,gCAA6B;AACpE,OAAO,EAAE,cAAc,EAAE,4BAAyB;AAClD,OAAO,EAAE,uBAAuB,EAAE,qCAAkC;AACpE,OAAO,EACL,0BAA0B,EAC1B,KAAK,oBAAoB,GAC1B,uCAAoC;AACrC,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EACvB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,mCAAgC"}
package/dist/index.js CHANGED
@@ -1,2 +1,5 @@
1
1
  export { AssistantCloud } from "./AssistantCloud.js";
2
+ export { CloudMessagePersistence } from "./CloudMessagePersistence.js";
3
+ export { createFormattedPersistence, } from "./FormattedCloudPersistence.js";
4
+ export { wrapSamplingHandler, createSamplingCollector, } from "./instrumentMcpSampling.js";
2
5
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,4BAAyB"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,4BAAyB;AAClD,OAAO,EAAE,uBAAuB,EAAE,qCAAkC;AACpE,OAAO,EACL,0BAA0B,GAE3B,uCAAoC;AACrC,OAAO,EACL,mBAAmB,EACnB,uBAAuB,GAGxB,mCAAgC"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * MCP sampling instrumentation utility.
3
+ *
4
+ * Wraps an MCP client's sampling handler to capture nested LLM calls
5
+ * (sampling/createMessage requests) made during tool execution.
6
+ * The captured data can be reported as child generation spans.
7
+ */
8
+ export type SamplingCallData = {
9
+ model_id?: string;
10
+ input_tokens?: number;
11
+ output_tokens?: number;
12
+ duration_ms?: number;
13
+ };
14
+ export type McpSamplingHandler = (request: McpSamplingRequest) => Promise<McpSamplingResponse>;
15
+ export type McpSamplingRequest = {
16
+ method: "sampling/createMessage";
17
+ params: {
18
+ messages: unknown[];
19
+ modelPreferences?: {
20
+ hints?: {
21
+ name?: string;
22
+ }[];
23
+ };
24
+ maxTokens?: number;
25
+ [key: string]: unknown;
26
+ };
27
+ };
28
+ export type McpSamplingResponse = {
29
+ model?: string;
30
+ content: unknown;
31
+ usage?: {
32
+ inputTokens?: number;
33
+ outputTokens?: number;
34
+ promptTokens?: number;
35
+ completionTokens?: number;
36
+ };
37
+ [key: string]: unknown;
38
+ };
39
+ /**
40
+ * Wraps an MCP sampling handler to intercept and measure sampling calls.
41
+ *
42
+ * @param handler - The original sampling handler from the MCP client
43
+ * @param onSamplingCall - Callback invoked with metrics for each sampling call
44
+ * @returns A wrapped handler that transparently captures sampling metrics
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * const samplingCalls: SamplingCallData[] = [];
49
+ * const wrapped = wrapSamplingHandler(
50
+ * originalHandler,
51
+ * (data) => samplingCalls.push(data),
52
+ * );
53
+ * // Use `wrapped` as the MCP client's sampling handler
54
+ * // After tool execution, `samplingCalls` contains metrics for all nested LLM calls
55
+ * ```
56
+ */
57
+ export declare function wrapSamplingHandler(handler: McpSamplingHandler, onSamplingCall: (data: SamplingCallData) => void): McpSamplingHandler;
58
+ /**
59
+ * Creates a collector that accumulates sampling call data during tool execution.
60
+ * Use with `wrapSamplingHandler` to capture all sampling calls for a tool invocation.
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * const collector = createSamplingCollector();
65
+ * const wrappedHandler = wrapSamplingHandler(handler, collector.collect);
66
+ * // ... execute MCP tool ...
67
+ * const calls = collector.getCalls(); // SamplingCallData[]
68
+ * ```
69
+ */
70
+ export declare function createSamplingCollector(): {
71
+ collect: (data: SamplingCallData) => number;
72
+ getCalls: () => SamplingCallData[];
73
+ reset: () => void;
74
+ };
75
+ //# sourceMappingURL=instrumentMcpSampling.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instrumentMcpSampling.d.ts","sourceRoot":"","sources":["../src/instrumentMcpSampling.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,CAC/B,OAAO,EAAE,kBAAkB,KACxB,OAAO,CAAC,mBAAmB,CAAC,CAAC;AAElC,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,wBAAwB,CAAC;IACjC,MAAM,EAAE;QACN,QAAQ,EAAE,OAAO,EAAE,CAAC;QACpB,gBAAgB,CAAC,EAAE;YAAE,KAAK,CAAC,EAAE;gBAAE,IAAI,CAAC,EAAE,MAAM,CAAA;aAAE,EAAE,CAAA;SAAE,CAAC;QACnD,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE;QACN,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;KAC3B,CAAC;IACF,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,kBAAkB,EAC3B,cAAc,EAAE,CAAC,IAAI,EAAE,gBAAgB,KAAK,IAAI,GAC/C,kBAAkB,CAuBpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB;oBAGnB,gBAAgB;;;EAMnC"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * MCP sampling instrumentation utility.
3
+ *
4
+ * Wraps an MCP client's sampling handler to capture nested LLM calls
5
+ * (sampling/createMessage requests) made during tool execution.
6
+ * The captured data can be reported as child generation spans.
7
+ */
8
+ /**
9
+ * Wraps an MCP sampling handler to intercept and measure sampling calls.
10
+ *
11
+ * @param handler - The original sampling handler from the MCP client
12
+ * @param onSamplingCall - Callback invoked with metrics for each sampling call
13
+ * @returns A wrapped handler that transparently captures sampling metrics
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * const samplingCalls: SamplingCallData[] = [];
18
+ * const wrapped = wrapSamplingHandler(
19
+ * originalHandler,
20
+ * (data) => samplingCalls.push(data),
21
+ * );
22
+ * // Use `wrapped` as the MCP client's sampling handler
23
+ * // After tool execution, `samplingCalls` contains metrics for all nested LLM calls
24
+ * ```
25
+ */
26
+ export function wrapSamplingHandler(handler, onSamplingCall) {
27
+ return async (request) => {
28
+ const startTime = Date.now();
29
+ const response = await handler(request);
30
+ const durationMs = Date.now() - startTime;
31
+ const modelId = response.model ?? request.params.modelPreferences?.hints?.[0]?.name;
32
+ const inputTokens = response.usage?.inputTokens ?? response.usage?.promptTokens;
33
+ const outputTokens = response.usage?.outputTokens ?? response.usage?.completionTokens;
34
+ onSamplingCall({
35
+ ...(modelId ? { model_id: modelId } : undefined),
36
+ ...(inputTokens != null ? { input_tokens: inputTokens } : undefined),
37
+ ...(outputTokens != null ? { output_tokens: outputTokens } : undefined),
38
+ duration_ms: durationMs,
39
+ });
40
+ return response;
41
+ };
42
+ }
43
+ /**
44
+ * Creates a collector that accumulates sampling call data during tool execution.
45
+ * Use with `wrapSamplingHandler` to capture all sampling calls for a tool invocation.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * const collector = createSamplingCollector();
50
+ * const wrappedHandler = wrapSamplingHandler(handler, collector.collect);
51
+ * // ... execute MCP tool ...
52
+ * const calls = collector.getCalls(); // SamplingCallData[]
53
+ * ```
54
+ */
55
+ export function createSamplingCollector() {
56
+ const calls = [];
57
+ return {
58
+ collect: (data) => calls.push(data),
59
+ getCalls: () => [...calls],
60
+ reset: () => {
61
+ calls.length = 0;
62
+ },
63
+ };
64
+ }
65
+ //# sourceMappingURL=instrumentMcpSampling.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instrumentMcpSampling.js","sourceRoot":"","sources":["../src/instrumentMcpSampling.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAmCH;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,mBAAmB,CACjC,OAA2B,EAC3B,cAAgD;IAEhD,OAAO,KAAK,EAAE,OAAO,EAAE,EAAE;QACvB,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC7B,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;QACxC,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;QAE1C,MAAM,OAAO,GACX,QAAQ,CAAC,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,gBAAgB,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC;QAEtE,MAAM,WAAW,GACf,QAAQ,CAAC,KAAK,EAAE,WAAW,IAAI,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;QAC9D,MAAM,YAAY,GAChB,QAAQ,CAAC,KAAK,EAAE,YAAY,IAAI,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAC;QAEnE,cAAc,CAAC;YACb,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;YAChD,GAAG,CAAC,WAAW,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;YACpE,GAAG,CAAC,YAAY,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;YACvE,WAAW,EAAE,UAAU;SACxB,CAAC,CAAC;QAEH,OAAO,QAAQ,CAAC;IAClB,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,uBAAuB;IACrC,MAAM,KAAK,GAAuB,EAAE,CAAC;IACrC,OAAO;QACL,OAAO,EAAE,CAAC,IAAsB,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC;QACrD,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,KAAK,CAAC;QAC1B,KAAK,EAAE,GAAG,EAAE;YACV,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;QACnB,CAAC;KACF,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assistant-cloud",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Cloud integration for assistant-ui",
5
5
  "keywords": [
6
6
  "assistant",
@@ -28,10 +28,10 @@
28
28
  ],
29
29
  "sideEffects": false,
30
30
  "dependencies": {
31
- "assistant-stream": "^0.3.1"
31
+ "assistant-stream": "^0.3.3"
32
32
  },
33
33
  "devDependencies": {
34
- "@types/node": "^25.2.0",
34
+ "@types/node": "^25.2.1",
35
35
  "vitest": "^4.0.18",
36
36
  "@assistant-ui/x-buildutils": "0.0.1"
37
37
  },
@@ -1,4 +1,8 @@
1
- import { AssistantCloudAPI, AssistantCloudConfig } from "./AssistantCloudAPI";
1
+ import {
2
+ AssistantCloudAPI,
3
+ AssistantCloudConfig,
4
+ AssistantCloudTelemetryConfig,
5
+ } from "./AssistantCloudAPI";
2
6
  import { AssistantCloudAuthTokens } from "./AssistantCloudAuthTokens";
3
7
  import { AssistantCloudRuns } from "./AssistantCloudRuns";
4
8
  import { AssistantCloudThreads } from "./AssistantCloudThreads";
@@ -9,6 +13,7 @@ export class AssistantCloud {
9
13
  public readonly auth;
10
14
  public readonly runs;
11
15
  public readonly files;
16
+ public readonly telemetry: AssistantCloudTelemetryConfig;
12
17
 
13
18
  constructor(config: AssistantCloudConfig) {
14
19
  const api = new AssistantCloudAPI(config);
@@ -18,5 +23,13 @@ export class AssistantCloud {
18
23
  };
19
24
  this.runs = new AssistantCloudRuns(api);
20
25
  this.files = new AssistantCloudFiles(api);
26
+
27
+ const t = config.telemetry;
28
+ this.telemetry =
29
+ t === false
30
+ ? { enabled: false }
31
+ : t === true || t === undefined
32
+ ? { enabled: true }
33
+ : { enabled: t.enabled !== false, ...t };
21
34
  }
22
35
  }
@@ -4,8 +4,21 @@ import {
4
4
  AssistantCloudAPIKeyAuthStrategy,
5
5
  AssistantCloudAnonymousAuthStrategy,
6
6
  } from "./AssistantCloudAuthStrategy";
7
+ import type { AssistantCloudRunReport } from "./AssistantCloudRuns";
7
8
 
8
- export type AssistantCloudConfig =
9
+ export type AssistantCloudTelemetryConfig = {
10
+ enabled?: boolean;
11
+ /**
12
+ * Called before each telemetry report is sent.
13
+ * Return a modified report to enrich it (e.g. add `model_id`),
14
+ * or return `null` to skip the report.
15
+ */
16
+ beforeReport?: (
17
+ report: AssistantCloudRunReport,
18
+ ) => AssistantCloudRunReport | null;
19
+ };
20
+
21
+ export type AssistantCloudConfig = (
9
22
  | {
10
23
  baseUrl: string;
11
24
  authToken: () => Promise<string | null>;
@@ -18,7 +31,21 @@ export type AssistantCloudConfig =
18
31
  | {
19
32
  baseUrl: string;
20
33
  anonymous: true;
21
- };
34
+ }
35
+ ) & {
36
+ /**
37
+ * Client-side run telemetry reporting. Default: `true`.
38
+ *
39
+ * When enabled, the SDK automatically reports run metadata (status, step
40
+ * count, tool calls, and token usage) to Assistant Cloud after each
41
+ * assistant message is saved. No message content is sent.
42
+ *
43
+ * - `true` / `undefined` — enabled with defaults
44
+ * - `false` — disabled
45
+ * - `{ beforeReport }` — enabled with a hook to enrich or filter reports
46
+ */
47
+ telemetry?: boolean | AssistantCloudTelemetryConfig;
48
+ };
22
49
 
23
50
  class CloudAPIError extends Error {
24
51
  constructor(message: string) {
@@ -104,7 +131,8 @@ export class AssistantCloudAPI {
104
131
  try {
105
132
  const body = JSON.parse(text);
106
133
  throw new CloudAPIError(body.message);
107
- } catch {
134
+ } catch (error) {
135
+ if (error instanceof CloudAPIError) throw error;
108
136
  throw new Error(
109
137
  `Request failed with status ${response.status}, ${text}`,
110
138
  );
@@ -0,0 +1,99 @@
1
+ import { AssistantCloudAPI } from "./AssistantCloudAPI";
2
+ import { AssistantStream, PlainTextDecoder } from "assistant-stream";
3
+
4
+ type AssistantCloudRunsStreamBody = {
5
+ thread_id: string;
6
+ assistant_id: "system/thread_title";
7
+ messages: readonly unknown[]; // TODO type
8
+ };
9
+
10
+ export type AssistantCloudRunReport = {
11
+ thread_id: string;
12
+ status: "completed" | "incomplete" | "error";
13
+ total_steps?: number;
14
+ tool_calls?: {
15
+ tool_name: string;
16
+ tool_call_id: string;
17
+ tool_args?: string;
18
+ tool_result?: string;
19
+ tool_source?: "mcp" | "frontend" | "backend";
20
+ start_ms?: number;
21
+ end_ms?: number;
22
+ sampling_calls?: {
23
+ model_id?: string;
24
+ input_tokens?: number;
25
+ output_tokens?: number;
26
+ duration_ms?: number;
27
+ }[];
28
+ }[];
29
+ steps?: {
30
+ input_tokens?: number;
31
+ output_tokens?: number;
32
+ tool_calls?: {
33
+ tool_name: string;
34
+ tool_call_id: string;
35
+ tool_args?: string;
36
+ tool_result?: string;
37
+ tool_source?: "mcp" | "frontend" | "backend";
38
+ start_ms?: number;
39
+ end_ms?: number;
40
+ sampling_calls?: {
41
+ model_id?: string;
42
+ input_tokens?: number;
43
+ output_tokens?: number;
44
+ duration_ms?: number;
45
+ }[];
46
+ }[];
47
+ start_ms?: number;
48
+ end_ms?: number;
49
+ }[];
50
+ input_tokens?: number;
51
+ output_tokens?: number;
52
+ model_id?: string;
53
+ provider_type?: string;
54
+ duration_ms?: number;
55
+ output_text?: string;
56
+ metadata?: Record<string, unknown>;
57
+ };
58
+
59
+ export class AssistantCloudRuns {
60
+ constructor(private cloud: AssistantCloudAPI) {}
61
+
62
+ public __internal_getAssistantOptions(assistantId: string) {
63
+ return {
64
+ api: `${this.cloud._baseUrl}/v1/runs/stream`,
65
+ headers: async () => {
66
+ const headers = await this.cloud._auth.getAuthHeaders();
67
+ if (!headers) throw new Error("Authorization failed");
68
+ return {
69
+ ...headers,
70
+ Accept: "text/plain",
71
+ };
72
+ },
73
+ body: {
74
+ assistant_id: assistantId,
75
+ response_format: "vercel-ai-data-stream/v1",
76
+ thread_id: "unstable_todo",
77
+ },
78
+ };
79
+ }
80
+
81
+ public async stream(
82
+ body: AssistantCloudRunsStreamBody,
83
+ ): Promise<AssistantStream> {
84
+ const response = await this.cloud.makeRawRequest("/runs/stream", {
85
+ method: "POST",
86
+ headers: {
87
+ Accept: "text/plain",
88
+ },
89
+ body,
90
+ });
91
+ return AssistantStream.fromResponse(response, new PlainTextDecoder());
92
+ }
93
+
94
+ public async report(
95
+ body: AssistantCloudRunReport,
96
+ ): Promise<{ run_id: string }> {
97
+ return this.cloud.makeRequest("/runs", { method: "POST", body });
98
+ }
99
+ }
@@ -29,6 +29,10 @@ type AssistantCloudMessageCreateResponse = {
29
29
  message_id: string;
30
30
  };
31
31
 
32
+ type AssistantCloudThreadMessageUpdateBody = {
33
+ content: ReadonlyJSONObject;
34
+ };
35
+
32
36
  export class AssistantCloudThreadMessages {
33
37
  constructor(private cloud: AssistantCloudAPI) {}
34
38
 
@@ -51,4 +55,15 @@ export class AssistantCloudThreadMessages {
51
55
  { method: "POST", body },
52
56
  );
53
57
  }
58
+
59
+ public async update(
60
+ threadId: string,
61
+ messageId: string,
62
+ body: AssistantCloudThreadMessageUpdateBody,
63
+ ): Promise<void> {
64
+ return this.cloud.makeRequest(
65
+ `/threads/${encodeURIComponent(threadId)}/messages/${encodeURIComponent(messageId)}`,
66
+ { method: "PUT", body },
67
+ );
68
+ }
54
69
  }
@@ -0,0 +1,123 @@
1
+ import type { ReadonlyJSONObject } from "assistant-stream/utils";
2
+ import type { AssistantCloud } from "./AssistantCloud";
3
+
4
+ /**
5
+ * Shared persistence logic for cloud message storage.
6
+ *
7
+ * Handles ID mapping (local → remote) and parent_id chaining for both:
8
+ * - AssistantCloudThreadHistoryAdapter (assistant-ui runtime)
9
+ * - useCloudChat (standalone AI SDK hook)
10
+ *
11
+ * The promise-based ID resolution handles concurrent appends — if message B's
12
+ * parent is message A, and A is still being created, we await A's promise
13
+ * to get its remote ID before creating B.
14
+ */
15
+ export class CloudMessagePersistence {
16
+ private idMapping: Record<string, string | Promise<string>> = {};
17
+
18
+ constructor(private cloud: AssistantCloud) {}
19
+
20
+ /**
21
+ * Persist a message to the cloud.
22
+ *
23
+ * @param threadId - Remote thread ID
24
+ * @param messageId - Local message ID (used for tracking)
25
+ * @param parentId - Local parent message ID (or null for first message)
26
+ * @param format - Message format (e.g., "aui/v0", "ai-sdk/v6")
27
+ * @param content - Message content (format-specific)
28
+ */
29
+ async append(
30
+ threadId: string,
31
+ messageId: string,
32
+ parentId: string | null,
33
+ format: string,
34
+ content: ReadonlyJSONObject,
35
+ ): Promise<void> {
36
+ // Resolve parent's remote ID if it exists (may be a promise if concurrent)
37
+ const resolvedParentId = parentId
38
+ ? ((await this.idMapping[parentId]) ?? parentId)
39
+ : null;
40
+
41
+ const task = this.cloud.threads.messages
42
+ .create(threadId, {
43
+ parent_id: resolvedParentId,
44
+ format,
45
+ content,
46
+ })
47
+ .then(({ message_id }) => {
48
+ this.idMapping[messageId] = message_id;
49
+ return message_id;
50
+ })
51
+ .catch((err) => {
52
+ // Only delete if we're still the active task (avoids clobbering a retry)
53
+ if (this.idMapping[messageId] === task) {
54
+ delete this.idMapping[messageId];
55
+ }
56
+ throw err;
57
+ });
58
+
59
+ // Store the promise immediately so concurrent appends can await it
60
+ this.idMapping[messageId] = task;
61
+ return task.then(() => {});
62
+ }
63
+
64
+ /**
65
+ * Update an already-persisted message in the cloud.
66
+ */
67
+ async update(
68
+ threadId: string,
69
+ messageId: string,
70
+ _format: string,
71
+ content: ReadonlyJSONObject,
72
+ ): Promise<void> {
73
+ const remoteId = await this.getRemoteId(messageId);
74
+ if (!remoteId) return; // not persisted yet, skip
75
+ await this.cloud.threads.messages.update(threadId, remoteId, { content });
76
+ }
77
+
78
+ /**
79
+ * Check if a message has been persisted (or is currently being persisted).
80
+ */
81
+ isPersisted(messageId: string): boolean {
82
+ return messageId in this.idMapping;
83
+ }
84
+
85
+ /**
86
+ * Get the remote ID for a local message ID (resolved).
87
+ * Returns undefined if not persisted.
88
+ */
89
+ async getRemoteId(messageId: string): Promise<string | undefined> {
90
+ const entry = this.idMapping[messageId];
91
+ if (!entry) return undefined;
92
+ return entry;
93
+ }
94
+
95
+ /**
96
+ * Load messages from the cloud and populate the ID mapping.
97
+ *
98
+ * The ID mapping is populated so that `isPersisted()` returns true for
99
+ * loaded messages, preventing re-persistence of already-stored messages.
100
+ *
101
+ * @param threadId - Remote thread ID
102
+ * @param format - Optional format filter
103
+ * @returns Array of cloud messages
104
+ */
105
+ async load(threadId: string, format?: string) {
106
+ const { messages } = await this.cloud.threads.messages.list(
107
+ threadId,
108
+ format ? { format } : undefined,
109
+ );
110
+ // Populate ID mapping so isPersisted() recognizes loaded messages
111
+ for (const m of messages) {
112
+ this.idMapping[m.id] = m.id;
113
+ }
114
+ return messages;
115
+ }
116
+
117
+ /**
118
+ * Reset the ID mapping (call when switching threads).
119
+ */
120
+ reset() {
121
+ this.idMapping = {};
122
+ }
123
+ }
@@ -0,0 +1,96 @@
1
+ import type { ReadonlyJSONObject } from "assistant-stream/utils";
2
+
3
+ /**
4
+ * Format adapter shape — structurally identical to the MessageFormatAdapter
5
+ * in @assistant-ui/react, but defined here to avoid cross-package type moves.
6
+ * TypeScript's structural typing ensures these are interchangeable.
7
+ */
8
+ export type MessageFormatAdapter<TMessage, TStorageFormat> = {
9
+ format: string;
10
+ encode(item: { parentId: string | null; message: TMessage }): TStorageFormat;
11
+ decode(stored: {
12
+ id: string;
13
+ parent_id: string | null;
14
+ format: string;
15
+ content: TStorageFormat;
16
+ }): { parentId: string | null; message: TMessage };
17
+ getId(message: TMessage): string;
18
+ };
19
+
20
+ /**
21
+ * Wraps a CloudMessagePersistence instance with format-aware encode/decode.
22
+ *
23
+ * This centralizes the pattern used by both:
24
+ * - useCloudChat (standalone AI SDK hook)
25
+ * - AssistantCloudThreadHistoryAdapter.withFormat() (assistant-ui runtime)
26
+ *
27
+ * The persistence parameter is typed structurally (not by class) so callers
28
+ * don't need to import CloudMessagePersistence directly.
29
+ */
30
+ export const createFormattedPersistence = <TMessage, TStorageFormat>(
31
+ persistence: {
32
+ append: (
33
+ threadId: string,
34
+ messageId: string,
35
+ parentId: string | null,
36
+ format: string,
37
+ content: ReadonlyJSONObject,
38
+ ) => Promise<void>;
39
+ load: (threadId: string, format?: string) => Promise<any[]>;
40
+ isPersisted: (messageId: string) => boolean;
41
+ update?: (
42
+ threadId: string,
43
+ messageId: string,
44
+ format: string,
45
+ content: ReadonlyJSONObject,
46
+ ) => Promise<void>;
47
+ },
48
+ adapter: MessageFormatAdapter<TMessage, TStorageFormat>,
49
+ ) => ({
50
+ append: async (
51
+ threadId: string,
52
+ item: { parentId: string | null; message: TMessage },
53
+ ): Promise<void> => {
54
+ const messageId = adapter.getId(item.message);
55
+ const encoded = adapter.encode(item);
56
+ return persistence.append(
57
+ threadId,
58
+ messageId,
59
+ item.parentId,
60
+ adapter.format,
61
+ encoded as ReadonlyJSONObject,
62
+ );
63
+ },
64
+ update: persistence.update
65
+ ? async (
66
+ threadId: string,
67
+ item: { parentId: string | null; message: TMessage },
68
+ messageId: string,
69
+ ): Promise<void> => {
70
+ const encoded = adapter.encode(item);
71
+ return persistence.update!(
72
+ threadId,
73
+ messageId,
74
+ adapter.format,
75
+ encoded as ReadonlyJSONObject,
76
+ );
77
+ }
78
+ : undefined,
79
+ load: async (threadId: string) => {
80
+ const messages = await persistence.load(threadId, adapter.format);
81
+ return {
82
+ messages: messages
83
+ .filter((m) => m.format === adapter.format)
84
+ .map((m) =>
85
+ adapter.decode({
86
+ id: m.id,
87
+ parent_id: m.parent_id,
88
+ format: m.format,
89
+ content: m.content as TStorageFormat,
90
+ }),
91
+ )
92
+ .reverse(),
93
+ };
94
+ },
95
+ isPersisted: (messageId: string) => persistence.isPersisted(messageId),
96
+ });
package/src/index.ts CHANGED
@@ -1,2 +1,15 @@
1
1
  export type { CloudMessage } from "./AssistantCloudThreadMessages";
2
+ export type { AssistantCloudTelemetryConfig } from "./AssistantCloudAPI";
3
+ export type { AssistantCloudRunReport } from "./AssistantCloudRuns";
2
4
  export { AssistantCloud } from "./AssistantCloud";
5
+ export { CloudMessagePersistence } from "./CloudMessagePersistence";
6
+ export {
7
+ createFormattedPersistence,
8
+ type MessageFormatAdapter,
9
+ } from "./FormattedCloudPersistence";
10
+ export {
11
+ wrapSamplingHandler,
12
+ createSamplingCollector,
13
+ type SamplingCallData,
14
+ type McpSamplingHandler,
15
+ } from "./instrumentMcpSampling";