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.
Git Worktrees vs. Temporary Directories
Section titled “Git Worktrees vs. Temporary Directories”| Backend | When to use | How 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 experiments | Creates 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.
Core Interfaces
Section titled “Core Interfaces”Workspace
Section titled “Workspace”Every isolated working directory implements the Workspace interface:
| Method | Returns | Description |
|---|---|---|
path() | Path | Absolute path to the isolated working directory |
id() | String | Human-readable identifier (branch name for worktrees, directory name otherwise) |
isActive() | boolean | Whether the workspace is still active (not yet cleaned up) |
close() | void | Clean 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 hereWorkspaceConfig
Section titled “WorkspaceConfig”Controls how workspaces are created:
| Field | Default | Description |
|---|---|---|
namePrefix | null (provider defaults to "agent") | Prefix for generated branch/directory names |
baseRef | "HEAD" | Git ref to branch from (ignored by DirectoryWorkspace) |
autoCleanup | true | Whether 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();WorkspaceProvider
Section titled “WorkspaceProvider”Factory interface for creating workspaces:
public interface WorkspaceProvider { Workspace create(WorkspaceConfig config); Workspace create(); // uses default config}Using Git Worktrees
Section titled “Using Git Worktrees”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).
Creating a Workspace
Section titled “Creating a Workspace”// Default: branch from HEAD, auto-cleanup on closetry (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}Custom Configuration
Section titled “Custom Configuration”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>}What Happens on Close
Section titled “What Happens on Close”When close() is called on a git worktree workspace:
git worktree remove <path>— removes the worktree- If the worktree is dirty (uncommitted changes), retries with
--force git branch -D <branch>— deletes the temporary branch- If any step fails, it logs a warning but does not throw
Keeping a Workspace
Section titled “Keeping a Workspace”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 manuallyUsing Directory Workspaces
Section titled “Using Directory Workspaces”For non-git projects, use DirectoryWorkspace directly:
// Empty temp directorytry (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}Automatic Lifecycle Management
Section titled “Automatic Lifecycle Management”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();Looking Up the Active Workspace
Section titled “Looking Up the Active Workspace”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.});Custom Configuration
Section titled “Custom Configuration”WorkspaceConfig config = WorkspaceConfig.builder() .namePrefix("coding") .baseRef("develop") .build();
WorkspaceLifecycleListener listener = WorkspaceLifecycleListener.of(provider, config);Monitoring
Section titled “Monitoring”Map<String, Workspace> active = listener.activeWorkspaces();active.forEach((task, ws) -> System.out.println(task + " -> " + ws.path()));Error Handling
Section titled “Error Handling”GitWorktreeProvider.of()throwsWorkspaceExceptionif the path is not a git repositoryWorkspaceProvider.create()throwsWorkspaceExceptionif worktree creation failsWorkspace.close()never throws — cleanup failures are logged at WARN levelWorkspaceLifecycleListenercatches all exceptions internally to avoid disrupting task execution
Dependency
Section titled “Dependency”// Gradleimplementation("net.agentensemble:agentensemble-workspace:$agentensembleVersion")
// Or via the BOMimplementation(platform("net.agentensemble:agentensemble-bom:$agentensembleVersion"))implementation("net.agentensemble:agentensemble-workspace")