@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,439 @@
1
+ import type { BaseStreamEventFields, ExecutionStatus, StreamEvent, TokenUsage, TurnResult } from "../../types.js";
2
+
3
+ /**
4
+ * Parser for Antigravity CLI (`agy`) headless `stream-json` output.
5
+ *
6
+ * Wire contract (https://antigravity.google/docs/cli/headless): one `init`
7
+ * event per process, `step_update` events while a turn runs, and one `result`
8
+ * event per turn. Every line is `{ "event": <name>, <name>: { ... } }`.
9
+ *
10
+ * - `step_update.step_type` is a closed vocabulary; observed values are
11
+ * `user_input`, `agent_response`, `tool`, and `checkpoint`. A step is
12
+ * `ACTIVE` while it runs and `DONE` when it finishes. `agent_response`
13
+ * streams its text as `text_delta` fragments across ACTIVE updates and a
14
+ * final DONE (short replies arrive as a single DONE).
15
+ * - `result.usage`, `num_turns`, and `duration_seconds` are cumulative over
16
+ * the process's session; `response` covers only the turn that emitted it.
17
+ * The parser turns the cumulative counters back into per-turn deltas.
18
+ *
19
+ * The parser is stateful (text accumulates per step, usage is differenced
20
+ * per turn), so use one instance per `agy` process.
21
+ */
22
+
23
+ export const ANTIGRAVITY_PROVIDER_TYPE = "antigravity";
24
+
25
+ /** Token counters as `agy` reports them. `output` includes `thinking`. */
26
+ export interface AgyUsage {
27
+ inputTokens: number;
28
+ outputTokens: number;
29
+ thinkingTokens: number;
30
+ cacheReadTokens: number;
31
+ totalTokens: number;
32
+ }
33
+
34
+ /** A parsed `result` event, with per-turn deltas alongside the raw totals. */
35
+ export interface AgyResult {
36
+ /** `SUCCESS`, `ERROR`, `CANCELED`, `INTERRUPTED`, `INVALID`, `WAITING`, or `RUNNING`. */
37
+ status: string;
38
+ /** Response text for the turn that emitted this result. */
39
+ response: string;
40
+ error: string | null;
41
+ conversationId: string | null;
42
+ /** Cumulative over the process's session. */
43
+ numTurns: number | null;
44
+ /** This turn's share of the cumulative `duration_seconds`, in ms. */
45
+ turnDurationMs: number | null;
46
+ /** Cumulative over the process's session. */
47
+ usage: AgyUsage | null;
48
+ /** This turn's share of the cumulative usage. */
49
+ turnUsage: AgyUsage | null;
50
+ /** Tools the CLI soft-denied because no permission rule allowed them. */
51
+ deniedActions: unknown[] | null;
52
+ structuredOutput: unknown;
53
+ raw: Record<string, unknown>;
54
+ }
55
+
56
+ /** `AGY_ERROR: {...}` stderr line printed when a turn fails on an agent or model API error. */
57
+ export interface AgyErrorReport {
58
+ message: string | null;
59
+ status: string | null;
60
+ code: number | string | null;
61
+ retryable: boolean | null;
62
+ raw: Record<string, unknown>;
63
+ }
64
+
65
+ export interface AgyStreamParserOptions {
66
+ /** Emit `assistant_delta` / `thinking_delta` events for streamed fragments. */
67
+ includePartialMessages?: boolean;
68
+ /** Conversation id already known (resume), used until the stream reports one. */
69
+ conversationId?: string | null;
70
+ }
71
+
72
+ export interface AgyParsedLine {
73
+ events: StreamEvent[];
74
+ result: AgyResult | null;
75
+ }
76
+
77
+ function rec(value: unknown): Record<string, unknown> | null {
78
+ return typeof value === "object" && value !== null && !Array.isArray(value)
79
+ ? value as Record<string, unknown>
80
+ : null;
81
+ }
82
+
83
+ function str(value: unknown): string | null {
84
+ return typeof value === "string" && value.length > 0 ? value : null;
85
+ }
86
+
87
+ function num(value: unknown): number | null {
88
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
89
+ }
90
+
91
+ function parseJsonObject(line: string): Record<string, unknown> | null {
92
+ const trimmed = line.trim();
93
+ if (!trimmed.startsWith("{")) return null;
94
+ try {
95
+ return rec(JSON.parse(trimmed));
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
100
+
101
+ export function parseAgyUsage(value: unknown): AgyUsage | null {
102
+ const u = rec(value);
103
+ if (!u) return null;
104
+ const inputTokens = num(u["input_tokens"]) ?? 0;
105
+ const outputTokens = num(u["output_tokens"]) ?? 0;
106
+ return {
107
+ inputTokens,
108
+ outputTokens,
109
+ thinkingTokens: num(u["thinking_tokens"]) ?? 0,
110
+ cacheReadTokens: num(u["cache_read_tokens"]) ?? 0,
111
+ totalTokens: num(u["total_tokens"]) ?? inputTokens + outputTokens,
112
+ };
113
+ }
114
+
115
+ function subtractUsage(total: AgyUsage, previous: AgyUsage | null): AgyUsage {
116
+ if (!previous) return total;
117
+ const diff = (a: number, b: number) => Math.max(0, a - b);
118
+ return {
119
+ inputTokens: diff(total.inputTokens, previous.inputTokens),
120
+ outputTokens: diff(total.outputTokens, previous.outputTokens),
121
+ thinkingTokens: diff(total.thinkingTokens, previous.thinkingTokens),
122
+ cacheReadTokens: diff(total.cacheReadTokens, previous.cacheReadTokens),
123
+ totalTokens: diff(total.totalTokens, previous.totalTokens),
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Map `agy` token counters onto agentex `TokenUsage`. `input_tokens` already
129
+ * includes cache reads (like Codex), and `output_tokens` already includes
130
+ * thinking, so the counters pass through without re-adding anything.
131
+ */
132
+ export function agyUsageToTokenUsage(usage: AgyUsage): TokenUsage {
133
+ return {
134
+ inputTokens: usage.inputTokens,
135
+ outputTokens: usage.outputTokens,
136
+ cachedInputTokens: usage.cacheReadTokens,
137
+ };
138
+ }
139
+
140
+ /** Key a usage record by model. `agy` reports the model only when one was pinned. */
141
+ export function agyUsageRecord(usage: AgyUsage | null, model: string | null): Record<string, TokenUsage> | undefined {
142
+ if (!usage) return undefined;
143
+ if (usage.inputTokens === 0 && usage.outputTokens === 0 && usage.cacheReadTokens === 0) return undefined;
144
+ return { [model ?? "default"]: agyUsageToTokenUsage(usage) };
145
+ }
146
+
147
+ function stringifyOutput(value: unknown): string {
148
+ if (value == null) return "";
149
+ if (typeof value === "string") return value;
150
+ try {
151
+ return JSON.stringify(value);
152
+ } catch {
153
+ return String(value);
154
+ }
155
+ }
156
+
157
+ export class AgyStreamParser {
158
+ conversationId: string | null;
159
+ model: string | null = null;
160
+ permissionMode: string | null = null;
161
+
162
+ private readonly includePartial: boolean;
163
+ private readonly responseText = new Map<number, string>();
164
+ private readonly thinkingText = new Map<number, string>();
165
+ private readonly toolCallsOpened = new Set<number>();
166
+ private previousUsage: AgyUsage | null = null;
167
+ private previousDurationSec = 0;
168
+
169
+ constructor(options: AgyStreamParserOptions = {}) {
170
+ this.includePartial = options.includePartialMessages === true;
171
+ this.conversationId = options.conversationId ?? null;
172
+ }
173
+
174
+ parseLine(line: string): AgyParsedLine {
175
+ const event = parseJsonObject(line);
176
+ if (!event) return { events: [], result: null };
177
+ const kind = str(event["event"]);
178
+ switch (kind) {
179
+ case "init":
180
+ return { events: this.onInit(event), result: null };
181
+ case "step_update":
182
+ return { events: this.onStep(event), result: null };
183
+ case "result":
184
+ return this.onResult(event);
185
+ default:
186
+ return {
187
+ events: [{ type: "unknown", subtype: kind ?? "unknown", ...this.base(event) }],
188
+ result: null,
189
+ };
190
+ }
191
+ }
192
+
193
+ private noteConversation(id: unknown): void {
194
+ const value = str(id);
195
+ if (value) this.conversationId = value;
196
+ }
197
+
198
+ private base(raw: Record<string, unknown>, messageId: string | null = null): BaseStreamEventFields {
199
+ return {
200
+ timestamp: new Date().toISOString(),
201
+ providerType: ANTIGRAVITY_PROVIDER_TYPE,
202
+ sessionId: this.conversationId,
203
+ messageId,
204
+ eventId: null,
205
+ turnId: null,
206
+ parentToolCallId: null,
207
+ raw,
208
+ };
209
+ }
210
+
211
+ private onInit(event: Record<string, unknown>): StreamEvent[] {
212
+ const init = rec(event["init"]) ?? {};
213
+ this.noteConversation(event["conversation_id"] ?? init["conversation_id"]);
214
+ this.model = str(init["model"]) ?? this.model;
215
+ this.permissionMode = str(init["permission_mode"]) ?? this.permissionMode;
216
+ const tools = Array.isArray(init["tools"])
217
+ ? (init["tools"] as unknown[]).filter((t): t is string => typeof t === "string")
218
+ : null;
219
+ return [{
220
+ type: "system",
221
+ subtype: "init",
222
+ model: this.model,
223
+ cwd: str(init["cwd"]),
224
+ tools,
225
+ permissionMode: this.permissionMode,
226
+ ...this.base(event),
227
+ }];
228
+ }
229
+
230
+ private onStep(event: Record<string, unknown>): StreamEvent[] {
231
+ const step = rec(event["step_update"]);
232
+ if (!step) return [];
233
+ this.noteConversation(step["conversation_id"]);
234
+ const index = num(step["step_index"]) ?? -1;
235
+ const state = str(step["state"]);
236
+ const done = state !== "ACTIVE";
237
+ const stepId = `${this.conversationId ?? "agy"}:${index}`;
238
+ const type = str(step["step_type"]);
239
+
240
+ // Thinking is not part of the documented stream, but the step schema has
241
+ // room for it; surface it if a build sends it rather than dropping it.
242
+ const events = this.onThinking(step, event, index, stepId, done);
243
+
244
+ if (type === "user_input" || type === "checkpoint") return events;
245
+
246
+ if (type === "agent_response") {
247
+ const delta = typeof step["text_delta"] === "string" ? step["text_delta"] as string : "";
248
+ const text = (this.responseText.get(index) ?? "") + delta;
249
+ this.responseText.set(index, text);
250
+ if (delta && this.includePartial) {
251
+ events.push({ type: "assistant_delta", text: delta, blockIndex: 0, ...this.base(event, stepId) });
252
+ }
253
+ if (done) {
254
+ this.responseText.delete(index);
255
+ if (text.trim()) events.push({ type: "assistant", text, ...this.base(event, stepId) });
256
+ }
257
+ return events;
258
+ }
259
+
260
+ const toolInfo = rec(step["tool_info"]);
261
+ const subagentInfo = rec(step["subagent_info"]);
262
+ if (type === "tool" || toolInfo || subagentInfo) {
263
+ const name = str(step["tool_name"]) ?? str(toolInfo?.["name"]) ?? (subagentInfo ? "subagent" : "tool");
264
+ if (!this.toolCallsOpened.has(index)) {
265
+ this.toolCallsOpened.add(index);
266
+ events.push({
267
+ type: "tool_call",
268
+ toolCallId: stepId,
269
+ name,
270
+ input: toolInfo?.["parameters"] ?? subagentInfo ?? null,
271
+ ...this.base(event, stepId),
272
+ });
273
+ }
274
+ if (done) {
275
+ this.toolCallsOpened.delete(index);
276
+ const error = rec(toolInfo?.["error"]);
277
+ const output = stringifyOutput(toolInfo?.["output"] ?? (subagentInfo ? subagentInfo : null));
278
+ events.push({
279
+ type: "tool_result",
280
+ toolCallId: stepId,
281
+ toolName: name,
282
+ content: output || str(error?.["message"]) || "",
283
+ isError: error !== null,
284
+ exitCode: null,
285
+ ...this.base(event, stepId),
286
+ });
287
+ }
288
+ return events;
289
+ }
290
+
291
+ // Forward-compat: a step type this parser does not model yet.
292
+ if (done) events.push({ type: "unknown", subtype: type ?? "step_update", ...this.base(event, stepId) });
293
+ return events;
294
+ }
295
+
296
+ private onThinking(
297
+ step: Record<string, unknown>,
298
+ event: Record<string, unknown>,
299
+ index: number,
300
+ stepId: string,
301
+ done: boolean,
302
+ ): StreamEvent[] {
303
+ const delta = typeof step["thinking_delta"] === "string" ? step["thinking_delta"] as string : "";
304
+ if (!delta && !this.thinkingText.has(index)) return [];
305
+ const events: StreamEvent[] = [];
306
+ const text = (this.thinkingText.get(index) ?? "") + delta;
307
+ this.thinkingText.set(index, text);
308
+ if (delta && this.includePartial) {
309
+ events.push({ type: "thinking_delta", text: delta, blockIndex: 0, ...this.base(event, stepId) });
310
+ }
311
+ if (done) {
312
+ this.thinkingText.delete(index);
313
+ if (text.trim()) events.push({ type: "thinking", text, ...this.base(event, stepId) });
314
+ }
315
+ return events;
316
+ }
317
+
318
+ private onResult(event: Record<string, unknown>): AgyParsedLine {
319
+ const payload = rec(event["result"]) ?? {};
320
+ this.noteConversation(payload["conversation_id"]);
321
+ const status = str(payload["status"]) ?? "ERROR";
322
+ const usage = parseAgyUsage(payload["usage"]);
323
+ const turnUsage = usage ? subtractUsage(usage, this.previousUsage) : null;
324
+ if (usage) this.previousUsage = usage;
325
+ const durationSec = num(payload["duration_seconds"]);
326
+ const turnDurationMs = durationSec == null
327
+ ? null
328
+ : Math.round(Math.max(0, durationSec - this.previousDurationSec) * 1000);
329
+ if (durationSec != null) this.previousDurationSec = durationSec;
330
+ // Steps of a finished turn never complete; don't leak them into the next.
331
+ this.responseText.clear();
332
+ this.thinkingText.clear();
333
+ this.toolCallsOpened.clear();
334
+
335
+ const result: AgyResult = {
336
+ status,
337
+ response: typeof payload["response"] === "string" ? payload["response"] as string : "",
338
+ error: str(payload["error"]),
339
+ conversationId: this.conversationId,
340
+ numTurns: num(payload["num_turns"]),
341
+ turnDurationMs,
342
+ usage,
343
+ turnUsage,
344
+ deniedActions: Array.isArray(payload["denied_actions"]) ? payload["denied_actions"] as unknown[] : null,
345
+ structuredOutput: payload["structured_output"],
346
+ raw: event,
347
+ };
348
+ return {
349
+ events: [{
350
+ type: "result",
351
+ text: result.response,
352
+ costUsd: null,
353
+ isError: status !== "SUCCESS",
354
+ stopReason: null,
355
+ terminalReason: status.toLowerCase(),
356
+ numTurns: result.numTurns,
357
+ durationMs: turnDurationMs,
358
+ ...this.base(event),
359
+ }],
360
+ result,
361
+ };
362
+ }
363
+ }
364
+
365
+ /** Parse a complete `stream-json` transcript. Returns every turn's result in order. */
366
+ export function parseAgyStreamJson(stdout: string, options?: AgyStreamParserOptions): {
367
+ events: StreamEvent[];
368
+ results: AgyResult[];
369
+ conversationId: string | null;
370
+ model: string | null;
371
+ } {
372
+ const parser = new AgyStreamParser(options);
373
+ const events: StreamEvent[] = [];
374
+ const results: AgyResult[] = [];
375
+ for (const line of stdout.split(/\r?\n/)) {
376
+ const parsed = parser.parseLine(line);
377
+ events.push(...parsed.events);
378
+ if (parsed.result) results.push(parsed.result);
379
+ }
380
+ return { events, results, conversationId: parser.conversationId, model: parser.model };
381
+ }
382
+
383
+ /** Parse an `AGY_ERROR: {...}` stderr line, or return null for any other line. */
384
+ export function parseAgyErrorLine(line: string): AgyErrorReport | null {
385
+ const match = line.match(/AGY_ERROR:\s*(\{.*\})\s*$/);
386
+ if (!match?.[1]) return null;
387
+ const raw = parseJsonObject(match[1]);
388
+ if (!raw) return null;
389
+ const code = raw["code"] ?? raw["http_code"] ?? raw["grpc_code"] ?? raw["status_code"] ?? null;
390
+ return {
391
+ message: str(raw["message"]) ?? str(raw["error"]),
392
+ status: str(raw["status"]) ?? str(raw["canonical_status"]) ?? str(raw["canonicalStatus"]),
393
+ code: typeof code === "number" || typeof code === "string" ? code : null,
394
+ retryable: typeof raw["retryable"] === "boolean" ? raw["retryable"] as boolean : null,
395
+ raw,
396
+ };
397
+ }
398
+
399
+ const AGY_AUTH_PROMPT_RE = /Authentication required|Please (?:visit the URL to log in|sign in)|authentication failed or timed out|not signed in|Failed to sign in/i;
400
+
401
+ /** True when `agy` output says the CLI has no usable sign-in. */
402
+ export function isAgyAuthRequired(text: string): boolean {
403
+ return AGY_AUTH_PROMPT_RE.test(text);
404
+ }
405
+
406
+ const RATE_LIMIT_RE = /RESOURCE_EXHAUSTED|rate.?limit|quota|\b429\b/i;
407
+
408
+ /** Error code for a failed turn, from the most specific signal available. */
409
+ export function classifyAgyFailure(result: AgyResult | null, agyError: AgyErrorReport | null): string {
410
+ const text = [result?.error, agyError?.message, agyError?.status, agyError?.code].filter(Boolean).join(" ");
411
+ if (isAgyAuthRequired(text) || /UNAUTHENTICATED|PERMISSION_DENIED|\b401\b/.test(text)) return "auth_required";
412
+ if (RATE_LIMIT_RE.test(text)) return "rate_limited";
413
+ if (/invalid model selection|not recognized as a known model/i.test(text)) return "invalid_model";
414
+ if (result?.status === "WAITING") return "waiting_for_input";
415
+ if (result?.status === "INVALID") return "invalid_state";
416
+ if (result?.status === "RUNNING") return "incomplete_turn";
417
+ return "agent_error";
418
+ }
419
+
420
+ /** Map an `agy` result status onto a one-shot `ExecutionStatus`. */
421
+ export function mapAgyExecutionStatus(status: string): ExecutionStatus {
422
+ switch (status) {
423
+ case "SUCCESS":
424
+ return "completed";
425
+ case "CANCELED":
426
+ case "INTERRUPTED":
427
+ return "aborted";
428
+ case "WAITING":
429
+ return "blocked";
430
+ default:
431
+ return "failed";
432
+ }
433
+ }
434
+
435
+ /** Map an `agy` result status onto a session `TurnResult` status. */
436
+ export function mapAgyTurnStatus(status: string): TurnResult["status"] {
437
+ const mapped = mapAgyExecutionStatus(status);
438
+ return mapped === "blocked" ? "failed" : mapped;
439
+ }
@@ -0,0 +1,89 @@
1
+ import type { ProviderRuntimeContext, ProviderRuntimeReport } from "../../types.js";
2
+ import { buildEnv, ensurePathInEnv } from "../../utils/env.js";
3
+ import { runChildProcess } from "../../utils/process.js";
4
+ import { listAntigravityModels } from "./discovery.js";
5
+ import { AGY_SIGN_IN_MESSAGE, findAgyBinary } from "./runtime.js";
6
+
7
+ /** Flags the adapter relies on. All shipped together in Antigravity CLI 1.1.15. */
8
+ const REQUIRED_FLAGS = ["--input-format", "--output-format", "--conversation", "--mode", "--model"];
9
+
10
+ export async function probeAntigravityCapabilities(
11
+ ctx: ProviderRuntimeContext = {},
12
+ ): Promise<ProviderRuntimeReport> {
13
+ let resolved;
14
+ try {
15
+ resolved = await findAgyBinary(ctx);
16
+ } catch (error) {
17
+ return {
18
+ binary: {
19
+ status: "missing",
20
+ command: null,
21
+ version: null,
22
+ protocolProfile: null,
23
+ reason: error instanceof Error ? error.message : String(error),
24
+ },
25
+ capabilities: {},
26
+ };
27
+ }
28
+ const env = buildEnv(ctx.env);
29
+ ensurePathInEnv(env);
30
+ const cwd = ctx.cwd ?? process.cwd();
31
+ const run = (runId: string, args: string[], timeoutSec: number) => runChildProcess({
32
+ runId,
33
+ command: resolved.bin,
34
+ args: [...resolved.prefixArgs, ...args],
35
+ cwd,
36
+ env,
37
+ timeoutSec,
38
+ }).catch(() => null);
39
+
40
+ const [versionRun, helpRun, models] = await Promise.all([
41
+ run("antigravity-version-probe", ["--version"], 5),
42
+ run("antigravity-protocol-probe", ["--help"], 10),
43
+ listAntigravityModels(ctx).catch((error: unknown) => (error instanceof Error ? error : new Error(String(error)))),
44
+ ]);
45
+ const version = versionRun
46
+ ? (`${versionRun.stdout}\n${versionRun.stderr}`).match(/\d+\.\d+(?:\.\d+)?/)?.[0] ?? null
47
+ : null;
48
+ const help = helpRun ? `${helpRun.stdout}\n${helpRun.stderr}` : "";
49
+ const missingFlags = REQUIRED_FLAGS.filter((flag) => !help.includes(flag));
50
+ const protocolSupported = help.length > 0 && missingFlags.length === 0 && /stream-json/.test(help);
51
+ const modelError = models instanceof Error ? models : null;
52
+ const modelDiscovery = Array.isArray(models) && models.length > 0;
53
+ const signedOut = modelError?.message === AGY_SIGN_IN_MESSAGE;
54
+
55
+ const supported = (value: boolean, reason?: string) => ({
56
+ supported: value,
57
+ status: value ? "supported" as const : "upgrade_required" as const,
58
+ ...(!value && reason ? { reason } : {}),
59
+ });
60
+ return {
61
+ binary: {
62
+ status: protocolSupported ? "supported" : "upgrade_required",
63
+ command: resolved.bin,
64
+ version,
65
+ protocolProfile: protocolSupported ? "agy-stream-json-v1" : null,
66
+ ...(!protocolSupported
67
+ ? { reason: `Update Antigravity CLI (\`agy update\`): missing ${missingFlags.join(", ") || "stream-json support"}` }
68
+ : {}),
69
+ },
70
+ capabilities: {
71
+ sessions: supported(protocolSupported, "Antigravity stream-json input is unavailable"),
72
+ resume: supported(protocolSupported, "Antigravity --conversation resume is unavailable"),
73
+ modelDiscovery: modelDiscovery
74
+ ? supported(true)
75
+ : {
76
+ supported: false,
77
+ // Not signed in is a setup step, not an outdated CLI.
78
+ status: signedOut ? "missing" as const : "upgrade_required" as const,
79
+ reason: signedOut ? AGY_SIGN_IN_MESSAGE : modelError?.message ?? "Antigravity CLI did not list any models",
80
+ },
81
+ planMode: supported(protocolSupported),
82
+ modes: supported(protocolSupported),
83
+ sessionModelChange: supported(false),
84
+ sessionVariantChange: supported(false),
85
+ sessionEffortChange: supported(false),
86
+ sessionModeChange: supported(false),
87
+ },
88
+ };
89
+ }
@@ -0,0 +1,64 @@
1
+ import * as path from "node:path";
2
+ import type { AgentMode, ProviderConfig, ProviderRuntimeContext } from "../../types.js";
3
+ import { findBinary, type ResolvedBinary } from "../../utils/binary.js";
4
+
5
+ /** Running `agy` with no arguments signs in through the browser. */
6
+ export const AGY_LOGIN_COMMAND = "agy";
7
+
8
+ export const AGY_SIGN_IN_MESSAGE = "Antigravity CLI is not signed in. Run `agy` once to sign in.";
9
+
10
+ export function findAgyBinary(ctx: ProviderRuntimeContext = {}): Promise<ResolvedBinary> {
11
+ return findBinary("agy", ctx.config?.command);
12
+ }
13
+
14
+ /**
15
+ * The CLI's execution modes (`--mode`). `default` is request-review: file
16
+ * edits wait for approval, which headless runs settle by policy. Shell
17
+ * commands follow the permission rules in every mode.
18
+ */
19
+ export const ANTIGRAVITY_MODES: AgentMode[] = [
20
+ { id: "default", name: "Default", description: "Review file edits before they are applied" },
21
+ { id: "accept-edits", name: "Accept edits", description: "Apply file edits without asking" },
22
+ { id: "plan", name: "Plan", description: "Investigate read-only and propose a plan before editing" },
23
+ ];
24
+
25
+ /** The `--mode` value for a config, or null for the CLI default. */
26
+ export function resolveAgyMode(config: ProviderConfig): string | null {
27
+ if (config.planMode) return "plan";
28
+ const requested = config.modeId ?? config.mode;
29
+ if (!requested || requested === "default") return null;
30
+ return requested;
31
+ }
32
+
33
+ /**
34
+ * Arguments for a headless `agy` process that reads user messages as NDJSON
35
+ * on stdin and writes `stream-json` events. One process serves a whole
36
+ * session; one-shot runs write a single message and close stdin.
37
+ */
38
+ export function buildAgyArgs(config: ProviderConfig, options: { resumeId: string | null; model: string | null }): string[] {
39
+ const args = ["--input-format", "stream-json", "--output-format", "stream-json"];
40
+ if (options.resumeId) args.push("--conversation", options.resumeId);
41
+ if (options.model) args.push("--model", options.model);
42
+ if (config.effort) args.push("--effort", config.effort);
43
+ const mode = resolveAgyMode(config);
44
+ if (mode) args.push("--mode", mode);
45
+ // Plan mode is the more conservative intent and wins over skipPermissions.
46
+ if (config.skipPermissions && !config.planMode) args.push("--dangerously-skip-permissions");
47
+ if (config.sandbox) args.push("--sandbox");
48
+ if (config.extraArgs) args.push(...config.extraArgs);
49
+ return args;
50
+ }
51
+
52
+ /**
53
+ * The conversation to resume from saved params, or null. Antigravity scopes
54
+ * conversations to the directory they ran in, so params saved under a
55
+ * different cwd start fresh instead of resuming somewhere unexpected.
56
+ */
57
+ export function readAgyResumeId(params: Record<string, unknown> | null | undefined, cwd: string): string | null {
58
+ if (!params) return null;
59
+ const id = params["sessionId"] ?? params["session_id"] ?? params["conversationId"];
60
+ if (typeof id !== "string" || !id.trim()) return null;
61
+ const savedCwd = params["cwd"];
62
+ if (typeof savedCwd === "string" && savedCwd && path.resolve(savedCwd) !== path.resolve(cwd)) return null;
63
+ return id.trim();
64
+ }