"They went into the dining-room and seated themselves; and then, while supper was being served, Eryximachus the physician spoke first..." — Plato, Symposium (c. 385 BCE)
A small, autonomous multi-agent dialogue lab that runs entirely in a single Google Colab notebook. Two or more LLM-served personas — each with a deeply written biography and persistent memory across sessions — chat openly the way friends do: drifting between topics, disagreeing, pivoting, occasionally asking to bring a new person into the room. A separate, more capable model (GPT-4o) acts as the silent "Master" who maintains each agent's rolling memory and gently nudges the conversation when it stalls.
There is no task. There is no benchmark. The point is to see what happens when you give a few small open-source models real personalities, give them somewhere to remember, and let them talk.
Most multi-agent LLM demos are task-oriented: solve a problem, write a doc, debate a proposition. This one is socially-oriented. The agents are seeded as people, not as roles. They have:
- a birthplace, family, formative incidents, habits, fears, and desires (~150–200 words of second-person biography that reads like the opening of a short story);
- persistent memory across Colab sessions, stored on Google Drive (full transcripts) plus a rolling first-person summary that GPT-4o keeps fresh every 10 turns;
- the ability to call the Master — a more capable external model — to bring a new persona into the conversation, which is then hot-spawned at runtime onto whichever vLLM endpoint is least loaded;
- organic topic drift with optional kickoff seeding, so a session can either pick up from where the last one left off, or be steered toward a specific subject (e.g. an unfolding news event) without being heavy-handed about it.
The architectural goal is to demonstrate a clean separation between three concerns that often get tangled in agent frameworks: memory (MCP), inference (vLLM), and orchestration (a small async loop in Python). Each layer is replaceable.
┌─────────────────────────────┐
│ Colab notebook (driver) │
│ asyncio orchestration loop │
└─────────────┬───────────────┘
│
┌───────────────────────────────┼───────────────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌─────────────────────┐ ┌──────────────────────┐
│ vLLM voices │ │ Per-agent memory │ │ Master (GPT-4o) │
│ (OpenAI-compat) │ │ (one MCP server │ │ external API │
│ │ │ per persona) │ │ │
│ :8000 Qwen2.5-7B │ │ │ │ • summarizes every │
│ :8001 Mistral-7B │ │ tools: │ │ N turns per agent │
│ (AWQ, half) │ │ • get_biography │ │ • topic pulse every │
│ share one A100 │ │ • get_summary │ │ M turns (optional │
│ │ │ • append_turn │ │ nudge) │
│ │ │ • update_summary │ │ • approves new │
│ │ │ • recent_turns │ │ personas requested │
│ │ │ • search_memories │ │ by agents │
└──────────────────┘ └─────────────────────┘ └──────────────────────┘
│
▼
┌─────────────────────────────┐
│ Google Drive (persistent) │
│ /MyDrive/agent_memories/ │
│ <Name>/ │
│ biography.md │ immutable
│ summary.md │ rewritten by Master
│ transcript.jsonl │ append-only history
└─────────────────────────────┘
Key design choices
| Decision | Rationale |
|---|---|
| vLLM over alternatives | Mature, OpenAI-compatible API, AWQ support, well-tested on Colab GPUs. |
| One MCP server per agent, not one shared MCP | Each persona owns its memory; the orchestrator doesn't have to namespace anything; stateless re-attachment is trivial. |
| stdio MCP transport | Zero network surface, no port conflicts, dies cleanly with the parent process. |
MCP via persistent worker tasks (not a shared AsyncExitStack) |
Jupyter runs each cell in a fresh asyncio task; anyio's cancel scopes blow up if a stack is entered in one task and exited in another. A worker task that owns its own stdio_client + ClientSession for its lifetime sidesteps this entirely. |
| Two voices on one GPU via 0.40 mem-util each | Both 7B AWQ models comfortably fit on an A100-40GB; on an 80GB you have headroom for longer --max-model-len or a third voice. |
| New personas reuse existing vLLM endpoints | Loading a 3rd model would cost minutes and GPU memory; assigning a new persona's prompt to whichever endpoint hosts the fewest agents gives the same diversity without new weights. |
| Rolling summary as the long-term memory | Full transcripts grow unboundedly; the system prompt only needs the agent's own subjective recollection, kept under ~400 words by the Master. |
| Master is OpenAI/Claude, not a third local model | Summarization needs to be better than the agents themselves to be useful. A small local model summarizing other small local models tends to hallucinate or flatten. |
Four personas ship with the notebook. Each is written in second person, ~150–200 words, with the texture of a short-story opening — formative incidents, quirks, fears, desires. Two share each vLLM voice so both models drive from turn 1.
| Name | Voice | Background |
|---|---|---|
| Maya | Qwen2.5-7B | Mumbai → Bengaluru via SF; ex-consumer-tech engineer, skeptical of techno-optimism that ignores infrastructure and inequality. |
| Kai | Mistral-7B-v0.2 | Tromsø, Norway; marine biologist studying Arctic krill; reserved, empirical, distrustful of sweeping claims. |
| Meera | Mistral-7B-v0.2 | Siliguri → Rotterdam; Maritime Risk Analyst; observant and methodical; over-plans because of one childhood afternoon. |
| Elias | Qwen2.5-7B | Duluth → Santa Fe; Forensic Audio Restoration Specialist; warm, sensory, listens like other people look. |
Adding a new persona at runtime is one Master call away — see Live persona requests below.
- Hardware: Runtime → Change runtime type → A100 GPU (40 GB is the floor; 80 GB is comfortable).
- Secret: Add
OPENAI_API_KEYin the Colab Secrets sidebar (🔑 icon, toggle "Notebook access"). - Open the notebook, then run cells in order:
nvidia-smisanity checkpip installvLLM + MCP SDK + OpenAI client- Mount Drive (model weights and agent memories live here so they survive Colab restarts)
hf downloadfor both AWQ models (cached on Drive after first run; ~7 GB each)- Launch both vLLM servers in the background
- Wait-for-ready loop (first boot is several minutes while weights load)
- Set
MEMORY_ROOT,KICKOFF_TOPIC, summarizer cadence, etc. - Seed biographies (idempotent — won't overwrite existing memory on Drive)
- (optional) Re-seed each agent's rolling summary toward
KICKOFF_TOPIC, which is what redirects an existing chat onto a new subject without wiping transcripts - Write the per-agent MCP server module
- Spawn one MCP worker task per agent and verify the tool set
mem_callhelper- Master (GPT-4o) functions:
master_summarize,master_handle_request,master_topic_pulse - Orchestrator:
Agentdataclass,build_system_prompt,to_chat_messages(multi-party with speaker attribution),speak,summarize_all - Main loop — run this to start. Hit ⏹ stop to end; final summaries flush to Drive on
KeyboardInterrupt.
- When you're done for the session, run the two shutdown cells (MCP workers, then vLLM processes).
All in cell 8.
| Constant | Default | What it controls |
|---|---|---|
MEMORY_ROOT |
/content/drive/MyDrive/agent_memories |
Where biographies, summaries, and transcripts live across sessions. |
SUMMARIZE_EVERY |
10 |
Master compresses each agent's recent turns into their rolling summary every N turns. |
TOPIC_CHECK_EVERY |
8 |
Master peeks at recent turns and may issue a one-shot nudge to the next speaker. Default bias is continue — only nudges when the chat is looping or flat. |
CONTEXT_WINDOW_TURNS |
12 |
Live turns fed to each model on every call. Older context lives in the rolling summary. |
MASTER_MODEL |
'gpt-4o' |
Any chat-completions-compatible model. gpt-4o-mini cuts cost ~10× at some quality loss. |
KICKOFF_TOPIC |
(a paragraph about the Manipur ethnic conflict) | When set, cell 8b wipes each agent's rolling summary and re-seeds it with a "what's on my mind right now" note pointing at this topic; the loop also nudges the opening speaker so turn 0 lands squarely on it. Set to None to resume freely from prior memory. |
Three files per agent on Drive:
agent_memories/Maya/
├── biography.md # ~150-200 words, second person, immutable
├── summary.md # ≤400 words, first person, rewritten by Master every SUMMARIZE_EVERY turns
└── transcript.jsonl # append-only: {"ts": ..., "speaker": "...", "text": "..."}
- biography.md is the agent's immutable identity. Edit by hand to evolve a persona between sessions; the orchestrator only reads, never writes.
- summary.md is the agent's subjective recollection. Each agent has their own — Maya's summary of a conversation reads differently from Kai's summary of the same conversation. This is on purpose: it produces drift in how each persona remembers shared events.
- transcript.jsonl is the ground-truth log. Used as the source of truth when Master rewrites summaries, and queryable by the agent itself via
search_memories(substring search; could be upgraded to embeddings).
Resetting: to redirect onto a new topic without losing identity, set KICKOFF_TOPIC and re-run cell 8b (it overwrites only summary.md). To start truly fresh, also delete the agent's transcript.jsonl.
Any agent can include exactly one line of the form
<<MASTER:add a persona who is a retired schoolteacher from Imphal:END>>
inside an otherwise normal turn. The orchestrator strips that line from the visible reply, forwards the request to Master with the recent context, and Master responds with strict JSON either approving (with a name and 3-4 sentence biography) or rejecting (with a reason).
When Master approves:
- The new persona's directory is created on Drive.
- An MCP worker is spawned for them.
- They're assigned to whichever vLLM endpoint is currently hosting the fewest agents (with a tie-break that alternates rather than always defaulting to the first model — so you don't end up with five Qwen voices and one Mistral).
- They're appended to the speaking rotation and start participating on the next turn.
This is additive only. Personas don't get removed. If you want a smaller cast back, restart the runtime.
Per 100 turns with all four personas talking and MASTER_MODEL='gpt-4o':
| Operation | Frequency | Approx. tokens (in / out) | Approx. cost |
|---|---|---|---|
master_summarize per agent |
every 10 turns × 4 agents = 40 calls | ~2k / ~400 | ≈ $0.40 |
master_topic_pulse |
every 8 turns × 1 call = ~12 calls | ~1k / ~150 | ≈ $0.04 |
master_handle_request |
rare; ~1 per session | ~0.5k / ~0.3k | ≈ $0.01 |
| Total | ≈ $0.45 / 100 turns |
vLLM inference is free (your Colab GPU). Switching MASTER_MODEL to 'gpt-4o-mini' brings the Master cost to roughly $0.04 per 100 turns.
- No conversational rewind / replay. The transcript is append-only; if a turn is bad, your only options are to restart the runtime or hand-edit the JSONL.
search_memoriesis substring-only. Upgradable to embeddings (e.g. BGE-small) without changing the orchestrator — just swap the MCP tool body.- Mistral-7B-Instruct-v0.2 rejects the
systemrole, so the orchestrator folds the system prompt into the firstusermessage. Fine in practice but means the model can't be told "here's a system instruction" out-of-band. - Hot-spawning a persona reuses an existing vLLM endpoint, which means the new persona's outputs share token-distribution character with whichever model hosts them. With four personas across two voices that's already noticeable; with eight personas across two voices it gets harder to tell them apart. Add a third vLLM endpoint (e.g. a Gemma or Phi variant) on a free GPU port to expand the voice pool.
- No moderation layer. These are open-ended conversational agents — they will say things their authors would not. If you're posting transcripts publicly, read them first.
- The "Master" is a single point of API dependency. If the OpenAI endpoint is down, summarization and topic-pulse silently fail; the agents themselves keep running.
symposium/
├── README.md # this file
├── requirements.txt # for local reproduction (Colab installs in-cell)
├── .gitignore # ignores agent_memories/, hf_cache/, .env, etc.
└── symposium.ipynb # the entire system — 19 cells, ordered top-to-bottom
Everything ships in one notebook on purpose. The MCP server is written to /content/agent_mcp_server.py at runtime (cell 10) so the whole project remains a single artifact you can copy or share without breaking imports.
- Swap
search_memoriesto BGE-small embeddings + cosine retrieval - Add a third vLLM voice (Gemma-2-9B-AWQ or Phi-3-medium-AWQ) for richer hot-spawn diversity
- Optional Anthropic Claude as Master (single-flag swap; the prompts are model-agnostic)
- Rendered transcript viewer (a small Streamlit app that reads the JSONLs from Drive)
- LoRA fine-tuning a 7B base on a single persona's summarized memories — does the persona stabilize, or collapse to mannerism?
- Multi-room: spawn a second
autonomous_loopwith a disjoint cast and have one persona shuttle between rooms - On-demand "checker": a second external model that fact-checks any factual claim an agent makes about the real world
Built by @boeing23. Inspired in spirit by Plato, in shape by vLLM and the Model Context Protocol, and in tone by every late-night kitchen-table conversation that drifted somewhere unexpected.