from __future__ import annotations

import logging
import os
from pathlib import Path
from datetime import datetime
from typing import Optional

def _should_redact() -> bool:
	try:
		v = os.environ.get("AGI_LLM_REDACT_LOGS", "1").strip()
		return v not in ("0", "false", "False")
	except Exception:
		return True

def _redact(text: str) -> str:
	if not _should_redact():
		return text
	try:
		from agi_dw.core.utils.redact import redact_text  # type: ignore
		t, _ = redact_text(text)
		return t
	except Exception:
		return text

class PromptLogger:
	"""Unified, PII-safe prompt/response logger.

	- Respects AGI_LLM_REDACT_LOGS env (on by default)
	- Writes to agi_dw/data/logs with timestamped filenames
	- Optional echo to stdout when echo=True
	"""

	def __init__(self, component: str, echo: bool = False) -> None:
		self.component = component
		self.echo = echo
		try:
			root = Path(__file__).resolve().parents[2]
		except Exception:
			root = Path.cwd()
		self.log_dir = root / "data" / "logs"
		try:
			self.log_dir.mkdir(parents=True, exist_ok=True)


PromptLogger

def _should_redact() -> bool:
	try:
		v = os.environ.get("AGI_LLM_REDACT_LOGS", "1").strip()
		return v not in ("0", "false", "False")
	except Exception:
		return True

def _redact(text: str) -> str:
	if not _should_redact():
		return text
	try:
		from agi_dw.core.utils.redact import redact_text  # type: ignore
		t, _ = redact_text(text)
		return t
	except Exception:
		return text

class PromptLogger:
	"""Unified, PII-safe prompt/response logger.

	- Respects AGI_LLM_REDACT_LOGS env (on by default)
	- Writes to agi_dw/data/logs with timestamped filenames
	- Optional echo to stdout when echo=True
	"""

	def __init__(self, component: str, echo: bool = False) -> None:
		self.component = component
		self.echo = echo
		try:
			root = Path(__file__).resolve().parents[2]
		except Exception:
			root = Path.cwd()
		self.log_dir = root / "data" / "logs"
		try:
			self.log_dir.mkdir(parents=True, exist_ok=True)
		except Exception:
			pass

	def _ts(self) -> str:
		try:
			return datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
		except Exception:
			return "00000000T000000Z"

	def log_text(self, kind: str, text: str) -> Optional[Path]:
		safe = _redact(text)
		if self.echo:
			try:
				print(f"[{self.component.upper()} {kind.upper()}]\n" + safe)
			except Exception:
				pass
		try:
			p = self.log_dir / f"{self.component}_{kind}_{self._ts()}.txt"
			p.write_text(safe, encoding="utf-8")
			return p
		except Exception:
			return None

def get_prompt_logger(component: str, enabled: bool, echo: bool = False) -> Optional[PromptLogger]:
	"""Return a PromptLogger when enabled, else None.

	Centralizes a common pattern scattered across harness/planner/verifier.
	"""
	try:
		return PromptLogger(component, echo=bool(echo)) if bool(enabled) else None
	except Exception:
		return None


		try:
			return datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
		except Exception:
			return "00000000T000000Z"

	def log_text(self, kind: str, text: str) -> Optional[Path]:
		safe = _redact(text)
		if self.echo:
			try:
				print(f"[{self.component.upper()} {kind.upper()}]\n" + safe)
			except Exception:
				pass
		try:
			p = self.log_dir / f"{self.component}_{kind}_{self._ts()}.txt"
			p.write_text(safe, encoding="utf-8")
			return p
		except Exception:
			return None

def get_prompt_logger(component: str, enabled: bool, echo: bool = False) -> Optional[PromptLogger]:
	"""Return a PromptLogger when enabled, else None.

	Centralizes a common pattern scattered across harness/planner/verifier.
	"""
	try:
		return PromptLogger(component, echo=bool(echo)) if bool(enabled) else None
	except Exception:
		return None


def _redact(text: str) -> str:
	if not _should_redact():
		return text
	try:
		from agi_dw.core.utils.redact import redact_text  # type: ignore
		t, _ = redact_text(text)
		return t
	except Exception:
		return text

class PromptLogger:
	"""Unified, PII-safe prompt/response logger.

	- Respects AGI_LLM_REDACT_LOGS env (on by default)
	- Writes to agi_dw/data/logs with timestamped filenames
	- Optional echo to stdout when echo=True
	"""

	def __init__(self, component: str, echo: bool = False) -> None:
		self.component = component
		self.echo = echo
		try:
			root = Path(__file__).resolve().parents[2]
		except Exception:
			root = Path.cwd()
		self.log_dir = root / "data" / "logs"
		try:
			self.log_dir.mkdir(parents=True, exist_ok=True)
		except Exception:
			pass

	def _ts(self) -> str:
		try:
			return datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
		except Exception:
			return "00000000T000000Z"

	def log_text(self, kind: str, text: str) -> Optional[Path]:
		safe = _redact(text)
		if self.echo:
			try:
				print(f"[{self.component.upper()} {kind.upper()}]\n" + safe)
			except Exception:
				pass
		try:
			p = self.log_dir / f"{self.component}_{kind}_{self._ts()}.txt"
			p.write_text(safe, encoding="utf-8")
			return p
		except Exception:
			return None


	"""Unified, PII-safe prompt/response logger.

	- Respects AGI_LLM_REDACT_LOGS env (on by default)
	- Writes to agi_dw/data/logs with timestamped filenames
	- Optional echo to stdout when echo=True
	"""

	def __init__(self, component: str, echo: bool = False) -> None:
		self.component = component
		self.echo = echo
		try:
			root = Path(__file__).resolve().parents[2]
		except Exception:
			root = Path.cwd()
		self.log_dir = root / "data" / "logs"
		try:
			self.log_dir.mkdir(parents=True, exist_ok=True)
		except Exception:
			pass

	def _ts(self) -> str:
		try:
			return datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
		except Exception:
			return "00000000T000000Z"

	def log_text(self, kind: str, text: str) -> Optional[Path]:
		safe = _redact(text)
		if self.echo:
			try:
				print(f"[{self.component.upper()} {kind.upper()}]\n" + safe)
			except Exception:
				pass
		try:
			p = self.log_dir / f"{self.component}_{kind}_{self._ts()}.txt"
			p.write_text(safe, encoding="utf-8")
			return p
		except Exception:
			return None

