Agent cognitive middleware. A proxy that reads what an agent believes from its own model output, in real time, with no code changes to the agent.
by LatticeAG
Axion is a Cloudflare Worker that sits in front of a model API. Point an agent at it by overriding the base URL. The Worker forwards each request upstream and streams the response straight back with zero added latency. After the response is delivered, it runs a rule-based parser over the assistant text to pull out reasoning fragments (causal claims, assumptions, intentions, cited evidence), stamps them with a confidence score, and stores them per session. A local dashboard reads them back as a timeline.
Named after the axion particle: theorized to exist, never directly observed, detected only through its effects. Agent beliefs are the same. They are invisible but they shape every decision, and this makes them visible.
Agent <-> Axion (CF Worker) <-> Model API
Honest scope. This repo is the Phase 1 observe path plus an opt-in schema enforce mode. It does not detect loops, block tool calls, or build a belief graph. See What is not built before you form expectations. The full product plan lives in BUILD-SPEC.md, which is the source of truth for scope.
- OpenAI-compatible proxy.
POST /v1/chat/completions, streaming and non-streaming. - Anthropic Messages proxy.
POST /v1/messages, streaming and non-streaming. - Passthrough auth. Forward the caller's
Authorizationorx-api-key. Fall back to theUPSTREAM_API_KEYsecret only when it is set. Otherwise return 401. NoBearer undefinedis ever sent upstream. - Zero-latency observe path. The response body is tee'd with
ReadableStream.tee(). One branch streams to the caller untouched; the other accumulates text for extraction inwaitUntil()after delivery. - Belief extraction. Regex patterns pull causal/assumption/intention/evidence fragments. Confidence starts at a per-pattern baseline and is nudged by additive markers, clamped to
[0.1, 1.0]. Every belief is stamped with the session id. - Durable session store. A Durable Object appends each response's beliefs as a batch to Durable Object storage.
GET /api/beliefs/:sessionIdreturns a flat chronologicalExtractedBelief[]. - Local dashboard. Paste or link a session id, see its timeline. Filter by type, minimum confidence, or low-confidence only.
- PolyVerdict enforce mode (opt-in). Send a JSON Schema and the Worker validates, coerces types, and retries the model up to 3 times before returning. Off by default.
- Tests and CI. Vitest suite,
npm test/npm run check, GitHub Actions on push and PR.
These appear in older drafts and in the roadmap. None of them are in the code:
- Belief DAG, parent/child edges, root-cause backtracking. The store is a flat timeline.
BeliefNode/BeliefDAGtypes exist but are marked@plannedand have no runtime. /api/sessionssession registry. The dashboard takes a pasted session id instead.- Axion Loop (loop detection) and Axion Gate (tool-call blocking).
- Semantic PolyVerdict, second-model verification, hallucination checks.
- Schema registry Durable Object, schema hash cache.
- Hosted multi-session SaaS dashboard.
| Layer | Name | Status | What it does |
|---|---|---|---|
| 1 | Axion Lens | Shipping (observe) | Extracts reasoning fragments from each response into a per-session timeline. Read-only. |
| 2 | Axion Loop | Planned | Detect when an agent is cycling the same reasoning and intervene with feedback. Not implemented. |
| 3 | Axion Gate | Planned | Verify tool calls before execution and block bad ones. Not implemented. |
Lens is read-only and cannot change agent behaviour. PolyVerdict enforce mode is a separate opt-in path that does change output (it can retry the model and coerce types), triggered only when the caller supplies a schema.
Agent (OpenAI- or Anthropic-compatible, sends x-axion-session)
|
v
Axion Worker (Cloudflare)
|- auth.ts resolve passthrough / server-key credentials, or 401
|- providers/ match POST /v1/chat/completions or POST /v1/messages
|- stream.ts ReadableStream.tee: caller branch + extraction branch
|- content.ts normalize SSE deltas / non-stream body to assistant text
|- extraction.ts waitUntil -> extractBeliefs({ sessionId }) -> DO
|- polyverdict/ opt-in enforce: validate + coerce + retry <=3
|
v
Model API (UPSTREAM_API_URL, default https://api.openai.com)
State: SessionDurableObject appends belief batches to DO storage.
Read: GET /api/beliefs/:sessionId -> { sessionId, beliefs: ExtractedBelief[] }
UI: GET /dashboard
Requires Node.js 20+ and a Cloudflare account for deploy.
npm install
cp .dev.vars.example .dev.vars # optional: set UPSTREAM_API_KEY
npm run dev
export OPENAI_BASE_URL=http://localhost:8787
# send header x-axion-session: my-session on your agent's requests
# dashboard: http://localhost:8787/dashboard?session=my-session
npm run checkThe proxy uses passthrough auth. If your agent already sends its own API key, you do not need UPSTREAM_API_KEY. Set it only if you want the Worker to hold the key and let callers omit it.
Anthropic agents route through POST /v1/messages:
export ANTHROPIC_BASE_URL=http://localhost:8787
# Claude Code and other Anthropic Messages clients hit POST /v1/messagesDeploy your own instance:
npx wrangler deploy
# -> https://your-axion-worker.devBeliefs are grouped by session. Send x-axion-session: <id> on agent requests to correlate a multi-turn run. If the header is absent the Worker generates a UUID per request and returns it in the x-axion-session response header, so a single call is still captured but multi-turn correlation needs the header.
Open http://localhost:8787/dashboard, paste the session id, and press Load. The id also reads from ?session= in the URL and from localStorage (axion.sessionId).
The beliefs API is unauthenticated in Phase 1. Anyone with a session id can read that session's beliefs. Treat the id like a capability token. See SECURITY.md.
Enforce mode is off unless the request carries a schema. Two triggers:
- Header
x-axion-schema: <JSON Schema as JSON>(URL-decoded if needed), or - Body
response_format: { "type": "json_schema", "json_schema": { "schema": { ... } } }.
When triggered, the Worker forces a non-streaming upstream call, parses the assistant JSON (stripping Markdown fences), validates it against the schema, and coerces primitive types ("42" to number, "true"/"false" to boolean, number to string). On a violation it appends the errors as a correction message and retries, up to 3 attempts total. On success it returns a provider-shaped JSON response. After 3 failed attempts it returns HTTP 422 with the violations. Lens still extracts from the delivered text.
The schema subset covers type, properties, required, items, enum, and nesting. Unknown keywords are ignored. There is no semantic or second-model verification.
Example (OpenAI path):
curl http://localhost:8787/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "x-axion-schema: {\"type\":\"object\",\"properties\":{\"score\":{\"type\":\"number\"}},\"required\":[\"score\"]}" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Rate this 1-10 as JSON."}]}'| Type | Trigger phrases | Baseline confidence |
|---|---|---|
| Causal | "because X", "because of X", "since X", "due to X", "as a result of X" | 0.8 to 0.85 |
| Assumption | "assuming X", "presumably X", "I'll assume X", "if X then Y" | 0.6 to 0.65 |
| Intention | "I'll X", "I'm going to X", "let me X", "I should X", "I plan/intend to X" | 0.75 |
| Evidence | "based on X", "according to X", "from the X", "the error says X" | 0.7 to 0.85 |
Confidence starts at the baseline and each marker found near the match adds its delta:
- definitely / certainly / absolutely: +0.2
- probably / likely: +0.1
- might / could be / possibly / may: -0.2
- not sure / uncertain / unsure: -0.3
The result is clamped to [0.1, 1.0]. This is a linguistic heuristic, not a truth signal. It reflects how the model hedged, nothing more.
axion/
|- src/
| |- proxy/
| | |- index.ts Worker entry: routing, observe + enforce branches
| | |- auth.ts passthrough / server-key credential resolution
| | |- stream.ts ReadableStream.tee + SSE parsing (OpenAI + Anthropic)
| | |- content.ts assistant-text normalization per provider
| | |- extraction.ts waitUntil glue to extractBeliefs + DO store
| | |- beliefs.ts GET /api/beliefs/:id -> DO
| | |- routes.ts dashboard static asset handler
| | |- providers/ openai + anthropic adapters, matcher, interface
| | |- types.ts Env + proxy request/response types
| |- lens/
| | |- patterns.ts regex belief patterns + confidence markers
| | |- extract.ts extraction engine (additive confidence, clamp)
| | |- types.ts ExtractedBelief; BeliefNode/BeliefDAG (@planned)
| |- polyverdict/
| | |- schema.ts JSON Schema subset validator + coercion
| | |- enforce.ts trigger detection, retry loop, hint injection
| | |- types.ts enforce types
| |- state/
| | |- SessionDurableObject.ts append batches, flatten on GET
| | |- sessionBeliefs.ts pure flatten / sessionId helpers
| |- dashboard/ React via CDN, no build step
|- BUILD-SPEC.md locked scope (source of truth)
|- SPEC.md TECHNICAL.md SPEC-PolyVerdict.md PLAN.md
|- wrangler.toml tsconfig.json package.json
- Runtime: Cloudflare Workers.
- State: Durable Objects, one per session, backed by Durable Object storage.
- Extraction: regex rules only, no model call, sub-millisecond.
- Dashboard: React from CDN, no bundler, served as static assets.
- Runtime dependencies: none.
wrangler,typescript, andvitestare dev-only.
- Loop (Phase 2) and Gate (Phase 3) are not implemented.
- The beliefs API is unauthenticated. A session id is a read capability. See SECURITY.md.
- Session storage is unbounded. Beliefs append to Durable Object storage and are never trimmed. Long-lived session ids will grow without limit. There is no rate limiting yet.
- Extraction is a regex heuristic. It misses reasoning that does not use the trigger phrases and will mis-parse unusual phrasing. Confidence reflects hedging words, not correctness.
- Without an
x-axion-sessionheader each request lands under a fresh UUID, so multi-turn correlation depends on the caller sending a stable id. - The dashboard loads React from a CDN and needs internet access for that page. The proxy itself does not.
- Enforce mode always returns non-streaming JSON, even if the client asked to stream. This is intentional so the full payload can be validated.
- Source: github.com/LatticeAG/Axion
- LatticeAG: latticeag.vercel.app
MIT. See LICENSE.
LatticeAG - Agents, together.