assistant-cloud 0.1.41 → 0.1.43
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 +3 -3
- package/dist/AssistantCloud.js +2 -2
- package/dist/AssistantCloud.js.map +1 -1
- package/dist/AssistantCloudAPI.d.ts.map +1 -1
- package/dist/AssistantCloudAPI.js +5 -5
- package/dist/AssistantCloudAPI.js.map +1 -1
- package/dist/AssistantCloudAuthStrategy.d.ts.map +1 -1
- package/dist/AssistantCloudAuthStrategy.js +68 -17
- package/dist/AssistantCloudAuthStrategy.js.map +1 -1
- package/dist/AssistantCloudRuns.d.ts +3 -13
- package/dist/AssistantCloudRuns.d.ts.map +1 -1
- package/dist/AssistantCloudRuns.js.map +1 -1
- package/dist/CloudMessagePersistence.d.ts.map +1 -1
- package/dist/CloudMessagePersistence.js +19 -11
- package/dist/CloudMessagePersistence.js.map +1 -1
- package/dist/generateThreadTitle.d.ts +15 -0
- package/dist/generateThreadTitle.d.ts.map +1 -0
- package/dist/generateThreadTitle.js +25 -0
- package/dist/generateThreadTitle.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/runTelemetry.d.ts +61 -0
- package/dist/runTelemetry.d.ts.map +1 -0
- package/dist/runTelemetry.js +82 -0
- package/dist/runTelemetry.js.map +1 -0
- package/package.json +5 -5
- package/src/AssistantCloud.ts +1 -1
- package/src/AssistantCloudAPI.ts +9 -8
- package/src/AssistantCloudAuthStrategy.ts +140 -46
- package/src/AssistantCloudRuns.ts +3 -14
- package/src/CloudMessagePersistence.ts +23 -19
- package/src/generateThreadTitle.test.ts +71 -0
- package/src/generateThreadTitle.ts +38 -0
- package/src/index.ts +10 -0
- package/src/runTelemetry.test.ts +171 -0
- package/src/runTelemetry.ts +144 -0
- package/src/tests/AssistantCloud.test.ts +39 -0
- package/src/tests/AssistantCloudAPI.test.ts +25 -0
- package/src/tests/AssistantCloudAuthStrategy.test.ts +284 -10
- package/src/tests/CloudMessagePersistence.test.ts +93 -0
|
@@ -0,0 +1,82 @@
|
|
|
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 any of the names the AI
|
|
62
|
+
* SDK has used: the current top-level ones, the legacy prompt/completion pair,
|
|
63
|
+
* and the v7 token detail objects. Returns undefined when no count is present,
|
|
64
|
+
* so callers can tell an empty usage object from a zeroed one.
|
|
65
|
+
*/
|
|
66
|
+
function normalizeRunTelemetryUsage(usage) {
|
|
67
|
+
const inputTokens = usage.inputTokens ?? usage.promptTokens;
|
|
68
|
+
const outputTokens = usage.outputTokens ?? usage.completionTokens;
|
|
69
|
+
const reasoningTokens = usage.reasoningTokens ?? usage.outputTokenDetails?.reasoningTokens;
|
|
70
|
+
const cachedInputTokens = usage.cachedInputTokens ?? usage.inputTokenDetails?.cacheReadTokens;
|
|
71
|
+
if (inputTokens == null && outputTokens == null && reasoningTokens == null && cachedInputTokens == null) return;
|
|
72
|
+
return {
|
|
73
|
+
...inputTokens != null ? { inputTokens } : void 0,
|
|
74
|
+
...outputTokens != null ? { outputTokens } : void 0,
|
|
75
|
+
...reasoningTokens != null ? { reasoningTokens } : void 0,
|
|
76
|
+
...cachedInputTokens != null ? { cachedInputTokens } : void 0
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
export { createRunTelemetryToolCall, normalizeRunTelemetryUsage, truncateRunTelemetryText };
|
|
81
|
+
|
|
82
|
+
//# 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 inputTokenDetails?: { cacheReadTokens?: number };\n outputTokenDetails?: { reasoningTokens?: number };\n};\n\n/**\n * Resolves the token counts a provider reports under any of the names the AI\n * SDK has used: the current top-level ones, the legacy prompt/completion pair,\n * and the v7 token detail objects. 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 // AI SDK v7 moved these under token detail objects; v6 kept them top-level.\n const reasoningTokens =\n usage.reasoningTokens ?? usage.outputTokenDetails?.reasoningTokens;\n const cachedInputTokens =\n usage.cachedInputTokens ?? usage.inputTokenDetails?.cacheReadTokens;\n\n if (\n inputTokens == null &&\n outputTokens == null &&\n reasoningTokens == null &&\n cachedInputTokens == null\n ) {\n return undefined;\n }\n\n return {\n ...(inputTokens != null ? { inputTokens } : undefined),\n ...(outputTokens != null ? { outputTokens } : undefined),\n ...(reasoningTokens != null ? { reasoningTokens } : undefined),\n ...(cachedInputTokens != null ? { cachedInputTokens } : 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;;;;;;;AAsBA,SAAgB,2BACd,OAC+B;CAC/B,MAAM,cAAc,MAAM,eAAe,MAAM;CAC/C,MAAM,eAAe,MAAM,gBAAgB,MAAM;CAEjD,MAAM,kBACJ,MAAM,mBAAmB,MAAM,oBAAoB;CACrD,MAAM,oBACJ,MAAM,qBAAqB,MAAM,mBAAmB;CAEtD,IACE,eAAe,QACf,gBAAgB,QAChB,mBAAmB,QACnB,qBAAqB,MAErB;CAGF,OAAO;EACL,GAAI,eAAe,OAAO,EAAE,YAAY,IAAI,KAAA;EAC5C,GAAI,gBAAgB,OAAO,EAAE,aAAa,IAAI,KAAA;EAC9C,GAAI,mBAAmB,OAAO,EAAE,gBAAgB,IAAI,KAAA;EACpD,GAAI,qBAAqB,OAAO,EAAE,kBAAkB,IAAI,KAAA;CAC1D;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "assistant-cloud",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.43",
|
|
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.
|
|
30
|
+
"assistant-stream": "^0.3.41"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@
|
|
34
|
-
"
|
|
35
|
-
"
|
|
33
|
+
"@assistant-ui/x-buildutils": "0.0.25",
|
|
34
|
+
"@types/node": "^26.4.0",
|
|
35
|
+
"vitest": "^4.1.11"
|
|
36
36
|
},
|
|
37
37
|
"publishConfig": {
|
|
38
38
|
"access": "public",
|
package/src/AssistantCloud.ts
CHANGED
package/src/AssistantCloudAPI.ts
CHANGED
|
@@ -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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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;
|
|
@@ -4,6 +4,34 @@ import {
|
|
|
4
4
|
readCloudString,
|
|
5
5
|
} from "./cloudResponse";
|
|
6
6
|
|
|
7
|
+
const AUTH_TOKEN_REQUEST_TIMEOUT_MS = 30_000;
|
|
8
|
+
|
|
9
|
+
const withAuthTokenDeadline = async <T>(
|
|
10
|
+
operation: string,
|
|
11
|
+
run: (signal: AbortSignal) => Promise<T>,
|
|
12
|
+
): Promise<T> => {
|
|
13
|
+
const controller = new AbortController();
|
|
14
|
+
let timedOut = false;
|
|
15
|
+
const timeout = setTimeout(() => {
|
|
16
|
+
timedOut = true;
|
|
17
|
+
controller.abort();
|
|
18
|
+
}, AUTH_TOKEN_REQUEST_TIMEOUT_MS);
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
return await run(controller.signal);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (timedOut) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Assistant Cloud ${operation} timed out after ${AUTH_TOKEN_REQUEST_TIMEOUT_MS}ms`,
|
|
26
|
+
{ cause: error },
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
throw error;
|
|
30
|
+
} finally {
|
|
31
|
+
clearTimeout(timeout);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
7
35
|
export type AssistantCloudAuthStrategy = {
|
|
8
36
|
readonly strategy: "anon" | "jwt" | "api-key";
|
|
9
37
|
getAuthHeaders(): Promise<Record<string, string> | false>;
|
|
@@ -247,6 +275,52 @@ const removeRefreshToken = (baseUrl: string): void => {
|
|
|
247
275
|
} catch {}
|
|
248
276
|
};
|
|
249
277
|
|
|
278
|
+
// In-flight sharing follows refresh-token storage scope to isolate server requests.
|
|
279
|
+
const anonymousAuthTokenRequests = new WeakMap<
|
|
280
|
+
Storage,
|
|
281
|
+
Map<string, Promise<string | null>>
|
|
282
|
+
>();
|
|
283
|
+
|
|
284
|
+
const getWebLockManager = (): LockManager | null => {
|
|
285
|
+
if (!("navigator" in globalThis)) return null;
|
|
286
|
+
return (
|
|
287
|
+
(globalThis as { navigator?: { locks?: LockManager } }).navigator?.locks ??
|
|
288
|
+
null
|
|
289
|
+
);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const getAnonymousAuthLockName = (baseUrl: string): string =>
|
|
293
|
+
`assistant-cloud:anonymous-auth:${baseUrl}`;
|
|
294
|
+
|
|
295
|
+
const getSharedAnonymousAuthToken = (
|
|
296
|
+
baseUrl: string,
|
|
297
|
+
requestToken: () => Promise<string | null>,
|
|
298
|
+
): Promise<string | null> => {
|
|
299
|
+
const storage = getLocalStorage();
|
|
300
|
+
if (!storage) return requestToken();
|
|
301
|
+
|
|
302
|
+
let storageRequests = anonymousAuthTokenRequests.get(storage);
|
|
303
|
+
if (!storageRequests) {
|
|
304
|
+
storageRequests = new Map();
|
|
305
|
+
anonymousAuthTokenRequests.set(storage, storageRequests);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const activeRequest = storageRequests.get(baseUrl);
|
|
309
|
+
if (activeRequest) return activeRequest;
|
|
310
|
+
|
|
311
|
+
const locks = getWebLockManager();
|
|
312
|
+
const request = locks
|
|
313
|
+
? locks.request(getAnonymousAuthLockName(baseUrl), requestToken)
|
|
314
|
+
: requestToken();
|
|
315
|
+
const sharedRequest = request.finally(() => {
|
|
316
|
+
if (storageRequests.get(baseUrl) === sharedRequest) {
|
|
317
|
+
storageRequests.delete(baseUrl);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
storageRequests.set(baseUrl, sharedRequest);
|
|
321
|
+
return sharedRequest;
|
|
322
|
+
};
|
|
323
|
+
|
|
250
324
|
export class AssistantCloudAnonymousAuthStrategy implements AssistantCloudAuthStrategy {
|
|
251
325
|
public readonly strategy = "anon";
|
|
252
326
|
|
|
@@ -255,70 +329,90 @@ export class AssistantCloudAnonymousAuthStrategy implements AssistantCloudAuthSt
|
|
|
255
329
|
|
|
256
330
|
constructor(baseUrl: string) {
|
|
257
331
|
this.baseUrl = baseUrl;
|
|
258
|
-
|
|
332
|
+
const requestAuthToken = async (): Promise<string | null> => {
|
|
259
333
|
const currentTime = Date.now();
|
|
260
334
|
const storedRefreshToken = readRefreshToken(this.baseUrl);
|
|
261
335
|
|
|
262
336
|
if (storedRefreshToken) {
|
|
263
337
|
const refreshExpiry = new Date(storedRefreshToken.expires_at).getTime();
|
|
264
338
|
if (refreshExpiry - currentTime > 30 * 1000) {
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
{
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
339
|
+
const refreshedAccessToken = await withAuthTokenDeadline(
|
|
340
|
+
"refresh token request",
|
|
341
|
+
async (signal) => {
|
|
342
|
+
const response = await fetch(
|
|
343
|
+
`${this.baseUrl}/v1/auth/tokens/refresh`,
|
|
344
|
+
{
|
|
345
|
+
method: "POST",
|
|
346
|
+
headers: { "Content-Type": "application/json" },
|
|
347
|
+
body: JSON.stringify({
|
|
348
|
+
refresh_token: storedRefreshToken.token,
|
|
349
|
+
}),
|
|
350
|
+
signal,
|
|
351
|
+
},
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
if (response.ok) {
|
|
355
|
+
const { data, accessToken } = await readAuthTokenResponse(
|
|
356
|
+
response,
|
|
357
|
+
"refresh auth token response",
|
|
358
|
+
);
|
|
359
|
+
if (data.refresh_token != null) {
|
|
360
|
+
writeRefreshToken(
|
|
361
|
+
this.baseUrl,
|
|
362
|
+
readRefreshTokenResponse(
|
|
363
|
+
data.refresh_token,
|
|
364
|
+
"refresh auth token response.refresh_token",
|
|
365
|
+
),
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
return accessToken;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (response.status === 429 || response.status >= 500) {
|
|
372
|
+
throw new Error(
|
|
373
|
+
`Assistant Cloud token refresh failed with status ${response.status}`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return null;
|
|
271
378
|
},
|
|
272
379
|
);
|
|
273
|
-
|
|
274
|
-
if (response.ok) {
|
|
275
|
-
const { data, accessToken } = await readAuthTokenResponse(
|
|
276
|
-
response,
|
|
277
|
-
"refresh auth token response",
|
|
278
|
-
);
|
|
279
|
-
if (data.refresh_token != null) {
|
|
280
|
-
writeRefreshToken(
|
|
281
|
-
this.baseUrl,
|
|
282
|
-
readRefreshTokenResponse(
|
|
283
|
-
data.refresh_token,
|
|
284
|
-
"refresh auth token response.refresh_token",
|
|
285
|
-
),
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
return accessToken;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
if (response.status === 429 || response.status >= 500) {
|
|
292
|
-
throw new Error(
|
|
293
|
-
`Assistant Cloud token refresh failed with status ${response.status}`,
|
|
294
|
-
);
|
|
295
|
-
}
|
|
380
|
+
if (refreshedAccessToken !== null) return refreshedAccessToken;
|
|
296
381
|
} else {
|
|
297
382
|
removeRefreshToken(this.baseUrl);
|
|
298
383
|
}
|
|
299
384
|
}
|
|
300
385
|
|
|
301
386
|
// No valid refresh token; request a new anonymous token
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
387
|
+
return withAuthTokenDeadline(
|
|
388
|
+
"anonymous token request",
|
|
389
|
+
async (signal) => {
|
|
390
|
+
const response = await fetch(
|
|
391
|
+
`${this.baseUrl}/v1/auth/tokens/anonymous`,
|
|
392
|
+
{ method: "POST", signal },
|
|
393
|
+
);
|
|
305
394
|
|
|
306
|
-
|
|
395
|
+
if (!response.ok) return null;
|
|
307
396
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
397
|
+
const { data, accessToken } = await readAuthTokenResponse(
|
|
398
|
+
response,
|
|
399
|
+
"anonymous auth token response",
|
|
400
|
+
);
|
|
312
401
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
402
|
+
writeRefreshToken(
|
|
403
|
+
this.baseUrl,
|
|
404
|
+
readRefreshTokenResponse(
|
|
405
|
+
data.refresh_token,
|
|
406
|
+
"anonymous auth token response.refresh_token",
|
|
407
|
+
),
|
|
408
|
+
);
|
|
409
|
+
return accessToken;
|
|
410
|
+
},
|
|
319
411
|
);
|
|
320
|
-
|
|
321
|
-
|
|
412
|
+
};
|
|
413
|
+
this.jwtStrategy = new AssistantCloudJWTAuthStrategy(() =>
|
|
414
|
+
getSharedAnonymousAuthToken(this.baseUrl, requestAuthToken),
|
|
415
|
+
);
|
|
322
416
|
}
|
|
323
417
|
|
|
324
418
|
public async getAuthHeaders(): Promise<Record<string, string> | false> {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AssistantCloudAPI } from "./AssistantCloudAPI";
|
|
2
|
-
import type {
|
|
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?:
|
|
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?:
|
|
29
|
+
tool_calls?: AssistantCloudRunReportToolCall[];
|
|
41
30
|
start_ms?: number;
|
|
42
31
|
end_ms?: number;
|
|
43
32
|
}[];
|
|
@@ -39,32 +39,36 @@ export class CloudMessagePersistence {
|
|
|
39
39
|
content: ReadonlyJSONObject,
|
|
40
40
|
): Promise<void> {
|
|
41
41
|
const cloud = this.getCloud();
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
const existing = this.idMapping[messageId];
|
|
43
|
+
if (existing instanceof Promise) {
|
|
44
|
+
await existing;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
46
47
|
|
|
47
|
-
const task =
|
|
48
|
-
|
|
48
|
+
const task = (async () => {
|
|
49
|
+
const resolvedParentId = parentId
|
|
50
|
+
? ((await this.idMapping[parentId]) ?? parentId)
|
|
51
|
+
: null;
|
|
52
|
+
const { message_id } = await cloud.threads.messages.create(threadId, {
|
|
49
53
|
parent_id: resolvedParentId,
|
|
50
54
|
format,
|
|
51
55
|
content,
|
|
52
|
-
})
|
|
53
|
-
.then(({ message_id }) => {
|
|
54
|
-
this.idMapping[messageId] = message_id;
|
|
55
|
-
return message_id;
|
|
56
|
-
})
|
|
57
|
-
.catch((err) => {
|
|
58
|
-
// Only delete if we're still the active task (avoids clobbering a retry)
|
|
59
|
-
if (this.idMapping[messageId] === task) {
|
|
60
|
-
delete this.idMapping[messageId];
|
|
61
|
-
}
|
|
62
|
-
throw err;
|
|
63
56
|
});
|
|
57
|
+
return message_id;
|
|
58
|
+
})();
|
|
64
59
|
|
|
65
|
-
// Store the promise immediately so concurrent appends can await it
|
|
66
60
|
this.idMapping[messageId] = task;
|
|
67
|
-
|
|
61
|
+
try {
|
|
62
|
+
const remoteId = await task;
|
|
63
|
+
if (this.idMapping[messageId] === task) {
|
|
64
|
+
this.idMapping[messageId] = remoteId;
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (this.idMapping[messageId] === task) {
|
|
68
|
+
delete this.idMapping[messageId];
|
|
69
|
+
}
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
/**
|
|
@@ -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 {
|