def get_prompt_logger(component: str, enabled: bool, echo: bool = False) -> Optional[PromptLogger]:
	"""Return a PromptLogger when enabled, else None.

	Centralizes a common pattern scattered across harness/planner/verifier.


	def __init__(self, component: str, echo: bool = False) -> None:
		self.component = component
		self.echo = echo
		try:
			root = Path(__file__).resolve().parents[2]
		except Exception:
			root = Path.cwd()
		self.log_dir = root / "data" / "logs"
		try:
			self.log_dir.mkdir(parents=True, exist_ok=True)
		except Exception:
			pass

	def _ts(self) -> str:
		try:
			return datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
		except Exception:
			return "00000000T000000Z"

	def log_text(self, kind: str, text: str) -> Optional[Path]:
		safe = _redact(text)
		if self.echo:
			try:
				print(f"[{self.component.upper()} {kind.upper()}]\n" + safe)
			except Exception:
				pass
		try:
			p = self.log_dir / f"{self.component}_{kind}_{self._ts()}.txt"
			p.write_text(safe, encoding="utf-8")
			return p
		except Exception:
			return None

def get_prompt_logger(component: str, enabled: bool, echo: bool = False) -> Optional[PromptLogger]:
	"""Return a PromptLogger when enabled, else None.

	Centralizes a common pattern scattered across harness/planner/verifier.
	"""
	try:
		return PromptLogger(component, echo=bool(echo)) if bool(enabled) else None
	except Exception:
		return None


from __future__ import annotations
import logging

import re
from typing import Tuple

EMAIL_RE = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")
PHONE_RE = re.compile(r"(?:(?:\+\d{1,3}[\s-]?)?(?:\(\d{2,4}\)[\s-]?)?\d{3,4}[\s-]?\d{3,4})")

def redact_text(text: str) -> Tuple[str, int]:
	"""Redact emails and phone-like numbers. Returns (redacted_text, num_replacements)."""
	count = 0
	def _email_sub(_m: re.Match) -> str:
		nonlocal count
		count += 1
		return "[REDACTED_EMAIL]"

	def _phone_sub(_m: re.Match) -> str:
		nonlocal count
		count += 1
		return "[REDACTED_PHONE]"

	out = EMAIL_RE.sub(_email_sub, text)
	out = PHONE_RE.sub(_phone_sub, out)
	return out, count


from __future__ import annotations
import logging

import yaml
from typing import Dict, List, Literal, Optional, Tuple

from agi_dw.core.llm.hf_client import HFClient

class CodeCritic:
    """Code review critic for filtering risky completions.

    Uses a small LLM to analyze code and return structured feedback.
    """

    def __init__(self, model: str = "meta-llama/Llama-3.1-8B-Instruct") -> None:
        self.llm = HFClient.get_cached(model)

    def review(self, code: str, lints: str = "") -> Tuple[bool, Dict[str, List[str] | Literal["low", "medium", "high"]]]:
        """Review code and return (is_safe, feedback).

        Args:
            code: The code to review
            lints: Optional linter/test output

        Returns:
            Tuple of:
            - is_safe: True if severity is low/medium, False if high
            - feedback: Dict with issues, recommendations, severity
        """
        prompt = (
            "You are a code review critic. Given a diff and optional linter/test outputs, return ONLY a short YAML mapping:\n"
            "issues: [strings]\nrecommendations: [strings]\nseverity: low|medium|high\n"
            "No prose.\n\nDIFF:\n" + code + "\n\nLINTS/TESTS:\n" + lints
        )
        try:
            text = self.llm.generate(prompt, max_new_tokens=300, temperature=0.0)
            data = yaml.safe_load(text)
            if not isinstance(data, dict):
                return False, {"issues": ["Invalid critic response"], "severity": "high", "recommendations": []}
            issues = data.get("issues", [])
            recs = data.get("recommendations", [])
            sev = str(data.get("severity", "high")).lower()
            if sev not in ("low", "medium", "high"):
                sev = "high"
            return (sev != "high", {
                "issues": issues,
                "recommendations": recs,
                "severity": sev,  # type: ignore
            })
        except Exception as e:
            return False, {
                "issues": [f"Critic error: {e}"],
                "severity": "high",
                "recommendations": [],
            }

def get_critic(model: Optional[str] = None) -> CodeCritic:
    """Get a shared CodeCritic instance."""
    if model is None:
        model = "meta-llama/Llama-3.2-3B"
    return CodeCritic(model)


            if not isinstance(data, dict):
                return False, {"issues": ["Invalid critic response"], "severity": "high", "recommendations": []}
            issues = data.get("issues", [])
            recs = data.get("recommendations", [])
            sev = str(data.get("severity", "high")).lower()
            if sev not in ("low", "medium", "high"):
                sev = "high"
            return (sev != "high", {
                "issues": issues,
                "recommendations": recs,
                "severity": sev,  # type: ignore
            })
        except Exception as e:
            return False, {
                "issues": [f"Critic error: {e}"],
                "severity": "high",
                "recommendations": [],
            }

def get_critic(model: Optional[str] = None) -> CodeCritic:
    """Get a shared CodeCritic instance."""
    if model is None:
        model = "meta-llama/Llama-3.2-3B"
    return CodeCritic(model)


from __future__ import annotations
import logging

import yaml
from typing import Dict, List, Literal, Optional, Tuple

from agi_dw.core.llm.hf_client import HFClient

class CodeCritic:
    """Code review critic for filtering risky completions.

    Uses a small LLM to analyze code and return structured feedback.
    """

    def __init__(self, model: str = "meta-llama/Llama-3.1-8B-Instruct") -> None:
        self.llm = HFClient.get_cached(model)

    def review(self, code: str, lints: str = "") -> Tuple[bool, Dict[str, List[str] | Literal["low", "medium", "high"]]]:
        """Review code and return (is_safe, feedback).

        Args:
            code: The code to review
            lints: Optional linter/test output

        Returns:
            Tuple of:
            - is_safe: True if severity is low/medium, False if high
            - feedback: Dict with issues, recommendations, severity
        """
        prompt = (
            "You are a code review critic. Given a diff and optional linter/test outputs, return ONLY a short YAML mapping:\n"
            "issues: [strings]\nrecommendations: [strings]\nseverity: low|medium|high\n"
            "No prose.\n\nDIFF:\n" + code + "\n\nLINTS/TESTS:\n" + lints
        )
        try:
            text = self.llm.generate(prompt, max_new_tokens=300, temperature=0.0)


from __future__ import annotations
import logging

import json
import os
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, TypeVar

T = TypeVar("T")
R = TypeVar("R")

def ensure_safe_env() -> None:
	os.environ.setdefault("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1")
	os.environ.setdefault("PYTHONDONTWRITEBYTECODE", "1")
	# Avoid HF tokenizers fork warnings/deadlocks in worker pools
	os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")

def strip_fences(text: str) -> str:
	s = (text or "").strip()
	if s.startswith("```"):
		try:
			st = s.find("\n"); en = s.rfind("```")
			if st != -1 and en != -1 and en > st:
				return s[st + 1 : en].strip()
		except Exception:
			return s
	return s

def precheck_code(text: str) -> tuple[bool, Optional[str]]:
	try:
		compile(text, "<completion>", "exec")
		return True, None
	except Exception as e:
		return False, str(e)

def retry_with_backoff(fn: Callable[[], R], retries: int = 1, backoff: float = 0.75) -> R | None:
	attempt = 0
	while attempt <= max(0, retries):
		try:
			return fn()
		except Exception:
			if attempt == retries:
				break
			time.sleep(backoff * (2 ** attempt))
			attempt += 1
	return None

def run_parallel(items: Iterable[T], worker: Callable[[T], R | None], max_workers: int = 4) -> List[R]:
	max_workers = max(1, int(max_workers or 1))
	results: List[R] = []
	with ThreadPoolExecutor(max_workers=max_workers) as ex:
		futs = [ex.submit(worker, it) for it in items]
		for fut in as_completed(futs):
			try:
				val = fut.result()
			except Exception:
				val = None
			if val is not None:
				results.append(val)
	return results

def load_code_index(index_path: Optional[str]) -> Optional[Dict[str, Any]]:
	if not index_path:
		return None
	try:
		from agi_dw.tools.code_index import index_python_repo  # type: ignore
		p = Path(index_path)
		root = p.parent if p.is_file() else p
		return index_python_repo(root)
	except Exception:
		return None

def inject_similar_functions(base: str, question: str, code_index: Optional[Dict[str, Any]], k: int) -> str:
	if not code_index or k <= 0:
		return base
	try:
		funcs: List[str] = []
		for fpath, flist in dict(code_index.get("functions", {})).items():
			for f in flist:
				name = str(f.get("name", "")).lower()
				if any(kw and kw in name for kw in str(question).lower().split()):
					funcs.append(f"{fpath}:{f.get('name')}")
					if len(funcs) >= k:
						break
			if len(funcs) >= k:
				break
		if funcs:
			return "Similar functions:\n" + "\n".join(funcs) + "\n\n" + base
		return base
	except Exception:
		return base

def add_common_bench_args(
	parser: Any,
	*,
	default_max_workers: int = 4,
	default_retries: int = 1,
	default_retry_backoff: float = 0.75,
	include_precheck: bool = True,
	include_logging: bool = True,
	include_critic: bool = True,
	include_index: bool = True,
) -> Any:
	"""Add common benchmark CLI flags to an argparse parser.

	Returns the same parser for chaining.
	"""
	parser.add_argument("--max-workers", type=int, default=default_max_workers)
	parser.add_argument("--retries", type=int, default=default_retries)
	parser.add_argument("--retry-backoff", type=float, default=default_retry_backoff)
	if include_precheck:
		parser.add_argument("--precheck", action="store_true", help="Fast syntax check before writing result")
	if include_logging:
		parser.add_argument("--log-prompts", action="store_true", help="Log prompts and completions safely")
	if include_critic:
		parser.add_argument("--critic", action="store_true", help="Run critic pass to filter high-risk completions")
		parser.add_argument("--critic-model", help="Model to use for critic (default: meta-llama/Llama-3.2-3B)")
	if include_index:
		parser.add_argument("--planner-index-k", type=int, help="Number of similar functions to include from index")
		parser.add_argument("--planner-index-path", help="Path to code index JSON for context injection")
	return parser

import re

def extract_target_function_name(prompt: str) -> Optional[str]:
	"""Extract the first function name from a HumanEval-style prompt."""
	try:
		for line in (prompt or "").splitlines():
			line = line.strip()
			if line.startswith("def "):
				m = re.match(r"def\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(", line)
				if m:
					return m.group(1)
	except Exception:
		return None
	return None

def sanitize_humaneval_completion(text: str, prompt: str) -> str:
	"""Sanitize LLM completion for HumanEval using generic sanitizer defaults."""
	options = {
		"strip_fences": True,
		"drop_prose_phrases": [
			"here is",
			"the function",
			"complete the python function",
			"rules:",
			"example usage:",
		],
		"drop_top_imports": True,
		"normalize_indent": True,
		"indent_width": 4,
		"enforce_body_mode": True,
		"drop_redef_from_prompt": True,
	}
	return sanitize_completion_generic(text, prompt, options)

def sanitize_completion_generic(text: str, prompt: str, options: Optional[Dict[str, Any]] = None) -> str:
	"""Generic sanitizer for code-body completions with registry-configurable options.

	Options (all optional):
	- strip_fences: bool (default True)
	- drop_prose_phrases: List[str] of lowercase phrases to remove when present as leading prose lines
	- drop_top_imports: bool (default True) - remove unindented top-level imports in body-only mode
	- normalize_indent: bool (default True) - rebase minimal indent and standardize to indent_width
	- indent_width: int (default 4)
	- enforce_body_mode: bool (default True) - keep only the function body
	- drop_redef_from_prompt: bool (default False) - if true, when the target function appears, drop its def line
	"""
	opts = options or {}
	strip_fences = bool(opts.get("strip_fences", True))
	drop_top_imports = bool(opts.get("drop_top_imports", True))
	normalize_indent = bool(opts.get("normalize_indent", True))
	indent_width = int(opts.get("indent_width", 4) or 4)
	enforce_body_mode = bool(opts.get("enforce_body_mode", True))
	drop_redef_from_prompt = bool(opts.get("drop_redef_from_prompt", False))
	drop_phrases: List[str] = [str(p).lower() for p in (opts.get("drop_prose_phrases") or [])]

	raw = (text or "")
	# Extract fenced content if requested
	if strip_fences and ("```" in raw):
		try:
			start = raw.find("```")
			end = raw.rfind("```")
			if end > start:
				inner = raw[start + 3 : end]
				# strip optional language tag on first line
				first_nl = inner.find("\n")
				if first_nl != -1 and inner[:first_nl].strip().lower().startswith("python"):
					inner = inner[first_nl + 1 :]
				raw = inner
		except Exception:
			pass
	lines = [ln.rstrip("\r\t ") for ln in raw.splitlines()]
	# Remove fence markers or obvious prose residue lines
	clean_lines: List[str] = []
	for ln in lines:
		strip = ln.strip()
		if strip.startswith("```"):
			continue
		low = strip.lower()
		if any(low.startswith(pfx) for pfx in drop_phrases):
			continue
		clean_lines.append(ln)
	lines = clean_lines
	# Optionally drop redefinition of target function (keep body)
	if drop_redef_from_prompt:
		name = extract_target_function_name(prompt or "") or ""
		if name:
			for i, ln in enumerate(lines):
				if ln.lstrip().startswith(f"def {name}"):
					lines = lines[i + 1 :]
					break
	# Drop top-level imports when requested
	if drop_top_imports:
		lines = [ln for ln in lines if not (ln and not ln.startswith((" ", "\t")) and (ln.startswith("import ") or ln.startswith("from ")))]
	# Body mode: keep only indented block starting from first non-empty
	_pre_body_lines = list(lines)
	body_lines: List[str] = []
	body_started = False
	if enforce_body_mode:
		for ln in lines:
			if not body_started and ln.strip():
				body_started = True
			if body_started:
				if ln.strip() and not ln.startswith((" ", "\t")):
					break
				body_lines.append(ln)
		# Fallback: indent everything if body ended up empty
		if not any(ln.strip() for ln in body_lines) and any(ln.strip() for ln in _pre_body_lines):
			lines = [((" " * indent_width) + ln.strip()) if ln.strip() else "" for ln in _pre_body_lines]
		else:
			lines = body_lines
	# Normalize indentation
	if normalize_indent:
		non_empty = [ln for ln in lines if ln.strip()]
		if non_empty:
			min_lead = min(len(ln) - len(ln.lstrip(" \t")) for ln in non_empty)
			rebased: List[str] = []
			for ln in lines:
				if not ln.strip():
					rebased.append(ln)
					continue
				lead = ln[: len(ln) - len(ln.lstrip(" \t"))]
				lead_spaces = lead.replace("\t", " " * indent_width)
				content = ln[len(lead) :]
				trimmed = lead_spaces[min_lead:] if min_lead <= len(lead_spaces) else ""
				rebased.append((" " * indent_width) + trimmed + content)
			lines = rebased
	# Trim leading blanks and finalize
	while lines and not lines[0].strip():
		lines.pop(0)
	body = "\n".join(lines).rstrip()
	# Ensure the body compiles; fallback: force uniform indent
	try:
		compile(body, "<completion>", "exec")
	except Exception:
		try:
			collapsed = []
			for ln in body.splitlines():
				collapsed.append(((" " * indent_width) + ln.strip()) if ln.strip() else "")
			body2 = "\n".join(collapsed).rstrip()
			compile(body2, "<completion>", "exec")
			return body2
		except Exception:
			pass
	return body
	raw = (text or "")
	# Extract fenced content if present
	if "```" in raw:
		try:
			start = raw.find("```")
			end = raw.rfind("```")
			if end > start:
				inner = raw[start + 3 : end]
				# strip optional language tag on first line
				first_nl = inner.find("\n")
				if first_nl != -1 and inner[:first_nl].strip().lower().startswith("python"):
					inner = inner[first_nl + 1 :]
				raw = inner
		except Exception:
			pass
	lines = [ln.rstrip("\r\t ") for ln in raw.splitlines()]
	# Remove fence markers or obvious prose residue lines
	clean_lines: List[str] = []
	for ln in lines:
		strip = ln.strip()
		if strip.startswith("```"):
			continue
		# Remove instruction echoes and rubric phrases
		low = strip.lower()
		if (
			low.startswith("here is")
			or low.startswith("the function")
			or low.startswith("complete the python function")
			or low.startswith("rules:")
			or low.startswith("example usage:")
		):
			continue
		clean_lines.append(ln)
	lines = clean_lines
	# If target function is repeated, drop its def and keep its body
	name = extract_target_function_name(prompt or "") or ""
	if name:
		for i, ln in enumerate(lines):
			if ln.lstrip().startswith(f"def {name}"):
				# Keep only the body after this def line
				body = lines[i + 1 :]
				lines = body
				break
	# Preserve nested helpers (def/class) and docstrings; body-only output can include them inside the function.
	# Drop top-level imports in body (only if not indented)
	lines = [ln for ln in lines if not (ln and not ln.startswith((" ", "\t")) and (ln.startswith("import ") or ln.startswith("from ")))]
	# Keep body and any lines that remain indented after the def; cut spill after dedent
	_pre_body_lines = list(lines)
	body_lines: List[str] = []
	body_started = False
	for ln in lines:
		if not body_started and ln.strip():
			# Start when we see first non-empty line (may be indented)
			body_started = True
		if body_started:
			if ln.strip() and not ln.startswith((" ", "\t")):
				# Truncate on first non-indented, non-empty line after body start
				break
			body_lines.append(ln)
	# Fallback: if body is empty because first line was not indented, indent all lines
	if not any(ln.strip() for ln in body_lines) and any(ln.strip() for ln in _pre_body_lines):
		lines = [(("    " + ln.strip()) if ln.strip() else "") for ln in _pre_body_lines]
	else:
		lines = body_lines
	# Normalize indentation: rebase minimal indent to exactly 4 spaces
	non_empty = [ln for ln in lines if ln.strip()]
	if non_empty:
		min_lead = min(len(ln) - len(ln.lstrip(" \t")) for ln in non_empty)
		# Convert tabs to 4 spaces in leading part, then rebase
		rebased: List[str] = []
		for ln in lines:
			if not ln.strip():
				rebased.append(ln)
				continue
			lead = ln[: len(ln) - len(ln.lstrip(" \t"))]
			lead_spaces = lead.replace("\t", "    ")
			content = ln[len(lead) :]
			trimmed = lead_spaces[min_lead:] if min_lead <= len(lead_spaces) else ""
			rebased.append("    " + trimmed + content)
		lines = rebased
	# Join back, trim leading blank lines
	while lines and not lines[0].strip():
		lines.pop(0)
	body = "\n".join(lines).rstrip()
	# Final sanity: try compile; if still syntax error due to indent, collapse to a single block by forcing uniform 4-space indent
	try:
		compile(body, "<completion>", "exec")
	except Exception:
		try:
			collapsed = []
			for ln in body.splitlines():
				if not ln.strip():
					collapsed.append("")
				else:
					collapsed.append("    " + ln.strip())
			body2 = "\n".join(collapsed).rstrip()
			compile(body2, "<completion>", "exec")
			return body2
		except Exception:
			pass
	return body

__all__ = [
	"ensure_safe_env",
	"strip_fences",
	"precheck_code",
	"retry_with_backoff",
	"run_parallel",
	"load_code_index",
	"inject_similar_functions",
	"add_common_bench_args",
	"extract_target_function_name",
	"sanitize_humaneval_completion",
	"sanitize_completion_generic",
]


from __future__ import annotations
import logging

import json
import os
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, TypeVar

T = TypeVar("T")
R = TypeVar("R")

def ensure_safe_env() -> None:
	os.environ.setdefault("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1")
	os.environ.setdefault("PYTHONDONTWRITEBYTECODE", "1")
	# Avoid HF tokenizers fork warnings/deadlocks in worker pools
	os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")

def strip_fences(text: str) -> str:
	s = (text or "").strip()
	if s.startswith("```"):
		try:
			st = s.find("\n"); en = s.rfind("```")
			if st != -1 and en != -1 and en > st:
				return s[st + 1 : en].strip()
		except Exception:
			return s
	return s

def precheck_code(text: str) -> tuple[bool, Optional[str]]:
	try:
		compile(text, "<completion>", "exec")
		return True, None
	except Exception as e:
		return False, str(e)


import json
import os
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, TypeVar

T = TypeVar("T")
R = TypeVar("R")

def ensure_safe_env() -> None:
	os.environ.setdefault("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1")
	os.environ.setdefault("PYTHONDONTWRITEBYTECODE", "1")
	# Avoid HF tokenizers fork warnings/deadlocks in worker pools
	os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")

def strip_fences(text: str) -> str:
	s = (text or "").strip()
	if s.startswith("```"):
		try:
			st = s.find("\n"); en = s.rfind("```")
			if st != -1 and en != -1 and en > st:
				return s[st + 1 : en].strip()
		except Exception:
			return s
	return s

def precheck_code(text: str) -> tuple[bool, Optional[str]]:
	try:
		compile(text, "<completion>", "exec")
		return True, None
	except Exception as e:
		return False, str(e)

def retry_with_backoff(fn: Callable[[], R], retries: int = 1, backoff: float = 0.75) -> R | None:
	attempt = 0
	while attempt <= max(0, retries):
		try:
			return fn()
		except Exception:
			if attempt == retries:
				break
			time.sleep(backoff * (2 ** attempt))
			attempt += 1


def ensure_safe_env() -> None:
	os.environ.setdefault("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1")
	os.environ.setdefault("PYTHONDONTWRITEBYTECODE", "1")
	# Avoid HF tokenizers fork warnings/deadlocks in worker pools
	os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")

def strip_fences(text: str) -> str:
	s = (text or "").strip()
	if s.startswith("```"):
		try:
			st = s.find("\n"); en = s.rfind("```")
			if st != -1 and en != -1 and en > st:
				return s[st + 1 : en].strip()
		except Exception:
			return s
	return s

def precheck_code(text: str) -> tuple[bool, Optional[str]]:
	try:
		compile(text, "<completion>", "exec")
		return True, None
	except Exception as e:
		return False, str(e)

def retry_with_backoff(fn: Callable[[], R], retries: int = 1, backoff: float = 0.75) -> R | None:
	attempt = 0
	while attempt <= max(0, retries):
		try:
			return fn()
		except Exception:
			if attempt == retries:
				break
			time.sleep(backoff * (2 ** attempt))
			attempt += 1
	return None

def run_parallel(items: Iterable[T], worker: Callable[[T], R | None], max_workers: int = 4) -> List[R]:
	max_workers = max(1, int(max_workers or 1))
	results: List[R] = []
	with ThreadPoolExecutor(max_workers=max_workers) as ex:
		futs = [ex.submit(worker, it) for it in items]


def strip_fences(text: str) -> str:
	s = (text or "").strip()
	if s.startswith("```"):
		try:
			st = s.find("\n"); en = s.rfind("```")
			if st != -1 and en != -1 and en > st:
				return s[st + 1 : en].strip()
		except Exception:
			return s
	return s

def precheck_code(text: str) -> tuple[bool, Optional[str]]:
	try:
		compile(text, "<completion>", "exec")
		return True, None
	except Exception as e:
		return False, str(e)

def retry_with_backoff(fn: Callable[[], R], retries: int = 1, backoff: float = 0.75) -> R | None:
	attempt = 0
	while attempt <= max(0, retries):
		try:
			return fn()
		except Exception:
			if attempt == retries:
				break
			time.sleep(backoff * (2 ** attempt))
			attempt += 1
	return None

def run_parallel(items: Iterable[T], worker: Callable[[T], R | None], max_workers: int = 4) -> List[R]:
	max_workers = max(1, int(max_workers or 1))
	results: List[R] = []
	with ThreadPoolExecutor(max_workers=max_workers) as ex:
		futs = [ex.submit(worker, it) for it in items]
		for fut in as_completed(futs):
			try:
				val = fut.result()
			except Exception:
				val = None
			if val is not None:
				results.append(val)
	return results

def load_code_index(index_path: Optional[str]) -> Optional[Dict[str, Any]]:
	if not index_path:
		return None


	try:
		compile(text, "<completion>", "exec")
		return True, None
	except Exception as e:
		return False, str(e)

def retry_with_backoff(fn: Callable[[], R], retries: int = 1, backoff: float = 0.75) -> R | None:
	attempt = 0
	while attempt <= max(0, retries):
		try:
			return fn()
		except Exception:
			if attempt == retries:
				break
			time.sleep(backoff * (2 ** attempt))
			attempt += 1
	return None

def run_parallel(items: Iterable[T], worker: Callable[[T], R | None], max_workers: int = 4) -> List[R]:
	max_workers = max(1, int(max_workers or 1))
	results: List[R] = []
	with ThreadPoolExecutor(max_workers=max_workers) as ex:
		futs = [ex.submit(worker, it) for it in items]
		for fut in as_completed(futs):
			try:
				val = fut.result()
			except Exception:
				val = None
			if val is not None:
				results.append(val)
	return results

def load_code_index(index_path: Optional[str]) -> Optional[Dict[str, Any]]:
	if not index_path:
		return None
	try:
		from agi_dw.tools.code_index import index_python_repo  # type: ignore
		p = Path(index_path)
		root = p.parent if p.is_file() else p
		return index_python_repo(root)
	except Exception:
		return None

def inject_similar_functions(base: str, question: str, code_index: Optional[Dict[str, Any]], k: int) -> str:
	if not code_index or k <= 0:
		return base
	try:
		funcs: List[str] = []
		for fpath, flist in dict(code_index.get("functions", {})).items():


			time.sleep(backoff * (2 ** attempt))
			attempt += 1
	return None

def run_parallel(items: Iterable[T], worker: Callable[[T], R | None], max_workers: int = 4) -> List[R]:
	max_workers = max(1, int(max_workers or 1))
	results: List[R] = []
	with ThreadPoolExecutor(max_workers=max_workers) as ex:
		futs = [ex.submit(worker, it) for it in items]
		for fut in as_completed(futs):
			try:
				val = fut.result()
			except Exception:
				val = None
			if val is not None:
				results.append(val)
	return results

def load_code_index(index_path: Optional[str]) -> Optional[Dict[str, Any]]:
	if not index_path:
		return None
	try:
		from agi_dw.tools.code_index import index_python_repo  # type: ignore
		p = Path(index_path)
		root = p.parent if p.is_file() else p
		return index_python_repo(root)
	except Exception:
		return None

def inject_similar_functions(base: str, question: str, code_index: Optional[Dict[str, Any]], k: int) -> str:
	if not code_index or k <= 0:
		return base
	try:
		funcs: List[str] = []
		for fpath, flist in dict(code_index.get("functions", {})).items():
			for f in flist:
				name = str(f.get("name", "")).lower()
				if any(kw and kw in name for kw in str(question).lower().split()):
					funcs.append(f"{fpath}:{f.get('name')}")
					if len(funcs) >= k:
						break
			if len(funcs) >= k:
				break
		if funcs:
			return "Similar functions:\n" + "\n".join(funcs) + "\n\n" + base
		return base
	except Exception:


				val = fut.result()
			except Exception:
				val = None
			if val is not None:
				results.append(val)
	return results

def load_code_index(index_path: Optional[str]) -> Optional[Dict[str, Any]]:
	if not index_path:
		return None
	try:
		from agi_dw.tools.code_index import index_python_repo  # type: ignore
		p = Path(index_path)
		root = p.parent if p.is_file() else p
		return index_python_repo(root)
	except Exception:
		return None

def inject_similar_functions(base: str, question: str, code_index: Optional[Dict[str, Any]], k: int) -> str:
	if not code_index or k <= 0:
		return base
	try:
		funcs: List[str] = []
		for fpath, flist in dict(code_index.get("functions", {})).items():
			for f in flist:
				name = str(f.get("name", "")).lower()
				if any(kw and kw in name for kw in str(question).lower().split()):
					funcs.append(f"{fpath}:{f.get('name')}")
					if len(funcs) >= k:
						break
			if len(funcs) >= k:
				break
		if funcs:
			return "Similar functions:\n" + "\n".join(funcs) + "\n\n" + base
		return base
	except Exception:
		return base

def add_common_bench_args(
	parser: Any,
	*,
	default_max_workers: int = 4,
	default_retries: int = 1,
	default_retry_backoff: float = 0.75,
	include_precheck: bool = True,
	include_logging: bool = True,
	include_critic: bool = True,
	include_index: bool = True,
) -> Any:
	"""Add common benchmark CLI flags to an argparse parser.

	Returns the same parser for chaining.
	"""
	parser.add_argument("--max-workers", type=int, default=default_max_workers)
	parser.add_argument("--retries", type=int, default=default_retries)
	parser.add_argument("--retry-backoff", type=float, default=default_retry_backoff)


	if not code_index or k <= 0:
		return base
	try:
		funcs: List[str] = []
		for fpath, flist in dict(code_index.get("functions", {})).items():
			for f in flist:
				name = str(f.get("name", "")).lower()
				if any(kw and kw in name for kw in str(question).lower().split()):
					funcs.append(f"{fpath}:{f.get('name')}")
					if len(funcs) >= k:
						break
			if len(funcs) >= k:
				break
		if funcs:
			return "Similar functions:\n" + "\n".join(funcs) + "\n\n" + base
		return base
	except Exception:
		return base

def add_common_bench_args(
	parser: Any,
	*,
	default_max_workers: int = 4,
	default_retries: int = 1,
	default_retry_backoff: float = 0.75,
	include_precheck: bool = True,
	include_logging: bool = True,
	include_critic: bool = True,
	include_index: bool = True,
) -> Any:
	"""Add common benchmark CLI flags to an argparse parser.

	Returns the same parser for chaining.
	"""
	parser.add_argument("--max-workers", type=int, default=default_max_workers)
	parser.add_argument("--retries", type=int, default=default_retries)
	parser.add_argument("--retry-backoff", type=float, default=default_retry_backoff)
	if include_precheck:
		parser.add_argument("--precheck", action="store_true", help="Fast syntax check before writing result")
	if include_logging:
		parser.add_argument("--log-prompts", action="store_true", help="Log prompts and completions safely")
	if include_critic:
		parser.add_argument("--critic", action="store_true", help="Run critic pass to filter high-risk completions")
		parser.add_argument("--critic-model", help="Model to use for critic (default: meta-llama/Llama-3.2-3B)")
	if include_index:
		parser.add_argument("--planner-index-k", type=int, help="Number of similar functions to include from index")
		parser.add_argument("--planner-index-path", help="Path to code index JSON for context injection")
	return parser

import re

def extract_target_function_name(prompt: str) -> Optional[str]:
	"""Extract the first function name from a HumanEval-style prompt."""
	try:
		for line in (prompt or "").splitlines():
			line = line.strip()
			if line.startswith("def "):
				m = re.match(r"def\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(", line)
				if m:
					return m.group(1)
	except Exception:
		return None
	return None

def sanitize_humaneval_completion(text: str, prompt: str) -> str:


	"""
	parser.add_argument("--max-workers", type=int, default=default_max_workers)
	parser.add_argument("--retries", type=int, default=default_retries)
	parser.add_argument("--retry-backoff", type=float, default=default_retry_backoff)
	if include_precheck:
		parser.add_argument("--precheck", action="store_true", help="Fast syntax check before writing result")
	if include_logging:
		parser.add_argument("--log-prompts", action="store_true", help="Log prompts and completions safely")
	if include_critic:
		parser.add_argument("--critic", action="store_true", help="Run critic pass to filter high-risk completions")
		parser.add_argument("--critic-model", help="Model to use for critic (default: meta-llama/Llama-3.2-3B)")
	if include_index:
		parser.add_argument("--planner-index-k", type=int, help="Number of similar functions to include from index")
		parser.add_argument("--planner-index-path", help="Path to code index JSON for context injection")
	return parser

