import { validateIdentifier } from './validation.js';
import { AfterInvocationEvent, AfterModelCallEvent, InitializedEvent, MessageAddedEvent } from '../hooks/events.js';
import { v7 as uuidV7 } from 'uuid';
import { logger } from '../logging/logger.js';
import { MultiAgentState } from '../multiagent/state.js';
import { takeSnapshot as takeMultiAgentSnapshot, loadSnapshot as loadMultiAgentSnapshot, } from '../multiagent/snapshot.js';
import { AfterMultiAgentInvocationEvent, AfterNodeCallEvent, BeforeMultiAgentInvocationEvent, } from '../multiagent/events.js';
/**
 * Manages session persistence for agents, enabling conversation state
 * to be saved and restored across invocations using pluggable storage backends.
 *
 * Also supports multi-agent orchestrators (Graph, Swarm) via the MultiAgentPlugin interface.
 * Scope is auto-detected based on whether initAgent or initMultiAgent is called.
 *
 * @example
 * ```typescript
 * import { SessionManager, FileStorage } from '@strands-agents/sdk'
 *
 * const session = new SessionManager({
 *   sessionId: 'my-session',
 *   storage: { snapshot: new FileStorage() },
 * })
 * const agent = new Agent({ sessionManager: session })
 * ```
 */
