Most of what makes an agent useful on a real codebase is plumbing nobody advertises.
I kept hitting the same handful of failures with coding agents, and none of them were the interesting* kind. An agent reads one file per call, and by the sixth one it’s lost the plot of why it started. Going to the refrigerator for every ingredient is a sure-fire way to make dinner late.
I’ve lost count of how many times an agent has clobbered changes I made because it was convinced it already knew what the file contained — still working from a copy it read before it wandered off to think, cheerfully writing that stale version back over the top of my work. Or it tells me a function has no callers, confidently and without hedging, when what really happened is that the lookup quietly failed, or it didn’t bother to look it up at all.
None of that is the model being dumb. It’s plumbing. So most of what’s in Konstruct is plumbing: how it reads files, how it edits them without clobbering your work, what it can ask about the shape of your code, and how much of what it tells you it can back up. What follows is the short version of how and why these things give your agent a better shot at getting it right the first time. Letting one work unattended for an hour is a harder problem, and it gets its own page.
* Interesting as in a genuinely hard computer science problem. You know — CAP theorem, distributed consensus, cache invalidation, naming things, off-by-one errors…
Reads and writes travel in batches
Most agent tooling reads one file per call: one trip to the refrigerator per ingredient. A change that touches eight files is eight round trips before any thinking starts, and eight chances to lose the thread on the way.
So the read and write tools take a list. One call fetches a set of files; one call applies a set of edits. Each entry comes back with its own status, so a batch isn’t all-or-nothing: four edits land, one gets rejected for a specific reason, and the agent is told exactly which was which instead of getting a single opaque failure. When a change has to apply completely or not at all, the agent can mark the batch atomic, and the tool undoes the writes that succeeded as soon as one of them fails.
Batches are bounded, though. A read batch that would haul back too much content stops and reports the rest as skipped, rather than tipping the whole repository into the context window. It is fewer round trips on an ordinary change rather than one call that swallows everything: eight files in a single fetch, and the agent still knows what it was doing by the end of them.
Edits are hash-verified
The stale write is the failure that bothers me most, and it isn’t about an agent taking too long to think. A file’s contents are sitting in its context from some earlier turn, so it assumes they’re still accurate, and any gap will do — a few requests, a coffee, a day. I’d work with an agent for a while, then make some changes by hand, and unless I’d remembered to commit first I was betting on it not deciding it knew better. Unfortunately it always decided it knew better, and nobody was keeping an eye out.
So every file read comes back with a content revision: a hash of the exact bytes the agent saw. An edit to an existing file has to carry that revision back with it. If the file has moved on in the meantime, the write is refused and reported as a conflict, and the agent goes back to re-read the thing and think again against what’s on disk.
It’s optimistic concurrency, the same check a database does, pointed at the one place an agent is most likely to destroy work and least likely to notice. It catches edits built on a stale read, and that is all it does. It doesn’t make an edit correct — a confident agent can still write the wrong thing to the right version of a file, and this check will let it through. What it does buy you is the end of committing defensively before you touch a file yourself.
A few intent-shaped tools, not fifty
Give a model thirty overlapping tools and two things go wrong. It picks the wrong one, grabbing grep when it wanted symbol lookup or list-files when it wanted search, and the schemas by themselves eat a serious slice of the context window before anyone has said anything.
Konstruct exposes a small set of tools shaped around intent rather than mechanism: search for finding things, read_code and write_code for file content, traverse_graph for structural questions, enumerate for listing a collection. Each one takes a mode or a match type instead of spawning a sibling tool. “Find text”, “find a file by glob” and “find something by meaning” are one tool with three match shapes, not three tools elbowing each other for the same slot.
The result is a smaller surface to describe, fewer wrong turns, and more of the context window left for your actual code. It holds up everywhere except structural graph queries, where neither the problem nor the vocabulary for it would fold into a single intent without bending over backwards and squinting at it sideways.
The graph knows who calls what
Konstruct builds a dependency graph of your workspace: imports, references and symbol definitions, as edges between actual files. It’s structure rather than similarity, so there are no embeddings and no text index involved in answering.
That turns questions an agent would otherwise guess at into questions with answers. “What breaks if I change this?” becomes a traversal of inbound edges ranked by proximity, instead of a hopeful grep for a function name that also appears in three unrelated comments. “What does this file depend on?” becomes a resolved edge list, instead of an import block taken at face value.
Coverage is a genuine limit: indexing and graph analysis currently handle TypeScript, Python, Java and Go. In a repository built on anything else there is less in the graph to work with, and the tool is explicit about that rather than handing back an empty result, which reads like a confident “nothing found”. The agent is told the question is out of range for this repository, and goes to search instead.
The graph also clusters the workspace into neighborhoods — groups of files that depend on each other in practice — and surfaces the ones relevant to your request at the start of a session. The agent starts out pointed at the right part of the repository, instead of spending its first several turns rediscovering a layout you already know.
Results arrive with their surroundings
A grep hit is a path and a line number. That is enough to find the text and not enough to do anything with it, so the turns that follow are spent asking the obvious follow-up questions: what is around this line, what else lives in this part of the repository, who calls the thing I just found. Every one of those is another round trip, and the answers were sitting in the graph the whole time.
So the ordinary tools carry them back with the result. A search hit comes with a suggested line range, so the next read asks for the forty lines around the match rather than the whole file, and it is tagged with the neighborhood it belongs to. Text search finds the line; the graph says what the line is attached to.
The less obvious ones are the tools where you would not think to ask. read_code hands back, alongside the file you asked for, the files that import it and the ones that sit in the same neighborhood — so an agent that opened one file knows what else it is going to have to care about. write_code and delete_code do the same on the way out: every successful write comes back with the inbound dependents of the file just changed, which is the blast radius of the edit, delivered at the moment it becomes relevant rather than when someone remembers to go looking. getGitDiff carries a graph impact summary for the changed files for the same reason.
None of this is an extra call the agent has to know to make. The enrichment runs over results that are already in hand — one readiness check and one neighbors query per tool call, however many files were involved — and it is best-effort throughout: a graph that is unavailable or mid-build never blocks a read, a write or a delete. Nothing structural is claimed against a graph that is not ready, either — if the index cannot be trusted for a file, the tool returns the file and says the graph had nothing to add, rather than offering a stale answer with a straight face.
Structural questions aren’t one question
Structural questions look like one category until you try to answer them. “What breaks if I rename this function?” and “what breaks if I move this whole directory?” are both blast-radius questions, and almost nothing about the traversal they need is shared: different depth, different direction, different idea of what a complete answer looks like.
So traverse_graph is one tool with a required mode. The tool still carries the intent; the mode names which structural question is being asked. There are fifteen. These are the ones that come up in refactoring work:
-
precheckOrientation before touching anything: readiness, shallow impact and neighbors in one call. Two profiles — edit stays shallow and fast, audit goes six levels deep when you actually want the full picture. -
impactThe main blast-radius query. Inbound edges, three levels deep, up to a hundred paths ranked by proximity.scope.under/over/modulefence it to a single package, for the common case where a widely-used file would otherwise pull back most of the repository. -
directory_impactMoving or extracting a whole directory. Depth ten, because the thing you are relocating is itself many layers deep. -
usage_refs/import_refsRenames. Call and member sites are a different set from import sites, and a rename usually needs both, separately. Budgeted at 500 paths — a rename must be exhaustive, not ranked. -
validate_symbol_refsChecks the graph’s reference count against a raw grep. If they disagree, the index is lying and you want to know before the rename, not after. -
dependencies/neighborsThe other direction: what this file pulls in, and what sits immediately around it. Depth one, because “what does this need” degenerates into the whole dependency tree fast.
What matters most about that list is invisible in it: each mode sets its own depth, breadth and direction. impact runs three levels inbound, directory_impact runs ten, the rename modes sweep up to 500 paths, and dependencies looks one level outbound. Those limits are what decides whether an answer is worth having. Search too shallow and a caller two hops away never shows up, so the rename ships broken. Search too deep and a query on a common utility returns most of the codebase — accurate, unreadable, and slow to produce.
Picking sensible bounds for each kind of question is real work, and it belongs in the tool rather than in a prompt. The mode is what sets the boundaries and the optimizations, and those differ enough from one question to the next to matter: be specific about what you want out of the graph and the agent gets back a short, meaningful set of results instead of being buried in them.
The agent is told when data is missing
A confident wrong answer is worse than an uncertain right one. More of the work in the graph layer went into working out how far to trust an answer than into producing it in the first place.
Trust is per-query, not global. A map published in 1800 is still perfectly good for mountains, rivers and coastlines. I just wouldn’t trust it to help me find the local supermarket. A graph built a commit ago is the same sort of document: nothing outside the files that commit touched has moved. So rather than flipping a repository-wide flag and refusing to answer, Konstruct answers the query and tells the agent which files fall inside the changed set and need re-checking. Degree, not doom.
An uncommitted working tree is not staleness. Your in-progress edits are merged as a live overlay on top of the built graph, so an uncommitted tree does not by itself invalidate an answer. The awkward case — the file you are actively editing has moved ahead of the index — is detected and reported on its own. The agent re-checks that one file directly and keeps trusting the rest.
Empty results carry meaning. “No results” is three different statements, and Konstruct separates them: the symbol is defined and has no references (a real dead-code signal), the thing you asked about is not in the index (ask a different way), and this cannot be determined right now (draw no conclusion). An agent that can’t tell these apart will report a function as unused when the truth is that it never managed to look. That’s the failure this whole design exists to prevent.
Indexing that doesn’t thrash
Computing something the first time somebody asks for it is a sensible default in a lot of systems: nothing gets built that nobody wanted. It works badly for indexing, because the work is not small and the thing waiting on it is an agent in the middle of a thought. One question about the graph turns into a rebuild, the agent stalls on work it caused itself, and your laptop spends the next few minutes doing its best impression of a hair dryer.
It is also work nobody needed to do. The agent that just edited those files has read them and written them and knows perfectly well what is in them, and an agent that hasn’t can read them directly — and gets told the index is behind for those particular files rather than being left to guess.
So reads never schedule work. Querying the graph is not a trigger for indexing; that is driven separately by a background reconciler, and an agent exploring the codebase does not stall behind a rebuild it set off. The rest of the policy is in the same spirit:
- Incremental by default. Commit deltas are reconciled against the existing graph. A full rebuild is reserved for an index that is missing or corrupt, and is a deliberate action rather than something that happens on its own while you work.
- Independent lifecycles. The dependency graph and the semantic search index are separate subsystems with separate progress and separate currency. One being mid-build does not block the other.
- Bounded cost per event. Work is proportional to what changed — counters rather than re-reading files, and an eviction policy on the in-memory caches so they stay a fixed size instead of growing for the length of a session.
Containment, and where it stops
File access is fenced at the point of access. Every read and write the agent makes goes through the workspace daemon, which is the only component that touches your code, and every path it opens is resolved through a rooted filesystem handle that refuses .. traversal, absolute paths, and symlinks pointing outside the workspace. The check happens where the file is opened, not in a filter somewhere upstream that has to anticipate every way of spelling a path. It’s the one guarantee in Konstruct that doesn’t depend on a pattern list being complete: the handle either resolves the path inside the workspace or the open fails.
Subprocesses are a different story. Once a command runs it is a real process on your machine, and nothing short of a container or a VM can promise it will stay put. And even then it isn’t guaranteed — container escapes happen, and VMs aren’t completely immune either. Konstruct doesn’t make that promise. What it tries to do is keep the agent from getting clever in the first place: it is never shown where it actually is on disk, which pushes it towards Konstruct’s own tools instead of improvising its own route to your files.
- The agent is not shown where it is. The real workspace root is replaced with a neutral
[workspace]token in command output and error text, and common home-directory paths are collapsed the same way. The root substitution is exact; the rest is a filter over known prefixes, so an unusual absolute path can survive it. - Commands are validated twice, on both sides of the boundary. Anything starting with an absolute path or
~, containing a..segment, or using a drive letter is rejected. The workspace daemon repeats that check itself rather than trusting the caller, so a request that skips the app layer is still refused at the point of execution. - An execution policy gates what can run at all. A small set of hard deny rules is always active. Beyond that, agent-issued commands always arrive carrying an explicit policy, which can run in allowlist mode — nothing executes unless it matches a rule you defined — and can forbid compound shell syntax, so the agent cannot chain
;,&&, pipes or$(…)to assemble its way past a check. - Working directories are set for the agent, not by it. Commands run where Konstruct puts them.
How much that adds up to depends on how you have it configured, so here are the defaults. Compound shell syntax is disallowed out of the box, and a short list of things is refused no matter how you configure it: rm -rf /, mkfs, dd if= and the classic fork bomb. Allowlist mode is not on by default: until you turn it on, or apply one of the built-in toolchain presets that turns it on for you, the path and policy checks are the main defense, and those are prefix and rule based. They cover the routes an agent is most likely to take. They are not a proof that every route is closed.
Closing that gap properly is active work, on three fronts. Skylark scripting will let the agent write and run scripts inside a sandboxed interpreter restricted to read-only access, instead of shelling out. Build sandboxes will restrict it to build commands you have explicitly defined, so “run the tests” means the command you wrote down and nothing else. And running the agent fully containerized is on the roadmap for teams who want the boundary to be the runtime’s job rather than Konstruct’s (at the cost of the setup and friction that real isolation always brings, which is why it will be something you opt into).
Until then, a process that has started can do anything your shell can do, and Konstruct does what it can to keep an agent from taking advantage of that. There are layers and knobs for it, running from free-range through to fairly restrictive, and where you sit on that spectrum is a trade-off you make rather than one Konstruct can make for you. It is a best effort either way, particularly with CLI-based agents that turn up with their own ideas about how to get at your files. What you get is defense in depth by default, and room to tighten things up as far as your situation calls for.