"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.OpenInferenceHandler = void 0;
const spanDeduplicator_1 = require("../../../utils/spanDeduplicator");
const toolCallAnalyzer_1 = require("../../../utils/toolCallAnalyzer");
const toolArgumentFields_1 = require("../../../utils/toolArgumentFields");
const utilts_1 = require("../../../utils/utilts");
const parseLlamaIndexCallToolIo_1 = require("../parseLlamaIndexCallToolIo");
/**
 * OpenInference / Phoenix-style LlamaIndex traces.
 * - Exact span names: AgentWorkflow.run, AgentWorkflow.call_tool, OpenAI.astream_chat, etc.
 * - Rich attributes: input.value, output.value, llm.model_name, openinference.span.kind.
 */
class OpenInferenceHandler {
    constructor() {
        this.kind = 'openinference';
        this.deduplicator = new spanDeduplicator_1.SpanDeduplicator();
        this.toolAnalyzer = new toolCallAnalyzer_1.ToolCallAnalyzer();
    }
    /**
     * Returns scoped spans (under AgentWorkflow.run) and unscoped spans (everything else).
     * When there are no workflow roots, scoped = all spans and unscoped = [].
     */
    getWorkflowScopedSpansSplit(spans) {
        const roots = spans.filter((s) => s.name === 'AgentWorkflow.run');
        if (roots.length === 0) {
            return { scoped: spans, unscoped: [] };
        }
        const parentOf = new Map();
        spans.forEach((s) => parentOf.set(s.id, s.parentSpanId ?? null));
        const isDescendantOf = (spanId, rootId) => {
            let id = spanId;
            while (id) {
                if (id === rootId) {
                    return true;
                }
                id = parentOf.get(id) ?? null;
            }
            return false;
        };
        const rootIds = new Set(roots.map((r) => r.id));
        const scoped = spans.filter((s) => rootIds.has(s.id) || rootIds.has(s.parentSpanId ?? '') || roots.some((r) => isDescendantOf(s.id, r.id)));
        const effectiveScoped = scoped.length > 0 ? scoped : spans;
        const scopedIds = new Set(effectiveScoped.map((s) => s.id));
        const unscoped = spans.filter((s) => !scopedIds.has(s.id));
        return { scoped: effectiveScoped, unscoped };
    }
    getWorkflowScopedSpans(spans) {
        const { scoped } = this.getWorkflowScopedSpansSplit(spans);
        return scoped;
    }
    getTools(spans) {
        const scope = this.getWorkflowScopedSpans(spans);
        this.deduplicator.reset();
        const tools = [];
        const seenTools = new Set();
        for (const span of scope) {
            if (this.deduplicator.isDuplicate(span)) {
                continue;
            }
            if (this.isToolSpan(span) && !seenTools.has(span.id)) {
                const tool = this.extractTool(span);
                if (tool) {
                    // Handoff is control-flow only; don't show as a tool node in the entity tree
                    if (tool.name === 'handoff' || tool.metadata?.isHandoff) {
                        seenTools.add(span.id);
                        continue;
                    }
                    tools.push(tool);
                    seenTools.add(span.id);
                }
            }
        }
        return tools;
    }
    getAgents(spans) {
        const scope = this.getWorkflowScopedSpans(spans);
        this.deduplicator.reset();
        const childMap = this.buildChildMap(scope);
        const toolIdToSpanId = this.buildToolIdToSpanIdMap(scope);
        const spanById = new Map(scope.map((s) => [s.id, s]));
        const byName = new Map();
        for (const span of scope) {
            if (this.deduplicator.isDuplicate(span)) {
                continue;
            }
            if (!this.isAgentSpan(span)) {
                continue;
            }
            const agent = this.extractAgent(span);
            if (!agent) {
                continue;
            }
            this.attachDescendantModelRollup(agent, span.id, scope, childMap);
            this.attachToolsCalledFromOutput(agent, span, toolIdToSpanId);
            let toolDefinitions = (0, toolArgumentFields_1.collectAgentToolDefinitions)(agent.id, scope, spanById, (s) => {
                if (s.name !== 'AgentWorkflow.call_tool') {
                    return false;
                }
                return !(0, parseLlamaIndexCallToolIo_1.parseLlamaIndexCallToolAttrs)((0, parseLlamaIndexCallToolIo_1.getMergedSpanAttrs)(s)).isHandoff;
            }, {
                resolveToolDisplayName: (sp, merged) => {
                    const p = (0, parseLlamaIndexCallToolIo_1.parseLlamaIndexCallToolAttrs)((0, parseLlamaIndexCallToolIo_1.getMergedSpanAttrs)(sp));
                    const n = String(p.toolName ?? merged.name ?? '').trim();
                    return n || merged.name;
                },
            });
            toolDefinitions = toolDefinitions.map((def) => {
                const sp = spanById.get(def.spanId);
                if (!sp) {
                    return def;
                }
                const parsed = (0, parseLlamaIndexCallToolIo_1.parseLlamaIndexCallToolAttrs)((0, parseLlamaIndexCallToolIo_1.getMergedSpanAttrs)(sp));
                const merged = (0, toolArgumentFields_1.mergedSpanAttributes)(sp);
                const argument = parsed.toolKwargs ?? def.argument;
                const output = parsed.output === undefined ? def.output : parsed.output;
                const argument_defination = (0, toolArgumentFields_1.getToolArgumentDefinitionFromAttrs)(merged) ??
                    (0, toolArgumentFields_1.inferArgumentDefinationFromInputValue)(argument) ??
                    def.argument_defination;
                const description = (0, toolArgumentFields_1.getToolDescriptionFromAttrs)(merged) ?? def.description;
                return {
                    ...def,
                    ...(argument !== undefined && { argument }),
                    ...(output !== undefined && { output }),
                    ...(description != null && { description }),
                    ...(argument_defination !== undefined && { argument_defination }),
                };
            });
            if (toolDefinitions.length === 0 && Array.isArray(agent.metadata?.invokedToolSpanIds)) {
                const fromInvoked = (agent.metadata?.invokedToolSpanIds)
                    .map((id) => spanById.get(id))
                    .filter((s) => s != null)
                    .filter((s) => s.name === 'AgentWorkflow.call_tool')
                    .filter((s) => !(0, parseLlamaIndexCallToolIo_1.parseLlamaIndexCallToolAttrs)((0, parseLlamaIndexCallToolIo_1.getMergedSpanAttrs)(s)).isHandoff)
                    .map((sp) => {
                    const parsed = (0, parseLlamaIndexCallToolIo_1.parseLlamaIndexCallToolAttrs)((0, parseLlamaIndexCallToolIo_1.getMergedSpanAttrs)(sp));
                    const merged = (0, toolArgumentFields_1.mergedSpanAttributes)(sp);
                    const name = String(parsed.toolName ?? this.toolAnalyzer.extractToolName(sp) ?? '').trim();
                    if (!name) {
                        return null;
                    }
                    const argument = parsed.toolKwargs ?? parsed.input;
                    const argument_defination = (0, toolArgumentFields_1.getToolArgumentDefinitionFromAttrs)(merged) ?? (0, toolArgumentFields_1.inferArgumentDefinationFromInputValue)(argument);
                    const description = (0, toolArgumentFields_1.getToolDescriptionFromAttrs)(merged);
                    return {
                        name,
                        spanId: sp.id,
                        ...(argument !== undefined && { argument }),
                        ...(parsed.output !== undefined && { output: parsed.output }),
                        ...(description != null && { description }),
                        ...(argument_defination !== undefined && { argument_defination }),
                    };
                })
                    .filter((d) => d != null);
                if (fromInvoked.length) {
                    toolDefinitions = fromInvoked;
                }
            }
            if (toolDefinitions.length) {
                agent.toolDefinitions = toolDefinitions;
            }
            const existing = byName.get(agent.name);
            if (existing) {
                this.mergeAgentInto(existing, agent);
            }
            else {
                byName.set(agent.name, agent);
            }
        }
        return Array.from(byName.values());
    }
    /** Merge a later agent step (same logical agent) into the first-seen one. */
    mergeAgentInto(existing, next) {
        var _b, _c, _d;
        if (next.ttft?.length) {
            existing.ttft = [...(existing.ttft ?? []), ...next.ttft];
        }
        existing.cumulativeDurationMs = (existing.cumulativeDurationMs ?? 0) + (next.cumulativeDurationMs ?? 0);
        const addTokens = (_a, b) => {
            if (!b) {
                return;
            }
            if (!existing.cumulativeTokenUsage) {
                existing.cumulativeTokenUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
            }
            const t = existing.cumulativeTokenUsage;
            t.inputTokens += b.inputTokens ?? 0;
            t.outputTokens += b.outputTokens ?? 0;
            t.totalTokens += b.totalTokens ?? ((b.inputTokens ?? 0) + (b.outputTokens ?? 0));
        };
        addTokens(existing.cumulativeTokenUsage, next.cumulativeTokenUsage);
        existing.metadata = existing.metadata ?? {};
        const nextMeta = next.metadata ?? {};
        if (nextMeta.invokedToolSpanIds?.length) {
            const seen = new Set(existing.metadata.invokedToolSpanIds ?? []);
            for (const id of nextMeta.invokedToolSpanIds) {
                if (!seen.has(id)) {
                    seen.add(id);
                    ((_b = existing.metadata).invokedToolSpanIds ?? (_b.invokedToolSpanIds = [])).push(id);
                }
            }
        }
        if (nextMeta.toolsCalled?.length) {
            const seen = new Set(existing.metadata.toolsCalled ?? []);
            for (const name of nextMeta.toolsCalled) {
                if (!seen.has(name)) {
                    seen.add(name);
                    ((_c = existing.metadata).toolsCalled ?? (_c.toolsCalled = [])).push(name);
                }
            }
        }
        if (nextMeta.toolCalls?.length) {
            existing.metadata.toolCalls = [...(existing.metadata.toolCalls ?? []), ...nextMeta.toolCalls];
        }
        if (next.toolDefinitions?.length) {
            const seen = new Set((existing.toolDefinitions ?? []).map((d) => d.spanId));
            for (const d of next.toolDefinitions) {
                if (!seen.has(d.spanId)) {
                    seen.add(d.spanId);
                    existing.toolDefinitions = [...(existing.toolDefinitions ?? []), d];
                }
            }
        }
        if (nextMeta.modelSpanIds?.length) {
            const seen = new Set(existing.metadata.modelSpanIds ?? []);
            for (const id of nextMeta.modelSpanIds) {
                if (!seen.has(id)) {
                    seen.add(id);
                    ((_d = existing.metadata).modelSpanIds ?? (_d.modelSpanIds = [])).push(id);
                }
            }
        }
    }
    getSpanAttrs(span) {
        return (span.attributes ?? {});
    }
    /** Trace duration is typically in nanoseconds; return ms for display. */
    durationMs(span) {
        const raw = Number(span.duration ?? 0);
        return raw >= 1e6 ? raw / 1e6 : raw;
    }
    /** Build parent -> list of child spans. */
    buildChildMap(spans) {
        const map = new Map();
        for (const s of spans) {
            const pid = s.parentSpanId ?? '';
            if (!pid) {
                continue;
            }
            if (!map.has(pid)) {
                map.set(pid, []);
            }
            map.get(pid).push(s);
        }
        return map;
    }
    /** Async-only OpenAI spans (prepare + astream). Safe to hide: standard traces have an OpenInference LLM span for the same call. */
    isAsyncOnlyModelSpan(span) {
        const n = span.name ?? '';
        return n === 'OpenAI._prepare_chat_with_tools' || n === 'OpenAI.astream_chat';
    }
    /** True when this agent step's only model descendants are _prepare_chat_with_tools and astream_chat (async-only). */
    isAsyncOnlyAgent(agentSpan, scope, childMap) {
        const descendantIds = this.getDescendantIds(agentSpan.id, childMap);
        const descendants = scope.filter((s) => descendantIds.has(s.id));
        const modelDescendants = descendants.filter((s) => this.isModelSpan(s));
        const toolDescendants = descendants.filter((s) => s.name === 'AgentWorkflow.call_tool' && !this.parseCallToolIo(s).isHandoff);
        if (toolDescendants.length > 0 || modelDescendants.length === 0) {
            return false;
        }
        return modelDescendants.every((s) => this.isAsyncOnlyModelSpan(s));
    }
    /** Collect all descendant span IDs of rootId (BFS). */
    getDescendantIds(rootId, childMap) {
        const out = new Set();
        const queue = [rootId];
        while (queue.length) {
            const id = queue.shift();
            const children = childMap.get(id) ?? [];
            for (const c of children) {
                if (!out.has(c.id)) {
                    out.add(c.id);
                    queue.push(c.id);
                }
            }
        }
        return out;
    }
    /** Get token/model rollup from descendant LLM spans (for DAG metadata or agent). */
    getDescendantModelRollup(agentSpanId, scope, childMap) {
        const descendantIds = this.getDescendantIds(agentSpanId, childMap);
        const modelSpans = scope.filter((s) => descendantIds.has(s.id) && this.isModelSpan(s));
        if (modelSpans.length === 0) {
            return null;
        }
        let inputTokens = 0;
        let outputTokens = 0;
        let totalTokens = 0;
        const modelNames = [];
        for (const s of modelSpans) {
            const m = this.extractModel(s);
            if (m) {
                inputTokens += m.tokenUsage.inputTokens ?? 0;
                outputTokens += m.tokenUsage.outputTokens ?? 0;
                totalTokens += m.tokenUsage.totalTokens ?? 0;
                if (m.modelName && m.modelName !== 'unknown') {
                    modelNames.push(m.modelName);
                }
            }
        }
        const cumulativeTokenUsage = {
            inputTokens,
            outputTokens,
            totalTokens: totalTokens || inputTokens + outputTokens
        };
        return {
            modelName: modelNames[0] ?? 'unknown',
            modelSpanIds: modelSpans.map((s) => s.id),
            cumulativeTokenUsage
        };
    }
    /** Attach token/model rollup from descendant LLM spans (OpenAI.astream_chat etc.) to this agent. */
    attachDescendantModelRollup(agent, agentSpanId, scope, childMap) {
        const descendantIds = this.getDescendantIds(agentSpanId, childMap);
        const modelSpans = scope.filter((s) => descendantIds.has(s.id) && this.isModelSpan(s));
        if (modelSpans.length === 0) {
            return;
        }
        let inputTokens = 0;
        let outputTokens = 0;
        let totalTokens = 0;
        const modelNames = [];
        const ttftList = [];
        let cumulativeDurationMs = 0;
        for (const s of modelSpans) {
            const m = this.extractModel(s);
            if (m) {
                inputTokens += m.tokenUsage.inputTokens ?? 0;
                outputTokens += m.tokenUsage.outputTokens ?? 0;
                totalTokens += m.tokenUsage.totalTokens ?? 0;
                if (m.modelName && m.modelName !== 'unknown') {
                    modelNames.push(m.modelName);
                }
                if (m.ttft != null) {
                    ttftList.push(m.ttft);
                }
                if (m.duration != null) {
                    cumulativeDurationMs += m.duration;
                }
            }
        }
        if (inputTokens || outputTokens || totalTokens) {
            agent.cumulativeTokenUsage = {
                inputTokens,
                outputTokens,
                totalTokens: totalTokens || inputTokens + outputTokens
            };
        }
        if (ttftList.length) {
            agent.ttft = ttftList;
        }
        if (cumulativeDurationMs) {
            agent.cumulativeDurationMs = cumulativeDurationMs;
        }
        if (modelNames.length) {
            agent.metadata = agent.metadata ?? {};
            agent.metadata.modelName = modelNames[0];
            agent.metadata.modelNames = [...new Set(modelNames)];
            agent.metadata.modelSpanIds = modelSpans.map((s) => s.id);
        }
    }
    /** Attach tools called in this agent step (from run_agent_step output.value tool_calls) and span ids. */
    attachToolsCalledFromOutput(agent, span, toolIdToSpanId) {
        if (span.name !== 'AgentWorkflow.run_agent_step') {
            return;
        }
        const toolCalls = this.parseRunAgentStepToolCalls(span);
        if (toolCalls.length === 0) {
            return;
        }
        agent.metadata = agent.metadata ?? {};
        agent.metadata.toolsCalled = toolCalls.map((tc) => tc.tool_name);
        const invokedToolSpanIds = toolCalls
            .map((tc) => toolIdToSpanId.get(tc.tool_id))
            .filter((id) => id != null);
        if (invokedToolSpanIds.length) {
            agent.metadata.invokedToolSpanIds = invokedToolSpanIds;
        }
        agent.metadata.toolCalls = toolCalls.map((tc) => ({
            tool_id: tc.tool_id,
            tool_name: tc.tool_name,
            tool_kwargs: tc.tool_kwargs
        }));
    }
    getModels(spans) {
        const { scoped } = this.getWorkflowScopedSpansSplit(spans);
        this.deduplicator.reset();
        const models = [];
        const seenModels = new Set();
        for (const span of scoped) {
            if (this.deduplicator.isDuplicate(span)) {
                continue;
            }
            if (this.isAsyncOnlyModelSpan(span)) {
                continue;
            }
            if (this.isModelSpan(span) && !seenModels.has(span.id)) {
                const model = this.extractModel(span);
                if (model) {
                    models.push(model);
                    seenModels.add(model.id);
                }
            }
        }
        return models;
    }
    /**
     * Returns models extracted from unscoped spans only.
     * Used only when scoped and unscoped sets differ (i.e. when there are workflow roots and spans outside them).
     */
    getModelsFromUnscoped(spans) {
        const { unscoped } = this.getWorkflowScopedSpansSplit(spans);
        if (unscoped.length === 0) {
            return [];
        }
        this.deduplicator.reset();
        const models = [];
        const seenModels = new Set();
        for (const span of unscoped) {
            if (this.deduplicator.isDuplicate(span)) {
                continue;
            }
            if (this.isAsyncOnlyModelSpan(span)) {
                continue;
            }
            if (this.isModelSpan(span) && !seenModels.has(span.id)) {
                const model = this.extractModel(span);
                if (model) {
                    models.push(model);
                    seenModels.add(model.id);
                }
            }
        }
        return models;
    }
    generateDag(spans) {
        const scope = this.getWorkflowScopedSpans(spans);
        this.deduplicator.reset();
        const uniqueSpans = scope.filter((s) => !this.deduplicator.isDuplicate(s));
        const childMap = this.buildChildMap(scope);
        const toolIdToSpanId = this.buildToolIdToSpanIdMap(scope);
        const parentOf = new Map(uniqueSpans.map((s) => [s.id, s.parentSpanId ?? null]));
        const nodeMap = new Map();
        const isHiddenSpan = (span) => span.name?.startsWith('FunctionTool.') ||
            (span.name === 'AgentWorkflow.call_tool' && this.parseCallToolIo(span).isHandoff === true) ||
            this.isAsyncOnlyModelSpan(span);
        const isNoisySpan = (span) => span.name === 'AgentWorkflow.init_run' ||
            span.name === 'AgentWorkflow.setup_agent' ||
            span.name === 'AgentWorkflow.parse_agent_output' ||
            span.name === 'AgentWorkflow.aggregate_tool_results';
        for (const span of uniqueSpans) {
            if (isHiddenSpan(span) || isNoisySpan(span)) {
                continue;
            }
            const type = this.getNodeType(span);
            const metadata = {
                timestamp: span.timestamp,
                duration: span.duration,
                parentSpanId: span.parentSpanId,
                attributes: this.getSpanAttrs(span)
            };
            if (type === 'agent') {
                const rollup = this.getDescendantModelRollup(span.id, scope, childMap);
                if (rollup) {
                    metadata.modelName = rollup.modelName;
                    metadata.modelSpanIds = rollup.modelSpanIds;
                    metadata.cumulativeTokenUsage = rollup.cumulativeTokenUsage;
                }
                const toolCalls = this.parseRunAgentStepToolCalls(span);
                if (toolCalls.length) {
                    metadata.toolsCalled = toolCalls.map((tc) => tc.tool_name);
                    const invokedToolSpanIds = toolCalls
                        .map((tc) => toolIdToSpanId.get(tc.tool_id))
                        .filter((id) => id != null);
                    if (invokedToolSpanIds.length) {
                        metadata.invokedToolSpanIds = invokedToolSpanIds;
                    }
                }
            }
            nodeMap.set(span.id, {
                id: span.id,
                type,
                name: span.name,
                children: [],
                metadata
            });
        }
        // Build children; reparent when parent is hidden or noisy (acall, handoff, init_run, setup_agent, etc.)
        for (const span of uniqueSpans) {
            if (!nodeMap.has(span.id)) {
                continue;
            }
            let pid = span.parentSpanId ?? null;
            while (pid && !nodeMap.has(pid)) {
                pid = parentOf.get(pid) ?? null;
            }
            if (pid) {
                nodeMap.get(pid).children.push(span.id);
            }
        }
        // Agent→tools hierarchy: reparent only tool nodes that exist in the DAG (exclude hidden handoff)
        const toolSpanIdsUnderAgents = new Set();
        for (const span of uniqueSpans) {
            if (this.getNodeType(span) !== 'agent') {
                continue;
            }
            const toolCalls = this.parseRunAgentStepToolCalls(span);
            const invokedIds = toolCalls
                .map((tc) => toolIdToSpanId.get(tc.tool_id))
                .filter((id) => id != null);
            for (const id of invokedIds) {
                toolSpanIdsUnderAgents.add(id);
            }
            const node = nodeMap.get(span.id);
            if (node && invokedIds.length) {
                node.children = [...node.children];
                for (const id of invokedIds) {
                    if (nodeMap.has(id) && !node.children.includes(id)) {
                        node.children.push(id);
                    }
                }
            }
        }
        // Run (root) should not list tool spans that are now under agents
        const rootSpan = uniqueSpans.find((s) => s.name === 'AgentWorkflow.run');
        const rootNode = rootSpan ? nodeMap.get(rootSpan.id) : null;
        if (rootNode && toolSpanIdsUnderAgents.size) {
            rootNode.children = rootNode.children.filter((id) => !toolSpanIdsUnderAgents.has(id));
        }
        // Prompt/response: wire from actual first and last entity in execution order (framework-specific, no manufactured edges)
        const isMeaningfulStep = (s) => {
            if (this.isAgentSpan(s)) {
                return true;
            }
            if (this.isModelSpan(s)) {
                return true;
            }
            if (s.name === 'AgentWorkflow.call_tool' && !this.parseCallToolIo(s).isHandoff) {
                return true;
            }
            return false;
        };
        const meaningfulByTime = uniqueSpans
            .filter(isMeaningfulStep)
            .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
        const firstSpanId = meaningfulByTime.length > 0 && nodeMap.has(meaningfulByTime[0].id) ? meaningfulByTime[0].id : undefined;
        const hasCompletion = (s) => {
            const attrs = this.getSpanAttrs(s);
            if (attrs['gen_ai.completion'] != null || attrs['genai.completion'] != null) {
                return true;
            }
            const events = s.events ?? [];
            if (events.some((e) => e.attributes?.['gen_ai.completion'] != null || e.attributes?.['genai.completion'] != null)) {
                return true;
            }
            return false;
        };
        const completionSpan = [...uniqueSpans].reverse().find(hasCompletion);
        const lastSpanId = completionSpan && nodeMap.has(completionSpan.id)
            ? completionSpan.id
            : meaningfulByTime.length > 0 && nodeMap.has(meaningfulByTime[meaningfulByTime.length - 1].id)
                ? meaningfulByTime[meaningfulByTime.length - 1].id
                : undefined;
        const outNodes = Array.from(nodeMap.values());
        if (firstSpanId != null) {
            outNodes.push({
                id: 'prompt',
                type: 'prompt',
                name: 'Prompt',
                children: [firstSpanId],
                metadata: {}
            });
        }
        if (lastSpanId != null) {
            const lastNode = nodeMap.get(lastSpanId);
            if (lastNode) {
                lastNode.children = [...lastNode.children, 'response'];
            }
            outNodes.push({
                id: 'response',
                type: 'response',
                name: 'Response',
                children: [],
                metadata: {}
            });
        }
        if (rootNode) {
            return [rootNode, ...outNodes.filter((n) => n.id !== rootNode.id)];
        }
        return outNodes;
    }
    generateLinearFlow(spans) {
        const scope = this.getWorkflowScopedSpans(spans);
        this.deduplicator.reset();
        const sorted = scope
            .filter((s) => !this.deduplicator.isDuplicate(s))
            .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
        const isMeaningfulStep = (span) => {
            if (this.isAgentSpan(span)) {
                return true;
            }
            if (this.isModelSpan(span)) {
                return true;
            }
            if (span.name === 'AgentWorkflow.call_tool' && !this.parseCallToolIo(span).isHandoff) {
                return true;
            }
            return false;
        };
        const meaningful = sorted.filter(isMeaningfulStep);
        const childMap = this.buildChildMap(scope);
        const toolIdToSpanId = this.buildToolIdToSpanIdMap(scope);
        const allowedTypes = new Set(['agent', 'tool', 'llm']);
        const candidates = meaningful
            .map((span) => ({ span, type: this.getNodeType(span) }))
            .filter(({ type }) => allowedTypes.has(type))
            .filter(({ span, type }) => {
            if (type === 'llm' && this.isAsyncOnlyModelSpan(span)) {
                return false;
            }
            return true;
        });
        const seenAgentNames = new Set();
        const steps = candidates.filter(({ span, type }) => {
            if (type !== 'agent') {
                return true;
            }
            const agentName = this.parseRunAgentStepAgentName(span) || span.name || '';
            const asyncOnly = this.isAsyncOnlyAgent(span, scope, childMap);
            if (asyncOnly && seenAgentNames.has(agentName)) {
                return false;
            }
            seenAgentNames.add(agentName);
            return true;
        });
        return steps.map(({ span, type }, index) => {
            const attrs = this.getSpanAttrs(span);
            const metadata = {
                parentSpanId: span.parentSpanId,
                operationName: utilts_1.FrameworkUtils.getAttr(attrs, 'gen_ai.operation.name', 'genai.operation.name'),
                workflowType: utilts_1.FrameworkUtils.getAttr(attrs, 'gen_ai.workflow.type', 'genai.workflow.type')
            };
            if (type === 'agent') {
                const rollup = this.getDescendantModelRollup(span.id, scope, childMap);
                if (rollup) {
                    metadata.modelName = rollup.modelName;
                    metadata.modelSpanIds = rollup.modelSpanIds;
                    metadata.cumulativeTokenUsage = rollup.cumulativeTokenUsage;
                }
                const toolCalls = this.parseRunAgentStepToolCalls(span);
                if (toolCalls.length) {
                    metadata.toolsCalled = toolCalls.map((tc) => tc.tool_name);
                    const invokedToolSpanIds = toolCalls
                        .map((tc) => toolIdToSpanId.get(tc.tool_id))
                        .filter((id) => id != null);
                    if (invokedToolSpanIds.length) {
                        metadata.invokedToolSpanIds = invokedToolSpanIds;
                    }
                }
            }
            const displayName = type === 'agent' ? (this.parseRunAgentStepAgentName(span) || span.name) :
                type === 'tool' ? (this.parseCallToolIo(span).toolName || span.name) :
                    type === 'llm' ? (utilts_1.FrameworkUtils.getAttr(attrs, 'llm.model_name', 'llm.model') ||
                        utilts_1.FrameworkUtils.getModelAttrResponseFirst(attrs)) :
                        span.name;
            return {
                stepNumber: index + 1,
                spanId: span.id,
                type,
                name: displayName,
                timestamp: span.timestamp,
                duration: span.duration,
                input: utilts_1.FrameworkUtils.getAttr(attrs, 'gen_ai.workflow.input', 'genai.workflow.input', 'gen_ai.prompt', 'genai.prompt', 'gen_ai.tool.input', 'genai.tool.input', 'input.value', 'input'),
                output: utilts_1.FrameworkUtils.getAttr(attrs, 'gen_ai.workflow.output', 'genai.workflow.output', 'gen_ai.completion', 'genai.completion', 'gen_ai.tool.output', 'genai.tool.output', 'output.value', 'output'),
                metadata
            };
        });
    }
    safeParseJson(val) {
        if (val == null) {
            return null;
        }
        if (typeof val === 'object') {
            return val;
        }
        if (typeof val !== 'string') {
            return null;
        }
        try {
            const parsed = JSON.parse(val);
            return typeof parsed === 'object' && parsed !== null ? parsed : null;
        }
        catch {
            return null;
        }
    }
    parseCallToolIo(span) {
        const attrs = this.getSpanAttrs(span);
        let inputVal = attrs['input.value'] ?? attrs['input'];
        const outputVal = attrs['output.value'] ?? attrs['output'];
        // New format: input.value can be JSON with "ev": "ToolCall(...)" or "ToolCallResult(...)"
        if (typeof inputVal === 'string') {
            const parsed = this.safeParseJson(inputVal);
            if (parsed && typeof parsed.ev === 'string') {
                inputVal = parsed.ev;
            }
        }
        const inputObj = this.safeParseJson(inputVal);
        const outputObj = this.safeParseJson(outputVal);
        let toolName;
        let toolKwargs;
        let isHandoff = false;
        const searchToolCall = (s) => {
            if (typeof s !== 'string' || (!s.includes('ToolCall(') && !s.includes('ToolCallResult('))) {
                return;
            }
            const nameMatch = s.match(/tool_name\s*=\s*['"]([^'"]+)['"]/);
            if (nameMatch) {
                toolName = nameMatch[1];
            }
            const kwargsMatch = s.match(/tool_kwargs\s*=\s*(\{[^}]+\})/);
            if (kwargsMatch) {
                try {
                    const kw = kwargsMatch[1].replace(/'/g, '"');
                    toolKwargs = JSON.parse(kw);
                }
                catch {
                    /**/
                }
                if (toolKwargs?.to_agent != null) {
                    isHandoff = true;
                }
            }
        };
        if (typeof inputVal === 'string') {
            searchToolCall(inputVal);
        }
        if (!toolName && typeof outputVal === 'string' && outputVal.includes('ToolCallResult(')) {
            searchToolCall(outputVal);
        }
        if (!toolName && outputObj && typeof outputObj.tool_name === 'string') {
            toolName = outputObj.tool_name;
            if (outputObj.tool_kwargs?.to_agent != null) {
                isHandoff = true;
            }
        }
        const output = outputObj?.tool_output
            ?? outputObj?.raw_output
            ?? outputObj
            ?? outputVal;
        return {
            toolName,
            toolKwargs,
            input: inputObj ?? inputVal,
            output,
            isHandoff
        };
    }
    parseRunAgentStepAgentName(span) {
        const attrs = this.getSpanAttrs(span);
        const outVal = attrs['output.value'] ?? attrs['output']
            ?? utilts_1.FrameworkUtils.getAttr(attrs, 'gen_ai.workflow.output', 'genai.workflow.output');
        const obj = this.safeParseJson(outVal);
        if (obj && typeof obj.current_agent_name === 'string') {
            return obj.current_agent_name;
        }
        const inVal = attrs['input.value'] ?? attrs['input'];
        if (typeof inVal === 'string') {
            const m = inVal.match(/current_agent_name\s*=\s*['"]([^'"]+)['"]/);
            if (m) {
                return m[1];
            }
        }
        return undefined;
    }
    /**
     * Parse run_agent_step output (stringified JSON) for tool_calls.
     * Tries: (1) top-level tool_calls, (2) response.additional_kwargs.tool_calls (id/function.name),
     * (3) regex on raw string for "tool_id"/"id" and "tool_name"/"name".
     */
    parseRunAgentStepToolCalls(span) {
        const attrs = this.getSpanAttrs(span);
        let outVal = attrs['output.value'] ?? attrs['output'];
        if (outVal == null) {
            return [];
        }
        // Handle double-stringified JSON
        if (typeof outVal === 'string') {
            const first = this.safeParseJson(outVal);
            if (typeof first === 'string') {
                outVal = this.safeParseJson(first) ?? first;
            }
            else {
                outVal = first;
            }
        }
        const obj = outVal && typeof outVal === 'object' ? outVal : null;
        // (1) Top-level tool_calls[] with tool_id, tool_name
        if (obj && Array.isArray(obj.tool_calls)) {
            const raw = obj.tool_calls;
            const out = raw
                .filter((tc) => typeof (tc.tool_id ?? tc.id) === 'string' && typeof (tc.tool_name ?? tc.name) === 'string')
                .map((tc) => ({
                tool_id: (tc.tool_id ?? tc.id),
                tool_name: (tc.tool_name ?? tc.name),
                tool_kwargs: tc.tool_kwargs
            }));
            if (out.length) {
                return out;
            }
        }
        // (2) response.additional_kwargs.tool_calls[] with id, function.name
        const response = obj && obj.response;
        const rawCalls = response?.additional_kwargs?.tool_calls;
        if (Array.isArray(rawCalls)) {
            const out = rawCalls
                .filter((tc) => typeof (tc.id ?? tc.function?.name) === 'string')
                .map((tc) => ({
                tool_id: (tc.id ?? ''),
                tool_name: (tc.function?.name ?? ''),
                tool_kwargs: typeof tc.function?.arguments === 'string' ? this.safeParseJson(tc.function.arguments) : undefined
            }))
                .filter((tc) => tc.tool_id && tc.tool_name);
            if (out.length) {
                return out;
            }
        }
        // (3) Regex fallback on the original string (stringified JSON)
        const rawStr = typeof attrs['output.value'] === 'string' ? attrs['output.value'] : typeof attrs['output'] === 'string' ? attrs['output'] : '';
        if (rawStr) {
            const toolIds = [...rawStr.matchAll(/"tool_id"\s*:\s*"([^"]+)"/g)].map((m) => m[1]);
            const idsFromFn = [...rawStr.matchAll(/"id"\s*:\s*"(call_[^"]+)"/g)].map((m) => m[1]);
            const ids = toolIds.length ? toolIds : idsFromFn;
            const names = [...rawStr.matchAll(/"tool_name"\s*:\s*"([^"]+)"/g)].map((m) => m[1]);
            const fnNames = [...rawStr.matchAll(/"name"\s*:\s*"([^"]+)"\s*,\s*"type"\s*:\s*"function"/g)].map((m) => m[1]);
            const nameList = names.length ? names : fnNames;
            if (ids.length && nameList.length) {
                const len = Math.min(ids.length, nameList.length);
                return ids.slice(0, len).map((tool_id, i) => ({
                    tool_id,
                    tool_name: nameList[i] ?? 'unknown',
                    tool_kwargs: undefined
                }));
            }
        }
        return [];
    }
    /** Map tool_id (from call_tool output.value) -> call_tool span id. */
    buildToolIdToSpanIdMap(scope) {
        const map = new Map();
        for (const span of scope) {
            if (span.name !== 'AgentWorkflow.call_tool') {
                continue;
            }
            const attrs = this.getSpanAttrs(span);
            const outputVal = attrs['output.value'] ?? attrs['output'];
            let toolId;
            const outputObj = this.safeParseJson(outputVal);
            if (outputObj && typeof outputObj.tool_id === 'string') {
                toolId = outputObj.tool_id;
            }
            if (!toolId && typeof outputVal === 'string' && (outputVal.includes('ToolCallResult(') || outputVal.includes('ToolCall('))) {
                const m = outputVal.match(/tool_id\s*=\s*['"]([^'"]+)['"]/);
                if (m) {
                    toolId = m[1];
                }
            }
            if (toolId) {
                map.set(toolId, span.id);
            }
        }
        return map;
    }
    isToolSpan(span) {
        if (span.name === 'AgentWorkflow.call_tool') {
            return true;
        }
        if (span.name === 'AgentWorkflow.aggregate_tool_results') {
            return false;
        }
        // FunctionTool.acall is the implementation span under call_tool; we only show the logical tool (call_tool)
        if (span.name?.startsWith('FunctionTool.')) {
            return false;
        }
        const attrs = this.getSpanAttrs(span);
        return ((span.name?.includes('tool') ?? false) ||
            utilts_1.FrameworkUtils.getAttr(attrs, 'gen_ai.tool.name', 'genai.tool.name') !== undefined);
    }
    /** LangChain/LangGraph-style routing and plumbing (LlamaIndex can use these when built on LangChain). */
    isInfrastructureWorkflow(span) {
        const attrs = this.getSpanAttrs(span);
        const wfType = String(utilts_1.FrameworkUtils.getAttr(attrs, 'gen_ai.workflow.type', 'genai.workflow.type') ?? '');
        const name = String(span.name ?? '');
        const infra = [
            'RunnableLambda',
            'RunnableSequence',
            'RunnableParallel',
            'ChatPromptTemplate',
            'StrOutputParser',
            'should_continue',
            'route_decision',
            'route_after_supervisor',
        ];
        return infra.some((p) => wfType.includes(p) || name.includes(p));
    }
    isAgentSpan(span) {
        if (this.isInfrastructureWorkflow(span)) {
            return false;
        }
        if (span.name === 'AgentWorkflow.run_agent_step') {
            return true;
        }
        if (span.name === 'AgentWorkflow.setup_agent' || span.name === 'AgentWorkflow.parse_agent_output') {
            return false;
        }
        const attrs = this.getSpanAttrs(span);
        return ((span.name?.includes('agent') ?? false) ||
            utilts_1.FrameworkUtils.getAttr(attrs, 'gen_ai.agent.name', 'genai.agent.name') !== undefined ||
            utilts_1.FrameworkUtils.getAttr(attrs, 'gen_ai.agent.role', 'genai.agent.role') !== undefined);
    }
    isModelSpan(span) {
        const attrs = this.getSpanAttrs(span);
        if (attrs['openinference.span.kind'] === 'LLM') {
            return true;
        }
        if (utilts_1.FrameworkUtils.getAttr(attrs, 'llm.model_name', 'llm.provider') != null) {
            return true;
        }
        if (/^OpenAI\.(astream_chat|acomplete|_prepare_chat)/.test(span.name ?? '')) {
            return true;
        }
        if (/^Anthropic\.|^Cohere\.|^Mistral\./.test(span.name ?? '')) {
            return true;
        }
        if (span.name.startsWith('chat ')) {
            return true;
        }
        if (span.name.includes('llm ')) {
            return true;
        }
        const opName = utilts_1.FrameworkUtils.getAttr(attrs, 'gen_ai.operation.name', 'genai.operation.name');
        if (opName === 'chat') {
            return true;
        }
        if (utilts_1.FrameworkUtils.getModelAttr(attrs, 'gen_ai.response.model', 'genai.response.model')) {
            return true;
        }
        if (utilts_1.FrameworkUtils.getModelAttr(attrs, 'gen_ai.request.model', 'genai.request.model')) {
            return true;
        }
        return false;
    }
    extractTool(span) {
        const parsed = (0, parseLlamaIndexCallToolIo_1.parseLlamaIndexCallToolAttrs)((0, parseLlamaIndexCallToolIo_1.getMergedSpanAttrs)(span));
        let toolName;
        let input;
        let output;
        let isHandoff = false;
        if (span.name === 'AgentWorkflow.call_tool') {
            toolName = parsed.toolName;
            input = parsed.input ?? parsed.toolKwargs;
            output = parsed.output;
            isHandoff = parsed.isHandoff ?? false;
        }
        const attrs = this.getSpanAttrs(span);
        if (!toolName) {
            toolName =
                this.toolAnalyzer.extractToolName(span) ||
                    utilts_1.FrameworkUtils.getAttr(attrs, 'gen_ai.tool.name', 'genai.tool.name');
        }
        if (!toolName) {
            return null;
        }
        if (input === undefined) {
            input = utilts_1.FrameworkUtils.getAttr(attrs, 'gen_ai.tool.input', 'genai.tool.input', 'gen_ai.workflow.input', 'genai.workflow.input');
        }
        if (output === undefined) {
            output = utilts_1.FrameworkUtils.getAttr(attrs, 'gen_ai.tool.output', 'genai.tool.output', 'gen_ai.workflow.output', 'genai.workflow.output');
        }
        const base = (0, utilts_1.buildCanonicalTool)({
            id: span.id,
            name: String(toolName),
            input,
            output,
            duration: this.durationMs(span),
            timestamp: span.timestamp,
            spanId: span.id,
            executionType: 'actual',
            metadata: isHandoff ? { isHandoff: true } : undefined,
        });
        return (0, parseLlamaIndexCallToolIo_1.enrichLlamaIndexToolFields)(base, span, parsed);
    }
    extractAgent(span) {
        const attrs = this.getSpanAttrs(span);
        const merged = (0, toolArgumentFields_1.mergedSpanAttributes)(span);
        const name = (span.name === 'AgentWorkflow.run_agent_step' ? this.parseRunAgentStepAgentName(span) : null) ||
            utilts_1.FrameworkUtils.getAttr(attrs, 'gen_ai.agent.name', 'genai.agent.name') ||
            span.name;
        const descriptionFromSetup = this.parseAgentDescriptionFromSetupInput(span);
        return (0, utilts_1.buildCanonicalAgent)({
            id: span.id,
            name: String(name),
            type: 'llamaindex',
            ...((0, toolArgumentFields_1.getAgentDescriptionFromAttrs)(merged) != null && {
                description: (0, toolArgumentFields_1.getAgentDescriptionFromAttrs)(merged),
            }),
            ...((0, toolArgumentFields_1.getAgentDescriptionFromAttrs)(merged) == null &&
                descriptionFromSetup != null && { description: descriptionFromSetup }),
            role: utilts_1.FrameworkUtils.getAttr(attrs, 'gen_ai.agent.role', 'genai.agent.role'),
            goal: utilts_1.FrameworkUtils.getAttr(attrs, 'gen_ai.agent.goal', 'genai.agent.goal'),
            metadata: { spanId: span.id, timestamp: span.timestamp, duration: span.duration, durationMs: this.durationMs(span) },
        });
    }
    /**
     * OpenInference run_agent_step often encodes agent instructions in input.value->ev(AgentSetup...).
     * Use the first "You are ..." sentence as fallback description when gen_ai.agent.description is absent.
     */
    parseAgentDescriptionFromSetupInput(span) {
        const attrs = this.getSpanAttrs(span);
        let inVal = attrs['input.value'] ?? attrs['input'];
        if (typeof inVal === 'string') {
            const parsed = this.safeParseJson(inVal);
            if (parsed && typeof parsed.ev === 'string') {
                inVal = parsed.ev;
            }
        }
        if (typeof inVal !== 'string') {
            return undefined;
        }
        const m = inVal.match(/text=\\?["'](You are[^"']{10,400})\\?["']/i);
        if (!m) {
            return undefined;
        }
        return m[1].trim();
    }
    extractModel(span) {
        const attrs = this.getSpanAttrs(span);
        const modelName = utilts_1.FrameworkUtils.getAttr(attrs, 'llm.model_name', 'llm.model') ||
            utilts_1.FrameworkUtils.getModelAttrResponseFirst(attrs);
        const provider = utilts_1.FrameworkUtils.getAttr(attrs, 'llm.provider', 'llm.system') ||
            utilts_1.FrameworkUtils.getAttr(attrs, 'gen_ai.system', 'genai.system');
        if (!modelName) {
            const match = span.name?.match(/^chat\s+(.+)$/i);
            if (!match) {
                return null;
            }
        }
        const finalModelName = modelName || span.name?.replace(/^chat\s+/i, '') || 'unknown';
        // OpenInference/LlamaIndex use llm.token_count.prompt | completion | total (often string); fallback to gen_ai.usage.* / llm.usage.*
        const inputTokens = utilts_1.FrameworkUtils.safeParseInt(utilts_1.FrameworkUtils.getAttr(attrs, 'llm.token_count.prompt', 'gen_ai.usage.input_tokens', 'genai.usage.inputtokens', 'llm.usage.input_tokens'));
        const outputTokens = utilts_1.FrameworkUtils.safeParseInt(utilts_1.FrameworkUtils.getAttr(attrs, 'llm.token_count.completion', 'gen_ai.usage.output_tokens', 'genai.usage.outputtokens', 'llm.usage.output_tokens'));
        let totalTokens = utilts_1.FrameworkUtils.safeParseInt(utilts_1.FrameworkUtils.getAttr(attrs, 'llm.token_count.total', 'gen_ai.usage.total_tokens', 'genai.usage.totaltokens', 'llm.usage.total_tokens'));
        if (!totalTokens && (inputTokens || outputTokens)) {
            totalTokens = inputTokens + outputTokens;
        }
        // Token details: OpenInference llm.token_count.prompt_details.cache_read, completion_details.reasoning
        const cacheRead = utilts_1.FrameworkUtils.safeParseInt(utilts_1.FrameworkUtils.getAttr(attrs, 'llm.token_count.prompt_details.cache_read', 'gen_ai.usage.cache_read_tokens', 'genai.usage.cache_read_tokens'));
        const cacheWrite = utilts_1.FrameworkUtils.safeParseInt(utilts_1.FrameworkUtils.getAttr(attrs, 'llm.token_count.completion_details.cache_write', 'gen_ai.usage.cache_write_tokens', 'genai.usage.cache_write_tokens'));
        const reasoningTokens = utilts_1.FrameworkUtils.safeParseInt(utilts_1.FrameworkUtils.getAttr(attrs, 'llm.token_count.completion_details.reasoning', 'gen_ai.usage.reasoning_tokens', 'genai.usage.reasoning_tokens'));
        const tokenUsage = {
            inputTokens,
            outputTokens,
            totalTokens: totalTokens || 0
        };
        if (cacheRead > 0) {
            tokenUsage.cacheRead = cacheRead;
        }
        if (cacheWrite > 0) {
            tokenUsage.cacheWrite = cacheWrite;
        }
        if (reasoningTokens > 0) {
            tokenUsage.reasoningTokens = reasoningTokens;
        }
        const prompt = utilts_1.FrameworkUtils.getAttr(attrs, 'gen_ai.prompt', 'genai.prompt') ??
            utilts_1.FrameworkUtils.getAttr(attrs, 'llm.input_messages.1.message.content', 'input.value');
        const completion = utilts_1.FrameworkUtils.getAttr(attrs, 'gen_ai.completion', 'genai.completion') ??
            utilts_1.FrameworkUtils.getAttr(attrs, 'llm.output_messages.0.message.contents.0.message_content.text', 'output.value');
        // TTFT: attribute first, else from first "First Token" event (eventsTimestamp/eventsName or span.events)
        let ttft = utilts_1.FrameworkUtils.safeParseFloat(attrs['gen_ai.server.time_to_first_token']);
        if (ttft == null) {
            const firstTokenMs = this.getTtftFromFirstTokenEvent(span);
            if (firstTokenMs != null) {
                ttft = firstTokenMs;
            }
        }
        return (0, utilts_1.buildCanonicalExtendedModel)({
            id: span.id,
            provider: provider ? String(provider) : 'openai',
            modelName: String(finalModelName),
            tokenUsage,
            spanId: span.id,
            timestamp: span.timestamp,
            temperature: utilts_1.FrameworkUtils.safeParseFloat(utilts_1.FrameworkUtils.getAttr(attrs, 'gen_ai.request.temperature', 'genai.request.temperature')),
            cost: utilts_1.FrameworkUtils.safeParseFloat(utilts_1.FrameworkUtils.getAttr(attrs, 'gen_ai.usage.cost', 'genai.usage.cost')) || 0,
            ttft: ttft ?? undefined,
            serverTimePerOutputToken: utilts_1.FrameworkUtils.safeParseFloat(attrs['gen_ai.server.time_per_output_token']),
            prompt,
            completion,
            duration: utilts_1.FrameworkUtils.safeParseFloat(span.duration ?? 0) ?? 0,
        });
    }
    /** TTFT from first event named like "First Token Stream Event" (eventsTimestamp/eventsName or span.events). */
    getTtftFromFirstTokenEvent(span) {
        const spanStart = span.timestamp ? new Date(span.timestamp).getTime() : Number.NaN;
        if (Number.isNaN(spanStart)) {
            return undefined;
        }
        const raw = span;
        if (Array.isArray(raw.eventsTimestamp) && Array.isArray(raw.eventsName)) {
            const idx = raw.eventsName.findIndex((n) => typeof n === 'string' && /first\s*token/i.test(n));
            if (idx >= 0 && raw.eventsTimestamp[idx]) {
                const eventMs = new Date(raw.eventsTimestamp[idx]).getTime();
                if (!Number.isNaN(eventMs)) {
                    return (eventMs - spanStart) / 1000;
                }
            }
        }
        if (span.events?.length) {
            const first = span.events.find((e) => e.name && /first\s*token/i.test(e.name));
            if (first?.timestamp) {
                const eventMs = new Date(first.timestamp).getTime();
                if (!Number.isNaN(eventMs)) {
                    return (eventMs - spanStart) / 1000;
                }
            }
        }
        return undefined;
    }
    getNodeType(span) {
        if (this.isModelSpan(span)) {
            return 'llm';
        }
        if (this.isToolSpan(span)) {
            if (span.name === 'AgentWorkflow.call_tool' && this.parseCallToolIo(span).isHandoff) {
                return 'workflow';
            }
            return 'tool';
        }
        if (this.isAgentSpan(span)) {
            return 'agent';
        }
        return 'workflow';
    }
}
exports.OpenInferenceHandler = OpenInferenceHandler;
//# sourceMappingURL=openInferenceHandler.js.map