The Open Knowledge Format (OKF v0.2) is a vendor-neutral way to give agents curated context: a folder of markdown files with YAML frontmatter. Every non-reserved .md file is a concept with at least a type; index.md files are directory listings for progressive disclosure, log.md files record change history, and ordinary markdown links between concepts form the graph. Files are read and indexed when the bundle loads. Model context is filled lazily: the root index goes into the system prompt, and the model pulls concepts into context with three read-only tools. Where skills carry instructions, knowledge bundles carry facts with provenance.

Authoring a concept

knowledge/metrics/active-users.md:

---
type: Metric
title: Active Users
description: Daily and monthly active users, counted from qualifying product events.
tags: [metric, engagement]
status: stable
stale_after: 2027-09-27T00:00:00Z
verified:
  - by: human:analytics-lead
    at: 2026-09-27T00:00:00Z
sources:
  - id: dau-model
    resource: /tables/events.md
---

A user is active on a day when they emit one qualifying event.[^dau-model]
Computed from the [events table](/tables/events.md).

/tables/events.md is bundle-root-relative; ./events.md is relative to the file, and a directory link such as tables/ resolves to tables/index.md. Inline and reference-style links become graph edges; links inside code examples, images, HTML comments, and external URLs do not. Broken links, unknown type values, and missing optional fields are all legal per the spec. Unknown frontmatter keys are kept on concept.frontmatter.

Loading never raises for non-conformant content. Files without frontmatter, with an empty type, with malformed YAML, or with an invalid YAML timestamp are skipped and reported in bundle.warnings. Invalid metadata on a root index produces a warning while its body stays readable. Hidden files and directories are ignored. index.md and log.md can be read with read_concept but are excluded from search.

Loading knowledge into an agent

from harnessx import Agent, AgentConfig

agent = Agent(
    config=AgentConfig(
        system_prompt="Answer from the knowledge bundle and cite concept paths and sources.",
    ),
    knowledge=[
        "./knowledge",                                                          # local folder
        "https://github.com/GoogleCloudPlatform/open-knowledge-format/tree/main/bundles/ga4",
    ],
)

Loading happens when the agent is constructed, like skill loading. The knowledge= argument also accepts a KnowledgeManager or GitSource entries:

from harnessx import GitSource, KnowledgeManager, OKFBundle

manager = KnowledgeManager.from_paths(
    ["./knowledge", GitSource("git@github.com:acme/kb.git", ref="v2", subdir="bundles/sales")],
    refresh=True,             # re-fetch cached clones
    max_body_chars=12_000,    # read_concept body budget (default 8,000)
    index_char_budget=4_000,  # root index characters per bundle in the prompt
)
agent = Agent(config=AgentConfig(system_prompt="..."), knowledge=manager)

bundle = OKFBundle.load("./knowledge")
concept = bundle.get("/metrics/active-users.md")
print(concept.type, concept.trust_tier, concept.is_stale(), bundle.warnings)
print(bundle.search("active users", limit=3, type="Metric", tags=["engagement"]))
print(bundle.neighbors("/metrics/active-users.md")["backlinks"])

Bundle names default to the folder name, the git subdirectory, or the repository name; two bundles with the same name raise ValueError, so pass OKFBundle.load(root, name=...) when they collide.

Loading from git

Git URLs are shallow-cloned into .agent_knowledge/ on first use and reused until refresh=True. GitHub and GitLab .../tree/<ref>/<subdir> links select a branch or tag and a subdirectory. The git binary must be on the path; a missing binary or a failed clone raises RuntimeError with the reason, and a failed clone is cleaned up so the next load can retry.

A clone is built in a temporary directory and published to the cache only once it is complete. One lock per repository and ref serializes threads and processes, and OKFBundle.from_git holds it through parsing, so a refresh can never change files halfway through a load. Subdirectories must resolve inside the repository, symlinks included: an escaping symlink raises ValueError, a missing subdirectory FileNotFoundError. OKFBundle.from_git(..., timeout=30) bounds both the lock wait (TimeoutError) and each git command. Archives and other remote formats are not fetched; extract them and pass the folder.

