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/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