import { createLogger } from "../../utils/logger/index.js";
import { init_logger } from "../../utils/logger.js";
import { extractProfileAgentId } from "../../config/agent-profile.js";
import { init_providers, resolveModel } from "../../providers/index.js";
import { applySubagentProgress } from "../workflow/agent-progress.js";
import { parseWorkflowScript } from "../workflow/parser.js";
import { getLastWorkflowMemory } from "../workflow/last-run-memory.js";
import { runWorkflow } from "../workflow/runtime.js";
import { previewValue, recomputeCounts, renderWorkflowText } from "../workflow/snapshot.js";
import { DelegateSubagentRunner } from "../workflow/subagent-runner.js";
import "../workflow/index.js";
import { resolveModelRef } from "../../config/agent-typed-models.js";
import { Type } from "@sinclair/typebox";
//#region src/agent/tools/workflow-tool.ts
/**
* `workflow` — the AgentTool the parent model calls to spawn a fan-out run.
*
* Shape mirrors `delegate-tool`: factory builds a closure over deps; `execute`
* parses the script, instantiates the {@link DelegateSubagentRunner}, drives the
* {@link runWorkflow} runtime, and pushes a live text snapshot through
* `onUpdate` for streaming UIs (TUI, gateway console).
*
* Why this lives in `src/agent/tools/` (not under `src/agent/workflow/`):
* the runtime is reusable infrastructure; the AgentTool wrapping is a
* presentation concern that depends on the AgentToolsFactory wiring. Keeping
* the wrapper here matches how `delegate-tool` and `execute-code-tool` are
* organised today.
*/
init_logger();
init_providers();
const log = createLogger("workflow-tool");
const DEFAULT_TIMEOUT_SEC = 1800;
const MAX_TIMEOUT_SEC = 14400;
const DEFAULT_MAX_CONCURRENCY = 16;
const DEFAULT_MAX_SUBAGENTS = 1e3;
const PUSH_UPDATE_THROTTLE_MS = 300;
const WorkflowToolSchema = Type.Object({
	name: Type.Optional(Type.String({ description: "Name of a saved workflow to run. Either `name` or `script` is required. Use `name` whenever the user references a known workflow (built-in or in ~/.xopc/workflows/)." })),
	script: Type.Optional(Type.String({ description: [
		"Raw JavaScript workflow script (no Markdown fences, no TypeScript syntax). Ignored when `name` is set.",
		"First statement: export const meta = { name: 'snake_case', description: 'short, human-readable' }.",
		"Use phase(title), agent(prompt, opts), parallel(arrayOfFunctions), pipeline(items, ...stages), log(message), args, and budget.",
		"The script must call agent() at least once.",
		"parallel() requires functions: await parallel(items.map(item => () => agent(...)))."
	].join(" ") })),
	args: Type.Optional(Type.Any({ description: "Optional JSON value exposed to the workflow script as the global `args`." }))
});
function createWorkflowTool(deps) {
	return {
		name: "workflow",
		label: "◆ Workflow",
		description: [
			"Run a deterministic JavaScript workflow that orchestrates multiple isolated subagents through agent(), parallel(), and pipeline().",
			"Two ways to invoke:",
			"  1. `name`: run a saved workflow from the catalog (built-in or ~/.xopc/workflows/). Prefer this when the user references a known name.",
			"  2. `script`: provide a raw JS workflow inline. Use when no saved workflow fits. Header is required: export const meta = { name, description }.",
			"Named-workflow triggers — call this tool with `{ name: \"<name>\" }` IMMEDIATELY when the user message is any of:",
			"  • a bare workflow name like \"/audit_repo\", \"/pr_review\", \"/research\", or \"audit_repo\"",
			"  • \"run the audit_repo workflow\", \"review this PR\", \"debug this error\", \"kick off research\", \"do a multi_perspective_review on X\" (extract args when natural: target, question, error, diff)",
			"  • after /workflows lists saved workflows and the user picks one",
			"Use phase(title) at runtime to mark progress groups. Each agent() returns a string, or a schema-validated object when opts.schema is set.",
			"Prefer for decomposable work: repo audits, PR review, incident triage, multi-perspective review, fan-out research, large refactors. Do not use for a single quick read/edit.",
			"parallel() takes thunks, not promises: parallel(items.map(item => () => agent(...))).",
			"pipeline(items, ...stages) interleaves items across stages — fastest path by default; only use parallel() when you genuinely need a cross-item barrier.",
			"Failed agent()/parallel()/pipeline() entries resolve to null; check before synthesizing.",
			"Do not use Date.now(), Math.random(), new Date(), require, import, fs, or network APIs — they are unavailable for determinism.",
			"Always end with a synthesis agent() that consolidates findings, especially when you fan out for review or research."
		].join("\n\n"),
		parameters: WorkflowToolSchema,
		async execute(_toolCallId, params, signal, onUpdate) {
			let script;
			let resolvedSource = "script";
			try {
				const resolved = resolveScript(params, deps.catalog);
				script = resolved.script;
				resolvedSource = resolved.source;
			} catch (e) {
				const message = e instanceof Error ? e.message : String(e);
				return {
					content: [{
						type: "text",
						text: `workflow: ${message}`
					}],
					details: { error: message }
				};
			}
			const wfCfg = deps.getConfig()?.agents?.defaults?.workflow;
			const concurrency = wfCfg?.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY;
			const maxSubagents = wfCfg?.maxSubagents ?? DEFAULT_MAX_SUBAGENTS;
			const timeoutSec = clampTimeout(wfCfg?.defaultTimeoutSec);
			let meta;
			try {
				meta = parseWorkflowScript(script).meta;
			} catch (e) {
				const message = e instanceof Error ? e.message : String(e);
				return {
					content: [{
						type: "text",
						text: resolvedSource === "name" ? `workflow "${params.name}" failed to parse: ${message}` : `workflow parse error: ${message}`
					}],
					details: { error: message }
				};
			}
			const snapshot = {
				name: meta.name,
				description: meta.description,
				phases: meta.phases?.map((p) => p.title) ?? [],
				logs: [],
				agents: [],
				agentCount: 0,
				runningCount: 0,
				doneCount: 0,
				errorCount: 0,
				skippedCount: 0
			};
			let lastUpdatePushedAtMs = Number.NEGATIVE_INFINITY;
			let liveUpdatesDisabled = false;
			const pushUpdate = (completed = false, immediate = false) => {
				if (liveUpdatesDisabled) return;
				recomputeCounts(snapshot);
				const nowMs = Date.now();
				if (!(completed || immediate || nowMs - lastUpdatePushedAtMs >= PUSH_UPDATE_THROTTLE_MS)) return;
				lastUpdatePushedAtMs = nowMs;
				try {
					onUpdate?.({
						content: [{
							type: "text",
							text: renderWorkflowText(snapshot, completed, { showResultPreviews: false })
						}],
						details: snapshot
					});
				} catch (e) {
					liveUpdatesDisabled = true;
					const message = e instanceof Error ? e.message : String(e);
					log.warn({
						err: e,
						errorMessage: message,
						workflow: meta.name
					}, `workflow live progress disabled: ${message}`);
				}
			};
			const subagentStream = wfCfg?.subagentStream ?? "steps";
			const runner = new DelegateSubagentRunner({
				workspace: deps.workspace,
				bus: deps.bus,
				getDefaultModel: deps.getSubagentModel,
				getConfig: deps.getConfig,
				toolExecutorConfig: deps.toolExecutorConfig,
				buildChildTools: deps.buildChildTools
			});
			const resolveModelId = (modelRef) => {
				const config = deps.getConfig();
				if (!config) throw new Error("workflow model resolution requires config");
				const sessionKey = deps.getCurrentSessionKey?.();
				return resolveModel(resolveModelRef(config, extractProfileAgentId(sessionKey, config), modelRef));
			};
			const controller = new AbortController();
			const onParentAbort = () => controller.abort();
			signal?.addEventListener("abort", onParentAbort, { once: true });
			const timeoutHandle = timeoutSec > 0 ? setTimeout(() => controller.abort(), timeoutSec * 1e3) : void 0;
			pushUpdate();
			try {
				const result = await runWorkflow(script, {
					runner,
					resolveModelId
				}, {
					cwd: deps.workspace,
					args: params.args,
					signal: controller.signal,
					concurrency,
					maxSubagents,
					onLog: (message) => {
						snapshot.logs.push(message);
						pushUpdate();
					},
					onPhase: (title) => {
						snapshot.currentPhase = title;
						if (!snapshot.phases.includes(title)) snapshot.phases.push(title);
						pushUpdate();
					},
					onAgentQueued: (event) => {
						snapshot.agents.push({
							id: event.id,
							label: event.label,
							phase: event.phase,
							prompt: event.prompt,
							status: "queued"
						});
						pushUpdate(false, true);
					},
					onAgentStart: (event) => {
						const agent = findAgentById(snapshot.agents, event.id);
						if (agent) {
							agent.status = "running";
							agent.startedAtMs = Date.now();
						} else snapshot.agents.push({
							id: event.id,
							label: event.label,
							phase: event.phase,
							prompt: event.prompt,
							status: "running",
							startedAtMs: Date.now()
						});
						pushUpdate(false, true);
					},
					onAgentEnd: (event) => {
						const agent = findAgentById(snapshot.agents, event.id);
						if (agent) {
							agent.status = event.status;
							agent.resultPreview = previewValue(event.result);
							if (agent.startedAtMs != null) agent.durationMs = Date.now() - agent.startedAtMs;
							agent.currentStep = void 0;
						}
						pushUpdate(false, true);
					},
					enhanceSubagentRun: subagentStream === "off" ? void 0 : ({ id }) => ({ onProgress: (event) => {
						const agent = findAgentById(snapshot.agents, id);
						if (!agent) return;
						if (applySubagentProgress(agent, event)) pushUpdate();
					} })
				});
				if (result.agentCount === 0) {
					const reason = "workflow scripts must call agent() at least once; this workflow declared phases but never ran a subagent.";
					snapshot.logs.push(reason);
					pushUpdate(true);
					return {
						content: [{
							type: "text",
							text: reason
						}],
						details: snapshot
					};
				}
				snapshot.result = result.result;
				snapshot.durationMs = result.durationMs;
				pushUpdate(true);
				try {
					getLastWorkflowMemory().record(deps.getCurrentSessionKey?.(), {
						script,
						metaName: result.meta.name,
						source: resolvedSource,
						recordedAt: Date.now()
					});
				} catch {}
				return {
					content: [{
						type: "text",
						text: `workflow ${result.meta.name} completed: ${result.agentCount} subagent(s), ${snapshot.errorCount} error(s).\n\nResult:\n${safeStringify(result.result)}`
					}],
					details: snapshot
				};
			} catch (e) {
				if (controller.signal.aborted) {
					for (const a of snapshot.agents) if (a.status === "running") {
						a.status = "skipped";
						a.error = "aborted";
					}
					pushUpdate(true);
					return {
						content: [{
							type: "text",
							text: signal?.aborted ? "workflow aborted" : `workflow timed out after ${timeoutSec}s`
						}],
						details: snapshot
					};
				}
				const message = e instanceof Error ? e.message : String(e);
				log.warn({
					err: e,
					errorMessage: message,
					workflow: meta.name
				}, `workflow failed: ${message}`);
				snapshot.logs.push(`workflow failed: ${message}`);
				pushUpdate(true);
				return {
					content: [{
						type: "text",
						text: `workflow failed: ${message}`
					}],
					details: snapshot
				};
			} finally {
				if (timeoutHandle) clearTimeout(timeoutHandle);
				signal?.removeEventListener("abort", onParentAbort);
			}
		}
	};
}
function normalizeScript(script) {
	let text = script.trim();
	const fence = text.match(/^```(?:js|javascript)?\s*\n([\s\S]*?)\n```$/i);
	if (fence) text = fence[1].trim();
	return text;
}
function resolveScript(params, catalog) {
	const name = params.name?.trim();
	if (name) return {
		script: catalog.load(name).script,
		source: "name"
	};
	if (!params.script || !params.script.trim()) throw new Error("either `name` or `script` is required.");
	return {
		script: normalizeScript(params.script),
		source: "script"
	};
}
function clampTimeout(requested) {
	const v = typeof requested === "number" && Number.isFinite(requested) ? requested : DEFAULT_TIMEOUT_SEC;
	if (v <= 0) return 0;
	return Math.min(MAX_TIMEOUT_SEC, Math.max(1, Math.floor(v)));
}
function findAgentById(agents, id) {
	for (let i = agents.length - 1; i >= 0; i--) if (agents[i].id === id) return agents[i];
}
function safeStringify(value) {
	try {
		return JSON.stringify(value, null, 2);
	} catch {
		return String(value);
	}
}
//#endregion
export { createWorkflowTool };

//# sourceMappingURL=workflow-tool.js.map