Tracking Context¶
Provena tracks every piece of context that flows into your AI agent. The primary
interface is the @trail.track() decorator, which automatically logs function
return values to the audit trail. For cases where a decorator does not fit,
trail.log() provides equivalent functionality as a direct call.
Basic Usage¶
Decorate any function whose return value should be governed. The decorator passes the return value through unchanged -- your application logic is not affected.
from provena import ContextTrail
trail = ContextTrail(backend="memory")
@trail.track(source="retriever")
def search_docs(query: str) -> str:
return "OpenShift 4.16 supports single-node deployments for edge."
result = search_docs("edge deployment")
print(result) # "OpenShift 4.16 supports single-node deployments for edge."
The decorator hashes the content, validates provenance, checks freshness, and appends a hash-chained record to storage -- all before returning the original value to the caller.
Tracking List Returns¶
When a function returns a list, each item is logged as a separate record. This is the natural fit for retriever functions that return multiple documents.
@trail.track(source="retriever")
def search(query: str) -> list[str]:
return [
"Minimum requirements: 8 vCPUs, 32 GB RAM.",
"Single-node deployments are supported for edge.",
]
results = search("requirements")
# Two separate records are created in the audit trail
print(trail.summary()["total"]) # 2
Tracking Dict Returns¶
Dictionary return values are JSON-serialized and logged as a single record.
@trail.track(source="tool:weather_api")
def get_weather(city: str) -> dict:
return {"city": city, "temp_c": 22, "conditions": "partly cloudy"}
weather = get_weather("Toronto")
# Logged as: '{"city": "Toronto", "temp_c": 22, "conditions": "partly cloudy"}'
Custom Content Extractor¶
When a function returns a complex object, use content_extractor to tell
Provena how to extract the loggable content.
from dataclasses import dataclass
@dataclass
class SearchResult:
text: str
score: float
metadata: dict
@trail.track(
source="retriever",
content_extractor=lambda r: r.text,
)
def ranked_search(query: str) -> SearchResult:
return SearchResult(
text="Pod scheduling uses node affinity rules.",
score=0.95,
metadata={"source": "k8s-docs"},
)
result = ranked_search("scheduling")
# Only result.text is logged to the trail
The extractor can also return a list to create multiple records from one call:
@trail.track(
source="retriever",
content_extractor=lambda results: [r.text for r in results],
)
def batch_search(query: str) -> list[SearchResult]:
return [
SearchResult(text="First result", score=0.9, metadata={}),
SearchResult(text="Second result", score=0.8, metadata={}),
]
Async Function Support¶
The decorator works transparently with async functions. No additional
configuration is needed.
import asyncio
@trail.track(source="tool:api_name")
async def fetch_data(url: str) -> str:
# In a real application, use aiohttp or httpx here
await asyncio.sleep(0.01)
return "Response payload from external API"
result = asyncio.run(fetch_data("https://api.example.com/data"))
Source Types¶
The source parameter identifies where the context came from. Use the string
format "type:name" for specificity:
| Source String | Use Case |
|---|---|
"retriever" |
Vector store or document retrieval |
"tool:api_name" |
External API tool calls |
"agent:planner" |
Output from a sub-agent |
"memory:long_term" |
Long-term memory retrieval |
"mcp:filesystem" |
Model Context Protocol server |
"custom" |
Anything that does not fit the above |
The part before the colon maps to a ContextSource enum value. The part after
the colon becomes the source_name in the audit record.
@trail.track(source="mcp:filesystem")
def read_config(path: str) -> str:
with open(path) as f:
return f.read()
@trail.track(source="agent:planner")
def plan_next_step(state: dict) -> str:
return "Retrieve the latest deployment manifest."
None Returns Are Skipped¶
If a tracked function returns None, no record is created. This prevents empty
entries from cluttering the audit trail.
@trail.track(source="retriever")
def maybe_search(query: str) -> str | None:
if not query.strip():
return None # No record logged
return "Found a relevant document."
maybe_search("") # Nothing logged
maybe_search("k8s") # One record logged
LangChain Document Support¶
Objects with a .page_content attribute (such as LangChain Document
instances) are handled automatically. The page_content string is extracted
and logged.
class Document:
"""Minimal LangChain-compatible document."""
def __init__(self, page_content: str, metadata: dict | None = None):
self.page_content = page_content
self.metadata = metadata or {}
@trail.track(source="retriever")
def langchain_retriever(query: str) -> list[Document]:
return [
Document(
page_content="Pod disruption budgets protect availability.",
metadata={"source": "https://k8s.io/docs/pdb"},
),
]
docs = langchain_retriever("availability")
# Logged content: "Pod disruption budgets protect availability."
Provenance auto-extraction
When the return value has a .metadata dict with source or source_url
keys, Provena automatically creates ProvenanceMetadata from it. See the
Provenance Validation guide for details.
Manual Logging with trail.log()¶
When a decorator does not fit your workflow, use trail.log() directly:
from provena import ContextTrail, ProvenanceMetadata
from datetime import datetime, timezone
trail = ContextTrail(backend="memory")
# Log a retrieval result with full provenance
record = trail.log(
content="Service mesh reduces inter-service latency by 40%.",
source="retriever",
source_name="knowledge_base",
provenance=ProvenanceMetadata(
source_url="https://docs.example.com/mesh",
author="Platform Team",
created_at=datetime(2025, 6, 1, tzinfo=timezone.utc),
),
)
print(record.chain_hash[:16]) # First 16 chars of the chain hash
trail.log() returns a TrailRecord on success, or None if a non-strict
error occurred.
Choosing between track() and log()
Use @trail.track() when you control the function definition and want
zero-touch logging. Use trail.log() when you receive content from
callbacks, event handlers, or third-party code where a decorator cannot
be applied.