/** biome-ignore-all lint/suspicious/noExplicitAny: anys are great */
import type { Context } from 'hono';
import { type Env } from './app';
import type { AppState } from './index';
/**
 * Result of parsing serialized thread data.
 * @internal
 */
export interface ParsedThreadData {
    flatStateJson?: string;
    metadata?: Record<string, unknown>;
}
/**
 * Parse serialized thread data, handling both old (flat state) and new ({ state, metadata }) formats.
 * @internal
 */
export declare function parseThreadData(raw: string | undefined): ParsedThreadData;
export type ThreadEventName = 'destroyed';
export type SessionEventName = 'completed';
/**
 * Represents a merge operation for thread state.
 * Used when state is modified without being loaded first.
 */
export interface MergeOperation {
    op: 'set' | 'delete' | 'clear' | 'push';
    key?: string;
    value?: unknown;
    maxRecords?: number;
}
/**
 * Async thread state storage with lazy loading.
 *
 * State is only fetched from storage when first accessed via a read operation.
 * Write operations can be batched and sent as a merge command without loading.
 *
 * @example
 * ```typescript
 * // Read triggers lazy load
 * const count = await ctx.thread.state.get<number>('messageCount');
 *
 * // Write queues operation (may not trigger load)
 * await ctx.thread.state.set('messageCount', (count ?? 0) + 1);
 *
 * // Check state status
 * if (ctx.thread.state.dirty) {
 *   console.log('State has pending changes');
 * }
 * ```
 */
export interface ThreadState {
    /**
     * Whether state has been loaded from storage.
     * True when state has been fetched via a read operation.
     */
    readonly loaded: boolean;
    /**
     * Whether state has pending changes.
     * True when there are queued writes (pending-writes state) or
     * modifications after loading (loaded state with changes).
     */
    readonly dirty: boolean;
    /**
     * Get a value from thread state.
     * Triggers lazy load if state hasn't been fetched yet.
     */
    get<T = unknown>(key: string): Promise<T | undefined>;
    /**
     * Set a value in thread state.
     * If state hasn't been loaded, queues the operation for merge.
     */
    set<T = unknown>(key: string, value: T): Promise<void>;
    /**
     * Check if a key exists in thread state.
     * Triggers lazy load if state hasn't been fetched yet.
     */
    has(key: string): Promise<boolean>;
    /**
     * Delete a key from thread state.
     * If state hasn't been loaded, queues the operation for merge.
     */
    delete(key: string): Promise<void>;
    /**
     * Clear all thread state.
     * If state hasn't been loaded, queues a clear operation for merge.
     */
    clear(): Promise<void>;
    /**
     * Get all entries as key-value pairs.
     * Triggers lazy load if state hasn't been fetched yet.
     */
    entries<T = unknown>(): Promise<[string, T][]>;
    /**
     * Get all keys.
     * Triggers lazy load if state hasn't been fetched yet.
     */
    keys(): Promise<string[]>;
    /**
     * Get all values.
     * Triggers lazy load if state hasn't been fetched yet.
     */
    values<T = unknown>(): Promise<T[]>;
    /**
     * Get the number of entries in state.
     * Triggers lazy load if state hasn't been fetched yet.
     */
    size(): Promise<number>;
    /**
     * Push a value to an array in thread state.
     * If the key doesn't exist, creates a new array with the value.
     * If state hasn't been loaded, queues the operation for efficient merge.
     *
     * @param key - The key of the array to push to
     * @param value - The value to push
     * @param maxRecords - Optional maximum number of records to keep (sliding window)
     *
     * @example
     * ```typescript
     * // Efficiently append messages without loading entire array
     * await ctx.thread.state.push('messages', { role: 'user', content: 'Hello' });
     * await ctx.thread.state.push('messages', { role: 'assistant', content: 'Hi!' });
     *
     * // Keep only the last 100 messages
     * await ctx.thread.state.push('messages', newMessage, 100);
     * ```
     */
    push<T = unknown>(key: string, value: T, maxRecords?: number): Promise<void>;
}
type ThreadEventCallback<T extends Thread> = (eventName: 'destroyed', thread: T) => Promise<void> | void;
type SessionEventCallback<T extends Session> = (eventName: 'completed', session: T) => Promise<void> | void;
/**
 * Represents a conversation thread that persists across multiple sessions.
 * Threads maintain state and can contain multiple request-response sessions.
 *
 * Threads are automatically managed by the runtime and stored in cookies.
 * They expire after 1 hour of inactivity by default.
 *
 * @example
 * ```typescript
 * // Access thread in agent handler
 * const agent = createAgent('conversation', {
 *   handler: async (ctx, input) => {
 *     // Get thread ID
 *     ctx.logger.info('Thread: %s', ctx.thread.id);
 *
 *     // Store data in thread state (persists across sessions)
 *     const count = await ctx.thread.state.get<number>('conversationCount') ?? 0;
 *     await ctx.thread.state.set('conversationCount', count + 1);
 *
 *     // Access metadata
 *     const meta = await ctx.thread.getMetadata();
 *     await ctx.thread.setMetadata({ ...meta, lastAccess: Date.now() });
 *
 *     // Listen for thread destruction
 *     ctx.thread.addEventListener('destroyed', (eventName, thread) => {
 *       ctx.logger.info('Thread destroyed: %s', thread.id);
 *     });
 *
 *     return 'Response';
 *   }
 * });
 * ```
 */
