| 1 | import { |
| 2 | ANTIGRAVITY_DEFAULT_MODEL, |
| 3 | ProviderDriverKind, |
| 4 | ProviderInstanceId, |
| 5 | type OrchestrationV2ProviderCapabilities, |
| 6 | type ProviderSetupError, |
| 7 | } from "@t3tools/contracts"; |
| 8 | import type { SelfInvocation } from "@t3tools/shared/nodeRuntime"; |
| 9 | import * as Crypto from "effect/Crypto"; |
| 10 | import * as Effect from "effect/Effect"; |
| 11 | import * as FileSystem from "effect/FileSystem"; |
| 12 | import * as Path from "effect/Path"; |
| 13 | import * as Exit from "effect/Exit"; |
| 14 | import * as Scope from "effect/Scope"; |
| 15 | import * as EffectAcpErrors from "effect-acp/errors"; |
| 16 | |
| 17 | import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost"; |
| 18 | import type { AntigravityAuth } from "../../provider/AntigravityAuth.ts"; |
| 19 | import type * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime"; |
| 20 | import { |
| 21 | antigravityPermissionMode, |
| 22 | applyAntigravityAcpModelSelection, |
| 23 | type AntigravityAcpRuntimeInput, |
| 24 | } from "../../provider/acp/AntigravityAcpSupport.ts"; |
| 25 | import { |
| 26 | readAntigravityClientTextFile, |
| 27 | writeAntigravityClientTextFile, |
| 28 | } from "../../provider/acp/AntigravityClientFiles.ts"; |
| 29 | import { |
| 30 | antigravityApprovalOptions, |
| 31 | antigravitySubagentOutput, |
| 32 | classifyAntigravitySubagentToolCall, |
| 33 | extractAntigravityUserInputQuestion, |
| 34 | makeAntigravityUserInputResponse, |
| 35 | normalizeAntigravityToolCall, |
| 36 | } from "../../provider/acp/AntigravityProtocol.ts"; |
| 37 | import { |
| 38 | AcpProviderCapabilitiesV2, |
| 39 | makeAcpAdapterV2, |
| 40 | type AcpAdapterV2Flavor, |
| 41 | type AcpAdapterV2RuntimeInput, |
| 42 | } from "@t3tools/provider-acp/server/adapter"; |
| 43 | |
| 44 | const ANTIGRAVITY_PROVIDER = ProviderDriverKind.make("antigravity"); |
| 45 | |
| 46 | const AntigravityProviderCapabilitiesV2 = { |
| 47 | ...AcpProviderCapabilitiesV2, |
| 48 | sessions: { |
| 49 | ...AcpProviderCapabilitiesV2.sessions, |
| 50 | supportsModelSwitchInSession: true, |
| 51 | supportsRuntimeModeSwitchInSession: true, |
| 52 | }, |
| 53 | tools: { |
| 54 | ...AcpProviderCapabilitiesV2.tools, |
| 55 | supportsMcpTools: true, |
| 56 | }, |
| 57 | subagents: { |
| 58 | ...AcpProviderCapabilitiesV2.subagents, |
| 59 | supportsSubagents: true, |
| 60 | }, |
| 61 | } satisfies OrchestrationV2ProviderCapabilities; |
| 62 | |
| 63 | export interface AntigravityAdapterV2Options { |
| 64 | readonly instanceId: ProviderInstanceId; |
| 65 | readonly selfInvocation: SelfInvocation; |
| 66 | /** Spawns the official agent with the instance's Google profile. */ |
| 67 | readonly makeRuntime: ( |
| 68 | input: Omit<AntigravityAcpRuntimeInput, "spawn" | "childProcessSpawner">, |
| 69 | ) => Effect.Effect< |
| 70 | AcpSessionRuntime.AcpSessionRuntime["Service"], |
| 71 | EffectAcpErrors.AcpError | ProviderSetupError, |
| 72 | Scope.Scope |
| 73 | >; |
| 74 | /** Serialises process ownership against sign-in and sign-out. */ |
| 75 | readonly withProcess: AntigravityAuth["withProcess"]; |
| 76 | /** Model to select for the provider default alias, from the manifest. */ |
| 77 | readonly defaultModel: Effect.Effect<string | undefined>; |
| 78 | readonly onSessionStarted?: ( |
| 79 | started: AcpSessionRuntime.AcpSessionRuntimeStartResult, |
| 80 | cwd: string, |
| 81 | ) => Effect.Effect<void>; |
| 82 | readonly onSessionEvent?: AcpAdapterV2Flavor["onSessionEvent"]; |
| 83 | readonly nativeLogging?: Parameters<typeof makeAcpAdapterV2>[0]["nativeLogging"]; |
| 84 | readonly continuationRequests?: Parameters<typeof makeAcpAdapterV2>[0]["continuationRequests"]; |
| 85 | } |
| 86 | |
| 87 | /** |
| 88 | * ACP 1.1.1 exposes subagent invocations as ordinary `start_subagent` tools |
| 89 | * without child session ids. Project them as subagent items keyed by the tool |
| 90 | * call so the timeline shows them as delegated work. |
| 91 | */ |
| 92 | const extractAntigravitySubagentUpdate: NonNullable<AcpAdapterV2Flavor["extractSubagentUpdate"]> = ( |
| 93 | toolCall, |
| 94 | ) => { |
| 95 | if (classifyAntigravitySubagentToolCall(toolCall) !== "subagent") { |
| 96 | return undefined; |
| 97 | } |
| 98 | const status = |
| 99 | toolCall.status === "failed" ? "failed" : toolCall.status === "pending" ? "pending" : "running"; |
| 100 | return { |
| 101 | nativeTaskId: toolCall.toolCallId, |
| 102 | prompt: |
| 103 | antigravitySubagentOutput(toolCall) ?? |
| 104 | toolCall.detail ?? |
| 105 | toolCall.title ?? |
| 106 | "Antigravity subagent batch", |
| 107 | title: "Antigravity subagent batch", |
| 108 | model: null, |
| 109 | status, |
| 110 | childSessionId: null, |
| 111 | result: status === "failed" ? (antigravitySubagentOutput(toolCall) ?? null) : null, |
| 112 | }; |
| 113 | }; |
| 114 | |
| 115 | /** The services the Antigravity flavor runs its runtime and session files with. */ |
| 116 | interface AntigravityFlavorServices { |
| 117 | readonly crypto: Crypto.Crypto; |
| 118 | readonly fileSystem: FileSystem.FileSystem; |
| 119 | readonly path: Path.Path; |
| 120 | readonly host: ProviderHost.ProviderHost["Service"]; |
| 121 | } |
| 122 | |
| 123 | export function makeAntigravityAcpAdapterFlavor( |
| 124 | options: AntigravityAdapterV2Options & AntigravityFlavorServices, |
| 125 | ): AcpAdapterV2Flavor { |
| 126 | // The attachments dir grant lets the agent read pasted files at the paths |
| 127 | // the turn text references. It is a leaf directory of uploads. A session |
| 128 | // without a workspace gets no workspace root rather than the server's cwd. |
| 129 | const antigravityClientFileRoots = (cwd: string | null) => |
| 130 | cwd === null ? [options.host.paths.attachmentsDir] : [cwd, options.host.paths.attachmentsDir]; |
| 131 | const makeRuntime = (input: AcpAdapterV2RuntimeInput) => |
| 132 | Effect.gen(function* () { |
| 133 | // AcpAdapterV2 owns the runtime scope; sign-in and sign-out stop the |
| 134 | // process by closing a child of it, and the adapter respawns on the next turn. |
| 135 | const scope = yield* Scope.fork(yield* Effect.scope); |
| 136 | const runtime = yield* options |
| 137 | .withProcess( |
| 138 | Scope.close(scope, Exit.void), |
| 139 | options.makeRuntime({ |
| 140 | ...input, |
| 141 | clientFileSystem: true, |
| 142 | additionalDirectories: [options.host.paths.attachmentsDir], |
| 143 | }), |
| 144 | ) |
| 145 | .pipe(Effect.provideService(Scope.Scope, scope)); |
| 146 | return { |
| 147 | ...runtime, |
| 148 | start: () => |
| 149 | runtime |
| 150 | .start() |
| 151 | .pipe( |
| 152 | Effect.tap( |
| 153 | (started) => options.onSessionStarted?.(started, input.cwd) ?? Effect.void, |
| 154 | ), |
| 155 | ), |
| 156 | }; |
| 157 | }).pipe( |
| 158 | Effect.provideService(Crypto.Crypto, options.crypto), |
| 159 | Effect.mapError((cause): EffectAcpErrors.AcpError => |
| 160 | cause._tag === "ProviderSetupError" |
| 161 | ? new EffectAcpErrors.AcpTransportError({ detail: cause.detail, cause }) |
| 162 | : cause, |
| 163 | ), |
| 164 | ); |
| 165 | |
| 166 | return { |
| 167 | driver: ANTIGRAVITY_PROVIDER, |
| 168 | runtimeHarness: "Antigravity", |
| 169 | capabilities: AntigravityProviderCapabilitiesV2, |
| 170 | makeRuntime, |
| 171 | // Loading history replays every tool call; resume restores the session |
| 172 | // without the replay and is what the official client does. |
| 173 | preferResumeSession: true, |
| 174 | subagentsIdleOnTurnCompletion: true, |
| 175 | ...(options.onSessionEvent === undefined ? {} : { onSessionEvent: options.onSessionEvent }), |
| 176 | applyModelSelection: ({ runtime, modelSelection }) => |
| 177 | Effect.gen(function* () { |
| 178 | const defaultModel = yield* options.defaultModel; |
| 179 | return yield* applyAntigravityAcpModelSelection({ |
| 180 | runtime, |
| 181 | model: |
| 182 | modelSelection.model === ANTIGRAVITY_DEFAULT_MODEL ? undefined : modelSelection.model, |
| 183 | defaultModel, |
| 184 | mapError: (cause) => cause, |
| 185 | }); |
| 186 | }), |
| 187 | sessionModeForPolicy: (policy) => antigravityPermissionMode(policy.runtimeMode), |
| 188 | clientFileSystem: { |
| 189 | readTextFile: (request, cwd) => |
| 190 | readAntigravityClientTextFile({ |
| 191 | fileSystem: options.fileSystem, |
| 192 | path: options.path, |
| 193 | allowedRoots: antigravityClientFileRoots(cwd), |
| 194 | request, |
| 195 | }), |
| 196 | writeTextFile: (request, cwd) => |
| 197 | writeAntigravityClientTextFile({ |
| 198 | fileSystem: options.fileSystem, |
| 199 | path: options.path, |
| 200 | allowedRoots: antigravityClientFileRoots(cwd), |
| 201 | request, |
| 202 | }), |
| 203 | }, |
| 204 | approvalOptions: antigravityApprovalOptions, |
| 205 | extractPermissionQuestion: (request) => { |
| 206 | const question = extractAntigravityUserInputQuestion(request); |
| 207 | if (question === undefined) return undefined; |
| 208 | return { |
| 209 | question: { |
| 210 | id: question.id, |
| 211 | header: question.header, |
| 212 | question: question.question, |
| 213 | options: question.options.map((option) => ({ |
| 214 | label: option.label, |
| 215 | description: option.description, |
| 216 | })), |
| 217 | multiSelect: false, |
| 218 | }, |
| 219 | respond: (answers) => makeAntigravityUserInputResponse(request, answers), |
| 220 | }; |
| 221 | }, |
| 222 | normalizeToolCall: normalizeAntigravityToolCall, |
| 223 | extractSubagentUpdate: extractAntigravitySubagentUpdate, |
| 224 | supportsImagePrompts: true, |
| 225 | supportsCompaction: true, |
| 226 | }; |
| 227 | } |
| 228 | |
| 229 | export const makeAntigravityAdapterV2 = Effect.fn("makeAntigravityAdapterV2")(function* ( |
| 230 | options: AntigravityAdapterV2Options, |
| 231 | ) { |
| 232 | const services: AntigravityFlavorServices = { |
| 233 | crypto: yield* Crypto.Crypto, |
| 234 | fileSystem: yield* FileSystem.FileSystem, |
| 235 | path: yield* Path.Path, |
| 236 | host: yield* ProviderHost.ProviderHost, |
| 237 | }; |
| 238 | return yield* makeAcpAdapterV2({ |
| 239 | instanceId: options.instanceId, |
| 240 | flavor: makeAntigravityAcpAdapterFlavor({ ...options, ...services }), |
| 241 | selfInvocation: options.selfInvocation, |
| 242 | ...(options.nativeLogging === undefined ? {} : { nativeLogging: options.nativeLogging }), |
| 243 | ...(options.continuationRequests === undefined |
| 244 | ? {} |
| 245 | : { continuationRequests: options.continuationRequests }), |
| 246 | }); |
| 247 | }); |