export class SessionManager {
    _sessionId;
    _storage;
    _saveLatestOn;
    _snapshotTrigger;
    _multiAgentSaveLatestOn;
    _multiAgentRestoredIds = new Set();
    /**
     * Unique identifier for this plugin.
     */
    get name() {
        return 'strands:session-manager';
    }
    constructor(config) {
        this._sessionId = validateIdentifier(config.sessionId ?? 'default-session');
        this._storage = { snapshot: config.storage.snapshot };
        this._saveLatestOn = config.saveLatestOn ?? 'invocation';
        this._multiAgentSaveLatestOn = config.multiAgentSaveLatestOn ?? 'node';
        this._snapshotTrigger = config.snapshotTrigger;
    }
    /** Initializes the plugin by registering lifecycle hook callbacks. */
    initAgent(agent) {
        agent.addHook(InitializedEvent, async (event) => {
            await this._onAgentInitialized(event);
        });
        if (this._saveLatestOn === 'message') {
            agent.addHook(MessageAddedEvent, async (event) => {
                await this._onMessageAdded(event);
            });
        }
        // Persist guardrail redactions immediately for auto-save strategies.
        // 'trigger' is an explicit opt-out from auto-saves, so the caller's snapshotTrigger
        // stays in control there.
        if (this._saveLatestOn !== 'trigger') {
            agent.addHook(AfterModelCallEvent, async (event) => {
                await this._onAfterModelCall(event);
            });
        }
        agent.addHook(AfterInvocationEvent, async (event) => {
            await this._onAfterAgentInvocation(event);
        });
    }
    _location(agent) {
        return { sessionId: this._sessionId, scope: 'agent', scopeId: agent.id };
    }
    async saveSnapshot(params) {
        const isAgent = 'messages' in params.target;
        const snapshot = isAgent
            ? params.target.takeSnapshot({ preset: 'session' })
            : takeMultiAgentSnapshot(params.target, params.state);
        const snapshotId = params.isLatest ? 'latest' : uuidV7();
        const location = isAgent
            ? this._location(params.target)
            : this._multiAgentLocation(params.target);
        await this._storage.snapshot.saveSnapshot({ location, snapshotId, isLatest: params.isLatest, snapshot });
    }
    /** Deletes all snapshots and manifests for this session from storage. */
    async deleteSession() {
        await this._storage.snapshot.deleteSession({ sessionId: this._sessionId });
    }
    /** Lists all available immutable snapshot IDs for the given agent target. */
    async listSnapshotIds(params) {
        return this._storage.snapshot.listSnapshotIds({
            location: this._location(params.target),
            ...(params.limit !== undefined && { limit: params.limit }),
            ...(params.startAfter !== undefined && { startAfter: params.startAfter }),
        });
    }
    async restoreSnapshot(params) {
        const isAgent = 'messages' in params.target;
        const location = isAgent
            ? this._location(params.target)
            : this._multiAgentLocation(params.target);
        const snapshot = await this._storage.snapshot.loadSnapshot({
            location,
            ...(params.snapshotId !== undefined && { snapshotId: params.snapshotId }),
        });
        if (!snapshot)
            return false;
        if (isAgent) {
            ;
            params.target.loadSnapshot(snapshot);
        }
        else {
            loadMultiAgentSnapshot(params.target, snapshot, params.state);
        }
        return true;
    }
    /** Restores session state on agent initialization. */
    async _onAgentInitialized(event) {
        const hadMessages = event.agent.messages.length > 0;
        const restored = await this.restoreSnapshot({ target: event.agent });
        if (restored && hadMessages) {
            logger.warn(`agent_id=<${event.agent.id}>, session_id=<${this._sessionId}> | agent had existing messages that were overwritten by session restore`);
        }
        // Stateful models manage conversation history server-side, so any messages
        // loaded from the snapshot would drift from the server's view on the next
        // invocation. Duck-type the agent's `model` since `LocalAgent` does not
        // expose it — `Agent` is the only implementor and always has one.
        const statefulModel = event.agent.model?.stateful;
        if (restored && statefulModel && event.agent.messages.length > 0) {
            logger.debug(`agent_id=<${event.agent.id}>, message_count=<${event.agent.messages.length}> | discarding restored messages for stateful model`);
            event.agent.messages.length = 0;
        }
    }
    /** Saves latest on invocation and fires the snapshot trigger if configured. */
    async _onAfterAgentInvocation(event) {
        if (this._saveLatestOn === 'invocation') {
            await this.saveSnapshot({ target: event.agent, isLatest: true });
        }
        if (this._snapshotTrigger?.({ agentData: event.agent })) {
            await this._saveImmutableAndLatest(event.agent);
        }
    }
    async _onMessageAdded(event) {
        await this.saveSnapshot({ target: event.agent, isLatest: true });
    }
    /**
     * Saves snapshot when a message is redacted after a model call.
     * Critical for ensuring guardrail redactions are persisted immediately.
     */
    async _onAfterModelCall(event) {
        // Only save if there was a redaction
        if (event.stopData?.redaction) {
            await this.saveSnapshot({ target: event.agent, isLatest: true });
        }
    }
    /** Captures one snapshot and writes it to both immutable history and snapshot_latest. */
    async _saveImmutableAndLatest(agent) {
        const snapshot = agent.takeSnapshot({ preset: 'session' });
        const snapshotId = uuidV7();
        await Promise.all([
            this._storage.snapshot.saveSnapshot({ location: this._location(agent), snapshotId, isLatest: false, snapshot }),
            this._storage.snapshot.saveSnapshot({
                location: this._location(agent),
                snapshotId: 'latest',
                isLatest: true,
                snapshot,
            }),
        ]);
    }
    // ---------------------------------------------------------------------------
    // Multi-agent
    // ---------------------------------------------------------------------------
    /** Initializes the multi-agent plugin by registering orchestrator lifecycle hooks. */
    initMultiAgent(orchestrator) {
        orchestrator.addHook(BeforeMultiAgentInvocationEvent, async (event) => {
            await this._onBeforeMultiAgentInvocation(event);
        });
        if (this._multiAgentSaveLatestOn === 'node') {
            orchestrator.addHook(AfterNodeCallEvent, async (event) => {
                await this._onAfterNodeCall(event);
            });
        }
        orchestrator.addHook(AfterMultiAgentInvocationEvent, async (event) => {
            await this._onAfterMultiAgentInvocation(event);
        });
    }
    _multiAgentLocation(orchestrator) {
        return { sessionId: this._sessionId, scope: 'multiAgent', scopeId: orchestrator.id };
    }
    /** Restores orchestrator state on first invocation (loads snapshot from storage once per orchestrator, then no-ops). */
    async _onBeforeMultiAgentInvocation(event) {
        if (this._multiAgentRestoredIds.has(event.orchestrator.id))
            return;
        this._multiAgentRestoredIds.add(event.orchestrator.id);
        const location = this._multiAgentLocation(event.orchestrator);
        const snapshot = await this._storage.snapshot.loadSnapshot({ location });
        if (!snapshot)
            return;
        loadMultiAgentSnapshot(event.orchestrator, snapshot, event.state);
    }
    /** Saves latest orchestrator snapshot after each node completes. */
    async _onAfterNodeCall(event) {
        await this.saveSnapshot({
            target: event.orchestrator,
            state: event.state,
            isLatest: true,
        });
    }
    /** Saves latest orchestrator snapshot after invocation completes. */
    async _onAfterMultiAgentInvocation(event) {
        await this.saveSnapshot({
            target: event.orchestrator,
            state: event.state,
            isLatest: true,
        });
    }
}
//# sourceMappingURL=session-manager.js.map