Skip to content

Policies

Custom session policies — bundle hooks that fire on agent events to govern continuation, autonomy, or session-scoped behavior.

A session policy is a named bundle of hooks that fires on agent events during a session. The two shipped policies are interactive (no hooks) and headless (a step-budget hook that ends the turn at a configurable cap). A capability ships a custom policy when the same agent should behave differently depending on which mode the user picks — tighter budget, stricter observation, an evaluation harness.

import typing as t
from dreadnode.agents.events import AgentStart, AgentStep
from dreadnode.agents.reactions import Finish
from dreadnode.core.hook import hook
from dreadnode.policies import SessionPolicy
from pydantic import Field, PrivateAttr
class TightBudgetPolicy(SessionPolicy):
name: t.ClassVar[str] = "tight-budget"
is_autonomous: t.ClassVar[bool] = True
display_label: t.ClassVar[str] = "tight"
max_steps: int = Field(default=5, gt=0)
_count: int = PrivateAttr(default=0)
@hook(AgentStart)
async def reset(self, _event: AgentStart) -> None:
self._count = 0
@hook(AgentStep)
async def stop_early(self, _event: AgentStep) -> Finish | None:
self._count += 1
if self._count >= self.max_steps:
return Finish(reason=f"max_steps={self.max_steps} reached")
return None

Drop this file under policies/ in your capability and the runtime registers it on load. Users swap to it with /policy tight-budget or {"policy": {"name": "tight-budget", "max_steps": 3}} over the API.

Policies bundle session-scoped hooks that the user opts into per session. Use one when you need behavior that’s:

  • Per-session, not always-on. Hooks that run for every session belong in the capability’s hooks/ directory; they don’t need a policy.
  • Named, so a user can swap to it via /policy <name> without knowing the implementation.
  • Stateful across the session’s events, where the state is meaningful only to one mode (a step counter, a denial budget).

Don’t reach for a policy to gate individual tool calls. Per-tool permission prompts are a separate runtime concern. Use a policy when the whole session should run differently.

Every policy declares three class-level fields. They’re ClassVar so Pydantic treats them as class attributes the runtime can read off the class without instantiating it.

FieldRequiredPurpose
nameyesRegistry key used by /policy <name> and the API. Unique across loaded policies.
is_autonomousdefault FalseWhen True, the runtime resolves any ask_user() call to deny instead of blocking on a human.
display_labeldefault ""Short string the TUI status bar renders when is_autonomous is True (e.g. "auto").

Decorate async methods with @hook(EventType) to register them. Each method receives self and the event:

import typing as t
from dreadnode.agents.events import AgentStart, ToolError
from dreadnode.core.hook import hook
from dreadnode.policies import SessionPolicy
from loguru import logger
class ObservedPolicy(SessionPolicy):
name: t.ClassVar[str] = "observed"
@hook(AgentStart)
async def announce(self, event: AgentStart) -> None:
logger.info("starting agent {}", event.agent_id)
@hook(ToolError)
async def record(self, event: ToolError) -> None:
# observe-only — no return value redirects the agent
logger.warning("tool {} errored: {}", event.tool_call.name, event.error)

A hook returns None to observe only, or a Reaction (Finish, Continue, others) to redirect the agent. The runtime collects every @hook-decorated method on the class via policy.hooks at the start of every turn and threads them into the agent’s hook bundle alongside the capability-shipped hooks.

The protocol — events, return reactions, conditions, scorers — is the same as standalone capability hooks. The full event list, decorator options, and Hook class live in the dreadnode.agents reference.

SessionPolicy is a Pydantic model, so configuration goes in normal annotated fields:

from pydantic import Field, PrivateAttr
class CappedPolicy(SessionPolicy):
name: t.ClassVar[str] = "capped"
is_autonomous: t.ClassVar[bool] = True
# config — settable via /policy capped max_steps=5
max_steps: int = Field(default=30, gt=0)
deny_message: str = "out of budget"
# private state — not exposed to API callers
_count: int = PrivateAttr(default=0)

extra="forbid" is set on the base, so a typo in /policy capped maxStep=5 raises a validation error rather than silently dropping the value. Use Field(...) for validation (gt, ge, regex, …) and PrivateAttr for runtime state — it stays out of the API spec and survives across turns within a single session.

Pydantic config validation is the only validation surface — there is no separate hook for declaring required tools or capability dependencies. If your policy needs a particular tool to be loaded, check for it inside the hook body and return Finish with a clear reason if it is missing.

Policy instances live for the session, so any state stored in self persists across user messages. If a counter or flag should reset between turns, hook AgentStart and clear it:

@hook(AgentStart)
async def reset(self, _event: AgentStart) -> None:
self._count = 0

HeadlessSessionPolicy does this for its step counter so the budget applies per turn, not per session.

my-capability/
capability.yaml
policies/
tight.py
strict.py

Auto-discovery scans policies/*.py for top-level classes with a non-empty name class attribute. Override with explicit listings in capability.yaml:

policies:
- policies/tight.py
- policies/strict.py

Set policies: [] to disable the directory entirely.

Once your capability is loaded, the policy joins the registry alongside interactive and headless:

/policy # list every registered policy
/policy capped # swap to capped with defaults
/policy capped max_steps=5 # swap with config args

The same name resolves through the API:

POST /api/sessions
{"policy": {"name": "capped", "max_steps": 5}}

POST /api/sessions/{id}/policy accepts the same shape for mid-session swaps. The TUI renders display_label in the status line whenever is_autonomous is true, so users always see what mode they’re in.

  • dreadnode.policiesSessionPolicy, register_policy, resolve_policy, registered_policy_names.
  • dreadnode.agents — the @hook decorator, the Hook class, and every event type a hook can listen for.