Playbooks¶
Structured execution plans for agents — declared step sequences with
expected tools, validation criteria, and guidance hints. Attach one
via AgentConfig.playbook; enforcement details below.
Models¶
Playbook ¶
PlaybookStep ¶
Bases: BaseModel
Individual step in a playbook.
Defines what tools are expected, hints for the agent, and optional validation criteria.
PlaybookPlan ¶
Bases: BaseModel
Active execution plan for a playbook.
Tracks progress through the playbook, including which steps have been completed, current step, and any deviations.
get_step_execution ¶
is_step_complete ¶
unresolved_required_steps ¶
Required steps that never completed — the conclusion contract.
A run may not honestly conclude while one of these is outstanding.
Source code in .sdk/src/tulip/playbooks/models.py
StepExecution ¶
Bases: BaseModel
Record of a single step's execution.
probe_coverage ¶
matched / required, and 1.0 when nothing was required.
Takes the probes rather than the step: the authoritative set is a step's
OWN probes plus those of every skill it uses, and only the enforcer
can resolve skills. Reading them off the step here produced a run that
reported 1.00 adherence while a violation on the same step said 1 of 3
matched — two answers to one question, found by running a real scenario
rather than a fixture.
A step that asked for nothing is fully covered by definition — the alternative is that every legacy playbook reports zero adherence, which would make the number worthless on the day it shipped.
Source code in .sdk/src/tulip/playbooks/models.py
unmatched_probes ¶
What this step was told to look at and did not — in declared order.
Source code in .sdk/src/tulip/playbooks/models.py
StepStatus ¶
Bases: StrEnum
Status of a playbook step.
Loader¶
load_playbook ¶
Load a playbook from various sources.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
str | Path | dict[str, Any]
|
Path to file, JSON string, or dictionary |
required |
Returns:
| Type | Description |
|---|---|
Playbook
|
Loaded and validated Playbook |
Examples:
>>> playbook = load_playbook("./playbooks/deploy.yaml")
>>> playbook = load_playbook({"id": "test", "name": "Test", "steps": []})
Source code in .sdk/src/tulip/playbooks/loader.py
PlaybookLoader ¶
Load playbooks from JSON and YAML files.
Supports loading from: - JSON files (.json) - YAML files (.yaml, .yml) - Dictionaries (for programmatic use)
load_file ¶
Load a playbook from a file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
Path to the playbook file (.json, .yaml, or .yml) |
required |
Returns:
| Type | Description |
|---|---|
Playbook
|
Loaded and validated Playbook |
Raises:
| Type | Description |
|---|---|
PlaybookLoadError
|
If file cannot be loaded or validated |
Source code in .sdk/src/tulip/playbooks/loader.py
load_dict ¶
Load a playbook from a dictionary.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
dict[str, Any]
|
Dictionary containing playbook definition |
required |
Returns:
| Type | Description |
|---|---|
Playbook
|
Loaded and validated Playbook |
Raises:
| Type | Description |
|---|---|
PlaybookLoadError
|
If data is invalid |
Source code in .sdk/src/tulip/playbooks/loader.py
load_json_string ¶
Load a playbook from a JSON string.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
json_string
|
str
|
JSON string containing playbook definition |
required |
Returns:
| Type | Description |
|---|---|
Playbook
|
Loaded and validated Playbook |
Raises:
| Type | Description |
|---|---|
PlaybookLoadError
|
If JSON is invalid or playbook validation fails |
Source code in .sdk/src/tulip/playbooks/loader.py
load_yaml_string ¶
Load a playbook from a YAML string.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
yaml_string
|
str
|
YAML string containing playbook definition |
required |
Returns:
| Type | Description |
|---|---|
Playbook
|
Loaded and validated Playbook |
Raises:
| Type | Description |
|---|---|
PlaybookLoadError
|
If YAML is invalid or playbook validation fails |
Source code in .sdk/src/tulip/playbooks/loader.py
PlaybookLoadError ¶
Enforcer¶
PlaybookEnforcer is the enforcement engine that holds the model to
the playbook's step sequence. PlaybookEnforcerHook is the
HookProvider wrapper around it, installed automatically when
AgentConfig.playbook is set.
PlaybookEnforcer ¶
Bases: BaseModel
Enforces playbook execution sequence and constraints.
The enforcer tracks progress through a playbook, validates tool calls, and provides hints to guide the agent through the execution plan.
Features: - Track completed steps - Validate tool calls match current step's expected tools - Provide hints for the next step - Block out-of-sequence execution when strict_sequence is True - Record violations for auditing
effective_probes ¶
The step's own probes plus those of every skill it is carried out with.
De-duplicated by name, step-declared first: a step may sharpen a skill's generic probe for its own context, and the more specific declaration is the one an author expects to win.
Source code in .sdk/src/tulip/playbooks/enforcer.py
adherence_score ¶
How much of the declared evidence this run gathered, 0..1.
Lives HERE, not on the plan, because the evidence a step owes is its own
probes plus those of every skill it uses, and only the enforcer holds
the skills. The plan-level version counted step-declared probes alone
and reported 1.00 for a run whose violations said 1 of 3 matched.
Averaged over steps REACHED, not the whole playbook: a run still in
flight should not read as non-compliant for being unfinished, and a run
that stopped early is already reported by unresolved_required_steps.
Treat it as a floor, not a grade — see ASSESSMENT-required-probes-critique.md. The violation list is the artefact worth showing a person.
Source code in .sdk/src/tulip/playbooks/enforcer.py
allowed_tools_for ¶
What this step may call, or None when it does not constrain calls.
THE reason uses had to exist before a skill's allowed_tools could
mean anything: a skill is prose folded into a system prompt, so on its
own there is no moment at which its allow-list applies. A step supplies
that moment. Outside a step that names it, a skill still constrains
nothing — which is honest, and is why this returns None rather than an
empty set when nothing is declared.
Source code in .sdk/src/tulip/playbooks/enforcer.py
from_playbook
classmethod
¶
from_playbook(playbook: Playbook, block_violations: bool = True, record_violations: bool = True, skills: Mapping[str, Any] | None = None) -> PlaybookEnforcer
Create an enforcer from a playbook.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
playbook
|
Playbook
|
The playbook to enforce |
required |
block_violations
|
bool
|
Whether to block violating tool calls |
True
|
record_violations
|
bool
|
Whether to record violations |
True
|
skills
|
Mapping[str, Any] | None
|
name -> Skill, for steps that name capabilities via |
None
|
Returns:
| Type | Description |
|---|---|
PlaybookEnforcer
|
Configured PlaybookEnforcer |
Source code in .sdk/src/tulip/playbooks/enforcer.py
validate_tool_call ¶
Validate a tool call against the current step.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tool_name
|
str
|
Name of the tool being called |
required |
Returns:
| Type | Description |
|---|---|
EnforcementResult
|
EnforcementResult indicating whether the call is allowed |
Source code in .sdk/src/tulip/playbooks/enforcer.py
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 | |
record_tool_call ¶
Record that a tool was called, and what it looked at.
arguments and result are what the step's required_probes are
matched against. They are optional so every existing caller keeps
working — a caller that passes neither simply gathers no evidence, and
a step with probes will report them unmatched, which is the honest
answer rather than a silent pass.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tool_name
|
str
|
Name of the tool that was called |
required |
arguments
|
Any
|
The call's arguments, searched for probe matches |
None
|
result
|
Any
|
The call's result, searched for probe matches |
None
|
Source code in .sdk/src/tulip/playbooks/enforcer.py
complete_current_step ¶
Mark the current step as complete and advance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
result
|
str | None
|
Optional result to record for the step |
None
|
Returns:
| Type | Description |
|---|---|
bool
|
True if advanced to next step, False if playbook is complete |
Source code in .sdk/src/tulip/playbooks/enforcer.py
skip_current_step ¶
Skip the current step.
Only works for non-required steps.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
reason
|
str | None
|
Optional reason for skipping |
None
|
Returns:
| Type | Description |
|---|---|
bool
|
True if step was skipped, False if step is required |
Source code in .sdk/src/tulip/playbooks/enforcer.py
fail_current_step ¶
Mark the current step as failed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
error
|
str
|
Error message |
required |
Source code in .sdk/src/tulip/playbooks/enforcer.py
get_next_step_hints ¶
Get hints for the next step after current.
Useful for looking ahead during execution.
Returns:
| Type | Description |
|---|---|
list[str]
|
List of hints for the next step, or empty if no next step |
Source code in .sdk/src/tulip/playbooks/enforcer.py
get_step_summary ¶
Get a summary of step execution status.
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Dictionary with step status summary |
Source code in .sdk/src/tulip/playbooks/enforcer.py
reset ¶
Reset the enforcer to start over.
Source code in .sdk/src/tulip/playbooks/enforcer.py
EnforcementResult ¶
Bases: BaseModel
Result of an enforcement check.
EnforcementViolation ¶
Bases: BaseModel
Record of an enforcement violation.
PlaybookEnforcerHook ¶
PlaybookEnforcerHook(playbook: Playbook, *, block_violations: bool = True, record_violations: bool = True, priority: int = HookPriority.SECURITY_DEFAULT)
Bases: HookProvider
Hook that enforces a :class:Playbook over an agent run.
Holds a single :class:PlaybookEnforcer instance and dispatches
before/after_tool_call events into it so step compliance is tracked
automatically. Auto-advances to the next step when the current step's
expected tool list is exhausted.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
playbook
|
Playbook
|
The playbook to enforce. A fresh |
required |
block_violations
|
bool
|
When True (default), a violating tool call is
cancelled via |
True
|
record_violations
|
bool
|
When True (default), violations land on
|
True
|
priority
|
int
|
Hook priority. Defaults to a high value so the enforcer runs before observability / retry hooks; that way a blocked tool call doesn't get logged as if it had executed. |
SECURITY_DEFAULT
|
Example
from tulip import Agent from tulip.playbooks.loader import load_playbook from tulip.playbooks.hook import PlaybookEnforcerHook
playbook = load_playbook("playbooks/triage.yaml") agent = Agent( model="openai:gpt-4o", tools=[search, classify, escalate], hooks=[PlaybookEnforcerHook(playbook)], ) result = agent.run_sync("Triage this incident.")
Source code in .sdk/src/tulip/playbooks/hook.py
enforcer
property
¶
Return the underlying enforcer for inspection (violations, progress).
on_before_tool_call
async
¶
Validate the call against the current step; cancel on violation.
Source code in .sdk/src/tulip/playbooks/hook.py
on_after_tool_call
async
¶
Record the call and auto-advance when the current step is satisfied.
The agent loop short-circuits past on_after_tool_call when the
before-hook cancelled the call, so anything reaching this method
actually executed.
Source code in .sdk/src/tulip/playbooks/hook.py
on_before_invocation
async
¶
Called before agent starts processing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prompt
|
str
|
The user prompt being processed |
required |
state
|
AgentState
|
Current agent state |
required |
Returns:
| Type | Description |
|---|---|
AgentState
|
Potentially modified agent state |
Source code in .sdk/src/tulip/hooks/provider.py
on_after_invocation
async
¶
Called after agent completes processing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
AgentState
|
Final agent state |
required |
success
|
bool
|
Whether execution completed successfully |
required |
Source code in .sdk/src/tulip/hooks/provider.py
on_iteration_start
async
¶
Called at the start of each agent iteration.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
iteration
|
int
|
Current iteration number (0-indexed) |
required |
state
|
AgentState
|
Current agent state |
required |
Source code in .sdk/src/tulip/hooks/provider.py
on_iteration_end
async
¶
Called at the end of each agent iteration.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
iteration
|
int
|
Current iteration number (0-indexed) |
required |
state
|
AgentState
|
Current agent state |
required |
Source code in .sdk/src/tulip/hooks/provider.py
on_before_model_call
async
¶
Called before each model.complete() call.
Modify event.messages to change what the model sees. event.tools is read-only (inspect only).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
event
|
BeforeModelCallEvent
|
Write-protected event. Writable: messages. |
required |
Source code in .sdk/src/tulip/hooks/provider.py
on_after_model_call
async
¶
Called after each model.complete() call.
Set event.retry = True to discard response and re-call. Set event.response to replace the response. event.messages is read-only.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
event
|
AfterModelCallEvent
|
Write-protected event. Writable: response, retry. |
required |
Source code in .sdk/src/tulip/hooks/provider.py
register_hooks ¶
Return which hooks this provider implements.
Returns:
| Type | Description |
|---|---|
dict[str, bool]
|
Dictionary mapping hook names to whether they are implemented. |
dict[str, bool]
|
Useful for optimization - registry can skip calling unimplemented hooks. |