import { init_string_coerce, normalizeOptionalString } from "../../utils/string-coerce.js";
import { emitSessionTranscriptUpdate } from "../../session/transcript-events.js";
import { boundedJsonUtf8Bytes, firstEnumerableOwnKeys, jsonUtf8BytesOrInfinity } from "../../infra/json-utf8-bytes.js";
import { formatContextLimitTruncationNotice } from "./tool-result-context-guard.js";
import { resolveLiveToolResultMaxChars, truncateToolResultMessage } from "./tool-result-truncation.js";
import { extractToolCallsFromAssistant, extractToolResultId } from "../transcript/tool-call-id.js";
import { makeMissingToolResult, sanitizeToolCallInputs } from "../transcript/session-transcript-repair.js";
//#region src/agent/embedded/session-tool-result-guard.ts
init_string_coerce();
/**
* Install the guard on a SessionManager and return its control API.
* Subsequent assistant/toolResult writes by pi-coding-agent flow through the
* guard transparently.
*/
function installSessionToolResultGuard(sessionManager, opts = {}) {
	const guard = new ToolResultGuard(sessionManager, opts);
	guard.attach();
	return {
		flushPendingToolResults: () => guard.flushPending(),
		clearPendingToolResults: () => guard.clearPending(),
		getPendingIds: () => guard.getPendingIds()
	};
}
/**
* Convenience wrapper used by the embedded runner pool: install the guard
* (idempotent), pin the size cap from the model context window, and expose
* `flushPendingToolResults` / `clearPendingToolResults` directly on the
* SessionManager instance so callers do not need to keep the install result.
*/
function guardSessionManager(sessionManager, opts) {
	if (typeof sessionManager.flushPendingToolResults === "function") return sessionManager;
	const result = installSessionToolResultGuard(sessionManager, {
		sessionKey: opts?.sessionKey,
		allowSyntheticToolResults: opts?.allowSyntheticToolResults,
		missingToolResultText: opts?.missingToolResultText,
		allowedToolNames: opts?.allowedToolNames,
		maxToolResultChars: typeof opts?.contextWindowTokens === "number" ? resolveLiveToolResultMaxChars({
			contextWindowTokens: opts.contextWindowTokens,
			cfg: opts?.config,
			agentId: opts?.agentId
		}) : void 0
	});
	const tagged = sessionManager;
	tagged.flushPendingToolResults = result.flushPendingToolResults;
	tagged.clearPendingToolResults = result.clearPendingToolResults;
	return tagged;
}
/**
* Recover the original (un-guarded) appendMessage for a session manager.
* Useful for callers that need a low-level "bypass the guard" write path.
*/
function getRawSessionAppendMessage(sessionManager) {
	return sessionManager[RAW_APPEND_MESSAGE] ?? sessionManager.appendMessage.bind(sessionManager);
}
function resolveMaxToolResultChars(opts) {
	return Math.max(1, opts.maxToolResultChars ?? 16e3);
}
function capToolResultSize(msg, maxChars) {
	if (msg.role !== "toolResult") return msg;
	return truncateToolResultMessage(msg, maxChars, {
		suffix: (truncatedChars) => formatContextLimitTruncationNotice(truncatedChars),
		minKeepChars: 2e3
	});
}
const MAX_PERSISTED_TOOL_RESULT_DETAILS_BYTES = 8192;
const MAX_PERSISTED_DETAIL_STRING_CHARS = 2e3;
const MAX_PERSISTED_DETAIL_SESSION_COUNT = 10;
const MAX_PERSISTED_DETAIL_FALLBACK_STRING_CHARS = 200;
function originalDetailsSizeFields(size) {
	return size.complete ? { originalDetailsBytes: size.bytes } : { originalDetailsBytesAtLeast: size.bytes };
}
function truncatePersistedDetailString(value, maxChars = MAX_PERSISTED_DETAIL_STRING_CHARS) {
	if (value.length <= maxChars) return value;
	return `${value.slice(0, maxChars)}\n\n[xopc persisted detail truncated: ${value.length - maxChars} chars omitted]`;
}
function sanitizePersistedSessionDetail(value) {
	if (!value || typeof value !== "object") return value;
	const src = value;
	const out = {};
	for (const key of [
		"sessionId",
		"status",
		"pid",
		"startedAt",
		"endedAt",
		"runtimeMs",
		"cwd",
		"name",
		"truncated",
		"exitCode",
		"exitSignal"
	]) {
		const field = src[key];
		if (field !== void 0) out[key] = typeof field === "string" ? truncatePersistedDetailString(field, 500) : field;
	}
	if (typeof src.command === "string") out.command = truncatePersistedDetailString(src.command, 500);
	return out;
}
function buildPersistedDetailsFallback(src, originalSize, sanitizedBytes) {
	const fallback = {
		persistedDetailsTruncated: true,
		finalDetailsTruncated: true,
		...originalDetailsSizeFields(originalSize)
	};
	if (sanitizedBytes !== void 0) fallback.sanitizedDetailsBytes = sanitizedBytes;
	if (src) {
		fallback.originalDetailKeys = firstEnumerableOwnKeys(src, 40);
		for (const key of [
			"status",
			"sessionId",
			"pid",
			"exitCode",
			"exitSignal",
			"truncated"
		]) {
			const field = src[key];
			if (field !== void 0) fallback[key] = typeof field === "string" ? truncatePersistedDetailString(field, MAX_PERSISTED_DETAIL_FALLBACK_STRING_CHARS) : field;
		}
	}
	return fallback;
}
function enforcePersistedDetailsByteCap(value, src, originalSize) {
	const sanitizedBytes = jsonUtf8BytesOrInfinity(value);
	if (sanitizedBytes <= MAX_PERSISTED_TOOL_RESULT_DETAILS_BYTES) return value;
	const fallback = buildPersistedDetailsFallback(src, originalSize, sanitizedBytes);
	if (jsonUtf8BytesOrInfinity(fallback) <= MAX_PERSISTED_TOOL_RESULT_DETAILS_BYTES) return fallback;
	return {
		persistedDetailsTruncated: true,
		finalDetailsTruncated: true,
		...originalDetailsSizeFields(originalSize),
		sanitizedDetailsBytes: sanitizedBytes
	};
}
function sanitizeToolResultDetailsForPersistence(details) {
	if (details === void 0 || details === null) return details;
	const originalSize = boundedJsonUtf8Bytes(details, MAX_PERSISTED_TOOL_RESULT_DETAILS_BYTES);
	if (originalSize.complete && originalSize.bytes <= MAX_PERSISTED_TOOL_RESULT_DETAILS_BYTES) return details;
	if (typeof details !== "object") return enforcePersistedDetailsByteCap({
		persistedDetailsTruncated: true,
		...originalDetailsSizeFields(originalSize),
		valueType: typeof details
	}, void 0, originalSize);
	const src = details;
	const out = {
		persistedDetailsTruncated: true,
		...originalDetailsSizeFields(originalSize),
		originalDetailKeys: firstEnumerableOwnKeys(src, 40)
	};
	for (const key of [
		"status",
		"sessionId",
		"pid",
		"startedAt",
		"endedAt",
		"cwd",
		"name",
		"exitCode",
		"exitSignal",
		"retryInMs",
		"total",
		"totalLines",
		"totalChars",
		"truncated",
		"fullOutputPath",
		"truncation"
	]) {
		const field = src[key];
		if (field !== void 0) out[key] = typeof field === "string" ? truncatePersistedDetailString(field) : field;
	}
	if (typeof src.tail === "string") out.tail = truncatePersistedDetailString(src.tail);
	if (Array.isArray(src.sessions)) {
		out.sessions = src.sessions.slice(0, MAX_PERSISTED_DETAIL_SESSION_COUNT).map(sanitizePersistedSessionDetail);
		if (src.sessions.length > MAX_PERSISTED_DETAIL_SESSION_COUNT) out.sessionsTruncated = src.sessions.length - MAX_PERSISTED_DETAIL_SESSION_COUNT;
	}
	return enforcePersistedDetailsByteCap(out, src, originalSize);
}
function capToolResultDetails(msg) {
	if (msg.role !== "toolResult") return msg;
	const details = msg.details;
	const sanitizedDetails = sanitizeToolResultDetailsForPersistence(details);
	if (sanitizedDetails === details) return msg;
	const next = { ...msg };
	next.details = sanitizedDetails;
	return next;
}
function capToolResultForPersistence(msg, maxChars) {
	return capToolResultDetails(capToolResultSize(msg, maxChars));
}
function normalizePersistedToolResultName(message, fallbackName) {
	if (message.role !== "toolResult") return message;
	const toolResult = message;
	const rawToolName = toolResult.toolName;
	const normalizedToolName = normalizeOptionalString(rawToolName);
	if (normalizedToolName) {
		if (rawToolName === normalizedToolName) return toolResult;
		return {
			...toolResult,
			toolName: normalizedToolName
		};
	}
	const normalizedFallback = normalizeOptionalString(fallbackName);
	if (normalizedFallback) return {
		...toolResult,
		toolName: normalizedFallback
	};
	if (typeof rawToolName === "string") return {
		...toolResult,
		toolName: "unknown"
	};
	return toolResult;
}
const RAW_APPEND_MESSAGE = Symbol("xopc.session.rawAppendMessage");
function rememberOriginalAppend(sessionManager) {
	const tagged = sessionManager;
	const stored = tagged[RAW_APPEND_MESSAGE];
	if (stored) return stored;
	const original = sessionManager.appendMessage.bind(sessionManager);
	tagged[RAW_APPEND_MESSAGE] = original;
	return original;
}
var PendingToolCallTracker = class {
	pending = /* @__PURE__ */ new Map();
	size() {
		return this.pending.size;
	}
	getToolName(id) {
		return this.pending.get(id);
	}
	delete(id) {
		this.pending.delete(id);
	}
	clear() {
		this.pending.clear();
	}
	trackToolCalls(calls) {
		for (const call of calls) this.pending.set(call.id, call.name);
	}
	entries() {
		return this.pending.entries();
	}
	getPendingIds() {
		return Array.from(this.pending.keys());
	}
	shouldFlushForSanitizedDrop() {
		return this.pending.size > 0;
	}
	shouldFlushBeforeNonToolResult(nextRole, toolCallCount) {
		return this.pending.size > 0 && (toolCallCount === 0 || nextRole !== "assistant");
	}
	shouldFlushBeforeNewToolCalls(toolCallCount) {
		return this.pending.size > 0 && toolCallCount > 0;
	}
};
var ToolResultGuard = class {
	sessionManager;
	opts;
	pending = new PendingToolCallTracker();
	originalAppend;
	allowSyntheticToolResults;
	maxToolResultChars;
	constructor(sessionManager, opts) {
		this.sessionManager = sessionManager;
		this.opts = opts;
		this.originalAppend = rememberOriginalAppend(sessionManager);
		this.allowSyntheticToolResults = opts.allowSyntheticToolResults ?? true;
		this.maxToolResultChars = resolveMaxToolResultChars(opts);
	}
	/** Monkey-patch the session manager so pi-coding-agent's internal appendMessage flows through us. */
	attach() {
		const bound = this.guardedAppend.bind(this);
		this.sessionManager.appendMessage = bound;
	}
	flushPending() {
		if (this.pending.size() === 0) return;
		if (this.allowSyntheticToolResults) for (const [id, name] of this.pending.entries()) {
			const synthetic = makeMissingToolResult({
				toolCallId: id,
				toolName: name,
				text: this.opts.missingToolResultText
			});
			const flushed = this.applyBeforeWriteHook(this.persistToolResult(this.persistMessage(synthetic), {
				toolCallId: id,
				toolName: name,
				isSynthetic: true
			}));
			if (flushed) this.originalAppend(capToolResultForPersistence(flushed, this.maxToolResultChars));
		}
		this.pending.clear();
	}
	clearPending() {
		this.pending.clear();
	}
	getPendingIds() {
		return this.pending.getPendingIds();
	}
	persistMessage(message) {
		const transformer = this.opts.transformMessageForPersistence;
		return transformer ? transformer(message) : message;
	}
	persistToolResult(message, meta) {
		const transformer = this.opts.transformToolResultForPersistence;
		return transformer ? transformer(message, meta) : message;
	}
	/**
	* Run the before_message_write hook. Returns the (possibly modified) message,
	* or null if the message should be blocked.
	*/
	applyBeforeWriteHook(msg) {
		const beforeWrite = this.opts.beforeMessageWriteHook;
		if (!beforeWrite) return msg;
		const result = beforeWrite({ message: msg });
		if (result?.block) return null;
		if (result?.message) return result.message;
		return msg;
	}
	guardedAppend(message) {
		let nextMessage = message;
		if (message.role === "assistant") {
			const sanitized = sanitizeToolCallInputs([message], { allowedToolNames: this.opts.allowedToolNames });
			if (sanitized.length === 0) {
				if (this.pending.shouldFlushForSanitizedDrop()) this.flushPending();
				return;
			}
			nextMessage = sanitized[0];
		}
		const nextRole = nextMessage.role;
		if (nextRole === "toolResult") {
			const id = extractToolResultId(nextMessage);
			const toolName = id ? this.pending.getToolName(id) : void 0;
			if (id) this.pending.delete(id);
			const normalizedToolResult = normalizePersistedToolResultName(nextMessage, toolName);
			const capped = capToolResultForPersistence(this.persistMessage(normalizedToolResult), this.maxToolResultChars);
			const persisted = this.applyBeforeWriteHook(this.persistToolResult(capped, {
				toolCallId: id ?? void 0,
				toolName,
				isSynthetic: false
			}));
			if (!persisted) return;
			return this.originalAppend(capToolResultForPersistence(persisted, this.maxToolResultChars));
		}
		const stopReason = nextMessage.stopReason;
		const toolCalls = nextRole === "assistant" && stopReason !== "aborted" && stopReason !== "error" ? extractToolCallsFromAssistant(nextMessage) : [];
		if (this.pending.shouldFlushBeforeNonToolResult(nextRole, toolCalls.length)) this.flushPending();
		if (this.pending.shouldFlushBeforeNewToolCalls(toolCalls.length)) this.flushPending();
		const finalMessage = this.applyBeforeWriteHook(this.persistMessage(nextMessage));
		if (!finalMessage) return;
		const result = this.originalAppend(finalMessage);
		const sessionFile = this.sessionManager.getSessionFile?.();
		if (sessionFile) emitSessionTranscriptUpdate({
			sessionFile,
			sessionKey: this.opts.sessionKey,
			message: finalMessage,
			messageId: typeof result === "string" ? result : void 0
		});
		if (toolCalls.length > 0) this.pending.trackToolCalls(toolCalls);
		return result;
	}
};
//#endregion
export { getRawSessionAppendMessage, guardSessionManager, installSessionToolResultGuard };

//# sourceMappingURL=session-tool-result-guard.js.map