import { RunRecord, AnalystFinding } from '@tangle-network/agent-eval';
import { AgentProfileResourceRef, AgentProfileMcpServer, AgentProfileHookCommand, AgentSubagentProfile, AgentProfile } from '@tangle-network/agent-interface';

/**
 * `@tangle-network/agent-runtime/lifecycle` — artifact-lifecycle FOUNDATION.
 *
 * The §1.5 law says an agent IS its `AgentProfile`, and the profile is the WHOLE
 * agent: prompt + skills + tools + mcp + hooks + subagents. This module names the
 * discrete, individually-promotable PIECES of that profile — "artifacts" — and
 * gives them stable ids so the rest of a self-improvement lifecycle (propose →
 * measure → promote → ship) has something concrete to hang off.
 *
 * This is PHASE 1: just the two primitives the rest hangs off.
 *   1. `ArtifactRegistry` — a typed catalog of profile artifacts with stable ids.
 *   2. `measureMarginalLift` — the with-vs-without ablation: how much score/cost a
 *      single artifact adds on top of a baseline profile.
 *
 * The per-surface lifecycles, the `BuildableSurface` author contract, and the
 * promotion-gate wiring are deferred to later phases.
 */

/**
 * The profile levers an artifact can target. One-to-one with the §1.5 profile
 * surface (`prompt + skills + tools + mcp + hooks + subagents`). Each kind maps to
 * exactly one field of `AgentProfile`, so an artifact can be applied onto a
 * baseline profile deterministically (see `applyArtifact`).
 */
type ArtifactKind = 'skill' | 'tool' | 'mcp' | 'hook' | 'subagent' | 'prompt';
/**
 * The payload for each `ArtifactKind`. The shapes are the SAME types the
 * `AgentProfile` field carries, so applying an artifact is a structural merge
 * onto the profile — never a bespoke per-kind transform.
 *
 *   - `prompt`   — an instruction line appended to `profile.prompt.instructions`.
 *   - `skill`    — a `SKILL.md`-style resource ref added to `profile.resources.skills`.
 *   - `tool`     — a tool grant: `{ enabled }` set under `profile.tools[name]`.
 *   - `mcp`      — one MCP server added under `profile.mcp[name]`.
 *   - `hook`     — one or more hook commands added under `profile.hooks[event]`.
 *   - `subagent` — one subagent profile added under `profile.subagents[name]`.
 */
interface ArtifactPayloads {
    prompt: {
        instruction: string;
    };
    skill: {
        resource: AgentProfileResourceRef;
    };
    tool: {
        enabled: boolean;
    };
    mcp: {
        server: AgentProfileMcpServer;
    };
    hook: {
        event: string;
        commands: AgentProfileHookCommand[];
    };
    subagent: {
        profile: AgentSubagentProfile;
    };
}
/**
 * A discrete, individually-promotable piece of an agent profile.
 *
 * `kind` selects the profile lever; `payload` is the kind-specific value; `key`
 * is the profile-field key the payload lands under (the tool name, the MCP server
 * name, the subagent name — unused for `prompt`, which appends). `id` is stable:
 * once registered, it never changes, so a marginal-lift measurement, a promotion
 * decision, and a ship record all reference the same artifact.
 */
interface ProfileArtifact<K extends ArtifactKind = ArtifactKind> {
    /** Stable id. Assigned by the registry at register time; never reassigned. */
    id: string;
    kind: K;
    /**
     * The profile-field key this artifact lands under (e.g. the tool name, the MCP
     * server name, the subagent name, the hook event). Optional for `prompt`
     * (instructions append, they have no key). Defaults to `id` when applying a
     * keyed artifact without an explicit key.
     */
    key?: string;
    /** Human-facing label for review surfaces. */
    name: string;
    /** Optional one-line description of what this artifact does. */
    description?: string;
    payload: ArtifactPayloads[K];
    /**
     * Lifecycle status — the full artifact state machine:
     *
     *   `candidate` → `active` → `decayed` (re-promotable)
     *                         ↘ `retired` (terminal)
     *
     *   - `candidate` — registered, not yet promoted. The default at register time.
     *   - `active`    — passed the promotion gate and carries a measured held-back
     *                   lift; the only status `composeProfile` folds into a profile.
     *   - `decayed`   — was active, but a later re-measure (`driftWatch`) found its
     *                   lift fell below the keep-bar. Demoted out of the composed
     *                   profile; kept as an auditable record and a re-promotion
     *                   candidate if a future re-measure recovers the lift.
     *   - `retired`   — permanently removed from the active set (`dedupeArtifacts`
     *                   retires the weaker half of a non-stacking pair). Terminal.
     *
     * The registry never auto-promotes; every transition is an explicit call
     * (`promote`/`promoteWithLift`/`demote`/`retire`).
     */
    status: ArtifactStatus;
    /** Free-form metadata (provenance, generation id, the measured lift, …). */
    metadata?: Record<string, unknown>;
}
/**
 * The artifact lifecycle states. `active` is the load-bearing one — it is the
 * sole status `composeProfile` folds into a deployable profile, and it is gated
 * by a measured held-back lift (the registry invariant). `decayed` and `retired`
 * are the two ways an artifact LEAVES the active set: a decayed artifact lost its
 * lift on re-measure (reversible — `driftWatch`), a retired one was deduped away
 * (terminal — `dedupeArtifacts`).
 */
