saico 2.8.0 → 2.8.1

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/README.md CHANGED
@@ -260,7 +260,7 @@ agent.getSessionInfo();
260
260
  await agent.closeSession(); // Saves full state to Store, cancels task
261
261
 
262
262
  // Restore from Store
263
- const restored = await Saico.rehydrate(agent._id, { store });
263
+ const restored = await Saico.rehydrate(agent.id, { store });
264
264
  ```
265
265
 
266
266
  ## Database Access
@@ -307,7 +307,7 @@ const restored = Saico.deserialize(json);
307
307
 
308
308
  // Durable persistence (compressed msgs, saved to Store)
309
309
  await agent.closeSession();
310
- const restored2 = await Saico.rehydrate(agent._id, { store });
310
+ const restored2 = await Saico.rehydrate(agent.id, { store });
311
311
  ```
312
312
 
313
313
  `serialize()` includes: id, name, prompt, userData, sessionConfig, tm_create, isolate, and full context state (raw messages, tool_digest). `closeSession()` saves the same shape but with compressed messages for durable storage.
@@ -330,7 +330,7 @@ Properties prefixed with `_` are internal and not persisted.
330
330
 
331
331
  ## Tool Implementation (TOOL_ methods)
332
332
 
333
- Define tool implementations as `TOOL_`-prefixed methods on your Saico subclass. When the LLM returns a tool call, Context automatically searches the Saico hierarchy (current → up parents → down children) to find and invoke the matching method with parsed arguments.
333
+ Define tool implementations as `TOOL_`-prefixed methods on your Saico subclass. When the LLM returns a tool call, Saico automatically searches the task hierarchy (current → up parents → down children) to find and invoke the matching method with parsed arguments.
334
334
 
335
335
  ```js
336
336
  class MyAgent extends Saico {
@@ -359,13 +359,13 @@ Return a string or `{ content: string, functions?: [] }`.
359
359
 
360
360
  ## Low-Level API
361
361
 
362
- For cases where you need a standalone context without the Saico master class:
362
+ For cases where you need a standalone message queue without the Saico master class:
363
363
 
364
364
  ```js
365
- const { createContext } = require('saico');
365
+ const { createMsgs } = require('saico');
366
366
 
367
- // Standalone context
368
- const ctx = createContext('System prompt', null, { tag: 'my-tag', token_limit: 4000 });
367
+ // Standalone message queue
368
+ const ctx = createMsgs('System prompt', { tag: 'my-tag', token_limit: 4000 });
369
369
  const reply = await ctx.sendMessage('user', 'Hello', functions);
370
370
  ```
371
371
 
@@ -390,7 +390,7 @@ saico/
390
390
  npm test
391
391
  ```
392
392
 
393
- 296 tests covering Saico lifecycle, context ownership, spawn/spawnAndRun, task hierarchy, message handling, tool calls, DB adapters, serialization, persistence (closeSession/rehydrate), and integration flows.
393
+ 290 tests covering Saico lifecycle, context ownership, spawn/spawnAndRun, task hierarchy, message handling, tool calls, DB adapters, serialization, persistence (closeSession/rehydrate), and integration flows.
394
394
 
395
395
  ## Requirements
396
396
 
package/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const Itask = require('./itask.js');
4
- const { Context, createContext } = require('./msgs.js');
4
+ const { Msgs, createMsgs } = require('./msgs.js');
5
5
  const { Store, DynamoBackend } = require('./store.js');
6
6
  const { Saico } = require('./saico.js');
7
7
  const { DynamoDBAdapter } = require('./dynamo.js');
