@mono-agent/agent-app 0.13.0 → 0.15.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 (192) hide show
  1. package/README.md +164 -37
  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 +133 -287
  7. package/dist/adapter-send-tools.js.map +1 -1
  8. package/dist/app-config.d.ts.map +1 -1
  9. package/dist/app-config.js +3 -0
  10. package/dist/app-config.js.map +1 -1
  11. package/dist/app-controller-channels.d.ts +41 -7
  12. package/dist/app-controller-channels.d.ts.map +1 -1
  13. package/dist/app-controller-channels.js +4 -4
  14. package/dist/app-controller-channels.js.map +1 -1
  15. package/dist/app-controller-continuation.d.ts +43 -11
  16. package/dist/app-controller-continuation.d.ts.map +1 -1
  17. package/dist/app-controller-continuation.js +1 -5
  18. package/dist/app-controller-continuation.js.map +1 -1
  19. package/dist/app-controller-lifecycle.d.ts +34 -5
  20. package/dist/app-controller-lifecycle.d.ts.map +1 -1
  21. package/dist/app-controller-lifecycle.js.map +1 -1
  22. package/dist/app-controller-maintenance.d.ts +46 -8
  23. package/dist/app-controller-maintenance.d.ts.map +1 -1
  24. package/dist/app-controller-maintenance.js +64 -1
  25. package/dist/app-controller-maintenance.js.map +1 -1
  26. package/dist/app-controller-memory-health.d.ts +39 -10
  27. package/dist/app-controller-memory-health.d.ts.map +1 -1
  28. package/dist/app-controller-memory-health.js.map +1 -1
  29. package/dist/app-controller-memory.d.ts +27 -5
  30. package/dist/app-controller-memory.d.ts.map +1 -1
  31. package/dist/app-controller-memory.js +0 -1
  32. package/dist/app-controller-memory.js.map +1 -1
  33. package/dist/app-controller-responder.d.ts +53 -7
  34. package/dist/app-controller-responder.d.ts.map +1 -1
  35. package/dist/app-controller-responder.js +5 -5
  36. package/dist/app-controller-responder.js.map +1 -1
  37. package/dist/app-controller-traceability.d.ts +63 -14
  38. package/dist/app-controller-traceability.d.ts.map +1 -1
  39. package/dist/app-controller-traceability.js.map +1 -1
  40. package/dist/app-controller-types.d.ts +63 -0
  41. package/dist/app-controller-types.d.ts.map +1 -0
  42. package/dist/app-controller-types.js +2 -0
  43. package/dist/app-controller-types.js.map +1 -0
  44. package/dist/app-controller-utils.d.ts +1 -1
  45. package/dist/app-controller-utils.d.ts.map +1 -1
  46. package/dist/app-controller-utils.js +1 -1
  47. package/dist/app-controller-utils.js.map +1 -1
  48. package/dist/app-controller.d.ts +5 -62
  49. package/dist/app-controller.d.ts.map +1 -1
  50. package/dist/app-controller.js +0 -6
  51. package/dist/app-controller.js.map +1 -1
  52. package/dist/background-environment.d.ts +1 -0
  53. package/dist/background-environment.d.ts.map +1 -1
  54. package/dist/background-environment.js +1 -0
  55. package/dist/background-environment.js.map +1 -1
  56. package/dist/background-lifecycle-utils.d.ts +27 -0
  57. package/dist/background-lifecycle-utils.d.ts.map +1 -0
  58. package/dist/background-lifecycle-utils.js +69 -0
  59. package/dist/background-lifecycle-utils.js.map +1 -0
  60. package/dist/background-log-maintenance.d.ts +6 -0
  61. package/dist/background-log-maintenance.d.ts.map +1 -0
  62. package/dist/background-log-maintenance.js +185 -0
  63. package/dist/background-log-maintenance.js.map +1 -0
  64. package/dist/background-runtime.d.ts +13 -2
  65. package/dist/background-runtime.d.ts.map +1 -1
  66. package/dist/background-runtime.js +27 -3
  67. package/dist/background-runtime.js.map +1 -1
  68. package/dist/background.d.ts +28 -5
  69. package/dist/background.d.ts.map +1 -1
  70. package/dist/background.js +229 -297
  71. package/dist/background.js.map +1 -1
  72. package/dist/channel-drivers/cron.js +8 -1
  73. package/dist/channel-drivers/cron.js.map +1 -1
  74. package/dist/channel-drivers/native-notify.d.ts +2 -0
  75. package/dist/channel-drivers/native-notify.d.ts.map +1 -1
  76. package/dist/channel-drivers/native-notify.js +14 -2
  77. package/dist/channel-drivers/native-notify.js.map +1 -1
  78. package/dist/channel-drivers/slack.d.ts.map +1 -1
  79. package/dist/channel-drivers/slack.js +49 -0
  80. package/dist/channel-drivers/slack.js.map +1 -1
  81. package/dist/channel-drivers/telegram.d.ts.map +1 -1
  82. package/dist/channel-drivers/telegram.js +7 -18
  83. package/dist/channel-drivers/telegram.js.map +1 -1
  84. package/dist/channel-drivers/tui.d.ts.map +1 -1
  85. package/dist/channel-drivers/tui.js +8 -0
  86. package/dist/channel-drivers/tui.js.map +1 -1
  87. package/dist/channel-plugins.d.ts +2 -11
  88. package/dist/channel-plugins.d.ts.map +1 -1
  89. package/dist/channel-plugins.js +63 -60
  90. package/dist/channel-plugins.js.map +1 -1
  91. package/dist/channel-status-display.js +2 -6
  92. package/dist/channel-status-display.js.map +1 -1
  93. package/dist/channels.d.ts +5 -6
  94. package/dist/channels.d.ts.map +1 -1
  95. package/dist/channels.js +7 -5
  96. package/dist/channels.js.map +1 -1
  97. package/dist/cli-args.d.ts +8 -2
  98. package/dist/cli-args.d.ts.map +1 -1
  99. package/dist/cli-args.js +98 -27
  100. package/dist/cli-args.js.map +1 -1
  101. package/dist/cli-background-command.d.ts.map +1 -1
  102. package/dist/cli-background-command.js +90 -15
  103. package/dist/cli-background-command.js.map +1 -1
  104. package/dist/cli-commands.d.ts.map +1 -1
  105. package/dist/cli-commands.js +0 -14
  106. package/dist/cli-commands.js.map +1 -1
  107. package/dist/cli-help.d.ts.map +1 -1
  108. package/dist/cli-help.js +1 -7
  109. package/dist/cli-help.js.map +1 -1
  110. package/dist/cli-runs-command.d.ts +0 -3
  111. package/dist/cli-runs-command.d.ts.map +1 -1
  112. package/dist/cli-runs-command.js +1 -5
  113. package/dist/cli-runs-command.js.map +1 -1
  114. package/dist/cli-validate-config-command.d.ts.map +1 -1
  115. package/dist/cli-validate-config-command.js +12 -8
  116. package/dist/cli-validate-config-command.js.map +1 -1
  117. package/dist/config-reference.d.ts +4 -1
  118. package/dist/config-reference.d.ts.map +1 -1
  119. package/dist/config-reference.js +245 -69
  120. package/dist/config-reference.js.map +1 -1
  121. package/dist/configured-agent.d.ts +2 -8
  122. package/dist/configured-agent.d.ts.map +1 -1
  123. package/dist/configured-agent.js +3 -25
  124. package/dist/configured-agent.js.map +1 -1
  125. package/dist/consumer-contract.d.ts.map +1 -1
  126. package/dist/consumer-contract.js +2 -4
  127. package/dist/consumer-contract.js.map +1 -1
  128. package/dist/doctor-observability.d.ts +7 -0
  129. package/dist/doctor-observability.d.ts.map +1 -0
  130. package/dist/doctor-observability.js +80 -0
  131. package/dist/doctor-observability.js.map +1 -0
  132. package/dist/doctor-types.d.ts +17 -0
  133. package/dist/doctor-types.d.ts.map +1 -0
  134. package/dist/doctor-types.js +2 -0
  135. package/dist/doctor-types.js.map +1 -0
  136. package/dist/doctor.d.ts +2 -12
  137. package/dist/doctor.d.ts.map +1 -1
  138. package/dist/doctor.js +8 -108
  139. package/dist/doctor.js.map +1 -1
  140. package/dist/first-run-readiness.d.ts +1 -1
  141. package/dist/first-run-readiness.d.ts.map +1 -1
  142. package/dist/first-run-readiness.js +1 -1
  143. package/dist/first-run-readiness.js.map +1 -1
  144. package/dist/index.d.ts +0 -2
  145. package/dist/index.d.ts.map +1 -1
  146. package/dist/index.js +0 -1
  147. package/dist/index.js.map +1 -1
  148. package/dist/interaction-bridge.d.ts +5 -22
  149. package/dist/interaction-bridge.d.ts.map +1 -1
  150. package/dist/interaction-bridge.js +251 -141
  151. package/dist/interaction-bridge.js.map +1 -1
  152. package/dist/launchd.d.ts +33 -1
  153. package/dist/launchd.d.ts.map +1 -1
  154. package/dist/launchd.js +129 -2
  155. package/dist/launchd.js.map +1 -1
  156. package/dist/managed-web-logs.d.ts +16 -0
  157. package/dist/managed-web-logs.d.ts.map +1 -0
  158. package/dist/managed-web-logs.js +153 -0
  159. package/dist/managed-web-logs.js.map +1 -0
  160. package/dist/modules/catalog.js +1 -1
  161. package/dist/modules/catalog.js.map +1 -1
  162. package/dist/modules/known-tools.d.ts +1 -1
  163. package/dist/modules/known-tools.d.ts.map +1 -1
  164. package/dist/modules/known-tools.js +0 -3
  165. package/dist/modules/known-tools.js.map +1 -1
  166. package/dist/posted-reply-history.d.ts.map +1 -1
  167. package/dist/posted-reply-history.js +3 -0
  168. package/dist/posted-reply-history.js.map +1 -1
  169. package/dist/run-history.d.ts +6 -2
  170. package/dist/run-history.d.ts.map +1 -1
  171. package/dist/run-history.js +476 -96
  172. package/dist/run-history.js.map +1 -1
  173. package/dist/ui.js +1 -1
  174. package/dist/ui.js.map +1 -1
  175. package/dist/web-command.d.ts +4 -1
  176. package/dist/web-command.d.ts.map +1 -1
  177. package/dist/web-command.js +31 -1
  178. package/dist/web-command.js.map +1 -1
  179. package/dist/wizard/prompts.d.ts.map +1 -1
  180. package/dist/wizard/prompts.js +3 -4
  181. package/dist/wizard/prompts.js.map +1 -1
  182. package/dist/wizard/run.js +3 -3
  183. package/dist/wizard/run.js.map +1 -1
  184. package/package.json +16 -16
  185. package/schema/mono-agent.config.schema.json +282 -164
  186. package/skills/mono-agent-composer/SKILL.md +5 -5
  187. package/skills/mono-agent-composer/references/config-blueprint.md +6 -13
  188. package/skills/mono-agent-composer/references/discovery-questions.md +1 -1
  189. package/skills/mono-agent-composer/references/feature-coverage.md +7 -8
  190. package/skills/mono-agent-composer/references/package-map.md +5 -6
  191. package/skills/mono-agent-composer/references/playbooks.md +3 -3
  192. package/skills/mono-agent-composer/references/validation.md +21 -1
