Build Compass: a workspace for people and agents
The deep track: build Compass, a workspace where people and agents share chats, a board and documents, and learn the runtime's contract by building each part of it, one primitive at a time.
Compass is a workspace where people and a team of AI agents share chats, a board and documents. In this track you build it yourself, for one person on their own machine, in 41 chapters.
It's the deep dive into the contract the runtime keeps for every caller, whichever way they come in:
- Who is asking, for whom, and where. Every call carries an actor, such as you or an agent working for you, and its work happens in a space.
- What can be done. Each thing anyone can do is an Action. A click, an agent's tool and a script call the same one.
- What anyone sees. A View shows the data in one place: the browser, the terminal or the model's prompt. Observed Views stay live.
- The rules, checked at the data. They filter every read and write, whichever way the call came in.
- The record. Every write and every Action call is an event that names who acted.
You import 2 of Copilotz's plugins as they are: models, which talks to model providers, and chat, which runs conversations, agents and their tools. You rebuild the rest of Compass's plugins yourself: identity, spaces, the board, documents, memory and more. Each keeps the contract of Copilotz's plugin of the same name (its collections, Actions, events, Views, resources and tools), so Copilotz's web and terminal front ends work with yours in its place. You leave out only what exists because several people share an app: invitations, roles, orgs and billing. The last part runs Compass on Copilotz's own plugins, which have them.
By the end you know how the runtime and its plugins work, because you built them. You drag a card in the browser. North reads the same board in its prompt, a script moves a card over the REST API, and an assistant reaches the board over MCP. All of them go through the same Actions, under the same rules, on the same record.
How Copilotz works, in one minute
An app is a list of plugins. A plugin is a plain object made of 6 primitives, and the runtime knows nothing else:
| Primitive | What it is |
|---|---|
| Collection | documents with a JSON Schema, read and written with the MongoDB collection API; every write records an event in the same commit |
| Action | something that can be done, by name, with a JSON Schema input and an allow saying who may call it; every call is on the record |
| Processor | what happens when events happen: it hears the events it names, runs at least once per event, in order per key |
| View | how data presents itself in one place: text for the model (the agent's prompt is a View), Ink for the terminal, React for the web |
| Resource | configuration as named plain objects, by kind (agents, tools, provider…); a later plugin patches an earlier one's |
| Adapter | one implementation of a port, by name: a model provider's protocol, an embedder |
Plugins reach each other only through the runtime, by name. They call each other's Actions, plug into each other's resource kinds, and hear each other's events. One owner writes everything to one store (in memory, SQLite or PostgreSQL), so every change is on the record, and every screen follows it live.
Each part of the contract has chapters of its own:
- Who and where: a who, a route and a rule for one person (Chapter 3), spaces (Chapters 7 and 8), and sign-in for many people (Chapter 40).
- Actions: the board's 7 Actions, the only way to change it (Chapter 9), and the tools that call them for North (Chapter 11).
- Views: a charter in North's prompt (Chapter 2), the board on the web and in the terminal (Chapter 10), and chat's history View replaced (Chapter 17).
- The record: the events one message leaves (Chapter 1), the store's tables (Chapter 5), and every save a version that says who, what and when (Chapter 12).
- Every way in: the REST API, the client and MCP (Chapters 33 to 35).
Three things people often get wrong, and the track shows each one running:
- Two kinds of events. A collection write records
<collection>.created,.updatedor.deleted. An Action call recordsaction.started, thenaction.completedoraction.failed. A plugin can also declare events of its own (chat'sturn.requested) and emit them. Callers call Actions; events are what Actions and writes produce. - Watching events and watching Views are different.
runtime.subscribehears every event as it's committed; Processors react to the ones they name.runtime.observerenders a View, and renders it again when what it read changes: that's how screens stay live. - Queries read; Actions write. A collection's named queries never write. Anything that changes data, however many documents it touches, is an Action.
What you need
- Node 24 or newer (the guide uses Node; Copilotz also runs on Deno, Bun and Cloudflare Workers).
- An OpenAI API key is optional. Without one, agents answer with Copilotz's offline fake model, and every test in the guide runs without a key.
- The track and its course are built on Copilotz
1.0.0-rc.29, and install that version. To move your Compass to a later release, follow the upgrade notes for rc.30 to rc.32 in the CHANGELOG: files upload on their own,allowis asked of tools too, and an agent's work reaches only the space it works in.
mkdir compass && cd compass
npm init -y && npm pkg set type=module
npm install copilotz@1.0.0-rc.29 react@19 react-dom@19
npm install -D typescript @types/node @types/react @types/react-dom esbuildNode runs TypeScript files as they are, so there's no build step for the server or the terminal. The editor and tsc need to know imports end in .ts; this tsconfig.json is the guide's:
{
"compilerOptions": {
"target": "ES2024",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2024", "DOM", "DOM.Iterable", "DOM.AsyncIterable"],
"types": ["node"],
"jsx": "react-jsx",
"jsxImportSource": "react",
"allowImportingTsExtensions": true,
"rewriteRelativeImportExtensions": false,
"verbatimModuleSyntax": true,
"erasableSyntaxOnly": true,
"strict": true,
"skipLibCheck": true,
"noEmit": true
}
}And the scripts the chapters run, in package.json (each arrives with the chapter that needs it):
"scripts": {
"hello": "node --env-file-if-exists=.env hello.ts",
"terminal": "node --disable-warning=ExperimentalWarning --env-file-if-exists=.env terminal.ts",
"dev": "node --disable-warning=ExperimentalWarning --env-file-if-exists=.env server.ts",
"test": "node --disable-warning=ExperimentalWarning --test --test-timeout=60000 --test-concurrency=1 \"plugins/*/tests/*.test.ts\" \"tests/*.test.ts\"",
"check": "tsc --noEmit -p ."
}A .env beside them may hold OPENAI_API_KEY=sk-…; the scripts read it if it's there.
The code at the end of each chapter comes with the Copilotz course: one git repository with a tag per lesson (lesson-1.1, lesson-1.2…), and each chapter is also one of its video lessons. Chapters show the files that matter; each ends with what else changed, and git diff lesson-a lesson-b shows all of it.
The chapters
Part 1: an agent of your own
- One agent, one message: compose a runtime from two of Copilotz's plugins and one of your own, send an agent a message, and read the events it leaves.
- North, in your terminal: move North into a plugin of your own, laid out by primitive; give the team a charter as a View in North's prompt; talk to North in Copilotz's terminal and see exactly what it's sent.
- Compass in the browser: rebuild Copilotz's identity plugin for one person on their own machine (a collection, an Action, a who, a route and a rule), then serve Compass's web page in its own look.
Part 2: your own plugins
- Today, every turn: tell every agent today's date with a turn context, and see why a prompt has a stable part and a part that changes: the provider's prompt cache.
- One store: keep Compass in one SQLite file: what the store's tables hold, each collection as a SQL view, and why the store has one writer at a time.
- Chats that name themselves: write a Processor: it hears chat's message.created events and names new chats with a small model. Then test it with a scripted model, swapped in through resources and an adapter.
- Spaces: rebuild Copilotz's spaces plugin for one person by its contract: what chat and other plugins ask of it, a rule that keeps shared documents in real spaces, and a parity test against Copilotz's own.
- Spaces on the web: give spaces a web front end: React Views the web shell places by name, a page for each space, and resource kinds that let other plugins add tabs without knowing your code.
Part 3: the board
- The board's data and Actions: rebuild Copilotz's boards: two collections that must always agree, seven Actions that are the only way to change them, and transactions that commit what several documents must agree on together.
- The board on screen: show the same board in two places with Views: a Board tab on every space's page, plugged in through a resource kind, and a terminal View printed by a command. Both stay live.
- The board for North: the third place the board shows is the model's prompt: six tools, offered wherever a conversation is in a space, that call the same Actions people use, and read back as plain text.
Part 4: documents
- Documents and their versions: rebuild Copilotz's documents plugin for one person by its contract: Markdown in a space, every save a version that says who, what and when, an old version brought back as a new one, and a stale save refused.
- Saves that merge, or refuse: two saves from the same version merge line by line, a three-way merge against the version both started from, and are refused only where they touch the same lines; and who has a document open is live, in a collection kept in memory.
- Documents on the web and in the terminal: the same documents shown by Views in two places: a Documents tab on every space, a page to write and save each version, live as others save, and /doc in the terminal.
- Documents for North: documents' third reader is the model: five tools, offered wherever a conversation is in a space, that read and write through the same commit as your saves, each change a version by the agent, for you.
Part 5: long conversations
- A line for every message: rebuild Copilotz's history plugin, starting with its Processor: as messages leave the recent tail, each gets a line, and pairs of lines merge up a binary tree, with a cheap model only for what's long.
- The outline in North's prompt: replace chat's history View: an agent is sent the summary lines first, then the recent messages whole, and chat reads only those, from where the history plugin says they start.
- Zoom: a summary line never replaces its messages: the zoom tool, offered once a thread is summarized, opens a line into the two it was made from, down to the message itself, whole.
Part 6: memory
- Memories found by meaning: rebuild the store half of Copilotz's memory plugin: statements about a person, an agent, a conversation or everyone, kept with an embedding, found by meaning and by words, and updated instead of repeated.
- Recall in the prompt: memory reaches agents as three of chat's contexts: how to use it in the cached system part, the core memories when a conversation begins, and each turn the memories relevant to what was last said.
- Remember, recall, forget: three tools with Copilotz's names and inputs let agents keep, search and drop memories; the charter says what's worth keeping, and the terminal and the web show what's kept.
- Memories on their own: rebuild Copilotz's memoryExtraction: a Processor counts each conversation's shared messages and, every few, asks a cheap model to write down what's worth keeping, through the same keep as remember.
Part 7: the team
- East, South and West: North gets a team: three more of the app's own agents, each with its own way of working and its own model alias, in every space, with the charter in each one's prompt.
- Asking in parallel: North brings teammates in with chat's send_message: questions that don't depend on each other go out in one step, the teammates work at the same time, and North resumes once, with every reply.
- Reading other conversations: Compass's coordination plugin, rebuilt for one person: two tools that let an agent list and read the other conversations of its space, through chat's own queries, storing nothing.
Part 8: skills and routines
- Skills: rebuild Copilotz's skills plugin for one person: know-how an agent reads only when a task calls for it, listed by name in the cached part of its prompt and read with one tool.
- Routines on a schedule: rebuild Copilotz's routines for one person: a message sent to an agent on a schedule, with time as one more caller of Actions, every run recorded, and tests that move time with a manual clock.
- Agents that schedule: "Remind me every Monday" from a chat: four tools that make, list and cancel routines as the person the agent works for, post back where they were asked, and let an agent name a run's chat for what it found.
Part 9: the web for agents
- Search, an adapter port: rebuild Copilotz's web tools for one person, starting with web_search: a tool that knows no search engine and asks an adapter port, so DuckDuckGo, Serper or an offline fake answers, all through the runtime's fetch.
- Reading a page safely: read_page fetches a page an agent chose and hands back its main text, marked as data: http(s) only, private and metadata addresses refused on every redirect, within a time, a size and a kind of content, and a few pages per turn.
Part 10: testing
- Fakes at the edges: test Compass as a person uses it, offline and in milliseconds: the app as app.ts makes it, run for real, with fakes only where it meets the world: a scripted model, a manual clock, and a model provider served on your machine.
- Parity: what every rebuilt plugin's parity test does, in one small helper: the same scenario on Copilotz's plugin and on yours, with ids and times taken out; and the parity test identity was missing, with testWho saying who each request is.
Part 11: other ways in
- The REST API: other programs reach Compass: the runtime answers /api/ itself, as whoever identity's who says a request is, so a script on your machine reads your board and moves a card through the same Actions and rules as a click.
- The client: Copilotz's client, connect, reaches the running server as plugin code reaches the store, over the socket or over HTTP; its watch hears every commit, so a script prints your board again whenever a card moves.
- Your board over MCP: an assistant on your machine (Claude Code, any MCP client) calls the board's tools, the same ones North uses: Compass's own mcp plugin, with Copilotz's contract, for one person, each call written in a chat of yours.
Part 12: shipping
- PostgreSQL and blobs: run the same Compass on PostgreSQL when DATABASE_URL says so, and keep large values once, as files in a folder of their own, with Copilotz's postgresStore and fileBlobs.
- Watching it run: Copilotz measures itself: the runtime's jobs and commits, and every model call by OpenTelemetry's GenAI conventions. Give it a meter, read the numbers at /metrics as Prometheus text or in the terminal, and send them to an OpenTelemetry collector.
- A computer for your agents: run Copilotz's computers plugin, unchanged, on Compass's spaces: a machine you connect with a one-time code, where North gets a terminal, jobs, files, a browser and web search, and asks before it runs anything. One change to spaces makes it fit: you are a member of your spaces, the way Copilotz's keep it.
Part 13: from one person to many
- Swapping in Copilotz's plugins: every plugin you rebuilt kept Copilotz's contract, so one setting runs Compass on Copilotz's own plugins instead, the real Compass's. A test runs the same end-to-end scenario on both lists, and shows where they differ on purpose.
- Sign-in and people: on Copilotz's identity, a request is whoever its session proves, so people sign in: by email and a password, with codes written to the log on this machine, or by name. Then a space gets members: Ana invites Bruno to Launch, he accepts, and he reads its board.
- Many orgs and billing: one address, many orgs: a directory runtime signs people in and knows their orgs, and each org is a Compass of its own, opened when used and closed when idle. With Copilotz's usage ledger, what each org's agents use is recorded and priced in that org alone.