saico 2.2.3 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saico",
3
- "version": "2.2.3",
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 };
package/sid.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const Itask = require('./itask.js');
4
- const { Context, createContext } = require('./context.js');
4
+ const { Context, createContext } = require('./msgs.js');
5
5
  const { Store } = require('./store.js');
6
6
 
7
7
  /**