talon-agent 5.2.2 → 5.4.0

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 (74) hide show
  1. package/README.md +10 -6
  2. package/package.json +1 -1
  3. package/src/app.ts +78 -0
  4. package/src/backend/agy/auth.ts +128 -0
  5. package/src/backend/agy/constants.ts +88 -0
  6. package/src/backend/agy/doctor.ts +161 -0
  7. package/src/backend/agy/effort.ts +76 -0
  8. package/src/backend/agy/events.ts +402 -0
  9. package/src/backend/agy/factory.ts +137 -0
  10. package/src/backend/agy/handler/index.ts +9 -0
  11. package/src/backend/agy/handler/message.ts +444 -0
  12. package/src/backend/agy/init.ts +64 -0
  13. package/src/backend/agy/mcp/config.ts +336 -0
  14. package/src/backend/agy/mcp/register.ts +134 -0
  15. package/src/backend/agy/models.ts +335 -0
  16. package/src/backend/agy/one-shot.ts +301 -0
  17. package/src/backend/agy/process/child.ts +378 -0
  18. package/src/backend/agy/process/orphans.ts +106 -0
  19. package/src/backend/agy/sessions.ts +90 -0
  20. package/src/backend/agy/state.ts +77 -0
  21. package/src/backend/builtins.ts +1 -0
  22. package/src/backend/codex/mcp-config.ts +1 -1
  23. package/src/backend/openai-agents/mcp-pool.ts +1 -1
  24. package/src/backend/runtime/index.ts +1 -1
  25. package/src/cli/commands/backup.ts +396 -0
  26. package/src/cli/config-view.ts +5 -0
  27. package/src/cli/config.ts +4 -2
  28. package/src/cli/events.ts +14 -0
  29. package/src/cli/index.ts +64 -45
  30. package/src/cli/setup.ts +49 -0
  31. package/src/core/agent-runtime/model-ref.ts +1 -0
  32. package/src/core/backup/archive/digest.ts +77 -0
  33. package/src/core/backup/archive/tar.ts +567 -0
  34. package/src/core/backup/archive/zstd.ts +31 -0
  35. package/src/core/backup/index.ts +54 -0
  36. package/src/core/backup/plan.ts +273 -0
  37. package/src/core/backup/restore.ts +410 -0
  38. package/src/core/backup/scheduler.ts +357 -0
  39. package/src/core/backup/snapshot.ts +408 -0
  40. package/src/core/backup/status.ts +194 -0
  41. package/src/core/backup/store.ts +312 -0
  42. package/src/core/backup/targets.ts +281 -0
  43. package/src/core/backup/types.ts +96 -0
  44. package/src/core/backup/upload.ts +172 -0
  45. package/src/core/bus/events.ts +45 -1
  46. package/src/core/config/index.ts +53 -0
  47. package/src/core/engine/gateway-actions/backup/index.ts +129 -0
  48. package/src/core/engine/gateway-actions/index.ts +4 -0
  49. package/src/core/mcp-hub/talon-server.ts +1 -1
  50. package/src/core/plugin/actions.ts +34 -0
  51. package/src/core/plugin/index.ts +5 -1
  52. package/src/core/tools/{ops/bridge.ts → bridge.ts} +7 -2
  53. package/src/core/tools/index.ts +2 -0
  54. package/src/core/tools/ops/backup.ts +67 -0
  55. package/src/core/tools/types.ts +2 -1
  56. package/src/core/update/self-update.ts +3 -0
  57. package/src/frontend/discord/callbacks/components/index.ts +3 -0
  58. package/src/frontend/discord/commands/backup.ts +203 -0
  59. package/src/frontend/discord/commands/definitions.ts +35 -0
  60. package/src/frontend/discord/commands/router.ts +3 -0
  61. package/src/frontend/telegram/callbacks/backup.ts +55 -0
  62. package/src/frontend/telegram/callbacks/index.ts +8 -0
  63. package/src/frontend/telegram/commands/backup.ts +209 -0
  64. package/src/frontend/telegram/commands/definitions.ts +4 -0
  65. package/src/frontend/telegram/commands/index.ts +2 -0
  66. package/src/storage/backup/index.ts +82 -0
  67. package/src/storage/backup/repo.ts +164 -0
  68. package/src/storage/db.ts +20 -0
  69. package/src/storage/sql/backups.sql +46 -0
  70. package/src/storage/sql/db.sql +8 -0
  71. package/src/storage/sql/schema.sql +30 -0
  72. package/src/storage/sql/statements.generated.ts +60 -1
  73. package/src/util/log.ts +1 -0
  74. /package/src/core/tools/{ops/mcp-env.ts → mcp-env.ts} +0 -0