@@ -34,7 +34,7 @@ module.exports = {
34
34
 
35
35
  // Core classes
36
36
  Itask,
37
- Context,
37
+ Msgs,
38
38
  Store,
39
39
  DynamoBackend,
40
40
 
@@ -42,7 +42,7 @@ module.exports = {
42
42
  init,
43
43
 
44
44
  // Factory
45
- createContext,
45
+ createMsgs,
46
46
 
47
47
  // Utilities (re-export from util.js)
48
48
  util: require('./util.js'),
package/msgs.js CHANGED
@@ -8,23 +8,17 @@ const { _log, _lerr, _ldbg } = util;
8
8
  const debug = 0;
9
9
 
10
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
11
+ * Msgs - Pure message queue with tool call handling, summarization, and LLM communication.
12
+ * Saico sets callback hooks after construction to wire in hierarchy access.
18
13
  */
19
- class Context {
20
- constructor(prompt, task, config = {}) {
14
+ class Msgs {
15
+ constructor(prompt, config = {}) {
21
16
  this.prompt = prompt;
22
- this.task = task; // Reference to owning Itask (replaces parent)
23
17
  this.tag = config.tag || crypto.randomBytes(4).toString('hex');
24
18
  this.token_limit = config.token_limit || 1000000000;
25
19
  this.lower_limit = this.token_limit * 0.85;
26
20
  this.upper_limit = this.token_limit * 0.98;
27
- this.functions = config.functions || task?.functions || null;
21
+ this.functions = config.functions || null;
28
22
 
29
23
  // Recursive depth and repetition control
30
24
  this.max_depth = config.max_depth || 5;
@@ -50,18 +44,15 @@ class Context {
50
44
  // Tool digest — persistent history of tool calls that mutated task state
51
45
  this.tool_digest = config.tool_digest || [];
52
46
 
47
+ // Callback hooks — set by Saico after construction
48
+ this._findToolImpl = null; // (toolName) => { saico, methodName } | null
49
+ this._getSnapshot = null; // () => serializable snapshot for dirty detection
50
+
53
51
  // Initialize messages: explicit msgs take priority over chat_history
54
52
  this._chat_history = config.chat_history || null;
55
53
  (config.msgs || []).forEach(m => this.push(m));
56
54
 
57
- _log('created Context for tag', this.tag);
58
- }
59
-
60
- // Set the task reference (used when context is created separately)
61
- setTask(task) {
62
- this.task = task;
63
- if (!this.functions)
64
- this.functions = task?.functions;
55
+ _log('created Msgs for tag', this.tag);
65
56
  }
66
57
 
67
58
  /**
@@ -135,32 +126,6 @@ class Context {
135
126
  this.tool_digest = this.tool_digest.slice(-this.TOOL_DIGEST_LIMIT);
136
127
  }
137
128
 
138
- // Get the parent context by traversing task hierarchy (via Saico)
139
- getParentContext() {
140
- if (!this.task || !this.task.parent)
141
- return null;
142
- let task = this.task.parent;
143
- while (task) {
144
- if (task._saico?.context) return task._saico.context;
145
- task = task.parent;
146
- }
147
- return null;
148
- }
149
-
150
- // Get all ancestor contexts via task hierarchy (via Saico)
151
- getAncestorContexts() {
152
- if (!this.task)
153
- return [];
154
- const contexts = [];
155
- let task = this.task.parent;
156
- while (task) {
157
- if (task._saico?.context)
158
- contexts.unshift(task._saico.context);
159
- task = task.parent;
160
- }
161
- return contexts;
162
- }
163
-
164
129
  _hasPendingToolCalls() {
165
130
  const toolCallMsgs = this._msgs.filter(m => m.msg.tool_calls);
166
131
 
@@ -334,16 +299,15 @@ class Context {
334
299
  };
335
300
  } else {
336
301
  this._trackActiveToolCall(call);
337
- const _snap = this.task
338
- ? JSON.stringify(this._snapshotPublicProps(this.task)) : null;
302
+ const _snap = this._getSnapshot
303
+ ? JSON.stringify(this._getSnapshot()) : null;
339
304
 
340
305
  try {
341
306
  const correspondingDeferred = deferredGroup.find(d => d.call.id === call.id);
342
307
  const timeout = correspondingDeferred?.originalMessage.opts.timeout;
343
308
 
344
309
  result = await this._executeToolCallWithTimeout(call, timeout);
345
- if (_snap !== null &&
346
- _snap !== JSON.stringify(this._snapshotPublicProps(this.task)))
310
+ if (_snap !== null && _snap !== JSON.stringify(this._getSnapshot()))
347
311
  this._appendToolDigest(call.function.name, result?.content || '');
348
312
  } finally {
349
313
  this._completeActiveToolCall(call);
@@ -433,24 +397,6 @@ class Context {
433
397
 
434
398
  getSummaries() { return this._msgs.filter(m => m.opts.summary); }
435
399
 
436
- // Get functions aggregated from this context and all ancestor contexts
437
- getFunctions() {
438
- const allFunctions = [];
439
-
440
- // Get functions from ancestor contexts via task hierarchy
441
- const ancestorContexts = this.getAncestorContexts();
442
- for (const ctx of ancestorContexts) {
443
- if (ctx.functions && Array.isArray(ctx.functions))
444
- allFunctions.push(...ctx.functions);
445
- }
446
-
447
- // Add our own functions
448
- if (this.functions && Array.isArray(this.functions))
449
- allFunctions.push(...this.functions);
450
-
451
- return allFunctions.length > 0 ? allFunctions : null;
452
- }
453
-
454
400
  async summarizeMessages() {
455
401
  const tokens = util.countTokens(this.__msgs);
456
402
  if (tokens < this.lower_limit)
@@ -459,7 +405,7 @@ class Context {
459
405
  }
460
406
 
461
407
  async close() {
462
- _log('Closing Context tag', this.tag);
408
+ _log('Closing Msgs tag', this.tag);
463
409
 
464
410
  if (this._sequential_mode && this._processing_sequential) {
465
411
  _ldbg('Sequential mode: waiting for current message to complete before closing tag', this.tag);
@@ -472,22 +418,8 @@ class Context {
472
418
  }
473
419
  }
474
420
 
475
- // Move waiting messages to parent context via task hierarchy
476
- const parentCtx = this.getParentContext();
477
- if (parentCtx && this._waitingQueue.length > 0) {
478
- _log('Moving', this._waitingQueue.length, 'waiting messages to parent context');
479
- parentCtx._waitingQueue.push(...this._waitingQueue);
480
- this._waitingQueue = [];
481
- }
482
-
483
- if (parentCtx && this._sequential_queue.length > 0) {
484
- _log('Moving', this._sequential_queue.length, 'sequential queue messages to parent context');
485
- parentCtx._sequential_queue.push(...this._sequential_queue);
486
- this._sequential_queue = [];
487
- }
488
-
489
- await this._summarizeContext(true, parentCtx);
490
- _log('Finished closing Context tag', this.tag);
421
+ await this._summarizeContext(true);
422
+ _log('Finished closing Msgs tag', this.tag);
491
423
  }
492
424
 
493
425
 
@@ -591,38 +523,6 @@ class Context {
591
523
  return summary;
592
524
  }
593
525
 
594
- // Get message context - walks up task hierarchy to collect prompts and summaries
595
- getMsgContext(add_tag) {
596
- const msgs = [];
597
-
598
- // Get context from ancestor tasks via task hierarchy
599
- const ancestorContexts = this.getAncestorContexts();
600
- for (const ctx of ancestorContexts) {
601
- if (ctx.prompt)
602
- msgs.push({role: 'system', content: ctx.prompt});
603
- // Add summaries from ancestor contexts
604
- const summaries = ctx._msgs.filter(m => m.opts.summary || m.msg.role === 'system').map(m => {
605
- if (add_tag)
606
- m.msg.tag = ctx.tag;
607
- return m.msg;
608
- });
609
- msgs.push(...summaries);
610
- }
611
-
612
- // Add this context's prompt
613
- if (this.prompt)
614
- msgs.push({role: 'system', content: this.prompt});
615
-
616
- // Add this context's summaries
617
- const mySummaries = this._msgs.filter(m => m.opts.summary || m.msg.role === 'system').map(m => {
618
- if (add_tag)
619
- m.msg.tag = this.tag;
620
- return m.msg;
621
- });
622
-
623
- return msgs.concat(mySummaries);
624
- }
625
-
626
526
  _createMsgObj(role, content, functions, opts) {
627
527
  const name = opts?.name;
628
528
  const tool_call_id = opts?.tool_call_id;
@@ -952,10 +852,10 @@ class Context {
952
852
  ? [...o.opts._aggregatedFunctions, ...messageFuncs]
953
853
  : null;
954
854
  } else {
955
- const hierarchyFuncs = this.getFunctions() || [];
855
+ const ownFuncs = this.functions || [];
956
856
  const messageFuncs = o.functions || [];
957
- funcs = [...hierarchyFuncs, ...messageFuncs].length > 0
958
- ? [...hierarchyFuncs, ...messageFuncs]
857
+ funcs = [...ownFuncs, ...messageFuncs].length > 0
858
+ ? [...ownFuncs, ...messageFuncs]
959
859
  : null;
960
860
  }
961
861
 
@@ -1024,15 +924,15 @@ class Context {
1024
924
 
1025
925
  for (const { call, isDuplicate } of toolCallsWithResults) {
1026
926
  if (!isDuplicate) {
1027
- const _snap = this.task
1028
- ? JSON.stringify(this._snapshotPublicProps(this.task)) : null;
927
+ const _snap = this._getSnapshot
928
+ ? JSON.stringify(this._getSnapshot()) : null;
1029
929
  try {
1030
930
  const result = await this._executeToolCallWithTimeout(
1031
931
  call, o.opts?.timeout);
1032
932
  const item = toolCallsWithResults.find(item => item.call.id === call.id);
1033
933
  if (item) item.result = result;
1034
934
  if (_snap !== null &&
1035
- _snap !== JSON.stringify(this._snapshotPublicProps(this.task)))
935
+ _snap !== JSON.stringify(this._getSnapshot()))
1036
936
  this._appendToolDigest(call.function.name, result?.content || '');
1037
937
  } finally {
1038
938
  this._completeActiveToolCall(call);
@@ -1088,39 +988,11 @@ class Context {
1088
988
  }
1089
989
 
1090
990
  /**
1091
- * Search the Saico hierarchy for a TOOL_<toolName> method.
1092
- * Order: current task walk UP parents → walk DOWN children (BFS).
991
+ * Find a TOOL_<toolName> implementation. Delegates to _findToolImpl callback
992
+ * set by Saico, which searches the hierarchy.
1093
993
  */
1094
994
  _findToolImplementation(toolName) {
1095
- const methodName = 'TOOL_' + toolName;
1096
- const check = (task) =>
1097
- task?._saico && typeof task._saico[methodName] === 'function' ? task._saico : null;
1098
-
1099
- // 1. Current task
1100
- let found = check(this.task);
1101
- if (found) return { saico: found, methodName };
1102
-
1103
- // 2. Walk UP parent chain
1104
- let t = this.task?.parent;
1105
- while (t) {
1106
- found = check(t);
1107
- if (found) return { saico: found, methodName };
1108
- t = t.parent;
1109
- }
1110
-
1111
- // 3. Walk DOWN from this.task (BFS)
1112
- if (this.task) {
1113
- const queue = [...this.task.child];
1114
- while (queue.length > 0) {
1115
- const child = queue.shift();
1116
- if (child._completed) continue;
1117
- found = check(child);
1118
- if (found) return { saico: found, methodName };
1119
- if (child.child?.size > 0) queue.push(...child.child);
1120
- }
1121
- }
1122
-
1123
- return null;
995
+ return this._findToolImpl ? this._findToolImpl(toolName) : null;
1124
996
  }
1125
997
 
1126
998
  async interpretAndApplyChanges(call) {
@@ -1169,35 +1041,11 @@ class Context {
1169
1041
  return { content, functions };
1170
1042
  }
1171
1043
 
1172
- // Spawn child context (creates a child task with its own context)
1173
- spawnChild(prompt, tag, config = {}) {
1174
- if (!this.task) {
1175
- // If no task, create a standalone context
1176
- return createContext(prompt, null, { ...config, tag });
1177
- }
1178
-
1179
- // Create a child task with its own context
1180
- const Itask = require('./itask.js');
1181
- const childTask = new Itask({
1182
- name: tag || 'child-context',
1183
- async: true,
1184
- }, []);
1185
- this.task.spawn(childTask);
1186
-
1187
- const childContext = new Context(prompt, childTask, { ...config, tag });
1188
- // Store context on Saico if present, otherwise just set on task reference
1189
- if (childTask._saico) {
1190
- childTask._saico.context = childContext;
1191
- childTask._saico.context_id = childContext.tag;
1192
- }
1193
-
1194
- return childContext;
1195
- }
1196
1044
  }
1197
1045
 
1198
- // Factory function to create a Context with Proxy wrapper
1199
- function createContext(prompt, task, config = {}) {
1200
- const instance = new Context(prompt, task, config);
1046
+ // Factory function to create a Msgs instance with Proxy wrapper
1047
+ function createMsgs(prompt, config = {}) {
1048
+ const instance = new Msgs(prompt, config);
1201
1049
 
1202
1050
  return new Proxy(instance, {
1203
1051
  get(target, prop, receiver) {
@@ -1246,4 +1094,4 @@ function createContext(prompt, task, config = {}) {
1246
1094
  });
1247
1095
  }
1248
1096
 
1249
- module.exports = { Context, createContext };
1097
+ module.exports = { Msgs, createMsgs };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saico",
3
- "version": "2.8.0",
3
+ "version": "2.8.1",
4
4
  "main": "index.js",
5
5
  "type": "commonjs",
6
6
  "description": "Hierarchical AI Conversation Orchestrator - Task hierarchy with conversation contexts",
package/saico.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  const crypto = require('crypto');
4
4
  const Itask = require('./itask.js');
5
- const { Context } = require('./msgs.js');
5
+ const { Msgs } = require('./msgs.js');
6
6
  const { Store } = require('./store.js');
7
7
  const util = require('./util.js');
8
8
 
@@ -47,7 +47,7 @@ class Saico {
47
47
  */
48
48
  constructor(opt = {}) {
49
49
  // Internal properties (underscore-prefixed, not persisted to Redis)
50
- this._id = opt.id || crypto.randomBytes(8).toString('hex');
50
+ this.id = opt.id || crypto.randomBytes(8).toString('hex');
51
51
  this._task = null;
52
52
  this._store = opt.store || Store.instance || null;
53
53
  this._opt = opt;
@@ -91,7 +91,7 @@ class Saico {
91
91
  try {
92
92
  const redis = require('./redis.js');
93
93
  if (redis.rclient && opt.redis !== false) {
94
- const key = 'saico:' + (opt.key || this._id);
94
+ const key = 'saico:' + (opt.key || this.id);
95
95
  return redis.createObservableForRedis(key, this);
96
96
  }
97
97
  } catch (e) { /* redis not available */ }
@@ -159,8 +159,14 @@ class Saico {
159
159
  const augmentedPrompt = effectivePrompt
160
160
  ? effectivePrompt + Saico.BACKEND_EXPLANATION
161
161
  : '';
162
- const context = new Context(augmentedPrompt, this._task, contextConfig);
163
- this.setContext(context);
162
+ const msgs = new Msgs(augmentedPrompt, contextConfig);
163
+ this.context = msgs;
164
+ this.context_id = makeId(16);
165
+ msgs.tag = this.context_id;
166
+
167
+ // Wire callbacks for hierarchy access
168
+ msgs._findToolImpl = (toolName) => this._findToolImpl(toolName);
169
+ msgs._getSnapshot = () => msgs._snapshotPublicProps(this);
164
170
  }
165
171
 
166
172
  return this;
@@ -168,29 +174,6 @@ class Saico {
168
174
 
169
175
  // ---- Context management (owned by Saico, not Itask) ----
170
176
 
171
- /**
172
- * Set context on this Saico instance.
173
- * Generates context_id, sets context.tag, and calls context.setTask().
174
- */
175
- setContext(context) {
176
- this.context = context;
177
- // Generate context_id if not already set
178
- if (!this.context_id) {
179
- if (this._store)
180
- this.context_id = this._store.generateId();
181
- else if (Store.instance)
182
- this.context_id = Store.instance.generateId();
183
- else
184
- this.context_id = makeId(16);
185
- }
186
- if (context) {
187
- context.tag = this.context_id;
188
- if (typeof context.setTask === 'function')
189
- context.setTask(this._task);
190
- }
191
- return this;
192
- }
193
-
194
177
  /**
195
178
  * Find the nearest context walking UP the Saico/task hierarchy.
196
179
  */
@@ -444,6 +427,41 @@ class Saico {
444
427
  return parts.length > 0 ? parts : null;
445
428
  }
446
429
 
430
+ // ---- Tool implementation search ----
431
+
432
+ /**
433
+ * Search the Saico hierarchy for a TOOL_<toolName> method.
434
+ * Order: current task → walk UP parents → walk DOWN children (BFS).
435
+ */
436
+ _findToolImpl(toolName) {
437
+ const methodName = 'TOOL_' + toolName;
438
+ const check = (task) =>
439
+ task?._saico && typeof task._saico[methodName] === 'function' ? task._saico : null;
440
+
441
+ let found = check(this._task);
442
+ if (found) return { saico: found, methodName };
443
+
444
+ let t = this._task?.parent;
445
+ while (t) {
446
+ found = check(t);
447
+ if (found) return { saico: found, methodName };
448
+ t = t.parent;
449
+ }
450
+
451
+ if (this._task) {
452
+ const queue = [...this._task.child];
453
+ while (queue.length > 0) {
454
+ const child = queue.shift();
455
+ if (child._completed) continue;
456
+ found = check(child);
457
+ if (found) return { saico: found, methodName };
458
+ if (child.child?.size > 0) queue.push(...child.child);
459
+ }
460
+ }
461
+
462
+ return null;
463
+ }
464
+
447
465
  // ---- User Data (absorbed from Sid) ----
448
466
 
449
467
  setUserData(key, value) {
@@ -464,7 +482,7 @@ class Saico {
464
482
 
465
483
  getSessionInfo() {
466
484
  return {
467
- id: this._id,
485
+ id: this.id,
468
486
  name: this.name,
469
487
  running: this._task?.running || false,
470
488
  completed: this._task?._completed || false,
@@ -488,7 +506,7 @@ class Saico {
488
506
  if (store && this.context) {
489
507
  const { chat_history, tool_digest } = await this.context.prepareForStorage();
490
508
  const data = {
491
- id: this._id,
509
+ id: this.id,
492
510
  name: this.name,
493
511
  prompt: this.prompt,
494
512
  userData: this.userData,
@@ -504,7 +522,7 @@ class Saico {
504
522
  functions: this.context.functions,
505
523
  },
506
524
  };
507
- await store.save(this._id, data);
525
+ await store.save(this.id, data);
508
526
  }
509
527
 
510
528
  this._task._ecancel();
@@ -617,7 +635,7 @@ class Saico {
617
635
  */
618
636
  serialize() {
619
637
  const data = {
620
- id: this._id,
638
+ id: this.id,
621
639
  name: this.name,
622
640
  prompt: this.prompt,
623
641
  userData: this.userData,