/**
 * OODAAgentImpl — sequential while+sleep cycle loop with phase persistence,
 * guards, HITL gate, resource limits, and replay-safe context propagation.
 *
 * Implements all 10 anti-patterns enforced by-design (see `./types.ts`).
 *
 * @public
 */
import { randomUUID } from "node:crypto";
import { SpanStatusCode } from "@opentelemetry/api";
import { DEFAULT_RESOURCE_LIMITS } from "./types.js";
import { EMPTY_SKILL_REGISTRY } from "./skills.js";
import { MissingDryRunFlagError } from "../../errors.js";
import { agentSpan, getTracer } from "../../tracking/gen-ai.js";
/**
 * Noop logger fallback when host does not inject one.
 * Prevents TypeError: Cannot read properties of undefined (0.8.1 fix).
 */
const NOOP_LOGGER = {
    info: () => undefined,
    warn: () => undefined,
    error: () => undefined,
    debug: () => undefined,
};
/** Internal sleep helper — kept private so tests grep cannot find a global setInterval. */
function sleep(ms) {
    return new Promise((resolve) => {
        if (ms <= 0) {
            resolve();
            return;
        }
        setTimeout(resolve, ms).unref?.();
    });
}
/**
 * Default in-memory step persister. Hosts MUST inject `insertStepImpl` /
 * `completeStepImpl` / `errorStepImpl` when persisting to Postgres.
 *
 * The default is intentionally trivial: it generates a UUID stepId and
 * a deterministic-but-fake leaf hash. This keeps the SDK runnable in
 * tests and dry-run hosts without requiring a DB.
 */
function defaultInsertStep() {
    return async () => ({ stepId: randomUUID() });
}
function defaultCompleteStep() {
    return async (stepId) => ({ leafHash: `0x${stepId.replace(/-/g, "")}` });
}
function defaultErrorStep() {
    return async () => { };
}
function defaultNotifySlack() {
    return async () => { };
}
/**
 * Build the fire-and-forget `ctx.emitStep` helper (SDK 1.6.1).
 *
 * Wraps `insertStep → completeStep | errorStep` in an async IIFE that swallows
 * any rejection via `logger.warn`. Caller-side is synchronous and never throws.
 */
function makeEmitStep(deps) {
    const { insertStep, completeStep, errorStep, logger } = deps;
    return (input) => {
        const status = input.status ?? "completed";
        const payload = input.payload ?? {};
        void (async () => {
            const { stepId } = await insertStep({
                type: input.type,
                phase: input.phase,
                payload,
            });
            if (status === "failed") {
                const err = input.error instanceof Error
                    ? input.error
                    : new Error(typeof input.error === "string"
                        ? input.error
                        : `${input.phase} failed`);
                await errorStep(stepId, err);
            }
            else {
                await completeStep(stepId, { ...payload, status });
            }
        })().catch((err) => logger.warn({
            phase: input.phase,
            status,
            err: err?.message ?? String(err),
        }, "[emitStep] failed"));
    };
}
// ─── Telemetry metadata sanitization (SDK 1.5.0 — sprint-B) ─────────────────
//
// `insertStep` callers pass arbitrary `input.payload` objects which we want to
// surface on `telemetry.step` events so the dashboard can render the real phase
// content (LLM prompt, tool output, decision body, etc.). The payload may
// contain secrets : we apply two transforms before emission.
//
//   1. PII redaction : recursively replace string values whose key matches
//      /secret|token|password|api[_-]?key/i with `"[REDACTED]"`. Bypassed when
//      `process.env.TELEMETRY_INCLUDE_PAYLOADS === "true"` (escape hatch for
//      sovereign self-hosters auditing their own runs).
//
//   2. Size cap : JSON-serialise and truncate at TELEMETRY_METADATA_MAX_BYTES
//      (4 KB). When truncated, the metadata object is replaced by a marker
//      `{ truncated: true, originalBytes }` so dashboards can flag the
//      omission rather than silently render half a payload.
//
// Side-effect free : caller's `input.payload` is never mutated.
const TELEMETRY_METADATA_MAX_BYTES = 4096;
const TELEMETRY_SENSITIVE_KEY_RE = /secret|token|password|api[_-]?key/i;
/**
 * Recursive redaction. Returns a structurally-cloned, sanitized copy.
 *
 * `seen` tracks references to detect cycles ; on a cycle we throw, which the
 * caller (`buildTelemetryMetadata`) converts to the `metadata_serialization_failed`
 * marker. We also cap depth at 32 to bound CPU on pathological nesting.
 */
const TELEMETRY_REDACT_MAX_DEPTH = 32;
function redactSensitiveKeys(value, seen = new WeakSet(), depth = 0) {
    if (value === null || typeof value !== "object")
        return value;
    if (depth > TELEMETRY_REDACT_MAX_DEPTH) {
        throw new Error("metadata depth exceeded");
    }
    if (seen.has(value)) {
        throw new Error("metadata cycle detected");
    }
    seen.add(value);
    if (Array.isArray(value)) {
        return value.map((v) => redactSensitiveKeys(v, seen, depth + 1));
    }
    const out = {};
    for (const [k, v] of Object.entries(value)) {
        if (TELEMETRY_SENSITIVE_KEY_RE.test(k)) {
            out[k] = "[REDACTED]";
        }
        else {
            out[k] = redactSensitiveKeys(v, seen, depth + 1);
        }
    }
    return out;
}
/**
 * Prepare an `insertStep` payload for emission on `telemetry.step.metadata`.
 *
 * Returns `undefined` when the payload is empty or absent — telemetry.step
 * keeps backward compat (metadata field omitted from the wire event).
 */