import re

def extract_target_function_name(prompt: str) -> Optional[str]:
	"""Extract the first function name from a HumanEval-style prompt."""
	try:
		for line in (prompt or "").splitlines():
			line = line.strip()
			if line.startswith("def "):
				m = re.match(r"def\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(", line)
				if m:
					return m.group(1)
	except Exception:
		return None
	return None

def sanitize_humaneval_completion(text: str, prompt: str) -> str:
	"""Sanitize LLM completion for HumanEval using generic sanitizer defaults."""
	options = {
		"strip_fences": True,
		"drop_prose_phrases": [
			"here is",
			"the function",
			"complete the python function",
			"rules:",
			"example usage:",
		],
		"drop_top_imports": True,
		"normalize_indent": True,
		"indent_width": 4,
		"enforce_body_mode": True,
		"drop_redef_from_prompt": True,
	}
	return sanitize_completion_generic(text, prompt, options)


	return parser

import re

def extract_target_function_name(prompt: str) -> Optional[str]:
	"""Extract the first function name from a HumanEval-style prompt."""
	try:
		for line in (prompt or "").splitlines():
			line = line.strip()
			if line.startswith("def "):
				m = re.match(r"def\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(", line)
				if m:
					return m.group(1)
	except Exception:
		return None
	return None

def sanitize_humaneval_completion(text: str, prompt: str) -> str:
	"""Sanitize LLM completion for HumanEval using generic sanitizer defaults."""
	options = {
		"strip_fences": True,
		"drop_prose_phrases": [
			"here is",
			"the function",
			"complete the python function",
			"rules:",
			"example usage:",
		],
		"drop_top_imports": True,
		"normalize_indent": True,
		"indent_width": 4,
		"enforce_body_mode": True,
		"drop_redef_from_prompt": True,
	}
	return sanitize_completion_generic(text, prompt, options)

def sanitize_completion_generic(text: str, prompt: str, options: Optional[Dict[str, Any]] = None) -> str:
	"""Generic sanitizer for code-body completions with registry-configurable options.

	Options (all optional):
	- strip_fences: bool (default True)
	- drop_prose_phrases: List[str] of lowercase phrases to remove when present as leading prose lines
	- drop_top_imports: bool (default True) - remove unindented top-level imports in body-only mode
	- normalize_indent: bool (default True) - rebase minimal indent and standardize to indent_width
	- indent_width: int (default 4)
	- enforce_body_mode: bool (default True) - keep only the function body
	- drop_redef_from_prompt: bool (default False) - if true, when the target function appears, drop its def line
	"""
	opts = options or {}
	strip_fences = bool(opts.get("strip_fences", True))
	drop_top_imports = bool(opts.get("drop_top_imports", True))
	normalize_indent = bool(opts.get("normalize_indent", True))
	indent_width = int(opts.get("indent_width", 4) or 4)
	enforce_body_mode = bool(opts.get("enforce_body_mode", True))


def sanitize_humaneval_completion(text: str, prompt: str) -> str:
	"""Sanitize LLM completion for HumanEval using generic sanitizer defaults."""
	options = {
		"strip_fences": True,
		"drop_prose_phrases": [
			"here is",
			"the function",
			"complete the python function",
			"rules:",
			"example usage:",
		],
		"drop_top_imports": True,
		"normalize_indent": True,
		"indent_width": 4,
		"enforce_body_mode": True,
		"drop_redef_from_prompt": True,
	}
	return sanitize_completion_generic(text, prompt, options)

def sanitize_completion_generic(text: str, prompt: str, options: Optional[Dict[str, Any]] = None) -> str:
	"""Generic sanitizer for code-body completions with registry-configurable options.

	Options (all optional):
	- strip_fences: bool (default True)
	- drop_prose_phrases: List[str] of lowercase phrases to remove when present as leading prose lines
	- drop_top_imports: bool (default True) - remove unindented top-level imports in body-only mode
	- normalize_indent: bool (default True) - rebase minimal indent and standardize to indent_width
	- indent_width: int (default 4)
	- enforce_body_mode: bool (default True) - keep only the function body
	- drop_redef_from_prompt: bool (default False) - if true, when the target function appears, drop its def line
	"""
	opts = options or {}
	strip_fences = bool(opts.get("strip_fences", True))
	drop_top_imports = bool(opts.get("drop_top_imports", True))
	normalize_indent = bool(opts.get("normalize_indent", True))
	indent_width = int(opts.get("indent_width", 4) or 4)
	enforce_body_mode = bool(opts.get("enforce_body_mode", True))
	drop_redef_from_prompt = bool(opts.get("drop_redef_from_prompt", False))
	drop_phrases: List[str] = [str(p).lower() for p in (opts.get("drop_prose_phrases") or [])]

	raw = (text or "")
	# Extract fenced content if requested
	if strip_fences and ("```" in raw):
		try:
			start = raw.find("```")
			end = raw.rfind("```")
			if end > start:
				inner = raw[start + 3 : end]
				# strip optional language tag on first line
				first_nl = inner.find("\n")
				if first_nl != -1 and inner[:first_nl].strip().lower().startswith("python"):
					inner = inner[first_nl + 1 :]
				raw = inner
		except Exception:
			pass
	lines = [ln.rstrip("\r\t ") for ln in raw.splitlines()]
	# Remove fence markers or obvious prose residue lines
	clean_lines: List[str] = []
	for ln in lines:
		strip = ln.strip()
		if strip.startswith("```"):
			continue
		low = strip.lower()
		if any(low.startswith(pfx) for pfx in drop_phrases):
			continue
		clean_lines.append(ln)
	lines = clean_lines
	# Optionally drop redefinition of target function (keep body)
	if drop_redef_from_prompt:
		name = extract_target_function_name(prompt or "") or ""
		if name:
			for i, ln in enumerate(lines):
				if ln.lstrip().startswith(f"def {name}"):
					lines = lines[i + 1 :]
					break
	# Drop top-level imports when requested
	if drop_top_imports:
		lines = [ln for ln in lines if not (ln and not ln.startswith((" ", "\t")) and (ln.startswith("import ") or ln.startswith("from ")))]
	# Body mode: keep only indented block starting from first non-empty
	_pre_body_lines = list(lines)
	body_lines: List[str] = []
	body_started = False
	if enforce_body_mode:
		for ln in lines:
			if not body_started and ln.strip():
				body_started = True
			if body_started:
				if ln.strip() and not ln.startswith((" ", "\t")):
					break
				body_lines.append(ln)
		# Fallback: indent everything if body ended up empty
		if not any(ln.strip() for ln in body_lines) and any(ln.strip() for ln in _pre_body_lines):
			lines = [((" " * indent_width) + ln.strip()) if ln.strip() else "" for ln in _pre_body_lines]
		else:
			lines = body_lines
	# Normalize indentation
	if normalize_indent:
		non_empty = [ln for ln in lines if ln.strip()]
		if non_empty:
			min_lead = min(len(ln) - len(ln.lstrip(" \t")) for ln in non_empty)
			rebased: List[str] = []
			for ln in lines:
				if not ln.strip():
					rebased.append(ln)
					continue
				lead = ln[: len(ln) - len(ln.lstrip(" \t"))]
				lead_spaces = lead.replace("\t", " " * indent_width)
				content = ln[len(lead) :]
				trimmed = lead_spaces[min_lead:] if min_lead <= len(lead_spaces) else ""
				rebased.append((" " * indent_width) + trimmed + content)
			lines = rebased
	# Trim leading blanks and finalize
	while lines and not lines[0].strip():
		lines.pop(0)
	body = "\n".join(lines).rstrip()
	# Ensure the body compiles; fallback: force uniform indent
	try:
		compile(body, "<completion>", "exec")
	except Exception:
		try:
			collapsed = []
			for ln in body.splitlines():
				collapsed.append(((" " * indent_width) + ln.strip()) if ln.strip() else "")
			body2 = "\n".join(collapsed).rstrip()
			compile(body2, "<completion>", "exec")
			return body2
		except Exception:
			pass
	return body
	raw = (text or "")
	# Extract fenced content if present
	if "```" in raw:
		try:
			start = raw.find("```")
			end = raw.rfind("```")
			if end > start:
				inner = raw[start + 3 : end]
				# strip optional language tag on first line
				first_nl = inner.find("\n")
				if first_nl != -1 and inner[:first_nl].strip().lower().startswith("python"):
					inner = inner[first_nl + 1 :]
				raw = inner
		except Exception:
			pass
	lines = [ln.rstrip("\r\t ") for ln in raw.splitlines()]
	# Remove fence markers or obvious prose residue lines
	clean_lines: List[str] = []
	for ln in lines:
		strip = ln.strip()
		if strip.startswith("```"):
			continue
		# Remove instruction echoes and rubric phrases
		low = strip.lower()
		if (
			low.startswith("here is")
			or low.startswith("the function")
			or low.startswith("complete the python function")
			or low.startswith("rules:")
			or low.startswith("example usage:")
		):
			continue
		clean_lines.append(ln)
	lines = clean_lines
	# If target function is repeated, drop its def and keep its body
	name = extract_target_function_name(prompt or "") or ""
	if name:
		for i, ln in enumerate(lines):
			if ln.lstrip().startswith(f"def {name}"):
				# Keep only the body after this def line
				body = lines[i + 1 :]
				lines = body
				break
	# Preserve nested helpers (def/class) and docstrings; body-only output can include them inside the function.
	# Drop top-level imports in body (only if not indented)
	lines = [ln for ln in lines if not (ln and not ln.startswith((" ", "\t")) and (ln.startswith("import ") or ln.startswith("from ")))]
	# Keep body and any lines that remain indented after the def; cut spill after dedent
	_pre_body_lines = list(lines)
	body_lines: List[str] = []
	body_started = False
	for ln in lines:
		if not body_started and ln.strip():
			# Start when we see first non-empty line (may be indented)
			body_started = True
		if body_started:
			if ln.strip() and not ln.startswith((" ", "\t")):
				# Truncate on first non-indented, non-empty line after body start
				break
			body_lines.append(ln)
	# Fallback: if body is empty because first line was not indented, indent all lines
	if not any(ln.strip() for ln in body_lines) and any(ln.strip() for ln in _pre_body_lines):
		lines = [(("    " + ln.strip()) if ln.strip() else "") for ln in _pre_body_lines]
	else:
		lines = body_lines
	# Normalize indentation: rebase minimal indent to exactly 4 spaces
	non_empty = [ln for ln in lines if ln.strip()]
	if non_empty:
		min_lead = min(len(ln) - len(ln.lstrip(" \t")) for ln in non_empty)
		# Convert tabs to 4 spaces in leading part, then rebase
		rebased: List[str] = []
		for ln in lines:
			if not ln.strip():
				rebased.append(ln)
				continue
			lead = ln[: len(ln) - len(ln.lstrip(" \t"))]
			lead_spaces = lead.replace("\t", "    ")
			content = ln[len(lead) :]
			trimmed = lead_spaces[min_lead:] if min_lead <= len(lead_spaces) else ""
			rebased.append("    " + trimmed + content)
		lines = rebased
	# Join back, trim leading blank lines
	while lines and not lines[0].strip():
		lines.pop(0)
	body = "\n".join(lines).rstrip()
	# Final sanity: try compile; if still syntax error due to indent, collapse to a single block by forcing uniform 4-space indent
	try:
		compile(body, "<completion>", "exec")
	except Exception:
		try:
			collapsed = []
			for ln in body.splitlines():
				if not ln.strip():
					collapsed.append("")
				else:
					collapsed.append("    " + ln.strip())
			body2 = "\n".join(collapsed).rstrip()
			compile(body2, "<completion>", "exec")
			return body2
		except Exception:
			pass
	return body

__all__ = [
	"ensure_safe_env",
	"strip_fences",
	"precheck_code",
	"retry_with_backoff",
	"run_parallel",
	"load_code_index",
	"inject_similar_functions",
	"add_common_bench_args",
	"extract_target_function_name",
	"sanitize_humaneval_completion",
	"sanitize_completion_generic",
]


from __future__ import annotations
import logging

from pathlib import Path
from typing import Any, Dict, Optional
import math

class WorldModelPrior:
	"""Lightweight wrapper around a joblib-packed WM (TF-IDF + linear models).

	Expects a joblib.dump of a dict with keys: {"vec", "clf", "reg"}.
	- vec: vectorizer with .transform
	- clf: classifier with .predict_proba
	- reg: regressor with .predict
	"""

	def __init__(self, vec: Any, clf: Any, reg: Any, cal: Any | None = None, risk_std: float | None = None) -> None:
		self.vec = vec
		self.clf = clf
		self.reg = reg
		self.cal = cal
		self.risk_std = float(risk_std) if risk_std is not None else None

	@classmethod
	def load(cls, path: str | Path) -> "WorldModelPrior":
		from joblib import load as joblib_load  # type: ignore
		pack = joblib_load(Path(path))
		return cls(vec=pack.get("vec"), clf=pack.get("clf"), reg=pack.get("reg"), cal=pack.get("cal"), risk_std=pack.get("risk_std"))

	def predict_prior(self, obs: Dict[str, Any], plan: Dict[str, Any], action: Dict[str, Any]) -> Optional[Dict[str, float]]:
		try:
			text = " \n ".join([
				str(obs),
				__safe_json(plan),
				__safe_json(action),
			])
			X = self.vec.transform([text]) if hasattr(self.vec, "transform") else None
			if X is None:
				return None
			s_prob = float(self.clf.predict_proba(X)[:, 1][0]) if hasattr(self.clf, "predict_proba") else 0.5
			# Optional probability calibration if available
			if getattr(self, "cal", None) is not None and hasattr(self.cal, "predict"):
				try:
					import numpy as _np  # type: ignore
					s_prob = float(self.cal.predict(_np.asarray([s_prob]))[0])
				except Exception:
					pass
			r_pred = float(self.reg.predict(X)[0]) if hasattr(self.reg, "predict") else 0.5
			r_pred = max(0.0, min(1.0, r_pred))
			out: Dict[str, float] = {"success_prob": s_prob, "risk": r_pred, "success_entropy": _binary_entropy(s_prob)}
			if self.risk_std is not None:
				out["risk_std"] = float(self.risk_std)
			return out
		except Exception:
			return None

	def rank_actions(self, obs: Dict[str, Any], plan: Dict[str, Any], actions: list[Dict[str, Any]]) -> list[Dict[str, Any]]:
		"""Score a list of candidate actions; return sorted list by risk asc, success_prob desc."""
		scored: list[Dict[str, Any]] = []
		for act in actions:
			prior = self.predict_prior(obs, plan, act) or {"success_prob": 0.5, "risk": 0.5}
			scored.append({
				"action": act,
				"success_prob": float(prior.get("success_prob", 0.5)),
				"risk": float(prior.get("risk", 0.5)),
				"success_entropy": float(prior.get("success_entropy", 0.0)),
				"risk_std": float(prior.get("risk_std", 0.0)) if "risk_std" in prior else 0.0,
			})
		scored.sort(key=lambda x: (x.get("risk", 0.5), -x.get("success_prob", 0.0)))
		return scored

