Modes
Documented for Python SDK
token-sentinel1.0.2.
TokenSentinel has three modes that control what happens when a rule fires: log, alert, and block. Pick the one that matches how much you trust the rules to be right.
sentinel = Sentinel(project="my-agent", mode="log") # default
sentinel = Sentinel(project="my-agent", mode="alert")
sentinel = Sentinel(project="my-agent", mode="block")
| Mode | What it does | When to use |
|---|---|---|
log | Calls your registered on_leak / on_waste handlers. Never raises for rules. | Day-one production. Always. |
alert | Same handler behavior as log. Historically named for “cloud-oriented” deployments; does not by itself enable cloud. | Same as log unless your team uses the name as a convention. |
block | Same as log plus raises LeakDetected / WasteDetected after handlers. | Once a rule has been trustworthy in log and you have a recovery path. |
Handlers always run when a rule fires (subject to min_confidence and dedup). Cloud delivery is a separate axis — see below.
Cloud delivery is orthogonal to mode
If you set both cloud_endpoint and api_key, the SDK ships events to TokenSentinel Cloud in log, alert, and block. Mode is stamped on each event for cloud analytics (e.g. savings weighting). If cloud is not configured, alert behaves like log locally.
Cloud is optional, Proprietary, and paid (Team / Pro / Enterprise). The Apache-2.0 SDK never phones home without those two kwargs.
log mode (default — safe for prod from day 1)
from token_sentinel import Sentinel
import anthropic
sentinel = Sentinel(project="my-agent", mode="log")
@sentinel.on_leak
def handle(event):
# ship to your logger / queue / metrics system
print(f"LEAK [{event.type}] {event.confidence:.2f} burn=${event.estimated_burn:.4f}")
client = sentinel.wrap(anthropic.Anthropic())
client.messages.create(model="claude-sonnet-4-6", max_tokens=10, messages=[...])
log is the default for a reason: the worst case is your handler runs and emits noise. The wrapped call always returns successfully. There is no path where TokenSentinel breaks your agent in log mode — handler exceptions are caught and swallowed, the underlying call's response is always returned.
This is what you should ship first. Always. Even if you ultimately want block.
alert mode
sentinel = Sentinel(
project="my-agent",
mode="alert",
# Optional paid cloud — works in log/alert/block when both are set:
cloud_endpoint="https://api.tokensentinel.dev",
api_key="ts_live_...",
)
@sentinel.on_leak
def handle(event):
print(f"LEAK [{event.type}] {event.confidence:.2f}")
Locally, alert is the same as log: handlers run, no raise. Use it if your org’s runbooks already say “alert mode” for cloud-connected agents.
With cloud configured (any mode), the SDK fire-and-forgets batched POSTs to {cloud_endpoint}/v1/events. Network failures never block the agent. The hosted dashboard (webhooks, retention, Intervention Pack, Pro judge/composites) is Proprietary — see FAQ and tokensentinel.dev.
block mode (hard-stop)
from token_sentinel import Sentinel, LeakDetected
import anthropic
sentinel = Sentinel(project="my-agent", mode="block")
client = sentinel.wrap(anthropic.Anthropic())
try:
response = client.messages.create(model="claude-sonnet-4-6", messages=[...])
except LeakDetected as exc:
# exc.event is the LeakEvent that caused the block
print(f"Blocked: {exc.event.type} confidence={exc.event.confidence:.2f}")
# Take whatever recovery action makes sense — kill the session, alert ops,
# surface to the user, etc.
block raises LeakDetected after the underlying LLM call has already returned. The exception propagates up to your code, which gets to decide what to do.
The exception carries the offending LeakEvent on exc.event, so you can inspect type, confidence, evidence, etc. before deciding to retry, fall back, or fail the request entirely.
Important caveats for block
- The LLM call already happened.
blockcannot prevent the current call from being billed — rules run after the provider returns. The savings come from stopping the next turns in a wasteful loop (and from your handler / exception recovery). - Handler exceptions are still caught. If your handler raises, that does not block the agent —
LeakDetectedcomes fromSentinel, not from your handler. - Streaming finalizes at stream close. Anthropic / OpenAI / Gemini / Bedrock streaming build the
CallRecordwhen the stream finishes. Preferwith stream:so raises propagate; GC cleanup of abandoned streams may suppress the exception after handlers ran.
Policy plane (paid cloud, mode-independent)
When cloud policy is configured (cloud_endpoint/policy_endpoint + api_key), record_call may raise BudgetExceeded, VelocityExceeded, or KillSwitchActive regardless of mode. Those exceptions subclass LeakDetected.
They still run after the wrapper’s provider call (same boundary as rules). Configure policy only if your paid cloud tier enables the Intervention Pack and you accept post-call halt semantics.
Per-rule confidence floors
Beyond modes, you can filter individual rules by confidence with the project-level min_confidence:
sentinel = Sentinel(project="my-agent", mode="log", min_confidence=0.7)
Any event below min_confidence is silently dropped before reaching handlers. Default is 0.5. Use this to silence noisy rules without removing them entirely.
For per-rule tuning (different thresholds per rule), use the config dict:
sentinel = Sentinel(
project="my-agent",
mode="log",
config={
# Tune individual rules
"tool_loop.cosine_threshold": 0.80, # default 0.70 — stricter
"tool_loop.min_calls": 5, # default 3 — fewer fires
"retry_storm.min_retries": 10, # default 5 — quieter
"context_bloat.slope_threshold": 3000, # default 1500 — only fire on big jumps
},
)
Each rule's config keys are documented in Leak rules.
Selecting a subset of rules
By default Sentinel runs all fifteen rules. To run only a specific subset, pass rules=:
sentinel = Sentinel(
project="my-agent",
mode="log",
rules=["embedding_waste", "retry_storm"], # only these two
)
The string in the list is the rule's name attribute. Valid names:
tool_loop, context_bloat, embedding_waste, zombie, model_misroute,
retry_storm, tool_definition_bloat, retrieval_thrash,
vision_re_upload, vision_high_detail_misroute, vision_cost_concentration,
audio_multichannel_doubling, voice_switching_loop, rerank_thrash, repair_loop
Default is "all".
Disabling rules is the right tool for "I don't care about this leak class" (e.g., a stateless inference API that can't have context_bloat). For "I want this rule but tuned looser", use config instead.
Recommended graduation pattern
Don't go straight to block. The right sequence for a new project:
- Week 1:
logeverywhere. Wire up handlers that ship events to your logging stack. Watch what fires. Note any handler-side false positives. - Week 2-4: tune. Adjust per-rule
configkeys on rules that are noisy for your traffic. The defaults err toward fewer false positives, but every project's ground truth is different. - Month 2:
blockon the highest-confidence rules. Start withembedding_waste(0.99 confidence, exact-hash match — never wrong) andretry_storm(0.9 confidence, deterministic hash repeat — almost never wrong). Leavetool_loopandmodel_misrouteinloglonger because they are heuristic. - Ongoing: per-rule mode. TokenSentinel doesn't currently support per-rule mode — it's all-or-nothing on
block. If you need finer control, run twoSentinelinstances on the same client (one inlog, one inblockwithrules=filter) and chain them.
When NOT to use block
- Your agent doesn't have a recovery path.
blockraisesLeakDetected; your code must catch it and do something sensible. If you'll just propagate the exception to the user, you are turning a quiet leak into a loud failure. - You haven't tuned thresholds yet. Defaults are good but not optimal for every workload. Run
logfirst, eyeball false positives, then promote. - The rule is heuristic and the cost of a wrong block is high.
model_misrouteis a useful signal but not always right; blocking it would prevent some legitimate frontier-model classification work.
Handler requirements
Handlers run synchronously, inline with the wrapped LLM call. A handler that takes 5s adds 5s of latency to every leak-firing LLM call your agent makes. This isn't a TokenSentinel problem — it's the handler's. Two rules:
- Don't do network I/O in a handler. Don't post to Slack, write to Datadog, or call an HTTP webhook directly. Queue the event to a background worker (
queue.Queue,concurrent.futures, etc.) and let the worker do the network call. - Don't sleep, lock contention, or
awaitin a handler. Handlers are sync; anawaitinside a handler that blocks on the running event loop will deadlock asyncio agents.
A safe handler pattern:
import queue
_event_q: queue.Queue = queue.Queue(maxsize=1000)
@sentinel.on_leak
def fast_handler(event):
try:
_event_q.put_nowait(event)
except queue.Full:
pass # drop on overflow rather than block
# in a separate worker thread / asyncio task:
def drain_events():
while True:
event = _event_q.get()
slack.post_message(event=event) # actual network call here, off the hot path
This matters most in block mode where the LeakDetected raise unwinds through the wrapped call — slow handlers extend that unwind in front of the user's exception handler.