| 1 | import * as ScriptProgress from "@t3tools/provider-core/server/ScriptProgress"; |
| 2 | import { |
| 3 | normalizeDevinSessionUpdate, |
| 4 | normalizeDevinToolCall, |
| 5 | extractDevinSubagentUpdate, |
| 6 | } from "./devinAcp.ts"; |
| 7 | import { |
| 8 | copilotClientCapabilitiesMeta, |
| 9 | extractCopilotSubagentUpdate, |
| 10 | makeCopilotSubagentRouting, |
| 11 | } from "./copilotAcp.ts"; |
| 12 | import { defaultInstanceIdForDriver, ProviderDriverKind } from "@t3tools/contracts"; |
| 13 | import { readCustomModelEntries } from "@t3tools/shared/model"; |
| 14 | import { AcpRegistrySettings } from "../settings.ts"; |
| 15 | import * as HostProcess from "@t3tools/shared/HostProcess"; |
| 16 | import { resolveSelfInvocation, type SelfInvocation } from "@t3tools/shared/nodeRuntime"; |
| 17 | import * as Crypto from "effect/Crypto"; |
| 18 | import * as Effect from "effect/Effect"; |
| 19 | import * as FileSystem from "effect/FileSystem"; |
| 20 | import * as Path from "effect/Path"; |
| 21 | import * as Layer from "effect/Layer"; |
| 22 | import * as Option from "effect/Option"; |
| 23 | import * as Schema from "effect/Schema"; |
| 24 | import type * as Scope from "effect/Scope"; |
| 25 | import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; |
| 26 | import * as EffectAcpErrors from "effect-acp/errors"; |
| 27 | |
| 28 | import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions"; |
| 29 | import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost"; |
| 30 | import { |
| 31 | normalizeAcpRegistryCommands, |
| 32 | normalizeAcpRegistryLiveConfiguration, |
| 33 | normalizeAcpRegistryWebUrl, |
| 34 | } from "./probe.ts"; |
| 35 | import * as AcpRegistrySupport from "./AcpRegistrySupport.ts"; |
| 36 | import * as AcpRegistryRuntimeCoordinator from "./AcpRegistryRuntimeCoordinator.ts"; |
| 37 | import * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime"; |
| 38 | import { makeAcpNativeLoggerFactory } from "@t3tools/provider-acp/server/nativeLogging"; |
| 39 | import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers"; |
| 40 | import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment"; |
| 41 | import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator"; |
| 42 | import { makeProviderFailure } from "@t3tools/provider-core/server/failure"; |
| 43 | import { |
| 44 | ProviderAdapterDriverCreateError, |
| 45 | type ProviderAdapterDriver, |
| 46 | type ProviderAdapterDriverCreateInput, |
| 47 | } from "@t3tools/provider-core/server/adapterDriver"; |
| 48 | import { |
| 49 | AcpProviderCapabilitiesV2, |
| 50 | makeAcpAdapterV2, |
| 51 | type AcpAdapterV2ExtensionContext, |
| 52 | type AcpAdapterV2Flavor, |
| 53 | type AcpAdapterV2RuntimeInput, |
| 54 | } from "@t3tools/provider-acp/server/adapter"; |
| 55 | |
| 56 | export const ACP_REGISTRY_PROVIDER = ProviderDriverKind.make("acpRegistry"); |
| 57 | export const ACP_REGISTRY_DEFAULT_INSTANCE_ID = defaultInstanceIdForDriver(ACP_REGISTRY_PROVIDER); |
| 58 | |
| 59 | const DEFAULT_ACP_REGISTRY_SETTINGS = Schema.decodeSync(AcpRegistrySettings)({}); |
| 60 | const isAcpRequestError = Schema.is(EffectAcpErrors.AcpRequestError); |
| 61 | |
| 62 | export 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). |
| 90 | const MISTRAL_VIBE_RATE_LIMITED = -31001; |
| 91 | |
| 92 | const 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. */ |
| 99 | export 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 | |
| 121 | export 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 | |
| 138 | function 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 | |
| 187 | export 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 | |
| 281 | export 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 | |
| 293 | export 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 | }; |