SPB Git

Repository Public

stall.ts 3.0 KB 82 lines typescript

1/**2 * Deterministic stall detection (CONTRACT §5.5). Fed only with journaled data, so a replay reaches the3 * same triggers at the same points.4 *5 *  - the same tool call (name + canonical input) N times in a row with the same result;6 *  - M model turns without progress (a successful file write, a commit, a criterion change, or an7 *    interaction with the owner).8 * First trigger ⇒ an operator note; second ⇒ a question to the owner; after the owner answered the9 * cycle starts again.10 */11import { canonicalJson } from '@aos/kernel';12import { sha256Hex } from '@aos/tools';1314export type StallAction = 'none' | 'note' | 'ask';1516export class StallDetector {17  private lastSig: string | null = null;18  private repeats = 0;19  private idleTurns = 0;20  private stage: 0 | 1 = 0;21  private reasonText = '';2223  constructor(24    private readonly repeatLimit: number,25    private readonly turnLimit: number,26  ) {}2728  /** One executed (or refused) tool call and what the model saw as its result. */29  recordCall(name: string, input: unknown, ok: boolean, content: string): void {30    const sig = sha256Hex(`${name}\n${canonicalJson(input)}\n${ok ? 1 : 0}\n${content}`);31    if (sig === this.lastSig) this.repeats++;32    else {33      this.lastSig = sig;34      this.repeats = 1;35    }36  }37  /** A model turn ended (with or without tool calls). */38  recordTurn(): void {39    this.idleTurns++;40  }41  /** A successful write / commit / criterion change, or an owner interaction. */42  progress(): void {43    this.idleTurns = 0;44  }4546  /** Call once per turn, after its tool batch. Resets the counter that fired. */47  check(): StallAction {48    let reason: string | null = null;49    if (this.repeats >= this.repeatLimit) reason = `the same tool call returned the same result ${this.repeats} times in a row`;50    else if (this.idleTurns >= this.turnLimit) reason = `${this.idleTurns} turns have passed without a successful write, commit or criterion change`;51    if (!reason) return 'none';52    this.reasonText = reason;53    this.repeats = 0;54    this.lastSig = null;55    this.idleTurns = 0;56    if (this.stage === 0) {57      this.stage = 1;58      return 'note';59    }60    this.stage = 0;61    return 'ask';62  }63  get reason(): string {64    return this.reasonText;65  }66}6768export const STALL_NOTE = (reason: string): string =>69  `You appear stuck: ${reason}. Stop and re-plan: state what you know, what you tried and why it failed, then take a different approach — ` +70  'or, if you cannot make progress without the owner, ask them one precise question with ask_owner, or finish with "blocked".';7172export const STALL_CONTINUE = 'Let it continue';73export const STALL_STOP = 'Stop the mission';74export function stallQuestion(reason: string, lastText: string): Record<string, unknown> {75  return {76    text: 'The agent appears stuck. How should it proceed?',77    detail: `Reason: ${reason} (for the second time).${lastText ? `\n\nLast message from the agent:\n${lastText.slice(0, 1500)}` : ''}\n\nReply with guidance, or choose an option.`,78    options: [STALL_CONTINUE, STALL_STOP],79    source: 'stall',80  };81}82
Files main
Menu

Theme