saico 2.9.0 → 2.9.2

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.
Files changed (3) hide show
  1. package/README.md +19 -13
  2. package/package.json +1 -1
  3. package/saico.js +39 -27
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Saico - Hierarchical AI Conversation Orchestrator
1
+ # Saico - Simple AI-Agent Conversation Orchestrator
2
2
 
3
3
  Saico is a Node.js library for building AI agents with hierarchical conversations, automatic context aggregation, and enterprise-grade tool calling. It manages nested task trees where each node can have its own message queue, system prompt, tools, and state — and the library automatically assembles the full payload sent to the LLM by walking the tree.
4
4
 
@@ -210,7 +210,7 @@ new Saico({
210
210
  // Storage
211
211
  redis: true, // Set false to skip Redis proxy
212
212
  key: 'custom-redis-key',
213
- store: 'my-table', // Table name for instance persistence (closeSession/rehydrate)
213
+ store: 'my-table', // Table name for instance persistence (store/closeSession/restore)
214
214
  dynamodb: { // DynamoDB config (creates instance-level adapter)
215
215
  region: 'us-east-1',
216
216
  credentials: { accessKeyId: '...', secretAccessKey: '...' },
@@ -258,28 +258,33 @@ agent.getSessionInfo();
258
258
  // userData, uptime
259
259
  // }
260
260
 
261
- await agent.closeSession(); // prepareForStorage + save to registered backend, cancels task
261
+ await agent.store(); // save to registered backend independently
262
+ await agent.closeSession(); // store + cancel task
262
263
 
263
264
  // Restore from registered backend
264
- const restored = await Saico.rehydrate(agent.id, { store: 'sessions' });
265
+ const restored = await Saico.restore(agent.id, { store: 'sessions' });
265
266
  ```
266
267
 
267
268
  ## Database Access
268
269
 
269
- Saico provides backend-agnostic DB methods. Configure via `Saico.registerBackend('dynamodb', config)` (library-level), `opt.dynamodb` (instance-level auto-creates adapter), or `opt.db` (any adapter). Table name is required on every call. Child Saico instances without their own DB inherit the parent's adapter automatically via `_getDb()`, which also falls back to the registered backend.
270
+ Saico provides backend-agnostic DB methods. Configure via `Saico.registerBackend('dynamodb', config)` (library-level), `opt.dynamodb` (instance-level auto-creates adapter), or `opt.db` (any adapter). Key, key value, and table default to `'id'`, `this.id`, and `this._storeName` so operating on own record is a one-liner. Child Saico instances inherit the parent's adapter via `_getDb()`, which also falls back to the registered backend.
270
271
 
271
272
  ```js
272
- // CRUD — table name required on every call
273
+ // CRUD — shorthand (defaults: key='id', value=this.id, table=this._storeName)
274
+ const me = await agent.dbGetItem(); // get own record
275
+ await agent.dbDeleteItem(); // delete own record
276
+ // Explicit
273
277
  await agent.dbPutItem({ id: '123', name: 'test' }, 'my-table');
274
278
  const item = await agent.dbGetItem('id', '123', 'my-table');
275
279
  await agent.dbDeleteItem('id', '123', 'my-table');
276
280
  const items = await agent.dbQuery('email-index', 'email', 'user@test.com', 'my-table');
277
281
  const all = await agent.dbGetAll('my-table');
278
282
 
279
- // Updates
280
- await agent.dbUpdate('id', '123', 'status', 'active', 'my-table');
281
- await agent.dbUpdatePath('id', '123', [{ key: 'nested' }], 'field', 'value', 'my-table');
282
- await agent.dbListAppend('id', '123', 'tags', 'new-tag', 'my-table');
283
+ // Updates — setKey, item first; key, keyValue, table last (with defaults)
284
+ await agent.dbUpdate('status', 'active'); // update own record
285
+ await agent.dbUpdate('status', 'active', 'id', '123', 'my-table');
286
+ await agent.dbUpdatePath([{ key: 'nested' }], 'field', 'value');
287
+ await agent.dbListAppend('tags', 'new-tag');
283
288
 
284
289
  // Counters
285
290
  const nextId = await agent.dbNextCounterId('OrderId', 'counters');
@@ -312,8 +317,9 @@ const json = await agent.serialize();
312
317
  const restored = await Saico.deserialize(json);
313
318
 
314
319
  // Durable persistence (uses registered backend + opt.store table name)
315
- await agent.closeSession();
316
- const restored2 = await Saico.rehydrate(agent.id, { store: 'sessions' });
320
+ await agent.store(); // save independently
321
+ await agent.closeSession(); // store + cancel task
322
+ const restored2 = await Saico.restore(agent.id, { store: 'sessions' });
317
323
  ```
318
324
 
319
325
  `prepareForStorage()` automatically picks up all non-underscore properties (id, name, prompt, userData, sessionConfig, tm_create, isolate, etc.) and produces compressed chat_history for the msgs Q.
@@ -405,7 +411,7 @@ saico/
405
411
  npm test
406
412
  ```
407
413
 
408
- 300 tests covering Saico lifecycle, msgs Q ownership, spawn/spawnAndRun, task hierarchy, message handling, tool calls, DB adapters, async serialization, prepareForStorage, backend registration, persistence (closeSession/rehydrate via registered backend), storage integration, and full hierarchy flows.
414
+ 300 tests covering Saico lifecycle, msgs Q ownership, spawn/spawnAndRun, task hierarchy, message handling, tool calls, DB adapters, async serialization, prepareForStorage, backend registration, persistence (store/closeSession/restore via registered backend), storage integration, and full hierarchy flows.
409
415
 
410
416
  ## Requirements
411
417
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saico",
3
- "version": "2.9.0",
3
+ "version": "2.9.2",
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
@@ -38,7 +38,8 @@ class Saico {
38
38
  * @param {boolean} [opt.redis=true] - Set false to skip Redis proxy
39
39
  * @param {boolean} [opt.createQ] - Create message Q on activate()
40
40
  * @param {boolean} [opt.isolate] - Isolate: don't aggregate from ancestors
41
- * @param {Object} [opt.dynamodb] - DynamoDB config { region, credentials: { accessKeyId, secretAccessKey }, client }
41
+ * @param {Object} [opt.dynamodb] - DynamoDB config { region, credentials: { accessKeyId, secretAccessKey },
42
+ * client }
42
43
  * @param {Object} [opt.db] - Pluggable DB backend
43
44
  * @param {string} [opt.store] - Table name for instance persistence
44
45
  * @param {Object} [opt.userData] - Initial user data
@@ -46,7 +47,8 @@ class Saico {
46
47
  */
47
48
  constructor(opt = {}) {
48
49
  // Internal properties (underscore-prefixed, not persisted to Redis)
49
- this.id = opt.id || crypto.randomBytes(8).toString('hex');
50
+ this.name = opt.name || this.constructor.name || 'saico';
51
+ this.id = opt.id || this._genId();
50
52
  this._task = null;
51
53
  this._storeName = (typeof opt.store === 'string') ? opt.store : null;
52
54
  this._opt = opt;
@@ -57,7 +59,6 @@ class Saico {
57
59
  this.msgs_id = null;
58
60
 
59
61
  // Public configuration
60
- this.name = opt.name || this.constructor.name || 'saico';
61
62
  this.prompt = opt.prompt || '';
62
63
  this.functions = opt.functions || null;
63
64
  this.createQ = opt.createQ || false;
@@ -501,19 +502,30 @@ class Saico {
501
502
  }
502
503
 
503
504
  /**
504
- * Close the session save state to registered backend, cancel task.
505
+ * Generate an id. Called by constructor when opt.id is not provided.
506
+ * Overridable by subclasses for custom ID schemes.
505
507
  */
506
- async closeSession() {
507
- if (!this._task) return;
508
+ _genId() {
509
+ return crypto.randomBytes(8).toString('hex');
510
+ }
508
511
 
509
- if (this._storeName && this.msgs) {
510
- const backend = Saico.getBackend();
511
- if (backend) {
512
- const data = await this.prepareForStorage();
513
- await backend.put(data, this._storeName);
514
- }
515
- }
512
+ /**
513
+ * Save instance state to registered backend under _storeName.
514
+ */
515
+ async store() {
516
+ if (!this._storeName) return;
517
+ const backend = Saico.getBackend();
518
+ if (!backend) return;
519
+ const data = await this.prepareForStorage();
520
+ await backend.put(data, this._storeName);
521
+ }
516
522
 
523
+ /**
524
+ * Close the session — cancel task. Call store() first if persistence needed.
525
+ */
526
+ async closeSession() {
527
+ if (!this._task) return;
528
+ await this.store();
517
529
  this._task._ecancel();
518
530
  }
519
531
 
@@ -534,23 +546,23 @@ class Saico {
534
546
  throw new Error('No DB backend configured. Call Saico.registerBackend() or set opt.db.');
535
547
  }
536
548
 
537
- async dbPutItem(item, table) {
549
+ async dbPutItem(item, table = this._storeName) {
538
550
  const db = this._getDb();
539
551
  return db.put(item, table);
540
552
  }
541
553
 
542
- async dbGetItem(key, value, table) {
554
+ async dbGetItem(key = 'id', value = this.id, table = this._storeName) {
543
555
  const db = this._getDb();
544
556
  const result = await db.get(key, value, table);
545
557
  return result ? this._deserializeRecord(result) : result;
546
558
  }
547
559
 
548
- async dbDeleteItem(key, value, table) {
560
+ async dbDeleteItem(key = 'id', value = this.id, table = this._storeName) {
549
561
  const db = this._getDb();
550
562
  return db.delete(key, value, table);
551
563
  }
552
564
 
553
- async dbQuery(index, key, value, table) {
565
+ async dbQuery(index, key, value, table = this._storeName) {
554
566
  const db = this._getDb();
555
567
  const results = await db.query(index, key, value, table);
556
568
  return Array.isArray(results)
@@ -558,7 +570,7 @@ class Saico {
558
570
  : results;
559
571
  }
560
572
 
561
- async dbGetAll(table) {
573
+ async dbGetAll(table = this._storeName) {
562
574
  const db = this._getDb();
563
575
  const results = await db.getAll(table);
564
576
  return Array.isArray(results)
@@ -566,42 +578,42 @@ class Saico {
566
578
  : results;
567
579
  }
568
580
 
569
- async dbUpdate(key, keyValue, setKey, item, table) {
581
+ async dbUpdate(setKey, item, key = 'id', keyValue = this.id, table = this._storeName) {
570
582
  const db = this._getDb();
571
583
  return db.update(key, keyValue, setKey, item, table);
572
584
  }
573
585
 
574
- async dbUpdatePath(key, keyValue, path, setKey, item, table) {
586
+ async dbUpdatePath(path, setKey, item, key = 'id', keyValue = this.id, table = this._storeName) {
575
587
  const db = this._getDb();
576
588
  return db.updatePath(key, keyValue, path, setKey, item, table);
577
589
  }
578
590
 
579
- async dbListAppend(key, keyValue, setKey, item, table) {
591
+ async dbListAppend(setKey, item, key = 'id', keyValue = this.id, table = this._storeName) {
580
592
  const db = this._getDb();
581
593
  return db.listAppend(key, keyValue, setKey, item, table);
582
594
  }
583
595
 
584
- async dbListAppendPath(key, keyValue, path, setKey, item, table) {
596
+ async dbListAppendPath(path, setKey, item, key = 'id', keyValue = this.id, table = this._storeName) {
585
597
  const db = this._getDb();
586
598
  return db.listAppendPath(key, keyValue, path, setKey, item, table);
587
599
  }
588
600
 
589
- async dbNextCounterId(counter, table) {
601
+ async dbNextCounterId(counter, table = this._storeName) {
590
602
  const db = this._getDb();
591
603
  return db.nextCounterId(counter, table);
592
604
  }
593
605
 
594
- async dbGetCounterValue(counter, table) {
606
+ async dbGetCounterValue(counter, table = this._storeName) {
595
607
  const db = this._getDb();
596
608
  return db.getCounterValue(counter, table);
597
609
  }
598
610
 
599
- async dbSetCounterValue(counter, value, table) {
611
+ async dbSetCounterValue(counter, value, table = this._storeName) {
600
612
  const db = this._getDb();
601
613
  return db.setCounterValue(counter, value, table);
602
614
  }
603
615
 
604
- async dbCountItems(table) {
616
+ async dbCountItems(table = this._storeName) {
605
617
  const db = this._getDb();
606
618
  return db.countItems(table);
607
619
  }
@@ -718,7 +730,7 @@ class Saico {
718
730
  * @param {Object} opt - Options (store: table name, backend, functions, states, etc.)
719
731
  * @returns {Promise<Saico|null>}
720
732
  */
721
- static async rehydrate(id, opt = {}) {
733
+ static async restore(id, opt = {}) {
722
734
  const backend = opt.backend || Saico.getBackend();
723
735
  if (!backend)
724
736
  throw new Error('No backend registered. Call Saico.registerBackend() first.');