@@ -0,0 +1,402 @@
1
+ /**
2
+ * Antigravity stream-json event model.
3
+ *
4
+ * The CLI emits newline-delimited JSON on stdout: exactly one `init`,
5
+ * any number of `step_update`s, and exactly one `result` per turn.
6
+ * Diagnostics never appear here — they go to stderr. This module owns
7
+ * the wire types, the line parser, and the translation of one
8
+ * `step_update` into shared stream-state mutations.
9
+ *
10
+ * The single agy-specific wrinkle is MCP: every MCP tool reaches the
11
+ * model through one generic native tool, `call_mcp_tool`, whose
12
+ * parameters are `{ServerName, ToolName, Arguments}`. Reporting that
13
+ * name verbatim would hide `end_turn` / `send` from the terminator
14
+ * logic and split the fleet-wide `tool_calls.*` metric keys, so
15
+ * [describeAgyTool] unwraps it and every consumer downstream sees the
16
+ * bare tool name like any other backend.
17
+ */
18
+
19
+ import { log, logWarn } from "../../util/log.js";
20
+ import { isTurnTerminator } from "../../core/tools/index.js";
21
+ import {
22
+ appendText,
23
+ recordToolUse,
24
+ recordToolCall,
25
+ type StreamState,
26
+ } from "../runtime/index.js";
27
+
28
+ // ── Wire types ──────────────────────────────────────────────────────────────
29
+
30
+ /** Token counters agy reports per step and per result. */
31
+ export interface AgyUsage {
32
+ input_tokens?: number;
33
+ output_tokens?: number;
34
+ /**
35
+ * Reasoning tokens. A SUBSET of `output_tokens`, not an addition to
36
+ * it — a 28-output/27-thinking turn produced a two-token reply. Never
37
+ * add the two together or every reasoning turn double-counts.
38
+ */
39
+ thinking_tokens?: number;
40
+ cache_read_tokens?: number;
41
+ total_tokens?: number;
42
+ }
43
+
44
+ /** `tool_info` on a tool step: the call, and its result once it lands. */
45
+ interface AgyToolInfo {
46
+ name?: string;
47
+ parameters?: Record<string, unknown>;
48
+ output?: string;
49
+ error?: { type?: string; message?: string };
50
+ }
51
+
52
+ type AgyStepState = "ACTIVE" | "DONE" | "ERROR";
53
+ type AgyStepType =
54
+ "user_input" | "agent_response" | "tool" | "checkpoint" | (string & {});
55
+
56
+ export interface AgyStepUpdate {
57
+ conversation_id?: string;
58
+ step_index?: number;
59
+ state?: AgyStepState;
60
+ step_type?: AgyStepType;
61
+ tool_name?: string;
62
+ text_delta?: string;
63
+ duration_seconds?: number;
64
+ usage?: AgyUsage;
65
+ tool_info?: AgyToolInfo;
66
+ }
67
+
68
+ interface AgyInit {
69
+ cwd?: string;
70
+ tools?: string[];
71
+ permission_mode?: string;
72
+ model?: string;
73
+ agent?: string;
74
+ }
75
+
76
+ /** Terminal states the CLI reports on `result.status`. */
77
+ type AgyResultStatus =
78
+ | "SUCCESS"
79
+ | "ERROR"
80
+ | "CANCELED"
81
+ | "INTERRUPTED"
82
+ | "INVALID"
83
+ | "WAITING"
84
+ | "RUNNING";
85
+
86
+ export interface AgyResult {
87
+ conversation_id?: string;
88
+ status?: AgyResultStatus;
89
+ response?: string;
90
+ error?: string;
91
+ duration_seconds?: number;
92
+ /** Cumulative over the session in stream-json input mode, not per turn. */
93
+ num_turns?: number;
94
+ /** Cumulative over the session in stream-json input mode, not per turn. */
95
+ usage?: AgyUsage;
96
+ structured_output?: unknown;
97
+ }
98
+
99
+ export type AgyEvent =
100
+ | { event: "init"; conversation_id?: string; init?: AgyInit }
101
+ | { event: "step_update"; step_update?: AgyStepUpdate }
102
+ | { event: "result"; result?: AgyResult };
103
+
104
+ // ── Parsing ─────────────────────────────────────────────────────────────────
105
+
106
+ /**
107
+ * Parse one NDJSON line into an [AgyEvent], or null when the line is
108
+ * blank, not JSON, or carries an `event` name this build doesn't know.
109
+ *
110
+ * Unknown event names are dropped rather than thrown on — the CLI's own
111
+ * stdin protocol does the same in the other direction, precisely so a
112
+ * newer CLI streaming a future event type doesn't crash the consumer.
113
+ */
114
+ export function parseAgyLine(line: string): AgyEvent | null {
115
+ const trimmed = line.trim();
116
+ if (!trimmed) return null;
117
+ let parsed: unknown;
118
+ try {
119
+ parsed = JSON.parse(trimmed);
120
+ } catch {
121
+ return null;
122
+ }
123
+ if (!parsed || typeof parsed !== "object") return null;
124
+ const name = (parsed as { event?: unknown }).event;
125
+ if (name !== "init" && name !== "step_update" && name !== "result") {
126
+ return null;
127
+ }
128
+ return parsed as AgyEvent;
129
+ }
130
+
131
+ /** Parse a whole NDJSON document (a fixture, a captured run). */
132
+ export function parseAgyStream(text: string): AgyEvent[] {
133
+ const events: AgyEvent[] = [];
134
+ for (const line of text.split("\n")) {
135
+ const event = parseAgyLine(line);
136
+ if (event) events.push(event);
137
+ }
138
+ return events;
139
+ }
140
+
141
+ // ── Tool identity ───────────────────────────────────────────────────────────
142
+
143
+ /** A tool step reduced to the vocabulary the rest of Talon speaks. */
144
+ export interface AgyToolShape {
145
+ /** Bare tool name — `check_time`, not `call_mcp_tool`. */
146
+ name: string;
147
+ /** MCP server the call was routed to, when it was an MCP call. */
148
+ server?: string;
149
+ input: Record<string, unknown>;
150
+ }
151
+
152
+ function asRecord(value: unknown): Record<string, unknown> {
153
+ return value && typeof value === "object" && !Array.isArray(value)
154
+ ? (value as Record<string, unknown>)
155
+ : {};
156
+ }
157
+
158
+ /**
159
+ * Unwrap a tool step into `{ name, server?, input }`.
160
+ *
161
+ * `call_mcp_tool` carries the real identity in its parameters
162
+ * (`ServerName` / `ToolName` / `Arguments`); every other tool is a
163
+ * native agy tool (`run_command`, `write_to_file`, `view_file`, …)
164
+ * reported under its own name, exactly as codex reports
165
+ * `command_execution` / `file_change`.
166
+ */
167
+ export function describeAgyTool(step: AgyStepUpdate): AgyToolShape {
168
+ const info = step.tool_info;
169
+ const rawName = info?.name ?? step.tool_name ?? "unknown";
170
+ if (rawName !== "call_mcp_tool") {
171
+ return { name: rawName, input: asRecord(info?.parameters) };
172
+ }
173
+ const params = asRecord(info?.parameters);
174
+ const toolName = typeof params.ToolName === "string" ? params.ToolName : "";
175
+ const server =
176
+ typeof params.ServerName === "string" ? params.ServerName : undefined;
177
+ return {
178
+ // A malformed call_mcp_tool (no ToolName) still has to report
179
+ // something; falling back to the wrapper name keeps the
180
+ // call→result pairing intact rather than emitting an empty id.
181
+ name: toolName || rawName,
182
+ ...(server ? { server } : {}),
183
+ input: asRecord(params.Arguments),
184
+ };
185
+ }
186
+
187
+ /** Stable per-turn id for a tool step, so start and end pair up. */
188
+ export function agyToolCallId(step: AgyStepUpdate): string {
189
+ return `${step.conversation_id ?? "agy"}:${step.step_index ?? 0}`;
190
+ }
191
+
192
+ // ── Stream-state translation ────────────────────────────────────────────────
193
+
194
+ export interface AgyEventContext {
195
+ state: StreamState;
196
+ chatId: string;
197
+ /** Tool call ids whose start was reported and await their terminal event. */
198
+ startedToolIds: Set<string>;
199
+ /** Tool call ids already settled — guards duplicate terminal events. */
200
+ settledToolIds: Set<string>;
201
+ toolMetrics: { count: number };
202
+ onStreamDelta?: (accumulated: string, phase?: "thinking" | "text") => void;
203
+ onToolUse?: (
204
+ toolName: string,
205
+ input: Record<string, unknown>,
206
+ meta?: { failed?: boolean },
207
+ ) => void;
208
+ onToolStart?: (
209
+ callId: string,
210
+ toolName: string,
211
+ input: Record<string, unknown>,
212
+ ) => void;
213
+ onToolEnd?: (
214
+ callId: string,
215
+ toolName: string,
216
+ meta?: { failed?: boolean },
217
+ ) => void;
218
+ }
219
+
220
+ export function createAgyEventContext(
221
+ state: StreamState,
222
+ chatId: string,
223
+ callbacks: Pick<
224
+ AgyEventContext,
225
+ "onStreamDelta" | "onToolUse" | "onToolStart" | "onToolEnd"
226
+ > = {},
227
+ ): AgyEventContext {
228
+ return {
229
+ state,
230
+ chatId,
231
+ startedToolIds: new Set<string>(),
232
+ settledToolIds: new Set<string>(),
233
+ toolMetrics: { count: 0 },
234
+ ...callbacks,
235
+ };
236
+ }
237
+
238
+ /**
239
+ * Apply one `step_update` to the shared stream state.
240
+ *
241
+ * `init` and `result` are handled by the process layer (they carry the
242
+ * conversation id and the turn's settlement), so this deals only with
243
+ * the two step types that mutate a turn: `agent_response` text and
244
+ * `tool` lifecycle. `user_input` and `checkpoint` steps are
245
+ * bookkeeping and produce nothing.
246
+ */
247
+ export function applyAgyStep(step: AgyStepUpdate, ctx: AgyEventContext): void {
248
+ if (step.step_type === "agent_response") {
249
+ applyAgentResponse(step, ctx);
250
+ return;
251
+ }
252
+ if (step.step_type === "tool") applyToolStep(step, ctx);
253
+ }
254
+
255
+ function applyAgentResponse(step: AgyStepUpdate, ctx: AgyEventContext): void {
256
+ const delta = step.text_delta;
257
+ if (!delta) return;
258
+ appendText(ctx.state, delta);
259
+ if (!ctx.onStreamDelta) return;
260
+ try {
261
+ ctx.onStreamDelta(
262
+ ctx.state.allResponseText + ctx.state.currentBlockText,
263
+ "text",
264
+ );
265
+ } catch {
266
+ /* non-fatal — a consumer bug must not kill the turn */
267
+ }
268
+ }
269
+
270
+ function applyToolStep(step: AgyStepUpdate, ctx: AgyEventContext): void {
271
+ const id = agyToolCallId(step);
272
+ const shape = describeAgyTool(step);
273
+ if (step.state === "ACTIVE") {
274
+ openToolCall(ctx, id, shape);
275
+ return;
276
+ }
277
+ if (step.state !== "DONE" && step.state !== "ERROR") return;
278
+ if (ctx.settledToolIds.has(id)) return;
279
+ ctx.settledToolIds.add(id);
280
+ settleToolCall(ctx, id, shape, step);
281
+ }
282
+
283
+ function openToolCall(
284
+ ctx: AgyEventContext,
285
+ id: string,
286
+ shape: AgyToolShape,
287
+ ): void {
288
+ if (ctx.startedToolIds.has(id) || !ctx.onToolStart) return;
289
+ ctx.startedToolIds.add(id);
290
+ try {
291
+ ctx.onToolStart(id, shape.name, shape.input);
292
+ } catch {
293
+ /* non-fatal */
294
+ }
295
+ }
296
+
297
+ function settleToolCall(
298
+ ctx: AgyEventContext,
299
+ id: string,
300
+ shape: AgyToolShape,
301
+ step: AgyStepUpdate,
302
+ ): void {
303
+ const failed = step.state === "ERROR" || Boolean(step.tool_info?.error);
304
+ // Every call counts, failed or not — the Claude SDK backend counts
305
+ // each tool_use block regardless of outcome, so agy must too or the
306
+ // fleet-wide `tool_calls.*` keys silently undercount.
307
+ recordToolCall(ctx.chatId, shape.name, "agy");
308
+ ctx.toolMetrics.count += 1;
309
+
310
+ if (failed) {
311
+ const detail = step.tool_info?.error?.message;
312
+ if (detail) {
313
+ logWarn(
314
+ "agent",
315
+ `[${ctx.chatId}] agy tool ${shape.name} failed: ${detail}`,
316
+ );
317
+ }
318
+ reportToolTerminal(ctx, id, shape, { failed: true });
319
+ return;
320
+ }
321
+
322
+ // Only a successful call mutates delivery state: `recordToolUse`
323
+ // captures the delivered-text norm and flips the terminator.
324
+ recordToolUse(ctx.state, shape.name, shape.input);
325
+ reportToolTerminal(ctx, id, shape);
326
+ if (isTurnTerminator(shape.name, shape.input)) {
327
+ log("agent", `[${ctx.chatId}] agy terminator fired: ${shape.name}`);
328
+ }
329
+ }
330
+
331
+ function reportToolTerminal(
332
+ ctx: AgyEventContext,
333
+ id: string,
334
+ shape: AgyToolShape,
335
+ meta?: { failed?: boolean },
336
+ ): void {
337
+ if (ctx.startedToolIds.delete(id) && ctx.onToolEnd) {
338
+ try {
339
+ ctx.onToolEnd(id, shape.name, meta);
340
+ } catch {
341
+ /* non-fatal */
342
+ }
343
+ return;
344
+ }
345
+ if (!ctx.onToolUse) return;
346
+ try {
347
+ ctx.onToolUse(shape.name, shape.input, meta);
348
+ } catch {
349
+ /* non-fatal */
350
+ }
351
+ }
352
+
353
+ // ── Usage ───────────────────────────────────────────────────────────────────
354
+
355
+ /** Talon's per-turn token shape, as read off an agy usage payload. */
356
+ export interface AgyTurnTokens {
357
+ inputTokens: number;
358
+ outputTokens: number;
359
+ cacheRead: number;
360
+ cacheWrite: number;
361
+ }
362
+
363
+ const nonNegative = (n: number | undefined): number => Math.max(0, n ?? 0);
364
+
365
+ /**
366
+ * Map an agy usage payload onto Talon's counters.
367
+ *
368
+ * `thinking_tokens` is deliberately NOT added to `output_tokens`: the
369
+ * CLI already counts reasoning inside output (28 output / 27 thinking
370
+ * for a two-token reply), so summing them would double-charge every
371
+ * reasoning turn. agy reports no cache writes, hence the constant 0 —
372
+ * which is why the backend registers `cacheMetrics: "read"`.
373
+ */
374
+ export function agyUsageToTokens(usage: AgyUsage | undefined): AgyTurnTokens {
375
+ return {
376
+ inputTokens: nonNegative(usage?.input_tokens),
377
+ outputTokens: nonNegative(usage?.output_tokens),
378
+ cacheRead: nonNegative(usage?.cache_read_tokens),
379
+ cacheWrite: 0,
380
+ };
381
+ }
382
+
383
+ /**
384
+ * Difference two cumulative usage snapshots.
385
+ *
386
+ * In `--input-format stream-json` the `result.usage` counters are
387
+ * cumulative over the whole session, so turn N's real cost is
388
+ * `after - before`. Clamped at zero in case the CLI ever resets a
389
+ * counter mid-session.
390
+ */
391
+ export function agyUsageDelta(
392
+ before: AgyTurnTokens | undefined,
393
+ after: AgyTurnTokens,
394
+ ): AgyTurnTokens {
395
+ if (!before) return after;
396
+ return {
397
+ inputTokens: Math.max(0, after.inputTokens - before.inputTokens),
398
+ outputTokens: Math.max(0, after.outputTokens - before.outputTokens),
399
+ cacheRead: Math.max(0, after.cacheRead - before.cacheRead),
400
+ cacheWrite: 0,
401
+ };
402
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Antigravity backend factory — wires Google's `agy` CLI into the
3
+ * registry.
4
+ *
5
+ * Returns a composed `Backend` with every capability slot Talon's core
6
+ * knows about: chat (+ interrupt), background (+ orphan eviction),
7
+ * models, sessions, tools, usage and control. The transport is a
8
+ * long-lived per-chat subprocess (`process.ts`), so the slots that
9
+ * exist for process-shaped backends — `sessions.resetChat`,
10
+ * `tools.refreshTools`, `background.evictOrphanSubprocesses` — are all
11
+ * meaningful here and all implemented.
12
+ */
13
+
14
+ import { registerBackend } from "../../core/agent-runtime/backend-registry.js";
15
+ import type { BackendFactory } from "../../core/agent-runtime/backend-registry.js";
16
+ import { log } from "../../util/log.js";
17
+ import { handlerToEvents } from "../runtime/turn/handler-to-events.js";
18
+ import { interruptChatTurn } from "../runtime/turn/turn-interrupt.js";
19
+ import {
20
+ composeBackend,
21
+ type ChatBackend,
22
+ type BackgroundRunner,
23
+ type ModelCatalog,
24
+ type SessionBackend,
25
+ type SystemControl,
26
+ type ToolRuntime,
27
+ type UsageTelemetry,
28
+ } from "../../core/agent-runtime/capabilities.js";
29
+
30
+ import { initAgyAgent } from "./init.js";
31
+ import { agyDoctorChecks } from "./doctor.js";
32
+ import { handleMessage } from "./handler/index.js";
33
+ import { runOneShotAgent } from "./one-shot.js";
34
+ import { evictOrphanSubprocesses } from "./process/orphans.js";
35
+ import { getState, resetState } from "./state.js";
36
+ import { resetChat, warmSession, refreshTools } from "./sessions.js";
37
+ import { killAllChildren } from "./process/child.js";
38
+ import { unregisterAllMcp } from "./mcp/register.js";
39
+ import {
40
+ resolveModel,
41
+ getDefaultModelId,
42
+ getModelInfo,
43
+ getSettingsPresentation,
44
+ getProviders,
45
+ getProviderModels,
46
+ formatModelError,
47
+ listModels,
48
+ resetModelCache,
49
+ } from "./models.js";
50
+
51
+ const agyFactory: BackendFactory = {
52
+ id: "agy",
53
+ label: "Antigravity",
54
+ doctor: (config, isActive) => agyDoctorChecks(config, isActive),
55
+
56
+ async init(config, ctx) {
57
+ initAgyAgent(config, ctx.getBridgePort, ctx.frontendName);
58
+ log("bot", "Backend: Antigravity (agy CLI, headless stream-json)");
59
+
60
+ const chat: ChatBackend = {
61
+ runChatTurn: (params) => handlerToEvents((p) => handleMessage(p), params),
62
+ interruptChatTurn: (chatId) => interruptChatTurn(chatId),
63
+ };
64
+
65
+ const background: BackgroundRunner = {
66
+ runOneShotAgent: (p) => runOneShotAgent(p),
67
+ evictOrphanSubprocesses: (label) => evictOrphanSubprocesses(label),
68
+ };
69
+
70
+ const models: ModelCatalog = {
71
+ resolveModelInfo: (q) => resolveModel(q),
72
+ getDefaultModelId: () => getDefaultModelId(),
73
+ getRawModelInfo: (id) => getModelInfo(id),
74
+ getSettingsPresentation: (m, options) =>
75
+ getSettingsPresentation(m, options),
76
+ getProviders: () => getProviders(),
77
+ getProviderModels: (p, pg, ps) => getProviderModels(p, pg, ps),
78
+ formatModelError: (q, r) => formatModelError(q, r),
79
+ listModels: (f) => listModels(f),
80
+ };
81
+
82
+ const sessions: SessionBackend = {
83
+ resetChat: (chatId) => resetChat(chatId),
84
+ warmSession: (chatId) => warmSession(chatId),
85
+ };
86
+
87
+ const tools: ToolRuntime = {
88
+ refreshTools: (chatId) => refreshTools(chatId),
89
+ };
90
+
91
+ // agy reports cache READS only (`cache_read_tokens`); there is no
92
+ // cache-write counter in its usage payload, hence `cacheMetrics: "read"`.
93
+ const usage: UsageTelemetry = {
94
+ getSessionSnapshot: async (chatId) => getState().lastUsage.get(chatId),
95
+ // No plan-usage endpoint exists: the CLI exposes `/usage` only as
96
+ // an interactive slash command that prints a human report, and
97
+ // there is no account API to read windows from. Reporting
98
+ // undefined lets /status fall through to another backend's plan.
99
+ getPlanUsage: async () => undefined,
100
+ };
101
+
102
+ const control: SystemControl = {
103
+ updateSystemPrompt: (prompt) => {
104
+ // Applies to the next NEW conversation only — a resumed one
105
+ // has the old prompt frozen into its history. Same semantics
106
+ // as codex.
107
+ getState().systemPromptOverride = prompt;
108
+ },
109
+ };
110
+
111
+ const backend = composeBackend({
112
+ id: "agy",
113
+ label: "Antigravity",
114
+ cacheMetrics: "read",
115
+ chat,
116
+ background,
117
+ models,
118
+ sessions,
119
+ tools,
120
+ usage,
121
+ control,
122
+ });
123
+
124
+ return {
125
+ backend,
126
+ cleanup: () => {
127
+ killAllChildren("shutdown");
128
+ unregisterAllMcp();
129
+ resetModelCache();
130
+ resetState();
131
+ log("bot", "Antigravity backend cleaned up");
132
+ },
133
+ };
134
+ },
135
+ };
136
+
137
+ registerBackend(agyFactory);
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Antigravity message handler barrel.
3
+ *
4
+ * One module today (`message.ts` — the turn orchestration); the
5
+ * directory exists so the split mirrors codex's when the handler grows
6
+ * a second responsibility.
7
+ */
8
+
9
+ export { handleMessage } from "./message.js";