topchester-ai 0.11.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 +1063 -143
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -382,6 +382,9 @@ const findFileTool = defineTool({
|
|
|
382
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.",
|
|
383
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\"}}",
|
|
384
384
|
argsSchema: findFileArgsSchema,
|
|
385
|
+
parallelSafe: true,
|
|
386
|
+
mutatesWorkspace: false,
|
|
387
|
+
resourceKeys: (args) => [`find:${args.path}`],
|
|
385
388
|
execute: (context, args) => findWorkspaceFilesByName(context.workspaceRoot, args, {
|
|
386
389
|
pathEnv: context.pathEnv,
|
|
387
390
|
logger: context.logger
|
|
@@ -918,6 +921,9 @@ const gitStatusTool = defineTool({
|
|
|
918
921
|
description: "Inspect structured Git branch and changed-file status inside the workspace.",
|
|
919
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}}",
|
|
920
923
|
argsSchema: gitStatusArgsSchema,
|
|
924
|
+
parallelSafe: true,
|
|
925
|
+
mutatesWorkspace: false,
|
|
926
|
+
resourceKeys: (args) => [`git-status:${args.path}`],
|
|
921
927
|
execute: (context, args) => inspectGitStatus(context, args)
|
|
922
928
|
});
|
|
923
929
|
const gitDiffTool = defineTool({
|
|
@@ -925,6 +931,9 @@ const gitDiffTool = defineTool({
|
|
|
925
931
|
description: "Inspect bounded Git diffs for staged, unstaged, and optionally untracked files.",
|
|
926
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}}",
|
|
927
933
|
argsSchema: gitDiffArgsSchema,
|
|
934
|
+
parallelSafe: true,
|
|
935
|
+
mutatesWorkspace: false,
|
|
936
|
+
resourceKeys: (args) => [`git-diff:${args.path ?? "."}:${args.scope}`],
|
|
928
937
|
execute: (context, args) => inspectGitDiff(context, args)
|
|
929
938
|
});
|
|
930
939
|
const gitLogTool = defineTool({
|
|
@@ -932,6 +941,9 @@ const gitLogTool = defineTool({
|
|
|
932
941
|
description: "Inspect recent Git commits as bounded structured summaries.",
|
|
933
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\"}}",
|
|
934
943
|
argsSchema: gitLogArgsSchema,
|
|
944
|
+
parallelSafe: true,
|
|
945
|
+
mutatesWorkspace: false,
|
|
946
|
+
resourceKeys: (args) => [`git-log:${args.path ?? "."}`],
|
|
935
947
|
execute: (context, args) => inspectGitLog(context, args)
|
|
936
948
|
});
|
|
937
949
|
const gitAddTool = defineTool({
|
|
@@ -1346,6 +1358,9 @@ const grepTool = defineTool({
|
|
|
1346
1358
|
pattern: z.string(),
|
|
1347
1359
|
path: z.string().optional()
|
|
1348
1360
|
}),
|
|
1361
|
+
parallelSafe: true,
|
|
1362
|
+
mutatesWorkspace: false,
|
|
1363
|
+
resourceKeys: (args) => [`grep:${args.path ?? "."}`],
|
|
1349
1364
|
execute: (context, args) => grepWorkspace(context.workspaceRoot, args, {
|
|
1350
1365
|
pathEnv: context.pathEnv,
|
|
1351
1366
|
logger: context.logger
|
|
@@ -2320,6 +2335,9 @@ const listFilesTool = defineTool({
|
|
|
2320
2335
|
recursive: z.boolean().optional().default(false),
|
|
2321
2336
|
limit: z.number().int().min(1).max(2e3).optional().default(500)
|
|
2322
2337
|
}),
|
|
2338
|
+
parallelSafe: true,
|
|
2339
|
+
mutatesWorkspace: false,
|
|
2340
|
+
resourceKeys: (args) => [`dir:${args.path}`],
|
|
2323
2341
|
execute: (context, args) => listWorkspaceFiles(context.workspaceRoot, args)
|
|
2324
2342
|
});
|
|
2325
2343
|
async function listWorkspaceFiles(workspaceRoot, args) {
|
|
@@ -2634,7 +2652,7 @@ function truncateText(text, width) {
|
|
|
2634
2652
|
const planTodoTool = defineTool({
|
|
2635
2653
|
name: "plan_todo",
|
|
2636
2654
|
description: "Replace the visible session task plan for multi-step work.",
|
|
2637
|
-
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. 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\"}]}}",
|
|
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\"}]}}",
|
|
2638
2656
|
argsSchema: planTodoArgsSchema,
|
|
2639
2657
|
async execute(context, args) {
|
|
2640
2658
|
if (!context.taskPlan) throw new Error("plan_todo requires runtime task-plan state.");
|
|
@@ -2653,6 +2671,9 @@ const readFileTool = defineTool({
|
|
|
2653
2671
|
description: "Read a UTF-8 file inside the workspace.",
|
|
2654
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\"}}",
|
|
2655
2673
|
argsSchema: z.object({ path: z.string() }),
|
|
2674
|
+
parallelSafe: true,
|
|
2675
|
+
mutatesWorkspace: false,
|
|
2676
|
+
resourceKeys: (args) => [`file:${args.path}`],
|
|
2656
2677
|
execute: (context, args) => readWorkspaceFile(context.workspaceRoot, args.path)
|
|
2657
2678
|
});
|
|
2658
2679
|
async function readWorkspaceFile(workspaceRoot, path) {
|
|
@@ -2669,6 +2690,42 @@ async function readWorkspaceFile(workspaceRoot, path) {
|
|
|
2669
2690
|
hash: `sha256:${createHash("sha256").update(bytes).digest("hex")}`
|
|
2670
2691
|
};
|
|
2671
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
|
+
});
|
|
2672
2729
|
const writeFileTool = defineTool({
|
|
2673
2730
|
name: "write_file",
|
|
2674
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.",
|
|
@@ -2881,6 +2938,7 @@ function isNodeError$2(error) {
|
|
|
2881
2938
|
//#endregion
|
|
2882
2939
|
//#region src/agent/tools/registry.ts
|
|
2883
2940
|
const toolRegistry = {
|
|
2941
|
+
[taskTool.name]: taskTool,
|
|
2884
2942
|
[planTodoTool.name]: planTodoTool,
|
|
2885
2943
|
[readFileTool.name]: readFileTool,
|
|
2886
2944
|
[listFilesTool.name]: listFilesTool,
|
|
@@ -2901,8 +2959,16 @@ function isToolName(name) {
|
|
|
2901
2959
|
function getToolDefinition(name) {
|
|
2902
2960
|
return toolRegistry[name];
|
|
2903
2961
|
}
|
|
2904
|
-
function getToolPromptLines() {
|
|
2905
|
-
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);
|
|
2906
2972
|
}
|
|
2907
2973
|
//#endregion
|
|
2908
2974
|
//#region src/agent/tools/xml-parser.ts
|
|
@@ -2991,7 +3057,7 @@ function parseToolCallWithSource(text, allowedSources = ["text-json", "text-xml"
|
|
|
2991
3057
|
if (allowedSources.includes("text-json")) {
|
|
2992
3058
|
const json = parseJsonToolCall(text);
|
|
2993
3059
|
if (json) return {
|
|
2994
|
-
|
|
3060
|
+
...json,
|
|
2995
3061
|
source: "text-json"
|
|
2996
3062
|
};
|
|
2997
3063
|
}
|
|
@@ -2999,7 +3065,8 @@ function parseToolCallWithSource(text, allowedSources = ["text-json", "text-xml"
|
|
|
2999
3065
|
const xml = parseXmlToolCall(text);
|
|
3000
3066
|
if (xml) return {
|
|
3001
3067
|
call: xml,
|
|
3002
|
-
source: "text-xml"
|
|
3068
|
+
source: "text-xml",
|
|
3069
|
+
remainder: ""
|
|
3003
3070
|
};
|
|
3004
3071
|
}
|
|
3005
3072
|
}
|
|
@@ -3014,12 +3081,16 @@ function parseNativeToolCall(toolName, args) {
|
|
|
3014
3081
|
};
|
|
3015
3082
|
}
|
|
3016
3083
|
function parseJsonToolCall(text) {
|
|
3017
|
-
const
|
|
3084
|
+
const { json, remainder } = extractToolJsonCandidate(stripJsonFence(text.trim()));
|
|
3018
3085
|
let value;
|
|
3019
3086
|
try {
|
|
3020
|
-
value = JSON.parse(
|
|
3087
|
+
value = JSON.parse(json);
|
|
3021
3088
|
} catch {
|
|
3022
|
-
|
|
3089
|
+
try {
|
|
3090
|
+
value = JSON.parse(escapeControlCharactersInJsonStrings(json));
|
|
3091
|
+
} catch {
|
|
3092
|
+
return;
|
|
3093
|
+
}
|
|
3023
3094
|
}
|
|
3024
3095
|
if (!isRecord$1(value) || typeof value.tool !== "string") return;
|
|
3025
3096
|
if (!isToolName(value.tool)) return;
|
|
@@ -3027,17 +3098,29 @@ function parseJsonToolCall(text) {
|
|
|
3027
3098
|
const parsed = definition.argsSchema.safeParse(value.args);
|
|
3028
3099
|
if (!parsed.success) return;
|
|
3029
3100
|
return {
|
|
3030
|
-
|
|
3031
|
-
|
|
3101
|
+
call: {
|
|
3102
|
+
tool: definition.name,
|
|
3103
|
+
args: parsed.data
|
|
3104
|
+
},
|
|
3105
|
+
remainder: remainder.trim()
|
|
3032
3106
|
};
|
|
3033
3107
|
}
|
|
3034
3108
|
function stripJsonFence(text) {
|
|
3035
3109
|
return text.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i)?.[1] ?? text;
|
|
3036
3110
|
}
|
|
3037
3111
|
function extractToolJsonCandidate(text) {
|
|
3038
|
-
if (!text.startsWith("{")) return
|
|
3112
|
+
if (!text.startsWith("{")) return {
|
|
3113
|
+
json: text,
|
|
3114
|
+
remainder: ""
|
|
3115
|
+
};
|
|
3039
3116
|
const endIndex = findJsonObjectEnd(text);
|
|
3040
|
-
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
|
+
};
|
|
3041
3124
|
}
|
|
3042
3125
|
function findJsonObjectEnd(text) {
|
|
3043
3126
|
let depth = 0;
|
|
@@ -3059,6 +3142,39 @@ function findJsonObjectEnd(text) {
|
|
|
3059
3142
|
}
|
|
3060
3143
|
}
|
|
3061
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
|
+
}
|
|
3062
3178
|
function isRecord$1(value) {
|
|
3063
3179
|
return typeof value === "object" && value !== null;
|
|
3064
3180
|
}
|
|
@@ -3112,6 +3228,7 @@ var ModelGateway = class ModelGateway {
|
|
|
3112
3228
|
model: resolved.model,
|
|
3113
3229
|
system: request.system,
|
|
3114
3230
|
prompt: request.prompt,
|
|
3231
|
+
providerOptions: buildProviderOptions(resolved.providerId, resolved.providerConfig),
|
|
3115
3232
|
abortSignal: request.abortSignal
|
|
3116
3233
|
});
|
|
3117
3234
|
const usage = normalizeUsage(result.usage, {
|
|
@@ -3131,9 +3248,17 @@ var ModelGateway = class ModelGateway {
|
|
|
3131
3248
|
const resolved = this.resolveModel(request.purpose ?? "agent.primary");
|
|
3132
3249
|
const override = request.toolProtocol ?? resolved.modelConfig.toolProtocol ?? resolved.providerConfig.toolProtocol ?? "auto";
|
|
3133
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
|
+
}
|
|
3134
3259
|
if (override === "native" || override === "auto") try {
|
|
3135
3260
|
const result = await this.generateNativeAgentStep(request, resolved, attempts);
|
|
3136
|
-
if (result.toolCalls.length > 0
|
|
3261
|
+
if (result.toolCalls.length > 0) return result;
|
|
3137
3262
|
const parsedTextCall = parseToolCallWithSource(result.text);
|
|
3138
3263
|
if (parsedTextCall) {
|
|
3139
3264
|
const fallbackProtocol = parsedTextCall.source === "text-xml" ? "text-xml" : "text-json";
|
|
@@ -3174,17 +3299,19 @@ var ModelGateway = class ModelGateway {
|
|
|
3174
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"]);
|
|
3175
3300
|
}
|
|
3176
3301
|
async *streamText(request) {
|
|
3302
|
+
const resolved = this.resolveModel(request.purpose);
|
|
3177
3303
|
yield* streamText({
|
|
3178
|
-
model:
|
|
3304
|
+
model: resolved.model,
|
|
3179
3305
|
system: request.system,
|
|
3180
3306
|
prompt: request.prompt,
|
|
3307
|
+
providerOptions: buildProviderOptions(resolved.providerId, resolved.providerConfig),
|
|
3181
3308
|
abortSignal: request.abortSignal
|
|
3182
3309
|
}).textStream;
|
|
3183
3310
|
}
|
|
3184
3311
|
async generateNativeAgentStep(request, resolved, attempts) {
|
|
3185
3312
|
const providerOptions = buildNativeProviderOptions(resolved.providerId, resolved.providerConfig);
|
|
3186
3313
|
const openRouterRoutingApplied = hasOpenRouterRoutingOptions(providerOptions, resolved.providerId);
|
|
3187
|
-
const result = await generateText({
|
|
3314
|
+
const result = request.onReasoning ? await this.streamNativeAgentStep(request, resolved, providerOptions) : await generateText({
|
|
3188
3315
|
model: resolved.model,
|
|
3189
3316
|
system: request.system,
|
|
3190
3317
|
prompt: request.prompt,
|
|
@@ -3227,10 +3354,11 @@ var ModelGateway = class ModelGateway {
|
|
|
3227
3354
|
};
|
|
3228
3355
|
}
|
|
3229
3356
|
async generateTextAgentStep(request, resolved, attempts, fallbackReason, providerRejectedTools, allowedSources) {
|
|
3230
|
-
const result = await generateText({
|
|
3357
|
+
const result = request.onReasoning ? await this.streamTextAgentStep(request, resolved) : await generateText({
|
|
3231
3358
|
model: resolved.model,
|
|
3232
3359
|
system: request.system,
|
|
3233
3360
|
prompt: request.prompt,
|
|
3361
|
+
providerOptions: buildProviderOptions(resolved.providerId, resolved.providerConfig),
|
|
3234
3362
|
abortSignal: request.abortSignal
|
|
3235
3363
|
});
|
|
3236
3364
|
const usage = normalizeUsage(result.usage, {
|
|
@@ -3266,25 +3394,178 @@ var ModelGateway = class ModelGateway {
|
|
|
3266
3394
|
openRouterRoutingApplied: false
|
|
3267
3395
|
};
|
|
3268
3396
|
}
|
|
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
|
+
}
|
|
3269
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
|
+
}
|
|
3270
3540
|
function resolveApiKey(config) {
|
|
3271
3541
|
if (config.apiKey !== void 0) return config.apiKey;
|
|
3272
3542
|
if (config.apiKeyEnv === void 0) return;
|
|
3273
3543
|
return process.env[config.apiKeyEnv];
|
|
3274
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
|
+
}
|
|
3275
3550
|
function buildNativeProviderOptions(providerId, config) {
|
|
3276
|
-
const options = {
|
|
3551
|
+
const options = {
|
|
3552
|
+
...buildProviderOptions(providerId, config)[providerId],
|
|
3553
|
+
parallel_tool_calls: false
|
|
3554
|
+
};
|
|
3277
3555
|
if (shouldApplyOpenRouterRoutingOptions(providerId, config)) options.provider = { require_parameters: true };
|
|
3278
3556
|
return { [providerId]: options };
|
|
3279
3557
|
}
|
|
3280
3558
|
function shouldApplyOpenRouterRoutingOptions(providerId, config) {
|
|
3281
3559
|
if (config.openRouterToolRouting === "force") return true;
|
|
3282
3560
|
if (config.openRouterToolRouting === "off") return false;
|
|
3283
|
-
return isOpenRouterProvider(providerId, config);
|
|
3561
|
+
return isOpenRouterProvider$1(providerId, config);
|
|
3284
3562
|
}
|
|
3285
|
-
function isOpenRouterProvider(providerId, config) {
|
|
3563
|
+
function isOpenRouterProvider$1(providerId, config) {
|
|
3286
3564
|
return providerId.toLowerCase().includes("openrouter") || config.baseURL.toLowerCase().includes("openrouter.ai");
|
|
3287
3565
|
}
|
|
3566
|
+
function shouldUseTextProtocolForOpenRouterStreaming(request, resolved) {
|
|
3567
|
+
return request.onReasoning !== void 0 && isOpenRouterProvider$1(resolved.providerId, resolved.providerConfig);
|
|
3568
|
+
}
|
|
3288
3569
|
function hasOpenRouterRoutingOptions(providerOptions, providerId) {
|
|
3289
3570
|
const options = providerOptions[providerId];
|
|
3290
3571
|
return Boolean(options && typeof options === "object" && "provider" in options);
|
|
@@ -3298,7 +3579,7 @@ function extractWarningMessages(warnings) {
|
|
|
3298
3579
|
return warnings.map((warning) => formatErrorMessage$2(warning));
|
|
3299
3580
|
}
|
|
3300
3581
|
function normalizeUsage(usage, context) {
|
|
3301
|
-
const costUsd = context && isOpenRouterProvider(context.providerId, context.providerConfig) ? extractOpenRouterCost(context.responseBody) : void 0;
|
|
3582
|
+
const costUsd = context && isOpenRouterProvider$1(context.providerId, context.providerConfig) ? extractOpenRouterCost(context.responseBody) : void 0;
|
|
3302
3583
|
if (!usage) return costUsd === void 0 ? void 0 : { costUsd };
|
|
3303
3584
|
const normalized = {
|
|
3304
3585
|
...typeof usage.inputTokens === "number" ? { inputTokens: usage.inputTokens } : {},
|
|
@@ -3334,6 +3615,10 @@ const toolProtocolSchema = z.enum([
|
|
|
3334
3615
|
"text-json",
|
|
3335
3616
|
"text-xml"
|
|
3336
3617
|
]);
|
|
3618
|
+
const openRouterAttributionHeaders = {
|
|
3619
|
+
"HTTP-Referer": "https://topchester.com",
|
|
3620
|
+
"X-Title": "Topchester"
|
|
3621
|
+
};
|
|
3337
3622
|
const providerSchema = z.object({
|
|
3338
3623
|
type: z.literal("openai-compatible"),
|
|
3339
3624
|
baseURL: z.string().url(),
|
|
@@ -3341,6 +3626,7 @@ const providerSchema = z.object({
|
|
|
3341
3626
|
apiKey: z.string().optional(),
|
|
3342
3627
|
headers: z.record(z.string(), z.string()).optional(),
|
|
3343
3628
|
supportsStructuredOutputs: z.boolean().optional(),
|
|
3629
|
+
service_tier: z.enum(["flex", "priority"]).optional(),
|
|
3344
3630
|
toolProtocol: toolProtocolSchema.optional(),
|
|
3345
3631
|
openRouterToolRouting: z.enum([
|
|
3346
3632
|
"auto",
|
|
@@ -3506,10 +3792,7 @@ function ensureKnownProvider(providers, provider) {
|
|
|
3506
3792
|
baseURL: "https://openrouter.ai/api/v1",
|
|
3507
3793
|
apiKeyEnv: "OPENROUTER_API_KEY",
|
|
3508
3794
|
supportsStructuredOutputs: true,
|
|
3509
|
-
headers: {
|
|
3510
|
-
"HTTP-Referer": "https://topchester.com",
|
|
3511
|
-
"X-Title": "Topchester"
|
|
3512
|
-
}
|
|
3795
|
+
headers: { ...openRouterAttributionHeaders }
|
|
3513
3796
|
};
|
|
3514
3797
|
}
|
|
3515
3798
|
function applyKnownProviderDefaults(providers) {
|
|
@@ -3519,8 +3802,15 @@ function applyKnownProviderDefaults(providers) {
|
|
|
3519
3802
|
provider.supportsStructuredOutputs ??= true;
|
|
3520
3803
|
provider.toolProtocol ??= "native";
|
|
3521
3804
|
}
|
|
3805
|
+
if (isOpenRouterProvider(providerId, provider.baseURL)) provider.headers = {
|
|
3806
|
+
...openRouterAttributionHeaders,
|
|
3807
|
+
...isPlainObject(provider.headers) ? provider.headers : {}
|
|
3808
|
+
};
|
|
3522
3809
|
}
|
|
3523
3810
|
}
|
|
3811
|
+
function isOpenRouterProvider(providerId, baseURL) {
|
|
3812
|
+
return providerId.toLowerCase().includes("openrouter") || baseURL.toLowerCase().includes("openrouter.ai");
|
|
3813
|
+
}
|
|
3524
3814
|
function isOpenAIProvider(providerId, baseURL) {
|
|
3525
3815
|
const normalizedProvider = providerId.toLowerCase();
|
|
3526
3816
|
const normalizedBaseURL = baseURL.toLowerCase();
|
|
@@ -5519,6 +5809,12 @@ function agentMessage(text, meta) {
|
|
|
5519
5809
|
meta
|
|
5520
5810
|
};
|
|
5521
5811
|
}
|
|
5812
|
+
function thinkingMessage(text) {
|
|
5813
|
+
return {
|
|
5814
|
+
kind: "thinking",
|
|
5815
|
+
text
|
|
5816
|
+
};
|
|
5817
|
+
}
|
|
5522
5818
|
function toolCallMessage(call, label, resultSummary) {
|
|
5523
5819
|
return resultSummary === void 0 ? {
|
|
5524
5820
|
kind: "tool_call",
|
|
@@ -5531,6 +5827,12 @@ function toolCallMessage(call, label, resultSummary) {
|
|
|
5531
5827
|
resultSummary
|
|
5532
5828
|
};
|
|
5533
5829
|
}
|
|
5830
|
+
function subagentMessage(message) {
|
|
5831
|
+
return {
|
|
5832
|
+
kind: "subagent",
|
|
5833
|
+
...message
|
|
5834
|
+
};
|
|
5835
|
+
}
|
|
5534
5836
|
function modalMessage(message) {
|
|
5535
5837
|
return {
|
|
5536
5838
|
kind: "modal",
|
|
@@ -5540,6 +5842,8 @@ function modalMessage(message) {
|
|
|
5540
5842
|
function renderChatMessage(message, options = {}) {
|
|
5541
5843
|
if (message.kind === "modal") return renderChatModal(message, options.selectedActionIndex);
|
|
5542
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));
|
|
5543
5847
|
if (message.text.length === 0) return [""];
|
|
5544
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");
|
|
5545
5849
|
if (message.kind === "user") return renderUserMessage(lines);
|
|
@@ -5572,6 +5876,18 @@ function renderToolCallMessage(message) {
|
|
|
5572
5876
|
const visibleLabel = message.resultSummary && !message.label.includes(message.resultSummary) ? `${message.label} ${message.resultSummary}` : message.label;
|
|
5573
5877
|
return [` ${ui.muted(expandTabs(visibleLabel))}`];
|
|
5574
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
|
+
}
|
|
5575
5891
|
function expandTabs(line) {
|
|
5576
5892
|
let column = 0;
|
|
5577
5893
|
let expanded = "";
|
|
@@ -5640,11 +5956,21 @@ const jsonValueSchema = z.lazy(() => z.union([
|
|
|
5640
5956
|
const sessionMetadataSchema = z.object({
|
|
5641
5957
|
version: z.literal(1),
|
|
5642
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(),
|
|
5643
5965
|
workspaceRoot: z.string().min(1),
|
|
5644
5966
|
createdAt: isoTimestampSchema,
|
|
5645
5967
|
updatedAt: isoTimestampSchema,
|
|
5646
5968
|
lastEventId: z.number().int().min(0)
|
|
5647
|
-
})
|
|
5969
|
+
}).transform((metadata) => ({
|
|
5970
|
+
...metadata,
|
|
5971
|
+
rootSessionId: metadata.rootSessionId ?? metadata.sessionId,
|
|
5972
|
+
source: metadata.source ?? "user"
|
|
5973
|
+
}));
|
|
5648
5974
|
const eventEnvelopeSchema = z.object({
|
|
5649
5975
|
version: z.literal(1),
|
|
5650
5976
|
id: z.number().int().positive(),
|
|
@@ -5696,15 +6022,74 @@ const choicePayloadSchema = z.object({
|
|
|
5696
6022
|
value: z.string().optional()
|
|
5697
6023
|
}))
|
|
5698
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
|
+
});
|
|
5699
6047
|
const sessionEventPayloadSchema = z.discriminatedUnion("kind", [
|
|
5700
6048
|
messagePayloadSchema,
|
|
5701
6049
|
toolCallPayloadSchema,
|
|
5702
6050
|
taskPlanPayloadSchema,
|
|
5703
6051
|
statusPayloadSchema,
|
|
5704
6052
|
knowledgeStatusPayloadSchema,
|
|
5705
|
-
choicePayloadSchema
|
|
6053
|
+
choicePayloadSchema,
|
|
6054
|
+
subagentStartedPayloadSchema,
|
|
6055
|
+
subagentEventPayloadSchema,
|
|
6056
|
+
subagentCompletedPayloadSchema,
|
|
6057
|
+
subagentFailedPayloadSchema
|
|
5706
6058
|
]);
|
|
5707
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
|
+
};
|
|
5708
6093
|
//#endregion
|
|
5709
6094
|
//#region src/session/store.ts
|
|
5710
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;
|
|
@@ -5720,6 +6105,8 @@ async function createSession(workspaceRoot) {
|
|
|
5720
6105
|
const metadata = {
|
|
5721
6106
|
version: 1,
|
|
5722
6107
|
sessionId,
|
|
6108
|
+
rootSessionId: sessionId,
|
|
6109
|
+
source: "user",
|
|
5723
6110
|
workspaceRoot,
|
|
5724
6111
|
createdAt,
|
|
5725
6112
|
updatedAt: createdAt,
|
|
@@ -5730,6 +6117,41 @@ async function createSession(workspaceRoot) {
|
|
|
5730
6117
|
await writeFile(eventsPath, "", { flag: "wx" });
|
|
5731
6118
|
return buildHandle(sessionDir, metadata);
|
|
5732
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
|
+
}
|
|
5733
6155
|
async function loadSessionForAppend(workspaceRoot, sessionId) {
|
|
5734
6156
|
const loaded = await loadSession(workspaceRoot, sessionId);
|
|
5735
6157
|
return buildHandle(loaded.sessionDir, loaded.metadata);
|
|
@@ -5803,6 +6225,10 @@ function rehydrateSession(events) {
|
|
|
5803
6225
|
};
|
|
5804
6226
|
break;
|
|
5805
6227
|
case "knowledge_status": break;
|
|
6228
|
+
case "subagent_started":
|
|
6229
|
+
case "subagent_event":
|
|
6230
|
+
case "subagent_completed":
|
|
6231
|
+
case "subagent_failed": break;
|
|
5806
6232
|
case "choice":
|
|
5807
6233
|
messages.push({
|
|
5808
6234
|
kind: "modal",
|
|
@@ -6101,19 +6527,25 @@ var BusyIndicator = class {
|
|
|
6101
6527
|
this.tui.requestRender();
|
|
6102
6528
|
}, 80);
|
|
6103
6529
|
}
|
|
6104
|
-
stop() {
|
|
6530
|
+
stop(options = {}) {
|
|
6105
6531
|
if (this.timer) {
|
|
6106
6532
|
clearInterval(this.timer);
|
|
6107
6533
|
this.timer = void 0;
|
|
6108
6534
|
}
|
|
6109
6535
|
this.app.setPromptHint(void 0);
|
|
6110
|
-
this.app.setEphemeralLine(void 0);
|
|
6536
|
+
if (options.clearEphemeralLine ?? true) this.app.setEphemeralLine(void 0);
|
|
6111
6537
|
}
|
|
6112
6538
|
setActivity(activity) {
|
|
6113
6539
|
this.activityOverride = activity;
|
|
6114
6540
|
this.render();
|
|
6115
6541
|
this.tui.requestRender();
|
|
6116
6542
|
}
|
|
6543
|
+
clearActivity() {
|
|
6544
|
+
if (!this.activityOverride) return;
|
|
6545
|
+
this.activityOverride = void 0;
|
|
6546
|
+
this.render();
|
|
6547
|
+
this.tui.requestRender();
|
|
6548
|
+
}
|
|
6117
6549
|
render() {
|
|
6118
6550
|
if (this.activityOverride) {
|
|
6119
6551
|
this.app.setEphemeralLine(`${this.frames[this.index]} ${this.activityOverride}`);
|
|
@@ -6124,6 +6556,33 @@ var BusyIndicator = class {
|
|
|
6124
6556
|
this.app.setEphemeralLine(`${this.frames[this.index]} ${this.options.activities[activityIndex]}`);
|
|
6125
6557
|
}
|
|
6126
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
|
+
}
|
|
6127
6586
|
//#endregion
|
|
6128
6587
|
//#region src/agent/commands.ts
|
|
6129
6588
|
const slashCommandSuggestions = [
|
|
@@ -6254,6 +6713,31 @@ const agentEvent = {
|
|
|
6254
6713
|
type: "choice",
|
|
6255
6714
|
...options
|
|
6256
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
|
+
};
|
|
6257
6741
|
}
|
|
6258
6742
|
};
|
|
6259
6743
|
function choiceAction(label, value) {
|
|
@@ -6579,7 +7063,9 @@ var ChatLayout = class {
|
|
|
6579
7063
|
text: message.text
|
|
6580
7064
|
}];
|
|
6581
7065
|
case "system":
|
|
7066
|
+
case "thinking":
|
|
6582
7067
|
case "tool_call":
|
|
7068
|
+
case "subagent":
|
|
6583
7069
|
case "modal": return [];
|
|
6584
7070
|
}
|
|
6585
7071
|
});
|
|
@@ -7100,6 +7586,73 @@ function isAbortError(error) {
|
|
|
7100
7586
|
return error.name === "AbortError" || error.message.toLowerCase().includes("aborted");
|
|
7101
7587
|
}
|
|
7102
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
|
|
7103
7656
|
//#region src/agent/tools/executor.ts
|
|
7104
7657
|
async function executeToolCall(workspaceRoot, call, options = {}) {
|
|
7105
7658
|
const startedAt = Date.now();
|
|
@@ -7107,9 +7660,17 @@ async function executeToolCall(workspaceRoot, call, options = {}) {
|
|
|
7107
7660
|
workspaceRoot,
|
|
7108
7661
|
pathEnv: options.pathEnv,
|
|
7109
7662
|
logger: options.logger,
|
|
7110
|
-
taskPlan: options.taskPlan
|
|
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
|
|
7111
7670
|
};
|
|
7112
7671
|
try {
|
|
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}".`);
|
|
7113
7674
|
const definition = getToolDefinition(call.tool);
|
|
7114
7675
|
const parsedCall = {
|
|
7115
7676
|
...call,
|
|
@@ -7263,11 +7824,17 @@ function formatErrorMessage(error) {
|
|
|
7263
7824
|
}
|
|
7264
7825
|
//#endregion
|
|
7265
7826
|
//#region src/agent/prompts.ts
|
|
7266
|
-
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();
|
|
7267
7831
|
return [
|
|
7268
7832
|
"You are Topchester, a plain-spoken terminal coding agent for software engineering work.",
|
|
7269
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.",
|
|
7270
7834
|
"",
|
|
7835
|
+
`Agent profile: ${profile.displayName} (${profile.id}).`,
|
|
7836
|
+
...profile.promptAdditions,
|
|
7837
|
+
"",
|
|
7271
7838
|
"Working style:",
|
|
7272
7839
|
"- Start by understanding the user's intent and the surrounding code before proposing or changing anything non-trivial.",
|
|
7273
7840
|
"- Prefer local project evidence over assumptions. Use search and read tools to find relevant files, examples, tests, commands, and conventions.",
|
|
@@ -7280,41 +7847,183 @@ function getChatSystemPrompt() {
|
|
|
7280
7847
|
"- Ask a clarifying question only when the missing information blocks useful progress or the safe interpretation is genuinely unclear.",
|
|
7281
7848
|
"",
|
|
7282
7849
|
"You have these tools available:",
|
|
7283
|
-
...
|
|
7850
|
+
...toolPromptLines,
|
|
7284
7851
|
"",
|
|
7285
7852
|
"Tool use:",
|
|
7286
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.",
|
|
7287
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.",
|
|
7288
7855
|
"- Do not claim to have read, created, edited, staged, committed, or run anything unless a tool result in this turn confirms it.",
|
|
7289
|
-
"
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
"- Use
|
|
7296
|
-
"- Use
|
|
7297
|
-
"
|
|
7298
|
-
"
|
|
7299
|
-
"- Use
|
|
7300
|
-
"-
|
|
7301
|
-
"- Use
|
|
7302
|
-
"
|
|
7303
|
-
"-
|
|
7304
|
-
"
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
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."] : [],
|
|
7309
7885
|
"- After each tool result, decide the next useful action from the new evidence. Continue until the request is handled or blocked.",
|
|
7310
7886
|
"Do not make up file contents or search results."
|
|
7311
7887
|
].join("\n");
|
|
7312
7888
|
}
|
|
7313
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
|
|
7314
8021
|
//#region src/agent/runtime.ts
|
|
7315
8022
|
const MAX_TOOL_CALLS_PER_TURN = 75;
|
|
7316
|
-
|
|
8023
|
+
const DEFAULT_TASK_CONCURRENCY = 3;
|
|
8024
|
+
var TopchesterAgentRuntime = class TopchesterAgentRuntime {
|
|
7317
8025
|
context;
|
|
8026
|
+
options;
|
|
7318
8027
|
taskPlan = createTaskPlanController();
|
|
7319
8028
|
/**
|
|
7320
8029
|
* Holds the shared application context for one runtime instance.
|
|
@@ -7322,8 +8031,9 @@ var TopchesterAgentRuntime = class {
|
|
|
7322
8031
|
* workspace, model gateway, logger, config, and task-plan state that
|
|
7323
8032
|
* are passed in by the CLI or TUI layer.
|
|
7324
8033
|
*/
|
|
7325
|
-
constructor(context) {
|
|
8034
|
+
constructor(context, options = {}) {
|
|
7326
8035
|
this.context = context;
|
|
8036
|
+
this.options = options;
|
|
7327
8037
|
}
|
|
7328
8038
|
/**
|
|
7329
8039
|
* Performs the lightweight startup model check used by the interactive
|
|
@@ -7348,34 +8058,46 @@ var TopchesterAgentRuntime = class {
|
|
|
7348
8058
|
return getKnowledgeStatusEvents(await this.getKnowledgeStatusWithNonCleanFileCount());
|
|
7349
8059
|
}
|
|
7350
8060
|
/**
|
|
7351
|
-
*
|
|
8061
|
+
* Streams one user chat turn through the agent loop. It builds the model
|
|
7352
8062
|
* prompt with relevant KB context, calls the model, executes any requested
|
|
7353
8063
|
* tools, feeds tool results back into the next prompt, and repeats until
|
|
7354
8064
|
* the model returns a normal assistant message or the loop hits its safety
|
|
7355
8065
|
* limit.
|
|
7356
8066
|
*
|
|
7357
|
-
*
|
|
7358
|
-
*
|
|
7359
|
-
*
|
|
7360
|
-
* closure before a final answer when the model leaves an open plan.
|
|
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.
|
|
7361
8070
|
*/
|
|
7362
|
-
async
|
|
7363
|
-
|
|
7364
|
-
const events = [];
|
|
7365
|
-
const emit = async (...nextEvents) => {
|
|
7366
|
-
events.push(...nextEvents);
|
|
7367
|
-
if (!onEvent) return;
|
|
7368
|
-
for (const event of nextEvents) await onEvent(event);
|
|
7369
|
-
};
|
|
7370
|
-
let nextPrompt = prompt;
|
|
8071
|
+
async *submitMessageStream(conversation, message, abortSignal, options = {}) {
|
|
8072
|
+
let nextPrompt = await this.buildPromptWithKnowledgeContext(buildConversationPrompt(conversation, message), message);
|
|
7371
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
|
+
});
|
|
7372
8091
|
let lastModelId = "model";
|
|
7373
8092
|
let afterTool;
|
|
7374
8093
|
let toolProtocolOverride = readToolProtocolEnvOverride();
|
|
7375
8094
|
let requestedPlanClosure = false;
|
|
7376
8095
|
for (let toolCalls = 0; toolCalls <= MAX_TOOL_CALLS_PER_TURN; toolCalls += 1) {
|
|
7377
8096
|
const startedAt = Date.now();
|
|
7378
|
-
const system = getChatSystemPrompt(
|
|
8097
|
+
const system = getChatSystemPrompt({
|
|
8098
|
+
profile,
|
|
8099
|
+
permissions
|
|
8100
|
+
});
|
|
7379
8101
|
this.context.logger.debug({
|
|
7380
8102
|
event: "model_prompt",
|
|
7381
8103
|
purpose: "agent.primary",
|
|
@@ -7391,12 +8113,15 @@ var TopchesterAgentRuntime = class {
|
|
|
7391
8113
|
system,
|
|
7392
8114
|
prompt: nextPrompt,
|
|
7393
8115
|
abortSignal,
|
|
7394
|
-
toolProtocol: toolProtocolOverride
|
|
8116
|
+
toolProtocol: toolProtocolOverride,
|
|
8117
|
+
onReasoning: options.onReasoning,
|
|
8118
|
+
tools
|
|
7395
8119
|
});
|
|
7396
8120
|
const durationMs = Date.now() - startedAt;
|
|
7397
8121
|
const toolCall = result.toolCalls[0];
|
|
7398
8122
|
totalDurationMs += durationMs;
|
|
7399
8123
|
lastModelId = result.modelId;
|
|
8124
|
+
addTokenUsageTotals(tokenUsageTotals, result.usage);
|
|
7400
8125
|
this.context.logger.debug({
|
|
7401
8126
|
event: "model_response",
|
|
7402
8127
|
purpose: "agent.primary",
|
|
@@ -7429,37 +8154,112 @@ var TopchesterAgentRuntime = class {
|
|
|
7429
8154
|
if (result.providerRejectedTools && result.toolProtocol === "text-json") toolProtocolOverride = "text-json";
|
|
7430
8155
|
else if (result.providerRejectedTools && result.toolProtocol === "text-xml") toolProtocolOverride = "text-xml";
|
|
7431
8156
|
if (!toolCall) {
|
|
7432
|
-
|
|
8157
|
+
const plan = this.taskPlan.get();
|
|
8158
|
+
const finalText = stripSuppressiblePlanTodoPrefix(result.text, plan) ?? result.text;
|
|
8159
|
+
if (hasOpenTaskPlan(plan)) {
|
|
7433
8160
|
if (!requestedPlanClosure) {
|
|
7434
8161
|
requestedPlanClosure = true;
|
|
7435
|
-
nextPrompt = `${nextPrompt}\n\n${formatOpenPlanClosureInstruction(
|
|
8162
|
+
nextPrompt = `${nextPrompt}\n\n${formatOpenPlanClosureInstruction(finalText, result.toolProtocol)}`;
|
|
7436
8163
|
continue;
|
|
7437
8164
|
}
|
|
7438
|
-
|
|
8165
|
+
yield agentEvent.taskPlan(this.taskPlan.update({ items: [] }));
|
|
7439
8166
|
}
|
|
7440
|
-
|
|
7441
|
-
|
|
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;
|
|
7442
8170
|
}
|
|
7443
8171
|
if (toolCalls === MAX_TOOL_CALLS_PER_TURN) {
|
|
7444
|
-
|
|
8172
|
+
yield agentEvent.choice({
|
|
7445
8173
|
tone: "warning",
|
|
7446
8174
|
title: "Tool call limit reached",
|
|
7447
8175
|
body: `Stopped after ${MAX_TOOL_CALLS_PER_TURN} tool calls in one turn. Continue starts another turn; abort leaves the call stopped.`,
|
|
7448
8176
|
actions: [choiceAction("Continue", "Continue the previous task from where you stopped."), choiceAction("Abort", ABORT_CHOICE_VALUE)]
|
|
7449
|
-
})
|
|
7450
|
-
|
|
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;
|
|
7451
8222
|
}
|
|
7452
8223
|
const executableToolCall = toolCall;
|
|
7453
|
-
const
|
|
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, {
|
|
7454
8232
|
logger: this.context.logger,
|
|
7455
|
-
taskPlan: this.taskPlan
|
|
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();
|
|
7456
8242
|
});
|
|
7457
|
-
await
|
|
7458
|
-
|
|
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);
|
|
7459
8247
|
afterTool = executableToolCall.tool;
|
|
7460
|
-
nextPrompt = `${nextPrompt}\n\n${formatToolResultForPrompt(toolResult)}\n\n${formatContinuationInstruction(result.toolProtocol, toolResult)}`;
|
|
8248
|
+
nextPrompt = `${nextPrompt}\n\n${formatToolResultForPrompt(toolResult)}\n\n${formatContinuationInstruction(result.toolProtocol, toolResult, isToolAllowed(permissions, "plan_todo"))}`;
|
|
8249
|
+
}
|
|
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);
|
|
7461
8262
|
}
|
|
7462
|
-
await emit(agentEvent.assistantMessage("I stopped because the tool loop ended unexpectedly.", formatAgentMessageMeta(lastModelId, totalDurationMs)), agentEvent.status("ready"));
|
|
7463
8263
|
return events;
|
|
7464
8264
|
}
|
|
7465
8265
|
/**
|
|
@@ -7503,6 +8303,13 @@ var TopchesterAgentRuntime = class {
|
|
|
7503
8303
|
* user's chat turn from reaching the model.
|
|
7504
8304
|
*/
|
|
7505
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
|
+
}
|
|
7506
8313
|
const status = getKnowledgeStatus(this.context.workspaceRoot);
|
|
7507
8314
|
if (!status.kbExists || !status.kbIsDirectory || status.kbContentState !== "ready") return prompt;
|
|
7508
8315
|
try {
|
|
@@ -7533,6 +8340,35 @@ var TopchesterAgentRuntime = class {
|
|
|
7533
8340
|
}
|
|
7534
8341
|
}
|
|
7535
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
|
+
}
|
|
7536
8372
|
/**
|
|
7537
8373
|
* Calls the configured model gateway for a single agent step and normalizes
|
|
7538
8374
|
* the result into the newer `ModelAgentResult` shape. Gateways that implement
|
|
@@ -7541,10 +8377,7 @@ var TopchesterAgentRuntime = class {
|
|
|
7541
8377
|
* so the rest of the runtime can use the same tool loop.
|
|
7542
8378
|
*/
|
|
7543
8379
|
async function generateAgentStep(context, request) {
|
|
7544
|
-
if ("generateAgentStep" in context.modelGateway && typeof context.modelGateway.generateAgentStep === "function") return context.modelGateway.generateAgentStep({
|
|
7545
|
-
...request,
|
|
7546
|
-
tools: Object.values(toolRegistry)
|
|
7547
|
-
});
|
|
8380
|
+
if ("generateAgentStep" in context.modelGateway && typeof context.modelGateway.generateAgentStep === "function") return context.modelGateway.generateAgentStep({ ...request });
|
|
7548
8381
|
const result = await context.modelGateway.generateText(request);
|
|
7549
8382
|
const parsed = parseToolCallWithSource(result.text);
|
|
7550
8383
|
const toolProtocol = parsed?.source === "text-xml" ? "text-xml" : "text-json";
|
|
@@ -7568,6 +8401,21 @@ async function generateAgentStep(context, request) {
|
|
|
7568
8401
|
openRouterRoutingApplied: false
|
|
7569
8402
|
};
|
|
7570
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
|
+
}
|
|
7571
8419
|
/**
|
|
7572
8420
|
* Reads the optional environment override for the tool-calling protocol.
|
|
7573
8421
|
* Invalid values are ignored instead of failing startup, which keeps local
|
|
@@ -7578,6 +8426,14 @@ function readToolProtocolEnvOverride() {
|
|
|
7578
8426
|
const value = process.env.TOPCHESTER_TOOL_PROTOCOL;
|
|
7579
8427
|
if (value === "auto" || value === "native" || value === "text-json" || value === "text-xml") return value;
|
|
7580
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
|
+
}
|
|
7581
8437
|
/**
|
|
7582
8438
|
* Applies TUI styling to per-file KB sync states. The raw scanner statuses
|
|
7583
8439
|
* are preserved as text, but success, warning, and error categories get
|
|
@@ -7651,6 +8507,7 @@ function formatToolResultForPrompt(result) {
|
|
|
7651
8507
|
"```"
|
|
7652
8508
|
].join("\n");
|
|
7653
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");
|
|
7654
8511
|
if (result.tool === "edit_file") return [
|
|
7655
8512
|
`Tool result from ${result.tool}${path}:`,
|
|
7656
8513
|
`before_hash: ${result.beforeHash}`,
|
|
@@ -7754,13 +8611,13 @@ function formatToolResultForPrompt(result) {
|
|
|
7754
8611
|
* restates the current tool-call protocol so the next model step remains
|
|
7755
8612
|
* parseable by the runtime.
|
|
7756
8613
|
*/
|
|
7757
|
-
function formatContinuationInstruction(protocol, result) {
|
|
8614
|
+
function formatContinuationInstruction(protocol, result, canUsePlanTodo = true) {
|
|
7758
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.";
|
|
7759
8616
|
return [
|
|
7760
8617
|
"Continue the user's request using the tool result above and the visible plan when one is active.",
|
|
7761
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." : "",
|
|
7762
|
-
"Update plan_todo after major progress changes.",
|
|
7763
|
-
"Before a final answer, close the visible plan by calling plan_todo with all finished items marked completed, or with [] if abandoning the plan.",
|
|
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." : "",
|
|
7764
8621
|
toolInstruction,
|
|
7765
8622
|
"Otherwise answer the user. Do not guess."
|
|
7766
8623
|
].filter(Boolean).join(" ");
|
|
@@ -7790,6 +8647,7 @@ function formatOpenPlanClosureInstruction(draftAnswer, protocol) {
|
|
|
7790
8647
|
function formatToolCallMessage(call, result) {
|
|
7791
8648
|
if (result && isToolErrorResult(result)) return `${call.tool} failed: ${result.error}`;
|
|
7792
8649
|
switch (call.tool) {
|
|
8650
|
+
case "task": return result?.tool === "task" ? `task: ${result.status} ${result.childSessionId}` : `task: ${call.args.description}`;
|
|
7793
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`;
|
|
7794
8652
|
case "read_file": return `read_file: ${call.args.path}`;
|
|
7795
8653
|
case "list_files": return `list_files: ${call.args.path}${call.args.recursive ? " (recursive)" : ""}`;
|
|
@@ -7838,8 +8696,22 @@ function formatWriteFileChangeSummary(result) {
|
|
|
7838
8696
|
* The model identifier and cumulative turn duration are kept together here
|
|
7839
8697
|
* so callers do not need to know how agent-loop timing should be presented.
|
|
7840
8698
|
*/
|
|
7841
|
-
function formatAgentMessageMeta(model, durationMs) {
|
|
7842
|
-
|
|
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`;
|
|
7843
8715
|
}
|
|
7844
8716
|
/**
|
|
7845
8717
|
* Converts elapsed milliseconds into the short human-readable duration used
|
|
@@ -7868,6 +8740,9 @@ function formatNumber(value, fractionDigits) {
|
|
|
7868
8740
|
maximumFractionDigits: fractionDigits
|
|
7869
8741
|
});
|
|
7870
8742
|
}
|
|
8743
|
+
function formatInteger(value) {
|
|
8744
|
+
return value.toLocaleString("en", { maximumFractionDigits: 0 });
|
|
8745
|
+
}
|
|
7871
8746
|
//#endregion
|
|
7872
8747
|
//#region src/tui/runtime-events.ts
|
|
7873
8748
|
function renderRuntimeEvent(event) {
|
|
@@ -7882,9 +8757,38 @@ function renderRuntimeEvent(event) {
|
|
|
7882
8757
|
actions: event.actions
|
|
7883
8758
|
})];
|
|
7884
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
|
+
})];
|
|
7885
8776
|
case "status": return [];
|
|
7886
8777
|
}
|
|
7887
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
|
+
}
|
|
7888
8792
|
function formatKbPathSource(status) {
|
|
7889
8793
|
return status.kbPathSource === "env" ? " (custom)" : "";
|
|
7890
8794
|
}
|
|
@@ -7933,10 +8837,10 @@ var TopchesterTuiShell = class {
|
|
|
7933
8837
|
});
|
|
7934
8838
|
app.setTaskPlan(this.options.initialTaskPlan);
|
|
7935
8839
|
app.setSubmitMessage((message) => {
|
|
7936
|
-
this.submitChatMessage(app, tui, message);
|
|
8840
|
+
this.startBackgroundTask(app, tui, "Chat", () => this.submitChatMessage(app, tui, message));
|
|
7937
8841
|
});
|
|
7938
8842
|
app.setSubmitCommand((command) => {
|
|
7939
|
-
this.submitSlashCommand(app, tui, command);
|
|
8843
|
+
this.startBackgroundTask(app, tui, "Command", () => this.submitSlashCommand(app, tui, command));
|
|
7940
8844
|
});
|
|
7941
8845
|
tui.addChild(app);
|
|
7942
8846
|
tui.setFocus(app);
|
|
@@ -7953,7 +8857,15 @@ var TopchesterTuiShell = class {
|
|
|
7953
8857
|
}
|
|
7954
8858
|
}));
|
|
7955
8859
|
tui.start();
|
|
7956
|
-
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
|
+
});
|
|
7957
8869
|
}
|
|
7958
8870
|
async checkAgent(app, tui) {
|
|
7959
8871
|
const busy = new BusyIndicator(app, tui, {
|
|
@@ -7980,7 +8892,7 @@ var TopchesterTuiShell = class {
|
|
|
7980
8892
|
app.addMessage(systemMessage("Agent check stopped."));
|
|
7981
8893
|
app.setStatus("ready");
|
|
7982
8894
|
} else {
|
|
7983
|
-
const message =
|
|
8895
|
+
const message = formatPlainError(error);
|
|
7984
8896
|
app.addMessage(systemMessage(`Agent check failed: ${message}`));
|
|
7985
8897
|
app.setStatus("agent check failed");
|
|
7986
8898
|
}
|
|
@@ -8002,6 +8914,7 @@ var TopchesterTuiShell = class {
|
|
|
8002
8914
|
]
|
|
8003
8915
|
});
|
|
8004
8916
|
const abortController = new AbortController();
|
|
8917
|
+
const reasoningDisplay = isStreamReasoningEnabledByEnv() ? createBusyReasoningSink(busy) : void 0;
|
|
8005
8918
|
let cancelled = false;
|
|
8006
8919
|
app.setCancelPending(() => {
|
|
8007
8920
|
cancelled = true;
|
|
@@ -8016,17 +8929,23 @@ var TopchesterTuiShell = class {
|
|
|
8016
8929
|
role: "user",
|
|
8017
8930
|
text: message
|
|
8018
8931
|
});
|
|
8019
|
-
await this.runtime.
|
|
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
|
+
}
|
|
8020
8940
|
await this.applyRuntimeEvents(app, [event], tui);
|
|
8021
8941
|
tui.requestRender();
|
|
8022
|
-
}
|
|
8942
|
+
}
|
|
8023
8943
|
} catch (error) {
|
|
8024
8944
|
if (cancelled) {
|
|
8025
8945
|
app.addMessage(systemMessage("Response stopped."));
|
|
8026
8946
|
app.setStatus("ready");
|
|
8027
8947
|
} else {
|
|
8028
|
-
|
|
8029
|
-
app.addMessage(systemMessage(`Chat failed: ${errorMessage}`));
|
|
8948
|
+
app.addMessage(systemMessage(`Chat failed: ${formatPlainError(error)}`));
|
|
8030
8949
|
app.setStatus("chat failed");
|
|
8031
8950
|
await this.persistPayloadWithWarning(app, {
|
|
8032
8951
|
kind: "status",
|
|
@@ -8055,8 +8974,7 @@ var TopchesterTuiShell = class {
|
|
|
8055
8974
|
busy.setActivity(event.message);
|
|
8056
8975
|
}), tui);
|
|
8057
8976
|
} catch (error) {
|
|
8058
|
-
|
|
8059
|
-
app.addMessage(systemMessage(`Command failed: ${errorMessage}`));
|
|
8977
|
+
app.addMessage(systemMessage(`Command failed: ${formatPlainError(error)}`));
|
|
8060
8978
|
app.setStatus("command failed");
|
|
8061
8979
|
await this.persistPayloadWithWarning(app, {
|
|
8062
8980
|
kind: "status",
|
|
@@ -8135,6 +9053,8 @@ function chatMessageToSessionPayload(message) {
|
|
|
8135
9053
|
text: message.text,
|
|
8136
9054
|
...message.meta === void 0 ? {} : { meta: message.meta }
|
|
8137
9055
|
};
|
|
9056
|
+
if (message.kind === "thinking") return;
|
|
9057
|
+
if (message.kind === "subagent") return;
|
|
8138
9058
|
if (message.kind === "modal") return {
|
|
8139
9059
|
kind: "choice",
|
|
8140
9060
|
tone: message.tone,
|
|
@@ -8148,38 +9068,6 @@ function chatMessageToSessionPayload(message) {
|
|
|
8148
9068
|
call: message.call
|
|
8149
9069
|
};
|
|
8150
9070
|
}
|
|
8151
|
-
function runtimeEventToSessionPayload(event) {
|
|
8152
|
-
switch (event.type) {
|
|
8153
|
-
case "message": return {
|
|
8154
|
-
kind: "message",
|
|
8155
|
-
role: event.role,
|
|
8156
|
-
text: event.text,
|
|
8157
|
-
...event.meta === void 0 ? {} : { meta: event.meta }
|
|
8158
|
-
};
|
|
8159
|
-
case "tool_call": return {
|
|
8160
|
-
kind: "tool_call",
|
|
8161
|
-
label: event.label,
|
|
8162
|
-
call: event.call
|
|
8163
|
-
};
|
|
8164
|
-
case "task_plan": return {
|
|
8165
|
-
kind: "task_plan",
|
|
8166
|
-
items: event.plan.items,
|
|
8167
|
-
updatedAt: event.plan.updatedAt
|
|
8168
|
-
};
|
|
8169
|
-
case "knowledge_status": return;
|
|
8170
|
-
case "choice": return {
|
|
8171
|
-
kind: "choice",
|
|
8172
|
-
tone: event.tone,
|
|
8173
|
-
title: event.title,
|
|
8174
|
-
...event.body === void 0 ? {} : { body: event.body },
|
|
8175
|
-
actions: event.actions
|
|
8176
|
-
};
|
|
8177
|
-
case "status": return {
|
|
8178
|
-
kind: "status",
|
|
8179
|
-
status: event.status
|
|
8180
|
-
};
|
|
8181
|
-
}
|
|
8182
|
-
}
|
|
8183
9071
|
function slashCommandToSessionPayload(command) {
|
|
8184
9072
|
return {
|
|
8185
9073
|
kind: "message",
|
|
@@ -8191,8 +9079,34 @@ function slashCommandToSessionPayload(command) {
|
|
|
8191
9079
|
}
|
|
8192
9080
|
};
|
|
8193
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
|
+
}
|
|
8194
9108
|
function formatPlainError(error) {
|
|
8195
|
-
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";
|
|
8196
9110
|
}
|
|
8197
9111
|
function printExitBanner(sessionId, durationMs) {
|
|
8198
9112
|
console.log("");
|
|
@@ -8336,8 +9250,8 @@ async function executeRunCommand(context, options) {
|
|
|
8336
9250
|
text: options.prompt,
|
|
8337
9251
|
inputType: "prompt"
|
|
8338
9252
|
});
|
|
8339
|
-
await
|
|
8340
|
-
|
|
9253
|
+
for await (const event of runtime.submitMessageStream(conversation, options.prompt, abortController.signal, { session })) await applyRuntimeEvent({
|
|
9254
|
+
event,
|
|
8341
9255
|
session,
|
|
8342
9256
|
jsonEvents,
|
|
8343
9257
|
runId,
|
|
@@ -8386,7 +9300,9 @@ async function loadConversation(workspaceRoot, resume) {
|
|
|
8386
9300
|
text: message.text
|
|
8387
9301
|
}];
|
|
8388
9302
|
case "system":
|
|
9303
|
+
case "thinking":
|
|
8389
9304
|
case "tool_call":
|
|
9305
|
+
case "subagent":
|
|
8390
9306
|
case "modal": return [];
|
|
8391
9307
|
}
|
|
8392
9308
|
});
|
|
@@ -8398,12 +9314,16 @@ async function persistStartupMessages(session, context) {
|
|
|
8398
9314
|
}
|
|
8399
9315
|
}
|
|
8400
9316
|
async function applyRuntimeEvents(options) {
|
|
8401
|
-
for (const event of options.events) {
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
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);
|
|
8407
9327
|
}
|
|
8408
9328
|
function printPlainEvent(event) {
|
|
8409
9329
|
if (event.type === "message") {
|