What the model sees

The system prompt gains a <knowledge> block with the workflow and each bundle's root index.md, truncated to index_char_budget characters. The tools, all ALLOW by default:

Tool Arguments Returns
search_concepts query, limit, type, tags Ranked hits: path, type, title, description, status, score
read_concept path, max_chars Metadata header (type, status, trust tier, staleness, tags, sources) and the body; also reads indexes and logs
get_neighbors path Outbound links, backlinks, and unresolved links, with titles

With several bundles loaded the tools take a bundle argument and the prompt says so. Paths work with or without .md, and a directory path reads its index. Lookups happen in an in-memory table, never on disk, so a path such as /../etc/passwd returns "Concept not found" rather than a file. A body longer than the budget ends with a marker that says how much was cut and how to ask for more.

How search ranks

search_concepts uses BM25F, BM25 with weighted fields:

Field Weight
Title 3
Tags 2.5
Description 2
Body 0.5

The score rewards rare terms, saturates repeated matches, and normalizes each field's length (k1=1.2, b=0.75). Draft concepts are multiplied by 0.8 and deprecated ones by 0.5; trust and staleness are shown when a concept is read and do not change the score. Tokens are Unicode-normalized and case-folded, and common English singular and plural forms match in both directions with a 0.95 multiplier for the expanded form. There is no general stemming, synonym expansion, or embedding. type and tags filters are case-insensitive, all requested tags must match, and limit is clamped to 1 to 50.

Scores are relative to the corpus that was searched. manager.search() across every bundle uses one combined index with shared statistics; a search scoped to one bundle, which is what the tool does, uses that bundle's own. Override the synchronous KnowledgeManager.search to plug in embeddings or hybrid retrieval: the tool calls it with the query, bundle, limit, type, and tags, and expects hits with the same path, type, title, description, status, score, and bundle keys.

Trust, freshness, and computation contracts

Trust tiers follow the spec and come from valid verified[].by actors:

Verification Trust tier
No valid actor unverified
Only process:<id> or <producer>/<version> actors machine-confirmed
Any human:<id> actor human-reviewed

An actor needs a non-empty id with no whitespace. Malformed entries such as {} or {by: "human:"} produce warnings and are left out of the normalized metadata; the raw values stay on concept.frontmatter. Trust is an author-supplied claim: the harness does not authenticate identities.

A concept is stale on or after stale_after. status defaults to stable; draft and deprecated are the other values. read_concept reports both in its header, and the prompt asks the model to prefer stable, human-reviewed, current concepts and to say when it relies on something else.

For Attested Computation concepts, read_concept also exposes the contract: runtime, parameters, computation, the executor and its receipt fields, and the attester, plus source credibility metadata. The harness retrieves the contract and its references; running the computation and attesting the result are the application's job.

Reacting to knowledge access

A KNOWLEDGE_ACCESSED hook fires on every tool call with tool, bundle, and found, plus query and hits for searches, path and body_chars for reads, and path and hits (outbound plus backlinks) for neighbors. A missing bundle or concept still fires, with found=False.

from harnessx import HookContext, HookEvent

async def on_knowledge(ctx: HookContext):
    print(ctx.data["tool"], ctx.data.get("query") or ctx.data.get("path"), ctx.data.get("hits"))

agent.hooks.on(HookEvent.KNOWLEDGE_ACCESSED, on_knowledge)

Limits in this release

Search is lexical; embeddings are a KnowledgeManager.search override away. Attested computations are surfaced, not executed. Bundles are loaded at construction; adding one to a running agent is not supported. Archives and non-git remote sources must be extracted first.

Full demo: examples/knowledge_agent.py, with a sample bundle under examples/knowledge/. Set AGENT_KNOWLEDGE to a folder or git URL to point the demo at another bundle, or launch it from harness-web. See Examples.