@cogitator-ai/openai-compat 19.0.16 → 20.0.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.
- package/README.md +74 -50
- package/dist/client/openai-adapter.d.ts +74 -37
- package/dist/client/openai-adapter.d.ts.map +1 -1
- package/dist/client/openai-adapter.js +416 -137
- package/dist/client/openai-adapter.js.map +1 -1
- package/dist/client/storage.d.ts +4 -0
- package/dist/client/storage.d.ts.map +1 -1
- package/dist/client/storage.js +80 -60
- package/dist/client/storage.js.map +1 -1
- package/dist/client/thread-manager.d.ts +45 -31
- package/dist/client/thread-manager.d.ts.map +1 -1
- package/dist/client/thread-manager.js +157 -115
- package/dist/client/thread-manager.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/server/api-server.d.ts +23 -1
- package/dist/server/api-server.d.ts.map +1 -1
- package/dist/server/api-server.js +42 -23
- package/dist/server/api-server.js.map +1 -1
- package/dist/server/middleware/auth.d.ts +2 -0
- package/dist/server/middleware/auth.d.ts.map +1 -1
- package/dist/server/middleware/auth.js +25 -2
- package/dist/server/middleware/auth.js.map +1 -1
- package/dist/server/routes/assistants.d.ts.map +1 -1
- package/dist/server/routes/assistants.js +16 -26
- package/dist/server/routes/assistants.js.map +1 -1
- package/dist/server/routes/files.d.ts.map +1 -1
- package/dist/server/routes/files.js +60 -56
- package/dist/server/routes/files.js.map +1 -1
- package/dist/server/routes/runs.d.ts.map +1 -1
- package/dist/server/routes/runs.js +102 -105
- package/dist/server/routes/runs.js.map +1 -1
- package/dist/server/routes/shared.d.ts +27 -0
- package/dist/server/routes/shared.d.ts.map +1 -0
- package/dist/server/routes/shared.js +54 -0
- package/dist/server/routes/shared.js.map +1 -0
- package/dist/server/routes/threads.d.ts.map +1 -1
- package/dist/server/routes/threads.js +29 -16
- package/dist/server/routes/threads.js.map +1 -1
- package/package.json +18 -6
|
@@ -1,46 +1,81 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenAI SDK Adapter
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* Creates an in-process adapter that can be used with the SDK's baseURL.
|
|
4
|
+
* Implements the OpenAI Assistants API semantics on top of Cogitator.
|
|
6
5
|
*/
|
|
7
6
|
import { Agent } from '@cogitator-ai/core';
|
|
8
7
|
import { EventEmitter } from 'events';
|
|
9
|
-
import { ThreadManager } from './thread-manager.js';
|
|
10
8
|
import { nanoid } from 'nanoid';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { ThreadManager } from './thread-manager.js';
|
|
11
|
+
export const COGITATOR_MODEL_ID = 'cogitator';
|
|
12
|
+
const ACTIVE_STATUSES = [
|
|
13
|
+
'queued',
|
|
14
|
+
'in_progress',
|
|
15
|
+
'requires_action',
|
|
16
|
+
'cancelling',
|
|
17
|
+
];
|
|
18
|
+
const RUN_TTL_SECONDS = 600;
|
|
19
|
+
class RunTerminatedError extends Error {
|
|
20
|
+
status;
|
|
21
|
+
constructor(status) {
|
|
22
|
+
super(status === 'cancelled' ? 'Run was cancelled' : 'Run expired waiting for tool outputs');
|
|
23
|
+
this.status = status;
|
|
24
|
+
this.name = 'RunTerminatedError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function nowSeconds() {
|
|
28
|
+
return Math.floor(Date.now() / 1000);
|
|
29
|
+
}
|
|
30
|
+
function isActive(status) {
|
|
31
|
+
return ACTIVE_STATUSES.includes(status);
|
|
32
|
+
}
|
|
33
|
+
function toAgentResponseFormat(format) {
|
|
34
|
+
if (!format || format === 'auto' || format.type === 'text')
|
|
35
|
+
return {};
|
|
36
|
+
if (format.type === 'json_object')
|
|
37
|
+
return { responseFormat: { type: 'json' } };
|
|
38
|
+
return {
|
|
39
|
+
responseFormat: { type: 'json' },
|
|
40
|
+
instructions: `Respond with a JSON object that matches this JSON Schema (${format.json_schema.name}):\n${JSON.stringify(format.json_schema.schema)}`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function toZodParameters(schema) {
|
|
44
|
+
try {
|
|
45
|
+
return z.fromJSONSchema(schema ?? { type: 'object', properties: {} });
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return z.record(z.string(), z.unknown());
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function renderTranscript(messages) {
|
|
52
|
+
return messages
|
|
53
|
+
.map((m) => `${m.role === 'assistant' ? 'Assistant' : 'User'}: ${m.content}`)
|
|
54
|
+
.join('\n\n');
|
|
55
|
+
}
|
|
11
56
|
/**
|
|
12
57
|
* OpenAI SDK Adapter
|
|
13
58
|
*
|
|
14
59
|
* @example
|
|
15
60
|
* ```typescript
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* const adapter = createOpenAIAdapter(cogitator, {
|
|
20
|
-
* tools: [calculator, datetime],
|
|
21
|
-
* });
|
|
22
|
-
*
|
|
23
|
-
* const openai = new OpenAI({
|
|
24
|
-
* apiKey: 'not-needed',
|
|
25
|
-
* baseURL: adapter.baseURL,
|
|
26
|
-
* });
|
|
27
|
-
*
|
|
28
|
-
* // Use OpenAI SDK as normal
|
|
29
|
-
* const assistant = await openai.beta.assistants.create({
|
|
30
|
-
* model: 'gpt-4o',
|
|
31
|
-
* name: 'My Assistant',
|
|
32
|
-
* });
|
|
61
|
+
* const adapter = createOpenAIAdapter(cogitator, { tools: [calculator] });
|
|
62
|
+
* const assistant = await adapter.createAssistant({ model: 'openai/gpt-4o' });
|
|
33
63
|
* ```
|
|
34
64
|
*/
|
|
35
65
|
export class OpenAIAdapter {
|
|
36
66
|
cogitator;
|
|
37
67
|
threadManager;
|
|
38
68
|
runs = new Map();
|
|
69
|
+
activeRunByThread = new Map();
|
|
39
70
|
tools;
|
|
71
|
+
defaultModel;
|
|
72
|
+
maxStoredRuns;
|
|
40
73
|
constructor(cogitator, options) {
|
|
41
74
|
this.cogitator = cogitator;
|
|
42
|
-
this.threadManager = new ThreadManager();
|
|
75
|
+
this.threadManager = new ThreadManager(options?.storage);
|
|
43
76
|
this.tools = options?.tools ?? [];
|
|
77
|
+
this.defaultModel = options?.defaultModel;
|
|
78
|
+
this.maxStoredRuns = options?.maxStoredRuns ?? 10_000;
|
|
44
79
|
}
|
|
45
80
|
/**
|
|
46
81
|
* Get the thread manager for direct access
|
|
@@ -73,12 +108,14 @@ export class OpenAIAdapter {
|
|
|
73
108
|
object: 'assistant',
|
|
74
109
|
created_at: stored.created_at,
|
|
75
110
|
name: stored.name,
|
|
76
|
-
description: null,
|
|
111
|
+
description: stored.description ?? null,
|
|
77
112
|
model: stored.model,
|
|
78
113
|
instructions: stored.instructions,
|
|
79
114
|
tools: stored.tools,
|
|
80
115
|
metadata: stored.metadata,
|
|
81
116
|
temperature: stored.temperature,
|
|
117
|
+
top_p: stored.top_p,
|
|
118
|
+
response_format: stored.response_format,
|
|
82
119
|
};
|
|
83
120
|
}
|
|
84
121
|
async createThread(metadata) {
|
|
@@ -103,7 +140,10 @@ export class OpenAIAdapter {
|
|
|
103
140
|
return this.threadManager.listMessages(threadId, options);
|
|
104
141
|
}
|
|
105
142
|
/**
|
|
106
|
-
* Create and
|
|
143
|
+
* Create a run and start executing it in the background.
|
|
144
|
+
*
|
|
145
|
+
* Rejects when the assistant or thread does not exist, or when the thread
|
|
146
|
+
* already has an active run.
|
|
107
147
|
*/
|
|
108
148
|
async createRun(threadId, request) {
|
|
109
149
|
const assistant = await this.threadManager.getAssistant(request.assistant_id);
|
|
@@ -114,8 +154,12 @@ export class OpenAIAdapter {
|
|
|
114
154
|
if (!thread) {
|
|
115
155
|
throw new Error(`Thread ${threadId} not found`);
|
|
116
156
|
}
|
|
157
|
+
const activeRunId = this.activeRunByThread.get(threadId);
|
|
158
|
+
if (activeRunId) {
|
|
159
|
+
throw new Error(`Thread ${threadId} already has an active run ${activeRunId}.`);
|
|
160
|
+
}
|
|
117
161
|
const runId = `run_${nanoid()}`;
|
|
118
|
-
const now =
|
|
162
|
+
const now = nowSeconds();
|
|
119
163
|
const run = {
|
|
120
164
|
id: runId,
|
|
121
165
|
object: 'thread.run',
|
|
@@ -125,7 +169,7 @@ export class OpenAIAdapter {
|
|
|
125
169
|
status: 'queued',
|
|
126
170
|
required_action: null,
|
|
127
171
|
last_error: null,
|
|
128
|
-
expires_at: now +
|
|
172
|
+
expires_at: now + RUN_TTL_SECONDS,
|
|
129
173
|
started_at: null,
|
|
130
174
|
cancelled_at: null,
|
|
131
175
|
failed_at: null,
|
|
@@ -133,44 +177,44 @@ export class OpenAIAdapter {
|
|
|
133
177
|
incomplete_details: null,
|
|
134
178
|
model: request.model ?? assistant.model,
|
|
135
179
|
instructions: request.instructions ?? assistant.instructions,
|
|
136
|
-
tools:
|
|
180
|
+
tools: request.tools ?? assistant.tools,
|
|
137
181
|
metadata: request.metadata ?? {},
|
|
138
182
|
usage: null,
|
|
139
183
|
temperature: request.temperature ?? assistant.temperature,
|
|
140
|
-
top_p: request.top_p,
|
|
184
|
+
top_p: request.top_p ?? assistant.top_p,
|
|
141
185
|
max_prompt_tokens: request.max_prompt_tokens,
|
|
142
186
|
max_completion_tokens: request.max_completion_tokens,
|
|
143
187
|
truncation_strategy: request.truncation_strategy,
|
|
144
|
-
response_format: request.response_format,
|
|
188
|
+
response_format: request.response_format ?? assistant.response_format,
|
|
145
189
|
tool_choice: request.tool_choice,
|
|
146
190
|
parallel_tool_calls: request.parallel_tool_calls,
|
|
147
191
|
};
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
192
|
+
const state = {
|
|
193
|
+
run,
|
|
194
|
+
abortController: new AbortController(),
|
|
195
|
+
emitter: new EventEmitter(),
|
|
196
|
+
events: [],
|
|
197
|
+
ended: false,
|
|
198
|
+
pendingCalls: [],
|
|
199
|
+
flushScheduled: false,
|
|
200
|
+
};
|
|
201
|
+
this.runs.set(runId, state);
|
|
202
|
+
this.activeRunByThread.set(threadId, runId);
|
|
203
|
+
this.evictFinishedRuns();
|
|
204
|
+
try {
|
|
205
|
+
for (const msg of request.additional_messages ?? []) {
|
|
153
206
|
await this.threadManager.addMessage(threadId, msg);
|
|
154
207
|
}
|
|
155
208
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
state.run.last_error = {
|
|
166
|
-
code: 'server_error',
|
|
167
|
-
message: errorMessage,
|
|
168
|
-
};
|
|
169
|
-
state.streamEmitter?.emit('event', 'thread.run.failed', state.run);
|
|
170
|
-
state.streamEmitter?.emit('end');
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
return run;
|
|
209
|
+
catch (error) {
|
|
210
|
+
this.runs.delete(runId);
|
|
211
|
+
this.activeRunByThread.delete(threadId);
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
this.emit(state, 'thread.run.created', run);
|
|
215
|
+
this.emit(state, 'thread.run.queued', run);
|
|
216
|
+
void this.executeRun(state, assistant, request);
|
|
217
|
+
return structuredClone(run);
|
|
174
218
|
}
|
|
175
219
|
/**
|
|
176
220
|
* Get a run by ID
|
|
@@ -178,25 +222,38 @@ export class OpenAIAdapter {
|
|
|
178
222
|
getRun(threadId, runId) {
|
|
179
223
|
const state = this.runs.get(runId);
|
|
180
224
|
if (state?.run.thread_id === threadId) {
|
|
181
|
-
return state.run;
|
|
225
|
+
return structuredClone(state.run);
|
|
182
226
|
}
|
|
183
227
|
return undefined;
|
|
184
228
|
}
|
|
185
229
|
/**
|
|
186
|
-
*
|
|
230
|
+
* List runs of a thread, newest first
|
|
231
|
+
*/
|
|
232
|
+
listRuns(threadId) {
|
|
233
|
+
return Array.from(this.runs.values())
|
|
234
|
+
.filter((state) => state.run.thread_id === threadId)
|
|
235
|
+
.map((state) => structuredClone(state.run))
|
|
236
|
+
.reverse();
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Cancel a run. Throws when the run already finished.
|
|
187
240
|
*/
|
|
188
241
|
cancelRun(threadId, runId) {
|
|
189
242
|
const state = this.runs.get(runId);
|
|
190
|
-
if (state?.run.thread_id
|
|
191
|
-
|
|
192
|
-
state.run.status = 'cancelled';
|
|
193
|
-
state.run.cancelled_at = Math.floor(Date.now() / 1000);
|
|
194
|
-
return state.run;
|
|
243
|
+
if (state?.run.thread_id !== threadId) {
|
|
244
|
+
return undefined;
|
|
195
245
|
}
|
|
196
|
-
|
|
246
|
+
if (!isActive(state.run.status) || state.run.status === 'cancelling') {
|
|
247
|
+
throw new Error(`Cannot cancel run with status '${state.run.status}'.`);
|
|
248
|
+
}
|
|
249
|
+
state.run.status = 'cancelling';
|
|
250
|
+
this.emit(state, 'thread.run.cancelling', state.run);
|
|
251
|
+
this.terminate(state, new RunTerminatedError('cancelled'));
|
|
252
|
+
return structuredClone(state.run);
|
|
197
253
|
}
|
|
198
254
|
/**
|
|
199
|
-
* Submit
|
|
255
|
+
* Submit outputs for the tool calls of a run that requires action.
|
|
256
|
+
* Outputs for every pending tool call must be provided.
|
|
200
257
|
*/
|
|
201
258
|
async submitToolOutputs(threadId, runId, request) {
|
|
202
259
|
const state = this.runs.get(runId);
|
|
@@ -204,53 +261,265 @@ export class OpenAIAdapter {
|
|
|
204
261
|
return undefined;
|
|
205
262
|
}
|
|
206
263
|
if (state.run.status !== 'requires_action') {
|
|
207
|
-
throw new Error(
|
|
264
|
+
throw new Error(`Run is not waiting for tool outputs (status: '${state.run.status}').`);
|
|
208
265
|
}
|
|
209
|
-
|
|
210
|
-
|
|
266
|
+
const outputs = new Map((request.tool_outputs ?? []).map((output) => [output.tool_call_id, output.output]));
|
|
267
|
+
const missing = state.pendingCalls.filter((pending) => !outputs.has(pending.call.id));
|
|
268
|
+
if (missing.length > 0) {
|
|
269
|
+
throw new Error(`Missing tool outputs for tool calls: ${missing.map((p) => p.call.id).join(', ')}`);
|
|
211
270
|
}
|
|
212
|
-
|
|
213
|
-
|
|
271
|
+
const unknown = [...outputs.keys()].filter((id) => !state.pendingCalls.some((pending) => pending.call.id === id));
|
|
272
|
+
if (unknown.length > 0) {
|
|
273
|
+
throw new Error(`Unknown tool call ids: ${unknown.join(', ')}`);
|
|
214
274
|
}
|
|
275
|
+
const pending = state.pendingCalls;
|
|
276
|
+
state.pendingCalls = [];
|
|
277
|
+
this.clearExpiry(state);
|
|
215
278
|
state.run.status = 'in_progress';
|
|
216
279
|
state.run.required_action = null;
|
|
217
|
-
|
|
280
|
+
this.emit(state, 'thread.run.in_progress', state.run);
|
|
281
|
+
for (const call of pending) {
|
|
282
|
+
call.resolve(outputs.get(call.call.id) ?? '');
|
|
283
|
+
}
|
|
284
|
+
return structuredClone(state.run);
|
|
218
285
|
}
|
|
219
286
|
/**
|
|
220
|
-
* Get the
|
|
287
|
+
* Get the event emitter of a run (all runs have one)
|
|
221
288
|
*/
|
|
222
289
|
getStreamEmitter(runId) {
|
|
223
|
-
return this.runs.get(runId)?.
|
|
290
|
+
return this.runs.get(runId)?.emitter;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Position in the run's event log; pass it to {@link streamRunEvents} to
|
|
294
|
+
* stream only events emitted after this point.
|
|
295
|
+
*/
|
|
296
|
+
getRunEventCursor(runId) {
|
|
297
|
+
return this.runs.get(runId)?.events.length ?? 0;
|
|
224
298
|
}
|
|
225
|
-
|
|
299
|
+
/**
|
|
300
|
+
* Stream run events as server-sent-event payloads, starting at `fromIndex`
|
|
301
|
+
* of the run's event log (0 replays the whole run). The stream ends after the
|
|
302
|
+
* next `done` event, which follows a terminal status or `requires_action`.
|
|
303
|
+
*/
|
|
304
|
+
async *streamRunEvents(runId, fromIndex = 0) {
|
|
226
305
|
const state = this.runs.get(runId);
|
|
227
306
|
if (!state)
|
|
228
307
|
return;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
308
|
+
let index = fromIndex;
|
|
309
|
+
let wake = null;
|
|
310
|
+
const onEvent = () => {
|
|
311
|
+
if (wake) {
|
|
312
|
+
wake();
|
|
313
|
+
wake = null;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
state.emitter.on('event', onEvent);
|
|
317
|
+
state.emitter.on('end', onEvent);
|
|
233
318
|
try {
|
|
234
|
-
|
|
319
|
+
while (true) {
|
|
320
|
+
if (index < state.events.length) {
|
|
321
|
+
const event = state.events[index++];
|
|
322
|
+
yield event;
|
|
323
|
+
if (event.event === 'done')
|
|
324
|
+
return;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (state.ended)
|
|
328
|
+
return;
|
|
329
|
+
await new Promise((resolve) => {
|
|
330
|
+
wake = resolve;
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
finally {
|
|
335
|
+
state.emitter.off('event', onEvent);
|
|
336
|
+
state.emitter.off('end', onEvent);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
emit(state, event, data) {
|
|
340
|
+
const payload = {
|
|
341
|
+
event,
|
|
342
|
+
data: typeof data === 'string' ? data : structuredClone(data),
|
|
343
|
+
};
|
|
344
|
+
state.events.push(payload);
|
|
345
|
+
state.emitter.emit('event', payload.event, payload.data);
|
|
346
|
+
}
|
|
347
|
+
end(state) {
|
|
348
|
+
if (state.ended)
|
|
349
|
+
return;
|
|
350
|
+
state.ended = true;
|
|
351
|
+
this.clearExpiry(state);
|
|
352
|
+
if (this.activeRunByThread.get(state.run.thread_id) === state.run.id) {
|
|
353
|
+
this.activeRunByThread.delete(state.run.thread_id);
|
|
354
|
+
}
|
|
355
|
+
state.emitter.emit('end');
|
|
356
|
+
}
|
|
357
|
+
clearExpiry(state) {
|
|
358
|
+
if (state.expiryTimer !== undefined) {
|
|
359
|
+
clearTimeout(state.expiryTimer);
|
|
360
|
+
state.expiryTimer = undefined;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
terminate(state, reason) {
|
|
364
|
+
this.clearExpiry(state);
|
|
365
|
+
const pending = state.pendingCalls;
|
|
366
|
+
state.pendingCalls = [];
|
|
367
|
+
for (const call of pending)
|
|
368
|
+
call.reject(reason);
|
|
369
|
+
state.abortController.abort(reason);
|
|
370
|
+
}
|
|
371
|
+
evictFinishedRuns() {
|
|
372
|
+
if (this.runs.size <= this.maxStoredRuns)
|
|
373
|
+
return;
|
|
374
|
+
for (const [id, state] of this.runs) {
|
|
375
|
+
if (this.runs.size <= this.maxStoredRuns)
|
|
376
|
+
return;
|
|
377
|
+
if (state.ended)
|
|
378
|
+
this.runs.delete(id);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
resolveModel(model) {
|
|
382
|
+
if (model !== COGITATOR_MODEL_ID)
|
|
383
|
+
return model;
|
|
384
|
+
if (!this.defaultModel) {
|
|
385
|
+
throw new Error(`Model '${COGITATOR_MODEL_ID}' requires the server to be configured with a defaultModel`);
|
|
386
|
+
}
|
|
387
|
+
return this.defaultModel;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Turn assistant `function` tools into Cogitator tools whose execution is
|
|
391
|
+
* delegated to the API client through `requires_action`.
|
|
392
|
+
*/
|
|
393
|
+
createClientTools(state, definitions) {
|
|
394
|
+
return definitions.map((definition) => {
|
|
395
|
+
const parameters = toZodParameters(definition.parameters);
|
|
396
|
+
return {
|
|
397
|
+
name: definition.name,
|
|
398
|
+
description: definition.description ?? definition.name,
|
|
399
|
+
parameters,
|
|
400
|
+
execute: (args) => new Promise((resolve, reject) => {
|
|
401
|
+
if (state.abortController.signal.aborted) {
|
|
402
|
+
reject(new RunTerminatedError('cancelled'));
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
state.pendingCalls.push({
|
|
406
|
+
call: {
|
|
407
|
+
id: `call_${nanoid()}`,
|
|
408
|
+
type: 'function',
|
|
409
|
+
function: { name: definition.name, arguments: JSON.stringify(args ?? {}) },
|
|
410
|
+
},
|
|
411
|
+
resolve,
|
|
412
|
+
reject,
|
|
413
|
+
});
|
|
414
|
+
this.scheduleRequiresAction(state);
|
|
415
|
+
}),
|
|
416
|
+
toJSON: () => {
|
|
417
|
+
const schema = (definition.parameters ?? {});
|
|
418
|
+
return {
|
|
419
|
+
name: definition.name,
|
|
420
|
+
description: definition.description ?? definition.name,
|
|
421
|
+
parameters: {
|
|
422
|
+
type: 'object',
|
|
423
|
+
properties: schema.properties ?? {},
|
|
424
|
+
required: schema.required,
|
|
425
|
+
},
|
|
426
|
+
};
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
scheduleRequiresAction(state) {
|
|
432
|
+
if (state.flushScheduled)
|
|
433
|
+
return;
|
|
434
|
+
state.flushScheduled = true;
|
|
435
|
+
setImmediate(() => {
|
|
436
|
+
state.flushScheduled = false;
|
|
437
|
+
if (state.pendingCalls.length === 0 || state.abortController.signal.aborted)
|
|
438
|
+
return;
|
|
439
|
+
state.run.status = 'requires_action';
|
|
440
|
+
state.run.required_action = {
|
|
441
|
+
type: 'submit_tool_outputs',
|
|
442
|
+
submit_tool_outputs: { tool_calls: state.pendingCalls.map((pending) => pending.call) },
|
|
443
|
+
};
|
|
444
|
+
state.run.expires_at = nowSeconds() + RUN_TTL_SECONDS;
|
|
445
|
+
this.emit(state, 'thread.run.requires_action', state.run);
|
|
446
|
+
this.emit(state, 'done', '[DONE]');
|
|
447
|
+
this.clearExpiry(state);
|
|
448
|
+
state.expiryTimer = setTimeout(() => {
|
|
449
|
+
this.terminate(state, new RunTerminatedError('expired'));
|
|
450
|
+
}, RUN_TTL_SECONDS * 1000);
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
async buildRunInput(threadId, request) {
|
|
454
|
+
let messages = await this.threadManager.getMessagesForLLM(threadId);
|
|
455
|
+
const truncation = request.truncation_strategy;
|
|
456
|
+
if (truncation?.type === 'last_messages' && truncation.last_messages) {
|
|
457
|
+
messages = messages.slice(-truncation.last_messages);
|
|
458
|
+
}
|
|
459
|
+
let lastUserIndex = -1;
|
|
460
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
461
|
+
if (messages[i].role === 'user') {
|
|
462
|
+
lastUserIndex = i;
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (lastUserIndex === -1) {
|
|
467
|
+
throw new Error('No user message found');
|
|
468
|
+
}
|
|
469
|
+
const current = messages[lastUserIndex];
|
|
470
|
+
const prior = messages.slice(0, lastUserIndex);
|
|
471
|
+
const input = prior.length > 0
|
|
472
|
+
? `Conversation so far:\n\n${renderTranscript(prior)}\n\nUser: ${current.content}`
|
|
473
|
+
: current.content;
|
|
474
|
+
return { input, images: current.images, hasHistory: prior.length > 0 };
|
|
475
|
+
}
|
|
476
|
+
async executeRun(state, assistant, request) {
|
|
477
|
+
const { run } = state;
|
|
478
|
+
const threadId = run.thread_id;
|
|
479
|
+
const signal = state.abortController.signal;
|
|
480
|
+
try {
|
|
481
|
+
run.status = 'in_progress';
|
|
482
|
+
run.started_at = nowSeconds();
|
|
483
|
+
this.emit(state, 'thread.run.in_progress', run);
|
|
484
|
+
const { input, images, hasHistory } = await this.buildRunInput(threadId, request);
|
|
485
|
+
const serverToolNames = new Set(this.tools.map((t) => t.name));
|
|
486
|
+
const functionDefinitions = run.tools.flatMap((t) => t.type === 'function' && !serverToolNames.has(t.function.name) ? [t.function] : []);
|
|
487
|
+
let tools = [...this.tools, ...this.createClientTools(state, functionDefinitions)];
|
|
488
|
+
const toolChoice = run.tool_choice;
|
|
489
|
+
if (toolChoice === 'none') {
|
|
490
|
+
tools = [];
|
|
491
|
+
}
|
|
492
|
+
else if (typeof toolChoice === 'object') {
|
|
493
|
+
tools = tools.filter((t) => t.name === toolChoice.function.name);
|
|
494
|
+
}
|
|
495
|
+
const format = toAgentResponseFormat(run.response_format);
|
|
496
|
+
const instructions = [
|
|
497
|
+
run.instructions ?? '',
|
|
498
|
+
request.additional_instructions ?? '',
|
|
499
|
+
format.instructions ?? '',
|
|
500
|
+
]
|
|
501
|
+
.filter((part) => part.length > 0)
|
|
502
|
+
.join('\n\n');
|
|
235
503
|
const agent = new Agent({
|
|
236
504
|
name: assistant.name ?? 'assistant',
|
|
237
|
-
model:
|
|
238
|
-
instructions
|
|
239
|
-
temperature:
|
|
240
|
-
|
|
505
|
+
model: this.resolveModel(run.model),
|
|
506
|
+
instructions,
|
|
507
|
+
temperature: run.temperature,
|
|
508
|
+
topP: run.top_p,
|
|
509
|
+
maxTokens: run.max_completion_tokens,
|
|
510
|
+
responseFormat: format.responseFormat,
|
|
511
|
+
tools,
|
|
241
512
|
});
|
|
242
|
-
const lastUserMessage = messages.filter((m) => m.role === 'user').pop();
|
|
243
|
-
if (!lastUserMessage) {
|
|
244
|
-
throw new Error('No user message found');
|
|
245
|
-
}
|
|
246
513
|
const messageId = `msg_${nanoid()}`;
|
|
247
|
-
let
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
514
|
+
let messageAnnounced = false;
|
|
515
|
+
const announceMessage = () => {
|
|
516
|
+
if (messageAnnounced)
|
|
517
|
+
return;
|
|
518
|
+
messageAnnounced = true;
|
|
519
|
+
const inProgress = {
|
|
251
520
|
id: messageId,
|
|
252
521
|
object: 'thread.message',
|
|
253
|
-
created_at:
|
|
522
|
+
created_at: nowSeconds(),
|
|
254
523
|
thread_id: threadId,
|
|
255
524
|
status: 'in_progress',
|
|
256
525
|
completed_at: null,
|
|
@@ -258,75 +527,85 @@ export class OpenAIAdapter {
|
|
|
258
527
|
role: 'assistant',
|
|
259
528
|
content: [],
|
|
260
529
|
assistant_id: assistant.id,
|
|
261
|
-
run_id:
|
|
530
|
+
run_id: run.id,
|
|
262
531
|
attachments: [],
|
|
263
532
|
metadata: {},
|
|
264
533
|
};
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
534
|
+
this.emit(state, 'thread.message.created', inProgress);
|
|
535
|
+
this.emit(state, 'thread.message.in_progress', inProgress);
|
|
536
|
+
};
|
|
537
|
+
let accumulated = '';
|
|
268
538
|
const result = await this.cogitator.run(agent, {
|
|
269
|
-
input
|
|
539
|
+
input,
|
|
540
|
+
images,
|
|
270
541
|
threadId,
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
},
|
|
288
|
-
};
|
|
289
|
-
emitter.emit('event', 'thread.message.delta', delta);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
: undefined,
|
|
542
|
+
signal,
|
|
543
|
+
stream: !!request.stream,
|
|
544
|
+
parallelToolCalls: run.parallel_tool_calls,
|
|
545
|
+
...(hasHistory && { loadHistory: false }),
|
|
546
|
+
onToken: (token) => {
|
|
547
|
+
if (signal.aborted)
|
|
548
|
+
return;
|
|
549
|
+
announceMessage();
|
|
550
|
+
accumulated += token;
|
|
551
|
+
const delta = {
|
|
552
|
+
id: messageId,
|
|
553
|
+
object: 'thread.message.delta',
|
|
554
|
+
delta: { content: [{ index: 0, type: 'text', text: { value: token } }] },
|
|
555
|
+
};
|
|
556
|
+
this.emit(state, 'thread.message.delta', delta);
|
|
557
|
+
},
|
|
293
558
|
});
|
|
294
|
-
if (
|
|
295
|
-
|
|
559
|
+
if (signal.aborted) {
|
|
560
|
+
throw signal.reason instanceof Error ? signal.reason : new RunTerminatedError('cancelled');
|
|
296
561
|
}
|
|
297
|
-
const finalContent = result.output ||
|
|
562
|
+
const finalContent = result.output || accumulated;
|
|
298
563
|
if (finalContent) {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
564
|
+
announceMessage();
|
|
565
|
+
const message = await this.threadManager.addAssistantMessage(threadId, finalContent, assistant.id, run.id, messageId);
|
|
566
|
+
if (message) {
|
|
567
|
+
this.emit(state, 'thread.message.completed', message);
|
|
302
568
|
}
|
|
303
569
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
570
|
+
run.status = 'completed';
|
|
571
|
+
run.completed_at = nowSeconds();
|
|
572
|
+
run.usage = result.usage
|
|
307
573
|
? {
|
|
308
574
|
prompt_tokens: result.usage.inputTokens,
|
|
309
575
|
completion_tokens: result.usage.outputTokens,
|
|
310
576
|
total_tokens: result.usage.totalTokens,
|
|
311
577
|
}
|
|
312
578
|
: null;
|
|
313
|
-
|
|
314
|
-
emitter?.emit('event', 'done', '[DONE]');
|
|
315
|
-
emitter?.emit('end');
|
|
579
|
+
this.emit(state, 'thread.run.completed', run);
|
|
316
580
|
}
|
|
317
581
|
catch (error) {
|
|
318
|
-
|
|
319
|
-
|
|
582
|
+
const reason = signal.aborted ? signal.reason : error;
|
|
583
|
+
if (reason instanceof RunTerminatedError) {
|
|
584
|
+
if (reason.status === 'cancelled') {
|
|
585
|
+
run.status = 'cancelled';
|
|
586
|
+
run.cancelled_at = nowSeconds();
|
|
587
|
+
this.emit(state, 'thread.run.cancelled', run);
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
run.status = 'expired';
|
|
591
|
+
this.emit(state, 'thread.run.expired', run);
|
|
592
|
+
}
|
|
320
593
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
594
|
+
else {
|
|
595
|
+
run.status = 'failed';
|
|
596
|
+
run.failed_at = nowSeconds();
|
|
597
|
+
run.last_error = {
|
|
598
|
+
code: 'server_error',
|
|
599
|
+
message: error instanceof Error ? error.message : String(error),
|
|
600
|
+
};
|
|
601
|
+
this.emit(state, 'thread.run.failed', run);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
finally {
|
|
605
|
+
run.required_action = null;
|
|
606
|
+
this.terminate(state, new RunTerminatedError('cancelled'));
|
|
607
|
+
this.emit(state, 'done', '[DONE]');
|
|
608
|
+
this.end(state);
|
|
330
609
|
}
|
|
331
610
|
}
|
|
332
611
|
}
|