tsunami-code 2.9.0 → 3.0.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/index.js +73 -8
- package/lib/loop.js +36 -2
- package/lib/prompt.js +4 -0
- package/lib/tools.js +196 -1
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -4,7 +4,8 @@ import chalk from 'chalk';
|
|
|
4
4
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
5
5
|
import { join } from 'path';
|
|
6
6
|
import os from 'os';
|
|
7
|
-
import { agentLoop, quickCompletion, setModel, getModel } from './lib/loop.js';
|
|
7
|
+
import { agentLoop, quickCompletion, setModel, getModel, tokenStats } from './lib/loop.js';
|
|
8
|
+
import { injectServerContext } from './lib/tools.js';
|
|
8
9
|
import { buildSystemPrompt } from './lib/prompt.js';
|
|
9
10
|
import { runPreflight, checkServer } from './lib/preflight.js';
|
|
10
11
|
import { setSession, undo, undoStackSize } from './lib/tools.js';
|
|
@@ -20,7 +21,7 @@ import {
|
|
|
20
21
|
getSessionContext
|
|
21
22
|
} from './lib/memory.js';
|
|
22
23
|
|
|
23
|
-
const VERSION = '
|
|
24
|
+
const VERSION = '3.0.0';
|
|
24
25
|
const CONFIG_DIR = join(os.homedir(), '.tsunami-code');
|
|
25
26
|
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
26
27
|
const DEFAULT_SERVER = 'https://radiometric-reita-amuck.ngrok-free.dev';
|
|
@@ -172,8 +173,31 @@ async function run() {
|
|
|
172
173
|
initProjectMemory(cwd);
|
|
173
174
|
setSession({ sessionDir, cwd });
|
|
174
175
|
|
|
176
|
+
// Inject server context into AgentTool
|
|
177
|
+
injectServerContext(serverUrl, buildSystemPrompt());
|
|
178
|
+
|
|
175
179
|
printBanner(serverUrl);
|
|
176
180
|
|
|
181
|
+
// Persistent history — load previous commands
|
|
182
|
+
const HISTORY_FILE = join(CONFIG_DIR, 'history.jsonl');
|
|
183
|
+
function appendHistory(line) {
|
|
184
|
+
try {
|
|
185
|
+
const entry = JSON.stringify({ cmd: line, ts: Date.now(), cwd }) + '\n';
|
|
186
|
+
import('fs').then(({ appendFileSync }) => { try { appendFileSync(HISTORY_FILE, entry, 'utf8'); } catch {} });
|
|
187
|
+
} catch {}
|
|
188
|
+
}
|
|
189
|
+
function loadHistory() {
|
|
190
|
+
try {
|
|
191
|
+
if (!existsSync(HISTORY_FILE)) return [];
|
|
192
|
+
return readFileSync(HISTORY_FILE, 'utf8')
|
|
193
|
+
.trim().split('\n').filter(Boolean)
|
|
194
|
+
.map(l => { try { return JSON.parse(l).cmd; } catch { return null; } })
|
|
195
|
+
.filter(Boolean).reverse().slice(0, 200);
|
|
196
|
+
} catch { return []; }
|
|
197
|
+
}
|
|
198
|
+
const historyEntries = loadHistory();
|
|
199
|
+
let historyIdx = -1;
|
|
200
|
+
|
|
177
201
|
// Preflight checks
|
|
178
202
|
process.stdout.write(dim(' Checking server connection...'));
|
|
179
203
|
const { errors, warnings } = await runPreflight(serverUrl);
|
|
@@ -356,10 +380,28 @@ async function run() {
|
|
|
356
380
|
console.log(red(` Unknown memory subcommand: ${sub}\n Try: /memory, /memory files, /memory view <file>, /memory clear\n`));
|
|
357
381
|
}
|
|
358
382
|
|
|
383
|
+
// Frustration detection — from leaked userPromptKeywords.ts pattern
|
|
384
|
+
const FRUSTRATION_PATTERNS = [
|
|
385
|
+
/\b(wtf|fuck|shit|damn|idiot|stupid|useless|broken|wrong|garbage|trash|terrible|awful|hate)\b/i,
|
|
386
|
+
/\b(not working|still broken|still wrong|same error|again|ugh|argh)\b/i,
|
|
387
|
+
/!{2,}/,
|
|
388
|
+
/\b(why (would|did|is|are|does|do) you|you (keep|always|never|can't|cannot|won't|don't))\b/i,
|
|
389
|
+
/\b(i (said|told|asked)|stop|listen|pay attention)\b/i
|
|
390
|
+
];
|
|
391
|
+
function detectFrustration(text) {
|
|
392
|
+
return FRUSTRATION_PATTERNS.some(p => p.test(text));
|
|
393
|
+
}
|
|
394
|
+
|
|
359
395
|
rl.on('line', async (input) => {
|
|
360
396
|
const line = input.trim();
|
|
361
397
|
if (!line) { rl.prompt(); return; }
|
|
362
398
|
|
|
399
|
+
// Append to persistent history
|
|
400
|
+
if (!line.startsWith('/')) {
|
|
401
|
+
appendHistory(line);
|
|
402
|
+
historyIdx = -1;
|
|
403
|
+
}
|
|
404
|
+
|
|
363
405
|
if (line.startsWith('/')) {
|
|
364
406
|
const parts = line.slice(1).split(' ');
|
|
365
407
|
const cmd = parts[0].toLowerCase();
|
|
@@ -384,6 +426,7 @@ async function run() {
|
|
|
384
426
|
['/status', 'Show context size and server'],
|
|
385
427
|
['/server <url>', 'Change model server URL'],
|
|
386
428
|
['/model [name]', 'Show or change active model (default: local)'],
|
|
429
|
+
['/history', 'Show recent command history'],
|
|
387
430
|
['/exit', 'Exit'],
|
|
388
431
|
];
|
|
389
432
|
for (const [c, desc] of cmds) {
|
|
@@ -430,13 +473,22 @@ async function run() {
|
|
|
430
473
|
console.log(dim(` CWD : ${cwd}\n`));
|
|
431
474
|
break;
|
|
432
475
|
}
|
|
433
|
-
case 'cost':
|
|
434
|
-
|
|
435
|
-
console.log(
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
476
|
+
case 'cost': {
|
|
477
|
+
const hasReal = tokenStats.requests > 0;
|
|
478
|
+
console.log(blue('\n Session Token Usage'));
|
|
479
|
+
if (hasReal) {
|
|
480
|
+
console.log(dim(` Input : ${tokenStats.input.toLocaleString()} tokens (actual)`));
|
|
481
|
+
console.log(dim(` Output : ${tokenStats.output.toLocaleString()} tokens (actual)`));
|
|
482
|
+
console.log(dim(` Total : ${(tokenStats.input + tokenStats.output).toLocaleString()} tokens`));
|
|
483
|
+
console.log(dim(` Requests: ${tokenStats.requests}`));
|
|
484
|
+
} else {
|
|
485
|
+
console.log(dim(` Input : ~${_inputTokens.toLocaleString()} (estimated)`));
|
|
486
|
+
console.log(dim(` Output : ~${_outputTokens.toLocaleString()} (estimated)`));
|
|
487
|
+
console.log(dim(` Total : ~${(_inputTokens + _outputTokens).toLocaleString()} (estimated)`));
|
|
488
|
+
}
|
|
489
|
+
console.log();
|
|
439
490
|
break;
|
|
491
|
+
}
|
|
440
492
|
case 'clear':
|
|
441
493
|
resetSession();
|
|
442
494
|
console.log(green(' Session cleared.\n'));
|
|
@@ -463,6 +515,14 @@ async function run() {
|
|
|
463
515
|
case 'memory':
|
|
464
516
|
await handleMemoryCommand(rest);
|
|
465
517
|
break;
|
|
518
|
+
case 'history': {
|
|
519
|
+
const recent = historyEntries.slice(0, 20);
|
|
520
|
+
if (recent.length === 0) { console.log(dim(' No history yet.\n')); break; }
|
|
521
|
+
console.log(blue(`\n Recent commands (${recent.length}):`));
|
|
522
|
+
recent.forEach((h, i) => console.log(dim(` ${String(i + 1).padStart(2)} ${h.slice(0, 100)}`)));
|
|
523
|
+
console.log();
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
466
526
|
case 'exit': case 'quit':
|
|
467
527
|
gracefulExit(0);
|
|
468
528
|
return;
|
|
@@ -479,6 +539,11 @@ async function run() {
|
|
|
479
539
|
userContent = `[Previous session summary]\n${lastSessionSummary}\n\n---\n\n${line}`;
|
|
480
540
|
}
|
|
481
541
|
|
|
542
|
+
// Frustration injection — tell the model to acknowledge and course-correct
|
|
543
|
+
if (detectFrustration(line)) {
|
|
544
|
+
userContent += '\n\n[system: User appears frustrated. Acknowledge the issue directly, do not repeat the same approach. Be concise and action-focused.]';
|
|
545
|
+
}
|
|
546
|
+
|
|
482
547
|
const fullMessages = [
|
|
483
548
|
{ role: 'system', content: systemPrompt },
|
|
484
549
|
...messages,
|
package/lib/loop.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fetch from 'node-fetch';
|
|
2
|
-
import { ALL_TOOLS } from './tools.js';
|
|
2
|
+
import { ALL_TOOLS, injectAgentLoop, injectServerContext } from './tools.js';
|
|
3
3
|
import {
|
|
4
4
|
assembleContext,
|
|
5
5
|
extractFilePaths,
|
|
@@ -29,6 +29,9 @@ function isDangerous(cmd) {
|
|
|
29
29
|
// Skip waitForServer after first successful connection
|
|
30
30
|
let _serverVerified = false;
|
|
31
31
|
|
|
32
|
+
// Real token tracking from API responses
|
|
33
|
+
export const tokenStats = { input: 0, output: 0, requests: 0 };
|
|
34
|
+
|
|
32
35
|
// Current model identifier — changeable at runtime via /model command
|
|
33
36
|
let _currentModel = 'local';
|
|
34
37
|
export function setModel(model) { _currentModel = model; }
|
|
@@ -234,6 +237,12 @@ async function streamCompletion(serverUrl, messages, onToken, memoryContext = ''
|
|
|
234
237
|
fullContent += token;
|
|
235
238
|
onToken(token);
|
|
236
239
|
}
|
|
240
|
+
// Capture real token counts from usage field (sent on final chunk by llama.cpp)
|
|
241
|
+
if (parsed.usage) {
|
|
242
|
+
tokenStats.input += parsed.usage.prompt_tokens || 0;
|
|
243
|
+
tokenStats.output += parsed.usage.completion_tokens || 0;
|
|
244
|
+
tokenStats.requests++;
|
|
245
|
+
}
|
|
237
246
|
}
|
|
238
247
|
}
|
|
239
248
|
|
|
@@ -278,6 +287,12 @@ export async function quickCompletion(serverUrl, systemPrompt, userMessage) {
|
|
|
278
287
|
}
|
|
279
288
|
}
|
|
280
289
|
|
|
290
|
+
// Self-register into tools.js so AgentTool can call back into us
|
|
291
|
+
// (done here at module load time to avoid circular import at parse time)
|
|
292
|
+
import('./tools.js').then(m => {
|
|
293
|
+
m.injectAgentLoop(agentLoop);
|
|
294
|
+
}).catch(() => {});
|
|
295
|
+
|
|
281
296
|
export async function agentLoop(serverUrl, messages, onToken, onToolCall, sessionInfo = null, confirmCallback = null, maxIterations = 15) {
|
|
282
297
|
const lastUserMsg = [...messages].reverse().find(m => m.role === 'user');
|
|
283
298
|
const currentTask = typeof lastUserMsg?.content === 'string' ? lastUserMsg.content : '';
|
|
@@ -347,7 +362,26 @@ export async function agentLoop(serverUrl, messages, onToken, onToolCall, sessio
|
|
|
347
362
|
|
|
348
363
|
onToolCall(tc.name, typeof tc.arguments === 'string' ? tc.arguments : JSON.stringify(tc.arguments));
|
|
349
364
|
const result = await runTool(tc.name, tc.arguments, sessionInfo, sessionFiles);
|
|
350
|
-
|
|
365
|
+
const resultStr = String(result);
|
|
366
|
+
|
|
367
|
+
// Handle Snip signal — surgically remove message indices from context
|
|
368
|
+
if (tc.name === 'Snip') {
|
|
369
|
+
try {
|
|
370
|
+
const sig = JSON.parse(resultStr);
|
|
371
|
+
if (sig.__snip__ && Array.isArray(sig.indices)) {
|
|
372
|
+
const toRemove = new Set(sig.indices.map(Number));
|
|
373
|
+
// messages[0] is system prompt, indices are 1-based user/assistant turns
|
|
374
|
+
const kept = messages.filter((_, i) => i === 0 || !toRemove.has(i - 1));
|
|
375
|
+
const removed = messages.length - kept.length;
|
|
376
|
+
messages.length = 0;
|
|
377
|
+
kept.forEach(m => messages.push(m));
|
|
378
|
+
results.push(`[Snip result]\nRemoved ${removed} messages from context. Reason: ${sig.reason}`);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
} catch {}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
results.push(`[${tc.name} result]\n${resultStr.slice(0, 8000)}`);
|
|
351
385
|
}
|
|
352
386
|
|
|
353
387
|
messages.push({
|
package/lib/prompt.js
CHANGED
|
@@ -67,8 +67,12 @@ Available tools: Bash, Read, Write, Edit, Glob, Grep, Note, Checkpoint. Use them
|
|
|
67
67
|
- **Note**: Save a permanent discovery to project memory (.tsunami/). Use liberally for traps, patterns, architectural decisions.
|
|
68
68
|
- **Checkpoint**: Save current task progress to session memory so work is resumable if the session ends.
|
|
69
69
|
- **WebFetch**: Fetch any URL and get the page content as text. Use for docs, GitHub files, APIs.
|
|
70
|
+
- **WebSearch**: Search the web via DuckDuckGo. Returns titles, URLs, snippets. Follow up with WebFetch.
|
|
70
71
|
- **TodoWrite**: Manage a persistent task list (add/complete/delete/list). Use for any multi-step task.
|
|
71
72
|
- **AskUser**: Ask the user a clarifying question when genuinely blocked. Use sparingly.
|
|
73
|
+
- **Agent**: Spawn a sub-agent to handle an independent task. Call multiple times in one response for parallel execution.
|
|
74
|
+
- **Snip**: Surgically remove specific messages from context to free space without losing everything.
|
|
75
|
+
- **Brief**: Write a working-memory note to yourself. Injected into next turn — ensures nothing is forgotten on long tasks.
|
|
72
76
|
</tools>
|
|
73
77
|
|
|
74
78
|
<reasoning_protocol>
|
package/lib/tools.js
CHANGED
|
@@ -453,4 +453,199 @@ Do NOT use for:
|
|
|
453
453
|
}
|
|
454
454
|
};
|
|
455
455
|
|
|
456
|
-
|
|
456
|
+
// ── WEB SEARCH ────────────────────────────────────────────────────────────────
|
|
457
|
+
export const WebSearchTool = {
|
|
458
|
+
name: 'WebSearch',
|
|
459
|
+
description: `Search the web and return results. Use when you need current information, documentation, error solutions, or anything not in the codebase.
|
|
460
|
+
|
|
461
|
+
Returns titles, URLs, and snippets for the top results. Follow up with WebFetch on a specific result to get the full content.`,
|
|
462
|
+
input_schema: {
|
|
463
|
+
type: 'object',
|
|
464
|
+
properties: {
|
|
465
|
+
query: { type: 'string', description: 'Search query' },
|
|
466
|
+
num_results: { type: 'number', description: 'Number of results to return (default 8, max 20)' }
|
|
467
|
+
},
|
|
468
|
+
required: ['query']
|
|
469
|
+
},
|
|
470
|
+
async run({ query, num_results = 8 }) {
|
|
471
|
+
try {
|
|
472
|
+
const n = Math.min(num_results, 20);
|
|
473
|
+
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
|
474
|
+
const controller = new AbortController();
|
|
475
|
+
const timer = setTimeout(() => controller.abort(), 12000);
|
|
476
|
+
const res = await fetch(url, {
|
|
477
|
+
signal: controller.signal,
|
|
478
|
+
headers: {
|
|
479
|
+
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36',
|
|
480
|
+
'Accept': 'text/html'
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
clearTimeout(timer);
|
|
484
|
+
if (!res.ok) return `Error: HTTP ${res.status}`;
|
|
485
|
+
const html = await res.text();
|
|
486
|
+
|
|
487
|
+
// Parse results from DuckDuckGo HTML
|
|
488
|
+
const results = [];
|
|
489
|
+
const resultBlocks = html.match(/<div class="result[^"]*"[\s\S]*?<\/div>\s*<\/div>/g) || [];
|
|
490
|
+
for (const block of resultBlocks.slice(0, n)) {
|
|
491
|
+
const titleMatch = block.match(/<a[^>]+class="result__a"[^>]*>([\s\S]*?)<\/a>/);
|
|
492
|
+
const urlMatch = block.match(/href="([^"]+)"/);
|
|
493
|
+
const snippetMatch = block.match(/<a[^>]+class="result__snippet"[^>]*>([\s\S]*?)<\/a>/);
|
|
494
|
+
|
|
495
|
+
const title = titleMatch ? titleMatch[1].replace(/<[^>]+>/g, '').trim() : '';
|
|
496
|
+
const href = urlMatch ? urlMatch[1] : '';
|
|
497
|
+
const snippet = snippetMatch ? snippetMatch[1].replace(/<[^>]+>/g, '').trim() : '';
|
|
498
|
+
|
|
499
|
+
// DuckDuckGo uses redirect URLs — extract the actual URL
|
|
500
|
+
const actualUrl = href.includes('uddg=')
|
|
501
|
+
? decodeURIComponent(href.match(/uddg=([^&]+)/)?.[1] || href)
|
|
502
|
+
: href;
|
|
503
|
+
|
|
504
|
+
if (title && actualUrl) {
|
|
505
|
+
results.push(`${results.length + 1}. ${title}\n ${actualUrl}\n ${snippet}`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (results.length === 0) return `No results found for: ${query}`;
|
|
510
|
+
return `Search results for "${query}":\n\n${results.join('\n\n')}`;
|
|
511
|
+
} catch (e) {
|
|
512
|
+
if (e.name === 'AbortError') return 'Error: Search timed out';
|
|
513
|
+
return `Error: ${e.message}`;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
// ── AGENT TOOL ────────────────────────────────────────────────────────────────
|
|
519
|
+
// Circular import prevention — agentLoop is injected at runtime by loop.js
|
|
520
|
+
let _agentLoopRef = null;
|
|
521
|
+
export function injectAgentLoop(fn) { _agentLoopRef = fn; }
|
|
522
|
+
|
|
523
|
+
export const AgentTool = {
|
|
524
|
+
name: 'Agent',
|
|
525
|
+
description: `Launch a sub-agent to handle an independent task in parallel. The sub-agent has access to all the same tools (Bash, Read, Write, Edit, Glob, Grep, WebFetch, WebSearch, etc.) and works autonomously until done.
|
|
526
|
+
|
|
527
|
+
USE THIS FOR:
|
|
528
|
+
- Independent subtasks that don't depend on each other (run multiple Agents in one response)
|
|
529
|
+
- Long-running research tasks you don't want in the main context
|
|
530
|
+
- Isolated operations (e.g. "analyze file A" + "analyze file B" simultaneously)
|
|
531
|
+
|
|
532
|
+
The agent returns a summary of what it did and what it found. Keep the task description specific and self-contained — the sub-agent has no knowledge of the current conversation.
|
|
533
|
+
|
|
534
|
+
IMPORTANT: You can call Agent multiple times in one response to run tasks truly in parallel.`,
|
|
535
|
+
input_schema: {
|
|
536
|
+
type: 'object',
|
|
537
|
+
properties: {
|
|
538
|
+
task: { type: 'string', description: 'Complete, self-contained task description for the sub-agent' },
|
|
539
|
+
serverUrl: { type: 'string', description: 'Override server URL (optional)' }
|
|
540
|
+
},
|
|
541
|
+
required: ['task']
|
|
542
|
+
},
|
|
543
|
+
async run({ task, serverUrl }) {
|
|
544
|
+
if (!_agentLoopRef) return 'Error: AgentTool not initialized (no agent loop reference)';
|
|
545
|
+
if (!_currentServerUrl) return 'Error: AgentTool not initialized (no server URL)';
|
|
546
|
+
|
|
547
|
+
const url = serverUrl || _currentServerUrl;
|
|
548
|
+
const subMessages = [
|
|
549
|
+
{ role: 'system', content: _agentSystemPrompt || 'You are a capable software engineering sub-agent. Complete the given task fully and return a summary of what you did.' },
|
|
550
|
+
{ role: 'user', content: task }
|
|
551
|
+
];
|
|
552
|
+
|
|
553
|
+
const outputTokens = [];
|
|
554
|
+
let done = false;
|
|
555
|
+
try {
|
|
556
|
+
await _agentLoopRef(
|
|
557
|
+
url,
|
|
558
|
+
subMessages,
|
|
559
|
+
(token) => { outputTokens.push(token); },
|
|
560
|
+
() => {}, // tool call display — silent in sub-agent
|
|
561
|
+
null, // no session info for sub-agents
|
|
562
|
+
null, // no confirm callback
|
|
563
|
+
10 // max iterations
|
|
564
|
+
);
|
|
565
|
+
done = true;
|
|
566
|
+
} catch (e) {
|
|
567
|
+
return `Sub-agent error: ${e.message}`;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Find the last assistant message as the result
|
|
571
|
+
const lastAssistant = [...subMessages].reverse().find(m => m.role === 'assistant');
|
|
572
|
+
const result = lastAssistant?.content || outputTokens.join('');
|
|
573
|
+
return `[Sub-agent result]\n${String(result).slice(0, 6000)}`;
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
// Server URL + system prompt injected by loop.js at startup
|
|
578
|
+
let _currentServerUrl = null;
|
|
579
|
+
let _agentSystemPrompt = null;
|
|
580
|
+
export function injectServerContext(serverUrl, systemPrompt) {
|
|
581
|
+
_currentServerUrl = serverUrl;
|
|
582
|
+
_agentSystemPrompt = systemPrompt;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// ── SNIP TOOL ─────────────────────────────────────────────────────────────────
|
|
586
|
+
// The model calls this to surgically remove specific turns from context
|
|
587
|
+
// loop.js handles the actual splice — this is a signal tool
|
|
588
|
+
export const SnipTool = {
|
|
589
|
+
name: 'Snip',
|
|
590
|
+
description: `Remove specific conversation turns from context to free up space, without losing the whole conversation like /compact does.
|
|
591
|
+
|
|
592
|
+
Use when:
|
|
593
|
+
- A specific tool result was very large and is no longer needed (e.g. a full file read you've already processed)
|
|
594
|
+
- An early exploration phase produced lots of output that's no longer relevant
|
|
595
|
+
- You want to keep recent context but drop stale earlier parts
|
|
596
|
+
|
|
597
|
+
The 'indices' are 0-based positions in the conversation (0 = first user message after system prompt).
|
|
598
|
+
Use /status to see current message count, then pick which to snip.`,
|
|
599
|
+
input_schema: {
|
|
600
|
+
type: 'object',
|
|
601
|
+
properties: {
|
|
602
|
+
indices: {
|
|
603
|
+
type: 'array',
|
|
604
|
+
items: { type: 'number' },
|
|
605
|
+
description: 'Array of 0-based message indices to remove from context'
|
|
606
|
+
},
|
|
607
|
+
reason: { type: 'string', description: 'Why you are snipping these (logged for transparency)' }
|
|
608
|
+
},
|
|
609
|
+
required: ['indices']
|
|
610
|
+
},
|
|
611
|
+
async run({ indices, reason }) {
|
|
612
|
+
// Actual snipping happens in agentLoop — this signals intent
|
|
613
|
+
return JSON.stringify({ __snip__: true, indices, reason: reason || 'context management' });
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
// ── BRIEF TOOL ────────────────────────────────────────────────────────────────
|
|
618
|
+
export const BriefTool = {
|
|
619
|
+
name: 'Brief',
|
|
620
|
+
description: `Write a self-briefing note to yourself about the current task state. This gets injected into context on the next turn, ensuring nothing is forgotten even if context is trimmed.
|
|
621
|
+
|
|
622
|
+
Use after major milestones in long tasks. Different from Checkpoint — Brief is for your own working memory, not user-visible progress.
|
|
623
|
+
|
|
624
|
+
Example:
|
|
625
|
+
Brief({ content: "Working on auth refactor. Done: DB schema updated, sessions table migrated. Current: updating login route. Not done: logout, password reset. Key discovery: sessions table uses TEXT not UUID for user_id — do not cast." })`,
|
|
626
|
+
input_schema: {
|
|
627
|
+
type: 'object',
|
|
628
|
+
properties: {
|
|
629
|
+
content: { type: 'string', description: 'Your working memory note — what you know, what is done, what is next, what to watch out for' }
|
|
630
|
+
},
|
|
631
|
+
required: ['content']
|
|
632
|
+
},
|
|
633
|
+
async run({ content }) {
|
|
634
|
+
try {
|
|
635
|
+
if (!_sessionDir) return 'Brief recorded (no session initialized)';
|
|
636
|
+
// Reuse checkpoint mechanism — both go to session context
|
|
637
|
+
updateContext(_sessionDir, `[BRIEF]\n${content}`);
|
|
638
|
+
return 'Brief saved to working memory.';
|
|
639
|
+
} catch (e) {
|
|
640
|
+
return `Brief recorded (write failed: ${e.message})`;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
export const ALL_TOOLS = [
|
|
646
|
+
BashTool, ReadTool, WriteTool, EditTool, GlobTool, GrepTool,
|
|
647
|
+
NoteTool, CheckpointTool,
|
|
648
|
+
WebFetchTool, WebSearchTool,
|
|
649
|
+
TodoWriteTool, AskUserTool,
|
|
650
|
+
AgentTool, SnipTool, BriefTool
|
|
651
|
+
];
|