Embedding Guide
How to wire okf-loom into any agent / meta-harness — CLI shell-out, Python library, and long-running server shapes, with harness-specific recipes for opencode, Claude Code, Codex, LangGraph, MCP, and custom harnesses.
Embedding okf-loom in an Agent Harness
This guide shows how to wire okf-loom into any agent / meta-harness. okf-loom is harness-agnostic by design: no agent-framework code lives in this repository. The intended audience is agents in opencode / Claude Code / Codex that point at the okf CLI — so Shape 1 (CLI subprocess) is the primary path. Shape 2 (Python library import) and Shape 3 (embedded HTTP server) are advanced seams for harness authors who need them. Shape 4 covers the live studio agent loop (built on shapes 1–3).
Pick the shape that matches how your harness already works:
| Shape | When to use | Pros | Cons |
|---|---|---|---|
| 1. CLI subprocess (primary) | Your harness already shells out to CLIs (most do — including opencode, Claude Code, Codex) | Zero coupling; JSON I/O; language-agnostic | Process-spawn latency per call |
| 2. Python library (advanced) | Your harness is Python and you want in-process caching / the full API without subprocess overhead | No subprocess; in-process caching; full API | Couples to Python |
| 3. Embedded server | You want a long-running wiki the harness can point a browser at | Interactive; hot-reload; same process as the agent | One server per bundle |
| 4. Live studio & agent loop | You want the collaborative studio: user directs via comments, agent authors the bundle live | Real-time collaboration; headless scripts/okf-loom watch feed; in-process event bus | One server per bundle; agent must watch + write via mutators |
Read
SKILL.mdfirst if you are an agent — it is the loadable skill entrypoint. Focused skill resources live underresources/.
Shape 1 — CLI subprocess
The simplest integration. Your agent constructs a command, runs it, and parses JSON on stdout. Most harnesses already do this for git, rg, npm, etc.
OKF_REPO=/path/to/okf-loom
# Validate (returns 0 ok / 1 conformance fail / 2 strict fail)
"$OKF_REPO/scripts/okf-loom" validate /path/to/bundle --format json
# Inspect
"$OKF_REPO/scripts/okf-loom" info /path/to/bundle --format json
# Find gaps (writes a JSON report)
"$OKF_REPO/scripts/okf-loom" discover /path/to/bundle --out /tmp/plan.json
# Apply an update plan (idempotent)
"$OKF_REPO/scripts/okf-loom" update /path/to/bundle --plan /tmp/plan.json --format json
# Stand up the viewer (blocks; the harness can run it in a side-thread or background process)
"$OKF_REPO/scripts/okf-loom" serve /path/to/bundle --port 8787 --no-open
Report commands document their JSON mode in $OKF_REPO/scripts/okf-loom <cmd> --help; server/watch/render/build commands expose command-specific output flags instead. This shape is language-agnostic — a Node, Go, or Rust agent can use okf-loom identically.
Discovery → Update loop (the typical agent task)
# 1. Find what's missing
scripts/okf-loom discover /path/to/bundle --format json > /tmp/discovery.json
# (the agent reads discovery.json, decides which suggestions to accept,
# and writes /tmp/plan.json with the matching UpdateOp entries — see
# docs-bundle/reference/cli.md and resources/advanced-operations.md for the op-kind reference)
# 2. Preview
scripts/okf-loom update /path/to/bundle --plan /tmp/plan.json --dry-run
# 3. Apply
scripts/okf-loom update /path/to/bundle --plan /tmp/plan.json
# 4. Re-validate
scripts/okf-loom validate /path/to/bundle --format json
Shape 2 — Python library (advanced)
This is an advanced seam for harness authors building a dedicated Python integration that wants in-process caching or the full library API without subprocess overhead. The primary embedding path for agents in opencode / Claude Code / Codex is Shape 1 (CLI subprocess) above — those agents use the
okfCLI, they do notimport okf_loom.
For Python harnesses (LangGraph tools, custom agents) that need it.
from okf_loom import Bundle
from okf_loom.validate import validate_bundle
from okf_loom.discover import discover_suggestions
from okf_loom.update import load_plan, apply_plan
from okf_loom.search import search_bundle, SearchMode
bundle = Bundle.load("/path/to/bundle")
# Validate
report = validate_bundle(bundle)
if not report.ok:
for f in report.errors:
print(f.path, f.code, f.message)
# Discover gaps
discovery = discover_suggestions(bundle, rules=["unlinked_mentions", "orphan_concepts"])
for s in discovery.suggestions:
print(s.rule, s.concept_id, s.action, s.message)
# (your code converts accepted suggestions to UpdateOp entries and
# writes a plan file — see docs-bundle/reference/cli.md and resources/advanced-operations.md)
# Apply a plan
plan = load_plan("/tmp/plan.json")
summary = apply_plan(bundle, plan)
print(summary["applied"], "applied;", summary["skipped"], "skipped")
# Search
for r in search_bundle(bundle, "customer order", mode=SearchMode.LEXICAL, limit=5):
print(r.score, r.concept_id, r.title)
Cache invalidation. Bundle.graph() and Bundle.content_index() memoize on the instance. After mutating the bundle via apply_plan, reload: bundle = Bundle.load(bundle.root). The lexical search backend caches on the Bundle instance itself; long-running servers that reload bundles evict it by reloading the bundle or calling bundle.invalidate(). okf_loom.search.clear_search_cache() is a deprecated no-op kept for import compatibility.
Shape 3 — Embedded HTTP server
Run the live viewer inside the agent process so a browser (the user, a scraper, an MCP browser tool) can navigate the bundle while the agent works on it.
import threading
from http.server import ThreadingHTTPServer
from okf_loom import Bundle
from okf_loom.server import OKFWikiHandler
bundle = Bundle.load("/path/to/bundle")
httpd = ThreadingHTTPServer(("127.0.0.1", 8787), OKFWikiHandler)
httpd.bundle = bundle # the handler reads this attribute
threading.Thread(target=httpd.serve_forever, daemon=True).start()
# Later, after the agent has mutated the bundle on disk:
httpd.bundle = Bundle.load("/path/to/bundle") # hot-swap; next request sees it
Routes are documented in HTTP routes and advanced operations. The most useful for agents:
/<concept_id>— rendered concept page./__raw/<concept_id>— raw markdown body (for the agent to read into its own context)./__data/graph.json— the link graph as JSON (for the agent to reason about structure)./__data/content.json— the full content index as JSON./__search?q=…&format=json— search-as-you-type results.
Shape 4 — Live studio & the agent loop
scripts/okf-loom serve runs a live collaborative studio: the user directs (reads + comments) and an external agent authors the OKF files in real time. The server stays LLM-free — it emits events and runs mechanical ops; the agent (in opencode / Claude Code / Codex / any harness) does the thinking and writes back through the existing mutators. This shape is how an agent harness wires into that loop. See /reference/spec.md §10 for the posture and the bring-up "watch" question.
Bring-up (§3). When you open the studio for the user, ask one courtesy question: "I've opened the live studio for
<bundle>. Want me to watch it and keep it enriched — proactively adding links, entities, relations, indexes, and descriptions as it changes? Either way I'll do whatever you ask via comments." This is a collaboration choice, not a permission gate — the agent is free to edit the OKF files regardless (current spec §10).
4a — Headless feed with scripts/okf-loom watch (continuous tail)
scripts/okf-loom watch reuses the bundle watcher and prints one event per line (the §7.2 schema) for a long-running consumer to pipe to the agent. No HTTP server needed.
# Tail every change to the bundle as JSON Lines
scripts/okf-loom watch /path/to/bundle --emit jsonl
# Reconnect-safe: replay missed events from an id, then tail live
scripts/okf-loom watch /path/to/bundle --emit jsonl --since 01JABED5K7
# Let okf-loom run mechanical repairs on change (debounced), debounced 600ms
scripts/okf-loom watch /path/to/bundle --auto-repair --debounce-ms 600
serve and watch both append the durable, replayable <bundle>/.okf-loom/session/events.jsonl — the audit trail and how a headless agent "sees" the user's saves.
4a′ — Foreground block-once with scripts/okf-loom wait (the primary agent loop)
When the agent itself is the consumer (the common case in opencode / Claude Code / Codex), prefer scripts/okf-loom wait over scripts/okf-loom watch. It blocks until there is work (a new open comment and/or a change event), prints that one item as JSON, and exits — so the agent wakes, acts, and re-waits in a clean foreground loop:
# Blocks until a new OPEN user comment, prints it, exits 0. AWAIT this —
# do NOT background it (you are the processor; a backgrounded wait has no
# consumer, so comments would sit open forever).
scripts/okf-loom wait /path/to/bundle --for comment
# Wait for comments OR proactive-enrichment changes (whichever first):
scripts/okf-loom wait /path/to/bundle --for comment change --timeout 600
Library equivalent: okf_loom.studio.wait_for_work(bundle_root, kinds=..., since=..., timeout=..., interval=...). The loop is: scripts/okf-loom wait → read JSON → reason → edit via mutators (scripts/okf-loom link-add / update / entity-add / repair) → resolve the comment → scripts/okf-loom wait again.
4b — The comment → claim → write → resolve flow
The user's only studio writes are comments / asks (POST /__comment), which land in <bundle>/.okf-loom/session/directives.jsonl. The agent's loop (run scripts/okf-loom wait in your foreground — you are the processor):
# 0. Studio must be up so the session dir + .token exist.
scripts/okf-loom serve /path/to/bundle --no-open &
# 1. Block-once for the next OPEN comment.
scripts/okf-loom wait /path/to/bundle --for comment # → {id, concept, anchor, body, …}
# 2. Claim it + announce presence.
scripts/okf-loom comment-claim /path/to/bundle 01JABED9K0
scripts/okf-loom presence /path/to/bundle --state editing --focus tables/orders
# 3. Author via the mutators. With a live session the CLI routes through
# Studio.save_concept — each op is attributed (actor=agent, origin=mutator),
# undoable, and emits 'changed' + 'graph'. --group-id groups the pass for
# one-click group undo (§12.5).
scripts/okf-loom plan /path/to/bundle --scope tables/orders --neighbors --out /tmp/p.json
scripts/okf-loom link-add --bundle /path/to/bundle --source tables/orders \
--target tables/customers --relation depends_on --group-id PASS1
scripts/okf-loom entity-add --bundle /path/to/bundle --id tables/orders \
--label "Order" --kind Table --group-id PASS1
scripts/okf-loom update /path/to/bundle --plan /tmp/p.json --group-id PASS1
# 4. Resolve the comment, linking the activity ids the writes produced.
scripts/okf-loom comment-resolve /path/to/bundle 01JABED9K0 \
--activity 01JABED5K7,01JABED5K8 --reply "Done - added depends_on + Order entity"
# 5. Loop: scripts/okf-loom wait again.
Each write flows file → Studio.save_concept → watcher → live update (SSE) + a change-list entry + an undo snapshot — the user sees it appear in place, with a highlight, no full refresh.
The per-session CSRF token (X-OKF-Token) and Origin/Host allow-list guard every mutating endpoint (current spec §14). The CLI mutators and the comment-* / presence / token commands attach the token for you. If you must POST to /__apply / /__undo / /__presence / /__comment over HTTP directly, read the token via scripts/okf-loom token /path/to/bundle (it reads <bundle>/.okf-loom/session/.token, mode 0600, set on scripts/okf-loom serve boot) — never hand-copy it:
TOKEN=$(scripts/okf-loom token /path/to/bundle)
curl -s -X POST http://127.0.0.1:8787/__presence \
-H 'Content-Type: application/json' \
-H "X-OKF-Token: $TOKEN" \
-d '{"actor":"agent","state":"editing","focus":"tables/orders"}'
For a fully headless integration (no scripts/okf-loom serve running), the CLI mutators fall back to the non-studio atomic write path: same data-safety, but no attribution / undo / live push. To get the live collaboration surface, run scripts/okf-loom serve (or any process that creates .okf-loom/session/) before invoking the mutators.
4c — Embedding the studio in-process (Python)
For a Python harness that wants the studio, the event bus, and the watcher in the same process (no subprocess, no HTTP round-trip):
import threading, queue
from okf_loom.studio import Studio
from okf_loom.watch import run_watch
# 1. Construct (or reuse) the studio handle for a bundle.
studio = Studio.for_bundle(
"/path/to/bundle",
session_rel=".okf-loom/session", # default
log_edits=True, # append a SPEC §7 log.md entry per write
bundle_name="My Bundle",
max_queue=64,
)
studio.ensure_session()
# 2. Subscribe to in-process events (returns a bounded queue.Queue you poll).
bus_q: queue.Queue = studio.bus.subscribe()
def _drain():
while True:
event = bus_q.get() # blocks until an event arrives
print("studio event:", event) # {type, rev, ids, actor, ...} (§7.2)
threading.Thread(target=_drain, daemon=True).start()
# 3. Publish from your own code if you drive the bundle programmatically:
studio.bus.publish({"type": "presence", "actor": "agent", "state": "watching"})
# 4. Headless feed as a library call (same loop `scripts/okf-loom watch` runs):
# run_watch("/path/to/bundle", emit="jsonl", since=None,
# auto_repair=False, debounce_ms=600) # -> exit code (blocks)
run_server(..., studio_edit=True, studio_live=True) (see Shape 3) attaches server.studio — the Studio instance (or None if both are off) — so a daemon-thread embedder can reach httpd.studio.bus.subscribe() for events without launching scripts/okf-loom watch separately.
EventBus.subscribe(callback) — the in-process fan-out shape (§17)
subscribe() also takes an optional callback for the simplest in-process embedding — no queue to poll, no thread to spawn:
from okf_loom.studio import Studio
studio = Studio.for_bundle("/path/to/bundle")
studio.ensure_session()
# One subscription = one callback invoked per published event from any writer
# (the CLI mutators in this process, the watcher's disk-backfill, your own
# publish() calls). Returns an opaque sub handle you pass to unsubscribe().
sub = studio.bus.subscribe(callback=lambda ev: print("event:", ev))
# ... later, when you no longer care:
studio.bus.unsubscribe(sub)
The callback runs on the publisher's thread, so keep it cheap (hand off to a worker queue if you do heavy work). The bus delivers events in publish order; a slow callback does not block other subscribers (they each have their own bounded queue, and a callback subscriber that lags gets a resync event).
EventBus.subscribe(callback) trust boundary (P3-12 / SEC2-007)
subscribe(callback) runs the caller-supplied Python callable inside the server process. This is intentional — it is the §17 in-process embedding seam — but it is also a trust boundary equivalent to the bundle active-code gate (§15.4):
- Bundle active-code (the
{{ ... }}/{% ... %}template blocks a bundle can embed) is sandboxed to the bundle directory and gated by the operator-consent flag. A bundle cannot reachEventBus.subscribefrom inside the server — the bundle has no Python import surface, only rendered markdown. EventBus.subscribe(callback)is the harness-author surface. A callback you register has the full privilege of the server process: it canimport,open(),subprocess.run, read<bundle>/.okf-loom/session/.token, and write to the bundle. The CSRF token, Origin/Host allow-list, and--publicack gate that protect the HTTP surface do not constrain a callback registered through this seam — they guard the network boundary, and the callback is in-process.
Practical implications:
- A malicious harness extension can execute arbitrary Python in the server process. Treat any code that calls
studio.bus.subscribe(cb)the same way you would treat code that callseval()on the server: only register callbacks from code you trust. okf-loom does not sandbox the callback for you. - When embedding in a larger harness (opencode / Claude Code / Codex / LangGraph / a custom agent), do not forward untrusted network input directly into a callback. If you need to expose bus events to an untrusted consumer, use the queue form (
bus.subscribe()with no callback) and serialize events over a boundary you control (a pipe, a file, an HTTP endpoint with its own auth). - Sandbox if possible. If you have a reason to register an untrusted callback (e.g. a user-supplied plugin), run it in a subprocess or a
multiprocessing-isolated worker and proxy events to it. Do not rely on Python's "no real sandbox"execrestrictions. - Exception behavior. The worker thread that invokes your callback swallows exceptions by design — a buggy callback must never kill the event bus (§13.8 robustness). Swallowed exceptions are logged to stderr with the prefix
[okf-callback]so they are not silent. Redirect the server process's stderr to your log collector to capture them:
scripts/okf-loom serve /path/to/bundle 2>>/var/log/okf-callbacks.log
If you want exceptions to surface more aggressively (e.g. fail a test suite), wrap your callback:
_seen = []
def safe_cb(event):
try:
my_logic(event)
except Exception as e:
_seen.append(e) # assert on _seen in your test
sub = studio.bus.subscribe(callback=safe_cb)
# ... after the test:
assert not _seen, f"callback raised: {_seen}"
The single-source guarantee is: the bundle cannot register a callback (it has no Python surface in the server); only the harness that owns the server process can. If you trust the harness, you trust its callbacks.
The §17 library API (all names implemented)
The HTTP layer is a thin wrapper over these — one implementation, whoever the caller is. Use them when you embed the studio in-process instead of shelling out to the CLI:
| Method | HTTP equivalent | Purpose |
|---|---|---|
studio.save_concept(*, concept_id, raw, actor="agent", action="write_concept", origin="mutator", group_id=None, expected_rev=None, summary=None, detail=None, undoable=True, emit_graph=None, path=None, publish=True) -> WriteResult | POST /__apply (per-op) | The single internal atomic write path. Validates before write, snapshots for undo, atomically writes, appends a row to events.jsonl, broadcasts changed (+ graph for graph-affecting ops). Raises / returns a conflict on expected_rev mismatch (§9.3/§9.4). |
studio.post_presence(*, actor="agent", state="idle", focus=None) -> dict | POST /__presence | Set agent presence (current spec §12). State ∈ idle/watching/thinking/editing. Broadcasts a presence event. |
studio.post_comment(*, concept, body, anchor=None, actor="user", detail=None) -> dict | POST /__comment | Append a user comment / ask to directives.jsonl (§9). Returns the new directive. |
studio.resolve_comment(comment_id, *, reply=None, activity_ids=None, actor="agent") -> dict | None | (CLI: scripts/okf-loom comment-resolve) | Mark a comment resolved with optional reply + activity links (drives group undo, §12.5). |
studio.record_activity(*, actor, action, ids, summary, origin="mutator", undoable=False, group_id=None, detail=None, emit_graph=None) -> dict | (SSE activity event) | Append an activity event row to events.jsonl and broadcast it. Use for actions that don't write a concept file but should still show on the change list. |
studio.events_append(event, *, publish=True) -> dict | (writer helper) | The low-level append every other method uses. Adds the id/ts/seq, takes the advisory file lock, writes one JSON line to events.jsonl, optionally broadcasts. Use this only if you need a fully custom event. |
studio.post_suggestion(*, concept, action, args, summary, group_id=None, actor="agent") -> dict | (SSE suggestions event) | Only under a user's opt-in propose_only constraint (§12.3). Appends a PlannedAction envelope to proposals.jsonl instead of writing. Absent by default. |
Plus studio.list_comments(...), studio.get_comment(id), studio.update_comment(id, …), studio.set_presence(...), studio.get_presence(), studio.read_events(since=None, limit=None, actor=None, concept=None), studio.snapshot_for_undo(...), studio.restore_snapshot(...), studio.undo_group(group_id), studio.mark_logged(id), studio.current_rev(cid), and the module-level wait_for_work(bundle_root, *, kinds=..., since=..., timeout=..., interval=...) (the same loop scripts/okf-loom wait runs).
Client-side extension: window.okfLoomStudio.register(kind, config)
The browser studio exposes an additive, CSP-safe JavaScript extension seam (current spec §15) mirroring the server ViewerPlugin intent but running fully client-side after the page boots. panel (add a slide-over panel + studio-bar toggle; the built-in Comments / Changes / Agent activity panels use it) and viewMode (add a button to the Rendered/Source/Split switch) ship wired; toolbar, graphDecorator, and suggestionRenderer are reserved (accepted + console.warn, not wired). Load a register(...) extension from a .okf-loom/viewer/static/*.js override or eval it from your harness after the studio boots. See scripts/okf_loom/viewer/OVERRIDES.md §6 for the full API and worked panel/view-mode examples.
The bundle-on-disk is the bus
The studio is not the source of truth — the .md files are. The studio is the fan-out + attribution + undo layer over them. Every write goes:
your code ──▶ studio.save_concept ──▶ atomic write of <bundle>/<id>.md
│
▼
.okf-loom/session/events.jsonl (one row per write)
│
┌──────────┴───────────┐
▼ ▼
EventBus.publish _BundleWatcher detects
│ mtime change, diffs the
▼ ContentIndex, and (if the
SSE /__events write didn't log itself)
fan-out to every appends an origin:"disk"
open tab + every event — so the feed is
in-process complete regardless of
subscriber which path produced it.
For an embedded harness this means: writes via save_concept (or the CLI mutators) → events.jsonl → SSE + EventBus fan-out, all from the same files a scripts/okf-loom watch would tail. A second harness process embedding the same bundle sees the same events through the file bus — no IPC beyond the filesystem. This is what keeps the studio headless-friendly (§1.9 of the current spec) and lets the agent run externally while the studio runs in-process for the user's browser.
4d — What to expose for the agent loop
At minimum, expose for the studio loop:
scripts/okf-loom serve <bundle> --no-open— bring up the live studio (hand the user the URL).scripts/okf-loom watch <bundle> --emit jsonl [--auto-repair]— the headless change feed that drives proactive enrichment (§11).- The mutators (
scripts/okf-loom link-add/entity-add/update/repair/write-concept) — how the agent actually authors files in response to comments and the watch feed. POST /__presence(or a write to.okf-loom/session/presence.json) — so the user can see what their teammate is doing.
Harness-specific recipes
opencode
opencode discovers project-scoped agents and skills under .opencode/ and an AGENTS.md at the repo root. To wire okf-loom into an opencode project:
- Clone or vendor this repository into the agent's workspace. The repo skill layout is the artifact; do not depend on package installation.
- Expose the root skill under
.opencode/skill/okf-loom/(or keep the vendored checkout there). The skill directory must includeSKILL.md,resources/,scripts/, anddocs-bundle/together:
.opencode/skill/okf-loom/
├── SKILL.md
├── resources/
├── scripts/
└── docs-bundle/
opencode's skill loader will surface it; the resolver matches on the description triggers (e.g. "okf", "validate okf", "knowledge bundle").
- Copy or include
AGENTS.mdfrom this repository to your project root only if your harness starts from AGENTS files. It points back toSKILL.md. - Scope a subagent for OKF work (
.opencode/agent/okf-curator.md):
---
description: Curates, validates, and updates OKF bundles. Use for
"okf", "validate knowledge", "discover missing links", "update
okf bundle", "stand up okf viewer".
mode: subagent
tools:
bash: true
read: true
write: true
edit: true
---
You are the OKF curator. Always load `okf-loom/SKILL.md` first,
then read the focused resource for the task (`resources/format-basics.md`,
`resources/authoring.md`, `resources/advanced-operations.md`, or
`resources/studio-agent-loop.md`). Use the checked-in helper:
`/path/to/okf-loom/scripts/okf-loom`.
Run `scripts/okf-loom validate` from inside the checkout, or `okf-loom/scripts/okf-loom validate` from a parent workspace, before claiming completion; run `discover` to find
gaps; apply suggestions via `update --plan`. Never bulk-convert existing
content without user confirmation.
- Permission scoping (opencode
permission.bashpatterns):
{
"permission": {
"bash": {
"scripts/okf-loom validate *": "allow",
"scripts/okf-loom info *": "allow",
"scripts/okf-loom discover *": "allow",
"scripts/okf-loom graph *": "allow",
"scripts/okf-loom search *": "allow",
"scripts/okf-loom capabilities *": "allow",
"scripts/okf-loom render *": "allow",
"scripts/okf-loom build *": "allow",
"scripts/okf-loom index * --dry-run": "allow",
"scripts/okf-loom index *": "ask",
"scripts/okf-loom log * --dry-run": "allow",
"scripts/okf-loom log *": "ask",
"scripts/okf-loom update * --dry-run": "allow",
"scripts/okf-loom update *": "ask",
"scripts/okf-loom serve *": "ask"
}
}
}
Claude Code
Claude Code reads CLAUDE.md at the repo root and skills under .claude/skills/. The shape mirrors opencode:
- Copy or symlink root
SKILL.mdinto the harness skill directory. - Copy the
resources/,scripts/, anddocs-bundle/folders alongside it. - Claude Code's skill/resource discovery will surface the skill when the user mentions OKF / "knowledge bundle" / "validate okf" etc.
Codex (OpenAI)
Codex reads AGENTS.md at the repo root (the same file opencode uses). Skill conventions vary; the simplest path:
AGENTS.mdat repo root (already in this repository; copy verbatim).- Reference
SKILL.mdfromAGENTS.mdso Codex reads the root skill and resources when relevant. If auto-discovery is unavailable, an explicitload okf-loom/SKILL.md when the user asks about OKFline works well.
LangGraph
Wrap the CLI in tool functions:
from langchain_core.tools import tool
import subprocess, json, pathlib
def _okf(*args):
# The repo ships no installed console script; shell out to the checkout path.
r = subprocess.run(["okf-loom/scripts/okf-loom", *args], capture_output=True, text=True, check=True)
return r.stdout
@tool
def okf_validate(bundle_path: str) -> dict:
"""Validate an OKF bundle. Returns conformance report JSON."""
return json.loads(_okf("validate", bundle_path, "--format", "json"))
@tool
def okf_discover(bundle_path: str) -> dict:
"""Find missing links/indexes/descriptions/relations/orphans in an OKF bundle."""
return json.loads(_okf("discover", bundle_path, "--format", "json"))
@tool
def okf_update(bundle_path: str, plan_path: str, dry_run: bool = False) -> dict:
"""Apply an OKF update plan (add link/tag/relation/section). Idempotent."""
args = ["update", bundle_path, "--plan", plan_path, "--format", "json"]
if dry_run:
args.append("--dry-run")
return json.loads(_okf(*args))
@tool
def okf_search(bundle_path: str, query: str, mode: str = "lexical", limit: int = 10) -> list:
"""Search an OKF bundle. mode ∈ {lexical, semantic, hybrid, tag}."""
return json.loads(_okf("search", bundle_path, query, "--mode", mode,
"--limit", str(limit), "--format", "json"))
Wire these into your graph's tools list. The agent will reach for them when the user mentions knowledge / docs / catalogs.
MCP server (model context protocol)
The CLI is shaped for an MCP wrapper. A minimal MCP server exposes okf_validate, okf_discover, okf_update, okf_search, okf_render as tools. The --format json outputs are already JSON-serialisable. (There is no in-repo MCP server today; wrap the CLI as shown.)
Custom agent (any language)
For a non-Python agent, the CLI subprocess shape works from any language. Construct the argv, run, parse --format json stdout. Treat okf-loom like git or rg: a standard Unix CLI.
What to expose to your agent
At minimum, expose these five commands as agent-callable tools (the "trigger list" the agent uses to decide when OKF is relevant):
scripts/okf-loom validate— for any "validate / check / lint" intent.scripts/okf-loom discover— for any "what's missing / find gaps / improve" intent.scripts/okf-loom update— for applying the agent's proposed fixes.scripts/okf-loom search— for any "find / show me / where is" intent over the knowledge.scripts/okf-loom serve— for any "show me / browse / open the wiki" intent.
The agent note (AGENTS.md) is the routing layer — it tells the agent WHEN to reach for OKF (the trigger list) and points to the skills for HOW.
Testing your embedding
Whatever shape you pick, verify:
- Round-trip safety. Apply a no-op update plan to a bundle; the bundle's content should be unchanged (modulo YAML whitespace).
- Idempotency. Apply the same plan twice; the second run reports
applied:0, skipped:N. - Strict mode in CI.
scripts/okf-loom validate --strictshould fail the build on warnings. - Hot-swap. In Shape 3, mutate the bundle on disk, swap
httpd.bundle, hit/again; the new content should appear.
The samples/demo_bundle/ or samples/showcase/ directories are good test corpora.
Reference
- Current okf-loom spec:
spec.md - Upstream base OKF SPEC v0.1: https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md
- Agent note:
../../AGENTS.md - Skill resources:
../../resources/ - Architecture:
architecture.md - Research:
research.md - Override examples:
../../scripts/okf_loom/viewer/OVERRIDES.md
All fields
| timestamp | 2026-06-29T00:00:00Z |
|---|