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/agent/interruption.ts4 * Description: Interruption controller — turn-scoped abort, durable Interrupted, dangling tool_use closure (ADR-11).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { findDanglingToolUseIds } from "../session/index.js";11import { CANCELLED_RESULT_CONTENT } from "../tools/index.js";12import type { KernelSession } from "./session-handle.js";1314/** Why a dangling tool_use is being closed with a synthetic tool_result. */15export type CancellationReason = "interrupted" | "resume-recovery" | "shutdown";1617/**18 * Close every `ToolRequested` lacking a terminal result with a synthetic,19 * durable `ToolCancelled` so the LLM history stays protocol-valid — every20 * tool_use gets exactly one tool_result (EVENT_MODEL.md §6.5).21 * Returns the toolUseIds that were closed.22 */23export function closeDanglingToolUses(24 session: KernelSession,25 reason: CancellationReason,26): string[] {27 const dangling = findDanglingToolUseIds(session.events());28 for (const toolUseId of dangling) {29 session.publishDurable({30 type: "tool.cancelled",31 payload: { toolUseId, reason, modelText: CANCELLED_RESULT_CONTENT },32 });33 }34 return dangling;35}3637/**38 * Resume recovery (EVENT_MODEL.md §6.5.2): after replay, any tool_use without39 * a terminal event is closed durably AT RESUME TIME — recovery is itself an40 * event, so the next replay needs no recovery.41 */42export function recoverDanglingOnResume(session: KernelSession): string[] {43 return closeDanglingToolUses(session, "resume-recovery");44}4546/**47 * The Esc path (ARCHITECTURE.md §8). `interrupt()`:48 * 1. records the durable `Interrupted` event (state the next `deriveNext`49 * observes — exit conditions derive from state, never in-memory flags),50 * 2. aborts the turn controller — the cancellation root for the model51 * stream and every in-flight tool (ADR-11).52 * Dangling tool_use blocks are then closed by the kernel's interrupted53 * branch via `closeDanglingToolUses`, keeping the session resumable.54 * Background `process`-managed processes are NOT touched.55 */56export class InterruptionController {57 readonly #session: KernelSession;58 #turn: AbortController | null = null;5960 constructor(session: KernelSession) {61 this.#session = session;62 }6364 /** Called by the kernel at the start of every run — registers the cancellation root. */65 beginTurn(controller: AbortController): void {66 this.#turn = controller;67 }6869 /** Called by the kernel when the run ends (any outcome). */70 endTurn(): void {71 this.#turn = null;72 }7374 /** True while a turn is running and not yet aborted. */75 get turnActive(): boolean {76 return this.#turn !== null && !this.#turn.signal.aborted;77 }7879 /**80 * Interrupt the current turn. Returns false when there is nothing to81 * interrupt (no active turn, or already aborted) — idempotent.82 */83 interrupt(): boolean {84 const turn = this.#turn;85 if (turn === null || turn.signal.aborted) return false;86 const pendingToolUseIds = findDanglingToolUseIds(this.#session.events());87 this.#session.publishDurable({88 type: "user.interrupted",89 payload: { scope: "turn", pendingToolUseIds },90 });91 turn.abort();92 return true;93 }94}95