SPB Git

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%
3.9 KB · 116 lines typescript
Raw Blame History
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