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.2 KB · 95 lines typescript
Raw Blame History
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