assistant-cloud 0.1.34 → 0.1.36
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/dist/AssistantCloud.d.ts +2 -1
- package/dist/AssistantCloud.d.ts.map +1 -1
- package/dist/AssistantCloud.js +4 -1
- package/dist/AssistantCloud.js.map +1 -1
- package/dist/AssistantCloudAPI.d.ts +5 -2
- package/dist/AssistantCloudAPI.d.ts.map +1 -1
- package/dist/AssistantCloudAPI.js +21 -11
- package/dist/AssistantCloudAPI.js.map +1 -1
- package/dist/AssistantCloudAuthStrategy.d.ts +2 -0
- package/dist/AssistantCloudAuthStrategy.d.ts.map +1 -1
- package/dist/AssistantCloudAuthStrategy.js +51 -10
- package/dist/AssistantCloudAuthStrategy.js.map +1 -1
- package/dist/AssistantCloudAuthTokens.d.ts +0 -1
- package/dist/AssistantCloudAuthTokens.d.ts.map +1 -1
- package/dist/AssistantCloudFiles.d.ts +0 -1
- package/dist/AssistantCloudFiles.d.ts.map +1 -1
- package/dist/AssistantCloudProjectThreadMessages.d.ts +19 -0
- package/dist/AssistantCloudProjectThreadMessages.d.ts.map +1 -0
- package/dist/AssistantCloudProjectThreadMessages.js +16 -0
- package/dist/AssistantCloudProjectThreadMessages.js.map +1 -0
- package/dist/AssistantCloudProjectThreads.d.ts +21 -0
- package/dist/AssistantCloudProjectThreads.d.ts.map +1 -0
- package/dist/AssistantCloudProjectThreads.js +22 -0
- package/dist/AssistantCloudProjectThreads.js.map +1 -0
- package/dist/AssistantCloudProjects.d.ts +10 -0
- package/dist/AssistantCloudProjects.d.ts.map +1 -0
- package/dist/AssistantCloudProjects.js +12 -0
- package/dist/AssistantCloudProjects.js.map +1 -0
- package/dist/AssistantCloudRuns.d.ts +0 -1
- package/dist/AssistantCloudRuns.d.ts.map +1 -1
- package/dist/AssistantCloudThreadMessages.d.ts +2 -2
- package/dist/AssistantCloudThreadMessages.d.ts.map +1 -1
- package/dist/AssistantCloudThreadMessages.js +15 -2
- package/dist/AssistantCloudThreadMessages.js.map +1 -1
- package/dist/AssistantCloudThreads.d.ts +2 -2
- package/dist/AssistantCloudThreads.d.ts.map +1 -1
- package/dist/AssistantCloudThreads.js +20 -3
- package/dist/AssistantCloudThreads.js.map +1 -1
- package/dist/CloudMessagePersistence.d.ts +2 -2
- package/dist/CloudMessagePersistence.d.ts.map +1 -1
- package/dist/CloudMessagePersistence.js +11 -6
- package/dist/CloudMessagePersistence.js.map +1 -1
- package/dist/FormattedCloudPersistence.d.ts +0 -1
- package/dist/FormattedCloudPersistence.d.ts.map +1 -1
- package/dist/cloudResponse.d.ts +16 -0
- package/dist/cloudResponse.d.ts.map +1 -0
- package/dist/cloudResponse.js +43 -0
- package/dist/cloudResponse.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -1
- package/dist/instrumentMcpSampling.d.ts.map +1 -1
- package/dist/tests/setup.d.ts +1 -1
- package/dist/tests/setup.js +2 -4
- package/dist/tests/setup.js.map +1 -1
- package/package.json +5 -5
- package/src/AssistantCloud.ts +3 -0
- package/src/AssistantCloudAPI.ts +31 -13
- package/src/AssistantCloudAuthStrategy.ts +68 -24
- package/src/AssistantCloudProjectThreadMessages.ts +40 -0
- package/src/AssistantCloudProjectThreads.ts +47 -0
- package/src/AssistantCloudProjects.ts +10 -0
- package/src/AssistantCloudThreadMessages.test.ts +41 -0
- package/src/AssistantCloudThreadMessages.ts +38 -3
- package/src/AssistantCloudThreads.test.ts +74 -0
- package/src/AssistantCloudThreads.ts +51 -3
- package/src/CloudMessagePersistence.ts +18 -5
- package/src/cloudResponse.test.ts +28 -0
- package/src/cloudResponse.ts +72 -0
- package/src/index.ts +2 -0
- package/src/tests/AssistantCloudAPI.test.ts +147 -8
- package/src/tests/AssistantCloudAuthStrategy.test.ts +191 -0
- package/src/tests/AssistantCloudProjects.test.ts +121 -0
- package/src/tests/CloudMessagePersistence.test.ts +40 -0
- package/src/tests/setup.ts +2 -9
|
@@ -39,6 +39,7 @@ export class AssistantCloudJWTAuthStrategy implements AssistantCloudAuthStrategy
|
|
|
39
39
|
|
|
40
40
|
private cachedToken: string | null = null;
|
|
41
41
|
private tokenExpiry: number | null = null;
|
|
42
|
+
private tokenRequest: Promise<Record<string, string> | false> | null = null;
|
|
42
43
|
#authTokenCallback: () => Promise<string | null>;
|
|
43
44
|
|
|
44
45
|
constructor(authTokenCallback: () => Promise<string | null>) {
|
|
@@ -57,14 +58,28 @@ export class AssistantCloudJWTAuthStrategy implements AssistantCloudAuthStrategy
|
|
|
57
58
|
return { Authorization: `Bearer ${this.cachedToken}` };
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
if (!this.tokenRequest) {
|
|
62
|
+
this.tokenRequest = this.fetchAuthHeaders();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const tokenRequest = this.tokenRequest;
|
|
66
|
+
try {
|
|
67
|
+
return await tokenRequest;
|
|
68
|
+
} finally {
|
|
69
|
+
if (this.tokenRequest === tokenRequest) {
|
|
70
|
+
this.tokenRequest = null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
63
74
|
|
|
64
|
-
|
|
65
|
-
|
|
75
|
+
private async fetchAuthHeaders(): Promise<Record<string, string> | false> {
|
|
76
|
+
const token = await this.#authTokenCallback();
|
|
77
|
+
if (!token) return false;
|
|
66
78
|
|
|
67
|
-
|
|
79
|
+
this.cachedToken = token;
|
|
80
|
+
this.tokenExpiry = getJwtExpiry(token);
|
|
81
|
+
|
|
82
|
+
return { Authorization: `Bearer ${token}` };
|
|
68
83
|
}
|
|
69
84
|
|
|
70
85
|
public readAuthHeaders(headers: Headers) {
|
|
@@ -109,6 +124,49 @@ export class AssistantCloudAPIKeyAuthStrategy implements AssistantCloudAuthStrat
|
|
|
109
124
|
|
|
110
125
|
const AUI_REFRESH_TOKEN_NAME = "aui:refresh_token";
|
|
111
126
|
|
|
127
|
+
const getLocalStorage = (): Storage | null => {
|
|
128
|
+
if (!("localStorage" in globalThis)) return null;
|
|
129
|
+
try {
|
|
130
|
+
return (globalThis as { localStorage: Storage }).localStorage;
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const readRefreshToken = ():
|
|
137
|
+
| { token: string; expires_at: string }
|
|
138
|
+
| undefined => {
|
|
139
|
+
const storage = getLocalStorage();
|
|
140
|
+
if (!storage) return undefined;
|
|
141
|
+
try {
|
|
142
|
+
const value = storage.getItem(AUI_REFRESH_TOKEN_NAME);
|
|
143
|
+
return value
|
|
144
|
+
? (JSON.parse(value) as { token: string; expires_at: string })
|
|
145
|
+
: undefined;
|
|
146
|
+
} catch {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const writeRefreshToken = (refreshToken: {
|
|
152
|
+
token: string;
|
|
153
|
+
expires_at: string;
|
|
154
|
+
}): void => {
|
|
155
|
+
const storage = getLocalStorage();
|
|
156
|
+
if (!storage) return;
|
|
157
|
+
try {
|
|
158
|
+
storage.setItem(AUI_REFRESH_TOKEN_NAME, JSON.stringify(refreshToken));
|
|
159
|
+
} catch {}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const removeRefreshToken = (): void => {
|
|
163
|
+
const storage = getLocalStorage();
|
|
164
|
+
if (!storage) return;
|
|
165
|
+
try {
|
|
166
|
+
storage.removeItem(AUI_REFRESH_TOKEN_NAME);
|
|
167
|
+
} catch {}
|
|
168
|
+
};
|
|
169
|
+
|
|
112
170
|
export class AssistantCloudAnonymousAuthStrategy implements AssistantCloudAuthStrategy {
|
|
113
171
|
public readonly strategy = "anon";
|
|
114
172
|
|
|
@@ -119,15 +177,7 @@ export class AssistantCloudAnonymousAuthStrategy implements AssistantCloudAuthSt
|
|
|
119
177
|
this.baseUrl = baseUrl;
|
|
120
178
|
this.jwtStrategy = new AssistantCloudJWTAuthStrategy(async () => {
|
|
121
179
|
const currentTime = Date.now();
|
|
122
|
-
const
|
|
123
|
-
AUI_REFRESH_TOKEN_NAME,
|
|
124
|
-
);
|
|
125
|
-
const storedRefreshToken = storedRefreshTokenJson
|
|
126
|
-
? (JSON.parse(storedRefreshTokenJson) as {
|
|
127
|
-
token: string;
|
|
128
|
-
expires_at: string;
|
|
129
|
-
})
|
|
130
|
-
: undefined;
|
|
180
|
+
const storedRefreshToken = readRefreshToken();
|
|
131
181
|
|
|
132
182
|
if (storedRefreshToken) {
|
|
133
183
|
const refreshExpiry = new Date(storedRefreshToken.expires_at).getTime();
|
|
@@ -145,15 +195,12 @@ export class AssistantCloudAnonymousAuthStrategy implements AssistantCloudAuthSt
|
|
|
145
195
|
const data = await response.json();
|
|
146
196
|
const { access_token, refresh_token } = data;
|
|
147
197
|
if (refresh_token) {
|
|
148
|
-
|
|
149
|
-
AUI_REFRESH_TOKEN_NAME,
|
|
150
|
-
JSON.stringify(refresh_token),
|
|
151
|
-
);
|
|
198
|
+
writeRefreshToken(refresh_token);
|
|
152
199
|
}
|
|
153
200
|
return access_token;
|
|
154
201
|
}
|
|
155
202
|
} else {
|
|
156
|
-
|
|
203
|
+
removeRefreshToken();
|
|
157
204
|
}
|
|
158
205
|
}
|
|
159
206
|
|
|
@@ -169,10 +216,7 @@ export class AssistantCloudAnonymousAuthStrategy implements AssistantCloudAuthSt
|
|
|
169
216
|
|
|
170
217
|
if (!access_token || !refresh_token) return null;
|
|
171
218
|
|
|
172
|
-
|
|
173
|
-
AUI_REFRESH_TOKEN_NAME,
|
|
174
|
-
JSON.stringify(refresh_token),
|
|
175
|
-
);
|
|
219
|
+
writeRefreshToken(refresh_token);
|
|
176
220
|
return access_token;
|
|
177
221
|
});
|
|
178
222
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { AssistantCloudAPI } from "./AssistantCloudAPI";
|
|
2
|
+
import {
|
|
3
|
+
decodeCloudMessage,
|
|
4
|
+
type CloudMessage,
|
|
5
|
+
} from "./AssistantCloudThreadMessages";
|
|
6
|
+
import { readCloudArray, readCloudRecord } from "./cloudResponse";
|
|
7
|
+
|
|
8
|
+
type AssistantCloudProjectThreadMessageListQuery = {
|
|
9
|
+
format?: string;
|
|
10
|
+
limit?: number;
|
|
11
|
+
after?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type AssistantCloudProjectThreadMessageListResponse = {
|
|
15
|
+
messages: CloudMessage[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export class AssistantCloudProjectThreadMessages {
|
|
19
|
+
constructor(private cloud: AssistantCloudAPI) {}
|
|
20
|
+
|
|
21
|
+
public async list(
|
|
22
|
+
threadId: string,
|
|
23
|
+
query?: AssistantCloudProjectThreadMessageListQuery,
|
|
24
|
+
): Promise<AssistantCloudProjectThreadMessageListResponse> {
|
|
25
|
+
const response = readCloudRecord(
|
|
26
|
+
await this.cloud.makeRequest(
|
|
27
|
+
`/projects/threads/${encodeURIComponent(threadId)}/messages`,
|
|
28
|
+
{ query },
|
|
29
|
+
),
|
|
30
|
+
"project thread message list response",
|
|
31
|
+
);
|
|
32
|
+
const messages = readCloudArray(response.messages, "messages");
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
messages: messages.map((message, index) =>
|
|
36
|
+
decodeCloudMessage(message, `messages[${index}]`),
|
|
37
|
+
),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { AssistantCloudAPI } from "./AssistantCloudAPI";
|
|
2
|
+
import { AssistantCloudProjectThreadMessages } from "./AssistantCloudProjectThreadMessages";
|
|
3
|
+
import { decodeCloudThread, type CloudThread } from "./AssistantCloudThreads";
|
|
4
|
+
import { readCloudArray, readCloudRecord } from "./cloudResponse";
|
|
5
|
+
|
|
6
|
+
type AssistantCloudProjectThreadsListQuery = {
|
|
7
|
+
is_archived?: boolean;
|
|
8
|
+
limit?: number;
|
|
9
|
+
after?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type AssistantCloudProjectThreadsListResponse = {
|
|
13
|
+
threads: CloudThread[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export class AssistantCloudProjectThreads {
|
|
17
|
+
public readonly messages: AssistantCloudProjectThreadMessages;
|
|
18
|
+
|
|
19
|
+
constructor(private cloud: AssistantCloudAPI) {
|
|
20
|
+
this.messages = new AssistantCloudProjectThreadMessages(cloud);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public async list(
|
|
24
|
+
query?: AssistantCloudProjectThreadsListQuery,
|
|
25
|
+
): Promise<AssistantCloudProjectThreadsListResponse> {
|
|
26
|
+
const response = readCloudRecord(
|
|
27
|
+
await this.cloud.makeRequest("/projects/threads", {
|
|
28
|
+
query: {
|
|
29
|
+
...query,
|
|
30
|
+
// The shared query serializer drops `false`; the wire accepts the
|
|
31
|
+
// string form, so the archive filter survives explicitly.
|
|
32
|
+
...(query?.is_archived !== undefined
|
|
33
|
+
? { is_archived: String(query.is_archived) }
|
|
34
|
+
: {}),
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
"project thread list response",
|
|
38
|
+
);
|
|
39
|
+
const threads = readCloudArray(response.threads, "threads");
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
threads: threads.map((thread, index) =>
|
|
43
|
+
decodeCloudThread(thread, `threads[${index}]`),
|
|
44
|
+
),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AssistantCloudAPI } from "./AssistantCloudAPI";
|
|
2
|
+
import { AssistantCloudProjectThreads } from "./AssistantCloudProjectThreads";
|
|
3
|
+
|
|
4
|
+
export class AssistantCloudProjects {
|
|
5
|
+
public readonly threads: AssistantCloudProjectThreads;
|
|
6
|
+
|
|
7
|
+
constructor(cloud: AssistantCloudAPI) {
|
|
8
|
+
this.threads = new AssistantCloudProjectThreads(cloud);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { AssistantCloudAPI } from "./AssistantCloudAPI";
|
|
3
|
+
import { AssistantCloudThreadMessages } from "./AssistantCloudThreadMessages";
|
|
4
|
+
|
|
5
|
+
const createCloudThreadMessages = () => {
|
|
6
|
+
const makeRequest = vi.fn();
|
|
7
|
+
const api = { makeRequest } as unknown as AssistantCloudAPI;
|
|
8
|
+
return {
|
|
9
|
+
messages: new AssistantCloudThreadMessages(api),
|
|
10
|
+
makeRequest,
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe("AssistantCloudThreadMessages responses", () => {
|
|
15
|
+
it("decodes canonical message responses without changing content", async () => {
|
|
16
|
+
const { messages, makeRequest } = createCloudThreadMessages();
|
|
17
|
+
makeRequest.mockResolvedValue({
|
|
18
|
+
messages: [
|
|
19
|
+
{
|
|
20
|
+
id: "message-1",
|
|
21
|
+
parent_id: null,
|
|
22
|
+
height: 0,
|
|
23
|
+
created_at: "2026-07-16T13:00:00.000Z",
|
|
24
|
+
updated_at: "2026-07-16T13:05:00.987Z",
|
|
25
|
+
format: "aui/v0",
|
|
26
|
+
content: { created_at: "leave-this-string-untouched" },
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const result = await messages.list("thread-1");
|
|
32
|
+
const message = result.messages[0]!;
|
|
33
|
+
|
|
34
|
+
expect(message.created_at).toBeInstanceOf(Date);
|
|
35
|
+
expect(message.updated_at).toBeInstanceOf(Date);
|
|
36
|
+
expect(message.updated_at.toISOString()).toBe("2026-07-16T13:05:00.987Z");
|
|
37
|
+
expect(message.content).toEqual({
|
|
38
|
+
created_at: "leave-this-string-untouched",
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import type { ReadonlyJSONObject } from "assistant-stream/utils";
|
|
2
2
|
import type { AssistantCloudAPI } from "./AssistantCloudAPI";
|
|
3
|
+
import {
|
|
4
|
+
readCloudArray,
|
|
5
|
+
readCloudInteger,
|
|
6
|
+
readCloudJSONObject,
|
|
7
|
+
readCloudNullableString,
|
|
8
|
+
readCloudRecord,
|
|
9
|
+
readCloudString,
|
|
10
|
+
readCloudTimestamp,
|
|
11
|
+
} from "./cloudResponse";
|
|
3
12
|
|
|
4
13
|
export type CloudMessage = {
|
|
5
14
|
id: string;
|
|
@@ -33,6 +42,22 @@ type AssistantCloudThreadMessageUpdateBody = {
|
|
|
33
42
|
content: ReadonlyJSONObject;
|
|
34
43
|
};
|
|
35
44
|
|
|
45
|
+
export const decodeCloudMessage = (
|
|
46
|
+
value: unknown,
|
|
47
|
+
field: string,
|
|
48
|
+
): CloudMessage => {
|
|
49
|
+
const message = readCloudRecord(value, field);
|
|
50
|
+
return {
|
|
51
|
+
id: readCloudString(message.id, `${field}.id`),
|
|
52
|
+
parent_id: readCloudNullableString(message.parent_id, `${field}.parent_id`),
|
|
53
|
+
height: readCloudInteger(message.height, `${field}.height`),
|
|
54
|
+
created_at: readCloudTimestamp(message.created_at, `${field}.created_at`),
|
|
55
|
+
updated_at: readCloudTimestamp(message.updated_at, `${field}.updated_at`),
|
|
56
|
+
format: readCloudString(message.format, `${field}.format`),
|
|
57
|
+
content: readCloudJSONObject(message.content, `${field}.content`),
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
36
61
|
export class AssistantCloudThreadMessages {
|
|
37
62
|
constructor(private cloud: AssistantCloudAPI) {}
|
|
38
63
|
|
|
@@ -40,10 +65,20 @@ export class AssistantCloudThreadMessages {
|
|
|
40
65
|
threadId: string,
|
|
41
66
|
query?: AssistantCloudThreadMessageListQuery,
|
|
42
67
|
): Promise<AssistantCloudThreadMessageListResponse> {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
68
|
+
const response = readCloudRecord(
|
|
69
|
+
await this.cloud.makeRequest(
|
|
70
|
+
`/threads/${encodeURIComponent(threadId)}/messages`,
|
|
71
|
+
{ query },
|
|
72
|
+
),
|
|
73
|
+
"thread message list response",
|
|
46
74
|
);
|
|
75
|
+
const messages = readCloudArray(response.messages, "messages");
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
messages: messages.map((message, index) =>
|
|
79
|
+
decodeCloudMessage(message, `messages[${index}]`),
|
|
80
|
+
),
|
|
81
|
+
};
|
|
47
82
|
}
|
|
48
83
|
|
|
49
84
|
public async create(
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { AssistantCloudAPI } from "./AssistantCloudAPI";
|
|
3
|
+
import { AssistantCloudThreads } from "./AssistantCloudThreads";
|
|
4
|
+
|
|
5
|
+
const createCloudThreads = () => {
|
|
6
|
+
const makeRequest = vi.fn();
|
|
7
|
+
const api = { makeRequest } as unknown as AssistantCloudAPI;
|
|
8
|
+
return { threads: new AssistantCloudThreads(api), makeRequest };
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const threadResponse = {
|
|
12
|
+
title: null,
|
|
13
|
+
last_message_at: "2026-07-16T12:30:00.000Z",
|
|
14
|
+
metadata: { created_at: "leave-this-string-untouched" },
|
|
15
|
+
external_id: null,
|
|
16
|
+
id: "thread-1",
|
|
17
|
+
project_id: "project-1",
|
|
18
|
+
created_at: "2026-07-16T12:00:00.000Z",
|
|
19
|
+
updated_at: "2026-07-16T12:15:00.123Z",
|
|
20
|
+
workspace_id: "workspace-1",
|
|
21
|
+
is_archived: false,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe("AssistantCloudThreads responses", () => {
|
|
25
|
+
it("decodes canonical thread list responses", async () => {
|
|
26
|
+
const { threads, makeRequest } = createCloudThreads();
|
|
27
|
+
makeRequest.mockResolvedValue({ threads: [threadResponse] });
|
|
28
|
+
|
|
29
|
+
const result = await threads.list();
|
|
30
|
+
const thread = result.threads[0]!;
|
|
31
|
+
|
|
32
|
+
expect(thread.created_at).toBeInstanceOf(Date);
|
|
33
|
+
expect(thread.updated_at).toBeInstanceOf(Date);
|
|
34
|
+
expect(thread.last_message_at).toBeInstanceOf(Date);
|
|
35
|
+
expect(thread.updated_at.toISOString()).toBe("2026-07-16T12:15:00.123Z");
|
|
36
|
+
expect(thread.title).toBe("");
|
|
37
|
+
expect(thread.metadata).toEqual({
|
|
38
|
+
created_at: "leave-this-string-untouched",
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("unwraps and decodes a single thread response", async () => {
|
|
43
|
+
const { threads, makeRequest } = createCloudThreads();
|
|
44
|
+
makeRequest.mockResolvedValue({ thread: threadResponse });
|
|
45
|
+
|
|
46
|
+
const result = await threads.get("thread-1");
|
|
47
|
+
|
|
48
|
+
expect(result.created_at).toBeInstanceOf(Date);
|
|
49
|
+
expect(result.created_at.toISOString()).toBe("2026-07-16T12:00:00.000Z");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("rejects a bare thread that is missing the response envelope", async () => {
|
|
53
|
+
const { threads, makeRequest } = createCloudThreads();
|
|
54
|
+
makeRequest.mockResolvedValue(threadResponse);
|
|
55
|
+
|
|
56
|
+
await expect(threads.get("thread-1")).rejects.toThrow(
|
|
57
|
+
'Invalid Assistant Cloud response for "thread": expected an object',
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("rejects genuinely invalid response timestamps with field context", async () => {
|
|
62
|
+
const { threads, makeRequest } = createCloudThreads();
|
|
63
|
+
makeRequest.mockResolvedValue({
|
|
64
|
+
thread: {
|
|
65
|
+
...threadResponse,
|
|
66
|
+
updated_at: "not-a-timestamp",
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await expect(threads.get("thread-1")).rejects.toThrow(
|
|
71
|
+
'Invalid Assistant Cloud response for "thread.updated_at": expected a canonical ISO timestamp',
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { AssistantCloudAPI } from "./AssistantCloudAPI";
|
|
2
2
|
import { AssistantCloudThreadMessages } from "./AssistantCloudThreadMessages";
|
|
3
|
+
import {
|
|
4
|
+
readCloudArray,
|
|
5
|
+
readCloudBoolean,
|
|
6
|
+
readCloudNullableString,
|
|
7
|
+
readCloudRecord,
|
|
8
|
+
readCloudString,
|
|
9
|
+
readCloudTimestamp,
|
|
10
|
+
} from "./cloudResponse";
|
|
3
11
|
|
|
4
12
|
type AssistantCloudThreadsListQuery = {
|
|
5
13
|
is_archived?: boolean;
|
|
@@ -7,7 +15,7 @@ type AssistantCloudThreadsListQuery = {
|
|
|
7
15
|
after?: string;
|
|
8
16
|
};
|
|
9
17
|
|
|
10
|
-
type CloudThread = {
|
|
18
|
+
export type CloudThread = {
|
|
11
19
|
title: string;
|
|
12
20
|
last_message_at: Date;
|
|
13
21
|
metadata: unknown;
|
|
@@ -42,6 +50,31 @@ type AssistantCloudThreadsUpdateBody = {
|
|
|
42
50
|
is_archived?: boolean | undefined;
|
|
43
51
|
};
|
|
44
52
|
|
|
53
|
+
export const decodeCloudThread = (
|
|
54
|
+
value: unknown,
|
|
55
|
+
field: string,
|
|
56
|
+
): CloudThread => {
|
|
57
|
+
const thread = readCloudRecord(value, field);
|
|
58
|
+
return {
|
|
59
|
+
title: readCloudNullableString(thread.title, `${field}.title`) ?? "",
|
|
60
|
+
last_message_at: readCloudTimestamp(
|
|
61
|
+
thread.last_message_at,
|
|
62
|
+
`${field}.last_message_at`,
|
|
63
|
+
),
|
|
64
|
+
metadata: thread.metadata,
|
|
65
|
+
external_id: readCloudNullableString(
|
|
66
|
+
thread.external_id,
|
|
67
|
+
`${field}.external_id`,
|
|
68
|
+
),
|
|
69
|
+
id: readCloudString(thread.id, `${field}.id`),
|
|
70
|
+
project_id: readCloudString(thread.project_id, `${field}.project_id`),
|
|
71
|
+
created_at: readCloudTimestamp(thread.created_at, `${field}.created_at`),
|
|
72
|
+
updated_at: readCloudTimestamp(thread.updated_at, `${field}.updated_at`),
|
|
73
|
+
workspace_id: readCloudString(thread.workspace_id, `${field}.workspace_id`),
|
|
74
|
+
is_archived: readCloudBoolean(thread.is_archived, `${field}.is_archived`),
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
|
|
45
78
|
export class AssistantCloudThreads {
|
|
46
79
|
public readonly messages: AssistantCloudThreadMessages;
|
|
47
80
|
|
|
@@ -52,11 +85,26 @@ export class AssistantCloudThreads {
|
|
|
52
85
|
public async list(
|
|
53
86
|
query?: AssistantCloudThreadsListQuery,
|
|
54
87
|
): Promise<AssistantCloudThreadsListResponse> {
|
|
55
|
-
|
|
88
|
+
const response = readCloudRecord(
|
|
89
|
+
await this.cloud.makeRequest("/threads", { query }),
|
|
90
|
+
"thread list response",
|
|
91
|
+
);
|
|
92
|
+
const threads = readCloudArray(response.threads, "threads");
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
threads: threads.map((thread, index) =>
|
|
96
|
+
decodeCloudThread(thread, `threads[${index}]`),
|
|
97
|
+
),
|
|
98
|
+
};
|
|
56
99
|
}
|
|
57
100
|
|
|
58
101
|
public async get(threadId: string): Promise<CloudThread> {
|
|
59
|
-
|
|
102
|
+
const response = readCloudRecord(
|
|
103
|
+
await this.cloud.makeRequest(`/threads/${encodeURIComponent(threadId)}`),
|
|
104
|
+
"thread response",
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return decodeCloudThread(response.thread, "thread");
|
|
60
108
|
}
|
|
61
109
|
|
|
62
110
|
public async create(
|
|
@@ -14,8 +14,13 @@ import type { AssistantCloud } from "./AssistantCloud";
|
|
|
14
14
|
*/
|
|
15
15
|
export class CloudMessagePersistence {
|
|
16
16
|
private idMapping: Record<string, string | Promise<string>> = {};
|
|
17
|
+
private getCloud: () => AssistantCloud;
|
|
17
18
|
|
|
18
|
-
constructor(
|
|
19
|
+
constructor(cloud: AssistantCloud);
|
|
20
|
+
constructor(getCloud: () => AssistantCloud);
|
|
21
|
+
constructor(cloud: AssistantCloud | (() => AssistantCloud)) {
|
|
22
|
+
this.getCloud = typeof cloud === "function" ? cloud : () => cloud;
|
|
23
|
+
}
|
|
19
24
|
|
|
20
25
|
/**
|
|
21
26
|
* Persist a message to the cloud.
|
|
@@ -33,12 +38,13 @@ export class CloudMessagePersistence {
|
|
|
33
38
|
format: string,
|
|
34
39
|
content: ReadonlyJSONObject,
|
|
35
40
|
): Promise<void> {
|
|
41
|
+
const cloud = this.getCloud();
|
|
36
42
|
// Resolve parent's remote ID if it exists (may be a promise if concurrent)
|
|
37
43
|
const resolvedParentId = parentId
|
|
38
44
|
? ((await this.idMapping[parentId]) ?? parentId)
|
|
39
45
|
: null;
|
|
40
46
|
|
|
41
|
-
const task =
|
|
47
|
+
const task = cloud.threads.messages
|
|
42
48
|
.create(threadId, {
|
|
43
49
|
parent_id: resolvedParentId,
|
|
44
50
|
format,
|
|
@@ -70,9 +76,15 @@ export class CloudMessagePersistence {
|
|
|
70
76
|
_format: string,
|
|
71
77
|
content: ReadonlyJSONObject,
|
|
72
78
|
): Promise<void> {
|
|
79
|
+
const cloud = this.getCloud();
|
|
73
80
|
const remoteId = await this.getRemoteId(messageId);
|
|
74
|
-
if (!remoteId)
|
|
75
|
-
|
|
81
|
+
if (!remoteId) {
|
|
82
|
+
console.warn(
|
|
83
|
+
`Skipping update for message ${messageId}: no remote id is mapped.`,
|
|
84
|
+
);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
await cloud.threads.messages.update(threadId, remoteId, { content });
|
|
76
88
|
}
|
|
77
89
|
|
|
78
90
|
/**
|
|
@@ -103,7 +115,8 @@ export class CloudMessagePersistence {
|
|
|
103
115
|
* @returns Array of cloud messages
|
|
104
116
|
*/
|
|
105
117
|
async load(threadId: string, format?: string) {
|
|
106
|
-
const
|
|
118
|
+
const cloud = this.getCloud();
|
|
119
|
+
const { messages } = await cloud.threads.messages.list(
|
|
107
120
|
threadId,
|
|
108
121
|
format ? { format } : undefined,
|
|
109
122
|
);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { CloudResponseError, readCloudTimestamp } from "./cloudResponse";
|
|
3
|
+
|
|
4
|
+
describe("readCloudTimestamp", () => {
|
|
5
|
+
it("decodes a canonical Cloud timestamp", () => {
|
|
6
|
+
expect(
|
|
7
|
+
readCloudTimestamp(
|
|
8
|
+
"2026-07-16T12:15:00.123Z",
|
|
9
|
+
"thread.updated_at",
|
|
10
|
+
).toISOString(),
|
|
11
|
+
).toBe("2026-07-16T12:15:00.123Z");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it.each([
|
|
15
|
+
"2026-02-30T12:15:00.000Z",
|
|
16
|
+
"2026-13-01T12:15:00Z",
|
|
17
|
+
"2026-07-16 12:15:00.123456",
|
|
18
|
+
"2026-07-16T12:15:00.123+00:00",
|
|
19
|
+
new Date("2026-07-16T12:15:00.123Z"),
|
|
20
|
+
"not-a-timestamp",
|
|
21
|
+
])("rejects invalid timestamp %s", (value) => {
|
|
22
|
+
const decode = () => readCloudTimestamp(value, "thread.updated_at");
|
|
23
|
+
expect(decode).toThrow(CloudResponseError);
|
|
24
|
+
expect(decode).toThrow(
|
|
25
|
+
'Invalid Assistant Cloud response for "thread.updated_at": expected a canonical ISO timestamp',
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ReadonlyJSONObject } from "assistant-stream/utils";
|
|
2
|
+
|
|
3
|
+
export class CloudResponseError extends Error {
|
|
4
|
+
constructor(message: string) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "CloudResponseError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const invalidCloudResponse = (field: string, expected: string) =>
|
|
11
|
+
new CloudResponseError(
|
|
12
|
+
`Invalid Assistant Cloud response for "${field}": expected ${expected}`,
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export const readCloudRecord = (
|
|
16
|
+
value: unknown,
|
|
17
|
+
field: string,
|
|
18
|
+
): Record<string, unknown> => {
|
|
19
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
20
|
+
throw invalidCloudResponse(field, "an object");
|
|
21
|
+
}
|
|
22
|
+
return value as Record<string, unknown>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const readCloudArray = (value: unknown, field: string): unknown[] => {
|
|
26
|
+
if (!Array.isArray(value)) throw invalidCloudResponse(field, "an array");
|
|
27
|
+
return value;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const readCloudString = (value: unknown, field: string): string => {
|
|
31
|
+
if (typeof value !== "string") throw invalidCloudResponse(field, "a string");
|
|
32
|
+
return value;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const readCloudNullableString = (
|
|
36
|
+
value: unknown,
|
|
37
|
+
field: string,
|
|
38
|
+
): string | null => {
|
|
39
|
+
if (value === null) return null;
|
|
40
|
+
return readCloudString(value, field);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const readCloudBoolean = (value: unknown, field: string): boolean => {
|
|
44
|
+
if (typeof value !== "boolean") {
|
|
45
|
+
throw invalidCloudResponse(field, "a boolean");
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const readCloudInteger = (value: unknown, field: string): number => {
|
|
51
|
+
if (!Number.isInteger(value)) throw invalidCloudResponse(field, "an integer");
|
|
52
|
+
return value as number;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const readCloudTimestamp = (value: unknown, field: string): Date => {
|
|
56
|
+
if (typeof value !== "string") {
|
|
57
|
+
throw invalidCloudResponse(field, "a canonical ISO timestamp");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const date = new Date(value);
|
|
61
|
+
// The round trip pins the backend DTO's Date#toISOString() wire contract.
|
|
62
|
+
if (Number.isNaN(date.getTime()) || date.toISOString() !== value) {
|
|
63
|
+
throw invalidCloudResponse(field, "a canonical ISO timestamp");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return date;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const readCloudJSONObject = (
|
|
70
|
+
value: unknown,
|
|
71
|
+
field: string,
|
|
72
|
+
): ReadonlyJSONObject => readCloudRecord(value, field) as ReadonlyJSONObject;
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export type { CloudMessage } from "./AssistantCloudThreadMessages";
|
|
2
2
|
export type { AssistantCloudTelemetryConfig } from "./AssistantCloudAPI";
|
|
3
|
+
export { CloudAPIError } from "./AssistantCloudAPI";
|
|
4
|
+
export { CloudResponseError } from "./cloudResponse";
|
|
3
5
|
export type { AssistantCloudRunReport } from "./AssistantCloudRuns";
|
|
4
6
|
export { AssistantCloud } from "./AssistantCloud";
|
|
5
7
|
export { CloudMessagePersistence } from "./CloudMessagePersistence";
|