beecork 2.8.2 → 2.8.3

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.
Files changed (2) hide show
  1. package/dist/index.js +103 -7
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -60,6 +60,13 @@ function normalizeEffort(raw) {
60
60
  const v = (raw ?? "").trim().toLowerCase();
61
61
  return EFFORTS.includes(v) ? v : void 0;
62
62
  }
63
+ function normalizeProviderSort(raw) {
64
+ if (raw == null) return "latency";
65
+ const v = raw.trim().toLowerCase();
66
+ if (v === "latency" || v === "price" || v === "throughput") return v;
67
+ if (["", "off", "none", "default", "0", "false", "no"].includes(v)) return "";
68
+ return "latency";
69
+ }
63
70
  function parseExtra(raw) {
64
71
  if (!raw || !raw.trim()) return {};
65
72
  try {
@@ -95,6 +102,10 @@ var config = {
95
102
  // transient API failures
96
103
  apiTimeoutMs: num("API_TIMEOUT_MS", 18e4),
97
104
  // per-attempt model-call timeout (generous for reasoning models)
105
+ // Run a batch's independent, read-only tool calls (read_file/search/list_dir/web_fetch/…) CONCURRENTLY
106
+ // instead of one-at-a-time — the model is told to batch independent calls, so this cashes in the win
107
+ // (N web_fetches take ~1× instead of N×). Mutating/approval/interactive tools always stay serial.
108
+ parallelTools: !["0", "false", "off", "no"].includes((process.env.PARALLEL_TOOLS ?? "").trim().toLowerCase()),
98
109
  // Tool operational limits
99
110
  execTimeoutMs: num("EXEC_TIMEOUT_MS", 3e4),
100
111
  // run_bash command timeout
@@ -115,6 +126,11 @@ var config = {
115
126
  // startup default; changed live via /effort
116
127
  openRouterExtra: parseExtra(process.env.OPENROUTER_EXTRA),
117
128
  // advanced: extra request-body params (sampling, provider routing, …)
129
+ // Provider routing (overridable by a `provider` block in OPENROUTER_EXTRA). The same model is served
130
+ // by many OpenRouter backends with very different time-to-first-token; "latency" (the default) routes
131
+ // to the fastest-responding one so replies start streaming right away instead of stalling on a slow
132
+ // provider. Set OPENROUTER_PROVIDER_SORT=off to fall back to OpenRouter's default load-balanced routing.
133
+ providerSort: normalizeProviderSort(process.env.OPENROUTER_PROVIDER_SORT),
118
134
  // Background tasks (run_bash background:true → check_task / stop_task)
119
135
  maxBackgroundTasks: num("MAX_BG_TASKS", 5),
120
136
  // per-session cap on concurrent background commands
@@ -1072,6 +1088,9 @@ function loadCatalog() {
1072
1088
  }).catch(() => {
1073
1089
  });
1074
1090
  }
1091
+ function primeCatalog() {
1092
+ loadCatalog();
1093
+ }
1075
1094
  var baseId = (slug) => slug.split(":")[0];
1076
1095
  function shouldSendReasoning(model) {
1077
1096
  loadCatalog();
@@ -2815,7 +2834,7 @@ function parseSSELine(line) {
2815
2834
  return out3;
2816
2835
  }
2817
2836
  function buildRequestBody(opts) {
2818
- const { model, messages, includeTools, effort, reasoningSupported, extra, tools } = opts;
2837
+ const { model, messages, includeTools, effort, reasoningSupported, extra, tools, providerSort } = opts;
2819
2838
  const body = { ...extra };
2820
2839
  body.model = model;
2821
2840
  body.messages = messages;
@@ -2824,6 +2843,7 @@ function buildRequestBody(opts) {
2824
2843
  if (reasoningSupported && !("reasoning" in extra)) {
2825
2844
  body.reasoning = effort === "off" ? { enabled: false } : { effort };
2826
2845
  }
2846
+ if (providerSort && !("provider" in extra)) body.provider = { sort: providerSort };
2827
2847
  return body;
2828
2848
  }
2829
2849
  function pruneReasoningForSend(messages) {
@@ -2849,7 +2869,8 @@ async function callModel(messages, includeTools = true, signal, opts) {
2849
2869
  effort: state.reasoningEffort,
2850
2870
  reasoningSupported: shouldSendReasoning(state.model),
2851
2871
  extra: config.openRouterExtra,
2852
- tools: opts?.tools
2872
+ tools: opts?.tools,
2873
+ providerSort: config.providerSort
2853
2874
  });
2854
2875
  const tries = config.retryAttempts;
2855
2876
  for (let attempt = 1; attempt <= tries; attempt++) {
@@ -3428,7 +3449,69 @@ async function handleAskUser(args) {
3428
3449
  if (choice) console.log(color.green(` \u2192 ${stripControl(choice.label)}`) + "\n");
3429
3450
  return askUserMessage(question, choice, true);
3430
3451
  }
3431
- async function handleToolCall(call, messages, step, deps) {
3452
+ var PARALLEL_SAFE = /* @__PURE__ */ new Set([
3453
+ "read_file",
3454
+ "search",
3455
+ "list_dir",
3456
+ "read_skill",
3457
+ "web_fetch",
3458
+ "web_search",
3459
+ "read_dev_signals",
3460
+ "check_task"
3461
+ ]);
3462
+ function lastMutationIndex(calls) {
3463
+ return calls.reduce((acc, c, i) => c.function.name === "write_file" || c.function.name === "edit_file" ? i : acc, -1);
3464
+ }
3465
+ function isParallelSafe(call, deps) {
3466
+ if (!config.parallelTools) return false;
3467
+ if (!PARALLEL_SAFE.has(call.function.name)) return false;
3468
+ const tool = toolsByName.get(call.function.name);
3469
+ if (!tool) return false;
3470
+ let args;
3471
+ try {
3472
+ args = JSON.parse(call.function.arguments);
3473
+ } catch {
3474
+ return false;
3475
+ }
3476
+ const decision = decideApproval(tool, args, {
3477
+ mode: state.mode,
3478
+ autoApprove: config.autoApprove,
3479
+ approvedTools: deps.approvedTools,
3480
+ approvedGuardKeys: deps.approvedGuardKeys,
3481
+ toolName: call.function.name,
3482
+ dangerouslySkip: config.dangerouslySkipPermissions
3483
+ });
3484
+ return decision.action === "run";
3485
+ }
3486
+ async function runReadOnlyBatch(block, messages, step, deps) {
3487
+ const { callCounts, signal } = deps;
3488
+ const parts = await Promise.all(block.map(async (call) => {
3489
+ const sig = `${call.function.name}:${call.function.arguments}`;
3490
+ const seen = (callCounts.get(sig) ?? 0) + 1;
3491
+ callCounts.set(sig, seen);
3492
+ if (seen >= config.loopRepeatLimit) {
3493
+ return { call, out: color.yellow(` \u21B3 skipped \u2014 repeated identical call ${seen}\xD7`), content: "You have already called this exact tool with these exact arguments several times; it is not making progress. Stop repeating it \u2014 try a different approach or give your final answer." };
3494
+ }
3495
+ let callArgs = {};
3496
+ try {
3497
+ callArgs = JSON.parse(call.function.arguments);
3498
+ } catch {
3499
+ }
3500
+ if (config.traceFile) trace.push({ tool: call.function.name, args: call.function.arguments, step });
3501
+ let result = await runTool(call, signal);
3502
+ const summary = summarizeResult(call.function.name, callArgs, result);
3503
+ if (result.length > config.maxToolResultChars) {
3504
+ result = result.slice(0, config.maxToolResultChars) + `
3505
+ \u2026[truncated ${result.length - config.maxToolResultChars} chars]`;
3506
+ }
3507
+ return { call, out: " " + renderToolCall(call.function.name, callArgs) + summary, content: result };
3508
+ }));
3509
+ for (const p of parts) {
3510
+ process.stdout.write(p.out + "\n");
3511
+ messages.push({ role: "tool", tool_call_id: p.call.id, content: p.content });
3512
+ }
3513
+ }
3514
+ async function handleToolCall(call, messages, step, deps, verifyThisCall = true) {
3432
3515
  const { approvedTools, approvedGuardKeys, callCounts, ask, signal } = deps;
3433
3516
  const pushTool = (content) => messages.push({ role: "tool", tool_call_id: call.id, content });
3434
3517
  const sig = `${call.function.name}:${call.function.arguments}`;
@@ -3503,7 +3586,7 @@ async function handleToolCall(call, messages, step, deps) {
3503
3586
  }
3504
3587
  const summary = summarizeResult(call.function.name, callArgs, result);
3505
3588
  let verifyOut = "";
3506
- if (config.verifyCommand && (call.function.name === "write_file" || call.function.name === "edit_file")) {
3589
+ if (verifyThisCall && config.verifyCommand && (call.function.name === "write_file" || call.function.name === "edit_file")) {
3507
3590
  verifyOut = await runVerify(signal);
3508
3591
  result += `
3509
3592
 
@@ -3560,9 +3643,21 @@ async function runTurn(messages, userInput, ask, approvedTools, approvedGuardKey
3560
3643
  break;
3561
3644
  }
3562
3645
  if (message.tool_calls && message.tool_calls.length > 0) {
3563
- for (const call of message.tool_calls) {
3564
- if (signal?.aborted) break;
3565
- await handleToolCall(call, messages, step, deps);
3646
+ const calls = message.tool_calls;
3647
+ const lastMutationIdx = lastMutationIndex(calls);
3648
+ let i = 0;
3649
+ while (i < calls.length && !signal?.aborted) {
3650
+ if (isParallelSafe(calls[i], deps)) {
3651
+ let j = i + 1;
3652
+ while (j < calls.length && isParallelSafe(calls[j], deps)) j++;
3653
+ if (j - i > 1) {
3654
+ await runReadOnlyBatch(calls.slice(i, j), messages, step, deps);
3655
+ i = j;
3656
+ continue;
3657
+ }
3658
+ }
3659
+ await handleToolCall(calls[i], messages, step, deps, i === lastMutationIdx);
3660
+ i++;
3566
3661
  }
3567
3662
  } else {
3568
3663
  answered = !steering2?.length;
@@ -4414,6 +4509,7 @@ ${instr.trusted}`;
4414
4509
  process.exit(1);
4415
4510
  }
4416
4511
  state.apiKey = apiKey;
4512
+ primeCatalog();
4417
4513
  const ask = tty ? (q) => readChoice(q) : (q) => rl.question(q);
4418
4514
  let activeTurn = null;
4419
4515
  let userTurns = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beecork",
3
- "version": "2.8.2",
3
+ "version": "2.8.3",
4
4
  "description": "beecork — a from-scratch CLI coding agent: multi-model (OpenRouter), BYOK, path-confined tools, built part by part.",
5
5
  "type": "module",
6
6
  "bin": {