pub trait Effect: Send + Sync {
type Output: Serialize + DeserializeOwned + Send;
Show 16 methods
// Required methods
fn descriptor(&self) -> EffectDescriptor;
fn perform<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<Self::Output, EffectError>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait;
// Provided methods
fn attach(&mut self, _provenance: &Provenance) { ... }
fn gen_ai_operation(&self) -> Option<&'static str> { ... }
fn mutates(&self) -> bool { ... }
fn recovery(&self) -> Recovery { ... }
fn retry(&self) -> RetryPolicy { ... }
fn max_sensitivity(&self) -> Sensitivity { ... }
fn sink_arguments(&self) -> Option<&Value> { ... }
fn protected_fields(&self) -> &[ProtectedField] { ... }
fn delegation_depth(&self) -> Option<usize> { ... }
fn source(&self) -> SourceId { ... }
fn trust(&self) -> Trust { ... }
fn output_sensitivity(&self) -> Sensitivity { ... }
fn spend(&self, _output: &Self::Output) -> Spend { ... }
fn reconcile<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<Reconciliation<Self::Output>, EffectError>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait { ... }
}Expand description
Anything non-deterministic or externally visible.
Implemented by drivers (clock, RNG, MCP, A2A, model, timer) — never by
skill authors, who reach effects through
StepCtx.
Required Associated Types§
Sourcetype Output: Serialize + DeserializeOwned + Send
type Output: Serialize + DeserializeOwned + Send
What comes back. Must round-trip through JSON: replay reconstructs it from the journal rather than from the driver.
Required Methods§
Sourcefn descriptor(&self) -> EffectDescriptor
fn descriptor(&self) -> EffectDescriptor
What this effect does. Hashed (with position) into the effect key.
Provided Methods§
Sourcefn attach(&mut self, _provenance: &Provenance)
fn attach(&mut self, _provenance: &Provenance)
Receive the run-scoped provenance for this call.
Defaulted to a no-op, because most effects send nothing outward that a callee could check. The ones that do — a tool call, a peer call — store it and put it on the wire.
It arrives here rather than at construction because the
EffectKey is part of it, and an effect has no
business knowing its own key: the key includes the effect’s position in
the run, which only the runtime knows. That is the same reasoning that
turned Effect::key() into Effect::descriptor(), and it is why this is
a hook rather than a constructor argument.
Sourcefn gen_ai_operation(&self) -> Option<&'static str>
fn gen_ai_operation(&self) -> Option<&'static str>
Which OpenTelemetry GenAI operation this effect is, if it is one.
Returned as the value of gen_ai.operation.name — execute_tool for a
tool call, chat for a completion. Observability tooling keys on that
attribute, so an effect that does not answer here is invisible as an
agent operation even though its span is emitted: a trace shows the
agent invocation and nothing about the calls inside it.
Defaulted to None, which is the honest answer for the effects that are
not GenAI operations at all — reading the clock, sleeping, writing case
state. Labelling those would make the convention meaningless.
The conventions are still pre-1.0, which is why the version this targets
is pinned in telemetry::SEMCONV_VERSION rather than tracked.
Sourcefn mutates(&self) -> bool
fn mutates(&self) -> bool
Whether this mutates external state.
Drives the recovery default and the policy engine’s resource.mutates
attribute. Defaults to true: an effect that forgets to declare itself
is treated as dangerous.
Sourcefn recovery(&self) -> Recovery
fn recovery(&self) -> Recovery
What to do when the outcome is unknown — after a crash, or after an
InDoubt failure. The two are the
same situation reached from different directions.
Sourcefn retry(&self) -> RetryPolicy
fn retry(&self) -> RetryPolicy
How many times to repeat this effect when it fails, and how far apart.
The policy is not the safety control — Recovery and the failure’s
Disposition are, and they are consulted
first. Raising max_attempts cannot make a mutating in-doubt call
retryable; it only governs failures that are already safe to repeat.
Defaults to RetryPolicy::default — three attempts with exponential
backoff. That default is safe for a mutating effect precisely because
the disposition gate stands in front of it.
Sourcefn max_sensitivity(&self) -> Sensitivity
fn max_sensitivity(&self) -> Sensitivity
The highest data sensitivity this sink may receive.
The runtime refuses to pass arguments above this ceiling, which is the control for the exfiltration path that actually matters: a legitimate-looking call carrying a secret read three steps earlier.
Sourcefn sink_arguments(&self) -> Option<&Value>
fn sink_arguments(&self) -> Option<&Value>
The exact value this effect will send to its sink.
StepCtx::sink compares this value to
the labeled value it checks. Returning None means the effect has not
bound its outbound arguments and is therefore refused by sink.
Without this binding a caller could present a harmless trusted value to
the gate while the effect sent unrelated attacker-controlled arguments.
Sourcefn protected_fields(&self) -> &[ProtectedField]
fn protected_fields(&self) -> &[ProtectedField]
Field-specific source and sensitivity rules for a structured sink.
Declared selectors are enforced for every effect: a read-only URL, tenant, model, path, or query can still carry authority. A mutating effect with no protected fields additionally receives the conservative whole-object taint gate. Once fields are declared, those selectors carry the stricter trust/source checks while ordinary content may remain untrusted. This avoids broad whole-value releases merely to preserve a trusted recipient or amount beside untrusted descriptive text.
Sourcefn delegation_depth(&self) -> Option<usize>
fn delegation_depth(&self) -> Option<usize>
The resulting delegation depth when this effect hands authority onward.
None means the effect does not delegate. A peer call returns the depth
of the attenuated chain it will put on the wire, allowing a manifest to
enforce a ceiling at the last boundary before dispatch without teaching
core about any particular peer protocol.
Sourcefn source(&self) -> SourceId
fn source(&self) -> SourceId
The provenance identity an untrusted result of this effect carries.
This is the name a ProtectedField::from_sources rule matches, so its
precision decides what a source rule can say. The default —
effect:{kind} — names only the effect family, and a family is too
coarse for the rule that matters: “the recipient must come from the CRM
lookup” is unsatisfiable when every granted tool answers as
effect:tool.call, because the rule then admits whichever tool an
injected prompt reached first.
So the effects whose outputs feed authority-bearing fields override this with the identity an operator actually grants:
- a tool call → its reference,
tool://server/name; - a commission →
agent/{capability}; - a model completion →
model:{provider}/{model}.
The default stays for effects nobody writes source rules about — the clock, a case read — where a finer name would be precision without a consumer.
Sourcefn trust(&self) -> Trust
fn trust(&self) -> Trust
How much this effect’s output may be trusted.
Defaults to Trust::Untrusted, and the direction of that default is
the point. An effect is how the deterministic zone reaches the outside
world, so its result is the outside world’s data — a tool response, a
peer’s answer, a model completion. Those are the three most important
untrusted inputs an agent runtime handles, and the whole architecture
rests on them being labelled at the source rather than remembered about
later.
Getting this wrong in the safe direction produces spurious taint, which
is an annoyance that shows up immediately as a refused sink. Getting it
wrong the other way is a prompt injection reaching a mutating tool, which
shows up as a wire transfer. So the effect that forgets to declare
anything gets the conservative answer — the same rule
Effect::recovery follows.
Declare Trust::Trusted only for effects that do not cross a trust
boundary: the runtime’s own journaled clock, a seeded RNG, a durable
timer. tests/guards/layering.rs requires each one to be named, so a fourth has
to argue for itself.
Sourcefn output_sensitivity(&self) -> Sensitivity
fn output_sensitivity(&self) -> Sensitivity
How sensitive this effect’s output is, at minimum.
The runtime takes the maximum of this and whatever the trust level
implies — an untrusted result is already Internal. So this can raise
sensitivity and never lower it, which is the only safe direction: an
effect that could declare its output less sensitive than its
provenance implies would be a laundering primitive with a polite name.
Declare it for effects that return things worth protecting: a vault read, a model completion over customer records, a peer’s answer about a named person. The egress ceiling on the next sink is what then does the work.
Sourcefn spend(&self, _output: &Self::Output) -> Spend
fn spend(&self, _output: &Self::Output) -> Spend
What this effect consumed.
Reported after the fact because only then is it known, and journaled in
the EffectDone record so replay adds up the same figures. Asking a
provider what something cost at replay time would give a moving answer,
and the budget verdict would move with it.
Sourcefn reconcile<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<Reconciliation<Self::Output>, EffectError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn reconcile<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<Reconciliation<Self::Output>, EffectError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
Ask the provider whether a call landed.
Called only when recovery is
Recovery::Reconcile and the outcome is genuinely unknown — after a
crash between “sent” and “recorded”, or after an
InDoubt failure. The two are the
same situation, and this is the only thing that resolves either without
guessing.
The probe must identify the call by something stable across attempts — an idempotency key, a client reference, an order id carried in the request. A probe that searches by timestamp or by “most recent” is not a probe; it is a guess with extra steps.
The result is journaled, so replay reads the verdict back rather than
probing again. The default is Inconclusive:
declaring Reconcile without implementing this escalates to an operator
rather than silently deciding either way.
Dyn Compatibility§
This trait is dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".
Implementations on Foreign Types§
Source§impl Effect for Box<dyn AnyEffect + '_>
So an erased effect dispatches exactly like a typed one, with no second
code path in the runtime that could drift from the first.
impl Effect for Box<dyn AnyEffect + '_>
So an erased effect dispatches exactly like a typed one, with no second code path in the runtime that could drift from the first.
Written over dyn AnyEffect + 'e rather than the 'static default: the
executor re-enters itself through a commission, so this impl has to hold for
whatever lifetime the surrounding future is inferred at, and pinning it to
'static makes that proof fail for every specific one.