@cogitator-ai/openai-compat 21.2.13 → 21.3.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 +88 -10
- package/dist/client/openai-adapter.d.ts +61 -19
- package/dist/client/openai-adapter.d.ts.map +1 -1
- package/dist/client/openai-adapter.js +270 -77
- package/dist/client/openai-adapter.js.map +1 -1
- package/dist/client/pagination.d.ts +26 -0
- package/dist/client/pagination.d.ts.map +1 -0
- package/dist/client/pagination.js +34 -0
- package/dist/client/pagination.js.map +1 -0
- package/dist/client/run-steps.d.ts +66 -0
- package/dist/client/run-steps.d.ts.map +1 -0
- package/dist/client/run-steps.js +210 -0
- package/dist/client/run-steps.js.map +1 -0
- package/dist/client/storage.d.ts +44 -4
- package/dist/client/storage.d.ts.map +1 -1
- package/dist/client/storage.js +111 -7
- package/dist/client/storage.js.map +1 -1
- package/dist/client/thread-manager.d.ts +36 -3
- package/dist/client/thread-manager.d.ts.map +1 -1
- package/dist/client/thread-manager.js +49 -34
- package/dist/client/thread-manager.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/server/api-server.d.ts +4 -0
- package/dist/server/api-server.d.ts.map +1 -1
- package/dist/server/api-server.js +1 -0
- package/dist/server/api-server.js.map +1 -1
- package/dist/server/middleware/auth.d.ts +13 -1
- package/dist/server/middleware/auth.d.ts.map +1 -1
- package/dist/server/middleware/auth.js +12 -5
- package/dist/server/middleware/auth.js.map +1 -1
- package/dist/server/routes/runs.d.ts.map +1 -1
- package/dist/server/routes/runs.js +45 -11
- package/dist/server/routes/runs.js.map +1 -1
- package/dist/server/routes/shared.d.ts +1 -15
- package/dist/server/routes/shared.d.ts.map +1 -1
- package/dist/server/routes/shared.js +1 -23
- package/dist/server/routes/shared.js.map +1 -1
- package/dist/server/routes/threads.d.ts.map +1 -1
- package/dist/server/routes/threads.js +14 -26
- package/dist/server/routes/threads.js.map +1 -1
- package/dist/types/openai-types.d.ts +23 -2
- package/dist/types/openai-types.d.ts.map +1 -1
- package/package.json +6 -6
|
@@ -8,8 +8,9 @@ import { CogitatorError, } from '@cogitator-ai/types';
|
|
|
8
8
|
import { EventEmitter } from 'events';
|
|
9
9
|
import { nanoid } from 'nanoid';
|
|
10
10
|
import { z } from 'zod';
|
|
11
|
-
import { ThreadManager } from './thread-manager.js';
|
|
11
|
+
import { ThreadManager, assistantMessage, } from './thread-manager.js';
|
|
12
12
|
import { InvalidRequestError } from './errors.js';
|
|
13
|
+
import { RunStepRecorder, isSettledStep, settledEvent } from './run-steps.js';
|
|
13
14
|
export const COGITATOR_MODEL_ID = 'cogitator';
|
|
14
15
|
const ACTIVE_STATUSES = [
|
|
15
16
|
'queued',
|
|
@@ -20,6 +21,15 @@ const ACTIVE_STATUSES = [
|
|
|
20
21
|
const RUN_TTL_SECONDS = 600;
|
|
21
22
|
const CHARS_PER_TOKEN = 4;
|
|
22
23
|
const TRANSCRIPT_HEADER = 'Conversation so far:\n\n';
|
|
24
|
+
function newRunMessage() {
|
|
25
|
+
return {
|
|
26
|
+
id: `msg_${nanoid()}`,
|
|
27
|
+
createdAt: nowSeconds(),
|
|
28
|
+
announced: false,
|
|
29
|
+
written: false,
|
|
30
|
+
text: '',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
23
33
|
class RunTerminatedError extends Error {
|
|
24
34
|
status;
|
|
25
35
|
constructor(status) {
|
|
@@ -40,6 +50,14 @@ function nowSeconds() {
|
|
|
40
50
|
function isActive(status) {
|
|
41
51
|
return ACTIVE_STATUSES.includes(status);
|
|
42
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Whether `requester` may use a thread or run of `owner`. Everything is reachable without a
|
|
55
|
+
* requester (no API keys configured, or a direct adapter call), and a thread stored before key
|
|
56
|
+
* scoping has no owner, so every key reaches it.
|
|
57
|
+
*/
|
|
58
|
+
function canAccess(owner, requester) {
|
|
59
|
+
return owner === undefined || requester === undefined || owner === requester;
|
|
60
|
+
}
|
|
43
61
|
/** The agent response format and extra instructions for an OpenAI `response_format`. */
|
|
44
62
|
export function toAgentResponseFormat(format) {
|
|
45
63
|
if (!format || format === 'auto' || format.type === 'text')
|
|
@@ -201,45 +219,70 @@ export class OpenAIAdapter {
|
|
|
201
219
|
response_format: stored.response_format,
|
|
202
220
|
};
|
|
203
221
|
}
|
|
204
|
-
|
|
205
|
-
|
|
222
|
+
/**
|
|
223
|
+
* Create a thread. With an `owner` (the API key that created it, see the server's `apiKeys`),
|
|
224
|
+
* only that owner reaches the thread, its messages and its runs.
|
|
225
|
+
*
|
|
226
|
+
* The thread, message and run methods below take the `owner` of the caller last: a thread or
|
|
227
|
+
* run of another owner is treated as missing. Without one, nothing is scoped.
|
|
228
|
+
*/
|
|
229
|
+
async createThread(metadata, owner) {
|
|
230
|
+
return this.threadManager.createThread(metadata, owner);
|
|
206
231
|
}
|
|
207
|
-
async getThread(id) {
|
|
208
|
-
return this.
|
|
232
|
+
async getThread(id, owner) {
|
|
233
|
+
return (await this.accessibleThread(id, owner))?.thread;
|
|
209
234
|
}
|
|
210
|
-
async updateThread(id, updates) {
|
|
235
|
+
async updateThread(id, updates, owner) {
|
|
236
|
+
if (!(await this.accessibleThread(id, owner)))
|
|
237
|
+
return undefined;
|
|
211
238
|
return this.threadManager.updateThread(id, updates);
|
|
212
239
|
}
|
|
213
|
-
async deleteThread(id) {
|
|
240
|
+
async deleteThread(id, owner) {
|
|
241
|
+
if (!(await this.accessibleThread(id, owner)))
|
|
242
|
+
return false;
|
|
214
243
|
return this.threadManager.deleteThread(id);
|
|
215
244
|
}
|
|
216
|
-
async addMessage(threadId, params) {
|
|
245
|
+
async addMessage(threadId, params, owner) {
|
|
246
|
+
if (!(await this.accessibleThread(threadId, owner)))
|
|
247
|
+
return undefined;
|
|
217
248
|
return this.threadManager.addMessage(threadId, params);
|
|
218
249
|
}
|
|
219
|
-
async getMessage(threadId, messageId) {
|
|
220
|
-
|
|
250
|
+
async getMessage(threadId, messageId, owner) {
|
|
251
|
+
const stored = await this.accessibleThread(threadId, owner);
|
|
252
|
+
return stored?.messages.find((message) => message.id === messageId);
|
|
221
253
|
}
|
|
222
|
-
async listMessages(threadId, options) {
|
|
254
|
+
async listMessages(threadId, options, owner) {
|
|
255
|
+
if (!(await this.accessibleThread(threadId, owner)))
|
|
256
|
+
return [];
|
|
223
257
|
return this.threadManager.listMessages(threadId, options);
|
|
224
258
|
}
|
|
259
|
+
async accessibleThread(id, owner) {
|
|
260
|
+
const stored = await this.threadManager.getStoredThread(id);
|
|
261
|
+
return stored && canAccess(stored.owner, owner) ? stored : undefined;
|
|
262
|
+
}
|
|
263
|
+
accessibleRun(threadId, runId, owner) {
|
|
264
|
+
const state = this.runs.get(runId);
|
|
265
|
+
return state?.run.thread_id === threadId && canAccess(state.owner, owner) ? state : undefined;
|
|
266
|
+
}
|
|
225
267
|
/**
|
|
226
268
|
* Create a run and start executing it in the background.
|
|
227
269
|
*
|
|
228
|
-
* Rejects when the assistant or thread does not exist
|
|
229
|
-
* already has an active run.
|
|
270
|
+
* Rejects when the assistant or thread does not exist (or the thread belongs to another
|
|
271
|
+
* `owner`), or when the thread already has an active run.
|
|
230
272
|
*/
|
|
231
|
-
async createRun(threadId, request) {
|
|
273
|
+
async createRun(threadId, request, owner) {
|
|
232
274
|
const assistant = await this.threadManager.getAssistant(request.assistant_id);
|
|
233
275
|
if (!assistant) {
|
|
234
276
|
throw new InvalidRequestError(`Assistant ${request.assistant_id} not found`, 'assistant_id');
|
|
235
277
|
}
|
|
236
|
-
const thread = await this.
|
|
278
|
+
const thread = await this.accessibleThread(threadId, owner);
|
|
237
279
|
if (!thread) {
|
|
238
280
|
throw new InvalidRequestError(`Thread ${threadId} not found`);
|
|
239
281
|
}
|
|
240
282
|
const activeRunId = this.activeRunByThread.get(threadId);
|
|
241
283
|
if (activeRunId) {
|
|
242
|
-
|
|
284
|
+
const visible = this.accessibleRun(threadId, activeRunId, owner) !== undefined;
|
|
285
|
+
throw new InvalidRequestError(`Thread ${threadId} already has an active run${visible ? ` ${activeRunId}` : ''}.`);
|
|
243
286
|
}
|
|
244
287
|
if (request.max_prompt_tokens !== undefined && !isPositiveInteger(request.max_prompt_tokens)) {
|
|
245
288
|
throw new InvalidRequestError('max_prompt_tokens must be a positive integer', 'max_prompt_tokens');
|
|
@@ -277,11 +320,16 @@ export class OpenAIAdapter {
|
|
|
277
320
|
};
|
|
278
321
|
const state = {
|
|
279
322
|
run,
|
|
323
|
+
...(owner !== undefined && { owner }),
|
|
324
|
+
steps: new RunStepRecorder(run, (event, data) => this.emitStep(state, event, data)),
|
|
280
325
|
abortController: new AbortController(),
|
|
281
326
|
emitter: new EventEmitter(),
|
|
282
327
|
events: [],
|
|
283
328
|
ended: false,
|
|
284
329
|
clientOutputs: new Map(),
|
|
330
|
+
messageWrites: [],
|
|
331
|
+
outbox: Promise.resolve(),
|
|
332
|
+
queued: 0,
|
|
285
333
|
};
|
|
286
334
|
this.runs.set(runId, state);
|
|
287
335
|
this.activeRunByThread.set(threadId, runId);
|
|
@@ -304,28 +352,35 @@ export class OpenAIAdapter {
|
|
|
304
352
|
/**
|
|
305
353
|
* Get a run by ID
|
|
306
354
|
*/
|
|
307
|
-
getRun(threadId, runId) {
|
|
308
|
-
const state = this.
|
|
309
|
-
|
|
310
|
-
return structuredClone(state.run);
|
|
311
|
-
}
|
|
312
|
-
return undefined;
|
|
355
|
+
getRun(threadId, runId, owner) {
|
|
356
|
+
const state = this.accessibleRun(threadId, runId, owner);
|
|
357
|
+
return state ? structuredClone(state.run) : undefined;
|
|
313
358
|
}
|
|
314
359
|
/**
|
|
315
360
|
* List runs of a thread, newest first
|
|
316
361
|
*/
|
|
317
|
-
listRuns(threadId) {
|
|
362
|
+
listRuns(threadId, owner) {
|
|
318
363
|
return Array.from(this.runs.values())
|
|
319
|
-
.filter((state) => state.run.thread_id === threadId)
|
|
364
|
+
.filter((state) => state.run.thread_id === threadId && canAccess(state.owner, owner))
|
|
320
365
|
.map((state) => structuredClone(state.run))
|
|
321
366
|
.reverse();
|
|
322
367
|
}
|
|
368
|
+
/**
|
|
369
|
+
* The steps of a run in the order they were created, or undefined when there is no such run.
|
|
370
|
+
* Steps are kept with their run (see `maxStoredRuns`).
|
|
371
|
+
*/
|
|
372
|
+
listRunSteps(threadId, runId, owner) {
|
|
373
|
+
return this.accessibleRun(threadId, runId, owner)?.steps.list();
|
|
374
|
+
}
|
|
375
|
+
getRunStep(threadId, runId, stepId, owner) {
|
|
376
|
+
return this.accessibleRun(threadId, runId, owner)?.steps.get(stepId);
|
|
377
|
+
}
|
|
323
378
|
/**
|
|
324
379
|
* Cancel a run. Throws when the run already finished.
|
|
325
380
|
*/
|
|
326
|
-
cancelRun(threadId, runId) {
|
|
327
|
-
const state = this.
|
|
328
|
-
if (state
|
|
381
|
+
cancelRun(threadId, runId, owner) {
|
|
382
|
+
const state = this.accessibleRun(threadId, runId, owner);
|
|
383
|
+
if (!state) {
|
|
329
384
|
return undefined;
|
|
330
385
|
}
|
|
331
386
|
if (!isActive(state.run.status) || state.run.status === 'cancelling') {
|
|
@@ -341,9 +396,9 @@ export class OpenAIAdapter {
|
|
|
341
396
|
* tool call must be provided: the result of a client-side function, or the decision on a
|
|
342
397
|
* server tool that needs approval (see `toolOutputToDecision`).
|
|
343
398
|
*/
|
|
344
|
-
async submitToolOutputs(threadId, runId, request) {
|
|
345
|
-
const state = this.
|
|
346
|
-
if (state
|
|
399
|
+
async submitToolOutputs(threadId, runId, request, owner) {
|
|
400
|
+
const state = this.accessibleRun(threadId, runId, owner);
|
|
401
|
+
if (!state) {
|
|
347
402
|
return undefined;
|
|
348
403
|
}
|
|
349
404
|
const pending = state.pending;
|
|
@@ -378,7 +433,8 @@ export class OpenAIAdapter {
|
|
|
378
433
|
* stream only events emitted after this point.
|
|
379
434
|
*/
|
|
380
435
|
getRunEventCursor(runId) {
|
|
381
|
-
|
|
436
|
+
const state = this.runs.get(runId);
|
|
437
|
+
return state ? state.events.length + state.queued : 0;
|
|
382
438
|
}
|
|
383
439
|
/**
|
|
384
440
|
* Stream run events as server-sent-event payloads, starting at `fromIndex`
|
|
@@ -425,10 +481,51 @@ export class OpenAIAdapter {
|
|
|
425
481
|
event,
|
|
426
482
|
data: typeof data === 'string' ? data : structuredClone(data),
|
|
427
483
|
};
|
|
484
|
+
if (state.queued === 0) {
|
|
485
|
+
this.deliver(state, payload);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
this.enqueue(state, async () => this.deliver(state, payload));
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Emit a step event. The settle event of a step that waits in the outbox is named by the
|
|
492
|
+
* status the step has when it goes out, so a message step whose message was not stored after
|
|
493
|
+
* all goes out failed.
|
|
494
|
+
*/
|
|
495
|
+
emitStep(state, event, data) {
|
|
496
|
+
if (state.queued === 0 || !isSettledStep(data)) {
|
|
497
|
+
this.emit(state, event, data);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
this.enqueue(state, async () => this.deliver(state, { event: settledEvent(data), data: structuredClone(data) }));
|
|
501
|
+
}
|
|
502
|
+
/** Send an event to the run's listeners: one that throws is logged and never stops the run */
|
|
503
|
+
deliver(state, payload) {
|
|
428
504
|
state.events.push(payload);
|
|
429
|
-
|
|
505
|
+
try {
|
|
506
|
+
state.emitter.emit('event', payload.event, payload.data);
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
console.error(`[CogitatorOpenAI] A listener of run ${state.run.id} failed:`, error);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
/** Runs `step` once every event queued before it is out, holding back the events after it */
|
|
513
|
+
enqueue(state, step) {
|
|
514
|
+
state.queued++;
|
|
515
|
+
state.outbox = state.outbox
|
|
516
|
+
.then(step)
|
|
517
|
+
.catch((error) => {
|
|
518
|
+
console.error(`[CogitatorOpenAI] Run ${state.run.id} could not send an event:`, error);
|
|
519
|
+
})
|
|
520
|
+
.finally(() => {
|
|
521
|
+
state.queued--;
|
|
522
|
+
});
|
|
430
523
|
}
|
|
431
524
|
end(state) {
|
|
525
|
+
if (state.queued > 0) {
|
|
526
|
+
void state.outbox.then(() => this.end(state));
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
432
529
|
if (state.ended)
|
|
433
530
|
return;
|
|
434
531
|
state.ended = true;
|
|
@@ -444,6 +541,7 @@ export class OpenAIAdapter {
|
|
|
444
541
|
state.expiryTimer = undefined;
|
|
445
542
|
}
|
|
446
543
|
}
|
|
544
|
+
/** Stops the run with `reason`: a cancel, an expiry, or a store of its messages that failed */
|
|
447
545
|
terminate(state, reason) {
|
|
448
546
|
this.clearExpiry(state);
|
|
449
547
|
const pending = state.pending;
|
|
@@ -522,11 +620,16 @@ export class OpenAIAdapter {
|
|
|
522
620
|
}
|
|
523
621
|
/**
|
|
524
622
|
* Put the run in `requires_action` with `calls` and wait for the client's outputs, or for the
|
|
525
|
-
* run to be cancelled or to expire (`RUN_TTL_SECONDS`).
|
|
623
|
+
* run to be cancelled or to expire (`RUN_TTL_SECONDS`). The messages before the pause are
|
|
624
|
+
* stored and their events sent first, so a client that sees `requires_action` has all of them,
|
|
625
|
+
* and a store that failed fails the run instead.
|
|
526
626
|
*/
|
|
527
|
-
awaitToolOutputs(state, calls) {
|
|
528
|
-
|
|
529
|
-
|
|
627
|
+
async awaitToolOutputs(state, calls) {
|
|
628
|
+
await Promise.all(state.messageWrites);
|
|
629
|
+
await state.outbox;
|
|
630
|
+
const { signal } = state.abortController;
|
|
631
|
+
if (signal.aborted) {
|
|
632
|
+
throw signal.reason instanceof Error ? signal.reason : new RunTerminatedError('cancelled');
|
|
530
633
|
}
|
|
531
634
|
return new Promise((resolve, reject) => {
|
|
532
635
|
state.pending = { calls, resolve, reject };
|
|
@@ -604,6 +707,31 @@ export class OpenAIAdapter {
|
|
|
604
707
|
const { run } = state;
|
|
605
708
|
const threadId = run.thread_id;
|
|
606
709
|
const signal = state.abortController.signal;
|
|
710
|
+
let message = newRunMessage();
|
|
711
|
+
const announceMessage = () => {
|
|
712
|
+
if (message.announced)
|
|
713
|
+
return;
|
|
714
|
+
message.announced = true;
|
|
715
|
+
message.createdAt = nowSeconds();
|
|
716
|
+
state.steps.messageStarted(message.id);
|
|
717
|
+
const inProgress = {
|
|
718
|
+
id: message.id,
|
|
719
|
+
object: 'thread.message',
|
|
720
|
+
created_at: message.createdAt,
|
|
721
|
+
thread_id: threadId,
|
|
722
|
+
status: 'in_progress',
|
|
723
|
+
completed_at: null,
|
|
724
|
+
incomplete_at: null,
|
|
725
|
+
role: 'assistant',
|
|
726
|
+
content: [],
|
|
727
|
+
assistant_id: assistant.id,
|
|
728
|
+
run_id: run.id,
|
|
729
|
+
attachments: [],
|
|
730
|
+
metadata: {},
|
|
731
|
+
};
|
|
732
|
+
this.emit(state, 'thread.message.created', inProgress);
|
|
733
|
+
this.emit(state, 'thread.message.in_progress', inProgress);
|
|
734
|
+
};
|
|
607
735
|
try {
|
|
608
736
|
run.status = 'in_progress';
|
|
609
737
|
run.started_at = nowSeconds();
|
|
@@ -638,31 +766,12 @@ export class OpenAIAdapter {
|
|
|
638
766
|
responseFormat: format.responseFormat,
|
|
639
767
|
tools,
|
|
640
768
|
});
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
const inProgress = {
|
|
648
|
-
id: messageId,
|
|
649
|
-
object: 'thread.message',
|
|
650
|
-
created_at: nowSeconds(),
|
|
651
|
-
thread_id: threadId,
|
|
652
|
-
status: 'in_progress',
|
|
653
|
-
completed_at: null,
|
|
654
|
-
incomplete_at: null,
|
|
655
|
-
role: 'assistant',
|
|
656
|
-
content: [],
|
|
657
|
-
assistant_id: assistant.id,
|
|
658
|
-
run_id: run.id,
|
|
659
|
-
attachments: [],
|
|
660
|
-
metadata: {},
|
|
661
|
-
};
|
|
662
|
-
this.emit(state, 'thread.message.created', inProgress);
|
|
663
|
-
this.emit(state, 'thread.message.in_progress', inProgress);
|
|
664
|
-
};
|
|
665
|
-
let accumulated = '';
|
|
769
|
+
/**
|
|
770
|
+
* Text of the latest model turn, as every middleware left it. A turn that writes text and
|
|
771
|
+
* calls tools becomes its own message, which a run without `stream: true` gets no tokens
|
|
772
|
+
* of, so it takes the text from here when the first call of the turn arrives.
|
|
773
|
+
*/
|
|
774
|
+
let turnText;
|
|
666
775
|
const turnOptions = {
|
|
667
776
|
signal,
|
|
668
777
|
stream: !!request.stream,
|
|
@@ -671,15 +780,35 @@ export class OpenAIAdapter {
|
|
|
671
780
|
if (signal.aborted)
|
|
672
781
|
return;
|
|
673
782
|
announceMessage();
|
|
674
|
-
|
|
783
|
+
message.text += token;
|
|
675
784
|
const delta = {
|
|
676
|
-
id:
|
|
785
|
+
id: message.id,
|
|
677
786
|
object: 'thread.message.delta',
|
|
678
787
|
delta: { content: [{ index: 0, type: 'text', text: { value: token } }] },
|
|
679
788
|
};
|
|
680
789
|
this.emit(state, 'thread.message.delta', delta);
|
|
681
790
|
},
|
|
682
791
|
onApproval: (approval) => clientToolNames.has(approval.toolName) ? 'pause' : undefined,
|
|
792
|
+
onModelTurn: (turn) => {
|
|
793
|
+
turnText = turn.content;
|
|
794
|
+
},
|
|
795
|
+
onToolCall: (call) => {
|
|
796
|
+
if (signal.aborted)
|
|
797
|
+
return;
|
|
798
|
+
if (turnText !== undefined && (message.announced || turnText)) {
|
|
799
|
+
announceMessage();
|
|
800
|
+
message.text = turnText;
|
|
801
|
+
}
|
|
802
|
+
turnText = undefined;
|
|
803
|
+
if (message.announced && !message.written) {
|
|
804
|
+
void this.sealMessage(state, assistant.id, message, message.text);
|
|
805
|
+
state.steps.messageFinished(null);
|
|
806
|
+
message = newRunMessage();
|
|
807
|
+
}
|
|
808
|
+
state.steps.toolCalled(call);
|
|
809
|
+
},
|
|
810
|
+
onToolResult: (result) => state.steps.toolReturned(result),
|
|
811
|
+
onSpan: (span) => state.steps.recordSpan(span),
|
|
683
812
|
};
|
|
684
813
|
let result = await this.cogitator.run(agent, {
|
|
685
814
|
...turnOptions,
|
|
@@ -701,7 +830,14 @@ export class OpenAIAdapter {
|
|
|
701
830
|
if (signal.aborted) {
|
|
702
831
|
throw signal.reason instanceof Error ? signal.reason : new RunTerminatedError('cancelled');
|
|
703
832
|
}
|
|
704
|
-
|
|
833
|
+
run.usage = result.usage
|
|
834
|
+
? {
|
|
835
|
+
prompt_tokens: result.usage.inputTokens,
|
|
836
|
+
completion_tokens: result.usage.outputTokens,
|
|
837
|
+
total_tokens: result.usage.totalTokens,
|
|
838
|
+
}
|
|
839
|
+
: null;
|
|
840
|
+
const finalContent = result.output || message.text;
|
|
705
841
|
const incompleteReason = result.truncated
|
|
706
842
|
? 'max_tokens'
|
|
707
843
|
: result.blocked === 'content_filter'
|
|
@@ -709,18 +845,11 @@ export class OpenAIAdapter {
|
|
|
709
845
|
: undefined;
|
|
710
846
|
if (finalContent || incompleteReason) {
|
|
711
847
|
announceMessage();
|
|
712
|
-
|
|
713
|
-
if (message) {
|
|
714
|
-
this.emit(state, incompleteReason ? 'thread.message.incomplete' : 'thread.message.completed', message);
|
|
715
|
-
}
|
|
848
|
+
void this.sealMessage(state, assistant.id, message, finalContent, incompleteReason);
|
|
716
849
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
completion_tokens: result.usage.outputTokens,
|
|
721
|
-
total_tokens: result.usage.totalTokens,
|
|
722
|
-
}
|
|
723
|
-
: null;
|
|
850
|
+
await Promise.all(state.messageWrites);
|
|
851
|
+
state.steps.messageFinished(run.usage);
|
|
852
|
+
state.steps.finish({ status: 'completed' });
|
|
724
853
|
if (result.truncated) {
|
|
725
854
|
run.status = 'incomplete';
|
|
726
855
|
run.incomplete_details = { reason: 'max_completion_tokens' };
|
|
@@ -733,9 +862,13 @@ export class OpenAIAdapter {
|
|
|
733
862
|
}
|
|
734
863
|
}
|
|
735
864
|
catch (error) {
|
|
865
|
+
await Promise.allSettled(state.messageWrites);
|
|
736
866
|
const reason = signal.aborted ? signal.reason : error;
|
|
737
867
|
if (reason instanceof RunTerminatedError) {
|
|
738
|
-
|
|
868
|
+
const cancelled = reason.status === 'cancelled';
|
|
869
|
+
await this.keepUnfinishedMessage(state, assistant.id, message, cancelled ? 'run_cancelled' : 'run_expired');
|
|
870
|
+
state.steps.finish({ status: reason.status });
|
|
871
|
+
if (cancelled) {
|
|
739
872
|
run.status = 'cancelled';
|
|
740
873
|
run.cancelled_at = nowSeconds();
|
|
741
874
|
this.emit(state, 'thread.run.cancelled', run);
|
|
@@ -746,14 +879,21 @@ export class OpenAIAdapter {
|
|
|
746
879
|
}
|
|
747
880
|
}
|
|
748
881
|
else if (reason instanceof PromptBudgetExceededError) {
|
|
882
|
+
state.steps.finish({ status: 'completed' });
|
|
749
883
|
run.status = 'incomplete';
|
|
750
884
|
run.incomplete_details = { reason: 'max_prompt_tokens' };
|
|
751
885
|
this.emit(state, 'thread.run.incomplete', run);
|
|
752
886
|
}
|
|
753
887
|
else {
|
|
888
|
+
const lastError = {
|
|
889
|
+
code: 'server_error',
|
|
890
|
+
message: this.clientErrorMessage(run, reason),
|
|
891
|
+
};
|
|
892
|
+
await this.keepUnfinishedMessage(state, assistant.id, message, 'run_failed');
|
|
893
|
+
state.steps.finish({ status: 'failed', error: lastError });
|
|
754
894
|
run.status = 'failed';
|
|
755
895
|
run.failed_at = nowSeconds();
|
|
756
|
-
run.last_error =
|
|
896
|
+
run.last_error = lastError;
|
|
757
897
|
this.emit(state, 'thread.run.failed', run);
|
|
758
898
|
}
|
|
759
899
|
}
|
|
@@ -764,6 +904,59 @@ export class OpenAIAdapter {
|
|
|
764
904
|
this.end(state);
|
|
765
905
|
}
|
|
766
906
|
}
|
|
907
|
+
/**
|
|
908
|
+
* Finish a message of the run: store it and emit its final event once it is stored, with the
|
|
909
|
+
* events of the next steps held back until then. Stores to one thread keep the order of the
|
|
910
|
+
* calls, and the run waits for them (`messageWrites`) before it completes. A store that fails,
|
|
911
|
+
* or finds the thread deleted, stops the run right away, and the message ends as incomplete
|
|
912
|
+
* (`run_failed`) instead of completed, as it was never kept.
|
|
913
|
+
*/
|
|
914
|
+
sealMessage(state, assistantId, message, content, incompleteReason) {
|
|
915
|
+
const threadId = state.run.thread_id;
|
|
916
|
+
const sealedAs = (reason) => assistantMessage({
|
|
917
|
+
threadId,
|
|
918
|
+
content,
|
|
919
|
+
assistantId,
|
|
920
|
+
runId: state.run.id,
|
|
921
|
+
messageId: message.id,
|
|
922
|
+
incompleteReason: reason,
|
|
923
|
+
createdAt: message.createdAt,
|
|
924
|
+
});
|
|
925
|
+
const sealed = sealedAs(incompleteReason);
|
|
926
|
+
message.written = true;
|
|
927
|
+
const write = this.threadManager.appendMessage(threadId, sealed).then((stored) => {
|
|
928
|
+
if (!stored) {
|
|
929
|
+
throw new InvalidRequestError(`Thread ${threadId} was deleted while the run was writing to it`);
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
this.enqueue(state, () => write.then(() => this.deliver(state, {
|
|
933
|
+
event: incompleteReason ? 'thread.message.incomplete' : 'thread.message.completed',
|
|
934
|
+
data: structuredClone(sealed),
|
|
935
|
+
}), () => {
|
|
936
|
+
state.steps.messageLost(message.id, {
|
|
937
|
+
code: 'server_error',
|
|
938
|
+
message: 'The message could not be stored',
|
|
939
|
+
});
|
|
940
|
+
this.deliver(state, { event: 'thread.message.incomplete', data: sealedAs('run_failed') });
|
|
941
|
+
}));
|
|
942
|
+
write.catch((error) => this.terminate(state, error instanceof Error ? error : new Error(String(error))));
|
|
943
|
+
state.messageWrites.push(write);
|
|
944
|
+
return write;
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Keep what the model wrote of a message the run announced but did not finish, as an
|
|
948
|
+
* incomplete message, so the `message_creation` step points at a message that exists.
|
|
949
|
+
*/
|
|
950
|
+
async keepUnfinishedMessage(state, assistantId, message, reason) {
|
|
951
|
+
if (!message.announced || message.written)
|
|
952
|
+
return;
|
|
953
|
+
try {
|
|
954
|
+
await this.sealMessage(state, assistantId, message, message.text, reason);
|
|
955
|
+
}
|
|
956
|
+
catch (error) {
|
|
957
|
+
console.error(`[CogitatorOpenAI] Run ${state.run.id} could not keep its unfinished message:`, error);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
767
960
|
}
|
|
768
961
|
/**
|
|
769
962
|
* Create an OpenAI adapter for Cogitator
|