@agentprojectcontext/apx 1.42.0 → 1.42.2
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/api.js +62 -0
- package/src/core/channels/telegram/ask-callbacks.js +238 -0
- 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/confirmation/adapters/telegram.js +20 -37
- 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/desktop/index.js +6 -1
- package/src/host/daemon/plugins/telegram/index.js +62 -360
- package/src/interfaces/web/package-lock.json +3 -3
|
@@ -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
|
|
@@ -24,7 +24,10 @@
|
|
|
24
24
|
// keyboard but before the user tapped, pendingStore.wasKnown() detects the
|
|
25
25
|
// SQLite row with no memory entry and we show "Expirado" instead of an error.
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
// Raw Bot API calls go through the shared client so endpoint boilerplate lives
|
|
28
|
+
// in one place (these used to be hand-rolled fetch calls duplicated here).
|
|
29
|
+
import { sendMessage, answerCallbackQuery as apiAnswerCallbackQuery, editMessageReplyMarkup } from "#core/channels/telegram/api.js";
|
|
30
|
+
|
|
28
31
|
const TIMEOUT_MS = 60_000; // 60 s — long enough for a human, short enough to not block forever
|
|
29
32
|
|
|
30
33
|
/**
|
|
@@ -81,51 +84,31 @@ export function createTelegramConfirmAdapter({ token, chatId, pendingStore }) {
|
|
|
81
84
|
|
|
82
85
|
async function sendConfirmKeyboard(token, chatId, description, correlationId, timeoutMs) {
|
|
83
86
|
const timeoutSec = Math.round(timeoutMs / 1000);
|
|
84
|
-
await
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
{ text: "✅ Yes", callback_data: `apx:confirm:${correlationId}:yes` },
|
|
96
|
-
{ text: "❌ No", callback_data: `apx:confirm:${correlationId}:no` },
|
|
97
|
-
]],
|
|
98
|
-
},
|
|
99
|
-
}),
|
|
87
|
+
await sendMessage(token, chatId, {
|
|
88
|
+
text:
|
|
89
|
+
`⚠️ *Confirm action*\n\n${escapeMarkdown(description)}\n\n` +
|
|
90
|
+
`_Expires in ${timeoutSec}s. No response → cancelled._`,
|
|
91
|
+
parse_mode: "Markdown",
|
|
92
|
+
reply_markup: {
|
|
93
|
+
inline_keyboard: [[
|
|
94
|
+
{ text: "✅ Yes", callback_data: `apx:confirm:${correlationId}:yes` },
|
|
95
|
+
{ text: "❌ No", callback_data: `apx:confirm:${correlationId}:no` },
|
|
96
|
+
]],
|
|
97
|
+
},
|
|
100
98
|
});
|
|
101
99
|
}
|
|
102
100
|
|
|
101
|
+
// best-effort — Telegram gives only ~30s to answer; after that it's already cleared
|
|
103
102
|
async function answerCallbackQuery(token, callbackQueryId, text) {
|
|
104
103
|
try {
|
|
105
|
-
await
|
|
106
|
-
|
|
107
|
-
headers: { "content-type": "application/json" },
|
|
108
|
-
body: JSON.stringify({ callback_query_id: callbackQueryId, text }),
|
|
109
|
-
});
|
|
110
|
-
} catch {
|
|
111
|
-
// best-effort — Telegram gives only 30s to answer; after that it's already cleared
|
|
112
|
-
}
|
|
104
|
+
await apiAnswerCallbackQuery(token, callbackQueryId, text);
|
|
105
|
+
} catch { /* best-effort */ }
|
|
113
106
|
}
|
|
114
107
|
|
|
115
108
|
async function editMessageButtons(token, chatId, messageId, inlineKeyboard) {
|
|
116
109
|
try {
|
|
117
|
-
await
|
|
118
|
-
|
|
119
|
-
headers: { "content-type": "application/json" },
|
|
120
|
-
body: JSON.stringify({
|
|
121
|
-
chat_id: chatId,
|
|
122
|
-
message_id: messageId,
|
|
123
|
-
reply_markup: { inline_keyboard: inlineKeyboard },
|
|
124
|
-
}),
|
|
125
|
-
});
|
|
126
|
-
} catch {
|
|
127
|
-
// best-effort
|
|
128
|
-
}
|
|
110
|
+
await editMessageReplyMarkup(token, chatId, messageId, { inline_keyboard: inlineKeyboard });
|
|
111
|
+
} catch { /* best-effort */ }
|
|
129
112
|
}
|
|
130
113
|
|
|
131
114
|
// Escape Markdown special chars so description text doesn't break Telegram markup.
|
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
|
};
|
|
@@ -140,7 +140,12 @@ async function _handleMessage({ ws, text, previousMessages }, { projects, config
|
|
|
140
140
|
channel: CHANNELS.DESKTOP,
|
|
141
141
|
...(slashed.handled ? { contextNote: slashed.contextNote } : {}),
|
|
142
142
|
channelMeta: { voice: true }, // desktop module is voice-first → spoken mode
|
|
143
|
-
|
|
143
|
+
// WS path: history was just appended with the current user turn (line 87),
|
|
144
|
+
// so drop it. HTTP path: `previousMessages` came in already excluding the
|
|
145
|
+
// current user turn (the renderer slices it off before POSTing), so
|
|
146
|
+
// dropping again would silently strip the last assistant reply — making
|
|
147
|
+
// every turn look like a fresh conversation to the model.
|
|
148
|
+
previousMessages: ws ? history.slice(0, -1) : history,
|
|
144
149
|
overrideModel: cfg.model || null,
|
|
145
150
|
signal: controller.signal,
|
|
146
151
|
onToken: (chunk) => { liveBuf += chunk; },
|