export function buildTelemetryMetadata(payload) {
    if (!payload || Object.keys(payload).length === 0)
        return undefined;
    const includeRaw = process.env.TELEMETRY_INCLUDE_PAYLOADS === "true";
    let sanitized;
    try {
        sanitized = includeRaw
            ? payload
            : redactSensitiveKeys(payload);
    }
    catch {
        // Circular structure crashes the recursive redactor — fail closed.
        return { error: "metadata_serialization_failed" };
    }
    let json;
    try {
        json = JSON.stringify(sanitized);
    }
    catch {
        return { error: "metadata_serialization_failed" };
    }
    if (Buffer.byteLength(json, "utf8") > TELEMETRY_METADATA_MAX_BYTES) {
        return {
            truncated: true,
            originalBytes: Buffer.byteLength(json, "utf8"),
        };
    }
    return sanitized;
}
/**
 * Race a promise against an `AbortSignal.timeout`-driven rejection.
 * Anti-pattern #9 enforcement.
 */
function withPhaseTimeout(p, timeoutMs) {
    return new Promise((resolve, reject) => {
        const t = setTimeout(() => {
            reject(new Error(`OODA phase timeout exceeded (${timeoutMs}ms)`));
        }, timeoutMs);
        t.unref?.();
        p.then((v) => {
            clearTimeout(t);
            resolve(v);
        }, (e) => {
            clearTimeout(t);
            reject(e);
        });
    });
}
/**
 * Validate phase definitions at construction time.
 * - observe + orient must be readOnly (anti-pattern #7)
 * - executionMode must be a known value (anti-pattern #6)
 */
function validateConfig(config) {
    if (config.executionMode !== "dry-run" && config.executionMode !== "live") {
        throw new Error(`OODAAgent: executionMode is REQUIRED ('dry-run' | 'live') — got ${JSON.stringify(config.executionMode)}`);
    }
    if (config.phases.observe.readOnly !== true) {
        throw new Error("OODAAgent: phases.observe MUST be readOnly:true (anti-pattern #7)");
    }
    if (config.phases.orient.readOnly !== true) {
        throw new Error("OODAAgent: phases.orient MUST be readOnly:true (anti-pattern #7)");
    }
    if (!Number.isFinite(config.intervalMs) || config.intervalMs < 0) {
        throw new Error(`OODAAgent: intervalMs must be >= 0, got ${config.intervalMs}`);
    }
}
/**
 * OODAAgentImpl — concrete implementation. Prefer `createOODAAgent` factory.
 */
