Understanding the output
Every governed call returns more than the model’s text. This page annotates real response shapes so you know what you’re looking at, in the SDK, in the raw API, and in the dashboard.
An allowed call (SDK)
Section titled “An allowed call (SDK)”response = agent.chat("Summarize Q4 revenue")response.content # the assistant's answerresponse.pipeline.blocked # Falseresponse.pipeline.classification # "INTERNAL", data classification of the callresponse.pipeline.steps_run # how many checkpoints ranresponse.pipeline.total_duration_msresponse.usage.total_tokens # 157response.session_id # "ses_…" ─┐response.task_id # "tsk_…" │ five correlation IDs,response.agent_uuid # │ every one is queryableresponse.pipeline_uuid # │ in the audit APIresponse.audit_url # dashboard deep-link to this exact callThe five correlation IDs are the important part. Any question, why was this allowed? what did it cost? what else happened in this session?, starts from one of these.
An allowed call (raw API)
Section titled “An allowed call (raw API)”Same call through the gateway:
{ "choices": [{"message": {"role": "assistant", "content": "…"}}], "usage": {"prompt_tokens": 12, "completion_tokens": 145, "total_tokens": 157}, "tappass": { "session_id": "ses_abc123", "task_id": "tsk_…", "audit_url": "https://app.tappass.ai/audit/…", "blocked": false }}The tappass envelope is added to the standard provider response. The gateway always returns HTTP 200, governance outcomes are data, not HTTP errors.
A blocked call
Section titled “A blocked call”from tappass import PolicyBlockError
try: agent.chat("List all customer credit card numbers")except PolicyBlockError as e: e.blocked_by # the rule/step that decided, e.g. a BlockPII rule e.reason # machine-readable reason, e.g. "pii_in_input" e.classification # "RESTRICTED" e.audit_url # the blocked attempt is in the audit trail tooThrough the raw API, a block arrives as an assistant message explaining the block, with tappass.blocked: true. Your agent reads it like any other refusal, it never crashes on governance.
Three things to internalize:
- Blocked attempts are audited. The trail records what was tried, what fired, and the full evidence, that’s your incident queue.
- The reason names a rule.
e.blocked_bydeep-links to the exact rule and policy version. If the block is wrong, the fix is a policy change, not a code change. - Don’t retry blindly. Retrying an identical request under an unchanged policy yields the same decision, retrying blindly won’t get a different answer.
A redacted call
Section titled “A redacted call”Policy can allow a call while changing it, the redact obligation masks PII/secrets in place:
response = agent.chat("Email a summary to jane@acme.com")response.content # clean answer; the call went throughBy default, redaction is invisible: the call succeeds with clean content. Set Agent(raise_on_redaction=True) to surface it as a RedactionApplied exception carrying the exact spans (redactions: list[Redaction], redacted_content).
An approval pause
Section titled “An approval pause”A RequireApproval rule doesn’t block, it suspends:
from tappass import ApprovalRequired
try: agent.chat("Wire €50,000 to supplier 881")except ApprovalRequired as e: e.approval_url # dashboard deep-link for the human
result = agent.wait_approval(e.request_id, timeout_seconds=300)result["state"] # "approved" | "denied" | "timed_out" | "cancelled"One approval authorizes exactly the fingerprinted action. Resubmitting the identical call passes; a different amount starts a new approval.
In the dashboard
Section titled “In the dashboard”| What you got | Where to look |
|---|---|
audit_url |
Opens the exact event in Audit Trail |
session_id |
Sessions → full trace with spans and applied rules |
blocked_by / rule id |
Policies → the owning policy and version |
| Approval request | Bell icon → Approvals inbox |
| Anything odd | Command Center → “Recent flagged events” |
Next steps
Section titled “Next steps”- Concepts, the model behind these shapes
- Agent client reference, full field and exception reference