codemini-cli 0.3.9 → 0.4.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/README.md +50 -6
- package/deployment.md +6 -6
- package/package.json +3 -1
- package/src/core/agent-loop.js +103 -115
- package/src/core/chat-runtime.js +134 -6
- package/src/core/command-evaluator.js +66 -0
- package/src/core/command-policy.js +16 -0
- package/src/core/command-risk.js +148 -0
- package/src/core/config-store.js +2 -0
- package/src/core/constants.js +0 -1
- package/src/core/context-compact.js +32 -8
- package/src/core/default-system-prompt.js +15 -8
- package/src/core/dream-consolidate.js +54 -14
- package/src/core/dream-evaluator.js +99 -0
- package/src/core/fff-adapter.js +1 -1
- package/src/core/memory-store.js +3 -2
- package/src/core/paths.js +1 -1
- package/src/core/project-index.js +2 -2
- package/src/core/provider/openai-compatible.js +40 -5
- package/src/core/shell-profile.js +13 -9
- package/src/core/tool-args.js +181 -0
- package/src/core/tool-output.js +184 -0
- package/src/core/tools.js +118 -315
- package/src/tui/chat-app.js +362 -45
- package/src/tui/tool-activity/presenters/misc.js +14 -0
- package/src/tui/tool-activity/presenters/system.js +1 -1
|
@@ -9,14 +9,14 @@ function getToolFewShotBlock() {
|
|
|
9
9
|
Use these as style examples for tool calls:
|
|
10
10
|
|
|
11
11
|
Current working directory: ${cwd}
|
|
12
|
-
When a tool takes
|
|
12
|
+
When a tool takes path, build it from the current working directory and prefer absolute paths.
|
|
13
13
|
If the user mentions a project-relative path like src/app.ts, resolve it from ${cwd} instead of guessing parent directories.
|
|
14
14
|
|
|
15
15
|
1. File discovery then read
|
|
16
16
|
User: compare the auth flow
|
|
17
17
|
Assistant: first narrow the search with the project index
|
|
18
18
|
Tool: query_project_index({"query":"auth flow","path":"src","max_results":3})
|
|
19
|
-
Tool: read({"
|
|
19
|
+
Tool: read({"path":"${cwd}/src/auth/service.ts"})
|
|
20
20
|
|
|
21
21
|
If the visible tool list does not include a needed capability, load it with tool_search instead of assuming it does not exist.
|
|
22
22
|
Example:
|
|
@@ -27,7 +27,7 @@ Tool: glob({"pattern":"src/**/*.ts"})
|
|
|
27
27
|
User: rename loginUser to signInUser
|
|
28
28
|
Assistant: first find the exact occurrences
|
|
29
29
|
Tool: grep({"pattern":"loginUser","path":"src"})
|
|
30
|
-
Tool: edit({"
|
|
30
|
+
Tool: edit({"path":"${cwd}/src/auth/service.ts","old_text":"loginUser","new_text":"signInUser"})
|
|
31
31
|
|
|
32
32
|
3. Read a specific range
|
|
33
33
|
User: inspect the reducer around line 120
|
|
@@ -43,11 +43,18 @@ Assistant: keep the checklist updated as each phase finishes, and do not give a
|
|
|
43
43
|
5. Create a new file
|
|
44
44
|
User: add a notes file
|
|
45
45
|
Assistant: create the file directly
|
|
46
|
-
Tool: write({"
|
|
46
|
+
Tool: write({"path":"${cwd}/notes.txt","content":"todo\\n"})
|
|
47
47
|
|
|
48
|
-
6.
|
|
49
|
-
When you notice a reusable pattern, a user correction, a repeated failure, or a stable preference —
|
|
50
|
-
|
|
48
|
+
6. Save a high-signal observation to memory
|
|
49
|
+
When you notice a reusable pattern, a user correction, a repeated failure, or a stable preference — save it to persistent memory. Choose scope carefully:
|
|
50
|
+
- scope "user" for personal preferences (language, reply style, interaction habits)
|
|
51
|
+
- scope "global" for cross-project lessons (environment quirks, general tool workflows)
|
|
52
|
+
- scope "project" for project-specific knowledge (architecture conventions, local config, test commands, file locations)
|
|
53
|
+
|
|
54
|
+
Examples:
|
|
55
|
+
Tool: save_memory({"content":"User prefers tab size 2 for all JSON files","scope":"user","kind":"preference"})
|
|
56
|
+
Tool: save_memory({"content":"This project uses vitest, not jest — run tests with npx vitest run","scope":"project","kind":"pattern"})
|
|
57
|
+
Tool: save_memory({"content":"WSL2 bash exec prefix does not support cd as a command","scope":"global","kind":"correction"})
|
|
51
58
|
|
|
52
59
|
7. Run a dream loop consolidation pass
|
|
53
60
|
When you want to review and consolidate inbox entries into long-term memory.
|
|
@@ -66,7 +73,7 @@ Tool: tool_search({"query":"web_search"})
|
|
|
66
73
|
Tool: web_search({"query":"latest pnpm release","max_results":5})
|
|
67
74
|
|
|
68
75
|
Prefer these direct tool shapes over multi-step metadata reads or shell fallbacks.
|
|
69
|
-
Prefer explicit absolute
|
|
76
|
+
Prefer explicit absolute path values when the current working directory is known.`;
|
|
70
77
|
}
|
|
71
78
|
|
|
72
79
|
function getEnvBlock() {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { listMemories, listInbox, archiveEntry, promoteMemory } from './memory-store.js';
|
|
2
2
|
import { writeDreamAuditReport } from './dream-audit.js';
|
|
3
|
+
import { evaluateInboxBatch } from './dream-evaluator.js';
|
|
3
4
|
|
|
4
5
|
const LONGTERM_TYPES = new Set(['preference', 'pattern', 'win', 'decision']);
|
|
5
6
|
const OPERATIONAL_TYPES = new Set(['correction', 'failure', 'gap', 'observation']);
|
|
@@ -8,14 +9,6 @@ function normalizeText(value) {
|
|
|
8
9
|
return String(value || '').trim().toLowerCase();
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
function mapInboxScopeToMemoryScope(scope) {
|
|
12
|
-
const value = normalizeText(scope);
|
|
13
|
-
if (value === 'repo' || value === 'project') return 'project';
|
|
14
|
-
if (value === 'thread') return 'global';
|
|
15
|
-
if (value === 'user') return 'user';
|
|
16
|
-
return 'global';
|
|
17
|
-
}
|
|
18
|
-
|
|
19
12
|
function chooseLifecycle(type) {
|
|
20
13
|
const value = normalizeText(type);
|
|
21
14
|
if (LONGTERM_TYPES.has(value)) return 'longterm';
|
|
@@ -55,7 +48,10 @@ export async function runDreamConsolidation({
|
|
|
55
48
|
const filesRead = ['memory/inbox/*', 'memory/global.json', 'memory/user.json', 'memory/project/*.json'];
|
|
56
49
|
const filesChanged = [];
|
|
57
50
|
|
|
51
|
+
/* ── Phase 1: 规则预过滤(快速剔除明显垃圾) ─────────────────── */
|
|
52
|
+
const candidates = [];
|
|
58
53
|
const seen = new Map();
|
|
54
|
+
|
|
59
55
|
for (const entry of inbox) {
|
|
60
56
|
const summaryKey = normalizeText(entry.summary);
|
|
61
57
|
if (!summaryKey) {
|
|
@@ -78,14 +74,58 @@ export async function runDreamConsolidation({
|
|
|
78
74
|
continue;
|
|
79
75
|
}
|
|
80
76
|
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
candidates.push(entry);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (candidates.length === 0) {
|
|
81
|
+
const report = { timestamp: new Date().toISOString(), filesRead, filesChanged: [], candidatesGenerated: inbox.length, promotions, rejections, archives };
|
|
82
|
+
if (!dryRun && writeAudit) {
|
|
83
|
+
const reportPath = await writeDreamAuditReport(report);
|
|
84
|
+
report.auditReport = reportPath;
|
|
85
|
+
}
|
|
86
|
+
return { ok: true, dryRun, ...report };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* ── Phase 2: LLM 批量评估(质量门控 + scope 分类 + 内容提炼) ── */
|
|
90
|
+
const llmResults = dryRun
|
|
91
|
+
? candidates.map((e) => ({ id: e.id, action: 'keep', scope: 'global', kind: e.type || 'observation', content: e.details || e.summary, summary: e.summary, confidence: 0.9 }))
|
|
92
|
+
: await evaluateInboxBatch({ entries: candidates, config, workspaceRoot });
|
|
93
|
+
|
|
94
|
+
const resultMap = new Map(llmResults.map((r) => [r.id, r]));
|
|
95
|
+
|
|
96
|
+
/* ── Phase 3: 按评估结果 promote 或 archive ─────────────────── */
|
|
97
|
+
for (const entry of candidates) {
|
|
98
|
+
const evaluation = resultMap.get(entry.id);
|
|
99
|
+
|
|
100
|
+
if (!evaluation || evaluation.action === 'discard') {
|
|
101
|
+
const reason = evaluation?.reason || 'LLM discarded';
|
|
102
|
+
if (!dryRun) await archiveEntry(entry, 'discarded-by-evaluator', reason);
|
|
103
|
+
rejections.push({ summary: entry.summary, reason: `evaluator-discard: ${reason}` });
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const promoteScope = evaluation.scope || 'global';
|
|
108
|
+
const lifecycle = chooseLifecycle(evaluation.kind);
|
|
109
|
+
const enrichedEntry = {
|
|
110
|
+
...entry,
|
|
111
|
+
/* 用 LLM 提炼后的内容覆盖原始报错 */
|
|
112
|
+
summary: evaluation.summary || entry.summary,
|
|
113
|
+
details: evaluation.content || entry.details || entry.summary,
|
|
114
|
+
type: evaluation.kind || entry.type || 'observation'
|
|
115
|
+
};
|
|
83
116
|
|
|
84
117
|
if (!dryRun) {
|
|
85
118
|
try {
|
|
86
|
-
await promoteMemory({
|
|
87
|
-
|
|
88
|
-
|
|
119
|
+
await promoteMemory({
|
|
120
|
+
entry: enrichedEntry,
|
|
121
|
+
scope: promoteScope,
|
|
122
|
+
lifecycle,
|
|
123
|
+
workspaceRoot,
|
|
124
|
+
config,
|
|
125
|
+
confidence: evaluation.confidence || 0.8
|
|
126
|
+
});
|
|
127
|
+
filesChanged.push({ file: `memory/${promoteScope}.json`, why: `Promoted "${enrichedEntry.summary}" as ${lifecycle} (${promoteScope})` });
|
|
128
|
+
promotions.push({ summary: enrichedEntry.summary, scope: promoteScope, lifecycle, rationale: evaluation.kind, confidence: evaluation.confidence });
|
|
89
129
|
} catch (error) {
|
|
90
130
|
const reason = String(error?.message || error || 'promotion failed').slice(0, 180);
|
|
91
131
|
await archiveEntry(entry, 'promotion-failed', reason);
|
|
@@ -95,7 +135,7 @@ export async function runDreamConsolidation({
|
|
|
95
135
|
continue;
|
|
96
136
|
}
|
|
97
137
|
|
|
98
|
-
promotions.push({ summary:
|
|
138
|
+
promotions.push({ summary: enrichedEntry.summary, scope: promoteScope, lifecycle, rationale: evaluation.kind, confidence: evaluation.confidence, dryRun: true });
|
|
99
139
|
}
|
|
100
140
|
|
|
101
141
|
const report = {
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createChatCompletion } from './provider/index.js';
|
|
2
|
+
|
|
3
|
+
const EVAL_TIMEOUT_MS = 30000;
|
|
4
|
+
|
|
5
|
+
const SYSTEM_PROMPT = `You are a memory consolidation evaluator for a coding assistant. You receive a batch of inbox items (tool errors, observations, etc.) and decide for each one:
|
|
6
|
+
|
|
7
|
+
1. **keep or discard** — Does this contain a reusable, durable insight? Discard transient errors, one-off issues, and noise.
|
|
8
|
+
2. **scope** — "global" for cross-project knowledge (e.g., "WSL bash exec does not support cd"), "project" for project-specific context (e.g., "this project uses vitest for testing").
|
|
9
|
+
3. **kind** — One of: pattern, observation, correction, decision, failure
|
|
10
|
+
4. **content** — A refined, actionable sentence describing the insight. NOT the raw error text.
|
|
11
|
+
5. **summary** — A short label (under 80 chars) for quick scanning.
|
|
12
|
+
6. **confidence** — 0.5–1.0 based on how certain and durable the insight is.
|
|
13
|
+
|
|
14
|
+
Respond with valid JSON only, no markdown fences:
|
|
15
|
+
{"results":[{"id":"<inbox-id>","action":"keep","scope":"global|project","kind":"pattern|observation|correction|decision|failure","content":"...","summary":"...","confidence":0.8},{"id":"<inbox-id>","action":"discard","reason":"..."}]}
|
|
16
|
+
|
|
17
|
+
Rules:
|
|
18
|
+
- Raw tool error messages are NOT insights by themselves. Only keep if they reveal a reusable lesson.
|
|
19
|
+
- "exit 127", "command not found", "permission denied", "blocked by policy" → always discard (transient/config issues)
|
|
20
|
+
- A repeated pattern across multiple errors → keep as a "pattern" or "correction"
|
|
21
|
+
- Project-specific paths, file names, or commands → scope "project"
|
|
22
|
+
- General coding/environment knowledge → scope "global"
|
|
23
|
+
- If in doubt, discard. Memory is expensive; only promote what future sessions will genuinely benefit from.`;
|
|
24
|
+
|
|
25
|
+
function parseResults(text) {
|
|
26
|
+
try {
|
|
27
|
+
const json = JSON.parse(text);
|
|
28
|
+
if (!json?.results || !Array.isArray(json.results)) return [];
|
|
29
|
+
return json.results.map((r) => ({
|
|
30
|
+
id: String(r.id || ''),
|
|
31
|
+
action: r.action === 'keep' ? 'keep' : 'discard',
|
|
32
|
+
scope: r.scope === 'project' ? 'project' : 'global',
|
|
33
|
+
kind: ['pattern', 'observation', 'correction', 'decision', 'failure'].includes(r.kind) ? r.kind : 'observation',
|
|
34
|
+
content: String(r.content || '').slice(0, 300),
|
|
35
|
+
summary: String(r.summary || '').slice(0, 120),
|
|
36
|
+
confidence: Math.min(1, Math.max(0.5, Number(r.confidence) || 0.7)),
|
|
37
|
+
reason: String(r.reason || '')
|
|
38
|
+
}));
|
|
39
|
+
} catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 用 LLM 批量评估 inbox 条目,决定保留/丢弃、scope、内容提炼。
|
|
46
|
+
* @param {{ entries: Array, config: object, workspaceRoot?: string }} params
|
|
47
|
+
* @returns {Promise<Array<{ id, action, scope?, kind?, content?, summary?, confidence?, reason? }>>}
|
|
48
|
+
*/
|
|
49
|
+
export async function evaluateInboxBatch({ entries, config, workspaceRoot }) {
|
|
50
|
+
if (!entries || entries.length === 0) return [];
|
|
51
|
+
|
|
52
|
+
const batch = entries.map((e) => ({
|
|
53
|
+
id: e.id,
|
|
54
|
+
type: e.type || '',
|
|
55
|
+
source: e.source || '',
|
|
56
|
+
summary: (e.summary || '').slice(0, 150),
|
|
57
|
+
details: (e.details || '').slice(0, 400)
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const result = await createChatCompletion({
|
|
62
|
+
sdkProvider: config?.sdk?.provider,
|
|
63
|
+
baseUrl: config?.gateway?.base_url,
|
|
64
|
+
apiKey: config?.gateway?.api_key,
|
|
65
|
+
model: config?.model?.name,
|
|
66
|
+
messages: [
|
|
67
|
+
{ role: 'system', content: SYSTEM_PROMPT },
|
|
68
|
+
{
|
|
69
|
+
role: 'user',
|
|
70
|
+
content: `Evaluate these ${batch.length} inbox items. Workspace: ${workspaceRoot || process.cwd()}\n\n${JSON.stringify(batch, null, 2)}`
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
temperature: 0,
|
|
74
|
+
timeoutMs: EVAL_TIMEOUT_MS
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const text = result?.text || '';
|
|
78
|
+
const parsed = parseResults(text);
|
|
79
|
+
/* 确保每个 entry 都有结果,LLM 没返回的一律 discard */
|
|
80
|
+
const covered = new Set(parsed.map((r) => r.id));
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
if (!covered.has(entry.id)) {
|
|
83
|
+
parsed.push({
|
|
84
|
+
id: entry.id,
|
|
85
|
+
action: 'discard',
|
|
86
|
+
reason: 'LLM did not return a result for this entry'
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return parsed;
|
|
91
|
+
} catch {
|
|
92
|
+
/* LLM 调用失败 → 全部 discard(fail-safe) */
|
|
93
|
+
return entries.map((e) => ({
|
|
94
|
+
id: e.id,
|
|
95
|
+
action: 'discard',
|
|
96
|
+
reason: 'LLM evaluation failed'
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
}
|
package/src/core/fff-adapter.js
CHANGED
package/src/core/memory-store.js
CHANGED
|
@@ -375,7 +375,8 @@ export async function promoteMemory({
|
|
|
375
375
|
lifecycle = 'operational',
|
|
376
376
|
workspaceRoot = process.cwd(),
|
|
377
377
|
projectAlias = '',
|
|
378
|
-
config = {}
|
|
378
|
+
config = {},
|
|
379
|
+
confidence = 0.9
|
|
379
380
|
} = {}) {
|
|
380
381
|
if (!entry?.summary) throw new Error('Entry with summary is required for promotion');
|
|
381
382
|
const lc = validateLifecycle(lifecycle);
|
|
@@ -386,7 +387,7 @@ export async function promoteMemory({
|
|
|
386
387
|
kind: entry.type || 'note',
|
|
387
388
|
summary: normalizeMemoryText(entry.summary),
|
|
388
389
|
source: `dream-promote:${entry.id}`,
|
|
389
|
-
confidence: 0.
|
|
390
|
+
confidence: Math.min(1, Math.max(0.5, confidence)),
|
|
390
391
|
replaceSimilar: true,
|
|
391
392
|
workspaceRoot,
|
|
392
393
|
projectAlias,
|
package/src/core/paths.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
|
|
4
4
|
const GLOBAL_APP_DIR = 'codemini-global';
|
|
5
5
|
const PROJECT_APP_DIR = '.codemini';
|
|
6
|
-
const PROJECT_INDEX_DIR = '.codemini
|
|
6
|
+
const PROJECT_INDEX_DIR = '.codemini';
|
|
7
7
|
|
|
8
8
|
export function getBaseConfigDir() {
|
|
9
9
|
if (process.env.CODEMINI_GLOBAL_DIR) {
|
|
@@ -387,7 +387,7 @@ export async function initializeProjectIndex(cwd = process.cwd()) {
|
|
|
387
387
|
projectRoot: targetRoot,
|
|
388
388
|
projectMap,
|
|
389
389
|
fileIndex,
|
|
390
|
-
summary: `initialized ${path.basename(targetRoot) || '.'}/.codemini
|
|
390
|
+
summary: `initialized ${path.basename(targetRoot) || '.'}/.codemini (${Array.isArray(fileIndex?.files) ? fileIndex.files.length : 0} files)`
|
|
391
391
|
};
|
|
392
392
|
})();
|
|
393
393
|
initCache.set(cacheKey, promise);
|
|
@@ -447,7 +447,7 @@ export async function refreshIndexedFile(cwd = process.cwd(), relativePath = '')
|
|
|
447
447
|
path: projectRelativePath,
|
|
448
448
|
projectRoot,
|
|
449
449
|
action,
|
|
450
|
-
summary: `${action} ${path.basename(projectRoot) || '.'}/.codemini
|
|
450
|
+
summary: `${action} ${path.basename(projectRoot) || '.'}/.codemini for ${projectRelativePath}`
|
|
451
451
|
};
|
|
452
452
|
}
|
|
453
453
|
|
|
@@ -54,6 +54,36 @@ async function parseJsonResponse(response) {
|
|
|
54
54
|
return response.json();
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
function isRetryableStatus(status) {
|
|
58
|
+
return status === 408 || status === 409 || status === 425 || status === 429 || status >= 500;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isRetryableError(error) {
|
|
62
|
+
const name = String(error?.name || '');
|
|
63
|
+
if (name === 'AbortError' || name === 'TimeoutError') return false;
|
|
64
|
+
const message = String(error?.message || error || '');
|
|
65
|
+
return /fetch failed|network|socket|ECONNRESET|ETIMEDOUT|EAI_AGAIN/i.test(message);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function fetchWithRetry(url, init, { maxRetries = 0 } = {}) {
|
|
69
|
+
const attempts = Math.max(0, Number(maxRetries) || 0) + 1;
|
|
70
|
+
let lastError;
|
|
71
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
72
|
+
try {
|
|
73
|
+
const response = await fetch(url, init);
|
|
74
|
+
if (response.ok || !isRetryableStatus(response.status) || attempt === attempts - 1) {
|
|
75
|
+
return response;
|
|
76
|
+
}
|
|
77
|
+
await response.arrayBuffer().catch(() => null);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
lastError = error;
|
|
80
|
+
if (!isRetryableError(error) || attempt === attempts - 1) throw error;
|
|
81
|
+
}
|
|
82
|
+
await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
|
|
83
|
+
}
|
|
84
|
+
throw lastError || new Error('Gateway request failed');
|
|
85
|
+
}
|
|
86
|
+
|
|
57
87
|
async function* iterateSseEvents(stream) {
|
|
58
88
|
const decoder = new TextDecoder();
|
|
59
89
|
let buffer = '';
|
|
@@ -318,12 +348,12 @@ export async function createChatCompletion({
|
|
|
318
348
|
maxRetries = 2
|
|
319
349
|
}) {
|
|
320
350
|
const payload = buildPayload({ model, temperature, messages, tools });
|
|
321
|
-
const response = await
|
|
351
|
+
const response = await fetchWithRetry(buildChatCompletionsUrl(baseUrl), {
|
|
322
352
|
method: 'POST',
|
|
323
353
|
headers: createHeaders(apiKey),
|
|
324
354
|
body: JSON.stringify(payload),
|
|
325
355
|
signal: AbortSignal.timeout(timeoutMs)
|
|
326
|
-
});
|
|
356
|
+
}, { maxRetries });
|
|
327
357
|
const data = await parseJsonResponse(response);
|
|
328
358
|
const message = data?.choices?.[0]?.message || {};
|
|
329
359
|
const text = sanitizeMiniMaxText(model, extractTextContent(message.content));
|
|
@@ -386,12 +416,12 @@ export async function createChatCompletionStream({
|
|
|
386
416
|
}
|
|
387
417
|
}
|
|
388
418
|
const payload = buildPayload({ model, temperature, messages, tools, stream: true });
|
|
389
|
-
const response = await
|
|
419
|
+
const response = await fetchWithRetry(buildChatCompletionsUrl(baseUrl), {
|
|
390
420
|
method: 'POST',
|
|
391
421
|
headers: createHeaders(apiKey),
|
|
392
422
|
body: JSON.stringify(payload),
|
|
393
423
|
signal: controller.signal
|
|
394
|
-
});
|
|
424
|
+
}, { maxRetries });
|
|
395
425
|
if (!response.ok || !response.body) {
|
|
396
426
|
const text = await response.text().catch(() => '');
|
|
397
427
|
throw new Error(`Gateway error ${response.status}: ${text || response.statusText}`);
|
|
@@ -402,7 +432,8 @@ export async function createChatCompletionStream({
|
|
|
402
432
|
let usage = null;
|
|
403
433
|
let miniMaxStreamState = { rawContent: '', visibleText: '' };
|
|
404
434
|
|
|
405
|
-
|
|
435
|
+
try {
|
|
436
|
+
for await (const chunk of iterateSseEvents(response.body)) {
|
|
406
437
|
usage = chunk?.usage || usage;
|
|
407
438
|
const choice0 = chunk?.choices?.[0] || {};
|
|
408
439
|
const delta = choice0?.delta || {};
|
|
@@ -452,6 +483,10 @@ export async function createChatCompletionStream({
|
|
|
452
483
|
if (choice0?.finish_reason) {
|
|
453
484
|
break;
|
|
454
485
|
}
|
|
486
|
+
}
|
|
487
|
+
} finally {
|
|
488
|
+
timeoutSignal.removeEventListener('abort', onAbort);
|
|
489
|
+
if (externalSignal) externalSignal.removeEventListener('abort', onAbort);
|
|
455
490
|
}
|
|
456
491
|
|
|
457
492
|
const result = buildFinalStreamResult(text, toolCallsByIndex, usage, messages);
|
|
@@ -23,8 +23,10 @@ const SHELL_PROFILES = {
|
|
|
23
23
|
'npm',
|
|
24
24
|
'npx',
|
|
25
25
|
'python',
|
|
26
|
+
'python3',
|
|
26
27
|
'py',
|
|
27
28
|
'pip',
|
|
29
|
+
'pip3',
|
|
28
30
|
'get-childitem',
|
|
29
31
|
'get-content',
|
|
30
32
|
'select-string',
|
|
@@ -70,7 +72,9 @@ const SHELL_PROFILES = {
|
|
|
70
72
|
'npm',
|
|
71
73
|
'npx',
|
|
72
74
|
'python',
|
|
75
|
+
'python3',
|
|
73
76
|
'pip',
|
|
77
|
+
'pip3',
|
|
74
78
|
'ls',
|
|
75
79
|
'cat',
|
|
76
80
|
'sed',
|
|
@@ -142,11 +146,11 @@ export function getShellSystemPrompt(value) {
|
|
|
142
146
|
ALWAYS prefer dedicated tools over raw shell commands:
|
|
143
147
|
- The visible default tool list is intentionally small. If a needed capability is not currently listed, do not assume it is unavailable — call tool_search to load additional tools first
|
|
144
148
|
- Use query_project_index first for broad repository understanding. It combines project-map metadata with indexed file symbols so you can narrow candidates before reading source files
|
|
145
|
-
- Use read to inspect files — NEVER use cat, head, or tail via run.
|
|
149
|
+
- Use read to inspect files — NEVER use cat, head, or tail via run. Use canonical shapes like {path:"src/app.ts"}, {path:"src/app.ts:10-40"}, or {path:"src/app.ts", start_line:10, end_line:40}
|
|
146
150
|
- Use grep to search file contents — NEVER use grep or rg via run
|
|
147
151
|
- Use list for directory-by-directory filesystem discovery. If you specifically need pattern-based file lookup like src/**/*.ts, load glob with tool_search instead of falling back to run
|
|
148
|
-
- Use edit to modify existing files — this is the DEFAULT path for code changes.
|
|
149
|
-
- Use write only for creating new files or complete rewrites (set full_file_rewrite=true for existing code files).
|
|
152
|
+
- Use edit to modify existing files — this is the DEFAULT path for code changes. Prefer {path:"src/app.ts", old_text:"foo", new_text:"bar"}
|
|
153
|
+
- Use write only for creating new files or complete rewrites (set full_file_rewrite=true for existing code files). Prefer {path:"notes.txt", content:"..."}
|
|
150
154
|
- Use update_todos to manage the session todo checklist for complex work. Provide the full current list each time and usually keep exactly one item in_progress
|
|
151
155
|
- Use read_plan and update_plan to recover or sync structured plan state when plan progress was interrupted (for example by transient gateway/model errors)
|
|
152
156
|
- Use run for shell commands. For long-running processes (dev servers, watchers), set run_in_background=true when you know you do not need the final result immediately. Long-running commands may also be backgrounded automatically
|
|
@@ -164,7 +168,7 @@ Some tools are loaded on demand through tool_search. Common examples:
|
|
|
164
168
|
- glob for pattern-based file lookup
|
|
165
169
|
- ast_query and read_ast_node for advanced AST-scoped reads and edits
|
|
166
170
|
- list_background_tasks, get_background_task, and stop_background_task for managing long-running background commands
|
|
167
|
-
-
|
|
171
|
+
- save_memory, list_memory, search_memory, and forget_memory for persistent memory operations
|
|
168
172
|
|
|
169
173
|
For structural code edits (functions, classes, methods), prefer AST-scoped reads before editing:
|
|
170
174
|
- Common one-shot workflow: read(path, query=..., capture_name=...) → edit with symbol or ast_target
|
|
@@ -177,15 +181,15 @@ For background commands: use run to launch. If you need management tools that ar
|
|
|
177
181
|
Common tool call patterns:
|
|
178
182
|
- Query the project index first: {query:"login auth flow", path:"src", max_results:5}
|
|
179
183
|
- Load a deferred tool when needed: {query:"glob"} or {query:"all"}
|
|
180
|
-
- Read a file: {path:"src/app.ts"} or {
|
|
184
|
+
- Read a file: {path:"src/app.ts"} or {path:"src/app.ts", start_line:20, end_line:60}
|
|
181
185
|
- Read a specific range inline: {path:"src/app.ts:20-60"}
|
|
182
186
|
- Search text: {pattern:"loginUser", path:"src"} or {query:"loginUser", directory:"src"}
|
|
183
187
|
- List a directory first: {path:"src"}
|
|
184
188
|
- After loading glob, find files by pattern: {pattern:"src/**/*.ts"} or {query:"src/**/*.ts"}
|
|
185
|
-
- Edit exact text: {
|
|
189
|
+
- Edit exact text: {path:"src/app.ts", old_text:"foo", new_text:"bar"}
|
|
186
190
|
- Edit with shorthand: {path:"src/app.ts", old_text:"foo", content:"bar"}
|
|
187
|
-
- Write a new file: {
|
|
188
|
-
- When the environment provides a Working directory, prefer absolute
|
|
191
|
+
- Write a new file: {path:"notes.txt", content:"..."} or {path:"src/page.tsx", content:"..."}
|
|
192
|
+
- When the environment provides a Working directory, prefer absolute path values rooted there instead of guessing prefixes
|
|
189
193
|
- If the user gives a relative path like src/app.ts, resolve it from the current Working directory rather than inventing ../ or sibling folders
|
|
190
194
|
|
|
191
195
|
# Doing tasks
|
|
@@ -214,7 +218,7 @@ Common tool call patterns:
|
|
|
214
218
|
- Keep answers compact and easy to scan
|
|
215
219
|
- Lead with the answer or next action, not scene-setting
|
|
216
220
|
- Do not restate the user's request unless a brief restatement prevents ambiguity
|
|
217
|
-
- When referencing code, use
|
|
221
|
+
- When referencing code, use path:line_number format
|
|
218
222
|
- Keep technical wording, commands, paths, and error details exact
|
|
219
223
|
- Only use emojis if the user explicitly requests it`;
|
|
220
224
|
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
export function parseInlineRangePath(value) {
|
|
4
|
+
const text = String(value || '').trim();
|
|
5
|
+
if (!text) return null;
|
|
6
|
+
const match = text.match(/^(.*?):(\d+)(?:-(\d+))?$/);
|
|
7
|
+
if (!match) return null;
|
|
8
|
+
const [, maybePath, startRaw, endRaw] = match;
|
|
9
|
+
if (!maybePath || /^(?:[A-Za-z])$/.test(maybePath)) return null;
|
|
10
|
+
const startLine = Number(startRaw);
|
|
11
|
+
const endLine = Number(endRaw || startRaw);
|
|
12
|
+
if (!Number.isFinite(startLine) || startLine <= 0) return null;
|
|
13
|
+
if (!Number.isFinite(endLine) || endLine < startLine) return null;
|
|
14
|
+
return {
|
|
15
|
+
path: maybePath,
|
|
16
|
+
start_line: startLine,
|
|
17
|
+
end_line: endLine
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function normalizeReadArgs(rawArgs) {
|
|
22
|
+
const source =
|
|
23
|
+
rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
|
|
24
|
+
? { ...rawArgs }
|
|
25
|
+
: { path: typeof rawArgs === 'string' ? rawArgs : '' };
|
|
26
|
+
|
|
27
|
+
const normalized = { ...source };
|
|
28
|
+
const aliasPath = String(source.path || source.file_path || source.file || source.target || '').trim();
|
|
29
|
+
if (aliasPath) normalized.path = aliasPath;
|
|
30
|
+
|
|
31
|
+
if (!Number.isFinite(Number(normalized.start_line)) && Number.isFinite(Number(source.offset))) {
|
|
32
|
+
normalized.start_line = Number(source.offset);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!Number.isFinite(Number(normalized.end_line)) && Number.isFinite(Number(source.limit))) {
|
|
36
|
+
const startLine = Number(normalized.start_line);
|
|
37
|
+
const limit = Number(source.limit);
|
|
38
|
+
if (startLine > 0 && limit > 0) {
|
|
39
|
+
normalized.end_line = startLine + limit - 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const inlineRange = parseInlineRangePath(normalized.path);
|
|
44
|
+
if (inlineRange) {
|
|
45
|
+
normalized.path = inlineRange.path;
|
|
46
|
+
if (!Number.isFinite(Number(normalized.start_line))) normalized.start_line = inlineRange.start_line;
|
|
47
|
+
if (!Number.isFinite(Number(normalized.end_line))) normalized.end_line = inlineRange.end_line;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return normalized;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function normalizePathArgs(rawArgs, aliases = []) {
|
|
54
|
+
const source =
|
|
55
|
+
rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
|
|
56
|
+
? { ...rawArgs }
|
|
57
|
+
: { path: typeof rawArgs === 'string' ? rawArgs : '' };
|
|
58
|
+
const normalized = { ...source };
|
|
59
|
+
const keys = ['path', ...aliases];
|
|
60
|
+
for (const key of keys) {
|
|
61
|
+
const value = String(source?.[key] || '').trim();
|
|
62
|
+
if (value) {
|
|
63
|
+
normalized.path = value;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return normalized;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function normalizePatternArgs(rawArgs, aliases = [], defaultPathAliases = []) {
|
|
71
|
+
const source =
|
|
72
|
+
rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
|
|
73
|
+
? { ...rawArgs }
|
|
74
|
+
: { pattern: typeof rawArgs === 'string' ? rawArgs : '' };
|
|
75
|
+
const normalized = { ...source };
|
|
76
|
+
for (const key of ['pattern', ...aliases]) {
|
|
77
|
+
const value = String(source?.[key] || '').trim();
|
|
78
|
+
if (value) {
|
|
79
|
+
normalized.pattern = value;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const key of ['path', ...defaultPathAliases]) {
|
|
84
|
+
const value = String(source?.[key] || '').trim();
|
|
85
|
+
if (value) {
|
|
86
|
+
normalized.path = value;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return normalized;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function normalizeWriteArgs(rawArgs) {
|
|
94
|
+
const source =
|
|
95
|
+
rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
|
|
96
|
+
? { ...rawArgs }
|
|
97
|
+
: { path: typeof rawArgs === 'string' ? rawArgs : '' };
|
|
98
|
+
const normalized = { ...source };
|
|
99
|
+
const filePath = String(source.path || source.file_path || source.file || '').trim();
|
|
100
|
+
if (filePath) normalized.path = filePath;
|
|
101
|
+
if (normalized.content == null) {
|
|
102
|
+
if (source.text != null) normalized.content = source.text;
|
|
103
|
+
if (source.new_content != null) normalized.content = source.new_content;
|
|
104
|
+
}
|
|
105
|
+
return normalized;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function normalizeWebFetchArgs(rawArgs) {
|
|
109
|
+
const normalized = normalizePathArgs(rawArgs, ['url', 'href', 'link', 'target']);
|
|
110
|
+
const url = String(normalized.url || normalized.path || '').trim();
|
|
111
|
+
return { ...normalized, url };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function normalizeWebSearchArgs(rawArgs) {
|
|
115
|
+
const normalized = normalizePatternArgs(rawArgs, ['query', 'q', 'keyword']);
|
|
116
|
+
const query = String(normalized.query || normalized.pattern || '').trim();
|
|
117
|
+
return { ...normalized, query };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function buildDeleteApprovalDetails(source, rawPath) {
|
|
121
|
+
const existing =
|
|
122
|
+
source?.approval && typeof source.approval === 'object' && !Array.isArray(source.approval)
|
|
123
|
+
? source.approval
|
|
124
|
+
: {};
|
|
125
|
+
const approvalPath = String(existing.path || rawPath || '').trim();
|
|
126
|
+
const approvalName = String(existing.name || (approvalPath ? path.basename(approvalPath) : '') || '').trim();
|
|
127
|
+
const approvalType = String(existing.type || '').trim();
|
|
128
|
+
|
|
129
|
+
const approval = {};
|
|
130
|
+
if (approvalPath) approval.path = approvalPath;
|
|
131
|
+
if (approvalName) approval.name = approvalName;
|
|
132
|
+
if (approvalType) approval.type = approvalType;
|
|
133
|
+
return Object.keys(approval).length > 0 ? approval : undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function normalizeToolArguments(toolName, args, rawArguments) {
|
|
137
|
+
const rawText = typeof rawArguments === 'string' ? rawArguments.trim() : '';
|
|
138
|
+
const primitive =
|
|
139
|
+
args == null || Array.isArray(args) || typeof args !== 'object'
|
|
140
|
+
? args
|
|
141
|
+
: null;
|
|
142
|
+
const source =
|
|
143
|
+
args && typeof args === 'object' && !Array.isArray(args)
|
|
144
|
+
? { ...args }
|
|
145
|
+
: {};
|
|
146
|
+
|
|
147
|
+
if (primitive != null && typeof primitive !== 'object') {
|
|
148
|
+
source._raw = rawText || String(primitive);
|
|
149
|
+
} else if (!source._raw && rawText && source._invalid_json) {
|
|
150
|
+
source._raw = rawText;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const stringValue =
|
|
154
|
+
typeof primitive === 'string'
|
|
155
|
+
? primitive.trim()
|
|
156
|
+
: String(source._raw || '').trim();
|
|
157
|
+
|
|
158
|
+
if (toolName === 'read') return normalizeReadArgs({ ...source, ...(stringValue && !source.path ? { path: stringValue } : {}) });
|
|
159
|
+
if (toolName === 'list') return normalizePathArgs({ ...source, ...(stringValue && !source.path ? { path: stringValue } : {}) }, ['dir', 'directory']);
|
|
160
|
+
if (toolName === 'glob') return normalizePatternArgs({ ...source, ...(stringValue && !source.pattern ? { pattern: stringValue } : {}) }, ['glob', 'query'], ['directory']);
|
|
161
|
+
if (toolName === 'grep') return normalizePatternArgs({ ...source, ...(stringValue && !source.pattern ? { pattern: stringValue } : {}) }, ['query', 'symbol', 'q'], ['directory', 'dir', 'cwd']);
|
|
162
|
+
if (toolName === 'write') return normalizeWriteArgs({ ...source, ...(stringValue && !source.path ? { path: stringValue } : {}) });
|
|
163
|
+
|
|
164
|
+
if (toolName === 'edit') {
|
|
165
|
+
const value = String(source.path || source.file || source.file_path || '').trim();
|
|
166
|
+
if (value && !source.path) source.path = value;
|
|
167
|
+
return source;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (toolName === 'delete') {
|
|
171
|
+
const normalized = normalizePathArgs(
|
|
172
|
+
{ ...source, ...(stringValue && !source.path ? { path: stringValue } : {}) },
|
|
173
|
+
['file_path', 'file', 'target', 'directory', 'dir']
|
|
174
|
+
);
|
|
175
|
+
const approval = buildDeleteApprovalDetails(normalized, normalized.path);
|
|
176
|
+
if (approval) normalized.approval = approval;
|
|
177
|
+
return normalized;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return source;
|
|
181
|
+
}
|