export class OODAAgentImpl {
    _config;
    _limits;
    _running = false;
    _cyclesCompleted = 0;
    _lastCycleAt;
    _activeCycle;
    constructor(config) {
        validateConfig(config);
        this._config = config;
        this._limits = {
            phaseTimeoutMs: config.resourceLimits?.phaseTimeoutMs ??
                DEFAULT_RESOURCE_LIMITS.phaseTimeoutMs,
            maxStepsPerCycle: config.resourceLimits?.maxStepsPerCycle ??
                DEFAULT_RESOURCE_LIMITS.maxStepsPerCycle,
            maxHeapMb: config.resourceLimits?.maxHeapMb ?? DEFAULT_RESOURCE_LIMITS.maxHeapMb,
        };
    }
    async start() {
        if (this._running)
            return;
        this._running = true;
        // Anti-pattern #1 — sequential while + sleep. NEVER setInterval.
        while (this._running) {
            this._activeCycle = this.runCycle();
            await this._activeCycle;
            this._activeCycle = undefined;
            if (!this._running)
                break;
            await sleep(this._config.intervalMs);
        }
    }
    async stop() {
        this._running = false;
        if (this._activeCycle) {
            try {
                await this._activeCycle;
            }
            catch {
                // Cycle errors already surfaced via runCycle's logging.
            }
        }
    }
    async triggerCycle(opts) {
        if (opts?.dryRun === undefined) {
            throw new MissingDryRunFlagError();
        }
        const overrideMode = opts.dryRun === true ? "dry-run" : undefined;
        return this.runCycle(overrideMode, opts.initialContext);
    }
    async *streamCycle(opts) {
        const runId = randomUUID();
        const cycleIndex = this._cyclesCompleted;
        const executionMode = opts.dryRun
            ? "dry-run"
            : this._config.executionMode;
        // ── Event channel ─────────────────────────────────────────────────────
        // We need to bridge an async-function-based execution (runCycleWithSink)
        // with an async generator consumer. We do this via a push queue + pull
        // mechanism: the sink pushes events into the queue; the generator drains
        // the queue as events arrive.
        const queue = [];
        let done = false;
        let resolveNext;
        function push(evt) {
            queue.push(evt);
            if (resolveNext) {
                const r = resolveNext;
                resolveNext = undefined;
                r();
            }
        }
        // Run the cycle body in the background; it calls push() for every event.
        const cyclePromise = this._runCycleWithSink(runId, cycleIndex, executionMode, push, opts.initialContext).finally(() => {
            done = true;
            if (resolveNext) {
                const r = resolveNext;
                resolveNext = undefined;
                r();
            }
        });
        // Drain the queue as events arrive.
        while (!done || queue.length > 0) {
            if (queue.length > 0) {
                yield queue.shift();
            }
            else {
                // Wait until the next push() or until done is set.
                await new Promise((res) => {
                    resolveNext = res;
                });
            }
        }
        // Await the cycle promise to surface any unhandled rejection.
        await cyclePromise;
    }
    getStatus() {
        return {
            running: this._running,
            lastCycleAt: this._lastCycleAt,
            cyclesCompleted: this._cyclesCompleted,
        };
    }
    // ─── internal ────────────────────────────────────────────────────────────
    async runCycle(overrideMode, initialContext) {
        const runId = randomUUID();
        const cycleIndex = this._cyclesCompleted;
        const executionMode = overrideMode ?? this._config.executionMode;
        const isReplay = false;
        const logger = this._config.logger ?? NOOP_LOGGER;
        const startedAt = new Date().toISOString();
        // ── Telemetry (ADR-ECO-039) ─────────────────────────────────────────────
        // Failures isolated by sink/bus — never block agent. Fire-and-forget on
        // exit paths so cycle latency is unaffected.
        const telemetry = this._config.telemetry;
        const telemetryStartFired = telemetry
            ? telemetry
                .start({
                runId,
                agentId: this._config.agentId,
                agentVersion: this._config.agentVersion ?? "0.0.0",
                model: "unknown",
                provider: "unknown",
                startedAt,
            })
                .catch((err) => {
                logger.warn({ err: err?.message ?? String(err) }, "telemetry.start.failed");
            })
            : Promise.resolve();
        // Accumulators populated by insertStep payloads — sent in finish event.
        const telemetryTotals = {
            inputTokens: 0,
            outputTokens: 0,
            toolCalls: 0,
            costUsd: 0,
        };
        // Captures the OutcomeRecord returned by this._config.outcomeMapping so
        // finishTelemetry can propagate it on the wire event. Set late (after
        // phase chain). Sink-agnostic — the SDK never persists this itself.
        let capturedOutcome;
        const finishTelemetry = (status, stopReason, errorMessage) => {
            if (!telemetry)
                return;
            void telemetryStartFired.then(() => telemetry
                .finish(runId, {
                status,
                stopReason,
                errorMessage,
                finishedAt: new Date().toISOString(),
                totalInputTokens: telemetryTotals.inputTokens,
                totalOutputTokens: telemetryTotals.outputTokens,
                totalCostUsd: telemetryTotals.costUsd,
                totalToolCalls: telemetryTotals.toolCalls,
                ...(capturedOutcome ? { outcome: capturedOutcome } : {}),
            })
                .catch((err) => logger.warn({ err: err?.message ?? String(err) }, "telemetry.finish.failed")));
        };
        // ── OTel: agent-level span for the full cycle ───────────────────────────
        const tracer = getTracer("vauban-agent-sdk.ooda");
        const rootSpan = agentSpan(tracer, {
            agentId: this._config.agentId,
            agentVersion: this._config.agentVersion ?? "0.0.0",
            runId,
        });
        rootSpan.setAttribute("vauban.ooda.cycle_index", cycleIndex);
        rootSpan.setAttribute("vauban.ooda.execution_mode", executionMode);
        let stepsThisCycle = 0;
        const limits = this._limits;
        // SDK 1.6.3 — track per-step input until completion. The telemetry.step
        // event is emitted at completeStep / errorStep (not at insertStep) so
        // metadata can carry BOTH the phase input AND the phase output in a
        // single row. Map keyed by the DB stepId (returned by baseInsertStep).
        // Bound by `maxStepsPerCycle` so this never grows unboundedly.
        const inFlightSteps = new Map();
        const emitStepEvent = (tracked, status, completionPayload) => {
            if (!telemetry)
                return;
            const merged = {
                ...tracked.inputPayload,
                ...completionPayload,
            };
            const inT = Number(merged.inputTokens ?? 0);
            const outT = Number(merged.outputTokens ?? 0);
            const cost = Number(merged.costUsd ?? 0);
            const tools = Number(merged.toolCalls ?? 0);
            telemetryTotals.inputTokens += inT;
            telemetryTotals.outputTokens += outT;
            telemetryTotals.costUsd += cost;
            telemetryTotals.toolCalls += tools;
            const durationMs = Math.max(0, Date.now() - tracked.startedAt);
            const metadata = buildTelemetryMetadata(merged);
            void telemetryStartFired.then(() => telemetry
                .step(runId, {
                stepIndex: tracked.stepIndex,
                kind: tracked.kind,
                status,
                inputTokens: inT,
                outputTokens: outT,
                toolCalls: tools,
                costUsd: cost,
                durationMs,
                ...(metadata !== undefined ? { metadata } : {}),
            })
                .catch((err) => logger.warn({ err: err?.message ?? String(err) }, "telemetry.step.failed")));
        };
        const baseInsertStep = this._config.insertStepImpl ?? defaultInsertStep();
        const insertStep = async (input) => {
            stepsThisCycle += 1;
            if (stepsThisCycle > limits.maxStepsPerCycle) {
                throw new Error(`OODA: maxStepsPerCycle exceeded (${limits.maxStepsPerCycle})`);
            }
            const stepIndex = stepsThisCycle - 1;
            const result = await baseInsertStep(input);
            if (telemetry) {
                inFlightSteps.set(result.stepId, {
                    stepIndex,
                    kind: input.phase ?? input.type ?? "step",
                    inputPayload: (input.payload ?? {}),
                    startedAt: Date.now(),
                });
            }
            return result;
        };
        const baseCompleteStep = this._config.completeStepImpl ?? defaultCompleteStep();
        const completeStep = async (stepId, payload) => {
            const tracked = inFlightSteps.get(stepId);
            if (tracked) {
                emitStepEvent(tracked, "completed", payload);
                inFlightSteps.delete(stepId);
            }
            return baseCompleteStep(stepId, payload);
        };
        const baseErrorStep = this._config.errorStepImpl ?? defaultErrorStep();
        const errorStep = async (stepId, err) => {
            const tracked = inFlightSteps.get(stepId);
            if (tracked) {
                emitStepEvent(tracked, "failed", {
                    errorMessage: err?.message ?? String(err),
                    errorName: err?.name,
                });
                inFlightSteps.delete(stepId);
            }
            return baseErrorStep(stepId, err);
        };
        let liveConfig = (this._config.config ??
            {});
        if (this._config.configLoader) {
            try {
                liveConfig = await this._config.configLoader.get(this._config.agentId);
            }
            catch (e) {
                logger.warn({ agentId: this._config.agentId, err: e.message }, "ooda.cycle.config_loader_failed — using static config");
            }
        }
        // completeStep + errorStep are the SDK-1.6.3-wrapped versions defined
        // above — they emit telemetry.step on completion with merged metadata.
        const ctx = Object.freeze({
            agentId: this._config.agentId,
            runId,
            cycleIndex,
            executionMode,
            isReplay,
            config: liveConfig,
            configLoader: this._config.configLoader,
            db: this._config.db,
            skills: this._config.skills ?? EMPTY_SKILL_REGISTRY,
            logger,
            deps: this._config.deps ?? {},
            insertStep,
            completeStep,
            errorStep,
            notifySlack: this._config.notifySlackImpl ?? defaultNotifySlack(),
            emitStep: makeEmitStep({ insertStep, completeStep, errorStep, logger }),
        });
        // ── Session guards ──────────────────────────────────────────────────────
        const sessionResult = await this._checkSessionGuards(this._config.sessionGuards ?? []);
        if (!sessionResult.ok) {
            logger.info({ runId, agentId: this._config.agentId, reason: sessionResult.reason }, "ooda.cycle.skipped.session_guard");
            rootSpan.setAttribute("vauban.ooda.status", "skipped");
            rootSpan.setAttribute("vauban.ooda.skip_reason", sessionResult.reason ?? "session_guard");
            rootSpan.setStatus({ code: SpanStatusCode.OK });
            rootSpan.end();
            this._cyclesCompleted += 1;
            this._lastCycleAt = new Date().toISOString();
            finishTelemetry("skipped", sessionResult.reason ?? "session_guard");
            return { runId, status: "skipped" };
        }
        // ── Risk guards ─────────────────────────────────────────────────────────
        const riskResult = await this._checkRiskGuards(this._config.riskGuards ?? [], ctx);
        if (!riskResult.ok) {
            logger.info({ runId, agentId: this._config.agentId, reason: riskResult.reason }, "ooda.cycle.skipped.risk_guard");
            rootSpan.setAttribute("vauban.ooda.status", "skipped");
            rootSpan.setAttribute("vauban.ooda.skip_reason", riskResult.reason ?? "risk_guard");
            rootSpan.setStatus({ code: SpanStatusCode.OK });
            rootSpan.end();
            this._cyclesCompleted += 1;
            this._lastCycleAt = new Date().toISOString();
            finishTelemetry("skipped", riskResult.reason ?? "risk_guard");
            return { runId, status: "skipped" };
        }
        // ── Heap watermark ──────────────────────────────────────────────────────
        const heapMb = process.memoryUsage().heapUsed / (1024 * 1024);
        if (heapMb > limits.maxHeapMb) {
            logger.warn({ runId, heapMb, limitMb: limits.maxHeapMb }, "ooda.cycle.heap_exceeded");
            rootSpan.setAttribute("vauban.ooda.status", "skipped");
            rootSpan.setAttribute("vauban.ooda.skip_reason", `heap_exceeded:${heapMb.toFixed(1)}mb`);
            rootSpan.setStatus({ code: SpanStatusCode.OK });
            rootSpan.end();
            this._cyclesCompleted += 1;
            this._lastCycleAt = new Date().toISOString();
            finishTelemetry("skipped", `heap_exceeded:${heapMb.toFixed(1)}mb`);
            return { runId, status: "skipped" };
        }
        // ── Phase chain ────────────────────────────────────────────────────────
        try {
            // Inject initialContext into observe phase for event-driven activation
            const observeInput = initialContext
                ? initialContext
                : undefined;
            const obs = await this._runOodaPhase(this._config.phases.observe, observeInput, ctx, tracer);
            const oriented = await this._runOodaPhase(this._config.phases.orient, obs, ctx, tracer);
            const decision = await this._runOodaPhase(this._config.phases.decide, oriented, ctx, tracer);
            const action = await this._runOodaPhase(this._config.phases.act, decision, ctx, tracer, { hitlGate: this._config.phases.act.hitlGate === true });
            // Optional 6th phase — self-critique between act and feedback
            const reflectInput = action;
            const feedbackInput = this._config.phases.reflect
                ? (await this._runOodaPhase(this._config.phases.reflect, { ...reflectInput, decision: decision }, ctx, tracer))
                : action;
            const feedback = await this._runOodaPhase(this._config.phases.feedback, feedbackInput, ctx, tracer);
            if (this._config.outcomeMapping) {
                try {
                    const outcome = this._config.outcomeMapping(feedback);
                    if (outcome) {
                        logger.info({ runId, outcome_type: outcome.outcome_type }, "ooda.cycle.outcome");
                        // Propagate to telemetry.finish wire event so sinks can persist
                        // it (CC sink writes agent_run.outcome_quality, OTLP exports as
                        // span attributes, etc.). The SDK itself never touches storage.
                        capturedOutcome = {
                            type: outcome.outcome_type,
                            valueCents: outcome.value_cents,
                            ...(outcome.quality !== undefined
                                ? { quality: outcome.quality }
                                : {}),
                            ...(outcome.confidence !== undefined
                                ? { confidence: outcome.confidence }
                                : {}),
                            ...(outcome.metadata !== undefined
                                ? { metadata: outcome.metadata }
                                : {}),
                        };
                    }
                }
                catch (e) {
                    logger.error({ runId, err: e.message }, "ooda.cycle.outcome_mapping_failed");
                }
            }
            // ── Skill capture (Hermes Procedural Learning Loop) ─────────────────
            // Dynamic import keeps bundle cold when feature is off.
            if (this._config.skillCapture?.enabled) {
                try {
                    const { captureSkillFromCycle } = await import("./skill-capture.js");
                    const cycleDurationMs = Date.now() - new Date(startedAt).getTime();
                    const trigger = {
                        toolCallCount: stepsThisCycle,
                        roi: capturedOutcome?.valueCents
                            ? capturedOutcome.valueCents / 100
                            : 0,
                        durationMs: cycleDurationMs,
                        wasReplay: isReplay,
                    };
                    const traceSummary = `Cycle ${cycleIndex} ${this._config.agentId}: ${stepsThisCycle} steps`;
                    const outcomeForCapture = capturedOutcome
                        ? {
                            outcome_type: capturedOutcome.type,
                            value_cents: capturedOutcome.valueCents,
                            ...(capturedOutcome.quality !== undefined
                                ? { quality: capturedOutcome.quality }
                                : {}),
                            ...(capturedOutcome.confidence !== undefined
                                ? { confidence: capturedOutcome.confidence }
                                : {}),
                            ...(capturedOutcome.metadata !== undefined
                                ? { metadata: capturedOutcome.metadata }
                                : {}),
                        }
                        : null;
                    await captureSkillFromCycle(this._config.agentId, runId, trigger, outcomeForCapture, traceSummary, this._config.skillCapture, logger);
                }
                catch (err) {
                    logger.warn({ runId, err: err.message }, "ooda.cycle.skill_capture_failed");
                }
            }
            rootSpan.setAttribute("vauban.ooda.status", "succeeded");
            rootSpan.setAttribute("vauban.ooda.steps", stepsThisCycle);
            rootSpan.setStatus({ code: SpanStatusCode.OK });
            rootSpan.end();
            this._cyclesCompleted += 1;
            this._lastCycleAt = new Date().toISOString();
            finishTelemetry("success");
            return { runId, status: "succeeded" };
        }
        catch (err) {
            logger.error({
                runId,
                agentId: this._config.agentId,
                err: err.message,
            }, "ooda.cycle.failed");
            rootSpan.setAttribute("vauban.ooda.status", "failed");
            rootSpan.setAttribute("vauban.ooda.steps", stepsThisCycle);
            rootSpan.setStatus({
                code: SpanStatusCode.ERROR,
                message: err.message,
            });
            rootSpan.recordException(err);
            rootSpan.end();
            this._cyclesCompleted += 1;
            this._lastCycleAt = new Date().toISOString();
            finishTelemetry("failed", undefined, err.message);
            return { runId, status: "failed" };
        }
    }
    /**
     * Execute one full cycle, pushing `CycleEvent` to `sink` as they occur.
     * Used by `streamCycle()` to decouple event emission from async generator
     * mechanics. Single-pass — each phase runs exactly once.
     */
    async _runCycleWithSink(runId, cycleIndex, executionMode, sink, _initialContext) {
        const cycleStart = Date.now();
        const isReplay = false;
        const logger = this._config.logger ?? NOOP_LOGGER;
        // ── OTel: agent-level span for the full stream cycle ────────────────────
        const tracer = getTracer("vauban-agent-sdk.ooda");
        const rootSpan = agentSpan(tracer, {
            agentId: this._config.agentId,
            agentVersion: this._config.agentVersion ?? "0.0.0",
            runId,
        });
        rootSpan.setAttribute("vauban.ooda.cycle_index", cycleIndex);
        rootSpan.setAttribute("vauban.ooda.execution_mode", executionMode);
        rootSpan.setAttribute("vauban.ooda.stream", true);
        // Step counter — anti-pattern #9 (maxStepsPerCycle).
        let stepsThisCycle = 0;
        const limits = this._limits;
        const baseInsertStep = this._config.insertStepImpl ?? defaultInsertStep();
        const insertStep = async (input) => {
            stepsThisCycle += 1;
            if (stepsThisCycle > limits.maxStepsPerCycle) {
                throw new Error(`OODA: maxStepsPerCycle exceeded (${limits.maxStepsPerCycle})`);
            }
            return baseInsertStep(input);
        };
        // Hot-reload config.
        let liveConfig = (this._config.config ??
            {});
        if (this._config.configLoader) {
            try {
                liveConfig = await this._config.configLoader.get(this._config.agentId);
            }
            catch (e) {
                logger.warn({ agentId: this._config.agentId, err: e.message }, "ooda.stream.config_loader_failed — using static config");
            }
        }
        const completeStepStream = this._config.completeStepImpl ?? defaultCompleteStep();
        const errorStepStream = this._config.errorStepImpl ?? defaultErrorStep();
        const ctx = Object.freeze({
            agentId: this._config.agentId,
            runId,
            cycleIndex,
            executionMode,
            isReplay,
            config: liveConfig,
            configLoader: this._config.configLoader,
            db: this._config.db,
            skills: this._config.skills ?? EMPTY_SKILL_REGISTRY,
            logger,
            deps: this._config.deps ?? {},
            insertStep,
            completeStep: completeStepStream,
            errorStep: errorStepStream,
            notifySlack: this._config.notifySlackImpl ?? defaultNotifySlack(),
            emitStep: makeEmitStep({
                insertStep,
                completeStep: completeStepStream,
                errorStep: errorStepStream,
                logger,
            }),
        });
        // ── Session guards ───────────────────────────────────────────────────
        const sessionResult = await this._checkSessionGuards(this._config.sessionGuards ?? []);
        if (!sessionResult.ok) {
            const reason = sessionResult.reason ?? "session_guard";
            sink({
                type: "guard_tripped",
                runId,
                cycleIndex,
                guard: "session",
                reason,
                ts: Date.now(),
            });
            sink({
                type: "cycle_skipped",
                runId,
                cycleIndex,
                reason,
                ts: Date.now(),
            });
            rootSpan.setAttribute("vauban.ooda.status", "skipped");
            rootSpan.setAttribute("vauban.ooda.skip_reason", reason);
            rootSpan.setStatus({ code: SpanStatusCode.OK });
            rootSpan.end();
            this._cyclesCompleted += 1;
            this._lastCycleAt = new Date().toISOString();
            return;
        }
        // ── Risk guards ──────────────────────────────────────────────────────
        const riskResult = await this._checkRiskGuards(this._config.riskGuards ?? [], ctx);
        if (!riskResult.ok) {
            const reason = riskResult.reason ?? "risk_guard";
            sink({
                type: "guard_tripped",
                runId,
                cycleIndex,
                guard: "risk",
                reason,
                ts: Date.now(),
            });
            sink({
                type: "cycle_skipped",
                runId,
                cycleIndex,
                reason,
                ts: Date.now(),
            });
            rootSpan.setAttribute("vauban.ooda.status", "skipped");
            rootSpan.setAttribute("vauban.ooda.skip_reason", reason);
            rootSpan.setStatus({ code: SpanStatusCode.OK });
            rootSpan.end();
            this._cyclesCompleted += 1;
            this._lastCycleAt = new Date().toISOString();
            return;
        }
        // ── Heap watermark ────────────────────────────────────────────────────
        const heapMb = process.memoryUsage().heapUsed / (1024 * 1024);
        if (heapMb > limits.maxHeapMb) {
            const reason = `heap_exceeded:${heapMb.toFixed(1)}mb`;
            sink({
                type: "guard_tripped",
                runId,
                cycleIndex,
                guard: "heap",
                reason,
                ts: Date.now(),
            });
            sink({
                type: "cycle_skipped",
                runId,
                cycleIndex,
                reason,
                ts: Date.now(),
            });
            rootSpan.setAttribute("vauban.ooda.status", "skipped");
            rootSpan.setAttribute("vauban.ooda.skip_reason", reason);
            rootSpan.setStatus({ code: SpanStatusCode.OK });
            rootSpan.end();
            this._cyclesCompleted += 1;
            this._lastCycleAt = new Date().toISOString();
            return;
        }
        // ── Phase runner (single-pass, emits events via sink) ─────────────────
        const runPhaseWithSink = async (phase, input, phaseCtx, phaseOpts) => {
            const phaseName = phase.type;
            const phaseSpan = tracer.startSpan(`ooda.phase.${phaseName}`);
            phaseSpan.setAttribute("vauban.ooda.run_id", runId);
            phaseSpan.setAttribute("vauban.ooda.phase", phaseName);
            sink({
                type: "phase_start",
                runId,
                cycleIndex,
                phase: phaseName,
                ts: Date.now(),
            });
            const phaseStart = Date.now();
            const step = await phaseCtx.insertStep({
                type: phase.type,
                phase: phaseName,
                // SDK 1.6.2 — alongside the determinism-oriented hash, surface the
                // actual phase input so dashboards can render real content. PII
                // redaction + 4 KB cap applied by buildTelemetryMetadata in the
                // insertStep wrapper, so payload may contain prompts/tool args/etc.
                payload: { input_hash: hashPayload(input), input },
            });
            phaseSpan.setAttribute("vauban.ooda.step_id", step.stepId);
            try {
                const out = await withPhaseTimeout(phase.fn(input, phaseCtx), this._limits.phaseTimeoutMs);
                if (phaseOpts?.hitlGate === true && this._config.waitForHITL) {
                    sink({
                        type: "hitl_waiting",
                        runId,
                        cycleIndex,
                        stepId: step.stepId,
                        payloadHash: hashPayload(out),
                        ts: Date.now(),
                    });
                    await this._config.waitForHITL({
                        runId: phaseCtx.runId,
                        stepId: step.stepId,
                        payload: out,
                    });
                    sink({
                        type: "hitl_approved",
                        runId,
                        cycleIndex,
                        stepId: step.stepId,
                        ts: Date.now(),
                    });
                }
                await phaseCtx.completeStep(step.stepId, {
                    // SDK 1.6.3 — surface the actual phase output alongside the
                    // determinism hash, so dashboards can show what each phase
                    // produced. PII redaction + 4 KB cap applied by the SDK wrapper.
                    output_hash: hashPayload(out),
                    output: out,
                });
                const durationMs = Date.now() - phaseStart;
                phaseSpan.setAttribute("vauban.ooda.phase_duration_ms", durationMs);
                phaseSpan.setStatus({ code: SpanStatusCode.OK });
                phaseSpan.end();
                sink({
                    type: "phase_complete",
                    runId,
                    cycleIndex,
                    phase: phaseName,
                    durationMs,
                    ts: Date.now(),
                });
                // Fire-and-await onStep (fail-soft — never aborts cycle).
                await this._fireOnStep({
                    cycleIndex,
                    runId,
                    phase: phase.type,
                    durationMs,
                    out,
                });
                return out;
            }
            catch (err) {
                await phaseCtx.errorStep(step.stepId, err);
                const durationMs = Date.now() - phaseStart;
                phaseSpan.setAttribute("vauban.ooda.phase_duration_ms", durationMs);
                phaseSpan.setStatus({
                    code: SpanStatusCode.ERROR,
                    message: err.message,
                });
                phaseSpan.recordException(err);
                phaseSpan.end();
                sink({
                    type: "phase_error",
                    runId,
                    cycleIndex,
                    phase: phaseName,
                    error: { message: err.message },
                    ts: Date.now(),
                });
                throw err;
            }
        };
        // ── Phase chain ──────────────────────────────────────────────────────
        try {
            const obs = await runPhaseWithSink(this._config.phases.observe, undefined, ctx);
            const oriented = await runPhaseWithSink(this._config.phases.orient, obs, ctx);
            const decision = await runPhaseWithSink(this._config.phases.decide, oriented, ctx);
            const action = await runPhaseWithSink(this._config.phases.act, decision, ctx, { hitlGate: this._config.phases.act.hitlGate === true });
            // Optional 6th phase — self-critique between act and feedback.
            // When omitted, move directly from act → feedback.
            const reflectInput = action;
            const feedbackInput = this._config.phases.reflect
                ? (await runPhaseWithSink(this._config.phases.reflect, { ...reflectInput, decision: decision }, ctx))
                : action;
            const feedback = await runPhaseWithSink(this._config.phases.feedback, feedbackInput, ctx);
            // Outcome mapping (optional, non-fatal in stream mode).
            let capturedOutcome;
            if (this._config.outcomeMapping) {
                try {
                    const outcome = this._config.outcomeMapping(feedback);
                    if (outcome) {
                        logger.info({ runId, outcome_type: outcome.outcome_type }, "ooda.stream.outcome");
                        capturedOutcome = {
                            type: outcome.outcome_type,
                            valueCents: outcome.value_cents,
                            ...(outcome.quality !== undefined
                                ? { quality: outcome.quality }
                                : {}),
                            ...(outcome.confidence !== undefined
                                ? { confidence: outcome.confidence }
                                : {}),
                            ...(outcome.metadata !== undefined
                                ? { metadata: outcome.metadata }
                                : {}),
                        };
                    }
                }
                catch (e) {
                    logger.error({ runId, err: e.message }, "ooda.stream.outcome_mapping_failed");
                }
            }
            // ── Skill capture (Hermes Procedural Learning Loop, stream variant) ─
            // Symmetric to runCycle (sprint-727:stream-capture-onstep). Dynamic
            // import keeps bundle cold when the feature is off.
            if (this._config.skillCapture?.enabled) {
                try {
                    const { captureSkillFromCycle } = await import("./skill-capture.js");
                    const cycleDurationMs = Date.now() - cycleStart;
                    const trigger = {
                        toolCallCount: stepsThisCycle,
                        roi: capturedOutcome?.valueCents
                            ? capturedOutcome.valueCents / 100
                            : 0,
                        durationMs: cycleDurationMs,
                        wasReplay: isReplay,
                    };
                    const traceSummary = `Cycle ${cycleIndex} ${this._config.agentId} (stream): ${stepsThisCycle} steps`;
                    const outcomeForCapture = capturedOutcome
                        ? {
                            outcome_type: capturedOutcome.type,
                            value_cents: capturedOutcome.valueCents,
                            ...(capturedOutcome.quality !== undefined
                                ? { quality: capturedOutcome.quality }
                                : {}),
                            ...(capturedOutcome.confidence !== undefined
                                ? { confidence: capturedOutcome.confidence }
                                : {}),
                            ...(capturedOutcome.metadata !== undefined
                                ? { metadata: capturedOutcome.metadata }
                                : {}),
                        }
                        : null;
                    await captureSkillFromCycle(this._config.agentId, runId, trigger, outcomeForCapture, traceSummary, this._config.skillCapture, logger);
                }
                catch (err) {
                    logger.warn({ runId, err: err.message }, "ooda.stream.skill_capture_failed");
                }
            }
            this._cyclesCompleted += 1;
            this._lastCycleAt = new Date().toISOString();
            rootSpan.setAttribute("vauban.ooda.status", "succeeded");
            rootSpan.setAttribute("vauban.ooda.steps", stepsThisCycle);
            rootSpan.setAttribute("vauban.ooda.duration_ms", Date.now() - cycleStart);
            rootSpan.setStatus({ code: SpanStatusCode.OK });
            rootSpan.end();
            sink({
                type: "cycle_complete",
                runId,
                cycleIndex,
                status: "succeeded",
                durationMs: Date.now() - cycleStart,
                ts: Date.now(),
            });
            // ── A4 timestamping (optional, non-blocking) ──────────────────────────
            // TODO(A-DEMO): rootHash will be provided by the Trace builder once A-DEMO
            // wires buildChain() into the cycle. Until then, no TSA call is made.
            //
            // When rootHash is available (A-DEMO integration), replace the block below:
            //
            //   if (this._config.timestamping?.when === 'on-cycle-complete') {
            //     try {
            //       await this._config.timestamping.port.request(rootHash);
            //     } catch (tsaErr) {
            //       sink({ type: 'timestamp_pending', runId, cycleIndex, rootHash, ts: Date.now() });
            //       if (this._config.timestamping.queue &&
            //           this._config.timestamping.queueHmacKeyProvider &&
            //           this._config.timestamping.queueHmacKeyId) {
            //         await this._config.timestamping.queue.enqueue(
            //           { runId, rootHash, queuedAt: Date.now() },
            //           this._config.timestamping.queueHmacKeyProvider,
            //           this._config.timestamping.queueHmacKeyId,
            //         ).catch(() => {}); // best-effort
            //       }
            //     }
            //   }
        }
        catch (err) {
            logger.error({ runId, agentId: this._config.agentId, err: err.message }, "ooda.stream.cycle.failed");
            this._cyclesCompleted += 1;
            this._lastCycleAt = new Date().toISOString();
            rootSpan.setAttribute("vauban.ooda.status", "failed");
            rootSpan.setAttribute("vauban.ooda.steps", stepsThisCycle);
            rootSpan.setAttribute("vauban.ooda.duration_ms", Date.now() - cycleStart);
            rootSpan.setStatus({
                code: SpanStatusCode.ERROR,
                message: err.message,
            });
            rootSpan.recordException(err);
            rootSpan.end();
            sink({
                type: "cycle_error",
                runId,
                cycleIndex,
                error: { message: err.message },
                ts: Date.now(),
            });
            sink({
                type: "cycle_complete",
                runId,
                cycleIndex,
                status: "failed",
                durationMs: Date.now() - cycleStart,
                ts: Date.now(),
            });
        }
    }
    /**
     * Run a single OODA phase with OTel span instrumentation. Used by runCycle().
     */
    async _runOodaPhase(phase, input, ctx, tracer, opts) {
        const phaseSpan = tracer.startSpan(`ooda.phase.${phase.type}`);
        phaseSpan.setAttribute("vauban.ooda.run_id", ctx.runId);
        phaseSpan.setAttribute("vauban.ooda.phase", phase.type);
        const phaseStart = Date.now();
        const step = await ctx.insertStep({
            type: phase.type,
            phase: phase.type,
            // SDK 1.6.2 — alongside the determinism-oriented hash, surface the
            // actual phase input so dashboards can render real content. PII
            // redaction + 4 KB cap applied by buildTelemetryMetadata in the
            // insertStep wrapper, so payload may contain prompts/tool args/etc.
            payload: { input_hash: hashPayload(input), input },
        });
        phaseSpan.setAttribute("vauban.ooda.step_id", step.stepId);
        try {
            const out = await withPhaseTimeout(phase.fn(input, ctx), this._limits.phaseTimeoutMs);
            if (opts?.hitlGate === true && this._config.waitForHITL) {
                await this._config.waitForHITL({
                    runId: ctx.runId,
                    stepId: step.stepId,
                    payload: out,
                });
            }
            await ctx.completeStep(step.stepId, {
                // SDK 1.6.3 — surface the actual phase output alongside the
                // determinism hash, so dashboards can show what each phase
                // produced. PII redaction + 4 KB cap applied by the SDK wrapper.
                output_hash: hashPayload(out),
                output: out,
            });
            const durationMs = Date.now() - phaseStart;
            phaseSpan.setAttribute("vauban.ooda.phase_duration_ms", durationMs);
            phaseSpan.setStatus({ code: SpanStatusCode.OK });
            phaseSpan.end();
            // Fire-and-await onStep callback (fail-soft — never aborts cycle).
            await this._fireOnStep({
                cycleIndex: ctx.cycleIndex,
                runId: ctx.runId,
                phase: phase.type,
                durationMs,
                out,
            });
            return out;
        }
        catch (err) {
            await ctx.errorStep(step.stepId, err);
            phaseSpan.setAttribute("vauban.ooda.phase_duration_ms", Date.now() - phaseStart);
            phaseSpan.setStatus({
                code: SpanStatusCode.ERROR,
                message: err.message,
            });
            phaseSpan.recordException(err);
            phaseSpan.end();
            throw err;
        }
    }
    /**
     * Fire-soft onStep callback (sprint-727:stream-capture-onstep).
     *
     * Best-effort extraction of token/cost/model metadata from the phase output
     * shape when it looks like a standard LLM-completion result (i.e. it carries
     * `inputTokens` / `outputTokens` / `costUsd` / `model` keys). Phases that do
     * NOT return such a shape simply omit those fields — the consumer (TUI etc.)
     * must treat them as optional.
     *
     * Errors thrown by the user-provided `onStep` are caught and logged. The
     * cycle is NEVER aborted on a callback failure.
     */
    async _fireOnStep(args) {
        const onStep = this._config.onStep;
        if (!onStep)
            return;
        const logger = this._config.logger ?? NOOP_LOGGER;
        try {
            const meta = (args.out && typeof args.out === "object" ? args.out : {});
            const tokensIn = typeof meta.inputTokens === "number" ? meta.inputTokens : undefined;
            const tokensOut = typeof meta.outputTokens === "number" ? meta.outputTokens : undefined;
            const costUsd = typeof meta.costUsd === "number" ? meta.costUsd : undefined;
            const model = typeof meta.model === "string" ? meta.model : undefined;
            const evt = {
                cycleIndex: args.cycleIndex,
                phase: args.phase,
                durationMs: args.durationMs,
                runId: args.runId,
                ...(tokensIn !== undefined ? { tokensIn } : {}),
                ...(tokensOut !== undefined ? { tokensOut } : {}),
                ...(costUsd !== undefined ? { costUsd } : {}),
                ...(model !== undefined ? { model } : {}),
            };
            await onStep(evt);
        }
        catch (err) {
            logger.warn({
                runId: args.runId,
                phase: args.phase,
                err: err?.message ?? String(err),
            }, "ooda.onstep_callback_failed");
        }
    }
    async _checkSessionGuards(guards) {
        const now = new Date();
        for (const g of guards) {
            const active = await g.isActive(now);
            if (!active)
                return { ok: false, reason: `session:${g.name}` };
        }
        return { ok: true };
    }
    async _checkRiskGuards(guards, ctx) {
        for (const g of guards) {
            const r = await g.check(ctx);
            if (!r.proceed) {
                return { ok: false, reason: `risk:${g.name}:${r.reason ?? "tripped"}` };
            }
        }
        return { ok: true };
    }
}
/** Cheap structural hash — just JSON length + first chars, NOT crypto. */
function hashPayload(v) {
    try {
        const s = JSON.stringify(v);
        if (!s)
            return "null";
        return `len=${s.length}:${s.slice(0, 32)}`;
    }
    catch {
        return "unserializable";
    }
}
//# sourceMappingURL=agent.js.map