@mono-agent/agent-app 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -5
- package/dist/adapter-send-tools-main.js +2 -2
- package/dist/adapter-send-tools-main.js.map +1 -1
- package/dist/adapter-send-tools.d.ts +72 -6
- package/dist/adapter-send-tools.d.ts.map +1 -1
- package/dist/adapter-send-tools.js +413 -38
- package/dist/adapter-send-tools.js.map +1 -1
- package/dist/app-config.d.ts +48 -11
- package/dist/app-config.d.ts.map +1 -1
- package/dist/app-config.js +98 -24
- package/dist/app-config.js.map +1 -1
- package/dist/app.d.ts +10 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +659 -48
- package/dist/app.js.map +1 -1
- package/dist/artifact-retention.d.ts +32 -0
- package/dist/artifact-retention.d.ts.map +1 -0
- package/dist/artifact-retention.js +111 -0
- package/dist/artifact-retention.js.map +1 -0
- package/dist/audit-runs.d.ts +12 -0
- package/dist/audit-runs.d.ts.map +1 -0
- package/dist/audit-runs.js +68 -0
- package/dist/audit-runs.js.map +1 -0
- package/dist/backfill.d.ts +3 -7
- package/dist/backfill.d.ts.map +1 -1
- package/dist/backfill.js +115 -11
- package/dist/backfill.js.map +1 -1
- package/dist/background.d.ts +8 -1
- package/dist/background.d.ts.map +1 -1
- package/dist/background.js +202 -4
- package/dist/background.js.map +1 -1
- package/dist/broadcast-recorder.d.ts +27 -0
- package/dist/broadcast-recorder.d.ts.map +1 -0
- package/dist/broadcast-recorder.js +82 -0
- package/dist/broadcast-recorder.js.map +1 -0
- package/dist/channel-config-view.d.ts +28 -0
- package/dist/channel-config-view.d.ts.map +1 -0
- package/dist/channel-config-view.js +79 -0
- package/dist/channel-config-view.js.map +1 -0
- package/dist/channel-gate.d.ts +22 -0
- package/dist/channel-gate.d.ts.map +1 -0
- package/dist/channel-gate.js +35 -0
- package/dist/channel-gate.js.map +1 -0
- package/dist/channel-plugins.d.ts +39 -0
- package/dist/channel-plugins.d.ts.map +1 -0
- package/dist/channel-plugins.js +346 -0
- package/dist/channel-plugins.js.map +1 -0
- package/dist/channels.d.ts +62 -73
- package/dist/channels.d.ts.map +1 -1
- package/dist/channels.js +833 -126
- package/dist/channels.js.map +1 -1
- package/dist/cli.d.ts +53 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +696 -28
- package/dist/cli.js.map +1 -1
- package/dist/config-reference.d.ts +24 -0
- package/dist/config-reference.d.ts.map +1 -0
- package/dist/config-reference.js +580 -0
- package/dist/config-reference.js.map +1 -0
- package/dist/configured-agent.d.ts +104 -0
- package/dist/configured-agent.d.ts.map +1 -0
- package/dist/configured-agent.js +578 -0
- package/dist/configured-agent.js.map +1 -0
- package/dist/consumer-contract.d.ts +36 -0
- package/dist/consumer-contract.d.ts.map +1 -0
- package/dist/consumer-contract.js +290 -0
- package/dist/consumer-contract.js.map +1 -0
- package/dist/doctor.d.ts +9 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +252 -55
- package/dist/doctor.js.map +1 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +23 -7
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +91 -26
- package/dist/init.js.map +1 -1
- package/dist/install-skill.d.ts +1 -1
- package/dist/install-skill.js +2 -2
- package/dist/install-skill.js.map +1 -1
- package/dist/interaction-bridge.d.ts +72 -0
- package/dist/interaction-bridge.d.ts.map +1 -0
- package/dist/interaction-bridge.js +354 -0
- package/dist/interaction-bridge.js.map +1 -0
- package/dist/memory-command.d.ts +10 -0
- package/dist/memory-command.d.ts.map +1 -0
- package/dist/memory-command.js +649 -0
- package/dist/memory-command.js.map +1 -0
- package/dist/memory-recall-main.js +1 -1
- package/dist/memory-recall-main.js.map +1 -1
- package/dist/memory-recall.d.ts +41 -5
- package/dist/memory-recall.d.ts.map +1 -1
- package/dist/memory-recall.js +75 -5
- package/dist/memory-recall.js.map +1 -1
- package/dist/memory-rituals.d.ts +8 -11
- package/dist/memory-rituals.d.ts.map +1 -1
- package/dist/memory-rituals.js +26 -25
- package/dist/memory-rituals.js.map +1 -1
- package/dist/metrics.d.ts +13 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +105 -0
- package/dist/metrics.js.map +1 -0
- package/dist/notify-destinations.d.ts +23 -0
- package/dist/notify-destinations.d.ts.map +1 -0
- package/dist/notify-destinations.js +79 -0
- package/dist/notify-destinations.js.map +1 -0
- package/dist/posted-message-index.d.ts +61 -0
- package/dist/posted-message-index.d.ts.map +1 -0
- package/dist/posted-message-index.js +178 -0
- package/dist/posted-message-index.js.map +1 -0
- package/dist/proactive-notify.d.ts +32 -0
- package/dist/proactive-notify.d.ts.map +1 -0
- package/dist/proactive-notify.js +62 -0
- package/dist/proactive-notify.js.map +1 -0
- package/dist/recipes/base.d.ts +15 -0
- package/dist/recipes/base.d.ts.map +1 -0
- package/dist/recipes/base.js +51 -0
- package/dist/recipes/base.js.map +1 -0
- package/dist/recipes/catalog.d.ts +4 -0
- package/dist/recipes/catalog.d.ts.map +1 -0
- package/dist/recipes/catalog.js +509 -0
- package/dist/recipes/catalog.js.map +1 -0
- package/dist/recipes/index.d.ts +11 -0
- package/dist/recipes/index.d.ts.map +1 -0
- package/dist/recipes/index.js +14 -0
- package/dist/recipes/index.js.map +1 -0
- package/dist/recipes/types.d.ts +70 -0
- package/dist/recipes/types.d.ts.map +1 -0
- package/dist/recipes/types.js +15 -0
- package/dist/recipes/types.js.map +1 -0
- package/dist/request-model-override.d.ts +67 -0
- package/dist/request-model-override.d.ts.map +1 -0
- package/dist/request-model-override.js +96 -0
- package/dist/request-model-override.js.map +1 -0
- package/dist/runs-health.d.ts +22 -0
- package/dist/runs-health.d.ts.map +1 -0
- package/dist/runs-health.js +144 -0
- package/dist/runs-health.js.map +1 -0
- package/dist/seen-conversations.d.ts +22 -0
- package/dist/seen-conversations.d.ts.map +1 -0
- package/dist/seen-conversations.js +120 -0
- package/dist/seen-conversations.js.map +1 -0
- package/dist/setup.d.ts +29 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +97 -0
- package/dist/setup.js.map +1 -0
- package/dist/trigger-overrides.d.ts +17 -0
- package/dist/trigger-overrides.d.ts.map +1 -0
- package/dist/trigger-overrides.js +30 -0
- package/dist/trigger-overrides.js.map +1 -0
- package/dist/tui-command.d.ts +56 -0
- package/dist/tui-command.d.ts.map +1 -0
- package/dist/tui-command.js +160 -0
- package/dist/tui-command.js.map +1 -0
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +4 -0
- package/dist/ui.js.map +1 -1
- package/dist/web-command.d.ts +39 -0
- package/dist/web-command.d.ts.map +1 -0
- package/dist/web-command.js +151 -0
- package/dist/web-command.js.map +1 -0
- package/package.json +17 -17
- package/schema/mono-agent.config.schema.json +1686 -0
- package/skills/mono-agent-composer/SKILL.md +34 -9
- package/skills/mono-agent-composer/agents/openai.yaml +4 -0
- package/skills/mono-agent-composer/references/config-blueprint.md +52 -18
- package/skills/mono-agent-composer/references/discovery-questions.md +14 -16
- package/skills/mono-agent-composer/references/feature-coverage.md +19 -15
- package/skills/mono-agent-composer/references/package-map.md +17 -19
- package/skills/mono-agent-composer/references/playbooks.md +209 -0
- package/skills/mono-agent-composer/references/validation.md +9 -1
package/dist/channels.js
CHANGED
|
@@ -1,50 +1,241 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
1
|
+
import { NOTHING_TO_REPORT_SENTINEL } from "@mono-agent/agent-contracts";
|
|
2
|
+
import { describeRunFailureKind } from "@mono-agent/observability";
|
|
3
|
+
import { discoverLocalProviderModels, modelReferenceKey, parseMonoRuntimeModelReference, resolveModelEffortLevels, } from "@mono-agent/runtime-adapter";
|
|
4
|
+
import { isAdapterSendToolAllowed } from "./adapter-send-tools.js";
|
|
5
|
+
import { isChannelConfigured } from "./channel-gate.js";
|
|
6
|
+
import { buildChannelConfigView } from "./channel-config-view.js";
|
|
7
|
+
import { resolveConfiguredChannelPlugins, } from "./channel-plugins.js";
|
|
8
|
+
import { findTriggerOverrideIssues } from "./trigger-overrides.js";
|
|
9
|
+
import { appendPostedMessage, lookupProducingConversation } from "./posted-message-index.js";
|
|
10
|
+
/** The channel ids this app can drive from config (third-party drivers add their own). */
|
|
11
|
+
export const BUILTIN_CHANNEL_IDS = [
|
|
12
|
+
"telegram",
|
|
13
|
+
"slack",
|
|
14
|
+
"webhook",
|
|
15
|
+
"openai-api",
|
|
16
|
+
"cron",
|
|
17
|
+
"tui",
|
|
18
|
+
"live",
|
|
19
|
+
];
|
|
20
|
+
let telegramModule;
|
|
21
|
+
let slackModule;
|
|
22
|
+
let webhookModule;
|
|
23
|
+
let openaiApiModule;
|
|
24
|
+
let cronModule;
|
|
25
|
+
let tuiModule;
|
|
26
|
+
const loadTelegramModule = async () => (telegramModule ??= await import("@mono-agent/telegram-adapter"));
|
|
27
|
+
const loadSlackModule = async () => (slackModule ??= await import("@mono-agent/slack-adapter"));
|
|
28
|
+
const loadWebhookModule = async () => (webhookModule ??= await import("@mono-agent/webhook-adapter"));
|
|
29
|
+
const loadOpenAIApiModule = async () => (openaiApiModule ??= await import("@mono-agent/openai-api-adapter"));
|
|
30
|
+
const loadCronModule = async () => (cronModule ??= await import("@mono-agent/cron-adapter"));
|
|
31
|
+
const loadTuiModule = async () => (tuiModule ??= await import("@mono-agent/operator-adapter"));
|
|
32
|
+
let liveModule;
|
|
33
|
+
const loadLiveModule = async () => (liveModule ??= await import("@mono-agent/operator-adapter"));
|
|
34
|
+
const TELEGRAM_GATE = { jsonKey: "telegram", envPrefix: "MONO_AGENT_TELEGRAM_" };
|
|
35
|
+
const SLACK_GATE = { jsonKey: "slack", envPrefix: "MONO_AGENT_SLACK_" };
|
|
36
|
+
const WEBHOOK_GATE = { jsonKey: "webhook", envPrefix: "MONO_AGENT_WEBHOOK_", dir: "webhook" };
|
|
37
|
+
const OPENAI_API_GATE = { jsonKey: "openaiApi", envPrefix: "MONO_AGENT_OPENAI_API_" };
|
|
38
|
+
const CRON_GATE = { jsonKey: "cron", envPrefix: "MONO_AGENT_CRON_", dir: "cron" };
|
|
39
|
+
const UNCONFIGURED_TELEGRAM_CONFIG = {
|
|
40
|
+
enabled: false,
|
|
41
|
+
botToken: "",
|
|
42
|
+
allowedChatIds: [],
|
|
43
|
+
allowAllChats: false,
|
|
44
|
+
};
|
|
45
|
+
const UNCONFIGURED_SLACK_CONFIG = {
|
|
46
|
+
enabled: false,
|
|
47
|
+
botToken: "",
|
|
48
|
+
appToken: "",
|
|
49
|
+
allowedChannelIds: [],
|
|
50
|
+
allowAllChannels: false,
|
|
51
|
+
botUserIds: [],
|
|
52
|
+
mentionTextAliases: [],
|
|
53
|
+
stripMentionText: false,
|
|
54
|
+
shortcuts: [],
|
|
55
|
+
homeTab: { enabled: false, buttons: [] },
|
|
56
|
+
};
|
|
57
|
+
const UNCONFIGURED_WEBHOOK_CONFIG = {
|
|
58
|
+
enabled: false,
|
|
59
|
+
host: "127.0.0.1",
|
|
60
|
+
port: 0,
|
|
61
|
+
allowNonLoopback: false,
|
|
62
|
+
retentionMs: 300_000,
|
|
63
|
+
maxStoredRequests: 100,
|
|
64
|
+
endpoints: [{ name: "default", path: "/webhook/invoke", mode: "sync", enabled: true }],
|
|
65
|
+
path: "/webhook/invoke",
|
|
66
|
+
defaultMode: "sync",
|
|
67
|
+
};
|
|
68
|
+
const UNCONFIGURED_OPENAI_API_CONFIG = {
|
|
69
|
+
enabled: false,
|
|
70
|
+
host: "127.0.0.1",
|
|
71
|
+
port: 0,
|
|
72
|
+
basePath: "/v1",
|
|
73
|
+
allowNonLoopback: false,
|
|
74
|
+
modelId: "agent",
|
|
75
|
+
};
|
|
76
|
+
const UNCONFIGURED_CRON_CONFIG = { jobs: [] };
|
|
77
|
+
/** Config-view section for a channel the gate found no intent for (no fields to annotate). */
|
|
78
|
+
function unconfiguredChannelView(id, label) {
|
|
79
|
+
return { id, label, status: "disabled", fields: [] };
|
|
80
|
+
}
|
|
10
81
|
export function createTelegramChannelDriver(overrides = {}) {
|
|
11
82
|
return {
|
|
12
83
|
id: "telegram",
|
|
13
84
|
label: "Telegram",
|
|
85
|
+
async configView(input) {
|
|
86
|
+
if (!(await isChannelConfigured(input, TELEGRAM_GATE))) {
|
|
87
|
+
return unconfiguredChannelView("telegram", "Telegram");
|
|
88
|
+
}
|
|
89
|
+
const adapter = await loadTelegramModule();
|
|
90
|
+
return await buildChannelConfigView(this, adapter.TELEGRAM_CONFIG_FIELDS, input);
|
|
91
|
+
},
|
|
14
92
|
async loadConfig(input) {
|
|
15
|
-
|
|
93
|
+
if (!(await isChannelConfigured(input, TELEGRAM_GATE))) {
|
|
94
|
+
return UNCONFIGURED_TELEGRAM_CONFIG;
|
|
95
|
+
}
|
|
96
|
+
const adapter = await loadTelegramModule();
|
|
97
|
+
return await adapter.loadTelegramAdapterConfig({ env: input.env, jsonPath: input.configPath });
|
|
16
98
|
},
|
|
17
99
|
isConfigError(error) {
|
|
18
|
-
return error instanceof TelegramAdapterConfigError;
|
|
100
|
+
return telegramModule !== undefined && error instanceof telegramModule.TelegramAdapterConfigError;
|
|
19
101
|
},
|
|
20
102
|
disabledReason(config) {
|
|
21
103
|
return config.enabled ? undefined : "Telegram is disabled.";
|
|
22
104
|
},
|
|
23
105
|
async start(input) {
|
|
24
|
-
const
|
|
106
|
+
const adapter = await loadTelegramModule();
|
|
107
|
+
const startAdapter = overrides.startAdapter ?? adapter.startTelegramAdapter;
|
|
25
108
|
const result = await startAdapter(telegramStartOptions(input, overrides));
|
|
109
|
+
// Register this channel's interaction sink so the host bridge can post
|
|
110
|
+
// ask_user questions and tool-progress status lines into Telegram chats.
|
|
111
|
+
// The sink enforces the adapter allowlist: a tool-supplied conversation
|
|
112
|
+
// can never reach a chat the operator never allowlisted.
|
|
113
|
+
input.interaction?.registerSink("telegram", {
|
|
114
|
+
postQuestion: async (conversationId, text) => {
|
|
115
|
+
await result.post(requireAllowedTelegramChat(conversationId, input), text);
|
|
116
|
+
},
|
|
117
|
+
postStatus: async (conversationId, text, statusOptions) => {
|
|
118
|
+
await result.postStatus(requireAllowedTelegramChat(conversationId, input), text, statusOptions);
|
|
119
|
+
},
|
|
120
|
+
});
|
|
26
121
|
return {
|
|
27
122
|
summary: {},
|
|
28
123
|
stop: () => result.stop(),
|
|
124
|
+
// Push delivery: a proactive nudge to telegram:<chat> runs as a turn on
|
|
125
|
+
// this chat's own harness and is delivered through the normal stream.
|
|
126
|
+
// Enforces the adapter allowlist so a payload-supplied destination cannot
|
|
127
|
+
// reach a chat the operator never allowlisted.
|
|
128
|
+
notify: async ({ conversationId, text, verbatim }) => {
|
|
129
|
+
const chatId = telegramChatIdFromConversation(conversationId);
|
|
130
|
+
if (chatId === undefined) {
|
|
131
|
+
input.logger?.warn?.("Telegram proactive notify skipped: unparseable destination.", { conversationId });
|
|
132
|
+
return { delivered: false, reason: "unparseable telegram destination" };
|
|
133
|
+
}
|
|
134
|
+
if (!input.config.allowAllChats && !input.config.allowedChatIds.includes(String(chatId))) {
|
|
135
|
+
input.logger?.warn?.("Telegram proactive notify skipped: destination not in allowlist.", { conversationId });
|
|
136
|
+
return { delivered: false, reason: "telegram chat is not in the adapter allowlist" };
|
|
137
|
+
}
|
|
138
|
+
// During configured quiet hours, deliver silently (disable_notification)
|
|
139
|
+
// so an overnight cron/webhook result lands without a push sound.
|
|
140
|
+
const silent = input.config.quietHours !== undefined &&
|
|
141
|
+
adapter.isWithinQuietHours(new Date(), input.config.quietHours);
|
|
142
|
+
const notifyOptions = verbatim === undefined && !silent
|
|
143
|
+
? undefined
|
|
144
|
+
: {
|
|
145
|
+
...(verbatim === undefined ? {} : { verbatim }),
|
|
146
|
+
...(silent ? { silent: true } : {}),
|
|
147
|
+
};
|
|
148
|
+
return await result.notify(chatId, text, notifyOptions);
|
|
149
|
+
},
|
|
29
150
|
};
|
|
30
151
|
},
|
|
31
152
|
};
|
|
32
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Download-path attachment options from config (`maxUploadBytes` stays with the
|
|
156
|
+
* send tools, which reload the config themselves).
|
|
157
|
+
*/
|
|
158
|
+
function telegramAttachmentOptions(config) {
|
|
159
|
+
const attachments = config.attachments;
|
|
160
|
+
if (attachments?.maxBytes === undefined && attachments?.downloadTimeoutMs === undefined) {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
...(attachments.maxBytes === undefined ? {} : { maxBytes: attachments.maxBytes }),
|
|
165
|
+
...(attachments.downloadTimeoutMs === undefined ? {} : { downloadTimeoutMs: attachments.downloadTimeoutMs }),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/** Resolve + allowlist-check a `telegram:<chat>` destination for interaction-sink posts. */
|
|
169
|
+
function requireAllowedTelegramChat(conversationId, input) {
|
|
170
|
+
const chatId = telegramChatIdFromConversation(conversationId);
|
|
171
|
+
if (chatId === undefined) {
|
|
172
|
+
throw new Error(`unparseable telegram destination: ${conversationId}`);
|
|
173
|
+
}
|
|
174
|
+
if (!input.config.allowAllChats && !input.config.allowedChatIds.includes(String(chatId))) {
|
|
175
|
+
throw new Error("telegram chat is not in the adapter allowlist.");
|
|
176
|
+
}
|
|
177
|
+
return chatId;
|
|
178
|
+
}
|
|
179
|
+
/** Extract the Telegram chat id from a `telegram:<chat>` conversationId (numeric ids become numbers; a rollover #bucket suffix is stripped). */
|
|
180
|
+
export function telegramChatIdFromConversation(conversationId) {
|
|
181
|
+
const prefix = "telegram:";
|
|
182
|
+
if (!conversationId.startsWith(prefix)) {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
const raw = conversationId.slice(prefix.length).split("#", 1)[0]?.trim();
|
|
186
|
+
if (raw === undefined || raw.length === 0) {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
return /^-?\d+$/u.test(raw) ? Number(raw) : raw;
|
|
190
|
+
}
|
|
33
191
|
export function createSlackChannelDriver(overrides = {}) {
|
|
34
192
|
return {
|
|
35
193
|
id: "slack",
|
|
36
194
|
label: "Slack",
|
|
195
|
+
async configView(input) {
|
|
196
|
+
if (!(await isChannelConfigured(input, SLACK_GATE))) {
|
|
197
|
+
return unconfiguredChannelView("slack", "Slack");
|
|
198
|
+
}
|
|
199
|
+
const adapter = await loadSlackModule();
|
|
200
|
+
return await buildChannelConfigView(this, adapter.SLACK_CONFIG_FIELDS, input);
|
|
201
|
+
},
|
|
37
202
|
async loadConfig(input) {
|
|
38
|
-
|
|
203
|
+
if (!(await isChannelConfigured(input, SLACK_GATE))) {
|
|
204
|
+
return UNCONFIGURED_SLACK_CONFIG;
|
|
205
|
+
}
|
|
206
|
+
const adapter = await loadSlackModule();
|
|
207
|
+
return await adapter.loadSlackAdapterConfig({ env: input.env, jsonPath: input.configPath });
|
|
39
208
|
},
|
|
40
209
|
isConfigError(error) {
|
|
41
|
-
return error instanceof SlackAdapterConfigError;
|
|
210
|
+
return slackModule !== undefined && error instanceof slackModule.SlackAdapterConfigError;
|
|
42
211
|
},
|
|
43
212
|
disabledReason(config) {
|
|
44
213
|
return config.enabled ? undefined : "Slack is disabled.";
|
|
45
214
|
},
|
|
46
215
|
async start(input) {
|
|
47
|
-
const
|
|
216
|
+
const adapter = await loadSlackModule();
|
|
217
|
+
const startAdapter = overrides.startAdapter ?? adapter.startSlackAdapter;
|
|
218
|
+
// Link posted messages to their producing conversation so an in-thread reply
|
|
219
|
+
// resumes that conversation instead of a fresh, history-less slack: thread.
|
|
220
|
+
const indexPath = input.postedMessageIndexPath;
|
|
221
|
+
// Forward the operator's optional Socket Mode resilience tuning; omitting a
|
|
222
|
+
// field lets the runner apply its own default.
|
|
223
|
+
const reconnect = {};
|
|
224
|
+
if (input.config.reconnectInitialBackoffMs !== undefined)
|
|
225
|
+
reconnect.initialMs = input.config.reconnectInitialBackoffMs;
|
|
226
|
+
if (input.config.reconnectMaxBackoffMs !== undefined)
|
|
227
|
+
reconnect.maxMs = input.config.reconnectMaxBackoffMs;
|
|
228
|
+
if (input.config.reconnectStabilityMs !== undefined)
|
|
229
|
+
reconnect.stabilityMs = input.config.reconnectStabilityMs;
|
|
230
|
+
if (input.config.reconnectStartupGraceMs !== undefined)
|
|
231
|
+
reconnect.startupGraceMs = input.config.reconnectStartupGraceMs;
|
|
232
|
+
if (input.config.drainDeadlineMs !== undefined)
|
|
233
|
+
reconnect.drainDeadlineMs = input.config.drainDeadlineMs;
|
|
234
|
+
const heartbeat = {};
|
|
235
|
+
if (input.config.heartbeatIntervalMs !== undefined)
|
|
236
|
+
heartbeat.intervalMs = input.config.heartbeatIntervalMs;
|
|
237
|
+
if (input.config.heartbeatTimeoutMs !== undefined)
|
|
238
|
+
heartbeat.timeoutMs = input.config.heartbeatTimeoutMs;
|
|
48
239
|
const result = await startAdapter({
|
|
49
240
|
botToken: input.config.botToken,
|
|
50
241
|
appToken: input.config.appToken,
|
|
@@ -53,84 +244,122 @@ export function createSlackChannelDriver(overrides = {}) {
|
|
|
53
244
|
botUserIds: input.config.botUserIds,
|
|
54
245
|
mentionTextAliases: input.config.mentionTextAliases,
|
|
55
246
|
stripMentionText: input.config.stripMentionText,
|
|
247
|
+
shortcuts: input.config.shortcuts,
|
|
248
|
+
homeTab: input.config.homeTab,
|
|
56
249
|
responder: input.responder,
|
|
250
|
+
// A Socket Mode drop is recoverable by construction (the runner always
|
|
251
|
+
// reconnects with backoff), so report it as DEGRADED — keep the responder/
|
|
252
|
+
// harness alive — rather than fatal. onConnectionRestored flips the channel
|
|
253
|
+
// back to running once a reconnect stays up past the stability window.
|
|
254
|
+
onConnectionLost: (reason) => input.onDegraded?.(reason),
|
|
255
|
+
onConnectionRestored: () => input.onRecovered?.(),
|
|
256
|
+
...(Object.keys(reconnect).length === 0 ? {} : { reconnect }),
|
|
257
|
+
...(Object.keys(heartbeat).length === 0 ? {} : { heartbeat }),
|
|
57
258
|
...(input.logger === undefined ? {} : { logger: input.logger }),
|
|
259
|
+
...(indexPath === undefined
|
|
260
|
+
? {}
|
|
261
|
+
: {
|
|
262
|
+
resolvePostIndex: (channelId, ts) => lookupProducingConversation(indexPath, channelId, ts),
|
|
263
|
+
recordPostedMessage: (channelId, ts, conversationId) => {
|
|
264
|
+
void appendPostedMessage(indexPath, { channelId, ts, conversationId });
|
|
265
|
+
},
|
|
266
|
+
}),
|
|
58
267
|
...(overrides.createApi === undefined ? {} : { createApi: overrides.createApi }),
|
|
59
268
|
...(overrides.webSocketFactory === undefined ? {} : { webSocketFactory: overrides.webSocketFactory }),
|
|
60
269
|
});
|
|
61
270
|
return {
|
|
62
271
|
summary: {},
|
|
63
272
|
stop: () => result.stop(),
|
|
273
|
+
// Push delivery: a proactive nudge to slack:<ch>[:<thread>] runs as a turn
|
|
274
|
+
// on that conversation's own harness and is delivered through the stream.
|
|
275
|
+
// Enforces the adapter allowlist so a payload-supplied destination cannot
|
|
276
|
+
// reach a channel the operator never allowlisted.
|
|
277
|
+
notify: async ({ conversationId, text, verbatim }) => {
|
|
278
|
+
const target = slackTargetFromConversation(conversationId);
|
|
279
|
+
if (target === undefined) {
|
|
280
|
+
input.logger?.warn?.("Slack proactive notify skipped: unparseable destination.", { conversationId });
|
|
281
|
+
return { delivered: false, reason: "unparseable slack destination" };
|
|
282
|
+
}
|
|
283
|
+
const normalized = target.channelId.trim().toLowerCase();
|
|
284
|
+
const allowed = input.config.allowAllChannels ||
|
|
285
|
+
input.config.allowedChannelIds.some((id) => id.trim().toLowerCase() === normalized);
|
|
286
|
+
if (!allowed) {
|
|
287
|
+
input.logger?.warn?.("Slack proactive notify skipped: destination not in allowlist.", { conversationId });
|
|
288
|
+
return { delivered: false, reason: "slack channel is not in the adapter allowlist" };
|
|
289
|
+
}
|
|
290
|
+
return await result.adapter.notify(target.channelId, target.threadTs, text, verbatim === undefined ? undefined : { verbatim });
|
|
291
|
+
},
|
|
64
292
|
};
|
|
65
293
|
},
|
|
66
294
|
};
|
|
67
295
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (config.agent === undefined || config.skill === undefined) {
|
|
83
|
-
return "A2A provider requires agent and skill configuration.";
|
|
84
|
-
}
|
|
296
|
+
/** Extract `{channelId, threadTs?}` from a `slack:<ch>[:<thread>]` conversationId (a rollover #bucket suffix is stripped). */
|
|
297
|
+
export function slackTargetFromConversation(conversationId) {
|
|
298
|
+
const prefix = "slack:";
|
|
299
|
+
if (!conversationId.startsWith(prefix)) {
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
const rest = conversationId.slice(prefix.length).split("#", 1)[0];
|
|
303
|
+
if (rest === undefined || rest.length === 0) {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
const colon = rest.indexOf(":");
|
|
307
|
+
if (colon < 0) {
|
|
308
|
+
const channelId = rest.trim();
|
|
309
|
+
if (channelId.length === 0) {
|
|
85
310
|
return undefined;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
version: config.agent.version,
|
|
105
|
-
...(config.agent.providerOrganization === undefined || config.agent.providerUrl === undefined
|
|
106
|
-
? {}
|
|
107
|
-
: {
|
|
108
|
-
provider: {
|
|
109
|
-
organization: config.agent.providerOrganization,
|
|
110
|
-
url: config.agent.providerUrl,
|
|
111
|
-
},
|
|
112
|
-
}),
|
|
113
|
-
},
|
|
114
|
-
skill: config.skill,
|
|
115
|
-
...(input.logger === undefined ? {} : { logger: input.logger }),
|
|
116
|
-
});
|
|
117
|
-
return {
|
|
118
|
-
summary: { agentCardUrl: provider.agentCardUrl },
|
|
119
|
-
stop: () => provider.stop(),
|
|
120
|
-
};
|
|
121
|
-
},
|
|
122
|
-
};
|
|
311
|
+
}
|
|
312
|
+
return { channelId };
|
|
313
|
+
}
|
|
314
|
+
const channelId = rest.slice(0, colon).trim();
|
|
315
|
+
const threadTs = rest.slice(colon + 1).trim();
|
|
316
|
+
if (channelId.length === 0) {
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
if (threadTs.length === 0) {
|
|
320
|
+
return { channelId };
|
|
321
|
+
}
|
|
322
|
+
// A canonical Slack threadTs (e.g. 1718800000.123456) never contains a colon, so a
|
|
323
|
+
// stray/double colon is an operator typo — reject it so the driver warns + skips
|
|
324
|
+
// cleanly rather than posting to the Slack API with a malformed thread_ts.
|
|
325
|
+
if (threadTs.includes(":")) {
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
return { channelId, threadTs };
|
|
123
329
|
}
|
|
330
|
+
// Default wall-clock bound for a webhook run, reclaiming a hung slot. Mirrors
|
|
331
|
+
// {@link DEFAULT_CRON_MAX_RUN_MS}; an operator can override via webhook.maxRunMs.
|
|
332
|
+
const DEFAULT_WEBHOOK_MAX_RUN_MS = 20 * 60 * 1000;
|
|
124
333
|
export function createWebhookChannelDriver(overrides = {}) {
|
|
125
334
|
return {
|
|
126
335
|
id: "webhook",
|
|
127
336
|
label: "Webhook",
|
|
337
|
+
async configView(input) {
|
|
338
|
+
if (!(await isChannelConfigured(input, WEBHOOK_GATE))) {
|
|
339
|
+
return unconfiguredChannelView("webhook", "Webhook");
|
|
340
|
+
}
|
|
341
|
+
const adapter = await loadWebhookModule();
|
|
342
|
+
return await buildChannelConfigView(this, adapter.WEBHOOK_CONFIG_FIELDS, input);
|
|
343
|
+
},
|
|
344
|
+
configIssues(config) {
|
|
345
|
+
return findTriggerOverrideIssues(config.endpoints
|
|
346
|
+
.filter((endpoint) => endpoint.enabled)
|
|
347
|
+
.map((endpoint) => ({
|
|
348
|
+
name: `webhook endpoint "${endpoint.name}"`,
|
|
349
|
+
...(endpoint.model === undefined ? {} : { model: endpoint.model }),
|
|
350
|
+
...(endpoint.effort === undefined ? {} : { effort: endpoint.effort }),
|
|
351
|
+
})));
|
|
352
|
+
},
|
|
128
353
|
async loadConfig(input) {
|
|
354
|
+
if (!(await isChannelConfigured(input, WEBHOOK_GATE))) {
|
|
355
|
+
return UNCONFIGURED_WEBHOOK_CONFIG;
|
|
356
|
+
}
|
|
357
|
+
const adapter = await loadWebhookModule();
|
|
129
358
|
// cwd is required so `webhook/*.md` endpoint files are discovered.
|
|
130
|
-
return await loadWebhookAdapterConfig({ env: input.env, jsonPath: input.configPath, cwd: input.cwd });
|
|
359
|
+
return await adapter.loadWebhookAdapterConfig({ env: input.env, jsonPath: input.configPath, cwd: input.cwd });
|
|
131
360
|
},
|
|
132
361
|
isConfigError(error) {
|
|
133
|
-
return error instanceof WebhookAdapterError;
|
|
362
|
+
return webhookModule !== undefined && error instanceof webhookModule.WebhookAdapterError;
|
|
134
363
|
},
|
|
135
364
|
disabledReason(config) {
|
|
136
365
|
return config.enabled ? undefined : "Webhook adapter is disabled.";
|
|
@@ -141,7 +370,10 @@ export function createWebhookChannelDriver(overrides = {}) {
|
|
|
141
370
|
: "Webhook adapter has no enabled endpoints.";
|
|
142
371
|
},
|
|
143
372
|
async start(input) {
|
|
144
|
-
const
|
|
373
|
+
const endpoints = input.config.endpoints.filter((endpoint) => endpoint.enabled);
|
|
374
|
+
const endpointByName = new Map(endpoints.map((endpoint) => [endpoint.name, endpoint]));
|
|
375
|
+
const adapterModule = await loadWebhookModule();
|
|
376
|
+
const adapterFactory = overrides.adapterFactory ?? adapterModule.startWebhookAdapter;
|
|
145
377
|
const adapter = await adapterFactory({
|
|
146
378
|
host: input.config.host,
|
|
147
379
|
port: input.config.port,
|
|
@@ -149,15 +381,31 @@ export function createWebhookChannelDriver(overrides = {}) {
|
|
|
149
381
|
defaultMode: input.config.defaultMode,
|
|
150
382
|
retentionMs: input.config.retentionMs,
|
|
151
383
|
maxStoredRequests: input.config.maxStoredRequests,
|
|
152
|
-
|
|
153
|
-
|
|
384
|
+
// Default-on max-run bound so a hung (esp. async) webhook can't hold a
|
|
385
|
+
// conversation slot forever; mirrors the cron adapter's 20-min default.
|
|
386
|
+
maxRunMs: input.config.maxRunMs ?? DEFAULT_WEBHOOK_MAX_RUN_MS,
|
|
387
|
+
endpoints: endpoints
|
|
154
388
|
.map((endpoint) => ({
|
|
155
389
|
name: endpoint.name,
|
|
156
390
|
path: endpoint.path,
|
|
157
391
|
mode: endpoint.mode,
|
|
158
392
|
...(endpoint.prompt === undefined ? {} : { prompt: endpoint.prompt }),
|
|
393
|
+
...(endpoint.notify === undefined ? {} : { notify: endpoint.notify }),
|
|
394
|
+
...(endpoint.notifyConversationId === undefined ? {} : { notifyConversationId: endpoint.notifyConversationId }),
|
|
395
|
+
...(endpoint.model === undefined ? {} : { model: endpoint.model }),
|
|
396
|
+
...(endpoint.effort === undefined ? {} : { effort: endpoint.effort }),
|
|
159
397
|
})),
|
|
160
398
|
responder: input.responder,
|
|
399
|
+
onResult: (status, request) => {
|
|
400
|
+
void deliverNativeWebhookNotification({
|
|
401
|
+
endpoint: endpointByName.get(request.metadata.webhook.endpointName),
|
|
402
|
+
status,
|
|
403
|
+
request,
|
|
404
|
+
...(input.notifyDestination === undefined ? {} : { notifyDestination: input.notifyDestination }),
|
|
405
|
+
...(input.listNotifyDestinations === undefined ? {} : { listNotifyDestinations: input.listNotifyDestinations }),
|
|
406
|
+
...(input.logger === undefined ? {} : { logger: input.logger }),
|
|
407
|
+
});
|
|
408
|
+
},
|
|
161
409
|
...(input.logger === undefined ? {} : { logger: input.logger }),
|
|
162
410
|
});
|
|
163
411
|
return {
|
|
@@ -175,17 +423,29 @@ export function createOpenAIApiChannelDriver(overrides = {}) {
|
|
|
175
423
|
return {
|
|
176
424
|
id: "openai-api",
|
|
177
425
|
label: "OpenAI API",
|
|
426
|
+
async configView(input) {
|
|
427
|
+
if (!(await isChannelConfigured(input, OPENAI_API_GATE))) {
|
|
428
|
+
return unconfiguredChannelView("openai-api", "OpenAI API");
|
|
429
|
+
}
|
|
430
|
+
const adapter = await loadOpenAIApiModule();
|
|
431
|
+
return await buildChannelConfigView(this, adapter.OPENAI_API_CONFIG_FIELDS, input, { jsonKey: "openaiApi" });
|
|
432
|
+
},
|
|
178
433
|
async loadConfig(input) {
|
|
179
|
-
|
|
434
|
+
if (!(await isChannelConfigured(input, OPENAI_API_GATE))) {
|
|
435
|
+
return UNCONFIGURED_OPENAI_API_CONFIG;
|
|
436
|
+
}
|
|
437
|
+
const adapter = await loadOpenAIApiModule();
|
|
438
|
+
return await adapter.loadOpenAIApiAdapterConfig({ env: input.env, jsonPath: input.configPath });
|
|
180
439
|
},
|
|
181
440
|
isConfigError(error) {
|
|
182
|
-
return error instanceof OpenAIApiAdapterError;
|
|
441
|
+
return openaiApiModule !== undefined && error instanceof openaiApiModule.OpenAIApiAdapterError;
|
|
183
442
|
},
|
|
184
443
|
disabledReason(config) {
|
|
185
444
|
return config.enabled ? undefined : "OpenAI API adapter is disabled.";
|
|
186
445
|
},
|
|
187
446
|
async start(input) {
|
|
188
|
-
const
|
|
447
|
+
const adapterModule = await loadOpenAIApiModule();
|
|
448
|
+
const adapterFactory = overrides.adapterFactory ?? adapterModule.startOpenAIApiAdapter;
|
|
189
449
|
const adapter = await adapterFactory({
|
|
190
450
|
host: input.config.host,
|
|
191
451
|
port: input.config.port,
|
|
@@ -203,15 +463,231 @@ export function createOpenAIApiChannelDriver(overrides = {}) {
|
|
|
203
463
|
},
|
|
204
464
|
};
|
|
205
465
|
}
|
|
466
|
+
/**
|
|
467
|
+
* `/v1/info` re-runs local-provider discovery at most this often; the TUI
|
|
468
|
+
* fetches info on connect + agent switch (not per turn), so this just bounds
|
|
469
|
+
* how often a flaky/slow local endpoint gets probed rather than affecting
|
|
470
|
+
* perceived freshness in practice.
|
|
471
|
+
*/
|
|
472
|
+
const LOCAL_MODEL_DISCOVERY_TTL_MS = 30_000;
|
|
473
|
+
/**
|
|
474
|
+
* The TUI stream endpoint deviates from the channels-off convention: with no
|
|
475
|
+
* `tui` section and no `MONO_AGENT_TUI_*` env it is ENABLED on loopback with
|
|
476
|
+
* an ephemeral port, so `mono-agent tui` can reach any running agent without a
|
|
477
|
+
* per-agent config edit. There is consequently no isChannelConfigured gate and
|
|
478
|
+
* no synthetic UNCONFIGURED constant — the real loader always answers (its
|
|
479
|
+
* empty-input output is `enabled: true` + defaults). `"tui": {"enabled": false}`
|
|
480
|
+
* opts out.
|
|
481
|
+
*/
|
|
482
|
+
export function createTuiChannelDriver(overrides = {}) {
|
|
483
|
+
return {
|
|
484
|
+
id: "tui",
|
|
485
|
+
label: "TUI",
|
|
486
|
+
async configView(input) {
|
|
487
|
+
const adapter = await loadTuiModule();
|
|
488
|
+
return await buildChannelConfigView(this, adapter.TUI_CONFIG_FIELDS, input, { jsonKey: "tui" });
|
|
489
|
+
},
|
|
490
|
+
async loadConfig(input) {
|
|
491
|
+
const adapter = await loadTuiModule();
|
|
492
|
+
return await adapter.loadTuiAdapterConfig({ env: input.env, jsonPath: input.configPath });
|
|
493
|
+
},
|
|
494
|
+
isConfigError(error) {
|
|
495
|
+
return tuiModule !== undefined && error instanceof tuiModule.TuiAdapterError;
|
|
496
|
+
},
|
|
497
|
+
disabledReason(config) {
|
|
498
|
+
return config.enabled ? undefined : "TUI stream endpoint is disabled.";
|
|
499
|
+
},
|
|
500
|
+
async start(input) {
|
|
501
|
+
const adapterModule = await loadTuiModule();
|
|
502
|
+
const adapterFactory = overrides.adapterFactory ?? adapterModule.startTuiAdapter;
|
|
503
|
+
const discoverModels = overrides.discoverModels ?? discoverLocalProviderModels;
|
|
504
|
+
const localProviders = input.coreConfig.providers?.local;
|
|
505
|
+
// The statically configured candidate models: the primary first, then
|
|
506
|
+
// each configured fallback, as canonical `modelReferenceKey` strings,
|
|
507
|
+
// de-duplicated (a fallback redundantly naming the primary collapses away).
|
|
508
|
+
const configModelKeys = [];
|
|
509
|
+
for (const ref of [input.coreConfig.runtime.model, ...(input.coreConfig.runtime.fallbackModels ?? [])]) {
|
|
510
|
+
const key = modelReferenceKey(ref);
|
|
511
|
+
if (!configModelKeys.includes(key)) {
|
|
512
|
+
configModelKeys.push(key);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
// Local-provider discovery is async and its result can change after
|
|
516
|
+
// startup (an endpoint started later, a model loaded/unloaded) — so
|
|
517
|
+
// `info` is a PROVIDER the adapter re-invokes on every GET /v1/info,
|
|
518
|
+
// rather than a value snapshotted once at channel composition. A short
|
|
519
|
+
// TTL cache keeps a flaky/slow endpoint from being hammered; the TUI
|
|
520
|
+
// only calls /v1/info on connect + agent switch, not per turn, so 30s
|
|
521
|
+
// of staleness there is unnoticeable in practice.
|
|
522
|
+
let discoveryCache;
|
|
523
|
+
const discoverModelsCached = async () => {
|
|
524
|
+
const now = Date.now();
|
|
525
|
+
if (discoveryCache !== undefined && now < discoveryCache.expiresAt) {
|
|
526
|
+
return discoveryCache.models;
|
|
527
|
+
}
|
|
528
|
+
const models = await discoverModels(localProviders);
|
|
529
|
+
discoveryCache = { expiresAt: now + LOCAL_MODEL_DISCOVERY_TTL_MS, models };
|
|
530
|
+
return models;
|
|
531
|
+
};
|
|
532
|
+
const buildInfo = async () => {
|
|
533
|
+
const discovered = await discoverModelsCached();
|
|
534
|
+
const labelByRef = new Map(discovered.map((model) => [model.ref, model.label]));
|
|
535
|
+
const models = [...configModelKeys];
|
|
536
|
+
for (const model of discovered) {
|
|
537
|
+
if (!models.includes(model.ref)) {
|
|
538
|
+
models.push(model.ref);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
const modelOptions = {};
|
|
542
|
+
for (const ref of models) {
|
|
543
|
+
let parsedRef;
|
|
544
|
+
try {
|
|
545
|
+
parsedRef = parseMonoRuntimeModelReference(ref);
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
const resolved = resolveModelEffortLevels(parsedRef, localProviders);
|
|
551
|
+
const label = labelByRef.get(ref);
|
|
552
|
+
const entry = {
|
|
553
|
+
...(resolved.effortLevels === undefined ? {} : { effortLevels: resolved.effortLevels }),
|
|
554
|
+
reasoning: resolved.reasoning,
|
|
555
|
+
...(resolved.reasoningMode === undefined ? {} : { reasoningMode: resolved.reasoningMode }),
|
|
556
|
+
...(label === undefined ? {} : { label }),
|
|
557
|
+
};
|
|
558
|
+
if (Object.keys(entry).length > 0) {
|
|
559
|
+
modelOptions[ref] = entry;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return {
|
|
563
|
+
model: modelReferenceKey(input.coreConfig.runtime.model),
|
|
564
|
+
...(input.coreConfig.runtime.effort === undefined ? {} : { effort: input.coreConfig.runtime.effort }),
|
|
565
|
+
models,
|
|
566
|
+
...(Object.keys(modelOptions).length === 0 ? {} : { modelOptions }),
|
|
567
|
+
};
|
|
568
|
+
};
|
|
569
|
+
const adapter = await adapterFactory({
|
|
570
|
+
host: input.config.host,
|
|
571
|
+
port: input.config.port,
|
|
572
|
+
basePath: input.config.basePath,
|
|
573
|
+
allowNonLoopback: input.config.allowNonLoopback,
|
|
574
|
+
...(input.config.apiKey === undefined ? {} : { apiKey: input.config.apiKey }),
|
|
575
|
+
responder: input.responder,
|
|
576
|
+
info: buildInfo,
|
|
577
|
+
// A dead endpoint must flip the channel to failed, not serve nothing
|
|
578
|
+
// silently — the TUI's only discovery signal is this channel's status.
|
|
579
|
+
onServerError: (reason) => input.onFailure(reason),
|
|
580
|
+
...(input.logger === undefined ? {} : { logger: input.logger }),
|
|
581
|
+
});
|
|
582
|
+
return {
|
|
583
|
+
summary: { baseUrl: adapter.baseUrl },
|
|
584
|
+
stop: () => adapter.stop(),
|
|
585
|
+
};
|
|
586
|
+
},
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* The live event relay, like the TUI endpoint, deviates from the channels-off
|
|
591
|
+
* convention: it is ENABLED by default on loopback with an ephemeral port so
|
|
592
|
+
* `mono-agent web` can observe any running agent without a per-agent config edit.
|
|
593
|
+
* It is passive and read-only — it relays the host's in-process run-event bus over
|
|
594
|
+
* SSE and never drives a turn. `"live": {"enabled": false}` opts out.
|
|
595
|
+
*/
|
|
596
|
+
export function createLiveChannelDriver(overrides = {}) {
|
|
597
|
+
return {
|
|
598
|
+
id: "live",
|
|
599
|
+
label: "Live",
|
|
600
|
+
async configView(input) {
|
|
601
|
+
const adapter = await loadLiveModule();
|
|
602
|
+
return await buildChannelConfigView(this, adapter.LIVE_CONFIG_FIELDS, input, { jsonKey: "live" });
|
|
603
|
+
},
|
|
604
|
+
async loadConfig(input) {
|
|
605
|
+
const adapter = await loadLiveModule();
|
|
606
|
+
return await adapter.loadLiveAdapterConfig({ env: input.env, jsonPath: input.configPath });
|
|
607
|
+
},
|
|
608
|
+
isConfigError(error) {
|
|
609
|
+
return liveModule !== undefined && error instanceof liveModule.LiveAdapterError;
|
|
610
|
+
},
|
|
611
|
+
disabledReason(config) {
|
|
612
|
+
return config.enabled ? undefined : "Live event relay is disabled.";
|
|
613
|
+
},
|
|
614
|
+
async start(input) {
|
|
615
|
+
const adapterModule = await loadLiveModule();
|
|
616
|
+
const adapterFactory = overrides.adapterFactory ?? adapterModule.startLiveAdapter;
|
|
617
|
+
// The host feeds the shared run-event bus (via the broadcast recorder). If it
|
|
618
|
+
// has none, still serve an (empty) stream rather than failing the channel.
|
|
619
|
+
const bus = input.liveEventBus ?? adapterModule.createLiveEventBus();
|
|
620
|
+
const adapter = await adapterFactory({
|
|
621
|
+
bus,
|
|
622
|
+
host: input.config.host,
|
|
623
|
+
port: input.config.port,
|
|
624
|
+
basePath: input.config.basePath,
|
|
625
|
+
allowNonLoopback: input.config.allowNonLoopback,
|
|
626
|
+
...(input.config.apiKey === undefined ? {} : { apiKey: input.config.apiKey }),
|
|
627
|
+
// A dead endpoint must flip the channel to failed, not serve nothing
|
|
628
|
+
// silently — the web surface's only per-agent live signal is this status.
|
|
629
|
+
onServerError: (reason) => input.onFailure(reason),
|
|
630
|
+
...(input.logger === undefined ? {} : { logger: input.logger }),
|
|
631
|
+
});
|
|
632
|
+
return {
|
|
633
|
+
summary: { baseUrl: adapter.baseUrl },
|
|
634
|
+
stop: () => adapter.stop(),
|
|
635
|
+
};
|
|
636
|
+
},
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* 20 minutes — comfortably above any real briefing/scan (observed max ~6.5 min) yet bounded, so a
|
|
641
|
+
* hung run is reclaimed rather than blocking the job indefinitely.
|
|
642
|
+
*/
|
|
643
|
+
const DEFAULT_CRON_MAX_RUN_MS = 20 * 60 * 1000;
|
|
644
|
+
const DEFAULT_CRON_FAILURE_NOTICE_COOLDOWN_HOURS = 6;
|
|
645
|
+
const MAX_CRON_FAILURE_NOTICE_ERROR_CHARS = 180;
|
|
646
|
+
/**
|
|
647
|
+
* Whether a notify-enabled turn's final text should suppress delivery: empty /
|
|
648
|
+
* whitespace-only (the agent chose to stay silent) or exactly the
|
|
649
|
+
* {@link NOTHING_TO_REPORT_SENTINEL} token. Matched trimmed and
|
|
650
|
+
* case-insensitively; never substring-matched, so a digest that merely mentions
|
|
651
|
+
* the phrase is still delivered.
|
|
652
|
+
*/
|
|
653
|
+
function suppressesNotification(text) {
|
|
654
|
+
const trimmed = text?.trim() ?? "";
|
|
655
|
+
return trimmed.length === 0 || trimmed.toUpperCase() === NOTHING_TO_REPORT_SENTINEL;
|
|
656
|
+
}
|
|
657
|
+
/** A conversationId a verbatim notification can be delivered to (a push channel with a notify hook). */
|
|
658
|
+
function isDeliverableConversation(conversationId) {
|
|
659
|
+
return conversationId.startsWith("telegram:") || conversationId.startsWith("slack:");
|
|
660
|
+
}
|
|
206
661
|
export function createCronChannelDriver(overrides = {}) {
|
|
662
|
+
const failureNoticeLastSentMsByJobId = new Map();
|
|
207
663
|
return {
|
|
208
664
|
id: "cron",
|
|
209
665
|
label: "Cron",
|
|
666
|
+
async configView(input) {
|
|
667
|
+
if (!(await isChannelConfigured(input, CRON_GATE))) {
|
|
668
|
+
return unconfiguredChannelView("cron", "Cron");
|
|
669
|
+
}
|
|
670
|
+
const adapter = await loadCronModule();
|
|
671
|
+
return await buildChannelConfigView(this, adapter.CRON_CONFIG_FIELDS, input);
|
|
672
|
+
},
|
|
673
|
+
configIssues(config) {
|
|
674
|
+
return findTriggerOverrideIssues(config.jobs
|
|
675
|
+
.filter((job) => job.enabled)
|
|
676
|
+
.map((job) => ({
|
|
677
|
+
name: `cron job "${job.id}"`,
|
|
678
|
+
...(job.model === undefined ? {} : { model: job.model }),
|
|
679
|
+
...(job.effort === undefined ? {} : { effort: job.effort }),
|
|
680
|
+
})));
|
|
681
|
+
},
|
|
210
682
|
async loadConfig(input) {
|
|
211
|
-
|
|
683
|
+
if (!(await isChannelConfigured(input, CRON_GATE))) {
|
|
684
|
+
return UNCONFIGURED_CRON_CONFIG;
|
|
685
|
+
}
|
|
686
|
+
const adapter = await loadCronModule();
|
|
687
|
+
return await adapter.loadCronAdapterConfig({ env: input.env, jsonPath: input.configPath, cwd: input.cwd });
|
|
212
688
|
},
|
|
213
689
|
isConfigError(error) {
|
|
214
|
-
return error instanceof CronAdapterError;
|
|
690
|
+
return cronModule !== undefined && error instanceof cronModule.CronAdapterError;
|
|
215
691
|
},
|
|
216
692
|
disabledReason(config) {
|
|
217
693
|
const enabledJobs = config.jobs.filter((job) => job.enabled);
|
|
@@ -219,25 +695,51 @@ export function createCronChannelDriver(overrides = {}) {
|
|
|
219
695
|
},
|
|
220
696
|
async start(input) {
|
|
221
697
|
const jobs = input.config.jobs.filter((job) => job.enabled);
|
|
222
|
-
const
|
|
698
|
+
const jobById = new Map(jobs.map((job) => [job.id, job]));
|
|
699
|
+
const adapterModule = await loadCronModule();
|
|
700
|
+
const adapterFactory = overrides.adapterFactory ?? adapterModule.startCronAdapter;
|
|
223
701
|
const adapter = adapterFactory({
|
|
224
702
|
responder: input.responder,
|
|
225
703
|
// Skip overlapping firings (a job still running when its next tick fires)
|
|
226
704
|
// — the legacy/default app behavior. This avoids the scheduler's
|
|
227
705
|
// unbounded "queue" default retaining stale ticks in memory when a job
|
|
228
706
|
// runs longer than its interval. (Queue/replace remain available to
|
|
229
|
-
// programmatic callers of startCronAdapter.)
|
|
707
|
+
// programmatic callers of startCronAdapter.) The watchdog below is what
|
|
708
|
+
// keeps "skip" safe: a hung run no longer pins the slot forever.
|
|
230
709
|
overlap: "skip",
|
|
710
|
+
// Reclaim a run whose responder never settles, so a single wedged run can't
|
|
711
|
+
// permanently skip every future firing of the job.
|
|
712
|
+
maxRunMs: overrides.maxRunMs ?? DEFAULT_CRON_MAX_RUN_MS,
|
|
231
713
|
jobs: jobs.map((job) => ({
|
|
232
714
|
id: job.id,
|
|
233
715
|
expression: job.expression,
|
|
234
716
|
timezone: job.timezone,
|
|
235
717
|
prompt: job.prompt,
|
|
236
718
|
...(job.conversationId === undefined ? {} : { conversationId: job.conversationId }),
|
|
719
|
+
...(job.maxRunMs === undefined ? {} : { maxRunMs: job.maxRunMs }),
|
|
720
|
+
...(job.notify === undefined ? {} : { notify: job.notify }),
|
|
721
|
+
...(job.notifyConversationId === undefined ? {} : { notifyConversationId: job.notifyConversationId }),
|
|
722
|
+
...(job.model === undefined ? {} : { model: job.model }),
|
|
723
|
+
...(job.effort === undefined ? {} : { effort: job.effort }),
|
|
237
724
|
})),
|
|
238
725
|
onResult: (result) => {
|
|
239
726
|
const level = result.kind === "failed" ? "error" : result.kind === "skipped" ? "warn" : "info";
|
|
240
727
|
input.logger?.[level]?.("Cron job finished.", { result });
|
|
728
|
+
void deliverCronModelExhaustionFailureNotice({
|
|
729
|
+
job: jobById.get(result.jobId),
|
|
730
|
+
result,
|
|
731
|
+
cooldowns: failureNoticeLastSentMsByJobId,
|
|
732
|
+
now: overrides.now ?? (() => new Date()),
|
|
733
|
+
...(input.notifyDestination === undefined ? {} : { notifyDestination: input.notifyDestination }),
|
|
734
|
+
...(input.logger === undefined ? {} : { logger: input.logger }),
|
|
735
|
+
});
|
|
736
|
+
void deliverNativeCronNotification({
|
|
737
|
+
job: jobById.get(result.jobId),
|
|
738
|
+
result,
|
|
739
|
+
...(input.notifyDestination === undefined ? {} : { notifyDestination: input.notifyDestination }),
|
|
740
|
+
...(input.listNotifyDestinations === undefined ? {} : { listNotifyDestinations: input.listNotifyDestinations }),
|
|
741
|
+
...(input.logger === undefined ? {} : { logger: input.logger }),
|
|
742
|
+
});
|
|
241
743
|
},
|
|
242
744
|
...(input.logger === undefined ? {} : { logger: input.logger }),
|
|
243
745
|
});
|
|
@@ -250,57 +752,238 @@ export function createCronChannelDriver(overrides = {}) {
|
|
|
250
752
|
},
|
|
251
753
|
};
|
|
252
754
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
755
|
+
async function deliverCronModelExhaustionFailureNotice(input) {
|
|
756
|
+
const job = input.job;
|
|
757
|
+
if (job?.notify !== true ||
|
|
758
|
+
input.result.kind !== "failed" ||
|
|
759
|
+
input.result.failureKind !== "provider_unavailable_exhausted") {
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (job.notifyConversationId === undefined) {
|
|
763
|
+
input.logger?.warn?.("Cron failure notice skipped: notifyConversationId is required.", { jobId: job.id });
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
if (input.notifyDestination === undefined) {
|
|
767
|
+
input.logger?.warn?.("Cron failure notice skipped: no delivery hook is available.", { jobId: job.id });
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
const nowMs = input.now().getTime();
|
|
771
|
+
const cooldownHours = job.notifyFailureCooldownHours ?? DEFAULT_CRON_FAILURE_NOTICE_COOLDOWN_HOURS;
|
|
772
|
+
const cooldownMs = cooldownHours * 60 * 60 * 1000;
|
|
773
|
+
const lastSentMs = input.cooldowns.get(job.id);
|
|
774
|
+
if (lastSentMs !== undefined && nowMs - lastSentMs < cooldownMs) {
|
|
775
|
+
input.logger?.info?.("Cron failure notice skipped: cooldown is active.", {
|
|
776
|
+
jobId: job.id,
|
|
777
|
+
cooldownHours,
|
|
778
|
+
});
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
const destination = job.notifyConversationId;
|
|
782
|
+
const text = buildCronModelExhaustionFailureNotice(job, input.result);
|
|
783
|
+
input.cooldowns.set(job.id, nowMs);
|
|
784
|
+
try {
|
|
785
|
+
const delivery = await input.notifyDestination(destination, text, { verbatim: true });
|
|
786
|
+
if (!delivery.delivered) {
|
|
787
|
+
input.logger?.warn?.("Cron failure notice was not delivered.", {
|
|
788
|
+
jobId: job.id,
|
|
789
|
+
conversationId: destination,
|
|
790
|
+
...(delivery.reason === undefined ? {} : { reason: delivery.reason }),
|
|
277
791
|
});
|
|
278
|
-
return
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
input.logger?.info?.("Cron failure notice delivered.", { jobId: job.id, conversationId: destination });
|
|
795
|
+
}
|
|
796
|
+
catch (error) {
|
|
797
|
+
input.logger?.warn?.("Cron failure notice failed.", {
|
|
798
|
+
jobId: job.id,
|
|
799
|
+
conversationId: destination,
|
|
800
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
function buildCronModelExhaustionFailureNotice(job, result) {
|
|
805
|
+
const jobId = oneLine(job.id);
|
|
806
|
+
const latestError = truncateOneLine(result.error, MAX_CRON_FAILURE_NOTICE_ERROR_CHARS);
|
|
807
|
+
const prefix = `Cron job "${jobId}" failed: all configured models failed.`;
|
|
808
|
+
return latestError.length === 0 ? prefix : `${prefix} Latest error: ${latestError}`;
|
|
809
|
+
}
|
|
810
|
+
function truncateOneLine(value, maxChars) {
|
|
811
|
+
const collapsed = oneLine(value);
|
|
812
|
+
if (collapsed.length <= maxChars) {
|
|
813
|
+
return collapsed;
|
|
814
|
+
}
|
|
815
|
+
return `${collapsed.slice(0, Math.max(0, maxChars - 3)).trimEnd()}...`;
|
|
816
|
+
}
|
|
817
|
+
function oneLine(value) {
|
|
818
|
+
return value.replace(/\s+/gu, " ").trim();
|
|
819
|
+
}
|
|
820
|
+
async function deliverNativeCronNotification(input) {
|
|
821
|
+
const job = input.job;
|
|
822
|
+
if (job?.notify !== true || input.result.kind !== "succeeded") {
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
const text = input.result.text;
|
|
826
|
+
// The agent stays silent by producing no final answer or the explicit sentinel.
|
|
827
|
+
if (text === undefined || suppressesNotification(text)) {
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
try {
|
|
831
|
+
if (input.notifyDestination === undefined) {
|
|
832
|
+
input.logger?.warn?.("Native cron notification skipped: no delivery hook is available.", { jobId: job.id });
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
const destination = await resolveNativeCronNotifyDestination({
|
|
836
|
+
job,
|
|
837
|
+
...(input.listNotifyDestinations === undefined ? {} : { listNotifyDestinations: input.listNotifyDestinations }),
|
|
838
|
+
...(input.logger === undefined ? {} : { logger: input.logger }),
|
|
839
|
+
});
|
|
840
|
+
if (destination === undefined) {
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
const delivery = await input.notifyDestination(destination, text, { verbatim: true });
|
|
844
|
+
if (!delivery.delivered) {
|
|
845
|
+
input.logger?.warn?.("Native cron notification was not delivered.", {
|
|
846
|
+
jobId: job.id,
|
|
847
|
+
conversationId: destination,
|
|
848
|
+
...(delivery.reason === undefined ? {} : { reason: delivery.reason }),
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
catch (error) {
|
|
853
|
+
input.logger?.warn?.("Native cron notification failed.", {
|
|
854
|
+
jobId: job.id,
|
|
855
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
async function resolveNativeCronNotifyDestination(input) {
|
|
860
|
+
if (input.job.notifyConversationId !== undefined) {
|
|
861
|
+
return input.job.notifyConversationId;
|
|
862
|
+
}
|
|
863
|
+
if (input.listNotifyDestinations === undefined) {
|
|
864
|
+
input.logger?.warn?.("Native cron notification skipped: no destination is configured and no destination resolver is available.", {
|
|
865
|
+
jobId: input.job.id,
|
|
866
|
+
});
|
|
867
|
+
return undefined;
|
|
868
|
+
}
|
|
869
|
+
const destinations = await input.listNotifyDestinations();
|
|
870
|
+
if (destinations.length !== 1) {
|
|
871
|
+
input.logger?.warn?.("Native cron notification skipped: destination inference requires exactly one candidate.", {
|
|
872
|
+
jobId: input.job.id,
|
|
873
|
+
destinationCount: destinations.length,
|
|
874
|
+
});
|
|
875
|
+
return undefined;
|
|
876
|
+
}
|
|
877
|
+
return destinations[0]?.conversationId;
|
|
878
|
+
}
|
|
879
|
+
async function deliverNativeWebhookNotification(input) {
|
|
880
|
+
const endpoint = input.endpoint;
|
|
881
|
+
if (endpoint?.notify !== true || input.status.status !== "succeeded") {
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
const text = input.status.text;
|
|
885
|
+
// The agent stays silent by producing no final answer or the explicit sentinel.
|
|
886
|
+
if (text === undefined || suppressesNotification(text)) {
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
const source = { endpointName: input.request.metadata.webhook.endpointName };
|
|
890
|
+
try {
|
|
891
|
+
if (input.notifyDestination === undefined) {
|
|
892
|
+
input.logger?.warn?.("Native webhook notification skipped: no delivery hook is available.", source);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
const destination = await resolveNativeWebhookNotifyDestination({
|
|
896
|
+
endpoint,
|
|
897
|
+
source,
|
|
898
|
+
requestConversationId: input.request.conversationId,
|
|
899
|
+
...(input.listNotifyDestinations === undefined ? {} : { listNotifyDestinations: input.listNotifyDestinations }),
|
|
900
|
+
...(input.logger === undefined ? {} : { logger: input.logger }),
|
|
901
|
+
});
|
|
902
|
+
if (destination === undefined) {
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
const delivery = await input.notifyDestination(destination, text, { verbatim: true });
|
|
906
|
+
if (!delivery.delivered) {
|
|
907
|
+
input.logger?.warn?.("Native webhook notification was not delivered.", {
|
|
908
|
+
...source,
|
|
909
|
+
conversationId: destination,
|
|
910
|
+
...(delivery.reason === undefined ? {} : { reason: delivery.reason }),
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
catch (error) {
|
|
915
|
+
input.logger?.warn?.("Native webhook notification failed.", {
|
|
916
|
+
...source,
|
|
917
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
async function resolveNativeWebhookNotifyDestination(input) {
|
|
922
|
+
if (input.endpoint.notifyConversationId !== undefined) {
|
|
923
|
+
return input.endpoint.notifyConversationId;
|
|
924
|
+
}
|
|
925
|
+
// Async-callback pattern: when the inbound payload names a deliverable chat
|
|
926
|
+
// (e.g. the service posts back the originating `telegram:`/`slack:` id), deliver
|
|
927
|
+
// the answer there. The owning channel's allowlist still bounds it.
|
|
928
|
+
if (input.requestConversationId !== undefined && isDeliverableConversation(input.requestConversationId)) {
|
|
929
|
+
return input.requestConversationId;
|
|
930
|
+
}
|
|
931
|
+
if (input.listNotifyDestinations === undefined) {
|
|
932
|
+
input.logger?.warn?.("Native webhook notification skipped: no destination is configured and no destination resolver is available.", input.source);
|
|
933
|
+
return undefined;
|
|
934
|
+
}
|
|
935
|
+
const destinations = await input.listNotifyDestinations();
|
|
936
|
+
if (destinations.length !== 1) {
|
|
937
|
+
input.logger?.warn?.("Native webhook notification skipped: destination inference requires exactly one candidate.", {
|
|
938
|
+
...input.source,
|
|
939
|
+
destinationCount: destinations.length,
|
|
940
|
+
});
|
|
941
|
+
return undefined;
|
|
942
|
+
}
|
|
943
|
+
return destinations[0]?.conversationId;
|
|
284
944
|
}
|
|
285
945
|
/** Every channel the app can drive, in startup/status display order. */
|
|
286
946
|
export function defaultChannelDrivers(overrides = {}) {
|
|
287
947
|
return [
|
|
288
948
|
createTelegramChannelDriver(overrides.telegram),
|
|
289
949
|
createSlackChannelDriver(overrides.slack),
|
|
290
|
-
createA2AChannelDriver(overrides.a2a),
|
|
291
950
|
createWebhookChannelDriver(overrides.webhook),
|
|
292
951
|
createOpenAIApiChannelDriver(overrides.openaiApi),
|
|
293
952
|
createCronChannelDriver(overrides.cron),
|
|
294
|
-
|
|
953
|
+
createTuiChannelDriver(overrides.tui),
|
|
954
|
+
createLiveChannelDriver(overrides.live),
|
|
295
955
|
];
|
|
296
956
|
}
|
|
957
|
+
export async function resolveChannelDrivers(input, overrides = {}) {
|
|
958
|
+
const drivers = [...defaultChannelDrivers(overrides)];
|
|
959
|
+
const plugins = await resolveConfiguredChannelPlugins(input, { reservedIds: BUILTIN_CHANNEL_IDS });
|
|
960
|
+
for (const plugin of plugins) {
|
|
961
|
+
const existing = drivers.findIndex((driver) => driver.id === plugin.id);
|
|
962
|
+
if (existing >= 0) {
|
|
963
|
+
drivers[existing] = plugin;
|
|
964
|
+
}
|
|
965
|
+
else {
|
|
966
|
+
drivers.push(plugin);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return drivers;
|
|
970
|
+
}
|
|
297
971
|
function telegramStartOptions(input, overrides) {
|
|
972
|
+
// Subscribe to (and handle) inline-keyboard taps only when the `telegram_ask`
|
|
973
|
+
// tool is permitted by the tool policy — so enabling the tool is the single
|
|
974
|
+
// switch that turns on the whole ask/answer round-trip. Without it the bot
|
|
975
|
+
// stays message-only and never registers the callback handler.
|
|
976
|
+
const callbacksEnabled = isAdapterSendToolAllowed("telegram_ask", {
|
|
977
|
+
allowedTools: input.coreConfig.tools.allowedTools,
|
|
978
|
+
disallowedTools: input.coreConfig.tools.disallowedTools,
|
|
979
|
+
});
|
|
298
980
|
return {
|
|
299
981
|
botToken: input.config.botToken,
|
|
300
982
|
allowedChatIds: [...input.config.allowedChatIds],
|
|
301
983
|
allowAllChats: input.config.allowAllChats,
|
|
302
984
|
responder: input.responder,
|
|
303
|
-
allowedUpdates: ["message"],
|
|
985
|
+
allowedUpdates: callbacksEnabled ? ["message", "callback_query"] : ["message"],
|
|
986
|
+
...(callbacksEnabled ? { callbacksEnabled: true } : {}),
|
|
304
987
|
deleteWebhookOnStart: true,
|
|
305
988
|
stream: {
|
|
306
989
|
initialStatusText: "Agent is thinking...",
|
|
@@ -316,9 +999,30 @@ function telegramStartOptions(input, overrides) {
|
|
|
316
999
|
unauthorizedText: "This chat is not allowlisted for this agent.",
|
|
317
1000
|
errorText: telegramErrorText,
|
|
318
1001
|
},
|
|
319
|
-
// A polling crash
|
|
320
|
-
//
|
|
321
|
-
|
|
1002
|
+
// A polling crash is recoverable by construction: the adapter always schedules
|
|
1003
|
+
// a backoff restart (it never gives up on its own), so report it as DEGRADED
|
|
1004
|
+
// (keep the responder/harness alive) rather than fatal. onPollingRecovered flips
|
|
1005
|
+
// the channel back to running once a restarted runner stays up.
|
|
1006
|
+
onPollingError: (error) => input.onDegraded?.(error instanceof Error ? error.message : String(error)),
|
|
1007
|
+
onPollingRecovered: () => input.onRecovered?.(),
|
|
1008
|
+
...(input.config.apiRoot === undefined ? {} : { apiRoot: input.config.apiRoot }),
|
|
1009
|
+
...(telegramAttachmentOptions(input.config) === undefined
|
|
1010
|
+
? {}
|
|
1011
|
+
: { attachments: telegramAttachmentOptions(input.config) }),
|
|
1012
|
+
...(input.interaction === undefined
|
|
1013
|
+
? {}
|
|
1014
|
+
: {
|
|
1015
|
+
pendingAsks: {
|
|
1016
|
+
tryResolve: (conversationId, answer) => input.interaction.tryResolveAsk(conversationId, answer),
|
|
1017
|
+
cancel: (conversationId) => {
|
|
1018
|
+
input.interaction.cancelAsks(conversationId);
|
|
1019
|
+
},
|
|
1020
|
+
},
|
|
1021
|
+
}),
|
|
1022
|
+
...(input.config.ipFamily === undefined ? {} : { transport: { ipFamily: input.config.ipFamily } }),
|
|
1023
|
+
...(input.config.pollWatchdogMs === undefined ? {} : { pollWatchdogMs: input.config.pollWatchdogMs }),
|
|
1024
|
+
...(input.config.commands === undefined ? {} : { commands: [...input.config.commands] }),
|
|
1025
|
+
...(input.config.reactions === undefined ? {} : { reactions: input.config.reactions }),
|
|
322
1026
|
...(input.logger === undefined ? {} : { logger: input.logger }),
|
|
323
1027
|
...(overrides.botFactory === undefined ? {} : { botFactory: overrides.botFactory }),
|
|
324
1028
|
...(overrides.runnerFactory === undefined ? {} : { runnerFactory: overrides.runnerFactory }),
|
|
@@ -326,21 +1030,24 @@ function telegramStartOptions(input, overrides) {
|
|
|
326
1030
|
}
|
|
327
1031
|
function telegramErrorText(input) {
|
|
328
1032
|
const failure = failureFromUnknown(input.error);
|
|
329
|
-
if (failure?.kind
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
return "Cancelled.";
|
|
1033
|
+
if (failure?.kind !== undefined) {
|
|
1034
|
+
const description = describeRunFailureKind({ failureKind: failure.kind });
|
|
1035
|
+
if (description.known || failure.message === undefined || failure.message.trim().length === 0) {
|
|
1036
|
+
const explanation = failure.kind === "usage_limit"
|
|
1037
|
+
? usageLimitExplanation(description.explanation, failure.details)
|
|
1038
|
+
: description.explanation;
|
|
1039
|
+
return `${explanation} ${description.nextStep}`;
|
|
1040
|
+
}
|
|
338
1041
|
}
|
|
339
1042
|
if (failure?.message !== undefined && failure.message.trim().length > 0) {
|
|
340
1043
|
return `I could not complete that message: ${failure.message}`;
|
|
341
1044
|
}
|
|
342
1045
|
return "I could not complete that message. Check the local artifact summary for details.";
|
|
343
1046
|
}
|
|
1047
|
+
function usageLimitExplanation(explanation, details) {
|
|
1048
|
+
const maxTurns = nestedNumber(details, ["diagnostics", "max_turns"]);
|
|
1049
|
+
return maxTurns === undefined ? explanation : `${explanation} Configured turn cap: ${maxTurns} turns.`;
|
|
1050
|
+
}
|
|
344
1051
|
function failureFromUnknown(error) {
|
|
345
1052
|
if (!isRecord(error)) {
|
|
346
1053
|
return undefined;
|