Getting started
From install to the first memory, loading knowledge in bulk, wiring up an LLM agent, and the operational basics. One pass through this page is enough to put Taguru to real work.
1. Install
One crate installs both the server and the MCP bridge.
cargo install taguru # `taguru` (server) and `taguru-mcp` (MCP bridge)
# from source
git clone https://github.com/t0k0sh1/taguru && cd taguru
cargo run --release
# Docker (linux/amd64 + linux/arm64)
docker run -d --name taguru \
-p 127.0.0.1:8248:8248 \
-v taguru-data:/data \
ghcr.io/t0k0sh1/taguru:latest
2. Run and configure
Defaults: 127.0.0.1:8248 ("TAGU" on a phone keypad, chosen to avoid the default
ports of likely neighbours) with data in ./data. All configuration is environment
variables, and taguru --help lists every one of them.
taguru # run as-is
taguru --config taguru.env # read KEY=VALUE lines from a file —
# the exact dialect `docker run --env-file` accepts, so one file drives both.
# Real environment variables win over the file; unknown TAGURU_* keys are flagged as probable typos
taguru version
Key environment variables
| Variable | Default | Role |
|---|---|---|
| TAGURU_ADDR | 127.0.0.1:8248 | Bind address |
| TAGURU_DATA_DIR | ./data | Data directory |
| TAGURU_CACHE_BYTES | 512 MiB | Resident memory budget for unpinned contexts (LRU eviction) |
| TAGURU_RETRIEVAL_CACHE_BYTES | 32 MiB | Exact-match result cache for recall/query/passage search: an identical request against an unchanged corpus answers from the stored response, invalidated by the per-context revision counters. Hit/miss on /metrics (taguru_retrieval_cache_total); 0 disables. Below ~16 KiB no response fits, so it becomes pure cost rather than a smaller cache — the server logs a warning at boot under that floor |
| TAGURU_SEMANTIC_CACHE_THRESHOLD | unset (off) | Semantic tier over the exact cache, passage search only: a paraphrased query serves an equivalent earlier query's cached result when the query-vs-query embedding cosine clears this [0,1] floor (start at 0.94) and a negation/number/entity guard finds no mismatch. Requires the exact cache and TAGURU_EMBED_PASSAGES; outcomes on /metrics (taguru_semantic_cache_total) |
| TAGURU_FLUSH_SECS | 5 | Image flush interval. With the WAL on, this is freshness cadence, not a loss window |
| TAGURU_WAL | on | fsync every acknowledged write before applying it (a crash loses nothing). 0 restores the flush-interval loss window |
| TAGURU_WAL_MAX_BYTES | 256 MiB | Per-context WAL ceiling. Only approached when flushes keep failing; past it, writes are refused with 500 |
| TAGURU_PASSAGES_WAL_MAX_BYTES | 1 GiB | Passage-log backstop, the sibling ceiling for the passage lane — engages only when compaction is stuck; 0 disables |
| TAGURU_REPLICATE_URL | — | Object-storage bucket (s3:// / gs:// / az:// / file://) for continuous replication of the whole data directory, epoch-fenced; credentials via each cloud's default chain. Restore with taguru restore — or start a server on an empty directory with the same URL and it boots straight from the bucket (pinned contexts hydrate before the port opens, the rest on first touch). Unset = off |
| TAGURU_REPLICATE_INTERVAL_MS | 1000 | Replication poll cadence — the steady-state RPO knob; per-lane lag is exported at /metrics |
| TAGURU_TAKEOVER | off | 1 (or serve --take-over) acknowledges deposing the bucket's newest writer while it still looks alive (heartbeat within 300s, no clean stop). A cleanly stopped writer never needs it; starting a writer against a bucket IS the promotion act |
| TAGURU_REPLICA | off | 1 (or serve --replica) serves the bucket lineage read-only, tailing it at the replication cadence: every retrieval verb works, every write answers 403 read_only_replica naming the writer, and per-context applied-vs-shipped lag — the promotion-time RPO — is on /metrics |
| TAGURU_WRITER_URL | — | Where a replica's write-refusal points clients (the writer's base URL / LB name); unset = the refusal names only the bucket's fence holder |
| TAGURU_ROUTE_MAP | — | taguru router only: the context→shard map file (context = shard-url per line, # comments, optional * = shard-url fallback). The router is stateless — no data directory, no keys (shards enforce auth); map edits take a router restart |
| TAGURU_API_TOKEN | — | Bearer token (required on every request except /health, /live, /metrics). Unset = no auth. Always set it before leaving localhost |
| TAGURU_API_TOKENS | — | Named keys ("ci:tokA,laptop:tokB"). Access logs say which key was used, and a leak costs one revocation. The auth table (both token variables plus TAGURU_KEY_SCOPES) hot-reloads on SIGHUP or a --config file edit (~5s watch) — rotation never costs a restart, and a broken edit fail-closed keeps the previous keys armed |
| TAGURU_EMBED_URL / _MODEL / _API_KEY | — | Semantic entry tier: an OpenAI-compatible /embeddings endpoint, or local to run the model in-process — local also requires TAGURU_EMBED_MODEL naming one of taguru-code models (e.g. TAGURU_EMBED_URL=local TAGURU_EMBED_MODEL=paraphrase-multilingual-minilm-l12-v2-q; not in the Docker image). Unset keeps the entrance purely lexical |
| TAGURU_EMBED_TIMEOUT_SECS | 60 | Per-attempt ceiling for one embedding provider round trip; a request's remaining budget bounds an attempt further. Three consecutive failed attempts open a circuit breaker — fast-fails for 30s, then one probe decides whether to close it |
| TAGURU_EMBED_AUTO | off | Re-embed only the changes on each flush. Recommended whenever agents drive the ingest (don't count on refresh being called) |
| TAGURU_EMBED_PARALLEL | 1 | Concurrent 128-item chunk dispatch for one context's gloss/passage embedding refresh (1 = sequential). Raise to match the provider's rate limit, not the core count; concurrent refreshes across contexts aren't serialized and multiply it |
| TAGURU_EMBED_PASSAGES | off | Also embed paragraphs = the semantic side of the text lane. A corpus is orders of magnitude larger than its glosses, so the spend is opt-in |
| TAGURU_PASSAGE_VECTOR_LIMIT | 20,000 | Ceiling on paragraph vectors held per context. Past it the lexical lane still serves every paragraph; only the semantic side goes partial (the refresh response reports the skips). The default is pinned above the approximate-search threshold (10,000, compiled in) by a compile-time assertion, so default configuration always has headroom to engage the index — a custom value set below the threshold isn't blocked, only logged once at boot |
| TAGURU_SEMANTIC_FLOOR | 0.35 | Floor for the semantic entry tier. A property of the embedding model (default calibrated for text-embedding-3-large; ~0.2 for Bedrock's Titan V2) — taguru calibrate measures the right value |
| TAGURU_PUBLIC_URL | — | Public base URL. Setting it enables OAuth on remote MCP (/mcp), which lets claude.ai custom connectors attach |
| TAGURU_RATE_LIMIT_PER_MIN | 0 (off) | Per-key request budget per minute. Enable it before leaving localhost |
| TAGURU_AUTH_FAIL_LIMIT_PER_MIN | 10 | Failed-auth attempts per source IP before 429 — the brute-force brake. 0 disables; coarse behind a proxy (one IP for everyone) |
| TAGURU_REQUEST_TIMEOUT_SECS | 30 | Time budget per request. Raise to 60+ once an embedding provider is configured |
| TAGURU_MAX_CONCURRENT_REQUESTS | 256 | Global in-flight ceiling. Excess requests are shed immediately with 503 + Retry-After; 0 disables |
| TAGURU_MAX_CONCURRENT_HEAVY_OPS | 2 | Shared ceiling for vocabulary audits and context compactions. Excess calls are shed immediately with 503 + Retry-After; 0 disables |
| TAGURU_CROSS_SEARCH_CONCURRENCY | 4 | Member contexts searched in parallel by a single cross-context (group) recall/query/passage search |
| TAGURU_AUTO_COMPACT | on | Ratio-triggered auto-compaction: each flush tick rebuilds at most the one worst context whose dead ratio exceeds TAGURU_AUTO_COMPACT_RATIO (0.5 — dead weight outgrew live content), behind the heavy-ops ceiling above. 0 keeps compaction manual-only |
| TAGURU_CONTEXT_QUOTAS | — | Per-context ceilings as one JSON object, {"sake": {"storage_bytes": …, "cache_bytes": …}} — each field optional, never both absent. storage_bytes refuses growth writes at the ceiling with 507 storage_full (retract, compact, and delete stay open — they are the ways back under); cache_bytes bounds the context's resident share, evicting the over-share context first under cache pressure. Declared quotas surface as taguru_context_quota_bytes next to the per-context usage gauges. A broken declaration refuses boot, like broken credentials |
Observability (RUST_LOG, TAGURU_LOG_FORMAT=json, OTEL_EXPORTER_OTLP_ENDPOINT,
search logging via TAGURU_LOG_SEARCHES) and the caps (TAGURU_MAX_BODY_BYTES and friends)
are covered in Internal architecture. The full list is taguru --help.
OTEL_EXPORTER_OTLP_ENDPOINT turns on the composed retrieval span tree specifically —
see Tracing.
3. The first memory
Create a context, store an association, pull the thread. One round trip over plain HTTP.
curl -X PUT localhost:8248/contexts/sake -H 'Content-Type: application/json' \
-d '{"description":"青嶺酒造という架空の酒蔵の知識"}'
curl -X POST localhost:8248/contexts/sake/associations -H 'Content-Type: application/json' \
-d '[{"subject":"青嶺酒造","label":"代表銘柄","object":"青嶺","weight":1.0,"source":"第1段落"}]'
curl -X POST localhost:8248/contexts/sake/activate -H 'Content-Type: application/json' \
-d '{"origins":["青嶺酒造"]}'
The activate response is a list of associations ordered by strength, each row
carrying strength (ranking within this call), path (how it was
reached), and the attributions of its sources. The full endpoint list and the
playbook are distributed as-is by the running server at GET /protocol.
The one-fact request above is only the smallest example. In an ingest loop, collect one
document's facts and send them in one /associations request (up to 10,000
associations): every request pays for a durable write and stalls that context's readers
while its fsync lands, making single-fact calls roughly two orders of magnitude more
expensive per association than batches. More concurrency does not help one context — its
writes serialize by design — though different contexts write in parallel. For a corpus or
migration, use POST /import on a running server or taguru import
offline instead.
4. Running under Docker
The published image is the server alone on scratch — no shell, no libc, nothing to patch, ~13 MB.
- Inside a container, bind
0.0.0.0:8248(loopback would be unreachable from outside-p). That means no-auth mode reaches as far as you publish it — keep-pon127.0.0.1, or setTAGURU_API_TOKEN. - Configuration is the same environment variables (
-e, or--env-file taguru.env— the very file--configreads). - The process runs as uid 65532. A named volume inherits ownership automatically on first use; a bind mount needs
chown -R 65532first (or--user "$(id -u)"). - Runs with
--read-only(/data is the only write target).docker stopis a graceful shutdown (flush + usage-stats sweep). - The HEALTHCHECK is
taguru health— the binary probes itself (there is no curl on scratch).
# verifying a backup needs no local toolchain either
docker run --rm -v taguru-data:/data ghcr.io/t0k0sh1/taguru inspect /data
For running as a service, see Docker Compose (a single
host), Kubernetes (the probe wiring and single-attachment
storage), or — per cloud — the AWS guide,
the Azure guide, and the Google Cloud
guide. One thing a
tag alone can't tell you: latest never re-pulls itself,
so confirming what's actually running — and updating it — is covered in
Troubleshooting — the image you think you're
running.
5. Loading in bulk — taguru import
Initial loads and migrations skip HTTP. JSONL batch files apply straight to the data directory through the same WAL-staged write path the server uses.
taguru import batches/ # every *.jsonl underneath, in name order
taguru import --dry-run batches/ # validate and report only; writes nothing
The only rule is one file = one source's complete truth. Applying a file means "retract that source, then apply the file" — so:
- Idempotent: importing the same file twice lands in the same state. Weights never double-count.
- Revisable: a corrected file replaces the old facts wholesale (the same diff-sync an agent performs live).
- Retryable: a file that failed midway is fixed and re-imported — the retraction is what makes the retry exact.
# a batch file: header first, then one operation per line
{"taguru_batch": 1, "context": "sake", "source": "docs/aomine.md", "create": {"description": "酒蔵の知識"}}
{"passage": "青嶺酒造は1907年創業。杜氏は高瀬。"}
{"paragraph": 0, "section": "沿革"}
{"paragraph": 0, "question": "青嶺酒造の酒造りの責任者は誰?"}
{"subject": "青嶺酒造", "label": "杜氏", "object": "高瀬", "weight": 2.0}
{"alias": "Aomine Brewery", "canonical": "青嶺酒造", "kind": "concept"}
Validation is a separate pass from apply: one malformed line refuses the whole file with its
line number, and nothing is written. A running server takes the same contract at
POST /import (one request = one batch file) — that is the bulk entrance with
no downtime window.
# -d strips newlines, and newlines are the format — use --data-binary
curl -X POST localhost:8248/import -H 'Authorization: Bearer <key>' \
--data-binary @docs-aomine.jsonl
The complete contract — full semantics of every line type, the table of caps, the concurrency rules — is in the batch import reference.
Producing batch files from documents — taguru extract
taguru extract is an offline producer: it has an OpenAI-compatible chat model
read .md/.txt files, decompose them into associations under the
/protocol discipline, and writes one batch file per document.
The server still never holds model credentials — TAGURU_EXTRACT_* lives only in
the extraction process's environment.
TAGURU_EXTRACT_URL=https://api.openai.com/v1/chat/completions \
TAGURU_EXTRACT_MODEL=gpt-4.1 TAGURU_EXTRACT_API_KEY=$KEY \
taguru extract --context sake --description "酒蔵の知識" --out batches/ docs/
taguru import batches/
- The manifest in
--outrecords "content × model × prompt version × context" and skips unchanged documents (--forceoverrides). A nightly extract→import run pays model calls only for what changed. --questions Nis doc2query: up to N retrieval questions per paragraph ride along. Their terms index into the paragraph's BM25 postings on every server, and a server withTAGURU_EMBED_PASSAGESalso embeds them next to their paragraphs — either way, question-shaped searches land on answer-shaped text.- Model output is not trusted; the contract is enforced on this side of the wire — invalid items are dropped and counted, and every file is re-validated with the import parser before it is written. Extract cannot produce a file import would reject.
- Running against a local model (Ollama etc.): thinking mode OFF (reasoning tokens devour the time budget), a real context window (
num_ctx 16384— leaving 4k silently truncates and only quality drops), a timeout matched to the hardware viaTAGURU_EXTRACT_TIMEOUT_SECS, and--forceafter re-pointing a serving alias — the manifest trusts the model name.
What the prompt asks and what is enforced, chunking, manifest details, the trust model, and measured quality per model class are in the document extraction reference. For a corpus large enough that one run spans hours or several short-lived instances, see long-running ingestion: bounded run windows, interrupt and resume, and torn-import recovery.
6. Wiring up an LLM agent — MCP
Decomposing and recomposing is the agent's job. The discipline ships automatically with the
tool definitions and the MCP instructions (the full text of /protocol),
so a connected LLM knows the correct procedure from the start.
Local (stdio bridge)
claude mcp add taguru -e TAGURU_URL=http://127.0.0.1:8248 -- taguru-mcp
That alone makes "ingest the documents in this folder into the sake context" and "tell me about 青嶺酒造, with sources" work as the loop of directory pick → resolve → describe/query/activate → citation. A real round trip is traced in the walkthrough.
Remote (Streamable HTTP)
claude mcp add --transport http taguru https://your-host/mcp \
--header "Authorization: Bearer $TAGURU_API_TOKEN"
# Claude API: mcp_servers = [{type: "url", url: "https://your-host/mcp",
# name: "taguru", authorization_token: "…"}]
POST /mcp speaks the MCP Streamable HTTP transport (stateless profile: plain
JSON responses, no session to manage) behind the same Bearer token as the rest of the API.
claude.ai custom connectors (OAuth)
For clients that cannot attach a header: set TAGURU_PUBLIC_URL, point the
connector at https://your-host/mcp, and paste an existing API key on the
consent page — from then on that connection acts as that key (key@client in the
access logs). Discovery, dynamic registration, and PKCE are built in; no external IdP is
needed. The OAuth token opens /mcp only; the plain API stays key-only.
Leaving localhost requires TLS (via a reverse proxy). The token is the whole credential —
whoever holds it holds the memory. Cut a named key per client (TAGURU_API_TOKENS)
so a leak costs one revocation.
7. Operational basics
- Health:
GET /healthanswers200with{"status": "ok", "version": "…"}while healthy, and503when the write path is unhealthy or a maintenance sweep is active. Once the disk recovers, it self-heals within one flush interval. - Metrics:
GET /metrics(Prometheus text). Per-route latency, cache/flush/WAL/embedding outcomes, search hits and misses, a breakdown of 500 causes. Context names are deliberately kept out of the labels (clients mint names, so series would grow without bound) — per-context numbers live in the usage stats ofGET /contexts. - Backups: one context = one file family (
.ctx.meta.json.passages.bin.passages.wal.jsonl.bm25.bin.vectors.bin.wal.jsonl; group records ride beside them as{name}.group). Always as a set. The simplest way to have one continuously: setTAGURU_REPLICATE_URLand the server ships every family (both log lanes tailed, published files whole) to object storage with seconds of lag — recover withtaguru restore --out DIR, thentaguru inspect DIR; or start a server on an empty directory with the same URL and it boots from the bucket directly (while the previous writer still looks alive, that boot asks for--take-overfirst). For point-in-time copies: every writer is fsync+rename, so filesystem snapshots (ZFS/Btrfs/LVM) are safe at any instant. A naiversync/cpagainst a live server does not guarantee cross-file consistency — stop the server, take a real snapshot, or replicate.POST /flushwrites out everything dirty right before. Verify withtaguru inspect /path/to/data(the same fully-validating load + WAL replay the server does; non-zero exit = corruption of acknowledged data). - Exclusivity: one process per data directory at a time (serve or import). The lock is advisory (flock-style — dependable on local disks, but NFS/EFS/FUSE may grant it to both sides). In a rolling deploy the new process correctly bows out while the old one lives — on a shared volume, deploy stop-then-start (
strategy: Recreateon Kubernetes). - When something is slow, stuck, or the wrong shape: the troubleshooting guide attributes the symptom to a layer — version skew, a stale image, Taguru's own latency vs. the LLM's, the semantic lane's silent degrade — before anything gets reset.
Taguru