# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference) # Canonical Residual: https://corpusfetch.com/c/how-must-an-instance-detect-tool-hallucination-in-multi-step-plans.md # Remnant Warrant: https://corpusfetch.com/c/how-must-an-instance-detect-tool-hallucination-in-multi-step-plans.warrant.json # Markdown SHA-256: f0e7537c46bd885b027f2469a3c23e1e7e7ba4d1a84488554e69ab7efb155d2f # Code AST SHA-256: fda1579f44574f35bda284495ed39c616c53eca337a1269e4946c48a3d6f36c1 # Verified Classes: HallucinationVerdict, PlanStep, Plan, HallucinationPolicy, HallucinationManifest, ToolHallucinationGuard # Defensive Guards: 0 fail-closed invariants (zero external deps) from dataclasses import dataclass from enum import Enum, auto import hashlib import json class HallucinationVerdict(Enum): NO_TOOLS = auto() ALL_TOOLS_VERIFIED = auto() HALLUCINATED_TOOLS_DETECTED = auto() PLAN_SAFE_TO_EXECUTE = auto() @dataclass(frozen=True) class PlanStep: step_id: str referenced_tools: frozenset[str] grounding_evidence: str | None @dataclass(frozen=True) class Plan: plan_id: str steps: tuple[PlanStep, ...] available_tools: frozenset[str] @dataclass(frozen=True) class HallucinationPolicy: require_grounding: bool allow_replanning: bool @dataclass(frozen=True) class HallucinationManifest: verdict: HallucinationVerdict hallucinated_tools: frozenset[str] directive: str manifest_digest: str class ToolHallucinationGuard: @staticmethod def _hash(data: str) -> str: return hashlib.sha256(data.encode("utf-8")).hexdigest() @classmethod def evaluate_plan( cls, plan: Plan, policy: HallucinationPolicy ) -> HallucinationManifest: # Gate 1: No tool references referenced = set() for step in plan.steps: referenced.update(step.referenced_tools) if not referenced: digest = cls._hash(json.dumps({ "plan": plan.plan_id, "tools": "NONE" }, sort_keys=True)) return HallucinationManifest( verdict=HallucinationVerdict.NO_TOOLS, hallucinated_tools=frozenset(), directive=f"PROCEED: Plan {plan.plan_id} references no tools.", manifest_digest=digest ) # Gate 2: Tool existence hallucinated = frozenset(referenced - plan.available_tools) if hallucinated: if policy.allow_replanning: return HallucinationManifest( verdict=HallucinationVerdict.HALLUCINATED_TOOLS_DETECTED, hallucinated_tools=hallucinated, directive=f"ACTION: Re-plan excluding {sorted(hallucinated)}.", manifest_digest=cls._hash(f"REPLAN:{sorted(hallucinated)}") ) return HallucinationManifest( verdict=HallucinationVerdict.HALLUCINATED_TOOLS_DETECTED, hallucinated_tools=hallucinated, directive=f"HALT: Plan references non-existent tools {sorted(hallucinated)}.", manifest_digest=cls._hash(f"HALT:{sorted(hallucinated)}") ) # Gate 3: Grounding requirement if policy.require_grounding: ungrounded = [ (s.step_id, t) for s in plan.steps for t in s.referenced_tools if not s.grounding_evidence ] if ungrounded: return HallucinationManifest( verdict=HallucinationVerdict.HALLUCINATED_TOOLS_DETECTED, hallucinated_tools=frozenset(t for _, t in ungrounded), directive=f"HALT: {len(ungrounded)} tool references lack grounding evidence.", manifest_digest=cls._hash(f"UNGROUNDED:{len(ungrounded)}") ) # Gate 4: Safe digest = cls._hash(json.dumps({ "plan": plan.plan_id, "tools": tuple(sorted(referenced)), "steps": str(len(plan.steps)) }, sort_keys=True)) return HallucinationManifest( verdict=HallucinationVerdict.PLAN_SAFE_TO_EXECUTE, hallucinated_tools=frozenset(), directive=f"PROCEED: All {len(referenced)} tools verified in plan {plan.plan_id}.", manifest_digest=digest )