type ArtifactStatus = 'candidate' | 'active' | 'decayed' | 'retired';
/** The input to `register` — everything on `ProfileArtifact` except the
 *  registry-owned `id` and `status`. An explicit `id` may be supplied for
 *  deterministic/idempotent registration; otherwise the registry assigns one. */
type ArtifactInput<K extends ArtifactKind = ArtifactKind> = Omit<ProfileArtifact<K>, 'id' | 'status'> & {
    id?: string;
    status?: ArtifactStatus;
};

/**
 * `measureMarginalLift` — the with-vs-without ablation for one artifact.
 *
 * "What does THIS one piece add?" is the question a lifecycle has to answer
 * before it can promote anything. The answer is an ablation: score the baseline
 * profile, score the baseline-plus-candidate profile, and report the delta. This
 * is the marginal contribution of the artifact — the same shape the project's
 * `OutcomeMeasurement` reports for an apply pass, but isolated to ONE artifact so
 * a registry can rank candidates by what they individually earn.
 *
 * The measurement is selector-agnostic: it takes an `EvalRunner` the caller
 * supplies (any function that scores a profile and reports cost), runs it twice,
 * and subtracts. It never judges, never picks a winner, never re-scores on a
 * holdout — those are the gate's job. It only quantifies the ablation so the gate
 * has a real number to decide on.
 */

/**
 * The result of running an eval over ONE profile: a composite score and the cost
 * to obtain it. This mirrors the project's score/cost convention (`composite`
 * from `OutcomeMeasurement`, `costUsd` from `LoopResult`), so a caller can pass a
 * thin wrapper over `runLoop` / `runBenchmark` / `runAgentEval` directly.
 */
interface EvalResult {
    /** Composite score in `[0, 1]` (higher is better) for the profile under test. */
    composite: number;
    /** USD cost to produce this result. */
    costUsd: number;
    /**
     * Per-task records the run produced, when the runner emits them. The marginal
     * lift only needs `composite`, but the held-out promotion gate (`HeldOutGate`)
     * pairs candidate vs baseline per-task holdout records by (experimentId, seed)
     * — so a runner feeding `heldOutPromotionGate` MUST populate this with rows
     * carrying both `search` and `holdout` split scores. Omit it for evals scored
     * to a single composite (then use `thresholdPromotionGate`).
     */
    runs?: RunRecord[];
    /** Optional opaque passthrough (per-task cells, the raw report, …). */
    details?: unknown;
}
/**
 * Scores a profile. The caller wires this to whatever eval they run — a
 * `runLoop` rollout, a `runBenchmark` campaign, a `runAgentEval` cohort — and
 * returns the composite + cost. `signal` is forwarded for cancellation.
 */
type EvalRunner = (profile: AgentProfile, signal?: AbortSignal) => Promise<EvalResult>;
interface MeasureMarginalLiftOptions {
    /** The profile the artifact is measured ON TOP OF (the "without" arm). */
    baseline: AgentProfile;
    /** The single artifact whose marginal contribution we want. */
    candidate: ProfileArtifact;
    /** The eval that scores a profile. Run once per arm (twice total). */
    evalRunner: EvalRunner;
    /**
     * A pre-computed baseline result, to skip the "without" run when the caller
     * already scored the baseline (e.g. measuring several candidates against the
     * same baseline). When set, the baseline arm is NOT re-run.
     */
    baselineResult?: EvalResult;
    /** Forwarded to both `evalRunner` invocations for cancellation. */
    signal?: AbortSignal;
}
/**
 * The marginal lift of one artifact: the with/without ablation.
 *
 * `scoreDelta = with.composite − without.composite`. A positive `scoreDelta` is
 * the evidence a gate needs to promote; a negative one is the signal to drop the
 * artifact. `costDelta` is the extra USD the artifact costs (often positive — a
 * new tool/MCP adds calls) and lets the gate weigh lift against spend.
 */
