@qxbyte/muse 0.1.3 → 0.2.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/README.md +101 -37
- package/dist/cli.js +6610 -1283
- package/dist/cli.js.map +1 -1
- package/dist/index.js +2232 -280
- package/dist/index.js.map +1 -1
- package/package.json +11 -3
package/dist/index.js
CHANGED
|
@@ -20,6 +20,15 @@ var Logger = class {
|
|
|
20
20
|
level = "info";
|
|
21
21
|
logPath;
|
|
22
22
|
fileEnabled = true;
|
|
23
|
+
/**
|
|
24
|
+
* 是否把 warn/error 同时打到 stderr。**默认 false**,因为 Ink TUI 会捕获 stderr
|
|
25
|
+
* 渲染区,造成 "[error] xxx" 拼接到 PermissionModeBar / footer 等正常 UI 后面的
|
|
26
|
+
* 视觉污染(B-19 修复)。需要 stderr 输出的场景(cli.tsx die / runOneShot)
|
|
27
|
+
* 都已经在用 process.stderr.write 显式打,不依赖 logger。
|
|
28
|
+
*
|
|
29
|
+
* 单元测试 / 后台脚本可显式 setStderrEnabled(true) 开启。
|
|
30
|
+
*/
|
|
31
|
+
stderrEnabled = false;
|
|
23
32
|
constructor() {
|
|
24
33
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
25
34
|
this.logPath = join(homedir(), ".muse", "logs", `${date}.jsonl`);
|
|
@@ -32,6 +41,9 @@ var Logger = class {
|
|
|
32
41
|
setLevel(level) {
|
|
33
42
|
this.level = level;
|
|
34
43
|
}
|
|
44
|
+
setStderrEnabled(enabled) {
|
|
45
|
+
this.stderrEnabled = enabled;
|
|
46
|
+
}
|
|
35
47
|
write(level, msg, extra) {
|
|
36
48
|
if (LEVELS[level] < LEVELS[this.level]) return;
|
|
37
49
|
const entry = {
|
|
@@ -46,7 +58,7 @@ var Logger = class {
|
|
|
46
58
|
} catch {
|
|
47
59
|
}
|
|
48
60
|
}
|
|
49
|
-
if (level === "warn" || level === "error") {
|
|
61
|
+
if (this.stderrEnabled && (level === "warn" || level === "error")) {
|
|
50
62
|
const prefix = level === "error" ? "[error]" : "[warn]";
|
|
51
63
|
process.stderr.write(`${prefix} ${msg}
|
|
52
64
|
`);
|
|
@@ -81,7 +93,11 @@ var DEFAULT_CAPABILITIES = {
|
|
|
81
93
|
parallelToolCalls: true,
|
|
82
94
|
vision: false,
|
|
83
95
|
jsonMode: true,
|
|
84
|
-
|
|
96
|
+
// 没在 models.local.json 显式声明 contextWindow 时的兜底值。
|
|
97
|
+
// 200k 是 2026 年主流 LLM(GPT-4.1 / Claude / DeepSeek-v3 / Qwen-Plus / GLM-4 等)的
|
|
98
|
+
// 常见容量,比 32k 兜底更接近真实,避免 ctx% 大幅虚高 / auto-compact 过早触发。
|
|
99
|
+
// 仍建议在每条 entry 显式写 contextWindow,避免依赖默认值。
|
|
100
|
+
maxContextWindow: 2e5
|
|
85
101
|
};
|
|
86
102
|
var OpenAICompatibleClient = class {
|
|
87
103
|
providerName;
|
|
@@ -201,8 +217,7 @@ function convertMessages(messages, systemPrompt) {
|
|
|
201
217
|
if (typeof msg.content === "string") {
|
|
202
218
|
result.push({ role: "user", content: msg.content });
|
|
203
219
|
} else {
|
|
204
|
-
|
|
205
|
-
result.push({ role: "user", content: text });
|
|
220
|
+
result.push({ role: "user", content: convertUserParts(msg.content) });
|
|
206
221
|
}
|
|
207
222
|
break;
|
|
208
223
|
case "assistant":
|
|
@@ -226,6 +241,33 @@ function convertMessages(messages, systemPrompt) {
|
|
|
226
241
|
}
|
|
227
242
|
return result;
|
|
228
243
|
}
|
|
244
|
+
function convertUserParts(parts) {
|
|
245
|
+
const out = [];
|
|
246
|
+
for (const part of parts) {
|
|
247
|
+
if (part.type === "text") {
|
|
248
|
+
out.push({ type: "text", text: part.text });
|
|
249
|
+
} else if (part.type === "image") {
|
|
250
|
+
out.push({
|
|
251
|
+
type: "image",
|
|
252
|
+
// SDK 接受 base64 字符串作为 DataContent
|
|
253
|
+
image: part.data,
|
|
254
|
+
mimeType: part.mediaType
|
|
255
|
+
});
|
|
256
|
+
} else if (part.type === "file") {
|
|
257
|
+
out.push({
|
|
258
|
+
type: "text",
|
|
259
|
+
text: `<file path="${part.path}"${part.mimeType ? ` mimeType="${part.mimeType}"` : ""}>
|
|
260
|
+
${part.text}
|
|
261
|
+
</file>`
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (out.length === 0) return "";
|
|
266
|
+
if (out.every((p) => p.type === "text")) {
|
|
267
|
+
return out.map((p) => p.text).join("\n\n");
|
|
268
|
+
}
|
|
269
|
+
return out;
|
|
270
|
+
}
|
|
229
271
|
function convertAssistantContent(msg) {
|
|
230
272
|
const parts = [];
|
|
231
273
|
for (const part of msg.content) {
|
|
@@ -266,11 +308,11 @@ function isRetryable(err) {
|
|
|
266
308
|
return false;
|
|
267
309
|
}
|
|
268
310
|
async function sleep(ms, abortSignal) {
|
|
269
|
-
await new Promise((
|
|
311
|
+
await new Promise((resolve8, reject) => {
|
|
270
312
|
if (abortSignal?.aborted) return reject(new Error("aborted"));
|
|
271
313
|
const t = setTimeout(() => {
|
|
272
314
|
abortSignal?.removeEventListener("abort", onAbort);
|
|
273
|
-
|
|
315
|
+
resolve8();
|
|
274
316
|
}, ms);
|
|
275
317
|
const onAbort = () => {
|
|
276
318
|
clearTimeout(t);
|
|
@@ -958,11 +1000,14 @@ var TodoSchema = z7.object({
|
|
|
958
1000
|
activeForm: z7.string().optional().describe("Present-continuous form for the spinner (e.g. 'Running the test suite').")
|
|
959
1001
|
});
|
|
960
1002
|
var TodoWriteArgs = z7.object({
|
|
961
|
-
todos: z7.array(TodoSchema).describe("Full list. Replaces the current store.")
|
|
1003
|
+
todos: z7.array(TodoSchema).describe("Full list. Replaces the current store."),
|
|
1004
|
+
listTitle: z7.string().optional().describe(
|
|
1005
|
+
"Short (2-6 word) imperative title describing the OVERALL goal of this todo list, e.g. 'Refactor auth flow' / '\u5206\u6790 Muse \u67B6\u6784' / 'Migrate to ESM'. Shown as the list header in UI. Keep it stable across calls within the same task; only change it if the goal genuinely shifts."
|
|
1006
|
+
)
|
|
962
1007
|
});
|
|
963
1008
|
var TodoWriteTool = defineTool({
|
|
964
1009
|
name: "TodoWrite",
|
|
965
|
-
description: "Maintain a structured task list for the current session. Pass the FULL list every call (it replaces the store). Mark exactly one task in_progress at a time; mark completed immediately when done; do not batch completions. Use when the task has 3+ distinct steps or is non-trivial. Skip for single trivial actions.",
|
|
1010
|
+
description: "Maintain a structured task list for the current session. Pass the FULL list every call (it replaces the store). Mark exactly one task in_progress at a time; mark completed immediately when done; do not batch completions. Use when the task has 3+ distinct steps or is non-trivial. Skip for single trivial actions. Always include `listTitle` summarizing the overall goal so the UI can label this batch meaningfully.",
|
|
966
1011
|
parameters: TodoWriteArgs,
|
|
967
1012
|
permission: "read",
|
|
968
1013
|
summarize: (args) => `TodoWrite(${args.todos.length} items)`,
|
|
@@ -1144,110 +1189,299 @@ function stripTags(s) {
|
|
|
1144
1189
|
import { z as z9 } from "zod";
|
|
1145
1190
|
|
|
1146
1191
|
// src/loop/memory.ts
|
|
1147
|
-
import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
1148
|
-
import { existsSync } from "fs";
|
|
1192
|
+
import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile3, unlink, readdir } from "fs/promises";
|
|
1193
|
+
import { existsSync, statSync } from "fs";
|
|
1149
1194
|
import { homedir as homedir3 } from "os";
|
|
1150
1195
|
import { join as join2 } from "path";
|
|
1151
1196
|
import { createHash } from "crypto";
|
|
1197
|
+
function trustRank(t) {
|
|
1198
|
+
switch (t) {
|
|
1199
|
+
case "trusted":
|
|
1200
|
+
return 2;
|
|
1201
|
+
case "verified":
|
|
1202
|
+
return 1;
|
|
1203
|
+
case "auto":
|
|
1204
|
+
return 0;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1152
1207
|
function projectHash(cwd) {
|
|
1153
1208
|
return createHash("sha256").update(cwd).digest("hex").slice(0, 16);
|
|
1154
1209
|
}
|
|
1155
1210
|
function memoryDir(cwd) {
|
|
1156
1211
|
return join2(homedir3(), ".muse", "projects", projectHash(cwd), "memory");
|
|
1157
1212
|
}
|
|
1158
|
-
function
|
|
1159
|
-
return join2(
|
|
1213
|
+
function globalMemoryDir() {
|
|
1214
|
+
return join2(homedir3(), ".muse", "memory");
|
|
1160
1215
|
}
|
|
1161
|
-
function
|
|
1162
|
-
return
|
|
1216
|
+
function scopeDir(cwd, scope) {
|
|
1217
|
+
return scope === "user" ? globalMemoryDir() : memoryDir(cwd);
|
|
1163
1218
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1219
|
+
function memoryIndexPath(cwd, scope = "project") {
|
|
1220
|
+
return join2(scopeDir(cwd, scope), "MEMORY.md");
|
|
1221
|
+
}
|
|
1222
|
+
function memoryFilePath(cwd, name, scope = "project") {
|
|
1223
|
+
return join2(scopeDir(cwd, scope), `${name}.md`);
|
|
1224
|
+
}
|
|
1225
|
+
async function readMemory(cwd, name, scope) {
|
|
1226
|
+
const candidates = scope ? [scope] : ["project", "user"];
|
|
1227
|
+
for (const s of candidates) {
|
|
1228
|
+
const filePath = memoryFilePath(cwd, name, s);
|
|
1229
|
+
if (!existsSync(filePath)) continue;
|
|
1230
|
+
const raw = await readFile4(filePath, "utf-8");
|
|
1231
|
+
const { frontmatter, body } = parseMemoryFile(raw, name, filePath);
|
|
1232
|
+
return { frontmatter, body, filePath, scope: s };
|
|
1233
|
+
}
|
|
1234
|
+
const where = scope ? `${scope} scope` : `project or user scope`;
|
|
1235
|
+
throw new Error(`Memory "${name}" does not exist in ${where}.`);
|
|
1236
|
+
}
|
|
1237
|
+
async function readMemoryFile(cwd, name, scope) {
|
|
1238
|
+
const file = await readMemory(cwd, name, scope);
|
|
1239
|
+
return readFile4(file.filePath, "utf-8");
|
|
1170
1240
|
}
|
|
1171
1241
|
async function writeMemory(cwd, opts) {
|
|
1172
|
-
const
|
|
1242
|
+
const scope = opts.scope ?? "project";
|
|
1243
|
+
const dir = scopeDir(cwd, scope);
|
|
1173
1244
|
await mkdir2(dir, { recursive: true });
|
|
1174
|
-
const filePath = memoryFilePath(cwd, opts.name);
|
|
1175
|
-
const
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1245
|
+
const filePath = memoryFilePath(cwd, opts.name, scope);
|
|
1246
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1247
|
+
const reqTrust = opts.trust ?? "auto";
|
|
1248
|
+
const reqSource = opts.source ?? "manual-write";
|
|
1249
|
+
let createdAt = now;
|
|
1250
|
+
let finalTrust = reqTrust;
|
|
1251
|
+
let finalSource = reqSource;
|
|
1252
|
+
const isCreating = !existsSync(filePath);
|
|
1253
|
+
if (!isCreating) {
|
|
1254
|
+
try {
|
|
1255
|
+
const raw = await readFile4(filePath, "utf-8");
|
|
1256
|
+
const existing = parseMemoryFile(raw, opts.name, filePath).frontmatter;
|
|
1257
|
+
createdAt = existing.created_at;
|
|
1258
|
+
if (trustRank(existing.trust) > trustRank(reqTrust)) {
|
|
1259
|
+
finalTrust = existing.trust;
|
|
1260
|
+
finalSource = existing.source;
|
|
1261
|
+
}
|
|
1262
|
+
} catch {
|
|
1263
|
+
createdAt = now;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
const fm = {
|
|
1267
|
+
name: opts.name,
|
|
1268
|
+
description: opts.description.replace(/\n/g, " ").trim(),
|
|
1269
|
+
type: opts.type,
|
|
1270
|
+
trust: finalTrust,
|
|
1271
|
+
source: finalSource,
|
|
1272
|
+
created_at: createdAt,
|
|
1273
|
+
updated_at: now
|
|
1274
|
+
};
|
|
1275
|
+
const content = serializeMemoryFile(fm, opts.body);
|
|
1187
1276
|
await writeFile3(filePath, content, "utf-8");
|
|
1188
|
-
const
|
|
1189
|
-
|
|
1190
|
-
|
|
1277
|
+
const indexUpdated = await upsertIndexLine(cwd, scope, fm);
|
|
1278
|
+
return { filePath, indexUpdated, created: isCreating, scope };
|
|
1279
|
+
}
|
|
1280
|
+
var FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?/;
|
|
1281
|
+
function parseMemoryFile(raw, fallbackName, filePath) {
|
|
1282
|
+
const m = raw.match(FRONTMATTER_RE);
|
|
1283
|
+
if (!m) {
|
|
1284
|
+
return {
|
|
1285
|
+
frontmatter: lazyDefaults(fallbackName, "", filePath),
|
|
1286
|
+
body: raw.trim()
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
const fmText = m[1];
|
|
1290
|
+
const body = raw.slice(m[0].length).trim();
|
|
1291
|
+
const lines = fmText.split("\n");
|
|
1292
|
+
let name = fallbackName;
|
|
1293
|
+
let description = "";
|
|
1294
|
+
let type = "user";
|
|
1295
|
+
let trust = "auto";
|
|
1296
|
+
let source = "manual-write";
|
|
1297
|
+
let createdAt = "";
|
|
1298
|
+
let updatedAt = "";
|
|
1299
|
+
let inMetadata = false;
|
|
1300
|
+
for (const line of lines) {
|
|
1301
|
+
if (line.match(/^metadata:\s*$/)) {
|
|
1302
|
+
inMetadata = true;
|
|
1303
|
+
continue;
|
|
1304
|
+
}
|
|
1305
|
+
if (!inMetadata) {
|
|
1306
|
+
const kv = parseKV(line);
|
|
1307
|
+
if (!kv) continue;
|
|
1308
|
+
if (kv.key === "name") name = kv.value;
|
|
1309
|
+
else if (kv.key === "description") description = kv.value;
|
|
1310
|
+
} else {
|
|
1311
|
+
if (!line.match(/^\s+\S/)) {
|
|
1312
|
+
inMetadata = false;
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
const kv = parseKV(line);
|
|
1316
|
+
if (!kv) continue;
|
|
1317
|
+
switch (kv.key) {
|
|
1318
|
+
case "type":
|
|
1319
|
+
if (isMemoryType(kv.value)) type = kv.value;
|
|
1320
|
+
break;
|
|
1321
|
+
case "trust":
|
|
1322
|
+
if (isTrustLevel(kv.value)) trust = kv.value;
|
|
1323
|
+
break;
|
|
1324
|
+
case "source":
|
|
1325
|
+
source = kv.value;
|
|
1326
|
+
break;
|
|
1327
|
+
case "created_at":
|
|
1328
|
+
createdAt = kv.value;
|
|
1329
|
+
break;
|
|
1330
|
+
case "updated_at":
|
|
1331
|
+
updatedAt = kv.value;
|
|
1332
|
+
break;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
if (!createdAt || !updatedAt) {
|
|
1337
|
+
const mtimeIso = safeFileMtime(filePath);
|
|
1338
|
+
if (!createdAt) createdAt = mtimeIso;
|
|
1339
|
+
if (!updatedAt) updatedAt = createdAt;
|
|
1340
|
+
}
|
|
1341
|
+
return {
|
|
1342
|
+
frontmatter: { name, description, type, trust, source, created_at: createdAt, updated_at: updatedAt },
|
|
1343
|
+
body
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
function parseKV(line) {
|
|
1347
|
+
const m = line.match(/^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*?)\s*$/);
|
|
1348
|
+
if (!m) return null;
|
|
1349
|
+
const key = m[1];
|
|
1350
|
+
let value = m[2];
|
|
1351
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1352
|
+
value = value.slice(1, -1);
|
|
1353
|
+
}
|
|
1354
|
+
return { key, value };
|
|
1355
|
+
}
|
|
1356
|
+
function isMemoryType(v) {
|
|
1357
|
+
return v === "user" || v === "feedback" || v === "project" || v === "reference";
|
|
1358
|
+
}
|
|
1359
|
+
function isTrustLevel(v) {
|
|
1360
|
+
return v === "trusted" || v === "verified" || v === "auto";
|
|
1361
|
+
}
|
|
1362
|
+
function safeFileMtime(filePath) {
|
|
1363
|
+
try {
|
|
1364
|
+
return statSync(filePath).mtime.toISOString();
|
|
1365
|
+
} catch {
|
|
1366
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
function lazyDefaults(name, description, filePath) {
|
|
1370
|
+
const mtimeIso = safeFileMtime(filePath);
|
|
1371
|
+
return {
|
|
1372
|
+
name,
|
|
1373
|
+
description,
|
|
1374
|
+
type: "user",
|
|
1375
|
+
trust: "auto",
|
|
1376
|
+
source: "manual-write",
|
|
1377
|
+
created_at: mtimeIso,
|
|
1378
|
+
updated_at: mtimeIso
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
function serializeMemoryFile(fm, body) {
|
|
1382
|
+
const lines = [
|
|
1383
|
+
"---",
|
|
1384
|
+
`name: ${fm.name}`,
|
|
1385
|
+
`description: ${escapeYamlValue(fm.description)}`,
|
|
1386
|
+
"metadata:",
|
|
1387
|
+
` type: ${fm.type}`,
|
|
1388
|
+
` trust: ${fm.trust}`,
|
|
1389
|
+
` source: ${fm.source}`,
|
|
1390
|
+
` created_at: ${fm.created_at}`,
|
|
1391
|
+
` updated_at: ${fm.updated_at}`,
|
|
1392
|
+
"---",
|
|
1393
|
+
"",
|
|
1394
|
+
body.trim(),
|
|
1395
|
+
""
|
|
1396
|
+
];
|
|
1397
|
+
return lines.join("\n");
|
|
1398
|
+
}
|
|
1399
|
+
function escapeYamlValue(v) {
|
|
1400
|
+
if (/[:#&*?{}[\]|>!%@`]/.test(v) || v.startsWith(" ") || v.endsWith(" ")) {
|
|
1401
|
+
return `"${v.replace(/"/g, '\\"')}"`;
|
|
1402
|
+
}
|
|
1403
|
+
return v;
|
|
1404
|
+
}
|
|
1405
|
+
var INDEX_LINE_RE = /^\[(trusted|verified|auto)\]\s+-\s+\[([a-zA-Z0-9-_]+)\]\(([^)]+)\)\s+—\s+(.*)$/;
|
|
1406
|
+
function formatIndexLine(fm) {
|
|
1407
|
+
return `[${fm.trust}] - [${fm.name}](${fm.name}.md) \u2014 ${fm.description}`;
|
|
1408
|
+
}
|
|
1409
|
+
async function upsertIndexLine(cwd, scope, fm) {
|
|
1410
|
+
const indexPath2 = memoryIndexPath(cwd, scope);
|
|
1411
|
+
let index = existsSync(indexPath2) ? await readFile4(indexPath2, "utf-8") : "";
|
|
1191
1412
|
const lines = index ? index.split("\n") : [];
|
|
1192
|
-
const
|
|
1193
|
-
const
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1413
|
+
const newLine = formatIndexLine(fm);
|
|
1414
|
+
const existingIdx = lines.findIndex((l) => {
|
|
1415
|
+
const m = l.match(INDEX_LINE_RE);
|
|
1416
|
+
if (m) return m[2] === fm.name;
|
|
1417
|
+
return l.startsWith(`- [${fm.name}](${fm.name}.md)`);
|
|
1418
|
+
});
|
|
1419
|
+
let changed = false;
|
|
1420
|
+
if (existingIdx >= 0) {
|
|
1421
|
+
if (lines[existingIdx] !== newLine) {
|
|
1422
|
+
lines[existingIdx] = newLine;
|
|
1423
|
+
changed = true;
|
|
1200
1424
|
}
|
|
1201
1425
|
} else {
|
|
1202
1426
|
lines.push(newLine);
|
|
1203
|
-
|
|
1427
|
+
changed = true;
|
|
1204
1428
|
}
|
|
1205
|
-
if (
|
|
1429
|
+
if (changed) {
|
|
1206
1430
|
const out = lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
|
|
1207
|
-
await
|
|
1431
|
+
await mkdir2(scopeDir(cwd, scope), { recursive: true });
|
|
1432
|
+
await writeFile3(indexPath2, out, "utf-8");
|
|
1208
1433
|
}
|
|
1209
|
-
return
|
|
1434
|
+
return changed;
|
|
1210
1435
|
}
|
|
1211
1436
|
|
|
1212
1437
|
// src/tools/builtin/memory.ts
|
|
1213
1438
|
var TYPES = ["user", "feedback", "project", "reference"];
|
|
1439
|
+
var SCOPE_VALUES = ["project", "user"];
|
|
1214
1440
|
var MemoryWriteArgs = z9.object({
|
|
1215
1441
|
name: z9.string().regex(/^[a-z0-9][a-z0-9-_]*$/i, "must be a kebab- or snake-style slug").describe("Short kebab/snake slug; used as filename (<name>.md) and index key."),
|
|
1216
1442
|
description: z9.string().describe("One-line summary used in MEMORY.md index (decides future relevance)."),
|
|
1217
1443
|
type: z9.enum(TYPES).describe("user | feedback | project | reference"),
|
|
1218
|
-
body: z9.string().describe("Memory content (markdown). For feedback/project, lead with the rule/fact then **Why:** and **How to apply:** lines.")
|
|
1444
|
+
body: z9.string().describe("Memory content (markdown). For feedback/project, lead with the rule/fact then **Why:** and **How to apply:** lines."),
|
|
1445
|
+
scope: z9.enum(SCOPE_VALUES).optional().describe(
|
|
1446
|
+
'Storage scope. "project"(default \u2014 safe bias) = saved under current project, not visible to other projects. "user"(global) = saved under ~/.muse/memory/, visible across ALL projects. Choose carefully:\n scope=project \u2014 project architecture / file conventions / team agreements (pnpm vs npm) / project-specific bugs+fixes / API contracts.\n When unsure, choose project (safer; user can /memory promote-scope later).\n scope=user \u2014 user role / working language / timezone / cross-project editor preferences (tabs/spaces, naming style, comment language) / tool preferences that apply EVERYWHERE / recurring feedback that reveals user personality.\n HARD RULE: personal preference items (tabs/spaces, comment language, editor) MUST be scope=user \u2014 do NOT save them as project memory just because cwd is in a project. If you cannot tell whether a fact is personal or project-specific, ask the user or default to project.'
|
|
1447
|
+
)
|
|
1219
1448
|
});
|
|
1220
1449
|
var MemoryWriteTool = defineTool({
|
|
1221
1450
|
name: "MemoryWrite",
|
|
1222
|
-
description: "Save a long-term memory file under ~/.muse/projects/<hash>/memory/<name>.md and update MEMORY.md index. Use for: user role/preferences, validated approach decisions (feedback), project facts (auto-convert relative dates), external system references. Do NOT save: code patterns derivable from the repo, git history, fix recipes, ephemeral task state.",
|
|
1451
|
+
description: "Save a long-term memory file under ~/.muse/[projects/<hash>/]memory/<name>.md and update MEMORY.md index. Choose scope carefully (see scope arg). Use for: user role/preferences (scope=user), validated approach decisions (feedback), project facts (auto-convert relative dates), external system references. Do NOT save: code patterns derivable from the repo, git history, fix recipes, ephemeral task state.",
|
|
1223
1452
|
parameters: MemoryWriteArgs,
|
|
1224
1453
|
permission: "write",
|
|
1225
|
-
summarize: (args) => `MemoryWrite(${args.name}, type=${args.type})`,
|
|
1454
|
+
summarize: (args) => `MemoryWrite(${args.name}, ${args.scope ?? "project"}, type=${args.type})`,
|
|
1226
1455
|
async execute(args, ctx) {
|
|
1227
|
-
const
|
|
1456
|
+
const result = await writeMemory(ctx.cwd, {
|
|
1228
1457
|
name: args.name,
|
|
1229
1458
|
description: args.description,
|
|
1230
1459
|
type: args.type,
|
|
1231
|
-
body: args.body
|
|
1460
|
+
body: args.body,
|
|
1461
|
+
trust: "auto",
|
|
1462
|
+
source: "manual-write",
|
|
1463
|
+
scope: args.scope
|
|
1464
|
+
// defaults to "project" inside writeMemory
|
|
1232
1465
|
});
|
|
1233
1466
|
return {
|
|
1234
|
-
content: `Saved memory "${args.name}" (${args.type}) \u2192 ${filePath}${indexUpdated ? "\nMEMORY.md updated." : ""}`,
|
|
1235
|
-
summary: `MemoryWrite ${args.name}`
|
|
1467
|
+
content: `Saved memory "${args.name}" (${args.type}, scope=${result.scope}, trust=auto) \u2192 ${result.filePath}${result.indexUpdated ? "\nMEMORY.md updated." : ""}`,
|
|
1468
|
+
summary: `MemoryWrite ${args.name} (${result.scope})`
|
|
1236
1469
|
};
|
|
1237
1470
|
}
|
|
1238
1471
|
});
|
|
1239
1472
|
var MemoryReadArgs = z9.object({
|
|
1240
|
-
name: z9.string().describe("Memory slug to read (no .md extension).")
|
|
1473
|
+
name: z9.string().describe("Memory slug to read (no .md extension)."),
|
|
1474
|
+
scope: z9.enum(SCOPE_VALUES).optional().describe("Optional scope to read from. Default: project first, fallback user. Specify when name exists in both scopes.")
|
|
1241
1475
|
});
|
|
1242
1476
|
var MemoryReadTool = defineTool({
|
|
1243
1477
|
name: "MemoryRead",
|
|
1244
|
-
description: "Read a specific long-term memory file by name. Use after seeing it referenced in MEMORY.md (which is auto-injected into the system prompt).",
|
|
1478
|
+
description: "Read a specific long-term memory file by name. Default lookup: project scope first, then user scope. Use after seeing it referenced in MEMORY.md (which is auto-injected into the system prompt). Pass scope='project' or 'user' to disambiguate when the same name exists in both.",
|
|
1245
1479
|
parameters: MemoryReadArgs,
|
|
1246
1480
|
permission: "read",
|
|
1247
|
-
summarize: (args) => `MemoryRead(${args.name})`,
|
|
1481
|
+
summarize: (args) => `MemoryRead(${args.name}${args.scope ? `, ${args.scope}` : ""})`,
|
|
1248
1482
|
async execute(args, ctx) {
|
|
1249
1483
|
try {
|
|
1250
|
-
const content = await readMemoryFile(ctx.cwd, args.name);
|
|
1484
|
+
const content = await readMemoryFile(ctx.cwd, args.name, args.scope);
|
|
1251
1485
|
return { content, summary: `MemoryRead ${args.name}` };
|
|
1252
1486
|
} catch (err) {
|
|
1253
1487
|
return { content: err instanceof Error ? err.message : String(err), isError: true };
|
|
@@ -1419,7 +1653,7 @@ var PermissionGate = class {
|
|
|
1419
1653
|
};
|
|
1420
1654
|
|
|
1421
1655
|
// src/session/jsonl.ts
|
|
1422
|
-
import { appendFile, mkdir as mkdir3, readdir, readFile as readFile5, stat as stat3 } from "fs/promises";
|
|
1656
|
+
import { appendFile, mkdir as mkdir3, readdir as readdir2, readFile as readFile5, stat as stat3 } from "fs/promises";
|
|
1423
1657
|
import { existsSync as existsSync2 } from "fs";
|
|
1424
1658
|
import { homedir as homedir4 } from "os";
|
|
1425
1659
|
import { dirname as dirname3, join as join3 } from "path";
|
|
@@ -1457,7 +1691,7 @@ var Session = class _Session {
|
|
|
1457
1691
|
static async resolve(cwd, idOrPrefix) {
|
|
1458
1692
|
const dir = sessionsDir(cwd);
|
|
1459
1693
|
if (!existsSync2(dir)) return void 0;
|
|
1460
|
-
const entries = await
|
|
1694
|
+
const entries = await readdir2(dir);
|
|
1461
1695
|
const matches = entries.filter((e) => e.endsWith(".jsonl") && e.startsWith(idOrPrefix));
|
|
1462
1696
|
if (matches.length === 0) return void 0;
|
|
1463
1697
|
if (matches.length > 1) {
|
|
@@ -1479,7 +1713,7 @@ var Session = class _Session {
|
|
|
1479
1713
|
static async listAll(cwd, limit) {
|
|
1480
1714
|
const dir = sessionsDir(cwd);
|
|
1481
1715
|
if (!existsSync2(dir)) return [];
|
|
1482
|
-
const entries = await
|
|
1716
|
+
const entries = await readdir2(dir);
|
|
1483
1717
|
const files = entries.filter((e) => e.endsWith(".jsonl"));
|
|
1484
1718
|
if (files.length === 0) return [];
|
|
1485
1719
|
const stats = await Promise.all(
|
|
@@ -1561,7 +1795,12 @@ async function readSummary(meta) {
|
|
|
1561
1795
|
let preview;
|
|
1562
1796
|
if (firstUser) {
|
|
1563
1797
|
const c = firstUser.message.content;
|
|
1564
|
-
const text = typeof c === "string" ? c : c.map((p) =>
|
|
1798
|
+
const text = typeof c === "string" ? c : c.map((p) => {
|
|
1799
|
+
if (p.type === "text") return p.text;
|
|
1800
|
+
if (p.type === "file") return `\u{1F4CE}`;
|
|
1801
|
+
if (p.type === "image") return `\u{1F5BC}`;
|
|
1802
|
+
return "";
|
|
1803
|
+
}).join(" ").trim();
|
|
1565
1804
|
preview = text.slice(0, 60).replace(/\s+/g, " ");
|
|
1566
1805
|
}
|
|
1567
1806
|
return { ...meta, preview, messageCount: messages.length };
|
|
@@ -1593,163 +1832,24 @@ Update via TodoWrite as you make progress. Keep exactly one item in_progress at
|
|
|
1593
1832
|
}
|
|
1594
1833
|
};
|
|
1595
1834
|
|
|
1596
|
-
// src/
|
|
1597
|
-
var
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
ctx;
|
|
1602
|
-
messages = [];
|
|
1603
|
-
todos = new TodoStore();
|
|
1604
|
-
getMessages() {
|
|
1605
|
-
return this.messages;
|
|
1606
|
-
}
|
|
1607
|
-
setMessages(msgs) {
|
|
1608
|
-
this.messages = msgs;
|
|
1835
|
+
// src/preprocess/types.ts
|
|
1836
|
+
var NOOP_LOGGER = {
|
|
1837
|
+
stage() {
|
|
1838
|
+
},
|
|
1839
|
+
warn() {
|
|
1609
1840
|
}
|
|
1610
|
-
|
|
1611
|
-
async runTurn(userInput) {
|
|
1612
|
-
const userMessage = { role: "user", content: userInput };
|
|
1613
|
-
this.messages.push(userMessage);
|
|
1614
|
-
await this.ctx.session.append({ type: "message", time: (/* @__PURE__ */ new Date()).toISOString(), message: userMessage });
|
|
1615
|
-
while (true) {
|
|
1616
|
-
const mode = this.ctx.permissions.getMode();
|
|
1617
|
-
const tools = this.ctx.tools.toLLMDefinitions(
|
|
1618
|
-
mode === "plan" ? (t) => t.permission === "read" : void 0
|
|
1619
|
-
);
|
|
1620
|
-
const todoSection = this.todos.toPromptSection();
|
|
1621
|
-
const systemPrompt = todoSection ? `${this.ctx.systemPrompt}
|
|
1841
|
+
};
|
|
1622
1842
|
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
const assistantParts = [];
|
|
1631
|
-
const toolCallsToRun = [];
|
|
1632
|
-
let lastError;
|
|
1633
|
-
for await (const ev of stream) {
|
|
1634
|
-
this.handleEvent(ev, assistantParts, toolCallsToRun, (e) => {
|
|
1635
|
-
lastError = e;
|
|
1636
|
-
});
|
|
1637
|
-
if (lastError) break;
|
|
1638
|
-
}
|
|
1639
|
-
if (lastError) {
|
|
1640
|
-
this.ctx.events?.onError?.(lastError);
|
|
1641
|
-
log.error("agent stream error", { msg: lastError.message });
|
|
1642
|
-
return;
|
|
1643
|
-
}
|
|
1644
|
-
const assistantMessage = { role: "assistant", content: assistantParts };
|
|
1645
|
-
this.messages.push(assistantMessage);
|
|
1646
|
-
await this.ctx.session.append({ type: "message", time: (/* @__PURE__ */ new Date()).toISOString(), message: assistantMessage });
|
|
1647
|
-
if (toolCallsToRun.length === 0) {
|
|
1648
|
-
this.ctx.events?.onTurnEnd?.();
|
|
1649
|
-
return;
|
|
1650
|
-
}
|
|
1651
|
-
this.ctx.events?.onAssistantTurn?.();
|
|
1652
|
-
for (const call of toolCallsToRun) {
|
|
1653
|
-
await this.runToolCall(call);
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
}
|
|
1657
|
-
handleEvent(ev, assistantParts, toolCallsToRun, onError) {
|
|
1658
|
-
switch (ev.type) {
|
|
1659
|
-
case "text":
|
|
1660
|
-
{
|
|
1661
|
-
const last = assistantParts[assistantParts.length - 1];
|
|
1662
|
-
if (last && last.type === "text") {
|
|
1663
|
-
last.text += ev.delta;
|
|
1664
|
-
} else {
|
|
1665
|
-
assistantParts.push({ type: "text", text: ev.delta });
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
this.ctx.events?.onText?.(ev.delta);
|
|
1669
|
-
break;
|
|
1670
|
-
case "tool_call_start":
|
|
1671
|
-
this.ctx.events?.onToolCallStart?.(ev.id, ev.name);
|
|
1672
|
-
break;
|
|
1673
|
-
case "tool_call_complete": {
|
|
1674
|
-
const callPart = { type: "tool_use", id: ev.id, name: ev.name, args: ev.args };
|
|
1675
|
-
assistantParts.push(callPart);
|
|
1676
|
-
toolCallsToRun.push(callPart);
|
|
1677
|
-
this.ctx.events?.onToolCallArgs?.(ev.id, ev.args);
|
|
1678
|
-
break;
|
|
1679
|
-
}
|
|
1680
|
-
case "finish":
|
|
1681
|
-
if (ev.usage) {
|
|
1682
|
-
this.ctx.events?.onUsage?.(ev.usage);
|
|
1683
|
-
this.ctx.session.append({
|
|
1684
|
-
type: "usage",
|
|
1685
|
-
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1686
|
-
usage: ev.usage,
|
|
1687
|
-
provider: this.ctx.llm.providerName,
|
|
1688
|
-
model: this.ctx.llm.model
|
|
1689
|
-
});
|
|
1690
|
-
}
|
|
1691
|
-
break;
|
|
1692
|
-
case "error":
|
|
1693
|
-
onError(ev.error);
|
|
1694
|
-
break;
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
async runToolCall(call) {
|
|
1698
|
-
const tool2 = this.ctx.tools.get(call.name);
|
|
1699
|
-
if (!tool2) {
|
|
1700
|
-
const result2 = `Tool "${call.name}" is not available.`;
|
|
1701
|
-
this.recordToolResult(call.id, call.name, result2, true);
|
|
1702
|
-
return;
|
|
1703
|
-
}
|
|
1704
|
-
const summary = tool2.summarize?.(call.args) ?? `${call.name}(...)`;
|
|
1705
|
-
const decision = this.ctx.permissions.decide({
|
|
1706
|
-
toolName: call.name,
|
|
1707
|
-
args: call.args,
|
|
1708
|
-
permission: tool2.permission
|
|
1709
|
-
});
|
|
1710
|
-
let approved = decision === "allow";
|
|
1711
|
-
if (decision === "deny") {
|
|
1712
|
-
const reason = this.ctx.permissions.getMode() === "plan" ? `Denied: you are in plan mode. Only read-only tools are available. Propose changes instead of executing.` : `Denied by policy: ${call.name}.`;
|
|
1713
|
-
this.recordToolResult(call.id, call.name, reason, true);
|
|
1714
|
-
return;
|
|
1715
|
-
}
|
|
1716
|
-
if (decision === "ask") {
|
|
1717
|
-
const userDecision = await this.ctx.events?.onPermissionRequest?.(call.name, call.args, summary) ?? "no";
|
|
1718
|
-
if (userDecision === "no") {
|
|
1719
|
-
this.recordToolResult(call.id, call.name, `User rejected ${call.name}.`, true);
|
|
1720
|
-
return;
|
|
1721
|
-
}
|
|
1722
|
-
if (userDecision === "session_allow") {
|
|
1723
|
-
this.ctx.permissions.allowForSession(call.name);
|
|
1724
|
-
}
|
|
1725
|
-
approved = true;
|
|
1726
|
-
}
|
|
1727
|
-
const toolCtx = {
|
|
1728
|
-
cwd: this.ctx.cwd,
|
|
1729
|
-
abortSignal: this.ctx.abortSignal,
|
|
1730
|
-
askPermission: async () => true,
|
|
1731
|
-
// 已在外层处理
|
|
1732
|
-
todos: this.todos,
|
|
1733
|
-
askQuestions: this.ctx.events?.onAskQuestions ? (qs) => this.ctx.events.onAskQuestions(qs) : void 0
|
|
1734
|
-
};
|
|
1735
|
-
const result = await this.ctx.tools.execute(call.name, call.args, toolCtx);
|
|
1736
|
-
this.recordToolResult(call.id, call.name, result.content, result.isError ?? false, result.summary, result.diff, result.kind);
|
|
1737
|
-
}
|
|
1738
|
-
recordToolResult(id, name, content, isError, summary, diff, kind) {
|
|
1739
|
-
const toolMsg = {
|
|
1740
|
-
role: "tool",
|
|
1741
|
-
toolUseId: id,
|
|
1742
|
-
content,
|
|
1743
|
-
isError,
|
|
1744
|
-
toolName: name,
|
|
1745
|
-
...diff ? { diff } : {},
|
|
1746
|
-
...summary ? { summary } : {},
|
|
1747
|
-
...kind ? { kind } : {}
|
|
1748
|
-
};
|
|
1749
|
-
this.messages.push(toolMsg);
|
|
1750
|
-
this.ctx.session.append({ type: "message", time: (/* @__PURE__ */ new Date()).toISOString(), message: toolMsg });
|
|
1751
|
-
this.ctx.events?.onToolResult?.(id, name, content, isError, summary);
|
|
1843
|
+
// src/preprocess/pipeline.ts
|
|
1844
|
+
var PipelineBlockedError = class extends Error {
|
|
1845
|
+
constructor(point, reason) {
|
|
1846
|
+
super(`blocked at ${point}: ${reason}`);
|
|
1847
|
+
this.point = point;
|
|
1848
|
+
this.reason = reason;
|
|
1849
|
+
this.name = "PipelineBlockedError";
|
|
1752
1850
|
}
|
|
1851
|
+
point;
|
|
1852
|
+
reason;
|
|
1753
1853
|
};
|
|
1754
1854
|
|
|
1755
1855
|
// src/loop/system-prompt.ts
|
|
@@ -1803,72 +1903,1917 @@ Below is MEMORY.md \u2014 your index of persistent facts about the user, project
|
|
|
1803
1903
|
return sections.join("\n\n");
|
|
1804
1904
|
}
|
|
1805
1905
|
|
|
1806
|
-
// src/
|
|
1906
|
+
// src/loop/hierarchy.ts
|
|
1807
1907
|
import { readFile as readFile6 } from "fs/promises";
|
|
1808
|
-
import { existsSync as existsSync3 } from "fs";
|
|
1908
|
+
import { existsSync as existsSync3, statSync as statSync2 } from "fs";
|
|
1809
1909
|
import { homedir as homedir6 } from "os";
|
|
1810
|
-
import { join as join4, resolve as resolve5 } from "path";
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
provider: z11.string().optional().describe("Fallback provider preset (only used when no models.local.json entry matches)."),
|
|
1821
|
-
model: z11.string().optional().describe("Active model id; should match an id in models.local.json."),
|
|
1822
|
-
temperature: z11.number().min(0).max(2).optional(),
|
|
1823
|
-
maxTokens: z11.number().int().positive().optional()
|
|
1824
|
-
});
|
|
1825
|
-
var PermissionsSchema = z11.object({
|
|
1826
|
-
allow: z11.array(z11.string()).optional(),
|
|
1827
|
-
ask: z11.array(z11.string()).optional(),
|
|
1828
|
-
deny: z11.array(z11.string()).optional(),
|
|
1829
|
-
defaultMode: z11.enum(["strict", "relaxed", "ask"]).optional()
|
|
1830
|
-
});
|
|
1831
|
-
var UISchema = z11.object({
|
|
1832
|
-
theme: z11.enum(["dark", "light"]).optional(),
|
|
1833
|
-
lang: z11.enum(["en", "zh-CN"]).optional(),
|
|
1834
|
-
showBanner: z11.boolean().optional()
|
|
1835
|
-
});
|
|
1836
|
-
var SettingsSchema = z11.object({
|
|
1837
|
-
llm: LLMConfigSchema.optional(),
|
|
1838
|
-
providers: z11.record(ProviderConfigSchema).optional(),
|
|
1839
|
-
permissions: PermissionsSchema.optional(),
|
|
1840
|
-
ui: UISchema.optional(),
|
|
1841
|
-
mcpServers: z11.record(z11.unknown()).optional(),
|
|
1842
|
-
skills: z11.object({
|
|
1843
|
-
enabled: z11.boolean().optional(),
|
|
1844
|
-
disabled: z11.array(z11.string()).optional()
|
|
1845
|
-
}).optional()
|
|
1846
|
-
}).passthrough();
|
|
1847
|
-
|
|
1848
|
-
// src/config/_env.ts
|
|
1849
|
-
var ENV_PATTERN2 = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
|
|
1850
|
-
function expandEnvVars(value) {
|
|
1851
|
-
if (typeof value === "string") {
|
|
1852
|
-
return value.replace(ENV_PATTERN2, (_match, name) => process.env[name] ?? "");
|
|
1910
|
+
import { dirname as dirname4, join as join4, resolve as resolve5 } from "path";
|
|
1911
|
+
function findProjectRoot(cwd) {
|
|
1912
|
+
let cur = resolve5(cwd);
|
|
1913
|
+
while (true) {
|
|
1914
|
+
if (existsSync3(join4(cur, ".git")) || existsSync3(join4(cur, ".muse"))) {
|
|
1915
|
+
return cur;
|
|
1916
|
+
}
|
|
1917
|
+
const parent = dirname4(cur);
|
|
1918
|
+
if (parent === cur) return resolve5(cwd);
|
|
1919
|
+
cur = parent;
|
|
1853
1920
|
}
|
|
1854
|
-
|
|
1855
|
-
|
|
1921
|
+
}
|
|
1922
|
+
async function loadSubdirMemory(absSubdir, opts = {}) {
|
|
1923
|
+
const sizeCap = opts.sizeCapBytes ?? 5120;
|
|
1924
|
+
const candidates = [
|
|
1925
|
+
{ path: join4(absSubdir, "MUSE.md"), source: "MUSE.md" }
|
|
1926
|
+
];
|
|
1927
|
+
if (opts.ignoreAgentsMd !== true) {
|
|
1928
|
+
candidates.push({ path: join4(absSubdir, "AGENTS.md"), source: "AGENTS.md" });
|
|
1856
1929
|
}
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1930
|
+
for (const { path, source } of candidates) {
|
|
1931
|
+
if (!existsSync3(path)) continue;
|
|
1932
|
+
try {
|
|
1933
|
+
let raw = (await readFile6(path, "utf-8")).trim();
|
|
1934
|
+
if (!raw) continue;
|
|
1935
|
+
const truncated = raw.length > sizeCap;
|
|
1936
|
+
if (truncated) {
|
|
1937
|
+
raw = raw.slice(0, sizeCap) + `
|
|
1938
|
+
|
|
1939
|
+
[... truncated (over ${sizeCap}B; use Read tool to view full file)]`;
|
|
1940
|
+
}
|
|
1941
|
+
return { content: raw, source, truncated };
|
|
1942
|
+
} catch {
|
|
1861
1943
|
}
|
|
1862
|
-
return result;
|
|
1863
1944
|
}
|
|
1864
|
-
return
|
|
1945
|
+
return null;
|
|
1865
1946
|
}
|
|
1866
1947
|
|
|
1867
|
-
// src/
|
|
1868
|
-
|
|
1869
|
-
|
|
1948
|
+
// src/loop/memory-index.ts
|
|
1949
|
+
import { readFile as readFile7, writeFile as writeFile4, mkdir as mkdir4, unlink as unlink2 } from "fs/promises";
|
|
1950
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1951
|
+
import { join as join5 } from "path";
|
|
1952
|
+
function indexPath(cwd, scope) {
|
|
1953
|
+
return join5(scopeDir(cwd, scope), ".index.json");
|
|
1870
1954
|
}
|
|
1871
|
-
|
|
1955
|
+
async function readPersistent(cwd, scope) {
|
|
1956
|
+
const path = indexPath(cwd, scope);
|
|
1957
|
+
if (!existsSync4(path)) return null;
|
|
1958
|
+
try {
|
|
1959
|
+
const raw = await readFile7(path, "utf-8");
|
|
1960
|
+
const parsed = JSON.parse(raw);
|
|
1961
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
1962
|
+
if (parsed.schemaVersion !== 1) return null;
|
|
1963
|
+
return parsed;
|
|
1964
|
+
} catch {
|
|
1965
|
+
return null;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
async function writePersistent(cwd, scope, persistent) {
|
|
1969
|
+
const dir = scopeDir(cwd, scope);
|
|
1970
|
+
await mkdir4(dir, { recursive: true });
|
|
1971
|
+
await writeFile4(indexPath(cwd, scope), JSON.stringify(persistent), "utf-8");
|
|
1972
|
+
}
|
|
1973
|
+
function snippet(body) {
|
|
1974
|
+
return body.length > 400 ? body.slice(0, 400) + "\n... [truncated]" : body;
|
|
1975
|
+
}
|
|
1976
|
+
function embedInputText(f) {
|
|
1977
|
+
return `${f.frontmatter.name}: ${f.frontmatter.description}`;
|
|
1978
|
+
}
|
|
1979
|
+
async function embedMemoryFile(provider, f) {
|
|
1980
|
+
const fm = f.frontmatter;
|
|
1981
|
+
const vector = await provider.embed(embedInputText(f));
|
|
1982
|
+
return {
|
|
1983
|
+
mtime: fm.updated_at,
|
|
1984
|
+
type: fm.type,
|
|
1985
|
+
trust: fm.trust,
|
|
1986
|
+
description: fm.description,
|
|
1987
|
+
bodySnippet: snippet(f.body),
|
|
1988
|
+
fullBody: f.body,
|
|
1989
|
+
vector
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
function makeEntry(name, scope, p) {
|
|
1993
|
+
return {
|
|
1994
|
+
name,
|
|
1995
|
+
type: p.type,
|
|
1996
|
+
trust: p.trust,
|
|
1997
|
+
scope,
|
|
1998
|
+
description: p.description,
|
|
1999
|
+
rawIndexLine: `[${p.trust}] - [${name}](${name}.md) \u2014 ${p.description}`,
|
|
2000
|
+
bodySnippet: p.bodySnippet,
|
|
2001
|
+
fullBody: p.fullBody,
|
|
2002
|
+
vector: p.vector
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
async function upsertMemoryEntry(index, name, scope) {
|
|
2006
|
+
let f;
|
|
2007
|
+
try {
|
|
2008
|
+
f = await readMemory(index.cwd, name, scope);
|
|
2009
|
+
} catch {
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
2012
|
+
const p = await embedMemoryFile(index.provider, f);
|
|
2013
|
+
const idx = index.entries.findIndex((e) => e.name === name && e.scope === f.scope);
|
|
2014
|
+
const newEntry = makeEntry(name, f.scope, p);
|
|
2015
|
+
if (idx >= 0) index.entries[idx] = newEntry;
|
|
2016
|
+
else index.entries.push(newEntry);
|
|
2017
|
+
const persistent = await readPersistent(index.cwd, f.scope) ?? {
|
|
2018
|
+
providerId: index.provider.id,
|
|
2019
|
+
dim: index.provider.dim,
|
|
2020
|
+
schemaVersion: 1,
|
|
2021
|
+
entries: {}
|
|
2022
|
+
};
|
|
2023
|
+
persistent.entries[name] = p;
|
|
2024
|
+
await writePersistent(index.cwd, f.scope, persistent);
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
// src/preprocess/tokenize.ts
|
|
2028
|
+
import { getEncoding } from "js-tiktoken";
|
|
2029
|
+
var _enc;
|
|
2030
|
+
function enc() {
|
|
2031
|
+
if (!_enc) _enc = getEncoding("cl100k_base");
|
|
2032
|
+
return _enc;
|
|
2033
|
+
}
|
|
2034
|
+
function countText(text) {
|
|
2035
|
+
if (!text) return 0;
|
|
2036
|
+
return enc().encode(text).length;
|
|
2037
|
+
}
|
|
2038
|
+
function countMessages(messages, systemPrompt, tools) {
|
|
2039
|
+
let total = 0;
|
|
2040
|
+
if (systemPrompt) total += countText(systemPrompt);
|
|
2041
|
+
for (const m of messages) {
|
|
2042
|
+
if (m.role === "tool") {
|
|
2043
|
+
total += countText(m.content);
|
|
2044
|
+
continue;
|
|
2045
|
+
}
|
|
2046
|
+
const content = m.content;
|
|
2047
|
+
if (typeof content === "string") {
|
|
2048
|
+
total += countText(content);
|
|
2049
|
+
} else if (Array.isArray(content)) {
|
|
2050
|
+
for (const p of content) {
|
|
2051
|
+
if (p.type === "text") total += countText(p.text ?? "");
|
|
2052
|
+
else if (p.type === "tool_use") {
|
|
2053
|
+
total += countText(p.name) + countText(JSON.stringify(p.args ?? {}));
|
|
2054
|
+
} else if (p.type === "file") {
|
|
2055
|
+
total += countText(p.path) + countText(p.text);
|
|
2056
|
+
} else if (p.type === "image") {
|
|
2057
|
+
total += countText(`[image: ${p.path ?? p.mediaType}]`);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
if (tools && tools.length > 0) {
|
|
2063
|
+
for (const t of tools) total += countText(JSON.stringify(t));
|
|
2064
|
+
}
|
|
2065
|
+
return total;
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
// src/preprocess/hooks.ts
|
|
2069
|
+
import { execa as execa3 } from "execa";
|
|
2070
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
2071
|
+
var MAX_TIMEOUT = 6e4;
|
|
2072
|
+
var SHELL_META = /[;|&><`$()]/;
|
|
2073
|
+
async function runHooks(point, input, hooks, logger = NOOP_LOGGER) {
|
|
2074
|
+
const specs = hooks?.[point];
|
|
2075
|
+
if (!specs || specs.length === 0) return {};
|
|
2076
|
+
let merged = {};
|
|
2077
|
+
let payload = { ...input };
|
|
2078
|
+
const matcherKey = pickMatcherKey(input);
|
|
2079
|
+
for (const spec of specs) {
|
|
2080
|
+
if (!matchesMatcher(spec.matcher, payload, matcherKey)) continue;
|
|
2081
|
+
if (SHELL_META.test(spec.command)) {
|
|
2082
|
+
logger.warn(`hook:${point}`, `command rejected: contains shell metachar`, { command: spec.command });
|
|
2083
|
+
continue;
|
|
2084
|
+
}
|
|
2085
|
+
const timeout = Math.min(spec.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT);
|
|
2086
|
+
try {
|
|
2087
|
+
const result = await execa3(spec.command, [], {
|
|
2088
|
+
input: JSON.stringify(payload),
|
|
2089
|
+
timeout,
|
|
2090
|
+
reject: false,
|
|
2091
|
+
env: hookEnv()
|
|
2092
|
+
});
|
|
2093
|
+
if (result.failed) {
|
|
2094
|
+
const reason = `exit ${result.exitCode ?? "?"} / signal ${result.signal ?? ""}`;
|
|
2095
|
+
handleHookError(point, spec, reason, logger);
|
|
2096
|
+
continue;
|
|
2097
|
+
}
|
|
2098
|
+
const text = (result.stdout ?? "").trim();
|
|
2099
|
+
if (!text) continue;
|
|
2100
|
+
let out;
|
|
2101
|
+
try {
|
|
2102
|
+
out = JSON.parse(text);
|
|
2103
|
+
} catch (err) {
|
|
2104
|
+
handleHookError(point, spec, `stdout not JSON: ${err.message}`, logger);
|
|
2105
|
+
continue;
|
|
2106
|
+
}
|
|
2107
|
+
if (out?.block && typeof out.block === "object" && typeof out.block.reason === "string") {
|
|
2108
|
+
throw new PipelineBlockedError(point, out.block.reason);
|
|
2109
|
+
}
|
|
2110
|
+
merged = { ...merged, ...out };
|
|
2111
|
+
payload = { ...payload, ...out };
|
|
2112
|
+
} catch (err) {
|
|
2113
|
+
if (err instanceof PipelineBlockedError) throw err;
|
|
2114
|
+
handleHookError(point, spec, err.message, logger);
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
return merged;
|
|
2118
|
+
}
|
|
2119
|
+
function handleHookError(point, spec, reason, logger) {
|
|
2120
|
+
const onError = spec.onError ?? "skip";
|
|
2121
|
+
logger.warn(`hook:${point}`, reason, { command: spec.command, onError });
|
|
2122
|
+
if (onError === "throw") {
|
|
2123
|
+
throw new Error(`hook ${point} failed: ${reason}`);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
function pickMatcherKey(input) {
|
|
2127
|
+
if (typeof input.toolName === "string") return "toolName";
|
|
2128
|
+
if (typeof input.path === "string") return "path";
|
|
2129
|
+
return void 0;
|
|
2130
|
+
}
|
|
2131
|
+
function matchesMatcher(matcher, payload, key) {
|
|
2132
|
+
if (!matcher || matcher === ".*") return true;
|
|
2133
|
+
if (!key) return true;
|
|
2134
|
+
const value = payload[key];
|
|
2135
|
+
if (typeof value !== "string") return true;
|
|
2136
|
+
try {
|
|
2137
|
+
return new RegExp(matcher).test(value);
|
|
2138
|
+
} catch {
|
|
2139
|
+
return false;
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
function hookEnv() {
|
|
2143
|
+
const allow = ["PATH", "HOME", "LANG", "LC_ALL", "TERM", "USER", "SHELL"];
|
|
2144
|
+
const out = {};
|
|
2145
|
+
for (const key of allow) {
|
|
2146
|
+
const v = process.env[key];
|
|
2147
|
+
if (v) out[key] = v;
|
|
2148
|
+
}
|
|
2149
|
+
return out;
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
// src/loop/prompts/summarize.ts
|
|
2153
|
+
var FACTS_BLOCK = `
|
|
2154
|
+
|
|
2155
|
+
---
|
|
2156
|
+
|
|
2157
|
+
# Extracted Facts for Long-term Memory (optional)
|
|
2158
|
+
|
|
2159
|
+
If \u2014 and only if \u2014 the conversation revealed durable facts worth keeping across future sessions,
|
|
2160
|
+
output one JSON code block in this format:
|
|
2161
|
+
|
|
2162
|
+
\`\`\`json
|
|
2163
|
+
{
|
|
2164
|
+
"facts": [
|
|
2165
|
+
{
|
|
2166
|
+
"name": "kebab-case-slug",
|
|
2167
|
+
"type": "user" | "feedback" | "project" | "reference",
|
|
2168
|
+
"description": "one-line summary used as memory index hook",
|
|
2169
|
+
"body": "markdown content of the fact"
|
|
2170
|
+
}
|
|
2171
|
+
]
|
|
2172
|
+
}
|
|
2173
|
+
\`\`\`
|
|
2174
|
+
|
|
2175
|
+
Strict constraints (read carefully \u2014 over-eager extraction breaks user trust):
|
|
2176
|
+
- Only include facts with **cross-session value**: user role / preference, project hard rule, validated approach
|
|
2177
|
+
- Do NOT include: code visible in the repo, git history facts, transient task state, errors already fixed
|
|
2178
|
+
- Do NOT save chitchat, model-internal reasoning, or speculation
|
|
2179
|
+
- If nothing meets the bar, output \`{"facts": []}\` \u2014 empty is better than noise
|
|
2180
|
+
- Each fact \`name\` must be a unique slug (kebab- or snake-case alphanumeric)
|
|
2181
|
+
- For \`feedback\` / \`project\` types, body should lead with the rule, then "Why:" and "How to apply:" lines`;
|
|
2182
|
+
function buildSummaryPrompt(transcript, schema) {
|
|
2183
|
+
if (schema === "9-section") {
|
|
2184
|
+
return SECTION_9_TEMPLATE.replace("{TRANSCRIPT}", transcript) + FACTS_BLOCK;
|
|
2185
|
+
}
|
|
2186
|
+
return SECTION_6_TEMPLATE.replace("{TRANSCRIPT}", transcript) + FACTS_BLOCK;
|
|
2187
|
+
}
|
|
2188
|
+
var SECTION_9_TEMPLATE = `Summarize the following conversation. Use **exactly these 9 sections in this order**. If a section has no content, write "(none)" \u2014 do not skip section numbers.
|
|
2189
|
+
|
|
2190
|
+
# Conversation Summary
|
|
2191
|
+
|
|
2192
|
+
1. **Primary Request and Intent**
|
|
2193
|
+
\u2014 Quote the user's original request verbatim (use > markdown quotes). Do not paraphrase.
|
|
2194
|
+
|
|
2195
|
+
2. **Key Technical Concepts**
|
|
2196
|
+
\u2014 Bullet list of frameworks, languages, libraries, patterns relevant to the conversation.
|
|
2197
|
+
|
|
2198
|
+
3. **Files and Code Sections**
|
|
2199
|
+
\u2014 File path + function / class name + what was read or changed. Be concrete.
|
|
2200
|
+
|
|
2201
|
+
4. **Errors and fixes**
|
|
2202
|
+
\u2014 Each error message (or class of error) + the specific fix taken. Future you will repeat these mistakes if this section is sloppy.
|
|
2203
|
+
|
|
2204
|
+
5. **Problem Solving**
|
|
2205
|
+
\u2014 Key reasoning steps and trade-offs considered. "I chose A over B because X."
|
|
2206
|
+
|
|
2207
|
+
6. **All user messages**
|
|
2208
|
+
\u2014 **List every user message verbatim**, separated by \`---\`. Sacred section: NEVER omit, paraphrase, or merge user messages.
|
|
2209
|
+
|
|
2210
|
+
7. **Pending Tasks**
|
|
2211
|
+
\u2014 TODOs the user explicitly stated and items the assistant identified as not-yet-done.
|
|
2212
|
+
|
|
2213
|
+
8. **Current Work**
|
|
2214
|
+
\u2014 What was actively being done at the moment of summarization. One paragraph.
|
|
2215
|
+
|
|
2216
|
+
9. **Optional Next Step**
|
|
2217
|
+
\u2014 Recommended next action, anchored in quote(s) from the user's recent messages. If unclear, write "(awaiting user direction)".
|
|
2218
|
+
|
|
2219
|
+
--- BEGIN CONVERSATION ---
|
|
2220
|
+
{TRANSCRIPT}
|
|
2221
|
+
--- END CONVERSATION ---`;
|
|
2222
|
+
var SECTION_6_TEMPLATE = `Summarize the following conversation. Use these 6 sections in order. Do not skip section numbers; write "(none)" for empty sections.
|
|
2223
|
+
|
|
2224
|
+
# Conversation Summary
|
|
2225
|
+
|
|
2226
|
+
1. **Primary Request and Intent** \u2014 Quote the user's original request verbatim.
|
|
2227
|
+
|
|
2228
|
+
2. **Concepts, Decisions, and Reasoning** \u2014 Frameworks involved + key trade-offs.
|
|
2229
|
+
|
|
2230
|
+
3. **Files and Changes** \u2014 File paths + what was modified.
|
|
2231
|
+
|
|
2232
|
+
4. **Errors and fixes** \u2014 Error messages + specific fixes.
|
|
2233
|
+
|
|
2234
|
+
5. **All user messages** \u2014 List every user message verbatim, separated by \`---\`. Sacred: never omit.
|
|
2235
|
+
|
|
2236
|
+
6. **Current Work + Next Step** \u2014 What was being done + recommended next action (anchored in user quotes).
|
|
2237
|
+
|
|
2238
|
+
--- BEGIN CONVERSATION ---
|
|
2239
|
+
{TRANSCRIPT}
|
|
2240
|
+
--- END CONVERSATION ---`;
|
|
2241
|
+
function extractFacts(summaryText) {
|
|
2242
|
+
const jsonBlock = summaryText.match(/```(?:json)?\s*\n([\s\S]*?)\n```/);
|
|
2243
|
+
if (!jsonBlock) return [];
|
|
2244
|
+
let parsed;
|
|
2245
|
+
try {
|
|
2246
|
+
parsed = JSON.parse(jsonBlock[1].trim());
|
|
2247
|
+
} catch {
|
|
2248
|
+
return [];
|
|
2249
|
+
}
|
|
2250
|
+
if (typeof parsed !== "object" || parsed === null) return [];
|
|
2251
|
+
const facts = parsed.facts;
|
|
2252
|
+
if (!Array.isArray(facts)) return [];
|
|
2253
|
+
const out = /* @__PURE__ */ new Map();
|
|
2254
|
+
for (const raw of facts) {
|
|
2255
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
2256
|
+
const r = raw;
|
|
2257
|
+
const name = typeof r.name === "string" ? r.name : "";
|
|
2258
|
+
const type = r.type;
|
|
2259
|
+
const description = typeof r.description === "string" ? r.description : "";
|
|
2260
|
+
const body = typeof r.body === "string" ? r.body : "";
|
|
2261
|
+
if (!name || !description || !body) continue;
|
|
2262
|
+
if (!/^[a-z0-9][a-z0-9-_]*$/i.test(name)) continue;
|
|
2263
|
+
if (type !== "user" && type !== "feedback" && type !== "project" && type !== "reference") continue;
|
|
2264
|
+
out.set(name, { name, type, description, body });
|
|
2265
|
+
}
|
|
2266
|
+
return [...out.values()];
|
|
2267
|
+
}
|
|
2268
|
+
function stripFactsBlock(summaryText) {
|
|
2269
|
+
return summaryText.replace(/---\s*\n\s*#?\s*Extracted Facts[\s\S]*?```(?:json)?[\s\S]*?```/i, "").replace(/```(?:json)?\s*\n[\s\S]*?\n```/g, (match) => {
|
|
2270
|
+
if (/"facts"\s*:/.test(match)) return "";
|
|
2271
|
+
return match;
|
|
2272
|
+
}).trim();
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
// src/loop/context.ts
|
|
2276
|
+
var CompactBlockedError = class extends Error {
|
|
2277
|
+
constructor(reason) {
|
|
2278
|
+
super(`compact blocked by PreCompact hook: ${reason}`);
|
|
2279
|
+
this.reason = reason;
|
|
2280
|
+
this.name = "CompactBlockedError";
|
|
2281
|
+
}
|
|
2282
|
+
reason;
|
|
2283
|
+
};
|
|
2284
|
+
async function compactMessages(messages, opts) {
|
|
2285
|
+
const keepRecent = opts.keepRecent ?? 4;
|
|
2286
|
+
const cutoff = findSafeCutoff(messages, keepRecent);
|
|
2287
|
+
if (cutoff <= 0) {
|
|
2288
|
+
return {
|
|
2289
|
+
newMessages: messages,
|
|
2290
|
+
summary: "",
|
|
2291
|
+
originalCount: messages.length,
|
|
2292
|
+
newCount: messages.length,
|
|
2293
|
+
noop: true
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
try {
|
|
2297
|
+
await runHooks(
|
|
2298
|
+
"PreCompact",
|
|
2299
|
+
{ messageCount: messages.length, cutoff, keepRecent },
|
|
2300
|
+
opts.hooks
|
|
2301
|
+
);
|
|
2302
|
+
} catch (err) {
|
|
2303
|
+
if (err instanceof PipelineBlockedError) {
|
|
2304
|
+
throw new CompactBlockedError(err.reason);
|
|
2305
|
+
}
|
|
2306
|
+
throw err;
|
|
2307
|
+
}
|
|
2308
|
+
const older = messages.slice(0, cutoff);
|
|
2309
|
+
const recent = messages.slice(cutoff);
|
|
2310
|
+
const schema = opts.schema ?? "9-section";
|
|
2311
|
+
const rawSummary = await summarizeConversation(
|
|
2312
|
+
older,
|
|
2313
|
+
opts.llm,
|
|
2314
|
+
schema,
|
|
2315
|
+
opts.abortSignal,
|
|
2316
|
+
opts.onProgress
|
|
2317
|
+
);
|
|
2318
|
+
const summary = stripFactsBlock(rawSummary);
|
|
2319
|
+
const facts = opts.cwd ? extractFacts(rawSummary) : [];
|
|
2320
|
+
const summaryMessage = {
|
|
2321
|
+
role: "user",
|
|
2322
|
+
content: `[Previous conversation summary]
|
|
2323
|
+
|
|
2324
|
+
${summary}
|
|
2325
|
+
|
|
2326
|
+
[End of summary. The conversation continues below.]`
|
|
2327
|
+
};
|
|
2328
|
+
const newMessages = [summaryMessage, ...recent];
|
|
2329
|
+
try {
|
|
2330
|
+
await runHooks(
|
|
2331
|
+
"PostCompact",
|
|
2332
|
+
{ before: messages.length, after: newMessages.length, summary, factCount: facts.length },
|
|
2333
|
+
opts.hooks
|
|
2334
|
+
);
|
|
2335
|
+
} catch (err) {
|
|
2336
|
+
if (err instanceof PipelineBlockedError) {
|
|
2337
|
+
} else {
|
|
2338
|
+
throw err;
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
let promotedFacts;
|
|
2342
|
+
const shouldPromote = opts.promoteFactsToMemory !== false && opts.cwd && facts.length > 0;
|
|
2343
|
+
if (shouldPromote) {
|
|
2344
|
+
promotedFacts = await promoteFactsToMemory(facts, opts.cwd, opts.hooks);
|
|
2345
|
+
}
|
|
2346
|
+
return {
|
|
2347
|
+
newMessages,
|
|
2348
|
+
summary,
|
|
2349
|
+
originalCount: messages.length,
|
|
2350
|
+
newCount: newMessages.length,
|
|
2351
|
+
noop: false,
|
|
2352
|
+
promotedFacts
|
|
2353
|
+
};
|
|
2354
|
+
}
|
|
2355
|
+
async function promoteFactsToMemory(facts, cwd, hooks) {
|
|
2356
|
+
const results = [];
|
|
2357
|
+
for (const fact of facts) {
|
|
2358
|
+
try {
|
|
2359
|
+
await runHooks(
|
|
2360
|
+
"MemoryPromote",
|
|
2361
|
+
{ name: fact.name, type: fact.type, description: fact.description, body: fact.body, source: "compact-promote" },
|
|
2362
|
+
hooks
|
|
2363
|
+
);
|
|
2364
|
+
} catch (err) {
|
|
2365
|
+
if (err instanceof PipelineBlockedError) {
|
|
2366
|
+
results.push({
|
|
2367
|
+
name: fact.name,
|
|
2368
|
+
type: fact.type,
|
|
2369
|
+
description: fact.description,
|
|
2370
|
+
status: "blocked",
|
|
2371
|
+
reason: err.reason
|
|
2372
|
+
});
|
|
2373
|
+
continue;
|
|
2374
|
+
}
|
|
2375
|
+
results.push({
|
|
2376
|
+
name: fact.name,
|
|
2377
|
+
type: fact.type,
|
|
2378
|
+
description: fact.description,
|
|
2379
|
+
status: "failed",
|
|
2380
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
2381
|
+
});
|
|
2382
|
+
continue;
|
|
2383
|
+
}
|
|
2384
|
+
try {
|
|
2385
|
+
await writeMemory(cwd, {
|
|
2386
|
+
name: fact.name,
|
|
2387
|
+
description: fact.description,
|
|
2388
|
+
type: fact.type,
|
|
2389
|
+
body: fact.body,
|
|
2390
|
+
trust: "auto",
|
|
2391
|
+
source: "compact-promote"
|
|
2392
|
+
});
|
|
2393
|
+
results.push({ name: fact.name, type: fact.type, description: fact.description, status: "saved" });
|
|
2394
|
+
} catch (err) {
|
|
2395
|
+
results.push({
|
|
2396
|
+
name: fact.name,
|
|
2397
|
+
type: fact.type,
|
|
2398
|
+
description: fact.description,
|
|
2399
|
+
status: "failed",
|
|
2400
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
2401
|
+
});
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
return results;
|
|
2405
|
+
}
|
|
2406
|
+
function findSafeCutoff(messages, keepRecent) {
|
|
2407
|
+
if (messages.length <= keepRecent) return 0;
|
|
2408
|
+
const ideal = Math.max(0, messages.length - keepRecent);
|
|
2409
|
+
for (let i = ideal; i > 0; i--) {
|
|
2410
|
+
if (messages[i].role !== "user") continue;
|
|
2411
|
+
if (hasUnresolvedToolUse(messages.slice(0, i))) continue;
|
|
2412
|
+
return i;
|
|
2413
|
+
}
|
|
2414
|
+
return 0;
|
|
2415
|
+
}
|
|
2416
|
+
function hasUnresolvedToolUse(older) {
|
|
2417
|
+
const seenToolUseIds = /* @__PURE__ */ new Set();
|
|
2418
|
+
const seenToolResultIds = /* @__PURE__ */ new Set();
|
|
2419
|
+
for (const msg of older) {
|
|
2420
|
+
if (msg.role === "assistant") {
|
|
2421
|
+
for (const part of msg.content) {
|
|
2422
|
+
if (part.type === "tool_use") seenToolUseIds.add(part.id);
|
|
2423
|
+
}
|
|
2424
|
+
} else if (msg.role === "tool") {
|
|
2425
|
+
seenToolResultIds.add(msg.toolUseId);
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
for (const id of seenToolUseIds) {
|
|
2429
|
+
if (!seenToolResultIds.has(id)) return true;
|
|
2430
|
+
}
|
|
2431
|
+
return false;
|
|
2432
|
+
}
|
|
2433
|
+
async function summarizeConversation(older, llm, schema, abortSignal, onProgress) {
|
|
2434
|
+
const transcript = renderTranscript(older);
|
|
2435
|
+
const prompt = [
|
|
2436
|
+
{
|
|
2437
|
+
role: "user",
|
|
2438
|
+
content: buildSummaryPrompt(transcript, schema)
|
|
2439
|
+
}
|
|
2440
|
+
];
|
|
2441
|
+
let text = "";
|
|
2442
|
+
for await (const ev of llm.stream({ messages: prompt, abortSignal })) {
|
|
2443
|
+
if (ev.type === "text") {
|
|
2444
|
+
text += ev.delta;
|
|
2445
|
+
onProgress?.(text.length);
|
|
2446
|
+
} else if (ev.type === "error") throw ev.error;
|
|
2447
|
+
}
|
|
2448
|
+
return text.trim() || "(empty summary)";
|
|
2449
|
+
}
|
|
2450
|
+
function renderTranscript(messages) {
|
|
2451
|
+
const lines = [];
|
|
2452
|
+
for (const msg of messages) {
|
|
2453
|
+
switch (msg.role) {
|
|
2454
|
+
case "system":
|
|
2455
|
+
lines.push(`[system]
|
|
2456
|
+
${msg.content}
|
|
2457
|
+
`);
|
|
2458
|
+
break;
|
|
2459
|
+
case "user":
|
|
2460
|
+
lines.push(`[user]
|
|
2461
|
+
${typeof msg.content === "string" ? msg.content : flattenContent(msg.content)}
|
|
2462
|
+
`);
|
|
2463
|
+
break;
|
|
2464
|
+
case "assistant":
|
|
2465
|
+
lines.push(`[assistant]
|
|
2466
|
+
${renderAssistant(msg)}
|
|
2467
|
+
`);
|
|
2468
|
+
break;
|
|
2469
|
+
case "tool":
|
|
2470
|
+
lines.push(`[tool result${msg.isError ? " ERROR" : ""}]
|
|
2471
|
+
${msg.content}
|
|
2472
|
+
`);
|
|
2473
|
+
break;
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
return lines.join("\n");
|
|
2477
|
+
}
|
|
2478
|
+
function renderAssistant(msg) {
|
|
2479
|
+
const parts = [];
|
|
2480
|
+
for (const part of msg.content) {
|
|
2481
|
+
if (part.type === "text") parts.push(part.text);
|
|
2482
|
+
else if (part.type === "tool_use") {
|
|
2483
|
+
parts.push(`<tool_call name="${part.name}" args=${JSON.stringify(part.args)} />`);
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
return parts.join("\n");
|
|
2487
|
+
}
|
|
2488
|
+
function flattenContent(parts) {
|
|
2489
|
+
const out = [];
|
|
2490
|
+
for (const p of parts) {
|
|
2491
|
+
if (p.type === "text") out.push(p.text);
|
|
2492
|
+
else if (p.type === "file") out.push(`[file: ${p.path}]`);
|
|
2493
|
+
else if (p.type === "image") out.push(`[image: ${p.path ?? p.mediaType}]`);
|
|
2494
|
+
}
|
|
2495
|
+
return out.join("\n");
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
// src/preprocess/request/budget-guard.ts
|
|
2499
|
+
var BudgetExceededError = class extends Error {
|
|
2500
|
+
constructor(estimated, budget, reason) {
|
|
2501
|
+
super(`context budget exceeded: estimated ${estimated} > budget ${budget} (${reason})`);
|
|
2502
|
+
this.estimated = estimated;
|
|
2503
|
+
this.budget = budget;
|
|
2504
|
+
this.reason = reason;
|
|
2505
|
+
this.name = "BudgetExceededError";
|
|
2506
|
+
}
|
|
2507
|
+
estimated;
|
|
2508
|
+
budget;
|
|
2509
|
+
reason;
|
|
2510
|
+
};
|
|
2511
|
+
|
|
2512
|
+
// src/preprocess/request/ctx.ts
|
|
2513
|
+
function createRequestCtx(init) {
|
|
2514
|
+
return {
|
|
2515
|
+
messages: init.messages,
|
|
2516
|
+
systemPrompt: "",
|
|
2517
|
+
tools: [],
|
|
2518
|
+
modelId: init.modelId,
|
|
2519
|
+
mode: init.mode,
|
|
2520
|
+
cwd: init.cwd,
|
|
2521
|
+
services: init.services,
|
|
2522
|
+
settings: init.settings ?? {}
|
|
2523
|
+
};
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
// src/loop/agent.ts
|
|
2527
|
+
import { existsSync as existsSync5 } from "fs";
|
|
2528
|
+
import { dirname as dirname5, isAbsolute as isAbsolute4, join as join6, relative, resolve as resolve6 } from "path";
|
|
2529
|
+
|
|
2530
|
+
// src/preprocess/truncate.ts
|
|
2531
|
+
var DEFAULT_BUDGET = 64 * 1024;
|
|
2532
|
+
|
|
2533
|
+
// node_modules/chalk/source/vendor/ansi-styles/index.js
|
|
2534
|
+
var ANSI_BACKGROUND_OFFSET = 10;
|
|
2535
|
+
var wrapAnsi16 = (offset = 0) => (code) => `\x1B[${code + offset}m`;
|
|
2536
|
+
var wrapAnsi256 = (offset = 0) => (code) => `\x1B[${38 + offset};5;${code}m`;
|
|
2537
|
+
var wrapAnsi16m = (offset = 0) => (red, green, blue) => `\x1B[${38 + offset};2;${red};${green};${blue}m`;
|
|
2538
|
+
var styles = {
|
|
2539
|
+
modifier: {
|
|
2540
|
+
reset: [0, 0],
|
|
2541
|
+
// 21 isn't widely supported and 22 does the same thing
|
|
2542
|
+
bold: [1, 22],
|
|
2543
|
+
dim: [2, 22],
|
|
2544
|
+
italic: [3, 23],
|
|
2545
|
+
underline: [4, 24],
|
|
2546
|
+
overline: [53, 55],
|
|
2547
|
+
inverse: [7, 27],
|
|
2548
|
+
hidden: [8, 28],
|
|
2549
|
+
strikethrough: [9, 29]
|
|
2550
|
+
},
|
|
2551
|
+
color: {
|
|
2552
|
+
black: [30, 39],
|
|
2553
|
+
red: [31, 39],
|
|
2554
|
+
green: [32, 39],
|
|
2555
|
+
yellow: [33, 39],
|
|
2556
|
+
blue: [34, 39],
|
|
2557
|
+
magenta: [35, 39],
|
|
2558
|
+
cyan: [36, 39],
|
|
2559
|
+
white: [37, 39],
|
|
2560
|
+
// Bright color
|
|
2561
|
+
blackBright: [90, 39],
|
|
2562
|
+
gray: [90, 39],
|
|
2563
|
+
// Alias of `blackBright`
|
|
2564
|
+
grey: [90, 39],
|
|
2565
|
+
// Alias of `blackBright`
|
|
2566
|
+
redBright: [91, 39],
|
|
2567
|
+
greenBright: [92, 39],
|
|
2568
|
+
yellowBright: [93, 39],
|
|
2569
|
+
blueBright: [94, 39],
|
|
2570
|
+
magentaBright: [95, 39],
|
|
2571
|
+
cyanBright: [96, 39],
|
|
2572
|
+
whiteBright: [97, 39]
|
|
2573
|
+
},
|
|
2574
|
+
bgColor: {
|
|
2575
|
+
bgBlack: [40, 49],
|
|
2576
|
+
bgRed: [41, 49],
|
|
2577
|
+
bgGreen: [42, 49],
|
|
2578
|
+
bgYellow: [43, 49],
|
|
2579
|
+
bgBlue: [44, 49],
|
|
2580
|
+
bgMagenta: [45, 49],
|
|
2581
|
+
bgCyan: [46, 49],
|
|
2582
|
+
bgWhite: [47, 49],
|
|
2583
|
+
// Bright color
|
|
2584
|
+
bgBlackBright: [100, 49],
|
|
2585
|
+
bgGray: [100, 49],
|
|
2586
|
+
// Alias of `bgBlackBright`
|
|
2587
|
+
bgGrey: [100, 49],
|
|
2588
|
+
// Alias of `bgBlackBright`
|
|
2589
|
+
bgRedBright: [101, 49],
|
|
2590
|
+
bgGreenBright: [102, 49],
|
|
2591
|
+
bgYellowBright: [103, 49],
|
|
2592
|
+
bgBlueBright: [104, 49],
|
|
2593
|
+
bgMagentaBright: [105, 49],
|
|
2594
|
+
bgCyanBright: [106, 49],
|
|
2595
|
+
bgWhiteBright: [107, 49]
|
|
2596
|
+
}
|
|
2597
|
+
};
|
|
2598
|
+
var modifierNames = Object.keys(styles.modifier);
|
|
2599
|
+
var foregroundColorNames = Object.keys(styles.color);
|
|
2600
|
+
var backgroundColorNames = Object.keys(styles.bgColor);
|
|
2601
|
+
var colorNames = [...foregroundColorNames, ...backgroundColorNames];
|
|
2602
|
+
function assembleStyles() {
|
|
2603
|
+
const codes = /* @__PURE__ */ new Map();
|
|
2604
|
+
for (const [groupName, group] of Object.entries(styles)) {
|
|
2605
|
+
for (const [styleName, style] of Object.entries(group)) {
|
|
2606
|
+
styles[styleName] = {
|
|
2607
|
+
open: `\x1B[${style[0]}m`,
|
|
2608
|
+
close: `\x1B[${style[1]}m`
|
|
2609
|
+
};
|
|
2610
|
+
group[styleName] = styles[styleName];
|
|
2611
|
+
codes.set(style[0], style[1]);
|
|
2612
|
+
}
|
|
2613
|
+
Object.defineProperty(styles, groupName, {
|
|
2614
|
+
value: group,
|
|
2615
|
+
enumerable: false
|
|
2616
|
+
});
|
|
2617
|
+
}
|
|
2618
|
+
Object.defineProperty(styles, "codes", {
|
|
2619
|
+
value: codes,
|
|
2620
|
+
enumerable: false
|
|
2621
|
+
});
|
|
2622
|
+
styles.color.close = "\x1B[39m";
|
|
2623
|
+
styles.bgColor.close = "\x1B[49m";
|
|
2624
|
+
styles.color.ansi = wrapAnsi16();
|
|
2625
|
+
styles.color.ansi256 = wrapAnsi256();
|
|
2626
|
+
styles.color.ansi16m = wrapAnsi16m();
|
|
2627
|
+
styles.bgColor.ansi = wrapAnsi16(ANSI_BACKGROUND_OFFSET);
|
|
2628
|
+
styles.bgColor.ansi256 = wrapAnsi256(ANSI_BACKGROUND_OFFSET);
|
|
2629
|
+
styles.bgColor.ansi16m = wrapAnsi16m(ANSI_BACKGROUND_OFFSET);
|
|
2630
|
+
Object.defineProperties(styles, {
|
|
2631
|
+
rgbToAnsi256: {
|
|
2632
|
+
value(red, green, blue) {
|
|
2633
|
+
if (red === green && green === blue) {
|
|
2634
|
+
if (red < 8) {
|
|
2635
|
+
return 16;
|
|
2636
|
+
}
|
|
2637
|
+
if (red > 248) {
|
|
2638
|
+
return 231;
|
|
2639
|
+
}
|
|
2640
|
+
return Math.round((red - 8) / 247 * 24) + 232;
|
|
2641
|
+
}
|
|
2642
|
+
return 16 + 36 * Math.round(red / 255 * 5) + 6 * Math.round(green / 255 * 5) + Math.round(blue / 255 * 5);
|
|
2643
|
+
},
|
|
2644
|
+
enumerable: false
|
|
2645
|
+
},
|
|
2646
|
+
hexToRgb: {
|
|
2647
|
+
value(hex) {
|
|
2648
|
+
const matches = /[a-f\d]{6}|[a-f\d]{3}/i.exec(hex.toString(16));
|
|
2649
|
+
if (!matches) {
|
|
2650
|
+
return [0, 0, 0];
|
|
2651
|
+
}
|
|
2652
|
+
let [colorString] = matches;
|
|
2653
|
+
if (colorString.length === 3) {
|
|
2654
|
+
colorString = [...colorString].map((character) => character + character).join("");
|
|
2655
|
+
}
|
|
2656
|
+
const integer = Number.parseInt(colorString, 16);
|
|
2657
|
+
return [
|
|
2658
|
+
/* eslint-disable no-bitwise */
|
|
2659
|
+
integer >> 16 & 255,
|
|
2660
|
+
integer >> 8 & 255,
|
|
2661
|
+
integer & 255
|
|
2662
|
+
/* eslint-enable no-bitwise */
|
|
2663
|
+
];
|
|
2664
|
+
},
|
|
2665
|
+
enumerable: false
|
|
2666
|
+
},
|
|
2667
|
+
hexToAnsi256: {
|
|
2668
|
+
value: (hex) => styles.rgbToAnsi256(...styles.hexToRgb(hex)),
|
|
2669
|
+
enumerable: false
|
|
2670
|
+
},
|
|
2671
|
+
ansi256ToAnsi: {
|
|
2672
|
+
value(code) {
|
|
2673
|
+
if (code < 8) {
|
|
2674
|
+
return 30 + code;
|
|
2675
|
+
}
|
|
2676
|
+
if (code < 16) {
|
|
2677
|
+
return 90 + (code - 8);
|
|
2678
|
+
}
|
|
2679
|
+
let red;
|
|
2680
|
+
let green;
|
|
2681
|
+
let blue;
|
|
2682
|
+
if (code >= 232) {
|
|
2683
|
+
red = ((code - 232) * 10 + 8) / 255;
|
|
2684
|
+
green = red;
|
|
2685
|
+
blue = red;
|
|
2686
|
+
} else {
|
|
2687
|
+
code -= 16;
|
|
2688
|
+
const remainder = code % 36;
|
|
2689
|
+
red = Math.floor(code / 36) / 5;
|
|
2690
|
+
green = Math.floor(remainder / 6) / 5;
|
|
2691
|
+
blue = remainder % 6 / 5;
|
|
2692
|
+
}
|
|
2693
|
+
const value = Math.max(red, green, blue) * 2;
|
|
2694
|
+
if (value === 0) {
|
|
2695
|
+
return 30;
|
|
2696
|
+
}
|
|
2697
|
+
let result = 30 + (Math.round(blue) << 2 | Math.round(green) << 1 | Math.round(red));
|
|
2698
|
+
if (value === 2) {
|
|
2699
|
+
result += 60;
|
|
2700
|
+
}
|
|
2701
|
+
return result;
|
|
2702
|
+
},
|
|
2703
|
+
enumerable: false
|
|
2704
|
+
},
|
|
2705
|
+
rgbToAnsi: {
|
|
2706
|
+
value: (red, green, blue) => styles.ansi256ToAnsi(styles.rgbToAnsi256(red, green, blue)),
|
|
2707
|
+
enumerable: false
|
|
2708
|
+
},
|
|
2709
|
+
hexToAnsi: {
|
|
2710
|
+
value: (hex) => styles.ansi256ToAnsi(styles.hexToAnsi256(hex)),
|
|
2711
|
+
enumerable: false
|
|
2712
|
+
}
|
|
2713
|
+
});
|
|
2714
|
+
return styles;
|
|
2715
|
+
}
|
|
2716
|
+
var ansiStyles = assembleStyles();
|
|
2717
|
+
var ansi_styles_default = ansiStyles;
|
|
2718
|
+
|
|
2719
|
+
// node_modules/chalk/source/vendor/supports-color/index.js
|
|
2720
|
+
import process2 from "process";
|
|
2721
|
+
import os from "os";
|
|
2722
|
+
import tty from "tty";
|
|
2723
|
+
function hasFlag(flag, argv = globalThis.Deno ? globalThis.Deno.args : process2.argv) {
|
|
2724
|
+
const prefix = flag.startsWith("-") ? "" : flag.length === 1 ? "-" : "--";
|
|
2725
|
+
const position = argv.indexOf(prefix + flag);
|
|
2726
|
+
const terminatorPosition = argv.indexOf("--");
|
|
2727
|
+
return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
|
|
2728
|
+
}
|
|
2729
|
+
var { env } = process2;
|
|
2730
|
+
var flagForceColor;
|
|
2731
|
+
if (hasFlag("no-color") || hasFlag("no-colors") || hasFlag("color=false") || hasFlag("color=never")) {
|
|
2732
|
+
flagForceColor = 0;
|
|
2733
|
+
} else if (hasFlag("color") || hasFlag("colors") || hasFlag("color=true") || hasFlag("color=always")) {
|
|
2734
|
+
flagForceColor = 1;
|
|
2735
|
+
}
|
|
2736
|
+
function envForceColor() {
|
|
2737
|
+
if ("FORCE_COLOR" in env) {
|
|
2738
|
+
if (env.FORCE_COLOR === "true") {
|
|
2739
|
+
return 1;
|
|
2740
|
+
}
|
|
2741
|
+
if (env.FORCE_COLOR === "false") {
|
|
2742
|
+
return 0;
|
|
2743
|
+
}
|
|
2744
|
+
return env.FORCE_COLOR.length === 0 ? 1 : Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3);
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
function translateLevel(level) {
|
|
2748
|
+
if (level === 0) {
|
|
2749
|
+
return false;
|
|
2750
|
+
}
|
|
2751
|
+
return {
|
|
2752
|
+
level,
|
|
2753
|
+
hasBasic: true,
|
|
2754
|
+
has256: level >= 2,
|
|
2755
|
+
has16m: level >= 3
|
|
2756
|
+
};
|
|
2757
|
+
}
|
|
2758
|
+
function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
|
|
2759
|
+
const noFlagForceColor = envForceColor();
|
|
2760
|
+
if (noFlagForceColor !== void 0) {
|
|
2761
|
+
flagForceColor = noFlagForceColor;
|
|
2762
|
+
}
|
|
2763
|
+
const forceColor = sniffFlags ? flagForceColor : noFlagForceColor;
|
|
2764
|
+
if (forceColor === 0) {
|
|
2765
|
+
return 0;
|
|
2766
|
+
}
|
|
2767
|
+
if (sniffFlags) {
|
|
2768
|
+
if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) {
|
|
2769
|
+
return 3;
|
|
2770
|
+
}
|
|
2771
|
+
if (hasFlag("color=256")) {
|
|
2772
|
+
return 2;
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
if ("TF_BUILD" in env && "AGENT_NAME" in env) {
|
|
2776
|
+
return 1;
|
|
2777
|
+
}
|
|
2778
|
+
if (haveStream && !streamIsTTY && forceColor === void 0) {
|
|
2779
|
+
return 0;
|
|
2780
|
+
}
|
|
2781
|
+
const min = forceColor || 0;
|
|
2782
|
+
if (env.TERM === "dumb") {
|
|
2783
|
+
return min;
|
|
2784
|
+
}
|
|
2785
|
+
if (process2.platform === "win32") {
|
|
2786
|
+
const osRelease = os.release().split(".");
|
|
2787
|
+
if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
|
|
2788
|
+
return Number(osRelease[2]) >= 14931 ? 3 : 2;
|
|
2789
|
+
}
|
|
2790
|
+
return 1;
|
|
2791
|
+
}
|
|
2792
|
+
if ("CI" in env) {
|
|
2793
|
+
if (["GITHUB_ACTIONS", "GITEA_ACTIONS", "CIRCLECI"].some((key) => key in env)) {
|
|
2794
|
+
return 3;
|
|
2795
|
+
}
|
|
2796
|
+
if (["TRAVIS", "APPVEYOR", "GITLAB_CI", "BUILDKITE", "DRONE"].some((sign) => sign in env) || env.CI_NAME === "codeship") {
|
|
2797
|
+
return 1;
|
|
2798
|
+
}
|
|
2799
|
+
return min;
|
|
2800
|
+
}
|
|
2801
|
+
if ("TEAMCITY_VERSION" in env) {
|
|
2802
|
+
return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0;
|
|
2803
|
+
}
|
|
2804
|
+
if (env.COLORTERM === "truecolor") {
|
|
2805
|
+
return 3;
|
|
2806
|
+
}
|
|
2807
|
+
if (env.TERM === "xterm-kitty") {
|
|
2808
|
+
return 3;
|
|
2809
|
+
}
|
|
2810
|
+
if (env.TERM === "xterm-ghostty") {
|
|
2811
|
+
return 3;
|
|
2812
|
+
}
|
|
2813
|
+
if (env.TERM === "wezterm") {
|
|
2814
|
+
return 3;
|
|
2815
|
+
}
|
|
2816
|
+
if ("TERM_PROGRAM" in env) {
|
|
2817
|
+
const version = Number.parseInt((env.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
|
|
2818
|
+
switch (env.TERM_PROGRAM) {
|
|
2819
|
+
case "iTerm.app": {
|
|
2820
|
+
return version >= 3 ? 3 : 2;
|
|
2821
|
+
}
|
|
2822
|
+
case "Apple_Terminal": {
|
|
2823
|
+
return 2;
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
if (/-256(color)?$/i.test(env.TERM)) {
|
|
2828
|
+
return 2;
|
|
2829
|
+
}
|
|
2830
|
+
if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) {
|
|
2831
|
+
return 1;
|
|
2832
|
+
}
|
|
2833
|
+
if ("COLORTERM" in env) {
|
|
2834
|
+
return 1;
|
|
2835
|
+
}
|
|
2836
|
+
return min;
|
|
2837
|
+
}
|
|
2838
|
+
function createSupportsColor(stream, options = {}) {
|
|
2839
|
+
const level = _supportsColor(stream, {
|
|
2840
|
+
streamIsTTY: stream && stream.isTTY,
|
|
2841
|
+
...options
|
|
2842
|
+
});
|
|
2843
|
+
return translateLevel(level);
|
|
2844
|
+
}
|
|
2845
|
+
var supportsColor = {
|
|
2846
|
+
stdout: createSupportsColor({ isTTY: tty.isatty(1) }),
|
|
2847
|
+
stderr: createSupportsColor({ isTTY: tty.isatty(2) })
|
|
2848
|
+
};
|
|
2849
|
+
var supports_color_default = supportsColor;
|
|
2850
|
+
|
|
2851
|
+
// node_modules/chalk/source/utilities.js
|
|
2852
|
+
function stringReplaceAll(string, substring, replacer) {
|
|
2853
|
+
let index = string.indexOf(substring);
|
|
2854
|
+
if (index === -1) {
|
|
2855
|
+
return string;
|
|
2856
|
+
}
|
|
2857
|
+
const substringLength = substring.length;
|
|
2858
|
+
let endIndex = 0;
|
|
2859
|
+
let returnValue = "";
|
|
2860
|
+
do {
|
|
2861
|
+
returnValue += string.slice(endIndex, index) + substring + replacer;
|
|
2862
|
+
endIndex = index + substringLength;
|
|
2863
|
+
index = string.indexOf(substring, endIndex);
|
|
2864
|
+
} while (index !== -1);
|
|
2865
|
+
returnValue += string.slice(endIndex);
|
|
2866
|
+
return returnValue;
|
|
2867
|
+
}
|
|
2868
|
+
function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
|
|
2869
|
+
let endIndex = 0;
|
|
2870
|
+
let returnValue = "";
|
|
2871
|
+
do {
|
|
2872
|
+
const gotCR = string[index - 1] === "\r";
|
|
2873
|
+
returnValue += string.slice(endIndex, gotCR ? index - 1 : index) + prefix + (gotCR ? "\r\n" : "\n") + postfix;
|
|
2874
|
+
endIndex = index + 1;
|
|
2875
|
+
index = string.indexOf("\n", endIndex);
|
|
2876
|
+
} while (index !== -1);
|
|
2877
|
+
returnValue += string.slice(endIndex);
|
|
2878
|
+
return returnValue;
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
// node_modules/chalk/source/index.js
|
|
2882
|
+
var { stdout: stdoutColor, stderr: stderrColor } = supports_color_default;
|
|
2883
|
+
var GENERATOR = /* @__PURE__ */ Symbol("GENERATOR");
|
|
2884
|
+
var STYLER = /* @__PURE__ */ Symbol("STYLER");
|
|
2885
|
+
var IS_EMPTY = /* @__PURE__ */ Symbol("IS_EMPTY");
|
|
2886
|
+
var levelMapping = [
|
|
2887
|
+
"ansi",
|
|
2888
|
+
"ansi",
|
|
2889
|
+
"ansi256",
|
|
2890
|
+
"ansi16m"
|
|
2891
|
+
];
|
|
2892
|
+
var styles2 = /* @__PURE__ */ Object.create(null);
|
|
2893
|
+
var applyOptions = (object, options = {}) => {
|
|
2894
|
+
if (options.level && !(Number.isInteger(options.level) && options.level >= 0 && options.level <= 3)) {
|
|
2895
|
+
throw new Error("The `level` option should be an integer from 0 to 3");
|
|
2896
|
+
}
|
|
2897
|
+
const colorLevel = stdoutColor ? stdoutColor.level : 0;
|
|
2898
|
+
object.level = options.level === void 0 ? colorLevel : options.level;
|
|
2899
|
+
};
|
|
2900
|
+
var chalkFactory = (options) => {
|
|
2901
|
+
const chalk2 = (...strings) => strings.join(" ");
|
|
2902
|
+
applyOptions(chalk2, options);
|
|
2903
|
+
Object.setPrototypeOf(chalk2, createChalk.prototype);
|
|
2904
|
+
return chalk2;
|
|
2905
|
+
};
|
|
2906
|
+
function createChalk(options) {
|
|
2907
|
+
return chalkFactory(options);
|
|
2908
|
+
}
|
|
2909
|
+
Object.setPrototypeOf(createChalk.prototype, Function.prototype);
|
|
2910
|
+
for (const [styleName, style] of Object.entries(ansi_styles_default)) {
|
|
2911
|
+
styles2[styleName] = {
|
|
2912
|
+
get() {
|
|
2913
|
+
const builder = createBuilder(this, createStyler(style.open, style.close, this[STYLER]), this[IS_EMPTY]);
|
|
2914
|
+
Object.defineProperty(this, styleName, { value: builder });
|
|
2915
|
+
return builder;
|
|
2916
|
+
}
|
|
2917
|
+
};
|
|
2918
|
+
}
|
|
2919
|
+
styles2.visible = {
|
|
2920
|
+
get() {
|
|
2921
|
+
const builder = createBuilder(this, this[STYLER], true);
|
|
2922
|
+
Object.defineProperty(this, "visible", { value: builder });
|
|
2923
|
+
return builder;
|
|
2924
|
+
}
|
|
2925
|
+
};
|
|
2926
|
+
var getModelAnsi = (model, level, type, ...arguments_) => {
|
|
2927
|
+
if (model === "rgb") {
|
|
2928
|
+
if (level === "ansi16m") {
|
|
2929
|
+
return ansi_styles_default[type].ansi16m(...arguments_);
|
|
2930
|
+
}
|
|
2931
|
+
if (level === "ansi256") {
|
|
2932
|
+
return ansi_styles_default[type].ansi256(ansi_styles_default.rgbToAnsi256(...arguments_));
|
|
2933
|
+
}
|
|
2934
|
+
return ansi_styles_default[type].ansi(ansi_styles_default.rgbToAnsi(...arguments_));
|
|
2935
|
+
}
|
|
2936
|
+
if (model === "hex") {
|
|
2937
|
+
return getModelAnsi("rgb", level, type, ...ansi_styles_default.hexToRgb(...arguments_));
|
|
2938
|
+
}
|
|
2939
|
+
return ansi_styles_default[type][model](...arguments_);
|
|
2940
|
+
};
|
|
2941
|
+
var usedModels = ["rgb", "hex", "ansi256"];
|
|
2942
|
+
for (const model of usedModels) {
|
|
2943
|
+
styles2[model] = {
|
|
2944
|
+
get() {
|
|
2945
|
+
const { level } = this;
|
|
2946
|
+
return function(...arguments_) {
|
|
2947
|
+
const styler = createStyler(getModelAnsi(model, levelMapping[level], "color", ...arguments_), ansi_styles_default.color.close, this[STYLER]);
|
|
2948
|
+
return createBuilder(this, styler, this[IS_EMPTY]);
|
|
2949
|
+
};
|
|
2950
|
+
}
|
|
2951
|
+
};
|
|
2952
|
+
const bgModel = "bg" + model[0].toUpperCase() + model.slice(1);
|
|
2953
|
+
styles2[bgModel] = {
|
|
2954
|
+
get() {
|
|
2955
|
+
const { level } = this;
|
|
2956
|
+
return function(...arguments_) {
|
|
2957
|
+
const styler = createStyler(getModelAnsi(model, levelMapping[level], "bgColor", ...arguments_), ansi_styles_default.bgColor.close, this[STYLER]);
|
|
2958
|
+
return createBuilder(this, styler, this[IS_EMPTY]);
|
|
2959
|
+
};
|
|
2960
|
+
}
|
|
2961
|
+
};
|
|
2962
|
+
}
|
|
2963
|
+
var proto = Object.defineProperties(() => {
|
|
2964
|
+
}, {
|
|
2965
|
+
...styles2,
|
|
2966
|
+
level: {
|
|
2967
|
+
enumerable: true,
|
|
2968
|
+
get() {
|
|
2969
|
+
return this[GENERATOR].level;
|
|
2970
|
+
},
|
|
2971
|
+
set(level) {
|
|
2972
|
+
this[GENERATOR].level = level;
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
});
|
|
2976
|
+
var createStyler = (open, close, parent) => {
|
|
2977
|
+
let openAll;
|
|
2978
|
+
let closeAll;
|
|
2979
|
+
if (parent === void 0) {
|
|
2980
|
+
openAll = open;
|
|
2981
|
+
closeAll = close;
|
|
2982
|
+
} else {
|
|
2983
|
+
openAll = parent.openAll + open;
|
|
2984
|
+
closeAll = close + parent.closeAll;
|
|
2985
|
+
}
|
|
2986
|
+
return {
|
|
2987
|
+
open,
|
|
2988
|
+
close,
|
|
2989
|
+
openAll,
|
|
2990
|
+
closeAll,
|
|
2991
|
+
parent
|
|
2992
|
+
};
|
|
2993
|
+
};
|
|
2994
|
+
var createBuilder = (self, _styler, _isEmpty) => {
|
|
2995
|
+
const builder = (...arguments_) => applyStyle(builder, arguments_.length === 1 ? "" + arguments_[0] : arguments_.join(" "));
|
|
2996
|
+
Object.setPrototypeOf(builder, proto);
|
|
2997
|
+
builder[GENERATOR] = self;
|
|
2998
|
+
builder[STYLER] = _styler;
|
|
2999
|
+
builder[IS_EMPTY] = _isEmpty;
|
|
3000
|
+
return builder;
|
|
3001
|
+
};
|
|
3002
|
+
var applyStyle = (self, string) => {
|
|
3003
|
+
if (self.level <= 0 || !string) {
|
|
3004
|
+
return self[IS_EMPTY] ? "" : string;
|
|
3005
|
+
}
|
|
3006
|
+
let styler = self[STYLER];
|
|
3007
|
+
if (styler === void 0) {
|
|
3008
|
+
return string;
|
|
3009
|
+
}
|
|
3010
|
+
const { openAll, closeAll } = styler;
|
|
3011
|
+
if (string.includes("\x1B")) {
|
|
3012
|
+
while (styler !== void 0) {
|
|
3013
|
+
string = stringReplaceAll(string, styler.close, styler.open);
|
|
3014
|
+
styler = styler.parent;
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
const lfIndex = string.indexOf("\n");
|
|
3018
|
+
if (lfIndex !== -1) {
|
|
3019
|
+
string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex);
|
|
3020
|
+
}
|
|
3021
|
+
return openAll + string + closeAll;
|
|
3022
|
+
};
|
|
3023
|
+
Object.defineProperties(createChalk.prototype, styles2);
|
|
3024
|
+
var chalk = createChalk();
|
|
3025
|
+
var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
|
|
3026
|
+
var source_default = chalk;
|
|
3027
|
+
|
|
3028
|
+
// src/preprocess/render/markdown.ts
|
|
3029
|
+
import { marked } from "marked";
|
|
3030
|
+
import { markedTerminal } from "marked-terminal";
|
|
3031
|
+
if (source_default.level === 0) source_default.level = 3;
|
|
3032
|
+
|
|
3033
|
+
// src/preprocess/result/ctx.ts
|
|
3034
|
+
function createResultCtx(init) {
|
|
3035
|
+
return {
|
|
3036
|
+
toolName: init.toolName,
|
|
3037
|
+
toolUseId: init.toolUseId,
|
|
3038
|
+
args: init.args,
|
|
3039
|
+
raw: init.raw,
|
|
3040
|
+
content: init.raw.content,
|
|
3041
|
+
summary: init.raw.summary,
|
|
3042
|
+
diff: init.raw.diff,
|
|
3043
|
+
warnings: [],
|
|
3044
|
+
settings: init.settings ?? {}
|
|
3045
|
+
};
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
// src/loop/agent.ts
|
|
3049
|
+
function combineSignals(...signals) {
|
|
3050
|
+
const live = signals.filter((s) => !!s);
|
|
3051
|
+
if (live.length === 0) return void 0;
|
|
3052
|
+
if (live.length === 1) return live[0];
|
|
3053
|
+
const combined = new AbortController();
|
|
3054
|
+
for (const s of live) {
|
|
3055
|
+
if (s.aborted) {
|
|
3056
|
+
combined.abort(s.reason);
|
|
3057
|
+
break;
|
|
3058
|
+
}
|
|
3059
|
+
s.addEventListener("abort", () => combined.abort(s.reason), { once: true });
|
|
3060
|
+
}
|
|
3061
|
+
return combined.signal;
|
|
3062
|
+
}
|
|
3063
|
+
function isAbortError(err) {
|
|
3064
|
+
if (!err) return false;
|
|
3065
|
+
if (err instanceof Error) {
|
|
3066
|
+
if (err.name === "AbortError") return true;
|
|
3067
|
+
const code = err.code;
|
|
3068
|
+
if (code === "ABORT_ERR" || code === "ECANCELED") return true;
|
|
3069
|
+
const msg = err.message.toLowerCase();
|
|
3070
|
+
if (msg.includes("aborted") || msg.includes("cancelled") || msg.includes("canceled")) return true;
|
|
3071
|
+
}
|
|
3072
|
+
return false;
|
|
3073
|
+
}
|
|
3074
|
+
var Agent = class {
|
|
3075
|
+
constructor(ctx) {
|
|
3076
|
+
this.ctx = ctx;
|
|
3077
|
+
this.todos = ctx.todos ?? new TodoStore();
|
|
3078
|
+
this.projectRoot = findProjectRoot(ctx.cwd);
|
|
3079
|
+
}
|
|
3080
|
+
ctx;
|
|
3081
|
+
messages = [];
|
|
3082
|
+
todos;
|
|
3083
|
+
/** 本轮 stream 发出时的 input tokens 估算值;finish 真实 usage 回来时减去,补差量给 UI。
|
|
3084
|
+
* 为什么:OpenAI 兼容 stream 的 usage 只在 finish 下发,期间 StatusLine 拿不到 token 数;
|
|
3085
|
+
* 先用 chars/4 估算并即刻推一次 onUsage,流式中就能显示 "↑ N tokens",真实值到达时无缝覆盖。 */
|
|
3086
|
+
lastEstimateInputTokens = 0;
|
|
3087
|
+
/** 当前轮次的 abort signal(runTurn 开始时设,结束清);runToolCall 读它给 execa 等用。 */
|
|
3088
|
+
turnAbortSignal;
|
|
3089
|
+
/**
|
|
3090
|
+
* "引导(guidance)"队列:模型还在跑时用户继续输入的内容暂存这里。
|
|
3091
|
+
* 主循环每轮 stream 启动前 check + flush;合并成一条 role:user 注入 messages,
|
|
3092
|
+
* 让 LLM 在下一轮看到"已跑完的 tool result + 用户新输入"一起决策。
|
|
3093
|
+
*
|
|
3094
|
+
* 注入点:工具批跑完、下一轮 LLM stream 之前。**不打断**正在跑的工具,避免模型
|
|
3095
|
+
* 因丢失中间结果而重跑(用户原话:"不应该直接终止某一个小的命令或者工具执行")。
|
|
3096
|
+
*
|
|
3097
|
+
* Esc 单击时若队列非空,优先清队列(轻撤销),还有空也不 abort turn。
|
|
3098
|
+
*/
|
|
3099
|
+
pendingGuidance = [];
|
|
3100
|
+
/**
|
|
3101
|
+
* II-1.3 子目录惰性加载状态:已注入过子目录 MUSE.md / AGENTS.md 的绝对路径集合。
|
|
3102
|
+
*
|
|
3103
|
+
* tool 操作触及未加载过的子目录(包含 MUSE.md 或 AGENTS.md)时,把内容附加到 tool result
|
|
3104
|
+
* prefix,让 LLM 看到该子目录的 guidance。同一子目录只注入一次(去重)。
|
|
3105
|
+
*
|
|
3106
|
+
* /clear 调 clearLoadedSubdirs() 重置;/resume 不重置(假设旧 session 已加载过)。
|
|
3107
|
+
*/
|
|
3108
|
+
loadedSubdirs = /* @__PURE__ */ new Set();
|
|
3109
|
+
projectRoot;
|
|
3110
|
+
/** 给 /clear 等场景重置子目录加载状态。 */
|
|
3111
|
+
clearLoadedSubdirs() {
|
|
3112
|
+
this.loadedSubdirs.clear();
|
|
3113
|
+
}
|
|
3114
|
+
/** 估算 input tokens:走 src/preprocess/tokenize.ts(js-tiktoken cl100k_base)。
|
|
3115
|
+
* 比旧 chars/4 精度高,trim-history / budget-guard 也共享同一个估算口径。 */
|
|
3116
|
+
estimateInputTokens(messages, systemPrompt, tools) {
|
|
3117
|
+
return countMessages(messages, systemPrompt, tools);
|
|
3118
|
+
}
|
|
3119
|
+
getMessages() {
|
|
3120
|
+
return this.messages;
|
|
3121
|
+
}
|
|
3122
|
+
setMessages(msgs) {
|
|
3123
|
+
this.messages = msgs;
|
|
3124
|
+
}
|
|
3125
|
+
/**
|
|
3126
|
+
* 把一段"引导"内容塞进队列。下一轮 stream 启动前会被合并成一条 role:user 注入 messages。
|
|
3127
|
+
* 调用方:App 的 handleSubmit 在 state.status !== "idle" 时走这条路径。
|
|
3128
|
+
*/
|
|
3129
|
+
enqueueGuidance(content) {
|
|
3130
|
+
const parts = typeof content === "string" ? [{ type: "text", text: content }] : content;
|
|
3131
|
+
this.pendingGuidance.push(parts);
|
|
3132
|
+
}
|
|
3133
|
+
/** 用户单击 Esc 时若队列非空,清掉(轻撤销;不 abort 当前 turn)。 */
|
|
3134
|
+
clearGuidance() {
|
|
3135
|
+
this.pendingGuidance.length = 0;
|
|
3136
|
+
}
|
|
3137
|
+
getPendingGuidanceCount() {
|
|
3138
|
+
return this.pendingGuidance.length;
|
|
3139
|
+
}
|
|
3140
|
+
/**
|
|
3141
|
+
* 把当前 pendingGuidance 合并成一条 role:user message push 进 messages,清空队列。
|
|
3142
|
+
* 多条 guidance 之间用空行分隔(模型把它们读成"一次性补充的几点")。
|
|
3143
|
+
* 返回 true 表示真的注入了内容;false 表示队列本就空。
|
|
3144
|
+
*/
|
|
3145
|
+
flushGuidance() {
|
|
3146
|
+
if (this.pendingGuidance.length === 0) return false;
|
|
3147
|
+
const queued = this.pendingGuidance;
|
|
3148
|
+
this.pendingGuidance = [];
|
|
3149
|
+
const merged = [];
|
|
3150
|
+
const SEP = "\n\n---\n\n";
|
|
3151
|
+
for (let i = 0; i < queued.length; i++) {
|
|
3152
|
+
const parts = queued[i];
|
|
3153
|
+
if (i > 0) {
|
|
3154
|
+
const tail = merged[merged.length - 1];
|
|
3155
|
+
if (tail && tail.type === "text") tail.text += SEP;
|
|
3156
|
+
else merged.push({ type: "text", text: SEP });
|
|
3157
|
+
}
|
|
3158
|
+
for (const p of parts) {
|
|
3159
|
+
const tail = merged[merged.length - 1];
|
|
3160
|
+
if (p.type === "text" && tail && tail.type === "text") {
|
|
3161
|
+
tail.text += p.text;
|
|
3162
|
+
} else {
|
|
3163
|
+
merged.push({ ...p });
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
const guidanceMsg = { role: "user", content: merged };
|
|
3168
|
+
this.messages.push(guidanceMsg);
|
|
3169
|
+
this.ctx.session.append({ type: "message", time: (/* @__PURE__ */ new Date()).toISOString(), message: guidanceMsg });
|
|
3170
|
+
this.ctx.events?.onGuidanceInjected?.(merged);
|
|
3171
|
+
return true;
|
|
3172
|
+
}
|
|
3173
|
+
/**
|
|
3174
|
+
* 执行一次完整的"用户输入 → 助手响应(含工具循环) → 等待下一轮输入"。
|
|
3175
|
+
*
|
|
3176
|
+
* userInput 可以是:
|
|
3177
|
+
* - string:纯文本(向后兼容,无附件场景)
|
|
3178
|
+
* - ContentPart[]:多 part 内容(text + file/image 附件)
|
|
3179
|
+
*
|
|
3180
|
+
* abortSignal:本轮专属信号(每轮新建,App.handleSubmit 传入);Esc 触发 abort
|
|
3181
|
+
* 时立刻打断 LLM stream + execa 工具 + 等待循环。优先级高于 ctx.abortSignal
|
|
3182
|
+
* (后者是进程级 / 启动时签名,用于全局退出)。
|
|
3183
|
+
*/
|
|
3184
|
+
async runTurn(userInput, abortSignal) {
|
|
3185
|
+
const turnSignal = combineSignals(abortSignal, this.ctx.abortSignal);
|
|
3186
|
+
this.turnAbortSignal = turnSignal;
|
|
3187
|
+
const userMessage = { role: "user", content: userInput };
|
|
3188
|
+
this.messages.push(userMessage);
|
|
3189
|
+
await this.ctx.session.append({ type: "message", time: (/* @__PURE__ */ new Date()).toISOString(), message: userMessage });
|
|
3190
|
+
while (true) {
|
|
3191
|
+
this.flushGuidance();
|
|
3192
|
+
let systemPrompt;
|
|
3193
|
+
let tools;
|
|
3194
|
+
let messagesForStream;
|
|
3195
|
+
try {
|
|
3196
|
+
const built = await this.buildRequest();
|
|
3197
|
+
systemPrompt = built.systemPrompt;
|
|
3198
|
+
tools = built.tools;
|
|
3199
|
+
messagesForStream = built.messages;
|
|
3200
|
+
} catch (err) {
|
|
3201
|
+
if (err instanceof BudgetExceededError) {
|
|
3202
|
+
this.ctx.events?.onError?.(err);
|
|
3203
|
+
log.error("budget exceeded", { msg: err.message });
|
|
3204
|
+
this.turnAbortSignal = void 0;
|
|
3205
|
+
return;
|
|
3206
|
+
}
|
|
3207
|
+
throw err;
|
|
3208
|
+
}
|
|
3209
|
+
try {
|
|
3210
|
+
const hookOut = await runHooks(
|
|
3211
|
+
"PreLLMRequest",
|
|
3212
|
+
{ messages: this.messages, systemPrompt, tools, modelId: this.ctx.llm.model },
|
|
3213
|
+
this.ctx.hooks,
|
|
3214
|
+
this.ctx.hookLogger
|
|
3215
|
+
);
|
|
3216
|
+
if (typeof hookOut.systemPrompt === "string") systemPrompt = hookOut.systemPrompt;
|
|
3217
|
+
if (Array.isArray(hookOut.messages)) messagesForStream = hookOut.messages;
|
|
3218
|
+
if (Array.isArray(hookOut.tools)) tools = hookOut.tools;
|
|
3219
|
+
} catch (err) {
|
|
3220
|
+
if (err instanceof PipelineBlockedError) {
|
|
3221
|
+
this.ctx.events?.onBlocked?.(err.reason);
|
|
3222
|
+
this.ctx.events?.onError?.(new Error(`PreLLMRequest blocked: ${err.reason}`));
|
|
3223
|
+
return;
|
|
3224
|
+
}
|
|
3225
|
+
throw err;
|
|
3226
|
+
}
|
|
3227
|
+
const estimate = this.estimateInputTokens(messagesForStream, systemPrompt, tools);
|
|
3228
|
+
this.lastEstimateInputTokens = estimate;
|
|
3229
|
+
this.ctx.events?.onEstimate?.(estimate);
|
|
3230
|
+
const stream = this.ctx.llm.stream({
|
|
3231
|
+
messages: messagesForStream,
|
|
3232
|
+
tools,
|
|
3233
|
+
systemPrompt,
|
|
3234
|
+
abortSignal: turnSignal
|
|
3235
|
+
});
|
|
3236
|
+
const assistantParts = [];
|
|
3237
|
+
const toolCallsToRun = [];
|
|
3238
|
+
let lastError;
|
|
3239
|
+
try {
|
|
3240
|
+
for await (const ev of stream) {
|
|
3241
|
+
this.handleEvent(ev, assistantParts, toolCallsToRun, (e) => {
|
|
3242
|
+
lastError = e;
|
|
3243
|
+
});
|
|
3244
|
+
if (lastError) break;
|
|
3245
|
+
if (turnSignal?.aborted) break;
|
|
3246
|
+
}
|
|
3247
|
+
} catch (err) {
|
|
3248
|
+
if (isAbortError(err) || turnSignal?.aborted) {
|
|
3249
|
+
this.persistInterruptedAssistant(assistantParts);
|
|
3250
|
+
this.ctx.events?.onTurnEnd?.();
|
|
3251
|
+
this.turnAbortSignal = void 0;
|
|
3252
|
+
return;
|
|
3253
|
+
}
|
|
3254
|
+
throw err;
|
|
3255
|
+
}
|
|
3256
|
+
if (lastError) {
|
|
3257
|
+
this.ctx.events?.onError?.(lastError);
|
|
3258
|
+
log.error("agent stream error", { msg: lastError.message });
|
|
3259
|
+
this.turnAbortSignal = void 0;
|
|
3260
|
+
return;
|
|
3261
|
+
}
|
|
3262
|
+
if (turnSignal?.aborted) {
|
|
3263
|
+
this.persistInterruptedAssistant(assistantParts);
|
|
3264
|
+
this.ctx.events?.onTurnEnd?.();
|
|
3265
|
+
this.turnAbortSignal = void 0;
|
|
3266
|
+
return;
|
|
3267
|
+
}
|
|
3268
|
+
const assistantMessage = { role: "assistant", content: assistantParts };
|
|
3269
|
+
this.messages.push(assistantMessage);
|
|
3270
|
+
await this.ctx.session.append({ type: "message", time: (/* @__PURE__ */ new Date()).toISOString(), message: assistantMessage });
|
|
3271
|
+
const assistantText = assistantParts.filter((p) => p.type === "text").map((p) => p.text).join("");
|
|
3272
|
+
const toolCalls = toolCallsToRun.map((t) => ({ id: t.id, name: t.name, args: t.args }));
|
|
3273
|
+
try {
|
|
3274
|
+
await runHooks(
|
|
3275
|
+
"PostLLMResponse",
|
|
3276
|
+
{ assistantText, toolCalls },
|
|
3277
|
+
this.ctx.hooks,
|
|
3278
|
+
this.ctx.hookLogger
|
|
3279
|
+
);
|
|
3280
|
+
} catch (err) {
|
|
3281
|
+
if (err instanceof PipelineBlockedError) {
|
|
3282
|
+
log.warn("PostLLMResponse tried to block, ignored", { reason: err.reason });
|
|
3283
|
+
} else {
|
|
3284
|
+
throw err;
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
if (toolCallsToRun.length === 0) {
|
|
3288
|
+
if (this.pendingGuidance.length > 0) continue;
|
|
3289
|
+
this.ctx.events?.onTurnEnd?.();
|
|
3290
|
+
this.turnAbortSignal = void 0;
|
|
3291
|
+
return;
|
|
3292
|
+
}
|
|
3293
|
+
this.ctx.events?.onAssistantTurn?.();
|
|
3294
|
+
for (const call of toolCallsToRun) {
|
|
3295
|
+
if (turnSignal?.aborted) {
|
|
3296
|
+
this.recordToolResult(call.id, call.name, `Interrupted by user (Esc).`, true);
|
|
3297
|
+
} else {
|
|
3298
|
+
await this.runToolCall(call);
|
|
3299
|
+
await new Promise((resolve8) => setImmediate(resolve8));
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
if (turnSignal?.aborted) {
|
|
3303
|
+
this.ctx.events?.onTurnEnd?.();
|
|
3304
|
+
this.turnAbortSignal = void 0;
|
|
3305
|
+
return;
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
/** 拼装本轮 system prompt + tools(+ 可能 trim 后的 messages)。
|
|
3310
|
+
* 优先走 RequestPipeline,无 pipeline 时回退到 legacy 路径。
|
|
3311
|
+
*
|
|
3312
|
+
* 注意:trim-history 可能替换 ctx.messages,这里把它回流出去给主循环作为
|
|
3313
|
+
* messagesForStream。**不污染** this.messages — 因为 trim 是临时为本次请求
|
|
3314
|
+
* 做的窗口缩减,真正的历史不动(budget-guard 触发的 compact 例外:那是
|
|
3315
|
+
* 通过 services.compact 真实写回 this.messages 的)。 */
|
|
3316
|
+
async buildRequest() {
|
|
3317
|
+
if (this.ctx.requestPipeline && this.ctx.requestServices) {
|
|
3318
|
+
const ctx = createRequestCtx({
|
|
3319
|
+
messages: this.messages,
|
|
3320
|
+
modelId: this.ctx.llm.model,
|
|
3321
|
+
mode: this.ctx.permissions.getMode(),
|
|
3322
|
+
cwd: this.ctx.cwd,
|
|
3323
|
+
services: {
|
|
3324
|
+
...this.ctx.requestServices,
|
|
3325
|
+
todos: this.todos,
|
|
3326
|
+
contextWindow: this.ctx.llm.capabilities.maxContextWindow,
|
|
3327
|
+
abortSignal: this.turnAbortSignal,
|
|
3328
|
+
// compact 闭包:budget-guard 触发时调用。真实改写 this.messages
|
|
3329
|
+
// (compact 是历史压缩,不可逆,要落到 agent state)。
|
|
3330
|
+
compact: async (signal) => {
|
|
3331
|
+
const result = await compactMessages(this.messages, {
|
|
3332
|
+
llm: this.ctx.llm,
|
|
3333
|
+
abortSignal: signal,
|
|
3334
|
+
hooks: this.ctx.hooks,
|
|
3335
|
+
cwd: this.ctx.cwd
|
|
3336
|
+
});
|
|
3337
|
+
this.messages = result.newMessages;
|
|
3338
|
+
if (result.promotedFacts && this.ctx.requestServices?.memoryEmbeddingIndex) {
|
|
3339
|
+
for (const f of result.promotedFacts) {
|
|
3340
|
+
if (f.status !== "saved") continue;
|
|
3341
|
+
try {
|
|
3342
|
+
await upsertMemoryEntry(this.ctx.requestServices.memoryEmbeddingIndex, f.name, "project");
|
|
3343
|
+
} catch (err) {
|
|
3344
|
+
log.warn("memory index upsert (compact-promote) failed", { name: f.name, msg: err.message });
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
return this.messages;
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
});
|
|
3352
|
+
await this.ctx.requestPipeline.run(ctx);
|
|
3353
|
+
return { systemPrompt: ctx.systemPrompt, tools: ctx.tools, messages: ctx.messages };
|
|
3354
|
+
}
|
|
3355
|
+
const mode = this.ctx.permissions.getMode();
|
|
3356
|
+
const tools = this.ctx.tools.toLLMDefinitions(
|
|
3357
|
+
mode === "plan" ? (t) => t.permission === "read" : void 0
|
|
3358
|
+
);
|
|
3359
|
+
const todoSection = this.todos.toPromptSection();
|
|
3360
|
+
const base = this.ctx.systemPrompt ?? "";
|
|
3361
|
+
const systemPrompt = todoSection ? `${base}
|
|
3362
|
+
|
|
3363
|
+
${todoSection}` : base;
|
|
3364
|
+
return { systemPrompt, tools, messages: this.messages };
|
|
3365
|
+
}
|
|
3366
|
+
handleEvent(ev, assistantParts, toolCallsToRun, onError) {
|
|
3367
|
+
switch (ev.type) {
|
|
3368
|
+
case "text":
|
|
3369
|
+
{
|
|
3370
|
+
const last = assistantParts[assistantParts.length - 1];
|
|
3371
|
+
if (last && last.type === "text") {
|
|
3372
|
+
last.text += ev.delta;
|
|
3373
|
+
} else {
|
|
3374
|
+
assistantParts.push({ type: "text", text: ev.delta });
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
this.ctx.events?.onText?.(ev.delta);
|
|
3378
|
+
break;
|
|
3379
|
+
case "tool_call_start":
|
|
3380
|
+
this.ctx.events?.onToolCallStart?.(ev.id, ev.name);
|
|
3381
|
+
break;
|
|
3382
|
+
case "tool_call_complete": {
|
|
3383
|
+
const callPart = { type: "tool_use", id: ev.id, name: ev.name, args: ev.args };
|
|
3384
|
+
assistantParts.push(callPart);
|
|
3385
|
+
toolCallsToRun.push(callPart);
|
|
3386
|
+
this.ctx.events?.onToolCallArgs?.(ev.id, ev.args);
|
|
3387
|
+
break;
|
|
3388
|
+
}
|
|
3389
|
+
case "finish":
|
|
3390
|
+
if (ev.usage) {
|
|
3391
|
+
const adjusted = {
|
|
3392
|
+
inputTokens: ev.usage.inputTokens - this.lastEstimateInputTokens,
|
|
3393
|
+
outputTokens: ev.usage.outputTokens,
|
|
3394
|
+
totalTokens: ev.usage.totalTokens - this.lastEstimateInputTokens
|
|
3395
|
+
};
|
|
3396
|
+
this.ctx.events?.onUsage?.(adjusted);
|
|
3397
|
+
this.ctx.session.append({
|
|
3398
|
+
type: "usage",
|
|
3399
|
+
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3400
|
+
usage: ev.usage,
|
|
3401
|
+
// session 写真实值,不写 estimate
|
|
3402
|
+
provider: this.ctx.llm.providerName,
|
|
3403
|
+
model: this.ctx.llm.model
|
|
3404
|
+
});
|
|
3405
|
+
}
|
|
3406
|
+
this.lastEstimateInputTokens = 0;
|
|
3407
|
+
break;
|
|
3408
|
+
case "error":
|
|
3409
|
+
onError(ev.error);
|
|
3410
|
+
break;
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
/** Esc 中断时:把"已经流出来"的 assistant 内容存进 history,标 [interrupted] 后缀。 */
|
|
3414
|
+
persistInterruptedAssistant(parts) {
|
|
3415
|
+
const cleanedParts = parts.filter((p) => p.type !== "tool_use");
|
|
3416
|
+
const lastIdx = cleanedParts.length - 1;
|
|
3417
|
+
if (lastIdx >= 0 && cleanedParts[lastIdx].type === "text") {
|
|
3418
|
+
const t = cleanedParts[lastIdx];
|
|
3419
|
+
cleanedParts[lastIdx] = { type: "text", text: `${t.text}
|
|
3420
|
+
|
|
3421
|
+
[interrupted]` };
|
|
3422
|
+
} else {
|
|
3423
|
+
cleanedParts.push({ type: "text", text: "[interrupted]" });
|
|
3424
|
+
}
|
|
3425
|
+
const assistantMessage = { role: "assistant", content: cleanedParts };
|
|
3426
|
+
this.messages.push(assistantMessage);
|
|
3427
|
+
this.ctx.session.append({ type: "message", time: (/* @__PURE__ */ new Date()).toISOString(), message: assistantMessage });
|
|
3428
|
+
}
|
|
3429
|
+
async runToolCall(call) {
|
|
3430
|
+
const tool2 = this.ctx.tools.get(call.name);
|
|
3431
|
+
if (!tool2) {
|
|
3432
|
+
const result = `Tool "${call.name}" is not available.`;
|
|
3433
|
+
this.recordToolResult(call.id, call.name, result, true);
|
|
3434
|
+
return;
|
|
3435
|
+
}
|
|
3436
|
+
const summary = tool2.summarize?.(call.args) ?? `${call.name}(...)`;
|
|
3437
|
+
const decision = this.ctx.permissions.decide({
|
|
3438
|
+
toolName: call.name,
|
|
3439
|
+
args: call.args,
|
|
3440
|
+
permission: tool2.permission
|
|
3441
|
+
});
|
|
3442
|
+
if (decision === "deny") {
|
|
3443
|
+
const reason = this.ctx.permissions.getMode() === "plan" ? `Denied: you are in plan mode. Only read-only tools are available. Propose changes instead of executing.` : `Denied by policy: ${call.name}.`;
|
|
3444
|
+
this.recordToolResult(call.id, call.name, reason, true);
|
|
3445
|
+
return;
|
|
3446
|
+
}
|
|
3447
|
+
if (decision === "ask") {
|
|
3448
|
+
const userDecision = await this.ctx.events?.onPermissionRequest?.(call.name, call.args, summary) ?? "no";
|
|
3449
|
+
if (userDecision === "no") {
|
|
3450
|
+
this.recordToolResult(call.id, call.name, `User rejected ${call.name}.`, true);
|
|
3451
|
+
return;
|
|
3452
|
+
}
|
|
3453
|
+
if (userDecision === "session_allow") {
|
|
3454
|
+
this.ctx.permissions.allowForSession(call.name);
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
let effectiveArgs = call.args;
|
|
3458
|
+
try {
|
|
3459
|
+
const hookOut = await runHooks(
|
|
3460
|
+
"PreToolUse",
|
|
3461
|
+
{ toolName: call.name, args: effectiveArgs },
|
|
3462
|
+
this.ctx.hooks,
|
|
3463
|
+
this.ctx.hookLogger
|
|
3464
|
+
);
|
|
3465
|
+
if (hookOut.args !== void 0) effectiveArgs = hookOut.args;
|
|
3466
|
+
} catch (err) {
|
|
3467
|
+
if (err instanceof PipelineBlockedError) {
|
|
3468
|
+
this.ctx.events?.onBlocked?.(err.reason);
|
|
3469
|
+
this.recordToolResult(call.id, call.name, `Blocked by PreToolUse hook: ${err.reason}`, true);
|
|
3470
|
+
return;
|
|
3471
|
+
}
|
|
3472
|
+
throw err;
|
|
3473
|
+
}
|
|
3474
|
+
const toolCtx = {
|
|
3475
|
+
cwd: this.ctx.cwd,
|
|
3476
|
+
abortSignal: this.turnAbortSignal ?? this.ctx.abortSignal,
|
|
3477
|
+
askPermission: async () => true,
|
|
3478
|
+
// 已在外层处理
|
|
3479
|
+
todos: this.todos,
|
|
3480
|
+
askQuestions: this.ctx.events?.onAskQuestions ? (qs) => this.ctx.events.onAskQuestions(qs) : void 0
|
|
3481
|
+
};
|
|
3482
|
+
const raw = await this.ctx.tools.execute(call.name, effectiveArgs, toolCtx);
|
|
3483
|
+
const processed = await this.postProcessResult({
|
|
3484
|
+
toolName: call.name,
|
|
3485
|
+
toolUseId: call.id,
|
|
3486
|
+
args: effectiveArgs,
|
|
3487
|
+
raw
|
|
3488
|
+
});
|
|
3489
|
+
this.recordToolResult(
|
|
3490
|
+
call.id,
|
|
3491
|
+
call.name,
|
|
3492
|
+
processed.content,
|
|
3493
|
+
processed.isError,
|
|
3494
|
+
processed.summary,
|
|
3495
|
+
processed.diff,
|
|
3496
|
+
processed.kind
|
|
3497
|
+
);
|
|
3498
|
+
}
|
|
3499
|
+
async postProcessResult(input) {
|
|
3500
|
+
let content = input.raw.content;
|
|
3501
|
+
let summary = input.raw.summary;
|
|
3502
|
+
let diff = input.raw.diff;
|
|
3503
|
+
const isError = input.raw.isError ?? false;
|
|
3504
|
+
let kind = input.raw.kind;
|
|
3505
|
+
if (this.ctx.resultPipeline) {
|
|
3506
|
+
const rctx = createResultCtx({
|
|
3507
|
+
toolName: input.toolName,
|
|
3508
|
+
toolUseId: input.toolUseId,
|
|
3509
|
+
args: input.args,
|
|
3510
|
+
raw: input.raw,
|
|
3511
|
+
settings: this.ctx.resultSettings
|
|
3512
|
+
});
|
|
3513
|
+
try {
|
|
3514
|
+
await this.ctx.resultPipeline.run(rctx);
|
|
3515
|
+
content = rctx.content;
|
|
3516
|
+
summary = rctx.summary;
|
|
3517
|
+
diff = rctx.diff;
|
|
3518
|
+
} catch (err) {
|
|
3519
|
+
log.warn("result pipeline error", { msg: err.message });
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
try {
|
|
3523
|
+
const hookOut = await runHooks(
|
|
3524
|
+
"PostToolUse",
|
|
3525
|
+
{ toolName: input.toolName, args: input.args, content, summary, isError },
|
|
3526
|
+
this.ctx.hooks,
|
|
3527
|
+
this.ctx.hookLogger
|
|
3528
|
+
);
|
|
3529
|
+
if (typeof hookOut.content === "string") content = hookOut.content;
|
|
3530
|
+
if (typeof hookOut.summary === "string") summary = hookOut.summary;
|
|
3531
|
+
} catch (err) {
|
|
3532
|
+
if (err instanceof PipelineBlockedError) {
|
|
3533
|
+
log.warn("PostToolUse hook tried to block, ignored", { reason: err.reason });
|
|
3534
|
+
} else {
|
|
3535
|
+
throw err;
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
if (input.toolName === "MemoryWrite" && !isError) {
|
|
3539
|
+
const index = this.ctx.requestServices?.memoryEmbeddingIndex;
|
|
3540
|
+
if (index) {
|
|
3541
|
+
try {
|
|
3542
|
+
const args = input.args;
|
|
3543
|
+
const name = typeof args?.name === "string" ? args.name : void 0;
|
|
3544
|
+
const scope = args?.scope === "user" ? "user" : "project";
|
|
3545
|
+
if (name) {
|
|
3546
|
+
await upsertMemoryEntry(index, name, scope);
|
|
3547
|
+
}
|
|
3548
|
+
} catch (err) {
|
|
3549
|
+
log.warn("memory index upsert failed", { msg: err.message });
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
3553
|
+
try {
|
|
3554
|
+
const subdirInjection = await this.maybeInjectSubdirHierarchy(input.toolName, input.args);
|
|
3555
|
+
if (subdirInjection) {
|
|
3556
|
+
content = `${subdirInjection}
|
|
3557
|
+
|
|
3558
|
+
---
|
|
3559
|
+
|
|
3560
|
+
[Original tool result]
|
|
3561
|
+
${content}`;
|
|
3562
|
+
}
|
|
3563
|
+
} catch (err) {
|
|
3564
|
+
log.warn("subdir hierarchy inject failed", { msg: err.message });
|
|
3565
|
+
}
|
|
3566
|
+
return { content, isError, summary, diff, kind };
|
|
3567
|
+
}
|
|
3568
|
+
/**
|
|
3569
|
+
* 检查工具操作的路径是否落入未加载过的子目录;子目录(向上找最近的)若含 MUSE.md / AGENTS.md
|
|
3570
|
+
* 就把内容封装成 system 注释返回(由 caller 前置到 tool result)。
|
|
3571
|
+
*/
|
|
3572
|
+
async maybeInjectSubdirHierarchy(toolName, args) {
|
|
3573
|
+
const path = extractToolPath(toolName, args);
|
|
3574
|
+
if (!path) return null;
|
|
3575
|
+
const absPath = isAbsolute4(path) ? path : resolve6(this.ctx.cwd, path);
|
|
3576
|
+
const subdir = findContainingSubdirWithHierarchy(absPath, this.projectRoot);
|
|
3577
|
+
if (!subdir) return null;
|
|
3578
|
+
if (this.loadedSubdirs.has(subdir)) return null;
|
|
3579
|
+
this.loadedSubdirs.add(subdir);
|
|
3580
|
+
const loaded = await loadSubdirMemory(subdir);
|
|
3581
|
+
if (!loaded) return null;
|
|
3582
|
+
const relPath = relative(this.projectRoot, subdir) || ".";
|
|
3583
|
+
const truncatedNote = loaded.truncated ? " (content truncated; use Read to view full file)" : "";
|
|
3584
|
+
return `[System: loaded ${loaded.source} from ${relPath}/${truncatedNote}]
|
|
3585
|
+
${loaded.content}`;
|
|
3586
|
+
}
|
|
3587
|
+
recordToolResult(id, name, content, isError, summary, diff, kind) {
|
|
3588
|
+
const toolMsg = {
|
|
3589
|
+
role: "tool",
|
|
3590
|
+
toolUseId: id,
|
|
3591
|
+
content,
|
|
3592
|
+
isError,
|
|
3593
|
+
toolName: name,
|
|
3594
|
+
...diff ? { diff } : {},
|
|
3595
|
+
...summary ? { summary } : {},
|
|
3596
|
+
...kind ? { kind } : {}
|
|
3597
|
+
};
|
|
3598
|
+
this.messages.push(toolMsg);
|
|
3599
|
+
this.ctx.session.append({ type: "message", time: (/* @__PURE__ */ new Date()).toISOString(), message: toolMsg });
|
|
3600
|
+
this.ctx.events?.onToolResult?.(id, name, content, isError, summary);
|
|
3601
|
+
}
|
|
3602
|
+
};
|
|
3603
|
+
function extractToolPath(toolName, args) {
|
|
3604
|
+
if (typeof args !== "object" || args === null) return null;
|
|
3605
|
+
if (toolName === "Read" || toolName === "Edit" || toolName === "Write") {
|
|
3606
|
+
const fp = args.file_path;
|
|
3607
|
+
if (typeof fp === "string") return fp;
|
|
3608
|
+
}
|
|
3609
|
+
if (toolName === "Grep" || toolName === "Glob") {
|
|
3610
|
+
const p = args.path;
|
|
3611
|
+
if (typeof p === "string") return p;
|
|
3612
|
+
}
|
|
3613
|
+
return null;
|
|
3614
|
+
}
|
|
3615
|
+
function findContainingSubdirWithHierarchy(absPath, projectRoot) {
|
|
3616
|
+
const rel = relative(projectRoot, absPath);
|
|
3617
|
+
if (!rel || rel.startsWith("..") || isAbsolute4(rel)) return null;
|
|
3618
|
+
let cur = dirname5(absPath);
|
|
3619
|
+
while (cur !== projectRoot && cur.startsWith(projectRoot)) {
|
|
3620
|
+
if (existsSync5(join6(cur, "MUSE.md")) || existsSync5(join6(cur, "AGENTS.md"))) {
|
|
3621
|
+
return cur;
|
|
3622
|
+
}
|
|
3623
|
+
const parent = dirname5(cur);
|
|
3624
|
+
if (parent === cur) break;
|
|
3625
|
+
cur = parent;
|
|
3626
|
+
}
|
|
3627
|
+
return null;
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
// src/config/loader.ts
|
|
3631
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
3632
|
+
import { existsSync as existsSync6 } from "fs";
|
|
3633
|
+
import { homedir as homedir7 } from "os";
|
|
3634
|
+
import { join as join7, resolve as resolve7 } from "path";
|
|
3635
|
+
|
|
3636
|
+
// src/config/types.ts
|
|
3637
|
+
import { z as z11 } from "zod";
|
|
3638
|
+
var ProviderConfigSchema = z11.object({
|
|
3639
|
+
apiKey: z11.string().optional(),
|
|
3640
|
+
baseUrl: z11.string().optional(),
|
|
3641
|
+
extraHeaders: z11.record(z11.string()).optional()
|
|
3642
|
+
}).passthrough();
|
|
3643
|
+
var LLMConfigSchema = z11.object({
|
|
3644
|
+
provider: z11.string().optional().describe("Fallback provider preset (only used when no models.local.json entry matches)."),
|
|
3645
|
+
model: z11.string().optional().describe("Active model id; should match an id in models.local.json."),
|
|
3646
|
+
temperature: z11.number().min(0).max(2).optional(),
|
|
3647
|
+
maxTokens: z11.number().int().positive().optional()
|
|
3648
|
+
});
|
|
3649
|
+
var PermissionsSchema = z11.object({
|
|
3650
|
+
allow: z11.array(z11.string()).optional(),
|
|
3651
|
+
ask: z11.array(z11.string()).optional(),
|
|
3652
|
+
deny: z11.array(z11.string()).optional(),
|
|
3653
|
+
defaultMode: z11.enum(["strict", "relaxed", "ask"]).optional()
|
|
3654
|
+
});
|
|
3655
|
+
var UISchema = z11.object({
|
|
3656
|
+
theme: z11.enum(["dark", "light"]).optional(),
|
|
3657
|
+
lang: z11.enum(["en", "zh-CN"]).optional(),
|
|
3658
|
+
showBanner: z11.boolean().optional()
|
|
3659
|
+
});
|
|
3660
|
+
var HookSpecSchema = z11.object({
|
|
3661
|
+
matcher: z11.string().optional(),
|
|
3662
|
+
command: z11.string(),
|
|
3663
|
+
timeout: z11.number().int().positive().optional(),
|
|
3664
|
+
onError: z11.enum(["skip", "throw"]).optional()
|
|
3665
|
+
});
|
|
3666
|
+
var HooksConfigSchema = z11.object({
|
|
3667
|
+
SessionStart: z11.array(HookSpecSchema).optional(),
|
|
3668
|
+
SessionEnd: z11.array(HookSpecSchema).optional(),
|
|
3669
|
+
UserPromptSubmit: z11.array(HookSpecSchema).optional(),
|
|
3670
|
+
PreLLMRequest: z11.array(HookSpecSchema).optional(),
|
|
3671
|
+
PostLLMResponse: z11.array(HookSpecSchema).optional(),
|
|
3672
|
+
PreToolUse: z11.array(HookSpecSchema).optional(),
|
|
3673
|
+
PostToolUse: z11.array(HookSpecSchema).optional(),
|
|
3674
|
+
PreCompact: z11.array(HookSpecSchema).optional(),
|
|
3675
|
+
PostCompact: z11.array(HookSpecSchema).optional(),
|
|
3676
|
+
MemoryPromote: z11.array(HookSpecSchema).optional()
|
|
3677
|
+
}).passthrough();
|
|
3678
|
+
var InputPreprocessSettingsSchema = z11.object({
|
|
3679
|
+
atFileExpand: z11.object({
|
|
3680
|
+
enabled: z11.boolean().optional(),
|
|
3681
|
+
maxBytes: z11.number().int().positive().optional()
|
|
3682
|
+
}).optional(),
|
|
3683
|
+
templateExpand: z11.object({
|
|
3684
|
+
enabled: z11.boolean().optional()
|
|
3685
|
+
}).optional(),
|
|
3686
|
+
maxChars: z11.number().int().positive().optional(),
|
|
3687
|
+
redactPreScan: z11.object({
|
|
3688
|
+
enabled: z11.boolean().optional()
|
|
3689
|
+
}).optional()
|
|
3690
|
+
}).passthrough();
|
|
3691
|
+
var RequestPreprocessSettingsSchema = z11.object({
|
|
3692
|
+
trimHistory: z11.object({
|
|
3693
|
+
enabled: z11.boolean().optional(),
|
|
3694
|
+
budgetRatio: z11.number().min(0).max(1).optional()
|
|
3695
|
+
}).optional(),
|
|
3696
|
+
budgetGuard: z11.object({
|
|
3697
|
+
enabled: z11.boolean().optional(),
|
|
3698
|
+
budgetRatio: z11.number().min(0).max(1).optional()
|
|
3699
|
+
}).optional(),
|
|
3700
|
+
redact: z11.object({
|
|
3701
|
+
enabled: z11.boolean().optional()
|
|
3702
|
+
}).optional()
|
|
3703
|
+
}).passthrough();
|
|
3704
|
+
var ResultPreprocessSettingsSchema = z11.object({
|
|
3705
|
+
truncate: z11.object({
|
|
3706
|
+
budgetBytes: z11.number().int().positive().optional()
|
|
3707
|
+
}).optional(),
|
|
3708
|
+
detectBinary: z11.object({
|
|
3709
|
+
enabled: z11.boolean().optional()
|
|
3710
|
+
}).optional(),
|
|
3711
|
+
summarize: z11.object({
|
|
3712
|
+
enabled: z11.boolean().optional()
|
|
3713
|
+
}).optional(),
|
|
3714
|
+
normalizeError: z11.object({
|
|
3715
|
+
enabled: z11.boolean().optional()
|
|
3716
|
+
}).optional(),
|
|
3717
|
+
redact: z11.object({
|
|
3718
|
+
enabled: z11.boolean().optional()
|
|
3719
|
+
}).optional(),
|
|
3720
|
+
injectDiff: z11.boolean().optional()
|
|
3721
|
+
}).passthrough();
|
|
3722
|
+
var RenderPreprocessSettingsSchema = z11.object({
|
|
3723
|
+
streamMarkdown: z11.object({
|
|
3724
|
+
enabled: z11.boolean().optional()
|
|
3725
|
+
}).optional(),
|
|
3726
|
+
collapseLong: z11.object({
|
|
3727
|
+
enabled: z11.boolean().optional(),
|
|
3728
|
+
threshold: z11.number().int().positive().optional()
|
|
3729
|
+
}).optional()
|
|
3730
|
+
}).passthrough();
|
|
3731
|
+
var PreprocessSettingsSchema = z11.object({
|
|
3732
|
+
input: InputPreprocessSettingsSchema.optional(),
|
|
3733
|
+
request: RequestPreprocessSettingsSchema.optional(),
|
|
3734
|
+
result: ResultPreprocessSettingsSchema.optional(),
|
|
3735
|
+
render: RenderPreprocessSettingsSchema.optional(),
|
|
3736
|
+
/** 全局禁用的 stage name 列表(kebab-case)。 */
|
|
3737
|
+
disable: z11.array(z11.string()).optional()
|
|
3738
|
+
}).passthrough();
|
|
3739
|
+
var SettingsSchema = z11.object({
|
|
3740
|
+
llm: LLMConfigSchema.optional(),
|
|
3741
|
+
providers: z11.record(ProviderConfigSchema).optional(),
|
|
3742
|
+
permissions: PermissionsSchema.optional(),
|
|
3743
|
+
ui: UISchema.optional(),
|
|
3744
|
+
mcpServers: z11.record(z11.unknown()).optional(),
|
|
3745
|
+
skills: z11.object({
|
|
3746
|
+
enabled: z11.boolean().optional(),
|
|
3747
|
+
disabled: z11.array(z11.string()).optional()
|
|
3748
|
+
}).optional(),
|
|
3749
|
+
hooks: HooksConfigSchema.optional(),
|
|
3750
|
+
preprocess: PreprocessSettingsSchema.optional(),
|
|
3751
|
+
/**
|
|
3752
|
+
* 启动时注入到 process.env 的额外环境变量(对齐业界 CLI Agent 的 settings.env 模式)。
|
|
3753
|
+
* 值必须是字符串(JSON 无 number→env 的隐式转换;约束传 "1" / "0" 这种字面值)。
|
|
3754
|
+
*
|
|
3755
|
+
* 当前支持的 muse 自识别变量:
|
|
3756
|
+
* MUSE_DISABLE_CURSOR_BLINK=1 关闭输入框光标闪烁(默认闪烁)
|
|
3757
|
+
* 其他 muse 模块可自行约定 MUSE_* 名字读取。
|
|
3758
|
+
*
|
|
3759
|
+
* 注意:这里写入的变量会进入 process.env,**但 Hook 子进程仍走环境变量白名单**
|
|
3760
|
+
* (见 src/preprocess/hooks.ts),不会自动透传给 hook 命令,避免泄露密钥。
|
|
3761
|
+
*/
|
|
3762
|
+
env: z11.record(z11.string()).optional(),
|
|
3763
|
+
/** Memory 模块设置(II-5 向量索引)。 */
|
|
3764
|
+
memory: z11.object({
|
|
3765
|
+
embedding: z11.object({
|
|
3766
|
+
/** 启用 embedding 召回(默认 false;关闭时 inject-memory 走传统全文)。 */
|
|
3767
|
+
enabled: z11.boolean().optional(),
|
|
3768
|
+
/** 后端 provider。默认 hash-bag(零依赖);设了 preset 自动走 openai-compatible。 */
|
|
3769
|
+
provider: z11.enum(["hash-bag", "openai-compatible", "openai", "local-minilm"]).optional(),
|
|
3770
|
+
/** Preset 名(dashscope-v3 / openai-3-small / openai-3-large / zhipu-3 / ollama-nomic / ollama-bge-m3)。 */
|
|
3771
|
+
preset: z11.string().optional(),
|
|
3772
|
+
/** Base URL(覆盖 preset 默认或自定义 provider 时填)。 */
|
|
3773
|
+
baseUrl: z11.string().optional(),
|
|
3774
|
+
/** 模型名(覆盖 preset 默认或自定义 provider 时填)。 */
|
|
3775
|
+
model: z11.string().optional(),
|
|
3776
|
+
/** 向量维度(用户根据模型官方说明调整;preset 给推荐默认,可覆盖)。
|
|
3777
|
+
* 设了此字段 → HTTP 请求带 dimensions 参数(MRL truncation);
|
|
3778
|
+
* 没设 → 走模型默认。
|
|
3779
|
+
* 启动期 muse 会 probe 一次校验实际维度,不匹配则降级 hash-bag + 提示修正。 */
|
|
3780
|
+
dim: z11.number().int().positive().optional(),
|
|
3781
|
+
/** API key(${ENV_VAR} 或明文;Ollama 等本地端点可省)。 */
|
|
3782
|
+
apiKey: z11.string().optional(),
|
|
3783
|
+
/** 检索 top-K(默认 5)。 */
|
|
3784
|
+
topK: z11.number().int().positive().optional(),
|
|
3785
|
+
/** memory 数量低于此值时退化到全注入(默认 3,2026-06-07 R5 修订)。 */
|
|
3786
|
+
minMemoryCount: z11.number().int().nonnegative().optional(),
|
|
3787
|
+
/** 注入 token 预算上限,超出按 trust 优先保留(默认 1500)。 */
|
|
3788
|
+
maxInjectTokens: z11.number().int().positive().optional()
|
|
3789
|
+
}).optional()
|
|
3790
|
+
}).optional()
|
|
3791
|
+
}).passthrough();
|
|
3792
|
+
|
|
3793
|
+
// src/config/_env.ts
|
|
3794
|
+
var ENV_PATTERN2 = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
|
|
3795
|
+
function expandEnvVars(value) {
|
|
3796
|
+
if (typeof value === "string") {
|
|
3797
|
+
return value.replace(ENV_PATTERN2, (_match, name) => process.env[name] ?? "");
|
|
3798
|
+
}
|
|
3799
|
+
if (Array.isArray(value)) {
|
|
3800
|
+
return value.map(expandEnvVars);
|
|
3801
|
+
}
|
|
3802
|
+
if (value && typeof value === "object") {
|
|
3803
|
+
const result = {};
|
|
3804
|
+
for (const [k, v] of Object.entries(value)) {
|
|
3805
|
+
result[k] = expandEnvVars(v);
|
|
3806
|
+
}
|
|
3807
|
+
return result;
|
|
3808
|
+
}
|
|
3809
|
+
return value;
|
|
3810
|
+
}
|
|
3811
|
+
|
|
3812
|
+
// src/config/loader.ts
|
|
3813
|
+
function formatZodIssues(issues) {
|
|
3814
|
+
return issues.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`).join("; ");
|
|
3815
|
+
}
|
|
3816
|
+
var DEFAULTS = {
|
|
1872
3817
|
llm: {
|
|
1873
3818
|
provider: "deepseek",
|
|
1874
3819
|
model: "deepseek-chat"
|
|
@@ -1894,9 +3839,9 @@ var DEFAULTS = {
|
|
|
1894
3839
|
}
|
|
1895
3840
|
};
|
|
1896
3841
|
async function readJsonIfExists(path) {
|
|
1897
|
-
if (!
|
|
3842
|
+
if (!existsSync6(path)) return void 0;
|
|
1898
3843
|
try {
|
|
1899
|
-
const raw = await
|
|
3844
|
+
const raw = await readFile8(path, "utf-8");
|
|
1900
3845
|
return JSON.parse(raw);
|
|
1901
3846
|
} catch (err) {
|
|
1902
3847
|
log.warn(`Failed to parse settings at ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -1924,9 +3869,9 @@ async function loadSettings(cwd = process.cwd()) {
|
|
|
1924
3869
|
const sources = ["<defaults>"];
|
|
1925
3870
|
let merged = DEFAULTS;
|
|
1926
3871
|
const candidates = [
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
3872
|
+
join7(homedir7(), ".muse", "settings.json"),
|
|
3873
|
+
join7(cwd, ".muse", "settings.json"),
|
|
3874
|
+
join7(cwd, ".muse", "settings.local.json")
|
|
1930
3875
|
];
|
|
1931
3876
|
for (const path of candidates) {
|
|
1932
3877
|
const raw = await readJsonIfExists(path);
|
|
@@ -1949,6 +3894,13 @@ async function loadSettings(cwd = process.cwd()) {
|
|
|
1949
3894
|
sources.push("env:MUSE_MODEL");
|
|
1950
3895
|
}
|
|
1951
3896
|
merged = expandEnvVars(merged);
|
|
3897
|
+
if (merged.env) {
|
|
3898
|
+
for (const [key, value] of Object.entries(merged.env)) {
|
|
3899
|
+
if (process.env[key] === void 0) {
|
|
3900
|
+
process.env[key] = value;
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
1952
3904
|
return { settings: merged, sources };
|
|
1953
3905
|
}
|
|
1954
3906
|
export {
|