@songsid/agend 2.1.2-beta.22 → 2.1.2-beta.24
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 +12 -0
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/config-validator.js +6 -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 +4 -0
- package/dist/fleet-manager.d.ts +37 -0
- package/dist/fleet-manager.js +267 -0
- package/dist/fleet-manager.js.map +1 -1
- package/dist/locale.js +1 -0
- package/dist/locale.js.map +1 -1
- package/dist/topic-commands.js +24 -0
- package/dist/topic-commands.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/usage/format-rich.js +6 -1
- package/dist/usage/format-rich.js.map +1 -1
- package/dist/usage/providers.d.ts +16 -0
- package/dist/usage/providers.js +67 -8
- package/dist/usage/providers.js.map +1 -1
- package/package.json +1 -1
package/dist/fleet-manager.js
CHANGED
|
@@ -146,6 +146,7 @@ const IPC_RECONNECT_POLL_MS = 250;
|
|
|
146
146
|
const CLASSIC_BACKEND_SELECTION_TIMEOUT_MS = 60_000;
|
|
147
147
|
const CLASSIC_BACKEND_CALLBACK_PREFIX = "classic-backend:";
|
|
148
148
|
const MODEL_SELECT_CALLBACK_PREFIX = "model-select:";
|
|
149
|
+
const EFFORT_SELECT_CALLBACK_PREFIX = "effort-select:";
|
|
149
150
|
const CLI_ENV_TTL_MS = 24 * 60 * 60 * 1000; // /model reads cached CLI env within 24h
|
|
150
151
|
export class FleetManager {
|
|
151
152
|
dataDir;
|
|
@@ -224,6 +225,8 @@ export class FleetManager {
|
|
|
224
225
|
classicChannels = null;
|
|
225
226
|
pendingClassicStarts = new Map();
|
|
226
227
|
/** In-flight /model selections, keyed by nonce (see handleModelSelection). */
|
|
228
|
+
/** In-flight /effort selections, same coordinator shape as pendingModelSelects. */
|
|
229
|
+
pendingEffortSelects = new Map();
|
|
227
230
|
pendingModelSelects = new Map();
|
|
228
231
|
// Model failover state
|
|
229
232
|
failoverActive = new Map(); // instance → current failover model
|
|
@@ -1679,6 +1682,8 @@ export class FleetManager {
|
|
|
1679
1682
|
return;
|
|
1680
1683
|
if (await this.handleModelSelection(data))
|
|
1681
1684
|
return;
|
|
1685
|
+
if (await this.handleEffortSelection(data))
|
|
1686
|
+
return;
|
|
1682
1687
|
if (data.callbackData.startsWith("hang:")) {
|
|
1683
1688
|
const parts = data.callbackData.split(":");
|
|
1684
1689
|
const action = parts[1];
|
|
@@ -1784,6 +1789,9 @@ export class FleetManager {
|
|
|
1784
1789
|
else if (data.command === "model") {
|
|
1785
1790
|
await this.handleModelSlash(data, adapterId);
|
|
1786
1791
|
}
|
|
1792
|
+
else if (data.command === "effort") {
|
|
1793
|
+
await this.handleEffortSlash(data, adapterId);
|
|
1794
|
+
}
|
|
1787
1795
|
else if (data.command === "cancel") {
|
|
1788
1796
|
const name = this.resolveSlashTarget(data.channelId, adapterId);
|
|
1789
1797
|
if (!name) {
|
|
@@ -1987,6 +1995,8 @@ export class FleetManager {
|
|
|
1987
1995
|
return;
|
|
1988
1996
|
if (await this.handleModelSelection(data))
|
|
1989
1997
|
return;
|
|
1998
|
+
if (await this.handleEffortSelection(data))
|
|
1999
|
+
return;
|
|
1990
2000
|
if (data.callbackData.startsWith("hang:")) {
|
|
1991
2001
|
const parts = data.callbackData.split(":");
|
|
1992
2002
|
const action = parts[1];
|
|
@@ -2079,6 +2089,9 @@ export class FleetManager {
|
|
|
2079
2089
|
else if (data.command === "model") {
|
|
2080
2090
|
await this.handleModelSlash(data, adapterId);
|
|
2081
2091
|
}
|
|
2092
|
+
else if (data.command === "effort") {
|
|
2093
|
+
await this.handleEffortSlash(data, adapterId);
|
|
2094
|
+
}
|
|
2082
2095
|
else if (data.command === "cancel") {
|
|
2083
2096
|
const name = this.resolveSlashTarget(data.channelId, adapterId);
|
|
2084
2097
|
if (!name) {
|
|
@@ -5227,6 +5240,170 @@ When users create specialized instances, suggest these configurations:
|
|
|
5227
5240
|
return env?.models ?? [];
|
|
5228
5241
|
}
|
|
5229
5242
|
/** `/model` slash handler (admin only). No arg → DC menu; `/model <name>` → apply directly. */
|
|
5243
|
+
/** Label an effort choice, marking the one currently configured. */
|
|
5244
|
+
effortChoiceLabel(level, current) {
|
|
5245
|
+
return level === current ? `✓ ${level}` : level;
|
|
5246
|
+
}
|
|
5247
|
+
effortMenuHeader(instanceName) {
|
|
5248
|
+
const { effort, source } = this.resolveInstanceEffort(instanceName);
|
|
5249
|
+
if (!effort)
|
|
5250
|
+
return "Current effort: (CLI default)";
|
|
5251
|
+
return source === "fleet-default"
|
|
5252
|
+
? `Current effort: ${effort} (fleet default)`
|
|
5253
|
+
: `Current effort: ${effort}`;
|
|
5254
|
+
}
|
|
5255
|
+
/** `/effort` — DC Select Menu, or apply directly when a level is given. */
|
|
5256
|
+
async handleEffortSlash(data, adapterId) {
|
|
5257
|
+
if (!this.isModelAdmin(data.userId, data.channelId, adapterId)) {
|
|
5258
|
+
await data.respond(t("permission.denied"));
|
|
5259
|
+
return;
|
|
5260
|
+
}
|
|
5261
|
+
const name = this.resolveSlashTarget(data.channelId, adapterId);
|
|
5262
|
+
if (!name) {
|
|
5263
|
+
await data.respond(t("classic.no_agent"));
|
|
5264
|
+
return;
|
|
5265
|
+
}
|
|
5266
|
+
const requested = (typeof data.options?.level === "string" ? data.options.level.trim() : "")
|
|
5267
|
+
|| (data.text?.trim() ?? "");
|
|
5268
|
+
if (requested) {
|
|
5269
|
+
await data.respond(await this.applyEffort(name, requested));
|
|
5270
|
+
return;
|
|
5271
|
+
}
|
|
5272
|
+
const levels = this.effortLevelsFor(name);
|
|
5273
|
+
if (levels.length === 0) {
|
|
5274
|
+
await data.respond(`❌ ${this.backendNameForInstance(name)} has no reasoning-effort setting.`);
|
|
5275
|
+
return;
|
|
5276
|
+
}
|
|
5277
|
+
if (!data.respondChoices) {
|
|
5278
|
+
await data.respond(`Usage: /effort <${levels.join("|")}>`);
|
|
5279
|
+
return;
|
|
5280
|
+
}
|
|
5281
|
+
const current = this.resolveInstanceEffort(name).effort;
|
|
5282
|
+
const nonce = randomBytes(6).toString("hex");
|
|
5283
|
+
const choices = levels.map(l => ({
|
|
5284
|
+
id: `${EFFORT_SELECT_CALLBACK_PREFIX}${nonce}:${l}`,
|
|
5285
|
+
label: this.effortChoiceLabel(l, current),
|
|
5286
|
+
}));
|
|
5287
|
+
const timer = setTimeout(() => this.pendingEffortSelects.delete(nonce), CLASSIC_BACKEND_SELECTION_TIMEOUT_MS);
|
|
5288
|
+
timer.unref?.();
|
|
5289
|
+
this.pendingEffortSelects.set(nonce, { instanceName: name, userId: data.userId, channelId: data.channelId, timer, respond: data.respond });
|
|
5290
|
+
try {
|
|
5291
|
+
await data.respondChoices(`${this.effortMenuHeader(name)}\nSelect a new effort level:`, choices);
|
|
5292
|
+
}
|
|
5293
|
+
catch (err) {
|
|
5294
|
+
this.pendingEffortSelects.delete(nonce);
|
|
5295
|
+
clearTimeout(timer);
|
|
5296
|
+
this.logger.warn({ err, instanceName: name }, "effort menu failed");
|
|
5297
|
+
await data.respond(`Usage: /effort <${levels.join("|")}>`);
|
|
5298
|
+
}
|
|
5299
|
+
}
|
|
5300
|
+
/** TG inline-keyboard effort menu. Returns null on success, else a fallback string. */
|
|
5301
|
+
async promptEffortMenu(instanceName, userId, channelId, adapter, chatId, threadId) {
|
|
5302
|
+
const levels = this.effortLevelsFor(instanceName);
|
|
5303
|
+
if (levels.length === 0) {
|
|
5304
|
+
return `❌ ${this.backendNameForInstance(instanceName)} has no reasoning-effort setting.`;
|
|
5305
|
+
}
|
|
5306
|
+
const current = this.resolveInstanceEffort(instanceName).effort;
|
|
5307
|
+
const nonce = randomBytes(6).toString("hex");
|
|
5308
|
+
const choices = levels.map(l => ({
|
|
5309
|
+
id: `${EFFORT_SELECT_CALLBACK_PREFIX}${nonce}:${l}`,
|
|
5310
|
+
label: this.effortChoiceLabel(l, current),
|
|
5311
|
+
}));
|
|
5312
|
+
const respond = async (text) => {
|
|
5313
|
+
await adapter.sendText(chatId, text, { threadId });
|
|
5314
|
+
return undefined;
|
|
5315
|
+
};
|
|
5316
|
+
const timer = setTimeout(() => {
|
|
5317
|
+
const p = this.pendingEffortSelects.get(nonce);
|
|
5318
|
+
if (p) {
|
|
5319
|
+
this.pendingEffortSelects.delete(nonce);
|
|
5320
|
+
p.respond("⏰ Effort selection expired.").catch(() => { });
|
|
5321
|
+
}
|
|
5322
|
+
}, CLASSIC_BACKEND_SELECTION_TIMEOUT_MS);
|
|
5323
|
+
timer.unref?.();
|
|
5324
|
+
this.pendingEffortSelects.set(nonce, { instanceName, userId, channelId, timer, respond, adapter, adapterChatId: chatId, adapterThreadId: threadId });
|
|
5325
|
+
try {
|
|
5326
|
+
const menuMessageId = await adapter.promptUser(chatId, `${this.effortMenuHeader(instanceName)}\nSelect a new effort level:`, choices, { threadId });
|
|
5327
|
+
const pending = this.pendingEffortSelects.get(nonce);
|
|
5328
|
+
if (pending)
|
|
5329
|
+
pending.menuMessageId = menuMessageId;
|
|
5330
|
+
return null;
|
|
5331
|
+
}
|
|
5332
|
+
catch (err) {
|
|
5333
|
+
this.pendingEffortSelects.delete(nonce);
|
|
5334
|
+
clearTimeout(timer);
|
|
5335
|
+
this.logger.warn({ err, instanceName }, "TG effort menu failed");
|
|
5336
|
+
return `Usage: /effort <${levels.join("|")}>`;
|
|
5337
|
+
}
|
|
5338
|
+
}
|
|
5339
|
+
/** Consume an `/effort` selection callback. Mirrors handleModelSelection. */
|
|
5340
|
+
async handleEffortSelection(data) {
|
|
5341
|
+
if (!data.callbackData.startsWith(EFFORT_SELECT_CALLBACK_PREFIX))
|
|
5342
|
+
return false;
|
|
5343
|
+
const match = data.callbackData.match(/^effort-select:([0-9a-f]+):(.+)$/);
|
|
5344
|
+
if (!match)
|
|
5345
|
+
return true;
|
|
5346
|
+
const pending = this.pendingEffortSelects.get(match[1]);
|
|
5347
|
+
if (!pending)
|
|
5348
|
+
return true;
|
|
5349
|
+
if (data.userId && data.userId !== pending.userId)
|
|
5350
|
+
return true;
|
|
5351
|
+
const cbChannel = data.threadId ?? data.chatId;
|
|
5352
|
+
if (cbChannel !== pending.channelId && data.chatId !== pending.channelId)
|
|
5353
|
+
return true;
|
|
5354
|
+
this.pendingEffortSelects.delete(match[1]);
|
|
5355
|
+
clearTimeout(pending.timer);
|
|
5356
|
+
const level = match[2];
|
|
5357
|
+
const progressText = `⏳ Setting ${pending.instanceName} effort to \`${level}\`…`;
|
|
5358
|
+
let progressMsgId;
|
|
5359
|
+
if (pending.adapter && pending.adapterChatId) {
|
|
5360
|
+
const menuMessageId = pending.menuMessageId ?? data.messageId;
|
|
5361
|
+
if (menuMessageId && pending.adapter.editMessageRemoveButtons) {
|
|
5362
|
+
try {
|
|
5363
|
+
await pending.adapter.editMessageRemoveButtons(pending.adapterChatId, menuMessageId, progressText, pending.adapterThreadId);
|
|
5364
|
+
progressMsgId = menuMessageId;
|
|
5365
|
+
}
|
|
5366
|
+
catch { /* fall back to a new message */ }
|
|
5367
|
+
}
|
|
5368
|
+
if (!progressMsgId) {
|
|
5369
|
+
try {
|
|
5370
|
+
const sent = await pending.adapter.sendText(pending.adapterChatId, progressText, { threadId: pending.adapterThreadId });
|
|
5371
|
+
progressMsgId = sent.messageId;
|
|
5372
|
+
}
|
|
5373
|
+
catch { /* non-fatal */ }
|
|
5374
|
+
}
|
|
5375
|
+
}
|
|
5376
|
+
else {
|
|
5377
|
+
await pending.respond(progressText).catch(() => { });
|
|
5378
|
+
}
|
|
5379
|
+
// Background-applied and guarded for the same reason as the model path: a
|
|
5380
|
+
// restart backend respawns the instance here, and an unguarded rejection
|
|
5381
|
+
// from a menu click must not take the fleet down.
|
|
5382
|
+
void (async () => {
|
|
5383
|
+
let result;
|
|
5384
|
+
try {
|
|
5385
|
+
result = await this.applyEffort(pending.instanceName, level);
|
|
5386
|
+
}
|
|
5387
|
+
catch (err) {
|
|
5388
|
+
this.logger.error({ err, instance: pending.instanceName, level }, "Effort switch failed");
|
|
5389
|
+
result = `Effort switch to \`${level}\` failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
5390
|
+
}
|
|
5391
|
+
if (pending.adapter && pending.adapterChatId) {
|
|
5392
|
+
if (progressMsgId) {
|
|
5393
|
+
pending.adapter.editMessage(pending.adapterChatId, progressMsgId, result, pending.adapterThreadId).catch(() => {
|
|
5394
|
+
pending.adapter.sendText(pending.adapterChatId, result, { threadId: pending.adapterThreadId }).catch(() => { });
|
|
5395
|
+
});
|
|
5396
|
+
}
|
|
5397
|
+
else {
|
|
5398
|
+
pending.adapter.sendText(pending.adapterChatId, result, { threadId: pending.adapterThreadId }).catch(() => { });
|
|
5399
|
+
}
|
|
5400
|
+
}
|
|
5401
|
+
else {
|
|
5402
|
+
await pending.respond(result).catch(() => { });
|
|
5403
|
+
}
|
|
5404
|
+
})();
|
|
5405
|
+
return true;
|
|
5406
|
+
}
|
|
5230
5407
|
async handleModelSlash(data, adapterId) {
|
|
5231
5408
|
if (!this.isModelAdmin(data.userId, data.channelId, adapterId)) {
|
|
5232
5409
|
await data.respond(t("permission.denied"));
|
|
@@ -5393,6 +5570,96 @@ When users create specialized instances, suggest these configurations:
|
|
|
5393
5570
|
return true;
|
|
5394
5571
|
}
|
|
5395
5572
|
/** Apply a model to an instance: runtime paste (claude-code) or persist + restart (others). */
|
|
5573
|
+
/** AgEnD's canonical effort ladder, low → max. Backends expose a subset. */
|
|
5574
|
+
static EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
5575
|
+
/** Effort levels this instance's backend actually accepts (empty = unsupported). */
|
|
5576
|
+
effortLevelsFor(instanceName) {
|
|
5577
|
+
try {
|
|
5578
|
+
const backend = createBackend(this.backendNameForInstance(instanceName), this.getInstanceDir(instanceName));
|
|
5579
|
+
if ((backend.getEffortStrategy?.() ?? "unsupported") === "unsupported")
|
|
5580
|
+
return [];
|
|
5581
|
+
return backend.getEffortLevels?.() ?? [];
|
|
5582
|
+
}
|
|
5583
|
+
catch {
|
|
5584
|
+
return [];
|
|
5585
|
+
}
|
|
5586
|
+
}
|
|
5587
|
+
/** Configured effort for an instance: per-instance, else fleet default, else none. */
|
|
5588
|
+
resolveInstanceEffort(instanceName) {
|
|
5589
|
+
const own = this.fleetConfig?.instances[instanceName]?.effort;
|
|
5590
|
+
if (own)
|
|
5591
|
+
return { effort: own, source: "instance" };
|
|
5592
|
+
const fallback = this.fleetConfig?.defaults?.effort;
|
|
5593
|
+
if (fallback)
|
|
5594
|
+
return { effort: fallback, source: "fleet-default" };
|
|
5595
|
+
return { effort: null, source: "unset" };
|
|
5596
|
+
}
|
|
5597
|
+
/**
|
|
5598
|
+
* Clamp a canonical level to the nearest one this backend supports.
|
|
5599
|
+
*
|
|
5600
|
+
* Clamping DOWN the ladder, never up: asking for `max` on a CLI that stops at
|
|
5601
|
+
* `high` should get high, not silently fall to low. The caller reports the
|
|
5602
|
+
* clamp — a user who asks for max and quietly receives high has been told the
|
|
5603
|
+
* request succeeded when it did not.
|
|
5604
|
+
*/
|
|
5605
|
+
static clampEffort(level, supported) {
|
|
5606
|
+
if (supported.includes(level))
|
|
5607
|
+
return level;
|
|
5608
|
+
const ladder = FleetManager.EFFORT_LEVELS;
|
|
5609
|
+
const wanted = ladder.indexOf(level);
|
|
5610
|
+
if (wanted < 0)
|
|
5611
|
+
return null;
|
|
5612
|
+
for (let i = wanted - 1; i >= 0; i--) {
|
|
5613
|
+
if (supported.includes(ladder[i]))
|
|
5614
|
+
return ladder[i];
|
|
5615
|
+
}
|
|
5616
|
+
return supported[0] ?? null;
|
|
5617
|
+
}
|
|
5618
|
+
/**
|
|
5619
|
+
* Apply a reasoning-effort level, mirroring applyModel's shape.
|
|
5620
|
+
*
|
|
5621
|
+
* runtime backends take `/effort <level>` in the pane and keep working;
|
|
5622
|
+
* restart backends only read it at launch, so it is persisted and the
|
|
5623
|
+
* instance respawns.
|
|
5624
|
+
*/
|
|
5625
|
+
async applyEffort(instanceName, requested) {
|
|
5626
|
+
const level = requested.trim().toLowerCase();
|
|
5627
|
+
const backendName = this.backendNameForInstance(instanceName);
|
|
5628
|
+
let strategy = "unsupported";
|
|
5629
|
+
let supported = [];
|
|
5630
|
+
try {
|
|
5631
|
+
const backend = createBackend(backendName, this.getInstanceDir(instanceName));
|
|
5632
|
+
strategy = backend.getEffortStrategy?.() ?? "unsupported";
|
|
5633
|
+
supported = backend.getEffortLevels?.() ?? [];
|
|
5634
|
+
}
|
|
5635
|
+
catch { /* treated as unsupported below */ }
|
|
5636
|
+
if (strategy === "unsupported" || supported.length === 0) {
|
|
5637
|
+
return `❌ ${backendName} has no reasoning-effort setting.`;
|
|
5638
|
+
}
|
|
5639
|
+
if (!FleetManager.EFFORT_LEVELS.includes(level)) {
|
|
5640
|
+
return `❌ Unknown effort level \`${level}\`. Use: ${FleetManager.EFFORT_LEVELS.join(", ")}.`;
|
|
5641
|
+
}
|
|
5642
|
+
const applied = FleetManager.clampEffort(level, supported);
|
|
5643
|
+
if (!applied)
|
|
5644
|
+
return `❌ ${backendName} accepts none of the canonical effort levels.`;
|
|
5645
|
+
const warn = applied === level
|
|
5646
|
+
? ""
|
|
5647
|
+
: `⚠️ Clamped to \`${applied}\` (\`${level}\` not supported by ${backendName}).\n`;
|
|
5648
|
+
// Persist either way: a runtime switch must survive the next respawn too,
|
|
5649
|
+
// or the instance silently reverts on restart.
|
|
5650
|
+
if (this.fleetConfig?.instances[instanceName]) {
|
|
5651
|
+
this.fleetConfig.instances[instanceName].effort = applied;
|
|
5652
|
+
this.saveFleetConfig();
|
|
5653
|
+
}
|
|
5654
|
+
if (strategy === "runtime") {
|
|
5655
|
+
if (!this.instanceIpcClients.get(instanceName))
|
|
5656
|
+
return `${warn}❌ ${instanceName} is not running.`;
|
|
5657
|
+
this.pasteRawToClassicInstance(instanceName, `/effort ${applied}`);
|
|
5658
|
+
return `${warn}✅ Set ${instanceName} effort to \`${applied}\` (runtime).`;
|
|
5659
|
+
}
|
|
5660
|
+
await this.restartSingleInstance(instanceName);
|
|
5661
|
+
return `${warn}✅ Set ${instanceName} effort to \`${applied}\` and restarted.`;
|
|
5662
|
+
}
|
|
5396
5663
|
async applyModel(instanceName, model) {
|
|
5397
5664
|
const backendName = this.backendNameForInstance(instanceName);
|
|
5398
5665
|
let strategy = "restart";
|