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.8 KB · 115 lines typescript
Raw Blame History
1/**2 * KHAELOR3 * File: src/agent/steering.ts4 * Description: Queued steering — mid-turn user text queued durably, injected only at the two safe seams (ADR-11).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import type { DurableEvent } from "../session/index.js";11import type { KernelSession } from "./session-handle.js";1213/** A queued instruction not yet injected into LLM history. */14export interface QueuedSteering {15  /** Envelope id of the SteeringQueued event (SteeringInjected references it). */16  queuedEventId: string;17  text: string;18}1920/** Steering-queued events without a matching SteeringInjected, in seq order. */21export function pendingSteering(events: readonly DurableEvent[]): QueuedSteering[] {22  const queued = new Map<string, string>();23  for (const event of events) {24    if (event.type === "user.steering-queued") {25      queued.set(event.id, event.payload.text);26    } else if (event.type === "user.steering-injected") {27      queued.delete(event.payload.queuedEventId);28    }29  }30  return [...queued.entries()].map(([queuedEventId, text]) => ({ queuedEventId, text }));31}3233/**34 * Determine the injection seam from recorded state (EVENT_MODEL.md §4):35 * post-tool-batch when the most recent conversation event is a tool result36 * (the queued text joins that tool-result user message), pre-model-call37 * otherwise (appended to the last user message) — never a bare mid-alternation38 * user message.39 */40export function steeringSeam(events: readonly DurableEvent[]): {41  seam: "post-tool-batch" | "pre-model-call";42  afterSeq: number;43} {44  let lastSeq = 0;45  let lastToolResultSeq = 0;46  let lastOtherConversationSeq = 0;47  for (const event of events) {48    lastSeq = event.seq;49    switch (event.type) {50      case "tool.completed":51      case "tool.failed":52      case "tool.cancelled":53        lastToolResultSeq = event.seq;54        break;55      case "user.message-created":56      case "model.text-block-completed":57      case "model.thinking-block-completed":58      case "tool.requested":59        lastOtherConversationSeq = event.seq;60        break;61      default:62        break;63    }64  }65  if (lastToolResultSeq > 0 && lastToolResultSeq > lastOtherConversationSeq) {66    return { seam: "post-tool-batch", afterSeq: lastToolResultSeq };67  }68  return { seam: "pre-model-call", afterSeq: lastSeq };69}7071/**72 * The steering queue (ARCHITECTURE.md §8). `queue()` records the instruction73 * durably the moment it arrives (it survives a crash before injection and74 * renders as `Queued instruction`); the kernel drains the queue with75 * `injectPending()` only at a safe seam — after tool results, before the76 * next model call — recorded as durable SteeringInjected events that make77 * replay byte-exact (ADR-7).78 */79export class SteeringQueue {80  readonly #session: KernelSession;8182  constructor(session: KernelSession) {83    this.#session = session;84  }8586  /** Record a mid-turn instruction. Safe to call at any time, even mid-stream. */87  queue(text: string): DurableEvent {88    return this.#session.publishDurable({ type: "user.steering-queued", payload: { text } });89  }9091  /** Queued instructions not yet injected. */92  pending(): QueuedSteering[] {93    return pendingSteering(this.#session.events());94  }9596  /**97   * Drain the queue at the current (safe) seam. Called by the kernel only98   * when no tool batch is pending and no model stream is running.99   * Returns the number of instructions injected.100   */101  injectPending(): number {102    const events = this.#session.events();103    const pending = pendingSteering(events);104    if (pending.length === 0) return 0;105    const { seam, afterSeq } = steeringSeam(events);106    for (const item of pending) {107      this.#session.publishDurable({108        type: "user.steering-injected",109        payload: { queuedEventId: item.queuedEventId, seam, afterSeq },110      });111    }112    return pending.length;113  }114}115