export interface Thread {
    /**
     * Unique thread identifier (e.g., "thrd_a1b2c3d4...").
     * Stored in cookie and persists across requests.
     */
    id: string;
    /**
     * Thread-scoped state storage with async lazy-loading.
     * State is only fetched from storage when first accessed via a read operation.
     *
     * @example
     * ```typescript
     * // Read triggers lazy load
     * const count = await ctx.thread.state.get<number>('messageCount');
     * // Write may queue operation without loading
     * await ctx.thread.state.set('messageCount', (count ?? 0) + 1);
     * ```
     */
    state: ThreadState;
    /**
     * Get thread metadata (lazy-loaded).
     * Unlike state, metadata is stored unencrypted for efficient filtering.
     *
     * @example
     * ```typescript
     * const meta = await ctx.thread.getMetadata();
     * console.log(meta.userId);
     * ```
     */
    getMetadata(): Promise<Record<string, unknown>>;
    /**
     * Set thread metadata (full replace).
     *
     * @example
     * ```typescript
     * await ctx.thread.setMetadata({ userId: 'user123', department: 'sales' });
     * ```
     */
    setMetadata(metadata: Record<string, unknown>): Promise<void>;
    /**
     * Register an event listener for when the thread is destroyed.
     * Thread is destroyed when it expires or is manually destroyed.
     *
     * @param eventName - Must be 'destroyed'
     * @param callback - Function called when thread is destroyed
     *
     * @example
     * ```typescript
     * ctx.thread.addEventListener('destroyed', (eventName, thread) => {
     *   ctx.logger.info('Cleaning up thread: %s', thread.id);
     * });
     * ```
     */
    addEventListener(eventName: 'destroyed', callback: (eventName: 'destroyed', thread: Thread) => Promise<void> | void): void;
    /**
     * Remove a previously registered 'destroyed' event listener.
     *
     * @param eventName - Must be 'destroyed'
     * @param callback - The callback function to remove
     */
    removeEventListener(eventName: 'destroyed', callback: (eventName: 'destroyed', thread: Thread) => Promise<void> | void): void;
    /**
     * Manually destroy the thread and clean up resources.
     * Fires the 'destroyed' event and removes thread from storage.
     *
     * @example
     * ```typescript
     * // Permanently delete the thread from storage
     * await ctx.thread.destroy();
     * ```
     */
    destroy(): Promise<void>;
    /**
     * Check if the thread has any data.
     * Returns true if thread state is empty (no data to save).
     * This is async because it may need to check lazy-loaded state.
     *
     * @example
     * ```typescript
     * if (await ctx.thread.empty()) {
     *   // Thread has no data, won't be persisted
     * }
     * ```
     */
    empty(): Promise<boolean>;
}
/**
 * Represents a single request-response session within a thread.
 * Sessions are scoped to a single agent execution and its sub-agent calls.
 *
 * Each HTTP request creates a new session with a unique ID, but shares the same thread.
 *
 * @example
 * ```typescript
 * const agent = createAgent('request-handler', {
 *   handler: async (ctx, input) => {
 *     // Get session ID (unique per request)
 *     ctx.logger.info('Session: %s', ctx.session.id);
 *
 *     // Store data in session state (only for this request)
 *     ctx.session.state.set('startTime', Date.now());
 *
 *     // Access parent thread
 *     ctx.logger.info('Thread: %s', ctx.session.thread.id);
 *
 *     // Listen for session completion
 *     ctx.session.addEventListener('completed', (eventName, session) => {
 *       const duration = Date.now() - (session.state.get('startTime') as number);
 *       ctx.logger.info('Session completed in %dms', duration);
 *     });
 *
 *     return 'Response';
 *   }
 * });
 * ```
 */
