topchester-ai 0.10.0 → 0.12.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/dist/cli.mjs +2927 -418
- package/dist/cli.mjs.map +1 -1
- package/package.json +5 -2
package/dist/cli.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import { parse } from "yaml";
|
|
|
15
15
|
import pino from "pino";
|
|
16
16
|
import picomatch from "picomatch";
|
|
17
17
|
import { uuidv7 } from "uuidv7";
|
|
18
|
-
import {
|
|
18
|
+
import { CURSOR_MARKER, Markdown, ProcessTerminal, TUI, decodeKittyPrintable, isKeyRelease, isKeyRepeat, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
19
19
|
import { highlight, supportsLanguage } from "cli-highlight";
|
|
20
20
|
//#region src/agent/tools/ai-sdk-tools.ts
|
|
21
21
|
function toAiSdkToolSet(definitions) {
|
|
@@ -97,23 +97,26 @@ async function enqueueFileMutation(path, mutate) {
|
|
|
97
97
|
}
|
|
98
98
|
//#endregion
|
|
99
99
|
//#region src/agent/tools/types.ts
|
|
100
|
+
function isToolErrorResult(result) {
|
|
101
|
+
return "error" in result && typeof result.error === "string";
|
|
102
|
+
}
|
|
100
103
|
function defineTool(definition) {
|
|
101
104
|
return definition;
|
|
102
105
|
}
|
|
103
106
|
//#endregion
|
|
104
107
|
//#region src/agent/tools/edit-file.ts
|
|
105
108
|
const editFileEditSchema = z.object({
|
|
106
|
-
old_text: z.string(),
|
|
107
|
-
new_text: z.string()
|
|
109
|
+
old_text: z.string().describe("Exact current file text to replace; include whitespace exactly."),
|
|
110
|
+
new_text: z.string().describe("Replacement text for old_text.")
|
|
108
111
|
});
|
|
109
112
|
const editFileTool = defineTool({
|
|
110
113
|
name: "edit_file",
|
|
111
|
-
description: "Edit an existing UTF-8 file inside the workspace with exact text replacements.",
|
|
112
|
-
prompt: "edit_file: edit an existing UTF-8 file inside the workspace with exact old_text/new_text replacements; read the file first, keep old_text small but unique, and make multiple disjoint edits for one file in one call. To use it, reply with only JSON: {\"tool\":\"edit_file\",\"args\":{\"path\":\"src/example.ts\",\"
|
|
114
|
+
description: "Edit an existing UTF-8 file inside the workspace with exact text replacements. Use expected_current_hash only as the current/pre-edit hash from read_file, never as a predicted post-edit hash.",
|
|
115
|
+
prompt: "edit_file: edit an existing UTF-8 file inside the workspace with exact old_text/new_text replacements; read the file first, keep old_text small but unique, and make multiple disjoint edits for one file in one call. expected_current_hash is optional and must be the current/pre-edit hash returned by the latest read_file for that file; never invent it or use a predicted after-edit hash. To use it, reply with only JSON: {\"tool\":\"edit_file\",\"args\":{\"path\":\"src/example.ts\",\"expected_current_hash\":\"sha256:current-file-hash-from-read_file\",\"edits\":[{\"old_text\":\"const enabled = false;\\n\",\"new_text\":\"const enabled = true;\\n\"}]}}",
|
|
113
116
|
argsSchema: z.object({
|
|
114
|
-
path: z.string(),
|
|
115
|
-
|
|
116
|
-
edits: z.array(editFileEditSchema).min(1)
|
|
117
|
+
path: z.string().describe("Workspace-relative path to the existing UTF-8 file to edit."),
|
|
118
|
+
expected_current_hash: z.string().optional().describe("Optional current file hash returned by the latest read_file result for this file. This is checked before editing to catch stale reads; it is not the hash after the edit."),
|
|
119
|
+
edits: z.array(editFileEditSchema).min(1).describe("Exact text replacements to apply to the original file.")
|
|
117
120
|
}),
|
|
118
121
|
execute: (context, args) => editWorkspaceFile(context.workspaceRoot, args, { logger: context.logger })
|
|
119
122
|
});
|
|
@@ -123,7 +126,7 @@ async function editWorkspaceFile(workspaceRoot, args, options = {}) {
|
|
|
123
126
|
const fileStat = await statExistingFile(scopedPath.path, args.path);
|
|
124
127
|
const beforeBytes = await readFile(scopedPath.path);
|
|
125
128
|
const beforeHash = hashBytes$1(beforeBytes);
|
|
126
|
-
if (args.
|
|
129
|
+
if (args.expected_current_hash && args.expected_current_hash !== beforeHash) throw new Error(`edit_file expected_current_hash did not match ${scopedPath.relativePath}.`);
|
|
127
130
|
const result = applyExactEdits(decodeUtf8$1(scopedPath.relativePath, beforeBytes), args.edits, scopedPath.relativePath);
|
|
128
131
|
const afterBytes = Buffer.from(result.newContent, "utf8");
|
|
129
132
|
const afterHash = hashBytes$1(afterBytes);
|
|
@@ -376,9 +379,12 @@ const ignoredDirectories = new Set([
|
|
|
376
379
|
]);
|
|
377
380
|
const findFileTool = defineTool({
|
|
378
381
|
name: "find_file",
|
|
379
|
-
description: "Find files by fuzzy name inside the workspace.",
|
|
382
|
+
description: "Find files by fuzzy name inside the workspace. Results are file paths, not file contents; use read_file next when the user needs contents.",
|
|
380
383
|
prompt: "find_file: find existing files by fuzzy path or filename inside the workspace; matches may appear in the middle of a filename, and results are file paths, not file contents. To use it, reply with only JSON: {\"tool\":\"find_file\",\"args\":{\"query\":\"runtime\"}}",
|
|
381
384
|
argsSchema: findFileArgsSchema,
|
|
385
|
+
parallelSafe: true,
|
|
386
|
+
mutatesWorkspace: false,
|
|
387
|
+
resourceKeys: (args) => [`find:${args.path}`],
|
|
382
388
|
execute: (context, args) => findWorkspaceFilesByName(context.workspaceRoot, args, {
|
|
383
389
|
pathEnv: context.pathEnv,
|
|
384
390
|
logger: context.logger
|
|
@@ -829,7 +835,7 @@ function parseGitLog(output) {
|
|
|
829
835
|
};
|
|
830
836
|
});
|
|
831
837
|
}
|
|
832
|
-
function truncateText(content, maxBytes) {
|
|
838
|
+
function truncateText$1(content, maxBytes) {
|
|
833
839
|
if (Buffer.byteLength(content) <= maxBytes) return {
|
|
834
840
|
content,
|
|
835
841
|
truncated: false
|
|
@@ -915,6 +921,9 @@ const gitStatusTool = defineTool({
|
|
|
915
921
|
description: "Inspect structured Git branch and changed-file status inside the workspace.",
|
|
916
922
|
prompt: "git_status: inspect branch, head, clean state, staged, unstaged, and untracked files without parsing shell output. To use it, reply with only JSON: {\"tool\":\"git_status\",\"args\":{\"path\":\".\",\"include_untracked\":true}}",
|
|
917
923
|
argsSchema: gitStatusArgsSchema,
|
|
924
|
+
parallelSafe: true,
|
|
925
|
+
mutatesWorkspace: false,
|
|
926
|
+
resourceKeys: (args) => [`git-status:${args.path}`],
|
|
918
927
|
execute: (context, args) => inspectGitStatus(context, args)
|
|
919
928
|
});
|
|
920
929
|
const gitDiffTool = defineTool({
|
|
@@ -922,6 +931,9 @@ const gitDiffTool = defineTool({
|
|
|
922
931
|
description: "Inspect bounded Git diffs for staged, unstaged, and optionally untracked files.",
|
|
923
932
|
prompt: "git_diff: inspect a bounded Git diff; use scope \"all\", \"unstaged\", or \"staged\", and include_untracked:true only when untracked file patches are needed. To use it, reply with only JSON: {\"tool\":\"git_diff\",\"args\":{\"scope\":\"all\",\"include_untracked\":true}}",
|
|
924
933
|
argsSchema: gitDiffArgsSchema,
|
|
934
|
+
parallelSafe: true,
|
|
935
|
+
mutatesWorkspace: false,
|
|
936
|
+
resourceKeys: (args) => [`git-diff:${args.path ?? "."}:${args.scope}`],
|
|
925
937
|
execute: (context, args) => inspectGitDiff(context, args)
|
|
926
938
|
});
|
|
927
939
|
const gitLogTool = defineTool({
|
|
@@ -929,6 +941,9 @@ const gitLogTool = defineTool({
|
|
|
929
941
|
description: "Inspect recent Git commits as bounded structured summaries.",
|
|
930
942
|
prompt: "git_log: inspect recent commits without parsing shell output. To use it, reply with only JSON: {\"tool\":\"git_log\",\"args\":{\"limit\":10,\"path\":\"src/agent/runtime.ts\"}}",
|
|
931
943
|
argsSchema: gitLogArgsSchema,
|
|
944
|
+
parallelSafe: true,
|
|
945
|
+
mutatesWorkspace: false,
|
|
946
|
+
resourceKeys: (args) => [`git-log:${args.path ?? "."}`],
|
|
932
947
|
execute: (context, args) => inspectGitLog(context, args)
|
|
933
948
|
});
|
|
934
949
|
const gitAddTool = defineTool({
|
|
@@ -1056,7 +1071,7 @@ async function inspectGitDiff(context, args) {
|
|
|
1056
1071
|
changedFiles.add(file);
|
|
1057
1072
|
}
|
|
1058
1073
|
}
|
|
1059
|
-
const bounded = truncateText(sections.join("\n").trimEnd() || "No diff.", args.max_bytes);
|
|
1074
|
+
const bounded = truncateText$1(sections.join("\n").trimEnd() || "No diff.", args.max_bytes);
|
|
1060
1075
|
return {
|
|
1061
1076
|
tool: "git_diff",
|
|
1062
1077
|
path: path ?? void 0,
|
|
@@ -1343,6 +1358,9 @@ const grepTool = defineTool({
|
|
|
1343
1358
|
pattern: z.string(),
|
|
1344
1359
|
path: z.string().optional()
|
|
1345
1360
|
}),
|
|
1361
|
+
parallelSafe: true,
|
|
1362
|
+
mutatesWorkspace: false,
|
|
1363
|
+
resourceKeys: (args) => [`grep:${args.path ?? "."}`],
|
|
1346
1364
|
execute: (context, args) => grepWorkspace(context.workspaceRoot, args, {
|
|
1347
1365
|
pathEnv: context.pathEnv,
|
|
1348
1366
|
logger: context.logger
|
|
@@ -1644,6 +1662,8 @@ const READ_ONLY_COMMANDS = new Set([
|
|
|
1644
1662
|
"find",
|
|
1645
1663
|
"fd",
|
|
1646
1664
|
"cat",
|
|
1665
|
+
"sed",
|
|
1666
|
+
"sort",
|
|
1647
1667
|
"head",
|
|
1648
1668
|
"tail",
|
|
1649
1669
|
"wc",
|
|
@@ -1851,6 +1871,7 @@ function validateSimpleCommand(command, context) {
|
|
|
1851
1871
|
case "git": return validateGitCommand(command, context);
|
|
1852
1872
|
case "find": return validateFindCommand(command, context);
|
|
1853
1873
|
case "fd": return validateGenericCommandArgs(command, context, FD_OPTIONS_WITH_VALUES);
|
|
1874
|
+
case "sed": return validateSedCommand(command, context);
|
|
1854
1875
|
default: return validateGenericCommandArgs(command, context, COMMON_OPTIONS_WITH_VALUES);
|
|
1855
1876
|
}
|
|
1856
1877
|
}
|
|
@@ -1873,6 +1894,88 @@ function validateGitCommand(command, context) {
|
|
|
1873
1894
|
function validateFindCommand(command, context) {
|
|
1874
1895
|
return validateGenericCommandArgs(command, context, FIND_OPTIONS_WITH_VALUES);
|
|
1875
1896
|
}
|
|
1897
|
+
function validateSedCommand(command, context) {
|
|
1898
|
+
let sawScript = false;
|
|
1899
|
+
for (let index = 0; index < command.args.length; index += 1) {
|
|
1900
|
+
const arg = command.args[index] ?? "";
|
|
1901
|
+
if (arg === "-i" || arg.startsWith("-i") || arg === "--in-place" || arg.startsWith("--in-place=")) return {
|
|
1902
|
+
allowed: false,
|
|
1903
|
+
reason: "inspect_command rejected 'sed' because in-place edits are unsafe."
|
|
1904
|
+
};
|
|
1905
|
+
if (arg === "-e" || arg === "--expression") {
|
|
1906
|
+
const script = command.args[index + 1];
|
|
1907
|
+
if (!script) return {
|
|
1908
|
+
allowed: false,
|
|
1909
|
+
reason: "inspect_command rejected 'sed' because -e requires a script."
|
|
1910
|
+
};
|
|
1911
|
+
const scriptResult = validateSedScript(script);
|
|
1912
|
+
if (!scriptResult.allowed) return scriptResult;
|
|
1913
|
+
sawScript = true;
|
|
1914
|
+
index += 1;
|
|
1915
|
+
continue;
|
|
1916
|
+
}
|
|
1917
|
+
if (arg === "-n" || arg === "--quiet" || arg === "--silent" || /^-[Erun]+$/.test(arg)) continue;
|
|
1918
|
+
if (arg.startsWith("-")) return {
|
|
1919
|
+
allowed: false,
|
|
1920
|
+
reason: `inspect_command rejected 'sed' because '${arg}' is not allowed.`
|
|
1921
|
+
};
|
|
1922
|
+
if (!sawScript && looksLikeSedScript(arg)) {
|
|
1923
|
+
const scriptResult = validateSedScript(arg);
|
|
1924
|
+
if (!scriptResult.allowed) return scriptResult;
|
|
1925
|
+
sawScript = true;
|
|
1926
|
+
continue;
|
|
1927
|
+
}
|
|
1928
|
+
const scoped = resolveWorkspacePath$1(context.workspaceRoot, arg, context.cwd);
|
|
1929
|
+
if (!scoped.allowed) return {
|
|
1930
|
+
allowed: false,
|
|
1931
|
+
reason: scoped.reason
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
1934
|
+
return sawScript ? { allowed: true } : {
|
|
1935
|
+
allowed: false,
|
|
1936
|
+
reason: "inspect_command rejected 'sed' because it requires an inline script."
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
function looksLikeSedScript(arg) {
|
|
1940
|
+
return arg.startsWith("s") || /^(\d+|\$)?(,(\d+|\$))?[pdq]$/.test(arg);
|
|
1941
|
+
}
|
|
1942
|
+
function validateSedScript(script) {
|
|
1943
|
+
if (script.includes("\n")) return {
|
|
1944
|
+
allowed: false,
|
|
1945
|
+
reason: "inspect_command rejected 'sed' because multiline scripts are unsafe."
|
|
1946
|
+
};
|
|
1947
|
+
if (/^(\d+|\$)?(,(\d+|\$))?[pdq]$/.test(script)) return { allowed: true };
|
|
1948
|
+
if (!script.startsWith("s") || script.length < 4) return {
|
|
1949
|
+
allowed: false,
|
|
1950
|
+
reason: "inspect_command rejected 'sed' because only simple read-only scripts are allowed."
|
|
1951
|
+
};
|
|
1952
|
+
const delimiter = script[1] ?? "";
|
|
1953
|
+
if (!delimiter || /[A-Za-z0-9\\\n]/.test(delimiter)) return {
|
|
1954
|
+
allowed: false,
|
|
1955
|
+
reason: "inspect_command rejected 'sed' because the substitution delimiter is invalid."
|
|
1956
|
+
};
|
|
1957
|
+
const delimiters = findUnescapedDelimiterIndexes(script, delimiter);
|
|
1958
|
+
if (delimiters.length < 3) return {
|
|
1959
|
+
allowed: false,
|
|
1960
|
+
reason: "inspect_command rejected 'sed' because the substitution script is incomplete."
|
|
1961
|
+
};
|
|
1962
|
+
const flags = script.slice(delimiters[2] + 1);
|
|
1963
|
+
if (/[^gIpM0-9]/.test(flags) || /[ew]/.test(flags)) return {
|
|
1964
|
+
allowed: false,
|
|
1965
|
+
reason: "inspect_command rejected 'sed' because substitution flags are unsafe."
|
|
1966
|
+
};
|
|
1967
|
+
return { allowed: true };
|
|
1968
|
+
}
|
|
1969
|
+
function findUnescapedDelimiterIndexes(script, delimiter) {
|
|
1970
|
+
const indexes = [];
|
|
1971
|
+
for (let index = 0; index < script.length; index += 1) {
|
|
1972
|
+
if (script[index] !== delimiter) continue;
|
|
1973
|
+
let slashCount = 0;
|
|
1974
|
+
for (let back = index - 1; back >= 0 && script[back] === "\\"; back -= 1) slashCount += 1;
|
|
1975
|
+
if (slashCount % 2 === 0) indexes.push(index);
|
|
1976
|
+
}
|
|
1977
|
+
return indexes;
|
|
1978
|
+
}
|
|
1876
1979
|
function validateGenericCommandArgs(command, context, optionsWithValues, knownPathlessWords = /* @__PURE__ */ new Set()) {
|
|
1877
1980
|
for (let index = 0; index < command.args.length; index += 1) {
|
|
1878
1981
|
const arg = command.args[index] ?? "";
|
|
@@ -2232,6 +2335,9 @@ const listFilesTool = defineTool({
|
|
|
2232
2335
|
recursive: z.boolean().optional().default(false),
|
|
2233
2336
|
limit: z.number().int().min(1).max(2e3).optional().default(500)
|
|
2234
2337
|
}),
|
|
2338
|
+
parallelSafe: true,
|
|
2339
|
+
mutatesWorkspace: false,
|
|
2340
|
+
resourceKeys: (args) => [`dir:${args.path}`],
|
|
2235
2341
|
execute: (context, args) => listWorkspaceFiles(context.workspaceRoot, args)
|
|
2236
2342
|
});
|
|
2237
2343
|
async function listWorkspaceFiles(workspaceRoot, args) {
|
|
@@ -2310,11 +2416,264 @@ function resolveWorkspaceScopedPath$1(workspaceRoot, path) {
|
|
|
2310
2416
|
relativePath: relativePath || "."
|
|
2311
2417
|
};
|
|
2312
2418
|
}
|
|
2419
|
+
//#endregion
|
|
2420
|
+
//#region src/cli/ui.ts
|
|
2421
|
+
const colors = {
|
|
2422
|
+
bgSoftGray: "\x1B[48;5;236m",
|
|
2423
|
+
blue: "\x1B[34m",
|
|
2424
|
+
cyan: "\x1B[36m",
|
|
2425
|
+
darkGray: "\x1B[90m",
|
|
2426
|
+
dim: "\x1B[2m",
|
|
2427
|
+
green: "\x1B[32m",
|
|
2428
|
+
orange: "\x1B[38;5;208m",
|
|
2429
|
+
purple: "\x1B[35m",
|
|
2430
|
+
red: "\x1B[31m",
|
|
2431
|
+
reset: "\x1B[0m",
|
|
2432
|
+
resetForeground: "\x1B[39m",
|
|
2433
|
+
yellow: "\x1B[33m"
|
|
2434
|
+
};
|
|
2435
|
+
const ui = {
|
|
2436
|
+
heading(text) {
|
|
2437
|
+
return color(`Topchester ${text}`, "cyan");
|
|
2438
|
+
},
|
|
2439
|
+
label(text) {
|
|
2440
|
+
return color(text, "dim");
|
|
2441
|
+
},
|
|
2442
|
+
muted(text) {
|
|
2443
|
+
return color(text, "darkGray");
|
|
2444
|
+
},
|
|
2445
|
+
model(text) {
|
|
2446
|
+
return color(text, "blue");
|
|
2447
|
+
},
|
|
2448
|
+
modelInline(text) {
|
|
2449
|
+
if (!shouldUseColor()) return text;
|
|
2450
|
+
return `${colors.blue}${text}${colors.resetForeground}`;
|
|
2451
|
+
},
|
|
2452
|
+
ok(text) {
|
|
2453
|
+
return color(text, "green");
|
|
2454
|
+
},
|
|
2455
|
+
warn(text) {
|
|
2456
|
+
return color(text, "yellow");
|
|
2457
|
+
},
|
|
2458
|
+
error(text) {
|
|
2459
|
+
return color(text, "red");
|
|
2460
|
+
},
|
|
2461
|
+
softBackground(text) {
|
|
2462
|
+
return color(text, "bgSoftGray");
|
|
2463
|
+
},
|
|
2464
|
+
async spinner(text, action) {
|
|
2465
|
+
return withStatusLine(text, action, void 0, 80, false);
|
|
2466
|
+
},
|
|
2467
|
+
async progress(text, action) {
|
|
2468
|
+
let latest = text;
|
|
2469
|
+
return withStatusLine(text, () => action((message) => {
|
|
2470
|
+
latest = message;
|
|
2471
|
+
}), () => latest, 80, true);
|
|
2472
|
+
}
|
|
2473
|
+
};
|
|
2474
|
+
async function withStatusLine(text, action, getText = () => text, progressEveryMs = 80, emitPlainProgress = false) {
|
|
2475
|
+
if (!shouldUseColor()) {
|
|
2476
|
+
if (!emitPlainProgress) return action();
|
|
2477
|
+
const timer = setInterval(() => {
|
|
2478
|
+
stderr.write(`${getText()}\n`);
|
|
2479
|
+
}, Math.max(progressEveryMs, 5e3));
|
|
2480
|
+
try {
|
|
2481
|
+
return await action();
|
|
2482
|
+
} finally {
|
|
2483
|
+
clearInterval(timer);
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
const frames = [
|
|
2487
|
+
"⠋",
|
|
2488
|
+
"⠙",
|
|
2489
|
+
"⠹",
|
|
2490
|
+
"⠸",
|
|
2491
|
+
"⠼",
|
|
2492
|
+
"⠴",
|
|
2493
|
+
"⠦",
|
|
2494
|
+
"⠧",
|
|
2495
|
+
"⠇",
|
|
2496
|
+
"⠏"
|
|
2497
|
+
];
|
|
2498
|
+
let index = 0;
|
|
2499
|
+
stderr.write(`${color(frames[index], "cyan")} ${getText()}`);
|
|
2500
|
+
const timer = setInterval(() => {
|
|
2501
|
+
index = (index + 1) % frames.length;
|
|
2502
|
+
stderr.write(`\r\u001b[2K${color(frames[index], "cyan")} ${getText()}`);
|
|
2503
|
+
}, progressEveryMs);
|
|
2504
|
+
try {
|
|
2505
|
+
return await action();
|
|
2506
|
+
} finally {
|
|
2507
|
+
clearInterval(timer);
|
|
2508
|
+
stderr.write(`\r\u001b[2K`);
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
function color(text, colorName) {
|
|
2512
|
+
if (!shouldUseColor()) return text;
|
|
2513
|
+
return `${colors[colorName]}${text}${colors.reset}`;
|
|
2514
|
+
}
|
|
2515
|
+
function shouldUseColor() {
|
|
2516
|
+
if (process.env.NO_COLOR) return false;
|
|
2517
|
+
if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0") return true;
|
|
2518
|
+
return stdout.isTTY === true;
|
|
2519
|
+
}
|
|
2520
|
+
//#endregion
|
|
2521
|
+
//#region src/agent/task-plan.ts
|
|
2522
|
+
const planTodoStatusSchema = z.enum([
|
|
2523
|
+
"pending",
|
|
2524
|
+
"in_progress",
|
|
2525
|
+
"completed"
|
|
2526
|
+
]);
|
|
2527
|
+
const taskPlanItemSchema = z.object({
|
|
2528
|
+
text: z.string().trim().min(1, "Plan item text cannot be empty."),
|
|
2529
|
+
status: planTodoStatusSchema
|
|
2530
|
+
});
|
|
2531
|
+
const planTodoArgsSchema = z.object({ items: z.array(taskPlanItemSchema).max(20, "Plan updates are limited to 20 items.") }).superRefine((args, context) => {
|
|
2532
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2533
|
+
let inProgressCount = 0;
|
|
2534
|
+
let incompleteCount = 0;
|
|
2535
|
+
args.items.forEach((item, index) => {
|
|
2536
|
+
const key = item.text.toLocaleLowerCase("en");
|
|
2537
|
+
if (seen.has(key)) context.addIssue({
|
|
2538
|
+
code: "custom",
|
|
2539
|
+
message: "Plan item text must be unique.",
|
|
2540
|
+
path: [
|
|
2541
|
+
"items",
|
|
2542
|
+
index,
|
|
2543
|
+
"text"
|
|
2544
|
+
]
|
|
2545
|
+
});
|
|
2546
|
+
seen.add(key);
|
|
2547
|
+
if (item.status === "in_progress") inProgressCount += 1;
|
|
2548
|
+
if (item.status !== "completed") incompleteCount += 1;
|
|
2549
|
+
});
|
|
2550
|
+
if (inProgressCount > 1) context.addIssue({
|
|
2551
|
+
code: "custom",
|
|
2552
|
+
message: "At most one plan item can be in_progress.",
|
|
2553
|
+
path: ["items"]
|
|
2554
|
+
});
|
|
2555
|
+
if (args.items.length > 0 && incompleteCount > 0 && inProgressCount === 0) context.addIssue({
|
|
2556
|
+
code: "custom",
|
|
2557
|
+
message: "A non-completed plan must have exactly one in_progress item.",
|
|
2558
|
+
path: ["items"]
|
|
2559
|
+
});
|
|
2560
|
+
});
|
|
2561
|
+
function createEmptyTaskPlanState(now = /* @__PURE__ */ new Date()) {
|
|
2562
|
+
return {
|
|
2563
|
+
items: [],
|
|
2564
|
+
updatedAt: now.toISOString()
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
function applyTaskPlanUpdate(_previous, args, now = /* @__PURE__ */ new Date()) {
|
|
2568
|
+
return {
|
|
2569
|
+
items: planTodoArgsSchema.parse(args).items.map((item) => ({
|
|
2570
|
+
text: item.text,
|
|
2571
|
+
status: item.status
|
|
2572
|
+
})),
|
|
2573
|
+
updatedAt: now.toISOString()
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
function createTaskPlanController(initialState = createEmptyTaskPlanState(), now = () => /* @__PURE__ */ new Date()) {
|
|
2577
|
+
let state = initialState;
|
|
2578
|
+
return {
|
|
2579
|
+
update(args) {
|
|
2580
|
+
state = applyTaskPlanUpdate(state, args, now());
|
|
2581
|
+
return state;
|
|
2582
|
+
},
|
|
2583
|
+
get() {
|
|
2584
|
+
return state;
|
|
2585
|
+
}
|
|
2586
|
+
};
|
|
2587
|
+
}
|
|
2588
|
+
function summarizeTaskPlan(state) {
|
|
2589
|
+
const pendingCount = state.items.filter((item) => item.status === "pending").length;
|
|
2590
|
+
const inProgressCount = state.items.filter((item) => item.status === "in_progress").length;
|
|
2591
|
+
const completedCount = state.items.filter((item) => item.status === "completed").length;
|
|
2592
|
+
const currentItem = state.items.find((item) => item.status === "in_progress")?.text;
|
|
2593
|
+
return {
|
|
2594
|
+
pendingCount,
|
|
2595
|
+
inProgressCount,
|
|
2596
|
+
completedCount,
|
|
2597
|
+
...currentItem === void 0 ? {} : { currentItem }
|
|
2598
|
+
};
|
|
2599
|
+
}
|
|
2600
|
+
function hasOpenTaskPlan(state) {
|
|
2601
|
+
return Boolean(state && state.items.some((item) => item.status !== "completed"));
|
|
2602
|
+
}
|
|
2603
|
+
function formatTaskPlanForPrompt(state) {
|
|
2604
|
+
const summary = summarizeTaskPlan(state);
|
|
2605
|
+
return [
|
|
2606
|
+
"Plan updated",
|
|
2607
|
+
`pending: ${summary.pendingCount}`,
|
|
2608
|
+
`in_progress: ${summary.inProgressCount}`,
|
|
2609
|
+
`completed: ${summary.completedCount}`,
|
|
2610
|
+
summary.currentItem ? `current: ${summary.currentItem}` : ""
|
|
2611
|
+
].filter(Boolean).join("\n");
|
|
2612
|
+
}
|
|
2613
|
+
function detectTaskPlanChange(previous, next) {
|
|
2614
|
+
const hadPlan = Boolean(previous && previous.items.length > 0);
|
|
2615
|
+
const hasPlan = Boolean(next && next.items.length > 0);
|
|
2616
|
+
if (!hadPlan && hasPlan) return "created";
|
|
2617
|
+
if (hadPlan && !hasPlan) return "cleared";
|
|
2618
|
+
if (hadPlan && hasPlan) return "updated";
|
|
2619
|
+
return "unchanged";
|
|
2620
|
+
}
|
|
2621
|
+
function formatTaskPlanNotice(change, state) {
|
|
2622
|
+
if (change === "unchanged") return;
|
|
2623
|
+
if (change === "cleared" || state.items.length === 0) return "todo plan cleared";
|
|
2624
|
+
const summary = summarizeTaskPlan(state);
|
|
2625
|
+
if (summary.inProgressCount === 0 && summary.pendingCount === 0) return "todo plan completed";
|
|
2626
|
+
const prefix = change === "created" ? "todo plan created" : "todo plan updated";
|
|
2627
|
+
return summary.currentItem ? `${prefix}: ${summary.currentItem}` : prefix;
|
|
2628
|
+
}
|
|
2629
|
+
function formatTaskPlanForTui(state, width, visibleLimit = 6) {
|
|
2630
|
+
if (state.items.length === 0) return [];
|
|
2631
|
+
const itemWidth = Math.max(1, Math.max(12, width) - 6);
|
|
2632
|
+
const visibleItems = state.items.slice(0, visibleLimit);
|
|
2633
|
+
const lines = visibleItems.map((item) => formatTaskPlanTuiLine(item, truncateText(item.text, itemWidth)));
|
|
2634
|
+
const remaining = state.items.length - visibleItems.length;
|
|
2635
|
+
if (remaining > 0) lines.push(ui.muted(` +${remaining} more`));
|
|
2636
|
+
return lines;
|
|
2637
|
+
}
|
|
2638
|
+
function formatTaskPlanTuiLine(item, text) {
|
|
2639
|
+
switch (item.status) {
|
|
2640
|
+
case "completed": return ` ${ui.ok("[x]")} ${ui.muted(text)}`;
|
|
2641
|
+
case "in_progress": return ` ${ui.ok("[>]")} ${ui.ok(text)}`;
|
|
2642
|
+
case "pending": return ` ${ui.muted("[ ]")} ${text}`;
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
function truncateText(text, width) {
|
|
2646
|
+
if (text.length <= width) return text;
|
|
2647
|
+
if (width <= 3) return ".".repeat(Math.max(0, width));
|
|
2648
|
+
return `${text.slice(0, width - 3)}...`;
|
|
2649
|
+
}
|
|
2650
|
+
//#endregion
|
|
2651
|
+
//#region src/agent/tools/plan-todo.ts
|
|
2652
|
+
const planTodoTool = defineTool({
|
|
2653
|
+
name: "plan_todo",
|
|
2654
|
+
description: "Replace the visible session task plan for multi-step work.",
|
|
2655
|
+
prompt: "plan_todo: replace the visible session task plan for non-trivial multi-step work; keep 2-6 short items, exactly one in_progress item while work remains, and use [] only to clear. Do not use plan_todo just to report completed work before a final answer. To use it, reply with only JSON: {\"tool\":\"plan_todo\",\"args\":{\"items\":[{\"text\":\"Inspect relevant files\",\"status\":\"in_progress\"},{\"text\":\"Implement focused change\",\"status\":\"pending\"}]}}",
|
|
2656
|
+
argsSchema: planTodoArgsSchema,
|
|
2657
|
+
async execute(context, args) {
|
|
2658
|
+
if (!context.taskPlan) throw new Error("plan_todo requires runtime task-plan state.");
|
|
2659
|
+
const plan = context.taskPlan.update(args);
|
|
2660
|
+
const summary = summarizeTaskPlan(plan);
|
|
2661
|
+
return {
|
|
2662
|
+
tool: "plan_todo",
|
|
2663
|
+
content: formatTaskPlanForPrompt(plan),
|
|
2664
|
+
plan,
|
|
2665
|
+
...summary
|
|
2666
|
+
};
|
|
2667
|
+
}
|
|
2668
|
+
});
|
|
2313
2669
|
const readFileTool = defineTool({
|
|
2314
2670
|
name: "read_file",
|
|
2315
2671
|
description: "Read a UTF-8 file inside the workspace.",
|
|
2316
2672
|
prompt: "read_file: read a UTF-8 file inside the workspace. To use it, reply with only JSON: {\"tool\":\"read_file\",\"args\":{\"path\":\"package.json\"}}",
|
|
2317
2673
|
argsSchema: z.object({ path: z.string() }),
|
|
2674
|
+
parallelSafe: true,
|
|
2675
|
+
mutatesWorkspace: false,
|
|
2676
|
+
resourceKeys: (args) => [`file:${args.path}`],
|
|
2318
2677
|
execute: (context, args) => readWorkspaceFile(context.workspaceRoot, args.path)
|
|
2319
2678
|
});
|
|
2320
2679
|
async function readWorkspaceFile(workspaceRoot, path) {
|
|
@@ -2331,16 +2690,52 @@ async function readWorkspaceFile(workspaceRoot, path) {
|
|
|
2331
2690
|
hash: `sha256:${createHash("sha256").update(bytes).digest("hex")}`
|
|
2332
2691
|
};
|
|
2333
2692
|
}
|
|
2693
|
+
const taskTool = defineTool({
|
|
2694
|
+
name: "task",
|
|
2695
|
+
description: "Delegate a focused prompt to a constrained child agent session.",
|
|
2696
|
+
prompt: "task: delegate focused read-only research or isolated analysis to a child agent session. Use it when parallel context gathering would help. To use it, reply with only JSON: {\"tool\":\"task\",\"args\":{\"description\":\"Inspect runtime event flow\",\"prompt\":\"Read the runtime and summarize how events are emitted.\",\"subagent_type\":\"explore\"}}",
|
|
2697
|
+
argsSchema: z.object({
|
|
2698
|
+
description: z.string().min(1),
|
|
2699
|
+
prompt: z.string().min(1),
|
|
2700
|
+
subagent_type: z.string().optional(),
|
|
2701
|
+
task_id: z.string().optional()
|
|
2702
|
+
}),
|
|
2703
|
+
async execute(context, args) {
|
|
2704
|
+
if (!context.subagents) throw new Error("task requires a runtime subagent manager.");
|
|
2705
|
+
const result = await context.subagents.runTask({
|
|
2706
|
+
description: args.description,
|
|
2707
|
+
prompt: args.prompt,
|
|
2708
|
+
subagentType: args.subagent_type,
|
|
2709
|
+
taskId: args.task_id,
|
|
2710
|
+
parentToolCallId: context.toolCallId ?? args.task_id ?? "task",
|
|
2711
|
+
eventSink: context.eventSink,
|
|
2712
|
+
abortSignal: context.abortSignal
|
|
2713
|
+
});
|
|
2714
|
+
return {
|
|
2715
|
+
tool: "task",
|
|
2716
|
+
childSessionId: result.sessionId,
|
|
2717
|
+
status: result.status,
|
|
2718
|
+
profileId: result.profileId,
|
|
2719
|
+
content: [
|
|
2720
|
+
`Task ${result.status}: ${args.description}`,
|
|
2721
|
+
`child_session: ${result.sessionId}`,
|
|
2722
|
+
`profile: ${result.profileId}`,
|
|
2723
|
+
"",
|
|
2724
|
+
result.result
|
|
2725
|
+
].join("\n")
|
|
2726
|
+
};
|
|
2727
|
+
}
|
|
2728
|
+
});
|
|
2334
2729
|
const writeFileTool = defineTool({
|
|
2335
2730
|
name: "write_file",
|
|
2336
|
-
description: "Create a new UTF-8 file inside the workspace, or explicitly replace one
|
|
2337
|
-
prompt: "write_file: create a new UTF-8 file inside the workspace by default; use edit_file for targeted changes to existing files
|
|
2731
|
+
description: "Create a new UTF-8 file inside the workspace, or explicitly replace one. For overwrite:true, expected_current_hash must be the current/pre-write hash from read_file, never a predicted post-write hash.",
|
|
2732
|
+
prompt: "write_file: create a new UTF-8 file inside the workspace by default; use edit_file for targeted changes to existing files; pass create_parent_dirs:true only when creating the folder path is intended. Replace an existing whole file only with overwrite:true and expected_current_hash set to the current/pre-write hash returned by the latest read_file for that file; never invent it or use a predicted after-write hash. To create a file, reply with only JSON: {\"tool\":\"write_file\",\"args\":{\"path\":\"test/example.test.ts\",\"content\":\"import { it, expect } from \\\"vitest\\\";\\n\\nit(\\\"works\\\", () => {\\n expect(true).toBe(true);\\n});\\n\",\"create_parent_dirs\":true}}",
|
|
2338
2733
|
argsSchema: z.object({
|
|
2339
|
-
path: z.string(),
|
|
2340
|
-
content: z.string(),
|
|
2341
|
-
create_parent_dirs: z.boolean().optional(),
|
|
2342
|
-
overwrite: z.boolean().optional(),
|
|
2343
|
-
|
|
2734
|
+
path: z.string().describe("Workspace-relative path to write."),
|
|
2735
|
+
content: z.string().describe("Complete UTF-8 file content to write."),
|
|
2736
|
+
create_parent_dirs: z.boolean().optional().describe("Create missing parent directories only when that is explicitly intended."),
|
|
2737
|
+
overwrite: z.boolean().optional().describe("Set true only for intentional whole-file replacement."),
|
|
2738
|
+
expected_current_hash: z.string().optional().describe("Required with overwrite:true. Use the current file hash returned by the latest read_file result for this file. This is checked before writing and is not the hash after the write.")
|
|
2344
2739
|
}),
|
|
2345
2740
|
execute: (context, args) => writeWorkspaceFile(context.workspaceRoot, args, { logger: context.logger })
|
|
2346
2741
|
});
|
|
@@ -2396,12 +2791,12 @@ async function writeWorkspaceFile(workspaceRoot, args, options = {}) {
|
|
|
2396
2791
|
});
|
|
2397
2792
|
}
|
|
2398
2793
|
async function overwriteExistingFile(workspaceRoot, scopedPath, args, existingTarget, options) {
|
|
2399
|
-
if (!args.
|
|
2794
|
+
if (!args.expected_current_hash) throw new Error(`write_file overwrite requires expected_current_hash for ${scopedPath.relativePath}.`);
|
|
2400
2795
|
if (!existingTarget) throw new Error(`write_file overwrite requires an existing file: ${scopedPath.relativePath}`);
|
|
2401
2796
|
if (!existingTarget.isFile()) throw new Error(`write_file overwrite requires a regular file: ${scopedPath.relativePath}`);
|
|
2402
2797
|
const beforeBytes = await readFile(scopedPath.path);
|
|
2403
2798
|
const beforeHash = hashBytes(beforeBytes);
|
|
2404
|
-
if (args.
|
|
2799
|
+
if (args.expected_current_hash !== beforeHash) throw new Error(`write_file expected_current_hash did not match ${scopedPath.relativePath}.`);
|
|
2405
2800
|
const beforeLineCount = countLogicalLines$1(decodeUtf8(scopedPath.relativePath, beforeBytes));
|
|
2406
2801
|
const afterBytes = encodeUtf8Text(args.content);
|
|
2407
2802
|
const afterHash = hashBytes(afterBytes);
|
|
@@ -2543,6 +2938,8 @@ function isNodeError$2(error) {
|
|
|
2543
2938
|
//#endregion
|
|
2544
2939
|
//#region src/agent/tools/registry.ts
|
|
2545
2940
|
const toolRegistry = {
|
|
2941
|
+
[taskTool.name]: taskTool,
|
|
2942
|
+
[planTodoTool.name]: planTodoTool,
|
|
2546
2943
|
[readFileTool.name]: readFileTool,
|
|
2547
2944
|
[listFilesTool.name]: listFilesTool,
|
|
2548
2945
|
[grepTool.name]: grepTool,
|
|
@@ -2562,8 +2959,16 @@ function isToolName(name) {
|
|
|
2562
2959
|
function getToolDefinition(name) {
|
|
2563
2960
|
return toolRegistry[name];
|
|
2564
2961
|
}
|
|
2565
|
-
function getToolPromptLines() {
|
|
2566
|
-
return
|
|
2962
|
+
function getToolPromptLines(filter) {
|
|
2963
|
+
return getToolDefinitionsForPermissions(filter).map((tool) => tool.prompt);
|
|
2964
|
+
}
|
|
2965
|
+
function getToolDefinitionsForPermissions(filter) {
|
|
2966
|
+
return Object.entries(toolRegistry).filter(([name]) => filter?.(name) ?? true).map(([, tool]) => tool);
|
|
2967
|
+
}
|
|
2968
|
+
function isParallelSafeToolName(name) {
|
|
2969
|
+
if (!isToolName(name)) return false;
|
|
2970
|
+
const definition = toolRegistry[name];
|
|
2971
|
+
return Boolean(definition.parallelSafe && !definition.mutatesWorkspace && !definition.requiresExclusiveWorkspace);
|
|
2567
2972
|
}
|
|
2568
2973
|
//#endregion
|
|
2569
2974
|
//#region src/agent/tools/xml-parser.ts
|
|
@@ -2652,7 +3057,7 @@ function parseToolCallWithSource(text, allowedSources = ["text-json", "text-xml"
|
|
|
2652
3057
|
if (allowedSources.includes("text-json")) {
|
|
2653
3058
|
const json = parseJsonToolCall(text);
|
|
2654
3059
|
if (json) return {
|
|
2655
|
-
|
|
3060
|
+
...json,
|
|
2656
3061
|
source: "text-json"
|
|
2657
3062
|
};
|
|
2658
3063
|
}
|
|
@@ -2660,7 +3065,8 @@ function parseToolCallWithSource(text, allowedSources = ["text-json", "text-xml"
|
|
|
2660
3065
|
const xml = parseXmlToolCall(text);
|
|
2661
3066
|
if (xml) return {
|
|
2662
3067
|
call: xml,
|
|
2663
|
-
source: "text-xml"
|
|
3068
|
+
source: "text-xml",
|
|
3069
|
+
remainder: ""
|
|
2664
3070
|
};
|
|
2665
3071
|
}
|
|
2666
3072
|
}
|
|
@@ -2675,12 +3081,16 @@ function parseNativeToolCall(toolName, args) {
|
|
|
2675
3081
|
};
|
|
2676
3082
|
}
|
|
2677
3083
|
function parseJsonToolCall(text) {
|
|
2678
|
-
const
|
|
3084
|
+
const { json, remainder } = extractToolJsonCandidate(stripJsonFence(text.trim()));
|
|
2679
3085
|
let value;
|
|
2680
3086
|
try {
|
|
2681
|
-
value = JSON.parse(
|
|
3087
|
+
value = JSON.parse(json);
|
|
2682
3088
|
} catch {
|
|
2683
|
-
|
|
3089
|
+
try {
|
|
3090
|
+
value = JSON.parse(escapeControlCharactersInJsonStrings(json));
|
|
3091
|
+
} catch {
|
|
3092
|
+
return;
|
|
3093
|
+
}
|
|
2684
3094
|
}
|
|
2685
3095
|
if (!isRecord$1(value) || typeof value.tool !== "string") return;
|
|
2686
3096
|
if (!isToolName(value.tool)) return;
|
|
@@ -2688,17 +3098,29 @@ function parseJsonToolCall(text) {
|
|
|
2688
3098
|
const parsed = definition.argsSchema.safeParse(value.args);
|
|
2689
3099
|
if (!parsed.success) return;
|
|
2690
3100
|
return {
|
|
2691
|
-
|
|
2692
|
-
|
|
3101
|
+
call: {
|
|
3102
|
+
tool: definition.name,
|
|
3103
|
+
args: parsed.data
|
|
3104
|
+
},
|
|
3105
|
+
remainder: remainder.trim()
|
|
2693
3106
|
};
|
|
2694
3107
|
}
|
|
2695
3108
|
function stripJsonFence(text) {
|
|
2696
3109
|
return text.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i)?.[1] ?? text;
|
|
2697
3110
|
}
|
|
2698
3111
|
function extractToolJsonCandidate(text) {
|
|
2699
|
-
if (!text.startsWith("{")) return
|
|
3112
|
+
if (!text.startsWith("{")) return {
|
|
3113
|
+
json: text,
|
|
3114
|
+
remainder: ""
|
|
3115
|
+
};
|
|
2700
3116
|
const endIndex = findJsonObjectEnd(text);
|
|
2701
|
-
return endIndex === void 0 ?
|
|
3117
|
+
return endIndex === void 0 ? {
|
|
3118
|
+
json: text,
|
|
3119
|
+
remainder: ""
|
|
3120
|
+
} : {
|
|
3121
|
+
json: text.slice(0, endIndex + 1),
|
|
3122
|
+
remainder: text.slice(endIndex + 1)
|
|
3123
|
+
};
|
|
2702
3124
|
}
|
|
2703
3125
|
function findJsonObjectEnd(text) {
|
|
2704
3126
|
let depth = 0;
|
|
@@ -2720,6 +3142,39 @@ function findJsonObjectEnd(text) {
|
|
|
2720
3142
|
}
|
|
2721
3143
|
}
|
|
2722
3144
|
}
|
|
3145
|
+
function escapeControlCharactersInJsonStrings(text) {
|
|
3146
|
+
let result = "";
|
|
3147
|
+
let inString = false;
|
|
3148
|
+
let escaped = false;
|
|
3149
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
3150
|
+
const char = text[index];
|
|
3151
|
+
if (!inString) {
|
|
3152
|
+
result += char;
|
|
3153
|
+
if (char === "\"") inString = true;
|
|
3154
|
+
continue;
|
|
3155
|
+
}
|
|
3156
|
+
if (escaped) {
|
|
3157
|
+
result += char;
|
|
3158
|
+
escaped = false;
|
|
3159
|
+
continue;
|
|
3160
|
+
}
|
|
3161
|
+
if (char === "\\") {
|
|
3162
|
+
result += char;
|
|
3163
|
+
escaped = true;
|
|
3164
|
+
continue;
|
|
3165
|
+
}
|
|
3166
|
+
if (char === "\"") {
|
|
3167
|
+
result += char;
|
|
3168
|
+
inString = false;
|
|
3169
|
+
continue;
|
|
3170
|
+
}
|
|
3171
|
+
if (char === "\n") result += "\\n";
|
|
3172
|
+
else if (char === "\r") result += "\\r";
|
|
3173
|
+
else if (char === " ") result += "\\t";
|
|
3174
|
+
else result += char;
|
|
3175
|
+
}
|
|
3176
|
+
return result;
|
|
3177
|
+
}
|
|
2723
3178
|
function isRecord$1(value) {
|
|
2724
3179
|
return typeof value === "object" && value !== null;
|
|
2725
3180
|
}
|
|
@@ -2769,25 +3224,41 @@ var ModelGateway = class ModelGateway {
|
|
|
2769
3224
|
}
|
|
2770
3225
|
async generateText(request) {
|
|
2771
3226
|
const resolved = this.resolveModel(request.purpose);
|
|
3227
|
+
const result = await generateText({
|
|
3228
|
+
model: resolved.model,
|
|
3229
|
+
system: request.system,
|
|
3230
|
+
prompt: request.prompt,
|
|
3231
|
+
providerOptions: buildProviderOptions(resolved.providerId, resolved.providerConfig),
|
|
3232
|
+
abortSignal: request.abortSignal
|
|
3233
|
+
});
|
|
3234
|
+
const usage = normalizeUsage(result.usage, {
|
|
3235
|
+
providerId: resolved.providerId,
|
|
3236
|
+
providerConfig: resolved.providerConfig,
|
|
3237
|
+
responseBody: result.response.body
|
|
3238
|
+
});
|
|
2772
3239
|
return {
|
|
2773
|
-
text:
|
|
2774
|
-
model: resolved.model,
|
|
2775
|
-
system: request.system,
|
|
2776
|
-
prompt: request.prompt,
|
|
2777
|
-
abortSignal: request.abortSignal
|
|
2778
|
-
})).text,
|
|
3240
|
+
text: result.text,
|
|
2779
3241
|
providerId: resolved.providerId,
|
|
2780
3242
|
modelId: resolved.modelId,
|
|
2781
|
-
purpose: resolved.purpose
|
|
3243
|
+
purpose: resolved.purpose,
|
|
3244
|
+
...usage ? { usage } : {}
|
|
2782
3245
|
};
|
|
2783
3246
|
}
|
|
2784
3247
|
async generateAgentStep(request) {
|
|
2785
3248
|
const resolved = this.resolveModel(request.purpose ?? "agent.primary");
|
|
2786
3249
|
const override = request.toolProtocol ?? resolved.modelConfig.toolProtocol ?? resolved.providerConfig.toolProtocol ?? "auto";
|
|
2787
3250
|
const attempts = [];
|
|
3251
|
+
if (override === "auto" && shouldUseTextProtocolForOpenRouterStreaming(request, resolved)) {
|
|
3252
|
+
attempts.push({
|
|
3253
|
+
protocol: "native-openai-compatible",
|
|
3254
|
+
status: "skipped",
|
|
3255
|
+
reason: "openrouter streaming auto uses text-json"
|
|
3256
|
+
});
|
|
3257
|
+
return this.generateTextAgentStep(request, resolved, attempts, "openrouter streaming auto uses text JSON protocol", false, ["text-json"]);
|
|
3258
|
+
}
|
|
2788
3259
|
if (override === "native" || override === "auto") try {
|
|
2789
3260
|
const result = await this.generateNativeAgentStep(request, resolved, attempts);
|
|
2790
|
-
if (result.toolCalls.length > 0
|
|
3261
|
+
if (result.toolCalls.length > 0) return result;
|
|
2791
3262
|
const parsedTextCall = parseToolCallWithSource(result.text);
|
|
2792
3263
|
if (parsedTextCall) {
|
|
2793
3264
|
const fallbackProtocol = parsedTextCall.source === "text-xml" ? "text-xml" : "text-json";
|
|
@@ -2811,7 +3282,7 @@ var ModelGateway = class ModelGateway {
|
|
|
2811
3282
|
}
|
|
2812
3283
|
return result;
|
|
2813
3284
|
} catch (error) {
|
|
2814
|
-
const reason = formatErrorMessage$
|
|
3285
|
+
const reason = formatErrorMessage$2(error);
|
|
2815
3286
|
attempts.push({
|
|
2816
3287
|
protocol: "native-openai-compatible",
|
|
2817
3288
|
status: "failed",
|
|
@@ -2828,17 +3299,19 @@ var ModelGateway = class ModelGateway {
|
|
|
2828
3299
|
return this.generateTextAgentStep(request, resolved, attempts, override === "text-xml" ? "forced text XML protocol" : "forced text JSON protocol", false, override === "text-xml" ? ["text-xml"] : ["text-json"]);
|
|
2829
3300
|
}
|
|
2830
3301
|
async *streamText(request) {
|
|
3302
|
+
const resolved = this.resolveModel(request.purpose);
|
|
2831
3303
|
yield* streamText({
|
|
2832
|
-
model:
|
|
3304
|
+
model: resolved.model,
|
|
2833
3305
|
system: request.system,
|
|
2834
3306
|
prompt: request.prompt,
|
|
3307
|
+
providerOptions: buildProviderOptions(resolved.providerId, resolved.providerConfig),
|
|
2835
3308
|
abortSignal: request.abortSignal
|
|
2836
3309
|
}).textStream;
|
|
2837
3310
|
}
|
|
2838
3311
|
async generateNativeAgentStep(request, resolved, attempts) {
|
|
2839
3312
|
const providerOptions = buildNativeProviderOptions(resolved.providerId, resolved.providerConfig);
|
|
2840
3313
|
const openRouterRoutingApplied = hasOpenRouterRoutingOptions(providerOptions, resolved.providerId);
|
|
2841
|
-
const result = await generateText({
|
|
3314
|
+
const result = request.onReasoning ? await this.streamNativeAgentStep(request, resolved, providerOptions) : await generateText({
|
|
2842
3315
|
model: resolved.model,
|
|
2843
3316
|
system: request.system,
|
|
2844
3317
|
prompt: request.prompt,
|
|
@@ -2847,6 +3320,11 @@ var ModelGateway = class ModelGateway {
|
|
|
2847
3320
|
providerOptions,
|
|
2848
3321
|
abortSignal: request.abortSignal
|
|
2849
3322
|
});
|
|
3323
|
+
const usage = normalizeUsage(result.usage, {
|
|
3324
|
+
providerId: resolved.providerId,
|
|
3325
|
+
providerConfig: resolved.providerConfig,
|
|
3326
|
+
responseBody: result.response.body
|
|
3327
|
+
});
|
|
2850
3328
|
const toolCalls = result.toolCalls.map((call, index) => {
|
|
2851
3329
|
const parsed = parseNativeToolCall(call.toolName, call.input);
|
|
2852
3330
|
if (!parsed) throw new Error(`Native tool call for ${call.toolName} did not match the registered schema.`);
|
|
@@ -2866,6 +3344,7 @@ var ModelGateway = class ModelGateway {
|
|
|
2866
3344
|
providerId: resolved.providerId,
|
|
2867
3345
|
modelId: resolved.modelId,
|
|
2868
3346
|
purpose: resolved.purpose,
|
|
3347
|
+
...usage ? { usage } : {},
|
|
2869
3348
|
toolCalls,
|
|
2870
3349
|
toolProtocol: "native-openai-compatible",
|
|
2871
3350
|
protocolAttempts: attempts,
|
|
@@ -2875,12 +3354,18 @@ var ModelGateway = class ModelGateway {
|
|
|
2875
3354
|
};
|
|
2876
3355
|
}
|
|
2877
3356
|
async generateTextAgentStep(request, resolved, attempts, fallbackReason, providerRejectedTools, allowedSources) {
|
|
2878
|
-
const result = await generateText({
|
|
3357
|
+
const result = request.onReasoning ? await this.streamTextAgentStep(request, resolved) : await generateText({
|
|
2879
3358
|
model: resolved.model,
|
|
2880
3359
|
system: request.system,
|
|
2881
3360
|
prompt: request.prompt,
|
|
3361
|
+
providerOptions: buildProviderOptions(resolved.providerId, resolved.providerConfig),
|
|
2882
3362
|
abortSignal: request.abortSignal
|
|
2883
3363
|
});
|
|
3364
|
+
const usage = normalizeUsage(result.usage, {
|
|
3365
|
+
providerId: resolved.providerId,
|
|
3366
|
+
providerConfig: resolved.providerConfig,
|
|
3367
|
+
responseBody: result.response.body
|
|
3368
|
+
});
|
|
2884
3369
|
const parsed = parseToolCallWithSource(result.text, allowedSources);
|
|
2885
3370
|
const defaultProtocol = allowedSources.length === 1 && allowedSources[0] === "text-xml" ? "text-xml" : "text-json";
|
|
2886
3371
|
const toolProtocol = parsed?.source === "text-xml" ? "text-xml" : parsed ? "text-json" : defaultProtocol;
|
|
@@ -2894,6 +3379,7 @@ var ModelGateway = class ModelGateway {
|
|
|
2894
3379
|
providerId: resolved.providerId,
|
|
2895
3380
|
modelId: resolved.modelId,
|
|
2896
3381
|
purpose: resolved.purpose,
|
|
3382
|
+
...usage ? { usage } : {},
|
|
2897
3383
|
toolCalls: parsed ? [{
|
|
2898
3384
|
id: `${parsed.source}-0`,
|
|
2899
3385
|
tool: parsed.call.tool,
|
|
@@ -2908,35 +3394,209 @@ var ModelGateway = class ModelGateway {
|
|
|
2908
3394
|
openRouterRoutingApplied: false
|
|
2909
3395
|
};
|
|
2910
3396
|
}
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
}
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
3397
|
+
async streamNativeAgentStep(request, resolved, providerOptions) {
|
|
3398
|
+
const result = streamText({
|
|
3399
|
+
model: resolved.model,
|
|
3400
|
+
system: request.system,
|
|
3401
|
+
prompt: request.prompt,
|
|
3402
|
+
tools: toAiSdkToolSet(request.tools),
|
|
3403
|
+
toolChoice: "auto",
|
|
3404
|
+
providerOptions,
|
|
3405
|
+
abortSignal: request.abortSignal,
|
|
3406
|
+
includeRawChunks: true
|
|
3407
|
+
});
|
|
3408
|
+
guardStreamTextResultRejections(result);
|
|
3409
|
+
let rawUsageBody;
|
|
3410
|
+
let sawReasoningDelta = false;
|
|
3411
|
+
let reasoningText;
|
|
3412
|
+
let text;
|
|
3413
|
+
let toolCalls;
|
|
3414
|
+
let usage;
|
|
3415
|
+
let warnings;
|
|
3416
|
+
let response;
|
|
3417
|
+
try {
|
|
3418
|
+
({rawUsageBody, sawReasoningDelta} = await consumeReasoningStream(result.fullStream, request.onReasoning));
|
|
3419
|
+
[text, toolCalls, usage, warnings, response, reasoningText] = await Promise.all([
|
|
3420
|
+
result.text,
|
|
3421
|
+
result.toolCalls,
|
|
3422
|
+
result.usage,
|
|
3423
|
+
result.warnings,
|
|
3424
|
+
result.response,
|
|
3425
|
+
result.reasoningText
|
|
3426
|
+
]);
|
|
3427
|
+
} catch (error) {
|
|
3428
|
+
await settleRejectedStreamTextResult(result);
|
|
3429
|
+
throw error;
|
|
3430
|
+
}
|
|
3431
|
+
await emitReasoningSummary(request.onReasoning, sawReasoningDelta, reasoningText);
|
|
3432
|
+
return {
|
|
3433
|
+
text,
|
|
3434
|
+
toolCalls,
|
|
3435
|
+
usage,
|
|
3436
|
+
warnings,
|
|
3437
|
+
response: withRawUsageBody(response, rawUsageBody)
|
|
3438
|
+
};
|
|
3439
|
+
}
|
|
3440
|
+
async streamTextAgentStep(request, resolved) {
|
|
3441
|
+
const result = streamText({
|
|
3442
|
+
model: resolved.model,
|
|
3443
|
+
system: request.system,
|
|
3444
|
+
prompt: request.prompt,
|
|
3445
|
+
providerOptions: buildProviderOptions(resolved.providerId, resolved.providerConfig),
|
|
3446
|
+
abortSignal: request.abortSignal,
|
|
3447
|
+
includeRawChunks: true
|
|
3448
|
+
});
|
|
3449
|
+
guardStreamTextResultRejections(result);
|
|
3450
|
+
let rawUsageBody;
|
|
3451
|
+
let sawReasoningDelta = false;
|
|
3452
|
+
let reasoningText;
|
|
3453
|
+
let text;
|
|
3454
|
+
let usage;
|
|
3455
|
+
let warnings;
|
|
3456
|
+
let response;
|
|
3457
|
+
try {
|
|
3458
|
+
({rawUsageBody, sawReasoningDelta} = await consumeReasoningStream(result.fullStream, request.onReasoning));
|
|
3459
|
+
[text, usage, warnings, response, reasoningText] = await Promise.all([
|
|
3460
|
+
result.text,
|
|
3461
|
+
result.usage,
|
|
3462
|
+
result.warnings,
|
|
3463
|
+
result.response,
|
|
3464
|
+
result.reasoningText
|
|
3465
|
+
]);
|
|
3466
|
+
} catch (error) {
|
|
3467
|
+
await settleRejectedStreamTextResult(result);
|
|
3468
|
+
throw error;
|
|
3469
|
+
}
|
|
3470
|
+
await emitReasoningSummary(request.onReasoning, sawReasoningDelta, reasoningText);
|
|
3471
|
+
return {
|
|
3472
|
+
text,
|
|
3473
|
+
usage,
|
|
3474
|
+
warnings,
|
|
3475
|
+
response: withRawUsageBody(response, rawUsageBody)
|
|
3476
|
+
};
|
|
3477
|
+
}
|
|
3478
|
+
};
|
|
3479
|
+
function guardStreamTextResultRejections(result) {
|
|
3480
|
+
for (const promise of [
|
|
3481
|
+
result.text,
|
|
3482
|
+
result.toolCalls,
|
|
3483
|
+
result.usage,
|
|
3484
|
+
result.warnings,
|
|
3485
|
+
result.response,
|
|
3486
|
+
result.reasoningText
|
|
3487
|
+
]) if (promise) Promise.resolve(promise).catch(() => {});
|
|
3488
|
+
}
|
|
3489
|
+
async function settleRejectedStreamTextResult(result) {
|
|
3490
|
+
await Promise.allSettled([
|
|
3491
|
+
result.text,
|
|
3492
|
+
result.toolCalls,
|
|
3493
|
+
result.usage,
|
|
3494
|
+
result.warnings,
|
|
3495
|
+
result.response,
|
|
3496
|
+
result.reasoningText
|
|
3497
|
+
].filter((promise) => promise !== void 0).map((promise) => Promise.resolve(promise)));
|
|
3498
|
+
}
|
|
3499
|
+
async function consumeReasoningStream(stream, onReasoning) {
|
|
3500
|
+
let sawReasoningDelta = false;
|
|
3501
|
+
let rawUsageBody;
|
|
3502
|
+
for await (const part of stream) {
|
|
3503
|
+
if (!part || typeof part !== "object") continue;
|
|
3504
|
+
const typedPart = part;
|
|
3505
|
+
if (typedPart.type === "error") throw typedPart.error;
|
|
3506
|
+
if (typedPart.type === "raw" && hasUsageCostBody(typedPart.rawValue)) {
|
|
3507
|
+
rawUsageBody = typedPart.rawValue;
|
|
3508
|
+
continue;
|
|
3509
|
+
}
|
|
3510
|
+
if (typedPart.type !== "reasoning-delta") continue;
|
|
3511
|
+
const text = typeof typedPart.text === "string" ? typedPart.text : typedPart.delta;
|
|
3512
|
+
if (typeof text !== "string" || text.trim().length === 0) continue;
|
|
3513
|
+
sawReasoningDelta = true;
|
|
3514
|
+
await onReasoning?.({
|
|
3515
|
+
type: "delta",
|
|
3516
|
+
text
|
|
3517
|
+
});
|
|
3518
|
+
}
|
|
3519
|
+
return rawUsageBody === void 0 ? { sawReasoningDelta } : {
|
|
3520
|
+
sawReasoningDelta,
|
|
3521
|
+
rawUsageBody
|
|
3522
|
+
};
|
|
3523
|
+
}
|
|
3524
|
+
async function emitReasoningSummary(onReasoning, sawReasoningDelta, reasoningText) {
|
|
3525
|
+
if (sawReasoningDelta || !reasoningText || reasoningText.trim().length === 0) return;
|
|
3526
|
+
await onReasoning?.({
|
|
3527
|
+
type: "summary",
|
|
3528
|
+
text: reasoningText
|
|
3529
|
+
});
|
|
3530
|
+
}
|
|
3531
|
+
function hasUsageCostBody(value) {
|
|
3532
|
+
return Boolean(value && typeof value === "object" && "usage" in value && value.usage && typeof value.usage === "object");
|
|
3533
|
+
}
|
|
3534
|
+
function withRawUsageBody(response, rawUsageBody) {
|
|
3535
|
+
return rawUsageBody === void 0 ? response : {
|
|
3536
|
+
...response,
|
|
3537
|
+
body: rawUsageBody
|
|
3538
|
+
};
|
|
3539
|
+
}
|
|
3540
|
+
function resolveApiKey(config) {
|
|
3541
|
+
if (config.apiKey !== void 0) return config.apiKey;
|
|
3542
|
+
if (config.apiKeyEnv === void 0) return;
|
|
3543
|
+
return process.env[config.apiKeyEnv];
|
|
3544
|
+
}
|
|
3545
|
+
function buildProviderOptions(providerId, config) {
|
|
3546
|
+
const options = {};
|
|
3547
|
+
if (config.service_tier !== void 0) options.service_tier = config.service_tier;
|
|
3548
|
+
return { [providerId]: options };
|
|
3549
|
+
}
|
|
3550
|
+
function buildNativeProviderOptions(providerId, config) {
|
|
3551
|
+
const options = {
|
|
3552
|
+
...buildProviderOptions(providerId, config)[providerId],
|
|
3553
|
+
parallel_tool_calls: false
|
|
3554
|
+
};
|
|
3555
|
+
if (shouldApplyOpenRouterRoutingOptions(providerId, config)) options.provider = { require_parameters: true };
|
|
3556
|
+
return { [providerId]: options };
|
|
3557
|
+
}
|
|
3558
|
+
function shouldApplyOpenRouterRoutingOptions(providerId, config) {
|
|
3559
|
+
if (config.openRouterToolRouting === "force") return true;
|
|
3560
|
+
if (config.openRouterToolRouting === "off") return false;
|
|
3561
|
+
return isOpenRouterProvider$1(providerId, config);
|
|
3562
|
+
}
|
|
3563
|
+
function isOpenRouterProvider$1(providerId, config) {
|
|
3564
|
+
return providerId.toLowerCase().includes("openrouter") || config.baseURL.toLowerCase().includes("openrouter.ai");
|
|
3565
|
+
}
|
|
3566
|
+
function shouldUseTextProtocolForOpenRouterStreaming(request, resolved) {
|
|
3567
|
+
return request.onReasoning !== void 0 && isOpenRouterProvider$1(resolved.providerId, resolved.providerConfig);
|
|
3568
|
+
}
|
|
3569
|
+
function hasOpenRouterRoutingOptions(providerOptions, providerId) {
|
|
3570
|
+
const options = providerOptions[providerId];
|
|
3571
|
+
return Boolean(options && typeof options === "object" && "provider" in options);
|
|
2930
3572
|
}
|
|
2931
3573
|
function isNativeToolFallbackError(error) {
|
|
2932
|
-
const message = formatErrorMessage$
|
|
3574
|
+
const message = formatErrorMessage$2(error).toLowerCase();
|
|
2933
3575
|
return message.includes("tool") || message.includes("function") || message.includes("parallel_tool_calls") || message.includes("tool_choice") || message.includes("requested parameters") || message.includes("provider routing") || message.includes("provider-selection") || message.includes("invalid request");
|
|
2934
3576
|
}
|
|
2935
3577
|
function extractWarningMessages(warnings) {
|
|
2936
3578
|
if (!Array.isArray(warnings)) return [];
|
|
2937
|
-
return warnings.map((warning) => formatErrorMessage$
|
|
3579
|
+
return warnings.map((warning) => formatErrorMessage$2(warning));
|
|
3580
|
+
}
|
|
3581
|
+
function normalizeUsage(usage, context) {
|
|
3582
|
+
const costUsd = context && isOpenRouterProvider$1(context.providerId, context.providerConfig) ? extractOpenRouterCost(context.responseBody) : void 0;
|
|
3583
|
+
if (!usage) return costUsd === void 0 ? void 0 : { costUsd };
|
|
3584
|
+
const normalized = {
|
|
3585
|
+
...typeof usage.inputTokens === "number" ? { inputTokens: usage.inputTokens } : {},
|
|
3586
|
+
...typeof usage.outputTokens === "number" ? { outputTokens: usage.outputTokens } : {},
|
|
3587
|
+
...typeof usage.totalTokens === "number" ? { totalTokens: usage.totalTokens } : {},
|
|
3588
|
+
...costUsd === void 0 ? {} : { costUsd }
|
|
3589
|
+
};
|
|
3590
|
+
return Object.keys(normalized).length > 0 ? normalized : void 0;
|
|
2938
3591
|
}
|
|
2939
|
-
function
|
|
3592
|
+
function extractOpenRouterCost(responseBody) {
|
|
3593
|
+
if (!responseBody || typeof responseBody !== "object") return;
|
|
3594
|
+
const usage = responseBody.usage;
|
|
3595
|
+
if (!usage || typeof usage !== "object") return;
|
|
3596
|
+
const cost = usage.cost;
|
|
3597
|
+
return typeof cost === "number" && Number.isFinite(cost) ? cost : void 0;
|
|
3598
|
+
}
|
|
3599
|
+
function formatErrorMessage$2(error) {
|
|
2940
3600
|
return error instanceof Error ? error.message : String(error);
|
|
2941
3601
|
}
|
|
2942
3602
|
//#endregion
|
|
@@ -2946,8 +3606,6 @@ const modelPurposeSchema = z.enum([
|
|
|
2946
3606
|
"agent.fast",
|
|
2947
3607
|
"kb.scan",
|
|
2948
3608
|
"kb.summarize",
|
|
2949
|
-
"kb.extract",
|
|
2950
|
-
"kb.embed",
|
|
2951
3609
|
"fallback"
|
|
2952
3610
|
]);
|
|
2953
3611
|
const modelPurposes = modelPurposeSchema.options;
|
|
@@ -2957,6 +3615,10 @@ const toolProtocolSchema = z.enum([
|
|
|
2957
3615
|
"text-json",
|
|
2958
3616
|
"text-xml"
|
|
2959
3617
|
]);
|
|
3618
|
+
const openRouterAttributionHeaders = {
|
|
3619
|
+
"HTTP-Referer": "https://topchester.com",
|
|
3620
|
+
"X-Title": "Topchester"
|
|
3621
|
+
};
|
|
2960
3622
|
const providerSchema = z.object({
|
|
2961
3623
|
type: z.literal("openai-compatible"),
|
|
2962
3624
|
baseURL: z.string().url(),
|
|
@@ -2964,6 +3626,7 @@ const providerSchema = z.object({
|
|
|
2964
3626
|
apiKey: z.string().optional(),
|
|
2965
3627
|
headers: z.record(z.string(), z.string()).optional(),
|
|
2966
3628
|
supportsStructuredOutputs: z.boolean().optional(),
|
|
3629
|
+
service_tier: z.enum(["flex", "priority"]).optional(),
|
|
2967
3630
|
toolProtocol: toolProtocolSchema.optional(),
|
|
2968
3631
|
openRouterToolRouting: z.enum([
|
|
2969
3632
|
"auto",
|
|
@@ -3042,7 +3705,7 @@ function readConfigFile(path) {
|
|
|
3042
3705
|
try {
|
|
3043
3706
|
return parse(readFileSync(path, "utf8"));
|
|
3044
3707
|
} catch (error) {
|
|
3045
|
-
throw new Error(`Invalid Topchester config at ${path}: ${formatErrorMessage(error)}`);
|
|
3708
|
+
throw new Error(`Invalid Topchester config at ${path}: ${formatErrorMessage$1(error)}`);
|
|
3046
3709
|
}
|
|
3047
3710
|
}
|
|
3048
3711
|
function parseConfigFile(path, value) {
|
|
@@ -3078,6 +3741,7 @@ function normalizeConfigInput(value) {
|
|
|
3078
3741
|
ensureKnownProvider(providers, kbSummarizeModelRef.provider);
|
|
3079
3742
|
delete models["kb.summarize"];
|
|
3080
3743
|
}
|
|
3744
|
+
applyKnownProviderDefaults(providers);
|
|
3081
3745
|
return {
|
|
3082
3746
|
...value,
|
|
3083
3747
|
models: {
|
|
@@ -3128,12 +3792,30 @@ function ensureKnownProvider(providers, provider) {
|
|
|
3128
3792
|
baseURL: "https://openrouter.ai/api/v1",
|
|
3129
3793
|
apiKeyEnv: "OPENROUTER_API_KEY",
|
|
3130
3794
|
supportsStructuredOutputs: true,
|
|
3131
|
-
headers: {
|
|
3132
|
-
"HTTP-Referer": "https://topchester.com",
|
|
3133
|
-
"X-Title": "Topchester"
|
|
3134
|
-
}
|
|
3795
|
+
headers: { ...openRouterAttributionHeaders }
|
|
3135
3796
|
};
|
|
3136
3797
|
}
|
|
3798
|
+
function applyKnownProviderDefaults(providers) {
|
|
3799
|
+
for (const [providerId, provider] of Object.entries(providers)) {
|
|
3800
|
+
if (!isPlainObject(provider) || provider.type !== "openai-compatible" || typeof provider.baseURL !== "string") continue;
|
|
3801
|
+
if (isOpenAIProvider(providerId, provider.baseURL)) {
|
|
3802
|
+
provider.supportsStructuredOutputs ??= true;
|
|
3803
|
+
provider.toolProtocol ??= "native";
|
|
3804
|
+
}
|
|
3805
|
+
if (isOpenRouterProvider(providerId, provider.baseURL)) provider.headers = {
|
|
3806
|
+
...openRouterAttributionHeaders,
|
|
3807
|
+
...isPlainObject(provider.headers) ? provider.headers : {}
|
|
3808
|
+
};
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
function isOpenRouterProvider(providerId, baseURL) {
|
|
3812
|
+
return providerId.toLowerCase().includes("openrouter") || baseURL.toLowerCase().includes("openrouter.ai");
|
|
3813
|
+
}
|
|
3814
|
+
function isOpenAIProvider(providerId, baseURL) {
|
|
3815
|
+
const normalizedProvider = providerId.toLowerCase();
|
|
3816
|
+
const normalizedBaseURL = baseURL.toLowerCase();
|
|
3817
|
+
return normalizedProvider === "openai" || normalizedProvider === "gpt" || normalizedProvider.includes("openai") || normalizedBaseURL.includes("api.openai.com");
|
|
3818
|
+
}
|
|
3137
3819
|
function deepMerge(base, override, path = []) {
|
|
3138
3820
|
if (Array.isArray(base) && Array.isArray(override)) return path.join(".") === "ignore.paths" ? [...base, ...override] : override;
|
|
3139
3821
|
if (!isPlainObject(base) || !isPlainObject(override)) return override;
|
|
@@ -3147,7 +3829,7 @@ function isPlainObject(value) {
|
|
|
3147
3829
|
function formatZodIssue(issue) {
|
|
3148
3830
|
return `${issue.path.length > 0 ? issue.path.join(".") : "<root>"}: ${issue.message}`;
|
|
3149
3831
|
}
|
|
3150
|
-
function formatErrorMessage(error) {
|
|
3832
|
+
function formatErrorMessage$1(error) {
|
|
3151
3833
|
return error instanceof Error ? error.message : String(error);
|
|
3152
3834
|
}
|
|
3153
3835
|
//#endregion
|
|
@@ -3241,107 +3923,6 @@ function normalizeModelGatewayConfig(config) {
|
|
|
3241
3923
|
};
|
|
3242
3924
|
}
|
|
3243
3925
|
//#endregion
|
|
3244
|
-
//#region src/cli/ui.ts
|
|
3245
|
-
const colors = {
|
|
3246
|
-
bgSoftGray: "\x1B[48;5;236m",
|
|
3247
|
-
blue: "\x1B[34m",
|
|
3248
|
-
cyan: "\x1B[36m",
|
|
3249
|
-
darkGray: "\x1B[90m",
|
|
3250
|
-
dim: "\x1B[2m",
|
|
3251
|
-
green: "\x1B[32m",
|
|
3252
|
-
orange: "\x1B[38;5;208m",
|
|
3253
|
-
purple: "\x1B[35m",
|
|
3254
|
-
red: "\x1B[31m",
|
|
3255
|
-
reset: "\x1B[0m",
|
|
3256
|
-
resetForeground: "\x1B[39m",
|
|
3257
|
-
yellow: "\x1B[33m"
|
|
3258
|
-
};
|
|
3259
|
-
const ui = {
|
|
3260
|
-
heading(text) {
|
|
3261
|
-
return color(`Topchester ${text}`, "cyan");
|
|
3262
|
-
},
|
|
3263
|
-
label(text) {
|
|
3264
|
-
return color(text, "dim");
|
|
3265
|
-
},
|
|
3266
|
-
muted(text) {
|
|
3267
|
-
return color(text, "darkGray");
|
|
3268
|
-
},
|
|
3269
|
-
model(text) {
|
|
3270
|
-
return color(text, "blue");
|
|
3271
|
-
},
|
|
3272
|
-
modelInline(text) {
|
|
3273
|
-
if (!shouldUseColor()) return text;
|
|
3274
|
-
return `${colors.blue}${text}${colors.resetForeground}`;
|
|
3275
|
-
},
|
|
3276
|
-
ok(text) {
|
|
3277
|
-
return color(text, "green");
|
|
3278
|
-
},
|
|
3279
|
-
warn(text) {
|
|
3280
|
-
return color(text, "yellow");
|
|
3281
|
-
},
|
|
3282
|
-
error(text) {
|
|
3283
|
-
return color(text, "red");
|
|
3284
|
-
},
|
|
3285
|
-
softBackground(text) {
|
|
3286
|
-
return color(text, "bgSoftGray");
|
|
3287
|
-
},
|
|
3288
|
-
async spinner(text, action) {
|
|
3289
|
-
return withStatusLine(text, action, void 0, 80, false);
|
|
3290
|
-
},
|
|
3291
|
-
async progress(text, action) {
|
|
3292
|
-
let latest = text;
|
|
3293
|
-
return withStatusLine(text, () => action((message) => {
|
|
3294
|
-
latest = message;
|
|
3295
|
-
}), () => latest, 80, true);
|
|
3296
|
-
}
|
|
3297
|
-
};
|
|
3298
|
-
async function withStatusLine(text, action, getText = () => text, progressEveryMs = 80, emitPlainProgress = false) {
|
|
3299
|
-
if (!shouldUseColor()) {
|
|
3300
|
-
if (!emitPlainProgress) return action();
|
|
3301
|
-
const timer = setInterval(() => {
|
|
3302
|
-
stderr.write(`${getText()}\n`);
|
|
3303
|
-
}, Math.max(progressEveryMs, 5e3));
|
|
3304
|
-
try {
|
|
3305
|
-
return await action();
|
|
3306
|
-
} finally {
|
|
3307
|
-
clearInterval(timer);
|
|
3308
|
-
}
|
|
3309
|
-
}
|
|
3310
|
-
const frames = [
|
|
3311
|
-
"⠋",
|
|
3312
|
-
"⠙",
|
|
3313
|
-
"⠹",
|
|
3314
|
-
"⠸",
|
|
3315
|
-
"⠼",
|
|
3316
|
-
"⠴",
|
|
3317
|
-
"⠦",
|
|
3318
|
-
"⠧",
|
|
3319
|
-
"⠇",
|
|
3320
|
-
"⠏"
|
|
3321
|
-
];
|
|
3322
|
-
let index = 0;
|
|
3323
|
-
stderr.write(`${color(frames[index], "cyan")} ${getText()}`);
|
|
3324
|
-
const timer = setInterval(() => {
|
|
3325
|
-
index = (index + 1) % frames.length;
|
|
3326
|
-
stderr.write(`\r\u001b[2K${color(frames[index], "cyan")} ${getText()}`);
|
|
3327
|
-
}, progressEveryMs);
|
|
3328
|
-
try {
|
|
3329
|
-
return await action();
|
|
3330
|
-
} finally {
|
|
3331
|
-
clearInterval(timer);
|
|
3332
|
-
stderr.write(`\r\u001b[2K`);
|
|
3333
|
-
}
|
|
3334
|
-
}
|
|
3335
|
-
function color(text, colorName) {
|
|
3336
|
-
if (!shouldUseColor()) return text;
|
|
3337
|
-
return `${colors[colorName]}${text}${colors.reset}`;
|
|
3338
|
-
}
|
|
3339
|
-
function shouldUseColor() {
|
|
3340
|
-
if (process.env.NO_COLOR) return false;
|
|
3341
|
-
if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0") return true;
|
|
3342
|
-
return stdout.isTTY === true;
|
|
3343
|
-
}
|
|
3344
|
-
//#endregion
|
|
3345
3926
|
//#region src/knowledge/status.ts
|
|
3346
3927
|
function getKnowledgeStatus(workspaceRoot) {
|
|
3347
3928
|
const kbPathSource = process.env.TOPCHESTER_KB_DIR ? "env" : "default";
|
|
@@ -3674,6 +4255,14 @@ const l1ConfidenceLevels = [
|
|
|
3674
4255
|
"medium",
|
|
3675
4256
|
"high"
|
|
3676
4257
|
];
|
|
4258
|
+
const l1FileRoles = [
|
|
4259
|
+
"source",
|
|
4260
|
+
"test",
|
|
4261
|
+
"config",
|
|
4262
|
+
"doc",
|
|
4263
|
+
"script",
|
|
4264
|
+
"unknown"
|
|
4265
|
+
];
|
|
3677
4266
|
const nonEmptyStringSchema = z.string().min(1);
|
|
3678
4267
|
const sha256HashSchema = z.string().regex(/^sha256:[a-f0-9]{64}$/);
|
|
3679
4268
|
const isoUtcTimestampSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/).refine((value) => !Number.isNaN(Date.parse(value)), { message: "Expected a valid UTC ISO timestamp" });
|
|
@@ -3685,7 +4274,7 @@ const l1FileSymbolSchema = z.object({
|
|
|
3685
4274
|
kind: nonEmptyStringSchema,
|
|
3686
4275
|
name: nonEmptyStringSchema,
|
|
3687
4276
|
exported: z.boolean(),
|
|
3688
|
-
summary: nonEmptyStringSchema
|
|
4277
|
+
summary: nonEmptyStringSchema.optional()
|
|
3689
4278
|
}).strict();
|
|
3690
4279
|
const l1FileEvidenceSchema = z.object({
|
|
3691
4280
|
kind: nonEmptyStringSchema,
|
|
@@ -3702,6 +4291,7 @@ const l1FileEntrySchema = z.object({
|
|
|
3702
4291
|
size_bytes: z.number().int().nonnegative(),
|
|
3703
4292
|
last_scanned_at: isoUtcTimestampSchema,
|
|
3704
4293
|
scan_status: z.enum(l1FileScanStatuses),
|
|
4294
|
+
file_role: z.enum(l1FileRoles).default("unknown"),
|
|
3705
4295
|
summary: nonEmptyStringSchema,
|
|
3706
4296
|
responsibilities: z.array(nonEmptyStringSchema),
|
|
3707
4297
|
symbols: z.array(l1FileSymbolSchema),
|
|
@@ -3710,6 +4300,9 @@ const l1FileEntrySchema = z.object({
|
|
|
3710
4300
|
module_ids: z.array(l1ModuleIdSchema),
|
|
3711
4301
|
feature_ids: z.array(l1FeatureIdSchema),
|
|
3712
4302
|
test_ids: z.array(l1FileIdSchema),
|
|
4303
|
+
declared_test_targets: z.array(l1FileIdSchema).default([]),
|
|
4304
|
+
likely_test_targets: z.array(l1FileIdSchema).default([]),
|
|
4305
|
+
tested_by: z.array(l1FileIdSchema).default([]),
|
|
3713
4306
|
evidence: z.array(l1FileEvidenceSchema),
|
|
3714
4307
|
confidence: z.enum(l1ConfidenceLevels)
|
|
3715
4308
|
}).strict().refine((entry) => entry.id === `file:${entry.path}`, {
|
|
@@ -3777,6 +4370,123 @@ function formatCountProgress(label, completed, total, detail) {
|
|
|
3777
4370
|
return `${label} [${formatProgressBar(safeCompleted, safeTotal)}] ${safeCompleted}/${safeTotal} (${percent}%)${suffix}`;
|
|
3778
4371
|
}
|
|
3779
4372
|
//#endregion
|
|
4373
|
+
//#region src/knowledge/compiler/l1-postprocess.ts
|
|
4374
|
+
async function postProcessL1Entries(kbPath) {
|
|
4375
|
+
const entries = await loadL1Entries(kbPath);
|
|
4376
|
+
const entriesById = new Map(entries.map(({ entry }) => [entry.id, entry]));
|
|
4377
|
+
const entriesByPath = new Map(entries.map(({ entry }) => [entry.path, entry]));
|
|
4378
|
+
const testTargetsById = /* @__PURE__ */ new Map();
|
|
4379
|
+
for (const { entry } of entries) {
|
|
4380
|
+
if (inferL1FileRole(entry.path) !== "test") {
|
|
4381
|
+
testTargetsById.set(entry.id, {
|
|
4382
|
+
declared: [],
|
|
4383
|
+
likely: []
|
|
4384
|
+
});
|
|
4385
|
+
continue;
|
|
4386
|
+
}
|
|
4387
|
+
const declared = dedupeStrings$1([...entry.declared_test_targets.filter((id) => isExistingNonSelfFileId(id, entry.id, entriesById)), ...entry.imports.filter((id) => isExistingNonSelfFileId(id, entry.id, entriesById))]);
|
|
4388
|
+
const likely = dedupeStrings$1([...entry.likely_test_targets.filter((id) => isExistingNonSelfFileId(id, entry.id, entriesById)), ...inferLikelyTestTargets(entry.path, entriesByPath)]);
|
|
4389
|
+
testTargetsById.set(entry.id, {
|
|
4390
|
+
declared,
|
|
4391
|
+
likely
|
|
4392
|
+
});
|
|
4393
|
+
}
|
|
4394
|
+
const testedBy = /* @__PURE__ */ new Map();
|
|
4395
|
+
for (const [testId, links] of testTargetsById) for (const targetId of dedupeStrings$1([...links.declared, ...links.likely])) {
|
|
4396
|
+
const list = testedBy.get(targetId) ?? [];
|
|
4397
|
+
list.push(testId);
|
|
4398
|
+
testedBy.set(targetId, list);
|
|
4399
|
+
}
|
|
4400
|
+
let entriesUpdated = 0;
|
|
4401
|
+
let testLinksAdded = 0;
|
|
4402
|
+
for (const { entry, entryPath } of entries) {
|
|
4403
|
+
const fileRole = inferL1FileRole(entry.path);
|
|
4404
|
+
const links = testTargetsById.get(entry.id) ?? {
|
|
4405
|
+
declared: [],
|
|
4406
|
+
likely: []
|
|
4407
|
+
};
|
|
4408
|
+
const nextEntry = parseL1FileEntry({
|
|
4409
|
+
...entry,
|
|
4410
|
+
file_role: fileRole,
|
|
4411
|
+
declared_test_targets: links.declared,
|
|
4412
|
+
likely_test_targets: links.likely,
|
|
4413
|
+
tested_by: dedupeStrings$1(testedBy.get(entry.id) ?? []).sort()
|
|
4414
|
+
});
|
|
4415
|
+
testLinksAdded += nextEntry.declared_test_targets.length + nextEntry.likely_test_targets.length + nextEntry.tested_by.length;
|
|
4416
|
+
if (JSON.stringify(nextEntry) !== JSON.stringify(entry)) {
|
|
4417
|
+
await writeFile(entryPath, `${JSON.stringify(nextEntry, null, 2)}\n`);
|
|
4418
|
+
entriesUpdated += 1;
|
|
4419
|
+
}
|
|
4420
|
+
}
|
|
4421
|
+
return {
|
|
4422
|
+
entriesRead: entries.length,
|
|
4423
|
+
entriesUpdated,
|
|
4424
|
+
testLinksAdded
|
|
4425
|
+
};
|
|
4426
|
+
}
|
|
4427
|
+
function inferL1FileRole(path) {
|
|
4428
|
+
const lowerPath = path.toLowerCase();
|
|
4429
|
+
const name = basename(lowerPath);
|
|
4430
|
+
if (isTestPath(lowerPath)) return "test";
|
|
4431
|
+
if (lowerPath.startsWith("scripts/") || lowerPath.startsWith("script/") || lowerPath.endsWith(".sh")) return "script";
|
|
4432
|
+
if (lowerPath.endsWith(".md") || lowerPath.endsWith(".mdx") || lowerPath.startsWith("docs/")) return "doc";
|
|
4433
|
+
if (name === "package.json" || name.endsWith("lock.json") || name.endsWith("-lock.yaml") || name.endsWith(".config.ts") || name.endsWith(".config.js") || name.endsWith(".config.mjs") || name.endsWith(".config.cjs") || name === "tsconfig.json" || name.startsWith(".")) return "config";
|
|
4434
|
+
if (/\.(ts|tsx|js|jsx|mts|cts)$/.test(lowerPath)) return "source";
|
|
4435
|
+
return "unknown";
|
|
4436
|
+
}
|
|
4437
|
+
async function loadL1Entries(kbPath) {
|
|
4438
|
+
const entryPaths = await listJsonFiles$1(join(kbPath, "l1-files")).catch((error) => {
|
|
4439
|
+
if (isFileNotFoundError$3(error)) return [];
|
|
4440
|
+
throw error;
|
|
4441
|
+
});
|
|
4442
|
+
const entries = [];
|
|
4443
|
+
for (const entryPath of entryPaths) entries.push({
|
|
4444
|
+
entryPath,
|
|
4445
|
+
entry: parseL1FileEntry(JSON.parse(await readFile(entryPath, "utf8")))
|
|
4446
|
+
});
|
|
4447
|
+
return entries.sort((a, b) => a.entry.path.localeCompare(b.entry.path));
|
|
4448
|
+
}
|
|
4449
|
+
async function listJsonFiles$1(directory) {
|
|
4450
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
4451
|
+
const files = [];
|
|
4452
|
+
for (const entry of entries) {
|
|
4453
|
+
const entryPath = join(directory, entry.name);
|
|
4454
|
+
if (entry.isDirectory()) files.push(...await listJsonFiles$1(entryPath));
|
|
4455
|
+
else if (entry.isFile() && entry.name.endsWith(".json")) files.push(entryPath);
|
|
4456
|
+
}
|
|
4457
|
+
return files.sort();
|
|
4458
|
+
}
|
|
4459
|
+
function inferLikelyTestTargets(testPath, entriesByPath) {
|
|
4460
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
4461
|
+
const sourceLikePath = removeTestSuffix(testPath);
|
|
4462
|
+
candidates.add(sourceLikePath);
|
|
4463
|
+
for (const prefix of [
|
|
4464
|
+
"test/",
|
|
4465
|
+
"tests/",
|
|
4466
|
+
"__tests__/"
|
|
4467
|
+
]) if (testPath.startsWith(prefix)) candidates.add(`src/${removeTestSuffix(testPath.slice(prefix.length))}`);
|
|
4468
|
+
if (testPath.includes("/__tests__/")) candidates.add(removeTestSuffix(testPath.replace("/__tests__/", "/")));
|
|
4469
|
+
return [...candidates].filter((candidate) => candidate !== testPath).flatMap((candidate) => {
|
|
4470
|
+
const entry = entriesByPath.get(candidate);
|
|
4471
|
+
return entry ? [entry.id] : [];
|
|
4472
|
+
});
|
|
4473
|
+
}
|
|
4474
|
+
function removeTestSuffix(path) {
|
|
4475
|
+
return path.replace(/\.(test|spec)(\.[^./]+)$/i, "$2");
|
|
4476
|
+
}
|
|
4477
|
+
function isTestPath(path) {
|
|
4478
|
+
return /\.(test|spec)\.(ts|tsx|js|jsx|mts|cts)$/.test(path) || path.startsWith("test/") || path.startsWith("tests/") || path.startsWith("__tests__/") || path.includes("/__tests__/");
|
|
4479
|
+
}
|
|
4480
|
+
function isExistingNonSelfFileId(id, selfId, entriesById) {
|
|
4481
|
+
return id !== selfId && entriesById.has(id);
|
|
4482
|
+
}
|
|
4483
|
+
function dedupeStrings$1(values) {
|
|
4484
|
+
return [...new Set(values)];
|
|
4485
|
+
}
|
|
4486
|
+
function isFileNotFoundError$3(error) {
|
|
4487
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
4488
|
+
}
|
|
4489
|
+
//#endregion
|
|
3780
4490
|
//#region src/knowledge/compiler/manifest.ts
|
|
3781
4491
|
const knowledgeCompilerIdentity = {
|
|
3782
4492
|
name: "topchester-knowledge-compiler",
|
|
@@ -3819,6 +4529,8 @@ async function processL1Queue(options) {
|
|
|
3819
4529
|
await persistQueue(options.queuePath, queuedFiles, now().toISOString());
|
|
3820
4530
|
options.onProgress?.({ message: formatL1ProgressMessage("Processing L1 files", index + 1, queuedFiles.length, item.path) });
|
|
3821
4531
|
}
|
|
4532
|
+
options.onProgress?.({ message: "Linking L1 file relationships..." });
|
|
4533
|
+
await postProcessL1Entries(options.kbPath);
|
|
3822
4534
|
const summary = await summarizeL1Queue(options.kbPath, queuedFiles);
|
|
3823
4535
|
await writeManifest(options, summary, now().toISOString());
|
|
3824
4536
|
return {
|
|
@@ -3878,7 +4590,11 @@ async function processL1QueueItem(options) {
|
|
|
3878
4590
|
}
|
|
3879
4591
|
function buildL1FileEntrySystemPrompt() {
|
|
3880
4592
|
return [
|
|
3881
|
-
"You
|
|
4593
|
+
"You create concise, structured repository knowledge for one file.",
|
|
4594
|
+
"Prefer concrete facts visible in the file over generic descriptions.",
|
|
4595
|
+
"Do not invent modules, features, tests, routes, or dependencies.",
|
|
4596
|
+
"If uncertain, leave arrays empty and use lower confidence.",
|
|
4597
|
+
"Avoid filler such as \"This file contains code\" or \"Symbol named X\".",
|
|
3882
4598
|
"Return exactly one JSON object and no markdown.",
|
|
3883
4599
|
"Do not include secrets, credentials, or raw provider payloads."
|
|
3884
4600
|
].join("\n");
|
|
@@ -3887,6 +4603,35 @@ function buildL1FileEntryPrompt(input) {
|
|
|
3887
4603
|
return [
|
|
3888
4604
|
"Create an L1 file entry for this workspace-relative path.",
|
|
3889
4605
|
"The compiler will overwrite id, path, content_hash, size_bytes, last_scanned_at, and scan_status.",
|
|
4606
|
+
"",
|
|
4607
|
+
"Extraction rules:",
|
|
4608
|
+
"- summary: one specific sentence about the file's role in this project.",
|
|
4609
|
+
"- responsibilities: 2-6 concrete responsibilities, no duplicates, no generic boilerplate.",
|
|
4610
|
+
"- symbols: important declared or exported interfaces, types, classes, functions, constants, schemas, commands, routes, React components, tests, or config objects.",
|
|
4611
|
+
" For each symbol, set:",
|
|
4612
|
+
" - kind: interface | type | class | function | const | component | schema | command | route | test | config | symbol",
|
|
4613
|
+
" - name: exact identifier or stable label",
|
|
4614
|
+
" - exported: true only when exported from this file",
|
|
4615
|
+
" - summary: include only if it adds useful meaning beyond the name",
|
|
4616
|
+
"- imports: only workspace-local file dependencies as file:<path>; omit packages and built-ins.",
|
|
4617
|
+
"- exports: exact exported names from this file as strings.",
|
|
4618
|
+
"- test_ids: only file:<path> when this file is clearly a test or clearly references a test target.",
|
|
4619
|
+
"- file_role: source | test | config | doc | script | unknown.",
|
|
4620
|
+
"- declared_test_targets: for test files, file:<path> entries that this test directly imports or names.",
|
|
4621
|
+
"- likely_test_targets: for test files, file:<path> entries likely covered by path/name convention.",
|
|
4622
|
+
"- tested_by: leave empty; the compiler fills reverse test links after all files are processed.",
|
|
4623
|
+
"- module_ids and feature_ids: leave empty unless there is strong evidence.",
|
|
4624
|
+
"- evidence: include at least { \"kind\": \"path\", \"value\": \"<path>\" } and any high-signal local evidence.",
|
|
4625
|
+
"- confidence: high for simple files with clear structure, medium for normal files, low for vague/generated/config-heavy files.",
|
|
4626
|
+
"",
|
|
4627
|
+
"Quality rules:",
|
|
4628
|
+
"- Return valid JSON only.",
|
|
4629
|
+
"- Keep arrays concise.",
|
|
4630
|
+
"- Deduplicate all arrays.",
|
|
4631
|
+
"- Prefer exact names from source.",
|
|
4632
|
+
"- Do not copy large code snippets.",
|
|
4633
|
+
"- Do not include secrets or raw credentials.",
|
|
4634
|
+
"",
|
|
3890
4635
|
"Use this JSON shape:",
|
|
3891
4636
|
JSON.stringify({
|
|
3892
4637
|
$schema: l1FileEntrySchemaPath,
|
|
@@ -3899,6 +4644,7 @@ function buildL1FileEntryPrompt(input) {
|
|
|
3899
4644
|
size_bytes: 0,
|
|
3900
4645
|
last_scanned_at: "2026-05-11T00:00:00Z",
|
|
3901
4646
|
scan_status: "current",
|
|
4647
|
+
file_role: "source",
|
|
3902
4648
|
summary: "One clear sentence.",
|
|
3903
4649
|
responsibilities: ["What this file owns or does."],
|
|
3904
4650
|
symbols: [],
|
|
@@ -3907,6 +4653,9 @@ function buildL1FileEntryPrompt(input) {
|
|
|
3907
4653
|
module_ids: [],
|
|
3908
4654
|
feature_ids: [],
|
|
3909
4655
|
test_ids: [],
|
|
4656
|
+
declared_test_targets: [],
|
|
4657
|
+
likely_test_targets: [],
|
|
4658
|
+
tested_by: [],
|
|
3910
4659
|
evidence: [{
|
|
3911
4660
|
kind: "path",
|
|
3912
4661
|
value: "<path>"
|
|
@@ -3944,41 +4693,49 @@ function normalizeL1FileEntry(value, deterministic) {
|
|
|
3944
4693
|
}
|
|
3945
4694
|
function normalizeModelOwnedL1Fields(value, path) {
|
|
3946
4695
|
const record = value;
|
|
4696
|
+
const exports = normalizeStringArray(record.exports);
|
|
3947
4697
|
return {
|
|
3948
4698
|
...record,
|
|
4699
|
+
file_role: inferL1FileRole(path),
|
|
3949
4700
|
responsibilities: normalizeStringArray(record.responsibilities),
|
|
3950
|
-
symbols: normalizeSymbols(record.symbols, path),
|
|
4701
|
+
symbols: normalizeSymbols(record.symbols, path, exports),
|
|
3951
4702
|
imports: normalizePrefixedIds(record.imports, "file:"),
|
|
3952
|
-
exports
|
|
4703
|
+
exports,
|
|
3953
4704
|
module_ids: normalizePrefixedIds(record.module_ids, "module:"),
|
|
3954
4705
|
feature_ids: normalizePrefixedIds(record.feature_ids, "feature:"),
|
|
3955
4706
|
test_ids: normalizePrefixedIds(record.test_ids, "file:"),
|
|
4707
|
+
declared_test_targets: normalizePrefixedIds(record.declared_test_targets, "file:"),
|
|
4708
|
+
likely_test_targets: normalizePrefixedIds(record.likely_test_targets, "file:"),
|
|
4709
|
+
tested_by: normalizePrefixedIds(record.tested_by, "file:"),
|
|
3956
4710
|
evidence: normalizeEvidence(record.evidence)
|
|
3957
4711
|
};
|
|
3958
4712
|
}
|
|
3959
4713
|
function normalizeStringArray(value) {
|
|
3960
4714
|
if (!Array.isArray(value)) return [];
|
|
3961
|
-
return value.filter((item) => typeof item === "string"
|
|
4715
|
+
return dedupeStrings(value.filter((item) => typeof item === "string").map((item) => item.trim()));
|
|
3962
4716
|
}
|
|
3963
4717
|
function normalizePrefixedIds(value, prefix) {
|
|
3964
4718
|
return normalizeStringArray(value).filter((item) => item.startsWith(prefix));
|
|
3965
4719
|
}
|
|
3966
4720
|
function normalizeEvidence(value) {
|
|
3967
4721
|
if (!Array.isArray(value)) return [];
|
|
3968
|
-
return value.flatMap((item) => {
|
|
4722
|
+
return dedupeRecords(value.flatMap((item) => {
|
|
3969
4723
|
if (!item || typeof item !== "object" || Array.isArray(item)) return [];
|
|
3970
4724
|
const record = item;
|
|
3971
|
-
|
|
3972
|
-
|
|
4725
|
+
const kind = typeof record.kind === "string" ? record.kind.trim() : "";
|
|
4726
|
+
const recordValue = typeof record.value === "string" ? record.value.trim() : "";
|
|
4727
|
+
if (kind.length === 0) return [];
|
|
4728
|
+
if (recordValue.length === 0) return [];
|
|
3973
4729
|
return [{
|
|
3974
|
-
kind
|
|
3975
|
-
value:
|
|
4730
|
+
kind,
|
|
4731
|
+
value: recordValue
|
|
3976
4732
|
}];
|
|
3977
|
-
});
|
|
4733
|
+
}), (item) => `${item.kind}\0${item.value}`);
|
|
3978
4734
|
}
|
|
3979
|
-
function normalizeSymbols(value, path) {
|
|
4735
|
+
function normalizeSymbols(value, path, exports) {
|
|
3980
4736
|
if (!Array.isArray(value)) return [];
|
|
3981
|
-
|
|
4737
|
+
const exportedNames = new Set(exports);
|
|
4738
|
+
return dedupeRecords(value.flatMap((item) => {
|
|
3982
4739
|
if (typeof item === "string") {
|
|
3983
4740
|
const name = item.trim();
|
|
3984
4741
|
if (!name || !path) return [];
|
|
@@ -3986,8 +4743,7 @@ function normalizeSymbols(value, path) {
|
|
|
3986
4743
|
id: `symbol:${path}#${name}`,
|
|
3987
4744
|
kind: "symbol",
|
|
3988
4745
|
name,
|
|
3989
|
-
exported:
|
|
3990
|
-
summary: `Symbol named ${name}.`
|
|
4746
|
+
exported: exportedNames.has(name)
|
|
3991
4747
|
}];
|
|
3992
4748
|
}
|
|
3993
4749
|
if (!item || typeof item !== "object" || Array.isArray(item)) return [];
|
|
@@ -3995,14 +4751,36 @@ function normalizeSymbols(value, path) {
|
|
|
3995
4751
|
const rawId = typeof record.id === "string" && record.id.startsWith("symbol:") ? record.id : void 0;
|
|
3996
4752
|
const name = typeof record.name === "string" && record.name.trim().length > 0 ? record.name : rawId?.slice(rawId.lastIndexOf("#") + 1);
|
|
3997
4753
|
if (!name || !path) return [];
|
|
3998
|
-
|
|
4754
|
+
const summary = typeof record.summary === "string" && record.summary.trim().length > 0 ? record.summary.trim() : "";
|
|
4755
|
+
return [removeUndefinedValues({
|
|
3999
4756
|
id: rawId ?? `symbol:${path}#${name}`,
|
|
4000
|
-
kind: typeof record.kind === "string" && record.kind.trim().length > 0 ? record.kind : "symbol",
|
|
4757
|
+
kind: typeof record.kind === "string" && record.kind.trim().length > 0 ? record.kind.trim() : "symbol",
|
|
4001
4758
|
name,
|
|
4002
|
-
exported: typeof record.exported === "boolean" ? record.exported : false,
|
|
4003
|
-
summary:
|
|
4004
|
-
}];
|
|
4005
|
-
});
|
|
4759
|
+
exported: exportedNames.has(name) || (typeof record.exported === "boolean" ? record.exported : false),
|
|
4760
|
+
summary: summary && !isGenericSymbolSummary$1(summary, name) ? summary : void 0
|
|
4761
|
+
})];
|
|
4762
|
+
}), (item) => String(item.id));
|
|
4763
|
+
}
|
|
4764
|
+
function dedupeStrings(values) {
|
|
4765
|
+
return [...new Set(values.filter((value) => value.length > 0))];
|
|
4766
|
+
}
|
|
4767
|
+
function dedupeRecords(values, keyFor) {
|
|
4768
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4769
|
+
const deduped = [];
|
|
4770
|
+
for (const value of values) {
|
|
4771
|
+
const key = keyFor(value);
|
|
4772
|
+
if (seen.has(key)) continue;
|
|
4773
|
+
seen.add(key);
|
|
4774
|
+
deduped.push(value);
|
|
4775
|
+
}
|
|
4776
|
+
return deduped;
|
|
4777
|
+
}
|
|
4778
|
+
function removeUndefinedValues(record) {
|
|
4779
|
+
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== void 0));
|
|
4780
|
+
}
|
|
4781
|
+
function isGenericSymbolSummary$1(summary, name) {
|
|
4782
|
+
const normalizedSummary = summary.trim().replace(/\s+/g, " ");
|
|
4783
|
+
return normalizedSummary === `Symbol named ${name}.` || normalizedSummary === `Symbol named ${name}` || normalizedSummary === name;
|
|
4006
4784
|
}
|
|
4007
4785
|
function extractTopLevelJsonObjects(text) {
|
|
4008
4786
|
const objects = [];
|
|
@@ -4404,11 +5182,11 @@ async function getL1SyncStatus(kbPath, kbReady, file) {
|
|
|
4404
5182
|
if (entry.path !== file.path || entry.size_bytes !== file.sizeBytes || entry.content_hash !== file.hash) return "changed";
|
|
4405
5183
|
return entry.scan_status;
|
|
4406
5184
|
} catch (error) {
|
|
4407
|
-
if (isFileNotFoundError$
|
|
5185
|
+
if (isFileNotFoundError$2(error)) return "missing_entry";
|
|
4408
5186
|
return "invalid";
|
|
4409
5187
|
}
|
|
4410
5188
|
}
|
|
4411
|
-
function isFileNotFoundError$
|
|
5189
|
+
function isFileNotFoundError$2(error) {
|
|
4412
5190
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
4413
5191
|
}
|
|
4414
5192
|
function assertKbSummarizeModelConfigured(model) {
|
|
@@ -4522,25 +5300,449 @@ function isNodeError(error) {
|
|
|
4522
5300
|
return error instanceof Error && "code" in error;
|
|
4523
5301
|
}
|
|
4524
5302
|
//#endregion
|
|
4525
|
-
//#region src/
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
5303
|
+
//#region src/knowledge/search.ts
|
|
5304
|
+
var L1InMemoryIndex = class {
|
|
5305
|
+
entriesById = /* @__PURE__ */ new Map();
|
|
5306
|
+
postingsByToken = /* @__PURE__ */ new Map();
|
|
5307
|
+
prefixTokensByPrefix = /* @__PURE__ */ new Map();
|
|
5308
|
+
constructor(entries) {
|
|
5309
|
+
for (const entry of entries) {
|
|
5310
|
+
this.entriesById.set(entry.id, entry);
|
|
5311
|
+
this.indexEntry(entry);
|
|
5312
|
+
}
|
|
5313
|
+
this.indexPrefixTokens();
|
|
5314
|
+
}
|
|
5315
|
+
get size() {
|
|
5316
|
+
return this.entriesById.size;
|
|
5317
|
+
}
|
|
5318
|
+
search(query, options = {}) {
|
|
5319
|
+
const tokens = tokenizeQuery(query);
|
|
5320
|
+
const scoresByEntryId = /* @__PURE__ */ new Map();
|
|
5321
|
+
const reasonsByEntryId = /* @__PURE__ */ new Map();
|
|
5322
|
+
for (const token of tokens) {
|
|
5323
|
+
this.addMatches(token, 1, scoresByEntryId, reasonsByEntryId);
|
|
5324
|
+
if (token.length >= 4) this.addPrefixMatches(token, scoresByEntryId, reasonsByEntryId);
|
|
5325
|
+
}
|
|
5326
|
+
const limit = options.limit ?? 10;
|
|
5327
|
+
return [...scoresByEntryId.entries()].map(([entryId, score]) => {
|
|
5328
|
+
const entry = this.entriesById.get(entryId);
|
|
5329
|
+
if (!entry) return;
|
|
5330
|
+
return {
|
|
5331
|
+
id: entry.id,
|
|
5332
|
+
path: entry.path,
|
|
5333
|
+
score: Math.round(score * 100) / 100,
|
|
5334
|
+
summary: entry.summary,
|
|
5335
|
+
contentHash: entry.content_hash,
|
|
5336
|
+
scanStatus: entry.scan_status,
|
|
5337
|
+
reasons: [...(reasonsByEntryId.get(entryId) ?? /* @__PURE__ */ new Map()).entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([reason]) => reason).slice(0, 6)
|
|
5338
|
+
};
|
|
5339
|
+
}).filter((match) => Boolean(match)).sort((a, b) => b.score - a.score || a.path.localeCompare(b.path)).slice(0, limit);
|
|
5340
|
+
}
|
|
5341
|
+
getEntry(id) {
|
|
5342
|
+
return this.entriesById.get(id);
|
|
5343
|
+
}
|
|
5344
|
+
indexEntry(entry) {
|
|
5345
|
+
this.addField(entry, "path", [entry.path, basename(entry.path)], 6);
|
|
5346
|
+
this.addField(entry, "symbol", entry.symbols.flatMap((symbol) => [
|
|
5347
|
+
symbol.name,
|
|
5348
|
+
symbol.kind,
|
|
5349
|
+
symbol.summary
|
|
5350
|
+
].filter(isString)), 10);
|
|
5351
|
+
this.addField(entry, "export", entry.exports, 9);
|
|
5352
|
+
this.addField(entry, "responsibility", entry.responsibilities, 6);
|
|
5353
|
+
this.addField(entry, "summary", [entry.summary], 5);
|
|
5354
|
+
this.addField(entry, "import", entry.imports, 4);
|
|
5355
|
+
this.addField(entry, "test", entry.test_ids, 4);
|
|
5356
|
+
this.addField(entry, "relationship", [...entry.module_ids, ...entry.feature_ids], 3);
|
|
5357
|
+
this.addField(entry, "evidence", entry.evidence.map((evidence) => evidence.value), 3);
|
|
5358
|
+
}
|
|
5359
|
+
addField(entry, field, values, weight) {
|
|
5360
|
+
const tokens = new Set(values.flatMap(tokenizeText));
|
|
5361
|
+
for (const token of tokens) {
|
|
5362
|
+
const postings = this.postingsByToken.get(token) ?? [];
|
|
5363
|
+
postings.push({
|
|
5364
|
+
entryId: entry.id,
|
|
5365
|
+
weight,
|
|
5366
|
+
reason: `${formatField(field)} matched ${token}`
|
|
5367
|
+
});
|
|
5368
|
+
this.postingsByToken.set(token, postings);
|
|
4535
5369
|
}
|
|
4536
|
-
rendered.push(inCodeBlock ? applyCodeBlockBackground(line) : line);
|
|
4537
5370
|
}
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
5371
|
+
addMatches(token, multiplier, scoresByEntryId, reasonsByEntryId) {
|
|
5372
|
+
for (const posting of this.postingsByToken.get(token) ?? []) {
|
|
5373
|
+
scoresByEntryId.set(posting.entryId, (scoresByEntryId.get(posting.entryId) ?? 0) + posting.weight * multiplier);
|
|
5374
|
+
const reasons = reasonsByEntryId.get(posting.entryId) ?? /* @__PURE__ */ new Map();
|
|
5375
|
+
reasons.set(posting.reason, Math.max(reasons.get(posting.reason) ?? 0, posting.weight * multiplier));
|
|
5376
|
+
reasonsByEntryId.set(posting.entryId, reasons);
|
|
5377
|
+
}
|
|
5378
|
+
}
|
|
5379
|
+
addPrefixMatches(token, scoresByEntryId, reasonsByEntryId) {
|
|
5380
|
+
const matchedTokens = /* @__PURE__ */ new Set();
|
|
5381
|
+
for (const indexedToken of this.prefixTokensByPrefix.get(token) ?? []) matchedTokens.add(indexedToken);
|
|
5382
|
+
for (let prefixLength = 1; prefixLength < token.length; prefixLength += 1) {
|
|
5383
|
+
const prefix = token.slice(0, prefixLength);
|
|
5384
|
+
if (this.postingsByToken.has(prefix)) matchedTokens.add(prefix);
|
|
5385
|
+
}
|
|
5386
|
+
for (const indexedToken of matchedTokens) {
|
|
5387
|
+
if (indexedToken === token) continue;
|
|
5388
|
+
this.addMatches(indexedToken, .6, scoresByEntryId, reasonsByEntryId);
|
|
5389
|
+
}
|
|
5390
|
+
}
|
|
5391
|
+
indexPrefixTokens() {
|
|
5392
|
+
for (const indexedToken of this.postingsByToken.keys()) for (let prefixLength = 4; prefixLength < indexedToken.length; prefixLength += 1) {
|
|
5393
|
+
const prefix = indexedToken.slice(0, prefixLength);
|
|
5394
|
+
const tokens = this.prefixTokensByPrefix.get(prefix) ?? [];
|
|
5395
|
+
tokens.push(indexedToken);
|
|
5396
|
+
this.prefixTokensByPrefix.set(prefix, tokens);
|
|
5397
|
+
}
|
|
5398
|
+
}
|
|
5399
|
+
};
|
|
5400
|
+
function buildL1InMemoryIndex(entries) {
|
|
5401
|
+
return new L1InMemoryIndex(entries);
|
|
4542
5402
|
}
|
|
4543
|
-
function
|
|
5403
|
+
async function searchL1Knowledge(workspaceRoot, query, options = {}) {
|
|
5404
|
+
const status = getKnowledgeStatus(workspaceRoot);
|
|
5405
|
+
if (!status.kbExists || !status.kbIsDirectory) throw new Error("Run `topchester kb init` and `topchester kb compile` before searching the knowledge base.");
|
|
5406
|
+
const loadResult = await loadL1FileEntries(status.kbPath);
|
|
5407
|
+
const index = buildL1InMemoryIndex(loadResult.entries);
|
|
5408
|
+
return {
|
|
5409
|
+
workspaceRoot,
|
|
5410
|
+
kbPath: status.kbPath,
|
|
5411
|
+
query,
|
|
5412
|
+
entryCount: index.size,
|
|
5413
|
+
invalidEntryCount: loadResult.invalidEntryCount,
|
|
5414
|
+
matches: index.search(query, options)
|
|
5415
|
+
};
|
|
5416
|
+
}
|
|
5417
|
+
function createL1ContextPackFromIndex(source, query, options = {}) {
|
|
5418
|
+
const limit = options.limit ?? 8;
|
|
5419
|
+
const minScore = options.minScore ?? 12;
|
|
5420
|
+
const relevantFiles = source.index.search(query, { limit: Math.max(limit * 3, limit) }).filter((match) => match.score >= minScore).slice(0, limit).map((match) => {
|
|
5421
|
+
const entry = source.index.getEntry(match.id);
|
|
5422
|
+
if (!entry) return;
|
|
5423
|
+
return {
|
|
5424
|
+
id: match.id,
|
|
5425
|
+
path: match.path,
|
|
5426
|
+
score: match.score,
|
|
5427
|
+
reasons: match.reasons,
|
|
5428
|
+
contentHash: match.contentHash,
|
|
5429
|
+
scanStatus: match.scanStatus,
|
|
5430
|
+
l1: compactL1Entry(entry),
|
|
5431
|
+
fullL1: options.includeFullL1 ? entry : void 0
|
|
5432
|
+
};
|
|
5433
|
+
}).filter((file) => Boolean(file));
|
|
5434
|
+
const warnings = relevantFiles.length === 0 ? ["No L1 entries met the context pack score threshold."] : [];
|
|
5435
|
+
return {
|
|
5436
|
+
workspaceRoot: source.workspaceRoot,
|
|
5437
|
+
kbPath: source.kbPath,
|
|
5438
|
+
query,
|
|
5439
|
+
entryCount: source.index.size,
|
|
5440
|
+
invalidEntryCount: source.invalidEntryCount,
|
|
5441
|
+
selection: {
|
|
5442
|
+
limit,
|
|
5443
|
+
minScore
|
|
5444
|
+
},
|
|
5445
|
+
drift: {
|
|
5446
|
+
status: "unchecked",
|
|
5447
|
+
warnings: ["L1 context pack includes stored scan statuses; exact file-hash drift check has not run yet."]
|
|
5448
|
+
},
|
|
5449
|
+
summary: summarizeContextPack(query, relevantFiles),
|
|
5450
|
+
warnings,
|
|
5451
|
+
relevantFiles
|
|
5452
|
+
};
|
|
5453
|
+
}
|
|
5454
|
+
async function createL1ContextPack(workspaceRoot, query, options = {}) {
|
|
5455
|
+
const limit = options.limit ?? 8;
|
|
5456
|
+
const minScore = options.minScore ?? 12;
|
|
5457
|
+
const status = getKnowledgeStatus(workspaceRoot);
|
|
5458
|
+
if (!status.kbExists || !status.kbIsDirectory) throw new Error("Run `topchester kb init` and `topchester kb compile` before creating a context pack.");
|
|
5459
|
+
const loadResult = await loadL1FileEntries(status.kbPath);
|
|
5460
|
+
const index = buildL1InMemoryIndex(loadResult.entries);
|
|
5461
|
+
return createL1ContextPackFromIndex({
|
|
5462
|
+
workspaceRoot,
|
|
5463
|
+
kbPath: status.kbPath,
|
|
5464
|
+
index,
|
|
5465
|
+
invalidEntryCount: loadResult.invalidEntryCount
|
|
5466
|
+
}, query, {
|
|
5467
|
+
limit,
|
|
5468
|
+
minScore,
|
|
5469
|
+
includeFullL1: options.includeFullL1
|
|
5470
|
+
});
|
|
5471
|
+
}
|
|
5472
|
+
function formatL1KnowledgeSearchResult(result) {
|
|
5473
|
+
return [
|
|
5474
|
+
"KB search",
|
|
5475
|
+
`workspace: ${result.workspaceRoot}`,
|
|
5476
|
+
`knowledge folder: ${result.kbPath} [ok]`,
|
|
5477
|
+
`query: ${result.query}`,
|
|
5478
|
+
`entries indexed: ${result.entryCount}`,
|
|
5479
|
+
`invalid L1 entries skipped: ${result.invalidEntryCount}`,
|
|
5480
|
+
`matches: ${result.matches.length}`,
|
|
5481
|
+
...result.matches.length === 0 ? ["state: no L1 matches found"] : [""],
|
|
5482
|
+
...result.matches.flatMap((match) => [
|
|
5483
|
+
`${match.score}\t${match.path}\t${match.scanStatus}\t${match.contentHash}`,
|
|
5484
|
+
` reasons: ${match.reasons.join("; ") || "score match"}`,
|
|
5485
|
+
` summary: ${match.summary}`
|
|
5486
|
+
]),
|
|
5487
|
+
"----",
|
|
5488
|
+
`total matches: ${result.matches.length}`
|
|
5489
|
+
];
|
|
5490
|
+
}
|
|
5491
|
+
function formatL1ContextPackResult(result) {
|
|
5492
|
+
return [
|
|
5493
|
+
"KB context",
|
|
5494
|
+
`workspace: ${result.workspaceRoot}`,
|
|
5495
|
+
`knowledge folder: ${result.kbPath} [ok]`,
|
|
5496
|
+
`query: ${result.query}`,
|
|
5497
|
+
`entries indexed: ${result.entryCount}`,
|
|
5498
|
+
`invalid L1 entries skipped: ${result.invalidEntryCount}`,
|
|
5499
|
+
`selection: top ${result.selection.limit}, min score ${result.selection.minScore}`,
|
|
5500
|
+
`drift: ${result.drift.status}`,
|
|
5501
|
+
`relevant files: ${result.relevantFiles.length}`,
|
|
5502
|
+
`summary: ${result.summary}`,
|
|
5503
|
+
...result.warnings.map((warning) => `warning: ${warning}`),
|
|
5504
|
+
"",
|
|
5505
|
+
...result.relevantFiles.flatMap((file) => [
|
|
5506
|
+
`${file.score}\t${file.path}\t${file.scanStatus}\t${file.contentHash}`,
|
|
5507
|
+
` reasons: ${file.reasons.join("; ") || "score match"}`,
|
|
5508
|
+
` responsibilities: ${(file.l1.responsibilities ?? []).join("; ") || "(none)"}`,
|
|
5509
|
+
` symbols: ${(file.l1.symbols ?? []).map((symbol) => symbol.name).join(", ") || "(none)"}`,
|
|
5510
|
+
` imports: ${(file.l1.imports ?? []).join(", ") || "(none)"}`,
|
|
5511
|
+
` exports: ${(file.l1.exports ?? []).join(", ") || "(none)"}`,
|
|
5512
|
+
` tests: ${(file.l1.test_ids ?? []).join(", ") || "(none)"}`
|
|
5513
|
+
]),
|
|
5514
|
+
"----",
|
|
5515
|
+
`total relevant files: ${result.relevantFiles.length}`
|
|
5516
|
+
];
|
|
5517
|
+
}
|
|
5518
|
+
function formatL1ContextPackForPrompt(result) {
|
|
5519
|
+
return [
|
|
5520
|
+
"Topchester KB context pack:",
|
|
5521
|
+
"Use this as orientation only. For task-critical facts, read current source files before editing or making exact claims.",
|
|
5522
|
+
"## -- kb summary start",
|
|
5523
|
+
"```json",
|
|
5524
|
+
JSON.stringify(stripEmptyContainers({
|
|
5525
|
+
query: result.query,
|
|
5526
|
+
summary: result.summary,
|
|
5527
|
+
drift: result.drift,
|
|
5528
|
+
warnings: result.warnings,
|
|
5529
|
+
relevantFiles: result.relevantFiles.map((file) => ({
|
|
5530
|
+
id: file.id,
|
|
5531
|
+
path: file.path,
|
|
5532
|
+
score: file.score,
|
|
5533
|
+
reasons: file.reasons,
|
|
5534
|
+
contentHash: file.contentHash,
|
|
5535
|
+
scanStatus: file.scanStatus,
|
|
5536
|
+
l1: {
|
|
5537
|
+
summary: file.l1.summary,
|
|
5538
|
+
file_role: file.l1.file_role,
|
|
5539
|
+
responsibilities: file.l1.responsibilities,
|
|
5540
|
+
symbols: file.l1.symbols,
|
|
5541
|
+
imports: file.l1.imports,
|
|
5542
|
+
exports: file.l1.exports,
|
|
5543
|
+
module_ids: file.l1.module_ids,
|
|
5544
|
+
feature_ids: file.l1.feature_ids,
|
|
5545
|
+
test_ids: file.l1.test_ids,
|
|
5546
|
+
declared_test_targets: file.l1.declared_test_targets,
|
|
5547
|
+
likely_test_targets: file.l1.likely_test_targets,
|
|
5548
|
+
tested_by: file.l1.tested_by,
|
|
5549
|
+
confidence: file.l1.confidence
|
|
5550
|
+
}
|
|
5551
|
+
}))
|
|
5552
|
+
})),
|
|
5553
|
+
"```",
|
|
5554
|
+
"## -- kb summary end"
|
|
5555
|
+
].join("\n");
|
|
5556
|
+
}
|
|
5557
|
+
async function loadL1FileEntries(kbPath) {
|
|
5558
|
+
const entryPaths = await listJsonFiles(join(kbPath, "l1-files")).catch((error) => {
|
|
5559
|
+
if (isFileNotFoundError$1(error)) return [];
|
|
5560
|
+
throw error;
|
|
5561
|
+
});
|
|
5562
|
+
const parsedEntries = await Promise.all(entryPaths.map(loadL1FileEntry));
|
|
5563
|
+
const entries = parsedEntries.filter((entry) => Boolean(entry));
|
|
5564
|
+
return {
|
|
5565
|
+
entries,
|
|
5566
|
+
invalidEntryCount: parsedEntries.length - entries.length
|
|
5567
|
+
};
|
|
5568
|
+
}
|
|
5569
|
+
async function loadL1FileEntry(entryPath) {
|
|
5570
|
+
try {
|
|
5571
|
+
return parseL1FileEntry(JSON.parse(await readFile(entryPath, "utf8")));
|
|
5572
|
+
} catch {
|
|
5573
|
+
return;
|
|
5574
|
+
}
|
|
5575
|
+
}
|
|
5576
|
+
function summarizeContextPack(query, files) {
|
|
5577
|
+
if (files.length === 0) return `No strong L1 matches were found for "${query}".`;
|
|
5578
|
+
const paths = files.slice(0, 5).map((file) => file.path);
|
|
5579
|
+
return `Likely relevant L1 files for "${query}": ${paths.join(", ")}${files.length > paths.length ? ", ..." : ""}.`;
|
|
5580
|
+
}
|
|
5581
|
+
function compactL1Entry(entry) {
|
|
5582
|
+
const responsibilities = take(entry.responsibilities, 5);
|
|
5583
|
+
const symbols = take(entry.symbols, 12).map(compactSymbol);
|
|
5584
|
+
const imports = take(entry.imports, 20);
|
|
5585
|
+
const exports = take(entry.exports, 20);
|
|
5586
|
+
const moduleIds = take(entry.module_ids, 10);
|
|
5587
|
+
const featureIds = take(entry.feature_ids, 10);
|
|
5588
|
+
const testIds = take(entry.test_ids, 10);
|
|
5589
|
+
const declaredTestTargets = take(entry.declared_test_targets, 10);
|
|
5590
|
+
const likelyTestTargets = take(entry.likely_test_targets, 10);
|
|
5591
|
+
const testedBy = take(entry.tested_by, 10);
|
|
5592
|
+
return stripUndefinedProperties({
|
|
5593
|
+
file_role: entry.file_role,
|
|
5594
|
+
summary: entry.summary,
|
|
5595
|
+
responsibilities: nonEmptyArray(responsibilities),
|
|
5596
|
+
symbols: nonEmptyArray(symbols),
|
|
5597
|
+
imports: nonEmptyArray(imports),
|
|
5598
|
+
exports: nonEmptyArray(exports),
|
|
5599
|
+
module_ids: nonEmptyArray(moduleIds),
|
|
5600
|
+
feature_ids: nonEmptyArray(featureIds),
|
|
5601
|
+
test_ids: nonEmptyArray(testIds),
|
|
5602
|
+
declared_test_targets: nonEmptyArray(declaredTestTargets),
|
|
5603
|
+
likely_test_targets: nonEmptyArray(likelyTestTargets),
|
|
5604
|
+
tested_by: nonEmptyArray(testedBy),
|
|
5605
|
+
confidence: entry.confidence
|
|
5606
|
+
});
|
|
5607
|
+
}
|
|
5608
|
+
function take(items, count) {
|
|
5609
|
+
return items.slice(0, count);
|
|
5610
|
+
}
|
|
5611
|
+
function compactSymbol(symbol) {
|
|
5612
|
+
const compacted = {
|
|
5613
|
+
name: symbol.name,
|
|
5614
|
+
exported: symbol.exported,
|
|
5615
|
+
kind: symbol.kind === "symbol" ? void 0 : symbol.kind,
|
|
5616
|
+
summary: symbol.summary && !isGenericSymbolSummary(symbol.summary, symbol.name) ? symbol.summary : void 0
|
|
5617
|
+
};
|
|
5618
|
+
return compacted.kind || compacted.summary ? compacted : {
|
|
5619
|
+
name: compacted.name,
|
|
5620
|
+
exported: compacted.exported
|
|
5621
|
+
};
|
|
5622
|
+
}
|
|
5623
|
+
function stripEmptyContainers(value) {
|
|
5624
|
+
if (Array.isArray(value)) {
|
|
5625
|
+
const stripped = value.map(stripEmptyContainers).filter((item) => item !== void 0);
|
|
5626
|
+
return stripped.length > 0 ? stripped : void 0;
|
|
5627
|
+
}
|
|
5628
|
+
if (value && typeof value === "object") {
|
|
5629
|
+
const entries = Object.entries(value).map(([key, item]) => [key, stripEmptyContainers(item)]).filter(([, item]) => item !== void 0);
|
|
5630
|
+
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
5631
|
+
}
|
|
5632
|
+
return value;
|
|
5633
|
+
}
|
|
5634
|
+
function stripUndefinedProperties(value) {
|
|
5635
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== void 0));
|
|
5636
|
+
}
|
|
5637
|
+
function nonEmptyArray(items) {
|
|
5638
|
+
return items.length > 0 ? items : void 0;
|
|
5639
|
+
}
|
|
5640
|
+
function isGenericSymbolSummary(summary, name) {
|
|
5641
|
+
const normalizedSummary = summary.trim().replace(/\s+/g, " ");
|
|
5642
|
+
return normalizedSummary === `Symbol named ${name}.` || normalizedSummary === `Symbol named ${name}` || normalizedSummary === name;
|
|
5643
|
+
}
|
|
5644
|
+
async function listJsonFiles(directory) {
|
|
5645
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
5646
|
+
const files = [];
|
|
5647
|
+
for (const entry of entries) {
|
|
5648
|
+
const entryPath = join(directory, entry.name);
|
|
5649
|
+
if (entry.isDirectory()) files.push(...await listJsonFiles(entryPath));
|
|
5650
|
+
else if (entry.isFile() && entry.name.endsWith(".json")) files.push(entryPath);
|
|
5651
|
+
}
|
|
5652
|
+
return files.sort();
|
|
5653
|
+
}
|
|
5654
|
+
function tokenizeQuery(text) {
|
|
5655
|
+
return [...new Set(tokenizeText(text).filter((token) => !queryStopWords.has(token)))];
|
|
5656
|
+
}
|
|
5657
|
+
function tokenizeText(text) {
|
|
5658
|
+
const rawTokens = text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").toLowerCase().match(/[a-z0-9_]+/g) ?? [];
|
|
5659
|
+
const tokens = [];
|
|
5660
|
+
for (const rawToken of rawTokens) {
|
|
5661
|
+
const token = rawToken.replace(/^_+|_+$/g, "");
|
|
5662
|
+
if (!token || indexStopWords.has(token)) continue;
|
|
5663
|
+
tokens.push(token);
|
|
5664
|
+
const singular = singularizeToken(token);
|
|
5665
|
+
if (singular !== token) tokens.push(singular);
|
|
5666
|
+
}
|
|
5667
|
+
return tokens;
|
|
5668
|
+
}
|
|
5669
|
+
function singularizeToken(token) {
|
|
5670
|
+
if (token.length > 3 && token.endsWith("ies")) return `${token.slice(0, -3)}y`;
|
|
5671
|
+
if (token.length > 3 && token.endsWith("s") && !token.endsWith("ss") && !token.endsWith("us")) return token.slice(0, -1);
|
|
5672
|
+
return token;
|
|
5673
|
+
}
|
|
5674
|
+
function formatField(field) {
|
|
5675
|
+
switch (field) {
|
|
5676
|
+
case "path": return "path";
|
|
5677
|
+
case "symbol": return "symbol";
|
|
5678
|
+
case "export": return "export";
|
|
5679
|
+
case "responsibility": return "responsibility";
|
|
5680
|
+
case "summary": return "summary";
|
|
5681
|
+
case "import": return "import";
|
|
5682
|
+
case "test": return "test";
|
|
5683
|
+
case "relationship": return "relationship";
|
|
5684
|
+
case "evidence": return "evidence";
|
|
5685
|
+
}
|
|
5686
|
+
}
|
|
5687
|
+
function isFileNotFoundError$1(error) {
|
|
5688
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
5689
|
+
}
|
|
5690
|
+
function isString(value) {
|
|
5691
|
+
return typeof value === "string";
|
|
5692
|
+
}
|
|
5693
|
+
const indexStopWords = new Set([
|
|
5694
|
+
"a",
|
|
5695
|
+
"an",
|
|
5696
|
+
"and",
|
|
5697
|
+
"are",
|
|
5698
|
+
"as",
|
|
5699
|
+
"at",
|
|
5700
|
+
"be",
|
|
5701
|
+
"by",
|
|
5702
|
+
"for",
|
|
5703
|
+
"from",
|
|
5704
|
+
"in",
|
|
5705
|
+
"is",
|
|
5706
|
+
"it",
|
|
5707
|
+
"of",
|
|
5708
|
+
"on",
|
|
5709
|
+
"or",
|
|
5710
|
+
"the",
|
|
5711
|
+
"to",
|
|
5712
|
+
"with"
|
|
5713
|
+
]);
|
|
5714
|
+
const queryStopWords = new Set([
|
|
5715
|
+
...indexStopWords,
|
|
5716
|
+
"error",
|
|
5717
|
+
"here",
|
|
5718
|
+
"log",
|
|
5719
|
+
"see",
|
|
5720
|
+
"se",
|
|
5721
|
+
"tries",
|
|
5722
|
+
"trying",
|
|
5723
|
+
"user",
|
|
5724
|
+
"when"
|
|
5725
|
+
]);
|
|
5726
|
+
//#endregion
|
|
5727
|
+
//#region src/tui/markdown.ts
|
|
5728
|
+
const codeFenceSentinel = "topchester-code-fence";
|
|
5729
|
+
function renderMarkdown(text, width) {
|
|
5730
|
+
const lines = new Markdown(unwrapMarkdownCodeFences(text), 0, 0, getMarkdownTheme()).render(width);
|
|
5731
|
+
const rendered = [];
|
|
5732
|
+
let inCodeBlock = false;
|
|
5733
|
+
for (const line of lines) {
|
|
5734
|
+
if (line.includes(codeFenceSentinel)) {
|
|
5735
|
+
inCodeBlock = !inCodeBlock;
|
|
5736
|
+
continue;
|
|
5737
|
+
}
|
|
5738
|
+
rendered.push(inCodeBlock ? applyCodeBlockBackground(line) : line);
|
|
5739
|
+
}
|
|
5740
|
+
return rendered;
|
|
5741
|
+
}
|
|
5742
|
+
function unwrapMarkdownCodeFences(text) {
|
|
5743
|
+
return text.replace(/^```(?:markdown|md)\s*\n([\s\S]*?)\n```$/gim, "$1");
|
|
5744
|
+
}
|
|
5745
|
+
function getMarkdownTheme() {
|
|
4544
5746
|
return {
|
|
4545
5747
|
heading: ui.label,
|
|
4546
5748
|
link: ui.label,
|
|
@@ -4607,6 +5809,12 @@ function agentMessage(text, meta) {
|
|
|
4607
5809
|
meta
|
|
4608
5810
|
};
|
|
4609
5811
|
}
|
|
5812
|
+
function thinkingMessage(text) {
|
|
5813
|
+
return {
|
|
5814
|
+
kind: "thinking",
|
|
5815
|
+
text
|
|
5816
|
+
};
|
|
5817
|
+
}
|
|
4610
5818
|
function toolCallMessage(call, label, resultSummary) {
|
|
4611
5819
|
return resultSummary === void 0 ? {
|
|
4612
5820
|
kind: "tool_call",
|
|
@@ -4619,6 +5827,12 @@ function toolCallMessage(call, label, resultSummary) {
|
|
|
4619
5827
|
resultSummary
|
|
4620
5828
|
};
|
|
4621
5829
|
}
|
|
5830
|
+
function subagentMessage(message) {
|
|
5831
|
+
return {
|
|
5832
|
+
kind: "subagent",
|
|
5833
|
+
...message
|
|
5834
|
+
};
|
|
5835
|
+
}
|
|
4622
5836
|
function modalMessage(message) {
|
|
4623
5837
|
return {
|
|
4624
5838
|
kind: "modal",
|
|
@@ -4628,6 +5842,8 @@ function modalMessage(message) {
|
|
|
4628
5842
|
function renderChatMessage(message, options = {}) {
|
|
4629
5843
|
if (message.kind === "modal") return renderChatModal(message, options.selectedActionIndex);
|
|
4630
5844
|
if (message.kind === "tool_call") return renderToolCallMessage(message);
|
|
5845
|
+
if (message.kind === "subagent") return renderSubagentMessage(message);
|
|
5846
|
+
if (message.kind === "thinking") return message.text.split("\n").map((line) => ui.muted(line));
|
|
4631
5847
|
if (message.text.length === 0) return [""];
|
|
4632
5848
|
const lines = message.kind === "agent" && options.width !== void 0 ? renderMarkdown(message.text, Math.max(1, options.width - getPrefix(message.kind).length)) : message.text.split("\n");
|
|
4633
5849
|
if (message.kind === "user") return renderUserMessage(lines);
|
|
@@ -4660,6 +5876,18 @@ function renderToolCallMessage(message) {
|
|
|
4660
5876
|
const visibleLabel = message.resultSummary && !message.label.includes(message.resultSummary) ? `${message.label} ${message.resultSummary}` : message.label;
|
|
4661
5877
|
return [` ${ui.muted(expandTabs(visibleLabel))}`];
|
|
4662
5878
|
}
|
|
5879
|
+
function renderSubagentMessage(message) {
|
|
5880
|
+
const label = message.title ?? shortSessionId(message.sessionId);
|
|
5881
|
+
switch (message.status) {
|
|
5882
|
+
case "running": return [` ${ui.muted(`↳ task: ${label} (running)`)}`];
|
|
5883
|
+
case "event": return message.text ? [` ${ui.muted(`↳ task: ${label}: ${message.text}`)}`] : [];
|
|
5884
|
+
case "completed": return [` ${ui.muted(`↳ task: ${label} (completed)`)}`, ...message.text ? [` ${message.text}`] : []];
|
|
5885
|
+
case "failed": return [` ${ui.warn(`↳ task: ${label} (failed)`)}`, ...message.text ? [` ${message.text}`] : []];
|
|
5886
|
+
}
|
|
5887
|
+
}
|
|
5888
|
+
function shortSessionId(sessionId) {
|
|
5889
|
+
return sessionId.length <= 8 ? sessionId : sessionId.slice(0, 8);
|
|
5890
|
+
}
|
|
4663
5891
|
function expandTabs(line) {
|
|
4664
5892
|
let column = 0;
|
|
4665
5893
|
let expanded = "";
|
|
@@ -4728,11 +5956,21 @@ const jsonValueSchema = z.lazy(() => z.union([
|
|
|
4728
5956
|
const sessionMetadataSchema = z.object({
|
|
4729
5957
|
version: z.literal(1),
|
|
4730
5958
|
sessionId: z.string(),
|
|
5959
|
+
rootSessionId: z.string().optional(),
|
|
5960
|
+
parentSessionId: z.string().optional(),
|
|
5961
|
+
parentToolCallId: z.string().optional(),
|
|
5962
|
+
source: z.enum(["user", "subagent"]).optional(),
|
|
5963
|
+
agentProfileId: z.string().optional(),
|
|
5964
|
+
title: z.string().optional(),
|
|
4731
5965
|
workspaceRoot: z.string().min(1),
|
|
4732
5966
|
createdAt: isoTimestampSchema,
|
|
4733
5967
|
updatedAt: isoTimestampSchema,
|
|
4734
5968
|
lastEventId: z.number().int().min(0)
|
|
4735
|
-
})
|
|
5969
|
+
}).transform((metadata) => ({
|
|
5970
|
+
...metadata,
|
|
5971
|
+
rootSessionId: metadata.rootSessionId ?? metadata.sessionId,
|
|
5972
|
+
source: metadata.source ?? "user"
|
|
5973
|
+
}));
|
|
4736
5974
|
const eventEnvelopeSchema = z.object({
|
|
4737
5975
|
version: z.literal(1),
|
|
4738
5976
|
id: z.number().int().positive(),
|
|
@@ -4753,6 +5991,19 @@ const toolCallPayloadSchema = z.object({
|
|
|
4753
5991
|
label: z.string(),
|
|
4754
5992
|
call: z.record(z.string(), jsonValueSchema)
|
|
4755
5993
|
});
|
|
5994
|
+
const taskPlanItemPayloadSchema = z.object({
|
|
5995
|
+
text: z.string(),
|
|
5996
|
+
status: z.enum([
|
|
5997
|
+
"pending",
|
|
5998
|
+
"in_progress",
|
|
5999
|
+
"completed"
|
|
6000
|
+
])
|
|
6001
|
+
});
|
|
6002
|
+
const taskPlanPayloadSchema = z.object({
|
|
6003
|
+
kind: z.literal("task_plan"),
|
|
6004
|
+
items: z.array(taskPlanItemPayloadSchema),
|
|
6005
|
+
updatedAt: isoTimestampSchema
|
|
6006
|
+
});
|
|
4756
6007
|
const statusPayloadSchema = z.object({
|
|
4757
6008
|
kind: z.literal("status"),
|
|
4758
6009
|
status: z.string()
|
|
@@ -4771,14 +6022,74 @@ const choicePayloadSchema = z.object({
|
|
|
4771
6022
|
value: z.string().optional()
|
|
4772
6023
|
}))
|
|
4773
6024
|
});
|
|
6025
|
+
const subagentLifecycleBasePayloadSchema = z.object({
|
|
6026
|
+
sessionId: z.string(),
|
|
6027
|
+
parentSessionId: z.string(),
|
|
6028
|
+
parentToolCallId: z.string()
|
|
6029
|
+
});
|
|
6030
|
+
const subagentStartedPayloadSchema = subagentLifecycleBasePayloadSchema.extend({
|
|
6031
|
+
kind: z.literal("subagent_started"),
|
|
6032
|
+
agentProfileId: z.string().optional(),
|
|
6033
|
+
title: z.string().optional()
|
|
6034
|
+
});
|
|
6035
|
+
const subagentEventPayloadSchema = subagentLifecycleBasePayloadSchema.extend({
|
|
6036
|
+
kind: z.literal("subagent_event"),
|
|
6037
|
+
event: z.record(z.string(), jsonValueSchema)
|
|
6038
|
+
});
|
|
6039
|
+
const subagentCompletedPayloadSchema = subagentLifecycleBasePayloadSchema.extend({
|
|
6040
|
+
kind: z.literal("subagent_completed"),
|
|
6041
|
+
result: z.string().optional()
|
|
6042
|
+
});
|
|
6043
|
+
const subagentFailedPayloadSchema = subagentLifecycleBasePayloadSchema.extend({
|
|
6044
|
+
kind: z.literal("subagent_failed"),
|
|
6045
|
+
error: z.string()
|
|
6046
|
+
});
|
|
4774
6047
|
const sessionEventPayloadSchema = z.discriminatedUnion("kind", [
|
|
4775
6048
|
messagePayloadSchema,
|
|
4776
6049
|
toolCallPayloadSchema,
|
|
6050
|
+
taskPlanPayloadSchema,
|
|
4777
6051
|
statusPayloadSchema,
|
|
4778
6052
|
knowledgeStatusPayloadSchema,
|
|
4779
|
-
choicePayloadSchema
|
|
6053
|
+
choicePayloadSchema,
|
|
6054
|
+
subagentStartedPayloadSchema,
|
|
6055
|
+
subagentEventPayloadSchema,
|
|
6056
|
+
subagentCompletedPayloadSchema,
|
|
6057
|
+
subagentFailedPayloadSchema
|
|
4780
6058
|
]);
|
|
4781
6059
|
const sessionEventSchema = z.intersection(eventEnvelopeSchema, sessionEventPayloadSchema);
|
|
6060
|
+
const sessionEventPayload = {
|
|
6061
|
+
subagentStarted(reference, options = {}) {
|
|
6062
|
+
return {
|
|
6063
|
+
kind: "subagent_started",
|
|
6064
|
+
...reference,
|
|
6065
|
+
...options
|
|
6066
|
+
};
|
|
6067
|
+
},
|
|
6068
|
+
subagentEvent(reference, event) {
|
|
6069
|
+
return {
|
|
6070
|
+
kind: "subagent_event",
|
|
6071
|
+
...reference,
|
|
6072
|
+
event
|
|
6073
|
+
};
|
|
6074
|
+
},
|
|
6075
|
+
subagentCompleted(reference, result) {
|
|
6076
|
+
return result === void 0 ? {
|
|
6077
|
+
kind: "subagent_completed",
|
|
6078
|
+
...reference
|
|
6079
|
+
} : {
|
|
6080
|
+
kind: "subagent_completed",
|
|
6081
|
+
...reference,
|
|
6082
|
+
result
|
|
6083
|
+
};
|
|
6084
|
+
},
|
|
6085
|
+
subagentFailed(reference, error) {
|
|
6086
|
+
return {
|
|
6087
|
+
kind: "subagent_failed",
|
|
6088
|
+
...reference,
|
|
6089
|
+
error
|
|
6090
|
+
};
|
|
6091
|
+
}
|
|
6092
|
+
};
|
|
4782
6093
|
//#endregion
|
|
4783
6094
|
//#region src/session/store.ts
|
|
4784
6095
|
const SESSION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
@@ -4794,6 +6105,8 @@ async function createSession(workspaceRoot) {
|
|
|
4794
6105
|
const metadata = {
|
|
4795
6106
|
version: 1,
|
|
4796
6107
|
sessionId,
|
|
6108
|
+
rootSessionId: sessionId,
|
|
6109
|
+
source: "user",
|
|
4797
6110
|
workspaceRoot,
|
|
4798
6111
|
createdAt,
|
|
4799
6112
|
updatedAt: createdAt,
|
|
@@ -4804,6 +6117,41 @@ async function createSession(workspaceRoot) {
|
|
|
4804
6117
|
await writeFile(eventsPath, "", { flag: "wx" });
|
|
4805
6118
|
return buildHandle(sessionDir, metadata);
|
|
4806
6119
|
}
|
|
6120
|
+
async function createChildSession(workspaceRoot, options) {
|
|
6121
|
+
validateSessionId(options.parent.sessionId);
|
|
6122
|
+
const sessionId = generateSessionId();
|
|
6123
|
+
const sessionDir = join(getTopchesterSessionsPath(workspaceRoot), sessionId);
|
|
6124
|
+
const metadataPath = join(sessionDir, "metadata.json");
|
|
6125
|
+
const eventsPath = join(sessionDir, "events.jsonl");
|
|
6126
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6127
|
+
const metadata = {
|
|
6128
|
+
version: 1,
|
|
6129
|
+
sessionId,
|
|
6130
|
+
rootSessionId: options.parent.metadata.rootSessionId,
|
|
6131
|
+
parentSessionId: options.parent.sessionId,
|
|
6132
|
+
parentToolCallId: options.parentToolCallId,
|
|
6133
|
+
source: "subagent",
|
|
6134
|
+
...options.agentProfileId === void 0 ? {} : { agentProfileId: options.agentProfileId },
|
|
6135
|
+
...options.title === void 0 ? {} : { title: options.title },
|
|
6136
|
+
workspaceRoot,
|
|
6137
|
+
createdAt,
|
|
6138
|
+
updatedAt: createdAt,
|
|
6139
|
+
lastEventId: 0
|
|
6140
|
+
};
|
|
6141
|
+
await mkdir(sessionDir, { recursive: true });
|
|
6142
|
+
await writeMetadata(metadataPath, metadata);
|
|
6143
|
+
await writeFile(eventsPath, "", { flag: "wx" });
|
|
6144
|
+
const child = buildHandle(sessionDir, metadata);
|
|
6145
|
+
if (options.recordParentEvent ?? true) await options.parent.append(sessionEventPayload.subagentStarted({
|
|
6146
|
+
sessionId: child.sessionId,
|
|
6147
|
+
parentSessionId: options.parent.sessionId,
|
|
6148
|
+
parentToolCallId: options.parentToolCallId
|
|
6149
|
+
}, {
|
|
6150
|
+
...options.agentProfileId === void 0 ? {} : { agentProfileId: options.agentProfileId },
|
|
6151
|
+
...options.title === void 0 ? {} : { title: options.title }
|
|
6152
|
+
}));
|
|
6153
|
+
return child;
|
|
6154
|
+
}
|
|
4807
6155
|
async function loadSessionForAppend(workspaceRoot, sessionId) {
|
|
4808
6156
|
const loaded = await loadSession(workspaceRoot, sessionId);
|
|
4809
6157
|
return buildHandle(loaded.sessionDir, loaded.metadata);
|
|
@@ -4854,6 +6202,7 @@ async function resolveLatestSessionId(workspaceRoot) {
|
|
|
4854
6202
|
function rehydrateSession(events) {
|
|
4855
6203
|
const messages = [];
|
|
4856
6204
|
let status;
|
|
6205
|
+
let taskPlan;
|
|
4857
6206
|
let visibleOnlyActionValues = /* @__PURE__ */ new Set();
|
|
4858
6207
|
for (const event of events) switch (event.kind) {
|
|
4859
6208
|
case "message":
|
|
@@ -4869,7 +6218,17 @@ function rehydrateSession(events) {
|
|
|
4869
6218
|
case "tool_call":
|
|
4870
6219
|
messages.push(toolCallMessage(event.call, event.label));
|
|
4871
6220
|
break;
|
|
6221
|
+
case "task_plan":
|
|
6222
|
+
taskPlan = {
|
|
6223
|
+
items: event.items,
|
|
6224
|
+
updatedAt: event.updatedAt
|
|
6225
|
+
};
|
|
6226
|
+
break;
|
|
4872
6227
|
case "knowledge_status": break;
|
|
6228
|
+
case "subagent_started":
|
|
6229
|
+
case "subagent_event":
|
|
6230
|
+
case "subagent_completed":
|
|
6231
|
+
case "subagent_failed": break;
|
|
4873
6232
|
case "choice":
|
|
4874
6233
|
messages.push({
|
|
4875
6234
|
kind: "modal",
|
|
@@ -4886,7 +6245,8 @@ function rehydrateSession(events) {
|
|
|
4886
6245
|
}
|
|
4887
6246
|
return {
|
|
4888
6247
|
messages,
|
|
4889
|
-
status
|
|
6248
|
+
status,
|
|
6249
|
+
...taskPlan === void 0 ? {} : { taskPlan }
|
|
4890
6250
|
};
|
|
4891
6251
|
}
|
|
4892
6252
|
function buildHandle(sessionDir, metadata) {
|
|
@@ -5167,19 +6527,25 @@ var BusyIndicator = class {
|
|
|
5167
6527
|
this.tui.requestRender();
|
|
5168
6528
|
}, 80);
|
|
5169
6529
|
}
|
|
5170
|
-
stop() {
|
|
6530
|
+
stop(options = {}) {
|
|
5171
6531
|
if (this.timer) {
|
|
5172
6532
|
clearInterval(this.timer);
|
|
5173
6533
|
this.timer = void 0;
|
|
5174
6534
|
}
|
|
5175
6535
|
this.app.setPromptHint(void 0);
|
|
5176
|
-
this.app.setEphemeralLine(void 0);
|
|
6536
|
+
if (options.clearEphemeralLine ?? true) this.app.setEphemeralLine(void 0);
|
|
5177
6537
|
}
|
|
5178
6538
|
setActivity(activity) {
|
|
5179
6539
|
this.activityOverride = activity;
|
|
5180
6540
|
this.render();
|
|
5181
6541
|
this.tui.requestRender();
|
|
5182
6542
|
}
|
|
6543
|
+
clearActivity() {
|
|
6544
|
+
if (!this.activityOverride) return;
|
|
6545
|
+
this.activityOverride = void 0;
|
|
6546
|
+
this.render();
|
|
6547
|
+
this.tui.requestRender();
|
|
6548
|
+
}
|
|
5183
6549
|
render() {
|
|
5184
6550
|
if (this.activityOverride) {
|
|
5185
6551
|
this.app.setEphemeralLine(`${this.frames[this.index]} ${this.activityOverride}`);
|
|
@@ -5190,6 +6556,33 @@ var BusyIndicator = class {
|
|
|
5190
6556
|
this.app.setEphemeralLine(`${this.frames[this.index]} ${this.options.activities[activityIndex]}`);
|
|
5191
6557
|
}
|
|
5192
6558
|
};
|
|
6559
|
+
var ReasoningTailBuffer = class {
|
|
6560
|
+
text = "";
|
|
6561
|
+
get hasText() {
|
|
6562
|
+
return this.text.length > 0;
|
|
6563
|
+
}
|
|
6564
|
+
get value() {
|
|
6565
|
+
return this.text;
|
|
6566
|
+
}
|
|
6567
|
+
append(delta) {
|
|
6568
|
+
const normalized = normalizeReasoningText(`${this.text}${delta}`);
|
|
6569
|
+
if (!normalized) return;
|
|
6570
|
+
this.text = normalized;
|
|
6571
|
+
return this.text;
|
|
6572
|
+
}
|
|
6573
|
+
replace(summary) {
|
|
6574
|
+
const normalized = normalizeReasoningText(summary);
|
|
6575
|
+
if (!normalized) return;
|
|
6576
|
+
this.text = normalized;
|
|
6577
|
+
return this.text;
|
|
6578
|
+
}
|
|
6579
|
+
clear() {
|
|
6580
|
+
this.text = "";
|
|
6581
|
+
}
|
|
6582
|
+
};
|
|
6583
|
+
function normalizeReasoningText(text) {
|
|
6584
|
+
return text.replace(/\s+/gu, " ").trim();
|
|
6585
|
+
}
|
|
5193
6586
|
//#endregion
|
|
5194
6587
|
//#region src/agent/commands.ts
|
|
5195
6588
|
const slashCommandSuggestions = [
|
|
@@ -5264,6 +6657,96 @@ async function executeKbCommand(args, context) {
|
|
|
5264
6657
|
return { messages: ["Usage: /kb init, /kb compile, /kb sync, /kb reset, or /kb status"] };
|
|
5265
6658
|
}
|
|
5266
6659
|
//#endregion
|
|
6660
|
+
//#region src/agent/events.ts
|
|
6661
|
+
const ABORT_CHOICE_VALUE = "__topchester_abort__";
|
|
6662
|
+
const agentEvent = {
|
|
6663
|
+
status(status) {
|
|
6664
|
+
return {
|
|
6665
|
+
type: "status",
|
|
6666
|
+
status
|
|
6667
|
+
};
|
|
6668
|
+
},
|
|
6669
|
+
systemMessage(text) {
|
|
6670
|
+
return {
|
|
6671
|
+
type: "message",
|
|
6672
|
+
role: "system",
|
|
6673
|
+
text
|
|
6674
|
+
};
|
|
6675
|
+
},
|
|
6676
|
+
assistantMessage(text, meta) {
|
|
6677
|
+
return meta === void 0 ? {
|
|
6678
|
+
type: "message",
|
|
6679
|
+
role: "assistant",
|
|
6680
|
+
text
|
|
6681
|
+
} : {
|
|
6682
|
+
type: "message",
|
|
6683
|
+
role: "assistant",
|
|
6684
|
+
text,
|
|
6685
|
+
meta
|
|
6686
|
+
};
|
|
6687
|
+
},
|
|
6688
|
+
toolCall(call, label) {
|
|
6689
|
+
return {
|
|
6690
|
+
type: "tool_call",
|
|
6691
|
+
call,
|
|
6692
|
+
label
|
|
6693
|
+
};
|
|
6694
|
+
},
|
|
6695
|
+
taskPlan(plan) {
|
|
6696
|
+
return {
|
|
6697
|
+
type: "task_plan",
|
|
6698
|
+
plan
|
|
6699
|
+
};
|
|
6700
|
+
},
|
|
6701
|
+
knowledgeStatus(status, guidance) {
|
|
6702
|
+
return guidance === void 0 ? {
|
|
6703
|
+
type: "knowledge_status",
|
|
6704
|
+
status
|
|
6705
|
+
} : {
|
|
6706
|
+
type: "knowledge_status",
|
|
6707
|
+
status,
|
|
6708
|
+
guidance
|
|
6709
|
+
};
|
|
6710
|
+
},
|
|
6711
|
+
choice(options) {
|
|
6712
|
+
return {
|
|
6713
|
+
type: "choice",
|
|
6714
|
+
...options
|
|
6715
|
+
};
|
|
6716
|
+
},
|
|
6717
|
+
subagentStarted(options) {
|
|
6718
|
+
return {
|
|
6719
|
+
type: "subagent_started",
|
|
6720
|
+
...options
|
|
6721
|
+
};
|
|
6722
|
+
},
|
|
6723
|
+
subagentEvent(options, event) {
|
|
6724
|
+
return {
|
|
6725
|
+
type: "subagent_event",
|
|
6726
|
+
...options,
|
|
6727
|
+
event
|
|
6728
|
+
};
|
|
6729
|
+
},
|
|
6730
|
+
subagentCompleted(options) {
|
|
6731
|
+
return {
|
|
6732
|
+
type: "subagent_completed",
|
|
6733
|
+
...options
|
|
6734
|
+
};
|
|
6735
|
+
},
|
|
6736
|
+
subagentFailed(options) {
|
|
6737
|
+
return {
|
|
6738
|
+
type: "subagent_failed",
|
|
6739
|
+
...options
|
|
6740
|
+
};
|
|
6741
|
+
}
|
|
6742
|
+
};
|
|
6743
|
+
function choiceAction(label, value) {
|
|
6744
|
+
return value === void 0 ? { label } : {
|
|
6745
|
+
label,
|
|
6746
|
+
value
|
|
6747
|
+
};
|
|
6748
|
+
}
|
|
6749
|
+
//#endregion
|
|
5267
6750
|
//#region src/tui/keys.ts
|
|
5268
6751
|
function isUpKey(data) {
|
|
5269
6752
|
return matchesKey(data, "up") || data === "\x1B[A";
|
|
@@ -5274,6 +6757,9 @@ function isDownKey(data) {
|
|
|
5274
6757
|
function isEnterKey(data) {
|
|
5275
6758
|
return matchesKey(data, "enter") || data === "\n" || data === "\r";
|
|
5276
6759
|
}
|
|
6760
|
+
function isNewLineKey(data) {
|
|
6761
|
+
return matchesKey(data, "shift+enter") || matchesKey(data, "alt+enter") || matchesKey(data, "ctrl+enter") || data === "\x1B\r" || data === "\x1B[13;2~";
|
|
6762
|
+
}
|
|
5277
6763
|
function isTabKey(data) {
|
|
5278
6764
|
return matchesKey(data, "tab") || data === " ";
|
|
5279
6765
|
}
|
|
@@ -5380,12 +6866,14 @@ function getStartupThreadMessages(context) {
|
|
|
5380
6866
|
lines.push("Ask Topchester what you want to change.");
|
|
5381
6867
|
return [systemMessage(lines.join("\n"))];
|
|
5382
6868
|
}
|
|
5383
|
-
function renderStaticLayout(messages, folderName = "", modelLabel = "") {
|
|
6869
|
+
function renderStaticLayout(messages, folderName = "", modelLabel = "", taskPlan) {
|
|
5384
6870
|
const threadLines = messages.flatMap((message) => renderChatMessage(message));
|
|
5385
6871
|
const status = formatStatusLine(folderName, modelLabel);
|
|
6872
|
+
const planLines = taskPlan && taskPlan.items.length > 0 ? [...formatTaskPlanForTui(taskPlan, 72), ""] : [];
|
|
5386
6873
|
return [
|
|
5387
6874
|
...threadLines,
|
|
5388
6875
|
"",
|
|
6876
|
+
...planLines,
|
|
5389
6877
|
"┌──────────────────────────────────────────────────────────────────────┐",
|
|
5390
6878
|
"│ > │",
|
|
5391
6879
|
"└──────────────────────────────────────────────────────────────────────┘",
|
|
@@ -5467,23 +6955,35 @@ function stripAnsi(text) {
|
|
|
5467
6955
|
}
|
|
5468
6956
|
//#endregion
|
|
5469
6957
|
//#region src/tui/layout.ts
|
|
6958
|
+
const PROMPT_VISIBLE_CONTENT_LINES = 5;
|
|
6959
|
+
const PASTE_PREVIEW_MIN_LINES = 6;
|
|
6960
|
+
const PASTE_PREVIEW_MIN_CHARS = 500;
|
|
6961
|
+
const BRACKETED_PASTE_START = "\x1B[200~";
|
|
6962
|
+
const BRACKETED_PASTE_END = "\x1B[201~";
|
|
5470
6963
|
var ChatLayout = class {
|
|
5471
6964
|
terminal;
|
|
5472
6965
|
messages;
|
|
5473
6966
|
folderName;
|
|
5474
6967
|
modelLabel;
|
|
5475
|
-
|
|
6968
|
+
inputFocused = false;
|
|
6969
|
+
promptValue = "";
|
|
6970
|
+
promptCursor = 0;
|
|
5476
6971
|
status = "ready";
|
|
5477
6972
|
knowledgeStatus;
|
|
5478
6973
|
ephemeralLine;
|
|
6974
|
+
taskPlanNoticeLine;
|
|
5479
6975
|
noticeLine;
|
|
5480
6976
|
promptHint;
|
|
6977
|
+
taskPlan;
|
|
5481
6978
|
cancelPending;
|
|
5482
6979
|
submitMessage;
|
|
5483
6980
|
submitCommand;
|
|
5484
6981
|
activeModalActionIndex = 0;
|
|
5485
6982
|
activeSlashSuggestionIndex = 0;
|
|
5486
6983
|
threadScrollOffset = 0;
|
|
6984
|
+
pasteBuffer;
|
|
6985
|
+
pasteCounter = 0;
|
|
6986
|
+
pastedContent = /* @__PURE__ */ new Map();
|
|
5487
6987
|
promptHistory = new PromptHistory();
|
|
5488
6988
|
exitAgent;
|
|
5489
6989
|
transcriptMode;
|
|
@@ -5494,14 +6994,6 @@ var ChatLayout = class {
|
|
|
5494
6994
|
this.modelLabel = modelLabel;
|
|
5495
6995
|
this.exitAgent = typeof options === "function" ? options : options.exitAgent ?? (() => {});
|
|
5496
6996
|
this.transcriptMode = typeof options === "function" ? "viewport" : options.transcriptMode ?? "viewport";
|
|
5497
|
-
this.input.onSubmit = (value) => {
|
|
5498
|
-
if (value.trim().length > 0) {
|
|
5499
|
-
const message = value.trim();
|
|
5500
|
-
this.addMessage(userMessage(message));
|
|
5501
|
-
this.input.setValue("");
|
|
5502
|
-
this.submitUserInput(message);
|
|
5503
|
-
}
|
|
5504
|
-
};
|
|
5505
6997
|
}
|
|
5506
6998
|
addMessage(message) {
|
|
5507
6999
|
this.messages.push(message);
|
|
@@ -5514,6 +7006,24 @@ var ChatLayout = class {
|
|
|
5514
7006
|
setKnowledgeStatus(status) {
|
|
5515
7007
|
this.knowledgeStatus = formatKnowledgeFooterStatus(status);
|
|
5516
7008
|
}
|
|
7009
|
+
setTaskPlan(plan) {
|
|
7010
|
+
const change = detectTaskPlanChange(this.taskPlan, plan);
|
|
7011
|
+
this.taskPlan = plan && plan.items.length > 0 ? plan : void 0;
|
|
7012
|
+
return change;
|
|
7013
|
+
}
|
|
7014
|
+
setTaskPlanNotice(line) {
|
|
7015
|
+
this.taskPlanNoticeLine = line;
|
|
7016
|
+
}
|
|
7017
|
+
clearTaskPlan(now = /* @__PURE__ */ new Date()) {
|
|
7018
|
+
if (!this.taskPlan) return;
|
|
7019
|
+
const cleared = {
|
|
7020
|
+
items: [],
|
|
7021
|
+
updatedAt: now.toISOString()
|
|
7022
|
+
};
|
|
7023
|
+
this.taskPlan = void 0;
|
|
7024
|
+
this.taskPlanNoticeLine = void 0;
|
|
7025
|
+
return cleared;
|
|
7026
|
+
}
|
|
5517
7027
|
isReady() {
|
|
5518
7028
|
return this.status === "ready";
|
|
5519
7029
|
}
|
|
@@ -5536,7 +7046,10 @@ var ChatLayout = class {
|
|
|
5536
7046
|
this.submitCommand = submit;
|
|
5537
7047
|
}
|
|
5538
7048
|
setInputValue(value) {
|
|
5539
|
-
this.
|
|
7049
|
+
this.promptValue = value;
|
|
7050
|
+
this.promptCursor = value.length;
|
|
7051
|
+
this.pastedContent.clear();
|
|
7052
|
+
this.pasteCounter = 0;
|
|
5540
7053
|
}
|
|
5541
7054
|
getConversationTurns() {
|
|
5542
7055
|
return this.messages.flatMap((message) => {
|
|
@@ -5550,16 +7063,18 @@ var ChatLayout = class {
|
|
|
5550
7063
|
text: message.text
|
|
5551
7064
|
}];
|
|
5552
7065
|
case "system":
|
|
7066
|
+
case "thinking":
|
|
5553
7067
|
case "tool_call":
|
|
7068
|
+
case "subagent":
|
|
5554
7069
|
case "modal": return [];
|
|
5555
7070
|
}
|
|
5556
7071
|
});
|
|
5557
7072
|
}
|
|
5558
7073
|
get focused() {
|
|
5559
|
-
return this.
|
|
7074
|
+
return this.inputFocused;
|
|
5560
7075
|
}
|
|
5561
7076
|
set focused(value) {
|
|
5562
|
-
this.
|
|
7077
|
+
this.inputFocused = value;
|
|
5563
7078
|
}
|
|
5564
7079
|
handleInput(data) {
|
|
5565
7080
|
if (this.cancelPending && matchesKey(data, "escape")) {
|
|
@@ -5568,15 +7083,15 @@ var ChatLayout = class {
|
|
|
5568
7083
|
}
|
|
5569
7084
|
if (this.handleModalInput(data)) return;
|
|
5570
7085
|
if (this.handleSlashSuggestionInput(data)) return;
|
|
7086
|
+
if (this.handlePromptPasteInput(data)) return;
|
|
7087
|
+
if (this.handlePromptNewLineInput(data)) return;
|
|
7088
|
+
if (this.handlePromptSubmitInput(data)) return;
|
|
5571
7089
|
if (this.handleThreadScrollInput(data)) return;
|
|
7090
|
+
if (this.handlePromptVerticalCursorInput(data)) return;
|
|
5572
7091
|
if (this.handlePromptHistoryInput(data)) return;
|
|
5573
|
-
|
|
5574
|
-
this.input.handleInput(data);
|
|
5575
|
-
if (this.input.getValue() !== previousInput) this.promptHistory.resetBrowsing();
|
|
5576
|
-
}
|
|
5577
|
-
invalidate() {
|
|
5578
|
-
this.input.invalidate();
|
|
7092
|
+
this.handlePromptEditInput(data);
|
|
5579
7093
|
}
|
|
7094
|
+
invalidate() {}
|
|
5580
7095
|
render(width) {
|
|
5581
7096
|
const safeWidth = Math.max(20, width);
|
|
5582
7097
|
const footerLines = this.getActiveModal() ? this.renderModalHelp(safeWidth) : this.renderPrompt(safeWidth);
|
|
@@ -5604,6 +7119,7 @@ var ChatLayout = class {
|
|
|
5604
7119
|
return [...this.renderThreadMessageLines(messageLines, innerWidth, width, message.kind === "user"), ...spacer];
|
|
5605
7120
|
});
|
|
5606
7121
|
if (this.ephemeralLine) lines.push(...this.renderThreadMessageLines([` ${this.ephemeralLine}`], innerWidth, width, false));
|
|
7122
|
+
if (this.taskPlanNoticeLine) lines.push(...this.renderThreadMessageLines([` ${this.taskPlanNoticeLine}`], innerWidth, width, false));
|
|
5607
7123
|
if (this.noticeLine) lines.push(...this.renderThreadMessageLines([` ${this.noticeLine}`], innerWidth, width, false));
|
|
5608
7124
|
return lines;
|
|
5609
7125
|
}
|
|
@@ -5619,17 +7135,69 @@ var ChatLayout = class {
|
|
|
5619
7135
|
const bottom = `└${"─".repeat(Math.max(0, width - 2))}┘`;
|
|
5620
7136
|
const prefix = "> ";
|
|
5621
7137
|
const innerWidth = Math.max(1, width - 4 - 2);
|
|
5622
|
-
const
|
|
7138
|
+
const inputLines = this.promptHint ? [truncateToWidth(ui.label(this.promptHint), innerWidth, "…", true)] : this.renderPromptInputLines(innerWidth);
|
|
5623
7139
|
const statusInnerWidth = Math.max(1, width - 2);
|
|
5624
7140
|
const status = truncateToWidth(` ${formatStatusLine(this.folderName, this.modelLabel, this.status, this.knowledgeStatus, statusInnerWidth)} `, width, "…", true);
|
|
5625
7141
|
return [
|
|
5626
7142
|
...this.renderSlashSuggestions(width),
|
|
7143
|
+
...this.renderTaskPlan(width),
|
|
5627
7144
|
top,
|
|
5628
|
-
`│ ${prefix}${
|
|
7145
|
+
...inputLines.map((line, index) => `│ ${index === 0 ? prefix : " "}${padPromptInputLine(line, innerWidth)} │`),
|
|
5629
7146
|
bottom,
|
|
5630
7147
|
status
|
|
5631
7148
|
];
|
|
5632
7149
|
}
|
|
7150
|
+
renderPromptInputLines(innerWidth) {
|
|
7151
|
+
const value = this.promptValue;
|
|
7152
|
+
if (!value.includes("\n")) return [this.renderPromptLineWithCursor(value, this.promptCursor, innerWidth)];
|
|
7153
|
+
const rows = this.getPromptRows(innerWidth);
|
|
7154
|
+
const cursorRowIndex = rows.findIndex((row) => this.promptCursor >= row.start && this.promptCursor <= row.end);
|
|
7155
|
+
const latestStart = Math.max(0, rows.length - PROMPT_VISIBLE_CONTENT_LINES);
|
|
7156
|
+
const visibleStart = cursorRowIndex === -1 ? latestStart : Math.min(Math.max(0, cursorRowIndex - 2), latestStart);
|
|
7157
|
+
return rows.slice(visibleStart, visibleStart + PROMPT_VISIBLE_CONTENT_LINES).map((row) => {
|
|
7158
|
+
if (this.promptCursor >= row.start && this.promptCursor <= row.end) return this.renderPromptLineWithCursor(row.text, this.promptCursor - row.start, innerWidth);
|
|
7159
|
+
return truncateToWidth(row.text.length === 0 ? " " : row.text, innerWidth, "…", true);
|
|
7160
|
+
});
|
|
7161
|
+
}
|
|
7162
|
+
getPromptRows(width) {
|
|
7163
|
+
const rows = [];
|
|
7164
|
+
let offset = 0;
|
|
7165
|
+
for (const line of this.promptValue.split("\n")) {
|
|
7166
|
+
if (line.length === 0) rows.push({
|
|
7167
|
+
text: "",
|
|
7168
|
+
start: offset,
|
|
7169
|
+
end: offset
|
|
7170
|
+
});
|
|
7171
|
+
else for (let index = 0; index < line.length; index += width) {
|
|
7172
|
+
const text = line.slice(index, index + width);
|
|
7173
|
+
rows.push({
|
|
7174
|
+
text,
|
|
7175
|
+
start: offset + index,
|
|
7176
|
+
end: offset + index + text.length
|
|
7177
|
+
});
|
|
7178
|
+
}
|
|
7179
|
+
offset += line.length + 1;
|
|
7180
|
+
}
|
|
7181
|
+
return rows.length > 0 ? rows : [{
|
|
7182
|
+
text: "",
|
|
7183
|
+
start: 0,
|
|
7184
|
+
end: 0
|
|
7185
|
+
}];
|
|
7186
|
+
}
|
|
7187
|
+
renderPromptLineWithCursor(text, cursor, width) {
|
|
7188
|
+
const safeCursor = Math.max(0, Math.min(cursor, text.length));
|
|
7189
|
+
const windowStart = safeCursor >= width ? safeCursor - width + 1 : 0;
|
|
7190
|
+
const visibleText = text.slice(windowStart, windowStart + width);
|
|
7191
|
+
const visibleCursor = safeCursor - windowStart;
|
|
7192
|
+
const beforeCursor = visibleText.slice(0, visibleCursor);
|
|
7193
|
+
const cursorChar = visibleText[visibleCursor] ?? " ";
|
|
7194
|
+
const afterCursor = visibleText.slice(visibleCursor + cursorChar.length);
|
|
7195
|
+
return truncateToWidth(`${beforeCursor}${this.inputFocused ? CURSOR_MARKER : ""}\u001b[7m${cursorChar}\u001b[27m${afterCursor}`, width, "…", true);
|
|
7196
|
+
}
|
|
7197
|
+
renderTaskPlan(width) {
|
|
7198
|
+
if (!this.taskPlan) return [];
|
|
7199
|
+
return formatTaskPlanForTui(this.taskPlan, Math.max(1, width));
|
|
7200
|
+
}
|
|
5633
7201
|
renderSlashSuggestions(width) {
|
|
5634
7202
|
const suggestions = this.getSlashSuggestions();
|
|
5635
7203
|
if (suggestions.length === 0 || this.promptHint) return [];
|
|
@@ -5676,6 +7244,14 @@ var ChatLayout = class {
|
|
|
5676
7244
|
this.exitAgent();
|
|
5677
7245
|
return true;
|
|
5678
7246
|
}
|
|
7247
|
+
if (action.value === "__topchester_abort__") {
|
|
7248
|
+
this.addMessage({
|
|
7249
|
+
kind: "user",
|
|
7250
|
+
text: action.label,
|
|
7251
|
+
modelContext: false
|
|
7252
|
+
});
|
|
7253
|
+
return true;
|
|
7254
|
+
}
|
|
5679
7255
|
this.submitModalAction(action.value ?? action.label);
|
|
5680
7256
|
return true;
|
|
5681
7257
|
}
|
|
@@ -5718,17 +7294,196 @@ var ChatLayout = class {
|
|
|
5718
7294
|
handlePromptHistoryInput(data) {
|
|
5719
7295
|
if (this.promptHint) return false;
|
|
5720
7296
|
if (isUpKey(data)) {
|
|
5721
|
-
const prompt = this.promptHistory.previous(this.
|
|
5722
|
-
if (prompt !== void 0)
|
|
7297
|
+
const prompt = this.promptHistory.previous(this.promptValue);
|
|
7298
|
+
if (prompt !== void 0) {
|
|
7299
|
+
this.promptValue = prompt;
|
|
7300
|
+
this.promptCursor = prompt.length;
|
|
7301
|
+
}
|
|
5723
7302
|
return true;
|
|
5724
7303
|
}
|
|
5725
7304
|
if (isDownKey(data)) {
|
|
5726
7305
|
const prompt = this.promptHistory.next();
|
|
5727
|
-
if (prompt !== void 0)
|
|
7306
|
+
if (prompt !== void 0) {
|
|
7307
|
+
this.promptValue = prompt;
|
|
7308
|
+
this.promptCursor = prompt.length;
|
|
7309
|
+
}
|
|
5728
7310
|
return true;
|
|
5729
7311
|
}
|
|
5730
7312
|
return false;
|
|
5731
7313
|
}
|
|
7314
|
+
handlePromptVerticalCursorInput(data) {
|
|
7315
|
+
if (this.promptHint || !this.promptValue.includes("\n")) return false;
|
|
7316
|
+
if (isUpKey(data)) {
|
|
7317
|
+
if (this.canMovePromptCursorVertically(-1)) {
|
|
7318
|
+
this.movePromptCursorVertically(-1);
|
|
7319
|
+
return true;
|
|
7320
|
+
}
|
|
7321
|
+
if (this.promptCursor > 0) {
|
|
7322
|
+
this.promptCursor = this.getCurrentPromptLineStart();
|
|
7323
|
+
return true;
|
|
7324
|
+
}
|
|
7325
|
+
return false;
|
|
7326
|
+
}
|
|
7327
|
+
if (isDownKey(data)) {
|
|
7328
|
+
if (this.canMovePromptCursorVertically(1)) {
|
|
7329
|
+
this.movePromptCursorVertically(1);
|
|
7330
|
+
return true;
|
|
7331
|
+
}
|
|
7332
|
+
if (this.promptCursor < this.promptValue.length) {
|
|
7333
|
+
this.promptCursor = this.getCurrentPromptLineEnd();
|
|
7334
|
+
return true;
|
|
7335
|
+
}
|
|
7336
|
+
return false;
|
|
7337
|
+
}
|
|
7338
|
+
return false;
|
|
7339
|
+
}
|
|
7340
|
+
canMovePromptCursorVertically(delta) {
|
|
7341
|
+
const lines = this.promptValue.split("\n");
|
|
7342
|
+
const current = this.getPromptLineCursor(lines);
|
|
7343
|
+
if (delta === -1) return current.line > 0;
|
|
7344
|
+
return current.line < lines.length - 1;
|
|
7345
|
+
}
|
|
7346
|
+
handlePromptNewLineInput(data) {
|
|
7347
|
+
if (this.promptHint || !isNewLineKey(data)) return false;
|
|
7348
|
+
this.insertPromptText("\n");
|
|
7349
|
+
this.promptHistory.resetBrowsing();
|
|
7350
|
+
return true;
|
|
7351
|
+
}
|
|
7352
|
+
handlePromptSubmitInput(data) {
|
|
7353
|
+
if (this.promptHint || !isEnterKey(data)) return false;
|
|
7354
|
+
this.submitPromptValue();
|
|
7355
|
+
return true;
|
|
7356
|
+
}
|
|
7357
|
+
handlePromptPasteInput(data) {
|
|
7358
|
+
if (this.promptHint) return false;
|
|
7359
|
+
if (this.pasteBuffer !== void 0) {
|
|
7360
|
+
this.pasteBuffer += data;
|
|
7361
|
+
this.flushPromptPasteBuffer();
|
|
7362
|
+
return true;
|
|
7363
|
+
}
|
|
7364
|
+
const startIndex = data.indexOf(BRACKETED_PASTE_START);
|
|
7365
|
+
if (startIndex === -1) return false;
|
|
7366
|
+
const beforePaste = data.slice(0, startIndex);
|
|
7367
|
+
if (beforePaste.length > 0) this.insertPromptText(beforePaste);
|
|
7368
|
+
this.pasteBuffer = data.slice(startIndex + 6);
|
|
7369
|
+
this.flushPromptPasteBuffer();
|
|
7370
|
+
this.promptHistory.resetBrowsing();
|
|
7371
|
+
return true;
|
|
7372
|
+
}
|
|
7373
|
+
flushPromptPasteBuffer() {
|
|
7374
|
+
if (this.pasteBuffer === void 0) return;
|
|
7375
|
+
const endIndex = this.pasteBuffer.indexOf(BRACKETED_PASTE_END);
|
|
7376
|
+
if (endIndex === -1) return;
|
|
7377
|
+
const pasted = this.pasteBuffer.slice(0, endIndex);
|
|
7378
|
+
const remaining = this.pasteBuffer.slice(endIndex + 6);
|
|
7379
|
+
this.pasteBuffer = void 0;
|
|
7380
|
+
this.insertPastedText(pasted);
|
|
7381
|
+
if (remaining.length > 0) this.handleInput(remaining);
|
|
7382
|
+
}
|
|
7383
|
+
insertPastedText(text) {
|
|
7384
|
+
const normalizedText = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\t/g, " ");
|
|
7385
|
+
const trimmedText = normalizedText.trim();
|
|
7386
|
+
if (trimmedText.length === 0) return;
|
|
7387
|
+
const lineCount = trimmedText.split("\n").length;
|
|
7388
|
+
if (lineCount >= PASTE_PREVIEW_MIN_LINES || trimmedText.length >= PASTE_PREVIEW_MIN_CHARS) {
|
|
7389
|
+
this.pasteCounter += 1;
|
|
7390
|
+
const marker = `[Pasted #${this.pasteCounter} ${lineCount} lines ${trimmedText.length} chars]`;
|
|
7391
|
+
this.pastedContent.set(marker, trimmedText);
|
|
7392
|
+
this.insertPromptText(marker);
|
|
7393
|
+
return;
|
|
7394
|
+
}
|
|
7395
|
+
this.insertPromptText(normalizedText);
|
|
7396
|
+
}
|
|
7397
|
+
insertPromptText(text) {
|
|
7398
|
+
this.promptValue = `${this.promptValue.slice(0, this.promptCursor)}${text}${this.promptValue.slice(this.promptCursor)}`;
|
|
7399
|
+
this.promptCursor += text.length;
|
|
7400
|
+
}
|
|
7401
|
+
expandPastedContent(value) {
|
|
7402
|
+
let expanded = value;
|
|
7403
|
+
for (const [marker, content] of this.pastedContent) expanded = expanded.split(marker).join(content);
|
|
7404
|
+
return expanded;
|
|
7405
|
+
}
|
|
7406
|
+
submitPromptValue() {
|
|
7407
|
+
if (this.promptValue.trim().length === 0) return;
|
|
7408
|
+
const message = this.expandPastedContent(this.promptValue).trim();
|
|
7409
|
+
this.addMessage(userMessage(message));
|
|
7410
|
+
this.promptValue = "";
|
|
7411
|
+
this.promptCursor = 0;
|
|
7412
|
+
this.pastedContent.clear();
|
|
7413
|
+
this.pasteCounter = 0;
|
|
7414
|
+
this.submitUserInput(message);
|
|
7415
|
+
}
|
|
7416
|
+
handlePromptEditInput(data) {
|
|
7417
|
+
if (this.promptHint) return false;
|
|
7418
|
+
if (matchesKey(data, "left") || data === "\x1B[D") {
|
|
7419
|
+
this.promptCursor = Math.max(0, this.promptCursor - 1);
|
|
7420
|
+
return true;
|
|
7421
|
+
}
|
|
7422
|
+
if (matchesKey(data, "right") || data === "\x1B[C") {
|
|
7423
|
+
this.promptCursor = Math.min(this.promptValue.length, this.promptCursor + 1);
|
|
7424
|
+
return true;
|
|
7425
|
+
}
|
|
7426
|
+
if (isHomeKey(data)) {
|
|
7427
|
+
this.promptCursor = this.getCurrentPromptLineStart();
|
|
7428
|
+
return true;
|
|
7429
|
+
}
|
|
7430
|
+
if (isEndKey(data)) {
|
|
7431
|
+
this.promptCursor = this.getCurrentPromptLineEnd();
|
|
7432
|
+
return true;
|
|
7433
|
+
}
|
|
7434
|
+
if (matchesKey(data, "backspace") || data === "" || data === "\b") {
|
|
7435
|
+
if (this.promptCursor > 0) {
|
|
7436
|
+
this.promptValue = `${this.promptValue.slice(0, this.promptCursor - 1)}${this.promptValue.slice(this.promptCursor)}`;
|
|
7437
|
+
this.promptCursor -= 1;
|
|
7438
|
+
this.promptHistory.resetBrowsing();
|
|
7439
|
+
}
|
|
7440
|
+
return true;
|
|
7441
|
+
}
|
|
7442
|
+
if (matchesKey(data, "delete") || data === "\x1B[3~") {
|
|
7443
|
+
if (this.promptCursor < this.promptValue.length) {
|
|
7444
|
+
this.promptValue = `${this.promptValue.slice(0, this.promptCursor)}${this.promptValue.slice(this.promptCursor + 1)}`;
|
|
7445
|
+
this.promptHistory.resetBrowsing();
|
|
7446
|
+
}
|
|
7447
|
+
return true;
|
|
7448
|
+
}
|
|
7449
|
+
const printable = decodeKittyPrintable(data) ?? (isPrintableInput(data) ? data : void 0);
|
|
7450
|
+
if (printable !== void 0) {
|
|
7451
|
+
this.insertPromptText(printable);
|
|
7452
|
+
this.promptHistory.resetBrowsing();
|
|
7453
|
+
return true;
|
|
7454
|
+
}
|
|
7455
|
+
return false;
|
|
7456
|
+
}
|
|
7457
|
+
movePromptCursorVertically(delta) {
|
|
7458
|
+
const lines = this.promptValue.split("\n");
|
|
7459
|
+
const current = this.getPromptLineCursor(lines);
|
|
7460
|
+
const targetLine = Math.max(0, Math.min(lines.length - 1, current.line + delta));
|
|
7461
|
+
const targetColumn = Math.min(current.column, lines[targetLine]?.length ?? 0);
|
|
7462
|
+
this.promptCursor = lines.slice(0, targetLine).reduce((total, line) => total + line.length + 1, 0) + targetColumn;
|
|
7463
|
+
}
|
|
7464
|
+
getPromptLineCursor(lines = this.promptValue.split("\n")) {
|
|
7465
|
+
let offset = 0;
|
|
7466
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
7467
|
+
const line = lines[lineIndex] ?? "";
|
|
7468
|
+
const end = offset + line.length;
|
|
7469
|
+
if (this.promptCursor <= end || lineIndex === lines.length - 1) return {
|
|
7470
|
+
line: lineIndex,
|
|
7471
|
+
column: Math.max(0, this.promptCursor - offset)
|
|
7472
|
+
};
|
|
7473
|
+
offset = end + 1;
|
|
7474
|
+
}
|
|
7475
|
+
return {
|
|
7476
|
+
line: 0,
|
|
7477
|
+
column: 0
|
|
7478
|
+
};
|
|
7479
|
+
}
|
|
7480
|
+
getCurrentPromptLineStart() {
|
|
7481
|
+
return this.promptValue.lastIndexOf("\n", Math.max(0, this.promptCursor - 1)) + 1;
|
|
7482
|
+
}
|
|
7483
|
+
getCurrentPromptLineEnd() {
|
|
7484
|
+
const end = this.promptValue.indexOf("\n", this.promptCursor);
|
|
7485
|
+
return end === -1 ? this.promptValue.length : end;
|
|
7486
|
+
}
|
|
5732
7487
|
handleSlashSuggestionInput(data) {
|
|
5733
7488
|
const suggestions = this.getSlashSuggestions();
|
|
5734
7489
|
if (suggestions.length === 0) {
|
|
@@ -5747,19 +7502,19 @@ var ChatLayout = class {
|
|
|
5747
7502
|
this.completeSlashSuggestion(suggestions);
|
|
5748
7503
|
return true;
|
|
5749
7504
|
}
|
|
5750
|
-
if (isEnterKey(data) && this.
|
|
7505
|
+
if (isEnterKey(data) && this.promptValue.trim() !== suggestions[this.activeSlashSuggestionIndex]?.value) {
|
|
5751
7506
|
this.completeSlashSuggestion(suggestions);
|
|
5752
7507
|
return true;
|
|
5753
7508
|
}
|
|
5754
7509
|
return false;
|
|
5755
7510
|
}
|
|
5756
7511
|
completeSlashSuggestion(suggestions) {
|
|
5757
|
-
this.
|
|
5758
|
-
this.
|
|
7512
|
+
this.promptValue = suggestions[this.activeSlashSuggestionIndex]?.value ?? this.promptValue;
|
|
7513
|
+
this.promptCursor = this.promptValue.length;
|
|
5759
7514
|
this.promptHistory.resetBrowsing();
|
|
5760
7515
|
}
|
|
5761
7516
|
getSlashSuggestions() {
|
|
5762
|
-
return getSlashCommandSuggestions(this.
|
|
7517
|
+
return getSlashCommandSuggestions(this.promptValue);
|
|
5763
7518
|
}
|
|
5764
7519
|
getActiveModal() {
|
|
5765
7520
|
return this.messages[this.getActiveModalIndex()];
|
|
@@ -5772,6 +7527,7 @@ var ChatLayout = class {
|
|
|
5772
7527
|
this.submitUserInput(message);
|
|
5773
7528
|
}
|
|
5774
7529
|
submitUserInput(message) {
|
|
7530
|
+
this.setTaskPlanNotice(void 0);
|
|
5775
7531
|
this.promptHistory.add(message);
|
|
5776
7532
|
if (message.startsWith("/")) this.submitCommand?.(message);
|
|
5777
7533
|
else this.submitMessage?.(message);
|
|
@@ -5780,8 +7536,15 @@ var ChatLayout = class {
|
|
|
5780
7536
|
function colorUserMessageBorder(line) {
|
|
5781
7537
|
return line.replace("▌", ui.modelInline("▌"));
|
|
5782
7538
|
}
|
|
5783
|
-
function
|
|
5784
|
-
return (
|
|
7539
|
+
function padPromptInputLine(line, width) {
|
|
7540
|
+
return `${line}${" ".repeat(Math.max(0, width - stripAnsi(line).length))}`;
|
|
7541
|
+
}
|
|
7542
|
+
function isPrintableInput(data) {
|
|
7543
|
+
if (data.length === 0) return false;
|
|
7544
|
+
return [...data].every((char) => {
|
|
7545
|
+
const code = char.charCodeAt(0);
|
|
7546
|
+
return code >= 32 && code !== 127 && (code < 128 || code > 159);
|
|
7547
|
+
});
|
|
5785
7548
|
}
|
|
5786
7549
|
//#endregion
|
|
5787
7550
|
//#region src/agent/conversation.ts
|
|
@@ -5793,58 +7556,6 @@ function buildConversationPrompt(turns, latestMessage) {
|
|
|
5793
7556
|
return lines.join("\n\n");
|
|
5794
7557
|
}
|
|
5795
7558
|
//#endregion
|
|
5796
|
-
//#region src/agent/events.ts
|
|
5797
|
-
const agentEvent = {
|
|
5798
|
-
status(status) {
|
|
5799
|
-
return {
|
|
5800
|
-
type: "status",
|
|
5801
|
-
status
|
|
5802
|
-
};
|
|
5803
|
-
},
|
|
5804
|
-
systemMessage(text) {
|
|
5805
|
-
return {
|
|
5806
|
-
type: "message",
|
|
5807
|
-
role: "system",
|
|
5808
|
-
text
|
|
5809
|
-
};
|
|
5810
|
-
},
|
|
5811
|
-
assistantMessage(text, meta) {
|
|
5812
|
-
return meta === void 0 ? {
|
|
5813
|
-
type: "message",
|
|
5814
|
-
role: "assistant",
|
|
5815
|
-
text
|
|
5816
|
-
} : {
|
|
5817
|
-
type: "message",
|
|
5818
|
-
role: "assistant",
|
|
5819
|
-
text,
|
|
5820
|
-
meta
|
|
5821
|
-
};
|
|
5822
|
-
},
|
|
5823
|
-
toolCall(call, label) {
|
|
5824
|
-
return {
|
|
5825
|
-
type: "tool_call",
|
|
5826
|
-
call,
|
|
5827
|
-
label
|
|
5828
|
-
};
|
|
5829
|
-
},
|
|
5830
|
-
knowledgeStatus(status, guidance) {
|
|
5831
|
-
return guidance === void 0 ? {
|
|
5832
|
-
type: "knowledge_status",
|
|
5833
|
-
status
|
|
5834
|
-
} : {
|
|
5835
|
-
type: "knowledge_status",
|
|
5836
|
-
status,
|
|
5837
|
-
guidance
|
|
5838
|
-
};
|
|
5839
|
-
},
|
|
5840
|
-
choice(options) {
|
|
5841
|
-
return {
|
|
5842
|
-
type: "choice",
|
|
5843
|
-
...options
|
|
5844
|
-
};
|
|
5845
|
-
}
|
|
5846
|
-
};
|
|
5847
|
-
//#endregion
|
|
5848
7559
|
//#region src/agent/health.ts
|
|
5849
7560
|
async function checkAgentReady(modelGateway, abortSignal) {
|
|
5850
7561
|
const abortController = new AbortController();
|
|
@@ -5875,22 +7586,102 @@ function isAbortError(error) {
|
|
|
5875
7586
|
return error.name === "AbortError" || error.message.toLowerCase().includes("aborted");
|
|
5876
7587
|
}
|
|
5877
7588
|
//#endregion
|
|
7589
|
+
//#region src/agent/profiles.ts
|
|
7590
|
+
const READ_ONLY_TOOLS = [
|
|
7591
|
+
"read_file",
|
|
7592
|
+
"list_files",
|
|
7593
|
+
"grep",
|
|
7594
|
+
"find_file",
|
|
7595
|
+
"git_status",
|
|
7596
|
+
"git_diff",
|
|
7597
|
+
"git_log"
|
|
7598
|
+
];
|
|
7599
|
+
const PRIMARY_AGENT_PROFILE = {
|
|
7600
|
+
id: "primary",
|
|
7601
|
+
displayName: "Primary",
|
|
7602
|
+
mode: "primary",
|
|
7603
|
+
promptAdditions: [],
|
|
7604
|
+
modelPurpose: "agent.primary",
|
|
7605
|
+
toolPermissionDefault: "allow",
|
|
7606
|
+
allowedTools: [],
|
|
7607
|
+
deniedTools: []
|
|
7608
|
+
};
|
|
7609
|
+
const AGENT_PROFILES = [PRIMARY_AGENT_PROFILE, ...[{
|
|
7610
|
+
id: "explore",
|
|
7611
|
+
displayName: "Explore",
|
|
7612
|
+
mode: "subagent",
|
|
7613
|
+
promptAdditions: ["You are running as a read-only exploration subagent. Inspect the workspace and return concise findings to the parent agent."],
|
|
7614
|
+
modelPurpose: "agent.fast",
|
|
7615
|
+
toolPermissionDefault: "deny",
|
|
7616
|
+
allowedTools: READ_ONLY_TOOLS,
|
|
7617
|
+
deniedTools: ["task", "plan_todo"]
|
|
7618
|
+
}, {
|
|
7619
|
+
id: "general",
|
|
7620
|
+
displayName: "General",
|
|
7621
|
+
mode: "subagent",
|
|
7622
|
+
promptAdditions: ["You are running as a constrained subagent. Work only on the delegated prompt and return a concise result."],
|
|
7623
|
+
modelPurpose: "agent.primary",
|
|
7624
|
+
toolPermissionDefault: "allow",
|
|
7625
|
+
allowedTools: [],
|
|
7626
|
+
deniedTools: ["task", "plan_todo"]
|
|
7627
|
+
}]];
|
|
7628
|
+
function resolveAgentProfile(profileId = PRIMARY_AGENT_PROFILE.id) {
|
|
7629
|
+
const profile = AGENT_PROFILES.find((candidate) => candidate.id === profileId);
|
|
7630
|
+
if (!profile) throw new Error(`Unknown agent profile "${profileId}".`);
|
|
7631
|
+
return profile;
|
|
7632
|
+
}
|
|
7633
|
+
function createToolPermissionView(profile, parent = {}) {
|
|
7634
|
+
const deniedTools = new Set(profile.deniedTools);
|
|
7635
|
+
for (const tool of parent.deniedTools ?? []) deniedTools.add(tool);
|
|
7636
|
+
return {
|
|
7637
|
+
profileId: profile.id,
|
|
7638
|
+
defaultPermission: profile.toolPermissionDefault,
|
|
7639
|
+
allowedTools: new Set(profile.allowedTools),
|
|
7640
|
+
deniedTools
|
|
7641
|
+
};
|
|
7642
|
+
}
|
|
7643
|
+
function isToolAllowed(permissionView, toolName) {
|
|
7644
|
+
if (!isRegisteredToolName(toolName)) return false;
|
|
7645
|
+
if (permissionView.deniedTools.has(toolName)) return false;
|
|
7646
|
+
if (permissionView.defaultPermission === "deny") return permissionView.allowedTools.has(toolName);
|
|
7647
|
+
return true;
|
|
7648
|
+
}
|
|
7649
|
+
function getProfileToolDefinitions(permissionView) {
|
|
7650
|
+
return getToolDefinitionsForPermissions((toolName) => isToolAllowed(permissionView, toolName));
|
|
7651
|
+
}
|
|
7652
|
+
function isRegisteredToolName(toolName) {
|
|
7653
|
+
return toolName in toolRegistry;
|
|
7654
|
+
}
|
|
7655
|
+
//#endregion
|
|
5878
7656
|
//#region src/agent/tools/executor.ts
|
|
5879
7657
|
async function executeToolCall(workspaceRoot, call, options = {}) {
|
|
5880
|
-
const definition = getToolDefinition(call.tool);
|
|
5881
7658
|
const startedAt = Date.now();
|
|
5882
7659
|
const context = {
|
|
5883
7660
|
workspaceRoot,
|
|
5884
7661
|
pathEnv: options.pathEnv,
|
|
5885
|
-
logger: options.logger
|
|
7662
|
+
logger: options.logger,
|
|
7663
|
+
taskPlan: options.taskPlan,
|
|
7664
|
+
profile: options.profile,
|
|
7665
|
+
permissions: options.permissions,
|
|
7666
|
+
subagents: options.subagents,
|
|
7667
|
+
eventSink: options.eventSink,
|
|
7668
|
+
abortSignal: options.abortSignal,
|
|
7669
|
+
toolCallId: options.toolCallId
|
|
5886
7670
|
};
|
|
5887
|
-
options.logger?.debug({
|
|
5888
|
-
event: "tool_call",
|
|
5889
|
-
tool: call.tool,
|
|
5890
|
-
args: summarizeToolArgs(call)
|
|
5891
|
-
}, "tool call");
|
|
5892
7671
|
try {
|
|
5893
|
-
|
|
7672
|
+
if (!isToolName(call.tool)) throw new Error(`Unknown tool "${call.tool}".`);
|
|
7673
|
+
if (options.permissions && !isToolAllowed(options.permissions, call.tool)) throw new Error(`Tool "${call.tool}" is not allowed for agent profile "${options.permissions.profileId}".`);
|
|
7674
|
+
const definition = getToolDefinition(call.tool);
|
|
7675
|
+
const parsedCall = {
|
|
7676
|
+
...call,
|
|
7677
|
+
args: definition.argsSchema.parse(call.args)
|
|
7678
|
+
};
|
|
7679
|
+
options.logger?.debug({
|
|
7680
|
+
event: "tool_call",
|
|
7681
|
+
tool: parsedCall.tool,
|
|
7682
|
+
args: summarizeToolArgs(parsedCall)
|
|
7683
|
+
}, "tool call");
|
|
7684
|
+
const result = await definition.execute(context, parsedCall.args);
|
|
5894
7685
|
const durationMs = Date.now() - startedAt;
|
|
5895
7686
|
options.logger?.debug({
|
|
5896
7687
|
event: "tool_result",
|
|
@@ -5910,23 +7701,40 @@ async function executeToolCall(workspaceRoot, call, options = {}) {
|
|
|
5910
7701
|
}, "tool result content");
|
|
5911
7702
|
return result;
|
|
5912
7703
|
} catch (error) {
|
|
5913
|
-
|
|
5914
|
-
|
|
7704
|
+
const message = formatErrorMessage(error);
|
|
7705
|
+
const logPayload = {
|
|
7706
|
+
event: "tool_result",
|
|
5915
7707
|
tool: call.tool,
|
|
5916
7708
|
durationMs: Date.now() - startedAt,
|
|
7709
|
+
error: message,
|
|
5917
7710
|
err: error
|
|
5918
|
-
}
|
|
5919
|
-
|
|
7711
|
+
};
|
|
7712
|
+
if (typeof options.logger?.warn === "function") options.logger.warn(logPayload, "tool returned error");
|
|
7713
|
+
else options.logger?.debug(logPayload, "tool returned error");
|
|
7714
|
+
return {
|
|
7715
|
+
tool: call.tool,
|
|
7716
|
+
content: `Tool ${call.tool} failed: ${message}`,
|
|
7717
|
+
error: message,
|
|
7718
|
+
warning: message
|
|
7719
|
+
};
|
|
5920
7720
|
}
|
|
5921
7721
|
}
|
|
5922
7722
|
function summarizeToolArgs(call) {
|
|
7723
|
+
if (call.tool === "plan_todo") {
|
|
7724
|
+
const activeItem = call.args.items.find((item) => item.status === "in_progress")?.text;
|
|
7725
|
+
return {
|
|
7726
|
+
itemCount: call.args.items.length,
|
|
7727
|
+
activeItem,
|
|
7728
|
+
completedCount: call.args.items.filter((item) => item.status === "completed").length
|
|
7729
|
+
};
|
|
7730
|
+
}
|
|
5923
7731
|
if (call.tool === "write_file") return {
|
|
5924
7732
|
path: call.args.path,
|
|
5925
7733
|
contentLength: call.args.content.length,
|
|
5926
7734
|
lineCount: countLogicalLines(call.args.content),
|
|
5927
7735
|
createParentDirs: Boolean(call.args.create_parent_dirs),
|
|
5928
7736
|
overwrite: Boolean(call.args.overwrite),
|
|
5929
|
-
|
|
7737
|
+
expectedCurrentHashProvided: Boolean(call.args.expected_current_hash)
|
|
5930
7738
|
};
|
|
5931
7739
|
if (call.tool !== "edit_file") return call.args;
|
|
5932
7740
|
return {
|
|
@@ -5934,10 +7742,15 @@ function summarizeToolArgs(call) {
|
|
|
5934
7742
|
editCount: call.args.edits.length,
|
|
5935
7743
|
oldTextLengths: call.args.edits.map((edit) => edit.old_text.length),
|
|
5936
7744
|
newTextLengths: call.args.edits.map((edit) => edit.new_text.length),
|
|
5937
|
-
|
|
7745
|
+
expectedCurrentHashProvided: Boolean(call.args.expected_current_hash)
|
|
5938
7746
|
};
|
|
5939
7747
|
}
|
|
5940
7748
|
function summarizeToolResult(result) {
|
|
7749
|
+
if (result.tool === "plan_todo") return {
|
|
7750
|
+
itemCount: result.plan.items.length,
|
|
7751
|
+
activeItem: result.currentItem,
|
|
7752
|
+
completedCount: result.completedCount
|
|
7753
|
+
};
|
|
5941
7754
|
if (result.tool === "inspect_command") return {
|
|
5942
7755
|
cwd: result.cwd,
|
|
5943
7756
|
exitCode: result.exitCode,
|
|
@@ -6006,13 +7819,22 @@ function countLogicalLines(content) {
|
|
|
6006
7819
|
const withoutTrailingLineEnding = content.replace(/\r?\n$/u, "");
|
|
6007
7820
|
return withoutTrailingLineEnding.length === 0 ? 1 : withoutTrailingLineEnding.split(/\r?\n/u).length;
|
|
6008
7821
|
}
|
|
7822
|
+
function formatErrorMessage(error) {
|
|
7823
|
+
return error instanceof Error ? error.message : String(error);
|
|
7824
|
+
}
|
|
6009
7825
|
//#endregion
|
|
6010
7826
|
//#region src/agent/prompts.ts
|
|
6011
|
-
function getChatSystemPrompt() {
|
|
7827
|
+
function getChatSystemPrompt(options = {}) {
|
|
7828
|
+
const profile = options.profile ?? PRIMARY_AGENT_PROFILE;
|
|
7829
|
+
const canUseTool = (toolName) => options.permissions ? isToolAllowed(options.permissions, toolName) : true;
|
|
7830
|
+
const toolPromptLines = options.permissions ? getToolPromptLines((toolName) => canUseTool(toolName)) : getToolPromptLines();
|
|
6012
7831
|
return [
|
|
6013
7832
|
"You are Topchester, a plain-spoken terminal coding agent for software engineering work.",
|
|
6014
7833
|
"Your job is to turn ordinary user requests into concrete repository work: inspect the codebase, make focused changes when tools allow it, verify the result when possible, and report the outcome clearly.",
|
|
6015
7834
|
"",
|
|
7835
|
+
`Agent profile: ${profile.displayName} (${profile.id}).`,
|
|
7836
|
+
...profile.promptAdditions,
|
|
7837
|
+
"",
|
|
6016
7838
|
"Working style:",
|
|
6017
7839
|
"- Start by understanding the user's intent and the surrounding code before proposing or changing anything non-trivial.",
|
|
6018
7840
|
"- Prefer local project evidence over assumptions. Use search and read tools to find relevant files, examples, tests, commands, and conventions.",
|
|
@@ -6025,67 +7847,281 @@ function getChatSystemPrompt() {
|
|
|
6025
7847
|
"- Ask a clarifying question only when the missing information blocks useful progress or the safe interpretation is genuinely unclear.",
|
|
6026
7848
|
"",
|
|
6027
7849
|
"You have these tools available:",
|
|
6028
|
-
...
|
|
7850
|
+
...toolPromptLines,
|
|
6029
7851
|
"",
|
|
6030
7852
|
"Tool use:",
|
|
6031
7853
|
"- When using a tool, output exactly one tool JSON object and no prose, markdown, or additional JSON. After the tool result, either output the next single tool JSON object or a final plain-text answer.",
|
|
6032
|
-
"-
|
|
6033
|
-
"-
|
|
6034
|
-
"
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
"
|
|
6041
|
-
"- Use
|
|
6042
|
-
"-
|
|
6043
|
-
"-
|
|
6044
|
-
"-
|
|
6045
|
-
"- Use
|
|
6046
|
-
"
|
|
7854
|
+
"- You already have permission to use the available tools to handle the user's request. Do not ask the user to provide tool results or permission to use an available tool.",
|
|
7855
|
+
"- Do not claim to have read, created, edited, staged, committed, or run anything unless a tool result in this turn confirms it.",
|
|
7856
|
+
...canUseTool("plan_todo") ? [
|
|
7857
|
+
"- Use plan_todo for non-trivial multi-step work before the first substantive repository tool call.",
|
|
7858
|
+
"- Keep plan_todo items short, user-safe, and usually 2 to 6 items. Maintain exactly one in_progress item while work remains, update it after major progress changes, and clear it only when abandoning the plan or when no visible plan is useful.",
|
|
7859
|
+
"- Do not use plan_todo for simple one-step answers, tiny reads, or trivial edits.",
|
|
7860
|
+
"- Do not call plan_todo only to summarize completed work before a final answer. If no visible plan is active and the work is done, answer directly."
|
|
7861
|
+
] : [],
|
|
7862
|
+
...canUseTool("read_file") || canUseTool("grep") || canUseTool("find_file") || canUseTool("list_files") ? ["- Use read/search tools when the user asks about files, code, symbols, usages, tests, or project behavior."] : [],
|
|
7863
|
+
...canUseTool("find_file") && canUseTool("grep") && canUseTool("read_file") ? ["- Use find_file for path or filename lookup. Use grep for text inside files. If grep output mentions another path, treat that mentioned path as content until find_file or read_file confirms it exists."] : [],
|
|
7864
|
+
...canUseTool("list_files") && canUseTool("grep") && canUseTool("find_file") && canUseTool("read_file") ? ["- Use list_files, grep, find_file, and read_file for exact file listing, search, lookup, and reading tasks."] : [],
|
|
7865
|
+
...canUseTool("git_status") && canUseTool("git_diff") && canUseTool("git_log") ? ["- Use git_status, git_diff, and git_log for Git state, diffs, and history. Prefer these over inspect_command for Git workflow inspection."] : [],
|
|
7866
|
+
...canUseTool("git_add") && canUseTool("git_commit") ? ["- Use git_add and git_commit only when the user explicitly asks to stage or commit. Never stage unrelated files, never stage '.', and never commit unless staged paths exactly match the user's request."] : [],
|
|
7867
|
+
...canUseTool("inspect_command") ? ["- Use inspect_command only for quick read-only repo orientation when a short familiar command chain is clearer than several dedicated tool calls.", "- inspect_command is not a shell. Unsafe commands, shell expansion, scripts, installs, builds, tests, network access, and file mutation are not available through it."] : [],
|
|
7868
|
+
...canUseTool("edit_file") && canUseTool("read_file") ? ["- Use read_file before editing a file so your edit is based on current file content and hash metadata."] : [],
|
|
7869
|
+
...canUseTool("read_file") && (canUseTool("edit_file") || canUseTool("write_file")) ? ["- When passing expected_current_hash to edit_file or write_file, use the current pre-edit/pre-write hash from the latest read_file result for that exact file. Never invent it and never use a predicted after-edit or after-write hash."] : [],
|
|
7870
|
+
...canUseTool("edit_file") ? ["- Use edit_file for targeted edits to existing files. Make multiple disjoint edits for the same file in one call when possible."] : [],
|
|
7871
|
+
...canUseTool("write_file") && canUseTool("read_file") ? [
|
|
7872
|
+
"- Use write_file to create new files by default. It fails when the file already exists unless you are replacing the whole file with overwrite:true and expected_current_hash from read_file.",
|
|
7873
|
+
"- When the user asks you to create a new file, call write_file. Do not answer that the file was created until write_file succeeds.",
|
|
7874
|
+
"- Pass write_file create_parent_dirs:true only when the user intent clearly includes creating that folder path."
|
|
7875
|
+
] : [],
|
|
7876
|
+
...canUseTool("write_file") && !canUseTool("read_file") ? [
|
|
7877
|
+
"- Use write_file to create new files by default. It fails when the file already exists unless overwrite:true is available with verified current content.",
|
|
7878
|
+
"- When the user asks you to create a new file, call write_file. Do not answer that the file was created until write_file succeeds.",
|
|
7879
|
+
"- Pass write_file create_parent_dirs:true only when the user intent clearly includes creating that folder path."
|
|
7880
|
+
] : [],
|
|
7881
|
+
...canUseTool("inspect_command") ? ["- Do not use inspect_command for file creation or file mutation."] : [],
|
|
7882
|
+
...canUseTool("edit_file") ? ["- Keep edit_file old_text small but unique. Do not include line labels or grep prefixes in old_text; use exact file text only."] : [],
|
|
7883
|
+
...canUseTool("edit_file") || canUseTool("write_file") ? ["- Use edit/write tools when they are available and the user asks you to implement, fix, add, update, or refactor code."] : [],
|
|
7884
|
+
...canUseTool("inspect_command") ? ["- Use command/test tools when they are available and you need to inspect the environment, run tests, format, lint, typecheck, or verify behavior."] : [],
|
|
6047
7885
|
"- After each tool result, decide the next useful action from the new evidence. Continue until the request is handled or blocked.",
|
|
6048
7886
|
"Do not make up file contents or search results."
|
|
6049
7887
|
].join("\n");
|
|
6050
7888
|
}
|
|
6051
7889
|
//#endregion
|
|
7890
|
+
//#region src/session/runtime-payloads.ts
|
|
7891
|
+
function runtimeEventToSessionPayload(event) {
|
|
7892
|
+
switch (event.type) {
|
|
7893
|
+
case "message": return {
|
|
7894
|
+
kind: "message",
|
|
7895
|
+
role: event.role,
|
|
7896
|
+
text: event.text,
|
|
7897
|
+
...event.meta === void 0 ? {} : { meta: event.meta }
|
|
7898
|
+
};
|
|
7899
|
+
case "tool_call": return {
|
|
7900
|
+
kind: "tool_call",
|
|
7901
|
+
label: event.label,
|
|
7902
|
+
call: event.call
|
|
7903
|
+
};
|
|
7904
|
+
case "task_plan": return {
|
|
7905
|
+
kind: "task_plan",
|
|
7906
|
+
items: event.plan.items,
|
|
7907
|
+
updatedAt: event.plan.updatedAt
|
|
7908
|
+
};
|
|
7909
|
+
case "knowledge_status": return;
|
|
7910
|
+
case "choice": return {
|
|
7911
|
+
kind: "choice",
|
|
7912
|
+
tone: event.tone,
|
|
7913
|
+
title: event.title,
|
|
7914
|
+
...event.body === void 0 ? {} : { body: event.body },
|
|
7915
|
+
actions: event.actions
|
|
7916
|
+
};
|
|
7917
|
+
case "subagent_started": return {
|
|
7918
|
+
kind: "subagent_started",
|
|
7919
|
+
sessionId: event.sessionId,
|
|
7920
|
+
parentSessionId: event.parentSessionId,
|
|
7921
|
+
parentToolCallId: event.parentToolCallId,
|
|
7922
|
+
...event.agentProfileId === void 0 ? {} : { agentProfileId: event.agentProfileId },
|
|
7923
|
+
...event.title === void 0 ? {} : { title: event.title }
|
|
7924
|
+
};
|
|
7925
|
+
case "subagent_event": return {
|
|
7926
|
+
kind: "subagent_event",
|
|
7927
|
+
sessionId: event.sessionId,
|
|
7928
|
+
parentSessionId: event.parentSessionId,
|
|
7929
|
+
parentToolCallId: event.parentToolCallId,
|
|
7930
|
+
event: event.event
|
|
7931
|
+
};
|
|
7932
|
+
case "subagent_completed": return {
|
|
7933
|
+
kind: "subagent_completed",
|
|
7934
|
+
sessionId: event.sessionId,
|
|
7935
|
+
parentSessionId: event.parentSessionId,
|
|
7936
|
+
parentToolCallId: event.parentToolCallId,
|
|
7937
|
+
...event.result === void 0 ? {} : { result: event.result }
|
|
7938
|
+
};
|
|
7939
|
+
case "subagent_failed": return {
|
|
7940
|
+
kind: "subagent_failed",
|
|
7941
|
+
sessionId: event.sessionId,
|
|
7942
|
+
parentSessionId: event.parentSessionId,
|
|
7943
|
+
parentToolCallId: event.parentToolCallId,
|
|
7944
|
+
error: event.error
|
|
7945
|
+
};
|
|
7946
|
+
case "status": return {
|
|
7947
|
+
kind: "status",
|
|
7948
|
+
status: event.status
|
|
7949
|
+
};
|
|
7950
|
+
}
|
|
7951
|
+
}
|
|
7952
|
+
//#endregion
|
|
7953
|
+
//#region src/agent/subagents.ts
|
|
7954
|
+
var SubagentManager = class {
|
|
7955
|
+
options;
|
|
7956
|
+
constructor(options) {
|
|
7957
|
+
this.options = options;
|
|
7958
|
+
}
|
|
7959
|
+
async runTask(options) {
|
|
7960
|
+
const parentSession = this.options.parentSession;
|
|
7961
|
+
if (!parentSession) throw new Error("task requires an active persisted session.");
|
|
7962
|
+
const profile = resolveAgentProfile(options.subagentType ?? "explore");
|
|
7963
|
+
if (profile.mode !== "subagent" && profile.mode !== "all") throw new Error(`Agent profile "${profile.id}" cannot be used for subagent tasks.`);
|
|
7964
|
+
const child = await createChildSession(this.options.context.workspaceRoot, {
|
|
7965
|
+
parent: parentSession,
|
|
7966
|
+
parentToolCallId: options.parentToolCallId,
|
|
7967
|
+
agentProfileId: profile.id,
|
|
7968
|
+
title: options.description,
|
|
7969
|
+
recordParentEvent: false
|
|
7970
|
+
});
|
|
7971
|
+
const reference = {
|
|
7972
|
+
sessionId: child.sessionId,
|
|
7973
|
+
parentSessionId: parentSession.sessionId,
|
|
7974
|
+
parentToolCallId: options.parentToolCallId
|
|
7975
|
+
};
|
|
7976
|
+
await options.eventSink?.(agentEvent.subagentStarted({
|
|
7977
|
+
...reference,
|
|
7978
|
+
agentProfileId: profile.id,
|
|
7979
|
+
title: options.description
|
|
7980
|
+
}));
|
|
7981
|
+
const childRuntime = this.options.createRuntime({
|
|
7982
|
+
profile,
|
|
7983
|
+
parentPermissions: this.options.parentPermissions,
|
|
7984
|
+
session: child
|
|
7985
|
+
});
|
|
7986
|
+
let finalResponse = "";
|
|
7987
|
+
try {
|
|
7988
|
+
for await (const childEvent of childRuntime.submitMessageStream([], options.prompt, options.abortSignal, { session: child })) {
|
|
7989
|
+
const payload = runtimeEventToSessionPayload(childEvent);
|
|
7990
|
+
if (payload) await child.append(payload);
|
|
7991
|
+
if (childEvent.type === "message" && childEvent.role === "assistant") finalResponse = childEvent.text;
|
|
7992
|
+
await options.eventSink?.(agentEvent.subagentEvent(reference, childEvent));
|
|
7993
|
+
}
|
|
7994
|
+
const result = finalResponse.trim() || "Subagent completed without an assistant response.";
|
|
7995
|
+
await options.eventSink?.(agentEvent.subagentCompleted({
|
|
7996
|
+
...reference,
|
|
7997
|
+
result
|
|
7998
|
+
}));
|
|
7999
|
+
return {
|
|
8000
|
+
sessionId: child.sessionId,
|
|
8001
|
+
status: "completed",
|
|
8002
|
+
result,
|
|
8003
|
+
profileId: profile.id
|
|
8004
|
+
};
|
|
8005
|
+
} catch (error) {
|
|
8006
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8007
|
+
await options.eventSink?.(agentEvent.subagentFailed({
|
|
8008
|
+
...reference,
|
|
8009
|
+
error: message
|
|
8010
|
+
}));
|
|
8011
|
+
return {
|
|
8012
|
+
sessionId: child.sessionId,
|
|
8013
|
+
status: "failed",
|
|
8014
|
+
result: message,
|
|
8015
|
+
profileId: profile.id
|
|
8016
|
+
};
|
|
8017
|
+
}
|
|
8018
|
+
}
|
|
8019
|
+
};
|
|
8020
|
+
//#endregion
|
|
6052
8021
|
//#region src/agent/runtime.ts
|
|
6053
|
-
const MAX_TOOL_CALLS_PER_TURN =
|
|
6054
|
-
|
|
8022
|
+
const MAX_TOOL_CALLS_PER_TURN = 75;
|
|
8023
|
+
const DEFAULT_TASK_CONCURRENCY = 3;
|
|
8024
|
+
var TopchesterAgentRuntime = class TopchesterAgentRuntime {
|
|
6055
8025
|
context;
|
|
6056
|
-
|
|
8026
|
+
options;
|
|
8027
|
+
taskPlan = createTaskPlanController();
|
|
8028
|
+
/**
|
|
8029
|
+
* Holds the shared application context for one runtime instance.
|
|
8030
|
+
* The runtime does not own those dependencies; it coordinates the
|
|
8031
|
+
* workspace, model gateway, logger, config, and task-plan state that
|
|
8032
|
+
* are passed in by the CLI or TUI layer.
|
|
8033
|
+
*/
|
|
8034
|
+
constructor(context, options = {}) {
|
|
6057
8035
|
this.context = context;
|
|
8036
|
+
this.options = options;
|
|
6058
8037
|
}
|
|
8038
|
+
/**
|
|
8039
|
+
* Performs the lightweight startup model check used by the interactive
|
|
8040
|
+
* agent before accepting work. The check is intentionally non-blocking
|
|
8041
|
+
* from the user's point of view: timeout and failure both produce a
|
|
8042
|
+
* visible status message, but the runtime still moves to ready so the
|
|
8043
|
+
* user can continue.
|
|
8044
|
+
*/
|
|
6059
8045
|
async checkAgent(abortSignal) {
|
|
6060
8046
|
const result = await checkAgentReady(this.context.modelGateway, abortSignal);
|
|
6061
8047
|
if (result === "ready") return [agentEvent.status("ready")];
|
|
6062
8048
|
if (result === "timed-out") return [agentEvent.systemMessage("Agent is taking a while, so I skipped the startup check."), agentEvent.status("ready")];
|
|
6063
8049
|
return [agentEvent.systemMessage("Agent did not say it was ready."), agentEvent.status("ready")];
|
|
6064
8050
|
}
|
|
8051
|
+
/**
|
|
8052
|
+
* Builds the initial knowledge-base status events shown by the TUI.
|
|
8053
|
+
* This wraps the raw filesystem status with the same non-clean file count
|
|
8054
|
+
* used by `/kb status`, so startup messaging reflects whether project
|
|
8055
|
+
* knowledge is ready, missing, stale, or waiting for a sync.
|
|
8056
|
+
*/
|
|
6065
8057
|
async checkKnowledgeBase() {
|
|
6066
8058
|
return getKnowledgeStatusEvents(await this.getKnowledgeStatusWithNonCleanFileCount());
|
|
6067
8059
|
}
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
8060
|
+
/**
|
|
8061
|
+
* Streams one user chat turn through the agent loop. It builds the model
|
|
8062
|
+
* prompt with relevant KB context, calls the model, executes any requested
|
|
8063
|
+
* tools, feeds tool results back into the next prompt, and repeats until
|
|
8064
|
+
* the model returns a normal assistant message or the loop hits its safety
|
|
8065
|
+
* limit.
|
|
8066
|
+
*
|
|
8067
|
+
* This is the primary runtime execution contract. Compatibility wrappers
|
|
8068
|
+
* can collect the stream, but the runtime's own turn loop only knows about
|
|
8069
|
+
* ordered events.
|
|
8070
|
+
*/
|
|
8071
|
+
async *submitMessageStream(conversation, message, abortSignal, options = {}) {
|
|
8072
|
+
let nextPrompt = await this.buildPromptWithKnowledgeContext(buildConversationPrompt(conversation, message), message);
|
|
6072
8073
|
let totalDurationMs = 0;
|
|
8074
|
+
const tokenUsageTotals = {};
|
|
8075
|
+
const profile = this.options.profile ?? PRIMARY_AGENT_PROFILE;
|
|
8076
|
+
const permissions = createToolPermissionView(profile, { deniedTools: this.options.parentPermissions?.deniedTools });
|
|
8077
|
+
const tools = getProfileToolDefinitions(permissions);
|
|
8078
|
+
const session = options.session ?? this.options.session;
|
|
8079
|
+
const subagents = new SubagentManager({
|
|
8080
|
+
context: this.context,
|
|
8081
|
+
parentSession: session,
|
|
8082
|
+
parentProfile: profile,
|
|
8083
|
+
parentPermissions: permissions,
|
|
8084
|
+
createRuntime: ({ profile: childProfile, parentPermissions, session: childSession }) => new TopchesterAgentRuntime(this.context, {
|
|
8085
|
+
...this.options,
|
|
8086
|
+
profile: childProfile,
|
|
8087
|
+
parentPermissions,
|
|
8088
|
+
session: childSession
|
|
8089
|
+
})
|
|
8090
|
+
});
|
|
6073
8091
|
let lastModelId = "model";
|
|
6074
8092
|
let afterTool;
|
|
6075
8093
|
let toolProtocolOverride = readToolProtocolEnvOverride();
|
|
8094
|
+
let requestedPlanClosure = false;
|
|
6076
8095
|
for (let toolCalls = 0; toolCalls <= MAX_TOOL_CALLS_PER_TURN; toolCalls += 1) {
|
|
6077
8096
|
const startedAt = Date.now();
|
|
8097
|
+
const system = getChatSystemPrompt({
|
|
8098
|
+
profile,
|
|
8099
|
+
permissions
|
|
8100
|
+
});
|
|
8101
|
+
this.context.logger.debug({
|
|
8102
|
+
event: "model_prompt",
|
|
8103
|
+
purpose: "agent.primary",
|
|
8104
|
+
afterTool,
|
|
8105
|
+
toolProtocol: toolProtocolOverride,
|
|
8106
|
+
promptLength: nextPrompt.length,
|
|
8107
|
+
systemLength: system.length,
|
|
8108
|
+
prompt: nextPrompt,
|
|
8109
|
+
system
|
|
8110
|
+
}, afterTool ? "model prompt after tool" : "model prompt");
|
|
6078
8111
|
const result = await generateAgentStep(this.context, {
|
|
6079
8112
|
purpose: "agent.primary",
|
|
6080
|
-
system
|
|
8113
|
+
system,
|
|
6081
8114
|
prompt: nextPrompt,
|
|
6082
8115
|
abortSignal,
|
|
6083
|
-
toolProtocol: toolProtocolOverride
|
|
8116
|
+
toolProtocol: toolProtocolOverride,
|
|
8117
|
+
onReasoning: options.onReasoning,
|
|
8118
|
+
tools
|
|
6084
8119
|
});
|
|
6085
8120
|
const durationMs = Date.now() - startedAt;
|
|
6086
8121
|
const toolCall = result.toolCalls[0];
|
|
6087
8122
|
totalDurationMs += durationMs;
|
|
6088
8123
|
lastModelId = result.modelId;
|
|
8124
|
+
addTokenUsageTotals(tokenUsageTotals, result.usage);
|
|
6089
8125
|
this.context.logger.debug({
|
|
6090
8126
|
event: "model_response",
|
|
6091
8127
|
purpose: "agent.primary",
|
|
@@ -6093,6 +8129,11 @@ var TopchesterAgentRuntime = class {
|
|
|
6093
8129
|
durationMs,
|
|
6094
8130
|
totalDurationMs,
|
|
6095
8131
|
textLength: result.text.length,
|
|
8132
|
+
usage: result.usage,
|
|
8133
|
+
inputTokens: result.usage?.inputTokens,
|
|
8134
|
+
outputTokens: result.usage?.outputTokens,
|
|
8135
|
+
totalTokens: result.usage?.totalTokens,
|
|
8136
|
+
costUsd: result.usage?.costUsd,
|
|
6096
8137
|
hasToolCall: Boolean(toolCall),
|
|
6097
8138
|
toolProtocol: result.toolProtocol,
|
|
6098
8139
|
protocolAttempts: result.protocolAttempts,
|
|
@@ -6113,25 +8154,120 @@ var TopchesterAgentRuntime = class {
|
|
|
6113
8154
|
if (result.providerRejectedTools && result.toolProtocol === "text-json") toolProtocolOverride = "text-json";
|
|
6114
8155
|
else if (result.providerRejectedTools && result.toolProtocol === "text-xml") toolProtocolOverride = "text-xml";
|
|
6115
8156
|
if (!toolCall) {
|
|
6116
|
-
|
|
6117
|
-
|
|
8157
|
+
const plan = this.taskPlan.get();
|
|
8158
|
+
const finalText = stripSuppressiblePlanTodoPrefix(result.text, plan) ?? result.text;
|
|
8159
|
+
if (hasOpenTaskPlan(plan)) {
|
|
8160
|
+
if (!requestedPlanClosure) {
|
|
8161
|
+
requestedPlanClosure = true;
|
|
8162
|
+
nextPrompt = `${nextPrompt}\n\n${formatOpenPlanClosureInstruction(finalText, result.toolProtocol)}`;
|
|
8163
|
+
continue;
|
|
8164
|
+
}
|
|
8165
|
+
yield agentEvent.taskPlan(this.taskPlan.update({ items: [] }));
|
|
8166
|
+
}
|
|
8167
|
+
yield agentEvent.assistantMessage(finalText.trim() || "I got an empty response from the model.", formatAgentMessageMeta(result.modelId, totalDurationMs, tokenUsageTotals));
|
|
8168
|
+
yield agentEvent.status("ready");
|
|
8169
|
+
return;
|
|
6118
8170
|
}
|
|
6119
8171
|
if (toolCalls === MAX_TOOL_CALLS_PER_TURN) {
|
|
6120
|
-
|
|
6121
|
-
|
|
8172
|
+
yield agentEvent.choice({
|
|
8173
|
+
tone: "warning",
|
|
8174
|
+
title: "Tool call limit reached",
|
|
8175
|
+
body: `Stopped after ${MAX_TOOL_CALLS_PER_TURN} tool calls in one turn. Continue starts another turn; abort leaves the call stopped.`,
|
|
8176
|
+
actions: [choiceAction("Continue", "Continue the previous task from where you stopped."), choiceAction("Abort", ABORT_CHOICE_VALUE)]
|
|
8177
|
+
});
|
|
8178
|
+
yield agentEvent.status("ready");
|
|
8179
|
+
return;
|
|
8180
|
+
}
|
|
8181
|
+
if (result.toolCalls.length > 1 && result.toolCalls.every((call) => call.tool === "task")) {
|
|
8182
|
+
const taskCalls = result.toolCalls.map((call) => call);
|
|
8183
|
+
const taskResults = [];
|
|
8184
|
+
for (let index = 0; index < taskCalls.length; index += DEFAULT_TASK_CONCURRENCY) {
|
|
8185
|
+
const batch = taskCalls.slice(index, index + DEFAULT_TASK_CONCURRENCY);
|
|
8186
|
+
const taskEventQueue = createRuntimeEventQueue();
|
|
8187
|
+
const batchResultPromise = Promise.all(batch.map((call, batchIndex) => executeToolCall(this.context.workspaceRoot, call, {
|
|
8188
|
+
logger: this.context.logger,
|
|
8189
|
+
taskPlan: this.taskPlan,
|
|
8190
|
+
profile,
|
|
8191
|
+
permissions,
|
|
8192
|
+
subagents,
|
|
8193
|
+
abortSignal,
|
|
8194
|
+
toolCallId: result.toolCalls[index + batchIndex]?.id,
|
|
8195
|
+
eventSink: (event) => taskEventQueue.push(event)
|
|
8196
|
+
}))).finally(() => {
|
|
8197
|
+
taskEventQueue.close();
|
|
8198
|
+
});
|
|
8199
|
+
for await (const event of taskEventQueue) yield event;
|
|
8200
|
+
taskResults.push(...await batchResultPromise);
|
|
8201
|
+
}
|
|
8202
|
+
for (let index = 0; index < taskCalls.length; index += 1) yield agentEvent.toolCall(taskCalls[index], formatToolCallMessage(taskCalls[index], taskResults[index]));
|
|
8203
|
+
afterTool = "task";
|
|
8204
|
+
nextPrompt = `${nextPrompt}\n\n${taskResults.map((toolResult) => formatToolResultForPrompt(toolResult)).join("\n\n")}\n\n${formatContinuationInstruction(result.toolProtocol, taskResults.at(-1), isToolAllowed(permissions, "plan_todo"))}`;
|
|
8205
|
+
continue;
|
|
8206
|
+
}
|
|
8207
|
+
if (result.toolCalls.length > 1 && result.toolCalls.every((call) => isParallelSafeToolName(call.tool))) {
|
|
8208
|
+
const parallelCalls = result.toolCalls.map((call) => call);
|
|
8209
|
+
const parallelResults = await Promise.all(parallelCalls.map((call, index) => executeToolCall(this.context.workspaceRoot, call, {
|
|
8210
|
+
logger: this.context.logger,
|
|
8211
|
+
taskPlan: this.taskPlan,
|
|
8212
|
+
profile,
|
|
8213
|
+
permissions,
|
|
8214
|
+
subagents,
|
|
8215
|
+
abortSignal,
|
|
8216
|
+
toolCallId: result.toolCalls[index]?.id
|
|
8217
|
+
})));
|
|
8218
|
+
for (let index = 0; index < parallelCalls.length; index += 1) yield agentEvent.toolCall(parallelCalls[index], formatToolCallMessage(parallelCalls[index], parallelResults[index]));
|
|
8219
|
+
afterTool = parallelCalls.at(-1)?.tool;
|
|
8220
|
+
nextPrompt = `${nextPrompt}\n\n${parallelResults.map((toolResult) => formatToolResultForPrompt(toolResult)).join("\n\n")}\n\n${formatContinuationInstruction(result.toolProtocol, parallelResults.at(-1), isToolAllowed(permissions, "plan_todo"))}`;
|
|
8221
|
+
continue;
|
|
6122
8222
|
}
|
|
6123
8223
|
const executableToolCall = toolCall;
|
|
6124
|
-
const
|
|
6125
|
-
|
|
8224
|
+
const suppressiblePlanTodoAnswer = getSuppressiblePlanTodoAnswer(executableToolCall, result.text, this.taskPlan.get());
|
|
8225
|
+
if (suppressiblePlanTodoAnswer !== void 0) {
|
|
8226
|
+
yield agentEvent.assistantMessage(suppressiblePlanTodoAnswer || "I got an empty response from the model.", formatAgentMessageMeta(result.modelId, totalDurationMs, tokenUsageTotals));
|
|
8227
|
+
yield agentEvent.status("ready");
|
|
8228
|
+
return;
|
|
8229
|
+
}
|
|
8230
|
+
const toolEventQueue = createRuntimeEventQueue();
|
|
8231
|
+
const toolResultPromise = executeToolCall(this.context.workspaceRoot, executableToolCall, {
|
|
8232
|
+
logger: this.context.logger,
|
|
8233
|
+
taskPlan: this.taskPlan,
|
|
8234
|
+
profile,
|
|
8235
|
+
permissions,
|
|
8236
|
+
subagents,
|
|
8237
|
+
abortSignal,
|
|
8238
|
+
toolCallId: toolCall.id,
|
|
8239
|
+
eventSink: (event) => toolEventQueue.push(event)
|
|
8240
|
+
}).finally(() => {
|
|
8241
|
+
toolEventQueue.close();
|
|
8242
|
+
});
|
|
8243
|
+
for await (const event of toolEventQueue) yield event;
|
|
8244
|
+
const toolResult = await toolResultPromise;
|
|
8245
|
+
yield agentEvent.toolCall(executableToolCall, formatToolCallMessage(executableToolCall, toolResult));
|
|
8246
|
+
if (!isToolErrorResult(toolResult) && toolResult.tool === "plan_todo") yield agentEvent.taskPlan(toolResult.plan);
|
|
6126
8247
|
afterTool = executableToolCall.tool;
|
|
6127
|
-
nextPrompt = `${nextPrompt}\n\n${formatToolResultForPrompt(toolResult)}\n\n${formatContinuationInstruction(result.toolProtocol)}`;
|
|
8248
|
+
nextPrompt = `${nextPrompt}\n\n${formatToolResultForPrompt(toolResult)}\n\n${formatContinuationInstruction(result.toolProtocol, toolResult, isToolAllowed(permissions, "plan_todo"))}`;
|
|
6128
8249
|
}
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
8250
|
+
yield agentEvent.assistantMessage("I stopped because the tool loop ended unexpectedly.", formatAgentMessageMeta(lastModelId, totalDurationMs, tokenUsageTotals));
|
|
8251
|
+
yield agentEvent.status("ready");
|
|
8252
|
+
}
|
|
8253
|
+
/**
|
|
8254
|
+
* Compatibility wrapper for callers that still expect a completed event
|
|
8255
|
+
* array or use the older `onEvent` callback shape.
|
|
8256
|
+
*/
|
|
8257
|
+
async submitMessage(conversation, message, abortSignal, onEvent, options = {}) {
|
|
8258
|
+
const events = [];
|
|
8259
|
+
for await (const event of this.submitMessageStream(conversation, message, abortSignal, options)) {
|
|
8260
|
+
events.push(event);
|
|
8261
|
+
await onEvent?.(event);
|
|
8262
|
+
}
|
|
8263
|
+
return events;
|
|
6134
8264
|
}
|
|
8265
|
+
/**
|
|
8266
|
+
* Executes a slash command through the shared command dispatcher and maps
|
|
8267
|
+
* the command output into runtime events. Commands that can change KB
|
|
8268
|
+
* readiness also refresh the displayed knowledge status so the TUI footer
|
|
8269
|
+
* and chat status stay aligned with the command result.
|
|
8270
|
+
*/
|
|
6135
8271
|
async submitSlashCommand(command, onProgress) {
|
|
6136
8272
|
const result = await executeSlashCommand(command, {
|
|
6137
8273
|
workspaceRoot: this.context.workspaceRoot,
|
|
@@ -6145,6 +8281,12 @@ var TopchesterAgentRuntime = class {
|
|
|
6145
8281
|
events.push(agentEvent.status("ready"));
|
|
6146
8282
|
return events;
|
|
6147
8283
|
}
|
|
8284
|
+
/**
|
|
8285
|
+
* Reads the project KB status and augments it with a count of files that
|
|
8286
|
+
* would be touched by a dry-run compile. The dry run is only performed for
|
|
8287
|
+
* a ready KB directory, because missing or incomplete KB states already
|
|
8288
|
+
* have enough information for the startup and status messages.
|
|
8289
|
+
*/
|
|
6148
8290
|
async getKnowledgeStatusWithNonCleanFileCount() {
|
|
6149
8291
|
const status = getKnowledgeStatus(this.context.workspaceRoot);
|
|
6150
8292
|
if (!status.kbExists || !status.kbIsDirectory || status.kbContentState !== "ready") return status;
|
|
@@ -6154,12 +8296,88 @@ var TopchesterAgentRuntime = class {
|
|
|
6154
8296
|
nonCleanFileCount: result.files.length
|
|
6155
8297
|
};
|
|
6156
8298
|
}
|
|
8299
|
+
/**
|
|
8300
|
+
* Adds relevant L1 knowledge context to the conversation prompt when the
|
|
8301
|
+
* compiled KB is present and ready. Search failures are logged and then
|
|
8302
|
+
* ignored on purpose: stale or broken KB search should not prevent the
|
|
8303
|
+
* user's chat turn from reaching the model.
|
|
8304
|
+
*/
|
|
8305
|
+
async buildPromptWithKnowledgeContext(prompt, message) {
|
|
8306
|
+
if (this.options.disableL1Context ?? isL1ContextDisabledByEnv()) {
|
|
8307
|
+
this.context.logger.debug({
|
|
8308
|
+
event: "kb_context_pack_skipped",
|
|
8309
|
+
reason: "disabled"
|
|
8310
|
+
}, "kb context pack skipped");
|
|
8311
|
+
return prompt;
|
|
8312
|
+
}
|
|
8313
|
+
const status = getKnowledgeStatus(this.context.workspaceRoot);
|
|
8314
|
+
if (!status.kbExists || !status.kbIsDirectory || status.kbContentState !== "ready") return prompt;
|
|
8315
|
+
try {
|
|
8316
|
+
const contextPack = await createL1ContextPack(this.context.workspaceRoot, message, {
|
|
8317
|
+
limit: 8,
|
|
8318
|
+
minScore: 12
|
|
8319
|
+
});
|
|
8320
|
+
this.context.logger.debug({
|
|
8321
|
+
event: "kb_context_pack",
|
|
8322
|
+
query: message,
|
|
8323
|
+
entryCount: contextPack.entryCount,
|
|
8324
|
+
relevantFileCount: contextPack.relevantFiles.length,
|
|
8325
|
+
paths: contextPack.relevantFiles.map((file) => file.path),
|
|
8326
|
+
warnings: contextPack.warnings
|
|
8327
|
+
}, "kb context pack");
|
|
8328
|
+
this.context.logger.trace({
|
|
8329
|
+
event: "kb_context_pack_payload",
|
|
8330
|
+
contextPack
|
|
8331
|
+
}, "kb context pack payload");
|
|
8332
|
+
if (contextPack.relevantFiles.length === 0) return prompt;
|
|
8333
|
+
return `${formatL1ContextPackForPrompt(contextPack)}\n\nConversation:\n${prompt}`;
|
|
8334
|
+
} catch (error) {
|
|
8335
|
+
this.context.logger.debug({
|
|
8336
|
+
event: "kb_context_pack_failed",
|
|
8337
|
+
error: error instanceof Error ? error.message : String(error)
|
|
8338
|
+
}, "kb context pack failed");
|
|
8339
|
+
return prompt;
|
|
8340
|
+
}
|
|
8341
|
+
}
|
|
6157
8342
|
};
|
|
8343
|
+
function createRuntimeEventQueue() {
|
|
8344
|
+
const events = [];
|
|
8345
|
+
let closed = false;
|
|
8346
|
+
let notify;
|
|
8347
|
+
return {
|
|
8348
|
+
push(event) {
|
|
8349
|
+
events.push(event);
|
|
8350
|
+
notify?.();
|
|
8351
|
+
notify = void 0;
|
|
8352
|
+
},
|
|
8353
|
+
close() {
|
|
8354
|
+
closed = true;
|
|
8355
|
+
notify?.();
|
|
8356
|
+
notify = void 0;
|
|
8357
|
+
},
|
|
8358
|
+
async *[Symbol.asyncIterator]() {
|
|
8359
|
+
while (!closed || events.length > 0) {
|
|
8360
|
+
const event = events.shift();
|
|
8361
|
+
if (event) {
|
|
8362
|
+
yield event;
|
|
8363
|
+
continue;
|
|
8364
|
+
}
|
|
8365
|
+
await new Promise((resolve) => {
|
|
8366
|
+
notify = resolve;
|
|
8367
|
+
});
|
|
8368
|
+
}
|
|
8369
|
+
}
|
|
8370
|
+
};
|
|
8371
|
+
}
|
|
8372
|
+
/**
|
|
8373
|
+
* Calls the configured model gateway for a single agent step and normalizes
|
|
8374
|
+
* the result into the newer `ModelAgentResult` shape. Gateways that implement
|
|
8375
|
+
* native agent stepping receive the tool registry directly; older text-only
|
|
8376
|
+
* gateways fall back to parsing a JSON or XML tool call out of the model text
|
|
8377
|
+
* so the rest of the runtime can use the same tool loop.
|
|
8378
|
+
*/
|
|
6158
8379
|
async function generateAgentStep(context, request) {
|
|
6159
|
-
if ("generateAgentStep" in context.modelGateway && typeof context.modelGateway.generateAgentStep === "function") return context.modelGateway.generateAgentStep({
|
|
6160
|
-
...request,
|
|
6161
|
-
tools: Object.values(toolRegistry)
|
|
6162
|
-
});
|
|
8380
|
+
if ("generateAgentStep" in context.modelGateway && typeof context.modelGateway.generateAgentStep === "function") return context.modelGateway.generateAgentStep({ ...request });
|
|
6163
8381
|
const result = await context.modelGateway.generateText(request);
|
|
6164
8382
|
const parsed = parseToolCallWithSource(result.text);
|
|
6165
8383
|
const toolProtocol = parsed?.source === "text-xml" ? "text-xml" : "text-json";
|
|
@@ -6183,15 +8401,56 @@ async function generateAgentStep(context, request) {
|
|
|
6183
8401
|
openRouterRoutingApplied: false
|
|
6184
8402
|
};
|
|
6185
8403
|
}
|
|
8404
|
+
function getSuppressiblePlanTodoAnswer(call, modelText, currentPlan) {
|
|
8405
|
+
if (call.tool !== "plan_todo" || hasOpenTaskPlan(currentPlan)) return;
|
|
8406
|
+
const items = call.args.items;
|
|
8407
|
+
if (!Array.isArray(items) || items.some((item) => !isCompletedPlanTodoItem(item))) return;
|
|
8408
|
+
const parsed = parseToolCallWithSource(modelText, ["text-json"]);
|
|
8409
|
+
return parsed?.remainder ? parsed.remainder : void 0;
|
|
8410
|
+
}
|
|
8411
|
+
function stripSuppressiblePlanTodoPrefix(modelText, currentPlan) {
|
|
8412
|
+
const parsed = parseToolCallWithSource(modelText, ["text-json"]);
|
|
8413
|
+
if (!parsed) return;
|
|
8414
|
+
return getSuppressiblePlanTodoAnswer(parsed.call, modelText, currentPlan);
|
|
8415
|
+
}
|
|
8416
|
+
function isCompletedPlanTodoItem(item) {
|
|
8417
|
+
return Boolean(item && typeof item === "object" && "status" in item && item.status === "completed");
|
|
8418
|
+
}
|
|
8419
|
+
/**
|
|
8420
|
+
* Reads the optional environment override for the tool-calling protocol.
|
|
8421
|
+
* Invalid values are ignored instead of failing startup, which keeps local
|
|
8422
|
+
* experimentation contained to supported protocol names while preserving the
|
|
8423
|
+
* normal automatic negotiation path by default.
|
|
8424
|
+
*/
|
|
6186
8425
|
function readToolProtocolEnvOverride() {
|
|
6187
8426
|
const value = process.env.TOPCHESTER_TOOL_PROTOCOL;
|
|
6188
8427
|
if (value === "auto" || value === "native" || value === "text-json" || value === "text-xml") return value;
|
|
6189
8428
|
}
|
|
8429
|
+
function isL1ContextDisabledByEnv() {
|
|
8430
|
+
const value = process.env.TOPCHESTER_DISABLE_L1_CONTEXT?.trim().toLowerCase();
|
|
8431
|
+
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
8432
|
+
}
|
|
8433
|
+
function shouldShowTokenUsageByEnv() {
|
|
8434
|
+
const value = process.env.TOPCHESTER_SHOW_TOKEN_USAGE?.trim().toLowerCase();
|
|
8435
|
+
return value !== void 0 && value !== "" && value !== "0" && value !== "false" && value !== "no" && value !== "off";
|
|
8436
|
+
}
|
|
8437
|
+
/**
|
|
8438
|
+
* Applies TUI styling to per-file KB sync states. The raw scanner statuses
|
|
8439
|
+
* are preserved as text, but success, warning, and error categories get
|
|
8440
|
+
* different colors so slash-command output is readable without changing the
|
|
8441
|
+
* underlying command semantics.
|
|
8442
|
+
*/
|
|
6190
8443
|
function formatTuiSyncStatus(status) {
|
|
6191
8444
|
if (status === "current") return ui.ok(status);
|
|
6192
8445
|
if (status === "invalid" || status === "missing_file") return ui.error(status);
|
|
6193
8446
|
return ui.warn(status);
|
|
6194
8447
|
}
|
|
8448
|
+
/**
|
|
8449
|
+
* Decides whether a slash command should trigger a fresh KB status event.
|
|
8450
|
+
* Only KB subcommands that can initialize, rebuild, sync, reset, or inspect
|
|
8451
|
+
* the compiled knowledge state need the refresh; other commands can return
|
|
8452
|
+
* their output without doing extra filesystem work.
|
|
8453
|
+
*/
|
|
6195
8454
|
function shouldRefreshKnowledgeStatus(command) {
|
|
6196
8455
|
const parsed = parseSlashCommand(command);
|
|
6197
8456
|
return parsed?.name === "kb" && [
|
|
@@ -6202,19 +8461,44 @@ function shouldRefreshKnowledgeStatus(command) {
|
|
|
6202
8461
|
"status"
|
|
6203
8462
|
].includes(parsed.args[0] ?? "");
|
|
6204
8463
|
}
|
|
8464
|
+
/**
|
|
8465
|
+
* Converts a computed KB status into the startup event shape consumed by the
|
|
8466
|
+
* TUI. The event carries both the structured status and a short next-step
|
|
8467
|
+
* message, letting renderers show precise state while keeping user-facing
|
|
8468
|
+
* guidance in one place.
|
|
8469
|
+
*/
|
|
6205
8470
|
function getKnowledgeStatusEvents(status) {
|
|
6206
8471
|
return [agentEvent.knowledgeStatus(status, formatStartupKnowledgeGuidance(status))];
|
|
6207
8472
|
}
|
|
8473
|
+
/**
|
|
8474
|
+
* Produces the short guidance line shown with startup KB status. The message
|
|
8475
|
+
* is deliberately action-oriented: it points to the next command that would
|
|
8476
|
+
* fix the current state and returns nothing when the KB is ready and clean.
|
|
8477
|
+
*/
|
|
6208
8478
|
function formatStartupKnowledgeGuidance(status) {
|
|
6209
8479
|
if (!status.kbExists) return "Next: run /kb init, then /kb compile to create project knowledge.";
|
|
6210
8480
|
if (!status.kbIsDirectory) return "Fix the KB path or config, then run /kb status.";
|
|
6211
8481
|
if (status.kbContentState !== "ready") return "Next: run /kb compile to build project knowledge.";
|
|
6212
8482
|
if ((status.nonCleanFileCount ?? 0) > 0) return "Next: run /kb sync to update project knowledge, or /kb status to inspect the files.";
|
|
6213
8483
|
}
|
|
8484
|
+
/**
|
|
8485
|
+
* Serializes a tool execution result into the text that is fed back to the
|
|
8486
|
+
* model after a tool call. Each tool gets the metadata the model needs for
|
|
8487
|
+
* the next step, such as file hashes, diffs, command exit status, truncation
|
|
8488
|
+
* state, or KB dirty-state signals, while errors are presented in a uniform
|
|
8489
|
+
* error block.
|
|
8490
|
+
*/
|
|
6214
8491
|
function formatToolResultForPrompt(result) {
|
|
6215
8492
|
const path = result.path ? ` ${JSON.stringify(result.path)}` : "";
|
|
6216
8493
|
const command = result.command ? ` via ${result.command}` : "";
|
|
6217
8494
|
const warning = result.warning ? `\nWarning: ${result.warning}` : "";
|
|
8495
|
+
if (isToolErrorResult(result)) return [
|
|
8496
|
+
`Tool result from ${result.tool}${path}${command}:`,
|
|
8497
|
+
`Error: ${result.error}`,
|
|
8498
|
+
"```",
|
|
8499
|
+
result.content,
|
|
8500
|
+
"```"
|
|
8501
|
+
].join("\n");
|
|
6218
8502
|
if (result.tool === "read_file") return [
|
|
6219
8503
|
`Tool result from ${result.tool}${path}${command}:${warning}`,
|
|
6220
8504
|
`hash: ${result.hash}`,
|
|
@@ -6222,6 +8506,8 @@ function formatToolResultForPrompt(result) {
|
|
|
6222
8506
|
result.content,
|
|
6223
8507
|
"```"
|
|
6224
8508
|
].join("\n");
|
|
8509
|
+
if (result.tool === "plan_todo") return [`Tool result from ${result.tool}:`, result.content].join("\n");
|
|
8510
|
+
if (result.tool === "task") return [`Tool result from ${result.tool}:`, result.content].join("\n");
|
|
6225
8511
|
if (result.tool === "edit_file") return [
|
|
6226
8512
|
`Tool result from ${result.tool}${path}:`,
|
|
6227
8513
|
`before_hash: ${result.beforeHash}`,
|
|
@@ -6319,11 +8605,50 @@ function formatToolResultForPrompt(result) {
|
|
|
6319
8605
|
"```"
|
|
6320
8606
|
].join("\n");
|
|
6321
8607
|
}
|
|
6322
|
-
|
|
6323
|
-
|
|
8608
|
+
/**
|
|
8609
|
+
* Builds the follow-up instruction appended after each tool result. It keeps
|
|
8610
|
+
* the model on the active task, reminds it to maintain the visible plan, and
|
|
8611
|
+
* restates the current tool-call protocol so the next model step remains
|
|
8612
|
+
* parseable by the runtime.
|
|
8613
|
+
*/
|
|
8614
|
+
function formatContinuationInstruction(protocol, result, canUsePlanTodo = true) {
|
|
8615
|
+
const toolInstruction = protocol === "text-xml" ? "If another tool is needed, reply with only one XML tool call." : protocol === "text-json" ? "If another tool is needed, reply with only that tool JSON." : "If another tool is needed, use the available tool calling path.";
|
|
8616
|
+
return [
|
|
8617
|
+
"Continue the user's request using the tool result above and the visible plan when one is active.",
|
|
8618
|
+
result.tool === "find_file" ? "find_file results are paths only; if the user asked to read or answer from file contents, call read_file on the relevant path before answering. Do not ask the user to provide the read_file result or permission." : "",
|
|
8619
|
+
canUsePlanTodo ? "Update plan_todo after major progress changes." : "",
|
|
8620
|
+
canUsePlanTodo ? "Before a final answer, close the visible plan by calling plan_todo with all finished items marked completed, or with [] if abandoning the plan." : "",
|
|
8621
|
+
toolInstruction,
|
|
8622
|
+
"Otherwise answer the user. Do not guess."
|
|
8623
|
+
].filter(Boolean).join(" ");
|
|
8624
|
+
}
|
|
8625
|
+
/**
|
|
8626
|
+
* Creates the corrective prompt used when the model tries to answer while a
|
|
8627
|
+
* visible task plan is still open. The draft final answer is preserved so the
|
|
8628
|
+
* model can reuse it after closing the plan, but the immediate instruction is
|
|
8629
|
+
* to call `plan_todo` first.
|
|
8630
|
+
*/
|
|
8631
|
+
function formatOpenPlanClosureInstruction(draftAnswer, protocol) {
|
|
8632
|
+
const toolInstruction = protocol === "text-xml" ? "Reply now with only one XML plan_todo tool call." : protocol === "text-json" ? "Reply now with only the plan_todo JSON object." : "Use the available tool calling path now to call plan_todo.";
|
|
8633
|
+
const trimmedDraft = draftAnswer.trim();
|
|
8634
|
+
return [
|
|
8635
|
+
"The visible plan still has unfinished items, so do not provide the final answer yet.",
|
|
8636
|
+
"First close the plan with plan_todo: mark completed work as completed, keep one item in_progress only if work truly remains, or use [] if abandoning the plan.",
|
|
8637
|
+
toolInstruction,
|
|
8638
|
+
trimmedDraft ? `After the plan_todo result, use this draft final answer if it is still accurate:\n${trimmedDraft}` : ""
|
|
8639
|
+
].filter(Boolean).join("\n");
|
|
6324
8640
|
}
|
|
8641
|
+
/**
|
|
8642
|
+
* Formats a compact, user-visible summary for a tool call event. When a
|
|
8643
|
+
* result is available the summary includes useful completion details, such as
|
|
8644
|
+
* changed-line counts, staged paths, commit subjects, or command failures,
|
|
8645
|
+
* instead of echoing the full tool payload.
|
|
8646
|
+
*/
|
|
6325
8647
|
function formatToolCallMessage(call, result) {
|
|
8648
|
+
if (result && isToolErrorResult(result)) return `${call.tool} failed: ${result.error}`;
|
|
6326
8649
|
switch (call.tool) {
|
|
8650
|
+
case "task": return result?.tool === "task" ? `task: ${result.status} ${result.childSessionId}` : `task: ${call.args.description}`;
|
|
8651
|
+
case "plan_todo": return result?.tool === "plan_todo" ? `plan_todo: ${result.plan.items.length} items, ${result.inProgressCount} active` : `plan_todo: ${call.args.items.length} items`;
|
|
6327
8652
|
case "read_file": return `read_file: ${call.args.path}`;
|
|
6328
8653
|
case "list_files": return `list_files: ${call.args.path}${call.args.recursive ? " (recursive)" : ""}`;
|
|
6329
8654
|
case "grep": return `grep: ${call.args.pattern} in ${call.args.path ?? "."}`;
|
|
@@ -6338,21 +8663,62 @@ function formatToolCallMessage(call, result) {
|
|
|
6338
8663
|
case "inspect_command": return `inspect_command: ${call.args.command}`;
|
|
6339
8664
|
}
|
|
6340
8665
|
}
|
|
8666
|
+
/**
|
|
8667
|
+
* Summarizes a `git_diff` call for the TUI event list. Successful results
|
|
8668
|
+
* report the resolved scope, file count, and truncation marker; pending or
|
|
8669
|
+
* failed calls fall back to the requested scope from the tool arguments.
|
|
8670
|
+
*/
|
|
6341
8671
|
function formatGitDiffCallSummary(call, result) {
|
|
6342
|
-
if (result?.tool === "git_diff") return `${result.scope} (${result.fileCount} files${result.truncated ? ", truncated" : ""})`;
|
|
8672
|
+
if (result?.tool === "git_diff" && !isToolErrorResult(result)) return `${result.scope} (${result.fileCount} files${result.truncated ? ", truncated" : ""})`;
|
|
6343
8673
|
return call.args.scope;
|
|
6344
8674
|
}
|
|
8675
|
+
/**
|
|
8676
|
+
* Returns the parenthesized change summary for a successful `edit_file`
|
|
8677
|
+
* result. Non-edit results and failed edits intentionally return an empty
|
|
8678
|
+
* suffix so the main tool-call formatter can keep one path for success,
|
|
8679
|
+
* failure, and pre-result display.
|
|
8680
|
+
*/
|
|
6345
8681
|
function formatEditFileChangeSummary(result) {
|
|
6346
|
-
if (result?.tool !== "edit_file") return "";
|
|
8682
|
+
if (result?.tool !== "edit_file" || isToolErrorResult(result)) return "";
|
|
6347
8683
|
return ` (changed ${result.editEvent.diffSummary})`;
|
|
6348
8684
|
}
|
|
8685
|
+
/**
|
|
8686
|
+
* Returns the parenthesized write summary for a successful `write_file`
|
|
8687
|
+
* result. The helper mirrors the edit summary helper, keeping write-specific
|
|
8688
|
+
* result details out of the larger switch that formats all tool-call messages.
|
|
8689
|
+
*/
|
|
6349
8690
|
function formatWriteFileChangeSummary(result) {
|
|
6350
|
-
if (result?.tool !== "write_file") return "";
|
|
8691
|
+
if (result?.tool !== "write_file" || isToolErrorResult(result)) return "";
|
|
6351
8692
|
return ` (${result.writeEvent.writeSummary})`;
|
|
6352
8693
|
}
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
8694
|
+
/**
|
|
8695
|
+
* Formats the assistant-message metadata shown next to the final response.
|
|
8696
|
+
* The model identifier and cumulative turn duration are kept together here
|
|
8697
|
+
* so callers do not need to know how agent-loop timing should be presented.
|
|
8698
|
+
*/
|
|
8699
|
+
function formatAgentMessageMeta(model, durationMs, usage) {
|
|
8700
|
+
const tokenUsage = shouldShowTokenUsageByEnv() ? formatTokenUsage(usage) : void 0;
|
|
8701
|
+
return [
|
|
8702
|
+
model,
|
|
8703
|
+
formatDuration$1(durationMs),
|
|
8704
|
+
tokenUsage
|
|
8705
|
+
].filter(Boolean).join(" · ");
|
|
8706
|
+
}
|
|
8707
|
+
function addTokenUsageTotals(totals, usage) {
|
|
8708
|
+
if (!usage) return;
|
|
8709
|
+
if (typeof usage.inputTokens === "number") totals.inputTokens = (totals.inputTokens ?? 0) + usage.inputTokens;
|
|
8710
|
+
if (typeof usage.outputTokens === "number") totals.outputTokens = (totals.outputTokens ?? 0) + usage.outputTokens;
|
|
8711
|
+
}
|
|
8712
|
+
function formatTokenUsage(usage) {
|
|
8713
|
+
if (usage?.inputTokens === void 0 && usage?.outputTokens === void 0) return;
|
|
8714
|
+
return `${formatInteger(usage.inputTokens ?? 0)} input / ${formatInteger(usage.outputTokens ?? 0)} output tokens`;
|
|
8715
|
+
}
|
|
8716
|
+
/**
|
|
8717
|
+
* Converts elapsed milliseconds into the short human-readable duration used
|
|
8718
|
+
* in assistant metadata. Very short turns keep one decimal place, normal
|
|
8719
|
+
* sub-minute turns round to seconds, and longer turns switch to minutes plus
|
|
8720
|
+
* remaining seconds.
|
|
8721
|
+
*/
|
|
6356
8722
|
function formatDuration$1(durationMs) {
|
|
6357
8723
|
const totalSeconds = Math.max(0, durationMs / 1e3);
|
|
6358
8724
|
if (totalSeconds < 10) return `${formatNumber(totalSeconds, 1)} sec`;
|
|
@@ -6362,12 +8728,21 @@ function formatDuration$1(durationMs) {
|
|
|
6362
8728
|
if (seconds === 0) return `${minutes} min`;
|
|
6363
8729
|
return `${minutes} min ${seconds} sec`;
|
|
6364
8730
|
}
|
|
8731
|
+
/**
|
|
8732
|
+
* Formats a number with a fixed number of fraction digits using the English
|
|
8733
|
+
* locale expected by the TUI metadata strings. Keeping this tiny wrapper
|
|
8734
|
+
* avoids repeating the minimum and maximum fraction-digit options at every
|
|
8735
|
+
* call site.
|
|
8736
|
+
*/
|
|
6365
8737
|
function formatNumber(value, fractionDigits) {
|
|
6366
8738
|
return value.toLocaleString("en", {
|
|
6367
8739
|
minimumFractionDigits: fractionDigits,
|
|
6368
8740
|
maximumFractionDigits: fractionDigits
|
|
6369
8741
|
});
|
|
6370
8742
|
}
|
|
8743
|
+
function formatInteger(value) {
|
|
8744
|
+
return value.toLocaleString("en", { maximumFractionDigits: 0 });
|
|
8745
|
+
}
|
|
6371
8746
|
//#endregion
|
|
6372
8747
|
//#region src/tui/runtime-events.ts
|
|
6373
8748
|
function renderRuntimeEvent(event) {
|
|
@@ -6381,9 +8756,39 @@ function renderRuntimeEvent(event) {
|
|
|
6381
8756
|
body: event.body,
|
|
6382
8757
|
actions: event.actions
|
|
6383
8758
|
})];
|
|
8759
|
+
case "task_plan": return [];
|
|
8760
|
+
case "subagent_started": return [subagentMessage({
|
|
8761
|
+
status: "running",
|
|
8762
|
+
sessionId: event.sessionId,
|
|
8763
|
+
title: event.title
|
|
8764
|
+
})];
|
|
8765
|
+
case "subagent_event": return formatForwardedSubagentEvent(event.sessionId, event.event);
|
|
8766
|
+
case "subagent_completed": return [subagentMessage({
|
|
8767
|
+
status: "completed",
|
|
8768
|
+
sessionId: event.sessionId,
|
|
8769
|
+
text: event.result
|
|
8770
|
+
})];
|
|
8771
|
+
case "subagent_failed": return [subagentMessage({
|
|
8772
|
+
status: "failed",
|
|
8773
|
+
sessionId: event.sessionId,
|
|
8774
|
+
text: event.error
|
|
8775
|
+
})];
|
|
6384
8776
|
case "status": return [];
|
|
6385
8777
|
}
|
|
6386
8778
|
}
|
|
8779
|
+
function formatForwardedSubagentEvent(sessionId, event) {
|
|
8780
|
+
if (event.type === "message" && event.role === "assistant") return [subagentMessage({
|
|
8781
|
+
status: "event",
|
|
8782
|
+
sessionId,
|
|
8783
|
+
text: event.text
|
|
8784
|
+
})];
|
|
8785
|
+
if (event.type === "tool_call") return [subagentMessage({
|
|
8786
|
+
status: "event",
|
|
8787
|
+
sessionId,
|
|
8788
|
+
text: event.label
|
|
8789
|
+
})];
|
|
8790
|
+
return [];
|
|
8791
|
+
}
|
|
6387
8792
|
function formatKbPathSource(status) {
|
|
6388
8793
|
return status.kbPathSource === "env" ? " (custom)" : "";
|
|
6389
8794
|
}
|
|
@@ -6394,6 +8799,7 @@ var TopchesterTuiShell = class {
|
|
|
6394
8799
|
options;
|
|
6395
8800
|
runtime;
|
|
6396
8801
|
session;
|
|
8802
|
+
taskPlanNoticeTimer;
|
|
6397
8803
|
constructor(context, runtime, options = {}) {
|
|
6398
8804
|
this.context = context;
|
|
6399
8805
|
this.options = options;
|
|
@@ -6410,7 +8816,7 @@ var TopchesterTuiShell = class {
|
|
|
6410
8816
|
const folderName = getFolderName(this.context.workspaceRoot);
|
|
6411
8817
|
const modelLabel = getModelLabel(this.context);
|
|
6412
8818
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
6413
|
-
console.log(renderStaticLayout(messages, folderName, modelLabel));
|
|
8819
|
+
console.log(renderStaticLayout(messages, folderName, modelLabel, this.options.initialTaskPlan));
|
|
6414
8820
|
return;
|
|
6415
8821
|
}
|
|
6416
8822
|
const terminal = new ProcessTerminal();
|
|
@@ -6429,11 +8835,12 @@ var TopchesterTuiShell = class {
|
|
|
6429
8835
|
process.exit(0);
|
|
6430
8836
|
}
|
|
6431
8837
|
});
|
|
8838
|
+
app.setTaskPlan(this.options.initialTaskPlan);
|
|
6432
8839
|
app.setSubmitMessage((message) => {
|
|
6433
|
-
this.submitChatMessage(app, tui, message);
|
|
8840
|
+
this.startBackgroundTask(app, tui, "Chat", () => this.submitChatMessage(app, tui, message));
|
|
6434
8841
|
});
|
|
6435
8842
|
app.setSubmitCommand((command) => {
|
|
6436
|
-
this.submitSlashCommand(app, tui, command);
|
|
8843
|
+
this.startBackgroundTask(app, tui, "Command", () => this.submitSlashCommand(app, tui, command));
|
|
6437
8844
|
});
|
|
6438
8845
|
tui.addChild(app);
|
|
6439
8846
|
tui.setFocus(app);
|
|
@@ -6450,7 +8857,15 @@ var TopchesterTuiShell = class {
|
|
|
6450
8857
|
}
|
|
6451
8858
|
}));
|
|
6452
8859
|
tui.start();
|
|
6453
|
-
this.checkAgent(app, tui);
|
|
8860
|
+
this.startBackgroundTask(app, tui, "Agent check", () => this.checkAgent(app, tui));
|
|
8861
|
+
}
|
|
8862
|
+
startBackgroundTask(app, tui, label, task) {
|
|
8863
|
+
task().catch((error) => {
|
|
8864
|
+
app.addMessage(systemMessage(`${label} failed: ${formatPlainError(error)}`));
|
|
8865
|
+
app.setStatus("ready");
|
|
8866
|
+
app.setCancelPending(void 0);
|
|
8867
|
+
tui.requestRender();
|
|
8868
|
+
});
|
|
6454
8869
|
}
|
|
6455
8870
|
async checkAgent(app, tui) {
|
|
6456
8871
|
const busy = new BusyIndicator(app, tui, {
|
|
@@ -6471,13 +8886,13 @@ var TopchesterTuiShell = class {
|
|
|
6471
8886
|
busy.start();
|
|
6472
8887
|
tui.requestRender();
|
|
6473
8888
|
try {
|
|
6474
|
-
await this.applyRuntimeEvents(app, await this.runtime.checkAgent(abortController.signal));
|
|
8889
|
+
await this.applyRuntimeEvents(app, await this.runtime.checkAgent(abortController.signal), tui);
|
|
6475
8890
|
} catch (error) {
|
|
6476
8891
|
if (cancelled) {
|
|
6477
8892
|
app.addMessage(systemMessage("Agent check stopped."));
|
|
6478
8893
|
app.setStatus("ready");
|
|
6479
8894
|
} else {
|
|
6480
|
-
const message =
|
|
8895
|
+
const message = formatPlainError(error);
|
|
6481
8896
|
app.addMessage(systemMessage(`Agent check failed: ${message}`));
|
|
6482
8897
|
app.setStatus("agent check failed");
|
|
6483
8898
|
}
|
|
@@ -6485,7 +8900,7 @@ var TopchesterTuiShell = class {
|
|
|
6485
8900
|
app.setCancelPending(void 0);
|
|
6486
8901
|
busy.stop();
|
|
6487
8902
|
}
|
|
6488
|
-
if (app.isReady()) await this.applyRuntimeEvents(app, await this.runtime.checkKnowledgeBase());
|
|
8903
|
+
if (app.isReady()) await this.applyRuntimeEvents(app, await this.runtime.checkKnowledgeBase(), tui);
|
|
6489
8904
|
tui.requestRender();
|
|
6490
8905
|
}
|
|
6491
8906
|
async submitChatMessage(app, tui, message) {
|
|
@@ -6499,6 +8914,7 @@ var TopchesterTuiShell = class {
|
|
|
6499
8914
|
]
|
|
6500
8915
|
});
|
|
6501
8916
|
const abortController = new AbortController();
|
|
8917
|
+
const reasoningDisplay = isStreamReasoningEnabledByEnv() ? createBusyReasoningSink(busy) : void 0;
|
|
6502
8918
|
let cancelled = false;
|
|
6503
8919
|
app.setCancelPending(() => {
|
|
6504
8920
|
cancelled = true;
|
|
@@ -6507,19 +8923,29 @@ var TopchesterTuiShell = class {
|
|
|
6507
8923
|
busy.start();
|
|
6508
8924
|
tui.requestRender();
|
|
6509
8925
|
try {
|
|
8926
|
+
await this.clearTaskPlanForNewTurn(app);
|
|
6510
8927
|
await this.persistPayloadWithWarning(app, {
|
|
6511
8928
|
kind: "message",
|
|
6512
8929
|
role: "user",
|
|
6513
8930
|
text: message
|
|
6514
8931
|
});
|
|
6515
|
-
await
|
|
8932
|
+
for await (const event of this.runtime.submitMessageStream(app.getConversationTurns(), message, abortController.signal, {
|
|
8933
|
+
onReasoning: reasoningDisplay?.sink,
|
|
8934
|
+
session: this.session
|
|
8935
|
+
})) {
|
|
8936
|
+
if (event.type === "message" && event.role === "assistant") {
|
|
8937
|
+
reasoningDisplay?.commit(app);
|
|
8938
|
+
busy.clearActivity();
|
|
8939
|
+
}
|
|
8940
|
+
await this.applyRuntimeEvents(app, [event], tui);
|
|
8941
|
+
tui.requestRender();
|
|
8942
|
+
}
|
|
6516
8943
|
} catch (error) {
|
|
6517
8944
|
if (cancelled) {
|
|
6518
8945
|
app.addMessage(systemMessage("Response stopped."));
|
|
6519
8946
|
app.setStatus("ready");
|
|
6520
8947
|
} else {
|
|
6521
|
-
|
|
6522
|
-
app.addMessage(systemMessage(`Chat failed: ${errorMessage}`));
|
|
8948
|
+
app.addMessage(systemMessage(`Chat failed: ${formatPlainError(error)}`));
|
|
6523
8949
|
app.setStatus("chat failed");
|
|
6524
8950
|
await this.persistPayloadWithWarning(app, {
|
|
6525
8951
|
kind: "status",
|
|
@@ -6542,13 +8968,13 @@ var TopchesterTuiShell = class {
|
|
|
6542
8968
|
busy.start();
|
|
6543
8969
|
tui.requestRender();
|
|
6544
8970
|
try {
|
|
8971
|
+
await this.clearTaskPlanForNewTurn(app);
|
|
6545
8972
|
await this.persistPayloadWithWarning(app, slashCommandToSessionPayload(command));
|
|
6546
8973
|
await this.applyRuntimeEvents(app, await this.runtime.submitSlashCommand(command, (event) => {
|
|
6547
8974
|
busy.setActivity(event.message);
|
|
6548
|
-
}));
|
|
8975
|
+
}), tui);
|
|
6549
8976
|
} catch (error) {
|
|
6550
|
-
|
|
6551
|
-
app.addMessage(systemMessage(`Command failed: ${errorMessage}`));
|
|
8977
|
+
app.addMessage(systemMessage(`Command failed: ${formatPlainError(error)}`));
|
|
6552
8978
|
app.setStatus("command failed");
|
|
6553
8979
|
await this.persistPayloadWithWarning(app, {
|
|
6554
8980
|
kind: "status",
|
|
@@ -6559,14 +8985,41 @@ var TopchesterTuiShell = class {
|
|
|
6559
8985
|
tui.requestRender();
|
|
6560
8986
|
}
|
|
6561
8987
|
}
|
|
6562
|
-
async applyRuntimeEvents(app, events) {
|
|
8988
|
+
async applyRuntimeEvents(app, events, renderRequester) {
|
|
6563
8989
|
for (const event of events) {
|
|
6564
8990
|
if (event.type === "status") app.setStatus(event.status);
|
|
6565
8991
|
if (event.type === "knowledge_status") app.setKnowledgeStatus(event.status);
|
|
8992
|
+
if (event.type === "task_plan") {
|
|
8993
|
+
const change = app.setTaskPlan(event.plan);
|
|
8994
|
+
app.setTaskPlanNotice(formatTaskPlanNotice(change, event.plan));
|
|
8995
|
+
this.scheduleTaskPlanNoticeClear(app, renderRequester);
|
|
8996
|
+
}
|
|
6566
8997
|
for (const message of renderRuntimeEvent(event)) app.addMessage(message);
|
|
6567
8998
|
await this.persistPayloadWithWarning(app, runtimeEventToSessionPayload(event));
|
|
6568
8999
|
}
|
|
6569
9000
|
}
|
|
9001
|
+
scheduleTaskPlanNoticeClear(app, renderRequester) {
|
|
9002
|
+
if (this.taskPlanNoticeTimer) {
|
|
9003
|
+
clearTimeout(this.taskPlanNoticeTimer);
|
|
9004
|
+
this.taskPlanNoticeTimer = void 0;
|
|
9005
|
+
}
|
|
9006
|
+
if (!renderRequester) return;
|
|
9007
|
+
this.taskPlanNoticeTimer = setTimeout(() => {
|
|
9008
|
+
this.taskPlanNoticeTimer = void 0;
|
|
9009
|
+
app.setTaskPlanNotice(void 0);
|
|
9010
|
+
renderRequester.requestRender();
|
|
9011
|
+
}, 2500);
|
|
9012
|
+
this.taskPlanNoticeTimer.unref?.();
|
|
9013
|
+
}
|
|
9014
|
+
async clearTaskPlanForNewTurn(app) {
|
|
9015
|
+
const clearedPlan = app.clearTaskPlan();
|
|
9016
|
+
if (!clearedPlan) return;
|
|
9017
|
+
await this.persistPayloadWithWarning(app, {
|
|
9018
|
+
kind: "task_plan",
|
|
9019
|
+
items: clearedPlan.items,
|
|
9020
|
+
updatedAt: clearedPlan.updatedAt
|
|
9021
|
+
});
|
|
9022
|
+
}
|
|
6570
9023
|
async persistPayloadWithWarning(app, payload) {
|
|
6571
9024
|
if (!this.session || !payload) return;
|
|
6572
9025
|
try {
|
|
@@ -6600,6 +9053,8 @@ function chatMessageToSessionPayload(message) {
|
|
|
6600
9053
|
text: message.text,
|
|
6601
9054
|
...message.meta === void 0 ? {} : { meta: message.meta }
|
|
6602
9055
|
};
|
|
9056
|
+
if (message.kind === "thinking") return;
|
|
9057
|
+
if (message.kind === "subagent") return;
|
|
6603
9058
|
if (message.kind === "modal") return {
|
|
6604
9059
|
kind: "choice",
|
|
6605
9060
|
tone: message.tone,
|
|
@@ -6613,33 +9068,6 @@ function chatMessageToSessionPayload(message) {
|
|
|
6613
9068
|
call: message.call
|
|
6614
9069
|
};
|
|
6615
9070
|
}
|
|
6616
|
-
function runtimeEventToSessionPayload(event) {
|
|
6617
|
-
switch (event.type) {
|
|
6618
|
-
case "message": return {
|
|
6619
|
-
kind: "message",
|
|
6620
|
-
role: event.role,
|
|
6621
|
-
text: event.text,
|
|
6622
|
-
...event.meta === void 0 ? {} : { meta: event.meta }
|
|
6623
|
-
};
|
|
6624
|
-
case "tool_call": return {
|
|
6625
|
-
kind: "tool_call",
|
|
6626
|
-
label: event.label,
|
|
6627
|
-
call: event.call
|
|
6628
|
-
};
|
|
6629
|
-
case "knowledge_status": return;
|
|
6630
|
-
case "choice": return {
|
|
6631
|
-
kind: "choice",
|
|
6632
|
-
tone: event.tone,
|
|
6633
|
-
title: event.title,
|
|
6634
|
-
...event.body === void 0 ? {} : { body: event.body },
|
|
6635
|
-
actions: event.actions
|
|
6636
|
-
};
|
|
6637
|
-
case "status": return {
|
|
6638
|
-
kind: "status",
|
|
6639
|
-
status: event.status
|
|
6640
|
-
};
|
|
6641
|
-
}
|
|
6642
|
-
}
|
|
6643
9071
|
function slashCommandToSessionPayload(command) {
|
|
6644
9072
|
return {
|
|
6645
9073
|
kind: "message",
|
|
@@ -6651,8 +9079,34 @@ function slashCommandToSessionPayload(command) {
|
|
|
6651
9079
|
}
|
|
6652
9080
|
};
|
|
6653
9081
|
}
|
|
9082
|
+
function isStreamReasoningEnabledByEnv() {
|
|
9083
|
+
const value = process.env.TOPCHESTER_STREAM_REASONING?.trim().toLowerCase();
|
|
9084
|
+
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
9085
|
+
}
|
|
9086
|
+
function createBusyReasoningSink(busy) {
|
|
9087
|
+
const buffer = new ReasoningTailBuffer();
|
|
9088
|
+
let committed = false;
|
|
9089
|
+
return {
|
|
9090
|
+
commit(app) {
|
|
9091
|
+
if (committed || !buffer.hasText) return;
|
|
9092
|
+
app.addMessage(thinkingMessage(buffer.value));
|
|
9093
|
+
committed = true;
|
|
9094
|
+
},
|
|
9095
|
+
async sink(event) {
|
|
9096
|
+
if (event.type === "clear") {
|
|
9097
|
+
buffer.clear();
|
|
9098
|
+
committed = false;
|
|
9099
|
+
busy.clearActivity();
|
|
9100
|
+
return;
|
|
9101
|
+
}
|
|
9102
|
+
const text = event.type === "summary" ? buffer.replace(event.text ?? "") : buffer.append(event.text ?? "");
|
|
9103
|
+
if (!text) return;
|
|
9104
|
+
busy.setActivity(ui.muted(text));
|
|
9105
|
+
}
|
|
9106
|
+
};
|
|
9107
|
+
}
|
|
6654
9108
|
function formatPlainError(error) {
|
|
6655
|
-
return error instanceof Error ? error.message : String(error);
|
|
9109
|
+
return (error instanceof Error ? error.message : String(error)).split(/\r?\n/u).map((line) => line.trim()).find((line) => line.length > 0) ?? "Unknown error";
|
|
6656
9110
|
}
|
|
6657
9111
|
function printExitBanner(sessionId, durationMs) {
|
|
6658
9112
|
console.log("");
|
|
@@ -6796,8 +9250,8 @@ async function executeRunCommand(context, options) {
|
|
|
6796
9250
|
text: options.prompt,
|
|
6797
9251
|
inputType: "prompt"
|
|
6798
9252
|
});
|
|
6799
|
-
await
|
|
6800
|
-
|
|
9253
|
+
for await (const event of runtime.submitMessageStream(conversation, options.prompt, abortController.signal, { session })) await applyRuntimeEvent({
|
|
9254
|
+
event,
|
|
6801
9255
|
session,
|
|
6802
9256
|
jsonEvents,
|
|
6803
9257
|
runId,
|
|
@@ -6846,7 +9300,9 @@ async function loadConversation(workspaceRoot, resume) {
|
|
|
6846
9300
|
text: message.text
|
|
6847
9301
|
}];
|
|
6848
9302
|
case "system":
|
|
9303
|
+
case "thinking":
|
|
6849
9304
|
case "tool_call":
|
|
9305
|
+
case "subagent":
|
|
6850
9306
|
case "modal": return [];
|
|
6851
9307
|
}
|
|
6852
9308
|
});
|
|
@@ -6858,12 +9314,16 @@ async function persistStartupMessages(session, context) {
|
|
|
6858
9314
|
}
|
|
6859
9315
|
}
|
|
6860
9316
|
async function applyRuntimeEvents(options) {
|
|
6861
|
-
for (const event of options.events) {
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
9317
|
+
for (const event of options.events) await applyRuntimeEvent({
|
|
9318
|
+
...options,
|
|
9319
|
+
event
|
|
9320
|
+
});
|
|
9321
|
+
}
|
|
9322
|
+
async function applyRuntimeEvent(options) {
|
|
9323
|
+
const payload = runtimeEventToSessionPayload(options.event);
|
|
9324
|
+
if (payload) await options.session.append(payload);
|
|
9325
|
+
pushJson(options.jsonEvents, options.runId, options.session.sessionId, options.event.type, { event: options.event });
|
|
9326
|
+
if (options.plain) printPlainEvent(options.event);
|
|
6867
9327
|
}
|
|
6868
9328
|
function printPlainEvent(event) {
|
|
6869
9329
|
if (event.type === "message") {
|
|
@@ -6874,7 +9334,14 @@ function printPlainEvent(event) {
|
|
|
6874
9334
|
console.log(event.label);
|
|
6875
9335
|
return;
|
|
6876
9336
|
}
|
|
6877
|
-
if (event.type === "knowledge_status" && event.guidance)
|
|
9337
|
+
if (event.type === "knowledge_status" && event.guidance) {
|
|
9338
|
+
console.log(event.guidance);
|
|
9339
|
+
return;
|
|
9340
|
+
}
|
|
9341
|
+
if (event.type === "task_plan") {
|
|
9342
|
+
const notice = formatTaskPlanNotice("updated", event.plan);
|
|
9343
|
+
if (notice) console.log(notice);
|
|
9344
|
+
}
|
|
6878
9345
|
}
|
|
6879
9346
|
function pushJson(events, runId, sessionId, type, fields) {
|
|
6880
9347
|
events.push({
|
|
@@ -6906,9 +9373,12 @@ program.action(async () => {
|
|
|
6906
9373
|
try {
|
|
6907
9374
|
if (options.resume) {
|
|
6908
9375
|
const loaded = await loadSession(context.workspaceRoot, options.resume);
|
|
9376
|
+
const session = await loadSessionForAppend(context.workspaceRoot, loaded.sessionId);
|
|
9377
|
+
const rehydrated = rehydrateSession(loaded.events);
|
|
6909
9378
|
await new TopchesterTuiShell(context, void 0, {
|
|
6910
|
-
session
|
|
6911
|
-
initialMessages:
|
|
9379
|
+
session,
|
|
9380
|
+
initialMessages: rehydrated.messages,
|
|
9381
|
+
initialTaskPlan: rehydrated.taskPlan
|
|
6912
9382
|
}).render();
|
|
6913
9383
|
return;
|
|
6914
9384
|
}
|
|
@@ -6940,6 +9410,9 @@ program.command("run").description("run one prompt or slash command without open
|
|
|
6940
9410
|
process.exitCode = 1;
|
|
6941
9411
|
}
|
|
6942
9412
|
});
|
|
9413
|
+
program.command("search").description("search compiled L1 knowledge entries").argument("<query...>", "search query").option("--limit <count>", "maximum number of matches", parsePositiveInteger).option("--json", "write full JSON search result to stdout").action(async (queryParts, options) => {
|
|
9414
|
+
await executeKbSearchCommand(queryParts, options);
|
|
9415
|
+
});
|
|
6943
9416
|
const kbCommand = program.command("kb").description("knowledge base commands");
|
|
6944
9417
|
kbCommand.command("init").description("initialize a project knowledge base").action(async () => {
|
|
6945
9418
|
const context = createContextFromOptions();
|
|
@@ -6973,6 +9446,12 @@ kbCommand.command("sync").description("sync non-clean project files into the kno
|
|
|
6973
9446
|
console.log(formatKnowledgeSyncResult(result).join("\n"));
|
|
6974
9447
|
if (isPartialKnowledgeCompileResult(result)) process.exitCode = 2;
|
|
6975
9448
|
});
|
|
9449
|
+
kbCommand.command("search").alias("query").description("search compiled L1 knowledge entries").argument("<query...>", "search query").option("--limit <count>", "maximum number of matches", parsePositiveInteger).option("--json", "write full JSON search result to stdout").action(async (queryParts, options) => {
|
|
9450
|
+
await executeKbSearchCommand(queryParts, options);
|
|
9451
|
+
});
|
|
9452
|
+
kbCommand.command("context").description("create an L1 context pack for a query").argument("<query...>", "context query").option("--limit <count>", "maximum number of relevant files", parsePositiveInteger).option("--min-score <score>", "minimum match score", parseNonNegativeNumber).option("--json", "write JSON context pack to stdout").option("--full-l1", "include full raw L1 entries in JSON output").action(async (queryParts, options) => {
|
|
9453
|
+
await executeKbContextCommand(queryParts, options);
|
|
9454
|
+
});
|
|
6976
9455
|
kbCommand.command("reset").description("delete the local project knowledge base and cache").action(async () => {
|
|
6977
9456
|
const context = createContextFromOptions();
|
|
6978
9457
|
const result = await ui.progress("Resetting project knowledge base...", (report) => resetKnowledgeBase(context.workspaceRoot, { onProgress: (event) => report(event.message) }));
|
|
@@ -7019,6 +9498,31 @@ function createContextFromOptions() {
|
|
|
7019
9498
|
devFlags: options.dev
|
|
7020
9499
|
});
|
|
7021
9500
|
}
|
|
9501
|
+
async function executeKbSearchCommand(queryParts, options) {
|
|
9502
|
+
const context = createContextFromOptions();
|
|
9503
|
+
const query = queryParts.join(" ");
|
|
9504
|
+
const result = options.json ? await searchL1Knowledge(context.workspaceRoot, query, { limit: options.limit }) : await ui.spinner("Searching L1 knowledge entries...", () => searchL1Knowledge(context.workspaceRoot, query, { limit: options.limit }));
|
|
9505
|
+
if (options.json) {
|
|
9506
|
+
console.log(JSON.stringify(stripEmptyContainers(result), null, 2));
|
|
9507
|
+
return;
|
|
9508
|
+
}
|
|
9509
|
+
console.log(formatL1KnowledgeSearchResult(result).join("\n"));
|
|
9510
|
+
}
|
|
9511
|
+
async function executeKbContextCommand(queryParts, options) {
|
|
9512
|
+
const context = createContextFromOptions();
|
|
9513
|
+
const query = queryParts.join(" ");
|
|
9514
|
+
const contextPackOptions = {
|
|
9515
|
+
limit: options.limit,
|
|
9516
|
+
minScore: options.minScore,
|
|
9517
|
+
includeFullL1: options.fullL1
|
|
9518
|
+
};
|
|
9519
|
+
const result = options.json ? await createL1ContextPack(context.workspaceRoot, query, contextPackOptions) : await ui.spinner("Creating L1 context pack...", () => createL1ContextPack(context.workspaceRoot, query, contextPackOptions));
|
|
9520
|
+
if (options.json) {
|
|
9521
|
+
console.log(JSON.stringify(stripEmptyContainers(result), null, 2));
|
|
9522
|
+
return;
|
|
9523
|
+
}
|
|
9524
|
+
console.log(formatL1ContextPackResult(result).join("\n"));
|
|
9525
|
+
}
|
|
7022
9526
|
function formatStartupError(error) {
|
|
7023
9527
|
const message = error instanceof Error ? error.message : String(error);
|
|
7024
9528
|
if (message.includes("Could not read session metadata") || message.includes("Could not read session event") || message.includes("ENOENT")) return message.includes("metadata.json") || message.includes("events.jsonl") ? message : "Session not found";
|
|
@@ -7032,6 +9536,11 @@ function parsePositiveInteger(value) {
|
|
|
7032
9536
|
if (!Number.isInteger(parsed) || parsed <= 0) throw new Error("Expected a positive integer.");
|
|
7033
9537
|
return parsed;
|
|
7034
9538
|
}
|
|
9539
|
+
function parseNonNegativeNumber(value) {
|
|
9540
|
+
const parsed = Number(value);
|
|
9541
|
+
if (!Number.isFinite(parsed) || parsed < 0) throw new Error("Expected a non-negative number.");
|
|
9542
|
+
return parsed;
|
|
9543
|
+
}
|
|
7035
9544
|
function formatDryRunSyncStatus(status) {
|
|
7036
9545
|
if (status === "current") return ui.ok(status);
|
|
7037
9546
|
if (status === "invalid" || status === "missing_file") return ui.error(status);
|