Agents and outbound effects
An agent can respect its capabilities while producing incorrect content. Permission to send an email is not business authority to promise a payment. Model instructions therefore do not replace a check before the effect.
Make checks unavoidable
Use this path: required evidence, agent proposal, deterministic policy, human approval when needed, then the effect. Do not give the agent the sending tool or make the policy check an optional capability it can skip.
This example deliberately uses a simple, testable policy: only one preapproved reply template is allowed. It is not a general prompt-injection detector.
module support.outbound
tool ReadCase
description "Read the submitted case without modifying it."
takes ticket as Text
gives Text
tool SendReply
description "Send an approved reply to the customer."
takes reply as Text
gives Text
requires network
function IsApprovedTemplate
takes draft as Text
gives Boolean
do
give draft == "We received your request and will review it."
agent DraftReply
takes caseText as Text
gives Text
responsibility "Draft a support reply from the case. Do not promise payments."
callable_by strategy Reply
strategy Reply
takes ticket as Text
gives Text
uses ReadCase
uses DraftReply
uses SendReply
do
ReadCase
with ticket as ticket
into caseText
DraftReply
with caseText as caseText
into draft
IsApprovedTemplate
with draft as draft
into allowed
if allowed == false
give "blocked_by_policy"
human.approve
with context as draft
into approval
SendReply
with reply as draft
into sent
give sent
The strategy reads the case before invoking the agent. This guarantees collection, not the model's comprehension. The pure function refuses every other wording, including payment promises. For richer policies, request a typed proposal and validate amounts, recipients and permissions against trusted business data. A word blacklist is not a substitute for that policy.
The ReadCase handler must genuinely have no effect and use
effect_contract.kind: "none". SendReply needs a reviewed network grant,
an external_write contract and an effect journal. Implement it with the SDK
and service-side idempotency. See idempotent effects.
The agent cannot choose SendReply or remove steps from the strategy.
Verify the workflow
- Approved content and human approval: one expected send for this operation.
- A payment promise:
blocked_by_policy, no send. - Allowed content followed by rejection or expiry: no send.
- Provider attempts to select
SendReplydirectly: authority refusal. - Resume the same operation after a crash: check the journal, receipt and service-side deduplication instead of creating a new replacement run.
- Missing case or read error: never reach sending.
Inspect isolated handler effects, not only run status. Test business rules separately from statistical model quality. For incident runbooks, likewise place required log reads and checks in the deterministic path before the decision and any restart.
Applying these rules to other processes
| Process | Touba composition | Host responsibility |
|---|---|---|
| Expense claims | category enum, pure threshold function with else if, successive approvals before payment | signer identity and separation, transactional cumulative budget, idempotent payment |
| Customer support | nested if for plan/priority rules, Optional of Customer for missing records | customer-data quality and content checks before sending |
| Reviewing a list | for with per-occurrence checks and approvals | track decisions and external effects, without reusing one approval for the whole list |
| Incident response | deterministic reads and policy, restricted diagnostic agent, approval before restart | health checks, operational authorization and restart reconciliation |
Two approval instructions require two decisions, not necessarily two distinct
people. A while remains bounded by the iteration budget; composing loops does
not allow unbounded execution. Use the SDK testing helpers
for success and refusal cases, then independently inspect effects in test services.
Responsibilities by layer
| Layer | Responsibility |
|---|---|
| Model | propose content or an action; quality remains an empirical question |
| Business code | validate content, evidence and business rules |
| Touba runtime | enforce authority, limits, contracts, human barriers and traces |
| Host and external service | authenticate people, perform effects, implement idempotency and reconciliation |
Here human.approve is mandatory. A model-proposed
request_human_review is discretionary: it cannot by itself guarantee a
review required by policy. See the server API for
private approval context and decision/resume correlation.