"""02 · Agent Core Components v1.1 demo.

离线、确定性组件契约实验：验证 Policy / Tools / State / Runtime 边界、
行动白名单、完成门禁与预算耗尽。标准库 only；不是真实 LLM / 工具调用。
运行：python3 02-Agent-Core-Components-v1.1-demo.py
"""
from dataclasses import dataclass, field
from typing import Any


@dataclass(frozen=True)
class Task:
    required: tuple[str, ...] = ("framework_a", "framework_b")
    allowed: frozenset[str] = frozenset({"lookup", "write_report"})
    max_steps: int = 8


@dataclass(frozen=True)
class Decision:
    action: str
    args: dict[str, Any] = field(default_factory=dict)


@dataclass
class State:
    facts: dict[str, str] = field(default_factory=dict)
    artifacts: dict[str, str] = field(default_factory=dict)
    observations: list[dict[str, Any]] = field(default_factory=list)
    steps: int = 0
    status: str = "RUNNING"


class Policy:
    """确定性决策器；可替换为模型适配器。"""

    def decide(self, task: Task, state: State) -> Decision:
        for name in task.required:
            if name not in state.facts:
                return Decision("lookup", {"name": name})
        if "report" not in state.artifacts:
            return Decision("write_report", {})
        return Decision("finish", {})


class Tools:
    def __init__(self):
        self.catalog = {
            "framework_a": "支持显式图状态与节点执行",
            "framework_b": "支持持久化工作流与活动执行",
        }

    def execute(self, decision: Decision, state: State) -> dict[str, Any]:
        if decision.action == "lookup":
            name = decision.args.get("name")
            if name not in self.catalog:
                return {"ok": False, "error": "NOT_FOUND"}
            return {"ok": True, "name": name, "fact": self.catalog[name]}
        if decision.action == "write_report":
            if not state.facts:
                return {"ok": False, "error": "NO_EVIDENCE"}
            content = "\n".join(
                f"{name}: {state.facts[name]}" for name in sorted(state.facts)
            )
            return {"ok": True, "artifact": "report", "content": content}
        return {"ok": False, "error": "UNKNOWN_TOOL"}


class Runtime:
    def __init__(self, policy=None, tools=None):
        self.policy = policy or Policy()
        self.tools = tools or Tools()

    def run(self, task: Task) -> State:
        state = State()
        while state.status == "RUNNING":
            if state.steps >= task.max_steps:
                state.status = "BUDGET_EXHAUSTED"
                break
            decision = self.policy.decide(task, state)
            if decision.action == "finish":
                complete = (
                    all(name in state.facts for name in task.required)
                    and "report" in state.artifacts
                )
                state.status = "COMPLETED" if complete else "FAILED_VALIDATION"
                break
            if decision.action not in task.allowed:
                state.status = "DENIED"
                break
            state.steps += 1
            observation = self.tools.execute(decision, state)
            state.observations.append(observation)
            if not observation["ok"]:
                state.status = "TOOL_FAILED"
                break
            if decision.action == "lookup":
                state.facts[observation["name"]] = observation["fact"]
            elif decision.action == "write_report":
                state.artifacts[observation["artifact"]] = observation["content"]
        return state


if __name__ == "__main__":
    task = Task()
    result = Runtime().run(task)
    assert result.status == "COMPLETED"
    assert result.steps == 3
    assert len(result.facts) == 2
    assert "framework_a" in result.artifacts["report"]

    class UnsafePolicy:
        def decide(self, task, state):
            return Decision("delete_all_files", {})

    denied = Runtime(policy=UnsafePolicy()).run(task)
    assert denied.status == "DENIED"
    assert denied.steps == 0

    class PrematurePolicy:
        def decide(self, task, state):
            return Decision("finish", {})

    premature = Runtime(policy=PrematurePolicy()).run(task)
    assert premature.status == "FAILED_VALIDATION"

    class LoopPolicy:
        def decide(self, task, state):
            return Decision("lookup", {"name": "framework_a"})

    exhausted = Runtime(policy=LoopPolicy()).run(Task(max_steps=2))
    assert exhausted.status == "BUDGET_EXHAUSTED"
    assert exhausted.steps == 2
    print("4 component-contract tests passed")