export interface Session {
    /**
     * Unique session identifier for this request.
     * Changes with each HTTP request, even within the same thread.
     */
    id: string;
    /**
     * The parent thread this session belongs to.
     * Multiple sessions can share the same thread.
     */
    thread: Thread;
    /**
     * Session-scoped state storage that only exists for this request.
     * Use this for temporary data that shouldn't persist across requests.
     *
     * @example
     * ```typescript
     * ctx.session.state.set('requestStartTime', Date.now());
     * ```
     */
    state: Map<string, unknown>;
    /**
     * Unencrypted metadata for filtering and querying sessions.
     * Unlike state, metadata is stored as-is in the database with GIN indexes
     * for efficient filtering. Initialized to empty object, only persisted if non-empty.
     *
     * @example
     * ```typescript
     * ctx.session.metadata.userId = 'user123';
     * ctx.session.metadata.requestType = 'chat';
     * ```
     */
    metadata: Record<string, unknown>;
    /**
     * Register an event listener for when the session completes.
     * Fired after the agent handler returns and response is sent.
     *
     * @param eventName - Must be 'completed'
     * @param callback - Function called when session completes
     *
     * @example
     * ```typescript
     * ctx.session.addEventListener('completed', (eventName, session) => {
     *   ctx.logger.info('Session finished: %s', session.id);
     * });
     * ```
     */
    addEventListener(eventName: 'completed', callback: (eventName: 'completed', session: Session) => Promise<void> | void): void;
    /**
     * Remove a previously registered 'completed' event listener.
     *
     * @param eventName - Must be 'completed'
     * @param callback - The callback function to remove
     */
    removeEventListener(eventName: 'completed', callback: (eventName: 'completed', session: Session) => Promise<void> | void): void;
    /**
     * Return the session data as a serializable string or return undefined if not
     * data should be serialized.
     */
    serializeUserData(): string | undefined;
}
/**
 * Represent an interface for handling how thread ids are generated or restored.
 */
export interface ThreadIDProvider {
    /**
     * A function that should return a thread id to be used for the incoming request.
     * The returning thread id must be globally unique and must start with the prefix
     * thrd_ such as `thrd_212c16896b974ffeb21a748f0eeba620`. The max length of the
     * string is 64 characters and the min length is 32 characters long
     * (including the prefix). The characters after the prefix must match the
     * regular expression [a-zA-Z0-9].
     *
     * @param appState - The app state from createApp setup function
     * @param ctx - Hono request context
     * @returns The thread id to use (can be async for signed cookies)
     */
    getThreadId(appState: AppState, ctx: Context<Env>): string | Promise<string>;
}
/**
 * Provider interface for managing thread lifecycle and persistence.
 * Implement this to customize how threads are stored and retrieved.
 *
 * The default implementation (DefaultThreadProvider) stores threads in-memory
 * with cookie-based identification and 1-hour expiration.
 *
 * Thread state is serialized using `getSerializedState()` which returns a JSON
 * envelope: `{ "state": {...}, "metadata": {...} }`. Use `parseThreadData()` to
 * correctly parse both old (flat) and new (envelope) formats on restore.
 *
 * @example
 * ```typescript
 * class RedisThreadProvider implements ThreadProvider {
 *   private redis: Redis;
 *
 *   async initialize(appState: AppState): Promise<void> {
 *     this.redis = await connectRedis();
 *   }
 *
 *   async restore(ctx: Context<Env>): Promise<Thread> {
 *     const threadId = ctx.req.header('x-thread-id') || getCookie(ctx, 'atid') || generateId('thrd');
 *     const data = await this.redis.get(`thread:${threadId}`);
 *
 *     // Parse stored data, handling both old and new formats
 *     const { flatStateJson, metadata } = parseThreadData(data);
 *     const thread = new DefaultThread(this, threadId, flatStateJson, metadata);
 *
 *     // Populate state from parsed data
 *     if (flatStateJson) {
 *       const stateObj = JSON.parse(flatStateJson);
 *       for (const [key, value] of Object.entries(stateObj)) {
 *         thread.state.set(key, value);
 *       }
 *     }
 *     return thread;
 *   }
 *
 *   async save(thread: Thread): Promise<void> {
 *     if (thread instanceof DefaultThread && thread.isDirty()) {
 *       await this.redis.setex(
 *         `thread:${thread.id}`,
 *         3600,
 *         thread.getSerializedState()
 *       );
 *     }
 *   }
 *
 *   async destroy(thread: Thread): Promise<void> {
 *     await this.redis.del(`thread:${thread.id}`);
 *   }
 * }
 *
 * // Use custom provider
 * const app = await createApp({
 *   services: {
 *     thread: new RedisThreadProvider()
 *   }
 * });
 * ```
 */
