@hyorman/copilot-proxy-core 1.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/out/assistants/index.d.ts +15 -0
- package/out/assistants/index.js +16 -0
- package/out/assistants/index.js.map +1 -0
- package/out/assistants/routes.d.ts +16 -0
- package/out/assistants/routes.js +597 -0
- package/out/assistants/routes.js.map +1 -0
- package/out/assistants/runner.d.ts +48 -0
- package/out/assistants/runner.js +851 -0
- package/out/assistants/runner.js.map +1 -0
- package/out/assistants/state.d.ts +81 -0
- package/out/assistants/state.js +351 -0
- package/out/assistants/state.js.map +1 -0
- package/out/assistants/tools.d.ts +4 -0
- package/out/assistants/tools.js +8 -0
- package/out/assistants/tools.js.map +1 -0
- package/out/assistants/types.d.ts +254 -0
- package/out/assistants/types.js +5 -0
- package/out/assistants/types.js.map +1 -0
- package/out/backend.d.ts +24 -0
- package/out/backend.js +12 -0
- package/out/backend.js.map +1 -0
- package/out/index.d.ts +13 -0
- package/out/index.js +21 -0
- package/out/index.js.map +1 -0
- package/out/server.d.ts +12 -0
- package/out/server.js +504 -0
- package/out/server.js.map +1 -0
- package/out/skills/index.d.ts +3 -0
- package/out/skills/index.js +4 -0
- package/out/skills/index.js.map +1 -0
- package/out/skills/manifest.d.ts +25 -0
- package/out/skills/manifest.js +96 -0
- package/out/skills/manifest.js.map +1 -0
- package/out/skills/resolver.d.ts +22 -0
- package/out/skills/resolver.js +66 -0
- package/out/skills/resolver.js.map +1 -0
- package/out/skills/routes.d.ts +3 -0
- package/out/skills/routes.js +191 -0
- package/out/skills/routes.js.map +1 -0
- package/out/skills/state.d.ts +35 -0
- package/out/skills/state.js +155 -0
- package/out/skills/state.js.map +1 -0
- package/out/skills/storage.d.ts +30 -0
- package/out/skills/storage.js +171 -0
- package/out/skills/storage.js.map +1 -0
- package/out/skills/types.d.ts +141 -0
- package/out/skills/types.js +8 -0
- package/out/skills/types.js.map +1 -0
- package/out/toolConvert.d.ts +24 -0
- package/out/toolConvert.js +56 -0
- package/out/toolConvert.js.map +1 -0
- package/out/types.d.ts +291 -0
- package/out/types.js +2 -0
- package/out/types.js.map +1 -0
- package/out/utils.d.ts +28 -0
- package/out/utils.js +81 -0
- package/out/utils.js.map +1 -0
- package/package.json +36 -0
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run Execution Engine
|
|
3
|
+
*
|
|
4
|
+
* Executes runs with support for:
|
|
5
|
+
* - Streaming mode (yields SSE events)
|
|
6
|
+
* - Non-streaming mode (returns promise)
|
|
7
|
+
* - Run steps tracking
|
|
8
|
+
* - Cancellation support
|
|
9
|
+
* - Tool calling (prompt-based)
|
|
10
|
+
*
|
|
11
|
+
* The executeRun function is a generator that yields StreamEvent objects.
|
|
12
|
+
* For non-streaming, consume all events and ignore them.
|
|
13
|
+
* For streaming, pipe events to SSE response.
|
|
14
|
+
*/
|
|
15
|
+
import { state } from './state.js';
|
|
16
|
+
import { createMessage } from '../utils.js';
|
|
17
|
+
import { assistantToolsToFunctionTools } from '../toolConvert.js';
|
|
18
|
+
import { resolveSkills, buildSkillInstructions } from '../skills/resolver.js';
|
|
19
|
+
let backend = null;
|
|
20
|
+
export function setRunnerBackend(b) {
|
|
21
|
+
backend = b;
|
|
22
|
+
}
|
|
23
|
+
function getBackend() {
|
|
24
|
+
if (!backend) {
|
|
25
|
+
throw new Error('Runner backend not initialized. Call setRunnerBackend() first.');
|
|
26
|
+
}
|
|
27
|
+
return backend;
|
|
28
|
+
}
|
|
29
|
+
// Active runs that can be cancelled
|
|
30
|
+
const activeRuns = new Map();
|
|
31
|
+
/**
|
|
32
|
+
* Extract text content from MessageContent array
|
|
33
|
+
*/
|
|
34
|
+
function extractTextFromContent(content) {
|
|
35
|
+
return content
|
|
36
|
+
.filter((c) => c.type === 'text')
|
|
37
|
+
.map(c => c.text.value)
|
|
38
|
+
.join('\n');
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Create a stream event
|
|
42
|
+
*/
|
|
43
|
+
function createEvent(event, data) {
|
|
44
|
+
return { event, data };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Execute a run as an async generator
|
|
48
|
+
* Yields StreamEvent objects for SSE streaming
|
|
49
|
+
*
|
|
50
|
+
* @param threadId - The thread ID
|
|
51
|
+
* @param runId - The run ID
|
|
52
|
+
* @param streaming - Whether to yield intermediate events
|
|
53
|
+
*/
|
|
54
|
+
export async function* executeRun(threadId, runId, streaming = false) {
|
|
55
|
+
const runKey = `${threadId}:${runId}`;
|
|
56
|
+
activeRuns.set(runKey, { cancelled: false });
|
|
57
|
+
try {
|
|
58
|
+
const run = state.getRun(threadId, runId);
|
|
59
|
+
const thread = state.getThread(threadId);
|
|
60
|
+
if (!run || !thread) {
|
|
61
|
+
state.updateRun(threadId, runId, {
|
|
62
|
+
status: 'failed',
|
|
63
|
+
failed_at: Math.floor(Date.now() / 1000),
|
|
64
|
+
last_error: { code: 'server_error', message: 'Thread or run not found' }
|
|
65
|
+
});
|
|
66
|
+
if (streaming) {
|
|
67
|
+
yield createEvent('thread.run.failed', state.getRun(threadId, runId));
|
|
68
|
+
yield createEvent('done', '[DONE]');
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const assistant = state.getAssistant(run.assistant_id);
|
|
73
|
+
if (!assistant) {
|
|
74
|
+
state.updateRun(threadId, runId, {
|
|
75
|
+
status: 'failed',
|
|
76
|
+
failed_at: Math.floor(Date.now() / 1000),
|
|
77
|
+
last_error: { code: 'server_error', message: 'Assistant not found' }
|
|
78
|
+
});
|
|
79
|
+
if (streaming) {
|
|
80
|
+
yield createEvent('thread.run.failed', state.getRun(threadId, runId));
|
|
81
|
+
yield createEvent('done', '[DONE]');
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// Check for cancellation
|
|
86
|
+
if (activeRuns.get(runKey)?.cancelled) {
|
|
87
|
+
state.updateRun(threadId, runId, {
|
|
88
|
+
status: 'cancelled',
|
|
89
|
+
cancelled_at: Math.floor(Date.now() / 1000)
|
|
90
|
+
});
|
|
91
|
+
if (streaming) {
|
|
92
|
+
yield createEvent('thread.run.cancelled', state.getRun(threadId, runId));
|
|
93
|
+
yield createEvent('done', '[DONE]');
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// Emit run queued event
|
|
98
|
+
if (streaming) {
|
|
99
|
+
yield createEvent('thread.run.queued', run);
|
|
100
|
+
}
|
|
101
|
+
// Mark as in progress
|
|
102
|
+
state.updateRun(threadId, runId, {
|
|
103
|
+
status: 'in_progress',
|
|
104
|
+
started_at: Math.floor(Date.now() / 1000)
|
|
105
|
+
});
|
|
106
|
+
if (streaming) {
|
|
107
|
+
yield createEvent('thread.run.in_progress', state.getRun(threadId, runId));
|
|
108
|
+
}
|
|
109
|
+
// Build messages array from thread
|
|
110
|
+
const threadMessages = state.getMessages(threadId, { order: 'asc' });
|
|
111
|
+
const chatMessages = [];
|
|
112
|
+
// Build system instructions (assistant instructions + run overrides)
|
|
113
|
+
// Tools are passed natively via processChatRequest, not injected into system prompt
|
|
114
|
+
let systemContent = '';
|
|
115
|
+
if (assistant.instructions) {
|
|
116
|
+
systemContent += assistant.instructions;
|
|
117
|
+
}
|
|
118
|
+
if (run.instructions) {
|
|
119
|
+
systemContent += (systemContent ? '\n\n' : '') + run.instructions;
|
|
120
|
+
}
|
|
121
|
+
// Resolve and inject skills
|
|
122
|
+
const skills = run.skills.length > 0 ? run.skills : assistant.skills;
|
|
123
|
+
if (skills.length > 0) {
|
|
124
|
+
try {
|
|
125
|
+
const resolved = resolveSkills(skills);
|
|
126
|
+
if (resolved.length > 0) {
|
|
127
|
+
systemContent += (systemContent ? '\n\n' : '') + buildSkillInstructions(resolved);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
console.warn('Failed to resolve skills for run', runId, err);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Get tools for native passing
|
|
135
|
+
const tools = run.tools.length > 0 ? run.tools : assistant.tools;
|
|
136
|
+
const functionTools = assistantToolsToFunctionTools(tools);
|
|
137
|
+
// Convert thread messages to chat messages
|
|
138
|
+
// Prepend system content to the first user message
|
|
139
|
+
let systemPrepended = false;
|
|
140
|
+
for (const msg of threadMessages.data) {
|
|
141
|
+
const textContent = extractTextFromContent(msg.content);
|
|
142
|
+
if (msg.role === 'user' && !systemPrepended && systemContent) {
|
|
143
|
+
// Prepend system instructions to first user message
|
|
144
|
+
chatMessages.push({
|
|
145
|
+
role: 'user',
|
|
146
|
+
content: `${systemContent}\n\n---\n\n${textContent}`
|
|
147
|
+
});
|
|
148
|
+
systemPrepended = true;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
chatMessages.push({
|
|
152
|
+
role: msg.role,
|
|
153
|
+
content: textContent
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// If no user messages but we have system content, add it as a user message
|
|
158
|
+
if (!systemPrepended && systemContent) {
|
|
159
|
+
chatMessages.unshift({
|
|
160
|
+
role: 'user',
|
|
161
|
+
content: systemContent
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
// Check for cancellation before calling LLM
|
|
165
|
+
if (activeRuns.get(runKey)?.cancelled) {
|
|
166
|
+
state.updateRun(threadId, runId, {
|
|
167
|
+
status: 'cancelled',
|
|
168
|
+
cancelled_at: Math.floor(Date.now() / 1000)
|
|
169
|
+
});
|
|
170
|
+
if (streaming) {
|
|
171
|
+
yield createEvent('thread.run.cancelled', state.getRun(threadId, runId));
|
|
172
|
+
yield createEvent('done', '[DONE]');
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
// Create message_creation run step
|
|
177
|
+
const stepId = state.generateStepId();
|
|
178
|
+
const messageId = state.generateMessageId();
|
|
179
|
+
const runStep = {
|
|
180
|
+
id: stepId,
|
|
181
|
+
object: 'thread.run.step',
|
|
182
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
183
|
+
run_id: runId,
|
|
184
|
+
assistant_id: assistant.id,
|
|
185
|
+
thread_id: threadId,
|
|
186
|
+
type: 'message_creation',
|
|
187
|
+
status: 'in_progress',
|
|
188
|
+
cancelled_at: null,
|
|
189
|
+
completed_at: null,
|
|
190
|
+
expired_at: null,
|
|
191
|
+
failed_at: null,
|
|
192
|
+
last_error: null,
|
|
193
|
+
step_details: {
|
|
194
|
+
type: 'message_creation',
|
|
195
|
+
message_creation: {
|
|
196
|
+
message_id: messageId
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
usage: null
|
|
200
|
+
};
|
|
201
|
+
state.addRunStep(runId, runStep);
|
|
202
|
+
if (streaming) {
|
|
203
|
+
yield createEvent('thread.run.step.created', runStep);
|
|
204
|
+
yield createEvent('thread.run.step.in_progress', runStep);
|
|
205
|
+
}
|
|
206
|
+
// Build request - use streaming mode if requested, pass tools natively
|
|
207
|
+
const request = {
|
|
208
|
+
model: run.model || assistant.model,
|
|
209
|
+
messages: chatMessages,
|
|
210
|
+
stream: streaming,
|
|
211
|
+
...(functionTools.length > 0 ? { tools: functionTools } : {}),
|
|
212
|
+
};
|
|
213
|
+
let fullContent = '';
|
|
214
|
+
let promptTokens = 0;
|
|
215
|
+
let completionTokens = 0;
|
|
216
|
+
if (streaming) {
|
|
217
|
+
// Streaming mode: yield message deltas
|
|
218
|
+
const streamIterator = await getBackend().processChatRequest(request);
|
|
219
|
+
// Create message in progress
|
|
220
|
+
const assistantMessage = createMessage({
|
|
221
|
+
threadId,
|
|
222
|
+
messageId,
|
|
223
|
+
content: '',
|
|
224
|
+
role: 'assistant',
|
|
225
|
+
assistantId: assistant.id,
|
|
226
|
+
runId,
|
|
227
|
+
status: 'in_progress',
|
|
228
|
+
});
|
|
229
|
+
state.addMessage(threadId, assistantMessage);
|
|
230
|
+
yield createEvent('thread.message.created', assistantMessage);
|
|
231
|
+
yield createEvent('thread.message.in_progress', assistantMessage);
|
|
232
|
+
let deltaIndex = 0;
|
|
233
|
+
const accumulatedToolCalls = [];
|
|
234
|
+
for await (const chunk of streamIterator) {
|
|
235
|
+
// Check for cancellation
|
|
236
|
+
if (activeRuns.get(runKey)?.cancelled) {
|
|
237
|
+
state.updateRun(threadId, runId, {
|
|
238
|
+
status: 'cancelled',
|
|
239
|
+
cancelled_at: Math.floor(Date.now() / 1000)
|
|
240
|
+
});
|
|
241
|
+
state.updateRunStep(runId, stepId, {
|
|
242
|
+
status: 'cancelled',
|
|
243
|
+
cancelled_at: Math.floor(Date.now() / 1000)
|
|
244
|
+
});
|
|
245
|
+
yield createEvent('thread.run.step.cancelled', state.getRunStep(runId, stepId));
|
|
246
|
+
yield createEvent('thread.run.cancelled', state.getRun(threadId, runId));
|
|
247
|
+
yield createEvent('done', '[DONE]');
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const content = chunk.choices[0]?.delta?.content ?? '';
|
|
251
|
+
if (content) {
|
|
252
|
+
fullContent += content;
|
|
253
|
+
// Emit message delta
|
|
254
|
+
const delta = {
|
|
255
|
+
id: messageId,
|
|
256
|
+
object: 'thread.message.delta',
|
|
257
|
+
delta: {
|
|
258
|
+
content: [{
|
|
259
|
+
index: deltaIndex,
|
|
260
|
+
type: 'text',
|
|
261
|
+
text: {
|
|
262
|
+
value: content
|
|
263
|
+
}
|
|
264
|
+
}]
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
yield createEvent('thread.message.delta', delta);
|
|
268
|
+
deltaIndex++;
|
|
269
|
+
}
|
|
270
|
+
// Check for native tool calls in delta
|
|
271
|
+
const chunkToolCalls = chunk.choices[0]?.delta?.tool_calls;
|
|
272
|
+
if (chunkToolCalls) {
|
|
273
|
+
for (const tc of chunkToolCalls) {
|
|
274
|
+
if (tc.id && tc.function?.name) {
|
|
275
|
+
accumulatedToolCalls.push({
|
|
276
|
+
id: tc.id,
|
|
277
|
+
type: 'function',
|
|
278
|
+
function: {
|
|
279
|
+
name: tc.function.name,
|
|
280
|
+
arguments: tc.function.arguments ?? '{}',
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// Check for finish reason
|
|
287
|
+
if (chunk.choices[0]?.finish_reason === 'stop' || chunk.choices[0]?.finish_reason === 'tool_calls') {
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Estimate tokens (rough approximation)
|
|
292
|
+
completionTokens = fullContent.length;
|
|
293
|
+
promptTokens = chatMessages.reduce((sum, m) => sum + (typeof m.content === 'string' ? m.content.length : 0), 0);
|
|
294
|
+
// Update message with full content
|
|
295
|
+
state.updateMessage(threadId, messageId, {
|
|
296
|
+
status: 'completed',
|
|
297
|
+
completed_at: Math.floor(Date.now() / 1000),
|
|
298
|
+
content: [{
|
|
299
|
+
type: 'text',
|
|
300
|
+
text: {
|
|
301
|
+
value: fullContent,
|
|
302
|
+
annotations: []
|
|
303
|
+
}
|
|
304
|
+
}]
|
|
305
|
+
});
|
|
306
|
+
yield createEvent('thread.message.completed', state.getMessage(threadId, messageId));
|
|
307
|
+
// Handle tool calls detected during streaming
|
|
308
|
+
if (accumulatedToolCalls.length > 0) {
|
|
309
|
+
// Complete the message_creation step
|
|
310
|
+
state.updateRunStep(runId, stepId, {
|
|
311
|
+
status: 'completed',
|
|
312
|
+
completed_at: Math.floor(Date.now() / 1000)
|
|
313
|
+
});
|
|
314
|
+
// Create tool_calls run step
|
|
315
|
+
const toolStepId = state.generateStepId();
|
|
316
|
+
const toolStep = {
|
|
317
|
+
id: toolStepId,
|
|
318
|
+
object: 'thread.run.step',
|
|
319
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
320
|
+
run_id: runId,
|
|
321
|
+
assistant_id: assistant.id,
|
|
322
|
+
thread_id: threadId,
|
|
323
|
+
type: 'tool_calls',
|
|
324
|
+
status: 'in_progress',
|
|
325
|
+
cancelled_at: null,
|
|
326
|
+
completed_at: null,
|
|
327
|
+
expired_at: null,
|
|
328
|
+
failed_at: null,
|
|
329
|
+
last_error: null,
|
|
330
|
+
step_details: {
|
|
331
|
+
type: 'tool_calls',
|
|
332
|
+
tool_calls: accumulatedToolCalls
|
|
333
|
+
},
|
|
334
|
+
usage: null
|
|
335
|
+
};
|
|
336
|
+
state.addRunStep(runId, toolStep);
|
|
337
|
+
// Save context for when tool outputs are submitted
|
|
338
|
+
const pendingContext = {
|
|
339
|
+
runId,
|
|
340
|
+
threadId,
|
|
341
|
+
toolCalls: accumulatedToolCalls,
|
|
342
|
+
partialContent: fullContent,
|
|
343
|
+
stepId: toolStepId
|
|
344
|
+
};
|
|
345
|
+
state.setPendingToolContext(runId, pendingContext);
|
|
346
|
+
// Update run to requires_action
|
|
347
|
+
state.updateRun(threadId, runId, {
|
|
348
|
+
status: 'requires_action',
|
|
349
|
+
required_action: {
|
|
350
|
+
type: 'submit_tool_outputs',
|
|
351
|
+
submit_tool_outputs: {
|
|
352
|
+
tool_calls: accumulatedToolCalls
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
yield createEvent('thread.run.step.created', toolStep);
|
|
357
|
+
yield createEvent('thread.run.requires_action', state.getRun(threadId, runId));
|
|
358
|
+
yield createEvent('done', '[DONE]');
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
// Non-streaming mode
|
|
364
|
+
const response = await getBackend().processChatRequest(request);
|
|
365
|
+
// Check for cancellation after LLM response
|
|
366
|
+
if (activeRuns.get(runKey)?.cancelled) {
|
|
367
|
+
state.updateRun(threadId, runId, {
|
|
368
|
+
status: 'cancelled',
|
|
369
|
+
cancelled_at: Math.floor(Date.now() / 1000)
|
|
370
|
+
});
|
|
371
|
+
state.updateRunStep(runId, stepId, {
|
|
372
|
+
status: 'cancelled',
|
|
373
|
+
cancelled_at: Math.floor(Date.now() / 1000)
|
|
374
|
+
});
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
// Extract response content
|
|
378
|
+
const responseContent = response.choices[0]?.message?.content;
|
|
379
|
+
if (!responseContent) {
|
|
380
|
+
state.updateRun(threadId, runId, {
|
|
381
|
+
status: 'failed',
|
|
382
|
+
failed_at: Math.floor(Date.now() / 1000),
|
|
383
|
+
last_error: { code: 'server_error', message: 'Empty response from model' }
|
|
384
|
+
});
|
|
385
|
+
state.updateRunStep(runId, stepId, {
|
|
386
|
+
status: 'failed',
|
|
387
|
+
failed_at: Math.floor(Date.now() / 1000),
|
|
388
|
+
last_error: { code: 'server_error', message: 'Empty response from model' }
|
|
389
|
+
});
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
fullContent = typeof responseContent === 'string'
|
|
393
|
+
? responseContent
|
|
394
|
+
: JSON.stringify(responseContent);
|
|
395
|
+
promptTokens = response.usage?.prompt_tokens ?? 0;
|
|
396
|
+
completionTokens = response.usage?.completion_tokens ?? fullContent.length;
|
|
397
|
+
// Check for native tool calls in the response
|
|
398
|
+
const responseToolCalls = response.choices?.[0]?.message?.tool_calls;
|
|
399
|
+
if (responseToolCalls && responseToolCalls.length > 0) {
|
|
400
|
+
// Complete the message_creation step (with partial content if any)
|
|
401
|
+
state.updateRunStep(runId, stepId, {
|
|
402
|
+
status: 'completed',
|
|
403
|
+
completed_at: Math.floor(Date.now() / 1000)
|
|
404
|
+
});
|
|
405
|
+
// Create tool_calls run step
|
|
406
|
+
const toolStepId = state.generateStepId();
|
|
407
|
+
const toolStep = {
|
|
408
|
+
id: toolStepId,
|
|
409
|
+
object: 'thread.run.step',
|
|
410
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
411
|
+
run_id: runId,
|
|
412
|
+
assistant_id: assistant.id,
|
|
413
|
+
thread_id: threadId,
|
|
414
|
+
type: 'tool_calls',
|
|
415
|
+
status: 'in_progress',
|
|
416
|
+
cancelled_at: null,
|
|
417
|
+
completed_at: null,
|
|
418
|
+
expired_at: null,
|
|
419
|
+
failed_at: null,
|
|
420
|
+
last_error: null,
|
|
421
|
+
step_details: {
|
|
422
|
+
type: 'tool_calls',
|
|
423
|
+
tool_calls: responseToolCalls
|
|
424
|
+
},
|
|
425
|
+
usage: null
|
|
426
|
+
};
|
|
427
|
+
state.addRunStep(runId, toolStep);
|
|
428
|
+
// Save context for when tool outputs are submitted
|
|
429
|
+
const pendingContext = {
|
|
430
|
+
runId,
|
|
431
|
+
threadId,
|
|
432
|
+
toolCalls: responseToolCalls,
|
|
433
|
+
partialContent: fullContent,
|
|
434
|
+
stepId: toolStepId
|
|
435
|
+
};
|
|
436
|
+
state.setPendingToolContext(runId, pendingContext);
|
|
437
|
+
// Update run to requires_action
|
|
438
|
+
state.updateRun(threadId, runId, {
|
|
439
|
+
status: 'requires_action',
|
|
440
|
+
required_action: {
|
|
441
|
+
type: 'submit_tool_outputs',
|
|
442
|
+
submit_tool_outputs: {
|
|
443
|
+
tool_calls: responseToolCalls
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
// Don't create message yet - wait for tool outputs
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
// Create assistant message
|
|
451
|
+
const assistantMessage = createMessage({
|
|
452
|
+
threadId,
|
|
453
|
+
messageId,
|
|
454
|
+
content: fullContent,
|
|
455
|
+
role: 'assistant',
|
|
456
|
+
assistantId: assistant.id,
|
|
457
|
+
runId,
|
|
458
|
+
});
|
|
459
|
+
state.addMessage(threadId, assistantMessage);
|
|
460
|
+
}
|
|
461
|
+
// Update run step as completed
|
|
462
|
+
const usage = {
|
|
463
|
+
prompt_tokens: promptTokens,
|
|
464
|
+
completion_tokens: completionTokens,
|
|
465
|
+
total_tokens: promptTokens + completionTokens
|
|
466
|
+
};
|
|
467
|
+
state.updateRunStep(runId, stepId, {
|
|
468
|
+
status: 'completed',
|
|
469
|
+
completed_at: Math.floor(Date.now() / 1000),
|
|
470
|
+
usage
|
|
471
|
+
});
|
|
472
|
+
if (streaming) {
|
|
473
|
+
yield createEvent('thread.run.step.completed', state.getRunStep(runId, stepId));
|
|
474
|
+
}
|
|
475
|
+
// Mark run as completed
|
|
476
|
+
state.updateRun(threadId, runId, {
|
|
477
|
+
status: 'completed',
|
|
478
|
+
completed_at: Math.floor(Date.now() / 1000),
|
|
479
|
+
usage
|
|
480
|
+
});
|
|
481
|
+
if (streaming) {
|
|
482
|
+
yield createEvent('thread.run.completed', state.getRun(threadId, runId));
|
|
483
|
+
yield createEvent('done', '[DONE]');
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
console.error('Run execution error:', error);
|
|
488
|
+
state.updateRun(threadId, runId, {
|
|
489
|
+
status: 'failed',
|
|
490
|
+
failed_at: Math.floor(Date.now() / 1000),
|
|
491
|
+
last_error: {
|
|
492
|
+
code: 'server_error',
|
|
493
|
+
message: error instanceof Error ? error.message : 'Unknown error'
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
if (streaming) {
|
|
497
|
+
yield createEvent('error', {
|
|
498
|
+
error: {
|
|
499
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
500
|
+
code: 'server_error'
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
yield createEvent('thread.run.failed', state.getRun(threadId, runId));
|
|
504
|
+
yield createEvent('done', '[DONE]');
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
finally {
|
|
508
|
+
activeRuns.delete(runKey);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Execute run without streaming (convenience wrapper)
|
|
513
|
+
* Consumes all events and returns when complete
|
|
514
|
+
*/
|
|
515
|
+
export async function executeRunNonStreaming(threadId, runId) {
|
|
516
|
+
const generator = executeRun(threadId, runId, false);
|
|
517
|
+
// Consume all events
|
|
518
|
+
for await (const _ of generator) {
|
|
519
|
+
// Discard events in non-streaming mode
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Request cancellation of a run
|
|
524
|
+
*/
|
|
525
|
+
export function requestRunCancellation(threadId, runId) {
|
|
526
|
+
const runKey = `${threadId}:${runId}`;
|
|
527
|
+
const activeRun = activeRuns.get(runKey);
|
|
528
|
+
if (activeRun) {
|
|
529
|
+
activeRun.cancelled = true;
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Check if a run is currently active
|
|
536
|
+
*/
|
|
537
|
+
export function isRunActive(threadId, runId) {
|
|
538
|
+
return activeRuns.has(`${threadId}:${runId}`);
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Continue a run after tool outputs have been submitted
|
|
542
|
+
* This resumes execution by adding tool results to the conversation and calling the model again
|
|
543
|
+
*/
|
|
544
|
+
export async function* continueRunWithToolOutputs(threadId, runId, toolOutputs, streaming = false) {
|
|
545
|
+
const runKey = `${threadId}:${runId}`;
|
|
546
|
+
activeRuns.set(runKey, { cancelled: false });
|
|
547
|
+
try {
|
|
548
|
+
const run = state.getRun(threadId, runId);
|
|
549
|
+
const pendingContext = state.getPendingToolContext(runId);
|
|
550
|
+
if (!run || !pendingContext) {
|
|
551
|
+
state.updateRun(threadId, runId, {
|
|
552
|
+
status: 'failed',
|
|
553
|
+
failed_at: Math.floor(Date.now() / 1000),
|
|
554
|
+
last_error: { code: 'server_error', message: 'Run or pending context not found' }
|
|
555
|
+
});
|
|
556
|
+
if (streaming) {
|
|
557
|
+
yield createEvent('thread.run.failed', state.getRun(threadId, runId));
|
|
558
|
+
yield createEvent('done', '[DONE]');
|
|
559
|
+
}
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
const assistant = state.getAssistant(run.assistant_id);
|
|
563
|
+
if (!assistant) {
|
|
564
|
+
state.updateRun(threadId, runId, {
|
|
565
|
+
status: 'failed',
|
|
566
|
+
failed_at: Math.floor(Date.now() / 1000),
|
|
567
|
+
last_error: { code: 'server_error', message: 'Assistant not found' }
|
|
568
|
+
});
|
|
569
|
+
if (streaming) {
|
|
570
|
+
yield createEvent('thread.run.failed', state.getRun(threadId, runId));
|
|
571
|
+
yield createEvent('done', '[DONE]');
|
|
572
|
+
}
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
// Update run status back to in_progress
|
|
576
|
+
state.updateRun(threadId, runId, {
|
|
577
|
+
status: 'in_progress',
|
|
578
|
+
required_action: null
|
|
579
|
+
});
|
|
580
|
+
if (streaming) {
|
|
581
|
+
yield createEvent('thread.run.in_progress', state.getRun(threadId, runId));
|
|
582
|
+
}
|
|
583
|
+
// Complete the tool_calls step
|
|
584
|
+
state.updateRunStep(runId, pendingContext.stepId, {
|
|
585
|
+
status: 'completed',
|
|
586
|
+
completed_at: Math.floor(Date.now() / 1000)
|
|
587
|
+
});
|
|
588
|
+
if (streaming) {
|
|
589
|
+
yield createEvent('thread.run.step.completed', state.getRunStep(runId, pendingContext.stepId));
|
|
590
|
+
}
|
|
591
|
+
// Build messages array including tool results
|
|
592
|
+
const threadMessages = state.getMessages(threadId, { order: 'asc' });
|
|
593
|
+
const chatMessages = [];
|
|
594
|
+
// Build system instructions
|
|
595
|
+
let systemContent = '';
|
|
596
|
+
if (assistant.instructions) {
|
|
597
|
+
systemContent += assistant.instructions;
|
|
598
|
+
}
|
|
599
|
+
if (run.instructions) {
|
|
600
|
+
systemContent += (systemContent ? '\n\n' : '') + run.instructions;
|
|
601
|
+
}
|
|
602
|
+
// Resolve and inject skills
|
|
603
|
+
const skillAttachments = run.skills.length > 0 ? run.skills : assistant.skills;
|
|
604
|
+
if (skillAttachments.length > 0) {
|
|
605
|
+
try {
|
|
606
|
+
const resolved = resolveSkills(skillAttachments);
|
|
607
|
+
if (resolved.length > 0) {
|
|
608
|
+
systemContent += (systemContent ? '\n\n' : '') + buildSkillInstructions(resolved);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
catch (err) {
|
|
612
|
+
console.warn('Failed to resolve skills for run', runId, err);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
// Get tools for native passing
|
|
616
|
+
const tools = run.tools.length > 0 ? run.tools : assistant.tools;
|
|
617
|
+
const functionTools = assistantToolsToFunctionTools(tools);
|
|
618
|
+
// Convert thread messages to chat messages
|
|
619
|
+
let systemPrepended = false;
|
|
620
|
+
for (const msg of threadMessages.data) {
|
|
621
|
+
const textContent = extractTextFromContent(msg.content);
|
|
622
|
+
if (msg.role === 'user' && !systemPrepended && systemContent) {
|
|
623
|
+
chatMessages.push({
|
|
624
|
+
role: 'user',
|
|
625
|
+
content: `${systemContent}\n\n---\n\n${textContent}`
|
|
626
|
+
});
|
|
627
|
+
systemPrepended = true;
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
chatMessages.push({
|
|
631
|
+
role: msg.role,
|
|
632
|
+
content: textContent
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
// If no user messages but we have system content, add it
|
|
637
|
+
if (!systemPrepended && systemContent) {
|
|
638
|
+
chatMessages.unshift({
|
|
639
|
+
role: 'user',
|
|
640
|
+
content: systemContent
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
// Add the assistant message with tool calls (as native tool call parts)
|
|
644
|
+
if (pendingContext.toolCalls.length > 0) {
|
|
645
|
+
if (pendingContext.partialContent) {
|
|
646
|
+
// Include partial content with the tool call assistant message
|
|
647
|
+
}
|
|
648
|
+
// Add assistant message with tool_calls for the conversation history
|
|
649
|
+
chatMessages.push({
|
|
650
|
+
role: 'assistant',
|
|
651
|
+
content: pendingContext.partialContent || null,
|
|
652
|
+
tool_calls: pendingContext.toolCalls,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
// Add tool results as individual tool messages
|
|
656
|
+
for (const output of toolOutputs) {
|
|
657
|
+
chatMessages.push({
|
|
658
|
+
role: 'tool',
|
|
659
|
+
tool_call_id: output.tool_call_id,
|
|
660
|
+
content: output.output,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
// Clear pending context
|
|
664
|
+
state.deletePendingToolContext(runId);
|
|
665
|
+
// Create new message_creation step for the continuation
|
|
666
|
+
const stepId = state.generateStepId();
|
|
667
|
+
const messageId = state.generateMessageId();
|
|
668
|
+
const runStep = {
|
|
669
|
+
id: stepId,
|
|
670
|
+
object: 'thread.run.step',
|
|
671
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
672
|
+
run_id: runId,
|
|
673
|
+
assistant_id: assistant.id,
|
|
674
|
+
thread_id: threadId,
|
|
675
|
+
type: 'message_creation',
|
|
676
|
+
status: 'in_progress',
|
|
677
|
+
cancelled_at: null,
|
|
678
|
+
completed_at: null,
|
|
679
|
+
expired_at: null,
|
|
680
|
+
failed_at: null,
|
|
681
|
+
last_error: null,
|
|
682
|
+
step_details: {
|
|
683
|
+
type: 'message_creation',
|
|
684
|
+
message_creation: {
|
|
685
|
+
message_id: messageId
|
|
686
|
+
}
|
|
687
|
+
},
|
|
688
|
+
usage: null
|
|
689
|
+
};
|
|
690
|
+
state.addRunStep(runId, runStep);
|
|
691
|
+
if (streaming) {
|
|
692
|
+
yield createEvent('thread.run.step.created', runStep);
|
|
693
|
+
yield createEvent('thread.run.step.in_progress', runStep);
|
|
694
|
+
}
|
|
695
|
+
// Build request with native tool support
|
|
696
|
+
const request = {
|
|
697
|
+
model: run.model || assistant.model,
|
|
698
|
+
messages: chatMessages,
|
|
699
|
+
stream: streaming,
|
|
700
|
+
...(functionTools.length > 0 ? { tools: functionTools } : {}),
|
|
701
|
+
};
|
|
702
|
+
let fullContent = '';
|
|
703
|
+
let promptTokens = 0;
|
|
704
|
+
let completionTokens = 0;
|
|
705
|
+
// Non-streaming continuation
|
|
706
|
+
const response = await getBackend().processChatRequest(request);
|
|
707
|
+
const responseContent = response.choices[0]?.message?.content;
|
|
708
|
+
if (!responseContent) {
|
|
709
|
+
state.updateRun(threadId, runId, {
|
|
710
|
+
status: 'failed',
|
|
711
|
+
failed_at: Math.floor(Date.now() / 1000),
|
|
712
|
+
last_error: { code: 'server_error', message: 'Empty response from model' }
|
|
713
|
+
});
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
fullContent = typeof responseContent === 'string'
|
|
717
|
+
? responseContent
|
|
718
|
+
: JSON.stringify(responseContent);
|
|
719
|
+
promptTokens = response.usage?.prompt_tokens ?? 0;
|
|
720
|
+
completionTokens = response.usage?.completion_tokens ?? fullContent.length;
|
|
721
|
+
// Check for more tool calls (native)
|
|
722
|
+
const responseToolCalls = response.choices?.[0]?.message?.tool_calls;
|
|
723
|
+
if (responseToolCalls && responseToolCalls.length > 0) {
|
|
724
|
+
// Complete the message_creation step
|
|
725
|
+
state.updateRunStep(runId, stepId, {
|
|
726
|
+
status: 'completed',
|
|
727
|
+
completed_at: Math.floor(Date.now() / 1000)
|
|
728
|
+
});
|
|
729
|
+
// Create new tool_calls step
|
|
730
|
+
const toolStepId = state.generateStepId();
|
|
731
|
+
const toolStep = {
|
|
732
|
+
id: toolStepId,
|
|
733
|
+
object: 'thread.run.step',
|
|
734
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
735
|
+
run_id: runId,
|
|
736
|
+
assistant_id: assistant.id,
|
|
737
|
+
thread_id: threadId,
|
|
738
|
+
type: 'tool_calls',
|
|
739
|
+
status: 'in_progress',
|
|
740
|
+
cancelled_at: null,
|
|
741
|
+
completed_at: null,
|
|
742
|
+
expired_at: null,
|
|
743
|
+
failed_at: null,
|
|
744
|
+
last_error: null,
|
|
745
|
+
step_details: {
|
|
746
|
+
type: 'tool_calls',
|
|
747
|
+
tool_calls: responseToolCalls
|
|
748
|
+
},
|
|
749
|
+
usage: null
|
|
750
|
+
};
|
|
751
|
+
state.addRunStep(runId, toolStep);
|
|
752
|
+
// Save context for next round
|
|
753
|
+
const newPendingContext = {
|
|
754
|
+
runId,
|
|
755
|
+
threadId,
|
|
756
|
+
toolCalls: responseToolCalls,
|
|
757
|
+
partialContent: fullContent,
|
|
758
|
+
stepId: toolStepId
|
|
759
|
+
};
|
|
760
|
+
state.setPendingToolContext(runId, newPendingContext);
|
|
761
|
+
// Update run to requires_action again
|
|
762
|
+
state.updateRun(threadId, runId, {
|
|
763
|
+
status: 'requires_action',
|
|
764
|
+
required_action: {
|
|
765
|
+
type: 'submit_tool_outputs',
|
|
766
|
+
submit_tool_outputs: {
|
|
767
|
+
tool_calls: responseToolCalls
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
if (streaming) {
|
|
772
|
+
yield createEvent('thread.run.step.created', toolStep);
|
|
773
|
+
yield createEvent('thread.run.requires_action', state.getRun(threadId, runId));
|
|
774
|
+
yield createEvent('done', '[DONE]');
|
|
775
|
+
}
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
// Create assistant message
|
|
779
|
+
const assistantMessage = createMessage({
|
|
780
|
+
threadId,
|
|
781
|
+
messageId,
|
|
782
|
+
content: fullContent,
|
|
783
|
+
role: 'assistant',
|
|
784
|
+
assistantId: assistant.id,
|
|
785
|
+
runId,
|
|
786
|
+
});
|
|
787
|
+
state.addMessage(threadId, assistantMessage);
|
|
788
|
+
if (streaming) {
|
|
789
|
+
yield createEvent('thread.message.created', assistantMessage);
|
|
790
|
+
yield createEvent('thread.message.completed', assistantMessage);
|
|
791
|
+
}
|
|
792
|
+
// Update run step
|
|
793
|
+
const usage = {
|
|
794
|
+
prompt_tokens: promptTokens,
|
|
795
|
+
completion_tokens: completionTokens,
|
|
796
|
+
total_tokens: promptTokens + completionTokens
|
|
797
|
+
};
|
|
798
|
+
state.updateRunStep(runId, stepId, {
|
|
799
|
+
status: 'completed',
|
|
800
|
+
completed_at: Math.floor(Date.now() / 1000),
|
|
801
|
+
usage
|
|
802
|
+
});
|
|
803
|
+
if (streaming) {
|
|
804
|
+
yield createEvent('thread.run.step.completed', state.getRunStep(runId, stepId));
|
|
805
|
+
}
|
|
806
|
+
// Mark run as completed
|
|
807
|
+
state.updateRun(threadId, runId, {
|
|
808
|
+
status: 'completed',
|
|
809
|
+
completed_at: Math.floor(Date.now() / 1000),
|
|
810
|
+
usage
|
|
811
|
+
});
|
|
812
|
+
if (streaming) {
|
|
813
|
+
yield createEvent('thread.run.completed', state.getRun(threadId, runId));
|
|
814
|
+
yield createEvent('done', '[DONE]');
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
catch (error) {
|
|
818
|
+
console.error('Continue run error:', error);
|
|
819
|
+
state.updateRun(threadId, runId, {
|
|
820
|
+
status: 'failed',
|
|
821
|
+
failed_at: Math.floor(Date.now() / 1000),
|
|
822
|
+
last_error: {
|
|
823
|
+
code: 'server_error',
|
|
824
|
+
message: error instanceof Error ? error.message : 'Unknown error'
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
if (streaming) {
|
|
828
|
+
yield createEvent('error', {
|
|
829
|
+
error: {
|
|
830
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
831
|
+
code: 'server_error'
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
yield createEvent('thread.run.failed', state.getRun(threadId, runId));
|
|
835
|
+
yield createEvent('done', '[DONE]');
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
finally {
|
|
839
|
+
activeRuns.delete(runKey);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Continue run with tool outputs (non-streaming wrapper)
|
|
844
|
+
*/
|
|
845
|
+
export async function continueRunWithToolOutputsNonStreaming(threadId, runId, toolOutputs) {
|
|
846
|
+
const generator = continueRunWithToolOutputs(threadId, runId, toolOutputs, false);
|
|
847
|
+
for await (const _ of generator) {
|
|
848
|
+
// Discard events
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
//# sourceMappingURL=runner.js.map
|