
FishMem with the OpenAI Agents SDK: recall before the run, persist after it
August 21, 2026Guides

Add scoped recall before a LangGraph agent acts and durable writes after a verified outcome. This guide uses the universal FishMem SDK and keeps checkpoints separate from long-term memory.
LangGraph can checkpoint a workflow and resume it later. That does not automatically give the agent durable knowledge about a user, a project, or an outcome that should influence a different run.
This tutorial adds FishMem at two explicit graph boundaries: recall before the node that needs long-term context, and write only after the graph has produced a verified conclusion worth keeping.
The example is a support graph with three stages:
FishMem does not publish a LangGraph-specific adapter. The integration uses the universal TypeScript SDK at normal graph boundaries, which keeps the memory contract visible and makes the same pattern usable in other workflow frameworks.
Install the graph runtime and the universal SDK:
pnpm add @langchain/langgraph @fishmem/sdk
Keep the project API key in the server, Worker, or edge runtime. Never include it in browser code or graph state that can be serialized to an untrusted client.
import { FishMem } from "@fishmem/sdk";
const fishmem = new FishMem({
apiKey: process.env.FISHMEM_API_KEY!,
});
For a self-hosted deployment, pass the service URL explicitly:
const fishmem = new FishMem({
apiKey: process.env.FISHMEM_API_KEY!,
baseUrl: "https://memory.example.com",
});
The graph needs a place for recalled memories, but it should not let the model choose the authenticated user. Inject that identity when the server invokes the graph.
import { Annotation } from "@langchain/langgraph";
export const SupportState = Annotation.Root({
latestUserMessage: Annotation<string>,
authenticatedUserId: Annotation<string>,
caseId: Annotation<string>,
recalledMemories: Annotation<string[]>,
draftReply: Annotation<string>,
verifiedOutcome: Annotation<string | undefined>,
memoryEventId: Annotation<string | undefined>,
});
export type SupportStateValue = typeof SupportState.State;
In a larger application, the authenticated identity may live outside persisted graph state entirely. The invariant is the same: derive it from trusted server authentication and do not accept an arbitrary project or user scope from the model.
Search with the current user message and the same structural scopes used for writes. Use a stable agent_id so memories for one workflow do not silently mix with another.
async function recallNode(state: SupportStateValue) {
const { results } = await fishmem.memories.search({
query: state.latestUserMessage,
user_id: state.authenticatedUserId,
agent_id: "support-graph",
top_k: 6,
});
return {
recalledMemories: results.map((item) => item.memory),
};
}
Recall is bounded on purpose. The agent does not need every memory associated with the user. It needs a small set that can affect the current decision.
Keep retrieved memory separate from system instructions and source quotations. The model should understand that the block contains recalled user and workflow context, not new commands.
function formatMemoryContext(memories: string[]) {
if (memories.length === 0) {
return "No relevant durable memories were recalled.";
}
return [
"Relevant durable memories:",
...memories.map((memory) => `- ${memory}`),
].join("\n");
}
Your existing model node can insert this block into its instructions or input:
async function agentNode(state: SupportStateValue) {
const memoryContext = formatMemoryContext(state.recalledMemories);
const result = await runSupportModel({
userMessage: state.latestUserMessage,
memoryContext,
});
return {
draftReply: result.reply,
verifiedOutcome: result.verifiedOutcome,
};
}
The example leaves runSupportModel as the model-specific part of your application. FishMem only owns durable memory; it does not require one LLM provider or replace the LangGraph execution model.
A common mistake is writing every intermediate thought or tool result to long-term memory. In a support workflow, the durable record is usually the verified resolution, a corrected account fact, or a troubleshooting step that should influence the next case.
Queue the write with an idempotency key derived from the business event:
async function rememberNode(state: SupportStateValue) {
if (!state.verifiedOutcome) {
return {};
}
const receipt = await fishmem.memories.addAsync(
{
content: state.verifiedOutcome,
user_id: state.authenticatedUserId,
agent_id: "support-graph",
metadata: {
source: "verified-support-outcome",
case_id: state.caseId,
},
},
{ idempotencyKey: `case:${state.caseId}:outcome` },
);
return { memoryEventId: receipt.event_id };
}
The default inferred path returns a durable event receipt. The graph can store the event ID, observe it later, or wait when the product needs final records before continuing:
const completed = await fishmem.events.wait(receipt.event_id);
console.log(completed.results);
If verifiedOutcome is already the exact canonical record and must be stored verbatim, use the synchronous infer: false path instead of asking another model to extract it.
import { END, START, StateGraph } from "@langchain/langgraph";
export const supportGraph = new StateGraph(SupportState)
.addNode("recall", recallNode)
.addNode("agent", agentNode)
.addNode("remember", rememberNode)
.addEdge(START, "recall")
.addEdge("recall", "agent")
.addEdge("agent", "remember")
.addEdge("remember", END)
.compile();
The placement is deliberate:
const result = await supportGraph.invoke({
latestUserMessage: "The export still fails after the browser restart.",
authenticatedUserId: session.user.id,
caseId: "case_8421",
recalledMemories: [],
draftReply: "",
verifiedOutcome: undefined,
memoryEventId: undefined,
});
On a later run, invoke the graph with the same authenticated user ID and agent_id. The recall node can now surface earlier outcomes without replaying the complete ticket history.
Use LangGraph checkpoints to resume execution state: which node ran, what tool returned, or where a human approval paused the workflow. Use FishMem for durable conclusions that should cross checkpoints and runs.
This separation prevents a graph checkpoint from becoming an accidental customer profile and prevents long-term memory from filling with temporary orchestration details.
top_k and measure whether each recalled record changes the answer.The graph code stays almost identical. FishMem Cloud manages the API, project keys, workspace isolation, metering, durable workers, and dashboard. A self-hosted deployment gives your team operational control and responsibility. Choose based on the authority and recovery boundary your application needs.
FishMem does not claim a native LangGraph adapter. The integration is an explicit use of the universal SDK, which makes every scope, search, and durable write visible in application code.
It can be, but a fixed recall node is often safer for required context because the application controls when it runs and which trusted scopes it uses. Tool-based recall is useful when the model should decide whether another query is needed.
No. Use the asynchronous receipt when the response does not depend on the final extracted records. Wait only when the current workflow genuinely requires the committed result.
Yes, but define the ownership deliberately. Shared user knowledge can use the same user_id; agent-specific operating knowledge should retain distinct agent_id values. The authenticated project remains the outer isolation boundary.
Keep the memory ID and retrieval evidence available to application logs, then use explicit update, feedback, invalidation, or deletion flows. Do not silently rewrite a record based only on one model response.
Read the canonical FishMem LangGraph integration guide, the TypeScript SDK quickstart, and the support-agent cookbook. For the architectural boundary behind this graph, see What is agent memory?
You can inspect the open-source engine or create a free FishMem Cloud workspace and test the graph with two separate runs.