Daydreams Logo
Concepts

Contexts

Managing state, memory, and behavior for agent interactions.

What is a Context?

A context is like a separate workspace for your agent. Think of it like having different tabs open in your browser - each tab has its own state and remembers different things.

Real Examples

Here are contexts that make agents stateful:

Chat Context

chat-context.ts
// Each user gets their own chat workspace
const chatContext = context({
  type: "chat",
  schema: z.object({
    userId: z.string(),
  }),
  create: () => ({
    messages: [],
    userPreferences: {},
    lastSeen: null,
  }),
  render: (state) => `
    Chat with ${state.args.userId}
    Recent messages: ${state.memory.messages.slice(-3).join("\n")}
  `,
});

Game Context

game-context.ts
// Each game session has its own state
const gameContext = context({
  type: "game",
  schema: z.object({
    gameId: z.string(),
  }),
  create: () => ({
    playerHealth: 100,
    level: 1,
    inventory: [],
    currentRoom: "start",
  }),
  render: (state) => `
    Game: ${state.args.gameId}
    Health: ${state.memory.playerHealth}
    Level: ${state.memory.level}
    Room: ${state.memory.currentRoom}
  `,
});

Project Context

project-context.ts
// Each project tracks its own progress
const projectContext = context({
  type: "project",
  schema: z.object({
    projectId: z.string(),
  }),
  create: () => ({
    tasks: [],
    status: "planning",
    teamMembers: [],
    deadlines: [],
  }),
  render: (state) => `
    Project: ${state.args.projectId}
    Status: ${state.memory.status}
    Tasks: ${state.memory.tasks.length} total
  `,
});

The Problem: Agents Need to Remember Different Things

Without contexts, your agent mixes everything together:

confused-agent.txt
User Alice: "My favorite color is blue"
User Bob: "What's Alice's favorite color?"
Agent: "Alice's favorite color is blue"
// ❌ Bob shouldn't see Alice's private info!

User in Game A: "Go north"
User in Game B: "What room am I in?"
Agent: "You went north" (from Game A!)
// ❌ Wrong game state mixed up!

Project Alpha discussion mixed with Project Beta tasks
// ❌ Complete chaos!

The Solution: Contexts Separate Everything

With contexts, each conversation/session/game stays separate:

organized-agent.txt
Alice's Chat Context:
- Alice: "My favorite color is blue"
- Agent remembers: Alice likes blue

Bob's Chat Context:
- Bob: "What's Alice's favorite color?"
- Agent: "I don't have information about Alice"
// ✅ Privacy maintained!

Game A Context:
- Player went north → remembers current room

Game B Context:
- Separate game state → different room
// ✅ No mixing of game states!

How Contexts Work in Your Agent

1. You Define Different Context Types

define-contexts.ts
const agent = createDreams({
  model: openai("gpt-4o"),
  contexts: [
    chatContext, // For user conversations
    gameContext, // For game sessions
    projectContext, // For project management
  ],
});

2. Inputs Route to Specific Context Instances

context-routing.ts
// Discord input routes to chat contexts
discordInput.subscribe((send, agent) => {
  discord.on("message", (msg) => {
    // Each user gets their own chat context instance
    send(
      chatContext,
      { userId: msg.author.id },
      {
        content: msg.content,
      }
    );
  });
});
 
// Game input routes to game contexts
gameInput.subscribe((send, agent) => {
  gameServer.on("move", (event) => {
    // Each game gets its own context instance
    send(
      gameContext,
      { gameId: event.gameId },
      {
        action: event.action,
      }
    );
  });
});

3. Agent Maintains Separate Memory

context-instances.txt
Chat Context Instances:
- chat:alice → { messages: [...], preferences: {...} }
- chat:bob   → { messages: [...], preferences: {...} }
- chat:carol → { messages: [...], preferences: {...} }

