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/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
+ */