DEV Community

Cover image for We built our agent a tool for codebase intelligence
Vektor Memory
Vektor Memory

Posted on

We built our agent a tool for codebase intelligence


Why we didn’t want a full index

The starting idea was simple. Give our autonomous coding agent a real understanding of the codebase it’s editing, not just whatever files it happens to grep into.

Note: This article has been created in natural human language to be read easily, devoid of complex technical jargon, so all readers can enjoy. View one of our other past 70 articles for deeper technical dives.

Open source tools like CodeGraph and code-review-graph already do this well. They parse your repo with tree-sitter, build a graph of every function, class, and import, and let an agent query it instead of re-reading files from scratch on every task.

The problem we found is that graphs cost to keep open: the bloat tax. Both of those tools build a persistent index on disk, and a background watcher keeps it in sync with every file save. That’s fine for a single project you keep open all day.

It’s the shaped tool for an agent that might touch a dozen different projects in an afternoon, each one spinning up a watcher and a .codegraph folder that outlives the task that needed it.

We wanted the intelligence without the standing cost. A code graph that shows up exactly when a task needs one and disappears the moment the task is done, autonomous, intelligent, and relevant.

Agent loop stubbornness

While building, an agent spent eight steps convinced our own file system was broken, looping.

It kept calling list_dir, getting a clean result back, and then telling us the tools were failing. Three different frontier LLM providers did the exact same thing, as our system is provider agnostic.

This was not a permissions error, just a model quietly hallucinating a filesystem outage while the directory listing sat right there in its own context window.

That error, and the two others we found chasing it, ended up teaching us more about how to build code intelligence improved for our own agent than the features we originally set out to build.

The devil is in the detailed refinement.

Sometimes simple code that works is far better than dozens of features that keep expanding into bloatware that 80% of users don’t use very often or need at all.

The three-tier gate

The actual research made this easier to justify than we expected. A 2026 paper called “Retrieval as a Decision” https://arxiv.org/abs/2511.09803 argues that most retrieval-augmented systems should treat the decision to retrieve at all as a first-class, trainable step, not something baked into a fixed pipeline. That’s close to what we ended up building, minus the trained part.

Every task our agent takes on gets classified into one of three tiers before any graph work happens. A small, single-file edit skips the graph entirely and goes straight to a plain file read.

A task that touches three or more files, or a shared file like a config or a types module, triggers a scoped build: parse just the touched files and whatever they import, out to two hops deep.

A request for a genuine architecture overview, something a user actually asked for by name, triggers a slower full pass starting from the project’s real entry points.

That middle tier is where the actual engine lives. It’s built entirely in Node, using web-tree-sitter compiled to WebAssembly rather than the native tree-sitter bindings most of these tools use.

All hail Node

That Node choice wasn’t a close call for us, and it’s worth being direct about why. Most of the graph tools in this space, including code-review-graph, are written in Python. We considered it, and we’re not doing it ever.

All of Slipstream is 100% Node already, top to bottom, and it works. Node has the largest package ecosystem of any runtime available today, more mature tooling around it than any alternative, and a single-language codebase means no subprocess bridge, no second package manager to keep in sync, no version mismatch between a Python venv and the Node process actually running the agent.

We like Python genuinely and use it for other projects. It’s a good language for a lot of jobs. It just isn’t the right tool for shipping one coherent, dependency-light agent runtime and bolting a Python subprocess onto a Node codebase.

To get tree-sitter bindings would have reintroduced the exact cross-language fragility we were trying to avoid in the first place. Node was staying, full stop, and the rest of the design had to work within that.

Native bindings need to be compiled per platform, and we'd already been burned by exactly that kind of native module fragility elsewhere in this codebase. WASM sidesteps the whole problem. No compiler needed, same parser accuracy, one dependency that just works the same way on every machine.

The graph itself lives entirely in memory, scoped to one agent session, and gets thrown away the moment that session ends. Nothing gets written to disk. No watcher runs while the agent is idle.

