topchester-ai 0.66.0 → 0.68.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/bin.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as runTopchesterCli } from "./cli-BS67FTk8.mjs";
2
+ import { t as runTopchesterCli } from "./cli-CRhzHqMB.mjs";
3
3
  //#region src/bin.ts
4
4
  await runTopchesterCli();
5
5
  //#endregion
@@ -9,8 +9,9 @@ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
9
9
  import { generateText, stepCountIs, streamText, tool } from "ai";
10
10
  import { ZodError, z } from "zod";
11
11
  import { execFile, spawn } from "node:child_process";
12
- import { accessSync, constants, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
12
+ import { accessSync, constants, createReadStream, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
13
13
  import { createHash, randomUUID } from "node:crypto";
14
+ import { TextDecoder as TextDecoder$1 } from "node:util";
14
15
  import { parseDocument } from "yaml";
15
16
  import { fileURLToPath } from "node:url";
16
17
  import pino from "pino";
@@ -420,6 +421,7 @@ function toAiSdkToolSet(definitions) {
420
421
  //#region src/agent/tools/process-runner.ts
421
422
  const DEFAULT_MAX_OUTPUT_BYTES$1 = 4e4;
422
423
  const DEFAULT_MAX_OUTPUT_LINES = 1e3;
424
+ const TERMINAL_BENCH_MAX_OUTPUT_BYTES = 2e4;
423
425
  async function runProcess(options) {
424
426
  const startedAt = Date.now();
425
427
  const pathEnv = options.pathEnv ?? process.env.PATH ?? "";
@@ -785,6 +787,8 @@ async function runBashCommand(workspaceRoot, args, options = {}) {
785
787
  pathEnv: options.pathEnv,
786
788
  timeoutMs: args.timeout_ms,
787
789
  abortSignal: options.abortSignal,
790
+ outputLimitBytes: options.benchmarkProfile === "terminal-bench" ? TERMINAL_BENCH_MAX_OUTPUT_BYTES : void 0,
791
+ outputLimitLines: options.benchmarkProfile === "terminal-bench" ? 500 : void 0,
788
792
  env: { NO_COLOR: "1" },
789
793
  missingExecutableLabel: "bash"
790
794
  });
@@ -3584,7 +3588,10 @@ const inspectCommandTool = defineTool({
3584
3588
  description: "Run a narrowly validated read-only command for repository orientation.",
3585
3589
  prompt: "inspect_command: run a safe read-only discovery command inside the workspace for quick repo orientation; prefer read_file, list_files, grep, and find_file for exact file tasks, and do not use it for builds, tests, installs, network, shell scripts, edits, or user-requested specific commands such as node --version, which node, or pnpm --version. To use it, reply with only JSON: {\"tool\":\"inspect_command\",\"args\":{\"command\":\"pwd && rg --files docs/plans | head -20\",\"workdir\":\".\",\"timeout_ms\":10000}}",
3586
3590
  argsSchema: inspectCommandArgsSchema,
3587
- execute: (context, args) => inspectWorkspaceCommand(context.workspaceRoot, args, { pathEnv: context.pathEnv })
3591
+ execute: (context, args) => inspectWorkspaceCommand(context.workspaceRoot, args, {
3592
+ pathEnv: context.pathEnv,
3593
+ benchmarkProfile: context.benchmarkProfile
3594
+ })
3588
3595
  });
3589
3596
  async function inspectWorkspaceCommand(workspaceRoot, args, options = {}) {
3590
3597
  const startedAt = Date.now();
@@ -3596,7 +3603,8 @@ async function inspectWorkspaceCommand(workspaceRoot, args, options = {}) {
3596
3603
  const result = await executePlan(decision.plan, {
3597
3604
  cwd,
3598
3605
  pathEnv: options.pathEnv ?? process.env.PATH ?? "",
3599
- deadlineAt
3606
+ deadlineAt,
3607
+ benchmarkProfile: options.benchmarkProfile
3600
3608
  });
3601
3609
  const durationMs = Date.now() - startedAt;
3602
3610
  return {
@@ -3634,14 +3642,16 @@ async function executePlan(plan, context) {
3634
3642
  timedOut: true,
3635
3643
  truncated
3636
3644
  };
3645
+ const outputLimits = getOutputLimits(context.benchmarkProfile);
3637
3646
  const result = await executePipeline(entry.pipeline, {
3638
3647
  ...context,
3639
3648
  timeoutMs: remainingMs
3640
3649
  });
3641
- stdout = appendBoundedOutput(stdout, result.stdout).output;
3642
- const nextStderr = appendBoundedOutput(stderr, result.stderr);
3650
+ const nextStdout = appendBoundedOutput(stdout, result.stdout, outputLimits);
3651
+ stdout = nextStdout.output;
3652
+ const nextStderr = appendBoundedOutput(stderr, result.stderr, outputLimits);
3643
3653
  stderr = nextStderr.output;
3644
- truncated = truncated || result.truncated || nextStderr.truncated;
3654
+ truncated = truncated || result.truncated || nextStdout.truncated || nextStderr.truncated;
3645
3655
  timedOut = timedOut || result.timedOut;
3646
3656
  lastExitCode = result.exitCode;
3647
3657
  if (result.missingExecutable || result.timedOut) return {
@@ -3681,7 +3691,8 @@ async function executePipeline(pipeline, context) {
3681
3691
  timeoutMs: remainingMs
3682
3692
  });
3683
3693
  input = result.stdout;
3684
- const nextStderr = appendBoundedOutput(stderr, result.stderr);
3694
+ const outputLimits = getOutputLimits(context.benchmarkProfile);
3695
+ const nextStderr = appendBoundedOutput(stderr, result.stderr, outputLimits);
3685
3696
  stderr = nextStderr.output;
3686
3697
  exitCode = result.exitCode;
3687
3698
  timedOut = timedOut || result.timedOut;
@@ -3718,6 +3729,8 @@ async function executeSimpleCommand(command, input, context) {
3718
3729
  cwd: context.cwd,
3719
3730
  pathEnv: context.pathEnv,
3720
3731
  timeoutMs: context.timeoutMs,
3732
+ outputLimitBytes: context.benchmarkProfile === "terminal-bench" ? TERMINAL_BENCH_MAX_OUTPUT_BYTES : void 0,
3733
+ outputLimitLines: context.benchmarkProfile === "terminal-bench" ? 500 : void 0,
3721
3734
  missingExecutableLabel: "inspect_command"
3722
3735
  });
3723
3736
  return {
@@ -3740,6 +3753,13 @@ function shouldExecuteEntry(entry, previousExitCode) {
3740
3753
  function getRemainingTimeoutMs(deadlineAt) {
3741
3754
  return deadlineAt - Date.now();
3742
3755
  }
3756
+ function getOutputLimits(benchmarkProfile) {
3757
+ if (benchmarkProfile !== "terminal-bench") return {};
3758
+ return {
3759
+ maxBytes: TERMINAL_BENCH_MAX_OUTPUT_BYTES,
3760
+ maxLines: 500
3761
+ };
3762
+ }
3743
3763
  function formatInspectCommandContent(result) {
3744
3764
  const sections = [];
3745
3765
  sections.push(result.stdout.trimEnd() || "(no output)");
@@ -4107,16 +4127,30 @@ const planTodoTool = defineTool({
4107
4127
  };
4108
4128
  }
4109
4129
  });
4130
+ //#endregion
4131
+ //#region src/agent/tools/read-file.ts
4132
+ const DEFAULT_MAX_UTF8_READ_BYTES = 512 * 1024;
4133
+ const TERMINAL_BENCH_MAX_UTF8_READ_BYTES = 64 * 1024;
4134
+ const SAMPLE_BYTES = 256;
4110
4135
  const readFileTool = defineTool({
4111
4136
  name: "read_file",
4112
4137
  description: "Read a UTF-8 file inside the workspace.",
4113
- 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\"}}",
4114
- argsSchema: z.object({ path: z.string() }),
4138
+ prompt: "read_file: read a UTF-8 file inside the workspace. For large files, use offset and limit to read a focused byte range. To use it, reply with only JSON: {\"tool\":\"read_file\",\"args\":{\"path\":\"package.json\"}}",
4139
+ argsSchema: z.object({
4140
+ path: z.string(),
4141
+ offset: z.number().int().min(0).optional(),
4142
+ limit: z.number().int().min(1).max(DEFAULT_MAX_UTF8_READ_BYTES).optional()
4143
+ }),
4115
4144
  parallelSafe: true,
4116
4145
  mutatesWorkspace: false,
4117
4146
  resourceKeys: (args) => [`file:${args.path}`],
4118
4147
  execute: async (context, args) => {
4119
- const result = await readWorkspaceFile(context.workspaceRoot, args.path);
4148
+ const result = await readWorkspaceFile(context.workspaceRoot, args.path, {
4149
+ offset: args.offset,
4150
+ limit: args.limit,
4151
+ benchmarkProfile: context.benchmarkProfile,
4152
+ cache: context.readFileCache
4153
+ });
4120
4154
  const projectInstructions = await resolveToolProjectInstructions(context, {
4121
4155
  targetPath: args.path,
4122
4156
  skipWhenTargetIsInstructionFile: true
@@ -4128,19 +4162,181 @@ const readFileTool = defineTool({
4128
4162
  };
4129
4163
  }
4130
4164
  });
4131
- async function readWorkspaceFile(workspaceRoot, path) {
4165
+ async function readWorkspaceFile(workspaceRoot, path, options = {}) {
4132
4166
  const resolvedWorkspace = resolve(workspaceRoot);
4133
4167
  const resolvedPath = isAbsolute(path) ? resolve(path) : resolve(resolvedWorkspace, path);
4134
4168
  const relativePath = relative(resolvedWorkspace, resolvedPath);
4169
+ const maxReadBytes = getReadFileMaxBytes(options.benchmarkProfile);
4135
4170
  if (relativePath.startsWith("..") || isAbsolute(relativePath)) throw new Error(`read_file can only read files inside the workspace: ${path}`);
4136
- const bytes = await readFile(resolvedPath);
4171
+ const fileStat = await stat(resolvedPath);
4172
+ if (!fileStat.isFile()) throw new Error(`read_file can only read regular files: ${path}`);
4173
+ const offset = options.offset ?? 0;
4174
+ const requestedLimit = options.limit;
4175
+ const hasExplicitRange = options.offset !== void 0 || requestedLimit !== void 0;
4176
+ if (offset > fileStat.size) throw new Error(`read_file offset ${offset} is beyond end of file ${relativePath || "."} (${fileStat.size} bytes).`);
4177
+ if (!hasExplicitRange && fileStat.size > maxReadBytes) {
4178
+ const [sample, hash] = await Promise.all([readFileSample(resolvedPath, SAMPLE_BYTES), hashFile$2(resolvedPath)]);
4179
+ return {
4180
+ tool: "read_file",
4181
+ path: relativePath || ".",
4182
+ content: formatSkippedReadSummary({
4183
+ reason: "too_large",
4184
+ relativePath: relativePath || ".",
4185
+ bytes: fileStat.size,
4186
+ maxReadBytes,
4187
+ sample
4188
+ }),
4189
+ hash,
4190
+ skipped: "too_large",
4191
+ bytes: fileStat.size,
4192
+ warning: `Skipped reading ${relativePath || "."}: file is ${fileStat.size} bytes, above the ${maxReadBytes} byte read_file limit.`
4193
+ };
4194
+ }
4195
+ const hashPromise = hashFile$2(resolvedPath);
4196
+ const bytes = hasExplicitRange ? await readFileRange(resolvedPath, offset, Math.min(requestedLimit ?? maxReadBytes, maxReadBytes)) : await readFile(resolvedPath);
4197
+ const hash = await hashPromise;
4198
+ const length = bytes.length;
4199
+ const truncated = hasExplicitRange ? offset + length < fileStat.size : false;
4200
+ const cacheKey = makeReadFileCacheKey(relativePath || ".", hash, fileStat.size, offset, length);
4201
+ if (options.cache?.seen.has(cacheKey)) return {
4202
+ tool: "read_file",
4203
+ path: relativePath || ".",
4204
+ content: formatDedupedReadSummary({
4205
+ relativePath: relativePath || ".",
4206
+ bytes: fileStat.size,
4207
+ hash,
4208
+ offset,
4209
+ length
4210
+ }),
4211
+ hash,
4212
+ bytes: fileStat.size,
4213
+ offset,
4214
+ length,
4215
+ deduped: true,
4216
+ truncated,
4217
+ warning: `Skipped repeated read of unchanged ${relativePath || "."} bytes ${offset}-${offset + length}.`
4218
+ };
4219
+ if (looksBinary$1(bytes) || !isValidUtf8(bytes)) {
4220
+ const sample = bytes.subarray(0, SAMPLE_BYTES);
4221
+ return {
4222
+ tool: "read_file",
4223
+ path: relativePath || ".",
4224
+ content: formatSkippedReadSummary({
4225
+ reason: "binary",
4226
+ relativePath: relativePath || ".",
4227
+ bytes: fileStat.size,
4228
+ maxReadBytes,
4229
+ sample
4230
+ }),
4231
+ hash,
4232
+ skipped: "binary",
4233
+ bytes: fileStat.size,
4234
+ warning: `Skipped reading ${relativePath || "."}: file appears to be binary or non-UTF-8.`
4235
+ };
4236
+ }
4137
4237
  const content = bytes.toString("utf8");
4138
- return {
4238
+ options.cache?.seen.set(cacheKey, {
4239
+ hash,
4240
+ bytes: fileStat.size,
4241
+ offset,
4242
+ length
4243
+ });
4244
+ const result = {
4139
4245
  tool: "read_file",
4140
4246
  path: relativePath || ".",
4141
- content,
4142
- hash: `sha256:${createHash("sha256").update(bytes).digest("hex")}`
4247
+ content: formatReadFileContent({
4248
+ content,
4249
+ relativePath: relativePath || ".",
4250
+ bytes: fileStat.size,
4251
+ offset,
4252
+ length,
4253
+ truncated,
4254
+ ranged: hasExplicitRange
4255
+ }),
4256
+ hash
4143
4257
  };
4258
+ if (hasExplicitRange) {
4259
+ result.bytes = fileStat.size;
4260
+ result.offset = offset;
4261
+ result.length = length;
4262
+ result.truncated = truncated;
4263
+ }
4264
+ return result;
4265
+ }
4266
+ function createReadFileCache() {
4267
+ return { seen: /* @__PURE__ */ new Map() };
4268
+ }
4269
+ function looksBinary$1(bytes) {
4270
+ return bytes.subarray(0, Math.min(bytes.length, SAMPLE_BYTES)).includes(0);
4271
+ }
4272
+ function isValidUtf8(bytes) {
4273
+ try {
4274
+ new TextDecoder$1("utf-8", { fatal: true }).decode(bytes);
4275
+ return true;
4276
+ } catch {
4277
+ return false;
4278
+ }
4279
+ }
4280
+ function formatSkippedReadSummary(options) {
4281
+ const reason = options.reason === "binary" ? "read_file did not return file contents because this file appears to be binary or non-UTF-8." : `read_file did not return file contents because this file is ${options.bytes} bytes, above the ${options.maxReadBytes} byte limit.`;
4282
+ const sampleHex = options.sample.length > 0 ? options.sample.toString("hex").match(/.{1,2}/g)?.join(" ") : "";
4283
+ return [
4284
+ reason,
4285
+ `path: ${options.relativePath}`,
4286
+ `bytes: ${options.bytes}`,
4287
+ sampleHex ? `first_${options.sample.length}_bytes_hex: ${sampleHex}` : "first_bytes_hex: <empty>",
4288
+ "Use read_file with offset and limit for a focused byte range, or shell inspection tools such as `file`, `xxd`, `readelf`, `objdump`, `strings`, or a focused parser instead of reading the whole file into context."
4289
+ ].join("\n");
4290
+ }
4291
+ function formatReadFileContent(options) {
4292
+ if (!options.ranged) return options.content;
4293
+ return [`read_file range: ${options.relativePath} bytes ${options.offset}-${options.offset + options.length} of ${options.bytes}${options.truncated ? " (truncated)" : ""}`, options.content].join("\n");
4294
+ }
4295
+ function formatDedupedReadSummary(options) {
4296
+ return [
4297
+ "read_file did not return file contents because this exact unchanged file range was already shown in this session.",
4298
+ `path: ${options.relativePath}`,
4299
+ `bytes: ${options.bytes}`,
4300
+ `hash: ${options.hash}`,
4301
+ `range: ${options.offset}-${options.offset + options.length}`,
4302
+ "Use a different offset/limit if you need another part of the file."
4303
+ ].join("\n");
4304
+ }
4305
+ async function readFileSample(path, maxBytes) {
4306
+ const handle = await open(path, "r");
4307
+ try {
4308
+ const sample = Buffer.alloc(maxBytes);
4309
+ const { bytesRead } = await handle.read(sample, 0, maxBytes, 0);
4310
+ return sample.subarray(0, bytesRead);
4311
+ } finally {
4312
+ await handle.close();
4313
+ }
4314
+ }
4315
+ async function readFileRange(path, offset, maxBytes) {
4316
+ const handle = await open(path, "r");
4317
+ try {
4318
+ const sample = Buffer.alloc(maxBytes);
4319
+ const { bytesRead } = await handle.read(sample, 0, maxBytes, offset);
4320
+ return sample.subarray(0, bytesRead);
4321
+ } finally {
4322
+ await handle.close();
4323
+ }
4324
+ }
4325
+ async function hashFile$2(path) {
4326
+ const hash = createHash("sha256");
4327
+ await new Promise((resolvePromise, reject) => {
4328
+ const stream = createReadStream(path);
4329
+ stream.on("data", (chunk) => hash.update(chunk));
4330
+ stream.on("error", reject);
4331
+ stream.on("end", resolvePromise);
4332
+ });
4333
+ return `sha256:${hash.digest("hex")}`;
4334
+ }
4335
+ function getReadFileMaxBytes(benchmarkProfile) {
4336
+ return benchmarkProfile === "terminal-bench" ? TERMINAL_BENCH_MAX_UTF8_READ_BYTES : DEFAULT_MAX_UTF8_READ_BYTES;
4337
+ }
4338
+ function makeReadFileCacheKey(path, hash, bytes, offset, length) {
4339
+ return `${path}\0${hash}\0${bytes}\0${offset}\0${length}`;
4144
4340
  }
4145
4341
  //#endregion
4146
4342
  //#region src/agent/tools/command-policy.ts
@@ -4583,18 +4779,29 @@ const runValidatorTool = defineTool({
4583
4779
  timeout_ms: z.number().int().min(100).max(6e5).optional().default(12e4)
4584
4780
  }),
4585
4781
  requiresExclusiveWorkspace: true,
4586
- execute: async (context, args) => runValidatorCommand(context.workspaceRoot, args, context.pathEnv, context.abortSignal)
4782
+ execute: async (context, args) => runValidatorCommand(context.workspaceRoot, args, {
4783
+ pathEnv: context.pathEnv,
4784
+ abortSignal: context.abortSignal,
4785
+ benchmarkProfile: context.benchmarkProfile
4786
+ })
4587
4787
  });
4588
- async function runValidatorCommand(workspaceRoot, args, pathEnv, abortSignal) {
4788
+ async function runValidatorCommand(workspaceRoot, args, pathEnvOrOptions, legacyAbortSignal) {
4789
+ const options = typeof pathEnvOrOptions === "string" ? {
4790
+ pathEnv: pathEnvOrOptions,
4791
+ abortSignal: legacyAbortSignal,
4792
+ benchmarkProfile: void 0
4793
+ } : pathEnvOrOptions ?? {};
4589
4794
  const decision = await validateValidatorCommand(args, { workspaceRoot });
4590
4795
  if (!decision.allowed) throw new Error(decision.reason);
4591
4796
  const result = await runProcess({
4592
4797
  executable: decision.plan.executable,
4593
4798
  args: decision.plan.args,
4594
4799
  cwd: decision.plan.cwd,
4595
- pathEnv,
4800
+ pathEnv: options.pathEnv,
4596
4801
  timeoutMs: args.timeout_ms,
4597
- abortSignal,
4802
+ abortSignal: options.abortSignal,
4803
+ outputLimitBytes: options.benchmarkProfile === "terminal-bench" ? TERMINAL_BENCH_MAX_OUTPUT_BYTES : void 0,
4804
+ outputLimitLines: options.benchmarkProfile === "terminal-bench" ? 500 : void 0,
4598
4805
  env: {
4599
4806
  CI: "1",
4600
4807
  NO_COLOR: "1"
@@ -12065,6 +12272,7 @@ async function executeToolCall(workspaceRoot, call, options = {}) {
12065
12272
  projectInstructions: options.projectInstructions,
12066
12273
  currentUserMessage: options.currentUserMessage,
12067
12274
  benchmarkProfile: options.benchmarkProfile,
12275
+ readFileCache: options.readFileCache,
12068
12276
  eventSink: options.eventSink,
12069
12277
  abortSignal: options.abortSignal,
12070
12278
  toolCallId: options.toolCallId
@@ -13319,6 +13527,7 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
13319
13527
  const requireFinishTask = isFinishTaskRequiredByEnv() && isToolAllowed(permissions, "finish_task");
13320
13528
  const projectInstructionToolState = { shownSourceKeys: /* @__PURE__ */ new Set() };
13321
13529
  const persistedProjectInstructionKeys = /* @__PURE__ */ new Set();
13530
+ const readFileCache = createReadFileCache();
13322
13531
  try {
13323
13532
  for (let toolCalls = 0; toolCalls <= maxToolCallsPerTurn; toolCalls += 1) {
13324
13533
  const startedAt = Date.now();
@@ -13504,6 +13713,7 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
13504
13713
  projectInstructions: projectInstructionToolState,
13505
13714
  currentUserMessage: message,
13506
13715
  benchmarkProfile: options.benchmarkProfile,
13716
+ readFileCache,
13507
13717
  abortSignal,
13508
13718
  toolCallId: entry.toolCallId,
13509
13719
  toolCatalog,
@@ -13575,6 +13785,7 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
13575
13785
  projectInstructions: projectInstructionToolState,
13576
13786
  currentUserMessage: message,
13577
13787
  benchmarkProfile: options.benchmarkProfile,
13788
+ readFileCache,
13578
13789
  abortSignal,
13579
13790
  toolCallId: entry.toolCallId,
13580
13791
  toolCatalog
@@ -13653,6 +13864,7 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
13653
13864
  projectInstructions: projectInstructionToolState,
13654
13865
  currentUserMessage: message,
13655
13866
  benchmarkProfile: options.benchmarkProfile,
13867
+ readFileCache,
13656
13868
  abortSignal,
13657
13869
  toolCallId: toolCall.id,
13658
13870
  toolCatalog,
@@ -14087,6 +14299,14 @@ function getToolCallDisplayDiff(result) {
14087
14299
  function readMaxToolCallsPerTurn() {
14088
14300
  const raw = process.env[MAX_TOOL_CALLS_PER_TURN_ENV]?.trim();
14089
14301
  if (!raw) return DEFAULT_MAX_TOOL_CALLS_PER_TURN;
14302
+ if ([
14303
+ "0",
14304
+ "off",
14305
+ "false",
14306
+ "none",
14307
+ "unlimited",
14308
+ "disabled"
14309
+ ].includes(raw.toLowerCase())) return Number.POSITIVE_INFINITY;
14090
14310
  const parsed = Number(raw);
14091
14311
  if (!Number.isInteger(parsed) || parsed <= 0) return DEFAULT_MAX_TOOL_CALLS_PER_TURN;
14092
14312
  return parsed;
@@ -16554,4 +16774,4 @@ function formatDryRunSyncStatus(status) {
16554
16774
  //#endregion
16555
16775
  export { runTopchesterCli as t };
16556
16776
 
16557
- //# sourceMappingURL=cli-BS67FTk8.mjs.map
16777
+ //# sourceMappingURL=cli-CRhzHqMB.mjs.map