tool-side-effects-tag
Declare what an LLM agent tool actually does so the scheduler / retry layer can make the right decision per-tool. Zero runtime deps.
use ;
;
;
;
let search = SearchWeb.side_effects;
let upsert = UpsertUser.side_effects;
let delete = DeleteAccount.side_effects;
assert!; // pure read
assert!; // writes
assert!; // idempotent write
assert!;
Why
Most agent loops treat every tool the same way. If retry is on, it's on for everything, including send_email, which is unfortunate when the network blip causes a duplicate. If parallelism is on, it's on for everything, including upsert_user, which races with itself.
tool-side-effects-tag is a one-line declaration so the dispatcher knows what to do per-tool. Zero magic.
The standard tags:
| Tag | What it means |
|---|---|
Read |
No state mutation. Safe to parallelize and retry. |
Write |
Mutates state. Not parallel-safe by default. |
Idempotent |
Same args produce the same effect. Retry-safe. |
Destructive |
Delete/drop/purge. Never auto-retry. |
External |
Third-party system (email, payments). Not retry-safe without Idempotent. |
Expensive |
High cost. Caller may want extra confirmation. |
Network |
Makes a network call. Subject to transient errors. |
Install
[]
= "0.1"
Optional serde support:
[]
= { = "0.1", = ["serde"] }
API
use ;
SideEffect
Debug + Clone + Copy + PartialEq + Eq + Hash, plus Display and FromStr over the lowercase slugs "read", "write", "idempotent", "destructive", "external", "expensive", "network". Unknown inputs return ParseSideEffectError.
SideEffects
Thin wrapper around HashSet<SideEffect>:
use ;
let mut s = new;
s.insert;
s.insert;
assert!;
assert_eq!;
s.remove;
for e in s.iter
SideEffects also implements FromIterator<SideEffect> so you can do:
use ;
let s: SideEffects = .into_iter.collect;
Tag<T>
Pair any value with a SideEffects set:
use ;
let mut effects = new;
effects.insert;
let tag = new;
assert_eq!;
assert!;
let inner = tag.into_inner;
Tag<T> itself implements HasSideEffects, so you can pass a Tag<MyTool> anywhere a HasSideEffects is expected.
Conservative defaults
- An untagged (empty) set returns
falsefor bothis_parallel_safeandis_retry_safe. If you don't know what a tool does, don't run it in parallel and don't retry it. - A destructive set returns
falseforis_retry_safeeven if also taggedIdempotent. Destructive intent overrides idempotent for retry purposes. If you actually want the retry, the caller should ask for it explicitly.
Companion libraries
llm-retry— gate retries onis_retry_safe(tool).agentleash— gate destructive calls on operator confirmation.tool-side-effects-tag— the Python sibling.
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.