Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,31 @@
- `packages/shared`: Shared runtime utilities consumed by both server and client applications. Uses explicit subpath exports (e.g. `@t3tools/shared/git`) — no barrel index.
- `packages/client-runtime`: Shared runtime package for sharing client code across web and mobile.

## Antigravity provider

The `antigravity` driver wraps the Antigravity CLI (`agy`), which has **no native agent
protocol**. `apps/server/src/provider/acp/antigravity/agyBridge.ts` presents the ACP surface
`AcpSessionRuntime` needs and runs each turn through `agy --print`, shipping as hidden
subcommands of the server binary (`t3 agy-acp`, `t3 agy-hook`).

A live event stream is reconstructed from two documented Antigravity sources that correlate on
step index (`stepIdx` in a hook == `step_index` in the transcript):

- **Hooks** (`PreToolUse`/`PostToolUse`/`Stop`) give tool name, arguments, and status. The bridge
registers them via a throwaway `--add-dir` workspace so nothing is written into the user's repo.
Both the session cwd and that hook workspace must be passed — `--add-dir` replaces the workspace
rather than adding to cwd, so passing only the hook dir hides the project from the agent.
- **The trajectory transcript** (`transcript.jsonl`) gives assistant text and real tool output as
the turn runs. Its record format is undocumented, so parsing degrades to emitting nothing.

Known constraints, all inherent to print mode:

- Tools are auto-approved (`--dangerously-skip-permissions`); no approval flow is possible.
- No image attachments, and no session modes.
- The model binds when the bridge spawns `agy`, so `sessionModelSwitch` is `"unsupported"`.
- Any `agy` subcommand spawned for a probe must set `stdin: "ignore"`. `agy` starts a language
server and will not emit output while stdin stays open, so the default `"pipe"` hangs it.

## Reference Repos

- Open-source Codex repo: https://github.com/openai/codex
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as CliError from "effect/unstable/cli/CliError";

import * as NetService from "@t3tools/shared/Net";
import packageJson from "../package.json" with { type: "json" };
import { agyAcpCommand, agyHookCommand } from "./cli/agy.ts";
import { authCommand } from "./cli/auth.ts";
import { connectCommand } from "./cli/connect.ts";
import { hasCloudPublicConfig } from "./cloud/publicConfig.ts";
Expand Down Expand Up @@ -49,6 +50,8 @@ export const makeCli = ({ cloudEnabled = hasCloudPublicConfig } = {}) =>
authCommand,
projectCommand,
serviceCommand,
agyAcpCommand,
agyHookCommand,
cloudEnabled ? connectCommand : connectUnavailableCommand,
]),
);
Expand Down
31 changes: 31 additions & 0 deletions apps/server/src/cli/agy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Hidden CLI entry points for the Antigravity ACP bridge.
*
* Both are internal plumbing rather than user-facing commands: the provider
* spawns `t3 agy-acp` as an ACP agent, and that bridge registers `t3 agy-hook
* <event>` as Antigravity's tool-lifecycle hook. Shipping them as subcommands
* of the server binary keeps the bridge inside the same bundle.
*
* @module cli/agy
*/
import * as Effect from "effect/Effect";
import { Command, Flag } from "effect/unstable/cli";

import { runAgyBridge, runAgyHook } from "../provider/acp/antigravity/agyBridge.ts";

export const agyAcpCommand = Command.make("agy-acp").pipe(
Command.withDescription("Run the Antigravity ACP compatibility bridge over stdio."),
Command.withHidden,
Command.withHandler(() => Effect.promise(() => runAgyBridge())),
);

export const agyHookCommand = Command.make("agy-hook", {
event: Flag.string("event").pipe(
Flag.withDescription("Antigravity hook event name."),
Flag.withDefault("unknown"),
),
}).pipe(
Command.withDescription("Handle one Antigravity tool-lifecycle hook event."),
Command.withHidden,
Command.withHandler(({ event }) => Effect.promise(() => runAgyHook(event))),
);
177 changes: 177 additions & 0 deletions apps/server/src/provider/Drivers/AntigravityDriver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/**
* AntigravityDriver — built-in driver for the Antigravity CLI (`agy`).
*
* Sessions run through T3 Code's bundled ACP bridge rather than a native agent
* protocol; see `provider/acp/antigravity/agyBridge.ts`.
*
* @module Drivers/AntigravityDriver
*/
import { AntigravitySettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";
import * as Duration from "effect/Duration";
import * as Crypto from "effect/Crypto";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Schema from "effect/Schema";
import { HttpClient } from "effect/unstable/http";
import { ChildProcessSpawner } from "effect/unstable/process";

import { ServerConfig } from "../../config.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { makeAntigravityTextGeneration } from "../../textGeneration/AntigravityTextGeneration.ts";
import { ProviderDriverError } from "../Errors.ts";
import { makeAntigravityAdapter } from "../Layers/AntigravityAdapter.ts";
import {
buildInitialAntigravityProviderSnapshot,
checkAntigravityProviderStatus,
enrichAntigravitySnapshot,
} from "../Layers/AntigravityProvider.ts";
import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";
import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
import {
defaultProviderContinuationIdentity,
type ProviderDriver,
type ProviderInstance,
} from "../ProviderDriver.ts";
import type { ServerProviderDraft } from "../providerSnapshot.ts";
import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
import {
makeManualOnlyProviderMaintenanceCapabilities,
makeStaticProviderMaintenanceResolver,
resolveProviderMaintenanceCapabilitiesEffect,
} from "../providerMaintenance.ts";
import {
haveProviderSnapshotSettingsChanged,
makeProviderSnapshotSettingsSource,
type ProviderSnapshotSettings,
} from "../providerUpdateSettings.ts";

const decodeAntigravitySettings = Schema.decodeSync(AntigravitySettings);

const DRIVER_KIND = ProviderDriverKind.make("antigravity");
const SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5);
// Antigravity ships its own installer and updater (`agy update`), so T3 Code
// never manages the binary itself.
const UPDATE = makeStaticProviderMaintenanceResolver(
makeManualOnlyProviderMaintenanceCapabilities({
provider: DRIVER_KIND,
packageName: null,
}),
);

