Coding Agents on the JVM: Project Detection, Workspace Isolation, and Tool Composition
Most agent frameworks treat coding tasks the same as any other task: give the agent a file-read tool and a file-write tool and hope for the best.
In practice, an agent that can read and write files is not the same as an agent that can reliably work on a codebase. The gap between “can modify files” and “can fix a bug in a Gradle project” is significant, and it is mostly about context that the agent needs but does not have.
The missing context
Section titled “The missing context”A coding agent needs to know things that a general-purpose agent does not:
- What kind of project is this? Is it Java with Gradle, Python with pip, TypeScript with npm? The build command, test command, and source layout all follow from this.
- Where is the code? Source roots like
src/main/javaare conventions, not universal truths. The agent needs to know where to look. - How do I verify my changes? Running
./gradlew testis fundamentally different from runningnpm test. The agent needs the right command for the project. - How do I avoid breaking things? If the agent edits files directly in the user’s working tree, a failed experiment leaves half-finished code behind.
Without this context, agents make predictable mistakes: they guess at build commands, search in wrong directories, and leave the codebase in a worse state than they found it.
Project detection as a first-class concern
Section titled “Project detection as a first-class concern”The approach I’ve been working on in AgentEnsemble treats project detection as an explicit step before tool assembly.
ProjectDetector.analyze(Path) scans the project root for build-file markers and returns a ProjectContext that captures language, build system, source roots, and the commands needed to build and test:
| Marker file | Language | Build command | Test command |
|---|---|---|---|
build.gradle.kts / build.gradle | Java | ./gradlew build | ./gradlew test |
pom.xml | Java | mvn compile | mvn test |
package.json + tsconfig.json | TypeScript | npm run build | npm test |
pyproject.toml / requirements.txt | Python | python -m build | python -m pytest |
go.mod | Go | go build ./... | go test ./... |
Cargo.toml | Rust | cargo build | cargo test |
This is not magic — it is a lookup table backed by file-existence checks. But it means the agent’s system prompt includes the correct build and test commands for the specific project, rather than generic instructions that may or may not apply.
The detected context is injected into the agent’s instructions automatically. The agent knows it is working on a Java/Gradle project with source at src/main/java and tests at src/test/java, and it knows that ./gradlew test is the verification command.
Workspace isolation via git worktrees
Section titled “Workspace isolation via git worktrees”The harder problem is safety. A coding agent that writes directly to the user’s working tree is an agent that can break your build, conflict with your uncommitted work, or leave half-finished refactoring behind if it fails partway through.
Git worktrees solve this cleanly. A worktree is a lightweight, branch-isolated copy of a repository that shares the same object store as the original. Creation is fast and disk-efficient because it does not duplicate the git history.
EnsembleOutput result = CodingEnsemble.runIsolated(model, repoRoot, CodingTask.implement("Add user profile endpoint"));That runIsolated call:
- Creates a git worktree from the current branch
- Runs the coding agent inside the worktree
- On success, preserves the worktree for review (you can inspect the changes, run tests again, then merge)
- On failure, cleans up the worktree automatically
The key interface is Workspace:
public interface Workspace extends AutoCloseable { Path path(); // Absolute path to the isolated directory void close(); // Clean up (remove worktree)}For non-git projects, a DirectoryWorkspace creates a temporary directory and optionally copies source files. But for the common case — a git repository — worktrees provide isolation without the cost of a full clone.
The tradeoff is that worktrees require a git repository. If you are working on a non-git project or a freshly initialized directory, the fallback to temporary directories is less elegant. But for the vast majority of real codebases, worktrees are the right abstraction.
Composable tool backends
Section titled “Composable tool backends”Different environments have different constraints. Some teams run pure-JVM deployments where Node.js is not available. Others already use MCP servers and want to reuse them. A coding agent framework should not force one approach.
AgentEnsemble provides three tool backends, selected via ToolBackend:
| Backend | Description | Requires |
|---|---|---|
AUTO | Detect best available backend | Nothing |
JAVA | Java-native coding tools (glob, search, edit, shell, git, build, test) | agentensemble-tools-coding on classpath |
MCP | MCP reference servers for filesystem + git | agentensemble-mcp + Node.js |
MINIMAL | FileReadTool only | Always available |
AUTO resolves in order: MCP > JAVA > MINIMAL. If neither optional module is on the classpath, the agent works with file-read only — limited, but functional for read-only analysis tasks.
The Java backend provides purpose-built coding tools:
- GlobTool — find files by pattern across the project
- GrepTool — search file contents with regex
- CodeEditTool — surgical line-range replacement (not full-file overwrite)
- ShellTool — execute build/test commands with output capture
- GitTool — status, diff, stage, commit
The MCP backend starts the official MCP filesystem and git reference servers as subprocesses and adapts their tools to the AgentTool interface. Both backends produce the same tool interface, so the rest of the framework does not care which one is active.
The one-liner and the builder
Section titled “The one-liner and the builder”For the common case, a single call handles everything:
EnsembleOutput result = CodingEnsemble.run(model, Path.of("/my/project"), CodingTask.fix("NullPointerException in UserService.getById()"));That call detects the project, assembles tools, generates a coding-specific system prompt, and runs the agent with a higher iteration limit (75 vs the default 25 — coding tasks typically need more rounds).
For more control, the builder exposes every knob:
Agent agent = CodingAgent.builder() .llm(model) .workingDirectory(Path.of("/my/project")) .toolBackend(ToolBackend.JAVA) .requireApproval(true) .maxIterations(75) .additionalTools(myCustomTool) .build();The builder returns a standard Agent — no subclassing, no special execution path. You can use it with Task, Ensemble, phases, or any other framework feature. The coding agent is composed from the same primitives as every other agent.
Pre-configured task types
Section titled “Pre-configured task types”Common coding workflows have predictable shapes. A bug-fix task needs different instructions than a feature implementation or a refactoring:
Task fix = CodingTask.fix("NullPointerException in handler");Task implement = CodingTask.implement("Add pagination to /api/users");Task refactor = CodingTask.refactor("Extract UserRepository interface");Each returns a standard Task with appropriate description and expected-output templates. They can be further customized:
Task task = CodingTask.fix("Some bug") .toBuilder() .expectedOutput("Custom expected output") .build();This is a convenience, not a requirement. You can always construct a Task manually and pass it to a coding agent.
Tradeoffs and limitations
Section titled “Tradeoffs and limitations”Project detection is heuristic. It works for standard project layouts but will not detect custom build systems or unconventional directory structures. The fallback is explicit configuration via the builder.
Iteration limits are a blunt instrument. A higher limit gives the agent more chances to iterate, but it also means higher token costs if the agent goes in circles. There is no substitute for good prompting and appropriate task scoping.
Workspace isolation adds a step. The agent works in a worktree, but the user still needs to review and merge the changes. This is deliberate — automated merge would undermine the safety guarantee — but it does add friction to the workflow.
Tool backend selection is build-time. You choose your backend by including the right dependency. Runtime switching between Java and MCP backends is possible via AUTO, but you cannot hot-swap mid-execution.
The design principle
Section titled “The design principle”The useful abstraction is not “an agent that can code” but “a standard agent with the right tools and context for coding tasks.” The coding agent is not a special type — it is a regular agent, assembled with project-aware tools, operating in an isolated workspace, and configured with appropriate iteration limits.
This matters because it means coding agents compose with everything else in the framework: phases, delegation, human review, metrics, traces. There is no separate execution path to maintain.
The coding agent modules are part of AgentEnsemble. The coding agents guide and workspace isolation guide cover the full API.