assistant-cloud 0.1.43 → 0.2.1

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 (120) hide show
  1. package/README.md +66 -0
  2. package/dist/AssistantCloud.d.ts +6 -1
  3. package/dist/AssistantCloud.d.ts.map +1 -1
  4. package/dist/AssistantCloud.js +13 -5
  5. package/dist/AssistantCloud.js.map +1 -1
  6. package/dist/AssistantCloudAPI.d.ts +28 -3
  7. package/dist/AssistantCloudAPI.d.ts.map +1 -1
  8. package/dist/AssistantCloudAPI.js +37 -10
  9. package/dist/AssistantCloudAPI.js.map +1 -1
  10. package/dist/AssistantCloudAuthStrategy.d.ts +4 -1
  11. package/dist/AssistantCloudAuthStrategy.d.ts.map +1 -1
  12. package/dist/AssistantCloudAuthStrategy.js +15 -3
  13. package/dist/AssistantCloudAuthStrategy.js.map +1 -1
  14. package/dist/AssistantCloudEvents.d.ts +32 -0
  15. package/dist/AssistantCloudEvents.d.ts.map +1 -0
  16. package/dist/AssistantCloudEvents.js +119 -0
  17. package/dist/AssistantCloudEvents.js.map +1 -0
  18. package/dist/AssistantCloudFiles.d.ts +12 -1
  19. package/dist/AssistantCloudFiles.d.ts.map +1 -1
  20. package/dist/AssistantCloudFiles.js +13 -1
  21. package/dist/AssistantCloudFiles.js.map +1 -1
  22. package/dist/AssistantCloudRuns.d.ts +12 -0
  23. package/dist/AssistantCloudRuns.d.ts.map +1 -1
  24. package/dist/AssistantCloudRuns.js +2 -1
  25. package/dist/AssistantCloudRuns.js.map +1 -1
  26. package/dist/AssistantCloudScores.d.ts +27 -0
  27. package/dist/AssistantCloudScores.d.ts.map +1 -0
  28. package/dist/AssistantCloudScores.js +30 -0
  29. package/dist/AssistantCloudScores.js.map +1 -0
  30. package/dist/AssistantCloudThreadMessages.d.ts +11 -1
  31. package/dist/AssistantCloudThreadMessages.d.ts.map +1 -1
  32. package/dist/AssistantCloudThreadMessages.js +12 -1
  33. package/dist/AssistantCloudThreadMessages.js.map +1 -1
  34. package/dist/AssistantCloudThreads.d.ts +8 -0
  35. package/dist/AssistantCloudThreads.d.ts.map +1 -1
  36. package/dist/AssistantCloudThreads.js +9 -1
  37. package/dist/AssistantCloudThreads.js.map +1 -1
  38. package/dist/CloudEngagementReporter.d.ts +74 -0
  39. package/dist/CloudEngagementReporter.d.ts.map +1 -0
  40. package/dist/CloudEngagementReporter.js +132 -0
  41. package/dist/CloudEngagementReporter.js.map +1 -0
  42. package/dist/CloudMessagePersistence.d.ts +12 -9
  43. package/dist/CloudMessagePersistence.d.ts.map +1 -1
  44. package/dist/CloudMessagePersistence.js +54 -20
  45. package/dist/CloudMessagePersistence.js.map +1 -1
  46. package/dist/CloudRunReporter.d.ts +20 -0
  47. package/dist/CloudRunReporter.d.ts.map +1 -0
  48. package/dist/CloudRunReporter.js +38 -0
  49. package/dist/CloudRunReporter.js.map +1 -0
  50. package/dist/FormattedCloudPersistence.d.ts +3 -8
  51. package/dist/FormattedCloudPersistence.d.ts.map +1 -1
  52. package/dist/FormattedCloudPersistence.js +3 -8
  53. package/dist/FormattedCloudPersistence.js.map +1 -1
  54. package/dist/ai-sdk/index.d.ts +33 -0
  55. package/dist/ai-sdk/index.d.ts.map +1 -0
  56. package/dist/ai-sdk/index.js +147 -0
  57. package/dist/ai-sdk/index.js.map +1 -0
  58. package/dist/cloudResponse.d.ts +4 -1
  59. package/dist/cloudResponse.d.ts.map +1 -1
  60. package/dist/cloudResponse.js +14 -1
  61. package/dist/cloudResponse.js.map +1 -1
  62. package/dist/generateThreadTitle.js +1 -0
  63. package/dist/generateThreadTitle.js.map +1 -1
  64. package/dist/index.d.ts +10 -4
  65. package/dist/index.js +7 -2
  66. package/dist/instrumentMcpSampling.d.ts.map +1 -1
  67. package/dist/instrumentMcpSampling.js +12 -2
  68. package/dist/instrumentMcpSampling.js.map +1 -1
  69. package/dist/runTelemetry.d.ts +86 -7
  70. package/dist/runTelemetry.d.ts.map +1 -1
  71. package/dist/runTelemetry.js +144 -1
  72. package/dist/runTelemetry.js.map +1 -1
  73. package/dist/telemetry/index.d.ts +32 -0
  74. package/dist/telemetry/index.d.ts.map +1 -0
  75. package/dist/telemetry/index.js +62 -0
  76. package/dist/telemetry/index.js.map +1 -0
  77. package/dist/version.d.ts +5 -0
  78. package/dist/version.d.ts.map +1 -0
  79. package/dist/version.js +6 -0
  80. package/dist/version.js.map +1 -0
  81. package/package.json +43 -6
  82. package/src/AssistantCloud.ts +23 -8
  83. package/src/AssistantCloudAPI.ts +72 -10
  84. package/src/AssistantCloudAuthStrategy.ts +17 -2
  85. package/src/AssistantCloudEvents.test.ts +149 -0
  86. package/src/AssistantCloudEvents.ts +190 -0
  87. package/src/AssistantCloudFiles.test.ts +59 -0
  88. package/src/AssistantCloudFiles.ts +39 -0
  89. package/src/AssistantCloudRuns.ts +13 -1
  90. package/src/AssistantCloudScores.test.ts +113 -0
  91. package/src/AssistantCloudScores.ts +61 -0
  92. package/src/AssistantCloudThreadMessages.test.ts +51 -0
  93. package/src/AssistantCloudThreadMessages.ts +33 -0
  94. package/src/AssistantCloudThreads.test.ts +21 -0
  95. package/src/AssistantCloudThreads.ts +21 -0
  96. package/src/CloudEngagementReporter.ts +231 -0
  97. package/src/CloudMessagePersistence.ts +70 -27
  98. package/src/CloudRunReporter.ts +38 -0
  99. package/src/FormattedCloudPersistence.ts +3 -8
  100. package/src/ai-sdk/index.test.ts +258 -0
  101. package/src/ai-sdk/index.ts +222 -0
  102. package/src/cloudResponse.test.ts +34 -1
  103. package/src/cloudResponse.ts +30 -0
  104. package/src/generateThreadTitle.test.ts +32 -0
  105. package/src/generateThreadTitle.ts +1 -0
  106. package/src/index.ts +35 -2
  107. package/src/instrumentMcpSampling.test.ts +102 -0
  108. package/src/instrumentMcpSampling.ts +16 -2
  109. package/src/runTelemetry.test.ts +325 -0
  110. package/src/runTelemetry.ts +274 -6
  111. package/src/telemetry/index.test.ts +252 -0
  112. package/src/telemetry/index.ts +98 -0
  113. package/src/tests/AssistantCloud.test.ts +55 -1
  114. package/src/tests/AssistantCloudAPI.test.ts +95 -4
  115. package/src/tests/AssistantCloudAuthStrategy.test.ts +93 -2
  116. package/src/tests/AssistantCloudProjects.test.ts +1 -5
  117. package/src/tests/CloudEngagementReporter.test.ts +184 -0
  118. package/src/tests/CloudMessagePersistence.test.ts +350 -0
  119. package/src/tests/CloudRunReporter.test.ts +100 -0
  120. package/src/version.ts +4 -0
