import { createHash } from 'crypto';
import { parse } from 'acorn';
import { knownSecretValuesFromEnv, redactSecrets, sanitizeForTerminal } from '../redact.js';
import { extractShellPrograms, isShellExecTool } from './shell-programs.js';
export const TOOL_INPUT_MAX_BYTES = 16 * 1024;
export const TOOL_SUCCESS_OUTPUT_MAX_BYTES = 1024;
export const TOOL_ERROR_OUTPUT_MAX_BYTES = 4 * 1024;
export const TOOL_SESSION_EVIDENCE_MAX_BYTES = 5 * 1024 * 1024;
export const TOOL_PENDING_MAX_BYTES = 1024 * 1024;
export const TOOL_PENDING_MAX_CALLS = 256;
export const TOOL_CHANGED_MAX_CALLS = 10_000;
export const TOOL_INDEX_LIMIT_ORDINAL = Number.MAX_SAFE_INTEGER;
export const TOOL_TEXT_PROCESSING_MAX_BYTES = 64 * 1024;
export const TOOL_SHELL_PARSE_MAX_BYTES = 64 * 1024;
// Bumped to 8 for the per-call end timestamp (PHNX-3437): the extractor now
// records when a call's result arrived, so a re-index re-derives it for rows
// stored by an older extractor.
export const TOOL_INDEX_VERSION = 8;
const BASE64_BLOCK = /(?:[A-Za-z0-9+/]{256,}={0,2})/g;
const SECRET_FIELD = /(?:token|secret|password|authorization|cookie|api[_-]?key|private[_-]?key)$/i;
const KNOWN_SECRET_VALUES = knownSecretValuesFromEnv();
function byteBound(value, maxBytes) {
    if (Buffer.byteLength(value, 'utf8') <= maxBytes)
        return value;
    const suffix = '\n[truncated]';
    const limit = Math.max(0, maxBytes - Buffer.byteLength(suffix));
    let end = Math.min(value.length, limit);
    while (end > 0 && Buffer.byteLength(value.slice(0, end), 'utf8') > limit)
        end--;
    return value.slice(0, end) + suffix;
}
function bytePrefix(value, maxBytes) {
    if (value.length <= maxBytes && Buffer.byteLength(value) <= maxBytes)
        return value;
    let end = Math.min(value.length, maxBytes);
    while (end > 0 && Buffer.byteLength(value.slice(0, end)) > maxBytes)
        end--;
    return value.slice(0, end);
}
/**
 * Bound regex/parser work while retaining enough look-ahead to redact a secret
 * that crosses the 64 KiB persistence boundary. An oversized configured secret
 * makes a large source wholly opaque rather than risking a partial disclosure.
 */