If the same project gets touched again five minutes later in the same session, the graph gets reused and extended instead of rebuilt from scratch, so the zero-footprint choice doesn’t mean paying the full cost twice.

We also gave the agent three tools instead of the twenty-eight a tool like code-review-graph exposes. A cheap existence check that just asks, "Does this pattern already appear in this one file?" a scoped blast-radius expansion for when a change actually touches multiple files, and a full architecture pass for the rare cases that warrant one. Fewer tools means less for the model to choose wrong, and the gate is doing the choosing anyway.

To add, there's a general rule worth stating plainly here: WASM only pays off when you cross the JS boundary rarely and do real work on the other side of it. 

Call into a WASM module constantly for small pieces of data and you end up paying string-copy and serialization costs on every single call, often for work that would have been just as cheap in JavaScript to begin with, since V8's own native JSON.parse and string handling are already heavily optimized. 

Call into it infrequently for a substantial chunk of work, and the crossing cost becomes irrelevant next to what you get back. Our CodeGraph engine sits firmly on the right side of that line. 
Web-tree-sitter gets called once per file, to parse a whole source string into an AST when a task actually needs it, not once per keystroke or once per token in some hot loop. 

That's exactly the profile WASM is built to win at: infrequent calls doing real work, not death by a thousand small boundary crossings. It's also part of why we didn't reach for native tree-sitter bindings or a Python subprocess instead. 

Every language boundary adds the same kind of tax, whether it's JS to WASM or Node to a Python child process, and the only way to actually come out ahead is to cross it as rarely as possible and make each crossing count.

Three papers and why we didn’t build a fourth open source tool

Before writing any of this, we spent time on three pieces of recent research, because the question we kept running into wasn’t “can we build a code graph.”

Anyone can build a code graph; it's straightforward!

The real question was when an agent should be allowed to use one.

The first is “Retrieval as a Decision: Training-Free Adaptive Gating for Efficient RAG,” from late 2025. Its argument is that most retrieval systems treat retrieval as mandatory, something that runs on every request whether it helps or not, when it should be treated as a decision with a real cost and a real chance of being unnecessary.

That’s the paper our gate is a direct answer to. Every task gets classified before any graph work happens, and most small edits never touch the graph at all.

The second is a 2026 survey called “SoK: Agentic Retrieval-Augmented Generation,” https://arxiv.org/abs/2603.07379 which lays out a taxonomy for how agent systems plan, retrieve, and manage memory.

Its main point, restated plainly, is that the field keeps building more aggressive retrieval pipelines when what actually helps is cost-aware orchestration: knowing when to spend the tool call and when not to.

That’s the argument for keeping the tool count small. Three tools with a gate in front of them beat twenty-eight tools and a model left to guess which one applies.

The third, “A-RAG,” https://arxiv.org/html/2602.03442v1, proposes exposing retrieval as a small set of tiered interfaces instead of one flat search function so a model can reach for the cheapest tool that could plausibly answer the question before escalating to something heavier.

That’s where the fourth, even simpler tool tier came from after the initial build: a pure existence check that costs almost nothing, sitting below the three tools we started with.

None of the three papers describe a finished product. They describe an architectural shape. The actual code, the WASM parser, the session cache, and the tier classifier—all of that is ours. But the shape mattered because it’s the opposite of what the existing open-source options do.

CodeGraph and code-review-graph are genuinely useful tools, and code-review-graph in particular has real, published numbers behind it: an 8.2x average token reduction, 100% recall on blast-radius analysis, support for twenty-four languages.

If you run one project all day in one editor, either of them will likely serve you well. But both are built around the assumption that retrieval should always be available, which means a persistent index, a background watcher, and in code-review-graph’s case twenty-eight separate tools for a model to choose between on every call.

