saico 2.7.1 → 2.8.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 +15 -13
- package/msgs.js +47 -32
- package/package.json +1 -1
- package/saico.js +82 -79
package/README.md
CHANGED
|
@@ -156,29 +156,28 @@ When a Saico's context is not the deepest active one, its last 5 user/assistant
|
|
|
156
156
|
### Spawning Child Saico Instances
|
|
157
157
|
|
|
158
158
|
```js
|
|
159
|
-
// Child with its own conversation context
|
|
159
|
+
// Child with its own conversation context (auto-activated by spawn)
|
|
160
160
|
const child = new Saico({
|
|
161
161
|
name: 'subtask',
|
|
162
162
|
prompt: 'Handle this specific sub-task',
|
|
163
|
+
createQ: true,
|
|
163
164
|
functions: [/* child-specific tools */],
|
|
164
165
|
});
|
|
165
|
-
child.activate({ createQ: true });
|
|
166
166
|
agent.spawn(child);
|
|
167
167
|
await child.sendMessage('Working on subtask...');
|
|
168
168
|
|
|
169
169
|
// Child without context (uses parent's via findContext())
|
|
170
170
|
const simple = new Saico({ name: 'simple' });
|
|
171
|
-
simple.activate();
|
|
172
171
|
agent.spawn(simple);
|
|
173
172
|
await simple.sendMessage('Quick operation');
|
|
174
173
|
|
|
175
174
|
// spawnAndRun: spawn + schedule child task to run on nextTick
|
|
176
175
|
const runner = new Saico({ name: 'runner' });
|
|
177
|
-
runner.
|
|
176
|
+
runner.states = [async function() { return await this.sendMessage('Go'); }];
|
|
178
177
|
agent.spawnAndRun(runner);
|
|
179
178
|
```
|
|
180
179
|
|
|
181
|
-
|
|
180
|
+
Parent must be activated before calling `spawn()` or `spawnAndRun()`. Children are auto-activated if needed.
|
|
182
181
|
|
|
183
182
|
### Deactivation and Message Bubbling
|
|
184
183
|
|
|
@@ -258,7 +257,10 @@ agent.getSessionInfo();
|
|
|
258
257
|
// userData, uptime
|
|
259
258
|
// }
|
|
260
259
|
|
|
261
|
-
await agent.closeSession(); //
|
|
260
|
+
await agent.closeSession(); // Saves full state to Store, cancels task
|
|
261
|
+
|
|
262
|
+
// Restore from Store
|
|
263
|
+
const restored = await Saico.rehydrate(agent._id, { store });
|
|
262
264
|
```
|
|
263
265
|
|
|
264
266
|
## Database Access
|
|
@@ -299,16 +301,16 @@ class MyAgent extends Saico {
|
|
|
299
301
|
## Serialization
|
|
300
302
|
|
|
301
303
|
```js
|
|
302
|
-
//
|
|
304
|
+
// In-memory snapshot (raw msgs, used by Redis proxy)
|
|
303
305
|
const json = agent.serialize();
|
|
306
|
+
const restored = Saico.deserialize(json);
|
|
304
307
|
|
|
305
|
-
//
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
});
|
|
308
|
+
// Durable persistence (compressed msgs, saved to Store)
|
|
309
|
+
await agent.closeSession();
|
|
310
|
+
const restored2 = await Saico.rehydrate(agent._id, { store });
|
|
309
311
|
```
|
|
310
312
|
|
|
311
|
-
|
|
313
|
+
`serialize()` includes: id, name, prompt, userData, sessionConfig, tm_create, isolate, and full context state (raw messages, tool_digest). `closeSession()` saves the same shape but with compressed messages for durable storage.
|
|
312
314
|
|
|
313
315
|
## Redis Persistence
|
|
314
316
|
|
|
@@ -388,7 +390,7 @@ saico/
|
|
|
388
390
|
npm test
|
|
389
391
|
```
|
|
390
392
|
|
|
391
|
-
|
|
393
|
+
296 tests covering Saico lifecycle, context ownership, spawn/spawnAndRun, task hierarchy, message handling, tool calls, DB adapters, serialization, persistence (closeSession/rehydrate), and integration flows.
|
|
392
394
|
|
|
393
395
|
## Requirements
|
|
394
396
|
|
package/msgs.js
CHANGED
|
@@ -33,9 +33,6 @@ class Context {
|
|
|
33
33
|
this._deferred_tool_calls = [];
|
|
34
34
|
this._tool_call_sequence = [];
|
|
35
35
|
|
|
36
|
-
// Chat history persistence
|
|
37
|
-
this.chat_history = config.chat_history || null;
|
|
38
|
-
|
|
39
36
|
this._msgs = [];
|
|
40
37
|
this._waitingQueue = [];
|
|
41
38
|
this._active_tool_calls = new Map();
|
|
@@ -53,7 +50,8 @@ class Context {
|
|
|
53
50
|
// Tool digest — persistent history of tool calls that mutated task state
|
|
54
51
|
this.tool_digest = config.tool_digest || [];
|
|
55
52
|
|
|
56
|
-
// Initialize messages
|
|
53
|
+
// Initialize messages: explicit msgs take priority over chat_history
|
|
54
|
+
this._chat_history = config.chat_history || null;
|
|
57
55
|
(config.msgs || []).forEach(m => this.push(m));
|
|
58
56
|
|
|
59
57
|
_log('created Context for tag', this.tag);
|
|
@@ -66,6 +64,51 @@ class Context {
|
|
|
66
64
|
this.functions = task?.functions;
|
|
67
65
|
}
|
|
68
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Decompress _chat_history into _msgs. Call after construction when
|
|
69
|
+
* restoring from persisted state. No-op if chat_history is absent or
|
|
70
|
+
* _msgs were already provided via config.msgs.
|
|
71
|
+
*/
|
|
72
|
+
async initHistory() {
|
|
73
|
+
if (!this._chat_history || this._msgs.length > 0)
|
|
74
|
+
return;
|
|
75
|
+
const messages = await util.decompressMessages(this._chat_history);
|
|
76
|
+
if (!Array.isArray(messages) || messages.length === 0)
|
|
77
|
+
return;
|
|
78
|
+
for (const m of messages) {
|
|
79
|
+
this._msgs.push({
|
|
80
|
+
msg: m,
|
|
81
|
+
opts: {},
|
|
82
|
+
msgid: crypto.randomBytes(2).toString('hex'),
|
|
83
|
+
replied: 1,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Prepare the message Q for storage. Filters out tool calls, tool
|
|
90
|
+
* responses, and [BACKEND] messages, trims to QUEUE_LIMIT, compresses.
|
|
91
|
+
* Returns { chat_history, tool_digest }. Does NOT mutate _msgs.
|
|
92
|
+
*/
|
|
93
|
+
async prepareForStorage() {
|
|
94
|
+
const cleaned = this._msgs.filter(m => {
|
|
95
|
+
if (m.msg.tool_calls) return false;
|
|
96
|
+
if (m.msg.role === 'tool') return false;
|
|
97
|
+
if (typeof m.msg.content === 'string' && m.msg.content.startsWith('[BACKEND]')) return false;
|
|
98
|
+
return true;
|
|
99
|
+
}).map(m => m.msg);
|
|
100
|
+
|
|
101
|
+
const trimmed = cleaned.length > this.QUEUE_LIMIT
|
|
102
|
+
? cleaned.slice(-this.QUEUE_LIMIT)
|
|
103
|
+
: cleaned;
|
|
104
|
+
|
|
105
|
+
const chat_history = trimmed.length > 0
|
|
106
|
+
? await util.compressMessages(trimmed)
|
|
107
|
+
: null;
|
|
108
|
+
|
|
109
|
+
return { chat_history, tool_digest: this.tool_digest || [] };
|
|
110
|
+
}
|
|
111
|
+
|
|
69
112
|
// Snapshot all public (non-underscore) task properties for dirty detection.
|
|
70
113
|
// Mirrors the observable proxy convention: _ prefix = internal, ignored.
|
|
71
114
|
// Does NOT call serialize() — that is for persistence, not dirty detection.
|
|
@@ -447,34 +490,6 @@ class Context {
|
|
|
447
490
|
_log('Finished closing Context tag', this.tag);
|
|
448
491
|
}
|
|
449
492
|
|
|
450
|
-
// Load chat history from store into message queue
|
|
451
|
-
async loadHistory(store) {
|
|
452
|
-
if (!store || !this.tag)
|
|
453
|
-
return;
|
|
454
|
-
const data = await store.load(this.tag);
|
|
455
|
-
if (!data)
|
|
456
|
-
return;
|
|
457
|
-
if (Array.isArray(data.tool_digest))
|
|
458
|
-
this.tool_digest = data.tool_digest;
|
|
459
|
-
if (!data.chat_history)
|
|
460
|
-
return;
|
|
461
|
-
const messages = await util.decompressMessages(data.chat_history);
|
|
462
|
-
if (!Array.isArray(messages) || messages.length === 0)
|
|
463
|
-
return;
|
|
464
|
-
// Find the index after the last system message to insert history
|
|
465
|
-
let insertIdx = 0;
|
|
466
|
-
for (let i = 0; i < this._msgs.length; i++) {
|
|
467
|
-
if (this._msgs[i].msg.role === 'system')
|
|
468
|
-
insertIdx = i + 1;
|
|
469
|
-
}
|
|
470
|
-
const historyMsgs = messages.map(m => ({
|
|
471
|
-
msg: m,
|
|
472
|
-
opts: {},
|
|
473
|
-
msgid: crypto.randomBytes(2).toString('hex'),
|
|
474
|
-
replied: 1
|
|
475
|
-
}));
|
|
476
|
-
this._msgs.splice(insertIdx, 0, ...historyMsgs);
|
|
477
|
-
}
|
|
478
493
|
|
|
479
494
|
// Remove tool-related messages tagged with a specific tag
|
|
480
495
|
cleanToolCallsByTag(tag) {
|
package/package.json
CHANGED
package/saico.js
CHANGED
|
@@ -152,6 +152,7 @@ class Saico {
|
|
|
152
152
|
sequential_mode: opts.sequential_mode,
|
|
153
153
|
msgs: opts.msgs,
|
|
154
154
|
chat_history: opts.chat_history,
|
|
155
|
+
tool_digest: opts.tool_digest,
|
|
155
156
|
...opts.contextConfig,
|
|
156
157
|
};
|
|
157
158
|
|
|
@@ -223,51 +224,6 @@ class Saico {
|
|
|
223
224
|
return deepest ? deepest.context : null;
|
|
224
225
|
}
|
|
225
226
|
|
|
226
|
-
/**
|
|
227
|
-
* Close this Saico's context and bubble summary to parent.
|
|
228
|
-
*/
|
|
229
|
-
async closeContext() {
|
|
230
|
-
if (!this.context)
|
|
231
|
-
return;
|
|
232
|
-
|
|
233
|
-
// Clean tool call messages tagged with this context_id
|
|
234
|
-
if (this.context_id && typeof this.context.cleanToolCallsByTag === 'function')
|
|
235
|
-
this.context.cleanToolCallsByTag(this.context_id);
|
|
236
|
-
|
|
237
|
-
// Filter out tool calls and [BACKEND] messages, compress remaining as chat_history
|
|
238
|
-
const cleanedMsgs = this.context._msgs.filter(m => {
|
|
239
|
-
if (m.msg.tool_calls) return false;
|
|
240
|
-
if (m.msg.role === 'tool') return false;
|
|
241
|
-
if (typeof m.msg.content === 'string' && m.msg.content.startsWith('[BACKEND]')) return false;
|
|
242
|
-
return true;
|
|
243
|
-
}).map(m => m.msg);
|
|
244
|
-
|
|
245
|
-
// Trim to last QUEUE_LIMIT before persisting
|
|
246
|
-
const queueLimit = this.context.QUEUE_LIMIT || 30;
|
|
247
|
-
const trimmedMsgs = cleanedMsgs.length > queueLimit
|
|
248
|
-
? cleanedMsgs.slice(-queueLimit)
|
|
249
|
-
: cleanedMsgs;
|
|
250
|
-
|
|
251
|
-
if (trimmedMsgs.length > 0) {
|
|
252
|
-
const chat_history = await util.compressMessages(trimmedMsgs);
|
|
253
|
-
this.context.chat_history = chat_history;
|
|
254
|
-
|
|
255
|
-
// Persist to store
|
|
256
|
-
const store = this._store || Store.instance;
|
|
257
|
-
if (store && this.context_id) {
|
|
258
|
-
await store.save(this.context_id, {
|
|
259
|
-
chat_history,
|
|
260
|
-
tool_digest: this.context.tool_digest || [],
|
|
261
|
-
prompt: this.context.prompt,
|
|
262
|
-
tag: this.context.tag,
|
|
263
|
-
tm_closed: Date.now()
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
await this.context.close();
|
|
269
|
-
}
|
|
270
|
-
|
|
271
227
|
/**
|
|
272
228
|
* Deactivate — bubble cleaned messages to parent, close context, cancel task.
|
|
273
229
|
* Pushes cleaned messages (no tool calls, no BACKEND) into the parent's Q,
|
|
@@ -309,15 +265,16 @@ class Saico {
|
|
|
309
265
|
spawn(child) {
|
|
310
266
|
if (!this._task)
|
|
311
267
|
throw new Error('Not activated. Call activate() first.');
|
|
312
|
-
if (!(child instanceof Saico)
|
|
313
|
-
throw new Error('Child must be
|
|
268
|
+
if (!(child instanceof Saico))
|
|
269
|
+
throw new Error('Child must be a Saico instance.');
|
|
270
|
+
if (!child._task) child.activate();
|
|
314
271
|
this._task.spawn(child._task);
|
|
315
272
|
return child;
|
|
316
273
|
}
|
|
317
274
|
|
|
318
275
|
/**
|
|
319
276
|
* Spawn a child Saico and start its task running.
|
|
320
|
-
* @param {Saico} child -
|
|
277
|
+
* @param {Saico} child - A Saico instance (auto-activated if needed)
|
|
321
278
|
* @returns {Saico} the child (for chaining)
|
|
322
279
|
*/
|
|
323
280
|
spawnAndRun(child) {
|
|
@@ -518,10 +475,38 @@ class Saico {
|
|
|
518
475
|
};
|
|
519
476
|
}
|
|
520
477
|
|
|
478
|
+
/**
|
|
479
|
+
* Close the session — compress msgs, save full state to Store, cancel task.
|
|
480
|
+
* The saved object has the same shape as serialize() but with compressed
|
|
481
|
+
* context messages (chat_history) instead of raw _msgs.
|
|
482
|
+
*/
|
|
521
483
|
async closeSession() {
|
|
522
484
|
if (!this._task) return;
|
|
523
|
-
|
|
524
|
-
|
|
485
|
+
|
|
486
|
+
// Save full state to Store with compressed msgs
|
|
487
|
+
const store = this._store || Store.instance;
|
|
488
|
+
if (store && this.context) {
|
|
489
|
+
const { chat_history, tool_digest } = await this.context.prepareForStorage();
|
|
490
|
+
const data = {
|
|
491
|
+
id: this._id,
|
|
492
|
+
name: this.name,
|
|
493
|
+
prompt: this.prompt,
|
|
494
|
+
userData: this.userData,
|
|
495
|
+
sessionConfig: this.sessionConfig,
|
|
496
|
+
tm_create: this.tm_create,
|
|
497
|
+
isolate: this._isolate,
|
|
498
|
+
taskId: this._task.id,
|
|
499
|
+
context_id: this.context_id,
|
|
500
|
+
context: {
|
|
501
|
+
tag: this.context.tag,
|
|
502
|
+
chat_history,
|
|
503
|
+
tool_digest,
|
|
504
|
+
functions: this.context.functions,
|
|
505
|
+
},
|
|
506
|
+
};
|
|
507
|
+
await store.save(this._id, data);
|
|
508
|
+
}
|
|
509
|
+
|
|
525
510
|
this._task._ecancel();
|
|
526
511
|
}
|
|
527
512
|
|
|
@@ -625,6 +610,11 @@ class Saico {
|
|
|
625
610
|
|
|
626
611
|
// ---- Serialization ----
|
|
627
612
|
|
|
613
|
+
/**
|
|
614
|
+
* Serialize the Saico instance to a JSON string.
|
|
615
|
+
* Context messages are included as raw _msgs (for Redis / in-memory use).
|
|
616
|
+
* For durable storage with compressed msgs, use closeSession().
|
|
617
|
+
*/
|
|
628
618
|
serialize() {
|
|
629
619
|
const data = {
|
|
630
620
|
id: this._id,
|
|
@@ -635,24 +625,21 @@ class Saico {
|
|
|
635
625
|
tm_create: this.tm_create,
|
|
636
626
|
isolate: this._isolate,
|
|
637
627
|
};
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
chat_history: this.context.chat_history,
|
|
647
|
-
tool_digest: this.context.tool_digest,
|
|
648
|
-
} : null,
|
|
649
|
-
};
|
|
650
|
-
}
|
|
628
|
+
data.taskId = this._task?.id || null;
|
|
629
|
+
data.context_id = this.context_id || null;
|
|
630
|
+
data.context = this.context ? {
|
|
631
|
+
tag: this.context.tag,
|
|
632
|
+
msgs: this.context._msgs,
|
|
633
|
+
functions: this.context.functions,
|
|
634
|
+
tool_digest: this.context.tool_digest,
|
|
635
|
+
} : null;
|
|
651
636
|
return JSON.stringify(data);
|
|
652
637
|
}
|
|
653
638
|
|
|
654
639
|
/**
|
|
655
640
|
* Restore a Saico instance from serialized data.
|
|
641
|
+
* Supports both raw msgs (from serialize/Redis) and compressed
|
|
642
|
+
* chat_history (from closeSession/Store).
|
|
656
643
|
* @param {string|Object} data - Serialized data (JSON string or object)
|
|
657
644
|
* @param {Object} opt - Options (functions, store, states, etc.)
|
|
658
645
|
* @returns {Saico}
|
|
@@ -667,38 +654,54 @@ class Saico {
|
|
|
667
654
|
userData: parsed.userData,
|
|
668
655
|
sessionConfig: parsed.sessionConfig,
|
|
669
656
|
isolate: parsed.isolate,
|
|
670
|
-
functions: opt.functions || parsed.
|
|
657
|
+
functions: opt.functions || parsed.context?.functions,
|
|
671
658
|
store: opt.store,
|
|
672
659
|
redis: false, // No Redis proxy during deserialization
|
|
673
660
|
});
|
|
674
661
|
|
|
675
662
|
instance.tm_create = parsed.tm_create || instance.tm_create;
|
|
676
663
|
|
|
677
|
-
// Activate with restored
|
|
678
|
-
if (parsed.
|
|
664
|
+
// Activate with restored state if taskId exists
|
|
665
|
+
if (parsed.taskId) {
|
|
666
|
+
const ctx = parsed.context;
|
|
679
667
|
instance.activate({
|
|
680
|
-
createQ: !!
|
|
681
|
-
taskId: parsed.
|
|
682
|
-
tag:
|
|
683
|
-
chat_history:
|
|
684
|
-
functions: opt.functions ||
|
|
668
|
+
createQ: !!ctx,
|
|
669
|
+
taskId: parsed.taskId,
|
|
670
|
+
tag: ctx?.tag,
|
|
671
|
+
chat_history: ctx?.chat_history,
|
|
672
|
+
functions: opt.functions || ctx?.functions,
|
|
673
|
+
tool_digest: ctx?.tool_digest,
|
|
685
674
|
states: opt.states || [],
|
|
686
675
|
...opt,
|
|
687
676
|
});
|
|
688
677
|
|
|
689
|
-
// Restore
|
|
690
|
-
if (
|
|
691
|
-
instance.context._msgs =
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
// Restore tool_digest
|
|
695
|
-
if (Array.isArray(parsed.task.context?.tool_digest) && instance.context) {
|
|
696
|
-
instance.context.tool_digest = parsed.task.context.tool_digest;
|
|
678
|
+
// Restore raw msgs (from serialize/Redis) — takes priority over chat_history
|
|
679
|
+
if (ctx?.msgs && instance.context) {
|
|
680
|
+
instance.context._msgs = ctx.msgs;
|
|
697
681
|
}
|
|
698
682
|
}
|
|
699
683
|
|
|
700
684
|
return instance;
|
|
701
685
|
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Load a Saico instance from Store by id.
|
|
689
|
+
* @param {string} id - The Saico instance id
|
|
690
|
+
* @param {Object} opt - Options (store, functions, states, etc.)
|
|
691
|
+
* @returns {Promise<Saico|null>}
|
|
692
|
+
*/
|
|
693
|
+
static async rehydrate(id, opt = {}) {
|
|
694
|
+
const store = opt.store || Store.instance;
|
|
695
|
+
if (!store)
|
|
696
|
+
throw new Error('No store available for rehydrate');
|
|
697
|
+
const data = await store.load(id);
|
|
698
|
+
if (!data) return null;
|
|
699
|
+
const instance = Saico.deserialize(data, opt);
|
|
700
|
+
// Decompress chat_history into _msgs if present
|
|
701
|
+
if (instance.context)
|
|
702
|
+
await instance.context.initHistory();
|
|
703
|
+
return instance;
|
|
704
|
+
}
|
|
702
705
|
}
|
|
703
706
|
|
|
704
707
|
// [BACKEND] explanation text appended to context prompts
|