saico 2.5.0 → 2.6.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.
Files changed (4) hide show
  1. package/README.md +19 -17
  2. package/dynamo.js +9 -9
  3. package/package.json +1 -1
  4. package/saico.js +45 -33
package/README.md CHANGED
@@ -69,7 +69,7 @@ Saico separates construction from activation:
69
69
  const agent = new Saico({
70
70
  name: 'agent',
71
71
  prompt: 'System prompt here',
72
- dynamodb_table: 'my_data', // optional DB
72
+ dynamodb: { region: 'us-east-1', credentials: { accessKeyId: 'AK', secretAccessKey: 'SK' } },
73
73
  });
74
74
 
75
75
  // DB methods work before activation
@@ -189,8 +189,10 @@ new Saico({
189
189
  // Storage
190
190
  redis: true, // Set false to skip Redis proxy
191
191
  key: 'custom-redis-key',
192
- dynamodb_table: 'table', // Auto-creates DynamoDB adapter
193
- dynamodb_region: 'us-east-1',
192
+ dynamodb: { // DynamoDB config (creates adapter)
193
+ region: 'us-east-1',
194
+ credentials: { accessKeyId: '...', secretAccessKey: '...' },
195
+ },
194
196
  db: customAdapter, // Any adapter with put/get/delete/query interface
195
197
 
196
198
  // User data
@@ -240,26 +242,26 @@ await agent.closeSession(); // Close context and cancel task
240
242
 
241
243
  ## Database Access
242
244
 
243
- Saico provides backend-agnostic DB methods. Configure via `dynamodb_table` (auto-creates DynamoDB adapter) or `db` (any adapter implementing the interface).
245
+ Saico provides backend-agnostic DB methods. Configure via `opt.dynamodb` (auto-creates DynamoDB 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()`.
244
246
 
245
247
  ```js
246
- // CRUD
247
- await agent.dbPutItem({ id: '123', name: 'test' });
248
- const item = await agent.dbGetItem('id', '123');
249
- await agent.dbDeleteItem('id', '123');
250
- const items = await agent.dbQuery('email-index', 'email', 'user@test.com');
251
- const all = await agent.dbGetAll();
248
+ // CRUD — table name required on every call
249
+ await agent.dbPutItem({ id: '123', name: 'test' }, 'my-table');
250
+ const item = await agent.dbGetItem('id', '123', 'my-table');
251
+ await agent.dbDeleteItem('id', '123', 'my-table');
252
+ const items = await agent.dbQuery('email-index', 'email', 'user@test.com', 'my-table');
253
+ const all = await agent.dbGetAll('my-table');
252
254
 
253
255
  // Updates
254
- await agent.dbUpdate('id', '123', 'status', 'active');
255
- await agent.dbUpdatePath('id', '123', [{ key: 'nested' }], 'field', 'value');
256
- await agent.dbListAppend('id', '123', 'tags', 'new-tag');
256
+ await agent.dbUpdate('id', '123', 'status', 'active', 'my-table');
257
+ await agent.dbUpdatePath('id', '123', [{ key: 'nested' }], 'field', 'value', 'my-table');
258
+ await agent.dbListAppend('id', '123', 'tags', 'new-tag', 'my-table');
257
259
 
258
260
  // Counters
259
- const nextId = await agent.dbNextCounterId('OrderId');
260
- const count = await agent.dbGetCounterValue('OrderId');
261
- await agent.dbSetCounterValue('OrderId', 100);
262
- const total = await agent.dbCountItems();
261
+ const nextId = await agent.dbNextCounterId('OrderId', 'counters');
262
+ const count = await agent.dbGetCounterValue('OrderId', 'counters');
263
+ await agent.dbSetCounterValue('OrderId', 100, 'counters');
264
+ const total = await agent.dbCountItems('my-table');
263
265
  ```
264
266
 
265
267
  Override `_deserializeRecord(raw)` to transform raw DB records on retrieval (e.g., restore class instances):
package/dynamo.js CHANGED
@@ -3,9 +3,8 @@
3
3
  /**
4
4
  * DynamoDBAdapter — Generic DynamoDB access layer.
5
5
  *
6
- * Generalized from ../backend/aws.js. Provides CRUD, update, list-append,
7
- * counter, and scan operations. All methods accept an optional `table`
8
- * parameter that defaults to `this.defaultTable`.
6
+ * Provides CRUD, update, list-append, counter, and scan operations.
7
+ * Table name is required on every call.
9
8
  *
10
9
  * AWS SDK v3 packages are required only when this module is loaded.
11
10
  */
@@ -18,14 +17,16 @@ const { unmarshall, marshall } = require('@aws-sdk/util-dynamodb');
18
17
  class DynamoDBAdapter {
19
18
  /**
20
19
  * @param {Object} opt
21
- * @param {string} opt.table - Default table name for all operations
22
20
  * @param {string} [opt.region='us-east-1'] - AWS region
21
+ * @param {Object} [opt.credentials] - AWS credentials { accessKeyId, secretAccessKey }
23
22
  * @param {DynamoDBClient} [opt.client] - Injectable DynamoDB client (for testing)
24
23
  */
25
24
  constructor(opt = {}) {
26
- this.defaultTable = opt.table || null;
27
25
  this._region = opt.region || 'us-east-1';
28
- this._client = opt.client || new DynamoDBClient({ region: this._region });
26
+ this._client = opt.client || new DynamoDBClient({
27
+ region: this._region,
28
+ ...(opt.credentials && { credentials: opt.credentials }),
29
+ });
29
30
  this.__docClient = null;
30
31
  }
31
32
 
@@ -36,9 +37,8 @@ class DynamoDBAdapter {
36
37
  }
37
38
 
38
39
  _table(table) {
39
- const t = table || this.defaultTable;
40
- if (!t) throw new Error('DynamoDBAdapter: no table specified');
41
- return t;
40
+ if (!table) throw new Error('DynamoDBAdapter: table name required');
41
+ return table;
42
42
  }
43
43
 
44
44
  _unmarshall(item) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saico",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
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
@@ -33,9 +33,7 @@ class Saico {
33
33
  * @param {string} [opt.key] - Redis key override (default: 'saico:<id>')
34
34
  * @param {boolean} [opt.redis=true] - Set false to skip Redis proxy
35
35
  * @param {boolean} [opt.isolate] - Isolate: don't aggregate from ancestors
36
- * @param {string} [opt.dynamodb_table] - DynamoDB table name (enables db accessor)
37
- * @param {string} [opt.dynamodb_region] - AWS region for DynamoDB
38
- * @param {Object} [opt.dynamodb_client] - Injectable DynamoDB client (for testing)
36
+ * @param {Object} [opt.dynamodb] - DynamoDB config { region, credentials: { accessKeyId, secretAccessKey }, client }
39
37
  * @param {Object} [opt.db] - Pluggable DB backend
40
38
  * @param {Object} [opt.store] - Store instance override
41
39
  * @param {Object} [opt.userData] - Initial user data
@@ -68,12 +66,12 @@ class Saico {
68
66
 
69
67
  // DB backend — pluggable storage adapter.
70
68
  this._db = opt.db || null;
71
- if (!this._db && opt.dynamodb_table) {
69
+ if (!this._db && opt.dynamodb) {
72
70
  const { DynamoDBAdapter } = require('./dynamo.js');
73
71
  this._db = new DynamoDBAdapter({
74
- table: opt.dynamodb_table,
75
- region: opt.dynamodb_region,
76
- client: opt.dynamodb_client,
72
+ region: opt.dynamodb.region,
73
+ credentials: opt.dynamodb.credentials,
74
+ client: opt.dynamodb.client,
77
75
  });
78
76
  }
79
77
 
@@ -446,76 +444,90 @@ class Saico {
446
444
 
447
445
  // ---- Generic DB access ----
448
446
 
447
+ /**
448
+ * Find a DB backend — own _db first, then walk UP the parent Saico chain.
449
+ * Throws if no backend found anywhere.
450
+ */
451
+ _getDb() {
452
+ if (this._db) return this._db;
453
+ let task = this._task?.parent;
454
+ while (task) {
455
+ if (task._saico?._db) return task._saico._db;
456
+ task = task.parent;
457
+ }
458
+ throw new Error('No DB backend configured. Set opt.dynamodb or opt.db on this Saico or an ancestor.');
459
+ }
460
+
449
461
  async dbPutItem(item, table) {
450
- if (!this._db) return;
451
- return this._db.put(item, table);
462
+ const db = this._getDb();
463
+ return db.put(item, table);
452
464
  }
453
465
 
454
466
  async dbGetItem(key, value, table) {
455
- if (!this._db) return;
456
- const result = await this._db.get(key, value, table);
467
+ const db = this._getDb();
468
+ const result = await db.get(key, value, table);
457
469
  return result ? this._deserializeRecord(result) : result;
458
470
  }
459
471
 
460
472
  async dbDeleteItem(key, value, table) {
461
- if (!this._db) return;
462
- return this._db.delete(key, value, table);
473
+ const db = this._getDb();
474
+ return db.delete(key, value, table);
463
475
  }
464
476
 
465
477
  async dbQuery(index, key, value, table) {
466
- if (!this._db) return;
467
- const results = await this._db.query(index, key, value, table);
478
+ const db = this._getDb();
479
+ const results = await db.query(index, key, value, table);
468
480
  return Array.isArray(results)
469
481
  ? results.map(r => this._deserializeRecord(r))
470
482
  : results;
471
483
  }
472
484
 
473
485
  async dbGetAll(table) {
474
- if (!this._db) return;
475
- const results = await this._db.getAll(table);
486
+ const db = this._getDb();
487
+ const results = await db.getAll(table);
476
488
  return Array.isArray(results)
477
489
  ? results.map(r => this._deserializeRecord(r))
478
490
  : results;
479
491
  }
480
492
 
481
493
  async dbUpdate(key, keyValue, setKey, item, table) {
482
- if (!this._db) return;
483
- return this._db.update(key, keyValue, setKey, item, table);
494
+ const db = this._getDb();
495
+ return db.update(key, keyValue, setKey, item, table);
484
496
  }
485
497
 
486
498
  async dbUpdatePath(key, keyValue, path, setKey, item, table) {
487
- if (!this._db) return;
488
- return this._db.updatePath(key, keyValue, path, setKey, item, table);
499
+ const db = this._getDb();
500
+ return db.updatePath(key, keyValue, path, setKey, item, table);
489
501
  }
490
502
 
491
503
  async dbListAppend(key, keyValue, setKey, item, table) {
492
- if (!this._db) return;
493
- return this._db.listAppend(key, keyValue, setKey, item, table);
504
+ const db = this._getDb();
505
+ return db.listAppend(key, keyValue, setKey, item, table);
494
506
  }
495
507
 
496
508
  async dbListAppendPath(key, keyValue, path, setKey, item, table) {
497
- if (!this._db) return;
498
- return this._db.listAppendPath(key, keyValue, path, setKey, item, table);
509
+ const db = this._getDb();
510
+ return db.listAppendPath(key, keyValue, path, setKey, item, table);
499
511
  }
500
512
 
501
513
  async dbNextCounterId(counter, table) {
502
- if (!this._db) return;
503
- return this._db.nextCounterId(counter, table);
514
+ const db = this._getDb();
515
+ return db.nextCounterId(counter, table);
504
516
  }
505
517
 
506
518
  async dbGetCounterValue(counter, table) {
507
- if (!this._db) return;
508
- return this._db.getCounterValue(counter, table);
519
+ const db = this._getDb();
520
+ return db.getCounterValue(counter, table);
509
521
  }
510
522
 
511
523
  async dbSetCounterValue(counter, value, table) {
512
- if (!this._db) return;
513
- return this._db.setCounterValue(counter, value, table);
524
+ const db = this._getDb();
525
+ return db.setCounterValue(counter, value, table);
514
526
  }
515
527
 
516
528
  async dbCountItems(table) {
517
- if (!this._db) return;
518
- return this._db.countItems(table);
529
+ const db = this._getDb();
530
+ return db.countItems(table);
519
531
  }
520
532
 
521
533
  // ---- DB deserialization hook ----