function processingWindow(value, knownValues) {
    if (value.length <= TOOL_TEXT_PROCESSING_MAX_BYTES
        && Buffer.byteLength(value) <= TOOL_TEXT_PROCESSING_MAX_BYTES)
        return value;
    const maxKnownBytes = knownValues.reduce((max, secret) => Math.max(max, Buffer.byteLength(secret)), 0);
    if (maxKnownBytes > TOOL_TEXT_PROCESSING_MAX_BYTES)
        return '[REDACTED oversized evidence]\n[truncated]';
    const boundaryLookahead = Math.max(4096, maxKnownBytes);
    return bytePrefix(value, TOOL_TEXT_PROCESSING_MAX_BYTES + boundaryLookahead);
}
export function sanitizeToolEvidenceText(value, maxBytes, knownValues = KNOWN_SECRET_VALUES) {
    const binaryMarked = processingWindow(value, knownValues)
        .replace(BASE64_BLOCK, (token) => `[base64:${token.length} chars]`);
    return byteBound(sanitizeForTerminal(redactSecrets(binaryMarked, knownValues)), maxBytes);
}
function safeIdentifier(value) {
    if (typeof value !== 'string' || value.length === 0)
        return undefined;
    if (value.length <= 512 && Buffer.byteLength(value) <= 512) {
        return sanitizeToolEvidenceText(value, 512);
    }
    const digest = createHash('sha256').update(value).digest('hex').slice(0, 16);
    return `${sanitizeToolEvidenceText(value, 480)}#${digest}`;
}
function boundedStableJson(value, maxBytes) {
    const suffix = '\n[truncated]';
    const capacity = Math.max(0, maxBytes - Buffer.byteLength(suffix));
    const seen = new WeakSet();
    let out = '';
    let bytes = 0;
    let truncated = false;
    const append = (text) => {
        if (truncated || text.length === 0)
            return;
        const available = capacity - bytes;
        if (available <= 0) {
            truncated = true;
            return;
        }
        const prefix = bytePrefix(text, available);
        out += prefix;
        bytes += Buffer.byteLength(prefix);
        if (prefix.length < text.length)
            truncated = true;
    };
    const write = (item, depth) => {
        if (truncated)
            return;
        if (depth > 64) {
            append(JSON.stringify('[depth limit]'));
            truncated = true;
            return;
        }
        if (typeof item === 'string') {
            const prefix = bytePrefix(item, Math.max(0, capacity - bytes));
            append(JSON.stringify(prefix));
            if (prefix.length < item.length)
                truncated = true;
            return;
        }
        if (item === null || typeof item === 'number' || typeof item === 'boolean') {
            append(JSON.stringify(item));
            return;
        }
        if (typeof item !== 'object') {
            append(JSON.stringify(String(item)));
            return;
        }
        if (seen.has(item)) {
            append(JSON.stringify('[circular]'));
            return;
        }
        seen.add(item);
        if (Array.isArray(item)) {
            append('[');
            for (let i = 0; i < item.length && !truncated; i++) {
                if (i > 0)
                    append(',');
                write(item[i], depth + 1);
            }
            append(']');
            return;
        }
        const record = item;
        const keys = [];
        let keyBytes = 0;
        let keysTruncated = false;
        for (const key in record) {
            if (!Object.prototype.hasOwnProperty.call(record, key))
                continue;
            keyBytes += Buffer.byteLength(key) + 4;
            if (keys.length >= 1024 || keyBytes > capacity - bytes) {
                keysTruncated = true;
                break;
            }
            keys.push(key);
        }
        keys.sort((a, b) => a.localeCompare(b));
        append('{');
        for (let i = 0; i < keys.length && !truncated; i++) {
            const key = keys[i];
            if (i > 0)
                append(',');
            append(JSON.stringify(key));
            append(':');
            write(SECRET_FIELD.test(key) ? '[REDACTED]' : record[key], depth + 1);
        }
        append('}');
        if (keysTruncated)
            truncated = true;
    };
    write(value, 0);
    return truncated ? bytePrefix(out, capacity) + suffix : out;
}
function canonicalInput(tool, args, command) {
    const shellCommand = command
        ?? (typeof args?.command === 'string' ? args.command : undefined)
        ?? (typeof args?.cmd === 'string' ? args.cmd : undefined);
    if (shellCommand)
        return sanitizeToolEvidenceText(shellCommand, TOOL_INPUT_MAX_BYTES);
    if (!args)
        return tool;
    let hasArgs = false;
    for (const key in args) {
        if (!Object.prototype.hasOwnProperty.call(args, key))
            continue;
        hasArgs = true;
        break;
    }
    if (!hasArgs)
        return tool;
    try {
        return sanitizeToolEvidenceText(boundedStableJson(args, TOOL_TEXT_PROCESSING_MAX_BYTES), TOOL_INPUT_MAX_BYTES);
    }
    catch {
        return sanitizeToolEvidenceText(String(args), TOOL_INPUT_MAX_BYTES);
    }
}
/** Extract literal shell commands from the Codex orchestration tool without evaluating transcript code. */
export function commandsFromCodexExec(source) {
    const commands = [];
    let root;
    try {
        root = parse(source, { ecmaVersion: 'latest', sourceType: 'module', allowAwaitOutsideFunction: true });
    }
    catch {
        return commands;
    }
    const visit = (value) => {
        if (!value || typeof value !== 'object')
            return;
        if (Array.isArray(value)) {
            value.forEach(visit);
            return;
        }
        const node = value;
        if (node.type === 'CallExpression'
            && node.callee?.type === 'MemberExpression'
            && node.callee.computed === false
            && node.callee.object?.type === 'Identifier'
            && node.callee.object.name === 'tools'
            && node.callee.property?.type === 'Identifier'
            && node.callee.property.name === 'exec_command'
            && node.arguments?.[0]?.type === 'ObjectExpression') {
            const property = node.arguments[0].properties.find((candidate) => candidate.type === 'Property'
                && candidate.kind === 'init'
                && ((candidate.key.type === 'Identifier' && candidate.key.name === 'cmd')
                    || (candidate.key.type === 'Literal' && candidate.key.value === 'cmd')));
            const commandValue = property?.value;
            if (commandValue?.type === 'Literal' && typeof commandValue.value === 'string') {
                commands.push(commandValue.value);
            }
            else if (commandValue?.type === 'TemplateLiteral' && commandValue.expressions.length === 0) {
                commands.push(commandValue.quasis[0]?.value.cooked ?? commandValue.quasis[0]?.value.raw ?? '');
            }
        }
        for (const child of Object.values(node))
            visit(child);
    };
    visit(root);
    return commands;
}
function commandFor(tool, args, command) {
    if (!isShellExecTool(tool))
        return undefined;
    const direct = command
        ?? (typeof args?.command === 'string' ? args.command : undefined)
        ?? (typeof args?.cmd === 'string' ? args.cmd : undefined);
    if (direct !== undefined)
        return direct;
    const input = typeof args?.input === 'string' ? args.input : undefined;
    if (!input || tool.toLowerCase() !== 'exec')
        return input;
    const boundedInput = bytePrefix(input, TOOL_SHELL_PARSE_MAX_BYTES);
    const commands = commandsFromCodexExec(boundedInput);
    if (commands.length > 0)
        return commands.join('\n');
    return /\btools\s*\./.test(input) ? undefined : input;
}
function resultObject(value) {
    return value && typeof value === 'object' && !Array.isArray(value)
        ? value
        : undefined;
}
function structuredField(source, name) {
    const metadata = resultObject(source?.metadata);
    return source?.[name] ?? metadata?.[name];
}
function structuredNumber(source, name) {
    const value = structuredField(source, name);
    return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
}
function structuredString(source, name) {
    const value = structuredField(source, name);
    return typeof value === 'string' ? value : undefined;
}
function resultText(value) {
    if (typeof value === 'string')
        return value;
    const object = resultObject(value);
    if (object) {
        for (const key of ['output', 'stdout', 'content', 'text', 'message']) {
            if (typeof object[key] === 'string')
                return object[key];
        }
        try {
            return boundedStableJson(object, TOOL_TEXT_PROCESSING_MAX_BYTES);
        }
        catch {
            return String(value);
        }
    }
    return value == null ? '' : String(value);
}
export function structuredToolResult(value) {
    const structured = resultObject(value);
    const success = structuredField(structured, 'success');
    const isError = success === false || structuredField(structured, 'is_error') === true;
    return {
        text: resultText(value),
        outcome: isError ? 'error' : success === true ? 'ok' : 'unknown',
        exitCode: structuredNumber(structured, 'exit_code'),
        statusCode: structuredNumber(structured, 'status_code'),
        errorCode: structuredString(structured, 'error_code'),
    };
}
function buildCall(ordinal, timestamp, tool, args, command, sourceCallId) {
    const rawShellCommand = commandFor(tool, args, command);
    const shellCommand = rawShellCommand
        ? sanitizeToolEvidenceText(rawShellCommand, TOOL_SHELL_PARSE_MAX_BYTES)
        : undefined;
    const shell = shellCommand
        ? extractShellPrograms(shellCommand)
        : { programs: [], occurrences: [], diagnostics: [] };
    return {
        ordinal,
        sourceCallId: safeIdentifier(sourceCallId),
        timestamp: sanitizeToolEvidenceText(timestamp, 128),
        tool: sanitizeToolEvidenceText(tool, 512),
        programs: shell.programs.map((program) => sanitizeToolEvidenceText(program, 512)),
        programOccurrences: shell.occurrences.map((occurrence) => ({
            program: sanitizeToolEvidenceText(occurrence.program, 512),
            role: occurrence.role,
        })),
        input: canonicalInput(tool, args, command),
        outcome: 'unknown',
        parseError: shell.diagnostics.length > 0 ? sanitizeToolEvidenceText(shell.diagnostics.join('; '), 1024) : undefined,
    };
}
export function toolCallEvidenceBytes(call) {
    return Buffer.byteLength([
        call.sourceCallId, call.timestamp, call.endTimestamp, call.tool, call.input, call.errorCode,
        call.output, call.error, call.parseError, ...call.programs,
        ...call.programOccurrences.map((occurrence) => `${occurrence.role}:${occurrence.program}`),
    ].filter((value) => typeof value === 'string').join('\0'));
}
function collectionLimitCall(timestamp) {
    return {
        ordinal: TOOL_INDEX_LIMIT_ORDINAL,
        timestamp,
        tool: 'index_limit',
        programs: [],
        programOccurrences: [],
        input: 'Tool evidence collection stopped at the 5 MiB or 10,000-call scan limit.',
        outcome: 'unknown',
        parseError: 'Additional tool calls were not retained during this scan.',
    };
}
const COLLECTION_LIMIT_RESERVE_BYTES = toolCallEvidenceBytes(collectionLimitCall('1970-01-01T00:00:00.000Z'));
export class ToolCallCollector {
    nextOrdinal;
    pendingById = new Map();
    pendingOrder = [];
    changed = new Map();
    changedSizes = new Map();
    pendingBytes = 0;
    changedBytes = 0;
    changedTruncated = false;
    constructor(snapshot) {
        this.nextOrdinal = snapshot?.nextOrdinal ?? 0;
        for (const call of snapshot?.pending ?? [])
            this.rememberPending({ ...call });
    }
    start(args) {
        const sourceCallId = safeIdentifier(args.sourceCallId);
        if (sourceCallId) {
            const existing = this.pendingById.get(sourceCallId);
            if (existing)
                return existing;
        }
        const timestamp = typeof args.timestamp === 'string' ? args.timestamp : '';
        const tool = typeof args.tool === 'string' && args.tool.length > 0 ? args.tool : 'unknown';
        const input = resultObject(args.input);
        const command = typeof args.command === 'string' ? args.command : undefined;
        const call = buildCall(this.nextOrdinal++, timestamp, tool, input, command, sourceCallId);
        if (this.markChanged(call))
            this.rememberPending(call);
        return call;
    }
    finish(args) {
        const tool = typeof args.tool === 'string' ? args.tool : undefined;
        const call = this.takePending(args.sourceCallId, tool);
        if (!call)
            return undefined;
        if (typeof args.timestamp === 'string' && args.timestamp.length > 0) {
            call.endTimestamp = sanitizeToolEvidenceText(args.timestamp, 128);
        }
        const outcome = args.outcome === 'ok' || args.outcome === 'error' || args.outcome === 'unknown'
            ? args.outcome
            : undefined;
        const error = typeof args.error === 'string' ? args.error : undefined;
        const output = typeof args.output === 'string' ? args.output : undefined;
        const success = typeof args.success === 'boolean' ? args.success : undefined;
        const isError = outcome === 'error' || success === false || error !== undefined;
        const raw = error ?? output ?? '';
        call.outcome = isError ? 'error' : outcome ?? (success === true ? 'ok' : 'unknown');
        call.exitCode = Number.isSafeInteger(args.exitCode) && args.exitCode >= 0 ? args.exitCode : undefined;
        call.statusCode = Number.isSafeInteger(args.statusCode) && args.statusCode >= 0 ? args.statusCode : undefined;
        call.errorCode = safeIdentifier(args.errorCode);
        if (isError || call.outcome === 'error') {
            call.error = sanitizeToolEvidenceText(raw, TOOL_ERROR_OUTPUT_MAX_BYTES);
            call.output = undefined;
        }
        else {
            call.output = sanitizeToolEvidenceText(raw, TOOL_SUCCESS_OUTPUT_MAX_BYTES);
            call.error = undefined;
        }
        this.markChanged(call);
        return call;
    }
    drainChanged() {
        const out = [...this.changed.values()];
        if (this.changedTruncated) {
            out.push(collectionLimitCall(new Date().toISOString()));
        }
        out.sort((a, b) => a.ordinal - b.ordinal);
        this.changed.clear();
        this.changedSizes.clear();
        this.changedBytes = 0;
        this.changedTruncated = false;
        return out;
    }
    snapshot() {
        return {
            v: 1,
            nextOrdinal: this.nextOrdinal,
            pending: this.pendingOrder.map((call) => ({ ...call })),
        };
    }
    recordIndexLimit() {
        this.changedTruncated = true;
    }
    rememberPending(call) {
        this.pendingOrder.push(call);
        this.pendingBytes += toolCallEvidenceBytes(call);
        if (call.sourceCallId)
            this.pendingById.set(call.sourceCallId, call);
        while (this.pendingOrder.length > TOOL_PENDING_MAX_CALLS || this.pendingBytes > TOOL_PENDING_MAX_BYTES) {
            const dropped = this.pendingOrder.shift();
            if (!dropped)
                break;
            this.pendingBytes -= toolCallEvidenceBytes(dropped);
            if (dropped.sourceCallId)
                this.pendingById.delete(dropped.sourceCallId);
            dropped.parseError = 'Pending result correlation dropped at the 1 MiB continuation limit.';
            this.markChanged(dropped);
        }
    }
    takePending(sourceCallId, tool) {
        const safeCallId = safeIdentifier(sourceCallId);
        const exact = safeCallId ? this.pendingById.get(safeCallId) : undefined;
        if (safeCallId) {
            if (!exact)
                return undefined;
            return this.removePending(exact);
        }
        const candidates = tool
            ? this.pendingOrder.filter((candidate) => candidate.tool === tool)
            : this.pendingOrder;
        // A missing harness id is safe only when one pending call can possibly own
        // the result. Do not attach concurrent same-tool output by arrival order.
        if (candidates.length !== 1)
            return undefined;
        return this.removePending(candidates[0]);
    }
    removePending(call) {
        const index = this.pendingOrder.indexOf(call);
        if (index >= 0)
            this.pendingOrder.splice(index, 1);
        this.pendingBytes -= toolCallEvidenceBytes(call);
        if (call.sourceCallId)
            this.pendingById.delete(call.sourceCallId);
        return call;
    }
    markChanged(call) {
        const previousBytes = this.changedSizes.get(call.ordinal) ?? 0;
        const nextBytes = toolCallEvidenceBytes(call);
        const nextTotal = this.changedBytes - previousBytes + nextBytes;
        const isNew = !this.changed.has(call.ordinal);
        if (nextTotal > TOOL_SESSION_EVIDENCE_MAX_BYTES - COLLECTION_LIMIT_RESERVE_BYTES
            || (isNew && this.changed.size >= TOOL_CHANGED_MAX_CALLS)) {
            if (!isNew) {
                this.changed.delete(call.ordinal);
                this.changedSizes.delete(call.ordinal);
                this.changedBytes -= previousBytes;
            }
            this.changedTruncated = true;
            return false;
        }
        this.changed.set(call.ordinal, call);
        this.changedSizes.set(call.ordinal, nextBytes);
        this.changedBytes = nextTotal;
        return true;
    }
}
/** Fold one normalized event into a collector (the full-file harness contract). */
function foldEventIntoCollector(collector, event) {
    if (event.type === 'tool_use') {
        collector.start({
            timestamp: event.timestamp,
            tool: event.tool || 'unknown',
            input: event.args,
            command: event.command,
            sourceCallId: event.callId,
        });
    }
    else if (event.type === 'tool_result') {
        collector.finish({
            timestamp: event.timestamp,
            sourceCallId: event.callId,
            tool: event.tool,
            success: event.success,
            outcome: event.outcome,
            exitCode: event.exitCode,
            statusCode: event.statusCode,
            errorCode: event.errorCode,
            output: event.output,
        });
    }
    else if (event.type === 'error' && event.tool) {
        const structuredError = event.outcome === 'error' || event.success === false;
        const evidence = event.content || event.output || 'Tool execution failed';
        collector.finish({
            timestamp: event.timestamp,
            sourceCallId: event.callId,
            tool: event.tool,
            success: structuredError ? false : undefined,
            outcome: event.outcome,
            exitCode: event.exitCode,
            statusCode: event.statusCode,
            errorCode: event.errorCode,
            error: structuredError ? evidence : undefined,
            output: structuredError ? undefined : evidence,
        });
    }
}
/**
 * Derive tool calls from a full-file harness's normalized events, optionally
 * RESUMING from a prior scan of the same append-only stream.
 *
 * Without `prior` this is a full parse from event 0 (identical to the legacy
 * {@link toolCallsFromEvents}). With `prior`, the collector is seeded from the
 * prior snapshot and only events at or after `prior.eventCount` are folded — so
 * an active session that grew by a few turns re-derives (and re-redacts) only
 * those new tool calls instead of re-sanitizing its entire history on every
 * daemon warm tick (PHNX-3411). The ordinals continue deterministically from the
 * snapshot, so folding [0..k) then [k..n) yields the same index as folding
 * [0..n) once, and the CHANGED set is safe to persist with `mode: 'append'`.
 *
 * The caller is responsible for only supplying `prior` when the stream is still
 * an append of what was scanned before (same source, un-truncated,
 * `prior.eventCount <= events.length`); anything else must full-scan.
 */
