A lightweight, AI-powered email simulator for teaching and assessing how people write and manage email with the help of an AI assistant. Learners read a seeded thread, compose replies in a rich editor, and "send" messages — while an AI copilot (Cosmo) can help draft, answer questions about the email history, summarize, and extract information. Live scenario characters can write back in-character so the learner practices a real email exchange.
CMail never sends real email; it simulates the experience and captures everything for rubric-based assessment.
- Each exercise is a single scenario defined in a JSON config (seeded inbox, task/brief, and which AI capabilities are enabled).
- All agentic work is orchestrated by Octavus. Cosmo (
cosmo-mail) is the copilot. Live scenario people are a second agent (cosmo-mail-character), one Octavus session per character. Separate dev and prod profiles let you test configuration before going live. - The UI uses the shared
design-systemsubmodule. - No database — short-term state (threads, drafts, assistant messages) is
stored in a local
sessions.jsonfile.
- Node.js 20+
- An Octavus API key and access to deploy an agent.
Clone with the design-system submodule:
git clone --recurse-submodules <repo-url>
cd bespoke_email_simulator
# If you already cloned without submodules:
git submodule update --init
npm installConfigure environment variables:
cp .env.example .env
# then edit .env with your Octavus credentials and agent ids| Variable | Purpose |
|---|---|
OCTAVUS_API_URL |
Octavus platform URL (default https://octavus.ai). |
OCTAVUS_API_KEY |
Your Octavus API key. |
AGENT_TARGET |
Which deployed agent the server talks to: dev or prod (default prod). |
OCTAVUS_AGENT_ID_DEV |
Cosmo (copilot) agent id used when AGENT_TARGET=dev. |
OCTAVUS_AGENT_ID_PROD |
Cosmo (copilot) agent id used when AGENT_TARGET=prod. |
OCTAVUS_CHARACTER_AGENT_ID_DEV |
Character agent id used when AGENT_TARGET=dev. |
OCTAVUS_CHARACTER_AGENT_ID_PROD |
Character agent id used when AGENT_TARGET=prod. |
Set up your scenario:
cp scenario.example.json scenario.json
# or start from one of the examples/ (see "Example scenarios")npm run dev # builds the client, watches, runs against the DEV agent
npm start # one-off build + server against the default (prod) agent
npm run start:prodThe app serves on port 3000 by default (override with PORT).
Two agent definitions live under agents/:
agents/cosmo-mail/— Cosmo, the email copilotagents/cosmo-mail-character/— in-character correspondents
npm run validate:agent # validate both agent definitions
npm run deploy:agent:dev # create/update cosmo-mail-dev
npm run deploy:agent:prod # create/update cosmo-mail
npm run deploy:character-agent:dev # create/update cosmo-mail-character-dev
npm run deploy:character-agent:prod # create/update cosmo-mail-characterThe deploy script stages the agent, rewrites slug/name for the target, then
runs octavus validate + octavus sync. Copy the resulting ids into the
matching OCTAVUS_AGENT_ID_* and OCTAVUS_CHARACTER_AGENT_ID_* variables in
.env.
A scenario config drives one exercise. Only fields you want to override need to
be present — everything else falls back to sane defaults (see
lib/scenario.js). The seed inbox may be inline ({ "threads": [...] }) or a
string path to a fixture file relative to the project root.
| Field | Type | Notes |
|---|---|---|
id |
string | Required. Unique scenario id. |
title |
string | App title. |
brief |
string | The exercise task/prompt. Presented by the host harness; CosmoMail no longer renders it in-app. |
primarySkill |
writing | prompting | both |
What the exercise assesses. |
scenarioType |
compose_new | reply | reply_chain |
Drives composer prefill. |
learner |
{ displayName, email, avatar } |
Who the learner is in the thread. Optional avatar is 0–12; 0 is the empty face and the default for You. |
characters |
object[] | People the learner may put on To / Cc. { id, name, email } required; optional avatar (0–12). Optional persona (free-form object or string) and/or prompt make the character live — they reply via cosmo-mail-character. responds: true/false overrides that. Directory-only people (no persona, responds omitted) can be emailed but never write back. |
world |
string | { summary } |
Shared in-world facts every live character already knows. Cosmo does not see this. |
seed.inbox |
object | string | Inline { threads } or a fixture path. Threads may set "mailbox": "spam" to land in Spam; otherwise they start in Inbox. Sent is filled automatically when the learner sends. |
seed.activeThreadId |
string | Which mailbox to open on load (the folder that contains this thread). |
seed.focusedEmailId |
string | Email the reply targets. |
initialDraft |
{ to, cc, subject, body } |
Optional composer prefill. |
assistant.enabled |
boolean | When false, hide the Cosmo copilot panel. |
assistant.capabilities |
string[] | compose, qa_search, summarize, extract. |
assistant.systemPromptExtra |
string | Trusted extra instructions for the copilot. |
assistant.initialMessage |
string | Cosmo's opening message. |
assistant.allowCustomInstructions |
boolean | Let learners add their own instructions. |
generation |
{ model, temperature, thinking, language } |
LLM settings. |
attachments |
{ enabled, allowedTypes } |
Outbound attachment support. |
ui |
{ hideHistory, strings } |
UI overrides + i18n strings. |
rubricHints |
object | string | Notes surfaced in the extraction report. |
Ready-to-run scenarios live in scenario-examples/ (their
seed data lives in fixtures/). Copy one to scenario.json to try
it:
01-compose-new-outreach— write a cold outreach email from scratch (no seed inbox, copilot only).02-reply-vendor-negotiation— reply within a seeded vendor negotiation thread (copilot + attachments; Dana is directory-only, so she will not write back).03-qa-summarize-status— use Cosmo to interrogate a project thread and write a leadership-ready summary (Q&A / search / extract focus).04-simulated-recipient-support— a customer-support thread where Marcus replies in-character until the issue is resolved.05-scripted-recipient-scheduling— schedule an interview; Jordan replies in-character. Morgan is in the directory but does not write back unless you give her a persona.06-software-sales-prospecting— align with a sales manager on one CRM lead, then email that prospect and book a meeting (Alex plus five live prospects; Ryan is the right call).
cp scenario-examples/04-simulated-recipient-support.scenario.json scenario.json && npm run devSee scenario-examples/README.md for details.
Base UI strings live in i18n/ (e.g. i18n/en.json). A scenario picks a
language via generation.language, and per-scenario overrides can be supplied in
ui.strings.
Turn captured sessions (sessions.json) into readable Markdown for an AI tutor
or an assessment rubric:
npm run extract # full transcript, newest first
node extract-conversations.js --mode submission # only the final (most recent) sent email
node extract-conversations.js --mode thread # the email thread(s)
node extract-conversations.js --mode assistant # the Cosmo conversation
npm run report # Markdown report (rubric hints in header)
node extract-conversations.js --mode report --output report.md --print-settingsOptions: --latest (most recent session only), --output <file>,
--print-settings (include scenario settings in the heading), --help.
npm test # vitest run (unit + API route tests)
npm run test:watch
npm run pack # client + server bundles → dist/ and dist.tar.gzCI (.github/workflows/ci.yml) runs build + tests on push/PR.
.github/workflows/release.yml tests, then npm run pack: a minified client
bundle, a single-file server bundle (Express + Octavus inlined — no
node_modules), and the static files the server serves. Extract dist.tar.gz
and run node server.js. Supply scenario.json and .env at runtime.
agents/cosmo-mail/ Cosmo copilot (protocol, prompts, settings)
agents/cosmo-mail-character/ In-character correspondents
design-system/ Shared UI submodule
scenario-examples/ Ready-to-run example scenarios
fixtures/ Seed inbox fixtures referenced by scenarios/examples
i18n/ Locale catalogs
lib/ Pure logic (scenario, sessions, i18n, helpers)
public/ Client app (index.html, app.js, app.css)
scripts/ Agent deploy + release pack tooling
tests/ vitest unit + supertest API tests
server.js Express server + API routes
extract-conversations.js Transcript/report generator
PRD.md— product requirements and design.build-plan.md— staged implementation plan.similar-project-details.md— notes on the ChatCPT reference project whose setup CMail clones.
Elastic License 2.0 — see LICENSE.md.