Skip to content
Merged
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
10 changes: 8 additions & 2 deletions src/cli/commands/profile.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Command } from "commander";
import {
baseConfig,
extractProfileSnapshot,
getProfileSnapshot,
listProfiles,
Expand Down Expand Up @@ -119,7 +120,12 @@ export function applyProfileAction(
}

if (action === "save") {
const snapshot = extractProfileSnapshot(resolveEffectiveConfig(config));
// Snapshot the explicit target, never the active profile. Resolving
// without a name would fall back to activeProfile and stamp the other
// profile's agents into this one. A new name starts from the base setup.
const existing = getProfileSnapshot(config, name);
const source = existing === undefined ? baseConfig(config) : resolveEffectiveConfig(config, name);
const snapshot = extractProfileSnapshot(source);
const profiles = { ...(config.profiles ?? {}), [name]: snapshot };
const next: RunAgentConfig = { ...config, profiles };
return {
Expand Down Expand Up @@ -237,7 +243,7 @@ export function registerProfileCommand(program: Command): void {

const named: ReadonlyArray<{ action: Exclude<ProfileAction, "list">; description: string }> = [
{ action: "show", description: "print a saved profile" },
{ action: "save", description: "snapshot the current setup as a profile" },
{ action: "save", description: "snapshot the base setup as a profile (the active profile itself re-saves its own setup)" },
{ action: "use", description: "make a profile the active setup" },
{ action: "delete", description: "delete a saved profile" },
];
Expand Down
29 changes: 23 additions & 6 deletions src/cli/commands/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -625,9 +625,19 @@ function watchResize(listener: () => void): () => void {
export async function runModelSetupWizard(options: ModelWizardOptions = {}): Promise<RunAgentConfig> {
const loaded = options.config ?? loadConfig();
const profile = options.profile !== undefined ? parseProfileName(options.profile) : undefined;
// --profile edits one saved setup, shown exactly as saved. A name nobody
// saved yet starts from the base setup with an empty agent map: role
// screens show no inherited "atual", while sandbox/autocompact/orchestrator
// keep mirroring what the profile would inherit at launch (blanking them
// would silently downgrade the base values on confirm). Without --profile
// the base config is edited and the active snapshot is left untouched
// (see runSetupBatch below).
const snapshot = profile === undefined ? undefined : getProfileSnapshot(loaded, profile);
const config = profile === undefined
? loaded
: { ...baseConfig(loaded), ...getProfileSnapshot(loaded, profile) };
: snapshot === undefined
? { ...baseConfig(loaded), agents: {} }
: { ...baseConfig(loaded), ...snapshot };
if (!(options.isTTY ?? isInteractiveTerminal())) return config;

const output = options.output ?? process.stdout;
Expand All @@ -654,7 +664,7 @@ export async function runModelSetupWizard(options: ModelWizardOptions = {}): Pro
// triple harness:model:effort is chosen in one step instead of a second
// pass at the end. A second runScreens call is not an option: the picker
// owns raw mode for exactly one run per stream set.
const roleScreens = buildScreens(ROLES, harnesses, config.agents ?? {}, config.defaultAgent ?? "claude").map(
const roleScreens = buildScreens(ROLES, harnesses, config.agents ?? {}, loaded.defaultAgent ?? "claude").map(
(screen, roleIndex) => ({
...screen,
next: (result: ScreenResult): Screen[] => {
Expand Down Expand Up @@ -1361,13 +1371,20 @@ export async function runSetupBatch(
}

const current: RunAgentConfig = { ...DEFAULT_CONFIG, ...(read.config ?? {}) };
// --profile edits one saved setup instead of the active one. A name nobody
// saved yet starts from the base config, so creating a profile needs no
// separate gesture.
// --profile edits one saved setup instead of the active one. An existing
// name loads base plus its snapshot; a name nobody saved yet starts from
// the base setup with an empty agent map, so root agents never leak into
// the new profile as inherited "atual" while sandbox/autocompact/
// orchestrator keep their inherited values. Without --profile the base
// config is edited and saved profiles (including the active one) are left
// untouched.
const profile = options.profile;
const snapshot = profile === undefined ? undefined : getProfileSnapshot(current, profile);
const target: RunAgentConfig = profile === undefined
? current
: { ...baseConfig(current), ...getProfileSnapshot(current, profile) };
: snapshot === undefined
? { ...baseConfig(current), agents: {} }
: { ...baseConfig(current), ...snapshot };
const lastByRole = new Map<Role, number>();
options.binds.forEach((binding, index) => lastByRole.set(binding.role, index));
const winning = options.binds.filter((binding, index) => lastByRole.get(binding.role) === index);
Expand Down
124 changes: 121 additions & 3 deletions src/drivers/antigravity/driver.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,92 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { StringDecoder } from "node:string_decoder";
import { detectBinary, runCommandWithTimeout } from "../helpers.js";
import type { AgentInstallation, StartOptions } from "../../core/driver.js";
import type { AgentInstallation, DriverSession, ReattachRequest, StartOptions } from "../../core/driver.js";
import type { AgentCapabilities } from "../../core/capabilities.js";
import type { AgentEvent } from "../../core/events.js";
import type { ListModelsOptions, ModelInfo, ProviderModels } from "../../core/models.js";
import { createRuntimeHooks, SessionDriver } from "../session-driver.js";
import { parseAntigravityLine } from "./parser.js";
import { sessionLogPaths } from "../session-runtime.js";
import { createAntigravityParser, type AntigravityParser } from "./parser.js";

const MAX_REPLAY_READ_BYTES = 64 * 1024;

export function alignAntigravityLogOffset(stdoutPath: string, logOffset: number): number {
if (!Number.isSafeInteger(logOffset) || logOffset <= 0) return 0;

let fd: number | undefined;
try {
fd = fs.openSync(stdoutPath, "r");
const size = fs.fstatSync(fd).size;
if (logOffset > size) return 0;
let position = logOffset;
const buffer = Buffer.allocUnsafe(MAX_REPLAY_READ_BYTES);
while (position > 0) {
const start = Math.max(0, position - MAX_REPLAY_READ_BYTES);
const bytesRead = fs.readSync(fd, buffer, 0, position - start, start);
if (bytesRead === 0) return 0;
const newline = buffer.subarray(0, bytesRead).lastIndexOf(0x0a);
if (newline >= 0) return start + newline + 1;
position = start;
}
return 0;
} catch {
return 0;
} finally {
if (fd !== undefined) fs.closeSync(fd);
}
}

export function replayAntigravityOutput(
stdoutPath: string,
logOffset: number,
parser: AntigravityParser,
sessionId: string,
): void {
if (!Number.isSafeInteger(logOffset) || logOffset <= 0) return;

let fd: number | undefined;
try {
fd = fs.openSync(stdoutPath, "r");
const buffer = Buffer.allocUnsafe(Math.min(MAX_REPLAY_READ_BYTES, logOffset));
const decoder = new StringDecoder("utf8");
let position = 0;
let line = "";

while (position < logOffset) {
const bytesRead = fs.readSync(
fd,
buffer,
0,
Math.min(buffer.length, logOffset - position),
position,
);
if (bytesRead === 0) break;
position += bytesRead;

const chunk = decoder.write(buffer.subarray(0, bytesRead));
let start = 0;
for (let newline = chunk.indexOf("\n"); newline >= 0; newline = chunk.indexOf("\n", start)) {
line += chunk.slice(start, newline);
if (line) parser(line, sessionId);
line = "";
start = newline + 1;
}
line += chunk.slice(start);
}

// The persisted offset is the end of a complete line. Discard any
// unterminated fragment so an invalid offset cannot turn log bytes into
// assistant text or cause the live tailer to replay part of a line.
decoder.end();
} catch {
// A missing or unreadable old log does not prevent the runtime from attaching.
} finally {
if (fd !== undefined) fs.closeSync(fd);
}
}

// Pure so the flag spellings and effort clamping are testable without spawning agy.
export function buildAntigravityArgs(options: StartOptions): string[] {
Expand Down Expand Up @@ -86,8 +166,10 @@ export function parseAntigravityModelsList(stdout: string): ProviderModels[] {
export class AntigravityDriver extends SessionDriver {
readonly id = "antigravity" as const;

private readonly parser = createAntigravityParser();

protected readonly hooks = createRuntimeHooks({
parse: parseAntigravityLine,
parse: this.parser,
nativeKeys: ["conversation_id"],
harness: "Antigravity",
plainTextFallback: true,
Expand All @@ -97,6 +179,42 @@ export class AntigravityDriver extends SessionDriver {

private detectedPath?: string;

override async start(options: StartOptions): Promise<DriverSession> {
this.parser.reset(options.sessionId);
return super.start(options);
}

override async attach(request: ReattachRequest): Promise<void> {
this.parser.reset(request.sessionId);
const logOffset = alignAntigravityLogOffset(
sessionLogPaths(request.sessionId).stdoutPath,
request.logOffset ?? 0,
);
this.replayConsumedOutput(request.sessionId, logOffset);
await super.attach({ ...request, logOffset });
}

override async stop(session: DriverSession): Promise<void> {
try {
await super.stop(session);
} finally {
this.parser.reset(session.id);
}
}

override async *events(session: DriverSession): AsyncIterable<AgentEvent> {
for await (const event of super.events(session)) {
if (event.type === "session.completed" || event.type === "session.failed") {
this.parser.reset(session.id);
}
yield event;
}
}

private replayConsumedOutput(sessionId: string, logOffset?: number): void {
replayAntigravityOutput(sessionLogPaths(sessionId).stdoutPath, logOffset ?? 0, this.parser, sessionId);
}

protected override getCommand(): string {
if (this.detectedPath) return this.detectedPath;
const localBin = path.join(os.homedir(), ".local", "bin", "agy");
Expand Down
Loading
Loading