interface MarginalLift {
    /** The artifact id this measurement is for (stable, from the registry). */
    artifactId: string;
    /** Eval of `applyArtifact(baseline, candidate)`. */
    withArtifact: EvalResult;
    /** Eval of `baseline` alone. */
    withoutArtifact: EvalResult;
    /** `withArtifact.composite − withoutArtifact.composite`. */
    scoreDelta: number;
    /** `withArtifact.costUsd − withoutArtifact.costUsd`. */
    costDelta: number;
}
/**
 * Run the with/without ablation for `candidate` over `baseline` and return its
 * marginal score/cost contribution.
 *
 * The "without" arm scores the baseline profile unchanged; the "with" arm scores
 * `applyArtifact(baseline, candidate)`. Both use the same `evalRunner`, so the
 * delta isolates the artifact's effect (eval method held constant). The baseline
 * arm is skipped when `baselineResult` is supplied.
 *
 * @example
 *   const lift = await measureMarginalLift({
 *     baseline,
 *     candidate: registry.get(id)!,
 *     evalRunner: (profile) => scoreProfileOnCohort(profile),
 *   })
 *   if (lift.scoreDelta > 0) registry.promote(lift.artifactId)
 */
declare function measureMarginalLift(opts: MeasureMarginalLiftOptions): Promise<MarginalLift>;

/**
 * `PromotionGate` — the held-back exam that decides whether a measured candidate
 * artifact is promoted into the registry.
 *
 * The lifecycle measures each candidate's marginal lift (`measureMarginalLift`),
 * but a positive ablation delta on the practice problems is NOT enough to ship —
 * that is how you overfit. The gate is the SECOND, stricter test: it decides
 * promotion from a held-back split (fresh problems the candidate never tuned on)
 * with a significance bar, so a promoted artifact earned its place rather than
 * memorized the practice set.
 *
 * The interface is small and pluggable on purpose: production wires
 * `heldOutPromotionGate`, which delegates to agent-eval's paper-grade
 * `HeldOutGate` (paired-bootstrap CI on the held-out delta + an overfit-gap
 * check). A deterministic test can inject a stub gate. The orchestrator never
 * imports `HeldOutGate` directly — it only knows this interface — so the gate
 * policy is a configuration choice, not a hardcode.
 */

/** The verdict a gate returns for one candidate. */
interface PromotionVerdict {
    /** Whether to promote the candidate into the registry as `active`. */
    promote: boolean;
    /** Human-readable reason (surfaced in provenance + reports). */
    reason: string;
    /** Machine-readable rejection code, or `null` on promote. Mirrors the
     *  `HeldOutGate` rejection taxonomy when that gate is the backend. */
    rejectionCode: string | null;
}
/**
 * Decides whether ONE measured candidate is promoted. The lifecycle calls this
 * once per candidate, after `measureMarginalLift` has produced the ablation.
 *
 * `lift` carries the with/without ablation (the marginal contribution); the gate
 * MAY use it directly (the simple "positive lift on the held-back split" policy)
 * OR ignore the scalar and decide from the paired per-task records the eval
 * produced (`lift.withArtifact.runs` / `lift.withoutArtifact.runs`) when a
 * significance gate like `HeldOutGate` is the backend.
 */
interface PromotionGate {
    /** Stable label for the gate policy (provenance). */
    kind: string;
    decide(lift: MarginalLift): PromotionVerdict;
}
/**
 * The simplest honest gate: promote iff the candidate's marginal lift on the
 * held-back split clears `minDelta` (default `> 0`). It reads only the scalar
 * `scoreDelta`, so it works with any `EvalRunner` (no per-task records needed).
 *
 * Use this when the eval is already scored ON the held-back split and you want a
 * threshold, not a significance test — e.g. a deterministic fixture domain, or a
 * cheap first pass before a paired-bootstrap gate. For paper-grade promotion on
 * noisy live evals, prefer `heldOutPromotionGate`.
 */
declare function thresholdPromotionGate(minDelta?: number): PromotionGate;
interface HeldOutPromotionGateOptions {
    /** Stable label of the baseline candidate the held-out records pair against. */
    baselineKey: string;
    /** Minimum paired (candidate, baseline) holdout observations. Default 3. */
    minProductiveRuns?: number;
    /** Lower-bound on the bootstrap-CI of the median paired holdout delta. Default 0. */
    pairedDeltaThreshold?: number;
    /** Max allowed worsening of the (search − holdout) overfit gap. Default 0.15. */
    overfitGapThreshold?: number;
    /** Deterministic bootstrap seed (reproducible CIs). Default unseeded. */
    seed?: number;
    /** Hard ceiling on the candidate's median per-task USD cost. Default none. */
    costPerTaskCeiling?: number;
}
/**
 * The paper-grade promotion gate: delegate to agent-eval's `HeldOutGate`, which
 * pairs the candidate and baseline per-task holdout records by (experimentId,
 * seed), runs a paired-bootstrap CI on the median delta, and checks the
 * overfit gap. Promotes only when the held-out generalization is real, not luck.
 *
 * REQUIRES the eval to surface per-task records: `lift.withArtifact.runs` (the
 * candidate arm) and `lift.withoutArtifact.runs` (the baseline arm), each
 * carrying matched seeds with both `search` and `holdout` split scores. When the
 * records are absent, this gate FAILS LOUD — a held-out significance claim with
 * no per-task data behind it would be a fabricated number, which the
 * no-silent-fallback doctrine forbids. Use `thresholdPromotionGate` for evals
 * that only produce a scalar composite.
 */
