saico 2.1.0 → 2.2.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/context.js +114 -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,83 @@ 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+2: Each level's prompt followed immediately by its state summary
|
|
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
|
-
|
|
736
|
-
|
|
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);
|
|
811
|
+
const ctxSummary = ctx.getStateSummary();
|
|
812
|
+
if (ctxSummary)
|
|
813
|
+
fullQueue.push({role: 'system', content: '[State Summary]\n' + ctxSummary});
|
|
751
814
|
}
|
|
752
|
-
|
|
753
|
-
// Add this context's prompt and messages
|
|
754
815
|
if (this.prompt) {
|
|
755
816
|
const prompt = {role: 'system', content: this.prompt};
|
|
756
|
-
if (add_tag)
|
|
757
|
-
prompt.tag = this.tag;
|
|
817
|
+
if (add_tag) prompt.tag = this.tag;
|
|
758
818
|
fullQueue.push(prompt);
|
|
759
819
|
}
|
|
820
|
+
const stateSummary = this.getStateSummary();
|
|
821
|
+
if (stateSummary)
|
|
822
|
+
fullQueue.push({role: 'system', content: '[State Summary]\n' + stateSummary});
|
|
823
|
+
|
|
824
|
+
// Layer 3: Tool digest (if non-empty)
|
|
825
|
+
if (this.tool_digest.length > 0) {
|
|
826
|
+
const digestText = this.tool_digest.map(entry =>
|
|
827
|
+
`[${new Date(entry.tm).toISOString()}] ${entry.tool}: ${entry.result}`
|
|
828
|
+
).join('\n');
|
|
829
|
+
fullQueue.push({role: 'system', content: '[Tool Activity Log]\n' + digestText});
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Layer 4: Ancestor summaries only (no full ancestor messages)
|
|
833
|
+
for (const ctx of ancestorContexts) {
|
|
834
|
+
const summaries = ctx._msgs
|
|
835
|
+
.filter(m => m.opts.summary)
|
|
836
|
+
.map(m => add_tag ? Object.assign({}, m.msg, {tag: ctx.tag}) : m.msg);
|
|
837
|
+
fullQueue.push(...summaries);
|
|
838
|
+
}
|
|
760
839
|
|
|
840
|
+
// Own messages — filter by tag if requested, then slice to QUEUE_LIMIT
|
|
761
841
|
let my_msgs;
|
|
762
842
|
if (tag_filter !== undefined) {
|
|
763
843
|
my_msgs = this._msgs.filter(m => {
|
|
764
|
-
if (m.msg.role === 'system') return true;
|
|
765
844
|
if (m.opts.summary) return true;
|
|
766
845
|
if (m.opts.tag === tag_filter) return true;
|
|
767
846
|
return false;
|
|
@@ -771,9 +850,9 @@ class Context {
|
|
|
771
850
|
}
|
|
772
851
|
|
|
773
852
|
if (add_tag)
|
|
774
|
-
my_msgs = my_msgs.map(m => Object.assign({tag: this.tag}
|
|
853
|
+
my_msgs = my_msgs.map(m => Object.assign({}, m, {tag: this.tag}));
|
|
775
854
|
|
|
776
|
-
fullQueue.push(...my_msgs);
|
|
855
|
+
fullQueue.push(...this._getQueueSlice(my_msgs, this.QUEUE_LIMIT));
|
|
777
856
|
|
|
778
857
|
return this._validateToolResponses(fullQueue);
|
|
779
858
|
}
|
|
@@ -864,7 +943,6 @@ class Context {
|
|
|
864
943
|
this._processWaitingQueue();
|
|
865
944
|
}
|
|
866
945
|
|
|
867
|
-
await this.summarizeMessages();
|
|
868
946
|
Q = this._createMsgQ(false, o.opts?.tag);
|
|
869
947
|
|
|
870
948
|
// Aggregate functions from hierarchy and merge with message-specific functions
|
|
@@ -939,11 +1017,16 @@ class Context {
|
|
|
939
1017
|
|
|
940
1018
|
for (const { call, isDuplicate } of toolCallsWithResults) {
|
|
941
1019
|
if (!isDuplicate) {
|
|
1020
|
+
const _snap = this.task
|
|
1021
|
+
? JSON.stringify(this._snapshotPublicProps(this.task)) : null;
|
|
942
1022
|
try {
|
|
943
1023
|
const result = await this._executeToolCallWithTimeout(
|
|
944
1024
|
call, o.opts?.handler, o.opts?.timeout);
|
|
945
1025
|
const item = toolCallsWithResults.find(item => item.call.id === call.id);
|
|
946
1026
|
if (item) item.result = result;
|
|
1027
|
+
if (_snap !== null &&
|
|
1028
|
+
_snap !== JSON.stringify(this._snapshotPublicProps(this.task)))
|
|
1029
|
+
this._appendToolDigest(call.function.name, result?.content || '');
|
|
947
1030
|
} finally {
|
|
948
1031
|
this._completeActiveToolCall(call);
|
|
949
1032
|
}
|
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;
|