import { BestAgentError } from "@best-agent/foundation";
import { relative, resolve } from "node:path";
import { z } from "zod";
import { toolInputSchema, typedToolInputSchema } from "../schema.js";
import { createShellCapabilityRequests } from "./shell.js";
import { isPlainObject, readStringField } from "./shared.js";
const PROCESS_SESSION_READ_MAX_WAIT_MS = 120_000;
const SESSION_ID_SCHEMA = z
    .string()
    .min(1)
    .describe("Process session id returned by process-session-start.");
const PROCESS_SESSION_COMMAND_INPUT_SCHEMA = z.strictObject({
    command: z.string().min(1).describe("Shell command string for the long-running process."),
    cwd: z.string().optional().describe("Optional workspace directory to run the process from."),
});
const SESSION_START_SCHEMA = typedToolInputSchema(PROCESS_SESSION_COMMAND_INPUT_SCHEMA);
const SESSION_READ_SCHEMA = toolInputSchema(z.strictObject({
    session_id: SESSION_ID_SCHEMA,
    wait_ms: z
        .int()
        .min(0)
        .max(PROCESS_SESSION_READ_MAX_WAIT_MS)
        .optional()
        .describe("Optional wait for new output or process exit."),
    max_bytes: z
        .int()
        .min(1)
        .max(64 * 1024)
        .optional()
        .describe("Max stdout/stderr tail bytes per stream."),
}));
const SESSION_WRITE_SCHEMA = toolInputSchema(z.strictObject({
    session_id: SESSION_ID_SCHEMA,
    input: z
        .string()
        .describe("Bytes to write to stdin. Include newline when pressing Enter is intended."),
}));
const SESSION_STOP_SCHEMA = toolInputSchema(z.strictObject({ session_id: SESSION_ID_SCHEMA }));
export function createProcessSessionTools() {
    return [
        createProcessSessionStartTool(),
        createProcessSessionReadTool(),
        createProcessSessionWriteTool(),
        createProcessSessionStopTool(),
    ];
}
function createProcessSessionStartTool() {
    return {
        approvalClass: "external-side-effect",
        concurrencyGroup: "process-session-start",
        description: "Start a long-running process session with persistent stdin/stdout/stderr. Use for REPLs, interactive CLIs, VM consoles, debuggers, services, benchmark runs, and test suites that need later input or bounded output polling.",
        effect: "execute",
        executionClass: "external-side-effect",
        expectedDurationClass: "long",
        heartbeatPolicy: "optional",
        idempotent: false,
        inputSchema: SESSION_START_SCHEMA,
        name: "process-session-start",
        replayPolicy: "never",
        source: "built-in",
        capabilities(input, context) {
            const effectiveCwd = resolveProcessSessionCwd(input.cwd, context.cwd);
            return createShellCapabilityRequests(resolveProcessSessionArgv(input), effectiveCwd, {
                shellCommand: true,
            });
        },
        async execute(input, context) {
            const effectiveCwd = resolveProcessSessionCwd(input.cwd, context.cwd);
            if (!context.sandbox.isDirectory(effectiveCwd)) {
                throw new BestAgentError("EXEC_INVALID", `The process-session-start cwd must be an existing directory inside the workspace: ${input.cwd ?? context.cwd}`);
            }
            const argv = resolveProcessSessionArgv(input);
            const snapshot = context.sandbox.startProcessSession(argv, effectiveCwd, formatProcessSessionCommand(input), { sandboxProfile: context.sandboxProfile });
            let initial = await context.sandbox.readProcessSession(snapshot.id, {
                ...(context.abortSignal ? { signal: context.abortSignal } : {}),
                waitMs: 150,
            });
            if (initial.status === "running" &&
                (initial.stdout.trim().length > 0 || initial.stderr.trim().length > 0)) {
                initial = await context.sandbox.readProcessSession(snapshot.id, {
                    ...(context.abortSignal ? { signal: context.abortSignal } : {}),
                    waitMs: 250,
                });
            }
            if (initial.status === "completed" && initial.exitCode !== 0) {
                const finalSnapshot = await context.sandbox.stopProcessSession(snapshot.id);
                const output = renderProcessSessionSnapshot(finalSnapshot);
                context.setResultMetadata?.({ outputManaged: true });
                throw new BestAgentError("EXEC_FAILED", output);
            }
            const output = renderProcessSessionSnapshot(initial);
            context.setResultMetadata?.({ outputManaged: true });
            return output;
        },
        formatInput(input) {
            return formatProcessSessionCommand(input);
        },
    };
}
function createProcessSessionReadTool() {
    return {
        approvalClass: "read-only",
        description: "Read bounded output from a running or completed process session.",
        effect: "read",
        executionClass: "read-only",
        idempotent: true,
        inputSchema: SESSION_READ_SCHEMA,
        name: "process-session-read",
        source: "built-in",
        capabilities() {
            return [];
        },
        async execute(input, context) {
            let output;
            try {
                const snapshot = await context.sandbox.readProcessSession(input.session_id, {
                    maxBytes: input.max_bytes,
                    ...(context.abortSignal ? { signal: context.abortSignal } : {}),
                    waitMs: input.wait_ms,
                    wakeOnOutput: false,
                });
                output = renderProcessSessionSnapshot(snapshot);
            }
            catch (error) {
                if (!isUnknownProcessSessionError(error, input.session_id)) {
                    throw error;
                }
                output = renderUnavailableProcessSession(input.session_id);
            }
            context.setResultMetadata?.({ outputManaged: true });
            return output;
        },
        formatInput(input) {
            return input.wait_ms ? `${input.session_id} (wait ${input.wait_ms}ms)` : input.session_id;
        },
        legacyAdapter: {
            adapt(input) {
                return normalizeProcessSessionReadInput(input);
            },
        },
    };
}
function createProcessSessionWriteTool() {
    return {
        approvalClass: "external-side-effect",
        concurrencyGroup: "process-session-io",
        description: "Write input to a running process session stdin.",
        effect: "process",
        executionClass: "external-side-effect",
        idempotent: false,
        inputSchema: SESSION_WRITE_SCHEMA,
        name: "process-session-write",
        replayPolicy: "never",
        source: "built-in",
        capabilities() {
            return [];
        },
        execute(input, context) {
            context.sandbox.writeProcessSession(input.session_id, input.input);
            return `Wrote ${Buffer.byteLength(input.input, "utf8")} bytes to process session ${input.session_id}.`;
        },
        formatInput(input) {
            return `${input.session_id} (${Buffer.byteLength(input.input, "utf8")} bytes)`;
        },
        summarize(input) {
            return `Tool process-session-write wrote ${Buffer.byteLength(input.input, "utf8")} bytes to process session ${input.session_id}.`;
        },
        legacyAdapter: {
            adapt(input) {
                return normalizeProcessSessionWriteInput(input);
            },
        },
    };
}
function createProcessSessionStopTool() {
    return {
        approvalClass: "external-side-effect",
        concurrencyGroup: "process-session-stop",
        description: "Stop a running process session and evict retained output.",
        effect: "process",
        executionClass: "external-side-effect",
        idempotent: false,
        inputSchema: SESSION_STOP_SCHEMA,
        name: "process-session-stop",
        replayPolicy: "never",
        source: "built-in",
        capabilities() {
            return [];
        },
        async execute(input, context) {
            const snapshot = await context.sandbox.stopProcessSession(input.session_id);
            const output = renderProcessSessionSnapshot(snapshot);
            context.setResultMetadata?.({ outputManaged: true });
            return output;
        },
        formatInput(input) {
            return input.session_id;
        },
        legacyAdapter: {
            adapt(input) {
                return normalizeSessionIdInput("process-session-stop", input);
            },
        },
    };
}
function normalizeProcessSessionReadInput(input) {
    const base = normalizeSessionIdInput("process-session-read", input);
    if (!isPlainObject(input)) {
        return base;
    }
    return {
        ...base,
        ...optionalField("wait_ms", normalizeOptionalIntField("process-session-read", "wait_ms", input, ["wait_ms", "waitMs"])),
        ...optionalField("max_bytes", normalizeOptionalIntField("process-session-read", "max_bytes", input, [
            "max_bytes",
            "maxBytes",
        ])),
    };
}
function normalizeProcessSessionWriteInput(input) {
    if (!isPlainObject(input)) {
        throw new BestAgentError("TOOL_INPUT_INVALID", "process-session-write requires an object input with session_id and input.");
    }
    const session_id = readStringField(input, ["session_id", "sessionId"]);
    const text = readStringField(input, ["input", "text"]);
    if (!session_id || text === undefined) {
        throw new BestAgentError("TOOL_INPUT_INVALID", "process-session-write requires session_id and input.");
    }
    return { session_id, input: text };
}
function normalizeSessionIdInput(toolName, input) {
    if (typeof input === "string") {
        const session_id = input.trim();
        if (!session_id) {
            throw new BestAgentError("TOOL_INPUT_INVALID", `${toolName} requires a non-empty session id.`);
        }
        return { session_id };
    }
    if (!isPlainObject(input)) {
        throw new BestAgentError("TOOL_INPUT_INVALID", `${toolName} requires an object input with session_id.`);
    }
    const session_id = readStringField(input, ["session_id", "sessionId"]);
    if (!session_id) {
        throw new BestAgentError("TOOL_INPUT_INVALID", `${toolName} requires session_id.`);
    }
    return { session_id };
}
function normalizeOptionalIntField(toolName, field, input, names) {
    for (const name of names) {
        if (input[name] !== undefined) {
            return normalizeOptionalInt(toolName, field, input[name]);
        }
    }
    return undefined;
}
function normalizeOptionalInt(toolName, field, value) {
    if (typeof value === "string" && /^\d+$/.test(value.trim())) {
        return Number(value.trim());
    }
    if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
        throw new BestAgentError("TOOL_INPUT_INVALID", `${toolName} ${field} must be a non-negative integer.`);
    }
    return value;
}
function resolveProcessSessionCwd(inputCwd, workspaceCwd) {
    if (!inputCwd) {
        return workspaceCwd;
    }
    const resolved = resolve(workspaceCwd, inputCwd);
    const rel = relative(workspaceCwd, resolved);
    if (rel.startsWith("..") || rel === ".." || resolve(rel) === rel) {
        throw new BestAgentError("EXEC_INVALID", `The process-session cwd must stay inside the workspace: ${JSON.stringify(inputCwd)}.`);
    }
    return resolved;
}
function optionalField(key, value) {
    return value === undefined ? {} : { [key]: value };
}
function resolveProcessSessionArgv(input) {
    return process.platform === "win32"
        ? [process.env.ComSpec ?? "cmd.exe", "/d", "/s", "/c", input.command]
        : ["/bin/sh", "-lc", input.command];
}
function formatProcessSessionCommand(input) {
    return input.command;
}
function renderProcessSessionSnapshot(snapshot) {
    const lines = [
        `session_id: ${snapshot.id}`,
        `status: ${snapshot.status}`,
        `cwd: ${snapshot.cwd}`,
        `command: ${snapshot.command}`,
        snapshot.status === "completed" ? `exit_code: ${snapshot.exitCode}` : undefined,
        snapshot.signalCode ? `signal_code: ${snapshot.signalCode}` : undefined,
        snapshot.stdout.trimEnd() ? `stdout:\n${snapshot.stdout.trimEnd()}` : undefined,
        snapshot.stderr.trimEnd() ? `stderr:\n${snapshot.stderr.trimEnd()}` : undefined,
    ];
    return lines.filter(Boolean).join("\n");
}
function isUnknownProcessSessionError(error, sessionId) {
    return (error instanceof BestAgentError &&
        error.code === "EXEC_INVALID" &&
        error.message === `Unknown process session: ${sessionId}`);
}
function renderUnavailableProcessSession(sessionId) {
    return [
        `session_id: ${sessionId}`,
        "status: unavailable",
        "reason: no live process session exists in this runtime",
        "The process may have completed, been stopped, or been terminated when the previous runtime/thread was closed. Do not retry this session id. If the user asked for progress, explain that the original process is no longer running and ask whether to start a new run.",
    ].join("\n");
}
//# sourceMappingURL=process-session-tools.js.map