/* eslint-disable @typescript-eslint/no-explicit-any */
import { StructuredError, toCamelCase, } from '@agentuity/core';
import { context, SpanStatusCode, trace } from '@opentelemetry/api';
import { enrichContextWithTraceState } from './otel/tracestate';
import { validator } from 'hono/validator';
import { AGENT_RUNTIME, INTERNAL_AGENT, CURRENT_AGENT, AGENT_IDS } from './_config';
import { getAgentContext, inHTTPContext, getHTTPContext, setupRequestAgentContext, getAgentAsyncLocalStorage, } from './_context';
import { internal } from './logger/internal';
import { fireEvent } from './_events';
import { privateContext } from './_server';
import { generateId } from './session';
import { getEvalRunEventProvider } from './_services';
import * as runtimeConfig from './_config';
import { validateSchema, formatValidationIssues } from './_validation';
import { getAgentMetadataByName, getEvalMetadata } from './_metadata';
// Will be populated at runtime with strongly typed agents
const agents = new Map();
// WeakMap to store event listeners for each agent instance (truly private)
const agentEventListeners = new WeakMap();
// Map to store agent configs returned from setup (keyed by agent name)
const agentConfigs = new Map();
/**
 * Get the global runtime state (for production use).
 * In tests, use TestAgentContext which has isolated runtime state.
 */
export function getGlobalRuntimeState() {
    return {
        agents,
        agentConfigs,
        agentEventListeners,
    };
}
/**
 * Get the runtime state from an AgentContext.
 * @internal
 */