export interface ThreadProvider {
    /**
     * Initialize the provider when the app starts.
     * Use this to set up connections, start cleanup intervals, etc.
     *
     * @param appState - The app state from createApp setup function
     */
    initialize(appState: AppState): Promise<void>;
    /**
     * Set the provider to use for generating / restoring the thread id
     * on new requests.  Overrides the built-in provider when set.
     *
     * @param provider - the provider implementation
     */
    setThreadIDProvider(provider: ThreadIDProvider): void;
    /**
     * Restore or create a thread from the HTTP request context.
     * Should check cookies for existing thread ID or create a new one.
     *
     * @param ctx - Hono request context
     * @returns The restored or newly created thread
     */
    restore(ctx: Context<Env>): Promise<Thread>;
    /**
     * Persist thread state to storage.
     * Called periodically to save thread data.
     *
     * @param thread - The thread to save
     */
    save(thread: Thread): Promise<void>;
    /**
     * Destroy a thread and clean up resources.
     * Should fire the 'destroyed' event and remove from storage.
     *
     * @param thread - The thread to destroy
     */
    destroy(thread: Thread): Promise<void>;
}
/**
 * Provider interface for managing session lifecycle and persistence.
 * Implement this to customize how sessions are stored and retrieved.
 *
 * The default implementation (DefaultSessionProvider) stores sessions in-memory
 * and automatically cleans them up after completion.
 *
 * @example
 * ```typescript
 * class PostgresSessionProvider implements SessionProvider {
 *   private db: Database;
 *
 *   async initialize(appState: AppState): Promise<void> {
 *     this.db = appState.db;
 *   }
 *
 *   async restore(thread: Thread, sessionId: string): Promise<Session> {
 *     const row = await this.db.query(
 *       'SELECT state FROM sessions WHERE id = $1',
 *       [sessionId]
 *     );
 *     const session = new DefaultSession(thread, sessionId);
 *     if (row) {
 *       session.state = new Map(JSON.parse(row.state));
 *     }
 *     return session;
 *   }
 *
 *   async save(session: Session): Promise<void> {
 *     await this.db.query(
 *       'INSERT INTO sessions (id, thread_id, state) VALUES ($1, $2, $3)',
 *       [session.id, session.thread.id, JSON.stringify([...session.state])]
 *     );
 *   }
 * }
 *
 * // Use custom provider
 * const app = await createApp({
 *   services: {
 *     session: new PostgresSessionProvider()
 *   }
 * });
 * ```
 */