@@ -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,42 +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 = `The user answered:\n${poll.body.answer}`;
355
- 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
+ });
356
369
  }
357
370
  if (status === "expired") {
358
- 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 });
359
373
  }
360
374
  if (status === "cancelled") {
361
375
  return askToolResult("The user cancelled the current run. Stop this task.", {
362
376
  answered: false,
363
377
  reason: "cancelled",
364
- }, extraStructured);
378
+ interactionId,
379
+ });
365
380
  }
366
381
  }
367
382
  }
@@ -369,11 +384,56 @@ async function awaitBridgeAsk(settings, askId, toolName, extra, fetchImpl, signa
369
384
  // A cancelled MCP call must not leave a bridge ask pending until its full
370
385
  // human timeout. Cleanup gets its own bounded signal (not the aborted call
371
386
  // signal), and its failure must never replace the primary tool failure.
372
- await cleanupBridgeAskBestEffort(settings, fetchImpl, askId);
387
+ await cleanupBridgeAskBestEffort(settings, fetchImpl, interactionId);
373
388
  throw error;
374
389
  }
375
390
  }
376
- 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) {
377
437
  const controller = new AbortController();
378
438
  let deadlineTimer;
379
439
  const deadline = new Promise((resolve) => {
@@ -385,7 +445,7 @@ async function cleanupBridgeAskBestEffort(settings, fetchImpl, askId) {
385
445
  });
386
446
  // Attach the rejection handler before racing. If a test seam or nonstandard
387
447
  // fetch ignores abort and rejects later, it cannot become an unhandled error.
388
- 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);
389
449
  try {
390
450
  await Promise.race([cleanup, deadline]);
391
451
  }
@@ -564,16 +624,22 @@ function registerSlackSendTool(server, settings, client, formatMarkdownForSlack,
564
624
  };
565
625
  });
