Skip to content
Source
Sign inSign up

AntigravityAdapterV2.test.ts

1,245 lines50.9 KB

1import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { assert, describe, it } from "@effect/vitest";
4import {
5 ChatFileAttachment,
6 MessageId,
7 NodeId,
8 ProjectId,
9 ProviderInstanceId,
10 ProviderSessionId,
11 RunAttemptId,
12 RunId,
13 ThreadId,
14} from "@t3tools/contracts";
15import { resolveSelfInvocation } from "@t3tools/shared/nodeRuntime";
16import { htmlRenderFromToolItem } from "@t3tools/shared/toolOutput";
17import * as SecureRandom from "effect/crypto/SecureRandom";
18import * as DateTime from "effect/DateTime";
19import * as Deferred from "effect/Deferred";
20import * as Effect from "effect/Effect";
21import * as Exit from "effect/Exit";
22import * as FileSystem from "effect/FileSystem";
23import * as Layer from "effect/Layer";
24import * as Path from "effect/Path";
25import * as Stream from "effect/Stream";
26import { ChildProcessSpawner } from "effect/process";
27import type * as EffectAcpSchema from "effect-acp/compat";
28import * as EffectAcpErrors from "effect-acp/errors";
29​
30import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
31import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
32import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";
33import type * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime";
34import { makeAntigravityAcpRuntime } from "../../provider/acp/AntigravityAcpSupport.ts";
35import { normalizeAntigravitySessionUpdate } from "../../provider/acp/AntigravityProtocol.ts";
36import { ANTIGRAVITY_SIGN_IN_REQUIRED_MESSAGE } from "../../provider/antigravityAuthSupport.ts";
37import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
38import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
39import {
40 makeAntigravityAcpAdapterFlavor,
41 makeAntigravityAdapterV2,
42} from "./AntigravityAdapterV2.ts";
43​
44const flavor = makeAntigravityAcpAdapterFlavor({
45 instanceId: ProviderInstanceId.make("antigravity-test"),
46 random: undefined as never,
47 fileSystem: undefined as never,
48 path: undefined as never,
49 host: undefined as never,
50 selfInvocation: undefined as never,
51 makeRuntime: () => Effect.die("not spawned in this test"),
52 withProcess: (_stop, task) => task,
53 defaultModel: Effect.undefined,
54});
55​
56function permissionRequest(
57 toolCallId: string,
58 options: ReadonlyArray<EffectAcpSchema.PermissionOption>,
59): EffectAcpSchema.RequestPermissionRequest {
60 return {
61 sessionId: "session-1",
62 options,
63 toolCall: { toolCallId, title: "Which branch?" },
64 };
66​
67describe("AntigravityAdapterV2 flavor", () => {
68 it("keeps a successful subagent launch running until the parent turn becomes idle", () => {
69 const batch = flavor.extractSubagentUpdate?.({
70 toolCallId: "trajectory:4",
71 title: "Running start_subagent",
72 kind: "other",
73 status: "completed",
74 data: { rawOutput: "Started two agents." },
75 });
76 assert.equal(batch?.status, "running");
77 assert.equal(batch?.prompt, "Started two agents.");
78 assert.isNull(batch?.result);
79 assert.isTrue(flavor.subagentsIdleOnTurnCompletion);
80​
81 // Permission prompts carry the launch; later updates for the same call do not.
82 const launch = flavor.extractSubagentUpdate?.({
83 toolCallId: "35bea692fa95447fbdae6333fec5515f",
84 title: "Run invoke_subagent?",
85 kind: "other",
86 status: "pending",
87 data: {
88 rawInput: {
89 Subagents: [{ TypeName: "research", Role: "Readme Reader", Prompt: "Read README.md" }],
90 },
91 },
92 });
93 assert.equal(launch?.title, "Readme Reader");
94 assert.equal(launch?.prompt, "Read README.md");
95 });
96​
97 it("routes interaction_* permission requests to the question card", () => {
98 const question = flavor.extractPermissionQuestion?.(
99 permissionRequest("interaction_1", [
100 { optionId: "main", name: "main", kind: "allow_once" },
101 { optionId: "dev", name: "dev", kind: "allow_once" },
102 ]),
103 );
104 assert.isDefined(question);
105 assert.deepEqual(
106 question?.question.options.map((option) => option.label),
107 ["main", "dev"],
108 );
109 assert.deepEqual(question?.respond({ interaction_1: "dev" }), {
110 outcome: { outcome: "selected", optionId: "dev" },
111 });
112 assert.isUndefined(question?.respond({ interaction_1: "nope" }));
113 assert.isUndefined(
114 flavor.extractPermissionQuestion?.(
115 permissionRequest("tool_1", [{ optionId: "allow", name: "Allow", kind: "allow_once" }]),
116 ),
117 );
118 });
119​
120 it("keeps the question card to the agent's own options so a typed answer cannot be sent", () => {
121 const question = flavor.extractPermissionQuestion?.(
122 permissionRequest("interaction_2", [
123 { optionId: "opt-main", name: "main", kind: "allow_once" },
124 { optionId: "opt-dev", name: "dev", kind: "allow_once" },
125 ]),
126 );
127 assert.strictEqual(question?.question.allowCustomAnswer, false);
128 assert.deepEqual(
129 question?.question.options.map((option) => option.value),
130 ["opt-main", "opt-dev"],
131 );
132 assert.deepEqual(question?.respond({ interaction_2: "opt-dev" }), {
133 outcome: { outcome: "selected", optionId: "opt-dev" },
134 });
135 });
136});
137​
138const layerSession = Layer.mergeAll(
139 NodeServices.layer,
140 NodeCrypto.layer,
141 IdAllocator.layer,
142 McpProviderSessions.layer,
143 TestProviderHost.layer().pipe(
144 Layer.provide(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer)),
145 ),
146);
147​
148describe("AntigravityAdapterV2 compaction", () => {
149 it.effect("does not expose unsupported manual compaction on the provider session", () =>
150 Effect.gen(function* () {
151 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
152 const path = yield* Path.Path;
153 const crypto = yield* SecureRandom.SecureRandom;
154 const mockAgentPath = yield* path.fromFileUrl(
155 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
156 );
157 const instanceId = ProviderInstanceId.make("antigravity-compaction-test");
158 const adapter = yield* makeAntigravityAdapterV2({
159 instanceId,
160 selfInvocation: yield* resolveSelfInvocation(),
161 makeRuntime: (input) =>
162 makeAntigravityAcpRuntime({
163 ...input,
164 childProcessSpawner,
165 spawn: {
166 command: process.execPath,
167 args: [mockAgentPath],
168 cwd: input.cwd,
169 env: { T3_ACP_ANTIGRAVITY: "1" },
170 },
171 }).pipe(Effect.provideService(SecureRandom.SecureRandom, crypto)),
172 withProcess: (_stop, task) => task,
173 defaultModel: Effect.undefined,
174 });
175 const session = yield* adapter.openSession({
176 threadId: ThreadId.make("thread-antigravity-compaction"),
177 providerSessionId: ProviderSessionId.make("provider-session-antigravity-compaction"),
178 modelSelection: { instanceId, model: "gemini-test-low" },
179 runtimePolicy: ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
180 runtimeMode: "full-access",
181 interactionMode: "default",
182 cwd: process.cwd(),
183 }),
184 });
185 assert.isUndefined(session.compactThread);
186 }).pipe(Effect.provide(layerSession), Effect.scoped),
187 );
188});
189​
190describe("AntigravityAdapterV2 session open", () => {
191 it.effect.each([
192 {
193 name: "browser sign-in",
194 cause: new EffectAcpErrors.AcpTransportError({
195 detail: ANTIGRAVITY_SIGN_IN_REQUIRED_MESSAGE,
196 cause: undefined,
197 }),
198 requiresSignIn: true,
199 },
200 {
201 name: "ACP authentication required",
202 cause: EffectAcpErrors.AcpRequestError.authRequired(),
203 requiresSignIn: true,
204 },
205 {
206 name: "transport failure",
207 cause: new EffectAcpErrors.AcpTransportError({
208 detail: "Connection closed",
209 cause: undefined,
210 }),
211 requiresSignIn: undefined,
212 },
213 ] as const)("classifies $name during session open", ({ cause, requiresSignIn }) =>
214 Effect.gen(function* () {
215 const instanceId = ProviderInstanceId.make("antigravity-sign-in-test");
216 const adapter = yield* makeAntigravityAdapterV2({
217 instanceId,
218 selfInvocation: yield* resolveSelfInvocation(),
219 makeRuntime: () => Effect.fail(cause),
220 withProcess: (_stop, task) => task,
221 defaultModel: Effect.undefined,
222 });
223 const threadId = ThreadId.make("thread-antigravity-sign-in");
224 const error = yield* adapter
225 .openSession({
226 threadId,
227 providerSessionId: ProviderSessionId.make("provider-session-antigravity-sign-in"),
228 modelSelection: { instanceId, model: "gemini-test-low" },
229 runtimePolicy: ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
230 runtimeMode: "full-access",
231 interactionMode: "default",
232 cwd: process.cwd(),
233 }),
234 })
235 .pipe(Effect.flip);
236​
237 assert.equal(error._tag, "ProviderAdapterOpenSessionError");
238 assert.equal(
239 error._tag === "ProviderAdapterOpenSessionError" && error.requiresSignIn,
240 requiresSignIn,
241 );
242 }).pipe(Effect.provide(layerSession), Effect.scoped),
243 );
244});
245​
246describe("AntigravityAdapterV2 client file system", () => {
247 it.effect("serves confined reads and leaves edits to the agent's diff tools", () =>
248 Effect.gen(function* () {
249 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
250 const fileSystem = yield* FileSystem.FileSystem;
251 const path = yield* Path.Path;
252 const host = yield* ProviderHost.ProviderHost;
253 const mockAgentPath = yield* path.fromFileUrl(
254 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
255 );
256 type RuntimeService = AcpSessionRuntime.AcpSessionRuntime["Service"];
257 // effect-acp keeps the last handler registered per method; so does this.
258 let readTextFile: Parameters<RuntimeService["handleReadTextFile"]>[0] | undefined;
259 let writeTextFile: Parameters<RuntimeService["handleWriteTextFile"]>[0] | undefined;
260 const logs = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-antigravity-log-" });
261 const requestLog = path.join(logs, "requests.ndjson");
262 const crypto = yield* SecureRandom.SecureRandom;
263 const instanceId = ProviderInstanceId.make("antigravity-containment-test");
264 const adapter = yield* makeAntigravityAdapterV2({
265 instanceId,
266 selfInvocation: yield* resolveSelfInvocation(),
267 makeRuntime: (input) =>
268 makeAntigravityAcpRuntime({
269 ...input,
270 childProcessSpawner,
271 spawn: {
272 command: process.execPath,
273 args: [mockAgentPath],
274 cwd: input.cwd,
275 env: { T3_ACP_ANTIGRAVITY: "1", T3_ACP_REQUEST_LOG_PATH: requestLog },
276 },
277 }).pipe(
278 Effect.provideService(SecureRandom.SecureRandom, crypto),
279 Effect.map((runtime): RuntimeService => ({
280 ...runtime,
281 handleReadTextFile: (handler) =>
282 Effect.sync(() => {
283 readTextFile = handler;
284 }).pipe(Effect.andThen(runtime.handleReadTextFile(handler))),
285 handleWriteTextFile: (handler) =>
286 Effect.sync(() => {
287 writeTextFile = handler;
288 }).pipe(Effect.andThen(runtime.handleWriteTextFile(handler))),
289 })),
290 ),
291 withProcess: (_stop, task) => task,
292 defaultModel: Effect.undefined,
293 });
294 const workspace = yield* fileSystem.makeTempDirectoryScoped({
295 prefix: "t3-antigravity-workspace-",
296 });
297 const outside = yield* fileSystem.makeTempDirectoryScoped({
298 prefix: "t3-antigravity-outside-",
299 });
300 const outsideFile = path.join(outside, "secret.txt");
301 yield* fileSystem.writeFileString(outsideFile, "secret");
302 const attachment = path.join(host.paths.attachmentsDir, "pasted.txt");
303 yield* fileSystem.writeFileString(attachment, "pasted");
304​
305 const threadId = ThreadId.make("thread-antigravity-containment");
306 const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
307 runtimeMode: "full-access",
308 interactionMode: "default",
309 cwd: workspace,
310 });
311 const modelSelection = { instanceId, model: "gemini-test-low" } as const;
312 const session = yield* adapter.openSession({
313 threadId,
314 providerSessionId: ProviderSessionId.make("provider-session-antigravity-containment"),
315 modelSelection,
316 runtimePolicy,
317 });
318 yield* session.ensureThread({ threadId, modelSelection, runtimePolicy });
319 const initialize = (yield* fileSystem.readFileString(requestLog))
320 .trim()
321 .split("\n")
322 .map(
323 (line) =>
324 JSON.parse(line) as {
325 method?: string;
326 params?: { clientCapabilities?: { fs?: unknown }; clientInfo?: { name?: string } };
327 },
328 )
329 .find((request) => request.method === "initialize");
330 assert.deepEqual(initialize?.params?.clientCapabilities?.fs, {
331 readTextFile: true,
332 writeTextFile: false,
333 });
334 assert.equal(initialize?.params?.clientInfo?.name, "zed");
335 assert.isUndefined(writeTextFile);
336 if (readTextFile === undefined) {
337 return yield* Effect.die("Antigravity sessions must serve client file reads");
338 }
339 const context = (method: string) => ({ requestId: `test-${method}`, method });
340​
341 const insidePath = path.join(workspace, "src", "inside.ts");
342 yield* fileSystem.makeDirectory(path.dirname(insidePath), { recursive: true });
343 yield* fileSystem.writeFileString(insidePath, "inside");
344 const pasted = yield* readTextFile(
345 { sessionId: "mock-session-1", path: attachment },
346 context("fs/read_text_file"),
347 );
348 assert.equal(pasted.content, "pasted");
349 yield* fileSystem.writeFileString(insidePath, "\uFEFFfirst\né中🙂\nlast");
350 const sliced = yield* readTextFile(
351 { sessionId: "mock-session-1", path: insidePath, line: 2, limit: 1 },
352 context("fs/read_text_file"),
353 );
354 assert.equal(sliced.content, "é中🙂");
355 yield* fileSystem.writeFileString(insidePath, "inside");
356 for (const [name, bytes] of [
357 ["image.png", new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])],
358 ["data.bin", new Uint8Array([0x61, 0, 0x62])],
359 ] as const) {
360 const binaryPath = path.join(workspace, name);
361 yield* fileSystem.writeFile(binaryPath, bytes);
362 const binaryRead = yield* readTextFile(
363 { sessionId: "mock-session-1", path: binaryPath },
364 context("fs/read_text_file"),
365 ).pipe(Effect.flip);
366 assert.propertyVal(binaryRead, "code", -32602, `${name} is refused as binary`);
367 }
368​
369 const outsideRead = yield* readTextFile(
370 { sessionId: "mock-session-1", path: outsideFile },
371 context("fs/read_text_file"),
372 ).pipe(Effect.exit);
373 assert.isTrue(Exit.isFailure(outsideRead));
374 const linkPath = path.join(workspace, "linked-secret.txt");
375 yield* fileSystem.symlink(outsideFile, linkPath);
376 const linkedRead = yield* readTextFile(
377 { sessionId: "mock-session-1", path: linkPath },
378 context("fs/read_text_file"),
379 ).pipe(Effect.exit);
380 assert.isTrue(Exit.isFailure(linkedRead), "a read through an escaping symlink is denied");
381 const insideLink = path.join(workspace, "inside-link.ts");
382 yield* fileSystem.symlink(insidePath, insideLink);
383 const viaInsideLink = yield* readTextFile(
384 { sessionId: "mock-session-1", path: insideLink },
385 context("fs/read_text_file"),
386 );
387 assert.equal(viaInsideLink.content, "inside");
388 }).pipe(Effect.provide(layerSession), Effect.scoped),
389 );
390});
391​
392describe("AntigravityAdapterV2 workspace changes", () => {
393 it.effect("confines file requests to the workspace of the turn in progress", () =>
394 Effect.gen(function* () {
395 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
396 const fileSystem = yield* FileSystem.FileSystem;
397 const path = yield* Path.Path;
398 const crypto = yield* SecureRandom.SecureRandom;
399 const mockAgentPath = yield* path.fromFileUrl(
400 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
401 );
402 type RuntimeService = AcpSessionRuntime.AcpSessionRuntime["Service"];
403 const promptReceived = yield* Deferred.make<Parameters<RuntimeService["prompt"]>[0]>();
404 const host = yield* ProviderHost.ProviderHost;
405 const document = ChatFileAttachment.make({
406 type: "file",
407 id: "native-pdf",
408 name: "diagram.pdf",
409 mimeType: "application/pdf",
410 sizeBytes: 9,
411 });
412 const documentPath = host.resolveAttachmentPath(document)!;
413 yield* fileSystem.writeFileString(documentPath, "%PDF-1.7");
414 let readTextFile: Parameters<RuntimeService["handleReadTextFile"]>[0] | undefined;
415 const instanceId = ProviderInstanceId.make("antigravity-workspace-change-test");
416 const adapter = yield* makeAntigravityAdapterV2({
417 instanceId,
418 selfInvocation: yield* resolveSelfInvocation(),
419 makeRuntime: (input) =>
420 makeAntigravityAcpRuntime({
421 ...input,
422 childProcessSpawner,
423 spawn: {
424 command: process.execPath,
425 args: [mockAgentPath],
426 cwd: input.cwd,
427 env: { T3_ACP_ANTIGRAVITY: "1", T3_ACP_HANG_PROMPT_FOREVER: "1" },
428 },
429 }).pipe(
430 Effect.provideService(SecureRandom.SecureRandom, crypto),
431 Effect.map((runtime): RuntimeService => ({
432 ...runtime,
433 prompt: (request) =>
434 Deferred.succeed(promptReceived, request).pipe(
435 Effect.andThen(runtime.prompt(request)),
436 ),
437 start: () =>
438 runtime.start().pipe(
439 Effect.map((started) => ({
440 ...started,
441 initializeResult: {
442 ...started.initializeResult,
443 agentCapabilities: {
444 ...started.initializeResult.agentCapabilities,
445 promptCapabilities: { image: true, audio: true, embeddedContext: true },
446 },
447 },
448 })),
449 ),
450 handleReadTextFile: (handler) =>
451 Effect.sync(() => {
452 readTextFile = handler;
453 }).pipe(Effect.andThen(runtime.handleReadTextFile(handler))),
454 })),
455 ),
456 withProcess: (_stop, task) => task,
457 defaultModel: Effect.undefined,
458 });
459 const workspaceA = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-ag-a-" });
460 const workspaceB = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-ag-b-" });
461 yield* fileSystem.writeFileString(path.join(workspaceA, "a.txt"), "from a");
462 yield* fileSystem.writeFileString(path.join(workspaceB, "b.txt"), "from b");
463 const policyFor = (cwd: string) =>
464 ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
465 runtimeMode: "full-access",
466 interactionMode: "default",
467 cwd,
468 });
469 const threadId = ThreadId.make("thread-antigravity-workspace-change");
470 const modelSelection = { instanceId, model: "gemini-test-low" } as const;
471 const session = yield* adapter.openSession({
472 threadId,
473 providerSessionId: ProviderSessionId.make("provider-session-antigravity-workspace-change"),
474 modelSelection,
475 runtimePolicy: policyFor(workspaceA),
476 });
477 const providerThread = yield* session.ensureThread({
478 threadId,
479 modelSelection,
480 runtimePolicy: policyFor(workspaceA),
481 });
482 const now = yield* DateTime.now;
483 yield* session
484 .startTurn({
485 appThread: {
486 createdBy: "user",
487 creationSource: "web",
488 id: threadId,
489 projectId: ProjectId.make("project-antigravity-workspace-change"),
490 title: "Antigravity workspace change",
491 providerInstanceId: instanceId,
492 modelSelection,
493 runtimeMode: "full-access",
494 interactionMode: "default",
495 branch: null,
496 worktreePath: workspaceB,
497 activeProviderThreadId: providerThread.id,
498 lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: threadId },
499 forkedFrom: null,
500 createdAt: now,
501 updatedAt: now,
502 archivedAt: null,
503 settledOverride: null,
504 settledAt: null,
505 lastVisitedAt: null,
506 deletedAt: null,
507 },
508 threadId,
509 runId: RunId.make("run-antigravity-workspace-change"),
510 runOrdinal: 1,
511 providerTurnOrdinal: 1,
512 attemptId: RunAttemptId.make("attempt-antigravity-workspace-change"),
513 rootNodeId: NodeId.make("node-antigravity-workspace-change"),
514 providerThread,
515 message: {
516 createdBy: "user",
517 creationSource: "web",
518 messageId: MessageId.make("message-antigravity-workspace-change"),
519 text: "read b.txt",
520 attachments: [document],
521 },
522 modelSelection,
523 runtimePolicy: policyFor(workspaceB),
524 })
525 .pipe(Effect.forkScoped);
526 yield* session.events.pipe(
527 Stream.filter((event) => event.type === "provider_turn.updated"),
528 Stream.runHead,
529 );
530 const sent = yield* Deferred.await(promptReceived);
531 assert.deepEqual(
532 sent.prompt.filter((block) => block.type === "resource_link"),
533 [
534 {
535 type: "resource_link",
536 uri: (yield* path.toFileUrl(documentPath)).href,
537 name: "diagram.pdf",
538 mimeType: "application/pdf",
539 size: 8,
540 },
541 ],
542 );
543 assert.include(JSON.stringify(sent.prompt), JSON.stringify(documentPath).slice(1, -1));
544 if (readTextFile === undefined) {
545 return yield* Effect.die("Antigravity sessions must serve client file requests");
546 }
547 const context = { requestId: "test-read", method: "fs/read_text_file" };
548 const fromB = yield* readTextFile(
549 { sessionId: "mock-session-1", path: path.join(workspaceB, "b.txt") },
550 context,
551 );
552 assert.equal(fromB.content, "from b");
553 const fromA = yield* readTextFile(
554 { sessionId: "mock-session-1", path: path.join(workspaceA, "a.txt") },
555 context,
556 ).pipe(Effect.exit);
557 assert.isTrue(Exit.isFailure(fromA), "the previous workspace is no longer readable");
558 }).pipe(Effect.provide(layerSession), Effect.scoped),
559 );
560});
561​
562describe("AntigravityAdapterV2 html_render pages", () => {
563 it.effect("shows the page html_render published when the agent reports only a summary", () =>
564 Effect.gen(function* () {
565 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
566 const path = yield* Path.Path;
567 const crypto = yield* SecureRandom.SecureRandom;
568 const mcpSessions = yield* McpProviderSessions.McpProviderSessions;
569 const mockAgentPath = yield* path.fromFileUrl(
570 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
571 );
572 type RuntimeService = AcpSessionRuntime.AcpSessionRuntime["Service"];
573 let sessionUpdate: Parameters<RuntimeService["handleSessionUpdate"]>[0] | undefined;
574 const instanceId = ProviderInstanceId.make("antigravity-html-render-test");
575 const threadId = ThreadId.make("thread-antigravity-html-render");
576 const page = (name: string) => ({
577 attachmentId: `${threadId}-${name}-html`,
578 title: "Revenue",
579 height: 240,
580 });
581 // Long enough that Antigravity bounds the html the adapter sees.
582 const rows = Array.from({ length: 500 }, (_, row) => `<tr><td>Row ${row}</td></tr>`);
583 const report = { html: `<table>${rows.join("")}</table>`, title: "Revenue", height: 240 };
584 const failed = { ...report, html: "<p>Revenue</p>" };
585 const errored = { ...report, html: "<p>Revenue error</p>" };
586 const adapter = yield* makeAntigravityAdapterV2({
587 instanceId,
588 selfInvocation: yield* resolveSelfInvocation(),
589 makeRuntime: (input) =>
590 makeAntigravityAcpRuntime({
591 ...input,
592 childProcessSpawner,
593 spawn: {
594 command: process.execPath,
595 args: [mockAgentPath],
596 cwd: input.cwd,
597 env: { T3_ACP_ANTIGRAVITY: "1" },
598 },
599 }).pipe(
600 Effect.provideService(SecureRandom.SecureRandom, crypto),
601 Effect.map((runtime): RuntimeService => ({
602 ...runtime,
603 handleSessionUpdate: (handler) =>
604 Effect.sync(() => {
605 sessionUpdate = handler;
606 }).pipe(Effect.andThen(runtime.handleSessionUpdate(handler))),
607 // agy_acp_server 1.3/1.4 without a permission prompt: the call is
608 // titled "Running <tool>" and completes with the step's summary
609 // in place of the MCP result.
610 prompt: () =>
611 Effect.gen(function* () {
612 const send = (update: EffectAcpSchema.SessionUpdate) =>
613 sessionUpdate === undefined
614 ? Effect.die("the adapter must handle session updates")
615 : sessionUpdate(
616 normalizeAntigravitySessionUpdate({
617 sessionId: "mock-session-1",
618 update,
619 }),
620 );
621 const calls = [
622 ["published", report],
623 ["unpublished", { ...report, html: "<p>Costs</p>" }],
624 ["failed", failed],
625 ["error", errored],
626 ] as const;
627 for (const [toolCallId, rawInput] of calls) {
628 yield* send({
629 sessionUpdate: "tool_call",
630 toolCallId,
631 title: "Running html_render",
632 kind: "other",
633 status: "in_progress",
634 rawInput,
635 });
636 }
637 yield* mcpSessions.recordHtmlRender(threadId, report, page("report"));
638 yield* mcpSessions.recordHtmlRender(threadId, failed, page("failed"));
639 yield* mcpSessions.recordHtmlRender(threadId, errored, page("error"));
640 for (const [toolCallId, status] of [
641 ["unpublished", "completed"],
642 ["failed", "failed"],
643 ["published", "completed"],
644 ["published", "completed"],
645 ] as const) {
646 yield* send({
647 sessionUpdate: "tool_call_update",
648 toolCallId,
649 status,
650 rawOutput: `Summary of ${toolCallId}`,
651 });
652 }
653 yield* send({
654 sessionUpdate: "tool_call_update",
655 toolCallId: "error",
656 status: "completed",
657 rawOutput: { result: { isError: true, content: [] } },
658 });
659 for (const [name, rawInput] of [
660 ["failed", failed],
661 ["error", errored],
662 ] as const) {
663 const toolCallId = `${name}-retry`;
664 yield* send({
665 sessionUpdate: "tool_call",
666 toolCallId,
667 title: "Running html_render",
668 kind: "other",
669 status: "in_progress",
670 rawInput,
671 });
672 yield* mcpSessions.recordHtmlRender(threadId, rawInput, page(toolCallId));
673 yield* send({
674 sessionUpdate: "tool_call_update",
675 toolCallId: name,
676 status: name === "failed" ? "failed" : "completed",
677 rawOutput:
678 name === "failed"
679 ? "Summary of failed"
680 : { result: { isError: true, content: [] } },
681 });
682 yield* send({
683 sessionUpdate: "tool_call_update",
684 toolCallId,
685 status: "completed",
686 rawOutput: `Summary of ${toolCallId}`,
687 });
688 }
689 yield* mcpSessions.recordHtmlRender(threadId, report, page("replacement"));
690 yield* send({
691 sessionUpdate: "tool_call_update",
692 toolCallId: "published",
693 status: "completed",
694 rawOutput: { htmlRender: page("replacement") },
695 });
696 yield* send({
697 sessionUpdate: "tool_call_update",
698 toolCallId: "published",
699 status: "completed",
700 rawOutput: "Summary of published",
701 });
702 return { stopReason: "end_turn" as const };
703 }),
704 })),
705 ),
706 withProcess: (_stop, task) => task,
707 defaultModel: Effect.undefined,
708 });
709 const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
710 runtimeMode: "full-access",
711 interactionMode: "default",
712 cwd: process.cwd(),
713 });
714 const modelSelection = { instanceId, model: "gemini-test-low" } as const;
715 const session = yield* adapter.openSession({
716 threadId,
717 providerSessionId: ProviderSessionId.make("provider-session-antigravity-html-render"),
718 modelSelection,
719 runtimePolicy,
720 });
721 const providerThread = yield* session.ensureThread({
722 threadId,
723 modelSelection,
724 runtimePolicy,
725 });
726 const now = yield* DateTime.now;
727 yield* session.startTurn({
728 appThread: {
729 createdBy: "user",
730 creationSource: "web",
731 id: threadId,
732 projectId: ProjectId.make("project-antigravity-html-render"),
733 title: "Antigravity html_render",
734 providerInstanceId: instanceId,
735 modelSelection,
736 runtimeMode: "full-access",
737 interactionMode: "default",
738 branch: null,
739 worktreePath: process.cwd(),
740 activeProviderThreadId: providerThread.id,
741 lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: threadId },
742 forkedFrom: null,
743 createdAt: now,
744 updatedAt: now,
745 archivedAt: null,
746 settledOverride: null,
747 settledAt: null,
748 lastVisitedAt: null,
749 deletedAt: null,
750 },
751 threadId,
752 runId: RunId.make("run-antigravity-html-render"),
753 runOrdinal: 1,
754 providerTurnOrdinal: 1,
755 attemptId: RunAttemptId.make("attempt-antigravity-html-render"),
756 rootNodeId: NodeId.make("node-antigravity-html-render"),
757 providerThread,
758 message: {
759 createdBy: "user",
760 creationSource: "web",
761 messageId: MessageId.make("message-antigravity-html-render"),
762 text: "chart the revenue",
763 attachments: [],
764 },
765 modelSelection,
766 runtimePolicy,
767 });
768 const events = Array.from(
769 yield* session.events.pipe(
770 Stream.takeUntil((event) => event.type === "turn.terminal"),
771 Stream.runCollect,
772 ),
773 );
774 const tool = (nativeId: string) => {
775 const event = events.findLast(
776 (event) =>
777 event.type === "turn_item.updated" &&
778 event.turnItem.nativeItemRef?.nativeId === nativeId,
779 );
780 if (event?.type !== "turn_item.updated" || event.turnItem.type !== "dynamic_tool") {
781 return undefined;
782 }
783 const item = event.turnItem;
784 return {
785 status: item.status,
786 toolName: item.toolName,
787 page: htmlRenderFromToolItem(item),
788 output: item.output,
789 };
790 };
791​
792 assert.deepEqual(tool("published"), {
793 status: "completed",
794 toolName: "t3-code.html_render",
795 page: page("replacement"),
796 output: { htmlRender: page("replacement") },
797 });
798 assert.deepEqual(tool("unpublished"), {
799 status: "completed",
800 toolName: "t3-code.html_render",
801 page: undefined,
802 output: "Summary of unpublished",
803 });
804 assert.deepEqual(tool("failed"), {
805 status: "failed",
806 toolName: "t3-code.html_render",
807 page: undefined,
808 output: "Summary of failed",
809 });
810 assert.deepEqual(tool("error"), {
811 status: "completed",
812 toolName: "t3-code.html_render",
813 page: undefined,
814 output: [],
815 });
816 assert.deepEqual(
817 events.flatMap((event) =>
818 event.type === "turn_item.updated" &&
819 event.turnItem.type === "dynamic_tool" &&
820 event.turnItem.nativeItemRef?.nativeId === "published" &&
821 event.turnItem.status === "completed"
822 ? [htmlRenderFromToolItem(event.turnItem)]
823 : [],
824 ),
825 [page("report"), page("report"), page("replacement"), page("replacement")],
826 );
827 for (const name of ["failed-retry", "error-retry"]) {
828 assert.deepEqual(tool(name), {
829 status: "completed",
830 toolName: "t3-code.html_render",
831 page: page(name),
832 output: { htmlRender: page(name) },
833 });
834 }
835 assert.strictEqual(yield* mcpSessions.takeHtmlRender(threadId, report), undefined);
836 assert.strictEqual(yield* mcpSessions.takeHtmlRender(threadId, failed), undefined);
837 assert.strictEqual(yield* mcpSessions.takeHtmlRender(threadId, errored), undefined);
838 }).pipe(Effect.provide(layerSession), Effect.scoped),
839 );
840});
841​
842describe("AntigravityAdapterV2 subagent reads", () => {
843 it.effect("completes a read T3 served when the agent later fails it with a step error", () =>
844 Effect.gen(function* () {
845 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
846 const fileSystem = yield* FileSystem.FileSystem;
847 const path = yield* Path.Path;
848 const crypto = yield* SecureRandom.SecureRandom;
849 const mockAgentPath = yield* path.fromFileUrl(
850 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
851 );
852 const workspace = yield* fileSystem.makeTempDirectoryScoped({
853 prefix: "t3-antigravity-reads-",
854 });
855 const outside = yield* fileSystem.makeTempDirectoryScoped({
856 prefix: "t3-antigravity-reads-outside-",
857 });
858 const served = path.join(workspace, "served.ts");
859 yield* fileSystem.writeFileString(served, "export {};");
860 const denied = path.join(outside, "secret.txt");
861 yield* fileSystem.writeFileString(denied, "secret");
862 const unseen = path.join(workspace, "unseen.ts");
863 const retried = path.join(workspace, "retried.ts");
864 const stepError = 'Model produced invalid output. ("model output error")';
865 type RuntimeService = AcpSessionRuntime.AcpSessionRuntime["Service"];
866 let sessionUpdate: Parameters<RuntimeService["handleSessionUpdate"]>[0] | undefined;
867 let readTextFile: Parameters<RuntimeService["handleReadTextFile"]>[0] | undefined;
868 const instanceId = ProviderInstanceId.make("antigravity-subagent-reads-test");
869 const threadId = ThreadId.make("thread-antigravity-subagent-reads");
870 const adapter = yield* makeAntigravityAdapterV2({
871 instanceId,
872 selfInvocation: yield* resolveSelfInvocation(),
873 makeRuntime: (input) =>
874 makeAntigravityAcpRuntime({
875 ...input,
876 childProcessSpawner,
877 spawn: {
878 command: process.execPath,
879 args: [mockAgentPath],
880 cwd: input.cwd,
881 env: { T3_ACP_ANTIGRAVITY: "1" },
882 },
883 }).pipe(
884 Effect.provideService(SecureRandom.SecureRandom, crypto),
885 Effect.map((runtime): RuntimeService => ({
886 ...runtime,
887 handleSessionUpdate: (handler) =>
888 Effect.sync(() => {
889 sessionUpdate = handler;
890 }).pipe(Effect.andThen(runtime.handleSessionUpdate(handler))),
891 handleReadTextFile: (handler) =>
892 Effect.sync(() => {
893 readTextFile = handler;
894 }).pipe(Effect.andThen(runtime.handleReadTextFile(handler))),
895 // agy_acp_server announces a subagent's client_view_file on the parent session,
896 // never completes it, then fails every open call with the next step error.
897 prompt: () =>
898 Effect.gen(function* () {
899 if (sessionUpdate === undefined || readTextFile === undefined) {
900 return yield* Effect.die("the adapter must handle updates and reads");
901 }
902 const send = sessionUpdate;
903 const handleRead = readTextFile;
904 const update = (update: EffectAcpSchema.SessionUpdate) =>
905 send(
906 normalizeAntigravitySessionUpdate({ sessionId: "mock-session-1", update }),
907 );
908 // The agent's own view_file never reaches T3, so it must not take a served read.
909 const reads = [
910 ["call_native", served, "Running view_file"],
911 ["call_served", served, "Running client_view_file"],
912 ["call_missing", retried, "Running client_view_file"],
913 ["call_retried", retried, "Running client_view_file"],
914 ["call_denied", denied, "Running client_view_file"],
915 ["call_unseen", unseen, "Running client_view_file"],
916 ] as const;
917 for (const [toolCallId, target, title] of reads) {
918 yield* update({
919 sessionUpdate: "tool_call",
920 toolCallId,
921 title,
922 kind: "read",
923 status: "in_progress",
924 rawInput: { absolute_path: target },
925 locations: [{ path: target }],
926 });
927 }
928 const serve = (target: string) =>
929 handleRead(
930 { sessionId: "subagent-conversation", path: target },
931 { requestId: `read-${target}`, method: "fs/read_text_file" },
932 ).pipe(Effect.exit);
933 yield* serve(served);
934 yield* serve(retried);
935 yield* fileSystem.writeFileString(retried, "export {};").pipe(Effect.orDie);
936 yield* serve(retried);
937 yield* serve(denied);
938 for (const [toolCallId] of reads) {
939 yield* update({
940 sessionUpdate: "tool_call_update",
941 toolCallId,
942 status: "failed",
943 rawOutput: stepError,
944 });
945 }
946 return { stopReason: "end_turn" as const };
947 }),
948 })),
949 ),
950 withProcess: (_stop, task) => task,
951 defaultModel: Effect.undefined,
952 });
953 const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
954 runtimeMode: "full-access",
955 interactionMode: "default",
956 cwd: workspace,
957 });
958 const modelSelection = { instanceId, model: "gemini-test-low" } as const;
959 const session = yield* adapter.openSession({
960 threadId,
961 providerSessionId: ProviderSessionId.make("provider-session-antigravity-subagent-reads"),
962 modelSelection,
963 runtimePolicy,
964 });
965 const providerThread = yield* session.ensureThread({
966 threadId,
967 modelSelection,
968 runtimePolicy,
969 });
970 const now = yield* DateTime.now;
971 yield* session.startTurn({
972 appThread: {
973 createdBy: "user",
974 creationSource: "web",
975 id: threadId,
976 projectId: ProjectId.make("project-antigravity-subagent-reads"),
977 title: "Antigravity subagent reads",
978 providerInstanceId: instanceId,
979 modelSelection,
980 runtimeMode: "full-access",
981 interactionMode: "default",
982 branch: null,
983 worktreePath: workspace,
984 activeProviderThreadId: providerThread.id,
985 lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: threadId },
986 forkedFrom: null,
987 createdAt: now,
988 updatedAt: now,
989 archivedAt: null,
990 settledOverride: null,
991 settledAt: null,
992 lastVisitedAt: null,
993 deletedAt: null,
994 },
995 threadId,
996 runId: RunId.make("run-antigravity-subagent-reads"),
997 runOrdinal: 1,
998 providerTurnOrdinal: 1,
999 attemptId: RunAttemptId.make("attempt-antigravity-subagent-reads"),
1000 rootNodeId: NodeId.make("node-antigravity-subagent-reads"),
1001 providerThread,
1002 message: {
1003 createdBy: "user",
1004 creationSource: "web",
1005 messageId: MessageId.make("message-antigravity-subagent-reads"),
1006 text: "review the codebase",
1007 attachments: [],
1008 },
1009 modelSelection,
1010 runtimePolicy,
1011 });
1012 const events = Array.from(
1013 yield* session.events.pipe(
1014 Stream.takeUntil((event) => event.type === "turn.terminal"),
1015 Stream.runCollect,
1016 ),
1017 );
1018 const read = (nativeId: string) => {
1019 const event = events.findLast(
1020 (event) =>
1021 event.type === "turn_item.updated" &&
1022 event.turnItem.nativeItemRef?.nativeId === nativeId,
1023 );
1024 return event?.type === "turn_item.updated" && event.turnItem.type === "dynamic_tool"
1025 ? { status: event.turnItem.status, output: event.turnItem.output }
1026 : undefined;
1027 };
1028​
1029 const firstEvent = (nativeId: string, status: string) =>
1030 events.findIndex(
1031 (event) =>
1032 event.type === "turn_item.updated" &&
1033 event.turnItem.nativeItemRef?.nativeId === nativeId &&
1034 event.turnItem.status === status,
1035 );
1036 // T3 completes a served read itself instead of leaving it running until the flush.
1037 assert.isAbove(firstEvent("call_served", "completed"), -1);
1038 assert.isBelow(firstEvent("call_served", "completed"), firstEvent("call_native", "failed"));
1039 const failed = { status: "failed" as const, output: stepError };
1040 assert.deepEqual(read("call_served"), { status: "completed", output: undefined });
1041 assert.deepEqual(read("call_retried"), { status: "completed", output: undefined });
1042 assert.deepEqual(read("call_native"), failed);
1043 assert.deepEqual(read("call_missing"), failed);
1044 assert.deepEqual(read("call_denied"), failed);
1045 assert.deepEqual(read("call_unseen"), failed);
1046 }).pipe(Effect.provide(layerSession), Effect.scoped),
1047 );
1048});
1049​
1050describe("AntigravityAdapterV2 runtime mode ceiling", () => {
1051 const modesSetFor = (pickedMode: string) =>
1052 Effect.gen(function* () {
1053 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
1054 const path = yield* Path.Path;
1055 const crypto = yield* SecureRandom.SecureRandom;
1056 const mockAgentPath = yield* path.fromFileUrl(
1057 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
1058 );
1059 type RuntimeService = AcpSessionRuntime.AcpSessionRuntime["Service"];
1060 const modesSet: Array<string> = [];
1061 const instanceId = ProviderInstanceId.make("antigravity-ceiling-test");
1062 const adapter = yield* makeAntigravityAdapterV2({
1063 instanceId,
1064 selfInvocation: yield* resolveSelfInvocation(),
1065 makeRuntime: (input) =>
1066 makeAntigravityAcpRuntime({
1067 ...input,
1068 childProcessSpawner,
1069 spawn: {
1070 command: process.execPath,
1071 args: [mockAgentPath],
1072 cwd: input.cwd,
1073 env: { T3_ACP_ANTIGRAVITY: "1" },
1074 },
1075 }).pipe(
1076 Effect.provideService(SecureRandom.SecureRandom, crypto),
1077 Effect.map((runtime): RuntimeService => ({
1078 ...runtime,
1079 setMode: (modeId) =>
1080 Effect.sync(() => {
1081 modesSet.push(modeId);
1082 }).pipe(Effect.andThen(runtime.setMode(modeId))),
1083 })),
1084 ),
1085 withProcess: (_stop, task) => task,
1086 defaultModel: Effect.undefined,
1087 });
1088 const threadId = ThreadId.make("thread-antigravity-ceiling");
1089 yield* adapter.openSession({
1090 threadId,
1091 providerSessionId: ProviderSessionId.make("provider-session-antigravity-ceiling"),
1092 modelSelection: {
1093 instanceId,
1094 model: "gemini-test-low",
1095 options: [{ id: "_t3/session-mode", value: pickedMode }],
1096 },
1097 runtimePolicy: ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
1098 runtimeMode: "approval-required",
1099 interactionMode: "default",
1100 cwd: process.cwd(),
1101 maxRuntimeMode: "auto-accept-edits",
1102 }),
1103 });
1104 return modesSet;
1105 }).pipe(Effect.provide(layerSession), Effect.scoped);
1106​
1107 it.effect("does not apply a picked session mode above the policy ceiling", () =>
1108 Effect.gen(function* () {
1109 assert.deepEqual(yield* modesSetFor("yolo"), ["default"]);
1110 // A mode under the ceiling is still the user's to pick.
1111 assert.deepEqual(yield* modesSetFor("auto_edit"), ["default", "auto_edit"]);
1112 }),
1113 );
1114});
1115​
1116describe("AntigravityAdapterV2 session system prompt", () => {
1117 it.effect.each(["1.4.5", "1.4.1"])(
1118 "carries PR linking once in the %s session setup or once per turn",
1119 (agentVersion) =>
1120 Effect.gen(function* () {
1121 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
1122 const fileSystem = yield* FileSystem.FileSystem;
1123 const path = yield* Path.Path;
1124 const crypto = yield* SecureRandom.SecureRandom;
1125 const mockAgentPath = yield* path.fromFileUrl(
1126 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
1127 );
1128 const logs = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-antigravity-log-" });
1129 const requestLog = path.join(logs, "requests.ndjson");
1130 type RuntimeService = AcpSessionRuntime.AcpSessionRuntime["Service"];
1131 const promptReceived = yield* Deferred.make<Parameters<RuntimeService["prompt"]>[0]>();
1132 const instanceId = ProviderInstanceId.make("antigravity-system-prompt-test");
1133 const adapter = yield* makeAntigravityAdapterV2({
1134 instanceId,
1135 selfInvocation: yield* resolveSelfInvocation(),
1136 makeRuntime: (input) =>
1137 makeAntigravityAcpRuntime({
1138 ...input,
1139 childProcessSpawner,
1140 spawn: {
1141 command: process.execPath,
1142 args: [mockAgentPath],
1143 cwd: input.cwd,
1144 env: {
1145 T3_ACP_ANTIGRAVITY: "1",
1146 T3_ACP_AGENT_VERSION: agentVersion,
1147 T3_ACP_REQUEST_LOG_PATH: requestLog,
1148 T3_ACP_HANG_PROMPT_FOREVER: "1",
1149 },
1150 },
1151 }).pipe(
1152 Effect.provideService(SecureRandom.SecureRandom, crypto),
1153 Effect.map((runtime): RuntimeService => ({
1154 ...runtime,
1155 prompt: (request) =>
1156 Deferred.succeed(promptReceived, request).pipe(
1157 Effect.andThen(runtime.prompt(request)),
1158 ),
1159 })),
1160 ),
1161 withProcess: (_stop, task) => task,
1162 defaultModel: Effect.undefined,
1163 });
1164 const workspace = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-ag-sp-" });
1165 const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
1166 runtimeMode: "full-access",
1167 interactionMode: "default",
1168 cwd: workspace,
1169 });
1170 const threadId = ThreadId.make("thread-antigravity-system-prompt");
1171 const modelSelection = { instanceId, model: "gemini-test-low" } as const;
1172 const session = yield* adapter.openSession({
1173 threadId,
1174 providerSessionId: ProviderSessionId.make("provider-session-antigravity-system-prompt"),
1175 modelSelection,
1176 runtimePolicy,
1177 });
1178 const providerThread = yield* session.ensureThread({
1179 threadId,
1180 modelSelection,
1181 runtimePolicy,
1182 });
1183 const now = yield* DateTime.now;
1184 yield* session
1185 .startTurn({
1186 appThread: {
1187 createdBy: "user",
1188 creationSource: "web",
1189 id: threadId,
1190 projectId: ProjectId.make("project-antigravity-system-prompt"),
1191 title: "Antigravity system prompt",
1192 providerInstanceId: instanceId,
1193 modelSelection,
1194 runtimeMode: "full-access",
1195 interactionMode: "default",
1196 branch: null,
1197 worktreePath: null,
1198 activeProviderThreadId: providerThread.id,
1199 lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: threadId },
1200 forkedFrom: null,
1201 createdAt: now,
1202 updatedAt: now,
1203 archivedAt: null,
1204 settledOverride: null,
1205 settledAt: null,
1206 lastVisitedAt: null,
1207 deletedAt: null,
1208 },
1209 threadId,
1210 runId: RunId.make("run-antigravity-system-prompt"),
1211 runOrdinal: 1,
1212 providerTurnOrdinal: 1,
1213 attemptId: RunAttemptId.make("attempt-antigravity-system-prompt"),
1214 rootNodeId: NodeId.make("node-antigravity-system-prompt"),
1215 providerThread,
1216 message: {
1217 createdBy: "user",
1218 creationSource: "web",
1219 messageId: MessageId.make("message-antigravity-system-prompt"),
1220 text: "open a PR",
1221 attachments: [],
1222 },
1223 modelSelection,
1224 runtimePolicy,
1225 })
1226 .pipe(Effect.forkScoped);
1227 const sent = JSON.stringify((yield* Deferred.await(promptReceived)).prompt);
1228 const sessionNew = (yield* fileSystem.readFileString(requestLog))
1229 .trim()
1230 .split("\n")
1231 .map((line) => JSON.parse(line) as { method?: string; params?: { _meta?: unknown } })
1232 .find((request) => request.method === "session/new");
1233 const append = JSON.stringify(sessionNew?.params?._meta ?? null);
1234 // Identity stays per turn either way.
1235 assert.include(sent, "through the Antigravity harness, as gemini-test-low");
1236 if (agentVersion === "1.4.5") {
1237 assert.include(append, "pull_request_linking");
1238 assert.notInclude(sent, "<pull_request_linking>");
1239 } else {
1240 assert.notInclude(append, "systemPrompt");
1241 assert.include(sent, "<pull_request_linking>");
1243 }).pipe(Effect.provide(layerSession), Effect.scoped),
1244 );
1245});