saico 2.4.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.
package/README.md CHANGED
@@ -27,7 +27,6 @@ class MyAgent extends Saico {
27
27
  super({
28
28
  name: 'my-agent',
29
29
  prompt: 'You are a helpful assistant.',
30
- tool_handler: (name, args) => this.handleTool(name, args),
31
30
  functions: [{
32
31
  type: 'function',
33
32
  function: {
@@ -43,11 +42,9 @@ class MyAgent extends Saico {
43
42
  });
44
43
  }
45
44
 
46
- async handleTool(name, argsString) {
47
- const args = JSON.parse(argsString);
48
- if (name === 'get_weather')
49
- return `Weather in ${args.location}: 72F, sunny`;
50
- return 'Unknown tool';
45
+ // Tool implementations — define TOOL_ prefix methods
46
+ async TOOL_get_weather(args) {
47
+ return `Weather in ${args.location}: 72F, sunny`;
51
48
  }
52
49
  }
53
50
 
@@ -72,7 +69,7 @@ Saico separates construction from activation:
72
69
  const agent = new Saico({
73
70
  name: 'agent',
74
71
  prompt: 'System prompt here',
75
- dynamodb_table: 'my_data', // optional DB
72
+ dynamodb: { region: 'us-east-1', credentials: { accessKeyId: 'AK', secretAccessKey: 'SK' } },
76
73
  });
77
74
 
78
75
  // DB methods work before activation
@@ -145,7 +142,6 @@ When a Saico's context is not the deepest active one, its last 5 user/assistant
145
142
  const child = agent.spawnTaskWithContext({
146
143
  name: 'subtask',
147
144
  prompt: 'Handle this specific sub-task',
148
- tool_handler: (name, args) => handleSubTools(name, args),
149
145
  functions: [/* child-specific tools */]
150
146
  }, [
151
147
  async function main() {
@@ -177,7 +173,6 @@ new Saico({
177
173
 
178
174
  // AI config
179
175
  prompt: 'System prompt',
180
- tool_handler: fn, // async (name, argsString) => result
181
176
  functions: [], // OpenAI function definitions
182
177
 
183
178
  // Behavior
@@ -194,8 +189,10 @@ new Saico({
194
189
  // Storage
195
190
  redis: true, // Set false to skip Redis proxy
196
191
  key: 'custom-redis-key',
197
- dynamodb_table: 'table', // Auto-creates DynamoDB adapter
198
- dynamodb_region: 'us-east-1',
192
+ dynamodb: { // DynamoDB config (creates adapter)
193
+ region: 'us-east-1',
194
+ credentials: { accessKeyId: '...', secretAccessKey: '...' },
195
+ },
199
196
  db: customAdapter, // Any adapter with put/get/delete/query interface
200
197
 
201
198
  // User data
@@ -245,26 +242,26 @@ await agent.closeSession(); // Close context and cancel task
245
242
 
246
243
  ## Database Access
247
244
 
248
- 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()`.
249
246
 
250
247
  ```js
251
- // CRUD
252
- await agent.dbPutItem({ id: '123', name: 'test' });
253
- const item = await agent.dbGetItem('id', '123');
254
- await agent.dbDeleteItem('id', '123');
255
- const items = await agent.dbQuery('email-index', 'email', 'user@test.com');
256
- 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');
257
254
 
258
255
  // Updates
259
- await agent.dbUpdate('id', '123', 'status', 'active');
260
- await agent.dbUpdatePath('id', '123', [{ key: 'nested' }], 'field', 'value');
261
- 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');
262
259
 
263
260
  // Counters
264
- const nextId = await agent.dbNextCounterId('OrderId');
265
- const count = await agent.dbGetCounterValue('OrderId');
266
- await agent.dbSetCounterValue('OrderId', 100);
267
- 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');
268
265
  ```
269
266
 
270
267
  Override `_deserializeRecord(raw)` to transform raw DB records on retrieval (e.g., restore class instances):
@@ -286,7 +283,6 @@ const json = agent.serialize();
286
283
 
287
284
  // Restore
288
285
  const restored = Saico.deserialize(json, {
289
- tool_handler: myHandler,
290
286
  functions: myFunctions,
291
287
  });
292
288
  ```
@@ -309,16 +305,26 @@ agent.someProperty = 'value'; // Auto-saved to Redis
309
305
 
310
306
  Properties prefixed with `_` are internal and not persisted.
311
307
 
312
- ## Tool Handler Interface
308
+ ## Tool Implementation (TOOL_ methods)
309
+
310
+ 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.
313
311
 
314
312
  ```js
315
- async function toolHandler(toolName, argumentsString) {
316
- const args = JSON.parse(argumentsString);
317
- // Execute tool logic
318
- return result; // string or { content: string, functions?: [] }
313
+ class MyAgent extends Saico {
314
+ async TOOL_get_weather(args) {
315
+ // args is already JSON.parse'd
316
+ return `Weather in ${args.location}: 72F, sunny`;
317
+ }
318
+
319
+ async TOOL_search(args) {
320
+ const results = await search(args.query);
321
+ return { content: JSON.stringify(results), functions: updatedTools };
322
+ }
319
323
  }
320
324
  ```
321
325
 
326
+ Return a string or `{ content: string, functions?: [] }`.
327
+
322
328
  ### Tool Safety Features
323
329
 
324
330
  - **Depth control** — `max_depth` (default: 5) prevents infinite tool call recursion
@@ -339,13 +345,12 @@ const { createTask, createContext, createQ } = require('saico');
339
345
  const task = createTask({
340
346
  name: 'my-task',
341
347
  prompt: 'You are helpful',
342
- tool_handler: handler,
343
348
  functions: tools
344
349
  });
345
350
  const reply = await task.sendMessage('Hello');
346
351
 
347
352
  // Standalone context (legacy)
348
- const ctx = createQ('System prompt', null, 'tag', 4000, null, handler);
353
+ const ctx = createQ('System prompt', null, 'tag', 4000);
349
354
  const reply = await ctx.sendMessage('user', 'Hello', functions);
350
355
  ```
351
356
 
@@ -371,7 +376,7 @@ saico/
371
376
  npm test
372
377
  ```
373
378
 
374
- 294 tests covering Saico lifecycle, task hierarchy, message handling, tool calls, DB adapters, serialization, and integration flows.
379
+ 293 tests covering Saico lifecycle, task hierarchy, message handling, tool calls, DB adapters, serialization, and integration flows.
375
380
 
376
381
  ## Requirements
377
382
 
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/index.js CHANGED
@@ -54,7 +54,6 @@ async function init(config = {}) {
54
54
  * @param {Object|string} opt - Task options or name string
55
55
  * @param {string} opt.name - Task name
56
56
  * @param {string} opt.prompt - System prompt (if provided, creates a context)
57
- * @param {Function} opt.tool_handler - Tool handler function
58
57
  * @param {Array} opt.functions - Available functions for AI
59
58
  * @param {boolean} opt.cancel - Whether task is cancelable
60
59
  * @param {Object} opt.bind - Bind context for state functions
@@ -80,7 +79,6 @@ function createTask(opt, states = []) {
80
79
  token_limit: opt.token_limit,
81
80
  max_depth: opt.max_depth,
82
81
  max_tool_repetition: opt.max_tool_repetition,
83
- tool_handler: opt.tool_handler,
84
82
  functions: opt.functions,
85
83
  sequential_mode: opt.sequential_mode
86
84
  });
@@ -99,11 +97,10 @@ function createTask(opt, states = []) {
99
97
  * @param {string} tag - Context tag identifier
100
98
  * @param {number} token_limit - Token limit for summarization
101
99
  * @param {Array} msgs - Initial messages
102
- * @param {Function} tool_handler - Tool handler function
103
100
  * @param {Object} config - Additional configuration
104
101
  * @returns {Context} Proxied Context instance
105
102
  */
106
- function createQ(prompt, parent, tag, token_limit, msgs, tool_handler, config = {}) {
103
+ function createQ(prompt, parent, tag, token_limit, msgs, config = {}) {
107
104
  // For backward compatibility, if parent is a Context, get its task
108
105
  let task = null;
109
106
  if (parent && parent.task) {
@@ -114,7 +111,6 @@ function createQ(prompt, parent, tag, token_limit, msgs, tool_handler, config =
114
111
  tag,
115
112
  token_limit,
116
113
  msgs,
117
- tool_handler,
118
114
  ...config
119
115
  });
120
116
 
package/itask.js CHANGED
@@ -110,7 +110,6 @@ function Itask(opt, states){
110
110
  // Store options for context creation (prompt, functions, etc.)
111
111
  this.prompt = opt.prompt;
112
112
  this.functions = opt.functions;
113
- this.tool_handler = opt.tool_handler;
114
113
 
115
114
  // register root if no explicit spawn_parent provided
116
115
  // If opt.spawn_parent provided, spawn under it
package/msgs.js CHANGED
@@ -24,7 +24,6 @@ class Context {
24
24
  this.token_limit = config.token_limit || 1000000000;
25
25
  this.lower_limit = this.token_limit * 0.85;
26
26
  this.upper_limit = this.token_limit * 0.98;
27
- this.tool_handler = config.tool_handler || task?.tool_handler;
28
27
  this.functions = config.functions || task?.functions || null;
29
28
 
30
29
  // Recursive depth and repetition control
@@ -63,8 +62,6 @@ class Context {
63
62
  // Set the task reference (used when context is created separately)
64
63
  setTask(task) {
65
64
  this.task = task;
66
- if (!this.tool_handler)
67
- this.tool_handler = task?.tool_handler;
68
65
  if (!this.functions)
69
66
  this.functions = task?.functions;
70
67
  }
@@ -287,10 +284,9 @@ class Context {
287
284
 
288
285
  try {
289
286
  const correspondingDeferred = deferredGroup.find(d => d.call.id === call.id);
290
- const handler = correspondingDeferred?.originalMessage.opts.handler || this.tool_handler;
291
287
  const timeout = correspondingDeferred?.originalMessage.opts.timeout;
292
288
 
293
- result = await this._executeToolCallWithTimeout(call, handler, timeout);
289
+ result = await this._executeToolCallWithTimeout(call, timeout);
294
290
  if (_snap !== null &&
295
291
  _snap !== JSON.stringify(this._snapshotPublicProps(this.task)))
296
292
  this._appendToolDigest(call.function.name, result?.content || '');
@@ -669,7 +665,7 @@ class Context {
669
665
  }
670
666
  }
671
667
 
672
- async _executeToolCallWithTimeout(call, handler, customTimeoutMs = null) {
668
+ async _executeToolCallWithTimeout(call, customTimeoutMs = null) {
673
669
  const timeoutMs = customTimeoutMs || 5000;
674
670
 
675
671
  return new Promise(async (resolve) => {
@@ -688,7 +684,7 @@ class Context {
688
684
  }, timeoutMs);
689
685
 
690
686
  try {
691
- const result = await this.interpretAndApplyChanges(call, handler);
687
+ const result = await this.interpretAndApplyChanges(call);
692
688
 
693
689
  if (!completed) {
694
690
  completed = true;
@@ -1005,7 +1001,7 @@ class Context {
1005
1001
  ? JSON.stringify(this._snapshotPublicProps(this.task)) : null;
1006
1002
  try {
1007
1003
  const result = await this._executeToolCallWithTimeout(
1008
- call, o.opts?.handler, o.opts?.timeout);
1004
+ call, o.opts?.timeout);
1009
1005
  const item = toolCallsWithResults.find(item => item.call.id === call.id);
1010
1006
  if (item) item.result = result;
1011
1007
  if (_snap !== null &&
@@ -1064,27 +1060,84 @@ class Context {
1064
1060
  }
1065
1061
  }
1066
1062
 
1067
- async interpretAndApplyChanges(call, handler) {
1068
- _log('apply tool', call.function.name, 'have handler', !!handler, !!this.tool_handler);
1063
+ /**
1064
+ * Search the Saico hierarchy for a TOOL_<toolName> method.
1065
+ * Order: current task → walk UP parents → walk DOWN children (BFS).
1066
+ */
1067
+ _findToolImplementation(toolName) {
1068
+ const methodName = 'TOOL_' + toolName;
1069
+ const check = (task) =>
1070
+ task?._saico && typeof task._saico[methodName] === 'function' ? task._saico : null;
1071
+
1072
+ // 1. Current task
1073
+ let found = check(this.task);
1074
+ if (found) return { saico: found, methodName };
1075
+
1076
+ // 2. Walk UP parent chain
1077
+ let t = this.task?.parent;
1078
+ while (t) {
1079
+ found = check(t);
1080
+ if (found) return { saico: found, methodName };
1081
+ t = t.parent;
1082
+ }
1083
+
1084
+ // 3. Walk DOWN from this.task (BFS)
1085
+ if (this.task) {
1086
+ const queue = [...this.task.child];
1087
+ while (queue.length > 0) {
1088
+ const child = queue.shift();
1089
+ if (child._completed) continue;
1090
+ found = check(child);
1091
+ if (found) return { saico: found, methodName };
1092
+ if (child.child?.size > 0) queue.push(...child.child);
1093
+ }
1094
+ }
1095
+
1096
+ return null;
1097
+ }
1098
+
1099
+ async interpretAndApplyChanges(call) {
1069
1100
  if (!call)
1070
1101
  return { content: '', functions: null };
1071
1102
 
1072
- _log('invoking function', call.function.name);
1073
- handler ||= this.tool_handler;
1074
- let result = await handler(call.function.name, call.function.arguments);
1103
+ const toolName = call.function.name;
1104
+ _log('apply tool', toolName);
1105
+
1106
+ const impl = this._findToolImplementation(toolName);
1107
+ if (!impl) {
1108
+ _log('No TOOL_ method found for:', toolName);
1109
+ return {
1110
+ content: `Error: No implementation found for tool "${toolName}". ` +
1111
+ `Expected a TOOL_${toolName}(args) method on a Saico instance in the hierarchy.`,
1112
+ functions: null
1113
+ };
1114
+ }
1115
+
1116
+ _log('invoking TOOL_' + toolName, 'on', impl.saico.name || impl.saico.constructor.name);
1117
+
1118
+ let parsedArgs;
1119
+ try {
1120
+ parsedArgs = JSON.parse(call.function.arguments);
1121
+ } catch (e) {
1122
+ return {
1123
+ content: `Error: Failed to parse arguments for tool "${toolName}": ${e.message}`,
1124
+ functions: null
1125
+ };
1126
+ }
1127
+
1128
+ let result = await impl.saico[impl.methodName](parsedArgs);
1075
1129
 
1076
1130
  let content = result?.content || result || '';
1077
1131
  let functions = result?.functions || null;
1078
1132
 
1079
1133
  if (content && typeof content !== 'string')
1080
1134
  content = JSON.stringify(content);
1081
- else if (!content)
1082
- {
1083
- content = `tool call ${call.function.name} ${call.id} completed. do not reply. wait for the next msg `
1084
- +`from the user`;
1135
+ else if (!content) {
1136
+ content = `tool call ${toolName} ${call.id} completed. do not reply. wait for the next msg `
1137
+ + `from the user`;
1085
1138
  }
1086
1139
 
1087
- _log('FUNCTION RESULT', call.function.name, call.id, content.substring(0, 50) + '...',
1140
+ _log('FUNCTION RESULT', toolName, call.id, content.substring(0, 50) + '...',
1088
1141
  functions ? 'with functions' : 'no functions');
1089
1142
  return { content, functions };
1090
1143
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saico",
3
- "version": "2.4.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
@@ -29,14 +29,11 @@ class Saico {
29
29
  * @param {string} [opt.id] - Instance ID (auto-generated if omitted)
30
30
  * @param {string} [opt.name] - Instance name (defaults to class name)
31
31
  * @param {string} [opt.prompt] - Class-level system prompt
32
- * @param {Function} [opt.tool_handler] - Tool handler function
33
32
  * @param {Array} [opt.functions] - Available AI functions
34
33
  * @param {string} [opt.key] - Redis key override (default: 'saico:<id>')
35
34
  * @param {boolean} [opt.redis=true] - Set false to skip Redis proxy
36
35
  * @param {boolean} [opt.isolate] - Isolate: don't aggregate from ancestors
37
- * @param {string} [opt.dynamodb_table] - DynamoDB table name (enables db accessor)
38
- * @param {string} [opt.dynamodb_region] - AWS region for DynamoDB
39
- * @param {Object} [opt.dynamodb_client] - Injectable DynamoDB client (for testing)
36
+ * @param {Object} [opt.dynamodb] - DynamoDB config { region, credentials: { accessKeyId, secretAccessKey }, client }
40
37
  * @param {Object} [opt.db] - Pluggable DB backend
41
38
  * @param {Object} [opt.store] - Store instance override
42
39
  * @param {Object} [opt.userData] - Initial user data
@@ -53,7 +50,6 @@ class Saico {
53
50
  // Public configuration
54
51
  this.name = opt.name || this.constructor.name || 'saico';
55
52
  this.prompt = opt.prompt || '';
56
- this.tool_handler = opt.tool_handler || null;
57
53
  this.functions = opt.functions || null;
58
54
 
59
55
  // Absorbed from Sid
@@ -70,12 +66,12 @@ class Saico {
70
66
 
71
67
  // DB backend — pluggable storage adapter.
72
68
  this._db = opt.db || null;
73
- if (!this._db && opt.dynamodb_table) {
69
+ if (!this._db && opt.dynamodb) {
74
70
  const { DynamoDBAdapter } = require('./dynamo.js');
75
71
  this._db = new DynamoDBAdapter({
76
- table: opt.dynamodb_table,
77
- region: opt.dynamodb_region,
78
- client: opt.dynamodb_client,
72
+ region: opt.dynamodb.region,
73
+ credentials: opt.dynamodb.credentials,
74
+ client: opt.dynamodb.client,
79
75
  });
80
76
  }
81
77
 
@@ -96,7 +92,6 @@ class Saico {
96
92
  * @param {Object} opts
97
93
  * @param {boolean} [opts.createQ] - If true, attach a message Q (Context)
98
94
  * @param {string} [opts.prompt] - Additional prompt (appended to class-level)
99
- * @param {Function} [opts.tool_handler] - Override tool handler
100
95
  * @param {Array} [opts.functions] - Override functions
101
96
  * @param {Array} [opts.states] - Task state functions
102
97
  * @param {Itask} [opts.parent] - Parent task to spawn under
@@ -126,7 +121,6 @@ class Saico {
126
121
  id: opts.taskId,
127
122
  async: true,
128
123
  store: this._store,
129
- tool_handler: opts.tool_handler || this.tool_handler,
130
124
  functions: opts.functions || this.functions,
131
125
  bind: this, // State functions run with Saico instance as `this`
132
126
  };
@@ -148,7 +142,6 @@ class Saico {
148
142
  max_tool_repetition: opts.max_tool_repetition ?? this.sessionConfig.max_tool_repetition,
149
143
  queue_limit: opts.queue_limit ?? this.sessionConfig.queue_limit,
150
144
  min_chat_messages: opts.min_chat_messages ?? this.sessionConfig.min_chat_messages,
151
- tool_handler: taskOpt.tool_handler,
152
145
  functions: taskOpt.functions,
153
146
  sequential_mode: opts.sequential_mode,
154
147
  msgs: opts.msgs,
@@ -334,7 +327,6 @@ class Saico {
334
327
  max_tool_repetition: opt.max_tool_repetition ?? this.sessionConfig.max_tool_repetition,
335
328
  queue_limit: opt.queue_limit ?? this.sessionConfig.queue_limit,
336
329
  min_chat_messages: opt.min_chat_messages ?? this.sessionConfig.min_chat_messages,
337
- tool_handler: opt.tool_handler || this.tool_handler,
338
330
  functions: opt.functions || this.functions,
339
331
  });
340
332
  childTask.setContext(childContext);
@@ -452,76 +444,90 @@ class Saico {
452
444
 
453
445
  // ---- Generic DB access ----
454
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
+
455
461
  async dbPutItem(item, table) {
456
- if (!this._db) return;
457
- return this._db.put(item, table);
462
+ const db = this._getDb();
463
+ return db.put(item, table);
458
464
  }
459
465
 
460
466
  async dbGetItem(key, value, table) {
461
- if (!this._db) return;
462
- const result = await this._db.get(key, value, table);
467
+ const db = this._getDb();
468
+ const result = await db.get(key, value, table);
463
469
  return result ? this._deserializeRecord(result) : result;
464
470
  }
465
471
 
466
472
  async dbDeleteItem(key, value, table) {
467
- if (!this._db) return;
468
- return this._db.delete(key, value, table);
473
+ const db = this._getDb();
474
+ return db.delete(key, value, table);
469
475
  }
470
476
 
471
477
  async dbQuery(index, key, value, table) {
472
- if (!this._db) return;
473
- 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);
474
480
  return Array.isArray(results)
475
481
  ? results.map(r => this._deserializeRecord(r))
476
482
  : results;
477
483
  }
478
484
 
479
485
  async dbGetAll(table) {
480
- if (!this._db) return;
481
- const results = await this._db.getAll(table);
486
+ const db = this._getDb();
487
+ const results = await db.getAll(table);
482
488
  return Array.isArray(results)
483
489
  ? results.map(r => this._deserializeRecord(r))
484
490
  : results;
485
491
  }
486
492
 
487
493
  async dbUpdate(key, keyValue, setKey, item, table) {
488
- if (!this._db) return;
489
- return this._db.update(key, keyValue, setKey, item, table);
494
+ const db = this._getDb();
495
+ return db.update(key, keyValue, setKey, item, table);
490
496
  }
491
497
 
492
498
  async dbUpdatePath(key, keyValue, path, setKey, item, table) {
493
- if (!this._db) return;
494
- 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);
495
501
  }
496
502
 
497
503
  async dbListAppend(key, keyValue, setKey, item, table) {
498
- if (!this._db) return;
499
- return this._db.listAppend(key, keyValue, setKey, item, table);
504
+ const db = this._getDb();
505
+ return db.listAppend(key, keyValue, setKey, item, table);
500
506
  }
501
507
 
502
508
  async dbListAppendPath(key, keyValue, path, setKey, item, table) {
503
- if (!this._db) return;
504
- 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);
505
511
  }
506
512
 
507
513
  async dbNextCounterId(counter, table) {
508
- if (!this._db) return;
509
- return this._db.nextCounterId(counter, table);
514
+ const db = this._getDb();
515
+ return db.nextCounterId(counter, table);
510
516
  }
511
517
 
512
518
  async dbGetCounterValue(counter, table) {
513
- if (!this._db) return;
514
- return this._db.getCounterValue(counter, table);
519
+ const db = this._getDb();
520
+ return db.getCounterValue(counter, table);
515
521
  }
516
522
 
517
523
  async dbSetCounterValue(counter, value, table) {
518
- if (!this._db) return;
519
- return this._db.setCounterValue(counter, value, table);
524
+ const db = this._getDb();
525
+ return db.setCounterValue(counter, value, table);
520
526
  }
521
527
 
522
528
  async dbCountItems(table) {
523
- if (!this._db) return;
524
- return this._db.countItems(table);
529
+ const db = this._getDb();
530
+ return db.countItems(table);
525
531
  }
526
532
 
527
533
  // ---- DB deserialization hook ----
@@ -565,7 +571,7 @@ class Saico {
565
571
  /**
566
572
  * Restore a Saico instance from serialized data.
567
573
  * @param {string|Object} data - Serialized data (JSON string or object)
568
- * @param {Object} opt - Options (tool_handler, functions, store, states, etc.)
574
+ * @param {Object} opt - Options (functions, store, states, etc.)
569
575
  * @returns {Saico}
570
576
  */
571
577
  static deserialize(data, opt = {}) {
@@ -578,7 +584,6 @@ class Saico {
578
584
  userData: parsed.userData,
579
585
  sessionConfig: parsed.sessionConfig,
580
586
  isolate: parsed.isolate,
581
- tool_handler: opt.tool_handler,
582
587
  functions: opt.functions || parsed.task?.context?.functions,
583
588
  store: opt.store,
584
589
  redis: false, // No Redis proxy during deserialization
@@ -593,7 +598,6 @@ class Saico {
593
598
  taskId: parsed.task.id,
594
599
  tag: parsed.task.context?.tag,
595
600
  chat_history: parsed.task.context?.chat_history,
596
- tool_handler: opt.tool_handler,
597
601
  functions: opt.functions || parsed.task.context?.functions,
598
602
  states: opt.states || [],
599
603
  ...opt,