import type { EnvironmentConfig, GitCloneConfig, Stimulus } from "../eval/types.js"; import type { Executor, ExecutorOptions } from "../executor/types.js"; import type { ReasoningEffort } from "../graders/llm/types.js"; import type { Skill } from "../skill/types.js"; import type { Trajectory } from "../trajectory/types.js"; export interface EvalRunOptions { /** The prompt to send to the agent */ prompt: string; /** Stimulus name (used to label the trajectory) */ stimulusName?: string; /** * Full Stimulus object from an eval spec. When provided, this is passed * directly to the executor so the trajectory embeds grader configs, * environment, etc. When omitted, a minimal { name, prompt } is used. */ stimulus?: Stimulus; /** Skills to load (empty array for baseline runs) */ skills: Skill[]; /** Original working directory for the eval run (used as context for skill discovery; the actual agent workspace is created separately) */ workDir: string; /** The executor to use */ executor: Executor; /** Hard wall-clock cap in ms; expiry aborts and fails the run (default 120_000). */ timeout?: number; /** Agent working-time limit in ms. Forwarded to {@link ExecutorOptions.maxAgentDurationMs}. */ maxAgentDurationMs?: number; /** Model override */ model?: string; /** Reasoning effort for the agent under test. Forwarded to * {@link ExecutorOptions.reasoningEffort}. */ reasoningEffort?: ReasoningEffort; /** Executor-specific config (opaque), forwarded to the executor's `executorConfig` option. */ executorConfig?: unknown; /** Environment setup (files to copy, commands to run, git worktree to create) */ environment?: EnvironmentConfig; /** * Base directory for resolving relative paths in environment config * (e.g., `git.source`, `file.src`). When omitted, relative paths are * used as-is (only safe when already absolute). */ baseDir?: string; /** * Explicit workspace directory for the agent run. * * In --eval-spec mode this is a parent directory — the CLI creates a * per-stimulus subdirectory under it (e.g., `/my-stimulus/`). * In inline-prompt mode it is used directly as the workspace. * * When set, cleanup() becomes a no-op so the workspace (and any git * worktree created inside it) is preserved for inspection. */ workspace?: string; /** * A pre-acquired workspace from {@link PreparedWorkspace.acquire}. When * provided, runEval uses `workDir` as the agent's workspace and skips * environment setup. The caller (typically `runMultiTrial`) is * responsible for terminating the acquired workspace's lifecycle via * `preserve()` or `release()` after the run completes. A defensive * `release()` after a successful `preserve()` is permitted as a no-op * cleanup. */ preparedWorkspace?: AcquiredWorkspace; /** * Callback invoked for each raw executor event during execution. * Forwarded to the executor's `onRawEvent` option. */ onRawEvent?: (event: unknown) => void; /** * Per-run native executor session-log destination. * Forwarded to the executor's `sessionLog` option. */ sessionLog?: ExecutorOptions["sessionLog"]; /** * W3C trace context captured by the caller for this execution. * Forwarded to executors that propagate traces into external runtimes. */ traceContext?: ExecutorOptions["traceContext"]; /** * When true, capture a unified diff of workspace changes and record it on * `trajectory.workspacePatch`. */ captureWorkspacePatch?: boolean; } export interface EvalRunResult { /** The trajectory captured during the run */ trajectory: Trajectory; /** Path to the working directory after the run (for file-based grading) */ workDir: string; /** * Present when the workspace is a git worktree (environment.git * `type: "worktree"`). Lets a `--workspace` preserve relocate the worktree's * git metadata instead of orphaning it. Absent for clone/no-git runs. */ worktree?: GitWorktreeHandle; /** Cleanup function — call when done grading */ cleanup: () => Promise; } /** * A reusable, post-environment-setup workspace snapshot that can be * materialized into independent per-trial workspaces. Designed to amortize * the cost of environment setup across multiple trials of a stimulus. * * Concurrency: `acquire()` may be called concurrently. Each call returns an * independent {@link AcquiredWorkspace} so trials of the same stimulus can * run in parallel against the same prepared snapshot. * * Note: the snapshot is copied per acquisition. Environment setups whose * artifacts contain absolute paths (e.g. python venvs with hardcoded * shebangs, build caches with embedded paths) may break when a trial runs * from a different copy than the one in which setup ran. Such evals should * either avoid `prepareWorkspace` or restructure their setup to be * path-independent. */ export interface PreparedWorkspace { /** * Acquire a fresh workspace dir cloned from the prepared snapshot. May be * called concurrently. The caller MUST terminate the returned * {@link AcquiredWorkspace}'s lifecycle by calling exactly one of * `preserve()` or `release()`. A subsequent `release()` after a successful * `preserve()` is permitted as a defensive cleanup (it's a no-op when the * source is already gone) — see {@link AcquiredWorkspace} for details. * * Rejects with a "disposed" error if `dispose()` has been called or runs * concurrently with this acquisition. */ acquire(): Promise; /** * Dispose the underlying snapshot. Waits for any in-flight `acquire()` * calls to finish (so they either complete with a valid clone or reject * with the "disposed" error — never with raw filesystem errors against a * vanishing snapshot). After dispose, future `acquire()` calls reject. * AcquiredWorkspaces already obtained remain valid until preserved or * released. */ dispose(): Promise; } /** * One independent workspace materialized from a {@link PreparedWorkspace}. * * Lifecycle: created by `PreparedWorkspace.acquire()`. The owner must call * exactly one of `preserve()` or `release()` to terminate the lifecycle. * `release()` is also always safe to call after `preserve()` as a defensive * cleanup — it best-effort scrubs any stranded source temp dir (e.g., from * an EXDEV copy whose post-copy source removal failed). */ export interface AcquiredWorkspace { /** The independent workspace directory for this acquisition. */ workDir: string; /** * Move the workspace contents to a destination directory (used for * `--workspace`-preserved trial dirs). * * Idempotent: repeated or concurrent calls — only the first claim runs to * completion; subsequent calls short-circuit and return without touching * the filesystem. Callers should not invoke `preserve()` with different * destinations on the same AcquiredWorkspace; only the first destination * is populated. * * On success, `destDir` is fully populated. The source temp dir is removed * best-effort; if cleanup fails (rare — typically a tmpfs anomaly), * `preserve()` still resolves and a subsequent `release()` will retry the * source-dir cleanup. */ preserve(destDir: string): Promise; /** * Best-effort cleanup of the source temp workspace. Idempotent and always * safe to call: after a successful `preserve()` (rename or EXDEV+rm), this * is a no-op; after a thrown or partial `preserve()`, it scrubs any * stranded `workDir`. Callers that always pair `preserve()` with a * `release()` in a `finally` block get leak-free behavior for free. */ release(): Promise; } /** * Execute a single eval run: setup environment, run agent, return trajectory. * * The caller is responsible for calling `result.cleanup()` when done * (e.g., after grading has inspected workspace files). */ export declare function runEval(options: EvalRunOptions): Promise; /** * Lifecycle handle for a detached git worktree created for an eval run. * * A worktree records its path in two places — the worktree's own `.git` file and * the source repo's `.git/worktrees//gitdir`. When a run happens in a temp * dir that is later moved onto a `--workspace` path (the default, retry-eligible * flow), a plain directory move updates neither, orphaning the worktree. The * handle lets the preserve step {@link GitWorktreeHandle.relocate} the worktree — * running `git worktree repair` (Git 2.30+) to re-link both references — instead * of leaving broken metadata behind. * * After a successful relocate the worktree is *transferred* to a user-owned * path, so {@link GitWorktreeHandle.remove} becomes a no-op: running * `git worktree remove` then would prune the just-preserved worktree and * re-break it. */ export interface GitWorktreeHandle { /** Absolute path to the source repo this worktree belongs to. */ readonly source: string; /** * Re-link the worktree metadata after its directory has been moved to * `newPath`, reconciling any duplicate/stale admin entry, and mark it * transferred so {@link remove} is a no-op. Throws if `git worktree repair` * fails, so the caller can roll the move back. */ relocate(newPath: string): Promise; /** * Best-effort re-link after a failed placement left the worktree at * `survivingPath`. Returns the handle to the attached state so normal * {@link remove} cleanup applies. Never throws. */ rollbackTo(survivingPath: string): Promise; /** * Detach the worktree from its source repo via `git worktree remove`. A no-op * once the worktree has been transferred to a user-owned path. */ remove(): Promise; } export interface GitCloneSetupOptions { /** Remove the partially initialized workspace when clone setup fails. Defaults to `true`. */ cleanupOnFailure?: boolean; /** Environment for Git subprocesses. Replaces `process.env` when provided. */ env?: NodeJS.ProcessEnv; } /** * Clone a remote repo into `workDir` at `git.ref` (default branch when omitted), * honoring optional shallow depth and sparse-checkout paths via init + fetch + * detached checkout. The workspace *is* the clone, so there's no cleanup to * return. A clone is not atomic (`git init` writes `.git` before a later step * can fail). `cleanupOnFailure` defaults to `true` and recursively removes * `workDir` after a setup failure; callers that own the directory and want to * inspect partial state must pass `false`. When provided, `env` replaces * `process.env` for Git subprocesses rather than augmenting it. */ export declare function setupGitClone(git: GitCloneConfig, workDir: string, options?: GitCloneSetupOptions): Promise; /** * Apply a stimulus's environment config to `workDir`: git worktree/clone → files * → skills → commands. Returns a cleanup for the git worktree (if created); * callers own removing `workDir` itself. Exported for reuse by standalone * workspace materialization ({@link materializeWorkspace}). */ export declare function applyEnvironment(env: EnvironmentConfig, workDir: string, baseDir?: string, cleanupOnFailure?: boolean, assetsDir?: string): Promise<(() => Promise) | undefined>; /** True when the environment stages any setup file outside the graded workspace. */ export declare function environmentStagesAssets(env?: EnvironmentConfig): boolean; /** * Overlay `grading_environment.files` into an already-materialized grading * workspace (`workDir`) for the grading step only. * * Unlike {@link EnvironmentConfig.files} (the agent environment), these are * staged **after** the agent has run (and after the workspace diff is captured), * so the agent under test never sees them and they don't appear in the recorded * diff. This is meant to run against a **disposable** grading workspace (see * {@link gradeInDisposableWorkspace}), which owns teardown — so there is no * cleanup to return: the whole grading view is destroyed after grading. * * Staging is **authoritative and never follows symlinks**: because the grading * view is a copy of the agent's own output, an agent could have left a symlink * as a destination ancestor to redirect the write outside the view, or * pre-created a file at a fixture's `dest` to shadow it. For each entry this: * 1. rejects a `dest` that is absolute, contains `..`, or names no file * (confinement — enforced again here as a backstop to lint-time checks); * 2. walks the destination ancestors with `lstat` (no follow), replacing any * symlink or non-directory in the way with a real directory; * 3. `lstat`s the final `dest` and removes whatever is there (file, dir, or * symlink — without following it) before copying the fixture in. * * So a trusted grader file deterministically wins its declared `dest`, and no * agent-created symlink is ever traversed. * * `baseDir` resolves relative `src` paths (the eval-file directory), matching * {@link EnvironmentConfig.files}. * * **Backend author contract.** {@link import("../backend/types.js").Backend} * implementations that grade in their own environment should prefer * {@link gradeInDisposableWorkspace}, which calls this against a throwaway copy * and guarantees the produced workspace is exported untouched. Skipping grader * staging entirely silently drops the feature: a workspace grader that reads a * grader file passes on {@link LocalBackend} but fails on the backend that * forgot to stage them — a backend-dependent grading discrepancy. */ export declare function applyGraderFiles(graderFiles: Array<{ src: string; dest: string; }> | undefined, workDir: string, baseDir?: string): Promise; /** * Grade against a **disposable copy** of the produced workspace, so grading * never mutates the tree that gets preserved/exported. * * The workspace the executor produced (`trajectory.workDir`) is treated as * frozen: this copies it into a private throwaway view, overlays * `grading_environment.files` into the *copy*, runs `grade` against a trajectory * clone pointed at the copy, then destroys the copy wholesale. The original * workspace is never touched, so: * * - grader-only files can't leak into a preserved/exported workspace even if * cleanup is interrupted (the export egresses the untouched original); * - a `program` grader that mutates files (builds, caches) only scribbles on the * throwaway copy, not the evaluated result. * * The clone drops `baselineGitDir`/`baselineRef` so a diff grader can't * *recompute* a diff against the fixture-overlaid copy (which would leak grader * files into diff evidence). To keep valid diff evidence, the diff is computed * against the *original* workspace before cloning (when it wasn't already * captured as `trajectory.diff`/`diffPath`) and carried onto the clone as an * immutable inline `diff`. `assetsDir`/`artifactDir` live outside `workDir` and * are NOT copied — Stage-2 isolation covers `workDir` only; a grader reading * those dirs still sees the real (agent-writable) directories. * * Note: a byte/reflink copy is not an adversarial sandbox — agent-authored code * run by a grader can still reach outside the copy via absolute symlinks. Real * isolation of untrusted grader execution requires a sandbox, out of scope here. */ export declare function gradeInDisposableWorkspace(args: { trajectory: Trajectory; graderFiles: Array<{ src: string; dest: string; }> | undefined; baseDir?: string; grade: (gradingTrajectory: Trajectory) => Promise; }): Promise; /** A freshly materialized workspace plus a best-effort cleanup function. */ export interface MaterializedWorkspace { /** Absolute path to the materialized workspace directory. */ workDir: string; /** * Absolute path to the managed assets dir holding setup inputs staged with * `dest_root: "assets"`. Present only when the environment stages assets. * Removed by {@link cleanup}. */ assetsDir?: string; /** Idempotent cleanup — removes the workspace and any git worktree. */ cleanup: () => Promise; } /** * Materialize a stimulus's starting environment into a fresh temp directory * without executing an agent. The caller owns grading and must call * {@link MaterializedWorkspace.cleanup}. Omit `environment` for an empty dir. * * Pass `targetDir` to materialize into a caller-owned directory instead of a * temp dir (e.g. `oracle --workspace `). The directory is created if * missing. For file/skill/command environments `cleanup` leaves a caller-owned * root in place (only temp dirs are removed). Note: when `environment.git` is a * worktree the workspace *is* the git worktree, so `cleanup` runs * `git worktree remove`, which deletes the directory even when caller-owned; a * clone leaves no external metadata, so `cleanup` leaves a caller-owned root in * place. On setup failure a caller-owned git workspace is preserved (not * removed) so the partial state can be inspected, matching `runEval`'s * user-owned contract. */ export declare function materializeWorkspace(environment: EnvironmentConfig | undefined, baseDir?: string, prefix?: string, targetDir?: string): Promise; /** * Prepare a workspace for multi-trial reuse via a snapshot-and-clone model. * * Runs `applyEnvironment()` once into a temp dir, captures a snapshot of * the post-setup state, then discards the setup dir. Each call to * `acquire()` materializes a fresh, independent workspace by copying the * snapshot into a new temp dir — multiple acquisitions can run concurrently * because no state is shared between them. * Environment setups whose artifacts contain absolute paths (venv shebangs, * build caches with embedded paths) may produce broken trials when the * workspace path differs between setup and execution. Such evals should opt * out of `prepareWorkspace` or restructure their setup to be path-independent. */ export declare function prepareWorkspace(env: EnvironmentConfig, baseDir?: string): Promise; export declare function hasCacheableEnvironmentSetup(env?: EnvironmentConfig): boolean; export declare function resolveEnvironmentSkillSources(env: EnvironmentConfig | undefined, baseDir?: string): string[]; //# sourceMappingURL=run.d.ts.map