# Knowledge Docs

Knowledge docs are the AI's **rulebook layer** — the workspace-specific facts and rules an
agent can't know on its own: your price list, your product codes, your SOPs, your shipping
tariffs, your glossary of in-house terms. You write them once; the agent **searches and reads
them on demand** while it works, pulling in only the lines it needs.

They are deliberately **not** injected into the agent wholesale. Instead the agent retrieves
from them line by line — grep, then read — so a 10,000-line tariff book costs nothing until a
question actually touches it, and a 10MB one is no different.

This is a **capability + usage** guide. For the exact input schema of any tool named here, run
`lotics tools <tool_name>`.

## Rulebook, not methodology

Put in a knowledge doc the things the model **cannot guess**: your specific numbers, codes,
names, exceptions, and policies. Do **not** put in it general skills the model already has
("how to write a polite email", "how to summarize"). If the agent would get it right without
the doc, the doc is noise.

## Creating a doc — `lotics knowledge create`

```bash
lotics knowledge create --name "Shipping tariffs" \
  --description "HS-coded rates; searchable by lane and code" \
  --tags "HS,nhập khẩu" \        # file it as you make it — see Classifying, below
  --from ./tariffs.md            # the body is a Markdown file; or --content '<inline>'
```

This reads the body from your filesystem and creates the doc through `create_knowledge`
(prints the new id). A raw `lotics run create_knowledge @doc.json` works too — read a large
`content` from a file or stdin. `create_knowledge` takes five fields, three of them required:

- `name` — what it is. Just the name: **grouping belongs in `tags`, not in a prefix baked into
  every title.**
- `description` — what the doc is **and what to grep it for**: its vocabulary and the synonyms
  an ambiguous query would use. Surfaced in the tree before any body is read, and truncated at
  200 characters. See *Write for grep* below.
- `content` — Markdown.
- `tags` *(optional)* — the labels someone would filter by, e.g. `["Hồ sơ NOXH", "Khoản 13"]`.
  Give **every** one that is true of the doc, not just the main one — a corpus is narrowed by
  intersecting tags, so a doc carrying one label can only ever be found down one path. On
  `update_knowledge` the array **replaces** what is stored, so resend the labels it should keep.
- `files` *(optional)* — file ids of the source `.docx`/`.pdf`/`.xlsx` the doc was built from,
  stored alongside it so a reader can open the original. The body stays the searchable text.

A new doc is created **owned by you, active in your own agent context, and private** — no one
else can see it yet. Changing a doc's default-active state is done through the web app / REST
surface, not this tool.

## Access vs. activation — both are required

A knowledge doc reaches an agent only when **two** conditions hold for that member:

1. **Access** — the member can `use` the doc. New docs are private to the owner; share them
   with other members or groups using the IAM tool `share_resource` (category **Admin**;
   `unshare_resource` to revoke).
2. **Activation** — the doc is *active* in that member's agent context. Activation is
   **separate from sharing**: a member can have access to a doc that is switched off in their
   context, and it won't reach their agent. Each doc has a default-active state (on by default),
   and each member can override it on or off for themselves. Activation is toggled in the web
   app, per member — there is no CLI tool for it.

So: shared + active → the agent can find and read it. Shared but deactivated → invisible to
that member's agent.

## How an agent uses a doc — ls, grep, cat

Docs are not injected wholesale. The agent works the corpus like a filesystem, and all three
verbs respect access + activation, so only docs the caller may use ever surface.

1. **`list_knowledge`** — `ls`. The corpus as a flat list: id, name, tags, and a truncated
   description; `tag` narrows it. No bodies.
2. **`grep_knowledge`** — `grep -rn`. Match across every readable doc (or one doc, or one
   tag), returning **doc, line number, and the matching line**. It runs inside Postgres,
   so only matching lines cross the wire.
3. **`read_knowledge`** — `cat` / `sed -n 'X,Yp'`. Read a doc whole or by line range, to see
   the context around a hit.

Matching is **not ranked** — there is no index and no tokenizer, which is why a corpus in any
script works and why nothing goes stale. The consequence is that the *agent* does the
narrowing: a distinctive phrase is sharply selective, a whole question matches everything.
`grep_knowledge` always reports `total_matches`, so "too broad" is visible and cheap to fix.

