Taguru
getting started

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

VariableDefaultRole
TAGURU_ADDR127.0.0.1:8248Bind address
TAGURU_DATA_DIR./dataData directory
TAGURU_CACHE_BYTES512 MiBResident memory budget for unpinned contexts (LRU eviction)
TAGURU_RETRIEVAL_CACHE_BYTES32 MiBExact-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_THRESHOLDunset (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_SECS5Image flush interval. With the WAL on, this is freshness cadence, not a loss window
TAGURU_WALonfsync every acknowledged write before applying it (a crash loses nothing). 0 restores the flush-interval loss window
TAGURU_WAL_MAX_BYTES256 MiBPer-context WAL ceiling. Only approached when flushes keep failing; past it, writes are refused with 500
TAGURU_PASSAGES_WAL_MAX_BYTES1 GiBPassage-log backstop, the sibling ceiling for the passage lane — engages only when compaction is stuck; 0 disables
TAGURU_REPLICATE_URLObject-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_MS1000Replication poll cadence — the steady-state RPO knob; per-lane lag is exported at /metrics
TAGURU_TAKEOVERoff1 (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_REPLICAoff1 (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_URLWhere 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_MAPtaguru 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_TOKENBearer token (required on every request except /health, /live, /metrics). Unset = no auth. Always set it before leaving localhost
TAGURU_API_TOKENSNamed 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_KEYSemantic 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_SECS60Per-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_AUTOoffRe-embed only the changes on each flush. Recommended whenever agents drive the ingest (don't count on refresh being called)
TAGURU_EMBED_PARALLEL1Concurrent 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_PASSAGESoffAlso 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_LIMIT20,000Ceiling 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_FLOOR0.35Floor 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_URLPublic base URL. Setting it enables OAuth on remote MCP (/mcp), which lets claude.ai custom connectors attach
TAGURU_RATE_LIMIT_PER_MIN0 (off)Per-key request budget per minute. Enable it before leaving localhost
TAGURU_AUTH_FAIL_LIMIT_PER_MIN10Failed-auth attempts per source IP before 429 — the brute-force brake. 0 disables; coarse behind a proxy (one IP for everyone)
TAGURU_REQUEST_TIMEOUT_SECS30Time budget per request. Raise to 60+ once an embedding provider is configured
TAGURU_MAX_CONCURRENT_REQUESTS256Global in-flight ceiling. Excess requests are shed immediately with 503 + Retry-After; 0 disables
TAGURU_MAX_CONCURRENT_HEAVY_OPS2Shared ceiling for vocabulary audits and context compactions. Excess calls are shed immediately with 503 + Retry-After; 0 disables
TAGURU_CROSS_SEARCH_CONCURRENCY4Member contexts searched in parallel by a single cross-context (group) recall/query/passage search
TAGURU_AUTO_COMPACTonRatio-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_QUOTASPer-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 -p on 127.0.0.1, or set TAGURU_API_TOKEN.
  • Configuration is the same environment variables (-e, or --env-file taguru.env — the very file --config reads).
  • The process runs as uid 65532. A named volume inherits ownership automatically on first use; a bind mount needs chown -R 65532 first (or --user "$(id -u)").
  • Runs with --read-only (/data is the only write target). docker stop is 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 --out records "content × model × prompt version × context" and skips unchanged documents (--force overrides). A nightly extract→import run pays model calls only for what changed.
  • --questions N is 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 with TAGURU_EMBED_PASSAGES also 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 via TAGURU_EXTRACT_TIMEOUT_SECS, and --force after 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 /health answers 200 with {"status": "ok", "version": "…"} while healthy, and 503 when 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 of GET /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: set TAGURU_REPLICATE_URL and the server ships every family (both log lanes tailed, published files whole) to object storage with seconds of lag — recover with taguru restore --out DIR, then taguru 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-over first). For point-in-time copies: every writer is fsync+rename, so filesystem snapshots (ZFS/Btrfs/LVM) are safe at any instant. A naive rsync/cp against a live server does not guarantee cross-file consistency — stop the server, take a real snapshot, or replicate. POST /flush writes out everything dirty right before. Verify with taguru 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: Recreate on 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.