1import * as ScriptProgress from "@t3tools/provider-core/server/ScriptProgress";
2import {
3 normalizeDevinSessionUpdate,
4 normalizeDevinToolCall,
5 extractDevinSubagentUpdate,
6} from "./devinAcp.ts";
7import {
8 copilotClientCapabilitiesMeta,
9 extractCopilotSubagentUpdate,
10 makeCopilotSubagentRouting,
11} from "./copilotAcp.ts";
12import { defaultInstanceIdForDriver, ProviderDriverKind } from "@t3tools/contracts";
13import { readCustomModelEntries } from "@t3tools/shared/model";
14import { AcpRegistrySettings } from "../settings.ts";
15import * as HostProcess from "@t3tools/shared/HostProcess";
16import { resolveSelfInvocation, type SelfInvocation } from "@t3tools/shared/nodeRuntime";
17import * as Crypto from "effect/Crypto";
18import * as Effect from "effect/Effect";
19import * as FileSystem from "effect/FileSystem";
20import * as Path from "effect/Path";
21import * as Layer from "effect/Layer";
22import * as Option from "effect/Option";
23import * as Schema from "effect/Schema";
24import type * as Scope from "effect/Scope";
25import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
26import * as EffectAcpErrors from "effect-acp/errors";
27
28import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
29import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
30import {
31 normalizeAcpRegistryCommands,
32 normalizeAcpRegistryLiveConfiguration,
33 normalizeAcpRegistryWebUrl,
34} from "./probe.ts";
35import * as AcpRegistrySupport from "./AcpRegistrySupport.ts";
36import * as AcpRegistryRuntimeCoordinator from "./AcpRegistryRuntimeCoordinator.ts";
37import * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime";
38import { makeAcpNativeLoggerFactory } from "@t3tools/provider-acp/server/nativeLogging";
39import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers";
40import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment";
41import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
42import { makeProviderFailure } from "@t3tools/provider-core/server/failure";
43import {
44 ProviderAdapterDriverCreateError,
45 type ProviderAdapterDriver,
46 type ProviderAdapterDriverCreateInput,
47} from "@t3tools/provider-core/server/adapterDriver";
48import {
49 AcpProviderCapabilitiesV2,
50 makeAcpAdapterV2,
51 type AcpAdapterV2ExtensionContext,
52 type AcpAdapterV2Flavor,
53 type AcpAdapterV2RuntimeInput,
54} from "@t3tools/provider-acp/server/adapter";
55
56export const ACP_REGISTRY_PROVIDER = ProviderDriverKind.make("acpRegistry");
57export const ACP_REGISTRY_DEFAULT_INSTANCE_ID = defaultInstanceIdForDriver(ACP_REGISTRY_PROVIDER);
58
59const DEFAULT_ACP_REGISTRY_SETTINGS = Schema.decodeSync(AcpRegistrySettings)({});
60const isAcpRequestError = Schema.is(EffectAcpErrors.AcpRequestError);
61
62export interface AcpRegistryAdapterV2Options {
63 readonly instanceId: Parameters<typeof makeAcpAdapterV2>[0]["instanceId"];
64 readonly settings: AcpRegistrySettings;
65 readonly environment: NodeJS.ProcessEnv;
66 readonly selfInvocation: SelfInvocation;
67 readonly nativeLogging?: Parameters<typeof makeAcpAdapterV2>[0]["nativeLogging"];
68 readonly makeRuntime?: (
69 input: AcpAdapterV2RuntimeInput,
70 ) => Effect.Effect<
71 AcpSessionRuntime.AcpSessionRuntime["Service"],
72 EffectAcpErrors.AcpError,
73 ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | Scope.Scope
74 >;
75 readonly assertComplete?: Effect.Effect<void, EffectAcpErrors.AcpError>;
76}
77
78// ─── Per-agent exceptions ────────────────────────────────────────────────────
79// This adapter serves every ACP registry agent through the plain ACP spec.
80// Agent-specific behavior does not belong here: an agent that needs it gets a
81// dedicated driver (as Grok and Antigravity have). The few exceptions below
82// predate that rule and are small presentation hooks, not permission or tool
83// behavior. Copilot's subagent routing was approved later on the same terms.
84//
85// Agents changing this file: do NOT add another `agentId === "..."` branch,
86// agent table, or agent-specific hook without explicit approval from the
87// maintainer in the conversation. Propose a dedicated driver instead.
88
89// Mistral Vibe: its application error code for rate limits (not an ACP code).
90const MISTRAL_VIBE_RATE_LIMITED = -31001;
91
92const MistralVibeSessionRetrying = Schema.Struct({
93 sessionId: Schema.String,
94 category: Schema.Literals(["rate_limited", "server_error", "timed_out", "connection", "unknown"]),
95 detail: Schema.String,
96});
97
98/** Mistral Vibe (v2.25.5) reports SDK backoff through this ACP extension. */
99export function registerMistralVibeAcpExtensions(context: AcpAdapterV2ExtensionContext) {
100 return context.runtime.handleExtNotification(
101 "_session/retrying",
102 MistralVibeSessionRetrying,
103 (notice) =>
104 context.reportProviderRetry({
105 sessionId: notice.sessionId,
106 failure: makeProviderFailure({
107 message: notice.detail,
108 class:
109 notice.category === "rate_limited"
110 ? "usage_limit"
111 : notice.category === "unknown"
112 ? "provider_error"
113 : "transport_error",
114 retryable: true,
115 }),
116 }),
117 );
118}
119// ─── End per-agent exceptions (Devin's gates are marked in makeAcpRegistryAdapterV2) ───
120
121export function acpRegistryPromptFailure(agentId: string, cause: unknown) {
122 return makeProviderFailure({
123 cause,
124 ...(isAcpRequestError(cause)
125 ? {
126 message: cause.errorMessage,
127 code: String(cause.code),
128 class:
129 // Per-agent exception: see the note above registerMistralVibeAcpExtensions.
130 agentId === "mistral-vibe" && cause.code === MISTRAL_VIBE_RATE_LIMITED
131 ? ("usage_limit" as const)
132 : ("provider_error" as const),
133 }
134 : { class: "provider_error" as const }),
135 });
136}
137
138function makeAcpRegistryRuntime(
139 options: AcpRegistryAdapterV2Options,
140 catalog: AcpRegistrySupport.AcpRegistryCatalog["Service"],
141) {
142 return (
143 input: AcpAdapterV2RuntimeInput,
144 ): Effect.Effect<
145 AcpSessionRuntime.AcpSessionRuntime["Service"],
146 EffectAcpErrors.AcpError,
147 ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | Scope.Scope
148 > =>
149 Effect.gen(function* () {
150 const { processEnvironment, ...runtimeInput } = input;
151 const resolved = yield* catalog
152 .resolve(options.settings, input.cwd, options.environment)
153 .pipe(
154 Effect.mapError(
155 (cause) =>
156 new EffectAcpErrors.AcpSpawnError({
157 command: options.settings.agentId || ACP_REGISTRY_PROVIDER,
158 cause,
159 }),
160 ),
161 );
162 const context = yield* Layer.build(
163 AcpSessionRuntime.layer({
164 ...runtimeInput,
165 spawn:
166 processEnvironment === undefined
167 ? resolved.spawn
168 : {
169 ...resolved.spawn,
170 env: ScriptProgress.withScriptProgressEnvironment(
171 resolved.spawn.env ?? {},
172 processEnvironment,
173 ),
174 },
175 ...(options.settings.authMethodId ? { authMethodId: options.settings.authMethodId } : {}),
176 customModels: readCustomModelEntries(options.settings.customModels).map(
177 (entry) => entry.slug,
178 ),
179 }),
180 );
181 return yield* Effect.service(AcpSessionRuntime.AcpSessionRuntime).pipe(
182 Effect.provide(context),
183 );
184 });
185}
186
187export const makeAcpRegistryAdapterV2 = Effect.fn("makeAcpRegistryAdapterV2")(function* (
188 options: AcpRegistryAdapterV2Options,
189) {
190 const catalog = yield* AcpRegistrySupport.AcpRegistryCatalog;
191 const runtimeCoordinator = Option.getOrUndefined(
192 yield* Effect.serviceOption(AcpRegistryRuntimeCoordinator.AcpRegistryRuntimeCoordinator),
193 );
194 const registryAgentId = options.settings.source === "local" ? "" : options.settings.agentId;
195 const startupKey =
196 options.settings.source === "local" ? `local:${options.instanceId}` : registryAgentId;
197 const isDevin = registryAgentId === "devin";
198 // Local installs launch the `copilot` binary directly with no registry id.
199 const isCopilot =
200 registryAgentId === "github-copilot-cli" ||
201 (options.settings.source === "local" &&
202 /^copilot(\.exe|\.cmd)?$/i.test(options.settings.commandPath.split(/[\\/]/).pop() ?? ""));
203 const copilotRouting = isCopilot ? makeCopilotSubagentRouting() : undefined;
204 const flavor: AcpAdapterV2Flavor = {
205 driver: ACP_REGISTRY_PROVIDER,
206 capabilities: AcpProviderCapabilitiesV2,
207 promptFailure: (cause) => acpRegistryPromptFailure(registryAgentId, cause),
208 // Per-agent exceptions (Mistral Vibe, Devin, Copilot): see the note above
209 // registerMistralVibeAcpExtensions before adding any more.
210 ...(registryAgentId === "mistral-vibe"
211 ? { registerExtensions: registerMistralVibeAcpExtensions }
212 : {}),
213 ...(isDevin
214 ? {
215 clientCapabilitiesMeta: {
216 "cognition.ai/subagentSupport": true,
217 "cognition.ai/messageGrouping": true,
218 },
219 normalizeSessionUpdate: normalizeDevinSessionUpdate,
220 normalizeToolCall: normalizeDevinToolCall,
221 extractSubagentUpdate: extractDevinSubagentUpdate,
222 }
223 : {}),
224 ...(copilotRouting === undefined
225 ? {}
226 : {
227 clientCapabilitiesMeta: copilotClientCapabilitiesMeta,
228 registerExtensions: copilotRouting.registerExtensions,
229 normalizeSessionUpdate: copilotRouting.normalizeSessionUpdate,
230 extractSubagentUpdate: extractCopilotSubagentUpdate,
231 }),
232 makeRuntime: options.makeRuntime ?? makeAcpRegistryRuntime(options, catalog),
233 ...(runtimeCoordinator === undefined
234 ? {}
235 : {
236 onAvailableCommandsUpdate: (commands) =>
237 runtimeCoordinator.publishAvailableCommands(
238 options.instanceId,
239 normalizeAcpRegistryCommands(commands),
240 ),
241 onSessionConfigurationUpdate: (configOptions, modeState) =>
242 runtimeCoordinator.publishLiveConfiguration(
243 options.instanceId,
244 normalizeAcpRegistryLiveConfiguration(configOptions, modeState),
245 ),
246 onUrlElicitation: ({ elicitationId, url, message }) => {
247 const normalizedUrl = normalizeAcpRegistryWebUrl(url);
248 if (normalizedUrl === undefined || elicitationId.trim().length === 0) {
249 return Effect.succeed(false);
250 }
251 return runtimeCoordinator.requestUrlAuthentication(options.instanceId, {
252 elicitationId: elicitationId.trim().slice(0, 256),
253 url: normalizedUrl,
254 message: message.trim().slice(0, 1_024),
255 });
256 },
257 withRuntimeStartup: <A, E, R>(effect: Effect.Effect<A, E, R>) =>
258 runtimeCoordinator.withForegroundStartup(startupKey, effect),
259 }),
260 ...(options.assertComplete === undefined ? {} : { assertComplete: options.assertComplete }),
261 };
262 return yield* makeAcpAdapterV2({
263 instanceId: options.instanceId,
264 flavor,
265 selfInvocation: options.selfInvocation,
266 // Per-agent exception (see the note above registerMistralVibeAcpExtensions):
267 // Devin runs commands through client terminals and has no ask mode over
268 // ACP to fall back on. Every other registry agent runs its own.
269 ...(isDevin
270 ? {
271 clientTerminals: {
272 environment: options.environment,
273 shellCommands: true,
274 },
275 }
276 : {}),
277 ...(options.nativeLogging === undefined ? {} : { nativeLogging: options.nativeLogging }),
278 });
279});
280
281export type AcpRegistryAdapterV2DriverEnv =
282 | ChildProcessSpawner.ChildProcessSpawner
283 | Crypto.Crypto
284 | FileSystem.FileSystem
285 | AcpRegistrySupport.AcpRegistryCatalog
286 | IdAllocator.IdAllocatorV2
287 | Path.Path
288 | ScriptProgress.ScriptProgress
289 | McpProviderSessions.McpProviderSessions
290 | ProviderEventLoggers.ProviderEventLoggers
291 | ProviderHost.ProviderHost;
292
293export const AcpRegistryAdapterV2Driver: ProviderAdapterDriver<
294 AcpRegistrySettings,
295 AcpRegistryAdapterV2DriverEnv
296> = {
297 driverKind: ACP_REGISTRY_PROVIDER,
298 configSchema: AcpRegistrySettings,
299 defaultConfig: (): AcpRegistrySettings => DEFAULT_ACP_REGISTRY_SETTINGS,
300 create: Effect.fn("AcpRegistryAdapterV2Driver.create")(
301 function* (input: ProviderAdapterDriverCreateInput<AcpRegistrySettings>) {
302 const hostEnvironment = yield* HostProcess.Environment;
303 const selfInvocation = yield* resolveSelfInvocation();
304 const providerEventLoggers = yield* ProviderEventLoggers.ProviderEventLoggers;
305 const makeNativeLogger = yield* makeAcpNativeLoggerFactory();
306 return yield* makeAcpRegistryAdapterV2({
307 instanceId: input.instanceId,
308 settings: { ...input.config, enabled: input.enabled },
309 environment: yield* mergeProviderInstanceEnvironment(input.environment, hostEnvironment),
310 selfInvocation,
311 nativeLogging: (threadId) =>
312 makeNativeLogger({
313 nativeEventLogger: providerEventLoggers.native,
314 provider: ACP_REGISTRY_PROVIDER,
315 threadId,
316 }),
317 });
318 },
319 (effect, input) =>
320 effect.pipe(
321 Effect.mapError(
322 (cause) =>
323 new ProviderAdapterDriverCreateError({
324 driver: ACP_REGISTRY_PROVIDER,
325 instanceId: input.instanceId,
326 detail: "Failed to create ACP Registry adapter.",
327 cause,
328 }),
329 ),
330 ),
331 ),
332};