declare function heldOutPromotionGate(opts: HeldOutPromotionGateOptions): PromotionGate;

/**
 * `CandidateGenerator` — the ONE per-surface seam of the artifact lifecycle.
 *
 * Everything else in the lifecycle (measure → gate → store → compose) is
 * surface-agnostic: it operates on `ProfileArtifact`s regardless of whether they
 * are skills, tools, prompts, or MCP servers. The ONLY thing that varies by
 * surface is HOW a fresh candidate piece is produced from the agent's history —
 * a skill is DISTILLED from traces then refined; a tool grant is proposed from a
 * "the agent lacked an action" finding; a prompt line is drafted from a recurring
 * mistake. That per-surface logic is isolated here, behind one interface, so the
 * orchestrator never grows a `switch (surface)`.
 *
 * A generator is pure-ish: given the lifecycle context (the baseline profile, the
 * captured traces/findings, the registry of what already exists), it returns zero
 * or more `ArtifactInput`s — candidate pieces that have NOT been measured yet.
 * The orchestrator owns registration, measurement, gating, and storage; the
 * generator owns only "what new pieces could this surface contribute".
 *
 * This is the interface the per-surface stages (skills, tools, prompt, mcp)
 * implement. The skill generator (`skillGenerator`) is the reference
 * implementation and the literal answer to "an empty profile has no skills":
 * its `distill` step CREATES a skill from traces (the step `skillOpt` cannot do),
 * then `refine` optimizes it.
 */

/**
 * The read-only context a generator sees when proposing candidates. It is the
 * agent's HISTORY (what to learn from) plus the agent's CURRENT shape (what
 * already exists, so the generator does not re-propose a duplicate).
 */
interface GenerateContext {
    /** The baseline profile candidates are proposed on top of. A generator reads
     *  it to avoid re-proposing something the profile already has. */
    baseline: AgentProfile;
    /** The domain/agent id the lifecycle is running for — namespaces provenance
     *  and lets a generator scope its proposals (e.g. distill from this domain's
     *  traces only). */
    domain: string;
    /** Trace-analyst findings to ground proposals in observed behavior. The
     *  firewall holds: these are OBSERVED signals, never judge verdicts. */
    findings: ReadonlyArray<AnalystFinding>;
    /** Raw captured trace text/records to distill from, opaque to the
     *  orchestrator. A skill generator's `distill` reads this; a tool generator
     *  may ignore it and read `findings` instead. */
    traces?: unknown;
    /** Cooperative cancellation, forwarded from `runLifecycle`. */
    signal?: AbortSignal;
}
/**
 * Produces fresh, UNMEASURED candidate artifacts for ONE profile surface.
 *
 * `kind` is the `ArtifactKind` this generator targets (so the orchestrator can
 * report and group by surface). `generate` returns candidate inputs the
 * orchestrator will register, measure (`measureMarginalLift`), gate
 * (`HeldOutGate`), and — on a pass — promote into the registry. Returning `[]`
 * is valid: the surface had nothing to contribute this round.
 */
interface CandidateGenerator<K extends ArtifactKind = ArtifactKind> {
    /** The profile surface this generator targets. */
    kind: K;
    /**
     * Propose candidate artifacts from the lifecycle context. MUST NOT measure,
     * gate, or register — that is the orchestrator's job. MUST NOT mutate
     * `ctx.baseline`. Returns unmeasured `ArtifactInput`s (no lift score yet); the
     * orchestrator stamps provenance + the measured lift before promotion.
     */
    generate(ctx: GenerateContext): Promise<ArtifactInput<K>[]>;
}

export { type ArtifactKind as A, type CandidateGenerator as C, type EvalRunner as E, type GenerateContext as G, type HeldOutPromotionGateOptions as H, type MarginalLift as M, type PromotionGate as P, type ProfileArtifact as a, type ArtifactStatus as b, type ArtifactInput as c, type EvalResult as d, type PromotionVerdict as e, type ArtifactPayloads as f, type MeasureMarginalLiftOptions as g, heldOutPromotionGate as h, measureMarginalLift as m, thresholdPromotionGate as t };
