saico 2.2.2 → 2.3.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/context.js +3 -1181
- package/dynamo.js +227 -0
- package/index.js +7 -1
- package/itask.js +5 -1
- package/msgs.js +1213 -0
- package/package.json +14 -1
- package/saico.js +345 -0
- package/sid.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "saico",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"description": "Hierarchical AI Conversation Orchestrator - Task hierarchy with conversation contexts",
|
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
"index.js",
|
|
17
17
|
"itask.js",
|
|
18
18
|
"context.js",
|
|
19
|
+
"msgs.js",
|
|
19
20
|
"sid.js",
|
|
21
|
+
"saico.js",
|
|
22
|
+
"dynamo.js",
|
|
20
23
|
"openai.js",
|
|
21
24
|
"util.js",
|
|
22
25
|
"redis.js",
|
|
@@ -32,6 +35,16 @@
|
|
|
32
35
|
"tiktoken": "^1.0.17",
|
|
33
36
|
"redis": "^4.7.0"
|
|
34
37
|
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@aws-sdk/client-dynamodb": "^3.0.0",
|
|
40
|
+
"@aws-sdk/lib-dynamodb": "^3.0.0",
|
|
41
|
+
"@aws-sdk/util-dynamodb": "^3.0.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"@aws-sdk/client-dynamodb": { "optional": true },
|
|
45
|
+
"@aws-sdk/lib-dynamodb": { "optional": true },
|
|
46
|
+
"@aws-sdk/util-dynamodb": { "optional": true }
|
|
47
|
+
},
|
|
35
48
|
"devDependencies": {
|
|
36
49
|
"chai": "^4.5.0",
|
|
37
50
|
"chai-http": "^4.4.0",
|
package/saico.js
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const Itask = require('./itask.js');
|
|
5
|
+
const { Context } = require('./msgs.js');
|
|
6
|
+
const { Store } = require('./store.js');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Saico — Master class for building AI-powered services.
|
|
10
|
+
*
|
|
11
|
+
* External users extend this class instead of Itask. It separates object
|
|
12
|
+
* lifecycle from task activation:
|
|
13
|
+
*
|
|
14
|
+
* - Construction: sets up storage (Redis observable + optional DynamoDB),
|
|
15
|
+
* class-level prompt, tool config. No Itask is created yet.
|
|
16
|
+
* - activate(opts): creates the internal Itask and optionally attaches a
|
|
17
|
+
* message Q context (when opts.createQ is true).
|
|
18
|
+
* - DB access works before and after activation.
|
|
19
|
+
*
|
|
20
|
+
* `new Saico(opt)` returns a Redis observable proxy of the instance when
|
|
21
|
+
* Redis is available, enabling automatic persistence of public properties.
|
|
22
|
+
*/
|
|
23
|
+
class Saico {
|
|
24
|
+
/**
|
|
25
|
+
* @param {Object} opt
|
|
26
|
+
* @param {string} [opt.id] - Instance ID (auto-generated if omitted)
|
|
27
|
+
* @param {string} [opt.name] - Instance name (defaults to class name)
|
|
28
|
+
* @param {string} [opt.prompt] - Class-level system prompt
|
|
29
|
+
* @param {Function} [opt.tool_handler] - Tool handler function
|
|
30
|
+
* @param {Array} [opt.functions] - Available AI functions
|
|
31
|
+
* @param {string} [opt.key] - Redis key override (default: 'saico:<id>')
|
|
32
|
+
* @param {boolean} [opt.redis=true] - Set false to skip Redis proxy
|
|
33
|
+
* @param {string} [opt.dynamodb_table] - DynamoDB table name (enables db accessor)
|
|
34
|
+
* @param {string} [opt.dynamodb_region] - AWS region for DynamoDB
|
|
35
|
+
* @param {Object} [opt.dynamodb_client] - Injectable DynamoDB client (for testing)
|
|
36
|
+
* @param {Object} [opt.store] - Store instance override
|
|
37
|
+
*/
|
|
38
|
+
constructor(opt = {}) {
|
|
39
|
+
// Internal properties (underscore-prefixed, not persisted to Redis)
|
|
40
|
+
this._id = opt.id || crypto.randomBytes(8).toString('hex');
|
|
41
|
+
this._task = null;
|
|
42
|
+
this._store = opt.store || Store.instance || null;
|
|
43
|
+
this._opt = opt;
|
|
44
|
+
|
|
45
|
+
// Public configuration
|
|
46
|
+
this.name = opt.name || this.constructor.name || 'saico';
|
|
47
|
+
this.prompt = opt.prompt || '';
|
|
48
|
+
this.tool_handler = opt.tool_handler || null;
|
|
49
|
+
this.functions = opt.functions || null;
|
|
50
|
+
|
|
51
|
+
// DB backend — pluggable storage adapter.
|
|
52
|
+
// Any adapter that implements the same interface (put/get/delete/query/
|
|
53
|
+
// getAll/update/updatePath/listAppend/listAppendPath/nextCounterId/
|
|
54
|
+
// getCounterValue/setCounterValue/countItems) can be used.
|
|
55
|
+
this._db = opt.db || null;
|
|
56
|
+
if (!this._db && opt.dynamodb_table) {
|
|
57
|
+
const { DynamoDBAdapter } = require('./dynamo.js');
|
|
58
|
+
this._db = new DynamoDBAdapter({
|
|
59
|
+
table: opt.dynamodb_table,
|
|
60
|
+
region: opt.dynamodb_region,
|
|
61
|
+
client: opt.dynamodb_client,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Return Redis observable proxy (must be last in constructor).
|
|
66
|
+
// Subclasses calling super() will receive the proxy as `this`.
|
|
67
|
+
try {
|
|
68
|
+
const redis = require('./redis.js');
|
|
69
|
+
if (redis.rclient && opt.redis !== false) {
|
|
70
|
+
const key = 'saico:' + (opt.key || this._id);
|
|
71
|
+
return redis.createObservableForRedis(key, this);
|
|
72
|
+
}
|
|
73
|
+
} catch (e) { /* redis not available */ }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create the internal Itask and optionally a message Q context.
|
|
78
|
+
*
|
|
79
|
+
* @param {Object} opts
|
|
80
|
+
* @param {boolean} [opts.createQ] - If true, attach a message Q (Context)
|
|
81
|
+
* @param {string} [opts.prompt] - Additional prompt (appended to class-level)
|
|
82
|
+
* @param {Function} [opts.tool_handler] - Override tool handler
|
|
83
|
+
* @param {Array} [opts.functions] - Override functions
|
|
84
|
+
* @param {Array} [opts.states] - Task state functions
|
|
85
|
+
* @param {Itask} [opts.parent] - Parent task to spawn under
|
|
86
|
+
* @param {string} [opts.taskId] - Custom task ID
|
|
87
|
+
* @param {number} [opts.token_limit] - Token limit for context
|
|
88
|
+
* @param {number} [opts.max_depth] - Max tool call depth
|
|
89
|
+
* @param {number} [opts.max_tool_repetition] - Max tool repetition
|
|
90
|
+
* @param {number} [opts.queue_limit] - Message queue limit
|
|
91
|
+
* @param {number} [opts.min_chat_messages] - Min chat messages in queue
|
|
92
|
+
* @param {boolean} [opts.sequential_mode] - Sequential message processing
|
|
93
|
+
* @param {Array} [opts.msgs] - Initial messages
|
|
94
|
+
* @param {*} [opts.chat_history] - Chat history to restore
|
|
95
|
+
* @param {Object} [opts.contextConfig] - Additional Context config overrides
|
|
96
|
+
* @returns {Saico} this instance (for chaining)
|
|
97
|
+
*/
|
|
98
|
+
activate(opts = {}) {
|
|
99
|
+
if (this._task)
|
|
100
|
+
throw new Error('Already activated. Call deactivate() first.');
|
|
101
|
+
|
|
102
|
+
const states = opts.states || [];
|
|
103
|
+
|
|
104
|
+
// Build effective prompt: class-level + activation-level
|
|
105
|
+
const effectivePrompt = [this.prompt, opts.prompt].filter(Boolean).join('\n');
|
|
106
|
+
|
|
107
|
+
const taskOpt = {
|
|
108
|
+
name: this.name,
|
|
109
|
+
id: opts.taskId,
|
|
110
|
+
async: true,
|
|
111
|
+
store: this._store,
|
|
112
|
+
tool_handler: opts.tool_handler || this.tool_handler,
|
|
113
|
+
functions: opts.functions || this.functions,
|
|
114
|
+
bind: this, // State functions run with Saico instance as `this`
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (opts.parent)
|
|
118
|
+
taskOpt.spawn_parent = opts.parent;
|
|
119
|
+
|
|
120
|
+
this._task = new Itask(taskOpt, states);
|
|
121
|
+
|
|
122
|
+
// Delegate getStateSummary from task to this Saico instance
|
|
123
|
+
const saicoInstance = this;
|
|
124
|
+
this._task.getStateSummary = function () {
|
|
125
|
+
return saicoInstance.getStateSummary();
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Create message Q context if requested (only via createQ flag, NOT prompt)
|
|
129
|
+
if (opts.createQ) {
|
|
130
|
+
const contextConfig = {
|
|
131
|
+
tag: opts.tag || this._task.id,
|
|
132
|
+
token_limit: opts.token_limit,
|
|
133
|
+
max_depth: opts.max_depth,
|
|
134
|
+
max_tool_repetition: opts.max_tool_repetition,
|
|
135
|
+
queue_limit: opts.queue_limit,
|
|
136
|
+
min_chat_messages: opts.min_chat_messages,
|
|
137
|
+
tool_handler: taskOpt.tool_handler,
|
|
138
|
+
functions: taskOpt.functions,
|
|
139
|
+
sequential_mode: opts.sequential_mode,
|
|
140
|
+
msgs: opts.msgs,
|
|
141
|
+
chat_history: opts.chat_history,
|
|
142
|
+
...opts.contextConfig,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const augmentedPrompt = effectivePrompt
|
|
146
|
+
? effectivePrompt + Itask.BACKEND_EXPLANATION
|
|
147
|
+
: '';
|
|
148
|
+
const context = new Context(augmentedPrompt, this._task, contextConfig);
|
|
149
|
+
this._task.setContext(context);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return this;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Deactivate — close context, cancel task, clean up.
|
|
157
|
+
*/
|
|
158
|
+
async deactivate() {
|
|
159
|
+
if (!this._task) return;
|
|
160
|
+
if (this._task.context)
|
|
161
|
+
await this._task.closeContext();
|
|
162
|
+
this._task._ecancel();
|
|
163
|
+
this._task = null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---- Message relay ----
|
|
167
|
+
|
|
168
|
+
async sendMessage(content, functions, opts) {
|
|
169
|
+
if (!this._task)
|
|
170
|
+
throw new Error('Not activated. Call activate() first.');
|
|
171
|
+
return this._task.sendMessage(content, functions, opts);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async recvChatMessage(content, opts) {
|
|
175
|
+
if (!this._task)
|
|
176
|
+
throw new Error('Not activated. Call activate() first.');
|
|
177
|
+
return this._task.recvChatMessage(content, opts);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---- Task delegation ----
|
|
181
|
+
|
|
182
|
+
get task() { return this._task; }
|
|
183
|
+
get context() { return this._task?.context || null; }
|
|
184
|
+
get context_id() { return this._task?.context_id || null; }
|
|
185
|
+
get isActive() { return !!this._task && !this._task._completed; }
|
|
186
|
+
|
|
187
|
+
spawnTaskWithContext(opt, states) {
|
|
188
|
+
if (!this._task)
|
|
189
|
+
throw new Error('Not activated. Call activate() first.');
|
|
190
|
+
if (typeof opt === 'string')
|
|
191
|
+
opt = { name: opt };
|
|
192
|
+
|
|
193
|
+
const childTask = new Itask({
|
|
194
|
+
...opt,
|
|
195
|
+
spawn_parent: this._task,
|
|
196
|
+
store: this._store,
|
|
197
|
+
async: true,
|
|
198
|
+
}, states || []);
|
|
199
|
+
|
|
200
|
+
if (opt.prompt) {
|
|
201
|
+
const childContext = new Context(opt.prompt, childTask, {
|
|
202
|
+
tag: opt.tag || childTask.id,
|
|
203
|
+
token_limit: opt.token_limit,
|
|
204
|
+
max_depth: opt.max_depth,
|
|
205
|
+
max_tool_repetition: opt.max_tool_repetition,
|
|
206
|
+
queue_limit: opt.queue_limit,
|
|
207
|
+
min_chat_messages: opt.min_chat_messages,
|
|
208
|
+
tool_handler: opt.tool_handler || this.tool_handler,
|
|
209
|
+
functions: opt.functions || this.functions,
|
|
210
|
+
});
|
|
211
|
+
childTask.setContext(childContext);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
process.nextTick(() => {
|
|
215
|
+
try { childTask._run(); } catch (e) { console.error(e); }
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
return childTask;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
spawnTask(opt, states) {
|
|
222
|
+
if (!this._task)
|
|
223
|
+
throw new Error('Not activated. Call activate() first.');
|
|
224
|
+
if (typeof opt === 'string')
|
|
225
|
+
opt = { name: opt };
|
|
226
|
+
|
|
227
|
+
const childTask = new Itask({
|
|
228
|
+
...opt,
|
|
229
|
+
spawn_parent: this._task,
|
|
230
|
+
store: this._store,
|
|
231
|
+
async: true,
|
|
232
|
+
}, states || []);
|
|
233
|
+
|
|
234
|
+
process.nextTick(() => {
|
|
235
|
+
try { childTask._run(); } catch (e) { console.error(e); }
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
return childTask;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ---- Generic DB access ----
|
|
242
|
+
// These delegate to whatever _db backend was configured (DynamoDB, MongoDB,
|
|
243
|
+
// etc). Upper layers call these and don't care about the storage impl.
|
|
244
|
+
// All are no-ops (return undefined) when no backend is configured.
|
|
245
|
+
|
|
246
|
+
async dbPutItem(item, table) {
|
|
247
|
+
if (!this._db) return;
|
|
248
|
+
return this._db.put(item, table);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async dbGetItem(key, value, table) {
|
|
252
|
+
if (!this._db) return;
|
|
253
|
+
return this._db.get(key, value, table);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async dbDeleteItem(key, value, table) {
|
|
257
|
+
if (!this._db) return;
|
|
258
|
+
return this._db.delete(key, value, table);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async dbQuery(index, key, value, table) {
|
|
262
|
+
if (!this._db) return;
|
|
263
|
+
return this._db.query(index, key, value, table);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async dbGetAll(table) {
|
|
267
|
+
if (!this._db) return;
|
|
268
|
+
return this._db.getAll(table);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async dbUpdate(key, keyValue, setKey, item, table) {
|
|
272
|
+
if (!this._db) return;
|
|
273
|
+
return this._db.update(key, keyValue, setKey, item, table);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async dbUpdatePath(key, keyValue, path, setKey, item, table) {
|
|
277
|
+
if (!this._db) return;
|
|
278
|
+
return this._db.updatePath(key, keyValue, path, setKey, item, table);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async dbListAppend(key, keyValue, setKey, item, table) {
|
|
282
|
+
if (!this._db) return;
|
|
283
|
+
return this._db.listAppend(key, keyValue, setKey, item, table);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async dbListAppendPath(key, keyValue, path, setKey, item, table) {
|
|
287
|
+
if (!this._db) return;
|
|
288
|
+
return this._db.listAppendPath(key, keyValue, path, setKey, item, table);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async dbNextCounterId(counter, table) {
|
|
292
|
+
if (!this._db) return;
|
|
293
|
+
return this._db.nextCounterId(counter, table);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async dbGetCounterValue(counter, table) {
|
|
297
|
+
if (!this._db) return;
|
|
298
|
+
return this._db.getCounterValue(counter, table);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async dbSetCounterValue(counter, value, table) {
|
|
302
|
+
if (!this._db) return;
|
|
303
|
+
return this._db.setCounterValue(counter, value, table);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async dbCountItems(table) {
|
|
307
|
+
if (!this._db) return;
|
|
308
|
+
return this._db.countItems(table);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ---- Overridable hooks ----
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Override in subclasses to provide a state summary that appears
|
|
315
|
+
* in the message queue sent to the AI model.
|
|
316
|
+
* @returns {string}
|
|
317
|
+
*/
|
|
318
|
+
getStateSummary() { return ''; }
|
|
319
|
+
|
|
320
|
+
// ---- Serialization ----
|
|
321
|
+
|
|
322
|
+
serialize() {
|
|
323
|
+
const data = {
|
|
324
|
+
id: this._id,
|
|
325
|
+
name: this.name,
|
|
326
|
+
prompt: this.prompt,
|
|
327
|
+
};
|
|
328
|
+
if (this._task) {
|
|
329
|
+
data.task = {
|
|
330
|
+
id: this._task.id,
|
|
331
|
+
context_id: this._task.context_id,
|
|
332
|
+
context: this._task.context ? {
|
|
333
|
+
tag: this._task.context.tag,
|
|
334
|
+
msgs: this._task.context._msgs,
|
|
335
|
+
functions: this._task.context.functions,
|
|
336
|
+
chat_history: this._task.context.chat_history,
|
|
337
|
+
tool_digest: this._task.context.tool_digest,
|
|
338
|
+
} : null,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
return JSON.stringify(data);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
module.exports = { Saico };
|