saico 2.2.3 → 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/context.js +3 -1211
- package/dynamo.js +227 -0
- package/index.js +8 -5
- package/itask.js +16 -3
- package/msgs.js +1167 -0
- package/package.json +14 -2
- package/saico.js +617 -0
- package/sid.js +0 -248
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "saico",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.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,9 @@
|
|
|
16
16
|
"index.js",
|
|
17
17
|
"itask.js",
|
|
18
18
|
"context.js",
|
|
19
|
-
"
|
|
19
|
+
"msgs.js",
|
|
20
|
+
"saico.js",
|
|
21
|
+
"dynamo.js",
|
|
20
22
|
"openai.js",
|
|
21
23
|
"util.js",
|
|
22
24
|
"redis.js",
|
|
@@ -32,6 +34,16 @@
|
|
|
32
34
|
"tiktoken": "^1.0.17",
|
|
33
35
|
"redis": "^4.7.0"
|
|
34
36
|
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@aws-sdk/client-dynamodb": "^3.0.0",
|
|
39
|
+
"@aws-sdk/lib-dynamodb": "^3.0.0",
|
|
40
|
+
"@aws-sdk/util-dynamodb": "^3.0.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependenciesMeta": {
|
|
43
|
+
"@aws-sdk/client-dynamodb": { "optional": true },
|
|
44
|
+
"@aws-sdk/lib-dynamodb": { "optional": true },
|
|
45
|
+
"@aws-sdk/util-dynamodb": { "optional": true }
|
|
46
|
+
},
|
|
35
47
|
"devDependencies": {
|
|
36
48
|
"chai": "^4.5.0",
|
|
37
49
|
"chai-http": "^4.4.0",
|
package/saico.js
ADDED
|
@@ -0,0 +1,617 @@
|
|
|
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
|
+
* Saico orchestrates the full message payload sent to the LLM by walking its
|
|
21
|
+
* parent chain to aggregate prompts, tools, digests, and state summaries.
|
|
22
|
+
*
|
|
23
|
+
* `new Saico(opt)` returns a Redis observable proxy of the instance when
|
|
24
|
+
* Redis is available, enabling automatic persistence of public properties.
|
|
25
|
+
*/
|
|
26
|
+
class Saico {
|
|
27
|
+
/**
|
|
28
|
+
* @param {Object} opt
|
|
29
|
+
* @param {string} [opt.id] - Instance ID (auto-generated if omitted)
|
|
30
|
+
* @param {string} [opt.name] - Instance name (defaults to class name)
|
|
31
|
+
* @param {string} [opt.prompt] - Class-level system prompt
|
|
32
|
+
* @param {Function} [opt.tool_handler] - Tool handler function
|
|
33
|
+
* @param {Array} [opt.functions] - Available AI functions
|
|
34
|
+
* @param {string} [opt.key] - Redis key override (default: 'saico:<id>')
|
|
35
|
+
* @param {boolean} [opt.redis=true] - Set false to skip Redis proxy
|
|
36
|
+
* @param {boolean} [opt.isolate] - Isolate: don't aggregate from ancestors
|
|
37
|
+
* @param {string} [opt.dynamodb_table] - DynamoDB table name (enables db accessor)
|
|
38
|
+
* @param {string} [opt.dynamodb_region] - AWS region for DynamoDB
|
|
39
|
+
* @param {Object} [opt.dynamodb_client] - Injectable DynamoDB client (for testing)
|
|
40
|
+
* @param {Object} [opt.db] - Pluggable DB backend
|
|
41
|
+
* @param {Object} [opt.store] - Store instance override
|
|
42
|
+
* @param {Object} [opt.userData] - Initial user data
|
|
43
|
+
* @param {Object} [opt.sessionConfig] - Session config overrides
|
|
44
|
+
*/
|
|
45
|
+
constructor(opt = {}) {
|
|
46
|
+
// Internal properties (underscore-prefixed, not persisted to Redis)
|
|
47
|
+
this._id = opt.id || crypto.randomBytes(8).toString('hex');
|
|
48
|
+
this._task = null;
|
|
49
|
+
this._store = opt.store || Store.instance || null;
|
|
50
|
+
this._opt = opt;
|
|
51
|
+
this._isolate = opt.isolate || false;
|
|
52
|
+
|
|
53
|
+
// Public configuration
|
|
54
|
+
this.name = opt.name || this.constructor.name || 'saico';
|
|
55
|
+
this.prompt = opt.prompt || '';
|
|
56
|
+
this.tool_handler = opt.tool_handler || null;
|
|
57
|
+
this.functions = opt.functions || null;
|
|
58
|
+
|
|
59
|
+
// Absorbed from Sid
|
|
60
|
+
this.userData = opt.userData || {};
|
|
61
|
+
this.tm_create = Date.now();
|
|
62
|
+
this.sessionConfig = {
|
|
63
|
+
token_limit: opt.token_limit,
|
|
64
|
+
max_depth: opt.max_depth,
|
|
65
|
+
max_tool_repetition: opt.max_tool_repetition,
|
|
66
|
+
queue_limit: opt.queue_limit,
|
|
67
|
+
min_chat_messages: opt.min_chat_messages,
|
|
68
|
+
...opt.sessionConfig,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// DB backend — pluggable storage adapter.
|
|
72
|
+
this._db = opt.db || null;
|
|
73
|
+
if (!this._db && opt.dynamodb_table) {
|
|
74
|
+
const { DynamoDBAdapter } = require('./dynamo.js');
|
|
75
|
+
this._db = new DynamoDBAdapter({
|
|
76
|
+
table: opt.dynamodb_table,
|
|
77
|
+
region: opt.dynamodb_region,
|
|
78
|
+
client: opt.dynamodb_client,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Return Redis observable proxy (must be last in constructor).
|
|
83
|
+
// Subclasses calling super() will receive the proxy as `this`.
|
|
84
|
+
try {
|
|
85
|
+
const redis = require('./redis.js');
|
|
86
|
+
if (redis.rclient && opt.redis !== false) {
|
|
87
|
+
const key = 'saico:' + (opt.key || this._id);
|
|
88
|
+
return redis.createObservableForRedis(key, this);
|
|
89
|
+
}
|
|
90
|
+
} catch (e) { /* redis not available */ }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create the internal Itask and optionally a message Q context.
|
|
95
|
+
*
|
|
96
|
+
* @param {Object} opts
|
|
97
|
+
* @param {boolean} [opts.createQ] - If true, attach a message Q (Context)
|
|
98
|
+
* @param {string} [opts.prompt] - Additional prompt (appended to class-level)
|
|
99
|
+
* @param {Function} [opts.tool_handler] - Override tool handler
|
|
100
|
+
* @param {Array} [opts.functions] - Override functions
|
|
101
|
+
* @param {Array} [opts.states] - Task state functions
|
|
102
|
+
* @param {Itask} [opts.parent] - Parent task to spawn under
|
|
103
|
+
* @param {string} [opts.taskId] - Custom task ID
|
|
104
|
+
* @param {number} [opts.token_limit] - Token limit for context
|
|
105
|
+
* @param {number} [opts.max_depth] - Max tool call depth
|
|
106
|
+
* @param {number} [opts.max_tool_repetition] - Max tool repetition
|
|
107
|
+
* @param {number} [opts.queue_limit] - Message queue limit
|
|
108
|
+
* @param {number} [opts.min_chat_messages] - Min chat messages in queue
|
|
109
|
+
* @param {boolean} [opts.sequential_mode] - Sequential message processing
|
|
110
|
+
* @param {Array} [opts.msgs] - Initial messages
|
|
111
|
+
* @param {*} [opts.chat_history] - Chat history to restore
|
|
112
|
+
* @param {Object} [opts.contextConfig] - Additional Context config overrides
|
|
113
|
+
* @returns {Saico} this instance (for chaining)
|
|
114
|
+
*/
|
|
115
|
+
activate(opts = {}) {
|
|
116
|
+
if (this._task)
|
|
117
|
+
throw new Error('Already activated. Call deactivate() first.');
|
|
118
|
+
|
|
119
|
+
const states = opts.states || [];
|
|
120
|
+
|
|
121
|
+
// Build effective prompt: class-level + activation-level
|
|
122
|
+
const effectivePrompt = [this.prompt, opts.prompt].filter(Boolean).join('\n');
|
|
123
|
+
|
|
124
|
+
const taskOpt = {
|
|
125
|
+
name: this.name,
|
|
126
|
+
id: opts.taskId,
|
|
127
|
+
async: true,
|
|
128
|
+
store: this._store,
|
|
129
|
+
tool_handler: opts.tool_handler || this.tool_handler,
|
|
130
|
+
functions: opts.functions || this.functions,
|
|
131
|
+
bind: this, // State functions run with Saico instance as `this`
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
if (opts.parent)
|
|
135
|
+
taskOpt.spawn_parent = opts.parent;
|
|
136
|
+
|
|
137
|
+
this._task = new Itask(taskOpt, states);
|
|
138
|
+
|
|
139
|
+
// Store Saico reference on task for parent chain traversal
|
|
140
|
+
this._task._saico = this;
|
|
141
|
+
|
|
142
|
+
// Create message Q context if requested (only via createQ flag, NOT prompt)
|
|
143
|
+
if (opts.createQ) {
|
|
144
|
+
const contextConfig = {
|
|
145
|
+
tag: opts.tag || this._task.id,
|
|
146
|
+
token_limit: opts.token_limit ?? this.sessionConfig.token_limit,
|
|
147
|
+
max_depth: opts.max_depth ?? this.sessionConfig.max_depth,
|
|
148
|
+
max_tool_repetition: opts.max_tool_repetition ?? this.sessionConfig.max_tool_repetition,
|
|
149
|
+
queue_limit: opts.queue_limit ?? this.sessionConfig.queue_limit,
|
|
150
|
+
min_chat_messages: opts.min_chat_messages ?? this.sessionConfig.min_chat_messages,
|
|
151
|
+
tool_handler: taskOpt.tool_handler,
|
|
152
|
+
functions: taskOpt.functions,
|
|
153
|
+
sequential_mode: opts.sequential_mode,
|
|
154
|
+
msgs: opts.msgs,
|
|
155
|
+
chat_history: opts.chat_history,
|
|
156
|
+
...opts.contextConfig,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const augmentedPrompt = effectivePrompt
|
|
160
|
+
? effectivePrompt + Itask.BACKEND_EXPLANATION
|
|
161
|
+
: '';
|
|
162
|
+
const context = new Context(augmentedPrompt, this._task, contextConfig);
|
|
163
|
+
this._task.setContext(context);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return this;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Deactivate — bubble cleaned messages to parent, close context, cancel task.
|
|
171
|
+
* Pushes cleaned messages (no tool calls, no BACKEND) into the parent's Q,
|
|
172
|
+
* then closes the context without the default summary bubbling.
|
|
173
|
+
*/
|
|
174
|
+
async deactivate() {
|
|
175
|
+
if (!this._task) return;
|
|
176
|
+
if (this._task.context) {
|
|
177
|
+
// Find parent context to bubble cleaned messages
|
|
178
|
+
let parentTask = this._task.parent;
|
|
179
|
+
let parentCtx = null;
|
|
180
|
+
while (parentTask) {
|
|
181
|
+
if (parentTask.context) { parentCtx = parentTask.context; break; }
|
|
182
|
+
parentTask = parentTask.parent;
|
|
183
|
+
}
|
|
184
|
+
if (parentCtx) {
|
|
185
|
+
const cleaned = this.getRecentMessages(Infinity);
|
|
186
|
+
for (const msg of cleaned)
|
|
187
|
+
parentCtx.push(msg);
|
|
188
|
+
}
|
|
189
|
+
// Clean tool calls and close context without additional summary bubbling.
|
|
190
|
+
// We already pushed cleaned messages above — closeContext's own
|
|
191
|
+
// summarization would double-bubble.
|
|
192
|
+
if (this._task.context_id && typeof this._task.context.cleanToolCallsByTag === 'function')
|
|
193
|
+
this._task.context.cleanToolCallsByTag(this._task.context_id);
|
|
194
|
+
this._task.context = null;
|
|
195
|
+
}
|
|
196
|
+
this._task._ecancel();
|
|
197
|
+
this._task = null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ---- Saico parent chain traversal ----
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Walk up the Saico parent chain (stop at isolate boundary or root).
|
|
204
|
+
* Returns array ordered root -> ... -> this.
|
|
205
|
+
*/
|
|
206
|
+
_getSaicoAncestors() {
|
|
207
|
+
const chain = [this];
|
|
208
|
+
if (this._isolate) return chain;
|
|
209
|
+
let task = this._task?.parent;
|
|
210
|
+
while (task) {
|
|
211
|
+
if (task._saico) {
|
|
212
|
+
chain.unshift(task._saico);
|
|
213
|
+
if (task._saico._isolate) break;
|
|
214
|
+
}
|
|
215
|
+
task = task.parent;
|
|
216
|
+
}
|
|
217
|
+
return chain; // root -> ... -> this
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Build preamble and aggregated functions by walking the Saico chain.
|
|
222
|
+
* @param {Context} activeCtx - The deepest active context (for state summary logic)
|
|
223
|
+
* @returns {{ preamble: Array, allFunctions: Array }}
|
|
224
|
+
*/
|
|
225
|
+
_buildPreamble(activeCtx) {
|
|
226
|
+
const chain = this._getSaicoAncestors();
|
|
227
|
+
const preamble = [];
|
|
228
|
+
const allFunctions = [];
|
|
229
|
+
|
|
230
|
+
for (const saico of chain) {
|
|
231
|
+
// Prompt
|
|
232
|
+
if (saico.prompt)
|
|
233
|
+
preamble.push({ role: 'system', content: saico.prompt });
|
|
234
|
+
|
|
235
|
+
// State summary (can return array)
|
|
236
|
+
const summary = saico._getStateSummary(activeCtx);
|
|
237
|
+
if (Array.isArray(summary)) {
|
|
238
|
+
for (const item of summary) {
|
|
239
|
+
if (typeof item === 'string')
|
|
240
|
+
preamble.push({ role: 'system', content: '[State Summary]\n' + item });
|
|
241
|
+
else
|
|
242
|
+
preamble.push(item); // {role, content} message object
|
|
243
|
+
}
|
|
244
|
+
} else if (summary) {
|
|
245
|
+
preamble.push({ role: 'system', content: '[State Summary]\n' + summary });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Tools digest
|
|
249
|
+
if (saico.context?.tool_digest?.length > 0) {
|
|
250
|
+
const digestText = saico.context.tool_digest.map(entry =>
|
|
251
|
+
`[${new Date(entry.tm).toISOString()}] ${entry.tool}: ${entry.result}`
|
|
252
|
+
).join('\n');
|
|
253
|
+
preamble.push({ role: 'system', content: '[Tool Activity Log]\n' + digestText });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Collect functions
|
|
257
|
+
if (saico.functions) allFunctions.push(...saico.functions);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return { preamble, allFunctions };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ---- Message orchestration ----
|
|
264
|
+
|
|
265
|
+
async sendMessage(content, functions, opts) {
|
|
266
|
+
if (!this._task)
|
|
267
|
+
throw new Error('Not activated. Call activate() first.');
|
|
268
|
+
|
|
269
|
+
// Find the active context (own or walk up)
|
|
270
|
+
let ctx = this._task.getContext() || this._task.findContext();
|
|
271
|
+
if (!ctx)
|
|
272
|
+
throw new Error('No context available');
|
|
273
|
+
|
|
274
|
+
// Build preamble by walking Saico chain
|
|
275
|
+
const activeCtx = this._task.findDeepestContext() || ctx;
|
|
276
|
+
const { preamble, allFunctions } = this._buildPreamble(activeCtx);
|
|
277
|
+
|
|
278
|
+
// Merge with call-specific functions
|
|
279
|
+
if (functions) allFunctions.push(...(Array.isArray(functions) ? functions : [functions]));
|
|
280
|
+
|
|
281
|
+
opts = Object.assign({}, opts, {
|
|
282
|
+
tag: this._task.context_id,
|
|
283
|
+
_preamble: preamble,
|
|
284
|
+
_aggregatedFunctions: allFunctions.length > 0 ? allFunctions : null,
|
|
285
|
+
});
|
|
286
|
+
return ctx.sendMessage('user', '[BACKEND] ' + content, null, opts);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async recvChatMessage(content, opts) {
|
|
290
|
+
if (!this._task)
|
|
291
|
+
throw new Error('Not activated. Call activate() first.');
|
|
292
|
+
|
|
293
|
+
// Route DOWN to deepest descendant with a msg Q
|
|
294
|
+
const ctx = this._task.findDeepestContext();
|
|
295
|
+
if (!ctx)
|
|
296
|
+
throw new Error('No context available');
|
|
297
|
+
|
|
298
|
+
// Build preamble by walking Saico chain
|
|
299
|
+
const { preamble, allFunctions } = this._buildPreamble(ctx);
|
|
300
|
+
|
|
301
|
+
opts = Object.assign({}, opts, {
|
|
302
|
+
tag: ctx.tag,
|
|
303
|
+
_preamble: preamble,
|
|
304
|
+
_aggregatedFunctions: allFunctions.length > 0 ? allFunctions : null,
|
|
305
|
+
});
|
|
306
|
+
return ctx.sendMessage('user', content, null, opts);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ---- Task delegation ----
|
|
310
|
+
|
|
311
|
+
get task() { return this._task; }
|
|
312
|
+
get context() { return this._task?.context || null; }
|
|
313
|
+
get context_id() { return this._task?.context_id || null; }
|
|
314
|
+
get isActive() { return !!this._task && !this._task._completed; }
|
|
315
|
+
|
|
316
|
+
spawnTaskWithContext(opt, states) {
|
|
317
|
+
if (!this._task)
|
|
318
|
+
throw new Error('Not activated. Call activate() first.');
|
|
319
|
+
if (typeof opt === 'string')
|
|
320
|
+
opt = { name: opt };
|
|
321
|
+
|
|
322
|
+
const childTask = new Itask({
|
|
323
|
+
...opt,
|
|
324
|
+
spawn_parent: this._task,
|
|
325
|
+
store: this._store,
|
|
326
|
+
async: true,
|
|
327
|
+
}, states || []);
|
|
328
|
+
|
|
329
|
+
if (opt.prompt) {
|
|
330
|
+
const childContext = new Context(opt.prompt, childTask, {
|
|
331
|
+
tag: opt.tag || childTask.id,
|
|
332
|
+
token_limit: opt.token_limit ?? this.sessionConfig.token_limit,
|
|
333
|
+
max_depth: opt.max_depth ?? this.sessionConfig.max_depth,
|
|
334
|
+
max_tool_repetition: opt.max_tool_repetition ?? this.sessionConfig.max_tool_repetition,
|
|
335
|
+
queue_limit: opt.queue_limit ?? this.sessionConfig.queue_limit,
|
|
336
|
+
min_chat_messages: opt.min_chat_messages ?? this.sessionConfig.min_chat_messages,
|
|
337
|
+
tool_handler: opt.tool_handler || this.tool_handler,
|
|
338
|
+
functions: opt.functions || this.functions,
|
|
339
|
+
});
|
|
340
|
+
childTask.setContext(childContext);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
process.nextTick(() => {
|
|
344
|
+
try { childTask._run(); } catch (e) { console.error(e); }
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
return childTask;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
spawnTask(opt, states) {
|
|
351
|
+
if (!this._task)
|
|
352
|
+
throw new Error('Not activated. Call activate() first.');
|
|
353
|
+
if (typeof opt === 'string')
|
|
354
|
+
opt = { name: opt };
|
|
355
|
+
|
|
356
|
+
const childTask = new Itask({
|
|
357
|
+
...opt,
|
|
358
|
+
spawn_parent: this._task,
|
|
359
|
+
store: this._store,
|
|
360
|
+
async: true,
|
|
361
|
+
}, states || []);
|
|
362
|
+
|
|
363
|
+
process.nextTick(() => {
|
|
364
|
+
try { childTask._run(); } catch (e) { console.error(e); }
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
return childTask;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ---- State Summary ----
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Override in subclasses to provide a state summary.
|
|
374
|
+
* @returns {string}
|
|
375
|
+
*/
|
|
376
|
+
getStateSummary() { return ''; }
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Get recent user/assistant messages (filtering out tool calls and BACKEND msgs).
|
|
380
|
+
* @param {number} n - Max number of messages to return
|
|
381
|
+
* @returns {Array<{role: string, content: string}>}
|
|
382
|
+
*/
|
|
383
|
+
getRecentMessages(n = 5) {
|
|
384
|
+
if (!this.context) return [];
|
|
385
|
+
return this.context._msgs
|
|
386
|
+
.filter(m => {
|
|
387
|
+
if (m.msg.role === 'tool' || m.msg.tool_calls) return false;
|
|
388
|
+
if (typeof m.msg.content === 'string' && m.msg.content.startsWith('[BACKEND]')) return false;
|
|
389
|
+
return m.msg.role === 'user' || m.msg.role === 'assistant';
|
|
390
|
+
})
|
|
391
|
+
.slice(-n)
|
|
392
|
+
.map(m => ({ role: m.msg.role, content: m.msg.content }));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Internal state summary builder. Includes own getStateSummary() and,
|
|
397
|
+
* if this context is NOT the active (deepest) Q, includes recent messages.
|
|
398
|
+
* @param {Context} activeCtx - The deepest active context
|
|
399
|
+
* @returns {Array|string|null}
|
|
400
|
+
*/
|
|
401
|
+
_getStateSummary(activeCtx) {
|
|
402
|
+
const parts = [];
|
|
403
|
+
const own = this.getStateSummary();
|
|
404
|
+
if (own) parts.push(own);
|
|
405
|
+
|
|
406
|
+
// If this context is NOT the active (deepest) Q, include recent messages
|
|
407
|
+
if (this.context && activeCtx && this.context !== activeCtx) {
|
|
408
|
+
const recent = this.getRecentMessages(5);
|
|
409
|
+
if (recent.length > 0) parts.push(...recent);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return parts.length > 0 ? parts : null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ---- User Data (absorbed from Sid) ----
|
|
416
|
+
|
|
417
|
+
setUserData(key, value) {
|
|
418
|
+
this.userData[key] = value;
|
|
419
|
+
return this;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
getUserData(key) {
|
|
423
|
+
return key ? this.userData[key] : this.userData;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
clearUserData() {
|
|
427
|
+
this.userData = {};
|
|
428
|
+
return this;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ---- Session Info ----
|
|
432
|
+
|
|
433
|
+
getSessionInfo() {
|
|
434
|
+
return {
|
|
435
|
+
id: this._id,
|
|
436
|
+
name: this.name,
|
|
437
|
+
running: this._task?.running || false,
|
|
438
|
+
completed: this._task?._completed || false,
|
|
439
|
+
messageCount: this.context?.length || 0,
|
|
440
|
+
childCount: this._task?.child?.size || 0,
|
|
441
|
+
userData: this.userData,
|
|
442
|
+
uptime: Date.now() - this.tm_create,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async closeSession() {
|
|
447
|
+
if (!this._task) return;
|
|
448
|
+
if (this._task.context)
|
|
449
|
+
await this._task.context.close();
|
|
450
|
+
this._task._ecancel();
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ---- Generic DB access ----
|
|
454
|
+
|
|
455
|
+
async dbPutItem(item, table) {
|
|
456
|
+
if (!this._db) return;
|
|
457
|
+
return this._db.put(item, table);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async dbGetItem(key, value, table) {
|
|
461
|
+
if (!this._db) return;
|
|
462
|
+
const result = await this._db.get(key, value, table);
|
|
463
|
+
return result ? this._deserializeRecord(result) : result;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async dbDeleteItem(key, value, table) {
|
|
467
|
+
if (!this._db) return;
|
|
468
|
+
return this._db.delete(key, value, table);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async dbQuery(index, key, value, table) {
|
|
472
|
+
if (!this._db) return;
|
|
473
|
+
const results = await this._db.query(index, key, value, table);
|
|
474
|
+
return Array.isArray(results)
|
|
475
|
+
? results.map(r => this._deserializeRecord(r))
|
|
476
|
+
: results;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async dbGetAll(table) {
|
|
480
|
+
if (!this._db) return;
|
|
481
|
+
const results = await this._db.getAll(table);
|
|
482
|
+
return Array.isArray(results)
|
|
483
|
+
? results.map(r => this._deserializeRecord(r))
|
|
484
|
+
: results;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async dbUpdate(key, keyValue, setKey, item, table) {
|
|
488
|
+
if (!this._db) return;
|
|
489
|
+
return this._db.update(key, keyValue, setKey, item, table);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async dbUpdatePath(key, keyValue, path, setKey, item, table) {
|
|
493
|
+
if (!this._db) return;
|
|
494
|
+
return this._db.updatePath(key, keyValue, path, setKey, item, table);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async dbListAppend(key, keyValue, setKey, item, table) {
|
|
498
|
+
if (!this._db) return;
|
|
499
|
+
return this._db.listAppend(key, keyValue, setKey, item, table);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async dbListAppendPath(key, keyValue, path, setKey, item, table) {
|
|
503
|
+
if (!this._db) return;
|
|
504
|
+
return this._db.listAppendPath(key, keyValue, path, setKey, item, table);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async dbNextCounterId(counter, table) {
|
|
508
|
+
if (!this._db) return;
|
|
509
|
+
return this._db.nextCounterId(counter, table);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async dbGetCounterValue(counter, table) {
|
|
513
|
+
if (!this._db) return;
|
|
514
|
+
return this._db.getCounterValue(counter, table);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async dbSetCounterValue(counter, value, table) {
|
|
518
|
+
if (!this._db) return;
|
|
519
|
+
return this._db.setCounterValue(counter, value, table);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async dbCountItems(table) {
|
|
523
|
+
if (!this._db) return;
|
|
524
|
+
return this._db.countItems(table);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// ---- DB deserialization hook ----
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Override in subclasses to transform raw DB records (e.g. restore class instances).
|
|
531
|
+
* Called by dbGetItem, dbQuery, dbGetAll.
|
|
532
|
+
* @param {Object} raw - Raw record from DB
|
|
533
|
+
* @returns {Object} Transformed record
|
|
534
|
+
*/
|
|
535
|
+
_deserializeRecord(raw) { return raw; }
|
|
536
|
+
|
|
537
|
+
// ---- Serialization ----
|
|
538
|
+
|
|
539
|
+
serialize() {
|
|
540
|
+
const data = {
|
|
541
|
+
id: this._id,
|
|
542
|
+
name: this.name,
|
|
543
|
+
prompt: this.prompt,
|
|
544
|
+
userData: this.userData,
|
|
545
|
+
sessionConfig: this.sessionConfig,
|
|
546
|
+
tm_create: this.tm_create,
|
|
547
|
+
isolate: this._isolate,
|
|
548
|
+
};
|
|
549
|
+
if (this._task) {
|
|
550
|
+
data.task = {
|
|
551
|
+
id: this._task.id,
|
|
552
|
+
context_id: this._task.context_id,
|
|
553
|
+
context: this._task.context ? {
|
|
554
|
+
tag: this._task.context.tag,
|
|
555
|
+
msgs: this._task.context._msgs,
|
|
556
|
+
functions: this._task.context.functions,
|
|
557
|
+
chat_history: this._task.context.chat_history,
|
|
558
|
+
tool_digest: this._task.context.tool_digest,
|
|
559
|
+
} : null,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
return JSON.stringify(data);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Restore a Saico instance from serialized data.
|
|
567
|
+
* @param {string|Object} data - Serialized data (JSON string or object)
|
|
568
|
+
* @param {Object} opt - Options (tool_handler, functions, store, states, etc.)
|
|
569
|
+
* @returns {Saico}
|
|
570
|
+
*/
|
|
571
|
+
static deserialize(data, opt = {}) {
|
|
572
|
+
const parsed = typeof data === 'string' ? JSON.parse(data) : data;
|
|
573
|
+
|
|
574
|
+
const instance = new Saico({
|
|
575
|
+
id: parsed.id,
|
|
576
|
+
name: parsed.name,
|
|
577
|
+
prompt: parsed.prompt,
|
|
578
|
+
userData: parsed.userData,
|
|
579
|
+
sessionConfig: parsed.sessionConfig,
|
|
580
|
+
isolate: parsed.isolate,
|
|
581
|
+
tool_handler: opt.tool_handler,
|
|
582
|
+
functions: opt.functions || parsed.task?.context?.functions,
|
|
583
|
+
store: opt.store,
|
|
584
|
+
redis: false, // No Redis proxy during deserialization
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
instance.tm_create = parsed.tm_create || instance.tm_create;
|
|
588
|
+
|
|
589
|
+
// Activate with restored context if task data exists
|
|
590
|
+
if (parsed.task) {
|
|
591
|
+
instance.activate({
|
|
592
|
+
createQ: !!parsed.task.context,
|
|
593
|
+
taskId: parsed.task.id,
|
|
594
|
+
tag: parsed.task.context?.tag,
|
|
595
|
+
chat_history: parsed.task.context?.chat_history,
|
|
596
|
+
tool_handler: opt.tool_handler,
|
|
597
|
+
functions: opt.functions || parsed.task.context?.functions,
|
|
598
|
+
states: opt.states || [],
|
|
599
|
+
...opt,
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// Restore messages to context
|
|
603
|
+
if (parsed.task.context?.msgs && instance._task.context) {
|
|
604
|
+
instance._task.context._msgs = parsed.task.context.msgs;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Restore tool_digest
|
|
608
|
+
if (Array.isArray(parsed.task.context?.tool_digest) && instance._task.context) {
|
|
609
|
+
instance._task.context.tool_digest = parsed.task.context.tool_digest;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return instance;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
module.exports = { Saico };
|