566
626
  }
567
- function registerTelegramSendTool(server, settings, client, deliveryHistory, fetchImpl = globalThis.fetch) {
627
+ function registerTelegramSendTool(server, settings, client, adapter, deliveryHistory, fetchImpl = globalThis.fetch) {
568
628
  server.registerTool("TelegramSendMessage", {
569
629
  title: "Send Telegram message",
570
- 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.",
571
631
  inputSchema: {
572
632
  chat_id: z.union([z.string().min(1), z.number().int()]).describe("Telegram chat id from the adapter allowlist."),
573
633
  text: z.string().min(1).describe("Message text to send."),
574
634
  parse_mode: z.string().min(1).optional().describe("Optional Telegram parse mode, e.g. MarkdownV2 or HTML."),
575
635
  reply_to_message_id: z.number().int().optional().describe("Optional message id to reply to."),
576
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."),
577
643
  },
578
644
  }, async (args, extra) => {
579
645
  assertTelegramChatAllowed(settings, args.chat_id, "TelegramSendMessage");
@@ -583,6 +649,16 @@ function registerTelegramSendTool(server, settings, client, deliveryHistory, fet
583
649
  ...(args.parse_mode === undefined ? {} : { parse_mode: args.parse_mode }),
584
650
  ...(args.reply_to_message_id === undefined ? {} : { reply_to_message_id: args.reply_to_message_id }),
585
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
+ }),
586
662
  }, { signal: extra.signal });
587
663
  const history = await recordAdapterDeliveryHistory({
588
664
  settings: deliveryHistory,
@@ -598,246 +674,12 @@ function registerTelegramSendTool(server, settings, client, deliveryHistory, fet
598
674
  ok: true,
599
675
  chat_id: result.chat.id,
600
676
  message_id: result.message_id,
677
+ ...(args.reply_options === undefined ? {} : { reply_options: args.reply_options }),
601
678
  ...(history === undefined ? {} : { history }),
602
679
  },
603
680
  };
604
681
  });
