/** * sim/agent.ts — the user-simulator behavioral core, ported from mirofish * `engine/agent/agent.py`. * * UserSimulatorAgent = a functional kernel (immutable frames) inside an async * OO shell: open/run/step/execute/end are async I/O; get/fork/fromFrame are * sync pure data. One instance = one logical timeline — never run step/run * concurrently on the same instance (concurrency lives between instances). * The end (runtime) is acquired by the host and merely borrowed here. * * The run loop's verdict order is behavior, not style — it mirrors the Python * source clause for clause: * error streak → broken-ReAct streak → give_up → done (closure gate, with * the persistence bypass) → max_turns timeout → stuck abandon; non-terminal * steps get the terminal/stuck nudges injected as system observations. */ import type { MutableModels, ThinkingLevel } from "@earendil-works/pi-ai"; import { type Action, type ClosureCheck, type Part, type SessionFrame, type SessionStatus, type SimRuntime, type StepRecord, type TriggerRequest } from "./models.ts"; export { NO_PROGRESS_NOTE } from "./prompts.ts"; /** Consecutive inference-degraded steps before the session breaker trips. */ export declare const ERROR_STREAK_LIMIT = 3; /** Consecutive "actions without a thought" steps before the breaker trips. */ export declare const BROKEN_REACT_STREAK_LIMIT = 3; /** Consecutive unchanged observations before the change-approach nudge. */ export declare const STUCK_NUDGE_AT = 3; /** Consecutive unchanged observations before an honest abandon. */ export declare const STUCK_ABANDON_LIMIT = 5; /** done retried ≥N times with ≥N real operates → closure gate lets it pass. */ export declare const DONE_PERSIST_ACCEPT = 3; /** Trailing count of steps whose inference degraded (error non-null). */ export declare function errorStreak(history: StepRecord[]): number; /** Trailing count of broken-chain steps: empty thought + actions, not errored * — the fingerprint of a hidden-reasoning model breaking the ReAct chain. */ export declare function brokenReactStreak(history: StepRecord[]): number; /** Trailing count of steps whose world state did not change at all. */ export declare function noProgressStreak(history: StepRecord[]): number; /** check "substring:X" → hard match; "semantic:X"/empty → judged; no check → * the text itself is the semantic expectation. */ export declare function parseCheck(cc: ClosureCheck): ["substring" | "semantic", string]; export interface SimAgentOptions { runtime?: SimRuntime | null; /** "provider/model-id"; unset → defaultSimModel(). */ model?: string; thinkingLevel?: ThinkingLevel | null; debug?: boolean; /** Test seam: pre-configured registry (faux provider). */ models?: MutableModels; } export type OnStep = (agent: UserSimulatorAgent) => Promise | void; export declare class UserSimulatorAgent { private frame; private rt; private model?; private thinkingLevel?; private debug; private models?; private opened; constructor(trigger: TriggerRequest, opts?: SimAgentOptions); /** Resume from an existing frame (pause/persistence/counterfactual branch). * Rebuilds sink.thread from history unless the injected runtime already has * one; resets `opened` so run() re-runs init and re-observes. */ static fromFrame(frame: SessionFrame, opts?: SimAgentOptions): UserSimulatorAgent; get runtime(): SimRuntime | null; /** Opening (a first-class lifecycle moment): run runtime.init for the first * observation. Idempotent — one opening per instance. Calls no LLM, appends * no StepRecord, never acquires/releases the end. */ open(): Promise; /** Autonomous: open → loop (decide → execute → new observation) until a * terminal verdict. seed = observation parts prepended before opening. */ run(seed?: Part[] | null, onStep?: OnStep): Promise; private runLoop; /** Driven mode: one reasoning step; observation = the result of executing * the previous step's actions. Serial per instance. The optional signal * aborts the underlying LLM call for real (world-layer step timeouts must * not leave a zombie inference appending history after the verdict). */ step(observation: Part[], signal?: AbortSignal): Promise; /** Execute this step's actions into the next observation. operate delegates * to the execution layer; speak/done/give_up never touch the end; unknown * types answer gracefully. No output → a light end observation keeps the * causal chain intact (vision ends keep one frame per step). */ execute(actions: Action[]): Promise; private executeActions; /** Fold sink.cost (executor model usage) into the step just appended — one * step's cost covers both the planner and the executor models. */ private foldExecCost; /** Fold sink.opTrace (per-operate grounding trails) into operate_log — * audit/display, never fed back to the planner. */ private foldOpTrace; /** Append execution-phase spans to the step's trace (the decide phase landed * in step()); both phases form one timeline. */ private foldTrace; private recordUseCalls; /** Seal the final status (the end is NOT released — that is the host's). */ end(status?: SessionStatus): Promise; /** Closure gate: unmet success checks (substring hard-matched against recent * evidence; semantic checks judged by the mini verifier). Empty = may close. */ closureUnmet(): Promise; /** The built-in mini closure judge. Conservative wording; judge-infra * failures FAIL OPEN (a broken judge must not drag a good run to timeout). */ private semanticCheckMet; /** Frame snapshot; get().history.at(-1) is the latest step. */ get(): SessionFrame; /** Branch a new instance from step index (reuses history[0..idx], new * session id, status back to running, fresh timeline). Pure data — the * branch carries NO runtime; attach a fresh one before running. */ fork(fromIndex: number): UserSimulatorAgent; /** Stamp the causal-order Lamport clock on the latest step (world layer). */ stampLc(lc: number): void; }