@songsid/agend 2.1.4-beta.8 → 2.1.4-beta.9
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/channel/adapters/discord.js +7 -0
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/config-validator.js +3 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +50 -0
- package/dist/daemon.js +173 -34
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-manager.d.ts +22 -0
- package/dist/fleet-manager.js +162 -25
- package/dist/fleet-manager.js.map +1 -1
- package/dist/locale.js +10 -0
- package/dist/locale.js.map +1 -1
- package/dist/tool-progress.d.ts +40 -0
- package/dist/tool-progress.js +289 -0
- package/dist/tool-progress.js.map +1 -0
- package/dist/topic-commands.d.ts +15 -0
- package/dist/topic-commands.js +56 -0
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-monitor.d.ts +15 -2
- package/dist/transcript-monitor.js +63 -17
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +106 -0
- package/dist/transcript-sources.js +428 -0
- package/dist/transcript-sources.js.map +1 -0
- package/dist/types.d.ts +6 -0
- package/dist/workflow-templates/default.md +2 -1
- package/package.json +1 -1
package/dist/fleet-manager.d.ts
CHANGED
|
@@ -106,6 +106,8 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
106
106
|
readonly replyDeduper: ReplyDeduper;
|
|
107
107
|
/** instanceName → what it is doing right now, when the backend can tell us. */
|
|
108
108
|
private instanceActivity;
|
|
109
|
+
/** instanceName → this turn's tool list (multi-line), for the bubble. */
|
|
110
|
+
private instanceProgress;
|
|
109
111
|
/** instanceName → tail of deliveries waiting for its IPC to come back. */
|
|
110
112
|
private ipcWaitTails;
|
|
111
113
|
/** instanceName → restart currently executing; concurrent callers join it. */
|
|
@@ -644,6 +646,26 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
644
646
|
* never used to decide anything — purely what the user is shown.
|
|
645
647
|
*/
|
|
646
648
|
private cacheInstanceActivity;
|
|
649
|
+
/**
|
|
650
|
+
* Cache the turn's tool-progress list and push it into the instance's live
|
|
651
|
+
* bubble, coalesced so a burst of tool events cannot flood the Bot API
|
|
652
|
+
* (Telegram's flood limit is per-chat across ALL forum topics, so edits are
|
|
653
|
+
* rate-limited per bubble AND ride behind the daemon-side 3s coalescer).
|
|
654
|
+
*/
|
|
655
|
+
private cacheInstanceProgress;
|
|
656
|
+
/** At most one progress-driven edit per bubble per TOOL_PROGRESS_EDIT_MIN_MS. */
|
|
657
|
+
private scheduleProgressEdit;
|
|
658
|
+
/**
|
|
659
|
+
* The ONE composer for the bubble text. Both writers — the elapsed-time
|
|
660
|
+
* ticker and the tool-progress push — go through here; two independent
|
|
661
|
+
* renderers editing the same message is how the progress list used to get
|
|
662
|
+
* wiped by the next elapsed tick (#528 trap 2).
|
|
663
|
+
*/
|
|
664
|
+
private composeBubbleText;
|
|
665
|
+
/** Pure composition of header + tool list, exposed for tests. */
|
|
666
|
+
static bubbleText(elapsedMs: number, activity: string | undefined, minElapsedMs: number, progress: string | undefined): string;
|
|
667
|
+
/** Recompose and edit the bubble in place; skips when nothing changed. */
|
|
668
|
+
private refreshBubble;
|
|
647
669
|
/**
|
|
648
670
|
* Refresh the button's text in place while the instance keeps working.
|
|
649
671
|
*
|
package/dist/fleet-manager.js
CHANGED
|
@@ -135,6 +135,8 @@ const CANCEL_BTN_LEDGER_FILE = "cancel-buttons.json";
|
|
|
135
135
|
* exactly one progress message per turn, and it is the cancel button itself.
|
|
136
136
|
*/
|
|
137
137
|
const PROGRESS_UPDATE_INTERVAL_MS = 60_000;
|
|
138
|
+
/** Floor between tool-progress-driven bubble edits (Telegram flood safety). */
|
|
139
|
+
const TOOL_PROGRESS_EDIT_MIN_MS = 4_000;
|
|
138
140
|
/** Elapsed time is only shown once work has clearly outlasted a quick answer. */
|
|
139
141
|
/**
|
|
140
142
|
* Default delay before the button starts showing elapsed time. Configurable via
|
|
@@ -253,6 +255,8 @@ export class FleetManager {
|
|
|
253
255
|
replyDeduper = new ReplyDeduper();
|
|
254
256
|
/** instanceName → what it is doing right now, when the backend can tell us. */
|
|
255
257
|
instanceActivity = new Map();
|
|
258
|
+
/** instanceName → this turn's tool list (multi-line), for the bubble. */
|
|
259
|
+
instanceProgress = new Map();
|
|
256
260
|
/** instanceName → tail of deliveries waiting for its IPC to come back. */
|
|
257
261
|
ipcWaitTails = new Map();
|
|
258
262
|
/** instanceName → restart currently executing; concurrent callers join it. */
|
|
@@ -2235,6 +2239,26 @@ export class FleetManager {
|
|
|
2235
2239
|
const result = await this.topicCommands.sendCompact(name);
|
|
2236
2240
|
await data.respond(result);
|
|
2237
2241
|
}
|
|
2242
|
+
else if (data.command === "steer") {
|
|
2243
|
+
const name = this.resolveSlashTarget(data.channelId, adapterId);
|
|
2244
|
+
if (!name) {
|
|
2245
|
+
await data.respond(t("classic.no_agent"));
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
const steerText = String(data.options?.message ?? "").trim();
|
|
2249
|
+
if (!steerText) {
|
|
2250
|
+
await data.respond(t("steer.usage"));
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
// chat_id/message_id stay empty: a slash interaction has no channel
|
|
2254
|
+
// message to react to, and an empty chat_id keeps updateLastChat from
|
|
2255
|
+
// rerouting the instance's replies to the slash context.
|
|
2256
|
+
const result = this.topicCommands.sendSteer(name, steerText, {
|
|
2257
|
+
chatId: "", messageId: "", username: data.username ?? "user",
|
|
2258
|
+
userId: data.userId ?? "", threadId: undefined, adapterId, source: "discord",
|
|
2259
|
+
});
|
|
2260
|
+
await data.respond(result);
|
|
2261
|
+
}
|
|
2238
2262
|
else if (data.command === "clear") {
|
|
2239
2263
|
await this.handleClearSlash(data, adapterId);
|
|
2240
2264
|
}
|
|
@@ -2359,6 +2383,26 @@ export class FleetManager {
|
|
|
2359
2383
|
const result = await this.topicCommands.sendCompact(name);
|
|
2360
2384
|
await data.respond(result);
|
|
2361
2385
|
}
|
|
2386
|
+
else if (data.command === "steer") {
|
|
2387
|
+
const name = this.resolveSlashTarget(data.channelId, adapterId);
|
|
2388
|
+
if (!name) {
|
|
2389
|
+
await data.respond(t("classic.no_agent"));
|
|
2390
|
+
return;
|
|
2391
|
+
}
|
|
2392
|
+
const steerText = String(data.options?.message ?? "").trim();
|
|
2393
|
+
if (!steerText) {
|
|
2394
|
+
await data.respond(t("steer.usage"));
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
2397
|
+
// chat_id/message_id stay empty: a slash interaction has no channel
|
|
2398
|
+
// message to react to, and an empty chat_id keeps updateLastChat from
|
|
2399
|
+
// rerouting the instance's replies to the slash context.
|
|
2400
|
+
const result = this.topicCommands.sendSteer(name, steerText, {
|
|
2401
|
+
chatId: "", messageId: "", username: data.username ?? "user",
|
|
2402
|
+
userId: data.userId ?? "", threadId: undefined, adapterId, source: "discord",
|
|
2403
|
+
});
|
|
2404
|
+
await data.respond(result);
|
|
2405
|
+
}
|
|
2362
2406
|
}, this.logger, "adapter.slash_command"));
|
|
2363
2407
|
await this.topicCommands.registerBotCommands().catch(e => this.logger.warn({ err: e }, "registerBotCommands failed (non-fatal)"));
|
|
2364
2408
|
// Background-probe each backend's CLI env (version/models) → cli-env cache.
|
|
@@ -2641,6 +2685,26 @@ export class FleetManager {
|
|
|
2641
2685
|
const result = await this.topicCommands.sendCompact(name);
|
|
2642
2686
|
await data.respond(result);
|
|
2643
2687
|
}
|
|
2688
|
+
else if (data.command === "steer") {
|
|
2689
|
+
const name = this.resolveSlashTarget(data.channelId, adapterId);
|
|
2690
|
+
if (!name) {
|
|
2691
|
+
await data.respond(t("classic.no_agent"));
|
|
2692
|
+
return;
|
|
2693
|
+
}
|
|
2694
|
+
const steerText = String(data.options?.message ?? "").trim();
|
|
2695
|
+
if (!steerText) {
|
|
2696
|
+
await data.respond(t("steer.usage"));
|
|
2697
|
+
return;
|
|
2698
|
+
}
|
|
2699
|
+
// chat_id/message_id stay empty: a slash interaction has no channel
|
|
2700
|
+
// message to react to, and an empty chat_id keeps updateLastChat from
|
|
2701
|
+
// rerouting the instance's replies to the slash context.
|
|
2702
|
+
const result = this.topicCommands.sendSteer(name, steerText, {
|
|
2703
|
+
chatId: "", messageId: "", username: data.username ?? "user",
|
|
2704
|
+
userId: data.userId ?? "", threadId: undefined, adapterId, source: "discord",
|
|
2705
|
+
});
|
|
2706
|
+
await data.respond(result);
|
|
2707
|
+
}
|
|
2644
2708
|
}, this.logger, `adapter[${adapterId}].slash_command`));
|
|
2645
2709
|
await adapter.start();
|
|
2646
2710
|
if (channelConfig.group_id) {
|
|
@@ -2758,6 +2822,9 @@ export class FleetManager {
|
|
|
2758
2822
|
else if (msg.type === "instance_activity") {
|
|
2759
2823
|
this.cacheInstanceActivity(name, msg.activity);
|
|
2760
2824
|
}
|
|
2825
|
+
else if (msg.type === "instance_progress") {
|
|
2826
|
+
this.cacheInstanceProgress(name, msg.progress || null);
|
|
2827
|
+
}
|
|
2761
2828
|
else if (msg.type === "instance_state" || msg.type === "instance_state_response") {
|
|
2762
2829
|
this.cacheInstanceExecutionState(name, msg);
|
|
2763
2830
|
if (msg.type === "instance_state_response") {
|
|
@@ -3205,6 +3272,24 @@ export class FleetManager {
|
|
|
3205
3272
|
await msgAdapter?.sendText(chatId, result);
|
|
3206
3273
|
return;
|
|
3207
3274
|
}
|
|
3275
|
+
// /steer — interject into the running turn. Not admin-gated: anyone who
|
|
3276
|
+
// can talk to this agent can send it a message; steer only changes when
|
|
3277
|
+
// it lands, and it keeps the full [user:] formatting (unlike /raw).
|
|
3278
|
+
if (text === "/steer" || text.startsWith("/steer ") || text.startsWith("/steer@")) {
|
|
3279
|
+
const steerName = this.classicChannels.getInstanceByChannel(chatId, msg.adapterId);
|
|
3280
|
+
if (!steerName) {
|
|
3281
|
+
await msgAdapter?.sendText(chatId, t("classic.no_agent_start"));
|
|
3282
|
+
return;
|
|
3283
|
+
}
|
|
3284
|
+
const steerContent = text.replace(/^\/steer(@\S+)?/, "").trim();
|
|
3285
|
+
if (!steerContent) {
|
|
3286
|
+
await msgAdapter?.sendText(chatId, t("steer.usage"));
|
|
3287
|
+
return;
|
|
3288
|
+
}
|
|
3289
|
+
const result = this.topicCommands.sendSteer(steerName, steerContent, msg);
|
|
3290
|
+
await msgAdapter?.sendText(chatId, result);
|
|
3291
|
+
return;
|
|
3292
|
+
}
|
|
3208
3293
|
// Handle /clear command (admin only) — unlike /compact this starts a
|
|
3209
3294
|
// fresh conversation and intentionally discards the current history.
|
|
3210
3295
|
if (text === "/clear" || text.startsWith("/clear@")) {
|
|
@@ -5130,6 +5215,80 @@ export class FleetManager {
|
|
|
5130
5215
|
else
|
|
5131
5216
|
this.instanceActivity.delete(name);
|
|
5132
5217
|
}
|
|
5218
|
+
/**
|
|
5219
|
+
* Cache the turn's tool-progress list and push it into the instance's live
|
|
5220
|
+
* bubble, coalesced so a burst of tool events cannot flood the Bot API
|
|
5221
|
+
* (Telegram's flood limit is per-chat across ALL forum topics, so edits are
|
|
5222
|
+
* rate-limited per bubble AND ride behind the daemon-side 3s coalescer).
|
|
5223
|
+
*/
|
|
5224
|
+
cacheInstanceProgress(name, progress) {
|
|
5225
|
+
if (progress)
|
|
5226
|
+
this.instanceProgress.set(name, progress);
|
|
5227
|
+
else
|
|
5228
|
+
this.instanceProgress.delete(name);
|
|
5229
|
+
for (const entry of this.cancelButtons.values()) {
|
|
5230
|
+
if (entry.instanceName === name)
|
|
5231
|
+
this.scheduleProgressEdit(entry);
|
|
5232
|
+
}
|
|
5233
|
+
}
|
|
5234
|
+
/** At most one progress-driven edit per bubble per TOOL_PROGRESS_EDIT_MIN_MS. */
|
|
5235
|
+
scheduleProgressEdit(entry) {
|
|
5236
|
+
const since = Date.now() - (entry.lastProgressEditAt ?? 0);
|
|
5237
|
+
if (since >= TOOL_PROGRESS_EDIT_MIN_MS) {
|
|
5238
|
+
this.refreshBubble(entry);
|
|
5239
|
+
return;
|
|
5240
|
+
}
|
|
5241
|
+
if (entry.progressEditTimer)
|
|
5242
|
+
return; // trailing edit already scheduled
|
|
5243
|
+
entry.progressEditTimer = setTimeout(() => {
|
|
5244
|
+
entry.progressEditTimer = undefined;
|
|
5245
|
+
this.refreshBubble(entry);
|
|
5246
|
+
}, TOOL_PROGRESS_EDIT_MIN_MS - since);
|
|
5247
|
+
entry.progressEditTimer.unref?.();
|
|
5248
|
+
}
|
|
5249
|
+
/**
|
|
5250
|
+
* The ONE composer for the bubble text. Both writers — the elapsed-time
|
|
5251
|
+
* ticker and the tool-progress push — go through here; two independent
|
|
5252
|
+
* renderers editing the same message is how the progress list used to get
|
|
5253
|
+
* wiped by the next elapsed tick (#528 trap 2).
|
|
5254
|
+
*/
|
|
5255
|
+
composeBubbleText(entry) {
|
|
5256
|
+
return FleetManager.bubbleText(Date.now() - (entry.startedAt ?? Date.now()), this.instanceActivity.get(entry.instanceName), this.progressMinElapsedMs(), this.instanceProgress.get(entry.instanceName));
|
|
5257
|
+
}
|
|
5258
|
+
/** Pure composition of header + tool list, exposed for tests. */
|
|
5259
|
+
static bubbleText(elapsedMs, activity, minElapsedMs, progress) {
|
|
5260
|
+
const header = FleetManager.progressText(elapsedMs,
|
|
5261
|
+
// The single-line activity detail is redundant once a tool list exists.
|
|
5262
|
+
progress ? undefined : activity, minElapsedMs);
|
|
5263
|
+
return progress ? `${header}\n${progress}` : header;
|
|
5264
|
+
}
|
|
5265
|
+
/** Recompose and edit the bubble in place; skips when nothing changed. */
|
|
5266
|
+
refreshBubble(entry) {
|
|
5267
|
+
if (!this.cancelButtons.has(entry.messageId)) {
|
|
5268
|
+
clearInterval(entry.progressTimer);
|
|
5269
|
+
return;
|
|
5270
|
+
}
|
|
5271
|
+
const text = this.composeBubbleText(entry);
|
|
5272
|
+
if (text === entry.lastProgressText)
|
|
5273
|
+
return; // nothing changed — skip the API call
|
|
5274
|
+
const adapter = this.getAdapterForInstance(entry.instanceName) ?? this.adapter;
|
|
5275
|
+
if (!adapter?.editAlert)
|
|
5276
|
+
return;
|
|
5277
|
+
entry.lastProgressText = text;
|
|
5278
|
+
entry.lastProgressEditAt = Date.now();
|
|
5279
|
+
adapter.editAlert(entry.chatId, entry.messageId, {
|
|
5280
|
+
type: "cancel",
|
|
5281
|
+
instanceName: entry.instanceName,
|
|
5282
|
+
message: text,
|
|
5283
|
+
choices: [{ id: `cancel:${entry.instanceName}`, label: t("cancel.button") }],
|
|
5284
|
+
}, entry.threadId ? { threadId: entry.threadId } : undefined)
|
|
5285
|
+
.catch(err => {
|
|
5286
|
+
// A failed progress edit must never escalate: the button still works and
|
|
5287
|
+
// the next tick retries. Common causes are a deleted message or a
|
|
5288
|
+
// rate limit.
|
|
5289
|
+
this.logger.debug({ err, instanceName: entry.instanceName }, "Progress edit failed");
|
|
5290
|
+
});
|
|
5291
|
+
}
|
|
5133
5292
|
/**
|
|
5134
5293
|
* Refresh the button's text in place while the instance keeps working.
|
|
5135
5294
|
*
|
|
@@ -5147,31 +5306,7 @@ export class FleetManager {
|
|
|
5147
5306
|
return PROGRESS_MIN_ELAPSED_MS;
|
|
5148
5307
|
}
|
|
5149
5308
|
startProgressTicker(entry) {
|
|
5150
|
-
const tick = () =>
|
|
5151
|
-
if (!this.cancelButtons.has(entry.messageId)) {
|
|
5152
|
-
clearInterval(entry.progressTimer);
|
|
5153
|
-
return;
|
|
5154
|
-
}
|
|
5155
|
-
const text = FleetManager.progressText(Date.now() - (entry.startedAt ?? Date.now()), this.instanceActivity.get(entry.instanceName), this.progressMinElapsedMs());
|
|
5156
|
-
if (text === entry.lastProgressText)
|
|
5157
|
-
return; // nothing changed — skip the API call
|
|
5158
|
-
const adapter = this.getAdapterForInstance(entry.instanceName) ?? this.adapter;
|
|
5159
|
-
if (!adapter?.editAlert)
|
|
5160
|
-
return;
|
|
5161
|
-
entry.lastProgressText = text;
|
|
5162
|
-
adapter.editAlert(entry.chatId, entry.messageId, {
|
|
5163
|
-
type: "cancel",
|
|
5164
|
-
instanceName: entry.instanceName,
|
|
5165
|
-
message: text,
|
|
5166
|
-
choices: [{ id: `cancel:${entry.instanceName}`, label: t("cancel.button") }],
|
|
5167
|
-
}, entry.threadId ? { threadId: entry.threadId } : undefined)
|
|
5168
|
-
.catch(err => {
|
|
5169
|
-
// A failed progress edit must never escalate: the button still works and
|
|
5170
|
-
// the next tick retries. Common causes are a deleted message or a
|
|
5171
|
-
// rate limit.
|
|
5172
|
-
this.logger.debug({ err, instanceName: entry.instanceName }, "Progress edit failed");
|
|
5173
|
-
});
|
|
5174
|
-
};
|
|
5309
|
+
const tick = () => this.refreshBubble(entry);
|
|
5175
5310
|
entry.progressTimer = setInterval(tick, PROGRESS_UPDATE_INTERVAL_MS);
|
|
5176
5311
|
entry.progressTimer.unref?.();
|
|
5177
5312
|
// One extra tick right when the threshold passes, so a 30s threshold shows
|
|
@@ -5239,6 +5374,8 @@ export class FleetManager {
|
|
|
5239
5374
|
clearInterval(entry.idleCheckTimer);
|
|
5240
5375
|
if (entry.progressTimer)
|
|
5241
5376
|
clearInterval(entry.progressTimer);
|
|
5377
|
+
if (entry.progressEditTimer)
|
|
5378
|
+
clearTimeout(entry.progressEditTimer);
|
|
5242
5379
|
if (entry.replyGraceTimer)
|
|
5243
5380
|
clearTimeout(entry.replyGraceTimer);
|
|
5244
5381
|
this.cancelButtons.delete(entry.messageId);
|