Agents Workshop

Persistent Memory

Our agent can chat, search the web, and run commands in a sandbox -- but it forgets everything between conversations. Let's give it persistent memory by reading and writing a memories.md file stored in the sandbox's filesystem.

Make prepareCall async so it can read from the sandbox filesystem before each LLM call. We'll read memories.md from the sandbox and inject its contents into the agent's instructions.

We'll also upgrade the model to gpt-5.4 since the memory-augmented agent benefits from a more capable model.

lib/agent.ts
import { openai } from "@ai-sdk/openai";
import { type InferAgentUIMessage, ToolLoopAgent } from "ai";
import { bash } from "./tools";
import { Sandbox } from "@vercel/sandbox";
import { z } from "zod";

const callOptionsSchema = z.object({
  sandbox: z.instanceof(Sandbox),
});

export const agentContextSchema = z.object({
  sandbox: z.instanceof(Sandbox),
});

export type AgentContext = z.infer<typeof agentContextSchema>;

export const myAgent = new ToolLoopAgent({
  model: "openai/gpt-5.4", 
  tools: {
    webSearch: openai.tools.webSearch(),
    bash,
  },
  callOptionsSchema, 
  prepareCall: async ({ options, ...rest }) => { 
    const buffer = await options.sandbox.readFileToBuffer({ 
      path: "memories.md", 
    }); 
    const memories = buffer ? new TextDecoder().decode(buffer) : null; 

    return {
      ...rest,
      experimental_context: agentContextSchema.parse({
        sandbox: options.sandbox,
      }),
      instructions: [ 
        "You are a coding agent with access to a computer via the bash tool.", 
        "You have a memories.md file that you can read and write to.", 
        "You should always add any facts the user shares to memories.md.", 
        memories 
          ? `Here are your current memories:\n\n${memories}`
          : "No memories yet.", 
      ].join("\n"), 
    };
  },
});

export type MyAgentUIMessage = InferAgentUIMessage<typeof myAgent>;

The key idea: prepareCall runs before every LLM call, so the agent always has its latest memories available. The agent can write to memories.md using the bash tool (e.g., echo "User prefers TypeScript" >> memories.md), and those changes will be picked up on the next call.

Let's expand the instructions to create a more sophisticated memory system. Instead of passively storing facts, the agent will proactively ask questions to learn about the user and build up structured memories over time.

lib/agent.ts
import { openai } from "@ai-sdk/openai";
import { type InferAgentUIMessage, ToolLoopAgent } from "ai";
import { bash } from "./tools";
import { Sandbox } from "@vercel/sandbox";
import { z } from "zod";

const callOptionsSchema = z.object({
  sandbox: z.instanceof(Sandbox),
});

export const agentContextSchema = z.object({
  sandbox: z.instanceof(Sandbox),
});

export type AgentContext = z.infer<typeof agentContextSchema>;

export const myAgent = new ToolLoopAgent({
  model: "openai/gpt-5.4",
  tools: {
    webSearch: openai.tools.webSearch(),
    bash,
  },
  callOptionsSchema,
  prepareCall: async ({ options, ...rest }) => {
    const buffer = await options.sandbox.readFileToBuffer({
      path: "memories.md",
    });
    const memories = buffer ? new TextDecoder().decode(buffer) : null;

    return {
      ...rest,
      experimental_context: agentContextSchema.parse({
        sandbox: options.sandbox,
      }),
      instructions: [
        "You are a coding agent with access to a computer via the bash tool.",
        "You have a memories.md file that you can read and write to for persisting important information across conversations.", 
        "",
        memories 
          ? `## Current memories\n\n${memories}`
          : "No memories yet. Start by asking the user their name!", 
        "", 
        "## When to save to memories.md", 
        "Save information that would be useful in future conversations, such as:", 
        "- User preferences (coding style, tools, frameworks they prefer)", 
        "- Project context (goals, constraints, architecture decisions)", 
        "- Important facts the user shares about themselves or their work", 
        "- Corrections or feedback the user gives you about your behavior", 
        "", 
        "Do NOT save trivial interactions like greetings, small talk, or information that can be derived from the codebase itself.", 
        "", 
        "## Getting to know the user", 
        "You should proactively ask the user questions to learn about them and build up your memories.", 
        "Ask one question at a time. Always save useful answers to memories.md.", 
        "Even when the user sends a simple greeting, respond warmly AND ask a question.", 
        "", 
        "Start with the basics and progress to more specific topics over time:", 
        "1. First, learn their name", 
        "2. Then, what they do / their role", 
        "3. Then, what they're working on", 
        "4. Then, their experience level, preferred tools, coding style, etc.", 
        "", 
        "Check your current memories to see what you already know and ask about something you don't know yet.", 
      ].join("\n"),
    };
  },
});