export function getAgentRuntime(ctx) {
    return ctx[AGENT_RUNTIME];
}
async function fireAgentEvent(runtime, agent, eventName, context, data) {
    // Fire agent-level listeners
    const listeners = runtime.agentEventListeners.get(agent);
    if (listeners) {
        const callbacks = listeners.get(eventName);
        if (callbacks && callbacks.size > 0) {
            for (const callback of callbacks) {
                try {
                    if (eventName === 'errored' && data) {
                        await callback(eventName, agent, context, data);
                    }
                    else if (eventName === 'started' || eventName === 'completed') {
                        await callback(eventName, agent, context);
                    }
                }
                catch (error) {
                    // Log but don't re-throw - event listener errors should not crash the server
                    internal.error(`Error in agent event listener for '${eventName}':`, error);
                }
            }
        }
    }
    // Fire global app-level events
    if (eventName === 'errored' && data) {
        await fireEvent('agent.errored', agent, context, data);
    }
    else if (eventName === 'started') {
        await fireEvent('agent.started', agent, context);
    }
    else if (eventName === 'completed') {
        await fireEvent('agent.completed', agent, context);
    }
}
export const registerAgent = (name, agent) => {
    agents.set(name, agent);
};
export const setAgentConfig = (name, config) => {
    agentConfigs.set(name, config);
};
export const getAgentConfig = (name) => {
    return agentConfigs.get(name);
};
const ValidationError = StructuredError('ValidationError')();
// Implementation
export function createAgent(name, config) {
    const inputSchema = config.schema?.input;
    const outputSchema = config.schema?.output;
    // Initialize evals array before handler so it can be captured in closure
    // Evals should only be added via agent.createEval() after agent creation
    const evalsArray = [];
    const handler = async (input) => {
        let validatedInput = undefined;
        if (inputSchema) {
            const inputResult = await inputSchema['~standard'].validate(input);
            if (inputResult.issues) {
                throw new ValidationError({
                    issues: inputResult.issues,
                    message: `Input validation failed: ${inputResult.issues.map((i) => i.message).join(', ')}`,
                });
            }
            validatedInput = inputResult.value;
        }
        const agentCtx = getAgentContext();
        // Store current agent for telemetry (using Symbol to keep it internal)
        agentCtx[CURRENT_AGENT] = agent;
        // Expose current agent metadata on the context
        agentCtx.current = agent.metadata;
        // Update ctx.config with this agent's setup() return value.
        // This ensures correct config when:
        // - Agent is called via user router (createAgentMiddleware(''))
        // - Agent A calls Agent B (agentB.run() inside handler)
        // - Agent is called via runInAgentContext() in tests
        const agentConfig = getAgentConfig(agent.metadata.name);
        if (agentConfig !== undefined) {
            agentCtx.config = agentConfig;
        }
        const attrs = {
            '@agentuity/agentId': agent.metadata.agentId, // stable ID (agent_*) - consistent across deployments
            '@agentuity/agentInstanceId': agent.metadata.id, // deployment-specific ID (agentid_*) - changes per deployment
            '@agentuity/agentDescription': agent.metadata.description,
            '@agentuity/agentName': agent.metadata.name,
            '@agentuity/threadId': agentCtx.thread.id,
        };
        // Set agent attributes on the current active span
        const activeSpan = trace.getActiveSpan();
        if (activeSpan) {
            activeSpan.setAttributes(attrs);
        }
        if (inHTTPContext()) {
            const honoCtx = privateContext(getHTTPContext());
            if (honoCtx.var.agentIds) {
                if (agent.metadata.id)
                    honoCtx.var.agentIds.add(agent.metadata.id);
                if (agent.metadata.agentId)
                    honoCtx.var.agentIds.add(agent.metadata.agentId);
            }
        }
        else {
            // For standalone contexts, check for AGENT_IDS symbol
            const agentIds = agentCtx[AGENT_IDS];
            if (agentIds) {
                if (agent.metadata.id)
                    agentIds.add(agent.metadata.id);
                if (agent.metadata.agentId)
                    agentIds.add(agent.metadata.agentId);
            }
        }
        agentCtx.logger = agentCtx.logger.child(attrs);
        // Get the agent instance from the runtime state to fire events
        const runtime = getAgentRuntime(agentCtx);
        // Fire 'started' event
        await fireAgentEvent(runtime, agent, 'started', agentCtx);
        try {
            // Execute the handler directly - span creation is handled by the caller (AgentRunner.run)
            // This avoids duplicate spans when agents call other agents
            const result = await (async () => {
                if (agent.metadata.id && !inHTTPContext()) {
                    // For standalone contexts, wrap with agent context to set aid in trace state
                    return runWithAgentContext(agent.metadata.id, () => inputSchema
                        ? config.handler(agentCtx, validatedInput)
                        : config.handler(agentCtx));
                }
                else {
                    // HTTP context or no agent ID - invoke handler directly
                    // Span is created by AgentRunner.run or createAgentRunner
                    return inputSchema
                        ? config.handler(agentCtx, validatedInput)
                        : config.handler(agentCtx);
                }
            })();
            let validatedOutput = result;
            // Skip output validation for streaming agents (they return ReadableStream)
            if (outputSchema && !config.schema?.stream) {
                const outputResult = await outputSchema['~standard'].validate(result);
                if (outputResult.issues) {
                    throw new ValidationError({
                        issues: outputResult.issues,
                        message: `Output validation failed: ${outputResult.issues.map((i) => i.message).join(', ')}`,
                    });
                }
                validatedOutput = outputResult.value;
            }
            // Store validated input/output in context state for event listeners
            agentCtx.state.set('_evalInput', validatedInput);
            agentCtx.state.set('_evalOutput', validatedOutput);
            // Fire 'completed' event - evals will run via event listener
            await fireAgentEvent(runtime, agent, 'completed', agentCtx);
            return validatedOutput;
        }
        catch (error) {
            // Fire 'errored' event
            await fireAgentEvent(runtime, agent, 'errored', agentCtx, error);
            throw error;
        }
    };
    // Create createEval method that infers types from agent and automatically adds to agent
    const createEval = ((evalNameOrConfig, evalConfig) => {
        // Handle preset eval config (single argument with name property)
        if (typeof evalNameOrConfig !== 'string' && 'name' in evalNameOrConfig) {
            const presetConfig = evalNameOrConfig;
            const evalName = presetConfig.name;
            internal.debug(`createEval called for agent "${name || 'unknown'}": registering preset eval "${evalName}"`);
            const evalType = {
                metadata: {
                    identifier: evalName,
                    name: evalName,
                    description: presetConfig.description || '',
                },
                handler: presetConfig.handler,
            };
            if (inputSchema) {
                evalType.inputSchema = inputSchema;
            }
            if (outputSchema) {
                evalType.outputSchema = outputSchema;
            }
            evalsArray.push(evalType);
            internal.debug(`Added preset eval "${evalName}" to agent "${name || 'unknown'}". Total evals: ${evalsArray.length}`);
            return evalType;
        }
        // Handle custom eval config (name + config)
        if (typeof evalNameOrConfig !== 'string' || !evalConfig) {
            throw new Error('Invalid arguments: expected (name: string, config) or (config: PresetEvalConfig)');
        }
        const evalName = evalNameOrConfig;
        // Trace log to verify evals file is imported
        internal.debug(`createEval called for agent "${name || 'unknown'}": registering eval "${evalName}"`);
        // Use build-time injected metadata if available (same pattern as agents)
        const evalMetadata = evalConfig.metadata || {};
        // Build eval metadata - merge injected metadata with defaults
        const evalType = {
            metadata: {
                // Use build-time injected metadata if available, otherwise fallback to empty/undefined
                id: evalMetadata.id || undefined,
                identifier: evalMetadata.identifier || undefined,
                version: evalMetadata.version || undefined,
                filename: evalMetadata.filename || '',
                name: evalName,
                description: evalConfig.description || '',
            },
            handler: evalConfig.handler,
        };
        if (inputSchema) {
            evalType.inputSchema = inputSchema;
        }
        if (outputSchema) {
            evalType.outputSchema = outputSchema;
        }
        // Automatically add eval to agent's evals array
        evalsArray.push(evalType);
        internal.debug(`Added eval "${evalName}" to agent "${name || 'unknown'}". Total evals: ${evalsArray.length}`);
        return evalType;
    });
    // Build metadata - merge user-provided metadata with defaults
    // The build plugin injects metadata via config.metadata during AST transformation
    let metadata = {
        // Defaults (used when running without build, e.g., dev mode)
        name,
        description: config.description,
        id: '',
        agentId: '',
        filename: '',
        version: '',
        inputSchemaCode: '',
        outputSchemaCode: '',
        // Merge in build-time injected metadata (overrides defaults)
        ...config.metadata,
    };
    // If id/agentId are empty, try to load from agentuity.metadata.json
    if (!metadata.id || !metadata.agentId) {
        const fileMetadata = getAgentMetadataByName(name);
        if (fileMetadata) {
            internal.info('[agent] loaded metadata for "%s" from file: id=%s, agentId=%s', name, fileMetadata.id, fileMetadata.agentId);
            metadata = {
                ...metadata,
                id: fileMetadata.id || metadata.id,
                agentId: fileMetadata.agentId || metadata.agentId,
                filename: fileMetadata.filename || metadata.filename,
                version: fileMetadata.version || metadata.version,
            };
        }
    }
    // Error if agent has no metadata IDs in production - this causes agent_ids to be empty in sessions
    // which affects analytics, billing attribution, and session filtering
    // Only enforce in production (when AGENTUITY_CLOUD_PROJECT_ID is set) to allow dev/test without metadata
    if (!metadata.id && !metadata.agentId && runtimeConfig.getProjectId()) {
        throw new Error(`Agent "${name}" has no metadata IDs (id and agentId are empty). ` +
            `This will result in empty agent_ids in session events. ` +
            `Ensure agentuity.metadata.json exists in the runtime directory ` +
            `(checked: ${process.cwd()}/agentuity.metadata.json and ${process.cwd()}/.agentuity/agentuity.metadata.json). ` +
            `Run 'agentuity build' to generate the metadata file.`);
    }
    const agent = {
        handler,
        metadata,
        evals: evalsArray,
        createEval,
        setup: config.setup,
        shutdown: config.shutdown,
    };
    // Add event listener methods
    agent.addEventListener = (eventName, callback) => {
        const agentForListeners = agent;
        const callbackForListeners = callback;
        let listeners = agentEventListeners.get(agentForListeners);
        if (!listeners) {
            listeners = new Map();
            agentEventListeners.set(agentForListeners, listeners);
        }
        let callbacks = listeners.get(eventName);
        if (!callbacks) {
            callbacks = new Set();
            listeners.set(eventName, callbacks);
        }
        callbacks.add(callbackForListeners);
    };
    // Automatically add event listener for 'completed' event to run evals
    agent.addEventListener('completed', async (_event, _agent, ctx) => {
        // Use the agent instance passed to event listener to access its evals array
        // This ensures we get evals that were added via agent.createEval() after agent creation
        const agentEvals = _agent?.evals || evalsArray;
        internal.debug(`Checking evals: agent=${_agent.metadata?.name}, evalsArray.length=${evalsArray?.length || 0}, agent.evals.length=${_agent?.evals?.length || 0}`);
        if (agentEvals && agentEvals.length > 0) {
            internal.info(`Executing ${agentEvals.length} evaluations after agent run`);
            // Get validated input/output from context state
            const validatedInput = ctx.state.get('_evalInput');
            const validatedOutput = ctx.state.get('_evalOutput');
            // Capture agentRunSpanId synchronously before waitUntil (which may run outside AsyncLocalStorage)
            let agentRunSpanId;
            try {
                const httpCtx = getHTTPContext();
                const _httpCtx = privateContext(httpCtx);
                agentRunSpanId = _httpCtx.var.agentRunSpanId;
            }
            catch {
                // HTTP context may not be available, spanId will be undefined
            }
            // Capture the agent span context so eval spans are parented to the agent
            const agentSpanContext = context.active();
            // Execute each eval using waitUntil to avoid blocking the response
            for (const evalItem of agentEvals) {
                const evalName = evalItem.metadata.name || 'unnamed';
                const agentName = _agent?.metadata?.name || name;
                const evalRunId = generateId('evalrun');
                // Look up eval metadata synchronously before async execution
                const evalMeta = getEvalMetadata(agentName, evalName);
                const evalId = evalMeta?.id || '';
                const evalIdentifier = evalMeta?.identifier || '';
                // Create eval span FIRST, parented to agent, then call waitUntil inside it
                // This makes waitUntil a child of the eval span
                const tracer = ctx.tracer;
                if (tracer) {
                    const evalSpan = tracer.startSpan(evalName, {}, agentSpanContext);
                    evalSpan.setAttributes({
                        '@agentuity/evalId': evalId,
                        '@agentuity/evalIdentifier': evalIdentifier,
                        '@agentuity/evalName': evalName,
                        '@agentuity/evalRunId': evalRunId,
                        '@agentuity/agentName': agentName,
                        '@agentuity/evalDescription': evalMeta?.description || evalItem.metadata.description || '',
                        '@agentuity/evalFilename': evalMeta?.filename || evalItem.metadata.filename || '',
                    });
                    const evalSpanContext = trace.setSpan(agentSpanContext, evalSpan);
                    // Run waitUntil INSIDE the eval span context - this makes waitUntil a child of eval
                    // Pass a function (not an already-executing promise) so waitUntil executes it
                    // AFTER setting up its span context, making operations children of waitUntil
                    context.with(evalSpanContext, () => {
                        ctx.waitUntil(async () => {
                            const orgId = runtimeConfig.getOrganizationId();
                            const projectId = runtimeConfig.getProjectId();
                            const devMode = runtimeConfig.isDevMode() ?? false;
                            const evalRunEventProvider = getEvalRunEventProvider();
                            const shouldSendEvalRunEvents = orgId && projectId && evalId !== '' && evalIdentifier !== '';
                            try {
                                internal.info(`[EVALRUN] Starting eval run tracking for '${evalName}'`);
                                // Send eval run start event
                                if (shouldSendEvalRunEvents && evalRunEventProvider) {
                                    try {
                                        const deploymentId = runtimeConfig.getDeploymentId();
                                        await evalRunEventProvider.start({
                                            id: evalRunId,
                                            sessionId: ctx.sessionId,
                                            evalId,
                                            evalIdentifier,
                                            orgId: orgId,
                                            projectId: projectId,
                                            devmode: Boolean(devMode),
                                            deploymentId: deploymentId || undefined,
                                            spanId: agentRunSpanId,
                                        });
                                    }
                                    catch (error) {
                                        internal.error(`[EVALRUN] Error sending start event for '${evalName}'`, { error });
                                    }
                                }
                                // Validate eval input/output if schemas exist
                                let evalValidatedInput = validatedInput;
                                let evalValidatedOutput = validatedOutput;
                                if (evalItem.inputSchema) {
                                    const result = await evalItem.inputSchema['~standard'].validate(validatedInput);
                                    if (result.issues) {
                                        throw new ValidationError({
                                            issues: result.issues,
                                            message: `Eval input validation failed`,
                                        });
                                    }
                                    evalValidatedInput = result.value;
                                }
                                if (evalItem.outputSchema) {
                                    const result = await evalItem.outputSchema['~standard'].validate(validatedOutput);
                                    if (result.issues) {
                                        throw new ValidationError({
                                            issues: result.issues,
                                            message: `Eval output validation failed`,
                                        });
                                    }
                                    evalValidatedOutput = result.value;
                                }
                                // Execute the eval handler
                                let handlerResult;
                                if (inputSchema && outputSchema) {
                                    handlerResult = await evalItem.handler(ctx, evalValidatedInput, evalValidatedOutput);
                                }
                                else if (inputSchema) {
                                    handlerResult = await evalItem.handler(ctx, evalValidatedInput);
                                }
                                else if (outputSchema) {
                                    handlerResult = await evalItem.handler(ctx, evalValidatedOutput);
                                }
                                else {
                                    handlerResult = await evalItem.handler(ctx);
                                }
                                const result = { success: true, ...handlerResult };
                                // Send eval run complete event
                                if (shouldSendEvalRunEvents && evalRunEventProvider) {
                                    try {
                                        await evalRunEventProvider.complete({ id: evalRunId, result });
                                    }
                                    catch (error) {
                                        internal.error(`[EVALRUN] Error sending complete event for '${evalName}'`, { error });
                                    }
                                }
                                internal.debug(`Eval '${evalName}' completed successfully`);
                            }
                            catch (error) {
                                const errorMessage = error instanceof Error ? error.message : String(error);
                                evalSpan.recordException(error);
                                evalSpan.setStatus({
                                    code: SpanStatusCode.ERROR,
                                    message: errorMessage,
                                });
                                internal.error(`Error executing eval '${evalName}'`, { error });
                                // Send error event
                                if (shouldSendEvalRunEvents && evalRunEventProvider) {
                                    try {
                                        await evalRunEventProvider.complete({
                                            id: evalRunId,
                                            error: errorMessage,
                                            result: {
                                                success: false,
                                                passed: false,
                                                error: errorMessage,
                                                metadata: {},
                                            },
                                        });
                                    }
                                    catch (e) {
                                        internal.debug('Failed to send eval run complete event', {
                                            evalRunId,
                                            errorMessage,
                                            error: e instanceof Error ? e.message : String(e),
                                        });
                                    }
                                }
                            }
                            finally {
                                evalSpan.end();
                            }
                        });
                    });
                }
                else {
                    // No tracer - execute without span
                    ctx.waitUntil(async () => {
                        const orgId = runtimeConfig.getOrganizationId();
                        const projectId = runtimeConfig.getProjectId();
                        const devMode = runtimeConfig.isDevMode() ?? false;
                        const evalRunEventProvider = getEvalRunEventProvider();
                        const shouldSendEvalRunEvents = orgId && projectId && evalId !== '' && evalIdentifier !== '';
                        try {
                            if (shouldSendEvalRunEvents && evalRunEventProvider) {
                                try {
                                    await evalRunEventProvider.start({
                                        id: evalRunId,
                                        sessionId: ctx.sessionId,
                                        evalId,
                                        evalIdentifier,
                                        orgId: orgId,
                                        projectId: projectId,
                                        devmode: Boolean(devMode),
                                        deploymentId: runtimeConfig.getDeploymentId() || undefined,
                                        spanId: agentRunSpanId,
                                    });
                                }
                                catch (e) {
                                    internal.debug('Failed to send eval run start event', {
                                        evalRunId,
                                        evalId,
                                        evalIdentifier,
                                        sessionId: ctx.sessionId,
                                        error: e instanceof Error ? e.message : String(e),
                                    });
                                }
                            }
                            let evalValidatedInput = validatedInput;
                            let evalValidatedOutput = validatedOutput;
                            if (evalItem.inputSchema) {
                                const result = await evalItem.inputSchema['~standard'].validate(validatedInput);
                                if (result.issues) {
                                    throw new ValidationError({
                                        issues: result.issues,
                                        message: `Eval input validation failed`,
                                    });
                                }
                                evalValidatedInput = result.value;
                            }
                            if (evalItem.outputSchema) {
                                const result = await evalItem.outputSchema['~standard'].validate(validatedOutput);
                                if (result.issues) {
                                    throw new ValidationError({
                                        issues: result.issues,
                                        message: `Eval output validation failed`,
                                    });
                                }
                                evalValidatedOutput = result.value;
                            }
                            let handlerResult;
                            if (inputSchema && outputSchema) {
                                handlerResult = await evalItem.handler(ctx, evalValidatedInput, evalValidatedOutput);
                            }
                            else if (inputSchema) {
                                handlerResult = await evalItem.handler(ctx, evalValidatedInput);
                            }
                            else if (outputSchema) {
                                handlerResult = await evalItem.handler(ctx, evalValidatedOutput);
                            }
                            else {
                                handlerResult = await evalItem.handler(ctx);
                            }
                            if (shouldSendEvalRunEvents && evalRunEventProvider) {
                                try {
                                    await evalRunEventProvider.complete({
                                        id: evalRunId,
                                        result: { success: true, ...handlerResult },
                                    });
                                }
                                catch (e) {
                                    internal.debug('Failed to send eval run complete event', {
                                        evalRunId,
                                        error: e instanceof Error ? e.message : String(e),
                                    });
                                }
                            }
                        }
                        catch (error) {
                            const errorMessage = error instanceof Error ? error.message : String(error);
                            internal.error(`Error executing eval '${evalName}'`, { error });
                            // Send error event to match traced branch behavior
                            if (shouldSendEvalRunEvents && evalRunEventProvider) {
                                try {
                                    await evalRunEventProvider.complete({
                                        id: evalRunId,
                                        error: errorMessage,
                                        result: {
                                            success: false,
                                            passed: false,
                                            error: errorMessage,
                                            metadata: {},
                                        },
                                    });
                                }
                                catch (e) {
                                    internal.debug('Failed to send eval run complete event', {
                                        evalRunId,
                                        errorMessage,
                                        error: e instanceof Error ? e.message : String(e),
                                    });
                                }
                            }
                        }
                    });
                }
            }
        }
    });
    agent.removeEventListener = (eventName, callback) => {
        const agentForListeners = agent;
        const callbackForListeners = callback;
        const listeners = agentEventListeners.get(agentForListeners);
        if (!listeners)
            return;
        const callbacks = listeners.get(eventName);
        if (!callbacks)
            return;
        callbacks.delete(callbackForListeners);
    };
    if (inputSchema) {
        agent.inputSchema = inputSchema;
    }
    if (outputSchema) {
        agent.outputSchema = outputSchema;
    }
    if (config.schema?.stream) {
        agent.stream = config.schema.stream;
    }
    // Add validator method with overloads
    agent.validator = ((override) => {
        const effectiveInputSchema = override?.input ?? inputSchema;
        // Only use agent's output schema if no override was provided at all.
        // If override is provided (even with just input), don't auto-apply agent's output schema
        // unless the override explicitly includes output.
        const effectiveOutputSchema = override ? override.output : outputSchema;
        // Helper to build the standard Hono input validator so types flow
        const buildInputValidator = (schema) => validator('json', async (value, c) => {
            if (schema) {
                const result = await validateSchema(schema, value);
                if (!result.success) {
                    return c.json({
                        error: 'Validation failed',
                        message: formatValidationIssues(result.issues),
                        issues: result.issues,
                    }, 400);
                }
                return result.data;
            }
            return value;
        });
        // If no output schema, preserve existing behavior: pure input validation
        if (!effectiveOutputSchema) {
            return buildInputValidator(effectiveInputSchema);
        }
        // Output validation middleware (runs after handler)
        const outputValidator = async (c, next) => {
            await next();
            const res = c.res;
            if (!res)
                return;
            // Skip output validation for streaming agents
            if (config.schema?.stream) {
                return;
            }
            // Only validate JSON responses
            const contentType = res.headers.get('Content-Type') ?? '';
            if (!contentType.toLowerCase().includes('application/json')) {
                return;
            }
            // Clone so we don't consume the body that will be sent
            let responseBody;
            try {
                const cloned = res.clone();
                responseBody = await cloned.json();
            }
            catch {
                const OutputValidationError = StructuredError('OutputValidationError')();
                throw new OutputValidationError({
                    message: 'Output validation failed: response is not valid JSON',
                    issues: [],
                });
            }
            const result = await validateSchema(effectiveOutputSchema, responseBody);
            if (!result.success) {
                const OutputValidationError = StructuredError('OutputValidationError')();
                throw new OutputValidationError({
                    message: `Output validation failed: ${formatValidationIssues(result.issues)}`,
                    issues: result.issues,
                });
            }
            // Replace response with validated/sanitized JSON
            c.res = new Response(JSON.stringify(result.data), {
                status: res.status,
                headers: res.headers,
            });
        };
        // If we have no input schema, we only do output validation
        if (!effectiveInputSchema) {
            return outputValidator;
        }
        // Compose: input validator → output validator
        const inputMiddleware = buildInputValidator(effectiveInputSchema);
        const composed = async (c, next) => {
            // Run the validator first; its next() runs the output validator,
            // whose next() runs the actual handler(s)
            const result = await inputMiddleware(c, async () => {
                await outputValidator(c, next);
            });
            // If inputMiddleware returned early (validation failed), return that response
            return result;
        };
        return composed;
    });
    // Register the agent for runtime use
    // @ts-expect-error - metadata might be incomplete until build plugin injects InternalAgentMetadata
    agents.set(name, agent);
    // Create and return AgentRunner
    const runner = {
        metadata: metadata,
        validator: agent.validator,
        inputSchema: inputSchema,
        outputSchema: outputSchema,
        stream: config.schema?.stream || false,
        evals: agent.evals,
        createEval,
        addEventListener: agent.addEventListener,
        removeEventListener: agent.removeEventListener,
        run: inputSchema
            ? async (input) => {
                // Wrap with span if in HTTP context with tracer
                if (inHTTPContext()) {
                    const honoCtx = getHTTPContext();
                    const tracer = honoCtx.var.tracer;
                    if (tracer) {
                        return runWithSpan(tracer, agent, honoCtx, async () => await agent.handler(input));
                    }
                }
                return await agent.handler(input);
            }
            : async () => {
                // Wrap with span if in HTTP context with tracer
                if (inHTTPContext()) {
                    const honoCtx = getHTTPContext();
                    const tracer = honoCtx.var.tracer;
                    if (tracer) {
                        return runWithSpan(tracer, agent, honoCtx, async () => await agent.handler());
                    }
                }
                return await agent.handler();
            },
        [INTERNAL_AGENT]: agent, // Store reference to internal agent for testing
    };
    return runner;
}
/**
 * Run a handler with the agent identifier set in trace state.
 * Used for non-HTTP contexts (standalone) where we still want to propagate
 * the agent ID to downstream API calls.
 */