def __safe_json(obj: Any) -> str:
	try:
		import json  # type: ignore
		return json.dumps(obj, ensure_ascii=False)
	except Exception:
		return str(obj)

def _binary_entropy(p: float) -> float:
	# Binary entropy in bits
	p = max(1e-6, min(1.0 - 1e-6, float(p)))
	return -p * math.log(p, 2) - (1.0 - p) * math.log(1.0 - p, 2)


			return out
		except Exception:
			return None

	def rank_actions(self, obs: Dict[str, Any], plan: Dict[str, Any], actions: list[Dict[str, Any]]) -> list[Dict[str, Any]]:
		"""Score a list of candidate actions; return sorted list by risk asc, success_prob desc."""
		scored: list[Dict[str, Any]] = []
		for act in actions:
			prior = self.predict_prior(obs, plan, act) or {"success_prob": 0.5, "risk": 0.5}
			scored.append({
				"action": act,
				"success_prob": float(prior.get("success_prob", 0.5)),
				"risk": float(prior.get("risk", 0.5)),
				"success_entropy": float(prior.get("success_entropy", 0.0)),
				"risk_std": float(prior.get("risk_std", 0.0)) if "risk_std" in prior else 0.0,
			})
		scored.sort(key=lambda x: (x.get("risk", 0.5), -x.get("success_prob", 0.0)))
		return scored

def __safe_json(obj: Any) -> str:
	try:
		import json  # type: ignore
		return json.dumps(obj, ensure_ascii=False)
	except Exception:
		return str(obj)

def _binary_entropy(p: float) -> float:
	# Binary entropy in bits
	p = max(1e-6, min(1.0 - 1e-6, float(p)))
	return -p * math.log(p, 2) - (1.0 - p) * math.log(1.0 - p, 2)


			prior = self.predict_prior(obs, plan, act) or {"success_prob": 0.5, "risk": 0.5}
			scored.append({
				"action": act,
				"success_prob": float(prior.get("success_prob", 0.5)),
				"risk": float(prior.get("risk", 0.5)),
				"success_entropy": float(prior.get("success_entropy", 0.0)),
				"risk_std": float(prior.get("risk_std", 0.0)) if "risk_std" in prior else 0.0,
			})
		scored.sort(key=lambda x: (x.get("risk", 0.5), -x.get("success_prob", 0.0)))
		return scored

def __safe_json(obj: Any) -> str:
	try:
		import json  # type: ignore
		return json.dumps(obj, ensure_ascii=False)
	except Exception:
		return str(obj)

def _binary_entropy(p: float) -> float:
	# Binary entropy in bits
	p = max(1e-6, min(1.0 - 1e-6, float(p)))
	return -p * math.log(p, 2) - (1.0 - p) * math.log(1.0 - p, 2)


from __future__ import annotations
import logging

from pathlib import Path
from typing import Any, Dict, Optional
import math

class WorldModelPrior:
	"""Lightweight wrapper around a joblib-packed WM (TF-IDF + linear models).

	Expects a joblib.dump of a dict with keys: {"vec", "clf", "reg"}.
	- vec: vectorizer with .transform
	- clf: classifier with .predict_proba
	- reg: regressor with .predict
	"""

	def __init__(self, vec: Any, clf: Any, reg: Any, cal: Any | None = None, risk_std: float | None = None) -> None:
		self.vec = vec
		self.clf = clf
		self.reg = reg
		self.cal = cal
		self.risk_std = float(risk_std) if risk_std is not None else None

	@classmethod
	def load(cls, path: str | Path) -> "WorldModelPrior":
		from joblib import load as joblib_load  # type: ignore
		pack = joblib_load(Path(path))
		return cls(vec=pack.get("vec"), clf=pack.get("clf"), reg=pack.get("reg"), cal=pack.get("cal"), risk_std=pack.get("risk_std"))

	def predict_prior(self, obs: Dict[str, Any], plan: Dict[str, Any], action: Dict[str, Any]) -> Optional[Dict[str, float]]:
		try:
			text = " \n ".join([
				str(obs),
				__safe_json(plan),
				__safe_json(action),
			])
			X = self.vec.transform([text]) if hasattr(self.vec, "transform") else None
			if X is None:
				return None
			s_prob = float(self.clf.predict_proba(X)[:, 1][0]) if hasattr(self.clf, "predict_proba") else 0.5
			# Optional probability calibration if available
			if getattr(self, "cal", None) is not None and hasattr(self.cal, "predict"):


load

import math

