Skip to content
AgentEnsemble AgentEnsemble
Get Started

Workspace Isolation

Coding agents make experimental changes. Without isolation, those changes land directly in the user’s working tree — potentially breaking their build, conflicting with uncommitted work, or leaving half-finished code behind if the agent fails.

The agentensemble-workspace module provides workspace isolation using git worktrees for git-based projects and temporary directories for non-git projects.

BackendWhen to useHow it works
Git worktree (GitWorktreeProvider)Git repositories (the common case)Creates a zero-copy, branch-isolated working directory from the same repo
Temporary directory (DirectoryWorkspace)Non-git projects or quick experimentsCreates a temp directory, optionally copies source files

Git worktrees are preferred because they share the object store with the main repository, so creation is fast and disk-efficient.

Every isolated working directory implements the Workspace interface:

MethodReturnsDescription
path()PathAbsolute path to the isolated working directory
id()StringHuman-readable identifier (branch name for worktrees, directory name otherwise)
isActive()booleanWhether the workspace is still active (not yet cleaned up)
close()voidClean up: remove worktree + branch, or delete temp directory

Workspace extends AutoCloseable, so you can use try-with-resources:

try (Workspace ws = provider.create()) {
// All changes happen inside ws.path()
}
// Workspace is automatically cleaned up here

Controls how workspaces are created:

FieldDefaultDescription
namePrefixnull (provider defaults to "agent")Prefix for generated branch/directory names
baseRef"HEAD"Git ref to branch from (ignored by DirectoryWorkspace)
autoCleanuptrueWhether close() removes the worktree/directory
workspacesDir<repoRoot>/.agentensemble/workspaces/Where to create workspaces
WorkspaceConfig config = WorkspaceConfig.builder()
.namePrefix("fix-login-bug")
.baseRef("main")
.autoCleanup(true)
.build();

Factory interface for creating workspaces:

public interface WorkspaceProvider {
Workspace create(WorkspaceConfig config);
Workspace create(); // uses default config
}
GitWorktreeProvider provider = GitWorktreeProvider.of(Path.of("/path/to/repo"));

The of() method validates that the path is a git repository (has a .git directory or file).

// Default: branch from HEAD, auto-cleanup on close
try (Workspace ws = provider.create()) {
Path workDir = ws.path();
// workDir is a fully functional git worktree on its own branch
// e.g., /path/to/repo/.agentensemble/workspaces/agent-a1b2c3d4
}
WorkspaceConfig config = WorkspaceConfig.builder()
.namePrefix("refactor")
.baseRef("feature/auth")
.workspacesDir(Path.of("/tmp/workspaces"))
.build();
try (Workspace ws = provider.create(config)) {
// Branch: refactor-<uuid>, based on feature/auth
// Located in /tmp/workspaces/refactor-<uuid>
}

When close() is called on a git worktree workspace:

  1. git worktree remove <path> — removes the worktree
  2. If the worktree is dirty (uncommitted changes), retries with --force
  3. git branch -D <branch> — deletes the temporary branch
  4. If any step fails, it logs a warning but does not throw

Set autoCleanup(false) to keep the worktree after close:

WorkspaceConfig config = WorkspaceConfig.builder()
.autoCleanup(false)
.build();
Workspace ws = provider.create(config);
// ... agent does its work ...
ws.close();
// Worktree and branch still exist -- user can inspect and merge manually

For non-git projects, use DirectoryWorkspace directly:

// Empty temp directory
try (DirectoryWorkspace ws = DirectoryWorkspace.createTemp()) {
Files.writeString(ws.path().resolve("main.py"), "print('hello')");
}
// Copy from an existing directory (.git is automatically skipped)
try (DirectoryWorkspace ws = DirectoryWorkspace.createTemp(Path.of("/project/src"))) {
// ws.path() contains a copy of /project/src
}

WorkspaceLifecycleListener is an EnsembleListener that creates a workspace when a task starts and cleans it up when the task completes or fails. Register it on your ensemble:

GitWorktreeProvider provider = GitWorktreeProvider.of(repoRoot);
WorkspaceLifecycleListener listener = WorkspaceLifecycleListener.of(provider);
EnsembleOutput result = Ensemble.builder()
.chatLanguageModel(model)
.listener(listener)
.task(Task.of("Refactor the authentication module"))
.build()
.run();

During task execution, tools can look up their workspace:

Optional<Workspace> ws = listener.getWorkspace(taskIndex, taskDescription);
ws.ifPresent(workspace -> {
Path workDir = workspace.path();
// Use workDir for file operations, builds, etc.
});
WorkspaceConfig config = WorkspaceConfig.builder()
.namePrefix("coding")
.baseRef("develop")
.build();
WorkspaceLifecycleListener listener = WorkspaceLifecycleListener.of(provider, config);
Map<String, Workspace> active = listener.activeWorkspaces();
active.forEach((task, ws) ->
System.out.println(task + " -> " + ws.path()));
  • GitWorktreeProvider.of() throws WorkspaceException if the path is not a git repository
  • WorkspaceProvider.create() throws WorkspaceException if worktree creation fails
  • Workspace.close() never throws — cleanup failures are logged at WARN level
  • WorkspaceLifecycleListener catches all exceptions internally to avoid disrupting task execution
// Gradle
implementation("net.agentensemble:agentensemble-workspace:$agentensembleVersion")
// Or via the BOM
implementation(platform("net.agentensemble:agentensemble-bom:$agentensembleVersion"))
implementation("net.agentensemble:agentensemble-workspace")