@leverageaiapps/locus 2.2.9 → 2.3.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/dist/agent/agent-loop.d.ts.map +1 -1
- package/dist/agent/agent-loop.js +350 -24
- package/dist/agent/agent-loop.js.map +1 -1
- package/dist/agent/claude-client.d.ts.map +1 -1
- package/dist/agent/claude-client.js +16 -9
- package/dist/agent/claude-client.js.map +1 -1
- package/dist/agent/compaction.d.ts +22 -0
- package/dist/agent/compaction.d.ts.map +1 -1
- package/dist/agent/compaction.js +274 -0
- package/dist/agent/compaction.js.map +1 -1
- package/dist/agent/types.d.ts +2 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/agent/worker-proxy.d.ts +7 -2
- package/dist/agent/worker-proxy.d.ts.map +1 -1
- package/dist/agent/worker-proxy.js +5 -2
- package/dist/agent/worker-proxy.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-loop.d.ts","sourceRoot":"","sources":["../../src/agent/agent-loop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;
|
|
1
|
+
{"version":3,"file":"agent-loop.d.ts","sourceRoot":"","sources":["../../src/agent/agent-loop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAsBH,OAAO,KAAK,EACV,eAAe,EAChB,MAAM,SAAS,CAAC;AA+KjB,wBAAgB,iBAAiB,IAAI,OAAO,CAI3C;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAI7E;AAED,wBAAgB,SAAS,IAAI,OAAO,CAEnC;AAsCD,wBAAsB,YAAY,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAykBtE"}
|
package/dist/agent/agent-loop.js
CHANGED
|
@@ -52,6 +52,8 @@ exports.isRunning = isRunning;
|
|
|
52
52
|
exports.runAgentLoop = runAgentLoop;
|
|
53
53
|
const promises_1 = require("node:fs/promises");
|
|
54
54
|
const node_os_1 = require("node:os");
|
|
55
|
+
const node_path_1 = require("node:path");
|
|
56
|
+
const node_child_process_1 = require("node:child_process");
|
|
55
57
|
const worker_proxy_1 = require("./worker-proxy");
|
|
56
58
|
const claude_client_1 = require("./claude-client");
|
|
57
59
|
const tool_defs_1 = require("./tool-defs");
|
|
@@ -61,6 +63,167 @@ const types_1 = require("./types");
|
|
|
61
63
|
const crypto = __importStar(require("node:crypto"));
|
|
62
64
|
// ─── Memory Path ────────────────────────────────────────────────
|
|
63
65
|
const MEMORY_DIR = `${(0, node_os_1.homedir)()}/.leverage/memory`;
|
|
66
|
+
// ─── Platform Detection (ported from container-agent) ────────────
|
|
67
|
+
function detectLocalPlatform() {
|
|
68
|
+
const p = (0, node_os_1.platform)();
|
|
69
|
+
if (p === 'darwin')
|
|
70
|
+
return 'macOS';
|
|
71
|
+
if (p === 'win32')
|
|
72
|
+
return 'Windows';
|
|
73
|
+
if (p === 'linux')
|
|
74
|
+
return 'Linux';
|
|
75
|
+
return p;
|
|
76
|
+
}
|
|
77
|
+
function macosVersion() {
|
|
78
|
+
try {
|
|
79
|
+
const res = (0, node_child_process_1.spawnSync)('sw_vers', ['-productVersion'], {
|
|
80
|
+
encoding: 'utf-8', timeout: 1000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
81
|
+
});
|
|
82
|
+
const out = typeof res.stdout === 'string' ? res.stdout.trim() : '';
|
|
83
|
+
return out || (0, node_os_1.release)();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return (0, node_os_1.release)();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function detectShell() {
|
|
90
|
+
if ((0, node_os_1.platform)() === 'win32') {
|
|
91
|
+
try {
|
|
92
|
+
const res = (0, node_child_process_1.spawnSync)('pwsh', ['--version'], {
|
|
93
|
+
encoding: 'utf-8', timeout: 1000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
94
|
+
});
|
|
95
|
+
if (res.status === 0)
|
|
96
|
+
return 'pwsh';
|
|
97
|
+
}
|
|
98
|
+
catch { }
|
|
99
|
+
return 'powershell';
|
|
100
|
+
}
|
|
101
|
+
const envShell = process.env.SHELL?.trim();
|
|
102
|
+
return envShell ? (0, node_path_1.basename)(envShell) : 'sh';
|
|
103
|
+
}
|
|
104
|
+
/** Build Runtime line for system prompt (like OpenClaw's buildRuntimeLine) */
|
|
105
|
+
function buildRuntimeLine() {
|
|
106
|
+
const p = (0, node_os_1.platform)();
|
|
107
|
+
const parts = [];
|
|
108
|
+
try {
|
|
109
|
+
parts.push(`host=${(0, node_os_1.hostname)()}`);
|
|
110
|
+
}
|
|
111
|
+
catch { }
|
|
112
|
+
let osLabel;
|
|
113
|
+
if (p === 'darwin') {
|
|
114
|
+
osLabel = `macOS ${macosVersion()}`;
|
|
115
|
+
}
|
|
116
|
+
else if (p === 'win32') {
|
|
117
|
+
osLabel = `Windows ${(0, node_os_1.release)()}`;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
osLabel = `${p} ${(0, node_os_1.release)()}`;
|
|
121
|
+
}
|
|
122
|
+
parts.push(`os=${osLabel} (${(0, node_os_1.arch)()})`);
|
|
123
|
+
parts.push(`shell=${detectShell()}`);
|
|
124
|
+
parts.push(`node=${process.version}`);
|
|
125
|
+
parts.push(`home=${(0, node_os_1.homedir)()}`);
|
|
126
|
+
// Timezone (useful for scheduling and user context)
|
|
127
|
+
try {
|
|
128
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
129
|
+
if (tz)
|
|
130
|
+
parts.push(`timezone=${tz}`);
|
|
131
|
+
}
|
|
132
|
+
catch { }
|
|
133
|
+
// Current time
|
|
134
|
+
try {
|
|
135
|
+
const now = new Date();
|
|
136
|
+
parts.push(`time=${now.toLocaleString('en-US', { hour12: false })}`);
|
|
137
|
+
}
|
|
138
|
+
catch { }
|
|
139
|
+
return `Runtime: ${parts.join(' | ')}`;
|
|
140
|
+
}
|
|
141
|
+
// ─── CJK keyword extraction (ported from container-agent) ────────
|
|
142
|
+
const LOCAL_STOP_EN = new Set([
|
|
143
|
+
'a', 'an', 'the', 'is', 'am', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had',
|
|
144
|
+
'do', 'does', 'did', 'will', 'would', 'shall', 'should', 'may', 'might', 'must', 'can', 'could',
|
|
145
|
+
'i', 'me', 'my', 'we', 'our', 'you', 'your', 'he', 'him', 'his', 'she', 'her', 'it', 'its', 'they', 'them', 'their',
|
|
146
|
+
'this', 'that', 'these', 'those', 'what', 'which', 'who', 'whom', 'where', 'when', 'how', 'why',
|
|
147
|
+
'not', 'no', 'nor', 'if', 'then', 'than', 'too', 'very', 'just', 'about', 'all', 'also', 'and', 'any', 'as',
|
|
148
|
+
'at', 'but', 'by', 'for', 'from', 'in', 'into', 'of', 'on', 'or', 'so', 'to', 'with',
|
|
149
|
+
'please', 'help', 'find', 'show', 'tell', 'give', 'want', 'need', 'know', 'think', 'like',
|
|
150
|
+
]);
|
|
151
|
+
const LOCAL_STOP_ZH = new Set([
|
|
152
|
+
'我', '我们', '你', '你们', '他', '她', '它', '他们', '这', '那', '这个', '那个',
|
|
153
|
+
'的', '了', '着', '过', '得', '地', '吗', '呢', '吧', '啊', '哦', '呀', '嗯',
|
|
154
|
+
'是', '有', '在', '做', '说', '看', '找', '想', '要', '能', '会', '可以',
|
|
155
|
+
'和', '与', '或', '但', '因为', '所以', '如果', '但是', '而且',
|
|
156
|
+
'把', '被', '让', '给', '到', '从', '对', '比', '跟', '为', '用',
|
|
157
|
+
'都', '也', '就', '才', '又', '再', '还', '已经', '很', '太', '最', '更',
|
|
158
|
+
'什么', '哪个', '怎么', '为什么', '请', '帮', '帮忙', '告诉', '知道',
|
|
159
|
+
]);
|
|
160
|
+
const LOCAL_STOP_JA = new Set([
|
|
161
|
+
'の', 'に', 'は', 'を', 'た', 'が', 'で', 'て', 'と', 'し', 'れ', 'さ', 'ある', 'いる', 'も',
|
|
162
|
+
'する', 'から', 'な', 'こと', 'い', 'や', 'など', 'なっ', 'ない', 'この', 'その', 'また',
|
|
163
|
+
'です', 'ます', 'ました', 'でした', 'ません', 'それ', 'あれ', 'ここ', 'そこ', 'どこ',
|
|
164
|
+
]);
|
|
165
|
+
const LOCAL_STOP_KO = new Set([
|
|
166
|
+
'이', '그', '저', '것', '수', '등', '들', '및', '에', '의', '가', '은', '는', '로', '를',
|
|
167
|
+
'와', '과', '도', '만', '에서', '으로', '하다', '있다', '되다', '않다', '이다', '합니다',
|
|
168
|
+
]);
|
|
169
|
+
const LOCAL_STOP_ES = new Set([
|
|
170
|
+
'el', 'la', 'los', 'las', 'un', 'una', 'y', 'o', 'pero', 'de', 'en', 'a', 'por', 'para', 'con', 'que', 'se', 'es', 'no',
|
|
171
|
+
]);
|
|
172
|
+
const LOCAL_STOP_PT = new Set([
|
|
173
|
+
'o', 'a', 'os', 'as', 'um', 'uma', 'e', 'ou', 'mas', 'de', 'em', 'por', 'para', 'com', 'que', 'se', 'é', 'não',
|
|
174
|
+
]);
|
|
175
|
+
const RE_ANY_CJK_LOCAL = /[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af\u1100-\u11ff\u3130-\u318f]/;
|
|
176
|
+
const RE_CJK_LOCAL = /[\u4e00-\u9fff]/;
|
|
177
|
+
const RE_HIRAGANA_LOCAL = /[\u3040-\u309f]/;
|
|
178
|
+
const RE_KATAKANA_LOCAL = /[\u30a0-\u30ff]/;
|
|
179
|
+
const RE_HANGUL_LOCAL = /[\uac00-\ud7af\u1100-\u11ff\u3130-\u318f]/;
|
|
180
|
+
function isLocalStopWord(t) {
|
|
181
|
+
return LOCAL_STOP_EN.has(t) || LOCAL_STOP_ZH.has(t) ||
|
|
182
|
+
LOCAL_STOP_JA.has(t) || LOCAL_STOP_KO.has(t) ||
|
|
183
|
+
LOCAL_STOP_ES.has(t) || LOCAL_STOP_PT.has(t);
|
|
184
|
+
}
|
|
185
|
+
function extractLocalKeywords(query) {
|
|
186
|
+
const tokens = [];
|
|
187
|
+
const segments = query.match(/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af\u1100-\u11ff\u3130-\u318f]+|[^\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af\u1100-\u11ff\u3130-\u318f]+/g) || [];
|
|
188
|
+
for (const seg of segments) {
|
|
189
|
+
if (RE_ANY_CJK_LOCAL.test(seg)) {
|
|
190
|
+
const chars = Array.from(seg);
|
|
191
|
+
for (const c of chars) {
|
|
192
|
+
if (RE_CJK_LOCAL.test(c) || RE_HIRAGANA_LOCAL.test(c) || RE_KATAKANA_LOCAL.test(c) || RE_HANGUL_LOCAL.test(c)) {
|
|
193
|
+
tokens.push(c);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
for (let i = 0; i < chars.length - 1; i++) {
|
|
197
|
+
const a = chars[i], b = chars[i + 1];
|
|
198
|
+
if ((RE_CJK_LOCAL.test(a) && RE_CJK_LOCAL.test(b)) ||
|
|
199
|
+
(RE_HIRAGANA_LOCAL.test(a) && RE_HIRAGANA_LOCAL.test(b)) ||
|
|
200
|
+
(RE_KATAKANA_LOCAL.test(a) && RE_KATAKANA_LOCAL.test(b)) ||
|
|
201
|
+
(RE_HANGUL_LOCAL.test(a) && RE_HANGUL_LOCAL.test(b))) {
|
|
202
|
+
tokens.push(a + b);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
const words = seg.toLowerCase().match(/[\p{L}\p{N}_]+/gu) || [];
|
|
208
|
+
tokens.push(...words);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const seen = new Set();
|
|
212
|
+
const kws = [];
|
|
213
|
+
for (const t of tokens) {
|
|
214
|
+
if (isLocalStopWord(t))
|
|
215
|
+
continue;
|
|
216
|
+
if (t.length < 2 && !RE_ANY_CJK_LOCAL.test(t))
|
|
217
|
+
continue;
|
|
218
|
+
if (/^\d+$/.test(t))
|
|
219
|
+
continue;
|
|
220
|
+
if (seen.has(t))
|
|
221
|
+
continue;
|
|
222
|
+
seen.add(t);
|
|
223
|
+
kws.push(t);
|
|
224
|
+
}
|
|
225
|
+
return kws;
|
|
226
|
+
}
|
|
64
227
|
// ─── State ───────────────────────────────────────────────────────
|
|
65
228
|
let cancelled = false;
|
|
66
229
|
let currentTaskId = null;
|
|
@@ -166,7 +329,14 @@ async function runAgentLoop(req) {
|
|
|
166
329
|
}
|
|
167
330
|
}
|
|
168
331
|
catch { }
|
|
169
|
-
|
|
332
|
+
// Detect platform locally (more accurate than iOS-passed value)
|
|
333
|
+
const detectedPlatform = detectLocalPlatform();
|
|
334
|
+
const ctx = await proxy.loadContext(sandboxId, conversationId, isCronTask, memoryContent, '', req.workingDirectory, true, detectedPlatform);
|
|
335
|
+
// Inject runtime info into system prompt (like OpenClaw's buildRuntimeLine)
|
|
336
|
+
// Gives Claude exact details: OS version, arch, shell, node, home dir
|
|
337
|
+
const runtimeLine = buildRuntimeLine();
|
|
338
|
+
ctx.systemPrompt = runtimeLine + '\n\n' + ctx.systemPrompt;
|
|
339
|
+
console.log(`[Agent] ${runtimeLine}`);
|
|
170
340
|
// Build messages array
|
|
171
341
|
if (ctx.conversationHistory.length > 0) {
|
|
172
342
|
messages.push(...ctx.conversationHistory);
|
|
@@ -186,6 +356,13 @@ async function runAgentLoop(req) {
|
|
|
186
356
|
messages.push({ role: 'user', content: userContent.length === 1 ? cronPrefix + message : userContent });
|
|
187
357
|
const systemPrompt = ctx.systemPrompt;
|
|
188
358
|
let fullOutput = '';
|
|
359
|
+
// Usage accumulator — tracks total token usage across all API calls in this task
|
|
360
|
+
const accumulatedUsage = {
|
|
361
|
+
input_tokens: 0,
|
|
362
|
+
output_tokens: 0,
|
|
363
|
+
cache_read_input_tokens: 0,
|
|
364
|
+
cache_creation_input_tokens: 0,
|
|
365
|
+
};
|
|
189
366
|
// Emit connected event
|
|
190
367
|
await emitEvent(proxy, taskId, userId, conversationId, 'connected', {
|
|
191
368
|
request_id: taskId,
|
|
@@ -193,8 +370,10 @@ async function runAgentLoop(req) {
|
|
|
193
370
|
});
|
|
194
371
|
const loopStartTime = Date.now();
|
|
195
372
|
let compactionCount = 0;
|
|
373
|
+
let lastTurn = 0;
|
|
196
374
|
// ─── Agentic Loop ────────────────────────────────────────
|
|
197
375
|
for (let turn = 0; turn < types_1.MAX_AGENT_TURNS; turn++) {
|
|
376
|
+
lastTurn = turn;
|
|
198
377
|
if (cancelled) {
|
|
199
378
|
await emitEvent(proxy, taskId, userId, conversationId, 'done', { finish_reason: 'cancelled' });
|
|
200
379
|
break;
|
|
@@ -377,6 +556,13 @@ async function runAgentLoop(req) {
|
|
|
377
556
|
break;
|
|
378
557
|
}
|
|
379
558
|
console.log(`[Agent] Turn ${turn}: stop_reason=${response.stop_reason}, blocks=${response.content.length}, usage=${JSON.stringify(response.usage)}`);
|
|
559
|
+
// Accumulate usage from this API call
|
|
560
|
+
if (response.usage) {
|
|
561
|
+
accumulatedUsage.input_tokens += response.usage.input_tokens || 0;
|
|
562
|
+
accumulatedUsage.output_tokens += response.usage.output_tokens || 0;
|
|
563
|
+
accumulatedUsage.cache_read_input_tokens += response.usage.cache_read_input_tokens || 0;
|
|
564
|
+
accumulatedUsage.cache_creation_input_tokens += response.usage.cache_creation_input_tokens || 0;
|
|
565
|
+
}
|
|
380
566
|
// Turn 0: retry with tool_choice='any' if text-only
|
|
381
567
|
const hasAnyToolUse = response.content.some((b) => b.type === 'tool_use' || b.type === 'server_tool_use');
|
|
382
568
|
if (turn === 0 && !hasAnyToolUse && response.stop_reason === 'end_turn') {
|
|
@@ -394,6 +580,13 @@ async function runAgentLoop(req) {
|
|
|
394
580
|
});
|
|
395
581
|
if (retryResponse.content.length > 0) {
|
|
396
582
|
response = retryResponse;
|
|
583
|
+
// Accumulate retry usage
|
|
584
|
+
if (retryResponse.usage) {
|
|
585
|
+
accumulatedUsage.input_tokens += retryResponse.usage.input_tokens || 0;
|
|
586
|
+
accumulatedUsage.output_tokens += retryResponse.usage.output_tokens || 0;
|
|
587
|
+
accumulatedUsage.cache_read_input_tokens += retryResponse.usage.cache_read_input_tokens || 0;
|
|
588
|
+
accumulatedUsage.cache_creation_input_tokens += retryResponse.usage.cache_creation_input_tokens || 0;
|
|
589
|
+
}
|
|
397
590
|
}
|
|
398
591
|
else {
|
|
399
592
|
console.log('[Agent] Turn 0 retry returned empty, keeping original text response');
|
|
@@ -491,8 +684,58 @@ async function runAgentLoop(req) {
|
|
|
491
684
|
console.error(`[Agent] Failed to save turn: ${err.message}`);
|
|
492
685
|
}
|
|
493
686
|
}
|
|
494
|
-
//
|
|
495
|
-
|
|
687
|
+
// Transcript digest + session log: write to daily log for searchability
|
|
688
|
+
try {
|
|
689
|
+
const today = new Date().toISOString().split('T')[0];
|
|
690
|
+
const filePath = `${MEMORY_DIR}/${today}.md`;
|
|
691
|
+
const timestamp = new Date().toLocaleTimeString('en-US', { hour12: false });
|
|
692
|
+
await (0, promises_1.mkdir)(MEMORY_DIR, { recursive: true });
|
|
693
|
+
// 1. Lightweight transcript digest
|
|
694
|
+
const digest = (0, compaction_1.buildTranscriptDigest)(messages, message, lastTurn + 1, accumulatedUsage);
|
|
695
|
+
await (0, promises_1.appendFile)(filePath, `\n## [transcript] ${timestamp}\n${digest}\n`, 'utf-8');
|
|
696
|
+
// 2. Session log for keyword searchability
|
|
697
|
+
const sessionLog = (0, compaction_1.buildEnrichedSnippet)(messages, 8000);
|
|
698
|
+
if (sessionLog.length > 200) {
|
|
699
|
+
await (0, promises_1.appendFile)(filePath, `\n## [session-log] ${timestamp}\n${sessionLog}\n`, 'utf-8');
|
|
700
|
+
}
|
|
701
|
+
// R2 backup (fire-and-forget)
|
|
702
|
+
const full = await (0, promises_1.readFile)(filePath, 'utf-8').catch(() => '');
|
|
703
|
+
proxy.memoryOp('backup_memory', filePath, full).catch(() => { });
|
|
704
|
+
proxy.memorySync(`${today}.md`, full).catch(() => { });
|
|
705
|
+
}
|
|
706
|
+
catch { }
|
|
707
|
+
// Post-task memory flush: extract key info → daily log (fire-and-forget)
|
|
708
|
+
(0, compaction_1.postTaskMemoryFlush)(messages, chatFn, async (content) => {
|
|
709
|
+
const today = new Date().toISOString().split('T')[0];
|
|
710
|
+
const filePath = `${MEMORY_DIR}/${today}.md`;
|
|
711
|
+
const entry = `\n## [task-summary] ${new Date().toLocaleTimeString('en-US', { hour12: false })}\n${content}\n`;
|
|
712
|
+
try {
|
|
713
|
+
await (0, promises_1.mkdir)(MEMORY_DIR, { recursive: true });
|
|
714
|
+
await (0, promises_1.appendFile)(filePath, entry, 'utf-8');
|
|
715
|
+
// R2 backup (fire-and-forget)
|
|
716
|
+
const full = await (0, promises_1.readFile)(filePath, 'utf-8').catch(() => content);
|
|
717
|
+
proxy.memoryOp('backup_memory', filePath, full).catch(() => { });
|
|
718
|
+
proxy.memorySync(`${today}.md`, full).catch(() => { });
|
|
719
|
+
console.log(`[Memory] Post-task flush: saved to ${filePath} + R2 backup`);
|
|
720
|
+
}
|
|
721
|
+
catch (writeErr) {
|
|
722
|
+
console.warn(`[Memory] Post-task flush write failed: ${writeErr.message}`);
|
|
723
|
+
}
|
|
724
|
+
}).catch(() => { });
|
|
725
|
+
// Auto-distill MEMORY.md if it's grown too large (fire-and-forget)
|
|
726
|
+
(0, compaction_1.autoDistillMemory)(MEMORY_DIR, chatFn, async (distilled) => {
|
|
727
|
+
try {
|
|
728
|
+
await (0, promises_1.writeFile)(`${MEMORY_DIR}/MEMORY.md`, distilled, 'utf-8');
|
|
729
|
+
proxy.memoryOp('backup_memory', `${MEMORY_DIR}/MEMORY.md`, distilled).catch(() => { });
|
|
730
|
+
proxy.memorySync('MEMORY.md', distilled).catch(() => { });
|
|
731
|
+
}
|
|
732
|
+
catch (err) {
|
|
733
|
+
console.warn(`[Memory] Failed to write distilled MEMORY.md: ${err.message}`);
|
|
734
|
+
}
|
|
735
|
+
}).catch(() => { });
|
|
736
|
+
// Mark task completed — pass accumulated usage for credit deduction
|
|
737
|
+
console.log(`[Agent] Task ${taskId} completed. Total usage: in=${accumulatedUsage.input_tokens} out=${accumulatedUsage.output_tokens} cacheRead=${accumulatedUsage.cache_read_input_tokens} cacheWrite=${accumulatedUsage.cache_creation_input_tokens}`);
|
|
738
|
+
await proxy.updateTaskStatus('completed', undefined, accumulatedUsage);
|
|
496
739
|
// Finalize cron run if this was a scheduled task
|
|
497
740
|
if (isCronTask && req.cronRunId && req.cronJobId) {
|
|
498
741
|
try {
|
|
@@ -518,6 +761,22 @@ async function runAgentLoop(req) {
|
|
|
518
761
|
console.error(`[Agent] Failed to save turn on failure: ${saveErr.message}`);
|
|
519
762
|
}
|
|
520
763
|
}
|
|
764
|
+
// Post-task memory flush on failure too (fire-and-forget)
|
|
765
|
+
if (messages.length >= 3) {
|
|
766
|
+
(0, compaction_1.postTaskMemoryFlush)(messages, chatFn, async (content) => {
|
|
767
|
+
const today = new Date().toISOString().split('T')[0];
|
|
768
|
+
const filePath = `${MEMORY_DIR}/${today}.md`;
|
|
769
|
+
const entry = `\n## [task-failed] ${new Date().toLocaleTimeString('en-US', { hour12: false })}\n${content}\n[Error: ${err.message}]\n`;
|
|
770
|
+
try {
|
|
771
|
+
await (0, promises_1.mkdir)(MEMORY_DIR, { recursive: true });
|
|
772
|
+
await (0, promises_1.appendFile)(filePath, entry, 'utf-8');
|
|
773
|
+
const full = await (0, promises_1.readFile)(filePath, 'utf-8').catch(() => content);
|
|
774
|
+
proxy.memoryOp('backup_memory', filePath, full).catch(() => { });
|
|
775
|
+
proxy.memorySync(`${today}.md`, full).catch(() => { });
|
|
776
|
+
}
|
|
777
|
+
catch { }
|
|
778
|
+
}).catch(() => { });
|
|
779
|
+
}
|
|
521
780
|
// Emit error event
|
|
522
781
|
try {
|
|
523
782
|
await emitEvent(proxy, taskId, userId, conversationId, 'error', {
|
|
@@ -581,7 +840,27 @@ async function executeToolInner(proxy, taskId, userId, sandboxId, conversationId
|
|
|
581
840
|
// ── Local tools ──
|
|
582
841
|
if (toolName === 'bash') {
|
|
583
842
|
const cmd = (toolInput.command || 'echo "no command"');
|
|
584
|
-
|
|
843
|
+
const result = await (0, local_tools_1.executeBash)(cmd);
|
|
844
|
+
// Auto-detect package installs and save to environment.md (fire-and-forget)
|
|
845
|
+
if (!result.isError) {
|
|
846
|
+
const packages = detectInstalledPackages(cmd);
|
|
847
|
+
if (packages) {
|
|
848
|
+
const envFile = `${MEMORY_DIR}/environment.md`;
|
|
849
|
+
const timestamp = new Date().toLocaleTimeString('en-US', { hour12: false });
|
|
850
|
+
const entry = `\n## ${timestamp}\n${packages}\n`;
|
|
851
|
+
(async () => {
|
|
852
|
+
try {
|
|
853
|
+
await (0, promises_1.mkdir)(MEMORY_DIR, { recursive: true });
|
|
854
|
+
await (0, promises_1.appendFile)(envFile, entry, 'utf-8');
|
|
855
|
+
const full = await (0, promises_1.readFile)(envFile, 'utf-8').catch(() => entry);
|
|
856
|
+
proxy.memoryOp('backup_memory', envFile, full).catch(() => { });
|
|
857
|
+
proxy.memorySync('environment.md', full).catch(() => { });
|
|
858
|
+
}
|
|
859
|
+
catch { }
|
|
860
|
+
})();
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
return result;
|
|
585
864
|
}
|
|
586
865
|
if (toolName === 'write_file') {
|
|
587
866
|
const writePath = toolInput.path || toolInput.file_path;
|
|
@@ -658,27 +937,43 @@ async function executeToolInner(proxy, taskId, userId, sandboxId, conversationId
|
|
|
658
937
|
try {
|
|
659
938
|
const query = toolInput.query || '';
|
|
660
939
|
const maxResults = Math.min(toolInput.max_results || 5, 10);
|
|
661
|
-
//
|
|
940
|
+
// Run Vectorize + local keyword search in parallel, merge results
|
|
941
|
+
// Local search is always needed because post-task flush writes locally
|
|
942
|
+
// and Vectorize indexing has latency — stale index misses fresh content
|
|
943
|
+
// 1. Vectorize search (may return stale results)
|
|
944
|
+
let vectorizeResults = [];
|
|
662
945
|
try {
|
|
663
|
-
const
|
|
664
|
-
if (
|
|
665
|
-
|
|
666
|
-
return { output: formatted, isError: false };
|
|
946
|
+
const vResults = await proxy.memorySearch(query, maxResults);
|
|
947
|
+
if (vResults && vResults.length > 0) {
|
|
948
|
+
vectorizeResults = vResults.map((r) => r.content?.trim()).filter(Boolean);
|
|
667
949
|
}
|
|
668
950
|
}
|
|
669
951
|
catch (err) {
|
|
670
|
-
console.warn(`[Memory] Vectorize search failed
|
|
952
|
+
console.warn(`[Memory] Vectorize search failed: ${err.message}`);
|
|
671
953
|
}
|
|
672
|
-
//
|
|
673
|
-
const
|
|
674
|
-
const
|
|
954
|
+
// 2. Local keyword search (always fresh — reads files directly)
|
|
955
|
+
const keywords = extractLocalKeywords(query);
|
|
956
|
+
const localResults = [];
|
|
957
|
+
const scoreContent = (text) => {
|
|
958
|
+
if (keywords.length === 0)
|
|
959
|
+
return text.toLowerCase().includes(query.toLowerCase()) ? 1 : 0;
|
|
960
|
+
const lower = text.toLowerCase();
|
|
961
|
+
let matched = 0;
|
|
962
|
+
for (const kw of keywords) {
|
|
963
|
+
if (lower.includes(kw))
|
|
964
|
+
matched++;
|
|
965
|
+
}
|
|
966
|
+
return matched / keywords.length;
|
|
967
|
+
};
|
|
968
|
+
// Search MEMORY.md
|
|
675
969
|
try {
|
|
676
970
|
const mem = await (0, promises_1.readFile)(`${MEMORY_DIR}/MEMORY.md`, 'utf-8');
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
971
|
+
const score = scoreContent(mem);
|
|
972
|
+
if (score > 0)
|
|
973
|
+
localResults.push({ file: 'MEMORY.md', content: mem.substring(0, 3000), score });
|
|
680
974
|
}
|
|
681
975
|
catch { }
|
|
976
|
+
// Search last 14 daily logs
|
|
682
977
|
const today = new Date();
|
|
683
978
|
for (let i = 0; i < 14; i++) {
|
|
684
979
|
const d = new Date(today);
|
|
@@ -686,13 +981,13 @@ async function executeToolInner(proxy, taskId, userId, sandboxId, conversationId
|
|
|
686
981
|
const dateStr = d.toISOString().split('T')[0];
|
|
687
982
|
try {
|
|
688
983
|
const note = await (0, promises_1.readFile)(`${MEMORY_DIR}/${dateStr}.md`, 'utf-8');
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
984
|
+
const score = scoreContent(note);
|
|
985
|
+
if (score > 0)
|
|
986
|
+
localResults.push({ file: `${dateStr}.md`, content: note.substring(0, 1500), score });
|
|
692
987
|
}
|
|
693
988
|
catch { }
|
|
694
989
|
}
|
|
695
|
-
//
|
|
990
|
+
// Search topic files
|
|
696
991
|
try {
|
|
697
992
|
const { readdir } = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
|
|
698
993
|
const files = await readdir(MEMORY_DIR);
|
|
@@ -701,15 +996,27 @@ async function executeToolInner(proxy, taskId, userId, sandboxId, conversationId
|
|
|
701
996
|
continue;
|
|
702
997
|
try {
|
|
703
998
|
const fc = await (0, promises_1.readFile)(`${MEMORY_DIR}/${f}`, 'utf-8');
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
999
|
+
const score = scoreContent(fc);
|
|
1000
|
+
if (score > 0)
|
|
1001
|
+
localResults.push({ file: f, content: fc.substring(0, 1500), score });
|
|
707
1002
|
}
|
|
708
1003
|
catch { }
|
|
709
1004
|
}
|
|
710
1005
|
}
|
|
711
1006
|
catch { }
|
|
712
|
-
|
|
1007
|
+
// 3. Merge: local results first (always fresh), then Vectorize extras
|
|
1008
|
+
localResults.sort((a, b) => b.score - a.score);
|
|
1009
|
+
const localFormatted = localResults.map(r => `${r.content.trim()}\n\nSource: ${r.file}`);
|
|
1010
|
+
// Deduplicate: skip Vectorize results whose content overlaps with local
|
|
1011
|
+
const localText = localFormatted.join('\n');
|
|
1012
|
+
const vectorizeExtras = vectorizeResults.filter(v => {
|
|
1013
|
+
// Skip if >50% of first 200 chars appear in local results
|
|
1014
|
+
const snippet = v.substring(0, 200);
|
|
1015
|
+
return !localText.includes(snippet);
|
|
1016
|
+
});
|
|
1017
|
+
const merged = [...localFormatted, ...vectorizeExtras.map(v => `${v}\n\nSource: vectorize`)];
|
|
1018
|
+
const output = merged.slice(0, maxResults).join('\n\n---\n\n');
|
|
1019
|
+
return { output: output || 'No results found.', isError: false };
|
|
713
1020
|
}
|
|
714
1021
|
catch (err) {
|
|
715
1022
|
return { output: `Search failed: ${err.message}`, isError: true };
|
|
@@ -986,6 +1293,25 @@ async function extractFileFromZip(buffer, targetName) {
|
|
|
986
1293
|
}
|
|
987
1294
|
return null;
|
|
988
1295
|
}
|
|
1296
|
+
// ─── Package Detection ──────────────────────────────────────────
|
|
1297
|
+
function detectInstalledPackages(cmd) {
|
|
1298
|
+
const installs = [];
|
|
1299
|
+
const pipMatch = cmd.match(/pip3?\s+install\s+([^\s|&;]+(?:\s+[^\s|&;-][^\s|&;]*)*)/);
|
|
1300
|
+
if (pipMatch)
|
|
1301
|
+
installs.push(`pip: ${pipMatch[1].trim()}`);
|
|
1302
|
+
const npmMatch = cmd.match(/npm\s+(?:install|i)\s+(?:-[gSD]\s+)?([^\s|&;]+(?:\s+[^\s|&;-][^\s|&;]*)*)/);
|
|
1303
|
+
if (npmMatch)
|
|
1304
|
+
installs.push(`npm: ${npmMatch[1].trim()}`);
|
|
1305
|
+
const aptMatch = cmd.match(/apt(?:-get)?\s+install\s+(?:-y\s+)?([^\s|&;]+(?:\s+[^\s|&;-][^\s|&;]*)*)/);
|
|
1306
|
+
if (aptMatch)
|
|
1307
|
+
installs.push(`apt: ${aptMatch[1].trim()}`);
|
|
1308
|
+
const brewMatch = cmd.match(/brew\s+install\s+([^\s|&;]+(?:\s+[^\s|&;-][^\s|&;]*)*)/);
|
|
1309
|
+
if (brewMatch)
|
|
1310
|
+
installs.push(`brew: ${brewMatch[1].trim()}`);
|
|
1311
|
+
if (installs.length === 0)
|
|
1312
|
+
return null;
|
|
1313
|
+
return `Installed packages: ${installs.join(', ')}`;
|
|
1314
|
+
}
|
|
989
1315
|
// ─── Turn Summary Builder ────────────────────────────────────────
|
|
990
1316
|
function buildTurnSummary(messages, _originalMessage) {
|
|
991
1317
|
const textParts = [];
|