@agentprojectcontext/apx 1.33.0 → 1.33.1
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/skills/apc-context/SKILL.md +2 -5
- package/src/core/apc/parser.js +1 -1
- package/src/core/apc/scaffold.js +3 -1
- package/src/core/apc/skill-sync.js +3 -1
- package/src/core/engines/gemini.js +28 -11
- package/src/core/engines/index.js +11 -1
- package/src/host/daemon/api/engines.js +31 -1
- package/src/host/daemon/plugins/telegram/dispatch.js +573 -0
- package/src/host/daemon/plugins/telegram/helpers.js +130 -0
- package/src/host/daemon/plugins/telegram/index.js +19 -682
- package/src/interfaces/web/dist/assets/index-Aaiw8BZN.css +1 -0
- package/src/interfaces/web/dist/assets/{index-DWsE_8Nz.js → index-DPqtjDjh.js} +18 -18
- package/src/interfaces/web/dist/assets/{index-DWsE_8Nz.js.map → index-DPqtjDjh.js.map} +1 -1
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/src/components/ModelCombobox.tsx +42 -7
- package/src/interfaces/web/dist/assets/index-7dVT2O1S.css +0 -1
|
@@ -50,124 +50,23 @@ import * as askFlow from "./ask.js";
|
|
|
50
50
|
// API_BASE re-imported from ./media.js below
|
|
51
51
|
const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
52
52
|
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
? `\nMaster agent for this channel: **${routeToAgent}**. Prefer ` +
|
|
69
|
-
`delegating substantive work to that agent via call_agent({ project: ` +
|
|
70
|
-
`"${target?.name || target?.path || ""}", agent: "${routeToAgent}", ` +
|
|
71
|
-
"prompt: <user message> }) rather than answering yourself, unless " +
|
|
72
|
-
"the message is small-talk or a quick factual reply."
|
|
73
|
-
: "";
|
|
74
|
-
return {
|
|
75
|
-
channelName,
|
|
76
|
-
author,
|
|
77
|
-
chatId,
|
|
78
|
-
projectBlock,
|
|
79
|
-
routeBlock,
|
|
80
|
-
// Also expose raw fields for any future surface / log that wants them.
|
|
81
|
-
...(target ? {
|
|
82
|
-
projectId: String(target.id),
|
|
83
|
-
projectName: target.name || "",
|
|
84
|
-
projectPath: target.path || "",
|
|
85
|
-
} : {}),
|
|
86
|
-
...(routeToAgent ? { routeToAgent } : {}),
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Media sending helpers moved to ./media.js.
|
|
53
|
+
// All non-class-bound helpers live in ./helpers.js so the file stays
|
|
54
|
+
// focused on the poller class + dispatch wiring.
|
|
55
|
+
import {
|
|
56
|
+
buildTelegramMeta,
|
|
57
|
+
loadState,
|
|
58
|
+
saveState,
|
|
59
|
+
resolveBotToken,
|
|
60
|
+
resolveChatId,
|
|
61
|
+
tokenSource,
|
|
62
|
+
resolveChannels,
|
|
63
|
+
sleep,
|
|
64
|
+
} from "./helpers.js";
|
|
65
|
+
import { handleUpdate } from "./dispatch.js";
|
|
66
|
+
|
|
67
|
+
// ---------- media sending helpers (re-exports) ------------------------------
|
|
91
68
|
import { sendPhoto, sendVoice, sendDocument, sendAudio, downloadTelegramFile, API_BASE } from "./media.js";
|
|
92
69
|
export { sendPhoto, sendVoice, sendDocument, sendAudio };
|
|
93
|
-
function loadState() {
|
|
94
|
-
if (!fs.existsSync(TELEGRAM_STATE_PATH)) return { channels: {} };
|
|
95
|
-
try {
|
|
96
|
-
const raw = JSON.parse(fs.readFileSync(TELEGRAM_STATE_PATH, "utf8"));
|
|
97
|
-
return { channels: raw.channels || {}, _legacy_offset: raw.offset || 0 };
|
|
98
|
-
} catch {
|
|
99
|
-
return { channels: {} };
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function saveState(state) {
|
|
104
|
-
fs.writeFileSync(
|
|
105
|
-
TELEGRAM_STATE_PATH,
|
|
106
|
-
JSON.stringify({ ...state, updated_at: nowIso() }, null, 2) + "\n"
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// ---------- env-fallback helpers --------------------------------------------
|
|
111
|
-
|
|
112
|
-
function resolveBotToken(channel) {
|
|
113
|
-
return (
|
|
114
|
-
channel.bot_token ||
|
|
115
|
-
process.env.BOT_TELEGRAM_TOKEN ||
|
|
116
|
-
process.env.TELEGRAM_BOT_TOKEN ||
|
|
117
|
-
""
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function resolveChatId(channel) {
|
|
122
|
-
return (
|
|
123
|
-
channel.chat_id ||
|
|
124
|
-
process.env.TELEGRAM_CHAT_ID ||
|
|
125
|
-
process.env.BOT_TELEGRAM_CHAT_ID ||
|
|
126
|
-
""
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function tokenSource(channel) {
|
|
131
|
-
if (channel.bot_token) return "config";
|
|
132
|
-
if (process.env.BOT_TELEGRAM_TOKEN) return "env:BOT_TELEGRAM_TOKEN";
|
|
133
|
-
if (process.env.TELEGRAM_BOT_TOKEN) return "env:TELEGRAM_BOT_TOKEN";
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// ---------- channel-list resolution -----------------------------------------
|
|
138
|
-
|
|
139
|
-
function resolveChannels(globalConfig) {
|
|
140
|
-
const tg = globalConfig.telegram || {};
|
|
141
|
-
if (Array.isArray(tg.channels) && tg.channels.length > 0) {
|
|
142
|
-
return tg.channels.map((c, i) => ({
|
|
143
|
-
name: c.name || `channel-${i + 1}`,
|
|
144
|
-
bot_token: c.bot_token || "",
|
|
145
|
-
chat_id: c.chat_id || "",
|
|
146
|
-
route_to_agent: c.route_to_agent || "",
|
|
147
|
-
project: c.project || null,
|
|
148
|
-
respond_with_engine:
|
|
149
|
-
c.respond_with_engine !== undefined
|
|
150
|
-
? c.respond_with_engine
|
|
151
|
-
: tg.respond_with_engine !== false,
|
|
152
|
-
poll_interval_ms: c.poll_interval_ms || tg.poll_interval_ms || 1500,
|
|
153
|
-
}));
|
|
154
|
-
}
|
|
155
|
-
// Legacy single-channel mode
|
|
156
|
-
if (!tg.bot_token && !process.env.BOT_TELEGRAM_TOKEN && !process.env.TELEGRAM_BOT_TOKEN) {
|
|
157
|
-
return [];
|
|
158
|
-
}
|
|
159
|
-
return [
|
|
160
|
-
{
|
|
161
|
-
name: "default",
|
|
162
|
-
bot_token: tg.bot_token || "",
|
|
163
|
-
chat_id: tg.chat_id || "",
|
|
164
|
-
route_to_agent: tg.route_to_agent || "",
|
|
165
|
-
project: null,
|
|
166
|
-
respond_with_engine: tg.respond_with_engine !== false,
|
|
167
|
-
poll_interval_ms: tg.poll_interval_ms || 1500,
|
|
168
|
-
},
|
|
169
|
-
];
|
|
170
|
-
}
|
|
171
70
|
|
|
172
71
|
// ---------- per-channel poller ----------------------------------------------
|
|
173
72
|
|
|
@@ -275,569 +174,11 @@ class ChannelPoller {
|
|
|
275
174
|
return json.result || [];
|
|
276
175
|
}
|
|
277
176
|
|
|
177
|
+
// Method body lives in ./dispatch.js as `handleUpdate(self, u)` so this file
|
|
178
|
+
// stays focused on poller lifecycle. The function reaches every internal
|
|
179
|
+
// poller field through the `self` it receives.
|
|
278
180
|
async _handleUpdate(u) {
|
|
279
|
-
this
|
|
280
|
-
|
|
281
|
-
// Inline keyboard button press: route to the confirmation adapter.
|
|
282
|
-
if (u.callback_query) {
|
|
283
|
-
await this._handleCallbackQuery(u.callback_query);
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const msg = u.message || u.edited_message;
|
|
288
|
-
if (!msg) return;
|
|
289
|
-
const target = this.resolveProject();
|
|
290
|
-
if (!target) {
|
|
291
|
-
this.log(`telegram[${this.channel.name}] update ${u.update_id} ignored — no target project`);
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
const author =
|
|
295
|
-
msg.from?.username
|
|
296
|
-
? "@" + msg.from.username
|
|
297
|
-
: `${msg.from?.first_name || ""} ${msg.from?.last_name || ""}`.trim() || "unknown";
|
|
298
|
-
const chat_id = msg.chat?.id;
|
|
299
|
-
|
|
300
|
-
// Resolve WHO is writing (owner / known contact / guest), keyed by the
|
|
301
|
-
// stable Telegram user_id. Records unknown senders and, on a fresh private
|
|
302
|
-
// channel with no owner yet, claims this sender as the owner. Mutates the
|
|
303
|
-
// in-memory globalConfig in place so later messages in this daemon session
|
|
304
|
-
// see the update. The resulting block is injected into whichever agent
|
|
305
|
-
// answers (super-agent OR a routed project agent).
|
|
306
|
-
const { sender } = registerSender({
|
|
307
|
-
cfg: this.globalConfig,
|
|
308
|
-
channelName: this.channel.name,
|
|
309
|
-
from: msg.from,
|
|
310
|
-
chatType: msg.chat?.type,
|
|
311
|
-
});
|
|
312
|
-
const relationshipBlock = buildRelationshipBlock(sender);
|
|
313
|
-
// Role-based tool gating for the super-agent path (guests → no tools).
|
|
314
|
-
const allowedTools = resolveAllowedTools(this.globalConfig, sender);
|
|
315
|
-
|
|
316
|
-
// Default Interrupt: abort any running request for this chat_id
|
|
317
|
-
if (chat_id) {
|
|
318
|
-
const prev = this.activeRequests.get(chat_id);
|
|
319
|
-
if (prev) {
|
|
320
|
-
this.log(`telegram[${this.channel.name}] interrupting previous request for chat ${chat_id}`);
|
|
321
|
-
prev.abort();
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
const abortCtrl = new AbortController();
|
|
325
|
-
if (chat_id) this.activeRequests.set(chat_id, abortCtrl);
|
|
326
|
-
|
|
327
|
-
let text = msg.text || msg.caption || "";
|
|
328
|
-
|
|
329
|
-
// ── Incoming photo handling ───────────────────────────────────────────
|
|
330
|
-
if (msg.photo && msg.photo.length > 0) {
|
|
331
|
-
// Telegram sends multiple sizes; pick the largest
|
|
332
|
-
const bestPhoto = msg.photo.reduce((a, b) => (b.file_size > a.file_size ? b : a));
|
|
333
|
-
const token = resolveBotToken(this.channel);
|
|
334
|
-
const mediaDir = path.join(APX_HOME, "media");
|
|
335
|
-
fs.mkdirSync(mediaDir, { recursive: true });
|
|
336
|
-
try {
|
|
337
|
-
const localPath = await downloadTelegramFile(token, bestPhoto.file_id, mediaDir);
|
|
338
|
-
this.log(`telegram[${this.channel.name}] photo saved: ${localPath}`);
|
|
339
|
-
appendGlobalMessage({
|
|
340
|
-
channel: CHANNELS.TELEGRAM,
|
|
341
|
-
direction: "in",
|
|
342
|
-
type: "photo",
|
|
343
|
-
actor_id: msg.from?.id ? String(msg.from.id) : author,
|
|
344
|
-
external_id: String(u.update_id),
|
|
345
|
-
author,
|
|
346
|
-
body: text || "[photo]",
|
|
347
|
-
meta: {
|
|
348
|
-
chat_id,
|
|
349
|
-
user_id: msg.from?.id || null,
|
|
350
|
-
message_id: msg.message_id,
|
|
351
|
-
tg_channel: this.channel.name,
|
|
352
|
-
local_path: localPath,
|
|
353
|
-
file_id: bestPhoto.file_id,
|
|
354
|
-
width: bestPhoto.width,
|
|
355
|
-
height: bestPhoto.height,
|
|
356
|
-
},
|
|
357
|
-
});
|
|
358
|
-
} catch (e) {
|
|
359
|
-
this.log(`telegram[${this.channel.name}] photo download failed: ${e.message}`);
|
|
360
|
-
}
|
|
361
|
-
// If there's a caption, continue to handle it as text; otherwise return
|
|
362
|
-
if (!text) return;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// ── Incoming voice / audio handling ──────────────────────────────────
|
|
366
|
-
// Telegram sends `voice` for the press-and-hold mic recording (.oga/opus)
|
|
367
|
-
// and `audio` for uploaded audio files (mp3/m4a/etc.). Either way we
|
|
368
|
-
// download, run it through Whisper, prefix the result with `[audio] `
|
|
369
|
-
// and let the rest of the message flow handle it as plain text.
|
|
370
|
-
const incomingAudio = msg.voice || msg.audio;
|
|
371
|
-
if (incomingAudio && incomingAudio.file_id) {
|
|
372
|
-
const token = resolveBotToken(this.channel);
|
|
373
|
-
const mediaDir = path.join(APX_HOME, "media");
|
|
374
|
-
fs.mkdirSync(mediaDir, { recursive: true });
|
|
375
|
-
// Show "typing…" right away — download + transcription is the slow part of
|
|
376
|
-
// a voice message, and the reply-path typing (below) only starts after it,
|
|
377
|
-
// so without this the chat sits silent for seconds with no feedback.
|
|
378
|
-
const stopVoiceTyping = this._startTyping(chat_id);
|
|
379
|
-
let localPath = null;
|
|
380
|
-
let transcript = "";
|
|
381
|
-
let transcribeError = null;
|
|
382
|
-
let transcribeBackend = null;
|
|
383
|
-
try {
|
|
384
|
-
localPath = await downloadTelegramFile(token, incomingAudio.file_id, mediaDir);
|
|
385
|
-
this.log(`telegram[${this.channel.name}] audio saved: ${localPath}`);
|
|
386
|
-
} catch (e) {
|
|
387
|
-
this.log(`telegram[${this.channel.name}] audio download failed: ${e.message}`);
|
|
388
|
-
}
|
|
389
|
-
if (localPath) {
|
|
390
|
-
try {
|
|
391
|
-
const result = await transcribeAudioFile(localPath);
|
|
392
|
-
transcript = result.text || "";
|
|
393
|
-
transcribeBackend = result.backend;
|
|
394
|
-
this.log(`telegram[${this.channel.name}] audio transcribed via ${transcribeBackend} (${transcript.length} chars, lang=${result.language || "?"})`);
|
|
395
|
-
} catch (e) {
|
|
396
|
-
transcribeError = e.message;
|
|
397
|
-
this.log(`telegram[${this.channel.name}] audio transcription failed: ${e.message}`);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
stopVoiceTyping(); // reply-path typing takes over from here
|
|
401
|
-
const audioBody = transcript
|
|
402
|
-
? `[audio] ${transcript}`
|
|
403
|
-
: `[audio] (transcription unavailable${transcribeError ? ": " + transcribeError : ""})`;
|
|
404
|
-
|
|
405
|
-
appendGlobalMessage({
|
|
406
|
-
channel: CHANNELS.TELEGRAM,
|
|
407
|
-
direction: "in",
|
|
408
|
-
type: "audio",
|
|
409
|
-
actor_id: msg.from?.id ? String(msg.from.id) : author,
|
|
410
|
-
external_id: String(u.update_id),
|
|
411
|
-
author,
|
|
412
|
-
body: audioBody,
|
|
413
|
-
meta: {
|
|
414
|
-
chat_id,
|
|
415
|
-
user_id: msg.from?.id || null,
|
|
416
|
-
message_id: msg.message_id,
|
|
417
|
-
tg_channel: this.channel.name,
|
|
418
|
-
local_path: localPath,
|
|
419
|
-
file_id: incomingAudio.file_id,
|
|
420
|
-
duration: incomingAudio.duration,
|
|
421
|
-
mime_type: incomingAudio.mime_type,
|
|
422
|
-
transcription_backend: transcribeBackend,
|
|
423
|
-
transcription_error: transcribeError,
|
|
424
|
-
},
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
// Inject the transcribed text into `text` so the rest of the agent
|
|
428
|
-
// pipeline treats it identically to a typed message. If there was a
|
|
429
|
-
// caption alongside the audio, prepend the audio marker to it.
|
|
430
|
-
text = text ? `${audioBody}\n${text}` : audioBody;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// If there's a pending ask_questions flow for this chat AND the current
|
|
434
|
-
// question is free-text, treat this message as the answer rather than a
|
|
435
|
-
// brand-new turn. Returns true when the message was consumed.
|
|
436
|
-
if (chat_id && text && await this._maybeConsumeAskTextAnswer({ chat_id, text })) {
|
|
437
|
-
// Still log the inbound so the chat history records what the user said.
|
|
438
|
-
appendGlobalMessage({
|
|
439
|
-
channel: CHANNELS.TELEGRAM,
|
|
440
|
-
direction: "in",
|
|
441
|
-
type: "user",
|
|
442
|
-
actor_id: msg.from?.id ? String(msg.from.id) : author,
|
|
443
|
-
external_id: String(u.update_id),
|
|
444
|
-
author,
|
|
445
|
-
body: text,
|
|
446
|
-
meta: {
|
|
447
|
-
chat_id,
|
|
448
|
-
user_id: msg.from?.id || null,
|
|
449
|
-
message_id: msg.message_id,
|
|
450
|
-
tg_channel: this.channel.name,
|
|
451
|
-
ask_answer: true,
|
|
452
|
-
},
|
|
453
|
-
});
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// /reset or /new wipes the rolling context for this chat. We just
|
|
458
|
-
// remember a marker timestamp; subsequent inbounds will only consider
|
|
459
|
-
// history newer than this. Implemented by writing a synthetic message
|
|
460
|
-
// with a known marker so getRecentTelegramTurns naturally cuts off.
|
|
461
|
-
const isReset = /^\/(reset|new)\b/i.test(text.trim());
|
|
462
|
-
|
|
463
|
-
// Pull the prior conversation BEFORE we log this inbound — so the
|
|
464
|
-
// current message isn't part of its own history. We then prune anything
|
|
465
|
-
// older than the most recent /reset for this chat_id.
|
|
466
|
-
let previousMessages = [];
|
|
467
|
-
if (chat_id && !isReset) {
|
|
468
|
-
previousMessages = getRecentTelegramTurnsFromFs({
|
|
469
|
-
chat_id,
|
|
470
|
-
keepRecent: 40,
|
|
471
|
-
max_age_hours: 24,
|
|
472
|
-
});
|
|
473
|
-
// Progressive compaction (Pieza 3) runs OUT of the reply path: if this
|
|
474
|
-
// chat is over threshold, summarize the oldest turns in the background so
|
|
475
|
-
// the next turn reads a [RESUMEN COMPACTADO] instead of raw history. Never
|
|
476
|
-
// awaited — adds zero latency to this reply, degrades gracefully.
|
|
477
|
-
compactChannelIfNeeded({
|
|
478
|
-
channel: CHANNELS.TELEGRAM,
|
|
479
|
-
chat_id,
|
|
480
|
-
config: this.globalConfig,
|
|
481
|
-
log: this.log,
|
|
482
|
-
}).catch(() => {});
|
|
483
|
-
// Honour a /reset marker: drop everything up to and including it.
|
|
484
|
-
const lastResetIdx = (() => {
|
|
485
|
-
for (let i = previousMessages.length - 1; i >= 0; i--) {
|
|
486
|
-
if (
|
|
487
|
-
previousMessages[i].role === "user" &&
|
|
488
|
-
/^\/(reset|new)\b/i.test(previousMessages[i].content.trim())
|
|
489
|
-
) {
|
|
490
|
-
return i;
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
return -1;
|
|
494
|
-
})();
|
|
495
|
-
if (lastResetIdx >= 0) {
|
|
496
|
-
previousMessages = previousMessages.slice(lastResetIdx + 1);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Always log inbound to global store (~/.apx/messages/telegram/)
|
|
501
|
-
appendGlobalMessage({
|
|
502
|
-
channel: CHANNELS.TELEGRAM,
|
|
503
|
-
direction: "in",
|
|
504
|
-
type: "user",
|
|
505
|
-
actor_id: msg.from?.id ? String(msg.from.id) : author,
|
|
506
|
-
external_id: String(u.update_id),
|
|
507
|
-
author,
|
|
508
|
-
body: text,
|
|
509
|
-
meta: {
|
|
510
|
-
chat_id,
|
|
511
|
-
user_id: msg.from?.id || null,
|
|
512
|
-
message_id: msg.message_id,
|
|
513
|
-
tg_channel: this.channel.name,
|
|
514
|
-
},
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
// Super-agent is ALWAYS active on Telegram: respond_with_engine === false
|
|
518
|
-
// used to silently drop user messages, which looked to the user like the
|
|
519
|
-
// bot ignored them. Honour the legacy flag only as a soft hint (skip the
|
|
520
|
-
// routed-agent shortcut so we fall straight to super-agent) but never let
|
|
521
|
-
// it short-circuit the whole reply. To genuinely silence the bot, disable
|
|
522
|
-
// the channel entirely (telegram.enabled = false in config).
|
|
523
|
-
const skipRoutedAgent = this.channel.respond_with_engine === false;
|
|
524
|
-
if (!text) return;
|
|
525
|
-
|
|
526
|
-
// Short-circuit /reset / /new: send an ack and don't engage the engine.
|
|
527
|
-
// The marker we just logged is enough — getRecentTelegramTurns will
|
|
528
|
-
// honor it for future messages.
|
|
529
|
-
if (isReset) {
|
|
530
|
-
try {
|
|
531
|
-
const ack = "Done, context cleared. Starting fresh. What do you need?";
|
|
532
|
-
await this._send({ chat_id, text: ack });
|
|
533
|
-
appendGlobalMessage({
|
|
534
|
-
channel: CHANNELS.TELEGRAM,
|
|
535
|
-
direction: "out",
|
|
536
|
-
type: "agent",
|
|
537
|
-
actor_id: SUPERAGENT_ACTOR_ID,
|
|
538
|
-
actor_kind: "superagent",
|
|
539
|
-
agent_slug: SUPERAGENT_ACTOR_ID,
|
|
540
|
-
author: resolveAgentName(this.globalConfig),
|
|
541
|
-
body: ack,
|
|
542
|
-
meta: { chat_id, tg_channel: this.channel.name, in_reply_to: u.update_id, reset: true },
|
|
543
|
-
});
|
|
544
|
-
} catch (e) {
|
|
545
|
-
this.log(`telegram[${this.channel.name}] reset ack failed: ${e.message}`);
|
|
546
|
-
}
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
// Start "typing..." indicator. Stops when we send the reply (or fail).
|
|
551
|
-
const stopTyping = this._startTyping(chat_id);
|
|
552
|
-
|
|
553
|
-
let replyText;
|
|
554
|
-
let replyAuthor;
|
|
555
|
-
let replyActorId; // stable id: super_agent | agent slug
|
|
556
|
-
let replyKind; // actor_kind: superagent | agent
|
|
557
|
-
const projectCfg = target.config || this.globalConfig;
|
|
558
|
-
// Display name for the super-agent persona on this channel (from identity.json).
|
|
559
|
-
const agentDisplay = resolveAgentName(this.globalConfig);
|
|
560
|
-
|
|
561
|
-
// Try the project's chosen agent first (skipped if the legacy
|
|
562
|
-
// respond_with_engine === false hint asked to bypass routed agents).
|
|
563
|
-
const routeSlug = skipRoutedAgent ? null : this.channel.route_to_agent;
|
|
564
|
-
if (routeSlug) {
|
|
565
|
-
const agent = readAgents(target.path).find((a) => a.slug === routeSlug);
|
|
566
|
-
if (agent && agent.fields.Model) {
|
|
567
|
-
try {
|
|
568
|
-
const system = buildAgentSystem(target, agent, {
|
|
569
|
-
invocation: "telegram",
|
|
570
|
-
channel: this.channel.name,
|
|
571
|
-
caller: author,
|
|
572
|
-
extraParts: [relationshipBlock],
|
|
573
|
-
});
|
|
574
|
-
const result = await callEngine({
|
|
575
|
-
modelId: agent.fields.Model,
|
|
576
|
-
system,
|
|
577
|
-
messages: [{ role: "user", content: text }],
|
|
578
|
-
config: projectCfg,
|
|
579
|
-
});
|
|
580
|
-
replyText = result.text;
|
|
581
|
-
replyAuthor = agent.slug;
|
|
582
|
-
replyActorId = agent.slug;
|
|
583
|
-
replyKind = "agent";
|
|
584
|
-
} catch (e) {
|
|
585
|
-
this.log(`telegram[${this.channel.name}] agent reply failed: ${e.message}`);
|
|
586
|
-
replyText = `[apx error] ${e.message.slice(0, 200)}`;
|
|
587
|
-
replyAuthor = agentDisplay;
|
|
588
|
-
replyActorId = SUPERAGENT_ACTOR_ID;
|
|
589
|
-
replyKind = "superagent";
|
|
590
|
-
}
|
|
591
|
-
} else {
|
|
592
|
-
this.log(
|
|
593
|
-
`telegram[${this.channel.name}] route_to_agent="${routeSlug}" not usable (missing or no model) → trying super-agent`
|
|
594
|
-
);
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
// Fallback: super-agent — STREAMED.
|
|
599
|
-
// Each iteration's assistant text is sent to Telegram as its own message
|
|
600
|
-
// the moment the model produces it (its running commentary), so the user
|
|
601
|
-
// sees a real back-and-forth instead of one giant final dump. Tool calls
|
|
602
|
-
// are logged to the message store — visible via apx log / apx search and
|
|
603
|
-
// to channels that render tools — but NEVER sent to Telegram; tools are
|
|
604
|
-
// internal. The conversation saved on disk is the full, real exchange;
|
|
605
|
-
// Telegram is just the prose-only view of it.
|
|
606
|
-
let saUsage = null;
|
|
607
|
-
let streamedCount = 0;
|
|
608
|
-
let lastStreamedText = "";
|
|
609
|
-
// Telegram shows the user ONLY prose — never the tool calls. On an action
|
|
610
|
-
// request the model often jumps straight to a tool with no preamble text,
|
|
611
|
-
// so the user would stare at a silent chat until the final reply. Send one
|
|
612
|
-
// short localized heads-up the moment real work starts (first tool_start),
|
|
613
|
-
// but only if the agent didn't already write its own "on it" line.
|
|
614
|
-
let sentHeadsUp = false;
|
|
615
|
-
const headsUpPhrase = () => {
|
|
616
|
-
const lang = (this.globalConfig?.user?.language || "es").slice(0, 2);
|
|
617
|
-
const byLang = {
|
|
618
|
-
es: "Dale, estoy con eso… 🛠️",
|
|
619
|
-
en: "On it — working on that… 🛠️",
|
|
620
|
-
pt: "Já estou nisso… 🛠️",
|
|
621
|
-
};
|
|
622
|
-
return byLang[lang] || byLang.es;
|
|
623
|
-
};
|
|
624
|
-
if (!replyText && isSuperAgentEnabled(this.globalConfig)) {
|
|
625
|
-
const onEvent = async (ev) => {
|
|
626
|
-
try {
|
|
627
|
-
if (ev.type === "tool_start" && !sentHeadsUp && streamedCount === 0) {
|
|
628
|
-
sentHeadsUp = true;
|
|
629
|
-
const heads = headsUpPhrase();
|
|
630
|
-
await this._send({ chat_id, text: heads });
|
|
631
|
-
appendGlobalMessage({
|
|
632
|
-
channel: CHANNELS.TELEGRAM,
|
|
633
|
-
direction: "out",
|
|
634
|
-
type: "agent",
|
|
635
|
-
actor_id: SUPERAGENT_ACTOR_ID,
|
|
636
|
-
actor_kind: "superagent",
|
|
637
|
-
agent_slug: SUPERAGENT_ACTOR_ID,
|
|
638
|
-
author: agentDisplay,
|
|
639
|
-
body: heads,
|
|
640
|
-
meta: { chat_id, tg_channel: this.channel.name, in_reply_to: u.update_id, heads_up: true },
|
|
641
|
-
});
|
|
642
|
-
return;
|
|
643
|
-
}
|
|
644
|
-
if (ev.type === "assistant_text" && ev.text) {
|
|
645
|
-
const piece = stripThinking(ev.text).trim();
|
|
646
|
-
if (!piece) return;
|
|
647
|
-
await this._send({ chat_id, text: piece });
|
|
648
|
-
lastStreamedText = piece;
|
|
649
|
-
streamedCount += 1;
|
|
650
|
-
appendGlobalMessage({
|
|
651
|
-
channel: CHANNELS.TELEGRAM,
|
|
652
|
-
direction: "out",
|
|
653
|
-
type: "agent",
|
|
654
|
-
actor_id: SUPERAGENT_ACTOR_ID,
|
|
655
|
-
actor_kind: "superagent",
|
|
656
|
-
agent_slug: SUPERAGENT_ACTOR_ID,
|
|
657
|
-
author: agentDisplay,
|
|
658
|
-
body: piece,
|
|
659
|
-
meta: {
|
|
660
|
-
chat_id,
|
|
661
|
-
tg_channel: this.channel.name,
|
|
662
|
-
in_reply_to: u.update_id,
|
|
663
|
-
streamed: true,
|
|
664
|
-
iteration: ev.iteration,
|
|
665
|
-
},
|
|
666
|
-
});
|
|
667
|
-
} else if (ev.type === "tool_result" && ev.trace) {
|
|
668
|
-
// Logged for the audit trail / other channels — NOT sent to Telegram.
|
|
669
|
-
const t = ev.trace;
|
|
670
|
-
appendGlobalMessage({
|
|
671
|
-
channel: CHANNELS.TELEGRAM,
|
|
672
|
-
direction: "out",
|
|
673
|
-
type: "tool",
|
|
674
|
-
actor_id: t.tool,
|
|
675
|
-
actor_kind: "tool",
|
|
676
|
-
author: agentDisplay,
|
|
677
|
-
body: `${t.tool}(${JSON.stringify(t.args || {}).slice(0, 200)})`,
|
|
678
|
-
meta: {
|
|
679
|
-
chat_id,
|
|
680
|
-
tg_channel: this.channel.name,
|
|
681
|
-
in_reply_to: u.update_id,
|
|
682
|
-
tool: t.tool,
|
|
683
|
-
args: t.args,
|
|
684
|
-
result: t.result,
|
|
685
|
-
iteration: ev.iteration,
|
|
686
|
-
},
|
|
687
|
-
});
|
|
688
|
-
}
|
|
689
|
-
} catch (e) {
|
|
690
|
-
// A failed intermediate send must not abort the whole run.
|
|
691
|
-
this.log(`telegram[${this.channel.name}] stream event failed: ${e.message}`);
|
|
692
|
-
}
|
|
693
|
-
};
|
|
694
|
-
|
|
695
|
-
const confirmAdapter = createTelegramConfirmAdapter({
|
|
696
|
-
token: resolveBotToken(this.channel),
|
|
697
|
-
chatId: chat_id,
|
|
698
|
-
pendingStore: getConfirmStore(),
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
// `/slug ...` shortcut: load the matching skill body into contextNote
|
|
702
|
-
// and strip the prefix from the user prompt before sending to the loop.
|
|
703
|
-
const slashed = tryResolveSkillCommand(text, { projectPath: target?.path });
|
|
704
|
-
const slashedPrompt = slashed.handled ? slashed.prompt : text;
|
|
705
|
-
const slashedContextNote = slashed.handled ? slashed.contextNote : "";
|
|
706
|
-
|
|
707
|
-
try {
|
|
708
|
-
const sa = await runSuperAgent({
|
|
709
|
-
globalConfig: this.globalConfig,
|
|
710
|
-
projects: this.projects,
|
|
711
|
-
plugins: this.plugins,
|
|
712
|
-
registries: this.registries,
|
|
713
|
-
prompt: slashedPrompt,
|
|
714
|
-
previousMessages,
|
|
715
|
-
channel: CHANNELS.TELEGRAM,
|
|
716
|
-
relationshipBlock,
|
|
717
|
-
allowedTools,
|
|
718
|
-
contextNote: slashedContextNote || undefined,
|
|
719
|
-
channelMeta: buildTelegramMeta({
|
|
720
|
-
channelName: this.channel.name,
|
|
721
|
-
author,
|
|
722
|
-
chatId: chat_id,
|
|
723
|
-
target,
|
|
724
|
-
routeToAgent: this.channel.route_to_agent,
|
|
725
|
-
}),
|
|
726
|
-
signal: abortCtrl.signal,
|
|
727
|
-
onEvent,
|
|
728
|
-
requestConfirmation: confirmAdapter.requestConfirmation,
|
|
729
|
-
});
|
|
730
|
-
replyText = sa.text;
|
|
731
|
-
replyAuthor = sa.name || agentDisplay;
|
|
732
|
-
replyActorId = SUPERAGENT_ACTOR_ID;
|
|
733
|
-
replyKind = "superagent";
|
|
734
|
-
saUsage = sa.usage;
|
|
735
|
-
|
|
736
|
-
// ── ask_questions integration ────────────────────────────────────
|
|
737
|
-
// If the super-agent ended this turn by calling ask_questions, hand
|
|
738
|
-
// off to the inline-keyboard flow instead of sending the bare
|
|
739
|
-
// assistant text. The flow keeps state per chat_id and re-runs the
|
|
740
|
-
// super-agent once every answer is collected.
|
|
741
|
-
const askQuestions = askFlow.extractAskQuestionsFromTrace(sa.trace);
|
|
742
|
-
if (askQuestions && chat_id) {
|
|
743
|
-
if (chat_id) this.activeRequests.delete(chat_id);
|
|
744
|
-
stopTyping();
|
|
745
|
-
try {
|
|
746
|
-
await this._startAskFlow({
|
|
747
|
-
chat_id,
|
|
748
|
-
projectId: target?.id,
|
|
749
|
-
authorId: msg.from?.id,
|
|
750
|
-
questions: askQuestions,
|
|
751
|
-
author,
|
|
752
|
-
agentDisplay,
|
|
753
|
-
relationshipBlock,
|
|
754
|
-
allowedTools,
|
|
755
|
-
target,
|
|
756
|
-
sender,
|
|
757
|
-
update_id: u.update_id,
|
|
758
|
-
});
|
|
759
|
-
} catch (e) {
|
|
760
|
-
this.log(`telegram[${this.channel.name}] ask flow start failed: ${e.message}`);
|
|
761
|
-
}
|
|
762
|
-
return; // The reply for this turn IS the ask flow.
|
|
763
|
-
}
|
|
764
|
-
} catch (e) {
|
|
765
|
-
if (abortCtrl.signal.aborted) {
|
|
766
|
-
// A newer message superseded this one. Whatever streamed so far is
|
|
767
|
-
// already sent + logged; the newer message's run continues the
|
|
768
|
-
// thread from that history.
|
|
769
|
-
this.log(`telegram[${this.channel.name}] request aborted for chat ${chat_id}`);
|
|
770
|
-
if (chat_id) this.activeRequests.delete(chat_id);
|
|
771
|
-
stopTyping();
|
|
772
|
-
return;
|
|
773
|
-
}
|
|
774
|
-
this.log(`telegram[${this.channel.name}] super-agent failed: ${e.message}`);
|
|
775
|
-
// Surface the failure to the user instead of silently dropping the
|
|
776
|
-
// turn — otherwise from the chat side it looks like the bot ignored
|
|
777
|
-
// the message. Keep the message short and non-leaking.
|
|
778
|
-
replyText = `⚠️ Could not generate a reply right now (${e.message || "internal error"}).`;
|
|
779
|
-
replyAuthor = agentDisplay;
|
|
780
|
-
replyActorId = SUPERAGENT_ACTOR_ID;
|
|
781
|
-
replyKind = "superagent";
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
if (chat_id) this.activeRequests.delete(chat_id);
|
|
786
|
-
|
|
787
|
-
// Final answer. The intermediate prose was already streamed; only send the
|
|
788
|
-
// final text if it's non-empty AND not a duplicate of the last streamed
|
|
789
|
-
// piece (the loop can end on an iteration whose text was already sent).
|
|
790
|
-
// If nothing streamed and there's no final text, send a minimal ack so the
|
|
791
|
-
// turn isn't silently empty.
|
|
792
|
-
const finalClean = replyText ? stripThinking(replyText).trim() : "";
|
|
793
|
-
let toSend = "";
|
|
794
|
-
if (finalClean && finalClean !== lastStreamedText) toSend = finalClean;
|
|
795
|
-
else if (!finalClean && streamedCount === 0) toSend = "Listo.";
|
|
796
|
-
|
|
797
|
-
stopTyping();
|
|
798
|
-
if (!toSend) return; // everything was already streamed — nothing left to send
|
|
799
|
-
|
|
800
|
-
try {
|
|
801
|
-
await this._send({ chat_id, text: toSend });
|
|
802
|
-
const meta = {
|
|
803
|
-
chat_id,
|
|
804
|
-
tg_channel: this.channel.name,
|
|
805
|
-
in_reply_to: u.update_id,
|
|
806
|
-
final: true,
|
|
807
|
-
};
|
|
808
|
-
if (replyText && stripThinking(replyText) !== replyText) meta.thinking_stripped = true;
|
|
809
|
-
if (saUsage) meta.usage = saUsage;
|
|
810
|
-
appendGlobalMessage({
|
|
811
|
-
channel: CHANNELS.TELEGRAM,
|
|
812
|
-
direction: "out",
|
|
813
|
-
type: "agent",
|
|
814
|
-
actor_id: replyActorId || SUPERAGENT_ACTOR_ID,
|
|
815
|
-
actor_kind: replyKind || "superagent",
|
|
816
|
-
agent_slug: replyActorId || SUPERAGENT_ACTOR_ID,
|
|
817
|
-
author: replyAuthor || agentDisplay,
|
|
818
|
-
body: toSend,
|
|
819
|
-
meta,
|
|
820
|
-
});
|
|
821
|
-
} catch (e) {
|
|
822
|
-
this.log(`telegram[${this.channel.name}] send-back error: ${e.message}`);
|
|
823
|
-
appendGlobalMessage({
|
|
824
|
-
channel: CHANNELS.TELEGRAM,
|
|
825
|
-
direction: "out",
|
|
826
|
-
type: "agent",
|
|
827
|
-
actor_id: replyActorId || SUPERAGENT_ACTOR_ID,
|
|
828
|
-
actor_kind: replyKind || "superagent",
|
|
829
|
-
agent_slug: replyActorId || SUPERAGENT_ACTOR_ID,
|
|
830
|
-
author: replyAuthor || agentDisplay,
|
|
831
|
-
body: `[send_failed] ${toSend}`,
|
|
832
|
-
meta: {
|
|
833
|
-
chat_id,
|
|
834
|
-
tg_channel: this.channel.name,
|
|
835
|
-
in_reply_to: u.update_id,
|
|
836
|
-
send_error: e.message,
|
|
837
|
-
...(saUsage ? { usage: saUsage } : {}),
|
|
838
|
-
},
|
|
839
|
-
});
|
|
840
|
-
}
|
|
181
|
+
return handleUpdate(this, u);
|
|
841
182
|
}
|
|
842
183
|
|
|
843
184
|
async _handleCallbackQuery(callbackQuery) {
|
|
@@ -1208,10 +549,6 @@ class ChannelPoller {
|
|
|
1208
549
|
}
|
|
1209
550
|
}
|
|
1210
551
|
|
|
1211
|
-
function sleep(ms) {
|
|
1212
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
552
|
// ---------- plugin export ---------------------------------------------------
|
|
1216
553
|
|
|
1217
554
|
export default {
|