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/events.ts4 * Description: The complete typed event catalog — envelopes, durable/ephemeral split (EVENT_MODEL.md), including the v2 phase-gate, verify, memory, and subtask events.5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910/** Schema version written on every durable JSONL line (EVENT_MODEL.md §5.4). */11export const EVENT_SCHEMA_VERSION = 1 as const;12export type EventSchemaVersion = typeof EVENT_SCHEMA_VERSION;1314// ───────────────────────────── envelopes ─────────────────────────────1516/** Durable envelope — one JSONL line per event. */17export interface Durable<T extends string, P> {18 v: EventSchemaVersion; // schema version of this event line19 id: string; // ULID — globally unique, time-ordered20 sessionId: string;21 seq: number; // monotonic per session, gapless, assigned at append time22 ts: number; // epoch milliseconds23 parentId?: string; // reserved, always absent in V1 (ADR-3: linear log, tree-ready)24 type: T;25 payload: P;26}2728/** Ephemeral envelope — bus-only, never persisted. No seq (no log position), no v. */29export interface Ephemeral<T extends string, P> {30 id: string; // ULID (correlation/debugging)31 sessionId: string;32 ts: number;33 type: T;34 payload: P;35}3637// ───────────────────────── shared payload types ─────────────────────────3839export type StopReason = "end_turn" | "tool_use" | "max_tokens" | "refusal";4041export interface ModelUsage {42 // real API fields only43 inputTokens: number;44 outputTokens: number;45 cacheReadTokens: number;46 cacheWriteTokens: number;47}4849export interface DiffStats {50 added: number;51 removed: number;52}5354export interface CheckResult {55 command: string;56 exitCode: number;57 summary: string; // e.g. "148 passed", "typecheck passed"58 durationMs: number;59}6061export interface CompletionEvidence {62 // CLAUDE.md §1763 objective: string;64 changedFiles: string[];65 checks: CheckResult[];66 unresolvedIssues: string[];67}6869export interface GitBaseline {70 branch: string;71 dirtyFiles: string[];72 untrackedFiles: string[];73 diffHash: string; // hash of `git diff` output at capture time74}7576export type ModelErrorKind =77 | "retryable"78 | "context-overflow"79 | "auth"80 | "invalid-request"81 | "cancelled";8283export type ToolName =84 | "read"85 | "write"86 | "edit"87 | "grep"88 | "glob"89 | "bash"90 | "process"91 | "design"92 | "remember"93 | "symbols"94 | "refs";9596// ─────────────────────── phase gates (durable, v2 §1) ───────────────────────9798export type Phase = "understand" | "design" | "implement";99100/** The structured design the agent must produce before implement is unlocked. */101export interface DesignArtifact {102 /** Reformulation of the need by the agent. */103 goal: string;104 /** Files it plans to modify. */105 filesTouched: string[];106 /** Technical approach, 5–15 lines. */107 approach: string;108 /** Identified risks. */109 risks: string[];110 /** How it will prove the change works. */111 verification: string;112 /** What it will NOT do. */113 outOfScope: string[];114}115116// ─────────────────────── session lifecycle (durable) ───────────────────────117118export type SessionStarted = Durable<119 "session.started",120 {121 title: string;122 projectHash: string;123 workingDirectory: string;124 gitBranch: string | null;125 model: string;126 auxModel: string;127 khaelorVersion: string;128 }129>;130131export type SessionResumed = Durable<132 "session.resumed",133 {134 khaelorVersion: string;135 replayedSeq: number; // highest seq replayed136 model: string; // active model after resume (may differ — swappable)137 toolNames: ToolName[]; // verified add-only vs. original set (ADR-3 resume contract)138 }139>;140141export type SessionRenamed = Durable<"session.renamed", { title: string }>;142143export type ModelChanged = Durable<144 "session.model-changed",145 {146 from: string;147 to: string;148 reason: "user" | "config";149 }150>;151152export type BaselineRecorded = Durable<153 "git.baseline-recorded",154 {155 when: "session-start" | "pre-first-edit";156 baseline: GitBaseline;157 }158>;159160// ───────────────────────── user input (durable) ─────────────────────────161162export type UserMessageCreated = Durable<163 "user.message-created",164 {165 text: string; // byte-exact — enters LLM history verbatim166 mentions: { path: string; range?: { start: number; end: number } }[];167 }168>;169170export type SteeringQueued = Durable<"user.steering-queued", { text: string }>;171172export type SteeringInjected = Durable<173 "user.steering-injected",174 {175 queuedEventId: string; // id of the SteeringQueued event176 seam: "post-tool-batch" | "pre-model-call";177 afterSeq: number; // injection position in history — makes replay exact (ADR-7)178 }179>;180181export type Interrupted = Durable<182 "user.interrupted",183 {184 scope: "turn"; // V1: Esc aborts the turn (model stream + in-flight tools)185 pendingToolUseIds: string[]; // tools that will be closed via ToolCancelled186 }187>;188189// ──────────────────────────── model stream ────────────────────────────190191export type ModelRequestStarted = Durable<192 "model.request-started",193 {194 requestId: string; // correlates all blocks/usage of this call195 model: string;196 purpose: "main" | "compaction" | "verification-nudge";197 contextStats: {198 // for /context history — estimates labeled as such199 estimatedInputTokens: number;200 sections: { name: string; estimatedTokens: number }[];201 };202 }203>;204205export type ModelTextDelta = Ephemeral<206 "model.text-delta",207 {208 requestId: string;209 blockIndex: number;210 text: string;211 }212>;213214export type ModelThinkingDelta = Ephemeral<215 "model.thinking-delta",216 {217 requestId: string;218 blockIndex: number;219 text: string;220 }221>;222223export type ToolCallStarted = Ephemeral<224 "model.tool-call-started",225 {226 requestId: string;227 blockIndex: number;228 toolUseId: string;229 toolName: ToolName;230 }231>;232233export type ToolInputDelta = Ephemeral<234 "model.tool-input-delta",235 {236 requestId: string;237 blockIndex: number;238 toolUseId: string;239 partialJson: string;240 }241>;242243export type ModelTextBlockCompleted = Durable<244 "model.text-block-completed",245 {246 requestId: string;247 blockIndex: number;248 text: string; // byte-exact settled block249 }250>;251252export type ModelThinkingBlockCompleted = Durable<253 "model.thinking-block-completed",254 {255 requestId: string;256 blockIndex: number;257 thinking: string;258 signature: string; // required for byte-exact API replay in tool loops259 }260>;261262export type ToolRequested = Durable<263 "tool.requested",264 {265 requestId: string;266 blockIndex: number;267 toolUseId: string; // Anthropic tool_use id — pairing key (§6.5)268 toolName: ToolName;269 input: unknown; // complete parsed input — byte-exact via canonical JSON (§5.1)270 }271>;272273export type ModelResponseCompleted = Durable<274 "model.response-completed",275 {276 requestId: string;277 stopReason: StopReason;278 usage: ModelUsage; // REAL API usage — sole source for cost/compaction279 durationMs: number;280 }281>;282283export type ModelRequestFailed = Durable<284 "model.request-failed",285 {286 requestId: string;287 kind: ModelErrorKind;288 message: string; // redacted — never headers/keys289 status?: number;290 retriesExhausted: boolean;291 }292>;293294// ─────────────────────── permissions (durable) ───────────────────────295296export type PermissionRequested = Durable<297 "permission.requested",298 {299 permissionRequestId: string;300 toolUseId: string;301 capability: string; // Capability302 descriptor: string; // human-meaningful: "Run `npm install` in ~/dev/project"303 suggestion?: { capability: string; pattern: string }; // "always allow git push *" (ADR-9)304 }305>;306307export type PermissionGranted = Durable<308 "permission.granted",309 {310 permissionRequestId: string;311 scope: "once" | "always-project"; // "always" also persists a config rule (ADR-9)312 }313>;314315export type PermissionDenied = Durable<316 "permission.denied",317 {318 permissionRequestId: string;319 source: "user" | "policy" | "hardline" | "timeout"; // silence is not consent (ADR-9)320 feedback: string; // returned to the model as the tool observation321 }322>;323324// ─────────────── tool execution (durable + one ephemeral) ───────────────325326export type ToolApproved = Durable<327 "tool.approved",328 {329 toolUseId: string;330 via: "policy-allow" | "user-once" | "user-always" | "rule";331 }332>;333334export type ToolStarted = Durable<335 "tool.started",336 {337 toolUseId: string;338 toolName: ToolName;339 }340>;341342export type ToolOutput = Ephemeral<343 "tool.output",344 {345 toolUseId: string;346 chunk: string; // live chunk, ANSI-stripped for TUI347 }348>;349350export type ToolCompleted = Durable<351 "tool.completed",352 {353 toolUseId: string;354 modelText: string; // byte-exact tool_result content (budget-truncated w/ markers)355 spillFile?: string; // full output under ~/.khaelor/spill/ (ADR-8)356 durationMs: number;357 ui: {358 // presentation DATA, not presentation (Rule 3)359 kind: "read" | "search" | "edit" | "exec" | "process";360 summary: string; // e.g. `Search "ContextEngine" · 14 matches`361 diffStats?: DiffStats;362 exitCode?: number;363 matchCount?: number;364 };365 }366>;367368export type ToolFailed = Durable<369 "tool.failed",370 {371 toolUseId: string;372 modelText: string; // byte-exact is_error tool_result content373 errorKind:374 | "invalid-input"375 | "not-found"376 | "ambiguous-edit"377 | "exec-error"378 | "timeout"379 | "permission-denied"380 | "phase-blocked"381 | "internal";382 durationMs: number;383 }384>;385386export type ToolCancelled = Durable<387 "tool.cancelled",388 {389 toolUseId: string;390 reason: "interrupted" | "resume-recovery" | "shutdown";391 modelText: string; // synthetic result, e.g. "[Tool execution cancelled by user]"392 }393>;394395// ─────────────────────── file activity (durable) ───────────────────────396397export type FileRead = Durable<398 "file.read",399 {400 path: string; // relative to workspace cwd401 range?: { start: number; end: number };402 bytes: number;403 mtimeMs: number; // external-modification detection on later writes404 toolUseId: string;405 }406>;407408export type FileModified = Durable<409 "file.modified",410 {411 path: string;412 operation: "write" | "edit";413 diffStats: DiffStats;414 diff?: string; // unified diff, capped (default 32 KiB) with truncation marker415 toolUseId: string;416 }417>;418419// ─────────────── processes (durable + one ephemeral) ───────────────420421export type ProcessStarted = Durable<422 "process.started",423 {424 processId: string; // KHAELOR id (stable across PID reuse)425 pid: number;426 command: string;427 cwd: string;428 name?: string;429 toolUseId: string;430 }431>;432433export type ProcessOutput = Ephemeral<434 "process.output",435 {436 processId: string;437 stream: "stdout" | "stderr";438 chunk: string;439 }440>;441442export type ProcessExited = Durable<443 "process.exited",444 {445 processId: string;446 exitCode: number | null; // null = signal-killed447 cause: "exited" | "stopped-by-tool" | "khaelor-shutdown" | "crashed";448 durationMs: number;449 }450>;451452// ─────────────────────── context engine (durable) ───────────────────────453454export type ContextPruned = Durable<455 "context.pruned",456 {457 toolUseIds: string[]; // results blanked with the FIXED placeholder string458 placeholder: string; // recorded so replay is byte-exact even if default changes459 tokensReclaimedEstimate: number;460 }461>;462463export type ContextCompacted = Durable<464 "context.compacted",465 {466 checkpointYaml: string; // the structured checkpoint (ARCHITECTURE.md §6.4), verbatim467 cut: { fromSeq: number; toSeq: number }; // replaced range — pairing-safe boundary (§6.5)468 trigger: "proactive-token-budget" | "reactive-overflow" | "user-command";469 tokensBefore: number; // from real usage accounting470 summaryModel: string; // the auxModel used471 }472>;473474// ─────────────────────── completion (durable) ───────────────────────475476export type VerificationRequested = Durable<477 "task.verification-requested",478 {479 attempt: 1 | 2;480 detectedChecks: string[]; // e.g. ["npm test", "npx tsc --noEmit"]481 withheldCandidateSeq: number; // seq of the withheld answer's final text block (ADR-12)482 }483>;484485export type TaskCompleted = Durable<486 "task.completed",487 {488 evidence: CompletionEvidence;489 }490>;491492export type TaskFailed = Durable<493 "task.failed",494 {495 reason: "model-fatal-error" | "iteration-budget-exhausted" | "user-abandoned";496 detail: string;497 }498>;499500// ─────────────────────── phase gate events (durable, v2 §1) ───────────────────────501502export type PhaseEntered = Durable<503 "phase.entered",504 {505 phase: Phase;506 /** What caused the transition — the workflow, an approval, or a user override. */507 via: "session-start" | "design-submitted" | "approval" | "user-override";508 }509>;510511export type PhaseArtifact = Durable<512 "phase.artifact",513 {514 artifactId: string;515 artifact: DesignArtifact;516 }517>;518519export type PhaseApproved = Durable<520 "phase.approved",521 {522 phase: Phase;523 approvedBy: "user" | "auto-policy" | "user-override";524 artifactId?: string;525 }526>;527528export type PhaseRejected = Durable<529 "phase.rejected",530 {531 phase: Phase;532 reason: string;533 artifactId?: string;534 }535>;536537// ─────────────────────── native verification (durable, v2 §4) ───────────────────────538539export type VerifyResult = Durable<540 "verify.result",541 {542 /** Check name from verify config: "typecheck", "test", "lint", … */543 check: string;544 command: string;545 ok: boolean;546 exitCode: number | null;547 /** Intelligently truncated output — errors first. */548 output: string;549 durationMs: number;550 }551>;552553// ─────────────────────── project memory (durable, v2 §5) ───────────────────────554555export type MemoryWritten = Durable<556 "memory.written",557 {558 section: string;559 entry: string;560 confidence: "high" | "medium" | "low";561 toolUseId: string;562 }563>;564565// ─────────────────────── parallel subtasks (durable, v2 §6) ───────────────────────566567export type SubtaskCreated = Durable<568 "subtask.created",569 {570 taskId: string;571 description: string;572 /** Child session id (its own JSONL, meta.parent points here). */573 childSessionId: string;574 worktreePath: string;575 branch: string;576 }577>;578579export type SubtaskCompleted = Durable<580 "subtask.completed",581 {582 taskId: string;583 outcome: "done" | "failed" | "interrupted";584 diffStats: DiffStats;585 verifyOk: boolean | null;586 detail: string;587 }588>;589590// ───────────────────────────── unions ─────────────────────────────591592export type DurableEvent =593 | SessionStarted594 | SessionResumed595 | SessionRenamed596 | ModelChanged597 | BaselineRecorded598 | UserMessageCreated599 | SteeringQueued600 | SteeringInjected601 | Interrupted602 | ModelRequestStarted603 | ModelTextBlockCompleted604 | ModelThinkingBlockCompleted605 | ToolRequested606 | ModelResponseCompleted607 | ModelRequestFailed608 | PermissionRequested609 | PermissionGranted610 | PermissionDenied611 | ToolApproved612 | ToolStarted613 | ToolCompleted614 | ToolFailed615 | ToolCancelled616 | FileRead617 | FileModified618 | ProcessStarted619 | ProcessExited620 | ContextPruned621 | ContextCompacted622 | VerificationRequested623 | TaskCompleted624 | TaskFailed625 | PhaseEntered626 | PhaseArtifact627 | PhaseApproved628 | PhaseRejected629 | VerifyResult630 | MemoryWritten631 | SubtaskCreated632 | SubtaskCompleted;633634export type EphemeralEvent =635 | ModelTextDelta636 | ModelThinkingDelta637 | ToolCallStarted638 | ToolInputDelta639 | ToolOutput640 | ProcessOutput;641642export type KhaelorEvent = DurableEvent | EphemeralEvent;643644export type DurableEventType = DurableEvent["type"];645export type EphemeralEventType = EphemeralEvent["type"];646647/** What producers hand to the bus/store; envelope fields are assigned at append time. */648export type DurableEventInput = {649 [T in DurableEventType]: { type: T; payload: Extract<DurableEvent, { type: T }>["payload"] };650}[DurableEventType];651652/** What producers hand to the bus for ephemeral publication. */653export type EphemeralEventInput = {654 [T in EphemeralEventType]: { type: T; payload: Extract<EphemeralEvent, { type: T }>["payload"] };655}[EphemeralEventType];656657// ─────────────── durable/ephemeral classification (runtime) ───────────────658659const DURABLE_TYPE_LIST = [660 "session.started",661 "session.resumed",662 "session.renamed",663 "session.model-changed",664 "git.baseline-recorded",665 "user.message-created",666 "user.steering-queued",667 "user.steering-injected",668 "user.interrupted",669 "model.request-started",670 "model.text-block-completed",671 "model.thinking-block-completed",672 "tool.requested",673 "model.response-completed",674 "model.request-failed",675 "permission.requested",676 "permission.granted",677 "permission.denied",678 "tool.approved",679 "tool.started",680 "tool.completed",681 "tool.failed",682 "tool.cancelled",683 "file.read",684 "file.modified",685 "process.started",686 "process.exited",687 "context.pruned",688 "context.compacted",689 "task.verification-requested",690 "task.completed",691 "task.failed",692 "phase.entered",693 "phase.artifact",694 "phase.approved",695 "phase.rejected",696 "verify.result",697 "memory.written",698 "subtask.created",699 "subtask.completed",700] as const satisfies readonly DurableEventType[];701702const EPHEMERAL_TYPE_LIST = [703 "model.text-delta",704 "model.thinking-delta",705 "model.tool-call-started",706 "model.tool-input-delta",707 "tool.output",708 "process.output",709] as const satisfies readonly EphemeralEventType[];710711type AssertNever<T extends never> = T;712/** Compile-time exhaustiveness guards: fail to compile if a union member is missing from a list. */713export type _DurableListIsExhaustive = AssertNever<714 Exclude<DurableEventType, (typeof DURABLE_TYPE_LIST)[number]>715>;716export type _EphemeralListIsExhaustive = AssertNever<717 Exclude<EphemeralEventType, (typeof EPHEMERAL_TYPE_LIST)[number]>718>;719720/** The durable event types (EVENT_MODEL.md §3 + v2 additions). */721export const DURABLE_EVENT_TYPES: ReadonlySet<DurableEventType> = new Set(DURABLE_TYPE_LIST);722723/** The 6 ephemeral event types (EVENT_MODEL.md §3). */724export const EPHEMERAL_EVENT_TYPES: ReadonlySet<EphemeralEventType> = new Set(EPHEMERAL_TYPE_LIST);725726export function isDurableEventType(type: string): type is DurableEventType {727 return DURABLE_EVENT_TYPES.has(type as DurableEventType);728}729730export function isEphemeralEventType(type: string): type is EphemeralEventType {731 return EPHEMERAL_EVENT_TYPES.has(type as EphemeralEventType);732}733734/** Runtime classification of a full event by its `type` string. */735export function isDurableEvent(event: KhaelorEvent): event is DurableEvent {736 return isDurableEventType(event.type);737}738