export interface SessionProvider {
    /**
     * Initialize the provider when the app starts.
     * Use this to set up database connections or other resources.
     *
     * @param appState - The app state from createApp setup function
     */
    initialize(appState: AppState): Promise<void>;
    /**
     * Restore or create a session for the given thread and session ID.
     * Should load existing session data or create a new session.
     *
     * @param thread - The parent thread for this session
     * @param sessionId - The unique session identifier
     * @returns The restored or newly created session
     */
    restore(thread: Thread, sessionId: string): Promise<Session>;
    /**
     * Persist session state and fire completion events.
     * Called after the agent handler completes.
     *
     * @param session - The session to save
     */
    save(session: Session): Promise<void>;
}
export declare function generateId(prefix?: string): string;
/**
 * Validates a thread ID against runtime constraints:
 * - Must start with 'thrd_'
 * - Must be at least 32 characters long (including prefix)
 * - Must be less than 64 characters long
 * - Must contain only [a-zA-Z0-9] after 'thrd_' prefix (no dashes for maximum randomness)
 */
export declare function isValidThreadId(threadId: string): boolean;
/**
 * Validates a thread ID and throws detailed error messages for debugging.
 * @param threadId The thread ID to validate
 * @throws Error with detailed message if validation fails
 */
export declare function validateThreadIdOrThrow(threadId: string): void;
/**
 * Determines if the connection is secure (HTTPS) by checking the request protocol
 * and x-forwarded-proto header (for reverse proxy scenarios).
 * Defaults to false (HTTP) if unable to determine.
 */
export declare function isSecureConnection(ctx: Context<Env>): boolean;
/**
 * Signs a thread ID using HMAC SHA-256 and returns it in the format: threadId;signature
 * Format: thrd_abc123;base64signature
 */
export declare function signThreadId(threadId: string, secret: string): Promise<string>;
/**
 * Verifies a signed thread ID header and returns the thread ID if valid, or undefined if invalid.
 * Expected format: thrd_abc123;base64signature
 */
export declare function verifySignedThreadId(signedValue: string, secret: string): Promise<string | undefined>;
/**
 * DefaultThreadIDProvider will look for an HTTP header `x-thread-id` first,
 * then fall back to a signed cookie named `atid`, and use that as the thread id.
 * If not found, generate a new one. Validates incoming thread IDs against
 * runtime constraints. Uses AGENTUITY_SDK_KEY for signing, falls back to 'agentuity'.
 */
export declare class DefaultThreadIDProvider implements ThreadIDProvider {
    private getSecret;
    getThreadId(_appState: AppState, ctx: Context<Env>): Promise<string>;
}
type LazyStateStatus = 'idle' | 'pending-writes' | 'loaded';
type RestoreFn = () => Promise<{
    state: Map<string, unknown>;
    metadata: Record<string, unknown>;
}>;
export declare class LazyThreadState implements ThreadState {
    #private;
    constructor(restoreFn: RestoreFn);
    get loaded(): boolean;
    get dirty(): boolean;
    private ensureLoaded;
    private doLoad;
    get<T = unknown>(key: string): Promise<T | undefined>;
    set<T = unknown>(key: string, value: T): Promise<void>;
    has(key: string): Promise<boolean>;
    delete(key: string): Promise<void>;
    clear(): Promise<void>;
    entries<T = unknown>(): Promise<[string, T][]>;
    keys(): Promise<string[]>;
    values<T = unknown>(): Promise<T[]>;
    size(): Promise<number>;
    push<T = unknown>(key: string, value: T, maxRecords?: number): Promise<void>;
    /**
     * Get the current status for save logic
     * @internal
     */
    getStatus(): LazyStateStatus;
    /**
     * Get pending operations for merge command
     * @internal
     */
    getPendingOperations(): MergeOperation[];
    /**
     * Get serialized state for full save.
     * Ensures state is loaded before serializing.
     * @internal
     */
    getSerializedState(): Promise<Record<string, unknown>>;
}
export declare class DefaultThread implements Thread {
    #private;
    readonly id: string;
    readonly state: LazyThreadState;
    private provider;
    constructor(provider: ThreadProvider, id: string, restoreFn: RestoreFn, initialMetadata?: Record<string, unknown>);
    private ensureMetadataLoaded;
    private doLoadMetadata;
    getMetadata(): Promise<Record<string, unknown>>;
    setMetadata(metadata: Record<string, unknown>): Promise<void>;
    addEventListener(eventName: ThreadEventName, callback: ThreadEventCallback<any>): void;
    removeEventListener(eventName: ThreadEventName, callback: ThreadEventCallback<any>): void;
    fireEvent(eventName: ThreadEventName): Promise<void>;
    destroy(): Promise<void>;
    /**
     * Check if thread has any data (state or metadata)
     */
    empty(): Promise<boolean>;
    /**
     * Check if thread needs saving
     * @internal
     */
    needsSave(): boolean;
    /**
     * Get the save mode for this thread
     * @internal
     */
    getSaveMode(): 'none' | 'merge' | 'full';
    /**
     * Get pending operations for merge command
     * @internal
     */
    getPendingOperations(): MergeOperation[];
    /**
     * Get metadata for saving (returns null if not loaded/modified)
     * @internal
     */
    getMetadataForSave(): Record<string, unknown> | undefined;
    /**
     * Get serialized state for full save.
     * Ensures state is loaded before serializing.
     * @internal
     */
    getSerializedState(): Promise<string>;
}
export declare class DefaultSession implements Session {
    readonly id: string;
    readonly thread: Thread;
    readonly state: Map<string, unknown>;
    metadata: Record<string, unknown>;
    constructor(thread: Thread, id: string, metadata?: Record<string, unknown>);
    addEventListener(eventName: SessionEventName, callback: SessionEventCallback<any>): void;
    removeEventListener(eventName: SessionEventName, callback: SessionEventCallback<any>): void;
    fireEvent(eventName: SessionEventName): Promise<void>;
    /**
     * Serialize session state to JSON string for persistence.
     * Returns undefined if state is empty or exceeds 1MB limit.
     * @internal
     */
    serializeUserData(): string | undefined;
}
/**
 * WebSocket client for thread state persistence.
 *
 * **WARNING: This class is exported for testing purposes only and is subject to change
 * without notice. Do not use this class directly in production code.**
 *
 * @internal
 * @experimental
 */
