/**
 * economy/outcome-tracker — Per-cycle cost accounting, in-memory store.
 *
 * Closes the "agent_run.cost_usd always zero" gap (Sprint 0 v1).
 *
 * Design principles:
 *   • No DB dependency — substrate is product-agnostic.
 *   • Host wires DB persistence via OutcomeHook.
 *   • Cost computed from TierPolicy pricing; unknown models default to 0.
 */
import { lookupTier } from "./tier-policy.js";
// ─── OutcomeTracker ───────────────────────────────────────────────────────
/**
 * OutcomeTracker records token usage per cycle and computes cost_usd
 * from the injected TierPolicy.
 *
 * Usage:
 *   const tracker = new OutcomeTracker(policy, hook);
 *   const cost = tracker.record(runId, "litellm/default-fast", 1200, 300, 1800);
 *   await tracker.flush(runId);
 */
export class OutcomeTracker {
    _policy;
    _hook;
    /** In-memory store: runId → list of CycleCost records. */
    _store = new Map();
    constructor(policy, hook) {
        this._policy = policy;
        this._hook = hook;
    }
    /**
     * Record tokens used by one cycle.
     *
     * @param runId        - Unique run identifier.
     * @param model        - "provider/model" string, e.g. "litellm/default-fast" or "groq/llama-3.3-70b-versatile".
     * @param inputTokens  - Number of input tokens consumed.
     * @param outputTokens - Number of output tokens produced.
     * @param durationMs   - Wall-clock duration of the cycle.
     * @param opts         - Optional: skillId for Sprint C skill attribution.
     */
    record(runId, model, inputTokens, outputTokens, durationMs, opts) {
        const { provider, modelName } = parseModel(model);
        const tier = lookupTier(provider, modelName);
        const costUsd = tier !== undefined
            ? (inputTokens / 1_000_000) * tier.costPerMTokenIn +
                (outputTokens / 1_000_000) * tier.costPerMTokenOut
            : 0;
        // Also try via policy catalog for consumer-injected tiers.
        // We use the "standard" category degraded mode to get a list, then check
        // if any matches — this is a fallback for consumer tiers not in built-in catalog.
        let tierLabel = tier?.label ?? "unknown";
        if (tier === undefined) {
            const candidates = this._policy.tiersFor("standard", "degraded");
            const match = candidates.find((t) => t.provider === provider && t.model === modelName);
            if (match !== undefined) {
                tierLabel = match.label;
                const matchedCost = (inputTokens / 1_000_000) * match.costPerMTokenIn +
                    (outputTokens / 1_000_000) * match.costPerMTokenOut;
                const cycleCost = {
                    runId,
                    inputTokens,
                    outputTokens,
                    costUsd: matchedCost,
                    modelTier: tierLabel,
                    durationMs,
                    ...(opts?.skillId !== undefined ? { skillId: opts.skillId } : {}),
                    ...(opts?.outcomeValue !== undefined
                        ? { outcomeValue: opts.outcomeValue }
                        : {}),
                    ...(opts?.category !== undefined ? { category: opts.category } : {}),
                };
                this._push(runId, cycleCost);
                return cycleCost;
            }
        }
        const cycleCost = {
            runId,
            inputTokens,
            outputTokens,
            costUsd,
            modelTier: tierLabel,
            durationMs,
            ...(opts?.skillId !== undefined ? { skillId: opts.skillId } : {}),
            ...(opts?.outcomeValue !== undefined
                ? { outcomeValue: opts.outcomeValue }
                : {}),
            ...(opts?.category !== undefined ? { category: opts.category } : {}),
        };
        this._push(runId, cycleCost);
        return cycleCost;
    }
    /**
     * Flush all recorded cycles for a runId to the consumer hook.
     * Noop if no hook is configured or no records exist for the runId.
     */
    async flush(runId) {
        if (this._hook === undefined)
            return;
        const records = this._store.get(runId);
        if (records === undefined || records.length === 0)
            return;
        for (const cost of records) {
            await this._hook.onCycleEnd(runId, cost);
        }
    }
    /**
     * Get all recorded costs, optionally filtered to a trailing time window.
     *
     * @param windowMs - If provided, only return records within the last windowMs milliseconds.
     *                   The tracker uses the current time as the reference point and
     *                   estimates record time by insertion order (no timestamp stored per record).
     *                   For window filtering, use totalCostInWindow() which is time-aware.
     */
    getCosts(windowMs) {
        const all = [];
        for (const records of this._store.values()) {
            all.push(...records);
        }
        if (windowMs === undefined)
            return all;
        // Without per-record timestamps, return all (host should use totalCostInWindow).
        return all;
    }
    /**
     * Running total cost (USD) across all recorded cycles within the time window.
     * Default window: last 1 hour (3_600_000 ms).
     *
     * Uses an internal timestamped wrapper to implement accurate windowing.
     */
    totalCostInWindow(windowMs = 3_600_000) {
        const cutoff = Date.now() - windowMs;
        let total = 0;
        for (const records of this._timestamped.values()) {
            for (const r of records) {
                if (r.ts >= cutoff) {
                    total += r.cost.costUsd;
                }
            }
        }
        return total;
    }
    // ─── Private helpers ───────────────────────────────────────────────────
    /** Timestamped record store for window queries. */
    _timestamped = new Map();
    _push(runId, cost) {
        // Plain store
        const list = this._store.get(runId) ?? [];
        list.push(cost);
        this._store.set(runId, list);
        // Timestamped store
        const tsList = this._timestamped.get(runId) ?? [];
        tsList.push({ ts: Date.now(), cost });
        this._timestamped.set(runId, tsList);
    }
}
// ─── Internal helpers ─────────────────────────────────────────────────────
/**
 * Parse "provider/model" string.
 * If no "/" is present, treats the whole string as the model name with an
 * empty provider (will not match any built-in tier).
 */
function parseModel(model) {
    const idx = model.indexOf("/");
    if (idx === -1) {
        return { provider: "", modelName: model };
    }
    return {
        provider: model.slice(0, idx),
        modelName: model.slice(idx + 1),
    };
}
//# sourceMappingURL=outcome-tracker.js.map