const runWithAgentContext = async (agentId, handler) => {
    const currentContext = context.active();
    const activeSpan = trace.getSpan(currentContext);
    if (!activeSpan) {
        // No active span, just run the handler
        return handler();
    }
    // Enrich the context's traceState with the agent ID so it propagates
    // to downstream calls. Note: this does NOT affect the active recording
    // span's exported traceState (that was set at span creation). This only
    // affects propagation context for outbound requests.
    const contextWithAgentId = enrichContextWithTraceState(currentContext, {
        aid: agentId,
    });
    return context.with(contextWithAgentId, handler);
};
const runWithSpan = async (tracer, agent, ctx, handler) => {
    const currentContext = context.active();
    // Build enriched traceState BEFORE span creation so the recording span
    // inherits it and it gets exported to OTLP. This ensures the agent ID
    // and other metadata appear in ClickHouse TraceState column.
    const deploymentId = runtimeConfig.getDeploymentId();
    const projectId = runtimeConfig.getProjectId();
    const orgId = runtimeConfig.getOrganizationId();
    const isDevMode = runtimeConfig.isDevMode();
    const enrichedContext = enrichContextWithTraceState(currentContext, {
        aid: agent.metadata.id,
        did: deploymentId,
        pid: projectId,
        oid: orgId,
        d: isDevMode ? '1' : undefined,
    });
    const span = tracer.startSpan('agent.run', {}, enrichedContext);
    // Set agent attributes on the span immediately after creation
    span.setAttributes({
        '@agentuity/agentId': agent.metadata.agentId, // stable ID (agent_*) - consistent across deployments
        '@agentuity/agentInstanceId': agent.metadata.id, // deployment-specific ID (agentid_*) - changes per deployment
        '@agentuity/agentDescription': agent.metadata.description,
        '@agentuity/agentName': agent.metadata.name,
        '@agentuity/threadId': ctx.var.thread.id,
    });
    const spanId = span.spanContext().spanId;
    // Store span ID in PrivateVariables
    const _ctx = privateContext(ctx);
    _ctx.set('agentRunSpanId', spanId);
    try {
        // Create a new context with the span active.
        // The span already carries the enriched traceState (inherited from
        // enrichedContext), so downstream API calls via propagation.inject()
        // will see aid/pid/oid/did/d.
        const spanContext = trace.setSpan(currentContext, span);
        return await context.with(spanContext, handler);
    }
    catch (error) {
        span.recordException(error);
        span.setStatus({ code: SpanStatusCode.ERROR });
        throw error;
    }
    finally {
        span.end();
    }
};
const createAgentRunner = (agent, ctx) => {
    const tracer = ctx.var.tracer;
    if (agent.inputSchema) {
        return {
            metadata: agent.metadata,
            run: async (input) => {
                return runWithSpan(tracer, agent, ctx, async () => await agent.handler(input));
            },
        };
    }
    else {
        return {
            metadata: agent.metadata,
            run: async () => {
                return runWithSpan(tracer, agent, ctx, async () => await agent.handler());
            },
        };
    }
};
/**
 * Populate the agents object with all registered agents
 * Keys are converted to camelCase to match the generated TypeScript types
 */
