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 Crypto from "effect/Crypto";
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 Scope from "effect/Scope";
15import * as EffectAcpErrors from "effect-acp/errors";
16
17import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
18import type { AntigravityAuth } from "../../provider/AntigravityAuth.ts";
19import type * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime";
20import {
21 antigravityPermissionMode,
22 applyAntigravityAcpModelSelection,
23 type AntigravityAcpRuntimeInput,
24} from "../../provider/acp/AntigravityAcpSupport.ts";
25import {
26 readAntigravityClientTextFile,
27 writeAntigravityClientTextFile,
28} from "../../provider/acp/AntigravityClientFiles.ts";
29import {
30 antigravityApprovalOptions,
31 antigravitySubagentOutput,
32 classifyAntigravitySubagentToolCall,
33 extractAntigravityUserInputQuestion,
34 makeAntigravityUserInputResponse,
35 normalizeAntigravityToolCall,
36} from "../../provider/acp/AntigravityProtocol.ts";
37import {
38 AcpProviderCapabilitiesV2,
39 makeAcpAdapterV2,
40 type AcpAdapterV2Flavor,
41 type AcpAdapterV2RuntimeInput,
42} from "@t3tools/provider-acp/server/adapter";
43
44const ANTIGRAVITY_PROVIDER = ProviderDriverKind.make("antigravity");
45
46const 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
63export 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 */
92const 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. */
116interface AntigravityFlavorServices {
117 readonly crypto: Crypto.Crypto;
118 readonly fileSystem: FileSystem.FileSystem;
119 readonly path: Path.Path;
120 readonly host: ProviderHost.ProviderHost["Service"];
121}
122
123export 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
229export 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});