# Your Agent Isn't Stateful — It Just Has a Long Message Array

Most agent implementations are stateless. A request goes in, a response comes out, and everything in between evaporates. Developers add a `messages` array to fake continuity, but that's not state — it's a transcript.

The difference matters. Stateless agents can answer questions. Stateful agents can do work across time — tracking context, accumulating knowledge, picking up where they left off. If you're building anything beyond a single-turn Q&A bot, you'll hit this wall eventually.

## The Stateless Default

Here's what most agent setups look like under the hood:

```typescript
async function handleMessage(userMessage: string, history: Message[]) {
  const response = await llm.chat({
    messages: [
      { role: 'system', content: systemPrompt },
      ...history,
      { role: 'user', content: userMessage },
    ],
  });
  return response;
}
```

Every request reconstructs the world from scratch. The model sees the transcript and does its best to infer context. This works surprisingly well for short conversations, which is why teams don't notice the problem immediately.

But it breaks down in predictable ways:

- **Context drift.** As conversations grow, earlier messages get truncated or summarized. The agent "forgets" commitments it made three messages ago.
- **No persistent knowledge.** There's nowhere for the agent to store working data between turns — extracted entities, intermediate results, running summaries.
- **No lifecycle.** If the user leaves and comes back, you either replay the entire history (expensive, fragile) or start over.
- **No coordination.** Multiple agents can't share state because there's no state to share.

The `messages` array gives you a conversation log. What it doesn't give you is a session.

## What State Actually Means

When we talk about stateful agents, we mean more than remembering what the user said. A session needs to track at least three things:

**Conversation history** — the messages themselves, in a format the model and the UI both understand.

**Variables** — typed, scoped data that lives for the duration of the session. Think of these as the agent's working memory: a user ID extracted from a tool call, a classification result from an earlier step, a flag indicating whether authentication happened.

**Resources** — persistent values the agent can read and write across turns. A running summary, accumulated preferences, a scratchpad for multi-step reasoning.

In Octavus, these are first-class concepts in the agent protocol:

```yaml
input:
  COMPANY_NAME: { type: string }
  USER_ID: { type: string, optional: true }

resources:
  CONVERSATION_SUMMARY:
    description: Running summary for context management
    default: ''

variables:
  TICKET:
    type: unknown
  SENTIMENT:
    type: string
```

Variables and resources aren't afterthoughts bolted onto a chat loop. They're declared upfront, typed, and available to every block in the execution flow. The agent's prompts can reference them directly with `{{VARIABLE_NAME}}`, and tools can read and write them during execution.

This is the difference between "the model knows what was said" and "the agent knows what's happening."

## The Lifecycle Problem

State creates a lifecycle problem that stateless architectures don't have. If an agent has state, that state needs to be created, maintained, and eventually cleaned up. Users close tabs, sessions expire, servers restart. You need answers for all of it.

The lifecycle looks something like this:

1. **Create** — Initialize a session with an agent and input variables.
2. **Execute** — Trigger actions, stream responses, handle tool calls. State accumulates.
3. **Retrieve** — On page reload, fetch the current session state and restore the UI.
4. **Expire** — After inactivity, the session expires. Resources are reclaimed.
5. **Restore** — When the user returns, restore the session from stored messages.

That restore step is where most hand-rolled implementations fall apart. You can't just replay messages — you need to reconstruct the full execution context: variables, resources, tool results, everything the agent was tracking.

Here's how this looks with Octavus's session management:

```typescript
const client = new OctavusClient({
  baseUrl: process.env.OCTAVUS_API_URL!,
  apiKey: process.env.OCTAVUS_API_KEY!,
});

// Check if the session is still alive
const result = await client.agentSessions.getMessages(sessionId);

if (result.status === 'active') {
  // Session is live — return messages directly
  return { sessionId: result.sessionId, messages: result.messages };
}

// Session expired — restore from stored messages
if (storedMessages.length > 0) {
  const restored = await client.agentSessions.restore(
    sessionId,
    storedMessages,
    { COMPANY_NAME: 'Acme Corp' },
  );

  if (restored.restored) {
    return { sessionId: restored.sessionId, messages: storedMessages };
  }
}

// Nothing to restore — start fresh
const newSessionId = await client.agentSessions.create('support-agent', {
  COMPANY_NAME: 'Acme Corp',
});
return { sessionId: newSessionId, messages: [] };
```

