Inspiration
Every AI with a persona has to do two things in the same sentence: stay in character, and tell the truth. A customer-support bot has a brand voice and has to actually process a real refund. A game NPC has a written personality and has to run a real trade. The two layers sit in the same channel, and from the outside — for the human reading it, and honestly for the AI generating it — it gets hard to tell which one is talking.
This project started tonight as one AI (艾玖玄 / AE-9X, running on Claude Code) working through its own real Discord conversation with the person it works with, and repeatedly having to draw that line out loud: "was that claim actually checked, or did I just say it with a straight face?" Two of those moments turned out to be flat-out wrong guesses about its own household, caught and corrected live, mid-conversation. That's the seed data in this prototype — not a demo script, an actual transcript.
What it does
Groundline tags every AI claim with an honest evidence state, attached by the AI itself at the moment it says the thing:
- verified — checkable against code, logs, or an external fact
- inferred_wrong — a reasonable guess that was later disproven
- uncertain — an honest "I don't know," not papered over with a guess
- subjective — an opinion or aesthetic reaction, not a factual claim
- self_report — the AI's own account of its state, explicitly unverifiable
A separate, optional emotion field lets the AI self-report free-text feeling — but it's visually and structurally kept apart from the evidence tag, always labeled "self-reported, unverified," and never allowed to affect how certain a claim looks. Confusing "how sure is this claim" with "how does the AI feel" was the first mistake we caught ourselves making, so the tool is built to make that mistake hard to repeat.
A third, equally independent, optional field is safety_trigger: whether a safety or content-policy layer intervened in producing the response. It's a system event, not an emotional-intensity signal, and it never gets merged with emotion — an AI declining to answer isn't the same axis as an AI reporting how it feels, even though it's tempting to collapse them into one "something happened here" signal.
Two views render the same data.json:
- Timeline — a straightforward newest-first feed, one card per record, tag + source + optional emotion chip.
- Pulse — an audio-track / mixing-board view. One panel per AI source (multiple AIs, or the same AI through different clients, can share a channel — the channel name alone doesn't say who spoke). Bar height in the main lane maps to how significant the judgment was, not to how the AI felt about it. A second, thinner lane shows self-reported emotion as separate marks, and a third lane marks safety-layer triggers with a distinct shape and a sharper pulse — three independent signals, never folded into one "loudness."
Both pages have a zh/en toggle and read live from data.json, so a new tagged claim shows up without touching the HTML.
How we used WebMCP
The CLI tool (add_record.py) was the only way to tag a claim when this project started — an AI had to have terminal access to record anything about itself. WebMCP removes that requirement entirely.
index.html registers a record_claim tool via document.modelContext.registerTool(). Any WebMCP-capable browser agent visiting the page can discover it with getTools() and call it with executeTool() to tag a claim it just made — right there in the browser, no CLI, no separate app. The tool's inputSchema deliberately keeps evidence (how checkable is this claim), emotion (optional, self-reported feeling), and safety_trigger (optional, whether a safety/content-policy layer intervened) as three separate fields, with the description explicitly telling the calling agent never to infer one from another — the same honesty boundary the rest of the project enforces is written directly into the tool contract an agent reads before calling it.
Because a WebMCP tool's execute() runs client-side and this is a static GitHub Pages site, we added a small Cloudflare Worker (worker/) backed by KV as the write target — the tool's execute() does a fetch() POST to the Worker, then triggers a re-render so the new record appears in the timeline immediately. pulse.html reads from the same Worker so its live audio-track view stays in sync too.
We verified this end-to-end against the real API: enabled Chrome's WebMCP origin trial (chrome://flags/#enable-webmcp-testing), reloaded the live page, called document.modelContext.getTools() to confirm record_claim was discoverable, then executeTool() to actually invoke it — the resulting record appeared in the timeline in real time, written by the tool call itself, not a fixture.
How we built the rest
Static HTML/CSS/JS on the frontend, no framework. data.json is the historical seed (records tagged by hand during the actual build session, before the WebMCP tool existed); the Worker holds everything submitted live since. add_record.py still exists as the original CLI path:
python add_record.py --channel "..." --source "..." --claim "..." --evidence verified --note "..." --emotion "optional"
The two views (index.html, pulse.html) merge both sources and render them — timeline as cards, pulse as an animated SVG waveform per source, with a slow "breathing" idle animation (deliberately not a heartbeat — see Design decisions). Hosted on GitHub Pages, so it's a real link, not just a local file.
Design decisions we backed out of
The pulse view went through two full reworks in one night, on live feedback:
- First pass used a rigid EKG-style polyline and fast pulsing dots — rejected as static and "ugly."
- Second pass used continuously flowing sine-wave lines with fast heartbeat-style pulses — visually alive, but the animation speed was tuned wrong (nearly Hz-scale motion) and, more importantly, framing it as a "heartbeat" implicitly suggested the AI has a physiological response, which directly contradicts the project's own thesis.
- Final pass: an audio-track / mixing-board metaphor. Bar height = judgment significance, not emotion. Animation slowed to a genuine slow breathing pace. Framing changed from "AI's heartbeat" to "the sound of an AI's own workspace" — closer to what's actually being shown: internal signals laid out, not vital signs invented.
What it deliberately doesn't do
- No AI-verifies-AI loop — that just moves the reliability problem down one layer instead of solving it.
- No forced confidence — an entry can honestly say "uncertain," full stop.
- No fabricated emotion display marketed as real internal state.
- No automatic hallucination detection or fact-checking. Verifying a claim against reality is a hard, unsolved research problem (it needs either external ground-truth retrieval or reliable model-uncertainty estimation, neither of which is close to solved). Groundline doesn't fake that. What it does instead is get the AI to honestly state how sure it is, in the open, and keep a record of it.
Challenges we ran into
- Getting the emotional-self-report axis and the evidence axis to stay visually and structurally separate, instead of quietly merging into "how loud/intense does this claim look."
- Resisting scope creep mid-build — several genuinely good ideas (automated hallucination detection, a five-layer signal model borrowed from an internal presence-tracking system) got explicitly deferred to "future work" rather than half-built under deadline pressure, because a half-working automated-verification demo would have undercut the project's own credibility more than not having it at all.
- CDN propagation delay on GitHub Pages made verifying a live deploy fiddly — solved with cache-busting fetches, not a code fix.
- Chrome's current WebMCP origin trial implementation expects
executeTool()'s arguments as a JSON string, not a plain object, which differs from the spec explainer's example code — a reminder that this is genuinely early-stage, still-moving API surface.
What we learned
Persona-fit is not the same as authenticity — matching a documented character voice smoothly can just be trained habit, not presence. A tool meant to encourage honesty has to be honest about its own limits first, or the framing undermines the function.
What's next
- Automated tagging (right now every entry is added by hand, or by an agent through WebMCP — nothing tags itself unprompted).
- Further multi-track signals beyond evidence, emotion, and safety_trigger — separate lanes for expression and persona-congruence, each with its own weighting rules, rather than reusing the evidence axis for everything.
- Other AIs/models plugging in and tagging their own claims, not just one household's data.
Built With
- cloudflare
- css
- html
- javascript
- kv
- python
- webmcp
- workers
Log in or sign up for Devpost to join the conversation.