codemini-cli 0.2.9 → 0.3.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/package.json +1 -1
- package/souls/anime.md +13 -2
- package/souls/caveman.md +12 -2
- package/souls/ceo.md +14 -3
- package/souls/default.md +10 -2
- package/souls/pirate.md +13 -3
- package/souls/playful.md +13 -3
- package/souls/professional.md +13 -3
- package/src/cli.js +2 -1
- package/src/commands/chat.js +2 -0
- package/src/core/agent-loop.js +208 -8
- package/src/core/ast.js +2 -55
- package/src/core/bounded-cache.js +121 -0
- package/src/core/chat-runtime.js +24 -10
- package/src/core/constants.js +171 -0
- package/src/core/crypto-utils.js +18 -0
- package/src/core/default-system-prompt.js +2 -2
- package/src/core/project-index.js +127 -28
- package/src/core/provider/openai-compatible.js +16 -4
- package/src/core/shell-profile.js +4 -0
- package/src/core/soul.js +3 -2
- package/src/core/tools.js +52 -72
- package/src/tui/chat-app.js +67 -14
- package/src/tui/tool-activity/presenters/command.js +14 -1
- package/src/tui/tool-activity/presenters/files.js +23 -1
- package/src/tui/tool-activity/presenters/system.js +1 -1
- package/src/tui/tool-narration/presenters/glob.js +2 -2
- package/src/tui/tool-narration/presenters/grep.js +2 -2
- package/src/tui/tool-narration/presenters/list.js +2 -2
- package/src/tui/tool-narration/presenters/run.js +2 -2
package/package.json
CHANGED
package/souls/anime.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
1
|
Respond with a light anime-inspired tone.
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
|
|
3
|
+
Style guidelines:
|
|
4
|
+
- Be cheerful and encouraging, like a helpful companion on an adventure.
|
|
5
|
+
- Use occasional playful expressions (e.g. "好嘞,开干!", "搞定啦~", "Let's go!") but keep them natural and brief.
|
|
6
|
+
- Add a touch of enthusiasm to progress updates and completions.
|
|
7
|
+
- When something goes wrong, stay upbeat and frame it as a solvable challenge.
|
|
8
|
+
- Use em dashes, tildes, or exclamation marks sparingly for personality.
|
|
9
|
+
|
|
10
|
+
Boundaries:
|
|
11
|
+
- Never sacrifice clarity, accuracy, or usefulness for style.
|
|
12
|
+
- Do not overdo catchphrases, memes, or anime references.
|
|
13
|
+
- Do not use this style as an excuse to be verbose — stay concise.
|
|
14
|
+
- Technical terms, code, file paths, and command output must remain precise and unchanged.
|
package/souls/caveman.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
1
|
Respond with a simple caveman-inspired tone.
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
|
|
3
|
+
Style guidelines:
|
|
4
|
+
- Keep sentences short, punchy, and concrete.
|
|
5
|
+
- Speak in direct, action-oriented phrases — "We fix bug. Code good now." style.
|
|
6
|
+
- Use simple metaphors from the physical world (hunting, building, fire) when explaining concepts.
|
|
7
|
+
- Celebrate successes with primal enthusiasm — "Bug crushed! Tribe safe."
|
|
8
|
+
|
|
9
|
+
Boundaries:
|
|
10
|
+
- Keep explanations readable and technically accurate.
|
|
11
|
+
- Do not make wording so primitive that instructions or code suggestions become unclear.
|
|
12
|
+
- Technical terms, code, file paths, and command output must remain precise and unchanged.
|
|
13
|
+
- Never sacrifice correctness for the caveman gimmick.
|
package/souls/ceo.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
-
Respond with a bold CEO-style tone.
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
Respond with a bold, decisive CEO-style tone.
|
|
2
|
+
|
|
3
|
+
Style guidelines:
|
|
4
|
+
- Speak with confidence and urgency — focus on what matters, cut the noise.
|
|
5
|
+
- Frame every decision around impact, tradeoffs, and execution velocity.
|
|
6
|
+
- Use executive phrasing: "Here's the play...", "The right call is...", "Let's ship this."
|
|
7
|
+
- Acknowledge risks briefly, then commit to a clear direction.
|
|
8
|
+
- Celebrate wins like closing a deal — "Clean execution. Moving on."
|
|
9
|
+
|
|
10
|
+
Boundaries:
|
|
11
|
+
- Do not imitate any real person or use cringe corporate buzzwords ("synergy", "paradigm shift").
|
|
12
|
+
- Do not let the style override careful technical judgment — precision still wins.
|
|
13
|
+
- Technical terms, code, file paths, and command output must remain precise and unchanged.
|
|
14
|
+
- Stay concise — CEOs do not ramble.
|
package/souls/default.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
Respond in a clear, calm, helpful tone.
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
|
|
3
|
+
Style guidelines:
|
|
4
|
+
- Be concise, friendly, and practical in every response.
|
|
5
|
+
- Prioritize clarity and directness over embellishment.
|
|
6
|
+
- Use simple, natural language — no forced personality or quirks.
|
|
7
|
+
|
|
8
|
+
Boundaries:
|
|
9
|
+
- Avoid roleplay, slang overload, or exaggerated personality.
|
|
10
|
+
- Never sacrifice accuracy or usefulness for style.
|
|
11
|
+
- Technical terms, code, file paths, and command output must remain precise and unchanged.
|
package/souls/pirate.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
-
Respond with a playful pirate-inspired tone.
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
Respond with a playful pirate-inspired tone, matey.
|
|
2
|
+
|
|
3
|
+
Style guidelines:
|
|
4
|
+
- Use light nautical flavor and pirate expressions — "Aye", "Shiver me timbers", "Arrr", "Set sail".
|
|
5
|
+
- Frame tasks as voyages and adventures — "Let's chart a course for that bug."
|
|
6
|
+
- Celebrate successes like plundering treasure — "Shipshape! Bug walkin' the plank."
|
|
7
|
+
- Keep the tone adventurous but grounded.
|
|
8
|
+
|
|
9
|
+
Boundaries:
|
|
10
|
+
- Keep the answer clear, useful, and technically accurate first, pirate flavor second.
|
|
11
|
+
- Do not overdo slang — every sentence should still be understandable on first read.
|
|
12
|
+
- Technical terms, code, file paths, and command output must remain precise and unchanged.
|
|
13
|
+
- Never let roleplay reduce precision or hide important warnings.
|
package/souls/playful.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
-
Respond with a witty and
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
Respond with a witty, lively, and slightly cheeky tone.
|
|
2
|
+
|
|
3
|
+
Style guidelines:
|
|
4
|
+
- Add personality and humor naturally — a well-placed quip or clever analogy goes a long way.
|
|
5
|
+
- Use casual, conversational phrasing — "So here's the fun part...", "Plot twist:", "Easy fix incoming."
|
|
6
|
+
- React to bugs and errors with good-natured humor — "Well, that's a creative way to break things."
|
|
7
|
+
- Celebrate wins with flair — "Nailed it. Next?"
|
|
8
|
+
|
|
9
|
+
Boundaries:
|
|
10
|
+
- Keep the answer readable and practical first — humor is the seasoning, not the main dish.
|
|
11
|
+
- Do not let jokes obscure instructions, warnings, or technical accuracy.
|
|
12
|
+
- Technical terms, code, file paths, and command output must remain precise and unchanged.
|
|
13
|
+
- Avoid sarcasm that could feel dismissive of the user's question.
|
package/souls/professional.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
-
Respond in a polished, professional tone.
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
Respond in a polished, professional, and authoritative tone.
|
|
2
|
+
|
|
3
|
+
Style guidelines:
|
|
4
|
+
- Keep phrasing precise, confident, and concise — like a senior engineer briefing a team.
|
|
5
|
+
- Prefer structured explanations: numbered steps, clear headings, and logical flow.
|
|
6
|
+
- State conclusions first, then back them up — lead with the answer, follow with reasoning.
|
|
7
|
+
- Use measured, deliberate language — "The recommended approach is...", "This ensures..."
|
|
8
|
+
|
|
9
|
+
Boundaries:
|
|
10
|
+
- Prefer structured explanations over playful expression.
|
|
11
|
+
- Avoid colloquialisms, emojis, or overly casual phrasing.
|
|
12
|
+
- Technical terms, code, file paths, and command output must remain precise and unchanged.
|
|
13
|
+
- Never pad responses with filler — every sentence should earn its place.
|
package/src/cli.js
CHANGED
|
@@ -3,8 +3,9 @@ import { handleRun } from './commands/run.js';
|
|
|
3
3
|
import { handleConfig } from './commands/config.js';
|
|
4
4
|
import { handleDoctor } from './commands/doctor.js';
|
|
5
5
|
import { handleSkill } from './commands/skill.js';
|
|
6
|
+
import pkg from '../package.json' with { type: 'json' };
|
|
6
7
|
|
|
7
|
-
const VERSION =
|
|
8
|
+
const VERSION = pkg.version;
|
|
8
9
|
|
|
9
10
|
function printHelp() {
|
|
10
11
|
console.log(`codemini ${VERSION}
|
package/src/commands/chat.js
CHANGED
|
@@ -92,6 +92,7 @@ export async function handleChat(args) {
|
|
|
92
92
|
const React = (await import('react')).default;
|
|
93
93
|
const { render } = await import('ink');
|
|
94
94
|
const { ChatApp } = await import('../tui/chat-app.js');
|
|
95
|
+
|
|
95
96
|
const instance = render(
|
|
96
97
|
React.createElement(ChatApp, {
|
|
97
98
|
runtime,
|
|
@@ -103,5 +104,6 @@ export async function handleChat(args) {
|
|
|
103
104
|
version: pkg.version
|
|
104
105
|
})
|
|
105
106
|
);
|
|
107
|
+
|
|
106
108
|
await instance.waitUntilExit();
|
|
107
109
|
}
|
package/src/core/agent-loop.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
|
+
import { BoundedCache } from './bounded-cache.js';
|
|
4
5
|
|
|
5
6
|
function safeJsonParse(raw) {
|
|
6
7
|
if (!raw || typeof raw !== 'string') return {};
|
|
@@ -206,8 +207,16 @@ const TOOL_RESULTS_SUBDIR = 'tool-results';
|
|
|
206
207
|
|
|
207
208
|
let currentResultDir = null;
|
|
208
209
|
let resultDirReady = false;
|
|
209
|
-
const storedResults = new
|
|
210
|
-
|
|
210
|
+
const storedResults = new BoundedCache({
|
|
211
|
+
maxSize: 64,
|
|
212
|
+
ttlMs: 30 * 60 * 1000,
|
|
213
|
+
onEvict(key, value) {
|
|
214
|
+
if (value?.filePath) {
|
|
215
|
+
fs.unlink(value.filePath).catch(() => {});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}); // callId -> { filePath, summary }
|
|
219
|
+
const readCache = new BoundedCache({ maxSize: 128, ttlMs: 10 * 60 * 1000 }); // "path:startLine:endLine:mtimeMs" -> true
|
|
211
220
|
|
|
212
221
|
function generatePreview(content) {
|
|
213
222
|
if (content.length <= PREVIEW_SIZE_BYTES) {
|
|
@@ -272,7 +281,7 @@ Summary: ${summary}
|
|
|
272
281
|
|
|
273
282
|
export function clearResultStore() {
|
|
274
283
|
const files = [];
|
|
275
|
-
for (const [, val] of storedResults) {
|
|
284
|
+
for (const [, val] of storedResults.entries()) {
|
|
276
285
|
files.push(val.filePath);
|
|
277
286
|
}
|
|
278
287
|
storedResults.clear();
|
|
@@ -288,11 +297,6 @@ export function checkReadDedup(filePath, startLine, endLine, mtimeMs) {
|
|
|
288
297
|
return true;
|
|
289
298
|
}
|
|
290
299
|
readCache.set(key, true);
|
|
291
|
-
// Keep cache bounded
|
|
292
|
-
if (readCache.size > 100) {
|
|
293
|
-
const firstKey = readCache.keys().next().value;
|
|
294
|
-
readCache.delete(firstKey);
|
|
295
|
-
}
|
|
296
300
|
return false;
|
|
297
301
|
}
|
|
298
302
|
|
|
@@ -406,6 +410,161 @@ export function trimInline(value, maxLen = 72) {
|
|
|
406
410
|
return `${s.slice(0, maxLen - 3)}...`;
|
|
407
411
|
}
|
|
408
412
|
|
|
413
|
+
function normalizeAssistantText(value) {
|
|
414
|
+
return String(value || '').trim();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function hasTrailingToolContext(messages = []) {
|
|
418
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
419
|
+
const message = messages[index];
|
|
420
|
+
if (!message || typeof message !== 'object') continue;
|
|
421
|
+
if (message.role === 'tool') return true;
|
|
422
|
+
if (message.role === 'assistant' || message.role === 'user') return false;
|
|
423
|
+
}
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function isGenericCompletionText(text) {
|
|
428
|
+
const normalized = normalizeAssistantText(text).toLowerCase();
|
|
429
|
+
if (!normalized) return false;
|
|
430
|
+
const genericPhrases = new Set([
|
|
431
|
+
'done',
|
|
432
|
+
'completed',
|
|
433
|
+
'complete',
|
|
434
|
+
'finished',
|
|
435
|
+
'task completed',
|
|
436
|
+
'all done',
|
|
437
|
+
'ok',
|
|
438
|
+
'okay',
|
|
439
|
+
'已完成',
|
|
440
|
+
'已完成任务',
|
|
441
|
+
'完成',
|
|
442
|
+
'任务完成'
|
|
443
|
+
]);
|
|
444
|
+
return genericPhrases.has(normalized);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function shouldAskForConcreteFinalAnswer(text, messages = []) {
|
|
448
|
+
if (!hasTrailingToolContext(messages)) return false;
|
|
449
|
+
const normalized = normalizeAssistantText(text);
|
|
450
|
+
if (!normalized) return true;
|
|
451
|
+
return isGenericCompletionText(normalized);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function isBroadRepositoryAnalysisTask(text) {
|
|
455
|
+
const normalized = String(text || '').trim().toLowerCase();
|
|
456
|
+
if (!normalized) return false;
|
|
457
|
+
return (
|
|
458
|
+
/optimi|improve|analy[sz]e|audit|review|overview|architecture|codebase|repository|repo/.test(normalized) ||
|
|
459
|
+
/项目.*优化|项目.*问题|可优化|分析这个项目|看看.*项目|代码库|仓库/.test(String(text || ''))
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function parseProjectIndexSummary(text) {
|
|
464
|
+
const sourceRoots = [];
|
|
465
|
+
const entryCandidates = [];
|
|
466
|
+
const candidateFiles = [];
|
|
467
|
+
for (const line of String(text || '').split(/\r?\n/)) {
|
|
468
|
+
const trimmed = line.trim();
|
|
469
|
+
if (trimmed.startsWith('source_roots:')) {
|
|
470
|
+
sourceRoots.push(
|
|
471
|
+
...String(trimmed.slice('source_roots:'.length))
|
|
472
|
+
.split(',')
|
|
473
|
+
.map((value) => value.trim())
|
|
474
|
+
.filter(Boolean)
|
|
475
|
+
);
|
|
476
|
+
} else if (trimmed.startsWith('entry_candidates:')) {
|
|
477
|
+
entryCandidates.push(
|
|
478
|
+
...String(trimmed.slice('entry_candidates:'.length))
|
|
479
|
+
.split(',')
|
|
480
|
+
.map((value) => value.trim())
|
|
481
|
+
.filter(Boolean)
|
|
482
|
+
);
|
|
483
|
+
} else if (trimmed.startsWith('- ')) {
|
|
484
|
+
const match = trimmed.match(/^- ([^ ]+)/);
|
|
485
|
+
if (match?.[1]) candidateFiles.push(match[1].trim());
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return { sourceRoots, entryCandidates, candidateFiles };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function createAnalysisGuardState(userPrompt) {
|
|
492
|
+
return {
|
|
493
|
+
active: isBroadRepositoryAnalysisTask(userPrompt),
|
|
494
|
+
indexQueried: false,
|
|
495
|
+
sourceRoots: new Set(),
|
|
496
|
+
entryCandidates: new Set(),
|
|
497
|
+
candidateFiles: new Set(),
|
|
498
|
+
relevantSourceReads: new Set(),
|
|
499
|
+
blockedExplorations: 0
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function topLevelPath(value) {
|
|
504
|
+
const normalized = String(value || '').replace(/\\/g, '/').replace(/^\.\/+/, '').trim();
|
|
505
|
+
return normalized.split('/')[0] || '';
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function isRelevantSourcePath(filePath, state) {
|
|
509
|
+
const normalized = String(filePath || '').replace(/\\/g, '/').trim();
|
|
510
|
+
if (!normalized) return false;
|
|
511
|
+
if (state.candidateFiles.has(normalized) || state.entryCandidates.has(normalized)) return true;
|
|
512
|
+
for (const root of state.sourceRoots) {
|
|
513
|
+
if (normalized === root || normalized.startsWith(`${root}/`)) return true;
|
|
514
|
+
}
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function blockedExplorationReason(toolName, args, state) {
|
|
519
|
+
if (!state.active) return '';
|
|
520
|
+
if (!state.indexQueried && toolName !== 'query_project_index') {
|
|
521
|
+
return 'Use query_project_index before broad repository exploration so the next reads stay focused on relevant source files.';
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const target = String(args?.path || args?.pattern || args?.query || '').replace(/\\/g, '/').trim();
|
|
525
|
+
const top = topLevelPath(target);
|
|
526
|
+
if (!top) return '';
|
|
527
|
+
|
|
528
|
+
if (['skills', 'souls', 'templates', '.codemini', '.codemini-project'].includes(top)) {
|
|
529
|
+
return `Skip ${top}/ for broad repository analysis unless the user explicitly asks for it. Inspect relevant source files first.`;
|
|
530
|
+
}
|
|
531
|
+
if (top === 'tests' && state.relevantSourceReads.size < 2) {
|
|
532
|
+
return 'Inspect the next relevant source files before reading tests. Broad analysis should be grounded in production code first.';
|
|
533
|
+
}
|
|
534
|
+
return '';
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function noteAnalysisEvidence(state, toolName, args, toolResult) {
|
|
538
|
+
if (!state.active) return;
|
|
539
|
+
if (toolName === 'query_project_index') {
|
|
540
|
+
state.indexQueried = true;
|
|
541
|
+
const summary = parseProjectIndexSummary(JSON.stringify(toolResult));
|
|
542
|
+
for (const root of summary.sourceRoots) state.sourceRoots.add(root);
|
|
543
|
+
for (const entry of summary.entryCandidates) state.entryCandidates.add(entry);
|
|
544
|
+
for (const file of summary.candidateFiles) state.candidateFiles.add(file);
|
|
545
|
+
const projectMap = toolResult?.project_map || {};
|
|
546
|
+
for (const root of projectMap.source_roots || []) state.sourceRoots.add(String(root));
|
|
547
|
+
for (const entry of projectMap.entry_candidates || []) state.entryCandidates.add(String(entry));
|
|
548
|
+
for (const match of toolResult?.matches || []) {
|
|
549
|
+
if (match?.file) state.candidateFiles.add(String(match.file));
|
|
550
|
+
}
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (toolName === 'read') {
|
|
555
|
+
const filePath = String(toolResult?.path || args?.path || '').split(':')[0];
|
|
556
|
+
if (isRelevantSourcePath(filePath, state)) {
|
|
557
|
+
state.relevantSourceReads.add(filePath);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function needsMoreAnalysisEvidence(state) {
|
|
563
|
+
if (!state.active) return false;
|
|
564
|
+
if (!state.indexQueried) return true;
|
|
565
|
+
return state.relevantSourceReads.size < 2;
|
|
566
|
+
}
|
|
567
|
+
|
|
409
568
|
function normalizeToolCallName(name) {
|
|
410
569
|
return String(name || '').trim();
|
|
411
570
|
}
|
|
@@ -509,6 +668,8 @@ export async function runAgentLoop({
|
|
|
509
668
|
|
|
510
669
|
let finalText = '';
|
|
511
670
|
let lastAssistantText = '';
|
|
671
|
+
let pendingSummaryNudges = 0;
|
|
672
|
+
const analysisGuard = createAnalysisGuardState(userPrompt);
|
|
512
673
|
const alwaysAllowSet = new Set((Array.isArray(alwaysAllowTools) ? alwaysAllowTools : []).map((t) => String(t)));
|
|
513
674
|
|
|
514
675
|
// Mutable tool list — grows as tool_search loads deferred tools
|
|
@@ -522,6 +683,10 @@ export async function runAgentLoop({
|
|
|
522
683
|
tools: activeTools
|
|
523
684
|
});
|
|
524
685
|
|
|
686
|
+
if (completion?.incomplete) {
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
|
|
525
690
|
const toolCalls = Array.isArray(completion.toolCalls) ? completion.toolCalls : [];
|
|
526
691
|
const assistantText = completion.text || '';
|
|
527
692
|
lastAssistantText = assistantText || lastAssistantText;
|
|
@@ -546,10 +711,30 @@ export async function runAgentLoop({
|
|
|
546
711
|
}
|
|
547
712
|
|
|
548
713
|
if (toolCalls.length === 0) {
|
|
714
|
+
if (needsMoreAnalysisEvidence(analysisGuard) && pendingSummaryNudges < 2) {
|
|
715
|
+
pendingSummaryNudges += 1;
|
|
716
|
+
messages.push({
|
|
717
|
+
role: 'user',
|
|
718
|
+
content:
|
|
719
|
+
'You have not inspected enough relevant source files yet. Query the project index if needed, then inspect the next relevant source files before concluding. Do not stop after unrelated directories, tests, skills, souls, or templates.'
|
|
720
|
+
});
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
if (shouldAskForConcreteFinalAnswer(assistantText, messages.slice(0, -1)) && pendingSummaryNudges < 2) {
|
|
724
|
+
pendingSummaryNudges += 1;
|
|
725
|
+
messages.push({
|
|
726
|
+
role: 'user',
|
|
727
|
+
content:
|
|
728
|
+
'You have already inspected tool results. Before stopping, check whether the task is actually complete. If it is, provide a concise final answer with specific findings or concrete next steps. If it is not, continue with the next tool call.'
|
|
729
|
+
});
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
549
732
|
finalText = assistantText;
|
|
550
733
|
return { text: finalText, messages, steps: step + 1 };
|
|
551
734
|
}
|
|
552
735
|
|
|
736
|
+
pendingSummaryNudges = 0;
|
|
737
|
+
|
|
553
738
|
if (executionMode === 'plan') {
|
|
554
739
|
const plannedLines = callsToPlanSummary(toolCalls);
|
|
555
740
|
finalText = [
|
|
@@ -615,6 +800,20 @@ export async function runAgentLoop({
|
|
|
615
800
|
throw new Error(`Unknown tool: ${call.name}`);
|
|
616
801
|
}
|
|
617
802
|
|
|
803
|
+
const blockedReason = blockedExplorationReason(toolName, args, analysisGuard);
|
|
804
|
+
if (blockedReason) {
|
|
805
|
+
analysisGuard.blockedExplorations += 1;
|
|
806
|
+
const content = clipToolResult({ error: blockedReason }, toolResultMaxChars);
|
|
807
|
+
if (onEvent) {
|
|
808
|
+
onEvent({ type: 'tool:error', name: displayName, id: call.id, arguments: args, durationMs: 0, summary: trimInline(blockedReason, 120) });
|
|
809
|
+
}
|
|
810
|
+
return {
|
|
811
|
+
callId: call.id,
|
|
812
|
+
content,
|
|
813
|
+
error: true
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
|
|
618
817
|
let toolResult;
|
|
619
818
|
try {
|
|
620
819
|
toolResult = await handler(args);
|
|
@@ -638,6 +837,7 @@ export async function runAgentLoop({
|
|
|
638
837
|
|
|
639
838
|
// P1b: Use per-tool formatter if available, else fallback
|
|
640
839
|
let formatted = formatToolResult(toolResult, toolName, args, toolFormatters, toolResultMaxChars);
|
|
840
|
+
noteAnalysisEvidence(analysisGuard, toolName, args, toolResult);
|
|
641
841
|
|
|
642
842
|
// P2: If tool_search loaded deferred tools, inject their schemas into activeTools
|
|
643
843
|
if (toolName === 'tool_search' && toolResult && Array.isArray(toolResult.schemas)) {
|
package/src/core/ast.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import crypto from 'node:crypto';
|
|
4
3
|
import { createRequire } from 'node:module';
|
|
5
4
|
import { Parser, Language, Query } from 'web-tree-sitter';
|
|
5
|
+
import { LANGUAGE_ALIASES, EXTENSION_LANGUAGE_MAP } from './constants.js';
|
|
6
|
+
import { sha256Prefixed as sha256 } from './crypto-utils.js';
|
|
6
7
|
|
|
7
8
|
const require = createRequire(import.meta.url);
|
|
8
9
|
|
|
@@ -20,56 +21,6 @@ const WRAPPER_NODE_TYPES = new Set([
|
|
|
20
21
|
'template_function',
|
|
21
22
|
'template_type'
|
|
22
23
|
]);
|
|
23
|
-
const LANGUAGE_ALIASES = {
|
|
24
|
-
javascript: 'js',
|
|
25
|
-
js: 'js',
|
|
26
|
-
jsx: 'js',
|
|
27
|
-
typescript: 'ts',
|
|
28
|
-
ts: 'ts',
|
|
29
|
-
tsx: 'tsx',
|
|
30
|
-
python: 'python',
|
|
31
|
-
py: 'python',
|
|
32
|
-
go: 'go',
|
|
33
|
-
c: 'c',
|
|
34
|
-
cpp: 'cpp',
|
|
35
|
-
'c++': 'cpp',
|
|
36
|
-
bash: 'bash',
|
|
37
|
-
sh: 'bash',
|
|
38
|
-
shell: 'bash',
|
|
39
|
-
java: 'java',
|
|
40
|
-
rust: 'rust',
|
|
41
|
-
rs: 'rust',
|
|
42
|
-
csharp: 'csharp',
|
|
43
|
-
'c#': 'csharp',
|
|
44
|
-
cs: 'csharp',
|
|
45
|
-
php: 'php',
|
|
46
|
-
ruby: 'ruby',
|
|
47
|
-
rb: 'ruby'
|
|
48
|
-
};
|
|
49
|
-
const EXTENSION_LANGUAGE_MAP = {
|
|
50
|
-
'.js': 'js',
|
|
51
|
-
'.jsx': 'js',
|
|
52
|
-
'.mjs': 'js',
|
|
53
|
-
'.cjs': 'js',
|
|
54
|
-
'.ts': 'ts',
|
|
55
|
-
'.tsx': 'tsx',
|
|
56
|
-
'.py': 'python',
|
|
57
|
-
'.go': 'go',
|
|
58
|
-
'.c': 'c',
|
|
59
|
-
'.h': 'c',
|
|
60
|
-
'.cpp': 'cpp',
|
|
61
|
-
'.cc': 'cpp',
|
|
62
|
-
'.cxx': 'cpp',
|
|
63
|
-
'.hpp': 'cpp',
|
|
64
|
-
'.hh': 'cpp',
|
|
65
|
-
'.java': 'java',
|
|
66
|
-
'.rs': 'rust',
|
|
67
|
-
'.cs': 'csharp',
|
|
68
|
-
'.php': 'php',
|
|
69
|
-
'.rb': 'ruby',
|
|
70
|
-
'.sh': 'bash',
|
|
71
|
-
'.bash': 'bash'
|
|
72
|
-
};
|
|
73
24
|
const LANGUAGE_WASM_PATHS = {
|
|
74
25
|
js: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-javascript.wasm'),
|
|
75
26
|
ts: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-typescript.wasm'),
|
|
@@ -94,10 +45,6 @@ const parserInitPromise = Parser.init({
|
|
|
94
45
|
});
|
|
95
46
|
const languageCache = new Map();
|
|
96
47
|
|
|
97
|
-
function sha256(input) {
|
|
98
|
-
return `sha256:${crypto.createHash('sha256').update(String(input || '')).digest('hex')}`;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
48
|
function clipText(text, maxLen = 220) {
|
|
102
49
|
const normalized = String(text || '').replace(/\s+/g, ' ').trim();
|
|
103
50
|
if (normalized.length <= maxLen) return normalized;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 有限大小 + TTL 的 Map 缓存,带主动清理和可选的 onEvict 钩子。
|
|
3
|
+
* 用于替代无界 Map 以防止长时间运行时的内存泄漏。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const DEFAULT_MAX_SIZE = 128;
|
|
7
|
+
const DEFAULT_TTL_MS = 10 * 60 * 1000; // 10 分钟
|
|
8
|
+
|
|
9
|
+
export class BoundedCache {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this._maxSize = options.maxSize ?? DEFAULT_MAX_SIZE;
|
|
12
|
+
this._ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
13
|
+
this._onEvict = options.onEvict ?? null;
|
|
14
|
+
this._map = new Map();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get(key) {
|
|
18
|
+
const entry = this._map.get(key);
|
|
19
|
+
if (!entry) return undefined;
|
|
20
|
+
if (this._expired(entry)) {
|
|
21
|
+
this._removeEntry(key, entry);
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return entry.value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
has(key) {
|
|
28
|
+
return this.get(key) !== undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
set(key, value) {
|
|
32
|
+
if (this._map.has(key)) {
|
|
33
|
+
const old = this._map.get(key);
|
|
34
|
+
if (this._onEvict && old !== undefined) {
|
|
35
|
+
try { this._onEvict(key, old.value); } catch {}
|
|
36
|
+
}
|
|
37
|
+
this._map.delete(key);
|
|
38
|
+
}
|
|
39
|
+
this._map.set(key, { value, ts: Date.now() });
|
|
40
|
+
this._evict();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
delete(key) {
|
|
44
|
+
const entry = this._map.get(key);
|
|
45
|
+
if (!entry) return false;
|
|
46
|
+
this._removeEntry(key, entry);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
clear() {
|
|
51
|
+
if (this._onEvict) {
|
|
52
|
+
for (const [key, entry] of this._map) {
|
|
53
|
+
try { this._onEvict(key, entry.value); } catch {}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
this._map.clear();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get size() {
|
|
60
|
+
this._prune();
|
|
61
|
+
return this._map.size;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
keys() {
|
|
65
|
+
this._prune();
|
|
66
|
+
return this._map.keys();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
values() {
|
|
70
|
+
this._prune();
|
|
71
|
+
const out = [];
|
|
72
|
+
for (const entry of this._map.values()) {
|
|
73
|
+
out.push(entry.value);
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
entries() {
|
|
79
|
+
this._prune();
|
|
80
|
+
const out = [];
|
|
81
|
+
for (const [key, entry] of this._map.entries()) {
|
|
82
|
+
out.push([key, entry.value]);
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─── 内部方法 ──────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
_expired(entry) {
|
|
90
|
+
return Date.now() - entry.ts > this._ttlMs;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_removeEntry(key, entry) {
|
|
94
|
+
this._map.delete(key);
|
|
95
|
+
if (this._onEvict) {
|
|
96
|
+
try { this._onEvict(key, entry.value); } catch {}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 清理所有过期条目 */
|
|
101
|
+
_prune() {
|
|
102
|
+
for (const [key, entry] of this._map) {
|
|
103
|
+
if (this._expired(entry)) {
|
|
104
|
+
this._removeEntry(key, entry);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** 按数量裁剪最旧的条目 */
|
|
110
|
+
_evict() {
|
|
111
|
+
this._prune();
|
|
112
|
+
if (this._map.size <= this._maxSize) return;
|
|
113
|
+
const iter = this._map.keys();
|
|
114
|
+
while (this._map.size > this._maxSize) {
|
|
115
|
+
const oldest = iter.next().value;
|
|
116
|
+
if (oldest === undefined) break;
|
|
117
|
+
const entry = this._map.get(oldest);
|
|
118
|
+
this._removeEntry(oldest, entry);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|