Skip to content

feat: GitHub Copilot provider via ACP - #7885

Draft
balazstasi wants to merge 10 commits into
pingdotgg:mainfrom
balazstasi:t3code/add-copilot-acp-support
Draft

feat: GitHub Copilot provider via ACP#7885
balazstasi wants to merge 10 commits into
pingdotgg:mainfrom
balazstasi:t3code/add-copilot-acp-support

Conversation

@balazstasi

@balazstasi balazstasi commented Aug 22, 2026

Copy link
Copy Markdown

Problem

T3 Code had no GitHub Copilot support. Copilot CLI now ships a native ACP server (copilot --acp, public preview), which means it can reuse the shared ACP runtime that Grok and Cursor already drive — no new transport needed.

How

Ten commits, bottom-up, each independently readable:

  1. contractsCopilotSettings (opt-in, like Grok/Cursor).
  2. shared ACP runtime — parse available_commands_update into an event; buffer session updates that land before startup settles instead of dropping them (Copilot advertises commands right after session/new); first-sighting tool calls with rawInput are no longer suppressed; empty authMethodId skips authenticate (Copilot auths outside ACP). All provider-neutral.
  3. mock agent — flags for commands / foreground subagent / background-agent flows.
  4. CopilotAcpSupport — spawn + runtime factory + model helpers.
  5. CopilotAdapter — turn loop (steering, approvals, cancel) plus two Copilot-specific behaviors:
    • task tool calls project onto task.started/progress/completed so subagents render on the Agents surface.
    • Background launches park the turn settlement: copilot-cli answers end_turn early and keeps streaming progress afterwards; the turn stays routable until a follow-up call reports the agent idle, then the parked turn.completed flushes. Stop cancels outright.
  6. CopilotProvider — version probe + one short ACP session collecting models and slash-command skills (skills map to ServerProviderSkill with /name as path).
  7. driver registration + text generation (runs on the session's current model).
  8. tests — mock-driven: prompt streaming, subagent projection (no double render), background flow holding the turn open, skills collection.
  9. clients — picker entry, settings metadata (preview badge), web/mobile icons.
  10. docs — internals/providers.md.

Verification

  • 4 new adapter tests + full mock suite green; grok/cursor/acp suites unaffected (99 passing).
  • Typecheck clean across server/web/mobile/contracts; server lint clean.
  • Live-checked against copilot-cli 1.0.80: skills populate from the wire; background subagent run streams its post-end_turn work into the original turn and completes when read_agent reports idle (ground truth taken from native event logs).

Known ceilings (marked ponytail: in source)

  • Idle detection prose-matches read_agent output until upstream exposes structured lifecycle events.
  • No resume across restarts (agent advertises loadSession: false); skills refresh at probe time only.

ox-alpha-free via OpenCode

Note

Add GitHub Copilot provider via ACP

  • Introduces a full copilot provider driver registered in builtInDrivers.ts, with settings schema (enabled, binaryPath, customModels) added to settings.ts and UI entries in web and mobile.
  • Implements the Copilot ACP adapter in CopilotAdapter.ts covering session lifecycle, turn orchestration with steering and cancellation, permission mediation, attachment handling, per-turn model selection, background subagent task projection, and event streaming via PubSub.
  • Adds provider health check and discovery in CopilotProvider.ts: runs copilot --version (4s timeout), then ACP discovery (25s timeout) to collect models and available commands as skills.
  • Adds Copilot-backed text generation in CopilotTextGeneration.ts for commit messages, PR content, branch names, and thread titles with streaming capture and JSON schema decoding.
  • Extends shared ACP runtime in AcpSessionRuntime.ts to buffer pre-start session updates, skip authenticate when authMethodId is empty, emit AvailableCommandsChanged events, and emit first-seen tool calls with non-empty rawInput.
  • Risk: rollbackThread in CopilotAdapter returns an unsupported-operation error; consumers expecting rollback will fail. shouldEmitToolCallUpdate now emits first-seen tool calls with non-empty rawInput, which may increase event volume for other ACP-based providers sharing this runtime.
📊 Macroscope summarized 714a699. 17 files reviewed, 3 issues evaluated, 0 issues filtered, 3 comments posted

🗂️ Filtered Issues

CopilotSettings mirrors the other opt-in CLI providers (enabled flag,
binaryPath, customModels) and wires into ServerSettings.providers and the
settings patch map so instances can be configured from Settings.
…tarting agents

Three capabilities the Copilot ACP binding needs, all provider-neutral:

- session/update available_commands_update now parses into an
  AvailableCommandsChanged event (skills arrive as slash commands).
- Session updates that land before startup settles are buffered and
  re-dispatched to the root session instead of dropped; Copilot
  advertises commands immediately after session/new. The load-replay
  idle gate keeps receiving touches while buffered.
- First-sighting tool calls that carry rawInput are no longer suppressed
  until detail arrives, so subagent launches announce themselves;
  input-less placeholders stay suppressed. An empty authMethodId skips
  the authenticate round-trip for agents that auth outside ACP.
…asks

Three opt-in flags for the shared mock agent: emit
available_commands_update after session/new, a foreground task tool
call, and the copilot-cli background-agent shape (launch, early
end_turn, then post-turn chunks plus an idle report).
Spawn input for `copilot --acp`, a runtime factory that skips ACP
authenticate (GitHub login/BYOK happen outside the protocol), and model
selection helpers mirroring the Grok binding.
Turn loop follows the Grok ACP adapter (prompt steering, approvals,
cancel) minus the xAI quirks. Two Copilot-specific behaviors:

- task tool calls (rawInput.agent_type) emit task.started/progress/
  completed with agentKind "agent" so they render on the Agents
  surface instead of opaque tool rows.
- Background launches (mode: "background") park the turn settlement:
  copilot-cli answers end_turn early and keeps streaming progress, so
  the turn stays routable until a follow-up call reports the agent idle;
  then task.completed flushes the parked turn.completed. Stop cancels
  outright; a new sendTurn closes leftovers as stopped.
…P probe

Status probe runs `copilot --version` then one short-lived ACP session
that collects the model list from session setup and skills from
available_commands_update. Skills map onto ServerProviderSkill with the
invocation (`/name`) as path since upstream exposes no skill files.
CopilotDriver wires adapter, snapshot maintenance and text generation;
text generation intentionally runs on the session's current model
instead of pinning one. Driver added to BUILT_IN_DRIVERS.
Mock-agent driven: basic prompt streaming, subagent tool calls emitting
task events (no double render), background flow holding the turn open
until idle, and skills collection from available_commands_update.
Picker entry, browser-safe driver metadata with a preview badge, GitHub
mark icons for web and mobile.
Built-in driver table gains copilot plus a note on command-based skills
and background-task turn holds.
@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 508c4c5a-d2db-451d-a993-301daf7ea060

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Aug 22, 2026
Comment on lines +559 to +561
if (terminalStatus) {
ctx.heldOpenTaskIds.delete(taskId);
yield* offerRuntimeEvent({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium Layers/CopilotAdapter.ts:559

Repeated terminal ToolCallUpdated notifications emit duplicate task.completed events for the same taskId, causing clients to apply completion handling more than once. emitSubagentTaskEvents deletes the live-task entry but never checks it before emitting; ignore terminal updates whose taskId is no longer live.

         if (terminalStatus) {
+          if (!ctx.heldOpenTaskIds.has(taskId)) {
+            return;
+          }
           ctx.heldOpenTaskIds.delete(taskId);
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CopilotAdapter.ts around lines 559-561:

Repeated terminal `ToolCallUpdated` notifications emit duplicate `task.completed` events for the same `taskId`, causing clients to apply completion handling more than once. `emitSubagentTaskEvents` deletes the live-task entry but never checks it before emitting; ignore terminal updates whose `taskId` is no longer live.

const rawInput = toolCall.data.rawInput;
if (isRecord(rawInput)) {
for (const key of ["agent_type", "agent", "agentName", "agent_name"] as const) {
const value = rawInput[key];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 High Layers/CopilotAdapter.ts:147

A non-empty rawInput.agent_type value other than "task" is classified as a subagent launch, so ordinary tool calls are removed from the tool timeline and added to heldOpenTaskIds; this can leave turn.completed parked without that tool's completion signal. Restrict the agent_type check to the documented value "task" while retaining the fallback heuristics for the other keys.

      for (const key of ["agent_type", "agent", "agentName", "agent_name"] as const) {
-      if (typeof value === "string" && value.trim().length > 0) {
+      if (
+        typeof value === "string" &&
+        value.trim().length > 0 &&
+        (key !== "agent_type" || value === "task")
+      ) {
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CopilotAdapter.ts around line 147:

A non-empty `rawInput.agent_type` value other than `"task"` is classified as a subagent launch, so ordinary tool calls are removed from the tool timeline and added to `heldOpenTaskIds`; this can leave `turn.completed` parked without that tool's completion signal. Restrict the `agent_type` check to the documented value `"task"` while retaining the fallback heuristics for the other keys.

startOnce.pipe(
Effect.tap((result) =>
Ref.set(startStateRef, { _tag: "Started", result }).pipe(
Effect.andThen(drainPreStartUpdates(result.sessionId)),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium acp/AcpSessionRuntime.ts:721

A live session/update arriving after Ref.set(startStateRef, { _tag: "Started", result }) but before drainPreStartUpdates completes is processed immediately, ahead of older buffered notifications. This reverses wire order and can emit or update order-sensitive state incorrectly; keep updates buffered until draining finishes and transition through one ordered path.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/acp/AcpSessionRuntime.ts around line 721:

A live `session/update` arriving after `Ref.set(startStateRef, { _tag: "Started", result })` but before `drainPreStartUpdates` completes is processed immediately, ahead of older buffered notifications. This reverses wire order and can emit or update order-sensitive state incorrectly; keep updates buffered until draining finishes and transition through one ordered path.

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Effect service conventions review of the new Copilot driver/adapter/provider modules. Structure, namespace imports, dependency acquisition, and layer/make usage all follow the existing ACP provider pattern. Three error-modeling findings: wrapper detail fields are built from cause.message (or a stringified defect) instead of stable structural attributes, which also feeds the caller-visible message getter of ProviderDriverError / ProviderAdapter*Error.

Posted via Macroscope — Effect Service Conventions

new ProviderAdapterProcessError({
provider: PROVIDER,
threadId: input.threadId,
detail: cause.message,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

detail only copies cause.message, which then becomes the ProviderAdapterProcessError message. Suggest a stable detail describing the stage; the ACP error stays available as cause.

Suggested change
detail: cause.message,
detail: "Failed to start the GitHub Copilot ACP session.",

Posted via Macroscope — Effect Service Conventions

new ProviderAdapterRequestError({
provider: PROVIDER,
method: "session/prompt",
detail: cause.message,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same here: detail duplicates cause.message and drops the resource context that is available at this boundary. Suggest naming the failing operation/resource and keeping the filesystem error as cause.

Suggested change
detail: cause.message,
detail: `Failed to read attachment '${attachment.id}'.`,

Posted via Macroscope — Effect Service Conventions

new ProviderDriverError({
driver: DRIVER_KIND,
instanceId,
detail: `Failed to build GitHub Copilot snapshot: ${cause.message ?? String(cause)}`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

detail interpolates cause.message / String(cause), and ProviderDriverError.message is derived from detail — so the wrapper message comes from the cause (and can carry arbitrary defect text). Consider keeping a stable, structural detail and letting cause carry the underlying failure.

Suggested change
detail: `Failed to build GitHub Copilot snapshot: ${cause.message ?? String(cause)}`,
detail: "Failed to build the GitHub Copilot provider snapshot.",

Posted via Macroscope — Effect Service Conventions

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

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant