| author | |
| committer | |
| log | baf67b6e3accd48d510fd0d39a4e6b978a1eea80 |
| tree | 16f54313f76035e495878ff6ca735fc9ad2dd307 |
| parent | f25e4428961982e3935300cfd3c78f7d0fac4ee9 |
| signature | Signed by PGP key B5690EEEBB952194 |
10 files changed, 113 insertions(+), 10 deletions(-)
apps/server/src/provider/Drivers/AntigravityDriver.ts+4| ... | ... | @@ -233,6 +233,9 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD |
| 233 | 233 | if (event._tag === "EventStreamBarrier") { |
| 234 | 234 | return Deferred.succeed(event.acknowledge, undefined).pipe(Effect.asVoid); |
| 235 | 235 | } |
| 236 | if (event._tag === "ConfigOptionsUpdated") { | |
| 237 | return provider.onConfigOptionsUpdated(event.configOptions); | |
| 238 | } | |
| 236 | 239 | return event._tag === "AvailableCommandsUpdated" |
| 237 | 240 | ? provider.onAvailableCommands(event.availableCommands) |
| 238 | 241 | : Effect.void; |
| ... | ... | @@ -299,6 +302,7 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD |
| 299 | 302 | withProcess: authFlow.withProcess, |
| 300 | 303 | defaultModel, |
| 301 | 304 | onSessionStarted: provider.onSessionStarted, |
| 305 | onConfigOptionsUpdated: provider.onConfigOptionsUpdated, | |
| 302 | 306 | onAvailableCommands: provider.onAvailableCommands, |
| 303 | 307 | onAuthRequired: provider.onAuthRequired, |
| 304 | 308 | ...(loggers.native ? { nativeEventLogger: loggers.native } : {}), |
apps/server/src/provider/Layers/AntigravityAdapter.test.ts+8| ... | ... | @@ -297,6 +297,7 @@ it.layer(layer)("AntigravityAdapter", (it) => { |
| 297 | 297 | ); |
| 298 | 298 | const requestLog = path.join(cwd, "requests.ndjson"); |
| 299 | 299 | const commands: string[] = []; |
| 300 | const modelSelections: string[] = []; | |
| 300 | 301 | const observed: ProviderRuntimeEvent[] = []; |
| 301 | 302 | const completed = yield* Deferred.make<void>(); |
| 302 | 303 | const adapter = yield* makeAntigravityAdapter(decodeSettings({ enabled: true }), { |
| ... | ... | @@ -322,6 +323,11 @@ it.layer(layer)("AntigravityAdapter", (it) => { |
| 322 | 323 | Effect.sync(() => { |
| 323 | 324 | commands.push(...available.map((command) => command.name)); |
| 324 | 325 | }), |
| 326 | onConfigOptionsUpdated: (configOptions) => | |
| 327 | Effect.sync(() => { | |
| 328 | const model = configOptions.find((option) => option.category === "model"); | |
| 329 | if (model?.type === "select") modelSelections.push(model.currentValue); | |
| 330 | }), | |
| 325 | 331 | }); |
| 326 | 332 | yield* adapter.streamEvents.pipe( |
| 327 | 333 | Stream.runForEach((event) => |
| ... | ... | @@ -350,6 +356,8 @@ it.layer(layer)("AntigravityAdapter", (it) => { |
| 350 | 356 | yield* adapter.sendTurn({ threadId, input: "Reply with one short line." }); |
| 351 | 357 | yield* Deferred.await(completed); |
| 352 | 358 | expect(commands).toEqual(["plan", "logout", "plan", "logout"]); |
| 359 | expect(modelSelections.length).toBeGreaterThan(0); | |
| 360 | expect(modelSelections.every((model) => model === nativeAlternative)).toBe(true); | |
| 353 | 361 | expect( |
| 354 | 362 | observed |
| 355 | 363 | .filter((event) => event.type === "content.delta") |
apps/server/src/provider/Layers/AntigravityAdapter.ts+6| ... | ... | @@ -133,6 +133,9 @@ export interface AntigravityAdapterOptions { |
| 133 | 133 | commands: ReadonlyArray<EffectAcpSchema.AvailableCommand>, |
| 134 | 134 | cwd: string, |
| 135 | 135 | ) => Effect.Effect<void>; |
| 136 | readonly onConfigOptionsUpdated?: ( | |
| 137 | configOptions: ReadonlyArray<EffectAcpSchema.SessionConfigOption>, | |
| 138 | ) => Effect.Effect<void>; | |
| 136 | 139 | readonly onAuthRequired?: Effect.Effect<void>; |
| 137 | 140 | /** Model the provider default alias selects, when the account offers it. */ |
| 138 | 141 | readonly defaultModel?: Effect.Effect<string | undefined>; |
| ... | ... | @@ -503,6 +506,9 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi |
| 503 | 506 | case "AvailableCommandsUpdated": |
| 504 | 507 | yield* options.onAvailableCommands?.(event.availableCommands, context.cwd) ?? Effect.void; |
| 505 | 508 | return; |
| 509 | case "ConfigOptionsUpdated": | |
| 510 | yield* options.onConfigOptionsUpdated?.(event.configOptions) ?? Effect.void; | |
| 511 | return; | |
| 506 | 512 | case "ConnectionTerminated": |
| 507 | 513 | context.stopped = true; |
| 508 | 514 | context.disconnected = true; |
apps/server/src/provider/Layers/AntigravityProvider.test.ts+35-2| ... | ... | @@ -175,7 +175,6 @@ describe("Antigravity model catalog", () => { |
| 175 | 175 | |
| 176 | 176 | it("uses legacy session models only when model config is absent", () => { |
| 177 | 177 | const fromLegacy = buildAntigravityModelsFromSession({ |
| 178 | sessionId: "legacy-session", | |
| 179 | 178 | models: sessionSetupResult.models, |
| 180 | 179 | }); |
| 181 | 180 | expect(fromLegacy).toEqual(buildAntigravityModelsFromSession(sessionSetupResult)); |
| ... | ... | @@ -189,7 +188,6 @@ describe("Antigravity model catalog", () => { |
| 189 | 188 | |
| 190 | 189 | it("flattens native option groups without combining distinct model IDs", () => { |
| 191 | 190 | const models = buildAntigravityModelsFromSession({ |
| 192 | sessionId: "grouped-session", | |
| 193 | 191 | configOptions: [ |
| 194 | 192 | { |
| 195 | 193 | ...modelConfig, |
| ... | ... | @@ -356,7 +354,9 @@ it.layer(testLayer)("Antigravity provider snapshots", (it) => { |
| 356 | 354 | supportsTextGeneration: false, |
| 357 | 355 | }); |
| 358 | 356 | yield* harness.provider.onAvailableCommands(commands, "/workspace"); |
| 357 | yield* harness.provider.onConfigOptionsUpdated([modelConfig]); | |
| 359 | 358 | expect((yield* harness.provider.snapshotForCwd("/workspace")).slashCommands).toEqual([]); |
| 359 | expect((yield* harness.provider.snapshot.getSnapshot).models).toEqual([]); | |
| 360 | 360 | } |
| 361 | 361 | const refreshed = yield* harness.provider.snapshot.refresh; |
| 362 | 362 | expect(refreshed.auth.status).toBe("unauthenticated"); |
| ... | ... | @@ -365,6 +365,39 @@ it.layer(testLayer)("Antigravity provider snapshots", (it) => { |
| 365 | 365 | ), |
| 366 | 366 | ); |
| 367 | 367 | |
| 368 | it.effect("replaces live model choices and accepts an empty catalog", () => | |
| 369 | Effect.scoped( | |
| 370 | Effect.gen(function* () { | |
| 371 | const harness = yield* makeHarness(); | |
| 372 | yield* harness.initialize; | |
| 373 | yield* harness.provider.onSessionStarted(started, "/workspace"); | |
| 374 | yield* harness.provider.onAvailableCommands(commands, "/workspace"); | |
| 375 | const before = yield* harness.provider.snapshot.getSnapshot; | |
| 376 | const configOptions = [ | |
| 377 | { | |
| 378 | ...modelConfig, | |
| 379 | currentValue: "gemini-3.8-flash-high", | |
| 380 | options: modelOptions.slice(0, 3), | |
| 381 | }, | |
| 382 | ]; | |
| 383 | const nextSnapshot = yield* Stream.toPull( | |
| 384 | harness.provider.snapshot.streamChanges.pipe( | |
| 385 | Stream.filter((snapshot) => snapshot.models.length === 3), | |
| 386 | ), | |
| 387 | ); | |
| 388 | yield* harness.provider.onConfigOptionsUpdated(configOptions); | |
| 389 | expect((yield* nextSnapshot)[0]).toMatchObject({ | |
| 390 | models: buildAntigravityModelsFromSession({ configOptions }), | |
| 391 | auth: before.auth, | |
| 392 | workspaceSnapshots: before.workspaceSnapshots, | |
| 393 | slashCommands: commands, | |
| 394 | }); | |
| 395 | yield* harness.provider.onConfigOptionsUpdated([]); | |
| 396 | expect((yield* harness.provider.snapshot.getSnapshot).models).toEqual([]); | |
| 397 | }), | |
| 398 | ), | |
| 399 | ); | |
| 400 | ||
| 368 | 401 | it.effect("replaces one account's catalog instead of combining accounts", () => |
| 369 | 402 | Effect.scoped( |
| 370 | 403 | Effect.gen(function* () { |
apps/server/src/provider/Layers/AntigravityProvider.ts+15-1| ... | ... | @@ -37,7 +37,10 @@ const SIGN_IN_MESSAGE = "Sign in with Google to use Antigravity."; |
| 37 | 37 | const AUTH_UNCHECKED_MESSAGE = |
| 38 | 38 | "Antigravity is installed. Google account access is not checked yet."; |
| 39 | 39 | |
| 40 | type SessionSetupResult = AcpSessionRuntimeStartResult["sessionSetupResult"]; | |
| 40 | type SessionSetupResult = Pick< | |
| 41 | AcpSessionRuntimeStartResult["sessionSetupResult"], | |
| 42 | "configOptions" | "models" | |
| 43 | >; | |
| 41 | 44 | |
| 42 | 45 | /** Keep the native model IDs, including model-specific thinking levels. */ |
| 43 | 46 | export function buildAntigravityModelsFromSession( |
| ... | ... | @@ -299,6 +302,16 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func |
| 299 | 302 | }); |
| 300 | 303 | }); |
| 301 | 304 | |
| 305 | const onConfigOptionsUpdated = Effect.fn("AntigravityProvider.onConfigOptionsUpdated")(function* ( | |
| 306 | configOptions: ReadonlyArray<EffectAcpSchema.SessionConfigOption>, | |
| 307 | ) { | |
| 308 | const models = buildAntigravityModelsFromSession({ configOptions }); | |
| 309 | yield* SubscriptionRef.update(metadata, (state) => { | |
| 310 | if (state.draft.auth.status !== "authenticated") return state; | |
| 311 | return { ...state, draft: { ...state.draft, models } }; | |
| 312 | }); | |
| 313 | }); | |
| 314 | ||
| 302 | 315 | const onAvailableCommands = Effect.fn("AntigravityProvider.onAvailableCommands")(function* ( |
| 303 | 316 | commands: ReadonlyArray<EffectAcpSchema.AvailableCommand>, |
| 304 | 317 | cwd?: string, |
| ... | ... | @@ -373,6 +386,7 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func |
| 373 | 386 | return { |
| 374 | 387 | snapshot: { ...managed, getSnapshot }, |
| 375 | 388 | onSessionStarted, |
| 389 | onConfigOptionsUpdated, | |
| 376 | 390 | onAvailableCommands, |
| 377 | 391 | onSignedOut: clearAccountMetadata(), |
| 378 | 392 | onAuthRequired: clearAccountMetadata(), |
apps/server/src/provider/acp/AcpJsonRpcConnection.test.ts+18| ... | ... | @@ -69,11 +69,15 @@ describe("AcpSessionRuntime", () => { |
| 69 | 69 | expect(events.map((event) => event._tag)).toEqual([ |
| 70 | 70 | "AvailableCommandsUpdated", |
| 71 | 71 | "ModeChanged", |
| 72 | "ConfigOptionsUpdated", | |
| 72 | 73 | ]); |
| 73 | 74 | expect(events[0]).toMatchObject({ |
| 74 | 75 | availableCommands: [{ name: "plan", description: "Native command" }], |
| 75 | 76 | }); |
| 76 | 77 | expect(yield* runtime.getModeState).toMatchObject({ currentModeId: "code" }); |
| 78 | expect(events[2]).toMatchObject({ | |
| 79 | configOptions: yield* runtime.getConfigOptions, | |
| 80 | }); | |
| 77 | 81 | expect( |
| 78 | 82 | (yield* runtime.getConfigOptions).find((option) => option.category === "model"), |
| 79 | 83 | ).toMatchObject({ currentValue: "gpt-5.4" }); |
| ... | ... | @@ -81,6 +85,20 @@ describe("AcpSessionRuntime", () => { |
| 81 | 85 | ); |
| 82 | 86 | } |
| 83 | 87 | |
| 88 | it.effect("publishes model changes returned by a config request and live notifications", () => | |
| 89 | Effect.gen(function* () { | |
| 90 | const runtime = yield* AcpSessionRuntime.make(mockRuntimeOptions); | |
| 91 | yield* runtime.start(); | |
| 92 | const updates = yield* Stream.toPull( | |
| 93 | runtime.getEvents().pipe(Stream.filter((event) => event._tag === "ConfigOptionsUpdated")), | |
| 94 | ); | |
| 95 | const selected = yield* runtime.setConfigOption("model", "composer-2"); | |
| 96 | expect((yield* updates)[0]?.configOptions).toEqual(selected.configOptions); | |
| 97 | yield* runtime.request("_test/startup-metadata", {}); | |
| 98 | expect((yield* updates)[0]?.configOptions).toEqual(yield* runtime.getConfigOptions); | |
| 99 | }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), | |
| 100 | ); | |
| 101 | ||
| 84 | 102 | it.effect("awaits native resume instead of using the load replay idle fallback", () => |
| 85 | 103 | Effect.gen(function* () { |
| 86 | 104 | const resumeStarted = yield* Deferred.make<void>(); |
apps/server/src/provider/acp/AcpRuntimeModel.ts+13| ... | ... | @@ -90,6 +90,11 @@ export type AcpParsedSessionEvent = |
| 90 | 90 | readonly availableCommands: ReadonlyArray<EffectAcpSchema.AvailableCommand>; |
| 91 | 91 | readonly rawPayload: unknown; |
| 92 | 92 | } |
| 93 | | { | |
| 94 | readonly _tag: "ConfigOptionsUpdated"; | |
| 95 | readonly configOptions: ReadonlyArray<EffectAcpSchema.SessionConfigOption>; | |
| 96 | readonly rawPayload: unknown; | |
| 97 | } | |
| 93 | 98 | | { |
| 94 | 99 | readonly _tag: "AssistantItemStarted"; |
| 95 | 100 | readonly itemId: string; |
| ... | ... | @@ -788,6 +793,14 @@ export function parseSessionUpdateEvent(params: EffectAcpSchema.SessionNotificat |
| 788 | 793 | let modeId: string | undefined; |
| 789 | 794 | |
| 790 | 795 | switch (upd.sessionUpdate) { |
| 796 | case "config_option_update": { | |
| 797 | events.push({ | |
| 798 | _tag: "ConfigOptionsUpdated", | |
| 799 | configOptions: upd.configOptions, | |
| 800 | rawPayload: params, | |
| 801 | }); | |
| 802 | break; | |
| 803 | } | |
| 791 | 804 | case "available_commands_update": { |
| 792 | 805 | events.push({ |
| 793 | 806 | _tag: "AvailableCommandsUpdated", |
apps/server/src/provider/acp/AcpSessionRuntime.ts+11-7| ... | ... | @@ -615,13 +615,17 @@ export const make = ( |
| 615 | 615 | }); |
| 616 | 616 | }); |
| 617 | 617 | |
| 618 | const updateConfigOptions = ( | |
| 619 | response: | |
| 620 | | EffectAcpSchema.SetSessionConfigOptionResponse | |
| 621 | | EffectAcpSchema.LoadSessionResponse | |
| 622 | | EffectAcpSchema.NewSessionResponse | |
| 623 | | EffectAcpSchema.ResumeSessionResponse, | |
| 624 | ): Effect.Effect<void> => Ref.set(configOptionsRef, sessionConfigOptionsFromSetup(response)); | |
| 618 | const updateConfigOptions = Effect.fn("AcpSessionRuntime.updateConfigOptions")(function* ( | |
| 619 | response: EffectAcpSchema.SetSessionConfigOptionResponse, | |
| 620 | ) { | |
| 621 | const configOptions = sessionConfigOptionsFromSetup(response); | |
| 622 | yield* Ref.set(configOptionsRef, configOptions); | |
| 623 | yield* Queue.offer(eventQueue, { | |
| 624 | _tag: "ConfigOptionsUpdated", | |
| 625 | configOptions, | |
| 626 | rawPayload: response, | |
| 627 | }); | |
| 628 | }); | |
| 625 | 629 | |
| 626 | 630 | const updateCurrentModeId = (modeId: string): Effect.Effect<void> => |
| 627 | 631 | Ref.update(modeStateRef, (current) => |
docs/internals/providers.md+2| ... | ... | @@ -168,6 +168,8 @@ Account access starts unknown and becomes authenticated after successful session |
| 168 | 168 | including an explicit model refresh. |
| 169 | 169 | The [provider snapshot][antigravity-provider] takes models and commands from setup and native |
| 170 | 170 | updates. It preserves returned Gemini model IDs, labels, order, and thinking-level choices. |
| 171 | ACP `config_option_update` notifications and `session/set_config_option` responses replace | |
| 172 | the instance's model catalog. Child session notifications do not change the root catalog. | |
| 171 | 173 | The registry treats a successful empty catalog as authoritative and clears cached metadata |
| 172 | 174 | after sign-out. It must not retain a previous account's models. Cached models do not prove |
| 173 | 175 | current access. The auth response does not supply an email, plan tier, or reliable quota. |
docs/user/providers-antigravity.md+1| ... | ... | @@ -107,6 +107,7 @@ installations. Clear **Binary path** to use managed installation controls. |
| 107 | 107 | The current official ACP exposes Gemini models only. T3 Code uses the model IDs and names |
| 108 | 108 | returned for your account, including any model choices with different thinking levels. Models |
| 109 | 109 | available in other Antigravity apps might not be available through this agent. |
| 110 | The model picker updates when a running session reports new model choices. | |
| 110 | 111 | |
| 111 | 112 | New threads use Gemini 3.8 Flash (High) when your account offers it. Older Gemini generations |
| 112 | 113 | stay available under **Legacy models** in the picker. |