@agentex/agent 0.0.37 → 0.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +54 -17
  3. package/dist/providers/antigravity/codec.d.ts +4 -0
  4. package/dist/providers/antigravity/codec.d.ts.map +1 -0
  5. package/dist/providers/antigravity/codec.js +27 -0
  6. package/dist/providers/antigravity/codec.js.map +1 -0
  7. package/dist/providers/antigravity/connection.d.ts +100 -0
  8. package/dist/providers/antigravity/connection.d.ts.map +1 -0
  9. package/dist/providers/antigravity/connection.js +267 -0
  10. package/dist/providers/antigravity/connection.js.map +1 -0
  11. package/dist/providers/antigravity/discovery.d.ts +11 -0
  12. package/dist/providers/antigravity/discovery.d.ts.map +1 -0
  13. package/dist/providers/antigravity/discovery.js +92 -0
  14. package/dist/providers/antigravity/discovery.js.map +1 -0
  15. package/dist/providers/antigravity/execute.d.ts +9 -0
  16. package/dist/providers/antigravity/execute.d.ts.map +1 -0
  17. package/dist/providers/antigravity/execute.js +206 -0
  18. package/dist/providers/antigravity/execute.js.map +1 -0
  19. package/dist/providers/antigravity/index.d.ts +12 -0
  20. package/dist/providers/antigravity/index.d.ts.map +1 -0
  21. package/dist/providers/antigravity/index.js +46 -0
  22. package/dist/providers/antigravity/index.js.map +1 -0
  23. package/dist/providers/antigravity/parse.d.ts +114 -0
  24. package/dist/providers/antigravity/parse.d.ts.map +1 -0
  25. package/dist/providers/antigravity/parse.js +369 -0
  26. package/dist/providers/antigravity/parse.js.map +1 -0
  27. package/dist/providers/antigravity/probe.d.ts +3 -0
  28. package/dist/providers/antigravity/probe.d.ts.map +1 -0
  29. package/dist/providers/antigravity/probe.js +84 -0
  30. package/dist/providers/antigravity/probe.js.map +1 -0
  31. package/dist/providers/antigravity/runtime.d.ts +30 -0
  32. package/dist/providers/antigravity/runtime.d.ts.map +1 -0
  33. package/dist/providers/antigravity/runtime.js +69 -0
  34. package/dist/providers/antigravity/runtime.js.map +1 -0
  35. package/dist/providers/antigravity/session.d.ts +7 -0
  36. package/dist/providers/antigravity/session.d.ts.map +1 -0
  37. package/dist/providers/antigravity/session.js +317 -0
  38. package/dist/providers/antigravity/session.js.map +1 -0
  39. package/dist/providers/gemini/index.d.ts.map +1 -1
  40. package/dist/providers/gemini/index.js +6 -0
  41. package/dist/providers/gemini/index.js.map +1 -1
  42. package/dist/registry.d.ts.map +1 -1
  43. package/dist/registry.js +2 -0
  44. package/dist/registry.js.map +1 -1
  45. package/dist/types.d.ts +6 -5
  46. package/dist/types.d.ts.map +1 -1
  47. package/dist/types.js.map +1 -1
  48. package/dist/utils/auth.d.ts +1 -1
  49. package/dist/utils/auth.d.ts.map +1 -1
  50. package/dist/utils/auth.js +79 -1
  51. package/dist/utils/auth.js.map +1 -1
  52. package/dist/utils/binary.d.ts.map +1 -1
  53. package/dist/utils/binary.js +10 -0
  54. package/dist/utils/binary.js.map +1 -1
  55. package/dist/utils/instructions.d.ts +36 -12
  56. package/dist/utils/instructions.d.ts.map +1 -1
  57. package/dist/utils/instructions.js +116 -62
  58. package/dist/utils/instructions.js.map +1 -1
  59. package/dist/utils/runtime-homes.d.ts.map +1 -1
  60. package/dist/utils/runtime-homes.js +4 -0
  61. package/dist/utils/runtime-homes.js.map +1 -1
  62. package/dist/utils/skills.d.ts +3 -3
  63. package/dist/utils/skills.d.ts.map +1 -1
  64. package/dist/utils/skills.js +16 -2
  65. package/dist/utils/skills.js.map +1 -1
  66. package/package.json +3 -2
  67. package/src/providers/antigravity/codec.ts +29 -0
  68. package/src/providers/antigravity/connection.ts +304 -0
  69. package/src/providers/antigravity/discovery.ts +90 -0
  70. package/src/providers/antigravity/execute.ts +214 -0
  71. package/src/providers/antigravity/index.ts +48 -0
  72. package/src/providers/antigravity/parse.ts +439 -0
  73. package/src/providers/antigravity/probe.ts +89 -0
  74. package/src/providers/antigravity/runtime.ts +64 -0
  75. package/src/providers/antigravity/session.ts +359 -0
  76. package/src/providers/gemini/index.ts +6 -0
  77. package/src/registry.ts +2 -0
  78. package/src/types.ts +6 -5
  79. package/src/utils/auth.ts +82 -1
  80. package/src/utils/binary.ts +10 -0
  81. package/src/utils/instructions.ts +162 -73
  82. package/src/utils/runtime-homes.ts +4 -0
  83. package/src/utils/skills.ts +17 -4
