@agentprojectcontext/apx 1.41.0 → 1.42.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/src/core/agent/constants.js +10 -0
- package/src/core/agent/run-agent.js +36 -18
- package/src/core/channels/telegram/dispatch.js +60 -310
- package/src/core/channels/telegram/helpers.js +28 -1
- package/src/core/channels/telegram/inbound/audio.js +82 -0
- package/src/core/channels/telegram/inbound/photo.js +63 -0
- package/src/core/channels/telegram/reply.js +204 -0
- package/src/core/config/index.js +5 -0
- package/src/core/i18n/en.js +4 -0
- package/src/core/i18n/es.js +4 -0
- package/src/core/i18n/pt.js +4 -0
- package/src/host/daemon/plugins/telegram/index.js +45 -53
- package/src/interfaces/desktop/renderer.js +43 -41
- package/src/interfaces/desktop/style.css +15 -6
- package/src/interfaces/web/dist/assets/{index-DW7j3cXB.js → index-BReF4_xV.js} +21 -21
- package/src/interfaces/web/dist/assets/{index-DW7j3cXB.js.map → index-BReF4_xV.js.map} +1 -1
- package/src/interfaces/web/dist/index.html +1 -1
- package/src/interfaces/web/package-lock.json +3 -3
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Inbound Telegram VOICE/AUDIO handling, split out of dispatch.js. Telegram
|
|
2
|
+
// sends `voice` for the press-and-hold mic recording (.oga/opus) and `audio`
|
|
3
|
+
// for uploaded audio files (mp3/m4a/etc.). Either way we download, run it
|
|
4
|
+
// through Whisper, prefix the result with `[audio] ` and let the rest of the
|
|
5
|
+
// message flow handle it as plain text.
|
|
6
|
+
//
|
|
7
|
+
// Takes the poller instance (`self`, for logging, channel + the typing
|
|
8
|
+
// indicator) plus the parsed update context, and returns the `text` the rest of
|
|
9
|
+
// the pipeline should run — the transcript merged into any existing caption.
|
|
10
|
+
import { appendGlobalMessage } from "#core/stores/messages.js";
|
|
11
|
+
import { CHANNELS } from "#core/constants/channels.js";
|
|
12
|
+
import { transcribe as transcribeAudioFile } from "#core/voice/transcription.js";
|
|
13
|
+
import { resolveBotToken, telegramMediaDir } from "../helpers.js";
|
|
14
|
+
import { downloadTelegramFile } from "../media.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {object} self poller instance (uses self.log, self.channel, self._startTyping)
|
|
18
|
+
* @param {object} ctx { msg, u, author, chat_id, text, incomingAudio }
|
|
19
|
+
* @returns {Promise<{ text: string }>} text to continue the pipeline with
|
|
20
|
+
*/
|
|
21
|
+
export async function handleIncomingAudio(self, { msg, u, author, chat_id, text, incomingAudio }) {
|
|
22
|
+
const token = resolveBotToken(self.channel);
|
|
23
|
+
const mediaDir = telegramMediaDir();
|
|
24
|
+
|
|
25
|
+
// Show "typing…" right away — download + transcription is the slow part of a
|
|
26
|
+
// voice message, and the reply-path typing only starts after it, so without
|
|
27
|
+
// this the chat sits silent for seconds with no feedback.
|
|
28
|
+
const stopVoiceTyping = self._startTyping(chat_id);
|
|
29
|
+
let localPath = null;
|
|
30
|
+
let transcript = "";
|
|
31
|
+
let transcribeError = null;
|
|
32
|
+
let transcribeBackend = null;
|
|
33
|
+
try {
|
|
34
|
+
localPath = await downloadTelegramFile(token, incomingAudio.file_id, mediaDir);
|
|
35
|
+
self.log(`telegram[${self.channel.name}] audio saved: ${localPath}`);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
self.log(`telegram[${self.channel.name}] audio download failed: ${e.message}`);
|
|
38
|
+
}
|
|
39
|
+
if (localPath) {
|
|
40
|
+
try {
|
|
41
|
+
const result = await transcribeAudioFile(localPath);
|
|
42
|
+
transcript = result.text || "";
|
|
43
|
+
transcribeBackend = result.backend;
|
|
44
|
+
self.log(`telegram[${self.channel.name}] audio transcribed via ${transcribeBackend} (${transcript.length} chars, lang=${result.language || "?"})`);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
transcribeError = e.message;
|
|
47
|
+
self.log(`telegram[${self.channel.name}] audio transcription failed: ${e.message}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
stopVoiceTyping(); // reply-path typing takes over from here
|
|
51
|
+
|
|
52
|
+
const audioBody = transcript
|
|
53
|
+
? `[audio] ${transcript}`
|
|
54
|
+
: `[audio] (transcription unavailable${transcribeError ? ": " + transcribeError : ""})`;
|
|
55
|
+
|
|
56
|
+
appendGlobalMessage({
|
|
57
|
+
channel: CHANNELS.TELEGRAM,
|
|
58
|
+
direction: "in",
|
|
59
|
+
type: "audio",
|
|
60
|
+
actor_id: msg.from?.id ? String(msg.from.id) : author,
|
|
61
|
+
external_id: String(u.update_id),
|
|
62
|
+
author,
|
|
63
|
+
body: audioBody,
|
|
64
|
+
meta: {
|
|
65
|
+
chat_id,
|
|
66
|
+
user_id: msg.from?.id || null,
|
|
67
|
+
message_id: msg.message_id,
|
|
68
|
+
tg_channel: self.channel.name,
|
|
69
|
+
local_path: localPath,
|
|
70
|
+
file_id: incomingAudio.file_id,
|
|
71
|
+
duration: incomingAudio.duration,
|
|
72
|
+
mime_type: incomingAudio.mime_type,
|
|
73
|
+
transcription_backend: transcribeBackend,
|
|
74
|
+
transcription_error: transcribeError,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Inject the transcribed text into `text` so the rest of the agent pipeline
|
|
79
|
+
// treats it identically to a typed message. If there was a caption alongside
|
|
80
|
+
// the audio, prepend the audio marker to it.
|
|
81
|
+
return { text: text ? `${audioBody}\n${text}` : audioBody };
|
|
82
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Inbound Telegram PHOTO handling, split out of dispatch.js so the dispatcher
|
|
2
|
+
// stays focused on routing. Pure of the poller's lifecycle: it takes the poller
|
|
3
|
+
// instance (`self`, for logging + channel) plus the parsed update context, and
|
|
4
|
+
// returns the (possibly rewritten) `text` the rest of the pipeline should run.
|
|
5
|
+
//
|
|
6
|
+
// Vision note: we do NOT have image understanding yet — the engine layer can't
|
|
7
|
+
// pass image content to the model. So we download + archive the photo and then
|
|
8
|
+
// inject an internal `[image]` marker into `text` so the agent ALWAYS produces a
|
|
9
|
+
// reply in its own words (never goes silent on a no-caption photo). The reply is
|
|
10
|
+
// model-authored; the marker only tells the model an image arrived and that it
|
|
11
|
+
// can't see the pixels yet. Mirrors the `[audio]` marker convention.
|
|
12
|
+
import { appendGlobalMessage } from "#core/stores/messages.js";
|
|
13
|
+
import { CHANNELS } from "#core/constants/channels.js";
|
|
14
|
+
import { resolveBotToken, telegramMediaDir } from "../helpers.js";
|
|
15
|
+
import { downloadTelegramFile } from "../media.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {object} self poller instance (uses self.log, self.channel)
|
|
19
|
+
* @param {object} ctx { msg, u, author, chat_id, text }
|
|
20
|
+
* @returns {Promise<{ text: string }>} text to continue the pipeline with
|
|
21
|
+
*/
|
|
22
|
+
export async function handleIncomingPhoto(self, { msg, u, author, chat_id, text }) {
|
|
23
|
+
// Telegram sends multiple sizes; pick the largest.
|
|
24
|
+
const bestPhoto = msg.photo.reduce((a, b) => (b.file_size > a.file_size ? b : a));
|
|
25
|
+
const token = resolveBotToken(self.channel);
|
|
26
|
+
const mediaDir = telegramMediaDir();
|
|
27
|
+
|
|
28
|
+
let localPath = null;
|
|
29
|
+
try {
|
|
30
|
+
localPath = await downloadTelegramFile(token, bestPhoto.file_id, mediaDir);
|
|
31
|
+
self.log(`telegram[${self.channel.name}] photo saved: ${localPath}`);
|
|
32
|
+
} catch (e) {
|
|
33
|
+
self.log(`telegram[${self.channel.name}] photo download failed: ${e.message}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Archive the inbound photo regardless of download outcome, so chat history
|
|
37
|
+
// records it even if the file fetch failed.
|
|
38
|
+
appendGlobalMessage({
|
|
39
|
+
channel: CHANNELS.TELEGRAM,
|
|
40
|
+
direction: "in",
|
|
41
|
+
type: "photo",
|
|
42
|
+
actor_id: msg.from?.id ? String(msg.from.id) : author,
|
|
43
|
+
external_id: String(u.update_id),
|
|
44
|
+
author,
|
|
45
|
+
body: text || "[photo]",
|
|
46
|
+
meta: {
|
|
47
|
+
chat_id,
|
|
48
|
+
user_id: msg.from?.id || null,
|
|
49
|
+
message_id: msg.message_id,
|
|
50
|
+
tg_channel: self.channel.name,
|
|
51
|
+
local_path: localPath,
|
|
52
|
+
file_id: bestPhoto.file_id,
|
|
53
|
+
width: bestPhoto.width,
|
|
54
|
+
height: bestPhoto.height,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Guard: never go silent. Hand the agent an internal marker so it replies in
|
|
59
|
+
// its own words. No vision yet → say so, in-band, so the model doesn't
|
|
60
|
+
// hallucinate "seeing" the image.
|
|
61
|
+
const marker = "[image attached — you cannot see its contents yet]";
|
|
62
|
+
return { text: text ? `${marker} ${text}` : marker };
|
|
63
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// Shared Telegram super-agent reply path. Both the inbound dispatcher
|
|
2
|
+
// (handleUpdate) and the ask-flow resume (_runResumedTurn in the host poller)
|
|
3
|
+
// drive the SAME streamed turn through these helpers, so behavior — autonomy
|
|
4
|
+
// budget, streaming, never-silent floor, localized errors, rich channelMeta —
|
|
5
|
+
// can't drift between the two entry points. It did drift: the resume path was a
|
|
6
|
+
// stale hand-rolled copy that missed maxIters, streaming and i18n. One source
|
|
7
|
+
// of truth fixes that for good.
|
|
8
|
+
import { runSuperAgent } from "#core/agent/super-agent.js";
|
|
9
|
+
import { TELEGRAM_TOOL_ITERS } from "#core/agent/constants.js";
|
|
10
|
+
import { stripThinking } from "#core/util/thinking.js";
|
|
11
|
+
import { appendGlobalMessage } from "#core/stores/messages.js";
|
|
12
|
+
import { CHANNELS } from "#core/constants/channels.js";
|
|
13
|
+
import { SUPERAGENT_ACTOR_ID } from "#core/identity/index.js";
|
|
14
|
+
import { createTelegramConfirmAdapter } from "#core/confirmation/adapters/telegram.js";
|
|
15
|
+
import { getConfirmationStore as getConfirmStore } from "#core/confirmation/pending-store.js";
|
|
16
|
+
import { t, resolveLang } from "#core/i18n/index.js";
|
|
17
|
+
import { buildTelegramMeta, resolveBotToken } from "./helpers.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build the streaming event handler for a Telegram super-agent turn. Sends a
|
|
21
|
+
* one-shot localized heads-up the moment real work starts (first tool), streams
|
|
22
|
+
* each assistant-text iteration as its own chat message, and logs tool calls
|
|
23
|
+
* (audit trail / other channels — never sent to Telegram). Returns the handler
|
|
24
|
+
* plus a live `state` the caller reads AFTER the run to drive the final send.
|
|
25
|
+
*
|
|
26
|
+
* @returns {{ onEvent: Function, state: { streamedCount: number, lastStreamedText: string } }}
|
|
27
|
+
*/
|
|
28
|
+
export function buildStreamHandler(self, { chat_id, update_id, agentDisplay }) {
|
|
29
|
+
const state = { streamedCount: 0, lastStreamedText: "", sentHeadsUp: false };
|
|
30
|
+
const onEvent = async (ev) => {
|
|
31
|
+
try {
|
|
32
|
+
if (ev.type === "tool_start" && !state.sentHeadsUp && state.streamedCount === 0) {
|
|
33
|
+
state.sentHeadsUp = true;
|
|
34
|
+
const heads = t("telegram.heads_up", { lang: resolveLang(self.globalConfig) });
|
|
35
|
+
await self._send({ chat_id, text: heads });
|
|
36
|
+
appendGlobalMessage({
|
|
37
|
+
channel: CHANNELS.TELEGRAM,
|
|
38
|
+
direction: "out",
|
|
39
|
+
type: "agent",
|
|
40
|
+
actor_id: SUPERAGENT_ACTOR_ID,
|
|
41
|
+
actor_kind: "superagent",
|
|
42
|
+
agent_slug: SUPERAGENT_ACTOR_ID,
|
|
43
|
+
author: agentDisplay,
|
|
44
|
+
body: heads,
|
|
45
|
+
meta: { chat_id, tg_channel: self.channel.name, in_reply_to: update_id, heads_up: true },
|
|
46
|
+
});
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (ev.type === "assistant_text" && ev.text) {
|
|
50
|
+
const piece = stripThinking(ev.text).trim();
|
|
51
|
+
if (!piece) return;
|
|
52
|
+
await self._send({ chat_id, text: piece });
|
|
53
|
+
state.lastStreamedText = piece;
|
|
54
|
+
state.streamedCount += 1;
|
|
55
|
+
appendGlobalMessage({
|
|
56
|
+
channel: CHANNELS.TELEGRAM,
|
|
57
|
+
direction: "out",
|
|
58
|
+
type: "agent",
|
|
59
|
+
actor_id: SUPERAGENT_ACTOR_ID,
|
|
60
|
+
actor_kind: "superagent",
|
|
61
|
+
agent_slug: SUPERAGENT_ACTOR_ID,
|
|
62
|
+
author: agentDisplay,
|
|
63
|
+
body: piece,
|
|
64
|
+
meta: { chat_id, tg_channel: self.channel.name, in_reply_to: update_id, streamed: true, iteration: ev.iteration },
|
|
65
|
+
});
|
|
66
|
+
} else if (ev.type === "tool_result" && ev.trace) {
|
|
67
|
+
// Logged for the audit trail / other channels — NOT sent to Telegram.
|
|
68
|
+
const tr = ev.trace;
|
|
69
|
+
appendGlobalMessage({
|
|
70
|
+
channel: CHANNELS.TELEGRAM,
|
|
71
|
+
direction: "out",
|
|
72
|
+
type: "tool",
|
|
73
|
+
actor_id: tr.tool,
|
|
74
|
+
actor_kind: "tool",
|
|
75
|
+
author: agentDisplay,
|
|
76
|
+
body: `${tr.tool}(${JSON.stringify(tr.args || {}).slice(0, 200)})`,
|
|
77
|
+
meta: { chat_id, tg_channel: self.channel.name, in_reply_to: update_id, tool: tr.tool, args: tr.args, result: tr.result, iteration: ev.iteration },
|
|
78
|
+
});
|
|
79
|
+
} else if (ev.type === "engine_failed") {
|
|
80
|
+
// A model in the fallback chain errored; the loop is rotating to the
|
|
81
|
+
// next one. Log so a mid-turn provider failure is diagnosable.
|
|
82
|
+
self.log(`telegram[${self.channel.name}] engine_failed: ${ev.model || "?"} (${ev.reason || "?"}) → ${ev.retry_with || "end of chain"}`);
|
|
83
|
+
} else if (ev.type === "model_routed" || ev.type === "model_retry") {
|
|
84
|
+
self.log(`telegram[${self.channel.name}] ${ev.type}: model=${ev.model || "?"}${ev.reason ? ` reason=${ev.reason}` : ""}${ev.from_fallback ? " (fallback)" : ""}`);
|
|
85
|
+
}
|
|
86
|
+
} catch (e) {
|
|
87
|
+
// A failed intermediate send must not abort the whole run.
|
|
88
|
+
self.log(`telegram[${self.channel.name}] stream event failed: ${e.message}`);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
return { onEvent, state };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Run the super-agent for a Telegram turn with the canonical channel config:
|
|
96
|
+
* the autonomy budget (telegram_max_iters → TELEGRAM_TOOL_ITERS), rich
|
|
97
|
+
* channelMeta (project pin + route), the confirmation adapter, and streaming.
|
|
98
|
+
* The single place this call is configured — change it once, both entry points
|
|
99
|
+
* inherit it. Throws on failure (caller decides abort-vs-error handling).
|
|
100
|
+
*/
|
|
101
|
+
export function runTelegramSuperAgent(self, {
|
|
102
|
+
chat_id, prompt, previousMessages, target, author, relationshipBlock,
|
|
103
|
+
allowedTools, contextNote, signal, onEvent,
|
|
104
|
+
}) {
|
|
105
|
+
const confirmAdapter = createTelegramConfirmAdapter({
|
|
106
|
+
token: resolveBotToken(self.channel),
|
|
107
|
+
chatId: chat_id,
|
|
108
|
+
pendingStore: getConfirmStore(),
|
|
109
|
+
});
|
|
110
|
+
return runSuperAgent({
|
|
111
|
+
globalConfig: self.globalConfig,
|
|
112
|
+
projects: self.projects,
|
|
113
|
+
plugins: self.plugins,
|
|
114
|
+
registries: self.registries,
|
|
115
|
+
prompt,
|
|
116
|
+
previousMessages,
|
|
117
|
+
channel: CHANNELS.TELEGRAM,
|
|
118
|
+
relationshipBlock,
|
|
119
|
+
allowedTools,
|
|
120
|
+
contextNote: contextNote || undefined,
|
|
121
|
+
channelMeta: buildTelegramMeta({
|
|
122
|
+
channelName: self.channel.name,
|
|
123
|
+
author,
|
|
124
|
+
chatId: chat_id,
|
|
125
|
+
target,
|
|
126
|
+
routeToAgent: self.channel.route_to_agent,
|
|
127
|
+
}),
|
|
128
|
+
signal,
|
|
129
|
+
onEvent,
|
|
130
|
+
requestConfirmation: confirmAdapter.requestConfirmation,
|
|
131
|
+
// Autonomy budget: Telegram is the "do the whole task for me" surface, so it
|
|
132
|
+
// gets a real multi-step budget instead of the conversational default (which
|
|
133
|
+
// cut tasks off after ~9 actions to ask "continue?"). Tunable via
|
|
134
|
+
// config.super_agent.telegram_max_iters.
|
|
135
|
+
maxIters: Number(self.globalConfig?.super_agent?.telegram_max_iters) || TELEGRAM_TOOL_ITERS,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Localized "couldn't reply" text for a failed super-agent turn (model itself
|
|
140
|
+
* failed, so it can't author this — templated, but follows the user's language). */
|
|
141
|
+
export function telegramErrorText(self, e) {
|
|
142
|
+
return t("telegram.error_generic", {
|
|
143
|
+
lang: resolveLang(self.globalConfig),
|
|
144
|
+
vars: { error: e?.message || "internal error" },
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Send the final reply for a turn and log it. The intermediate prose was already
|
|
150
|
+
* streamed, so we only send `replyText` if it's non-empty AND not a duplicate of
|
|
151
|
+
* the last streamed piece. Never ends on silence: a turn that streamed/acted but
|
|
152
|
+
* produced no closing gets a neutral "continue?"; a pure chit-chat turn that did
|
|
153
|
+
* nothing gets a short ack. Caller stops the typing indicator before calling.
|
|
154
|
+
*/
|
|
155
|
+
export async function sendFinalReply(self, {
|
|
156
|
+
chat_id, update_id, replyText, replyAuthor, replyActorId, replyKind,
|
|
157
|
+
saUsage = null, streamedCount = 0, lastStreamedText = "", agentDisplay,
|
|
158
|
+
extraMeta = {},
|
|
159
|
+
}) {
|
|
160
|
+
const finalClean = replyText ? stripThinking(replyText).trim() : "";
|
|
161
|
+
let toSend = "";
|
|
162
|
+
if (finalClean && finalClean !== lastStreamedText) {
|
|
163
|
+
toSend = finalClean;
|
|
164
|
+
} else if (!finalClean) {
|
|
165
|
+
const lang = resolveLang(self.globalConfig);
|
|
166
|
+
toSend = streamedCount === 0
|
|
167
|
+
? t("telegram.fallback_listo", { lang })
|
|
168
|
+
: t("telegram.fallback_continue", { lang });
|
|
169
|
+
}
|
|
170
|
+
if (!toSend) return; // everything was already streamed — nothing left to send
|
|
171
|
+
|
|
172
|
+
const actorId = replyActorId || SUPERAGENT_ACTOR_ID;
|
|
173
|
+
const kind = replyKind || "superagent";
|
|
174
|
+
try {
|
|
175
|
+
await self._send({ chat_id, text: toSend });
|
|
176
|
+
const meta = { chat_id, tg_channel: self.channel.name, in_reply_to: update_id, final: true, ...extraMeta };
|
|
177
|
+
if (replyText && stripThinking(replyText) !== replyText) meta.thinking_stripped = true;
|
|
178
|
+
if (saUsage) meta.usage = saUsage;
|
|
179
|
+
appendGlobalMessage({
|
|
180
|
+
channel: CHANNELS.TELEGRAM,
|
|
181
|
+
direction: "out",
|
|
182
|
+
type: "agent",
|
|
183
|
+
actor_id: actorId,
|
|
184
|
+
actor_kind: kind,
|
|
185
|
+
agent_slug: actorId,
|
|
186
|
+
author: replyAuthor || agentDisplay,
|
|
187
|
+
body: toSend,
|
|
188
|
+
meta,
|
|
189
|
+
});
|
|
190
|
+
} catch (e) {
|
|
191
|
+
self.log(`telegram[${self.channel.name}] send-back error: ${e.message}`);
|
|
192
|
+
appendGlobalMessage({
|
|
193
|
+
channel: CHANNELS.TELEGRAM,
|
|
194
|
+
direction: "out",
|
|
195
|
+
type: "agent",
|
|
196
|
+
actor_id: actorId,
|
|
197
|
+
actor_kind: kind,
|
|
198
|
+
agent_slug: actorId,
|
|
199
|
+
author: replyAuthor || agentDisplay,
|
|
200
|
+
body: `[send_failed] ${toSend}`,
|
|
201
|
+
meta: { chat_id, tg_channel: self.channel.name, in_reply_to: update_id, send_error: e.message, ...(saUsage ? { usage: saUsage } : {}) },
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
package/src/core/config/index.js
CHANGED
|
@@ -57,6 +57,11 @@ const DEFAULT_CONFIG = {
|
|
|
57
57
|
system: "", // optional override; defaults in src/core/agent/prompts/
|
|
58
58
|
permission_mode: PERMISSION_MODES.AUTOMATICO, // total | automatico | permiso
|
|
59
59
|
allowed_tools: [], // used by permission_mode="permiso"
|
|
60
|
+
// Per-turn tool-loop budget for the Telegram super-agent. Higher = more
|
|
61
|
+
// autonomous (chains explore→edit→verify→close before replying); lower =
|
|
62
|
+
// snappier but more "want me to continue?" hand-backs. 0/unset → built-in
|
|
63
|
+
// default (TELEGRAM_TOOL_ITERS in src/core/agent/constants.js).
|
|
64
|
+
telegram_max_iters: 0,
|
|
60
65
|
// Model fallback: ordered list. Each item carries its own provider
|
|
61
66
|
// prefix; the array order IS the attempt order. The router tries the
|
|
62
67
|
// primary (super_agent.model) first, then walks this list, skipping
|
package/src/core/i18n/en.js
CHANGED
|
@@ -4,4 +4,8 @@ export default {
|
|
|
4
4
|
"telegram.reset_ack": "Done, context cleared. Starting fresh. What do you need?",
|
|
5
5
|
"telegram.fallback_listo": "Done.",
|
|
6
6
|
"telegram.fallback_continue": "Made some headway. Want me to keep going?",
|
|
7
|
+
// Host-emitted error floors (the model itself failed, so it can't author
|
|
8
|
+
// these — they stay templated, but at least follow the user's language).
|
|
9
|
+
"telegram.error_agent": "⚠️ The agent hit an error ({error}).",
|
|
10
|
+
"telegram.error_generic": "⚠️ Couldn't reply right now ({error}).",
|
|
7
11
|
};
|
package/src/core/i18n/es.js
CHANGED
|
@@ -6,4 +6,8 @@ export default {
|
|
|
6
6
|
"telegram.reset_ack": "Listo, contexto borrado. Arranco un hilo nuevo, ¿qué necesitás?",
|
|
7
7
|
"telegram.fallback_listo": "Listo.",
|
|
8
8
|
"telegram.fallback_continue": "Avancé con eso. ¿Querés que siga?",
|
|
9
|
+
// Pisos de error emitidos por el host (el modelo falló, no puede redactarlos
|
|
10
|
+
// él mismo — quedan fijos, pero al menos respetan el idioma del usuario).
|
|
11
|
+
"telegram.error_agent": "⚠️ El agente tuvo un error ({error}).",
|
|
12
|
+
"telegram.error_generic": "⚠️ No pude responder ahora mismo ({error}).",
|
|
9
13
|
};
|
package/src/core/i18n/pt.js
CHANGED
|
@@ -4,4 +4,8 @@ export default {
|
|
|
4
4
|
"telegram.reset_ack": "Pronto, contexto limpo. Começando do zero — do que você precisa?",
|
|
5
5
|
"telegram.fallback_listo": "Pronto.",
|
|
6
6
|
"telegram.fallback_continue": "Avancei com isso. Quer que eu continue?",
|
|
7
|
+
// Pisos de erro emitidos pelo host (o modelo falhou, não pode redigi-los —
|
|
8
|
+
// ficam fixos, mas ao menos seguem o idioma do usuário).
|
|
9
|
+
"telegram.error_agent": "⚠️ O agente encontrou um erro ({error}).",
|
|
10
|
+
"telegram.error_generic": "⚠️ Não consegui responder agora ({error}).",
|
|
7
11
|
};
|
|
@@ -27,23 +27,15 @@
|
|
|
27
27
|
// "poll_interval_ms": 1500
|
|
28
28
|
// }
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
import { stripThinking } from "#core/util/thinking.js";
|
|
30
|
+
// This poller is intentionally thin: per-update logic lives in core/channels/
|
|
31
|
+
// telegram/ (dispatch + reply + ask + inbound). It keeps only what the *running
|
|
32
|
+
// process* needs — lifecycle, the poll loop, offset state and the inline-keyboard
|
|
33
|
+
// callbacks. The earlier dispatch extraction left a pile of now-dead imports
|
|
34
|
+
// here; only what's actually referenced below remains.
|
|
36
35
|
import { getRecentTelegramTurnsFromFs, appendGlobalMessage } from "#core/stores/messages.js";
|
|
37
|
-
import { compactChannelIfNeeded } from "#core/memory/index.js";
|
|
38
|
-
import { readAgents } from "#core/apc/parser.js";
|
|
39
|
-
import { buildAgentSystem } from "#core/agent/build-agent-system.js";
|
|
40
|
-
import { transcribe as transcribeAudioFile } from "#core/voice/transcription.js";
|
|
41
36
|
import { resolveAgentName, SUPERAGENT_ACTOR_ID } from "#core/identity/index.js";
|
|
42
|
-
import { registerSender, resolveAllowedTools } from "#core/identity/telegram.js";
|
|
43
|
-
import { buildRelationshipBlock } from "#core/agent/index.js";
|
|
44
37
|
import { getConfirmationStore as getConfirmStore } from "#core/confirmation/pending-store.js";
|
|
45
38
|
import { CHANNELS } from "#core/constants/channels.js";
|
|
46
|
-
import { tryResolveSkillCommand } from "#core/agent/skills/trigger.js";
|
|
47
39
|
import { createTelegramConfirmAdapter } from "#core/confirmation/adapters/telegram.js";
|
|
48
40
|
import * as askFlow from "#core/channels/telegram/ask.js";
|
|
49
41
|
|
|
@@ -53,7 +45,6 @@ const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
|
53
45
|
// All non-class-bound channel logic lives in core/channels/telegram/ — this
|
|
54
46
|
// file stays focused on the poller class + plugin lifecycle wiring.
|
|
55
47
|
import {
|
|
56
|
-
buildTelegramMeta,
|
|
57
48
|
loadState,
|
|
58
49
|
saveState,
|
|
59
50
|
resolveBotToken,
|
|
@@ -63,9 +54,10 @@ import {
|
|
|
63
54
|
sleep,
|
|
64
55
|
} from "#core/channels/telegram/helpers.js";
|
|
65
56
|
import { handleUpdate } from "#core/channels/telegram/dispatch.js";
|
|
57
|
+
import { buildStreamHandler, runTelegramSuperAgent, telegramErrorText, sendFinalReply } from "#core/channels/telegram/reply.js";
|
|
66
58
|
|
|
67
59
|
// ---------- media sending helpers (re-exports) ------------------------------
|
|
68
|
-
import { sendPhoto, sendVoice, sendDocument, sendAudio,
|
|
60
|
+
import { sendPhoto, sendVoice, sendDocument, sendAudio, API_BASE } from "#core/channels/telegram/media.js";
|
|
69
61
|
export { sendPhoto, sendVoice, sendDocument, sendAudio };
|
|
70
62
|
|
|
71
63
|
// ---------- per-channel poller ----------------------------------------------
|
|
@@ -328,9 +320,9 @@ class ChannelPoller {
|
|
|
328
320
|
}
|
|
329
321
|
|
|
330
322
|
// Run a follow-up super-agent turn with the compiled answers as the user
|
|
331
|
-
// prompt.
|
|
332
|
-
//
|
|
333
|
-
// model decides to ask again.
|
|
323
|
+
// prompt. Shares the exact reply path as a normal inbound turn (core/channels/
|
|
324
|
+
// telegram/reply.js) — only the photo/audio/reset preamble is skipped.
|
|
325
|
+
// Re-enters the ask flow if the model decides to ask again.
|
|
334
326
|
async _runResumedTurn(ctx) {
|
|
335
327
|
const { chat_id, compiled, target, relationshipBlock, allowedTools, author, agentDisplay, update_id, sender, authorId } = ctx;
|
|
336
328
|
if (!chat_id) return;
|
|
@@ -359,25 +351,32 @@ class ChannelPoller {
|
|
|
359
351
|
max_age_hours: 24,
|
|
360
352
|
});
|
|
361
353
|
|
|
354
|
+
// Drive the resume through the SAME shared reply path as a normal inbound
|
|
355
|
+
// turn (see core/channels/telegram/reply.js): streaming, the autonomy budget
|
|
356
|
+
// (maxIters), the never-silent floor, localized errors and rich channelMeta.
|
|
357
|
+
// This used to be a hand-rolled copy that silently lagged behind the main
|
|
358
|
+
// path — now there's one source of truth.
|
|
359
|
+
const { onEvent, state } = buildStreamHandler(this, { chat_id, update_id, agentDisplay });
|
|
362
360
|
const stopTyping = this._startTyping(chat_id);
|
|
361
|
+
let replyText;
|
|
362
|
+
let replyAuthor;
|
|
363
|
+
let saUsage = null;
|
|
363
364
|
try {
|
|
364
|
-
const sa = await
|
|
365
|
-
|
|
366
|
-
projects: this.projects,
|
|
367
|
-
plugins: this.plugins,
|
|
368
|
-
registries: this.registries,
|
|
365
|
+
const sa = await runTelegramSuperAgent(this, {
|
|
366
|
+
chat_id,
|
|
369
367
|
prompt: compiled,
|
|
370
368
|
previousMessages,
|
|
371
|
-
|
|
369
|
+
target,
|
|
370
|
+
author,
|
|
372
371
|
relationshipBlock,
|
|
373
372
|
allowedTools,
|
|
374
|
-
|
|
373
|
+
onEvent,
|
|
375
374
|
});
|
|
376
|
-
stopTyping();
|
|
377
375
|
|
|
378
376
|
// Did the model ask again? Restart the flow instead of replying.
|
|
379
377
|
const followupAsk = askFlow.extractAskQuestionsFromTrace(sa.trace);
|
|
380
378
|
if (followupAsk) {
|
|
379
|
+
stopTyping();
|
|
381
380
|
await this._startAskFlow({
|
|
382
381
|
chat_id,
|
|
383
382
|
projectId: target?.id,
|
|
@@ -393,36 +392,29 @@ class ChannelPoller {
|
|
|
393
392
|
});
|
|
394
393
|
return;
|
|
395
394
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
await this._send({ chat_id, text: replyText });
|
|
400
|
-
appendGlobalMessage({
|
|
401
|
-
channel: CHANNELS.TELEGRAM,
|
|
402
|
-
direction: "out",
|
|
403
|
-
type: "agent",
|
|
404
|
-
actor_id: SUPERAGENT_ACTOR_ID,
|
|
405
|
-
actor_kind: "superagent",
|
|
406
|
-
agent_slug: SUPERAGENT_ACTOR_ID,
|
|
407
|
-
author: sa.name || agentDisplay,
|
|
408
|
-
body: replyText,
|
|
409
|
-
meta: {
|
|
410
|
-
chat_id,
|
|
411
|
-
tg_channel: this.channel.name,
|
|
412
|
-
in_reply_to: update_id,
|
|
413
|
-
final: true,
|
|
414
|
-
ask_resume: true,
|
|
415
|
-
...(sa.usage ? { usage: sa.usage } : {}),
|
|
416
|
-
},
|
|
417
|
-
});
|
|
418
|
-
}
|
|
395
|
+
replyText = sa.text;
|
|
396
|
+
replyAuthor = sa.name || agentDisplay;
|
|
397
|
+
saUsage = sa.usage;
|
|
419
398
|
} catch (e) {
|
|
420
|
-
stopTyping();
|
|
421
399
|
this.log(`telegram[${this.channel.name}] ask resume failed: ${e.message}`);
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
} catch { /* best-effort */ }
|
|
400
|
+
replyText = telegramErrorText(this, e);
|
|
401
|
+
replyAuthor = agentDisplay;
|
|
425
402
|
}
|
|
403
|
+
|
|
404
|
+
stopTyping();
|
|
405
|
+
await sendFinalReply(this, {
|
|
406
|
+
chat_id,
|
|
407
|
+
update_id,
|
|
408
|
+
replyText,
|
|
409
|
+
replyAuthor,
|
|
410
|
+
replyActorId: SUPERAGENT_ACTOR_ID,
|
|
411
|
+
replyKind: "superagent",
|
|
412
|
+
saUsage,
|
|
413
|
+
streamedCount: state.streamedCount,
|
|
414
|
+
lastStreamedText: state.lastStreamedText,
|
|
415
|
+
agentDisplay,
|
|
416
|
+
extraMeta: { ask_resume: true },
|
|
417
|
+
});
|
|
426
418
|
}
|
|
427
419
|
|
|
428
420
|
// Show "typing..." indicator in the chat. Telegram clears it automatically
|