saico 2.3.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 +287 -297
- package/index.js +2 -9
- package/itask.js +16 -4
- package/msgs.js +106 -99
- package/package.json +1 -2
- package/saico.js +305 -41
- package/sid.js +0 -248
package/README.md
CHANGED
|
@@ -1,398 +1,388 @@
|
|
|
1
|
-
# Saico -
|
|
1
|
+
# Saico - Hierarchical AI 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 conversation context, system prompt, tools, and state — and the library automatically assembles the full payload sent to the LLM by walking the tree.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Features
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- **Hierarchical conversations** — Parent-child task trees with automatic prompt, tool, and state summary aggregation
|
|
8
|
+
- **Token-aware summarization** — Automatic summarization when message history approaches token limits
|
|
9
|
+
- **Tool calling** — Depth control, deferred execution, duplicate detection, repetition prevention, and timeout handling
|
|
10
|
+
- **Pluggable storage** — Optional Redis persistence (auto-save via proxy) and pluggable DB backends (DynamoDB adapter included)
|
|
11
|
+
- **Isolation boundaries** — `opt.isolate` stops ancestor aggregation at any node in the tree
|
|
12
|
+
- **Serialization** — Full state save/restore for long-running agents
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
- 🧵 **Scoped Memory** — Manage sub-conversations independently while maintaining parent relevance.
|
|
11
|
-
- 🔁 **Token-Aware Summarization** — Automatically summarize message history based on token thresholds.
|
|
12
|
-
- 💬 **Message-Level Metadata** — Track reply state, summaries, and custom flags.
|
|
13
|
-
- 🛠️ **OpenAI-Compatible Format** — Built for seamless interaction with OpenAI-compatible APIs.
|
|
14
|
-
- 🧰 **Proxy-Based Interface** — Interact with message history like an array, with extra powers.
|
|
15
|
-
- **🚀 NEW: Tool Calls** — Complete tool calling system with depth control, deferred execution, and safety features.
|
|
14
|
+
## Installation
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
```bash
|
|
17
|
+
npm install saico
|
|
18
|
+
```
|
|
18
19
|
|
|
19
|
-
##
|
|
20
|
+
## Quick Start
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
```js
|
|
23
|
+
const { Saico } = require('saico');
|
|
24
|
+
|
|
25
|
+
class MyAgent extends Saico {
|
|
26
|
+
constructor() {
|
|
27
|
+
super({
|
|
28
|
+
name: 'my-agent',
|
|
29
|
+
prompt: 'You are a helpful assistant.',
|
|
30
|
+
functions: [{
|
|
31
|
+
type: 'function',
|
|
32
|
+
function: {
|
|
33
|
+
name: 'get_weather',
|
|
34
|
+
description: 'Get weather for a location',
|
|
35
|
+
parameters: {
|
|
36
|
+
type: 'object',
|
|
37
|
+
properties: { location: { type: 'string' } },
|
|
38
|
+
required: ['location']
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}]
|
|
42
|
+
});
|
|
43
|
+
}
|
|
22
44
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
- **🔁 Repetition Prevention** — Block excessive repeated tool calls (default: 20 max)
|
|
29
|
-
- **📥 Message Queuing** — Messages automatically queue when tool calls are pending
|
|
30
|
-
- **👨👩👧👦 Parent-Child Inheritance** — Unresponded tool calls move from parent to child contexts
|
|
45
|
+
// Tool implementations — define TOOL_ prefix methods
|
|
46
|
+
async TOOL_get_weather(args) {
|
|
47
|
+
return `Weather in ${args.location}: 72F, sunny`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
31
50
|
|
|
32
|
-
|
|
51
|
+
const agent = new MyAgent();
|
|
52
|
+
agent.activate({ createQ: true });
|
|
33
53
|
|
|
34
|
-
|
|
54
|
+
// Backend message (prefixed with [BACKEND] automatically)
|
|
55
|
+
const reply = await agent.sendMessage('What is the weather in Tokyo?');
|
|
35
56
|
|
|
36
|
-
|
|
37
|
-
|
|
57
|
+
// User-facing chat message (routed to deepest active context)
|
|
58
|
+
const chatReply = await agent.recvChatMessage('Hello!');
|
|
38
59
|
```
|
|
39
60
|
|
|
40
|
-
|
|
61
|
+
## Core Concepts
|
|
41
62
|
|
|
42
|
-
|
|
43
|
-
git clone https://github.com/wanderli-ai/saico
|
|
44
|
-
cd saico
|
|
45
|
-
```
|
|
63
|
+
### Saico Lifecycle
|
|
46
64
|
|
|
47
|
-
|
|
65
|
+
Saico separates construction from activation:
|
|
48
66
|
|
|
49
|
-
|
|
67
|
+
```js
|
|
68
|
+
// 1. Construct — sets up config, Redis proxy, DB access. No task yet.
|
|
69
|
+
const agent = new Saico({
|
|
70
|
+
name: 'agent',
|
|
71
|
+
prompt: 'System prompt here',
|
|
72
|
+
dynamodb_table: 'my_data', // optional DB
|
|
73
|
+
});
|
|
50
74
|
|
|
51
|
-
|
|
75
|
+
// DB methods work before activation
|
|
76
|
+
const item = await agent.dbGetItem('id', '123');
|
|
52
77
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// Define your tool handler
|
|
57
|
-
async function toolHandler(toolName, argumentsString) {
|
|
58
|
-
const args = JSON.parse(argumentsString);
|
|
59
|
-
|
|
60
|
-
switch (toolName) {
|
|
61
|
-
case 'get_weather':
|
|
62
|
-
return `Weather in ${args.location}: 72°F, sunny`;
|
|
63
|
-
case 'book_hotel':
|
|
64
|
-
return `Booked ${args.hotel} for ${args.nights} nights`;
|
|
65
|
-
default:
|
|
66
|
-
return 'Tool not found';
|
|
67
|
-
}
|
|
68
|
-
}
|
|
78
|
+
// 2. Activate — creates internal task + optional message queue context
|
|
79
|
+
agent.activate({ createQ: true });
|
|
69
80
|
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
null, // initial messages
|
|
77
|
-
toolHandler, // tool handler function
|
|
78
|
-
{ max_depth: 5, max_tool_repetition: 20 } // config
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
// Send a message that might trigger tool calls
|
|
82
|
-
await q.sendMessage('user', 'What\'s the weather in New York?');
|
|
81
|
+
// 3. Use — send messages, spawn children
|
|
82
|
+
await agent.sendMessage('Do something');
|
|
83
|
+
await agent.recvChatMessage('User says hello');
|
|
84
|
+
|
|
85
|
+
// 4. Deactivate — bubbles cleaned messages to parent, closes context
|
|
86
|
+
await agent.deactivate();
|
|
83
87
|
```
|
|
84
88
|
|
|
85
|
-
###
|
|
89
|
+
### Message Orchestration
|
|
86
90
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
91
|
+
When `sendMessage()` or `recvChatMessage()` is called, Saico walks the parent chain to build the full LLM payload:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
Root Saico (prompt: "You are a manager")
|
|
95
|
+
+-- Child Saico (prompt: "Handle bookings")
|
|
96
|
+
+-- Grandchild Saico (prompt: "Process payment")
|
|
97
|
+
sendMessage("Charge $50")
|
|
98
|
+
|
|
|
99
|
+
v
|
|
100
|
+
Preamble built automatically:
|
|
101
|
+
[Root prompt] [Root state summary] [Root tool digest]
|
|
102
|
+
[Child prompt] [Child state summary + recent msgs] [Child tool digest]
|
|
103
|
+
[Grandchild prompt] [Grandchild state summary]
|
|
104
|
+
... then the actual message queue messages ...
|
|
105
|
+
|
|
106
|
+
Functions aggregated from all levels.
|
|
99
107
|
```
|
|
100
108
|
|
|
101
|
-
|
|
109
|
+
- **`sendMessage(content, functions, opts)`** — Sends a backend message (auto-prefixed `[BACKEND]`). Uses the current or nearest ancestor context.
|
|
110
|
+
- **`recvChatMessage(content, opts)`** — Routes a user chat message DOWN to the deepest descendant with a message queue.
|
|
111
|
+
|
|
112
|
+
### Isolation
|
|
113
|
+
|
|
114
|
+
Set `isolate: true` to prevent ancestor aggregation:
|
|
102
115
|
|
|
103
116
|
```js
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
8000,
|
|
109
|
-
null,
|
|
110
|
-
toolHandler,
|
|
111
|
-
{
|
|
112
|
-
max_depth: 8, // Allow deeper tool call chains
|
|
113
|
-
max_tool_repetition: 10 // Be more strict about repetitions
|
|
114
|
-
}
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
// Send message with custom tool options
|
|
118
|
-
await q.sendMessage('user', 'Plan my trip', null, {
|
|
119
|
-
handler: customToolHandler, // Override default tool handler
|
|
120
|
-
timeout: 10000, // 10 second timeout for this message's tools
|
|
121
|
-
nofunc: false // Ensure tool calls are enabled
|
|
117
|
+
const isolated = new Saico({
|
|
118
|
+
name: 'isolated-agent',
|
|
119
|
+
prompt: 'Independent context',
|
|
120
|
+
isolate: true // won't include parent prompts/tools/summaries
|
|
122
121
|
});
|
|
123
122
|
```
|
|
124
123
|
|
|
125
|
-
###
|
|
124
|
+
### State Summaries
|
|
126
125
|
|
|
127
|
-
|
|
128
|
-
[Main] (toolHandler: generalTools)
|
|
129
|
-
├── [hotels] (inherits generalTools) ➜ tool calls + summary returned to [Main]
|
|
130
|
-
└── [flights] (inherits generalTools) ➜ tool calls + summary returned to [Main]
|
|
131
|
-
```
|
|
126
|
+
Override `getStateSummary()` in your subclass to provide dynamic state context:
|
|
132
127
|
|
|
133
|
-
|
|
128
|
+
```js
|
|
129
|
+
class OrderAgent extends Saico {
|
|
130
|
+
getStateSummary() {
|
|
131
|
+
return `Active order: #${this.orderId}, items: ${this.items.length}`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
134
135
|
|
|
135
|
-
|
|
136
|
+
When a Saico's context is not the deepest active one, its last 5 user/assistant messages are also included in the state summary automatically.
|
|
136
137
|
|
|
137
|
-
|
|
138
|
+
### Spawning Child Tasks
|
|
138
139
|
|
|
139
140
|
```js
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
141
|
+
// Child with its own conversation context
|
|
142
|
+
const child = agent.spawnTaskWithContext({
|
|
143
|
+
name: 'subtask',
|
|
144
|
+
prompt: 'Handle this specific sub-task',
|
|
145
|
+
functions: [/* child-specific tools */]
|
|
146
|
+
}, [
|
|
147
|
+
async function main() {
|
|
148
|
+
return await this.sendMessage('Working on subtask...');
|
|
149
|
+
}
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
// Child without context (uses parent's)
|
|
153
|
+
const simple = agent.spawnTask({ name: 'simple' }, [
|
|
154
|
+
async function main() {
|
|
155
|
+
await this.sendMessage('Quick operation');
|
|
156
|
+
}
|
|
157
|
+
]);
|
|
158
158
|
```
|
|
159
159
|
|
|
160
|
-
|
|
160
|
+
Child tasks inherit `sessionConfig` defaults (token_limit, max_depth, etc.) from the parent Saico.
|
|
161
161
|
|
|
162
|
-
|
|
163
|
-
* `q.length` — Total messages
|
|
164
|
-
* `q.pushSummary(summary)` — Manually inject a summary
|
|
165
|
-
* `q.getMsgContext()` — Get summarized parent chain
|
|
166
|
-
* `q.serialize()` — Export current state
|
|
167
|
-
* **NEW**: `q._hasPendingToolCalls()` — Check for pending tool executions
|
|
168
|
-
* **NEW**: `q._processWaitingQueue()` — Manually process queued messages
|
|
162
|
+
### Deactivation and Message Bubbling
|
|
169
163
|
|
|
170
|
-
|
|
164
|
+
When a Saico deactivates, cleaned messages (no tool calls, no `[BACKEND]` messages) are pushed into the parent's message queue, preserving conversation continuity.
|
|
171
165
|
|
|
172
|
-
##
|
|
166
|
+
## Constructor Options
|
|
173
167
|
|
|
174
|
-
### Depth Control & Deferred Execution
|
|
175
168
|
```js
|
|
176
|
-
|
|
177
|
-
|
|
169
|
+
new Saico({
|
|
170
|
+
// Identity
|
|
171
|
+
id: 'custom-id', // Auto-generated if omitted
|
|
172
|
+
name: 'my-agent', // Defaults to class name
|
|
173
|
+
|
|
174
|
+
// AI config
|
|
175
|
+
prompt: 'System prompt',
|
|
176
|
+
functions: [], // OpenAI function definitions
|
|
177
|
+
|
|
178
|
+
// Behavior
|
|
179
|
+
isolate: false, // Stop ancestor aggregation
|
|
180
|
+
|
|
181
|
+
// Session config (defaults for this agent and its children)
|
|
182
|
+
token_limit: 4000,
|
|
183
|
+
max_depth: 5, // Max tool call recursion depth
|
|
184
|
+
max_tool_repetition: 20, // Max consecutive repeated tool calls
|
|
185
|
+
queue_limit: 100, // Message queue limit
|
|
186
|
+
min_chat_messages: 5, // Min messages to keep in queue
|
|
187
|
+
sessionConfig: {}, // Override any of the above
|
|
188
|
+
|
|
189
|
+
// Storage
|
|
190
|
+
redis: true, // Set false to skip Redis proxy
|
|
191
|
+
key: 'custom-redis-key',
|
|
192
|
+
dynamodb_table: 'table', // Auto-creates DynamoDB adapter
|
|
193
|
+
dynamodb_region: 'us-east-1',
|
|
194
|
+
db: customAdapter, // Any adapter with put/get/delete/query interface
|
|
195
|
+
|
|
196
|
+
// User data
|
|
197
|
+
userData: {}, // Arbitrary user metadata
|
|
178
198
|
});
|
|
179
|
-
|
|
180
|
-
// When max depth reached:
|
|
181
|
-
// 1. Tool calls are deferred (not executed immediately)
|
|
182
|
-
// 2. Conversation continues normally
|
|
183
|
-
// 3. Deferred tools execute when depth reduces
|
|
184
|
-
// 4. Results are seamlessly integrated back
|
|
185
199
|
```
|
|
186
200
|
|
|
187
|
-
|
|
201
|
+
## Activate Options
|
|
202
|
+
|
|
188
203
|
```js
|
|
189
|
-
|
|
190
|
-
|
|
204
|
+
agent.activate({
|
|
205
|
+
createQ: true, // Create message queue context
|
|
206
|
+
prompt: 'Extra prompt', // Appended to class-level prompt
|
|
207
|
+
states: [], // Task state functions
|
|
208
|
+
parent: parentTask, // Parent task to spawn under
|
|
209
|
+
taskId: 'custom-id',
|
|
210
|
+
sequential_mode: true, // Process messages sequentially
|
|
211
|
+
|
|
212
|
+
// Override session config for this activation
|
|
213
|
+
token_limit: 8000,
|
|
214
|
+
max_depth: 10,
|
|
215
|
+
queue_limit: 200,
|
|
191
216
|
});
|
|
192
|
-
|
|
193
|
-
// Automatically filters excessive repeated tool calls
|
|
194
|
-
// Logs: "Dropping excessive tool call: get_weather (hit max_tool_repetition=5)"
|
|
195
217
|
```
|
|
196
218
|
|
|
197
|
-
|
|
198
|
-
```js
|
|
199
|
-
// If two identical tool calls (same name + arguments) are active:
|
|
200
|
-
// Second call returns: "Duplicate call detected. Please wait for previous call to complete."
|
|
201
|
-
```
|
|
219
|
+
## User Data
|
|
202
220
|
|
|
203
|
-
### Timeout Handling
|
|
204
221
|
```js
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
//
|
|
209
|
-
await q.sendMessage('user', 'Run slow analysis', null, { timeout: 30000 });
|
|
222
|
+
agent.setUserData('preference', 'dark-mode'); // returns this (chainable)
|
|
223
|
+
agent.getUserData('preference'); // 'dark-mode'
|
|
224
|
+
agent.getUserData(); // { preference: 'dark-mode' }
|
|
225
|
+
agent.clearUserData(); // returns this
|
|
210
226
|
```
|
|
211
227
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
## 🧪 Summary Behavior
|
|
215
|
-
|
|
216
|
-
Summaries trigger when total token count exceeds 85% of the limit and are always triggered when `close()` is called.
|
|
217
|
-
Summaries are:
|
|
218
|
-
|
|
219
|
-
* Injected as special `[SUMMARY]: ...` messages
|
|
220
|
-
* Bubbled up into the parent context
|
|
221
|
-
* Excluded from re-summarization unless explicitly kept
|
|
222
|
-
* **NEW**: Include tool call results in summarization context
|
|
228
|
+
## Session Info
|
|
223
229
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
* 🗃️ **Support for serializing `Messages` class**
|
|
235
|
-
* 🔍 **Efficient diff-checking** (saves only when changed)
|
|
236
|
-
* **NEW**: **Tool call state persistence** (active calls, deferred calls, waiting queues)
|
|
230
|
+
```js
|
|
231
|
+
agent.getSessionInfo();
|
|
232
|
+
// {
|
|
233
|
+
// id, name, running, completed,
|
|
234
|
+
// messageCount, childCount,
|
|
235
|
+
// userData, uptime
|
|
236
|
+
// }
|
|
237
|
+
|
|
238
|
+
await agent.closeSession(); // Close context and cancel task
|
|
239
|
+
```
|
|
237
240
|
|
|
238
|
-
|
|
241
|
+
## Database Access
|
|
239
242
|
|
|
240
|
-
|
|
243
|
+
Saico provides backend-agnostic DB methods. Configure via `dynamodb_table` (auto-creates DynamoDB adapter) or `db` (any adapter implementing the interface).
|
|
241
244
|
|
|
242
|
-
```
|
|
243
|
-
|
|
245
|
+
```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();
|
|
252
|
+
|
|
253
|
+
// 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');
|
|
257
|
+
|
|
258
|
+
// 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();
|
|
244
263
|
```
|
|
245
264
|
|
|
246
|
-
|
|
265
|
+
Override `_deserializeRecord(raw)` to transform raw DB records on retrieval (e.g., restore class instances):
|
|
247
266
|
|
|
248
267
|
```js
|
|
249
|
-
|
|
250
|
-
|
|
268
|
+
class MyAgent extends Saico {
|
|
269
|
+
_deserializeRecord(raw) {
|
|
270
|
+
if (raw.type === 'order') return new Order(raw);
|
|
271
|
+
return raw;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
251
274
|
```
|
|
252
275
|
|
|
253
|
-
|
|
276
|
+
## Serialization
|
|
254
277
|
|
|
255
278
|
```js
|
|
256
|
-
|
|
257
|
-
const
|
|
279
|
+
// Save
|
|
280
|
+
const json = agent.serialize();
|
|
258
281
|
|
|
259
|
-
//
|
|
260
|
-
const
|
|
282
|
+
// Restore
|
|
283
|
+
const restored = Saico.deserialize(json, {
|
|
284
|
+
functions: myFunctions,
|
|
285
|
+
});
|
|
261
286
|
```
|
|
262
287
|
|
|
263
|
-
|
|
288
|
+
Serialization includes: id, name, prompt, userData, sessionConfig, tm_create, isolate, and full context state (messages, tool_digest, chat_history).
|
|
264
289
|
|
|
265
|
-
|
|
290
|
+
## Redis Persistence
|
|
266
291
|
|
|
267
|
-
|
|
292
|
+
When Redis is initialized, Saico instances are automatically wrapped in an observable proxy. Any property change triggers a debounced save to Redis.
|
|
268
293
|
|
|
269
|
-
|
|
294
|
+
```js
|
|
295
|
+
const { init } = require('saico');
|
|
270
296
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
* Object updates are **debounced (1s)** and only saved if actual changes are detected.
|
|
274
|
-
* **NEW**: Tool call tracking data is sanitized automatically
|
|
297
|
+
// Initialize with Redis
|
|
298
|
+
await init({ redis: true });
|
|
275
299
|
|
|
276
|
-
|
|
300
|
+
const agent = new Saico({ name: 'persistent-agent' });
|
|
301
|
+
agent.someProperty = 'value'; // Auto-saved to Redis
|
|
302
|
+
```
|
|
277
303
|
|
|
278
|
-
|
|
304
|
+
Properties prefixed with `_` are internal and not persisted.
|
|
279
305
|
|
|
280
|
-
|
|
306
|
+
## Tool Implementation (TOOL_ methods)
|
|
281
307
|
|
|
282
|
-
|
|
283
|
-
* Backward compatibility with legacy `functions` format
|
|
284
|
-
* Automatic format conversion in openai.js
|
|
285
|
-
* Built-in retry logic with exponential backoff for rate limits
|
|
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.
|
|
286
309
|
|
|
287
310
|
```js
|
|
288
|
-
|
|
289
|
-
{
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
tool_calls: [{
|
|
293
|
-
id: 'call_abc123',
|
|
294
|
-
type: 'function',
|
|
295
|
-
function: {
|
|
296
|
-
name: 'get_weather',
|
|
297
|
-
arguments: '{"location": "New York"}'
|
|
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`;
|
|
298
315
|
}
|
|
299
|
-
}]
|
|
300
|
-
}
|
|
301
316
|
|
|
302
|
-
|
|
317
|
+
async TOOL_search(args) {
|
|
318
|
+
const results = await search(args.query);
|
|
319
|
+
return { content: JSON.stringify(results), functions: updatedTools };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
303
322
|
```
|
|
304
323
|
|
|
305
|
-
|
|
324
|
+
Return a string or `{ content: string, functions?: [] }`.
|
|
306
325
|
|
|
307
|
-
|
|
326
|
+
### Tool Safety Features
|
|
308
327
|
|
|
309
|
-
|
|
328
|
+
- **Depth control** — `max_depth` (default: 5) prevents infinite tool call recursion
|
|
329
|
+
- **Deferred execution** — Tool calls defer when max depth is reached, resume when depth reduces
|
|
330
|
+
- **Duplicate detection** — Identical active tool calls are blocked
|
|
331
|
+
- **Repetition prevention** — `max_tool_repetition` (default: 20) blocks excessive repeated calls
|
|
332
|
+
- **Timeout handling** — Configurable timeout (default: 5s) with graceful failure
|
|
333
|
+
- **Message queuing** — Messages queue automatically when tool calls are pending
|
|
310
334
|
|
|
311
|
-
|
|
312
|
-
* **NEW**: Tool calls functionality (12 tests):
|
|
313
|
-
- Basic tool execution
|
|
314
|
-
- Depth limits and deferred execution
|
|
315
|
-
- Repetition prevention and filtering
|
|
316
|
-
- Duplicate detection
|
|
317
|
-
- Message queuing systems
|
|
318
|
-
- Timeout handling
|
|
319
|
-
- Parent-child tool inheritance
|
|
335
|
+
## Low-Level API
|
|
320
336
|
|
|
321
|
-
|
|
322
|
-
npm test # Run full test suite
|
|
323
|
-
```
|
|
337
|
+
For cases where you don't need the Saico master class:
|
|
324
338
|
|
|
325
|
-
|
|
339
|
+
```js
|
|
340
|
+
const { createTask, createContext, createQ } = require('saico');
|
|
326
341
|
|
|
327
|
-
|
|
342
|
+
// Create a task with context
|
|
343
|
+
const task = createTask({
|
|
344
|
+
name: 'my-task',
|
|
345
|
+
prompt: 'You are helpful',
|
|
346
|
+
functions: tools
|
|
347
|
+
});
|
|
348
|
+
const reply = await task.sendMessage('Hello');
|
|
328
349
|
|
|
350
|
+
// Standalone context (legacy)
|
|
351
|
+
const ctx = createQ('System prompt', null, 'tag', 4000);
|
|
352
|
+
const reply = await ctx.sendMessage('user', 'Hello', functions);
|
|
329
353
|
```
|
|
330
|
-
.
|
|
331
|
-
├── saico.js # Core implementation with tool calls
|
|
332
|
-
├── openai.js # OpenAI API wrapper with tools support
|
|
333
|
-
├── redis.js # Saico compatible redis wrapper
|
|
334
|
-
├── util.js # Utilities: token counting, etc.
|
|
335
|
-
├── test.js # Comprehensive test suite
|
|
336
|
-
├── msgs.js # Original enhanced version (reference)
|
|
337
|
-
└── README.md # This file
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
---
|
|
341
|
-
|
|
342
|
-
## 🚀 Migration Guide
|
|
343
354
|
|
|
344
|
-
|
|
355
|
+
## Project Structure
|
|
345
356
|
|
|
346
|
-
### Old API:
|
|
347
|
-
```js
|
|
348
|
-
const q = createQ(prompt, opts, msgs, parent);
|
|
349
357
|
```
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
358
|
+
saico/
|
|
359
|
+
+-- index.js # Entry point, exports all components
|
|
360
|
+
+-- saico.js # Saico master class
|
|
361
|
+
+-- itask.js # Base task class (hierarchy, states, cancellation)
|
|
362
|
+
+-- msgs.js # Conversation context (message queue, tool calls, summarization)
|
|
363
|
+
+-- context.js # Backward-compat shim for msgs.js
|
|
364
|
+
+-- dynamo.js # DynamoDB storage adapter
|
|
365
|
+
+-- store.js # Storage abstraction (Redis + pluggable backends)
|
|
366
|
+
+-- openai.js # OpenAI API wrapper with retry logic
|
|
367
|
+
+-- redis.js # Redis persistence with observable proxy
|
|
368
|
+
+-- util.js # Utilities (token counting, logging)
|
|
354
369
|
```
|
|
355
370
|
|
|
356
|
-
|
|
357
|
-
- Constructor parameter order changed
|
|
358
|
-
- `opts.tag` → `tag` parameter
|
|
359
|
-
- `opts.token_limit` → `token_limit` parameter
|
|
360
|
-
- Added `tool_handler` and `config` parameters
|
|
361
|
-
- `function_call` → `tool_calls` in OpenAI responses
|
|
362
|
-
|
|
363
|
-
---
|
|
371
|
+
## Testing
|
|
364
372
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
---
|
|
370
|
-
|
|
371
|
-
## 🙌 Contributing
|
|
372
|
-
|
|
373
|
-
Pull requests, issues, and suggestions welcome! Please fork the repo and open a PR, or submit issues directly.
|
|
374
|
-
|
|
375
|
-
Areas where contributions are especially welcome:
|
|
376
|
-
- Additional tool call safety features
|
|
377
|
-
- Performance optimizations for large conversations
|
|
378
|
-
- Extended test coverage
|
|
379
|
-
- Documentation improvements
|
|
380
|
-
|
|
381
|
-
---
|
|
382
|
-
|
|
383
|
-
## 📣 Acknowledgements
|
|
373
|
+
```bash
|
|
374
|
+
npm test
|
|
375
|
+
```
|
|
384
376
|
|
|
385
|
-
|
|
377
|
+
293 tests covering Saico lifecycle, task hierarchy, message handling, tool calls, DB adapters, serialization, and integration flows.
|
|
386
378
|
|
|
387
|
-
|
|
379
|
+
## Requirements
|
|
388
380
|
|
|
389
|
-
|
|
381
|
+
- Node.js >= 16.0.0
|
|
382
|
+
- `OPENAI_API_KEY` environment variable for LLM calls
|
|
383
|
+
- Redis (optional, for auto-persistence)
|
|
384
|
+
- AWS SDK v3 (optional peer dependency, for DynamoDB)
|
|
390
385
|
|
|
391
|
-
|
|
392
|
-
- [ ] **Advanced tool call analytics** and monitoring
|
|
393
|
-
- [ ] **Custom summarization strategies**
|
|
394
|
-
- [ ] **Tool call result caching**
|
|
395
|
-
- [ ] **Streaming tool call responses**
|
|
396
|
-
- [ ] **Tool call permission systems**
|
|
386
|
+
## License
|
|
397
387
|
|
|
398
|
-
|
|
388
|
+
ISC
|