chatkit-bun 0.0.2
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 +202 -0
- package/package.json +40 -0
- package/src/actions.ts +39 -0
- package/src/agents/accumulate.ts +43 -0
- package/src/agents/annotations.ts +157 -0
- package/src/agents/context.ts +190 -0
- package/src/agents/converter.ts +290 -0
- package/src/agents/index.ts +25 -0
- package/src/agents/stream.ts +1053 -0
- package/src/agents/types.ts +30 -0
- package/src/agents/workflows.ts +220 -0
- package/src/errors.ts +19 -0
- package/src/http.ts +60 -0
- package/src/index.ts +11 -0
- package/src/serialization.ts +75 -0
- package/src/server.ts +874 -0
- package/src/sqlite-store.ts +400 -0
- package/src/store.ts +98 -0
- package/src/types/core.ts +322 -0
- package/src/types/server.ts +396 -0
- package/src/widgets/components.ts +188 -0
- package/src/widgets/diff.ts +151 -0
- package/src/widgets/index.ts +6 -0
- package/src/widgets/serialization.ts +46 -0
- package/src/widgets/stream.ts +104 -0
- package/src/widgets/template.ts +180 -0
- package/src/widgets/types.ts +52 -0
- package/types/actions.d.ts +19 -0
- package/types/agents/accumulate.d.ts +4 -0
- package/types/agents/annotations.d.ts +21 -0
- package/types/agents/context.d.ts +35 -0
- package/types/agents/converter.d.ts +60 -0
- package/types/agents/index.d.ts +9 -0
- package/types/agents/stream.d.ts +4 -0
- package/types/agents/types.d.ts +26 -0
- package/types/agents/workflows.d.ts +34 -0
- package/types/errors.d.ts +11 -0
- package/types/http.d.ts +6 -0
- package/types/index.d.ts +11 -0
- package/types/serialization.d.ts +8 -0
- package/types/server.d.ts +73 -0
- package/types/sqlite-store.d.ts +43 -0
- package/types/store.d.ts +45 -0
- package/types/types/core.d.ts +1220 -0
- package/types/types/server.d.ts +5841 -0
- package/types/widgets/components.d.ts +144 -0
- package/types/widgets/diff.d.ts +7 -0
- package/types/widgets/index.d.ts +6 -0
- package/types/widgets/serialization.d.ts +2 -0
- package/types/widgets/stream.d.ts +10 -0
- package/types/widgets/template.d.ts +19 -0
- package/types/widgets/types.d.ts +24 -0
package/src/server.ts
ADDED
|
@@ -0,0 +1,874 @@
|
|
|
1
|
+
import { NotFoundError, UnsupportedOperationError, ValidationError } from "./errors";
|
|
2
|
+
import { decodeJsonBytes, encodeJsonBytes } from "./serialization";
|
|
3
|
+
import type { AttachmentStore, Store } from "./store";
|
|
4
|
+
import { ThreadMetadataSchema, type Page, type ThreadItem, type ThreadMetadata } from "./types/core";
|
|
5
|
+
import {
|
|
6
|
+
ChatKitRequestSchema,
|
|
7
|
+
DEFAULT_PAGE_SIZE,
|
|
8
|
+
SyncCustomActionResponseSchema,
|
|
9
|
+
ThreadStreamEventSchema,
|
|
10
|
+
TranscriptionResultSchema,
|
|
11
|
+
isStreamingRequest,
|
|
12
|
+
type AudioInput,
|
|
13
|
+
type ChatKitRequest,
|
|
14
|
+
type FeedbackKind,
|
|
15
|
+
type NonStreamingRequest,
|
|
16
|
+
type StreamOptions,
|
|
17
|
+
type StreamingRequest,
|
|
18
|
+
type StructuredInputSubmission,
|
|
19
|
+
type SyncCustomActionResponse,
|
|
20
|
+
type Thread,
|
|
21
|
+
type ThreadCustomActionParams,
|
|
22
|
+
type ThreadItemUpdate,
|
|
23
|
+
type ThreadStreamEvent,
|
|
24
|
+
type TranscriptionResult,
|
|
25
|
+
type UserMessageInput,
|
|
26
|
+
} from "./types/server";
|
|
27
|
+
|
|
28
|
+
const sseEncoder = new TextEncoder();
|
|
29
|
+
const sseDecoder = new TextDecoder();
|
|
30
|
+
|
|
31
|
+
type UserMessageItem = Extract<ThreadItem, { type: "user_message" }>;
|
|
32
|
+
type AssistantMessageItem = Extract<ThreadItem, { type: "assistant_message" }>;
|
|
33
|
+
type ClientToolCallItem = Extract<ThreadItem, { type: "client_tool_call" }>;
|
|
34
|
+
type StructuredInputItem = Extract<ThreadItem, { type: "structured_input" }>;
|
|
35
|
+
type WidgetItem = Extract<ThreadItem, { type: "widget" }>;
|
|
36
|
+
type ProcessRequestInput = string | Uint8Array | ArrayBuffer;
|
|
37
|
+
|
|
38
|
+
export class StreamingResult implements AsyncIterable<Uint8Array> {
|
|
39
|
+
constructor(readonly jsonEvents: AsyncIterable<Uint8Array>) {}
|
|
40
|
+
|
|
41
|
+
[Symbol.asyncIterator](): AsyncIterator<Uint8Array> {
|
|
42
|
+
return this.jsonEvents[Symbol.asyncIterator]();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class NonStreamingResult {
|
|
47
|
+
constructor(readonly json: Uint8Array) {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class StreamCancelledError extends Error {
|
|
51
|
+
constructor(message = "Stream cancelled") {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = "StreamCancelledError";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export abstract class ChatKitServer<TContext = unknown> {
|
|
58
|
+
constructor(
|
|
59
|
+
readonly store: Store<TContext>,
|
|
60
|
+
readonly attachmentStore: AttachmentStore<TContext> | null = null,
|
|
61
|
+
) {}
|
|
62
|
+
|
|
63
|
+
abstract respond(
|
|
64
|
+
thread: ThreadMetadata,
|
|
65
|
+
inputUserMessage: UserMessageItem | null,
|
|
66
|
+
context: TContext,
|
|
67
|
+
): AsyncIterable<ThreadStreamEvent>;
|
|
68
|
+
|
|
69
|
+
async addFeedback(
|
|
70
|
+
_threadId: string,
|
|
71
|
+
_itemIds: string[],
|
|
72
|
+
_kind: FeedbackKind,
|
|
73
|
+
_context: TContext,
|
|
74
|
+
): Promise<void> {}
|
|
75
|
+
|
|
76
|
+
async transcribe(_audio: AudioInput, _context: TContext): Promise<TranscriptionResult> {
|
|
77
|
+
throw new UnsupportedOperationError(
|
|
78
|
+
"transcribe() must be overridden to support the input.transcribe request.",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async *action(
|
|
83
|
+
_thread: ThreadMetadata,
|
|
84
|
+
_action: ThreadCustomActionParams["action"],
|
|
85
|
+
_sender: WidgetItem | null,
|
|
86
|
+
_context: TContext,
|
|
87
|
+
): AsyncIterable<ThreadStreamEvent> {
|
|
88
|
+
throw new UnsupportedOperationError(
|
|
89
|
+
"The action() method must be overridden to react to actions.",
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async syncAction(
|
|
94
|
+
_thread: ThreadMetadata,
|
|
95
|
+
_action: ThreadCustomActionParams["action"],
|
|
96
|
+
_sender: WidgetItem | null,
|
|
97
|
+
_context: TContext,
|
|
98
|
+
): Promise<SyncCustomActionResponse> {
|
|
99
|
+
throw new UnsupportedOperationError(
|
|
100
|
+
"The syncAction() method must be overridden to react to sync actions.",
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
getStreamOptions(_thread: ThreadMetadata, _context: TContext): StreamOptions {
|
|
105
|
+
return { allow_cancel: true };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async handleStreamCancelled(
|
|
109
|
+
thread: ThreadMetadata,
|
|
110
|
+
pendingItems: ThreadItem[],
|
|
111
|
+
context: TContext,
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
const messagesToSave = pendingItems.filter(
|
|
114
|
+
(item): item is AssistantMessageItem =>
|
|
115
|
+
item.type === "assistant_message" && item.content.some((part) => part.text.trim().length > 0),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
for (const message of messagesToSave) {
|
|
119
|
+
await this.store.saveItem(thread.id, message, context);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await this.store.addThreadItem(
|
|
123
|
+
thread.id,
|
|
124
|
+
{
|
|
125
|
+
id: this.store.generateItemId("sdk_hidden_context", thread, context),
|
|
126
|
+
thread_id: thread.id,
|
|
127
|
+
created_at: new Date().toISOString(),
|
|
128
|
+
type: "sdk_hidden_context",
|
|
129
|
+
content: "Stream cancelled by client.",
|
|
130
|
+
},
|
|
131
|
+
context,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async process(
|
|
136
|
+
request: ProcessRequestInput,
|
|
137
|
+
context: TContext,
|
|
138
|
+
): Promise<StreamingResult | NonStreamingResult> {
|
|
139
|
+
const parsed: ChatKitRequest = ChatKitRequestSchema.parse(decodeJsonBytes(request));
|
|
140
|
+
|
|
141
|
+
if (isStreamingRequest(parsed)) {
|
|
142
|
+
return new StreamingResult(this.processStreaming(parsed, context));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return new NonStreamingResult(await this.processNonStreaming(parsed, context));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
protected async processNonStreaming(
|
|
149
|
+
request: NonStreamingRequest,
|
|
150
|
+
context: TContext,
|
|
151
|
+
): Promise<Uint8Array> {
|
|
152
|
+
switch (request.type) {
|
|
153
|
+
case "threads.get_by_id": {
|
|
154
|
+
return this.serialize(this.toThreadResponse(await this.loadFullThread(request.params.thread_id, context)));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
case "threads.list": {
|
|
158
|
+
const { limit, after, order } = request.params;
|
|
159
|
+
const page = await this.store.loadThreads(
|
|
160
|
+
limit ?? DEFAULT_PAGE_SIZE,
|
|
161
|
+
after ?? null,
|
|
162
|
+
order,
|
|
163
|
+
context,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
return this.serialize({
|
|
167
|
+
...page,
|
|
168
|
+
data: page.data.map((thread) => this.toThreadResponse(thread)),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
case "items.list": {
|
|
173
|
+
const { thread_id, limit, after, order } = request.params;
|
|
174
|
+
const page = await this.store.loadThreadItems(
|
|
175
|
+
thread_id,
|
|
176
|
+
after ?? null,
|
|
177
|
+
limit ?? DEFAULT_PAGE_SIZE,
|
|
178
|
+
order,
|
|
179
|
+
context,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
return this.serialize({
|
|
183
|
+
...page,
|
|
184
|
+
data: page.data.filter((item) => !this.isHiddenItem(item)),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
case "items.feedback": {
|
|
189
|
+
const { thread_id, item_ids, kind } = request.params;
|
|
190
|
+
await this.addFeedback(thread_id, item_ids, kind, context);
|
|
191
|
+
return this.serialize({});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
case "attachments.create": {
|
|
195
|
+
const attachment = await this.getAttachmentStore().createAttachment(request.params, context);
|
|
196
|
+
await this.store.saveAttachment(attachment, context);
|
|
197
|
+
return this.serialize(attachment);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
case "attachments.delete": {
|
|
201
|
+
const { attachment_id } = request.params;
|
|
202
|
+
await this.store.loadAttachment(attachment_id, context);
|
|
203
|
+
await this.getAttachmentStore().deleteAttachment(attachment_id, context);
|
|
204
|
+
await this.store.deleteAttachment(attachment_id, context);
|
|
205
|
+
return this.serialize({});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
case "threads.update": {
|
|
209
|
+
const thread = await this.store.loadThread(request.params.thread_id, context);
|
|
210
|
+
const updatedThread = { ...thread, title: request.params.title };
|
|
211
|
+
await this.store.saveThread(updatedThread, context);
|
|
212
|
+
return this.serialize(this.toThreadResponse(updatedThread));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
case "threads.delete": {
|
|
216
|
+
await this.store.deleteThread(request.params.thread_id, context);
|
|
217
|
+
return this.serialize({});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
case "input.transcribe": {
|
|
221
|
+
const { audio_base64, mime_type } = request.params;
|
|
222
|
+
const audio: AudioInput = {
|
|
223
|
+
data: Uint8Array.from(Buffer.from(audio_base64, "base64")),
|
|
224
|
+
mime_type,
|
|
225
|
+
get mediaType() {
|
|
226
|
+
return mime_type.split(";")[0]!.trim();
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
const result = TranscriptionResultSchema.parse(await this.transcribe(audio, context));
|
|
230
|
+
return this.serialize(result);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
case "threads.sync_custom_action": {
|
|
234
|
+
return this.serialize(await this.processSyncCustomAction(request, context));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
default: {
|
|
238
|
+
const _exhaustive: never = request;
|
|
239
|
+
return this.serialize(_exhaustive);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
protected async *processStreaming(
|
|
245
|
+
request: StreamingRequest,
|
|
246
|
+
context: TContext,
|
|
247
|
+
): AsyncIterable<Uint8Array> {
|
|
248
|
+
for await (const event of this.processStreamingImpl(request, context)) {
|
|
249
|
+
const json = sseDecoder.decode(this.serialize(event));
|
|
250
|
+
yield sseEncoder.encode(`data: ${json}\n\n`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
protected async *processStreamingImpl(
|
|
255
|
+
request: StreamingRequest,
|
|
256
|
+
context: TContext,
|
|
257
|
+
): AsyncIterable<ThreadStreamEvent> {
|
|
258
|
+
switch (request.type) {
|
|
259
|
+
case "threads.create": {
|
|
260
|
+
const thread: Thread = {
|
|
261
|
+
id: this.store.generateThreadId(context),
|
|
262
|
+
created_at: new Date().toISOString(),
|
|
263
|
+
status: { type: "active" },
|
|
264
|
+
metadata: {},
|
|
265
|
+
items: { data: [], has_more: false, after: null },
|
|
266
|
+
};
|
|
267
|
+
await this.store.saveThread(ThreadMetadataSchema.parse(thread), context);
|
|
268
|
+
const userMessage = await this.buildUserMessageItem(request.params.input, thread, context);
|
|
269
|
+
await this.persistUserMessageItem(thread, userMessage, context);
|
|
270
|
+
|
|
271
|
+
yield { type: "thread.created", thread: this.toThreadResponse(thread) };
|
|
272
|
+
yield { type: "thread.item.done", item: userMessage };
|
|
273
|
+
yield* this.processEvents(thread, context, () => this.respond(thread, userMessage, context));
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
case "threads.add_user_message": {
|
|
278
|
+
const thread = await this.store.loadThread(request.params.thread_id, context);
|
|
279
|
+
const userMessage = await this.buildUserMessageItem(request.params.input, thread, context);
|
|
280
|
+
yield* this.processNewThreadItemRespond(thread, userMessage, context);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
default:
|
|
285
|
+
yield* this.processStreamingContinuation(request, context);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
protected async buildUserMessageItem(
|
|
290
|
+
input: UserMessageInput,
|
|
291
|
+
thread: ThreadMetadata,
|
|
292
|
+
context: TContext,
|
|
293
|
+
): Promise<UserMessageItem> {
|
|
294
|
+
return {
|
|
295
|
+
id: this.store.generateItemId("message", thread, context),
|
|
296
|
+
type: "user_message",
|
|
297
|
+
thread_id: thread.id,
|
|
298
|
+
created_at: new Date().toISOString(),
|
|
299
|
+
content: input.content,
|
|
300
|
+
attachments: await Promise.all(
|
|
301
|
+
input.attachments.map(async (attachmentId) => ({
|
|
302
|
+
...(await this.store.loadAttachment(attachmentId, context)),
|
|
303
|
+
thread_id: thread.id,
|
|
304
|
+
})),
|
|
305
|
+
),
|
|
306
|
+
quoted_text: input.quoted_text,
|
|
307
|
+
inference_options: input.inference_options,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
protected async *processNewThreadItemRespond(
|
|
312
|
+
thread: ThreadMetadata,
|
|
313
|
+
item: UserMessageItem,
|
|
314
|
+
context: TContext,
|
|
315
|
+
): AsyncIterable<ThreadStreamEvent> {
|
|
316
|
+
await this.persistUserMessageItem(thread, item, context);
|
|
317
|
+
yield { type: "thread.item.done", item };
|
|
318
|
+
yield* this.processEvents(thread, context, () => this.respond(thread, item, context));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
protected async persistUserMessageItem(
|
|
322
|
+
thread: ThreadMetadata,
|
|
323
|
+
item: UserMessageItem,
|
|
324
|
+
context: TContext,
|
|
325
|
+
): Promise<void> {
|
|
326
|
+
for (const attachment of item.attachments) {
|
|
327
|
+
await this.store.saveAttachment(attachment, context);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
await this.store.addThreadItem(thread.id, item, context);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
protected async *processStreamingContinuation(
|
|
334
|
+
request: Exclude<StreamingRequest, { type: "threads.create" | "threads.add_user_message" }>,
|
|
335
|
+
context: TContext,
|
|
336
|
+
): AsyncIterable<ThreadStreamEvent> {
|
|
337
|
+
switch (request.type) {
|
|
338
|
+
case "threads.add_client_tool_output": {
|
|
339
|
+
const thread = await this.store.loadThread(request.params.thread_id, context);
|
|
340
|
+
let toolCall: ClientToolCallItem | null = null;
|
|
341
|
+
|
|
342
|
+
for await (const item of this.loadThreadItemsDescending(thread, context)) {
|
|
343
|
+
if (item.type === "client_tool_call" && item.status === "pending") {
|
|
344
|
+
toolCall = item;
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (!toolCall) {
|
|
350
|
+
throw new Error(`Last thread item in ${thread.id} was not a ClientToolCallItem`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
await this.store.saveItem(
|
|
354
|
+
thread.id,
|
|
355
|
+
{ ...toolCall, status: "completed", output: request.params.result },
|
|
356
|
+
context,
|
|
357
|
+
);
|
|
358
|
+
await this.cleanupPendingClientToolCall(thread, context);
|
|
359
|
+
yield* this.processEvents(thread, context, () => this.respond(thread, null, context));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
case "threads.add_structured_input": {
|
|
364
|
+
const thread = await this.store.loadThread(request.params.thread_id, context);
|
|
365
|
+
const item = await this.store.loadItem(thread.id, request.params.item_id, context);
|
|
366
|
+
|
|
367
|
+
if (item.type !== "structured_input") {
|
|
368
|
+
throw new Error(`Item ${request.params.item_id} is not a StructuredInputItem`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const updatedItem = this.applyStructuredInputSubmission(item, request.params.input);
|
|
372
|
+
const server = this;
|
|
373
|
+
yield* this.processEvents(thread, context, async function* (): AsyncIterable<ThreadStreamEvent> {
|
|
374
|
+
yield { type: "thread.item.replaced", item: updatedItem };
|
|
375
|
+
yield* server.respond(thread, null, context);
|
|
376
|
+
});
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
case "threads.retry_after_item": {
|
|
381
|
+
const thread = await this.store.loadThread(request.params.thread_id, context);
|
|
382
|
+
const userMessage = await this.removeItemsAfterUserMessage(
|
|
383
|
+
thread,
|
|
384
|
+
request.params.item_id,
|
|
385
|
+
context,
|
|
386
|
+
);
|
|
387
|
+
yield* this.processEvents(thread, context, () => this.respond(thread, userMessage, context));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
case "threads.custom_action": {
|
|
392
|
+
const thread = await this.store.loadThread(request.params.thread_id, context);
|
|
393
|
+
const sender =
|
|
394
|
+
request.params.item_id != null
|
|
395
|
+
? await this.store.loadItem(thread.id, request.params.item_id, context)
|
|
396
|
+
: null;
|
|
397
|
+
|
|
398
|
+
if (sender && sender.type !== "widget") {
|
|
399
|
+
yield { type: "error", code: "stream.error", allow_retry: false };
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
yield* this.processEvents(thread, context, () =>
|
|
404
|
+
this.action(thread, request.params.action, sender, context),
|
|
405
|
+
);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
default: {
|
|
410
|
+
const _exhaustive: never = request;
|
|
411
|
+
return _exhaustive;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
protected async *loadThreadItemsDescending(
|
|
417
|
+
thread: ThreadMetadata,
|
|
418
|
+
context: TContext,
|
|
419
|
+
): AsyncIterable<ThreadItem> {
|
|
420
|
+
let after: string | null = null;
|
|
421
|
+
|
|
422
|
+
while (true) {
|
|
423
|
+
const page = await this.store.loadThreadItems(
|
|
424
|
+
thread.id,
|
|
425
|
+
after,
|
|
426
|
+
DEFAULT_PAGE_SIZE,
|
|
427
|
+
"desc",
|
|
428
|
+
context,
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
yield* page.data;
|
|
432
|
+
|
|
433
|
+
if (!page.has_more) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
after = page.after ?? null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
protected async cleanupPendingClientToolCall(
|
|
442
|
+
thread: ThreadMetadata,
|
|
443
|
+
context: TContext,
|
|
444
|
+
): Promise<void> {
|
|
445
|
+
const pendingItemIds: string[] = [];
|
|
446
|
+
|
|
447
|
+
for await (const item of this.loadThreadItemsDescending(thread, context)) {
|
|
448
|
+
if (item.type === "client_tool_call" && item.status === "pending") {
|
|
449
|
+
pendingItemIds.push(item.id);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
for (const itemId of pendingItemIds) {
|
|
454
|
+
await this.store.deleteThreadItem(thread.id, itemId, context);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
protected applyStructuredInputSubmission(
|
|
459
|
+
item: StructuredInputItem,
|
|
460
|
+
submission: StructuredInputSubmission,
|
|
461
|
+
): StructuredInputItem {
|
|
462
|
+
if (item.status !== "pending") {
|
|
463
|
+
throw new Error(`Structured input item ${item.id} is not pending`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
...item,
|
|
468
|
+
status: submission.status,
|
|
469
|
+
inputs: item.inputs.map((question) => {
|
|
470
|
+
const answer = submission.answers[question.id];
|
|
471
|
+
|
|
472
|
+
if (
|
|
473
|
+
submission.status === "skipped" ||
|
|
474
|
+
!answer ||
|
|
475
|
+
answer.skipped ||
|
|
476
|
+
(answer.values?.length ?? 0) === 0
|
|
477
|
+
) {
|
|
478
|
+
return { ...question, answer: { values: [], skipped: true } };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const values =
|
|
482
|
+
question.type === "multiple_choice" && !question.multiple
|
|
483
|
+
? answer.values!.slice(0, 1)
|
|
484
|
+
: answer.values!;
|
|
485
|
+
|
|
486
|
+
return { ...question, answer: { values, skipped: false } };
|
|
487
|
+
}),
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
protected async removeItemsAfterUserMessage(
|
|
492
|
+
thread: ThreadMetadata,
|
|
493
|
+
itemId: string,
|
|
494
|
+
context: TContext,
|
|
495
|
+
): Promise<UserMessageItem> {
|
|
496
|
+
const itemsToRemove: ThreadItem[] = [];
|
|
497
|
+
let after: string | null = null;
|
|
498
|
+
|
|
499
|
+
while (true) {
|
|
500
|
+
const page = await this.store.loadThreadItems(
|
|
501
|
+
thread.id,
|
|
502
|
+
after,
|
|
503
|
+
DEFAULT_PAGE_SIZE,
|
|
504
|
+
"desc",
|
|
505
|
+
context,
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
for (const item of page.data) {
|
|
509
|
+
if (item.id === itemId) {
|
|
510
|
+
if (item.type !== "user_message") {
|
|
511
|
+
throw new Error(`Item ${itemId} is not a user message`);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
for (const itemToRemove of itemsToRemove) {
|
|
515
|
+
await this.store.deleteThreadItem(thread.id, itemToRemove.id, context);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return item;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
itemsToRemove.push(item);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (!page.has_more) {
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
after = page.after ?? null;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
throw new Error(`Item ${itemId} was not found`);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
protected async *processEvents(
|
|
535
|
+
thread: ThreadMetadata,
|
|
536
|
+
context: TContext,
|
|
537
|
+
stream: () => AsyncIterable<ThreadStreamEvent>,
|
|
538
|
+
): AsyncIterable<ThreadStreamEvent> {
|
|
539
|
+
yield { type: "stream_options", stream_options: this.getStreamOptions(thread, context) };
|
|
540
|
+
let lastThread = structuredClone(thread);
|
|
541
|
+
const pendingItems = new Map<string, ThreadItem>();
|
|
542
|
+
const updatedPendingItemIds = new Set<string>();
|
|
543
|
+
let completedNormally = false;
|
|
544
|
+
let cancellationHandled = false;
|
|
545
|
+
const saveThreadIfChanged = async (): Promise<boolean> => {
|
|
546
|
+
if (!this.hasThreadChanged(thread, lastThread)) {
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
lastThread = structuredClone(thread);
|
|
551
|
+
await this.store.saveThread(thread, context);
|
|
552
|
+
return true;
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
try {
|
|
556
|
+
for await (const rawEvent of stream()) {
|
|
557
|
+
const event = ThreadStreamEventSchema.parse(rawEvent);
|
|
558
|
+
let suppressClientEvent = false;
|
|
559
|
+
|
|
560
|
+
if (event.type === "thread.item.added") {
|
|
561
|
+
pendingItems.set(event.item.id, structuredClone(event.item));
|
|
562
|
+
suppressClientEvent = this.isHiddenItem(event.item);
|
|
563
|
+
} else if (event.type === "thread.item.done") {
|
|
564
|
+
const pendingItem = pendingItems.get(event.item.id);
|
|
565
|
+
const itemToSave = this.mergePendingUpdatesIntoDoneItem(
|
|
566
|
+
event.item,
|
|
567
|
+
pendingItem,
|
|
568
|
+
updatedPendingItemIds.has(event.item.id),
|
|
569
|
+
);
|
|
570
|
+
if (pendingItem) {
|
|
571
|
+
await this.store.addThreadItem(thread.id, itemToSave, context);
|
|
572
|
+
} else {
|
|
573
|
+
await this.store.saveItem(thread.id, itemToSave, context);
|
|
574
|
+
}
|
|
575
|
+
pendingItems.delete(event.item.id);
|
|
576
|
+
updatedPendingItemIds.delete(event.item.id);
|
|
577
|
+
suppressClientEvent = this.isHiddenItem(itemToSave);
|
|
578
|
+
} else if (event.type === "thread.item.removed") {
|
|
579
|
+
const pendingItem = pendingItems.get(event.item_id);
|
|
580
|
+
suppressClientEvent =
|
|
581
|
+
(pendingItem != null && this.isHiddenItem(pendingItem)) ||
|
|
582
|
+
(pendingItem == null && (await this.isStoredHiddenItem(thread.id, event.item_id, context)));
|
|
583
|
+
|
|
584
|
+
if (pendingItem == null) {
|
|
585
|
+
await this.store.deleteThreadItem(thread.id, event.item_id, context);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
pendingItems.delete(event.item_id);
|
|
589
|
+
updatedPendingItemIds.delete(event.item_id);
|
|
590
|
+
} else if (event.type === "thread.item.replaced") {
|
|
591
|
+
await this.store.saveItem(thread.id, event.item, context);
|
|
592
|
+
pendingItems.delete(event.item.id);
|
|
593
|
+
updatedPendingItemIds.delete(event.item.id);
|
|
594
|
+
suppressClientEvent = this.isHiddenItem(event.item);
|
|
595
|
+
} else if (event.type === "thread.item.updated") {
|
|
596
|
+
suppressClientEvent = await this.isKnownHiddenItem(
|
|
597
|
+
thread.id,
|
|
598
|
+
event.item_id,
|
|
599
|
+
pendingItems,
|
|
600
|
+
context,
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
if (this.updatePendingItems(pendingItems, event.item_id, event.update)) {
|
|
604
|
+
updatedPendingItemIds.add(event.item_id);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (!suppressClientEvent) {
|
|
609
|
+
yield event;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (await saveThreadIfChanged()) {
|
|
613
|
+
yield { type: "thread.updated", thread: this.toThreadResponse(thread) };
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
completedNormally = true;
|
|
617
|
+
} catch (error) {
|
|
618
|
+
if (error instanceof StreamCancelledError) {
|
|
619
|
+
cancellationHandled = true;
|
|
620
|
+
await saveThreadIfChanged();
|
|
621
|
+
await this.handleStreamCancelled(thread, [...pendingItems.values()], context);
|
|
622
|
+
throw error;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
completedNormally = true;
|
|
626
|
+
yield { type: "error", code: "stream.error", allow_retry: true };
|
|
627
|
+
} finally {
|
|
628
|
+
if (!completedNormally && !cancellationHandled) {
|
|
629
|
+
await saveThreadIfChanged();
|
|
630
|
+
await this.handleStreamCancelled(thread, [...pendingItems.values()], context);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (await saveThreadIfChanged()) {
|
|
635
|
+
yield { type: "thread.updated", thread: this.toThreadResponse(thread) };
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
protected updatePendingItems(
|
|
640
|
+
pendingItems: Map<string, ThreadItem>,
|
|
641
|
+
itemId: string,
|
|
642
|
+
update: ThreadItemUpdate,
|
|
643
|
+
): boolean {
|
|
644
|
+
const item = pendingItems.get(itemId);
|
|
645
|
+
if (!item) {
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (
|
|
650
|
+
item.type === "assistant_message" &&
|
|
651
|
+
(update.type === "assistant_message.content_part.added" ||
|
|
652
|
+
update.type === "assistant_message.content_part.text_delta" ||
|
|
653
|
+
update.type === "assistant_message.content_part.annotation_added" ||
|
|
654
|
+
update.type === "assistant_message.content_part.done")
|
|
655
|
+
) {
|
|
656
|
+
pendingItems.set(itemId, this.applyAssistantMessageUpdate(item, update));
|
|
657
|
+
return true;
|
|
658
|
+
} else if (item.type === "workflow" && update.type === "workflow.task.added") {
|
|
659
|
+
item.workflow.tasks.splice(update.task_index, 0, update.task);
|
|
660
|
+
pendingItems.set(itemId, item);
|
|
661
|
+
return true;
|
|
662
|
+
} else if (item.type === "workflow" && update.type === "workflow.task.updated") {
|
|
663
|
+
item.workflow.tasks[update.task_index] = update.task;
|
|
664
|
+
pendingItems.set(itemId, item);
|
|
665
|
+
return true;
|
|
666
|
+
} else if (item.type === "generated_image" && update.type === "generated_image.updated") {
|
|
667
|
+
pendingItems.set(itemId, { ...item, image: update.image });
|
|
668
|
+
return true;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
protected mergePendingUpdatesIntoDoneItem(
|
|
675
|
+
doneItem: ThreadItem,
|
|
676
|
+
pendingItem: ThreadItem | undefined,
|
|
677
|
+
hasPendingUpdates: boolean,
|
|
678
|
+
): ThreadItem {
|
|
679
|
+
if (!hasPendingUpdates || !pendingItem) {
|
|
680
|
+
return doneItem;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (doneItem.type === "assistant_message" && pendingItem.type === "assistant_message") {
|
|
684
|
+
return { ...doneItem, content: this.mergeAssistantMessageContent(doneItem, pendingItem) };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (doneItem.type === "workflow" && pendingItem.type === "workflow") {
|
|
688
|
+
return {
|
|
689
|
+
...doneItem,
|
|
690
|
+
workflow: {
|
|
691
|
+
...doneItem.workflow,
|
|
692
|
+
tasks: pendingItem.workflow.tasks,
|
|
693
|
+
},
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (doneItem.type === "generated_image" && pendingItem.type === "generated_image") {
|
|
698
|
+
return doneItem.image ? doneItem : { ...doneItem, image: pendingItem.image };
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return doneItem;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
private mergeAssistantMessageContent(
|
|
705
|
+
doneItem: AssistantMessageItem,
|
|
706
|
+
pendingItem: AssistantMessageItem,
|
|
707
|
+
): AssistantMessageItem["content"] {
|
|
708
|
+
const content: AssistantMessageItem["content"] = [];
|
|
709
|
+
const contentLength = Math.max(doneItem.content.length, pendingItem.content.length);
|
|
710
|
+
|
|
711
|
+
for (let index = 0; index < contentLength; index++) {
|
|
712
|
+
const donePart = doneItem.content[index];
|
|
713
|
+
const pendingPart = pendingItem.content[index];
|
|
714
|
+
|
|
715
|
+
if (!donePart) {
|
|
716
|
+
if (pendingPart) {
|
|
717
|
+
content.push(pendingPart);
|
|
718
|
+
}
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (!pendingPart) {
|
|
723
|
+
content.push(donePart);
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
content.push({
|
|
728
|
+
...donePart,
|
|
729
|
+
text: donePart.text.length > 0 ? donePart.text : pendingPart.text,
|
|
730
|
+
annotations:
|
|
731
|
+
donePart.annotations.length > 0
|
|
732
|
+
? donePart.annotations
|
|
733
|
+
: pendingPart.annotations,
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
return content;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
protected applyAssistantMessageUpdate(
|
|
741
|
+
item: AssistantMessageItem,
|
|
742
|
+
update: Extract<ThreadItemUpdate, { type: `assistant_message.${string}` }>,
|
|
743
|
+
): AssistantMessageItem {
|
|
744
|
+
const content = item.content.map((part) => ({ ...part, annotations: [...part.annotations] }));
|
|
745
|
+
|
|
746
|
+
while (content.length <= update.content_index) {
|
|
747
|
+
content.push({ type: "output_text", text: "", annotations: [] });
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const current = content[update.content_index];
|
|
751
|
+
if (!current) {
|
|
752
|
+
return { ...item, content };
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (update.type === "assistant_message.content_part.added") {
|
|
756
|
+
content[update.content_index] = update.content;
|
|
757
|
+
} else if (update.type === "assistant_message.content_part.text_delta") {
|
|
758
|
+
content[update.content_index] = {
|
|
759
|
+
...current,
|
|
760
|
+
text: current.text + update.delta,
|
|
761
|
+
};
|
|
762
|
+
} else if (update.type === "assistant_message.content_part.annotation_added") {
|
|
763
|
+
const annotations = [...current.annotations];
|
|
764
|
+
annotations.splice(update.annotation_index, 0, update.annotation);
|
|
765
|
+
content[update.content_index] = { ...current, annotations };
|
|
766
|
+
} else if (update.type === "assistant_message.content_part.done") {
|
|
767
|
+
content[update.content_index] =
|
|
768
|
+
update.content.annotations.length === 0 && current.annotations.length > 0
|
|
769
|
+
? { ...update.content, annotations: current.annotations }
|
|
770
|
+
: update.content;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
return { ...item, content };
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
private hasThreadChanged(thread: ThreadMetadata, lastThread: ThreadMetadata): boolean {
|
|
777
|
+
return JSON.stringify(thread) !== JSON.stringify(lastThread);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
protected serialize(value: unknown): Uint8Array {
|
|
781
|
+
return encodeJsonBytes(value);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
private getAttachmentStore(): AttachmentStore<TContext> {
|
|
785
|
+
if (!this.attachmentStore) {
|
|
786
|
+
throw new UnsupportedOperationError(
|
|
787
|
+
"AttachmentStore is not configured. Provide an AttachmentStore to ChatKitServer to handle file operations.",
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return this.attachmentStore;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
protected async loadFullThread(threadId: string, context: TContext): Promise<Thread> {
|
|
795
|
+
const thread = await this.store.loadThread(threadId, context);
|
|
796
|
+
const items = await this.store.loadThreadItems(
|
|
797
|
+
threadId,
|
|
798
|
+
null,
|
|
799
|
+
DEFAULT_PAGE_SIZE,
|
|
800
|
+
"asc",
|
|
801
|
+
context,
|
|
802
|
+
);
|
|
803
|
+
|
|
804
|
+
return { ...thread, items };
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
protected toThreadResponse(thread: ThreadMetadata | Thread): Thread {
|
|
808
|
+
const items: Page<ThreadItem> =
|
|
809
|
+
"items" in thread
|
|
810
|
+
? {
|
|
811
|
+
...thread.items,
|
|
812
|
+
data: thread.items.data.filter((item) => !this.isHiddenItem(item)),
|
|
813
|
+
}
|
|
814
|
+
: { data: [], has_more: false, after: null };
|
|
815
|
+
const { metadata: _metadata, ...threadResponse } = thread;
|
|
816
|
+
|
|
817
|
+
return { ...threadResponse, items } as Thread;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
protected isHiddenItem(item: ThreadItem): boolean {
|
|
821
|
+
return item.type === "hidden_context_item" || item.type === "sdk_hidden_context";
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
private async isKnownHiddenItem(
|
|
825
|
+
threadId: string,
|
|
826
|
+
itemId: string,
|
|
827
|
+
pendingItems: Map<string, ThreadItem>,
|
|
828
|
+
context: TContext,
|
|
829
|
+
): Promise<boolean> {
|
|
830
|
+
const pendingItem = pendingItems.get(itemId);
|
|
831
|
+
if (pendingItem) {
|
|
832
|
+
return this.isHiddenItem(pendingItem);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
return this.isStoredHiddenItem(threadId, itemId, context);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
private async isStoredHiddenItem(
|
|
839
|
+
threadId: string,
|
|
840
|
+
itemId: string,
|
|
841
|
+
context: TContext,
|
|
842
|
+
): Promise<boolean> {
|
|
843
|
+
try {
|
|
844
|
+
return this.isHiddenItem(await this.store.loadItem(threadId, itemId, context));
|
|
845
|
+
} catch (error) {
|
|
846
|
+
if (error instanceof NotFoundError) {
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
throw error;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
protected async processSyncCustomAction(
|
|
855
|
+
request: Extract<NonStreamingRequest, { type: "threads.sync_custom_action" }>,
|
|
856
|
+
context: TContext,
|
|
857
|
+
): Promise<SyncCustomActionResponse> {
|
|
858
|
+
const { thread_id, item_id } = request.params;
|
|
859
|
+
const thread = await this.store.loadThread(thread_id, context);
|
|
860
|
+
let sender: WidgetItem | null = null;
|
|
861
|
+
|
|
862
|
+
if (item_id != null) {
|
|
863
|
+
const item = await this.store.loadItem(thread_id, item_id, context);
|
|
864
|
+
if (item.type !== "widget") {
|
|
865
|
+
throw new ValidationError("Sync custom actions can only be sent by widget items.");
|
|
866
|
+
}
|
|
867
|
+
sender = item;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
return SyncCustomActionResponseSchema.parse(
|
|
871
|
+
await this.syncAction(thread, request.params.action, sender, context),
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
}
|