
barkup 0.4.0 is out. It adds one small, heavily measured feature to the /view module: content search. Version 0.3 shipped focused views, which made editing nearly free once your application knew which node ids an edit concerned, and it shipped with an honest caveat attached: finding those ids was your problem. As of 0.4, the model can find them itself, with one tool call, and we can tell you exactly how well that works because we benchmarked it before shipping it.
The short version of the evidence: give the model a minimal view of the tree's root and a find_nodes search tool, and it grounds plain-language edit requests ("rename the image named maple-ember") at oracle-level accuracy on the frontier model we tested and at whole-tree accuracy on the cheap one, at a median of a single search call and roughly a tenth of the input tokens of pasting the tree. The same study also measured the fashionable alternative, an embedding retriever, and found it no better than keyword matching on structural references. So what shipped is the thing that measured well: deterministic, dependency-free keyword search, about thirty lines of code, benchmark numbers attached.
The API
Three new exports in @kevinpeckham/barkup/view, alongside the existing renderView and VIEW_PROMPT_RULES:
import {
findNodes,
renderSearch,
SEARCH_PROMPT_RULES,
NO_MATCHES_MESSAGE,
} from "@kevinpeckham/barkup/view";
// The scorer: distinct-keyword overlap against each node's type,
// name, and attributes. Returns the top ids (default 5), never
// zero-score filler.
const ids = findNodes(tree, "maple-ember image");
// ["n417", ...]
// The composition the benchmark actually scored: search, then render
// the matches in place with their ancestors, everything else
// collapsed. Returns null when nothing matches.
const result = renderSearch(grammar, tree, "maple-ember image");
if (result !== null && result.ok) {
console.log(result.html);
}Wiring it up as an agent tool is the whole integration. With the Vercel AI SDK it looks like this:
import { tool } from "ai";
import { z } from "zod";
const find_nodes = tool({
description:
"Search the tree by content: returns the best-matching nodes, shown in place with their ancestors.",
inputSchema: z.object({ query: z.string() }),
execute: async ({ query }) => {
const result = renderSearch(grammar, tree, query);
if (result === null) return NO_MATCHES_MESSAGE;
if (!result.ok) throw new Error("view config error");
return result.html;
},
});Append SEARCH_PROMPT_RULES to your system prompt (it is the exact pre-registered block the benchmark scored), show the model a minimal view of the root as its starting context, and let it search. The NO_MATCHES_MESSAGE export is the exact miss text from the benchmark harness; returning it teaches the model to reword rather than stall.
The recipe, now in three tiers
The /view docs graduated from a caveat to a decision table:
- Your app knows the ids. Render the view directly. Studies I
- You only have a human description. Skeleton view plus
- Your patcher is expensive and your trees are big. Ground with
and J: accuracy statistically unchanged from full-tree input, at 1 to 2% of the tokens.
find_nodes. Study N: 43 of 45 on claude-sonnet-4.5 (equal to its ids-given oracle bound), 39 of 45 on gemini-3.5-flash (equal to its whole-tree score), median one search call.
a cheap model first (full tree in, target ids out), then let the expensive model patch against the focused view. Study N's cross cell: accuracy held while the frontier model's median input dropped to about 1,500 tokens, a 97% reduction.
And the session guidance is unchanged by two further attempts to simplify it away: keep the full conversation history AND attach a fresh minimal view every turn. Stateless sessions failed the gate in Study M and failed it again in Study O with exact positions printed on every node.
What we deliberately did not ship
No embedding index, no vector store, no async retrieval pipeline. Study N measured an off-the-shelf embedding retriever against the keyword scorer and it added nothing on this task family (target coverage 23 of 45 versus 24 of 45), so barkup ships the thing that is deterministic, synchronous, and reproducible. If your application needs semantic retrieval over prose-heavy trees, you can still do it: renderView accepts whatever focus ids your retriever produces.
The usual caveats travel with the numbers: two models, a generated corpus, trees of 300 to 1000 nodes, and one pre-registered benchmark series behind all of it. Everything is reproducible from the benchmark repo, including a disclosed tie-ordering divergence between the benched scorer and this shipped port (breadth-first versus document order on equal scores; the details are in the repo's REPORT.md). If you find more of those, we want the issue.