export function scanEventToolCalls(events, prior) {
    const startIndex = prior ? Math.min(prior.eventCount, events.length) : 0;
    const collector = new ToolCallCollector(prior?.snapshot);
    for (let i = startIndex; i < events.length; i++)
        foldEventIntoCollector(collector, events[i]);
    return { calls: collector.drainChanged(), snapshot: collector.snapshot(), eventCount: events.length };
}
/** Build indexed calls from the normalized parser contract used by full-file harnesses. */
export function toolCallsFromEvents(events) {
    return scanEventToolCalls(events).calls;
}
export function toolCallKey(sessionId, ordinal) {
    return createHash('sha256').update(`${sessionId}\0${ordinal}`).digest('hex').slice(0, 20);
}
/** Fold one raw Claude record into the resumable call collector. */
export function collectClaudeToolCalls(collector, parsed) {
    const timestamp = typeof parsed?.timestamp === 'string' ? parsed.timestamp : new Date().toISOString();
    if (parsed?.type === 'assistant') {
        for (const block of parsed.message?.content ?? []) {
            if (block?.type !== 'tool_use')
                continue;
            collector.start({
                timestamp,
                tool: typeof block.name === 'string' ? block.name : 'unknown',
                input: block.input && typeof block.input === 'object' ? block.input : {},
                command: typeof block.input?.command === 'string' ? block.input.command : undefined,
                sourceCallId: typeof block.id === 'string' ? block.id : undefined,
            });
        }
    }
    else if (parsed?.type === 'user') {
        for (const block of Array.isArray(parsed.message?.content) ? parsed.message.content : []) {
            if (block?.type !== 'tool_result')
                continue;
            const text = typeof block.content === 'string'
                ? block.content
                : Array.isArray(block.content)
                    ? block.content.filter((part) => part?.type === 'text').map((part) => part.text || '').join('\n')
                    : '';
            const structured = resultObject(block);
            collector.finish({
                timestamp,
                sourceCallId: typeof block.tool_use_id === 'string' ? block.tool_use_id : undefined,
                success: block.is_error !== true,
                output: block.is_error === true ? undefined : text,
                error: block.is_error === true ? text || 'Tool execution failed' : undefined,
                exitCode: structuredNumber(structured, 'exit_code'),
                statusCode: structuredNumber(structured, 'status_code'),
                errorCode: structuredString(structured, 'error_code'),
            });
        }
    }
}
/** Fold one raw Codex rollout record into the resumable call collector. */
export function collectCodexToolCalls(collector, parsed) {
    const timestamp = typeof parsed?.timestamp === 'string' ? parsed.timestamp : new Date().toISOString();
    const payload = parsed?.payload ?? {};
    if (parsed?.type === 'event_msg' && payload.type === 'web_search_end') {
        collector.start({ timestamp, tool: 'WebSearch', input: { query: String(payload.query || '') } });
        return;
    }
    if (parsed?.type !== 'response_item')
        return;
    if (payload.type === 'function_call') {
        let input = {};
        try {
            input = typeof payload.arguments === 'string' ? JSON.parse(payload.arguments) : (payload.arguments || {});
        }
        catch {
            input = { raw: String(payload.arguments || '') };
        }
        collector.start({
            timestamp,
            tool: String(payload.name || 'unknown'),
            input,
            command: typeof input.command === 'string' ? input.command : typeof input.cmd === 'string' ? input.cmd : undefined,
            sourceCallId: typeof (payload.call_id || payload.id) === 'string' ? (payload.call_id || payload.id) : undefined,
        });
    }
    else if (payload.type === 'custom_tool_call') {
        collector.start({
            timestamp,
            tool: String(payload.name || 'unknown'),
            input: { input: String(payload.input || '') },
            sourceCallId: typeof (payload.call_id || payload.id) === 'string' ? (payload.call_id || payload.id) : undefined,
        });
    }
    else if (payload.type === 'function_call_output' || payload.type === 'custom_tool_call_output') {
        const result = structuredToolResult(payload.output);
        const isError = result.outcome === 'error';
        collector.finish({
            timestamp,
            sourceCallId: typeof (payload.call_id || payload.id) === 'string' ? (payload.call_id || payload.id) : undefined,
            outcome: result.outcome,
            output: isError ? undefined : result.text,
            error: isError ? result.text || 'Tool execution failed' : undefined,
            exitCode: result.exitCode,
            statusCode: result.statusCode,
            errorCode: result.errorCode,
        });
    }
}
