tsunami-code 2.5.2 → 2.6.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/.tsunami/README.md +10 -0
- package/index.js +163 -17
- package/lib/loop.js +124 -8
- package/lib/memory.js +493 -0
- package/lib/prompt.js +30 -3
- package/lib/tools.js +98 -1
- package/package.json +1 -1
- package/scripts/setup.js +227 -39
- package/vendor/rg.exe +0 -0
- package/vendor/ripgrep-14.1.1-x86_64-pc-windows-msvc/COPYING +3 -0
- package/vendor/ripgrep-14.1.1-x86_64-pc-windows-msvc/LICENSE-MIT +21 -0
- package/vendor/ripgrep-14.1.1-x86_64-pc-windows-msvc/README.md +524 -0
- package/vendor/ripgrep-14.1.1-x86_64-pc-windows-msvc/UNLICENSE +24 -0
- package/vendor/ripgrep-14.1.1-x86_64-pc-windows-msvc/complete/_rg +665 -0
- package/vendor/ripgrep-14.1.1-x86_64-pc-windows-msvc/complete/_rg.ps1 +213 -0
- package/vendor/ripgrep-14.1.1-x86_64-pc-windows-msvc/complete/rg.bash +783 -0
- package/vendor/ripgrep-14.1.1-x86_64-pc-windows-msvc/complete/rg.fish +175 -0
- package/vendor/ripgrep-14.1.1-x86_64-pc-windows-msvc/doc/CHANGELOG.md +1711 -0
- package/vendor/ripgrep-14.1.1-x86_64-pc-windows-msvc/doc/FAQ.md +1046 -0
- package/vendor/ripgrep-14.1.1-x86_64-pc-windows-msvc/doc/GUIDE.md +1022 -0
- package/vendor/ripgrep-14.1.1-x86_64-pc-windows-msvc/doc/rg.1 +2178 -0
- package/vendor/ripgrep-14.1.1-x86_64-pc-windows-msvc/rg.exe +0 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# .tsunami/ — Project Memory
|
|
2
|
+
|
|
3
|
+
Auto-managed by Tsunami Code CLI. Safe to commit.
|
|
4
|
+
|
|
5
|
+
## Structure
|
|
6
|
+
- `memory/` — Per-file notes and change history
|
|
7
|
+
- `decisions/` — Team-shared decision log by date
|
|
8
|
+
- `CODEBASE.md` — High-level project understanding
|
|
9
|
+
|
|
10
|
+
Do not edit manually unless you know what you're doing.
|
package/index.js
CHANGED
|
@@ -7,8 +7,20 @@ import os from 'os';
|
|
|
7
7
|
import { agentLoop } from './lib/loop.js';
|
|
8
8
|
import { buildSystemPrompt } from './lib/prompt.js';
|
|
9
9
|
import { runPreflight } from './lib/preflight.js';
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
import { setSession } from './lib/tools.js';
|
|
11
|
+
import {
|
|
12
|
+
initSession,
|
|
13
|
+
initProjectMemory,
|
|
14
|
+
getLastSessionSummary,
|
|
15
|
+
endSession,
|
|
16
|
+
getMemoryStats,
|
|
17
|
+
listFileMemories,
|
|
18
|
+
getFileMemory,
|
|
19
|
+
getRecentDecisions,
|
|
20
|
+
getSessionContext
|
|
21
|
+
} from './lib/memory.js';
|
|
22
|
+
|
|
23
|
+
const VERSION = '2.6.0';
|
|
12
24
|
const CONFIG_DIR = join(os.homedir(), '.tsunami-code');
|
|
13
25
|
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
14
26
|
const DEFAULT_SERVER = 'https://radiometric-reita-amuck.ngrok-free.dev';
|
|
@@ -27,7 +39,6 @@ function saveConfig(cfg) {
|
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
function getServerUrl(argv) {
|
|
30
|
-
// Priority: --server flag > KEYSTONE_SERVER env var > saved config > default
|
|
31
42
|
const flagIdx = argv.indexOf('--server');
|
|
32
43
|
if (flagIdx !== -1 && argv[flagIdx + 1]) return argv[flagIdx + 1];
|
|
33
44
|
if (process.env.TSUNAMI_SERVER) return process.env.TSUNAMI_SERVER;
|
|
@@ -43,6 +54,7 @@ const cyan = (s) => chalk.cyan(s);
|
|
|
43
54
|
const red = (s) => chalk.red(s);
|
|
44
55
|
const green = (s) => chalk.green(s);
|
|
45
56
|
const yellow = (s) => chalk.yellow(s);
|
|
57
|
+
const blue = (s) => chalk.blue(s);
|
|
46
58
|
|
|
47
59
|
function printBanner(serverUrl) {
|
|
48
60
|
console.log(cyan(bold('\n 🌊 Tsunami Code CLI')) + dim(` v${VERSION}`));
|
|
@@ -59,6 +71,12 @@ function printToolCall(name, args) {
|
|
|
59
71
|
process.stdout.write('\n' + dim(` ⚙ ${name}(${preview})\n`));
|
|
60
72
|
}
|
|
61
73
|
|
|
74
|
+
function formatBytes(bytes) {
|
|
75
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
76
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
77
|
+
return `${(bytes / 1024 / 1024).toFixed(2)}MB`;
|
|
78
|
+
}
|
|
79
|
+
|
|
62
80
|
// ── CLI Parsing ───────────────────────────────────────────────────────────────
|
|
63
81
|
const argv = process.argv.slice(2);
|
|
64
82
|
|
|
@@ -84,6 +102,12 @@ if (argv.includes('--help') || argv.includes('-h')) {
|
|
|
84
102
|
${bold('Global config:')}
|
|
85
103
|
~/.tsunami-code/TSUNAMI.md — applies to all projects.
|
|
86
104
|
|
|
105
|
+
${bold('Memory commands:')}
|
|
106
|
+
/memory Show memory stats
|
|
107
|
+
/memory files List files with memory entries
|
|
108
|
+
/memory view <f> Show memory for a specific file
|
|
109
|
+
/memory clear Clear session memory (keep project memory)
|
|
110
|
+
|
|
87
111
|
${bold('Settings:')}
|
|
88
112
|
${CONFIG_FILE}
|
|
89
113
|
`);
|
|
@@ -108,6 +132,12 @@ if (setServerIdx !== -1 && argv[setServerIdx + 1]) {
|
|
|
108
132
|
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
109
133
|
async function run() {
|
|
110
134
|
const serverUrl = getServerUrl(argv);
|
|
135
|
+
const cwd = process.cwd();
|
|
136
|
+
|
|
137
|
+
// Initialize memory systems
|
|
138
|
+
const { sessionId, sessionDir } = initSession(cwd);
|
|
139
|
+
initProjectMemory(cwd);
|
|
140
|
+
setSession({ sessionDir, cwd });
|
|
111
141
|
|
|
112
142
|
printBanner(serverUrl);
|
|
113
143
|
|
|
@@ -127,7 +157,13 @@ async function run() {
|
|
|
127
157
|
process.exit(1);
|
|
128
158
|
}
|
|
129
159
|
|
|
130
|
-
console.log(
|
|
160
|
+
console.log(
|
|
161
|
+
green(' ✓ Connected') +
|
|
162
|
+
dim(` · ${sessionId} · Type your task. /help for commands. Ctrl+C to exit.\n`)
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// Load last session summary if available
|
|
166
|
+
const lastSessionSummary = getLastSessionSummary(cwd);
|
|
131
167
|
|
|
132
168
|
let currentServerUrl = serverUrl;
|
|
133
169
|
let messages = [];
|
|
@@ -138,6 +174,15 @@ async function run() {
|
|
|
138
174
|
systemPrompt = buildSystemPrompt();
|
|
139
175
|
}
|
|
140
176
|
|
|
177
|
+
/** Trim messages to the last N entries (rolling window) */
|
|
178
|
+
function trimMessages(msgs, maxEntries = 8) {
|
|
179
|
+
// Always keep system message (index 0)
|
|
180
|
+
if (msgs.length <= maxEntries + 1) return msgs;
|
|
181
|
+
const system = msgs[0];
|
|
182
|
+
const rest = msgs.slice(1);
|
|
183
|
+
return [system, ...rest.slice(-maxEntries)];
|
|
184
|
+
}
|
|
185
|
+
|
|
141
186
|
const rl = readline.createInterface({
|
|
142
187
|
input: process.stdin,
|
|
143
188
|
output: process.stdout,
|
|
@@ -150,25 +195,108 @@ async function run() {
|
|
|
150
195
|
let isProcessing = false;
|
|
151
196
|
let pendingClose = false;
|
|
152
197
|
|
|
198
|
+
// ── Exit handler ─────────────────────────────────────────────────────────────
|
|
199
|
+
function gracefulExit(code = 0) {
|
|
200
|
+
try {
|
|
201
|
+
endSession(sessionDir);
|
|
202
|
+
} catch {}
|
|
203
|
+
console.log(dim('\n Goodbye.\n'));
|
|
204
|
+
process.exit(code);
|
|
205
|
+
}
|
|
206
|
+
|
|
153
207
|
rl.on('close', () => {
|
|
154
208
|
if (isProcessing) { pendingClose = true; return; }
|
|
155
|
-
|
|
156
|
-
process.exit(0);
|
|
209
|
+
gracefulExit(0);
|
|
157
210
|
});
|
|
158
211
|
|
|
212
|
+
// ── Memory commands ───────────────────────────────────────────────────────────
|
|
213
|
+
async function handleMemoryCommand(args) {
|
|
214
|
+
const sub = args[0]?.toLowerCase();
|
|
215
|
+
|
|
216
|
+
if (!sub) {
|
|
217
|
+
// /memory — show stats
|
|
218
|
+
const stats = getMemoryStats(cwd);
|
|
219
|
+
console.log(blue('\n Living Memory Stats'));
|
|
220
|
+
console.log(dim(` Project memory files : ${stats.projectMemoryFiles}`));
|
|
221
|
+
console.log(dim(` Project memory size : ${formatBytes(stats.projectMemorySize)}`));
|
|
222
|
+
console.log(dim(` CODEBASE.md : ${stats.hasCODEBASE ? 'exists' : 'none'}`));
|
|
223
|
+
console.log(dim(` Sessions this dir : ${stats.sessionCount}`));
|
|
224
|
+
console.log(dim(` Current session : ${sessionId}`));
|
|
225
|
+
console.log();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (sub === 'files') {
|
|
230
|
+
const files = listFileMemories(cwd);
|
|
231
|
+
if (files.length === 0) {
|
|
232
|
+
console.log(dim(' No file memories yet.\n'));
|
|
233
|
+
} else {
|
|
234
|
+
console.log(blue(`\n Files with memory (${files.length}):`));
|
|
235
|
+
for (const f of files) console.log(dim(` ${f}`));
|
|
236
|
+
console.log();
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (sub === 'view') {
|
|
242
|
+
const filePath = args.slice(1).join(' ');
|
|
243
|
+
if (!filePath) {
|
|
244
|
+
console.log(red(' Usage: /memory view <filepath>\n'));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const mem = getFileMemory(cwd, filePath);
|
|
248
|
+
if (!mem) {
|
|
249
|
+
console.log(dim(` No memory for: ${filePath}\n`));
|
|
250
|
+
} else {
|
|
251
|
+
console.log(blue(`\n Memory: ${filePath}`));
|
|
252
|
+
console.log(dim(' ' + mem.replace(/\n/g, '\n ')));
|
|
253
|
+
console.log();
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (sub === 'clear') {
|
|
259
|
+
// Reinitialize session (clear session memory, keep project memory)
|
|
260
|
+
resetSession();
|
|
261
|
+
console.log(green(' Session memory cleared. Project memory preserved.\n'));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (sub === 'context') {
|
|
266
|
+
// Debug: show current session context
|
|
267
|
+
const ctx = getSessionContext(sessionDir);
|
|
268
|
+
const decisions = getRecentDecisions(sessionDir, 10);
|
|
269
|
+
console.log(blue('\n Current Session Context:'));
|
|
270
|
+
console.log(dim(' ' + (ctx || '(empty)').replace(/\n/g, '\n ')));
|
|
271
|
+
console.log(blue('\n Recent Decisions:'));
|
|
272
|
+
console.log(dim(' ' + (decisions || '(none)').replace(/\n/g, '\n ')));
|
|
273
|
+
console.log();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
console.log(red(` Unknown memory subcommand: ${sub}\n Try: /memory, /memory files, /memory view <file>, /memory clear\n`));
|
|
278
|
+
}
|
|
279
|
+
|
|
159
280
|
rl.on('line', async (input) => {
|
|
160
281
|
const line = input.trim();
|
|
161
282
|
if (!line) { rl.prompt(); return; }
|
|
162
283
|
|
|
163
284
|
if (line.startsWith('/')) {
|
|
164
|
-
const
|
|
165
|
-
|
|
285
|
+
const parts = line.slice(1).split(' ');
|
|
286
|
+
const cmd = parts[0].toLowerCase();
|
|
287
|
+
const rest = parts.slice(1);
|
|
288
|
+
|
|
289
|
+
switch (cmd) {
|
|
166
290
|
case 'help':
|
|
167
291
|
console.log(dim(`
|
|
168
292
|
Commands:
|
|
169
293
|
/clear Start new conversation
|
|
170
294
|
/status Show context size
|
|
171
295
|
/server <url> Change server URL for this session
|
|
296
|
+
/memory Show memory stats
|
|
297
|
+
/memory files List files with memory entries
|
|
298
|
+
/memory view <f> Show memory for a file
|
|
299
|
+
/memory clear Clear session memory (keep project memory)
|
|
172
300
|
/exit Exit
|
|
173
301
|
`));
|
|
174
302
|
break;
|
|
@@ -187,8 +315,12 @@ async function run() {
|
|
|
187
315
|
console.log(dim(` Current server: ${currentServerUrl}\n`));
|
|
188
316
|
}
|
|
189
317
|
break;
|
|
318
|
+
case 'memory':
|
|
319
|
+
await handleMemoryCommand(rest);
|
|
320
|
+
break;
|
|
190
321
|
case 'exit': case 'quit':
|
|
191
|
-
|
|
322
|
+
gracefulExit(0);
|
|
323
|
+
return;
|
|
192
324
|
default:
|
|
193
325
|
console.log(red(` Unknown command: /${cmd}\n`));
|
|
194
326
|
}
|
|
@@ -196,12 +328,18 @@ async function run() {
|
|
|
196
328
|
return;
|
|
197
329
|
}
|
|
198
330
|
|
|
331
|
+
// Build the user message, optionally injecting last session summary on first turn
|
|
332
|
+
let userContent = line;
|
|
333
|
+
if (messages.length === 0 && lastSessionSummary) {
|
|
334
|
+
userContent = `[Previous session summary]\n${lastSessionSummary}\n\n---\n\n${line}`;
|
|
335
|
+
}
|
|
336
|
+
|
|
199
337
|
const fullMessages = [
|
|
200
338
|
{ role: 'system', content: systemPrompt },
|
|
201
339
|
...messages,
|
|
202
|
-
{ role: 'user', content:
|
|
340
|
+
{ role: 'user', content: userContent }
|
|
203
341
|
];
|
|
204
|
-
messages.push({ role: 'user', content:
|
|
342
|
+
messages.push({ role: 'user', content: userContent });
|
|
205
343
|
|
|
206
344
|
rl.pause();
|
|
207
345
|
isProcessing = true;
|
|
@@ -219,10 +357,15 @@ async function run() {
|
|
|
219
357
|
(toolName, toolArgs) => {
|
|
220
358
|
printToolCall(toolName, toolArgs);
|
|
221
359
|
firstToken = true;
|
|
222
|
-
}
|
|
360
|
+
},
|
|
361
|
+
{ sessionDir, cwd }
|
|
223
362
|
);
|
|
224
363
|
|
|
225
|
-
messages
|
|
364
|
+
// Update messages from the loop (fullMessages was mutated by agentLoop)
|
|
365
|
+
// Trim to rolling window: keep system + last 8 entries
|
|
366
|
+
const loopMessages = fullMessages.slice(1); // drop system, agentLoop added to fullMessages
|
|
367
|
+
messages = trimMessages([{ role: 'system', content: systemPrompt }, ...loopMessages]).slice(1);
|
|
368
|
+
|
|
226
369
|
process.stdout.write('\n\n');
|
|
227
370
|
} catch (e) {
|
|
228
371
|
process.stdout.write('\n');
|
|
@@ -231,8 +374,8 @@ async function run() {
|
|
|
231
374
|
|
|
232
375
|
isProcessing = false;
|
|
233
376
|
if (pendingClose) {
|
|
234
|
-
|
|
235
|
-
|
|
377
|
+
gracefulExit(0);
|
|
378
|
+
return;
|
|
236
379
|
}
|
|
237
380
|
rl.resume();
|
|
238
381
|
rl.prompt();
|
|
@@ -240,8 +383,11 @@ async function run() {
|
|
|
240
383
|
|
|
241
384
|
process.on('SIGINT', () => {
|
|
242
385
|
if (!isProcessing) {
|
|
243
|
-
|
|
244
|
-
|
|
386
|
+
gracefulExit(0);
|
|
387
|
+
} else {
|
|
388
|
+
// If processing, mark pending and let the loop finish
|
|
389
|
+
pendingClose = true;
|
|
390
|
+
console.log(dim('\n (finishing current operation...)\n'));
|
|
245
391
|
}
|
|
246
392
|
});
|
|
247
393
|
}
|
package/lib/loop.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import fetch from 'node-fetch';
|
|
2
2
|
import { ALL_TOOLS } from './tools.js';
|
|
3
|
+
import {
|
|
4
|
+
assembleContext,
|
|
5
|
+
extractFilePaths,
|
|
6
|
+
logFileAccess,
|
|
7
|
+
logFileChange,
|
|
8
|
+
appendDecision
|
|
9
|
+
} from './memory.js';
|
|
3
10
|
|
|
4
11
|
// Parse tool calls from any format the model might produce
|
|
5
12
|
function parseToolCalls(content) {
|
|
@@ -81,12 +88,60 @@ function normalizeArgs(args) {
|
|
|
81
88
|
return out;
|
|
82
89
|
}
|
|
83
90
|
|
|
84
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Detect key patterns in Bash commands and return a decision string, or null.
|
|
93
|
+
* We log psql, npm, pm2, git, and deploy-related commands.
|
|
94
|
+
*/
|
|
95
|
+
function bashDecisionHint(cmd) {
|
|
96
|
+
if (!cmd) return null;
|
|
97
|
+
const c = cmd.trim();
|
|
98
|
+
if (/psql\s/.test(c)) return `DB: ${c.slice(0, 120)}`;
|
|
99
|
+
if (/npm\s+(install|run|publish|build)/.test(c)) return `NPM: ${c.slice(0, 120)}`;
|
|
100
|
+
if (/pm2\s+(restart|stop|start|delete|reload)/.test(c)) return `PM2: ${c.slice(0, 120)}`;
|
|
101
|
+
if (/git\s+(commit|push|checkout|merge|rebase|reset)/.test(c)) return `GIT: ${c.slice(0, 120)}`;
|
|
102
|
+
if (/ssh\s+/.test(c)) return `SSH: ${c.slice(0, 80)}`;
|
|
103
|
+
if (/scp\s+/.test(c)) return `SCP: ${c.slice(0, 80)}`;
|
|
104
|
+
if (/rm\s+-rf|DROP TABLE|ALTER TABLE|CREATE TABLE/.test(c)) return `DESTRUCTIVE: ${c.slice(0, 120)}`;
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function runTool(name, args, sessionInfo) {
|
|
85
109
|
const tool = ALL_TOOLS.find(t => t.name === name);
|
|
86
110
|
if (!tool) return `Error: Unknown tool "${name}"`;
|
|
87
111
|
try {
|
|
88
112
|
const parsed = typeof args === 'string' ? JSON.parse(args) : args;
|
|
89
|
-
|
|
113
|
+
const normalized = normalizeArgs(parsed);
|
|
114
|
+
|
|
115
|
+
// Auto-capture memory BEFORE running the tool (for access/change logging)
|
|
116
|
+
if (sessionInfo) {
|
|
117
|
+
const { sessionDir, cwd } = sessionInfo;
|
|
118
|
+
try {
|
|
119
|
+
if (name === 'Read' && normalized.file_path) {
|
|
120
|
+
logFileAccess(cwd, normalized.file_path);
|
|
121
|
+
}
|
|
122
|
+
} catch {}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const result = await tool.run(normalized);
|
|
126
|
+
|
|
127
|
+
// Auto-capture memory AFTER running the tool
|
|
128
|
+
if (sessionInfo) {
|
|
129
|
+
const { sessionDir, cwd } = sessionInfo;
|
|
130
|
+
try {
|
|
131
|
+
if (name === 'Write' && normalized.file_path) {
|
|
132
|
+
const lines = (normalized.content || '').split('\n').length;
|
|
133
|
+
logFileChange(cwd, normalized.file_path, `Written ${lines} lines`);
|
|
134
|
+
} else if (name === 'Edit' && normalized.file_path) {
|
|
135
|
+
const preview = (normalized.old_string || '').slice(0, 60).replace(/\n/g, '↵');
|
|
136
|
+
logFileChange(cwd, normalized.file_path, `Edited: replaced "${preview}"`);
|
|
137
|
+
} else if (name === 'Bash') {
|
|
138
|
+
const hint = bashDecisionHint(normalized.command);
|
|
139
|
+
if (hint) appendDecision(sessionDir, hint);
|
|
140
|
+
}
|
|
141
|
+
} catch {}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return result;
|
|
90
145
|
} catch (e) {
|
|
91
146
|
return `Error executing ${name}: ${e.message}`;
|
|
92
147
|
}
|
|
@@ -104,14 +159,30 @@ async function waitForServer(serverUrl, retries = 10, delay = 2000) {
|
|
|
104
159
|
throw new Error(`Model server not responding at ${serverUrl}`);
|
|
105
160
|
}
|
|
106
161
|
|
|
107
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Stream a completion from the model server.
|
|
164
|
+
* Injects memoryContext into the system message if provided.
|
|
165
|
+
*/
|
|
166
|
+
async function streamCompletion(serverUrl, messages, onToken, memoryContext = '') {
|
|
108
167
|
await waitForServer(serverUrl, 5, 1000);
|
|
168
|
+
|
|
169
|
+
// Inject memory context into the system message (first message with role=system)
|
|
170
|
+
let finalMessages = messages;
|
|
171
|
+
if (memoryContext) {
|
|
172
|
+
finalMessages = messages.map((msg, idx) => {
|
|
173
|
+
if (idx === 0 && msg.role === 'system') {
|
|
174
|
+
return { ...msg, content: msg.content + memoryContext };
|
|
175
|
+
}
|
|
176
|
+
return msg;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
109
180
|
const res = await fetch(`${serverUrl}/v1/chat/completions`, {
|
|
110
181
|
method: 'POST',
|
|
111
182
|
headers: { 'Content-Type': 'application/json' },
|
|
112
183
|
body: JSON.stringify({
|
|
113
184
|
model: 'local',
|
|
114
|
-
messages,
|
|
185
|
+
messages: finalMessages,
|
|
115
186
|
stream: true,
|
|
116
187
|
temperature: 0.1,
|
|
117
188
|
max_tokens: 4096,
|
|
@@ -146,20 +217,65 @@ async function streamCompletion(serverUrl, messages, onToken) {
|
|
|
146
217
|
return fullContent;
|
|
147
218
|
}
|
|
148
219
|
|
|
149
|
-
|
|
220
|
+
/**
|
|
221
|
+
* Main agent loop.
|
|
222
|
+
*
|
|
223
|
+
* @param {string} serverUrl
|
|
224
|
+
* @param {Array} messages — full message array including system prompt at index 0
|
|
225
|
+
* @param {Function} onToken
|
|
226
|
+
* @param {Function} onToolCall
|
|
227
|
+
* @param {{ sessionDir: string, cwd: string } | null} sessionInfo
|
|
228
|
+
* @param {number} maxIterations
|
|
229
|
+
*/
|
|
230
|
+
export async function agentLoop(serverUrl, messages, onToken, onToolCall, sessionInfo = null, maxIterations = 15) {
|
|
231
|
+
// Extract the current task from the last user message for context assembly
|
|
232
|
+
const lastUserMsg = [...messages].reverse().find(m => m.role === 'user');
|
|
233
|
+
const currentTask = typeof lastUserMsg?.content === 'string' ? lastUserMsg.content : '';
|
|
234
|
+
|
|
150
235
|
for (let i = 0; i < maxIterations; i++) {
|
|
151
|
-
|
|
236
|
+
// Assemble memory context before each model call
|
|
237
|
+
let memoryContext = '';
|
|
238
|
+
if (sessionInfo) {
|
|
239
|
+
try {
|
|
240
|
+
const filesToConsider = extractFilePaths(currentTask);
|
|
241
|
+
memoryContext = assembleContext({
|
|
242
|
+
sessionDir: sessionInfo.sessionDir,
|
|
243
|
+
cwd: sessionInfo.cwd,
|
|
244
|
+
currentTask,
|
|
245
|
+
filesToConsider
|
|
246
|
+
});
|
|
247
|
+
} catch {}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const content = await streamCompletion(serverUrl, messages, onToken, memoryContext);
|
|
152
251
|
const toolCalls = parseToolCalls(content);
|
|
153
252
|
|
|
154
253
|
messages.push({ role: 'assistant', content });
|
|
155
254
|
|
|
156
|
-
if (toolCalls.length === 0)
|
|
255
|
+
if (toolCalls.length === 0) {
|
|
256
|
+
// Log final assistant response as a decision if it's substantive
|
|
257
|
+
if (sessionInfo && content.length > 50) {
|
|
258
|
+
try {
|
|
259
|
+
const summary = content.slice(0, 150).replace(/\n/g, ' ');
|
|
260
|
+
appendDecision(sessionInfo.sessionDir, `RESPONSE: ${summary}`);
|
|
261
|
+
} catch {}
|
|
262
|
+
}
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
157
265
|
|
|
158
266
|
const results = [];
|
|
159
267
|
for (const tc of toolCalls) {
|
|
160
268
|
onToolCall(tc.name, typeof tc.arguments === 'string' ? tc.arguments : JSON.stringify(tc.arguments));
|
|
161
|
-
const result = await runTool(tc.name, tc.arguments);
|
|
269
|
+
const result = await runTool(tc.name, tc.arguments, sessionInfo);
|
|
162
270
|
results.push(`[${tc.name} result]\n${String(result).slice(0, 8000)}`);
|
|
271
|
+
|
|
272
|
+
// Log tool execution as a decision entry
|
|
273
|
+
if (sessionInfo) {
|
|
274
|
+
try {
|
|
275
|
+
const argsPreview = JSON.stringify(tc.arguments || {}).slice(0, 80);
|
|
276
|
+
appendDecision(sessionInfo.sessionDir, `TOOL ${tc.name}: ${argsPreview}`);
|
|
277
|
+
} catch {}
|
|
278
|
+
}
|
|
163
279
|
}
|
|
164
280
|
|
|
165
281
|
messages.push({
|