AI agent state management: resume a run without paying twice
Design state for an AI agent you build: task state in your own store, a checkpoint per step, tool calls safe to retry, and long-term memory that expires.
Last verified against official docs on
Contents
An AI agent has three kinds of state: the context window for one call, task state for the run, and long-term memory across runs. Save task state outside the model at every step, so a run can resume after a crash or a human approval. Then make tool calls safe to repeat, because LangGraph, OpenAI’s Agents SDK, Google’s ADK and Temporal all say a resumed step can run again.
The framework behaviour below comes from each project’s official docs, last verified on 1 November 2026. None of it was tested for this post.
This is part 30 of the series. Part 14 covered the context of a coding agent you use. This part is about an agent you build and run yourself.
The context window, task state and long-term memory hold different things
An agent’s context window is everything the model sees in one call. When the call ends, it is gone, unless your code sends it again. Task state is where the current run stands. Long-term memory is what one run leaves for later runs.
| Kind | Holds | Lives in | Lasts | Example |
|---|---|---|---|---|
| Context window | Instructions, recent messages, tool results for this call | The model request | One call | The last few messages and the order details |
| Task state | The plan, finished steps, their results, the pending action, approvals | Your database or a checkpointer | One run, which can take days | “Step 3 of 5, waiting for approval” |
| Long-term memory | Facts and lessons a later run needs | A store your code searches | Until it expires or is deleted | “Prefers refunds as store credit” |
The frameworks draw the same lines. LangGraph’s persistence docs use checkpointers for “short-term, thread-scoped memory” and stores for “long-term, cross-thread memory”. Google’s ADK memory docs put it as “Session / State: It’s your short-term memory during one specific chat.”
The usual trap is chat history as the one record of task state. If step 3 is a message and nothing more, a restart loses it. Or the model has to work out from old text where it stopped.
When you build the agent, you own the loop, and you pay for any step that runs twice.
Keep task state in your own store, one step at a time
Task state belongs in storage your code controls, and your code writes it at each step. The model proposes the next action. Your code checks and records it before anything runs.
An example task record for a made-up refund agent, one row per run:
run_id r_7f3a one per run; also the checkpoint key
status awaiting_approval
current_step 4 of 5 the next step to run
plan the steps the agent proposed, saved once
step_results the output of each finished step
pending_action issue_refund(order 1042, amount 40.00)
idempotency_key r_7f3a:step-4:refund
approved_by empty until a person approves
updated_at 2026-10-30T10:42:00ZName the allowed moves between statuses, and reject any other. An example set of transitions for the same agent:
received -> planning -> running -> awaiting_approval -> running -> done
running -> failed (save the error and the last finished step)
awaiting_approval -> cancelled (the person said no)Write the step down before its side effect, and the result after it. A restart then finds the last step on record. If the record says “refund started” with no result, the retry section below covers what to do.
ADK’s state docs make the same point.
“State should always be updated as part of adding an Event to the session
history using session_service.append_event().” Changing the state object
directly “bypasses the ADK’s event tracking and can lead to lost data”.
Checkpoint every step so a run resumes after a crash or an approval
A checkpoint is a saved copy of the task state after a step finishes. With one, a crashed run starts again from the last finished step. A run waiting on a person can save its state and stop, then pick up when the answer arrives.
LangGraph’s checkpointer docs say a checkpointer “saves a snapshot of graph state at each super-step, organized into threads”. Its interrupts docs describe the approval case: the graph saves its state and “waits indefinitely until you resume execution”.
Check where the checkpoint lives. LangGraph’s InMemorySaver keeps
checkpoints in RAM, and “When the process restarts, all checkpoints are lost”
(persistence docs).
ADK’s state docs say the same of
InMemorySessionService: “State is lost on restart.”
| Framework or SDK | Task state and resume | Long-term memory | On repeats and expiry |
|---|---|---|---|
| LangGraph (persistence, checkpointers, interrupts, stores) | A checkpointer saves state per thread_id at each super-step; PostgresSaver is the production example; interrupt() pauses for a person |
Stores hold “long-term, cross-thread memory” under namespaces, with optional semantic search | On resume, “any code before the interrupt runs again”; “Prune old checkpoints periodically or set a retention policy” |
| OpenAI Agents SDK (sessions, human in the loop) | Sessions store conversation history in SQLite, Redis, SQLAlchemy and other backends; “RunState is designed to be durable” for paused approvals | Session history per session ID; DaprSession and EncryptedSession add TTL |
“Approval permits a tool call to execute; it does not guarantee exactly-once execution.” |
| Google ADK (state, memory, resume) | session.state, persistent with DatabaseSessionService; resumable workflows from ADK Python 1.16, by invocation ID |
MemoryService with add_session_to_memory and search_memory |
Tools “are run at least once, and may run more than once when resuming a workflow” |
| Anthropic memory tool (docs) | No checkpointer; the added system prompt says “ASSUME INTERRUPTION” and asks Claude to record progress in memory | Files under /memories, run by your handler on storage you control |
“Periodically delete memory files that haven’t been accessed in a long time.” |
| Temporal (workflow execution, activities, retry policies) | A Workflow Execution “resumes execution from the latest state” after a failure; LLM calls belong in Activities, which retry | Not covered on these pages | Activities “may be executed more than once” |
Every row comes from the linked docs, last verified on 1 November 2026, and none was tested for this post. None of these is required. A table with a status column, a checkpoint per step and an idempotency key covers the same ground in your own database. The frameworks package it for you.
Make every tool call with a side effect safe to run twice
A tool call with a side effect changes something outside the agent, such as charging a card or sending an email. A resumed run can repeat that call, and four of the projects above say so in their own words.
Temporal’s activity docs describe the classic case. The work completes, “but the Worker crashes just before it notifies the Temporal Service”, so the Activity is retried. The OpenAI Agents SDK’s human-in-the-loop docs give the agent version: “a payment can complete even though the saved snapshot has no record of the payment result.”
The fix is an idempotency key: one ID per logical action, the same on every retry, checked by the system that does the work. Temporal’s docs say these keys “are enforced by the service you are calling from your Activity, not by the Activity itself”. The same page notes that most payment processors’ APIs accept one with the request.
An example tool, not run, with made-up db and payments objects:
def issue_refund(run_id: str, step_id: str, order_id: str, amount: str):
key = f"{run_id}:{step_id}:refund" # the same on every retry of this step
saved = db.get_action(key)
if saved and saved.status == "done":
return saved.result # this step already finished
db.save_action(key, status="started")
# after a crash, this call goes out again with the same key
result = payments.refund(order_id, amount, idempotency_key=key)
db.save_action(key, status="done", result=result)
return resultThe local record skips the call when the step is known to be done. If the record says “started” with no result, the call goes out again with the same key. The payment provider’s check then stops a second refund. The same OpenAI page says “a local completion flag alone” cannot make “the external action and the saved run state one atomic operation”.
Some destinations take no key. For those, the OpenAI human-in-the-loop docs say to reconcile the outcome with the destination before retrying. “If the outcome remains uncertain, the application should record it as unknown and avoid automatic replay.” A person then decides.
Where the call sits matters too. LangGraph’s
interrupts docs
suggest placing side effects after the interrupt call, or in a separate
node. Temporal’s
retry policy docs
suggest one Activity per step, so a retry repeats the failed step alone.
Give long-term memory a scope and an expiry date
Long-term memory is for facts a future run needs and cannot read from a system of record. A preference the user stated is one. A lesson from a failed run is another.
Some things stay out of long-term memory:
- Live data, such as an order’s status. It belongs in the order system, and a remembered copy goes stale.
- Task progress. That is task state, saved in the run’s checkpoint.
- Secrets and sensitive data. Anthropic’s memory tool docs say to “add validation that strips sensitive data before your handler writes the file”.
Retrieval decides whether memory helps. Scope every lookup to a namespace,
usually the user. LangGraph’s
stores docs use a
namespace such as (user_id, "memories") and return up to limit items
(default 10). ADK’s
MemoryService offers search_memory,
which an agent usually calls through a tool. Searching memory is a retrieval
problem. Part 29 covers retrieval for
RAG, from metadata filters to retrieval tests.
Give each memory a source and a date, so old facts can be found and removed. An example entry:
namespace user_481 / preferences
text Prefers refunds as store credit
source run r_7f3a, stated by the user
created_at 2026-10-30
last_used_at 2026-10-30
review_after 2027-04-30Expire memory on a schedule. Anthropic’s memory tool docs suggest deleting files “that haven’t been accessed in a long time”. When a new fact contradicts an old one, update the entry in place. Anthropic’s docs also offer an optional prompt line: “You can rename or delete files that are no longer relevant.”
When memory and live data disagree, trust the live data. Put the memory’s date in the context, so the model can see how old a fact is.
Two prompts and a checklist for agent state
Both prompts work in any coding agent that can read your repository.
Prompt 1: map the state in an existing agent
An example prompt for a read-only review of an agent you already run.
Task: map how <the agent or service> keeps state.
Start from: <path or entry point>
Do not change code and do not call any model or external API.
Answer each question with the file and line:
1. Context window: what goes into each model call, and what decides it?
2. Task state: where is the run's progress saved, and when?
Is it saved before and after every tool call?
3. Resume: if the process stops mid-run, what happens on restart?
Does anything keep state in memory alone?
4. Approvals: how does a run wait for a person, and where is it saved?
5. Tools: list every tool with a side effect. Does each one use an
idempotency key the destination checks?
6. Long-term memory: what is written, under which namespace, with
what date, and what ever deletes it?
Mark anything you could not confirm as unverified.
Never print, expose or commit secret values.
Do not commit, push, deploy, run destructive commands or make external
changes unless I explicitly authorise it.
If two reasonable attempts fail, stop and report the failure rather
than repeatedly guessing.
End with the three gaps most likely to repeat a side effect or lose a run.Prompt 2: make one tool call safe to retry
An example prompt for planning the change to one tool before any code moves.
Task: make <tool name> safe to run more than once for the same step.
First, plan only:
1. What does the tool change outside the agent?
2. Does the destination accept an idempotency key? Quote its docs.
3. Propose a key that stays the same across retries of one step,
such as the run ID plus the step ID.
4. Where will the tool record "started" and "done" with the result?
5. If the destination takes no key, how will the code check the real
outcome before a retry, and when will it mark the outcome unknown
and stop for a person?
6. Which test proves a second call with the same key changes nothing?
Wait for my approval before editing.
Never print, expose or commit secret values.
Do not commit, push, deploy, run destructive commands or make external
changes unless I explicitly authorise it.
If two reasonable attempts fail, stop and report the failure rather
than repeatedly guessing.Run the test from step 6 against a sandbox account.
An agent state checklist
An example checklist, in the order of this post:
AGENT STATE: REVIEW
□ Task state saved outside the model, keyed by a run ID
□ Allowed status changes written down; others rejected
□ Step recorded before its side effect, result after
□ Checkpoints in persistent storage, not process memory
□ Paused runs saved server-side; approver checked
□ Every side-effect tool has an idempotency key
□ Unknown outcomes reconciled or sent to a person
□ Long-term memory scoped by namespace, with source and date
□ No live data, task progress or secrets in memory
□ Old memory and old checkpoints deleted on a scheduleQuick answers
What is the difference between agent state and agent memory?
Agent state, or task state, is where one run stands: its plan and the steps finished so far. Agent memory is long-term: facts and lessons kept across runs, such as a user’s preference. State ends with the run, while memory lasts until it expires.
What is a checkpointer in an AI agent framework?
A checkpointer saves a snapshot of an agent’s state after each step, keyed by a thread or run ID. LangGraph uses checkpointers so a graph can resume after a failure or wait for human input. In-memory checkpointers lose everything on restart.
How does an AI agent resume after a human approval?
An AI agent pauses at the approval step, saves its state, and stops. When the person answers, your code loads the saved state by its run ID and continues. LangGraph, the OpenAI Agents SDK and Google’s ADK each document a version of this.
Why do AI agent tool calls need idempotency keys?
A crash between a tool’s action and its saved result can make that tool run again when the agent resumes. An idempotency key, sent to the destination, lets the destination spot the repeat and skip it, so a payment or ticket is not created twice.
How long should an AI agent keep long-term memory?
Long-term memory should last as long as it stays true and useful. Record a source and a date on each entry, update entries that change, and delete entries nobody has read in a long time. Anthropic’s memory tool docs recommend this kind of periodic deletion.
More on AI coding is in the AI category.

Kousigan A
Build with AI. Ship with care.
Software engineer building real products with AI assistants. I write down what works: prompts, rules, skills and agents, and how to ship safely.