saico 2.6.1 → 2.7.1
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 +50 -38
- package/index.js +2 -110
- package/itask.js +4 -207
- package/msgs.js +23 -9
- package/package.json +1 -2
- package/saico.js +181 -93
- package/context.js +0 -5
package/README.md
CHANGED
|
@@ -69,14 +69,15 @@ Saico separates construction from activation:
|
|
|
69
69
|
const agent = new Saico({
|
|
70
70
|
name: 'agent',
|
|
71
71
|
prompt: 'System prompt here',
|
|
72
|
+
createQ: true, // message Q created on activate()
|
|
72
73
|
dynamodb: { region: 'us-east-1', credentials: { accessKeyId: 'AK', secretAccessKey: 'SK' } },
|
|
73
74
|
});
|
|
74
75
|
|
|
75
76
|
// DB methods work before activation
|
|
76
77
|
const item = await agent.dbGetItem('id', '123');
|
|
77
78
|
|
|
78
|
-
// 2. Activate — creates internal task +
|
|
79
|
-
agent.activate(
|
|
79
|
+
// 2. Activate — creates internal task + message Q (from this.createQ)
|
|
80
|
+
agent.activate();
|
|
80
81
|
|
|
81
82
|
// 3. Use — send messages, spawn children
|
|
82
83
|
await agent.sendMessage('Do something');
|
|
@@ -86,6 +87,23 @@ await agent.recvChatMessage('User says hello');
|
|
|
86
87
|
await agent.deactivate();
|
|
87
88
|
```
|
|
88
89
|
|
|
90
|
+
Subclasses can also define `this.states` (task functions) in the constructor — `activate()` picks them up automatically:
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
class MyAgent extends Saico {
|
|
94
|
+
constructor() {
|
|
95
|
+
super({ name: 'agent', prompt: 'You are helpful', createQ: true });
|
|
96
|
+
this.states = [
|
|
97
|
+
async function main() {
|
|
98
|
+
return await this.sendMessage('Starting...');
|
|
99
|
+
}
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const agent = new MyAgent();
|
|
104
|
+
agent.activate(); // no params needed — uses this.createQ and this.states
|
|
105
|
+
```
|
|
106
|
+
|
|
89
107
|
### Message Orchestration
|
|
90
108
|
|
|
91
109
|
When `sendMessage()` or `recvChatMessage()` is called, Saico walks the parent chain to build the full LLM payload:
|
|
@@ -135,29 +153,32 @@ class OrderAgent extends Saico {
|
|
|
135
153
|
|
|
136
154
|
When a Saico's context is not the deepest active one, its last 5 user/assistant messages are also included in the state summary automatically.
|
|
137
155
|
|
|
138
|
-
### Spawning Child
|
|
156
|
+
### Spawning Child Saico Instances
|
|
139
157
|
|
|
140
158
|
```js
|
|
141
159
|
// Child with its own conversation context
|
|
142
|
-
const child =
|
|
160
|
+
const child = new Saico({
|
|
143
161
|
name: 'subtask',
|
|
144
162
|
prompt: 'Handle this specific sub-task',
|
|
145
|
-
functions: [/* child-specific tools */]
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
163
|
+
functions: [/* child-specific tools */],
|
|
164
|
+
});
|
|
165
|
+
child.activate({ createQ: true });
|
|
166
|
+
agent.spawn(child);
|
|
167
|
+
await child.sendMessage('Working on subtask...');
|
|
168
|
+
|
|
169
|
+
// Child without context (uses parent's via findContext())
|
|
170
|
+
const simple = new Saico({ name: 'simple' });
|
|
171
|
+
simple.activate();
|
|
172
|
+
agent.spawn(simple);
|
|
173
|
+
await simple.sendMessage('Quick operation');
|
|
174
|
+
|
|
175
|
+
// spawnAndRun: spawn + schedule child task to run on nextTick
|
|
176
|
+
const runner = new Saico({ name: 'runner' });
|
|
177
|
+
runner.activate({ states: [async function() { return await this.sendMessage('Go'); }] });
|
|
178
|
+
agent.spawnAndRun(runner);
|
|
158
179
|
```
|
|
159
180
|
|
|
160
|
-
|
|
181
|
+
Both parent and child must be activated before calling `spawn()` or `spawnAndRun()`.
|
|
161
182
|
|
|
162
183
|
### Deactivation and Message Bubbling
|
|
163
184
|
|
|
@@ -174,6 +195,7 @@ new Saico({
|
|
|
174
195
|
// AI config
|
|
175
196
|
prompt: 'System prompt',
|
|
176
197
|
functions: [], // OpenAI function definitions
|
|
198
|
+
createQ: false, // Create message Q on activate() (also settable as this.createQ)
|
|
177
199
|
|
|
178
200
|
// Behavior
|
|
179
201
|
isolate: false, // Stop ancestor aggregation
|
|
@@ -204,10 +226,9 @@ new Saico({
|
|
|
204
226
|
|
|
205
227
|
```js
|
|
206
228
|
agent.activate({
|
|
207
|
-
createQ: true, //
|
|
229
|
+
createQ: true, // Override this.createQ for this activation
|
|
208
230
|
prompt: 'Extra prompt', // Appended to class-level prompt
|
|
209
|
-
states: [], //
|
|
210
|
-
parent: parentTask, // Parent task to spawn under
|
|
231
|
+
states: [], // Override this.states for this activation
|
|
211
232
|
taskId: 'custom-id',
|
|
212
233
|
sequential_mode: true, // Process messages sequentially
|
|
213
234
|
|
|
@@ -336,21 +357,13 @@ Return a string or `{ content: string, functions?: [] }`.
|
|
|
336
357
|
|
|
337
358
|
## Low-Level API
|
|
338
359
|
|
|
339
|
-
For cases where you
|
|
360
|
+
For cases where you need a standalone context without the Saico master class:
|
|
340
361
|
|
|
341
362
|
```js
|
|
342
|
-
const {
|
|
343
|
-
|
|
344
|
-
// Create a task with context
|
|
345
|
-
const task = createTask({
|
|
346
|
-
name: 'my-task',
|
|
347
|
-
prompt: 'You are helpful',
|
|
348
|
-
functions: tools
|
|
349
|
-
});
|
|
350
|
-
const reply = await task.sendMessage('Hello');
|
|
363
|
+
const { createContext } = require('saico');
|
|
351
364
|
|
|
352
|
-
// Standalone context
|
|
353
|
-
const ctx =
|
|
365
|
+
// Standalone context
|
|
366
|
+
const ctx = createContext('System prompt', null, { tag: 'my-tag', token_limit: 4000 });
|
|
354
367
|
const reply = await ctx.sendMessage('user', 'Hello', functions);
|
|
355
368
|
```
|
|
356
369
|
|
|
@@ -358,11 +371,10 @@ const reply = await ctx.sendMessage('user', 'Hello', functions);
|
|
|
358
371
|
|
|
359
372
|
```
|
|
360
373
|
saico/
|
|
361
|
-
+-- index.js #
|
|
362
|
-
+-- saico.js # Saico master class
|
|
363
|
-
+-- itask.js #
|
|
374
|
+
+-- index.js # Thin barrel file, exports all components
|
|
375
|
+
+-- saico.js # Saico master class — owns context, spawn, DB, orchestration
|
|
376
|
+
+-- itask.js # Pure task runner — hierarchy, states, cancellation, promises
|
|
364
377
|
+-- msgs.js # Conversation context (message queue, tool calls, summarization)
|
|
365
|
-
+-- context.js # Backward-compat shim for msgs.js
|
|
366
378
|
+-- dynamo.js # DynamoDB storage adapter
|
|
367
379
|
+-- store.js # Storage abstraction (Redis + pluggable backends)
|
|
368
380
|
+-- openai.js # OpenAI API wrapper with retry logic
|
|
@@ -376,7 +388,7 @@ saico/
|
|
|
376
388
|
npm test
|
|
377
389
|
```
|
|
378
390
|
|
|
379
|
-
|
|
391
|
+
284 tests covering Saico lifecycle, context ownership, spawn/spawnAndRun, task hierarchy, message handling, tool calls, DB adapters, serialization, and integration flows.
|
|
380
392
|
|
|
381
393
|
## Requirements
|
|
382
394
|
|
package/index.js
CHANGED
|
@@ -1,32 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Saico Library - Hierarchical AI Conversation Orchestrator
|
|
5
|
-
*
|
|
6
|
-
* Combines task hierarchy (Itask) with conversation contexts (Context) to create
|
|
7
|
-
* a unified system for managing AI conversations with:
|
|
8
|
-
* - Task-based organizational structure
|
|
9
|
-
* - Optional conversation contexts attached to tasks
|
|
10
|
-
* - Hierarchical message aggregation with function collection
|
|
11
|
-
* - Full tool_calls support with depth control
|
|
12
|
-
* - Storage persistence (Redis cache + optional DB backend)
|
|
13
|
-
*
|
|
14
|
-
* Main Components:
|
|
15
|
-
* - Saico: Master class (external users extend this)
|
|
16
|
-
* - Itask: Base task class for all tasks (supports states, cancellation, promises)
|
|
17
|
-
* - Context: Conversation context with message handling and tool calls
|
|
18
|
-
* - Store: Storage abstraction layer (Redis + optional backends like DynamoDB)
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
3
|
const Itask = require('./itask.js');
|
|
22
4
|
const { Context, createContext } = require('./msgs.js');
|
|
23
5
|
const { Store, DynamoBackend } = require('./store.js');
|
|
24
6
|
const { Saico } = require('./saico.js');
|
|
25
7
|
const { DynamoDBAdapter } = require('./dynamo.js');
|
|
26
8
|
|
|
27
|
-
// Wire up Context class reference in Itask to avoid circular dependency
|
|
28
|
-
Itask.Context = Context;
|
|
29
|
-
|
|
30
9
|
/**
|
|
31
10
|
* Initialize Saico with storage configuration.
|
|
32
11
|
* Sets up the Store singleton and optionally initializes Redis.
|
|
@@ -48,89 +27,6 @@ async function init(config = {}) {
|
|
|
48
27
|
return store;
|
|
49
28
|
}
|
|
50
29
|
|
|
51
|
-
/**
|
|
52
|
-
* Create a new task with optional context.
|
|
53
|
-
*
|
|
54
|
-
* @param {Object|string} opt - Task options or name string
|
|
55
|
-
* @param {string} opt.name - Task name
|
|
56
|
-
* @param {string} opt.prompt - System prompt (if provided, creates a context)
|
|
57
|
-
* @param {Array} opt.functions - Available functions for AI
|
|
58
|
-
* @param {boolean} opt.cancel - Whether task is cancelable
|
|
59
|
-
* @param {Object} opt.bind - Bind context for state functions
|
|
60
|
-
* @param {Itask} opt.spawn_parent - Parent task to spawn under
|
|
61
|
-
* @param {boolean} opt.async - If true, don't auto-run
|
|
62
|
-
* @param {Object} opt.store - Store instance for persistence
|
|
63
|
-
* @param {Array} states - Array of state functions
|
|
64
|
-
* @returns {Itask} The created task
|
|
65
|
-
*/
|
|
66
|
-
function createTask(opt, states = []) {
|
|
67
|
-
if (typeof opt === 'string')
|
|
68
|
-
opt = { name: opt };
|
|
69
|
-
|
|
70
|
-
if (!opt.store)
|
|
71
|
-
opt.store = Store.instance;
|
|
72
|
-
|
|
73
|
-
const task = new Itask(opt, states);
|
|
74
|
-
|
|
75
|
-
// Auto-create context if prompt is provided
|
|
76
|
-
if (opt.prompt) {
|
|
77
|
-
const context = new Context(opt.prompt, task, {
|
|
78
|
-
tag: opt.tag || task.id,
|
|
79
|
-
token_limit: opt.token_limit,
|
|
80
|
-
max_depth: opt.max_depth,
|
|
81
|
-
max_tool_repetition: opt.max_tool_repetition,
|
|
82
|
-
functions: opt.functions,
|
|
83
|
-
sequential_mode: opt.sequential_mode
|
|
84
|
-
});
|
|
85
|
-
task.setContext(context);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return task;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Legacy createQ function for backward compatibility.
|
|
93
|
-
* Creates a standalone Context (not attached to a task).
|
|
94
|
-
*
|
|
95
|
-
* @param {string} prompt - System prompt
|
|
96
|
-
* @param {Context} parent - Parent context (legacy, will be converted to task-based)
|
|
97
|
-
* @param {string} tag - Context tag identifier
|
|
98
|
-
* @param {number} token_limit - Token limit for summarization
|
|
99
|
-
* @param {Array} msgs - Initial messages
|
|
100
|
-
* @param {Object} config - Additional configuration
|
|
101
|
-
* @returns {Context} Proxied Context instance
|
|
102
|
-
*/
|
|
103
|
-
function createQ(prompt, parent, tag, token_limit, msgs, config = {}) {
|
|
104
|
-
// For backward compatibility, if parent is a Context, get its task
|
|
105
|
-
let task = null;
|
|
106
|
-
if (parent && parent.task) {
|
|
107
|
-
task = parent.task;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const context = createContext(prompt, task, {
|
|
111
|
-
tag,
|
|
112
|
-
token_limit,
|
|
113
|
-
msgs,
|
|
114
|
-
...config
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
// If there's a parent context, set up the relationship via tasks
|
|
118
|
-
if (parent && parent.task) {
|
|
119
|
-
// Create a child task to hold this context
|
|
120
|
-
const childTask = new Itask({
|
|
121
|
-
name: tag || 'child-context',
|
|
122
|
-
async: true,
|
|
123
|
-
spawn_parent: parent.task,
|
|
124
|
-
store: Store.instance
|
|
125
|
-
}, []);
|
|
126
|
-
context.setTask(childTask);
|
|
127
|
-
childTask.setContext(context);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return context;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Export all components
|
|
134
30
|
module.exports = {
|
|
135
31
|
// Master class (external users extend this)
|
|
136
32
|
Saico,
|
|
@@ -145,13 +41,9 @@ module.exports = {
|
|
|
145
41
|
// Initialization
|
|
146
42
|
init,
|
|
147
43
|
|
|
148
|
-
// Factory
|
|
149
|
-
createTask,
|
|
44
|
+
// Factory
|
|
150
45
|
createContext,
|
|
151
46
|
|
|
152
|
-
// Legacy compatibility
|
|
153
|
-
createQ,
|
|
154
|
-
|
|
155
47
|
// Utilities (re-export from util.js)
|
|
156
48
|
util: require('./util.js'),
|
|
157
49
|
|
|
@@ -159,5 +51,5 @@ module.exports = {
|
|
|
159
51
|
openai: require('./openai.js'),
|
|
160
52
|
|
|
161
53
|
// Redis persistence (re-export)
|
|
162
|
-
redis: require('./redis.js')
|
|
54
|
+
redis: require('./redis.js'),
|
|
163
55
|
};
|
package/itask.js
CHANGED
|
@@ -14,13 +14,11 @@
|
|
|
14
14
|
|
|
15
15
|
'use strict';
|
|
16
16
|
|
|
17
|
-
const assert = require('assert');
|
|
18
17
|
const EventEmitter = require('events');
|
|
19
18
|
const crypto = require('crypto');
|
|
20
19
|
const util = require('./util.js');
|
|
21
|
-
const { Store } = require('./store.js');
|
|
22
20
|
|
|
23
|
-
const { _log, lerr , _ldbg
|
|
21
|
+
const { _log, lerr , _ldbg } = util;
|
|
24
22
|
|
|
25
23
|
/* ---------- utility ---------- */
|
|
26
24
|
function makeId(len = 12){
|
|
@@ -99,26 +97,9 @@ function Itask(opt, states){
|
|
|
99
97
|
this._cancel_state_idx = this.states.findIndex(s => s.cancel);
|
|
100
98
|
this._root_registered = false;
|
|
101
99
|
|
|
102
|
-
//
|
|
103
|
-
this
|
|
104
|
-
this.
|
|
105
|
-
|
|
106
|
-
// Storage persistence
|
|
107
|
-
this.context_id = opt.context_id || null;
|
|
108
|
-
this._store = opt.store || Store.instance || null;
|
|
109
|
-
|
|
110
|
-
// Store options for context creation (prompt, functions, etc.)
|
|
111
|
-
this.prompt = opt.prompt;
|
|
112
|
-
this.functions = opt.functions;
|
|
113
|
-
|
|
114
|
-
// register root if no explicit spawn_parent provided
|
|
115
|
-
// If opt.spawn_parent provided, spawn under it
|
|
116
|
-
if (opt.spawn_parent && opt.spawn_parent instanceof Itask){
|
|
117
|
-
opt.spawn_parent.spawn(this);
|
|
118
|
-
} else {
|
|
119
|
-
Itask.root.add(this);
|
|
120
|
-
this._root_registered = true;
|
|
121
|
-
}
|
|
100
|
+
// All tasks register as root; parent set via itask.spawn()
|
|
101
|
+
Itask.root.add(this);
|
|
102
|
+
this._root_registered = true;
|
|
122
103
|
|
|
123
104
|
// async option defers immediate run
|
|
124
105
|
if (!opt.async){
|
|
@@ -376,15 +357,6 @@ Itask.prototype.spawn = function spawn(child){
|
|
|
376
357
|
Itask.root.delete(child);
|
|
377
358
|
child._root_registered = false;
|
|
378
359
|
}
|
|
379
|
-
// Auto-wrap with redis observable for live state persistence
|
|
380
|
-
if (child.context_id) {
|
|
381
|
-
try {
|
|
382
|
-
const redis = require('./redis.js');
|
|
383
|
-
if (redis.rclient) {
|
|
384
|
-
redis.createObservableForRedis('saico:' + child.context_id, child);
|
|
385
|
-
}
|
|
386
|
-
} catch (e) { /* redis not available */ }
|
|
387
|
-
}
|
|
388
360
|
// ensure async-created children begin execution
|
|
389
361
|
if (!child.running && !child._completed){
|
|
390
362
|
process.nextTick(() => {
|
|
@@ -594,181 +566,6 @@ Itask.ps = function ps(){
|
|
|
594
566
|
return out || '<no roots>';
|
|
595
567
|
};
|
|
596
568
|
|
|
597
|
-
/* ---------- context management ---------- */
|
|
598
|
-
// [BACKEND] explanation text appended to context prompts
|
|
599
|
-
Itask.BACKEND_EXPLANATION = '\nNote: Messages prefixed with [BACKEND] are from the backend ' +
|
|
600
|
-
'server, not the user. They contain server instructions, data updates, or system context. ' +
|
|
601
|
-
'Treat them as authoritative system-level information.';
|
|
602
|
-
|
|
603
|
-
// Get the context for this task, optionally creating one if needed
|
|
604
|
-
Itask.prototype.getContext = function getContext(createIfMissing = false){
|
|
605
|
-
if (this.context)
|
|
606
|
-
return this.context;
|
|
607
|
-
if (createIfMissing && this.prompt){
|
|
608
|
-
// Lazy context creation - requires Context class to be set
|
|
609
|
-
if (Itask.Context){
|
|
610
|
-
const augmentedPrompt = this.prompt + Itask.BACKEND_EXPLANATION;
|
|
611
|
-
this.context = new Itask.Context(augmentedPrompt, this, this._contextConfig);
|
|
612
|
-
this.setContext(this.context);
|
|
613
|
-
return this.context;
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
return null;
|
|
617
|
-
};
|
|
618
|
-
|
|
619
|
-
// Set context for this task
|
|
620
|
-
Itask.prototype.setContext = function setContext(context){
|
|
621
|
-
this.context = context;
|
|
622
|
-
// Generate context_id if not already set
|
|
623
|
-
if (!this.context_id) {
|
|
624
|
-
if (this._store)
|
|
625
|
-
this.context_id = this._store.generateId();
|
|
626
|
-
else if (Store.instance)
|
|
627
|
-
this.context_id = Store.instance.generateId();
|
|
628
|
-
else
|
|
629
|
-
this.context_id = makeId(16);
|
|
630
|
-
}
|
|
631
|
-
if (context) {
|
|
632
|
-
context.tag = this.context_id;
|
|
633
|
-
if (typeof context.setTask === 'function')
|
|
634
|
-
context.setTask(this);
|
|
635
|
-
}
|
|
636
|
-
return this;
|
|
637
|
-
};
|
|
638
|
-
|
|
639
|
-
// Get all ancestor contexts (walking up the task hierarchy)
|
|
640
|
-
Itask.prototype.getAncestorContexts = function getAncestorContexts(){
|
|
641
|
-
const contexts = [];
|
|
642
|
-
let task = this;
|
|
643
|
-
while (task){
|
|
644
|
-
if (task.context)
|
|
645
|
-
contexts.unshift(task.context); // Add to front so ancestors come first
|
|
646
|
-
task = task.parent;
|
|
647
|
-
}
|
|
648
|
-
return contexts;
|
|
649
|
-
};
|
|
650
|
-
|
|
651
|
-
// Find the nearest context in the hierarchy (this task or ancestors)
|
|
652
|
-
Itask.prototype.findContext = function findContext(){
|
|
653
|
-
let task = this;
|
|
654
|
-
while (task){
|
|
655
|
-
if (task.context)
|
|
656
|
-
return task.context;
|
|
657
|
-
task = task.parent;
|
|
658
|
-
}
|
|
659
|
-
return null;
|
|
660
|
-
};
|
|
661
|
-
|
|
662
|
-
// Send a backend message using the context hierarchy
|
|
663
|
-
// New signature: sendMessage(content, functions, opts)
|
|
664
|
-
// Always sends as role='user' with '[BACKEND] ' prefix
|
|
665
|
-
Itask.prototype.sendMessage = async function sendMessage(content, functions, opts){
|
|
666
|
-
// First try our own context
|
|
667
|
-
let ctx = this.getContext();
|
|
668
|
-
if (!ctx){
|
|
669
|
-
// Walk up to find a context
|
|
670
|
-
ctx = this.findContext();
|
|
671
|
-
}
|
|
672
|
-
if (!ctx){
|
|
673
|
-
throw new Error('No context available in task hierarchy to send message');
|
|
674
|
-
}
|
|
675
|
-
opts = Object.assign({}, opts, { tag: this.context_id });
|
|
676
|
-
return ctx.sendMessage('user', '[BACKEND] ' + content, functions, opts);
|
|
677
|
-
};
|
|
678
|
-
|
|
679
|
-
// Receive a user chat message (no [BACKEND] prefix)
|
|
680
|
-
Itask.prototype.recvChatMessage = async function recvChatMessage(content, opts){
|
|
681
|
-
let ctx = this.getContext();
|
|
682
|
-
if (!ctx){
|
|
683
|
-
ctx = this.findContext();
|
|
684
|
-
}
|
|
685
|
-
if (!ctx){
|
|
686
|
-
throw new Error('No context available in task hierarchy to receive message');
|
|
687
|
-
}
|
|
688
|
-
opts = Object.assign({}, opts, { tag: this.context_id });
|
|
689
|
-
return ctx.sendMessage('user', content, null, opts);
|
|
690
|
-
};
|
|
691
|
-
|
|
692
|
-
// Aggregate functions from all contexts in the hierarchy
|
|
693
|
-
Itask.prototype.getHierarchyFunctions = function getHierarchyFunctions(){
|
|
694
|
-
const allFunctions = [];
|
|
695
|
-
const contexts = this.getAncestorContexts();
|
|
696
|
-
for (const ctx of contexts){
|
|
697
|
-
if (ctx.functions && Array.isArray(ctx.functions))
|
|
698
|
-
allFunctions.push(...ctx.functions);
|
|
699
|
-
}
|
|
700
|
-
// Add this task's own functions if not already in a context
|
|
701
|
-
if (this.functions && !this.context)
|
|
702
|
-
allFunctions.push(...this.functions);
|
|
703
|
-
return allFunctions;
|
|
704
|
-
};
|
|
705
|
-
|
|
706
|
-
// Close this task's context (if any) and bubble summary to parent
|
|
707
|
-
Itask.prototype.closeContext = async function closeContext(){
|
|
708
|
-
if (!this.context)
|
|
709
|
-
return;
|
|
710
|
-
|
|
711
|
-
// Clean tool call messages tagged with this context_id
|
|
712
|
-
if (this.context_id && typeof this.context.cleanToolCallsByTag === 'function')
|
|
713
|
-
this.context.cleanToolCallsByTag(this.context_id);
|
|
714
|
-
|
|
715
|
-
// Filter out tool calls and [BACKEND] messages, compress remaining as chat_history
|
|
716
|
-
const cleanedMsgs = this.context._msgs.filter(m => {
|
|
717
|
-
if (m.msg.tool_calls)
|
|
718
|
-
return false;
|
|
719
|
-
if (m.msg.role === 'tool')
|
|
720
|
-
return false;
|
|
721
|
-
if (typeof m.msg.content === 'string' && m.msg.content.startsWith('[BACKEND]'))
|
|
722
|
-
return false;
|
|
723
|
-
return true;
|
|
724
|
-
}).map(m => m.msg);
|
|
725
|
-
|
|
726
|
-
// Trim to last QUEUE_LIMIT before persisting
|
|
727
|
-
const queueLimit = this.context.QUEUE_LIMIT || 30;
|
|
728
|
-
const trimmedMsgs = cleanedMsgs.length > queueLimit
|
|
729
|
-
? cleanedMsgs.slice(-queueLimit)
|
|
730
|
-
: cleanedMsgs;
|
|
731
|
-
|
|
732
|
-
if (trimmedMsgs.length > 0) {
|
|
733
|
-
const chat_history = await util.compressMessages(trimmedMsgs);
|
|
734
|
-
this.context.chat_history = chat_history;
|
|
735
|
-
|
|
736
|
-
// Persist to store
|
|
737
|
-
const store = this._store || Store.instance;
|
|
738
|
-
if (store && this.context_id) {
|
|
739
|
-
await store.save(this.context_id, {
|
|
740
|
-
chat_history,
|
|
741
|
-
tool_digest: this.context.tool_digest || [],
|
|
742
|
-
prompt: this.context.prompt,
|
|
743
|
-
tag: this.context.tag,
|
|
744
|
-
tm_closed: Date.now()
|
|
745
|
-
});
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
await this.context.close();
|
|
750
|
-
};
|
|
751
|
-
|
|
752
|
-
// Walk DOWN to find the deepest active descendant with a context
|
|
753
|
-
Itask.prototype.findDeepestContext = function findDeepestContext() {
|
|
754
|
-
let deepest = this.context ? { context: this.context, depth: 0 } : null;
|
|
755
|
-
const search = (task, depth) => {
|
|
756
|
-
for (const child of task.child) {
|
|
757
|
-
if (child._completed) continue;
|
|
758
|
-
if (child.context) {
|
|
759
|
-
if (!deepest || depth + 1 >= deepest.depth)
|
|
760
|
-
deepest = { context: child.context, depth: depth + 1 };
|
|
761
|
-
}
|
|
762
|
-
search(child, depth + 1);
|
|
763
|
-
}
|
|
764
|
-
};
|
|
765
|
-
search(this, 0);
|
|
766
|
-
return deepest ? deepest.context : null;
|
|
767
|
-
};
|
|
768
|
-
|
|
769
|
-
// Reference to Context class (set by index.js to avoid circular dependency)
|
|
770
|
-
Itask.Context = null;
|
|
771
|
-
|
|
772
569
|
/* ---------- export ---------- */
|
|
773
570
|
module.exports = Itask;
|
|
774
571
|
|
package/msgs.js
CHANGED
|
@@ -92,18 +92,30 @@ class Context {
|
|
|
92
92
|
this.tool_digest = this.tool_digest.slice(-this.TOOL_DIGEST_LIMIT);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
// Get the parent context by traversing task hierarchy
|
|
95
|
+
// Get the parent context by traversing task hierarchy (via Saico)
|
|
96
96
|
getParentContext() {
|
|
97
97
|
if (!this.task || !this.task.parent)
|
|
98
98
|
return null;
|
|
99
|
-
|
|
99
|
+
let task = this.task.parent;
|
|
100
|
+
while (task) {
|
|
101
|
+
if (task._saico?.context) return task._saico.context;
|
|
102
|
+
task = task.parent;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
100
105
|
}
|
|
101
106
|
|
|
102
|
-
// Get all ancestor contexts via task hierarchy
|
|
107
|
+
// Get all ancestor contexts via task hierarchy (via Saico)
|
|
103
108
|
getAncestorContexts() {
|
|
104
109
|
if (!this.task)
|
|
105
110
|
return [];
|
|
106
|
-
|
|
111
|
+
const contexts = [];
|
|
112
|
+
let task = this.task.parent;
|
|
113
|
+
while (task) {
|
|
114
|
+
if (task._saico?.context)
|
|
115
|
+
contexts.unshift(task._saico.context);
|
|
116
|
+
task = task.parent;
|
|
117
|
+
}
|
|
118
|
+
return contexts;
|
|
107
119
|
}
|
|
108
120
|
|
|
109
121
|
_hasPendingToolCalls() {
|
|
@@ -1145,7 +1157,7 @@ class Context {
|
|
|
1145
1157
|
// Spawn child context (creates a child task with its own context)
|
|
1146
1158
|
spawnChild(prompt, tag, config = {}) {
|
|
1147
1159
|
if (!this.task) {
|
|
1148
|
-
// If no task, create a standalone context
|
|
1160
|
+
// If no task, create a standalone context
|
|
1149
1161
|
return createContext(prompt, null, { ...config, tag });
|
|
1150
1162
|
}
|
|
1151
1163
|
|
|
@@ -1153,14 +1165,16 @@ class Context {
|
|
|
1153
1165
|
const Itask = require('./itask.js');
|
|
1154
1166
|
const childTask = new Itask({
|
|
1155
1167
|
name: tag || 'child-context',
|
|
1156
|
-
prompt,
|
|
1157
1168
|
async: true,
|
|
1158
|
-
spawn_parent: this.task,
|
|
1159
|
-
contextConfig: config
|
|
1160
1169
|
}, []);
|
|
1170
|
+
this.task.spawn(childTask);
|
|
1161
1171
|
|
|
1162
1172
|
const childContext = new Context(prompt, childTask, { ...config, tag });
|
|
1163
|
-
|
|
1173
|
+
// Store context on Saico if present, otherwise just set on task reference
|
|
1174
|
+
if (childTask._saico) {
|
|
1175
|
+
childTask._saico.context = childContext;
|
|
1176
|
+
childTask._saico.context_id = childContext.tag;
|
|
1177
|
+
}
|
|
1164
1178
|
|
|
1165
1179
|
return childContext;
|
|
1166
1180
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "saico",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.1",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"description": "Hierarchical AI Conversation Orchestrator - Task hierarchy with conversation contexts",
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
"files": [
|
|
16
16
|
"index.js",
|
|
17
17
|
"itask.js",
|
|
18
|
-
"context.js",
|
|
19
18
|
"msgs.js",
|
|
20
19
|
"saico.js",
|
|
21
20
|
"dynamo.js",
|
package/saico.js
CHANGED
|
@@ -4,6 +4,11 @@ const crypto = require('crypto');
|
|
|
4
4
|
const Itask = require('./itask.js');
|
|
5
5
|
const { Context } = require('./msgs.js');
|
|
6
6
|
const { Store } = require('./store.js');
|
|
7
|
+
const util = require('./util.js');
|
|
8
|
+
|
|
9
|
+
function makeId(len = 12){
|
|
10
|
+
return crypto.randomBytes(Math.ceil(len/2)).toString('hex').slice(0, len);
|
|
11
|
+
}
|
|
7
12
|
|
|
8
13
|
/**
|
|
9
14
|
* Saico — Master class for building AI-powered services.
|
|
@@ -32,6 +37,7 @@ class Saico {
|
|
|
32
37
|
* @param {Array} [opt.functions] - Available AI functions
|
|
33
38
|
* @param {string} [opt.key] - Redis key override (default: 'saico:<id>')
|
|
34
39
|
* @param {boolean} [opt.redis=true] - Set false to skip Redis proxy
|
|
40
|
+
* @param {boolean} [opt.createQ] - Create message Q context on activate()
|
|
35
41
|
* @param {boolean} [opt.isolate] - Isolate: don't aggregate from ancestors
|
|
36
42
|
* @param {Object} [opt.dynamodb] - DynamoDB config { region, credentials: { accessKeyId, secretAccessKey }, client }
|
|
37
43
|
* @param {Object} [opt.db] - Pluggable DB backend
|
|
@@ -47,10 +53,15 @@ class Saico {
|
|
|
47
53
|
this._opt = opt;
|
|
48
54
|
this._isolate = opt.isolate || false;
|
|
49
55
|
|
|
56
|
+
// Context owned directly by Saico (not Itask)
|
|
57
|
+
this.context = null;
|
|
58
|
+
this.context_id = null;
|
|
59
|
+
|
|
50
60
|
// Public configuration
|
|
51
61
|
this.name = opt.name || this.constructor.name || 'saico';
|
|
52
62
|
this.prompt = opt.prompt || '';
|
|
53
63
|
this.functions = opt.functions || null;
|
|
64
|
+
this.createQ = opt.createQ || false;
|
|
54
65
|
|
|
55
66
|
// Absorbed from Sid
|
|
56
67
|
this.userData = opt.userData || {};
|
|
@@ -90,11 +101,10 @@ class Saico {
|
|
|
90
101
|
* Create the internal Itask and optionally a message Q context.
|
|
91
102
|
*
|
|
92
103
|
* @param {Object} opts
|
|
93
|
-
* @param {boolean} [opts.createQ] -
|
|
104
|
+
* @param {boolean} [opts.createQ] - Override this.createQ for this activation
|
|
94
105
|
* @param {string} [opts.prompt] - Additional prompt (appended to class-level)
|
|
95
106
|
* @param {Array} [opts.functions] - Override functions
|
|
96
|
-
* @param {Array} [opts.states] -
|
|
97
|
-
* @param {Itask} [opts.parent] - Parent task to spawn under
|
|
107
|
+
* @param {Array} [opts.states] - Override this.states for this activation
|
|
98
108
|
* @param {string} [opts.taskId] - Custom task ID
|
|
99
109
|
* @param {number} [opts.token_limit] - Token limit for context
|
|
100
110
|
* @param {number} [opts.max_depth] - Max tool call depth
|
|
@@ -111,7 +121,7 @@ class Saico {
|
|
|
111
121
|
if (this._task)
|
|
112
122
|
throw new Error('Already activated. Call deactivate() first.');
|
|
113
123
|
|
|
114
|
-
const states = opts.states || [];
|
|
124
|
+
const states = opts.states || this.states || [];
|
|
115
125
|
|
|
116
126
|
// Build effective prompt: class-level + activation-level
|
|
117
127
|
const effectivePrompt = [this.prompt, opts.prompt].filter(Boolean).join('\n');
|
|
@@ -120,21 +130,17 @@ class Saico {
|
|
|
120
130
|
name: this.name,
|
|
121
131
|
id: opts.taskId,
|
|
122
132
|
async: true,
|
|
123
|
-
store: this._store,
|
|
124
|
-
functions: opts.functions || this.functions,
|
|
125
133
|
bind: this, // State functions run with Saico instance as `this`
|
|
126
134
|
};
|
|
127
135
|
|
|
128
|
-
if (opts.parent)
|
|
129
|
-
taskOpt.spawn_parent = opts.parent;
|
|
130
|
-
|
|
131
136
|
this._task = new Itask(taskOpt, states);
|
|
132
137
|
|
|
133
138
|
// Store Saico reference on task for parent chain traversal
|
|
134
139
|
this._task._saico = this;
|
|
135
140
|
|
|
136
|
-
// Create message Q context if requested (
|
|
137
|
-
if (opts.createQ) {
|
|
141
|
+
// Create message Q context if requested (class-level or activate-level)
|
|
142
|
+
if (opts.createQ ?? this.createQ) {
|
|
143
|
+
const functions = opts.functions || this.functions;
|
|
138
144
|
const contextConfig = {
|
|
139
145
|
tag: opts.tag || this._task.id,
|
|
140
146
|
token_limit: opts.token_limit ?? this.sessionConfig.token_limit,
|
|
@@ -142,7 +148,7 @@ class Saico {
|
|
|
142
148
|
max_tool_repetition: opts.max_tool_repetition ?? this.sessionConfig.max_tool_repetition,
|
|
143
149
|
queue_limit: opts.queue_limit ?? this.sessionConfig.queue_limit,
|
|
144
150
|
min_chat_messages: opts.min_chat_messages ?? this.sessionConfig.min_chat_messages,
|
|
145
|
-
functions
|
|
151
|
+
functions,
|
|
146
152
|
sequential_mode: opts.sequential_mode,
|
|
147
153
|
msgs: opts.msgs,
|
|
148
154
|
chat_history: opts.chat_history,
|
|
@@ -150,15 +156,118 @@ class Saico {
|
|
|
150
156
|
};
|
|
151
157
|
|
|
152
158
|
const augmentedPrompt = effectivePrompt
|
|
153
|
-
? effectivePrompt +
|
|
159
|
+
? effectivePrompt + Saico.BACKEND_EXPLANATION
|
|
154
160
|
: '';
|
|
155
161
|
const context = new Context(augmentedPrompt, this._task, contextConfig);
|
|
156
|
-
this.
|
|
162
|
+
this.setContext(context);
|
|
157
163
|
}
|
|
158
164
|
|
|
159
165
|
return this;
|
|
160
166
|
}
|
|
161
167
|
|
|
168
|
+
// ---- Context management (owned by Saico, not Itask) ----
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Set context on this Saico instance.
|
|
172
|
+
* Generates context_id, sets context.tag, and calls context.setTask().
|
|
173
|
+
*/
|
|
174
|
+
setContext(context) {
|
|
175
|
+
this.context = context;
|
|
176
|
+
// Generate context_id if not already set
|
|
177
|
+
if (!this.context_id) {
|
|
178
|
+
if (this._store)
|
|
179
|
+
this.context_id = this._store.generateId();
|
|
180
|
+
else if (Store.instance)
|
|
181
|
+
this.context_id = Store.instance.generateId();
|
|
182
|
+
else
|
|
183
|
+
this.context_id = makeId(16);
|
|
184
|
+
}
|
|
185
|
+
if (context) {
|
|
186
|
+
context.tag = this.context_id;
|
|
187
|
+
if (typeof context.setTask === 'function')
|
|
188
|
+
context.setTask(this._task);
|
|
189
|
+
}
|
|
190
|
+
return this;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Find the nearest context walking UP the Saico/task hierarchy.
|
|
195
|
+
*/
|
|
196
|
+
findContext() {
|
|
197
|
+
if (this.context) return this.context;
|
|
198
|
+
let task = this._task?.parent;
|
|
199
|
+
while (task) {
|
|
200
|
+
if (task._saico?.context) return task._saico.context;
|
|
201
|
+
task = task.parent;
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Walk DOWN to find the deepest active descendant with a context.
|
|
208
|
+
*/
|
|
209
|
+
findDeepestContext() {
|
|
210
|
+
if (!this._task) return this.context || null;
|
|
211
|
+
let deepest = this.context ? { context: this.context, depth: 0 } : null;
|
|
212
|
+
const search = (task, depth) => {
|
|
213
|
+
for (const child of task.child) {
|
|
214
|
+
if (child._completed) continue;
|
|
215
|
+
if (child._saico?.context) {
|
|
216
|
+
if (!deepest || depth + 1 >= deepest.depth)
|
|
217
|
+
deepest = { context: child._saico.context, depth: depth + 1 };
|
|
218
|
+
}
|
|
219
|
+
search(child, depth + 1);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
search(this._task, 0);
|
|
223
|
+
return deepest ? deepest.context : null;
|
|
224
|
+
}
|
|
225
|
+
|
|
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
|
+
|
|
162
271
|
/**
|
|
163
272
|
* Deactivate — bubble cleaned messages to parent, close context, cancel task.
|
|
164
273
|
* Pushes cleaned messages (no tool calls, no BACKEND) into the parent's Q,
|
|
@@ -166,12 +275,12 @@ class Saico {
|
|
|
166
275
|
*/
|
|
167
276
|
async deactivate() {
|
|
168
277
|
if (!this._task) return;
|
|
169
|
-
if (this.
|
|
278
|
+
if (this.context) {
|
|
170
279
|
// Find parent context to bubble cleaned messages
|
|
171
280
|
let parentTask = this._task.parent;
|
|
172
281
|
let parentCtx = null;
|
|
173
282
|
while (parentTask) {
|
|
174
|
-
if (parentTask.context) { parentCtx = parentTask.context; break; }
|
|
283
|
+
if (parentTask._saico?.context) { parentCtx = parentTask._saico.context; break; }
|
|
175
284
|
parentTask = parentTask.parent;
|
|
176
285
|
}
|
|
177
286
|
if (parentCtx) {
|
|
@@ -180,16 +289,45 @@ class Saico {
|
|
|
180
289
|
parentCtx.push(msg);
|
|
181
290
|
}
|
|
182
291
|
// Clean tool calls and close context without additional summary bubbling.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
this._task.context = null;
|
|
292
|
+
if (this.context_id && typeof this.context.cleanToolCallsByTag === 'function')
|
|
293
|
+
this.context.cleanToolCallsByTag(this.context_id);
|
|
294
|
+
this.context = null;
|
|
295
|
+
this.context_id = null;
|
|
188
296
|
}
|
|
189
297
|
this._task._ecancel();
|
|
190
298
|
this._task = null;
|
|
191
299
|
}
|
|
192
300
|
|
|
301
|
+
// ---- Spawn ----
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Spawn a child Saico under this Saico's task hierarchy.
|
|
305
|
+
* Both parent and child must be activated.
|
|
306
|
+
* @param {Saico} child - An activated Saico instance
|
|
307
|
+
* @returns {Saico} the child (for chaining)
|
|
308
|
+
*/
|
|
309
|
+
spawn(child) {
|
|
310
|
+
if (!this._task)
|
|
311
|
+
throw new Error('Not activated. Call activate() first.');
|
|
312
|
+
if (!(child instanceof Saico) || !child._task)
|
|
313
|
+
throw new Error('Child must be an activated Saico instance.');
|
|
314
|
+
this._task.spawn(child._task);
|
|
315
|
+
return child;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Spawn a child Saico and start its task running.
|
|
320
|
+
* @param {Saico} child - An activated Saico instance
|
|
321
|
+
* @returns {Saico} the child (for chaining)
|
|
322
|
+
*/
|
|
323
|
+
spawnAndRun(child) {
|
|
324
|
+
this.spawn(child);
|
|
325
|
+
process.nextTick(() => {
|
|
326
|
+
try { child._task._run(); } catch (e) { console.error(e); }
|
|
327
|
+
});
|
|
328
|
+
return child;
|
|
329
|
+
}
|
|
330
|
+
|
|
193
331
|
// ---- Saico parent chain traversal ----
|
|
194
332
|
|
|
195
333
|
/**
|
|
@@ -260,19 +398,19 @@ class Saico {
|
|
|
260
398
|
throw new Error('Not activated. Call activate() first.');
|
|
261
399
|
|
|
262
400
|
// Find the active context (own or walk up)
|
|
263
|
-
let ctx = this.
|
|
401
|
+
let ctx = this.findContext();
|
|
264
402
|
if (!ctx)
|
|
265
403
|
throw new Error('No context available');
|
|
266
404
|
|
|
267
405
|
// Build preamble by walking Saico chain
|
|
268
|
-
const activeCtx = this.
|
|
406
|
+
const activeCtx = this.findDeepestContext() || ctx;
|
|
269
407
|
const { preamble, allFunctions } = this._buildPreamble(activeCtx);
|
|
270
408
|
|
|
271
409
|
// Merge with call-specific functions
|
|
272
410
|
if (functions) allFunctions.push(...(Array.isArray(functions) ? functions : [functions]));
|
|
273
411
|
|
|
274
412
|
opts = Object.assign({}, opts, {
|
|
275
|
-
tag: this.
|
|
413
|
+
tag: this.context_id,
|
|
276
414
|
_preamble: preamble,
|
|
277
415
|
_aggregatedFunctions: allFunctions.length > 0 ? allFunctions : null,
|
|
278
416
|
});
|
|
@@ -284,7 +422,7 @@ class Saico {
|
|
|
284
422
|
throw new Error('Not activated. Call activate() first.');
|
|
285
423
|
|
|
286
424
|
// Route DOWN to deepest descendant with a msg Q
|
|
287
|
-
const ctx = this.
|
|
425
|
+
const ctx = this.findDeepestContext();
|
|
288
426
|
if (!ctx)
|
|
289
427
|
throw new Error('No context available');
|
|
290
428
|
|
|
@@ -302,63 +440,8 @@ class Saico {
|
|
|
302
440
|
// ---- Task delegation ----
|
|
303
441
|
|
|
304
442
|
get task() { return this._task; }
|
|
305
|
-
get context() { return this._task?.context || null; }
|
|
306
|
-
get context_id() { return this._task?.context_id || null; }
|
|
307
443
|
get isActive() { return !!this._task && !this._task._completed; }
|
|
308
444
|
|
|
309
|
-
spawnTaskWithContext(opt, states) {
|
|
310
|
-
if (!this._task)
|
|
311
|
-
throw new Error('Not activated. Call activate() first.');
|
|
312
|
-
if (typeof opt === 'string')
|
|
313
|
-
opt = { name: opt };
|
|
314
|
-
|
|
315
|
-
const childTask = new Itask({
|
|
316
|
-
...opt,
|
|
317
|
-
spawn_parent: this._task,
|
|
318
|
-
store: this._store,
|
|
319
|
-
async: true,
|
|
320
|
-
}, states || []);
|
|
321
|
-
|
|
322
|
-
if (opt.prompt) {
|
|
323
|
-
const childContext = new Context(opt.prompt, childTask, {
|
|
324
|
-
tag: opt.tag || childTask.id,
|
|
325
|
-
token_limit: opt.token_limit ?? this.sessionConfig.token_limit,
|
|
326
|
-
max_depth: opt.max_depth ?? this.sessionConfig.max_depth,
|
|
327
|
-
max_tool_repetition: opt.max_tool_repetition ?? this.sessionConfig.max_tool_repetition,
|
|
328
|
-
queue_limit: opt.queue_limit ?? this.sessionConfig.queue_limit,
|
|
329
|
-
min_chat_messages: opt.min_chat_messages ?? this.sessionConfig.min_chat_messages,
|
|
330
|
-
functions: opt.functions || this.functions,
|
|
331
|
-
});
|
|
332
|
-
childTask.setContext(childContext);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
process.nextTick(() => {
|
|
336
|
-
try { childTask._run(); } catch (e) { console.error(e); }
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
return childTask;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
spawnTask(opt, states) {
|
|
343
|
-
if (!this._task)
|
|
344
|
-
throw new Error('Not activated. Call activate() first.');
|
|
345
|
-
if (typeof opt === 'string')
|
|
346
|
-
opt = { name: opt };
|
|
347
|
-
|
|
348
|
-
const childTask = new Itask({
|
|
349
|
-
...opt,
|
|
350
|
-
spawn_parent: this._task,
|
|
351
|
-
store: this._store,
|
|
352
|
-
async: true,
|
|
353
|
-
}, states || []);
|
|
354
|
-
|
|
355
|
-
process.nextTick(() => {
|
|
356
|
-
try { childTask._run(); } catch (e) { console.error(e); }
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
return childTask;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
445
|
// ---- State Summary ----
|
|
363
446
|
|
|
364
447
|
/**
|
|
@@ -437,8 +520,8 @@ class Saico {
|
|
|
437
520
|
|
|
438
521
|
async closeSession() {
|
|
439
522
|
if (!this._task) return;
|
|
440
|
-
if (this.
|
|
441
|
-
await this.
|
|
523
|
+
if (this.context)
|
|
524
|
+
await this.context.close();
|
|
442
525
|
this._task._ecancel();
|
|
443
526
|
}
|
|
444
527
|
|
|
@@ -555,13 +638,13 @@ class Saico {
|
|
|
555
638
|
if (this._task) {
|
|
556
639
|
data.task = {
|
|
557
640
|
id: this._task.id,
|
|
558
|
-
context_id: this.
|
|
559
|
-
context: this.
|
|
560
|
-
tag: this.
|
|
561
|
-
msgs: this.
|
|
562
|
-
functions: this.
|
|
563
|
-
chat_history: this.
|
|
564
|
-
tool_digest: this.
|
|
641
|
+
context_id: this.context_id,
|
|
642
|
+
context: this.context ? {
|
|
643
|
+
tag: this.context.tag,
|
|
644
|
+
msgs: this.context._msgs,
|
|
645
|
+
functions: this.context.functions,
|
|
646
|
+
chat_history: this.context.chat_history,
|
|
647
|
+
tool_digest: this.context.tool_digest,
|
|
565
648
|
} : null,
|
|
566
649
|
};
|
|
567
650
|
}
|
|
@@ -604,13 +687,13 @@ class Saico {
|
|
|
604
687
|
});
|
|
605
688
|
|
|
606
689
|
// Restore messages to context
|
|
607
|
-
if (parsed.task.context?.msgs && instance.
|
|
608
|
-
instance.
|
|
690
|
+
if (parsed.task.context?.msgs && instance.context) {
|
|
691
|
+
instance.context._msgs = parsed.task.context.msgs;
|
|
609
692
|
}
|
|
610
693
|
|
|
611
694
|
// Restore tool_digest
|
|
612
|
-
if (Array.isArray(parsed.task.context?.tool_digest) && instance.
|
|
613
|
-
instance.
|
|
695
|
+
if (Array.isArray(parsed.task.context?.tool_digest) && instance.context) {
|
|
696
|
+
instance.context.tool_digest = parsed.task.context.tool_digest;
|
|
614
697
|
}
|
|
615
698
|
}
|
|
616
699
|
|
|
@@ -618,4 +701,9 @@ class Saico {
|
|
|
618
701
|
}
|
|
619
702
|
}
|
|
620
703
|
|
|
704
|
+
// [BACKEND] explanation text appended to context prompts
|
|
705
|
+
Saico.BACKEND_EXPLANATION = '\nNote: Messages prefixed with [BACKEND] are from the backend ' +
|
|
706
|
+
'server, not the user. They contain server instructions, data updates, or system context. ' +
|
|
707
|
+
'Treat them as authoritative system-level information.';
|
|
708
|
+
|
|
621
709
|
module.exports = { Saico };
|
package/context.js
DELETED