saico 2.3.0 → 2.4.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 -300
- package/index.js +1 -4
- package/itask.js +16 -3
- package/msgs.js +35 -81
- package/package.json +1 -2
- package/saico.js +307 -35
- package/sid.js +0 -248
package/README.md
CHANGED
|
@@ -1,398 +1,385 @@
|
|
|
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
|
+
tool_handler: (name, args) => this.handleTool(name, args),
|
|
31
|
+
functions: [{
|
|
32
|
+
type: 'function',
|
|
33
|
+
function: {
|
|
34
|
+
name: 'get_weather',
|
|
35
|
+
description: 'Get weather for a location',
|
|
36
|
+
parameters: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: { location: { type: 'string' } },
|
|
39
|
+
required: ['location']
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}]
|
|
43
|
+
});
|
|
44
|
+
}
|
|
22
45
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
- **👨👩👧👦 Parent-Child Inheritance** — Unresponded tool calls move from parent to child contexts
|
|
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';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
31
53
|
|
|
32
|
-
|
|
54
|
+
const agent = new MyAgent();
|
|
55
|
+
agent.activate({ createQ: true });
|
|
33
56
|
|
|
34
|
-
|
|
57
|
+
// Backend message (prefixed with [BACKEND] automatically)
|
|
58
|
+
const reply = await agent.sendMessage('What is the weather in Tokyo?');
|
|
35
59
|
|
|
36
|
-
|
|
37
|
-
|
|
60
|
+
// User-facing chat message (routed to deepest active context)
|
|
61
|
+
const chatReply = await agent.recvChatMessage('Hello!');
|
|
38
62
|
```
|
|
39
63
|
|
|
40
|
-
|
|
64
|
+
## Core Concepts
|
|
41
65
|
|
|
42
|
-
|
|
43
|
-
git clone https://github.com/wanderli-ai/saico
|
|
44
|
-
cd saico
|
|
45
|
-
```
|
|
66
|
+
### Saico Lifecycle
|
|
46
67
|
|
|
47
|
-
|
|
68
|
+
Saico separates construction from activation:
|
|
48
69
|
|
|
49
|
-
|
|
70
|
+
```js
|
|
71
|
+
// 1. Construct — sets up config, Redis proxy, DB access. No task yet.
|
|
72
|
+
const agent = new Saico({
|
|
73
|
+
name: 'agent',
|
|
74
|
+
prompt: 'System prompt here',
|
|
75
|
+
dynamodb_table: 'my_data', // optional DB
|
|
76
|
+
});
|
|
50
77
|
|
|
51
|
-
|
|
78
|
+
// DB methods work before activation
|
|
79
|
+
const item = await agent.dbGetItem('id', '123');
|
|
52
80
|
|
|
53
|
-
|
|
54
|
-
|
|
81
|
+
// 2. Activate — creates internal task + optional message queue context
|
|
82
|
+
agent.activate({ createQ: true });
|
|
55
83
|
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
}
|
|
84
|
+
// 3. Use — send messages, spawn children
|
|
85
|
+
await agent.sendMessage('Do something');
|
|
86
|
+
await agent.recvChatMessage('User says hello');
|
|
69
87
|
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
"You are a helpful assistant.", // prompt
|
|
73
|
-
null, // parent (null for root)
|
|
74
|
-
"main", // tag
|
|
75
|
-
4000, // token limit
|
|
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?');
|
|
88
|
+
// 4. Deactivate — bubbles cleaned messages to parent, closes context
|
|
89
|
+
await agent.deactivate();
|
|
83
90
|
```
|
|
84
91
|
|
|
85
|
-
###
|
|
92
|
+
### Message Orchestration
|
|
86
93
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
When `sendMessage()` or `recvChatMessage()` is called, Saico walks the parent chain to build the full LLM payload:
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
Root Saico (prompt: "You are a manager")
|
|
98
|
+
+-- Child Saico (prompt: "Handle bookings")
|
|
99
|
+
+-- Grandchild Saico (prompt: "Process payment")
|
|
100
|
+
sendMessage("Charge $50")
|
|
101
|
+
|
|
|
102
|
+
v
|
|
103
|
+
Preamble built automatically:
|
|
104
|
+
[Root prompt] [Root state summary] [Root tool digest]
|
|
105
|
+
[Child prompt] [Child state summary + recent msgs] [Child tool digest]
|
|
106
|
+
[Grandchild prompt] [Grandchild state summary]
|
|
107
|
+
... then the actual message queue messages ...
|
|
108
|
+
|
|
109
|
+
Functions aggregated from all levels.
|
|
99
110
|
```
|
|
100
111
|
|
|
101
|
-
|
|
112
|
+
- **`sendMessage(content, functions, opts)`** — Sends a backend message (auto-prefixed `[BACKEND]`). Uses the current or nearest ancestor context.
|
|
113
|
+
- **`recvChatMessage(content, opts)`** — Routes a user chat message DOWN to the deepest descendant with a message queue.
|
|
114
|
+
|
|
115
|
+
### Isolation
|
|
116
|
+
|
|
117
|
+
Set `isolate: true` to prevent ancestor aggregation:
|
|
102
118
|
|
|
103
119
|
```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
|
|
120
|
+
const isolated = new Saico({
|
|
121
|
+
name: 'isolated-agent',
|
|
122
|
+
prompt: 'Independent context',
|
|
123
|
+
isolate: true // won't include parent prompts/tools/summaries
|
|
122
124
|
});
|
|
123
125
|
```
|
|
124
126
|
|
|
125
|
-
###
|
|
127
|
+
### State Summaries
|
|
126
128
|
|
|
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
|
-
```
|
|
129
|
+
Override `getStateSummary()` in your subclass to provide dynamic state context:
|
|
132
130
|
|
|
133
|
-
|
|
131
|
+
```js
|
|
132
|
+
class OrderAgent extends Saico {
|
|
133
|
+
getStateSummary() {
|
|
134
|
+
return `Active order: #${this.orderId}, items: ${this.items.length}`;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
134
138
|
|
|
135
|
-
|
|
139
|
+
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
140
|
|
|
137
|
-
|
|
141
|
+
### Spawning Child Tasks
|
|
138
142
|
|
|
139
143
|
```js
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
name
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
144
|
+
// Child with its own conversation context
|
|
145
|
+
const child = agent.spawnTaskWithContext({
|
|
146
|
+
name: 'subtask',
|
|
147
|
+
prompt: 'Handle this specific sub-task',
|
|
148
|
+
tool_handler: (name, args) => handleSubTools(name, args),
|
|
149
|
+
functions: [/* child-specific tools */]
|
|
150
|
+
}, [
|
|
151
|
+
async function main() {
|
|
152
|
+
return await this.sendMessage('Working on subtask...');
|
|
153
|
+
}
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
// Child without context (uses parent's)
|
|
157
|
+
const simple = agent.spawnTask({ name: 'simple' }, [
|
|
158
|
+
async function main() {
|
|
159
|
+
await this.sendMessage('Quick operation');
|
|
160
|
+
}
|
|
161
|
+
]);
|
|
158
162
|
```
|
|
159
163
|
|
|
160
|
-
|
|
164
|
+
Child tasks inherit `sessionConfig` defaults (token_limit, max_depth, etc.) from the parent Saico.
|
|
161
165
|
|
|
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
|
|
166
|
+
### Deactivation and Message Bubbling
|
|
169
167
|
|
|
170
|
-
|
|
168
|
+
When a Saico deactivates, cleaned messages (no tool calls, no `[BACKEND]` messages) are pushed into the parent's message queue, preserving conversation continuity.
|
|
171
169
|
|
|
172
|
-
##
|
|
170
|
+
## Constructor Options
|
|
173
171
|
|
|
174
|
-
### Depth Control & Deferred Execution
|
|
175
172
|
```js
|
|
176
|
-
|
|
177
|
-
|
|
173
|
+
new Saico({
|
|
174
|
+
// Identity
|
|
175
|
+
id: 'custom-id', // Auto-generated if omitted
|
|
176
|
+
name: 'my-agent', // Defaults to class name
|
|
177
|
+
|
|
178
|
+
// AI config
|
|
179
|
+
prompt: 'System prompt',
|
|
180
|
+
tool_handler: fn, // async (name, argsString) => result
|
|
181
|
+
functions: [], // OpenAI function definitions
|
|
182
|
+
|
|
183
|
+
// Behavior
|
|
184
|
+
isolate: false, // Stop ancestor aggregation
|
|
185
|
+
|
|
186
|
+
// Session config (defaults for this agent and its children)
|
|
187
|
+
token_limit: 4000,
|
|
188
|
+
max_depth: 5, // Max tool call recursion depth
|
|
189
|
+
max_tool_repetition: 20, // Max consecutive repeated tool calls
|
|
190
|
+
queue_limit: 100, // Message queue limit
|
|
191
|
+
min_chat_messages: 5, // Min messages to keep in queue
|
|
192
|
+
sessionConfig: {}, // Override any of the above
|
|
193
|
+
|
|
194
|
+
// Storage
|
|
195
|
+
redis: true, // Set false to skip Redis proxy
|
|
196
|
+
key: 'custom-redis-key',
|
|
197
|
+
dynamodb_table: 'table', // Auto-creates DynamoDB adapter
|
|
198
|
+
dynamodb_region: 'us-east-1',
|
|
199
|
+
db: customAdapter, // Any adapter with put/get/delete/query interface
|
|
200
|
+
|
|
201
|
+
// User data
|
|
202
|
+
userData: {}, // Arbitrary user metadata
|
|
178
203
|
});
|
|
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
204
|
```
|
|
186
205
|
|
|
187
|
-
|
|
206
|
+
## Activate Options
|
|
207
|
+
|
|
188
208
|
```js
|
|
189
|
-
|
|
190
|
-
|
|
209
|
+
agent.activate({
|
|
210
|
+
createQ: true, // Create message queue context
|
|
211
|
+
prompt: 'Extra prompt', // Appended to class-level prompt
|
|
212
|
+
states: [], // Task state functions
|
|
213
|
+
parent: parentTask, // Parent task to spawn under
|
|
214
|
+
taskId: 'custom-id',
|
|
215
|
+
sequential_mode: true, // Process messages sequentially
|
|
216
|
+
|
|
217
|
+
// Override session config for this activation
|
|
218
|
+
token_limit: 8000,
|
|
219
|
+
max_depth: 10,
|
|
220
|
+
queue_limit: 200,
|
|
191
221
|
});
|
|
192
|
-
|
|
193
|
-
// Automatically filters excessive repeated tool calls
|
|
194
|
-
// Logs: "Dropping excessive tool call: get_weather (hit max_tool_repetition=5)"
|
|
195
222
|
```
|
|
196
223
|
|
|
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
|
-
```
|
|
224
|
+
## User Data
|
|
202
225
|
|
|
203
|
-
### Timeout Handling
|
|
204
226
|
```js
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
//
|
|
209
|
-
await q.sendMessage('user', 'Run slow analysis', null, { timeout: 30000 });
|
|
227
|
+
agent.setUserData('preference', 'dark-mode'); // returns this (chainable)
|
|
228
|
+
agent.getUserData('preference'); // 'dark-mode'
|
|
229
|
+
agent.getUserData(); // { preference: 'dark-mode' }
|
|
230
|
+
agent.clearUserData(); // returns this
|
|
210
231
|
```
|
|
211
232
|
|
|
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
|
|
223
|
-
|
|
224
|
-
---
|
|
233
|
+
## Session Info
|
|
225
234
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
* **NEW**: **Tool call state persistence** (active calls, deferred calls, waiting queues)
|
|
235
|
+
```js
|
|
236
|
+
agent.getSessionInfo();
|
|
237
|
+
// {
|
|
238
|
+
// id, name, running, completed,
|
|
239
|
+
// messageCount, childCount,
|
|
240
|
+
// userData, uptime
|
|
241
|
+
// }
|
|
242
|
+
|
|
243
|
+
await agent.closeSession(); // Close context and cancel task
|
|
244
|
+
```
|
|
237
245
|
|
|
238
|
-
|
|
246
|
+
## Database Access
|
|
239
247
|
|
|
240
|
-
|
|
248
|
+
Saico provides backend-agnostic DB methods. Configure via `dynamodb_table` (auto-creates DynamoDB adapter) or `db` (any adapter implementing the interface).
|
|
241
249
|
|
|
242
|
-
```
|
|
243
|
-
|
|
250
|
+
```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();
|
|
257
|
+
|
|
258
|
+
// 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');
|
|
262
|
+
|
|
263
|
+
// 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();
|
|
244
268
|
```
|
|
245
269
|
|
|
246
|
-
|
|
270
|
+
Override `_deserializeRecord(raw)` to transform raw DB records on retrieval (e.g., restore class instances):
|
|
247
271
|
|
|
248
272
|
```js
|
|
249
|
-
|
|
250
|
-
|
|
273
|
+
class MyAgent extends Saico {
|
|
274
|
+
_deserializeRecord(raw) {
|
|
275
|
+
if (raw.type === 'order') return new Order(raw);
|
|
276
|
+
return raw;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
251
279
|
```
|
|
252
280
|
|
|
253
|
-
|
|
281
|
+
## Serialization
|
|
254
282
|
|
|
255
283
|
```js
|
|
256
|
-
|
|
257
|
-
const
|
|
284
|
+
// Save
|
|
285
|
+
const json = agent.serialize();
|
|
258
286
|
|
|
259
|
-
//
|
|
260
|
-
const
|
|
287
|
+
// Restore
|
|
288
|
+
const restored = Saico.deserialize(json, {
|
|
289
|
+
tool_handler: myHandler,
|
|
290
|
+
functions: myFunctions,
|
|
291
|
+
});
|
|
261
292
|
```
|
|
262
293
|
|
|
263
|
-
|
|
294
|
+
Serialization includes: id, name, prompt, userData, sessionConfig, tm_create, isolate, and full context state (messages, tool_digest, chat_history).
|
|
264
295
|
|
|
265
|
-
|
|
296
|
+
## Redis Persistence
|
|
266
297
|
|
|
267
|
-
|
|
298
|
+
When Redis is initialized, Saico instances are automatically wrapped in an observable proxy. Any property change triggers a debounced save to Redis.
|
|
268
299
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
* All keys starting with `_` are ignored.
|
|
272
|
-
* Custom `.serialize()` methods (like on `Messages`) are respected.
|
|
273
|
-
* Object updates are **debounced (1s)** and only saved if actual changes are detected.
|
|
274
|
-
* **NEW**: Tool call tracking data is sanitized automatically
|
|
300
|
+
```js
|
|
301
|
+
const { init } = require('saico');
|
|
275
302
|
|
|
276
|
-
|
|
303
|
+
// Initialize with Redis
|
|
304
|
+
await init({ redis: true });
|
|
277
305
|
|
|
278
|
-
|
|
306
|
+
const agent = new Saico({ name: 'persistent-agent' });
|
|
307
|
+
agent.someProperty = 'value'; // Auto-saved to Redis
|
|
308
|
+
```
|
|
279
309
|
|
|
280
|
-
|
|
310
|
+
Properties prefixed with `_` are internal and not persisted.
|
|
281
311
|
|
|
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
|
|
312
|
+
## Tool Handler Interface
|
|
286
313
|
|
|
287
314
|
```js
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
content:
|
|
292
|
-
tool_calls: [{
|
|
293
|
-
id: 'call_abc123',
|
|
294
|
-
type: 'function',
|
|
295
|
-
function: {
|
|
296
|
-
name: 'get_weather',
|
|
297
|
-
arguments: '{"location": "New York"}'
|
|
298
|
-
}
|
|
299
|
-
}]
|
|
315
|
+
async function toolHandler(toolName, argumentsString) {
|
|
316
|
+
const args = JSON.parse(argumentsString);
|
|
317
|
+
// Execute tool logic
|
|
318
|
+
return result; // string or { content: string, functions?: [] }
|
|
300
319
|
}
|
|
301
|
-
|
|
302
|
-
// Saico handles the complete tool execution cycle automatically
|
|
303
320
|
```
|
|
304
321
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
## 🧪 Testing
|
|
322
|
+
### Tool Safety Features
|
|
308
323
|
|
|
309
|
-
|
|
324
|
+
- **Depth control** — `max_depth` (default: 5) prevents infinite tool call recursion
|
|
325
|
+
- **Deferred execution** — Tool calls defer when max depth is reached, resume when depth reduces
|
|
326
|
+
- **Duplicate detection** — Identical active tool calls are blocked
|
|
327
|
+
- **Repetition prevention** — `max_tool_repetition` (default: 20) blocks excessive repeated calls
|
|
328
|
+
- **Timeout handling** — Configurable timeout (default: 5s) with graceful failure
|
|
329
|
+
- **Message queuing** — Messages queue automatically when tool calls are pending
|
|
310
330
|
|
|
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
|
|
320
|
-
|
|
321
|
-
```bash
|
|
322
|
-
npm test # Run full test suite
|
|
323
|
-
```
|
|
331
|
+
## Low-Level API
|
|
324
332
|
|
|
325
|
-
|
|
333
|
+
For cases where you don't need the Saico master class:
|
|
326
334
|
|
|
327
|
-
|
|
335
|
+
```js
|
|
336
|
+
const { createTask, createContext, createQ } = require('saico');
|
|
337
|
+
|
|
338
|
+
// Create a task with context
|
|
339
|
+
const task = createTask({
|
|
340
|
+
name: 'my-task',
|
|
341
|
+
prompt: 'You are helpful',
|
|
342
|
+
tool_handler: handler,
|
|
343
|
+
functions: tools
|
|
344
|
+
});
|
|
345
|
+
const reply = await task.sendMessage('Hello');
|
|
328
346
|
|
|
347
|
+
// Standalone context (legacy)
|
|
348
|
+
const ctx = createQ('System prompt', null, 'tag', 4000, null, handler);
|
|
349
|
+
const reply = await ctx.sendMessage('user', 'Hello', functions);
|
|
329
350
|
```
|
|
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
351
|
|
|
342
|
-
##
|
|
352
|
+
## Project Structure
|
|
343
353
|
|
|
344
|
-
If upgrading from older versions:
|
|
345
|
-
|
|
346
|
-
### Old API:
|
|
347
|
-
```js
|
|
348
|
-
const q = createQ(prompt, opts, msgs, parent);
|
|
349
354
|
```
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
355
|
+
saico/
|
|
356
|
+
+-- index.js # Entry point, exports all components
|
|
357
|
+
+-- saico.js # Saico master class
|
|
358
|
+
+-- itask.js # Base task class (hierarchy, states, cancellation)
|
|
359
|
+
+-- msgs.js # Conversation context (message queue, tool calls, summarization)
|
|
360
|
+
+-- context.js # Backward-compat shim for msgs.js
|
|
361
|
+
+-- dynamo.js # DynamoDB storage adapter
|
|
362
|
+
+-- store.js # Storage abstraction (Redis + pluggable backends)
|
|
363
|
+
+-- openai.js # OpenAI API wrapper with retry logic
|
|
364
|
+
+-- redis.js # Redis persistence with observable proxy
|
|
365
|
+
+-- util.js # Utilities (token counting, logging)
|
|
354
366
|
```
|
|
355
367
|
|
|
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
|
-
---
|
|
368
|
+
## Testing
|
|
364
369
|
|
|
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
|
|
370
|
+
```bash
|
|
371
|
+
npm test
|
|
372
|
+
```
|
|
384
373
|
|
|
385
|
-
|
|
374
|
+
294 tests covering Saico lifecycle, task hierarchy, message handling, tool calls, DB adapters, serialization, and integration flows.
|
|
386
375
|
|
|
387
|
-
|
|
376
|
+
## Requirements
|
|
388
377
|
|
|
389
|
-
|
|
378
|
+
- Node.js >= 16.0.0
|
|
379
|
+
- `OPENAI_API_KEY` environment variable for LLM calls
|
|
380
|
+
- Redis (optional, for auto-persistence)
|
|
381
|
+
- AWS SDK v3 (optional peer dependency, for DynamoDB)
|
|
390
382
|
|
|
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**
|
|
383
|
+
## License
|
|
397
384
|
|
|
398
|
-
|
|
385
|
+
ISC
|