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/itask.js
ADDED
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
/* itask.js
|
|
2
|
+
*
|
|
3
|
+
* Node.js-only lightweight Etask-like task runner with:
|
|
4
|
+
* - E.root registry
|
|
5
|
+
* - named state parsing (try, catch, finally, cancel)
|
|
6
|
+
* - id generation and ps() debug output
|
|
7
|
+
* - cooperative cancel via _ecancel()
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* const Itask = require('./itask');
|
|
11
|
+
* const t = new Itask({name:'root'}, [stateFn1, stateFn2_cancel$label]);
|
|
12
|
+
* await t; // thenable
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const assert = require('assert');
|
|
18
|
+
const EventEmitter = require('events');
|
|
19
|
+
const crypto = require('crypto');
|
|
20
|
+
const util = require('./util.js');
|
|
21
|
+
|
|
22
|
+
const { _log, lerr , _ldbg, daysSince, minSince, shallowEqual, filterArray, logEvent } = util;
|
|
23
|
+
|
|
24
|
+
/* ---------- utility ---------- */
|
|
25
|
+
function makeId(len = 12){
|
|
26
|
+
return crypto.randomBytes(Math.ceil(len/2)).toString('hex').slice(0, len);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* ---------- state type parser ---------- */
|
|
30
|
+
function parse_state_type(fn){
|
|
31
|
+
// returns { label, try_catch, catch, finally, cancel, sig, aux }
|
|
32
|
+
const type = { label: undefined, try_catch: false, catch: false,
|
|
33
|
+
finally: false, cancel: false, sig: false, aux: false };
|
|
34
|
+
if (!fn || typeof fn.name !== 'string' || fn.name.length === 0){
|
|
35
|
+
return type;
|
|
36
|
+
}
|
|
37
|
+
// original etask used name split by '$' where left side may contain
|
|
38
|
+
// modifiers separated by '_' and right part is label
|
|
39
|
+
const parts = fn.name.split('$');
|
|
40
|
+
if (parts.length === 1){
|
|
41
|
+
type.label = parts[0] || undefined;
|
|
42
|
+
} else {
|
|
43
|
+
if (parts[1].length) type.label = parts[1];
|
|
44
|
+
const left = parts[0].split('_');
|
|
45
|
+
for (let j = 0; j < left.length; j++){
|
|
46
|
+
const f = left[j];
|
|
47
|
+
if (f === 'try'){
|
|
48
|
+
type.try_catch = true;
|
|
49
|
+
if (left[j+1] === 'catch') j++;
|
|
50
|
+
} else if (f === 'catch') {
|
|
51
|
+
type.catch = true;
|
|
52
|
+
} else if (f === 'finally' || f === 'ensure') {
|
|
53
|
+
type.finally = true;
|
|
54
|
+
} else if (f === 'cancel') {
|
|
55
|
+
type.cancel = true;
|
|
56
|
+
} else {
|
|
57
|
+
// unknown token -> ignore (keeps compatibility)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (type.catch || type.finally || type.cancel) type.sig = true;
|
|
62
|
+
type.aux = type.catch || type.finally || type.cancel;
|
|
63
|
+
return type;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* ---------- constructor / prototype ---------- */
|
|
67
|
+
function Itask(opt, states){
|
|
68
|
+
if (!(this instanceof Itask)){
|
|
69
|
+
if (Array.isArray(opt) || typeof opt === 'function'){
|
|
70
|
+
states = opt;
|
|
71
|
+
opt = {};
|
|
72
|
+
}
|
|
73
|
+
if (typeof opt === 'string') opt = { name: opt };
|
|
74
|
+
if (typeof states === 'function') states = [states];
|
|
75
|
+
return new Itask(opt || {}, states || []);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
EventEmitter.call(this);
|
|
79
|
+
opt = opt || {};
|
|
80
|
+
this.id = makeId(10);
|
|
81
|
+
this.name = opt.name;
|
|
82
|
+
this.cancelable = !!opt.cancel;
|
|
83
|
+
this.info = opt.info || {};
|
|
84
|
+
this.bind = opt.bind; // optional bind context for state functions
|
|
85
|
+
this.funcs = Array.isArray(states) ? states.slice() : [];
|
|
86
|
+
this.states = this.funcs.map(fn => parse_state_type(fn));
|
|
87
|
+
this.cur_state = 0;
|
|
88
|
+
this.running = false;
|
|
89
|
+
this.tm_create = Date.now();
|
|
90
|
+
this.tm_completed = null;
|
|
91
|
+
this.error = undefined;
|
|
92
|
+
this.retval = undefined;
|
|
93
|
+
this.parent = undefined;
|
|
94
|
+
this.child = new Set();
|
|
95
|
+
this._finally_cbs = [];
|
|
96
|
+
this._oncomplete = [];
|
|
97
|
+
this._completed = false;
|
|
98
|
+
this._cancel_state_idx = this.states.findIndex(s => s.cancel);
|
|
99
|
+
this._root_registered = false;
|
|
100
|
+
|
|
101
|
+
// Context support - optional conversation context attached to this task
|
|
102
|
+
this.context = null;
|
|
103
|
+
this._contextConfig = opt.contextConfig || {};
|
|
104
|
+
|
|
105
|
+
// Store options for context creation (prompt, functions, etc.)
|
|
106
|
+
this.prompt = opt.prompt;
|
|
107
|
+
this.functions = opt.functions;
|
|
108
|
+
this.tool_handler = opt.tool_handler;
|
|
109
|
+
|
|
110
|
+
// register root if no explicit spawn_parent provided
|
|
111
|
+
// If opt.spawn_parent provided, spawn under it
|
|
112
|
+
if (opt.spawn_parent && opt.spawn_parent instanceof Itask){
|
|
113
|
+
opt.spawn_parent.spawn(this);
|
|
114
|
+
} else {
|
|
115
|
+
Itask.root.add(this);
|
|
116
|
+
this._root_registered = true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// async option defers immediate run
|
|
120
|
+
if (!opt.async){
|
|
121
|
+
process.nextTick(()=> {
|
|
122
|
+
// don't throw, run safely
|
|
123
|
+
try { this._run(); } catch (e){ lerr.perr(e); }
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
Itask.prototype = Object.create(EventEmitter.prototype);
|
|
128
|
+
Itask.prototype.constructor = Itask;
|
|
129
|
+
|
|
130
|
+
/* root registry */
|
|
131
|
+
Itask.root = new Set();
|
|
132
|
+
|
|
133
|
+
/* ---------- core run loop (named-state aware) ---------- */
|
|
134
|
+
Itask.prototype._run = async function _run(){
|
|
135
|
+
if (this._completed) return;
|
|
136
|
+
if (this.running) return;
|
|
137
|
+
this.running = true;
|
|
138
|
+
|
|
139
|
+
while (!this._completed && this.cur_state < this.funcs.length){
|
|
140
|
+
const fn = this.funcs[this.cur_state];
|
|
141
|
+
const stateInfo = this.states[this.cur_state] || {};
|
|
142
|
+
_ldbg(`[ITASK ${this.name}] Running state ${this.cur_state}: ${stateInfo.label || 'unnamed'}, ` +
|
|
143
|
+
`catch=${stateInfo.catch}, finally=${stateInfo.finally}, cancel=${stateInfo.cancel}`);
|
|
144
|
+
let rv;
|
|
145
|
+
try {
|
|
146
|
+
// Use bind context if provided, otherwise use itask instance
|
|
147
|
+
const context = this.bind || this;
|
|
148
|
+
rv = fn.call(context, this.error || this.retval);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
_ldbg(`[ITASK ${this.name}] State threw synchronously: ${err.message}`);
|
|
151
|
+
this.error = err;
|
|
152
|
+
// on error, advance to appropriate recovery state below
|
|
153
|
+
// do not throw here
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// handle Itask child returned
|
|
157
|
+
if (rv instanceof Itask){
|
|
158
|
+
this.spawn(rv);
|
|
159
|
+
try {
|
|
160
|
+
const res = await rv;
|
|
161
|
+
this.retval = res;
|
|
162
|
+
this.error = undefined;
|
|
163
|
+
} catch (err){
|
|
164
|
+
this.error = err;
|
|
165
|
+
}
|
|
166
|
+
// Handle state advancement
|
|
167
|
+
if (this.error === undefined){
|
|
168
|
+
// advance to next non-aux state
|
|
169
|
+
this.cur_state = this._next_non_aux(this.cur_state);
|
|
170
|
+
} else {
|
|
171
|
+
// Check if this is a cancellation error and we have a cancel state (that we haven't passed yet)
|
|
172
|
+
if (this.error.message === 'cancelled' && this._cancel_state_idx !== -1
|
|
173
|
+
&& this.cur_state < this._cancel_state_idx){
|
|
174
|
+
this.cur_state = this._cancel_state_idx;
|
|
175
|
+
} else {
|
|
176
|
+
// on error: find next catch; if none -> complete (finally will still run)
|
|
177
|
+
const nextErrIdx = this._next_error_handler(this.cur_state);
|
|
178
|
+
if (nextErrIdx === -1){
|
|
179
|
+
// no handler; complete
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
this.cur_state = nextErrIdx;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// handle promise-like
|
|
189
|
+
if (rv && typeof rv.then === 'function'){
|
|
190
|
+
_ldbg(`[ITASK ${this.name}] State returned promise, awaiting...`);
|
|
191
|
+
try {
|
|
192
|
+
const res = await rv;
|
|
193
|
+
_ldbg(`[ITASK ${this.name}] Promise resolved successfully`);
|
|
194
|
+
this.retval = res;
|
|
195
|
+
this.error = undefined;
|
|
196
|
+
} catch (err){
|
|
197
|
+
_ldbg(`[ITASK ${this.name}] Promise rejected with: ${err.message}`);
|
|
198
|
+
this.error = err;
|
|
199
|
+
}
|
|
200
|
+
// Handle state advancement
|
|
201
|
+
if (this.error === undefined){
|
|
202
|
+
// advance to next non-aux state
|
|
203
|
+
this.cur_state = this._next_non_aux(this.cur_state);
|
|
204
|
+
} else {
|
|
205
|
+
// Check if this is a cancellation error and we have a cancel state (that we haven't passed yet)
|
|
206
|
+
if (this.error.message === 'cancelled' && this._cancel_state_idx !== -1
|
|
207
|
+
&& this.cur_state < this._cancel_state_idx){
|
|
208
|
+
this.cur_state = this._cancel_state_idx;
|
|
209
|
+
} else {
|
|
210
|
+
// on error: find next catch; if none -> complete (finally will still run)
|
|
211
|
+
const nextErrIdx = this._next_error_handler(this.cur_state);
|
|
212
|
+
if (nextErrIdx === -1){
|
|
213
|
+
// no handler; complete
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
this.cur_state = nextErrIdx;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// synchronous value
|
|
223
|
+
if (stateInfo.catch)
|
|
224
|
+
this.error = undefined;
|
|
225
|
+
// Cancel state clears the error after handling (like catch does)
|
|
226
|
+
if (stateInfo.cancel)
|
|
227
|
+
this.error = undefined;
|
|
228
|
+
if (this.error === undefined){
|
|
229
|
+
this.retval = rv;
|
|
230
|
+
// advance to next non-aux state
|
|
231
|
+
this.cur_state = this._next_non_aux(this.cur_state);
|
|
232
|
+
} else {
|
|
233
|
+
// Check if this is a cancellation error and we have a cancel state (that we haven't passed yet)
|
|
234
|
+
if (this.error.message === 'cancelled' && this._cancel_state_idx !== -1
|
|
235
|
+
&& this.cur_state < this._cancel_state_idx){
|
|
236
|
+
this.cur_state = this._cancel_state_idx;
|
|
237
|
+
} else {
|
|
238
|
+
// on error: find next catch; if none -> complete (finally will still run)
|
|
239
|
+
const nextErrIdx = this._next_error_handler(this.cur_state);
|
|
240
|
+
if (nextErrIdx === -1){
|
|
241
|
+
// no handler; complete
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
this.cur_state = nextErrIdx;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// loop continues
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// finished loop -> execute finally state if exists (always runs)
|
|
251
|
+
const finallyIdx = this.states.findIndex(s => s.finally);
|
|
252
|
+
_ldbg(`[ITASK ${this.name}] Finished main loop, finallyIdx=${finallyIdx}, e=${this.error?.message}`);
|
|
253
|
+
if (finallyIdx !== -1 && !this._completed)
|
|
254
|
+
{
|
|
255
|
+
_ldbg(`[ITASK ${this.name}] Running finally state`);
|
|
256
|
+
const fn = this.funcs[finallyIdx];
|
|
257
|
+
try
|
|
258
|
+
{
|
|
259
|
+
// Use bind context if provided, otherwise use itask instance
|
|
260
|
+
const context = this.bind || this;
|
|
261
|
+
const rv = fn.call(context, this.error || this.retval);
|
|
262
|
+
if (rv instanceof Itask)
|
|
263
|
+
{
|
|
264
|
+
this.spawn(rv);
|
|
265
|
+
await rv;
|
|
266
|
+
}
|
|
267
|
+
else if (rv && typeof rv.then === 'function')
|
|
268
|
+
await rv;
|
|
269
|
+
_ldbg(`[ITASK ${this.name}] Finally state completed`);
|
|
270
|
+
}
|
|
271
|
+
catch (err)
|
|
272
|
+
{
|
|
273
|
+
_ldbg(`[ITASK ${this.name}] Finally state threw: ${err.message}`);
|
|
274
|
+
// finally errors override existing error
|
|
275
|
+
this.error = err;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Wait for all children to complete first (bottom-up completion)
|
|
280
|
+
if (this.child && this.child.size > 0) {
|
|
281
|
+
_ldbg(`[ITASK ${this.name}] Waiting for ${this.child.size} children to complete`);
|
|
282
|
+
const children = Array.from(this.child);
|
|
283
|
+
try {
|
|
284
|
+
// Wait for children with a configurable timeout (default 5000ms)
|
|
285
|
+
const timeoutMs = this.child_completion_timeout ?? 5000;
|
|
286
|
+
const childPromises = children.map(c => new Promise((resolve) => {
|
|
287
|
+
if (c._completed) {
|
|
288
|
+
resolve();
|
|
289
|
+
} else {
|
|
290
|
+
c._oncomplete.push(() => resolve());
|
|
291
|
+
}
|
|
292
|
+
}));
|
|
293
|
+
const timeout = new Promise((resolve) => setTimeout(() => {
|
|
294
|
+
_ldbg(`[ITASK ${this.name}] Timeout waiting for children after ${timeoutMs}ms, forcing completion`);
|
|
295
|
+
resolve();
|
|
296
|
+
}, timeoutMs));
|
|
297
|
+
await Promise.race([Promise.all(childPromises), timeout]);
|
|
298
|
+
} catch (e) { lerr.perr(e); }
|
|
299
|
+
_ldbg(`[ITASK ${this.name}] Done waiting for children`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// complete - error will propagate to parent if uncaught
|
|
303
|
+
_ldbg(`[ITASK ${this.name}] Calling _complete_internal, e=${this.error?.message}`);
|
|
304
|
+
this._complete_internal();
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// find next index > cur that is NOT aux (aux = catch/finally/cancel)
|
|
308
|
+
Itask.prototype._next_non_aux = function _next_non_aux(cur){
|
|
309
|
+
let i = cur + 1;
|
|
310
|
+
while (i < this.states.length && (this.states[i].catch || this.states[i].finally || this.states[i].cancel)) i++;
|
|
311
|
+
return i;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// on error, find next catch state that handles error
|
|
315
|
+
Itask.prototype._next_error_handler = function _next_error_handler(cur){
|
|
316
|
+
for (let i = cur + 1; i < this.states.length; i++){
|
|
317
|
+
if (this.states[i].catch) return i;
|
|
318
|
+
}
|
|
319
|
+
return -1;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
/* ---------- finalization ---------- */
|
|
323
|
+
Itask.prototype._complete_internal = function _complete_internal(){
|
|
324
|
+
if (this._completed) return;
|
|
325
|
+
_ldbg(`[ITASK ${this.name}] _complete_internal called, e=${this.error?.message}`);
|
|
326
|
+
this._completed = true;
|
|
327
|
+
this.tm_completed = Date.now();
|
|
328
|
+
this.running = false;
|
|
329
|
+
|
|
330
|
+
// run finally callbacks
|
|
331
|
+
try {
|
|
332
|
+
const context = this.bind || this;
|
|
333
|
+
for (const cb of this._finally_cbs){
|
|
334
|
+
try { cb.call(context, this.error, this.retval); } catch (e){ lerr.perr(e); }
|
|
335
|
+
}
|
|
336
|
+
} catch (e){ lerr.perr(e); }
|
|
337
|
+
|
|
338
|
+
// emit event
|
|
339
|
+
try { this.emit('finally', this.error, this.retval); } catch (e){ lerr.perr(e); }
|
|
340
|
+
|
|
341
|
+
// notify promise consumers
|
|
342
|
+
for (const cb of this._oncomplete){
|
|
343
|
+
try { cb(this.error, this.retval); } catch (e){ lerr.perr(e); }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// remove from parent's child set
|
|
347
|
+
if (this.parent) {
|
|
348
|
+
this.parent.child.delete(this);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// cleanup from root registry if present and no parent
|
|
352
|
+
if (this._root_registered && this.parent === undefined){
|
|
353
|
+
Itask.root.delete(this);
|
|
354
|
+
this._root_registered = false;
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
/* ---------- spawn / parent-child ---------- */
|
|
359
|
+
Itask.prototype.spawn = function spawn(child){
|
|
360
|
+
if (!child) return;
|
|
361
|
+
if (child instanceof Itask){
|
|
362
|
+
// attach as child
|
|
363
|
+
if (child.parent && child.parent !== this){
|
|
364
|
+
child.parent.child.delete(child);
|
|
365
|
+
}
|
|
366
|
+
child.parent = this;
|
|
367
|
+
this.child.add(child);
|
|
368
|
+
// if child was previously registered as root, remove it
|
|
369
|
+
if (child._root_registered){
|
|
370
|
+
Itask.root.delete(child);
|
|
371
|
+
child._root_registered = false;
|
|
372
|
+
}
|
|
373
|
+
// ensure async-created children begin execution
|
|
374
|
+
if (!child.running && !child._completed){
|
|
375
|
+
process.nextTick(() => {
|
|
376
|
+
try { child._run(); } catch (e){ lerr.perr(e); }
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
return child;
|
|
380
|
+
}
|
|
381
|
+
if (typeof child.then === 'function'){
|
|
382
|
+
// wrap promise into an Itask
|
|
383
|
+
const wrap = new Itask({ name: 'promise-wrap', cancel: false }, [function(){ return child; }]);
|
|
384
|
+
this.spawn(wrap);
|
|
385
|
+
return wrap;
|
|
386
|
+
}
|
|
387
|
+
throw new Error('spawn accepts Itask or Promise-like');
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
/* ---------- cancellation (cooperative) ---------- */
|
|
391
|
+
Itask.prototype._ecancel = function _ecancel(arg){
|
|
392
|
+
_ldbg(`[ITASK ${this.name}] _ecancel called, running=${this.running}, completed=${this._completed}, ` +
|
|
393
|
+
`cancel_state_idx=${this._cancel_state_idx}`);
|
|
394
|
+
this.cancel_arg = arg;
|
|
395
|
+
// if cancel state exists, jump to it
|
|
396
|
+
if (!this._completed){
|
|
397
|
+
this.error = new Error('cancelled');
|
|
398
|
+
_ldbg(`[ITASK ${this.name}] Set error to 'cancelled'`);
|
|
399
|
+
if (this._cancel_state_idx !== -1){
|
|
400
|
+
// ensure next iteration runs the cancel state
|
|
401
|
+
this.cur_state = Math.max(0, this._cancel_state_idx);
|
|
402
|
+
_ldbg(`[ITASK ${this.name}] Jumped to cancel state at index ${this.cur_state}`);
|
|
403
|
+
}
|
|
404
|
+
// if task is waiting on wait(), reject to unblock it
|
|
405
|
+
this._cancel_wait(this.error);
|
|
406
|
+
_ldbg(`[ITASK ${this.name}] Called _cancel_wait`);
|
|
407
|
+
}
|
|
408
|
+
// cancel children
|
|
409
|
+
this._ecancel_child();
|
|
410
|
+
// if idle (not running) finalize
|
|
411
|
+
if (!this.running) {
|
|
412
|
+
_ldbg(`[ITASK ${this.name}] Task not running, will complete after children`);
|
|
413
|
+
// Wait for children to complete, then call _complete_internal
|
|
414
|
+
setImmediate(async () => {
|
|
415
|
+
if (this.child && this.child.size > 0) {
|
|
416
|
+
_ldbg(`[ITASK ${this.name}] Waiting for ${this.child.size} children to complete`);
|
|
417
|
+
const children = Array.from(this.child);
|
|
418
|
+
try {
|
|
419
|
+
// Wait for children with a configurable timeout (default 5000ms)
|
|
420
|
+
const timeoutMs = this.child_completion_timeout ?? 5000;
|
|
421
|
+
const childPromises = children.map(c => new Promise((resolve) => {
|
|
422
|
+
if (c._completed) {
|
|
423
|
+
resolve();
|
|
424
|
+
} else {
|
|
425
|
+
c._oncomplete.push(() => resolve());
|
|
426
|
+
}
|
|
427
|
+
}));
|
|
428
|
+
const timeout = new Promise((resolve) => setTimeout(() => {
|
|
429
|
+
_ldbg(`[ITASK ${this.name}] Timeout waiting for children after ${timeoutMs}ms, `+
|
|
430
|
+
`forcing completion`);
|
|
431
|
+
resolve();
|
|
432
|
+
}, timeoutMs));
|
|
433
|
+
await Promise.race([Promise.all(childPromises), timeout]);
|
|
434
|
+
} catch (e) { lerr.perr(e); }
|
|
435
|
+
_ldbg(`[ITASK ${this.name}] Done waiting for children`);
|
|
436
|
+
}
|
|
437
|
+
this._complete_internal();
|
|
438
|
+
});
|
|
439
|
+
} else {
|
|
440
|
+
_ldbg(`[ITASK ${this.name}] Task still running, will complete naturally`);
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
Itask.prototype._ecancel_child = function _ecancel_child(){
|
|
445
|
+
if (!this.child || !this.child.size) return;
|
|
446
|
+
const children = Array.from(this.child);
|
|
447
|
+
for (const c of children){
|
|
448
|
+
try { c._ecancel(); } catch (e){ lerr.perr(e); }
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
/* ---------- thenable / promise interop ---------- */
|
|
453
|
+
Itask.prototype.then = function then(onRes, onErr){
|
|
454
|
+
return new Promise((resolve, reject) => {
|
|
455
|
+
if (this._completed){
|
|
456
|
+
return this.error ? reject(this.error) : resolve(this.retval);
|
|
457
|
+
}
|
|
458
|
+
this._oncomplete.push((err, res) => {
|
|
459
|
+
if (err) return reject(err);
|
|
460
|
+
return resolve(res);
|
|
461
|
+
});
|
|
462
|
+
}).then(onRes, onErr);
|
|
463
|
+
};
|
|
464
|
+
Itask.prototype.catch = function(onErr){ return this.then(undefined, onErr); };
|
|
465
|
+
Itask.prototype.finally = function finally_(cb, name){
|
|
466
|
+
if (typeof cb === 'function'){
|
|
467
|
+
if (this._completed) {
|
|
468
|
+
const context = this.bind || this;
|
|
469
|
+
try { cb.call(context, this.error, this.retval); } catch (e){ lerr.perr(e); }
|
|
470
|
+
} else {
|
|
471
|
+
this._finally_cbs.push(cb);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return this;
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
/* ---------- utilities ---------- */
|
|
478
|
+
Itask.sleep = function sleep(ms){
|
|
479
|
+
return new Itask({ name: 'sleep', cancel: true }, [function(){
|
|
480
|
+
return new Promise((resolve) => {
|
|
481
|
+
const t = setTimeout(() => resolve(), ms);
|
|
482
|
+
this.finally(()=> clearTimeout(t));
|
|
483
|
+
});
|
|
484
|
+
}]);
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
Itask.all = function all(arr){
|
|
488
|
+
if (!Array.isArray(arr)) throw new Error('Itask.all expects array');
|
|
489
|
+
return new Itask({ name: 'all', cancel: true }, [function(){
|
|
490
|
+
const tasks = arr.map(x => (x instanceof Itask) ? x : new Itask({ name: 'wrap' }, [function(){ return x; }]));
|
|
491
|
+
for (const t of tasks) this.spawn(t);
|
|
492
|
+
return Promise.all(tasks.map(t => t));
|
|
493
|
+
}]);
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
/* ---------- convenience: external completion ---------- */
|
|
497
|
+
Itask.prototype.return = function(ret){
|
|
498
|
+
if (this._completed) return this;
|
|
499
|
+
this.retval = ret;
|
|
500
|
+
this.error = undefined;
|
|
501
|
+
this._complete_internal();
|
|
502
|
+
return this;
|
|
503
|
+
};
|
|
504
|
+
Itask.prototype.throw = function(err){
|
|
505
|
+
if (this._completed) return this;
|
|
506
|
+
this.error = (err instanceof Error) ? err : new Error(String(err));
|
|
507
|
+
this._complete_internal();
|
|
508
|
+
return this;
|
|
509
|
+
};
|
|
510
|
+
Itask.prototype.wait = function(){
|
|
511
|
+
return new Promise((resolve, reject) => {
|
|
512
|
+
this._continue_resolve = resolve;
|
|
513
|
+
this._continue_reject = reject;
|
|
514
|
+
});
|
|
515
|
+
};
|
|
516
|
+
Itask.prototype._cancel_wait = function(err){
|
|
517
|
+
if (this._continue_reject){
|
|
518
|
+
_ldbg(`[ITASK ${this.name}] _cancel_wait: rejecting pending wait() with error`);
|
|
519
|
+
const reject = this._continue_reject;
|
|
520
|
+
this._continue_resolve = null;
|
|
521
|
+
this._continue_reject = null;
|
|
522
|
+
reject(err || new Error('cancelled'));
|
|
523
|
+
} else if (this._continue_resolve){
|
|
524
|
+
_ldbg(`[ITASK ${this.name}] _cancel_wait: resolving pending wait() with undefined`);
|
|
525
|
+
const resolve = this._continue_resolve;
|
|
526
|
+
this._continue_resolve = null;
|
|
527
|
+
this._continue_reject = null;
|
|
528
|
+
resolve(undefined);
|
|
529
|
+
} else {
|
|
530
|
+
_ldbg(`[ITASK ${this.name}] _cancel_wait: no pending wait()`);
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
Itask.prototype.continue = function(ret){
|
|
534
|
+
if (this._completed)
|
|
535
|
+
return this;
|
|
536
|
+
this.retval = ret;
|
|
537
|
+
this.error = undefined;
|
|
538
|
+
if (this._continue_resolve)
|
|
539
|
+
{
|
|
540
|
+
const resolve = this._continue_resolve;
|
|
541
|
+
this._continue_resolve = null;
|
|
542
|
+
this._continue_reject = null;
|
|
543
|
+
resolve(ret);
|
|
544
|
+
}
|
|
545
|
+
return this;
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
/* ---------- introspection / ps ---------- */
|
|
549
|
+
Itask.prototype.is_running = function(){ return this.running && !this._completed; };
|
|
550
|
+
Itask.prototype.is_completed = function(){ return this._completed; };
|
|
551
|
+
Itask.prototype.shortname = function(){ return this.name || ('itask#'+this.id); };
|
|
552
|
+
|
|
553
|
+
Itask.prototype._ps_lines = function(prefix, last){
|
|
554
|
+
const parts = [];
|
|
555
|
+
const marker = last ? '\\_ ' : '|\\_ ';
|
|
556
|
+
const own = prefix + (last ? '\\_ ' : '|\\_ ') + this.shortname() +
|
|
557
|
+
(this._completed ? ' (done)' : '') + ' [' + this.id + ']';
|
|
558
|
+
parts.push(own);
|
|
559
|
+
const kids = Array.from(this.child);
|
|
560
|
+
for (let i = 0; i < kids.length; i++){
|
|
561
|
+
const isLast = i === kids.length - 1;
|
|
562
|
+
const child = kids[i];
|
|
563
|
+
const childPrefix = prefix + (last ? ' ' : '| ');
|
|
564
|
+
parts.push(...child._ps_lines(childPrefix, isLast));
|
|
565
|
+
}
|
|
566
|
+
return parts;
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
Itask.ps = function ps(){
|
|
570
|
+
let out = '';
|
|
571
|
+
const roots = Array.from(Itask.root);
|
|
572
|
+
for (let i = 0; i < roots.length; i++){
|
|
573
|
+
const r = roots[i];
|
|
574
|
+
const lines = r._ps_lines('', i === roots.length - 1);
|
|
575
|
+
out += lines.join('\n') + (i < roots.length - 1 ? '\n' : '');
|
|
576
|
+
}
|
|
577
|
+
return out || '<no roots>';
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
/* ---------- context management ---------- */
|
|
581
|
+
// Get the context for this task, optionally creating one if needed
|
|
582
|
+
Itask.prototype.getContext = function getContext(createIfMissing = false){
|
|
583
|
+
if (this.context)
|
|
584
|
+
return this.context;
|
|
585
|
+
if (createIfMissing && this.prompt){
|
|
586
|
+
// Lazy context creation - requires Context class to be set
|
|
587
|
+
if (Itask.Context){
|
|
588
|
+
this.context = new Itask.Context(this.prompt, this, this._contextConfig);
|
|
589
|
+
return this.context;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return null;
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
// Set context for this task
|
|
596
|
+
Itask.prototype.setContext = function setContext(context){
|
|
597
|
+
this.context = context;
|
|
598
|
+
if (context && typeof context.setTask === 'function')
|
|
599
|
+
context.setTask(this);
|
|
600
|
+
return this;
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
// Get all ancestor contexts (walking up the task hierarchy)
|
|
604
|
+
Itask.prototype.getAncestorContexts = function getAncestorContexts(){
|
|
605
|
+
const contexts = [];
|
|
606
|
+
let task = this;
|
|
607
|
+
while (task){
|
|
608
|
+
if (task.context)
|
|
609
|
+
contexts.unshift(task.context); // Add to front so ancestors come first
|
|
610
|
+
task = task.parent;
|
|
611
|
+
}
|
|
612
|
+
return contexts;
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
// Find the nearest context in the hierarchy (this task or ancestors)
|
|
616
|
+
Itask.prototype.findContext = function findContext(){
|
|
617
|
+
let task = this;
|
|
618
|
+
while (task){
|
|
619
|
+
if (task.context)
|
|
620
|
+
return task.context;
|
|
621
|
+
task = task.parent;
|
|
622
|
+
}
|
|
623
|
+
return null;
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
// Send a message using the context hierarchy
|
|
627
|
+
// Delegates to the task's context, or walks up to find one
|
|
628
|
+
Itask.prototype.sendMessage = async function sendMessage(role, content, functions, opts){
|
|
629
|
+
// First try our own context
|
|
630
|
+
let ctx = this.getContext();
|
|
631
|
+
if (!ctx){
|
|
632
|
+
// Walk up to find a context
|
|
633
|
+
ctx = this.findContext();
|
|
634
|
+
}
|
|
635
|
+
if (!ctx){
|
|
636
|
+
throw new Error('No context available in task hierarchy to send message');
|
|
637
|
+
}
|
|
638
|
+
// Don't pass functions here - let context aggregate from hierarchy
|
|
639
|
+
// If caller wants to override, they can pass functions in opts
|
|
640
|
+
return ctx.sendMessage(role, content, functions, opts);
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
// Aggregate functions from all contexts in the hierarchy
|
|
644
|
+
Itask.prototype.getHierarchyFunctions = function getHierarchyFunctions(){
|
|
645
|
+
const allFunctions = [];
|
|
646
|
+
const contexts = this.getAncestorContexts();
|
|
647
|
+
for (const ctx of contexts){
|
|
648
|
+
if (ctx.functions && Array.isArray(ctx.functions))
|
|
649
|
+
allFunctions.push(...ctx.functions);
|
|
650
|
+
}
|
|
651
|
+
// Add this task's own functions if not already in a context
|
|
652
|
+
if (this.functions && !this.context)
|
|
653
|
+
allFunctions.push(...this.functions);
|
|
654
|
+
return allFunctions;
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
// Close this task's context (if any) and bubble summary to parent
|
|
658
|
+
Itask.prototype.closeContext = async function closeContext(){
|
|
659
|
+
if (!this.context)
|
|
660
|
+
return;
|
|
661
|
+
await this.context.close();
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
// Reference to Context class (set by index.js to avoid circular dependency)
|
|
665
|
+
Itask.Context = null;
|
|
666
|
+
|
|
667
|
+
/* ---------- export ---------- */
|
|
668
|
+
module.exports = Itask;
|
|
669
|
+
|
|
670
|
+
/* ---------- notes ----------
|
|
671
|
+
* - Named states: function names can carry modifiers and labels:
|
|
672
|
+
* e.g. function try_catch$mylabel(){} -> parsed as try_catch with label mylabel
|
|
673
|
+
* supported modifiers: try, catch, finally, cancel
|
|
674
|
+
* - _ecancel() will attempt to jump to a cancel state if declared; otherwise it
|
|
675
|
+
* sets error='cancelled' and propagates cancellation to children.
|
|
676
|
+
* - Itask.root contains root tasks (tasks with no parent).
|
|
677
|
+
* - ps() returns a readable hierarchical snapshot for debugging.
|
|
678
|
+
*/
|