@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/app.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
import { registerTraceSource } from "@mono-agent/observability";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
2
|
+
import { createLiveEventBus } from "@mono-agent/agent-contracts";
|
|
3
|
+
import { pruneTraceSources, reconcileStaleRunArtifacts, registerTraceSource } from "@mono-agent/observability";
|
|
4
|
+
import { modelReferenceKey } from "@mono-agent/runtime-adapter";
|
|
5
|
+
import { describeSandboxEffectiveState, resolveSandboxEffectiveState, sandboxEffectiveStateWarning, } from "@mono-agent/runtime-adapter";
|
|
6
|
+
import { isAppCoreConfigError, loadAppCoreConfig, resolveAppArtifactDir, resolveAppObservabilityExporters, resolveAppTraceGlobalDiscovery, resolveAppTraceHeartbeatMs, resolveAppTraceRegistryDir, resolveAppTraceSourceId, resolveAppTraceSourceLabel, resolveAppTraceStaleAfterMs, resolveGlobalTraceRegistryDir, resolveTraceTmpdirRoot, shouldMirrorTraceSourceGlobally, } from "./app-config.js";
|
|
7
|
+
import { createConfiguredAgentResponder, createConfiguredAgentRuntime, createConfiguredMemory, } from "./configured-agent.js";
|
|
8
|
+
import { resolveChannelDrivers } from "./channels.js";
|
|
9
|
+
import { routeProactiveNotification } from "./proactive-notify.js";
|
|
10
|
+
import { adapterSendToolNames, createAdapterSendToolsRuntimeExtension, isAdapterSendToolAllowed, resolveAdapterSendToolsSettings, } from "./adapter-send-tools.js";
|
|
11
|
+
import { loadInteractionSettings, startInteractionBridge } from "./interaction-bridge.js";
|
|
7
12
|
import { startMemoryRituals } from "./memory-rituals.js";
|
|
13
|
+
import { startArtifactRetentionScheduler } from "./artifact-retention.js";
|
|
8
14
|
import { createMemoryRecallRuntimeExtension, resolveMemoryRecallSettings, } from "./memory-recall.js";
|
|
15
|
+
import { createRequestModelOverrideRuntimeExtension } from "./request-model-override.js";
|
|
16
|
+
import { resolveNotifyDestinations } from "./notify-destinations.js";
|
|
17
|
+
import { resolvePostedMessageIndexPath } from "./posted-message-index.js";
|
|
9
18
|
/**
|
|
10
19
|
* Starts a config-first mono-agent host in `cwd`: traceability first, then every
|
|
11
20
|
* configured channel in parallel. Channels with incomplete config report
|
|
@@ -16,7 +25,7 @@ export async function startMonoAgentApp(options = {}) {
|
|
|
16
25
|
const cwd = resolve(options.cwd ?? process.cwd());
|
|
17
26
|
const configPath = resolve(cwd, options.configPath ?? "mono-agent.config.json");
|
|
18
27
|
const env = options.env ?? process.env;
|
|
19
|
-
const drivers = options.drivers ??
|
|
28
|
+
const drivers = options.drivers ?? await resolveChannelDrivers({ env, cwd, configPath });
|
|
20
29
|
const controller = new MonoAgentAppController({
|
|
21
30
|
cwd,
|
|
22
31
|
configPath,
|
|
@@ -24,8 +33,10 @@ export async function startMonoAgentApp(options = {}) {
|
|
|
24
33
|
drivers,
|
|
25
34
|
...(options.logger === undefined ? {} : { logger: options.logger }),
|
|
26
35
|
...(options.runtime === undefined ? {} : { runtime: options.runtime }),
|
|
36
|
+
...(options.sandboxEngine === undefined ? {} : { sandboxEngine: options.sandboxEngine }),
|
|
27
37
|
...(options.traceDefaults === undefined ? {} : { traceDefaults: options.traceDefaults }),
|
|
28
38
|
});
|
|
39
|
+
await controller.refreshSandboxStatus("startup");
|
|
29
40
|
await controller.startTraceability("startup");
|
|
30
41
|
await controller.startExporters("startup");
|
|
31
42
|
await Promise.all(drivers.map((driver) => controller?.startChannelIfConfigured(driver.id, "startup")));
|
|
@@ -33,6 +44,16 @@ export async function startMonoAgentApp(options = {}) {
|
|
|
33
44
|
await controller.refreshTraceSource("startup-complete");
|
|
34
45
|
return controller;
|
|
35
46
|
}
|
|
47
|
+
const DEFAULT_SANDBOX_STATUS = sandboxStatusFromState({
|
|
48
|
+
configured: false,
|
|
49
|
+
configuredMode: undefined,
|
|
50
|
+
effective: "off",
|
|
51
|
+
engine: undefined,
|
|
52
|
+
engineAvailable: undefined,
|
|
53
|
+
fallback: undefined,
|
|
54
|
+
fallbackActive: false,
|
|
55
|
+
unsafeAllowHostProcess: false,
|
|
56
|
+
});
|
|
36
57
|
class MonoAgentAppController {
|
|
37
58
|
configPath;
|
|
38
59
|
cwd;
|
|
@@ -41,11 +62,15 @@ class MonoAgentAppController {
|
|
|
41
62
|
driversById;
|
|
42
63
|
logger;
|
|
43
64
|
runtime;
|
|
65
|
+
sandboxEngine;
|
|
44
66
|
traceDefaults;
|
|
45
67
|
activeRuntimes = [];
|
|
46
68
|
statuses = new Map();
|
|
47
69
|
running = new Map();
|
|
48
70
|
startsInFlight = new Map();
|
|
71
|
+
/** Captured at construction (~process start): the cutoff for reclaiming orphaned "running" runs. */
|
|
72
|
+
processStartMs = Date.now();
|
|
73
|
+
staleRunsReconciled = false;
|
|
49
74
|
traceabilityStatusValue = {
|
|
50
75
|
kind: "disabled",
|
|
51
76
|
reason: "Traceability has not started yet.",
|
|
@@ -54,16 +79,38 @@ class MonoAgentAppController {
|
|
|
54
79
|
kind: "disabled",
|
|
55
80
|
reason: "No observability exporter configured.",
|
|
56
81
|
};
|
|
82
|
+
sandboxStatusValue = DEFAULT_SANDBOX_STATUS;
|
|
83
|
+
selectedSkillsValue;
|
|
84
|
+
sessionMetadataValue;
|
|
57
85
|
/** The exporter the responder threads into agent-host (first configured exporter). */
|
|
58
86
|
resolvedExporter;
|
|
59
87
|
traceSource;
|
|
88
|
+
traceRefreshTail = Promise.resolve();
|
|
89
|
+
// Best-effort mirror of `traceSource` into the machine-wide global registry,
|
|
90
|
+
// present only when `shouldMirrorTraceSourceGlobally` gates it on (see
|
|
91
|
+
// `startTraceability`). Kept in lockstep with `traceSource` on every
|
|
92
|
+
// refresh/stop so both manifests describe the same instance identically.
|
|
93
|
+
globalTraceSource;
|
|
94
|
+
artifactRetentionScheduler;
|
|
95
|
+
artifactRetentionGeneration = 0;
|
|
60
96
|
memoryRituals;
|
|
61
97
|
// One shared memory store across all channel responders + the ritual scheduler, so there is a single
|
|
62
98
|
// memory.db handle (not one per channel plus one for rituals). Rebuilt on config reload, closed on stop.
|
|
63
99
|
sharedMemory = undefined;
|
|
64
100
|
sharedMemoryBuilt = false;
|
|
101
|
+
sharedMemoryBuild;
|
|
65
102
|
configApplyTail = Promise.resolve();
|
|
66
103
|
stopped = false;
|
|
104
|
+
// Interaction bridge (ask_user + tool progress): lazily started once, shared
|
|
105
|
+
// by every channel; the exported env keys are tracked for cleanup on stop.
|
|
106
|
+
interactionBridge;
|
|
107
|
+
interactionBridgeStart;
|
|
108
|
+
interactionBridgeEnvKeys = [];
|
|
109
|
+
// Shared in-process run-event bus: every run's recorder publishes to it (via the
|
|
110
|
+
// broadcast recorder threaded as `runEventSink`), and the `live` channel relays
|
|
111
|
+
// it over SSE. One instance for the app's lifetime — cheap, bounded ring buffer,
|
|
112
|
+
// and it must exist before any responder is built (like the interaction bridge).
|
|
113
|
+
liveEventBus = createLiveEventBus();
|
|
67
114
|
constructor(input) {
|
|
68
115
|
this.cwd = input.cwd;
|
|
69
116
|
this.configPath = input.configPath;
|
|
@@ -72,6 +119,7 @@ class MonoAgentAppController {
|
|
|
72
119
|
this.driversById = new Map(input.drivers.map((driver) => [driver.id, driver]));
|
|
73
120
|
this.logger = input.logger;
|
|
74
121
|
this.runtime = input.runtime;
|
|
122
|
+
this.sandboxEngine = input.sandboxEngine;
|
|
75
123
|
this.traceDefaults = input.traceDefaults;
|
|
76
124
|
for (const driver of input.drivers) {
|
|
77
125
|
this.statuses.set(driver.id, {
|
|
@@ -86,6 +134,12 @@ class MonoAgentAppController {
|
|
|
86
134
|
get exporterStatus() {
|
|
87
135
|
return this.exporterStatusValue;
|
|
88
136
|
}
|
|
137
|
+
get sandboxStatus() {
|
|
138
|
+
return this.sandboxStatusValue;
|
|
139
|
+
}
|
|
140
|
+
get selectedSkills() {
|
|
141
|
+
return this.selectedSkillsValue;
|
|
142
|
+
}
|
|
89
143
|
channelStatus(id) {
|
|
90
144
|
const status = this.statuses.get(id);
|
|
91
145
|
if (status === undefined) {
|
|
@@ -109,8 +163,10 @@ class MonoAgentAppController {
|
|
|
109
163
|
await this.stopChannel(driver.id, `${reason}:reload`);
|
|
110
164
|
}
|
|
111
165
|
this.stopMemoryRituals();
|
|
166
|
+
this.stopArtifactRetentionScheduler();
|
|
112
167
|
await this.resetSharedMemory();
|
|
113
168
|
await this.stopTraceSource(`${reason}:reload`);
|
|
169
|
+
await this.refreshSandboxStatus(reason);
|
|
114
170
|
await this.startTraceability(reason);
|
|
115
171
|
await this.startExporters(reason);
|
|
116
172
|
await Promise.all(this.drivers.map((driver) => this.startChannelIfConfigured(driver.id, reason)));
|
|
@@ -147,16 +203,20 @@ class MonoAgentAppController {
|
|
|
147
203
|
if (this.stopped) {
|
|
148
204
|
return this.traceabilityStatusValue;
|
|
149
205
|
}
|
|
206
|
+
let artifactDirForRetention;
|
|
150
207
|
try {
|
|
151
208
|
const input = { env: this.env, cwd: this.cwd, configPath: this.configPath };
|
|
152
|
-
|
|
209
|
+
await this.refreshSelectedSkillsSnapshot(reason);
|
|
210
|
+
const [registryDir, artifactDir, sourceId, label, heartbeatMs, globalDiscovery] = await Promise.all([
|
|
153
211
|
resolveAppTraceRegistryDir(input),
|
|
154
212
|
resolveAppArtifactDir(input),
|
|
155
213
|
resolveAppTraceSourceId(input, this.traceDefaults),
|
|
156
214
|
resolveAppTraceSourceLabel(input, this.traceDefaults),
|
|
157
215
|
resolveAppTraceHeartbeatMs(input),
|
|
216
|
+
resolveAppTraceGlobalDiscovery(input),
|
|
158
217
|
]);
|
|
159
|
-
|
|
218
|
+
artifactDirForRetention = artifactDir;
|
|
219
|
+
const registerOptions = {
|
|
160
220
|
registryDir,
|
|
161
221
|
sourceId,
|
|
162
222
|
label,
|
|
@@ -166,17 +226,102 @@ class MonoAgentAppController {
|
|
|
166
226
|
configPath: this.configPath,
|
|
167
227
|
metadata: this.traceMetadata(reason),
|
|
168
228
|
heartbeatMs,
|
|
169
|
-
}
|
|
229
|
+
};
|
|
230
|
+
this.traceSource = await registerTraceSource(registerOptions);
|
|
170
231
|
this.traceabilityStatusValue = { kind: "running", sourceId, registryDir, artifactDir };
|
|
171
232
|
this.logger?.info?.("Traceability source registered.", { reason, sourceId, registryDir, artifactDir });
|
|
233
|
+
void pruneTraceSources({ registryDir });
|
|
234
|
+
// Best-effort global mirror: makes this agent discoverable by `mono-agent
|
|
235
|
+
// tui` run anywhere on the machine, even when its own registryDir is a
|
|
236
|
+
// config-local override (e.g. `mono-agent init`'s scaffold). A mirror
|
|
237
|
+
// failure must never affect the primary registration above.
|
|
238
|
+
const globalRegistryDir = resolveGlobalTraceRegistryDir(this.env);
|
|
239
|
+
const tmpdirRoot = resolveTraceTmpdirRoot(this.env);
|
|
240
|
+
if (shouldMirrorTraceSourceGlobally({ registryDir, globalRegistryDir, globalDiscovery, tmpdirRoot })) {
|
|
241
|
+
try {
|
|
242
|
+
this.globalTraceSource = await registerTraceSource({ ...registerOptions, registryDir: globalRegistryDir });
|
|
243
|
+
void pruneTraceSources({ registryDir: globalRegistryDir });
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
this.globalTraceSource = undefined;
|
|
247
|
+
this.logger?.warn?.("Global trace-source mirror registration failed.", { reason: reasonOf(error) });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
this.globalTraceSource = undefined;
|
|
252
|
+
}
|
|
172
253
|
}
|
|
173
254
|
catch (error) {
|
|
174
255
|
const failure = reasonOf(error);
|
|
175
256
|
this.traceabilityStatusValue = { kind: "failed", reason: failure };
|
|
176
257
|
this.logger?.error?.("Traceability source registration failed.", { reason: failure });
|
|
177
258
|
}
|
|
259
|
+
finally {
|
|
260
|
+
if (artifactDirForRetention !== undefined) {
|
|
261
|
+
// Fire-and-forget: orphan reclamation and retention are best-effort cleanup
|
|
262
|
+
// and must not gate startup readiness. Keep them independent from trace
|
|
263
|
+
// source registration; a broken registry should not disable artifact GC.
|
|
264
|
+
this.restartArtifactRetentionScheduler(artifactDirForRetention, reason);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
178
267
|
return this.traceabilityStatusValue;
|
|
179
268
|
}
|
|
269
|
+
async refreshSandboxStatus(reason) {
|
|
270
|
+
try {
|
|
271
|
+
const input = { env: this.env, cwd: this.cwd, configPath: this.configPath };
|
|
272
|
+
const coreConfig = await loadAppCoreConfig(input);
|
|
273
|
+
const state = await resolveSandboxEffectiveState({
|
|
274
|
+
...(coreConfig.sandbox === undefined ? {} : { policy: coreConfig.sandbox }),
|
|
275
|
+
...(this.sandboxEngine === undefined ? {} : { engine: this.sandboxEngine }),
|
|
276
|
+
});
|
|
277
|
+
const status = sandboxStatusFromState(state);
|
|
278
|
+
this.sandboxStatusValue = status;
|
|
279
|
+
if (status.warning !== undefined) {
|
|
280
|
+
this.logger?.warn?.(status.warning, { reason, detail: status.detail });
|
|
281
|
+
}
|
|
282
|
+
return status;
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
const resolutionError = reasonOf(error);
|
|
286
|
+
const status = {
|
|
287
|
+
...DEFAULT_SANDBOX_STATUS,
|
|
288
|
+
detail: `Sandbox status unavailable until agent config loads: ${resolutionError}`,
|
|
289
|
+
resolutionError,
|
|
290
|
+
};
|
|
291
|
+
this.sandboxStatusValue = status;
|
|
292
|
+
this.logger?.info?.("Sandbox status unavailable until agent config loads.", { reason, detail: resolutionError });
|
|
293
|
+
return status;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* One-shot at startup: a process that crashed mid-run leaves its summary stuck at "running"
|
|
298
|
+
* forever (a ghost run in `status`/observability). Reclaim those — any "running" summary that
|
|
299
|
+
* began before THIS process started — by rewriting them to "interrupted". Gated so a config
|
|
300
|
+
* reload (which re-runs startTraceability) does not repeat the scan. Best-effort: never fatal.
|
|
301
|
+
*/
|
|
302
|
+
async reconcileStaleRunsOnce(artifactDir) {
|
|
303
|
+
if (this.staleRunsReconciled) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
this.staleRunsReconciled = true;
|
|
307
|
+
try {
|
|
308
|
+
const { reconciled, warnings } = await reconcileStaleRunArtifacts(artifactDir, {
|
|
309
|
+
startedBeforeMs: this.processStartMs,
|
|
310
|
+
});
|
|
311
|
+
for (const warning of warnings) {
|
|
312
|
+
this.logger?.warn?.(`Stale-run reconciliation: ${warning}`);
|
|
313
|
+
}
|
|
314
|
+
if (reconciled.length > 0) {
|
|
315
|
+
this.logger?.info?.('Reclaimed orphaned runs left as "running" by a prior process.', {
|
|
316
|
+
count: reconciled.length,
|
|
317
|
+
runIds: reconciled.slice(0, 20),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
this.logger?.warn?.("Stale-run reconciliation failed.", { reason: reasonOf(error) });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
180
325
|
/**
|
|
181
326
|
* Resolve the configured observability exporter(s) and publish the export
|
|
182
327
|
* status. No reachability probe runs here — Phoenix may start after the agent,
|
|
@@ -235,18 +380,84 @@ class MonoAgentAppController {
|
|
|
235
380
|
void this.refreshTraceSource("exporter-warning").catch(() => undefined);
|
|
236
381
|
}
|
|
237
382
|
async refreshTraceSource(reason) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
383
|
+
const run = async () => {
|
|
384
|
+
if (this.traceSource === undefined) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const patch = { transports: this.activeTransports(), metadata: this.traceMetadata(reason) };
|
|
388
|
+
try {
|
|
389
|
+
await this.traceSource.update(patch);
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
this.logger?.warn?.("Traceability source update failed.", { reason: reasonOf(error) });
|
|
393
|
+
}
|
|
394
|
+
if (this.globalTraceSource !== undefined) {
|
|
395
|
+
try {
|
|
396
|
+
await this.globalTraceSource.update(patch);
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
this.logger?.warn?.("Global trace-source mirror update failed.", { reason: reasonOf(error) });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
const next = this.traceRefreshTail.then(run, run);
|
|
404
|
+
this.traceRefreshTail = next.then(() => undefined, () => undefined);
|
|
405
|
+
await next;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Start the interaction bridge once, when the ask_user tool is allowed by the
|
|
409
|
+
* tool policy or the operator configured the `interaction` block. Exports the
|
|
410
|
+
* bridge env into the app env AND process env so settings resolution and
|
|
411
|
+
* spawned stdio tool children (which inherit process.env) can reach it.
|
|
412
|
+
*/
|
|
413
|
+
ensureInteractionBridge(coreConfig) {
|
|
414
|
+
this.interactionBridgeStart ??= (async () => {
|
|
415
|
+
const askUserAllowed = isAdapterSendToolAllowed("ask_user", {
|
|
416
|
+
allowedTools: coreConfig.tools.allowedTools,
|
|
417
|
+
disallowedTools: coreConfig.tools.disallowedTools,
|
|
245
418
|
});
|
|
419
|
+
const settings = await loadInteractionSettings({ env: this.env, configPath: this.configPath });
|
|
420
|
+
if (!askUserAllowed && !settings.configured) {
|
|
421
|
+
return undefined;
|
|
422
|
+
}
|
|
423
|
+
try {
|
|
424
|
+
const bridge = await startInteractionBridge({
|
|
425
|
+
host: settings.host,
|
|
426
|
+
port: settings.port,
|
|
427
|
+
askTimeoutMs: settings.askTimeoutMs,
|
|
428
|
+
...(this.logger === undefined ? {} : { logger: this.logger }),
|
|
429
|
+
});
|
|
430
|
+
const bridgeEnv = bridge.env();
|
|
431
|
+
this.interactionBridgeEnvKeys = Object.keys(bridgeEnv);
|
|
432
|
+
Object.assign(this.env, bridgeEnv);
|
|
433
|
+
if (this.env !== process.env) {
|
|
434
|
+
Object.assign(process.env, bridgeEnv);
|
|
435
|
+
}
|
|
436
|
+
this.interactionBridge = bridge;
|
|
437
|
+
this.logger?.info?.("Interaction bridge started.", { url: bridge.url });
|
|
438
|
+
return bridge;
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
this.logger?.warn?.("Interaction bridge failed to start; ask_user and tool progress are unavailable.", {
|
|
442
|
+
reason: reasonOf(error),
|
|
443
|
+
});
|
|
444
|
+
return undefined;
|
|
445
|
+
}
|
|
446
|
+
})();
|
|
447
|
+
return this.interactionBridgeStart;
|
|
448
|
+
}
|
|
449
|
+
async stopInteractionBridge() {
|
|
450
|
+
const bridge = this.interactionBridge;
|
|
451
|
+
this.interactionBridge = undefined;
|
|
452
|
+
this.interactionBridgeStart = undefined;
|
|
453
|
+
for (const key of this.interactionBridgeEnvKeys) {
|
|
454
|
+
delete this.env[key];
|
|
455
|
+
if (this.env !== process.env) {
|
|
456
|
+
delete process.env[key];
|
|
457
|
+
}
|
|
246
458
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
459
|
+
this.interactionBridgeEnvKeys = [];
|
|
460
|
+
await bridge?.stop().catch(() => undefined);
|
|
250
461
|
}
|
|
251
462
|
async stop() {
|
|
252
463
|
if (this.stopped) {
|
|
@@ -256,7 +467,9 @@ class MonoAgentAppController {
|
|
|
256
467
|
for (const driver of this.drivers) {
|
|
257
468
|
await this.stopChannel(driver.id, "stop");
|
|
258
469
|
}
|
|
470
|
+
await this.stopInteractionBridge();
|
|
259
471
|
this.stopMemoryRituals();
|
|
472
|
+
this.stopArtifactRetentionScheduler();
|
|
260
473
|
await this.resetSharedMemory();
|
|
261
474
|
await this.stopTraceSource("stop");
|
|
262
475
|
for (const runtime of this.activeRuntimes.splice(0)) {
|
|
@@ -271,26 +484,30 @@ class MonoAgentAppController {
|
|
|
271
484
|
try {
|
|
272
485
|
const input = { env: this.env, cwd: this.cwd, configPath: this.configPath };
|
|
273
486
|
coreConfig = await loadAppCoreConfig(input);
|
|
487
|
+
this.rememberSelectedSkills(coreConfig);
|
|
274
488
|
}
|
|
275
489
|
catch {
|
|
276
|
-
// Config not ready yet —
|
|
490
|
+
// Config not ready yet — consolidation will start on the next applyConfigChange.
|
|
277
491
|
return;
|
|
278
492
|
}
|
|
279
493
|
if (coreConfig.memory?.mode !== "bujo") {
|
|
280
494
|
return;
|
|
281
495
|
}
|
|
282
|
-
const runtime = this.runtime ?? createConfiguredAgentRuntime(
|
|
496
|
+
const runtime = this.runtime ?? createConfiguredAgentRuntime({
|
|
497
|
+
config: coreConfig,
|
|
498
|
+
...(this.sandboxEngine === undefined ? {} : { sandboxEngine: this.sandboxEngine }),
|
|
499
|
+
});
|
|
283
500
|
if (!this.activeRuntimes.includes(runtime)) {
|
|
284
501
|
this.activeRuntimes.push(runtime);
|
|
285
502
|
}
|
|
286
|
-
const store = this.memoryStore(coreConfig
|
|
287
|
-
// Duck-type: only bujo-tier BujoMemoryStore has
|
|
503
|
+
const store = await this.memoryStore(coreConfig);
|
|
504
|
+
// Duck-type: only bujo-tier BujoMemoryStore has consolidate().
|
|
288
505
|
// Cast through unknown to bypass the MemoryStore contract's type mismatch.
|
|
289
506
|
const storeAsAny = store;
|
|
290
507
|
if (store === undefined ||
|
|
291
|
-
typeof storeAsAny["
|
|
292
|
-
typeof storeAsAny["migrate"] !== "function" ||
|
|
508
|
+
typeof storeAsAny["consolidate"] !== "function" ||
|
|
293
509
|
typeof storeAsAny["tier"] !== "function") {
|
|
510
|
+
this.logger?.info?.("Memory consolidation scheduler skipped — store does not support consolidate().", { reason });
|
|
294
511
|
return;
|
|
295
512
|
}
|
|
296
513
|
const bujoStore = store;
|
|
@@ -299,13 +516,12 @@ class MonoAgentAppController {
|
|
|
299
516
|
// scheduler started in that case — log an accurate skip instead.
|
|
300
517
|
const tier = bujoStore.tier();
|
|
301
518
|
if (tier !== "bujo") {
|
|
302
|
-
this.logger?.info?.("Memory
|
|
519
|
+
this.logger?.info?.("Memory consolidation scheduler skipped — configured bujo mode resolved to the journal tier because memory.llm is missing.", { reason, tier });
|
|
303
520
|
return;
|
|
304
521
|
}
|
|
305
522
|
this.memoryRituals = startMemoryRituals({
|
|
306
523
|
store: bujoStore,
|
|
307
|
-
...(coreConfig.memory.
|
|
308
|
-
...(coreConfig.memory.migration !== undefined && { migration: coreConfig.memory.migration }),
|
|
524
|
+
...(coreConfig.memory.consolidation !== undefined && { consolidation: coreConfig.memory.consolidation }),
|
|
309
525
|
...(this.logger !== undefined && {
|
|
310
526
|
logger: {
|
|
311
527
|
info: (m) => this.logger?.info?.(m),
|
|
@@ -313,7 +529,7 @@ class MonoAgentAppController {
|
|
|
313
529
|
},
|
|
314
530
|
}),
|
|
315
531
|
});
|
|
316
|
-
this.logger?.info?.("Memory
|
|
532
|
+
this.logger?.info?.("Memory consolidation scheduler started.", { reason, mode: "bujo" });
|
|
317
533
|
}
|
|
318
534
|
stopMemoryRituals() {
|
|
319
535
|
const rituals = this.memoryRituals;
|
|
@@ -322,7 +538,94 @@ class MonoAgentAppController {
|
|
|
322
538
|
}
|
|
323
539
|
this.memoryRituals = undefined;
|
|
324
540
|
rituals.stop();
|
|
325
|
-
this.logger?.info?.("Memory
|
|
541
|
+
this.logger?.info?.("Memory consolidation scheduler stopped.");
|
|
542
|
+
}
|
|
543
|
+
restartArtifactRetentionScheduler(artifactDir, reason) {
|
|
544
|
+
this.stopArtifactRetentionScheduler();
|
|
545
|
+
const generation = ++this.artifactRetentionGeneration;
|
|
546
|
+
void (async () => {
|
|
547
|
+
let coreConfig;
|
|
548
|
+
try {
|
|
549
|
+
const input = { env: this.env, cwd: this.cwd, configPath: this.configPath };
|
|
550
|
+
coreConfig = await loadAppCoreConfig(input);
|
|
551
|
+
this.rememberSelectedSkills(coreConfig);
|
|
552
|
+
}
|
|
553
|
+
catch (error) {
|
|
554
|
+
this.logger?.warn?.("Artifact retention scheduler skipped until core config loads.", { reason: reasonOf(error) });
|
|
555
|
+
void this.reconcileStaleRunsOnce(artifactDir);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (this.stopped || generation !== this.artifactRetentionGeneration) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
this.artifactRetentionScheduler = startArtifactRetentionScheduler({
|
|
562
|
+
artifactDir,
|
|
563
|
+
retention: coreConfig.artifacts.retention,
|
|
564
|
+
memoryRetention: coreConfig.artifacts.memoryRetention,
|
|
565
|
+
...(this.logger === undefined ? {} : { logger: this.logger }),
|
|
566
|
+
beforeFirstRun: () => this.reconcileStaleRunsOnce(artifactDir),
|
|
567
|
+
});
|
|
568
|
+
this.logger?.info?.("Artifact retention scheduler started.", {
|
|
569
|
+
reason,
|
|
570
|
+
artifactDir,
|
|
571
|
+
agent: {
|
|
572
|
+
maxAgeDays: coreConfig.artifacts.retention.maxAgeDays,
|
|
573
|
+
maxCount: coreConfig.artifacts.retention.maxCount,
|
|
574
|
+
dryRun: coreConfig.artifacts.retention.dryRun,
|
|
575
|
+
},
|
|
576
|
+
memory: {
|
|
577
|
+
maxAgeDays: coreConfig.artifacts.memoryRetention.maxAgeDays,
|
|
578
|
+
maxCount: coreConfig.artifacts.memoryRetention.maxCount,
|
|
579
|
+
dryRun: coreConfig.artifacts.memoryRetention.dryRun,
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
})();
|
|
583
|
+
}
|
|
584
|
+
stopArtifactRetentionScheduler() {
|
|
585
|
+
this.artifactRetentionGeneration += 1;
|
|
586
|
+
const scheduler = this.artifactRetentionScheduler;
|
|
587
|
+
if (scheduler === undefined) {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
this.artifactRetentionScheduler = undefined;
|
|
591
|
+
scheduler.stop();
|
|
592
|
+
this.logger?.info?.("Artifact retention scheduler stopped.");
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Deliver a native cron/webhook notification to whichever running channel owns
|
|
596
|
+
* the destination conversationId. With `verbatim`, the channel posts `text`
|
|
597
|
+
* unchanged (no model call) and records it to history; otherwise it runs `text`
|
|
598
|
+
* as a turn. Best-effort: an unavailable/unsupported destination is warned and
|
|
599
|
+
* skipped, never thrown back to the cron/webhook trigger.
|
|
600
|
+
*/
|
|
601
|
+
async notifyDestination(conversationId, text, options) {
|
|
602
|
+
const result = await routeProactiveNotification({
|
|
603
|
+
conversationId,
|
|
604
|
+
text,
|
|
605
|
+
running: this.running,
|
|
606
|
+
...(options?.verbatim === undefined ? {} : { verbatim: options.verbatim }),
|
|
607
|
+
...(this.logger === undefined ? {} : { logger: this.logger }),
|
|
608
|
+
});
|
|
609
|
+
// Make the delivery outcome inspectable (the failure cases already warn inside
|
|
610
|
+
// the router / channel hooks; log the success path too so a notify is auditable).
|
|
611
|
+
if (result.delivered) {
|
|
612
|
+
this.logger?.info?.("Proactive notification delivered.", { conversationId });
|
|
613
|
+
}
|
|
614
|
+
return result;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Candidate destinations for native cron/webhook notification delivery, used to
|
|
618
|
+
* infer a target when a job/endpoint sets no explicit `notifyConversationId`.
|
|
619
|
+
*/
|
|
620
|
+
async listNotifyDestinations() {
|
|
621
|
+
const input = { env: this.env, cwd: this.cwd, configPath: this.configPath };
|
|
622
|
+
const artifactDir = await resolveAppArtifactDir(input);
|
|
623
|
+
return await resolveNotifyDestinations({
|
|
624
|
+
input,
|
|
625
|
+
artifactDir,
|
|
626
|
+
isRunning: (id) => this.running.has(id),
|
|
627
|
+
...(this.logger === undefined ? {} : { logger: this.logger }),
|
|
628
|
+
});
|
|
326
629
|
}
|
|
327
630
|
async startChannel(driver, reason) {
|
|
328
631
|
const input = { env: this.env, cwd: this.cwd, configPath: this.configPath };
|
|
@@ -344,9 +647,16 @@ class MonoAgentAppController {
|
|
|
344
647
|
if (waitingReason !== undefined) {
|
|
345
648
|
return this.setStatus(driver.id, { kind: "waiting_for_config", reason: waitingReason });
|
|
346
649
|
}
|
|
650
|
+
// Structural issues (e.g. an invalid per-trigger model override) fail
|
|
651
|
+
// `validate` but only WARN here: the run-time override path ignores the
|
|
652
|
+
// bad value and falls back, so starting is still the safe choice.
|
|
653
|
+
for (const issue of driver.configIssues?.(config) ?? []) {
|
|
654
|
+
this.logger?.warn?.("Channel config issue (run `mono-agent validate`).", { channel: driver.id, issue });
|
|
655
|
+
}
|
|
347
656
|
let coreConfig;
|
|
348
657
|
try {
|
|
349
658
|
coreConfig = await loadAppCoreConfig(input);
|
|
659
|
+
this.rememberSelectedSkills(coreConfig);
|
|
350
660
|
}
|
|
351
661
|
catch (error) {
|
|
352
662
|
if (isAppCoreConfigError(error)) {
|
|
@@ -355,6 +665,13 @@ class MonoAgentAppController {
|
|
|
355
665
|
}
|
|
356
666
|
throw error;
|
|
357
667
|
}
|
|
668
|
+
// Resolve the posted-message index path once so the Slack driver can link
|
|
669
|
+
// posted messages to their producing conversation (in-thread reply continuity).
|
|
670
|
+
const postedMessageIndexPath = resolvePostedMessageIndexPath(await resolveAppArtifactDir(input));
|
|
671
|
+
// The bridge must exist BEFORE the responder is built (ask_user settings
|
|
672
|
+
// resolution reads the exported bridge env) and before driver.start (sink
|
|
673
|
+
// registration + pending-ask interception).
|
|
674
|
+
const interactionBridge = await this.ensureInteractionBridge(coreConfig);
|
|
358
675
|
try {
|
|
359
676
|
const responder = await this.buildResponder(coreConfig);
|
|
360
677
|
// The AgentResponder contract has no dispose(), but the configured responder
|
|
@@ -371,6 +688,11 @@ class MonoAgentAppController {
|
|
|
371
688
|
coreConfig,
|
|
372
689
|
responder,
|
|
373
690
|
cwd: this.cwd,
|
|
691
|
+
notifyDestination: (conversationId, text, options) => this.notifyDestination(conversationId, text, options),
|
|
692
|
+
listNotifyDestinations: () => this.listNotifyDestinations(),
|
|
693
|
+
postedMessageIndexPath,
|
|
694
|
+
...(interactionBridge === undefined ? {} : { interaction: interactionBridge }),
|
|
695
|
+
liveEventBus: this.liveEventBus,
|
|
374
696
|
...(this.logger === undefined ? {} : { logger: this.logger }),
|
|
375
697
|
onFailure: (failureReason) => {
|
|
376
698
|
this.running.delete(driver.id);
|
|
@@ -387,6 +709,32 @@ class MonoAgentAppController {
|
|
|
387
709
|
});
|
|
388
710
|
});
|
|
389
711
|
},
|
|
712
|
+
// A self-recovering transport (e.g. telegram poll crash on a network blip).
|
|
713
|
+
// Unlike onFailure, this must NOT delete the running entry or dispose the
|
|
714
|
+
// responder: the channel owns its own restart, so the harness stays alive
|
|
715
|
+
// for the restarted transport to deliver into, and a later stop/reload still
|
|
716
|
+
// finds the entry and disposes it exactly once.
|
|
717
|
+
onDegraded: (reason) => {
|
|
718
|
+
if (this.statuses.get(driver.id)?.kind === "degraded") {
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
this.setStatus(driver.id, { kind: "degraded", reason });
|
|
722
|
+
this.logger?.warn?.(`${driver.label} channel degraded; transport is recovering.`, { reason });
|
|
723
|
+
},
|
|
724
|
+
// The transport's self-recovery succeeded. Flip back to running, reusing the
|
|
725
|
+
// preserved entry's summary. Guard on the entry: a recovery that races a
|
|
726
|
+
// stop/reload must not resurrect a torn-down channel's status.
|
|
727
|
+
onRecovered: () => {
|
|
728
|
+
const entry = this.running.get(driver.id);
|
|
729
|
+
if (entry === undefined) {
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
if (this.statuses.get(driver.id)?.kind !== "degraded") {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
this.setStatus(driver.id, { kind: "running", summary: entry.summary });
|
|
736
|
+
this.logger?.info?.(`${driver.label} channel recovered.`, {});
|
|
737
|
+
},
|
|
390
738
|
});
|
|
391
739
|
this.running.set(driver.id, {
|
|
392
740
|
...runningChannel,
|
|
@@ -431,14 +779,33 @@ class MonoAgentAppController {
|
|
|
431
779
|
if (!this.activeRuntimes.includes(runtime)) {
|
|
432
780
|
this.activeRuntimes.push(runtime);
|
|
433
781
|
}
|
|
434
|
-
const memory = this.memoryStore(coreConfig
|
|
782
|
+
const memory = await this.memoryStore(coreConfig);
|
|
435
783
|
const memoryRecall = this.memoryRecallRuntimeOptions(coreConfig);
|
|
784
|
+
const supermemoryMcp = this.supermemoryMcpRuntimeOptions(coreConfig);
|
|
436
785
|
const adapterSendTools = await this.adapterSendToolsRuntimeOptions(coreConfig);
|
|
437
|
-
|
|
786
|
+
// Always active: a no-op for interactive turns (which carry no cron/webhook
|
|
787
|
+
// metadata), it applies the per-trigger model/effort override otherwise.
|
|
788
|
+
const requestModelOverride = this.requestModelOverrideRuntimeOptions(coreConfig);
|
|
789
|
+
const runtimeOptionsForRequest = composeRuntimeOptionExtensions([
|
|
790
|
+
memoryRecall,
|
|
791
|
+
supermemoryMcp,
|
|
792
|
+
adapterSendTools,
|
|
793
|
+
requestModelOverride,
|
|
794
|
+
]);
|
|
795
|
+
// The override factory is only needed when fallbacks are configured: the
|
|
796
|
+
// fallback router freezes the model chain, so an override must run on a runtime
|
|
797
|
+
// whose chain has it as primary. With no fallbacks the shared (plain) runtime
|
|
798
|
+
// honors the per-run model directly, so building a separate runtime would be
|
|
799
|
+
// redundant. Omit the factory there and the harness uses the shared runtime.
|
|
800
|
+
const runtimeForModel = (coreConfig.runtime.fallbackModels?.length ?? 0) > 0
|
|
801
|
+
? this.buildRuntimeForModel(coreConfig)
|
|
802
|
+
: undefined;
|
|
438
803
|
const observabilityContext = await this.observabilityContext();
|
|
439
|
-
const responder = createConfiguredAgentResponder({
|
|
804
|
+
const responder = await createConfiguredAgentResponder({
|
|
440
805
|
config: coreConfig,
|
|
441
806
|
runtime,
|
|
807
|
+
...(runtimeForModel === undefined ? {} : { runtimeForModel }),
|
|
808
|
+
...(this.sandboxEngine === undefined ? {} : { sandboxEngine: this.sandboxEngine }),
|
|
442
809
|
...(memory !== undefined && { memory }),
|
|
443
810
|
...(runtimeOptionsForRequest === undefined ? {} : { runtimeOptionsForRequest }),
|
|
444
811
|
// Thread run-identifying context onto exported spans and surface per-run
|
|
@@ -446,9 +813,54 @@ class MonoAgentAppController {
|
|
|
446
813
|
// when config.observability.exporters is non-empty).
|
|
447
814
|
observabilityContext,
|
|
448
815
|
exporterWarn: (warning) => this.recordExporterWarning(warning),
|
|
816
|
+
onSessionEvent: (event) => this.recordSessionEvent(event, coreConfig),
|
|
817
|
+
// Publish every run's start/event/finish to the shared bus so the `live`
|
|
818
|
+
// channel can relay it. Best-effort + additive (see broadcast recorder).
|
|
819
|
+
runEventSink: this.liveEventBus,
|
|
449
820
|
});
|
|
450
821
|
return responder;
|
|
451
822
|
}
|
|
823
|
+
/**
|
|
824
|
+
* Per-request extension that applies a cron/webhook/tui per-trigger model +
|
|
825
|
+
* effort override (validated, warn-and-ignore on bad input). Threads the
|
|
826
|
+
* configured local providers so a LOCAL-model override recomputes its endpoint
|
|
827
|
+
* block for the OVERRIDE model (see request-model-override doc). Composed
|
|
828
|
+
* alongside the memory/adapter extensions.
|
|
829
|
+
*/
|
|
830
|
+
requestModelOverrideRuntimeOptions(coreConfig) {
|
|
831
|
+
const extension = createRequestModelOverrideRuntimeExtension({
|
|
832
|
+
...(this.logger === undefined ? {} : { logger: this.logger }),
|
|
833
|
+
...(coreConfig.providers?.local === undefined ? {} : { localProviders: coreConfig.providers.local }),
|
|
834
|
+
});
|
|
835
|
+
return async (input) => extension({ request: input.request });
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Memoized factory for runtimes bound to a per-request override model. Reuses
|
|
839
|
+
* createConfiguredAgentRuntime so the override becomes the fallback-chain
|
|
840
|
+
* primary with the configured backups after it (override + keep fallbacks).
|
|
841
|
+
* Built runtimes register in `activeRuntimes`, which is disposed on `stop()`
|
|
842
|
+
* (config reload rebuilds the responder but does not drain prior runtimes —
|
|
843
|
+
* same lifetime as the base runtime built in `buildResponder`).
|
|
844
|
+
*/
|
|
845
|
+
buildRuntimeForModel(coreConfig) {
|
|
846
|
+
const cache = new Map();
|
|
847
|
+
return (model, executionMode) => {
|
|
848
|
+
const key = `${modelReferenceKey(model)}|${executionMode ?? ""}`;
|
|
849
|
+
const cached = cache.get(key);
|
|
850
|
+
if (cached !== undefined) {
|
|
851
|
+
return cached;
|
|
852
|
+
}
|
|
853
|
+
const runtime = createConfiguredAgentRuntime({
|
|
854
|
+
config: coreConfig,
|
|
855
|
+
model,
|
|
856
|
+
...(executionMode === undefined ? {} : { executionMode }),
|
|
857
|
+
...(this.sandboxEngine === undefined ? {} : { sandboxEngine: this.sandboxEngine }),
|
|
858
|
+
});
|
|
859
|
+
cache.set(key, runtime);
|
|
860
|
+
this.activeRuntimes.push(runtime);
|
|
861
|
+
return runtime;
|
|
862
|
+
};
|
|
863
|
+
}
|
|
452
864
|
/**
|
|
453
865
|
* Run-identifying context threaded onto exported spans (Phoenix shows the same
|
|
454
866
|
* source/run identifiers as the local trace-source registry, so local artifact
|
|
@@ -469,10 +881,37 @@ class MonoAgentAppController {
|
|
|
469
881
|
return undefined;
|
|
470
882
|
}
|
|
471
883
|
this.logger?.info?.("Read-only memory_recall tool enabled.", {
|
|
472
|
-
provider: settings.embeddings?.provider ?? "fts-only",
|
|
884
|
+
provider: "supermemory" in settings ? "supermemory" : settings.embeddings?.provider ?? "fts-only",
|
|
473
885
|
});
|
|
474
886
|
return createMemoryRecallRuntimeExtension(settings, this.cwd);
|
|
475
887
|
}
|
|
888
|
+
/**
|
|
889
|
+
* Optional CLOUD-ONLY escape hatch: when `memory.supermemory.exposeMcpServer` is on, ALSO inject
|
|
890
|
+
* Supermemory's hosted MCP server alongside the in-app `memory_recall` tool. The hosted MCP cannot
|
|
891
|
+
* point at a self-hosted instance, so self-hosters rely on the in-app recall tool; this just adds
|
|
892
|
+
* the cloud server's richer tools for cloud deployments. Requires an apiKey (skipped + warned if
|
|
893
|
+
* absent).
|
|
894
|
+
*/
|
|
895
|
+
supermemoryMcpRuntimeOptions(coreConfig) {
|
|
896
|
+
const memory = coreConfig.memory;
|
|
897
|
+
if (memory?.backend !== "supermemory" || memory.supermemory?.exposeMcpServer !== true) {
|
|
898
|
+
return undefined;
|
|
899
|
+
}
|
|
900
|
+
const apiKey = memory.supermemory.apiKey;
|
|
901
|
+
if (apiKey === undefined) {
|
|
902
|
+
this.logger?.warn?.("memory.supermemory.exposeMcpServer is on but no apiKey is set; the hosted Supermemory MCP server (cloud-only) was not injected.");
|
|
903
|
+
return undefined;
|
|
904
|
+
}
|
|
905
|
+
this.logger?.info?.("Supermemory hosted MCP server injected (cloud-only).");
|
|
906
|
+
const entry = {
|
|
907
|
+
supermemory: {
|
|
908
|
+
type: "http",
|
|
909
|
+
url: "https://mcp.supermemory.ai/mcp",
|
|
910
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
911
|
+
},
|
|
912
|
+
};
|
|
913
|
+
return async () => ({ runtimeOptions: { mcpServers: entry }, cleanup: async () => { } });
|
|
914
|
+
}
|
|
476
915
|
async adapterSendToolsRuntimeOptions(coreConfig) {
|
|
477
916
|
const input = { env: this.env, cwd: this.cwd, configPath: this.configPath };
|
|
478
917
|
const settings = await resolveAdapterSendToolsSettings(input, {
|
|
@@ -485,25 +924,60 @@ class MonoAgentAppController {
|
|
|
485
924
|
}
|
|
486
925
|
const toolNames = adapterSendToolNames(settings);
|
|
487
926
|
this.logger?.info?.("Adapter send tools enabled.", { tools: toolNames });
|
|
488
|
-
|
|
927
|
+
// Forward the posted-message index path so `slack_send_message` links each post
|
|
928
|
+
// back to the producing conversation (so a later in-thread reply resumes it).
|
|
929
|
+
const indexPath = resolvePostedMessageIndexPath(await resolveAppArtifactDir(input));
|
|
930
|
+
return createAdapterSendToolsRuntimeExtension(this.configPath, this.cwd, toolNames, indexPath);
|
|
489
931
|
}
|
|
490
932
|
/** Build the configured memory store once and share it across responders + the ritual scheduler. */
|
|
491
|
-
memoryStore(coreConfig
|
|
492
|
-
if (
|
|
933
|
+
async memoryStore(coreConfig) {
|
|
934
|
+
if (this.sharedMemoryBuilt) {
|
|
935
|
+
return this.sharedMemory;
|
|
936
|
+
}
|
|
937
|
+
if (this.sharedMemoryBuild !== undefined) {
|
|
938
|
+
return await this.sharedMemoryBuild;
|
|
939
|
+
}
|
|
940
|
+
this.sharedMemoryBuild = (async () => {
|
|
493
941
|
const appLogger = this.logger;
|
|
494
942
|
const logger = appLogger?.warn !== undefined
|
|
495
943
|
? { warn: (message) => { appLogger.warn?.(message); } }
|
|
496
944
|
: undefined;
|
|
497
|
-
|
|
945
|
+
// Thread the per-app observability context so the bujo memory LLM records
|
|
946
|
+
// capture and consolidation runs through the same JSONL + Phoenix pipeline
|
|
947
|
+
// as channel runs (gated by `memory.llm.trace`, default on). The context is
|
|
948
|
+
// per-app (not per-request), so caching it into the shared store is correct.
|
|
949
|
+
//
|
|
950
|
+
// The channel runtime is intentionally NOT passed: the memory LLM must run
|
|
951
|
+
// on `config.memory.llm.model`, but the channel runtime carries the channel
|
|
952
|
+
// fallback chain whose primary is `config.runtime.model` and the fallback
|
|
953
|
+
// router overrides each run's per-call model. createConfiguredMemory builds
|
|
954
|
+
// the memory LLM its own fallback-free runtime when no `memoryRuntime` is set.
|
|
955
|
+
const observabilityContext = await this.observabilityContext();
|
|
956
|
+
const observability = {
|
|
957
|
+
observabilityContext,
|
|
958
|
+
exporterWarn: (warning) => this.recordExporterWarning(warning),
|
|
959
|
+
runEventSink: this.liveEventBus,
|
|
960
|
+
};
|
|
961
|
+
this.sharedMemory = await createConfiguredMemory(coreConfig, {
|
|
962
|
+
...(logger === undefined ? {} : { logger }),
|
|
963
|
+
observability,
|
|
964
|
+
});
|
|
498
965
|
this.sharedMemoryBuilt = true;
|
|
966
|
+
return this.sharedMemory;
|
|
967
|
+
})();
|
|
968
|
+
try {
|
|
969
|
+
return await this.sharedMemoryBuild;
|
|
970
|
+
}
|
|
971
|
+
finally {
|
|
972
|
+
this.sharedMemoryBuild = undefined;
|
|
499
973
|
}
|
|
500
|
-
return this.sharedMemory;
|
|
501
974
|
}
|
|
502
975
|
/** Close + clear the shared memory store (on config reload or stop) so the next build is fresh. */
|
|
503
976
|
async resetSharedMemory() {
|
|
504
977
|
const mem = this.sharedMemory;
|
|
505
978
|
this.sharedMemory = undefined;
|
|
506
979
|
this.sharedMemoryBuilt = false;
|
|
980
|
+
this.sharedMemoryBuild = undefined;
|
|
507
981
|
if (mem?.flush !== undefined) {
|
|
508
982
|
await Promise.resolve(mem.flush()).catch(() => undefined);
|
|
509
983
|
}
|
|
@@ -522,7 +996,8 @@ class MonoAgentAppController {
|
|
|
522
996
|
}
|
|
523
997
|
applyResult() {
|
|
524
998
|
const transports = this.activeTransports();
|
|
525
|
-
const
|
|
999
|
+
const statusEntries = [...this.statuses.entries()];
|
|
1000
|
+
const statuses = statusEntries.map(([, status]) => status);
|
|
526
1001
|
const failedChannel = statuses.find((status) => status.kind === "failed");
|
|
527
1002
|
const failure = failedChannel?.kind === "failed"
|
|
528
1003
|
? failedChannel.reason
|
|
@@ -536,8 +1011,10 @@ class MonoAgentAppController {
|
|
|
536
1011
|
transports,
|
|
537
1012
|
};
|
|
538
1013
|
}
|
|
539
|
-
|
|
540
|
-
|
|
1014
|
+
// A degraded channel is still serving (transport self-recovering, harness alive),
|
|
1015
|
+
// so it counts as running for the "is anything live?" check.
|
|
1016
|
+
const hasServingChannel = statusEntries.some(([id, status]) => id !== "live" && (status.kind === "running" || status.kind === "degraded"));
|
|
1017
|
+
if (!hasServingChannel && statuses.some((status) => status.kind === "waiting_for_config")) {
|
|
541
1018
|
return {
|
|
542
1019
|
kind: "waiting_for_config",
|
|
543
1020
|
message: "Saved config, but no agent channel is running yet.",
|
|
@@ -552,16 +1029,19 @@ class MonoAgentAppController {
|
|
|
552
1029
|
}
|
|
553
1030
|
async stopTraceSource(reason) {
|
|
554
1031
|
const traceSource = this.traceSource;
|
|
555
|
-
|
|
1032
|
+
const globalTraceSource = this.globalTraceSource;
|
|
1033
|
+
if (traceSource === undefined && globalTraceSource === undefined) {
|
|
556
1034
|
return;
|
|
557
1035
|
}
|
|
558
1036
|
this.traceSource = undefined;
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
}).catch((error) => {
|
|
1037
|
+
this.globalTraceSource = undefined;
|
|
1038
|
+
const patch = { metadata: this.traceMetadata(reason), transports: this.activeTransports() };
|
|
1039
|
+
await traceSource?.stop(patch).catch((error) => {
|
|
563
1040
|
this.logger?.warn?.("Traceability source stop update failed.", { reason: reasonOf(error) });
|
|
564
1041
|
});
|
|
1042
|
+
await globalTraceSource?.stop(patch).catch((error) => {
|
|
1043
|
+
this.logger?.warn?.("Global trace-source mirror stop update failed.", { reason: reasonOf(error) });
|
|
1044
|
+
});
|
|
565
1045
|
if (!this.stopped) {
|
|
566
1046
|
this.traceabilityStatusValue = { kind: "disabled", reason: "Traceability source stopped while applying config." };
|
|
567
1047
|
}
|
|
@@ -569,12 +1049,57 @@ class MonoAgentAppController {
|
|
|
569
1049
|
activeTransports() {
|
|
570
1050
|
const transports = [];
|
|
571
1051
|
for (const driver of this.drivers) {
|
|
572
|
-
|
|
1052
|
+
const kind = this.statuses.get(driver.id)?.kind;
|
|
1053
|
+
// A degraded channel is still an active transport (self-recovering, serving).
|
|
1054
|
+
if (kind === "running" || kind === "degraded") {
|
|
573
1055
|
transports.push(driver.id);
|
|
574
1056
|
}
|
|
575
1057
|
}
|
|
576
1058
|
return transports;
|
|
577
1059
|
}
|
|
1060
|
+
async refreshSelectedSkillsSnapshot(reason) {
|
|
1061
|
+
try {
|
|
1062
|
+
const input = { env: this.env, cwd: this.cwd, configPath: this.configPath };
|
|
1063
|
+
this.rememberSelectedSkills(await loadAppCoreConfig(input));
|
|
1064
|
+
}
|
|
1065
|
+
catch (error) {
|
|
1066
|
+
this.selectedSkillsValue = undefined;
|
|
1067
|
+
this.logger?.info?.("Selected skills unavailable until agent config loads.", { reason, detail: reasonOf(error) });
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
rememberSelectedSkills(coreConfig) {
|
|
1071
|
+
this.selectedSkillsValue = [...coreConfig.context.selectedSkills];
|
|
1072
|
+
}
|
|
1073
|
+
recordSessionEvent(event, coreConfig) {
|
|
1074
|
+
const now = new Date();
|
|
1075
|
+
const nextRolloverAt = coreConfig.runtime.session.rollover === "daily"
|
|
1076
|
+
? nextDailyRolloverAt(now, coreConfig.runtime.session.rolloverTimezone)
|
|
1077
|
+
: undefined;
|
|
1078
|
+
const snapshot = event.snapshot ?? [];
|
|
1079
|
+
const current = snapshot.find((entry) => entry.conversationId === event.conversationId);
|
|
1080
|
+
const providerSessionId = current?.providerSessionId ?? event.providerSessionId;
|
|
1081
|
+
this.sessionMetadataValue = {
|
|
1082
|
+
currentBucketId: event.conversationId,
|
|
1083
|
+
state: current === undefined ? "cold" : "warm",
|
|
1084
|
+
event: event.kind,
|
|
1085
|
+
updatedAt: now.toISOString(),
|
|
1086
|
+
...(snapshot.length === 0 ? {} : { snapshot }),
|
|
1087
|
+
...(providerSessionId === undefined ? {} : { providerSessionId }),
|
|
1088
|
+
...(current?.createdAt === undefined ? {} : { createdAt: current.createdAt }),
|
|
1089
|
+
...(current?.lastActivityAt === undefined ? {} : { lastActivityAt: current.lastActivityAt }),
|
|
1090
|
+
...(current?.busy === undefined ? {} : { busy: current.busy }),
|
|
1091
|
+
...(event.reason === undefined ? {} : { reason: event.reason }),
|
|
1092
|
+
...(nextRolloverAt === undefined ? {} : { nextRolloverAt }),
|
|
1093
|
+
};
|
|
1094
|
+
if (event.kind === "evicted") {
|
|
1095
|
+
this.logger?.info?.("Provider session evicted.", {
|
|
1096
|
+
conversationId: event.conversationId,
|
|
1097
|
+
providerSessionId: event.providerSessionId,
|
|
1098
|
+
reason: event.reason,
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
void this.refreshTraceSource(`session-${event.kind}`).catch(() => undefined);
|
|
1102
|
+
}
|
|
578
1103
|
traceMetadata(reason) {
|
|
579
1104
|
const channels = {};
|
|
580
1105
|
for (const driver of this.drivers) {
|
|
@@ -606,10 +1131,96 @@ class MonoAgentAppController {
|
|
|
606
1131
|
},
|
|
607
1132
|
}
|
|
608
1133
|
: {}),
|
|
1134
|
+
sandbox: {
|
|
1135
|
+
configured: this.sandboxStatusValue.configured,
|
|
1136
|
+
configuredMode: this.sandboxStatusValue.configuredMode,
|
|
1137
|
+
effective: this.sandboxStatusValue.effective,
|
|
1138
|
+
engine: this.sandboxStatusValue.engine,
|
|
1139
|
+
engineAvailable: this.sandboxStatusValue.engineAvailable,
|
|
1140
|
+
fallback: this.sandboxStatusValue.fallback,
|
|
1141
|
+
fallbackActive: this.sandboxStatusValue.fallbackActive,
|
|
1142
|
+
unsafeAllowHostProcess: this.sandboxStatusValue.unsafeAllowHostProcess,
|
|
1143
|
+
detail: this.sandboxStatusValue.detail,
|
|
1144
|
+
...(this.sandboxStatusValue.warning === undefined ? {} : { warning: this.sandboxStatusValue.warning }),
|
|
1145
|
+
...(this.sandboxStatusValue.resolutionError === undefined
|
|
1146
|
+
? {}
|
|
1147
|
+
: { resolutionError: this.sandboxStatusValue.resolutionError }),
|
|
1148
|
+
},
|
|
1149
|
+
...(this.selectedSkillsValue === undefined
|
|
1150
|
+
? {}
|
|
1151
|
+
: {
|
|
1152
|
+
context: {
|
|
1153
|
+
selectedSkills: [...this.selectedSkillsValue],
|
|
1154
|
+
},
|
|
1155
|
+
}),
|
|
1156
|
+
...(this.sessionMetadataValue === undefined ? {} : { session: this.sessionMetadataValue }),
|
|
609
1157
|
channels,
|
|
610
1158
|
};
|
|
611
1159
|
}
|
|
612
1160
|
}
|
|
1161
|
+
function nextDailyRolloverAt(now, timezone) {
|
|
1162
|
+
if (timezone === undefined || timezone.trim().length === 0) {
|
|
1163
|
+
const next = new Date(now);
|
|
1164
|
+
next.setHours(24, 0, 0, 0);
|
|
1165
|
+
return next.toISOString();
|
|
1166
|
+
}
|
|
1167
|
+
try {
|
|
1168
|
+
const parts = datePartsInTimeZone(now, timezone);
|
|
1169
|
+
return new Date(zonedDateTimeToUtcMs({
|
|
1170
|
+
year: parts.year,
|
|
1171
|
+
month: parts.month,
|
|
1172
|
+
day: parts.day + 1,
|
|
1173
|
+
hour: 0,
|
|
1174
|
+
minute: 0,
|
|
1175
|
+
second: 0,
|
|
1176
|
+
}, timezone)).toISOString();
|
|
1177
|
+
}
|
|
1178
|
+
catch {
|
|
1179
|
+
const next = new Date(now);
|
|
1180
|
+
next.setHours(24, 0, 0, 0);
|
|
1181
|
+
return next.toISOString();
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
function zonedDateTimeToUtcMs(parts, timezone) {
|
|
1185
|
+
const targetAsUtc = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
|
|
1186
|
+
let utcMs = targetAsUtc - timeZoneOffsetMs(new Date(targetAsUtc), timezone);
|
|
1187
|
+
utcMs = targetAsUtc - timeZoneOffsetMs(new Date(utcMs), timezone);
|
|
1188
|
+
return utcMs;
|
|
1189
|
+
}
|
|
1190
|
+
function timeZoneOffsetMs(date, timezone) {
|
|
1191
|
+
const parts = datePartsInTimeZone(date, timezone);
|
|
1192
|
+
const asUtc = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
|
|
1193
|
+
return asUtc - date.getTime();
|
|
1194
|
+
}
|
|
1195
|
+
function datePartsInTimeZone(date, timezone) {
|
|
1196
|
+
const formatter = new Intl.DateTimeFormat("en-CA", {
|
|
1197
|
+
timeZone: timezone,
|
|
1198
|
+
year: "numeric",
|
|
1199
|
+
month: "2-digit",
|
|
1200
|
+
day: "2-digit",
|
|
1201
|
+
hour: "2-digit",
|
|
1202
|
+
minute: "2-digit",
|
|
1203
|
+
second: "2-digit",
|
|
1204
|
+
hourCycle: "h23",
|
|
1205
|
+
});
|
|
1206
|
+
const entries = new Map(formatter.formatToParts(date).map((part) => [part.type, part.value]));
|
|
1207
|
+
return {
|
|
1208
|
+
year: Number(entries.get("year")),
|
|
1209
|
+
month: Number(entries.get("month")),
|
|
1210
|
+
day: Number(entries.get("day")),
|
|
1211
|
+
hour: Number(entries.get("hour")),
|
|
1212
|
+
minute: Number(entries.get("minute")),
|
|
1213
|
+
second: Number(entries.get("second")),
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
function sandboxStatusFromState(state) {
|
|
1217
|
+
const warning = sandboxEffectiveStateWarning(state);
|
|
1218
|
+
return {
|
|
1219
|
+
...state,
|
|
1220
|
+
detail: describeSandboxEffectiveState(state),
|
|
1221
|
+
...(warning === undefined ? {} : { warning }),
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
613
1224
|
function reasonOf(error) {
|
|
614
1225
|
return error instanceof Error ? error.message : String(error);
|
|
615
1226
|
}
|