@mono-agent/agent-app 0.12.0 → 0.14.0
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/README.md +190 -33
- package/dist/adapter-send-tools-main.js +0 -1
- package/dist/adapter-send-tools-main.js.map +1 -1
- package/dist/adapter-send-tools.d.ts +8 -5
- package/dist/adapter-send-tools.d.ts.map +1 -1
- package/dist/adapter-send-tools.js +195 -257
- package/dist/adapter-send-tools.js.map +1 -1
- package/dist/app-controller-channels.d.ts.map +1 -1
- package/dist/app-controller-channels.js +2 -1
- package/dist/app-controller-channels.js.map +1 -1
- package/dist/app-controller-continuation.d.ts.map +1 -1
- package/dist/app-controller-continuation.js +1 -5
- package/dist/app-controller-continuation.js.map +1 -1
- package/dist/app-controller-lifecycle.d.ts.map +1 -1
- package/dist/app-controller-lifecycle.js +2 -6
- package/dist/app-controller-lifecycle.js.map +1 -1
- package/dist/app-controller-maintenance.d.ts +2 -1
- package/dist/app-controller-maintenance.d.ts.map +1 -1
- package/dist/app-controller-maintenance.js +66 -2
- package/dist/app-controller-maintenance.js.map +1 -1
- package/dist/app-controller-memory-health.d.ts +1 -1
- package/dist/app-controller-memory-health.d.ts.map +1 -1
- package/dist/app-controller-memory-health.js +5 -1
- package/dist/app-controller-memory-health.js.map +1 -1
- package/dist/app-controller-responder.d.ts.map +1 -1
- package/dist/app-controller-responder.js +12 -4
- package/dist/app-controller-responder.js.map +1 -1
- package/dist/app-controller-traceability.d.ts.map +1 -1
- package/dist/app-controller-traceability.js +15 -1
- package/dist/app-controller-traceability.js.map +1 -1
- package/dist/app-controller-utils.js +1 -1
- package/dist/app-controller-utils.js.map +1 -1
- package/dist/app-controller.d.ts +14 -1
- package/dist/app-controller.d.ts.map +1 -1
- package/dist/app-controller.js +61 -9
- package/dist/app-controller.js.map +1 -1
- package/dist/audit-runs.d.ts.map +1 -1
- package/dist/audit-runs.js +12 -2
- package/dist/audit-runs.js.map +1 -1
- package/dist/background-environment.d.ts +1 -0
- package/dist/background-environment.d.ts.map +1 -1
- package/dist/background-environment.js +1 -0
- package/dist/background-environment.js.map +1 -1
- package/dist/background-runtime.d.ts +39 -2
- package/dist/background-runtime.d.ts.map +1 -1
- package/dist/background-runtime.js +901 -49
- package/dist/background-runtime.js.map +1 -1
- package/dist/background.d.ts +36 -3
- package/dist/background.d.ts.map +1 -1
- package/dist/background.js +404 -57
- package/dist/background.js.map +1 -1
- package/dist/channel-drivers/cron.js +8 -1
- package/dist/channel-drivers/cron.js.map +1 -1
- package/dist/channel-drivers/live.d.ts +3 -2
- package/dist/channel-drivers/live.d.ts.map +1 -1
- package/dist/channel-drivers/live.js +3 -2
- package/dist/channel-drivers/live.js.map +1 -1
- package/dist/channel-drivers/native-notify.d.ts +2 -0
- package/dist/channel-drivers/native-notify.d.ts.map +1 -1
- package/dist/channel-drivers/native-notify.js +14 -2
- package/dist/channel-drivers/native-notify.js.map +1 -1
- package/dist/channel-drivers/slack.d.ts.map +1 -1
- package/dist/channel-drivers/slack.js +55 -0
- package/dist/channel-drivers/slack.js.map +1 -1
- package/dist/channel-drivers/telegram.d.ts.map +1 -1
- package/dist/channel-drivers/telegram.js +19 -14
- package/dist/channel-drivers/telegram.js.map +1 -1
- package/dist/channel-drivers/tui.d.ts.map +1 -1
- package/dist/channel-drivers/tui.js +8 -0
- package/dist/channel-drivers/tui.js.map +1 -1
- package/dist/channel-runtime-controls.d.ts +23 -0
- package/dist/channel-runtime-controls.d.ts.map +1 -0
- package/dist/channel-runtime-controls.js +63 -0
- package/dist/channel-runtime-controls.js.map +1 -0
- package/dist/channel-status-display.d.ts +15 -0
- package/dist/channel-status-display.d.ts.map +1 -0
- package/dist/channel-status-display.js +48 -0
- package/dist/channel-status-display.js.map +1 -0
- package/dist/cli-args.d.ts +23 -22
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +136 -83
- package/dist/cli-args.js.map +1 -1
- package/dist/cli-background-command.d.ts +1 -0
- package/dist/cli-background-command.d.ts.map +1 -1
- package/dist/cli-background-command.js +158 -27
- package/dist/cli-background-command.js.map +1 -1
- package/dist/cli-commands.d.ts +2 -2
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +27 -59
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli-help.d.ts +19 -1
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +203 -67
- package/dist/cli-help.js.map +1 -1
- package/dist/cli-init-command.d.ts.map +1 -1
- package/dist/cli-init-command.js +4 -18
- package/dist/cli-init-command.js.map +1 -1
- package/dist/cli-install-skill-command.d.ts.map +1 -1
- package/dist/cli-install-skill-command.js +32 -3
- package/dist/cli-install-skill-command.js.map +1 -1
- package/dist/cli-runs-command.d.ts +10 -0
- package/dist/cli-runs-command.d.ts.map +1 -0
- package/dist/cli-runs-command.js +66 -0
- package/dist/cli-runs-command.js.map +1 -0
- package/dist/cli-sandbox-command.d.ts +1 -1
- package/dist/cli-sandbox-command.d.ts.map +1 -1
- package/dist/cli-sandbox-command.js +14 -2
- package/dist/cli-sandbox-command.js.map +1 -1
- package/dist/cli-validate-config-command.d.ts +24 -5
- package/dist/cli-validate-config-command.d.ts.map +1 -1
- package/dist/cli-validate-config-command.js +120 -41
- package/dist/cli-validate-config-command.js.map +1 -1
- package/dist/config-reference.d.ts.map +1 -1
- package/dist/config-reference.js +4 -5
- package/dist/config-reference.js.map +1 -1
- package/dist/docs-mcp-pairing.d.ts +35 -0
- package/dist/docs-mcp-pairing.d.ts.map +1 -0
- package/dist/docs-mcp-pairing.js +311 -0
- package/dist/docs-mcp-pairing.js.map +1 -0
- package/dist/doctor.d.ts +6 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +4 -5
- package/dist/doctor.js.map +1 -1
- package/dist/install-skill.d.ts +2 -0
- package/dist/install-skill.d.ts.map +1 -1
- package/dist/install-skill.js +10 -4
- package/dist/install-skill.js.map +1 -1
- package/dist/interaction-bridge.d.ts +5 -20
- package/dist/interaction-bridge.d.ts.map +1 -1
- package/dist/interaction-bridge.js +255 -100
- package/dist/interaction-bridge.js.map +1 -1
- package/dist/launchd.d.ts +35 -1
- package/dist/launchd.d.ts.map +1 -1
- package/dist/launchd.js +131 -2
- package/dist/launchd.js.map +1 -1
- package/dist/managed-runtime-publication.d.ts +22 -0
- package/dist/managed-runtime-publication.d.ts.map +1 -0
- package/dist/managed-runtime-publication.js +91 -0
- package/dist/managed-runtime-publication.js.map +1 -0
- package/dist/managed-startup.d.ts +8 -0
- package/dist/managed-startup.d.ts.map +1 -0
- package/dist/managed-startup.js +12 -0
- package/dist/managed-startup.js.map +1 -0
- package/dist/memory-retrieval.d.ts.map +1 -1
- package/dist/memory-retrieval.js +31 -18
- package/dist/memory-retrieval.js.map +1 -1
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +12 -2
- package/dist/metrics.js.map +1 -1
- package/dist/modules/catalog.js +1 -1
- package/dist/modules/catalog.js.map +1 -1
- package/dist/modules/known-tools.d.ts +1 -1
- package/dist/modules/known-tools.d.ts.map +1 -1
- package/dist/modules/known-tools.js +0 -3
- package/dist/modules/known-tools.js.map +1 -1
- package/dist/posted-reply-history.d.ts.map +1 -1
- package/dist/posted-reply-history.js +7 -0
- package/dist/posted-reply-history.js.map +1 -1
- package/dist/request-model-override.d.ts +2 -2
- package/dist/request-model-override.js +8 -4
- package/dist/request-model-override.js.map +1 -1
- package/dist/run-history.d.ts +6 -2
- package/dist/run-history.d.ts.map +1 -1
- package/dist/run-history.js +511 -118
- package/dist/run-history.js.map +1 -1
- package/dist/runtime-provenance.d.ts.map +1 -1
- package/dist/runtime-provenance.js +10 -5
- package/dist/runtime-provenance.js.map +1 -1
- package/dist/tui-command.d.ts.map +1 -1
- package/dist/tui-command.js +2 -1
- package/dist/tui-command.js.map +1 -1
- package/dist/ui.js +1 -1
- package/dist/ui.js.map +1 -1
- package/dist/wizard/from-flags.d.ts +0 -1
- package/dist/wizard/from-flags.d.ts.map +1 -1
- package/dist/wizard/from-flags.js +1 -8
- package/dist/wizard/from-flags.js.map +1 -1
- package/dist/wizard/index.d.ts +1 -1
- package/dist/wizard/index.d.ts.map +1 -1
- package/dist/wizard/index.js +1 -1
- package/dist/wizard/index.js.map +1 -1
- package/dist/wizard/presets.d.ts +3 -12
- package/dist/wizard/presets.d.ts.map +1 -1
- package/dist/wizard/presets.js +3 -15
- package/dist/wizard/presets.js.map +1 -1
- package/dist/wizard/prompts.d.ts.map +1 -1
- package/dist/wizard/prompts.js +3 -4
- package/dist/wizard/prompts.js.map +1 -1
- package/dist/wizard/run.js +3 -3
- package/dist/wizard/run.js.map +1 -1
- package/package.json +15 -16
- package/schema/mono-agent.config.schema.json +4 -4
- package/skills/mono-agent-composer/SKILL.md +13 -5
- package/skills/mono-agent-composer/agents/openai.yaml +1 -1
- package/skills/mono-agent-composer/references/config-blueprint.md +18 -10
- package/skills/mono-agent-composer/references/discovery-questions.md +8 -5
- package/skills/mono-agent-composer/references/feature-coverage.md +10 -9
- package/skills/mono-agent-composer/references/package-map.md +10 -4
- package/skills/mono-agent-composer/references/playbooks.md +6 -6
- package/skills/mono-agent-composer/references/validation.md +24 -2
|
@@ -29,13 +29,8 @@ const ASK_BRIDGE_CLEANUP_TIMEOUT_MS = 1_000;
|
|
|
29
29
|
export const ADAPTER_SEND_TOOLS_MCP_SERVER_NAME = "mono-agent-adapter-send";
|
|
30
30
|
export async function resolveAdapterSendToolsSettings(input, options = {}) {
|
|
31
31
|
const telegramSendAllowed = isAdapterToolAllowed("TelegramSendMessage", options);
|
|
32
|
-
const telegramAskAllowed = options.suppressInteractionTools !== true
|
|
33
|
-
&& isAdapterToolAllowed("TelegramAskButtons", options);
|
|
34
32
|
const telegramFileAllowed = isAdapterToolAllowed("TelegramSendFile", options);
|
|
35
|
-
const telegramAnyAllowed = telegramSendAllowed ||
|
|
36
|
-
const telegramAskBridge = telegramAskAllowed
|
|
37
|
-
? resolveAskUserToolSettings(input.env, options.interaction)
|
|
38
|
-
: undefined;
|
|
33
|
+
const telegramAnyAllowed = telegramSendAllowed || telegramFileAllowed;
|
|
39
34
|
const [slack, telegram] = await Promise.all([
|
|
40
35
|
isAdapterToolAllowed("SlackSendMessage", options)
|
|
41
36
|
? resolveSlackSendToolSettings(input, options)
|
|
@@ -43,9 +38,8 @@ export async function resolveAdapterSendToolsSettings(input, options = {}) {
|
|
|
43
38
|
telegramAnyAllowed
|
|
44
39
|
? resolveTelegramSendToolSettings(input, options, {
|
|
45
40
|
send: telegramSendAllowed,
|
|
46
|
-
ask: telegramAskAllowed,
|
|
47
41
|
file: telegramFileAllowed,
|
|
48
|
-
}
|
|
42
|
+
})
|
|
49
43
|
: undefined,
|
|
50
44
|
]);
|
|
51
45
|
const askUser = options.suppressInteractionTools !== true && isAdapterToolAllowed("AskUser", options)
|
|
@@ -74,13 +68,15 @@ function resolveAskUserToolSettings(env, interaction) {
|
|
|
74
68
|
const timeoutRaw = Number(optionalString(env.MONO_AGENT_ASK_USER_TIMEOUT_MS));
|
|
75
69
|
const timeoutMs = interaction?.timeoutMs
|
|
76
70
|
?? (Number.isFinite(timeoutRaw) && timeoutRaw >= 1000 ? timeoutRaw : 600_000);
|
|
77
|
-
const
|
|
71
|
+
const producerConversationId = optionalString(env.MONO_AGENT_ADAPTER_TOOLS_PRODUCING_CONVERSATION_ID);
|
|
72
|
+
const interactionConversationId = optionalString(env.MONO_AGENT_ADAPTER_TOOLS_INTERACTION_CONVERSATION_ID);
|
|
78
73
|
const runId = optionalString(env.MONO_AGENT_ADAPTER_TOOLS_PRODUCING_RUN_ID);
|
|
79
74
|
return {
|
|
80
75
|
bridgeUrl,
|
|
81
76
|
bridgeToken,
|
|
82
77
|
timeoutMs,
|
|
83
|
-
...(
|
|
78
|
+
...(producerConversationId === undefined ? {} : { producerConversationId }),
|
|
79
|
+
...(interactionConversationId === undefined ? {} : { interactionConversationId }),
|
|
84
80
|
...(runId === undefined ? {} : { runId }),
|
|
85
81
|
};
|
|
86
82
|
}
|
|
@@ -92,9 +88,6 @@ export function adapterSendToolNames(settings) {
|
|
|
92
88
|
if (settings.telegram?.tools.send === true) {
|
|
93
89
|
names.push("TelegramSendMessage");
|
|
94
90
|
}
|
|
95
|
-
if (settings.telegram?.tools.ask === true) {
|
|
96
|
-
names.push("TelegramAskButtons");
|
|
97
|
-
}
|
|
98
91
|
if (settings.telegram?.tools.file === true) {
|
|
99
92
|
names.push("TelegramSendFile");
|
|
100
93
|
}
|
|
@@ -118,6 +111,9 @@ export function adapterSendToolsMcpEnv(configPath, allowedTools, context, intera
|
|
|
118
111
|
...(context?.conversationId === undefined
|
|
119
112
|
? {}
|
|
120
113
|
: { MONO_AGENT_ADAPTER_TOOLS_PRODUCING_CONVERSATION_ID: context.conversationId }),
|
|
114
|
+
...(context?.interactionConversationId === undefined
|
|
115
|
+
? {}
|
|
116
|
+
: { MONO_AGENT_ADAPTER_TOOLS_INTERACTION_CONVERSATION_ID: context.interactionConversationId }),
|
|
121
117
|
...(context?.runId === undefined
|
|
122
118
|
? {}
|
|
123
119
|
: { MONO_AGENT_ADAPTER_TOOLS_PRODUCING_RUN_ID: context.runId }),
|
|
@@ -199,6 +195,7 @@ export function adapterSendToolsMcpServerSpec(configPath, cwd, allowedTools, con
|
|
|
199
195
|
export function createAdapterSendToolsRuntimeExtension(configPath, cwd, allowedTools, indexPath, interaction, runOutputRoot, deliveryHistoryCapabilityIssuer) {
|
|
200
196
|
return async (input) => {
|
|
201
197
|
const conversationId = input?.request?.conversationId;
|
|
198
|
+
const interactionConversationId = input?.request?.replyTo?.conversationId ?? conversationId;
|
|
202
199
|
const runId = input?.runId;
|
|
203
200
|
const hasConversation = typeof conversationId === "string" && conversationId.trim().length > 0;
|
|
204
201
|
const hasRunId = typeof runId === "string" && runId.trim().length > 0;
|
|
@@ -224,6 +221,9 @@ export function createAdapterSendToolsRuntimeExtension(configPath, cwd, allowedT
|
|
|
224
221
|
const context = hasConversation || hasRunId
|
|
225
222
|
? {
|
|
226
223
|
...(hasConversation ? { conversationId } : {}),
|
|
224
|
+
...(typeof interactionConversationId === "string" && interactionConversationId.trim().length > 0
|
|
225
|
+
? { interactionConversationId }
|
|
226
|
+
: {}),
|
|
227
227
|
...(hasRunId ? { runId } : {}),
|
|
228
228
|
...(indexPath === undefined ? {} : { indexPath }),
|
|
229
229
|
...(runOutput === undefined
|
|
@@ -281,24 +281,24 @@ export async function createAdapterSendToolsServer(settings, clients, indexing,
|
|
|
281
281
|
registerSlackSendTool(server, settings.slack, clients.slack, adapter.formatMarkdownForSlack, indexing, options.deliveryHistory, options.fetchImpl ?? globalThis.fetch);
|
|
282
282
|
}
|
|
283
283
|
if (settings.telegram !== undefined && clients.telegram !== undefined) {
|
|
284
|
-
// Loaded once here (not per-register-call) because registerTelegramAskTool
|
|
285
|
-
// needs TELEGRAM_ASK_MAX_OPTIONS synchronously while building its zod
|
|
286
|
-
// schema, at registration time — not deferred into the request handler.
|
|
287
284
|
const adapter = await loadTelegramModule();
|
|
288
|
-
if (settings.telegram.tools.send) {
|
|
289
|
-
registerTelegramSendTool(server, settings.telegram, clients.telegram, options.deliveryHistory, options.fetchImpl ?? globalThis.fetch);
|
|
290
|
-
}
|
|
291
|
-
if (settings.telegram.tools.ask) {
|
|
292
|
-
registerTelegramAskTool(server, settings.telegram, clients.telegram, adapter, options.fetchImpl ?? globalThis.fetch);
|
|
285
|
+
if (settings.telegram.tools.send && clients.telegram.sendMessage !== undefined) {
|
|
286
|
+
registerTelegramSendTool(server, settings.telegram, { sendMessage: clients.telegram.sendMessage }, adapter, options.deliveryHistory, options.fetchImpl ?? globalThis.fetch);
|
|
293
287
|
}
|
|
294
|
-
if (settings.telegram.tools.file
|
|
288
|
+
if (settings.telegram.tools.file
|
|
289
|
+
&& (clients.telegram.sendDocument !== undefined || clients.telegram.sendPhoto !== undefined)) {
|
|
295
290
|
registerTelegramSendFileTool(server, settings.telegram, clients.telegram, adapter);
|
|
296
291
|
}
|
|
297
292
|
}
|
|
298
293
|
// AskUser needs a target conversation; the parent app process resolves the
|
|
299
294
|
// settings without one (for tool-name gating) and must not register the tool.
|
|
300
|
-
if (settings.askUser?.
|
|
301
|
-
|
|
295
|
+
if (settings.askUser?.producerConversationId !== undefined
|
|
296
|
+
&& settings.askUser.interactionConversationId !== undefined) {
|
|
297
|
+
registerAskUserTool(server, {
|
|
298
|
+
...settings.askUser,
|
|
299
|
+
producerConversationId: settings.askUser.producerConversationId,
|
|
300
|
+
interactionConversationId: settings.askUser.interactionConversationId,
|
|
301
|
+
}, options.fetchImpl ?? globalThis.fetch);
|
|
302
302
|
}
|
|
303
303
|
return server;
|
|
304
304
|
}
|
|
@@ -307,17 +307,26 @@ const ASK_USER_POLL_WAIT_MS = 20_000;
|
|
|
307
307
|
function registerAskUserTool(server, settings, fetchImpl) {
|
|
308
308
|
server.registerTool("AskUser", {
|
|
309
309
|
title: "Ask the user and wait",
|
|
310
|
-
description: "Ask the user
|
|
310
|
+
description: "Ask 1–5 related questions and WAIT for the user to answer them in the current conversation. Each question must offer 2–3 concise options with descriptions; the UI also permits a custom reply. Use multiSelect only when several choices may be combined. Put long decision context or a draft in message. A second concurrent AskUser call fails. If the wait expires, finish gracefully using the returned partial answers and state any assumptions.",
|
|
311
311
|
inputSchema: {
|
|
312
|
-
|
|
312
|
+
message: z.string().min(1).max(4_096).optional().describe("Optional context or draft shown above the questions."),
|
|
313
|
+
questions: z.array(z.object({
|
|
314
|
+
header: z.string().min(1).max(12).describe("Short section label, at most 12 characters."),
|
|
315
|
+
question: z.string().min(1).max(1_000).describe("The decision or information needed."),
|
|
316
|
+
options: z.array(z.object({
|
|
317
|
+
label: z.string().min(1).max(75).describe("Concise selectable answer."),
|
|
318
|
+
description: z.string().min(1).max(300).describe("What choosing this option means."),
|
|
319
|
+
})).min(2).max(3),
|
|
320
|
+
multiSelect: z.boolean().optional().describe("Allow selecting more than one proposed answer before Done."),
|
|
321
|
+
})).min(1).max(5),
|
|
313
322
|
},
|
|
314
323
|
}, async (args, extra) => {
|
|
315
324
|
const created = await askBridgeRequest(settings, fetchImpl, "POST", "/v1/asks", {
|
|
316
|
-
conversationId: settings.
|
|
317
|
-
producerConversationId: settings.
|
|
325
|
+
conversationId: settings.interactionConversationId,
|
|
326
|
+
producerConversationId: settings.producerConversationId,
|
|
318
327
|
runId: settings.runId,
|
|
319
|
-
|
|
320
|
-
|
|
328
|
+
message: args.message,
|
|
329
|
+
questions: args.questions,
|
|
321
330
|
timeoutMs: settings.timeoutMs,
|
|
322
331
|
}, extra.signal);
|
|
323
332
|
if (created.status === 409) {
|
|
@@ -326,44 +335,48 @@ function registerAskUserTool(server, settings, fetchImpl) {
|
|
|
326
335
|
if (created.status === 501) {
|
|
327
336
|
return askToolResult("This conversation's channel does not support interactive asks. Ask your question in your final reply instead.", { answered: false, reason: "unsupported_channel" });
|
|
328
337
|
}
|
|
329
|
-
if (created.status !== 201 || typeof created.body.
|
|
338
|
+
if (created.status !== 201 || typeof created.body.interactionId !== "string") {
|
|
330
339
|
throw new Error(`AskUser: the interaction bridge rejected the ask (HTTP ${String(created.status)}).`);
|
|
331
340
|
}
|
|
332
|
-
return await awaitBridgeAsk(settings, created.body.
|
|
341
|
+
return await awaitBridgeAsk(settings, created.body.interactionId, extra, fetchImpl, extra.signal);
|
|
333
342
|
});
|
|
334
343
|
}
|
|
335
|
-
function askToolResult(text, structured
|
|
344
|
+
function askToolResult(text, structured) {
|
|
336
345
|
return {
|
|
337
346
|
content: [{ type: "text", text }],
|
|
338
|
-
structuredContent: { ok: true, ...
|
|
347
|
+
structuredContent: { ok: true, ...structured },
|
|
339
348
|
};
|
|
340
349
|
}
|
|
341
|
-
async function awaitBridgeAsk(settings,
|
|
350
|
+
async function awaitBridgeAsk(settings, interactionId, extra, fetchImpl, signal) {
|
|
342
351
|
const startedMs = Date.now();
|
|
343
352
|
try {
|
|
344
353
|
for (;;) {
|
|
345
|
-
const poll = await askBridgeRequest(settings, fetchImpl, "GET", `/v1/asks/${encodeURIComponent(
|
|
354
|
+
const poll = await askBridgeRequest(settings, fetchImpl, "GET", `/v1/asks/${encodeURIComponent(interactionId)}?waitMs=${String(ASK_USER_POLL_WAIT_MS)}`, undefined, signal);
|
|
346
355
|
if (poll.status !== 200) {
|
|
347
|
-
throw new Error(
|
|
356
|
+
throw new Error(`AskUser: lost the pending ask (HTTP ${String(poll.status)}).`);
|
|
348
357
|
}
|
|
349
358
|
// Keep-alive: progress notifications reset the runtime's MCP inactivity
|
|
350
359
|
// timeout so a long human wait cannot kill the tool call.
|
|
351
360
|
await sendAskProgress(extra, Math.round((Date.now() - startedMs) / 1000));
|
|
352
361
|
const status = poll.body.status;
|
|
353
|
-
if (status === "answered"
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
:
|
|
357
|
-
|
|
362
|
+
if (status === "answered") {
|
|
363
|
+
const answer = formatBridgeAskAnswers(poll.body);
|
|
364
|
+
return askToolResult(`The user answered:\n${answer.text}`, {
|
|
365
|
+
answered: true,
|
|
366
|
+
interactionId,
|
|
367
|
+
answers: answer.answers,
|
|
368
|
+
});
|
|
358
369
|
}
|
|
359
370
|
if (status === "expired") {
|
|
360
|
-
|
|
371
|
+
const partial = formatBridgeAskAnswers(poll.body);
|
|
372
|
+
return askToolResult(`${partial.answers.length === 0 ? "The user did not answer" : `The user answered only:\n${partial.text}\n\nThe remaining questions were not answered`} within the wait window. Their next message will be a new turn; finish gracefully and state any assumptions.`, { answered: false, reason: "timeout", interactionId, answers: partial.answers });
|
|
361
373
|
}
|
|
362
374
|
if (status === "cancelled") {
|
|
363
375
|
return askToolResult("The user cancelled the current run. Stop this task.", {
|
|
364
376
|
answered: false,
|
|
365
377
|
reason: "cancelled",
|
|
366
|
-
|
|
378
|
+
interactionId,
|
|
379
|
+
});
|
|
367
380
|
}
|
|
368
381
|
}
|
|
369
382
|
}
|
|
@@ -371,11 +384,56 @@ async function awaitBridgeAsk(settings, askId, toolName, extra, fetchImpl, signa
|
|
|
371
384
|
// A cancelled MCP call must not leave a bridge ask pending until its full
|
|
372
385
|
// human timeout. Cleanup gets its own bounded signal (not the aborted call
|
|
373
386
|
// signal), and its failure must never replace the primary tool failure.
|
|
374
|
-
await cleanupBridgeAskBestEffort(settings, fetchImpl,
|
|
387
|
+
await cleanupBridgeAskBestEffort(settings, fetchImpl, interactionId);
|
|
375
388
|
throw error;
|
|
376
389
|
}
|
|
377
390
|
}
|
|
378
|
-
|
|
391
|
+
function formatBridgeAskAnswers(body) {
|
|
392
|
+
const questions = Array.isArray(body.questions) ? body.questions : [];
|
|
393
|
+
const answers = Array.isArray(body.answers) ? body.answers : [];
|
|
394
|
+
const normalized = [];
|
|
395
|
+
const lines = [];
|
|
396
|
+
for (const rawAnswer of answers) {
|
|
397
|
+
if (typeof rawAnswer !== "object" || rawAnswer === null || Array.isArray(rawAnswer))
|
|
398
|
+
continue;
|
|
399
|
+
const answer = rawAnswer;
|
|
400
|
+
const questionId = typeof answer.questionId === "string" ? answer.questionId : undefined;
|
|
401
|
+
if (questionId === undefined)
|
|
402
|
+
continue;
|
|
403
|
+
const question = questions.find((candidate) => typeof candidate === "object"
|
|
404
|
+
&& candidate !== null
|
|
405
|
+
&& !Array.isArray(candidate)
|
|
406
|
+
&& candidate.id === questionId);
|
|
407
|
+
const options = Array.isArray(question?.options) ? question.options : [];
|
|
408
|
+
const selectedOptionIds = Array.isArray(answer.selectedOptionIds)
|
|
409
|
+
? answer.selectedOptionIds.filter((value) => typeof value === "string")
|
|
410
|
+
: [];
|
|
411
|
+
const selectedOptions = selectedOptionIds.flatMap((optionId) => {
|
|
412
|
+
const option = options.find((candidate) => typeof candidate === "object"
|
|
413
|
+
&& candidate !== null
|
|
414
|
+
&& !Array.isArray(candidate)
|
|
415
|
+
&& candidate.id === optionId);
|
|
416
|
+
return typeof option?.label === "string"
|
|
417
|
+
? [{ id: optionId, label: option.label, ...(typeof option.description === "string" ? { description: option.description } : {}) }]
|
|
418
|
+
: [];
|
|
419
|
+
});
|
|
420
|
+
const customReply = typeof answer.customReply === "string" ? answer.customReply : undefined;
|
|
421
|
+
const header = typeof question?.header === "string" ? question.header : questionId;
|
|
422
|
+
const rendered = [
|
|
423
|
+
...selectedOptions.map((option) => option.label),
|
|
424
|
+
...(customReply === undefined ? [] : [customReply]),
|
|
425
|
+
].join(", ");
|
|
426
|
+
lines.push(`- ${header}: ${rendered}`);
|
|
427
|
+
normalized.push({
|
|
428
|
+
questionId,
|
|
429
|
+
header,
|
|
430
|
+
selectedOptions,
|
|
431
|
+
...(customReply === undefined ? {} : { customReply }),
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
return { text: lines.join("\n") || "(no answers)", answers: normalized };
|
|
435
|
+
}
|
|
436
|
+
async function cleanupBridgeAskBestEffort(settings, fetchImpl, interactionId) {
|
|
379
437
|
const controller = new AbortController();
|
|
380
438
|
let deadlineTimer;
|
|
381
439
|
const deadline = new Promise((resolve) => {
|
|
@@ -387,7 +445,7 @@ async function cleanupBridgeAskBestEffort(settings, fetchImpl, askId) {
|
|
|
387
445
|
});
|
|
388
446
|
// Attach the rejection handler before racing. If a test seam or nonstandard
|
|
389
447
|
// fetch ignores abort and rejects later, it cannot become an unhandled error.
|
|
390
|
-
const cleanup = askBridgeRequest(settings, fetchImpl, "DELETE", `/v1/asks/${encodeURIComponent(
|
|
448
|
+
const cleanup = askBridgeRequest(settings, fetchImpl, "DELETE", `/v1/asks/${encodeURIComponent(interactionId)}`, undefined, controller.signal).then(() => undefined, () => undefined);
|
|
391
449
|
try {
|
|
392
450
|
await Promise.race([cleanup, deadline]);
|
|
393
451
|
}
|
|
@@ -566,25 +624,41 @@ function registerSlackSendTool(server, settings, client, formatMarkdownForSlack,
|
|
|
566
624
|
};
|
|
567
625
|
});
|
|
568
626
|
}
|
|
569
|
-
function registerTelegramSendTool(server, settings, client, deliveryHistory, fetchImpl = globalThis.fetch) {
|
|
627
|
+
function registerTelegramSendTool(server, settings, client, adapter, deliveryHistory, fetchImpl = globalThis.fetch) {
|
|
570
628
|
server.registerTool("TelegramSendMessage", {
|
|
571
629
|
title: "Send Telegram message",
|
|
572
|
-
description: "Send a message to an allowed Telegram chat
|
|
630
|
+
description: "Send a message to an allowed Telegram chat. Optionally add 2–8 non-blocking reply buttons; a tap arrives later as a new user turn.",
|
|
573
631
|
inputSchema: {
|
|
574
632
|
chat_id: z.union([z.string().min(1), z.number().int()]).describe("Telegram chat id from the adapter allowlist."),
|
|
575
633
|
text: z.string().min(1).describe("Message text to send."),
|
|
576
634
|
parse_mode: z.string().min(1).optional().describe("Optional Telegram parse mode, e.g. MarkdownV2 or HTML."),
|
|
577
635
|
reply_to_message_id: z.number().int().optional().describe("Optional message id to reply to."),
|
|
578
636
|
disable_web_page_preview: z.boolean().optional().describe("Disable Telegram link previews."),
|
|
637
|
+
reply_options: z
|
|
638
|
+
.array(z.string().min(1).max(75))
|
|
639
|
+
.min(2)
|
|
640
|
+
.max(8)
|
|
641
|
+
.optional()
|
|
642
|
+
.describe("Optional non-blocking reply button labels. Use AskUser when this run must wait for the selection."),
|
|
579
643
|
},
|
|
580
644
|
}, async (args, extra) => {
|
|
581
|
-
assertTelegramChatAllowed(settings, args.chat_id);
|
|
645
|
+
assertTelegramChatAllowed(settings, args.chat_id, "TelegramSendMessage");
|
|
582
646
|
const result = await client.sendMessage({
|
|
583
647
|
chat_id: args.chat_id,
|
|
584
648
|
text: args.text,
|
|
585
649
|
...(args.parse_mode === undefined ? {} : { parse_mode: args.parse_mode }),
|
|
586
650
|
...(args.reply_to_message_id === undefined ? {} : { reply_to_message_id: args.reply_to_message_id }),
|
|
587
651
|
...(args.disable_web_page_preview === undefined ? {} : { disable_web_page_preview: args.disable_web_page_preview }),
|
|
652
|
+
...(args.reply_options === undefined
|
|
653
|
+
? {}
|
|
654
|
+
: {
|
|
655
|
+
reply_markup: {
|
|
656
|
+
inline_keyboard: args.reply_options.map((label, index) => [{
|
|
657
|
+
text: label,
|
|
658
|
+
callback_data: adapter.telegramReplyCallbackData(index),
|
|
659
|
+
}]),
|
|
660
|
+
},
|
|
661
|
+
}),
|
|
588
662
|
}, { signal: extra.signal });
|
|
589
663
|
const history = await recordAdapterDeliveryHistory({
|
|
590
664
|
settings: deliveryHistory,
|
|
@@ -600,211 +674,53 @@ function registerTelegramSendTool(server, settings, client, deliveryHistory, fet
|
|
|
600
674
|
ok: true,
|
|
601
675
|
chat_id: result.chat.id,
|
|
602
676
|
message_id: result.message_id,
|
|
677
|
+
...(args.reply_options === undefined ? {} : { reply_options: args.reply_options }),
|
|
603
678
|
...(history === undefined ? {} : { history }),
|
|
604
679
|
},
|
|
605
680
|
};
|
|
606
681
|
});
|
|
607
682
|
}
|
|
608
|
-
/** Marker appended (buttons kept) when a Telegram ask times out. */
|
|
609
|
-
const TELEGRAM_ASK_EXPIRED_MARKER = "⌛ Expired — tapping a button still reaches me and starts a new turn.";
|
|
610
|
-
/** Marker appended (buttons kept) when a Telegram ask is cancelled. */
|
|
611
|
-
const TELEGRAM_ASK_CANCELLED_MARKER = "🚫 Cancelled — tapping a button still reaches me and starts a new turn.";
|
|
612
|
-
/** Bounded own-signal budget for the best-effort expiry edit — never the aborted call signal. */
|
|
613
|
-
const TELEGRAM_ASK_EXPIRY_EDIT_TIMEOUT_MS = 2_000;
|
|
614
|
-
/** A Telegram 400 is the parse-entity rejection we fall back from (raw text, no parse_mode). */
|
|
615
|
-
function isTelegramParseError(error, adapter) {
|
|
616
|
-
return error instanceof adapter.TelegramApiError && error.errorCode === 400;
|
|
617
|
-
}
|
|
618
|
-
/**
|
|
619
|
-
* Send the ask keyboard rendered as MarkdownV2, retrying ONCE with the raw source and
|
|
620
|
-
* no parse_mode when Telegram rejects the entities with a 400 parse error. Any other
|
|
621
|
-
* failure propagates so the caller can clean up a pending bridge ask.
|
|
622
|
-
*/
|
|
623
|
-
async function sendTelegramAskKeyboard(client, adapter, params) {
|
|
624
|
-
try {
|
|
625
|
-
return await client.sendMessage({
|
|
626
|
-
chat_id: params.chat_id,
|
|
627
|
-
text: params.rendered,
|
|
628
|
-
parse_mode: "MarkdownV2",
|
|
629
|
-
reply_markup: params.replyMarkup,
|
|
630
|
-
}, { signal: params.signal });
|
|
631
|
-
}
|
|
632
|
-
catch (error) {
|
|
633
|
-
if (!isTelegramParseError(error, adapter)) {
|
|
634
|
-
throw error;
|
|
635
|
-
}
|
|
636
|
-
return await client.sendMessage({
|
|
637
|
-
chat_id: params.chat_id,
|
|
638
|
-
text: params.rawSource,
|
|
639
|
-
reply_markup: params.replyMarkup,
|
|
640
|
-
}, { signal: params.signal });
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
/**
|
|
644
|
-
* Best-effort: append an expiry/cancel note to the buttons message while KEEPING the
|
|
645
|
-
* inline keyboard (Telegram strips the keyboard on `editMessageText` unless reply_markup
|
|
646
|
-
* is re-sent). Renders through the same MarkdownV2-with-plain-fallback path. Uses its own
|
|
647
|
-
* bounded signal — never the aborted call signal — and swallows every failure so a failed
|
|
648
|
-
* edit can never replace the primary ask result.
|
|
649
|
-
*/
|
|
650
|
-
async function editExpiredTelegramAskMessage(client, adapter, params) {
|
|
651
|
-
try {
|
|
652
|
-
const source = `${params.rawSource}\n\n${params.marker}`;
|
|
653
|
-
const rendered = adapter.renderTelegramMarkdown(source);
|
|
654
|
-
const signal = AbortSignal.timeout(TELEGRAM_ASK_EXPIRY_EDIT_TIMEOUT_MS);
|
|
655
|
-
try {
|
|
656
|
-
await client.editMessageText({
|
|
657
|
-
chat_id: params.chat_id,
|
|
658
|
-
message_id: params.message_id,
|
|
659
|
-
text: rendered,
|
|
660
|
-
parse_mode: "MarkdownV2",
|
|
661
|
-
reply_markup: params.replyMarkup,
|
|
662
|
-
}, { signal });
|
|
663
|
-
}
|
|
664
|
-
catch (error) {
|
|
665
|
-
if (!isTelegramParseError(error, adapter)) {
|
|
666
|
-
throw error;
|
|
667
|
-
}
|
|
668
|
-
await client.editMessageText({
|
|
669
|
-
chat_id: params.chat_id,
|
|
670
|
-
message_id: params.message_id,
|
|
671
|
-
text: source,
|
|
672
|
-
reply_markup: params.replyMarkup,
|
|
673
|
-
}, { signal });
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
catch {
|
|
677
|
-
// Best-effort: a failed expiry edit must never replace the ask result.
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
function registerTelegramAskTool(server, settings, client, adapter, fetchImpl) {
|
|
681
|
-
server.registerTool("TelegramAskButtons", {
|
|
682
|
-
title: "Ask via Telegram buttons",
|
|
683
|
-
description: "Ask the owner a question in an allowed Telegram chat with tappable inline-keyboard buttons (e.g. a confirmation or a multiple-choice). By default WAITS for their tap (up to the ask timeout) and returns the tapped label; a second concurrent ask in the same chat fails. On a proactive or scheduled turn (cron/heartbeat) set `wait:false`: the buttons are sent and the tool returns immediately, and the tap arrives later as a new message quoting the question.",
|
|
684
|
-
inputSchema: {
|
|
685
|
-
chat_id: z.union([z.string().min(1), z.number().int()]).describe("Telegram chat id from the adapter allowlist."),
|
|
686
|
-
question: z.string().min(1).describe("The question to show above the buttons."),
|
|
687
|
-
options: z
|
|
688
|
-
.array(z.string().min(1).max(100))
|
|
689
|
-
.min(2)
|
|
690
|
-
.max(adapter.TELEGRAM_ASK_MAX_OPTIONS)
|
|
691
|
-
.describe("Button labels (2–8). The label the user taps is echoed back as a new message."),
|
|
692
|
-
note: z.string().min(1).optional().describe("Optional extra context shown beneath the question."),
|
|
693
|
-
wait: z
|
|
694
|
-
.boolean()
|
|
695
|
-
.optional()
|
|
696
|
-
.describe("Default true: block until the user taps (up to the ask timeout). Set false on proactive/scheduled (cron/heartbeat) turns — send the buttons and return immediately; the tapped answer arrives later as a new message quoting the question."),
|
|
697
|
-
},
|
|
698
|
-
}, async (args, extra) => {
|
|
699
|
-
assertTelegramChatAllowed(settings, args.chat_id);
|
|
700
|
-
const inlineKeyboard = args.options.map((label, index) => [
|
|
701
|
-
{ text: label, callback_data: adapter.telegramAskCallbackData(index) },
|
|
702
|
-
]);
|
|
703
|
-
const replyMarkup = { inline_keyboard: inlineKeyboard };
|
|
704
|
-
// Raw source is what the bridge records and what the plain-text fallback (and the
|
|
705
|
-
// expiry edit) re-send; the rendered form is only for the MarkdownV2 send.
|
|
706
|
-
const source = args.note === undefined ? args.question : `${args.question}\n\n${args.note}`;
|
|
707
|
-
const rendered = adapter.renderTelegramMarkdown(source);
|
|
708
|
-
// Non-blocking mode (proactive/scheduled turns): send the buttons and return.
|
|
709
|
-
// No bridge is consulted — this must work even with no bridge configured.
|
|
710
|
-
if (args.wait === false) {
|
|
711
|
-
const sent = await sendTelegramAskKeyboard(client, adapter, {
|
|
712
|
-
chat_id: args.chat_id,
|
|
713
|
-
rendered,
|
|
714
|
-
rawSource: source,
|
|
715
|
-
replyMarkup,
|
|
716
|
-
signal: extra.signal,
|
|
717
|
-
});
|
|
718
|
-
return askToolResult(`Sent the question with ${String(args.options.length)} buttons to ${String(sent.chat.id)} (message ${String(sent.message_id)}). Not waiting — the tapped answer will arrive as a new message.`, { answered: false, reason: "not_waiting" }, { chat_id: sent.chat.id, message_id: sent.message_id, options: args.options });
|
|
719
|
-
}
|
|
720
|
-
// Blocking mode requires the interaction bridge to correlate the tap.
|
|
721
|
-
const bridge = settings.askBridge;
|
|
722
|
-
if (bridge === undefined) {
|
|
723
|
-
return askToolResult("TelegramAskButtons requires a live interaction bridge, but none is configured.", {
|
|
724
|
-
answered: false,
|
|
725
|
-
reason: "unsupported_channel",
|
|
726
|
-
});
|
|
727
|
-
}
|
|
728
|
-
const conversationId = `telegram:${String(args.chat_id)}`;
|
|
729
|
-
const created = await askBridgeRequest(bridge, fetchImpl, "POST", "/v1/asks", {
|
|
730
|
-
conversationId,
|
|
731
|
-
producerConversationId: bridge.conversationId,
|
|
732
|
-
runId: bridge.runId,
|
|
733
|
-
toolName: "TelegramAskButtons",
|
|
734
|
-
question: source,
|
|
735
|
-
options: args.options,
|
|
736
|
-
timeoutMs: bridge.timeoutMs,
|
|
737
|
-
postQuestion: false,
|
|
738
|
-
answerKind: "callback",
|
|
739
|
-
}, extra.signal);
|
|
740
|
-
if (created.status === 409) {
|
|
741
|
-
return askToolResult("A question is already pending for this Telegram chat. Wait for its answer instead of asking again.", { answered: false, reason: "already_pending" }, { chat_id: args.chat_id });
|
|
742
|
-
}
|
|
743
|
-
if (created.status === 501) {
|
|
744
|
-
return askToolResult("This Telegram chat is not registered with the interaction bridge. Ask your question in your final reply instead.", { answered: false, reason: "unsupported_channel" }, { chat_id: args.chat_id });
|
|
745
|
-
}
|
|
746
|
-
if (created.status !== 201 || typeof created.body.askId !== "string") {
|
|
747
|
-
throw new Error(`TelegramAskButtons: the interaction bridge rejected the ask (HTTP ${String(created.status)}).`);
|
|
748
|
-
}
|
|
749
|
-
const askId = created.body.askId;
|
|
750
|
-
let result;
|
|
751
|
-
try {
|
|
752
|
-
result = await sendTelegramAskKeyboard(client, adapter, {
|
|
753
|
-
chat_id: args.chat_id,
|
|
754
|
-
rendered,
|
|
755
|
-
rawSource: source,
|
|
756
|
-
replyMarkup,
|
|
757
|
-
signal: extra.signal,
|
|
758
|
-
});
|
|
759
|
-
}
|
|
760
|
-
catch (error) {
|
|
761
|
-
await cleanupBridgeAskBestEffort(bridge, fetchImpl, askId);
|
|
762
|
-
throw error;
|
|
763
|
-
}
|
|
764
|
-
const askResult = await awaitBridgeAsk(bridge, askId, "TelegramAskButtons", extra, fetchImpl, extra.signal, {
|
|
765
|
-
chat_id: result.chat.id,
|
|
766
|
-
message_id: result.message_id,
|
|
767
|
-
options: args.options,
|
|
768
|
-
});
|
|
769
|
-
// An expired/cancelled ask leaves a live-looking keyboard forever; append a note
|
|
770
|
-
// while keeping the buttons (a tap still starts a fresh turn). Best-effort only.
|
|
771
|
-
const reason = askResult.structuredContent.reason;
|
|
772
|
-
if (reason === "timeout" || reason === "cancelled") {
|
|
773
|
-
await editExpiredTelegramAskMessage(client, adapter, {
|
|
774
|
-
chat_id: result.chat.id,
|
|
775
|
-
message_id: result.message_id,
|
|
776
|
-
rawSource: source,
|
|
777
|
-
marker: reason === "timeout" ? TELEGRAM_ASK_EXPIRED_MARKER : TELEGRAM_ASK_CANCELLED_MARKER,
|
|
778
|
-
replyMarkup,
|
|
779
|
-
});
|
|
780
|
-
}
|
|
781
|
-
return askResult;
|
|
782
|
-
});
|
|
783
|
-
}
|
|
784
683
|
/**
|
|
785
684
|
* Register the single `TelegramSendFile` tool. A required `kind` param selects
|
|
786
685
|
* `"document"` (any file, shown as a downloadable document) or `"photo"` (an image
|
|
787
686
|
* shown inline). Both accept the file as base64 `data` (with a `filename`) OR a
|
|
788
687
|
* workspace `path` (filename derived from the basename), enforce the adapter
|
|
789
688
|
* allowlist, and bound the size to the adapter's inbound cap before uploading via
|
|
790
|
-
* the adapter-owned sender.
|
|
689
|
+
* the adapter-owned sender. Producing-conversation scope omits `chat_id` from the
|
|
690
|
+
* schema and derives it from trusted request context instead.
|
|
791
691
|
*/
|
|
792
692
|
function registerTelegramSendFileTool(server, settings, client, adapter) {
|
|
693
|
+
const producingConversationScope = settings.sendTools?.scope === "producing-conversation";
|
|
694
|
+
const inputSchema = {
|
|
695
|
+
kind: z.enum(["document", "photo"]).describe("`document` for any file (downloadable), `photo` for an image shown inline."),
|
|
696
|
+
...(producingConversationScope
|
|
697
|
+
? {}
|
|
698
|
+
: {
|
|
699
|
+
chat_id: z
|
|
700
|
+
.union([z.string().min(1), z.number().int()])
|
|
701
|
+
.describe("Telegram chat id from the adapter allowlist."),
|
|
702
|
+
}),
|
|
703
|
+
data: z.string().min(1).optional().describe("Base64-encoded file bytes. Provide this or `path`."),
|
|
704
|
+
path: z.string().min(1).optional().describe("Path to a file to upload (resolved against the agent working dir). Provide this or `data`."),
|
|
705
|
+
filename: z.string().min(1).optional().describe("Filename to present. Required with `data` for a document; derived from `path` otherwise."),
|
|
706
|
+
caption: z.string().min(1).optional().describe("Optional caption shown with the file."),
|
|
707
|
+
};
|
|
793
708
|
server.registerTool("TelegramSendFile", {
|
|
794
709
|
title: "Send Telegram file",
|
|
795
|
-
description:
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
data: z.string().min(1).optional().describe("Base64-encoded file bytes. Provide this or `path`."),
|
|
800
|
-
path: z.string().min(1).optional().describe("Path to a file to upload (resolved against the agent working dir). Provide this or `data`."),
|
|
801
|
-
filename: z.string().min(1).optional().describe("Filename to present. Required with `data` for a document; derived from `path` otherwise."),
|
|
802
|
-
caption: z.string().min(1).optional().describe("Optional caption shown with the file."),
|
|
803
|
-
},
|
|
710
|
+
description: producingConversationScope
|
|
711
|
+
? "Upload and send a file to the Telegram conversation that produced this run. The host binds the destination; provide only the file content or path. Set `kind:\"document\"` to send any file (shown as a downloadable document) or `kind:\"photo\"` to send an image inline. Strict run-output paths are always read through a pinned descriptor."
|
|
712
|
+
: "Upload and send a file to an allowed Telegram chat. Set `kind:\"document\"` to send any file (shown as a downloadable document) or `kind:\"photo\"` to send an image inline. Provide the bytes as base64 `data` (with a `filename` — required for a document), or a workspace `path`. A self-hosted Bot API can stream legacy path uploads.",
|
|
713
|
+
inputSchema,
|
|
804
714
|
}, async (args, extra) => {
|
|
805
715
|
extra.signal.throwIfAborted();
|
|
806
716
|
const kind = args.kind;
|
|
807
|
-
|
|
717
|
+
const sendDocument = client.sendDocument;
|
|
718
|
+
const sendPhoto = client.sendPhoto;
|
|
719
|
+
if ((kind === "document" && sendDocument === undefined) || (kind === "photo" && sendPhoto === undefined)) {
|
|
720
|
+
throw new Error(`TelegramSendFile: the ${kind} sender is unavailable.`);
|
|
721
|
+
}
|
|
722
|
+
const requestedChatId = "chat_id" in args ? args.chat_id : undefined;
|
|
723
|
+
const chatId = resolveTelegramSendFileChatId(settings, requestedChatId);
|
|
808
724
|
if ((args.data !== undefined) === (args.path !== undefined)) {
|
|
809
725
|
throw new Error("provide exactly one of `data` (base64) or `path`.");
|
|
810
726
|
}
|
|
@@ -829,16 +745,13 @@ function registerTelegramSendFileTool(server, settings, client, adapter) {
|
|
|
829
745
|
throw new Error(`file exceeds the ${String(maxUploadBytes)}-byte upload cap.`);
|
|
830
746
|
}
|
|
831
747
|
try {
|
|
832
|
-
const sent = await
|
|
833
|
-
chat_id:
|
|
748
|
+
const sent = await sendDocument({
|
|
749
|
+
chat_id: chatId,
|
|
834
750
|
document: pathToFileURL(uploadPath).href,
|
|
835
751
|
...(args.caption === undefined ? {} : { caption: args.caption }),
|
|
836
752
|
}, { signal: extra.signal });
|
|
837
753
|
const name = basename(uploadPath);
|
|
838
|
-
return
|
|
839
|
-
content: [{ type: "text", text: `Sent ${kind} ${sent.message_id} (${name}) to ${String(sent.chat.id)}.` }],
|
|
840
|
-
structuredContent: { ok: true, chat_id: sent.chat.id, message_id: sent.message_id, filename: name },
|
|
841
|
-
};
|
|
754
|
+
return telegramSendFileResult(producingConversationScope, kind, sent, name);
|
|
842
755
|
}
|
|
843
756
|
catch (error) {
|
|
844
757
|
// Retry buffered exactly once; rethrow anything that isn't a server-side rejection.
|
|
@@ -857,24 +770,50 @@ function registerTelegramSendFileTool(server, settings, client, adapter) {
|
|
|
857
770
|
signal: extra.signal,
|
|
858
771
|
});
|
|
859
772
|
const result = kind === "document"
|
|
860
|
-
? await
|
|
861
|
-
chat_id:
|
|
773
|
+
? await sendDocument({
|
|
774
|
+
chat_id: chatId,
|
|
862
775
|
document: bytes,
|
|
863
776
|
filename,
|
|
864
777
|
...(args.caption === undefined ? {} : { caption: args.caption }),
|
|
865
778
|
}, { signal: extra.signal })
|
|
866
|
-
: await
|
|
867
|
-
chat_id:
|
|
779
|
+
: await sendPhoto({
|
|
780
|
+
chat_id: chatId,
|
|
868
781
|
photo: bytes,
|
|
869
782
|
filename,
|
|
870
783
|
...(args.caption === undefined ? {} : { caption: args.caption }),
|
|
871
784
|
}, { signal: extra.signal });
|
|
872
|
-
return
|
|
873
|
-
content: [{ type: "text", text: `Sent ${kind} ${result.message_id} (${filename}) to ${String(result.chat.id)}.` }],
|
|
874
|
-
structuredContent: { ok: true, chat_id: result.chat.id, message_id: result.message_id, filename },
|
|
875
|
-
};
|
|
785
|
+
return telegramSendFileResult(producingConversationScope, kind, result, filename);
|
|
876
786
|
});
|
|
877
787
|
}
|
|
788
|
+
function resolveTelegramSendFileChatId(settings, requestedChatId) {
|
|
789
|
+
if (settings.sendTools?.scope === "producing-conversation") {
|
|
790
|
+
const producingChatId = telegramChatIdFromConversation(settings.producingConversationId);
|
|
791
|
+
if (producingChatId === undefined) {
|
|
792
|
+
throw new Error("TelegramSendFile: producing Telegram conversation context is unavailable.");
|
|
793
|
+
}
|
|
794
|
+
assertTelegramChatAllowed(settings, producingChatId, "TelegramSendFile");
|
|
795
|
+
return producingChatId;
|
|
796
|
+
}
|
|
797
|
+
if (requestedChatId === undefined) {
|
|
798
|
+
throw new Error("TelegramSendFile: chat_id is required outside producing-conversation scope.");
|
|
799
|
+
}
|
|
800
|
+
assertTelegramChatAllowed(settings, requestedChatId, "TelegramSendFile");
|
|
801
|
+
return requestedChatId;
|
|
802
|
+
}
|
|
803
|
+
function telegramSendFileResult(producingConversationScope, kind, sent, filename) {
|
|
804
|
+
const destination = producingConversationScope
|
|
805
|
+
? "the producing Telegram conversation"
|
|
806
|
+
: String(sent.chat.id);
|
|
807
|
+
return {
|
|
808
|
+
content: [{ type: "text", text: `Sent ${kind} ${sent.message_id} (${filename}) to ${destination}.` }],
|
|
809
|
+
structuredContent: {
|
|
810
|
+
ok: true,
|
|
811
|
+
...(producingConversationScope ? {} : { chat_id: sent.chat.id }),
|
|
812
|
+
message_id: sent.message_id,
|
|
813
|
+
filename,
|
|
814
|
+
},
|
|
815
|
+
};
|
|
816
|
+
}
|
|
878
817
|
/** Resolve the upload bytes + filename from exactly one of base64 `data` or a `path`. */
|
|
879
818
|
async function resolveTelegramFileBytes(input) {
|
|
880
819
|
const hasData = input.data !== undefined;
|
|
@@ -1033,7 +972,7 @@ async function resolveSlackSendToolSettings(input, options) {
|
|
|
1033
972
|
return undefined;
|
|
1034
973
|
}
|
|
1035
974
|
}
|
|
1036
|
-
async function resolveTelegramSendToolSettings(input, options, tools
|
|
975
|
+
async function resolveTelegramSendToolSettings(input, options, tools) {
|
|
1037
976
|
try {
|
|
1038
977
|
const adapter = await loadTelegramModule();
|
|
1039
978
|
const config = await adapter.loadTelegramAdapterConfig({ env: input.env, jsonPath: input.configPath });
|
|
@@ -1050,7 +989,6 @@ async function resolveTelegramSendToolSettings(input, options, tools, askBridge)
|
|
|
1050
989
|
...(config.apiRoot === undefined ? {} : { apiRoot: config.apiRoot }),
|
|
1051
990
|
maxUploadBytes: config.attachments?.maxUploadBytes ?? adapter.DEFAULT_ATTACHMENT_MAX_BYTES,
|
|
1052
991
|
tools,
|
|
1053
|
-
...(askBridge === undefined ? {} : { askBridge }),
|
|
1054
992
|
...(config.sendTools === undefined ? {} : { sendTools: config.sendTools }),
|
|
1055
993
|
...(producingConversationId === undefined ? {} : { producingConversationId }),
|
|
1056
994
|
...(runOutputDir === undefined ? {} : { runOutputDir }),
|
|
@@ -1070,21 +1008,21 @@ function assertSlackChannelAllowed(settings, channel) {
|
|
|
1070
1008
|
}
|
|
1071
1009
|
throw new Error("SlackSendMessage: channel is not allowed by Slack adapter config.");
|
|
1072
1010
|
}
|
|
1073
|
-
function assertTelegramChatAllowed(settings, chatId) {
|
|
1011
|
+
function assertTelegramChatAllowed(settings, chatId, toolName) {
|
|
1074
1012
|
const normalized = String(chatId);
|
|
1075
1013
|
if (settings.sendTools?.scope === "producing-conversation") {
|
|
1076
1014
|
const producingChatId = telegramChatIdFromConversation(settings.producingConversationId);
|
|
1077
1015
|
if (producingChatId === undefined) {
|
|
1078
|
-
throw new Error(
|
|
1016
|
+
throw new Error(`${toolName}: producing Telegram conversation context is unavailable.`);
|
|
1079
1017
|
}
|
|
1080
1018
|
if (normalized !== producingChatId) {
|
|
1081
|
-
throw new Error(
|
|
1019
|
+
throw new Error(`${toolName}: chat_id must match the producing Telegram conversation.`);
|
|
1082
1020
|
}
|
|
1083
1021
|
}
|
|
1084
1022
|
if (settings.allowAllChats || settings.allowedChatIds.includes(normalized)) {
|
|
1085
1023
|
return;
|
|
1086
1024
|
}
|
|
1087
|
-
throw new Error(
|
|
1025
|
+
throw new Error(`${toolName}: chat_id is not allowed by Telegram adapter config.`);
|
|
1088
1026
|
}
|
|
1089
1027
|
function telegramChatIdFromConversation(conversationId) {
|
|
1090
1028
|
if (conversationId === undefined)
|