import type { Headroom } from '../devices/health.js';
/**
 * Live signal for one pool device, gathered by the caller (SSH probes) and
 * passed into the pure pick. Every field is optional/best-effort: a device with
 * no signal is neither excluded nor preferred — it degrades to the roster-count
 * path. `reachable === false` and `installed === false` are the only two
 * exclusion signals here; `headroom` drives the overload exclusion + the load
 * tier of the rank. See {@link DeviceStats}/{@link headroom} in devices/health.
 */
export interface DevicePlacementSignal {
    /** SSH probe answered. `false` → excluded (unreachable). undefined → unknown. */
    reachable?: boolean;
    /**
     * The probe was killed for exceeding its budget rather than failing to
     * connect. Ranking treats it exactly like `reachable:false` — an
     * unresponsive box is still not placeable — but the operator-facing
     * exclusion reason says "probe timed out", not "unreachable" (PHNX-3682).
     */
    timedOut?: boolean;
    /** Headroom bucket from load+memory. `'loaded'` → excluded (overloaded). */
    headroom?: Headroom;
    /** Normalized CPU load percent (finer rank tiebreak within a headroom tier). */
    loadPercent?: number;
    /** Memory pressure percent (used with loadPercent for the load cost). */
    memPercent?: number;
    /** Requested agent installed on the device. `false` → excluded (can't run). */
    installed?: boolean;
    /** Requested agent has at least one launch-ready account. Ranks a ready device first. */
    signedIn?: boolean;
    /** Requested agent has at least one row the interactive account picker can launch. */
    pickerEligible?: boolean;
}
/** Team fields the placement cascade reads (a subset of TeamMeta). */
export interface PlacementTeam {
    devices?: string[];
}
/**
 * Optional placement inputs beyond the team + roster. `maxConcurrent` maps a
 * device name to its `agents.max-concurrent` cap (from the device doc — read
 * locally via `readMaxConcurrentCaps`, never probed over SSH). Teams counts
 * the team's OWN roster against the cap (device-global counting would need an
 * SSH probe per candidate — out of the hot path; AGI EXT auto-launch is the
 * device-wide counter). Only the least-loaded AUTO-PICK (cascade step 3)
 * honors caps: an explicit pin or a pool of one is the user's own choice and
 * is never second-guessed.
 */
export interface PlacementOptions {
    /** Worker pool used when the team does not declare an explicit device pool. */
    defaultDevices?: string[];
    maxConcurrent?: Record<string, number>;
    /**
     * Live per-device signals (RUSH-2002). When present, the many-device pick
     * filters unreachable/overloaded/not-installed devices and ranks the rest by
     * health + harness + load. Absent → the pick stays a pure roster count.
     */
    signals?: Map<string, DevicePlacementSignal>;
    /** Human label of the requested agent (e.g. `claude@2.1.112`) for the
     * fail-loud message. */
    agentLabel?: string;
    /**
     * Normalized hosts boosted with `auto-launch.preferred` (set by
     * `agents devices prefer <name>`). A preferred device ranks ahead of a
     * non-preferred one among the eligible survivors — after the signed-in tier
     * (a preferred box that can't run the agent is still no use) and before load,
     * so an operator boost overrides load-based ordering without overriding hard
     * health. Empty/undefined leaves the ranking unchanged. See
     * {@link autoLaunchPreferredSet}.
     */
    preferred?: ReadonlySet<string>;
}
/** Why a device was excluded from the viable set, for the fail-loud message. */
export type ExclusionReason = 'unreachable' | 'probe-timed-out' | 'overloaded' | 'capped' | 'not-installed';
/** A pool device dropped from the auto-pick, with the reason + live detail. */
export interface ExcludedDevice {
    device: string;
    reason: ExclusionReason;
    /** Present for `capped`: `running/cap`. */
    detail?: string;
}
/**
 * No device in the pool can host the teammate. Carries the per-device reasons so
 * the caller can print exactly why the pool is unusable instead of silently
 * degrading to a local run the user did not ask for.
 */
export declare class NoViableDeviceError extends Error {
    readonly excluded: ExcludedDevice[];
    readonly agentLabel?: string;
    constructor(excluded: ExcludedDevice[], agentLabel?: string);
}
/**
 * Whether a failed placement can become viable without changing the team.
 * Capacity and load are time-varying; a pool with at least one such exclusion
 * must remain schedulable so a later supervisor wave can retry it. A pool that
 * is only missing the requested harness is a durable configuration failure.
 */
export declare function isTransientPlacementBlock(error: unknown): error is NoViableDeviceError;
/** Build the user-facing fail-loud message from the per-device reasons. */
export declare function formatNoViableMessage(excluded: ExcludedDevice[], agentLabel?: string): string;
/**
 * A roster entry the load counter reads — the shape any teammate satisfies
 * (AgentProcess included). `status` is compared against `'running'` (the
 * AgentStatus.RUNNING value) without importing the enum, keeping this leaf pure.
 */
export interface RosterEntry {
    hostName: string | null;
    status: string;
}
/**
 * Pool devices excluded from auto-pick because they are at (or over) their
 * `agents.max-concurrent` cap. Returned with the live counts so the caller can
 * state the reason to the user instead of the device silently never winning.
 */
export declare function cappedDevices(devices: string[], roster: RosterEntry[], maxConcurrent: Record<string, number>): Array<{
    device: string;
    running: number;
    cap: number;
}>;
/**
 * Pick the least-loaded device from the pool — the one with the fewest RUNNING
 * teammates currently assigned to it. Ties break by pool order (first wins), so
 * an empty pool fills round-robin-ish as teammates launch. Pure: counts the
 * roster, no I/O.
 *
 * With `maxConcurrent`, devices at their cap are excluded; if EVERY device is
 * capped this throws naming each cap and the fix — a loud failure beats a
 * teammate silently landing on a machine its operator capped.
 */
export declare function pickLeastLoaded(devices: string[], roster: RosterEntry[], maxConcurrent?: Record<string, number>): string;
/**
 * Split the pool into the viable set and the excluded set (with reasons), from
 * the caps + live signals. Pure — the caller decides what to do with an empty
 * viable set. Reasons are checked in a stable order (unreachable → not-installed
 * → overloaded → capped) so a device reports its most fundamental blocker first.
 */
export declare function classifyExclusions(devices: string[], roster: RosterEntry[], opts?: PlacementOptions): {
    eligible: string[];
    excluded: ExcludedDevice[];
};
/**
 * Health-, harness-, and load-aware pick over a pool (RUSH-2002). Filters
 * unreachable / overloaded / capped / not-installed devices, then ranks the
 * survivors by: (a) agent signed in, (b) lower load (headroom tier, then raw
 * load cost), (c) fewer running teammates, ties broken by pool order. Throws
 * {@link NoViableDeviceError} when nothing survives — never returns a local
 * fallback. Pure: all I/O is pre-resolved into `opts.signals`.
 */
export declare function pickBestDevice(devices: string[], roster: RosterEntry[], opts?: PlacementOptions): string;
/**
 * Resolve where a teammate runs. Returns `{ device: null }` only when the
 * explicitly/default-selected device is the local machine, and
 * `{ device: <name> }` for a remote placement. See the cascade in the module
 * header. Throws {@link NoViableDeviceError} when a required pool has no viable
 * host — the caller surfaces it at `teams start` rather than falling back local.
 */
export declare function resolvePlacement(team: PlacementTeam, explicitDevice: string | null, roster: RosterEntry[], opts?: PlacementOptions): {
    device: string | null;
};
