Migration Guide: 0.11.0 to 0.12.0¶
This release introduces the sandboxing foundation: the ExecBackend and Workspace SPIs, directory confinement for the search tools, and the spring-ai-agent-utils-docker-cli module. It also adds two agent-loop utilities: InterruptAdvisor (cooperative cancellation of in-flight turns) and ToolCallListener (tool invocation observability). There are no breaking API changes — all existing code compiles and runs unchanged. There is one behavioral change to review (background shells) and a few output-level changes worth knowing about.
Dependency Version¶
<dependency>
<groupId>org.springaicommunity</groupId>
<artifactId>spring-ai-agent-utils</artifactId>
<version>0.12.0</version>
</dependency>
The Spring AI dependency is unchanged (2.0.1).
Behavioral change: background shells are per-instance¶
In 0.11.0 the background-shell registry was JVM-global: any ShellTools instance could read (BashOutput) or kill (KillShell) a shell started through any other instance. In 0.12.0 each ShellTools instance owns its background shells — separate instances have separate shell namespaces.
This is the right default (separate agents/sessions no longer see each other's shells), but if you relied on cross-instance visibility, those lookups now return Error: No background shell found with ID ... with no compile-time signal.
Before (0.11.0 — worked by accident of the global registry):
ShellTools agentA = ShellTools.builder().build();
ShellTools agentB = ShellTools.builder().build();
// agentB could read/kill shells started by agentA
After (0.12.0 — share one instance where shared visibility is intended):
Note: Claude subagents already share the single ShellTools created by ClaudeSubagentType, so they keep a shared namespace without changes.
Deprecations¶
Move off the deprecated no-argument constructors to the builders — they now carry the configuration that matters (execution backend, working directory, confinement):
// Before
ShellTools shell = new ShellTools();
GrepTool grep = new GrepTool();
// After
ShellTools shell = ShellTools.builder().workingDirectory(workDir).build();
GrepTool grep = GrepTool.builder().workingDirectory(workDir).build();
Output-level changes (no code impact)¶
These change what the model (or a log reader) sees, not any API:
AgentEnvironment.gitStatus()no longer leaks git error text into the rendered block. Previously, in a repository with no localmain/masterand noorigin/HEADsymref, the "Main branch" line could contain a literalfatal: ...message; failed git commands now contribute empty strings and the main-branch detection falls back tomain. Git commands also run through theExecBackendSPI (platform shell) instead of a privateProcessBuilder.FileSystemTools.readheader saysShowing lines 1-N of at least Mwhen the line limit truncates the read, instead of implying the file was fully counted.Bashwith a blank command returnsError: command must not be blankinstead of spawning a shell.- Synchronous
Bashrun ids moved to their ownshell_run_<n>namespace so they can never collide with background shell ids.
New in 0.12.0 (opt-in, no migration required)¶
ExecBackendSPI (org.springaicommunity.agent.common.exec) — pluggable command execution;ShellTools.builder().execBackend(...)andAgentEnvironment.gitStatus(ExecBackend).LocalExecBackend(the default) addsworkingDirectory,cleanEnvironmentandshellCommandoptions.WorkspaceSPI (org.springaicommunity.agent.common.workspace) — root directory + host-to-model path display; one-callworkspace(...)option on the tool builders,SkillsToolbase-path mapping,AgentEnvironment.info(Workspace).- Directory confinement for search tools —
allowedDirectory(...)onGrepTool,GlobToolandListDirectoryTool, sharing the FileSystemTools jail semantics. Note: when confinement is configured, directory traversal does not follow symbolic links (unconfined behavior is unchanged). spring-ai-agent-utils-docker-cli— a DockerExecBackendrunning commands in a sandbox container (newexec-backends/module group, managed by the BOM).InterruptAdvisor(org.springaicommunity.agent.advisors) — cooperative cancellation of in-flight agentic turns. Polls an application-suppliedBooleanSupplierbefore each model request; its default order (HIGHEST_PRECEDENCE + 400) sits just inside the tool-calling advisor so the check re-runs on every tool-call round. When the signal fires, the turn unwinds with the newTurnInterruptedException(withstream(), it arrives as the Flux error signal). Replaces hand-rolled cancellation flags checked inside tools.ToolCallListener+ToolCallListeners.wrap/wrapAll(org.springaicommunity.agent.tools) — observe every tool invocation (audit logs, SSE progress, metrics) without writing aToolCallbackdecorator.beforeCallreturns an opaque correlation context handed back toafterCall/onError;onErrorreturns an error string to report to the model ornullto rethrow. Definition and metadata are delegated untouched, so wrapping composes with any callback source. If you had your ownToolCallbackdecorators for logging, these can be replaced with a listener.
See Workspace & Exec SPI for the overview and DockerCliExecBackend for the full sandbox recipe. For the agent-loop utilities, see InterruptAdvisor and ToolCallListener.