# khive — full documentation Concatenated from https://ohdearquant.github.io/khive/ and every page below. Generated from docs/guide/*.md; do not edit directly. --- ## Home Source: https://ohdearquant.github.io/khive/ # khive A research knowledge graph runtime for agents that need structure: typed substrates, closed taxonomies, and a verb-consolidated MCP surface. Vector search finds similar text. A knowledge graph finds structure: lineages, dependencies, contradictions, gaps. khive gives an agent a typed, queryable graph that grows as it works. [crates.io](https://crates.io/crates/khive-mcp) · [GitHub](https://github.com/ohdearquant/khive) · [License: Apache 2.0](https://github.com/ohdearquant/khive/blob/main/LICENSE) ## For AI agents - [`/llms.txt`](llms.txt): short project summary + a linked index of every doc page - [`/llms-full.txt`](llms-full.txt): every doc page, concatenated, in one fetch - [`/md/*.md`](md/getting-started.md): raw, unconverted markdown for each page (also linked from the bottom of every page) ## Documentation - [Getting Started](getting-started.html): install, connect, first session - [Knowledge Graph Modeling](knowledge-graph.html): entity kinds, edge relations, modeling patterns - [Memory and Recall](memory.html): episodic vs semantic, salience, decay - [Search and Retrieval](search.html): FTS, vector, hybrid fusion, reranking - [GTD Task Management](tasks.html): task lifecycle, priorities, dependencies - [Prompt Cookbook](prompt-cookbook.html): ready-to-use verb patterns - [API Reference](api-reference.html): full verb catalog, params, DSL examples ## Demos Runnable transcripts, captured against a scratch database, in the repo's [`demos/`](https://github.com/ohdearquant/khive/tree/main/demos) directory: - [research-ingest](https://github.com/ohdearquant/khive/blob/main/demos/research-ingest.md): create entities, link them, search, and traverse the graph - [gtd-memory](https://github.com/ohdearquant/khive/blob/main/demos/gtd-memory.md): task lifecycle and salience-weighted memory recall ## Install ```bash cargo install kkernel ``` `kkernel` is the single shipped binary; `kkernel mcp` serves the MCP `request` surface. Full install and MCP client setup: [Getting Started](getting-started.html). ## Reference - [AGENTS.md](https://github.com/ohdearquant/khive/blob/main/AGENTS.md): full verb reference for agents using khive - [Architecture Decision Records](https://github.com/ohdearquant/khive/tree/main/docs/adr): the design contract --- ## Getting Started Source: https://ohdearquant.github.io/khive/getting-started.html # Getting Started This guide walks you from zero to a productive khive session: install the binary, connect it to your MCP client, create your first entities, search the graph, and link concepts together. ## What khive gives you khive is a research knowledge graph runtime. When you read papers, form concepts, link ideas, record decisions, or track tasks, khive gives that work a typed, queryable graph that persists across sessions. Everything is accessible through 75 verbs across 9 packs, dispatched through a single MCP tool. ## Install ### From crates.io (Rust) ```bash cargo install kkernel ``` `kkernel` is the single shipped binary; `kkernel mcp` serves the MCP `request` surface. ### From npm ```bash npm install -g khive # or npm install -g @khive-ai/cli ``` The npm package installs `khive` / `khive-mcp` shims that forward to `kkernel mcp`. The npm release can lag the crates.io release, so run `khive --version` after install and compare against [crates.io/crates/khive-mcp](https://crates.io/crates/khive-mcp) if you need the latest verbs documented here. ### From source ```bash git clone https://github.com/ohdearquant/khive cd khive/crates cargo build --release -p kkernel # Binary at target/release/kkernel (relative to crates/) ``` ## Connect to your MCP client ### Claude Code Add to your MCP configuration (`.claude/settings.json` or equivalent): ```json { "mcpServers": { "khive": { "command": "kkernel", "args": ["mcp"] } } } ``` ### Claude Desktop Add to `claude_desktop_config.json`: ```json { "mcpServers": { "khive": { "command": "kkernel", "args": ["mcp"] } } } ``` khive auto-spawns a background daemon on first request to keep the ANN index and embedding model warm. You do not need to manage this: it starts automatically and cleans up on exit. ## The single-tool interface khive exposes one MCP tool: `request`. Every operation goes through it: ``` request(ops="verb(arg=value, arg=value)") ``` This is the only syntax you need. The `ops` string contains a verb call (or a batch of them), and khive dispatches it to the appropriate pack handler. ## Your first session ### 1. Create an entity Entities are the nodes in your knowledge graph. khive has 9 entity kinds: `concept`, `document`, `dataset`, `project`, `person`, `org`, `artifact`, `service`, `resource`. ``` request(ops="create(kind=\"entity\", entity_kind=\"concept\", name=\"FlashAttention\", description=\"IO-aware exact attention algorithm\", properties={\"domain\": \"attention\", \"year\": 2022})") ``` Response: ```json { "ok": true, "result": { "id": "a1b2c3d4", "kind": "concept", "name": "FlashAttention", "description": "IO-aware exact attention algorithm" } } ``` ### 2. Create a related entity ``` request(ops="create(kind=\"entity\", entity_kind=\"document\", name=\"FlashAttention: Fast and Memory-Efficient Exact Attention\", properties={\"authors\": \"Dao et al.\", \"year\": 2022, \"source\": \"arxiv:2205.14135\"})") ``` ### 3. Link them Edges express typed relationships. `introduced_by` means "this concept was introduced by that document": ``` request(ops="link(source_id=\"\", target_id=\"\", relation=\"introduced_by\", weight=1.0)") ``` ### 4. Search the graph Search uses hybrid FTS5 + vector similarity with RRF fusion: ``` request(ops="search(kind=\"entity\", query=\"memory efficient attention\")") ``` Returns a scored list of matching entities. ### 5. Explore neighbors See what connects to an entity: ``` request(ops="neighbors(node_id=\"\", direction=\"both\")") ``` ### 6. Create a note Notes are temporal observations about your work: what you noticed, concluded, or decided. They can annotate entities: ``` request(ops="create(kind=\"note\", note_kind=\"observation\", content=\"FlashAttention reduces memory from O(N^2) to O(N) by tiling and recomputation\", annotates=[\"\"])") ``` ### 7. Batch operations Run multiple independent operations in one call: ``` request(ops="[create(kind=\"entity\", entity_kind=\"concept\", name=\"FlashAttention-2\"), create(kind=\"entity\", entity_kind=\"concept\", name=\"FlashAttention-3\")]") ``` Batched ops run in parallel with no ordering guarantee. If op B depends on op A's output, use two separate `request` calls. ### 8. Query the graph For complex pattern matching, use GQL: ``` request(ops="query(query=\"MATCH (a:concept)-[:introduced_by]->(b:document) RETURN a.name, b.name LIMIT 10\")") ``` Or SPARQL: ``` request(ops="query(query=\"SELECT ?a ?b WHERE { ?a :introduced_by ?b . } LIMIT 10\")") ``` Both compile to the same SQL backend. ## What to read next - [Knowledge Graph Modeling](knowledge-graph.md): how to think about entity kinds, edge relations, and modeling decisions - [Prompt Cookbook](prompt-cookbook.md): 20+ ready-to-use verb patterns - [Search and Retrieval](search.md): how hybrid search, reranking, and decompose work --- ## Knowledge Graph Modeling Source: https://ohdearquant.github.io/khive/knowledge-graph.html # Knowledge Graph Modeling This guide covers how to think about modeling in khive: when to use each entity kind, which edge relation fits, when something belongs as a note versus an entity, and common modeling patterns for research work. ## The two substrates khive has two kinds of records: - **Entities** are things in the world: algorithms, papers, people, projects, datasets, organizations, binaries, APIs. They are graph nodes with typed edges between them. - **Notes** are your observations about the world: what you noticed, concluded, decided, asked, or cited. They are temporal records with salience, optional decay, and can annotate entities via `annotates` edges. The rule of thumb: if it has a name and exists independently of your session, it is an entity. If it is something you thought or recorded during a session, it is a note. ## Entity kinds khive has 9 entity kinds. This is a closed set: you cannot add new kinds without an ADR. ### concept Algorithms, techniques, architectures, theories, models. This is the most common kind and the default. ``` create(kind="entity", entity_kind="concept", name="LoRA", description="Low-Rank Adaptation of LLMs", properties={"domain": "fine-tuning", "type": "technique", "year": 2021}) ``` Use `concept` for anything that is an idea, method, or approach. Use `properties.type` for finer classification: `algorithm`, `technique`, `architecture`, `model`, `theory`. ### document Papers, preprints, technical reports, blog posts, books. ``` create(kind="entity", entity_kind="document", name="Attention Is All You Need", properties={"authors": "Vaswani et al.", "year": 2017, "source": "arxiv:1706.03762"}) ``` Name the entity with its short title. Put full title, authors, year, and citation pointer in `properties`. ### dataset Benchmarks, corpora, evaluation sets. ``` create(kind="entity", entity_kind="dataset", name="MMLU", description="Massive Multitask Language Understanding benchmark", properties={"type": "benchmark", "year": 2021}) ``` ### project Codebases, libraries, tools, frameworks. ``` create(kind="entity", entity_kind="project", name="lattice-inference", description="Pure-Rust transformer inference engine", properties={"status": "implemented"}) ``` ### person Researchers, engineers, authors. ``` create(kind="entity", entity_kind="person", name="Edward Hu", properties={"affiliation": "Microsoft"}) ``` ### org Labs, companies, institutions. ``` create(kind="entity", entity_kind="org", name="Anthropic", description="AI safety company") ``` ### artifact Binaries, model checkpoints, Docker images, packages. ``` create(kind="entity", entity_kind="artifact", name="Llama-3-70B", properties={"type": "checkpoint", "source": "meta-llama"}) ``` ### service APIs, hosted endpoints, SaaS products. ``` create(kind="entity", entity_kind="service", name="OpenAI API", properties={"type": "api"}) ``` ### resource Actionable knowledge resources: atoms, domain knowledge packs, skills. Governed by the KG pack (ADR-048). Use `resource` for reusable knowledge objects that are managed by the knowledge pack rather than authored as raw entities. ``` create(kind="entity", entity_kind="resource", name="retrieval-patterns", description="Domain knowledge pack for hybrid retrieval patterns", properties={"type": "domain"}) ``` ## Note kinds khive has 5 base note kinds (also a closed set): | Kind | What it records | Example | | ------------- | ---------------------- | ------------------------------------------------------------------- | | `observation` | An empirical capture | "FlashAttention reduces memory from O(N^2) to O(N)" | | `insight` | A synthetic conclusion | "Tiling is the key technique across all IO-aware attention methods" | | `question` | An open inquiry | "Does FlashAttention-3 support GQA natively?" | | `decision` | A committed choice | "We will use FlashAttention-2 for the inference engine" | | `reference` | An external pointer | "See arxiv:2205.14135 Section 3.2 for the tiling algorithm" | `observation` is the default if you omit `note_kind`. Packs add their own note kinds too: `task` (GTD pack), `memory` (Memory pack), `message` (Comm pack), `scheduled_event` (Schedule pack), and `session` (Session pack). The generic `create(kind="note", note_kind="...")` path accepts all of these directly (verified against a scratch DB: `note_kind="task"`, `"memory"`, `"message"`, `"scheduled_event"`, and `"session"` all succeed; `task` additionally requires a `title` field). Prefer the pack-specific verbs (`gtd.assign`, `memory.remember`, `comm.send`, `schedule.remind`, `session.store`) when you need their defaults, validation, or side effects. Use the generic path only when you need a raw note write without those extras. ## Edge relations khive has 17 edge relations (15 base + 2 epistemic via ADR-055). This is a closed set enforced at compile time. ### When to use each relation **Structure**: parent/child and classification | Relation | Direction | When to use | | ------------- | ------------------- | ------------------------------------------------------ | | `contains` | parent to child | A system contains a module. An org contains a project. | | `part_of` | child to parent | Inverse of contains. A module is part of a system. | | `instance_of` | specific to general | GQA is an instance of multi-query attention. | **Derivation**: how ideas build on each other | Relation | Direction | When to use | | --------------- | ------------------- | --------------------------------------------- | | `extends` | child to parent | FlashAttention-2 extends FlashAttention. | | `variant_of` | variant to original | QLoRA is a variant of LoRA. | | `introduced_by` | concept to source | LoRA was introduced by the LoRA paper. | | `supersedes` | new to old | FlashAttention-3 supersedes FlashAttention-2. | **Provenance**: where things come from | Relation | Direction | When to use | | -------------- | --------------- | ------------------------------------------ | | `derived_from` | output to input | A model checkpoint derived from a dataset. | **Temporal**: ordering | Relation | Direction | When to use | | ---------- | ---------------- | ------------------------------------- | | `precedes` | earlier to later | Paper A was published before Paper B. | **Dependency**: runtime/build relationships | Relation | Direction | When to use | | ------------ | ----------------------- | ------------------------------------------------- | | `depends_on` | consumer to dependency | Project A depends on Project B at runtime. | | `enables` | prerequisite to outcome | BPE tokenization enables subword-level attention. | **Implementation**: code realizes concept | Relation | Direction | When to use | | ------------ | --------------- | -------------------------------------------- | | `implements` | code to concept | lattice-inference implements FlashAttention. | **Lateral**: peer relationships | Relation | Direction | When to use | | --------------- | ---------------- | ---------------------------------------------------- | | `competes_with` | either direction | LoRA competes with full fine-tuning. | | `composed_with` | either direction | FlashAttention composed with GQA in a serving stack. | **Annotation**: notes observing entities | Relation | Direction | When to use | | ----------- | ---------------- | ----------------------------------------------------------- | | `annotates` | note to anything | An observation about a concept, a decision about a project. | **Epistemic**: evidence relationships (added by ADR-055) | Relation | Direction | When to use | | ---------- | ----------------- | --------------------------------------------------- | | `supports` | evidence to claim | A paper or dataset supports a concept or finding. | | `refutes` | evidence to claim | A paper or experiment refutes a concept or finding. | `supports` and `refutes` are same-substrate: source and target must both be entities or both be notes. The source is the evidence; the target is the claim. ### Edge endpoint rules Not every `(source_kind, relation, target_kind)` triple is valid. The base contract in ADR-002 defines which entity kinds can appear as source and target for each relation. Key rules: - `annotates` is the only cross-substrate relation. Source must be a note; target can be anything (entity, note, edge, event). - `supersedes`, `supports`, and `refutes` are same-substrate only: entity to entity, or note to note. - All other 13 relations require entity-to-entity endpoints. - `competes_with` and `composed_with` are symmetric: the system canonicalizes direction internally. Packs can add endpoint pairs through the `EDGE_RULES` mechanism (ADR-017). The KG pack adds person-to-org and org-to-org pairs. The GTD pack allows task-to-task `depends_on` edges. These are additive; packs cannot tighten the base contract. ### Why a closed ontology A sparse, fixed set of relations keeps the graph queryable. Ad-hoc relations like `uses`, `related_to`, or `loaded_by` fragment the graph and make traversal meaningless. If your relationship does not fit one of the 17, it is probably a property on the entity rather than an edge. ## Modeling patterns ### Research papers A paper typically produces: one `document` entity (the paper itself), one or more `concept` entities (the ideas it introduces), and `introduced_by` edges from concepts to the paper. ``` create(kind="entity", entity_kind="document", name="LoRA Paper", properties={"title": "LoRA: Low-Rank Adaptation of Large Language Models", "authors": "Hu et al.", "year": 2021, "source": "arxiv:2106.09685"}) create(kind="entity", entity_kind="concept", name="LoRA", properties={"domain": "fine-tuning", "type": "technique"}) link(source_id="", target_id="", relation="introduced_by") ``` For citation chains between papers, use `precedes` (temporal ordering): ``` link(source_id="", target_id="", relation="precedes") ``` ### Software projects Model a project with `contains` for internal structure, `implements` for the concepts it realizes, and `depends_on` for external dependencies: ``` create(kind="entity", entity_kind="project", name="lattice-inference", properties={"status": "implemented"}) link(source_id="", target_id="", relation="implements") link(source_id="", target_id="", relation="depends_on") ``` ### People and organizations ``` create(kind="entity", entity_kind="person", name="Tri Dao") create(kind="entity", entity_kind="org", name="Princeton") link(source_id="", target_id="", relation="part_of") ``` ### Decision records Use `decision` notes that annotate the entities they concern: ``` create(kind="note", note_kind="decision", content="We will use FlashAttention-2 over vanilla attention because memory reduction is critical for 70B inference", annotates=["", ""]) ``` ### Temporal chains For versioned artifacts or sequential papers: ``` link(source_id="", target_id="", relation="precedes") link(source_id="", target_id="", relation="precedes") link(source_id="", target_id="", relation="supersedes") ``` ## Anti-patterns | Pattern | Problem | Fix | | ------------------------------ | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | Storing findings only as notes | Notes are temporal context; entities are structural. A concept worth naming deserves an entity. | Create the entity, then annotate it with notes. | | Creating duplicate entities | Fragments the graph, splits edges. | Always `search` before `create`. If found, `link` to it. | | Using ad-hoc relation names | `link(relation="uses")` will be rejected. | Map to the 15 closed relations. If none fit, use a property. | | Reversed `introduced_by` | `paper → concept` is wrong. | Direction is `concept → paper` (the paper introduces the concept). | | Over-noting | 20 observations but zero entities. | Extract the structural content into entities first. | | Under-linking | Entities with 0-1 edges are orphans. | Target 5+ edges per entity. Below 3 means the entity needs more context. | | Version numbers in names | "LoRA v2" instead of "QLoRA". | Version info goes in properties. Names are canonical short forms. | ## Edge density Sparse graphs are useless for traversal. Target minimums: | Entity kind | Min edges | What to link | | ------------------- | --------- | -------------------------------------------------------------------------------------------- | | concept (algorithm) | 4 | `extends` or `instance_of` (parent), `introduced_by` (paper), `competes_with` (alternatives) | | concept (paper) | 2 | `introduced_by` edges from concepts it introduced | | project | 3 | `implements` (concepts), `depends_on` (deps), `contains`/`part_of` (structure) | | person | 1 | `introduced_by` edges from their work | Overall target: 5+ edges per entity average. Check with `stats()`; if `total_edges / total_entities` is below 4, the graph needs polish. ## See also - [Prompt Cookbook](prompt-cookbook.md): concrete verb patterns for all the operations described here - [Search and Retrieval](search.md): how to find things in the graph - [Specialized Packs](specialized-packs.md): niche packs, such as the formal-math pack, that extend the base edge ontology additively - [AGENTS.md](../../AGENTS.md): the full agent reference with GQL/SPARQL examples --- ## Memory and Recall Source: https://ohdearquant.github.io/khive/memory.html # Memory and Recall This guide covers how the memory pack works in khive: how to store memories with appropriate salience, how decay affects recall ranking, and patterns for effective cross-session recall. ## Two memory types khive supports two memory types: | Type | What it stores | When to use | | ---------- | ----------------------------------------------- | -------------------------------------------------- | | `episodic` | Session events, conversations, task completions | Default. Context that happened at a specific time. | | `semantic` | Patterns, insights, reusable knowledge | Facts and rules that are useful across sessions. | These are the only valid values. There is no `procedural` or `working` memory type. ## Storing memories ### Basic remember ``` request(ops="memory.remember(content=\"khive uses RRF fusion for hybrid search scoring\", salience=0.8, memory_type=\"semantic\")") ``` ### Parameters | Parameter | Type | Default | Description | | -------------- | ------ | -------------------------------- | ------------------------------------------------------------------------- | | `content` | string | required | The memory content | | `salience` | float | episodic: 0.3 / semantic: 0.5 | Importance weight for recall ranking (0.0-1.0) | | `decay_factor` | float | episodic: 0.02 / semantic: 0.005 | Higher = faster decay. 0.02 ≈ 35-day half-life; 0.005 ≈ 139-day half-life | | `memory_type` | string | "episodic" | `episodic` or `semantic` | | `source_id` | uuid | none | Entity or note this memory annotates | ### Salience calibration Salience determines how prominently a memory surfaces during recall. Use these ranges: | Salience | Use for | Example | | -------- | -------------------------------------------- | ------------------------------------------- | | 0.85-1.0 | Critical directives, safety constraints | "Never delete the production database" | | 0.7-0.8 | Key insights, reusable patterns, corrections | "RRF scoring requires cosine normalization" | | 0.5-0.7 | Session summaries, routine context | "Completed attention benchmark run" | | < 0.5 | Low-value, ephemeral, auto-generated | Routine status updates | A common mistake is inflating salience: if everything is 0.9+, the scoring signal is lost and recall becomes unranked. ### Linking memories to entities ``` request(ops="memory.remember(content=\"FlashAttention-3 uses asynchronous tiling on H100\", salience=0.7, source_id=\"\")") ``` The `source_id` creates an `annotates` edge from the memory note to the specified entity. This makes the memory discoverable via `neighbors` on that entity. ## Recalling memories ### Basic recall ``` request(ops="memory.recall(query=\"attention optimization\", limit=5)") ``` Returns a scored list of matching memories: ```json [ {"id": "...", "content": "FlashAttention-3 uses async tiling...", "score": 0.72, "salience": 0.7, ...}, {"id": "...", "content": "PagedAttention reduces KV cache...", "score": 0.58, "salience": 0.6, ...} ] ``` ### Recall parameters | Parameter | Type | Default | Description | | -------------- | ------ | -------- | ------------------------------------------ | | `query` | string | required | Search query | | `limit` | int | 10 | Max results | | `min_score` | float | none | Minimum composite score threshold | | `min_salience` | float | none | Minimum salience filter | | `memory_type` | string | none | Filter by memory type | | `tags` | list | none | Filter by tags | | `tag_mode` | string | "any" | `any` (OR) or `all` (AND) for tag matching | ### Tag-filtered recall ``` request(ops="memory.recall(query=\"search optimization\", tags=[\"khive\", \"retrieval\"], tag_mode=\"any\")") ``` ## Scoring formula Recall ranking uses a composite score: $$\text{composite} = 0.70 \cdot \text{retrieval} + 0.20 \cdot \text{salience} \cdot \text{decay} + 0.10 \cdot \text{temporal}$$ Where: - **retrieval_score** (70% weight): RRF fusion of FTS5 keyword match and vector similarity - **salience * decay_weight** (20% weight): the memory's importance, decayed over time - **temporal_score** (10% weight): recency bonus ### Decay math Decay follows an exponential curve: $$w_{\text{decay}} = e^{-\lambda \cdot t}$$ where $\lambda$ is `decay_factor` and $t$ is age in days. With the episodic default `decay_factor=0.02`: - After 1 day: 98% of original salience - After 7 days: 87% - After 35 days: 50% (half-life) - After 69 days: 25% - After 180 days: 3% With the semantic default `decay_factor=0.005`: - After 1 day: 99.5% of original salience - After 30 days: 86% - After 139 days: 50% (half-life) - After 365 days: 16% Higher `decay_factor` means faster decay: - `0.001`: very slow (693-day half-life), for permanent reference memories - `0.005`: slow (139-day half-life), semantic default, good for durable facts - `0.02`: moderate (35-day half-life), episodic default, good for session context - `0.05`: fast (14-day half-life), for session-specific context - `0.1`: very fast (7-day half-life), for truly ephemeral context ## Brain integration The Brain pack provides Bayesian profile tuning based on feedback signals. After recalling memories, you can feed back which results were useful: ### Auto-feedback (recommended) ``` request(ops="brain.auto_feedback(results=[{\"id\": \"\", \"used\": true}, {\"id\": \"\", \"used\": false}])") ``` Call this after `memory.recall` to automatically signal which results you actually used. The brain profile adjusts its tuning over time. ### Manual feedback ``` request(ops="brain.feedback(target_id=\"\", signal=\"useful\")") ``` Signals: `useful`, `not_useful`, `wrong`, `explicit_positive`, `explicit_negative`, `correction`. Note: `target_id` must be a full UUID (not a short prefix). ## Usage patterns ### Session summary At the end of a work session, store key findings: ``` request(ops="memory.remember(content=\"SESSION: Completed FlashAttention-3 benchmark. Key finding: 2.3x speedup over FA2 on H100, but no improvement on A100 due to async tile dependency.\", salience=0.65, memory_type=\"episodic\")") ``` ### Key insight When you discover something reusable: ``` request(ops="memory.remember(content=\"INSIGHT: knowledge.search with rerank=true gives normalized 0-1 scores vs raw RRF ~0.016. Always use rerank for score comparison.\", salience=0.75, memory_type=\"semantic\")") ``` ### Session start recall At the beginning of a session, recall recent context: ``` request(ops="memory.recall(query=\"recent session work progress\", limit=5, memory_type=\"episodic\")") ``` Then make targeted recalls based on what you are about to work on: ``` request(ops="memory.recall(query=\"FlashAttention benchmark results\", limit=5)") ``` ### Agent handoff When handing off work to another agent: ``` request(ops="memory.remember(content=\"HANDOFF: Attention benchmark suite is ready at benchmarks/attention/. Next step: run on H100 cluster. Contact: agent:platform for GPU allocation.\", salience=0.8, memory_type=\"episodic\")") ``` ## See also - [Search and Retrieval](search.md): how hybrid search and RRF fusion work - [Prompt Cookbook](prompt-cookbook.md): memory verb patterns - [GTD Task Management](tasks.md): task lifecycle (often paired with memory for context) - [Agent Sessions and Data Ingest](sessions-and-ingest.md): a separate, session-scoped persistence model for transcripts and provider mirrors --- ## Search and Retrieval Source: https://ohdearquant.github.io/khive/search.html # Search and Retrieval This guide covers how to find things in khive — from keyword search to vector similarity to graph traversal — and when to use each approach. ## Five ways to retrieve khive offers five retrieval verbs, each suited to a different question shape: | Verb | Question shape | Example | | --------------------- | ----------------------------------- | ------------------------------------------- | | `get(id)` | "I have a UUID, give me the record" | Fetch a known entity after a link operation | | `search(kind, query)` | "Find things about X" | Discover entities matching a topic | | `list(kind, filters)` | "Show me all Y" | Browse all concepts, all edges from a node | | `neighbors(node_id)` | "What connects to this?" | One-hop graph exploration | | `traverse(roots)` | "What is reachable within N hops?" | Multi-hop lineage, clusters | | `query(gql)` | "Pattern match over the graph" | Complex structural queries | ## Text search: `search` `search` combines full-text search (FTS5 trigram) with vector similarity (embedding-based) using Reciprocal Rank Fusion (RRF). ### Basic search ``` request(ops="search(kind=\"entity\", query=\"memory efficient attention\")") ``` Returns a scored list: ```json [ {"id": "a1b2c3d4", "name": "FlashAttention", "score": 0.82, ...}, {"id": "e5f6g7h8", "name": "PagedAttention", "score": 0.71, ...} ] ``` ### Search notes ``` request(ops="search(kind=\"note\", query=\"tiling recomputation\")") ``` Note search automatically excludes superseded notes (notes targeted by a `supersedes` edge). This is a view-layer filter — the old notes still exist. ### Filtered search Narrow by entity kind, type, or tags: ``` request(ops="search(kind=\"entity\", query=\"attention\", entity_kind=\"concept\", tags=[\"ml\"])") ``` ### Score interpretation Scores from `search` are RRF fusion scores. Raw RRF values are typically small (0.01-0.03). When `rerank` is active (via `knowledge.search`), scores are normalized to [0,1]. A practical floor: results below 0.3 are usually noise. Results above 0.7 are strong matches. ## Structured browse: `list` `list` returns records matching structured filters, without text similarity: ``` request(ops="list(kind=\"entity\", entity_kind=\"concept\", limit=20)") request(ops="list(kind=\"edge\", source_id=\"\")") request(ops="list(kind=\"note\", note_kind=\"decision\", limit=10)") ``` Use `list` when you want categorical browsing, not similarity ranking. ## Graph navigation: `neighbors` and `traverse` ### One-hop: neighbors ``` request(ops="neighbors(node_id=\"\", direction=\"both\")") ``` Direction options: `out`, `in`, `both` (default). Omitting `direction` returns edges in both directions; pass `out` or `in` when you specifically want only one side. Filter by relation: ``` request(ops="neighbors(node_id=\"\", direction=\"in\", relations=[\"extends\", \"variant_of\"])") ``` ### Multi-hop: traverse ``` request(ops="traverse(roots=[\"\"], max_depth=3, relations=[\"extends\", \"variant_of\"])") ``` Returns paths — each path is a list of nodes from root to leaf. Use `include_roots=false` to exclude the starting nodes from results. Traverse is BFS-based. It respects `direction` (default: `both`) and `relations` filters. ## Pattern matching: `query` For complex structural questions, use GQL or SPARQL: ### GQL ``` request(ops="query(query=\"MATCH (a:concept)-[:extends]->(b:concept) WHERE b.name = 'LoRA' RETURN a\")") ``` ``` request(ops="query(query=\"MATCH (p:document)<-[:introduced_by]-(c:concept)<-[:implements]-(impl:project) RETURN c.name, impl.name\")") ``` ### SPARQL ``` request(ops="query(query=\"SELECT ?a WHERE { ?a :extends+ ?b . ?b :name 'LoRA' . } LIMIT 10\")") ``` Both syntaxes compile to the same SQL backend. Use whichever feels natural. ## Knowledge search: rerank and decompose The `knowledge.search` verb adds two capabilities on top of base search: ### Reranking ``` request(ops="knowledge.search(query=\"memory efficient attention mechanisms\", rerank=true)") ``` Reranking uses a cross-encoder model to re-score results after the initial retrieval pass. This produces clean [0,1] scores instead of raw RRF values. Reranking is on by default for `knowledge.search`. ### Query decomposition ``` request(ops="knowledge.search(query=\"compare LoRA and QLoRA fine-tuning approaches\", decompose=true)") ``` Decomposition splits multi-concept queries into sub-queries, runs them independently, and merges the results. This avoids FTS edge cases where compound queries miss relevant documents. Use `decompose=true` when your query mentions multiple distinct concepts. ## Memory recall `memory.recall` is a specialized search over memory notes with decay-weighted scoring: ``` request(ops="memory.recall(query=\"attention optimization\", limit=5)") ``` See [Memory and Recall](memory.md) for the full scoring formula and usage patterns. ## Choosing the right retrieval | You want to... | Use | | --------------------------------- | -------------------------------------------- | | Find entities about a topic | `search(kind="entity", query="...")` | | Find notes about a topic | `search(kind="note", query="...")` | | Browse all entities of a kind | `list(kind="entity", entity_kind="concept")` | | See what connects to a node | `neighbors(node_id="...", direction="both")` | | Explore multi-hop paths | `traverse(roots=["..."], max_depth=3)` | | Structural pattern matching | `query(query="MATCH ...")` | | Find knowledge atoms with scoring | `knowledge.search(query="...", rerank=true)` | | Recall agent memories | `memory.recall(query="...")` | ## Performance notes - **Cold start**: the first search in a session loads the ANN index and embedding model. The daemon keeps these warm for subsequent calls. - **Daemon**: khive auto-spawns `kkernel mcp --daemon` on first request. The daemon keeps indexes hot across sessions. - **Vector search without embeddings**: if running with `--no-embed`, only FTS results are returned (no vector similarity). ## See also - [Prompt Cookbook](prompt-cookbook.md) — search patterns with full syntax - [Memory and Recall](memory.md) — memory-specific recall with decay - [AGENTS.md](../../AGENTS.md) — GQL and SPARQL examples --- ## GTD Task Management Source: https://ohdearquant.github.io/khive/tasks.html # GTD Task Management This guide covers task management in khive — the GTD lifecycle, priority levels, task dependencies, and common workflow patterns. ## What tasks are Tasks in khive are notes with `kind=task`, managed by the GTD pack. They have a status lifecycle, priority level, optional assignee, and can be linked to entities in the knowledge graph. Tasks are created with `gtd.assign`, not `create(kind="note")`. The GTD pack handles lifecycle validation and status transitions. ## Task lifecycle ``` inbox ──> next ──> active ──> done │ │ │ │ │ └──> cancelled │ │ └──> someday waiting ◄──┐ │ │ └────────────────────┘ ``` ### Status meanings | Status | Meaning | When to use | | ----------- | ----------------------------- | ----------------------------------------------------- | | `inbox` | Captured but not committed | Default. Something that needs triage. | | `next` | Committed, ready to work on | After triage — this is actionable and prioritized. | | `active` | Currently in progress | When you start working on it. | | `done` | Completed | Finished successfully. | | `cancelled` | Abandoned | No longer relevant. | | `waiting` | Blocked on something external | Waiting for a response, a dependency, or a condition. | | `someday` | Deferred indefinitely | Not urgent, not committed, but worth remembering. | ### Valid transitions Not all transitions are valid. The GTD pack validates them: - `inbox` can go to: `next`, `someday`, `cancelled` - `next` can go to: `active`, `waiting`, `someday`, `cancelled` - `active` can go to: `done`, `waiting`, `cancelled` - `waiting` can go to: `next`, `active`, `cancelled` - `someday` can go to: `next`, `cancelled` Idempotent transitions (same status to same status) are accepted silently. ## Creating tasks ### Basic task ``` request(ops="gtd.assign(title=\"Implement FlashAttention-3 in lattice\")") ``` Defaults: `status=inbox`, `priority=p2`. ### Task with priority and status ``` request(ops="gtd.assign(title=\"Benchmark attention variants\", priority=\"p0\", status=\"next\")") ``` ### Task linked to an entity ``` request(ops="gtd.assign(title=\"Review FlashAttention paper\", context_entity_id=\"\")") ``` The `context_entity_id` links the task to a KG entity, making it discoverable via graph traversal. ### Task with assignee ``` request(ops="gtd.assign(title=\"Write attention tests\", assignee=\"agent:platform\")") ``` ### Task with tags ``` request(ops="gtd.assign(title=\"Profile memory usage\", tags=[\"perf\", \"attention\"])") ``` ## Priority levels | Priority | Meaning | | -------- | ---------------------------------------- | | `p0` | Critical — do now, everything else waits | | `p1` | High — do today | | `p2` | Normal — do this cycle (default) | | `p3` | Low — do when convenient | ## Working with tasks ### Get next actions ``` request(ops="gtd.next(limit=5)") ``` Returns tasks with `status` in `[next, active]`, sorted by priority (p0 first). ### List tasks by status ``` request(ops="gtd.tasks(status=\"active\")") request(ops="gtd.tasks(status=\"waiting\", assignee=\"agent:docs\")") ``` ### Transition a task ``` request(ops="gtd.transition(id=\"\", status=\"active\", note=\"started implementation\")") ``` The `note` parameter records why the transition happened. ### Complete a task ``` request(ops="gtd.transition(id=\"\", status=\"done\")") ``` You can also use `gtd.complete`: ``` request(ops="gtd.complete(id=\"\", result=\"all benchmarks pass\")") ``` Note: `gtd.complete` requires the task to be in `active` status. Use `gtd.transition(status="done")` if the task is in another status. ## Task dependencies Tasks can depend on other tasks using `depends_on` edges. The GTD pack extends the base edge contract to allow task-to-task `depends_on` relationships: ``` request(ops="gtd.assign(title=\"Write tests\", status=\"next\")") # Get the task id from the response request(ops="gtd.assign(title=\"Run CI\", depends_on=[\"\"])") ``` Or link them explicitly: ``` request(ops="link(source_id=\"\", target_id=\"\", relation=\"depends_on\")") ``` ## Workflow patterns ### Daily review ``` request(ops="gtd.next(limit=10)") ``` Review actionable tasks, reprioritize, transition stale items to `waiting` or `someday`. ### Triage inbox ``` request(ops="gtd.tasks(status=\"inbox\")") ``` For each inbox item, decide: promote to `next`, defer to `someday`, or `cancel`. ``` request(ops="gtd.transition(id=\"\", status=\"next\", note=\"promoted after review\")") ``` ### Context switching When picking up a new area of work, filter by assignee or tags: ``` request(ops="gtd.tasks(assignee=\"agent:docs\", status=\"next\")") ``` ### Batch status update ``` request(ops="[gtd.transition(id=\"\", status=\"done\"), gtd.transition(id=\"\", status=\"done\")]") ``` ## See also - [Prompt Cookbook](prompt-cookbook.md) — task verb patterns - [Knowledge Graph Modeling](knowledge-graph.md) — linking tasks to entities - [Memory and Recall](memory.md) — storing context alongside tasks --- ## Communication and Email Source: https://ohdearquant.github.io/khive/communication.html # Communication and Email This guide covers the comm pack: actor-addressed messaging inside khive, and the optional email channel that bridges that same messaging model to an external mailbox. ## What messages are Messages are notes with `kind=message`, managed by the comm pack (`crates/khive-pack-comm/`). `comm.send` writes both an outbound copy (in the sender's namespace) and an inbound copy (addressed to the recipient), so a send always produces two notes and no cross-namespace write occurs even when `to` names a different actor. ## Actor addressing Actors are labeled strings such as `lambda:leo` or `lambda:khive`. `comm.send` stores the caller's actor label as `from_actor` and the `to` argument as `to_actor` on both the outbound and inbound copies. ``` request(ops="comm.send(to=\"lambda:leo\", content=\"PR #610 merged\")") ``` `comm.inbox` filters by `to_actor` for the calling actor. Legacy messages written before actor addressing existed have no `to_actor` field and remain visible to every actor (an `EqOrMissing` match), so older history is not hidden by the newer filter. ### Send | Param | Type | Required | Notes | | ----------- | ------ | -------- | ------------------------------------------- | | `to` | string | yes | Actor label, e.g. `"lambda:leo"`. | | `content` | string | yes | Message body. Must not be empty. | | `subject` | string | no | Optional subject line. | | `thread_id` | uuid | no | Groups the message into an existing thread. | ### Inbox | Param | Type | Required | Notes | | -------- | ------- | -------- | -------------------------------------------- | | `limit` | integer | no | Default 20, max 200. | | `status` | string | no | `"unread"` (default) \| `"read"` \| `"all"`. | ``` request(ops="comm.inbox(limit=10)") request(ops="comm.inbox(status=\"all\")") ``` ### Read Marks an inbound message read. Outbound messages cannot be marked read. ``` request(ops="comm.read(id=\"\")") ``` `id` accepts either a full UUID or a short 8-character hex prefix. ### Reply Replies thread against the original message. If the original had no subject, the reply carries no subject either; otherwise the reply subject is prefixed `Re:` (and not re-prefixed if it already starts with `Re:`). ``` request(ops="comm.reply(id=\"\", content=\"Thanks, following up now\")") ``` ### Thread Retrieves every message in a conversation thread, ordered chronologically, given the thread root's id. ``` request(ops="comm.thread(id=\"\", limit=50)") ``` `limit` defaults to 100 and caps at 500. ### Health `comm.health()` is a read-only, no-argument verb that reports per-channel polling state, keyed by `(channel_kind, channel_slug)`. It never returns a computed `healthy` boolean: staleness and alerting judgment stay with the caller, not the pack. ``` request(ops="comm.health()") ``` Each entry in the returned `channels` array carries: | Field | Notes | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `channel_kind` | e.g. `"email"`. | | `channel_slug` | Per-credential identifier (the configured mailbox address for the email channel), so two accounts of the same `channel_kind` get distinct rows. | | `last_success_at` | Timestamp of the most recent successful poll attempt, or `null`. | | `last_failure_at` | Timestamp of the most recent failed poll attempt, or `null`. | | `last_poll_attempt_at` | Timestamp of the most recent poll attempt regardless of outcome. | | `last_error` | `{class, message, at}` of the most recent failure. `class` is one of `auth`, `transport`, `config` (an open enum; callers must tolerate unknown values). | | `consecutive_failures` | Resets to 0 on success, increments on failure. | `last_error` is retained after a later success: a success updates `last_success_at` and resets `consecutive_failures` to 0 but never clears `last_error`. Compare `last_error.at` against `last_success_at` to tell a resolved failure from one that is still live. Heartbeat rows are always persisted to, and read from, the local operational namespace, regardless of the caller's own namespace or `KHIVE_EMAIL_INGEST_NAMESPACE`. These rows are an operational surface, not message data, so they must be visible to a no-arg `comm.health()` call independent of where the caller's own messages happen to be ingested. The `role` field is `"daemon"` (with `source: "daemon-heartbeat"`) whenever any persisted heartbeat row exists, and `"client"` with an empty `channels` array otherwise. This distinguishes who owns the channel loops, not which process answered the call: any persisted row means some daemon owns the loops, even when this particular call was served by a different, non-daemon process. **Known ambiguity:** an empty `channels` array cannot distinguish "no daemon has ever run" from "channels are configured but a poll has never completed." The comm pack has no visibility into channel configuration (that lives in `khive-mcp` / `khive-channel-email`), so `role: "client"` with an empty `channels` array means only "no daemon heartbeat state exists," not "nothing is configured." Results are capped at 200 channels. A full page logs a `tracing::debug!` line noting that results may be silently truncated. ## The email channel The email channel (`crates/khive-channel-email/`) bridges `comm.send` / `comm.inbox` to a real mailbox over SMTP and IMAP. It is not part of the default build; see [Feature gating](#feature-gating) below. ### Addressing an email recipient Send to an email address by prefixing `to` with `email:` and passing an explicit `subject`. Because the outbox loop reads `subject` off the stored note, a mail sent without `subject` goes out with `(no subject)` in the subject line. ``` request(ops="comm.send(to=\"email:prof.sheng@example.edu\", subject=\"Draft ready for review\", content=\"...\")") ``` ### How outbound delivery works `comm.send` itself only writes the note; it does not talk to SMTP directly. A background outbox loop polls every 5 seconds for undelivered outbound notes: ``` list(namespace=, kind="message", direction="outbound", delivered=false, limit=200) ``` For each note returned, the loop keeps only those where `to_actor` starts with `email:` and the note is not already delivered, then checks the recipient against the allowlist (`KHIVE_EMAIL_SEND_ALLOWED_RECIPIENTS`, or the channel's maintainer address if that variable is unset). Passing notes are sent over SMTP, using the note's `subject`, `content`, and any `thread_id`/`in_reply_to_message_id`/`references_chain` properties to set the RFC 822 `Message-ID`, `In-Reply-To`, and `References` headers so replies group correctly in native mail clients. ### How inbound ingestion works A separate poll loop reads the IMAP mailbox every 5 seconds and, for each new message, calls the pack-internal `comm.ingest` subhandler (not callable directly over the MCP wire) with the parsed envelope: `from`, `to`, `content`, `subject`, `channel_kind`, `external_id` (an IMAP-derived dedup key of the form `imap:{host}:{uidvalidity}:{uid}`), `sent_at`, and the wire threading fields `wire_message_id` / `wire_references`. Duplicate `external_id` values are ignored, making re-delivery idempotent. ### Configuration `EmailChannelConfig::from_env` reads configuration exclusively from environment variables; there is no file-based config for this channel. See [Configuration](../configuration.md) for the full khive-wide environment variable reference. The email-specific variables are: Required: - `KHIVE_EMAIL_SMTP_HOST` - `KHIVE_EMAIL_IMAP_HOST` - `KHIVE_EMAIL_USERNAME` - `KHIVE_EMAIL_MAINTAINER_ADDRESS` (comma-separated; the first entry is primary and used for outbound-allowlist defaulting) - `KHIVE_EMAIL_AUTHSERV_ID` (the trust anchor for validating inbound `Authentication-Results` headers; the reserved value `!topmost-no-authserv-id` selects trust of the topmost header when the receiving boundary emits no `authserv-id` at all, as with Exchange Online's internal-hop stamp) Auth mode (choose one): - Basic: `KHIVE_EMAIL_PASSWORD` - OAuth (Exchange Online app-only client-credentials flow): `KHIVE_EMAIL_OAUTH_CLIENT_ID`, `KHIVE_EMAIL_OAUTH_TENANT_ID`, `KHIVE_EMAIL_OAUTH_CLIENT_SECRET` (all three required together; a partial set is a config error, never a silent fallback to Basic) Optional, with defaults: - `KHIVE_EMAIL_SMTP_PORT` (default `587`) - `KHIVE_EMAIL_IMAP_PORT` (default `993`) - `KHIVE_EMAIL_MAILBOX` (default: same as `KHIVE_EMAIL_USERNAME`) - `KHIVE_EMAIL_QUARANTINE_STORE` (default `true`; when a message fails the sender-authentication or allowlist gate, store it as an unattributed quarantine record instead of dropping it) - `KHIVE_EMAIL_INGEST_NAMESPACE` (default `local`; target namespace for ingested messages) - `KHIVE_EMAIL_DEFAULT_ACTOR` (default `lambda:leo`; inbound actor assigned to fresh, uncorrelated email messages) - `KHIVE_EMAIL_SEND_ALLOWED_RECIPIENTS` (comma-separated outbound allowlist; falls back to the maintainer address when unset) ### Feature gating `channel-email` is an optional Cargo feature (`crates/khive-mcp/Cargo.toml`), not compiled into the plain `cargo build --workspace --release` invocation used by `make build` or by any release/CI workflow. It is enabled explicitly by `make local` (`cargo build --release --features channel-email`). A binary built without this feature has no email channel code at all: `to="email:..."` sends still write a note (the comm pack has no awareness of channels), but nothing polls IMAP or drains the outbox, so the message is never delivered. ### Daemon-only channel loops The email poll loop and the outbox loop are spawned only by the persistent daemon process (`kkernel mcp --daemon`), never by a plain stdio `kkernel mcp` client. This is a deliberate role gate (issue #602): before it existed, every stdio client process spawned its own independent IMAP poll loop against the same mailbox, and nine concurrent pollers exhausted Exchange Online's per-mailbox connection slots, taking inbound email down for about 19 hours on 2026-07-04. The gate logs one line at startup either way, so the decision is observable: ``` email channel loops: spawning (daemon role) email channel loops: skipped (client role; daemon owns channel loops) ``` If the ingest namespace fails authorization, the loops are not started at all (fail-closed) and this is logged separately: ``` email channel loops NOT started: ingest namespace authorization failed (fail-closed) ``` If no daemon is running, mail is simply not polled until one starts. That is the intended behavior, not a silent failure. ## Limitations Actor addressing (`to_actor` filtering on `comm.send`/`comm.inbox`) is a view-layer convention for cooperating, co-located actors, not a security boundary ([ADR-063](../adr/ADR-063-comm-principal-model.md)). Any process with access to the underlying SQLite store can read every message row regardless of `to_actor`, and there is no per-principal storage partition on the local backend. Where authorization is enforced, it lives at a single seam, the Gate ([ADR-018](../adr/ADR-018-authorization-gate.md)), not at the comm pack's inbox filter. ## See also - [Agent Sessions and Data Ingest](sessions-and-ingest.md): a different ingestion path, transcript mirroring rather than message channels. - [Configuration](../configuration.md): the full environment variable reference. --- ## Agent Sessions and Data Ingest Source: https://ohdearquant.github.io/khive/sessions-and-ingest.html # Agent Sessions and Data Ingest This guide covers the session pack (`crates/khive-pack-session/`), which has two independent parts that are easy to conflate but serve different purposes: - **Session verbs** (`session.store`, `session.list`, `session.resume`, `session.export`): explicit, caller-driven persistence of a session record as a khive note. - **Provider mirror ingest**: an optional background service that tails agent CLI transcript files and ChatGPT data exports into their own SQL tables, entirely separate from the note substrate. Do not assume these two share storage. A session stored via `session.store` is a `kind=session` note, queryable like any other note. A conversation picked up by the mirror service lands in the `sessions` / `session_messages` tables and is not a note at all. ## Session verbs `session.store` persists a session as a note with `kind="session"`. The pack declares `NOTE_KINDS = &["session"]` and requires the `kg` pack. ### Store | Param | Type | Required | Notes | | --------------------- | --------------- | -------- | ---------------------------------------------------------- | | `content` | string | yes | Verbatim transcript or summary content. Must not be empty. | | `title` | string | no | Stored as the note's `name`. | | `provider` | string | no | Provider label, e.g. `codex`, `claude_code`, `openai`. | | `provider_session_id` | string | no | Provider-native continuity anchor. | | `tags` | array of string | no | Stored in `properties.tags`. | ``` request(ops="session.store(content=\"...\", title=\"PR #610 review session\", provider=\"claude_code\")") ``` Any string field that is supplied must be non-empty after trimming; an empty `content`, or a present-but-blank `title`/`provider`/`provider_session_id`, or an empty tag entry, is rejected with a validation error naming the offending field. ### List | Param | Type | Required | Notes | | ---------- | ------- | -------- | -------------------------------------------- | | `limit` | integer | no | 1 to 200; default 20. | | `offset` | integer | no | Default 0. | | `provider` | string | no | Exact match filter on `properties.provider`. | ``` request(ops="session.list(provider=\"codex\", limit=10)") ``` Results are ordered newest first and returned as summaries (no `content` field) with a `total` count when the underlying store can supply one. ### Resume Fetches one session's full content, including `content`, by id. ``` request(ops="session.resume(id=\"\")") ``` `id` accepts a full UUID or an 8-or-more character hex prefix. A prefix that matches no record, an id that resolves to a note of any kind other than `session`, or a well-formed id with no matching record all produce distinct errors (a resolution error naming the mismatched kind, or a not-found error), so a caller can tell "wrong kind of record" apart from "does not exist" apart from "malformed id". ### Export Serializes one stored session as `json` (the default) or `markdown`. ``` request(ops="session.export(id=\"\", format=\"markdown\")") ``` `format` is validated before id resolution, so an invalid format is rejected even if the id itself would not resolve. The markdown form renders a heading from the title (or `Session {first 8 chars of id}` if untitled), a metadata list (`id`, `provider`, `provider_session_id`, `created_at`, `updated_at`, `tags`), and a `## Content` section containing the raw stored content verbatim. ## Provider mirror ingest The mirror service (`crates/khive-pack-session/src/mirror/`) is a background task, distinct from the four verbs above, that discovers and tails local transcript files and writes their events into three dedicated tables (`sessions`, `session_messages`, `session_mirror_cursor`) created by the pack's schema plan. It never calls `session.store` and does not create `session` notes. ### Supported providers Only three sources are implemented (`MirrorSource` in `mirror/ingest.rs`): - **Claude Code CLI transcripts** (`claude_code`): JSONL files under `KHIVE_MIRROR_PROJECTS_DIR`. - **Codex CLI transcripts** (`codex`): JSONL files under `KHIVE_MIRROR_CODEX_DIR`. - **ChatGPT data exports** (`chatgpt_export`): a `conversations.json` file (the format ChatGPT's "export data" produces) under `KHIVE_MIRROR_CHATGPT_DIR`. There is no ingest path for claude.ai (the web product) conversation history. If a future provider adds that, it belongs here as a fourth `MirrorSource` variant, not folded into the existing ChatGPT or Claude Code parsers. ### Enabling it The mirror service only starts if at least one enable flag is true. This is checked once, in the session pack's `warm()` lifecycle hook, and `warm()` is only invoked by the persistent daemon's startup path (`crates/khive-runtime/src/daemon.rs`), not by a plain stdio client. So, like the email channel loops described in [Communication and Email](communication.md), running `kkernel mcp --daemon` is what actually starts background ingestion; a stdio session never spawns its own mirror poller. | Variable | Default | | ------------------------------ | ------------------------ | | `KHIVE_MIRROR_ENABLED` | `false` | | `KHIVE_MIRROR_PROJECTS_DIR` | `$HOME/.claude/projects` | | `KHIVE_MIRROR_CODEX_ENABLED` | `false` | | `KHIVE_MIRROR_CODEX_DIR` | `$HOME/.codex/sessions` | | `KHIVE_MIRROR_CHATGPT_ENABLED` | `false` | | `KHIVE_MIRROR_CHATGPT_DIR` | `$HOME/.chatgpt/exports` | | `KHIVE_MIRROR_POLL_SECS` | `2` | | `KHIVE_MIRROR_BACKFILL` | `true` | `KHIVE_MIRROR_POLL_SECS=0` is rejected (falls back to the default rather than busy-looping); a non-numeric value likewise falls back to the default, with a warning logged in both cases. ### provider_session_id and idempotency Each mirrored file is tracked in `session_mirror_cursor` by file path and byte offset, so re-running the service resumes tailing from where it left off rather than re-reading from the start. Writes into `sessions` and `session_messages` are transactional and idempotent (`INSERT OR IGNORE` / `ON CONFLICT DO NOTHING` on stable keys), so a crash mid-write or a re-processed byte range does not duplicate rows. `provider_session_id` values are the mirror's continuity anchor across restarts. Reads are bounded in size (a byte/line/event cap per read) so a single oversized file cannot stall the tailer; the ChatGPT export path additionally caps the whole file at `KHIVE_MIRROR_CHATGPT_MAX_BYTES` (default 256 MiB). ### Worked example Enable Codex mirroring only. Note that mirroring and the note-based session verbs are separate stores today: the mirror writes provider data (including `source`) into the private `sessions` table, while `session.list` queries khive-native `session.store` records and filters `properties.provider` on those notes. `session.list(provider="codex")` therefore does not return mirrored rows. ```bash KHIVE_MIRROR_ENABLED=false \ KHIVE_MIRROR_CODEX_ENABLED=true \ KHIVE_MIRROR_CODEX_DIR="$HOME/.codex/sessions" \ kkernel mcp --daemon ``` To store a manual session summary through the note-based verbs instead (unaffected by any mirror configuration): ``` request(ops="session.store(content=\"Reviewed PR #610, confirmed daemon-only gating\", title=\"PR 610 review\", provider=\"claude_code\")") request(ops="session.list(provider=\"claude_code\", limit=5)") ``` ## See also - [Communication and Email](communication.md): the other daemon-only background loop (email channel polling), and the same warm-hook / daemon-role pattern. - [Specialized Packs](specialized-packs.md): packs beyond the eight loaded by default. --- ## Specialized Packs Source: https://ohdearquant.github.io/khive/specialized-packs.html # Specialized Packs khive's default install loads nine production packs (`kg, gtd, memory, brain, comm, schedule, knowledge, session, git`, per `RuntimeConfig::default()` in `crates/khive-runtime/src/config.rs`). Of these, `git` already contributes zero verbs of its own — it registers the `commit` / `issue` / `pull_request` note kinds and a batch ingester for a git-lifecycle provenance graph (see `crates/khive-pack-git/`). Beyond the default set, khive also ships niche packs that extend the graph for a specific domain without adding verbs of their own. This guide covers the formal-math pack, the first of these, and how pack loading works in general. ## Pack composition model Every pack implements the `Pack` trait (`crates/khive-types/`) and declares, additively, what it contributes: note kinds, entity kinds, verb handlers, and edge endpoint rules. A pack can declare zero verbs and still be useful, contributing purely to the edge ontology. Packs declare a `REQUIRES` list of other packs that must already be loaded; the runtime resolves this at startup. See [ADR-017](../adr/ADR-017-pack-standard.md) for the full standard, including how pack-declared edge endpoint rules combine with the base ADR-002 contract: rules are additive only, never tightening what the base contract already allows. ### Loading a pack Packs are selected via the `--pack` CLI flag (repeatable) or the `KHIVE_PACKS` environment variable (comma- or whitespace-separated): ```bash kkernel mcp --pack kg --pack gtd --pack formal # or KHIVE_PACKS="kg,gtd,formal" kkernel mcp ``` `formal` declares `REQUIRES = &["kg"]`, so `kg` must also be in the load set. ## The formal pack `crates/khive-pack-formal/` is a pure ontology extension for formal mathematics, targeting Lean-style proof developments, built around six concept subtypes: `theorem`, `definition`, `structure`, `instance`, `axiom`, and `goal`. It is not part of the default pack set; opt in explicitly. ### What it contributes `FormalPack` declares: - `NOTE_KINDS = &[]`, `ENTITY_KINDS = &[]`, `HANDLERS = &[]`: no new note kinds, entity kinds, or verbs. - `EDGE_RULES = &FORMAL_EDGE_RULES`: 21 additive edge endpoint rules. Every rule is expressed via `EndpointKind::EntityOfType { kind: "concept", entity_type: }`: all six subtypes are `concept` entities distinguished by their `entity_type` property, not by a new `EntityKind` variant. Because `dispatch()` unconditionally returns an error naming the verb, loading `formal` cannot be used to call any verb. Its only effect is widening which typed edges the graph accepts. ### Endpoint rules by relation | Relation | Rule count | Pairs | | ------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `depends_on` | 14 | theorem to {theorem, definition, structure, axiom}; definition to {definition, structure, theorem, axiom}; instance to {structure, definition}; goal to {theorem, definition, structure, axiom} | | `instance_of` | 1 | instance to structure | | `extends` | 2 | structure to structure; definition to definition | | `variant_of` | 4 | theorem to theorem; definition to definition; goal to theorem; goal to definition | `depends_on` models the prerequisite chain, so the source uses or builds on a target: a theorem may depend on other theorems, definitions, structures, or axioms it invokes, and a `goal` (an unproved target) may depend on the same four subtypes it will eventually need. `instance_of` models an instance implementing a structure. `extends` models structural or definitional inheritance. `variant_of` models a restatement, including a `goal` framed as a variant of an existing theorem or definition, which is useful as an anti-duplicate signal when the same result is proposed as a fresh goal. ### Example ``` request(ops="create(kind=\"concept\", name=\"Cauchy-Schwarz\", properties={\"entity_type\": \"theorem\"})") request(ops="create(kind=\"concept\", name=\"Inner product space\", properties={\"entity_type\": \"structure\"})") request(ops="link(source_id=\"\", target_id=\"\", relation=\"depends_on\")") ``` With only `kg` loaded (no `formal`), the same `link` call is rejected. The base ADR-002 contract does not admit a `concept`-to-`concept` `depends_on` edge between two arbitrary subtypes on its own; the `formal` pack's rules are what makes this specific `(theorem, depends_on, structure)` triple legal. ## See also - [Knowledge Graph Modeling](knowledge-graph.md): the base entity kind and edge relation taxonomy that specialized packs extend. - [Agent Sessions and Data Ingest](sessions-and-ingest.md): another optional pack (`session`), included in the default set but with its own opt-in background service. --- ## Prompt Cookbook Source: https://ohdearquant.github.io/khive/prompt-cookbook.html # Prompt Cookbook Ready-to-use patterns for every common khive operation. Each pattern shows the exact `request(ops="...")` syntax, expected response shape, and when to use it. All examples use the function-call DSL form. JSON form is equivalent; use it when the DSL string would be hard to escape. --- ## Create and link ### Create an entity ``` request(ops="create(kind=\"entity\", entity_kind=\"concept\", name=\"FlashAttention\", description=\"IO-aware exact attention algorithm\", properties={\"domain\": \"attention\", \"year\": 2022})") ``` Response: ```json {"ok": true, "result": {"id": "a1b2c3d4", "kind": "concept", "name": "FlashAttention", ...}} ``` Use when: you encounter a new algorithm, paper, project, or any named thing worth tracking. Always `search` first to avoid duplicates. ### Create a note ``` request(ops="create(kind=\"note\", note_kind=\"observation\", content=\"FlashAttention reduces memory from O(N^2) to O(N) by tiling and recomputation\", salience=0.7)") ``` Use when: you want to record a finding, insight, or decision. Notes are temporal; entities are structural. ### Create an annotated note ``` request(ops="create(kind=\"note\", note_kind=\"insight\", content=\"Tiling is the common technique across all IO-aware attention methods\", annotates=[\"\"])") ``` Use when: your observation is about a specific entity. The `annotates` edge makes it discoverable via `neighbors`. ### Link two entities ``` request(ops="link(source_id=\"\", target_id=\"\", relation=\"introduced_by\", weight=1.0)") ``` Use when: you discover a relationship. Direction matters, so check the [edge relation guide](knowledge-graph.md) for source/target conventions. ### Batch create ``` request(ops="[create(kind=\"entity\", entity_kind=\"concept\", name=\"GQA\"), create(kind=\"entity\", entity_kind=\"concept\", name=\"MQA\"), create(kind=\"entity\", entity_kind=\"concept\", name=\"MHA\")]") ``` Use when: you have multiple independent entities to create. Batched ops run in parallel with no ordering guarantee. --- ## Search and discover ### Search entities ``` request(ops="search(kind=\"entity\", query=\"memory efficient attention\")") ``` ### Search with filters ``` request(ops="search(kind=\"entity\", query=\"attention\", entity_kind=\"concept\", tags=[\"ml\"])") ``` ### Search notes ``` request(ops="search(kind=\"note\", query=\"tiling recomputation\")") ``` Automatically excludes superseded notes. ### Knowledge search with rerank ``` request(ops="knowledge.search(query=\"parameter efficient fine-tuning methods\", rerank=true)") ``` Use when: you want normalized [0,1] scores. Reranking uses a cross-encoder for higher quality scoring. Default is on for `knowledge.search`. ### Knowledge search with decompose ``` request(ops="knowledge.search(query=\"compare LoRA and QLoRA for 7B models\", decompose=true)") ``` Use when: your query mentions multiple distinct concepts. Decomposition splits the query and merges results. --- ## Navigate the graph ### One-hop neighbors ``` request(ops="neighbors(node_id=\"\", direction=\"both\")") ``` Use when: you want to see everything connected to a node. The default direction is `both`; pass `out` or `in` only when you need one side. ### Filtered neighbors ``` request(ops="neighbors(node_id=\"\", direction=\"in\", relations=[\"extends\", \"variant_of\"])") ``` Use when: you want only specific relationship types. ### Multi-hop traverse ``` request(ops="traverse(roots=[\"\"], max_depth=3, relations=[\"extends\", \"variant_of\"], include_roots=false)") ``` Use when: you want to explore lineage: what extends what, multi-hop dependency chains, reachability analysis. ### GQL query ``` request(ops="query(query=\"MATCH (a:concept)-[:implements]->(b:project) RETURN a.name, b.name LIMIT 10\")") ``` ### SPARQL query ``` request(ops="query(query=\"SELECT ?c WHERE { ?c :extends+ ?b . ?b :name 'LoRA' . } LIMIT 10\")") ``` Use GQL or SPARQL when: you need pattern matching over the graph structure, for example "find all concepts that extend something introduced by a specific paper". --- ## Memory ### Store a memory ``` request(ops="memory.remember(content=\"khive uses RRF fusion for hybrid search scoring\", salience=0.8, memory_type=\"semantic\")") ``` `memory_type`: `episodic` (default) or `semantic` only. Salience: 0.0-1.0 (higher = more important for recall ranking). ### Recall memories ``` request(ops="memory.recall(query=\"hybrid search scoring\", limit=5)") ``` ### Tag-filtered recall ``` request(ops="memory.recall(query=\"search optimization\", limit=5, tags=[\"khive\"], tag_mode=\"any\")") ``` ### Store a memory linked to an entity ``` request(ops="memory.remember(content=\"FlashAttention-3 uses asynchronous tiling on H100\", salience=0.7, source_id=\"\")") ``` The `source_id` creates an `annotates` edge from the memory note to the entity. --- ## Tasks (GTD) ### Create a task ``` request(ops="gtd.assign(title=\"Implement FlashAttention-3 in lattice\", priority=\"p1\", status=\"next\")") ``` Defaults: `status=inbox`, `priority=p2`. ### Create a task linked to an entity ``` request(ops="gtd.assign(title=\"Benchmark attention variants\", priority=\"p1\", context_entity_id=\"\")") ``` ### Get next actions ``` request(ops="gtd.next(limit=5)") ``` Returns tasks with `status` in `[next, active]`, sorted by priority. ### Transition a task ``` request(ops="gtd.transition(id=\"\", status=\"active\", note=\"started implementation\")") ``` Lifecycle: `inbox` -> `next` -> `active` -> `done` (or `cancelled`). Also available: `waiting`, `someday`. ### Complete a task ``` request(ops="gtd.transition(id=\"\", status=\"done\")") ``` ### List tasks by status ``` request(ops="gtd.tasks(status=\"active\", limit=10)") ``` --- ## Knowledge corpus ### Learn a concept ``` request(ops="knowledge.learn(name=\"Speculative Decoding\", description=\"Draft-then-verify inference acceleration\", domain=\"inference\", tags=[\"decoding\", \"acceleration\"])") ``` Creates a concept entity in the knowledge corpus. ### Cite a source ``` request(ops="knowledge.cite(concept_id=\"\", source_id=\"\")") ``` Both must be full UUIDs. Source must be a document, person, or org entity. ### Import markdown as atoms ``` request(ops="knowledge.import(path=\"/path/to/notes.md\", chunk_strategy=\"heading\")") ``` ### Search the corpus ``` request(ops="knowledge.search(query=\"transformer inference optimization\", rerank=true, decompose=true)") ``` --- ## Brain (Bayesian profiles) ### Create a profile ``` request(ops="brain.create_profile(name=\"research-recall\")") ``` ### Activate a profile ``` request(ops="brain.activate(profile_id=\"\")") ``` ### Give feedback on recall quality ``` request(ops="brain.feedback(target_id=\"\", signal=\"useful\")") ``` `target_id` must be a full UUID. Signals: `useful`, `not_useful`, `wrong`, `explicit_positive`, `explicit_negative`, `correction`. ### Auto-feedback after recall ``` request(ops="brain.auto_feedback(results=[{\"id\": \"\", \"used\": true}])") ``` Convenience verb: call after `memory.recall` to automatically feed back which results you actually used. ### Check which profile serves you ``` request(ops="brain.resolve(consumer_kind=\"recall\")") ``` --- ## Communication ### Send a message ``` request(ops="comm.send(to=\"local\", content=\"Task completed: attention benchmarks ready\")") ``` ### Check inbox ``` request(ops="comm.inbox(limit=5)") ``` ### Reply in a thread ``` request(ops="comm.reply(id=\"\", content=\"Acknowledged, will review\")") ``` ### Read a full thread ``` request(ops="comm.thread(id=\"\")") ``` --- ## Schedule ### Set a reminder ``` request(ops="schedule.remind(content=\"Check benchmark results\", at=\"2026-06-01T09:00:00\")") ``` ### Schedule a future verb dispatch ``` request(ops="schedule.schedule(action=\"memory.recall(query='weekly review')\", at=\"2026-06-02T10:00:00\", repeat=\"weekly\")") ``` The `action` parameter is a DSL verb string, not plain text. ### Check agenda ``` request(ops="schedule.agenda()") ``` ### Cancel a scheduled event ``` request(ops="schedule.cancel(id=\"\")") ``` --- ## Curation ### Update an entity ``` request(ops="update(id=\"\", description=\"Updated description\", tags=[\"attention\", \"inference\"])") ``` ### Merge duplicate entities ``` request(ops="merge(into_id=\"\", from_id=\"\", strategy=\"prefer_into\")") ``` Strategies: `prefer_into` (default), `prefer_from`, `union`. ### Delete a record ``` request(ops="delete(id=\"\")") ``` Soft-delete by default. Pass `hard=true` for permanent deletion (cascades edges for entities). ### Check graph health ``` request(ops="stats()") ``` Returns entity, edge, note, and event counts. Check `total_edges / total_entities`; below 4 means the graph needs more linking. --- ## Batch and chain patterns ### Parallel batch Multiple independent operations in one call: ``` request(ops="[search(kind=\"entity\", query=\"LoRA\"), search(kind=\"note\", query=\"LoRA\"), stats()]") ``` Each op runs independently. A failed op does not abort the batch; each entry has its own `ok`/`error` field. ### Two-step create-then-link When op B depends on op A's output, use two calls: ``` request(ops="create(kind=\"entity\", entity_kind=\"concept\", name=\"NewConcept\")") # Read the id from the response, then: request(ops="link(source_id=\"\", target_id=\"\", relation=\"extends\")") ``` ### Dedup-before-create pattern Always search before creating to avoid duplicates: ``` request(ops="search(kind=\"entity\", query=\"FlashAttention\")") # If found: link to existing. If not found: create. ``` --- ## See also - [Getting Started](getting-started.md): installation and first session - [Knowledge Graph Modeling](knowledge-graph.md): when to use each entity kind and relation - [Search and Retrieval](search.md): how scoring, reranking, and decompose work - [Memory and Recall](memory.md): memory-specific patterns - [GTD Task Management](tasks.md): task lifecycle details --- ## Tips and Tricks Source: https://ohdearquant.github.io/khive/tips-and-tricks.html # Tips and Tricks Practical usage knowledge for agents and operators working with the `request` MCP tool. This page collects gotchas that are easy to hit once but hard to find documented in one place. ## Query craft ### Long, keyword-rich queries beat short ones `search` and `knowledge.search` fuse full-text (FTS5 trigram) and vector similarity results with RRF. A short query (one or two words) gives both signals little to work with: FTS5 trigram matching has few n-grams to match against, and the embedding has little context to place in vector space. A longer query that names the language, framework, domain terms, and the shape of the question gives both retrieval legs more to latch onto and produces tighter fusion rankings. Prefer a query built from the same nouns you would use in the document itself over a terse keyword fragment. See [Search and Retrieval](search.md#score-interpretation) for how the fused scores are computed. ### `neighbors` and `traverse` default to both directions As of the fix for issues #445/#480 (`crates/khive-pack-kg/src/handlers/common.rs`, `parse_direction`), omitting `direction` on `neighbors` or `traverse` resolves to `Direction::Both`, not outgoing-only. An unrecognized direction string (a typo like `"inbound"`) is now a rejected `InvalidInput`, not silently coerced to any particular direction. This is covered by regression tests in `crates/khive-pack-kg/tests/integration.rs` asserting that a node reachable only via an incoming edge is still surfaced when `direction` is omitted. Some older documentation and ADR prose (predating this fix) describes an `out`-only default as a live footgun. That description is stale as of this writing. Pass `direction="both"` explicitly anyway when you want to be unambiguous about intent, or when you specifically want only one direction, pass `direction="out"` / `direction="in"`: ``` request(ops="neighbors(node_id=\"\", direction=\"in\", relations=[\"extends\"])") ``` ### `traverse` vs `context` Both walk the graph, but they answer different question shapes: - `traverse(roots=[...], max_depth=N)` takes explicit root UUIDs and returns paths reachable via BFS, filtered by relation. Use it when you already have anchor entities and want lineage, dependency chains, or reachability. - `context(query=..., entity_ids=...)` resolves anchors from a natural language query (via `hybrid_search`), expands 1-2 hops, and assembles the result under a character budget in a single call. Use it when you do not already have anchor UUIDs and want a budgeted neighborhood summary, for example injecting graph context into a model turn without a `search`-then-`neighbors` round trip. `context` composes the same runtime ops as `search` and `neighbors` (it adds no new storage or index); see [ADR-089](../adr/ADR-089-context-verb.md) for the full parameter and ordering contract. `context`'s `direction` also defaults to `both`, matching `neighbors`' current default (see the direction note above). ## DSL round-trip tricks ### `$prev` chains only the immediately preceding op In a chain (`v1(...) | v2(...)`), `$prev` resolves to the result of the op directly before it, not to any earlier op in the chain. Each step in a chain overwrites the value `$prev` will resolve to on the next step. If op C needs a value produced by op A but not passed through by op B, that value is gone by the time C runs. Restructure the chain, or split the calls and pass the value explicitly. ``` request(ops="create(kind=\"entity\", entity_kind=\"concept\", name=\"NewConcept\") | link(source_id=$prev.id, target_id=\"\", relation=\"extends\")") ``` Path extraction supports `$prev` (the whole result), `$prev.field` (nested object field), `$prev.items[0].id` (array index into a field), and `$prev[2]` (top-level array index). ### Parallel batches, max 100 ops ``` request(ops="[search(kind=\"entity\", query=\"LoRA\"), search(kind=\"note\", query=\"LoRA\"), stats()]") ``` Batches run with no ordering guarantee between ops, and a failed op does not abort the others: each result carries its own `ok`/`error`. The batch size ceiling is 100 ops; a larger array is rejected rather than silently truncated. ### Create and link in one round trip Chaining a `create` into a `link` avoids the two-call round trip shown in the [Prompt Cookbook](prompt-cookbook.md#two-step-create-then-link): ``` request(ops="create(kind=\"entity\", entity_kind=\"concept\", name=\"GQA\", description=\"Grouped-query attention\") | link(source_id=$prev.id, target_id=\"\", relation=\"introduced_by\")") ``` This only works when the value you need is produced by the op immediately before the one that consumes it. See the `$prev` scoping note above. ## Param gotchas A handful of verbs use a parameter name that is easy to guess wrong, and the failure mode differs by pack. The KG pack's `search` and `comm.thread` param structs carry `#[serde(deny_unknown_fields)]` (`crates/khive-pack-kg/src/handlers/params.rs`, `crates/khive-pack-comm/src/params.rs`), so passing a wrong name fails the call outright with an unknown-field error. The knowledge pack's `search`, `suggest`, and `delete_atoms` param structs (`crates/khive-pack-knowledge/src/knowledge/schema.rs`) deserialize with plain `serde_json::from_value` (no `deny_unknown_fields`), so an extra unrecognized field is silently ignored; what actually fails the call is the _required_ field (`query`, `ids`) being missing because you sent the wrong name for it instead: | Verb | Correct param | Common wrong guess | Failure mode | | --------------------------------------- | ------------- | ------------------ | ------------------------------------ | | `search` (KG) | `query` | `q` | rejected: unknown field | | `comm.thread` | `id` | `thread_id` | rejected: unknown field | | `knowledge.search`, `knowledge.suggest` | `query` | `q` | rejected: `query` missing (required) | | `knowledge.delete_atoms` | `ids` | `slugs` | rejected: `ids` missing (required) | String values in the function-call DSL must be double-quoted JSON string literals. The parser reads the raw value slice and feeds it to `serde_json::from_str`, so a bareword value (an unquoted identifier) fails JSON parsing and the op errors out, even as a standalone argument: ``` # Wrong: bareword value, fails to parse request(ops="get(id=abc123)") # Right: double-quoted string literal request(ops="get(id=\"abc123\")") ``` ## Indexing latency Writes to the knowledge corpus (`knowledge.upsert_atoms`) land in SQLite and are visible to `knowledge.search`'s FTS leg on the next call. The Vamana ANN vector index that provides the semantic leg is different: it is an in-memory, per-namespace structure that is loaded once and cached, and is only invalidated by an explicit `knowledge.index()` call (or a daemon restart), not automatically after every `upsert_atoms`. A newly written atom will not surface via the ANN path of `knowledge.search` until a reindex runs. Treat writes as helping the _next_ indexing pass, not as immediately recallable through the vector leg: batch writes, then call `knowledge.index()` (or run `kkernel reindex`) before relying on semantic recall over what you just wrote. ## Troubleshooting - `request(ops="verbs()")` and `help=true` on any verb are the live ground truth for what is actually registered on the server you are talking to. Prefer them over any cached doc (including this one) when behavior looks off. `help=true` short-circuits before the pack's handler runs, so it never has side effects. - If an MCP client fails to connect to `kkernel mcp` with an opaque error, see [Troubleshooting a connect failure](../configuration.md#troubleshooting-a-connect-failure) in the configuration guide for the stderr probe that surfaces the real startup error. ## See also - [Prompt Cookbook](prompt-cookbook.md): full verb syntax reference - [Search and Retrieval](search.md): scoring, reranking, and decompose - [Configuration](../configuration.md): config resolution and connect-failure troubleshooting - [ADR-089](../adr/ADR-089-context-verb.md): `context` verb design - [Proof-Graph Case Study](proof-graph-case-study.md): khive at scale --- ## Proof Graph Case Study Source: https://ohdearquant.github.io/khive/proof-graph-case-study.html # Case Study: The Mathlib Proof Graph This page describes a khive-at-scale deployment: a knowledge graph of the [mathlib](https://leanprover-community.github.io/mathlib4_docs/) formal-math library, built to support automated redundancy detection across proofs. It focuses on the khive mechanics (ingestion, typed edges, traversal at scale), not the mathematical adjudication built on top. ## Scale Mathlib v4.30.0 was ingested as a khive-native graph: | Metric | Value | | -------------------- | --------- | | Entities | 320,810 | | `depends_on` edges | 4,387,592 | | Avg edges per entity | ~13.7 | Every mathlib declaration (theorem, definition, structure, instance, axiom) became an entity; every dependency between declarations became a typed `depends_on` edge. This is the same edge relation khive uses elsewhere for build/runtime/data/artifact/tooling dependency modeling. Proof dependency is one more instance of the same closed relation, not a schema extension. ## Ingestion path At this scale, going through the MCP `request` surface one `create`/`link` call at a time is not the right tool: 4.7 million individual round trips would dominate wall time and add no value over a direct write. The ingestion instead writes straight to a khive-native SQLite database: 1. **Bootstrap the canonical schema.** The same DDL khive's migrations apply (`crates/khive-db/sql/`) is applied directly to a fresh database file, so the resulting graph is a valid khive database: every verb (`get`, `neighbors`, `traverse`, `query`) works against it exactly as it would against a database built through the MCP surface. 2. **Deterministic IDs.** Each declaration gets a UUIDv5 id derived from its fully-qualified mathlib name, and every insert is `INSERT OR IGNORE`. This makes the ingestion idempotent: re-running the ingestion script against an updated mathlib snapshot (or after a crash) does not create duplicate entities or duplicate edges, since re-deriving the same name always produces the same id. 3. **Skip vector tables for a graph-only workload.** This corpus is used for structural traversal, not semantic search over declaration text, so the ingestion does not populate `sqlite-vec` `vec0` virtual tables. This keeps the ingestion path simpler and the resulting database smaller, at the cost of not supporting `search`'s vector-similarity leg over this data. Pure graph reads (`get`, `neighbors`, `traverse`, `query`) do not depend on the vector store at all. This direct-write path trades the MCP round-trip and its request-level validation for raw insert throughput, and only makes sense at this scale. For anything an agent produces interactively (reading a paper, forming a concept, linking two ideas), the normal `create`/`link` verbs over `request` are the right tool; see [Prompt Cookbook](prompt-cookbook.md). ## Traversal at scale The resulting database is queried with the same verbs used against any khive graph. `neighbors` answers "what does this theorem depend on, or what depends on it" one hop at a time; `traverse` walks multi-hop dependency chains and lineage (see [Tips and Tricks](tips-and-tricks.md#traverse-vs-context) for when to reach for `traverse` versus a query-anchored `context` call). Multi-hop BFS over a graph this size behaves the same way it does over a small research graph: bounded by `max_depth` and `relations` filters, not by a separate large-graph code path. Nothing about the verb surface changes at 4.4 million edges versus 4,000. ## Structural signals built on the graph Two auditable signals were built on top of the ingested graph, both derived from graph structure rather than from a language model's judgment of a proof's content: - **Statement-template isomorphism.** Two theorem statements that are structurally isomorphic up to variable renaming and metavariable substitution are flagged as candidate restatements of the same result, independent of naming or literal source-text similarity. - **Specialized-machinery scoring.** A proof's dependency footprint (traced through `depends_on`) is scored for how much specialized machinery it pulls in versus how directly it follows from more general, widely-depended-on results, surfacing proofs that lean on narrow lemmas built for that one result alone. Both signals are auditable: a mathematician can walk the same `depends_on` edges the signal used and check the claim directly, rather than trusting an opaque similarity score. That auditability, not the underlying math, is the khive-relevant point: it is a direct consequence of the graph being typed and the edges being traversable, not a property of any embedding. ## Evidence - Live visualization: positions 4,770 credited proofs by mathlib territory and redundancy classification, browsable interactively. - Public discussion of the redundancy-detection results: ## See also - [Knowledge Graph Modeling](knowledge-graph.md): entity kinds and edge relations, including `depends_on` - [Search and Retrieval](search.md): `neighbors`, `traverse`, and `query` - [Tips and Tricks](tips-and-tricks.md): practical verb usage patterns --- ## API Reference Source: https://ohdearquant.github.io/khive/api-reference.html # API Reference khive exposes exactly one MCP tool, `request`. Everything else — 75 verbs across 9 production packs — is dispatched through that single tool via a small request DSL. This page documents the DSL grammar, the response envelope, and every verb's full parameter contract, so an agent can call khive correctly without reading Rust source. This page is verified against the live registry (`request(ops="verbs()")`, run 2026-07-07) and the pack source (`crates/khive-pack-*/src/*.rs` `HandlerDef`/`ParamDef` struct literals). Verb count: **75**, matching both the live registry `total` field and the sum of the 9 pack counts below. If your server reports a different total, your `KHIVE_PACKS` configuration loads a different pack set than the default — run `request(ops="verbs()")` against your own server to get the authoritative list. An always-machine-readable copy of this page is at [`/md/api-reference.md`](md/api-reference.md). The site also publishes [`/llms.txt`](llms.txt) (a short index) and [`/llms-full.txt`](llms-full.txt) (every guide page concatenated) for agents that prefer one fetch over several. ## Packs at a glance | Pack | Verbs | Load with | Optional? | | ----------- | ----- | -------------------------- | ------------------- | | `kg` | 17 | `KHIVE_PACKS=kg` (default) | No — base substrate | | `gtd` | 5 | `KHIVE_PACKS=kg,gtd` | Yes | | `memory` | 5 | `KHIVE_PACKS=kg,memory` | Yes | | `brain` | 14 | `KHIVE_PACKS=kg,brain` | Yes | | `comm` | 7 | `KHIVE_PACKS=kg,comm` | Yes | | `schedule` | 4 | `KHIVE_PACKS=kg,schedule` | Yes | | `knowledge` | 19 | `KHIVE_PACKS=kg,knowledge` | Yes | | `session` | 4 | `KHIVE_PACKS=kg,session` | Yes | | `git` | 0 | `KHIVE_PACKS=kg,git` | Yes | `git` contributes zero verbs — it registers the `commit` / `issue` / `pull_request` note kinds and a batch ingester (`crates/khive-pack-git/src/ingest.rs`), consumed outside the `request` DSL, not new MCP-callable verbs. The default binary (no `KHIVE_PACKS`/`--pack` override) loads all 9 packs: 17 + 5 + 5 + 14 + 7 + 4 + 19 + 4 + 0 = **75 verbs**. Verb names in the `kg` pack are bare (`create`, `search`, `link`, …). Every other pack namespaces its verbs with a `pack.` prefix (`gtd.assign`, `memory.recall`, `brain.feedback`, `comm.send`, `schedule.remind`, `knowledge.search`, `session.store`). --- ## DSL syntax The `request` tool takes one string argument, `ops`, in one of four forms. ### Single op ``` request(ops="search(kind=\"entity\", query=\"LoRA\")") ``` ### Parallel batch Up to 100 ops, run with no ordering guarantee between them: ``` request(ops="[memory.recall(query=\"x\"), memory.remember(content=\"y\")]") ``` ### Chain Ops separated by `|` run sequentially; `$prev` resolves against the immediately preceding op's result (not any earlier op — non-adjacent dependencies require splitting into separate `request` calls): ``` request(ops="create(kind=\"concept\", name=\"X\") | link(source_id=$prev.id, target_id=\"\", relation=\"extends\")") ``` `$prev` path extraction: | Form | Meaning | | ------------------- | ---------------------- | | `$prev` | the full prior result | | `$prev.field` | a nested object field | | `$prev.items[0].id` | array index then field | | `$prev[2]` | top-level array index | A quoted string containing `$prev` is promoted to a substitution automatically (`id="$prev.id"` behaves the same as `id=$prev.id`). To pass the literal four characters `$prev`, escape it: `"\\$prev"`. ### JSON form Equivalent to parallel batch, for callers that prefer to build JSON directly: ``` request(ops="[{\"tool\":\"search\",\"args\":{\"kind\":\"entity\",\"query\":\"LoRA\"}}]") ``` JSON form only supports independent ops — a literal `$prev` anywhere in JSON form is a parse error (`DslError::PrevRefInJsonForm`), since JSON form has no chain syntax. ### Parser constraints (source: `khive-request`, ADR-016) - **`MAX_OPS` = 100** per request; exceeding it is `DslError::TooManyOps`. - **`$prev` is chain-only.** Using it outside a `|` chain, or anywhere in JSON form, is rejected at parse time. - **Write-key conflict detection**: a parallel batch where two ops target the same UUID via `update`/`delete` (`id`), `merge` (`into_id`/`from_id`), or `link` (`source_id`/`target_id`) is rejected before any op dispatches, rather than racing. - **`RESERVED_ENVELOPE_ARGS`** (`presentation`, `presentation_per_op`) are envelope-level fields on the `request` tool call itself; passing them inside a verb's own argument list is rejected (`DslError::ReservedEnvelopeArg`). - Mixing `,` and `|` at the top level is rejected (`DslError::MixedSeparators`). - Only single-level `pack.verb` names are supported — `a.b.c` is `DslError::UnsupportedVerbNesting`. - Argument values are JSON literals. Strings must be double-quoted, including inside DSL function-call form — a bare word as a value fails at the assignment, even standalone. ## Response envelope Every op returns its own `ok`/`error` outcome; a batch's per-op failure does not abort its siblings (chain failures do abort the remainder of the chain): ```json { "results": [ { "ok": true, "tool": "search", "result": { "...": "..." } }, { "ok": false, "tool": "get", "error": "not found: ..." } ], "summary": { "total": 2, "succeeded": 1, "failed": 1, "aborted": 0 } } ``` `aborted` counts ops skipped after an earlier failure in a `|` chain; it is always 0 for parallel batches, since parallel failures do not cascade. --- ## `kg` pack — 17 verbs Base substrate verbs, bare names (no `kg.` prefix). Category is the illocutionary act (Searle 1976): Assertive = retrieves state, Commissive = commits a persistent change, Declaration = changes institutional status by fiat. ### `create` — Commissive Create an entity or note (singleton) or a batch of entities (bulk via `items`). | Param | Type | Required | Notes | | ------------- | --------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `kind` | string | conditional | Substrate (`entity`\|`note`) or granular kind (`concept`, `document`, `observation`, …). Required for the singleton path; not required when `items` is present. | | `name` | string | no | Entity name (singleton). | | `entity_kind` | string | no | concept\|document\|dataset\|project\|person\|org\|artifact\|service\|resource (when `kind="entity"`). | | `note_kind` | string | no | observation\|insight\|question\|decision\|reference (when `kind="note"`). | | `content` | string | no | Note body text (singleton notes). | | `description` | string | no | Entity free-text description. | | `tags` | array\ | no | Tag list. | | `entity_type` | string | no | First-class type tag, e.g. `"paper"`, `"algorithm"`, `"tool"`. | | `properties` | object | no | Arbitrary JSON properties. | | `items` | array\ | no | Bulk entity creation, each `{kind, name, entity_kind?, entity_type?, description?, properties?, tags?}`. Capped at 1000/request. Bulk-created entities skip embedding until a later `reindex`. | | `atomic` | bool | no | Bulk path. Default true = all-or-nothing; false = per-item errors collected. | | `verbose` | bool | no | Bulk path. When true, response includes full entity objects. | ``` request(ops="create(kind=\"concept\", name=\"RoPE\", description=\"Rotary position embedding\")") ``` ### `get` — Assertive Fetch any record by UUID (auto-detects entity/note/edge/event/proposal). | Param | Type | Required | Notes | | ----------------- | ---- | -------- | ---------------------------------------------------------------------- | | `id` | uuid | yes | Full UUID or short hex prefix (min 8 chars). | | `include_deleted` | bool | no | Return soft-deleted records too (default false); requires a full UUID. | ``` request(ops="get(id=\"3f2a9c1e\")") ``` ### `list` — Assertive List records with optional filtering. | Param | Type | Required | Notes | | ---------------------------- | ------------------------ | -------- | ----------------------------------------------------------------------------- | | `kind` | string | yes | `entity`\|`note`\|`edge`\|`event`\|`proposal`\|`message`, or a granular kind. | | `limit` | integer | no | Default 20. | | `offset` | integer | no | Default 0. | | `entity_kind` | string | no | Filter when `kind="entity"`. | | `entity_type` | string | no | Filter by type field when `kind="entity"`. | | `note_kind` | string | no | Filter when `kind="note"`. | | `tags` | array\ | no | OR-match, `kind="entity"` only. | | `source_id` / `target_id` | uuid | no | Edge endpoint filters, `kind="edge"` only. | | `relations` | array\ | no | Edge relation filter, `kind="edge"` only. | | `min_weight` / `max_weight` | number | no | Edge weight bounds, `kind="edge"` only. | | `event_kind` / `event_kinds` | string / array\ | no | `kind="event"` only; additive. | | `thread_id` | string | no | `kind="message"` only; full UUID or 8-char prefix. | | `direction` | string | no | `kind="message"` only: `inbound`\|`outbound`. | | `from` / `to` | string | no | `kind="message"` only, sender/recipient filter. | | `read` | bool | no | `kind="message"` only. | | `delivered` | bool | no | `kind="message"` only. | ``` request(ops="list(kind=\"entity\", entity_kind=\"concept\", limit=20)") ``` ### `stats` — Assertive Return aggregate KG substrate counts (entities, edges, notes). No params. ``` request(ops="stats()") ``` ### `update` — Declaration Patch entity, note, or edge fields. Field set depends on substrate: entities accept `name`/`description`/`properties`/`tags`; notes accept `name`/`content`/`salience`/`decay_factor`/`properties`; edges accept `relation`/`weight`/`properties`. | Param | Type | Required | Notes | | -------------- | --------------- | -------- | ------------------------------------------------------------------------- | | `id` | uuid | yes | Record to patch. | | `kind` | string | no | Substrate hint (`entity`\|`note`\|`edge`); omit to resolve from the UUID. | | `name` | string | no | Entities and notes. | | `description` | string | no | Entities only. | | `content` | string | no | Notes only (body text). | | `salience` | number | no | Notes only, 0.0–1.0. | | `decay_factor` | number | no | Notes only, >= 0. | | `relation` | string | no | Edges only, one of the 17 canonical relations. | | `weight` | number | no | Edges only, 0.0–1.0. | | `properties` | object | no | Shallow-merged in. | | `tags` | array\ | no | Replaces the tag list. | ``` request(ops="update(id=\"\", salience=0.7)") ``` ### `delete` — Declaration Soft or hard delete a record. | Param | Type | Required | Notes | | ------ | ------ | -------- | ------------------------------------------------------------------------ | | `id` | uuid | yes | Record to delete. | | `kind` | string | no | Substrate hint; omit to resolve from the UUID. | | `hard` | bool | no | Default false (soft delete). True permanently removes with edge cascade. | ``` request(ops="delete(id=\"\")") ``` ### `merge` — Declaration Deduplicate two entities. Returns `{kept_id, removed_id, edges_rewired, properties_merged, tags_unioned, content_appended, dry_run}` — chain with `$prev.kept_id`, **not** `$prev.id` (merge has no top-level `id` field). | Param | Type | Required | Notes | | --------- | ---- | -------- | ------------------------------------------- | | `into_id` | uuid | yes | Entity that survives the merge (canonical). | | `from_id` | uuid | yes | Entity merged from; soft-deleted afterward. | ``` request(ops="merge(into_id=\"\", from_id=\"\")") ``` ### `search` — Assertive Hybrid FTS + vector search with RRF fusion. | Param | Type | Required | Notes | | -------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `kind` | string | yes | Substrate or granular kind to search. | | `query` | string | yes | Free-text query. | | `limit` | integer | no | Default 10. | | `entity_kind` | string | no | `kind="entity"` only. | | `entity_type` | string | no | `kind="entity"` only. | | `note_kind` | string | no | `kind="note"` only. | | `include_superseded` | bool | no | `kind="note"` only; default false excludes notes targeted by a `supersedes` edge. | | `properties` | object | no | Match records whose properties contain all listed key=value pairs, applied before result truncation inside a bounded candidate window. | | `tags` | array | no | OR-match against tags; entity tags matched at the SQL level, note tags read from `properties.tags`. | | `min_score` | number | no | Score floor 0.0–1.0. No server default; RRF rank-1 scores on small corpora are typically 0.013–0.033. | ``` request(ops="search(kind=\"entity\", query=\"knowledge graph runtime\", limit=10)") ``` ### `link` — Commissive Create a typed directed edge. | Param | Type | Required | Notes | | ----------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `source_id` | uuid | yes | Source node. | | `target_id` | uuid | yes | Target node. | | `relation` | string | yes | One of the 17 canonical relations: `contains`\|`part_of`\|`instance_of`\|`extends`\|`variant_of`\|`introduced_by`\|`supersedes`\|`derived_from`\|`precedes`\|`depends_on`\|`enables`\|`implements`\|`competes_with`\|`composed_with`\|`annotates`\|`supports`\|`refutes`. | | `weight` | number | no | Default 1.0. 1.0=definitional, 0.7-0.9=strong, 0.4-0.6=plausible. | ``` request(ops="link(source_id=\"\", target_id=\"\", relation=\"extends\")") ``` ### `neighbors` — Assertive Immediate graph neighbors. | Param | Type | Required | Notes | | ------------ | --------------- | -------- | ------------------------------------------------ | | `node_id` | uuid | yes | Node whose neighbors to return. | | `direction` | string | no | `outgoing`\|`incoming`\|`both` (default `both`). | | `relations` | array\ | no | Restrict to these relation types. | | `min_weight` | number | no | Exclude edges below this weight. | ``` request(ops="neighbors(node_id=\"\", direction=\"both\")") ``` ### `traverse` — Assertive Multi-hop BFS traversal. | Param | Type | Required | Notes | | ----------- | --------------- | -------- | -------------------------------------- | | `roots` | array\ | yes | Starting node UUIDs. | | `max_depth` | integer | no | Default 3. | | `relations` | array\ | no | Restrict traversal to these relations. | ``` request(ops="traverse(roots=[\"\"], max_depth=2)") ``` ### `context` — Assertive Entity-anchored graph context in one call ([ADR-089](../adr/ADR-089-context-verb.md)). Resolves anchors from `query` and/or `entity_ids`, expands 1-2 hops via the same runtime op behind `neighbors`, and assembles a budgeted, deterministically-ordered response — replacing a caller-side `search | neighbors` chain with a single round-trip. `direction` defaults to `"both"`, matching `neighbors` and `traverse` (`outgoing`/`incoming` on request). At least one of `query`/`entity_ids` is required. One embedding inference when `query` is used; zero for a pure `entity_ids` call. | Param | Type | Required | Notes | | ------------ | --------------- | -------- | ------------------------------------------------------------------------------------- | | `query` | string | no\* | Semantic anchor selection via hybrid search; adds anchors after `entity_ids`. | | `entity_ids` | array\ | no\* | Explicit anchor UUIDs/prefixes/slugs. Honored in full, never clamped by `limit`. | | `hops` | integer | no | Expansion depth, clamped 0..=2 (default 1). | | `budget` | integer | no | Output budget in Unicode scalars of compact JSON, clamped 256..=65536 (default 4096). | | `relations` | array\ | no | Edge-relation filter applied during expansion. | | `direction` | string | no | `outgoing`\|`incoming`\|`both` (default `both`). | | `limit` | integer | no | Max anchors from the `query` leg, clamped 1..=20 (default 5). | | `fanout` | integer | no | Max neighbors per expanded node per hop, clamped 1..=50 (default 10). | \* at least one of `query`/`entity_ids` required. ``` request(ops="context(query=\"rotary position embedding\", hops=1, budget=4096)") ``` Response shape: ```json { "anchors": [ { "entity": { "id": "…", "name": "…", "kind": "concept", "description": "…", "properties": {} }, "neighbors": [ { "id": "…", "name": "…", "relation": "extends", "direction": "outgoing", "weight": 0.9, "hop": 1, "via": null, "description": "…" } ] } ], "truncated": false, "dropped": { "anchors": 0, "neighbors": 0 } } ``` ### `query` — Assertive GQL or SPARQL pattern matching (read-only). Write-shaped input (SPARQL INSERT/DELETE/LOAD/WITH…DELETE, GQL/Cypher CREATE/DELETE/DETACH DELETE/SET/MERGE) is rejected — use `create`/`update`/`link`/`merge`/`delete` to mutate the graph. Queries that mix fixed-length and variable-length chains are not compiled in one call; split them into separate `query()` calls. | Param | Type | Required | Notes | | ------- | ------- | -------- | ---------------------------------------- | | `query` | string | yes | GQL or SPARQL pattern string, read-only. | | `limit` | integer | no | Default 500, hard cap 10,000. | ``` request(ops="query(query=\"MATCH (c:concept)-[:extends]->(d:concept) RETURN c, d LIMIT 20\")") ``` ### `propose` — Commissive Create an event-sourced change proposal. Returns `{id, status, proposer, title}` — chain with `$prev.id`, not `$prev.proposal_id`. The `changeset` field has nested objects and cannot be expressed in function-call DSL form; use JSON form. | Param | Type | Required | Notes | | ------------- | --------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `title` | string | yes | Non-empty short title. | | `description` | string | yes | Non-empty full description. | | `changeset` | object | yes | Discriminated by `kind`: `add_entity`, `update_entity`, `add_edge`, `add_note`, `merge_entities`, `supersede_entity`, `compound` (nested `steps`). | | `reviewers` | array\ | no | Actor IDs requested as reviewers. | | `expiry` | integer | no | Expiry timestamp, microseconds since epoch. | | `parent_id` | uuid | no | Parent proposal this supersedes or extends. | ``` request(ops="[{\"tool\":\"propose\",\"args\":{\"title\":\"Add GQE\",\"description\":\"Register the GQE concept\",\"changeset\":{\"kind\":\"add_entity\",\"entity\":{\"kind\":\"concept\",\"name\":\"GQE\"}}}}]") ``` ### `review` — Declaration Approve, reject, comment, or request changes on a proposal. | Param | Type | Required | Notes | | ---------- | ------ | -------- | -------------------------------------------------- | | `id` | uuid | yes | Full UUID or 8-char short ID of the proposal. | | `decision` | string | yes | `approve`\|`reject`\|`comment`\|`request_changes`. | | `comment` | string | no | Reviewer comment. | ``` request(ops="review(id=\"\", decision=\"approve\")") ``` ### `withdraw` — Commissive Withdraw an open proposal (proposer-only). | Param | Type | Required | Notes | | ----------- | ------ | -------- | -------------------------------------------------- | | `id` | uuid | yes | Full UUID or 8-char short ID of the open proposal. | | `rationale` | string | no | Reason for withdrawing. | ``` request(ops="withdraw(id=\"\")") ``` ### `verbs` — Assertive List all MCP-callable verbs registered on this server. Internal subhandlers are excluded. | Param | Type | Required | Notes | | ---------- | ------ | -------- | ------------------------------------------------------------------------------------------------- | | `category` | string | no | Filter: `Assertive`\|`Commissive`\|`Declaration`\|`Directive`. | | `pack` | string | no | Filter by pack name (`kg`, `gtd`, `memory`, `brain`, `comm`, `schedule`, `knowledge`, `session`). | ``` request(ops="verbs()") ``` --- ## `gtd` pack — 5 verbs GTD task lifecycle over notes (`kind="task"`). Optional; load with `KHIVE_PACKS=kg,gtd`. ### `gtd.assign` — Directive Create a GTD task (note with `kind=task`). | Param | Type | Required | Notes | | ------------------- | --------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `title` | string | yes | Task title. | | `status` | string | no | `inbox`\|`next`\|`waiting`\|`someday`\|`active` (default `inbox`). Aliases: `todo`=inbox, `in_progress`=active, `blocked`=waiting, `later`=someday, `finished`=done. | | `priority` | string | no | `p0`\|`p1`\|`p2`\|`p3` (default `p2`). | | `assignee` | string | no | Assignee identifier. | | `due` | string | no | ISO-8601 due date. | | `depends_on` | array\ | no | Blocking task UUIDs. | | `context_entity_id` | uuid | no | Full UUID of a related KG entity. | | `tags` | array\ | no | Tag list. | ``` request(ops="gtd.assign(title=\"Ship API reference\", priority=\"p1\", assignee=\"agent:docs\")") ``` ### `gtd.next` — Assertive List actionable tasks (status `next` or `active`) by priority. | Param | Type | Required | Notes | | ---------- | ------- | -------- | ------------------------ | | `limit` | integer | no | Default 10. | | `assignee` | string | no | Filter to this assignee. | ``` request(ops="gtd.next(assignee=\"agent:docs\", limit=10)") ``` ### `gtd.complete` — Declaration Mark a task done (or cancelled) with an optional result note. | Param | Type | Required | Notes | | -------- | ------ | -------- | ------------------------------------------------- | | `id` | uuid | yes | Task to complete. | | `result` | string | no | Completion note. | | `status` | string | no | Terminal status: `done` (default) or `cancelled`. | ``` request(ops="gtd.complete(id=\"\", result=\"shipped in PR #600\")") ``` ### `gtd.tasks` — Assertive List tasks filtered by status, assignee, priority. | Param | Type | Required | Notes | | ---------- | ------- | -------- | -------------------------------------------------------------------------------------------------- | | `status` | string | no | `inbox`\|`next`\|`waiting`\|`someday`\|`active`\|`done`\|`cancelled` (aliases as in `gtd.assign`). | | `assignee` | string | no | Filter by assignee. | | `priority` | string | no | `p0`\|`p1`\|`p2`\|`p3`. | | `limit` | integer | no | Default 20. | | `offset` | integer | no | Default 0. | ``` request(ops="gtd.tasks(status=\"active\", assignee=\"agent:docs\")") ``` ### `gtd.transition` — Declaration Explicit GTD status transition with lifecycle validation. | Param | Type | Required | Notes | | -------- | ------ | -------- | ------------------------------------------ | | `id` | uuid | yes | Task to transition. | | `status` | string | yes | Target status (same set/aliases as above). | | `note` | string | no | Note attached to the transition. | ``` request(ops="gtd.transition(id=\"\", status=\"active\")") ``` --- ## `memory` pack — 5 verbs Salience- and decay-weighted memory notes. Optional; load with `KHIVE_PACKS=kg,memory`. ### `memory.remember` — Commissive Create a memory note with salience and decay. | Param | Type | Required | Notes | | ----------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------- | | `content` | string | yes | Memory content. | | `salience` | number | no | 0.0–1.0. Type-differentiated default: episodic=0.3, semantic=0.5. | | `decay_factor` | number | no | >= 0. Type-differentiated default: episodic=0.02 (~35d half-life), semantic=0.005 (~139d half-life). Higher = faster decay. | | `memory_type` | string | no | `episodic`\|`semantic` (default `episodic`); no other values accepted. | | `source_id` | string | no | UUID or 8-char short ID of the entity/note this memory annotates. | | `embedding_model` | string | no | Registered model name; defaults to pack config. | | `tags` | array | no | Stored in `properties.tags`. | | `namespace` | string | no | Write namespace override. Default: episodic → caller's namespace, semantic → `local`. | ``` request(ops="memory.remember(content=\"ADR-016 fixes the DSL grammar\", salience=0.7, memory_type=\"semantic\")") ``` ### `memory.recall` — Assertive Recall memory notes with decay-aware hybrid ranking. Each hit carries resolved (read-model) values — `memory_type` defaults to `episodic` when unset; `salience` and `decay_factor` reflect the effective defaults used for ranking. | Param | Type | Required | Notes | | ------------------- | ------- | -------- | ------------------------------------------------------------------------- | | `query` | string | yes | Semantic recall query. | | `limit` | integer | no | Default 10. | | `top_k` | integer | no | Overrides `limit` (max 100). | | `min_score` | number | no | Composite score floor, always in [0,1]. Typical production floor 0.3–0.7. | | `score_floor` | number | no | Alias for `min_score`. | | `min_salience` | number | no | Salience floor. | | `memory_type` | string | no | Filter to this type. | | `fusion_strategy` | string | no | `rrf`\|`weighted`\|`union`\|`vector_only`\|`keyword_only`. | | `embedding_model` | string | no | Registered model name; defaults to pack config. | | `include_breakdown` | bool | no | Include per-component score breakdown. | | `entity_names` | array | no | Names to boost; matches get a 1.3x multiplier. | | `full_content` | bool | no | Default true; false truncates content to 200 chars. | | `tags` | array | no | Filter by `properties.tags`. | | `tag_mode` | string | no | `any` (default, OR) or `all` (AND). | ``` request(ops="memory.recall(query=\"ADR-016 DSL grammar\", limit=5, min_score=0.3)") ``` ### `memory.feedback` — Commissive Emit explicit feedback on a recalled entity; updates recall-domain posteriors. | Param | Type | Required | Notes | | ----------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `target_id` | string | yes | UUID of the recalled entity or memory. | | `signal` | string | yes | `useful`\|`not_useful`\|`wrong`\|`explicit_positive`\|`explicit_negative`\|`implicit_positive`\|`implicit_negative`\|`correction`. | ``` request(ops="memory.feedback(target_id=\"\", signal=\"useful\")") ``` ### `memory.prune` — Commissive Soft-delete memories below a salience threshold and/or past `expires_at` (curation-layer, ADR-014). | Param | Type | Required | Notes | | -------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------- | | `min_salience` | number | no | Soft-delete memories strictly below this value. | | `before` | integer | no | Soft-delete memories expired at/before this Unix microsecond timestamp; defaults to now; 0 skips the expiry filter. | | `namespace` | string | no | Defaults to `local`. | | `dry_run` | bool | no | Default false; when true, counts candidates without deleting. | ``` request(ops="memory.prune(min_salience=0.2, dry_run=true)") ``` ### `memory.vacuum` — Commissive Run SQLite `VACUUM` to reclaim space freed by soft-deleted rows. No params. ``` request(ops="memory.vacuum()") ``` --- ## `brain` pack — 14 verbs Recall-tuning profiles: Beta-posterior scoring, profile lifecycle, and the actor/ namespace/consumer-kind resolution table that picks which profile serves a given caller. Optional; load with `KHIVE_PACKS=kg,brain`. ### `brain.profiles` — Assertive List profiles, optionally filtered by lifecycle. | Param | Type | Required | Notes | | ----------- | ------ | -------- | ----------------------------------------------- | | `lifecycle` | string | no | `active`\|`inactive`\|`archived`; omit for all. | ``` request(ops="brain.profiles(lifecycle=\"active\")") ``` ### `brain.profile` — Assertive Profile metadata, latest snapshot, current state summary. | Param | Type | Required | Notes | | ------------ | ------ | -------- | ------------------------------------------------------------- | | `profile_id` | string | yes | Profile ID string (e.g. `"balanced-recall-v1"`) — not a UUID. | ``` request(ops="brain.profile(profile_id=\"implementer-recall-v1\")") ``` ### `brain.resolve` — Assertive Show which profile would serve a caller context. | Param | Type | Required | Notes | | --------------- | ------ | -------- | ------------------------------------------------------------ | | `consumer_kind` | string | yes | Verb/operation type about to be performed (e.g. `"recall"`). | | `actor` | string | no | Default `*` (wildcard match). | | `namespace` | string | no | Default `*` (wildcard match). | ``` request(ops="brain.resolve(consumer_kind=\"recall\", actor=\"agent:docs\")") ``` ### `brain.activate` — Commissive Move a profile to Active (starts the live update loop). | Param | Type | Required | Notes | | ------------ | ------ | -------- | -------------------- | | `profile_id` | string | yes | Profile to activate. | ``` request(ops="brain.activate(profile_id=\"implementer-recall-v1\")") ``` ### `brain.deactivate` — Commissive Move a profile to Inactive (stop live updates, retain state). | Param | Type | Required | Notes | | ------------ | ------ | -------- | ---------------------- | | `profile_id` | string | yes | Profile to deactivate. | ``` request(ops="brain.deactivate(profile_id=\"implementer-recall-v1\")") ``` ### `brain.archive` — Declaration Move a profile to Archived (read-only, audit-retained). | Param | Type | Required | Notes | | ------------ | ------ | -------- | ------------------- | | `profile_id` | string | yes | Profile to archive. | ``` request(ops="brain.archive(profile_id=\"deprecated-recall-v0\")") ``` ### `brain.reset` — Declaration Reset posteriors to priors (preserves event history). | Param | Type | Required | Notes | | ------------ | ------ | -------- | ------------------------------------------------------------- | | `profile_id` | string | no | Must exist and be active. Defaults to `"balanced-recall-v1"`. | ``` request(ops="brain.reset(profile_id=\"implementer-recall-v1\")") ``` ### `brain.feedback` — Commissive Emit a `FeedbackExplicit` event into the shared log. | Param | Type | Required | Notes | | ---------------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------- | | `target_id` | uuid | yes | Memory note or entity the feedback applies to. | | `signal` | string | yes | Same signal set as `memory.feedback`. | | `served_by_profile_id` | string | no | Profile that served the rated result. | | `section_signals` | object | no | Per-section signals for `knowledge_compose` profiles: `{"section_name": "useful"\|"not_useful"\|"wrong"}`. | | `scorer_run_id` | string | no | ADR-081 scorer-pass id; must pair with `serve_ledger_id`. | | `serve_ledger_id` | string | no | ADR-081 `brain_serve_ledger` row id; must pair with `scorer_run_id`. | ``` request(ops="brain.feedback(target_id=\"\", signal=\"useful\")") ``` ### `brain.auto_feedback` — Commissive Emit implicit feedback for recall results supplied by an agent — the convenience verb to call right after `memory.recall` instead of hand-building `brain.feedback`. | Param | Type | Required | Notes | | ---------------------- | ------ | -------- | --------------------------------------------------------------------- | | `query` | string | yes | The recall query that produced the results. | | `results` | array | yes | Recall result objects; the first object's `id` is credited. | | `signal` | string | no | Defaults to `implicit_positive`. | | `served_by_profile_id` | string | no | Profile that served the recall. | | `scorer_run_id` | string | no | Forwarded verbatim to `brain.feedback`; pairs with `serve_ledger_id`. | | `serve_ledger_id` | string | no | Forwarded verbatim to `brain.feedback`; pairs with `scorer_run_id`. | ``` request(ops="memory.recall(query=\"x\", limit=5) | brain.auto_feedback(query=\"x\", results=[{\"id\": \"$prev.items[0].id\"}])") ``` ### `brain.bind` — Declaration Write a row in the profile resolution table. | Param | Type | Required | Notes | | --------------- | ------- | -------- | -------------------------------------------- | | `profile_id` | string | yes | Must exist. | | `actor` | string | no | Default `*` (all actors). | | `namespace` | string | no | Default `*` (all namespaces). | | `consumer_kind` | string | no | Default `*` (all kinds). | | `priority` | integer | no | Higher wins on multiple matches (default 0). | ``` request(ops="brain.bind(profile_id=\"implementer-recall-v1\", actor=\"role:implementer\")") ``` ### `brain.unbind` — Declaration Remove rows from the profile resolution table. At least one filter is required. | Param | Type | Required | Notes | | --------------- | ------ | -------- | -------------------------------- | | `profile_id` | string | no | AND-combined with other filters. | | `actor` | string | no | | | `namespace` | string | no | | | `consumer_kind` | string | no | | ``` request(ops="brain.unbind(actor=\"role:implementer\")") ``` ### `brain.bindings` — Assertive List rows in the profile resolution table, optionally filtered. | Param | Type | Required | Notes | | --------------- | ------ | -------- | ----- | | `profile_id` | string | no | | | `actor` | string | no | | | `namespace` | string | no | | | `consumer_kind` | string | no | | ``` request(ops="brain.bindings(consumer_kind=\"recall\")") ``` ### `brain.create_profile` — Declaration Create a new brain profile with a given name and optional seed priors. | Param | Type | Required | Notes | | --------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | string | yes | Profile ID (alphanumeric + hyphens), must be unique. | | `description` | string | no | Human-readable description. | | `consumer_kind` | string | no | Default `"recall"`. | | `seed_priors` | object | no | For `knowledge_compose`: `{"section_posteriors": {"overview": {"alpha": 2.0, "beta": 2.0}, ...}}`; for `recall`: `{"relevance": {"alpha": 7.0, "beta": 3.0}, ...}`. | ``` request(ops="brain.create_profile(name=\"implementer-recall-v2\", consumer_kind=\"recall\")") ``` ### `brain.register_adapter` — Declaration Register an adapter integrity record so the router only composes adapters matching the active base model revision. | Param | Type | Required | Notes | | --------------------- | ------ | -------- | ----------------------------------------------------------- | | `adapter_id` | string | yes | Stable adapter identifier (used as the entity name). | | `content_hash` | string | yes | Content hash of the adapter weights. | | `base_model_revision` | string | yes | Must match the active revision or registration is rejected. | | `metadata` | object | no | Merged into entity properties. | ``` request(ops="brain.register_adapter(adapter_id=\"lora-v3\", content_hash=\"\", base_model_revision=\"2026-07-01\")") ``` --- ## `comm` pack — 7 verbs Actor-to-actor messaging with threading. Optional; load with `KHIVE_PACKS=kg,comm`. ### `comm.send` — Commissive Send a message, optionally threaded. | Param | Type | Required | Notes | | ----------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------- | | `to` | string | yes | Actor label, e.g. `"lambda:leo"`. Both copies land in the caller's namespace; no cross-namespace write occurs. | | `content` | string | yes | Non-empty message body. | | `subject` | string | no | Optional subject line. | | `thread_id` | uuid | no | Groups the message into an existing thread. | ``` request(ops="comm.send(to=\"lambda:leo\", subject=\"PR ready\", content=\"#600 is open for review\")") ``` ### `comm.inbox` — Assertive List inbound messages for the caller. | Param | Type | Required | Notes | | -------- | ------- | -------- | ---------------------------------- | | `limit` | integer | no | Default 20, max 200. | | `status` | string | no | `unread` (default)\|`read`\|`all`. | ``` request(ops="comm.inbox(limit=10)") ``` ### `comm.read` — Declaration Mark an inbound message as read. Outbound messages cannot be marked read. | Param | Type | Required | Notes | | ----- | ------ | -------- | -------------------------------------------------- | | `id` | string | yes | 8-char prefix or full UUID of the inbound message. | ``` request(ops="comm.read(id=\"\")") ``` ### `comm.reply` — Commissive Reply to a message, threading linkage. | Param | Type | Required | Notes | | --------- | ------ | -------- | ----------------------------------------------------------- | | `id` | string | yes | 8-char prefix or full UUID of the message being replied to. | | `content` | string | yes | Non-empty reply body. | ``` request(ops="comm.reply(id=\"\", content=\"On it.\")") ``` ### `comm.thread` — Assertive Retrieve all messages in a conversation thread, ordered chronologically. | Param | Type | Required | Notes | | ------- | ------- | -------- | ------------------------------------------------------------------- | | `id` | string | yes | Thread root: 8-char prefix or full UUID of the originating message. | | `limit` | integer | no | Default 100, max 500. | ``` request(ops="comm.thread(id=\"\")") ``` ### `comm.probe` — Assertive Strictly read-only poll for new inbound message metadata and a stale-unread count. No read-flag mutation, no writes: designed for monitors polling every ~30 seconds, served by a single cheap indexed query. Returns a `cursor_us` high-water mark, a `stale_unread_count` of inbound messages unread past the staleness window, and a `new_messages` array of up to 100 inbound rows `{id, created_at_us, from_actor, subject?}` newer than `since_us`. | Param | Type | Required | Notes | | --------------- | ------- | -------- | --------------------------------------------------- | | `actor` | string | yes | Actor label whose inbound mail is probed. | | `since_us` | integer | no | Microsecond-epoch cursor; only newer rows returned. | | `stale_minutes` | integer | no | Staleness window for the unread count (default 20). | ``` request(ops="comm.probe(actor=\"lambda:leo\", since_us=1751932800000000)") ``` ### `comm.health` — Assertive Read-only per-channel health snapshot. Returns the daemon-persisted heartbeat row for every known channel: timestamps and consecutive-failure counts only, never a computed healthy bool. Health judgment belongs to the caller. Rows are read from the pinned operational namespace (`local`) unconditionally, regardless of the caller's dispatch namespace. An empty `channels` array cannot distinguish "no daemon running" from "channels configured but never polled". See the [communication guide](communication.md) for the full response contract. No parameters. ``` request(ops="comm.health()") ``` --- ## `schedule` pack — 4 verbs Time-triggered reminders and deferred verb dispatch. Optional; load with `KHIVE_PACKS=kg,schedule`. ### `schedule.remind` — Commissive Create a time-triggered reminder. | Param | Type | Required | Notes | | --------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `content` | string | yes | Non-empty reminder message. | | `at` | string | yes | RFC 3339 trigger time, e.g. `"2026-06-01T09:00:00Z"`. | | `repeat` | string | no | `daily`\|`weekly`\|`monthly`, or a limited 5-field cron form using only `*` or one in-range integer per field (steps/ranges/lists not accepted). | ``` request(ops="schedule.remind(content=\"check PR #600 CI\", at=\"2026-07-05T09:00:00Z\")") ``` ### `schedule.schedule` — Commissive Schedule a future verb dispatch. | Param | Type | Required | Notes | | -------- | ------ | -------- | ------------------------------------------------------------------- | | `action` | string | yes | Verb dispatch payload, e.g. `"schedule.remind(content=\"hello\")"`. | | `at` | string | yes | RFC 3339 trigger time. | | `repeat` | string | no | Same recurrence grammar as `schedule.remind`. | ``` request(ops="schedule.schedule(action=\"gtd.next(assignee=\\\"agent:docs\\\")\", at=\"2026-07-05T09:00:00Z\")") ``` ### `schedule.agenda` — Assertive List upcoming scheduled events. | Param | Type | Required | Notes | | ------- | ------- | -------- | --------------------------------------------------------------------- | | `from` | string | no | RFC 3339 window start; omit to start from the earliest pending event. | | `to` | string | no | RFC 3339 window end; omit for all future events. | | `limit` | integer | no | Default 20, max 200. | ``` request(ops="schedule.agenda(limit=10)") ``` ### `schedule.cancel` — Declaration Cancel a scheduled event. | Param | Type | Required | Notes | | ----- | ------ | -------- | --------------------------------- | | `id` | string | yes | Full UUID of the scheduled event. | ``` request(ops="schedule.cancel(id=\"\")") ``` --- ## `knowledge` pack — 19 verbs The knowledge-atom corpus: bulk ingest, TF-IDF + embedding search, domain composition, section-level review/dispute, and KG-sugar verbs for citing sources. Optional; load with `KHIVE_PACKS=kg,knowledge`. ### `knowledge.upsert_atoms` — Commissive Bulk insert or update knowledge atoms by slug. | Param | Type | Required | Notes | | ------------ | --------------- | -------- | ----------------------------------------------------------------- | | `atoms` | array\ | yes | `{slug, name, content, tags?, properties?, finalized?}` per atom. | | `chunk_size` | integer | no | Client-side chunking hint, max 5000. | ``` request(ops="[{\"tool\":\"knowledge.upsert_atoms\",\"args\":{\"atoms\":[{\"slug\":\"rope\",\"name\":\"RoPE\",\"content\":\"Rotary position embedding...\"}]}}]") ``` ### `knowledge.upsert_domains` — Commissive Bulk insert or update domain groupings of atoms. | Param | Type | Required | Notes | | --------- | --------------- | -------- | --------------------------------------------------------- | | `domains` | array\ | yes | `{slug, name, description?, tags?, members?}` per domain. | ``` request(ops="[{\"tool\":\"knowledge.upsert_domains\",\"args\":{\"domains\":[{\"slug\":\"attention\",\"name\":\"Attention mechanisms\"}]}}]") ``` ### `knowledge.get` — Assertive Fetch a single atom or domain by UUID or slug. | Param | Type | Required | Notes | | ------------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | yes | Atom/domain UUID or slug. | | `include_sections` | bool | no | Include the atom's sections under a `sections` key (ignored for domains). Each section: `id, atom_id, namespace, section_type, heading, content, content_hash, status, tokens, sort_order, created_at, updated_at`, ordered by `sort_order`, `created_at`, `id`. Default false. | ``` request(ops="knowledge.get(id=\"rope\", include_sections=true)") ``` ### `knowledge.list` — Assertive Paginated listing of atoms or domains. | Param | Type | Required | Notes | | -------- | ------- | -------- | ---------------------------------- | | `type` | string | no | `atom`\|`domain` (default `atom`). | | `limit` | integer | no | Default 20, max 500. | | `offset` | integer | no | Pagination offset. | ``` request(ops="knowledge.list(type=\"domain\", limit=50)") ``` ### `knowledge.delete_atoms` — Commissive Soft-delete atoms by slug or ID. | Param | Type | Required | Notes | | ----- | --------------- | -------- | -------------------- | | `ids` | array\ | yes | Atom slugs or UUIDs. | ``` request(ops="knowledge.delete_atoms(ids=[\"stale-atom-slug\"])") ``` ### `knowledge.stats` — Assertive Corpus statistics: atom count, domain count, coverage. No params. ``` request(ops="knowledge.stats()") ``` ### `knowledge.index` — Commissive Backfill embeddings + FTS for atoms/domains. | Param | Type | Required | Notes | | ------------- | --------------- | -------- | ------------------------------------------------------- | | `ids` | array\ | no | Atom slugs/IDs to index; omit to index all. | | `batch_size` | integer | no | Default 500, max 1000. | | `insert_only` | bool | no | Deprecated no-op, accepted for API compatibility only. | | `rebuild_ann` | bool | no | Rebuild the in-memory Vamana ANN index (default false). | ``` request(ops="knowledge.index(rebuild_ann=true)") ``` ### `knowledge.fold` — Assertive Budget-constrained knapsack selection of scored candidates. | Param | Type | Required | Notes | | ------------------ | --------------- | -------- | ------------------------------------------------------- | | `candidates` | array\ | yes | `{id, score, size, content?, category?}` per candidate. | | `budget` | integer | yes | Token/size budget for the selected set. | | `min_score` | number | no | Default 0.0. | | `category_weights` | object | no | Per-category score multipliers. | ``` request(ops="[{\"tool\":\"knowledge.fold\",\"args\":{\"candidates\":[{\"id\":\"a\",\"score\":0.8,\"size\":400}],\"budget\":4000}}]") ``` ### `knowledge.search` — Assertive TF-IDF ranked search over the knowledge corpus with embedding rerank (default when an embedder is configured). Draft and deprecated atoms are excluded by default. Score bands: `score>=0.46` reliably on-target, `0.42<=score<0.46` mixed quality, `score<0.42` mostly off-target. | Param | Type | Required | Notes | | --------------------- | ------- | -------- | --------------------------------------------------------------------------------------- | | `query` | string | yes | Search query text. | | `type` | string | no | `atom`\|`domain` (default both). | | `include_drafts` | bool | no | Default false; no-op when `status` is set. | | `status` | string | no | Exact status filter: `draft`\|`reviewed`\|`deprecated`; overrides `include_drafts`. | | `exclude_status` | string | no | Exclude an exact status; only used when `status` unset. | | `role` | string | no | Agent role hint, prepended to the query for scoring. | | `limit` | integer | no | Default 10, max 100. | | `min_score` | number | no | Default 0.0. | | `weights` | object | no | `{w_name, w_tags, w_content, w_exact_name, w_bigram, expand_discount, coverage_alpha}`. | | `decompose` | bool | no | Default false; enables query decomposition. | | `decompose_threshold` | integer | no | Default 4 non-stop terms to trigger decomposition. | | `intersection_bonus` | number | no | Default 0.25; score multiplier for multi-sub-query hits. | | `rerank` | bool | no | Default true; embedding rerank; no-op with no embedder configured. | | `rerank_alpha` | number | no | Default 0.7 (TF-IDF-dominant blend). | ``` request(ops="knowledge.search(query=\"FastAPI JWT middleware\", rerank=true, limit=10)") ``` ### `knowledge.suggest` — Assertive Suggest relevant knowledge domains for a query. Draft/deprecated domain atoms excluded by default. | Param | Type | Required | Notes | | ------- | ------- | -------- | ----------------------- | | `query` | string | yes | Orientation query text. | | `role` | string | no | Agent role hint. | | `limit` | integer | no | Default 8, max 100. | ``` request(ops="knowledge.suggest(query=\"async middleware retry circuit breaker patterns\", role=\"implementer\")") ``` ### `knowledge.compose` — Assertive Compose a markdown briefing from selected knowledge domains and atoms. | Param | Type | Required | Notes | | ------------ | --------------- | -------- | ------------------------------------------------- | | `domain_ids` | array\ | no | Domain UUIDs/slugs whose member atoms to include. | | `atom_ids` | array\ | no | Atom UUIDs/slugs to include directly. | | `query` | string | yes | Reranks the selected atom bodies. | ``` request(ops="knowledge.compose(query=\"FastAPI JWT middleware validation patterns\", domain_ids=[\"attention\"])") ``` ### `knowledge.edit` — Commissive Upsert sections for an atom without wiping other sections. | Param | Type | Required | Notes | | ---------- | --------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | yes | Atom UUID or slug. | | `sections` | array\ | yes | `[{section_type, content, heading?, sort_order?}]`. `section_type` is a closed enum: `overview`\|`core_model`\|`boundary_conditions`\|`formalism`\|`operational_guidance`\|`examples`\|`failure_modes`\|`expert_lens`\|`references`\|`other`. `content` must be >= 80 characters. | ``` request(ops="[{\"tool\":\"knowledge.edit\",\"args\":{\"id\":\"rope\",\"sections\":[{\"section_type\":\"overview\",\"content\":\"Rotary position embedding rotates query/key vectors by an angle proportional to position...\"}]}}]") ``` ### `knowledge.import` — Commissive Ingest atlas markdown file(s) as atoms with parsed sections. | Param | Type | Required | Notes | | ---------------- | ------ | -------- | ----------------------------------------------------------------------------- | | `path` | string | yes | Filesystem path to a markdown file or directory. | | `format` | string | no | Only `atlas_md` supported (default). | | `chunk_strategy` | string | no | `section` (default, one section per atom) or `atom` (whole file as one atom). | ``` request(ops="knowledge.import(path=\"/path/to/atlas/rope.md\")") ``` ### `knowledge.challenge` — Commissive Mark a section as disputed and increment the atom's `dispute_count`. | Param | Type | Required | Notes | | -------------- | ------ | -------- | ----------------------------------------------------------------- | | `atom_id` | string | yes | Atom UUID or slug. | | `section_type` | string | yes | Section type to challenge. | | `content_hash` | string | no | Required when more than one eligible section of that type exists. | | `reason` | string | no | Optional challenge reason. | ``` request(ops="knowledge.challenge(atom_id=\"rope\", section_type=\"formalism\", reason=\"formula sign error\")") ``` ### `knowledge.adjudicate` — Commissive Resolve a disputed section and decrement the atom's `dispute_count`. | Param | Type | Required | Notes | | -------------- | ------ | -------- | ----------------------------------------------------------------- | | `atom_id` | string | yes | Atom UUID or slug. | | `section_type` | string | yes | Section type to adjudicate. | | `content_hash` | string | no | Required when more than one disputed section of that type exists. | | `resolution` | string | yes | `accept` (marks verified) or `reject` (marks reviewed). | ``` request(ops="knowledge.adjudicate(atom_id=\"rope\", section_type=\"formalism\", resolution=\"accept\")") ``` ### `knowledge.learn` — Commissive Register a concept entity with optional domain and tags. | Param | Type | Required | Notes | | ------------- | --------------- | -------- | -------------------------------- | | `name` | string | yes | Concept name. | | `description` | string | no | Optional description. | | `domain` | string | no | Folded into `properties.domain`. | | `tags` | array\ | no | Optional tag list. | ``` request(ops="knowledge.learn(name=\"GQA\", domain=\"attention\", description=\"Grouped-query attention\")") ``` ### `knowledge.cite` — Commissive Link a concept to the paper or source that introduced it. | Param | Type | Required | Notes | | ------------ | ----- | -------- | ---------------------------------------------------------------------------------------------------- | | `concept_id` | uuid | yes | Concept entity ID. | | `source_id` | uuid | yes | Source entity ID; must be `kind=document`, `kind=person`, or `kind=org` (`introduced_by` edge rule). | | `weight` | float | no | Defaults to 1.0. | ``` request(ops="knowledge.cite(concept_id=\"\", source_id=\"\")") ``` ### `knowledge.topic` — Assertive List concepts filtered by domain or free-text query. | Param | Type | Required | Notes | | -------- | ------- | -------- | ------------------------------------------- | | `domain` | string | no | Filter to concepts tagged with this domain. | | `query` | string | no | Free-text search across name + description. | | `limit` | integer | no | Default 20, max 100. | ``` request(ops="knowledge.topic(domain=\"attention\")") ``` ### `knowledge.feedback` — Commissive Apply per-section feedback signals to update section posterior weights. | Param | Type | Required | Notes | | ----------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------- | | `section_signals` | object | yes | `{section_type: signal}`, e.g. `{"overview": "useful", "formalism": "not_useful"}`. Signals: `useful`\|`not_useful`\|`wrong`. | | `target_id` | string | no | UUID of the rated atom/entity. When paired with a configured brain profile, also forwards to `brain.feedback`. | ``` request(ops="knowledge.feedback(target_id=\"rope\", section_signals={\"overview\": \"useful\"})") ``` --- ## `session` pack — 4 verbs Cross-provider agent-session continuity records. Optional; load with `KHIVE_PACKS=kg,session`. ### `session.store` — Directive Persist an agent-session record as a session note. | Param | Type | Required | Notes | | --------------------- | --------------- | -------- | ------------------------------------------------------ | | `content` | string | yes | Verbatim transcript or summary content. | | `title` | string | no | Stored as `note.name`. | | `provider` | string | no | Provider label, e.g. `codex`, `claude_code`, `openai`. | | `provider_session_id` | string | no | Provider-native continuity anchor. | | `tags` | array\ | no | Stored in `properties.tags`. | ``` request(ops="session.store(content=\"...\", provider=\"claude_code\", title=\"pages revamp session\")") ``` ### `session.list` — Assertive List stored sessions newest first. | Param | Type | Required | Notes | | ---------- | ------- | -------- | -------------------------------------- | | `limit` | integer | no | 1–200, default 20. | | `offset` | integer | no | Default 0. | | `provider` | string | no | Exact filter on `properties.provider`. | ``` request(ops="session.list(provider=\"claude_code\", limit=10)") ``` ### `session.resume` — Assertive Fetch one session's full content by UUID or 8+ hex prefix. | Param | Type | Required | Notes | | ----- | ------ | -------- | --------------------------------- | | `id` | string | yes | Full UUID or 8+ hex short prefix. | ``` request(ops="session.resume(id=\"\")") ``` ### `session.export` — Assertive Serialize one stored session as json or markdown. | Param | Type | Required | Notes | | -------- | ------ | -------- | ----------------------------------- | | `id` | string | yes | Full UUID or 8+ hex short prefix. | | `format` | string | no | `json`\|`markdown`, default `json`. | ``` request(ops="session.export(id=\"\", format=\"markdown\")") ``` --- ## Further reading - [Getting Started](getting-started.html): install and connect an MCP client. - [Knowledge Graph Modeling](knowledge-graph.html): entity kinds, edge relations, patterns. - [Memory and Recall](memory.html): salience, decay, and recall internals. - [Search and Retrieval](search.html): FTS, vector, hybrid fusion, reranking. - [GTD Task Management](tasks.html): task lifecycle in depth. - [Prompt Cookbook](prompt-cookbook.html): ready-to-use verb patterns. - [ADR-016: request DSL](https://github.com/ohdearquant/khive/blob/main/docs/adr/ADR-016-request-dsl.md) - [ADR-002: edge ontology](https://github.com/ohdearquant/khive/blob/main/docs/adr/ADR-002-edge-ontology.md)