Coordinate external effects safely
Record intent before execution, replay completed work, and fail closed when a write may already have happened.
Why effects need a boundary
A model call, tool, or worker can fail after an external system accepted a write but before the caller received success. At that point, “retry” may mean “charge twice,” “send twice,” or “delete the next resource.”
Runifold calls this an ambiguous effect. The runtime does not erase that uncertainty behind a generic transient error.
Use the effect boundary for externally visible writes whose outcome must be recovered: payments, notifications, provisioning, approvals, and destructive operations. Ordinary pure calculation does not need it.
The write-ahead lifecycle
EffectExecutor coordinates one logical operation through durable states:
| State | Meaning | Safe next action |
|---|---|---|
| Prepared | intent is durable; handler has not started | execute |
| Started | handler may have reached the destination | apply recovery policy |
| Completed | canonical output is durable | replay output, no I/O |
| Failed | a definitive handler error is durable | return the same failure |
use std::sync::Arc;
use runifold::{
EffectExecutor, EffectRecoveryPolicy, InMemoryEffectStore,
};
let executor = EffectExecutor::new(Arc::new(InMemoryEffectStore::new()));
let outcome = executor
.execute(
effect_request,
&run,
&handler,
EffectRecoveryPolicy::RejectAmbiguous,
)
.await?;Use a durable EffectStore when recovery must survive process restart. The
in-memory store is appropriate only for tests and process-local behavior.
Recovery policy
RejectAmbiguous is the default. It refuses to repeat a Started operation
whose result is unknown.
RetrySafe still retries only when the request contract proves it safe:
- Pure and ReadOnly operations;
- IdempotentWrite with a stable idempotency key.
It never turns a non-idempotent write into a safe operation. An idempotency key must identify the same logical request at the destination, not merely a fresh retry attempt.
Agent tools and effects
Tool descriptors declare effect and risk. The Agent uses those semantics
when coordinating callable execution. Completed calls can be replayed from a
shared effect store; a different request at the same execution position is
rejected instead of receiving the wrong cached result.
Inject a durable shared executor when an Agent checkpoint may resume in another
process. Keep ToolError safe for model visibility and retain detailed
operator diagnostics outside the transcript.
Cancellation is not rollback. If cancellation wins while the handler is in flight, the effect remains ambiguous until reconciled.
Production checklist
- classify every write by actual destination behavior;
- derive stable idempotency keys from business identity;
- persist intent before sending the external request;
- use bounded deadlines but preserve ambiguous state after timeout;
- keep full effect outputs out of journal events unless explicitly approved;
- expose a reconciliation queue for ambiguous high-risk effects;
- fault-test “destination committed, response was lost.”
If you cannot explain how an operation recovers after that last failure, it is not ready for automatic retry.