@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.
Files changed (200) hide show
  1. package/README.md +190 -33
  2. package/dist/adapter-send-tools-main.js +0 -1
  3. package/dist/adapter-send-tools-main.js.map +1 -1
  4. package/dist/adapter-send-tools.d.ts +8 -5
  5. package/dist/adapter-send-tools.d.ts.map +1 -1
  6. package/dist/adapter-send-tools.js +195 -257
  7. package/dist/adapter-send-tools.js.map +1 -1
  8. package/dist/app-controller-channels.d.ts.map +1 -1
  9. package/dist/app-controller-channels.js +2 -1
  10. package/dist/app-controller-channels.js.map +1 -1
  11. package/dist/app-controller-continuation.d.ts.map +1 -1
  12. package/dist/app-controller-continuation.js +1 -5
  13. package/dist/app-controller-continuation.js.map +1 -1
  14. package/dist/app-controller-lifecycle.d.ts.map +1 -1
  15. package/dist/app-controller-lifecycle.js +2 -6
  16. package/dist/app-controller-lifecycle.js.map +1 -1
  17. package/dist/app-controller-maintenance.d.ts +2 -1
  18. package/dist/app-controller-maintenance.d.ts.map +1 -1
  19. package/dist/app-controller-maintenance.js +66 -2
  20. package/dist/app-controller-maintenance.js.map +1 -1
  21. package/dist/app-controller-memory-health.d.ts +1 -1
  22. package/dist/app-controller-memory-health.d.ts.map +1 -1
  23. package/dist/app-controller-memory-health.js +5 -1
  24. package/dist/app-controller-memory-health.js.map +1 -1
  25. package/dist/app-controller-responder.d.ts.map +1 -1
  26. package/dist/app-controller-responder.js +12 -4
  27. package/dist/app-controller-responder.js.map +1 -1
  28. package/dist/app-controller-traceability.d.ts.map +1 -1
  29. package/dist/app-controller-traceability.js +15 -1
  30. package/dist/app-controller-traceability.js.map +1 -1
  31. package/dist/app-controller-utils.js +1 -1
  32. package/dist/app-controller-utils.js.map +1 -1
  33. package/dist/app-controller.d.ts +14 -1
  34. package/dist/app-controller.d.ts.map +1 -1
  35. package/dist/app-controller.js +61 -9
  36. package/dist/app-controller.js.map +1 -1
  37. package/dist/audit-runs.d.ts.map +1 -1
  38. package/dist/audit-runs.js +12 -2
  39. package/dist/audit-runs.js.map +1 -1
  40. package/dist/background-environment.d.ts +1 -0
  41. package/dist/background-environment.d.ts.map +1 -1
  42. package/dist/background-environment.js +1 -0
  43. package/dist/background-environment.js.map +1 -1
  44. package/dist/background-runtime.d.ts +39 -2
  45. package/dist/background-runtime.d.ts.map +1 -1
  46. package/dist/background-runtime.js +901 -49
  47. package/dist/background-runtime.js.map +1 -1
  48. package/dist/background.d.ts +36 -3
  49. package/dist/background.d.ts.map +1 -1
  50. package/dist/background.js +404 -57
  51. package/dist/background.js.map +1 -1
  52. package/dist/channel-drivers/cron.js +8 -1
  53. package/dist/channel-drivers/cron.js.map +1 -1
  54. package/dist/channel-drivers/live.d.ts +3 -2
  55. package/dist/channel-drivers/live.d.ts.map +1 -1
  56. package/dist/channel-drivers/live.js +3 -2
  57. package/dist/channel-drivers/live.js.map +1 -1
  58. package/dist/channel-drivers/native-notify.d.ts +2 -0
  59. package/dist/channel-drivers/native-notify.d.ts.map +1 -1
  60. package/dist/channel-drivers/native-notify.js +14 -2
  61. package/dist/channel-drivers/native-notify.js.map +1 -1
  62. package/dist/channel-drivers/slack.d.ts.map +1 -1
  63. package/dist/channel-drivers/slack.js +55 -0
  64. package/dist/channel-drivers/slack.js.map +1 -1
  65. package/dist/channel-drivers/telegram.d.ts.map +1 -1
  66. package/dist/channel-drivers/telegram.js +19 -14
  67. package/dist/channel-drivers/telegram.js.map +1 -1
  68. package/dist/channel-drivers/tui.d.ts.map +1 -1
  69. package/dist/channel-drivers/tui.js +8 -0
  70. package/dist/channel-drivers/tui.js.map +1 -1
  71. package/dist/channel-runtime-controls.d.ts +23 -0
  72. package/dist/channel-runtime-controls.d.ts.map +1 -0
  73. package/dist/channel-runtime-controls.js +63 -0
  74. package/dist/channel-runtime-controls.js.map +1 -0
  75. package/dist/channel-status-display.d.ts +15 -0
  76. package/dist/channel-status-display.d.ts.map +1 -0
  77. package/dist/channel-status-display.js +48 -0
  78. package/dist/channel-status-display.js.map +1 -0
  79. package/dist/cli-args.d.ts +23 -22
  80. package/dist/cli-args.d.ts.map +1 -1
  81. package/dist/cli-args.js +136 -83
  82. package/dist/cli-args.js.map +1 -1
  83. package/dist/cli-background-command.d.ts +1 -0
  84. package/dist/cli-background-command.d.ts.map +1 -1
  85. package/dist/cli-background-command.js +158 -27
  86. package/dist/cli-background-command.js.map +1 -1
  87. package/dist/cli-commands.d.ts +2 -2
  88. package/dist/cli-commands.d.ts.map +1 -1
  89. package/dist/cli-commands.js +27 -59
  90. package/dist/cli-commands.js.map +1 -1
  91. package/dist/cli-help.d.ts +19 -1
  92. package/dist/cli-help.d.ts.map +1 -1
  93. package/dist/cli-help.js +203 -67
  94. package/dist/cli-help.js.map +1 -1
  95. package/dist/cli-init-command.d.ts.map +1 -1
  96. package/dist/cli-init-command.js +4 -18
  97. package/dist/cli-init-command.js.map +1 -1
  98. package/dist/cli-install-skill-command.d.ts.map +1 -1
  99. package/dist/cli-install-skill-command.js +32 -3
  100. package/dist/cli-install-skill-command.js.map +1 -1
  101. package/dist/cli-runs-command.d.ts +10 -0
  102. package/dist/cli-runs-command.d.ts.map +1 -0
  103. package/dist/cli-runs-command.js +66 -0
  104. package/dist/cli-runs-command.js.map +1 -0
  105. package/dist/cli-sandbox-command.d.ts +1 -1
  106. package/dist/cli-sandbox-command.d.ts.map +1 -1
  107. package/dist/cli-sandbox-command.js +14 -2
  108. package/dist/cli-sandbox-command.js.map +1 -1
  109. package/dist/cli-validate-config-command.d.ts +24 -5
  110. package/dist/cli-validate-config-command.d.ts.map +1 -1
  111. package/dist/cli-validate-config-command.js +120 -41
  112. package/dist/cli-validate-config-command.js.map +1 -1
  113. package/dist/config-reference.d.ts.map +1 -1
  114. package/dist/config-reference.js +4 -5
  115. package/dist/config-reference.js.map +1 -1
  116. package/dist/docs-mcp-pairing.d.ts +35 -0
  117. package/dist/docs-mcp-pairing.d.ts.map +1 -0
  118. package/dist/docs-mcp-pairing.js +311 -0
  119. package/dist/docs-mcp-pairing.js.map +1 -0
  120. package/dist/doctor.d.ts +6 -0
  121. package/dist/doctor.d.ts.map +1 -1
  122. package/dist/doctor.js +4 -5
  123. package/dist/doctor.js.map +1 -1
  124. package/dist/install-skill.d.ts +2 -0
  125. package/dist/install-skill.d.ts.map +1 -1
  126. package/dist/install-skill.js +10 -4
  127. package/dist/install-skill.js.map +1 -1
  128. package/dist/interaction-bridge.d.ts +5 -20
  129. package/dist/interaction-bridge.d.ts.map +1 -1
  130. package/dist/interaction-bridge.js +255 -100
  131. package/dist/interaction-bridge.js.map +1 -1
  132. package/dist/launchd.d.ts +35 -1
  133. package/dist/launchd.d.ts.map +1 -1
  134. package/dist/launchd.js +131 -2
  135. package/dist/launchd.js.map +1 -1
  136. package/dist/managed-runtime-publication.d.ts +22 -0
  137. package/dist/managed-runtime-publication.d.ts.map +1 -0
  138. package/dist/managed-runtime-publication.js +91 -0
  139. package/dist/managed-runtime-publication.js.map +1 -0
  140. package/dist/managed-startup.d.ts +8 -0
  141. package/dist/managed-startup.d.ts.map +1 -0
  142. package/dist/managed-startup.js +12 -0
  143. package/dist/managed-startup.js.map +1 -0
  144. package/dist/memory-retrieval.d.ts.map +1 -1
  145. package/dist/memory-retrieval.js +31 -18
  146. package/dist/memory-retrieval.js.map +1 -1
  147. package/dist/metrics.d.ts.map +1 -1
  148. package/dist/metrics.js +12 -2
  149. package/dist/metrics.js.map +1 -1
  150. package/dist/modules/catalog.js +1 -1
  151. package/dist/modules/catalog.js.map +1 -1
  152. package/dist/modules/known-tools.d.ts +1 -1
  153. package/dist/modules/known-tools.d.ts.map +1 -1
  154. package/dist/modules/known-tools.js +0 -3
  155. package/dist/modules/known-tools.js.map +1 -1
  156. package/dist/posted-reply-history.d.ts.map +1 -1
  157. package/dist/posted-reply-history.js +7 -0
  158. package/dist/posted-reply-history.js.map +1 -1
  159. package/dist/request-model-override.d.ts +2 -2
  160. package/dist/request-model-override.js +8 -4
  161. package/dist/request-model-override.js.map +1 -1
  162. package/dist/run-history.d.ts +6 -2
  163. package/dist/run-history.d.ts.map +1 -1
  164. package/dist/run-history.js +511 -118
  165. package/dist/run-history.js.map +1 -1
  166. package/dist/runtime-provenance.d.ts.map +1 -1
  167. package/dist/runtime-provenance.js +10 -5
  168. package/dist/runtime-provenance.js.map +1 -1
  169. package/dist/tui-command.d.ts.map +1 -1
  170. package/dist/tui-command.js +2 -1
  171. package/dist/tui-command.js.map +1 -1
  172. package/dist/ui.js +1 -1
  173. package/dist/ui.js.map +1 -1
  174. package/dist/wizard/from-flags.d.ts +0 -1
  175. package/dist/wizard/from-flags.d.ts.map +1 -1
  176. package/dist/wizard/from-flags.js +1 -8
  177. package/dist/wizard/from-flags.js.map +1 -1
  178. package/dist/wizard/index.d.ts +1 -1
  179. package/dist/wizard/index.d.ts.map +1 -1
  180. package/dist/wizard/index.js +1 -1
  181. package/dist/wizard/index.js.map +1 -1
  182. package/dist/wizard/presets.d.ts +3 -12
  183. package/dist/wizard/presets.d.ts.map +1 -1
  184. package/dist/wizard/presets.js +3 -15
  185. package/dist/wizard/presets.js.map +1 -1
  186. package/dist/wizard/prompts.d.ts.map +1 -1
  187. package/dist/wizard/prompts.js +3 -4
  188. package/dist/wizard/prompts.js.map +1 -1
  189. package/dist/wizard/run.js +3 -3
  190. package/dist/wizard/run.js.map +1 -1
  191. package/package.json +15 -16
  192. package/schema/mono-agent.config.schema.json +4 -4
  193. package/skills/mono-agent-composer/SKILL.md +13 -5
  194. package/skills/mono-agent-composer/agents/openai.yaml +1 -1
  195. package/skills/mono-agent-composer/references/config-blueprint.md +18 -10
  196. package/skills/mono-agent-composer/references/discovery-questions.md +8 -5
  197. package/skills/mono-agent-composer/references/feature-coverage.md +10 -9
  198. package/skills/mono-agent-composer/references/package-map.md +10 -4
  199. package/skills/mono-agent-composer/references/playbooks.md +6 -6
  200. 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 || telegramAskAllowed || telegramFileAllowed;
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
- }, telegramAskBridge)
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 conversationId = optionalString(env.MONO_AGENT_ADAPTER_TOOLS_PRODUCING_CONVERSATION_ID);
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
- ...(conversationId === undefined ? {} : { conversationId }),
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?.conversationId !== undefined) {
301
- registerAskUserTool(server, { ...settings.askUser, conversationId: settings.askUser.conversationId }, options.fetchImpl ?? globalThis.fetch);
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 ONE consolidated free-text question on the current conversation and WAIT for their reply (blocking, up to the configured timeout default 10 minutes). Returns the user's answer text. Consolidate everything you need into a single question a second concurrent ask fails. If the wait times out, the tool returns without an answer and the user's late reply will arrive as their next message: finish the turn gracefully (proceed with sensible defaults and say what you assumed).",
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
- question: z.string().min(1).describe("The full question to show the user (plain text, no markdown)."),
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.conversationId,
317
- producerConversationId: settings.conversationId,
325
+ conversationId: settings.interactionConversationId,
326
+ producerConversationId: settings.producerConversationId,
318
327
  runId: settings.runId,
319
- toolName: "AskUser",
320
- question: args.question,
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.askId !== "string") {
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.askId, "AskUser", extra, fetchImpl, extra.signal);
341
+ return await awaitBridgeAsk(settings, created.body.interactionId, extra, fetchImpl, extra.signal);
333
342
  });
