@kody-ade/kody-engine 0.4.152 → 0.4.153

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/bin/kody.js +90 -57
  2. package/package.json +1 -1
package/dist/bin/kody.js CHANGED
@@ -1061,7 +1061,7 @@ var init_loadPriorArt = __esm({
1061
1061
  // package.json
1062
1062
  var package_default = {
1063
1063
  name: "@kody-ade/kody-engine",
1064
- version: "0.4.152",
1064
+ version: "0.4.153",
1065
1065
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
1066
1066
  license: "MIT",
1067
1067
  type: "module",
@@ -1952,6 +1952,14 @@ async function runAgent(opts) {
1952
1952
  `[kody agent] transient connection error (attempt ${attempt + 1}/${MAX_CONNECTION_RETRIES + 1}); retrying in ${Math.round(delayMs / 1e3)}s: ${errorMessage}
1953
1953
  `
1954
1954
  );
1955
+ if (opts.ensureBackend) {
1956
+ try {
1957
+ await opts.ensureBackend();
1958
+ } catch (e) {
1959
+ process.stderr.write(`[kody agent] backend recovery failed: ${e instanceof Error ? e.message : String(e)}
1960
+ `);
1961
+ }
1962
+ }
1955
1963
  await new Promise((r) => setTimeout(r, delayMs));
1956
1964
  }
1957
1965
  const submittedState = getSubmitted?.();
@@ -3850,65 +3858,85 @@ function generateLitellmConfigYaml(model) {
3850
3858
  ""
3851
3859
  ].join("\n");
3852
3860
  }
3853
- async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL) {
3854
- if (!needsLitellmProxy(model)) return null;
3855
- if (await checkLitellmHealth(url)) {
3856
- return { url, kill: () => {
3857
- } };
3858
- }
3859
- let cmd = "litellm";
3861
+ function resolveLitellmCommand() {
3860
3862
  try {
3861
3863
  execFileSync4("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
3864
+ return "litellm";
3862
3865
  } catch {
3863
3866
  try {
3864
3867
  execFileSync4("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
3865
- cmd = "python3";
3868
+ return "python3";
3866
3869
  } catch {
3867
3870
  throw new Error("litellm not installed \u2014 run: pip install 'litellm[proxy]'");
3868
3871
  }
3869
3872
  }
3870
- const configPath = path13.join(os2.tmpdir(), `kody-litellm-${Date.now()}.yaml`);
3871
- fs14.writeFileSync(configPath, generateLitellmConfigYaml(model));
3873
+ }
3874
+ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL) {
3875
+ if (!needsLitellmProxy(model)) return null;
3876
+ const cmd = resolveLitellmCommand();
3872
3877
  const portMatch = url.match(/:(\d+)/);
3873
3878
  const port = portMatch ? portMatch[1] : "4000";
3874
- const args = cmd === "litellm" ? ["--config", configPath, "--port", port] : ["-m", "litellm", "--config", configPath, "--port", port];
3875
- const dotenvVars = readDotenvApiKeys(projectDir);
3876
- const logPath = path13.join(os2.tmpdir(), `kody-litellm-${Date.now()}.log`);
3877
- const outFd = fs14.openSync(logPath, "w");
3878
- const child = spawn3(cmd, args, {
3879
- stdio: ["ignore", outFd, outFd],
3880
- detached: true,
3881
- env: stripBlockingEnv({ ...process.env, ...dotenvVars })
3882
- });
3883
- fs14.closeSync(outFd);
3884
- const timeoutMs = resolveLitellmTimeoutMs();
3885
- const deadline = Date.now() + timeoutMs;
3886
- while (Date.now() < deadline) {
3887
- await new Promise((r) => setTimeout(r, LITELLM_HEALTH_POLL_INTERVAL_MS));
3888
- if (await checkLitellmHealth(url)) {
3889
- return {
3890
- url,
3891
- kill: () => {
3892
- try {
3893
- child.kill();
3894
- } catch {
3895
- }
3896
- }
3897
- };
3879
+ const childEnv = stripBlockingEnv({ ...process.env, ...readDotenvApiKeys(projectDir) });
3880
+ let child;
3881
+ let logPath;
3882
+ const spawnProxy = () => {
3883
+ const configPath = path13.join(os2.tmpdir(), `kody-litellm-${Date.now()}.yaml`);
3884
+ fs14.writeFileSync(configPath, generateLitellmConfigYaml(model));
3885
+ const args = cmd === "litellm" ? ["--config", configPath, "--port", port] : ["-m", "litellm", "--config", configPath, "--port", port];
3886
+ const nextLogPath = path13.join(os2.tmpdir(), `kody-litellm-${Date.now()}.log`);
3887
+ const outFd = fs14.openSync(nextLogPath, "w");
3888
+ child = spawn3(cmd, args, { stdio: ["ignore", outFd, outFd], detached: true, env: childEnv });
3889
+ fs14.closeSync(outFd);
3890
+ logPath = nextLogPath;
3891
+ };
3892
+ const waitForHealth = async () => {
3893
+ const deadline = Date.now() + resolveLitellmTimeoutMs();
3894
+ while (Date.now() < deadline) {
3895
+ await new Promise((r) => setTimeout(r, LITELLM_HEALTH_POLL_INTERVAL_MS));
3896
+ if (await checkLitellmHealth(url)) return true;
3898
3897
  }
3898
+ return false;
3899
+ };
3900
+ const readLogTail = () => {
3901
+ if (!logPath) return "";
3902
+ try {
3903
+ return fs14.readFileSync(logPath, "utf-8").slice(-2e3);
3904
+ } catch {
3905
+ return "";
3906
+ }
3907
+ };
3908
+ const killChild = () => {
3909
+ try {
3910
+ child?.kill();
3911
+ } catch {
3912
+ }
3913
+ };
3914
+ const ensureHealthy = async () => {
3915
+ if (await checkLitellmHealth(url)) return true;
3916
+ const tail = readLogTail();
3917
+ process.stderr.write(
3918
+ `[kody litellm] proxy unreachable mid-run; restarting.${tail ? ` Last log:
3919
+ ${tail}
3920
+ ` : "\n"}`
3921
+ );
3922
+ killChild();
3923
+ spawnProxy();
3924
+ return waitForHealth();
3925
+ };
3926
+ if (await checkLitellmHealth(url)) {
3927
+ return { url, kill: killChild, ensureHealthy };
3899
3928
  }
3900
- let logTail = "";
3901
- try {
3902
- logTail = fs14.readFileSync(logPath, "utf-8").slice(-2e3);
3903
- } catch {
3904
- }
3905
- try {
3906
- child.kill();
3907
- } catch {
3929
+ spawnProxy();
3930
+ if (!await waitForHealth()) {
3931
+ const tail = readLogTail();
3932
+ killChild();
3933
+ const seconds = Math.round(resolveLitellmTimeoutMs() / 1e3);
3934
+ throw new Error(
3935
+ `LiteLLM proxy failed to start within ${seconds}s (KODY_LITELLM_TIMEOUT_SEC overrides). Log tail:
3936
+ ${tail}`
3937
+ );
3908
3938
  }
3909
- const seconds = Math.round(timeoutMs / 1e3);
3910
- throw new Error(`LiteLLM proxy failed to start within ${seconds}s (KODY_LITELLM_TIMEOUT_SEC overrides). Log tail:
3911
- ${logTail}`);
3939
+ return { url, kill: killChild, ensureHealthy };
3912
3940
  }
3913
3941
  function readDotenvApiKeys(projectDir) {
3914
3942
  const dotenvPath = path13.join(projectDir, ".env");
@@ -13106,7 +13134,10 @@ async function runExecutable(profileName, input) {
13106
13134
  try {
13107
13135
  model = parseProviderModel(modelSpec);
13108
13136
  } catch (err) {
13109
- return finishAndEnd({ exitCode: 99, reason: `agent.model invalid: ${err instanceof Error ? err.message : String(err)}` });
13137
+ return finishAndEnd({
13138
+ exitCode: 99,
13139
+ reason: `agent.model invalid: ${err instanceof Error ? err.message : String(err)}`
13140
+ });
13110
13141
  }
13111
13142
  let litellm = null;
13112
13143
  try {
@@ -13150,11 +13181,15 @@ async function runExecutable(profileName, input) {
13150
13181
  const syntheticPath = ctx.data.syntheticPluginPath;
13151
13182
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
13152
13183
  const agents = loadSubagents(profile);
13184
+ const lm = litellm;
13153
13185
  return runAgent({
13154
13186
  prompt,
13155
13187
  model,
13156
13188
  cwd: input.cwd,
13157
- litellmUrl: litellm?.url ?? null,
13189
+ litellmUrl: lm?.url ?? null,
13190
+ // On a connection drop mid-run, restart the (possibly crashed) proxy
13191
+ // before the agent retries. No-op for direct-Anthropic runs (lm null).
13192
+ ensureBackend: lm ? () => lm.ensureHealthy().then(() => void 0) : void 0,
13158
13193
  verbose: input.verbose,
13159
13194
  quiet: input.quiet,
13160
13195
  ndjsonDir,
@@ -13224,7 +13259,10 @@ async function runExecutable(profileName, input) {
13224
13259
  } else if (!ctx.skipAgent) {
13225
13260
  const prompt = ctx.data.prompt;
13226
13261
  if (!prompt) {
13227
- return finishAndEnd({ exitCode: 99, reason: "composePrompt did not produce a prompt (ctx.data.prompt missing)" });
13262
+ return finishAndEnd({
13263
+ exitCode: 99,
13264
+ reason: "composePrompt did not produce a prompt (ctx.data.prompt missing)"
13265
+ });
13228
13266
  }
13229
13267
  emitEvent(input.cwd, { executable: profileName, kind: "agent_start" });
13230
13268
  agentResult = await invokeAgent(prompt);
@@ -13285,10 +13323,7 @@ async function runExecutable(profileName, input) {
13285
13323
  const runId = resolveRunId2();
13286
13324
  const dir = pathMod.join(input.cwd, ".kody", "runs", runId, "crashes");
13287
13325
  fsMod.mkdirSync(dir, { recursive: true });
13288
- const file = pathMod.join(
13289
- dir,
13290
- `${label.replace(/[^a-zA-Z0-9_-]/g, "_")}-${Date.now()}.json`
13291
- );
13326
+ const file = pathMod.join(dir, `${label.replace(/[^a-zA-Z0-9_-]/g, "_")}-${Date.now()}.json`);
13292
13327
  fsMod.writeFileSync(
13293
13328
  file,
13294
13329
  JSON.stringify(
@@ -13328,10 +13363,8 @@ async function runExecutable(profileName, input) {
13328
13363
  try {
13329
13364
  const missing2 = verifyTaskArtifacts(taskArtifacts.absDir);
13330
13365
  if (missing2.length > 0) {
13331
- process.stderr.write(
13332
- `[task-artifacts] task ${taskArtifacts.taskId} missing: ${missing2.join(", ")}
13333
- `
13334
- );
13366
+ process.stderr.write(`[task-artifacts] task ${taskArtifacts.taskId} missing: ${missing2.join(", ")}
13367
+ `);
13335
13368
  }
13336
13369
  } catch {
13337
13370
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.152",
3
+ "version": "0.4.153",
4
4
  "description": "kody — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
5
5
  "license": "MIT",
6
6
  "type": "module",