/** * sim/models.ts — data contracts of the user-simulator (L1), ported from * mirofish `engine/models.py` (+ the `common` primitives it builds on). * * The planning layer's constitution, condensed: * (persona + history + observation) → behavior. Every step is observe → * think → act (ReAct). No explicit goal field — intent stays implicit in * free-text thought/history. History is the cognitive chain and is never * compressed. Frames are immutable data: each step yields a new frame, * history only appends; fork = truncate + new id. * * Two kinds of objects live here and must never be mixed: * - pure data (Persona/Scenario/StepRecord/SessionFrame/World…): JSON-ready, * serializable, forkable; * - live resources (SimRuntime/ExecSink/Executor): process-bound handles that * never enter a frame. */ /** created/updated/finished trio, ISO-8601 UTC strings (JSON-ready). */ export interface Timestamps { created_at: string; updated_at?: string | null; finished_at?: string | null; } export declare function newTimestamps(): Timestamps; /** Return a finished copy (finished_at = updated_at = now). */ export declare function finishTimestamps(ts: Timestamps): Timestamps; /** created_at → finished_at in ms; null while unfinished. */ export declare function durationMs(ts: Timestamps | undefined | null): number | null; /** Per-call token usage, langchain UsageMetadata shape (kept verbatim so the * viewer/metrics field names match the mirofish records bit for bit). */ export interface UsageMetadata { input_tokens: number; output_tokens: number; total_tokens: number; /** e.g. { cache_read, cache_creation } */ input_token_details?: Record; /** e.g. { reasoning } */ output_token_details?: Record; } /** Usage bucketed by model name. Pure token accounting — no dollars here. */ export type ModelUsage = Record; /** Sum several {model: usage} maps by model name. */ export declare function mergeUsage(...dicts: Array): ModelUsage; /** One block of observation/utterance. `ref` is a reference (path/URL), never * inlined base64. `to` = addressed recipients (undefined ⇒ broadcast); * `lc` = Lamport stamp (world layer). */ export interface Part { modality: "text" | "image" | "video" | "audio" | "dom"; source?: string | null; text?: string | null; ref?: string | null; to?: string[] | null; lc?: number | null; } export interface PersonaProfile { text: string; } export interface PersonaSkill { name: string; text: string; } /** One retrievable episode (rag-as-self unit): situation is the retrieval key. */ export interface PersonaEpisode { situation: string; behavior?: string; outcome?: string; } /** Free-text memory (injected whole when no episodes) or discrete episodes * recalled top-k by situation. */ export interface PersonaMemory { text?: string; episodes?: PersonaEpisode[]; } export interface Persona { id: string; profile: PersonaProfile; skills: PersonaSkill[]; memory: PersonaMemory; tags?: string[]; } export type ScenarioKind = "task_completion" | "open_exploration" | "social_interaction" | "evaluation_probe"; export interface ScenarioGoal { text: string; } export interface ScenarioEnv { text: string; } /** Machine-checkable closure predicate: check = "substring:X" (hard match in * recent evidence) or "semantic:X"/undefined (judged by the mini verifier). */ export interface ClosureCheck { text: string; check?: string | null; } export interface ScenarioClosure { text: string; success?: ClosureCheck[]; abandon?: ClosureCheck[]; /** The one hard budget (live-step cap; bounds infinite silence too). */ max_turns: number; } export interface Scenario { id: string; kind: ScenarioKind; goal: ScenarioGoal; env: ScenarioEnv; closure: ScenarioClosure; tags?: string[]; } /** One thing done; speak/done/give_up are actions too. * type ∈ registered use names ∪ {speak, done, give_up, operate, write_file}. */ export interface Action { type: string; args: Record; } /** Observable detail of one use execution (debug channel). */ export interface UseCall { name: string; args: Record; ok: boolean; error?: string | null; result: Part[]; latency_ms: number; } /** Full inner detail of a step (debug only; not a causal contract). */ export interface StepDebug { model: string; system_prompt: string; user_message: string; raw_response: string; llm_latency_ms: number; use_calls: UseCall[]; } /** One executor sub-action inside an operate (ported from the simulator SDK's * SubStep TypedDict — every key declared, so no evidence is silently dropped). */ export interface SubStep { action?: string; args?: Record; /** Human-readable result — exactly what the executor model saw, unedited. */ result?: string; result_json?: Record | null; ok?: boolean; /** Post-substep screenshot ref (none on shell). */ shot?: string | null; t_ms?: number; /** Screen did not change after the action — honesty marker. */ noop?: boolean; /** Identical to the previous action; consecutive count starting at 2. */ repeat?: number; recording?: string | null; } /** Executor phase timing breakdown. */ export interface PhaseTiming { llm_ms?: number; llm_rounds?: number; observe_ms?: number; act_ms?: number; shot_ms?: number; upload_wait_ms?: number; } /** Per-operate grounding trail: audit/display only, never fed back to the * planner. Folded into StepRecord.operate_log via the sink. */ export interface OperateTrace { command: string; ok: boolean; summary: string; steps: SubStep[]; ts: Timestamps; cost: ModelUsage; timing: PhaseTiming; } export type StepOrigin = "live" | "injected" | "synth" | "fork"; /** One step = observe → think → act. Invariant: observation == the result of * executing the previous step's actions. */ export interface StepRecord { ts: Timestamps; observation: Part[]; thought: string; /** Empty = genuine silence (sparse by default — the anti-bias stance). */ actions: Action[]; operate_log: OperateTrace[]; /** Planner + executor usage for this step, bucketed by model. */ cost: ModelUsage; /** Non-null = inference degraded to silence this step; consecutive errors * trip the session-level breaker. */ error?: string | null; /** Lamport stamp (actor causal order); null outside world scheduling. */ lc?: number | null; /** Flat span list for this step (see sim/trace.ts); waterfall + metrics. */ trace: SpanDict[]; debug?: StepDebug | null; origin?: StepOrigin; } /** Budget rule: only steps this session actually executed (origin=="live") * count against max_turns. Injected/resumed history still enters context and * the behavior guards (streaks look at full history — faithfully simulating a * person who carries that past). */ export declare function liveTurns(history: StepRecord[]): number; /** Flat span: tree via `p` (parent index). kind llm/device/process never nest * into each other (metrics sums by kind); "phase" is a grouping container. */ export interface SpanDict { i: number; p: number | null; name: string; kind: "llm" | "device" | "process" | "phase" | string; t0: number; ms: number; ok?: boolean; note?: string; attrs?: Record; } /** Options for one grounding delegation. maxSteps is clamped to [1, 40]. */ export interface ExecOptions { context?: string; expect?: string; maxSteps?: number | null; } export declare function clampMaxSteps(v: unknown): number | null; /** Result of one grounding delegation (ExecResult in the Python SDK). */ export interface ExecResult { ok: boolean; command: string; steps: SubStep[]; summary: string; cost: ModelUsage; timing: PhaseTiming; /** Executor-side span tree, grafted into the caller's trace. */ trace: SpanDict[]; } /** An injected high-level capability. With an executor attached, uses belong to * the execution layer; without one they are promoted to first-class planner * tools (see prompt.toolSpecs). */ export interface Use { name: string; description: string; /** Flat {name: description} map or a JSON-schema object. */ params?: Record; /** Target end kind; "" = end-agnostic. */ end?: string; /** In-process callback. */ run?: (rt: SimRuntime, args: Record) => Promise; } /** * The execution layer behind `operate` — mirofish's `Sandbox` handle reduced * to the surface L1 actually uses. The default implementation (P2) wraps one * pi-gui agent sub-loop per operate; a shape executor exists for offline runs. */ export interface Executor { /** End kind: "browser" | "mobile" | "desktop" | "shell". Gates the operate * vocabulary (shell = literal command; vision = semantic intent). */ readonly kind: string; exec(command: string, opts: ExecOptions, hooks?: ExecHooks): Promise; /** Current-frame ref for observations (screenshot path/URL); null when the * end has no picture (shell) or capture failed. */ snapshot?(): Promise; /** Light state probe (browser page title etc.). */ state?(): Promise<{ title?: string; } | null>; /** Shell only: write a file, returns byte count. */ writeText?(path: string, content: string): Promise; } export interface ExecHooks { /** Live per-substep push (host-injected). */ onSubstep?: (substep: Record) => Promise | void; /** Client-side uses available to the executor. */ uses?: Use[]; } /** Per-step execution side-channel (resource, never enters a frame). * cost/opTrace flow back from runOperate and are drained into the StepRecord * each step; thread keeps a rolling 3-line digest of recent delegations. */ export interface ExecSink { cost: ModelUsage; opTrace: OperateTrace[]; live?: ((substep: Record) => Promise | void) | null; thread: string[]; } export declare function newExecSink(): ExecSink; /** End container: executor handle + opening init + execution-layer uses. * Holding handles = resource, not data: never serialized, never forked. */ export interface SimRuntime { /** null = no end (pure cognition; no operate verb). */ executor: Executor | null; /** Opening observation producer: await init(rt) → history[0].observation. */ init?: ((rt: SimRuntime) => Promise) | null; uses: Use[]; sink: ExecSink; } /** End kind of a runtime; null = endless. Drives vocabulary gating. */ export declare function runtimeType(rt: SimRuntime | null | undefined): string | null; /** Pure-data session input. Engagement has no knob — it emerges from the * persona text (AgentMode was deliberately removed upstream). */ export interface TriggerRequest { persona: Persona; scenario: Scenario; } export type SessionStatus = "running" | "succeeded" | "abandoned" | "timeout" | "error"; export interface SessionFrame { session_id: string; trigger: TriggerRequest; status: SessionStatus; ts: Timestamps; /** The causal chain itself (faithful, never compressed by default). */ history: StepRecord[]; } export declare function newSessionFrame(trigger: TriggerRequest): SessionFrame; /** history[-1] or null. */ export declare function currentStep(frame: SessionFrame): StepRecord | null; /** Whole-session cost: per-step cost summed by model name. */ export declare function frameCost(frame: SessionFrame): ModelUsage; /** World-level closure. quiesce_rounds (0 = off): all running agents silent * (non-terminal + no operate + no speak) for N consecutive rounds ⇒ early stop. */ export interface WorldClosure { text?: string; max_steps: number; quiesce_rounds?: number; } export interface InMemoryEnvSpec { kind: "in_memory"; } export interface FileSystemEnvSpec { kind: "filesystem"; root?: string | null; } /** Several agents share one live end (same forum/group chat); the live * executor is injected at build time (borrowed, never acquired here). */ export interface SharedEndEnvSpec { kind: "shared_end"; } export interface PlatformEnvSpec { kind: "platform"; feed?: string; feed_size?: number; seed?: number | null; social_p?: number; social_edges?: Array<[string, string]> | null; } export interface IsolatedEnvSpec { kind: "isolated"; framing?: string; } export type EnvironmentSpec = InMemoryEnvSpec | FileSystemEnvSpec | SharedEndEnvSpec | PlatformEnvSpec | IsolatedEnvSpec; /** Thin world declaration (agents-agnostic). No id, no sim clock, no * activation policy — the actor event loop is the only execution model. */ export interface World { situation: string; environment?: EnvironmentSpec; closure: WorldClosure; tags?: string[]; } /** Derived panorama (computed, never stored). */ export interface WorldSnapshot { step_no: number; medium: Part[]; frames: Record; }