@@ -0,0 +1,304 @@
1
+ import { spawn, type ChildProcess } from "node:child_process";
2
+ import type { StreamEvent } from "../../types.js";
3
+ import { killProcessTree } from "../../utils/process.js";
4
+ import {
5
+ ANTIGRAVITY_PROVIDER_TYPE,
6
+ isAgyAuthRequired,
7
+ parseAgyErrorLine,
8
+ type AgyErrorReport,
9
+ type AgyResult,
10
+ type AgyStreamParser,
11
+ } from "./parse.js";
12
+ import { AGY_LOGIN_COMMAND } from "./runtime.js";
13
+
14
+ const STDERR_TAIL_BYTES = 16 * 1024;
15
+
16
+ export interface AgyConnectionOptions {
17
+ command: string;
18
+ args: string[];
19
+ cwd: string;
20
+ env: Record<string, string>;
21
+ parser: AgyStreamParser;
22
+ onEvent?: (event: StreamEvent) => void | Promise<void>;
23
+ onOutput?: (stream: "stdout" | "stderr", chunk: string) => void | Promise<void>;
24
+ }
25
+
26
+ /** How a turn (or the whole process) ended, as seen by the host. */
27
+ export interface AgyTurnOutcome {
28
+ /** The turn's `result` event, or null when the process exited first. */
29
+ result: AgyResult | null;
30
+ /** The process has exited. */
31
+ exited: boolean;
32
+ exitCode: number | null;
33
+ signal: string | null;
34
+ /** The CLI asked for a sign-in, which a headless run can never complete. */
35
+ authRequired: boolean;
36
+ agyError: AgyErrorReport | null;
37
+ /** Last ~16KB of stderr, for error messages. */
38
+ stderr: string;
39
+ }
40
+
41
+ /**
42
+ * One headless `agy` process speaking `stream-json` in both directions.
43
+ *
44
+ * Every stdout line is parsed and dispatched through a single promise chain,
45
+ * so `onEvent` handlers run strictly in wire order and a turn's waiter
46
+ * resolves only after the handler for its `result` event has settled.
47
+ */
48
+ export class AgyConnection {
49
+ readonly pid: number | null;
50
+ authRequired = false;
51
+ agyError: AgyErrorReport | null = null;
52
+
53
+ private readonly child: ChildProcess;
54
+ private chain: Promise<void> = Promise.resolve();
55
+ private stdoutBuffer = "";
56
+ private stderrLineBuffer = "";
57
+ private stderrTail = "";
58
+ private exitInfo: { code: number | null; signal: string | null } | null = null;
59
+ private finalized = false;
60
+ private readonly waiters: Array<(outcome: AgyTurnOutcome) => void> = [];
61
+ private readonly exitWaiters: Array<() => void> = [];
62
+
63
+ constructor(private readonly options: AgyConnectionOptions) {
64
+ this.child = spawn(options.command, options.args, {
65
+ cwd: options.cwd,
66
+ env: options.env,
67
+ stdio: ["pipe", "pipe", "pipe"],
68
+ shell: false,
69
+ // A process group lets interrupt/terminate reach the commands agy runs.
70
+ detached: process.platform !== "win32",
71
+ });
72
+ this.pid = this.child.pid ?? null;
73
+
74
+ this.child.stdin?.on("error", () => { /* agy exited; the exit path reports it */ });
75
+ this.child.stdout?.setEncoding("utf-8");
76
+ this.child.stderr?.setEncoding("utf-8");
77
+ this.child.stdout?.on("data", (chunk: string) => this.onStdout(chunk));
78
+ this.child.stderr?.on("data", (chunk: string) => this.onStderr(chunk));
79
+ this.child.on("error", (err) => {
80
+ this.appendStderrTail(`${err.message}\n`);
81
+ this.finalize(null, null);
82
+ });
83
+ this.child.on("close", (code, signal) => this.finalize(code, signal ?? null));
84
+ }
85
+
86
+ get alive(): boolean {
87
+ return this.exitInfo === null;
88
+ }
89
+
90
+ /** Exit status, or null while the process runs. */
91
+ get exit(): { code: number | null; signal: string | null } | null {
92
+ return this.exitInfo;
93
+ }
94
+
95
+ /** Last ~16KB of stderr. */
96
+ get stderr(): string {
97
+ return this.stderrTail;
98
+ }
99
+
100
+ /** Resolves once the process has exited and every queued event was dispatched. */
101
+ exited(): Promise<void> {
102
+ if (this.finalized) return this.chain;
103
+ return new Promise((resolve) => this.exitWaiters.push(resolve));
104
+ }
105
+
106
+ /** Send one user message. Resolve it with `nextTurn()`. */
107
+ write(text: string): void {
108
+ if (!this.alive || !this.child.stdin || this.child.stdin.destroyed) return;
109
+ this.child.stdin.write(`${JSON.stringify({ event: "user", message: { content: text } })}\n`);
110
+ }
111
+
112
+ /** Close stdin. `agy` finishes the current turn and exits. */
113
+ endInput(): void {
114
+ try {
115
+ this.child.stdin?.end();
116
+ } catch {
117
+ /* already closed */
118
+ }
119
+ }
120
+
121
+ /** Resolves with the next `result` event, or with the exit when no result comes. */
122
+ nextTurn(): Promise<AgyTurnOutcome> {
123
+ if (this.finalized) return Promise.resolve(this.outcome(null));
124
+ return new Promise((resolve) => this.waiters.push(resolve));
125
+ }
126
+
127
+ /** Signal the whole process group. */
128
+ signal(signal: NodeJS.Signals): void {
129
+ if (this.pid != null && this.alive) killProcessTree(this.pid, signal);
130
+ }
131
+
132
+ /** True when the process exited within `ms`. */
133
+ async waitForExit(ms: number): Promise<boolean> {
134
+ return (await within(this.exited().then(() => true), ms)) === true;
135
+ }
136
+
137
+ /**
138
+ * Stop the process: close stdin and give it a moment to exit on its own
139
+ * (an idle `agy` flushes its conversation history and exits), then
140
+ * SIGTERM, then SIGKILL after `graceSec`.
141
+ */
142
+ async terminate(graceSec = 5, idleWaitMs = 1000): Promise<void> {
143
+ if (this.finalized) return;
144
+ this.endInput();
145
+ if (idleWaitMs > 0 && await this.waitForExit(idleWaitMs)) return;
146
+ this.signal("SIGTERM");
147
+ if (await this.waitForExit(graceSec * 1000)) return;
148
+ this.signal("SIGKILL");
149
+ await this.waitForExit(1000);
150
+ }
151
+
152
+ private onStdout(chunk: string): void {
153
+ this.stdoutBuffer += chunk;
154
+ const lines = this.stdoutBuffer.split("\n");
155
+ this.stdoutBuffer = lines.pop() ?? "";
156
+ for (const line of lines) this.enqueueLine(line);
157
+ }
158
+
159
+ private enqueueLine(rawLine: string): void {
160
+ const line = rawLine.replace(/\r$/, "");
161
+ if (!line.trim()) return;
162
+ this.chain = this.chain.then(async () => {
163
+ await this.emitOutput("stdout", `${line}\n`);
164
+ const parsed = this.options.parser.parseLine(line);
165
+ for (const event of parsed.events) await this.emitEvent(event);
166
+ if (parsed.result) this.waiters.shift()?.(this.outcome(parsed.result));
167
+ });
168
+ }
169
+
170
+ private onStderr(chunk: string): void {
171
+ this.appendStderrTail(chunk);
172
+ this.chain = this.chain.then(() => this.emitOutput("stderr", chunk));
173
+ this.stderrLineBuffer += chunk;
174
+ const lines = this.stderrLineBuffer.split("\n");
175
+ this.stderrLineBuffer = lines.pop() ?? "";
176
+ for (const line of lines) this.inspectStderrLine(line);
177
+ // The sign-in prompt can sit on an unterminated line while agy waits.
178
+ if (!this.authRequired && isAgyAuthRequired(this.stderrLineBuffer)) this.onAuthRequired(this.stderrLineBuffer);
179
+ }
180
+
181
+ private inspectStderrLine(line: string): void {
182
+ const report = parseAgyErrorLine(line);
183
+ if (report) this.agyError = report;
184
+ if (!this.authRequired && isAgyAuthRequired(line)) this.onAuthRequired(line);
185
+ }
186
+
187
+ /**
188
+ * A headless run without a cached sign-in prints a login URL and waits a
189
+ * full minute for a code nobody can paste. Fail fast instead: report it
190
+ * and stop the process.
191
+ */
192
+ private onAuthRequired(message: string): void {
193
+ this.authRequired = true;
194
+ const event: StreamEvent = {
195
+ type: "auth_required",
196
+ httpStatus: null,
197
+ reason: "missing",
198
+ loginCommand: AGY_LOGIN_COMMAND,
199
+ message: message.trim() || "Antigravity CLI requires sign-in.",
200
+ timestamp: new Date().toISOString(),
201
+ providerType: ANTIGRAVITY_PROVIDER_TYPE,
202
+ sessionId: this.options.parser.conversationId,
203
+ messageId: null,
204
+ eventId: null,
205
+ turnId: null,
206
+ parentToolCallId: null,
207
+ raw: { stderr: message },
208
+ };
209
+ this.chain = this.chain.then(() => this.emitEvent(event));
210
+ this.signal("SIGTERM");
211
+ }
212
+
213
+ private appendStderrTail(chunk: string): void {
214
+ this.stderrTail = (this.stderrTail + chunk).slice(-STDERR_TAIL_BYTES);
215
+ }
216
+
217
+ private finalize(code: number | null, signal: string | null): void {
218
+ if (this.exitInfo === null) this.exitInfo = { code, signal };
219
+ if (this.finalized) return;
220
+ this.finalized = true;
221
+ if (this.stdoutBuffer.trim()) this.enqueueLine(this.stdoutBuffer);
222
+ this.stdoutBuffer = "";
223
+ if (this.stderrLineBuffer.trim()) this.inspectStderrLine(this.stderrLineBuffer);
224
+ this.stderrLineBuffer = "";
225
+ this.chain = this.chain.then(() => {
226
+ for (const waiter of this.waiters.splice(0)) waiter(this.outcome(null));
227
+ for (const waiter of this.exitWaiters.splice(0)) waiter();
228
+ });
229
+ }
230
+
231
+ private outcome(result: AgyResult | null): AgyTurnOutcome {
232
+ return {
233
+ result,
234
+ exited: this.exitInfo !== null,
235
+ exitCode: this.exitInfo?.code ?? null,
236
+ signal: this.exitInfo?.signal ?? null,
237
+ authRequired: this.authRequired,
238
+ agyError: this.agyError,
239
+ stderr: this.stderrTail,
240
+ };
241
+ }
242
+
243
+ private async emitEvent(event: StreamEvent): Promise<void> {
244
+ if (!this.options.onEvent) return;
245
+ try {
246
+ await this.options.onEvent(event);
247
+ } catch {
248
+ /* a throwing handler must not break the stream */
249
+ }
250
+ }
251
+
252
+ private async emitOutput(stream: "stdout" | "stderr", chunk: string): Promise<void> {
253
+ if (!this.options.onOutput) return;
254
+ try {
255
+ await this.options.onOutput(stream, chunk);
256
+ } catch {
257
+ /* swallow */
258
+ }
259
+ }
260
+ }
261
+
262
+ /** First meaningful stderr line, skipping the sign-in URL noise. */
263
+ export function firstAgyErrorLine(stderr: string): string | null {
264
+ for (const raw of stderr.split(/\r?\n/)) {
265
+ const line = raw.trim();
266
+ if (!line || /^https?:\/\//.test(line) || line.startsWith("AGY_ERROR:")) continue;
267
+ return line.replace(/^error:\s*/i, "");
268
+ }
269
+ return null;
270
+ }
271
+
272
+ /** Waits for `promise` up to `ms`; resolves null on timeout. */
273
+ export function within<T>(promise: Promise<T>, ms: number): Promise<T | null> {
274
+ return new Promise((resolve) => {
275
+ const timer = setTimeout(() => resolve(null), ms);
276
+ promise.then(
277
+ (value) => {
278
+ clearTimeout(timer);
279
+ resolve(value);
280
+ },
281
+ () => {
282
+ clearTimeout(timer);
283
+ resolve(null);
284
+ },
285
+ );
286
+ });
287
+ }
288
+
289
+ /**
290
+ * Interrupt the running turn like Ctrl+C would: SIGINT the process group,
291
+ * give agy a moment to report the turn as INTERRUPTED, and stop the process
292
+ * if it does not. Resolves with whatever outcome arrived.
293
+ */
294
+ export async function interruptAgyTurn(
295
+ connection: AgyConnection,
296
+ pending: Promise<AgyTurnOutcome>,
297
+ graceSec = 5,
298
+ ): Promise<AgyTurnOutcome | null> {
299
+ connection.signal("SIGINT");
300
+ const settled = await within(pending, 3000);
301
+ if (settled) return settled;
302
+ await connection.terminate(graceSec, 0);
303
+ return within(pending, 2000);
304
+ }
@@ -0,0 +1,90 @@
1
+ import type { AgentMode, ListModelsOptions, ListModesOptions, ProviderModel } from "../../types.js";
2
+ import { buildEnv, ensurePathInEnv } from "../../utils/env.js";
3
+ import { withModelCache } from "../../utils/model-cache.js";
4
+ import { runChildProcess } from "../../utils/process.js";
5
+ import { isAgyAuthRequired } from "./parse.js";
6
+ import { ANTIGRAVITY_MODES, AGY_SIGN_IN_MESSAGE, findAgyBinary } from "./runtime.js";
7
+
8
+ function rec(value: unknown): Record<string, unknown> {
9
+ return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
10
+ }
11
+
12
+ function fromJson(output: string): ProviderModel[] | null {
13
+ try {
14
+ const parsed = JSON.parse(output) as unknown;
15
+ const list = Array.isArray(parsed) ? parsed : rec(parsed)["models"];
16
+ if (!Array.isArray(list)) return null;
17
+ return list.map((value) => {
18
+ if (typeof value === "string") return { id: value, name: value };
19
+ const model = rec(value);
20
+ const id = [model["slug"], model["id"], model["model"]].find((v): v is string => typeof v === "string" && v.length > 0) ?? "";
21
+ const name = [model["display_name"], model["displayName"], model["name"], model["label"]]
22
+ .find((v): v is string => typeof v === "string" && v.length > 0);
23
+ return { id, name: name ?? id };
24
+ }).filter((model) => model.id);
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Parse `agy models`. The JSON form is preferred when a build supports it;
32
+ * the text form is one model per line: a slug, two or more spaces, then the
33
+ * display name, for example `gemini-3.1-pro-high Gemini 3.1 Pro (High)`.
34
+ */
35
+ export function parseAntigravityModels(output: string): ProviderModel[] {
36
+ const trimmed = output.trim();
37
+ if (!trimmed) return [];
38
+ const json = trimmed.startsWith("{") || trimmed.startsWith("[") ? fromJson(trimmed) : null;
39
+ if (json?.length) return json;
40
+
41
+ const seen = new Set<string>();
42
+ const models: ProviderModel[] = [];
43
+ for (const raw of trimmed.split(/\r?\n/)) {
44
+ const line = raw.replace(/^\s*(?:[*•>-]|→)\s*/, "").trim();
45
+ if (!line || /^(?:available\s+)?models?:?$/i.test(line) || /^fetching\b/i.test(line) || line === "...") continue;
46
+ const match = line.match(/^([A-Za-z0-9][\w./:-]*)(?:\s{2,}|\t+)(.+)$/) ?? line.match(/^([A-Za-z0-9][\w./:-]*)$/);
47
+ const id = match?.[1];
48
+ if (!id || seen.has(id) || /^(?:error|warning|usage)$/i.test(id)) continue;
49
+ seen.add(id);
50
+ const name = match?.[2]?.replace(/\s*\((?:current|default|selected)\)\s*$/i, "").trim();
51
+ models.push({ id, name: name || id });
52
+ }
53
+ return models;
54
+ }
55
+
56
+ export async function listAntigravityModels(options: ListModelsOptions = {}): Promise<ProviderModel[]> {
57
+ return withModelCache("antigravity", options, options.cacheTtlMs, () => discoverAntigravityModels(options));
58
+ }
59
+
60
+ async function discoverAntigravityModels(options: ListModelsOptions): Promise<ProviderModel[]> {
61
+ const resolved = await findAgyBinary(options);
62
+ const env = buildEnv(options.env);
63
+ ensurePathInEnv(env);
64
+ const cwd = options.cwd ?? process.cwd();
65
+ let lastOutput = "";
66
+ for (const args of [["models", "--output-format", "json"], ["models"]]) {
67
+ const result = await runChildProcess({
68
+ runId: "antigravity-model-discovery",
69
+ command: resolved.bin,
70
+ args: [...resolved.prefixArgs, ...args],
71
+ cwd,
72
+ env,
73
+ timeoutSec: 20,
74
+ });
75
+ lastOutput = `${result.stdout}\n${result.stderr}`;
76
+ if (isAgyAuthRequired(lastOutput)) {
77
+ throw new Error(AGY_SIGN_IN_MESSAGE);
78
+ }
79
+ if (result.exitCode !== 0) continue;
80
+ const models = parseAntigravityModels(result.stdout);
81
+ if (models.length) return models;
82
+ }
83
+ const detail = lastOutput.split(/\r?\n/).map((line) => line.trim()).find((line) => /error/i.test(line));
84
+ throw new Error(detail ?? "The installed Antigravity CLI did not list any models");
85
+ }
86
+
87
+ /** Antigravity's modes are a fixed set (`--mode`); listing them spawns nothing. */
88
+ export async function listAntigravityModes(_options: ListModesOptions = {}): Promise<AgentMode[]> {
89
+ return ANTIGRAVITY_MODES.map((mode) => ({ ...mode }));
90
+ }
@@ -0,0 +1,214 @@
1
+ import type { ExecutionContext, ExecutionResult, ExecutionStatus, StreamEvent } from "../../types.js";
2
+ import { detectAuth } from "../../utils/auth.js";
3
+ import { buildEnv, ensurePathInEnv } from "../../utils/env.js";
4
+ import { resolveInstructions } from "../../utils/instructions.js";
5
+ import { injectHomeSkills } from "../../utils/skills.js";
6
+ import { uuidv7 } from "../../utils/uuid.js";
7
+ import { prepareWorkspace, type PreparedWorkspace } from "../../utils/workspace.js";
8
+ import { AgyConnection, firstAgyErrorLine, interruptAgyTurn, type AgyTurnOutcome } from "./connection.js";
9
+ import {
10
+ AgyStreamParser,
11
+ agyUsageRecord,
12
+ classifyAgyFailure,
13
+ mapAgyExecutionStatus,
14
+ } from "./parse.js";
15
+ import { AGY_SIGN_IN_MESSAGE, buildAgyArgs, findAgyBinary, readAgyResumeId } from "./runtime.js";
16
+
17
+ /**
18
+ * One-shot Antigravity run: start `agy` in stream-json mode, write the prompt
19
+ * as a single NDJSON message, close stdin, and wait for the process to exit.
20
+ * The prompt travels over stdin, so it is never size-limited by argv or
21
+ * visible in `ps`.
22
+ */
23
+ export async function executeAntigravityProvider(ctx: ExecutionContext): Promise<ExecutionResult> {
24
+ const runId = ctx.runId ?? uuidv7();
25
+ const startedAt = new Date().toISOString();
26
+ const startMs = Date.now();
27
+ const config = ctx.config ?? {};
28
+ const requestedModel = ctx.model ?? config.model ?? null;
29
+ let cwd = ctx.cwd ?? process.cwd();
30
+
31
+ const finish = (fields: Partial<ExecutionResult> & Pick<ExecutionResult, "status">): ExecutionResult => ({
32
+ runId,
33
+ exitCode: null,
34
+ signal: null,
35
+ startedAt,
36
+ completedAt: new Date().toISOString(),
37
+ durationMs: Date.now() - startMs,
38
+ errorMessage: null,
39
+ errorCode: null,
40
+ costUsd: null,
41
+ model: requestedModel,
42
+ summary: null,
43
+ sessionParams: null,
44
+ sessionDisplayId: null,
45
+ clearSession: false,
46
+ billingType: null,
47
+ ...fields,
48
+ });
49
+
50
+ ctx.onLifecycle?.({ phase: "preparing", step: "binary" });
51
+ let binary;
52
+ try {
53
+ binary = await findAgyBinary({ cwd, ...(ctx.env ? { env: ctx.env } : {}), config });
54
+ } catch (err) {
55
+ const errorMessage = err instanceof Error ? err.message : "Antigravity CLI (agy) not found";
56
+ ctx.onLifecycle?.({ phase: "error", message: errorMessage });
57
+ return finish({ status: "failed", errorMessage, errorCode: "binary_not_found", model: null });
58
+ }
59
+
60
+ let workspace: PreparedWorkspace | undefined;
61
+ if (config.workspace) {
62
+ ctx.onLifecycle?.({ phase: "preparing", step: "workspace" });
63
+ workspace = await prepareWorkspace(cwd, config.workspace);
64
+ cwd = workspace.cwd;
65
+ }
66
+
67
+ ctx.onLifecycle?.({ phase: "preparing", step: "auth" });
68
+ const env = buildEnv(ctx.env);
69
+ ensurePathInEnv(env);
70
+ const billingType = detectAuth("antigravity", env).billingType;
71
+
72
+ const resumeId = readAgyResumeId(ctx.sessionParams, cwd);
73
+
74
+ ctx.onLifecycle?.({ phase: "preparing", step: "instructions" });
75
+ // A resumed conversation already carries its instructions.
76
+ const instructions = resumeId ? null : await resolveInstructions(config.instructionsFile);
77
+ const prompt = instructions ? `${instructions}\n\n${ctx.prompt}` : ctx.prompt;
78
+
79
+ if (config.skillDirs && config.skillDirs.length > 0) {
80
+ ctx.onLifecycle?.({ phase: "preparing", step: "skills" });
81
+ try {
82
+ await injectHomeSkills(config.skillDirs, "antigravity");
83
+ } catch {
84
+ // Non-fatal
85
+ }
86
+ }
87
+
88
+ const parser = new AgyStreamParser({
89
+ includePartialMessages: config.includePartialMessages,
90
+ conversationId: resumeId,
91
+ });
92
+ const assistantText: string[] = [];
93
+ const onEvent = async (event: StreamEvent): Promise<void> => {
94
+ if (event.type === "assistant") assistantText.push(event.text);
95
+ await ctx.onEvent?.(event);
96
+ };
97
+
98
+ ctx.onLifecycle?.({ phase: "spawning" });
99
+ const connection = new AgyConnection({
100
+ command: binary.bin,
101
+ args: [...binary.prefixArgs, ...buildAgyArgs(config, { resumeId, model: requestedModel })],
102
+ cwd,
103
+ env,
104
+ parser,
105
+ onEvent,
106
+ ...(ctx.onOutput ? { onOutput: ctx.onOutput } : {}),
107
+ });
108
+ if (connection.pid != null) {
109
+ ctx.onLifecycle?.({ phase: "running", pid: connection.pid });
110
+ ctx.onStart?.(connection.pid);
111
+ }
112
+
113
+ const pending = connection.nextTurn();
114
+ connection.write(prompt);
115
+ connection.endInput();
116
+
117
+ // Guards cover the whole run, including agy finishing background work
118
+ // after it delivers the answer.
119
+ let timer: ReturnType<typeof setTimeout> | null = null;
120
+ const guards: Promise<"timeout" | "aborted">[] = [];
121
+ if (config.timeoutSec && config.timeoutSec > 0) {
122
+ guards.push(new Promise((resolve) => { timer = setTimeout(() => resolve("timeout"), config.timeoutSec! * 1000); }));
123
+ }
124
+ let onAbort: (() => void) | null = null;
125
+ if (ctx.signal) {
126
+ const signal = ctx.signal;
127
+ guards.push(new Promise((resolve) => {
128
+ if (signal.aborted) resolve("aborted");
129
+ onAbort = () => resolve("aborted");
130
+ signal.addEventListener("abort", onAbort, { once: true });
131
+ }));
132
+ }
133
+
134
+ let stopped: "timeout" | "aborted" | null = null;
135
+ let outcome: AgyTurnOutcome | null;
136
+ const first = await Promise.race([pending, ...guards]);
137
+ if (first === "timeout" || first === "aborted") {
138
+ stopped = first;
139
+ outcome = await interruptAgyTurn(connection, pending, config.graceSec);
140
+ // The run is over either way; never leave the process behind.
141
+ await connection.terminate(config.graceSec);
142
+ } else {
143
+ outcome = first;
144
+ if (!outcome.exited) {
145
+ const ended = await Promise.race([connection.exited().then(() => "exited" as const), ...guards]);
146
+ if (ended !== "exited") await connection.terminate(config.graceSec, 0);
147
+ }
148
+ }
149
+ if (timer) clearTimeout(timer);
150
+ if (onAbort && ctx.signal) ctx.signal.removeEventListener("abort", onAbort);
151
+ // Exit status is final only once the process has gone.
152
+ await connection.waitForExit(1000);
153
+
154
+ const result = outcome?.result ?? null;
155
+ const authRequired = connection.authRequired;
156
+ const agyError = connection.agyError;
157
+ const stderr = connection.stderr;
158
+ const exitCode = connection.exit?.code ?? null;
159
+ const exitSignal = connection.exit?.signal ?? null;
160
+
161
+ let status: ExecutionStatus;
162
+ let errorCode: string | null = null;
163
+ let errorMessage: string | null = null;
164
+ if (stopped) {
165
+ status = stopped;
166
+ errorCode = stopped;
167
+ errorMessage = stopped === "timeout" ? `Timed out after ${config.timeoutSec ?? 0}s` : "Run aborted";
168
+ } else if (result) {
169
+ status = mapAgyExecutionStatus(result.status);
170
+ if (status !== "completed") {
171
+ errorCode = status === "aborted" ? "aborted" : authRequired ? "auth_required" : classifyAgyFailure(result, agyError);
172
+ errorMessage = errorCode === "auth_required"
173
+ ? AGY_SIGN_IN_MESSAGE
174
+ : result.error ?? agyError?.message ?? `Antigravity run ended with ${result.status}`;
175
+ }
176
+ } else if (authRequired) {
177
+ status = "failed";
178
+ errorCode = "auth_required";
179
+ errorMessage = AGY_SIGN_IN_MESSAGE;
180
+ } else {
181
+ status = "failed";
182
+ errorCode = exitCode === 0 ? "incomplete_turn" : "agent_error";
183
+ errorMessage = agyError?.message
184
+ ?? firstAgyErrorLine(stderr)
185
+ ?? `Antigravity CLI exited with code ${exitCode ?? "null"} without a result`;
186
+ }
187
+
188
+ if (status === "completed") ctx.onLifecycle?.({ phase: "completed" });
189
+ else if (status === "aborted") ctx.onLifecycle?.({ phase: "cancelled" });
190
+ else ctx.onLifecycle?.({ phase: "error", message: errorMessage ?? "Unknown error" });
191
+
192
+ const conversationId = parser.conversationId;
193
+ const model = parser.model ?? requestedModel;
194
+ const usage = agyUsageRecord(result?.turnUsage ?? null, model);
195
+ return finish({
196
+ exitCode,
197
+ signal: exitSignal,
198
+ status,
199
+ errorMessage,
200
+ errorCode,
201
+ ...(usage ? { usage } : {}),
202
+ model,
203
+ summary: result?.response.trim() || assistantText.join("\n\n").trim() || null,
204
+ sessionParams: conversationId ? { sessionId: conversationId, cwd } : null,
205
+ sessionDisplayId: conversationId,
206
+ // A --conversation id agy could not find starts a new conversation.
207
+ clearSession: Boolean(resumeId && conversationId && conversationId !== resumeId),
208
+ billingType,
209
+ numTurns: result?.numTurns ?? null,
210
+ ...(result?.deniedActions?.length ? { permissionDenials: result.deniedActions } : {}),
211
+ raw: result?.raw ?? null,
212
+ ...(workspace ? { workspace } : {}),
213
+ });
214
+ }
@@ -0,0 +1,48 @@
1
+ import type { AgentSession, ProviderModule, SessionContext } from "../../types.js";
2
+ import { resolveAuthForProvider } from "../../utils/auth.js";
3
+ import { antigravitySessionCodec } from "./codec.js";
4
+
5
+ /**
6
+ * Google Antigravity CLI (`agy`), the successor to Gemini CLI for Google
7
+ * accounts (Free, AI Pro, AI Ultra) since Gemini CLI stopped serving them on
8
+ * 2026-06-18. Driven through its headless `stream-json` mode, which keeps one
9
+ * process per session and resumes conversations with `--conversation`.
10
+ *
11
+ * Install: `curl -fsSL https://antigravity.google/cli/install.sh | bash`,
12
+ * then run `agy` once to sign in. Headless runs reuse the cached sign-in.
13
+ */
14
+ export const antigravityProvider: ProviderModule = {
15
+ type: "antigravity",
16
+ capabilities: {
17
+ sessions: true,
18
+ modelDiscovery: true,
19
+ quotaProbing: false,
20
+ mcp: false,
21
+ skills: true,
22
+ instructions: true,
23
+ workspace: true,
24
+ planMode: true,
25
+ concurrentSend: false,
26
+ cancelQueuedMessage: false,
27
+ stopTask: false,
28
+ modes: true,
29
+ resume: true,
30
+ modelVariants: false,
31
+ permissionRequests: false,
32
+ questionRequests: false,
33
+ upstreamProviderDisconnect: false,
34
+ sessionModelChange: false,
35
+ sessionVariantChange: false,
36
+ sessionEffortChange: false,
37
+ sessionModeChange: false,
38
+ },
39
+ // Everything below loads lazily on first use.
40
+ execute: async (ctx) => (await import("./execute.js")).executeAntigravityProvider(ctx),
41
+ createSession: async (ctx: SessionContext): Promise<AgentSession> =>
42
+ (await import("./session.js")).createAntigravitySession(ctx),
43
+ resolveAuth: (ctx) => resolveAuthForProvider("antigravity", ctx),
44
+ sessionCodec: antigravitySessionCodec,
45
+ listModels: (options) => import("./discovery.js").then((m) => m.listAntigravityModels(options)),
46
+ listModes: (options) => import("./discovery.js").then((m) => m.listAntigravityModes(options)),
47
+ probeCapabilities: (ctx) => import("./probe.js").then((m) => m.probeAntigravityCapabilities(ctx)),
48
+ };