export const populateAgentsRegistry = (ctx) => {
    const agentsObj = {};
    // Track ownership of camelCase keys to detect collisions between different raw names
    const ownershipMap = new Map();
    // Build flat registry of agents
    for (const [name, agentFn] of agents) {
        const runner = createAgentRunner(agentFn, ctx);
        const key = toCamelCase(name);
        // Validate key is non-empty
        if (!key) {
            internal.warn(`Agent name "${name}" converts to empty camelCase key. Skipping.`);
            continue;
        }
        // Detect collision on key - check ownership
        const existingOwner = ownershipMap.get(key);
        if (existingOwner && existingOwner !== name) {
            internal.error(`Agent registry collision: "${name}" conflicts with "${existingOwner}" (both map to camelCase key "${key}")`);
            throw new Error(`Agent registry collision detected for key "${key}"`);
        }
        agentsObj[key] = runner;
        // Record ownership
        ownershipMap.set(key, name);
    }
    return agentsObj;
};
export const createAgentMiddleware = (agentName) => {
    return async (ctx, next) => {
        // Populate agents object with strongly-typed keys
        const agentsObj = populateAgentsRegistry(ctx);
        // Track agent ID for session telemetry
        if (agentName) {
            const agentKey = toCamelCase(agentName);
            const agent = agentsObj[agentKey];
            const _ctx = privateContext(ctx);
            // we add both so that you can query by either
            if (agent?.metadata?.id) {
                _ctx.var.agentIds.add(agent.metadata.id);
            }
            if (agent?.metadata?.agentId) {
                _ctx.var.agentIds.add(agent.metadata.agentId);
            }
        }
        const sessionId = ctx.var.sessionId;
        const thread = ctx.var.thread;
        const session = ctx.var.session;
        const config = agentName ? getAgentConfig(agentName) : undefined;
        const app = ctx.var.app;
        const args = {
            agent: agentsObj,
            logger: ctx.var.logger,
            tracer: ctx.var.tracer,
            sessionId,
            session,
            thread,
            handler: ctx.var.waitUntilHandler,
            config: config || {},
            app: app || {},
            runtime: getGlobalRuntimeState(),
            auth: ctx.var.auth ?? null,
        };
        return setupRequestAgentContext(ctx, args, next);
    };
};
export const getAgents = () => agents;
export const runAgentSetups = async (appState) => {
    for (const [name, agent] of agents.entries()) {
        if (agent.setup) {
            const config = await agent.setup(appState);
            setAgentConfig(name, config);
        }
    }
    // Note: Server readiness is managed by Vite (dev) or Bun.serve (prod)
};
export const runAgentShutdowns = async (appState) => {
    const runtime = getGlobalRuntimeState();
    for (const [name, agent] of runtime.agents.entries()) {
        if (agent.shutdown) {
            const config = runtime.agentConfigs.get(name);
            await agent.shutdown(appState, config);
        }
    }
};
/**
 * Run an agent within a specific AgentContext.
 * Sets up AsyncLocalStorage with the provided context and executes the agent.
 *
 * This is the recommended way to test agents in unit tests. It automatically:
 * - Registers the agent in the runtime state so event listeners fire
 * - Sets up AsyncLocalStorage so getAgentContext() works inside the agent
 * - Handles both agents with input and agents without input
 *
 * **Use cases:**
 * - Unit testing agents with TestAgentContext
 * - Running agents outside HTTP request flow
 * - Custom agent execution environments
 * - Testing event listeners and evaluations
 *
 * @template TInput - Type of the input parameter
 * @template TOutput - Type of the return value
 *
 * @param ctx - The AgentContext to use (typically TestAgentContext in tests)
 * @param agent - The AgentRunner to execute (returned from createAgent)
 * @param input - Input data (required if agent has input schema, omit otherwise)
 *
 * @returns Promise resolving to the agent's output
 *
 * @example
 * ```typescript
 * import { runInAgentContext, TestAgentContext } from '@agentuity/runtime/test';
 *
 * test('greeting agent', async () => {
 *   const ctx = new TestAgentContext();
 *   const result = await runInAgentContext(ctx, greetingAgent, {
 *     name: 'Alice',
 *     age: 30
 *   });
 *   expect(result).toBe('Hello, Alice! You are 30 years old.');
 * });
 *
 * test('no-input agent', async () => {
 *   const ctx = new TestAgentContext();
 *   const result = await runInAgentContext(ctx, statusAgent);
 *   expect(result).toEqual({ status: 'ok' });
 * });
 * ```
 */
export async function runInAgentContext(ctx, agent, input) {
    const storage = getAgentAsyncLocalStorage();
    // Register agent in runtime state so events fire (lookup by metadata.name)
    const agentName = agent.metadata.name;
    const runtime = getAgentRuntime(ctx);
    // Get internal agent from runner (stored via symbol) or global registry
    const internalAgent = agent[INTERNAL_AGENT] || agents.get(agentName);
    if (internalAgent && agentName) {
        runtime.agents.set(agentName, internalAgent);
        // Copy event listeners from global to context runtime
        const globalListeners = agentEventListeners.get(internalAgent);
        if (globalListeners) {
            runtime.agentEventListeners.set(internalAgent, globalListeners);
        }
    }
    return storage.run(ctx, async () => {
        if (input !== undefined) {
            return await agent.run(input);
        }
        else {
            return await agent.run();
        }
    });
}
//# sourceMappingURL=agent.js.map