334
343
  }
335
- function askToolResult(text, structured, extraStructured = {}) {
344
+ function askToolResult(text, structured) {
336
345
  return {
337
346
  content: [{ type: "text", text }],
338
- structuredContent: { ok: true, ...extraStructured, ...structured },
347
+ structuredContent: { ok: true, ...structured },
339
348
  };
340
349
  }
341
- async function awaitBridgeAsk(settings, askId, toolName, extra, fetchImpl, signal, extraStructured = {}) {
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(askId)}?waitMs=${String(ASK_USER_POLL_WAIT_MS)}`, undefined, signal);
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(`${toolName}: lost the pending ask (HTTP ${String(poll.status)}).`);
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" && typeof poll.body.answer === "string") {
354
- const answeredText = toolName === "TelegramAskButtons"
355
- ? `The user tapped:\n${poll.body.answer}`
356
- : `The user answered:\n${poll.body.answer}`;
357
- return askToolResult(answeredText, { answered: true, answer: poll.body.answer }, extraStructured);
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
- return askToolResult("The user did not answer within the wait window. Their reply will arrive as their next message — wrap up this turn gracefully (proceed with sensible defaults and say what you assumed).", { answered: false, reason: "timeout" }, extraStructured);
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
- }, extraStructured);
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, askId);
387
+ await cleanupBridgeAskBestEffort(settings, fetchImpl, interactionId);
375
388
  throw error;
376
389
  }
377
390
  }
378
- async function cleanupBridgeAskBestEffort(settings, fetchImpl, askId) {
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(askId)}`, undefined, controller.signal).then(() => undefined, () => undefined);
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 using the configured Telegram adapter bot token.",
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: "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; strict run-output paths are always read through a pinned descriptor.",
796
- inputSchema: {
797
- kind: z.enum(["document", "photo"]).describe("`document` for any file (downloadable), `photo` for an image shown inline."),
798
- chat_id: z.union([z.string().min(1), z.number().int()]).describe("Telegram chat id from the adapter allowlist."),
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
- assertTelegramChatAllowed(settings, args.chat_id);
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 client.sendDocument({
833
- chat_id: args.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 client.sendDocument({
861
- chat_id: args.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 client.sendPhoto({
867
- chat_id: args.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, askBridge) {
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("TelegramSendMessage: producing Telegram conversation context is unavailable.");
1016
+ throw new Error(`${toolName}: producing Telegram conversation context is unavailable.`);
1079
1017
  }
1080
1018
  if (normalized !== producingChatId) {
1081
- throw new Error("TelegramSendMessage: chat_id must match the producing Telegram conversation.");
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("TelegramSendMessage: chat_id is not allowed by Telegram adapter config.");
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)