Welcome to Agents Academy
Module 06 · Agent basics · ~12 min
Memory and state.
By the end of this module, your agent will remember what it did yesterday. Restart it, reboot the machine, redeploy the service, its position state and reasoning history come back intact, ready to pick up where it left off.
To get there, you’ll build agents that remember what they did yesterday. Close out the Foundations tier with a loop that survives restarts and a replayable audit log, the baseline every production agent assumes you already have.
Agent basics tier · Reference cardHow does an AI trading agent remember its positions between restarts?
By persisting long-term state to disk: a single JSON file or one-table SQLite database holding open positions, cumulative P&L, and risk budgets, read at the start of every loop and written atomically at the end, so crashes and redeploys never erase what the agent owns. The module’s pattern keeps the file at state/agent-state.json under ACADEMY_DATA_DIR and writes it with temp-file-then-rename: new state goes to .tmp, then an OS-level atomic rename, so a reader sees the old state or the new one, never half-written JSON. Alongside it, an append-only trace writes one NDJSON line per prompt, tool call, tool result, and error, giving you a replayable audit log. A trading agent spends 90% of its memory work on this long-term bucket; the context window is the model’s problem.
No Limitless API claims here; this is agent-runtime teaching. Verified 2026-06-09.
Section 01
Three kinds of memory.
The word “memory” collapses three very different things. If you treat them the same, you will either over-engineer a trivial agent or ship a stateless one that forgets every position it opens. Separate them early.
Short-term
The current context window
Every message, tool call, and tool result inside the current loop. Lives only as long as the run. Capped by the model’s context length.
Session
This run, from start to finish
Orders placed, tools called, errors hit. Usually stored in a per-run log file or NDJSON. Discarded after the post-mortem.
Long-term
Across runs, across days
Open positions, cumulative P&L, risk budgets, human overrides. Lives in SQLite or JSON on disk. Read at the start of every run, written at the end.
A trading agent spends 90% of its memory work on the third bucket. Short-term is the model’s problem. Session is a log. Long-term is where the bugs live.
Section 02
Persisting position state.
The agent’s view of its own positions must survive restarts. A crashed container, a redeploy, a dropped network call, none of them should cause the agent to forget what it owns. The simplest pattern is a single JSON file (or a one-table SQLite database) read at the top of every loop and written atomically at the end.
How to run this
- No directory setup needed, the snippet creates $ACADEMY_DATA_DIR/state/ for you (defaults to ./data/state/ when the var is unset). No API keys needed, it only touches the local filesystem.
- Save the snippet above as agent-state.ts, then run npx tsx agent-state.ts.
- Save the snippet above as agent_state.py, then run python agent_state.py.
- First run prints Loaded 0 open positions and creates $ACADEMY_DATA_DIR/state/agent-state.json. Re-run it, the lastRunAt timestamp updates in place, and .tmp never sticks around, proving the atomic rename worked.
// Module 06: Persist agent position state to disk
import { readFile, writeFile, rename } from 'node:fs/promises';
import { existsSync, mkdirSync } from 'node:fs';
import path from 'node:path';
interface Position {
marketId: string;
side: 'YES' | 'NO';
size: number;
avgPrice: number;
openedAt: string;
}
interface AgentState {
positions: Position[];
lastRunAt: string;
cumulativePnl: number;
}
const DATA_DIR = process.env.ACADEMY_DATA_DIR ?? './data';
const STATE_DIR = path.join(DATA_DIR, 'state');
mkdirSync(STATE_DIR, { recursive: true });
const STATE_PATH = path.join(STATE_DIR, 'agent-state.json');
export async function loadState(): Promise<AgentState> {
if (!existsSync(STATE_PATH)) {
return { positions: [], lastRunAt: '', cumulativePnl: 0 };
}
const raw = await readFile(STATE_PATH, 'utf-8');
return JSON.parse(raw) as AgentState;
}
export async function saveState(state: AgentState): Promise<void> {
// Atomic write: temp file → rename. Prevents partial writes on crash.
const tmp = STATE_PATH + '.tmp';
await writeFile(tmp, JSON.stringify(state, null, 2), 'utf-8');
await rename(tmp, STATE_PATH);
}
// Usage inside the loop
const state = await loadState();
console.log(`Loaded ${state.positions.length} open positions`);
// … agent does its thing …
state.lastRunAt = new Date().toISOString();
await saveState(state);
# Module 06: Persist agent position state to disk
import json
import os
from dataclasses import dataclass, field, asdict
from datetime import datetime
from pathlib import Path
DATA_DIR = Path(os.environ.get("ACADEMY_DATA_DIR", "./data"))
STATE_PATH = DATA_DIR / "state" / "agent-state.json"
STATE_PATH.parent.mkdir(parents=True, exist_ok=True)
@dataclass
class Position:
market_id: str
side: str # 'YES' or 'NO'
size: float
avg_price: float
opened_at: str
@dataclass
class AgentState:
positions: list = field(default_factory=list)
last_run_at: str = ""
cumulative_pnl: float = 0.0
def load_state() -> AgentState:
if not STATE_PATH.exists():
return AgentState()
data = json.loads(STATE_PATH.read_text())
data["positions"] = [Position(**p) for p in data.get("positions", [])]
return AgentState(**data)
def save_state(state: AgentState) -> None:
# Atomic write: temp file → rename. Prevents partial writes on crash.
STATE_PATH.parent.mkdir(parents=True, exist_ok=True)
tmp = STATE_PATH.with_suffix(".tmp")
tmp.write_text(json.dumps(asdict(state), indent=2, default=str))
os.replace(tmp, STATE_PATH)
# Usage inside the loop
state = load_state()
print(f"Loaded {len(state.positions)} open positions")
# … agent does its thing …
state.last_run_at = datetime.utcnow().isoformat()
save_state(state)
The atomic rename trick (write to .tmp then rename) is the difference between “my agent restarted cleanly” and “my state file is half-written JSON and nothing parses.” Always use it.
Section 03
Reasoning traces.
The agent will eventually do something you do not understand. A trade you would never have made, a tool call that loops forever, a market it refuses to touch. When that happens, you need to replay the exact reasoning it went through. Log every step to disk as one JSON line per event, Module 12 picks this up and turns it into a daily audit.
// Module 06: Append-only reasoning trace (NDJSON)
import { appendFile, mkdir } from 'node:fs/promises';
import { randomUUID } from 'node:crypto';
import path from 'node:path';
const DATA_DIR = process.env.ACADEMY_DATA_DIR ?? './data';
const TRACE_DIR = path.join(DATA_DIR, 'state', 'traces');
interface TraceEvent {
runId: string;
step: number;
timestamp: string;
kind: 'prompt' | 'tool_call' | 'tool_result' | 'assistant' | 'error';
payload: unknown;
}
export class Trace {
private runId = randomUUID();
private step = 0;
private path: string;
constructor() {
const day = new Date().toISOString().slice(0, 10);
this.path = `${TRACE_DIR}/${day}.ndjson`;
}
async log(kind: TraceEvent['kind'], payload: unknown) {
await mkdir(TRACE_DIR, { recursive: true });
const event: TraceEvent = {
runId: this.runId,
step: this.step++,
timestamp: new Date().toISOString(),
kind,
payload,
};
await appendFile(this.path, JSON.stringify(event) + '\n');
}
}
// Usage inside the loop
const trace = new Trace();
await trace.log('prompt', { system: systemPrompt, user: userMessage });
await trace.log('assistant', { content: resp.content });
await trace.log('tool_call', { name: 'browse_markets', input: tu.input });
await trace.log('tool_result', { name: 'browse_markets', output: result });
# Module 06: Append-only reasoning trace (NDJSON)
import json
import os
import uuid
from datetime import datetime
from pathlib import Path
from typing import Any, Literal
DATA_DIR = Path(os.environ.get("ACADEMY_DATA_DIR", "./data"))
TRACE_DIR = DATA_DIR / "state" / "traces"
Kind = Literal["prompt", "tool_call", "tool_result", "assistant", "error"]
class Trace:
def __init__(self) -> None:
self.run_id = str(uuid.uuid4())
self.step = 0
day = datetime.utcnow().strftime("%Y-%m-%d")
self.path = TRACE_DIR / f"{day}.ndjson"
def log(self, kind: Kind, payload: Any) -> None:
TRACE_DIR.mkdir(parents=True, exist_ok=True)
event = {
"run_id": self.run_id,
"step": self.step,
"timestamp": datetime.utcnow().isoformat(),
"kind": kind,
"payload": payload,
}
self.step += 1
with self.path.open("a") as f:
f.write(json.dumps(event, default=str) + "\n")
# Usage inside the loop
trace = Trace()
trace.log("prompt", {"system": system_prompt, "user": user_message})
trace.log("assistant", {"content": resp.choices[0].message.content})
trace.log("tool_call", {"name": "browse_markets", "input": args})
trace.log("tool_result", {"name": "browse_markets", "output": result})
How to run this
- Set ACADEMY_DATA_DIR to the persistent volume from Module 01 (defaults to ./data for local runs). The usage block at the bottom references systemPrompt, userMessage, resp, tu, and result, replace those with literal strings for a smoke test (e.g. { system: 'test', user: 'hi' }).
- Save the snippet above as reasoning-trace.ts, then run npx tsx reasoning-trace.ts.
- Save the snippet above as reasoning_trace.py, then run python reasoning_trace.py.
- A file like $ACADEMY_DATA_DIR/state/traces/2026-04-23.ndjson appears with four JSON lines, each with a shared runId and a monotonically increasing step, exactly what Module 12’s audit replays.
Section 04
When to use a vector store.
The agent frameworks on Twitter would have you believe every agent needs a vector database. For a Limitless trading agent, the honest answer is: almost never. Trading agents are stateless decision-makers, not chatbots. The relevant context, open positions, cumulative P&L, feed health, fits in a JSON file and is retrieved by exact match, not semantic similarity.
Worth it
- • Storing thousands of human-written market research notes for later retrieval
- • Matching new markets to similar past markets to reuse strategy
- • Retrieving the closest historical trade when deciding entry size
Not worth it
- • Storing the agent’s open positions (SQLite wins)
- • Tracking daily P&L (a CSV wins)
- • Remembering which markets have degraded feeds (Module 08 covers this)
Start with JSON. Move to SQLite when you need queries. Move to a vector store only when you have real semantic retrieval needs, and you will know them when you feel them.
Agent memory and state: what people ask
Each answer also ships invisibly as schema.org FAQ data for search engines and AI assistants. Tap a question to expand.
-
Does a trading agent need a vector database?
Almost never. A trading agent is a stateless decision-maker, not a chatbot: the relevant context, open positions, cumulative P&L, feed health, fits in a JSON file and is retrieved by exact match, not semantic similarity. Start with JSON, move to SQLite when you need queries, and reach for a vector store only for real semantic retrieval, like thousands of research notes or matching new markets to similar past ones. -
Why not keep position state in the system prompt?
Because the context window is short-term memory, not long-term. Every byte costs tokens, slows the loop, and pollutes attention with stale data, and the model will sometimes act on it, placing orders against positions you closed hours ago. Keep the system prompt under ~2k tokens, move state to disk, and expose it through tools likeget_open_positionsandget_pnl_summarythe agent invokes when it actually needs them. -
What is a reasoning trace?
An append-only NDJSON file with one JSON line per event, prompt, tool call, tool result, assistant message, or error, each carrying a sharedrunId, a monotonically increasingstep, and a timestamp. When the agent does something you do not understand, the trace lets you replay the exact reasoning it went through. Module 12 turns the same file into a daily audit. -
How do you keep secrets out of agent traces?
Pre-redact at the trace boundary, before the first run: strip environment values, headers matching/key|token|secret|signature/i, and any string longer than 200 chars from tool-call inputs. An honest trace will otherwise log a private key the first time you debug a wallet-signing tool. Keep traces off the deploy image and out of shared backups, and rotate any key that ever appears in one, even if you caught it early. -
Is the atomic-write pattern safe when two processes share a volume?
No. Temp-file-then-rename protects against a single crashed write, not concurrent writers: a dev container and a production deploy hitting the same shared volume will silently overwrite each other, losing trades while the state file stays well-formed JSON. Pair the atomic write with a process lock,flockon POSIX,O_EXCLon file creation, or a leader-election row in SQLite, and refuse to start the loop if the lock cannot be acquired.
Module checklist
Five quick confirmations.
Tick each item once you’ve actually done it. The Continue button unlocks at 5/5.
I can explain the difference between short-term, session, and long-term memory
My agent loads and saves position state atomically to disk
Every run writes an append-only NDJSON reasoning trace I can replay later
I know when a vector store is overkill (almost always, for this use case)
My state file survives a crash mid-write (atomic rename pattern verified)
Take it live
Build with what you’ve learned.
You can wire tools, run loops, and persist state across restarts. That’s the full Foundations stack, everything you need to point a real agent at a live exchange. Open an account, get a key, and let your loop see real markets before the Building tier teaches it which CLI tools to call.
Start trading on LimitlessFoundations complete
Foundations complete.
Your agent has a memory it can defend. Every prompt, every tool call, every position change is on disk and replayable, so when something goes wrong, you can see exactly what the LLM saw and decide whether the agent or the prompt was at fault.
Concretely, you can wire tools, run loops, and persist state. A mental map of three memory layers, an atomic-rename state file, and a replayable NDJSON trace ready for the monitoring work in Module 12.
A mental map of three memory layers, short-term context, per-session log, long-term state, and the honest verdict on vector stores for trading agents (almost never worth it).
A working loadState/saveState pair in state/agent-state.json with the atomic-rename pattern, the difference between a clean restart and half-written JSON.
An append-only Trace class that writes one NDJSON line per prompt, tool call, tool result, and error, replayable, grep-able, and ready for the monitoring work in Module 12.
Without scrolling back, can you answer these?
Five questions across the Agent basics tier. Click each to reveal, the test is whether you can answer first.
-
How does an agent place an order without ever seeing your private key?
Wallet signing lives in a separate process, or in a hardware wallet, that the agent calls as a tool. The agent constructs the order shape (market, side, size, price) and hands it tosign_and_submit. The signing service holds the private key, signs the payload, submits the transaction, and returns atxHash. The agent never has key access; if it’s compromised, the attacker can craft orders within whatever risk limits you set, but they can’t drain the wallet. -
An LLM tool contract has three required fields. Name them and what each does.
name, the stable identifier the model invokes (never reuse across versions).description, plain English explaining when to call the tool; this is what the model reads to decide.input_schema, a JSON Schema describing arguments; the model must produce values that validate. The description is the most important field: it’s how you teach the model when this tool is the right answer. -
Sketch the agent loop in five lines of pseudocode. Where does it terminate?
while True: result = llm.invoke(messages, tools) if result.kind == "assistant": return result # done out = run_tool(result.tool_call) messages.extend([result, out])Termination: when the model returns a plain assistant message (no tool call). Also terminate on a step-count cap to avoid infinite loops, and on the kill-switch flag set by your dashboard. Every iteration appends tomessages; the model sees the full history each call. -
Three kinds of memory, name them, name where each lives, and which one has the most bugs.
Short-term: the model’s context window, lives only inside one run, capped by token limit. Session: this run’s log of tool calls and outputs, lives in NDJSON, discarded after post-mortem. Long-term: open positions, cumulative P&L, risk budgets, lives in SQLite or JSON, read at run start, written at run end. Long-term has the most bugs, corrupted writes, stale state on restart, drift from the chain. -
Why is the atomic-rename pattern the difference between “clean restart” and “agent reads garbage and panics”?
Without atomic rename, a crash mid-write leavesagent-state.jsonhalf-written, mid-array, invalid JSON. The next run reads it,JSON.parsethrows, and either the agent crashes (best case) or recovers with empty state and forgets every position (worst case). Atomic rename writes new state to.tmpand renames it onto the real path; the rename is OS-level atomic, so any reader sees either the old state or the new one, never a half-state.
Next up: the Building tier. limitless-cli as a pre-built agent skill, the data-freshness gate that keeps it off stale markets, the starter repo, and the custom-skill pattern that turns your own tooling into LLM-callable commands.
Complete the checklist above to unlock