@cogitator-ai/openai-compat 21.2.13 → 21.3.0

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.
Files changed (46) hide show
  1. package/README.md +88 -10
  2. package/dist/client/openai-adapter.d.ts +61 -19
  3. package/dist/client/openai-adapter.d.ts.map +1 -1
  4. package/dist/client/openai-adapter.js +270 -77
  5. package/dist/client/openai-adapter.js.map +1 -1
  6. package/dist/client/pagination.d.ts +26 -0
  7. package/dist/client/pagination.d.ts.map +1 -0
  8. package/dist/client/pagination.js +34 -0
  9. package/dist/client/pagination.js.map +1 -0
  10. package/dist/client/run-steps.d.ts +66 -0
  11. package/dist/client/run-steps.d.ts.map +1 -0
  12. package/dist/client/run-steps.js +210 -0
  13. package/dist/client/run-steps.js.map +1 -0
  14. package/dist/client/storage.d.ts +44 -4
  15. package/dist/client/storage.d.ts.map +1 -1
  16. package/dist/client/storage.js +111 -7
  17. package/dist/client/storage.js.map +1 -1
  18. package/dist/client/thread-manager.d.ts +36 -3
  19. package/dist/client/thread-manager.d.ts.map +1 -1
  20. package/dist/client/thread-manager.js +49 -34
  21. package/dist/client/thread-manager.js.map +1 -1
  22. package/dist/index.d.ts +4 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +1 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/server/api-server.d.ts +4 -0
  27. package/dist/server/api-server.d.ts.map +1 -1
  28. package/dist/server/api-server.js +1 -0
  29. package/dist/server/api-server.js.map +1 -1
  30. package/dist/server/middleware/auth.d.ts +13 -1
  31. package/dist/server/middleware/auth.d.ts.map +1 -1
  32. package/dist/server/middleware/auth.js +12 -5
  33. package/dist/server/middleware/auth.js.map +1 -1
  34. package/dist/server/routes/runs.d.ts.map +1 -1
  35. package/dist/server/routes/runs.js +45 -11
  36. package/dist/server/routes/runs.js.map +1 -1
  37. package/dist/server/routes/shared.d.ts +1 -15
  38. package/dist/server/routes/shared.d.ts.map +1 -1
  39. package/dist/server/routes/shared.js +1 -23
  40. package/dist/server/routes/shared.js.map +1 -1
  41. package/dist/server/routes/threads.d.ts.map +1 -1
  42. package/dist/server/routes/threads.js +14 -26
  43. package/dist/server/routes/threads.js.map +1 -1
  44. package/dist/types/openai-types.d.ts +23 -2
  45. package/dist/types/openai-types.d.ts.map +1 -1
  46. 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
- async createThread(metadata) {
205
- return this.threadManager.createThread(metadata);
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.threadManager.getThread(id);
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
- return this.threadManager.getMessage(threadId, messageId);
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, or when the thread
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.threadManager.getThread(threadId);
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
- throw new InvalidRequestError(`Thread ${threadId} already has an active run ${activeRunId}.`);
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.runs.get(runId);
309
- if (state?.run.thread_id === threadId) {
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.runs.get(runId);
328
- if (state?.run.thread_id !== threadId) {
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.runs.get(runId);
346
- if (state?.run.thread_id !== threadId) {
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
- return this.runs.get(runId)?.events.length ?? 0;
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
- state.emitter.emit('event', payload.event, payload.data);
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
- if (state.abortController.signal.aborted) {
529
- return Promise.reject(new RunTerminatedError('cancelled'));
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
- const messageId = `msg_${nanoid()}`;
642
- let messageAnnounced = false;
643
- const announceMessage = () => {
644
- if (messageAnnounced)
645
- return;
646
- messageAnnounced = true;
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
- accumulated += token;
783
+ message.text += token;
675
784
  const delta = {
676
- id: messageId,
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
- const finalContent = result.output || accumulated;
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
- const message = await this.threadManager.addAssistantMessage(threadId, finalContent, assistant.id, run.id, messageId, incompleteReason);
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
- run.usage = result.usage
718
- ? {
719
- prompt_tokens: result.usage.inputTokens,
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
- if (reason.status === 'cancelled') {
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 = { code: 'server_error', message: this.clientErrorMessage(run, 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