### Literal by default, expression on request

`pattern` is **literal text** — every character matches itself. Set `regex: true` to read it as
a POSIX regular expression instead.

```
grep_knowledge({ pattern: "C/O (Form E)" })
grep_knowledge({ pattern: "\\yNK\\y", regex: true, case_sensitive: true })
```

The default is the safe one rather than the conventional one, and the asymmetry is worth
knowing. As an expression, `C/O (Form E)` searches for `C/O Form E` — not in the document — and
returns a confident zero; `[CŨ]` becomes a one-character class and matches every `C` in the
corpus. Both look like ordinary answers. A literal search has no such failure: the worst case
is an expression sent without the flag, which finds nothing and says so, naming the search that
ran and what to pass instead. That last part is **measured, not guessed** — an empty result tells
you whether reading the pattern as an expression would have matched, so an alternation sent without
the flag (`phrase A|phrase B`) comes back naming `regex: true` rather than advising you to shorten a
pattern that was never too broad.

### Matching options

- **Tone marks match exactly by default.** `diacritic_insensitive: true` folds them, so `ca phe`
  matches `cà phê` — reach for it when the pattern was typed without them, not by habit: folding
  strips the pattern to ASCII, where a short Vietnamese word matches inside unrelated words
  (`mã` finds `manifest`). An empty result tells you when folding would have matched.
- **Case folds by default**, independently of diacritics. `case_sensitive: true` matches case
  exactly — useful for an acronym (`NK` vs `nk`) that a folded search would blur.
- **Whitespace is normalized on both sides.** A body converted from PDF, Word or Excel carries
  non-breaking spaces, soft hyphens, zero-width marks and padded runs that nobody types into a
  query; those fold to ordinary single spaces before matching, so a correct search does not return
  a silent zero on text that is present. A literal pattern is normalized the same way; an
  expression is left byte-exact, since collapsing its whitespace would rewrite it.
- **What `regex: true` supports**: quantifiers, character classes, alternation, anchors,
  backreferences, non-greedy forms, `(?i)`, and both lookahead and lookbehind. A word boundary
  is **`\y`**, not `\b` — Postgres spells it differently, and `\b` is rewritten for you rather
  than silently matching nothing. Named groups and `\p{…}` are likewise rewritten to their POSIX
  equivalents. A `\p{…}` with no POSIX equivalent, and a malformed expression, are both refused
  with the reason.

Where an expression cannot express the question at all — a value that must be computed, a
layout matching cannot address — stage the doc into a code run instead (`code_exec` with
`knowledge_doc_ids` puts it at `inputs/knowledge/<id>.md` as a real file).

### What a result is bounded by

A result carries at most 40.000 characters. Matched lines are admitted first and context fills
what remains, so an answer is never dropped to make room for a neighbouring line. Anything the
budget cut is REPORTED — `chars_elided_matches` (an answer was dropped) and
`context_omitted_matches` (an answer was kept without the context you asked for) mean different
things and call for different fixes: narrow the pattern, or ask for fewer `context_lines`.
Individual lines longer than 600 characters are clipped and marked, with `read_knowledge` giving
the rest.

`total_matches` is always the true total, even when fewer are shown.

## Write for grep — the rules that decide whether an answer is findable

Retrieval addresses **lines**. Every rule below follows from that one fact.

- **One self-contained fact per line.** A grep hit returns *that line*. For dense or tabular
  data — a price row, a tariff code, a charge entry — put the whole record on one line.
- **Repeat the searchable terms on every line; headings do not carry down.** A line reading
  `Rate: 15%` under a heading `Roasted coffee` will never match a search for `coffee`. Restate
  the identifying terms inline, even when it reads redundantly to a human. This is the single
  rule most often got wrong.
- **Keep a line under ~600 characters.** Past that the line is truncated in the agent's view
  and marked as cut; the agent can still `read_knowledge` for the rest, but it costs a round
  trip. Put the identifying terms early and the long tail late.
- **Put synonyms and translations on the line itself.** A bilingual row (`Cà phê, đã rang /
  Coffee, roasted`) matches queries in either language for free. The same trick carries
  colloquial terms next to official ones.