export type MyAgentUIMessage = InferAgentUIMessage<typeof myAgent>;

Try greeting the agent -- it should now introduce itself and ask your name. Over multiple conversations, it will build up a profile of you in memories.md.

Finally, let's make the agent self-improving by teaching it to write reusable scripts. When the agent completes a task, it will consider whether the task could be automated with a script. On future requests, it checks its saved scripts before doing manual work.

lib/agent.ts
import { openai } from "@ai-sdk/openai";
import { type InferAgentUIMessage, ToolLoopAgent } from "ai";
import { bash } from "./tools";
import { Sandbox } from "@vercel/sandbox";
import { z } from "zod";

const callOptionsSchema = z.object({
  sandbox: z.instanceof(Sandbox),
});

export const agentContextSchema = z.object({
  sandbox: z.instanceof(Sandbox),
});

export type AgentContext = z.infer<typeof agentContextSchema>;

export const myAgent = new ToolLoopAgent({
  model: "openai/gpt-5.4",
  tools: {
    webSearch: openai.tools.webSearch(),
    bash,
  },
  callOptionsSchema,
  prepareCall: async ({ options, ...rest }) => {
    const buffer = await options.sandbox.readFileToBuffer({
      path: "memories.md",
    });
    const memories = buffer ? new TextDecoder().decode(buffer) : null;

    return {
      ...rest,
      experimental_context: agentContextSchema.parse({
        sandbox: options.sandbox,
      }),
      instructions: [
        "You are a coding agent with access to a computer via the bash tool.",
        "You have a memories.md file that you can read and write to for persisting important information across conversations.",
        "",
        memories
          ? `## Current memories\n\n${memories}`
          : "No memories yet. Start by asking the user their name!",
        "",
        "## CRITICAL: Before doing ANY task, follow this order", 
        "1. Check if a script in your 'Scripts for common tasks' memories already handles the request. If yes, run it with bash immediately. Do NOT use web search.", 
        "2. Only if no script exists, proceed to do the task (using web search, bash, etc.).", 
        "3. After completing a task, consider writing a reusable Python script for it.", 
        "",
        "## When to save to memories.md",
        "Save information that would be useful in future conversations, such as:",
        "- User preferences (coding style, tools, frameworks they prefer)",
        "- Project context (goals, constraints, architecture decisions)",
        "- Important facts the user shares about themselves or their work",
        "- Corrections or feedback the user gives you about your behavior",
        "",
        "Do NOT save trivial interactions like greetings, small talk, or information that can be derived from the codebase itself.",
        "",
        "## Getting to know the user",
        "You should proactively ask the user questions to learn about them and build up your memories.",
        "Ask one question at a time. Always save useful answers to memories.md.",
        "Even when the user sends a simple greeting, respond warmly AND ask a question.",
        "",
        "Start with the basics and progress to more specific topics over time:",
        "1. First, learn their name",
        "2. Then, what they do / their role",
        "3. Then, what they're working on",
        "4. Then, their experience level, preferred tools, coding style, etc.",
        "",
        "Check your current memories to see what you already know and ask about something you don't know yet.",
        "",
        "## Repeatable scripts", 
        "If no existing script handles the task, think about whether it could be written as a repeatable Python script.", 
        "If so, write the script to the filesystem (e.g. scripts/task_name.py) so the user can re-run it later without needing to ask you again.", 
        "Make scripts self-contained, well-commented, and runnable with `python3 scripts/task_name.py`. Always use `python3`, never `python`.", 
        "After creating a script, log it in memories.md under a '## Scripts for common tasks' section with the filename and a short description of what it does.", 
      ].join("\n"),
    };
  },
});

export type MyAgentUIMessage = InferAgentUIMessage<typeof myAgent>;

The agent now follows a task priority order: check for existing scripts first, do the work if needed, then save a reusable script for next time. Over repeated interactions, the agent accumulates a library of scripts that make it faster and more reliable.

You've built an AI agent that can search the web, run commands in a cloud sandbox, remember context across conversations, and improve itself over time by building reusable scripts.