spb/khaelor Public
KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.
TypeScript 82.9%
HTML 14.9%
CSS 1.1%
JavaScript 0.7%
1/**2 * KHAELOR3 * File: src/session/sdiff.ts4 * Description: Structured diff between two session runs — turns, tool calls, files, tokens (v2 design §2).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import type { DurableEvent } from "./events.js";11import { buildFileChangeSet, buildUsageTotals } from "./projections.js";1213export interface SessionRunSummary {14 sessionId: string;15 models: string[];16 agentTurns: number;17 toolCalls: number;18 filesModified: string[];19 inputTokens: number;20 outputTokens: number;21 cacheReadTokens: number;22 verifyFailures: number;23 outcome: "completed" | "failed" | "open";24}2526/** Fold one session's durable stream into the /sdiff row (real usage only — Rule #4). */27export function summarizeSessionRun(28 sessionId: string,29 events: readonly DurableEvent[],30): SessionRunSummary {31 const usage = buildUsageTotals(events);32 const files = buildFileChangeSet(events);33 const models = new Set<string>();34 let agentTurns = 0;35 let toolCalls = 0;36 let verifyFailures = 0;37 let outcome: SessionRunSummary["outcome"] = "open";38 for (const event of events) {39 switch (event.type) {40 case "model.request-started":41 models.add(event.payload.model);42 break;43 case "model.response-completed":44 agentTurns += 1;45 break;46 case "tool.requested":47 toolCalls += 1;48 break;49 case "verify.result":50 if (!event.payload.ok) verifyFailures += 1;51 break;52 case "task.completed":53 outcome = "completed";54 break;55 case "task.failed":56 outcome = "failed";57 break;58 default:59 break;60 }61 }62 return {63 sessionId,64 models: [...models],65 agentTurns,66 toolCalls,67 filesModified: [...files.changes.keys()].sort(),68 inputTokens: usage.totals.inputTokens,69 outputTokens: usage.totals.outputTokens,70 cacheReadTokens: usage.totals.cacheReadTokens,71 verifyFailures,72 outcome,73 };74}7576function formatTokens(n: number): string {77 if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;78 if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;79 return String(n);80}8182function row(label: string, a: string, b: string, widths: [number, number, number]): string {83 return ` ${label.padEnd(widths[0])} ${a.padEnd(widths[1])} ${b.padEnd(widths[2])}`.trimEnd();84}8586/** Render the /sdiff table: two runs side by side (v2 §2 — "run it twice, diff the runs"). */87export function renderSessionDiff(a: SessionRunSummary, b: SessionRunSummary): string[] {88 const widths: [number, number, number] = [18, 28, 28];89 const shortId = (id: string): string => id.slice(0, 12);90 const filesA = a.filesModified;91 const filesB = b.filesModified;92 const onlyA = filesA.filter((f) => !filesB.includes(f));93 const onlyB = filesB.filter((f) => !filesA.includes(f));9495 const lines = [96 row("", `${shortId(a.sessionId)} (${a.models.join(",") || "—"})`, `${shortId(b.sessionId)} (${b.models.join(",") || "—"})`, widths),97 row("agent turns", String(a.agentTurns), String(b.agentTurns), widths),98 row("tool calls", String(a.toolCalls), String(b.toolCalls), widths),99 row("files modified", String(filesA.length), String(filesB.length), widths),100 row(101 "tokens (in/out)",102 `${formatTokens(a.inputTokens)} / ${formatTokens(a.outputTokens)}`,103 `${formatTokens(b.inputTokens)} / ${formatTokens(b.outputTokens)}`,104 widths,105 ),106 row("cache reads", formatTokens(a.cacheReadTokens), formatTokens(b.cacheReadTokens), widths),107 row("verify failures", String(a.verifyFailures), String(b.verifyFailures), widths),108 row("outcome", a.outcome, b.outcome, widths),109 ];110 if (onlyA.length > 0) lines.push(` files only in A: ${onlyA.join(", ")}`);111 if (onlyB.length > 0) lines.push(` files only in B: ${onlyB.join(", ")}`);112 const common = filesA.filter((f) => filesB.includes(f));113 if (common.length > 0) lines.push(` files in both: ${common.join(", ")}`);114 return lines;115}116