@mono-agent/agent-app 0.3.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 +24 -10
- package/dist/adapter-send-tools-main.d.ts +3 -0
- package/dist/adapter-send-tools-main.d.ts.map +1 -0
- package/dist/adapter-send-tools-main.js +20 -0
- package/dist/adapter-send-tools-main.js.map +1 -0
- package/dist/adapter-send-tools.d.ts +125 -0
- package/dist/adapter-send-tools.d.ts.map +1 -0
- package/dist/adapter-send-tools.js +629 -0
- package/dist/adapter-send-tools.js.map +1 -0
- package/dist/app-config.d.ts +86 -26
- package/dist/app-config.d.ts.map +1 -1
- package/dist/app-config.js +284 -90
- package/dist/app-config.js.map +1 -1
- package/dist/app.d.ts +51 -20
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +889 -119
- 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 +75 -0
- package/dist/backfill.d.ts.map +1 -0
- package/dist/backfill.js +350 -0
- package/dist/backfill.js.map +1 -0
- package/dist/background.d.ts +8 -5
- package/dist/background.d.ts.map +1 -1
- package/dist/background.js +279 -53
- 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 -66
- package/dist/channels.d.ts.map +1 -1
- package/dist/channels.js +850 -120
- package/dist/channels.js.map +1 -1
- package/dist/cli.d.ts +103 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +984 -112
- 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 +17 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +392 -95
- package/dist/doctor.js.map +1 -1
- package/dist/index.d.ts +14 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- 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/launchd.d.ts +0 -2
- package/dist/launchd.d.ts.map +1 -1
- package/dist/launchd.js +0 -2
- package/dist/launchd.js.map +1 -1
- 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.d.ts +3 -0
- package/dist/memory-recall-main.d.ts.map +1 -0
- package/dist/memory-recall-main.js +30 -0
- package/dist/memory-recall-main.js.map +1 -0
- package/dist/memory-recall.d.ts +147 -0
- package/dist/memory-recall.d.ts.map +1 -0
- package/dist/memory-recall.js +345 -0
- package/dist/memory-recall.js.map +1 -0
- 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/sessions.d.ts +21 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +45 -0
- package/dist/sessions.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 +54 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +157 -0
- package/dist/ui.js.map +1 -0
- 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 +22 -17
- package/schema/mono-agent.config.schema.json +1686 -0
- package/skills/mono-agent-composer/SKILL.md +38 -12
- package/skills/mono-agent-composer/agents/openai.yaml +4 -0
- package/skills/mono-agent-composer/references/config-blueprint.md +84 -31
- package/skills/mono-agent-composer/references/discovery-questions.md +49 -36
- package/skills/mono-agent-composer/references/feature-coverage.md +26 -19
- package/skills/mono-agent-composer/references/package-map.md +20 -22
- package/skills/mono-agent-composer/references/playbooks.md +209 -0
- package/skills/mono-agent-composer/references/validation.md +14 -5
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,130 +244,389 @@ 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
|
-
|
|
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) {
|
|
310
|
+
return undefined;
|
|
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 };
|
|
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;
|
|
333
|
+
export function createWebhookChannelDriver(overrides = {}) {
|
|
69
334
|
return {
|
|
70
|
-
id: "
|
|
71
|
-
label: "
|
|
335
|
+
id: "webhook",
|
|
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
|
+
},
|
|
72
353
|
async loadConfig(input) {
|
|
73
|
-
|
|
354
|
+
if (!(await isChannelConfigured(input, WEBHOOK_GATE))) {
|
|
355
|
+
return UNCONFIGURED_WEBHOOK_CONFIG;
|
|
356
|
+
}
|
|
357
|
+
const adapter = await loadWebhookModule();
|
|
358
|
+
// cwd is required so `webhook/*.md` endpoint files are discovered.
|
|
359
|
+
return await adapter.loadWebhookAdapterConfig({ env: input.env, jsonPath: input.configPath, cwd: input.cwd });
|
|
74
360
|
},
|
|
75
361
|
isConfigError(error) {
|
|
76
|
-
return
|
|
362
|
+
return webhookModule !== undefined && error instanceof webhookModule.WebhookAdapterError;
|
|
77
363
|
},
|
|
78
364
|
disabledReason(config) {
|
|
79
|
-
return config.
|
|
365
|
+
return config.enabled ? undefined : "Webhook adapter is disabled.";
|
|
80
366
|
},
|
|
81
367
|
waitingReason(config) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return undefined;
|
|
368
|
+
return config.endpoints.some((endpoint) => endpoint.enabled)
|
|
369
|
+
? undefined
|
|
370
|
+
: "Webhook adapter has no enabled endpoints.";
|
|
86
371
|
},
|
|
87
372
|
async start(input) {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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;
|
|
377
|
+
const adapter = await adapterFactory({
|
|
378
|
+
host: input.config.host,
|
|
379
|
+
port: input.config.port,
|
|
380
|
+
allowNonLoopback: input.config.allowNonLoopback,
|
|
381
|
+
defaultMode: input.config.defaultMode,
|
|
382
|
+
retentionMs: input.config.retentionMs,
|
|
383
|
+
maxStoredRequests: input.config.maxStoredRequests,
|
|
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
|
|
388
|
+
.map((endpoint) => ({
|
|
389
|
+
name: endpoint.name,
|
|
390
|
+
path: endpoint.path,
|
|
391
|
+
mode: endpoint.mode,
|
|
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 }),
|
|
397
|
+
})),
|
|
100
398
|
responder: input.responder,
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
? {}
|
|
107
|
-
: {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
url: config.agent.providerUrl,
|
|
111
|
-
},
|
|
112
|
-
}),
|
|
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
|
+
});
|
|
113
408
|
},
|
|
114
|
-
skill: config.skill,
|
|
115
409
|
...(input.logger === undefined ? {} : { logger: input.logger }),
|
|
116
410
|
});
|
|
117
411
|
return {
|
|
118
|
-
summary: {
|
|
119
|
-
|
|
412
|
+
summary: {
|
|
413
|
+
invokeUrl: adapter.invokeUrl,
|
|
414
|
+
port: adapter.port,
|
|
415
|
+
invokeUrls: Object.fromEntries((adapter.endpoints ?? []).map((endpoint) => [endpoint.name, endpoint.invokeUrl])),
|
|
416
|
+
},
|
|
417
|
+
stop: () => adapter.stop(),
|
|
120
418
|
};
|
|
121
419
|
},
|
|
122
420
|
};
|
|
123
421
|
}
|
|
124
|
-
export function
|
|
422
|
+
export function createOpenAIApiChannelDriver(overrides = {}) {
|
|
125
423
|
return {
|
|
126
|
-
id: "
|
|
127
|
-
label: "
|
|
424
|
+
id: "openai-api",
|
|
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
|
+
},
|
|
128
433
|
async loadConfig(input) {
|
|
129
|
-
|
|
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 });
|
|
130
439
|
},
|
|
131
440
|
isConfigError(error) {
|
|
132
|
-
return error instanceof
|
|
441
|
+
return openaiApiModule !== undefined && error instanceof openaiApiModule.OpenAIApiAdapterError;
|
|
133
442
|
},
|
|
134
443
|
disabledReason(config) {
|
|
135
|
-
return config.enabled ? undefined : "
|
|
444
|
+
return config.enabled ? undefined : "OpenAI API adapter is disabled.";
|
|
136
445
|
},
|
|
137
446
|
async start(input) {
|
|
138
|
-
const
|
|
447
|
+
const adapterModule = await loadOpenAIApiModule();
|
|
448
|
+
const adapterFactory = overrides.adapterFactory ?? adapterModule.startOpenAIApiAdapter;
|
|
139
449
|
const adapter = await adapterFactory({
|
|
140
450
|
host: input.config.host,
|
|
141
451
|
port: input.config.port,
|
|
142
|
-
|
|
452
|
+
basePath: input.config.basePath,
|
|
143
453
|
allowNonLoopback: input.config.allowNonLoopback,
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
maxStoredRequests: input.config.maxStoredRequests,
|
|
454
|
+
...(input.config.apiKey === undefined ? {} : { apiKey: input.config.apiKey }),
|
|
455
|
+
modelId: input.config.modelId,
|
|
147
456
|
responder: input.responder,
|
|
148
457
|
...(input.logger === undefined ? {} : { logger: input.logger }),
|
|
149
458
|
});
|
|
150
459
|
return {
|
|
151
|
-
summary: {
|
|
460
|
+
summary: { baseUrl: adapter.baseUrl },
|
|
152
461
|
stop: () => adapter.stop(),
|
|
153
462
|
};
|
|
154
463
|
},
|
|
155
464
|
};
|
|
156
465
|
}
|
|
157
|
-
|
|
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 = {}) {
|
|
158
483
|
return {
|
|
159
|
-
id: "
|
|
160
|
-
label: "
|
|
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
|
+
},
|
|
161
490
|
async loadConfig(input) {
|
|
162
|
-
|
|
491
|
+
const adapter = await loadTuiModule();
|
|
492
|
+
return await adapter.loadTuiAdapterConfig({ env: input.env, jsonPath: input.configPath });
|
|
163
493
|
},
|
|
164
494
|
isConfigError(error) {
|
|
165
|
-
return error instanceof
|
|
495
|
+
return tuiModule !== undefined && error instanceof tuiModule.TuiAdapterError;
|
|
166
496
|
},
|
|
167
497
|
disabledReason(config) {
|
|
168
|
-
return config.enabled ? undefined : "
|
|
498
|
+
return config.enabled ? undefined : "TUI stream endpoint is disabled.";
|
|
169
499
|
},
|
|
170
500
|
async start(input) {
|
|
171
|
-
const
|
|
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
|
+
};
|
|
172
569
|
const adapter = await adapterFactory({
|
|
173
570
|
host: input.config.host,
|
|
174
571
|
port: input.config.port,
|
|
175
572
|
basePath: input.config.basePath,
|
|
176
573
|
allowNonLoopback: input.config.allowNonLoopback,
|
|
177
574
|
...(input.config.apiKey === undefined ? {} : { apiKey: input.config.apiKey }),
|
|
178
|
-
modelId: input.config.modelId,
|
|
179
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),
|
|
180
630
|
...(input.logger === undefined ? {} : { logger: input.logger }),
|
|
181
631
|
});
|
|
182
632
|
return {
|
|
@@ -186,15 +636,58 @@ export function createOpenAIApiChannelDriver(overrides = {}) {
|
|
|
186
636
|
},
|
|
187
637
|
};
|
|
188
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
|
+
}
|
|
189
661
|
export function createCronChannelDriver(overrides = {}) {
|
|
662
|
+
const failureNoticeLastSentMsByJobId = new Map();
|
|
190
663
|
return {
|
|
191
664
|
id: "cron",
|
|
192
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
|
+
},
|
|
193
682
|
async loadConfig(input) {
|
|
194
|
-
|
|
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 });
|
|
195
688
|
},
|
|
196
689
|
isConfigError(error) {
|
|
197
|
-
return error instanceof CronAdapterError;
|
|
690
|
+
return cronModule !== undefined && error instanceof cronModule.CronAdapterError;
|
|
198
691
|
},
|
|
199
692
|
disabledReason(config) {
|
|
200
693
|
const enabledJobs = config.jobs.filter((job) => job.enabled);
|
|
@@ -202,19 +695,51 @@ export function createCronChannelDriver(overrides = {}) {
|
|
|
202
695
|
},
|
|
203
696
|
async start(input) {
|
|
204
697
|
const jobs = input.config.jobs.filter((job) => job.enabled);
|
|
205
|
-
const
|
|
698
|
+
const jobById = new Map(jobs.map((job) => [job.id, job]));
|
|
699
|
+
const adapterModule = await loadCronModule();
|
|
700
|
+
const adapterFactory = overrides.adapterFactory ?? adapterModule.startCronAdapter;
|
|
206
701
|
const adapter = adapterFactory({
|
|
207
702
|
responder: input.responder,
|
|
703
|
+
// Skip overlapping firings (a job still running when its next tick fires)
|
|
704
|
+
// — the legacy/default app behavior. This avoids the scheduler's
|
|
705
|
+
// unbounded "queue" default retaining stale ticks in memory when a job
|
|
706
|
+
// runs longer than its interval. (Queue/replace remain available to
|
|
707
|
+
// programmatic callers of startCronAdapter.) The watchdog below is what
|
|
708
|
+
// keeps "skip" safe: a hung run no longer pins the slot forever.
|
|
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,
|
|
208
713
|
jobs: jobs.map((job) => ({
|
|
209
714
|
id: job.id,
|
|
210
715
|
expression: job.expression,
|
|
211
716
|
timezone: job.timezone,
|
|
212
717
|
prompt: job.prompt,
|
|
213
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 }),
|
|
214
724
|
})),
|
|
215
725
|
onResult: (result) => {
|
|
216
726
|
const level = result.kind === "failed" ? "error" : result.kind === "skipped" ? "warn" : "info";
|
|
217
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
|
+
});
|
|
218
743
|
},
|
|
219
744
|
...(input.logger === undefined ? {} : { logger: input.logger }),
|
|
220
745
|
});
|
|
@@ -227,57 +752,238 @@ export function createCronChannelDriver(overrides = {}) {
|
|
|
227
752
|
},
|
|
228
753
|
};
|
|
229
754
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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 }),
|
|
254
791
|
});
|
|
255
|
-
return
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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;
|
|
261
944
|
}
|
|
262
945
|
/** Every channel the app can drive, in startup/status display order. */
|
|
263
946
|
export function defaultChannelDrivers(overrides = {}) {
|
|
264
947
|
return [
|
|
265
948
|
createTelegramChannelDriver(overrides.telegram),
|
|
266
949
|
createSlackChannelDriver(overrides.slack),
|
|
267
|
-
createA2AChannelDriver(overrides.a2a),
|
|
268
950
|
createWebhookChannelDriver(overrides.webhook),
|
|
269
951
|
createOpenAIApiChannelDriver(overrides.openaiApi),
|
|
270
952
|
createCronChannelDriver(overrides.cron),
|
|
271
|
-
|
|
953
|
+
createTuiChannelDriver(overrides.tui),
|
|
954
|
+
createLiveChannelDriver(overrides.live),
|
|
272
955
|
];
|
|
273
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
|
+
}
|
|
274
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
|
+
});
|
|
275
980
|
return {
|
|
276
981
|
botToken: input.config.botToken,
|
|
277
982
|
allowedChatIds: [...input.config.allowedChatIds],
|
|
278
983
|
allowAllChats: input.config.allowAllChats,
|
|
279
984
|
responder: input.responder,
|
|
280
|
-
allowedUpdates: ["message"],
|
|
985
|
+
allowedUpdates: callbacksEnabled ? ["message", "callback_query"] : ["message"],
|
|
986
|
+
...(callbacksEnabled ? { callbacksEnabled: true } : {}),
|
|
281
987
|
deleteWebhookOnStart: true,
|
|
282
988
|
stream: {
|
|
283
989
|
initialStatusText: "Agent is thinking...",
|
|
@@ -293,9 +999,30 @@ function telegramStartOptions(input, overrides) {
|
|
|
293
999
|
unauthorizedText: "This chat is not allowlisted for this agent.",
|
|
294
1000
|
errorText: telegramErrorText,
|
|
295
1001
|
},
|
|
296
|
-
// A polling crash
|
|
297
|
-
//
|
|
298
|
-
|
|
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 }),
|
|
299
1026
|
...(input.logger === undefined ? {} : { logger: input.logger }),
|
|
300
1027
|
...(overrides.botFactory === undefined ? {} : { botFactory: overrides.botFactory }),
|
|
301
1028
|
...(overrides.runnerFactory === undefined ? {} : { runnerFactory: overrides.runnerFactory }),
|
|
@@ -303,21 +1030,24 @@ function telegramStartOptions(input, overrides) {
|
|
|
303
1030
|
}
|
|
304
1031
|
function telegramErrorText(input) {
|
|
305
1032
|
const failure = failureFromUnknown(input.error);
|
|
306
|
-
if (failure?.kind
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
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
|
+
}
|
|
315
1041
|
}
|
|
316
1042
|
if (failure?.message !== undefined && failure.message.trim().length > 0) {
|
|
317
1043
|
return `I could not complete that message: ${failure.message}`;
|
|
318
1044
|
}
|
|
319
1045
|
return "I could not complete that message. Check the local artifact summary for details.";
|
|
320
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
|
+
}
|
|
321
1051
|
function failureFromUnknown(error) {
|
|
322
1052
|
if (!isRecord(error)) {
|
|
323
1053
|
return undefined;
|