@kody-ade/kody-engine 0.4.43 → 0.4.45

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/kody.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@kody-ade/kody-engine",
6
- version: "0.4.43",
6
+ version: "0.4.45",
7
7
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
8
8
  license: "MIT",
9
9
  type: "module",
@@ -390,7 +390,27 @@ function formatBytes(bytes) {
390
390
  }
391
391
 
392
392
  // src/agent.ts
393
+ function classifySubtype(subtype) {
394
+ if (!subtype) return "generic_failed";
395
+ const lower = subtype.toLowerCase();
396
+ if (lower === "success") return "ok";
397
+ if (lower.includes("max_turns") || lower.includes("max-turns")) return "out_of_turns";
398
+ if (lower.includes("rate_limit") || lower.includes("rate-limit")) return "rate_limit";
399
+ if (lower.includes("tool")) return "tool_error";
400
+ if (lower.includes("error")) return "model_error";
401
+ return "generic_failed";
402
+ }
393
403
  var DEFAULT_ALLOWED_TOOLS = ["Bash", "Edit", "Read", "Write", "Glob", "Grep"];
404
+ var DEFAULT_TURN_TIMEOUT_MS = 3e5;
405
+ function resolveTurnTimeoutMs(opts) {
406
+ if (opts.maxTurnTimeoutMs !== void 0 && opts.maxTurnTimeoutMs !== null) {
407
+ return opts.maxTurnTimeoutMs > 0 ? opts.maxTurnTimeoutMs : 0;
408
+ }
409
+ const envSec = Number(process.env.KODY_TURN_TIMEOUT_SEC);
410
+ if (Number.isFinite(envSec) && envSec > 0) return Math.floor(envSec * 1e3);
411
+ if (Number.isFinite(envSec) && envSec <= 0) return 0;
412
+ return DEFAULT_TURN_TIMEOUT_MS;
413
+ }
394
414
  async function runAgent(opts) {
395
415
  const ndjsonDir = opts.ndjsonDir ?? path3.join(opts.cwd, ".kody");
396
416
  fs3.mkdirSync(ndjsonDir, { recursive: true });
@@ -416,10 +436,14 @@ async function runAgent(opts) {
416
436
  }
417
437
  const resultTexts = [];
418
438
  let outcome = "failed";
439
+ let outcomeKind = "generic_failed";
419
440
  let errorMessage;
420
441
  const tokens = { input: 0, output: 0, cacheRead: 0, cacheCreate: 0 };
421
442
  let messageCount = 0;
422
443
  const startedAt = Date.now();
444
+ const turnTimeoutMs = resolveTurnTimeoutMs(opts);
445
+ let ndjsonWriteFailed = false;
446
+ let ndjsonWriteError;
423
447
  try {
424
448
  const queryOptions = {
425
449
  model: opts.model.model,
@@ -456,12 +480,45 @@ async function runAgent(opts) {
456
480
  // biome-ignore lint/suspicious/noExplicitAny: SDK options type is narrow; mcpServers is runtime-passthrough.
457
481
  options: queryOptions
458
482
  });
459
- for await (const msg of result) {
483
+ const iterator = typeof result[Symbol.asyncIterator] === "function" ? result[Symbol.asyncIterator]() : result;
484
+ while (true) {
485
+ const nextPromise = iterator.next();
486
+ let timedOut = false;
487
+ let timer;
488
+ let next;
489
+ if (turnTimeoutMs > 0) {
490
+ const timeoutPromise = new Promise((resolve4) => {
491
+ timer = setTimeout(() => {
492
+ timedOut = true;
493
+ resolve4({ done: true, value: void 0 });
494
+ }, turnTimeoutMs);
495
+ });
496
+ next = await Promise.race([nextPromise, timeoutPromise]);
497
+ if (timer) clearTimeout(timer);
498
+ } else {
499
+ next = await nextPromise;
500
+ }
501
+ if (timedOut) {
502
+ outcome = "failed";
503
+ outcomeKind = "stalled";
504
+ errorMessage = `agent stalled: no SDK message in ${Math.round(turnTimeoutMs / 1e3)}s`;
505
+ if (typeof iterator.return === "function") {
506
+ try {
507
+ await iterator.return(void 0);
508
+ } catch {
509
+ }
510
+ }
511
+ break;
512
+ }
513
+ if (next.done) break;
514
+ const msg = next.value;
460
515
  messageCount++;
461
516
  try {
462
517
  fullLog.write(`${JSON.stringify(msg)}
463
518
  `);
464
- } catch {
519
+ } catch (e) {
520
+ ndjsonWriteFailed = true;
521
+ ndjsonWriteError = e instanceof Error ? e.message : String(e);
465
522
  }
466
523
  const line = renderEvent(msg, { verbose: opts.verbose, quiet: opts.quiet });
467
524
  if (line) process.stdout.write(`${line}
@@ -481,16 +538,19 @@ async function runAgent(opts) {
481
538
  if (m.type === "result") {
482
539
  if (m.subtype === "success") {
483
540
  outcome = "completed";
541
+ outcomeKind = "ok";
484
542
  const text = (typeof m.result === "string" ? m.result : "").trim();
485
543
  if (text) resultTexts.push(text);
486
544
  } else {
487
545
  outcome = "failed";
546
+ outcomeKind = classifySubtype(m.subtype);
488
547
  errorMessage = `result subtype: ${m.subtype ?? "unknown"}`;
489
548
  }
490
549
  }
491
550
  }
492
551
  } catch (e) {
493
552
  outcome = "failed";
553
+ outcomeKind = "model_error";
494
554
  errorMessage = e instanceof Error ? e.message : String(e);
495
555
  } finally {
496
556
  try {
@@ -498,9 +558,14 @@ async function runAgent(opts) {
498
558
  } catch {
499
559
  }
500
560
  }
561
+ if (ndjsonWriteFailed) {
562
+ process.stderr.write(`[kody agent] NDJSON write failed (post-mortem may be incomplete): ${ndjsonWriteError ?? "unknown error"}
563
+ `);
564
+ }
501
565
  const finalText = resultTexts.join("\n\n---\n\n");
502
566
  return {
503
567
  outcome,
568
+ outcomeKind,
504
569
  finalText,
505
570
  error: errorMessage,
506
571
  ndjsonPath,
@@ -2018,6 +2083,13 @@ async function checkLitellmHealth(url) {
2018
2083
  return false;
2019
2084
  }
2020
2085
  }
2086
+ var DEFAULT_LITELLM_STARTUP_TIMEOUT_SEC = 60;
2087
+ var LITELLM_HEALTH_POLL_INTERVAL_MS = 2e3;
2088
+ function resolveLitellmTimeoutMs() {
2089
+ const envSec = Number(process.env.KODY_LITELLM_TIMEOUT_SEC);
2090
+ if (Number.isFinite(envSec) && envSec > 0) return Math.floor(envSec * 1e3);
2091
+ return DEFAULT_LITELLM_STARTUP_TIMEOUT_SEC * 1e3;
2092
+ }
2021
2093
  function generateLitellmConfigYaml(model) {
2022
2094
  const apiKeyVar = providerApiKeyEnvVar(model.provider);
2023
2095
  return [
@@ -2063,8 +2135,10 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
2063
2135
  env: stripBlockingEnv({ ...process.env, ...dotenvVars })
2064
2136
  });
2065
2137
  fs10.closeSync(outFd);
2066
- for (let i = 0; i < 30; i++) {
2067
- await new Promise((r) => setTimeout(r, 2e3));
2138
+ const timeoutMs = resolveLitellmTimeoutMs();
2139
+ const deadline = Date.now() + timeoutMs;
2140
+ while (Date.now() < deadline) {
2141
+ await new Promise((r) => setTimeout(r, LITELLM_HEALTH_POLL_INTERVAL_MS));
2068
2142
  if (await checkLitellmHealth(url)) {
2069
2143
  return {
2070
2144
  url,
@@ -2086,7 +2160,8 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
2086
2160
  child.kill();
2087
2161
  } catch {
2088
2162
  }
2089
- throw new Error(`LiteLLM proxy failed to start within 60s. Log tail:
2163
+ const seconds = Math.round(timeoutMs / 1e3);
2164
+ throw new Error(`LiteLLM proxy failed to start within ${seconds}s (KODY_LITELLM_TIMEOUT_SEC overrides). Log tail:
2090
2165
  ${logTail}`);
2091
2166
  }
2092
2167
  function readDotenvApiKeys(projectDir) {
@@ -6875,6 +6950,7 @@ var parseAgentResult2 = async (ctx, profile, agentResult) => {
6875
6950
  ctx.data.agentFailureReason = parsed.failureReason;
6876
6951
  ctx.data.agentMarkerMissing = parsed.markerMissing;
6877
6952
  ctx.data.agentOutcome = agentResult.outcome;
6953
+ ctx.data.agentOutcomeKind = agentResult.outcomeKind;
6878
6954
  ctx.data.agentError = agentResult.error;
6879
6955
  const modeSeg = (ctx.args.mode ?? profile.name).replace(/-/g, "_").toUpperCase();
6880
6956
  if (parsed.done) {
@@ -9100,7 +9176,8 @@ async function runExecutable(profileName, input) {
9100
9176
  return finishAndEnd({ exitCode: 99, reason: `config error: ${err instanceof Error ? err.message : String(err)}` });
9101
9177
  }
9102
9178
  }
9103
- const modelSpec = profile.claudeCode.model === "inherit" ? config.agent.model : profile.claudeCode.model;
9179
+ const perExecutableModel = config.agent.perExecutable?.[profileName];
9180
+ const modelSpec = perExecutableModel ? perExecutableModel : profile.claudeCode.model === "inherit" ? config.agent.model : profile.claudeCode.model;
9104
9181
  let model;
9105
9182
  try {
9106
9183
  model = parseProviderModel(modelSpec);
@@ -9204,6 +9281,7 @@ async function runExecutable(profileName, input) {
9204
9281
  durationMs: agentResult.durationMs,
9205
9282
  outcome: agentResult.outcome === "completed" ? "ok" : "failed",
9206
9283
  meta: {
9284
+ kind: agentResult.outcomeKind,
9207
9285
  ...agentResult.tokens ? { tokens: agentResult.tokens } : {},
9208
9286
  ...typeof agentResult.messageCount === "number" ? { messageCount: agentResult.messageCount } : {},
9209
9287
  ...agentResult.error ? { error: agentResult.error } : {}
@@ -10126,8 +10204,8 @@ async function runScheduledFanOut(cwd, args, opts) {
10126
10204
  return 99;
10127
10205
  }
10128
10206
  const config = loadConfig(cwd);
10129
- let worstExit = 0;
10130
- for (const match of matches) {
10207
+ const serial = process.env.KODY_SERIAL_WATCHES === "1";
10208
+ const runWatch = async (match) => {
10131
10209
  process.stdout.write(`
10132
10210
  \u2192 kody: running watch \`${match.executable}\`
10133
10211
  `);
@@ -10144,13 +10222,27 @@ async function runScheduledFanOut(cwd, args, opts) {
10144
10222
  `[kody] watch \`${match.executable}\` exited ${result.exitCode}: ${result.reason ?? "(no reason)"}
10145
10223
  `
10146
10224
  );
10147
- if (result.exitCode > worstExit) worstExit = result.exitCode;
10225
+ return result.exitCode;
10148
10226
  }
10227
+ return 0;
10149
10228
  } catch (err) {
10150
10229
  const msg = err instanceof Error ? err.message : String(err);
10151
10230
  process.stderr.write(`[kody] watch \`${match.executable}\` crashed: ${msg}
10152
10231
  `);
10153
- worstExit = Math.max(worstExit, 99);
10232
+ return 99;
10233
+ }
10234
+ };
10235
+ let worstExit = 0;
10236
+ if (serial) {
10237
+ for (const match of matches) {
10238
+ const code = await runWatch(match);
10239
+ if (code > worstExit) worstExit = code;
10240
+ }
10241
+ } else {
10242
+ const settled = await Promise.allSettled(matches.map((m) => runWatch(m)));
10243
+ for (const r of settled) {
10244
+ const code = r.status === "fulfilled" ? r.value : 99;
10245
+ if (code > worstExit) worstExit = code;
10154
10246
  }
10155
10247
  }
10156
10248
  return worstExit;
@@ -20,34 +20,16 @@
20
20
  "Read",
21
21
  "Grep",
22
22
  "Glob",
23
- "Bash",
24
- "mcp__playwright"
23
+ "Bash"
25
24
  ],
26
25
  "hooks": ["block-write"],
27
26
  "skills": [],
28
27
  "commands": [],
29
28
  "subagents": [],
30
29
  "plugins": [],
31
- "mcpServers": [
32
- {
33
- "name": "playwright",
34
- "command": "npx",
35
- "args": ["-y", "--package=@playwright/mcp@latest", "--", "playwright-mcp"]
36
- }
37
- ]
30
+ "mcpServers": []
38
31
  },
39
- "cliTools": [
40
- {
41
- "name": "playwright",
42
- "install": {
43
- "required": false,
44
- "checkCommand": "ls \"$HOME/.cache/ms-playwright\" 2>/dev/null | grep -q '^chromium'",
45
- "installCommand": "npx --yes playwright install --with-deps chromium"
46
- },
47
- "verify": "ls \"$HOME/.cache/ms-playwright\" 2>/dev/null | grep -q '^chromium'",
48
- "usage": ""
49
- }
50
- ],
32
+ "cliTools": [],
51
33
  "scripts": {
52
34
  "preflight": [
53
35
  {
@@ -21,34 +21,16 @@
21
21
  "Read",
22
22
  "Grep",
23
23
  "Glob",
24
- "Bash",
25
- "mcp__playwright"
24
+ "Bash"
26
25
  ],
27
26
  "hooks": ["block-write"],
28
27
  "skills": [],
29
28
  "commands": [],
30
29
  "subagents": [],
31
30
  "plugins": [],
32
- "mcpServers": [
33
- {
34
- "name": "playwright",
35
- "command": "npx",
36
- "args": ["-y", "--package=@playwright/mcp@latest", "--", "playwright-mcp"]
37
- }
38
- ]
31
+ "mcpServers": []
39
32
  },
40
- "cliTools": [
41
- {
42
- "name": "playwright",
43
- "install": {
44
- "required": false,
45
- "checkCommand": "ls \"$HOME/.cache/ms-playwright\" 2>/dev/null | grep -q '^chromium'",
46
- "installCommand": "npx --yes playwright install --with-deps chromium"
47
- },
48
- "verify": "ls \"$HOME/.cache/ms-playwright\" 2>/dev/null | grep -q '^chromium'",
49
- "usage": ""
50
- }
51
- ],
33
+ "cliTools": [],
52
34
  "scripts": {
53
35
  "preflight": [
54
36
  {
@@ -135,6 +135,14 @@
135
135
  "description": "Single 'provider/model' string used by kody (single-session pipeline). Use 'claude/...' or 'anthropic/...' for direct Anthropic API; anything else routes through LiteLLM proxy.",
136
136
  "examples": ["claude/claude-sonnet-4-6", "minimax/MiniMax-M2.7-highspeed"]
137
137
  },
138
+ "perExecutable": {
139
+ "type": "object",
140
+ "description": "Per-executable model override. Wins over agent.model for the matching stage. Example: {\"classify\": \"claude/claude-haiku-4-5-20251001\", \"plan\": \"claude/claude-opus-4-7\"}.",
141
+ "additionalProperties": {
142
+ "type": "string",
143
+ "pattern": "^[^/]+/.+$"
144
+ }
145
+ },
138
146
  "modelMap": {
139
147
  "type": "object",
140
148
  "description": "Maps model tiers to 'provider/model' strings. Use 'claude/...' or 'anthropic/...' for direct Anthropic API; anything else routes through LiteLLM proxy.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.43",
3
+ "version": "0.4.45",
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",