saico 2.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/context.js ADDED
@@ -0,0 +1,1056 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const openai = require('./openai.js');
5
+ const util = require('./util.js');
6
+
7
+ const { _log, _lerr, _ldbg } = util;
8
+ const debug = 0;
9
+
10
+ /**
11
+ * Context - Conversation context that can be attached to any Itask.
12
+ *
13
+ * Key differences from the old Messages class:
14
+ * - Uses task hierarchy instead of parent/child messages
15
+ * - task reference replaces parent reference
16
+ * - getMsgContext() traverses task hierarchy
17
+ * - _createMsgQ() aggregates from task ancestors
18
+ */
19
+ class Context {
20
+ constructor(prompt, task, config = {}) {
21
+ this.prompt = prompt;
22
+ this.task = task; // Reference to owning Itask (replaces parent)
23
+ this.tag = config.tag || crypto.randomBytes(4).toString('hex');
24
+ this.token_limit = config.token_limit || 1000000000;
25
+ this.lower_limit = this.token_limit * 0.85;
26
+ this.upper_limit = this.token_limit * 0.98;
27
+ this.tool_handler = config.tool_handler || task?.tool_handler;
28
+ this.functions = config.functions || task?.functions || null;
29
+
30
+ // Recursive depth and repetition control
31
+ this.max_depth = config.max_depth || 5;
32
+ this.max_tool_repetition = config.max_tool_repetition || 20;
33
+ this._current_depth = 0;
34
+ this._deferred_tool_calls = [];
35
+ this._tool_call_sequence = [];
36
+
37
+ this._msgs = [];
38
+ this._waitingQueue = [];
39
+ this._active_tool_calls = new Map();
40
+
41
+ // Sequential mode support
42
+ this._sequential_queue = [];
43
+ this._processing_sequential = false;
44
+ this._sequential_mode = config.sequential_mode || false;
45
+
46
+ // Initialize messages if provided
47
+ (config.msgs || []).forEach(m => this.push(m));
48
+
49
+ _log('created Context for tag', this.tag);
50
+ }
51
+
52
+ // Set the task reference (used when context is created separately)
53
+ setTask(task) {
54
+ this.task = task;
55
+ if (!this.tool_handler)
56
+ this.tool_handler = task?.tool_handler;
57
+ if (!this.functions)
58
+ this.functions = task?.functions;
59
+ }
60
+
61
+ // Get the parent context by traversing task hierarchy
62
+ getParentContext() {
63
+ if (!this.task || !this.task.parent)
64
+ return null;
65
+ return this.task.parent.findContext ? this.task.parent.findContext() : null;
66
+ }
67
+
68
+ // Get all ancestor contexts via task hierarchy
69
+ getAncestorContexts() {
70
+ if (!this.task)
71
+ return [];
72
+ return this.task.getAncestorContexts().filter(ctx => ctx !== this);
73
+ }
74
+
75
+ _hasPendingToolCalls() {
76
+ const toolCallMsgs = this._msgs.filter(m => m.msg.tool_calls);
77
+
78
+ for (const toolCallMsg of toolCallMsgs) {
79
+ const toolCalls = toolCallMsg.msg.tool_calls;
80
+ const toolCallIds = toolCalls.map(tc => tc.id);
81
+
82
+ const toolReplies = this._msgs.filter(m =>
83
+ m.msg.role === 'tool' &&
84
+ toolCallIds.includes(m.msg.tool_call_id)
85
+ );
86
+
87
+ const repliedCallIds = new Set(toolReplies.map(r => r.msg.tool_call_id));
88
+ const deferredCallIds = new Set(this._deferred_tool_calls.map(d => d.call.id));
89
+ const unRepliedCalls = toolCalls.filter(tc =>
90
+ !repliedCallIds.has(tc.id) && !deferredCallIds.has(tc.id)
91
+ );
92
+
93
+ if (unRepliedCalls.length > 0)
94
+ return true;
95
+ }
96
+
97
+ return false;
98
+ }
99
+
100
+ _processWaitingQueue() {
101
+ _log('Processing waiting queue,', this._waitingQueue.length, 'messages waiting');
102
+
103
+ this._waitingQueue.forEach(waitingMessage => {
104
+ _log('Adding queued message to queue:', waitingMessage.role,
105
+ waitingMessage.content?.slice(0, 50));
106
+ this._createMsgObj(
107
+ waitingMessage.role,
108
+ waitingMessage.content,
109
+ waitingMessage.functions,
110
+ waitingMessage.opts
111
+ );
112
+ });
113
+
114
+ this._waitingQueue = [];
115
+ }
116
+
117
+ async _processSequentialQueue() {
118
+ if (this._processing_sequential || this._sequential_queue.length === 0)
119
+ return;
120
+
121
+ _ldbg('[' + this.tag + '] Starting sequential queue processing');
122
+ this._processing_sequential = true;
123
+
124
+ try {
125
+ while (this._sequential_queue.length > 0) {
126
+ const queuedMsg = this._sequential_queue.shift();
127
+ _ldbg('Processing sequential message:', queuedMsg.role,
128
+ queuedMsg.content?.slice(0, 50));
129
+
130
+ try {
131
+ const result = await this._sendMessageInternal(
132
+ queuedMsg.role,
133
+ queuedMsg.content,
134
+ queuedMsg.functions,
135
+ queuedMsg.opts
136
+ );
137
+
138
+ if (queuedMsg.resolve)
139
+ queuedMsg.resolve(result);
140
+ } catch (err) {
141
+ if (queuedMsg.reject)
142
+ queuedMsg.reject(err);
143
+ else
144
+ _lerr('Error processing queued message:', err);
145
+ }
146
+ }
147
+ _ldbg('[' + this.tag + '] Sequential queue processing completed');
148
+ } catch (err) {
149
+ _lerr('Error processing sequential queue:', err);
150
+ } finally {
151
+ _ldbg('[' + this.tag + '] Setting _processing_sequential = false');
152
+ this._processing_sequential = false;
153
+ }
154
+ }
155
+
156
+ _trackToolCall(toolName) {
157
+ this._tool_call_sequence.push(toolName);
158
+
159
+ if (this._tool_call_sequence.length > this.max_tool_repetition * 2) {
160
+ this._tool_call_sequence = this._tool_call_sequence.slice(-this.max_tool_repetition);
161
+ }
162
+ }
163
+
164
+ _shouldDropToolCall(toolName) {
165
+ if (this._tool_call_sequence.length < this.max_tool_repetition)
166
+ return false;
167
+
168
+ let consecutiveCount = 0;
169
+ for (let i = this._tool_call_sequence.length - 1; i >= 0; i--) {
170
+ if (this._tool_call_sequence[i] === toolName) {
171
+ consecutiveCount++;
172
+ } else {
173
+ break;
174
+ }
175
+ }
176
+
177
+ return consecutiveCount >= this.max_tool_repetition;
178
+ }
179
+
180
+ _resetToolSequenceIfDifferent(newToolNames) {
181
+ if (!newToolNames || newToolNames.length === 0) {
182
+ this._tool_call_sequence = [];
183
+ return;
184
+ }
185
+
186
+ const lastTool = this._tool_call_sequence[this._tool_call_sequence.length - 1];
187
+ if (!lastTool || !newToolNames.includes(lastTool)) {
188
+ this._tool_call_sequence = [];
189
+ }
190
+ }
191
+
192
+ _filterExcessiveToolCalls(toolCalls) {
193
+ if (!toolCalls || toolCalls.length === 0) return toolCalls;
194
+
195
+ return toolCalls.filter(call => {
196
+ const toolName = call.function.name;
197
+ if (this._shouldDropToolCall(toolName)) {
198
+ _log('Dropping excessive tool call:', toolName,
199
+ '(hit max_tool_repetition=' + this.max_tool_repetition + ')');
200
+ return false;
201
+ }
202
+ return true;
203
+ });
204
+ }
205
+
206
+ async _processDeferredToolCalls() {
207
+ if (this._deferred_tool_calls.length === 0) return;
208
+
209
+ _log('Processing deferred tool calls:', this._deferred_tool_calls.length);
210
+
211
+ const deferredCalls = [...this._deferred_tool_calls];
212
+ this._deferred_tool_calls = [];
213
+
214
+ const callsByMessage = new Map();
215
+ for (const deferred of deferredCalls) {
216
+ const key = deferred.originalMessage.msgid;
217
+ if (!callsByMessage.has(key)) {
218
+ callsByMessage.set(key, []);
219
+ }
220
+ callsByMessage.get(key).push(deferred);
221
+ }
222
+
223
+ for (const [msgid, deferredGroup] of callsByMessage) {
224
+ _log('Processing deferred group for message', msgid + ':', deferredGroup.length, 'calls');
225
+
226
+ const toolCalls = deferredGroup.map(d => d.call);
227
+ const toolNames = toolCalls.map(call => call.function.name);
228
+ this._resetToolSequenceIfDifferent(toolNames);
229
+
230
+ const filteredToolCalls = this._filterExcessiveToolCalls(toolCalls);
231
+
232
+ let reply2 = {};
233
+ for (const [i, call] of filteredToolCalls.entries()) {
234
+ const toolName = call.function.name;
235
+ this._trackToolCall(toolName);
236
+
237
+ let result;
238
+
239
+ if (this._isDuplicateToolCall(call)) {
240
+ _log('Duplicate deferred tool call detected:', call.function.name);
241
+ result = {
242
+ content: `Duplicate call detected. An identical "${call.function.name}" ` +
243
+ `tool call with the same arguments is already running.`,
244
+ functions: null
245
+ };
246
+ } else {
247
+ this._trackActiveToolCall(call);
248
+
249
+ try {
250
+ const correspondingDeferred = deferredGroup.find(d => d.call.id === call.id);
251
+ const handler = correspondingDeferred?.originalMessage.opts.handler || this.tool_handler;
252
+ const timeout = correspondingDeferred?.originalMessage.opts.timeout;
253
+
254
+ result = await this._executeToolCallWithTimeout(call, handler, timeout);
255
+ } finally {
256
+ this._completeActiveToolCall(call);
257
+ }
258
+ }
259
+
260
+ const correspondingDeferred = deferredGroup.find(d => d.call.id === call.id);
261
+ const opts = {
262
+ name: call.function.name,
263
+ tool_call_id: call.id,
264
+ _recursive_depth: 1,
265
+ model: correspondingDeferred?.originalMessage.opts.model
266
+ };
267
+ const content = result ? (result.content || result) : '';
268
+ const functions = (i === filteredToolCalls.length - 1 && result && result.functions)
269
+ ? result.functions : null;
270
+
271
+ if (i === filteredToolCalls.length - 1) {
272
+ reply2 = await this.sendMessage('tool', content, functions, opts);
273
+ } else {
274
+ const toolResponse = this._createMsgObj('tool', content, null, opts);
275
+ toolResponse.replied = 1;
276
+ this._insertToolResponseAtCorrectPosition(toolResponse, call.id);
277
+ }
278
+ }
279
+ }
280
+ }
281
+
282
+ get messages() {
283
+ return this.__msgs;
284
+ }
285
+
286
+ set messages(value) {
287
+ if (Array.isArray(value)) {
288
+ this._msgs = value.map(m => m.msg ? m : {msg: m, opts: {}, replied: 1});
289
+ } else {
290
+ throw new Error("messages must be assigned an array");
291
+ }
292
+ }
293
+
294
+ push(msg) {
295
+ const m = {msg: msg.msg || msg, opts: msg.opts || {}, msgid: msg.msgid || 0, replied: msg.replied || 2};
296
+ return this._msgs.push(m);
297
+ }
298
+
299
+ pushSummary(summary) {
300
+ const idx = this.push({role: 'user', content: '[SUMMARY]: ' + summary});
301
+ this._msgs[idx - 1].opts.summary = true;
302
+ }
303
+
304
+ toJSON() {
305
+ return this.__msgs;
306
+ }
307
+
308
+ filter(callback) {
309
+ return this.__msgs.filter(callback);
310
+ }
311
+
312
+ concat(arr) {
313
+ return this.__msgs.concat(arr);
314
+ }
315
+
316
+ slice(start, end) {
317
+ return this.__msgs.slice(start, end);
318
+ }
319
+
320
+ reverse() {
321
+ this._msgs.reverse();
322
+ return this;
323
+ }
324
+
325
+ [Symbol.iterator]() {
326
+ return (function* () {
327
+ for (const item of this._msgs) {
328
+ yield item.msg;
329
+ }
330
+ }).call(this);
331
+ }
332
+
333
+ get __msgs() { return this._msgs.map(m => m.msg); }
334
+
335
+ get length() {
336
+ return this._msgs.length;
337
+ }
338
+
339
+ serialize() { return JSON.stringify(this._msgs); }
340
+
341
+ getSummaries() { return this._msgs.filter(m => m.opts.summary); }
342
+
343
+ // Get functions aggregated from this context and all ancestor contexts
344
+ getFunctions() {
345
+ const allFunctions = [];
346
+
347
+ // Get functions from ancestor contexts via task hierarchy
348
+ const ancestorContexts = this.getAncestorContexts();
349
+ for (const ctx of ancestorContexts) {
350
+ if (ctx.functions && Array.isArray(ctx.functions))
351
+ allFunctions.push(...ctx.functions);
352
+ }
353
+
354
+ // Add our own functions
355
+ if (this.functions && Array.isArray(this.functions))
356
+ allFunctions.push(...this.functions);
357
+
358
+ return allFunctions.length > 0 ? allFunctions : null;
359
+ }
360
+
361
+ async summarizeMessages() {
362
+ const tokens = util.countTokens(this.__msgs);
363
+ if (tokens < this.lower_limit)
364
+ return;
365
+ await this._summarizeContext();
366
+ }
367
+
368
+ async close() {
369
+ _log('Closing Context tag', this.tag);
370
+
371
+ if (this._sequential_mode && this._processing_sequential) {
372
+ _ldbg('Sequential mode: waiting for current message to complete before closing tag', this.tag);
373
+ let waitCount = 0;
374
+ while (this._processing_sequential) {
375
+ await new Promise(resolve => setTimeout(resolve, 100));
376
+ waitCount++;
377
+ if (waitCount % 10 === 0)
378
+ _ldbg('Sequential mode: still waiting for tag', this.tag, 'after', waitCount, 'iterations');
379
+ }
380
+ }
381
+
382
+ // Move waiting messages to parent context via task hierarchy
383
+ const parentCtx = this.getParentContext();
384
+ if (parentCtx && this._waitingQueue.length > 0) {
385
+ _log('Moving', this._waitingQueue.length, 'waiting messages to parent context');
386
+ parentCtx._waitingQueue.push(...this._waitingQueue);
387
+ this._waitingQueue = [];
388
+ }
389
+
390
+ if (parentCtx && this._sequential_queue.length > 0) {
391
+ _log('Moving', this._sequential_queue.length, 'sequential queue messages to parent context');
392
+ parentCtx._sequential_queue.push(...this._sequential_queue);
393
+ this._sequential_queue = [];
394
+ }
395
+
396
+ await this._summarizeContext(true, parentCtx);
397
+ _log('Finished closing Context tag', this.tag);
398
+ }
399
+
400
+ async _summarizeContext(close, targetCtx) {
401
+ const keep = this._msgs.filter(m => !close && m.summary);
402
+ const summarize = this._msgs.filter(m => (!close || !m.summary) && m.replied);
403
+ const not_replied = this._msgs.filter(m => !m.replied);
404
+ _ldbg('Start summarize messages. # messages', summarize.length, '(total msgs:', this._msgs.length + ')');
405
+
406
+ if (!summarize.length) {
407
+ _ldbg('[' + this.tag + '] No messages to summarize');
408
+ return;
409
+ }
410
+
411
+ const msgs = (close ? [{role: 'system', content: this.prompt}] : []).concat(summarize.map(m => m.msg));
412
+ const summary = await this._summarizeMessages(msgs);
413
+ this._msgs = keep;
414
+
415
+ if (summary) {
416
+ if (close && targetCtx)
417
+ targetCtx.pushSummary(summary);
418
+ else
419
+ this.pushSummary(summary);
420
+ }
421
+
422
+ this._msgs.push(...not_replied);
423
+ _log('Summarized', this.tag, '(close', close + ') conversation to', util.countTokens(this.__msgs),
424
+ 'tokens # messages', this._msgs.length);
425
+ }
426
+
427
+ async _summarizeMessages(msgs) {
428
+ let chunks = [msgs];
429
+ const tokens = util.countTokens(chunks[0]);
430
+
431
+ if (tokens > this.upper_limit) {
432
+ chunks = [];
433
+ let chunk_msgs = [];
434
+ let chunk = '';
435
+
436
+ msgs.forEach(m => {
437
+ if (typeof m !== 'object' || Array.isArray(m))
438
+ return _lerr('discarding msg with corrupt structure', m);
439
+ const keys = Object.keys(m);
440
+ for (const k of keys) {
441
+ if (!['role', 'content', 'refusal', 'name', 'tool', 'tool_calls'].includes(k))
442
+ return _lerr('discarding msg with corrupt key', k);
443
+ }
444
+ if (util.countTokens(m) > this.upper_limit)
445
+ return _lerr('discard abnormal size message', tokens, 'tokens\n' + m.content?.slice(0, 1500));
446
+ if (m.function)
447
+ m.content = '<function data>';
448
+ const str = `${m.role.toUpperCase()}: ${m.content || JSON.stringify(m.tool_calls)}`;
449
+ if (util.countTokens(chunk + str + '\n') < this.token_limit / 2) {
450
+ chunk += str + '\n';
451
+ chunk_msgs.push(m);
452
+ } else {
453
+ chunks.push(chunk_msgs);
454
+ chunk = '';
455
+ chunk_msgs = [];
456
+ }
457
+ });
458
+ if (chunk_msgs.length)
459
+ chunks.push(chunk_msgs);
460
+ }
461
+
462
+ if (!chunks.length)
463
+ return _log('No msgs for summary found');
464
+
465
+ _log('Summarizing messages. tokens', tokens, 'messages', msgs.length, 'using', chunks.length, 'chunks');
466
+
467
+ let reply = await openai.send([{role: 'system', content:
468
+ 'Please summarize the following conversation. The summary should be one or two paragraphs as follows:' +
469
+ '- First paragraph: the purpose of the conversation and the outcome' +
470
+ '- Second paragraph (optional): next steps or pending requests that should be considered' +
471
+ '- Do not include system errors in the summary.\n' +
472
+ '- Formulate the summary from the AI agent\'s perspective\n' +
473
+ '\nConversation:\n' +
474
+ (chunks.length > 1 ? 'The conversation will be uploaded in ' + chunks.length +
475
+ ' chunks. Wait for the last one then summarize all.\nChunk 1:\n'
476
+ : 'The conversation to summarize:\n') + JSON.stringify(chunks[0])}]);
477
+
478
+ let summary = reply.content;
479
+ for (let i = 1; i < chunks.length; i++) {
480
+ reply = await openai.send([{role: 'system', content:
481
+ 'Chunk ' + (i === chunks.length ? 'last' : i) + ':\n' + JSON.stringify(chunks[i])}]);
482
+ summary = 'Summary of ' + this.tag + ' conversation:\n' + reply.content;
483
+ }
484
+ return summary;
485
+ }
486
+
487
+ // Get message context - walks up task hierarchy to collect prompts and summaries
488
+ getMsgContext(add_tag) {
489
+ const msgs = [];
490
+
491
+ // Get context from ancestor tasks via task hierarchy
492
+ const ancestorContexts = this.getAncestorContexts();
493
+ for (const ctx of ancestorContexts) {
494
+ if (ctx.prompt)
495
+ msgs.push({role: 'system', content: ctx.prompt});
496
+ // Add summaries from ancestor contexts
497
+ const summaries = ctx._msgs.filter(m => m.opts.summary || m.msg.role === 'system').map(m => {
498
+ if (add_tag)
499
+ m.msg.tag = ctx.tag;
500
+ return m.msg;
501
+ });
502
+ msgs.push(...summaries);
503
+ }
504
+
505
+ // Add this context's prompt
506
+ if (this.prompt)
507
+ msgs.push({role: 'system', content: this.prompt});
508
+
509
+ // Add this context's summaries
510
+ const mySummaries = this._msgs.filter(m => m.opts.summary || m.msg.role === 'system').map(m => {
511
+ if (add_tag)
512
+ m.msg.tag = this.tag;
513
+ return m.msg;
514
+ });
515
+
516
+ return msgs.concat(mySummaries);
517
+ }
518
+
519
+ _createMsgObj(role, content, functions, opts) {
520
+ const name = opts?.name;
521
+ const tool_call_id = opts?.tool_call_id;
522
+ const msg = { role, content, ...(name && { name }), ...(tool_call_id && { tool_call_id }) };
523
+ const msgid = crypto.randomBytes(2).toString('hex');
524
+ const o = {msg, opts: opts || {}, functions, msgid, replied: 0};
525
+ this._msgs.forEach(m => m.opts.noreply ||= !m.replied);
526
+ this._msgs.push(o);
527
+ return o;
528
+ }
529
+
530
+ _insertToolResponseAtCorrectPosition(toolResponseObj, tool_call_id) {
531
+ let insertIndex = -1;
532
+ let originalInsertIndex = -1;
533
+
534
+ for (let i = this._msgs.length - 1; i >= 0; i--) {
535
+ const msg = this._msgs[i];
536
+ if (msg.msg.tool_calls) {
537
+ const hasMatchingCall = msg.msg.tool_calls.some(call => call.id === tool_call_id);
538
+ if (hasMatchingCall) {
539
+ if (insertIndex === -1)
540
+ insertIndex = i + 1;
541
+
542
+ if (msg.msg.content !== 'Processing deferred tool calls') {
543
+ originalInsertIndex = i + 1;
544
+ break;
545
+ }
546
+ }
547
+ }
548
+ }
549
+
550
+ const finalInsertIndex = originalInsertIndex !== -1 ? originalInsertIndex : insertIndex;
551
+
552
+ if (finalInsertIndex !== -1 && finalInsertIndex < this._msgs.length) {
553
+ const lastIndex = this._msgs.length - 1;
554
+ if (this._msgs[lastIndex] === toolResponseObj) {
555
+ this._msgs.pop();
556
+ this._msgs.splice(finalInsertIndex, 0, toolResponseObj);
557
+ }
558
+ }
559
+ }
560
+
561
+ _getToolCallKey(call) {
562
+ return `${call.function.name}:${call.function.arguments}`;
563
+ }
564
+
565
+ _isDuplicateToolCall(call) {
566
+ const key = this._getToolCallKey(call);
567
+ return this._active_tool_calls.has(key);
568
+ }
569
+
570
+ _trackActiveToolCall(call) {
571
+ const key = this._getToolCallKey(call);
572
+ this._active_tool_calls.set(key, {
573
+ call_id: call.id,
574
+ started_at: Date.now(),
575
+ function_name: call.function.name
576
+ });
577
+ _log('Tracking active tool call:', key);
578
+ }
579
+
580
+ _completeActiveToolCall(call) {
581
+ const key = this._getToolCallKey(call);
582
+ if (this._active_tool_calls.has(key)) {
583
+ this._active_tool_calls.delete(key);
584
+ _log('Completed active tool call:', key);
585
+ }
586
+ }
587
+
588
+ async _executeToolCallWithTimeout(call, handler, customTimeoutMs = null) {
589
+ const timeoutMs = customTimeoutMs || 5000;
590
+
591
+ return new Promise(async (resolve) => {
592
+ let timeoutId;
593
+ let completed = false;
594
+
595
+ timeoutId = setTimeout(() => {
596
+ if (!completed) {
597
+ completed = true;
598
+ _log('Tool call timed out after', timeoutMs + 'ms:', call.function.name);
599
+ resolve({
600
+ content: `Tool call "${call.function.name}" timed out after ${timeoutMs/1000} seconds.`,
601
+ functions: null
602
+ });
603
+ }
604
+ }, timeoutMs);
605
+
606
+ try {
607
+ const result = await this.interpretAndApplyChanges(call, handler);
608
+
609
+ if (!completed) {
610
+ completed = true;
611
+ clearTimeout(timeoutId);
612
+ resolve(result);
613
+ }
614
+ } catch (error) {
615
+ if (!completed) {
616
+ completed = true;
617
+ clearTimeout(timeoutId);
618
+ _lerr('Tool call failed with error:', call.function.name, error.message);
619
+ resolve({
620
+ content: `Tool call "${call.function.name}" failed with error: ${error.message}`,
621
+ functions: null
622
+ });
623
+ }
624
+ }
625
+ });
626
+ }
627
+
628
+ _validateToolResponses(msgs) {
629
+ const toolCallIds = new Set();
630
+ const toolResponseIds = new Set();
631
+
632
+ for (const msg of msgs) {
633
+ if (msg.tool_calls) {
634
+ for (const toolCall of msg.tool_calls)
635
+ toolCallIds.add(toolCall.id);
636
+ }
637
+ if (msg.role === 'tool' && msg.tool_call_id)
638
+ toolResponseIds.add(msg.tool_call_id);
639
+ }
640
+
641
+ const validatedMsgs = [];
642
+ const orphanedCalls = [];
643
+
644
+ for (const msg of msgs) {
645
+ if (msg.role === 'tool' && msg.tool_call_id) {
646
+ if (toolCallIds.has(msg.tool_call_id))
647
+ validatedMsgs.push(msg);
648
+ else
649
+ _log('Removing orphaned tool response with tool_call_id:', msg.tool_call_id);
650
+ } else if (msg.role === 'assistant' && msg.tool_calls) {
651
+ const validToolCalls = [];
652
+ for (const toolCall of msg.tool_calls) {
653
+ if (toolResponseIds.has(toolCall.id))
654
+ validToolCalls.push(toolCall);
655
+ else
656
+ orphanedCalls.push({
657
+ tool_call_id: toolCall.id,
658
+ function_name: toolCall.function?.name
659
+ });
660
+ }
661
+
662
+ if (validToolCalls.length > 0)
663
+ validatedMsgs.push({...msg, tool_calls: validToolCalls});
664
+ else if (msg.content && msg.content.trim() !== '') {
665
+ const cleanedMsg = {...msg};
666
+ delete cleanedMsg.tool_calls;
667
+ validatedMsgs.push(cleanedMsg);
668
+ }
669
+ } else {
670
+ validatedMsgs.push(msg);
671
+ }
672
+ }
673
+
674
+ if (orphanedCalls.length > 0)
675
+ _lerr('Removed tool calls without responses:', JSON.stringify(orphanedCalls, null, 2));
676
+
677
+ return validatedMsgs;
678
+ }
679
+
680
+ // Build message queue - aggregates from task hierarchy
681
+ _createMsgQ(add_tag, tag_filter) {
682
+ const fullQueue = [];
683
+
684
+ // Get messages from ancestor contexts via task hierarchy
685
+ const ancestorContexts = this.getAncestorContexts();
686
+ for (const ctx of ancestorContexts) {
687
+ if (ctx.prompt) {
688
+ const prompt = {role: 'system', content: ctx.prompt};
689
+ if (add_tag)
690
+ prompt.tag = ctx.tag;
691
+ fullQueue.push(prompt);
692
+ }
693
+
694
+ let ctxMsgs;
695
+ if (tag_filter !== undefined) {
696
+ ctxMsgs = ctx._msgs.filter(m => {
697
+ if (m.msg.role === 'system') return true;
698
+ if (m.opts.summary) return true;
699
+ if (m.opts.tag === tag_filter) return true;
700
+ return false;
701
+ }).map(m => m.msg);
702
+ } else {
703
+ ctxMsgs = ctx.__msgs;
704
+ }
705
+
706
+ if (add_tag)
707
+ ctxMsgs = ctxMsgs.map(m => Object.assign({tag: ctx.tag}, m));
708
+
709
+ fullQueue.push(...ctxMsgs);
710
+ }
711
+
712
+ // Add this context's prompt and messages
713
+ if (this.prompt) {
714
+ const prompt = {role: 'system', content: this.prompt};
715
+ if (add_tag)
716
+ prompt.tag = this.tag;
717
+ fullQueue.push(prompt);
718
+ }
719
+
720
+ let my_msgs;
721
+ if (tag_filter !== undefined) {
722
+ my_msgs = this._msgs.filter(m => {
723
+ if (m.msg.role === 'system') return true;
724
+ if (m.opts.summary) return true;
725
+ if (m.opts.tag === tag_filter) return true;
726
+ return false;
727
+ }).map(m => m.msg);
728
+ } else {
729
+ my_msgs = this.__msgs;
730
+ }
731
+
732
+ if (add_tag)
733
+ my_msgs = my_msgs.map(m => Object.assign({tag: this.tag}, m));
734
+
735
+ fullQueue.push(...my_msgs);
736
+
737
+ return this._validateToolResponses(fullQueue);
738
+ }
739
+
740
+ async sendMessage(role, content, functions, opts) {
741
+ if (!content)
742
+ return console.error('trying to send a message with no content');
743
+
744
+ const isRecursiveCall = opts?._recursive_depth !== undefined;
745
+
746
+ if (this._sequential_mode && this._processing_sequential && !isRecursiveCall) {
747
+ _log('Sequential mode: queueing message:', role, content?.slice(0, 50));
748
+ return new Promise((resolve, reject) => {
749
+ this._sequential_queue.push({ role, content, functions, opts, resolve, reject });
750
+ });
751
+ }
752
+
753
+ return await this._sendMessageInternal(role, content, functions, opts);
754
+ }
755
+
756
+ async _sendMessageInternal(role, content, functions, opts) {
757
+ const isRecursiveCall = opts?._recursive_depth !== undefined;
758
+
759
+ if (!isRecursiveCall)
760
+ this._current_depth++;
761
+
762
+ const currentDepth = isRecursiveCall ? opts._recursive_depth : this._current_depth;
763
+
764
+ const wasProcessing = this._processing_sequential;
765
+ if (this._sequential_mode && !isRecursiveCall) {
766
+ _ldbg('[' + this.tag + '] _sendMessageInternal setting _processing_sequential = true');
767
+ this._processing_sequential = true;
768
+ }
769
+
770
+ try {
771
+ if (this._hasPendingToolCalls() && role !== 'tool') {
772
+ _log('Tool calls pending, queueing message:', role, content?.slice(0, 50));
773
+ this._waitingQueue.push({ role, content, functions, opts });
774
+ return { content: '', queued: true };
775
+ }
776
+
777
+ const o = this._createMsgObj(role, content, functions, opts);
778
+
779
+ if (role === 'tool' && opts?.tool_call_id)
780
+ this._insertToolResponseAtCorrectPosition(o, opts.tool_call_id);
781
+
782
+ return await this._processSendMessage(o, currentDepth);
783
+ } finally {
784
+ if (!isRecursiveCall) {
785
+ this._current_depth--;
786
+
787
+ if (this._sequential_mode) {
788
+ _ldbg('[' + this.tag + '] restoring _processing_sequential to:', wasProcessing);
789
+ this._processing_sequential = wasProcessing;
790
+ }
791
+
792
+ if (this._sequential_mode && !wasProcessing)
793
+ setImmediate(() => this._processSequentialQueue());
794
+ }
795
+ }
796
+ }
797
+
798
+ _debugQDump(Q, functions) {
799
+ if (util.is_mocha && process.env.PROD)
800
+ return;
801
+ const dbgQ = Q || this._createMsgQ(true);
802
+ if (debug) {
803
+ console.log('MSGQDEBUG - Q:', JSON.stringify(dbgQ.map(m => ({
804
+ role: m.role,
805
+ content: m.content?.substring?.(0, 50),
806
+ tool_calls: m.tool_calls,
807
+ tool_call_id: m.tool_call_id,
808
+ tag: m.tag
809
+ })), 0, 4), functions?.map?.(f => f.name));
810
+ }
811
+ }
812
+
813
+ async _processSendMessage(o, depth) {
814
+ let Q;
815
+ try {
816
+ const name = o.opts?.name;
817
+ _log('@@@@@@@@@ [>>(' + depth + ') ' + o.msgid + (o.opts?.tag ? '-' + o.opts.tag : '') +
818
+ (name ? ' F(' + name + ')' : '') + ' ] SEND-AI', o.msg.role,
819
+ o.msg.content.slice(0, 2000) + (o.msg.content?.length > 2000 ? '... ' : ''));
820
+
821
+ if (this._waitingQueue.length > 0 && !this._hasPendingToolCalls()) {
822
+ _log('Processing waiting queue before OpenAI call:', this._waitingQueue.length, 'messages');
823
+ this._processWaitingQueue();
824
+ }
825
+
826
+ await this.summarizeMessages();
827
+ Q = this._createMsgQ(false, o.opts?.tag);
828
+
829
+ // Aggregate functions from hierarchy and merge with message-specific functions
830
+ const hierarchyFuncs = this.getFunctions() || [];
831
+ const messageFuncs = o.functions || [];
832
+ const funcs = [...hierarchyFuncs, ...messageFuncs].length > 0
833
+ ? [...hierarchyFuncs, ...messageFuncs]
834
+ : null;
835
+
836
+ if (debug)
837
+ this._debugQDump(Q, funcs);
838
+
839
+ const reply = await openai.send(Q, funcs, o.opts?.model);
840
+
841
+ _log('@@@@@@@@@ [<<', o.msgid + (reply.tool_calls ? ' TC:' + (reply.tool_calls?.length || 0) : '') +
842
+ ' ] REPLY-AI', reply.role,
843
+ (reply.content && !o.opts?.noreply ? ' Content: ' + reply.content.slice(0, 2000) : '') +
844
+ (reply.content?.length > 2000 ? '... ' : '') +
845
+ (reply.tool_calls && !o.opts?.nofunc ? '\nCall tools ' + JSON.stringify(reply.tool_calls, 0, 4) : ''));
846
+
847
+ o.replied = 1;
848
+ delete o.functions;
849
+
850
+ if (o.opts?.nofunc)
851
+ delete reply.tool_calls;
852
+ if (o.opts?.debug_empty && !reply.content)
853
+ this._debugQDump(Q, o.functions);
854
+
855
+ this._msgs.push({msg: reply, msgid: o.msgid, opts: o.opts || {}, replied: 3});
856
+
857
+ let reply2 = {};
858
+ if (reply?.tool_calls) {
859
+ const toolNames = reply.tool_calls.map(call => call.function.name);
860
+ this._resetToolSequenceIfDifferent(toolNames);
861
+
862
+ const filteredToolCalls = this._filterExcessiveToolCalls(reply.tool_calls);
863
+
864
+ let toolCallsToProcess = filteredToolCalls;
865
+ let deferredToolCalls = [];
866
+
867
+ if (depth >= this.max_depth && filteredToolCalls.length > 0) {
868
+ _log('Max depth', this.max_depth, 'reached at depth', depth, ', deferring',
869
+ filteredToolCalls.length, 'tool calls');
870
+ deferredToolCalls = filteredToolCalls;
871
+ toolCallsToProcess = [];
872
+
873
+ this._deferred_tool_calls.push(...deferredToolCalls.map(call => ({
874
+ call,
875
+ originalMessage: o,
876
+ depth: depth
877
+ })));
878
+ }
879
+
880
+ const toolCallsWithResults = [];
881
+ for (const call of toolCallsToProcess) {
882
+ const toolName = call.function.name;
883
+ this._trackToolCall(toolName);
884
+
885
+ if (this._isDuplicateToolCall(call)) {
886
+ _log('Duplicate tool call detected:', call.function.name);
887
+ const result = {
888
+ content: `Duplicate call detected. An identical "${call.function.name}" ` +
889
+ `tool call with the same arguments is already running.`,
890
+ functions: null
891
+ };
892
+ toolCallsWithResults.push({ call, result, isDuplicate: true });
893
+ } else {
894
+ this._trackActiveToolCall(call);
895
+ toolCallsWithResults.push({ call, result: null, isDuplicate: false });
896
+ }
897
+ }
898
+
899
+ for (const { call, isDuplicate } of toolCallsWithResults) {
900
+ if (!isDuplicate) {
901
+ try {
902
+ const result = await this._executeToolCallWithTimeout(
903
+ call, o.opts?.handler, o.opts?.timeout);
904
+ const item = toolCallsWithResults.find(item => item.call.id === call.id);
905
+ if (item) item.result = result;
906
+ } finally {
907
+ this._completeActiveToolCall(call);
908
+ }
909
+ }
910
+ }
911
+
912
+ for (const [i, { call, result }] of toolCallsWithResults.entries()) {
913
+ const opts = {
914
+ name: call.function.name,
915
+ tool_call_id: call.id,
916
+ _recursive_depth: depth + 1,
917
+ model: o.opts?.model
918
+ };
919
+ const content = result ? (result.content || result) : '';
920
+ const functions = (i === toolCallsWithResults.length - 1 && result && result.functions)
921
+ ? result.functions : null;
922
+
923
+ if (i === toolCallsWithResults.length - 1)
924
+ reply2 = await this.sendMessage('tool', content, functions, opts);
925
+ else {
926
+ const toolResponse = this._createMsgObj('tool', content, null, opts);
927
+ toolResponse.replied = 1;
928
+ this._insertToolResponseAtCorrectPosition(toolResponse, call.id);
929
+ }
930
+ }
931
+ }
932
+
933
+ reply.content ||= '';
934
+ reply.content += reply2?.content ? '\n' + reply2.content : '';
935
+
936
+ const hasPending = this._hasPendingToolCalls();
937
+ const queueLength = this._waitingQueue.length;
938
+
939
+ if (!hasPending && queueLength > 0) {
940
+ _log('No more pending tool calls, processing', queueLength, 'waiting messages');
941
+ this._processWaitingQueue();
942
+ }
943
+
944
+ const isRecursiveCall = o.opts?._recursive_depth !== undefined;
945
+ if (!isRecursiveCall && this._current_depth === 1 && !this._hasPendingToolCalls()
946
+ && this._waitingQueue.length === 0 && this._deferred_tool_calls.length > 0) {
947
+ _log('Processing', this._deferred_tool_calls.length, 'deferred tool calls');
948
+ await this._processDeferredToolCalls();
949
+ }
950
+
951
+ return reply;
952
+ } catch (err) {
953
+ console.error('sendMessage error:', err);
954
+ this._debugQDump(Q, o?.functions);
955
+ throw err;
956
+ }
957
+ }
958
+
959
+ async interpretAndApplyChanges(call, handler) {
960
+ _log('apply tool', call.function.name, 'have handler', !!handler, !!this.tool_handler);
961
+ if (!call)
962
+ return { content: '', functions: null };
963
+
964
+ _log('invoking function', call.function.name);
965
+ handler ||= this.tool_handler;
966
+ let result = await handler(call.function.name, call.function.arguments);
967
+
968
+ let content = result?.content || result || '';
969
+ let functions = result?.functions || null;
970
+
971
+ if (content && typeof content !== 'string')
972
+ content = JSON.stringify(content);
973
+ else if (!content)
974
+ content = `tool call ${call.function.name} ${call.id} completed. do not reply. wait for the next msg from the user`;
975
+
976
+ _log('FUNCTION RESULT', call.function.name, call.id, content.substring(0, 50) + '...',
977
+ functions ? 'with functions' : 'no functions');
978
+ return { content, functions };
979
+ }
980
+
981
+ // Spawn child context (creates a child task with its own context)
982
+ spawnChild(prompt, tag, config = {}) {
983
+ if (!this.task) {
984
+ // If no task, create a standalone context (legacy mode)
985
+ return createContext(prompt, null, { ...config, tag });
986
+ }
987
+
988
+ // Create a child task with its own context
989
+ const Itask = require('./itask.js');
990
+ const childTask = new Itask({
991
+ name: tag || 'child-context',
992
+ prompt,
993
+ async: true,
994
+ spawn_parent: this.task,
995
+ contextConfig: config
996
+ }, []);
997
+
998
+ const childContext = new Context(prompt, childTask, { ...config, tag });
999
+ childTask.setContext(childContext);
1000
+
1001
+ return childContext;
1002
+ }
1003
+ }
1004
+
1005
+ // Factory function to create a Context with Proxy wrapper
1006
+ function createContext(prompt, task, config = {}) {
1007
+ const instance = new Context(prompt, task, config);
1008
+
1009
+ return new Proxy(instance, {
1010
+ get(target, prop, receiver) {
1011
+ if (typeof prop === 'string' && !isNaN(prop)) {
1012
+ return target._msgs[Number(prop)]?.msg;
1013
+ }
1014
+
1015
+ if (typeof target._msgs[prop] === 'function') {
1016
+ return target[prop].bind(target);
1017
+ }
1018
+
1019
+ if (prop === 'length') {
1020
+ return target._msgs.length;
1021
+ }
1022
+
1023
+ return Reflect.get(target, prop, receiver);
1024
+ },
1025
+
1026
+ set(target, prop, value, receiver) {
1027
+ if (typeof prop === 'string' && !isNaN(prop)) {
1028
+ target._msgs[Number(prop)] = {msg: value};
1029
+ return true;
1030
+ }
1031
+
1032
+ return Reflect.set(target, prop, value, receiver);
1033
+ },
1034
+
1035
+ has(target, prop) {
1036
+ if (typeof prop === 'string' && !isNaN(prop)) return true;
1037
+ if (prop in target._msgs) return true;
1038
+ return prop in target;
1039
+ },
1040
+
1041
+ ownKeys(target) {
1042
+ const keys = Reflect.ownKeys(target);
1043
+ const msgKeys = Object.keys(target._msgs);
1044
+ return [...new Set([...msgKeys, ...keys])];
1045
+ },
1046
+
1047
+ getOwnPropertyDescriptor(target, prop) {
1048
+ if (typeof prop === 'string' && !isNaN(prop)) {
1049
+ return Object.getOwnPropertyDescriptor(target._msgs, prop);
1050
+ }
1051
+ return Object.getOwnPropertyDescriptor(target, prop);
1052
+ }
1053
+ });
1054
+ }
1055
+
1056
+ module.exports = { Context, createContext };