@songsid/agend 2.1.2-beta.23 → 2.1.2-beta.25
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/dist/backend/antigravity.d.ts +2 -0
- package/dist/backend/antigravity.js +5 -0
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +2 -0
- package/dist/backend/claude-code.js +4 -0
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex.d.ts +2 -0
- package/dist/backend/codex.js +5 -0
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/grok.d.ts +2 -0
- package/dist/backend/grok.js +6 -0
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.d.ts +2 -0
- package/dist/backend/kiro.js +8 -1
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +24 -0
- package/dist/backend/types.js +12 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/adapters/discord.js +17 -8
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.js +5 -1
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +4 -2
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/config-validator.js +12 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.js +1 -0
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-context.d.ts +9 -0
- package/dist/fleet-manager.d.ts +50 -1
- package/dist/fleet-manager.js +337 -16
- package/dist/fleet-manager.js.map +1 -1
- package/dist/locale.js +1 -0
- package/dist/locale.js.map +1 -1
- package/dist/outbound-handlers.js +31 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +3 -0
- package/dist/outbound-schemas.js +3 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/topic-commands.js +30 -3
- package/dist/topic-commands.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/usage/format-rich.js +8 -0
- package/dist/usage/format-rich.js.map +1 -1
- package/dist/usage/providers.js +25 -1
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/usage-api.js +61 -5
- package/dist/usage/usage-api.js.map +1 -1
- package/package.json +1 -1
package/dist/fleet-manager.d.ts
CHANGED
|
@@ -112,6 +112,8 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
112
112
|
classicChannels: ClassicChannelManager | null;
|
|
113
113
|
private pendingClassicStarts;
|
|
114
114
|
/** In-flight /model selections, keyed by nonce (see handleModelSelection). */
|
|
115
|
+
/** In-flight /effort selections, same coordinator shape as pendingModelSelects. */
|
|
116
|
+
private pendingEffortSelects;
|
|
115
117
|
private pendingModelSelects;
|
|
116
118
|
private failoverActive;
|
|
117
119
|
readonly ipcStoppingInstances: Set<string>;
|
|
@@ -499,7 +501,7 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
499
501
|
* progress indicator (#409) — the channel showed nothing at all during long work,
|
|
500
502
|
* and once the agent had replied once there was no sign it was still going.
|
|
501
503
|
*/
|
|
502
|
-
static progressText(elapsedMs: number, activity?: string | null): string;
|
|
504
|
+
static progressText(elapsedMs: number, activity?: string | null, minElapsedMs?: number): string;
|
|
503
505
|
/**
|
|
504
506
|
* Make a tool summary safe to paste into a channel message.
|
|
505
507
|
*
|
|
@@ -524,6 +526,8 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
524
526
|
* and the Bot API treats that as "clear the keyboard" — so editing with it would
|
|
525
527
|
* delete the very cancel button this is trying to keep alive.
|
|
526
528
|
*/
|
|
529
|
+
/** Configured threshold before elapsed time appears, in ms. */
|
|
530
|
+
progressMinElapsedMs(): number;
|
|
527
531
|
private startProgressTicker;
|
|
528
532
|
/**
|
|
529
533
|
* After a reply: give the instance REPLY_RETIRE_GRACE_MS to resume working; if
|
|
@@ -636,6 +640,15 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
636
640
|
/** Best-effort model list for `/model`: cached CLI env first, else live probe. Never throws. */
|
|
637
641
|
private getModelOptions;
|
|
638
642
|
/** `/model` slash handler (admin only). No arg → DC menu; `/model <name>` → apply directly. */
|
|
643
|
+
/** Label an effort choice, marking the one currently configured. */
|
|
644
|
+
private effortChoiceLabel;
|
|
645
|
+
private effortMenuHeader;
|
|
646
|
+
/** `/effort` — DC Select Menu, or apply directly when a level is given. */
|
|
647
|
+
private handleEffortSlash;
|
|
648
|
+
/** TG inline-keyboard effort menu. Returns null on success, else a fallback string. */
|
|
649
|
+
promptEffortMenu(instanceName: string, userId: string, channelId: string, adapter: ChannelAdapter, chatId: string, threadId?: string): Promise<string | null>;
|
|
650
|
+
/** Consume an `/effort` selection callback. Mirrors handleModelSelection. */
|
|
651
|
+
private handleEffortSelection;
|
|
639
652
|
private handleModelSlash;
|
|
640
653
|
/**
|
|
641
654
|
* Show a TG inline-keyboard model-selection menu. Reuses the same
|
|
@@ -646,7 +659,43 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
646
659
|
/** Consume a `/model` selection callback. Returns true for all model-select ids (incl. stale). */
|
|
647
660
|
private handleModelSelection;
|
|
648
661
|
/** Apply a model to an instance: runtime paste (claude-code) or persist + restart (others). */
|
|
662
|
+
/** AgEnD's canonical effort ladder, low → max. Backends expose a subset. */
|
|
663
|
+
static readonly EFFORT_LEVELS: readonly ["low", "medium", "high", "xhigh", "max"];
|
|
664
|
+
/** How this instance's backend applies an effort change. */
|
|
665
|
+
effortStrategyFor(instanceName: string): "runtime" | "restart" | "unsupported";
|
|
666
|
+
/** Effort levels this instance's backend actually accepts (empty = unsupported). */
|
|
667
|
+
effortLevelsFor(instanceName: string): string[];
|
|
668
|
+
/** Configured effort for an instance: per-instance, else fleet default, else none. */
|
|
669
|
+
resolveInstanceEffort(instanceName: string): {
|
|
670
|
+
effort: string | null;
|
|
671
|
+
source: "instance" | "fleet-default" | "unset";
|
|
672
|
+
};
|
|
673
|
+
/**
|
|
674
|
+
* Clamp a canonical level to the nearest one this backend supports.
|
|
675
|
+
*
|
|
676
|
+
* Clamping DOWN the ladder, never up: asking for `max` on a CLI that stops at
|
|
677
|
+
* `high` should get high, not silently fall to low. The caller reports the
|
|
678
|
+
* clamp — a user who asks for max and quietly receives high has been told the
|
|
679
|
+
* request succeeded when it did not.
|
|
680
|
+
*/
|
|
681
|
+
static clampEffort(level: string, supported: string[]): string | null;
|
|
682
|
+
/**
|
|
683
|
+
* Apply a reasoning-effort level, mirroring applyModel's shape.
|
|
684
|
+
*
|
|
685
|
+
* runtime backends take `/effort <level>` in the pane and keep working;
|
|
686
|
+
* restart backends only read it at launch, so it is persisted and the
|
|
687
|
+
* instance respawns.
|
|
688
|
+
*/
|
|
689
|
+
applyEffort(instanceName: string, requested: string): Promise<string>;
|
|
649
690
|
applyModel(instanceName: string, model: string): Promise<string>;
|
|
691
|
+
/**
|
|
692
|
+
* The trailing "Current effort: …" line for a /model reply.
|
|
693
|
+
*
|
|
694
|
+
* Model and effort interact (a cheaper model at max effort is a different
|
|
695
|
+
* trade than a bigger one at low), so showing the effort in force right after
|
|
696
|
+
* a switch saves the round trip of asking. Empty when the backend has none.
|
|
697
|
+
*/
|
|
698
|
+
private effortSuffix;
|
|
650
699
|
/** Read recent chat log for agent context */
|
|
651
700
|
private getRecentChatLog;
|
|
652
701
|
/** Return a user-facing blocker without mutating ClassicBot state. */
|
package/dist/fleet-manager.js
CHANGED
|
@@ -122,19 +122,24 @@ const CANCEL_BTN_LEDGER_FILE = "cancel-buttons.json";
|
|
|
122
122
|
*/
|
|
123
123
|
const PROGRESS_UPDATE_INTERVAL_MS = 60_000;
|
|
124
124
|
/** Elapsed time is only shown once work has clearly outlasted a quick answer. */
|
|
125
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Default delay before the button starts showing elapsed time. Configurable via
|
|
127
|
+
* `defaults.progress_min_elapsed` (seconds) in fleet.yaml. 30s is the balance
|
|
128
|
+
* point: most quick answers finish inside it (no churn for ordinary turns),
|
|
129
|
+
* while anything real shows signs of life well before the old two minutes.
|
|
130
|
+
*/
|
|
131
|
+
const PROGRESS_MIN_ELAPSED_MS = 30_000;
|
|
126
132
|
/** How much of a tool summary the progress line will show before eliding. */
|
|
127
133
|
const PROGRESS_ACTIVITY_MAX_CHARS = 48;
|
|
128
134
|
/**
|
|
129
|
-
* Emoji AgEnD itself stamps on messages as
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* already attached to these glyphs, that reading is ambiguous anyway.
|
|
135
|
+
* Emoji AgEnD itself stamps on messages as the delivery-status ladder
|
|
136
|
+
* (⏳ queued, 👀 delivered, ✅ confirmed, ❌ failed). These are machine
|
|
137
|
+
* indicators, not opinions, so they never enter the reactions queue — from
|
|
138
|
+
* anyone. This exact-emoji filter is the ONLY bot filtering left: bot-to-bot
|
|
139
|
+
* reactions are otherwise delivered on purpose (agents signal each other), and
|
|
140
|
+
* 🫡 passes too — it reads as a deliberate acknowledgement, not plumbing.
|
|
136
141
|
*/
|
|
137
|
-
const DELIVERY_STATUS_EMOJIS = new Set(["👀", "⏳", "✅", "❌"
|
|
142
|
+
const DELIVERY_STATUS_EMOJIS = new Set(["👀", "⏳", "✅", "❌"]);
|
|
138
143
|
/**
|
|
139
144
|
* How long a delivery waits out a disconnected instance IPC before giving up.
|
|
140
145
|
*
|
|
@@ -146,6 +151,7 @@ const IPC_RECONNECT_POLL_MS = 250;
|
|
|
146
151
|
const CLASSIC_BACKEND_SELECTION_TIMEOUT_MS = 60_000;
|
|
147
152
|
const CLASSIC_BACKEND_CALLBACK_PREFIX = "classic-backend:";
|
|
148
153
|
const MODEL_SELECT_CALLBACK_PREFIX = "model-select:";
|
|
154
|
+
const EFFORT_SELECT_CALLBACK_PREFIX = "effort-select:";
|
|
149
155
|
const CLI_ENV_TTL_MS = 24 * 60 * 60 * 1000; // /model reads cached CLI env within 24h
|
|
150
156
|
export class FleetManager {
|
|
151
157
|
dataDir;
|
|
@@ -224,6 +230,8 @@ export class FleetManager {
|
|
|
224
230
|
classicChannels = null;
|
|
225
231
|
pendingClassicStarts = new Map();
|
|
226
232
|
/** In-flight /model selections, keyed by nonce (see handleModelSelection). */
|
|
233
|
+
/** In-flight /effort selections, same coordinator shape as pendingModelSelects. */
|
|
234
|
+
pendingEffortSelects = new Map();
|
|
227
235
|
pendingModelSelects = new Map();
|
|
228
236
|
// Model failover state
|
|
229
237
|
failoverActive = new Map(); // instance → current failover model
|
|
@@ -1679,6 +1687,8 @@ export class FleetManager {
|
|
|
1679
1687
|
return;
|
|
1680
1688
|
if (await this.handleModelSelection(data))
|
|
1681
1689
|
return;
|
|
1690
|
+
if (await this.handleEffortSelection(data))
|
|
1691
|
+
return;
|
|
1682
1692
|
if (data.callbackData.startsWith("hang:")) {
|
|
1683
1693
|
const parts = data.callbackData.split(":");
|
|
1684
1694
|
const action = parts[1];
|
|
@@ -1784,6 +1794,9 @@ export class FleetManager {
|
|
|
1784
1794
|
else if (data.command === "model") {
|
|
1785
1795
|
await this.handleModelSlash(data, adapterId);
|
|
1786
1796
|
}
|
|
1797
|
+
else if (data.command === "effort") {
|
|
1798
|
+
await this.handleEffortSlash(data, adapterId);
|
|
1799
|
+
}
|
|
1787
1800
|
else if (data.command === "cancel") {
|
|
1788
1801
|
const name = this.resolveSlashTarget(data.channelId, adapterId);
|
|
1789
1802
|
if (!name) {
|
|
@@ -1987,6 +2000,8 @@ export class FleetManager {
|
|
|
1987
2000
|
return;
|
|
1988
2001
|
if (await this.handleModelSelection(data))
|
|
1989
2002
|
return;
|
|
2003
|
+
if (await this.handleEffortSelection(data))
|
|
2004
|
+
return;
|
|
1990
2005
|
if (data.callbackData.startsWith("hang:")) {
|
|
1991
2006
|
const parts = data.callbackData.split(":");
|
|
1992
2007
|
const action = parts[1];
|
|
@@ -2079,6 +2094,9 @@ export class FleetManager {
|
|
|
2079
2094
|
else if (data.command === "model") {
|
|
2080
2095
|
await this.handleModelSlash(data, adapterId);
|
|
2081
2096
|
}
|
|
2097
|
+
else if (data.command === "effort") {
|
|
2098
|
+
await this.handleEffortSlash(data, adapterId);
|
|
2099
|
+
}
|
|
2082
2100
|
else if (data.command === "cancel") {
|
|
2083
2101
|
const name = this.resolveSlashTarget(data.channelId, adapterId);
|
|
2084
2102
|
if (!name) {
|
|
@@ -4213,8 +4231,8 @@ export class FleetManager {
|
|
|
4213
4231
|
* progress indicator (#409) — the channel showed nothing at all during long work,
|
|
4214
4232
|
* and once the agent had replied once there was no sign it was still going.
|
|
4215
4233
|
*/
|
|
4216
|
-
static progressText(elapsedMs, activity) {
|
|
4217
|
-
if (elapsedMs <
|
|
4234
|
+
static progressText(elapsedMs, activity, minElapsedMs = PROGRESS_MIN_ELAPSED_MS) {
|
|
4235
|
+
if (elapsedMs < minElapsedMs)
|
|
4218
4236
|
return "👀 處理中…";
|
|
4219
4237
|
const totalSeconds = Math.floor(elapsedMs / 1000);
|
|
4220
4238
|
const minutes = Math.floor(totalSeconds / 60);
|
|
@@ -4265,13 +4283,22 @@ export class FleetManager {
|
|
|
4265
4283
|
* and the Bot API treats that as "clear the keyboard" — so editing with it would
|
|
4266
4284
|
* delete the very cancel button this is trying to keep alive.
|
|
4267
4285
|
*/
|
|
4286
|
+
/** Configured threshold before elapsed time appears, in ms. */
|
|
4287
|
+
progressMinElapsedMs() {
|
|
4288
|
+
const seconds = this.fleetConfig?.defaults
|
|
4289
|
+
?.progress_min_elapsed;
|
|
4290
|
+
if (typeof seconds === "number" && Number.isFinite(seconds) && seconds >= 0) {
|
|
4291
|
+
return seconds * 1000;
|
|
4292
|
+
}
|
|
4293
|
+
return PROGRESS_MIN_ELAPSED_MS;
|
|
4294
|
+
}
|
|
4268
4295
|
startProgressTicker(entry) {
|
|
4269
|
-
|
|
4296
|
+
const tick = () => {
|
|
4270
4297
|
if (!this.cancelButtons.has(entry.messageId)) {
|
|
4271
4298
|
clearInterval(entry.progressTimer);
|
|
4272
4299
|
return;
|
|
4273
4300
|
}
|
|
4274
|
-
const text = FleetManager.progressText(Date.now() - (entry.startedAt ?? Date.now()), this.instanceActivity.get(entry.instanceName));
|
|
4301
|
+
const text = FleetManager.progressText(Date.now() - (entry.startedAt ?? Date.now()), this.instanceActivity.get(entry.instanceName), this.progressMinElapsedMs());
|
|
4275
4302
|
if (text === entry.lastProgressText)
|
|
4276
4303
|
return; // nothing changed — skip the API call
|
|
4277
4304
|
const adapter = this.getAdapterForInstance(entry.instanceName) ?? this.adapter;
|
|
@@ -4290,8 +4317,17 @@ export class FleetManager {
|
|
|
4290
4317
|
// rate limit.
|
|
4291
4318
|
this.logger.debug({ err, instanceName: entry.instanceName }, "Progress edit failed");
|
|
4292
4319
|
});
|
|
4293
|
-
}
|
|
4320
|
+
};
|
|
4321
|
+
entry.progressTimer = setInterval(tick, PROGRESS_UPDATE_INTERVAL_MS);
|
|
4294
4322
|
entry.progressTimer.unref?.();
|
|
4323
|
+
// One extra tick right when the threshold passes, so a 30s threshold shows
|
|
4324
|
+
// time at ~30s instead of waiting for the first 60s interval. Costs at most
|
|
4325
|
+
// one additional edit per turn that lives past the threshold.
|
|
4326
|
+
const firstAt = this.progressMinElapsedMs() - (Date.now() - (entry.startedAt ?? Date.now()));
|
|
4327
|
+
if (firstAt > 0 && firstAt < PROGRESS_UPDATE_INTERVAL_MS) {
|
|
4328
|
+
const firstTick = setTimeout(tick, firstAt);
|
|
4329
|
+
firstTick.unref?.();
|
|
4330
|
+
}
|
|
4295
4331
|
}
|
|
4296
4332
|
/**
|
|
4297
4333
|
* After a reply: give the instance REPLY_RETIRE_GRACE_MS to resume working; if
|
|
@@ -5227,6 +5263,170 @@ When users create specialized instances, suggest these configurations:
|
|
|
5227
5263
|
return env?.models ?? [];
|
|
5228
5264
|
}
|
|
5229
5265
|
/** `/model` slash handler (admin only). No arg → DC menu; `/model <name>` → apply directly. */
|
|
5266
|
+
/** Label an effort choice, marking the one currently configured. */
|
|
5267
|
+
effortChoiceLabel(level, current) {
|
|
5268
|
+
return level === current ? `✓ ${level}` : level;
|
|
5269
|
+
}
|
|
5270
|
+
effortMenuHeader(instanceName) {
|
|
5271
|
+
const { effort, source } = this.resolveInstanceEffort(instanceName);
|
|
5272
|
+
if (!effort)
|
|
5273
|
+
return "Current effort: (CLI default)";
|
|
5274
|
+
return source === "fleet-default"
|
|
5275
|
+
? `Current effort: ${effort} (fleet default)`
|
|
5276
|
+
: `Current effort: ${effort}`;
|
|
5277
|
+
}
|
|
5278
|
+
/** `/effort` — DC Select Menu, or apply directly when a level is given. */
|
|
5279
|
+
async handleEffortSlash(data, adapterId) {
|
|
5280
|
+
if (!this.isModelAdmin(data.userId, data.channelId, adapterId)) {
|
|
5281
|
+
await data.respond(t("permission.denied"));
|
|
5282
|
+
return;
|
|
5283
|
+
}
|
|
5284
|
+
const name = this.resolveSlashTarget(data.channelId, adapterId);
|
|
5285
|
+
if (!name) {
|
|
5286
|
+
await data.respond(t("classic.no_agent"));
|
|
5287
|
+
return;
|
|
5288
|
+
}
|
|
5289
|
+
const requested = (typeof data.options?.level === "string" ? data.options.level.trim() : "")
|
|
5290
|
+
|| (data.text?.trim() ?? "");
|
|
5291
|
+
if (requested) {
|
|
5292
|
+
await data.respond(await this.applyEffort(name, requested));
|
|
5293
|
+
return;
|
|
5294
|
+
}
|
|
5295
|
+
const levels = this.effortLevelsFor(name);
|
|
5296
|
+
if (levels.length === 0) {
|
|
5297
|
+
await data.respond(`❌ ${this.backendNameForInstance(name)} has no reasoning-effort setting.`);
|
|
5298
|
+
return;
|
|
5299
|
+
}
|
|
5300
|
+
if (!data.respondChoices) {
|
|
5301
|
+
await data.respond(`Usage: /effort <${levels.join("|")}>`);
|
|
5302
|
+
return;
|
|
5303
|
+
}
|
|
5304
|
+
const current = this.resolveInstanceEffort(name).effort;
|
|
5305
|
+
const nonce = randomBytes(6).toString("hex");
|
|
5306
|
+
const choices = levels.map(l => ({
|
|
5307
|
+
id: `${EFFORT_SELECT_CALLBACK_PREFIX}${nonce}:${l}`,
|
|
5308
|
+
label: this.effortChoiceLabel(l, current),
|
|
5309
|
+
}));
|
|
5310
|
+
const timer = setTimeout(() => this.pendingEffortSelects.delete(nonce), CLASSIC_BACKEND_SELECTION_TIMEOUT_MS);
|
|
5311
|
+
timer.unref?.();
|
|
5312
|
+
this.pendingEffortSelects.set(nonce, { instanceName: name, userId: data.userId, channelId: data.channelId, timer, respond: data.respond });
|
|
5313
|
+
try {
|
|
5314
|
+
await data.respondChoices(`${this.effortMenuHeader(name)}\nSelect a new effort level:`, choices);
|
|
5315
|
+
}
|
|
5316
|
+
catch (err) {
|
|
5317
|
+
this.pendingEffortSelects.delete(nonce);
|
|
5318
|
+
clearTimeout(timer);
|
|
5319
|
+
this.logger.warn({ err, instanceName: name }, "effort menu failed");
|
|
5320
|
+
await data.respond(`Usage: /effort <${levels.join("|")}>`);
|
|
5321
|
+
}
|
|
5322
|
+
}
|
|
5323
|
+
/** TG inline-keyboard effort menu. Returns null on success, else a fallback string. */
|
|
5324
|
+
async promptEffortMenu(instanceName, userId, channelId, adapter, chatId, threadId) {
|
|
5325
|
+
const levels = this.effortLevelsFor(instanceName);
|
|
5326
|
+
if (levels.length === 0) {
|
|
5327
|
+
return `❌ ${this.backendNameForInstance(instanceName)} has no reasoning-effort setting.`;
|
|
5328
|
+
}
|
|
5329
|
+
const current = this.resolveInstanceEffort(instanceName).effort;
|
|
5330
|
+
const nonce = randomBytes(6).toString("hex");
|
|
5331
|
+
const choices = levels.map(l => ({
|
|
5332
|
+
id: `${EFFORT_SELECT_CALLBACK_PREFIX}${nonce}:${l}`,
|
|
5333
|
+
label: this.effortChoiceLabel(l, current),
|
|
5334
|
+
}));
|
|
5335
|
+
const respond = async (text) => {
|
|
5336
|
+
await adapter.sendText(chatId, text, { threadId });
|
|
5337
|
+
return undefined;
|
|
5338
|
+
};
|
|
5339
|
+
const timer = setTimeout(() => {
|
|
5340
|
+
const p = this.pendingEffortSelects.get(nonce);
|
|
5341
|
+
if (p) {
|
|
5342
|
+
this.pendingEffortSelects.delete(nonce);
|
|
5343
|
+
p.respond("⏰ Effort selection expired.").catch(() => { });
|
|
5344
|
+
}
|
|
5345
|
+
}, CLASSIC_BACKEND_SELECTION_TIMEOUT_MS);
|
|
5346
|
+
timer.unref?.();
|
|
5347
|
+
this.pendingEffortSelects.set(nonce, { instanceName, userId, channelId, timer, respond, adapter, adapterChatId: chatId, adapterThreadId: threadId });
|
|
5348
|
+
try {
|
|
5349
|
+
const menuMessageId = await adapter.promptUser(chatId, `${this.effortMenuHeader(instanceName)}\nSelect a new effort level:`, choices, { threadId });
|
|
5350
|
+
const pending = this.pendingEffortSelects.get(nonce);
|
|
5351
|
+
if (pending)
|
|
5352
|
+
pending.menuMessageId = menuMessageId;
|
|
5353
|
+
return null;
|
|
5354
|
+
}
|
|
5355
|
+
catch (err) {
|
|
5356
|
+
this.pendingEffortSelects.delete(nonce);
|
|
5357
|
+
clearTimeout(timer);
|
|
5358
|
+
this.logger.warn({ err, instanceName }, "TG effort menu failed");
|
|
5359
|
+
return `Usage: /effort <${levels.join("|")}>`;
|
|
5360
|
+
}
|
|
5361
|
+
}
|
|
5362
|
+
/** Consume an `/effort` selection callback. Mirrors handleModelSelection. */
|
|
5363
|
+
async handleEffortSelection(data) {
|
|
5364
|
+
if (!data.callbackData.startsWith(EFFORT_SELECT_CALLBACK_PREFIX))
|
|
5365
|
+
return false;
|
|
5366
|
+
const match = data.callbackData.match(/^effort-select:([0-9a-f]+):(.+)$/);
|
|
5367
|
+
if (!match)
|
|
5368
|
+
return true;
|
|
5369
|
+
const pending = this.pendingEffortSelects.get(match[1]);
|
|
5370
|
+
if (!pending)
|
|
5371
|
+
return true;
|
|
5372
|
+
if (data.userId && data.userId !== pending.userId)
|
|
5373
|
+
return true;
|
|
5374
|
+
const cbChannel = data.threadId ?? data.chatId;
|
|
5375
|
+
if (cbChannel !== pending.channelId && data.chatId !== pending.channelId)
|
|
5376
|
+
return true;
|
|
5377
|
+
this.pendingEffortSelects.delete(match[1]);
|
|
5378
|
+
clearTimeout(pending.timer);
|
|
5379
|
+
const level = match[2];
|
|
5380
|
+
const progressText = `⏳ Setting ${pending.instanceName} effort to \`${level}\`…`;
|
|
5381
|
+
let progressMsgId;
|
|
5382
|
+
if (pending.adapter && pending.adapterChatId) {
|
|
5383
|
+
const menuMessageId = pending.menuMessageId ?? data.messageId;
|
|
5384
|
+
if (menuMessageId && pending.adapter.editMessageRemoveButtons) {
|
|
5385
|
+
try {
|
|
5386
|
+
await pending.adapter.editMessageRemoveButtons(pending.adapterChatId, menuMessageId, progressText, pending.adapterThreadId);
|
|
5387
|
+
progressMsgId = menuMessageId;
|
|
5388
|
+
}
|
|
5389
|
+
catch { /* fall back to a new message */ }
|
|
5390
|
+
}
|
|
5391
|
+
if (!progressMsgId) {
|
|
5392
|
+
try {
|
|
5393
|
+
const sent = await pending.adapter.sendText(pending.adapterChatId, progressText, { threadId: pending.adapterThreadId });
|
|
5394
|
+
progressMsgId = sent.messageId;
|
|
5395
|
+
}
|
|
5396
|
+
catch { /* non-fatal */ }
|
|
5397
|
+
}
|
|
5398
|
+
}
|
|
5399
|
+
else {
|
|
5400
|
+
await pending.respond(progressText).catch(() => { });
|
|
5401
|
+
}
|
|
5402
|
+
// Background-applied and guarded for the same reason as the model path: a
|
|
5403
|
+
// restart backend respawns the instance here, and an unguarded rejection
|
|
5404
|
+
// from a menu click must not take the fleet down.
|
|
5405
|
+
void (async () => {
|
|
5406
|
+
let result;
|
|
5407
|
+
try {
|
|
5408
|
+
result = await this.applyEffort(pending.instanceName, level);
|
|
5409
|
+
}
|
|
5410
|
+
catch (err) {
|
|
5411
|
+
this.logger.error({ err, instance: pending.instanceName, level }, "Effort switch failed");
|
|
5412
|
+
result = `Effort switch to \`${level}\` failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
5413
|
+
}
|
|
5414
|
+
if (pending.adapter && pending.adapterChatId) {
|
|
5415
|
+
if (progressMsgId) {
|
|
5416
|
+
pending.adapter.editMessage(pending.adapterChatId, progressMsgId, result, pending.adapterThreadId).catch(() => {
|
|
5417
|
+
pending.adapter.sendText(pending.adapterChatId, result, { threadId: pending.adapterThreadId }).catch(() => { });
|
|
5418
|
+
});
|
|
5419
|
+
}
|
|
5420
|
+
else {
|
|
5421
|
+
pending.adapter.sendText(pending.adapterChatId, result, { threadId: pending.adapterThreadId }).catch(() => { });
|
|
5422
|
+
}
|
|
5423
|
+
}
|
|
5424
|
+
else {
|
|
5425
|
+
await pending.respond(result).catch(() => { });
|
|
5426
|
+
}
|
|
5427
|
+
})();
|
|
5428
|
+
return true;
|
|
5429
|
+
}
|
|
5230
5430
|
async handleModelSlash(data, adapterId) {
|
|
5231
5431
|
if (!this.isModelAdmin(data.userId, data.channelId, adapterId)) {
|
|
5232
5432
|
await data.respond(t("permission.denied"));
|
|
@@ -5393,6 +5593,110 @@ When users create specialized instances, suggest these configurations:
|
|
|
5393
5593
|
return true;
|
|
5394
5594
|
}
|
|
5395
5595
|
/** Apply a model to an instance: runtime paste (claude-code) or persist + restart (others). */
|
|
5596
|
+
/** AgEnD's canonical effort ladder, low → max. Backends expose a subset. */
|
|
5597
|
+
static EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
5598
|
+
/** How this instance's backend applies an effort change. */
|
|
5599
|
+
effortStrategyFor(instanceName) {
|
|
5600
|
+
try {
|
|
5601
|
+
const backend = createBackend(this.backendNameForInstance(instanceName), this.getInstanceDir(instanceName));
|
|
5602
|
+
const strategy = backend.getEffortStrategy?.() ?? "unsupported";
|
|
5603
|
+
// A backend claiming support but listing no levels is unusable either way.
|
|
5604
|
+
return strategy !== "unsupported" && (backend.getEffortLevels?.() ?? []).length > 0
|
|
5605
|
+
? strategy
|
|
5606
|
+
: "unsupported";
|
|
5607
|
+
}
|
|
5608
|
+
catch {
|
|
5609
|
+
return "unsupported";
|
|
5610
|
+
}
|
|
5611
|
+
}
|
|
5612
|
+
/** Effort levels this instance's backend actually accepts (empty = unsupported). */
|
|
5613
|
+
effortLevelsFor(instanceName) {
|
|
5614
|
+
try {
|
|
5615
|
+
const backend = createBackend(this.backendNameForInstance(instanceName), this.getInstanceDir(instanceName));
|
|
5616
|
+
if ((backend.getEffortStrategy?.() ?? "unsupported") === "unsupported")
|
|
5617
|
+
return [];
|
|
5618
|
+
return backend.getEffortLevels?.() ?? [];
|
|
5619
|
+
}
|
|
5620
|
+
catch {
|
|
5621
|
+
return [];
|
|
5622
|
+
}
|
|
5623
|
+
}
|
|
5624
|
+
/** Configured effort for an instance: per-instance, else fleet default, else none. */
|
|
5625
|
+
resolveInstanceEffort(instanceName) {
|
|
5626
|
+
const own = this.fleetConfig?.instances[instanceName]?.effort;
|
|
5627
|
+
if (own)
|
|
5628
|
+
return { effort: own, source: "instance" };
|
|
5629
|
+
const fallback = this.fleetConfig?.defaults?.effort;
|
|
5630
|
+
if (fallback)
|
|
5631
|
+
return { effort: fallback, source: "fleet-default" };
|
|
5632
|
+
return { effort: null, source: "unset" };
|
|
5633
|
+
}
|
|
5634
|
+
/**
|
|
5635
|
+
* Clamp a canonical level to the nearest one this backend supports.
|
|
5636
|
+
*
|
|
5637
|
+
* Clamping DOWN the ladder, never up: asking for `max` on a CLI that stops at
|
|
5638
|
+
* `high` should get high, not silently fall to low. The caller reports the
|
|
5639
|
+
* clamp — a user who asks for max and quietly receives high has been told the
|
|
5640
|
+
* request succeeded when it did not.
|
|
5641
|
+
*/
|
|
5642
|
+
static clampEffort(level, supported) {
|
|
5643
|
+
if (supported.includes(level))
|
|
5644
|
+
return level;
|
|
5645
|
+
const ladder = FleetManager.EFFORT_LEVELS;
|
|
5646
|
+
const wanted = ladder.indexOf(level);
|
|
5647
|
+
if (wanted < 0)
|
|
5648
|
+
return null;
|
|
5649
|
+
for (let i = wanted - 1; i >= 0; i--) {
|
|
5650
|
+
if (supported.includes(ladder[i]))
|
|
5651
|
+
return ladder[i];
|
|
5652
|
+
}
|
|
5653
|
+
return supported[0] ?? null;
|
|
5654
|
+
}
|
|
5655
|
+
/**
|
|
5656
|
+
* Apply a reasoning-effort level, mirroring applyModel's shape.
|
|
5657
|
+
*
|
|
5658
|
+
* runtime backends take `/effort <level>` in the pane and keep working;
|
|
5659
|
+
* restart backends only read it at launch, so it is persisted and the
|
|
5660
|
+
* instance respawns.
|
|
5661
|
+
*/
|
|
5662
|
+
async applyEffort(instanceName, requested) {
|
|
5663
|
+
const level = requested.trim().toLowerCase();
|
|
5664
|
+
const backendName = this.backendNameForInstance(instanceName);
|
|
5665
|
+
let strategy = "unsupported";
|
|
5666
|
+
let supported = [];
|
|
5667
|
+
try {
|
|
5668
|
+
const backend = createBackend(backendName, this.getInstanceDir(instanceName));
|
|
5669
|
+
strategy = backend.getEffortStrategy?.() ?? "unsupported";
|
|
5670
|
+
supported = backend.getEffortLevels?.() ?? [];
|
|
5671
|
+
}
|
|
5672
|
+
catch { /* treated as unsupported below */ }
|
|
5673
|
+
if (strategy === "unsupported" || supported.length === 0) {
|
|
5674
|
+
return `❌ ${backendName} has no reasoning-effort setting.`;
|
|
5675
|
+
}
|
|
5676
|
+
if (!FleetManager.EFFORT_LEVELS.includes(level)) {
|
|
5677
|
+
return `❌ Unknown effort level \`${level}\`. Use: ${FleetManager.EFFORT_LEVELS.join(", ")}.`;
|
|
5678
|
+
}
|
|
5679
|
+
const applied = FleetManager.clampEffort(level, supported);
|
|
5680
|
+
if (!applied)
|
|
5681
|
+
return `❌ ${backendName} accepts none of the canonical effort levels.`;
|
|
5682
|
+
const warn = applied === level
|
|
5683
|
+
? ""
|
|
5684
|
+
: `⚠️ Clamped to \`${applied}\` (\`${level}\` not supported by ${backendName}).\n`;
|
|
5685
|
+
// Persist either way: a runtime switch must survive the next respawn too,
|
|
5686
|
+
// or the instance silently reverts on restart.
|
|
5687
|
+
if (this.fleetConfig?.instances[instanceName]) {
|
|
5688
|
+
this.fleetConfig.instances[instanceName].effort = applied;
|
|
5689
|
+
this.saveFleetConfig();
|
|
5690
|
+
}
|
|
5691
|
+
if (strategy === "runtime") {
|
|
5692
|
+
if (!this.instanceIpcClients.get(instanceName))
|
|
5693
|
+
return `${warn}❌ ${instanceName} is not running.`;
|
|
5694
|
+
this.pasteRawToClassicInstance(instanceName, `/effort ${applied}`);
|
|
5695
|
+
return `${warn}✅ Set ${instanceName} effort to \`${applied}\` (runtime).`;
|
|
5696
|
+
}
|
|
5697
|
+
await this.restartSingleInstance(instanceName);
|
|
5698
|
+
return `${warn}✅ Set ${instanceName} effort to \`${applied}\` and restarted.`;
|
|
5699
|
+
}
|
|
5396
5700
|
async applyModel(instanceName, model) {
|
|
5397
5701
|
const backendName = this.backendNameForInstance(instanceName);
|
|
5398
5702
|
let strategy = "restart";
|
|
@@ -5405,7 +5709,7 @@ When users create specialized instances, suggest these configurations:
|
|
|
5405
5709
|
if (!this.instanceIpcClients.get(instanceName))
|
|
5406
5710
|
return `${warn}❌ ${instanceName} is not running.`;
|
|
5407
5711
|
this.pasteRawToClassicInstance(instanceName, `/model ${model}`);
|
|
5408
|
-
return `${warn}✅ Switched ${instanceName} to \`${model}\` (runtime)
|
|
5712
|
+
return `${warn}✅ Switched ${instanceName} to \`${model}\` (runtime).${this.effortSuffix(instanceName)}`;
|
|
5409
5713
|
}
|
|
5410
5714
|
// restart: persist the model so the respawned CLI launches with it.
|
|
5411
5715
|
let persisted = false;
|
|
@@ -5420,7 +5724,24 @@ When users create specialized instances, suggest these configurations:
|
|
|
5420
5724
|
if (!persisted)
|
|
5421
5725
|
return `${warn}❌ Could not set model for ${instanceName}.`;
|
|
5422
5726
|
await this.restartSingleInstance(instanceName);
|
|
5423
|
-
return `${warn}✅ Set ${instanceName} to \`${model}\` and restarted
|
|
5727
|
+
return `${warn}✅ Set ${instanceName} to \`${model}\` and restarted.${this.effortSuffix(instanceName)}`;
|
|
5728
|
+
}
|
|
5729
|
+
/**
|
|
5730
|
+
* The trailing "Current effort: …" line for a /model reply.
|
|
5731
|
+
*
|
|
5732
|
+
* Model and effort interact (a cheaper model at max effort is a different
|
|
5733
|
+
* trade than a bigger one at low), so showing the effort in force right after
|
|
5734
|
+
* a switch saves the round trip of asking. Empty when the backend has none.
|
|
5735
|
+
*/
|
|
5736
|
+
effortSuffix(instanceName) {
|
|
5737
|
+
if (this.effortLevelsFor(instanceName).length === 0)
|
|
5738
|
+
return "";
|
|
5739
|
+
const { effort, source } = this.resolveInstanceEffort(instanceName);
|
|
5740
|
+
if (!effort)
|
|
5741
|
+
return "\nCurrent effort: (CLI default)";
|
|
5742
|
+
return source === "fleet-default"
|
|
5743
|
+
? `\nCurrent effort: ${effort} (fleet default)`
|
|
5744
|
+
: `\nCurrent effort: ${effort}`;
|
|
5424
5745
|
}
|
|
5425
5746
|
/** Read recent chat log for agent context */
|
|
5426
5747
|
getRecentChatLog(instanceName, maxLines = 10) {
|