That’s a build strategy we didn’t want to make, as an agent that jumps between projects doesn’t want a watcher spinning up in the background of every one of them. A smaller or cheaper model doesn’t reason well with twenty-eight tool options in front of it, and the three separate provider bugs we found tonight are proof of exactly how easily that kind of complexity turns into silent failure instead of a helpful answer.

For a user, the practical difference is that nothing installs a daemon on your machine, nothing leaves a folder behind after the task is done, and the agent doesn’t get slower or more confused as the tool list grows. You get code intelligence sized to the task in front of you, not a standing index you’re paying for whether you’re using it or not.

In summary, our tool is honed and specific to the task, keeping bloat and errors down.

Will it do everything—Swiss Army knife style? NO, that's not the design.

What actually broke whilst testing

We wired all of this into our agent’s real tool loop and ran it against an actual task today in testing: refactor error handling across three files into a shared helper. That’s when the filesystem hallucination showed up, and it took real digging to find the cause.

Our agent talks to most language models through a hand-rolled JSON protocol since not every provider supports native tool calling the way Claude does. After every tool call, the result gets folded back into the conversation for the next turn. We found the exact line doing that folding, and it was silently destroying the tool’s output.

When the model’s most recent message contained an array of structured data instead of a plain string, which happens on every single turn after the first tool call, JavaScript’s default array-to-string conversion turned it into the literal text [object Object].

The real directory listing, the actual file the model needed to see, was gone by the time it reached the model. Every provider except the one using a different, correctly typed code path hit this on every turn. That's why three separate models all failed the same strange way.

We rewrote that step to render tool results as plain, readable text instead of letting JavaScript guess. The very next run, the model read the output correctly and reasoned about it like a normal conversation, because for the first time it actually was one.

Fixing that surfaced a second error immediately. Our search_code tool kept reporting zero files scanned, even for files we knew existed. The glob pattern the model was passing, something like */.js, was being matched against a bare filename with no directory in it at all, so a pattern built around slashes could never match anything. One rewrite of the glob logic later, the same search returned real matches across nearly two hundred files.

Neither of those issues had anything to do with the graph engine we set out to build. They were sitting in the surrounding agent harness the whole time, and they only became visible because we ran the graph tools inside a real conversation instead of testing them in isolation.

That’s the actual real-world testing example. A feature that only gets exercised in a unit test can look finished and still be sitting inside a loop that quietly breaks it the first time a real model uses it.

Agent Screen in Slipstream 1.8.0

Where this lives now

All of this ships as part of Slipstream 1.8.0’s autonomous agent, the same one behind the AGENT tab in the Slipstream GUI. Point it at a real project, give it a task, and the gate makes the retrieval decision on its own. You’ll see it in the CODEMAP panel, which used to run a blind, unscoped walk of the entire workspace every time you clicked it.

It now shows exactly what the agent actually needed context for, scoped to the files it touched, with a small receipt explaining why: how many files, how many hops, and whether the reverse-dependency index was already warm from an earlier step in the same session.

We also shipped a few smaller additions past the initial build. A fourth, even more cost-effective tool tier for a pure existence check before the agent commits to reading a whole file. A real session-scoped reverse-dependency index, built once from actual import resolution instead of a repeated string scan, so "What else imports this file?" answers reliably instead of approximately.

Basic route detection for Express and Next.js projects, so an API route shows up as a route in the graph instead of just another anonymous function.

None of it is running as a background service waiting for you to open a project. It runs when a task actually needs it, and it gets out of the way the moment the task is done.

If you’re already running Slipstream, the update is a normal npm refresh, and the agent tools show up automatically the next time you open a project with the code tool group enabled.

If you haven't tried our autonomous agent yet, this is a good week to start. Point it at something you already know well, give it a real multi-file task, and watch the CODEMAP panel light up with exactly the files it actually needed, not the four hundred it used to grab out of habit.

Full setup and docs are at vektormemory.com/docs and 1.8.0 info at https://vektormemory.com/docs/changelog

AI Agent
Agentic Ai
LLM
Code
Knowledge Graph

Top comments (0)