//#region src/headless-tools.ts
/**
* Parses a headless-tool interrupt `value` from the graph. Accepts both
* `toolCall` (LangChain JS) and `tool_call` (Python / JSON snake_case).
*/
function parseHeadlessToolInterruptPayload(value) {
	if (typeof value !== "object" || value == null) return null;
	const v = value;
	if (v.type !== "tool") return null;
	const rawTc = v.toolCall ?? v.tool_call;
	if (typeof rawTc !== "object" || rawTc == null) return null;
	const tc = rawTc;
	if (typeof tc.name !== "string") return null;
	return {
		type: "tool",
		toolCall: {
			id: typeof tc.id === "string" ? tc.id : void 0,
			name: tc.name,
			args: tc.args
		}
	};
}
/**
* Strip headless-tool interrupts from a user-facing interrupt list.
*/
function filterOutHeadlessToolInterrupts(interrupts) {
	return interrupts.filter((interrupt) => interrupt.value == null || !isHeadlessToolInterrupt(interrupt.value));
}
function isHeadlessToolInterrupt(interrupt) {
	return parseHeadlessToolInterruptPayload(interrupt) != null;
}
function findHeadlessTool(tools, name) {
	return tools.find((tool) => tool.tool.name === name);
}
async function executeHeadlessTool(implementation, args, onTool) {
	const startTime = Date.now();
	onTool?.({
		phase: "start",
		name: implementation.tool.name,
		args
	});
	try {
		const result = await implementation.execute(args);
		const duration = Date.now() - startTime;
		onTool?.({
			phase: "success",
			name: implementation.tool.name,
			args,
			result,
			duration
		});
		return {
			success: true,
			result
		};
	} catch (err) {
		const error = err instanceof Error ? err : new Error(String(err));
		const duration = Date.now() - startTime;
		onTool?.({
			phase: "error",
			name: implementation.tool.name,
			args,
			error,
			duration
		});
		return {
			success: false,
			error
		};
	}
}
async function handleHeadlessToolInterrupt(interrupt, tools, onTool) {
	const { toolCall } = interrupt;
	const implementation = findHeadlessTool(tools, toolCall.name);
	if (!implementation) {
		const error = /* @__PURE__ */ new Error(`Headless tool "${toolCall.name}" is not registered. Available tools: ${tools.map((tool) => tool.tool.name).join(", ") || "none"}`);
		onTool?.({
			phase: "error",
			name: toolCall.name,
			args: toolCall.args,
			error,
			duration: 0
		});
		return {
			toolCallId: toolCall.id,
			value: { error: error.message }
		};
	}
	const result = await executeHeadlessTool(implementation, toolCall.args, onTool);
	if (result.success) return {
		toolCallId: toolCall.id,
		value: result.result
	};
	return {
		toolCallId: toolCall.id,
		value: { error: result.error.message }
	};
}
function headlessToolResumeCommand(result) {
	return {
		resume: result.toolCallId ? { [result.toolCallId]: result.value } : result.value,
		keyedByInterruptId: false
	};
}
/**
* Merge headless-tool results into one resume command. Use interrupt-id keys
* whenever the stream provided them so the resume does not need to rediscover
* a pending interrupt from mutable client state.
*/
function headlessToolsBatchResumeCommand(entries) {
	if (entries.length === 0) return {
		resume: {},
		keyedByInterruptId: false
	};
	if (!entries.every((entry) => entry.interruptId.length > 0) && entries.length === 1) {
		const [entry] = entries;
		return headlessToolResumeCommand({
			toolCallId: entry.toolCallId,
			value: entry.value
		});
	}
	const resume = {};
	for (const entry of entries) {
		if (entry.interruptId.length === 0) continue;
		resume[entry.interruptId] = entry.toolCallId != null && entry.toolCallId.length > 0 ? { [entry.toolCallId]: entry.value } : entry.value;
	}
	return {
		resume,
		keyedByInterruptId: true
	};
}
/**
* Resume a headless-tool batch on the v1 commands transport.
*
* {@link headlessToolsBatchResumeCommand} still returns a legacy
* `{ resume }` command shape for callers on the old runs/stream API.
* On v1 `StreamController`, that payload must be sent through
* {@link HeadlessToolResumeController.respond} /
* {@link HeadlessToolResumeController.respondAll} — not `submit(null,
* { command })`, which dispatches `run.start` without a resume value.
*/
function applyHeadlessToolResumeCommand(controller, command) {
	const { resume, keyedByInterruptId } = command;
	if (resume == null) return Promise.resolve();
	if (keyedByInterruptId === true || keyedByInterruptId !== false && isInterruptIdKeyedResume(resume)) return controller.respondAll(resume);
	return controller.respond(resume);
}
/**
* True when a resume payload is keyed by protocol interrupt id at the top
* level (for {@link HeadlessToolResumeController.respondAll}).
*
* Prefer {@link HeadlessToolResumeCommand.keyedByInterruptId} from
* {@link headlessToolsBatchResumeCommand} when ids are not inferable
* (for example `{ "int-1": result }` without a nested tool-call map).
*
* When `interrupts` is provided, a single top-level key that matches a
* known protocol interrupt id is treated as interrupt-keyed.
*/
function isInterruptIdKeyedResume(resume, interrupts) {
	if (resume == null || typeof resume !== "object" || Array.isArray(resume)) return false;
	const record = resume;
	const keys = Object.keys(record);
	if (keys.length === 0) return false;
	if (keys.length > 1) return true;
	const [key] = keys;
	if (interrupts?.length) {
		if (new Set(interrupts.map((entry) => entry.interruptId)).has(key)) return true;
	}
	return /^[0-9a-f]{32}$/i.test(key);
}
/**
* Reads the tool-call id from a headless-tool resume command shaped as
* `{ [toolCallId]: result }`.
*/
function extractHeadlessToolCallIdFromResumeCommand(resume) {
	if (resume == null || typeof resume !== "object" || Array.isArray(resume)) return;
	const keys = Object.keys(resume);
	if (keys.length !== 1) return void 0;
	return keys[0];
}
/**
* Pick the protocol interrupt that matches a headless-tool resume payload.
* Falls back to the newest unresolved interrupt for non-keyed resumes.
*/
function resolveInterruptTargetForHeadlessResume(resume, interrupts, resolvedInterruptIds) {
	const toolCallId = extractHeadlessToolCallIdFromResumeCommand(resume);
	if (toolCallId != null) for (let i = interrupts.length - 1; i >= 0; i -= 1) {
		const entry = interrupts[i];
		if (entry == null || resolvedInterruptIds.has(entry.interruptId)) continue;
		if (parseHeadlessToolInterruptPayload(entry.payload)?.toolCall.id === toolCallId) return {
			interruptId: entry.interruptId,
			namespace: [...entry.namespace]
		};
	}
	for (let i = interrupts.length - 1; i >= 0; i -= 1) {
		const entry = interrupts[i];
		if (entry == null || resolvedInterruptIds.has(entry.interruptId)) continue;
		return {
			interruptId: entry.interruptId,
			namespace: [...entry.namespace]
		};
	}
	return null;
}
const coalescedHeadlessFlushes = /* @__PURE__ */ new WeakMap();
/**
* Coalesce rapid headless-tool flush triggers into one microtask so parallel
* `input.requested` events observed back-to-back batch into a single resume.
* Vue/Svelte/Angular watchers run synchronously per event; without this,
* the first interrupt can be claimed before the second arrives and resume
* splits into staggered single-tool commands.
*/
function scheduleCoalescedHeadlessToolFlush(handledIds, run) {
	let state = coalescedHeadlessFlushes.get(handledIds);
	if (state == null) {
		state = {
			scheduled: false,
			run: () => {}
		};
		coalescedHeadlessFlushes.set(handledIds, state);
	}
	state.run = run;
	if (state.scheduled) return;
	state.scheduled = true;
	Promise.resolve().then(() => {
		state.scheduled = false;
		state.run();
	});
}
/**
* Execute and resume all newly seen headless-tool interrupts from a values
* payload. Callers own `handledIds` and should clear it when the thread changes.
*/
function flushPendingHeadlessToolInterrupts(values, tools, handledIds, options) {
	if (!tools?.length || !values) return;
	const interrupts = values.__interrupt__;
	if (!Array.isArray(interrupts) || interrupts.length === 0) return;
	const defer = options.defer ?? ((run) => run());
	const pending = [];
	const seenToolCallIds = /* @__PURE__ */ new Set();
	for (const interrupt of interrupts) {
		const headlessInterrupt = parseHeadlessToolInterruptPayload(interrupt.value);
		if (!headlessInterrupt) continue;
		const interruptId = interrupt.id ?? headlessInterrupt.toolCall.id ?? "";
		const toolCallId = headlessInterrupt.toolCall.id ?? "";
		if (handledIds.has(interruptId)) continue;
		if (toolCallId && handledIds.has(toolCallId)) continue;
		if (toolCallId && seenToolCallIds.has(toolCallId)) continue;
		if (toolCallId) seenToolCallIds.add(toolCallId);
		handledIds.add(interruptId);
		if (toolCallId) handledIds.add(toolCallId);
		pending.push({
			interruptId,
			headlessInterrupt,
			toolCallId
		});
	}
	if (pending.length === 0) return;
	defer(() => {
		(async () => {
			const results = await Promise.all(pending.map(async ({ interruptId, headlessInterrupt, toolCallId }) => {
				const result = await handleHeadlessToolInterrupt(headlessInterrupt, tools, options.onTool);
				return {
					interruptId,
					toolCallId: result.toolCallId ?? toolCallId,
					value: result.value
				};
			}));
			await Promise.resolve(options.resumeSubmit(headlessToolsBatchResumeCommand(results)));
		})();
	});
}
//#endregion
export { applyHeadlessToolResumeCommand, executeHeadlessTool, filterOutHeadlessToolInterrupts, findHeadlessTool, flushPendingHeadlessToolInterrupts, handleHeadlessToolInterrupt, headlessToolResumeCommand, isHeadlessToolInterrupt, parseHeadlessToolInterruptPayload, resolveInterruptTargetForHeadlessResume, scheduleCoalescedHeadlessToolFlush };

//# sourceMappingURL=headless-tools.js.map