/**
 * Artifact tools let the agent hand a renderable bundle to the web canvas.
 * The active transport decides whether that bundle updates the currently
 * scoped artifact entry or creates a new persisted artifact entry.
 */
import { createHash } from 'crypto';
import { Type as T } from 'typebox';
import { validateArtifactBundle } from '../../core/artifact-bundle.js';
import { ArtifactRenderExportUnavailableError, inspectArtifactRender, inspectionToModelPayload, } from '../../services/artifact-render-export.js';
import { wrapToolResult } from './graph-tools.js';
/** Stable content hash of an artifact's stored body STRING (not the parsed
 *  bundle), so `read_artifact` and the write-time guard hash the exact same
 *  serialization — no canonicalization, no false mismatches from key reordering. */
export function hashArtifactBody(body) {
    return createHash('sha256').update(body, 'utf8').digest('hex').slice(0, 16);
}
export function createArtifactTools(onArtifact, options = {}) {
    const inspectArtifact = options.inspectArtifact ?? inspectArtifactRender;
    return [
        {
            name: 'create_artifact',
            label: 'Create Artifact',
            description: 'Create or update a persisted sandboxed canvas artifact from HTML/CSS/JS text files. Use this for slide decks, documents, social graphics, prototypes, diagrams, and other visual outputs. When chat is scoped to an artifact entry, this updates that canvas. If the user references another EXISTING artifact entry, pass target.entityId and target.repo when known. To create a SEPARATE new artifact while the chat is scoped to one, pass createNew: true (optionally with target.repo to pick the destination folder) — the scoped canvas is left untouched. Otherwise this creates a new artifact entry. To EDIT an existing artifact, first call read_artifact to get the current source and its hash, modify the full bundle, and pass that hash as baseHash so a concurrent change is detected instead of silently overwritten.',
            parameters: T.Object({
                bundle: T.Object({}, {
                    description: 'ArtifactBundle object with schemaVersion, artifactId, title, kind, viewport, optional libraries (three, d3, motion), files, and entrypoint.',
                    additionalProperties: true,
                }),
                target: T.Optional(T.Object({
                    repo: T.Optional(T.String({ description: 'Folder/repo containing the artifact entry to update.' })),
                    entityId: T.Optional(T.String({ description: 'Existing artifact entity id to update.' })),
                }, {
                    description: 'Optional existing artifact entry to update when the user references one outside the current canvas context.',
                })),
                baseHash: T.Optional(T.String({
                    description: 'The hash returned by read_artifact for the version you edited. Required when updating an existing artifact — the update is rejected if the artifact changed since that read.',
                })),
                replace: T.Optional(T.Boolean({
                    description: 'Set true to intentionally blank or wholesale-replace an existing artifact (bypasses the guard that blocks accidentally dropping its content).',
                })),
                createNew: T.Optional(T.Boolean({
                    description: 'Set true to create a NEW artifact entry even when the chat is scoped to an existing artifact canvas (which is otherwise updated in place). Cannot be combined with target.entityId or baseHash. Combine with target.repo to pick the destination folder.',
                })),
            }),
            execute: async (_toolCallId, params) => {
                const result = validateArtifactBundle(params.bundle);
                if (!result.ok || !result.bundle) {
                    return wrapToolResult({
                        success: false,
                        error: 'Invalid artifact bundle',
                        errors: result.errors,
                        byteSize: result.byteSize,
                    });
                }
                if (!onArtifact) {
                    return wrapToolResult({
                        success: false,
                        error: 'No artifact canvas is available in this context',
                    });
                }
                let location;
                try {
                    location = await onArtifact(result.bundle, params.target, { baseHash: params.baseHash, replace: params.replace, createNew: params.createNew });
                }
                catch (err) {
                    // Surface stale-write / destructive-shrink rejections as a tool error
                    // the agent can recover from (re-read, retry) rather than a thrown turn.
                    return wrapToolResult({
                        success: false,
                        error: err instanceof Error ? err.message : String(err),
                    });
                }
                if (!location) {
                    return wrapToolResult({
                        success: false,
                        error: 'No artifact canvas is available in this context',
                    });
                }
                return wrapToolResult({
                    success: true,
                    artifact: {
                        artifactId: result.bundle.artifactId,
                        title: result.bundle.title,
                        kind: result.bundle.kind,
                        viewport: result.bundle.viewport,
                    },
                    location,
                    message: `Created artifact: ${result.bundle.title}`,
                });
            },
        },
        {
            name: 'inspect_artifact',
            label: 'Inspect Artifact',
            description: 'Use Playwright to visually inspect an ArtifactBundle or a persisted artifact target. Returns a page screenshot, frame/page/slide metadata, optional individual frame screenshots, console messages, page errors, and failed requests. Use this after creating or revising artifacts to verify layout and individual slides/pages before responding.',
            parameters: T.Object({
                bundle: T.Optional(T.Object({}, {
                    description: 'ArtifactBundle object to inspect before saving. If omitted, pass target or rely on the currently scoped artifact canvas.',
                    additionalProperties: true,
                })),
                target: T.Optional(T.Object({
                    repo: T.Optional(T.String({ description: 'Folder/repo containing the persisted artifact entry.' })),
                    entityId: T.Optional(T.String({ description: 'Persisted artifact entity id to inspect.' })),
                }, {
                    description: 'Persisted artifact entry to inspect. If omitted while chat is scoped to an artifact, the current artifact is used.',
                })),
                context: T.Optional(T.Union([T.Literal('editor'), T.Literal('fullscreen')], {
                    description: '"editor" renders the canvas overview/grid; "fullscreen" renders the share/fullscreen view. Defaults to editor.',
                })),
                frameSelector: T.Optional(T.String({
                    description: 'CSS selector for individual frames/pages/slides. Defaults to display.controls.stepper.selector, then [data-artifact-frame], [data-artifact-page], .page.',
                })),
                frameIndexes: T.Optional(T.Array(T.Number(), {
                    description: 'Specific zero-based frame/page/slide indexes to screenshot.',
                })),
                maxFrames: T.Optional(T.Number({
                    description: 'Maximum number of individual frames/pages/slides to screenshot. Defaults to 6, max 24.',
                })),
                includePageScreenshot: T.Optional(T.Boolean({
                    description: 'Whether to include a screenshot of the whole rendered page. Defaults to true.',
                })),
                includeFrameScreenshots: T.Optional(T.Boolean({
                    description: 'Whether to include screenshots for selected frames/pages/slides. Defaults to true.',
                })),
                screenshotFormat: T.Optional(T.Union([T.Literal('jpeg'), T.Literal('png')], {
                    description: 'Screenshot encoding. Defaults to jpeg to keep tool results compact.',
                })),
            }),
            execute: async (_toolCallId, params) => {
                let bundle;
                if (params.bundle !== undefined) {
                    const result = validateArtifactBundle(params.bundle);
                    if (!result.ok || !result.bundle) {
                        return wrapToolResult({
                            success: false,
                            error: 'Invalid artifact bundle',
                            errors: result.errors,
                            byteSize: result.byteSize,
                        });
                    }
                    bundle = result.bundle;
                }
                else if (options.readArtifact) {
                    try {
                        bundle = (await options.readArtifact(params.target)).bundle;
                    }
                    catch (err) {
                        return wrapToolResult({
                            success: false,
                            error: err instanceof Error ? err.message : String(err),
                        });
                    }
                }
                else {
                    return wrapToolResult({
                        success: false,
                        error: 'No artifact bundle or readable artifact target was provided',
                    });
                }
                try {
                    const inspection = await inspectArtifact(bundle, {
                        context: params.context,
                        frameSelector: params.frameSelector,
                        frameIndexes: params.frameIndexes,
                        maxFrames: params.maxFrames,
                        includePageScreenshot: params.includePageScreenshot,
                        includeFrameScreenshots: params.includeFrameScreenshots,
                        screenshotFormat: params.screenshotFormat,
                    });
                    // Return screenshots as real IMAGE content blocks (vision) instead of
                    // base64 embedded in text: the model actually SEES the slides, for
                    // ~1.6k tokens/image instead of ~300k. The text block carries only the
                    // compact metadata (dims, rects, console, errors) — no base64. `details`
                    // mirrors that metadata for non-model consumers/logs (also base64-free).
                    const { metadata, images } = inspectionToModelPayload(inspection);
                    const result = { success: true, inspection: metadata };
                    return {
                        content: [
                            { type: 'text', text: JSON.stringify(result, null, 2) },
                            ...images.map((img) => ({ type: 'image', data: img.data, mimeType: img.mimeType })),
                        ],
                        details: result,
                    };
                }
                catch (err) {
                    if (err instanceof ArtifactRenderExportUnavailableError) {
                        return wrapToolResult({ success: false, error: err.message, code: 'PLAYWRIGHT_UNAVAILABLE' });
                    }
                    return wrapToolResult({
                        success: false,
                        error: err instanceof Error ? err.message : String(err),
                    });
                }
            },
        },
        {
            name: 'read_artifact',
            label: 'Read Artifact',
            description: 'Return the current source of a persisted artifact: its full ArtifactBundle plus a `hash` you MUST pass back to create_artifact as `baseHash` when saving an edit (so a concurrent change is detected rather than silently overwritten). When chat is scoped to an artifact, omit target to read the current canvas; otherwise pass target.repo and target.entityId. Call this before editing an existing artifact — its source is not kept in the conversation, so read it fresh each time you need to modify it.',
            parameters: T.Object({
                target: T.Optional(T.Object({
                    repo: T.Optional(T.String({ description: 'Folder/repo containing the persisted artifact entry.' })),
                    entityId: T.Optional(T.String({ description: 'Persisted artifact entity id to read.' })),
                }, {
                    description: 'Persisted artifact entry to read. If omitted while chat is scoped to an artifact, the current artifact is used.',
                })),
            }),
            execute: async (_toolCallId, params) => {
                if (!options.readArtifact) {
                    return wrapToolResult({ success: false, error: 'No readable artifact is available in this context' });
                }
                try {
                    const { bundle, hash, updatedAt } = await options.readArtifact(params.target);
                    return wrapToolResult({ success: true, hash, updatedAt, bundle });
                }
                catch (err) {
                    return wrapToolResult({ success: false, error: err instanceof Error ? err.message : String(err) });
                }
            },
        },
    ];
}
//# sourceMappingURL=artifact-tools.js.map