agent-outbox
Wrap a tool so an agent can retry it without running the side effect twice.
Same session + tool + args → first call runs, later calls replay.
uv add agent-outbox
# or
pip install agent-outbox
Until the first PyPI release lands, install from GitHub:
uv add git+https://github.com/adp811/agent-outbox
LangGraph extra (only needed for the sample graph): uv add "agent-outbox[langgraph]"
From this repo:
uv sync
uv run agent-outbox-demo # retries create_ticket 5 times, 1 ticket
uv run agent-outbox-graph # same storm, as a LangGraph agent → tools loop
uv run agent-outbox-eval # 23/23 retry-storm cases
Usage
The tool is a normal function or client method. You wrap the call with run:
from agent_outbox import ToolExecutor, OutboxStore
store = OutboxStore() # or OutboxStore("outbox.db") to persist
ex = ToolExecutor(store)
class TicketClient:
def create_ticket(
self,
title: str,
severity: str,
idempotency_key: str | None = None,
) -> dict:
return httpx.post(
"https://tickets.example/v1/tickets",
json={"title": title, "severity": severity},
headers={"Idempotency-Key": idempotency_key},
).json()
tickets = TicketClient()
args = {"title": "kserve replica crashloop", "severity": "high"}
session = "agent-session-1"
# first call: POSTs
a = ex.run("create_ticket", tickets.create_ticket, args, session)
# agent retries the same call: no second POST, same payload
b = ex.run("create_ticket", tickets.create_ticket, args, session)
run takes:
| arg | meaning |
|---|---|
tool |
name, part of the key |
fn |
the function to call once |
args |
kwargs passed to fn (key order does not matter) |
session_id |
one agent run / conversation |
Returned Execution: ok, replay, result, error.
A new session, tool name, or arg value is a new call and will fire again.
If fn has an idempotency_key parameter, run fills it in so the downstream API can also dedupe.
LangGraph tool node
The graph is a normal agent → tools loop. The agent is scripted (it keeps emitting the same create_ticket call — no LLM). The tools node is the wrap:
from langgraph.graph import END, START, StateGraph
from agent_outbox import ToolExecutor, OutboxStore
def tools(state):
execution = ex.run(
"create_ticket",
tickets.create_ticket,
{"title": state["title"], "severity": state["severity"]},
state["session_id"],
)
return {"replay": execution.replay, "result": execution.result}
graph = StateGraph(AgentState)
graph.add_node("agent", agent) # decides to retry
graph.add_node("tools", tools) # outbox absorbs the storm
graph.add_edge(START, "agent")
graph.add_conditional_edges("agent", route, {"tools": "tools", "__end__": END})
graph.add_edge("tools", "agent")
Five trips through tools, one create_ticket side effect. uv run agent-outbox-graph runs it.
If the tool fails
The first attempt is recorded as failed. Retries return that error and do not call fn again.
If a retry lands while the first call is still running
Waiters poll the outbox until the owner finishes (default wait_timeout=10s) and then replay the same result. They do not start a second side effect.
If the process dies mid-call
Pending rows have a lease (lease_seconds=30 by default). Heartbeats refresh it while fn is running. After the lease expires, another worker can reclaim and run fn again.
That reclaim is at-least-once: if the original worker already performed the side effect and crashed before complete(), a reclaim will fire twice. Downstream Idempotency-Key is how you close that gap.
Optional: write spans
from agent_outbox import JsonlTracer, ToolExecutor, OutboxStore
tracer = JsonlTracer("spans.jsonl")
ex = ToolExecutor(OutboxStore("outbox.db"), tracer)
Each attempt logs tool, key, session_id, replay, status.
Tests
uv sync
uv run pytest
uv run agent-outbox-eval
CI runs pytest, the 23-case eval, and a wheel install on Python 3.11–3.13.
Release to PyPI
CI is automatic on push. Publishing is a GitHub Release (v0.1.0).
One-time PyPI setup: pypi.org → Publishing → pending publisher. Project name agent-outbox, owner adp811, repo agent-outbox, workflow publish.yml. Then create a GitHub release tagged v0.1.1.
Metadata
Release files for agent-outbox 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| agent_outbox-0.1.1.tar.gz | 97.3 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| agent_outbox-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 111.4 kB
Release files / agent_outbox-0.1.1.tar.gz
| Download URL | agent_outbox-0.1.1.tar.gz |
|---|---|
| Size | 97.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bc8d81cd1481e6c8471d91cbf9d52bf06edd2873817e28006c09cb0b9edad912
|
|
BLAKE2b-256 checksum How to use checksums |
d78dc4d16b870a64e60d23ba6aebc82a374ff7fb2c432db4084b9e52a4462274
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.6 {"installer":{"name":"uv","version":"0.12.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / agent_outbox-0.1.1-py3-none-any.whl
| Download URL | agent_outbox-0.1.1-py3-none-any.whl |
|---|---|
| Size | 14.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8224699f4e516bb1d853d911de15ed282806bef493f0f5b851cb8451008c4008
|
|
BLAKE2b-256 checksum How to use checksums |
2dd856f4c3ef883ea6cb6606064d933b386a839fb5dafa94d8e9ffeda8971a0c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.6 {"installer":{"name":"uv","version":"0.12.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for agent-outbox 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.