/**
 * Configuration options for ThreadWebSocketClient
 */
export interface ThreadWebSocketClientOptions {
    /** Connection timeout in milliseconds (default: 10000) */
    connectionTimeoutMs?: number;
    /** Request timeout in milliseconds (default: 10000) */
    requestTimeoutMs?: number;
    /** Base delay for reconnection backoff in milliseconds (default: 1000) */
    reconnectBaseDelayMs?: number;
    /** Maximum delay for reconnection backoff in milliseconds (default: 30000) */
    reconnectMaxDelayMs?: number;
    /** Maximum number of reconnection attempts (default: 5) */
    maxReconnectAttempts?: number;
}
export declare class ThreadWebSocketClient {
    private ws;
    private authenticated;
    private pendingRequests;
    private reconnectAttempts;
    private maxReconnectAttempts;
    private apiKey;
    private wsUrl;
    private wsConnecting;
    private reconnectTimer;
    private isDisposed;
    private initialConnectResolve;
    private initialConnectReject;
    private connectionTimeoutMs;
    private requestTimeoutMs;
    private reconnectBaseDelayMs;
    private reconnectMaxDelayMs;
    constructor(apiKey: string, wsUrl: string, options?: ThreadWebSocketClientOptions);
    connect(): Promise<void>;
    restore(threadId: string): Promise<string | undefined>;
    save(threadId: string, userData: string, threadMetadata?: Record<string, unknown>): Promise<void>;
    delete(threadId: string): Promise<void>;
    merge(threadId: string, operations: MergeOperation[], metadata?: Record<string, unknown>): Promise<void>;
    cleanup(): void;
}
export declare class DefaultThreadProvider implements ThreadProvider {
    private appState;
    private wsClient;
    private wsConnecting;
    private threadIDProvider;
    initialize(appState: AppState): Promise<void>;
    setThreadIDProvider(provider: ThreadIDProvider): void;
    restore(ctx: Context<Env>): Promise<Thread>;
    save(thread: Thread): Promise<void>;
    destroy(thread: Thread): Promise<void>;
}
export declare class DefaultSessionProvider implements SessionProvider {
    private sessions;
    initialize(_appState: AppState): Promise<void>;
    restore(thread: Thread, sessionId: string): Promise<Session>;
    save(session: Session): Promise<void>;
}
export {};
//# sourceMappingURL=session.d.ts.map