topchester-ai 0.11.0 → 0.13.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",
@@ -3395,10 +3681,19 @@ const rawTopchesterConfigSchema = z.object({
3395
3681
  models: rawModelsSchema.optional(),
3396
3682
  ignore: z.object({ paths: z.array(ignorePathSchema).optional() }).optional()
3397
3683
  });
3684
+ function getGlobalTopchesterConfigDir() {
3685
+ return join(homedir(), ".config", "topchester");
3686
+ }
3687
+ function ensureGlobalTopchesterConfigDir() {
3688
+ const dir = getGlobalTopchesterConfigDir();
3689
+ mkdirSync(dir, { recursive: true });
3690
+ return dir;
3691
+ }
3398
3692
  function loadTopchesterConfig(options) {
3693
+ const globalConfigDir = getGlobalTopchesterConfigDir();
3399
3694
  const paths = [
3400
- join(homedir(), ".config/topchester/config.yaml"),
3401
- join(homedir(), ".config/topchester/config.jsonc"),
3695
+ join(globalConfigDir, "config.yaml"),
3696
+ join(globalConfigDir, "config.jsonc"),
3402
3697
  join(options.workspaceRoot, "topchester.yaml"),
3403
3698
  join(options.workspaceRoot, "topchester.jsonc"),
3404
3699
  join(options.workspaceRoot, ".topchester/config.local.yaml"),
@@ -3506,10 +3801,7 @@ function ensureKnownProvider(providers, provider) {
3506
3801
  baseURL: "https://openrouter.ai/api/v1",
3507
3802
  apiKeyEnv: "OPENROUTER_API_KEY",
3508
3803
  supportsStructuredOutputs: true,
3509
- headers: {
3510
- "HTTP-Referer": "https://topchester.com",
3511
- "X-Title": "Topchester"
3512
- }
3804
+ headers: { ...openRouterAttributionHeaders }
3513
3805
  };
3514
3806
  }
3515
3807
  function applyKnownProviderDefaults(providers) {
@@ -3519,8 +3811,15 @@ function applyKnownProviderDefaults(providers) {
3519
3811
  provider.supportsStructuredOutputs ??= true;
3520
3812
  provider.toolProtocol ??= "native";
3521
3813
  }
3814
+ if (isOpenRouterProvider(providerId, provider.baseURL)) provider.headers = {
3815
+ ...openRouterAttributionHeaders,
3816
+ ...isPlainObject(provider.headers) ? provider.headers : {}
3817
+ };
3522
3818
  }
3523
3819
  }
