Effect contracts
The action leaves once, and success is what a second pinned tool observed.
A tool that returned 200 has not proved that anything happened.
The inline decision point answers whether an agent may attempt an action. It does not answer whether the action happened, whether it happened once, or whether it stayed inside the bounds the person who approved it had in mind. Treating the response as completion proves the transport worked and nothing else.
The dangerous case is not failure, it is ambiguity. A timeout hides two outcomes needing opposite responses — nothing happened, or everything happened and the acknowledgement was lost — and an agent framework’s default is to retry. Retrying a read is free. Retrying a payment is a second payment.
How it works
Step 9 of the request path, and the durable recovery after it. The order is encoded in the evaluator, and where each step sits is the reason the guarantee holds.
- 01
A contract is drafted against exact descriptor digests
The draft names an action tool and a different verifier tool by server, tool name and the exact SHA-256 of the approved descriptor, plus, where compensation exists, a distinct compensation verifier. Activation refuses unless every binding is present, pinned and matching. - 02
Preconditions run, the claim is taken, the action leaves once
Policy, data controls and the payload-bound approval inspect the exact payload the contract will send. The claim on the business key is taken last, after redaction and before the first byte leaves, in the transaction that writes the run and outbox rows. A second delivery of the same key is refused as a non-retryable conflict, and a transport timeout moves the run to verifying and never back to dispatch. - 03
The verifier is called, and the predicates decide
The verifier is invoked with arguments mapped from the original request, and its result must carry an observation timestamp that is post-dispatch and no older than the freshness bound. Every postcondition and invariant must match. A pending answer is polled again inside a bounded cap; exhaustion parks the run for manual review rather than replaying a mutating stage.
- beat 01Name the caller
- 1Authenticate
- 2Resolve the agent
Nothing is decided for an anonymous caller. The key resolves to one registered agent with a named owner, and its roles, its ceilings and any stop switch in force are read live from the store on every request, not from a cached copy, so a suspension takes effect on the next call instead of after a redeploy.
- beat 02Start the record, then read what is being sent
- 3Open the trace
- 4Sanitise
- 5Scan
The trace is opened before anything can refuse the request, which is why a blocked call is recorded as thoroughly as one that went through. Only then is the text normalised and scanned, in that order, so the detectors see what the model will read rather than what a human reviewer can see.
- beat 03Decide once
- 6Govern
- 6bIntersect with the named human
- 7Enact
One function returns one verdict: allow, block, or hold it for a named person to approve. Every check lives in that one place rather than scattered along the path, which is what makes it possible to read your rules and know what they do.
- beat 04Send it, and only then spend the money
- 8Route
- 9Call upstream
Routing fixes which provider and model will serve the call, so where an agent has a USD ceiling it is tested against the price you will pay, not the one the caller typed. Routing is also the last step that can still refuse locally: after it, the request has left your network.
- beat 05Govern the answer, then count the cost
- 10Govern the response
- 11Meter and record
A tool call the model proposes is judged before it reaches your agent. Metering runs last and on every ending, including the refusals and the streams a client abandoned halfway, because the tokens were spent either way and a partial trace is still evidence.
Step names and their grouping are the product’s own, and the order is encoded in the evaluator rather than drawn for the page.
A closed language: six operators, no code.
Values move between stages only by RFC 6901 JSON Pointer copies, and conditions use exactly six operators. There is no expression language, no template interpolation and no way to run code: the alternative considered and rejected was a second sandbox platform inside the governance boundary. Pointer resolution is unforgiving: only own data properties are read, prototype segments are refused, and predicates do not coerce types.
- No stage may read the action’s response
- Every set evaluated after dispatch reads the verifier’s result and nothing else, because the action response is what a timeout loses.
- Four distinct tools
- Validation refuses a contract whose action and verifier are the same tool, or whose compensation stages target the action, the verifier or each other.
- Versions, never rewrites
- Editing a contract’s semantics creates version N+1 and never rewrites N. Lifecycle status is outside the digest, so retiring a revision blocks new dispatch without destroying the digest historical runs name.
action serverId + toolName + descriptorHash + mappings
verifier a DIFFERENT pinned tool
compensation? optional, and requires the next line
compensationVerifier? a fourth distinct pinned tool
idempotencyKeyPointer /request_id non-root
verificationTimestampPointer /observed_at non-root
onVerificationFailure compensate | manual_review
preconditions over original_arguments
postconditions over verifier_result, at least one
invariants, failureConditions, compensationPostconditions
operators exists equals not_equals
greater_than less_than contains
no expressions no templates no code unknown fields invalidSuccess is what a second pinned tool observed.
Token Observe deduplicates on a value your own system already knows. A non-root pointer names the business idempotency value, and every stage must carry it: a verifier addressable by a different identity than the action it verifies is not a verifier. The value is reduced to an HMAC digest and never stored raw, and a unique index on it holds across agents, teams and revisions.
The agent does not receive the action’s response until the effect is proved; if verification does not pass, the caller is refused and told not to retry. Automatic compensation is opted into twice: the contract must carry a compensation tool and a distinct compensation verifier, and a separate set of failure conditions must match fresh evidence. Without that proof the run goes to manual review, because compensating an effect that has merely not appeared yet is a second unauthorised action against the same account. A compensation response is recorded as accepted; the run reads compensated only once the compensation verifier observes it landed.
pending -> reserved -> executing -> executed -> verifying
verified -> committed
executing -> verifying action response ambiguous
reserved..verified -> compensation_required -> compensating
-> compensation_verifying -> compensated
-> compensation_failed
reserved..verified -> manual_review_required
manual_review_required -> operator_attested_committed
-> operator_attested_not_committed
terminal committed compensated both attestationsEach line closes off an assumption a reader would otherwise carry into a proof of concept.
What this does not doWhat this leans on
MCP gateway
The half of an agent that moves money is a tool call, not a sentence. This is the checkpoint that call goes through.
Human approvals
One human decision, bound to one exact payload, spendable once.
Audit chain
Every administrative act hash-chained, and anchored with a signature your auditor can check alone.
Where this comes up
Jobs that draw on it
Guides that lean on it
How is this different from retrying with an idempotency key?
An idempotency key asks the downstream system to deduplicate. An effect contract deduplicates before the request is sent, then checks what happened: a unique durable claim on the business key is taken before the first byte leaves, and a different pinned tool then observes the result. The key still has to be honoured downstream; the difference is that a lost response leads to a read rather than a retry.
What happens when the action times out?
The run moves from executing to verifying and the verifier is called. Nothing is re-dispatched, because a timeout hides two opposite outcomes and both blind retry and assumed failure are unsafe, so the state graph offers neither move. If the verifier proves the effect, the run commits and the caller is told the response was lost but the effect stands. Otherwise it enters manual review with the ambiguity visible.
What stops someone writing a verifier that always says yes?
Nothing in the product. A verifier is a contract-bound read-after-write check by a different pinned tool: separation of role, not an independent trust authority. What is enforced is narrower and still useful: a distinct tool pinned to an exact descriptor digest, unable to read the action’s response, and required to observe freshly and post-dispatch.
Prefer to ask a person? Write to James
Bring us the agent you are least comfortable with.
Create a cloud account and workspace. No card or server installation. Model calls and Google BigQuery scans can incur separate provider charges.