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/LICENSE +21 -0
- package/README.md +398 -0
- package/context.js +1056 -0
- package/index.js +130 -0
- package/itask.js +678 -0
- package/openai.js +72 -0
- package/package.json +49 -0
- package/redis.js +123 -0
- package/sid.js +207 -0
- package/util.js +110 -0
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 };
|