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 carrying at least a type. Two names are reserved: index.md is a
directory listing and log.md records change history. Ordinary markdown links
between concepts form the graph, so nothing declares it.
Context fills lazily. Only the root index goes into the system prompt, and the model pulls concepts in 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 legal per the spec, and
unknown frontmatter keys stay on concept.frontmatter.
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"])
A concept's trust tier records how it was verified, from unverified
through machine-confirmed to human-reviewed.
A bundle is named after its folder, its git subdirectory, or its repository.
Two bundles with the same name raise ValueError. 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 saying why, and a failed clone is cleaned up so the next load
can retry.
Subdirectories must resolve inside the repository. A symlink that escapes it
raises ValueError, and a missing subdirectory raises FileNotFoundError.
OKFBundle.from_git(..., timeout=30) bounds the wait 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. Paths work
with or without .md, and a directory path reads its index. Lookups use an
in-memory table, never the disk, so /../etc/passwd returns "Concept not
found" rather than a file. A truncated body ends with a marker saying how much
was cut and how to ask for more.
How search ranks
search_concepts ranks with BM25F, a keyword scoring function that weights
each field separately.
| Field | Weight |
|---|---|
| Title | 3 |
| Tags | 2.5 |
| Description | 2 |
| Body | 0.5 |
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. 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 searched. manager.search() uses one
combined index across every bundle; the tool searches one bundle, using that
bundle's own statistics. Override the synchronous KnowledgeManager.search to
plug in embeddings or hybrid retrieval. It receives the query, bundle, limit,
type, and tags, and must return 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, while 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,
the attester, and source credibility metadata. The harness retrieves the
contract; running the computation and attesting the result are the
application's job.
Reacting to knowledge access
KNOWLEDGE_ACCESSED fires on every one of the three tool calls. Every payload
carries tool, bundle and found, plus:
| Tool | Also carries |
|---|---|
search_concepts |
query, hits |
read_concept |
path, body_chars |
get_neighbors |
path, hits (outbound plus backlinks) |
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)
Run the demo
Full demo: examples/04-context/knowledge_bundles_okf.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.