@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/app.js
CHANGED
|
@@ -1,104 +1,117 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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";
|
|
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";
|
|
8
18
|
/**
|
|
9
|
-
* Starts a config-first mono-agent host in `cwd`:
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
19
|
+
* Starts a config-first mono-agent host in `cwd`: traceability first, then every
|
|
20
|
+
* configured channel in parallel. Channels with incomplete config report
|
|
21
|
+
* `waiting_for_config` instead of blocking the rest. The host runs headless;
|
|
22
|
+
* config changes take effect on the next restart.
|
|
13
23
|
*/
|
|
14
24
|
export async function startMonoAgentApp(options = {}) {
|
|
15
25
|
const cwd = resolve(options.cwd ?? process.cwd());
|
|
16
26
|
const configPath = resolve(cwd, options.configPath ?? "mono-agent.config.json");
|
|
17
27
|
const env = options.env ?? process.env;
|
|
18
|
-
const drivers = options.drivers ??
|
|
19
|
-
const
|
|
20
|
-
let controller;
|
|
21
|
-
let consoleServer;
|
|
22
|
-
const consoleSettings = await resolveAppConsoleSettings(input);
|
|
23
|
-
const consolePort = options.operatorConsolePort ?? consoleSettings.port;
|
|
24
|
-
if (options.operatorConsole !== false && consoleSettings.enabled) {
|
|
25
|
-
const consoleFactory = options.operatorConsoleFactory ?? startOperatorConsole;
|
|
26
|
-
consoleServer = await consoleFactory({
|
|
27
|
-
configPath,
|
|
28
|
-
cwd,
|
|
29
|
-
fieldGroups: options.fieldGroups ?? MONO_AGENT_APP_FIELD_GROUPS,
|
|
30
|
-
observability: {
|
|
31
|
-
artifactDir: () => resolveAppArtifactDir(input),
|
|
32
|
-
maxRuns: 100,
|
|
33
|
-
maxEventsPerRun: 750,
|
|
34
|
-
},
|
|
35
|
-
traceability: {
|
|
36
|
-
registryDir: () => resolveAppTraceRegistryDir(input),
|
|
37
|
-
staleAfterMs: () => resolveAppTraceStaleAfterMs(input),
|
|
38
|
-
maxRuns: 100,
|
|
39
|
-
maxEventsPerRun: 750,
|
|
40
|
-
},
|
|
41
|
-
applyConfigWrite: async () => {
|
|
42
|
-
if (controller === undefined) {
|
|
43
|
-
return {
|
|
44
|
-
kind: "failed",
|
|
45
|
-
message: "Mono agent app lifecycle is not ready to apply config changes.",
|
|
46
|
-
transports: [],
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
return await controller.applyConfigChange("operator-console-write");
|
|
50
|
-
},
|
|
51
|
-
...(consolePort === undefined ? {} : { port: consolePort }),
|
|
52
|
-
log: (event) => {
|
|
53
|
-
logOperatorConsoleEvent(options.logger, event);
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
controller = new MonoAgentAppController({
|
|
58
|
-
consoleServer,
|
|
28
|
+
const drivers = options.drivers ?? await resolveChannelDrivers({ env, cwd, configPath });
|
|
29
|
+
const controller = new MonoAgentAppController({
|
|
59
30
|
cwd,
|
|
60
31
|
configPath,
|
|
61
32
|
env,
|
|
62
33
|
drivers,
|
|
63
34
|
...(options.logger === undefined ? {} : { logger: options.logger }),
|
|
64
35
|
...(options.runtime === undefined ? {} : { runtime: options.runtime }),
|
|
36
|
+
...(options.sandboxEngine === undefined ? {} : { sandboxEngine: options.sandboxEngine }),
|
|
65
37
|
...(options.traceDefaults === undefined ? {} : { traceDefaults: options.traceDefaults }),
|
|
66
38
|
});
|
|
39
|
+
await controller.refreshSandboxStatus("startup");
|
|
67
40
|
await controller.startTraceability("startup");
|
|
41
|
+
await controller.startExporters("startup");
|
|
68
42
|
await Promise.all(drivers.map((driver) => controller?.startChannelIfConfigured(driver.id, "startup")));
|
|
69
43
|
await controller.startMemoryRitualsIfConfigured("startup");
|
|
70
44
|
await controller.refreshTraceSource("startup-complete");
|
|
71
45
|
return controller;
|
|
72
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
|
+
});
|
|
73
57
|
class MonoAgentAppController {
|
|
74
|
-
operatorConsole;
|
|
75
58
|
configPath;
|
|
76
|
-
consoleServer;
|
|
77
59
|
cwd;
|
|
78
60
|
env;
|
|
79
61
|
drivers;
|
|
80
62
|
driversById;
|
|
81
63
|
logger;
|
|
82
64
|
runtime;
|
|
65
|
+
sandboxEngine;
|
|
83
66
|
traceDefaults;
|
|
84
67
|
activeRuntimes = [];
|
|
85
68
|
statuses = new Map();
|
|
86
69
|
running = new Map();
|
|
87
70
|
startsInFlight = new Map();
|
|
71
|
+
/** Captured at construction (~process start): the cutoff for reclaiming orphaned "running" runs. */
|
|
72
|
+
processStartMs = Date.now();
|
|
73
|
+
staleRunsReconciled = false;
|
|
88
74
|
traceabilityStatusValue = {
|
|
89
75
|
kind: "disabled",
|
|
90
76
|
reason: "Traceability has not started yet.",
|
|
91
77
|
};
|
|
78
|
+
exporterStatusValue = {
|
|
79
|
+
kind: "disabled",
|
|
80
|
+
reason: "No observability exporter configured.",
|
|
81
|
+
};
|
|
82
|
+
sandboxStatusValue = DEFAULT_SANDBOX_STATUS;
|
|
83
|
+
selectedSkillsValue;
|
|
84
|
+
sessionMetadataValue;
|
|
85
|
+
/** The exporter the responder threads into agent-host (first configured exporter). */
|
|
86
|
+
resolvedExporter;
|
|
92
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;
|
|
93
96
|
memoryRituals;
|
|
94
97
|
// One shared memory store across all channel responders + the ritual scheduler, so there is a single
|
|
95
98
|
// memory.db handle (not one per channel plus one for rituals). Rebuilt on config reload, closed on stop.
|
|
96
99
|
sharedMemory = undefined;
|
|
97
100
|
sharedMemoryBuilt = false;
|
|
101
|
+
sharedMemoryBuild;
|
|
98
102
|
configApplyTail = Promise.resolve();
|
|
99
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();
|
|
100
114
|
constructor(input) {
|
|
101
|
-
this.consoleServer = input.consoleServer;
|
|
102
115
|
this.cwd = input.cwd;
|
|
103
116
|
this.configPath = input.configPath;
|
|
104
117
|
this.env = input.env;
|
|
@@ -106,6 +119,7 @@ class MonoAgentAppController {
|
|
|
106
119
|
this.driversById = new Map(input.drivers.map((driver) => [driver.id, driver]));
|
|
107
120
|
this.logger = input.logger;
|
|
108
121
|
this.runtime = input.runtime;
|
|
122
|
+
this.sandboxEngine = input.sandboxEngine;
|
|
109
123
|
this.traceDefaults = input.traceDefaults;
|
|
110
124
|
for (const driver of input.drivers) {
|
|
111
125
|
this.statuses.set(driver.id, {
|
|
@@ -113,18 +127,19 @@ class MonoAgentAppController {
|
|
|
113
127
|
reason: `${driver.label} has not been configured yet.`,
|
|
114
128
|
});
|
|
115
129
|
}
|
|
116
|
-
this.operatorConsole = input.consoleServer === undefined
|
|
117
|
-
? undefined
|
|
118
|
-
: {
|
|
119
|
-
url: input.consoleServer.url,
|
|
120
|
-
appUrl: `${input.consoleServer.url}/?t=${input.consoleServer.token}`,
|
|
121
|
-
token: input.consoleServer.token,
|
|
122
|
-
configPath: input.configPath,
|
|
123
|
-
};
|
|
124
130
|
}
|
|
125
131
|
get traceabilityStatus() {
|
|
126
132
|
return this.traceabilityStatusValue;
|
|
127
133
|
}
|
|
134
|
+
get exporterStatus() {
|
|
135
|
+
return this.exporterStatusValue;
|
|
136
|
+
}
|
|
137
|
+
get sandboxStatus() {
|
|
138
|
+
return this.sandboxStatusValue;
|
|
139
|
+
}
|
|
140
|
+
get selectedSkills() {
|
|
141
|
+
return this.selectedSkillsValue;
|
|
142
|
+
}
|
|
128
143
|
channelStatus(id) {
|
|
129
144
|
const status = this.statuses.get(id);
|
|
130
145
|
if (status === undefined) {
|
|
@@ -148,9 +163,12 @@ class MonoAgentAppController {
|
|
|
148
163
|
await this.stopChannel(driver.id, `${reason}:reload`);
|
|
149
164
|
}
|
|
150
165
|
this.stopMemoryRituals();
|
|
166
|
+
this.stopArtifactRetentionScheduler();
|
|
151
167
|
await this.resetSharedMemory();
|
|
152
168
|
await this.stopTraceSource(`${reason}:reload`);
|
|
169
|
+
await this.refreshSandboxStatus(reason);
|
|
153
170
|
await this.startTraceability(reason);
|
|
171
|
+
await this.startExporters(reason);
|
|
154
172
|
await Promise.all(this.drivers.map((driver) => this.startChannelIfConfigured(driver.id, reason)));
|
|
155
173
|
await this.startMemoryRitualsIfConfigured(reason);
|
|
156
174
|
await this.refreshTraceSource(`${reason}:complete`);
|
|
@@ -166,11 +184,6 @@ class MonoAgentAppController {
|
|
|
166
184
|
return this.channelStatus(id);
|
|
167
185
|
}
|
|
168
186
|
if (this.running.has(id)) {
|
|
169
|
-
if (reason === "operator-console-write") {
|
|
170
|
-
this.logger?.info?.(`${driver.label} is already running; restart the app to apply later config changes.`, {
|
|
171
|
-
status: "running",
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
187
|
await this.refreshTraceSource(reason);
|
|
175
188
|
return this.channelStatus(id);
|
|
176
189
|
}
|
|
@@ -190,16 +203,20 @@ class MonoAgentAppController {
|
|
|
190
203
|
if (this.stopped) {
|
|
191
204
|
return this.traceabilityStatusValue;
|
|
192
205
|
}
|
|
206
|
+
let artifactDirForRetention;
|
|
193
207
|
try {
|
|
194
208
|
const input = { env: this.env, cwd: this.cwd, configPath: this.configPath };
|
|
195
|
-
|
|
209
|
+
await this.refreshSelectedSkillsSnapshot(reason);
|
|
210
|
+
const [registryDir, artifactDir, sourceId, label, heartbeatMs, globalDiscovery] = await Promise.all([
|
|
196
211
|
resolveAppTraceRegistryDir(input),
|
|
197
212
|
resolveAppArtifactDir(input),
|
|
198
213
|
resolveAppTraceSourceId(input, this.traceDefaults),
|
|
199
214
|
resolveAppTraceSourceLabel(input, this.traceDefaults),
|
|
200
215
|
resolveAppTraceHeartbeatMs(input),
|
|
216
|
+
resolveAppTraceGlobalDiscovery(input),
|
|
201
217
|
]);
|
|
202
|
-
|
|
218
|
+
artifactDirForRetention = artifactDir;
|
|
219
|
+
const registerOptions = {
|
|
203
220
|
registryDir,
|
|
204
221
|
sourceId,
|
|
205
222
|
label,
|
|
@@ -209,30 +226,238 @@ class MonoAgentAppController {
|
|
|
209
226
|
configPath: this.configPath,
|
|
210
227
|
metadata: this.traceMetadata(reason),
|
|
211
228
|
heartbeatMs,
|
|
212
|
-
}
|
|
229
|
+
};
|
|
230
|
+
this.traceSource = await registerTraceSource(registerOptions);
|
|
213
231
|
this.traceabilityStatusValue = { kind: "running", sourceId, registryDir, artifactDir };
|
|
214
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
|
+
}
|
|
215
253
|
}
|
|
216
254
|
catch (error) {
|
|
217
255
|
const failure = reasonOf(error);
|
|
218
256
|
this.traceabilityStatusValue = { kind: "failed", reason: failure };
|
|
219
257
|
this.logger?.error?.("Traceability source registration failed.", { reason: failure });
|
|
220
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
|
+
}
|
|
221
267
|
return this.traceabilityStatusValue;
|
|
222
268
|
}
|
|
223
|
-
async
|
|
224
|
-
|
|
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) {
|
|
225
304
|
return;
|
|
226
305
|
}
|
|
306
|
+
this.staleRunsReconciled = true;
|
|
227
307
|
try {
|
|
228
|
-
await
|
|
229
|
-
|
|
230
|
-
metadata: this.traceMetadata(reason),
|
|
308
|
+
const { reconciled, warnings } = await reconcileStaleRunArtifacts(artifactDir, {
|
|
309
|
+
startedBeforeMs: this.processStartMs,
|
|
231
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
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Resolve the configured observability exporter(s) and publish the export
|
|
327
|
+
* status. No reachability probe runs here — Phoenix may start after the agent,
|
|
328
|
+
* so an unreachable endpoint must not block startup (that probe runs in
|
|
329
|
+
* `validate`). A present-but-invalid exporter config surfaces as `failed`.
|
|
330
|
+
*/
|
|
331
|
+
async startExporters(reason) {
|
|
332
|
+
if (this.stopped) {
|
|
333
|
+
return this.exporterStatusValue;
|
|
334
|
+
}
|
|
335
|
+
const input = { env: this.env, cwd: this.cwd, configPath: this.configPath };
|
|
336
|
+
let exporters;
|
|
337
|
+
try {
|
|
338
|
+
exporters = await resolveAppObservabilityExporters(input);
|
|
232
339
|
}
|
|
233
340
|
catch (error) {
|
|
234
|
-
this.
|
|
341
|
+
this.resolvedExporter = undefined;
|
|
342
|
+
this.exporterStatusValue = { kind: "failed", reason: reasonOf(error) };
|
|
343
|
+
this.logger?.error?.("Observability exporter config is invalid.", { reason: reasonOf(error) });
|
|
344
|
+
return this.exporterStatusValue;
|
|
345
|
+
}
|
|
346
|
+
const exporter = exporters[0];
|
|
347
|
+
if (exporter === undefined) {
|
|
348
|
+
this.resolvedExporter = undefined;
|
|
349
|
+
this.exporterStatusValue = { kind: "disabled", reason: "No observability exporter configured." };
|
|
350
|
+
return this.exporterStatusValue;
|
|
351
|
+
}
|
|
352
|
+
this.resolvedExporter = exporter;
|
|
353
|
+
this.exporterStatusValue = {
|
|
354
|
+
kind: "configured",
|
|
355
|
+
endpoint: exporter.endpoint,
|
|
356
|
+
includeSensitiveData: exporter.includeSensitiveData ?? false,
|
|
357
|
+
};
|
|
358
|
+
this.logger?.info?.("Observability exporter configured.", {
|
|
359
|
+
reason,
|
|
360
|
+
endpoint: exporter.endpoint,
|
|
361
|
+
includeSensitiveData: exporter.includeSensitiveData ?? false,
|
|
362
|
+
});
|
|
363
|
+
return this.exporterStatusValue;
|
|
364
|
+
}
|
|
365
|
+
/** Record a best-effort export warning so `status` can surface it without failing the run. */
|
|
366
|
+
recordExporterWarning(warning) {
|
|
367
|
+
const current = this.exporterStatusValue;
|
|
368
|
+
if (current.kind !== "configured") {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const message = `${warning.phase}: ${warning.message}`;
|
|
372
|
+
// The "fail" phase fires only when export fails on the run-failure path;
|
|
373
|
+
// surface it as lastError so operators can tell it apart from a transient
|
|
374
|
+
// best-effort warning. The run outcome is unchanged either way.
|
|
375
|
+
this.exporterStatusValue =
|
|
376
|
+
warning.phase === "fail" ? { ...current, lastError: message } : { ...current, lastWarning: message };
|
|
377
|
+
this.logger?.warn?.("Observability export warning.", { phase: warning.phase, message: warning.message });
|
|
378
|
+
// Persist to the trace-source manifest so the detached `mono-agent status`
|
|
379
|
+
// (which reads the manifest, not this live object) can surface it too.
|
|
380
|
+
void this.refreshTraceSource("exporter-warning").catch(() => undefined);
|
|
381
|
+
}
|
|
382
|
+
async refreshTraceSource(reason) {
|
|
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,
|
|
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
|
+
}
|
|
235
458
|
}
|
|
459
|
+
this.interactionBridgeEnvKeys = [];
|
|
460
|
+
await bridge?.stop().catch(() => undefined);
|
|
236
461
|
}
|
|
237
462
|
async stop() {
|
|
238
463
|
if (this.stopped) {
|
|
@@ -242,10 +467,11 @@ class MonoAgentAppController {
|
|
|
242
467
|
for (const driver of this.drivers) {
|
|
243
468
|
await this.stopChannel(driver.id, "stop");
|
|
244
469
|
}
|
|
470
|
+
await this.stopInteractionBridge();
|
|
245
471
|
this.stopMemoryRituals();
|
|
472
|
+
this.stopArtifactRetentionScheduler();
|
|
246
473
|
await this.resetSharedMemory();
|
|
247
474
|
await this.stopTraceSource("stop");
|
|
248
|
-
await this.consoleServer?.stop();
|
|
249
475
|
for (const runtime of this.activeRuntimes.splice(0)) {
|
|
250
476
|
await runtime.disposeAllSessions?.().catch(() => undefined);
|
|
251
477
|
}
|
|
@@ -258,22 +484,30 @@ class MonoAgentAppController {
|
|
|
258
484
|
try {
|
|
259
485
|
const input = { env: this.env, cwd: this.cwd, configPath: this.configPath };
|
|
260
486
|
coreConfig = await loadAppCoreConfig(input);
|
|
487
|
+
this.rememberSelectedSkills(coreConfig);
|
|
261
488
|
}
|
|
262
489
|
catch {
|
|
263
|
-
// Config not ready yet —
|
|
490
|
+
// Config not ready yet — consolidation will start on the next applyConfigChange.
|
|
264
491
|
return;
|
|
265
492
|
}
|
|
266
493
|
if (coreConfig.memory?.mode !== "bujo") {
|
|
267
494
|
return;
|
|
268
495
|
}
|
|
269
|
-
const
|
|
270
|
-
|
|
496
|
+
const runtime = this.runtime ?? createConfiguredAgentRuntime({
|
|
497
|
+
config: coreConfig,
|
|
498
|
+
...(this.sandboxEngine === undefined ? {} : { sandboxEngine: this.sandboxEngine }),
|
|
499
|
+
});
|
|
500
|
+
if (!this.activeRuntimes.includes(runtime)) {
|
|
501
|
+
this.activeRuntimes.push(runtime);
|
|
502
|
+
}
|
|
503
|
+
const store = await this.memoryStore(coreConfig);
|
|
504
|
+
// Duck-type: only bujo-tier BujoMemoryStore has consolidate().
|
|
271
505
|
// Cast through unknown to bypass the MemoryStore contract's type mismatch.
|
|
272
506
|
const storeAsAny = store;
|
|
273
507
|
if (store === undefined ||
|
|
274
|
-
typeof storeAsAny["
|
|
275
|
-
typeof storeAsAny["migrate"] !== "function" ||
|
|
508
|
+
typeof storeAsAny["consolidate"] !== "function" ||
|
|
276
509
|
typeof storeAsAny["tier"] !== "function") {
|
|
510
|
+
this.logger?.info?.("Memory consolidation scheduler skipped — store does not support consolidate().", { reason });
|
|
277
511
|
return;
|
|
278
512
|
}
|
|
279
513
|
const bujoStore = store;
|
|
@@ -282,13 +516,12 @@ class MonoAgentAppController {
|
|
|
282
516
|
// scheduler started in that case — log an accurate skip instead.
|
|
283
517
|
const tier = bujoStore.tier();
|
|
284
518
|
if (tier !== "bujo") {
|
|
285
|
-
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 });
|
|
286
520
|
return;
|
|
287
521
|
}
|
|
288
522
|
this.memoryRituals = startMemoryRituals({
|
|
289
523
|
store: bujoStore,
|
|
290
|
-
...(coreConfig.memory.
|
|
291
|
-
...(coreConfig.memory.migration !== undefined && { migration: coreConfig.memory.migration }),
|
|
524
|
+
...(coreConfig.memory.consolidation !== undefined && { consolidation: coreConfig.memory.consolidation }),
|
|
292
525
|
...(this.logger !== undefined && {
|
|
293
526
|
logger: {
|
|
294
527
|
info: (m) => this.logger?.info?.(m),
|
|
@@ -296,7 +529,7 @@ class MonoAgentAppController {
|
|
|
296
529
|
},
|
|
297
530
|
}),
|
|
298
531
|
});
|
|
299
|
-
this.logger?.info?.("Memory
|
|
532
|
+
this.logger?.info?.("Memory consolidation scheduler started.", { reason, mode: "bujo" });
|
|
300
533
|
}
|
|
301
534
|
stopMemoryRituals() {
|
|
302
535
|
const rituals = this.memoryRituals;
|
|
@@ -305,7 +538,94 @@ class MonoAgentAppController {
|
|
|
305
538
|
}
|
|
306
539
|
this.memoryRituals = undefined;
|
|
307
540
|
rituals.stop();
|
|
308
|
-
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
|
+
});
|
|
309
629
|
}
|
|
310
630
|
async startChannel(driver, reason) {
|
|
311
631
|
const input = { env: this.env, cwd: this.cwd, configPath: this.configPath };
|
|
@@ -327,9 +647,16 @@ class MonoAgentAppController {
|
|
|
327
647
|
if (waitingReason !== undefined) {
|
|
328
648
|
return this.setStatus(driver.id, { kind: "waiting_for_config", reason: waitingReason });
|
|
329
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
|
+
}
|
|
330
656
|
let coreConfig;
|
|
331
657
|
try {
|
|
332
658
|
coreConfig = await loadAppCoreConfig(input);
|
|
659
|
+
this.rememberSelectedSkills(coreConfig);
|
|
333
660
|
}
|
|
334
661
|
catch (error) {
|
|
335
662
|
if (isAppCoreConfigError(error)) {
|
|
@@ -338,21 +665,82 @@ class MonoAgentAppController {
|
|
|
338
665
|
}
|
|
339
666
|
throw error;
|
|
340
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);
|
|
341
675
|
try {
|
|
342
|
-
const responder = this.buildResponder(coreConfig);
|
|
676
|
+
const responder = await this.buildResponder(coreConfig);
|
|
677
|
+
// The AgentResponder contract has no dispose(), but the configured responder
|
|
678
|
+
// (createAgentResponder) exposes one that tears down the harness + live-session
|
|
679
|
+
// manager. Read it LAZILY (at teardown time) off the `responder` so both the
|
|
680
|
+
// normal stop/reload path AND the onFailure (transport-death) path retire the
|
|
681
|
+
// per-channel harness rather than orphan it, and any wrapper applied to the
|
|
682
|
+
// responder is honored. Duck-typed, consistent with the resetSharedMemory /
|
|
683
|
+
// bujo-store patterns elsewhere in this file.
|
|
684
|
+
const disposeResponder = () => responder.dispose?.();
|
|
685
|
+
const hasDispose = typeof responder.dispose === "function";
|
|
343
686
|
const runningChannel = await driver.start({
|
|
344
687
|
config,
|
|
345
688
|
coreConfig,
|
|
346
689
|
responder,
|
|
347
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,
|
|
348
696
|
...(this.logger === undefined ? {} : { logger: this.logger }),
|
|
349
697
|
onFailure: (failureReason) => {
|
|
350
698
|
this.running.delete(driver.id);
|
|
351
699
|
this.setStatus(driver.id, { kind: "failed", reason: failureReason });
|
|
352
700
|
this.logger?.error?.(`${driver.label} channel stopped with an error.`, { reason: failureReason });
|
|
701
|
+
// The running-channel entry (which holds the stop/reload dispose handle)
|
|
702
|
+
// was just deleted, so dispose the responder here too — otherwise a
|
|
703
|
+
// transport death orphans the per-channel harness/live-session manager
|
|
704
|
+
// (the stop/reload path early-returns on the now-missing entry).
|
|
705
|
+
void disposeResponder()?.catch((error) => {
|
|
706
|
+
this.logger?.warn?.(`${driver.label} responder did not dispose cleanly after failure.`, {
|
|
707
|
+
reason: failureReason,
|
|
708
|
+
error: reasonOf(error),
|
|
709
|
+
});
|
|
710
|
+
});
|
|
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.`, {});
|
|
353
737
|
},
|
|
354
738
|
});
|
|
355
|
-
this.running.set(driver.id,
|
|
739
|
+
this.running.set(driver.id, {
|
|
740
|
+
...runningChannel,
|
|
741
|
+
stop: () => runningChannel.stop(),
|
|
742
|
+
...(hasDispose ? { dispose: () => disposeResponder() ?? Promise.resolve() } : {}),
|
|
743
|
+
});
|
|
356
744
|
const status = this.setStatus(driver.id, { kind: "running", summary: runningChannel.summary });
|
|
357
745
|
this.logger?.info?.(`${driver.label} channel is running.`, { reason, ...runningChannel.summary });
|
|
358
746
|
return status;
|
|
@@ -373,6 +761,12 @@ class MonoAgentAppController {
|
|
|
373
761
|
await runningChannel.stop().catch((error) => {
|
|
374
762
|
this.logger?.warn?.(`${driver.label} channel did not stop cleanly.`, { reason, error: reasonOf(error) });
|
|
375
763
|
});
|
|
764
|
+
// Stop the transport first (so no new turns arrive), then dispose the responder
|
|
765
|
+
// so the harness/live-session manager and warm provider sessions are retired
|
|
766
|
+
// rather than lingering against stale config across a reload.
|
|
767
|
+
await runningChannel.dispose?.().catch((error) => {
|
|
768
|
+
this.logger?.warn?.(`${driver.label} responder did not dispose cleanly.`, { reason, error: reasonOf(error) });
|
|
769
|
+
});
|
|
376
770
|
if (!this.stopped) {
|
|
377
771
|
this.setStatus(id, {
|
|
378
772
|
kind: "waiting_for_config",
|
|
@@ -380,35 +774,210 @@ class MonoAgentAppController {
|
|
|
380
774
|
});
|
|
381
775
|
}
|
|
382
776
|
}
|
|
383
|
-
buildResponder(coreConfig) {
|
|
777
|
+
async buildResponder(coreConfig) {
|
|
384
778
|
const runtime = this.runtime ?? createConfiguredAgentRuntime(coreConfig);
|
|
385
779
|
if (!this.activeRuntimes.includes(runtime)) {
|
|
386
780
|
this.activeRuntimes.push(runtime);
|
|
387
781
|
}
|
|
388
|
-
const memory = this.memoryStore(coreConfig);
|
|
389
|
-
|
|
782
|
+
const memory = await this.memoryStore(coreConfig);
|
|
783
|
+
const memoryRecall = this.memoryRecallRuntimeOptions(coreConfig);
|
|
784
|
+
const supermemoryMcp = this.supermemoryMcpRuntimeOptions(coreConfig);
|
|
785
|
+
const adapterSendTools = await this.adapterSendToolsRuntimeOptions(coreConfig);
|
|
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;
|
|
803
|
+
const observabilityContext = await this.observabilityContext();
|
|
804
|
+
const responder = await createConfiguredAgentResponder({
|
|
390
805
|
config: coreConfig,
|
|
391
806
|
runtime,
|
|
807
|
+
...(runtimeForModel === undefined ? {} : { runtimeForModel }),
|
|
808
|
+
...(this.sandboxEngine === undefined ? {} : { sandboxEngine: this.sandboxEngine }),
|
|
392
809
|
...(memory !== undefined && { memory }),
|
|
810
|
+
...(runtimeOptionsForRequest === undefined ? {} : { runtimeOptionsForRequest }),
|
|
811
|
+
// Thread run-identifying context onto exported spans and surface per-run
|
|
812
|
+
// export warnings to `exporterStatus` (agent-host only builds the exporter
|
|
813
|
+
// when config.observability.exporters is non-empty).
|
|
814
|
+
observabilityContext,
|
|
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,
|
|
820
|
+
});
|
|
821
|
+
return responder;
|
|
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
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Run-identifying context threaded onto exported spans (Phoenix shows the same
|
|
866
|
+
* source/run identifiers as the local trace-source registry, so local artifact
|
|
867
|
+
* lookup stays possible). Resolved with the same source-id/label resolvers the
|
|
868
|
+
* trace source uses.
|
|
869
|
+
*/
|
|
870
|
+
async observabilityContext() {
|
|
871
|
+
const input = { env: this.env, cwd: this.cwd, configPath: this.configPath };
|
|
872
|
+
const [sourceId, sourceLabel] = await Promise.all([
|
|
873
|
+
resolveAppTraceSourceId(input, this.traceDefaults),
|
|
874
|
+
resolveAppTraceSourceLabel(input, this.traceDefaults),
|
|
875
|
+
]);
|
|
876
|
+
return { sourceId, sourceLabel, configPath: this.configPath };
|
|
877
|
+
}
|
|
878
|
+
memoryRecallRuntimeOptions(coreConfig) {
|
|
879
|
+
const settings = resolveMemoryRecallSettings(coreConfig);
|
|
880
|
+
if (settings === undefined) {
|
|
881
|
+
return undefined;
|
|
882
|
+
}
|
|
883
|
+
this.logger?.info?.("Read-only memory_recall tool enabled.", {
|
|
884
|
+
provider: "supermemory" in settings ? "supermemory" : settings.embeddings?.provider ?? "fts-only",
|
|
885
|
+
});
|
|
886
|
+
return createMemoryRecallRuntimeExtension(settings, this.cwd);
|
|
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
|
+
}
|
|
915
|
+
async adapterSendToolsRuntimeOptions(coreConfig) {
|
|
916
|
+
const input = { env: this.env, cwd: this.cwd, configPath: this.configPath };
|
|
917
|
+
const settings = await resolveAdapterSendToolsSettings(input, {
|
|
918
|
+
allowedTools: coreConfig.tools.allowedTools,
|
|
919
|
+
disallowedTools: coreConfig.tools.disallowedTools,
|
|
920
|
+
logger: this.logger,
|
|
393
921
|
});
|
|
922
|
+
if (settings === undefined) {
|
|
923
|
+
return undefined;
|
|
924
|
+
}
|
|
925
|
+
const toolNames = adapterSendToolNames(settings);
|
|
926
|
+
this.logger?.info?.("Adapter send tools enabled.", { tools: toolNames });
|
|
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);
|
|
394
931
|
}
|
|
395
932
|
/** Build the configured memory store once and share it across responders + the ritual scheduler. */
|
|
396
|
-
memoryStore(coreConfig) {
|
|
397
|
-
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 () => {
|
|
398
941
|
const appLogger = this.logger;
|
|
399
942
|
const logger = appLogger?.warn !== undefined
|
|
400
943
|
? { warn: (message) => { appLogger.warn?.(message); } }
|
|
401
944
|
: undefined;
|
|
402
|
-
|
|
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
|
+
});
|
|
403
965
|
this.sharedMemoryBuilt = true;
|
|
966
|
+
return this.sharedMemory;
|
|
967
|
+
})();
|
|
968
|
+
try {
|
|
969
|
+
return await this.sharedMemoryBuild;
|
|
970
|
+
}
|
|
971
|
+
finally {
|
|
972
|
+
this.sharedMemoryBuild = undefined;
|
|
404
973
|
}
|
|
405
|
-
return this.sharedMemory;
|
|
406
974
|
}
|
|
407
975
|
/** Close + clear the shared memory store (on config reload or stop) so the next build is fresh. */
|
|
408
976
|
async resetSharedMemory() {
|
|
409
977
|
const mem = this.sharedMemory;
|
|
410
978
|
this.sharedMemory = undefined;
|
|
411
979
|
this.sharedMemoryBuilt = false;
|
|
980
|
+
this.sharedMemoryBuild = undefined;
|
|
412
981
|
if (mem?.flush !== undefined) {
|
|
413
982
|
await Promise.resolve(mem.flush()).catch(() => undefined);
|
|
414
983
|
}
|
|
@@ -427,7 +996,8 @@ class MonoAgentAppController {
|
|
|
427
996
|
}
|
|
428
997
|
applyResult() {
|
|
429
998
|
const transports = this.activeTransports();
|
|
430
|
-
const
|
|
999
|
+
const statusEntries = [...this.statuses.entries()];
|
|
1000
|
+
const statuses = statusEntries.map(([, status]) => status);
|
|
431
1001
|
const failedChannel = statuses.find((status) => status.kind === "failed");
|
|
432
1002
|
const failure = failedChannel?.kind === "failed"
|
|
433
1003
|
? failedChannel.reason
|
|
@@ -441,8 +1011,10 @@ class MonoAgentAppController {
|
|
|
441
1011
|
transports,
|
|
442
1012
|
};
|
|
443
1013
|
}
|
|
444
|
-
|
|
445
|
-
|
|
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")) {
|
|
446
1018
|
return {
|
|
447
1019
|
kind: "waiting_for_config",
|
|
448
1020
|
message: "Saved config, but no agent channel is running yet.",
|
|
@@ -457,32 +1029,77 @@ class MonoAgentAppController {
|
|
|
457
1029
|
}
|
|
458
1030
|
async stopTraceSource(reason) {
|
|
459
1031
|
const traceSource = this.traceSource;
|
|
460
|
-
|
|
1032
|
+
const globalTraceSource = this.globalTraceSource;
|
|
1033
|
+
if (traceSource === undefined && globalTraceSource === undefined) {
|
|
461
1034
|
return;
|
|
462
1035
|
}
|
|
463
1036
|
this.traceSource = undefined;
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
}).catch((error) => {
|
|
1037
|
+
this.globalTraceSource = undefined;
|
|
1038
|
+
const patch = { metadata: this.traceMetadata(reason), transports: this.activeTransports() };
|
|
1039
|
+
await traceSource?.stop(patch).catch((error) => {
|
|
468
1040
|
this.logger?.warn?.("Traceability source stop update failed.", { reason: reasonOf(error) });
|
|
469
1041
|
});
|
|
1042
|
+
await globalTraceSource?.stop(patch).catch((error) => {
|
|
1043
|
+
this.logger?.warn?.("Global trace-source mirror stop update failed.", { reason: reasonOf(error) });
|
|
1044
|
+
});
|
|
470
1045
|
if (!this.stopped) {
|
|
471
1046
|
this.traceabilityStatusValue = { kind: "disabled", reason: "Traceability source stopped while applying config." };
|
|
472
1047
|
}
|
|
473
1048
|
}
|
|
474
1049
|
activeTransports() {
|
|
475
1050
|
const transports = [];
|
|
476
|
-
if (this.consoleServer !== undefined) {
|
|
477
|
-
transports.push("operator-console");
|
|
478
|
-
}
|
|
479
1051
|
for (const driver of this.drivers) {
|
|
480
|
-
|
|
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") {
|
|
481
1055
|
transports.push(driver.id);
|
|
482
1056
|
}
|
|
483
1057
|
}
|
|
484
1058
|
return transports;
|
|
485
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
|
+
}
|
|
486
1103
|
traceMetadata(reason) {
|
|
487
1104
|
const channels = {};
|
|
488
1105
|
for (const driver of this.drivers) {
|
|
@@ -496,29 +1113,182 @@ class MonoAgentAppController {
|
|
|
496
1113
|
}
|
|
497
1114
|
return {
|
|
498
1115
|
reason,
|
|
499
|
-
...(this.
|
|
1116
|
+
...(this.exporterStatusValue.kind === "configured"
|
|
1117
|
+
? {
|
|
1118
|
+
observability: {
|
|
1119
|
+
// Persist only the endpoint + warning/error strings (never headers
|
|
1120
|
+
// or secrets) so the detached `status` reader can surface exporter
|
|
1121
|
+
// state. JSONL artifacts always remain local.
|
|
1122
|
+
endpoint: this.exporterStatusValue.endpoint,
|
|
1123
|
+
includeSensitiveData: this.exporterStatusValue.includeSensitiveData,
|
|
1124
|
+
jsonlArtifactsLocal: true,
|
|
1125
|
+
...(this.exporterStatusValue.lastWarning === undefined
|
|
1126
|
+
? {}
|
|
1127
|
+
: { lastWarning: this.exporterStatusValue.lastWarning }),
|
|
1128
|
+
...(this.exporterStatusValue.lastError === undefined
|
|
1129
|
+
? {}
|
|
1130
|
+
: { lastError: this.exporterStatusValue.lastError }),
|
|
1131
|
+
},
|
|
1132
|
+
}
|
|
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
|
|
500
1150
|
? {}
|
|
501
1151
|
: {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
// access token. A detached `status`/`start` reader surfaces this
|
|
505
|
-
// URL and points at the logs for the tokenized link.
|
|
506
|
-
url: this.operatorConsole.url,
|
|
507
|
-
configPath: this.operatorConsole.configPath,
|
|
1152
|
+
context: {
|
|
1153
|
+
selectedSkills: [...this.selectedSkillsValue],
|
|
508
1154
|
},
|
|
509
1155
|
}),
|
|
1156
|
+
...(this.sessionMetadataValue === undefined ? {} : { session: this.sessionMetadataValue }),
|
|
510
1157
|
channels,
|
|
511
1158
|
};
|
|
512
1159
|
}
|
|
513
1160
|
}
|
|
514
|
-
function
|
|
515
|
-
if (
|
|
516
|
-
|
|
517
|
-
|
|
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();
|
|
518
1166
|
}
|
|
519
|
-
|
|
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
|
+
};
|
|
520
1223
|
}
|
|
521
1224
|
function reasonOf(error) {
|
|
522
1225
|
return error instanceof Error ? error.message : String(error);
|
|
523
1226
|
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Compose several per-request runtime-options extensions into one. Each extension is invoked per
|
|
1229
|
+
* request; mergeable runtime option maps/lists are unioned and their cleanups are chained. Returns
|
|
1230
|
+
* `undefined` when no extension is active, so the host omits `runtimeOptionsForRequest` entirely.
|
|
1231
|
+
*/
|
|
1232
|
+
function composeRuntimeOptionExtensions(extensions) {
|
|
1233
|
+
const active = extensions.filter((extension) => extension !== undefined);
|
|
1234
|
+
if (active.length === 0) {
|
|
1235
|
+
return undefined;
|
|
1236
|
+
}
|
|
1237
|
+
if (active.length === 1) {
|
|
1238
|
+
return active[0];
|
|
1239
|
+
}
|
|
1240
|
+
return async (input) => {
|
|
1241
|
+
const results = await Promise.all(active.map((extension) => extension(input)));
|
|
1242
|
+
const runtimeOptions = {};
|
|
1243
|
+
for (const result of results) {
|
|
1244
|
+
mergeRuntimeOptions(runtimeOptions, result.runtimeOptions);
|
|
1245
|
+
}
|
|
1246
|
+
return {
|
|
1247
|
+
runtimeOptions,
|
|
1248
|
+
cleanup: async () => {
|
|
1249
|
+
// Chain every cleanup; run them all even if one rejects so no extension leaks resources.
|
|
1250
|
+
await Promise.all(results.map(async (result) => result.cleanup?.()));
|
|
1251
|
+
},
|
|
1252
|
+
};
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
function mergeRuntimeOptions(target, next) {
|
|
1256
|
+
if (next === undefined) {
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
for (const [key, value] of Object.entries(next)) {
|
|
1260
|
+
if (value === undefined) {
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
if (key === "allowedTools" || key === "disallowedTools") {
|
|
1264
|
+
target[key] = mergeStringLists(target[key], value);
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1267
|
+
if (key === "mcpServers") {
|
|
1268
|
+
target[key] = {
|
|
1269
|
+
...(isRecord(target[key]) ? target[key] : {}),
|
|
1270
|
+
...(isRecord(value) ? value : {}),
|
|
1271
|
+
};
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
target[key] = value;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
function mergeStringLists(current, next) {
|
|
1278
|
+
const out = [];
|
|
1279
|
+
for (const list of [current, next]) {
|
|
1280
|
+
if (!Array.isArray(list)) {
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
1283
|
+
for (const item of list) {
|
|
1284
|
+
if (typeof item === "string" && !out.includes(item)) {
|
|
1285
|
+
out.push(item);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
return out;
|
|
1290
|
+
}
|
|
1291
|
+
function isRecord(value) {
|
|
1292
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1293
|
+
}
|
|
524
1294
|
//# sourceMappingURL=app.js.map
|