saico 2.6.1 → 2.7.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 +27 -34
- package/index.js +2 -110
- package/itask.js +4 -207
- package/msgs.js +23 -9
- package/package.json +1 -2
- package/saico.js +174 -88
- package/context.js +0 -5
package/README.md
CHANGED
|
@@ -135,29 +135,32 @@ class OrderAgent extends Saico {
|
|
|
135
135
|
|
|
136
136
|
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
137
|
|
|
138
|
-
### Spawning Child
|
|
138
|
+
### Spawning Child Saico Instances
|
|
139
139
|
|
|
140
140
|
```js
|
|
141
141
|
// Child with its own conversation context
|
|
142
|
-
const child =
|
|
142
|
+
const child = new Saico({
|
|
143
143
|
name: 'subtask',
|
|
144
144
|
prompt: 'Handle this specific sub-task',
|
|
145
|
-
functions: [/* child-specific tools */]
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
145
|
+
functions: [/* child-specific tools */],
|
|
146
|
+
});
|
|
147
|
+
child.activate({ createQ: true });
|
|
148
|
+
agent.spawn(child);
|
|
149
|
+
await child.sendMessage('Working on subtask...');
|
|
150
|
+
|
|
151
|
+
// Child without context (uses parent's via findContext())
|
|
152
|
+
const simple = new Saico({ name: 'simple' });
|
|
153
|
+
simple.activate();
|
|
154
|
+
agent.spawn(simple);
|
|
155
|
+
await simple.sendMessage('Quick operation');
|
|
156
|
+
|
|
157
|
+
// spawnAndRun: spawn + schedule child task to run on nextTick
|
|
158
|
+
const runner = new Saico({ name: 'runner' });
|
|
159
|
+
runner.activate({ states: [async function() { return await this.sendMessage('Go'); }] });
|
|
160
|
+
agent.spawnAndRun(runner);
|
|
158
161
|
```
|
|
159
162
|
|
|
160
|
-
|
|
163
|
+
Both parent and child must be activated before calling `spawn()` or `spawnAndRun()`.
|
|
161
164
|
|
|
162
165
|
### Deactivation and Message Bubbling
|
|
163
166
|
|
|
@@ -207,7 +210,6 @@ agent.activate({
|
|
|
207
210
|
createQ: true, // Create message queue context
|
|
208
211
|
prompt: 'Extra prompt', // Appended to class-level prompt
|
|
209
212
|
states: [], // Task state functions
|
|
210
|
-
parent: parentTask, // Parent task to spawn under
|
|
211
213
|
taskId: 'custom-id',
|
|
212
214
|
sequential_mode: true, // Process messages sequentially
|
|
213
215
|
|
|
@@ -336,21 +338,13 @@ Return a string or `{ content: string, functions?: [] }`.
|
|
|
336
338
|
|
|
337
339
|
## Low-Level API
|
|
338
340
|
|
|
339
|
-
For cases where you
|
|
341
|
+
For cases where you need a standalone context without the Saico master class:
|
|
340
342
|
|
|
341
343
|
```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');
|
|
344
|
+
const { createContext } = require('saico');
|
|
351
345
|
|
|
352
|
-
// Standalone context
|
|
353
|
-
const ctx =
|
|
346
|
+
// Standalone context
|
|
347
|
+
const ctx = createContext('System prompt', null, { tag: 'my-tag', token_limit: 4000 });
|
|
354
348
|
const reply = await ctx.sendMessage('user', 'Hello', functions);
|
|
355
349
|
```
|
|
356
350
|
|
|
@@ -358,11 +352,10 @@ const reply = await ctx.sendMessage('user', 'Hello', functions);
|
|
|
358
352
|
|
|
359
353
|
```
|
|
360
354
|
saico/
|
|
361
|
-
+-- index.js #
|
|
362
|
-
+-- saico.js # Saico master class
|
|
363
|
-
+-- itask.js #
|
|
355
|
+
+-- index.js # Thin barrel file, exports all components
|
|
356
|
+
+-- saico.js # Saico master class — owns context, spawn, DB, orchestration
|
|
357
|
+
+-- itask.js # Pure task runner — hierarchy, states, cancellation, promises
|
|
364
358
|
+-- msgs.js # Conversation context (message queue, tool calls, summarization)
|
|
365
|
-
+-- context.js # Backward-compat shim for msgs.js
|
|
366
359
|
+-- dynamo.js # DynamoDB storage adapter
|
|
367
360
|
+-- store.js # Storage abstraction (Redis + pluggable backends)
|
|
368
361
|
+-- openai.js # OpenAI API wrapper with retry logic
|
|
@@ -376,7 +369,7 @@ saico/
|
|
|
376
369
|
npm test
|
|
377
370
|
```
|
|
378
371
|
|
|
379
|
-
|
|
372
|
+
284 tests covering Saico lifecycle, context ownership, spawn/spawnAndRun, task hierarchy, message handling, tool calls, DB adapters, serialization, and integration flows.
|
|
380
373
|
|
|
381
374
|
## Requirements
|
|
382
375
|
|
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.0",
|
|
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.
|
|
@@ -47,6 +52,10 @@ class Saico {
|
|
|
47
52
|
this._opt = opt;
|
|
48
53
|
this._isolate = opt.isolate || false;
|
|
49
54
|
|
|
55
|
+
// Context owned directly by Saico (not Itask)
|
|
56
|
+
this.context = null;
|
|
57
|
+
this.context_id = null;
|
|
58
|
+
|
|
50
59
|
// Public configuration
|
|
51
60
|
this.name = opt.name || this.constructor.name || 'saico';
|
|
52
61
|
this.prompt = opt.prompt || '';
|
|
@@ -94,7 +103,6 @@ class Saico {
|
|
|
94
103
|
* @param {string} [opts.prompt] - Additional prompt (appended to class-level)
|
|
95
104
|
* @param {Array} [opts.functions] - Override functions
|
|
96
105
|
* @param {Array} [opts.states] - Task state functions
|
|
97
|
-
* @param {Itask} [opts.parent] - Parent task to spawn under
|
|
98
106
|
* @param {string} [opts.taskId] - Custom task ID
|
|
99
107
|
* @param {number} [opts.token_limit] - Token limit for context
|
|
100
108
|
* @param {number} [opts.max_depth] - Max tool call depth
|
|
@@ -120,14 +128,9 @@ class Saico {
|
|
|
120
128
|
name: this.name,
|
|
121
129
|
id: opts.taskId,
|
|
122
130
|
async: true,
|
|
123
|
-
store: this._store,
|
|
124
|
-
functions: opts.functions || this.functions,
|
|
125
131
|
bind: this, // State functions run with Saico instance as `this`
|
|
126
132
|
};
|
|
127
133
|
|
|
128
|
-
if (opts.parent)
|
|
129
|
-
taskOpt.spawn_parent = opts.parent;
|
|
130
|
-
|
|
131
134
|
this._task = new Itask(taskOpt, states);
|
|
132
135
|
|
|
133
136
|
// Store Saico reference on task for parent chain traversal
|
|
@@ -135,6 +138,7 @@ class Saico {
|
|
|
135
138
|
|
|
136
139
|
// Create message Q context if requested (only via createQ flag, NOT prompt)
|
|
137
140
|
if (opts.createQ) {
|
|
141
|
+
const functions = opts.functions || this.functions;
|
|
138
142
|
const contextConfig = {
|
|
139
143
|
tag: opts.tag || this._task.id,
|
|
140
144
|
token_limit: opts.token_limit ?? this.sessionConfig.token_limit,
|
|
@@ -142,7 +146,7 @@ class Saico {
|
|
|
142
146
|
max_tool_repetition: opts.max_tool_repetition ?? this.sessionConfig.max_tool_repetition,
|
|
143
147
|
queue_limit: opts.queue_limit ?? this.sessionConfig.queue_limit,
|
|
144
148
|
min_chat_messages: opts.min_chat_messages ?? this.sessionConfig.min_chat_messages,
|
|
145
|
-
functions
|
|
149
|
+
functions,
|
|
146
150
|
sequential_mode: opts.sequential_mode,
|
|
147
151
|
msgs: opts.msgs,
|
|
148
152
|
chat_history: opts.chat_history,
|
|
@@ -150,15 +154,118 @@ class Saico {
|
|
|
150
154
|
};
|
|
151
155
|
|
|
152
156
|
const augmentedPrompt = effectivePrompt
|
|
153
|
-
? effectivePrompt +
|
|
157
|
+
? effectivePrompt + Saico.BACKEND_EXPLANATION
|
|
154
158
|
: '';
|
|
155
159
|
const context = new Context(augmentedPrompt, this._task, contextConfig);
|
|
156
|
-
this.
|
|
160
|
+
this.setContext(context);
|
|
157
161
|
}
|
|
158
162
|
|
|
159
163
|
return this;
|
|
160
164
|
}
|
|
161
165
|
|
|
166
|
+
// ---- Context management (owned by Saico, not Itask) ----
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Set context on this Saico instance.
|
|
170
|
+
* Generates context_id, sets context.tag, and calls context.setTask().
|
|
171
|
+
*/
|
|
172
|
+
setContext(context) {
|
|
173
|
+
this.context = context;
|
|
174
|
+
// Generate context_id if not already set
|
|
175
|
+
if (!this.context_id) {
|
|
176
|
+
if (this._store)
|
|
177
|
+
this.context_id = this._store.generateId();
|
|
178
|
+
else if (Store.instance)
|
|
179
|
+
this.context_id = Store.instance.generateId();
|
|
180
|
+
else
|
|
181
|
+
this.context_id = makeId(16);
|
|
182
|
+
}
|
|
183
|
+
if (context) {
|
|
184
|
+
context.tag = this.context_id;
|
|
185
|
+
if (typeof context.setTask === 'function')
|
|
186
|
+
context.setTask(this._task);
|
|
187
|
+
}
|
|
188
|
+
return this;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Find the nearest context walking UP the Saico/task hierarchy.
|
|
193
|
+
*/
|
|
194
|
+
findContext() {
|
|
195
|
+
if (this.context) return this.context;
|
|
196
|
+
let task = this._task?.parent;
|
|
197
|
+
while (task) {
|
|
198
|
+
if (task._saico?.context) return task._saico.context;
|
|
199
|
+
task = task.parent;
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Walk DOWN to find the deepest active descendant with a context.
|
|
206
|
+
*/
|
|
207
|
+
findDeepestContext() {
|
|
208
|
+
if (!this._task) return this.context || null;
|
|
209
|
+
let deepest = this.context ? { context: this.context, depth: 0 } : null;
|
|
210
|
+
const search = (task, depth) => {
|
|
211
|
+
for (const child of task.child) {
|
|
212
|
+
if (child._completed) continue;
|
|
213
|
+
if (child._saico?.context) {
|
|
214
|
+
if (!deepest || depth + 1 >= deepest.depth)
|
|
215
|
+
deepest = { context: child._saico.context, depth: depth + 1 };
|
|
216
|
+
}
|
|
217
|
+
search(child, depth + 1);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
search(this._task, 0);
|
|
221
|
+
return deepest ? deepest.context : null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Close this Saico's context and bubble summary to parent.
|
|
226
|
+
*/
|
|
227
|
+
async closeContext() {
|
|
228
|
+
if (!this.context)
|
|
229
|
+
return;
|
|
230
|
+
|
|
231
|
+
// Clean tool call messages tagged with this context_id
|
|
232
|
+
if (this.context_id && typeof this.context.cleanToolCallsByTag === 'function')
|
|
233
|
+
this.context.cleanToolCallsByTag(this.context_id);
|
|
234
|
+
|
|
235
|
+
// Filter out tool calls and [BACKEND] messages, compress remaining as chat_history
|
|
236
|
+
const cleanedMsgs = this.context._msgs.filter(m => {
|
|
237
|
+
if (m.msg.tool_calls) return false;
|
|
238
|
+
if (m.msg.role === 'tool') return false;
|
|
239
|
+
if (typeof m.msg.content === 'string' && m.msg.content.startsWith('[BACKEND]')) return false;
|
|
240
|
+
return true;
|
|
241
|
+
}).map(m => m.msg);
|
|
242
|
+
|
|
243
|
+
// Trim to last QUEUE_LIMIT before persisting
|
|
244
|
+
const queueLimit = this.context.QUEUE_LIMIT || 30;
|
|
245
|
+
const trimmedMsgs = cleanedMsgs.length > queueLimit
|
|
246
|
+
? cleanedMsgs.slice(-queueLimit)
|
|
247
|
+
: cleanedMsgs;
|
|
248
|
+
|
|
249
|
+
if (trimmedMsgs.length > 0) {
|
|
250
|
+
const chat_history = await util.compressMessages(trimmedMsgs);
|
|
251
|
+
this.context.chat_history = chat_history;
|
|
252
|
+
|
|
253
|
+
// Persist to store
|
|
254
|
+
const store = this._store || Store.instance;
|
|
255
|
+
if (store && this.context_id) {
|
|
256
|
+
await store.save(this.context_id, {
|
|
257
|
+
chat_history,
|
|
258
|
+
tool_digest: this.context.tool_digest || [],
|
|
259
|
+
prompt: this.context.prompt,
|
|
260
|
+
tag: this.context.tag,
|
|
261
|
+
tm_closed: Date.now()
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
await this.context.close();
|
|
267
|
+
}
|
|
268
|
+
|
|
162
269
|
/**
|
|
163
270
|
* Deactivate — bubble cleaned messages to parent, close context, cancel task.
|
|
164
271
|
* Pushes cleaned messages (no tool calls, no BACKEND) into the parent's Q,
|
|
@@ -166,12 +273,12 @@ class Saico {
|
|
|
166
273
|
*/
|
|
167
274
|
async deactivate() {
|
|
168
275
|
if (!this._task) return;
|
|
169
|
-
if (this.
|
|
276
|
+
if (this.context) {
|
|
170
277
|
// Find parent context to bubble cleaned messages
|
|
171
278
|
let parentTask = this._task.parent;
|
|
172
279
|
let parentCtx = null;
|
|
173
280
|
while (parentTask) {
|
|
174
|
-
if (parentTask.context) { parentCtx = parentTask.context; break; }
|
|
281
|
+
if (parentTask._saico?.context) { parentCtx = parentTask._saico.context; break; }
|
|
175
282
|
parentTask = parentTask.parent;
|
|
176
283
|
}
|
|
177
284
|
if (parentCtx) {
|
|
@@ -180,16 +287,45 @@ class Saico {
|
|
|
180
287
|
parentCtx.push(msg);
|
|
181
288
|
}
|
|
182
289
|
// Clean tool calls and close context without additional summary bubbling.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
this._task.context = null;
|
|
290
|
+
if (this.context_id && typeof this.context.cleanToolCallsByTag === 'function')
|
|
291
|
+
this.context.cleanToolCallsByTag(this.context_id);
|
|
292
|
+
this.context = null;
|
|
293
|
+
this.context_id = null;
|
|
188
294
|
}
|
|
189
295
|
this._task._ecancel();
|
|
190
296
|
this._task = null;
|
|
191
297
|
}
|
|
192
298
|
|
|
299
|
+
// ---- Spawn ----
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Spawn a child Saico under this Saico's task hierarchy.
|
|
303
|
+
* Both parent and child must be activated.
|
|
304
|
+
* @param {Saico} child - An activated Saico instance
|
|
305
|
+
* @returns {Saico} the child (for chaining)
|
|
306
|
+
*/
|
|
307
|
+
spawn(child) {
|
|
308
|
+
if (!this._task)
|
|
309
|
+
throw new Error('Not activated. Call activate() first.');
|
|
310
|
+
if (!(child instanceof Saico) || !child._task)
|
|
311
|
+
throw new Error('Child must be an activated Saico instance.');
|
|
312
|
+
this._task.spawn(child._task);
|
|
313
|
+
return child;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Spawn a child Saico and start its task running.
|
|
318
|
+
* @param {Saico} child - An activated Saico instance
|
|
319
|
+
* @returns {Saico} the child (for chaining)
|
|
320
|
+
*/
|
|
321
|
+
spawnAndRun(child) {
|
|
322
|
+
this.spawn(child);
|
|
323
|
+
process.nextTick(() => {
|
|
324
|
+
try { child._task._run(); } catch (e) { console.error(e); }
|
|
325
|
+
});
|
|
326
|
+
return child;
|
|
327
|
+
}
|
|
328
|
+
|
|
193
329
|
// ---- Saico parent chain traversal ----
|
|
194
330
|
|
|
195
331
|
/**
|
|
@@ -260,19 +396,19 @@ class Saico {
|
|
|
260
396
|
throw new Error('Not activated. Call activate() first.');
|
|
261
397
|
|
|
262
398
|
// Find the active context (own or walk up)
|
|
263
|
-
let ctx = this.
|
|
399
|
+
let ctx = this.findContext();
|
|
264
400
|
if (!ctx)
|
|
265
401
|
throw new Error('No context available');
|
|
266
402
|
|
|
267
403
|
// Build preamble by walking Saico chain
|
|
268
|
-
const activeCtx = this.
|
|
404
|
+
const activeCtx = this.findDeepestContext() || ctx;
|
|
269
405
|
const { preamble, allFunctions } = this._buildPreamble(activeCtx);
|
|
270
406
|
|
|
271
407
|
// Merge with call-specific functions
|
|
272
408
|
if (functions) allFunctions.push(...(Array.isArray(functions) ? functions : [functions]));
|
|
273
409
|
|
|
274
410
|
opts = Object.assign({}, opts, {
|
|
275
|
-
tag: this.
|
|
411
|
+
tag: this.context_id,
|
|
276
412
|
_preamble: preamble,
|
|
277
413
|
_aggregatedFunctions: allFunctions.length > 0 ? allFunctions : null,
|
|
278
414
|
});
|
|
@@ -284,7 +420,7 @@ class Saico {
|
|
|
284
420
|
throw new Error('Not activated. Call activate() first.');
|
|
285
421
|
|
|
286
422
|
// Route DOWN to deepest descendant with a msg Q
|
|
287
|
-
const ctx = this.
|
|
423
|
+
const ctx = this.findDeepestContext();
|
|
288
424
|
if (!ctx)
|
|
289
425
|
throw new Error('No context available');
|
|
290
426
|
|
|
@@ -302,63 +438,8 @@ class Saico {
|
|
|
302
438
|
// ---- Task delegation ----
|
|
303
439
|
|
|
304
440
|
get task() { return this._task; }
|
|
305
|
-
get context() { return this._task?.context || null; }
|
|
306
|
-
get context_id() { return this._task?.context_id || null; }
|
|
307
441
|
get isActive() { return !!this._task && !this._task._completed; }
|
|
308
442
|
|
|
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
443
|
// ---- State Summary ----
|
|
363
444
|
|
|
364
445
|
/**
|
|
@@ -437,8 +518,8 @@ class Saico {
|
|
|
437
518
|
|
|
438
519
|
async closeSession() {
|
|
439
520
|
if (!this._task) return;
|
|
440
|
-
if (this.
|
|
441
|
-
await this.
|
|
521
|
+
if (this.context)
|
|
522
|
+
await this.context.close();
|
|
442
523
|
this._task._ecancel();
|
|
443
524
|
}
|
|
444
525
|
|
|
@@ -555,13 +636,13 @@ class Saico {
|
|
|
555
636
|
if (this._task) {
|
|
556
637
|
data.task = {
|
|
557
638
|
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.
|
|
639
|
+
context_id: this.context_id,
|
|
640
|
+
context: this.context ? {
|
|
641
|
+
tag: this.context.tag,
|
|
642
|
+
msgs: this.context._msgs,
|
|
643
|
+
functions: this.context.functions,
|
|
644
|
+
chat_history: this.context.chat_history,
|
|
645
|
+
tool_digest: this.context.tool_digest,
|
|
565
646
|
} : null,
|
|
566
647
|
};
|
|
567
648
|
}
|
|
@@ -604,13 +685,13 @@ class Saico {
|
|
|
604
685
|
});
|
|
605
686
|
|
|
606
687
|
// Restore messages to context
|
|
607
|
-
if (parsed.task.context?.msgs && instance.
|
|
608
|
-
instance.
|
|
688
|
+
if (parsed.task.context?.msgs && instance.context) {
|
|
689
|
+
instance.context._msgs = parsed.task.context.msgs;
|
|
609
690
|
}
|
|
610
691
|
|
|
611
692
|
// Restore tool_digest
|
|
612
|
-
if (Array.isArray(parsed.task.context?.tool_digest) && instance.
|
|
613
|
-
instance.
|
|
693
|
+
if (Array.isArray(parsed.task.context?.tool_digest) && instance.context) {
|
|
694
|
+
instance.context.tool_digest = parsed.task.context.tool_digest;
|
|
614
695
|
}
|
|
615
696
|
}
|
|
616
697
|
|
|
@@ -618,4 +699,9 @@ class Saico {
|
|
|
618
699
|
}
|
|
619
700
|
}
|
|
620
701
|
|
|
702
|
+
// [BACKEND] explanation text appended to context prompts
|
|
703
|
+
Saico.BACKEND_EXPLANATION = '\nNote: Messages prefixed with [BACKEND] are from the backend ' +
|
|
704
|
+
'server, not the user. They contain server instructions, data updates, or system context. ' +
|
|
705
|
+
'Treat them as authoritative system-level information.';
|
|
706
|
+
|
|
621
707
|
module.exports = { Saico };
|
package/context.js
DELETED