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. enabled turns memory on. scope lets the agent read and write both scopes: session records stay with the journal that wrote them, and agent records reach every journal's session. tools gives the agent add_memory, read_memory, and delete_memory besides search_memory, and pinned lets it pin records within a 500-token budget.
  • harness.history. index: "messages" keeps each turn's message and the agent's replies searchable with search_history.
  • The script. Each model call takes the next response from the first entry whose when matches. The journal name is part of the script: the first turn of the session for agent-memory-north saves two memories, any later turn searches memory and history, and any other first turn only searches memory.
Entry whenWhat it plays
purpose == 'turn', north, first turnadd_memory of a session fact, add_memory of a pinned agent rule, then a reply.
purpose == 'turn' && session.step > 0search_memory, search_history, then a reply.
purpose == 'turn'search_memory, then a reply.
purpose == 'memory', north, first turnThe 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.output events named add_memory. Each returns the record it stored, with its ID and scope. The first is a session record; the second, which the script gave scope: "agent" and pinned: true, is an agent record:

    {
      "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.response events 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\"}]" }], ... }
    
  • usage events 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.input and tool.output pair named search_memory with "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_memory call, for When 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.pinned lists the rule again, and the recall before the first model call finds all three records.

  • The search_memory call 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_history call 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's Weekly payouts description, 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

SymptomCheck
No session or turn appearsThe 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 errorThe 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 nothingThe journal's name must be exactly agent-memory-north: the script's when matches it.
memories is empty right after a turnA record appears shortly after its turn commits. Run the query again.
An earlier run's agent also runs on your journalsIt 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_exhaustedA 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 errorThe 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.

Previous
A Long-Running Agent with Sessions