talon-agent 3.16.1 → 3.18.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/package.json +1 -1
- package/src/app.ts +2 -0
- package/src/backend/codex/factory.ts +9 -0
- package/src/backend/codex/plan-usage.ts +160 -0
- package/src/backend/kilo/factory.ts +2 -19
- package/src/backend/opencode/factory.ts +2 -15
- package/src/backend/remote-server/model-catalog/presentation.ts +102 -25
- package/src/backend/remote-server/model-catalog/provider.ts +10 -6
- package/src/bootstrap.ts +14 -0
- package/src/cli/doctor.ts +11 -1
- package/src/core/background/plan-alerts.ts +115 -0
- package/src/core/doctor.ts +71 -8
- package/src/frontend/discord/callbacks/components.ts +260 -16
- package/src/frontend/discord/commands/admin.ts +188 -14
- package/src/frontend/discord/commands/definitions.ts +37 -2
- package/src/frontend/discord/commands/info.ts +38 -1
- package/src/frontend/discord/commands/router.ts +21 -1
- package/src/frontend/discord/commands/settings.ts +10 -18
- package/src/frontend/discord/helpers.ts +148 -1
- package/src/frontend/discord/index.ts +10 -1
- package/src/frontend/discord/middleware.ts +24 -0
- package/src/frontend/discord/model-picker.ts +234 -0
- package/src/frontend/shared/plan-usage-report.ts +72 -0
- package/src/frontend/telegram/commands/admin.ts +10 -0
- package/src/frontend/telegram/commands/definitions.ts +1 -0
- package/src/frontend/telegram/helpers/diagnostics.ts +38 -4
- package/src/util/config.ts +10 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/usage` data gathering — plan limits across every backend the config
|
|
3
|
+
* exposes, not just the one serving this chat.
|
|
4
|
+
*
|
|
5
|
+
* Most backends have no plan to report: a gateway (Kilo, OpenCode) bills
|
|
6
|
+
* through whichever provider it fronts, and an API-key install pays per
|
|
7
|
+
* token with no window to be near the end of. Those are listed with a
|
|
8
|
+
* reason rather than omitted, so the answer to "am I close to a limit?" is
|
|
9
|
+
* never silence.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { TalonConfig } from "../../util/config.js";
|
|
13
|
+
import type { PlanUsage } from "../../core/agent-runtime/capabilities.js";
|
|
14
|
+
import {
|
|
15
|
+
listAvailableBackends,
|
|
16
|
+
getPooledBackend,
|
|
17
|
+
} from "../../core/engine/backend-controller/index.js";
|
|
18
|
+
import { buildPlanDisplay, type PlanDisplay } from "./status-context.js";
|
|
19
|
+
|
|
20
|
+
export interface BackendUsageEntry {
|
|
21
|
+
id: string;
|
|
22
|
+
label: string;
|
|
23
|
+
/** Rendered windows, or null when this backend reported nothing. */
|
|
24
|
+
plan: PlanDisplay | null;
|
|
25
|
+
/** Why there is nothing to show. Absent when `plan` is set. */
|
|
26
|
+
note?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* One entry per exposed backend, in config order.
|
|
31
|
+
*
|
|
32
|
+
* Only backends already running are queried — booting one to read a
|
|
33
|
+
* number would spawn a subprocess or a server per idle provider, which is
|
|
34
|
+
* far more than a status command should cost.
|
|
35
|
+
*/
|
|
36
|
+
export async function collectPlanUsage(
|
|
37
|
+
config: TalonConfig,
|
|
38
|
+
): Promise<BackendUsageEntry[]> {
|
|
39
|
+
const entries: BackendUsageEntry[] = [];
|
|
40
|
+
|
|
41
|
+
for (const { id, label } of listAvailableBackends(config)) {
|
|
42
|
+
const backend = getPooledBackend(id);
|
|
43
|
+
if (!backend) {
|
|
44
|
+
entries.push({ id, label, plan: null, note: "not running" });
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (!backend.usage?.getPlanUsage) {
|
|
48
|
+
entries.push({
|
|
49
|
+
id,
|
|
50
|
+
label,
|
|
51
|
+
plan: null,
|
|
52
|
+
note: "no plan limits on this backend",
|
|
53
|
+
});
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let usage: PlanUsage | undefined;
|
|
58
|
+
try {
|
|
59
|
+
usage = await backend.usage.getPlanUsage();
|
|
60
|
+
} catch {
|
|
61
|
+
usage = undefined;
|
|
62
|
+
}
|
|
63
|
+
const plan = buildPlanDisplay(usage);
|
|
64
|
+
entries.push(
|
|
65
|
+
plan
|
|
66
|
+
? { id, label, plan }
|
|
67
|
+
: { id, label, plan: null, note: "no usage information available" },
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return entries;
|
|
72
|
+
}
|
|
@@ -21,7 +21,9 @@ import {
|
|
|
21
21
|
renderDoctorMessage,
|
|
22
22
|
renderMetricsKeyboard,
|
|
23
23
|
renderMetricsPanel,
|
|
24
|
+
renderUsageMessage,
|
|
24
25
|
} from "../helpers/index.js";
|
|
26
|
+
import { collectPlanUsage } from "../../shared/plan-usage-report.js";
|
|
25
27
|
import { collectDoctorReport } from "../../../core/doctor.js";
|
|
26
28
|
import { handleAdminCommand } from "../admin.js";
|
|
27
29
|
import { getTodayMetrics } from "../../../storage/metrics.js";
|
|
@@ -53,6 +55,14 @@ export function registerAdminCommands(
|
|
|
53
55
|
});
|
|
54
56
|
});
|
|
55
57
|
|
|
58
|
+
// /usage — plan limits across every exposed backend, not just this
|
|
59
|
+
// chat's. Not admin-gated: it says how close the shared account is to a
|
|
60
|
+
// wall, which is exactly what a user hitting one needs to know.
|
|
61
|
+
bot.command("usage", async (ctx) => {
|
|
62
|
+
const entries = await collectPlanUsage(config);
|
|
63
|
+
await ctx.reply(renderUsageMessage(entries), { parse_mode: "HTML" });
|
|
64
|
+
});
|
|
65
|
+
|
|
56
66
|
bot.command("doctor", async (ctx) => {
|
|
57
67
|
if (!isAuthorizedAdmin(ctx)) {
|
|
58
68
|
await ctx.reply("Not authorized.");
|
|
@@ -28,6 +28,7 @@ export const TELEGRAM_COMMANDS: ReadonlyArray<{
|
|
|
28
28
|
{ command: "pulse", description: "Conversation engagement settings" },
|
|
29
29
|
{ command: "reset", description: "Clear session and start fresh" },
|
|
30
30
|
{ command: "restart", description: "Restart the bot (admin)" },
|
|
31
|
+
{ command: "usage", description: "Plan limits across every backend" },
|
|
31
32
|
{ command: "metrics", description: "Aggregate performance metrics" },
|
|
32
33
|
{
|
|
33
34
|
command: "doctor",
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { escapeHtml } from "../formatting.js";
|
|
6
6
|
import type { DoctorReport } from "../../../core/doctor.js";
|
|
7
7
|
import type { MeshPingResult } from "../../../core/mesh/service.js";
|
|
8
|
+
import type { BackendUsageEntry } from "../../shared/plan-usage-report.js";
|
|
8
9
|
import type { SettingsButton } from "./menu.js";
|
|
9
10
|
import { formatDuration, formatBytes } from "./format.js";
|
|
10
11
|
|
|
@@ -208,14 +209,47 @@ const DOCTOR_ICONS: Record<string, string> = {
|
|
|
208
209
|
* when this renders, the bot is by definition running, so the CLI's
|
|
209
210
|
* "is the bot up" probe becomes an uptime line instead.
|
|
210
211
|
*/
|
|
212
|
+
/** Render the `/usage` report — one block per exposed backend. */
|
|
213
|
+
export function renderUsageMessage(entries: BackendUsageEntry[]): string {
|
|
214
|
+
const lines = ["<b>📊 Plan usage</b>"];
|
|
215
|
+
|
|
216
|
+
for (const entry of entries) {
|
|
217
|
+
const name = escapeHtml(entry.label || entry.id);
|
|
218
|
+
if (!entry.plan) {
|
|
219
|
+
lines.push("", `<b>${name}</b> — <i>${escapeHtml(entry.note ?? "")}</i>`);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const age = entry.plan.ageLabel ? ` <i>(${entry.plan.ageLabel})</i>` : "";
|
|
223
|
+
const plan = entry.plan.plan ? ` · ${escapeHtml(entry.plan.plan)}` : "";
|
|
224
|
+
lines.push("", `<b>${name}</b>${plan}${age}`);
|
|
225
|
+
for (const w of entry.plan.windows) {
|
|
226
|
+
const reset = w.resetLabel ? ` reset ${w.resetLabel}` : "";
|
|
227
|
+
lines.push(
|
|
228
|
+
` <code>${escapeHtml(w.label.padEnd(6))}${w.bar} ${String(w.percent).padStart(3)}%</code>${reset}`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return lines.join("\n");
|
|
234
|
+
}
|
|
235
|
+
|
|
211
236
|
export function renderDoctorMessage(report: DoctorReport): string {
|
|
212
237
|
const lines = ["<b>🩺 Talon Doctor</b>", "", "<b>Environment</b>"];
|
|
213
238
|
|
|
214
|
-
|
|
239
|
+
const render = (check: DoctorReport["checks"][number]): string => {
|
|
215
240
|
const detail = check.detail ? ` (${escapeHtml(check.detail)})` : "";
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
241
|
+
return `${DOCTOR_ICONS[check.status]} ${escapeHtml(check.label)}${detail}`;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
for (const check of report.checks.filter((c) => !c.inactive)) {
|
|
245
|
+
lines.push(render(check));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Configured-but-idle backends get their own block: they describe what a
|
|
249
|
+
// switch would run into, not the state of the running deployment.
|
|
250
|
+
const idle = report.checks.filter((c) => c.inactive);
|
|
251
|
+
if (idle.length > 0) {
|
|
252
|
+
lines.push("", "<b>Other backends</b>", ...idle.map(render));
|
|
219
253
|
}
|
|
220
254
|
|
|
221
255
|
lines.push("", "<b>Native modules</b>");
|
package/src/util/config.ts
CHANGED
|
@@ -340,6 +340,16 @@ const configSchema = z.object({
|
|
|
340
340
|
allowedUsers: z.array(z.number().int()).optional(), // Whitelist of user IDs allowed to DM the bot
|
|
341
341
|
pulse: z.boolean().default(true),
|
|
342
342
|
pulseIntervalMs: z.number().int().min(60000).default(300000),
|
|
343
|
+
/**
|
|
344
|
+
* Warn the admin chat when a subscription rate-limit window crosses
|
|
345
|
+
* `planAlertThreshold`. Off by default. Needs a backend that reports plan
|
|
346
|
+
* limits (Claude on a subscription); one message per window per reset
|
|
347
|
+
* cycle.
|
|
348
|
+
*/
|
|
349
|
+
planAlerts: z.boolean().default(false),
|
|
350
|
+
planAlertThreshold: z.number().int().min(1).max(100).default(80),
|
|
351
|
+
/** Chat that receives plan warnings. Defaults to `adminUserId`. */
|
|
352
|
+
planAlertChatId: z.string().optional(),
|
|
343
353
|
/** Background memory-consolidation (dream) runs. Mirrors `pulse`/`heartbeat`. */
|
|
344
354
|
dream: z.boolean().default(true),
|
|
345
355
|
/**
|