@kody-ade/kody-engine 0.4.402 → 0.4.403

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 +128 -157
  2. package/package.json +1 -1
package/dist/bin/kody.js CHANGED
@@ -15,7 +15,7 @@ var init_package = __esm({
15
15
  "package.json"() {
16
16
  package_default = {
17
17
  name: "@kody-ade/kody-engine",
18
- version: "0.4.402",
18
+ version: "0.4.403",
19
19
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative implementation profiles.",
20
20
  license: "MIT",
21
21
  type: "module",
@@ -21590,8 +21590,8 @@ init_agents();
21590
21590
  init_config();
21591
21591
  init_registry();
21592
21592
  init_task_artifacts();
21593
- import * as fs15 from "fs";
21594
- import * as path16 from "path";
21593
+ import * as fs14 from "fs";
21594
+ import * as path15 from "path";
21595
21595
 
21596
21596
  // src/chat/attachments.ts
21597
21597
  init_runtimePaths();
@@ -21999,163 +21999,69 @@ function makeRunId(sessionId, suffix) {
21999
21999
  // src/chat/session-store.ts
22000
22000
  init_convex_client();
22001
22001
  import { anyApi as anyApi2 } from "convex/server";
22002
-
22003
- // src/chat/session.ts
22004
- import * as fs14 from "fs";
22005
- import * as path15 from "path";
22006
- import posixPath3 from "path/posix";
22007
- function sessionFilePath(cwd, sessionId) {
22008
- return path15.join(cwd, ".kody-engine", "runtime", "sessions", `${sessionId}.jsonl`);
22009
- }
22010
- function readMeta(file) {
22011
- if (!fs14.existsSync(file)) return null;
22012
- const raw = fs14.readFileSync(file, "utf-8");
22013
- const firstLine2 = raw.split("\n", 1)[0]?.trim();
22014
- if (!firstLine2) return null;
22015
- try {
22016
- const parsed = JSON.parse(firstLine2);
22017
- if (parsed.type !== "meta") return null;
22018
- if (parsed.mode !== "one-shot" && parsed.mode !== "interactive") return null;
22019
- return parsed;
22020
- } catch {
22021
- return null;
22022
- }
22023
- }
22024
- function readSession(file) {
22025
- if (!fs14.existsSync(file)) return [];
22026
- const raw = fs14.readFileSync(file, "utf-8").trim();
22027
- if (!raw) return [];
22028
- const turns = [];
22029
- for (const line of raw.split("\n")) {
22030
- if (!line.trim()) continue;
22031
- try {
22032
- const parsed = JSON.parse(line);
22033
- if (parsed.role !== "user" && parsed.role !== "assistant") continue;
22034
- if (typeof parsed.content !== "string") continue;
22035
- turns.push(parsed);
22036
- } catch {
22037
- }
22038
- }
22039
- return turns;
22040
- }
22041
- function appendTurn(file, turn) {
22042
- fs14.mkdirSync(path15.dirname(file), { recursive: true });
22043
- const line = JSON.stringify({
22044
- role: turn.role,
22045
- content: turn.content,
22046
- timestamp: turn.timestamp,
22047
- toolCalls: turn.toolCalls ?? []
22048
- });
22049
- fs14.appendFileSync(file, `${line}
22050
- `);
22051
- }
22052
- function seedInitialMessage(file, message) {
22053
- if (!message.trim()) return false;
22054
- const turns = readSession(file);
22055
- const lastUser = [...turns].reverse().find((t) => t.role === "user");
22056
- if (lastUser && lastUser.content === message) return false;
22057
- appendTurn(file, {
22058
- role: "user",
22059
- content: message,
22060
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
22061
- });
22062
- return true;
22002
+ function currentEpochTurns(result) {
22003
+ const ordered = [...result.entries].sort((left, right) => left.seq - right.seq);
22004
+ const lastHandoffSeq = ordered.filter((item) => item.entry.kind === "agent-handoff").at(-1)?.seq ?? -1;
22005
+ return ordered.flatMap(
22006
+ (item) => item.seq > lastHandoffSeq && item.entry.kind === "message" && (item.entry.status === "committed" || item.entry.status === "pending") ? [
22007
+ {
22008
+ role: item.entry.role,
22009
+ content: item.entry.content,
22010
+ timestamp: item.entry.createdAt,
22011
+ toolCalls: []
22012
+ }
22013
+ ] : []
22014
+ );
22063
22015
  }
22064
-
22065
- // src/chat/session-store.ts
22066
- function isChatTurn(value) {
22067
- if (!value || typeof value !== "object") return false;
22068
- const t = value;
22069
- return (t.role === "user" || t.role === "assistant") && typeof t.content === "string";
22016
+ function entryKey(sessionId, turn) {
22017
+ const safeTime = turn.timestamp.replace(/[^a-zA-Z0-9._-]/g, "-");
22018
+ return `engine-${sessionId}-${turn.role}-${safeTime}`;
22070
22019
  }
22071
22020
  function createSessionStore(opts) {
22072
22021
  const logger = opts.logger ?? {
22073
- info: (m) => process.stdout.write(`[kody:chat:store] ${m}
22022
+ info: (message) => process.stdout.write(`[kody:chat:store] ${message}
22074
22023
  `),
22075
- warn: (m) => process.stderr.write(`[kody:chat:store] ${m}
22024
+ warn: (message) => process.stderr.write(`[kody:chat:store] ${message}
22076
22025
  `)
22077
22026
  };
22078
22027
  const client = opts.client !== void 0 ? opts.client : createConvexClientFromEnv();
22079
22028
  const tenantId2 = opts.tenantId ?? process.env.GITHUB_REPOSITORY ?? "";
22080
- if (client && tenantId2) {
22081
- logger.info(`session ${opts.sessionId}: using Convex transcript store (tenant ${tenantId2})`);
22082
- return createConvexStore({
22083
- client,
22029
+ if (!client || !tenantId2) {
22030
+ throw new Error(
22031
+ "Canonical Convex conversation storage is required (CONVEX_URL, KODY_SERVICE_KEY, and GITHUB_REPOSITORY)"
22032
+ );
22033
+ }
22034
+ logger.info(`conversation ${opts.sessionId}: using canonical Convex store (tenant ${tenantId2})`);
22035
+ const read = async () => {
22036
+ const result = await client.query(anyApi2.conversations.get, {
22084
22037
  tenantId: tenantId2,
22085
- sessionId: opts.sessionId,
22086
- sessionFile: opts.sessionFile,
22087
- logger
22038
+ conversationId: opts.sessionId
22088
22039
  });
22089
- }
22090
- if (client && !tenantId2) {
22091
- if (process.env.GITHUB_ACTIONS === "true") {
22092
- throw new Error("Convex chat backend requires GITHUB_REPOSITORY in GitHub Actions");
22093
- }
22094
- logger.warn(`session ${opts.sessionId}: Convex configured without a tenant; using local runtime storage`);
22095
- } else {
22096
- if (process.env.GITHUB_ACTIONS === "true") {
22097
- throw new Error("Convex chat backend is required in GitHub Actions (CONVEX_URL and KODY_SERVICE_KEY)");
22098
- }
22099
- logger.info(`session ${opts.sessionId}: backend unavailable; using local runtime storage`);
22100
- }
22101
- return createLocalStore(opts.sessionFile);
22102
- }
22103
- function createLocalStore(sessionFile) {
22104
- return {
22105
- backend: "local",
22106
- readTurns: async () => readSession(sessionFile),
22107
- appendTurn: async (turn) => {
22108
- appendTurn(sessionFile, turn);
22109
- }
22110
- };
22111
- }
22112
- function normalizeTurn(turn) {
22113
- return {
22114
- role: turn.role,
22115
- content: turn.content,
22116
- timestamp: turn.timestamp,
22117
- toolCalls: turn.toolCalls ?? []
22118
- };
22119
- }
22120
- function createConvexStore(args) {
22121
- const { client, tenantId: tenantId2, sessionId, sessionFile, logger } = args;
22122
- let sessionUpserted = false;
22123
- const appendToConvex = async (turn) => {
22124
- if (!sessionUpserted) {
22125
- try {
22126
- const meta = readMeta(sessionFile) ?? { type: "meta", mode: "one-shot" };
22127
- await client.mutation(anyApi2.chatSessions.upsert, {
22128
- tenantId: tenantId2,
22129
- sessionId,
22130
- meta,
22131
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
22132
- });
22133
- sessionUpserted = true;
22134
- } catch (err) {
22135
- logger.warn(
22136
- `session ${sessionId}: chatSessions.upsert failed: ${err instanceof Error ? err.message : String(err)}`
22137
- );
22138
- }
22139
- }
22140
- await client.mutation(anyApi2.chatTurns.append, { tenantId: tenantId2, sessionId, turn });
22040
+ if (!result) throw new Error(`Conversation not found: ${opts.sessionId}`);
22041
+ return result;
22141
22042
  };
22142
22043
  return {
22143
22044
  backend: "convex",
22144
- readTurns: async () => {
22145
- const docs = await client.query(anyApi2.chatTurns.list, { tenantId: tenantId2, sessionId });
22146
- const convexTurns = [...docs].sort((a, b) => a.seq - b.seq).map((doc) => doc.turn).filter(isChatTurn);
22147
- if (convexTurns.length > 0) return convexTurns;
22148
- const tail = readSession(sessionFile).map(normalizeTurn);
22149
- if (tail.length === 0) return convexTurns;
22150
- logger.info(`session ${sessionId}: importing ${tail.length} runtime turn(s) into Convex`);
22151
- for (const turn of tail) {
22152
- await appendToConvex(turn);
22153
- }
22154
- return tail;
22155
- },
22045
+ readActiveAgent: async () => (await read()).conversation.activeAgent,
22046
+ readTurns: async () => currentEpochTurns(await read()),
22156
22047
  appendTurn: async (turn) => {
22157
- const normalized = normalizeTurn(turn);
22158
- await appendToConvex(normalized);
22048
+ const result = await read();
22049
+ const id = entryKey(opts.sessionId, turn);
22050
+ await client.mutation(anyApi2.conversations.appendEntry, {
22051
+ tenantId: tenantId2,
22052
+ conversationId: opts.sessionId,
22053
+ entryId: id,
22054
+ idempotencyKey: id,
22055
+ entry: {
22056
+ kind: "message",
22057
+ role: turn.role,
22058
+ author: turn.role === "assistant" ? { kind: "agent", ...result.conversation.activeAgent } : { kind: "user", actorId: "engine-runtime" },
22059
+ content: turn.content,
22060
+ status: "committed",
22061
+ turnId: id,
22062
+ createdAt: turn.timestamp
22063
+ }
22064
+ });
22159
22065
  }
22160
22066
  };
22161
22067
  }
@@ -22279,7 +22185,7 @@ function buildImplementationCatalog() {
22279
22185
  const entries = [];
22280
22186
  for (const { name, profilePath } of discovered) {
22281
22187
  try {
22282
- const raw = JSON.parse(fs15.readFileSync(profilePath, "utf-8"));
22188
+ const raw = JSON.parse(fs14.readFileSync(profilePath, "utf-8"));
22283
22189
  const describe = typeof raw.describe === "string" ? raw.describe : "";
22284
22190
  const firstSentence = describe.split(/(?<=[.!?])\s+/, 1)[0] ?? "";
22285
22191
  entries.push({ name, describe: firstSentence.trim() });
@@ -22320,7 +22226,8 @@ async function runChatTurn(opts) {
22320
22226
  }
22321
22227
  const { turns: promptTurns, imagePaths } = prepareAttachments(turns, opts.cwd, opts.sessionId);
22322
22228
  const basePrompt = opts.systemPrompt ?? readSystemPromptOverride(opts.cwd) ?? (opts.model.protocol === "openai" ? OPENAI_CHAT_SYSTEM_PROMPT : CHAT_SYSTEM_PROMPT);
22323
- const agentIdentityBlock = readAgentIdentityBlock(opts.cwd, opts.agentIdentity);
22229
+ const activeAgent = await store.readActiveAgent();
22230
+ const agentIdentityBlock = readAgentIdentityBlock(opts.cwd, opts.agentIdentity ?? { slug: activeAgent.slug });
22324
22231
  const catalog = buildImplementationCatalog();
22325
22232
  const taskArtifactsPaths = {
22326
22233
  ...prepareTaskArtifactsDir(opts.cwd, opts.sessionId),
@@ -22386,7 +22293,7 @@ async function runChatTurn(opts) {
22386
22293
  quiet: opts.quiet,
22387
22294
  additionalDirectories: [
22388
22295
  taskArtifactsPaths.absDir,
22389
- ...Array.from(new Set(imagePaths.map((p2) => path16.dirname(p2))))
22296
+ ...Array.from(new Set(imagePaths.map((p2) => path15.dirname(p2))))
22390
22297
  ],
22391
22298
  systemPromptAppend: systemPrompt,
22392
22299
  ...opts.reasoningEffort ? { reasoningEffort: opts.reasoningEffort } : {},
@@ -22569,10 +22476,10 @@ async function emit(sink, type, sessionId, suffix, payload) {
22569
22476
  var MEMORY_INDEX_REL = ".kody-engine/runtime/memory/INDEX.md";
22570
22477
  var MAX_INDEX_BYTES = 8e3;
22571
22478
  function readMemoryIndexBlock(cwd) {
22572
- const indexPath = path16.join(cwd, MEMORY_INDEX_REL);
22479
+ const indexPath = path15.join(cwd, MEMORY_INDEX_REL);
22573
22480
  let raw;
22574
22481
  try {
22575
- raw = fs15.readFileSync(indexPath, "utf-8");
22482
+ raw = fs14.readFileSync(indexPath, "utf-8");
22576
22483
  } catch {
22577
22484
  return "";
22578
22485
  }
@@ -22592,17 +22499,17 @@ _\u2026 (memory index truncated; use recall_search to read more)_` : trimmed;
22592
22499
  var CONTEXT_DIR_REL = ".kody-engine/runtime/context";
22593
22500
  var MAX_CONTEXT_BYTES = 12e3;
22594
22501
  function readContextBlock(cwd) {
22595
- const dir = path16.join(cwd, CONTEXT_DIR_REL);
22502
+ const dir = path15.join(cwd, CONTEXT_DIR_REL);
22596
22503
  let files;
22597
22504
  try {
22598
- files = fs15.readdirSync(dir).filter((f) => f.endsWith(".md")).sort();
22505
+ files = fs14.readdirSync(dir).filter((f) => f.endsWith(".md")).sort();
22599
22506
  } catch {
22600
22507
  return "";
22601
22508
  }
22602
22509
  const sections = [];
22603
22510
  for (const file of files) {
22604
22511
  try {
22605
- const content = fs15.readFileSync(path16.join(dir, file), "utf-8").trim();
22512
+ const content = fs14.readFileSync(path15.join(dir, file), "utf-8").trim();
22606
22513
  if (content) sections.push(`### ${file.replace(/\.md$/, "")}
22607
22514
 
22608
22515
  ${content}`);
@@ -22628,7 +22535,7 @@ var SYSTEM_PROMPT_OVERRIDE_REL = ".kody-engine/runtime/system-prompt.md";
22628
22535
  function readSystemPromptOverride(cwd) {
22629
22536
  let raw;
22630
22537
  try {
22631
- raw = fs15.readFileSync(path16.join(cwd, SYSTEM_PROMPT_OVERRIDE_REL), "utf-8");
22538
+ raw = fs14.readFileSync(path15.join(cwd, SYSTEM_PROMPT_OVERRIDE_REL), "utf-8");
22632
22539
  } catch {
22633
22540
  return null;
22634
22541
  }
@@ -22636,10 +22543,10 @@ function readSystemPromptOverride(cwd) {
22636
22543
  return trimmed.length > 0 ? trimmed : null;
22637
22544
  }
22638
22545
  function readInstructionsBlock(cwd) {
22639
- const instructionsPath = path16.join(cwd, INSTRUCTIONS_REL);
22546
+ const instructionsPath = path15.join(cwd, INSTRUCTIONS_REL);
22640
22547
  let raw;
22641
22548
  try {
22642
- raw = fs15.readFileSync(instructionsPath, "utf-8");
22549
+ raw = fs14.readFileSync(instructionsPath, "utf-8");
22643
22550
  } catch {
22644
22551
  return "";
22645
22552
  }
@@ -22672,6 +22579,68 @@ function resolveBrainDriver(runtime) {
22672
22579
  return driver;
22673
22580
  }
22674
22581
 
22582
+ // src/chat/session.ts
22583
+ import * as fs15 from "fs";
22584
+ import * as path16 from "path";
22585
+ import posixPath3 from "path/posix";
22586
+ function sessionFilePath(cwd, sessionId) {
22587
+ return path16.join(cwd, ".kody-engine", "runtime", "sessions", `${sessionId}.jsonl`);
22588
+ }
22589
+ function readMeta(file) {
22590
+ if (!fs15.existsSync(file)) return null;
22591
+ const raw = fs15.readFileSync(file, "utf-8");
22592
+ const firstLine2 = raw.split("\n", 1)[0]?.trim();
22593
+ if (!firstLine2) return null;
22594
+ try {
22595
+ const parsed = JSON.parse(firstLine2);
22596
+ if (parsed.type !== "meta") return null;
22597
+ if (parsed.mode !== "one-shot" && parsed.mode !== "interactive") return null;
22598
+ return parsed;
22599
+ } catch {
22600
+ return null;
22601
+ }
22602
+ }
22603
+ function readSession(file) {
22604
+ if (!fs15.existsSync(file)) return [];
22605
+ const raw = fs15.readFileSync(file, "utf-8").trim();
22606
+ if (!raw) return [];
22607
+ const turns = [];
22608
+ for (const line of raw.split("\n")) {
22609
+ if (!line.trim()) continue;
22610
+ try {
22611
+ const parsed = JSON.parse(line);
22612
+ if (parsed.role !== "user" && parsed.role !== "assistant") continue;
22613
+ if (typeof parsed.content !== "string") continue;
22614
+ turns.push(parsed);
22615
+ } catch {
22616
+ }
22617
+ }
22618
+ return turns;
22619
+ }
22620
+ function appendTurn(file, turn) {
22621
+ fs15.mkdirSync(path16.dirname(file), { recursive: true });
22622
+ const line = JSON.stringify({
22623
+ role: turn.role,
22624
+ content: turn.content,
22625
+ timestamp: turn.timestamp,
22626
+ toolCalls: turn.toolCalls ?? []
22627
+ });
22628
+ fs15.appendFileSync(file, `${line}
22629
+ `);
22630
+ }
22631
+ function seedInitialMessage(file, message) {
22632
+ if (!message.trim()) return false;
22633
+ const turns = readSession(file);
22634
+ const lastUser = [...turns].reverse().find((t) => t.role === "user");
22635
+ if (lastUser && lastUser.content === message) return false;
22636
+ appendTurn(file, {
22637
+ role: "user",
22638
+ content: message,
22639
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
22640
+ });
22641
+ return true;
22642
+ }
22643
+
22675
22644
  // src/servers/brain-serve.ts
22676
22645
  init_config();
22677
22646
 
@@ -24436,7 +24405,7 @@ async function handleChatTurn(req, res, chatId, opts) {
24436
24405
  }
24437
24406
  const stateToken = repoToken || envGithubToken();
24438
24407
  const sessionFile = sessionFilePath(agentCwd, chatId);
24439
- const sessionStore = createSessionStore({
24408
+ const sessionStore = opts.createStore({
24440
24409
  sessionId: chatId,
24441
24410
  sessionFile,
24442
24411
  ...repo ? { tenantId: repo } : {}
@@ -24491,6 +24460,7 @@ async function handleChatTurn(req, res, chatId, opts) {
24491
24460
  }
24492
24461
  function buildServer(opts) {
24493
24462
  const runTurn = opts.runTurn ?? runChatTurn;
24463
+ const createStore = opts.createStore ?? createSessionStore;
24494
24464
  const cloneRepo = opts.cloneRepo ?? defaultCloneRepo;
24495
24465
  const reposRoot = opts.reposRoot ?? path48.join(path48.dirname(path48.resolve(opts.cwd)), "repos");
24496
24466
  return createServer2(async (req, res) => {
@@ -24521,6 +24491,7 @@ function buildServer(opts) {
24521
24491
  model: opts.model,
24522
24492
  litellmUrl: opts.litellmUrl,
24523
24493
  runTurn,
24494
+ createStore,
24524
24495
  driver: opts.driver
24525
24496
  });
24526
24497
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.402",
3
+ "version": "0.4.403",
4
4
  "description": "kody — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative implementation profiles.",
5
5
  "license": "MIT",
6
6
  "type": "module",