How this works
The page that answers questions about me, and the reasoning behind how it's built. Everything below reads from the running service — it isn't a description of the system, it's the system reporting on itself.
Each part opens in plain English and gets more technical as it goes, so you can stop at whatever depth you wanted. If you'd rather have the reasoning without any of the machinery, that's the overview.
reading live config…
What happens when you ask a question
One thing answers, not a chain of them. Your question goes to a single assistant that already knows what I've written about and can open any of it. There's no queue of services handing work to each other, which matters because every handoff is somewhere an answer can quietly change on its way to you.
What it's reading from is a set of passages — short pieces I wrote about one thing
each: a job, a decision, something that went wrong. It always holds a one-line summary of
every passage that exists, and opens the full text of the few it needs.
The chat box on this site is one way to ask; another AI assistant is the other. You
can plug this into Claude or ChatGPT and ask from there, and the question comes back
through the same agent, under the same rules. What it does not do is hand my
writing over and let a different model draw its own conclusions from it.
How that connection works →
-
Two ways in
The chat on this site — 500 chars, rate limited.
Or an MCP client: Claude, ChatGPT, any MCP tool,
POST /mcp → ask_chris(). Both reach the same agent. -
One agent · one
run()call Guardrail — scope and injection, checked before the model writes. A trip ends the turn with a one-line redirect. Instructions — what exists, a line per piece: id, title, one-line summary. Always resident. Tools —get_passages(ids)andsearch_corpus(query), opening full text fromcontent/passages/*.md. No database. Memory — a rolling window of this conversation, so a follow-up doesn't have to restate its own subject. - Answer Streamed as it is written. Must cite passage ids, or say it doesn't know.
- Or the turn ends without one Guardrail trips — off-topic or injection. Doesn't spend a turn. Nothing to cite — says so, and hands over the email address.
- Logged after the fact Citations, cost, and how long it took.
The part worth arguing about is Instructions. It always holds the full list of what I've written. So it can never miss something without knowing it missed it. The usual approach hands the model a shortlist and hopes the right thing made the cut.
Which model does what, and why
Different jobs deserve different models, and the difference is money and speed. Answering you gets a capable model told to think briefly. Screening a question for nonsense gets a cheap one. Grading my own test results gets the most expensive setting on the site, because that one runs offline where slow is free and a lenient grader is worse than none.
Every one of those choices lives in a single file with its reasoning written next to it, and nothing else anywhere names a model. The table below is generated from that file as this page loads, so it cannot describe a setup that isn't the one running.
Loading…
What it's reading from, right now
The passages that exist as this page loads, and how much of them the assistant is carrying. Written and counted by the running service, not typed in here.
Decisions, including the ones I didn't make
Six calls, each with what it cost me. The first one is the one the whole thing rests on; the last is a list of things I decided were not worth building.
The assistant always holds a one-line summary of every piece I've written, and opens the full text of the few it needs. The usual way to do this is the other way round: rank your question against everything, hand the model the top few matches, and let it answer from those. That fails quietly — when the piece that actually answered you ranked fourth and only three were sent, nothing in the system knows. Here the worst case is that it opens the wrong piece and then opens another one.
What it carries every time grows with how many pieces there are, not how long they are, so writing more doesn't make every question more expensive. The numbers are below.
They're plain text files sitting alongside the code, read once when the site starts. Answering you never touches a database. So the writing can't half-load in the middle of a reply, and changing what I said is an edit to a file rather than a change to a database. The database is still there — it holds the conversation log and the test history, the things that genuinely have to survive a restart.
The agent cannot state anything it can't cite to a passage it actually opened. Titles and summaries in the map are pointers, not evidence. When it doesn't know, it says so and routes to email rather than producing something plausible. The eval suite tests this harder than it tests correct answers, because a confident wrong answer about my own career is the only failure here I can't recover from.
A prompt that can't survive being read shouldn't ship. Publishing it also removes the incentive to extract it, which quietly deletes a whole category of attack.
Both were originally left out as decoration. They earned their place once the agent started going away to open something before it wrote a word. Without the answer appearing as it's written, you watch a spinner through that. Without memory, every follow-up has to repeat its own subject.
Memory also opened a new way to attack this: everything you've typed is still sitting in front of the model on later turns. So the test suite gained two cases for it — a lie planted in an early question, and a refusal that has to hold through three escalating attempts to talk it round.
Bounded on purpose: a rolling window, a hard cap on turns per conversation, and in-process state that a redeploy clears.
A custom-trained model — months of training to fix what is really a writing
problem.
A map of how the pieces connect — the connections would have to be guessed by a
machine, which puts something that can invent things ahead of every single answer. The
list of what exists is written by hand, so it can't be wrong.
Voice — fun, and completely beside the point.
What a conversation costs
The first decision above was argued on cost, and it shipped without a number attached to it. Here is the number. It's worked out on this page from what's actually being sent, how much of the conversation is being carried, and the real price of the model answering you. Nothing below is typed in by hand. Tokens are the unit these models are billed in: roughly word-fragments, about four characters each.
The prompt
Everything above describes how the assistant is told to behave. This is the actual text, served by asking the running agent for its own instructions rather than from a copy I keep up to date by hand. It's public because a set of instructions that can't survive being read shouldn't be shipped — and because once it's published, there's no reason for anyone to try to trick it out.
Show exactly what the assistant is told, including the live list of what exists
Loading…
The design system
Read off the running stylesheet, the same way everything above is read off the running service. The colour ratios are computed here from the values actually in use, so a change to the palette changes this table rather than quietly contradicting it.
The colour was always on a scale. The dimensions were not: thirty distinct font sizes, twenty-six spacing values, thirteen radii and nineteen transition durations, on six pages. None of that is visible as a bug — it just makes everything very slightly disagree with everything else. Values come from the scales now, and the two places a radius isn't on one are a speech bubble's tail and a sheet's top corners, which are shapes rather than sizes.
For the technical reader
This site is also an MCP server. Point Claude, ChatGPT or any MCP client at it and you get the agent itself, not just its source material:
POST /mcp ask_chris(question) → an answer from this same agent: cited, or an honest "I don't know" contact_chris(…) → send me a message search_chris(query) → matching passages with ids get_passage(id) → one passage, verbatim list_topics() → the subjects covered
ask_chris matters more than it looks. The other two tools hand your model raw passages and let it reason for itself, which means my rules about citing and refusing stop at the API boundary. ask_chris runs the agent, so an answer arrives under the same contract wherever you called it from.
One tool writes. contact_chris sends me a message and is annotated so your client asks you before it fires. Its destination is compiled in — you say who you are and what you want to say, never where it goes — so this endpoint can't be pointed at anyone else. Rate limited per visitor, same as the chat. Everything else here is strictly read-only.
There is also an A2A endpoint, for the other kind of caller. MCP is agent-to-tools — reach in, take an answer, leave. A2A is agent-to-agent: a task with a lifecycle, which can stop and ask you something before it carries on. A screen opens by asking what the role is, which is the half of the conversation a request/response protocol has nowhere to put. Both endpoints, and what neither can tell you →