@@ -18,14 +18,9 @@ export type MessageFormatAdapter<TMessage, TStorageFormat> = {
18
18
  };
19
19
 
20
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.
21
+ * Wraps a CloudMessagePersistence with a MessageFormatAdapter's encode and
22
+ * decode. The persistence parameter is typed structurally, so a caller does
23
+ * not need to import the class.
29
24
  */
30
25
  export const createFormattedPersistence = <TMessage, TStorageFormat>(
31
26
  persistence: {
@@ -0,0 +1,258 @@
1
+ import type { UIMessage } from "ai";
2
+ import { describe, expect, it } from "vitest";
3
+ import {
4
+ aiSDKV6FormatAdapter,
5
+ extractAISDKRunTelemetry,
6
+ type AISDKMessageLike,
7
+ } from "./index";
8
+
9
+ const assistant = (
10
+ parts: UIMessage["parts"],
11
+ metadata?: Record<string, unknown>,
12
+ id = "a1",
13
+ ): UIMessage =>
14
+ ({
15
+ id,
16
+ role: "assistant",
17
+ parts,
18
+ ...(metadata ? { metadata } : {}),
19
+ }) as UIMessage;
20
+
21
+ describe("aiSDKV6FormatAdapter", () => {
22
+ it("stores a message without its id and restores it from the row", () => {
23
+ const message = assistant([{ type: "text", text: "hi" }], { a: 1 });
24
+ const content = aiSDKV6FormatAdapter.encode({ parentId: null, message });
25
+ expect(content).toEqual({
26
+ role: "assistant",
27
+ parts: [{ type: "text", text: "hi" }],
28
+ metadata: { a: 1 },
29
+ });
30
+ expect(
31
+ aiSDKV6FormatAdapter.decode({
32
+ id: "a1",
33
+ parent_id: "u1",
34
+ format: "ai-sdk/v6",
35
+ content,
36
+ }),
37
+ ).toEqual({ parentId: "u1", message });
38
+ expect(aiSDKV6FormatAdapter.getId(message)).toBe("a1");
39
+ });
40
+ });
41
+
42
+ describe("extractAISDKRunTelemetry", () => {
43
+ it("returns null without an assistant message", () => {
44
+ expect(
45
+ extractAISDKRunTelemetry([
46
+ { role: "user", parts: [{ type: "text", text: "q" }] },
47
+ ]),
48
+ ).toBeNull();
49
+ });
50
+
51
+ it("reads tool calls, steps and usage from one streamed message", () => {
52
+ const message = assistant(
53
+ [
54
+ { type: "step-start" },
55
+ {
56
+ type: "tool-weather",
57
+ toolCallId: "call_1",
58
+ state: "output-available",
59
+ input: { city: "Oslo" },
60
+ output: { temp: 3 },
61
+ } as UIMessage["parts"][number],
62
+ { type: "step-start" },
63
+ {
64
+ type: "dynamic-tool",
65
+ toolName: "search",
66
+ toolCallId: "call_2",
67
+ state: "output-available",
68
+ input: { q: "x" },
69
+ output: [{ type: "text", text: "found" }],
70
+ } as UIMessage["parts"][number],
71
+ { type: "step-start" },
72
+ { type: "text", text: "Cold." },
73
+ ],
74
+ {
75
+ usage: { inputTokens: 10, outputTokens: 5 },
76
+ steps: [
77
+ { usage: { promptTokens: 4, completionTokens: 1 } },
78
+ { usage: { inputTokens: 6, outputTokens: 4 } },
79
+ {},
80
+ ],
81
+ samplingCalls: { call_2: [{ duration_ms: 20 }] },
82
+ modelId: "gpt-5.6-luna",
83
+ },
84
+ );
85
+
86
+ expect(extractAISDKRunTelemetry([message])).toEqual({
87
+ assistantMessageId: "a1",
88
+ status: "completed",
89
+ toolCalls: [
90
+ {
91
+ tool_name: "weather",
92
+ tool_call_id: "call_1",
93
+ tool_args: '{"city":"Oslo"}',
94
+ tool_result: '{"temp":3}',
95
+ tool_source: "frontend",
96
+ },
97
+ {
98
+ tool_name: "search",
99
+ tool_call_id: "call_2",
100
+ tool_args: '{"q":"x"}',
101
+ tool_result: '[{"type":"text","text":"found"}]',
102
+ tool_source: "mcp",
103
+ sampling_calls: [{ duration_ms: 20 }],
104
+ },
105
+ ],
106
+ steps: [
107
+ {
108
+ usage: { promptTokens: 4, completionTokens: 1 },
109
+ toolCalls: [expect.objectContaining({ tool_call_id: "call_1" })],
110
+ finishReason: "tool-calls",
111
+ },
112
+ {
113
+ usage: { inputTokens: 6, outputTokens: 4 },
114
+ toolCalls: [expect.objectContaining({ tool_call_id: "call_2" })],
115
+ finishReason: "tool-calls",
116
+ },
117
+ {},
118
+ ],
119
+ totalSteps: 3,
120
+ outputText: "Cold.",
121
+ usage: { inputTokens: 10, outputTokens: 5 },
122
+ modelId: "gpt-5.6-luna",
123
+ metadata: message.metadata,
124
+ });
125
+ });
126
+
127
+ it("sums usage over stored rows without ids and reads legacy tool fields", () => {
128
+ const rows: AISDKMessageLike[] = [
129
+ {
130
+ role: "assistant",
131
+ parts: [
132
+ { type: "step-start" },
133
+ {
134
+ type: "tool-lookup",
135
+ toolCallId: "call_1",
136
+ state: "output-available",
137
+ args: { id: 1 },
138
+ result: "ok",
139
+ } as unknown as UIMessage["parts"][number],
140
+ ],
141
+ metadata: { steps: [{ usage: { inputTokens: 2, outputTokens: 1 } }] },
142
+ },
143
+ {
144
+ role: "assistant",
145
+ parts: [{ type: "step-start" }, { type: "text", text: "done" }],
146
+ metadata: {
147
+ steps: [{ usage: { inputTokens: 3, reasoningTokens: 7 } }],
148
+ },
149
+ },
150
+ ];
151
+ const result = extractAISDKRunTelemetry(rows);
152
+ expect(result?.assistantMessageId).toBeUndefined();
153
+ expect(result?.status).toBe("completed");
154
+ expect(result?.totalSteps).toBe(2);
155
+ expect(result?.usage).toEqual({
156
+ inputTokens: 5,
157
+ outputTokens: 1,
158
+ reasoningTokens: 7,
159
+ });
160
+ expect(result?.toolCalls).toEqual([
161
+ {
162
+ tool_name: "lookup",
163
+ tool_call_id: "call_1",
164
+ tool_args: '{"id":1}',
165
+ tool_result: '"ok"',
166
+ tool_source: "frontend",
167
+ },
168
+ ]);
169
+ });
170
+
171
+ it("attaches sampling calls from the row that made the tool call", () => {
172
+ const rows: AISDKMessageLike[] = [
173
+ {
174
+ role: "assistant",
175
+ parts: [
176
+ {
177
+ type: "dynamic-tool",
178
+ toolName: "search",
179
+ toolCallId: "call_1",
180
+ state: "output-available",
181
+ input: {},
182
+ output: "x",
183
+ } as UIMessage["parts"][number],
184
+ ],
185
+ metadata: { samplingCalls: { call_1: [{ duration_ms: 5 }] } },
186
+ },
187
+ {
188
+ role: "assistant",
189
+ parts: [{ type: "text", text: "done" }],
190
+ metadata: { samplingCalls: {} },
191
+ },
192
+ ];
193
+ expect(extractAISDKRunTelemetry(rows)?.toolCalls).toEqual([
194
+ expect.objectContaining({
195
+ tool_call_id: "call_1",
196
+ tool_source: "mcp",
197
+ sampling_calls: [{ duration_ms: 5 }],
198
+ }),
199
+ ]);
200
+ });
201
+
202
+ it("reads a tool only run as completed with its step closed by the calls", () => {
203
+ const call = (toolCallId: string, state: string, output?: unknown) =>
204
+ ({
205
+ type: "tool-search",
206
+ toolCallId,
207
+ state,
208
+ input: {},
209
+ ...(output !== undefined ? { output } : {}),
210
+ }) as UIMessage["parts"][number];
211
+
212
+ const answered = extractAISDKRunTelemetry([
213
+ assistant([{ type: "step-start" }, call("a", "output-available", [])]),
214
+ ]);
215
+ expect(answered?.status).toBe("completed");
216
+ expect(answered?.steps?.[0]?.finishReason).toBe("tool-calls");
217
+ expect(answered?.outputText).toBeUndefined();
218
+
219
+ const pending = extractAISDKRunTelemetry([
220
+ assistant([{ type: "step-start" }, call("b", "input-available")]),
221
+ ]);
222
+ expect(pending?.status).toBe("completed");
223
+ expect(pending?.steps?.[0]?.finishReason).toBe("tool-calls");
224
+ });
225
+
226
+ it("reads incomplete for a run that produced nothing", () => {
227
+ const empty = extractAISDKRunTelemetry([
228
+ assistant([{ type: "step-start" }]),
229
+ ]);
230
+ expect(empty?.status).toBe("incomplete");
231
+ expect(empty?.totalSteps).toBe(1);
232
+ expect(empty?.steps?.[0]?.finishReason).toBeUndefined();
233
+ expect(empty?.outputText).toBeUndefined();
234
+ });
235
+
236
+ it("reads no steps without step markers", () => {
237
+ const result = extractAISDKRunTelemetry([
238
+ assistant([{ type: "text", text: "hi" }]),
239
+ ]);
240
+ expect(result?.steps).toBeUndefined();
241
+ expect(result?.totalSteps).toBeUndefined();
242
+ });
243
+
244
+ it("skips a stored part without a type", () => {
245
+ const result = extractAISDKRunTelemetry([
246
+ {
247
+ role: "assistant",
248
+ parts: [
249
+ null,
250
+ { toolCallId: "a" },
251
+ { type: "text", text: "hi" },
252
+ ] as never,
253
+ },
254
+ ]);
255
+ expect(result?.outputText).toBe("hi");
256
+ expect(result?.toolCalls).toBeUndefined();
257
+ });
258
+ });
@@ -0,0 +1,222 @@
1
+ import type { UIMessage } from "ai";
2
+ import type { MessageFormatAdapter } from "../FormattedCloudPersistence";
3
+ import type { SamplingCallData } from "../instrumentMcpSampling";
4
+ import {
5
+ type AssistantCloudRunReportToolCall,
6
+ createRunTelemetryToolCall,
7
+ extractRunTelemetryModelId,
8
+ normalizeRunTelemetryUsage,
9
+ type RunMessageTelemetry,
10
+ type RunReportStepInit,
11
+ type RunTelemetryUsage,
12
+ type RunTelemetryUsageInit,
13
+ truncateRunTelemetryText,
14
+ } from "../runTelemetry";
15
+
16
+ export type AISDKStorageFormat = Omit<UIMessage, "id">;
17
+
18
+ /** The stored form of an AI SDK message: the message without its id. */
19
+ export const aiSDKV6FormatAdapter: MessageFormatAdapter<
20
+ UIMessage,
21
+ AISDKStorageFormat
22
+ > = {
23
+ format: "ai-sdk/v6",
24
+ encode: ({ message: { id: _id, ...message } }) => message,
25
+ decode: (stored) => ({
26
+ parentId: stored.parent_id,
27
+ message: { id: stored.id, ...stored.content } as UIMessage,
28
+ }),
29
+ getId: (message) => message.id,
30
+ };
31
+
32
+ /**
33
+ * A message as an AI SDK integration holds it, or as the cloud stored it under
34
+ * the ai-sdk/v6 format, which drops the id. Parts are typed by their `type`
35
+ * alone, so this shape and the telemetry read from it need nothing from `ai`.
36
+ */
37
+ export type AISDKMessageLike = {
38
+ id?: string | undefined;
39
+ role: string;
40
+ parts: readonly { type: string; [key: string]: unknown }[];
41
+ metadata?: unknown;
42
+ };
43
+
44
+ type Part = Record<string, unknown> & { type: string };
45
+
46
+ function isRecord(value: unknown): value is Record<string, unknown> {
47
+ return value !== null && typeof value === "object";
48
+ }
49
+
50
+ function isPart(value: unknown): value is Part {
51
+ return isRecord(value) && typeof value.type === "string";
52
+ }
53
+
54
+ /**
55
+ * The AI SDK's own tool part rules, kept here so the entry loads without the
56
+ * `ai` runtime: a static tool part is `tool-<name>`, a dynamic one is
57
+ * `dynamic-tool` with its name in `toolName`.
58
+ */
59
+ function toolCallOf(part: Part): AssistantCloudRunReportToolCall | undefined {
60
+ if (typeof part.toolCallId !== "string") return undefined;
61
+ const isStatic = part.type.startsWith("tool-");
62
+ if (!isStatic && part.type !== "dynamic-tool") return undefined;
63
+ const toolName = isStatic
64
+ ? part.type.slice("tool-".length)
65
+ : typeof part.toolName === "string"
66
+ ? part.toolName
67
+ : undefined;
68
+ if (!toolName) return undefined;
69
+ return createRunTelemetryToolCall({
70
+ toolName,
71
+ toolCallId: part.toolCallId,
72
+ args: part.input ?? part.args,
73
+ result: part.output ?? part.result,
74
+ toolSource: isStatic ? "frontend" : "mcp",
75
+ });
76
+ }
77
+
78
+ function attachSamplingCalls(
79
+ toolCalls: readonly AssistantCloudRunReportToolCall[],
80
+ metadata: Record<string, unknown> | undefined,
81
+ ): void {
82
+ const samplingCalls = isRecord(metadata?.samplingCalls)
83
+ ? (metadata.samplingCalls as Record<string, SamplingCallData[]>)
84
+ : undefined;
85
+ if (!samplingCalls) return;
86
+ for (const toolCall of toolCalls) {
87
+ const calls = samplingCalls[toolCall.tool_call_id];
88
+ if (Array.isArray(calls) && calls.length > 0) {
89
+ toolCall.sampling_calls = calls;
90
+ }
91
+ }
92
+ }
93
+
94
+ function stepUsages(
95
+ metadata: Record<string, unknown> | undefined,
96
+ ): (RunTelemetryUsageInit | undefined)[] {
97
+ const steps = metadata?.steps;
98
+ if (!Array.isArray(steps)) return [];
99
+ return steps.map((step) =>
100
+ isRecord(step) && isRecord(step.usage)
101
+ ? (step.usage as RunTelemetryUsageInit)
102
+ : undefined,
103
+ );
104
+ }
105
+
106
+ function sumUsage(usages: readonly RunTelemetryUsage[]): RunTelemetryUsage {
107
+ const total: RunTelemetryUsage = {};
108
+ for (const usage of usages) {
109
+ if (usage.inputTokens != null) {
110
+ total.inputTokens = (total.inputTokens ?? 0) + usage.inputTokens;
111
+ }
112
+ if (usage.outputTokens != null) {
113
+ total.outputTokens = (total.outputTokens ?? 0) + usage.outputTokens;
114
+ }
115
+ if (usage.reasoningTokens != null) {
116
+ total.reasoningTokens =
117
+ (total.reasoningTokens ?? 0) + usage.reasoningTokens;
118
+ }
119
+ if (usage.cachedInputTokens != null) {
120
+ total.cachedInputTokens =
121
+ (total.cachedInputTokens ?? 0) + usage.cachedInputTokens;
122
+ }
123
+ }
124
+ return total;
125
+ }
126
+
127
+ /**
128
+ * The usage a message reports: `metadata.usage` when the integration copied
129
+ * the run total there, else the sum over `metadata.steps[].usage`.
130
+ */
131
+ function messageUsage(
132
+ metadata: Record<string, unknown> | undefined,
133
+ ): RunTelemetryUsage | undefined {
134
+ const total = isRecord(metadata?.usage)
135
+ ? normalizeRunTelemetryUsage(metadata.usage as RunTelemetryUsageInit)
136
+ : undefined;
137
+ if (total) return total;
138
+ const perStep = stepUsages(metadata).flatMap((usage) => {
139
+ const normalized = usage ? normalizeRunTelemetryUsage(usage) : undefined;
140
+ return normalized ? [normalized] : [];
141
+ });
142
+ return perStep.length > 0 ? sumUsage(perStep) : undefined;
143
+ }
144
+
145
+ /**
146
+ * Reads the run report fields out of the assistant messages of one run. A run
147
+ * the AI SDK streamed as one message is one element; a run the cloud stored as
148
+ * several assistant rows is aggregated, step by step, in order. Returns null
149
+ * when no assistant message is present. Status reads completed when the run
150
+ * produced text or tool calls, as a live finish with reason `tool-calls` does;
151
+ * an integration that observed the finish event overrides it.
152
+ */
153
+ export function extractAISDKRunTelemetry(
154
+ messages: readonly AISDKMessageLike[],
155
+ ): RunMessageTelemetry | null {
156
+ const textParts: string[] = [];
157
+ const toolCalls: AssistantCloudRunReportToolCall[] = [];
158
+ const steps: RunReportStepInit[] = [];
159
+ const usages: RunTelemetryUsage[] = [];
160
+ let assistant: AISDKMessageLike | undefined;
161
+
162
+ for (const message of messages) {
163
+ if (message.role !== "assistant") continue;
164
+ assistant = message;
165
+ const metadata = isRecord(message.metadata) ? message.metadata : undefined;
166
+ const usagePerStep = stepUsages(metadata);
167
+ const messageToolCalls: AssistantCloudRunReportToolCall[] = [];
168
+ let step: RunReportStepInit | undefined;
169
+ let stepIndex = -1;
170
+
171
+ for (const part of message.parts) {
172
+ if (!isPart(part)) continue;
173
+ if (part.type === "step-start") {
174
+ stepIndex += 1;
175
+ const usage = usagePerStep[stepIndex];
176
+ step = usage ? { usage } : {};
177
+ steps.push(step);
178
+ continue;
179
+ }
180
+ if (part.type === "text" && typeof part.text === "string" && part.text) {
181
+ textParts.push(part.text);
182
+ continue;
183
+ }
184
+ const toolCall = toolCallOf(part);
185
+ if (!toolCall) continue;
186
+ toolCalls.push(toolCall);
187
+ messageToolCalls.push(toolCall);
188
+ if (step) {
189
+ step.toolCalls = [...(step.toolCalls ?? []), toolCall];
190
+ step.finishReason = "tool-calls";
191
+ }
192
+ }
193
+
194
+ attachSamplingCalls(messageToolCalls, metadata);
195
+ const usage = messageUsage(metadata);
196
+ if (usage) usages.push(usage);
197
+ }
198
+
199
+ if (!assistant) return null;
200
+
201
+ const metadata = isRecord(assistant.metadata)
202
+ ? assistant.metadata
203
+ : undefined;
204
+
205
+ const usage = usages.length > 0 ? sumUsage(usages) : undefined;
206
+ const modelId = extractRunTelemetryModelId(metadata);
207
+ const completed = textParts.length > 0 || toolCalls.length > 0;
208
+ return {
209
+ ...(assistant.id !== undefined
210
+ ? { assistantMessageId: assistant.id }
211
+ : undefined),
212
+ status: completed ? "completed" : "incomplete",
213
+ ...(toolCalls.length > 0 ? { toolCalls } : undefined),
214
+ ...(steps.length > 0 ? { steps, totalSteps: steps.length } : undefined),
215
+ ...(textParts.length > 0
216
+ ? { outputText: truncateRunTelemetryText(textParts.join("")) }
217
+ : undefined),
218
+ ...(usage ? { usage } : undefined),
219
+ ...(modelId ? { modelId } : undefined),
220
+ ...(metadata ? { metadata } : undefined),
221
+ };
222
+ }
@@ -1,5 +1,10 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { CloudResponseError, readCloudTimestamp } from "./cloudResponse";
2
+ import {
3
+ CloudResponseError,
4
+ readCloudNullableNumber,
5
+ readCloudNumber,
6
+ readCloudTimestamp,
7
+ } from "./cloudResponse";
3
8
 
4
9
  describe("readCloudTimestamp", () => {
5
10
  it("decodes a canonical Cloud timestamp", () => {
@@ -26,3 +31,31 @@ describe("readCloudTimestamp", () => {
26
31
  );
27
32
  });
28
33
  });
34
+
35
+ describe("readCloudNumber", () => {
36
+ it("returns a finite number", () => {
37
+ expect(readCloudNumber(0.5, "score.value")).toBe(0.5);
38
+ expect(readCloudNumber(0, "score.value")).toBe(0);
39
+ });
40
+
41
+ it.each([null, undefined, "1", true, Number.NaN, Number.POSITIVE_INFINITY])(
42
+ "rejects %s",
43
+ (value) => {
44
+ const decode = () => readCloudNumber(value, "score.value");
45
+ expect(decode).toThrow(CloudResponseError);
46
+ expect(decode).toThrow(
47
+ 'Invalid Assistant Cloud response for "score.value": expected a number',
48
+ );
49
+ },
50
+ );
51
+ });
52
+
53
+ describe("readCloudNullableNumber", () => {
54
+ it("passes null through and validates everything else", () => {
55
+ expect(readCloudNullableNumber(null, "score.value")).toBeNull();
56
+ expect(readCloudNullableNumber(1, "score.value")).toBe(1);
57
+ expect(() => readCloudNullableNumber("1", "score.value")).toThrow(
58
+ CloudResponseError,
59
+ );
60
+ });
61
+ });
@@ -32,6 +32,36 @@ export const readCloudString = (value: unknown, field: string): string => {
32
32
  return value;
33
33
  };
34
34
 
35
+ export const readCloudNumber = (value: unknown, field: string): number => {
36
+ if (typeof value !== "number" || !Number.isFinite(value)) {
37
+ throw invalidCloudResponse(field, "a number");
38
+ }
39
+ return value;
40
+ };
41
+
42
+ export const readCloudNullableNumber = (
43
+ value: unknown,
44
+ field: string,
45
+ ): number | null => {
46
+ if (value === null) return null;
47
+ return readCloudNumber(value, field);
48
+ };
49
+
50
+ export const readCloudEnum = <const T extends readonly string[]>(
51
+ value: unknown,
52
+ field: string,
53
+ allowed: T,
54
+ ): T[number] => {
55
+ const text = readCloudString(value, field);
56
+ if (!allowed.includes(text)) {
57
+ throw invalidCloudResponse(
58
+ field,
59
+ `one of ${allowed.map((entry) => `"${entry}"`).join(", ")}`,
60
+ );
61
+ }
62
+ return text;
63
+ };
64
+
35
65
  export const readCloudNullableString = (
36
66
  value: unknown,
37
67
  field: string,
@@ -68,4 +68,36 @@ describe("generateThreadTitle", () => {
68
68
 
69
69
  expect(update).not.toHaveBeenCalled();
70
70
  });
71
+
72
+ it("returns null without updating for a whitespace-only title", async () => {
73
+ const { cloud, update } = createCloud(
74
+ titleStream({ type: "text-delta", textDelta: " \n\t" }),
75
+ );
76
+
77
+ await expect(
78
+ generateThreadTitle(cloud, {
79
+ threadId: "thread-1",
80
+ messages: [],
81
+ }),
82
+ ).resolves.toBeNull();
83
+
84
+ expect(update).not.toHaveBeenCalled();
85
+ });
86
+
87
+ it("trims surrounding whitespace from a generated title", async () => {
88
+ const { cloud, update } = createCloud(
89
+ titleStream({ type: "text-delta", textDelta: " Weather chat \n" }),
90
+ );
91
+
92
+ await expect(
93
+ generateThreadTitle(cloud, {
94
+ threadId: "thread-1",
95
+ messages: [],
96
+ }),
97
+ ).resolves.toBe("Weather chat");
98
+
99
+ expect(update).toHaveBeenCalledExactlyOnceWith("thread-1", {
100
+ title: "Weather chat",
101
+ });
102
+ });
71
103
  });
@@ -30,6 +30,7 @@ export async function generateThreadTitle(
30
30
  reader.releaseLock();
31
31
  }
32
32
 
33
+ title = title.trim();
33
34
  if (title) {
34
35
  await cloud.threads.update(options.threadId, { title });
35
36
  }
package/src/index.ts CHANGED
@@ -1,19 +1,52 @@
1
- export type { CloudMessage } from "./AssistantCloudThreadMessages";
2
- export type { AssistantCloudTelemetryConfig } from "./AssistantCloudAPI";
1
+ export type {
2
+ CloudMessage,
3
+ AssistantCloudThreadMessageFeedbackBody,
4
+ AssistantCloudThreadMessageFeedbackResponse,
5
+ } from "./AssistantCloudThreadMessages";
6
+ export type {
7
+ AssistantCloudTelemetryConfig,
8
+ SdkIdentity,
9
+ } from "./AssistantCloudAPI";
10
+ export {
11
+ AssistantCloudEvents,
12
+ type AssistantCloudEvent,
13
+ type AssistantCloudEventKind,
14
+ } from "./AssistantCloudEvents";
15
+ export {
16
+ AssistantCloudScores,
17
+ type AssistantCloudScoreBody,
18
+ type AssistantCloudScoreResponse,
19
+ } from "./AssistantCloudScores";
20
+ export type { GeneratePresignedDownloadUrlResponse } from "./AssistantCloudFiles";
3
21
  export { CloudAPIError } from "./AssistantCloudAPI";
4
22
  export { CloudResponseError } from "./cloudResponse";
5
23
  export { generateThreadTitle } from "./generateThreadTitle";
6
24
  export type { AssistantCloudRunReport } from "./AssistantCloudRuns";
7
25
  export {
26
+ createRunReport,
8
27
  createRunTelemetryToolCall,
28
+ deriveRunOutcome,
29
+ describeRunError,
30
+ extractRunTelemetryModelId,
9
31
  normalizeRunTelemetryUsage,
10
32
  truncateRunTelemetryText,
11
33
  type AssistantCloudRunReportToolCall,
34
+ type RunMessageTelemetry,
35
+ type RunReportInit,
36
+ type RunReportOutcome,
37
+ type RunReportStepInit,
12
38
  type RunTelemetryToolCallInit,
13
39
  type RunTelemetryUsage,
14
40
  type RunTelemetryUsageInit,
15
41
  } from "./runTelemetry";
16
42
  export { AssistantCloud } from "./AssistantCloud";
43
+ export { CloudRunReporter, type CloudRunReportInit } from "./CloudRunReporter";
44
+ export {
45
+ CloudEngagementReporter,
46
+ type EngagementEventIds,
47
+ type EngagementIdResolver,
48
+ } from "./CloudEngagementReporter";
49
+ export { readAnonymousRefreshToken } from "./AssistantCloudAuthStrategy";
17
50
  export { CloudMessagePersistence } from "./CloudMessagePersistence";
18
51
  export {
19
52
  createFormattedPersistence,