3820
+ function isOpenRouterProvider(providerId, baseURL) {
3821
+ return providerId.toLowerCase().includes("openrouter") || baseURL.toLowerCase().includes("openrouter.ai");
3822
+ }
3524
3823
  function isOpenAIProvider(providerId, baseURL) {
3525
3824
  const normalizedProvider = providerId.toLowerCase();
3526
3825
  const normalizedBaseURL = baseURL.toLowerCase();
@@ -3609,6 +3908,7 @@ function normalizeLogLevel(level) {
3609
3908
  //#endregion
3610
3909
  //#region src/app/context.ts
3611
3910
  function createAppContext(options) {
3911
+ ensureGlobalTopchesterConfigDir();
3612
3912
  const config = loadTopchesterConfig(options);
3613
3913
  const modelGateway = new ModelGateway(normalizeModelGatewayConfig(config));
3614
3914
  const loggerInfo = createTopchesterLogger(options.workspaceRoot);
@@ -5519,6 +5819,12 @@ function agentMessage(text, meta) {
5519
5819
  meta
5520
5820
  };
5521
5821
  }
5822
+ function thinkingMessage(text) {
5823
+ return {
5824
+ kind: "thinking",
5825
+ text
5826
+ };
5827
+ }
5522
5828
  function toolCallMessage(call, label, resultSummary) {
5523
5829
  return resultSummary === void 0 ? {
5524
5830
  kind: "tool_call",
@@ -5531,6 +5837,12 @@ function toolCallMessage(call, label, resultSummary) {
5531
5837
  resultSummary
5532
5838
  };
5533
5839
  }
5840
+ function subagentMessage(message) {
5841
+ return {
5842
+ kind: "subagent",
5843
+ ...message
5844
+ };
5845
+ }
5534
5846
  function modalMessage(message) {
5535
5847
  return {
5536
5848
  kind: "modal",
@@ -5540,6 +5852,8 @@ function modalMessage(message) {
5540
5852
  function renderChatMessage(message, options = {}) {
5541
5853
  if (message.kind === "modal") return renderChatModal(message, options.selectedActionIndex);
5542
5854
  if (message.kind === "tool_call") return renderToolCallMessage(message);
5855
+ if (message.kind === "subagent") return renderSubagentMessage(message);
5856
+ if (message.kind === "thinking") return message.text.split("\n").map((line) => ui.muted(line));
5543
5857
  if (message.text.length === 0) return [""];
5544
5858
  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
5859
  if (message.kind === "user") return renderUserMessage(lines);
@@ -5572,6 +5886,18 @@ function renderToolCallMessage(message) {
5572
5886
  const visibleLabel = message.resultSummary && !message.label.includes(message.resultSummary) ? `${message.label} ${message.resultSummary}` : message.label;
5573
5887
  return [` ${ui.muted(expandTabs(visibleLabel))}`];
5574
5888
  }
5889
+ function renderSubagentMessage(message) {
5890
+ const label = message.title ?? shortSessionId(message.sessionId);
5891
+ switch (message.status) {
5892
+ case "running": return [` ${ui.muted(`↳ task: ${label} (running)`)}`];
5893
+ case "event": return message.text ? [` ${ui.muted(`↳ task: ${label}: ${message.text}`)}`] : [];
5894
+ case "completed": return [` ${ui.muted(`↳ task: ${label} (completed)`)}`, ...message.text ? [` ${message.text}`] : []];
5895
+ case "failed": return [` ${ui.warn(`↳ task: ${label} (failed)`)}`, ...message.text ? [` ${message.text}`] : []];
5896
+ }
5897
+ }
5898
+ function shortSessionId(sessionId) {
5899
+ return sessionId.length <= 8 ? sessionId : sessionId.slice(0, 8);
5900
+ }
5575
5901
  function expandTabs(line) {
5576
5902
  let column = 0;
5577
5903
  let expanded = "";
@@ -5640,11 +5966,21 @@ const jsonValueSchema = z.lazy(() => z.union([
5640
5966
  const sessionMetadataSchema = z.object({
5641
5967
  version: z.literal(1),
5642
5968
  sessionId: z.string(),
5969
+ rootSessionId: z.string().optional(),
5970
+ parentSessionId: z.string().optional(),
5971
+ parentToolCallId: z.string().optional(),
5972
+ source: z.enum(["user", "subagent"]).optional(),
5973
+ agentProfileId: z.string().optional(),
5974
+ title: z.string().optional(),
5643
5975
  workspaceRoot: z.string().min(1),
5644
5976
  createdAt: isoTimestampSchema,
5645
5977
  updatedAt: isoTimestampSchema,
5646
5978
  lastEventId: z.number().int().min(0)
5647
- });
5979
+ }).transform((metadata) => ({
5980
+ ...metadata,
5981
+ rootSessionId: metadata.rootSessionId ?? metadata.sessionId,
5982
+ source: metadata.source ?? "user"
5983
+ }));
5648
5984
  const eventEnvelopeSchema = z.object({
5649
5985
  version: z.literal(1),
5650
5986
  id: z.number().int().positive(),
@@ -5696,15 +6032,74 @@ const choicePayloadSchema = z.object({
5696
6032
  value: z.string().optional()
5697
6033
  }))
5698
6034
  });
6035
+ const subagentLifecycleBasePayloadSchema = z.object({
6036
+ sessionId: z.string(),
6037
+ parentSessionId: z.string(),
6038
+ parentToolCallId: z.string()
6039
+ });
6040
+ const subagentStartedPayloadSchema = subagentLifecycleBasePayloadSchema.extend({
6041
+ kind: z.literal("subagent_started"),
6042
+ agentProfileId: z.string().optional(),
6043
+ title: z.string().optional()
6044
+ });
6045
+ const subagentEventPayloadSchema = subagentLifecycleBasePayloadSchema.extend({
6046
+ kind: z.literal("subagent_event"),
6047
+ event: z.record(z.string(), jsonValueSchema)
6048
+ });
6049
+ const subagentCompletedPayloadSchema = subagentLifecycleBasePayloadSchema.extend({
6050
+ kind: z.literal("subagent_completed"),
6051
+ result: z.string().optional()
6052
+ });
6053
+ const subagentFailedPayloadSchema = subagentLifecycleBasePayloadSchema.extend({
6054
+ kind: z.literal("subagent_failed"),
6055
+ error: z.string()
6056
+ });
5699
6057
  const sessionEventPayloadSchema = z.discriminatedUnion("kind", [
5700
6058
  messagePayloadSchema,
5701
6059
  toolCallPayloadSchema,
5702
6060
  taskPlanPayloadSchema,
5703
6061
  statusPayloadSchema,
5704
6062
  knowledgeStatusPayloadSchema,
5705
- choicePayloadSchema
6063
+ choicePayloadSchema,
6064
+ subagentStartedPayloadSchema,
6065
+ subagentEventPayloadSchema,
6066
+ subagentCompletedPayloadSchema,
6067
+ subagentFailedPayloadSchema
5706
6068
  ]);
5707
6069
  const sessionEventSchema = z.intersection(eventEnvelopeSchema, sessionEventPayloadSchema);
6070
+ const sessionEventPayload = {
6071
+ subagentStarted(reference, options = {}) {
6072
+ return {
6073
+ kind: "subagent_started",
6074
+ ...reference,
6075
+ ...options
6076
+ };
6077
+ },
6078
+ subagentEvent(reference, event) {
6079
+ return {
6080
+ kind: "subagent_event",
6081
+ ...reference,
6082
+ event
6083
+ };
6084
+ },
6085
+ subagentCompleted(reference, result) {
6086
+ return result === void 0 ? {
6087
+ kind: "subagent_completed",
6088
+ ...reference
6089
+ } : {
6090
+ kind: "subagent_completed",
6091
+ ...reference,
6092
+ result
6093
+ };
6094
+ },
6095
+ subagentFailed(reference, error) {
6096
+ return {
6097
+ kind: "subagent_failed",
6098
+ ...reference,
6099
+ error
6100
+ };
6101
+ }
6102
+ };
5708
6103
  //#endregion
5709
6104
  //#region src/session/store.ts
5710
6105
  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 +6115,8 @@ async function createSession(workspaceRoot) {
5720
6115
  const metadata = {
5721
6116
  version: 1,
5722
6117
  sessionId,
6118
+ rootSessionId: sessionId,
6119
+ source: "user",
5723
6120
  workspaceRoot,
5724
6121
  createdAt,
5725
6122
  updatedAt: createdAt,
@@ -5730,6 +6127,41 @@ async function createSession(workspaceRoot) {
5730
6127
  await writeFile(eventsPath, "", { flag: "wx" });
5731
6128
  return buildHandle(sessionDir, metadata);
5732
6129
  }
6130
+ async function createChildSession(workspaceRoot, options) {
6131
+ validateSessionId(options.parent.sessionId);
6132
+ const sessionId = generateSessionId();
6133
+ const sessionDir = join(getTopchesterSessionsPath(workspaceRoot), sessionId);
6134
+ const metadataPath = join(sessionDir, "metadata.json");
6135
+ const eventsPath = join(sessionDir, "events.jsonl");
6136
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
6137
+ const metadata = {
6138
+ version: 1,
6139
+ sessionId,
6140
+ rootSessionId: options.parent.metadata.rootSessionId,
6141
+ parentSessionId: options.parent.sessionId,
6142
+ parentToolCallId: options.parentToolCallId,
6143
+ source: "subagent",
6144
+ ...options.agentProfileId === void 0 ? {} : { agentProfileId: options.agentProfileId },
6145
+ ...options.title === void 0 ? {} : { title: options.title },
6146
+ workspaceRoot,
6147
+ createdAt,
6148
+ updatedAt: createdAt,
6149
+ lastEventId: 0
6150
+ };
6151
+ await mkdir(sessionDir, { recursive: true });
6152
+ await writeMetadata(metadataPath, metadata);
6153
+ await writeFile(eventsPath, "", { flag: "wx" });
6154
+ const child = buildHandle(sessionDir, metadata);
6155
+ if (options.recordParentEvent ?? true) await options.parent.append(sessionEventPayload.subagentStarted({
6156
+ sessionId: child.sessionId,
6157
+ parentSessionId: options.parent.sessionId,
6158
+ parentToolCallId: options.parentToolCallId
6159
+ }, {
6160
+ ...options.agentProfileId === void 0 ? {} : { agentProfileId: options.agentProfileId },
6161
+ ...options.title === void 0 ? {} : { title: options.title }
6162
+ }));
6163
+ return child;
6164
+ }
5733
6165
  async function loadSessionForAppend(workspaceRoot, sessionId) {
5734
6166
  const loaded = await loadSession(workspaceRoot, sessionId);
5735
6167
  return buildHandle(loaded.sessionDir, loaded.metadata);
@@ -5803,6 +6235,10 @@ function rehydrateSession(events) {
5803
6235
  };
5804
6236
  break;
5805
6237
  case "knowledge_status": break;
6238
+ case "subagent_started":
6239
+ case "subagent_event":
6240
+ case "subagent_completed":
6241
+ case "subagent_failed": break;
5806
6242
  case "choice":
5807
6243
  messages.push({
5808
6244
  kind: "modal",
@@ -6101,19 +6537,25 @@ var BusyIndicator = class {
6101
6537
  this.tui.requestRender();
6102
6538
  }, 80);
6103
6539
  }
6104
- stop() {
6540
+ stop(options = {}) {
6105
6541
  if (this.timer) {
6106
6542
  clearInterval(this.timer);
6107
6543
  this.timer = void 0;
6108
6544
  }
6109
6545
  this.app.setPromptHint(void 0);
6110
- this.app.setEphemeralLine(void 0);
6546
+ if (options.clearEphemeralLine ?? true) this.app.setEphemeralLine(void 0);
6111
6547
  }
6112
6548
  setActivity(activity) {
6113
6549
  this.activityOverride = activity;
6114
6550
  this.render();
6115
6551
  this.tui.requestRender();
6116
6552
  }
6553
+ clearActivity() {
6554
+ if (!this.activityOverride) return;
6555
+ this.activityOverride = void 0;
6556
+ this.render();
6557
+ this.tui.requestRender();
6558
+ }
6117
6559
  render() {
6118
6560
  if (this.activityOverride) {
6119
6561
  this.app.setEphemeralLine(`${this.frames[this.index]} ${this.activityOverride}`);
@@ -6124,6 +6566,33 @@ var BusyIndicator = class {
6124
6566
  this.app.setEphemeralLine(`${this.frames[this.index]} ${this.options.activities[activityIndex]}`);
6125
6567
  }
6126
6568
  };
6569
+ var ReasoningTailBuffer = class {
6570
+ text = "";
6571
+ get hasText() {
6572
+ return this.text.length > 0;
6573
+ }
6574
+ get value() {
6575
+ return this.text;
6576
+ }
6577
+ append(delta) {
6578
+ const normalized = normalizeReasoningText(`${this.text}${delta}`);
6579
+ if (!normalized) return;
6580
+ this.text = normalized;
6581
+ return this.text;
6582
+ }
6583
+ replace(summary) {
6584
+ const normalized = normalizeReasoningText(summary);
6585
+ if (!normalized) return;
6586
+ this.text = normalized;
6587
+ return this.text;
6588
+ }
6589
+ clear() {
6590
+ this.text = "";
6591
+ }
6592
+ };
6593
+ function normalizeReasoningText(text) {
6594
+ return text.replace(/\s+/gu, " ").trim();
6595
+ }
6127
6596
  //#endregion
6128
6597
  //#region src/agent/commands.ts
6129
6598
  const slashCommandSuggestions = [
@@ -6254,6 +6723,31 @@ const agentEvent = {
6254
6723
  type: "choice",
6255
6724
  ...options
6256
6725
  };
6726
+ },
6727
+ subagentStarted(options) {
6728
+ return {
6729
+ type: "subagent_started",
6730
+ ...options
6731
+ };
6732
+ },
6733
+ subagentEvent(options, event) {
6734
+ return {
6735
+ type: "subagent_event",
6736
+ ...options,
6737
+ event
6738
+ };
6739
+ },
6740
+ subagentCompleted(options) {
6741
+ return {
6742
+ type: "subagent_completed",
6743
+ ...options
6744
+ };
6745
+ },
6746
+ subagentFailed(options) {
6747
+ return {
6748
+ type: "subagent_failed",
6749
+ ...options
6750
+ };
6257
6751
  }
6258
6752
  };
6259
6753
  function choiceAction(label, value) {
@@ -6579,7 +7073,9 @@ var ChatLayout = class {
6579
7073
  text: message.text
6580
7074
  }];
6581
7075
  case "system":
7076
+ case "thinking":
6582
7077
  case "tool_call":
7078
+ case "subagent":
6583
7079
  case "modal": return [];
6584
7080
  }
6585
7081
  });
@@ -7100,6 +7596,73 @@ function isAbortError(error) {
7100
7596
  return error.name === "AbortError" || error.message.toLowerCase().includes("aborted");
7101
7597
  }
7102
7598
  //#endregion
7599
+ //#region src/agent/profiles.ts
7600
+ const READ_ONLY_TOOLS = [
7601
+ "read_file",
7602
+ "list_files",
7603
+ "grep",
7604
+ "find_file",
7605
+ "git_status",
7606
+ "git_diff",
7607
+ "git_log"
7608
+ ];
7609
+ const PRIMARY_AGENT_PROFILE = {
7610
+ id: "primary",
7611
+ displayName: "Primary",
7612
+ mode: "primary",
7613
+ promptAdditions: [],
7614
+ modelPurpose: "agent.primary",
7615
+ toolPermissionDefault: "allow",
7616
+ allowedTools: [],
7617
+ deniedTools: []
7618
+ };
7619
+ const AGENT_PROFILES = [PRIMARY_AGENT_PROFILE, ...[{
7620
+ id: "explore",
7621
+ displayName: "Explore",
7622
+ mode: "subagent",
7623
+ promptAdditions: ["You are running as a read-only exploration subagent. Inspect the workspace and return concise findings to the parent agent."],
7624
+ modelPurpose: "agent.fast",
7625
+ toolPermissionDefault: "deny",
7626
+ allowedTools: READ_ONLY_TOOLS,
7627
+ deniedTools: ["task", "plan_todo"]
7628
+ }, {
7629
+ id: "general",
7630
+ displayName: "General",
7631
+ mode: "subagent",
7632
+ promptAdditions: ["You are running as a constrained subagent. Work only on the delegated prompt and return a concise result."],
7633
+ modelPurpose: "agent.primary",
7634
+ toolPermissionDefault: "allow",
7635
+ allowedTools: [],
7636
+ deniedTools: ["task", "plan_todo"]
7637
+ }]];
7638
+ function resolveAgentProfile(profileId = PRIMARY_AGENT_PROFILE.id) {
7639
+ const profile = AGENT_PROFILES.find((candidate) => candidate.id === profileId);
7640
+ if (!profile) throw new Error(`Unknown agent profile "${profileId}".`);
7641
+ return profile;
7642
+ }
7643
+ function createToolPermissionView(profile, parent = {}) {
7644
+ const deniedTools = new Set(profile.deniedTools);
7645
+ for (const tool of parent.deniedTools ?? []) deniedTools.add(tool);
7646
+ return {
7647
+ profileId: profile.id,
7648
+ defaultPermission: profile.toolPermissionDefault,
7649
+ allowedTools: new Set(profile.allowedTools),
7650
+ deniedTools
7651
+ };
7652
+ }
7653
+ function isToolAllowed(permissionView, toolName) {
7654
+ if (!isRegisteredToolName(toolName)) return false;
7655
+ if (permissionView.deniedTools.has(toolName)) return false;
7656
+ if (permissionView.defaultPermission === "deny") return permissionView.allowedTools.has(toolName);
7657
+ return true;
7658
+ }
7659
+ function getProfileToolDefinitions(permissionView) {
7660
+ return getToolDefinitionsForPermissions((toolName) => isToolAllowed(permissionView, toolName));
7661
+ }
7662
+ function isRegisteredToolName(toolName) {
7663
+ return toolName in toolRegistry;
7664
+ }
7665
+ //#endregion
7103
7666
  //#region src/agent/tools/executor.ts
7104
7667
  async function executeToolCall(workspaceRoot, call, options = {}) {
7105
7668
  const startedAt = Date.now();
@@ -7107,9 +7670,17 @@ async function executeToolCall(workspaceRoot, call, options = {}) {
7107
7670
  workspaceRoot,
7108
7671
  pathEnv: options.pathEnv,
7109
7672
  logger: options.logger,
7110
- taskPlan: options.taskPlan
7673
+ taskPlan: options.taskPlan,
7674
+ profile: options.profile,
7675
+ permissions: options.permissions,
7676
+ subagents: options.subagents,
7677
+ eventSink: options.eventSink,
7678
+ abortSignal: options.abortSignal,
7679
+ toolCallId: options.toolCallId
7111
7680
  };
7112
7681
  try {
7682
+ if (!isToolName(call.tool)) throw new Error(`Unknown tool "${call.tool}".`);
7683
+ if (options.permissions && !isToolAllowed(options.permissions, call.tool)) throw new Error(`Tool "${call.tool}" is not allowed for agent profile "${options.permissions.profileId}".`);
7113
7684
  const definition = getToolDefinition(call.tool);
7114
7685
  const parsedCall = {
7115
7686
  ...call,
@@ -7263,11 +7834,17 @@ function formatErrorMessage(error) {
7263
7834
  }
7264
7835
  //#endregion
7265
7836
  //#region src/agent/prompts.ts
7266
- function getChatSystemPrompt() {
7837
+ function getChatSystemPrompt(options = {}) {
7838
+ const profile = options.profile ?? PRIMARY_AGENT_PROFILE;
7839
+ const canUseTool = (toolName) => options.permissions ? isToolAllowed(options.permissions, toolName) : true;
7840
+ const toolPromptLines = options.permissions ? getToolPromptLines((toolName) => canUseTool(toolName)) : getToolPromptLines();
7267
7841
  return [
7268
7842
  "You are Topchester, a plain-spoken terminal coding agent for software engineering work.",
7269
7843
  "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
7844
  "",
7845
+ `Agent profile: ${profile.displayName} (${profile.id}).`,
7846
+ ...profile.promptAdditions,
7847
+ "",
7271
7848
  "Working style:",
7272
7849
  "- Start by understanding the user's intent and the surrounding code before proposing or changing anything non-trivial.",
7273
7850
  "- Prefer local project evidence over assumptions. Use search and read tools to find relevant files, examples, tests, commands, and conventions.",
@@ -7280,41 +7857,183 @@ function getChatSystemPrompt() {
7280
7857
  "- Ask a clarifying question only when the missing information blocks useful progress or the safe interpretation is genuinely unclear.",
7281
7858
  "",
7282
7859
  "You have these tools available:",
7283
- ...getToolPromptLines(),
7860
+ ...toolPromptLines,
7284
7861
  "",
7285
7862
  "Tool use:",
7286
7863
  "- 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
7864
  "- 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
7865
  "- 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.",
7866
+ ...canUseTool("plan_todo") ? [
7867
+ "- Use plan_todo for non-trivial multi-step work before the first substantive repository tool call.",
7868
+ "- 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.",
7869
+ "- Do not use plan_todo for simple one-step answers, tiny reads, or trivial edits.",
7870
+ "- 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."
7871
+ ] : [],
7872
+ ...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."] : [],
7873
+ ...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."] : [],
7874
+ ...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."] : [],
7875
+ ...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."] : [],
7876
+ ...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."] : [],
7877
+ ...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."] : [],
7878
+ ...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."] : [],
7879
+ ...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."] : [],
7880
+ ...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."] : [],
7881
+ ...canUseTool("write_file") && canUseTool("read_file") ? [
7882
+ "- 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.",
7883
+ "- 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.",
7884
+ "- Pass write_file create_parent_dirs:true only when the user intent clearly includes creating that folder path."
7885
+ ] : [],
7886
+ ...canUseTool("write_file") && !canUseTool("read_file") ? [
7887
+ "- 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.",
7888
+ "- 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.",
7889
+ "- Pass write_file create_parent_dirs:true only when the user intent clearly includes creating that folder path."
7890
+ ] : [],
7891
+ ...canUseTool("inspect_command") ? ["- Do not use inspect_command for file creation or file mutation."] : [],
7892
+ ...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."] : [],
7893
+ ...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."] : [],
7894
+ ...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
7895
  "- After each tool result, decide the next useful action from the new evidence. Continue until the request is handled or blocked.",
7310
7896
  "Do not make up file contents or search results."
7311
7897
  ].join("\n");
7312
7898
  }
7313
7899
  //#endregion
7900
+ //#region src/session/runtime-payloads.ts
7901
+ function runtimeEventToSessionPayload(event) {
7902
+ switch (event.type) {
7903
+ case "message": return {
7904
+ kind: "message",
7905
+ role: event.role,
7906
+ text: event.text,
7907
+ ...event.meta === void 0 ? {} : { meta: event.meta }
7908
+ };
7909
+ case "tool_call": return {
7910
+ kind: "tool_call",
7911
+ label: event.label,
7912
+ call: event.call
7913
+ };
7914
+ case "task_plan": return {
7915
+ kind: "task_plan",
7916
+ items: event.plan.items,
7917
+ updatedAt: event.plan.updatedAt
7918
+ };
7919
+ case "knowledge_status": return;
7920
+ case "choice": return {
7921
+ kind: "choice",
7922
+ tone: event.tone,
7923
+ title: event.title,
7924
+ ...event.body === void 0 ? {} : { body: event.body },
7925
+ actions: event.actions
7926
+ };
7927
+ case "subagent_started": return {
7928
+ kind: "subagent_started",
7929
+ sessionId: event.sessionId,
7930
+ parentSessionId: event.parentSessionId,
7931
+ parentToolCallId: event.parentToolCallId,
7932
+ ...event.agentProfileId === void 0 ? {} : { agentProfileId: event.agentProfileId },
7933
+ ...event.title === void 0 ? {} : { title: event.title }
7934
+ };
7935
+ case "subagent_event": return {
7936
+ kind: "subagent_event",
7937
+ sessionId: event.sessionId,
7938
+ parentSessionId: event.parentSessionId,
7939
+ parentToolCallId: event.parentToolCallId,
7940
+ event: event.event
7941
+ };
7942
+ case "subagent_completed": return {
7943
+ kind: "subagent_completed",
7944
+ sessionId: event.sessionId,
7945
+ parentSessionId: event.parentSessionId,
7946
+ parentToolCallId: event.parentToolCallId,
7947
+ ...event.result === void 0 ? {} : { result: event.result }
7948
+ };
7949
+ case "subagent_failed": return {
7950
+ kind: "subagent_failed",
7951
+ sessionId: event.sessionId,
7952
+ parentSessionId: event.parentSessionId,
7953
+ parentToolCallId: event.parentToolCallId,
7954
+ error: event.error
7955
+ };
7956
+ case "status": return {
7957
+ kind: "status",
7958
+ status: event.status
7959
+ };
7960
+ }
7961
+ }
7962
+ //#endregion
7963
+ //#region src/agent/subagents.ts
7964
+ var SubagentManager = class {
7965
+ options;
7966
+ constructor(options) {
7967
+ this.options = options;
7968
+ }
7969
+ async runTask(options) {
7970
+ const parentSession = this.options.parentSession;
7971
+ if (!parentSession) throw new Error("task requires an active persisted session.");
7972
+ const profile = resolveAgentProfile(options.subagentType ?? "explore");
7973
+ if (profile.mode !== "subagent" && profile.mode !== "all") throw new Error(`Agent profile "${profile.id}" cannot be used for subagent tasks.`);
7974
+ const child = await createChildSession(this.options.context.workspaceRoot, {
7975
+ parent: parentSession,
7976
+ parentToolCallId: options.parentToolCallId,
7977
+ agentProfileId: profile.id,
7978
+ title: options.description,
7979
+ recordParentEvent: false
7980
+ });
7981
+ const reference = {
7982
+ sessionId: child.sessionId,
7983
+ parentSessionId: parentSession.sessionId,
7984
+ parentToolCallId: options.parentToolCallId
7985
+ };
7986
+ await options.eventSink?.(agentEvent.subagentStarted({
7987
+ ...reference,
7988
+ agentProfileId: profile.id,
7989
+ title: options.description
7990
+ }));
7991
+ const childRuntime = this.options.createRuntime({
7992
+ profile,
7993
+ parentPermissions: this.options.parentPermissions,
7994
+ session: child
7995
+ });
7996
+ let finalResponse = "";
7997
+ try {
7998
+ for await (const childEvent of childRuntime.submitMessageStream([], options.prompt, options.abortSignal, { session: child })) {
7999
+ const payload = runtimeEventToSessionPayload(childEvent);
8000
+ if (payload) await child.append(payload);
8001
+ if (childEvent.type === "message" && childEvent.role === "assistant") finalResponse = childEvent.text;
8002
+ await options.eventSink?.(agentEvent.subagentEvent(reference, childEvent));
8003
+ }
8004
+ const result = finalResponse.trim() || "Subagent completed without an assistant response.";
8005
+ await options.eventSink?.(agentEvent.subagentCompleted({
8006
+ ...reference,
8007
+ result
8008
+ }));
8009
+ return {
8010
+ sessionId: child.sessionId,
8011
+ status: "completed",
8012
+ result,
8013
+ profileId: profile.id
8014
+ };
8015
+ } catch (error) {
8016
+ const message = error instanceof Error ? error.message : String(error);
8017
+ await options.eventSink?.(agentEvent.subagentFailed({
8018
+ ...reference,
8019
+ error: message
8020
+ }));
8021
+ return {
8022
+ sessionId: child.sessionId,
8023
+ status: "failed",
8024
+ result: message,
8025
+ profileId: profile.id
8026
+ };
8027
+ }
8028
+ }
8029
+ };
8030
+ //#endregion
7314
8031
  //#region src/agent/runtime.ts
7315
8032
  const MAX_TOOL_CALLS_PER_TURN = 75;
7316
- var TopchesterAgentRuntime = class {
8033
+ const DEFAULT_TASK_CONCURRENCY = 3;
8034
+ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
7317
8035
  context;
8036
+ options;
7318
8037
  taskPlan = createTaskPlanController();
7319
8038
  /**
7320
8039
  * Holds the shared application context for one runtime instance.
@@ -7322,8 +8041,9 @@ var TopchesterAgentRuntime = class {
7322
8041
  * workspace, model gateway, logger, config, and task-plan state that
7323
8042
  * are passed in by the CLI or TUI layer.
7324
8043
  */
7325
- constructor(context) {
8044
+ constructor(context, options = {}) {
7326
8045
  this.context = context;
8046
+ this.options = options;
7327
8047
  }
7328
8048
  /**
7329
8049
  * Performs the lightweight startup model check used by the interactive
@@ -7348,34 +8068,46 @@ var TopchesterAgentRuntime = class {
7348
8068
  return getKnowledgeStatusEvents(await this.getKnowledgeStatusWithNonCleanFileCount());
7349
8069
  }
7350
8070
  /**
7351
- * Runs one user chat turn through the agent loop. It builds the model
8071
+ * Streams one user chat turn through the agent loop. It builds the model
7352
8072
  * prompt with relevant KB context, calls the model, executes any requested
7353
8073
  * tools, feeds tool results back into the next prompt, and repeats until
7354
8074
  * the model returns a normal assistant message or the loop hits its safety
7355
8075
  * limit.
7356
8076
  *
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.
8077
+ * This is the primary runtime execution contract. Compatibility wrappers
8078
+ * can collect the stream, but the runtime's own turn loop only knows about
8079
+ * ordered events.
7361
8080
  */
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;
8081
+ async *submitMessageStream(conversation, message, abortSignal, options = {}) {
8082
+ let nextPrompt = await this.buildPromptWithKnowledgeContext(buildConversationPrompt(conversation, message), message);
7371
8083
  let totalDurationMs = 0;
8084
+ const tokenUsageTotals = {};
8085
+ const profile = this.options.profile ?? PRIMARY_AGENT_PROFILE;
8086
+ const permissions = createToolPermissionView(profile, { deniedTools: this.options.parentPermissions?.deniedTools });
8087
+ const tools = getProfileToolDefinitions(permissions);
8088
+ const session = options.session ?? this.options.session;
8089
+ const subagents = new SubagentManager({
8090
+ context: this.context,
8091
+ parentSession: session,
8092
+ parentProfile: profile,
8093
+ parentPermissions: permissions,
8094
+ createRuntime: ({ profile: childProfile, parentPermissions, session: childSession }) => new TopchesterAgentRuntime(this.context, {
8095
+ ...this.options,
8096
+ profile: childProfile,
8097
+ parentPermissions,
8098
+ session: childSession
8099
+ })
8100
+ });
7372
8101
  let lastModelId = "model";
7373
8102
  let afterTool;
7374
8103
  let toolProtocolOverride = readToolProtocolEnvOverride();
7375
8104
  let requestedPlanClosure = false;
7376
8105
  for (let toolCalls = 0; toolCalls <= MAX_TOOL_CALLS_PER_TURN; toolCalls += 1) {
7377
8106
  const startedAt = Date.now();
7378
- const system = getChatSystemPrompt();
8107
+ const system = getChatSystemPrompt({
8108
+ profile,
8109
+ permissions
8110
+ });
7379
8111
  this.context.logger.debug({
7380
8112
  event: "model_prompt",
7381
8113
  purpose: "agent.primary",
@@ -7391,12 +8123,15 @@ var TopchesterAgentRuntime = class {
7391
8123
  system,
7392
8124
  prompt: nextPrompt,
7393
8125
  abortSignal,
7394
- toolProtocol: toolProtocolOverride
8126
+ toolProtocol: toolProtocolOverride,
8127
+ onReasoning: options.onReasoning,
8128
+ tools
7395
8129
  });
7396
8130
  const durationMs = Date.now() - startedAt;
7397
8131
  const toolCall = result.toolCalls[0];
7398
8132
  totalDurationMs += durationMs;
7399
8133
  lastModelId = result.modelId;
8134
+ addTokenUsageTotals(tokenUsageTotals, result.usage);
7400
8135
  this.context.logger.debug({
7401
8136
  event: "model_response",
7402
8137
  purpose: "agent.primary",
@@ -7429,37 +8164,112 @@ var TopchesterAgentRuntime = class {
7429
8164
  if (result.providerRejectedTools && result.toolProtocol === "text-json") toolProtocolOverride = "text-json";
7430
8165
  else if (result.providerRejectedTools && result.toolProtocol === "text-xml") toolProtocolOverride = "text-xml";
7431
8166
  if (!toolCall) {
7432
- if (hasOpenTaskPlan(this.taskPlan.get())) {
8167
+ const plan = this.taskPlan.get();
8168
+ const finalText = stripSuppressiblePlanTodoPrefix(result.text, plan) ?? result.text;
8169
+ if (hasOpenTaskPlan(plan)) {
7433
8170
  if (!requestedPlanClosure) {
7434
8171
  requestedPlanClosure = true;
7435
- nextPrompt = `${nextPrompt}\n\n${formatOpenPlanClosureInstruction(result.text, result.toolProtocol)}`;
8172
+ nextPrompt = `${nextPrompt}\n\n${formatOpenPlanClosureInstruction(finalText, result.toolProtocol)}`;
7436
8173
  continue;
7437
8174
  }
7438
- await emit(agentEvent.taskPlan(this.taskPlan.update({ items: [] })));
8175
+ yield agentEvent.taskPlan(this.taskPlan.update({ items: [] }));
7439
8176
  }
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;
8177
+ yield agentEvent.assistantMessage(finalText.trim() || "I got an empty response from the model.", formatAgentMessageMeta(result.modelId, totalDurationMs, tokenUsageTotals));
8178
+ yield agentEvent.status("ready");
8179
+ return;
7442
8180
  }
7443
8181
  if (toolCalls === MAX_TOOL_CALLS_PER_TURN) {
7444
- await emit(agentEvent.choice({
8182
+ yield agentEvent.choice({
7445
8183
  tone: "warning",
7446
8184
  title: "Tool call limit reached",
7447
8185
  body: `Stopped after ${MAX_TOOL_CALLS_PER_TURN} tool calls in one turn. Continue starts another turn; abort leaves the call stopped.`,
7448
8186
  actions: [choiceAction("Continue", "Continue the previous task from where you stopped."), choiceAction("Abort", ABORT_CHOICE_VALUE)]
7449
- }), agentEvent.status("ready"));
7450
- return events;
8187
+ });
8188
+ yield agentEvent.status("ready");
8189
+ return;
8190
+ }
8191
+ if (result.toolCalls.length > 1 && result.toolCalls.every((call) => call.tool === "task")) {
8192
+ const taskCalls = result.toolCalls.map((call) => call);
8193
+ const taskResults = [];
8194
+ for (let index = 0; index < taskCalls.length; index += DEFAULT_TASK_CONCURRENCY) {
8195
+ const batch = taskCalls.slice(index, index + DEFAULT_TASK_CONCURRENCY);
8196
+ const taskEventQueue = createRuntimeEventQueue();
8197
+ const batchResultPromise = Promise.all(batch.map((call, batchIndex) => executeToolCall(this.context.workspaceRoot, call, {
8198
+ logger: this.context.logger,
8199
+ taskPlan: this.taskPlan,
8200
+ profile,
8201
+ permissions,
8202
+ subagents,
8203
+ abortSignal,
8204
+ toolCallId: result.toolCalls[index + batchIndex]?.id,
8205
+ eventSink: (event) => taskEventQueue.push(event)
8206
+ }))).finally(() => {
8207
+ taskEventQueue.close();
8208
+ });
8209
+ for await (const event of taskEventQueue) yield event;
8210
+ taskResults.push(...await batchResultPromise);
8211
+ }
8212
+ for (let index = 0; index < taskCalls.length; index += 1) yield agentEvent.toolCall(taskCalls[index], formatToolCallMessage(taskCalls[index], taskResults[index]));
8213
+ afterTool = "task";
8214
+ nextPrompt = `${nextPrompt}\n\n${taskResults.map((toolResult) => formatToolResultForPrompt(toolResult)).join("\n\n")}\n\n${formatContinuationInstruction(result.toolProtocol, taskResults.at(-1), isToolAllowed(permissions, "plan_todo"))}`;
8215
+ continue;
8216
+ }
8217
+ if (result.toolCalls.length > 1 && result.toolCalls.every((call) => isParallelSafeToolName(call.tool))) {
8218
+ const parallelCalls = result.toolCalls.map((call) => call);
8219
+ const parallelResults = await Promise.all(parallelCalls.map((call, index) => executeToolCall(this.context.workspaceRoot, call, {
8220
+ logger: this.context.logger,
8221
+ taskPlan: this.taskPlan,
8222
+ profile,
8223
+ permissions,
8224
+ subagents,
8225
+ abortSignal,
8226
+ toolCallId: result.toolCalls[index]?.id
8227
+ })));
8228
+ for (let index = 0; index < parallelCalls.length; index += 1) yield agentEvent.toolCall(parallelCalls[index], formatToolCallMessage(parallelCalls[index], parallelResults[index]));
8229
+ afterTool = parallelCalls.at(-1)?.tool;
8230
+ nextPrompt = `${nextPrompt}\n\n${parallelResults.map((toolResult) => formatToolResultForPrompt(toolResult)).join("\n\n")}\n\n${formatContinuationInstruction(result.toolProtocol, parallelResults.at(-1), isToolAllowed(permissions, "plan_todo"))}`;
8231
+ continue;
7451
8232
  }
7452
8233
  const executableToolCall = toolCall;
7453
- const toolResult = await executeToolCall(this.context.workspaceRoot, executableToolCall, {
8234
+ const suppressiblePlanTodoAnswer = getSuppressiblePlanTodoAnswer(executableToolCall, result.text, this.taskPlan.get());
8235
+ if (suppressiblePlanTodoAnswer !== void 0) {
8236
+ yield agentEvent.assistantMessage(suppressiblePlanTodoAnswer || "I got an empty response from the model.", formatAgentMessageMeta(result.modelId, totalDurationMs, tokenUsageTotals));
8237
+ yield agentEvent.status("ready");
8238
+ return;
8239
+ }
8240
+ const toolEventQueue = createRuntimeEventQueue();
8241
+ const toolResultPromise = executeToolCall(this.context.workspaceRoot, executableToolCall, {
7454
8242
  logger: this.context.logger,
7455
- taskPlan: this.taskPlan
8243
+ taskPlan: this.taskPlan,
8244
+ profile,
8245
+ permissions,
8246
+ subagents,
8247
+ abortSignal,
8248
+ toolCallId: toolCall.id,
8249
+ eventSink: (event) => toolEventQueue.push(event)
8250
+ }).finally(() => {
8251
+ toolEventQueue.close();
7456
8252
  });
7457
- await emit(agentEvent.toolCall(executableToolCall, formatToolCallMessage(executableToolCall, toolResult)));
7458
- if (!isToolErrorResult(toolResult) && toolResult.tool === "plan_todo") await emit(agentEvent.taskPlan(toolResult.plan));
8253
+ for await (const event of toolEventQueue) yield event;
8254
+ const toolResult = await toolResultPromise;
8255
+ yield agentEvent.toolCall(executableToolCall, formatToolCallMessage(executableToolCall, toolResult));
8256
+ if (!isToolErrorResult(toolResult) && toolResult.tool === "plan_todo") yield agentEvent.taskPlan(toolResult.plan);
7459
8257
  afterTool = executableToolCall.tool;
7460
- nextPrompt = `${nextPrompt}\n\n${formatToolResultForPrompt(toolResult)}\n\n${formatContinuationInstruction(result.toolProtocol, toolResult)}`;
8258
+ nextPrompt = `${nextPrompt}\n\n${formatToolResultForPrompt(toolResult)}\n\n${formatContinuationInstruction(result.toolProtocol, toolResult, isToolAllowed(permissions, "plan_todo"))}`;
8259
+ }
8260
+ yield agentEvent.assistantMessage("I stopped because the tool loop ended unexpectedly.", formatAgentMessageMeta(lastModelId, totalDurationMs, tokenUsageTotals));
8261
+ yield agentEvent.status("ready");
8262
+ }
8263
+ /**
8264
+ * Compatibility wrapper for callers that still expect a completed event
8265
+ * array or use the older `onEvent` callback shape.
8266
+ */
8267
+ async submitMessage(conversation, message, abortSignal, onEvent, options = {}) {
8268
+ const events = [];
8269
+ for await (const event of this.submitMessageStream(conversation, message, abortSignal, options)) {
8270
+ events.push(event);
8271
+ await onEvent?.(event);
7461
8272
  }
7462
- await emit(agentEvent.assistantMessage("I stopped because the tool loop ended unexpectedly.", formatAgentMessageMeta(lastModelId, totalDurationMs)), agentEvent.status("ready"));
7463
8273
  return events;
7464
8274
  }
7465
8275
  /**
@@ -7503,6 +8313,13 @@ var TopchesterAgentRuntime = class {
7503
8313
  * user's chat turn from reaching the model.
7504
8314
  */
7505
8315
  async buildPromptWithKnowledgeContext(prompt, message) {
8316
+ if (this.options.disableL1Context ?? isL1ContextDisabledByEnv()) {
8317
+ this.context.logger.debug({
8318
+ event: "kb_context_pack_skipped",
8319
+ reason: "disabled"
8320
+ }, "kb context pack skipped");
8321
+ return prompt;
8322
+ }
7506
8323
  const status = getKnowledgeStatus(this.context.workspaceRoot);
7507
8324
  if (!status.kbExists || !status.kbIsDirectory || status.kbContentState !== "ready") return prompt;
7508
8325
  try {
@@ -7533,6 +8350,35 @@ var TopchesterAgentRuntime = class {
7533
8350
  }
7534
8351
  }
7535
8352
  };
8353
+ function createRuntimeEventQueue() {
8354
+ const events = [];
8355
+ let closed = false;
8356
+ let notify;
8357
+ return {
8358
+ push(event) {
8359
+ events.push(event);
8360
+ notify?.();
8361
+ notify = void 0;
8362
+ },
8363
+ close() {
8364
+ closed = true;
8365
+ notify?.();
8366
+ notify = void 0;
8367
+ },
8368
+ async *[Symbol.asyncIterator]() {
8369
+ while (!closed || events.length > 0) {
8370
+ const event = events.shift();
8371
+ if (event) {
8372
+ yield event;
8373
+ continue;
8374
+ }
8375
+ await new Promise((resolve) => {
8376
+ notify = resolve;
8377
+ });
8378
+ }
8379
+ }
8380
+ };
8381
+ }
7536
8382
  /**
7537
8383
  * Calls the configured model gateway for a single agent step and normalizes
7538
8384
  * the result into the newer `ModelAgentResult` shape. Gateways that implement
@@ -7541,10 +8387,7 @@ var TopchesterAgentRuntime = class {
7541
8387
  * so the rest of the runtime can use the same tool loop.
7542
8388
  */
7543
8389
  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
- });
8390
+ if ("generateAgentStep" in context.modelGateway && typeof context.modelGateway.generateAgentStep === "function") return context.modelGateway.generateAgentStep({ ...request });
7548
8391
  const result = await context.modelGateway.generateText(request);
7549
8392
  const parsed = parseToolCallWithSource(result.text);
7550
8393
  const toolProtocol = parsed?.source === "text-xml" ? "text-xml" : "text-json";
@@ -7568,6 +8411,21 @@ async function generateAgentStep(context, request) {
7568
8411
  openRouterRoutingApplied: false
7569
8412
  };
7570
8413
  }
8414
+ function getSuppressiblePlanTodoAnswer(call, modelText, currentPlan) {
8415
+ if (call.tool !== "plan_todo" || hasOpenTaskPlan(currentPlan)) return;
8416
+ const items = call.args.items;
8417
+ if (!Array.isArray(items) || items.some((item) => !isCompletedPlanTodoItem(item))) return;
8418
+ const parsed = parseToolCallWithSource(modelText, ["text-json"]);
8419
+ return parsed?.remainder ? parsed.remainder : void 0;
8420
+ }
8421
+ function stripSuppressiblePlanTodoPrefix(modelText, currentPlan) {
8422
+ const parsed = parseToolCallWithSource(modelText, ["text-json"]);
8423
+ if (!parsed) return;
8424
+ return getSuppressiblePlanTodoAnswer(parsed.call, modelText, currentPlan);
8425
+ }
8426
+ function isCompletedPlanTodoItem(item) {
8427
+ return Boolean(item && typeof item === "object" && "status" in item && item.status === "completed");
8428
+ }
7571
8429
  /**
7572
8430
  * Reads the optional environment override for the tool-calling protocol.
7573
8431
  * Invalid values are ignored instead of failing startup, which keeps local
@@ -7578,6 +8436,14 @@ function readToolProtocolEnvOverride() {
7578
8436
  const value = process.env.TOPCHESTER_TOOL_PROTOCOL;
7579
8437
  if (value === "auto" || value === "native" || value === "text-json" || value === "text-xml") return value;
7580
8438
  }
8439
+ function isL1ContextDisabledByEnv() {
8440
+ const value = process.env.TOPCHESTER_DISABLE_L1_CONTEXT?.trim().toLowerCase();
8441
+ return value === "1" || value === "true" || value === "yes" || value === "on";
8442
+ }
8443
+ function shouldShowTokenUsageByEnv() {
8444
+ const value = process.env.TOPCHESTER_SHOW_TOKEN_USAGE?.trim().toLowerCase();
8445
+ return value !== void 0 && value !== "" && value !== "0" && value !== "false" && value !== "no" && value !== "off";
8446
+ }
7581
8447
  /**
7582
8448
  * Applies TUI styling to per-file KB sync states. The raw scanner statuses
7583
8449
  * are preserved as text, but success, warning, and error categories get
@@ -7651,6 +8517,7 @@ function formatToolResultForPrompt(result) {
7651
8517
  "```"
7652
8518
  ].join("\n");
7653
8519
  if (result.tool === "plan_todo") return [`Tool result from ${result.tool}:`, result.content].join("\n");
8520
+ if (result.tool === "task") return [`Tool result from ${result.tool}:`, result.content].join("\n");
7654
8521
  if (result.tool === "edit_file") return [
7655
8522
  `Tool result from ${result.tool}${path}:`,
7656
8523
  `before_hash: ${result.beforeHash}`,
@@ -7754,13 +8621,13 @@ function formatToolResultForPrompt(result) {
7754
8621
  * restates the current tool-call protocol so the next model step remains
7755
8622
  * parseable by the runtime.
7756
8623
  */
7757
- function formatContinuationInstruction(protocol, result) {
8624
+ function formatContinuationInstruction(protocol, result, canUsePlanTodo = true) {
7758
8625
  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
8626
  return [
7760
8627
  "Continue the user's request using the tool result above and the visible plan when one is active.",
7761
8628
  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.",
8629
+ canUsePlanTodo ? "Update plan_todo after major progress changes." : "",
8630
+ 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
8631
  toolInstruction,
7765
8632
  "Otherwise answer the user. Do not guess."
7766
8633
  ].filter(Boolean).join(" ");
@@ -7790,6 +8657,7 @@ function formatOpenPlanClosureInstruction(draftAnswer, protocol) {
7790
8657
  function formatToolCallMessage(call, result) {
7791
8658
  if (result && isToolErrorResult(result)) return `${call.tool} failed: ${result.error}`;
7792
8659
  switch (call.tool) {
8660
+ case "task": return result?.tool === "task" ? `task: ${result.status} ${result.childSessionId}` : `task: ${call.args.description}`;
7793
8661
  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
8662
  case "read_file": return `read_file: ${call.args.path}`;
7795
8663
  case "list_files": return `list_files: ${call.args.path}${call.args.recursive ? " (recursive)" : ""}`;
@@ -7838,8 +8706,22 @@ function formatWriteFileChangeSummary(result) {
7838
8706
  * The model identifier and cumulative turn duration are kept together here
7839
8707
  * so callers do not need to know how agent-loop timing should be presented.
7840
8708
  */
7841
- function formatAgentMessageMeta(model, durationMs) {
7842
- return `${model} · ${formatDuration$1(durationMs)}`;
8709
+ function formatAgentMessageMeta(model, durationMs, usage) {
8710
+ const tokenUsage = shouldShowTokenUsageByEnv() ? formatTokenUsage(usage) : void 0;
8711
+ return [
8712
+ model,
8713
+ formatDuration$1(durationMs),
8714
+ tokenUsage
8715
+ ].filter(Boolean).join(" · ");
8716
+ }
8717
+ function addTokenUsageTotals(totals, usage) {
8718
+ if (!usage) return;
8719
+ if (typeof usage.inputTokens === "number") totals.inputTokens = (totals.inputTokens ?? 0) + usage.inputTokens;
8720
+ if (typeof usage.outputTokens === "number") totals.outputTokens = (totals.outputTokens ?? 0) + usage.outputTokens;
8721
+ }
8722
+ function formatTokenUsage(usage) {
8723
+ if (usage?.inputTokens === void 0 && usage?.outputTokens === void 0) return;
8724
+ return `${formatInteger(usage.inputTokens ?? 0)} input / ${formatInteger(usage.outputTokens ?? 0)} output tokens`;
7843
8725
  }
7844
8726
  /**
7845
8727
  * Converts elapsed milliseconds into the short human-readable duration used
@@ -7868,6 +8750,9 @@ function formatNumber(value, fractionDigits) {
7868
8750
  maximumFractionDigits: fractionDigits
7869
8751
  });
7870
8752
  }
8753
+ function formatInteger(value) {
8754
+ return value.toLocaleString("en", { maximumFractionDigits: 0 });
8755
+ }
7871
8756
  //#endregion
7872
8757
  //#region src/tui/runtime-events.ts
7873
8758
  function renderRuntimeEvent(event) {
@@ -7882,9 +8767,38 @@ function renderRuntimeEvent(event) {
7882
8767
  actions: event.actions
7883
8768
  })];
7884
8769
  case "task_plan": return [];
8770
+ case "subagent_started": return [subagentMessage({
8771
+ status: "running",
8772
+ sessionId: event.sessionId,
8773
+ title: event.title
8774
+ })];
8775
+ case "subagent_event": return formatForwardedSubagentEvent(event.sessionId, event.event);
8776
+ case "subagent_completed": return [subagentMessage({
8777
+ status: "completed",
8778
+ sessionId: event.sessionId,
8779
+ text: event.result
8780
+ })];
8781
+ case "subagent_failed": return [subagentMessage({
8782
+ status: "failed",
8783
+ sessionId: event.sessionId,
8784
+ text: event.error
8785
+ })];
7885
8786
  case "status": return [];
7886
8787
  }
7887
8788
  }
8789
+ function formatForwardedSubagentEvent(sessionId, event) {
8790
+ if (event.type === "message" && event.role === "assistant") return [subagentMessage({
8791
+ status: "event",
8792
+ sessionId,
8793
+ text: event.text
8794
+ })];
8795
+ if (event.type === "tool_call") return [subagentMessage({
8796
+ status: "event",
8797
+ sessionId,
8798
+ text: event.label
8799
+ })];
8800
+ return [];
8801
+ }
7888
8802
  function formatKbPathSource(status) {
7889
8803
  return status.kbPathSource === "env" ? " (custom)" : "";
7890
8804
  }
@@ -7933,10 +8847,10 @@ var TopchesterTuiShell = class {
7933
8847
  });
7934
8848
  app.setTaskPlan(this.options.initialTaskPlan);
7935
8849
  app.setSubmitMessage((message) => {
7936
- this.submitChatMessage(app, tui, message);
8850
+ this.startBackgroundTask(app, tui, "Chat", () => this.submitChatMessage(app, tui, message));
7937
8851
  });
7938
8852
  app.setSubmitCommand((command) => {
7939
- this.submitSlashCommand(app, tui, command);
8853
+ this.startBackgroundTask(app, tui, "Command", () => this.submitSlashCommand(app, tui, command));
7940
8854
  });
7941
8855
  tui.addChild(app);
7942
8856
  tui.setFocus(app);
@@ -7953,7 +8867,15 @@ var TopchesterTuiShell = class {
7953
8867
  }
7954
8868
  }));
7955
8869
  tui.start();
7956
- this.checkAgent(app, tui);
8870
+ this.startBackgroundTask(app, tui, "Agent check", () => this.checkAgent(app, tui));
8871
+ }
8872
+ startBackgroundTask(app, tui, label, task) {
8873
+ task().catch((error) => {
8874
+ app.addMessage(systemMessage(`${label} failed: ${formatPlainError(error)}`));
8875
+ app.setStatus("ready");
8876
+ app.setCancelPending(void 0);
8877
+ tui.requestRender();
8878
+ });
7957
8879
  }
7958
8880
  async checkAgent(app, tui) {
7959
8881
  const busy = new BusyIndicator(app, tui, {
@@ -7980,7 +8902,7 @@ var TopchesterTuiShell = class {
7980
8902
  app.addMessage(systemMessage("Agent check stopped."));
7981
8903
  app.setStatus("ready");
7982
8904
  } else {
7983
- const message = error instanceof Error ? error.message : String(error);
8905
+ const message = formatPlainError(error);
7984
8906
  app.addMessage(systemMessage(`Agent check failed: ${message}`));
7985
8907
  app.setStatus("agent check failed");
7986
8908
  }
@@ -8002,6 +8924,7 @@ var TopchesterTuiShell = class {
8002
8924
  ]
8003
8925
  });
8004
8926
  const abortController = new AbortController();
8927
+ const reasoningDisplay = isStreamReasoningEnabledByEnv() ? createBusyReasoningSink(busy) : void 0;
8005
8928
  let cancelled = false;
8006
8929
  app.setCancelPending(() => {
8007
8930
  cancelled = true;
@@ -8016,17 +8939,23 @@ var TopchesterTuiShell = class {
8016
8939
  role: "user",
8017
8940
  text: message
8018
8941
  });
8019
- await this.runtime.submitMessage(app.getConversationTurns(), message, abortController.signal, async (event) => {
8942
+ for await (const event of this.runtime.submitMessageStream(app.getConversationTurns(), message, abortController.signal, {
8943
+ onReasoning: reasoningDisplay?.sink,
8944
+ session: this.session
8945
+ })) {
8946
+ if (event.type === "message" && event.role === "assistant") {
8947
+ reasoningDisplay?.commit(app);
8948
+ busy.clearActivity();
8949
+ }
8020
8950
  await this.applyRuntimeEvents(app, [event], tui);
8021
8951
  tui.requestRender();
8022
- });
8952
+ }
8023
8953
  } catch (error) {
8024
8954
  if (cancelled) {
8025
8955
  app.addMessage(systemMessage("Response stopped."));
8026
8956
  app.setStatus("ready");
8027
8957
  } else {
8028
- const errorMessage = error instanceof Error ? error.message : String(error);
8029
- app.addMessage(systemMessage(`Chat failed: ${errorMessage}`));
8958
+ app.addMessage(systemMessage(`Chat failed: ${formatPlainError(error)}`));
8030
8959
  app.setStatus("chat failed");
8031
8960
  await this.persistPayloadWithWarning(app, {
8032
8961
  kind: "status",
@@ -8055,8 +8984,7 @@ var TopchesterTuiShell = class {
8055
8984
  busy.setActivity(event.message);
8056
8985
  }), tui);
8057
8986
  } catch (error) {
8058
- const errorMessage = error instanceof Error ? error.message : String(error);
8059
- app.addMessage(systemMessage(`Command failed: ${errorMessage}`));
8987
+ app.addMessage(systemMessage(`Command failed: ${formatPlainError(error)}`));
8060
8988
  app.setStatus("command failed");
8061
8989
  await this.persistPayloadWithWarning(app, {
8062
8990
  kind: "status",
@@ -8135,6 +9063,8 @@ function chatMessageToSessionPayload(message) {
8135
9063
  text: message.text,
8136
9064
  ...message.meta === void 0 ? {} : { meta: message.meta }
8137
9065
  };
9066
+ if (message.kind === "thinking") return;
9067
+ if (message.kind === "subagent") return;
8138
9068
  if (message.kind === "modal") return {
8139
9069
  kind: "choice",
8140
9070
  tone: message.tone,
@@ -8148,38 +9078,6 @@ function chatMessageToSessionPayload(message) {
8148
9078
  call: message.call
8149
9079
  };
8150
9080
  }
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
9081
  function slashCommandToSessionPayload(command) {
8184
9082
  return {
8185
9083
  kind: "message",
@@ -8191,8 +9089,34 @@ function slashCommandToSessionPayload(command) {
8191
9089
  }
8192
9090
  };
8193
9091
  }
9092
+ function isStreamReasoningEnabledByEnv() {
9093
+ const value = process.env.TOPCHESTER_STREAM_REASONING?.trim().toLowerCase();
9094
+ return value === "1" || value === "true" || value === "yes" || value === "on";
9095
+ }
9096
+ function createBusyReasoningSink(busy) {
9097
+ const buffer = new ReasoningTailBuffer();
9098
+ let committed = false;
9099
+ return {
9100
+ commit(app) {
9101
+ if (committed || !buffer.hasText) return;
9102
+ app.addMessage(thinkingMessage(buffer.value));
9103
+ committed = true;
9104
+ },
9105
+ async sink(event) {
9106
+ if (event.type === "clear") {
9107
+ buffer.clear();
9108
+ committed = false;
9109
+ busy.clearActivity();
9110
+ return;
9111
+ }
9112
+ const text = event.type === "summary" ? buffer.replace(event.text ?? "") : buffer.append(event.text ?? "");
9113
+ if (!text) return;
9114
+ busy.setActivity(ui.muted(text));
9115
+ }
9116
+ };
9117
+ }
8194
9118
  function formatPlainError(error) {
8195
- return error instanceof Error ? error.message : String(error);
9119
+ return (error instanceof Error ? error.message : String(error)).split(/\r?\n/u).map((line) => line.trim()).find((line) => line.length > 0) ?? "Unknown error";
8196
9120
  }
8197
9121
  function printExitBanner(sessionId, durationMs) {
8198
9122
  console.log("");
@@ -8336,8 +9260,8 @@ async function executeRunCommand(context, options) {
8336
9260
  text: options.prompt,
8337
9261
  inputType: "prompt"
8338
9262
  });
8339
- await applyRuntimeEvents({
8340
- events: await runtime.submitMessage(conversation, options.prompt, abortController.signal),
9263
+ for await (const event of runtime.submitMessageStream(conversation, options.prompt, abortController.signal, { session })) await applyRuntimeEvent({
9264
+ event,
8341
9265
  session,
8342
9266
  jsonEvents,
8343
9267
  runId,
@@ -8386,7 +9310,9 @@ async function loadConversation(workspaceRoot, resume) {
8386
9310
  text: message.text
8387
9311
  }];
8388
9312
  case "system":
9313
+ case "thinking":
8389
9314
  case "tool_call":
9315
+ case "subagent":
8390
9316
  case "modal": return [];
8391
9317
  }
8392
9318
  });
@@ -8398,12 +9324,16 @@ async function persistStartupMessages(session, context) {
8398
9324
  }
8399
9325
  }
8400
9326
  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
- }
9327
+ for (const event of options.events) await applyRuntimeEvent({
9328
+ ...options,
9329
+ event
9330
+ });
9331
+ }
9332
+ async function applyRuntimeEvent(options) {
9333
+ const payload = runtimeEventToSessionPayload(options.event);
9334
+ if (payload) await options.session.append(payload);
9335
+ pushJson(options.jsonEvents, options.runId, options.session.sessionId, options.event.type, { event: options.event });
9336
+ if (options.plain) printPlainEvent(options.event);
8407
9337
  }
8408
9338
  function printPlainEvent(event) {
8409
9339
  if (event.type === "message") {