saico 2.8.0 → 2.9.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 +48 -33
- package/index.js +13 -12
- package/itask.js +3 -0
- package/msgs.js +38 -182
- package/package.json +1 -1
- package/redis.js +7 -5
- package/saico.js +210 -161
- package/store.js +1 -126
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Saico - Hierarchical AI Conversation Orchestrator
|
|
2
2
|
|
|
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
|
|
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 message queue, 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
8
|
- **Token-aware summarization** — Automatic summarization when message history approaches token limits
|
|
9
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)
|
|
10
|
+
- **Pluggable storage** — Optional Redis persistence (auto-save via proxy), library-level backend registration (`Saico.registerBackend`), and pluggable DB backends (DynamoDB adapter included)
|
|
11
11
|
- **Isolation boundaries** — `opt.isolate` stops ancestor aggregation at any node in the tree
|
|
12
12
|
- **Serialization** — Full state save/restore for long-running agents
|
|
13
13
|
|
|
@@ -54,7 +54,7 @@ agent.activate({ createQ: true });
|
|
|
54
54
|
// Backend message (prefixed with [BACKEND] automatically)
|
|
55
55
|
const reply = await agent.sendMessage('What is the weather in Tokyo?');
|
|
56
56
|
|
|
57
|
-
// User-facing chat message (routed to deepest active
|
|
57
|
+
// User-facing chat message (routed to deepest active msgs Q)
|
|
58
58
|
const chatReply = await agent.recvChatMessage('Hello!');
|
|
59
59
|
```
|
|
60
60
|
|
|
@@ -83,7 +83,7 @@ agent.activate();
|
|
|
83
83
|
await agent.sendMessage('Do something');
|
|
84
84
|
await agent.recvChatMessage('User says hello');
|
|
85
85
|
|
|
86
|
-
// 4. Deactivate — bubbles cleaned messages to parent, closes
|
|
86
|
+
// 4. Deactivate — bubbles cleaned messages to parent, closes msgs Q
|
|
87
87
|
await agent.deactivate();
|
|
88
88
|
```
|
|
89
89
|
|
|
@@ -124,7 +124,7 @@ Root Saico (prompt: "You are a manager")
|
|
|
124
124
|
Functions aggregated from all levels.
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
-
- **`sendMessage(content, functions, opts)`** — Sends a backend message (auto-prefixed `[BACKEND]`). Uses the current or nearest ancestor
|
|
127
|
+
- **`sendMessage(content, functions, opts)`** — Sends a backend message (auto-prefixed `[BACKEND]`). Uses the current or nearest ancestor msgs Q.
|
|
128
128
|
- **`recvChatMessage(content, opts)`** — Routes a user chat message DOWN to the deepest descendant with a message queue.
|
|
129
129
|
|
|
130
130
|
### Isolation
|
|
@@ -151,12 +151,12 @@ class OrderAgent extends Saico {
|
|
|
151
151
|
}
|
|
152
152
|
```
|
|
153
153
|
|
|
154
|
-
When a Saico's
|
|
154
|
+
When a Saico's msgs Q is not the deepest active one, its last 5 user/assistant messages are also included in the state summary automatically.
|
|
155
155
|
|
|
156
156
|
### Spawning Child Saico Instances
|
|
157
157
|
|
|
158
158
|
```js
|
|
159
|
-
// Child with its own
|
|
159
|
+
// Child with its own msgs Q (auto-activated by spawn)
|
|
160
160
|
const child = new Saico({
|
|
161
161
|
name: 'subtask',
|
|
162
162
|
prompt: 'Handle this specific sub-task',
|
|
@@ -166,7 +166,7 @@ const child = new Saico({
|
|
|
166
166
|
agent.spawn(child);
|
|
167
167
|
await child.sendMessage('Working on subtask...');
|
|
168
168
|
|
|
169
|
-
// Child without
|
|
169
|
+
// Child without msgs Q (uses parent's via findMsgs())
|
|
170
170
|
const simple = new Saico({ name: 'simple' });
|
|
171
171
|
agent.spawn(simple);
|
|
172
172
|
await simple.sendMessage('Quick operation');
|
|
@@ -210,7 +210,8 @@ new Saico({
|
|
|
210
210
|
// Storage
|
|
211
211
|
redis: true, // Set false to skip Redis proxy
|
|
212
212
|
key: 'custom-redis-key',
|
|
213
|
-
|
|
213
|
+
store: 'my-table', // Table name for instance persistence (closeSession/rehydrate)
|
|
214
|
+
dynamodb: { // DynamoDB config (creates instance-level adapter)
|
|
214
215
|
region: 'us-east-1',
|
|
215
216
|
credentials: { accessKeyId: '...', secretAccessKey: '...' },
|
|
216
217
|
},
|
|
@@ -257,15 +258,15 @@ agent.getSessionInfo();
|
|
|
257
258
|
// userData, uptime
|
|
258
259
|
// }
|
|
259
260
|
|
|
260
|
-
await agent.closeSession(); //
|
|
261
|
+
await agent.closeSession(); // prepareForStorage + save to registered backend, cancels task
|
|
261
262
|
|
|
262
|
-
// Restore from
|
|
263
|
-
const restored = await Saico.rehydrate(agent.
|
|
263
|
+
// Restore from registered backend
|
|
264
|
+
const restored = await Saico.rehydrate(agent.id, { store: 'sessions' });
|
|
264
265
|
```
|
|
265
266
|
|
|
266
267
|
## Database Access
|
|
267
268
|
|
|
268
|
-
Saico provides backend-agnostic DB methods. Configure via `opt.dynamodb` (auto-creates
|
|
269
|
+
Saico provides backend-agnostic DB methods. Configure via `Saico.registerBackend('dynamodb', config)` (library-level), `opt.dynamodb` (instance-level auto-creates adapter), or `opt.db` (any adapter). Table name is required on every call. Child Saico instances without their own DB inherit the parent's adapter automatically via `_getDb()`, which also falls back to the registered backend.
|
|
269
270
|
|
|
270
271
|
```js
|
|
271
272
|
// CRUD — table name required on every call
|
|
@@ -300,28 +301,42 @@ class MyAgent extends Saico {
|
|
|
300
301
|
|
|
301
302
|
## Serialization
|
|
302
303
|
|
|
304
|
+
Both `serialize()` and `Saico.deserialize()` are async. `serialize()` calls `prepareForStorage()` first (strips `_` props, skips functions/states, compresses msgs) then `JSON.stringify`s the result.
|
|
305
|
+
|
|
303
306
|
```js
|
|
304
|
-
//
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
+
// prepareForStorage — clean snapshot
|
|
308
|
+
const data = await agent.prepareForStorage();
|
|
309
|
+
|
|
310
|
+
// serialize/deserialize
|
|
311
|
+
const json = await agent.serialize();
|
|
312
|
+
const restored = await Saico.deserialize(json);
|
|
307
313
|
|
|
308
|
-
// Durable persistence (
|
|
314
|
+
// Durable persistence (uses registered backend + opt.store table name)
|
|
309
315
|
await agent.closeSession();
|
|
310
|
-
const restored2 = await Saico.rehydrate(agent.
|
|
316
|
+
const restored2 = await Saico.rehydrate(agent.id, { store: 'sessions' });
|
|
311
317
|
```
|
|
312
318
|
|
|
313
|
-
`
|
|
314
|
-
|
|
315
|
-
## Redis Persistence
|
|
319
|
+
`prepareForStorage()` automatically picks up all non-underscore properties (id, name, prompt, userData, sessionConfig, tm_create, isolate, etc.) and produces compressed chat_history for the msgs Q.
|
|
316
320
|
|
|
317
|
-
|
|
321
|
+
## Initialization
|
|
318
322
|
|
|
319
323
|
```js
|
|
320
|
-
const { init } = require('saico');
|
|
324
|
+
const { Saico, init } = require('saico');
|
|
325
|
+
|
|
326
|
+
// Initialize Redis (default: enabled) and register DynamoDB backend
|
|
327
|
+
await init({
|
|
328
|
+
dynamodb: { region: 'us-east-1', credentials: { accessKeyId: 'AK', secretAccessKey: 'SK' } },
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Or register backend directly
|
|
332
|
+
Saico.registerBackend('dynamodb', { region: 'us-east-1', credentials: { ... } });
|
|
333
|
+
```
|
|
321
334
|
|
|
322
|
-
|
|
323
|
-
|
|
335
|
+
## Redis Persistence
|
|
336
|
+
|
|
337
|
+
When Redis is initialized (default: enabled via `init()`), Saico instances are automatically wrapped in an observable proxy. Any property change triggers a debounced save to Redis.
|
|
324
338
|
|
|
339
|
+
```js
|
|
325
340
|
const agent = new Saico({ name: 'persistent-agent' });
|
|
326
341
|
agent.someProperty = 'value'; // Auto-saved to Redis
|
|
327
342
|
```
|
|
@@ -330,7 +345,7 @@ Properties prefixed with `_` are internal and not persisted.
|
|
|
330
345
|
|
|
331
346
|
## Tool Implementation (TOOL_ methods)
|
|
332
347
|
|
|
333
|
-
Define tool implementations as `TOOL_`-prefixed methods on your Saico subclass. When the LLM returns a tool call,
|
|
348
|
+
Define tool implementations as `TOOL_`-prefixed methods on your Saico subclass. When the LLM returns a tool call, Saico automatically searches the task hierarchy (current → up parents → down children) to find and invoke the matching method with parsed arguments.
|
|
334
349
|
|
|
335
350
|
```js
|
|
336
351
|
class MyAgent extends Saico {
|
|
@@ -359,13 +374,13 @@ Return a string or `{ content: string, functions?: [] }`.
|
|
|
359
374
|
|
|
360
375
|
## Low-Level API
|
|
361
376
|
|
|
362
|
-
For cases where you need a standalone
|
|
377
|
+
For cases where you need a standalone message queue without the Saico master class:
|
|
363
378
|
|
|
364
379
|
```js
|
|
365
|
-
const {
|
|
380
|
+
const { createMsgs } = require('saico');
|
|
366
381
|
|
|
367
|
-
// Standalone
|
|
368
|
-
const ctx =
|
|
382
|
+
// Standalone message queue
|
|
383
|
+
const ctx = createMsgs('System prompt', { tag: 'my-tag', token_limit: 4000 });
|
|
369
384
|
const reply = await ctx.sendMessage('user', 'Hello', functions);
|
|
370
385
|
```
|
|
371
386
|
|
|
@@ -374,11 +389,11 @@ const reply = await ctx.sendMessage('user', 'Hello', functions);
|
|
|
374
389
|
```
|
|
375
390
|
saico/
|
|
376
391
|
+-- index.js # Thin barrel file, exports all components
|
|
377
|
-
+-- saico.js # Saico master class — owns
|
|
392
|
+
+-- saico.js # Saico master class — owns msgs Q, spawn, DB, orchestration
|
|
378
393
|
+-- itask.js # Pure task runner — hierarchy, states, cancellation, promises
|
|
379
394
|
+-- msgs.js # Conversation context (message queue, tool calls, summarization)
|
|
380
395
|
+-- dynamo.js # DynamoDB storage adapter
|
|
381
|
-
+-- store.js #
|
|
396
|
+
+-- store.js # Minimal storage shell (Redis helper + ID generation)
|
|
382
397
|
+-- openai.js # OpenAI API wrapper with retry logic
|
|
383
398
|
+-- redis.js # Redis persistence with observable proxy
|
|
384
399
|
+-- util.js # Utilities (token counting, logging)
|
|
@@ -390,7 +405,7 @@ saico/
|
|
|
390
405
|
npm test
|
|
391
406
|
```
|
|
392
407
|
|
|
393
|
-
|
|
408
|
+
300 tests covering Saico lifecycle, msgs Q ownership, spawn/spawnAndRun, task hierarchy, message handling, tool calls, DB adapters, async serialization, prepareForStorage, backend registration, persistence (closeSession/rehydrate via registered backend), storage integration, and full hierarchy flows.
|
|
394
409
|
|
|
395
410
|
## Requirements
|
|
396
411
|
|
package/index.js
CHANGED
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const Itask = require('./itask.js');
|
|
4
|
-
const {
|
|
5
|
-
const { Store
|
|
4
|
+
const { Msgs, createMsgs } = require('./msgs.js');
|
|
5
|
+
const { Store } = require('./store.js');
|
|
6
6
|
const { Saico } = require('./saico.js');
|
|
7
7
|
const { DynamoDBAdapter } = require('./dynamo.js');
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Initialize Saico with storage configuration.
|
|
11
|
-
*
|
|
11
|
+
* Registers the backend and optionally initializes Redis.
|
|
12
12
|
*
|
|
13
13
|
* @param {Object} config - Configuration options
|
|
14
|
-
* @param {boolean} config.redis -
|
|
15
|
-
* @param {Object} config.dynamodb - DynamoDB
|
|
14
|
+
* @param {boolean} [config.redis=true] - Set false to skip Redis init
|
|
15
|
+
* @param {Object} [config.dynamodb] - DynamoDB config { region, credentials, client }
|
|
16
16
|
* @returns {Store} The initialized Store instance
|
|
17
17
|
*/
|
|
18
18
|
async function init(config = {}) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (config.redis) {
|
|
19
|
+
if (config.redis !== false) {
|
|
22
20
|
const redis = require('./redis.js');
|
|
23
21
|
await redis.init();
|
|
24
|
-
store.setRedis(redis.rclient);
|
|
25
22
|
}
|
|
26
23
|
|
|
24
|
+
if (config.dynamodb)
|
|
25
|
+
Saico.registerBackend('dynamodb', config.dynamodb);
|
|
26
|
+
|
|
27
|
+
// Legacy: still init Store shell
|
|
28
|
+
const store = Store.init(config);
|
|
27
29
|
return store;
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -34,15 +36,14 @@ module.exports = {
|
|
|
34
36
|
|
|
35
37
|
// Core classes
|
|
36
38
|
Itask,
|
|
37
|
-
|
|
39
|
+
Msgs,
|
|
38
40
|
Store,
|
|
39
|
-
DynamoBackend,
|
|
40
41
|
|
|
41
42
|
// Initialization
|
|
42
43
|
init,
|
|
43
44
|
|
|
44
45
|
// Factory
|
|
45
|
-
|
|
46
|
+
createMsgs,
|
|
46
47
|
|
|
47
48
|
// Utilities (re-export from util.js)
|
|
48
49
|
util: require('./util.js'),
|
package/itask.js
CHANGED
|
@@ -534,6 +534,9 @@ Itask.prototype.continue = function(ret){
|
|
|
534
534
|
return this;
|
|
535
535
|
};
|
|
536
536
|
|
|
537
|
+
/* ---------- serialization ---------- */
|
|
538
|
+
Itask.prototype.serialize = function(){ return JSON.stringify({}); };
|
|
539
|
+
|
|
537
540
|
/* ---------- introspection / ps ---------- */
|
|
538
541
|
Itask.prototype.is_running = function(){ return this.running && !this._completed; };
|
|
539
542
|
Itask.prototype.is_completed = function(){ return this._completed; };
|
package/msgs.js
CHANGED
|
@@ -8,23 +8,17 @@ const { _log, _lerr, _ldbg } = util;
|
|
|
8
8
|
const debug = 0;
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* Key differences from the old Messages class:
|
|
14
|
-
* - Uses task hierarchy instead of parent/child messages
|
|
15
|
-
* - task reference replaces parent reference
|
|
16
|
-
* - getMsgContext() traverses task hierarchy
|
|
17
|
-
* - _createMsgQ() aggregates from task ancestors
|
|
11
|
+
* Msgs - Pure message queue with tool call handling, summarization, and LLM communication.
|
|
12
|
+
* Saico sets callback hooks after construction to wire in hierarchy access.
|
|
18
13
|
*/
|
|
19
|
-
class
|
|
20
|
-
constructor(prompt,
|
|
14
|
+
class Msgs {
|
|
15
|
+
constructor(prompt, config = {}) {
|
|
21
16
|
this.prompt = prompt;
|
|
22
|
-
this.task = task; // Reference to owning Itask (replaces parent)
|
|
23
17
|
this.tag = config.tag || crypto.randomBytes(4).toString('hex');
|
|
24
18
|
this.token_limit = config.token_limit || 1000000000;
|
|
25
19
|
this.lower_limit = this.token_limit * 0.85;
|
|
26
20
|
this.upper_limit = this.token_limit * 0.98;
|
|
27
|
-
this.functions = config.functions ||
|
|
21
|
+
this.functions = config.functions || null;
|
|
28
22
|
|
|
29
23
|
// Recursive depth and repetition control
|
|
30
24
|
this.max_depth = config.max_depth || 5;
|
|
@@ -50,18 +44,15 @@ class Context {
|
|
|
50
44
|
// Tool digest — persistent history of tool calls that mutated task state
|
|
51
45
|
this.tool_digest = config.tool_digest || [];
|
|
52
46
|
|
|
47
|
+
// Callback hooks — set by Saico after construction
|
|
48
|
+
this._findToolImpl = null; // (toolName) => { saico, methodName } | null
|
|
49
|
+
this._getSnapshot = null; // () => serializable snapshot for dirty detection
|
|
50
|
+
|
|
53
51
|
// Initialize messages: explicit msgs take priority over chat_history
|
|
54
52
|
this._chat_history = config.chat_history || null;
|
|
55
53
|
(config.msgs || []).forEach(m => this.push(m));
|
|
56
54
|
|
|
57
|
-
_log('created
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Set the task reference (used when context is created separately)
|
|
61
|
-
setTask(task) {
|
|
62
|
-
this.task = task;
|
|
63
|
-
if (!this.functions)
|
|
64
|
-
this.functions = task?.functions;
|
|
55
|
+
_log('created Msgs for tag', this.tag);
|
|
65
56
|
}
|
|
66
57
|
|
|
67
58
|
/**
|
|
@@ -135,32 +126,6 @@ class Context {
|
|
|
135
126
|
this.tool_digest = this.tool_digest.slice(-this.TOOL_DIGEST_LIMIT);
|
|
136
127
|
}
|
|
137
128
|
|
|
138
|
-
// Get the parent context by traversing task hierarchy (via Saico)
|
|
139
|
-
getParentContext() {
|
|
140
|
-
if (!this.task || !this.task.parent)
|
|
141
|
-
return null;
|
|
142
|
-
let task = this.task.parent;
|
|
143
|
-
while (task) {
|
|
144
|
-
if (task._saico?.context) return task._saico.context;
|
|
145
|
-
task = task.parent;
|
|
146
|
-
}
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Get all ancestor contexts via task hierarchy (via Saico)
|
|
151
|
-
getAncestorContexts() {
|
|
152
|
-
if (!this.task)
|
|
153
|
-
return [];
|
|
154
|
-
const contexts = [];
|
|
155
|
-
let task = this.task.parent;
|
|
156
|
-
while (task) {
|
|
157
|
-
if (task._saico?.context)
|
|
158
|
-
contexts.unshift(task._saico.context);
|
|
159
|
-
task = task.parent;
|
|
160
|
-
}
|
|
161
|
-
return contexts;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
129
|
_hasPendingToolCalls() {
|
|
165
130
|
const toolCallMsgs = this._msgs.filter(m => m.msg.tool_calls);
|
|
166
131
|
|
|
@@ -334,16 +299,15 @@ class Context {
|
|
|
334
299
|
};
|
|
335
300
|
} else {
|
|
336
301
|
this._trackActiveToolCall(call);
|
|
337
|
-
const _snap = this.
|
|
338
|
-
? JSON.stringify(this.
|
|
302
|
+
const _snap = this._getSnapshot
|
|
303
|
+
? JSON.stringify(this._getSnapshot()) : null;
|
|
339
304
|
|
|
340
305
|
try {
|
|
341
306
|
const correspondingDeferred = deferredGroup.find(d => d.call.id === call.id);
|
|
342
307
|
const timeout = correspondingDeferred?.originalMessage.opts.timeout;
|
|
343
308
|
|
|
344
309
|
result = await this._executeToolCallWithTimeout(call, timeout);
|
|
345
|
-
if (_snap !== null &&
|
|
346
|
-
_snap !== JSON.stringify(this._snapshotPublicProps(this.task)))
|
|
310
|
+
if (_snap !== null && _snap !== JSON.stringify(this._getSnapshot()))
|
|
347
311
|
this._appendToolDigest(call.function.name, result?.content || '');
|
|
348
312
|
} finally {
|
|
349
313
|
this._completeActiveToolCall(call);
|
|
@@ -429,28 +393,18 @@ class Context {
|
|
|
429
393
|
return this._msgs.length;
|
|
430
394
|
}
|
|
431
395
|
|
|
432
|
-
serialize() {
|
|
396
|
+
async serialize() {
|
|
397
|
+
const { chat_history, tool_digest } = await this.prepareForStorage();
|
|
398
|
+
return JSON.stringify({
|
|
399
|
+
tag: this.tag,
|
|
400
|
+
chat_history,
|
|
401
|
+
tool_digest,
|
|
402
|
+
functions: this.functions,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
433
405
|
|
|
434
406
|
getSummaries() { return this._msgs.filter(m => m.opts.summary); }
|
|
435
407
|
|
|
436
|
-
// Get functions aggregated from this context and all ancestor contexts
|
|
437
|
-
getFunctions() {
|
|
438
|
-
const allFunctions = [];
|
|
439
|
-
|
|
440
|
-
// Get functions from ancestor contexts via task hierarchy
|
|
441
|
-
const ancestorContexts = this.getAncestorContexts();
|
|
442
|
-
for (const ctx of ancestorContexts) {
|
|
443
|
-
if (ctx.functions && Array.isArray(ctx.functions))
|
|
444
|
-
allFunctions.push(...ctx.functions);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// Add our own functions
|
|
448
|
-
if (this.functions && Array.isArray(this.functions))
|
|
449
|
-
allFunctions.push(...this.functions);
|
|
450
|
-
|
|
451
|
-
return allFunctions.length > 0 ? allFunctions : null;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
408
|
async summarizeMessages() {
|
|
455
409
|
const tokens = util.countTokens(this.__msgs);
|
|
456
410
|
if (tokens < this.lower_limit)
|
|
@@ -459,7 +413,7 @@ class Context {
|
|
|
459
413
|
}
|
|
460
414
|
|
|
461
415
|
async close() {
|
|
462
|
-
_log('Closing
|
|
416
|
+
_log('Closing Msgs tag', this.tag);
|
|
463
417
|
|
|
464
418
|
if (this._sequential_mode && this._processing_sequential) {
|
|
465
419
|
_ldbg('Sequential mode: waiting for current message to complete before closing tag', this.tag);
|
|
@@ -472,22 +426,8 @@ class Context {
|
|
|
472
426
|
}
|
|
473
427
|
}
|
|
474
428
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
if (parentCtx && this._waitingQueue.length > 0) {
|
|
478
|
-
_log('Moving', this._waitingQueue.length, 'waiting messages to parent context');
|
|
479
|
-
parentCtx._waitingQueue.push(...this._waitingQueue);
|
|
480
|
-
this._waitingQueue = [];
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
if (parentCtx && this._sequential_queue.length > 0) {
|
|
484
|
-
_log('Moving', this._sequential_queue.length, 'sequential queue messages to parent context');
|
|
485
|
-
parentCtx._sequential_queue.push(...this._sequential_queue);
|
|
486
|
-
this._sequential_queue = [];
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
await this._summarizeContext(true, parentCtx);
|
|
490
|
-
_log('Finished closing Context tag', this.tag);
|
|
429
|
+
await this._summarizeContext(true);
|
|
430
|
+
_log('Finished closing Msgs tag', this.tag);
|
|
491
431
|
}
|
|
492
432
|
|
|
493
433
|
|
|
@@ -591,38 +531,6 @@ class Context {
|
|
|
591
531
|
return summary;
|
|
592
532
|
}
|
|
593
533
|
|
|
594
|
-
// Get message context - walks up task hierarchy to collect prompts and summaries
|
|
595
|
-
getMsgContext(add_tag) {
|
|
596
|
-
const msgs = [];
|
|
597
|
-
|
|
598
|
-
// Get context from ancestor tasks via task hierarchy
|
|
599
|
-
const ancestorContexts = this.getAncestorContexts();
|
|
600
|
-
for (const ctx of ancestorContexts) {
|
|
601
|
-
if (ctx.prompt)
|
|
602
|
-
msgs.push({role: 'system', content: ctx.prompt});
|
|
603
|
-
// Add summaries from ancestor contexts
|
|
604
|
-
const summaries = ctx._msgs.filter(m => m.opts.summary || m.msg.role === 'system').map(m => {
|
|
605
|
-
if (add_tag)
|
|
606
|
-
m.msg.tag = ctx.tag;
|
|
607
|
-
return m.msg;
|
|
608
|
-
});
|
|
609
|
-
msgs.push(...summaries);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
// Add this context's prompt
|
|
613
|
-
if (this.prompt)
|
|
614
|
-
msgs.push({role: 'system', content: this.prompt});
|
|
615
|
-
|
|
616
|
-
// Add this context's summaries
|
|
617
|
-
const mySummaries = this._msgs.filter(m => m.opts.summary || m.msg.role === 'system').map(m => {
|
|
618
|
-
if (add_tag)
|
|
619
|
-
m.msg.tag = this.tag;
|
|
620
|
-
return m.msg;
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
return msgs.concat(mySummaries);
|
|
624
|
-
}
|
|
625
|
-
|
|
626
534
|
_createMsgObj(role, content, functions, opts) {
|
|
627
535
|
const name = opts?.name;
|
|
628
536
|
const tool_call_id = opts?.tool_call_id;
|
|
@@ -952,10 +860,10 @@ class Context {
|
|
|
952
860
|
? [...o.opts._aggregatedFunctions, ...messageFuncs]
|
|
953
861
|
: null;
|
|
954
862
|
} else {
|
|
955
|
-
const
|
|
863
|
+
const ownFuncs = this.functions || [];
|
|
956
864
|
const messageFuncs = o.functions || [];
|
|
957
|
-
funcs = [...
|
|
958
|
-
? [...
|
|
865
|
+
funcs = [...ownFuncs, ...messageFuncs].length > 0
|
|
866
|
+
? [...ownFuncs, ...messageFuncs]
|
|
959
867
|
: null;
|
|
960
868
|
}
|
|
961
869
|
|
|
@@ -1024,15 +932,15 @@ class Context {
|
|
|
1024
932
|
|
|
1025
933
|
for (const { call, isDuplicate } of toolCallsWithResults) {
|
|
1026
934
|
if (!isDuplicate) {
|
|
1027
|
-
const _snap = this.
|
|
1028
|
-
? JSON.stringify(this.
|
|
935
|
+
const _snap = this._getSnapshot
|
|
936
|
+
? JSON.stringify(this._getSnapshot()) : null;
|
|
1029
937
|
try {
|
|
1030
938
|
const result = await this._executeToolCallWithTimeout(
|
|
1031
939
|
call, o.opts?.timeout);
|
|
1032
940
|
const item = toolCallsWithResults.find(item => item.call.id === call.id);
|
|
1033
941
|
if (item) item.result = result;
|
|
1034
942
|
if (_snap !== null &&
|
|
1035
|
-
_snap !== JSON.stringify(this.
|
|
943
|
+
_snap !== JSON.stringify(this._getSnapshot()))
|
|
1036
944
|
this._appendToolDigest(call.function.name, result?.content || '');
|
|
1037
945
|
} finally {
|
|
1038
946
|
this._completeActiveToolCall(call);
|
|
@@ -1088,39 +996,11 @@ class Context {
|
|
|
1088
996
|
}
|
|
1089
997
|
|
|
1090
998
|
/**
|
|
1091
|
-
*
|
|
1092
|
-
*
|
|
999
|
+
* Find a TOOL_<toolName> implementation. Delegates to _findToolImpl callback
|
|
1000
|
+
* set by Saico, which searches the hierarchy.
|
|
1093
1001
|
*/
|
|
1094
1002
|
_findToolImplementation(toolName) {
|
|
1095
|
-
|
|
1096
|
-
const check = (task) =>
|
|
1097
|
-
task?._saico && typeof task._saico[methodName] === 'function' ? task._saico : null;
|
|
1098
|
-
|
|
1099
|
-
// 1. Current task
|
|
1100
|
-
let found = check(this.task);
|
|
1101
|
-
if (found) return { saico: found, methodName };
|
|
1102
|
-
|
|
1103
|
-
// 2. Walk UP parent chain
|
|
1104
|
-
let t = this.task?.parent;
|
|
1105
|
-
while (t) {
|
|
1106
|
-
found = check(t);
|
|
1107
|
-
if (found) return { saico: found, methodName };
|
|
1108
|
-
t = t.parent;
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
// 3. Walk DOWN from this.task (BFS)
|
|
1112
|
-
if (this.task) {
|
|
1113
|
-
const queue = [...this.task.child];
|
|
1114
|
-
while (queue.length > 0) {
|
|
1115
|
-
const child = queue.shift();
|
|
1116
|
-
if (child._completed) continue;
|
|
1117
|
-
found = check(child);
|
|
1118
|
-
if (found) return { saico: found, methodName };
|
|
1119
|
-
if (child.child?.size > 0) queue.push(...child.child);
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
return null;
|
|
1003
|
+
return this._findToolImpl ? this._findToolImpl(toolName) : null;
|
|
1124
1004
|
}
|
|
1125
1005
|
|
|
1126
1006
|
async interpretAndApplyChanges(call) {
|
|
@@ -1169,35 +1049,11 @@ class Context {
|
|
|
1169
1049
|
return { content, functions };
|
|
1170
1050
|
}
|
|
1171
1051
|
|
|
1172
|
-
// Spawn child context (creates a child task with its own context)
|
|
1173
|
-
spawnChild(prompt, tag, config = {}) {
|
|
1174
|
-
if (!this.task) {
|
|
1175
|
-
// If no task, create a standalone context
|
|
1176
|
-
return createContext(prompt, null, { ...config, tag });
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
// Create a child task with its own context
|
|
1180
|
-
const Itask = require('./itask.js');
|
|
1181
|
-
const childTask = new Itask({
|
|
1182
|
-
name: tag || 'child-context',
|
|
1183
|
-
async: true,
|
|
1184
|
-
}, []);
|
|
1185
|
-
this.task.spawn(childTask);
|
|
1186
|
-
|
|
1187
|
-
const childContext = new Context(prompt, childTask, { ...config, tag });
|
|
1188
|
-
// Store context on Saico if present, otherwise just set on task reference
|
|
1189
|
-
if (childTask._saico) {
|
|
1190
|
-
childTask._saico.context = childContext;
|
|
1191
|
-
childTask._saico.context_id = childContext.tag;
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
return childContext;
|
|
1195
|
-
}
|
|
1196
1052
|
}
|
|
1197
1053
|
|
|
1198
|
-
// Factory function to create a
|
|
1199
|
-
function
|
|
1200
|
-
const instance = new
|
|
1054
|
+
// Factory function to create a Msgs instance with Proxy wrapper
|
|
1055
|
+
function createMsgs(prompt, config = {}) {
|
|
1056
|
+
const instance = new Msgs(prompt, config);
|
|
1201
1057
|
|
|
1202
1058
|
return new Proxy(instance, {
|
|
1203
1059
|
get(target, prop, receiver) {
|
|
@@ -1246,4 +1102,4 @@ function createContext(prompt, task, config = {}) {
|
|
|
1246
1102
|
});
|
|
1247
1103
|
}
|
|
1248
1104
|
|
|
1249
|
-
module.exports = {
|
|
1105
|
+
module.exports = { Msgs, createMsgs };
|
package/package.json
CHANGED
package/redis.js
CHANGED
|
@@ -39,11 +39,13 @@ function createObservableForRedis(key, obj) {
|
|
|
39
39
|
let lastSavedObject = null; // Cache for the last-saved sanitized object
|
|
40
40
|
let lastSavedTimestamp = null; // Timestamp of the last save to Redis
|
|
41
41
|
|
|
42
|
-
const saveToRedis = debounce(() => {
|
|
42
|
+
const saveToRedis = debounce(async () => {
|
|
43
43
|
const sanitizedObj = sanitizeObject(obj);
|
|
44
44
|
|
|
45
45
|
// Compare sanitized object with the last-saved object
|
|
46
|
-
|
|
46
|
+
const serializedNew = await serialize(sanitizedObj);
|
|
47
|
+
const serializedOld = await serialize(lastSavedObject);
|
|
48
|
+
if (serializedNew === serializedOld) {
|
|
47
49
|
logDebug("No changes detected, skipping save.");
|
|
48
50
|
return;
|
|
49
51
|
}
|
|
@@ -51,7 +53,7 @@ function createObservableForRedis(key, obj) {
|
|
|
51
53
|
lastSavedObject = sanitizedObj;
|
|
52
54
|
lastSavedTimestamp = Date.now(); // Update the last saved timestamp
|
|
53
55
|
sanitizedObj.lastSave = lastSavedTimestamp;
|
|
54
|
-
rclient.set(key,
|
|
56
|
+
await rclient.set(key, serializedNew);
|
|
55
57
|
logDebug("Saved to Redis:", key, `at ${lastSavedTimestamp}`);
|
|
56
58
|
}, 1000);
|
|
57
59
|
|
|
@@ -100,9 +102,9 @@ function createObservableForRedis(key, obj) {
|
|
|
100
102
|
},
|
|
101
103
|
};
|
|
102
104
|
|
|
103
|
-
function serialize(obj) {
|
|
105
|
+
async function serialize(obj) {
|
|
104
106
|
if (typeof obj == 'object' && typeof obj?.serialize == 'function')
|
|
105
|
-
return obj.serialize();
|
|
107
|
+
return await obj.serialize();
|
|
106
108
|
return JSON.stringify(obj);
|
|
107
109
|
}
|
|
108
110
|
|