saico 2.2.2 → 2.3.0

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