	# Adapter bank resolution
	def _resolve_adapter(bank_name: Optional[str], kind: str) -> Optional[str]:
		if not bank_name:
			return None
		try:
			from agi_dw.core.llm.adapter_router import pick_from_bank  # type: ignore
			bank = pick_from_bank(root_dir, bank_name)
			return bank.get(kind)
		except Exception:
			return None

	planner_adapter = planner.adapter_dir or _resolve_adapter(planner.adapter_bank, "planner")
	verifier_adapter = verifier.adapter_dir or _resolve_adapter(verifier.adapter_bank, "verifier")

	# Candidate generation
	candidates: List[Dict[str, Any]] = []
	if planner.candidates > 1:
		if planner.seeded and domain == "cli":
			try:
				def _seeded(tname: Optional[str]) -> List[Dict[str, Any]]:
					if tname == "count_lines":
						return [
							{"subgoals": ["read file", "count lines"], "tools": ["cli"], "constraints": {}},
							{"subgoals": ["count lines"], "tools": ["cli"], "constraints": {"prefer_wc": True}},
						]
					elif tname == "grep_error":
						return [
							{"subgoals": ["search ERROR"], "tools": ["cli"], "constraints": {}},
							{"subgoals": ["scan file", "filter lines"], "tools": ["cli"], "constraints": {"case_sensitive": True}},
						]
					return [{"subgoals": ["plan"], "tools": ["cli"], "constraints": {}}]
				candidates = _seeded(task_name)
			except Exception:
				candidates = []
		if not candidates and planner.use_tot:
			try:
				from scripts.misc.plan_tot import generate_plan_candidates  # type: ignore
				candidates = generate_plan_candidates(obs_aug, planner.model, num_candidates=int(planner.candidates)) or []
			except Exception:
				candidates = []
		if not candidates:
			for _ in range(max(1, int(planner.candidates))):
				with trace_span("plan_emit", {"structured": str(planner.structured_mode)}):
					p = emit_plan(
						obs_aug,
						model=planner.model,
						timeout_sec=planner.timeout_sec,
						use_llm=True,
						backend=planner.backend,
						log_prompts=log_prompts,
						adapter_dir=planner_adapter,


import logging
import json
import os
from typing import Any, Dict
from pathlib import Path
from datetime import datetime

try:
	import yaml  # type: ignore
except Exception:
	yaml = None

import torch

from agi_dw.core.llm.hf_client import HFClient
from agi_dw.core.utils.prompt_logger import PromptLogger, get_prompt_logger  # type: ignore
from agi_dw.core.ops.tracing import trace_span, meter_cost  # type: ignore
from agi_dw.tools.artifact_cache import is_enabled as cache_enabled, get_cached_text as cache_get, set_cached_text as cache_set  # type: ignore

def emit_plan(
    obs: Dict[str, Any],
    model: str = "meta-llama/Llama-3.2-3B",
    timeout_sec: int = 8,
    use_llm: bool = True,
    backend: str = "hf",
    log_prompts: bool = False,
    adapter_dir: str | None = None,
    structured_mode: str = "none",
) -> Dict[str, Any]:
	if use_llm:
		with trace_span("emit_plan", {"backend": backend, "structured": structured_mode}):
			logger = get_prompt_logger("planner", bool(log_prompts), echo=bool(log_prompts))
			def _log(kind: str, text: str) -> None:
				logger.log_text(kind, text)
			def _red(s: str) -> str:
				# PromptLogger internally redacts on write; use same policy for stdout/returns
				try:
					from agi_dw.core.utils.redact import redact_text  # type: ignore
					rs, _ = redact_text(s)
					return rs
				except Exception:
					return s
			prompt = (
				"You are a planner. Given an observation, return ONLY a YAML mapping with keys "
				"subgoals (string array), tools (string array), constraints (mapping). No prose.\n"
				"Example:\nsubgoals: ['step a','step b']\ntools: ['tool1']\nconstraints: {}\n\n"
				f"Observation (JSON):\n{json.dumps(obs)}\nOutput YAML:"
			)
			if backend == "hf":
				try:
					# Prefer local PEFT adapter if provided
					if adapter_dir:
						try:
							from agi_dw.core.llm.adapter_cache import AdapterCache  # type: ignore
							tok, peft_model = AdapterCache.get(model, adapter_dir)
							if structured_mode == "json":
								# Optional structured decoding via Outlines
								try:
									from outlines import models as _out_models  # type: ignore
									from outlines import generate as _out_generate  # type: ignore
									schema = {
										"type": "object",
										"properties": {
											"subgoals": {
												"type": "array",
												"items": {
													"type": "object",
													"properties": {
														"description": {"type": "string"},
														"requires": {
															"type": "array",
															"items": {"type": "string"}
														},
														"provides": {
															"type": "array",
															"items": {"type": "string"}
														},
														"validation": {
															"type": "object",
															"properties": {
																"required_elements": {
																	"type": "array",
																	"items": {"type": "string"}
																},
																"success_criteria": {"type": "string"}
															}
														}
													},
													"required": ["description"]
												}
											},
											"tools": {"type": "array", "items": {"type": "string"}},
											"constraints": {
												"type": "object",
												"properties": {
													"state_requirements": {
														"type": "object",
														"properties": {
															"form_fields": {
																"type": "array",
																"items": {"type": "string"}
															},
															"navigation": {
																"type": "array",
																"items": {"type": "string"}
															}
														}
													},
													"validation_points": {
														"type": "array",
														"items": {
															"type": "object",
															"properties": {
																"after_step": {"type": "integer"},
																"check": {"type": "string"}
															},
															"required": ["after_step", "check"]
														}
													}
												}
												},
											},
												"required": ["subgoals", "tools", "constraints"],
											"additionalProperties": True,
										}
									base_prompt = (
										"You are a planner. Given an observation, return ONLY a JSON object with:\n"
										"1. subgoals: array of step objects with:\n"
										"   - description: what to do\n"
										"   - requires: array of required state elements\n"
										"   - provides: array of state elements this step provides\n"
										"   - validation: object with required_elements and success_criteria\n"
										"2. tools: array of tool names to use\n"
										"3. constraints: object with:\n"
										"   - state_requirements: form_fields and navigation requirements\n"
										"   - validation_points: array of when and what to validate\n"
										"Example:\n"
										'{"subgoals":[{"description":"Navigate to login","requires":[],"provides":["login_page"],"validation":{"required_elements":["#login-form"]}}],'
										'"tools":["browser"],"constraints":{"state_requirements":{"form_fields":["username","password"]}}}\n\n'
										f"Observation (JSON):\n{json.dumps(obs)}\nOutput JSON:"
									)
									if log_prompts:
										rp = _red(base_prompt)
										print("[HF PLAN PROMPT (STRUCTURED)]\n" + rp)
										_log("prompt", rp)
									# Outlines uses its own model wrapper
									mdl = _out_models.transformers(model)
									generator = _out_generate.json(mdl, schema)
									text = generator(base_prompt)
									try:
										meter_cost("plan_structured", 1.0)
									except Exception:
										pass
								except Exception:
									# Fallback to plain generation with PEFT model
									enc = tok(base_prompt, return_tensors="pt").to(peft_model.device)
									with torch.inference_mode():
										out_ids = peft_model.generate(**enc, max_new_tokens=200, do_sample=False, num_beams=1)
									text = tok.decode(out_ids[0], skip_special_tokens=True)
							else:
								if log_prompts:
									rp = _red(prompt)
									print("[HF PLAN PROMPT]\n" + rp)
									_log("prompt", rp)
								enc = tok(prompt, return_tensors="pt").to(peft_model.device)
								with torch.inference_mode():
									out_ids = peft_model.generate(**enc, max_new_tokens=200, do_sample=False, num_beams=1)
								text = tok.decode(out_ids[0], skip_special_tokens=True)
						except Exception:
							# Fall back to base HF client
							adapter_dir = None
							text = ""
						if not adapter_dir:
							text = ""
							if structured_mode == "json":
								try:
									from outlines import models as _out_models  # type: ignore
									from outlines import generate as _out_generate  # type: ignore
									schema = {
										"type": "object",
										"properties": {
											"subgoals": {"type": "array", "items": {"type": "string"}},
											"tools": {"type": "array", "items": {"type": "string"}},
											"constraints": {"type": "object"},
										},
										"required": ["subgoals", "tools", "constraints"],
										"additionalProperties": True,
									}
									base_prompt = (
										"You are a planner. Given an observation, return ONLY a JSON object with keys "
										"subgoals (array of strings), tools (array of strings), constraints (object). No prose.\n"
										f"Observation (JSON):\n{json.dumps(obs)}\nOutput JSON:"
									)
									if log_prompts:
										_log("prompt", base_prompt)
									# Optional cache for structured planning
									use_cache = cache_enabled("AGI_PLAN_CACHE")
									ttl = int(os.environ.get("AGI_PLAN_CACHE_TTL", "3600") or 3600) if use_cache else 0
									text = ""
									if use_cache:
										cached = cache_get("planner", [model, "json", base_prompt], ttl)
										if isinstance(cached, str) and cached.strip():
											text = cached
									if not text:
										mdl = _out_models.transformers(model)
										generator = _out_generate.json(mdl, schema)
										text = generator(base_prompt)
										if use_cache and text:
											cache_set("planner", [model, "json", base_prompt], str(text))
								except Exception:
									text = ""
							# YAML fallback if structured generation disabled or failed
							if not text:
								client = HFClient.get_cached(model)
								if log_prompts:
									_log("prompt", prompt)
								# Optional cache for YAML planning
								use_cache = cache_enabled("AGI_PLAN_CACHE")
								ttl = int(os.environ.get("AGI_PLAN_CACHE_TTL", "3600") or 3600) if use_cache else 0
								text = ""
								if use_cache:
									cached = cache_get("planner", [model, "yaml", prompt], ttl)
									if isinstance(cached, str) and cached.strip():
										text = cached
								if not text:
									text = client.generate(prompt, max_new_tokens=200, temperature=0.0)
									if use_cache and text:
										cache_set("planner", [model, "yaml", prompt], str(text))
									try:
										meter_cost("plan_yaml", 1.0)
									except Exception:
										pass
								if log_prompts:
									_log("response", text)
					else:
						client = HFClient.get_cached(model)
						if log_prompts:
							_log("prompt", prompt)
						# Optional cache for YAML planning
						use_cache = cache_enabled("AGI_PLAN_CACHE")
						ttl = int(os.environ.get("AGI_PLAN_CACHE_TTL", "3600") or 3600) if use_cache else 0
						text = ""
						if use_cache:
							cached = cache_get("planner", [model, "yaml", prompt], ttl)
							if isinstance(cached, str) and cached.strip():
								text = cached
						if not text:
							text = client.generate(prompt, max_new_tokens=200, temperature=0.0)
							if use_cache and text:
								cache_set("planner", [model, "yaml", prompt], str(text))
							try:
								meter_cost("plan_yaml", 1.0)
							except Exception:
								pass
						if log_prompts:
							_log("response", text)
				except Exception:
					text = ""
				plan = _robust_struct_parse(text)
				if isinstance(plan, dict) and "subgoals" in plan and "tools" in plan and "constraints" in plan:
					return plan
			# end of hf backend planning block
	# Heuristic fallback by obs.kind
	kind = (obs or {}).get("kind")
	if kind == "cli":
		return {"version": "0.1", "subgoals": ["execute the necessary CLI command"], "tools": ["cli"], "constraints": {}}
	if kind == "dom":
		return {"version": "0.1", "subgoals": ["locate element and perform action"], "tools": ["browser"], "constraints": {}}
	return {"version": "0.1", "subgoals": ["perform task"], "tools": [], "constraints": {}}

def _robust_struct_parse(text: str) -> Dict[str, Any]:
	# Strip code fences/backticks if present
	ts = text.strip()
	if ts.startswith("```") and ts.endswith("```"):
		try:
			first_newline = text.find("\n")
			last_fence = text.rfind("```")
			if first_newline != -1 and last_fence != -1 and last_fence > first_newline:
				text = text[first_newline + 1:last_fence]
		except Exception:
			pass
	# YAML first
	if yaml is not None:
		try:
			y = yaml.safe_load(text)
			if isinstance(y, dict):
				return y
		except Exception:
			pass
	# JSON fallback
	t = text.strip()
	if t.startswith("{") and t.endswith("}"):
		try:
			return json.loads(t)
		except Exception:
			return {}
	start = t.find("{")
	end = t.rfind("}")
	if start != -1 and end != -1 and end > start:
		try:
			return json.loads(t[start : end + 1])
		except Exception:
			return {}
	return {}


import logging
import json
import os
from typing import Any, Dict
from pathlib import Path
from datetime import datetime

try:
	import yaml  # type: ignore
except Exception:
	yaml = None

import torch

from agi_dw.core.llm.hf_client import HFClient
from agi_dw.core.utils.prompt_logger import PromptLogger, get_prompt_logger  # type: ignore
from agi_dw.core.ops.tracing import trace_span, meter_cost  # type: ignore
from agi_dw.tools.artifact_cache import is_enabled as cache_enabled, get_cached_text as cache_get, set_cached_text as cache_set  # type: ignore

def emit_plan(
    obs: Dict[str, Any],
    model: str = "meta-llama/Llama-3.2-3B",
    timeout_sec: int = 8,
    use_llm: bool = True,
    backend: str = "hf",
    log_prompts: bool = False,
    adapter_dir: str | None = None,
    structured_mode: str = "none",
) -> Dict[str, Any]:
	if use_llm:
		with trace_span("emit_plan", {"backend": backend, "structured": structured_mode}):
			logger = get_prompt_logger("planner", bool(log_prompts), echo=bool(log_prompts))
			def _log(kind: str, text: str) -> None:
				logger.log_text(kind, text)
			def _red(s: str) -> str:
				# PromptLogger internally redacts on write; use same policy for stdout/returns
				try:
					from agi_dw.core.utils.redact import redact_text  # type: ignore
					rs, _ = redact_text(s)
					return rs
				except Exception:
					return s
			prompt = (
				"You are a planner. Given an observation, return ONLY a YAML mapping with keys "
				"subgoals (string array), tools (string array), constraints (mapping). No prose.\n"
				"Example:\nsubgoals: ['step a','step b']\ntools: ['tool1']\nconstraints: {}\n\n"
				f"Observation (JSON):\n{json.dumps(obs)}\nOutput YAML:"
			)
			if backend == "hf":
				try:
					# Prefer local PEFT adapter if provided
					if adapter_dir:
						try:
							from agi_dw.core.llm.adapter_cache import AdapterCache  # type: ignore
							tok, peft_model = AdapterCache.get(model, adapter_dir)
							if structured_mode == "json":
								# Optional structured decoding via Outlines
								try:
									from outlines import models as _out_models  # type: ignore
									from outlines import generate as _out_generate  # type: ignore
									schema = {
										"type": "object",
										"properties": {
											"subgoals": {
												"type": "array",
												"items": {
													"type": "object",
													"properties": {
														"description": {"type": "string"},
														"requires": {
															"type": "array",
															"items": {"type": "string"}
														},
														"provides": {
															"type": "array",
															"items": {"type": "string"}
														},
														"validation": {
															"type": "object",
															"properties": {
																"required_elements": {
																	"type": "array",
																	"items": {"type": "string"}
																},
																"success_criteria": {"type": "string"}
															}
														}
													},
													"required": ["description"]
												}
											},
											"tools": {"type": "array", "items": {"type": "string"}},
											"constraints": {
												"type": "object",
												"properties": {
													"state_requirements": {
														"type": "object",
														"properties": {
															"form_fields": {
																"type": "array",
																"items": {"type": "string"}
															},
															"navigation": {
																"type": "array",
																"items": {"type": "string"}
															}
														}
													},
													"validation_points": {
														"type": "array",
														"items": {
															"type": "object",
															"properties": {
																"after_step": {"type": "integer"},
																"check": {"type": "string"}
															},
															"required": ["after_step", "check"]
														}
													}
												}
												},
											},
												"required": ["subgoals", "tools", "constraints"],
											"additionalProperties": True,
										}
									base_prompt = (
										"You are a planner. Given an observation, return ONLY a JSON object with:\n"
										"1. subgoals: array of step objects with:\n"
										"   - description: what to do\n"
										"   - requires: array of required state elements\n"
										"   - provides: array of state elements this step provides\n"
										"   - validation: object with required_elements and success_criteria\n"
										"2. tools: array of tool names to use\n"
										"3. constraints: object with:\n"
										"   - state_requirements: form_fields and navigation requirements\n"
										"   - validation_points: array of when and what to validate\n"
										"Example:\n"
										'{"subgoals":[{"description":"Navigate to login","requires":[],"provides":["login_page"],"validation":{"required_elements":["#login-form"]}}],'
										'"tools":["browser"],"constraints":{"state_requirements":{"form_fields":["username","password"]}}}\n\n'
										f"Observation (JSON):\n{json.dumps(obs)}\nOutput JSON:"
									)
									if log_prompts:
										rp = _red(base_prompt)
										print("[HF PLAN PROMPT (STRUCTURED)]\n" + rp)
										_log("prompt", rp)
									# Outlines uses its own model wrapper
									mdl = _out_models.transformers(model)
									generator = _out_generate.json(mdl, schema)
									text = generator(base_prompt)
									try:
										meter_cost("plan_structured", 1.0)
									except Exception:
										pass
								except Exception:
									# Fallback to plain generation with PEFT model
									enc = tok(base_prompt, return_tensors="pt").to(peft_model.device)
									with torch.inference_mode():
										out_ids = peft_model.generate(**enc, max_new_tokens=200, do_sample=False, num_beams=1)
									text = tok.decode(out_ids[0], skip_special_tokens=True)
							else:
								if log_prompts:
									rp = _red(prompt)
									print("[HF PLAN PROMPT]\n" + rp)
									_log("prompt", rp)
								enc = tok(prompt, return_tensors="pt").to(peft_model.device)
								with torch.inference_mode():
									out_ids = peft_model.generate(**enc, max_new_tokens=200, do_sample=False, num_beams=1)
								text = tok.decode(out_ids[0], skip_special_tokens=True)
						except Exception:
							# Fall back to base HF client
							adapter_dir = None
							text = ""
						if not adapter_dir:
							text = ""
							if structured_mode == "json":
								try:
									from outlines import models as _out_models  # type: ignore
									from outlines import generate as _out_generate  # type: ignore
									schema = {
										"type": "object",
										"properties": {
											"subgoals": {"type": "array", "items": {"type": "string"}},
											"tools": {"type": "array", "items": {"type": "string"}},
											"constraints": {"type": "object"},
										},
										"required": ["subgoals", "tools", "constraints"],
										"additionalProperties": True,
									}
									base_prompt = (
										"You are a planner. Given an observation, return ONLY a JSON object with keys "
										"subgoals (array of strings), tools (array of strings), constraints (object). No prose.\n"
										f"Observation (JSON):\n{json.dumps(obs)}\nOutput JSON:"
									)
									if log_prompts:
										_log("prompt", base_prompt)
									# Optional cache for structured planning
									use_cache = cache_enabled("AGI_PLAN_CACHE")
									ttl = int(os.environ.get("AGI_PLAN_CACHE_TTL", "3600") or 3600) if use_cache else 0
									text = ""
									if use_cache:
										cached = cache_get("planner", [model, "json", base_prompt], ttl)
										if isinstance(cached, str) and cached.strip():
											text = cached
									if not text:
										mdl = _out_models.transformers(model)
										generator = _out_generate.json(mdl, schema)
										text = generator(base_prompt)
										if use_cache and text:
											cache_set("planner", [model, "json", base_prompt], str(text))
								except Exception:
									text = ""
							# YAML fallback if structured generation disabled or failed
							if not text:
								client = HFClient.get_cached(model)
								if log_prompts:
									_log("prompt", prompt)
								# Optional cache for YAML planning
								use_cache = cache_enabled("AGI_PLAN_CACHE")
								ttl = int(os.environ.get("AGI_PLAN_CACHE_TTL", "3600") or 3600) if use_cache else 0
								text = ""
								if use_cache:
									cached = cache_get("planner", [model, "yaml", prompt], ttl)
									if isinstance(cached, str) and cached.strip():
										text = cached
								if not text:
									text = client.generate(prompt, max_new_tokens=200, temperature=0.0)
									if use_cache and text:
										cache_set("planner", [model, "yaml", prompt], str(text))
									try:
										meter_cost("plan_yaml", 1.0)
									except Exception:
										pass
								if log_prompts:
									_log("response", text)
					else:
						client = HFClient.get_cached(model)
						if log_prompts:
							_log("prompt", prompt)
						# Optional cache for YAML planning
						use_cache = cache_enabled("AGI_PLAN_CACHE")
						ttl = int(os.environ.get("AGI_PLAN_CACHE_TTL", "3600") or 3600) if use_cache else 0
						text = ""
						if use_cache:
							cached = cache_get("planner", [model, "yaml", prompt], ttl)
							if isinstance(cached, str) and cached.strip():
								text = cached
						if not text:
							text = client.generate(prompt, max_new_tokens=200, temperature=0.0)
							if use_cache and text:
								cache_set("planner", [model, "yaml", prompt], str(text))
							try:
								meter_cost("plan_yaml", 1.0)
							except Exception:
								pass
						if log_prompts:
							_log("response", text)
				except Exception:
					text = ""
				plan = _robust_struct_parse(text)
				if isinstance(plan, dict) and "subgoals" in plan and "tools" in plan and "constraints" in plan:
					return plan
			# end of hf backend planning block
	# Heuristic fallback by obs.kind
	kind = (obs or {}).get("kind")
	if kind == "cli":
		return {"version": "0.1", "subgoals": ["execute the necessary CLI command"], "tools": ["cli"], "constraints": {}}
	if kind == "dom":
		return {"version": "0.1", "subgoals": ["locate element and perform action"], "tools": ["browser"], "constraints": {}}
	return {"version": "0.1", "subgoals": ["perform task"], "tools": [], "constraints": {}}

def _robust_struct_parse(text: str) -> Dict[str, Any]:
	# Strip code fences/backticks if present
	ts = text.strip()
	if ts.startswith("```") and ts.endswith("```"):
		try:
			first_newline = text.find("\n")
			last_fence = text.rfind("```")
			if first_newline != -1 and last_fence != -1 and last_fence > first_newline:
				text = text[first_newline + 1:last_fence]
		except Exception:
			pass
	# YAML first
	if yaml is not None:
		try:
			y = yaml.safe_load(text)
			if isinstance(y, dict):
				return y
		except Exception:


								meter_cost("plan_yaml", 1.0)
							except Exception:
								pass
						if log_prompts:
							_log("response", text)
				except Exception:
					text = ""
				plan = _robust_struct_parse(text)
				if isinstance(plan, dict) and "subgoals" in plan and "tools" in plan and "constraints" in plan:
					return plan
			# end of hf backend planning block
	# Heuristic fallback by obs.kind
	kind = (obs or {}).get("kind")
	if kind == "cli":
		return {"version": "0.1", "subgoals": ["execute the necessary CLI command"], "tools": ["cli"], "constraints": {}}
	if kind == "dom":
		return {"version": "0.1", "subgoals": ["locate element and perform action"], "tools": ["browser"], "constraints": {}}
	return {"version": "0.1", "subgoals": ["perform task"], "tools": [], "constraints": {}}

def _robust_struct_parse(text: str) -> Dict[str, Any]:
	# Strip code fences/backticks if present
	ts = text.strip()
	if ts.startswith("```") and ts.endswith("```"):
		try:
			first_newline = text.find("\n")
			last_fence = text.rfind("```")
			if first_newline != -1 and last_fence != -1 and last_fence > first_newline:
				text = text[first_newline + 1:last_fence]
		except Exception:
			pass
	# YAML first
	if yaml is not None:
		try:
			y = yaml.safe_load(text)
			if isinstance(y, dict):
				return y
		except Exception:
			pass
	# JSON fallback
	t = text.strip()
	if t.startswith("{") and t.endswith("}"):
		try:
			return json.loads(t)
		except Exception:
			return {}
	start = t.find("{")
	end = t.rfind("}")
	if start != -1 and end != -1 and end > start:
		try:
			return json.loads(t[start : end + 1])
		except Exception:
			return {}
	return {}


from agi_dw.core.llm.hf_client import HFClient
from agi_dw.core.utils.prompt_logger import PromptLogger, get_prompt_logger  # type: ignore
from agi_dw.core.ops.tracing import trace_span, meter_cost  # type: ignore
from agi_dw.tools.artifact_cache import is_enabled as cache_enabled, get_cached_text as cache_get, set_cached_text as cache_set  # type: ignore

def emit_plan(
    obs: Dict[str, Any],
    model: str = "meta-llama/Llama-3.2-3B",
    timeout_sec: int = 8,
    use_llm: bool = True,
    backend: str = "hf",
    log_prompts: bool = False,
    adapter_dir: str | None = None,
    structured_mode: str = "none",
) -> Dict[str, Any]:
	if use_llm:
		with trace_span("emit_plan", {"backend": backend, "structured": structured_mode}):
			logger = get_prompt_logger("planner", bool(log_prompts), echo=bool(log_prompts))
			def _log(kind: str, text: str) -> None:
				logger.log_text(kind, text)
			def _red(s: str) -> str:
				# PromptLogger internally redacts on write; use same policy for stdout/returns
				try:
					from agi_dw.core.utils.redact import redact_text  # type: ignore
					rs, _ = redact_text(s)
					return rs
				except Exception:
					return s
			prompt = (
				"You are a planner. Given an observation, return ONLY a YAML mapping with keys "
				"subgoals (string array), tools (string array), constraints (mapping). No prose.\n"
				"Example:\nsubgoals: ['step a','step b']\ntools: ['tool1']\nconstraints: {}\n\n"
				f"Observation (JSON):\n{json.dumps(obs)}\nOutput YAML:"
			)
			if backend == "hf":
				try:
					# Prefer local PEFT adapter if provided
					if adapter_dir:
						try:
							from agi_dw.core.llm.adapter_cache import AdapterCache  # type: ignore


from agi_dw.core.utils.prompt_logger import PromptLogger, get_prompt_logger  # type: ignore
from agi_dw.core.ops.tracing import trace_span, meter_cost  # type: ignore
from agi_dw.tools.artifact_cache import is_enabled as cache_enabled, get_cached_text as cache_get, set_cached_text as cache_set  # type: ignore

def emit_plan(
    obs: Dict[str, Any],
    model: str = "meta-llama/Llama-3.2-3B",
    timeout_sec: int = 8,
    use_llm: bool = True,
    backend: str = "hf",
    log_prompts: bool = False,
    adapter_dir: str | None = None,
    structured_mode: str = "none",
) -> Dict[str, Any]:
	if use_llm:
		with trace_span("emit_plan", {"backend": backend, "structured": structured_mode}):
			logger = get_prompt_logger("planner", bool(log_prompts), echo=bool(log_prompts))
			def _log(kind: str, text: str) -> None:
				logger.log_text(kind, text)
			def _red(s: str) -> str:
				# PromptLogger internally redacts on write; use same policy for stdout/returns
				try:
					from agi_dw.core.utils.redact import redact_text  # type: ignore
					rs, _ = redact_text(s)
					return rs
				except Exception:
					return s
			prompt = (
				"You are a planner. Given an observation, return ONLY a YAML mapping with keys "
				"subgoals (string array), tools (string array), constraints (mapping). No prose.\n"
				"Example:\nsubgoals: ['step a','step b']\ntools: ['tool1']\nconstraints: {}\n\n"
				f"Observation (JSON):\n{json.dumps(obs)}\nOutput YAML:"
			)
			if backend == "hf":
				try:
					# Prefer local PEFT adapter if provided
					if adapter_dir:
						try:
							from agi_dw.core.llm.adapter_cache import AdapterCache  # type: ignore
							tok, peft_model = AdapterCache.get(model, adapter_dir)
							if structured_mode == "json":
								# Optional structured decoding via Outlines
								try:
									from outlines import models as _out_models  # type: ignore
									from outlines import generate as _out_generate  # type: ignore
									schema = {
										"type": "object",


from __future__ import annotations

import os
import time
from pathlib import Path
from typing import Optional

def _locks_dir() -> Path:
	try:
		return Path(__file__).resolve().parents[2] / "data" / "locks"
	except Exception:
		return Path.cwd() / "data" / "locks"

def _lock_path(key: str) -> Path:
	k = str(key).strip()
	if not k:
		raise ValueError("empty lease key")
	return _locks_dir() / (k.replace("/", "_") + ".lock")

def acquire_lease(key: str, ttl_sec: int = 60) -> bool:
	"""Acquire a file-backed lease. Returns True if acquired, False if held by someone else.

	Writes pid and expiry timestamp into the lock file.
	"""
	p = _lock_path(key)
	p.parent.mkdir(parents=True, exist_ok=True)
	now = time.time()
	expiry = now + max(1, int(ttl_sec))
	# If exists, check expiry first
	if p.exists():
		try:
			content = p.read_text(encoding="utf-8")
			parts = content.split(",")
			prev_exp = float(parts[1]) if len(parts) > 1 else 0.0
			if prev_exp > now:
				return False
		except Exception:
			pass
	try:
		# Best-effort exclusive create; fall back to overwrite with check
		fd = os.open(str(p), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
		with os.fdopen(fd, "w", encoding="utf-8") as f:
			f.write(f"{os.getpid()},{expiry}")
		return True
	except FileExistsError:
		# Race: overwrite only if expired
		try:
			content = p.read_text(encoding="utf-8")
			parts = content.split(",")
			prev_exp = float(parts[1]) if len(parts) > 1 else 0.0
			if prev_exp <= now:
				p.write_text(f"{os.getpid()},{expiry}", encoding="utf-8")
				return True
			return False
		except Exception:
			return False

def release_lease(key: str) -> None:
	"""Release a lease if owned or expired (best-effort)."""
	try:
		p = _lock_path(key)
		p.unlink(missing_ok=True)
	except Exception:
		return


from __future__ import annotations

import os
import time
from pathlib import Path
from typing import Optional

def _locks_dir() -> Path:
	try:
		return Path(__file__).resolve().parents[2] / "data" / "locks"
	except Exception:
		return Path.cwd() / "data" / "locks"

def _lock_path(key: str) -> Path:
	k = str(key).strip()
	if not k:
		raise ValueError("empty lease key")
	return _locks_dir() / (k.replace("/", "_") + ".lock")

def acquire_lease(key: str, ttl_sec: int = 60) -> bool:
	"""Acquire a file-backed lease. Returns True if acquired, False if held by someone else.

	Writes pid and expiry timestamp into the lock file.
	"""
	p = _lock_path(key)
	p.parent.mkdir(parents=True, exist_ok=True)
	now = time.time()
	expiry = now + max(1, int(ttl_sec))
	# If exists, check expiry first
	if p.exists():


from __future__ import annotations

import os
import time
from pathlib import Path
from typing import Optional

def _locks_dir() -> Path:
	try:
		return Path(__file__).resolve().parents[2] / "data" / "locks"
	except Exception:
		return Path.cwd() / "data" / "locks"

def _lock_path(key: str) -> Path:
	k = str(key).strip()
	if not k:
		raise ValueError("empty lease key")
	return _locks_dir() / (k.replace("/", "_") + ".lock")

def acquire_lease(key: str, ttl_sec: int = 60) -> bool:
	"""Acquire a file-backed lease. Returns True if acquired, False if held by someone else.

	Writes pid and expiry timestamp into the lock file.
	"""
	p = _lock_path(key)
	p.parent.mkdir(parents=True, exist_ok=True)
	now = time.time()
	expiry = now + max(1, int(ttl_sec))
	# If exists, check expiry first
	if p.exists():
		try:
			content = p.read_text(encoding="utf-8")
			parts = content.split(",")
			prev_exp = float(parts[1]) if len(parts) > 1 else 0.0
			if prev_exp > now:
				return False
		except Exception:


import os
import time
from pathlib import Path
from typing import Optional

def _locks_dir() -> Path:
	try:
		return Path(__file__).resolve().parents[2] / "data" / "locks"
	except Exception:
		return Path.cwd() / "data" / "locks"

def _lock_path(key: str) -> Path:
	k = str(key).strip()
	if not k:
		raise ValueError("empty lease key")
	return _locks_dir() / (k.replace("/", "_") + ".lock")

def acquire_lease(key: str, ttl_sec: int = 60) -> bool:
	"""Acquire a file-backed lease. Returns True if acquired, False if held by someone else.

	Writes pid and expiry timestamp into the lock file.
	"""
	p = _lock_path(key)
	p.parent.mkdir(parents=True, exist_ok=True)
	now = time.time()
	expiry = now + max(1, int(ttl_sec))
	# If exists, check expiry first
	if p.exists():
		try:
			content = p.read_text(encoding="utf-8")
			parts = content.split(",")
			prev_exp = float(parts[1]) if len(parts) > 1 else 0.0
			if prev_exp > now:
				return False
		except Exception:
			pass
	try:
		# Best-effort exclusive create; fall back to overwrite with check
		fd = os.open(str(p), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
		with os.fdopen(fd, "w", encoding="utf-8") as f:
			f.write(f"{os.getpid()},{expiry}")
		return True
	except FileExistsError:
		# Race: overwrite only if expired
		try:
			content = p.read_text(encoding="utf-8")
			parts = content.split(",")
			prev_exp = float(parts[1]) if len(parts) > 1 else 0.0
			if prev_exp <= now:
				p.write_text(f"{os.getpid()},{expiry}", encoding="utf-8")
				return True
			return False
		except Exception:
			return False

def release_lease(key: str) -> None:
	"""Release a lease if owned or expired (best-effort)."""
	try:
		p = _lock_path(key)
		p.unlink(missing_ok=True)
	except Exception:
		return


	try:
		# Best-effort exclusive create; fall back to overwrite with check
		fd = os.open(str(p), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
		with os.fdopen(fd, "w", encoding="utf-8") as f:
			f.write(f"{os.getpid()},{expiry}")
		return True
	except FileExistsError:
		# Race: overwrite only if expired
		try:
			content = p.read_text(encoding="utf-8")
			parts = content.split(",")
			prev_exp = float(parts[1]) if len(parts) > 1 else 0.0
			if prev_exp <= now:
				p.write_text(f"{os.getpid()},{expiry}", encoding="utf-8")
				return True
			return False
		except Exception:
			return False

def release_lease(key: str) -> None:
	"""Release a lease if owned or expired (best-effort)."""
	try:
		p = _lock_path(key)
		p.unlink(missing_ok=True)
	except Exception:
		return


from __future__ import annotations

import base64
import hashlib
import hmac
import os
from dataclasses import dataclass
from typing import Any, Optional, Tuple

@dataclass
class SecretHandle:
	name: str
	source: str
	_ref: str
	_value: Optional[bytes] = None

	def materialize(self) -> bytes:
		if self._value is not None:
			return self._value
		# Env source
		if self.source == "env":
			val = os.environ.get(self._ref)
			if val is None:
				raise KeyError(f"secret env var not set: {self._ref}")
			return val.encode("utf-8")
		# Keyring source
		if self.source == "keyring":
			try:
				import keyring  # type: ignore
			except Exception as e:
				raise RuntimeError("keyring not available") from e
			val = keyring.get_password("agi_dw", self._ref)
			if val is None:
				raise KeyError(f"secret not found in keyring: {self._ref}")
			return val.encode("utf-8")
		raise RuntimeError(f"unsupported secret source: {self.source}")

	def __repr__(self) -> str:  # safe
		return f"<SecretHandle name={self.name} source={self.source}>"

def secrets_get(name: str) -> SecretHandle:
	"""Resolve a secret by name.

	Resolution order:
	- Exact env var match by name
	- Uppercased variant
	- KEYRING lookup (service: agi_dw, username: name)
	"""
	if name in os.environ:
		return SecretHandle(name=name, source="env", _ref=name)
	up = name.upper()
	if up in os.environ:
		return SecretHandle(name=name, source="env", _ref=up)
	# Fall back to keyring handle (lazy materialization)
	return SecretHandle(name=name, source="keyring", _ref=name)

def redact_handle(h: SecretHandle) -> dict[str, Any]:
	return {"kind": "secret", "name": h.name, "source": h.source}

def sign_verify(data: bytes, key: SecretHandle) -> Tuple[str, bool]:
	"""Generate HMAC-SHA256 signature; verify by recomputing.

	Returns (signature_b64, ok) where ok is True if local roundtrip verify passes.
	"""
	key_bytes = key.materialize()
	sig = hmac.new(key_bytes, data, hashlib.sha256).digest()
	enc = base64.urlsafe_b64encode(sig).decode("ascii")
	ok = hmac.compare_digest(sig, hmac.new(key_bytes, data, hashlib.sha256).digest())
	return enc, bool(ok)

def encrypt_decrypt(data: bytes, key: SecretHandle) -> Tuple[bytes, bytes]:
	"""Encrypt and then decrypt using Fernet if available; else no-op passthrough.

	Returns (ciphertext, plaintext_roundtrip).
	"""
	try:
		from cryptography.fernet import Fernet  # type: ignore
		# Derive a 32-byte key via SHA256 of secret material
		k = hashlib.sha256(key.materialize()).digest()
		k_b64 = base64.urlsafe_b64encode(k)
		f = Fernet(k_b64)
		ct = f.encrypt(data)
		pt = f.decrypt(ct)
		return ct, pt
	except Exception:
		# Safe fallback: identity (not secure). Callers should gate on dependency.
		return data, data

def attest_env(nonce: str = "") -> dict[str, Any]:
	"""Return a minimal provenance bundle for the current environment.

	Includes python/os info, optional git head, and a self-signed HMAC if ATTEST_KEY is set.
	"""
	from platform import python_version, platform
	root = Path(__file__).resolve().parents[2]
	git_head = None
	try:
		import subprocess
		res = subprocess.run(["git", "rev-parse", "HEAD"], cwd=str(root), stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=2)
		if res.returncode == 0:
			git_head = (res.stdout or "").strip()
	except Exception:
		git_head = None
	info = {"python": python_version(), "platform": platform(), "git_head": git_head, "nonce": nonce or ""}
	key_name = os.environ.get("ATTEST_KEY_NAME", "attest")
	key_env = os.environ.get("ATTEST_KEY")
	if key_env:
		try:
			key = SecretHandle(name=key_name, source="env", _ref="ATTEST_KEY")
			sig, _ = sign_verify(json.dumps(info, sort_keys=True).encode("utf-8"), key)
			info["signature_hmac_b64"] = sig
		except Exception:
			pass
	return info


SecretHandle

from __future__ import annotations

import base64
import hashlib
import hmac
import os
from dataclasses import dataclass
from typing import Any, Optional, Tuple

@dataclass
class SecretHandle:
	name: str
	source: str
	_ref: str
	_value: Optional[bytes] = None

	def materialize(self) -> bytes:
		if self._value is not None:
			return self._value
		# Env source
		if self.source == "env":
			val = os.environ.get(self._ref)
			if val is None:
				raise KeyError(f"secret env var not set: {self._ref}")
			return val.encode("utf-8")
		# Keyring source
		if self.source == "keyring":
			try:
				import keyring  # type: ignore
			except Exception as e:
				raise RuntimeError("keyring not available") from e
			val = keyring.get_password("agi_dw", self._ref)
			if val is None:
				raise KeyError(f"secret not found in keyring: {self._ref}")
			return val.encode("utf-8")
		raise RuntimeError(f"unsupported secret source: {self.source}")

	def __repr__(self) -> str:  # safe
		return f"<SecretHandle name={self.name} source={self.source}>"

def secrets_get(name: str) -> SecretHandle:
	"""Resolve a secret by name.

	Resolution order:
	- Exact env var match by name
	- Uppercased variant
	- KEYRING lookup (service: agi_dw, username: name)
	"""
	if name in os.environ:
		return SecretHandle(name=name, source="env", _ref=name)
	up = name.upper()
	if up in os.environ:
		return SecretHandle(name=name, source="env", _ref=up)
	# Fall back to keyring handle (lazy materialization)
	return SecretHandle(name=name, source="keyring", _ref=name)

def redact_handle(h: SecretHandle) -> dict[str, Any]:


			val = os.environ.get(self._ref)
			if val is None:
				raise KeyError(f"secret env var not set: {self._ref}")
			return val.encode("utf-8")
		# Keyring source
		if self.source == "keyring":
			try:
				import keyring  # type: ignore
			except Exception as e:
				raise RuntimeError("keyring not available") from e
			val = keyring.get_password("agi_dw", self._ref)
			if val is None:
				raise KeyError(f"secret not found in keyring: {self._ref}")
			return val.encode("utf-8")
		raise RuntimeError(f"unsupported secret source: {self.source}")

	def __repr__(self) -> str:  # safe
		return f"<SecretHandle name={self.name} source={self.source}>"

def secrets_get(name: str) -> SecretHandle:
	"""Resolve a secret by name.

	Resolution order:
	- Exact env var match by name
	- Uppercased variant
	- KEYRING lookup (service: agi_dw, username: name)
	"""
	if name in os.environ:
		return SecretHandle(name=name, source="env", _ref=name)
	up = name.upper()
	if up in os.environ:
		return SecretHandle(name=name, source="env", _ref=up)
	# Fall back to keyring handle (lazy materialization)
	return SecretHandle(name=name, source="keyring", _ref=name)

def redact_handle(h: SecretHandle) -> dict[str, Any]:
	return {"kind": "secret", "name": h.name, "source": h.source}

def sign_verify(data: bytes, key: SecretHandle) -> Tuple[str, bool]:
	"""Generate HMAC-SHA256 signature; verify by recomputing.

	Returns (signature_b64, ok) where ok is True if local roundtrip verify passes.
	"""
	key_bytes = key.materialize()
	sig = hmac.new(key_bytes, data, hashlib.sha256).digest()
	enc = base64.urlsafe_b64encode(sig).decode("ascii")
	ok = hmac.compare_digest(sig, hmac.new(key_bytes, data, hashlib.sha256).digest())
	return enc, bool(ok)

def encrypt_decrypt(data: bytes, key: SecretHandle) -> Tuple[bytes, bytes]:
	"""Encrypt and then decrypt using Fernet if available; else no-op passthrough.


		return f"<SecretHandle name={self.name} source={self.source}>"

def secrets_get(name: str) -> SecretHandle:
	"""Resolve a secret by name.

	Resolution order:
	- Exact env var match by name
	- Uppercased variant
	- KEYRING lookup (service: agi_dw, username: name)
	"""
	if name in os.environ:
		return SecretHandle(name=name, source="env", _ref=name)
	up = name.upper()
	if up in os.environ:
		return SecretHandle(name=name, source="env", _ref=up)
	# Fall back to keyring handle (lazy materialization)
	return SecretHandle(name=name, source="keyring", _ref=name)

def redact_handle(h: SecretHandle) -> dict[str, Any]:
	return {"kind": "secret", "name": h.name, "source": h.source}

def sign_verify(data: bytes, key: SecretHandle) -> Tuple[str, bool]:
	"""Generate HMAC-SHA256 signature; verify by recomputing.

	Returns (signature_b64, ok) where ok is True if local roundtrip verify passes.
	"""
	key_bytes = key.materialize()
	sig = hmac.new(key_bytes, data, hashlib.sha256).digest()
	enc = base64.urlsafe_b64encode(sig).decode("ascii")
	ok = hmac.compare_digest(sig, hmac.new(key_bytes, data, hashlib.sha256).digest())
	return enc, bool(ok)

def encrypt_decrypt(data: bytes, key: SecretHandle) -> Tuple[bytes, bytes]:
	"""Encrypt and then decrypt using Fernet if available; else no-op passthrough.

	Returns (ciphertext, plaintext_roundtrip).
	"""
	try:


	"""Resolve a secret by name.

	Resolution order:
	- Exact env var match by name
	- Uppercased variant
	- KEYRING lookup (service: agi_dw, username: name)
	"""
	if name in os.environ:
		return SecretHandle(name=name, source="env", _ref=name)
	up = name.upper()
	if up in os.environ:
		return SecretHandle(name=name, source="env", _ref=up)
	# Fall back to keyring handle (lazy materialization)
	return SecretHandle(name=name, source="keyring", _ref=name)

def redact_handle(h: SecretHandle) -> dict[str, Any]:
	return {"kind": "secret", "name": h.name, "source": h.source}

def sign_verify(data: bytes, key: SecretHandle) -> Tuple[str, bool]:
	"""Generate HMAC-SHA256 signature; verify by recomputing.

	Returns (signature_b64, ok) where ok is True if local roundtrip verify passes.
	"""
	key_bytes = key.materialize()
	sig = hmac.new(key_bytes, data, hashlib.sha256).digest()
	enc = base64.urlsafe_b64encode(sig).decode("ascii")
	ok = hmac.compare_digest(sig, hmac.new(key_bytes, data, hashlib.sha256).digest())
	return enc, bool(ok)

def encrypt_decrypt(data: bytes, key: SecretHandle) -> Tuple[bytes, bytes]:
	"""Encrypt and then decrypt using Fernet if available; else no-op passthrough.

	Returns (ciphertext, plaintext_roundtrip).
	"""
	try:
		from cryptography.fernet import Fernet  # type: ignore
		# Derive a 32-byte key via SHA256 of secret material
		k = hashlib.sha256(key.materialize()).digest()
		k_b64 = base64.urlsafe_b64encode(k)
		f = Fernet(k_b64)
		ct = f.encrypt(data)
		pt = f.decrypt(ct)
		return ct, pt
	except Exception:
		# Safe fallback: identity (not secure). Callers should gate on dependency.
		return data, data


	# Fall back to keyring handle (lazy materialization)
	return SecretHandle(name=name, source="keyring", _ref=name)

def redact_handle(h: SecretHandle) -> dict[str, Any]:
	return {"kind": "secret", "name": h.name, "source": h.source}

def sign_verify(data: bytes, key: SecretHandle) -> Tuple[str, bool]:
	"""Generate HMAC-SHA256 signature; verify by recomputing.

	Returns (signature_b64, ok) where ok is True if local roundtrip verify passes.
	"""
	key_bytes = key.materialize()
	sig = hmac.new(key_bytes, data, hashlib.sha256).digest()
	enc = base64.urlsafe_b64encode(sig).decode("ascii")
	ok = hmac.compare_digest(sig, hmac.new(key_bytes, data, hashlib.sha256).digest())
	return enc, bool(ok)

def encrypt_decrypt(data: bytes, key: SecretHandle) -> Tuple[bytes, bytes]:
	"""Encrypt and then decrypt using Fernet if available; else no-op passthrough.

	Returns (ciphertext, plaintext_roundtrip).
	"""
	try:
		from cryptography.fernet import Fernet  # type: ignore
		# Derive a 32-byte key via SHA256 of secret material
		k = hashlib.sha256(key.materialize()).digest()
		k_b64 = base64.urlsafe_b64encode(k)
		f = Fernet(k_b64)
		ct = f.encrypt(data)
		pt = f.decrypt(ct)
		return ct, pt
	except Exception:
		# Safe fallback: identity (not secure). Callers should gate on dependency.
		return data, data

def attest_env(nonce: str = "") -> dict[str, Any]:
	"""Return a minimal provenance bundle for the current environment.

	Includes python/os info, optional git head, and a self-signed HMAC if ATTEST_KEY is set.
	"""
	from platform import python_version, platform
	root = Path(__file__).resolve().parents[2]
	git_head = None
	try:
		import subprocess
		res = subprocess.run(["git", "rev-parse", "HEAD"], cwd=str(root), stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=2)
		if res.returncode == 0:
			git_head = (res.stdout or "").strip()
	except Exception:
		git_head = None
	info = {"python": python_version(), "platform": platform(), "git_head": git_head, "nonce": nonce or ""}
	key_name = os.environ.get("ATTEST_KEY_NAME", "attest")
	key_env = os.environ.get("ATTEST_KEY")


def encrypt_decrypt(data: bytes, key: SecretHandle) -> Tuple[bytes, bytes]:
	"""Encrypt and then decrypt using Fernet if available; else no-op passthrough.

	Returns (ciphertext, plaintext_roundtrip).
	"""
	try:
		from cryptography.fernet import Fernet  # type: ignore
		# Derive a 32-byte key via SHA256 of secret material
		k = hashlib.sha256(key.materialize()).digest()
		k_b64 = base64.urlsafe_b64encode(k)
		f = Fernet(k_b64)
		ct = f.encrypt(data)
		pt = f.decrypt(ct)
		return ct, pt
	except Exception:
		# Safe fallback: identity (not secure). Callers should gate on dependency.
		return data, data

def attest_env(nonce: str = "") -> dict[str, Any]:
	"""Return a minimal provenance bundle for the current environment.

	Includes python/os info, optional git head, and a self-signed HMAC if ATTEST_KEY is set.
	"""
	from platform import python_version, platform
	root = Path(__file__).resolve().parents[2]
	git_head = None
	try:
		import subprocess
		res = subprocess.run(["git", "rev-parse", "HEAD"], cwd=str(root), stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=2)
		if res.returncode == 0:
			git_head = (res.stdout or "").strip()
	except Exception:
		git_head = None
	info = {"python": python_version(), "platform": platform(), "git_head": git_head, "nonce": nonce or ""}
	key_name = os.environ.get("ATTEST_KEY_NAME", "attest")
	key_env = os.environ.get("ATTEST_KEY")
	if key_env:
		try:
			key = SecretHandle(name=key_name, source="env", _ref="ATTEST_KEY")
			sig, _ = sign_verify(json.dumps(info, sort_keys=True).encode("utf-8"), key)
			info["signature_hmac_b64"] = sig
		except Exception:
			pass
	return info


materialize

from __future__ import annotations

import base64
import hashlib
import hmac
import os
from dataclasses import dataclass
from typing import Any, Optional, Tuple

@dataclass
class SecretHandle:
	name: str
	source: str
	_ref: str
	_value: Optional[bytes] = None

	def materialize(self) -> bytes:
		if self._value is not None:
			return self._value
		# Env source
		if self.source == "env":
			val = os.environ.get(self._ref)
			if val is None:
				raise KeyError(f"secret env var not set: {self._ref}")
			return val.encode("utf-8")
		# Keyring source
		if self.source == "keyring":
			try:
				import keyring  # type: ignore
			except Exception as e:
				raise RuntimeError("keyring not available") from e
			val = keyring.get_password("agi_dw", self._ref)
			if val is None:
				raise KeyError(f"secret not found in keyring: {self._ref}")
			return val.encode("utf-8")
		raise RuntimeError(f"unsupported secret source: {self.source}")

	def __repr__(self) -> str:  # safe
		return f"<SecretHandle name={self.name} source={self.source}>"

def secrets_get(name: str) -> SecretHandle:
	"""Resolve a secret by name.

	Resolution order:
	- Exact env var match by name
	- Uppercased variant
	- KEYRING lookup (service: agi_dw, username: name)
	"""
	if name in os.environ:
		return SecretHandle(name=name, source="env", _ref=name)
	up = name.upper()
	if up in os.environ:
		return SecretHandle(name=name, source="env", _ref=up)
	# Fall back to keyring handle (lazy materialization)
	return SecretHandle(name=name, source="keyring", _ref=name)


		if self._value is not None:
			return self._value
		# Env source
		if self.source == "env":
			val = os.environ.get(self._ref)
			if val is None:
				raise KeyError(f"secret env var not set: {self._ref}")
			return val.encode("utf-8")
		# Keyring source
		if self.source == "keyring":
			try:
				import keyring  # type: ignore
			except Exception as e:
				raise RuntimeError("keyring not available") from e
			val = keyring.get_password("agi_dw", self._ref)
			if val is None:
				raise KeyError(f"secret not found in keyring: {self._ref}")
			return val.encode("utf-8")
		raise RuntimeError(f"unsupported secret source: {self.source}")

	def __repr__(self) -> str:  # safe
		return f"<SecretHandle name={self.name} source={self.source}>"

def secrets_get(name: str) -> SecretHandle:
	"""Resolve a secret by name.

	Resolution order:
	- Exact env var match by name
	- Uppercased variant
	- KEYRING lookup (service: agi_dw, username: name)
	"""
	if name in os.environ:
		return SecretHandle(name=name, source="env", _ref=name)
	up = name.upper()
	if up in os.environ:
		return SecretHandle(name=name, source="env", _ref=up)
	# Fall back to keyring handle (lazy materialization)
	return SecretHandle(name=name, source="keyring", _ref=name)

def redact_handle(h: SecretHandle) -> dict[str, Any]:


from __future__ import annotations

import json
import os
import threading
import time
import uuid
from contextlib import contextmanager
from pathlib import Path
from typing import Any, Dict, Optional

_SPAN_LOCAL = threading.local()

def _now_iso() -> str:
	try:
		from datetime import datetime, timezone
		return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
	except Exception:
		return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())

def _span_dir() -> Path:
	root = Path(__file__).resolve().parents[2]
	base = Path(os.environ.get("AGI_SPAN_DIR", str(root / "data" / "traces" / "spans")))
	# Shard by date to avoid huge single files
	try:
		from datetime import datetime
		date_tag = datetime.utcnow().strftime("%Y%m%d")
	except Exception:
		date_tag = "00000000"
	return base / date_tag

def _write_jsonl(path: Path, obj: Dict[str, Any]) -> None:
	try:
		path.parent.mkdir(parents=True, exist_ok=True)
		with path.open("a", encoding="utf-8") as f:
			f.write(json.dumps(obj, ensure_ascii=False) + "\n")
	except Exception:
		pass

def _get_current_span() -> Optional[Dict[str, Any]]:
	return getattr(_SPAN_LOCAL, "current", None)

def _set_current_span(span: Optional[Dict[str, Any]]) -> None:
	setattr(_SPAN_LOCAL, "current", span)

@contextmanager
def trace_span(name: str, attrs: Optional[Dict[str, Any]] = None):
	"""Structured span context manager.

	Writes span start/end records to data/traces/spans.
	Accumulates per-span cost via meter_cost.
	"""
	parent = _get_current_span()
	span_id = uuid.uuid4().hex
	start_ts = time.time()
	rec_base: Dict[str, Any] = {
		"span_id": span_id,
		"parent_span_id": (parent.get("span_id") if isinstance(parent, dict) else None),
		"name": str(name),
		"attrs": dict(attrs or {}),
		"start_ts": start_ts,
		"start_iso": _now_iso(),
		"cost": {},
	}
	_set_current_span(rec_base)
	# Emit start event
	_write_jsonl(_span_dir() / "spans.jsonl", {**rec_base, "event": "start"})
	try:
		yield rec_base
	finally:
		end_ts = time.time()
		rec_base["end_ts"] = end_ts
		rec_base["end_iso"] = _now_iso()
		rec_base["duration_ms"] = int(max(0.0, (end_ts - start_ts)) * 1000)
		_write_jsonl(_span_dir() / "spans.jsonl", {**rec_base, "event": "end"})
		# Restore parent span
		_set_current_span(parent if isinstance(parent, dict) else None)

def meter_cost(kind: str, amount: float) -> None:
	"""Accumulate a numeric cost metric into the active span.

	If AGI_COST_RATE_* environment variables are set, callers may convert raw counts
	(e.g., tokens or seconds) to dollars externally and also meter those here.
	"""
	span = _get_current_span()
	if not isinstance(span, dict):
		return
	try:
		k = str(kind)
		v = float(amount)
		cost: Dict[str, float] = span.setdefault("cost", {})  # type: ignore[assignment]
		cost[k] = float(cost.get(k, 0.0) + v)
	except Exception:
		return

def current_span_id() -> Optional[str]:
	span = _get_current_span()
	return (span.get("span_id") if isinstance(span, dict) else None)


from __future__ import annotations

import json
import os
import threading
import time
import uuid
from contextlib import contextmanager
from pathlib import Path
from typing import Any, Dict, Optional

_SPAN_LOCAL = threading.local()

def _now_iso() -> str:
	try:
		from datetime import datetime, timezone
		return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
	except Exception:
		return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())

def _span_dir() -> Path:
	root = Path(__file__).resolve().parents[2]
	base = Path(os.environ.get("AGI_SPAN_DIR", str(root / "data" / "traces" / "spans")))
	# Shard by date to avoid huge single files
	try:
		from datetime import datetime
		date_tag = datetime.utcnow().strftime("%Y%m%d")
	except Exception:
		date_tag = "00000000"
	return base / date_tag

def _write_jsonl(path: Path, obj: Dict[str, Any]) -> None:
	try:
		path.parent.mkdir(parents=True, exist_ok=True)
		with path.open("a", encoding="utf-8") as f:
			f.write(json.dumps(obj, ensure_ascii=False) + "\n")
	except Exception:


import os
import threading
import time
import uuid
from contextlib import contextmanager
from pathlib import Path
from typing import Any, Dict, Optional

_SPAN_LOCAL = threading.local()

def _now_iso() -> str:
	try:
		from datetime import datetime, timezone
		return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
	except Exception:
		return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())

def _span_dir() -> Path:
	root = Path(__file__).resolve().parents[2]
	base = Path(os.environ.get("AGI_SPAN_DIR", str(root / "data" / "traces" / "spans")))
	# Shard by date to avoid huge single files
	try:
		from datetime import datetime
		date_tag = datetime.utcnow().strftime("%Y%m%d")
	except Exception:
		date_tag = "00000000"
	return base / date_tag

def _write_jsonl(path: Path, obj: Dict[str, Any]) -> None:
	try:
		path.parent.mkdir(parents=True, exist_ok=True)
		with path.open("a", encoding="utf-8") as f:
			f.write(json.dumps(obj, ensure_ascii=False) + "\n")
	except Exception:
		pass

def _get_current_span() -> Optional[Dict[str, Any]]:
	return getattr(_SPAN_LOCAL, "current", None)

def _set_current_span(span: Optional[Dict[str, Any]]) -> None:
	setattr(_SPAN_LOCAL, "current", span)

@contextmanager


def _now_iso() -> str:
	try:
		from datetime import datetime, timezone
		return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
	except Exception:
		return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())

def _span_dir() -> Path:
	root = Path(__file__).resolve().parents[2]
	base = Path(os.environ.get("AGI_SPAN_DIR", str(root / "data" / "traces" / "spans")))
	# Shard by date to avoid huge single files
	try:
		from datetime import datetime
		date_tag = datetime.utcnow().strftime("%Y%m%d")
	except Exception:
		date_tag = "00000000"
	return base / date_tag

def _write_jsonl(path: Path, obj: Dict[str, Any]) -> None:
	try:
		path.parent.mkdir(parents=True, exist_ok=True)
		with path.open("a", encoding="utf-8") as f:
			f.write(json.dumps(obj, ensure_ascii=False) + "\n")
	except Exception:
		pass

def _get_current_span() -> Optional[Dict[str, Any]]:
	return getattr(_SPAN_LOCAL, "current", None)

def _set_current_span(span: Optional[Dict[str, Any]]) -> None:
	setattr(_SPAN_LOCAL, "current", span)

@contextmanager
def trace_span(name: str, attrs: Optional[Dict[str, Any]] = None):
	"""Structured span context manager.

	Writes span start/end records to data/traces/spans.
	Accumulates per-span cost via meter_cost.
	"""
	parent = _get_current_span()
	span_id = uuid.uuid4().hex
	start_ts = time.time()


	root = Path(__file__).resolve().parents[2]
	base = Path(os.environ.get("AGI_SPAN_DIR", str(root / "data" / "traces" / "spans")))
	# Shard by date to avoid huge single files
	try:
		from datetime import datetime
		date_tag = datetime.utcnow().strftime("%Y%m%d")
	except Exception:
		date_tag = "00000000"
	return base / date_tag

def _write_jsonl(path: Path, obj: Dict[str, Any]) -> None:
	try:
		path.parent.mkdir(parents=True, exist_ok=True)
		with path.open("a", encoding="utf-8") as f:
			f.write(json.dumps(obj, ensure_ascii=False) + "\n")
	except Exception:
		pass

def _get_current_span() -> Optional[Dict[str, Any]]:
	return getattr(_SPAN_LOCAL, "current", None)

def _set_current_span(span: Optional[Dict[str, Any]]) -> None:
	setattr(_SPAN_LOCAL, "current", span)

@contextmanager
def trace_span(name: str, attrs: Optional[Dict[str, Any]] = None):
	"""Structured span context manager.

	Writes span start/end records to data/traces/spans.
	Accumulates per-span cost via meter_cost.
	"""
	parent = _get_current_span()
	span_id = uuid.uuid4().hex
	start_ts = time.time()
	rec_base: Dict[str, Any] = {
		"span_id": span_id,
		"parent_span_id": (parent.get("span_id") if isinstance(parent, dict) else None),
		"name": str(name),


		from datetime import datetime
		date_tag = datetime.utcnow().strftime("%Y%m%d")
	except Exception:
		date_tag = "00000000"
	return base / date_tag

def _write_jsonl(path: Path, obj: Dict[str, Any]) -> None:
	try:
		path.parent.mkdir(parents=True, exist_ok=True)
		with path.open("a", encoding="utf-8") as f:
			f.write(json.dumps(obj, ensure_ascii=False) + "\n")
	except Exception:
		pass

def _get_current_span() -> Optional[Dict[str, Any]]:
	return getattr(_SPAN_LOCAL, "current", None)

def _set_current_span(span: Optional[Dict[str, Any]]) -> None:
	setattr(_SPAN_LOCAL, "current", span)

@contextmanager
def trace_span(name: str, attrs: Optional[Dict[str, Any]] = None):
	"""Structured span context manager.

	Writes span start/end records to data/traces/spans.
	Accumulates per-span cost via meter_cost.
	"""
	parent = _get_current_span()
	span_id = uuid.uuid4().hex
	start_ts = time.time()
	rec_base: Dict[str, Any] = {
		"span_id": span_id,
		"parent_span_id": (parent.get("span_id") if isinstance(parent, dict) else None),
		"name": str(name),
		"attrs": dict(attrs or {}),
		"start_ts": start_ts,
		"start_iso": _now_iso(),
		"cost": {},


def _write_jsonl(path: Path, obj: Dict[str, Any]) -> None:
	try:
		path.parent.mkdir(parents=True, exist_ok=True)
		with path.open("a", encoding="utf-8") as f:
			f.write(json.dumps(obj, ensure_ascii=False) + "\n")
	except Exception:
		pass

def _get_current_span() -> Optional[Dict[str, Any]]:
	return getattr(_SPAN_LOCAL, "current", None)

def _set_current_span(span: Optional[Dict[str, Any]]) -> None:
	setattr(_SPAN_LOCAL, "current", span)

@contextmanager
def trace_span(name: str, attrs: Optional[Dict[str, Any]] = None):
	"""Structured span context manager.

	Writes span start/end records to data/traces/spans.
	Accumulates per-span cost via meter_cost.
	"""
	parent = _get_current_span()
	span_id = uuid.uuid4().hex
	start_ts = time.time()
	rec_base: Dict[str, Any] = {
		"span_id": span_id,
		"parent_span_id": (parent.get("span_id") if isinstance(parent, dict) else None),
		"name": str(name),
		"attrs": dict(attrs or {}),
		"start_ts": start_ts,
		"start_iso": _now_iso(),
		"cost": {},
	}
	_set_current_span(rec_base)
	# Emit start event
	_write_jsonl(_span_dir() / "spans.jsonl", {**rec_base, "event": "start"})
	try:
		yield rec_base
	finally:
		end_ts = time.time()
		rec_base["end_ts"] = end_ts
		rec_base["end_iso"] = _now_iso()
		rec_base["duration_ms"] = int(max(0.0, (end_ts - start_ts)) * 1000)
		_write_jsonl(_span_dir() / "spans.jsonl", {**rec_base, "event": "end"})
		# Restore parent span
		_set_current_span(parent if isinstance(parent, dict) else None)

def meter_cost(kind: str, amount: float) -> None:
	"""Accumulate a numeric cost metric into the active span.

	If AGI_COST_RATE_* environment variables are set, callers may convert raw counts
	(e.g., tokens or seconds) to dollars externally and also meter those here.
	"""
	span = _get_current_span()
	if not isinstance(span, dict):
		return
	try:
		k = str(kind)
		v = float(amount)
		cost: Dict[str, float] = span.setdefault("cost", {})  # type: ignore[assignment]
		cost[k] = float(cost.get(k, 0.0) + v)
	except Exception:
		return


		"attrs": dict(attrs or {}),
		"start_ts": start_ts,
		"start_iso": _now_iso(),
		"cost": {},
	}
	_set_current_span(rec_base)
	# Emit start event
	_write_jsonl(_span_dir() / "spans.jsonl", {**rec_base, "event": "start"})
	try:
		yield rec_base
	finally:
		end_ts = time.time()
		rec_base["end_ts"] = end_ts
		rec_base["end_iso"] = _now_iso()
		rec_base["duration_ms"] = int(max(0.0, (end_ts - start_ts)) * 1000)
		_write_jsonl(_span_dir() / "spans.jsonl", {**rec_base, "event": "end"})
		# Restore parent span
		_set_current_span(parent if isinstance(parent, dict) else None)

def meter_cost(kind: str, amount: float) -> None:
	"""Accumulate a numeric cost metric into the active span.

	If AGI_COST_RATE_* environment variables are set, callers may convert raw counts
	(e.g., tokens or seconds) to dollars externally and also meter those here.
	"""
	span = _get_current_span()
	if not isinstance(span, dict):
		return
	try:
		k = str(kind)
		v = float(amount)
		cost: Dict[str, float] = span.setdefault("cost", {})  # type: ignore[assignment]
		cost[k] = float(cost.get(k, 0.0) + v)
	except Exception:
		return

def current_span_id() -> Optional[str]:
	span = _get_current_span()
	return (span.get("span_id") if isinstance(span, dict) else None)


def meter_cost(kind: str, amount: float) -> None:
	"""Accumulate a numeric cost metric into the active span.

	If AGI_COST_RATE_* environment variables are set, callers may convert raw counts
	(e.g., tokens or seconds) to dollars externally and also meter those here.
	"""
	span = _get_current_span()
	if not isinstance(span, dict):
		return
	try:
		k = str(kind)
		v = float(amount)
		cost: Dict[str, float] = span.setdefault("cost", {})  # type: ignore[assignment]
		cost[k] = float(cost.get(k, 0.0) + v)
	except Exception:
		return

def current_span_id() -> Optional[str]:
	span = _get_current_span()
	return (span.get("span_id") if isinstance(span, dict) else None)


from __future__ import annotations

import json
import time
import uuid
from pathlib import Path
from typing import Any, Dict, Optional

def _jobs_dir() -> Path:
	try:
		return Path(__file__).resolve().parents[2] / "data" / "jobs"
	except Exception:
		return Path.cwd() / "data" / "jobs"

def job_enqueue(task_spec: Dict[str, Any], delay_sec: int = 0, schedule: Optional[str] = None) -> str:
	jid = uuid.uuid4().hex
	p = _jobs_dir() / f"{jid}.json"
	p.parent.mkdir(parents=True, exist_ok=True)
	rec = {
		"job_id": jid,
		"state": "queued",
		"task": task_spec,
		"created_ts": time.time(),
		"run_after": (time.time() + max(0, int(delay_sec))),
		"schedule": schedule,
	}
	p.write_text(json.dumps(rec, ensure_ascii=False, indent=2), encoding="utf-8")
	return jid

def job_status(job_id: str) -> Dict[str, Any]:
	p = _jobs_dir() / f"{job_id}.json"
	if not p.exists():
		return {"ok": False, "error": "not_found", "job_id": job_id}
	try:
		return json.loads(p.read_text(encoding="utf-8"))
	except Exception:
		return {"ok": False, "error": "read_error", "job_id": job_id}

def cron_set(spec: Dict[str, Any]) -> str:
	"""Store a simple cron-like spec for external runner to honor.

	Returns an id for the cron entry.
	"""
	cid = uuid.uuid4().hex
	p = _jobs_dir() / "cron" / f"{cid}.json"
	p.parent.mkdir(parents=True, exist_ok=True)
	p.write_text(json.dumps({"id": cid, **spec}, ensure_ascii=False, indent=2), encoding="utf-8")
	return cid


from __future__ import annotations

import json
import time
import uuid
from pathlib import Path
from typing import Any, Dict, Optional

def _jobs_dir() -> Path:
	try:
		return Path(__file__).resolve().parents[2] / "data" / "jobs"
	except Exception:
		return Path.cwd() / "data" / "jobs"

def job_enqueue(task_spec: Dict[str, Any], delay_sec: int = 0, schedule: Optional[str] = None) -> str:
	jid = uuid.uuid4().hex
	p = _jobs_dir() / f"{jid}.json"
	p.parent.mkdir(parents=True, exist_ok=True)
	rec = {
		"job_id": jid,
		"state": "queued",
		"task": task_spec,
		"created_ts": time.time(),
		"run_after": (time.time() + max(0, int(delay_sec))),
		"schedule": schedule,
	}
	p.write_text(json.dumps(rec, ensure_ascii=False, indent=2), encoding="utf-8")
	return jid

def job_status(job_id: str) -> Dict[str, Any]:
	p = _jobs_dir() / f"{job_id}.json"


from __future__ import annotations

import json
import time
import uuid
from pathlib import Path
from typing import Any, Dict, Optional

def _jobs_dir() -> Path:
	try:
		return Path(__file__).resolve().parents[2] / "data" / "jobs"
	except Exception:
		return Path.cwd() / "data" / "jobs"

def job_enqueue(task_spec: Dict[str, Any], delay_sec: int = 0, schedule: Optional[str] = None) -> str:
	jid = uuid.uuid4().hex
	p = _jobs_dir() / f"{jid}.json"
	p.parent.mkdir(parents=True, exist_ok=True)
	rec = {
		"job_id": jid,
		"state": "queued",
		"task": task_spec,
		"created_ts": time.time(),
		"run_after": (time.time() + max(0, int(delay_sec))),
		"schedule": schedule,
	}
	p.write_text(json.dumps(rec, ensure_ascii=False, indent=2), encoding="utf-8")
	return jid

def job_status(job_id: str) -> Dict[str, Any]:
	p = _jobs_dir() / f"{job_id}.json"
	if not p.exists():
		return {"ok": False, "error": "not_found", "job_id": job_id}
	try:
		return json.loads(p.read_text(encoding="utf-8"))
	except Exception:
		return {"ok": False, "error": "read_error", "job_id": job_id}

def cron_set(spec: Dict[str, Any]) -> str:
	"""Store a simple cron-like spec for external runner to honor.

	Returns an id for the cron entry.
	"""
	cid = uuid.uuid4().hex
	p = _jobs_dir() / "cron" / f"{cid}.json"
	p.parent.mkdir(parents=True, exist_ok=True)


	except Exception:
		return Path.cwd() / "data" / "jobs"

def job_enqueue(task_spec: Dict[str, Any], delay_sec: int = 0, schedule: Optional[str] = None) -> str:
	jid = uuid.uuid4().hex
	p = _jobs_dir() / f"{jid}.json"
	p.parent.mkdir(parents=True, exist_ok=True)
	rec = {
		"job_id": jid,
		"state": "queued",
		"task": task_spec,
		"created_ts": time.time(),
		"run_after": (time.time() + max(0, int(delay_sec))),
		"schedule": schedule,
	}
	p.write_text(json.dumps(rec, ensure_ascii=False, indent=2), encoding="utf-8")
	return jid

def job_status(job_id: str) -> Dict[str, Any]:
	p = _jobs_dir() / f"{job_id}.json"
	if not p.exists():
		return {"ok": False, "error": "not_found", "job_id": job_id}
	try:
		return json.loads(p.read_text(encoding="utf-8"))
	except Exception:
		return {"ok": False, "error": "read_error", "job_id": job_id}

def cron_set(spec: Dict[str, Any]) -> str:
	"""Store a simple cron-like spec for external runner to honor.

	Returns an id for the cron entry.
	"""
	cid = uuid.uuid4().hex
	p = _jobs_dir() / "cron" / f"{cid}.json"
	p.parent.mkdir(parents=True, exist_ok=True)
	p.write_text(json.dumps({"id": cid, **spec}, ensure_ascii=False, indent=2), encoding="utf-8")
	return cid


		"state": "queued",
		"task": task_spec,
		"created_ts": time.time(),
		"run_after": (time.time() + max(0, int(delay_sec))),
		"schedule": schedule,
	}
	p.write_text(json.dumps(rec, ensure_ascii=False, indent=2), encoding="utf-8")
	return jid

def job_status(job_id: str) -> Dict[str, Any]:
	p = _jobs_dir() / f"{job_id}.json"
	if not p.exists():
		return {"ok": False, "error": "not_found", "job_id": job_id}
	try:
		return json.loads(p.read_text(encoding="utf-8"))
	except Exception:
		return {"ok": False, "error": "read_error", "job_id": job_id}

def cron_set(spec: Dict[str, Any]) -> str:
	"""Store a simple cron-like spec for external runner to honor.

	Returns an id for the cron entry.
	"""
	cid = uuid.uuid4().hex
	p = _jobs_dir() / "cron" / f"{cid}.json"
	p.parent.mkdir(parents=True, exist_ok=True)
	p.write_text(json.dumps({"id": cid, **spec}, ensure_ascii=False, indent=2), encoding="utf-8")
	return cid


from __future__ import annotations

import hashlib
import json
import os
import time
from pathlib import Path
from typing import Any, Dict, Optional, Tuple, Union

def _root() -> Path:
	try:
		return Path(__file__).resolve().parents[2] / "data" / "artifacts"
	except Exception:
		return Path.cwd() / "data" / "artifacts"

def _safe_key_path(key: str) -> Path:
	# Normalize key into nested directories based on sha256 of key
	ks = str(key).strip()
	if not ks:
		raise ValueError("empty artifact key")
	uid = hashlib.sha256(ks.encode("utf-8")).hexdigest()
	return _root() / uid[:2] / uid[2:4] / uid

def artifact_put(key: str, blob: Union[bytes, str], ttl_sec: int = 0, backend: str = "fs") -> Dict[str, Any]:
	"""Store an artifact blob under a key with optional TTL.

	Backend 'fs' writes to data/artifacts; returns metadata including digest and path.
	"""
	if backend != "fs":
		raise NotImplementedError("only fs backend is implemented")
	data: bytes
	if isinstance(blob, str):
		data = blob.encode("utf-8")
	else:
		data = blob
	path = _safe_key_path(key)
	path.parent.mkdir(parents=True, exist_ok=True)
	digest = hashlib.sha256(data).hexdigest()
	bin_path = path.with_suffix(".bin")
	meta_path = path.with_suffix(".json")
	bin_path.write_bytes(data)
	meta = {
		"key": key,
		"backend": backend,
		"digest": digest,
		"size": len(data),
		"created_ts": time.time(),
		"ttl_sec": int(ttl_sec or 0),
		"path": str(bin_path),
	}
	meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
	return meta

def _expired(created_ts: float, ttl_sec: int) -> bool:
	return bool(ttl_sec > 0 and (time.time() - float(created_ts)) > float(ttl_sec))

def artifact_get(key: str, backend: str = "fs") -> Optional[Dict[str, Any]]:
	"""Load an artifact by key if present and not expired.

	Returns dict with bytes (as base64 str), digest, size, and path. Returns None if missing/expired.
	"""
	if backend != "fs":
		raise NotImplementedError("only fs backend is implemented")
	path = _safe_key_path(key)
	meta_path = path.with_suffix(".json")
	bin_path = path.with_suffix(".bin")
	if not meta_path.exists() or not bin_path.exists():
		return None
	try:
		meta = json.loads(meta_path.read_text(encoding="utf-8"))
		if _expired(float(meta.get("created_ts", 0.0)), int(meta.get("ttl_sec", 0))):
			return None
		data = bin_path.read_bytes()
		return {"key": key, "digest": str(meta.get("digest")), "size": len(data), "bytes": data, "path": str(bin_path)}
	except Exception:
		return None


from __future__ import annotations

import hashlib
import json
import os
import time
from pathlib import Path
from typing import Any, Dict, Optional, Tuple, Union

def _root() -> Path:
	try:
		return Path(__file__).resolve().parents[2] / "data" / "artifacts"
	except Exception:
		return Path.cwd() / "data" / "artifacts"

def _safe_key_path(key: str) -> Path:
	# Normalize key into nested directories based on sha256 of key
	ks = str(key).strip()
	if not ks:
		raise ValueError("empty artifact key")
	uid = hashlib.sha256(ks.encode("utf-8")).hexdigest()
	return _root() / uid[:2] / uid[2:4] / uid

def artifact_put(key: str, blob: Union[bytes, str], ttl_sec: int = 0, backend: str = "fs") -> Dict[str, Any]:
	"""Store an artifact blob under a key with optional TTL.

	Backend 'fs' writes to data/artifacts; returns metadata including digest and path.
	"""
	if backend != "fs":
		raise NotImplementedError("only fs backend is implemented")
	data: bytes
	if isinstance(blob, str):


from __future__ import annotations

import hashlib
import json
import os
import time
from pathlib import Path
from typing import Any, Dict, Optional, Tuple, Union

def _root() -> Path:
	try:
		return Path(__file__).resolve().parents[2] / "data" / "artifacts"
	except Exception:
		return Path.cwd() / "data" / "artifacts"

def _safe_key_path(key: str) -> Path:
	# Normalize key into nested directories based on sha256 of key
	ks = str(key).strip()
	if not ks:
		raise ValueError("empty artifact key")
	uid = hashlib.sha256(ks.encode("utf-8")).hexdigest()
	return _root() / uid[:2] / uid[2:4] / uid

def artifact_put(key: str, blob: Union[bytes, str], ttl_sec: int = 0, backend: str = "fs") -> Dict[str, Any]:
	"""Store an artifact blob under a key with optional TTL.

	Backend 'fs' writes to data/artifacts; returns metadata including digest and path.
	"""
	if backend != "fs":
		raise NotImplementedError("only fs backend is implemented")
	data: bytes
	if isinstance(blob, str):
		data = blob.encode("utf-8")
	else:
		data = blob
	path = _safe_key_path(key)
	path.parent.mkdir(parents=True, exist_ok=True)
	digest = hashlib.sha256(data).hexdigest()
	bin_path = path.with_suffix(".bin")
	meta_path = path.with_suffix(".json")
	bin_path.write_bytes(data)


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

def _root() -> Path:
	try:
		return Path(__file__).resolve().parents[2] / "data" / "artifacts"
	except Exception:
		return Path.cwd() / "data" / "artifacts"

def _safe_key_path(key: str) -> Path:
	# Normalize key into nested directories based on sha256 of key
	ks = str(key).strip()
	if not ks:
		raise ValueError("empty artifact key")
	uid = hashlib.sha256(ks.encode("utf-8")).hexdigest()
	return _root() / uid[:2] / uid[2:4] / uid

def artifact_put(key: str, blob: Union[bytes, str], ttl_sec: int = 0, backend: str = "fs") -> Dict[str, Any]:
	"""Store an artifact blob under a key with optional TTL.

	Backend 'fs' writes to data/artifacts; returns metadata including digest and path.
	"""
	if backend != "fs":
		raise NotImplementedError("only fs backend is implemented")
	data: bytes
	if isinstance(blob, str):
		data = blob.encode("utf-8")
	else:
		data = blob
	path = _safe_key_path(key)
	path.parent.mkdir(parents=True, exist_ok=True)
	digest = hashlib.sha256(data).hexdigest()
	bin_path = path.with_suffix(".bin")
	meta_path = path.with_suffix(".json")
	bin_path.write_bytes(data)
	meta = {
		"key": key,
		"backend": backend,
		"digest": digest,
		"size": len(data),
		"created_ts": time.time(),
		"ttl_sec": int(ttl_sec or 0),
		"path": str(bin_path),
	}
	meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
	return meta

def _expired(created_ts: float, ttl_sec: int) -> bool:
	return bool(ttl_sec > 0 and (time.time() - float(created_ts)) > float(ttl_sec))

def artifact_get(key: str, backend: str = "fs") -> Optional[Dict[str, Any]]:
	"""Load an artifact by key if present and not expired.

	Returns dict with bytes (as base64 str), digest, size, and path. Returns None if missing/expired.
	"""
	if backend != "fs":
		raise NotImplementedError("only fs backend is implemented")
	path = _safe_key_path(key)
	meta_path = path.with_suffix(".json")
	bin_path = path.with_suffix(".bin")
	if not meta_path.exists() or not bin_path.exists():
		return None
	try:
		meta = json.loads(meta_path.read_text(encoding="utf-8"))


		data = blob
	path = _safe_key_path(key)
	path.parent.mkdir(parents=True, exist_ok=True)
	digest = hashlib.sha256(data).hexdigest()
	bin_path = path.with_suffix(".bin")
	meta_path = path.with_suffix(".json")
	bin_path.write_bytes(data)
	meta = {
		"key": key,
		"backend": backend,
		"digest": digest,
		"size": len(data),
		"created_ts": time.time(),
		"ttl_sec": int(ttl_sec or 0),
		"path": str(bin_path),
	}
	meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
	return meta

def _expired(created_ts: float, ttl_sec: int) -> bool:
	return bool(ttl_sec > 0 and (time.time() - float(created_ts)) > float(ttl_sec))

def artifact_get(key: str, backend: str = "fs") -> Optional[Dict[str, Any]]:
	"""Load an artifact by key if present and not expired.

	Returns dict with bytes (as base64 str), digest, size, and path. Returns None if missing/expired.
	"""
	if backend != "fs":
		raise NotImplementedError("only fs backend is implemented")
	path = _safe_key_path(key)
	meta_path = path.with_suffix(".json")
	bin_path = path.with_suffix(".bin")
	if not meta_path.exists() or not bin_path.exists():
		return None
	try:
		meta = json.loads(meta_path.read_text(encoding="utf-8"))
		if _expired(float(meta.get("created_ts", 0.0)), int(meta.get("ttl_sec", 0))):
			return None
		data = bin_path.read_bytes()
		return {"key": key, "digest": str(meta.get("digest")), "size": len(data), "bytes": data, "path": str(bin_path)}


	bin_path = path.with_suffix(".bin")
	meta_path = path.with_suffix(".json")
	bin_path.write_bytes(data)
	meta = {
		"key": key,
		"backend": backend,
		"digest": digest,
		"size": len(data),
		"created_ts": time.time(),
		"ttl_sec": int(ttl_sec or 0),
		"path": str(bin_path),
	}
	meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
	return meta

def _expired(created_ts: float, ttl_sec: int) -> bool:
	return bool(ttl_sec > 0 and (time.time() - float(created_ts)) > float(ttl_sec))

def artifact_get(key: str, backend: str = "fs") -> Optional[Dict[str, Any]]:
	"""Load an artifact by key if present and not expired.

	Returns dict with bytes (as base64 str), digest, size, and path. Returns None if missing/expired.
	"""
	if backend != "fs":
		raise NotImplementedError("only fs backend is implemented")
	path = _safe_key_path(key)
	meta_path = path.with_suffix(".json")
	bin_path = path.with_suffix(".bin")
	if not meta_path.exists() or not bin_path.exists():
		return None
	try:
		meta = json.loads(meta_path.read_text(encoding="utf-8"))
		if _expired(float(meta.get("created_ts", 0.0)), int(meta.get("ttl_sec", 0))):
			return None
		data = bin_path.read_bytes()
		return {"key": key, "digest": str(meta.get("digest")), "size": len(data), "bytes": data, "path": str(bin_path)}
	except Exception:
		return None


from __future__ import annotations

import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

@dataclass
class MemoryAugmentConfig:
	# Episodic memory
	use_memory: bool = False
	mem_path: Optional[str] = None
	mem_topk: int = 3
	mem_recency: float = 0.0
	mem_query: Optional[str] = None
	# Code index (planner context)
	index_k: int = 0
	index_path: Optional[str] = None
	# Policy injections
	inject_dom_policy: bool = True
	inject_cli_policy: bool = True
	inject_caps: bool = True  # plan_risk.json caps/budgets

def retrieve_episodic(
	obs: Dict[str, Any],
	cfg: MemoryAugmentConfig,
) -> Tuple[List[Dict[str, Any]], Optional[float]]:
	"""Best-effort episodic memory retrieval. Returns (snippets, query_ms)."""
	if not bool(getattr(cfg, "use_memory", False)):
		return [], None
	try:
		from agi_dw.core.memory.episodic import EpisodicMemory  # type: ignore
		mem = EpisodicMemory.load(cfg.mem_path)
		qtxt = str(cfg.mem_query) if (getattr(cfg, "mem_query", None) not in (None, "")) else " \n ".join([str(obs)])
		import time as _t  # type: ignore
		_t0 = _t.time()
		snips = mem.query(qtxt, k=max(1, int(cfg.mem_topk)), recency_weight=float(cfg.mem_recency))
		ms = float(((_t.time() - _t0) * 1000.0))
		return snips, ms
	except Exception:
		return [], None

def _rank_code_index(obs: Dict[str, Any], idx_obj: Dict[str, Any], k: int) -> Optional[List[Dict[str, Any]]]:
	try:
		from agi_dw.tools.index_rank import rank_index_candidates  # type: ignore
		ranked = rank_index_candidates(obs, idx_obj, k)
		return ranked if ranked else None
	except Exception:
		return None

def augment_observation(
	obs: Dict[str, Any],
	root_dir: Path,
	cfg: MemoryAugmentConfig,
) -> Tuple[Dict[str, Any], List[Dict[str, Any]], Optional[float]]:
	"""
	Return (obs_aug, mem_snippets, mem_query_ms) with episodic memory, optional code index,
	and optional policy/caps injections.
	"""
	obs_aug = dict(obs)
	# Episodic memory
	snippets, query_ms = retrieve_episodic(obs, cfg)
	if snippets:
		obs_aug["memory"] = [s.get("text", "") for s in snippets]

	# Code index augmentation
	try:
		k = max(0, int(getattr(cfg, "index_k", 0) or 0))
		if k > 0:
			idx_obj = None
			ip = Path(getattr(cfg, "index_path", "") or "")
			if str(ip) and ip.exists():
				idx_obj = json.loads(ip.read_text(encoding="utf-8"))
			else:
				cwd = str((obs.get("meta", {}) or {}).get("cwd", ""))
				if cwd:
					try:
						from agi_dw.tools.code_index import index_python_repo  # type: ignore
						idx_obj = index_python_repo(cwd)
						try:
							ip.parent.mkdir(parents=True, exist_ok=True)
							ip.write_text(json.dumps(idx_obj, ensure_ascii=False, indent=2), encoding="utf-8")
						except Exception:
							pass
					except Exception:
						idx_obj = None
			# Fallback inspiration index in repo
			if not isinstance(idx_obj, dict) or not idx_obj:
				insp_path = root_dir / "data" / "sandbox" / "inspiration" / "index.json"
				if insp_path.exists():
					try:
						idx_obj = json.loads(insp_path.read_text(encoding="utf-8"))
					except Exception:
						idx_obj = None
			if isinstance(idx_obj, dict):
				ranked = _rank_code_index(obs, idx_obj, k)
				if ranked:
					obs_aug["index"] = ranked
	except Exception:
		pass

	# Policy/caps injections
	try:
		import os as _os  # type: ignore
		if cfg.inject_dom_policy:
			allow = _os.environ.get("AGI_DOM_ALLOWLIST")
			block = _os.environ.get("AGI_DOM_BLOCKLIST")
			if allow or block:
				obs_aug["policy"] = obs_aug.get("policy", {})
				if allow:
					obs_aug["policy"]["dom_allowlist"] = [s.strip() for s in str(allow).split(",") if s.strip()]
				if block:
					obs_aug["policy"]["dom_blocklist"] = [s.strip() for s in str(block).split(",") if s.strip()]
		if cfg.inject_cli_policy:
			env_allow = _os.environ.get("AGI_RUNNER_ENV_ALLOWLIST")
			if env_allow:
				obs_aug["policy"] = obs_aug.get("policy", {})
				obs_aug["policy"]["env_allowlist"] = [s.strip() for s in str(env_allow).split(",") if s.strip()]
		if cfg.inject_caps:
			pr_path = root_dir / "data" / "sandbox" / "tmp" / "plan_risk.json"
			if pr_path.exists():
				obj = json.loads(pr_path.read_text(encoding="utf-8"))
				caps = obj.get("caps", {}) if isinstance(obj, dict) else {}
				if isinstance(caps, dict) and caps:
					obs_aug["policy"] = obs_aug.get("policy", {})
					obs_aug["policy"]["caps"] = {
						k: int(v) if isinstance(v, (int, float)) and k != "max_risk" else float(v) if k == "max_risk" else v
						for k, v in caps.items()
					}
	except Exception:
		pass

	return obs_aug, snippets, query_ms

def match_skill_action(
	domain: str,
	root_dir: Path,
	obs: Dict[str, Any],
	action: Dict[str, Any],
) -> Tuple[Dict[str, Any], Dict[str, Optional[str]]]:
	"""
	Try to match a promoted skill and return possibly replaced action and any adapter overrides
	(e.g., {"verifier": path, "planner": path}).
	"""
	try:
		from agi_dw.core.memory.skills import SkillLibrary  # type: ignore
		lib = SkillLibrary(str(root_dir))
		adapters: Dict[str, Optional[str]] = {"verifier": None, "planner": None}
		if domain == "cli":
			tool = (action or {}).get("tool") if isinstance(action, dict) else None
			argv0 = None
			try:
				argv0 = (action or {}).get("args", {}).get("argv", [None])[0]
			except Exception:
				argv0 = None
			sig: Dict[str, str] = {}
			if tool:
				sig["tool"] = str(tool)
			if argv0:
				sig["argv0"] = str(argv0)
			if sig:
				sk = lib.match("cli", sig)
				if sk is not None and isinstance(sk.action, dict):
					action = sk.action
					action["skill_id"] = sk.id
					try:
						sk_obj = lib.get_by_id(sk.id)
						if sk_obj and sk_obj.adapters:
							adapters["verifier"] = sk_obj.adapters.get("verifier")
							adapters["planner"] = sk_obj.adapters.get("planner")
					except Exception:
						pass
			return action, adapters
		elif domain == "dom":
			meta = obs.get("meta", {}) if isinstance(obs, dict) else {}
			url_sig = meta.get("url") or (action.get("args", {}).get("url") if isinstance(action, dict) else None)
			sel_sig = meta.get("selector") or (action.get("args", {}).get("selector") if isinstance(action, dict) else None)
			sig = {"url": str(url_sig or ""), "selector": str(sel_sig or "")}
			sk = lib.match("dom", sig)
			if sk is not None and isinstance(sk.action, dict):
				action = sk.action
				action["skill_id"] = sk.id
				try:
					sk_obj = lib.get_by_id(sk.id)
					if sk_obj and sk_obj.adapters:
						adapters["verifier"] = sk_obj.adapters.get("verifier")
						adapters["planner"] = sk_obj.adapters.get("planner")
				except Exception:
					pass
			return action, adapters
		else:
			return action, {"verifier": None, "planner": None}
	except Exception:
		return action, {"verifier": None, "planner": None}

def get_adapter_bank(root_dir: Path, bank_name: Optional[str]) -> Dict[str, str]:
	"""Return an adapter mapping for roles (e.g., {"planner": dir, "verifier": dir}) from a named bank.

	If bank_name is None or missing, returns an empty mapping.
	"""
	if not bank_name:
		return {}
	try:
		from agi_dw.core.memory.skills import SkillLibrary  # type: ignore
		lib = SkillLibrary(str(root_dir))
		bank = lib.get_adapter_bank(str(bank_name)) or {}
		out: Dict[str, str] = {}
		for role in ("planner", "verifier"):
			val = bank.get(role)
			if isinstance(val, str) and val.strip():
				out[role] = val.strip()
		return out
	except Exception:
		return {}

def pick_skill_adapters(root_dir: Path, domain: str, signature: Dict[str, str]) -> Dict[str, str]:
	"""Attempt to find a matching skill and return its adapters mapping if present."""
	try:
		from agi_dw.core.memory.skills import SkillLibrary  # type: ignore
		lib = SkillLibrary(str(root_dir))
		sk = lib.match(domain, signature)
		if sk and sk.adapters:
			out: Dict[str, str] = {}
			for role in ("planner", "verifier"):
				val = sk.adapters.get(role) if isinstance(sk.adapters, dict) else None
				if isinstance(val, str) and val.strip():
					out[role] = val.strip()
			return out
	except Exception:
		pass
	return {}


MemoryAugmentConfig

from __future__ import annotations

import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

@dataclass
class MemoryAugmentConfig:
	# Episodic memory
	use_memory: bool = False
	mem_path: Optional[str] = None
	mem_topk: int = 3
	mem_recency: float = 0.0
	mem_query: Optional[str] = None
	# Code index (planner context)
	index_k: int = 0
	index_path: Optional[str] = None
	# Policy injections
	inject_dom_policy: bool = True
	inject_cli_policy: bool = True
	inject_caps: bool = True  # plan_risk.json caps/budgets

def retrieve_episodic(
	obs: Dict[str, Any],
	cfg: MemoryAugmentConfig,
) -> Tuple[List[Dict[str, Any]], Optional[float]]:
	"""Best-effort episodic memory retrieval. Returns (snippets, query_ms)."""
	if not bool(getattr(cfg, "use_memory", False)):
		return [], None
	try:
		from agi_dw.core.memory.episodic import EpisodicMemory  # type: ignore
		mem = EpisodicMemory.load(cfg.mem_path)
		qtxt = str(cfg.mem_query) if (getattr(cfg, "mem_query", None) not in (None, "")) else " \n ".join([str(obs)])
		import time as _t  # type: ignore
		_t0 = _t.time()
		snips = mem.query(qtxt, k=max(1, int(cfg.mem_topk)), recency_weight=float(cfg.mem_recency))
		ms = float(((_t.time() - _t0) * 1000.0))
		return snips, ms
	except Exception:
		return [], None


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

@dataclass
class MemoryAugmentConfig:
	# Episodic memory
	use_memory: bool = False
	mem_path: Optional[str] = None
	mem_topk: int = 3
	mem_recency: float = 0.0
	mem_query: Optional[str] = None
	# Code index (planner context)
	index_k: int = 0
	index_path: Optional[str] = None
	# Policy injections
	inject_dom_policy: bool = True
	inject_cli_policy: bool = True
	inject_caps: bool = True  # plan_risk.json caps/budgets

def retrieve_episodic(
	obs: Dict[str, Any],
	cfg: MemoryAugmentConfig,
) -> Tuple[List[Dict[str, Any]], Optional[float]]:
	"""Best-effort episodic memory retrieval. Returns (snippets, query_ms)."""
	if not bool(getattr(cfg, "use_memory", False)):
		return [], None
	try:
		from agi_dw.core.memory.episodic import EpisodicMemory  # type: ignore
		mem = EpisodicMemory.load(cfg.mem_path)
		qtxt = str(cfg.mem_query) if (getattr(cfg, "mem_query", None) not in (None, "")) else " \n ".join([str(obs)])
		import time as _t  # type: ignore
		_t0 = _t.time()
		snips = mem.query(qtxt, k=max(1, int(cfg.mem_topk)), recency_weight=float(cfg.mem_recency))
		ms = float(((_t.time() - _t0) * 1000.0))
		return snips, ms
	except Exception:
		return [], None

def _rank_code_index(obs: Dict[str, Any], idx_obj: Dict[str, Any], k: int) -> Optional[List[Dict[str, Any]]]:
	try:
		from agi_dw.tools.index_rank import rank_index_candidates  # type: ignore
		ranked = rank_index_candidates(obs, idx_obj, k)
		return ranked if ranked else None
	except Exception:
		return None

def augment_observation(
	obs: Dict[str, Any],
	root_dir: Path,
	cfg: MemoryAugmentConfig,
) -> Tuple[Dict[str, Any], List[Dict[str, Any]], Optional[float]]:
	"""
	Return (obs_aug, mem_snippets, mem_query_ms) with episodic memory, optional code index,
	and optional policy/caps injections.
	"""


def retrieve_episodic(
	obs: Dict[str, Any],
	cfg: MemoryAugmentConfig,
) -> Tuple[List[Dict[str, Any]], Optional[float]]:
	"""Best-effort episodic memory retrieval. Returns (snippets, query_ms)."""
	if not bool(getattr(cfg, "use_memory", False)):
		return [], None
	try:
		from agi_dw.core.memory.episodic import EpisodicMemory  # type: ignore
		mem = EpisodicMemory.load(cfg.mem_path)
		qtxt = str(cfg.mem_query) if (getattr(cfg, "mem_query", None) not in (None, "")) else " \n ".join([str(obs)])
		import time as _t  # type: ignore
		_t0 = _t.time()
		snips = mem.query(qtxt, k=max(1, int(cfg.mem_topk)), recency_weight=float(cfg.mem_recency))
		ms = float(((_t.time() - _t0) * 1000.0))
		return snips, ms
	except Exception:
		return [], None

def _rank_code_index(obs: Dict[str, Any], idx_obj: Dict[str, Any], k: int) -> Optional[List[Dict[str, Any]]]:
	try:
		from agi_dw.tools.index_rank import rank_index_candidates  # type: ignore
		ranked = rank_index_candidates(obs, idx_obj, k)
		return ranked if ranked else None
	except Exception:
		return None

def augment_observation(
	obs: Dict[str, Any],
	root_dir: Path,
	cfg: MemoryAugmentConfig,
) -> Tuple[Dict[str, Any], List[Dict[str, Any]], Optional[float]]:
	"""
	Return (obs_aug, mem_snippets, mem_query_ms) with episodic memory, optional code index,
	and optional policy/caps injections.
	"""
	obs_aug = dict(obs)
	# Episodic memory
	snippets, query_ms = retrieve_episodic(obs, cfg)
	if snippets:
		obs_aug["memory"] = [s.get("text", "") for s in snippets]

	# Code index augmentation
	try:
		k = max(0, int(getattr(cfg, "index_k", 0) or 0))


		mem = EpisodicMemory.load(cfg.mem_path)
		qtxt = str(cfg.mem_query) if (getattr(cfg, "mem_query", None) not in (None, "")) else " \n ".join([str(obs)])
		import time as _t  # type: ignore
		_t0 = _t.time()
		snips = mem.query(qtxt, k=max(1, int(cfg.mem_topk)), recency_weight=float(cfg.mem_recency))
		ms = float(((_t.time() - _t0) * 1000.0))
		return snips, ms
	except Exception:
		return [], None

def _rank_code_index(obs: Dict[str, Any], idx_obj: Dict[str, Any], k: int) -> Optional[List[Dict[str, Any]]]:
	try:
		from agi_dw.tools.index_rank import rank_index_candidates  # type: ignore
		ranked = rank_index_candidates(obs, idx_obj, k)
		return ranked if ranked else None
	except Exception:
		return None

def augment_observation(
	obs: Dict[str, Any],
	root_dir: Path,
	cfg: MemoryAugmentConfig,
) -> Tuple[Dict[str, Any], List[Dict[str, Any]], Optional[float]]:
	"""
	Return (obs_aug, mem_snippets, mem_query_ms) with episodic memory, optional code index,
	and optional policy/caps injections.
	"""
	obs_aug = dict(obs)
	# Episodic memory
	snippets, query_ms = retrieve_episodic(obs, cfg)
	if snippets:
		obs_aug["memory"] = [s.get("text", "") for s in snippets]

	# Code index augmentation
	try:
		k = max(0, int(getattr(cfg, "index_k", 0) or 0))
		if k > 0:
			idx_obj = None
			ip = Path(getattr(cfg, "index_path", "") or "")
			if str(ip) and ip.exists():
				idx_obj = json.loads(ip.read_text(encoding="utf-8"))
			else:
				cwd = str((obs.get("meta", {}) or {}).get("cwd", ""))
				if cwd:
					try:
						from agi_dw.tools.code_index import index_python_repo  # type: ignore
						idx_obj = index_python_repo(cwd)
						try:
							ip.parent.mkdir(parents=True, exist_ok=True)
							ip.write_text(json.dumps(idx_obj, ensure_ascii=False, indent=2), encoding="utf-8")
						except Exception:
							pass
					except Exception:
						idx_obj = None
			# Fallback inspiration index in repo
			if not isinstance(idx_obj, dict) or not idx_obj:
				insp_path = root_dir / "data" / "sandbox" / "inspiration" / "index.json"
				if insp_path.exists():
					try:
						idx_obj = json.loads(insp_path.read_text(encoding="utf-8"))
					except Exception:
						idx_obj = None
			if isinstance(idx_obj, dict):
				ranked = _rank_code_index(obs, idx_obj, k)
				if ranked:
					obs_aug["index"] = ranked
	except Exception:
		pass

	# Policy/caps injections
	try:
		import os as _os  # type: ignore
		if cfg.inject_dom_policy:
			allow = _os.environ.get("AGI_DOM_ALLOWLIST")
			block = _os.environ.get("AGI_DOM_BLOCKLIST")
			if allow or block:
				obs_aug["policy"] = obs_aug.get("policy", {})
				if allow:
					obs_aug["policy"]["dom_allowlist"] = [s.strip() for s in str(allow).split(",") if s.strip()]
				if block:
					obs_aug["policy"]["dom_blocklist"] = [s.strip() for s in str(block).split(",") if s.strip()]
		if cfg.inject_cli_policy:
			env_allow = _os.environ.get("AGI_RUNNER_ENV_ALLOWLIST")
			if env_allow:
				obs_aug["policy"] = obs_aug.get("policy", {})
				obs_aug["policy"]["env_allowlist"] = [s.strip() for s in str(env_allow).split(",") if s.strip()]
		if cfg.inject_caps:
			pr_path = root_dir / "data" / "sandbox" / "tmp" / "plan_risk.json"
			if pr_path.exists():
				obj = json.loads(pr_path.read_text(encoding="utf-8"))
				caps = obj.get("caps", {}) if isinstance(obj, dict) else {}
				if isinstance(caps, dict) and caps:
					obs_aug["policy"] = obs_aug.get("policy", {})
					obs_aug["policy"]["caps"] = {
						k: int(v) if isinstance(v, (int, float)) and k != "max_risk" else float(v) if k == "max_risk" else v
						for k, v in caps.items()
					}
	except Exception:
		pass

	return obs_aug, snippets, query_ms

def match_skill_action(
	domain: str,
	root_dir: Path,
	obs: Dict[str, Any],
	action: Dict[str, Any],
) -> Tuple[Dict[str, Any], Dict[str, Optional[str]]]:
	"""
	Try to match a promoted skill and return possibly replaced action and any adapter overrides
	(e.g., {"verifier": path, "planner": path}).
	"""
	try:
		from agi_dw.core.memory.skills import SkillLibrary  # type: ignore
		lib = SkillLibrary(str(root_dir))
		adapters: Dict[str, Optional[str]] = {"verifier": None, "planner": None}
		if domain == "cli":
			tool = (action or {}).get("tool") if isinstance(action, dict) else None
			argv0 = None
			try: