Skip to content

feat(acp): add reusable ACP provider controls#8314

Open
baxen wants to merge 3 commits intomainfrom
baxen-acpplus
Open

feat(acp): add reusable ACP provider controls#8314
baxen wants to merge 3 commits intomainfrom
baxen-acpplus

Conversation

@baxen
Copy link
Copy Markdown
Collaborator

@baxen baxen commented Apr 4, 2026

Motivation

This moves more ACP session and provider control into goose serve so clients like the new desktop app can behave as thin transports instead of owning backend lifecycle themselves. The main change is that Goose ACP now exposes the provider operations a client needs over ACP custom requests, including provider selection at session creation, provider switching, reset-to-default behavior, and provider discovery.

Alongside the new features, this significantly simplifies the ACP provider internals by removing abstractions that were unused within the codebase (CLI, desktop, TUI).

New features

  • ACP provider/config custom requests:
    • _goose/session/provider/update — switch provider (and optionally model/context limit) for a session
    • _goose/providers/list — discover available providers
    • _goose/config/read, _goose/config/upsert, _goose/config/remove — CRUD for goose config values
  • Provider as a session config option — provider selection now appears alongside mode and model in config_options, so ACP clients can switch providers via the same session/setConfigOption flow
  • _meta.provider on new_session — clients can choose a provider up front, avoiding a double-create (session + immediate provider switch)
  • Default-provider sentinel ("goose") — resetting to the default provider reads from config and clears any session-level model override
  • amp-acp and pi-acp providers — new ACP-backed providers following the same pattern as claude-acp/codex-acp/copilot-acp, registered as non-visible
  • manages_own_context() for ACP providers — skips goose-side context compaction and tool-pair summarization, matching the existing pattern for claude_code and gemini_cli

Simplifications

PermissionMapping removed

The old code let each provider definition (claude-acp, codex-acp, copilot-acp) specify preferred option IDs for permission responses (e.g. allow_option_id: Some("allow")). The new code matches by PermissionOptionKind (AllowOnce, AllowAlways, etc.) directly. This is correct because the ACP protocol defines option kinds precisely — matching by kind is the canonical approach. The preferred-ID override was never actually needed: copilot already used PermissionMapping::default() (kind-based fallback), and the IDs specified by claude/codex just happened to match their kinds anyway.

Callers updated: claude_acp.rs, codex_acp.rs, copilot_acp.rs all had their permission_mapping field removed. No other code referenced PermissionMapping.

Multi-session tracking removed (goose_to_acp_id, acp_to_goose_id, session_model)

The old code maintained bidirectional maps between goose session IDs and ACP session IDs, supporting multiple ACP sessions per provider instance. In practice, goose creates one provider instance per session — the multi-session machinery was unused complexity. The new code creates a single ACP session eagerly during connect() and stores it directly as an AcpSession struct.

Removed with it: ensure_session, has_session, resolve_acp_session_id, map_sessions_to_goose_ids, and all the goose_to_acp_id/acp_to_goose_id/session_model locking.

rejected_tool_calls made local to stream()

Was an Arc<TokioMutex<HashSet<String>>> shared across the provider. Since rejected tool call tracking is only needed within a single stream() call (tool calls don't span prompts), this is now a plain HashSet local to the stream closure. Simpler and avoids unnecessary async mutex overhead.

init_session / OnceCell removed

The old code lazily created a throwaway session just to discover the current model name (for ACP_CURRENT_MODEL resolution), then closed it if the agent supported close. The new code resolves the model from the eagerly-created session — no throwaway needed.

send_set_model, send_untyped, delete_session, list_sessions, close_session_by_acp_id removed

These were public methods on AcpProvider with no callers outside the struct itself (and its tests). send_set_model was used for mid-stream model forwarding which is now unnecessary with single-session semantics. send_untyped was only used by delete_session. list_sessions was only used by tests. close_session_by_acp_id was only used by init_session. The corresponding ClientRequest variants (SetModel, Untyped, CloseSession, ListSessions) are also removed.

auth_methods field removed

Was stored on the provider from the init response but never read by any caller in the codebase.

update_mode simplified

The old code had a "pre-initialization" guard that skipped the ACP call when no sessions existed yet (storing mode only in the shared mutex). With eager session creation, this guard is unnecessary. Also, mode_mapping[&mode] (which would panic on missing key) is replaced with .get(&mode).unwrap_or_else(...) — strictly safer.

Other changes

  • AcpProviderFactory type replaces ProviderConstructor in goose-acp, adding a provider name parameter to support runtime provider switching
  • ProviderEntry::metadata() accessor added
  • SessionUpdateBuilder::clear_provider_name / clear_model_config added for the reset-to-default flow
  • EnabledExtensionsState used in goose-acp server to pass session extensions when creating providers
  • ClientCapabilities sent during ACP initialize
  • Tests updated to cover provider discovery, config CRUD, provider switching/reset, and the simplified permission mapping

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 31544799b9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +209 to +212
export type UpsertConfigRequest = {
key: string;
isSecret: boolean;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add missing value field to UpsertConfigRequest type

The generated TypeScript request type for _goose/config/upsert omits the required value property, even though the server-side request schema requires it. As generated, strongly typed callers cannot construct a valid request without type assertions, and a caller following this type literally will send { key, isSecret } and get a runtime invalid-params error from the backend.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator

@angiejones angiejones left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

approving doc updates

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4a8f8b725d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d0b248a919

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +738 to +747
let notify = {
let mut notifiers = self.session_config_notifiers.lock().await;
notifiers
.entry(acp_session_id.clone())
.or_insert_with(|| Arc::new(Notify::new()))
.clone()
};

tokio::time::timeout(DEFERRED_INITIAL_CONFIG_TIMEOUT, notify.notified())
.await
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Re-check deferred config cache before waiting on notifier

There is a race in response_with_initial_config: if a config update arrives after the initial cache check but before this waiter registers/blocks, handle_deferred_config_update stores the update without notifying (no notifier existed yet), and this path can then wait until timeout despite data already being available. In that case model discovery fails with a timeout, and deferred config delivery can be dropped for ACP sessions.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0fcaef48ff

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Signed-off-by: Bradley Axen <baxen@squareup.com>
@block block deleted a comment from chatgpt-codex-connector bot Apr 4, 2026
@block block deleted a comment from chatgpt-codex-connector bot Apr 4, 2026
@block block deleted a comment from chatgpt-codex-connector bot Apr 4, 2026
@block block deleted a comment from chatgpt-codex-connector bot Apr 4, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 08d6b6ec61

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

.insert(session_id.to_string(), new_model.clone());
}
}
let session_id = self.acp_session_id();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Route ACP calls using the caller session ID

This change hard-wires every prompt to a single ACP session (self.acp_session_id()), ignoring the session_id provided by Goose. In this codebase, provider instances are reused across multiple Goose session IDs (for example via cloned Arc<dyn Provider> in subagent/orchestrator flows), so this causes cross-session context bleed: prompts and config updates from different Goose sessions end up in the same remote ACP conversation. Before this commit the provider kept a Goose↔ACP session map; removing that mapping makes multi-session behavior incorrect for ACP-backed providers.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there was exactly one user - orchestrator platform extension that is still hidden - that does this and i updated it. this is not correct otherwise

…de warnings

- Orchestrator now calls providers::create() for child agents instead of
  sharing the parent's Arc<dyn Provider>, matching the summon/delegate
  pattern and avoiding shared session state with ACP providers.
- Suppress unused variable warnings for telemetry-gated code in agent.rs
  and scheduler.rs.
- Add #[allow(dead_code)] on shared test modules in goose-acp tests,
  since each test binary only uses a subset of the common functions.

Signed-off-by: Bradley Axen <baxen@squareup.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 78aeee1d16

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


let model_state = build_model_state(&*provider).await?;
let mode_state = build_mode_state(self.goose_mode)?;
let provider_selection = session_provider_selection(&goose_session).to_string();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recompute provider option from persisted session before responding

on_new_session builds config_options from the pre-init goose_session object, but init_provider has already called agent.update_provider(...), which persists the resolved concrete provider/model into session storage. In the common case where _meta.provider is not set, goose_session.provider_name is still None, so the response advertises provider "goose"; later build_config_update/load_session reads the stored session and reports the concrete provider (for example openai), so the provider config option can flip without any user change.

Useful? React with 👍 / 👎.

let current_provider = agent.provider().await.map_err(|e| {
sacp::Error::internal_error().data(format!("Failed to get provider: {}", e))
})?;
let provider_name = current_provider.get_name().to_string();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve default-provider sentinel when applying model changes

on_set_model always derives provider_name from the active concrete provider and then calls agent.update_provider(...), which persists that concrete name. If a session was intentionally set to the default-provider sentinel ("goose") via update_provider, changing only the model through session/setConfigOption or session/setModel silently rewrites the session away from the sentinel, so future restores no longer follow the global default-provider behavior.

Useful? React with 👍 / 👎.

Signed-off-by: Bradley Axen <baxen@squareup.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b9fcac6a59

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1505 to +1506
} else if is_changing_provider {
ACP_CURRENT_MODEL.to_string()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use provider default model when provider changes

In update_provider, omitting model while switching to a different provider always sets default_model to ACP_CURRENT_MODEL ("current"). That sentinel is only resolved by ACP-backed providers in AcpProvider::connect; non-ACP providers keep model_name="current", so later prompts can be sent with an invalid model id and fail at runtime. This branch should choose a concrete model for the target provider (for example, its registry default) instead of forcing current for all provider changes.

Useful? React with 👍 / 👎.

Comment on lines +978 to +982
if let Some(ref provider_name) = requested_provider {
self.session_manager
.update(&goose_session.id)
.provider_name(provider_name)
.apply()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate requested provider before persisting new session

on_new_session persists _meta.provider directly into the newly created session without checking that the provider exists. If a client sends an unknown provider id, init_provider fails afterward and the request errors, but the session row created earlier remains stored, producing orphan ACP sessions that can accumulate from repeated bad requests. Validate the provider before writing it (or clean up the session on init failure).

Useful? React with 👍 / 👎.

@michaelneale
Copy link
Copy Markdown
Collaborator

I think this makes sense direction wise from what I know. The main acp consume was the TUI, will that need to be updated (I may have missed it in here) cc @alexhancock ?

@baxen looks like syntax error in docs, grok live test was a server outage it looks like.

@baxen
Copy link
Copy Markdown
Collaborator Author

baxen commented Apr 5, 2026

I think this makes sense direction wise from what I know. The main acp consume was the TUI, will that need to be updated (I may have missed it in here) cc @alexhancock ?

@baxen looks like syntax error in docs, grok live test was a server outage it looks like.

this is backwards compatible for the TUI!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants