Giving an Agent Memory
Enable long-term memory on a session agent, watch it save, recall, search, and pin memories, and inspect and delete its records through the API.
In this tutorial you give a session agent long-term memory. The agent keeps notes about payout journals: each journal is its own session. In the first session it saves a fact about that journal and a reporting rule for every journal, and pins the rule. You then open a second session and see which memories cross over, run a later turn of the first session that recalls and searches what it saved, search the session's history, and finally read and delete records through the API.
The agent uses the fixture model: a script of model responses that Twisp plays back in order. The script also answers the review that runs after each turn to pick out what is worth remembering, so every run gives the same result and nothing calls a real model. An optional last step runs that review with a Claude model.
You need a running local environment. Open GraphiQL in the local console and run each step as its own request. If you have not built an agent before, start with Your First Agent.
The UUIDs below are examples. If you have run this tutorial before, choose fresh UUIDs for the agent and the journals. Execution and memory IDs differ on every run: wherever a step uses one, substitute the ID from your own earlier response.
1. Create the agent
mutation CreateMemoryAgent {
actions {
createAction(
id: "2afe6c28-d18d-4f3e-801d-352740eb8925"
input: {
name: "agent-memory-notes"
kind: "agent"
agent: {
prompt: "You keep notes about payout journals. Remember what you learn about each journal, and how operators want payouts reported."
}
triggers: [
{
name: "opened"
event: "event.type == 'journal.inserted' && event.data.name.startsWith('agent-memory-')"
session: {
start: {
input: { journal_id: "string(event.data.journal_id)", name: "event.data.name" }
}
}
}
{
name: "updated"
event: "event.type == 'journal.updated' && event.data.name.startsWith('agent-memory-')"
session: { resume: { id: "string(event.data.journal_id)" } }
}
]
session: {
inputSchema: {
type: "object"
properties: { journal_id: { type: "string" }, name: { type: "string" } }
required: ["journal_id", "name"]
}
id: "session.input.journal_id"
}
config: {
runtime: "strands"
harness: {
memory: {
enabled: true
scope: { read: ["session", "agent"], write: ["session", "agent"] }
tools: ["add_memory", "read_memory", "delete_memory"]
pinned: { max_tokens: 500 }
}
history: { index: "messages" }
}
model: {
provider: "fixture"
script: [
{
when: "purpose == 'turn' && session.step == 0 && session.input.name == 'agent-memory-north'"
responses: [
[
{
toolUse: {
name: "add_memory"
input: { summary: "This journal pays out on Fridays.", kind: "fact" }
}
}
]
[
{
toolUse: {
name: "add_memory"
input: {
summary: "Report payout amounts in USD with two decimals."
kind: "guidance"
scope: "agent"
pinned: true
}
}
}
]
[{ text: "Saved what I learned about this journal." }]
]
}
{
when: "purpose == 'turn' && session.step > 0"
responses: [
[
{
toolUse: {
name: "search_memory"
input: { query: "When does this journal pay out?" }
}
}
]
[{ toolUse: { name: "search_history", input: { query: "weekly payouts" } } }]
[{ text: "Checked my notes and this journal's history." }]
]
}
{
when: "purpose == 'turn'"
responses: [
[
{
toolUse: {
name: "search_memory"
input: { query: "When does this journal pay out?" }
}
}
]
[{ text: "Checked my notes." }]
]
}
{
when: "purpose == 'memory' && session.step == 0 && session.input.name == 'agent-memory-north'"
responses: [
[
{
text: "[{\"summary\": \"The north journal was opened for weekly payouts.\", \"kind\": \"event\"}]"
}
]
[{ text: "[{\"candidate\": 1, \"action\": \"add\"}]" }]
]
}
{ when: "purpose == 'memory'", responses: [[{ text: "[]" }]] }
{
when: "purpose == 'outcome'"
responses: [{ outcome: { reply: "Done.", session: { status: "OPEN" } } }]
}
]
}
}
}
) {
actionId
version
status
pending
}
}
}
The response reports status: UNPUBLISHED and pending: ACTIVE. What the definition sets up:
- Sessions. Inserting a journal whose name starts with
agent-memory-starts a session for that journal, and updating it runs another turn of the same session. harness.memory.enabledturns memory on.scopelets the agent read and write both scopes:sessionrecords stay with the journal that wrote them, andagentrecords reach every journal's session.toolsgives the agentadd_memory,read_memory, anddelete_memorybesidessearch_memory, andpinnedlets it pin records within a 500-token budget.harness.history.index: "messages"keeps each turn's message and the agent's replies searchable withsearch_history.- The script. Each model call takes the next response from the first entry whose
whenmatches. The journal name is part of the script: the first turn of the session foragent-memory-northsaves two memories, any later turn searches memory and history, and any other first turn only searches memory.
Entry when | What it plays |
|---|---|
purpose == 'turn', north, first turn | add_memory of a session fact, add_memory of a pinned agent rule, then a reply. |
purpose == 'turn' && session.step > 0 | search_memory, search_history, then a reply. |
purpose == 'turn' | search_memory, then a reply. |
purpose == 'memory', north, first turn | The review after the turn: one candidate memory, then the decision to add it. |
purpose == 'memory' | The review after any other turn: nothing worth remembering. |
purpose == 'outcome' | The turn outcome: reply Done. and keep the session open. |
The memory entries stand in for the model calls that review each turn. The first call returns candidate memories as a JSON array. When a candidate resembles memories already kept, a second call decides whether to add it, replace one, delete one, or skip it.
Poll until the agent is live:
query ReadMemoryAgent {
actions {
action(id: "2afe6c28-d18d-4f3e-801d-352740eb8925") {
version
status
pending
}
}
}
{ "version": 2, "status": "ACTIVE", "pending": null }
2. Start the first session
Create the north journal. Its insert starts a session whose ID is the journal ID, and the first turn runs at once.
mutation OpenNorthJournal {
createJournal(
input: {
journalId: "c131e037-1afb-4f10-ba38-ee4956458f79"
name: "agent-memory-north"
description: "Weekly payouts"
}
) {
journalId
version
}
}
Read the session's turns:
query NorthSession {
actions {
action(id: "2afe6c28-d18d-4f3e-801d-352740eb8925") {
session(id: "c131e037-1afb-4f10-ba38-ee4956458f79") {
status
turns(first: 10) {
nodes {
number
status
executionId
execution {
status
invocation {
output
}
}
}
}
}
}
}
}
Within a few seconds, turn 1 is COMMITTED. Keep its executionId for the next step:
{
"status": "OPEN",
"turns": {
"nodes": [
{
"number": 1,
"status": "COMMITTED",
"executionId": "12a4007b-ac15-5163-ae66-7184f23adfc4",
"execution": { "status": "SUCCEEDED", "invocation": { "output": "Done." } }
}
]
}
}
3. Read what the turn saved
The turn's transcript records every model call and tool call:
query TurnTranscript($id: UUID!) {
actions {
execution(id: $id) {
invocation {
transcript(first: 100) {
nodes {
sequence
kind
data
}
}
}
}
}
}
Run it with {"id": "12a4007b-ac15-5163-ae66-7184f23adfc4"}. The transcript is long; look for these events:
Two
tool.outputevents namedadd_memory. Each returns the record it stored, with its ID and scope. The first is asessionrecord; the second, which the script gavescope: "agent"andpinned: true, is anagentrecord:{ "name": "add_memory", "output": { "content": [ { "text": "{\"stored\": {\"id\": \"ede55d89-f542-590f-8f6d-ddcf25947ce6\", \"summary\": \"Report payout amounts in USD with two decimals.\", \"kind\": \"guidance\", \"scope\": \"agent\", \"pinned\": true, \"created\": \"2026-10-11T17:54:25.140602Z\"}}" } ], "memories": [ { "memory_id": "ede55d89-f542-590f-8f6d-ddcf25947ce6", "summary": "Report payout amounts in USD with two decimals.", "content": "" } ], "status": "success" }, ... }After the outcome, two
model.responseevents with"purpose": "memory": the review of the turn. The first proposes a candidate, and the second, because the candidate resembles the records the turn just saved, decides to add it:{ "purpose": "memory", "content": [{ "text": "[{\"summary\": \"The north journal was opened for weekly payouts.\", \"kind\": \"event\"}]" }], ... } { "purpose": "memory", "content": [{ "text": "[{\"candidate\": 1, \"action\": \"add\"}]" }], ... }usageevents with"purpose": "embed": each memory search, including the review's comparison with existing memories, embeds its query so records can be found by meaning. No limit counts them.
4. List the agent's memory
Action.memories lists the agent's records, newest first. A record appears shortly after the turn that wrote it commits.
query AgentMemories {
actions {
action(id: "2afe6c28-d18d-4f3e-801d-352740eb8925") {
memories(first: 20) {
nodes {
memoryId
summary
kind
scope
source
pinned
status
sessionId
}
}
}
}
}
{
"nodes": [
{
"memoryId": "3c41d8a2-8d97-5263-b7a0-ff103bb6cf17",
"summary": "The north journal was opened for weekly payouts.",
"kind": "event",
"scope": "SESSION",
"source": "EXTRACTION",
"pinned": false,
"status": "ACTIVE",
"sessionId": "c131e037-1afb-4f10-ba38-ee4956458f79"
},
{
"memoryId": "ede55d89-f542-590f-8f6d-ddcf25947ce6",
"summary": "Report payout amounts in USD with two decimals.",
"kind": "guidance",
"scope": "AGENT",
"source": "TOOL",
"pinned": true,
"status": "ACTIVE",
"sessionId": "c131e037-1afb-4f10-ba38-ee4956458f79"
},
{
"memoryId": "a5476143-b73e-5d8c-9135-4b8420b120cd",
"summary": "This journal pays out on Fridays.",
"kind": "fact",
"scope": "SESSION",
"source": "TOOL",
"pinned": false,
"status": "ACTIVE",
"sessionId": "c131e037-1afb-4f10-ba38-ee4956458f79"
}
]
}
source says what wrote each record: TOOL for the agent's own add_memory calls, and EXTRACTION for the review after the turn. The review wrote session scope because its candidate did not ask for agent scope. Every record names the session that wrote it, whatever its scope.
5. Start a second session
Create the south journal. Its insert starts a second session, whose first turn only searches memory.
mutation OpenSouthJournal {
createJournal(
input: {
journalId: "3588d9bf-bdb4-4ad4-8c27-5759f80c4e8b"
name: "agent-memory-south"
description: "Weekly payouts"
}
) {
journalId
version
}
}
Read the session with NorthSession, replacing the session ID with 3588d9bf-bdb4-4ad4-8c27-5759f80c4e8b, and run TurnTranscript on its turn 1 execution. Three events show what crossed over:
The first event is
memory.pinned: the pinned rule, listed in this turn's instructions though another session wrote it.{ "memories": [{ "memory_id": "ede55d89-f542-590f-8f6d-ddcf25947ce6", "memory_version": 1 }] }A
tool.inputandtool.outputpair namedsearch_memorywith"recall": true: before its first model call, the agent recalled the records that best match the turn. It found only the agent-scope rule.The agent's own
search_memorycall, forWhen does this journal pay out?, also returns only the rule:{ "name": "search_memory", "output": { "content": [ { "text": "{\"memories\": [{\"id\": \"ede55d89-f542-590f-8f6d-ddcf25947ce6\", \"summary\": \"Report payout amounts in USD with two decimals.\", \"kind\": \"guidance\", \"scope\": \"agent\", \"pinned\": true, \"score\": 1.0, \"created\": \"2026-10-11T17:54:25.140602Z\"}]}" } ], ... } }
The north journal's Friday payouts are SESSION records, so the south session never sees them. Only what the agent saved with agent scope informs every session.
6. Recall and search in a later turn
Update the north journal. The update resumes the north session and runs its turn 2.
mutation UpdateNorthJournal {
updateJournal(
id: "c131e037-1afb-4f10-ba38-ee4956458f79"
input: { description: "Weekly payouts, reviewed" }
) {
journalId
version
}
}
Run NorthSession again for turn 2's executionId, then TurnTranscript on it:
memory.pinnedlists the rule again, and the recall before the first model call finds all three records.The
search_memorycall returns the north session's records and the rule, ranked by how well each matches the query:{ "memories": [ { "id": "a5476143-b73e-5d8c-9135-4b8420b120cd", "summary": "This journal pays out on Fridays.", "kind": "fact", "scope": "session", "score": 1.0, ... }, { "id": "3c41d8a2-8d97-5263-b7a0-ff103bb6cf17", "summary": "The north journal was opened for weekly payouts.", "kind": "event", "scope": "session", "score": 0.9934, ... }, { "id": "ede55d89-f542-590f-8f6d-ddcf25947ce6", "summary": "Report payout amounts in USD with two decimals.", "kind": "guidance", "scope": "agent", "pinned": true, "score": 0.5878, ... } ] }The
search_historycall searches the session's earlier turns. Each result names the turn and who it is from, and its text is what that turn said. Turn 1's message, which carried the journal'sWeekly payoutsdescription, comes first; the agent's replies from turn 1 follow, each with the tools it called:{ "events": [ { "id": "b17eac4c-b93f-53a9-b47a-22d9449ae721", "turn": 1, "text": "turn 1, user\n<turn session=\"c131e037-1afb-4f10-ba38-ee4956458f79\" step=\"0\" ...", "score": 1.0, "execution_id": "12a4007b-ac15-5163-ae66-7184f23adfc4", "sequence": 2 }, { "id": "bc6a3704-1609-5148-a4ef-cbfbad68aabf", "turn": 1, "text": "turn 1, assistant\n[called add_memory: {\"kind\":\"guidance\",\"pinned\":true,\"scope\":\"agent\",\"summary\":\"Report payout amounts in USD with two decimals.\"}]", ... }, ... ] }
Memory holds what the agent chose to keep, across sessions when its scope allows. History holds what one session's turns said, and only that session searches it.
7. Inspect a record
agentMemory reads one record by its agent and ID, including where it came from. Use the memoryId of the Friday fact from your step 4 response:
query OneMemory {
actions {
agentMemory(
actionId: "2afe6c28-d18d-4f3e-801d-352740eb8925"
id: "a5476143-b73e-5d8c-9135-4b8420b120cd"
) {
summary
status
scope
source
version
sessionId
executionId
actionVersion
modifiedBy
execution {
status
}
}
}
}
{
"summary": "This journal pays out on Fridays.",
"status": "ACTIVE",
"scope": "SESSION",
"source": "TOOL",
"version": 1,
"sessionId": "c131e037-1afb-4f10-ba38-ee4956458f79",
"executionId": "12a4007b-ac15-5163-ae66-7184f23adfc4",
"actionVersion": 2,
"modifiedBy": "arn:twisp:database:::action/2afe6c28-d18d-4f3e-801d-352740eb8925",
"execution": { "status": "SUCCEEDED" }
}
executionId is turn 1 of the north session, and modifiedBy is the agent's own principal: the agent wrote it.
8. Delete a record
Suppose the north journal moved its payouts. Delete the stale fact:
mutation ForgetFridays {
actions {
deleteAgentMemory(
actionId: "2afe6c28-d18d-4f3e-801d-352740eb8925"
id: "a5476143-b73e-5d8c-9135-4b8420b120cd"
) {
summary
status
version
modifiedBy
}
}
}
{
"summary": "This journal pays out on Fridays.",
"status": "DELETED",
"version": 2,
"modifiedBy": "arn:aws:iam::000000000000:user/twisp"
}
Run AgentMemories again: the Friday fact is gone, and the other two records remain. The deleted record is still readable, with every version:
query MemoryHistory {
actions {
agentMemory(
actionId: "2afe6c28-d18d-4f3e-801d-352740eb8925"
id: "a5476143-b73e-5d8c-9135-4b8420b120cd"
) {
status
history(first: 10) {
nodes {
version
status
modifiedBy
modified
}
}
}
}
}
{
"status": "DELETED",
"history": {
"nodes": [
{
"version": 2,
"status": "DELETED",
"modifiedBy": "arn:aws:iam::000000000000:user/twisp",
"modified": "2026-10-11T17:55:38.360631Z"
},
{
"version": 1,
"status": "ACTIVE",
"modifiedBy": "arn:twisp:database:::action/2afe6c28-d18d-4f3e-801d-352740eb8925",
"modified": "2026-10-11T17:54:25.103494Z"
}
]
}
}
Running ForgetFridays again returns the record unchanged.
9. Confirm the agent forgot
Update the north journal once more to run turn 3:
mutation UpdateNorthJournalAgain {
updateJournal(
id: "c131e037-1afb-4f10-ba38-ee4956458f79"
input: { description: "Weekly payouts, reviewed again" }
) {
journalId
version
}
}
In turn 3's transcript, the same search_memory call now returns only the two remaining records:
{
"memories": [
{ "id": "3c41d8a2-8d97-5263-b7a0-ff103bb6cf17", "summary": "The north journal was opened for weekly payouts.", "kind": "event", "scope": "session", "score": 1.0, ... },
{ "id": "ede55d89-f542-590f-8f6d-ddcf25947ce6", "summary": "Report payout amounts in USD with two decimals.", "kind": "guidance", "scope": "agent", "pinned": true, "score": 0.5903, ... }
]
}
10. Optional: extract with a Claude model
The script stood in for the review after each turn. With a real model, the agent decides for itself what is worth remembering. harness.memory.model names the model that reviews; the agent's own turns can stay on the fixture. This step calls a Claude model through the anthropic provider, so your local environment needs an Anthropic API key.
This agent has no session, so it reads and writes agent scope only. It runs once for a journal named agent-memory-west, and its scripted reply restates the journal's description:
mutation CreateExtractingAgent {
actions {
createAction(
id: "a04b1b0e-2a3e-4d7c-9a4f-5a3c1d2e7b61"
input: {
name: "agent-memory-extractor"
kind: "agent"
agent: { prompt: "You keep notes about payout journals." }
triggers: [
{ event: "event.type == 'journal.inserted' && event.data.name == 'agent-memory-west'" }
]
config: {
runtime: "strands"
harness: {
memory: {
enabled: true
scope: { read: ["agent"], write: ["agent"] }
model: { provider: "anthropic", id: "claude-haiku-5-5" }
}
}
model: {
provider: "fixture"
script: [
{
when: "purpose == 'turn'"
responses: [
[
{
text: "Noted: this journal settles payouts on the 1st and 15th, and disputes go to the treasury desk."
}
]
]
}
]
}
}
}
) {
actionId
status
pending
}
}
}
Poll ReadMemoryAgent with this agent's ID until it is ACTIVE, then create the journal:
mutation OpenWestJournal {
createJournal(
input: {
journalId: "6a0f6f2e-0d7e-4c4b-8a59-2f3c2b9e4d10"
name: "agent-memory-west"
description: "Settles payouts on the 1st and 15th. Disputes go to the treasury desk."
}
) {
journalId
}
}
After the run, list its memory:
query ExtractedMemories {
actions {
action(id: "a04b1b0e-2a3e-4d7c-9a4f-5a3c1d2e7b61") {
memories(first: 10) {
nodes {
summary
content
kind
scope
source
}
}
}
}
}
The model's wording varies from run to run. One run kept a single fact, with a summary to act on and the evidence in content:
{
"nodes": [
{
"summary": "Journal 6a0f6f2e-0d7e-4c4b-8a59-2f3c2b9e4d10 (name agent-memory-west) settles payouts on the 1st and 15th of each month; disputes go to the treasury desk.",
"content": "Recorded from a journal.inserted event at 2026-10-11T17:56:30Z. The journal description reads: \"Settles payouts on the 1st and 15th. Disputes go to the treasury desk.\"",
"kind": "fact",
"scope": "AGENT",
"source": "EXTRACTION"
}
]
}
In the run's transcript, the model.request with "purpose": "memory" names claude-haiku-5-5, and its usage event counts real tokens, while the turn's own calls name the fixture provider.
11. Clean up
Delete the agents so later journals named agent-memory-* start no sessions:
mutation DeleteMemoryAgents {
actions {
notes: deleteAction(id: "2afe6c28-d18d-4f3e-801d-352740eb8925") {
status
pending
}
extractor: deleteAction(id: "a04b1b0e-2a3e-4d7c-9a4f-5a3c1d2e7b61") {
status
pending
}
}
}
Each reports pending: DELETED. Deleting an agent is a soft delete: its sessions, executions, and memory records stay readable.
Troubleshooting
| Symptom | Check |
|---|---|
| No session or turn appears | The agent must be ACTIVE before the journal is created. Create a journal with a fresh ID and the same name. |
createJournal fails with a unique constraint error | The journal ID exists from an earlier run. Choose a fresh ID; keep the name, which the script matches. |
| Turn 1 of the north session saves nothing | The journal's name must be exactly agent-memory-north: the script's when matches it. |
memories is empty right after a turn | A record appears shortly after its turn commits. Run the query again. |
| An earlier run's agent also runs on your journals | It is still active and matches the same journal names. Delete it as in step 11. Its memory is its own: the new agent never sees it. |
A turn fails with fixture_exhausted | A model call found no response left in its entry. Each entry's position restarts every turn; check that the memory and outcome entries are present. |
| The optional step's run fails with a model error | The anthropic provider needs an Anthropic API key configured in the local environment. The rest of the tutorial calls no model. |
Read Memory for how to choose scopes, extraction, and pins, and the memory reference for every setting, tool, and record field. Session history covers harness.history, and Sessions walks through session turns in depth.