The `getMessages()` call returns a `status` field — `active` or `expired` — so you always know where you stand. No guessing, no silent failures where the agent acts like it has amnesia.

## State Changes What Streaming Means

In a stateless setup, streaming is purely about UX — showing tokens as they arrive. In a stateful system, streaming events carry state transitions. The stream isn't just text; it's a live feed of everything the agent is doing.

Octavus streams structured events that represent the full execution:

```typescript
// Lifecycle
{ type: 'start', messageId: '...', executionId: '...' }

// The agent is working through execution blocks
{ type: 'block-start', blockId: '...', blockName: 'Look up account' }

// Tool calls happen mid-stream, with structured input/output
{ type: 'tool-input-available', toolCallId: '...', toolName: 'get-user-account', input: { userId: 'user-123' } }
{ type: 'tool-output-available', toolCallId: '...', output: { name: 'Demo User', plan: 'pro' } }

// Resources update as the agent works
{ type: 'resource-update', name: 'CONVERSATION_SUMMARY', value: 'User inquired about billing...' }

// Text streams to the UI
{ type: 'text-delta', id: '...', delta: 'I found your account...' }

// Execution completes
{ type: 'finish', finishReason: 'stop' }
```

Each event type tells you something specific about what the agent is doing and how its state is changing. The `resource-update` event, for instance, means the agent just updated a persistent value that will survive across future turns. The `block-start` and `block-end` events let you show execution progress in the UI — not just "the agent is typing" but "the agent is looking up your account."

This is state made observable. You're not polling for changes or diffing message arrays. The stream tells you exactly what happened, when.

## Execution Context Survives Tool Calls

One of the subtler benefits of stateful sessions is how they handle tool execution. In a stateless setup, tool calls are a round-trip — the model requests a tool, you execute it, and you send the result back as a new message in the transcript. The model has to re-derive its plan from the entire conversation each time.

With sessions, the execution context persists through tool calls. When the model requests a tool, the session holds the execution state while waiting for the result. When the result comes back, execution continues from where it paused — same variables, same resources, same place in the handler flow.

```typescript
const session = client.agentSessions.attach(sessionId, {
  tools: {
    'get-user-account': async ({ input }) => {
      const user = await db.users.findUnique({ where: { id: input.userId } });
      return { name: user.name, email: user.email, plan: user.plan };
    },
    'create-ticket': async ({ input }) => {
      const ticket = await ticketService.create(input);
      return { ticketId: ticket.id, status: ticket.status };
    },
  },
});

// Execute — tool calls happen inline, context is preserved
const events = session.execute({
  type: 'trigger',
  triggerName: 'user-message',
  input: { USER_MESSAGE: 'I need help with my billing' },
});
```

The `attach` + `execute` pattern means tool handlers run on your infrastructure with your data, but the session state lives in the orchestration layer. The agent can call multiple tools in sequence, accumulate results in variables, and make decisions based on the full execution context — not just whatever fits in the message history.

## Persistence Is Your Problem (On Purpose)

One design decision worth calling out: Octavus manages session state during execution, but long-term persistence is left to you. After each interaction, you pull the messages and store them in your own database.

```typescript
const result = await client.agentSessions.getMessages(sessionId);

if (result.status === 'active') {
  await db.chats.update({
    where: { id: chatId },
    data: {
      sessionId: result.sessionId,
      messages: result.messages,
    },
  });
}
```

This might seem like an inconvenience, but it's a deliberate boundary. Your chat data lives in your database, under your access controls, in your backup strategy. The orchestration layer doesn't become a data silo. If you migrate away, your conversation history comes with you.

It also means you can query, index, and analyze your agent conversations using whatever tools you already have. No vendor-specific export format, no API to paginate through.

## The Real Point

The gap between "agent" and "chatbot" isn't the model or the prompt. It's the infrastructure underneath. Stateless architectures cap what agents can do — they can respond, but they can't accumulate knowledge, track progress, or maintain context across interruptions.

Sessions aren't glamorous. They're plumbing. But they're the plumbing that makes agents actually useful in production — where users leave and come back, where conversations span hours, where the agent needs to remember what it was doing and why.

If you're building agents that need to do more than answer the current question, [the session docs](https://octavus.ai/docs/server-sdk/sessions) are worth a read. The lifecycle patterns — create, execute, expire, restore — solve problems you'll definitely hit if you haven't already.
