@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/doctor.js
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access, mkdir, readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { listRecordedRuns } from "@mono-agent/observability";
|
|
5
|
+
import { serializeTraceSpans } from "@mono-agent/observability/otel";
|
|
6
|
+
import { describeMonoRuntimeSupport, modelReferenceKey, parseMonoRuntimeModelReference } from "@mono-agent/runtime-adapter";
|
|
7
|
+
import { buildMonoAgentConfigView, findJsonSecretConfigWarnings, findRemovedConfigWarnings, readMonoAgentConfigJson, redactMonoAgentConfig, resolveSupermemoryContainer, } from "@mono-agent/config";
|
|
8
|
+
import { describeSandboxEffectiveState, resolveSandboxEffectiveState, sandboxEffectiveStateWarning, } from "@mono-agent/runtime-adapter";
|
|
9
|
+
import { describeSensitiveDataExportWarning, isAppCoreConfigError, loadAppCoreConfig, phoenixAppBaseUrl, resolveAppArtifactDir, resolveAppObservabilityExporters, } from "./app-config.js";
|
|
10
|
+
import { adapterSendToolNames, resolveAdapterSendToolsSettings } from "./adapter-send-tools.js";
|
|
11
|
+
import { collectChannelConfigViews } from "./channel-config-view.js";
|
|
12
|
+
import { resolveChannelDrivers } from "./channels.js";
|
|
13
|
+
import { findUnknownAppConfigWarnings } from "./config-reference.js";
|
|
14
|
+
import { buildRunsHealthDisplay, RUNS_HEALTH_MAX_RUNS } from "./runs-health.js";
|
|
6
15
|
/**
|
|
7
16
|
* Loads every config section the app would use at start and reports it
|
|
8
17
|
* per-section, so an engineer can see exactly what would run, wait, or fail —
|
|
@@ -10,7 +19,9 @@ import { defaultChannelDrivers } from "./channels.js";
|
|
|
10
19
|
*/
|
|
11
20
|
export async function validateMonoAgentFolder(options) {
|
|
12
21
|
const sections = [];
|
|
13
|
-
const drivers = options.drivers ??
|
|
22
|
+
const drivers = options.drivers ?? await resolveChannelDrivers(options);
|
|
23
|
+
const liveness = options.liveness ?? true;
|
|
24
|
+
const allowFilesystemWrites = options.allowFilesystemWrites ?? true;
|
|
14
25
|
let coreConfig;
|
|
15
26
|
try {
|
|
16
27
|
coreConfig = await loadAppCoreConfig(options);
|
|
@@ -23,13 +34,33 @@ export async function validateMonoAgentFolder(options) {
|
|
|
23
34
|
sections.push({ id: "core", label: "Core config", status: "error", details: [error.message] });
|
|
24
35
|
}
|
|
25
36
|
if (coreConfig !== undefined) {
|
|
37
|
+
const jsonResult = await readMonoAgentConfigJson(options.configPath);
|
|
38
|
+
// Channel secrets (bot tokens, API keys) live outside the core view, so the
|
|
39
|
+
// placement check spans both: core sections + every channel's config view.
|
|
40
|
+
const configWarnings = [
|
|
41
|
+
...findUnknownAppConfigWarnings(jsonResult.json),
|
|
42
|
+
...findJsonSecretConfigWarnings([
|
|
43
|
+
...buildMonoAgentConfigView({
|
|
44
|
+
redacted: redactMonoAgentConfig(coreConfig),
|
|
45
|
+
json: jsonResult.json,
|
|
46
|
+
env: options.env,
|
|
47
|
+
}),
|
|
48
|
+
...(await collectChannelConfigViews(drivers, options)),
|
|
49
|
+
]),
|
|
50
|
+
...findRemovedConfigWarnings({ json: jsonResult.json, env: options.env }),
|
|
51
|
+
];
|
|
52
|
+
if (configWarnings.length > 0) {
|
|
53
|
+
sections.push({ id: "secret-placement", label: "Config warnings", status: "waiting", details: configWarnings });
|
|
54
|
+
}
|
|
26
55
|
sections.push(runtimeSection(coreConfig));
|
|
56
|
+
sections.push(await credentialsSection(coreConfig));
|
|
27
57
|
sections.push(await contextSection(coreConfig));
|
|
28
|
-
sections.push(await memorySection(coreConfig));
|
|
29
|
-
sections.push(await toolsSection(coreConfig));
|
|
30
|
-
sections.push(sandboxSection(coreConfig));
|
|
58
|
+
sections.push(await memorySection(coreConfig, liveness, allowFilesystemWrites));
|
|
59
|
+
sections.push(await toolsSection(coreConfig, options));
|
|
60
|
+
sections.push(await sandboxSection(coreConfig, options.sandboxEngine));
|
|
31
61
|
}
|
|
32
|
-
sections.push(await
|
|
62
|
+
sections.push(await exporterSection(options, liveness));
|
|
63
|
+
sections.push(await runsSection(options, coreConfig));
|
|
33
64
|
for (const driver of drivers) {
|
|
34
65
|
sections.push(await channelSection(driver, options));
|
|
35
66
|
}
|
|
@@ -61,6 +92,105 @@ function runtimeSection(config) {
|
|
|
61
92
|
}
|
|
62
93
|
return { id: "runtime", label: "Runtime", status, details };
|
|
63
94
|
}
|
|
95
|
+
/** Parses the Pi OAuth auth store (provider -> credentials) at `path`, best-effort. */
|
|
96
|
+
async function readPiAuthProviders(path) {
|
|
97
|
+
try {
|
|
98
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
99
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
100
|
+
return parsed;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Missing/invalid auth file is reported per-provider below, not thrown here.
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
/** Provider ids configured via the sibling `models.json` (custom/local providers need no OAuth). */
|
|
109
|
+
async function readPiCustomProviders(authPath) {
|
|
110
|
+
try {
|
|
111
|
+
const parsed = JSON.parse(await readFile(join(dirname(authPath), "models.json"), "utf8"));
|
|
112
|
+
const providers = parsed?.providers;
|
|
113
|
+
if (providers !== null && typeof providers === "object" && !Array.isArray(providers)) {
|
|
114
|
+
return new Set(Object.keys(providers));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// No models.json is fine; such providers simply won't be marked custom.
|
|
119
|
+
}
|
|
120
|
+
return new Set();
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Checks that every referenced model (primary, fallbacks, and the agent-host memory LLM)
|
|
124
|
+
* has discoverable credentials, so a keyless or expired-OAuth provider is caught at
|
|
125
|
+
* `validate` time instead of degrading crons/memory silently at runtime (the failure mode
|
|
126
|
+
* that broke memory capture for ~10 days: the auth store's OAuth token had quietly expired).
|
|
127
|
+
*
|
|
128
|
+
* The check is intentionally STATIC and read-only — a validator must not mutate the auth
|
|
129
|
+
* store or hit the network. For each Pi provider it inspects the auth store (`piAuthPath`)
|
|
130
|
+
* and the sibling `models.json`: a custom/local provider needs no OAuth; an OAuth provider
|
|
131
|
+
* absent from the store, or whose access token has expired, is flagged `waiting` with a
|
|
132
|
+
* re-auth hint. SDK-authenticated models (claude/codex) are validated by their own SDK and
|
|
133
|
+
* are noted but not key-checked here. `waiting` (never `error`) keeps the verdict non-fatal,
|
|
134
|
+
* mirroring the Ollama/Phoenix probes — the goal is visibility, not blocking start.
|
|
135
|
+
*/
|
|
136
|
+
async function credentialsSection(config) {
|
|
137
|
+
const refs = [
|
|
138
|
+
{ label: "Primary", ref: config.runtime.model },
|
|
139
|
+
...(config.runtime.fallbackModels ?? []).map((ref) => ({ label: "Fallback", ref })),
|
|
140
|
+
];
|
|
141
|
+
if (config.memory?.llm !== undefined && config.memory.llm.provider !== "ollama") {
|
|
142
|
+
try {
|
|
143
|
+
refs.push({ label: "Memory LLM", ref: parseMonoRuntimeModelReference(config.memory.llm.model) });
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// A malformed memory model reference is surfaced by the memory/runtime shape checks.
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const piRefs = refs.filter((r) => r.ref.sdk === "pi" && typeof r.ref.provider === "string");
|
|
150
|
+
if (piRefs.length === 0) {
|
|
151
|
+
return {
|
|
152
|
+
id: "credentials",
|
|
153
|
+
label: "Provider credentials",
|
|
154
|
+
status: "disabled",
|
|
155
|
+
details: ["No Pi provider-key models referenced (SDK-authenticated models are checked by their SDK)."],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const authPath = config.providers?.piAuthPath;
|
|
159
|
+
const authProviders = authPath === undefined ? undefined : await readPiAuthProviders(authPath);
|
|
160
|
+
const customProviders = authPath === undefined ? new Set() : await readPiCustomProviders(authPath);
|
|
161
|
+
const now = Date.now();
|
|
162
|
+
const details = [];
|
|
163
|
+
let status = "ok";
|
|
164
|
+
if (authPath !== undefined) {
|
|
165
|
+
details.push(`Pi auth store: ${authPath}`);
|
|
166
|
+
}
|
|
167
|
+
for (const { label, ref } of piRefs) {
|
|
168
|
+
const provider = ref.provider;
|
|
169
|
+
const refStr = referenceOf(ref);
|
|
170
|
+
if (customProviders.has(provider)) {
|
|
171
|
+
details.push(`${label} ${refStr}: provider \`${provider}\` configured via pi models.json.`);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const entry = authProviders?.[provider];
|
|
175
|
+
if (entry === undefined) {
|
|
176
|
+
status = "waiting";
|
|
177
|
+
details.push(`[WARN] ${label} ${refStr}: no Pi credentials found for provider \`${provider}\` (absent from the auth store and models.json). Authenticate it (\`pi auth login ${provider}\`) or set providers.piAuthPath.`);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const isOAuth = entry.type === "oauth" || typeof entry.expires === "number";
|
|
181
|
+
const expired = typeof entry.expires === "number" && entry.expires < now;
|
|
182
|
+
const whenNote = typeof entry.expires === "number" ? ` ${new Date(entry.expires).toISOString()}` : "";
|
|
183
|
+
if (isOAuth && expired) {
|
|
184
|
+
status = "waiting";
|
|
185
|
+
details.push(`[WARN] ${label} ${refStr}: OAuth token for \`${provider}\` expired${whenNote} — the runtime auto-refreshes, but if runs fail with "No API key for provider: ${provider}" the refresh is dead; re-authenticate: \`pi auth login ${provider}\`.`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
details.push(isOAuth
|
|
189
|
+
? `${label} ${refStr}: OAuth credentials for \`${provider}\` present (token valid${whenNote}).`
|
|
190
|
+
: `${label} ${refStr}: credentials for \`${provider}\` present.`);
|
|
191
|
+
}
|
|
192
|
+
return { id: "credentials", label: "Provider credentials", status, details };
|
|
193
|
+
}
|
|
64
194
|
async function contextSection(config) {
|
|
65
195
|
const details = [];
|
|
66
196
|
let status = "ok";
|
|
@@ -100,66 +230,77 @@ async function contextSection(config) {
|
|
|
100
230
|
}
|
|
101
231
|
return { id: "context", label: "Context & skills", status, details };
|
|
102
232
|
}
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
async function memorySection(config) {
|
|
233
|
+
const DEFAULT_CONSOLIDATION_CRON = "0 */2 * * *";
|
|
234
|
+
async function memorySection(config, liveness, allowFilesystemWrites) {
|
|
106
235
|
if (config.memory === undefined) {
|
|
107
236
|
return { id: "memory", label: "Memory", status: "disabled", details: ["No memory configured."] };
|
|
108
237
|
}
|
|
238
|
+
// External backend (e.g. supermemory): mode/embeddings/llm are bujo-only and ignored, so report the
|
|
239
|
+
// backend's own shape. We do not ping the instance here (config-shape check); the playbook covers
|
|
240
|
+
// starting the server.
|
|
241
|
+
if ((config.memory.backend ?? "bujo") === "supermemory") {
|
|
242
|
+
const sm = config.memory.supermemory;
|
|
243
|
+
if (sm === undefined) {
|
|
244
|
+
return {
|
|
245
|
+
id: "memory",
|
|
246
|
+
label: "Memory",
|
|
247
|
+
status: "error",
|
|
248
|
+
details: ["[ERROR] backend 'supermemory' requires a memory.supermemory block."],
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
id: "memory",
|
|
253
|
+
label: "Memory",
|
|
254
|
+
status: "ok",
|
|
255
|
+
details: [
|
|
256
|
+
`Backend: supermemory, writeMode: ${config.memory.writeMode}.`,
|
|
257
|
+
`Endpoint: ${sm.baseUrl} (container "${resolveSupermemoryContainer(config)}").`,
|
|
258
|
+
sm.apiKey === undefined
|
|
259
|
+
? "Auth: no API key configured (keyless — works only if the instance allows it)."
|
|
260
|
+
: "Auth: API key configured.",
|
|
261
|
+
"Start the Supermemory instance (e.g. `supermemory-server`) before sending turns; ingestion is async.",
|
|
262
|
+
],
|
|
263
|
+
};
|
|
264
|
+
}
|
|
109
265
|
const details = [
|
|
110
266
|
`Mode: ${config.memory.mode}, path: ${config.memory.path}, writeMode: ${config.memory.writeMode}.`,
|
|
111
267
|
];
|
|
268
|
+
if (config.memory.llm !== undefined) {
|
|
269
|
+
details.push(`Chat LLM: ${memoryLlmLabel(config.memory.llm)}.`);
|
|
270
|
+
}
|
|
112
271
|
if (config.memory.mode === "bujo") {
|
|
113
|
-
// Report
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
const reflectionCron = config.memory.reflection?.cron ?? DEFAULT_REFLECTION_CRON;
|
|
117
|
-
const migrationCron = config.memory.migration?.cron ?? DEFAULT_MIGRATION_CRON;
|
|
272
|
+
// Report consolidation scheduler cadence.
|
|
273
|
+
const consolidationEnabled = config.memory.consolidation?.enabled !== false;
|
|
274
|
+
const consolidationCron = config.memory.consolidation?.cron ?? DEFAULT_CONSOLIDATION_CRON;
|
|
118
275
|
const hasLlm = config.memory.llm !== undefined;
|
|
119
276
|
if (hasLlm) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
ritualParts.push(`reflection ${reflectionCron}`);
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
ritualParts.push("reflection disabled");
|
|
126
|
-
}
|
|
127
|
-
if (migrationEnabled) {
|
|
128
|
-
ritualParts.push(`migration ${migrationCron}`);
|
|
277
|
+
if (consolidationEnabled) {
|
|
278
|
+
details.push(`Consolidation: ${consolidationCron} (auto).`);
|
|
129
279
|
}
|
|
130
280
|
else {
|
|
131
|
-
|
|
281
|
+
details.push("Consolidation: disabled.");
|
|
132
282
|
}
|
|
133
|
-
details.push(`Rituals: ${ritualParts.join(" / ")} (auto).`);
|
|
134
283
|
}
|
|
135
284
|
else {
|
|
136
|
-
details.push("
|
|
285
|
+
details.push("Consolidation: not scheduled (no chat model — bujo runtime tier downgrades to journal).");
|
|
137
286
|
}
|
|
138
287
|
}
|
|
139
288
|
if (config.memory.mode === "journal" || config.memory.mode === "bujo") {
|
|
140
|
-
const warns = await memoryLivenessWarnings(config.memory);
|
|
289
|
+
const warns = await memoryLivenessWarnings(config.memory, liveness, allowFilesystemWrites);
|
|
141
290
|
if (warns.length > 0) {
|
|
142
291
|
return { id: "memory", label: "Memory", status: "waiting", details: [...details, ...warns] };
|
|
143
292
|
}
|
|
144
293
|
}
|
|
145
294
|
if (config.memory.mode === "lite") {
|
|
146
|
-
const liteWarns = await liteRootWritableWarning(config.memory.path);
|
|
295
|
+
const liteWarns = await liteRootWritableWarning(config.memory.path, allowFilesystemWrites);
|
|
147
296
|
if (liteWarns.length > 0) {
|
|
148
297
|
return { id: "memory", label: "Memory", status: "waiting", details: [...details, ...liteWarns] };
|
|
149
298
|
}
|
|
150
299
|
}
|
|
151
300
|
return { id: "memory", label: "Memory", status: "ok", details };
|
|
152
301
|
}
|
|
153
|
-
async function liteRootWritableWarning(memoryPath) {
|
|
154
|
-
|
|
155
|
-
await mkdir(memoryPath, { recursive: true });
|
|
156
|
-
return [];
|
|
157
|
-
}
|
|
158
|
-
catch (err) {
|
|
159
|
-
return [
|
|
160
|
-
`[WARN] lite memory root is not writable: ${memoryPath} (${err instanceof Error ? err.message : String(err)}). Fix filesystem permissions.`,
|
|
161
|
-
];
|
|
162
|
-
}
|
|
302
|
+
async function liteRootWritableWarning(memoryPath, allowFilesystemWrites) {
|
|
303
|
+
return await memoryRootWritableWarnings("lite", memoryPath, allowFilesystemWrites);
|
|
163
304
|
}
|
|
164
305
|
const BUJO_PROBE_TIMEOUT_MS = 3_000;
|
|
165
306
|
/** Probes Ollama /api/tags and returns a sorted list of model names, or throws. */
|
|
@@ -178,47 +319,101 @@ async function fetchOllamaModels(endpoint) {
|
|
|
178
319
|
clearTimeout(timer);
|
|
179
320
|
}
|
|
180
321
|
}
|
|
181
|
-
async function memoryLivenessWarnings(memory) {
|
|
322
|
+
async function memoryLivenessWarnings(memory, liveness, allowFilesystemWrites) {
|
|
182
323
|
const warns = [];
|
|
183
324
|
const mode = memory.mode;
|
|
184
325
|
const embeddingsUsesOllama = (memory.embeddings?.provider ?? "ollama") === "ollama";
|
|
185
326
|
const llmUsesOllama = memory.llm?.provider === "ollama";
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
|
|
327
|
+
const ollamaModelsByEndpoint = new Map();
|
|
328
|
+
// 1. Memory root writable (every embedded tier) — local I/O, always checked.
|
|
329
|
+
const rootWarns = await memoryRootWritableWarnings(mode, memory.path, allowFilesystemWrites);
|
|
330
|
+
warns.push(...rootWarns);
|
|
331
|
+
// Network-dependent probes below only ever produce `waiting`, so the start
|
|
332
|
+
// preflight skips them (liveness=false) without changing the pass/fail verdict.
|
|
333
|
+
if (!liveness) {
|
|
334
|
+
return warns;
|
|
335
|
+
}
|
|
336
|
+
async function modelsForOllamaEndpoint(endpoint) {
|
|
337
|
+
const normalizedEndpoint = endpoint.replace(/\/$/u, "");
|
|
338
|
+
if (ollamaModelsByEndpoint.has(normalizedEndpoint)) {
|
|
339
|
+
return ollamaModelsByEndpoint.get(normalizedEndpoint);
|
|
340
|
+
}
|
|
198
341
|
try {
|
|
199
|
-
|
|
342
|
+
const models = await fetchOllamaModels(normalizedEndpoint);
|
|
343
|
+
ollamaModelsByEndpoint.set(normalizedEndpoint, models);
|
|
344
|
+
return models;
|
|
200
345
|
}
|
|
201
346
|
catch (err) {
|
|
202
|
-
|
|
347
|
+
ollamaModelsByEndpoint.set(normalizedEndpoint, undefined);
|
|
348
|
+
warns.push(`[WARN] Ollama not reachable at ${normalizedEndpoint}; ${mode} memory components configured for that endpoint will fail at runtime (${err instanceof Error ? err.message : String(err)}). Start Ollama or fix the endpoint.`);
|
|
349
|
+
return undefined;
|
|
203
350
|
}
|
|
351
|
+
}
|
|
352
|
+
// 2. Ollama liveness — only probe components that actually use Ollama. OpenAI embeddings and
|
|
353
|
+
// agent-host chat LLMs have no local model list to validate here.
|
|
354
|
+
if (embeddingsUsesOllama) {
|
|
355
|
+
const endpoint = memory.embeddings?.endpoint ?? "http://localhost:11434";
|
|
356
|
+
const ollamaModels = await modelsForOllamaEndpoint(endpoint);
|
|
204
357
|
if (ollamaModels !== undefined) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
warns.push(`[WARN] Embeddings model ${embeddingsModel} not pulled — run \`ollama pull ${embeddingsModel}\`.`);
|
|
209
|
-
}
|
|
358
|
+
const embeddingsModel = memory.embeddings?.model ?? "nomic-embed-text:v1.5";
|
|
359
|
+
if (!ollamaModels.includes(embeddingsModel)) {
|
|
360
|
+
warns.push(`[WARN] Embeddings model ${embeddingsModel} not pulled — run \`ollama pull ${embeddingsModel}\`.`);
|
|
210
361
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (llmUsesOllama && memory.llm !== undefined) {
|
|
365
|
+
const endpoint = memory.llm.endpoint ?? "http://localhost:11434";
|
|
366
|
+
const ollamaModels = await modelsForOllamaEndpoint(endpoint);
|
|
367
|
+
if (ollamaModels !== undefined) {
|
|
368
|
+
const chatModel = memory.llm.model;
|
|
369
|
+
if (!ollamaModels.includes(chatModel)) {
|
|
370
|
+
warns.push(`[WARN] Chat LLM model ${chatModel} not pulled — run \`ollama pull ${chatModel}\`.`);
|
|
216
371
|
}
|
|
217
372
|
}
|
|
218
373
|
}
|
|
219
374
|
return warns;
|
|
220
375
|
}
|
|
221
|
-
async function
|
|
376
|
+
async function memoryRootWritableWarnings(mode, memoryPath, allowFilesystemWrites) {
|
|
377
|
+
if (allowFilesystemWrites) {
|
|
378
|
+
try {
|
|
379
|
+
await mkdir(memoryPath, { recursive: true });
|
|
380
|
+
return [];
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
return [
|
|
384
|
+
`[WARN] ${mode} memory root is not writable: ${memoryPath} (${err instanceof Error ? err.message : String(err)}). Fix filesystem permissions.`,
|
|
385
|
+
];
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
const info = await stat(memoryPath);
|
|
390
|
+
if (!info.isDirectory()) {
|
|
391
|
+
return [`[WARN] ${mode} memory root is not a directory: ${memoryPath}. Fix filesystem permissions.`];
|
|
392
|
+
}
|
|
393
|
+
await access(memoryPath, constants.W_OK);
|
|
394
|
+
return [];
|
|
395
|
+
}
|
|
396
|
+
catch (err) {
|
|
397
|
+
const code = err !== null && typeof err === "object" && "code" in err ? String(err.code) : undefined;
|
|
398
|
+
if (code === "ENOENT") {
|
|
399
|
+
return [
|
|
400
|
+
`[WARN] ${mode} memory root is missing: ${memoryPath}. Consumer validation is read-only and did not create it.`,
|
|
401
|
+
];
|
|
402
|
+
}
|
|
403
|
+
return [
|
|
404
|
+
`[WARN] ${mode} memory root is not writable: ${memoryPath} (${err instanceof Error ? err.message : String(err)}). Fix filesystem permissions.`,
|
|
405
|
+
];
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
function memoryLlmLabel(llm) {
|
|
409
|
+
if (llm === undefined) {
|
|
410
|
+
return "none";
|
|
411
|
+
}
|
|
412
|
+
return llm.provider === "ollama"
|
|
413
|
+
? `ollama:${llm.model}`
|
|
414
|
+
: `agent-host:${llm.model}${llm.executionMode === undefined ? "" : ` (${llm.executionMode})`}`;
|
|
415
|
+
}
|
|
416
|
+
async function toolsSection(config, input) {
|
|
222
417
|
const details = [];
|
|
223
418
|
let status = "ok";
|
|
224
419
|
details.push(config.tools.allowedTools.length === 0
|
|
@@ -236,48 +431,143 @@ async function toolsSection(config) {
|
|
|
236
431
|
details.push(`MCP config file is missing: ${config.tools.mcpConfigPath}`);
|
|
237
432
|
}
|
|
238
433
|
}
|
|
434
|
+
const adapterSendTools = await resolveAdapterSendToolsSettings(input, {
|
|
435
|
+
allowedTools: config.tools.allowedTools,
|
|
436
|
+
disallowedTools: config.tools.disallowedTools,
|
|
437
|
+
});
|
|
438
|
+
if (adapterSendTools === undefined) {
|
|
439
|
+
details.push("No adapter-derived send tools enabled.");
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
details.push(`Adapter send tools: ${adapterSendToolNames(adapterSendTools).join(", ")}.`);
|
|
443
|
+
}
|
|
239
444
|
return { id: "tools", label: "Tools & MCP", status, details };
|
|
240
445
|
}
|
|
241
|
-
function sandboxSection(config) {
|
|
446
|
+
async function sandboxSection(config, engine) {
|
|
242
447
|
if (config.sandbox === undefined) {
|
|
243
448
|
return { id: "sandbox", label: "Sandbox", status: "disabled", details: ["No sandbox policy configured."] };
|
|
244
449
|
}
|
|
450
|
+
const state = await resolveSandboxEffectiveState({
|
|
451
|
+
policy: config.sandbox,
|
|
452
|
+
...(engine === undefined ? {} : { engine }),
|
|
453
|
+
});
|
|
454
|
+
const warning = sandboxEffectiveStateWarning(state);
|
|
455
|
+
const details = [
|
|
456
|
+
`Mode: ${config.sandbox.mode}, network: ${config.sandbox.network.mode}, fallback: ${config.sandbox.fallback}.`,
|
|
457
|
+
describeSandboxEffectiveState(state),
|
|
458
|
+
...(warning === undefined ? [] : [warning]),
|
|
459
|
+
];
|
|
460
|
+
const status = warning !== undefined
|
|
461
|
+
? "waiting"
|
|
462
|
+
: state.effective === "off"
|
|
463
|
+
? "disabled"
|
|
464
|
+
: state.effective === "blocked"
|
|
465
|
+
? "waiting"
|
|
466
|
+
: "ok";
|
|
245
467
|
return {
|
|
246
468
|
id: "sandbox",
|
|
247
469
|
label: "Sandbox",
|
|
248
|
-
status
|
|
249
|
-
details
|
|
250
|
-
`Mode: ${config.sandbox.mode}, network: ${config.sandbox.network.mode}, fallback: ${config.sandbox.fallback}.`,
|
|
251
|
-
],
|
|
470
|
+
status,
|
|
471
|
+
details,
|
|
252
472
|
};
|
|
253
473
|
}
|
|
254
|
-
async function
|
|
474
|
+
async function runsSection(input, config) {
|
|
475
|
+
const artifactDir = await resolveAppArtifactDir(input);
|
|
476
|
+
const { totalRuns, runs, warnings } = await listRecordedRuns({ artifactDir, maxRuns: RUNS_HEALTH_MAX_RUNS, scope: "agent" });
|
|
477
|
+
const display = buildRunsHealthDisplay({ artifactDir, totalRuns, runs, warnings });
|
|
478
|
+
const retentionDetails = config === undefined
|
|
479
|
+
? []
|
|
480
|
+
: [
|
|
481
|
+
`Artifact retention: maxAgeDays=${config.artifacts.retention.maxAgeDays}, maxCount=${config.artifacts.retention.maxCount}, dryRun=${config.artifacts.retention.dryRun ? "true" : "false"}.`,
|
|
482
|
+
`Memory artifact retention: maxAgeDays=${config.artifacts.memoryRetention.maxAgeDays}, maxCount=${config.artifacts.memoryRetention.maxCount}, dryRun=${config.artifacts.memoryRetention.dryRun ? "true" : "false"}.`,
|
|
483
|
+
];
|
|
484
|
+
return { id: "runs", label: "Runs health", status: display.status, details: [...retentionDetails, ...display.details] };
|
|
485
|
+
}
|
|
486
|
+
const LOCAL_ARTIFACTS_NOTE = "JSONL artifacts remain local (the exporter is additive; export failures never affect them).";
|
|
487
|
+
/**
|
|
488
|
+
* Reports observability exporter state and, uniquely among the validation
|
|
489
|
+
* sections, performs a LIVE reachability probe of the Phoenix endpoint. An
|
|
490
|
+
* invalid exporter shape is a hard `error` (fails the report) so bad config is
|
|
491
|
+
* caught before startup, but an unreachable endpoint is only `waiting` — Phoenix
|
|
492
|
+
* may start after the agent, mirroring the Ollama-unreachable precedent so
|
|
493
|
+
* `validate` still passes. Probe failures are swallowed into a warning.
|
|
494
|
+
*/
|
|
495
|
+
async function exporterSection(input, liveness) {
|
|
496
|
+
let exporters;
|
|
255
497
|
try {
|
|
256
|
-
|
|
257
|
-
if (!settings.enabled) {
|
|
258
|
-
return {
|
|
259
|
-
id: "console",
|
|
260
|
-
label: "Operator console",
|
|
261
|
-
status: "disabled",
|
|
262
|
-
details: ["Disabled via console.enabled; start stays headless."],
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
return {
|
|
266
|
-
id: "console",
|
|
267
|
-
label: "Operator console",
|
|
268
|
-
status: "ok",
|
|
269
|
-
details: [
|
|
270
|
-
settings.port === undefined
|
|
271
|
-
? "Starts on a free loopback port."
|
|
272
|
-
: `Starts on loopback port ${settings.port}.`,
|
|
273
|
-
],
|
|
274
|
-
};
|
|
498
|
+
exporters = await resolveAppObservabilityExporters(input);
|
|
275
499
|
}
|
|
276
500
|
catch (error) {
|
|
277
501
|
if (!isAppCoreConfigError(error)) {
|
|
278
502
|
throw error;
|
|
279
503
|
}
|
|
280
|
-
return { id: "
|
|
504
|
+
return { id: "observability", label: "Observability exporter", status: "error", details: [error.message] };
|
|
505
|
+
}
|
|
506
|
+
if (exporters.length === 0) {
|
|
507
|
+
return {
|
|
508
|
+
id: "observability",
|
|
509
|
+
label: "Observability exporter",
|
|
510
|
+
status: "disabled",
|
|
511
|
+
details: ["No observability exporter configured."],
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
const exporter = exporters[0];
|
|
515
|
+
const details = [`Exporter: ${exporter.type} -> ${exporter.endpoint}`];
|
|
516
|
+
const appUrl = phoenixAppBaseUrl(exporter.endpoint);
|
|
517
|
+
if (appUrl !== undefined) {
|
|
518
|
+
details.push(`Phoenix app: ${appUrl}`);
|
|
519
|
+
}
|
|
520
|
+
if (exporter.includeSensitiveData) {
|
|
521
|
+
details.push(describeSensitiveDataExportWarning(exporter.endpoint));
|
|
522
|
+
}
|
|
523
|
+
// The reachability probe only ever yields `waiting`, so the start preflight
|
|
524
|
+
// skips it (liveness=false): the exporter shape is valid, and Phoenix may
|
|
525
|
+
// legitimately come up after the agent.
|
|
526
|
+
if (!liveness) {
|
|
527
|
+
details.push(LOCAL_ARTIFACTS_NOTE);
|
|
528
|
+
return { id: "observability", label: "Observability exporter", status: "ok", details };
|
|
529
|
+
}
|
|
530
|
+
const probeError = await probeExporterEndpoint(exporter.endpoint);
|
|
531
|
+
if (probeError !== undefined) {
|
|
532
|
+
details.push(`[WARN] Phoenix export not confirmed at ${exporter.endpoint} (${probeError}); exports will fail until it accepts OTLP protobuf. This is non-fatal.`);
|
|
533
|
+
details.push(LOCAL_ARTIFACTS_NOTE);
|
|
534
|
+
return { id: "observability", label: "Observability exporter", status: "waiting", details };
|
|
535
|
+
}
|
|
536
|
+
details.push(LOCAL_ARTIFACTS_NOTE);
|
|
537
|
+
return { id: "observability", label: "Observability exporter", status: "ok", details };
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Live export-compatibility probe: POSTs a tiny but VALID empty OTLP protobuf
|
|
541
|
+
* `ExportTraceServiceRequest` (zero spans, so nothing is recorded) with
|
|
542
|
+
* `content-type: application/x-protobuf` — exactly the wire format a real export
|
|
543
|
+
* uses. Returns undefined when the endpoint accepts it (2xx), else an error
|
|
544
|
+
* string. This catches what the old OPTIONS reachability probe missed: a server
|
|
545
|
+
* that is listening but rejects the export (e.g. Phoenix returns 415 for the
|
|
546
|
+
* wrong content type), so `validate` no longer reports `[ok]` for an endpoint
|
|
547
|
+
* that every real export would 415. A throw (refused / DNS / timeout) means
|
|
548
|
+
* nothing is listening. Kept non-fatal upstream (`waiting`, not `error`): Phoenix
|
|
549
|
+
* may start after the agent.
|
|
550
|
+
*/
|
|
551
|
+
async function probeExporterEndpoint(endpoint) {
|
|
552
|
+
const ctrl = new AbortController();
|
|
553
|
+
const timer = setTimeout(() => { ctrl.abort(); }, BUJO_PROBE_TIMEOUT_MS);
|
|
554
|
+
try {
|
|
555
|
+
const response = await fetch(endpoint, {
|
|
556
|
+
method: "POST",
|
|
557
|
+
headers: { "content-type": "application/x-protobuf" },
|
|
558
|
+
body: serializeTraceSpans([]),
|
|
559
|
+
signal: ctrl.signal,
|
|
560
|
+
});
|
|
561
|
+
if (!response.ok) {
|
|
562
|
+
return `HTTP ${response.status}`;
|
|
563
|
+
}
|
|
564
|
+
return undefined;
|
|
565
|
+
}
|
|
566
|
+
catch (err) {
|
|
567
|
+
return err instanceof Error ? err.message : String(err);
|
|
568
|
+
}
|
|
569
|
+
finally {
|
|
570
|
+
clearTimeout(timer);
|
|
281
571
|
}
|
|
282
572
|
}
|
|
283
573
|
async function channelSection(driver, input) {
|
|
@@ -292,6 +582,13 @@ async function channelSection(driver, input) {
|
|
|
292
582
|
if (waitingReason !== undefined) {
|
|
293
583
|
return { id, label: driver.label, status: "waiting", details: [waitingReason] };
|
|
294
584
|
}
|
|
585
|
+
// A structural issue (e.g. a typo'd per-trigger model override) fails
|
|
586
|
+
// validate loudly here; `start` still runs the channel and warn-ignores
|
|
587
|
+
// the bad value at run time.
|
|
588
|
+
const issues = driver.configIssues?.(config) ?? [];
|
|
589
|
+
if (issues.length > 0) {
|
|
590
|
+
return { id, label: driver.label, status: "error", details: [...issues] };
|
|
591
|
+
}
|
|
295
592
|
return { id, label: driver.label, status: "ok", details: ["Configured; will start with the app."] };
|
|
296
593
|
}
|
|
297
594
|
catch (error) {
|
|
@@ -303,7 +600,7 @@ async function channelSection(driver, input) {
|
|
|
303
600
|
}
|
|
304
601
|
}
|
|
305
602
|
function referenceOf(model) {
|
|
306
|
-
return model
|
|
603
|
+
return modelReferenceKey(model);
|
|
307
604
|
}
|
|
308
605
|
async function pathExists(path) {
|
|
309
606
|
try {
|