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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

agent_outbox-0.1.1.tar.gz (97.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

agent_outbox-0.1.1-py3-none-any.whl (14.1 kB view details)

Uploaded Python 3

File details

Details for the file agent_outbox-0.1.1.tar.gz.

File metadata

  • Download URL: agent_outbox-0.1.1.tar.gz
  • Upload date:
  • Size: 97.3 kB
  • Tags: Source
  • Uploaded using 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}

File hashes

Hashes for agent_outbox-0.1.1.tar.gz
Algorithm Hash digest
SHA256 bc8d81cd1481e6c8471d91cbf9d52bf06edd2873817e28006c09cb0b9edad912
MD5 20e9d0af1a8d320fb33ca2c3b105a2ff
BLAKE2b-256 d78dc4d16b870a64e60d23ba6aebc82a374ff7fb2c432db4084b9e52a4462274

See more details on using hashes here.

File details

Details for the file agent_outbox-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: agent_outbox-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 14.1 kB
  • Tags: Python 3
  • Uploaded using 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}

File hashes

Hashes for agent_outbox-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 8224699f4e516bb1d853d911de15ed282806bef493f0f5b851cb8451008c4008
MD5 f9f5587506820299770384d842ba7529
BLAKE2b-256 2dd856f4c3ef883ea6cb6606064d933b386a839fb5dafa94d8e9ffeda8971a0c

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page