The official Python SDK for Houndsight. Houndsight
is an agent leash, not just a camera — alongside trace collection, the
SDK exposes a synchronous policy gate (trail.leash) that blocks an agent
until a remote decision permits the action.
pip install houndsightimport houndsight as hs
hs.init(api_key="sk-hnd-...", agent="sales-pipeline")
with hs.trail(trigger="user_message", payload={"text": "Q2 forecast"}) as t:
with t.sniff("salesforce_query", layer="execute") as s:
s.cost(0.0001)
s.output({"deal_count": 17})
t.set_output("Posted forecast")Docs: https://docs.houndsight.ai · Discord: https://discord.gg/houndsight
Use trail.leash(...) before any action your agent shouldn't take without
oversight — sending external email, charging a card, deleting customer
data, posting publicly, hitting a destructive API.
decision = trail.leash(
action_name="stripe.charge",
action_summary=f"Charge customer ${amount/100}",
risk_signals={
"amount_usd": amount / 100,
"data_categories": ["financial"],
},
payload={"customer_id": cust_id, "amount_cents": amount},
timeout_seconds=600,
)
if not decision.approved:
log.warning("Leashed: %s. See %s", decision.reason, decision.trace_link)
return
# Use the reviewer's modifications if any.
payload = decision.modified_payload or original_payload
stripe.Charge.create(**payload)decision.decision is one of:
| value | approved | meaning |
|---|---|---|
approved |
True |
Gate allows the action. |
modified |
True |
Approved with edits — use decision.modified_payload. |
rejected |
False |
A human reviewer denied the action. |
blocked |
False |
A floor rule fired and blocked the action automatically. |
timeout |
False |
No decision within timeout_seconds, or the gate service was unreachable / returned 5xx. |
error |
False |
The gate service rejected the request as malformed (4xx). The gate is broken, not the action. Almost always means the SDK and server contracts have drifted; check decision.reason for the response body. |
async |
False |
Only with blocking=False. Decision pending server-side. |
timeout and error are both fail-closed but they mean different things —
the dashboard alerts on them differently. timeout is "we tried and the
gate didn't answer in time"; error is "we tried and the gate said our
request was wrong" (bad API key, bad endpoint URL, schema mismatch).
Calls block by default for up to timeout_seconds. Pass blocking=False
to fire-and-forget; check the dashboard at decision.trace_link for the
final outcome.
The SDK enforces nothing locally. leash() POSTs the request to
Houndsight, reads the server's decision, and returns it. That's the whole
mechanism — there is no local rules engine, no client-side policy file, no
"approved actions" allowlist baked into the SDK.
This is deliberate. A tampered SDK cannot bypass the gate because the
gate is gated by the presence of the leash() call itself. The only way
an agent skips the gate is to skip the call — and that manifests in the
trace as the absence of a review-layer step for the corresponding
sensitive action.
In other words:
leash()is a contract, not a sandbox.- Calling
leash()and then ignoringdecision.approvedis a bypass — and it is visible in the trace: there will be anapproved=Falsedecision followed by the action's step. The dashboard flags this. - Not calling
leash()on a sensitive action is also a bypass — and it is visible in the trace by what isn't there: a sensitive-classified step (e.g.stripe.charge) with no precedingreview-layer step. The dashboard flags this too.
The audit story relies on the SDK emitting a review-layer AgentStep
for every leash() call. The SDK guarantees this even on the failure
paths (timeout, network unreachable, server 5xx). Don't catch and swallow
exceptions from inside leash() — there aren't any to catch.
- Trails group an agent run end-to-end (
hs.trail(trigger=...)). - Sniffs record each step's input, output, cost, tokens, and per-call
events (
t.sniff(name, layer="...")). - Leashes block until a remote decision lets the agent proceed
(
t.leash(...)). See above. - Barks attach typed custom events (
hs.bark("cache_hit", key=...)). - Collars decorate functions for automatic tracing (
@hs.collar).