import { AGENT_MANAGER_DEFAULTS } from '../../constants/agent/index.js';
import { EMPTY_TOKEN_USAGE } from '../../constants/limits.js';
import { DelegationCapacityExceeded, } from '../../session/handoff/capacity.js';
import { isTerminalAgentTaskState } from '../../types/agent/task.js';
import { RUN_EVENT_SCHEMA_VERSION } from '../../types/run/schema-version.js';
import { createChildAbortController } from '../../utils/abort.js';
import { ZERO_COST } from '../../utils/cost.js';
import { toErrorMessage } from '../../utils/error.js';
import { generateTaskId } from '../../utils/id.js';
import { getRootLogger } from '../../utils/logger.js';
export class AgentManager {
    registry;
    instances = new Map();
    spawnRecords = new Map();
    completionCallbacks = new Map();
    listeners = [];
    log;
    config;
    evictionTimers = new Map();
    deps;
    constructor(registry, config, deps) {
        this.registry = registry;
        this.config = { ...AGENT_MANAGER_DEFAULTS, ...config };
        this.log = getRootLogger().child({ component: 'AgentManager' });
        this.deps = deps;
    }
    async sendMessage(options, context, listener) {
        if (context.depth >= this.config.maxDepth) {
            throw new Error(`Max task depth ${this.config.maxDepth} exceeded (current: ${context.depth}). Recursive agent delegation is limited to prevent resource exhaustion.`);
        }
        if (options.tenantId !== context.tenantId) {
            throw new Error(`Tenant mismatch: options.tenantId=${options.tenantId} differs from context.tenantId=${context.tenantId}. Cross-tenant spawn rejected (Convention #17).`);
        }
        const agent = this.registry.resolve(options.agentId);
        const childAbortController = createChildAbortController(context.parentAbortController);
        const maxAllocation = Math.floor(context.budgetTracker.remaining * this.config.maxBudgetFraction);
        const allocatedTokens = Math.min(options.budgetAllocation?.tokenBudget ?? maxAllocation, maxAllocation);
        context.budgetTracker.remaining -= allocatedTokens;
        // Phase 6: SubSession + child Session + WorkspaceRef triple. Happens
        // before taskId minting so a capacity failure short-circuits cleanly
        // with no observable state change.
        const spawnRecord = await this.provisionSpawn(options, context);
        const taskId = generateTaskId();
        const childParentActor = {
            kind: 'agent',
            agentId: context.parentAgentId,
            tenantId: context.tenantId,
            parentActor: context.parentActor,
        };
        const childContext = {
            parentRunId: context.parentRunId,
            parentAgentId: context.parentAgentId,
            parentAbortController: context.parentAbortController,
            depth: context.depth + 1,
            budgetTracker: context.budgetTracker,
            factoryOptions: context.factoryOptions,
            tenantId: context.tenantId,
            threadId: context.threadId,
            sessionId: spawnRecord.childSessionId,
            projectId: context.projectId,
            parentActor: childParentActor,
        };
        const agentTask = {
            taskId,
            agentId: options.agentId,
            agent,
            childAbortController,
            context: childContext,
            state: 'pending',
            pendingMessages: [],
            createdAt: Date.now(),
            runEventListener: listener,
        };
        this.instances.set(taskId, agentTask);
        this.spawnRecords.set(taskId, spawnRecord);
        this.emit({
            type: 'pending',
            taskId,
            agentId: options.agentId,
            parentAgentId: context.parentAgentId,
            depth: context.depth,
        });
        if (listener) {
            listener({
                type: 'agent_pending',
                runId: context.parentRunId,
                taskId,
                parentAgentId: context.parentAgentId,
                childAgentId: options.agentId,
                depth: context.depth,
            });
            const lineage = {
                parentSessionId: spawnRecord.parentSessionId,
                rootSessionId: spawnRecord.rootSessionId,
                depth: spawnRecord.childDepth,
            };
            listener({
                type: 'subsession_spawned',
                runId: context.parentRunId,
                subSessionId: spawnRecord.subSessionId,
                parentSessionId: spawnRecord.parentSessionId,
                spawnedBy: context.parentActor,
                lineage,
                schemaVersion: RUN_EVENT_SCHEMA_VERSION,
                at: new Date(),
            });
        }
        this.log.info(`Agent task pending: ${taskId} (${options.agentId}, depth=${context.depth})`);
        const definition = this.registry.getOrThrow(options.agentId);
        let childConfig;
        if (definition.configBuilder) {
            // Call the configBuilder regardless of whether factoryOptions were
            // supplied. BYO-provider flows (Bedrock IAM, custom ProviderRegistry)
            // commonly omit factoryOptions because the provider resolves its own
            // credentials; the builder still needs to run to wire provider+tools.
            // Defaults: empty factoryOptions when omitted; configOverrides win.
            childConfig = await definition.configBuilder({
                ...(context.factoryOptions ?? {}),
                tokenBudget: allocatedTokens,
                timeoutMs: options.budgetAllocation?.timeoutMs ?? context.budgetTracker.remaining,
                parentRunId: context.parentRunId,
                depth: context.depth + 1,
                ...options.configOverrides,
            });
            if (!childConfig.contextLevel && definition.contextLevel) {
                childConfig.contextLevel = definition.contextLevel;
            }
            // Propagate session-hierarchy scoping onto the child config. The
            // configBuilder may not have been updated to emit these yet; we
            // stamp them here so query() sees them regardless.
            childConfig.sessionId = spawnRecord?.childSessionId ?? context.sessionId;
            childConfig.threadId = context.threadId;
            childConfig.projectId = context.projectId;
            childConfig.tenantId = context.tenantId;
        }
        else {
            this.log.warn('No configBuilder, using bare config', {
                agentId: options.agentId,
            });
            childConfig = {
                model: options.configOverrides?.model ?? 'default',
                tokenBudget: allocatedTokens,
                timeoutMs: options.budgetAllocation?.timeoutMs ?? context.budgetTracker.remaining,
                temperature: options.configOverrides?.temperature,
                maxIterations: options.configOverrides?.maxIterations,
                maxResponseTokens: options.configOverrides?.maxResponseTokens,
                env: options.configOverrides?.env,
                sessionId: spawnRecord.childSessionId,
                threadId: context.threadId,
                projectId: context.projectId,
                tenantId: context.tenantId,
                parentRunId: context.parentRunId,
                depth: context.depth + 1,
            };
        }
        this.runChild(agentTask, options, childConfig, listener).catch((err) => {
            this.markFailed(taskId, toErrorMessage(err));
        });
        return agentTask;
    }
    cancel(taskId) {
        const agentTask = this.instances.get(taskId);
        if (!agentTask || isTerminalAgentTaskState(agentTask.state))
            return;
        agentTask.childAbortController.abort('canceled');
        this.markCanceled(taskId);
    }
    cancelAll(parentRunId) {
        for (const agentTask of this.listByParent(parentRunId)) {
            this.cancel(agentTask.taskId);
        }
    }
    async continueTask(taskId, message) {
        const agentTask = this.requireInstance(taskId);
        if (isTerminalAgentTaskState(agentTask.state)) {
            throw new Error(`Cannot continue terminal task: ${taskId} (state: ${agentTask.state})`);
        }
        agentTask.pendingMessages.push({
            role: 'user',
            content: message,
        });
        this.log.info(`Message queued for task ${taskId} via continueTask`);
    }
    queueMessage(taskId, message) {
        const agentTask = this.requireInstance(taskId);
        agentTask.pendingMessages.push(message);
    }
    drainMessages(taskId) {
        const agentTask = this.requireInstance(taskId);
        const messages = [...agentTask.pendingMessages];
        agentTask.pendingMessages.length = 0;
        return messages;
    }
    waitForCompletion(taskId) {
        const agentTask = this.instances.get(taskId);
        if (!agentTask) {
            return Promise.reject(new Error(`Agent task not found: "${taskId}"`));
        }
        if (isTerminalAgentTaskState(agentTask.state)) {
            return Promise.resolve();
        }
        return new Promise((resolve) => {
            const existing = this.completionCallbacks.get(taskId) ?? [];
            existing.push(resolve);
            this.completionCallbacks.set(taskId, existing);
        });
    }
    getInstance(taskId) {
        return this.instances.get(taskId);
    }
    getSpawnRecord(taskId) {
        return this.spawnRecords.get(taskId);
    }
    listByParent(parentRunId) {
        return Array.from(this.instances.values()).filter((t) => t.context.parentRunId === parentRunId);
    }
    listActive() {
        return Array.from(this.instances.values()).filter((t) => !isTerminalAgentTaskState(t.state));
    }
    getState(taskId) {
        return this.instances.get(taskId)?.state;
    }
    getRegistry() {
        return this.registry;
    }
    on(listener) {
        this.listeners.push(listener);
    }
    off(listener) {
        const index = this.listeners.indexOf(listener);
        if (index >= 0)
            this.listeners.splice(index, 1);
    }
    cleanup() {
        for (const [taskId, agentTask] of this.instances) {
            if (isTerminalAgentTaskState(agentTask.state)) {
                this.clearEvictionTimer(taskId);
                this.instances.delete(taskId);
                this.spawnRecords.delete(taskId);
            }
        }
    }
    dispose() {
        for (const taskId of this.instances.keys()) {
            this.clearEvictionTimer(taskId);
        }
        this.cancelAll('');
        this.instances.clear();
        this.spawnRecords.clear();
        this.listeners.length = 0;
    }
    async provisionSpawn(options, context) {
        // Phase 9: deps are unconditional required. Every spawn produces a
        // SubSession + Session + WorkspaceRef triple (Convention #0: no
        // partial/legacy path).
        const store = this.deps.sessionStore;
        // Thread archive gate — runs FIRST so an archived thread fails fastest
        // with the correct error (not DelegationCapacityExceeded or a project
        // lookup error). Phase 2.6 closes the gap the Phase 2.5 commit
        // flagged: without it, `ThreadManager.archive` could be undermined by
        // a concurrent spawn landing a live session post-archival.
        // Scope: this gate enforces the archive invariant at the production
        // ingress path (AgentManager.sendMessage + handoff flows). Direct
        // callers of `SessionStore.createSession` bypass it — the store layer
        // is intentionally unaware of thread status to preserve its
        // single-responsibility boundary.
        await this.deps.threadManager.requireOpen(context.threadId, context.tenantId);
        // Parent session cross-check: validate that `options.parentSessionId`
        // exists for this tenant AND lives under the same thread as the
        // context. A mismatched `context.threadId` would otherwise attach the
        // child's sub-session edge to a parent in a different thread —
        // corrupting the hierarchy invariant (cross-thread spawn is forbidden
        // by design). Mirrors the `source.threadId === assignment.threadId`
        // check in handoff (Phase 2.4).
        const parentSession = await store.getSession(options.parentSessionId, context.tenantId);
        if (!parentSession) {
            throw new Error(`Parent session ${options.parentSessionId} not found for tenant ${context.tenantId} — spawn rejected`);
        }
        if (parentSession.threadId !== context.threadId) {
            throw new Error(`Thread mismatch on spawn: parent session ${parentSession.id} is on thread ${parentSession.threadId}, but context.threadId=${context.threadId}. Cross-thread spawn is forbidden (session-hierarchy.md §6.3).`);
        }
        if (parentSession.projectId !== context.projectId) {
            throw new Error(`Project mismatch on spawn: parent session ${parentSession.id} is on project ${parentSession.projectId}, but context.projectId=${context.projectId}.`);
        }
        const project = await store.getProject(context.projectId, context.tenantId);
        if (!project) {
            throw new Error(`Project ${context.projectId} not found for tenant ${context.tenantId} — spawn rejected`);
        }
        // Capacity: depth + width. Depth uses the parent session's ancestry
        // chain; width counts existing direct children of the parent.
        await this.deps.capacity.validateDepth(options.parentSessionId, project.config.maxDelegationDepth, context.tenantId);
        await this.deps.capacity.validateWidth(options.parentSessionId, 1, project.config.maxDelegationWidth, context.tenantId);
        // Ancestry walk gives both the child depth and the root session id
        // attached to every sub-session event from here down.
        const parentAncestry = await store.getAncestry(options.parentSessionId, context.tenantId);
        const rootSessionId = parentAncestry[0] ?? options.parentSessionId;
        const childDepth = parentAncestry.length;
        const childActor = {
            kind: 'agent',
            agentId: options.agentId,
            tenantId: context.tenantId,
            parentActor: context.parentActor,
        };
        // Child session inherits the parent's threadId verbatim (cross-thread
        // spawn is forbidden by design — a delegated sub-agent stays on the
        // same topic). Phase 2.6 elides the previous parent-session read by
        // carrying `threadId` on `AgentTaskContext`.
        const childSession = await store.createSession({
            threadId: context.threadId,
            projectId: context.projectId,
            currentActor: childActor,
        }, context.tenantId);
        // Compensating rollback wraps every mutation after createSession so a
        // mid-flight failure (status flip, subsession insert, workspace driver)
        // leaves no orphan child session. Codex SPAWN-ROLLBACK critique (Phase
        // 2 review, 2026-04-18): without this, `workspaceRegistry.get().create`
        // throwing — or a concurrent `updateSession` race — strands an
        // `active` child session with no subsession edge, invisible to the
        // parent but counted against `maxDelegationWidth`.
        let subSession;
        let workspaceRef;
        try {
            // Flip to 'active' so the materializer's atomic write + status flip
            // lands on terminal — §5.3: pending→active→idle.
            await store.updateSession({ ...childSession, status: 'active' }, context.tenantId);
            subSession = await store.createSubSession({
                parentSessionId: options.parentSessionId,
                childSessionId: childSession.id,
                kind: 'agent_spawn',
                spawnedBy: context.parentActor,
                failureMode: 'delegate',
                completionMode: 'summary_ref',
            }, context.tenantId);
            // Workspace provisioning — best-effort. When the requested backend
            // is registered we create a new workspace for the child; failures
            // surface as WorkspaceBackendError and abort the spawn (Convention
            // #0: no silent fallback). Pattern doc §7.1 allows lazy
            // provisioning: an unregistered backend leaves `workspaceRef:
            // undefined` on the spawn record, not a hard error — the registry
            // is the capability surface.
            const backend = options.workspaceBackend ?? 'git-worktree';
            if (this.deps.workspaceRegistry.has(backend)) {
                const driver = this.deps.workspaceRegistry.get(backend);
                workspaceRef = await driver.create({ label: subSession.id });
            }
        }
        catch (err) {
            // Compensating rollback order is mandated by the store's
            // deny-by-default cascade policy (Convention #5): `deleteSession`
            // throws when any subsession still references it, so the subsession
            // record must be removed first. No failed-subsession audit row is
            // kept — the `subsession_spawned` run event never fired (we aborted
            // before `buildSpawnRecord`), so no observer is expecting one, and
            // leaving a `status: 'failed'` breadcrumb would be a dangling
            // record with no corresponding emission. The original `err` is the
            // caller-visible signal; cleanup errors are swallowed so they
            // cannot mask it.
            if (subSession !== undefined) {
                await store.deleteSubSession(subSession.id, context.tenantId).catch(() => undefined);
            }
            await store.deleteSession(childSession.id, context.tenantId).catch(() => undefined);
            throw err;
        }
        return {
            subSessionId: subSession.id,
            childSessionId: childSession.id,
            tenantId: context.tenantId,
            parentSessionId: options.parentSessionId,
            rootSessionId,
            childDepth,
            workspaceRef,
        };
    }
    async runChild(agentTask, options, childConfig, listener) {
        this.updateState(agentTask.taskId, 'running');
        this.emit({ type: 'running', taskId: agentTask.taskId });
        const input = {
            ...options.input,
            signal: agentTask.childAbortController.signal,
        };
        const spawnRecord = this.spawnRecords.get(agentTask.taskId);
        const childListener = this.wrapChildListener(listener, spawnRecord);
        const result = await agentTask.agent.run(input, childConfig, childListener);
        await this.finalizeChild(agentTask, result);
    }
    /**
     * Wraps the parent listener so every event emitted from the child's run
     * carries the session-hierarchy `lineage` + `schemaVersion: 2` stamp.
     * Replaces the old `Object.assign({sourceAgentId, parentTaskId}, event)`
     * loose-cast pattern entirely — the types now encode the linkage.
     */
    wrapChildListener(listener, spawnRecord) {
        if (!listener)
            return undefined;
        if (!spawnRecord)
            return listener;
        const lineage = {
            parentSessionId: spawnRecord.parentSessionId,
            rootSessionId: spawnRecord.rootSessionId,
            depth: spawnRecord.childDepth,
        };
        return async (event) => {
            const stamped = {
                ...event,
                lineage,
                schemaVersion: RUN_EVENT_SCHEMA_VERSION,
            };
            await listener(stamped);
        };
    }
    async finalizeChild(agentTask, result) {
        const spawnRecord = this.spawnRecords.get(agentTask.taskId);
        // Kernel terminalization (§8.1): Materializer seals the summary and
        // atomically flips the child session active→idle. Only run when the
        // child actually succeeded; failed sub-sessions skip materialization
        // and transition the sub-session record to 'failed' (§5.5).
        if (spawnRecord) {
            const store = this.deps.sessionStore;
            try {
                if (result.status === 'completed') {
                    const outcome = deriveOutcome(result);
                    const agentSummary = deriveAgentSummary(result);
                    const summary = await this.deps.summaryMaterializer.materialize({
                        sessionId: spawnRecord.childSessionId,
                        tenantId: spawnRecord.tenantId,
                        finalOutcome: outcome,
                        agentSummary,
                        declaredDeliverables: [],
                        keyDecisions: [],
                    });
                    const subSession = await store.getSubSession(spawnRecord.subSessionId, spawnRecord.tenantId);
                    if (subSession) {
                        await store.updateSubSession({ ...subSession, status: 'idle', summaryRef: summary.id }, spawnRecord.tenantId);
                    }
                }
                else {
                    // Non-success: mark sub-session failed and, when we own a
                    // workspace, dispose it. Dispose errors are logged but not
                    // propagated — the sub-session state is already persisted.
                    const subSession = await store.getSubSession(spawnRecord.subSessionId, spawnRecord.tenantId);
                    if (subSession) {
                        await store.updateSubSession({ ...subSession, status: 'failed' }, spawnRecord.tenantId);
                    }
                    if (spawnRecord.workspaceRef) {
                        const backend = spawnRecord.workspaceRef.meta.backend;
                        if (this.deps.workspaceRegistry.has(backend)) {
                            await this.deps.workspaceRegistry
                                .get(backend)
                                .dispose(spawnRecord.workspaceRef)
                                .catch((disposeErr) => {
                                this.log.warn('Workspace dispose failed', {
                                    backend,
                                    error: toErrorMessage(disposeErr),
                                });
                            });
                        }
                    }
                }
            }
            catch (err) {
                this.log.error('Sub-session finalization failed', {
                    taskId: agentTask.taskId,
                    error: toErrorMessage(err),
                });
            }
        }
        // Emit subsession_idled event on success before marking the task
        // completed — consumers expect the ordering `run_completed (child) →
        // subsession_idled → run_completed (parent)` per §10.5.
        if (spawnRecord && agentTask.runEventListener && result.status === 'completed') {
            const lineage = {
                parentSessionId: spawnRecord.parentSessionId,
                rootSessionId: spawnRecord.rootSessionId,
                depth: spawnRecord.childDepth,
            };
            try {
                await agentTask.runEventListener({
                    type: 'subsession_idled',
                    runId: agentTask.context.parentRunId,
                    subSessionId: spawnRecord.subSessionId,
                    parentSessionId: spawnRecord.parentSessionId,
                    lineage,
                    schemaVersion: RUN_EVENT_SCHEMA_VERSION,
                    at: new Date(),
                });
            }
            catch (err) {
                this.log.error('subsession_idled emission error', {
                    taskId: agentTask.taskId,
                    error: toErrorMessage(err),
                });
            }
        }
        this.markCompleted(agentTask.taskId, result);
    }
    markCompleted(taskId, result) {
        const agentTask = this.instances.get(taskId);
        if (!agentTask || isTerminalAgentTaskState(agentTask.state))
            return;
        agentTask.result = result;
        agentTask.completedAt = Date.now();
        this.updateState(taskId, 'completed');
        this.emit({ type: 'completed', taskId, result });
        this.emitRunEvent(agentTask, {
            type: 'agent_completed',
            runId: agentTask.context.parentRunId,
            taskId,
            result,
        });
        this.log.info(`Agent task completed: ${taskId}`);
        this.scheduleEviction(taskId);
        this.resolveCompletionCallbacks(taskId);
    }
    markFailed(taskId, error) {
        const agentTask = this.instances.get(taskId);
        if (!agentTask || isTerminalAgentTaskState(agentTask.state))
            return;
        agentTask.result = {
            runId: agentTask.context.parentRunId,
            status: 'failed',
            usage: { ...EMPTY_TOKEN_USAGE },
            cost: { ...ZERO_COST },
            iterations: 0,
            durationMs: Date.now() - agentTask.createdAt,
            messages: [],
            lastError: error,
        };
        agentTask.completedAt = Date.now();
        this.updateState(taskId, 'failed');
        this.emit({ type: 'failed', taskId, error });
        this.emitRunEvent(agentTask, {
            type: 'agent_failed',
            runId: agentTask.context.parentRunId,
            taskId,
            error,
        });
        this.log.error(`Agent task failed: ${taskId}`, { error });
        // Best-effort: mark sub-session failed + dispose workspace. The result
        // emission path already synthesized a failure result above.
        const spawnRecord = this.spawnRecords.get(taskId);
        if (spawnRecord) {
            this.failSubSession(spawnRecord).catch((err) => {
                this.log.warn('SubSession failure update failed', {
                    taskId,
                    error: toErrorMessage(err),
                });
            });
        }
        this.scheduleEviction(taskId);
        this.resolveCompletionCallbacks(taskId);
    }
    async failSubSession(spawnRecord) {
        const subSession = await this.deps.sessionStore.getSubSession(spawnRecord.subSessionId, spawnRecord.tenantId);
        if (subSession && subSession.status !== 'failed') {
            await this.deps.sessionStore.updateSubSession({ ...subSession, status: 'failed' }, spawnRecord.tenantId);
        }
        if (spawnRecord.workspaceRef) {
            const backend = spawnRecord.workspaceRef.meta.backend;
            if (this.deps.workspaceRegistry.has(backend)) {
                await this.deps.workspaceRegistry
                    .get(backend)
                    .dispose(spawnRecord.workspaceRef)
                    .catch(() => undefined);
            }
        }
    }
    markCanceled(taskId) {
        const agentTask = this.instances.get(taskId);
        if (!agentTask || isTerminalAgentTaskState(agentTask.state))
            return;
        agentTask.completedAt = Date.now();
        this.updateState(taskId, 'canceled');
        this.emit({ type: 'canceled', taskId });
        this.emitRunEvent(agentTask, {
            type: 'agent_canceled',
            runId: agentTask.context.parentRunId,
            taskId,
        });
        this.log.info(`Agent task canceled: ${taskId}`);
        this.scheduleEviction(taskId);
        this.resolveCompletionCallbacks(taskId);
    }
    updateState(taskId, state) {
        const agentTask = this.instances.get(taskId);
        if (agentTask) {
            agentTask.state = state;
        }
    }
    requireInstance(taskId) {
        const agentTask = this.instances.get(taskId);
        if (!agentTask) {
            throw new Error(`Agent task not found: "${taskId}"`);
        }
        return agentTask;
    }
    scheduleEviction(taskId) {
        const agentTask = this.instances.get(taskId);
        if (!agentTask)
            return;
        agentTask.evictAfter = Date.now() + this.config.evictionMs;
        const timer = setTimeout(() => {
            this.instances.delete(taskId);
            this.spawnRecords.delete(taskId);
            this.evictionTimers.delete(taskId);
            this.log.info(`Agent task evicted: ${taskId}`);
        }, this.config.evictionMs);
        this.evictionTimers.set(taskId, timer);
    }
    resolveCompletionCallbacks(taskId) {
        const callbacks = this.completionCallbacks.get(taskId);
        if (callbacks) {
            for (const resolve of callbacks)
                resolve();
            this.completionCallbacks.delete(taskId);
        }
    }
    clearEvictionTimer(taskId) {
        const timer = this.evictionTimers.get(taskId);
        if (timer) {
            clearTimeout(timer);
            this.evictionTimers.delete(taskId);
        }
    }
    emit(event) {
        for (const listener of this.listeners) {
            try {
                listener(event);
            }
            catch (err) {
                this.log.error('Agent lifecycle listener error', {
                    eventType: event.type,
                    error: toErrorMessage(err),
                });
            }
        }
    }
    emitRunEvent(agentTask, event) {
        if (!agentTask.runEventListener)
            return;
        try {
            agentTask.runEventListener(event);
        }
        catch (err) {
            this.log.error('RunEvent emission error', {
                eventType: event.type,
                taskId: agentTask.taskId,
                error: toErrorMessage(err),
            });
        }
    }
}
/**
 * Maps a {@link BaseAgentResult} to {@link SessionSummaryOutcome}. Phase 6
 * INTERPRETATION: `completed` → `succeeded`; any other status → `failed`.
 * A dedicated `partial` signal requires structured-output contracts on the
 * child's terminal turn (§8.1) which lands in a later phase.
 */
