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.
- package/README.md +66 -0
- package/dist/AssistantCloud.d.ts +6 -1
- package/dist/AssistantCloud.d.ts.map +1 -1
- package/dist/AssistantCloud.js +13 -5
- package/dist/AssistantCloud.js.map +1 -1
- package/dist/AssistantCloudAPI.d.ts +28 -3
- package/dist/AssistantCloudAPI.d.ts.map +1 -1
- package/dist/AssistantCloudAPI.js +37 -10
- package/dist/AssistantCloudAPI.js.map +1 -1
- package/dist/AssistantCloudAuthStrategy.d.ts +4 -1
- package/dist/AssistantCloudAuthStrategy.d.ts.map +1 -1
- package/dist/AssistantCloudAuthStrategy.js +15 -3
- package/dist/AssistantCloudAuthStrategy.js.map +1 -1
- package/dist/AssistantCloudEvents.d.ts +32 -0
- package/dist/AssistantCloudEvents.d.ts.map +1 -0
- package/dist/AssistantCloudEvents.js +119 -0
- package/dist/AssistantCloudEvents.js.map +1 -0
- package/dist/AssistantCloudFiles.d.ts +12 -1
- package/dist/AssistantCloudFiles.d.ts.map +1 -1
- package/dist/AssistantCloudFiles.js +13 -1
- package/dist/AssistantCloudFiles.js.map +1 -1
- package/dist/AssistantCloudRuns.d.ts +12 -0
- package/dist/AssistantCloudRuns.d.ts.map +1 -1
- package/dist/AssistantCloudRuns.js +2 -1
- package/dist/AssistantCloudRuns.js.map +1 -1
- package/dist/AssistantCloudScores.d.ts +27 -0
- package/dist/AssistantCloudScores.d.ts.map +1 -0
- package/dist/AssistantCloudScores.js +30 -0
- package/dist/AssistantCloudScores.js.map +1 -0
- package/dist/AssistantCloudThreadMessages.d.ts +11 -1
- package/dist/AssistantCloudThreadMessages.d.ts.map +1 -1
- package/dist/AssistantCloudThreadMessages.js +12 -1
- package/dist/AssistantCloudThreadMessages.js.map +1 -1
- package/dist/AssistantCloudThreads.d.ts +8 -0
- package/dist/AssistantCloudThreads.d.ts.map +1 -1
- package/dist/AssistantCloudThreads.js +9 -1
- package/dist/AssistantCloudThreads.js.map +1 -1
- package/dist/CloudEngagementReporter.d.ts +74 -0
- package/dist/CloudEngagementReporter.d.ts.map +1 -0
- package/dist/CloudEngagementReporter.js +132 -0
- package/dist/CloudEngagementReporter.js.map +1 -0
- package/dist/CloudMessagePersistence.d.ts +12 -9
- package/dist/CloudMessagePersistence.d.ts.map +1 -1
- package/dist/CloudMessagePersistence.js +54 -20
- package/dist/CloudMessagePersistence.js.map +1 -1
- package/dist/CloudRunReporter.d.ts +20 -0
- package/dist/CloudRunReporter.d.ts.map +1 -0
- package/dist/CloudRunReporter.js +38 -0
- package/dist/CloudRunReporter.js.map +1 -0
- package/dist/FormattedCloudPersistence.d.ts +3 -8
- package/dist/FormattedCloudPersistence.d.ts.map +1 -1
- package/dist/FormattedCloudPersistence.js +3 -8
- package/dist/FormattedCloudPersistence.js.map +1 -1
- package/dist/ai-sdk/index.d.ts +33 -0
- package/dist/ai-sdk/index.d.ts.map +1 -0
- package/dist/ai-sdk/index.js +147 -0
- package/dist/ai-sdk/index.js.map +1 -0
- package/dist/cloudResponse.d.ts +4 -1
- package/dist/cloudResponse.d.ts.map +1 -1
- package/dist/cloudResponse.js +14 -1
- package/dist/cloudResponse.js.map +1 -1
- package/dist/generateThreadTitle.js +1 -0
- package/dist/generateThreadTitle.js.map +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.js +7 -2
- package/dist/instrumentMcpSampling.d.ts.map +1 -1
- package/dist/instrumentMcpSampling.js +12 -2
- package/dist/instrumentMcpSampling.js.map +1 -1
- package/dist/runTelemetry.d.ts +86 -7
- package/dist/runTelemetry.d.ts.map +1 -1
- package/dist/runTelemetry.js +144 -1
- package/dist/runTelemetry.js.map +1 -1
- package/dist/telemetry/index.d.ts +32 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +62 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +6 -0
- package/dist/version.js.map +1 -0
- package/package.json +43 -6
- package/src/AssistantCloud.ts +23 -8
- package/src/AssistantCloudAPI.ts +72 -10
- package/src/AssistantCloudAuthStrategy.ts +17 -2
- package/src/AssistantCloudEvents.test.ts +149 -0
- package/src/AssistantCloudEvents.ts +190 -0
- package/src/AssistantCloudFiles.test.ts +59 -0
- package/src/AssistantCloudFiles.ts +39 -0
- package/src/AssistantCloudRuns.ts +13 -1
- package/src/AssistantCloudScores.test.ts +113 -0
- package/src/AssistantCloudScores.ts +61 -0
- package/src/AssistantCloudThreadMessages.test.ts +51 -0
- package/src/AssistantCloudThreadMessages.ts +33 -0
- package/src/AssistantCloudThreads.test.ts +21 -0
- package/src/AssistantCloudThreads.ts +21 -0
- package/src/CloudEngagementReporter.ts +231 -0
- package/src/CloudMessagePersistence.ts +70 -27
- package/src/CloudRunReporter.ts +38 -0
- package/src/FormattedCloudPersistence.ts +3 -8
- package/src/ai-sdk/index.test.ts +258 -0
- package/src/ai-sdk/index.ts +222 -0
- package/src/cloudResponse.test.ts +34 -1
- package/src/cloudResponse.ts +30 -0
- package/src/generateThreadTitle.test.ts +32 -0
- package/src/generateThreadTitle.ts +1 -0
- package/src/index.ts +35 -2
- package/src/instrumentMcpSampling.test.ts +102 -0
- package/src/instrumentMcpSampling.ts +16 -2
- package/src/runTelemetry.test.ts +325 -0
- package/src/runTelemetry.ts +274 -6
- package/src/telemetry/index.test.ts +252 -0
- package/src/telemetry/index.ts +98 -0
- package/src/tests/AssistantCloud.test.ts +55 -1
- package/src/tests/AssistantCloudAPI.test.ts +95 -4
- package/src/tests/AssistantCloudAuthStrategy.test.ts +93 -2
- package/src/tests/AssistantCloudProjects.test.ts +1 -5
- package/src/tests/CloudEngagementReporter.test.ts +184 -0
- package/src/tests/CloudMessagePersistence.test.ts +350 -0
- package/src/tests/CloudRunReporter.test.ts +100 -0
- package/src/version.ts +4 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { AssistantCloudAPI } from "./AssistantCloudAPI";
|
|
2
|
+
import {
|
|
3
|
+
readCloudEnum,
|
|
4
|
+
readCloudNullableNumber,
|
|
5
|
+
readCloudNullableString,
|
|
6
|
+
readCloudRecord,
|
|
7
|
+
readCloudString,
|
|
8
|
+
} from "./cloudResponse";
|
|
9
|
+
|
|
10
|
+
const SCORE_DATA_TYPES = ["numeric", "categorical", "boolean"] as const;
|
|
11
|
+
|
|
12
|
+
export type AssistantCloudScoreBody = {
|
|
13
|
+
name: string;
|
|
14
|
+
data_type: "numeric" | "categorical" | "boolean";
|
|
15
|
+
value?: number | boolean;
|
|
16
|
+
string_value?: string;
|
|
17
|
+
comment?: string;
|
|
18
|
+
thread_id?: string;
|
|
19
|
+
message_id?: string;
|
|
20
|
+
run_id?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type AssistantCloudScoreResponse = {
|
|
24
|
+
score_id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
data_type: "numeric" | "categorical" | "boolean";
|
|
27
|
+
value: number | null;
|
|
28
|
+
string_value: string | null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export class AssistantCloudScores {
|
|
32
|
+
private cloud: AssistantCloudAPI;
|
|
33
|
+
|
|
34
|
+
constructor(cloud: AssistantCloudAPI) {
|
|
35
|
+
this.cloud = cloud;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public async create(
|
|
39
|
+
body: AssistantCloudScoreBody,
|
|
40
|
+
): Promise<AssistantCloudScoreResponse> {
|
|
41
|
+
const response = readCloudRecord(
|
|
42
|
+
await this.cloud.makeRequest("/scores", { method: "POST", body }),
|
|
43
|
+
"score response",
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
score_id: readCloudString(response.score_id, "score response.score_id"),
|
|
48
|
+
name: readCloudString(response.name, "score response.name"),
|
|
49
|
+
data_type: readCloudEnum(
|
|
50
|
+
response.data_type,
|
|
51
|
+
"score response.data_type",
|
|
52
|
+
SCORE_DATA_TYPES,
|
|
53
|
+
),
|
|
54
|
+
value: readCloudNullableNumber(response.value, "score response.value"),
|
|
55
|
+
string_value: readCloudNullableString(
|
|
56
|
+
response.string_value,
|
|
57
|
+
"score response.string_value",
|
|
58
|
+
),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
import type { AssistantCloudAPI } from "./AssistantCloudAPI";
|
|
3
3
|
import { AssistantCloudThreadMessages } from "./AssistantCloudThreadMessages";
|
|
4
|
+
import { CloudResponseError } from "./cloudResponse";
|
|
4
5
|
|
|
5
6
|
const createCloudThreadMessages = () => {
|
|
6
7
|
const makeRequest = vi.fn();
|
|
@@ -32,6 +33,41 @@ describe("AssistantCloudThreadMessages responses", () => {
|
|
|
32
33
|
);
|
|
33
34
|
});
|
|
34
35
|
|
|
36
|
+
it("submits and validates message feedback", async () => {
|
|
37
|
+
const { messages, makeRequest } = createCloudThreadMessages();
|
|
38
|
+
const body = { type: "positive" as const };
|
|
39
|
+
makeRequest.mockResolvedValueOnce({
|
|
40
|
+
feedback_id: "feedback-1",
|
|
41
|
+
type: "positive",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await expect(
|
|
45
|
+
messages.feedback("thread/1", "message/1", body),
|
|
46
|
+
).resolves.toEqual({
|
|
47
|
+
feedback_id: "feedback-1",
|
|
48
|
+
type: "positive",
|
|
49
|
+
});
|
|
50
|
+
expect(makeRequest).toHaveBeenCalledWith(
|
|
51
|
+
"/threads/thread%2F1/messages/message%2F1/feedback",
|
|
52
|
+
{ method: "POST", body },
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
makeRequest.mockResolvedValueOnce({ type: "positive" });
|
|
56
|
+
|
|
57
|
+
await expect(
|
|
58
|
+
messages.feedback("thread-1", "message-1", body),
|
|
59
|
+
).rejects.toBeInstanceOf(CloudResponseError);
|
|
60
|
+
|
|
61
|
+
makeRequest.mockResolvedValueOnce({
|
|
62
|
+
feedback_id: "feedback-1",
|
|
63
|
+
type: "neutral",
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await expect(
|
|
67
|
+
messages.feedback("thread-1", "message-1", body),
|
|
68
|
+
).rejects.toThrow('expected one of "positive", "negative"');
|
|
69
|
+
});
|
|
70
|
+
|
|
35
71
|
it("decodes canonical message responses without changing content", async () => {
|
|
36
72
|
const { messages, makeRequest } = createCloudThreadMessages();
|
|
37
73
|
makeRequest.mockResolvedValue({
|
|
@@ -58,4 +94,19 @@ describe("AssistantCloudThreadMessages responses", () => {
|
|
|
58
94
|
created_at: "leave-this-string-untouched",
|
|
59
95
|
});
|
|
60
96
|
});
|
|
97
|
+
|
|
98
|
+
it("forwards the pagination query to the list endpoint", async () => {
|
|
99
|
+
const { messages, makeRequest } = createCloudThreadMessages();
|
|
100
|
+
makeRequest.mockResolvedValueOnce({ messages: [] });
|
|
101
|
+
|
|
102
|
+
await messages.list("thread/1", {
|
|
103
|
+
format: "aui/v0",
|
|
104
|
+
limit: 200,
|
|
105
|
+
after: "message-200",
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(makeRequest).toHaveBeenCalledWith("/threads/thread%2F1/messages", {
|
|
109
|
+
query: { format: "aui/v0", limit: 200, after: "message-200" },
|
|
110
|
+
});
|
|
111
|
+
});
|
|
61
112
|
});
|
|
@@ -2,6 +2,7 @@ import type { ReadonlyJSONObject } from "assistant-stream/utils";
|
|
|
2
2
|
import type { AssistantCloudAPI } from "./AssistantCloudAPI";
|
|
3
3
|
import {
|
|
4
4
|
readCloudArray,
|
|
5
|
+
readCloudEnum,
|
|
5
6
|
readCloudInteger,
|
|
6
7
|
readCloudJSONObject,
|
|
7
8
|
readCloudNullableString,
|
|
@@ -22,6 +23,8 @@ export type CloudMessage = {
|
|
|
22
23
|
|
|
23
24
|
type AssistantCloudThreadMessageListQuery = {
|
|
24
25
|
format?: string;
|
|
26
|
+
limit?: number;
|
|
27
|
+
after?: string;
|
|
25
28
|
};
|
|
26
29
|
|
|
27
30
|
type AssistantCloudThreadMessageListResponse = {
|
|
@@ -42,6 +45,17 @@ type AssistantCloudThreadMessageUpdateBody = {
|
|
|
42
45
|
content: ReadonlyJSONObject;
|
|
43
46
|
};
|
|
44
47
|
|
|
48
|
+
const MESSAGE_FEEDBACK_TYPES = ["positive", "negative"] as const;
|
|
49
|
+
|
|
50
|
+
export type AssistantCloudThreadMessageFeedbackBody = {
|
|
51
|
+
type: "positive" | "negative";
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type AssistantCloudThreadMessageFeedbackResponse = {
|
|
55
|
+
feedback_id: string;
|
|
56
|
+
type: "positive" | "negative";
|
|
57
|
+
};
|
|
58
|
+
|
|
45
59
|
export const decodeCloudMessage = (
|
|
46
60
|
value: unknown,
|
|
47
61
|
field: string,
|
|
@@ -112,4 +126,23 @@ export class AssistantCloudThreadMessages {
|
|
|
112
126
|
{ method: "PUT", body },
|
|
113
127
|
);
|
|
114
128
|
}
|
|
129
|
+
|
|
130
|
+
public async feedback(
|
|
131
|
+
threadId: string,
|
|
132
|
+
messageId: string,
|
|
133
|
+
body: AssistantCloudThreadMessageFeedbackBody,
|
|
134
|
+
): Promise<AssistantCloudThreadMessageFeedbackResponse> {
|
|
135
|
+
const response = readCloudRecord(
|
|
136
|
+
await this.cloud.makeRequest(
|
|
137
|
+
`/threads/${encodeURIComponent(threadId)}/messages/${encodeURIComponent(messageId)}/feedback`,
|
|
138
|
+
{ method: "POST", body },
|
|
139
|
+
),
|
|
140
|
+
"thread message feedback response",
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
feedback_id: readCloudString(response.feedback_id, "feedback_id"),
|
|
145
|
+
type: readCloudEnum(response.type, "type", MESSAGE_FEEDBACK_TYPES),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
115
148
|
}
|
|
@@ -39,6 +39,27 @@ describe("AssistantCloudThreads responses", () => {
|
|
|
39
39
|
);
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
+
it("claims anonymous threads and validates the moved count", async () => {
|
|
43
|
+
const { threads, makeRequest } = createCloudThreads();
|
|
44
|
+
makeRequest.mockResolvedValueOnce({ moved: 2 });
|
|
45
|
+
|
|
46
|
+
await expect(
|
|
47
|
+
threads.claim({ refresh_token: "anonymous-refresh" }),
|
|
48
|
+
).resolves.toEqual({ moved: 2 });
|
|
49
|
+
expect(makeRequest).toHaveBeenLastCalledWith("/threads/claim", {
|
|
50
|
+
method: "POST",
|
|
51
|
+
body: { refresh_token: "anonymous-refresh" },
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
makeRequest.mockResolvedValueOnce({ moved: 1.5 });
|
|
55
|
+
|
|
56
|
+
await expect(
|
|
57
|
+
threads.claim({ refresh_token: "anonymous-refresh" }),
|
|
58
|
+
).rejects.toThrow(
|
|
59
|
+
'Invalid Assistant Cloud response for "moved": expected an integer',
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
42
63
|
it("forwards both archive filter values", async () => {
|
|
43
64
|
const { threads, makeRequest } = createCloudThreads();
|
|
44
65
|
makeRequest.mockResolvedValue({ threads: [] });
|
|
@@ -3,6 +3,7 @@ import { AssistantCloudThreadMessages } from "./AssistantCloudThreadMessages";
|
|
|
3
3
|
import {
|
|
4
4
|
readCloudArray,
|
|
5
5
|
readCloudBoolean,
|
|
6
|
+
readCloudInteger,
|
|
6
7
|
readCloudNullableString,
|
|
7
8
|
readCloudRecord,
|
|
8
9
|
readCloudString,
|
|
@@ -43,6 +44,14 @@ type AssistantCloudThreadsCreateResponse = {
|
|
|
43
44
|
thread_id: string;
|
|
44
45
|
};
|
|
45
46
|
|
|
47
|
+
type AssistantCloudThreadsClaimBody = {
|
|
48
|
+
refresh_token: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type AssistantCloudThreadsClaimResponse = {
|
|
52
|
+
moved: number;
|
|
53
|
+
};
|
|
54
|
+
|
|
46
55
|
type AssistantCloudThreadsUpdateBody = {
|
|
47
56
|
title?: string | undefined;
|
|
48
57
|
last_message_at?: Date | undefined;
|
|
@@ -135,6 +144,18 @@ export class AssistantCloudThreads {
|
|
|
135
144
|
});
|
|
136
145
|
}
|
|
137
146
|
|
|
147
|
+
/** Moves every thread of the anonymous identity behind `refresh_token` into the caller's workspace. */
|
|
148
|
+
public async claim(
|
|
149
|
+
body: AssistantCloudThreadsClaimBody,
|
|
150
|
+
): Promise<AssistantCloudThreadsClaimResponse> {
|
|
151
|
+
const response = readCloudRecord(
|
|
152
|
+
await this.cloud.makeRequest("/threads/claim", { method: "POST", body }),
|
|
153
|
+
"thread claim response",
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
return { moved: readCloudInteger(response.moved, "moved") };
|
|
157
|
+
}
|
|
158
|
+
|
|
138
159
|
public async delete(threadId: string): Promise<void> {
|
|
139
160
|
return this.cloud.makeRequest(`/threads/${encodeURIComponent(threadId)}`, {
|
|
140
161
|
method: "DELETE",
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import type { AssistantCloud } from "./AssistantCloud";
|
|
2
|
+
import type {
|
|
3
|
+
AssistantCloudEvent,
|
|
4
|
+
AssistantCloudEventKind,
|
|
5
|
+
} from "./AssistantCloudEvents";
|
|
6
|
+
|
|
7
|
+
export type EngagementEventIds = Pick<
|
|
8
|
+
AssistantCloudEvent,
|
|
9
|
+
"thread_id" | "message_id" | "run_id"
|
|
10
|
+
>;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Turns the ids an integration knows (its own thread and message ids) into the
|
|
14
|
+
* ids the cloud stores. A send is the one event that may create the remote
|
|
15
|
+
* thread, so it asks for `awaitThread`; every other event reads the ids that
|
|
16
|
+
* already exist.
|
|
17
|
+
*/
|
|
18
|
+
export type EngagementIdResolver = (
|
|
19
|
+
threadId: string,
|
|
20
|
+
messageId: string | undefined,
|
|
21
|
+
options: { awaitThread: boolean },
|
|
22
|
+
) => EngagementEventIds | Promise<EngagementEventIds>;
|
|
23
|
+
|
|
24
|
+
type EngagementEventInit = Pick<AssistantCloudEvent, "value" | "props">;
|
|
25
|
+
|
|
26
|
+
const MAX_REMEMBERED_THREADS = 256;
|
|
27
|
+
|
|
28
|
+
function remember<T>(map: Map<string, T>, threadId: string, value: T): void {
|
|
29
|
+
map.delete(threadId);
|
|
30
|
+
map.set(threadId, value);
|
|
31
|
+
if (map.size > MAX_REMEMBERED_THREADS) {
|
|
32
|
+
map.delete(map.keys().next().value!);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function mark(set: Set<string>, threadId: string): void {
|
|
37
|
+
set.delete(threadId);
|
|
38
|
+
set.add(threadId);
|
|
39
|
+
if (set.size > MAX_REMEMBERED_THREADS) {
|
|
40
|
+
set.delete(set.values().next().value!);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const passThroughIds: EngagementIdResolver = (threadId, messageId) => ({
|
|
45
|
+
thread_id: threadId,
|
|
46
|
+
...(messageId !== undefined ? { message_id: messageId } : undefined),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Derives engagement events from what a chat integration observes and keeps
|
|
51
|
+
* the per thread state the events need: a run's start for the stop duration,
|
|
52
|
+
* a run's end for the time to the next message, one error and one suggestion
|
|
53
|
+
* list per run or thread. A started run is kept until it ends or stops; the
|
|
54
|
+
* rest is kept for the 256 most recently touched threads, so a long session
|
|
55
|
+
* does not grow it without bound. Delivery goes through the cloud's event
|
|
56
|
+
* buffer, so a disabled telemetry setting drops everything here as well.
|
|
57
|
+
*/
|
|
58
|
+
export class CloudEngagementReporter {
|
|
59
|
+
private readonly runStartedAt = new Map<string, number>();
|
|
60
|
+
private readonly runEndedAt = new Map<string, number>();
|
|
61
|
+
private readonly shownErrors = new Set<string>();
|
|
62
|
+
private readonly shownSuggestions = new Set<string>();
|
|
63
|
+
|
|
64
|
+
private readonly getCloud: () => AssistantCloud;
|
|
65
|
+
private readonly resolveIds: EngagementIdResolver;
|
|
66
|
+
|
|
67
|
+
constructor(
|
|
68
|
+
cloud: AssistantCloud | (() => AssistantCloud),
|
|
69
|
+
resolveIds: EngagementIdResolver = passThroughIds,
|
|
70
|
+
) {
|
|
71
|
+
this.getCloud = typeof cloud === "function" ? cloud : () => cloud;
|
|
72
|
+
this.resolveIds = resolveIds;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public runStarted(threadId: string): void {
|
|
76
|
+
this.runStartedAt.set(threadId, Date.now());
|
|
77
|
+
this.shownErrors.delete(threadId);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public runEnded(threadId: string): void {
|
|
81
|
+
this.runStartedAt.delete(threadId);
|
|
82
|
+
remember(this.runEndedAt, threadId, Date.now());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Reported once per started run, with the time the run had been going. */
|
|
86
|
+
public runStopped(threadId: string): void {
|
|
87
|
+
const startedAt = this.runStartedAt.get(threadId);
|
|
88
|
+
if (startedAt === undefined) return;
|
|
89
|
+
this.runStartedAt.delete(threadId);
|
|
90
|
+
this.track("run_stopped", threadId, undefined, {
|
|
91
|
+
value: Math.max(0, Date.now() - startedAt),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Carries the time since the previous run of the thread ended, when known. */
|
|
96
|
+
public messageSent(
|
|
97
|
+
threadId: string,
|
|
98
|
+
init: {
|
|
99
|
+
messageId?: string | undefined;
|
|
100
|
+
chars: number;
|
|
101
|
+
attachments: number;
|
|
102
|
+
},
|
|
103
|
+
): void {
|
|
104
|
+
const previousRunEndedAt = this.runEndedAt.get(threadId);
|
|
105
|
+
this.track(
|
|
106
|
+
"message_sent",
|
|
107
|
+
threadId,
|
|
108
|
+
init.messageId,
|
|
109
|
+
{
|
|
110
|
+
props: { chars: init.chars, attachments: init.attachments },
|
|
111
|
+
...(previousRunEndedAt !== undefined
|
|
112
|
+
? { value: Math.max(0, Date.now() - previousRunEndedAt) }
|
|
113
|
+
: undefined),
|
|
114
|
+
},
|
|
115
|
+
{ awaitThread: true },
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public messageEdited(
|
|
120
|
+
threadId: string,
|
|
121
|
+
init: { messageId: string; chars: number },
|
|
122
|
+
): void {
|
|
123
|
+
this.track("message_edited", threadId, init.messageId, {
|
|
124
|
+
props: { chars: init.chars },
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
public messageRegenerated(threadId: string, messageId?: string): void {
|
|
129
|
+
this.track("message_regenerated", threadId, messageId);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Reported once per run, so a retried render of the same error stays one event. */
|
|
133
|
+
public errorShown(
|
|
134
|
+
threadId: string,
|
|
135
|
+
init: { messageId?: string | undefined; reason: string },
|
|
136
|
+
): void {
|
|
137
|
+
if (this.shownErrors.has(threadId)) return;
|
|
138
|
+
mark(this.shownErrors, threadId);
|
|
139
|
+
this.track("error_shown", threadId, init.messageId, {
|
|
140
|
+
props: { reason: init.reason },
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Reported once per thread, with the number of suggestions on offer. */
|
|
145
|
+
public suggestionsShown(threadId: string, count: number): void {
|
|
146
|
+
if (this.shownSuggestions.has(threadId)) return;
|
|
147
|
+
mark(this.shownSuggestions, threadId);
|
|
148
|
+
this.track("suggestions_shown", threadId, undefined, { value: count });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
public suggestionClicked(threadId: string): void {
|
|
152
|
+
this.track("suggestion_clicked", threadId);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
public attachmentAdded(
|
|
156
|
+
threadId: string,
|
|
157
|
+
init: { messageId?: string | undefined; contentType?: string | undefined },
|
|
158
|
+
): void {
|
|
159
|
+
this.track("attachment_added", threadId, init.messageId, {
|
|
160
|
+
...(init.contentType ? { props: { type: init.contentType } } : undefined),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
public attachmentFailed(
|
|
165
|
+
threadId: string,
|
|
166
|
+
init: { messageId?: string | undefined; contentType?: string | undefined },
|
|
167
|
+
): void {
|
|
168
|
+
this.track("attachment_failed", threadId, init.messageId, {
|
|
169
|
+
...(init.contentType ? { props: { type: init.contentType } } : undefined),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
public voiceStarted(threadId: string): void {
|
|
174
|
+
this.track("voice_started", threadId);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
public speechStarted(threadId: string, messageId?: string): void {
|
|
178
|
+
this.track("speech_started", threadId, messageId);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
public branchSwitched(threadId: string, messageId?: string): void {
|
|
182
|
+
this.track("branch_switched", threadId, messageId);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
public messageCopied(threadId: string, messageId?: string): void {
|
|
186
|
+
this.track("message_copied", threadId, messageId);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
public toolApproved(
|
|
190
|
+
threadId: string,
|
|
191
|
+
messageId: string,
|
|
192
|
+
toolCallId: string,
|
|
193
|
+
toolName: string,
|
|
194
|
+
): void {
|
|
195
|
+
this.track("tool_approved", threadId, messageId, {
|
|
196
|
+
props: { toolCallId, toolName },
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
public toolRejected(
|
|
201
|
+
threadId: string,
|
|
202
|
+
messageId: string,
|
|
203
|
+
toolCallId: string,
|
|
204
|
+
toolName: string,
|
|
205
|
+
): void {
|
|
206
|
+
this.track("tool_rejected", threadId, messageId, {
|
|
207
|
+
props: { toolCallId, toolName },
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Reported only for a thread the cloud already knows. */
|
|
212
|
+
public threadSwitched(threadId: string): void {
|
|
213
|
+
this.track("thread_switched", threadId);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private track(
|
|
217
|
+
kind: AssistantCloudEventKind,
|
|
218
|
+
threadId: string,
|
|
219
|
+
messageId?: string,
|
|
220
|
+
init: EngagementEventInit = {},
|
|
221
|
+
options: { awaitThread: boolean } = { awaitThread: false },
|
|
222
|
+
): void {
|
|
223
|
+
void Promise.resolve()
|
|
224
|
+
.then(() => this.resolveIds(threadId, messageId, options))
|
|
225
|
+
.then((ids) => {
|
|
226
|
+
if (kind === "thread_switched" && !ids.thread_id) return;
|
|
227
|
+
this.getCloud().events.track({ kind, ...init, ...ids });
|
|
228
|
+
})
|
|
229
|
+
.catch(() => {});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
import type { ReadonlyJSONObject } from "assistant-stream/utils";
|
|
2
2
|
import type { AssistantCloud } from "./AssistantCloud";
|
|
3
|
+
import type { CloudMessage } from "./AssistantCloudThreadMessages";
|
|
4
|
+
|
|
5
|
+
const CLOUD_MESSAGE_PAGE_SIZE = 200;
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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.
|
|
8
|
+
* Appends, updates and loads cloud messages while mapping local ids to cloud
|
|
9
|
+
* ids and chaining parent_id. A parent that is still being created is awaited,
|
|
10
|
+
* so concurrent appends land under the right parent.
|
|
14
11
|
*/
|
|
15
12
|
export class CloudMessagePersistence {
|
|
16
|
-
private idMapping
|
|
13
|
+
private idMapping = new Map<string, string | Promise<string>>();
|
|
17
14
|
private getCloud: () => AssistantCloud;
|
|
18
15
|
|
|
19
16
|
constructor(cloud: AssistantCloud);
|
|
@@ -39,15 +36,16 @@ export class CloudMessagePersistence {
|
|
|
39
36
|
content: ReadonlyJSONObject,
|
|
40
37
|
): Promise<void> {
|
|
41
38
|
const cloud = this.getCloud();
|
|
42
|
-
const existing = this.idMapping
|
|
39
|
+
const existing = this.idMapping.get(messageId);
|
|
43
40
|
if (existing instanceof Promise) {
|
|
44
41
|
await existing;
|
|
45
42
|
return;
|
|
46
43
|
}
|
|
47
44
|
|
|
48
45
|
const task = (async () => {
|
|
46
|
+
const parentEntry = parentId ? this.idMapping.get(parentId) : undefined;
|
|
49
47
|
const resolvedParentId = parentId
|
|
50
|
-
? ((await
|
|
48
|
+
? ((await parentEntry) ?? parentId)
|
|
51
49
|
: null;
|
|
52
50
|
const { message_id } = await cloud.threads.messages.create(threadId, {
|
|
53
51
|
parent_id: resolvedParentId,
|
|
@@ -57,15 +55,15 @@ export class CloudMessagePersistence {
|
|
|
57
55
|
return message_id;
|
|
58
56
|
})();
|
|
59
57
|
|
|
60
|
-
this.idMapping
|
|
58
|
+
this.idMapping.set(messageId, task);
|
|
61
59
|
try {
|
|
62
60
|
const remoteId = await task;
|
|
63
|
-
if (this.idMapping
|
|
64
|
-
this.idMapping
|
|
61
|
+
if (this.idMapping.get(messageId) === task) {
|
|
62
|
+
this.idMapping.set(messageId, remoteId);
|
|
65
63
|
}
|
|
66
64
|
} catch (err) {
|
|
67
|
-
if (this.idMapping
|
|
68
|
-
|
|
65
|
+
if (this.idMapping.get(messageId) === task) {
|
|
66
|
+
this.idMapping.delete(messageId);
|
|
69
67
|
}
|
|
70
68
|
throw err;
|
|
71
69
|
}
|
|
@@ -95,7 +93,7 @@ export class CloudMessagePersistence {
|
|
|
95
93
|
* Check if a message has been persisted (or is currently being persisted).
|
|
96
94
|
*/
|
|
97
95
|
isPersisted(messageId: string): boolean {
|
|
98
|
-
return
|
|
96
|
+
return this.idMapping.has(messageId);
|
|
99
97
|
}
|
|
100
98
|
|
|
101
99
|
/**
|
|
@@ -103,38 +101,83 @@ export class CloudMessagePersistence {
|
|
|
103
101
|
* Returns undefined if not persisted.
|
|
104
102
|
*/
|
|
105
103
|
async getRemoteId(messageId: string): Promise<string | undefined> {
|
|
106
|
-
const entry = this.idMapping
|
|
104
|
+
const entry = this.idMapping.get(messageId);
|
|
107
105
|
if (!entry) return undefined;
|
|
108
106
|
return entry;
|
|
109
107
|
}
|
|
110
108
|
|
|
109
|
+
getResolvedRemoteId(messageId: string): string | undefined {
|
|
110
|
+
const entry = this.idMapping.get(messageId);
|
|
111
|
+
return typeof entry === "string" ? entry : undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
111
114
|
/**
|
|
112
115
|
* Load messages from the cloud and populate the ID mapping.
|
|
113
116
|
*
|
|
117
|
+
* The list endpoint caps a response at 200 rows, so pages are followed by
|
|
118
|
+
* message ID cursor until a short page and concatenated in server order.
|
|
119
|
+
*
|
|
114
120
|
* The ID mapping is populated so that `isPersisted()` returns true for
|
|
115
121
|
* loaded messages, preventing re-persistence of already-stored messages.
|
|
116
122
|
*
|
|
123
|
+
* A loaded ID that an append already maps keeps the remote ID from that append, and falls back to the loaded ID if the append fails.
|
|
124
|
+
*
|
|
117
125
|
* @param threadId - Remote thread ID
|
|
118
126
|
* @param format - Optional format filter
|
|
119
127
|
* @returns Array of cloud messages
|
|
120
128
|
*/
|
|
121
129
|
async load(threadId: string, format?: string) {
|
|
130
|
+
const idMapping = this.idMapping;
|
|
122
131
|
const cloud = this.getCloud();
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
132
|
+
const messages: CloudMessage[] = [];
|
|
133
|
+
const seen = new Set<string>();
|
|
134
|
+
let after: string | undefined;
|
|
135
|
+
|
|
136
|
+
while (true) {
|
|
137
|
+
const page = await cloud.threads.messages.list(threadId, {
|
|
138
|
+
...(format ? { format } : undefined),
|
|
139
|
+
limit: CLOUD_MESSAGE_PAGE_SIZE,
|
|
140
|
+
...(after ? { after } : undefined),
|
|
141
|
+
});
|
|
142
|
+
const last = page.messages.at(-1);
|
|
143
|
+
if (!last) break;
|
|
144
|
+
|
|
145
|
+
// A cursor the server cannot resolve drops the keyset filter and replays
|
|
146
|
+
// an earlier page, so already-seen rows end the walk instead of repeating.
|
|
147
|
+
const fresh = page.messages.filter((m) => !seen.has(m.id));
|
|
148
|
+
if (fresh.length === 0) break;
|
|
149
|
+
for (const m of fresh) seen.add(m.id);
|
|
150
|
+
|
|
151
|
+
messages.push(...fresh);
|
|
152
|
+
if (page.messages.length < CLOUD_MESSAGE_PAGE_SIZE) break;
|
|
153
|
+
after = last.id;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (this.idMapping === idMapping) {
|
|
157
|
+
for (const m of messages) {
|
|
158
|
+
const entry = idMapping.get(m.id);
|
|
159
|
+
if (entry === undefined) {
|
|
160
|
+
idMapping.set(m.id, m.id);
|
|
161
|
+
} else if (entry instanceof Promise) {
|
|
162
|
+
void entry.catch(() => {
|
|
163
|
+
const current = idMapping.get(m.id);
|
|
164
|
+
if (current === undefined || current === entry) {
|
|
165
|
+
idMapping.set(m.id, m.id);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
130
170
|
}
|
|
131
171
|
return messages;
|
|
132
172
|
}
|
|
133
173
|
|
|
134
174
|
/**
|
|
135
175
|
* Reset the ID mapping (call when switching threads).
|
|
176
|
+
*
|
|
177
|
+
* Pending `load()` and `append()` calls are not cancelled and still settle
|
|
178
|
+
* normally, but their results no longer populate the ID mapping.
|
|
136
179
|
*/
|
|
137
180
|
reset() {
|
|
138
|
-
this.idMapping =
|
|
181
|
+
this.idMapping = new Map();
|
|
139
182
|
}
|
|
140
183
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AssistantCloud } from "./AssistantCloud";
|
|
2
|
+
import { createRunReport, type RunReportInit } from "./runTelemetry";
|
|
3
|
+
|
|
4
|
+
export type CloudRunReportInit = Omit<RunReportInit, "telemetry">;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Sends run reports the way every client integration has to: nothing while
|
|
8
|
+
* telemetry is off, the cloud's environment, release and tags on every report,
|
|
9
|
+
* the `beforeReport` hook applied last, and a failed send that never surfaces.
|
|
10
|
+
* A report given a key is sent once per key, so an integration that observes
|
|
11
|
+
* the same finished run twice reports it once.
|
|
12
|
+
*/
|
|
13
|
+
export class CloudRunReporter {
|
|
14
|
+
private readonly reported = new Set<string>();
|
|
15
|
+
private readonly getCloud: () => AssistantCloud;
|
|
16
|
+
|
|
17
|
+
constructor(cloud: AssistantCloud | (() => AssistantCloud)) {
|
|
18
|
+
this.getCloud = typeof cloud === "function" ? cloud : () => cloud;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public async report(init: CloudRunReportInit, key?: string): Promise<void> {
|
|
22
|
+
try {
|
|
23
|
+
const cloud = this.getCloud();
|
|
24
|
+
if (!cloud.telemetry.enabled) return;
|
|
25
|
+
if (key !== undefined && this.reported.has(key)) return;
|
|
26
|
+
|
|
27
|
+
const initial = createRunReport({ ...init, telemetry: cloud.telemetry });
|
|
28
|
+
const { beforeReport } = cloud.telemetry;
|
|
29
|
+
const report = beforeReport ? beforeReport(initial) : initial;
|
|
30
|
+
if (!report) return;
|
|
31
|
+
|
|
32
|
+
if (key !== undefined) this.reported.add(key);
|
|
33
|
+
await cloud.runs.report(report);
|
|
34
|
+
} catch {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|