saico 2.4.0 → 2.5.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
 
@@ -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
@@ -286,7 +281,6 @@ const json = agent.serialize();
286
281
 
287
282
  // Restore
288
283
  const restored = Saico.deserialize(json, {
289
- tool_handler: myHandler,
290
284
  functions: myFunctions,
291
285
  });
292
286
  ```
@@ -309,16 +303,26 @@ agent.someProperty = 'value'; // Auto-saved to Redis
309
303
 
310
304
  Properties prefixed with `_` are internal and not persisted.
311
305
 
312
- ## Tool Handler Interface
306
+ ## Tool Implementation (TOOL_ methods)
307
+
308
+ 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
309
 
314
310
  ```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?: [] }
311
+ class MyAgent extends Saico {
312
+ async TOOL_get_weather(args) {
313
+ // args is already JSON.parse'd
314
+ return `Weather in ${args.location}: 72F, sunny`;
315
+ }
316
+
317
+ async TOOL_search(args) {
318
+ const results = await search(args.query);
319
+ return { content: JSON.stringify(results), functions: updatedTools };
320
+ }
319
321
  }
320
322
  ```
321
323
 
324
+ Return a string or `{ content: string, functions?: [] }`.
325
+
322
326
  ### Tool Safety Features
323
327
 
324
328
  - **Depth control** — `max_depth` (default: 5) prevents infinite tool call recursion
@@ -339,13 +343,12 @@ const { createTask, createContext, createQ } = require('saico');
339
343
  const task = createTask({
340
344
  name: 'my-task',
341
345
  prompt: 'You are helpful',
342
- tool_handler: handler,
343
346
  functions: tools
344
347
  });
345
348
  const reply = await task.sendMessage('Hello');
346
349
 
347
350
  // Standalone context (legacy)
348
- const ctx = createQ('System prompt', null, 'tag', 4000, null, handler);
351
+ const ctx = createQ('System prompt', null, 'tag', 4000);
349
352
  const reply = await ctx.sendMessage('user', 'Hello', functions);
350
353
  ```
351
354
 
@@ -371,7 +374,7 @@ saico/
371
374
  npm test
372
375
  ```
373
376
 
374
- 294 tests covering Saico lifecycle, task hierarchy, message handling, tool calls, DB adapters, serialization, and integration flows.
377
+ 293 tests covering Saico lifecycle, task hierarchy, message handling, tool calls, DB adapters, serialization, and integration flows.
375
378
 
376
379
  ## Requirements
377
380
 
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.5.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,7 +29,6 @@ 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
@@ -53,7 +52,6 @@ class Saico {
53
52
  // Public configuration
54
53
  this.name = opt.name || this.constructor.name || 'saico';
55
54
  this.prompt = opt.prompt || '';
56
- this.tool_handler = opt.tool_handler || null;
57
55
  this.functions = opt.functions || null;
58
56
 
59
57
  // Absorbed from Sid
@@ -96,7 +94,6 @@ class Saico {
96
94
  * @param {Object} opts
97
95
  * @param {boolean} [opts.createQ] - If true, attach a message Q (Context)
98
96
  * @param {string} [opts.prompt] - Additional prompt (appended to class-level)
99
- * @param {Function} [opts.tool_handler] - Override tool handler
100
97
  * @param {Array} [opts.functions] - Override functions
101
98
  * @param {Array} [opts.states] - Task state functions
102
99
  * @param {Itask} [opts.parent] - Parent task to spawn under
@@ -126,7 +123,6 @@ class Saico {
126
123
  id: opts.taskId,
127
124
  async: true,
128
125
  store: this._store,
129
- tool_handler: opts.tool_handler || this.tool_handler,
130
126
  functions: opts.functions || this.functions,
131
127
  bind: this, // State functions run with Saico instance as `this`
132
128
  };
@@ -148,7 +144,6 @@ class Saico {
148
144
  max_tool_repetition: opts.max_tool_repetition ?? this.sessionConfig.max_tool_repetition,
149
145
  queue_limit: opts.queue_limit ?? this.sessionConfig.queue_limit,
150
146
  min_chat_messages: opts.min_chat_messages ?? this.sessionConfig.min_chat_messages,
151
- tool_handler: taskOpt.tool_handler,
152
147
  functions: taskOpt.functions,
153
148
  sequential_mode: opts.sequential_mode,
154
149
  msgs: opts.msgs,
@@ -334,7 +329,6 @@ class Saico {
334
329
  max_tool_repetition: opt.max_tool_repetition ?? this.sessionConfig.max_tool_repetition,
335
330
  queue_limit: opt.queue_limit ?? this.sessionConfig.queue_limit,
336
331
  min_chat_messages: opt.min_chat_messages ?? this.sessionConfig.min_chat_messages,
337
- tool_handler: opt.tool_handler || this.tool_handler,
338
332
  functions: opt.functions || this.functions,
339
333
  });
340
334
  childTask.setContext(childContext);
@@ -565,7 +559,7 @@ class Saico {
565
559
  /**
566
560
  * Restore a Saico instance from serialized data.
567
561
  * @param {string|Object} data - Serialized data (JSON string or object)
568
- * @param {Object} opt - Options (tool_handler, functions, store, states, etc.)
562
+ * @param {Object} opt - Options (functions, store, states, etc.)
569
563
  * @returns {Saico}
570
564
  */
571
565
  static deserialize(data, opt = {}) {
@@ -578,7 +572,6 @@ class Saico {
578
572
  userData: parsed.userData,
579
573
  sessionConfig: parsed.sessionConfig,
580
574
  isolate: parsed.isolate,
581
- tool_handler: opt.tool_handler,
582
575
  functions: opt.functions || parsed.task?.context?.functions,
583
576
  store: opt.store,
584
577
  redis: false, // No Redis proxy during deserialization
@@ -593,7 +586,6 @@ class Saico {
593
586
  taskId: parsed.task.id,
594
587
  tag: parsed.task.context?.tag,
595
588
  chat_history: parsed.task.context?.chat_history,
596
- tool_handler: opt.tool_handler,
597
589
  functions: opt.functions || parsed.task.context?.functions,
598
590
  states: opt.states || [],
599
591
  ...opt,