Skip to content

dreadnode.policies

API reference for the dreadnode.policies module.

Per-session behavioral policies — agent-control hooks bound to a session.

A :class:SessionPolicy is a Pydantic-modelled class with hook methods, mirroring the :class:~dreadnode.agents.tools.Toolset pattern: subclass, declare config as fields, decorate methods with @hook(EventType), and the runtime collects them via :meth:SessionPolicy.get_hooks at turn start.

Two shipped implementations:

  • :class:InteractiveSessionPolicy — today’s TUI behavior. No continuation hooks; ask_user() flows through the runtime’s per-turn handler which publishes to both transports and awaits.
  • :class:HeadlessSessionPolicy — autonomous mode. Auto-denies ask_user() (the runtime sees is_autonomous=True and short-circuits the prompt) and attaches a max-step hook that emits Finish once a configurable cap is hit.

Policies are resolved by name via :func:resolve_policy so clients can request a mode with a simple string or \{"name": ..., **params\} dict without importing Python classes across process boundaries.

Class-level metadata fields the runtime and TUI read for status UI:

  • name — registry key. Required.
  • is_autonomous — whether the session has no human in the loop. The TUI tags labels and gates background-task notifications by this. The runtime auto-denies ask_user() when true.
  • display_label — short status-bar string when is_autonomous is true ("auto", "strict", …). Defaults to empty.

Headless mode + LLM-judged tool-call gating.

The runtime auto-denies ask_user() (inherited is_autonomous=True), enforces a per-turn step budget (inherited max_steps), and runs every tool call past a :class:ProcessJudge for allow/deny.

The judge sees a slice of the live trajectory selected by transcript_strategy. The default intent_plus_calls shows the user task plus the prior tool-call sequence (no responses) — the same cut Anthropic’s auto-mode uses for its own per-call gating. The other options trade prompt size and injection surface against how much context the judge has to reason with:

  • rubric_only — no transcript. Judge sees only the proposed call against the rubric. Cheapest, lowest signal.
  • intent_only — system + user-authored messages. The original smallest cut, useful when the rubric encodes everything you care about and you don’t want intermediate state to drift the judge.
  • intent_plus_calls (default) — adds the assistant tool-call sequence with any prose stripped from each call (no tool result content, no model justification text). The judge sees what the agent has been calling, not the words it used to justify those calls.
  • intent_plus_outputs_summaryintent_plus_calls plus tool results whose content has been replaced with a short LLM summary produced by the judge model. Assistant prose is stripped the same way; the judge sees calls + summarized results, no model-authored narrative. Caches per-tool_call_id so each result is summarized at most once per session. Costs an extra summary call per unique tool result, billed via the judge model.
  • full — the entire trajectory, including assistant prose. The only strategy that surfaces the model’s justification text to the judge. Maximum context, maximum surface.

The captured intent is also trimmed to fit the judge model’s context window: the system message and the original user task always survive, older tool-call/result messages drop first when the rendered transcript would exceed the budget. The trim emits a process_judge.intent_trimmed metric with dropped_messages and strategy attributes.

Example::

# Mid-session swap from the TUI:
# /policy guard judge_model=anthropic/claude-haiku-4-5
# /policy guard judge_model=anthropic/claude-haiku-4-5 transcript_strategy=full
# Or from the API:
POST /api/sessions/{id}/policy
{
"name": "guard",
"judge_model": "anthropic/claude-haiku-4-5",
"rubric": "In-scope: api.example.com only",
"transcript_strategy": "intent_plus_calls",
"max_steps": 20
}
hooks: list[Hook]

Inherited step-budget hooks plus the judge gate.

Autonomous mode — bounded execution, no human in the loop.

The runtime reads is_autonomous=True and resolves ask_user() to deny instantly without touching any transport. max_steps is enforced by an AgentStep hook that emits Finish(reason="max_steps=N reached") once the turn has run max_steps react cycles. The reset on AgentStart makes the counter per-turn rather than per-session, so a long chat with multiple turns each gets the full budget.

Default policy — no continuation hooks, no special prompt handling.

The runtime’s per-turn human-prompt handler does the publish/await dance directly when is_autonomous is false. This policy holds no state and contributes no hooks; it exists so the "interactive" registry key resolves to a real type.

Session-scoped agent-event hooks.

Subclass and decorate methods with @hook(EventType). The runtime calls :meth:get_hooks at turn start to collect bound Hook instances, walking the MRO so inherited hooks are included and per-class overrides win.

Class-level metadata fields:

  • name — registry key.
  • is_autonomous — runtime auto-denies ask_user() when true.
  • display_label — short label rendered by the TUI in autonomous sessions.

Per-policy configuration goes in normal Pydantic fields (e.g. HeadlessSessionPolicy.max_steps). extra="forbid" makes typos in resolve_policy payloads fail loudly. Hook is in ignored_types so the metaclass leaves @hook-decorated methods alone instead of trying to interpret them as fields — same trick :class:~dreadnode.agents.tools.Toolset uses for ToolMethod (which sidesteps it by inheriting from property).

hooks: list[Hook]

All hooks declared on this policy, bound to self.

Walks the MRO and returns every attribute that is a Hook descriptor, bound via :meth:Hook.__get__. Inherited hooks are included; subclass attributes of the same name shadow superclass ones (first occurrence in MRO order wins, mirroring :meth:~dreadnode.agents.tools.Toolset.get_tools).

get_policy_class(name: str) -> type[SessionPolicy] | None

Look up a registered policy class by name.

register_policy(
cls: type[SessionPolicy],
*,
name: str | None = None,
replace: bool = False,
) -> type[SessionPolicy]

Register a policy class into the global registry.

The registry key defaults to cls.name; pass name to override. Re-registering an existing name is a no-op unless replace=True. Returns the class unchanged so this function can be used as a decorator.

Capabilities ship policies by placing files under policies/; the capability loader picks them up and routes them through this function.

registered_policy_names() -> list[str]

Return sorted list of policy names currently in the registry.

resolve_policy(spec: _PolicySpec) -> SessionPolicy

Resolve a policy spec from the API into a policy instance.

spec may be:

  • None or "interactive" → default interactive policy
  • a string matching a registered name → policy with default params
  • a dict \{"name": ..., **params\} → policy with keyword params

Unknown names raise ValueError so mis-typed policy names in a request payload fail loudly at session-create time instead of silently falling back to interactive.