export type AntigravityDriverEnv =
| ChildProcessSpawner.ChildProcessSpawner
| Crypto.Crypto
| FileSystem.FileSystem
| HttpClient.HttpClient
| Path.Path
| ProviderEventLoggers
| ServerConfig
| ServerSettingsService;

const withInstanceIdentity =
(input: {
readonly instanceId: ProviderInstance["instanceId"];
readonly displayName: string | undefined;
readonly accentColor: string | undefined;
readonly continuationGroupKey: string;
}) =>
(snapshot: ServerProviderDraft): ServerProvider => ({
...snapshot,
instanceId: input.instanceId,
driver: DRIVER_KIND,
...(input.displayName ? { displayName: input.displayName } : {}),
...(input.accentColor ? { accentColor: input.accentColor } : {}),
continuation: { groupKey: input.continuationGroupKey },
});

export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityDriverEnv> = {
driverKind: DRIVER_KIND,
metadata: {
displayName: "Antigravity",
supportsMultipleInstances: true,
},
configSchema: AntigravitySettings,
defaultConfig: (): AntigravitySettings => decodeAntigravitySettings({}),
create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>
Effect.gen(function* () {
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const httpClient = yield* HttpClient.HttpClient;
const serverSettings = yield* ServerSettingsService;
const eventLoggers = yield* ProviderEventLoggers;
const processEnv = mergeProviderInstanceEnvironment(environment);
const continuationIdentity = defaultProviderContinuationIdentity({
driverKind: DRIVER_KIND,
instanceId,
});
const stampIdentity = withInstanceIdentity({
instanceId,
displayName,
accentColor,
continuationGroupKey: continuationIdentity.continuationKey,
});
const effectiveConfig = { ...config, enabled } satisfies AntigravitySettings;
const maintenanceCapabilities = yield* resolveProviderMaintenanceCapabilitiesEffect(UPDATE, {
binaryPath: effectiveConfig.binaryPath,
env: processEnv,
});

const adapter = yield* makeAntigravityAdapter(effectiveConfig, {
environment: processEnv,
...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}),
instanceId,
});
const textGeneration = yield* makeAntigravityTextGeneration(effectiveConfig, processEnv);

const checkProvider = checkAntigravityProviderStatus(effectiveConfig, processEnv).pipe(
Effect.map(stampIdentity),
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
);

const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
const snapshot = yield* makeManagedServerProvider<
ProviderSnapshotSettings<AntigravitySettings>
>({
maintenanceCapabilities,
getSettings: snapshotSettings.getSettings,
streamSettings: snapshotSettings.streamSettings,
haveSettingsChanged: haveProviderSnapshotSettingsChanged,
initialSnapshot: (settings) =>
buildInitialAntigravityProviderSnapshot(settings.provider).pipe(
Effect.map(stampIdentity),
),
checkProvider,
enrichSnapshot: ({ settings, snapshot: currentSnapshot, publishSnapshot }) =>
enrichAntigravitySnapshot({
snapshot: currentSnapshot,
maintenanceCapabilities,
enableProviderUpdateChecks: settings.enableProviderUpdateChecks,
publishSnapshot,
httpClient,
}),
refreshInterval: SNAPSHOT_REFRESH_INTERVAL,
}).pipe(
Effect.mapError(
(cause) =>
new ProviderDriverError({
driver: DRIVER_KIND,
instanceId,
detail: `Failed to build Antigravity snapshot: ${cause.message ?? String(cause)}`,
cause,
}),
),
);

return {
instanceId,
driverKind: DRIVER_KIND,
continuationIdentity,
displayName,
accentColor,
enabled,
snapshot,
adapter,
textGeneration,
} satisfies ProviderInstance;
}),
};
Loading
Loading