saico 2.1.0 → 2.2.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/context.js +113 -31
- package/itask.js +9 -2
- package/package.json +1 -1
- package/sid.js +7 -1
package/context.js
CHANGED
|
@@ -46,6 +46,14 @@ class Context {
|
|
|
46
46
|
this._processing_sequential = false;
|
|
47
47
|
this._sequential_mode = config.sequential_mode || false;
|
|
48
48
|
|
|
49
|
+
// Queue structure limits
|
|
50
|
+
this.QUEUE_LIMIT = config.queue_limit ?? 30;
|
|
51
|
+
this.TOOL_DIGEST_LIMIT = config.tool_digest_limit ?? 10;
|
|
52
|
+
this.MIN_CHAT_MESSAGES = config.min_chat_messages ?? 10;
|
|
53
|
+
|
|
54
|
+
// Tool digest — persistent history of tool calls that mutated task state
|
|
55
|
+
this.tool_digest = config.tool_digest || [];
|
|
56
|
+
|
|
49
57
|
// Initialize messages if provided
|
|
50
58
|
(config.msgs || []).forEach(m => this.push(m));
|
|
51
59
|
|
|
@@ -61,6 +69,35 @@ class Context {
|
|
|
61
69
|
this.functions = task?.functions;
|
|
62
70
|
}
|
|
63
71
|
|
|
72
|
+
// Overridable: extending classes provide current state summary
|
|
73
|
+
getStateSummary() { return ''; }
|
|
74
|
+
|
|
75
|
+
// Snapshot all public (non-underscore) task properties for dirty detection.
|
|
76
|
+
// Mirrors the observable proxy convention: _ prefix = internal, ignored.
|
|
77
|
+
// Does NOT call serialize() — that is for persistence, not dirty detection.
|
|
78
|
+
_snapshotPublicProps(obj, seen = new Set()) {
|
|
79
|
+
if (typeof obj !== 'object' || obj === null) return obj;
|
|
80
|
+
if (seen.has(obj)) return undefined; // circular ref — skip
|
|
81
|
+
seen.add(obj);
|
|
82
|
+
const out = Array.isArray(obj) ? [] : {};
|
|
83
|
+
for (const key of Object.keys(obj)) {
|
|
84
|
+
if (!key.startsWith('_') && typeof obj[key] !== 'function')
|
|
85
|
+
out[key] = this._snapshotPublicProps(obj[key], seen);
|
|
86
|
+
}
|
|
87
|
+
seen.delete(obj);
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Append a tool result to the persistent tool digest
|
|
92
|
+
_appendToolDigest(toolName, resultContent) {
|
|
93
|
+
const truncated = typeof resultContent === 'string'
|
|
94
|
+
? resultContent.slice(0, 500)
|
|
95
|
+
: String(resultContent ?? '').slice(0, 500);
|
|
96
|
+
this.tool_digest.push({ tool: toolName, result: truncated, tm: Date.now() });
|
|
97
|
+
if (this.tool_digest.length > this.TOOL_DIGEST_LIMIT)
|
|
98
|
+
this.tool_digest = this.tool_digest.slice(-this.TOOL_DIGEST_LIMIT);
|
|
99
|
+
}
|
|
100
|
+
|
|
64
101
|
// Get the parent context by traversing task hierarchy
|
|
65
102
|
getParentContext() {
|
|
66
103
|
if (!this.task || !this.task.parent)
|
|
@@ -248,6 +285,8 @@ class Context {
|
|
|
248
285
|
};
|
|
249
286
|
} else {
|
|
250
287
|
this._trackActiveToolCall(call);
|
|
288
|
+
const _snap = this.task
|
|
289
|
+
? JSON.stringify(this._snapshotPublicProps(this.task)) : null;
|
|
251
290
|
|
|
252
291
|
try {
|
|
253
292
|
const correspondingDeferred = deferredGroup.find(d => d.call.id === call.id);
|
|
@@ -255,6 +294,9 @@ class Context {
|
|
|
255
294
|
const timeout = correspondingDeferred?.originalMessage.opts.timeout;
|
|
256
295
|
|
|
257
296
|
result = await this._executeToolCallWithTimeout(call, handler, timeout);
|
|
297
|
+
if (_snap !== null &&
|
|
298
|
+
_snap !== JSON.stringify(this._snapshotPublicProps(this.task)))
|
|
299
|
+
this._appendToolDigest(call.function.name, result?.content || '');
|
|
258
300
|
} finally {
|
|
259
301
|
this._completeActiveToolCall(call);
|
|
260
302
|
}
|
|
@@ -405,7 +447,11 @@ class Context {
|
|
|
405
447
|
if (!store || !this.tag)
|
|
406
448
|
return;
|
|
407
449
|
const data = await store.load(this.tag);
|
|
408
|
-
if (!data
|
|
450
|
+
if (!data)
|
|
451
|
+
return;
|
|
452
|
+
if (Array.isArray(data.tool_digest))
|
|
453
|
+
this.tool_digest = data.tool_digest;
|
|
454
|
+
if (!data.chat_history)
|
|
409
455
|
return;
|
|
410
456
|
const messages = await util.decompressMessages(data.chat_history);
|
|
411
457
|
if (!Array.isArray(messages) || messages.length === 0)
|
|
@@ -718,50 +764,82 @@ class Context {
|
|
|
718
764
|
return validatedMsgs;
|
|
719
765
|
}
|
|
720
766
|
|
|
721
|
-
//
|
|
767
|
+
// Slice the last `limit` messages, walking back to avoid orphaning tool responses
|
|
768
|
+
// and expanding if fewer than MIN_CHAT_MESSAGES user/assistant messages are included.
|
|
769
|
+
_getQueueSlice(msgs, limit) {
|
|
770
|
+
if (msgs.length <= limit) return msgs;
|
|
771
|
+
|
|
772
|
+
let startIdx = msgs.length - limit;
|
|
773
|
+
|
|
774
|
+
// Walk backward if we'd start mid-tool-pair (tool response without its call)
|
|
775
|
+
while (startIdx > 0 && msgs[startIdx] && msgs[startIdx].role === 'tool')
|
|
776
|
+
startIdx--;
|
|
777
|
+
|
|
778
|
+
// Count user/assistant messages in the current window
|
|
779
|
+
let chatCount = 0;
|
|
780
|
+
for (let i = startIdx; i < msgs.length; i++) {
|
|
781
|
+
const role = msgs[i].role;
|
|
782
|
+
if (role === 'user' || role === 'assistant') chatCount++;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Expand backward until we have at least MIN_CHAT_MESSAGES chat messages
|
|
786
|
+
while (chatCount < this.MIN_CHAT_MESSAGES && startIdx > 0) {
|
|
787
|
+
startIdx--;
|
|
788
|
+
const role = msgs[startIdx].role;
|
|
789
|
+
if (role === 'user' || role === 'assistant') chatCount++;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
return msgs.slice(startIdx);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Build message queue — layered structure:
|
|
796
|
+
// Layer 1: System prompts from ancestor hierarchy + own prompt (transient)
|
|
797
|
+
// Layer 2: [State Summary] from getStateSummary() override (transient, if non-empty)
|
|
798
|
+
// Layer 3: [Tool Activity Log] from tool_digest (transient, if non-empty)
|
|
799
|
+
// Layer 4: Ancestor summaries only + last QUEUE_LIMIT of own messages (persistent)
|
|
722
800
|
_createMsgQ(add_tag, tag_filter) {
|
|
723
801
|
const fullQueue = [];
|
|
724
|
-
|
|
725
|
-
// Get messages from ancestor contexts via task hierarchy
|
|
726
802
|
const ancestorContexts = this.getAncestorContexts();
|
|
803
|
+
|
|
804
|
+
// Layer 1: System prompts from ancestor hierarchy + own prompt
|
|
727
805
|
for (const ctx of ancestorContexts) {
|
|
728
806
|
if (ctx.prompt) {
|
|
729
807
|
const prompt = {role: 'system', content: ctx.prompt};
|
|
730
|
-
if (add_tag)
|
|
731
|
-
prompt.tag = ctx.tag;
|
|
808
|
+
if (add_tag) prompt.tag = ctx.tag;
|
|
732
809
|
fullQueue.push(prompt);
|
|
733
810
|
}
|
|
734
|
-
|
|
735
|
-
let ctxMsgs;
|
|
736
|
-
if (tag_filter !== undefined) {
|
|
737
|
-
ctxMsgs = ctx._msgs.filter(m => {
|
|
738
|
-
if (m.msg.role === 'system') return true;
|
|
739
|
-
if (m.opts.summary) return true;
|
|
740
|
-
if (m.opts.tag === tag_filter) return true;
|
|
741
|
-
return false;
|
|
742
|
-
}).map(m => m.msg);
|
|
743
|
-
} else {
|
|
744
|
-
ctxMsgs = ctx.__msgs;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
if (add_tag)
|
|
748
|
-
ctxMsgs = ctxMsgs.map(m => Object.assign({tag: ctx.tag}, m));
|
|
749
|
-
|
|
750
|
-
fullQueue.push(...ctxMsgs);
|
|
751
811
|
}
|
|
752
|
-
|
|
753
|
-
// Add this context's prompt and messages
|
|
754
812
|
if (this.prompt) {
|
|
755
813
|
const prompt = {role: 'system', content: this.prompt};
|
|
756
|
-
if (add_tag)
|
|
757
|
-
prompt.tag = this.tag;
|
|
814
|
+
if (add_tag) prompt.tag = this.tag;
|
|
758
815
|
fullQueue.push(prompt);
|
|
759
816
|
}
|
|
760
817
|
|
|
818
|
+
// Layer 2: State summary (if non-empty)
|
|
819
|
+
const stateSummary = this.getStateSummary();
|
|
820
|
+
if (stateSummary)
|
|
821
|
+
fullQueue.push({role: 'system', content: '[State Summary]\n' + stateSummary});
|
|
822
|
+
|
|
823
|
+
// Layer 3: Tool digest (if non-empty)
|
|
824
|
+
if (this.tool_digest.length > 0) {
|
|
825
|
+
const digestText = this.tool_digest.map(entry =>
|
|
826
|
+
`[${new Date(entry.tm).toISOString()}] ${entry.tool}: ${entry.result}`
|
|
827
|
+
).join('\n');
|
|
828
|
+
fullQueue.push({role: 'system', content: '[Tool Activity Log]\n' + digestText});
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Layer 4: Ancestor summaries only (no full ancestor messages)
|
|
832
|
+
for (const ctx of ancestorContexts) {
|
|
833
|
+
const summaries = ctx._msgs
|
|
834
|
+
.filter(m => m.opts.summary)
|
|
835
|
+
.map(m => add_tag ? Object.assign({}, m.msg, {tag: ctx.tag}) : m.msg);
|
|
836
|
+
fullQueue.push(...summaries);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Own messages — filter by tag if requested, then slice to QUEUE_LIMIT
|
|
761
840
|
let my_msgs;
|
|
762
841
|
if (tag_filter !== undefined) {
|
|
763
842
|
my_msgs = this._msgs.filter(m => {
|
|
764
|
-
if (m.msg.role === 'system') return true;
|
|
765
843
|
if (m.opts.summary) return true;
|
|
766
844
|
if (m.opts.tag === tag_filter) return true;
|
|
767
845
|
return false;
|
|
@@ -771,9 +849,9 @@ class Context {
|
|
|
771
849
|
}
|
|
772
850
|
|
|
773
851
|
if (add_tag)
|
|
774
|
-
my_msgs = my_msgs.map(m => Object.assign({tag: this.tag}
|
|
852
|
+
my_msgs = my_msgs.map(m => Object.assign({}, m, {tag: this.tag}));
|
|
775
853
|
|
|
776
|
-
fullQueue.push(...my_msgs);
|
|
854
|
+
fullQueue.push(...this._getQueueSlice(my_msgs, this.QUEUE_LIMIT));
|
|
777
855
|
|
|
778
856
|
return this._validateToolResponses(fullQueue);
|
|
779
857
|
}
|
|
@@ -864,7 +942,6 @@ class Context {
|
|
|
864
942
|
this._processWaitingQueue();
|
|
865
943
|
}
|
|
866
944
|
|
|
867
|
-
await this.summarizeMessages();
|
|
868
945
|
Q = this._createMsgQ(false, o.opts?.tag);
|
|
869
946
|
|
|
870
947
|
// Aggregate functions from hierarchy and merge with message-specific functions
|
|
@@ -939,11 +1016,16 @@ class Context {
|
|
|
939
1016
|
|
|
940
1017
|
for (const { call, isDuplicate } of toolCallsWithResults) {
|
|
941
1018
|
if (!isDuplicate) {
|
|
1019
|
+
const _snap = this.task
|
|
1020
|
+
? JSON.stringify(this._snapshotPublicProps(this.task)) : null;
|
|
942
1021
|
try {
|
|
943
1022
|
const result = await this._executeToolCallWithTimeout(
|
|
944
1023
|
call, o.opts?.handler, o.opts?.timeout);
|
|
945
1024
|
const item = toolCallsWithResults.find(item => item.call.id === call.id);
|
|
946
1025
|
if (item) item.result = result;
|
|
1026
|
+
if (_snap !== null &&
|
|
1027
|
+
_snap !== JSON.stringify(this._snapshotPublicProps(this.task)))
|
|
1028
|
+
this._appendToolDigest(call.function.name, result?.content || '');
|
|
947
1029
|
} finally {
|
|
948
1030
|
this._completeActiveToolCall(call);
|
|
949
1031
|
}
|
package/itask.js
CHANGED
|
@@ -720,8 +720,14 @@ Itask.prototype.closeContext = async function closeContext(){
|
|
|
720
720
|
return true;
|
|
721
721
|
}).map(m => m.msg);
|
|
722
722
|
|
|
723
|
-
|
|
724
|
-
|
|
723
|
+
// Trim to last QUEUE_LIMIT before persisting
|
|
724
|
+
const queueLimit = this.context.QUEUE_LIMIT || 30;
|
|
725
|
+
const trimmedMsgs = cleanedMsgs.length > queueLimit
|
|
726
|
+
? cleanedMsgs.slice(-queueLimit)
|
|
727
|
+
: cleanedMsgs;
|
|
728
|
+
|
|
729
|
+
if (trimmedMsgs.length > 0) {
|
|
730
|
+
const chat_history = await util.compressMessages(trimmedMsgs);
|
|
725
731
|
this.context.chat_history = chat_history;
|
|
726
732
|
|
|
727
733
|
// Persist to store
|
|
@@ -729,6 +735,7 @@ Itask.prototype.closeContext = async function closeContext(){
|
|
|
729
735
|
if (store && this.context_id) {
|
|
730
736
|
await store.save(this.context_id, {
|
|
731
737
|
chat_history,
|
|
738
|
+
tool_digest: this.context.tool_digest || [],
|
|
732
739
|
prompt: this.context.prompt,
|
|
733
740
|
tag: this.context.tag,
|
|
734
741
|
tm_closed: Date.now()
|
package/package.json
CHANGED
package/sid.js
CHANGED
|
@@ -96,7 +96,8 @@ class Sid extends Itask {
|
|
|
96
96
|
tag: this.context.tag,
|
|
97
97
|
msgs: this.context._msgs,
|
|
98
98
|
functions: this.context.functions,
|
|
99
|
-
chat_history: this.context.chat_history
|
|
99
|
+
chat_history: this.context.chat_history,
|
|
100
|
+
tool_digest: this.context.tool_digest
|
|
100
101
|
},
|
|
101
102
|
tm_create: this.tm_create
|
|
102
103
|
});
|
|
@@ -130,6 +131,11 @@ class Sid extends Itask {
|
|
|
130
131
|
sid.context._msgs = parsed.context.msgs;
|
|
131
132
|
}
|
|
132
133
|
|
|
134
|
+
// Restore tool_digest
|
|
135
|
+
if (Array.isArray(parsed.context?.tool_digest)) {
|
|
136
|
+
sid.context.tool_digest = parsed.context.tool_digest;
|
|
137
|
+
}
|
|
138
|
+
|
|
133
139
|
// Load history from store if available
|
|
134
140
|
if (opt.store && parsed.context?.chat_history) {
|
|
135
141
|
sid.context.chat_history = parsed.context.chat_history;
|