saico 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +287 -297
- package/index.js +2 -9
- package/itask.js +16 -4
- package/msgs.js +106 -99
- package/package.json +1 -2
- package/saico.js +305 -41
- package/sid.js +0 -248
package/index.js
CHANGED
|
@@ -12,15 +12,14 @@
|
|
|
12
12
|
* - Storage persistence (Redis cache + optional DB backend)
|
|
13
13
|
*
|
|
14
14
|
* Main Components:
|
|
15
|
+
* - Saico: Master class (external users extend this)
|
|
15
16
|
* - Itask: Base task class for all tasks (supports states, cancellation, promises)
|
|
16
17
|
* - Context: Conversation context with message handling and tool calls
|
|
17
|
-
* - Sid: Session root task (extends Itask, always has a context)
|
|
18
18
|
* - Store: Storage abstraction layer (Redis + optional backends like DynamoDB)
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
const Itask = require('./itask.js');
|
|
22
22
|
const { Context, createContext } = require('./msgs.js');
|
|
23
|
-
const { Sid, createSid } = require('./sid.js');
|
|
24
23
|
const { Store, DynamoBackend } = require('./store.js');
|
|
25
24
|
const { Saico } = require('./saico.js');
|
|
26
25
|
const { DynamoDBAdapter } = require('./dynamo.js');
|
|
@@ -55,7 +54,6 @@ async function init(config = {}) {
|
|
|
55
54
|
* @param {Object|string} opt - Task options or name string
|
|
56
55
|
* @param {string} opt.name - Task name
|
|
57
56
|
* @param {string} opt.prompt - System prompt (if provided, creates a context)
|
|
58
|
-
* @param {Function} opt.tool_handler - Tool handler function
|
|
59
57
|
* @param {Array} opt.functions - Available functions for AI
|
|
60
58
|
* @param {boolean} opt.cancel - Whether task is cancelable
|
|
61
59
|
* @param {Object} opt.bind - Bind context for state functions
|
|
@@ -81,7 +79,6 @@ function createTask(opt, states = []) {
|
|
|
81
79
|
token_limit: opt.token_limit,
|
|
82
80
|
max_depth: opt.max_depth,
|
|
83
81
|
max_tool_repetition: opt.max_tool_repetition,
|
|
84
|
-
tool_handler: opt.tool_handler,
|
|
85
82
|
functions: opt.functions,
|
|
86
83
|
sequential_mode: opt.sequential_mode
|
|
87
84
|
});
|
|
@@ -100,11 +97,10 @@ function createTask(opt, states = []) {
|
|
|
100
97
|
* @param {string} tag - Context tag identifier
|
|
101
98
|
* @param {number} token_limit - Token limit for summarization
|
|
102
99
|
* @param {Array} msgs - Initial messages
|
|
103
|
-
* @param {Function} tool_handler - Tool handler function
|
|
104
100
|
* @param {Object} config - Additional configuration
|
|
105
101
|
* @returns {Context} Proxied Context instance
|
|
106
102
|
*/
|
|
107
|
-
function createQ(prompt, parent, tag, token_limit, msgs,
|
|
103
|
+
function createQ(prompt, parent, tag, token_limit, msgs, config = {}) {
|
|
108
104
|
// For backward compatibility, if parent is a Context, get its task
|
|
109
105
|
let task = null;
|
|
110
106
|
if (parent && parent.task) {
|
|
@@ -115,7 +111,6 @@ function createQ(prompt, parent, tag, token_limit, msgs, tool_handler, config =
|
|
|
115
111
|
tag,
|
|
116
112
|
token_limit,
|
|
117
113
|
msgs,
|
|
118
|
-
tool_handler,
|
|
119
114
|
...config
|
|
120
115
|
});
|
|
121
116
|
|
|
@@ -144,7 +139,6 @@ module.exports = {
|
|
|
144
139
|
// Core classes
|
|
145
140
|
Itask,
|
|
146
141
|
Context,
|
|
147
|
-
Sid,
|
|
148
142
|
Store,
|
|
149
143
|
DynamoBackend,
|
|
150
144
|
|
|
@@ -153,7 +147,6 @@ module.exports = {
|
|
|
153
147
|
|
|
154
148
|
// Factory functions
|
|
155
149
|
createTask,
|
|
156
|
-
createSid,
|
|
157
150
|
createContext,
|
|
158
151
|
|
|
159
152
|
// Legacy compatibility
|
package/itask.js
CHANGED
|
@@ -110,7 +110,6 @@ function Itask(opt, states){
|
|
|
110
110
|
// Store options for context creation (prompt, functions, etc.)
|
|
111
111
|
this.prompt = opt.prompt;
|
|
112
112
|
this.functions = opt.functions;
|
|
113
|
-
this.tool_handler = opt.tool_handler;
|
|
114
113
|
|
|
115
114
|
// register root if no explicit spawn_parent provided
|
|
116
115
|
// If opt.spawn_parent provided, spawn under it
|
|
@@ -746,9 +745,22 @@ Itask.prototype.closeContext = async function closeContext(){
|
|
|
746
745
|
await this.context.close();
|
|
747
746
|
};
|
|
748
747
|
|
|
749
|
-
//
|
|
750
|
-
|
|
751
|
-
|
|
748
|
+
// Walk DOWN to find the deepest active descendant with a context
|
|
749
|
+
Itask.prototype.findDeepestContext = function findDeepestContext() {
|
|
750
|
+
let deepest = this.context ? { context: this.context, depth: 0 } : null;
|
|
751
|
+
const search = (task, depth) => {
|
|
752
|
+
for (const child of task.child) {
|
|
753
|
+
if (child._completed) continue;
|
|
754
|
+
if (child.context) {
|
|
755
|
+
if (!deepest || depth + 1 >= deepest.depth)
|
|
756
|
+
deepest = { context: child.context, depth: depth + 1 };
|
|
757
|
+
}
|
|
758
|
+
search(child, depth + 1);
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
search(this, 0);
|
|
762
|
+
return deepest ? deepest.context : null;
|
|
763
|
+
};
|
|
752
764
|
|
|
753
765
|
// Reference to Context class (set by index.js to avoid circular dependency)
|
|
754
766
|
Itask.Context = null;
|
package/msgs.js
CHANGED
|
@@ -24,7 +24,6 @@ class Context {
|
|
|
24
24
|
this.token_limit = config.token_limit || 1000000000;
|
|
25
25
|
this.lower_limit = this.token_limit * 0.85;
|
|
26
26
|
this.upper_limit = this.token_limit * 0.98;
|
|
27
|
-
this.tool_handler = config.tool_handler || task?.tool_handler;
|
|
28
27
|
this.functions = config.functions || task?.functions || null;
|
|
29
28
|
|
|
30
29
|
// Recursive depth and repetition control
|
|
@@ -63,45 +62,10 @@ class Context {
|
|
|
63
62
|
// Set the task reference (used when context is created separately)
|
|
64
63
|
setTask(task) {
|
|
65
64
|
this.task = task;
|
|
66
|
-
if (!this.tool_handler)
|
|
67
|
-
this.tool_handler = task?.tool_handler;
|
|
68
65
|
if (!this.functions)
|
|
69
66
|
this.functions = task?.functions;
|
|
70
67
|
}
|
|
71
68
|
|
|
72
|
-
// Overridable: extending classes provide current state summary
|
|
73
|
-
getStateSummary() { return ''; }
|
|
74
|
-
|
|
75
|
-
// Recursively collect state summaries from child tasks that have no context
|
|
76
|
-
// (no msg Q), stopping at children that do have one.
|
|
77
|
-
_collectChildStateSummaries(task) {
|
|
78
|
-
if (!task.child || !task.child.size) return '';
|
|
79
|
-
const parts = [];
|
|
80
|
-
for (const child of task.child) {
|
|
81
|
-
if (child.context) continue; // has its own Q — boundary, stop here
|
|
82
|
-
if (typeof child.getStateSummary === 'function') {
|
|
83
|
-
const s = child.getStateSummary();
|
|
84
|
-
if (s) parts.push(s);
|
|
85
|
-
}
|
|
86
|
-
const nested = this._collectChildStateSummaries(child);
|
|
87
|
-
if (nested) parts.push(nested);
|
|
88
|
-
}
|
|
89
|
-
return parts.join('\n');
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Internal (not overridable): own getStateSummary() + summaries from all
|
|
93
|
-
// contextless descendants, stopping at the first child that has its own Q.
|
|
94
|
-
_getStateSummary() {
|
|
95
|
-
const parts = [];
|
|
96
|
-
const own = this.getStateSummary();
|
|
97
|
-
if (own) parts.push(own);
|
|
98
|
-
if (this.task) {
|
|
99
|
-
const childSummaries = this._collectChildStateSummaries(this.task);
|
|
100
|
-
if (childSummaries) parts.push(childSummaries);
|
|
101
|
-
}
|
|
102
|
-
return parts.join('\n');
|
|
103
|
-
}
|
|
104
|
-
|
|
105
69
|
// Snapshot all public (non-underscore) task properties for dirty detection.
|
|
106
70
|
// Mirrors the observable proxy convention: _ prefix = internal, ignored.
|
|
107
71
|
// Does NOT call serialize() — that is for persistence, not dirty detection.
|
|
@@ -320,10 +284,9 @@ class Context {
|
|
|
320
284
|
|
|
321
285
|
try {
|
|
322
286
|
const correspondingDeferred = deferredGroup.find(d => d.call.id === call.id);
|
|
323
|
-
const handler = correspondingDeferred?.originalMessage.opts.handler || this.tool_handler;
|
|
324
287
|
const timeout = correspondingDeferred?.originalMessage.opts.timeout;
|
|
325
288
|
|
|
326
|
-
result = await this._executeToolCallWithTimeout(call,
|
|
289
|
+
result = await this._executeToolCallWithTimeout(call, timeout);
|
|
327
290
|
if (_snap !== null &&
|
|
328
291
|
_snap !== JSON.stringify(this._snapshotPublicProps(this.task)))
|
|
329
292
|
this._appendToolDigest(call.function.name, result?.content || '');
|
|
@@ -702,7 +665,7 @@ class Context {
|
|
|
702
665
|
}
|
|
703
666
|
}
|
|
704
667
|
|
|
705
|
-
async _executeToolCallWithTimeout(call,
|
|
668
|
+
async _executeToolCallWithTimeout(call, customTimeoutMs = null) {
|
|
706
669
|
const timeoutMs = customTimeoutMs || 5000;
|
|
707
670
|
|
|
708
671
|
return new Promise(async (resolve) => {
|
|
@@ -721,7 +684,7 @@ class Context {
|
|
|
721
684
|
}, timeoutMs);
|
|
722
685
|
|
|
723
686
|
try {
|
|
724
|
-
const result = await this.interpretAndApplyChanges(call
|
|
687
|
+
const result = await this.interpretAndApplyChanges(call);
|
|
725
688
|
|
|
726
689
|
if (!completed) {
|
|
727
690
|
completed = true;
|
|
@@ -822,52 +785,31 @@ class Context {
|
|
|
822
785
|
return msgs.slice(startIdx);
|
|
823
786
|
}
|
|
824
787
|
|
|
825
|
-
// Build message queue
|
|
826
|
-
//
|
|
827
|
-
//
|
|
828
|
-
//
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
const prompt = {role: 'system', content: ctx.prompt};
|
|
838
|
-
if (add_tag) prompt.tag = ctx.tag;
|
|
788
|
+
// Build message queue.
|
|
789
|
+
// When preamble is provided (by Saico orchestrator), it is prepended as-is
|
|
790
|
+
// and does NOT count against QUEUE_LIMIT. Otherwise falls back to standalone
|
|
791
|
+
// behavior: own prompt + tool digest + own messages.
|
|
792
|
+
_createMsgQ(preamble, add_tag, tag_filter) {
|
|
793
|
+
const fullQueue = [...(preamble || [])];
|
|
794
|
+
|
|
795
|
+
// Standalone fallback — when no preamble provided, add own prompt/digest
|
|
796
|
+
if (!preamble) {
|
|
797
|
+
if (this.prompt) {
|
|
798
|
+
const prompt = {role: 'system', content: this.prompt};
|
|
799
|
+
if (add_tag) prompt.tag = this.tag;
|
|
839
800
|
fullQueue.push(prompt);
|
|
840
801
|
}
|
|
841
|
-
const ctxSummary = ctx._getStateSummary();
|
|
842
|
-
if (ctxSummary)
|
|
843
|
-
fullQueue.push({role: 'system', content: '[State Summary]\n' + ctxSummary});
|
|
844
|
-
}
|
|
845
|
-
if (this.prompt) {
|
|
846
|
-
const prompt = {role: 'system', content: this.prompt};
|
|
847
|
-
if (add_tag) prompt.tag = this.tag;
|
|
848
|
-
fullQueue.push(prompt);
|
|
849
|
-
}
|
|
850
|
-
const stateSummary = this._getStateSummary();
|
|
851
|
-
if (stateSummary)
|
|
852
|
-
fullQueue.push({role: 'system', content: '[State Summary]\n' + stateSummary});
|
|
853
|
-
|
|
854
|
-
// Layer 3: Tool digest (if non-empty)
|
|
855
|
-
if (this.tool_digest.length > 0) {
|
|
856
|
-
const digestText = this.tool_digest.map(entry =>
|
|
857
|
-
`[${new Date(entry.tm).toISOString()}] ${entry.tool}: ${entry.result}`
|
|
858
|
-
).join('\n');
|
|
859
|
-
fullQueue.push({role: 'system', content: '[Tool Activity Log]\n' + digestText});
|
|
860
|
-
}
|
|
861
802
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
.
|
|
866
|
-
.
|
|
867
|
-
|
|
803
|
+
if (this.tool_digest.length > 0) {
|
|
804
|
+
const digestText = this.tool_digest.map(entry =>
|
|
805
|
+
`[${new Date(entry.tm).toISOString()}] ${entry.tool}: ${entry.result}`
|
|
806
|
+
).join('\n');
|
|
807
|
+
fullQueue.push({role: 'system', content: '[Tool Activity Log]\n' + digestText});
|
|
808
|
+
}
|
|
868
809
|
}
|
|
869
810
|
|
|
870
811
|
// Own messages — filter by tag if requested, then slice to QUEUE_LIMIT
|
|
812
|
+
// QUEUE_LIMIT only applies here — preamble is not counted
|
|
871
813
|
let my_msgs;
|
|
872
814
|
if (tag_filter !== undefined) {
|
|
873
815
|
my_msgs = this._msgs.filter(m => {
|
|
@@ -948,7 +890,7 @@ class Context {
|
|
|
948
890
|
_debugQDump(Q, functions) {
|
|
949
891
|
if (util.is_mocha && process.env.PROD)
|
|
950
892
|
return;
|
|
951
|
-
const dbgQ = Q || this._createMsgQ(true);
|
|
893
|
+
const dbgQ = Q || this._createMsgQ(null, true);
|
|
952
894
|
if (debug) {
|
|
953
895
|
console.log('MSGQDEBUG - Q:', JSON.stringify(dbgQ.map(m => ({
|
|
954
896
|
role: m.role,
|
|
@@ -973,14 +915,22 @@ class Context {
|
|
|
973
915
|
this._processWaitingQueue();
|
|
974
916
|
}
|
|
975
917
|
|
|
976
|
-
Q = this._createMsgQ(false, o.opts?.tag);
|
|
918
|
+
Q = this._createMsgQ(o.opts?._preamble, false, o.opts?.tag);
|
|
977
919
|
|
|
978
|
-
//
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
920
|
+
// Use aggregated functions from Saico if provided, else fall back to own
|
|
921
|
+
let funcs;
|
|
922
|
+
if (o.opts?._aggregatedFunctions) {
|
|
923
|
+
const messageFuncs = o.functions || [];
|
|
924
|
+
funcs = [...o.opts._aggregatedFunctions, ...messageFuncs].length > 0
|
|
925
|
+
? [...o.opts._aggregatedFunctions, ...messageFuncs]
|
|
926
|
+
: null;
|
|
927
|
+
} else {
|
|
928
|
+
const hierarchyFuncs = this.getFunctions() || [];
|
|
929
|
+
const messageFuncs = o.functions || [];
|
|
930
|
+
funcs = [...hierarchyFuncs, ...messageFuncs].length > 0
|
|
931
|
+
? [...hierarchyFuncs, ...messageFuncs]
|
|
932
|
+
: null;
|
|
933
|
+
}
|
|
984
934
|
|
|
985
935
|
if (debug)
|
|
986
936
|
this._debugQDump(Q, funcs);
|
|
@@ -1051,7 +1001,7 @@ class Context {
|
|
|
1051
1001
|
? JSON.stringify(this._snapshotPublicProps(this.task)) : null;
|
|
1052
1002
|
try {
|
|
1053
1003
|
const result = await this._executeToolCallWithTimeout(
|
|
1054
|
-
call, o.opts?.
|
|
1004
|
+
call, o.opts?.timeout);
|
|
1055
1005
|
const item = toolCallsWithResults.find(item => item.call.id === call.id);
|
|
1056
1006
|
if (item) item.result = result;
|
|
1057
1007
|
if (_snap !== null &&
|
|
@@ -1110,27 +1060,84 @@ class Context {
|
|
|
1110
1060
|
}
|
|
1111
1061
|
}
|
|
1112
1062
|
|
|
1113
|
-
|
|
1114
|
-
|
|
1063
|
+
/**
|
|
1064
|
+
* Search the Saico hierarchy for a TOOL_<toolName> method.
|
|
1065
|
+
* Order: current task → walk UP parents → walk DOWN children (BFS).
|
|
1066
|
+
*/
|
|
1067
|
+
_findToolImplementation(toolName) {
|
|
1068
|
+
const methodName = 'TOOL_' + toolName;
|
|
1069
|
+
const check = (task) =>
|
|
1070
|
+
task?._saico && typeof task._saico[methodName] === 'function' ? task._saico : null;
|
|
1071
|
+
|
|
1072
|
+
// 1. Current task
|
|
1073
|
+
let found = check(this.task);
|
|
1074
|
+
if (found) return { saico: found, methodName };
|
|
1075
|
+
|
|
1076
|
+
// 2. Walk UP parent chain
|
|
1077
|
+
let t = this.task?.parent;
|
|
1078
|
+
while (t) {
|
|
1079
|
+
found = check(t);
|
|
1080
|
+
if (found) return { saico: found, methodName };
|
|
1081
|
+
t = t.parent;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// 3. Walk DOWN from this.task (BFS)
|
|
1085
|
+
if (this.task) {
|
|
1086
|
+
const queue = [...this.task.child];
|
|
1087
|
+
while (queue.length > 0) {
|
|
1088
|
+
const child = queue.shift();
|
|
1089
|
+
if (child._completed) continue;
|
|
1090
|
+
found = check(child);
|
|
1091
|
+
if (found) return { saico: found, methodName };
|
|
1092
|
+
if (child.child?.size > 0) queue.push(...child.child);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
async interpretAndApplyChanges(call) {
|
|
1115
1100
|
if (!call)
|
|
1116
1101
|
return { content: '', functions: null };
|
|
1117
1102
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1103
|
+
const toolName = call.function.name;
|
|
1104
|
+
_log('apply tool', toolName);
|
|
1105
|
+
|
|
1106
|
+
const impl = this._findToolImplementation(toolName);
|
|
1107
|
+
if (!impl) {
|
|
1108
|
+
_log('No TOOL_ method found for:', toolName);
|
|
1109
|
+
return {
|
|
1110
|
+
content: `Error: No implementation found for tool "${toolName}". ` +
|
|
1111
|
+
`Expected a TOOL_${toolName}(args) method on a Saico instance in the hierarchy.`,
|
|
1112
|
+
functions: null
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
_log('invoking TOOL_' + toolName, 'on', impl.saico.name || impl.saico.constructor.name);
|
|
1117
|
+
|
|
1118
|
+
let parsedArgs;
|
|
1119
|
+
try {
|
|
1120
|
+
parsedArgs = JSON.parse(call.function.arguments);
|
|
1121
|
+
} catch (e) {
|
|
1122
|
+
return {
|
|
1123
|
+
content: `Error: Failed to parse arguments for tool "${toolName}": ${e.message}`,
|
|
1124
|
+
functions: null
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
let result = await impl.saico[impl.methodName](parsedArgs);
|
|
1121
1129
|
|
|
1122
1130
|
let content = result?.content || result || '';
|
|
1123
1131
|
let functions = result?.functions || null;
|
|
1124
1132
|
|
|
1125
1133
|
if (content && typeof content !== 'string')
|
|
1126
1134
|
content = JSON.stringify(content);
|
|
1127
|
-
else if (!content)
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
+`from the user`;
|
|
1135
|
+
else if (!content) {
|
|
1136
|
+
content = `tool call ${toolName} ${call.id} completed. do not reply. wait for the next msg `
|
|
1137
|
+
+ `from the user`;
|
|
1131
1138
|
}
|
|
1132
1139
|
|
|
1133
|
-
_log('FUNCTION RESULT',
|
|
1140
|
+
_log('FUNCTION RESULT', toolName, call.id, content.substring(0, 50) + '...',
|
|
1134
1141
|
functions ? 'with functions' : 'no functions');
|
|
1135
1142
|
return { content, functions };
|
|
1136
1143
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "saico",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"description": "Hierarchical AI Conversation Orchestrator - Task hierarchy with conversation contexts",
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
"itask.js",
|
|
18
18
|
"context.js",
|
|
19
19
|
"msgs.js",
|
|
20
|
-
"sid.js",
|
|
21
20
|
"saico.js",
|
|
22
21
|
"dynamo.js",
|
|
23
22
|
"openai.js",
|