- **For prose, keep a rule and its exception close.** A hit returns one line, so a rule on line
  40 and its exception on line 90 can be retrieved apart. Use `context_lines` when reading, and
  keep related clauses adjacent when writing.

Markdown headings are ordinary text — useful for a human and for orienting a `read_knowledge`,
but they carry no retrieval weight of their own.

**The description is the discovery hint.** It is what the agent sees in the tree before it has
read a byte of the body, so it is the only clue for *what term to grep for*. Write it to name
the doc's vocabulary — the words that actually appear inside it. Keep it to a sentence: it is
truncated at 200 characters, and a keyword-stuffed description is cut, not rewarded.

## Updating a doc — `lotics knowledge update`

```bash
lotics knowledge update kdc_... --from ./tariffs.md   # replace the body (--name / --description too)
```

Send only the fields you're changing. `--from` / `--content` replaces the body; `--name` /
`--description` / `--tags` change metadata. `update_knowledge` resolves concurrency **internally** — it
re-reads the current content pointer and version-chains the new body — so there is no version
token to pass from the CLI. (The chat agent may instead send an `edits` array — anchored
replace / insert / append — for a surgical change; see `lotics tools update_knowledge`.) Refine
structure as you learn what users actually ask: add the synonym that failed to match, split a
line that was too coarse, restate a term the heading was carrying.

## Classifying a corpus — tags

A doc carries a SET of labels, not a folder. Order does not matter, a doc can wear several, and
the vocabulary is whatever the docs themselves use — a tag stops existing the moment nothing
carries it.

```bash
lotics knowledge update kdc_... --tags "HS,nhập khẩu"    # ONE doc: state its complete set
lotics knowledge tag kdc_a kdc_b --add HS --remove draft # MANY docs: a diff applied to each
```

The split is deliberate. Looking at one doc you know what it should carry, so `--tags` replaces.
Across a set you do not — the docs carry different labels — so `tag` adds and removes against
each doc's own set, and never states one classification for all of them. Removal ignores case;
adding a label a doc already carries changes nothing.

An agent narrows by tag rather than reading the whole corpus:

```bash
lotics tools list_knowledge   # takes an optional tag
lotics tools grep_knowledge   # same
```

## Taking a doc out of circulation — `hide`

```bash
lotics knowledge hide kdc_...              # superseded, draft, source material
lotics knowledge list --include-hidden     # find it again
lotics knowledge unhide kdc_...            # put it back
```

A hidden doc is out of every **listing**: it is gone from the Library's list, from
`list_knowledge`, and from the corpus `grep_knowledge` searches, so nothing finds it by browsing.
It is not deleted, not unshared, and still **reads when addressed by id** — `read_knowledge` with
its id, a code run staging it, an app agent that declares it. That is the point: hiding is a
statement about discovery, so it cannot silently break an app that depends on a doc by name.

Reach for it instead of `rm` whenever the material still matters: last year's tariff schedule, a
handbook a newer one replaced, the raw source a curated doc was written from.

## Knowledge from a starter

A knowledge doc can also arrive with a **starter** — a published snapshot of a workspace setup
that carries a corpus of docs (and document templates) along with it. Copying a starter creates
the docs in your workspace as ordinary knowledge docs: yours outright, edited and deleted like
any other, with no link back to where they came from. Authoring, sharing and retrieval work the
same either way.

## Reaching the tools

```bash
lotics knowledge list                   # catalog: id, name, tags, description
lotics knowledge get kdc_... -o doc.md  # read a body to a file (omit -o for stdout)
lotics tools update_knowledge           # full input schema for any knowledge tool
```

The Knowledge category covers `list_knowledge`, `create_knowledge`, `update_knowledge`, and
`delete_knowledge` — fronted by the `lotics knowledge list | create | get | update | tag | hide |
unhide | rm` commands, and reachable over MCP as well, so a corpus can be authored from whichever
surface you already work in. `list`, `tag`, `hide` and `unhide` go over REST rather than a tool, because
they are a person's view of the corpus: the tools answer what the ASSISTANT may browse, and a
hidden doc is out of that set by definition. Sharing a doc to other members is `share_resource` / `unshare_resource` (category
**Admin**).
