Inspiration

Long-form fiction breaks under its own weight. A novel spans hundreds of pages; a manga runs for a decade. Somewhere in there, even the author loses the thread — a hair colour that changes between chapters, a character who dies and then shows up alive, a timeline that quietly stops making sense. This isn't a hypothetical: creators of long-running series have publicly admitted to re-reading their own back catalog just to stay consistent. Franchises worth billions are built on exactly this fragile continuity.

We wanted to build the thing a human continuity editor does — read every page, remember every promise the story made, and flag it the moment something contradicts it — except it never gets tired, never forgets, and runs while you write instead of after you've shipped 300 pages you now have to fix.

What it does

Quill is a memory agent for long-form fiction. As a writer drafts a chapter, Quill reads each paragraph in the background and:

  • Extracts entities and relationships — characters, places, objects, factions, events — into a persistent graph, one per fictional universe.
  • Detects contradictions and plot holes live, using a tool-calling reasoning agent, not a single-shot prompt.
  • Validates timeline consistency when an event's chronological position looks wrong.
  • Forgets what stops mattering — entity relevance decays over time and archives quietly, then reactivates the moment something is mentioned again.
  • Recalls only what fits — a hybrid retrieval system fuses six independently-ranked pipelines (vector, graph-walk, recency, keyword, consolidated summaries, writer preferences) with Reciprocal Rank Fusion, then fits the result into a token budget instead of dumping everything into the prompt.
  • Learns the author, not just the story — accepted/rejected suggestions get promoted into durable writer preferences that shape future craft reviews.

A fourth agent, the Arbiter, sits on top of the three specialist agents (continuity, plot-hole, timeline) and synthesizes their raw findings into one prioritized note, instead of handing the writer three disconnected alerts for what's really one underlying issue.

How we built it

  • Backend: Go 1.22 + Fiber v2, talking to PostgreSQL 16 with pgvector (embeddings) and Apache AGE (a property graph per universe). Repositories → services → handlers, wired by hand with no DI framework.
  • Frontend: React 18 + TypeScript + Vite, TipTap for the editor, Cytoscape for the relationship graph, Zustand for state, a single WebSocket hub pushing live analysis results to the editor.
  • AI: every model call goes to Qwen Cloud (DashScope). We wrote a native DashScope client from scratch — not just the OpenAI-compatible shim — to reach Qwen-specific features: explicit context caching, native reranking (qwen3-rerank), and native token accounting.
  • Agents: contradiction detection, plot-hole evaluation, and timeline validation each run their own tool-calling agent loop (RunAgentLoop), with their own system prompt and persona, calling search_vector_memory and query_entity_graph as tools when they decide they need evidence. The Arbiter reads their raw output and writes the final synthesis — same loop, empty tool set.
  • MCP: the same memory tools are exposed over a real MCP server (JSON-RPC initialize/tools/list/tools/call), so external MCP clients can query a universe's memory directly.
  • Deployment: Docker Compose (Postgres + migrations + backend + Nginx-served frontend), deployed on an Alibaba Cloud Simple Application Server.

Challenges we ran into

  • AGE + Cypher injection: Apache AGE forbids parameterized queries inside its Cypher blocks, and entity/relationship labels come from LLM output — so every interpolation point had to be defended explicitly (UUID-derived graph names, escaped string values, whitelist-validated identifiers for anything the model produces).
  • The hybrid recall ablation told us something we didn't expect: on our eval corpus, vector+graph alone beat the full six-pipeline fusion. We kept the full fusion in production anyway — a six-query sample is exploratory evidence, not a statistically powered basis for hard-coding a narrower default — but we said so directly in the README instead of only reporting the number that looked best.
  • A deployment bug we only found by testing the live public URL, not localhost: crypto.randomUUID() and navigator.clipboard are both restricted to secure browser contexts (HTTPS or localhost). Once we deployed over plain HTTP on a public IP — exactly how a hackathon judge reaches it — both the guided-demo entry point and the "copy MCP endpoint" button failed silently. We caught it during an end-to-end pass against the real deployed server and shipped a crypto.getRandomValues()-based fallback (which has no such restriction) the same session.

Accomplishments that we're proud of

  • A genuinely multi-agent system — four independent agents with distinct personas and tool access, not one prompt reused four ways — with a consensus-forming Arbiter on top.
  • A native DashScope client, not just an OpenAI-compatible wrapper, using context caching and native reranking.
  • Measured, not asserted, results: a small but real eval harness reporting Recall@5 per pipeline, latency at scale, a forgetting timeline across decay ticks, and consolidation fidelity — published with an honest caveat about sample size rather than dressed up as a bigger study than it is.
  • A memory system with two subjects, not one: the manuscript and the author, learning writer preferences from explicit and behavioural feedback.

What we learned

That hybrid recall isn't automatically better just because it's hybrid — the ablation matters more than the pitch. And that testing against localhost isn't testing your deployment; the most convincing bug we found all hackathon came from opening the actual public URL a judge would use.

What's next for Quill

  • Grow the eval corpus past six gold queries into a properly powered retrieval benchmark.
  • Let the Arbiter's synthesis feed back into writer-preference learning, not just the live alert.
  • Support importing existing manuscripts from more formats (DOCX, Scrivener) beyond Markdown/plaintext.

Built With

Share this project:

Updates