605
682
  }
606
- /** Marker appended (buttons kept) when a Telegram ask times out. */
607
- const TELEGRAM_ASK_EXPIRED_MARKER = "⌛ Expired — tapping a button still reaches me and starts a new turn.";
608
- /** Marker appended (buttons kept) when a Telegram ask is cancelled. */
609
- const TELEGRAM_ASK_CANCELLED_MARKER = "🚫 Cancelled — tapping a button still reaches me and starts a new turn.";
610
- /** Bounded own-signal budget for the best-effort expiry edit — never the aborted call signal. */
611
- const TELEGRAM_ASK_EXPIRY_EDIT_TIMEOUT_MS = 2_000;
612
- /** A Telegram 400 is the parse-entity rejection we fall back from (raw text, no parse_mode). */
613
- function isTelegramParseError(error, adapter) {
614
- return error instanceof adapter.TelegramApiError && error.errorCode === 400;
615
- }
616
- /**
617
- * Send the ask keyboard rendered as MarkdownV2, retrying ONCE with the raw source and
618
- * no parse_mode when Telegram rejects the entities with a 400 parse error. Any other
619
- * failure propagates so the caller can clean up a pending bridge ask.
620
- */
621
- async function sendTelegramAskKeyboard(client, adapter, params) {
622
- try {
623
- return await client.sendMessage({
624
- chat_id: params.chat_id,
625
- text: params.rendered,
626
- parse_mode: "MarkdownV2",
627
- reply_markup: params.replyMarkup,
628
- }, { signal: params.signal });
629
- }
630
- catch (error) {
631
- if (!isTelegramParseError(error, adapter)) {
632
- throw error;
633
- }
634
- return await client.sendMessage({
635
- chat_id: params.chat_id,
636
- text: params.rawSource,
637
- reply_markup: params.replyMarkup,
638
- }, { signal: params.signal });
639
- }
640
- }
641
- /**
642
- * Best-effort: append an expiry/cancel note to the buttons message while KEEPING the
643
- * inline keyboard (Telegram strips the keyboard on `editMessageText` unless reply_markup
644
- * is re-sent). Renders through the same MarkdownV2-with-plain-fallback path. Uses its own
645
- * bounded signal — never the aborted call signal — and swallows every failure so a failed
646
- * edit can never replace the primary ask result.
647
- */
648
- async function editExpiredTelegramAskMessage(client, adapter, params) {
649
- try {
650
- const source = `${params.rawSource}\n\n${params.marker}`;
651
- const rendered = adapter.renderTelegramMarkdown(source);
652
- const signal = AbortSignal.timeout(TELEGRAM_ASK_EXPIRY_EDIT_TIMEOUT_MS);
653
- try {
654
- await client.editMessageText({
655
- chat_id: params.chat_id,
656
- message_id: params.message_id,
657
- text: rendered,
658
- parse_mode: "MarkdownV2",
659
- reply_markup: params.replyMarkup,
660
- }, { signal });
661
- }
662
- catch (error) {
663
- if (!isTelegramParseError(error, adapter)) {
664
- throw error;
665
- }
666
- await client.editMessageText({
667
- chat_id: params.chat_id,
668
- message_id: params.message_id,
669
- text: source,
670
- reply_markup: params.replyMarkup,
671
- }, { signal });
672
- }
673
- }
674
- catch {
675
- // Best-effort: a failed expiry edit must never replace the ask result.
676
- }
677
- }
678
- /**
679
- * Best-effort: remove a keyboard that was sent but could not be registered with
680
- * the interaction bridge. Re-editing without reply_markup clears the buttons;
681
- * use an independent bounded signal and preserve the primary bridge failure.
682
- */
683
- async function dismissUnregisteredTelegramAskKeyboard(client, adapter, params) {
684
- try {
685
- const rendered = adapter.renderTelegramMarkdown(params.rawSource);
686
- const signal = AbortSignal.timeout(TELEGRAM_ASK_EXPIRY_EDIT_TIMEOUT_MS);
687
- try {
688
- await client.editMessageText({
689
- chat_id: params.chat_id,
690
- message_id: params.message_id,
691
- text: rendered,
692
- parse_mode: "MarkdownV2",
693
- }, { signal });
694
- }
695
- catch (error) {
696
- if (!isTelegramParseError(error, adapter)) {
697
- throw error;
698
- }
699
- await client.editMessageText({
700
- chat_id: params.chat_id,
701
- message_id: params.message_id,
702
- text: params.rawSource,
703
- }, { signal });
704
- }
705
- }
706
- catch {
707
- // Best-effort: keyboard cleanup must not replace the bridge failure.
708
- }
709
- }
710
- function registerTelegramAskTool(server, settings, client, adapter, fetchImpl) {
711
- server.registerTool("TelegramAskButtons", {
712
- title: "Ask via Telegram buttons",
713
- 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 either a button tap or the owner's next plain-text custom reply (up to the ask timeout) and returns that answer; 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.",
714
- inputSchema: {
715
- chat_id: z.union([z.string().min(1), z.number().int()]).describe("Telegram chat id from the adapter allowlist."),
716
- question: z.string().min(1).describe("The question to show above the buttons."),
717
- options: z
718
- .array(z.string().min(1).max(100))
719
- .min(2)
720
- .max(adapter.TELEGRAM_ASK_MAX_OPTIONS)
721
- .describe("Button labels (2–8). A tap returns its label; while waiting, plain text is accepted as a custom answer."),
722
- note: z.string().min(1).optional().describe("Optional extra context shown beneath the question."),
723
- wait: z
724
- .boolean()
725
- .optional()
726
- .describe("Default true: block for a button tap or custom text reply (up to the ask timeout). Set false on proactive/scheduled (cron/heartbeat) turns — send the buttons and return immediately; a tapped answer arrives later as a new message quoting the question."),
727
- },
728
- }, async (args, extra) => {
729
- assertTelegramChatAllowed(settings, args.chat_id, "TelegramAskButtons");
730
- const inlineKeyboard = args.options.map((label, index) => [
731
- { text: label, callback_data: adapter.telegramAskCallbackData(index) },
732
- ]);
733
- const replyMarkup = { inline_keyboard: inlineKeyboard };
734
- // Raw source is what the bridge records and what the plain-text fallback (and the
735
- // expiry edit) re-send; the rendered form is only for the MarkdownV2 send.
736
- const source = args.note === undefined ? args.question : `${args.question}\n\n${args.note}`;
737
- const rendered = adapter.renderTelegramMarkdown(source);
738
- // Non-blocking mode (proactive/scheduled turns): send the buttons and return.
739
- // No bridge is consulted — this must work even with no bridge configured.
740
- if (args.wait === false) {
741
- const sent = await sendTelegramAskKeyboard(client, adapter, {
742
- chat_id: args.chat_id,
743
- rendered,
744
- rawSource: source,
745
- replyMarkup,
746
- signal: extra.signal,
747
- });
748
- 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 });
749
- }
750
- // Blocking mode requires the interaction bridge to correlate the tap.
751
- const bridge = settings.askBridge;
752
- if (bridge === undefined) {
753
- return askToolResult("TelegramAskButtons requires a live interaction bridge, but none is configured.", {
754
- answered: false,
755
- reason: "unsupported_channel",
756
- });
757
- }
758
- const conversationId = `telegram:${String(args.chat_id)}`;
759
- const created = await askBridgeRequest(bridge, fetchImpl, "POST", "/v1/asks", {
760
- conversationId,
761
- producerConversationId: bridge.conversationId,
762
- runId: bridge.runId,
763
- toolName: "TelegramAskButtons",
764
- question: source,
765
- options: args.options,
766
- timeoutMs: bridge.timeoutMs,
767
- postQuestion: false,
768
- answerKind: "callback",
769
- }, extra.signal);
770
- if (created.status === 409) {
771
- 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 });
772
- }
773
- if (created.status === 501) {
774
- 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 });
775
- }
776
- if (created.status !== 201 || typeof created.body.askId !== "string") {
777
- throw new Error(`TelegramAskButtons: the interaction bridge rejected the ask (HTTP ${String(created.status)}).`);
778
- }
779
- const askId = created.body.askId;
780
- let result;
781
- try {
782
- result = await sendTelegramAskKeyboard(client, adapter, {
783
- chat_id: args.chat_id,
784
- rendered,
785
- rawSource: source,
786
- replyMarkup,
787
- signal: extra.signal,
788
- });
789
- }
790
- catch (error) {
791
- await cleanupBridgeAskBestEffort(bridge, fetchImpl, askId);
792
- throw error;
793
- }
794
- const presentation = await (async () => {
795
- try {
796
- return await askBridgeRequest(bridge, fetchImpl, "PUT", `/v1/asks/${encodeURIComponent(askId)}/presentation`, { messageRef: String(result.message_id) }, extra.signal);
797
- }
798
- catch (error) {
799
- await Promise.all([
800
- cleanupBridgeAskBestEffort(bridge, fetchImpl, askId),
801
- dismissUnregisteredTelegramAskKeyboard(client, adapter, {
802
- chat_id: result.chat.id,
803
- message_id: result.message_id,
804
- rawSource: source,
805
- }),
806
- ]);
807
- throw error;
808
- }
809
- })();
810
- if (presentation.status !== 204) {
811
- await Promise.all([
812
- cleanupBridgeAskBestEffort(bridge, fetchImpl, askId),
813
- dismissUnregisteredTelegramAskKeyboard(client, adapter, {
814
- chat_id: result.chat.id,
815
- message_id: result.message_id,
816
- rawSource: source,
817
- }),
818
- ]);
819
- throw new Error(`TelegramAskButtons: the interaction bridge rejected the keyboard reference (HTTP ${String(presentation.status)}).`);
820
- }
821
- const askResult = await awaitBridgeAsk(bridge, askId, "TelegramAskButtons", extra, fetchImpl, extra.signal, {
822
- chat_id: result.chat.id,
823
- message_id: result.message_id,
824
- options: args.options,
825
- });
826
- // An expired/cancelled ask leaves a live-looking keyboard forever; append a note
827
- // while keeping the buttons (a tap still starts a fresh turn). Best-effort only.
828
- const reason = askResult.structuredContent.reason;
829
- if (reason === "timeout" || reason === "cancelled") {
830
- await editExpiredTelegramAskMessage(client, adapter, {
831
- chat_id: result.chat.id,
832
- message_id: result.message_id,
833
- rawSource: source,
834
- marker: reason === "timeout" ? TELEGRAM_ASK_EXPIRED_MARKER : TELEGRAM_ASK_CANCELLED_MARKER,
835
- replyMarkup,
836
- });
837
- }
838
- return askResult;
839
- });
840
- }
841
683
  /**
842
684
  * Register the single `TelegramSendFile` tool. A required `kind` param selects
843
685
  * `"document"` (any file, shown as a downloadable document) or `"photo"` (an image
@@ -872,6 +714,11 @@ function registerTelegramSendFileTool(server, settings, client, adapter) {
872
714
  }, async (args, extra) => {
873
715
  extra.signal.throwIfAborted();
874
716
  const kind = args.kind;
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
+ }
875
722
  const requestedChatId = "chat_id" in args ? args.chat_id : undefined;
876
723
  const chatId = resolveTelegramSendFileChatId(settings, requestedChatId);
877
724
  if ((args.data !== undefined) === (args.path !== undefined)) {
@@ -898,7 +745,7 @@ function registerTelegramSendFileTool(server, settings, client, adapter) {
898
745
  throw new Error(`file exceeds the ${String(maxUploadBytes)}-byte upload cap.`);
899
746
  }
900
747
  try {
901
- const sent = await client.sendDocument({
748
+ const sent = await sendDocument({
902
749
  chat_id: chatId,
903
750
  document: pathToFileURL(uploadPath).href,
904
751
  ...(args.caption === undefined ? {} : { caption: args.caption }),
@@ -923,13 +770,13 @@ function registerTelegramSendFileTool(server, settings, client, adapter) {
923
770
  signal: extra.signal,
924
771
  });
925
772
  const result = kind === "document"
926
- ? await client.sendDocument({
773
+ ? await sendDocument({
927
774
  chat_id: chatId,
928
775
  document: bytes,
929
776
  filename,
930
777
  ...(args.caption === undefined ? {} : { caption: args.caption }),
931
778
  }, { signal: extra.signal })
932
- : await client.sendPhoto({
779
+ : await sendPhoto({
933
780
  chat_id: chatId,
934
781
  photo: bytes,
935
782
  filename,
@@ -1125,7 +972,7 @@ async function resolveSlackSendToolSettings(input, options) {
1125
972
  return undefined;
1126
973
  }
1127
974
  }
1128
- async function resolveTelegramSendToolSettings(input, options, tools, askBridge) {
975
+ async function resolveTelegramSendToolSettings(input, options, tools) {
1129
976
  try {
1130
977
  const adapter = await loadTelegramModule();
1131
978
  const config = await adapter.loadTelegramAdapterConfig({ env: input.env, jsonPath: input.configPath });
@@ -1142,7 +989,6 @@ async function resolveTelegramSendToolSettings(input, options, tools, askBridge)
1142
989
  ...(config.apiRoot === undefined ? {} : { apiRoot: config.apiRoot }),
1143
990
  maxUploadBytes: config.attachments?.maxUploadBytes ?? adapter.DEFAULT_ATTACHMENT_MAX_BYTES,
1144
991
  tools,
1145
- ...(askBridge === undefined ? {} : { askBridge }),
1146
992
  ...(config.sendTools === undefined ? {} : { sendTools: config.sendTools }),
1147
993
  ...(producingConversationId === undefined ? {} : { producingConversationId }),
1148
994
  ...(runOutputDir === undefined ? {} : { runOutputDir }),