| title | Deterministic contracts and pattern catalog |
|---|---|
| description | The full deterministic pattern library, contract anatomy, and the failure strategies that run on violation. |
A deterministic contract is a binary pass/fail rule evaluated before each tool call. If the rule is violated, Sponsio acts before any side effect happens. This is the hot path: zero LLM calls, microsecond latency.
This page covers the shape of a contract, the four failure strategies, the full catalog of patterns that ship with Sponsio, and how to add a new one.
For the conceptual model (atom → pattern → formula → contract) see Concepts overview. For the full atom vocabulary see Architecture § Atoms.
A deterministic contract has four parts:
contract("policy gate before refund") # name
.assume("called `issue_refund`") # when the rule applies
.guarantees("must call `check_policy` before `issue_refund`") # what must hold
.strategy("block") # what to do on violation- Name: a human-readable label; shows up in logs, reports, and error messages.
- Assumption (A): the condition that triggers the rule. The rule only fires when A holds. Omit for unconditional rules.
- Guarantee (G): the temporal property that must hold when A is true.
- Strategy: what happens on violation:
DetBlock,EscalateToHuman,RedirectToSafe,WarnOnly, or a custom callable.
Both A and G can be natural-language strings or structured pattern calls. They compile down to LTL formulas over atoms. You never need to write the LTL by hand, but the engine ultimately checks the LTL.
Use a deterministic contract when the property is structurally observable: expressible with counters, regexes, paths, or ordering. Structural properties do not need semantic judgment, so they do not need an LLM in the hot path.
Typical use cases:
- Ordering: A must precede B; after X, Y is forbidden; every A must be followed by B.
- Rate and retry limits: at most N calls, cooldown between calls, bounded retries, loop detection.
- Irreversibility gates: once a commit or approval happens, downstream mutations are forbidden.
- Argument checks: blacklisted patterns, path scope limits, length or range caps.
- Permissions: static role-based access to certain tools.
- Exact-regex PII: SSN, credit card, email patterns that a regex can reliably catch.
Anti-pattern: do not use a deterministic contract for properties that need reading the text semantically (tone, relevance, whether something is truly PII). The deterministic engine does not evaluate those; keep contracts to what is structurally observable.
When a contract is violated, the call routes through a strategy. Four ship in the box.
| Strategy | Behavior |
|---|---|
DetBlock (block) |
Deny the call and raise SponsioBlocked to the framework. The agent can react and retry with a different plan. This is the default. |
EscalateToHuman (escalate) |
Deny the call AND fire user-supplied notifier callables (Slack webhook, email, oncall pager). Accepts notify=[callable, ...]. Notifier failures are isolated: a broken Slack hook does not crash the agent loop and does not silence the remaining notifiers. |
RedirectToSafe (redirect_to_safe) |
Substitute the offending call with a pre-declared safe tool. The agent continues on a safer path. Both unsafe and safe must be registered with the framework. The LangGraph adapter dispatches the substitute call transparently; other adapters surface result.redirected_to for the application to consume. |
WarnOnly (warn_only) |
Allow the call and emit a violation event to logs and dashboards. Useful when the contract is informational rather than enforcing. |
(callable) |
Custom callback. Receives the violated contract and the candidate event; returns a new strategy decision. |
In observe mode, no strategy runs. Violations are logged and surfaced in reports, but the call is not blocked. This is how most teams wire Sponsio in first. See Observe vs. enforce.
Run sponsio patterns on the CLI to browse this catalog interactively with NL examples.
| Pattern | NL example | What it enforces |
|---|---|---|
must_precede(A, B) |
"tool check_policymust precedeissue_refund" |
A must have been called before B can execute |
must_confirm(action) |
"tool delete_file requires confirmation" |
A confirmation step must precede the action |
requires_permission(tool, perm) |
"tool transferrequires permissionmanager" |
Agent must hold a static permission to use the tool |
no_data_leak(src, dest) |
"no data leak from read_dbtosend_email" |
Data must not flow between two agents/tools |
destructive_action_gate(action) |
"destructive action drop_table requires confirmation" |
A destructive tool needs an explicit gate step |
workflow_step(trigger, next_action) |
workflow_step(Atom("ctx", "roaming_status", "disabled"), Atom("called", "toggle_roaming")) |
When trigger holds, the next event must satisfy next_action. Prescriptive counterpart to block-style patterns: instead of "you must not do X", it says "you must do X next". Both arguments are arbitrary atoms (called(...), ctx(k, v), arg_field_has(...), etc.), so the same pattern covers tool-ordering, ctx-driven remediation, and arg-conditional follow-ups. |
| Pattern | NL example | What it enforces |
|---|---|---|
no_reversal(A, B) |
"after approve, tool reject is forbidden" |
Once A is called, B is permanently forbidden |
segregation_of_duty(A, B) |
"tools reviewandapprove must be by different agents" |
Same agent cannot perform both actions |
always_followed_by(A, B) |
"every refundmust be followed bynotify" |
Whenever A happens, B must eventually happen |
required_steps_completion(steps) |
"aml_checkmust complete beforeissue_loan" |
All steps must have completed before a gate is passed |
| Pattern | NL example | What it enforces |
|---|---|---|
rate_limit(action, N) |
"tool query_db at most 5 times" |
Action can be called at most N times total |
idempotent(action) |
"tool transfer at most 1 times" |
Action can be called at most once (special case of rate_limit) |
cooldown(action, N) |
"tool send_email cooldown of 3 steps" |
At least N steps between consecutive calls |
deadline(trigger, action, N) |
"tool respondwithin 3 steps ofreceive" |
Action must happen within N steps of trigger |
bounded_retry(action, N) |
"tool deploy at most 3 retries" |
Action limited to N retries |
loop_detection(action, N) |
"tool search must not loop more than 5 times" |
Detects repeated calls with similar args |
| Pattern | NL example | What it enforces |
|---|---|---|
mutual_exclusion(A, B) |
"tools approveandreject are mutually exclusive" |
At most one of A or B can ever be called |
tool_allowlist(tools) |
"agent may only call search, summarize" |
Only listed tools may be called |
| Pattern | NL example | What it enforces |
|---|---|---|
redirect_to_safe(unsafe, safe) |
"redirect issue_refundtolog_refund_request" |
Substitute a forbidden tool with a pre-approved alternative. Bundled with the RedirectToSafe strategy: a violation surfaces as action="redirected" with fallback_action=safe, the trace records the substitute call. |
| Pattern | NL example | What it enforces |
|---|---|---|
arg_blacklist(tool, field, patterns) |
"bash command must not contain rm -rf" |
An arg field must not match forbidden regex patterns |
scope_limit(tool, paths) |
"bash may only access files under /workspace" |
All file paths in tool args must be within allowed prefixes |
arg_length_limit(tool, field, N) |
"sql.query at most 500 chars" |
Argument length cap |
arg_value_range(tool, field, lo, hi) |
"transfer.amount between 0 and 10000" |
Numeric argument range |
data_intact(tool, field) |
"aml_reportmust not be edited afteraml_check" |
Payload field is immutable once written |
| Pattern | NL example | What it enforces |
|---|---|---|
untrusted_source_gate(tool) |
"content from untrusted sources requires review" |
Data from untrusted origin must pass a gate before use |
confirm_after_source(tool) |
"confirmation required after reading from web_search" |
A confirmation step must follow a source-read |
dangerous_bash_commands() |
"bash must not run rm -rf /, :(){: |
:&};:..." |
dangerous_sql_verbs() |
"sql must not issue DROP, TRUNCATE, ALTER" |
Built-in SQL verb blacklist |
irreversible_once(action) |
"post_tweet at most once per session" |
Irreversible actions capped to a single call |
| Pattern | NL example | What it enforces |
|---|---|---|
token_budget(N) |
"total LLM tokens under 50000" |
Session-wide token cap |
delegation_depth_limit(N) |
"sub-agent delegation at most 3 levels" |
Bounds recursive agent delegation |
| Pattern | NL example | What it enforces |
|---|---|---|
approval_active(action, role) |
"issue_refundrequires active approval frommanager" |
A specific role must have approved the action recently |
approval_freshness(approval, action, max_steps) |
"approve_prvalid for 10 steps beforemerge_pr" |
Approval must be within N steps of the gated action |
audit_after(action, audit) |
"every delete_usermust logaudit_event" |
Sensitive action must be followed by an audit-log step |
backup_before_destructive(backup, action) |
"snapshot_dbmust precededrop_table" |
Backup must run before any destructive action |
dry_run_before_commit(dry_run, commit) |
"planmust precedeapply" |
Plan / preview step required before commit |
sanitized_before_sink(source, sanitizer, sink) |
"untrusted_inputmust passsanitizebeforedb_write" |
Untrusted input must pass a sanitizer before reaching a sink |
| Pattern | NL example | What it enforces |
|---|---|---|
ctx_required(tool, key, values) |
"publish requires ctx[msg_verified]=true" |
A ctx(k, v) fact must be set before the tool runs |
ctx_matches_required(tool, key, regex) |
"issue_refundrequires caller_id matching^spiffe://prod/finance-" |
A ctx(k, v) value must match a regex |
| Pattern | NL example | What it enforces |
|---|---|---|
arg_allowlist(tool, field, patterns) |
"http_post url must match allowlist" |
Argument must match one of the allowed regex patterns |
duplicate_call_limit(tool, args_pattern, N) |
"send_email to same recipient at most 1 time" |
Cap on repeated calls with similar args |
time_since(predicate_key, max_seconds) |
"action within 60s of user_request" |
Bounded time window since a referenced predicate |
These are deterministic atoms that match against llm_response events via regex or exact string compare. They are distinct from stochastic atoms (judge-backed, like tone or faithfulness), which need an LLM judge at runtime and are not part of this OSS release.
| Pattern | NL example | What it enforces |
|---|---|---|
no_pii(fields) |
"response must not contain PII" |
Regex-detect SSN, credit card, email, phone in response |
no_keywords(words) |
"response must not mention competitors" |
Response cannot contain any of the given strings |
max_length(max_words, max_chars) |
"response under 200 words" |
Response length cap |
NL string
─▶ Pattern function (e.g., must_precede("A", "B"))
─▶ LTL formula: Not(called("B")) Until called("A")
─▶ Grounding: extract atoms from trace events
─▶ Evaluator: evaluate formula over atom valuations
─▶ True (pass) or False (block)
A few concrete compilations:
# must_precede("A", "B") compiles to:
Not(Atom("called", "B")) Until Atom("called", "A")
# rate_limit("X", 3) compiles to:
G(Le(Var("count(X)"), Const(3)))
# arg_blacklist("bash", "command", ["rm -rf"]) compiles to:
G(Implies(
Atom("called", "bash"),
Not(Atom("arg_field_has", "bash", "command", "rm -rf")),
))Six steps:
- Add a factory to
sponsio/patterns/library.py. - If it needs a new observable, add atom extraction in
sponsio/tracer/grounding.py. - Register it in the text DSL at
sponsio/generation/dsl_to_contract.py. - Tests in
tests/test_patterns.py(formula) andtests/test_nl_parser.py(NL round-trip). - Mirror in
ts/packages/sdk/src/core/patterns.ts, or add a row tots-sdk-parity.mdif TS cannot ground the atoms it uses. - Document a row here, plus a
### Addedentry inCHANGELOG.md.
For the full worked example end-to-end, with code excerpts from sanitized_before_sink, see CONTRIBUTING § Adding a new pattern.