@kernelonpanic/kitcode 1.3.1 → 1.3.2

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/index.js CHANGED
@@ -62,8 +62,7 @@ var themeSchema = z.object({
62
62
  var langSchema = z.enum(["en", "ru"]);
63
63
  var budgetSchema = z.object({
64
64
  maxTokensPerTurn: z.number().int().min(0).max(1e7).default(1e7),
65
- maxCostUsdPerTurn: z.number().nonnegative().max(1e3).default(0),
66
- maxSubagentsPerTurn: z.number().int().min(0).max(16).default(3)
65
+ maxCostUsdPerTurn: z.number().nonnegative().max(1e3).default(0)
67
66
  });
68
67
  var diagnosticsSchema = z.object({
69
68
  autoRun: z.boolean().default(true),
@@ -79,8 +78,7 @@ var configSchema = z.object({
79
78
  maxTokens: z.number().int().positive().max(2e5).default(64e3),
80
79
  budget: budgetSchema.default({
81
80
  maxTokensPerTurn: 1e7,
82
- maxCostUsdPerTurn: 0,
83
- maxSubagentsPerTurn: 3
81
+ maxCostUsdPerTurn: 0
84
82
  }),
85
83
  diagnostics: diagnosticsSchema.default({ autoRun: true, commands: [] }),
86
84
  providers: z.record(providerIdSchema, providerConfigSchema).default({}),
@@ -1814,8 +1812,23 @@ var MAX_PAUSE_RESUMES = 5;
1814
1812
  var MAX_RETRIES2 = 3;
1815
1813
  var INITIAL_BACKOFF_MS = 1e3;
1816
1814
  var MAX_TURN_STEPS = 64;
1817
- var MAX_TOOL_CALLS_PER_STEP = 32;
1815
+ function createExecutionLane() {
1816
+ let tail2 = Promise.resolve();
1817
+ return {
1818
+ run(operation) {
1819
+ const result = tail2.then(operation);
1820
+ tail2 = result.then(() => void 0, () => void 0);
1821
+ return result;
1822
+ }
1823
+ };
1824
+ }
1818
1825
  async function runTurn(cfg, history, hooks, signal) {
1826
+ const permissionLane = cfg.permissionLane ?? createExecutionLane();
1827
+ const originalHooks = hooks;
1828
+ hooks = {
1829
+ ...hooks,
1830
+ requestPermission: (request) => permissionLane.run(async () => signal.aborted ? "deny" : originalHooks.requestPermission(request))
1831
+ };
1819
1832
  let messages = [...history];
1820
1833
  let pauses = 0;
1821
1834
  let steps = 0;
@@ -1833,40 +1846,35 @@ async function runTurn(cfg, history, hooks, signal) {
1833
1846
  hooks.onEvent({ type: "turn_end", stopReason: "max_tokens" });
1834
1847
  return messages;
1835
1848
  }
1836
- const budgetDecision = cfg.budget?.beforeRequest({
1837
- modelRef: cfg.modelRef,
1838
- maxOutputTokens: cfg.maxTokens,
1839
- estimatedInputTokens: estimateRequestTokens(cfg, messages)
1840
- });
1841
- if (budgetDecision && !budgetDecision.allowed) {
1842
- hooks.onEvent({ type: "notice", level: "warn", text: budgetDecision.reason });
1843
- hooks.onEvent({ type: "turn_end", stopReason: "max_tokens" });
1844
- return messages;
1845
- }
1846
- hooks.onEvent({ type: "turn_start" });
1847
- const outcome = await consumeStream(
1848
- cfg,
1849
- messages,
1850
- hooks,
1851
- signal,
1852
- budgetDecision?.maxOutputTokens ?? cfg.maxTokens
1853
- );
1849
+ const request = async () => {
1850
+ if (signal.aborted) return { content: [], stopReason: "aborted" };
1851
+ const budgetDecision = cfg.budget?.beforeRequest({
1852
+ modelRef: cfg.modelRef,
1853
+ maxOutputTokens: cfg.maxTokens,
1854
+ estimatedInputTokens: estimateRequestTokens(cfg, messages)
1855
+ });
1856
+ if (budgetDecision && !budgetDecision.allowed) {
1857
+ hooks.onEvent({ type: "notice", level: "warn", text: budgetDecision.reason });
1858
+ hooks.onEvent({ type: "turn_end", stopReason: "max_tokens" });
1859
+ return null;
1860
+ }
1861
+ hooks.onEvent({ type: "turn_start" });
1862
+ return consumeStream(
1863
+ cfg,
1864
+ messages,
1865
+ hooks,
1866
+ signal,
1867
+ budgetDecision?.maxOutputTokens ?? cfg.maxTokens
1868
+ );
1869
+ };
1870
+ const outcome = await (cfg.modelRequestLane?.run(request) ?? request());
1871
+ if (!outcome) return messages;
1854
1872
  messages.push({ role: "assistant", content: outcome.content });
1855
1873
  if (outcome.stopReason === "aborted") {
1856
1874
  hooks.onEvent({ type: "turn_end", stopReason: "aborted" });
1857
1875
  return sanitizeHistory(messages);
1858
1876
  }
1859
1877
  if (outcome.stopReason === "tool_use") {
1860
- const callCount = outcome.content.filter((block) => block.type === "tool_use").length;
1861
- if (callCount > MAX_TOOL_CALLS_PER_STEP) {
1862
- hooks.onEvent({
1863
- type: "notice",
1864
- level: "warn",
1865
- text: `The model returned ${callCount} tool calls at once; the limit is ${MAX_TOOL_CALLS_PER_STEP}. None were run. Ask it to split the work into smaller batches.`
1866
- });
1867
- hooks.onEvent({ type: "turn_end", stopReason: "max_tokens" });
1868
- return sanitizeHistory(messages);
1869
- }
1870
1878
  const results = await runToolCalls(cfg, outcome.content, hooks, signal);
1871
1879
  if (results.length > 0) messages.push({ role: "user", content: results });
1872
1880
  if (signal.aborted) {
@@ -2054,10 +2062,32 @@ function estimateRequestTokens(cfg, messages) {
2054
2062
  async function runToolCalls(cfg, content, hooks, signal) {
2055
2063
  const calls = content.filter((block) => block.type === "tool_use");
2056
2064
  const results = [];
2057
- for (const call of calls) results.push(await runOneCall(cfg, call, hooks, signal));
2065
+ const run = (call) => {
2066
+ const operation = () => runOneCall(cfg, call, hooks, signal);
2067
+ return call.name !== "task" && cfg.executionLane ? cfg.executionLane.run(operation) : operation();
2068
+ };
2069
+ for (let index = 0; index < calls.length; ) {
2070
+ const call = calls[index];
2071
+ if (call.name !== "task") {
2072
+ results.push(await run(call));
2073
+ index += 1;
2074
+ continue;
2075
+ }
2076
+ const batch = [];
2077
+ while (calls[index]?.name === "task") {
2078
+ batch.push(run(calls[index]));
2079
+ index += 1;
2080
+ }
2081
+ const settled = await Promise.allSettled(batch);
2082
+ for (const result of settled) {
2083
+ if (result.status === "rejected") throw result.reason;
2084
+ results.push(result.value);
2085
+ }
2086
+ }
2058
2087
  return results;
2059
2088
  }
2060
2089
  async function runOneCall(cfg, call, hooks, signal) {
2090
+ if (signal.aborted) return toolError(call.id, "Interrupted by the user before this tool ran.");
2061
2091
  const tool = cfg.tools.get(call.name);
2062
2092
  if (!tool) {
2063
2093
  return toolError(call.id, `Unknown tool "${call.name}". Use only the tools provided.`);
@@ -2076,26 +2106,31 @@ async function runOneCall(cfg, call, hooks, signal) {
2076
2106
  }) !== "deny"
2077
2107
  };
2078
2108
  const requestedPermission = requestedPermissionFor(tool, call.input, ctx);
2079
- const gate = cfg.permissions.decide(tool, requestedPermission);
2080
- if (gate === "deny") {
2081
- return toolError(
2082
- call.id,
2083
- cfg.permissions.denyReason?.(tool, requestedPermission) ?? `Tool "${tool.name}" is disabled by the user's configuration. Do not retry it; find another way or ask.`
2084
- );
2085
- }
2086
- if (gate === "ask") {
2087
- const decision = await hooks.requestPermission({
2088
- toolName: tool.name,
2089
- summary: summary2,
2090
- input: call.input,
2091
- display: await previewCall(tool, call.input, ctx),
2092
- allowAlways: canAlwaysApprove(tool)
2093
- });
2094
- if (decision === "deny") {
2095
- return toolError(call.id, "The user declined this call. Do not retry it; ask how to proceed.");
2109
+ const permission = async () => {
2110
+ const gate = cfg.permissions.decide(tool, requestedPermission);
2111
+ if (gate === "deny") {
2112
+ return toolError(
2113
+ call.id,
2114
+ cfg.permissions.denyReason?.(tool, requestedPermission) ?? `Tool "${tool.name}" is disabled by the user's configuration. Do not retry it; find another way or ask.`
2115
+ );
2096
2116
  }
2097
- if (decision === "always") cfg.permissions.grantForSession(tool.name);
2098
- }
2117
+ if (gate === "ask") {
2118
+ const decision = await hooks.requestPermission({
2119
+ toolName: tool.name,
2120
+ summary: summary2,
2121
+ input: call.input,
2122
+ display: await previewCall(tool, call.input, ctx),
2123
+ allowAlways: canAlwaysApprove(tool)
2124
+ });
2125
+ if (decision === "deny") {
2126
+ return toolError(call.id, "The user declined this call. Do not retry it; ask how to proceed.");
2127
+ }
2128
+ if (decision === "always") cfg.permissions.grantForSession(tool.name);
2129
+ }
2130
+ return void 0;
2131
+ };
2132
+ const permissionResult = await permission();
2133
+ if (permissionResult) return permissionResult;
2099
2134
  if (signal.aborted) {
2100
2135
  return toolError(call.id, "Interrupted by the user before this tool ran.");
2101
2136
  }
@@ -2191,6 +2226,7 @@ function createTurnBudget(limits, resolvePricing = pricingFor) {
2191
2226
  costUsd
2192
2227
  });
2193
2228
  return {
2229
+ hasLimits: limits.maxTokensPerTurn > 0 || limits.maxCostUsdPerTurn > 0,
2194
2230
  beforeRequest(request) {
2195
2231
  const current = snapshot();
2196
2232
  if (limits.maxTokensPerTurn > 0 && current.tokens >= limits.maxTokensPerTurn) {
@@ -3667,6 +3703,9 @@ var SUBAGENT_SYSTEM = [
3667
3703
  'Your final message is the return value \u2014 it is the only thing the caller ever sees. End with the answer itself: the findings, the file paths, the conclusion. Never end with "I will now..." or a recap of the steps you took. Carry over every concrete detail the caller needs (paths, names, line numbers) and leave out the narration of how you found them.'
3668
3704
  ].join("\n");
3669
3705
  function createSubagentRunner(makeConfig, tools) {
3706
+ const permissionLane = createExecutionLane();
3707
+ const executionLane = createExecutionLane();
3708
+ const modelRequestLane = createExecutionLane();
3670
3709
  return {
3671
3710
  async run(request) {
3672
3711
  if (request.signal.aborted) return SUBAGENT_CANCELLED;
@@ -3674,6 +3713,9 @@ function createSubagentRunner(makeConfig, tools) {
3674
3713
  const base = makeConfig(SUBAGENT_SYSTEM, tools);
3675
3714
  const cfg = {
3676
3715
  ...base,
3716
+ permissionLane,
3717
+ executionLane,
3718
+ modelRequestLane: base.budget?.hasLimits ? modelRequestLane : void 0,
3677
3719
  provider: {
3678
3720
  id: base.provider.id,
3679
3721
  kind: base.provider.kind,
@@ -3898,7 +3940,7 @@ function clip(value) {
3898
3940
  // package.json
3899
3941
  var package_default = {
3900
3942
  name: "@kernelonpanic/kitcode",
3901
- version: "1.3.1",
3943
+ version: "1.3.2",
3902
3944
  description: "Terminal coding agent with a config you never have to write by hand",
3903
3945
  type: "module",
3904
3946
  license: "MIT",
@@ -3966,7 +4008,7 @@ var package_default = {
3966
4008
 
3967
4009
  // src/version.ts
3968
4010
  var KITCODE_VERSION = package_default.version;
3969
- var KITCODE_COMMIT = true ? "4320a2bd340f59e42397dd4afbfde0051ddfeb27" : "development";
4011
+ var KITCODE_COMMIT = true ? "993d55335487da4915fa49660911ba6e69aa3685" : "development";
3970
4012
 
3971
4013
  // src/mcp/client.ts
3972
4014
  var clientInfo = { name: "kitcode", version: KITCODE_VERSION };
@@ -6309,16 +6351,15 @@ function unknownSkill(name, available) {
6309
6351
  // src/tools/task.ts
6310
6352
  import { randomBytes as randomBytes3 } from "crypto";
6311
6353
  var MAX_PROGRESS_LINES = 10;
6312
- var MAX_SUBAGENTS_PER_TURN = 3;
6313
6354
  var DESCRIPTION = [
6314
6355
  "Hand one self-contained sub-task to a subagent that works in its own fresh context and reports back a single summary.",
6315
6356
  "Use it when the task is genuinely independent and its intermediate output would be long or noisy: a wide search across the repository, reading many files to answer one question, tracing where something is used.",
6316
6357
  "Do not use it for work that is a couple of tool calls, and do not use it when you already have what you need \u2014 call the tools yourself instead.",
6317
6358
  'The subagent shares this filesystem and working directory but has NO memory of this conversation and cannot ask you anything, so "prompt" must stand alone: the goal, the constraints, and exactly what to report back.',
6359
+ "For multiple independent tasks, issue consecutive task calls in the same response so they can run concurrently. Do not delegate overlapping file edits.",
6318
6360
  "It runs to completion and you receive only its final message."
6319
6361
  ].join(" ");
6320
- function createTaskTool(runner, maxSubagents = MAX_SUBAGENTS_PER_TURN) {
6321
- const spawnsPerTurn = /* @__PURE__ */ new WeakMap();
6362
+ function createTaskTool(runner) {
6322
6363
  return {
6323
6364
  name: TASK_TOOL_NAME,
6324
6365
  description: DESCRIPTION,
@@ -6340,14 +6381,6 @@ function createTaskTool(runner, maxSubagents = MAX_SUBAGENTS_PER_TURN) {
6340
6381
  if (instruction === "") {
6341
6382
  return { content: "task needs a prompt: the full instruction for the subagent.", isError: true };
6342
6383
  }
6343
- const spawned = (spawnsPerTurn.get(ctx.signal) ?? 0) + 1;
6344
- spawnsPerTurn.set(ctx.signal, spawned);
6345
- if (spawned > maxSubagents) {
6346
- return {
6347
- content: `Already delegated ${maxSubagents} subagents while answering this message, which is the limit. Do the remaining work yourself with the tools you have.`,
6348
- isError: true
6349
- };
6350
- }
6351
6384
  const progress = [];
6352
6385
  const subagentId = `sa${randomBytes3(4).toString("hex")}`;
6353
6386
  const description = field(input, "description") || "subagent";
@@ -6675,7 +6708,7 @@ async function boot(options) {
6675
6708
  }
6676
6709
  };
6677
6710
  tools.register([
6678
- createTaskTool(wrappedRunner, config.budget.maxSubagentsPerTurn)
6711
+ createTaskTool(wrappedRunner)
6679
6712
  ]);
6680
6713
  let persistQueue = Promise.resolve();
6681
6714
  let configQueue = Promise.resolve();
@@ -8815,9 +8848,6 @@ var PromptInput = memo(function PromptInput2({
8815
8848
  Box8,
8816
8849
  {
8817
8850
  width: "100%",
8818
- borderStyle: "round",
8819
- borderColor: disabled ? theme.warn : theme.accent,
8820
- borderDimColor: disabled,
8821
8851
  paddingX: 1,
8822
8852
  children: [
8823
8853
  /* @__PURE__ */ jsx8(Text8, { color: disabled ? "gray" : theme.accent, children: "\u203A " }),
@@ -9153,9 +9183,9 @@ import { createContext as createContext3, Fragment as Fragment4, useContext as u
9153
9183
  import stringWidth from "string-width";
9154
9184
  import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
9155
9185
  var DimContext = createContext3(false);
9156
- function Text10({ dimColor, ...props }) {
9186
+ function Text10({ dimColor, color, ...props }) {
9157
9187
  const dim = useContext3(DimContext);
9158
- return /* @__PURE__ */ jsx11(InkText, { ...props, dimColor: dimColor ?? dim });
9188
+ return /* @__PURE__ */ jsx11(InkText, { ...props, color: dim ? void 0 : color, dimColor: dimColor ?? dim });
9159
9189
  }
9160
9190
  function Markdown({ children, dim = false }) {
9161
9191
  const blocks = useMemo3(() => extractBlocks(children), [children]);
@@ -10344,7 +10374,6 @@ function App({
10344
10374
  const command = (firstSpace === -1 ? body : body.slice(0, firstSpace)).toLowerCase();
10345
10375
  const rawRest = firstSpace === -1 ? "" : body.slice(firstSpace).trim();
10346
10376
  const rest = rawRest ? rawRest.split(/\s+/) : [];
10347
- setInput("");
10348
10377
  switch (command) {
10349
10378
  case "help":
10350
10379
  notice(
@@ -11302,7 +11331,9 @@ Rename the file if you want a different name.`);
11302
11331
  const text = raw.trim();
11303
11332
  const submitSlash = () => {
11304
11333
  if (!detachedInput) setInput("");
11305
- if (busyRef.current || slashPendingRef.current > 0) {
11334
+ const command = text.slice(1).trim().split(/\s+/)[0]?.toLowerCase();
11335
+ const liveCommand = ["help", "usage", "subagents", "config", "exit", "quit"].includes(command ?? "");
11336
+ if (busyRef.current && !liveCommand || slashPendingRef.current > 0) {
11306
11337
  queueRef.current = [...queueRef.current, { kind: "command", line: text }];
11307
11338
  setPendingCount(queueRef.current.length);
11308
11339
  return;