class WorldModelPrior:
	"""Lightweight wrapper around a joblib-packed WM (TF-IDF + linear models).

	Expects a joblib.dump of a dict with keys: {"vec", "clf", "reg"}.
	- vec: vectorizer with .transform
	- clf: classifier with .predict_proba
	- reg: regressor with .predict
	"""

	def __init__(self, vec: Any, clf: Any, reg: Any, cal: Any | None = None, risk_std: float | None = None) -> None:
		self.vec = vec
		self.clf = clf
		self.reg = reg
		self.cal = cal
		self.risk_std = float(risk_std) if risk_std is not None else None

	@classmethod
	def load(cls, path: str | Path) -> "WorldModelPrior":
		from joblib import load as joblib_load  # type: ignore
		pack = joblib_load(Path(path))
		return cls(vec=pack.get("vec"), clf=pack.get("clf"), reg=pack.get("reg"), cal=pack.get("cal"), risk_std=pack.get("risk_std"))

	def predict_prior(self, obs: Dict[str, Any], plan: Dict[str, Any], action: Dict[str, Any]) -> Optional[Dict[str, float]]:
		try:
			text = " \n ".join([
				str(obs),
				__safe_json(plan),
				__safe_json(action),
			])
			X = self.vec.transform([text]) if hasattr(self.vec, "transform") else None
			if X is None:
				return None
			s_prob = float(self.clf.predict_proba(X)[:, 1][0]) if hasattr(self.clf, "predict_proba") else 0.5
			# Optional probability calibration if available
			if getattr(self, "cal", None) is not None and hasattr(self.cal, "predict"):
				try:
					import numpy as _np  # type: ignore
					s_prob = float(self.cal.predict(_np.asarray([s_prob]))[0])
				except Exception:
					pass
			r_pred = float(self.reg.predict(X)[0]) if hasattr(self.reg, "predict") else 0.5


	Expects a joblib.dump of a dict with keys: {"vec", "clf", "reg"}.
	- vec: vectorizer with .transform
	- clf: classifier with .predict_proba
	- reg: regressor with .predict
	"""

	def __init__(self, vec: Any, clf: Any, reg: Any, cal: Any | None = None, risk_std: float | None = None) -> None:
		self.vec = vec
		self.clf = clf
		self.reg = reg
		self.cal = cal
		self.risk_std = float(risk_std) if risk_std is not None else None

	@classmethod
	def load(cls, path: str | Path) -> "WorldModelPrior":
		from joblib import load as joblib_load  # type: ignore
		pack = joblib_load(Path(path))
		return cls(vec=pack.get("vec"), clf=pack.get("clf"), reg=pack.get("reg"), cal=pack.get("cal"), risk_std=pack.get("risk_std"))

	def predict_prior(self, obs: Dict[str, Any], plan: Dict[str, Any], action: Dict[str, Any]) -> Optional[Dict[str, float]]:
		try:
			text = " \n ".join([
				str(obs),
				__safe_json(plan),
				__safe_json(action),
			])
			X = self.vec.transform([text]) if hasattr(self.vec, "transform") else None
			if X is None:
				return None
			s_prob = float(self.clf.predict_proba(X)[:, 1][0]) if hasattr(self.clf, "predict_proba") else 0.5
			# Optional probability calibration if available
			if getattr(self, "cal", None) is not None and hasattr(self.cal, "predict"):
				try:
					import numpy as _np  # type: ignore
					s_prob = float(self.cal.predict(_np.asarray([s_prob]))[0])
				except Exception:
					pass
			r_pred = float(self.reg.predict(X)[0]) if hasattr(self.reg, "predict") else 0.5
			r_pred = max(0.0, min(1.0, r_pred))
			out: Dict[str, float] = {"success_prob": s_prob, "risk": r_pred, "success_entropy": _binary_entropy(s_prob)}
			if self.risk_std is not None:
				out["risk_std"] = float(self.risk_std)
			return out
		except Exception:
			return None

	def rank_actions(self, obs: Dict[str, Any], plan: Dict[str, Any], actions: list[Dict[str, Any]]) -> list[Dict[str, Any]]:
		"""Score a list of candidate actions; return sorted list by risk asc, success_prob desc."""
		scored: list[Dict[str, Any]] = []
		for act in actions:
			prior = self.predict_prior(obs, plan, act) or {"success_prob": 0.5, "risk": 0.5}
			scored.append({
				"action": act,
				"success_prob": float(prior.get("success_prob", 0.5)),
				"risk": float(prior.get("risk", 0.5)),
				"success_entropy": float(prior.get("success_entropy", 0.0)),
				"risk_std": float(prior.get("risk_std", 0.0)) if "risk_std" in prior else 0.0,
			})
		scored.sort(key=lambda x: (x.get("risk", 0.5), -x.get("success_prob", 0.0)))
		return scored

def __safe_json(obj: Any) -> str:
	try:
		import json  # type: ignore


			X = self.vec.transform([text]) if hasattr(self.vec, "transform") else None
			if X is None:
				return None
			s_prob = float(self.clf.predict_proba(X)[:, 1][0]) if hasattr(self.clf, "predict_proba") else 0.5
			# Optional probability calibration if available
			if getattr(self, "cal", None) is not None and hasattr(self.cal, "predict"):
				try:
					import numpy as _np  # type: ignore
					s_prob = float(self.cal.predict(_np.asarray([s_prob]))[0])
				except Exception:
					pass
			r_pred = float(self.reg.predict(X)[0]) if hasattr(self.reg, "predict") else 0.5
			r_pred = max(0.0, min(1.0, r_pred))
			out: Dict[str, float] = {"success_prob": s_prob, "risk": r_pred, "success_entropy": _binary_entropy(s_prob)}
			if self.risk_std is not None:
				out["risk_std"] = float(self.risk_std)
			return out
		except Exception:
			return None

	def rank_actions(self, obs: Dict[str, Any], plan: Dict[str, Any], actions: list[Dict[str, Any]]) -> list[Dict[str, Any]]:
		"""Score a list of candidate actions; return sorted list by risk asc, success_prob desc."""
		scored: list[Dict[str, Any]] = []
		for act in actions:
			prior = self.predict_prior(obs, plan, act) or {"success_prob": 0.5, "risk": 0.5}
			scored.append({
				"action": act,
				"success_prob": float(prior.get("success_prob", 0.5)),
				"risk": float(prior.get("risk", 0.5)),
				"success_entropy": float(prior.get("success_entropy", 0.0)),
				"risk_std": float(prior.get("risk_std", 0.0)) if "risk_std" in prior else 0.0,
			})
		scored.sort(key=lambda x: (x.get("risk", 0.5), -x.get("success_prob", 0.0)))
		return scored

def __safe_json(obj: Any) -> str:
	try:
		import json  # type: ignore
		return json.dumps(obj, ensure_ascii=False)
	except Exception:
		return str(obj)

def _binary_entropy(p: float) -> float:
	# Binary entropy in bits
	p = max(1e-6, min(1.0 - 1e-6, float(p)))
	return -p * math.log(p, 2) - (1.0 - p) * math.log(1.0 - p, 2)


from __future__ import annotations
import logging

from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

from .api import WorldModelPrior
from .rollout_api import RolloutAPI, RolloutConfig
from .next_state import NextStatePredictor

class WorldModelService:
	"""Central service for accessing world model priors and rollouts.

	Encapsulates model loading, prior prediction, and short-horizon rollout
	to provide a uniform interface to callers.
	"""

	def __init__(self, wm: Optional[WorldModelPrior], next_state_predictor: Optional[NextStatePredictor] = None) -> None:
		self._wm = wm
		self._nsp = next_state_predictor or NextStatePredictor()

	@classmethod
	def load_if_exists(cls, path: str | Path) -> Optional["WorldModelService"]:
		try:
			p = Path(path)
			if not p.exists():
				return None
			wm = WorldModelPrior.load(p)
			return cls(wm)
		except Exception:
			return None

	def get_prior(self, obs: Dict[str, Any], plan: Dict[str, Any], action: Dict[str, Any]) -> Optional[Dict[str, float]]:
		if self._wm is None:
			return None
		try:
			return self._wm.predict_prior(obs, plan, action)
		except Exception:
			return None

	def predict_prior(self, obs: Dict[str, Any], plan: Dict[str, Any], action: Dict[str, Any]) -> Dict[str, float]:
		"""Safe prior prediction with conservative defaults on failure."""
		try:
			p = self.get_prior(obs, plan, action) or {}
			return {
				"risk": float(p.get("risk", 0.5)),
				"success_prob": float(p.get("success_prob", 0.5)),
			}
		except Exception:
			return {"risk": 0.5, "success_prob": 0.5}

	def rollout_avg_risk(self, obs: Dict[str, Any], plan: Dict[str, Any], actions: List[Dict[str, Any]], horizon: int = 1) -> Optional[float]:
		"""Run a short-horizon rollout and return the average risk, or None on failure."""
		try:
			api = RolloutAPI(
				world_model=self._wm,
				next_state_predictor=self._nsp,
				config=RolloutConfig(horizon=max(1, int(horizon or 1)), enable_early_stopping=False, track_state_changes=True),
			)
			res = api.simulate_plan(initial_obs=obs, plan=plan, actions=list(actions or []))
			return float(res.avg_risk)
		except Exception:
			return None

	def rollout(self, obs: Dict[str, Any], plan: Dict[str, Any], actions: List[Dict[str, Any]], horizon: int = 1) -> Optional[Dict[str, Any]]:
		"""Run a short-horizon rollout and return a dict with steps and metrics."""
		try:
			api = RolloutAPI(
				world_model=self._wm,
				next_state_predictor=self._nsp,
				config=RolloutConfig(horizon=max(1, int(horizon or 1)), enable_early_stopping=False, track_state_changes=True),
			)
			res = api.simulate_plan(initial_obs=obs, plan=plan, actions=list(actions or []))
			return {
				"avg_risk": float(res.avg_risk),
				"cumulative_risk": float(res.cumulative_risk),
				"steps": [
					{
						"idx": int(s.idx),
						"action": dict(s.action or {}),
						"metrics": dict(s.metrics or {}),
						"observation": dict(s.observation or {}),
						"effects": dict(s.effects or {}),
					}
					for s in res.steps
				],
			}
		except Exception:
			return None


from __future__ import annotations
import logging

from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

from .api import WorldModelPrior
from .rollout_api import RolloutAPI, RolloutConfig
from .next_state import NextStatePredictor

class WorldModelService:
	"""Central service for accessing world model priors and rollouts.

	Encapsulates model loading, prior prediction, and short-horizon rollout
	to provide a uniform interface to callers.
	"""

	def __init__(self, wm: Optional[WorldModelPrior], next_state_predictor: Optional[NextStatePredictor] = None) -> None:
		self._wm = wm
		self._nsp = next_state_predictor or NextStatePredictor()

	@classmethod
	def load_if_exists(cls, path: str | Path) -> Optional["WorldModelService"]:
		try:
			p = Path(path)
			if not p.exists():
				return None
			wm = WorldModelPrior.load(p)
			return cls(wm)
		except Exception:
			return None

	def get_prior(self, obs: Dict[str, Any], plan: Dict[str, Any], action: Dict[str, Any]) -> Optional[Dict[str, float]]:
		if self._wm is None:
			return None
		try:
			return self._wm.predict_prior(obs, plan, action)
		except Exception:
			return None


from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

from .api import WorldModelPrior
from .rollout_api import RolloutAPI, RolloutConfig
from .next_state import NextStatePredictor

class WorldModelService:
	"""Central service for accessing world model priors and rollouts.

	Encapsulates model loading, prior prediction, and short-horizon rollout
	to provide a uniform interface to callers.
	"""

	def __init__(self, wm: Optional[WorldModelPrior], next_state_predictor: Optional[NextStatePredictor] = None) -> None:
		self._wm = wm
		self._nsp = next_state_predictor or NextStatePredictor()

	@classmethod
	def load_if_exists(cls, path: str | Path) -> Optional["WorldModelService"]:
		try:
			p = Path(path)
			if not p.exists():
				return None
			wm = WorldModelPrior.load(p)
			return cls(wm)
		except Exception:
			return None

	def get_prior(self, obs: Dict[str, Any], plan: Dict[str, Any], action: Dict[str, Any]) -> Optional[Dict[str, float]]:
		if self._wm is None:
			return None
		try:
			return self._wm.predict_prior(obs, plan, action)
		except Exception:
			return None

	def predict_prior(self, obs: Dict[str, Any], plan: Dict[str, Any], action: Dict[str, Any]) -> Dict[str, float]:
		"""Safe prior prediction with conservative defaults on failure."""
		try:
			p = self.get_prior(obs, plan, action) or {}
			return {
				"risk": float(p.get("risk", 0.5)),
				"success_prob": float(p.get("success_prob", 0.5)),
			}
		except Exception:
			return {"risk": 0.5, "success_prob": 0.5}


	Encapsulates model loading, prior prediction, and short-horizon rollout
	to provide a uniform interface to callers.
	"""

	def __init__(self, wm: Optional[WorldModelPrior], next_state_predictor: Optional[NextStatePredictor] = None) -> None:
		self._wm = wm
		self._nsp = next_state_predictor or NextStatePredictor()

	@classmethod
	def load_if_exists(cls, path: str | Path) -> Optional["WorldModelService"]:
		try:
			p = Path(path)
			if not p.exists():
				return None
			wm = WorldModelPrior.load(p)
			return cls(wm)
		except Exception:
			return None

	def get_prior(self, obs: Dict[str, Any], plan: Dict[str, Any], action: Dict[str, Any]) -> Optional[Dict[str, float]]:
		if self._wm is None:
			return None
		try:
			return self._wm.predict_prior(obs, plan, action)
		except Exception:
			return None

	def predict_prior(self, obs: Dict[str, Any], plan: Dict[str, Any], action: Dict[str, Any]) -> Dict[str, float]:
		"""Safe prior prediction with conservative defaults on failure."""
		try:
			p = self.get_prior(obs, plan, action) or {}
			return {
				"risk": float(p.get("risk", 0.5)),
				"success_prob": float(p.get("success_prob", 0.5)),
			}
		except Exception:
			return {"risk": 0.5, "success_prob": 0.5}

	def rollout_avg_risk(self, obs: Dict[str, Any], plan: Dict[str, Any], actions: List[Dict[str, Any]], horizon: int = 1) -> Optional[float]:
		"""Run a short-horizon rollout and return the average risk, or None on failure."""
		try:
			api = RolloutAPI(
				world_model=self._wm,
				next_state_predictor=self._nsp,
				config=RolloutConfig(horizon=max(1, int(horizon or 1)), enable_early_stopping=False, track_state_changes=True),
			)


	@classmethod
	def load_if_exists(cls, path: str | Path) -> Optional["WorldModelService"]:
		try:
			p = Path(path)
			if not p.exists():
				return None
			wm = WorldModelPrior.load(p)
			return cls(wm)
		except Exception:
			return None

	def get_prior(self, obs: Dict[str, Any], plan: Dict[str, Any], action: Dict[str, Any]) -> Optional[Dict[str, float]]:
		if self._wm is None:
			return None
		try:
			return self._wm.predict_prior(obs, plan, action)
		except Exception:
			return None

	def predict_prior(self, obs: Dict[str, Any], plan: Dict[str, Any], action: Dict[str, Any]) -> Dict[str, float]:
		"""Safe prior prediction with conservative defaults on failure."""
		try:
			p = self.get_prior(obs, plan, action) or {}
			return {
				"risk": float(p.get("risk", 0.5)),
				"success_prob": float(p.get("success_prob", 0.5)),
			}
		except Exception:
			return {"risk": 0.5, "success_prob": 0.5}

	def rollout_avg_risk(self, obs: Dict[str, Any], plan: Dict[str, Any], actions: List[Dict[str, Any]], horizon: int = 1) -> Optional[float]:
		"""Run a short-horizon rollout and return the average risk, or None on failure."""
		try:
			api = RolloutAPI(
				world_model=self._wm,
				next_state_predictor=self._nsp,
				config=RolloutConfig(horizon=max(1, int(horizon or 1)), enable_early_stopping=False, track_state_changes=True),
			)
			res = api.simulate_plan(initial_obs=obs, plan=plan, actions=list(actions or []))
			return float(res.avg_risk)
		except Exception:
			return None

	def rollout(self, obs: Dict[str, Any], plan: Dict[str, Any], actions: List[Dict[str, Any]], horizon: int = 1) -> Optional[Dict[str, Any]]:
		"""Run a short-horizon rollout and return a dict with steps and metrics."""
		try:
			api = RolloutAPI(
				world_model=self._wm,
				next_state_predictor=self._nsp,


	def get_prior(self, obs: Dict[str, Any], plan: Dict[str, Any], action: Dict[str, Any]) -> Optional[Dict[str, float]]:
		if self._wm is None:
			return None
		try:
			return self._wm.predict_prior(obs, plan, action)
		except Exception:
			return None

	def predict_prior(self, obs: Dict[str, Any], plan: Dict[str, Any], action: Dict[str, Any]) -> Dict[str, float]:
		"""Safe prior prediction with conservative defaults on failure."""
		try:
			p = self.get_prior(obs, plan, action) or {}
			return {
				"risk": float(p.get("risk", 0.5)),
				"success_prob": float(p.get("success_prob", 0.5)),
			}
		except Exception:
			return {"risk": 0.5, "success_prob": 0.5}

	def rollout_avg_risk(self, obs: Dict[str, Any], plan: Dict[str, Any], actions: List[Dict[str, Any]], horizon: int = 1) -> Optional[float]:
		"""Run a short-horizon rollout and return the average risk, or None on failure."""
		try:
			api = RolloutAPI(
				world_model=self._wm,
				next_state_predictor=self._nsp,
				config=RolloutConfig(horizon=max(1, int(horizon or 1)), enable_early_stopping=False, track_state_changes=True),
			)
			res = api.simulate_plan(initial_obs=obs, plan=plan, actions=list(actions or []))
			return float(res.avg_risk)
		except Exception:
			return None

	def rollout(self, obs: Dict[str, Any], plan: Dict[str, Any], actions: List[Dict[str, Any]], horizon: int = 1) -> Optional[Dict[str, Any]]:
		"""Run a short-horizon rollout and return a dict with steps and metrics."""
		try:
			api = RolloutAPI(
				world_model=self._wm,
				next_state_predictor=self._nsp,
				config=RolloutConfig(horizon=max(1, int(horizon or 1)), enable_early_stopping=False, track_state_changes=True),
			)
			res = api.simulate_plan(initial_obs=obs, plan=plan, actions=list(actions or []))
			return {
				"avg_risk": float(res.avg_risk),
				"cumulative_risk": float(res.cumulative_risk),
				"steps": [
					{
						"idx": int(s.idx),
						"action": dict(s.action or {}),
						"metrics": dict(s.metrics or {}),
						"observation": dict(s.observation or {}),
						"effects": dict(s.effects or {}),


rollout

			return {
				"risk": float(p.get("risk", 0.5)),
				"success_prob": float(p.get("success_prob", 0.5)),
			}
		except Exception:
			return {"risk": 0.5, "success_prob": 0.5}

	def rollout_avg_risk(self, obs: Dict[str, Any], plan: Dict[str, Any], actions: List[Dict[str, Any]], horizon: int = 1) -> Optional[float]:
		"""Run a short-horizon rollout and return the average risk, or None on failure."""
		try:
			api = RolloutAPI(
				world_model=self._wm,
				next_state_predictor=self._nsp,
				config=RolloutConfig(horizon=max(1, int(horizon or 1)), enable_early_stopping=False, track_state_changes=True),
			)
			res = api.simulate_plan(initial_obs=obs, plan=plan, actions=list(actions or []))
			return float(res.avg_risk)
		except Exception:
			return None

	def rollout(self, obs: Dict[str, Any], plan: Dict[str, Any], actions: List[Dict[str, Any]], horizon: int = 1) -> Optional[Dict[str, Any]]:
		"""Run a short-horizon rollout and return a dict with steps and metrics."""
		try:
			api = RolloutAPI(
				world_model=self._wm,
				next_state_predictor=self._nsp,
				config=RolloutConfig(horizon=max(1, int(horizon or 1)), enable_early_stopping=False, track_state_changes=True),
			)
			res = api.simulate_plan(initial_obs=obs, plan=plan, actions=list(actions or []))
			return {
				"avg_risk": float(res.avg_risk),
				"cumulative_risk": float(res.cumulative_risk),
				"steps": [
					{
						"idx": int(s.idx),
						"action": dict(s.action or {}),
						"metrics": dict(s.metrics or {}),
						"observation": dict(s.observation or {}),
						"effects": dict(s.effects or {}),
					}
					for s in res.steps
				],
			}
		except Exception:
			return None


from __future__ import annotations
import logging

from typing import Dict, Any, List, Optional

from .rollout_api import RolloutAPI, RolloutConfig

class ShortHorizonRollout:
	"""[DEPRECATED] Use WorldModelService instead.

	This class is deprecated and will be removed in a future version.
	Please use WorldModelService from service.py which provides a consolidated
	interface for world model operations including rollouts.

	Legacy docstring:
	k-step abstract action rollout using the world model prior.
	Extends scoring by producing a synthetic next observation per step via a
	simple next-state predictor. Designed for planning-only simulations.
	"""

	def __init__(self, wm_prior, next_state_predictor: Any | None = None) -> None:
		import warnings
		warnings.warn(
			"ShortHorizonRollout is deprecated. Use WorldModelService from service.py instead.",
			DeprecationWarning,
			stacklevel=2
		)
		self.wm = wm_prior
		# Lazy import to avoid hard dependency when not needed
		self._nsp = next_state_predictor

	def simulate(self, obs: Dict[str, Any], plan: Dict[str, Any], actions: List[Dict[str, Any]], horizon: int = 3) -> Dict[str, Any]:
		# Delegate to consolidated RolloutAPI while preserving legacy return shape
		effective_horizon = max(1, int(horizon))
		config = RolloutConfig(
			horizon=effective_horizon,
			# Disable early stopping to mimic legacy behavior of fixed-step rollout
			enable_early_stopping=False,
			track_state_changes=True,
		)
		api = RolloutAPI(
			world_model=self.wm,
			next_state_predictor=self._nsp,
			config=config,
		)
		res = api.simulate_plan(
			initial_obs=obs,
			plan=plan,
			actions=list(actions or []),
		)
		legacy_steps: List[Dict[str, Any]] = []
		for s in res.steps:
			metrics = dict(s.metrics or {})
			legacy_steps.append({
				"idx": int(s.idx),
				"action": dict(s.action or {}),
				"prior": {
					"risk": float(metrics.get("risk", 0.5)),
					"success_prob": float(metrics.get("success_prob", 0.5)),
					"success_entropy": float(metrics.get("success_entropy", 0.0)),
				},
				"next_obs": dict(s.observation or {}),
				"effects": dict(s.effects or {}),
			})
		return {
			"steps": legacy_steps,
			"cumulative_risk": float(res.cumulative_risk),
			"avg_risk": float(res.avg_risk),
		}


from __future__ import annotations
import logging

from typing import Dict, Any, List, Optional

from .rollout_api import RolloutAPI, RolloutConfig

class ShortHorizonRollout:
	"""[DEPRECATED] Use WorldModelService instead.

	This class is deprecated and will be removed in a future version.
	Please use WorldModelService from service.py which provides a consolidated
	interface for world model operations including rollouts.

	Legacy docstring:
	k-step abstract action rollout using the world model prior.
	Extends scoring by producing a synthetic next observation per step via a
	simple next-state predictor. Designed for planning-only simulations.
	"""

	def __init__(self, wm_prior, next_state_predictor: Any | None = None) -> None:
		import warnings
		warnings.warn(
			"ShortHorizonRollout is deprecated. Use WorldModelService from service.py instead.",
			DeprecationWarning,
			stacklevel=2
		)
		self.wm = wm_prior
		# Lazy import to avoid hard dependency when not needed
		self._nsp = next_state_predictor

	def simulate(self, obs: Dict[str, Any], plan: Dict[str, Any], actions: List[Dict[str, Any]], horizon: int = 3) -> Dict[str, Any]:
		# Delegate to consolidated RolloutAPI while preserving legacy return shape
		effective_horizon = max(1, int(horizon))
		config = RolloutConfig(
			horizon=effective_horizon,
			# Disable early stopping to mimic legacy behavior of fixed-step rollout
			enable_early_stopping=False,
			track_state_changes=True,
		)
		api = RolloutAPI(
			world_model=self.wm,
			next_state_predictor=self._nsp,
			config=config,
		)
		res = api.simulate_plan(
			initial_obs=obs,
			plan=plan,
			actions=list(actions or []),
		)


simulate

	Please use WorldModelService from service.py which provides a consolidated
	interface for world model operations including rollouts.

	Legacy docstring:
	k-step abstract action rollout using the world model prior.
	Extends scoring by producing a synthetic next observation per step via a
	simple next-state predictor. Designed for planning-only simulations.
	"""

	def __init__(self, wm_prior, next_state_predictor: Any | None = None) -> None:
		import warnings
		warnings.warn(
			"ShortHorizonRollout is deprecated. Use WorldModelService from service.py instead.",
			DeprecationWarning,
			stacklevel=2
		)
		self.wm = wm_prior
		# Lazy import to avoid hard dependency when not needed
		self._nsp = next_state_predictor

	def simulate(self, obs: Dict[str, Any], plan: Dict[str, Any], actions: List[Dict[str, Any]], horizon: int = 3) -> Dict[str, Any]:
		# Delegate to consolidated RolloutAPI while preserving legacy return shape
		effective_horizon = max(1, int(horizon))
		config = RolloutConfig(
			horizon=effective_horizon,
			# Disable early stopping to mimic legacy behavior of fixed-step rollout
			enable_early_stopping=False,
			track_state_changes=True,
		)
		api = RolloutAPI(
			world_model=self.wm,
			next_state_predictor=self._nsp,
			config=config,
		)
		res = api.simulate_plan(
			initial_obs=obs,
			plan=plan,
			actions=list(actions or []),
		)
		legacy_steps: List[Dict[str, Any]] = []
		for s in res.steps:
			metrics = dict(s.metrics or {})
			legacy_steps.append({
				"idx": int(s.idx),
				"action": dict(s.action or {}),
				"prior": {
					"risk": float(metrics.get("risk", 0.5)),
					"success_prob": float(metrics.get("success_prob", 0.5)),
					"success_entropy": float(metrics.get("success_entropy", 0.0)),
				},
				"next_obs": dict(s.observation or {}),
				"effects": dict(s.effects or {}),
			})
		return {
			"steps": legacy_steps,
			"cumulative_risk": float(res.cumulative_risk),
			"avg_risk": float(res.avg_risk),
		}


from __future__ import annotations
import logging

from typing import Dict, Any, List, Optional

class OffPolicyProposer:
	"""Use WorldModelPrior to rank and propose low-risk actions off-policy.

	For CLI: proposes between IL NN and T5 candidates.
	For DOM: proposes between NN and T5 candidates.
	"""

	def __init__(self, wm) -> None:
		self.wm = wm

	def propose_cli(self, obs: Dict[str, Any], plan: Dict[str, Any], il_data_path: str, t5_model_path: str) -> Dict[str, Any]:
		from agi_dw.core.actuator.service import ActuatorConfig, RouterExtras, select_action  # type: ignore
		# Build NN and T5 candidates via centralized service
		cfg_nn = ActuatorConfig(mode="nn", il_path=str(il_data_path))
		extra = RouterExtras(domain="cli")
		cand_nn, _ = select_action(obs, plan, cfg_nn, extra)
		cfg_t5 = ActuatorConfig(mode="t5", t5_model=str(t5_model_path))
		cand_t5, _ = select_action(obs, plan, cfg_t5, extra)
		cands: List[Dict[str, Any]] = []
		if isinstance(cand_nn, dict):
			cands.append(cand_nn)
		if isinstance(cand_t5, dict):
			cands.append(cand_t5)
		if not cands:
			return {"proposed": None, "scored": []}
		scored = self.wm.rank_actions(obs, plan, cands)
		return {"proposed": scored[0]["action"] if scored else None, "scored": scored}

	def propose_dom(self, obs: Dict[str, Any], plan: Dict[str, Any], il_data_path: str, t5_model_path: str) -> Dict[str, Any]:
		from agi_dw.core.actuator.service import ActuatorConfig, RouterExtras, select_action  # type: ignore
		cfg_nn = ActuatorConfig(mode="nn", il_path=str(il_data_path))
		extra = RouterExtras(domain="dom")
		cand_nn, _ = select_action(obs, plan, cfg_nn, extra)
		cfg_t5 = ActuatorConfig(mode="t5", t5_model=str(t5_model_path), dom_structured=True)
		cand_t5, _ = select_action(obs, plan, cfg_t5, extra)
		cands: List[Dict[str, Any]] = []
		if isinstance(cand_nn, dict):
			cands.append(cand_nn)
		if isinstance(cand_t5, dict):
			cands.append(cand_t5)
		if not cands:
			return {"proposed": None, "scored": []}
		scored = self.wm.rank_actions(obs, plan, cands)
		return {"proposed": scored[0]["action"] if scored else None, "scored": scored}


from __future__ import annotations
import logging

from typing import Dict, Any, List, Optional

class OffPolicyProposer:
	"""Use WorldModelPrior to rank and propose low-risk actions off-policy.

	For CLI: proposes between IL NN and T5 candidates.
	For DOM: proposes between NN and T5 candidates.
	"""

	def __init__(self, wm) -> None:
		self.wm = wm

	def propose_cli(self, obs: Dict[str, Any], plan: Dict[str, Any], il_data_path: str, t5_model_path: str) -> Dict[str, Any]:
		from agi_dw.core.actuator.service import ActuatorConfig, RouterExtras, select_action  # type: ignore
		# Build NN and T5 candidates via centralized service
		cfg_nn = ActuatorConfig(mode="nn", il_path=str(il_data_path))
		extra = RouterExtras(domain="cli")
		cand_nn, _ = select_action(obs, plan, cfg_nn, extra)
		cfg_t5 = ActuatorConfig(mode="t5", t5_model=str(t5_model_path))
		cand_t5, _ = select_action(obs, plan, cfg_t5, extra)
		cands: List[Dict[str, Any]] = []
		if isinstance(cand_nn, dict):
			cands.append(cand_nn)
		if isinstance(cand_t5, dict):
			cands.append(cand_t5)
		if not cands:
			return {"proposed": None, "scored": []}
		scored = self.wm.rank_actions(obs, plan, cands)
		return {"proposed": scored[0]["action"] if scored else None, "scored": scored}

	def propose_dom(self, obs: Dict[str, Any], plan: Dict[str, Any], il_data_path: str, t5_model_path: str) -> Dict[str, Any]:


		self.wm = wm

	def propose_cli(self, obs: Dict[str, Any], plan: Dict[str, Any], il_data_path: str, t5_model_path: str) -> Dict[str, Any]:
		from agi_dw.core.actuator.service import ActuatorConfig, RouterExtras, select_action  # type: ignore
		# Build NN and T5 candidates via centralized service
		cfg_nn = ActuatorConfig(mode="nn", il_path=str(il_data_path))
		extra = RouterExtras(domain="cli")
		cand_nn, _ = select_action(obs, plan, cfg_nn, extra)
		cfg_t5 = ActuatorConfig(mode="t5", t5_model=str(t5_model_path))
		cand_t5, _ = select_action(obs, plan, cfg_t5, extra)
		cands: List[Dict[str, Any]] = []
		if isinstance(cand_nn, dict):
			cands.append(cand_nn)
		if isinstance(cand_t5, dict):
			cands.append(cand_t5)
		if not cands:
			return {"proposed": None, "scored": []}
		scored = self.wm.rank_actions(obs, plan, cands)
		return {"proposed": scored[0]["action"] if scored else None, "scored": scored}

	def propose_dom(self, obs: Dict[str, Any], plan: Dict[str, Any], il_data_path: str, t5_model_path: str) -> Dict[str, Any]:
		from agi_dw.core.actuator.service import ActuatorConfig, RouterExtras, select_action  # type: ignore
		cfg_nn = ActuatorConfig(mode="nn", il_path=str(il_data_path))
		extra = RouterExtras(domain="dom")
		cand_nn, _ = select_action(obs, plan, cfg_nn, extra)
		cfg_t5 = ActuatorConfig(mode="t5", t5_model=str(t5_model_path), dom_structured=True)
		cand_t5, _ = select_action(obs, plan, cfg_t5, extra)
		cands: List[Dict[str, Any]] = []
		if isinstance(cand_nn, dict):
			cands.append(cand_nn)
		if isinstance(cand_t5, dict):
			cands.append(cand_t5)
		if not cands:
			return {"proposed": None, "scored": []}
		scored = self.wm.rank_actions(obs, plan, cands)
		return {"proposed": scored[0]["action"] if scored else None, "scored": scored}


from __future__ import annotations
import logging
import json
import re
from typing import Dict, Any, Tuple, Optional

class NextStatePredictor:
	"""Next-state predictor for world model rollouts.

	Predicts observation changes and side effects from actions:
	- DOM: navigation, form states, click effects
	- CLI: commands, working directory, output types
	- Generic: action metadata and timestamps
	"""

	def predict_next(self, obs: Dict[str, Any], action: Dict[str, Any]) -> Tuple[Dict[str, Any], Dict[str, Any]]:
		try:
			obs_next: Dict[str, Any] = dict(obs or {})
			effects: Dict[str, Any] = {}

			# Convert action to string for pattern matching
			a_json = _safe_json(action)

			# DOM action handling
			if self._is_dom_action(a_json):
				obs_next, effects = self._handle_dom_action(obs_next, action, a_json)

			# CLI action handling
			elif self._is_cli_action(a_json):
				obs_next, effects = self._handle_cli_action(obs_next, action, a_json)

			# Record action metadata
			self._record_action_metadata(obs_next, action)

			return obs_next, effects
		except Exception as e:
			# Log error but continue with safe fallback
			effects = {"error": str(e)}
			return dict(obs or {}), effects

	def _is_dom_action(self, a_json: str) -> bool:
		"""Check if action is DOM-related."""
		dom_patterns = [
			"browser.read",
			"browser.click",
			"browser.fill",
			"url",
			"selector",
			"form",
			"button",
			"input"
		]
		return any(p in a_json for p in dom_patterns)

	def _is_cli_action(self, a_json: str) -> bool:
		"""Check if action is CLI-related."""
		cli_patterns = [
			"argv",
			"tool",
			"cmd",
			"shell",
			"terminal",
			"exec"
		]
		return any(p in a_json for p in cli_patterns)

	def _handle_dom_action(
		self, obs_next: Dict[str, Any], action: Dict[str, Any], a_json: str
	) -> Tuple[Dict[str, Any], Dict[str, Any]]:
		"""Handle DOM-related actions and predict state changes."""
		effects: Dict[str, Any] = {}

		# Extract basic DOM info
		url = _extract_between(a_json, "http", '"')
		selector = _extract_key(a_json, "selector")

		# Track navigation
		if url:
			obs_next["last_url"] = url
			effects["url"] = url
			# Clear form state on navigation
			obs_next.pop("form_state", None)

		# Track element interaction
		if selector:
			obs_next["last_selector"] = selector
			effects["selector"] = selector

			# Predict form state changes
			if "fill" in a_json:
				form_state = obs_next.get("form_state", {})
				value = _extract_key(a_json, "value")
				if value:
					form_state[selector] = value
					obs_next["form_state"] = form_state
					effects["form_update"] = {selector: value}

			# Predict click effects
			elif "click" in a_json:
				effects["clicked"] = selector
				# Clear form state if it looks like a submit
				if re.search(r"submit|login|signup|send|save", selector.lower()):
					obs_next.pop("form_state", None)
					effects["form_submit"] = True

		return obs_next, effects

	def _handle_cli_action(
		self, obs_next: Dict[str, Any], action: Dict[str, Any], a_json: str
	) -> Tuple[Dict[str, Any], Dict[str, Any]]:
		"""Handle CLI-related actions and predict command output."""
		effects: Dict[str, Any] = {}

		# Extract command info
		cmd = _extract_argv0(a_json)
		if cmd:
			obs_next["last_cmd"] = cmd
			effects["cmd"] = cmd

			# Predict common command patterns
			if cmd in ["ls", "dir"]:
				effects["output_type"] = "file_list"
			elif cmd in ["cat", "type"]:
				effects["output_type"] = "file_content"
			elif cmd in ["grep", "find"]:
				effects["output_type"] = "search_results"
			elif cmd in ["git"]:
				effects["output_type"] = "git_status"

			# Track working directory
			if cmd == "cd":
				try:
					args = json.loads(a_json).get("argv", [])
					if len(args) > 1:
						new_dir = args[1]
						obs_next["cwd"] = new_dir
						effects["cwd"] = new_dir
				except Exception:
					pass

		return obs_next, effects

	def _record_action_metadata(self, obs_next: Dict[str, Any], action: Dict[str, Any]) -> None:
		"""Record general action metadata in observation."""
		try:
			if isinstance(action, dict) and action:
				obs_next["last_action_kind"] = next(iter(action.keys()))
				if "timestamp" in action:
					obs_next["last_action_time"] = action["timestamp"]
		except Exception:
			pass

def _safe_json(obj: Any) -> str:
	"""Safely convert object to JSON string."""
	try:
		return json.dumps(obj, ensure_ascii=False)
	except Exception:
		return str(obj)

def _extract_between(s: str, start_token: str, end_token: str) -> Optional[str]:
	"""Extract substring between tokens with better error handling."""
	try:
		start = s.find(start_token)
		if start == -1:
			return None
		end = s.find(end_token, start)
		if end == -1:
			end = len(s)
		return s[start:end][:256]
	except Exception:
		return None

def _extract_key(s: str, key: str) -> Optional[str]:
	"""Extract JSON key value with validation."""
	try:
		pattern = r"\b" + re.escape(key) + r"\"\s*:\s*\"([^\"]+)\""
		m = re.search(pattern, s)
		if not m:
			return None
		value = m.group(1)[:256]
		return value if value.strip() else None
	except Exception:
		return None

def _extract_argv0(s: str) -> Optional[str]:
	"""Extract first argument from argv array."""
	try:
		m = re.search(r"\[\s*\"([^\"]+)\"", s)
		if not m:
			return None
		cmd = m.group(1)[:128]
		return cmd if cmd.strip() else None
	except Exception:
		return None


NextStatePredictor

from __future__ import annotations
import logging
import json
import re
from typing import Dict, Any, Tuple, Optional

class NextStatePredictor:
	"""Next-state predictor for world model rollouts.

	Predicts observation changes and side effects from actions:
	- DOM: navigation, form states, click effects
	- CLI: commands, working directory, output types
	- Generic: action metadata and timestamps
	"""

	def predict_next(self, obs: Dict[str, Any], action: Dict[str, Any]) -> Tuple[Dict[str, Any], Dict[str, Any]]:
		try:
			obs_next: Dict[str, Any] = dict(obs or {})
			effects: Dict[str, Any] = {}

			# Convert action to string for pattern matching
			a_json = _safe_json(action)

			# DOM action handling
			if self._is_dom_action(a_json):
				obs_next, effects = self._handle_dom_action(obs_next, action, a_json)

			# CLI action handling
			elif self._is_cli_action(a_json):
				obs_next, effects = self._handle_cli_action(obs_next, action, a_json)

			# Record action metadata
			self._record_action_metadata(obs_next, action)

			return obs_next, effects
		except Exception as e:
			# Log error but continue with safe fallback
			effects = {"error": str(e)}
			return dict(obs or {}), effects

	def _is_dom_action(self, a_json: str) -> bool:
		"""Check if action is DOM-related."""
		dom_patterns = [
			"browser.read",
			"browser.click",
			"browser.fill",
			"url",
			"selector",
			"form",
			"button",
			"input"
		]
		return any(p in a_json for p in dom_patterns)

	def _is_cli_action(self, a_json: str) -> bool:
		"""Check if action is CLI-related."""
		cli_patterns = [
			"argv",
			"tool",
			"cmd",
			"shell",
			"terminal",
			"exec"
		]
		return any(p in a_json for p in cli_patterns)

	def _handle_dom_action(
		self, obs_next: Dict[str, Any], action: Dict[str, Any], a_json: str
	) -> Tuple[Dict[str, Any], Dict[str, Any]]:
		"""Handle DOM-related actions and predict state changes."""
		effects: Dict[str, Any] = {}

		# Extract basic DOM info
		url = _extract_between(a_json, "http", '"')
		selector = _extract_key(a_json, "selector")

		# Track navigation
		if url:
			obs_next["last_url"] = url
			effects["url"] = url
			# Clear form state on navigation
			obs_next.pop("form_state", None)

		# Track element interaction
		if selector:
			obs_next["last_selector"] = selector
			effects["selector"] = selector

			# Predict form state changes
			if "fill" in a_json:
				form_state = obs_next.get("form_state", {})
				value = _extract_key(a_json, "value")
				if value:
					form_state[selector] = value
					obs_next["form_state"] = form_state
					effects["form_update"] = {selector: value}

			# Predict click effects
			elif "click" in a_json:
				effects["clicked"] = selector
				# Clear form state if it looks like a submit
				if re.search(r"submit|login|signup|send|save", selector.lower()):
					obs_next.pop("form_state", None)
					effects["form_submit"] = True

		return obs_next, effects

	def _handle_cli_action(
		self, obs_next: Dict[str, Any], action: Dict[str, Any], a_json: str
	) -> Tuple[Dict[str, Any], Dict[str, Any]]:
		"""Handle CLI-related actions and predict command output."""
		effects: Dict[str, Any] = {}

		# Extract command info
		cmd = _extract_argv0(a_json)
		if cmd:
			obs_next["last_cmd"] = cmd
			effects["cmd"] = cmd

			# Predict common command patterns
			if cmd in ["ls", "dir"]:
				effects["output_type"] = "file_list"
			elif cmd in ["cat", "type"]:
				effects["output_type"] = "file_content"
			elif cmd in ["grep", "find"]:
				effects["output_type"] = "search_results"
			elif cmd in ["git"]:
				effects["output_type"] = "git_status"

			# Track working directory
			if cmd == "cd":
				try:
					args = json.loads(a_json).get("argv", [])
					if len(args) > 1:
						new_dir = args[1]
						obs_next["cwd"] = new_dir
						effects["cwd"] = new_dir
				except Exception:
					pass

		return obs_next, effects

	def _record_action_metadata(self, obs_next: Dict[str, Any], action: Dict[str, Any]) -> None:
		"""Record general action metadata in observation."""
		try:
			if isinstance(action, dict) and action:
				obs_next["last_action_kind"] = next(iter(action.keys()))
				if "timestamp" in action:
					obs_next["last_action_time"] = action["timestamp"]
		except Exception:
			pass

def _safe_json(obj: Any) -> str:
	"""Safely convert object to JSON string."""
	try:
		return json.dumps(obj, ensure_ascii=False)
	except Exception:
		return str(obj)

def _extract_between(s: str, start_token: str, end_token: str) -> Optional[str]:
	"""Extract substring between tokens with better error handling."""
	try:
		start = s.find(start_token)
		if start == -1:
			return None
		end = s.find(end_token, start)
		if end == -1:
			end = len(s)
		return s[start:end][:256]


					if len(args) > 1:
						new_dir = args[1]
						obs_next["cwd"] = new_dir
						effects["cwd"] = new_dir
				except Exception:
					pass

		return obs_next, effects

	def _record_action_metadata(self, obs_next: Dict[str, Any], action: Dict[str, Any]) -> None:
		"""Record general action metadata in observation."""
		try:
			if isinstance(action, dict) and action:
				obs_next["last_action_kind"] = next(iter(action.keys()))
				if "timestamp" in action:
					obs_next["last_action_time"] = action["timestamp"]
		except Exception:
			pass

def _safe_json(obj: Any) -> str:
	"""Safely convert object to JSON string."""
	try:
		return json.dumps(obj, ensure_ascii=False)
	except Exception:
		return str(obj)

def _extract_between(s: str, start_token: str, end_token: str) -> Optional[str]:
	"""Extract substring between tokens with better error handling."""
	try:
		start = s.find(start_token)
		if start == -1:
			return None
		end = s.find(end_token, start)
		if end == -1:
			end = len(s)
		return s[start:end][:256]
	except Exception:
		return None

def _extract_key(s: str, key: str) -> Optional[str]:
	"""Extract JSON key value with validation."""
	try:
		pattern = r"\b" + re.escape(key) + r"\"\s*:\s*\"([^\"]+)\""


	def _record_action_metadata(self, obs_next: Dict[str, Any], action: Dict[str, Any]) -> None:
		"""Record general action metadata in observation."""
		try:
			if isinstance(action, dict) and action:
				obs_next["last_action_kind"] = next(iter(action.keys()))
				if "timestamp" in action:
					obs_next["last_action_time"] = action["timestamp"]
		except Exception:
			pass

def _safe_json(obj: Any) -> str:
	"""Safely convert object to JSON string."""
	try:
		return json.dumps(obj, ensure_ascii=False)
	except Exception:
		return str(obj)

def _extract_between(s: str, start_token: str, end_token: str) -> Optional[str]:
	"""Extract substring between tokens with better error handling."""
	try:
		start = s.find(start_token)
		if start == -1:
			return None
		end = s.find(end_token, start)
		if end == -1:
			end = len(s)
		return s[start:end][:256]
	except Exception:
		return None

def _extract_key(s: str, key: str) -> Optional[str]:
	"""Extract JSON key value with validation."""
	try:
		pattern = r"\b" + re.escape(key) + r"\"\s*:\s*\"([^\"]+)\""
		m = re.search(pattern, s)
		if not m:
			return None
		value = m.group(1)[:256]
		return value if value.strip() else None
	except Exception:
		return None

def _extract_argv0(s: str) -> Optional[str]:
	"""Extract first argument from argv array."""
	try:
		m = re.search(r"\[\s*\"([^\"]+)\"", s)
		if not m: