Changelog¶
All notable changes to Spring AI ACP Harness Client are documented here. The project follows Semantic Versioning (MAJOR.MINOR.PATCH).
[0.7.0] - 2026-11-07¶
Documentation¶
- The project now goes by SAAHC (pronounced "sock") for short. The documentation introduces it as "Spring AI ACP Harness Client (SAAHC)" and uses it after that. Nothing technical is renamed: artifacts, packages, properties and types keep their names.
Changed¶
- ACP Java SDK 0.80.0 (#28). An application that uses the SDK's own types through this library meets its breaking changes; its CHANGELOG has a migration for each. The ones most likely to show:
StopReason,ToolCallStatus,PermissionOptionKind,PlanEntryStatus,PlanEntryPriorityandRoleare open value records rather than enums, so a newer agent's value no longer fails to read. Compare them withequalsand switch onvalue(). Observation tags,AcpController'sstopReasonandAcpChatModel's finish reason keep their upper-case spellings (END_TURN).ToolCall,ToolCallUpdateand their notification gain anamecomponent, and the short session-update constructors no longer take the discriminator.- A select option's values are
SessionConfigSelectOptions, flat or grouped; read them withallOptions(). Grouped options are matched like flat ones. - The legacy
modelsstate andsession/set_modelare no longer used. They never became stable ACP, and the SDK no longer models them.Mechanism.SESSION_MODELis removed. goose, Codex and OpenCode all offer the model as a config option, which is unchanged; an agent that offers models only the old way now getson-unsupportedhandling formodel.ScriptedAcpAgent.Builder.models(...)still advertises the old state, for testing exactly that. providers/setsends the provider asproviderId, the schema's name for it and the one Codex reads. The SDK used to sendid.- A turn that times out, or whose stream is cancelled, and whose agent has not answered
session/cancelwithin 5 seconds, also sends$/cancel_requestfor itssession/prompt, from the SDK. goose and OpenCode ignore it. session/closeis sent only to an agent that advertisessessionCapabilities.close, which the SDK now enforces. goose and OpenCode do.- A config option the SDK cannot read at all, such as a select mixing options and groups, now
fails
session/newrather than being skipped. An option of a type it does not know is still kept, asUnknownSessionConfigOption. - Removed
SessionConfigRecorderandSessionUpdateDecoder, which worked around SDK gaps that 0.80.0 closes, and therecorderparameter ofDefaultAcpHarnessClientandDefaultAcpHarnessSessions.session/loadandsession/resumeare no longer serialized. AcpHarnessTurnnow releases the session's turn permit itself, when the agent is done with the session, so a caller ofAcpHarnessTurn.promptacquires it and no longer releases it.
Fixed¶
- A supervised agent (
serve.transport: websocket) that failed to become ready was restarted by its own exit. After a start that failed, the supervisor went on spawning the agent in the background, although the caller had been told it did not start. A restart that failed scheduled two more, and the second spawned an agent beside the first that the application's shutdown never stopped. Only an agent that became ready and then exits is now restarted, once. Reported, with a proposed fix and its test, by @zjxzjw in #31. - A per-turn
timeoutlonger thanspring.ai.acp.harness.timeoutwas ignored: the SDK boundedsession/promptby the client-wide timeout, so the turn failed at that one. The SDK no longer bounds prompts, so the turn's own timeout applies (#25). - A turn that timed out left its
session/promptin the SDK's table of pending requests until the agent answered it, so an agent that hung and ignoredsession/cancelgrew that table by one entry per timed-out turn. The SDK now forgets a request its caller gave up on. - A turn that exceeded
timeoutended withFailedbut never sentsession/cancel, so the agent kept working on the prompt, spending tokens on a result nobody received. A timed-out turn now sendssession/cancel, as a cancelled stream already did (#20). - The next turn on a session could overlap one that was cancelled or timed out. The session was
handed on as soon as
session/cancelwas sent, but ACP lets the agent keep sending until it answers the cancelled prompt, and goose, Codex and OpenCode all do: ausage_updateorsession_info_updatea few milliseconds later, which the next turn received as its own. The agent could also get the next prompt while still finishing the last. The session now stays busy until the agent answers the cancelled prompt, or for 5 seconds if it never does, and what it sends meanwhile is dropped. The caller still hears that the turn ended at once (#27). - A permission request still open when its turn was cancelled or timed out was never answered, as
ACP requires, and with
policy: askthe person went on being asked. If they answered, their choice went to the agent for a turn that had already ended. The client now answers such a requestcancelledat once, answers any that arrive while the agent winds down the same way, and interrupts thePermissionPromptstill asking; a later answer is discarded. goose 1.57 was seen to ignore a late answer, so nothing ran (#26).
[0.6.0] - 2026-11-06¶
Added¶
PromptSpec.waitIfBusy(Duration)sets how long a turn waits for a named session that is still running an earlier turn, before failing with the newAcpHarnessSessionBusyException.Duration.ZEROfails at once; the default is still the turn's timeout. A session is now free again before its caller hears that a turn ended, so a caller that goes straight on to the next turn never finds it busy.
Changed¶
- Breaking: Java packages move from
org.springaicommunity.acp.*toorg.springaicommunity.acp.harness.*, to match the artifacts and the properties. Update your imports, and anylogging.level.org.springaicommunity.acpsetting, which otherwise stops matching without an error. The JDBC schema for MCP OAuth moves with them, toorg/springaicommunity/acp/harness/mcp/oauth/acp-mcp-oauth-schema.sql. - Breaking: every
AcpAgent…type becomesAcpHarness…, starting withAcpAgentClient, which becomesAcpHarnessClient(withDefaultAcpHarnessClient,AcpHarnessClientPool,AcpHarnessClientFactoryandAcpHarnessClientException, and the beanacpAgentClientbecomesacpHarnessClient). This library creates no agent: it configures an existing agent harness such as Goose, Codex or OpenCode, launches or attaches to it, and talks to it over ACP, so the types describe the harness and now say so, as the project, its artifacts, its packages and itsspring.ai.acp.harness.*properties already did. ACP itself calls the far end of the connection the agent, and the documentation keeps that word for the running program. HenceAcpHarnessRuntime,AcpHarnessRuntimeProvider,AcpHarnessLaunchSpec,AcpHarnessEnvironment,AcpHarnessNotice,AcpHarnessSession,AcpHarnessSessions,AcpHarnessTurn,AcpHarnessEvent,AcpHarnessEventMapper,AcpHarnessInfo,AcpHarnessSettings,AcpHarnessOptions,AcpHarnessObservations,AcpHarnessTurnObservationContext,AcpHarnessExecutor,AcpHarnessLogWatcher,AcpHarnessProcessSupervisor,UnsupportedAcpHarnessOperationException,UnsupportedAcpHarnessOptionException, the nestedAcpHarnessClient.AcpHarnessResponseandAcpHarnessStream, the registry runtime'sAcpHarnessRegistry,AcpHarnessInstallerandAcpHarnessInstallException, the BootAcpHarnessConfigDataLoader,AcpHarnessConfigDataLocationResolverandAcpHarnessConfigDataResource(formerlyAcpAgentsConfigData…, with an s), and the test kit'sAcpHarnessRuntimeContractandAcpHarnessProbe.AcpHarnessClient.agentInfo()becomesharnessInfo(), on the pool too. The beansacpAgentSettings,acpAgentRegistry,acpAgentInstaller,acpAgentObservationsandacpAgentExecutorfollow asacpHarness…, and the adapter beans an application overrides by name are nowgooseHarnessRuntime,codexHarnessRuntime,openCodeHarnessRuntimeandregistryHarnessRuntimeProviderinstead of…AgentRuntime…. The rule is mechanical, so replacing eachAcpAgentorAcpAgentstype-name prefix withAcpHarnessmigrates an application, and the compiler finds every site; a bean overridden by its old name is the one thing it cannot catch, so check those four by hand. Unchanged: packages, properties, metric names, and the test doubles that play the ACP agent role on the wire,ScriptedAcpAgentandLiveAcpAgents, since those are agents in the protocol's sense. - Breaking: the observations and their tags are named the way Spring AI names its own, so a
dashboard or alert on the old names must be updated. The two observations are
spring.ai.acp.harness.turnandspring.ai.acp.harness.tool.call(wereacp.turnandacp.tool.call); the model tag is the OpenTelemetrygen_ai.response.model(wasacp.model), the key Spring AI's chat models carry, so one breakdown by model covers both; every other key keeps its suffix underspring.ai.acp.harness.instead ofacp.(.runtime,.session.kind,.outcome,.session.name,.context.used,.context.size,.cost,.tool.kind,.tool.status,.tool.title,.tool.id); and both observations gain Spring AI'sspring.ai.kindtag,acp_harness_turnoracp_harness_tool_call. The outcome deliberately stays a single-valued tag of our own rather thangen_ai.response.finish_reasons, which Spring AI records as a high-cardinality list on the span. The bareacp.root was no library's to claim: ACP itself defines no telemetry names, and any other ACP library could have used the same ones. TheAcpChatModelresponse metadata keys follow, and now spell the same thing the same way as the turn span:acp.runtime,acp.session,acp.context.usedandacp.context.sizebecomespring.ai.acp.harness.runtime,.session.name,.context.usedand.context.size;acp.cost.amountandacp.cost.currencykeep their typed split as.cost.amountand.cost.currency. An absent model is taggednone, Spring AI's spelling on that key, where it wasunknown. For a customObservationConvention, the key constants are unchanged in meaning: the newspring.ai.kindconstant isSPRING_AI_KINDon both low-cardinality enums, andToolCallLowCardinalityKeys.KINDis still the ACP tool kind. - Breaking: configuration properties move from
spring.acp.*tospring.ai.acp.harness.*, and environment variables fromSPRING_ACP_*toSPRING_AI_ACP_HARNESS_*. The old root is shared withacp-autoconfig(spring.acp.client.*,spring.acp.agent.*), and the new one follows thespring.ai.*convention of the Spring AI Community projects. Update your configuration: the old keys are no longer read, and nothing reports them, so aspring.acp.*.enabled=falsestops taking effect. Theacp:config import prefix, metric names and Java packages are unchanged. - Breaking: the API no longer returns or accepts
java.util.Optional. A value that may be absent is now a plain reference annotated with JSpecify's@Nullable, as Spring Framework 7 and Spring AI do, soclient.session(name),client.harnessInfo(),sessions().find(name),SessionConfiguration.applied(option),AcpHarnessSession.principal(),RuntimeOptions.text(path)and thefind*accessors ofProviderSpecandAcpHarnessSettingsreturn null for "none". The SPI follows:AcpHarnessRuntime.authMethod,toolNameOf,planOf,logDirectoryandnoticeOf,AcpHarnessRuntimeProvider.forId,McpCredentialsProvider.credentialsFor,McpCredentials.upstream,ModelCatalog.modelsOf,SessionPrincipalResolver.current,PermissionPrompt.askand bothPermissionPolicy.decidemethods return null instead of empty, anddecideandPermissionQuestiontake a nullable tool name.AcpHarnessOptionsdrops itsfind*methods, since its record accessors already say the same thing;StoredSessionlikewise dropsfindCwd,findTitleandfindUpdatedAt, andProviderSpecdropsfindBaseUrl.AcpHarnessEvent.UsageUpdated.contextFraction()returns aDoubleor null instead of anOptionalDouble. Replace.isPresent()with a null check and.orElse(x)with a conditional to migrate; the compiler finds every call site, since the types changed. - Every package of a published module is
@NullMarked, so a reference is non-null unless it says@Nullable, and the build enforces that with NullAway: main code that passes, returns or stores a possibly-null value where a non-null one is declared no longer compiles. Nothing changes for a caller, except that a@Nullableon this API is now something the compiler checked rather than a comment. A few signatures gained a@Nullablethe behaviour already implied, such asAcpHarnessExecutor.getVersion(),SkillFetcher.Fetched.revision(),McpServerSpec.Http.url(),AcpHarnessLaunchSpec.WebSocket.process()and the optional components ofSkillSpec.GitandAcpController's request and response records;SecretRedactor.redactno longer accepts null. A few behaviours changed where a null used to flow on:SessionConfiguration.of(option)returns aNOT_REQUESTEDresolution instead of null for an option the configuration never mentioned; a registry entry with no id, package, archive or command is skipped when the catalogue is parsed instead of failing at launch, and a registry agent with no distribution fails at launch with a message; a stored MCP OAuth client registration missing a required field fails with a message naming it; and anauth: oauthserver declared without aurlis refused when the credentials provider is built rather than when the first user's session opens. For a contributor,.mvn/jvm.confignow opens the javac internals Error Prone needs, andspring-boot-configuration-processoris named on the compiler's processor path. - Breaking: the core library is now the artifact
spring-ai-acp-harness-client, dropping the-coresuffix, the Spring AIChatModeladapter isspring-ai-acp-harness-client-chatmodelinstead of-ai, and the parent POM is published asspring-ai-acp-harness-client-parent, so each name says what it is. Applications use a starter and rarely name the core artifact; one that names either changes the artifact id. Java packages are unchanged. The modules are also grouped intoruntimes/,auto-configurations/,starters/andintegrations/folders in the repository, which changes no artifact id. - Releases are published to Maven Central, and snapshots to the Maven Central snapshot repository. Versions up to 0.5.0 remain in the Artifact Registry repository.
- The reference documentation moved from a single
docs/user-guide.htmlto a versioned MkDocs site, published to GitHub Pages fromdocs/. - Breaking:
AcpController's handlers returnMonoorFlux, all buthealth, so that no request blocks a Netty event-loop thread. Requests over HTTP are unaffected; an application that calls the controller's methods directly subscribes to what they return. - Breaking:
GET {path}/sessionson the HTTP endpoint returns[{"name", "sessionId", "title", "updatedAt"}], naming each of the caller's open sessions the way the caller did, so it can be continued or closed, instead of the agent's stored sessions. It no longer needs the agent to implementsession/list(no more 501), asks the agent nothing when the caller has nothing open, and lists every open session, including one the agent has not saved yet or that runs in another process of the pool. - The HTTP endpoint answers a request for a named session that is still running an earlier turn with a 409 at once, instead of waiting for it for up to the request's timeout and then returning a 502. Waiting held a thread on the scheduler that also answers agents' permission and workspace requests, so one caller sending overlapping turns to one session could stall every other user's agent.
Fixed¶
- A new session that failed while being configured, such as an unsupported model with
on-unsupported: fail, stayed open on the agent untilsession-ttl, with its MCP route still live. In a pool these sessions used up capacity, so later turns were refused with a "holding its maximum" error instead of reporting the real failure. A session that was already open is kept, so a bad per-turn override does not end a conversation. The same applied to a session attached withsessions().loadorresume, which stayed loaded on the agent; it is now closed there too. - A
session/newthe agent answered after the caller stopped waiting, as when a cancelled turn interrupts its thread, named a session nothing could close; it is now closed when the answer arrives. Asession/newnot answered withintimeoutnow fails with anAcpHarnessClientExceptioninstead of anIllegalStateException; an answer after that is dropped by the ACP SDK, so a session created that late still stays on the agent. - Subscribing twice to one throwaway
AcpHarnessStreamnow runs two sessions. They used to share one, so the first to finish could close it under the other. - A closed
AcpHarnessClientPoolwould start new agents on request, and a connection still being built when the pool closed was never closed. A closed pool now refuses work with anAcpHarnessClientException, and a connection that finishes building after the close is closed at once. Closing does not interrupt or wait for a connect in progress. - The HTTP endpoint blocked on the Netty event loop, so on a real server
/prompt,/streamand listing sessions failed with a 500, and closing a session never reached the agent. It now runs turns without holding a thread. - The HTTP endpoint gave sessions no owner, so any caller could continue or close another user's
session by naming it, and
GET {path}/sessionslisted everyone's. Session names are now scoped to the caller, and listing returns only the caller's open sessions. Names are limited to 109 characters, and the endpoint can no longer reach a session the application opened under the same name. On OpenCode,spring.ai.acp.harness.pool.max-processesnow limits how many endpoint users can hold sessions. /streamended normally when a turn failed. It now returns a 502, or aborts the connection if text was already sent.- An invalid session name on the HTTP endpoint returned a 500, after the pool had assigned the name to a process. It is now a 400 before anything else happens.
- A full pool is reported by the new
AcpHarnessCapacityException, which the HTTP endpoint returns as a 503 instead of a 502 or 500.
[0.5.0] - 2026-10-20¶
Changed¶
- Renamed from Spring AI ACP to Spring AI ACP Harness Client, ahead of moving to the Spring AI
Community organisation. Every artifact moves from
spring-ai-acp-*tospring-ai-acp-harness-client-*(for examplespring-ai-acp-boot-starter-goosebecomesspring-ai-acp-harness-client-boot-starter-goose) under the sameorg.springaicommunitygroupId; update your dependencies. Java packages (org.springaicommunity.acp.*) and configuration properties (spring.acp.*) are unchanged. - The live-test switches follow the new name:
-Dspring-ai-acp-harness-client.test.live=true,-Dspring-ai-acp-harness-client.test.<runtimeId>.modeland-Dspring-ai-acp-harness-client.test.registry.live=true. - The agent now sees the client as
spring-ai-acp-harness-clientin ACPinitialize, as does an MCP authorization server registering it whenspring.application.nameis unset. State kept on disk keeps the former name so an upgrade finds it: the registry cache (~/.spring-ai-acp/agents), the.spring-ai-acp-skillmarker, and the MCP sign-in file of an application without aspring.application.name. Such an application's console does take the new name, for its title and for its input history, which therefore starts afresh. - Every type named for the agent now says
AcpAgent, so that it no longer reads as one of Spring AI's own agent types beside it:AgentClientbecomesAcpAgentClient(and its nestedAgentResponseandAgentStream,AcpAgentResponseandAcpAgentStream),AgentSession/AgentSessionsbecomeAcpAgentSession/AcpAgentSessions,AgentEventbecomesAcpAgentEvent, and likewiseAcpAgentOptions,AcpAgentSettings,AcpAgentInfo,AcpAgentNotice,AcpAgentExecutor,AcpAgentClientException,UnsupportedAcpAgentOperationExceptionandUnsupportedAcpAgentOptionException. The adapter SPI follows (AcpAgentRuntime,AcpAgentRuntimeProvider,AcpAgentLaunchSpec,AcpAgentEnvironment), as does the test kit (AcpAgentRuntimeContract,AcpAgentProbe,ScriptedAcpAgent,LiveAcpAgents) and every internal type. The rule is mechanical, so replacing each\bAgent…\btype name with itsAcpAgent…spelling migrates an application; packages, bean names, configuration properties and the standalone configuration file are unchanged. Two exceptions: the transport for a supervised served agent,WebSocketAgentTransport, becomesSupervisedWebSocketTransport, since the SDK's*AcpAgentTransportnames are for the agent's side of the connection; andAcpAgentSessionshares its simple name with the SDK's agent-sidecom.agentclientprotocol.sdk.agent.AcpAgentSession, a class an application using this library has no reason to import. - A standalone configuration file must now be imported with the
acp:prefix:spring.config.import: optional:acp:agents.yaml. A bareoptional:agents.yamlis no longer claimed by name, because Boot asks this library's resolver before its own, and claiming the name took the file from any other library that reads anagents.yaml. Without the prefix Boot now loads the file as ordinary configuration, so its keys missspring.acpand are silently ignored: addacp:to the import.
Fixed¶
spring-ai-acp-harness-client-mcp-oauth: a sign-in that had lapsed — its refresh token no longer honoured — was never cleared, so the user was never asked to sign in again and every session after failed to load the server (with goose, "discover and legacy initialize both failed"). The authorization server behind the Tanzu MCP gateway refuses such a refresh with a 401, which Spring Security reads asinvalid_token_responseinstead ofinvalid_grant; an OAuth error in a 401 from the token endpoint is now read as what it says.spring-ai-acp-harness-client-mcp-oauth: an expired token is now refreshed as the session opens, so a refresh the server refuses leads straight to signing in again (a browser inmode: local, a redirect inmode: web) instead of failing the session's first request. A refresh that fails without a refusal, such as an authorization server that is down, still keeps the token for next time.
[0.4.0] - 2026-10-04¶
Changed¶
- ACP Java SDK 0.18.0, with its Jackson 3 JSON module (
acp-json-jackson3). The SDK no longer carries a JSON implementation inacp-core, and this library now uses Jackson 3 throughout, as Spring Boot 4 and Spring AI 2 do. Jackson 2 no longer arrives through Spring AI ACP: an application that usedcom.fasterxml.jackson.databindwithout declaring it must now add it itself. Jackson's annotations (com.fasterxml.jackson.annotation) are unchanged. - Spring Boot 4.1.1, which brings Jackson 3.1.5, the version the SDK is built against.
Fixed¶
- With
goose serve, an answer to the agent (a permission decision, a file read, a terminal result) could be dropped when it was sent at the same moment as a new request, leaving that turn waiting until it timed out. The SDK fixed the same race in its own WebSocket transport in 0.18.0. - With
goose serve, a request in flight when the connection drops now fails at once instead of waiting out the request timeout, so the turn ends with an error straight away.
[0.3.1] - 2026-10-01¶
Added¶
auth: providedfor an MCP server whose credentials come from the application's ownMcpCredentialsProvider. Such a server may omiturl: the provider supplies the upstream withMcpCredentials.routed(url, credentials)when the session opens, as a credential broker that issues a token together with its endpoint does. A URL-less server the provider does not route is left out of that session instead of refusing it.- A Tanzu AI Models (GenAI) service bound on Cloud Foundry becomes the provider with no
configuration. Its OpenAI-compatible endpoint is used as
spring.acp.provider.base-urlandapi-key, and when nospring.acp.modelis set, the firstTOOLS-capable model in its catalog is chosen. - An explicitly configured provider wins.
spring.acp.tanzu-ai.enabledandservice-namecontrol it.CODEX_ACP_CLI_PATHnames a local codex-acp for the Codex runtime to launch in place ofnpx, asGOOSE_CLI_PATHandOPENCODE_CLI_PATHdo for theirs. The Codex supply buildpack exports it.spring.acp.runtimes.codex.commandstill wins, and so does an explicitpackage, which asks for npx.- Goose's
todolist arrives asAgentEvent.PlanUpdated. Goose keeps its task list in a tool and sends no ACPplanupdate, so eachtodo__todo_writecall is read as the agent's plan instead of being reported as a tool call.AgentRuntime.planOfis the hook any adapter can use for an agent that does the same. - Helpers for runtime adapters, so their shared rules live in core:
AgentEnvironment.layeredbuilds a launch environment in the order every adapter uses. Agent defaults come first, then provider credentials, then the tier-3envblock, which wins.Executables.fromEnvironmentreads an executable path from an environment variable such asGOOSE_CLI_PATH, and checks it at startup.AgentSettings.findModel()returns the requested model, or empty when it is blank.
Changed¶
AcpPropertiesand its nested types are records, bound through their constructors. Code that reads them uses record accessors (properties.model(),properties.pool().sessionTtl()) in place of getters. Property names, defaults and metadata are unchanged, except thatspring.acp.protocol.max-versionnow lists its default (1) in the metadata. A test that declarednew AcpProperties()as a bean registers it with@EnableConfigurationProperties(AcpProperties.class)instead.
Fixed¶
- OpenCode sessions no longer call MCP servers as another user, or through a closed session's
route. OpenCode keeps one MCP client per server name for its whole process, replaced by each
session/new, so every session used the route of the one opened last. A runtime now declares this withAgentRuntime.mcpScope()(PROCESSfor OpenCode). The pool then keeps each such process to one principal at a time, restarting an idle process before it serves someone else, andMcpAccessgives the principal one route shared by all of their sessions, held until the last closes.spring.acp.pool.max-processescaps how many users hold OpenCode sessions at once, and each costs a whole OpenCode process (about 650 MB); see "Processes, users and memory" in the user guide.
[0.3.0] - 2026-09-24¶
Initial release as Spring AI ACP, under the org.springaicommunity groupId and the Apache
License 2.0.
Agents and runtimes¶
AgentClient, a fluent client that drives any Agent Client Protocol v1 coding agent and streams each turn asAgentEvents, with exactly one terminal event per turn.- Compiled runtime adapters for Goose, Codex (
codex-acp) and OpenCode, selected byspring.acp.runtime, andspring-ai-acp-boot-starter-registry, which launches any agent the ACP registry publishes from a cached catalogue, with SHA-256-verified downloads. - A supervised
goose serveover WebSocket, as an alternative to stdio. - ACP version negotiation; v2 is behind a feature flag and refused.
Configuration¶
- Three configuration tiers: portable (
spring.acp.*), negotiated (model,modeandprovider, resolved against the options the agent advertises, withon-unsupported= fail/warn/ignore), and runtime-specific (spring.acp.runtimes.<id>.*). - Bring-your-own OpenAI-compatible endpoints through
spring.acp.provider.base-url, with the endpoint's own model listing deciding which models are valid. - A standalone
agents.yaml, loaded as Spring config data.
Sessions, MCP and skills¶
- Capability-gated session list, load, resume and delete, with connection pooling and idle session sweeping.
- MCP servers declared per session, with credentials kept out of the agent: HTTP servers that need them are reached through a loopback proxy, one unguessable route per session.
spring-ai-acp-mcp-oauth: per-user MCP-spec OAuth, redirecting through Spring Security in a web application or signing in through a loopback browser flow in a terminal one.- Detection of MCP servers that goose fails to load, which ACP never reports on the wire,
surfaced as
AgentClient.notices()and acted on throughspring.acp.mcp.on-server-failure. - Skills installed from Git (optionally pinned to a commit) into the workspace.
Permissions and the workspace¶
- Permission policies (
deny,allowlist,auto-approveandask), withaskanswered by a person through aPermissionPrompt. - A workspace jail that confines the agent's filesystem and terminal requests by real path.
Integrations¶
AcpChatModel, which publishes the agent as a Spring AIChatModel.- An opt-in HTTP endpoint, and the
AgentExecutorfacade. spring-ai-acp-console, a terminal chat over the application's agent for prototyping it.- A Micrometer observation per turn and per tool call, and
AgentEvent.UsageUpdated.
Testing¶
spring-ai-acp-test:AgentRuntimeContract, the cross-adapter contract test;AgentProbe, which gates live suites on a real session; andScriptedAgent, a fake agent speaking raw JSON-RPC for wire-format tests. Live-agent suites are opt-in with-Dspring-ai-acp.test.live=true.