@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.
Files changed (39) hide show
  1. package/dist/backend/antigravity.d.ts +2 -0
  2. package/dist/backend/antigravity.js +5 -0
  3. package/dist/backend/antigravity.js.map +1 -1
  4. package/dist/backend/claude-code.d.ts +2 -0
  5. package/dist/backend/claude-code.js +4 -0
  6. package/dist/backend/claude-code.js.map +1 -1
  7. package/dist/backend/codex.d.ts +2 -0
  8. package/dist/backend/codex.js +5 -0
  9. package/dist/backend/codex.js.map +1 -1
  10. package/dist/backend/grok.d.ts +2 -0
  11. package/dist/backend/grok.js +6 -0
  12. package/dist/backend/grok.js.map +1 -1
  13. package/dist/backend/kiro.d.ts +2 -0
  14. package/dist/backend/kiro.js +8 -1
  15. package/dist/backend/kiro.js.map +1 -1
  16. package/dist/backend/types.d.ts +24 -0
  17. package/dist/backend/types.js +12 -0
  18. package/dist/backend/types.js.map +1 -1
  19. package/dist/channel/adapters/discord.js +12 -0
  20. package/dist/channel/adapters/discord.js.map +1 -1
  21. package/dist/config-validator.js +6 -0
  22. package/dist/config-validator.js.map +1 -1
  23. package/dist/daemon.js +1 -0
  24. package/dist/daemon.js.map +1 -1
  25. package/dist/fleet-context.d.ts +4 -0
  26. package/dist/fleet-manager.d.ts +37 -0
  27. package/dist/fleet-manager.js +267 -0
  28. package/dist/fleet-manager.js.map +1 -1
  29. package/dist/locale.js +1 -0
  30. package/dist/locale.js.map +1 -1
  31. package/dist/topic-commands.js +24 -0
  32. package/dist/topic-commands.js.map +1 -1
  33. package/dist/types.d.ts +2 -0
  34. package/dist/usage/format-rich.js +6 -1
  35. package/dist/usage/format-rich.js.map +1 -1
  36. package/dist/usage/providers.d.ts +16 -0
  37. package/dist/usage/providers.js +67 -8
  38. package/dist/usage/providers.js.map +1 -1
  39. package/package.json +1 -1
@@ -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";