Game Context Instances:
- game:session1 → { health: 80, level: 3, room: "forest" }
- game:session2 → { health: 100, level: 1, room: "start" }
- game:session3 → { health: 45, level: 7, room: "dungeon" }

All completely separate!

Creating Your First Context

Here's a simple todo list context:

todo-context.ts
import { context } from "@daydreamsai/core";
import { z } from "zod";
 
// Define what this context remembers
interface TodoMemory {
  tasks: { id: string; title: string; done: boolean }[];
  createdAt: string;
}
 
export const todoContext = context<TodoMemory>({
  // Type identifies this kind of context
  type: "todo",
 
  // Schema defines how to identify specific instances
  schema: z.object({
    listId: z.string().describe("Unique ID for this todo list"),
  }),
 
  // Create initial memory when first accessed
  create: () => ({
    tasks: [],
    createdAt: new Date().toISOString(),
  }),
 
  // How this context appears to the LLM
  render: (state) => {
    const { tasks } = state.memory;
    const pending = tasks.filter((t) => !t.done).length;
    const completed = tasks.filter((t) => t.done).length;
 
    return `
Todo List: ${state.args.listId}
Tasks: ${pending} pending, ${completed} completed
 
Recent tasks:
${tasks
  .slice(-5)
  .map((t) => `${t.done ? "✅" : "⏳"} ${t.title}`)
  .join("\n")}
    `;
  },
 
  // Instructions for the LLM when this context is active
  instructions:
    "Help the user manage their todo list. You can add, complete, and list tasks.",
});

Use it in your agent:

agent-with-todo.ts
const agent = createDreams({
  model: openai("gpt-4o"),
  contexts: [todoContext],
});
 
// Now users can have separate todo lists:
// todo:work → Work tasks
// todo:personal → Personal tasks
// todo:shopping → Shopping list
// Each maintains separate state!

Context Memory: What Gets Remembered

Context memory persists between conversations:

memory-example.ts
// First conversation
User: "Add 'buy milk' to my shopping list"
Agent: → todoContext(listId: "shopping")
       → memory.tasks.push({id: "1", title: "buy milk", done: false})
"Added 'buy milk' to your shopping list"
 
// Later conversation (hours/days later)
User: "What's on my shopping list?"
Agent: → todoContext(listId: "shopping")
       → Loads saved memory: {tasks: [{title: "buy milk", done: false}]}
"You have 'buy milk' on your shopping list"
 
// ✅ Context remembered the task across conversations!

Multiple Contexts in One Agent

Your agent can switch between different contexts:

context-switching.xml
<!-- User starts in chat context -->
<input type="discord:message" userId="alice">
  "Add 'finish project' to my work todo list"
</input>
 
<!-- Agent recognizes this needs todo context -->
<response>
  <reasoning>User wants to add a task to their work todo list. I should use the todo context.</reasoning>
 
  <!-- Switch to todo context -->
  <action_call name="add-task" context="todo" args='{"listId": "work"}'>
    {"title": "finish project"}
  </action_call>
 
  <!-- Respond back in chat context -->
  <output type="discord:message" channelId="123">
    Added "finish project" to your work todo list!
  </output>
</response>

Advanced: Context-Specific Actions

You can attach actions that only work in certain contexts:

context-specific-actions.ts
const todoContextWithActions = todoContext.setActions([
  action({
    name: "add-task",
    description: "Adds a new task to the todo list",
    schema: z.object({
      title: z.string(),
    }),
    handler: async ({ title }, ctx) => {
      // ctx.memory is automatically typed as TodoMemory!
      const newTask = {
        id: crypto.randomUUID(),
        title,
        done: false,
      };
 
      ctx.memory.tasks.push(newTask);
 
      return {
        success: true,
        taskId: newTask.id,
        message: `Added "${title}" to the list`,
      };
    },
  }),
 
  action({
    name: "complete-task",
    description: "Marks a task as completed",
    schema: z.object({
      taskId: z.string(),
    }),
    handler: async ({ taskId }, ctx) => {
      const task = ctx.memory.tasks.find((t) => t.id === taskId);
      if (!task) {
        return { success: false, message: "Task not found" };
      }
 
      task.done = true;
 
      return {
        success: true,
        message: `Completed "${task.title}"`,
      };
    },
  }),
]);

