Skip to main content

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.

CI

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)

Source distribution for agent-outbox 0.1.1
File Size Uploaded Provenance
agent_outbox-0.1.1.tar.gz 97.3 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for agent-outbox 0.1.1
File Interpreter ABI Platform
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.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page