Skip to content
Source
Sign inSign up

AntigravityAdapterV2.ts

311 lines11.9 KB

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 supportsNativeGoals?: Effect.Effect<boolean>;
86 readonly onSessionStarted?: (
87 started: AcpSessionRuntime.AcpSessionRuntimeStartResult,
88 cwd: string,
89 ) => Effect.Effect<void>;
90 readonly onSessionEvent?: AcpAdapterV2Flavor["onSessionEvent"];
91 readonly nativeLogging?: Parameters<typeof makeAcpAdapterV2>[0]["nativeLogging"];
92 readonly continuationRequests?: Parameters<typeof makeAcpAdapterV2>[0]["continuationRequests"];
93 /** The agent's GEMINI_HOME. Its session logs show turns that ended on an upstream error. */
94 readonly profileDirectory?: string;
96​
97/**
98 * Native subagent tools report no child session ids, so children stay read-only.
99 * Only permission prompts carry the launch; NativeChildWorkQuery matches their ids by this text.
100 */
101const extractAntigravitySubagentUpdate: NonNullable<AcpAdapterV2Flavor["extractSubagentUpdate"]> = (
102 toolCall,
103) => {
104 if (classifyAntigravitySubagentToolCall(toolCall) !== "subagent") {
105 return undefined;
106 }
107 const status =
108 toolCall.status === "failed" ? "failed" : toolCall.status === "pending" ? "pending" : "running";
109 const launches = antigravitySubagentLaunches(toolCall.data.rawInput);
110 const launch = launches === undefined ? undefined : describeAntigravitySubagentLaunches(launches);
111 return {
112 nativeTaskId: toolCall.toolCallId,
113 prompt:
114 launch?.prompt ??
115 antigravitySubagentOutput(toolCall) ??
116 toolCall.detail ??
117 toolCall.title ??
118 ANTIGRAVITY_SUBAGENT_BATCH_TITLE,
119 title: launch?.title ?? ANTIGRAVITY_SUBAGENT_BATCH_TITLE,
120 model: null,
121 status,
122 childSessionId: null,
123 result: status === "failed" ? (antigravitySubagentOutput(toolCall) ?? null) : null,
124 };
125};
126​
127interface AntigravityFlavorServices {
128 readonly random: SecureRandom.SecureRandom["Service"];
129 readonly fileSystem: FileSystem.FileSystem;
130 readonly path: Path.Path;
131 readonly host: ProviderHost.ProviderHost["Service"];
133​
134export function makeAntigravityAcpAdapterFlavor(
135 options: AntigravityAdapterV2Options & AntigravityFlavorServices,
136): AcpAdapterV2Flavor {
137 const { profileDirectory } = options;
138 const antigravityClientFileRoots = (cwd: string | null) =>
139 cwd === null ? [options.host.paths.attachmentsDir] : [cwd, options.host.paths.attachmentsDir];
140 const makeRuntime = Effect.fn("AntigravityAdapterV2.makeRuntime")(
141 function* (input: AcpAdapterV2RuntimeInput) {
142 const scope = yield* Scope.fork(yield* Effect.scope);
143 const runtime = yield* options
144 .withProcess(
145 Scope.close(scope, Exit.void),
146 options.makeRuntime({
147 ...input,
148 clientFileReads: true,
149 sessionSystemPromptAppend: PULL_REQUEST_LINKING_INSTRUCTIONS,
150 additionalDirectories: [options.host.paths.attachmentsDir],
151 }),
152 )
153 .pipe(Effect.provideService(Scope.Scope, scope));
154 // Repair keeps state across updates, so each runtime must have one session-update handler.
155 const reads = makeAntigravityReadRepair();
156 // Served reads are reported from the read handler, so they take turns with agent updates.
157 const updates = yield* Semaphore.make(1);
158 let projectUpdate: Parameters<typeof runtime.handleSessionUpdate>[0] | undefined;
159 return {
160 ...runtime,
161 handleReadTextFile: (handler) =>
162 runtime.handleReadTextFile((request, context) =>
163 handler(request, context).pipe(
164 Effect.onExit((exit) => {
165 const served = reads.attempted(request.path, Exit.isSuccess(exit));
166 if (served === undefined || projectUpdate === undefined) return Effect.void;
167 return updates
168 .withPermit(
169 projectUpdate({
170 sessionId: served.sessionId,
171 update: {
172 sessionUpdate: "tool_call_update",
173 toolCallId: served.toolCallId,
174 status: "completed",
175 },
176 }),
177 )
178 .pipe(Effect.ignore);
179 }),
180 ),
181 ),
182 prompt: (payload, promptOptions) =>
183 Effect.sync(reads.reset).pipe(Effect.andThen(runtime.prompt(payload, promptOptions))),
184 handleSessionUpdate: (handler) => {
185 projectUpdate = handler;
186 return runtime.handleSessionUpdate((notification) =>
187 updates.withPermit(handler(reads.repair(notification))),
188 );
189 },
190 start: () =>
191 runtime
192 .start()
193 .pipe(
194 Effect.tap(
195 (started) => options.onSessionStarted?.(started, input.cwd) ?? Effect.void,
196 ),
197 ),
198 } satisfies typeof runtime;
199 },
200 Effect.provideService(SecureRandom.SecureRandom, options.random),
201 Effect.mapError((cause): EffectAcpErrors.AcpError =>
202 cause._tag === "ProviderSetupError"
203 ? new EffectAcpErrors.AcpTransportError({ detail: cause.detail, cause })
204 : cause,
205 ),
206 );
207​
208 return {
209 driver: ANTIGRAVITY_PROVIDER,
210 runtimeHarness: "Antigravity",
211 capabilities: AntigravityProviderCapabilitiesV2,
212 supportsNativeGoals: options.supportsNativeGoals ?? Effect.succeed(false),
213 // Agents that advertise HTTP MCP skip the stdio bridge process; others keep stdio.
214 mcpOverHttp: true,
215 sessionCarriesPullRequestLinking: antigravityAppendsSystemPrompt,
216 makeRuntime,
217 buildAttachmentPrompt: ({
218 text,
219 attachments,
220 resolveAttachmentPath,
221 promptCapabilities,
222 agentVersion,
223 currentModel,
224 }) =>
225 buildAntigravityPrompt({
226 input: text,
227 attachments,
228 resolveAttachmentPath,
229 promptCapabilities,
230 agentVersion,
231 currentModel,
232 }).pipe(
233 Effect.map((blocks) => (text.trim() ? blocks.slice(1) : blocks)),
234 Effect.provideService(FileSystem.FileSystem, options.fileSystem),
235 Effect.provideService(Path.Path, options.path),
236 ),
237 // Loading history replays tool calls; resume avoids replay.
238 subagentsIdleOnTurnCompletion: true,
239 ...(options.onSessionEvent === undefined ? {} : { onSessionEvent: options.onSessionEvent }),
240 applyModelSelection: Effect.fn("AntigravityAdapterV2.applyModelSelection")(function* ({
241 runtime,
242 modelSelection,
243 }) {
244 const defaultModel = yield* options.defaultModel;
245 return yield* applyAntigravityAcpModelSelection({
246 runtime,
247 model:
248 modelSelection.model === ANTIGRAVITY_DEFAULT_MODEL ? undefined : modelSelection.model,
249 defaultModel,
250 mapError: (cause) => cause,
251 });
252 }),
253 sessionModeForPolicy: (policy) => antigravityPermissionMode(policy.runtimeMode),
254 runtimeModeForSessionMode: antigravityRuntimeModeForSessionMode,
255 clientFileSystem: {
256 readTextFile: (request, cwd) =>
257 readAntigravityClientTextFile({
258 fileSystem: options.fileSystem,
259 path: options.path,
260 allowedRoots: antigravityClientFileRoots(cwd),
261 request,
262 }),
263 },
264 approvalOptions: antigravityApprovalOptions,
265 extractPermissionQuestion: (request) => {
266 const question = extractAntigravityUserInputQuestion(request);
267 if (question === undefined) return undefined;
268 return {
269 question,
270 respond: (answers) => makeAntigravityUserInputResponse(request, answers),
271 };
272 },
273 normalizeToolCall: normalizeAntigravityToolCall,
274 extractSubagentUpdate: extractAntigravitySubagentUpdate,
275 supportsImagePrompts: true,
276 // ACP 1.3.0 treats /compact as ordinary model input.
277 supportsCompaction: false,
278 requiresSignIn: isAntigravitySignInRequiredError,
279 // Antigravity can leave a prompt open without updates; long tools and approvals pause this.
280 promptInactivityTimeout: "15 minutes",
281 // 1.4.5 returns the root trajectory's Gemini usageMetadata delta; 1.4.1 returns none.
282 reportsPerTurnPromptUsage: true,
283 geminiPromptUsage: true,
284 ...(profileDirectory === undefined
285 ? {}
286 : {
287 watchPromptOutcome: (sessionId: string) =>
288 watchAntigravityPromptOutcome(profileDirectory, sessionId),
289 }),
290 };
292​
293export const makeAntigravityAdapterV2 = Effect.fn("makeAntigravityAdapterV2")(function* (
294 options: AntigravityAdapterV2Options,
295) {
296 const services: AntigravityFlavorServices = {
297 random: yield* SecureRandom.SecureRandom,
298 fileSystem: yield* FileSystem.FileSystem,
299 path: yield* Path.Path,
300 host: yield* ProviderHost.ProviderHost,
301 };
302 return yield* makeAcpAdapterV2({
303 instanceId: options.instanceId,
304 flavor: makeAntigravityAcpAdapterFlavor({ ...options, ...services }),
305 selfInvocation: options.selfInvocation,
306 ...(options.nativeLogging === undefined ? {} : { nativeLogging: options.nativeLogging }),
307 ...(options.continuationRequests === undefined
308 ? {}
309 : { continuationRequests: options.continuationRequests }),
310 });
311});