import { parseWorkflowScript } from "./parser.js";
import { availableParallelism } from "node:os";
import { Script, createContext } from "node:vm";
//#region src/agent/workflow/runtime.ts
/**
* Sandboxed workflow runtime.
*
* Parses the script via {@link parseWorkflowScript} (which strips the `meta`
* export), wraps the remaining body in an async IIFE, and runs it inside a
* Node `vm` context with a curated set of globals:
*
*   - `agent(prompt, opts)` — spawns a subagent through the injected
*     {@link SubagentRunner} and returns its result (string, or schema-validated
*     object). Failures resolve to `null`.
*   - `parallel(thunks)` — concurrent fan-out; thunks (not promises!) so the
*     limiter sees each agent() call.
*   - `pipeline(items, ...stages)` — per-item sequential stages, items run
*     concurrently (no stage barrier). Each stage receives
*     `(prevResult, originalItem, index)`. A stage that throws drops that item
*     to `null` and skips remaining stages.
*   - `phase(title)` — marks the current phase; surfaces through `onPhase`.
*   - `log(message)` — appends to the workflow log.
*   - `budget` — `{ total, spent(), remaining() }` for self-pacing scripts.
*   - `args`, `cwd`, `process.cwd()`.
*
* The runtime is the only code that touches `vm`. It exposes no IO surface,
* carries no LLM dependency, and is fully driven by injected callbacks — that
* means the workflow tool, tests, and any future runner share one runtime.
*/
const DEFAULT_CONCURRENCY_FLOOR = 1;
const DEFAULT_CONCURRENCY_CEILING = 16;
const DEFAULT_MAX_SUBAGENTS = 1e3;
async function runWorkflow(script, deps, options) {
	const started = Date.now();
	const { meta, body } = parseWorkflowScript(script);
	const state = {
		logs: [],
		phases: [],
		agentCount: 0,
		spent: 0
	};
	const concurrency = clampConcurrency(options.concurrency);
	const maxSubagents = Math.max(1, options.maxSubagents ?? DEFAULT_MAX_SUBAGENTS);
	const limiter = createLimiter(concurrency);
	const pendingAgentRuns = /* @__PURE__ */ new Set();
	const phaseDefaultModels = buildPhaseModelMap(meta.phases);
	const log = (message) => {
		const text = String(message);
		state.logs.push(text);
		options.onLog?.(text);
	};
	const phase = (title) => {
		const text = requireString(title, "phase title");
		state.currentPhase = text;
		if (!state.phases.includes(text)) state.phases.push(text);
		options.onPhase?.(text);
	};
	const budget = Object.freeze({
		total: options.tokenBudget ?? null,
		spent: () => state.spent,
		remaining: () => options.tokenBudget == null ? Number.POSITIVE_INFINITY : Math.max(0, options.tokenBudget - state.spent)
	});
	const throwIfAborted = () => {
		if (options.signal?.aborted) throw new Error("workflow aborted");
	};
	const resolveAgentModel = (normalized, assignedPhase) => {
		const realId = normalized.model?.trim();
		if (realId) {
			if (!deps.resolveModelId) throw new Error("workflow runtime missing resolveModelId; cannot resolve real model id");
			return deps.resolveModelId(realId);
		}
		if (assignedPhase) {
			const phaseRef = phaseDefaultModels.get(assignedPhase);
			if (phaseRef) {
				if (!deps.resolveModelId) return void 0;
				return deps.resolveModelId(phaseRef);
			}
		}
	};
	const agent = async (prompt, agentOptions = {}) => {
		throwIfAborted();
		if (budget.total !== null && budget.remaining() <= 0) throw new Error("workflow token budget exhausted");
		if (state.agentCount >= maxSubagents) throw new Error(`workflow agent quota exhausted (max ${maxSubagents})`);
		const taskPrompt = requireString(prompt, "agent prompt");
		const normalized = normalizeAgentOptions(agentOptions);
		const assignedPhase = normalized.phase ?? state.currentPhase;
		const requestedLabel = normalized.label?.trim();
		state.agentCount += 1;
		const id = state.agentCount;
		const label = requestedLabel || defaultAgentLabel(assignedPhase, id);
		options.onAgentQueued?.({
			id,
			label,
			phase: assignedPhase,
			prompt: taskPrompt
		});
		const runPromise = limiter(async () => {
			options.onAgentStart?.({
				id,
				label,
				phase: assignedPhase,
				prompt: taskPrompt
			});
			try {
				throwIfAborted();
				const resolvedModel = resolveAgentModel(normalized, assignedPhase);
				const enhanced = options.enhanceSubagentRun?.({
					id,
					label,
					phase: assignedPhase,
					prompt: taskPrompt
				});
				const result = await deps.runner.run(taskPrompt, {
					label,
					schema: normalized.schema,
					allowedToolNames: normalized.toolset,
					maxIterations: normalized.maxIterations,
					phase: assignedPhase,
					signal: options.signal,
					model: resolvedModel,
					...enhanced
				});
				throwIfAborted();
				const status = result === null ? "error" : "done";
				state.spent += estimateTokens(result);
				options.onAgentEnd?.({
					id,
					label,
					phase: assignedPhase,
					result,
					status
				});
				return result;
			} catch (e) {
				if (options.signal?.aborted) {
					options.onAgentEnd?.({
						id,
						label,
						phase: assignedPhase,
						result: null,
						status: "skipped"
					});
					throw e;
				}
				log(`agent ${label} failed: ${e instanceof Error ? e.message : String(e)}`);
				options.onAgentEnd?.({
					id,
					label,
					phase: assignedPhase,
					result: null,
					status: "error"
				});
				return null;
			}
		});
		pendingAgentRuns.add(runPromise);
		runPromise.then(() => pendingAgentRuns.delete(runPromise), () => pendingAgentRuns.delete(runPromise));
		return runPromise;
	};
	const parallel = async (thunks) => {
		throwIfAborted();
		if (!Array.isArray(thunks)) throw new TypeError("parallel() expects an array of functions");
		for (const t of thunks) if (typeof t !== "function") throw new TypeError("parallel() expects an array of functions, not promises. Wrap each call: () => agent(...)");
		return Promise.all(thunks.map(async (thunk, index) => {
			try {
				return await thunk();
			} catch (e) {
				if (options.signal?.aborted) throw e;
				log(`parallel[${index}] failed: ${e instanceof Error ? e.message : String(e)}`);
				return null;
			}
		}));
	};
	const pipeline = async (items, ...stages) => {
		throwIfAborted();
		if (!Array.isArray(items)) throw new TypeError("pipeline() expects an array as the first argument");
		for (const stage of stages) if (typeof stage !== "function") throw new TypeError("pipeline() stages must be functions: pipeline(items, item => ..., result => ...)");
		const typedStages = stages;
		return Promise.all(items.map(async (item, index) => {
			let value = item;
			for (const stage of typedStages) try {
				throwIfAborted();
				value = await stage(value, item, index);
				throwIfAborted();
			} catch (e) {
				if (options.signal?.aborted) throw e;
				log(`pipeline[${index}] failed: ${e instanceof Error ? e.message : String(e)}`);
				return null;
			}
			return value;
		}));
	};
	const context = createContext({
		agent,
		parallel,
		pipeline,
		log,
		phase,
		args: options.args,
		cwd: options.cwd,
		process: Object.freeze({ cwd: () => options.cwd }),
		budget,
		console: {
			log,
			info: log,
			warn: (m) => log(`[warn] ${String(m)}`),
			error: (m) => log(`[error] ${String(m)}`)
		},
		JSON,
		Math,
		Array,
		Object,
		String,
		Number,
		Boolean,
		Set,
		Map,
		Promise
	});
	const script$ = new Script(`(async () => {\n${body}\n})()`, { filename: `${meta.name}.workflow.js` });
	let result;
	try {
		result = await script$.runInContext(context);
		await Promise.allSettled([...pendingAgentRuns]);
	} catch (e) {
		await Promise.allSettled([...pendingAgentRuns]);
		throw rewriteScriptError(e);
	}
	assertStructuredCloneable(result, "workflow result");
	return {
		meta,
		result,
		logs: state.logs,
		phases: state.phases,
		agentCount: state.agentCount,
		durationMs: Date.now() - started
	};
}
function emptySnapshotFor(name, description, phaseTitles) {
	return {
		name,
		description,
		phases: phaseTitles ? [...phaseTitles] : [],
		logs: [],
		agents: [],
		agentCount: 0,
		runningCount: 0,
		doneCount: 0,
		errorCount: 0,
		skippedCount: 0
	};
}
function clampConcurrency(requested) {
	if (typeof requested === "number" && Number.isFinite(requested) && requested >= 1) return Math.min(Math.floor(requested), DEFAULT_CONCURRENCY_CEILING);
	let cpu = DEFAULT_CONCURRENCY_CEILING;
	try {
		cpu = availableParallelism();
	} catch {}
	return Math.max(DEFAULT_CONCURRENCY_FLOOR, Math.min(DEFAULT_CONCURRENCY_CEILING, cpu - 2));
}
function createLimiter(limit) {
	let active = 0;
	const queue = [];
	const next = () => {
		active -= 1;
		const resume = queue.shift();
		if (resume) resume();
	};
	return async (fn) => {
		if (active >= limit) await new Promise((resolve) => queue.push(resolve));
		active += 1;
		try {
			return await fn();
		} finally {
			next();
		}
	};
}
function buildPhaseModelMap(phases) {
	const out = /* @__PURE__ */ new Map();
	if (!phases) return out;
	for (const p of phases) if (p && typeof p.title === "string" && typeof p.model === "string" && p.model.trim()) out.set(p.title, p.model.trim());
	return out;
}
function requireString(value, name) {
	if (typeof value !== "string") throw new TypeError(`${name} must be a string`);
	return value;
}
function optionalString(value, name) {
	if (value === void 0) return void 0;
	return requireString(value, name);
}
function normalizeAgentOptions(value) {
	if (value === void 0 || value === null) return {};
	if (typeof value !== "object") throw new TypeError("agent options must be an object");
	const options = value;
	const toolset = options.toolset;
	if (toolset !== void 0) {
		if (!Array.isArray(toolset) || toolset.some((t) => typeof t !== "string")) throw new TypeError("agent toolset must be an array of strings");
	}
	const maxIterations = options.maxIterations;
	if (maxIterations !== void 0) {
		if (typeof maxIterations !== "number" || !Number.isFinite(maxIterations) || maxIterations < 1) throw new TypeError("agent maxIterations must be a positive number");
	}
	const modelStr = optionalString(options.model, "agent model");
	return {
		label: optionalString(options.label, "agent label"),
		phase: optionalString(options.phase, "agent phase"),
		schema: options.schema,
		model: modelStr,
		toolset,
		maxIterations
	};
}
function assertStructuredCloneable(value, name) {
	try {
		structuredClone(value);
	} catch (e) {
		const promisePath = findPromisePath(value, "");
		if (promisePath !== null) throw new Error(`${name} contains a Promise at \`${promisePath || "<root>"}\` — missing 'await' before parallel()/pipeline()/agent(). Async return auto-unwraps a bare Promise, but a Promise nested in an object/array is returned as-is.`);
		const detail = e instanceof Error ? ` ${e.message}` : "";
		throw new Error(`${name} must be structured-cloneable; did you forget to await agent(), parallel(), or pipeline()?${detail}`);
	}
}
/**
* Find the first Promise lurking in `value`, returning a JS-path like
* "results[0].pending". Returns null when no Promise is found. Bounded
* depth/breadth so it never explodes on weird user shapes.
*/
function findPromisePath(value, path, depth = 0) {
	if (depth > 4) return null;
	if (isThenable(value)) return path;
	if (Array.isArray(value)) {
		for (let i = 0; i < Math.min(value.length, 32); i++) {
			const inner = findPromisePath(value[i], `${path}[${i}]`, depth + 1);
			if (inner !== null) return inner;
		}
		return null;
	}
	if (value !== null && typeof value === "object") {
		let count = 0;
		for (const [key, child] of Object.entries(value)) {
			if (count++ > 32) break;
			const inner = findPromisePath(child, path ? `${path}.${key}` : key, depth + 1);
			if (inner !== null) return inner;
		}
	}
	return null;
}
function isThenable(value) {
	return value !== null && typeof value === "object" && typeof value.then === "function";
}
/**
* Rewrite TypeErrors that look like "called .map on a Promise" with an
* await-shaped hint. Static lint catches the obvious form at parse time;
* this is the safety net for cases lint can't see (dynamic indirection,
* values stashed in helper closures).
*/
const PROMISE_METHOD_ERROR = /\.(map|filter|forEach|flat|flatMap|find|some|every|reduce|join|length|then)\b is not a function/;
function rewriteScriptError(error) {
	const asError = error;
	if (!(asError?.name === "TypeError" && typeof asError.message === "string")) return error instanceof Error ? error : new Error(String(error));
	const message = asError.message;
	if (!PROMISE_METHOD_ERROR.test(message)) return error instanceof Error ? error : new Error(message);
	return /* @__PURE__ */ new Error(`${message}\n\nHint: parallel()/pipeline()/agent() return a Promise — 'await' the call before using its result.\n  ❌ const r = parallel(...); r.map(...)\n  ✅ const r = await parallel(...); r.map(...)`);
}
function defaultAgentLabel(phase, index) {
	return phase ? `${phase} agent ${index}` : `agent ${index}`;
}
function estimateTokens(value) {
	if (value === null || value === void 0) return 0;
	try {
		return Math.ceil(JSON.stringify(value).length / 4);
	} catch {
		return Math.ceil(String(value).length / 4);
	}
}
//#endregion
export { emptySnapshotFor, runWorkflow };

//# sourceMappingURL=runtime.js.map