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.
The open, local-first agent observability + collaboration stack: record, replay, diff, and inspect agent behavior - then plan and ship with humans in the same workspace.
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, a regex lens pulls reasoning fragments from visible assistant text, stamps them with a linguistic confidence score, and stores them per session.
What ships today is a regex lens behind a transparent proxy. It is not a LangSmith replacement, not a prompt playground, not an eval harness, and not a hosted SaaS. Inspection, not another trace backend.
Agent <-> Axion (CF Worker) <-> Model API
- OpenAI Chat Completions proxy.
POST /v1/chat/completions, streaming and non-streaming. Default basehttps://api.openai.com. - Anthropic Messages proxy.
POST /v1/messages, streaming and non-streaming. Default basehttps://api.anthropic.com. LegacyUPSTREAM_API_URLoverrides OpenAI only. - Passthrough auth. Forward the caller's
Authorizationorx-api-key. Fall back to theUPSTREAM_API_KEYsecret only when it is set. Anthropic Bearer tokens are also copied tox-api-key. NeverBearer undefined. - Zero-latency observe path.
ReadableStream.tee(): one branch to the caller untouched, extraction inwaitUntil()after delivery. - Eight belief types. causal, assumption, intention, evidence, uncertainty, contradiction, planning, self-correction. Baselines live in
BELIEF_TYPE_CONFIDENCE_BASELINES. Markers nudge, then clamp to[0.1, 1.0]at extraction. Read-time decay is0.9 ^ turnsAgo(newest batch is 0). Decay may go below 0.1. Stored batches keep original confidence. - Sharded session store. Up to 200 batches per session (
AXION_MAX_BELIEF_BATCHES, clamped[20, 1000]). Legacy single-key"beliefs"arrays migrate on write. Registry holds at most 5000 sessions. - Read APIs behind a token.
AXION_READ_TOKENviaAuthorization: Bearer <token>orx-axion-read-token. SSE also accepts?readToken=because EventSource cannot set headers. Local escape:AXION_OPEN_READ=true. Missing token fails closed. - Session registry.
GET /api/sessionsreturns 20 records per page. Search, export, usage, and live SSE exist and require the same token. - Public payloads omit raw model text.
GET /api/beliefs/:idsendsrawText: "". JSON export includesbatches[].rawTextonly with?includeRaw=1. Markdown never includes raw source. - Local dashboard. Paste or link a session id. Send the read token from a local field. Full session picker, SSE live-append, usage panel, and export buttons are not in this UI yet.
- PolyVerdict enforce mode (opt-in). JSON Schema validate/coerce, retry up to 3 times. Off by default. Usage on success is summed across attempts.
- Native tool-call capture. OpenAI
tool_callsand Anthropictool_usebecomeObservedActionrecords on the same batch. Beliefs GET returns{ sessionId, beliefs, actions }. - Signed belief-batch webhook. After a successful store, Axion POSTs
axion.belief_batch.v1toAXION_BELIEF_WEBHOOK_URLinwaitUntil. HMAC header whenAXION_WEBHOOK_SECRETis set. - Health.
GET /api/healthis unauthenticated liveness.GET /api/readyrequires the read token and pings the registry. - Read rate limits. Cache API windows: 30 search, 6 export-all, 120 other authenticated reads per token per minute.
- Tests and CI.
npm run checkistsc --noEmit && vitest run. Node 20+.
- Belief DAG, parent/child edges, root-cause backtracking. The store is a flat timeline.
BeliefNode/BeliefDAGtypes are marked planned and have no runtime. - Axion Loop (loop detection) and Axion Gate (tool-call blocking).
- Semantic PolyVerdict, second-model verification, hallucination checks.
- Hidden chain-of-thought recovery. The lens reads visible assistant text only.
- A quality score. Confidence is linguistic extraction confidence, possibly decayed.
- PII classification. Secret regex redaction is a bounded detector, not completeness.
- Hosted multi-session SaaS, billing, or an npm
axion/lenspackage. This repo is a private Worker. Deploy your own instance.
| Method + path | Auth | Notes |
|---|---|---|
POST /v1/chat/completions |
upstream passthrough | OpenAI observe or enforce |
POST /v1/messages |
upstream passthrough | Anthropic observe or enforce |
GET /api/health |
none | { ok, name, version } |
GET /api/ready |
read token | { ok, registry: "up"|"down" } |
GET /api/beliefs/:id |
read token | { sessionId, beliefs, actions } decayed, no raw source |
GET /api/search |
read token | requires AXION_CURSOR_SECRET; scan budget 40 |
GET /api/export/all |
read token | page of 20 |
GET /api/sessions |
read token | page size 20 |
GET /api/sessions/:id |
read token | one registry record |
GET /api/sessions/:id/export/json |
read token | ?includeRaw=1 for batch rawText |
GET /api/sessions/:id/export/markdown |
read token | never includes raw source |
GET /api/sessions/:id/usage |
read token | cumulative tokens |
GET /api/sse/:id |
read token or ?readToken= |
live-only, no replay |
OPTIONS /api/* |
none | CORS preflight |
GET /dashboard* |
none for HTML/assets | JSON calls still need the token |
GET /styles.css, GET /app.js |
none | legacy dashboard asset aliases |
GET / |
none | 302 to /dashboard |
Proxy POST bodies over AXION_MAX_BODY_BYTES (default 1 MiB) return 413. Registry messageCount is captured model calls, not inbound messages[] length.
Production deploys must set AXION_READ_TOKEN (wrangler secret put AXION_READ_TOKEN). Every read route listed above requires it unless AXION_OPEN_READ=true in local .dev.vars. Do not set AXION_OPEN_READ in wrangler.toml [vars].
CORS reflects Origin only when it exactly matches AXION_CORS_ORIGIN. Unset or mismatched origin means no Access-Control-Allow-Origin header. Same-origin dashboard loads still work.
Search cursors are HMAC-signed with server-only AXION_CURSOR_SECRET. If that secret is unset, GET /api/search returns 503.
Authenticated reads are rate-limited via the Cache API (60 second buckets, keyed by token or client IP):
- 30
GET /api/searchper token per minute - 6
GET /api/export/allper token per minute - 120 other authenticated reads per token per minute
GET /api/health and proxy POSTs are not limited here. 429 includes Retry-After: 60. If the Cache API is missing, search and export-all return 503 rather than a fake isolate-local counter.
See SECURITY.md.
Requires Node.js 20+ and a Cloudflare account for deploy.
npm ci
cp .dev.vars.example .dev.vars
# set AXION_READ_TOKEN, or AXION_OPEN_READ=true for a local demo
# optional: UPSTREAM_API_KEY if callers will not send their own key
npm run dev
export OPENAI_BASE_URL=http://localhost:8787
# send header x-axion-session: my-session on your agent's requests
# send header x-axion-read-token: <token> on dashboard / API reads
# dashboard: http://localhost:8787/dashboard/?session=my-session
npm run checkAnthropic agents route through POST /v1/messages and default to https://api.anthropic.com:
export ANTHROPIC_BASE_URL=http://localhost:8787Deploy your own instance:
npx wrangler secret put UPSTREAM_API_KEY
npx wrangler secret put AXION_READ_TOKEN
npx wrangler secret put AXION_CURSOR_SECRET
npx wrangler secret put AXION_WEBHOOK_SECRET
npx wrangler deployBeliefs are grouped by session. Send x-axion-session: <id> on agent requests. If the header is absent the Worker generates a UUID per request and returns it as x-axion-session, so a single call is still captured. Multi-turn correlation needs a stable header.
Open http://localhost:8787/dashboard/, paste the session id, enter the read token, and press Load. The id also reads from ?session=. Session id is an identifier, not an authz token.
SSE is live-only. Initial state comes from GET /api/beliefs/:id. There is no Last-Event-ID replay.
Axion is a rolling inspect window, not an archive. Export authenticated JSON if you need a snapshot. The 200-batch cap drops the oldest batch.
Enforce mode is off unless the request carries a schema. Two triggers, header first:
- Header
x-axion-schema: <JSON Schema as JSON>(URL-decoded if needed), or - Body
response_format: { "type": "json_schema", "json_schema": { "schema": { ... } } }.
The Worker forces a non-streaming upstream call, parses assistant JSON, validates, and coerces primitive types. On a violation it retries, up to 3 attempts total. Success returns provider-shaped JSON whose usage is the sum of every attempt. After 3 failed attempts it returns HTTP 422. Every enforce response sets x-axion-enforce-attempts. 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.
When an upstream completion includes OpenAI tool_calls or Anthropic tool_use, the Worker stores them on that turn's batch as actions. GET /api/beliefs/:id returns { sessionId, beliefs, actions } with actions concatenated in storage order. An empty actions array is still stored so a tool-only turn creates a batch.
Each ObservedAction has id, name (max 128 chars), provider, source (tool_calls or tool_use), argumentFingerprint (sha256 hex), argumentFingerprintSource (canonical or raw on parse failure), argumentBytes, and sourceClass: "tool_observed". Raw arguments are not stored unless AXION_STORE_TOOL_ARGS=true, and then only after secret redaction.
Same-turn overlay, no embeddings: for each action, if exactly one belief in that batch has type intention or planning and empty actionTaken, set actionTaken to the tool name. If several match, attach to the last one in source order. Never invent a belief. Parsing failure never blocks the proxy.
After a successful Durable Object store, if AXION_BELIEF_WEBHOOK_URL is set, Axion POSTs a redacted batch from waitUntil. The observe response is already returned, so a slow or down sink cannot add latency or 5xx the proxy. Failed stores do not notify. Delivery retries twice inside the same waitUntil promise, 2s timeout per attempt. Failures console.error and increment meta.webhookFailures.
Payload spec axion.belief_batch.v1:
{
spec: "axion.belief_batch.v1",
sessionId,
timestamp,
provider?,
modelName?,
usage?,
inboundMessageCount?,
callsInSession,
beliefs, // rawText stripped
actions,
redactions
}
Headers: Content-Type: application/json, User-Agent: axion-webhook/0.1.0, x-axion-session: <id>. When AXION_WEBHOOK_SECRET is set, x-axion-signature: sha256=<hex> is HMAC-SHA256 of the raw body. If the secret is unset, Axion omits the signature and refuses to send unless AXION_WEBHOOK_ALLOW_UNSIGNED=true (local only).
Langfuse mapping, documented not coded: put the JSON under metadata.axion on a generation span. Honeycomb/OTLP mapping: one event axion.belief_batch with axion.session_id, axion.belief_count, axion.action_names, axion.belief_types. This cycle does not ship an OTLP client.
| Type | Baseline |
|---|---|
| causal | 0.7 |
| assumption | 0.5 |
| intention | 0.8 |
| evidence | 0.6 |
| uncertainty | 0.3 |
| contradiction | 0.4 |
| planning | 0.6 |
| self-correction | 0.5 |
Markers in an 80-character window, summed per distinct category, then clamped to [0.1, 1.0] at extraction:
- certain: +0.2
- likely: +0.1
- possible: -0.2
- uncertain: -0.3
Read-time decay multiplies stored confidence by 0.9 ^ turnsAgo. The dashboard treats values below 0.4 as low confidence. This is a linguistic heuristic, not a truth signal.
axion/
|- src/
| |- proxy/ Worker entry, providers, read APIs, extraction glue
| |- lens/ regex patterns + extractBeliefs
| |- polyverdict/ opt-in schema enforce
| |- redact/ secret regex before persist
| |- state/ SessionDurableObject + registry
| |- dashboard/ static React UI, no bundler
|- wrangler.toml tsconfig.json package.json
|- AGENT.md README.md SECURITY.md CONTRIBUTING.md
- Loop and Gate are not implemented.
- Belief DAG, parent/child edges, and root-cause routes are not implemented. The store is a flat timeline.
- The lens is regex. It misses reasoning that does not use the trigger phrases.
- SSE is live-only. Disconnects lose events that happened while you were gone.
- Secret regex is not PII completeness.
- The inspect window is 200 batches per session and 5000 registry rows.
- Two providers only: OpenAI Chat Completions and Anthropic Messages.
- Dashboard this cycle is paste-session plus a read-token field and an error banner. Session picker, live SSE, usage, and export buttons are later work.
Hosted platforms like The Context Company, Sentrial, BentoLabs, and Armature detect what agents DID wrong - from traces, tool calls, and post-hoc reconstruction. Axion reads what agents BELIEVED from the model's own streamed output in real time: zero instrumentation, no code changes, no upload. It is the "why" layer their trace pipelines cannot reach, and it feeds into Langfuse, Arize, and Braintrust spans as structured metadata instead of replacing them.
Everything the new YC agent-observability startups are building - open-sourced, self-hosted, MIT.
- Source: github.com/LatticeAG/Axion
- LatticeAG: latticeag.vercel.app
MIT. See LICENSE.