OpenAI Agents SDK¶
Provena integrates with the OpenAI Agents SDK through run hooks that record tool outputs and agent handoff events to a tamper-evident audit trail asynchronously.
Installation¶
This installs openai-agents>=0.1.
Quick start¶
from agents import Agent, Runner
from provena import ContextTrail
from provena.integrations.openai_agents import ProvenaRunHooks
# Initialize the trail and hooks
trail = ContextTrail(storage_path="agents_audit.db")
hooks = ProvenaRunHooks(trail=trail)
agent = Agent(name="researcher", instructions="Conduct research and use tools.")
# Pass the hooks instance to Runner.run
result = await Runner.run(agent, input="Find latest report", hooks=hooks)
How it works¶
ProvenaRunHooks extends agents.RunHooks[Any]. It hooks into two core execution lifecycle events:
on_tool_end -- tool completion¶
When a tool finishes execution, on_tool_end logs the tool result:
async def on_tool_end(
self,
context: RunHooksContext[Any],
agent: Any,
tool: Any,
result: str,
) -> None:
tool_name = getattr(tool, "name", "unknown") # (1)!
agent_name = getattr(agent, "name", "unknown") # (2)!
await asyncio.to_thread( # (3)!
self._trail.log,
content=result,
source=ContextSource.TOOL,
source_name=f"openai:{tool_name}",
metadata={"agent": agent_name},
)
- Tool name is resolved from
tool.name(fallback"unknown"). - Agent name is resolved from
agent.name(fallback"unknown"). - Non-blocking asynchronous execution: Logging is executed on a worker thread via
asyncio.to_thread(), ensuring database writes do not block the active asyncio event loop. - Recorded with source type
TOOL, source nameopenai:<tool_name>, and metadata{"agent": agent_name}.
on_handoff -- agent delegation¶
When execution is handed off from one agent to another, on_handoff captures the transition:
async def on_handoff(
self,
context: RunHooksContext[Any],
from_agent: Any,
to_agent: Any,
) -> None:
from_name = getattr(from_agent, "name", "unknown") # (1)!
to_name = getattr(to_agent, "name", "unknown") # (2)!
await asyncio.to_thread( # (3)!
self._trail.log,
content=f"Handoff from {from_name} to {to_name}",
source=ContextSource.AGENT,
source_name=f"openai:{from_name}",
metadata={"to_agent": to_name},
)
- Source agent name from
from_agent.name(fallback"unknown"). - Target agent name from
to_agent.name(fallback"unknown"). - Offloaded to a worker thread via
asyncio.to_thread(). - Recorded with source type
AGENT, source nameopenai:<from_name>, content"Handoff from <from_name> to <to_name>", and metadata{"to_agent": to_name}.
Logged attributes summary¶
| Event | Source Type | Source Name | Logged Content | Metadata |
|---|---|---|---|---|
| Tool execution | ContextSource.TOOL ("tool") |
openai:<tool_name> |
String tool result | {"agent": "<agent_name>"} |
| Agent handoff | ContextSource.AGENT ("agent") |
openai:<from_name> |
"Handoff from X to Y" |
{"to_agent": "<to_agent>"} |
Thread safety and async design
Because Provena's backend storage and hash calculation perform synchronous disk I/O, ProvenaRunHooks uses asyncio.to_thread for all trail logging operations. This guarantees high-throughput asynchronous execution without event loop latency spikes.
Provenance tracking & limitations¶
- No provenance auto-extraction: Neither
on_tool_endnoron_handoffextractsProvenanceMetadatafields (source_url,author,created_at). - Default status: Records logged through
ProvenaRunHookshave provenance statusMISSING. - Scope: The hooks record tool outputs and agent handoffs. Model inputs and internal chain-of-thought tokens are not captured unless passed through tool responses.
Adding provenance to tool results
If your tools query external APIs or databases, you can log the raw external response with full ProvenanceMetadata before returning the formatted string to the agent.
Full working example¶
import asyncio
from agents import Agent, Runner, function_tool
from provena import ContextTrail
from provena.integrations.openai_agents import ProvenaRunHooks
# Define a tool
@function_tool
def fetch_system_status() -> str:
"""Check the health status of system services."""
return "All systems operational. Latency 14ms."
# Define specialist agents
triage_agent = Agent(
name="triage_agent",
instructions="Review requests and hand off to specialist.",
)
status_agent = Agent(
name="status_agent",
instructions="Report system status using available tools.",
tools=[fetch_system_status],
)
# Allow triage agent to hand off to status agent
triage_agent.handoffs = [status_agent]
async def main():
trail = ContextTrail(storage_path="agents_audit.db")
hooks = ProvenaRunHooks(trail=trail)
# Execute workflow with hooks
result = await Runner.run(
triage_agent,
input="Check the system status please.",
hooks=hooks,
)
print(f"Final output: {result.final_output}")
# Verify audit trail
verdict = trail.verify_chain()
print(f"Chain intact: {verdict.intact} ({verdict.total_records} records verified)")
trail.close()
if __name__ == "__main__":
asyncio.run(main())
Verifying the trail¶
After execution, verify the cryptographic trail of tools and handoffs:
summary = trail.summary()
print(f"Total records: {summary['total']}")
print(f"Sources: {summary['sources']}")
verdict = trail.verify_chain()
assert verdict.intact, f"Chain broken: {verdict.details}"
print(f"Chain intact: {verdict.total_records} records verified")
records = trail.query(limit=10)
for r in records:
print(f" [{r['source']}] {r['source_name']}: {r['content'][:40]}...")
trail.close()
Expected output:
Total records: 2
Sources: {'agent': 1, 'tool': 1}
Chain intact: 2 records verified
[agent] openai:triage_agent: Handoff from triage_agent to status_agent...
[tool] openai:fetch_system_status: All systems operational. Latency 14ms...