Now these actions only appear when the todo context is active!

Context Lifecycle

Contexts have hooks for different stages:

context-lifecycle.ts
const advancedContext = context({
  type: "advanced",
  schema: z.object({ sessionId: z.string() }),
 
  // Called when context instance is first created
  create: (state, agent) => {
    agent.logger.info(`Creating new session: ${state.key}`);
    return {
      startTime: Date.now(),
      interactions: 0,
    };
  },
 
  // Called before each LLM interaction
  onStep: async (ctx, agent) => {
    ctx.memory.interactions++;
  },
 
  // Called when a conversation/run completes
  onRun: async (ctx, agent) => {
    const duration = Date.now() - ctx.memory.startTime;
    agent.logger.info(`Session ${ctx.key} lasted ${duration}ms`);
  },
 
  // Called if there's an error
  onError: async (error, ctx, agent) => {
    agent.logger.error(`Error in session ${ctx.key}:`, error);
  },
});

Best Practices

1. Design Clear Boundaries

good-context-design.ts
// ✅ Good - clear, specific purpose
const userProfileContext = context({
  type: "user-profile",
  schema: z.object({ userId: z.string() }),
  // Manages user preferences, settings, history
});
 
const orderContext = context({
  type: "order",
  schema: z.object({ orderId: z.string() }),
  // Manages specific order state, items, shipping
});
 
// ❌ Bad - too broad, unclear purpose
const stuffContext = context({
  type: "stuff",
  schema: z.object({ id: z.string() }),
  // What does this manage? Everything? Nothing clear.
});

2. Keep Memory Structures Simple

good-memory-structure.ts
// ✅ Good - clear, simple structure
interface ChatMemory {
  messages: Array<{
    sender: "user" | "agent";
    content: string;
    timestamp: number;
  }>;
  userPreferences: {
    language?: string;
    timezone?: string;
  };
}
 
// ❌ Bad - overly complex, nested
interface OverComplexMemory {
  data: {
    nested: {
      deeply: {
        structured: {
          confusing: {
            memory: any;
          };
        };
      };
    };
  };
}

3. Write Helpful Render Functions

good-render-function.ts
// ✅ Good - concise, relevant information
render: (state) => `
  Shopping Cart: ${state.args.cartId}
  Items: ${state.memory.items.length}
  Total: $${state.memory.total.toFixed(2)}
  
  Recent items:
  ${state.memory.items
    .slice(-3)
    .map((item) => `- ${item.name} ($${item.price})`)
    .join("\n")}
`;
 
// ❌ Bad - too much information, overwhelming
render: (state) => JSON.stringify(state.memory, null, 2); // Dumps everything!

4. Use Descriptive Schema

good-schema.ts
// ✅ Good - clear descriptions
schema: z.object({
  userId: z.string().uuid().describe("Unique identifier for the user"),
  sessionType: z
    .enum(["support", "sales", "general"])
    .describe("Type of support session"),
});
 
// ❌ Bad - no descriptions, unclear
schema: z.object({
  id: z.string(),
  type: z.string(),
});

Key Takeaways

  • Contexts separate state - Each conversation/session/game gets its own memory
  • Instance-based - Same context type, different instances for different users/sessions
  • Memory persists - State is saved between conversations automatically
  • LLM sees context - Render function shows current state to the AI
  • Context-specific actions - Attach actions that only work in certain contexts
  • Clear boundaries - Design contexts around specific tasks or domains

Contexts are what make your agent stateful and able to maintain separate conversations and tasks without mixing things up. They're the foundation for building agents that can remember and manage complex, ongoing interactions.