1import {
2 ANTIGRAVITY_DEFAULT_MODEL,
3 ProviderDriverKind,
4 ProviderInstanceId,
5 type OrchestrationV2ProviderCapabilities,
6 type ProviderSetupError,
7} from "@t3tools/contracts";
8import type { SelfInvocation } from "@t3tools/shared/nodeRuntime";
9import * as SecureRandom from "effect/crypto/SecureRandom";
10import * as Effect from "effect/Effect";
11import * as FileSystem from "effect/FileSystem";
12import * as Path from "effect/Path";
13import * as Exit from "effect/Exit";
14import * as Semaphore from "effect/Semaphore";
15import * as Scope from "effect/Scope";
16import * as EffectAcpErrors from "effect-acp/errors";
17
18import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
19import { PULL_REQUEST_LINKING_INSTRUCTIONS } from "@t3tools/provider-core/server/runtimeInstructions";
20import type { AntigravityAuth } from "../../provider/AntigravityAuth.ts";
21import type * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime";
22import {
23 antigravityAppendsSystemPrompt,
24 antigravityPermissionMode,
25 antigravityRuntimeModeForSessionMode,
26 applyAntigravityAcpModelSelection,
27 buildAntigravityPrompt,
28 type AntigravityAcpRuntimeInput,
29} from "../../provider/acp/AntigravityAcpSupport.ts";
30import {
31 ANTIGRAVITY_SUBAGENT_BATCH_TITLE,
32 antigravitySubagentLaunches,
33 describeAntigravitySubagentLaunches,
34} from "../../provider/acp/AntigravityChildWork.ts";
35import { readAntigravityClientTextFile } from "../../provider/acp/AntigravityClientFiles.ts";
36import { watchAntigravityPromptOutcome } from "../../provider/acp/AntigravityTurnOutcome.ts";
37import {
38 antigravityApprovalOptions,
39 antigravitySubagentOutput,
40 classifyAntigravitySubagentToolCall,
41 extractAntigravityUserInputQuestion,
42 makeAntigravityReadRepair,
43 makeAntigravityUserInputResponse,
44 normalizeAntigravityToolCall,
45} from "../../provider/acp/AntigravityProtocol.ts";
46import { isAntigravitySignInRequiredError } from "../../provider/antigravityAuthSupport.ts";
47import {
48 AcpProviderCapabilitiesV2,
49 makeAcpAdapterV2,
50 type AcpAdapterV2Flavor,
51 type AcpAdapterV2RuntimeInput,
52} from "@t3tools/provider-acp/server/adapter";
53
54const ANTIGRAVITY_PROVIDER = ProviderDriverKind.make("antigravity");
55
56const AntigravityProviderCapabilitiesV2 = {
57 ...AcpProviderCapabilitiesV2,
58 sessions: {
59 ...AcpProviderCapabilitiesV2.sessions,
60 supportsModelSwitchInSession: true,
61 supportsRuntimeModeSwitchInSession: true,
62 },
63 tools: {
64 ...AcpProviderCapabilitiesV2.tools,
65 supportsMcpTools: true,
66 },
67 subagents: {
68 ...AcpProviderCapabilitiesV2.subagents,
69 supportsSubagents: true,
70 },
71} satisfies OrchestrationV2ProviderCapabilities;
72
73export interface AntigravityAdapterV2Options {
74 readonly instanceId: ProviderInstanceId;
75 readonly selfInvocation: SelfInvocation;
76 readonly makeRuntime: (
77 input: Omit<AntigravityAcpRuntimeInput, "spawn" | "childProcessSpawner">,
78 ) => Effect.Effect<
79 AcpSessionRuntime.AcpSessionRuntime["Service"],
80 EffectAcpErrors.AcpError | ProviderSetupError,
81 Scope.Scope
82 >;
83 readonly withProcess: AntigravityAuth["withProcess"];
84 readonly defaultModel: Effect.Effect<string | undefined>;
85 readonly onSessionStarted?: (
86 started: AcpSessionRuntime.AcpSessionRuntimeStartResult,
87 cwd: string,
88 ) => Effect.Effect<void>;
89 readonly onSessionEvent?: AcpAdapterV2Flavor["onSessionEvent"];
90 readonly nativeLogging?: Parameters<typeof makeAcpAdapterV2>[0]["nativeLogging"];
91 readonly continuationRequests?: Parameters<typeof makeAcpAdapterV2>[0]["continuationRequests"];
92 /** The agent's GEMINI_HOME. Its session logs show turns that ended on an upstream error. */
93 readonly profileDirectory?: string;
94}
95
96/**
97 * Native subagent tools report no child session ids, so children stay read-only.
98 * Only permission prompts carry the launch; NativeChildWorkQuery matches their ids by this text.
99 */
100const extractAntigravitySubagentUpdate: NonNullable<AcpAdapterV2Flavor["extractSubagentUpdate"]> = (
101 toolCall,
102) => {
103 if (classifyAntigravitySubagentToolCall(toolCall) !== "subagent") {
104 return undefined;
105 }
106 const status =
107 toolCall.status === "failed" ? "failed" : toolCall.status === "pending" ? "pending" : "running";
108 const launches = antigravitySubagentLaunches(toolCall.data.rawInput);
109 const launch = launches === undefined ? undefined : describeAntigravitySubagentLaunches(launches);
110 return {
111 nativeTaskId: toolCall.toolCallId,
112 prompt:
113 launch?.prompt ??
114 antigravitySubagentOutput(toolCall) ??
115 toolCall.detail ??
116 toolCall.title ??
117 ANTIGRAVITY_SUBAGENT_BATCH_TITLE,
118 title: launch?.title ?? ANTIGRAVITY_SUBAGENT_BATCH_TITLE,
119 model: null,
120 status,
121 childSessionId: null,
122 result: status === "failed" ? (antigravitySubagentOutput(toolCall) ?? null) : null,
123 };
124};
125
126interface AntigravityFlavorServices {
127 readonly random: SecureRandom.SecureRandom["Service"];
128 readonly fileSystem: FileSystem.FileSystem;
129 readonly path: Path.Path;
130 readonly host: ProviderHost.ProviderHost["Service"];
131}
132
133export function makeAntigravityAcpAdapterFlavor(
134 options: AntigravityAdapterV2Options & AntigravityFlavorServices,
135): AcpAdapterV2Flavor {
136 const { profileDirectory } = options;
137 const antigravityClientFileRoots = (cwd: string | null) =>
138 cwd === null ? [options.host.paths.attachmentsDir] : [cwd, options.host.paths.attachmentsDir];
139 const makeRuntime = Effect.fn("AntigravityAdapterV2.makeRuntime")(
140 function* (input: AcpAdapterV2RuntimeInput) {
141 const scope = yield* Scope.fork(yield* Effect.scope);
142 const runtime = yield* options
143 .withProcess(
144 Scope.close(scope, Exit.void),
145 options.makeRuntime({
146 ...input,
147 clientFileReads: true,
148 sessionSystemPromptAppend: PULL_REQUEST_LINKING_INSTRUCTIONS,
149 additionalDirectories: [options.host.paths.attachmentsDir],
150 }),
151 )
152 .pipe(Effect.provideService(Scope.Scope, scope));
153 // Repair keeps state across updates, so each runtime must have one session-update handler.
154 const reads = makeAntigravityReadRepair();
155 // Served reads are reported from the read handler, so they take turns with agent updates.
156 const updates = yield* Semaphore.make(1);
157 let projectUpdate: Parameters<typeof runtime.handleSessionUpdate>[0] | undefined;
158 return {
159 ...runtime,
160 handleReadTextFile: (handler) =>
161 runtime.handleReadTextFile((request, context) =>
162 handler(request, context).pipe(
163 Effect.onExit((exit) => {
164 const served = reads.attempted(request.path, Exit.isSuccess(exit));
165 if (served === undefined || projectUpdate === undefined) return Effect.void;
166 return updates
167 .withPermit(
168 projectUpdate({
169 sessionId: served.sessionId,
170 update: {
171 sessionUpdate: "tool_call_update",
172 toolCallId: served.toolCallId,
173 status: "completed",
174 },
175 }),
176 )
177 .pipe(Effect.ignore);
178 }),
179 ),
180 ),
181 prompt: (payload, promptOptions) =>
182 Effect.sync(reads.reset).pipe(Effect.andThen(runtime.prompt(payload, promptOptions))),
183 handleSessionUpdate: (handler) => {
184 projectUpdate = handler;
185 return runtime.handleSessionUpdate((notification) =>
186 updates.withPermit(handler(reads.repair(notification))),
187 );
188 },
189 start: () =>
190 runtime
191 .start()
192 .pipe(
193 Effect.tap(
194 (started) => options.onSessionStarted?.(started, input.cwd) ?? Effect.void,
195 ),
196 ),
197 } satisfies typeof runtime;
198 },
199 Effect.provideService(SecureRandom.SecureRandom, options.random),
200 Effect.mapError((cause): EffectAcpErrors.AcpError =>
201 cause._tag === "ProviderSetupError"
202 ? new EffectAcpErrors.AcpTransportError({ detail: cause.detail, cause })
203 : cause,
204 ),
205 );
206
207 return {
208 driver: ANTIGRAVITY_PROVIDER,
209 runtimeHarness: "Antigravity",
210 capabilities: AntigravityProviderCapabilitiesV2,
211 // Agents that advertise HTTP MCP skip the stdio bridge process; others keep stdio.
212 mcpOverHttp: true,
213 sessionCarriesPullRequestLinking: antigravityAppendsSystemPrompt,
214 makeRuntime,
215 buildPrompt: ({
216 text,
217 attachments,
218 resolveAttachmentPath,
219 promptCapabilities,
220 agentVersion,
221 currentModel,
222 }) =>
223 buildAntigravityPrompt({
224 input: text,
225 attachments,
226 resolveAttachmentPath,
227 promptCapabilities,
228 agentVersion,
229 currentModel,
230 }).pipe(
231 Effect.provideService(FileSystem.FileSystem, options.fileSystem),
232 Effect.provideService(Path.Path, options.path),
233 ),
234 // Loading history replays tool calls; resume avoids replay.
235 subagentsIdleOnTurnCompletion: true,
236 ...(options.onSessionEvent === undefined ? {} : { onSessionEvent: options.onSessionEvent }),
237 applyModelSelection: Effect.fn("AntigravityAdapterV2.applyModelSelection")(function* ({
238 runtime,
239 modelSelection,
240 }) {
241 const defaultModel = yield* options.defaultModel;
242 return yield* applyAntigravityAcpModelSelection({
243 runtime,
244 model:
245 modelSelection.model === ANTIGRAVITY_DEFAULT_MODEL ? undefined : modelSelection.model,
246 defaultModel,
247 mapError: (cause) => cause,
248 });
249 }),
250 sessionModeForPolicy: (policy) => antigravityPermissionMode(policy.runtimeMode),
251 runtimeModeForSessionMode: antigravityRuntimeModeForSessionMode,
252 clientFileSystem: {
253 readTextFile: (request, cwd) =>
254 readAntigravityClientTextFile({
255 fileSystem: options.fileSystem,
256 path: options.path,
257 allowedRoots: antigravityClientFileRoots(cwd),
258 request,
259 }),
260 },
261 approvalOptions: antigravityApprovalOptions,
262 extractPermissionQuestion: (request) => {
263 const question = extractAntigravityUserInputQuestion(request);
264 if (question === undefined) return undefined;
265 return {
266 question,
267 respond: (answers) => makeAntigravityUserInputResponse(request, answers),
268 };
269 },
270 normalizeToolCall: normalizeAntigravityToolCall,
271 extractSubagentUpdate: extractAntigravitySubagentUpdate,
272 supportsImagePrompts: true,
273 // ACP 1.3.0 treats /compact as ordinary model input.
274 supportsCompaction: false,
275 requiresSignIn: isAntigravitySignInRequiredError,
276 // Antigravity can leave a prompt open without updates; long tools and approvals pause this.
277 promptInactivityTimeout: "15 minutes",
278 // 1.4.5 returns the root trajectory's Gemini usageMetadata delta; 1.4.1 returns none.
279 reportsPerTurnPromptUsage: true,
280 geminiPromptUsage: true,
281 ...(profileDirectory === undefined
282 ? {}
283 : {
284 watchPromptOutcome: (sessionId: string) =>
285 watchAntigravityPromptOutcome(profileDirectory, sessionId),
286 }),
287 };
288}
289
290export const makeAntigravityAdapterV2 = Effect.fn("makeAntigravityAdapterV2")(function* (
291 options: AntigravityAdapterV2Options,
292) {
293 const services: AntigravityFlavorServices = {
294 random: yield* SecureRandom.SecureRandom,
295 fileSystem: yield* FileSystem.FileSystem,
296 path: yield* Path.Path,
297 host: yield* ProviderHost.ProviderHost,
298 };
299 return yield* makeAcpAdapterV2({
300 instanceId: options.instanceId,
301 flavor: makeAntigravityAcpAdapterFlavor({ ...options, ...services }),
302 selfInvocation: options.selfInvocation,
303 ...(options.nativeLogging === undefined ? {} : { nativeLogging: options.nativeLogging }),
304 ...(options.continuationRequests === undefined
305 ? {}
306 : { continuationRequests: options.continuationRequests }),
307 });
308});