Skip to main content

Format

All events are JSON objects emitted one per line (NDJSON). Every event has: One log file can hold many runs: a governance server writes every run it governs to the same place, and a project’s own log accumulates across invocations. run_id is what tells them apart, and seq is what makes a missing event visible.

ExecutionStarted

Emitted immediately before the child process is spawned. Always the first event in any log.
This is declared authority: what the agent was permitted to do, written down before it did anything. A log of refusals alone cannot answer “what was this agent allowed to do”, which is the question an auditor asks. config_hash is a fingerprint of the parsed config, so reformatting or adding a comment does not change it. Two runs with the same hash ran the same policy.

ExecutionStopped

Emitted on every exit path: clean completion, a policy stop, spawn failure, or internal error. Always the last event in a complete log.
If this event is missing from a run, the process crashed. That absence is itself a fact worth reading.

Stop reasons

| ToolDenied | A tool call was blocked because the tool is not in the allowlist. | | RuleDenied | A tool call was blocked by a custom rule or a per-tool max_calls limit. | | ManualStop | Execution was stopped programmatically. | | ProcessCrashed | The child process exited with a non-zero code unexpectedly. | | BridgeUnavailable | Enforcement was active but became unreachable during rule evaluation or a tool call. Nanny fails closed, silently continuing with ungoverned execution is never allowed. |

RulesDeclared

Emitted once when the agent declares which rules it registered. The second half of declared authority.
Deduped: a caller may safely redeclare, and a second event is emitted only when the set actually changes.

AgentScopeEntered

Emitted when a function annotated with #[nanny::agent("name")] is entered. Names which phase of the run the events that follow belong to.

AgentScopeExited

Emitted when a function annotated with #[nanny::agent("name")] returns, closing the bracket opened by AgentScopeEntered.
Per-scope usage is derived by reading the events between a matching AgentScopeEntered and AgentScopeExited, which is why both carry the name.

HarnessIdentified

Emitted once when the agentic harness running the loop is identified, either auto-detected by the Python SDK from imported frameworks or declared explicitly with nanny::set_harness in Rust. Attribution only: it never affects enforcement and never carries content. De-duplicated, so an SDK may safely re-send it on every call.

AppIdentified

Emitted once when a process declares which app it is, read from the committed .nanny/app.json. Same category as HarnessIdentified: attribution only, de-duplicated, safe to re-send. This is what lets one governance server, holding one API key, serve many apps and still have each attributed separately. Identity travels in the event stream rather than being derived from the credential, for the same reason OpenTelemetry makes service.name a resource attribute rather than a transport concern.

ToolAllowed

Emitted when Nanny permits a tool call to proceed.
cleared_by is what proves a control was operating. A rule that runs clean every time is doing its job, and without this it would leave no trace at all, making a healthy run indistinguishable from one where the rule was never reached.

ToolDenied

Emitted when Nanny blocks a tool call because the tool is not in the [tools] allowed list in nanny.toml.
No cleared_by here, and that is meaningful: the allowlist is checked before any rule runs, so no rule evaluated this call. “Never reached” and “ran clean” are different answers.

RuleDenied

Emitted when a custom rule or a per-tool max_calls limit blocks a tool call. The tool was on the allowlist but a rule returned a denial before the call executed.
Rules stop at the first denial, so rules registered after rule_name never ran. They are absent from cleared_by because they produced no verdict, not because they passed.

ToolFailed

Emitted when a permitted tool fails at runtime. Distinct from ToolDenied, the tool was allowed but encountered an error (network failure, bad arguments, timeout). No tokens are recorded on failure.

LlmUsageRecorded

Emitted when LLM token usage is reported to Nanny, via the Python SDK’s instrument() or the Rust SDK’s report_usage(). Records the measured input and output tokens, plus optional model, provider, and cache-usage labels.
cache_read/cache_write are reporting-only, same as model/provider: enforcement always debits input + output, unaffected by whether either is present. They exist so a downstream cost calculator can price cache-hit tokens at their real, much cheaper rate instead of treating all input as one undifferentiated price. Every provider uses its own field name and shape for cache usage, there’s no shared convention the way there is for input/output tokens, so the SDK normalizes each provider’s own vocabulary into these two generic fields; see nanny_sdk.instrument’s module docs for the per-provider mapping.