LangGraph

Resume a LangGraph agent after its process dies. recall-langgraph records the agent's working state, pointers to its work, and every turn to Recall as it runs. The next process that opens the same agent id starts from a briefing built from those records.

It sits beside LangGraph's checkpointer, not in place of it. The checkpointer still handles threads and interrupts however you configure it. The launch post explains the design and what we measured.

Install and use

pip install recall-langgraph langchain-openai

Durable tools need recall-langgraph 0.2.0 or later.

recall-langgraph also installs the polign_db server and polign CLI for Linux, macOS and Windows, so there is no separate database to download. langchain-openai is only for the model in this example.

from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent
from recall_langgraph import RecallResume, recall_tools

with RecallResume("billing-migrator", local_dir="./recall-data") as resume:
    agent = create_react_agent(
        ChatOpenAI(model="gpt-4.1"),
        tools=[*your_tools, *recall_tools(resume)],
        prompt="Keep your working state current with update_working_state when your plan changes.",
        pre_model_hook=resume.pre_model_hook,
        post_model_hook=resume.post_model_hook,
    )
    agent.invoke({"messages": [("user", "Carry on with the migration.")]})

Run it, kill it halfway, and run it again: the second run starts from what the first one wrote.

create_react_agent in LangGraph 1.x

create_react_agent is deprecated in LangGraph 1.x in favor of langchain.agents.create_agent, but it still ships in langgraph.prebuilt and the hooks work with it.

What the hooks do

The tools

recall_tools(resume) gives the model four tools.

ToolWhat the model uses it for
update_working_stateSave its goal, plan, progress, focus, decisions, open questions and notes. Fields it leaves out are kept.
milestoneMark a durable point, such as tests passing.
fetch_outputRead a large stored output in full, by the reference the briefing shows.
set_pointerRecord where a piece of work lives: a git branch or commit, an object, an environment, an external item, or a process.

Tool calls that touch the world

The records tell a new process what the agent was doing. They cannot tell it whether the last tool call happened. If the process died while send_email was running, the email may or may not have gone out, and an agent that carries on will often send it again.

Wrap those tools with resume.durable. Each call is then recorded before it runs and closed with its result when it returns, and the calls a crash interrupted are settled before the model sees its first briefing:

from recall_langgraph import ActionEnd, RecallResume, recall_tools

def sent_folder(action):
    # Ask the outside system whether the call took effect.
    if mailbox.has_sent_since(action.started_at, subject_in=action.args):
        return ActionEnd("done", resolution="found in the sent folder")
    return None  # cannot tell

resume = RecallResume("support-agent", resolver=sent_folder)
tools = resume.durable(
    [lookup_order, save_invoice, send_email],
    classes={"lookup_order": "pure", "save_invoice": "idempotent", "send_email": "external"},
)
agent = create_react_agent(model, [*tools, *recall_tools(resume)],
                           pre_model_hook=resume.pre_model_hook,
                           post_model_hook=resume.post_model_hook)

Every wrapped tool needs a class, given in classes or as the tool's metadata["recall_class"]. A tool without one is refused when you call durable, rather than given a guess.

ClassUse it forIf a crash interrupted the call
pureReads: lookups, searches, listingsClosed as abandoned. The model runs it again if it still needs the answer.
idempotentWrites that are safe to repeat with the same key: a PUT by id, a push of a known commit, a payment with an idempotency keyRun again with the same key before the first briefing, and the result goes into the history.
externalAnything that could happen twice: send, post, charge, deletePassed to resolver. Done or failed is recorded in the history. With no resolver, or when it returns None, the call is listed at the top of the briefing as one to check before repeating.

An idempotent tool gets the key in an argument named idempotency_key, the same key on every retry. Annotate it so the model is not asked for it:

from typing import Annotated
from langchain_core.tools import InjectedToolArg, tool

@tool
def save_invoice(invoice: str, total: int,
                 idempotency_key: Annotated[str, InjectedToolArg] = "") -> str:
    """Save an invoice."""
    return billing.put(invoice, total, idempotency_key=idempotency_key)

Settling happens once per resume; call resume.settle() to do it at a point you choose. A tool that raises is recorded as failed. A killed process or a cancelled call leaves the action open, since nothing says whether it took effect. A LangGraph interrupt() inside a tool closes the call as abandoned, because LangGraph runs the tool again when the graph resumes. In a kill test of 540 runs, durable tools never repeated an email or an invoice; Durable agent actions has the numbers.

Options

OptionDefaultWhat it does
clientnoneA polign_recall.Client opened with agent=True, to share one subprocess. Without it, RecallResume opens its own.
local_dirnoneKeep the records in this directory on this machine and run a server for it.
envprocess environmentPOLIGN_URL, POLIGN_API_KEY and the rest, for a server you run yourself.
token_budget8000Upper bound on the briefing, in tokens.
lease_ttl60Seconds each lease lasts. It is renewed in the background while the process runs.
holderhost, pid and a random suffixNames this process in the lease records.
output_threshold2000Turns longer than this many tokens are stored whole as outputs and shown in the briefing as a reference.
resolvernoneCalled with each interrupted external call on resume, to check whether it happened. See Tool calls that touch the world.

Where the records live

local_dir keeps the records on one machine. Agents that move between machines need one shared server: run polign-server with its store in your S3, GCS or Azure bucket where they can all reach it (Run polign_db shows how) and pass its address through env or the environment:

RecallResume(
    "billing-migrator",
    env={"POLIGN_URL": "http://memory.internal:23000", "POLIGN_API_KEY": key},
)

The agent's records live in their own collection, named after the memory collection with _agents added to the end.

Errors

Limits

Source & license

recall-langgraph is open source under the Apache License 2.0 and lives next to the Python client in github.com/Polign/polign, along with its own StateGraph nodes and the benchmark harness. Bug reports and pull requests go there.