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 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 Object.values(toolRegistry).map((tool) => tool.prompt);
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
- call: json,
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 trimmed = extractToolJsonCandidate(stripJsonFence(text.trim()));
3084
+ const { json, remainder } = extractToolJsonCandidate(stripJsonFence(text.trim()));
3018
3085
  let value;
3019
3086
  try {
3020
- value = JSON.parse(trimmed);
3087
+ value = JSON.parse(json);
3021
3088
  } catch {
3022
- return;
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
- tool: definition.name,
3031
- args: parsed.data
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 text;
3112
+ if (!text.startsWith("{")) return {
3113
+ json: text,
3114
+ remainder: ""
3115
+ };
3039
3116
  const endIndex = findJsonObjectEnd(text);
3040
- return endIndex === void 0 ? text : text.slice(0, endIndex + 1);
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 || override === "native") return result;
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: this.resolveModel(request.purpose).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 = { parallel_tool_calls: false };
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
- ...getToolPromptLines(),
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
- "- Use plan_todo for non-trivial multi-step work before the first substantive repository tool call.",
7290
- "- 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.",
7291
- "- Do not use plan_todo for simple one-step answers, tiny reads, or trivial edits.",
7292
- "- Use read/search tools when the user asks about files, code, symbols, usages, tests, or project behavior.",
7293
- "- 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.",
7294
- "- Use list_files, grep, find_file, and read_file for exact file listing, search, lookup, and reading tasks.",
7295
- "- Use git_status, git_diff, and git_log for Git state, diffs, and history. Prefer these over inspect_command for Git workflow inspection.",
7296
- "- 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.",
7297
- "- Use inspect_command only for quick read-only repo orientation when a short familiar command chain is clearer than several dedicated tool calls.",
7298
- "- inspect_command is not a shell. Unsafe commands, shell expansion, scripts, installs, builds, tests, network access, and file mutation are not available through it.",
7299
- "- Use read_file before editing a file so your edit is based on current file content and hash metadata.",
7300
- "- 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.",
7301
- "- Use edit_file for targeted edits to existing files. Make multiple disjoint edits for the same file in one call when possible.",
7302
- "- 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.",
7303
- "- 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.",
7304
- "- Pass write_file create_parent_dirs:true only when the user intent clearly includes creating that folder path.",
7305
- "- Do not use inspect_command for file creation or file mutation.",
7306
- "- Keep edit_file old_text small but unique. Do not include line labels or grep prefixes in old_text; use exact file text only.",
7307
- "- Use edit/write tools when they are available and the user asks you to implement, fix, add, update, or refactor code.",
7308
- "- Use command/test tools when they are available and you need to inspect the environment, run tests, format, lint, typecheck, or verify behavior.",
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
- var TopchesterAgentRuntime = class {
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
- * Runs one user chat turn through the agent loop. It builds the model
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
- * Events are accumulated for the caller and optionally streamed through
7358
- * `onEvent` as soon as tool calls, task-plan updates, choices, or final
7359
- * messages are available. The method also enforces visible task-plan
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 submitMessage(conversation, message, abortSignal, onEvent) {
7363
- const prompt = await this.buildPromptWithKnowledgeContext(buildConversationPrompt(conversation, message), message);
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
- if (hasOpenTaskPlan(this.taskPlan.get())) {
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(result.text, result.toolProtocol)}`;
8162
+ nextPrompt = `${nextPrompt}\n\n${formatOpenPlanClosureInstruction(finalText, result.toolProtocol)}`;
7436
8163
  continue;
7437
8164
  }
7438
- await emit(agentEvent.taskPlan(this.taskPlan.update({ items: [] })));
8165
+ yield agentEvent.taskPlan(this.taskPlan.update({ items: [] }));
7439
8166
  }
7440
- await emit(agentEvent.assistantMessage(result.text.trim() || "I got an empty response from the model.", formatAgentMessageMeta(result.modelId, totalDurationMs)), agentEvent.status("ready"));
7441
- return events;
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
- await emit(agentEvent.choice({
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
- }), agentEvent.status("ready"));
7450
- return events;
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 toolResult = await executeToolCall(this.context.workspaceRoot, executableToolCall, {
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 emit(agentEvent.toolCall(executableToolCall, formatToolCallMessage(executableToolCall, toolResult)));
7458
- if (!isToolErrorResult(toolResult) && toolResult.tool === "plan_todo") await emit(agentEvent.taskPlan(toolResult.plan));
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
- return `${model} · ${formatDuration$1(durationMs)}`;
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 = error instanceof Error ? error.message : String(error);
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.submitMessage(app.getConversationTurns(), message, abortController.signal, async (event) => {
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
- const errorMessage = error instanceof Error ? error.message : String(error);
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
- const errorMessage = error instanceof Error ? error.message : String(error);
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 applyRuntimeEvents({
8340
- events: await runtime.submitMessage(conversation, options.prompt, abortController.signal),
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
- const payload = runtimeEventToSessionPayload(event);
8403
- if (payload) await options.session.append(payload);
8404
- pushJson(options.jsonEvents, options.runId, options.session.sessionId, event.type, { event });
8405
- if (options.plain) printPlainEvent(event);
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") {