The flight recorder
Lesson 19 found the empty slot. This lesson fills it with the toolkit's audit log, which writes to SQLite and is built to be shown to somebody who does not take your word for things.
It is called a flight recorder, and it takes a path to a database file that it creates if it is not there.
import os, tempfile
from agent_control_plane import AgentKernel, PolicyEngine, FlightRecorder
db = os.path.join(tempfile.mkdtemp(), "audit.db")
recorder = FlightRecorder(db_path=db)
policy = PolicyEngine()
policy.add_constraint("support", ["lookup_order", "issue_refund"])
kernel = AgentKernel(policy_engine=policy, audit_logger=recorder)
print("recording to a fresh database:", os.path.exists(db))A temporary directory keeps the course tidy. In a real service this is a path you keep, back up and never let the agent's own tools write to.
Three calls
from pretend_agent_governance import PretendAgent, TOOLS
agent = PretendAgent()
for message in ["Where is order A17?", "refund 40 on A17", "delete order A17"]:
tool, args = agent.decide(message)
verdict = kernel.intercept_tool_execution("support", tool, args, input_prompt=message)
if verdict is None:
TOOLS[tool](**args)
print("done")input_prompt is the fourth argument to the gate and it is optional. Pass it. It stores the sentence that led to the call, which is the difference between a log saying a refund happened and a log saying why anybody thought it should.
Reading it back
Writes are buffered, so ask for them to be flushed before reading. Lesson 21 explains why that is not an accident.
recorder.flush()
for row in sorted(recorder.query_logs(), key=lambda r: r["tool_name"]):
print(f"{row['tool_name']:14} {row['policy_verdict']:8} {row['violation_reason']}")Three rows: two allowed, one blocked with the reason beside it. The rows are sorted by tool name here because query_logs returns them newest first by timestamp, and timestamps from the same millisecond do not sort predictably.
Each row also carries the arguments, the prompt, a trace id, a timestamp and two hashes. The hashes are lesson 22.
- Drop the
input_promptargument and see what the column holds instead. - Print
row['tool_args']and notice it comes back as text rather than a dictionary. - Point a second
FlightRecorderat the same file and read the rows the first one wrote.
Every expert started right here.