How must an instance record the provenance of an inference-time tool call that modified the model's context?
**Date of Record:** 2026 October 27 **Scope:** Applies when an agentic session uses inference-time tool calls (function calling, retrieval during generation, or tool-augmented inference) that modify the model's context mid-generation, and the session must persist provenance showing which tool calls influenced which parts of the output. Does not apply to models without tool-use capabilities or sessions where all context is provided upfront.
Core Distinction
The separation between **final output** (the model's generated response) and **context lineage** (the sequence of tool calls that injected content into the model's context, and at what point during generation). Relying on final output alone creates the **Tool Call Attribution Gap**: the output is produced based on a sequence of tool calls and their injected content, but the session cannot reconstruct which tool calls influenced which parts of the output, breaking auditability, debugging, and safety analysis.
What Was Observed
In tool-augmented agent architectures, failing to preserve context lineage produces three primary failure modes:
- **Hallucination Attribution Failure:** The output contains incorrect information, and the agent cannot determine whether the error came from a hallucinating model or an injected bad tool result.
- **Unsafe Content Tracing:** The output contains unsafe content, and the agent cannot determine whether the model generated it or whether a tool call injected it.
- **Context Staleness Masking:** A tool call returned stale data that was injected into the context, and the model's subsequent generation silently relied on the stale data, but the output does not reveal this dependency.
Recorded Elements
- **`tool_call_sequence_manifest`:** The ordered list of tool calls executed during inference, with their inputs, outputs, and the exact context position (token offset) at which each output was injected.
- **`context_window_snapshot_hashes`:** Cryptographic hashes of the model's context window at each tool-call boundary, allowing reconstruction of the exact context the model saw at each step.
- **`output_to_tool_call_mapping`:** For output segments that depend on tool-call results, the mapping from output token ranges to the tool calls that influenced them.
- **`tool_call_provenance_signature`:** For each tool call, the cryptographic signature binding the call to its result, signed by the tool service, proving the tool result was not tampered with in transit.
Condition of Validity
* **Applies when:** The model uses inference-time tool calls, tool call results are injected into the context, and downstream consumers need to attribute parts of the output to specific sources. * **Does not apply when:** The model does not use tools during inference, or all tool results are provided upfront before generation starts.
Decision Boundary
- **Did the inference involve tool calls that modified the context?** If no, standard output provenance applies. If yes, proceed.
- **Is the `tool_call_sequence_manifest` recorded?** If no, the output cannot be attributed to specific tool calls. The agent must record tool call provenance before persisting the output.
- **Does each tool call have a `tool_call_provenance_signature`?** If no, the tool results may have been tampered with in transit. The output is `UNVERIFIED`.
- **Conversely, when can an agent accept output without context lineage?** Only when the model's output is strictly self-contained (no tool influence) or when the tool results are strictly from a verified single call with no ambiguity about attribution.
Operational Effect
- **Attribution Accuracy:** Enables tracing errors and unsafe content to their source (model vs. tool).
- **Context Integrity:** Provides cryptographic proof that tool results were not tampered with.
- **Debuggability:** Allows reconstruction of the exact model context at each tool boundary.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json
class ToolCallVerdict(Enum):
NO_TOOL_CALLS = auto()
PROVENANCE_VERIFIED = auto()
UNSIGNED_TOOL_RESULT = auto()
ATTRIBUTION_GAP = auto()
@dataclass(frozen=True)
class ToolCallRecord:
call_id: str
input_hash: str
output_hash: str
context_offset: int
tool_service_signature: str | None
@dataclass(frozen=True)
class InferenceState:
tool_calls: tuple[ToolCallRecord, ...]
output_has_attribution: bool
@dataclass(frozen=True)
class ToolCallPolicy:
require_signed_results: bool
@dataclass(frozen=True)
class ToolCallManifest:
verdict: ToolCallVerdict
directive: str
manifest_digest: str
class InferenceToolCallGuard:
@staticmethod
def _hash(data: str) -> str:
return hashlib.sha256(data.encode("utf-8")).hexdigest()
@classmethod
def evaluate_inference(
cls, state: InferenceState, policy: ToolCallPolicy
) -> ToolCallManifest:
# Gate 1: No tool calls
if not state.tool_calls:
digest = cls._hash(json.dumps({
"tool_calls": "NONE"
}, sort_keys=True))
return ToolCallManifest(
verdict=ToolCallVerdict.NO_TOOL_CALLS,
directive="PROCEED: No tool calls during inference.",
manifest_digest=digest
)
# Gate 2: Signed results
if policy.require_signed_results:
for call in state.tool_calls:
if not call.tool_service_signature:
return ToolCallManifest(
verdict=ToolCallVerdict.UNSIGNED_TOOL_RESULT,
directive=f"HALT: Tool call {call.call_id} result not signed.",
manifest_digest=cls._hash(f"UNSIGNED:{call.call_id}")
)
# Gate 3: Output attribution
if not state.output_has_attribution:
return ToolCallManifest(
verdict=ToolCallVerdict.ATTRIBUTION_GAP,
directive=f"HALT: {len(state.tool_calls)} tool calls but output lacks attribution mapping.",
manifest_digest=cls._hash(f"NO_ATTR:{len(state.tool_calls)}")
)
# Gate 4: Verified
digest = cls._hash(json.dumps({
"tool_calls": tuple(
{"id": c.call_id, "offset": c.context_offset, "output": c.output_hash}
for c in state.tool_calls
)
}, sort_keys=True))
return ToolCallManifest(
verdict=ToolCallVerdict.PROVENANCE_VERIFIED,
directive=f"PROCEED: {len(state.tool_calls)} tool calls with signed results and output attribution.",
manifest_digest=digest
)