function deriveOutcome(result) {
    if (result.status === 'completed') {
        return { status: 'succeeded' };
    }
    const verdict = result.lastError ?? String(result.status);
    return { status: 'failed', verdict };
}
const SUMMARY_FALLBACK_MAX_CHARS = 4000;
/**
 * Pulls the agent's own narration from the final assistant message. §8.1:
 * agents may register a structured-output contract for this; when absent
 * we fall back to the last text block. Bounded by the summary char cap so
 * the materializer never rejects on length at this seam.
 */
function deriveAgentSummary(result) {
    const fromResult = result.result?.trim();
    if (fromResult) {
        return fromResult.length > SUMMARY_FALLBACK_MAX_CHARS
            ? fromResult.slice(0, SUMMARY_FALLBACK_MAX_CHARS)
            : fromResult;
    }
    for (let i = result.messages.length - 1; i >= 0; i--) {
        const msg = result.messages[i];
        if (msg?.role === 'assistant') {
            const content = typeof msg.content === 'string' ? msg.content : '';
            if (content.trim().length > 0) {
                return content.length > SUMMARY_FALLBACK_MAX_CHARS
                    ? content.slice(0, SUMMARY_FALLBACK_MAX_CHARS)
                    : content;
            }
        }
    }
    return '';
}
// Re-export the capacity-violation type so downstream consumers that import
// via the AgentManager module surface don't reach into session/handoff/.
export { DelegationCapacityExceeded };
//# sourceMappingURL=lifecycle.js.map