@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.
Files changed (58) hide show
  1. package/out/assistants/index.d.ts +15 -0
  2. package/out/assistants/index.js +16 -0
  3. package/out/assistants/index.js.map +1 -0
  4. package/out/assistants/routes.d.ts +16 -0
  5. package/out/assistants/routes.js +597 -0
  6. package/out/assistants/routes.js.map +1 -0
  7. package/out/assistants/runner.d.ts +48 -0
  8. package/out/assistants/runner.js +851 -0
  9. package/out/assistants/runner.js.map +1 -0
  10. package/out/assistants/state.d.ts +81 -0
  11. package/out/assistants/state.js +351 -0
  12. package/out/assistants/state.js.map +1 -0
  13. package/out/assistants/tools.d.ts +4 -0
  14. package/out/assistants/tools.js +8 -0
  15. package/out/assistants/tools.js.map +1 -0
  16. package/out/assistants/types.d.ts +254 -0
  17. package/out/assistants/types.js +5 -0
  18. package/out/assistants/types.js.map +1 -0
  19. package/out/backend.d.ts +24 -0
  20. package/out/backend.js +12 -0
  21. package/out/backend.js.map +1 -0
  22. package/out/index.d.ts +13 -0
  23. package/out/index.js +21 -0
  24. package/out/index.js.map +1 -0
  25. package/out/server.d.ts +12 -0
  26. package/out/server.js +504 -0
  27. package/out/server.js.map +1 -0
  28. package/out/skills/index.d.ts +3 -0
  29. package/out/skills/index.js +4 -0
  30. package/out/skills/index.js.map +1 -0
  31. package/out/skills/manifest.d.ts +25 -0
  32. package/out/skills/manifest.js +96 -0
  33. package/out/skills/manifest.js.map +1 -0
  34. package/out/skills/resolver.d.ts +22 -0
  35. package/out/skills/resolver.js +66 -0
  36. package/out/skills/resolver.js.map +1 -0
  37. package/out/skills/routes.d.ts +3 -0
  38. package/out/skills/routes.js +191 -0
  39. package/out/skills/routes.js.map +1 -0
  40. package/out/skills/state.d.ts +35 -0
  41. package/out/skills/state.js +155 -0
  42. package/out/skills/state.js.map +1 -0
  43. package/out/skills/storage.d.ts +30 -0
  44. package/out/skills/storage.js +171 -0
  45. package/out/skills/storage.js.map +1 -0
  46. package/out/skills/types.d.ts +141 -0
  47. package/out/skills/types.js +8 -0
  48. package/out/skills/types.js.map +1 -0
  49. package/out/toolConvert.d.ts +24 -0
  50. package/out/toolConvert.js +56 -0
  51. package/out/toolConvert.js.map +1 -0
  52. package/out/types.d.ts +291 -0
  53. package/out/types.js +2 -0
  54. package/out/types.js.map +1 -0
  55. package/out/utils.d.ts +28 -0
  56. package/out/utils.js +81 -0
  57. package/out/utils.js.map +1 -0
  58. 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