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.
- package/README.md +10 -6
- package/package.json +1 -1
- package/src/app.ts +78 -0
- package/src/backend/agy/auth.ts +128 -0
- package/src/backend/agy/constants.ts +88 -0
- package/src/backend/agy/doctor.ts +161 -0
- package/src/backend/agy/effort.ts +76 -0
- package/src/backend/agy/events.ts +402 -0
- package/src/backend/agy/factory.ts +137 -0
- package/src/backend/agy/handler/index.ts +9 -0
- package/src/backend/agy/handler/message.ts +444 -0
- package/src/backend/agy/init.ts +64 -0
- package/src/backend/agy/mcp/config.ts +336 -0
- package/src/backend/agy/mcp/register.ts +134 -0
- package/src/backend/agy/models.ts +335 -0
- package/src/backend/agy/one-shot.ts +301 -0
- package/src/backend/agy/process/child.ts +378 -0
- package/src/backend/agy/process/orphans.ts +106 -0
- package/src/backend/agy/sessions.ts +90 -0
- package/src/backend/agy/state.ts +77 -0
- package/src/backend/builtins.ts +1 -0
- package/src/backend/codex/mcp-config.ts +1 -1
- package/src/backend/openai-agents/mcp-pool.ts +1 -1
- package/src/backend/runtime/index.ts +1 -1
- package/src/cli/commands/backup.ts +396 -0
- package/src/cli/config-view.ts +5 -0
- package/src/cli/config.ts +4 -2
- package/src/cli/events.ts +14 -0
- package/src/cli/index.ts +64 -45
- package/src/cli/setup.ts +49 -0
- package/src/core/agent-runtime/model-ref.ts +1 -0
- package/src/core/backup/archive/digest.ts +77 -0
- package/src/core/backup/archive/tar.ts +567 -0
- package/src/core/backup/archive/zstd.ts +31 -0
- package/src/core/backup/index.ts +54 -0
- package/src/core/backup/plan.ts +273 -0
- package/src/core/backup/restore.ts +410 -0
- package/src/core/backup/scheduler.ts +357 -0
- package/src/core/backup/snapshot.ts +408 -0
- package/src/core/backup/status.ts +194 -0
- package/src/core/backup/store.ts +312 -0
- package/src/core/backup/targets.ts +281 -0
- package/src/core/backup/types.ts +96 -0
- package/src/core/backup/upload.ts +172 -0
- package/src/core/bus/events.ts +45 -1
- package/src/core/config/index.ts +53 -0
- package/src/core/engine/gateway-actions/backup/index.ts +129 -0
- package/src/core/engine/gateway-actions/index.ts +4 -0
- package/src/core/mcp-hub/talon-server.ts +1 -1
- package/src/core/plugin/actions.ts +34 -0
- package/src/core/plugin/index.ts +5 -1
- package/src/core/tools/{ops/bridge.ts → bridge.ts} +7 -2
- package/src/core/tools/index.ts +2 -0
- package/src/core/tools/ops/backup.ts +67 -0
- package/src/core/tools/types.ts +2 -1
- package/src/core/update/self-update.ts +3 -0
- package/src/frontend/discord/callbacks/components/index.ts +3 -0
- package/src/frontend/discord/commands/backup.ts +203 -0
- package/src/frontend/discord/commands/definitions.ts +35 -0
- package/src/frontend/discord/commands/router.ts +3 -0
- package/src/frontend/telegram/callbacks/backup.ts +55 -0
- package/src/frontend/telegram/callbacks/index.ts +8 -0
- package/src/frontend/telegram/commands/backup.ts +209 -0
- package/src/frontend/telegram/commands/definitions.ts +4 -0
- package/src/frontend/telegram/commands/index.ts +2 -0
- package/src/storage/backup/index.ts +82 -0
- package/src/storage/backup/repo.ts +164 -0
- package/src/storage/db.ts +20 -0
- package/src/storage/sql/backups.sql +46 -0
- package/src/storage/sql/db.sql +8 -0
- package/src/storage/sql/schema.sql +30 -0
- package/src/storage/sql/statements.generated.ts +60 -1
- package/src/util/log.ts +1 -0
- /package/src/core/tools/{ops/mcp-env.ts → mcp-env.ts} +0 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Antigravity main message handler.
|
|
3
|
+
*
|
|
4
|
+
* Drives one turn on the chat's long-lived `agy` child (see
|
|
5
|
+
* `../process.ts`) and folds the result back through the shared
|
|
6
|
+
* post-stream phases in `backend/runtime/turn/turn-phases.ts`.
|
|
7
|
+
*
|
|
8
|
+
* The agy-specific bits are:
|
|
9
|
+
*
|
|
10
|
+
* - MCP entries are written to agy's shared config file BEFORE the
|
|
11
|
+
* child spawns; the CLI reads them once at startup.
|
|
12
|
+
* - No system-prompt flag: the assembled prompt rides in as a fenced
|
|
13
|
+
* block on the first turn only (codex does the same).
|
|
14
|
+
* - A turn in a stream-json process cannot be cancelled in-flight,
|
|
15
|
+
* so the terminator kills the child; the next turn respawns it
|
|
16
|
+
* with `--conversation <id>` and the conversation continues.
|
|
17
|
+
* - `result.usage` is cumulative over the session, so a turn's real
|
|
18
|
+
* cost is the delta against the previous result.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type {
|
|
22
|
+
QueryParams,
|
|
23
|
+
QueryResult,
|
|
24
|
+
} from "../../runtime/turn/handler-types.js";
|
|
25
|
+
import { getSession, incrementTurns } from "../../../storage/sessions.js";
|
|
26
|
+
import { getChatSettings } from "../../../storage/chat-settings.js";
|
|
27
|
+
import type { DeliveryDecision } from "../../runtime/turn/delivery.js";
|
|
28
|
+
import { log, logError, logWarn } from "../../../util/log.js";
|
|
29
|
+
import { traceMessage } from "../../../util/trace.js";
|
|
30
|
+
import { incrementCounter } from "../../../storage/metrics.js";
|
|
31
|
+
import { dirs } from "../../../util/paths.js";
|
|
32
|
+
import { supportsReasoningLevel } from "../../../core/models/reasoning-levels.js";
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
createStreamState,
|
|
36
|
+
finalizeResponseText,
|
|
37
|
+
formatUserPrompt,
|
|
38
|
+
prepareSystemPrompt,
|
|
39
|
+
routeDelivery,
|
|
40
|
+
buildDeliveryFailureReminder,
|
|
41
|
+
TextBlockDeliveryError,
|
|
42
|
+
applyRetryDecision,
|
|
43
|
+
registerTurnInterrupt,
|
|
44
|
+
recordTokens,
|
|
45
|
+
accountTurn,
|
|
46
|
+
accountFailedTurn,
|
|
47
|
+
nameSessionFromFirstMessage,
|
|
48
|
+
finishCallbackTurn,
|
|
49
|
+
type StreamState,
|
|
50
|
+
} from "../../runtime/index.js";
|
|
51
|
+
import {
|
|
52
|
+
frontendsForChat,
|
|
53
|
+
nonTerminalFrontends,
|
|
54
|
+
} from "../../runtime/frontends.js";
|
|
55
|
+
|
|
56
|
+
import { agySystemPromptSuffix } from "../constants.js";
|
|
57
|
+
import { getState, agyBinary, type AgySessionUsage } from "../state.js";
|
|
58
|
+
import { registerMcpForChat } from "../mcp/register.js";
|
|
59
|
+
import {
|
|
60
|
+
applyAgyStep,
|
|
61
|
+
createAgyEventContext,
|
|
62
|
+
agyUsageToTokens,
|
|
63
|
+
agyUsageDelta,
|
|
64
|
+
type AgyEventContext,
|
|
65
|
+
type AgyResult,
|
|
66
|
+
type AgyStepUpdate,
|
|
67
|
+
} from "../events.js";
|
|
68
|
+
import {
|
|
69
|
+
applyEffortToModelId,
|
|
70
|
+
toAgyEffort,
|
|
71
|
+
type AgyEffort,
|
|
72
|
+
} from "../effort.js";
|
|
73
|
+
import { getCachedModels, getModelInfo, getDefaultModelId } from "../models.js";
|
|
74
|
+
import {
|
|
75
|
+
ensureChild,
|
|
76
|
+
killChild,
|
|
77
|
+
AgyTurnAborted,
|
|
78
|
+
type AgyChild,
|
|
79
|
+
type AgySpawnSpec,
|
|
80
|
+
} from "../process/child.js";
|
|
81
|
+
import { agyAuthError, isAgyAuthFailure } from "../auth.js";
|
|
82
|
+
|
|
83
|
+
const errMsg = (e: unknown): string =>
|
|
84
|
+
e instanceof Error ? e.message : String(e);
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The expected close on `end_turn` / a user interrupt. Killing the
|
|
88
|
+
* child IS how a turn is stopped here, so the resulting rejection is
|
|
89
|
+
* a clean completion, not an error to retry.
|
|
90
|
+
*/
|
|
91
|
+
function isTerminatorAbort(state: StreamState, err: unknown): boolean {
|
|
92
|
+
return (
|
|
93
|
+
state.turnTerminated &&
|
|
94
|
+
(err instanceof AgyTurnAborted || /abort/i.test(errMsg(err)))
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── Model + effort ──────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
interface ResolvedRun {
|
|
101
|
+
model: string;
|
|
102
|
+
effort: AgyEffort | undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Resolve the model and effort for this turn.
|
|
107
|
+
*
|
|
108
|
+
* Effort precedence (documented once, here): the requested level first
|
|
109
|
+
* re-points the model id at the sibling slug that bakes that level in
|
|
110
|
+
* — which is how agy actually expresses effort for most of its
|
|
111
|
+
* catalog — and is then ALSO passed as `--effort`, so an id with no
|
|
112
|
+
* suffix still honours it. A level agy cannot express falls through
|
|
113
|
+
* to the model's own default.
|
|
114
|
+
*/
|
|
115
|
+
async function resolveRun(
|
|
116
|
+
chatId: string,
|
|
117
|
+
requestedModel: string | undefined,
|
|
118
|
+
requestedEffort: ReturnType<typeof getChatSettings>["effort"],
|
|
119
|
+
): Promise<ResolvedRun> {
|
|
120
|
+
const baseModel = requestedModel ?? getDefaultModelId();
|
|
121
|
+
const info = await getModelInfo(baseModel).catch(() => undefined);
|
|
122
|
+
const available = info?.supportedReasoningLevels ?? [];
|
|
123
|
+
const effort =
|
|
124
|
+
requestedEffort && supportsReasoningLevel(requestedEffort, available)
|
|
125
|
+
? toAgyEffort(requestedEffort)
|
|
126
|
+
: undefined;
|
|
127
|
+
const catalogIds = getCachedModels().map((m) => m.id);
|
|
128
|
+
const model = applyEffortToModelId(baseModel, effort, catalogIds);
|
|
129
|
+
log(
|
|
130
|
+
"agent",
|
|
131
|
+
`[${chatId}] agy model resolved: ${model}${effort ? ` (effort=${effort})` : ""}`,
|
|
132
|
+
);
|
|
133
|
+
return { model, effort };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Turn plumbing ───────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Feed one step to the shared state, then enforce the terminator:
|
|
140
|
+
* once a delivery tool has shipped the reply there is nothing left to
|
|
141
|
+
* generate, and the only way to stop an agy turn is to kill the child.
|
|
142
|
+
*/
|
|
143
|
+
function makeStepHandler(
|
|
144
|
+
ctx: AgyEventContext,
|
|
145
|
+
chatId: string,
|
|
146
|
+
child: AgyChild,
|
|
147
|
+
): (step: AgyStepUpdate) => void {
|
|
148
|
+
return (step) => {
|
|
149
|
+
applyAgyStep(step, ctx);
|
|
150
|
+
if (!ctx.state.turnTerminated || !child.alive) return;
|
|
151
|
+
log("agent", `[${chatId}] terminator fired — killing agy child`);
|
|
152
|
+
killChild(chatId, "terminator");
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Record the turn's usage, delta'd off the session's cumulative counters. */
|
|
157
|
+
function settleUsage(
|
|
158
|
+
chatId: string,
|
|
159
|
+
state: StreamState,
|
|
160
|
+
result: AgyResult,
|
|
161
|
+
model: string,
|
|
162
|
+
): void {
|
|
163
|
+
const cumulative = agyUsageToTokens(result.usage);
|
|
164
|
+
const store = getState().lastUsage;
|
|
165
|
+
const previous = store.get(chatId);
|
|
166
|
+
const turn = agyUsageDelta(previous, cumulative);
|
|
167
|
+
recordTokens(state, turn);
|
|
168
|
+
const snapshot: AgySessionUsage = { ...cumulative, contextModelId: model };
|
|
169
|
+
// Keyed under BOTH ids on purpose: `/status` looks the snapshot up by
|
|
170
|
+
// the stored session id (agy's conversation id), while `resetChat`
|
|
171
|
+
// and the tests reach for it by chat id.
|
|
172
|
+
store.set(chatId, snapshot);
|
|
173
|
+
if (result.conversation_id) store.set(result.conversation_id, snapshot);
|
|
174
|
+
// `input_tokens` on the terminal result is the conversation's
|
|
175
|
+
// accumulated prompt, which is the best context-fill estimate agy
|
|
176
|
+
// gives us — there is no rollout file to read a real one from.
|
|
177
|
+
state.contextTokens = turn.inputTokens;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Surface a non-SUCCESS result as a synthetic error on the state. */
|
|
181
|
+
function applyResultStatus(state: StreamState, result: AgyResult): boolean {
|
|
182
|
+
if (!result.status || result.status === "SUCCESS") return false;
|
|
183
|
+
state.syntheticError =
|
|
184
|
+
result.error ?? `Antigravity turn ended with status ${result.status}`;
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── Failure ladder ──────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
async function recoverAgyFailure(inputs: {
|
|
191
|
+
err: unknown;
|
|
192
|
+
params: QueryParams;
|
|
193
|
+
retried: boolean;
|
|
194
|
+
model: string;
|
|
195
|
+
state: StreamState;
|
|
196
|
+
toolCalls: number;
|
|
197
|
+
t0: number;
|
|
198
|
+
stderr: string;
|
|
199
|
+
}): Promise<QueryResult> {
|
|
200
|
+
const { err, params, retried, model, state } = inputs;
|
|
201
|
+
const { chatId } = params;
|
|
202
|
+
|
|
203
|
+
// An unauthenticated CLI is the one failure a user can actually fix,
|
|
204
|
+
// and it only ever shows up on stderr. Swap it for a directed error
|
|
205
|
+
// before the generic ladder classifies it as an opaque exit.
|
|
206
|
+
const surfaced = isAgyAuthFailure(`${inputs.stderr} ${errMsg(err)}`)
|
|
207
|
+
? agyAuthError(err)
|
|
208
|
+
: err;
|
|
209
|
+
|
|
210
|
+
const decision = await applyRetryDecision({
|
|
211
|
+
err: surfaced,
|
|
212
|
+
chatId,
|
|
213
|
+
activeModel: model,
|
|
214
|
+
retried,
|
|
215
|
+
params,
|
|
216
|
+
recurseWithRetried: (p) => handleMessage(p, true),
|
|
217
|
+
backendLabel: "Antigravity",
|
|
218
|
+
resetNoun: "session",
|
|
219
|
+
});
|
|
220
|
+
if (decision.retry) return decision.retry;
|
|
221
|
+
|
|
222
|
+
accountFailedTurn({
|
|
223
|
+
backend: "agy",
|
|
224
|
+
chatId,
|
|
225
|
+
state,
|
|
226
|
+
durationMs: Date.now() - inputs.t0,
|
|
227
|
+
model,
|
|
228
|
+
toolCalls: inputs.toolCalls,
|
|
229
|
+
});
|
|
230
|
+
logError("agent", `[${chatId}] agy error: ${decision.classified.message}`);
|
|
231
|
+
throw decision.classified;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** The prompt text handed to the child for this turn. */
|
|
235
|
+
function buildTurnInput(inputs: {
|
|
236
|
+
params: QueryParams;
|
|
237
|
+
config: NonNullable<ReturnType<typeof getState>["config"]>;
|
|
238
|
+
session: ReturnType<typeof getSession>;
|
|
239
|
+
previousTurns: number;
|
|
240
|
+
}): string {
|
|
241
|
+
const { params, config, session, previousTurns } = inputs;
|
|
242
|
+
const override = getState().systemPromptOverride;
|
|
243
|
+
const { text: systemPrompt } = prepareSystemPrompt({
|
|
244
|
+
config: override ? { ...config, systemPrompt: override } : config,
|
|
245
|
+
previousTurns,
|
|
246
|
+
backendSuffix: agySystemPromptSuffix(
|
|
247
|
+
frontendsForChat(
|
|
248
|
+
params.chatId,
|
|
249
|
+
nonTerminalFrontends(config.frontend),
|
|
250
|
+
)[0] ?? "telegram",
|
|
251
|
+
),
|
|
252
|
+
chatId: params.chatId,
|
|
253
|
+
sessionEpoch: session.createdAt,
|
|
254
|
+
});
|
|
255
|
+
const prompt = formatUserPrompt({
|
|
256
|
+
text: params.text,
|
|
257
|
+
senderName: params.senderName ?? "user",
|
|
258
|
+
senderHandle: params.senderHandle,
|
|
259
|
+
isGroup: params.isGroup,
|
|
260
|
+
messageId: params.messageId,
|
|
261
|
+
retrievedMemory: params.retrievedMemory,
|
|
262
|
+
});
|
|
263
|
+
// No `--system` flag exists: prepend the assembled prompt on the
|
|
264
|
+
// first turn only; a resumed conversation already carries it.
|
|
265
|
+
return previousTurns === 0 ? `${systemPrompt}\n\n---\n\n${prompt}` : prompt;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Spawn shape for this chat's child, including the resume id. */
|
|
269
|
+
function spawnSpecFor(inputs: {
|
|
270
|
+
config: NonNullable<ReturnType<typeof getState>["config"]>;
|
|
271
|
+
chatId: string;
|
|
272
|
+
model: string;
|
|
273
|
+
effort: AgyEffort | undefined;
|
|
274
|
+
session: ReturnType<typeof getSession>;
|
|
275
|
+
}): AgySpawnSpec {
|
|
276
|
+
const workspace = inputs.config.workspace || dirs.workspace;
|
|
277
|
+
return {
|
|
278
|
+
binary: agyBinary(inputs.config.agyBinary),
|
|
279
|
+
cwd: workspace,
|
|
280
|
+
model: inputs.model,
|
|
281
|
+
effort: inputs.effort,
|
|
282
|
+
addDirs: [workspace],
|
|
283
|
+
...(inputs.session.sessionId
|
|
284
|
+
? { conversationId: inputs.session.sessionId }
|
|
285
|
+
: {}),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Route the turn's text through the shared delivery decision tree,
|
|
291
|
+
* converting a failed text-block delivery into the one-shot
|
|
292
|
+
* re-prompt every backend does.
|
|
293
|
+
*/
|
|
294
|
+
async function deliverTurn(
|
|
295
|
+
params: QueryParams,
|
|
296
|
+
state: StreamState,
|
|
297
|
+
responseText: string,
|
|
298
|
+
retried: boolean,
|
|
299
|
+
): Promise<{ decision: DeliveryDecision } | { retry: QueryResult }> {
|
|
300
|
+
try {
|
|
301
|
+
return {
|
|
302
|
+
decision: await routeDelivery({
|
|
303
|
+
backendLabel: "Antigravity",
|
|
304
|
+
chatId: params.chatId,
|
|
305
|
+
state,
|
|
306
|
+
responseText,
|
|
307
|
+
onTextBlock: params.onTextBlock,
|
|
308
|
+
metricNamespace: "agy",
|
|
309
|
+
propagateDeliveryFailure: true,
|
|
310
|
+
}),
|
|
311
|
+
};
|
|
312
|
+
} catch (err) {
|
|
313
|
+
if (!(err instanceof TextBlockDeliveryError) || retried) throw err;
|
|
314
|
+
incrementCounter("delivery.text_block_retry");
|
|
315
|
+
logWarn(
|
|
316
|
+
"agent",
|
|
317
|
+
`[${params.chatId}] ${err.message}; re-prompting Antigravity with delivery failure`,
|
|
318
|
+
);
|
|
319
|
+
return {
|
|
320
|
+
retry: await handleMessage(
|
|
321
|
+
{ ...params, text: buildDeliveryFailureReminder(err) },
|
|
322
|
+
true,
|
|
323
|
+
),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ── Main handler ────────────────────────────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
export async function handleMessage(
|
|
331
|
+
params: QueryParams,
|
|
332
|
+
_retried = false,
|
|
333
|
+
): Promise<QueryResult> {
|
|
334
|
+
const config = getState().config;
|
|
335
|
+
if (!config) throw new Error("Antigravity agent not initialized");
|
|
336
|
+
|
|
337
|
+
const { chatId, text, senderName, isGroup } = params;
|
|
338
|
+
const t0 = Date.now();
|
|
339
|
+
const session = getSession(chatId);
|
|
340
|
+
const previousTurns = session.turns;
|
|
341
|
+
const chatSettings = getChatSettings(chatId);
|
|
342
|
+
|
|
343
|
+
const { model, effort } = await resolveRun(
|
|
344
|
+
chatId,
|
|
345
|
+
params.model ?? chatSettings.model ?? config.model,
|
|
346
|
+
chatSettings.effort,
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
const inputText = buildTurnInput({ params, config, session, previousTurns });
|
|
350
|
+
|
|
351
|
+
log("agent", `[${chatId}] <- (${text.length} chars)`);
|
|
352
|
+
traceMessage(chatId, "in", text, { senderName, isGroup });
|
|
353
|
+
|
|
354
|
+
// MCP entries must exist on disk before the child spawns — agy reads
|
|
355
|
+
// its server list once, at process start.
|
|
356
|
+
registerMcpForChat(chatId);
|
|
357
|
+
const child = ensureChild(
|
|
358
|
+
chatId,
|
|
359
|
+
spawnSpecFor({ config, chatId, model, effort, session }),
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
const streamState = createStreamState(chatId);
|
|
363
|
+
const ctx = createAgyEventContext(streamState, chatId, {
|
|
364
|
+
onStreamDelta: params.onStreamDelta,
|
|
365
|
+
onToolUse: params.onToolUse,
|
|
366
|
+
onToolStart: params.onToolStart,
|
|
367
|
+
onToolEnd: params.onToolEnd,
|
|
368
|
+
});
|
|
369
|
+
// A user interrupt is a synthetic terminator: flagging first routes
|
|
370
|
+
// the kill through the same clean close a model-fired end_turn takes.
|
|
371
|
+
const unregisterInterrupt = registerTurnInterrupt(chatId, () => {
|
|
372
|
+
streamState.turnTerminated = true;
|
|
373
|
+
killChild(chatId, "interrupt");
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const setupMs = Date.now() - t0;
|
|
377
|
+
let turnMs = 0;
|
|
378
|
+
let result: AgyResult | undefined;
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
const turnStart = Date.now();
|
|
382
|
+
result = await child.runTurn(inputText, {
|
|
383
|
+
onStep: makeStepHandler(ctx, chatId, child),
|
|
384
|
+
});
|
|
385
|
+
turnMs = Date.now() - turnStart;
|
|
386
|
+
} catch (err) {
|
|
387
|
+
if (!isTerminatorAbort(streamState, err)) {
|
|
388
|
+
return await recoverAgyFailure({
|
|
389
|
+
err,
|
|
390
|
+
params,
|
|
391
|
+
retried: _retried,
|
|
392
|
+
model,
|
|
393
|
+
state: streamState,
|
|
394
|
+
toolCalls: ctx.toolMetrics.count,
|
|
395
|
+
t0,
|
|
396
|
+
stderr: child.stderrSnapshot,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
} finally {
|
|
400
|
+
unregisterInterrupt();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ── Post-turn accounting ──────────────────────────────────────────────────
|
|
404
|
+
|
|
405
|
+
const failed = result ? applyResultStatus(streamState, result) : false;
|
|
406
|
+
if (result) settleUsage(chatId, streamState, result, model);
|
|
407
|
+
|
|
408
|
+
const responseText = finalizeResponseText(streamState);
|
|
409
|
+
const durationMs = Date.now() - t0;
|
|
410
|
+
accountTurn({
|
|
411
|
+
chatId,
|
|
412
|
+
backend: "agy",
|
|
413
|
+
state: streamState,
|
|
414
|
+
durationMs,
|
|
415
|
+
model,
|
|
416
|
+
sessionId: child.conversationId,
|
|
417
|
+
failed,
|
|
418
|
+
toolCalls: ctx.toolMetrics.count,
|
|
419
|
+
context: {
|
|
420
|
+
contextTokens: streamState.contextTokens || undefined,
|
|
421
|
+
numApiCalls: streamState.numApiCalls || undefined,
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
nameSessionFromFirstMessage({ chatId, text, previousTurns });
|
|
425
|
+
|
|
426
|
+
const delivery = await deliverTurn(
|
|
427
|
+
params,
|
|
428
|
+
streamState,
|
|
429
|
+
responseText,
|
|
430
|
+
_retried,
|
|
431
|
+
);
|
|
432
|
+
if ("retry" in delivery) return delivery.retry;
|
|
433
|
+
|
|
434
|
+
incrementTurns(chatId);
|
|
435
|
+
return finishCallbackTurn({
|
|
436
|
+
chatId,
|
|
437
|
+
state: streamState,
|
|
438
|
+
responseText,
|
|
439
|
+
durationMs,
|
|
440
|
+
setupMs,
|
|
441
|
+
turnMs,
|
|
442
|
+
delivery: delivery.decision,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Antigravity backend initialisation.
|
|
3
|
+
*
|
|
4
|
+
* Cheap and synchronous where it can be: capture config, the
|
|
5
|
+
* gateway-port resolver and the owning frontend, then tidy the shared
|
|
6
|
+
* MCP config file. Children spawn lazily on the first turn, and the
|
|
7
|
+
* model catalog is probed in the background so `/model` is warm
|
|
8
|
+
* without adding a spawn to startup.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { TalonConfig } from "../../core/config/index.js";
|
|
12
|
+
import type { FrontendName } from "../../core/agent-runtime/backend-registry.js";
|
|
13
|
+
import { log, logWarn } from "../../util/log.js";
|
|
14
|
+
import { getState } from "./state.js";
|
|
15
|
+
import { detectAgyAuth } from "./auth.js";
|
|
16
|
+
import { pruneForeignTalonEntries } from "./mcp/register.js";
|
|
17
|
+
import { refreshModels, resetModelCache } from "./models.js";
|
|
18
|
+
|
|
19
|
+
/** Initialise the backend. Safe to call again on a config reload. */
|
|
20
|
+
export function initAgyAgent(
|
|
21
|
+
cfg: TalonConfig,
|
|
22
|
+
getGatewayPort?: () => number,
|
|
23
|
+
frontend?: FrontendName,
|
|
24
|
+
): void {
|
|
25
|
+
const state = getState();
|
|
26
|
+
state.config = cfg;
|
|
27
|
+
if (getGatewayPort) state.gatewayPortFn = getGatewayPort;
|
|
28
|
+
if (frontend) state.frontendName = frontend;
|
|
29
|
+
state.systemPromptOverride = null;
|
|
30
|
+
|
|
31
|
+
logAuth();
|
|
32
|
+
// agy's MCP config file is shared and persistent; anything under the
|
|
33
|
+
// Talon prefix that this boot did not write is a dead daemon's.
|
|
34
|
+
pruneForeignTalonEntries();
|
|
35
|
+
// Fire-and-forget: a cold `/model` would otherwise pay the spawn.
|
|
36
|
+
resetModelCache();
|
|
37
|
+
void refreshModels(true);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function logAuth(): void {
|
|
41
|
+
const auth = detectAgyAuth();
|
|
42
|
+
if (!auth.present) {
|
|
43
|
+
logWarn(
|
|
44
|
+
"agent",
|
|
45
|
+
"Antigravity: no cached credentials at " +
|
|
46
|
+
`${auth.path} — run \`agy\` once interactively on this host to sign in. ` +
|
|
47
|
+
"There is no API key for this backend; headless runs reuse the OAuth cache.",
|
|
48
|
+
);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (auth.expired && !auth.refreshable) {
|
|
52
|
+
logWarn(
|
|
53
|
+
"agent",
|
|
54
|
+
`Antigravity auth: token expired ${auth.expiry?.toISOString() ?? ""} ` +
|
|
55
|
+
"with no refresh token — run `agy` again to re-authenticate.",
|
|
56
|
+
);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
log(
|
|
60
|
+
"agent",
|
|
61
|
+
`Antigravity auth: ${auth.method ?? "oauth"} credentials cached` +
|
|
62
|
+
(auth.expiry ? ` (expiry ${auth.expiry.toISOString()})` : ""),
|
|
63
|
+
);
|
|
64
|
+
}
|