@barivia/barsom-mcp 0.23.10 → 0.23.12
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 +5 -4
- package/dist/audit.js +9 -3
- package/dist/figure_sections.js +63 -0
- package/dist/index.js +1 -1
- package/dist/job_monitor.js +20 -1
- package/dist/shared.js +51 -9
- package/dist/tools/account.js +1 -1
- package/dist/tools/explore_map.js +8 -5
- package/dist/tools/feedback.js +6 -1
- package/dist/tools/jobs.js +2 -2
- package/dist/tools/results.js +9 -1
- package/dist/tools/train.js +1 -1
- package/dist/tools/training_monitor.js +27 -15
- package/dist/ui-delivery.js +44 -10
- package/dist/views/src/views/results-explorer/index.html +10 -10
- package/dist/views/src/views/training-monitor/index.html +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @barivia/barsom-mcp
|
|
2
2
|
|
|
3
|
-
MCP proxy for the Barivia Analytics Engine — connects any stdio MCP client (Cursor, Claude Desktop, etc.) to the barSOM cloud API. The npm package is **`@barivia/barsom-mcp`**; many configs label the server **`analytics-engine`** (the MCP server name in the client JSON). **`guide_barsom_workflow`** is the canonical bootstrap: it loads plan-scoped workflow text from the Barivia API when online (tool map, async rules, training modes, SOP). If the API is unreachable, it returns a short offline stub. Core tools are `datasets`, `train` (`map`, `siom_map`, `impute`, `floop_siom` where entitled), `jobs` (status/list/compare/cancel/delete — lifecycle only), `results` (get/export/download/recolor), and `inference` (predict/batch_predict/impute_column/compare/project_columns/transition_flow/report). **Optional MCP App tools** (`training_monitor`, `results_explorer`) add embedded or localhost viz; they are **not required** to complete upload → train → poll → results, though `results_explorer` is the preferred way to browse figures.
|
|
3
|
+
MCP proxy for the Barivia Analytics Engine — connects any stdio MCP client (Cursor, Claude Desktop, etc.) to the barSOM cloud API. The npm package is **`@barivia/barsom-mcp`**; many configs label the server **`analytics-engine`** (the MCP server name in the client JSON). **`guide_barsom_workflow`** is the canonical bootstrap: it loads plan-scoped workflow text from the Barivia API when online (tool map, async rules, training modes, SOP). If the API is unreachable, it returns a short **connectivity-only** offline stub. Core tools are `datasets`, `train` (`map`, `siom_map`, `impute`, `floop_siom` where entitled), `jobs` (status/list/compare/cancel/delete — lifecycle only), `results` (get/export/download/recolor), and `inference` (predict/batch_predict/impute_column/compare/project_columns/transition_flow/report). **Optional MCP App tools** (`training_monitor`, `results_explorer`) add embedded or localhost viz; they are **not required** to complete upload → train → poll → results, though `results_explorer` is the preferred way to browse figures.
|
|
4
4
|
|
|
5
|
-
**Pre-training help (pick one):** **`prepare_training`** prompt = narrative checklist (tier-scoped from the API when online); **`training_guidance`** tool = structured presets and parameter hints (including
|
|
5
|
+
**Pre-training help (pick one):** **`prepare_training`** prompt = narrative checklist (tier-scoped from the API when online); **`training_guidance`** tool = structured presets, quality/timing heuristics, and parameter hints (including resolved per-column normalization). Review either, then submit with **`train`**. Parameter encyclopedias and quality targets are **API-gated** (valid key + plan); the published proxy catalog keeps short routing text only.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -56,7 +56,8 @@ MCP clients typically run it with **`npx`** (downloads on first use):
|
|
|
56
56
|
| `BARIVIA_WORKSPACE_EXTRA_ROOTS` | No | (empty) | Comma- or colon-separated absolute directories whose **realpath** targets are also allowed for uploads when the sandbox is on. Use for shared fixture trees reached via symlinks under the case workspace (e.g. `…/barivia-testing/data`). Does not weaken the default sandbox for paths outside these roots. |
|
|
57
57
|
| `BARIVIA_FETCH_TIMEOUT_MS` | No | `60000` | Per-request HTTP timeout (ms) to the API. Increase (e.g. `120000`) on slow networks or when the API does long-running work. Large dataset uploads use a separate longer timeout internally, and `datasets(analyze)` runs asynchronously (auto-polled) so it is not bound by this. A timeout is not auto-retried (re-firing slow work would only multiply load / risk duplicates). |
|
|
58
58
|
| `BARIVIA_VIZ_PORT` | No | OS-assigned | Local viz server on `127.0.0.1`. Port is **per MCP session** unless pinned — standalone `…/viz/…` URLs go stale after proxy restart. Set a fixed port for stable bookmark URLs. Diagnostic: `GET http://127.0.0.1:<port>/api/health?job_id=<id>`. |
|
|
59
|
-
| `BARIVIA_UI_DELIVERY` | No | `auto` | Client-capability override for App tools: `auto` \| `apps` \| `localhost` \| `inline`. Tool responses always lead with a standalone localhost URL (hosts may list `ui://` without mounting widgets) and include a structured `ui_delivery` block. `account(status)` reports detected Apps support, viz port, and active tier. |
|
|
59
|
+
| `BARIVIA_UI_DELIVERY` | No | `auto` | Client-capability override for App tools: `auto` \| `apps` \| `localhost` \| `inline` \| `text_only`. Tool responses always lead with a standalone localhost URL (hosts may list `ui://` without mounting widgets) and include a structured `ui_delivery` block. `account(status)` reports detected Apps support, viz port, and active tier. |
|
|
60
|
+
| `BARIVIA_UI_PREFER_LOCALHOST` | No | (off) | Set `1` to force `localhost` under `auto` even when the host advertises MCP Apps (Cursor list-without-mount workaround). Default `auto` behavior unchanged. |
|
|
60
61
|
| `BARIVIA_ENFORCE_WORKSPACE_SANDBOX` | No | `1` (enabled) | File uploads are constrained to the MCP workspace root (and optional `BARIVIA_WORKSPACE_EXTRA_ROOTS`) by default. Set to `0` or `false` to allow absolute paths anywhere on the machine (high trust). If uploads fail with "symlink resolves outside workspace", either point `BARIVIA_WORKSPACE_ROOT` / `BARIVIA_WORKSPACE_EXTRA_ROOTS` at the real data dir, copy the file into the workspace, or opt out with `BARIVIA_ENFORCE_WORKSPACE_SANDBOX=0`. |
|
|
61
62
|
|
|
62
63
|
Legacy `BARSOM_API_KEY` / `BARSOM_API_URL` / `BARSOM_WORKSPACE_ROOT` are also accepted as fallbacks.
|
|
@@ -193,7 +194,7 @@ The right viewer depends on **(MCP App support)** **and** **(can the human reach
|
|
|
193
194
|
### Migration notes
|
|
194
195
|
|
|
195
196
|
- **Results explorer figure dropdown (0.23.2):** `results_explorer` puts a grouped `<select>` above the plot (no scrolling the sidebar to switch figures), matching barmesh explorer UX. Sidebar keeps Metrics + Highlights only.
|
|
196
|
-
- **UI delivery + train auto-open (0.22.4+):** `train()` returns `training_monitor` App structured content so supporting hosts open the monitor in the same turn. App tools emit structured `ui_delivery` (`delivery`, `urls`, `hint_for_agent`) and always put the standalone viz URL first. Env: `BARIVIA_UI_DELIVERY
|
|
197
|
+
- **UI delivery + train auto-open (0.22.4+ / 0.23.11+):** `train()` returns `training_monitor` App structured content so supporting hosts open the monitor in the same turn. App tools emit structured `ui_delivery` (`delivery`, `urls`, `hint_for_agent`) and always put the standalone viz URL first. Env: `BARIVIA_UI_DELIVERY` (`auto` \| `apps` \| `localhost` \| `inline` \| `text_only`), `BARIVIA_UI_PREFER_LOCALHOST=1` (opt-in localhost under `auto`), `BARIVIA_VIZ_PORT`. Cyclic cos/sin component PNGs are omitted from default `summary.files` / `figures=all` (encoding for training; panels still appear in `combined` / recolor); opt in with train param `viz_upload_cyclic_components=true`.
|
|
197
198
|
- **Account surface (0.22.0, breaking):** `account(request_compute|compute_status|release_compute)` removed — AWS cloud burst leases are gone. Use `account(status|history|add_funds)`. Capacity is shared LOCAL/GKE pools.
|
|
198
199
|
- **Structured results + train hints (0.21.x, non-breaking):**
|
|
199
200
|
- **`results(action=get)`** returns **`structuredContent`** (job id/type, label, summary metrics, file list) alongside the text summary — same pattern as `training_monitor` / `results_explorer`. Agent hosts can read fields without parsing prose.
|
package/dist/audit.js
CHANGED
|
@@ -161,14 +161,20 @@ export async function runMcpToolAudit(tool, action, args, handler) {
|
|
|
161
161
|
/**
|
|
162
162
|
* Register an MCP tool with structured audit logging on each invocation.
|
|
163
163
|
*/
|
|
164
|
-
export function registerAuditedTool(server, name, description, schema, handler) {
|
|
165
|
-
|
|
164
|
+
export function registerAuditedTool(server, name, description, schema, handler, annotations) {
|
|
165
|
+
const cb = (async (args) => {
|
|
166
166
|
const rec = args;
|
|
167
167
|
const action = typeof rec.action === "string" && rec.action.length > 0 ? rec.action : "default";
|
|
168
168
|
// One trace per tool invocation: every apiCall inside this handler shares one
|
|
169
169
|
// trace_id, so the API + job chain reconstruct the whole logical action.
|
|
170
170
|
return runWithTrace(() => runMcpToolAudit(name, action, rec, () => handler(args)));
|
|
171
|
-
})
|
|
171
|
+
});
|
|
172
|
+
if (annotations) {
|
|
173
|
+
server.tool(name, description, schema, annotations, cb);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
server.tool(name, description, schema, cb);
|
|
177
|
+
}
|
|
172
178
|
}
|
|
173
179
|
/** Attach audit flags to a tool result (stripped before returning to MCP client). */
|
|
174
180
|
export function withAuditFlags(result, flags) {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical figure taxonomy for barsom results_explorer dropdown sections.
|
|
3
|
+
* Labels match the prior GROUP_LABELS (Summary → Diagnostics → Components → Other).
|
|
4
|
+
*/
|
|
5
|
+
export const FIGURE_SECTION_ORDER = [
|
|
6
|
+
"summary",
|
|
7
|
+
"diagnostic",
|
|
8
|
+
"component",
|
|
9
|
+
"artifact",
|
|
10
|
+
];
|
|
11
|
+
export const FIGURE_SECTION_LABELS = {
|
|
12
|
+
summary: "Summary",
|
|
13
|
+
diagnostic: "Diagnostics",
|
|
14
|
+
component: "Components",
|
|
15
|
+
artifact: "Other",
|
|
16
|
+
};
|
|
17
|
+
/** Map a figure base key into one of four explorer sections. */
|
|
18
|
+
export function figureSection(key) {
|
|
19
|
+
if (key === "combined" || key === "coverage_overview")
|
|
20
|
+
return "summary";
|
|
21
|
+
if (key.startsWith("component_"))
|
|
22
|
+
return "component";
|
|
23
|
+
if (key === "umatrix" ||
|
|
24
|
+
key === "hit_histogram" ||
|
|
25
|
+
key === "learning_curve" ||
|
|
26
|
+
key === "correlation" ||
|
|
27
|
+
key === "divergence_kl" ||
|
|
28
|
+
key === "divergence_w1" ||
|
|
29
|
+
key.startsWith("cluster")) {
|
|
30
|
+
return "diagnostic";
|
|
31
|
+
}
|
|
32
|
+
if (key.startsWith("component"))
|
|
33
|
+
return "component";
|
|
34
|
+
return "artifact";
|
|
35
|
+
}
|
|
36
|
+
export const FIGURE_SORT_RANK = {
|
|
37
|
+
combined: 0,
|
|
38
|
+
coverage_overview: 1,
|
|
39
|
+
umatrix: 10,
|
|
40
|
+
hit_histogram: 11,
|
|
41
|
+
learning_curve: 12,
|
|
42
|
+
correlation: 13,
|
|
43
|
+
divergence_kl: 14,
|
|
44
|
+
divergence_w1: 15,
|
|
45
|
+
};
|
|
46
|
+
export function sortRank(key) {
|
|
47
|
+
if (key in FIGURE_SORT_RANK)
|
|
48
|
+
return FIGURE_SORT_RANK[key];
|
|
49
|
+
if (key.startsWith("component_")) {
|
|
50
|
+
const n = Number(key.replace(/^component_/, "").split("_")[0]);
|
|
51
|
+
return 20 + (Number.isFinite(n) ? n : 0);
|
|
52
|
+
}
|
|
53
|
+
if (key.startsWith("cluster"))
|
|
54
|
+
return 16;
|
|
55
|
+
return 50;
|
|
56
|
+
}
|
|
57
|
+
/** Sort by section order, then canonical rank, then label. */
|
|
58
|
+
export function sortFiguresBySection(figures) {
|
|
59
|
+
return [...figures].sort((a, b) => FIGURE_SECTION_ORDER.indexOf(a.kind ?? figureSection(a.key)) -
|
|
60
|
+
FIGURE_SECTION_ORDER.indexOf(b.kind ?? figureSection(b.key)) ||
|
|
61
|
+
sortRank(a.key) - sortRank(b.key) ||
|
|
62
|
+
a.label.localeCompare(b.label));
|
|
63
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{McpServer as e}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as t}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as o}from"zod";import{getUiCapability as r,registerAppResource as s,RESOURCE_MIME_TYPE as n}from"@modelcontextprotocol/ext-apps/server";import{startVizServer as a}from"./viz-server.js";import{API_KEY as i,apiCall as l,apiRawCall as c,loadViewHtml as p,setVizPort as m,setClientSupportsMcpApps as u,CLIENT_VERSION as d}from"./shared.js";import{registerDatasetsTool as f}from"./tools/datasets.js";import{registerTrainTool as _}from"./tools/train.js";import{registerJobsTool as g,JOBS_DESCRIPTION_BASE as h}from"./tools/jobs.js";import{registerResultsTool as b}from"./tools/results.js";import{registerExploreMapTool as w,RESULTS_EXPLORER_URI as y}from"./tools/explore_map.js";import{registerAccountTool as v}from"./tools/account.js";import{registerInferenceTool as j}from"./tools/inference.js";import{registerGuideBarsomTool as x}from"./tools/guide_barsom.js";import{registerTrainingGuidanceTool as k}from"./tools/training_guidance.js";import{registerFeedbackTool as I}from"./tools/feedback.js";import{registerTrainingMonitorTool as
|
|
2
|
+
import{McpServer as e}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as t}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as o}from"zod";import{getUiCapability as r,registerAppResource as s,RESOURCE_MIME_TYPE as n}from"@modelcontextprotocol/ext-apps/server";import{startVizServer as a}from"./viz-server.js";import{API_KEY as i,apiCall as l,apiRawCall as c,loadViewHtml as p,setVizPort as m,setClientSupportsMcpApps as u,CLIENT_VERSION as d}from"./shared.js";import{registerDatasetsTool as f}from"./tools/datasets.js";import{registerTrainTool as _}from"./tools/train.js";import{registerJobsTool as g,JOBS_DESCRIPTION_BASE as h}from"./tools/jobs.js";import{registerResultsTool as b}from"./tools/results.js";import{registerExploreMapTool as w,RESULTS_EXPLORER_URI as y}from"./tools/explore_map.js";import{registerAccountTool as v}from"./tools/account.js";import{registerInferenceTool as j}from"./tools/inference.js";import{registerGuideBarsomTool as x}from"./tools/guide_barsom.js";import{registerTrainingGuidanceTool as k}from"./tools/training_guidance.js";import{registerFeedbackTool as I}from"./tools/feedback.js";import{registerTrainingMonitorTool as A,TRAINING_MONITOR_URI as P}from"./tools/training_monitor.js";import{resolvePrepareTrainingPrompt as S}from"./prepare_training_prompt.js";i||(console.error("Error: BARIVIA_API_KEY not set. Set it in your MCP client config."),process.exit(1));const T=new e({name:"analytics-engine",version:d,instructions:'# Barivia Mapping Analytics Engine\n\nSelf-organizing map (SOM) analytics via the Barivia API.\n\n## Bootstrap (do this first)\n\nCall `guide_barsom_workflow` for the plan-scoped tool map, training modes, async rules, and SOP (API when online). Call `training_guidance` / `prepare_training` for parameter and quality hints before guessing knobs.\n\n## Workflow (short)\n\n`datasets(upload)` → preview/analyze → `train(action=...)` (only modes your plan allows) → poll `jobs(status)` every 10–15s → `results(get)` then optional `results_explorer`. Scoring and frozen-map ops live in `inference`.\n\n## Tool taxonomy (one tool per stage)\n\n| Stage | Tool(s) |\n|-------|---------|\n| Data | `datasets` |\n| Train submit | `train` (map / siom_map / impute / floop_siom where entitled) |\n| Lifecycle | `jobs` (status/list/compare/cancel/delete — no train/score) |\n| Artifacts | `results` (get/export/download/recolor) |\n| Frozen-map ops | `inference` (predict/batch_predict/impute_column/compare/project_columns/transition_flow/report) |\n| Account | `account` (status/inventory/health) |\n| Bootstrap | `guide_barsom_workflow`, `training_guidance`, `prepare_training` |\n| Explore (UI) | `results_explorer`, `training_monitor` |\n| Feedback | `send_feedback` (after user agrees) |\n\n### Avoid these mis-routes\n- `jobs(compare)` = metrics across **training runs**. `inference(compare)` = density-diff vs a **second dataset**.\n- Post-training analysis is `inference`, not `results` (presentation/artifacts only).\n- Project columns → `inference(project_columns)`; formula → `datasets(add_expression, project_onto_job=...)`.\n\n## Async & constraints\n\n- Training returns `job_id` immediately — poll `jobs(status)` every 10–15s; running is not failed.\n- **Do not auto-subset** unless the user asks. Prefer `results(get, figures="none")` for sweeps.\n- Column names are case-sensitive. After recolor/transition_flow/project_columns, use the **new** job_id when returned.\n- On `api_unreachable`: one calm sentence, honor `retry_after_sec`; use `account(health)`. Timeouts are not auto-retried.\n\n## UI delivery\n\nTool results lead with a localhost viz URL and a structured `ui_delivery` block — follow `hint_for_agent`. Env: `BARIVIA_UI_DELIVERY` (auto|apps|localhost|inline|text_only), `BARIVIA_UI_PREFER_LOCALHOST=1` (opt-in localhost even when Apps advertised), `BARIVIA_VIZ_PORT`. Details: `account(status)`.'});s(T,y,y,{mimeType:n},async()=>{const e=await p("results-explorer");return{contents:[{uri:y,mimeType:n,text:e??"<html><body>Results Explorer view not built yet. Run: npm run build:views</body></html>"}]}}),s(T,P,P,{mimeType:n},async()=>{const e=await p("training-monitor");return{contents:[{uri:P,mimeType:n,text:e??"<html><body>Training Monitor view not built yet.</body></html>"}]}}),x(T),w(T),A(T),f(T),_(T),g(T,h),b(T),v(T),j(T),k(T),I(T),T.prompt("info","Short orientation for the Barivia Mapping MCP. For full plan-scoped workflow, tool map, and SOP, the model should call guide_barsom_workflow. Use when the user asks what this MCP can do or how to get started.",{},()=>({messages:[{role:"user",content:{type:"text",text:["Give a concise, scannable answer (headers + bullets):","","**What it is:** MCP client to the Barivia mapping engine (2D SOM / SIOM / FLooP-SIOM when entitled) over HTTPS.","","**First step:** Call `guide_barsom_workflow` for plan-scoped bootstrap (full tool list, async rules, training modes, optional MCP Apps, SOP).","","**Core path:** `datasets(upload)` → `datasets(preview)` + `datasets(analyze)` → choose training action → poll `jobs(status)` every 10–15s until completed → `results(get)` (all main figures/metrics; no separate analyze tool) → `results_explorer(job_id)` to browse figures.","",'**Key tools:** `datasets` (data; reduce_spectral for spectra/long blocks), `train` (map; siom_map/impute with SIOM entitlement; floop_siom when entitled; pass `label` for readable compare rows; preset=quick for a fast first map), `jobs` (status/list/compare/cancel/delete — lifecycle only), `results` (get/download/export/recolor; figures="none" for metrics-only), `inference` (predict; batch_predict; impute_column for topology-neighbor pool fill; compare; project_columns; transition_flow; report), `account` (status/credits/queue).',"","**Prep help:** `prepare_training` prompt (checklist) · `training_guidance` (presets/JSON hints + resolved normalization/params) — review, then submit with `train`.","","**Browse results:** `results_explorer(job_id)` is the preferred way to explore figures after a first `results(get)` glance; `training_monitor` shows live progress. Both optional — `results` + `jobs(status)` suffice headless.","","**After training:** `jobs(compare)` across runs, `results(recolor)`, `inference(project_columns)` for variables not in training, `inference(transition_flow)` only if rows are time-ordered.","","**Rules:** Running ≠ failed. Column names must match `datasets(preview)` exactly. Do not auto-subset — never call `datasets(subset)` / `sample_n` unless the user explicitly asks; train on the full uploaded `dataset_id`. Do not call `_fetch_figure` from chat (host/UI only); use `results(get)` or `results_explorer`.","","Offer `send_feedback` only after asking the user."].join("\n")}}]})),T.prompt("prepare_training","Narrative pre-training checklist (prompt). Use after upload and before train. Content is tier-scoped from the API when online. Prep ladder: this prompt = story checklist; training_guidance tool = JSON presets/parameter hints. Review, then submit with train.",{dataset_id:o.string().describe("Dataset ID to prepare for training")},async({dataset_id:e})=>{const t=await S(e);return{messages:[{role:"user",content:{type:"text",text:(t.used_fallback?`[offline fallback ${t.fallback_version}]\n\n`:"")+t.text}}]}});const O=new t;(async function(){try{const e=await a(l,c,p);m(e)}catch(e){process.env.BARIVIA_VIZ_PORT&&console.error("Barivia viz server failed to start:",e)}const e=T.server;e.oninitialized=()=>{const t=e.getClientCapabilities(),o=r(t);u(!!o?.mimeTypes?.includes(n))},await T.connect(O)})().catch(console.error);
|
package/dist/job_monitor.js
CHANGED
|
@@ -10,6 +10,9 @@ export const DEFAULT_BLOCK_UNTIL_SEC = 900;
|
|
|
10
10
|
export const DEFAULT_POLL_INTERVAL_SEC = 5;
|
|
11
11
|
export const MIN_POLL_INTERVAL_SEC = 5;
|
|
12
12
|
export const HEARTBEAT_POLLS = 1;
|
|
13
|
+
/** After this many unchanged polls, gently raise interval (never below MIN_POLL). */
|
|
14
|
+
export const IDLE_BACKOFF_AFTER_POLLS = 8;
|
|
15
|
+
export const MAX_POLL_INTERVAL_SEC = 15;
|
|
13
16
|
function sleep(ms) {
|
|
14
17
|
return new Promise((r) => setTimeout(r, ms));
|
|
15
18
|
}
|
|
@@ -218,12 +221,22 @@ export async function monitorJob(job_id, options = {}) {
|
|
|
218
221
|
let lastSnap = null;
|
|
219
222
|
let data = {};
|
|
220
223
|
let heartbeat = 0;
|
|
224
|
+
let idleStreak = 0;
|
|
221
225
|
while (Date.now() - start < blockMs) {
|
|
222
226
|
data = (await apiCall("GET", `/v1/jobs/${job_id}`));
|
|
223
227
|
const elapsedSec = Math.round((Date.now() - start) / 1000);
|
|
224
228
|
const snap = snapshotFromJob(data, elapsedSec);
|
|
225
229
|
heartbeat += 1;
|
|
226
230
|
const heartbeatDue = heartbeat >= HEARTBEAT_POLLS;
|
|
231
|
+
const unchanged = lastSnap != null &&
|
|
232
|
+
lastSnap.status === snap.status &&
|
|
233
|
+
lastSnap.progress_pct === snap.progress_pct &&
|
|
234
|
+
lastSnap.phase === snap.phase &&
|
|
235
|
+
lastSnap.epoch === snap.epoch;
|
|
236
|
+
if (unchanged)
|
|
237
|
+
idleStreak += 1;
|
|
238
|
+
else
|
|
239
|
+
idleStreak = 0;
|
|
227
240
|
if (shouldRecordSnapshot(lastSnap, snap) || heartbeatDue) {
|
|
228
241
|
snapshots.push(snap);
|
|
229
242
|
lastSnap = snap;
|
|
@@ -233,7 +246,13 @@ export async function monitorJob(job_id, options = {}) {
|
|
|
233
246
|
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
234
247
|
break;
|
|
235
248
|
}
|
|
236
|
-
|
|
249
|
+
// Gentle backoff on idle polls — raise interval, never below the 5s floor.
|
|
250
|
+
let waitMs = pollMs;
|
|
251
|
+
if (idleStreak >= IDLE_BACKOFF_AFTER_POLLS) {
|
|
252
|
+
const steps = Math.floor((idleStreak - IDLE_BACKOFF_AFTER_POLLS) / 2);
|
|
253
|
+
waitMs = Math.min(pollMs + steps * 5000, MAX_POLL_INTERVAL_SEC * 1000);
|
|
254
|
+
}
|
|
255
|
+
await sleep(Math.max(MIN_POLL_INTERVAL_SEC * 1000, waitMs));
|
|
237
256
|
}
|
|
238
257
|
const status = String(data.status ?? "");
|
|
239
258
|
const terminal = status === "completed" || status === "failed" || status === "cancelled";
|
package/dist/shared.js
CHANGED
|
@@ -27,10 +27,37 @@ export const RETRY_BASE_MS = parseInt(process.env.BARIVIA_RETRY_BASE_MS ?? "2000
|
|
|
27
27
|
export const RETRYABLE_STATUS = new Set([502, 503, 504]);
|
|
28
28
|
/** Cool-down hint after the burst is exhausted — agents should wait before another round. */
|
|
29
29
|
export const UNAVAILABLE_RETRY_AFTER_SEC = 30;
|
|
30
|
-
/**
|
|
31
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Delay between burst retries. Keeps the flat RETRY_BASE_MS default (short gateway
|
|
32
|
+
* blips); when the server sends Retry-After / retry_after_sec, sleep at least that long.
|
|
33
|
+
*/
|
|
34
|
+
export function retryDelayMs(_attempt, retryAfterMs) {
|
|
35
|
+
if (retryAfterMs != null && Number.isFinite(retryAfterMs) && retryAfterMs > 0) {
|
|
36
|
+
return Math.max(RETRY_BASE_MS, Math.floor(retryAfterMs));
|
|
37
|
+
}
|
|
32
38
|
return RETRY_BASE_MS;
|
|
33
39
|
}
|
|
40
|
+
/** Parse Retry-After header (delta-seconds) and/or JSON body retry_after_sec → ms. */
|
|
41
|
+
export function parseRetryAfterMs(resp, bodyText) {
|
|
42
|
+
const header = resp.headers.get("Retry-After");
|
|
43
|
+
if (header) {
|
|
44
|
+
const sec = parseInt(header.trim(), 10);
|
|
45
|
+
if (!Number.isNaN(sec) && sec >= 0)
|
|
46
|
+
return sec * 1000;
|
|
47
|
+
}
|
|
48
|
+
if (bodyText) {
|
|
49
|
+
try {
|
|
50
|
+
const j = JSON.parse(bodyText);
|
|
51
|
+
const sec = Number(j.retry_after_sec);
|
|
52
|
+
if (Number.isFinite(sec) && sec >= 0)
|
|
53
|
+
return sec * 1000;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
/* not JSON */
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
34
61
|
function newRequestId() {
|
|
35
62
|
return randomUUID();
|
|
36
63
|
}
|
|
@@ -39,7 +66,7 @@ function newRequestId() {
|
|
|
39
66
|
* X-Barsom-Client-Version so the server can annotate tool guidance with the
|
|
40
67
|
* wrapper version each action requires. Keep in sync with package.json on bump.
|
|
41
68
|
*/
|
|
42
|
-
export const CLIENT_VERSION = "0.23.
|
|
69
|
+
export const CLIENT_VERSION = "0.23.12";
|
|
43
70
|
/** User-facing links; keep aligned with barivia.se / api.barivia.se. */
|
|
44
71
|
export const PUBLIC_SITE_ORIGIN = "https://barivia.se";
|
|
45
72
|
/** Self-serve account dashboard (manage plan, billing, and API keys). */
|
|
@@ -244,7 +271,11 @@ export function isTransientError(err, status) {
|
|
|
244
271
|
return true; // network-level fetch failure
|
|
245
272
|
return false;
|
|
246
273
|
}
|
|
247
|
-
/**
|
|
274
|
+
/**
|
|
275
|
+
* Streaming SHA-256 of a file without reading it into memory.
|
|
276
|
+
* Used by tools/datasets.ts upload as Idempotency-Key = sha256(name+body) — do not
|
|
277
|
+
* invent a second key path here.
|
|
278
|
+
*/
|
|
248
279
|
export async function streamFileSha256(srcPath) {
|
|
249
280
|
return new Promise((resolve, reject) => {
|
|
250
281
|
const h = createHash("sha256");
|
|
@@ -814,7 +845,7 @@ export async function apiCall(method, path, body, extraHeaders, requestTimeoutMs
|
|
|
814
845
|
const text = await resp.text();
|
|
815
846
|
if (!resp.ok) {
|
|
816
847
|
if (attempt < MAX_RETRIES && isTransientError(null, resp.status)) {
|
|
817
|
-
const delayMs = retryDelayMs(attempt);
|
|
848
|
+
const delayMs = retryDelayMs(attempt, parseRetryAfterMs(resp, text));
|
|
818
849
|
logWarn("API retry", {
|
|
819
850
|
rid: requestId,
|
|
820
851
|
method,
|
|
@@ -845,7 +876,10 @@ export async function apiCall(method, path, body, extraHeaders, requestTimeoutMs
|
|
|
845
876
|
catch (err) {
|
|
846
877
|
lastError = err;
|
|
847
878
|
if (attempt < MAX_RETRIES && isTransientError(err)) {
|
|
848
|
-
const
|
|
879
|
+
const errRetryMs = typeof err.retryAfterSec === "number"
|
|
880
|
+
? err.retryAfterSec * 1000
|
|
881
|
+
: undefined;
|
|
882
|
+
const delayMs = retryDelayMs(attempt, errRetryMs);
|
|
849
883
|
logWarn("API retry", {
|
|
850
884
|
rid: requestId,
|
|
851
885
|
method,
|
|
@@ -889,8 +923,9 @@ export async function apiRawCall(path, requestTimeoutMs) {
|
|
|
889
923
|
},
|
|
890
924
|
}, effectiveTimeout);
|
|
891
925
|
if (!resp.ok) {
|
|
926
|
+
const text = await resp.text();
|
|
892
927
|
if (attempt < MAX_RETRIES && isTransientError(null, resp.status)) {
|
|
893
|
-
const delayMs = retryDelayMs(attempt);
|
|
928
|
+
const delayMs = retryDelayMs(attempt, parseRetryAfterMs(resp, text));
|
|
894
929
|
logWarn("API retry", {
|
|
895
930
|
rid: requestId,
|
|
896
931
|
path,
|
|
@@ -902,7 +937,6 @@ export async function apiRawCall(path, requestTimeoutMs) {
|
|
|
902
937
|
await new Promise((r) => setTimeout(r, delayMs));
|
|
903
938
|
continue;
|
|
904
939
|
}
|
|
905
|
-
const text = await resp.text();
|
|
906
940
|
throwApiError(resp.status, text, requestId);
|
|
907
941
|
}
|
|
908
942
|
const arrayBuf = await resp.arrayBuffer();
|
|
@@ -914,7 +948,10 @@ export async function apiRawCall(path, requestTimeoutMs) {
|
|
|
914
948
|
catch (err) {
|
|
915
949
|
lastError = err;
|
|
916
950
|
if (attempt < MAX_RETRIES && isTransientError(err)) {
|
|
917
|
-
const
|
|
951
|
+
const errRetryMs = typeof err.retryAfterSec === "number"
|
|
952
|
+
? err.retryAfterSec * 1000
|
|
953
|
+
: undefined;
|
|
954
|
+
const delayMs = retryDelayMs(attempt, errRetryMs);
|
|
918
955
|
logWarn("API retry", {
|
|
919
956
|
rid: requestId,
|
|
920
957
|
path,
|
|
@@ -1059,6 +1096,11 @@ export async function fetchTrainingGuidanceFromApi() {
|
|
|
1059
1096
|
catch (e) {
|
|
1060
1097
|
if (e?.httpStatus === 401 || e?.httpStatus === 403)
|
|
1061
1098
|
throw e;
|
|
1099
|
+
// Prefer last successful payload over a hard failure (TTL may have expired).
|
|
1100
|
+
if (_trainingGuidanceCache?.text) {
|
|
1101
|
+
return (_trainingGuidanceCache.text +
|
|
1102
|
+
"\n\n(Note: API unreachable — showing last cached guidance; may be stale.)");
|
|
1103
|
+
}
|
|
1062
1104
|
return "Could not fetch parameter guidance. Check API key and connectivity.";
|
|
1063
1105
|
}
|
|
1064
1106
|
}
|
package/dist/tools/account.js
CHANGED
|
@@ -46,7 +46,7 @@ export function registerAccountTool(server) {
|
|
|
46
46
|
action=status: Returns plan tier, compute class (CPU/GPU), usage limits, live queue state, training time estimates, and credit balance.
|
|
47
47
|
Use BEFORE large jobs to check GPU availability and estimate wait time.
|
|
48
48
|
When the API/gateway is down, returns structured \`{ok:false, retry_after_sec, …}\` — treat as a pause, not a crash.
|
|
49
|
-
action=inventory:
|
|
49
|
+
action=inventory: One-shot markdown overview — all datasets (slim) plus the latest N jobs (limit default 50). Not paginated for datasets; for more jobs use jobs(action=list, cursor=next_cursor, limit=…). Prefer this over hand-assembling datasets(list)+jobs(list).
|
|
50
50
|
action=health: Lightweight liveness/readiness (no R2). Use when tools return api_unreachable or before retrying deferred send_feedback.
|
|
51
51
|
Capacity is shared LOCAL/GKE worker pools — there is no per-key cloud burst lease.
|
|
52
52
|
NOT FOR: Training itself — use train(action=map). This tool only manages the account.`, {
|
|
@@ -4,7 +4,9 @@ import { registerAuditedTool, runMcpToolAudit } from "../audit.js";
|
|
|
4
4
|
import { apiCall, apiRawCall, BARSOM_VIZ_VIEWS, buildStandaloneVizPageUrl, buildVizRelatedUrls, getVizPort, getCaptionForImage, getResultsImagesToFetch, mimeForFilename, structuredTextResult, tryAttachImage, } from "../shared.js";
|
|
5
5
|
import { buildVizStandaloneUrl } from "../viz_links.js";
|
|
6
6
|
import { appendUiDeliveryContent, beginInlineFallback, buildStandaloneVizUrl, resolveUiDelivery, } from "../ui-delivery.js";
|
|
7
|
+
import { figureSection, sortFiguresBySection } from "../figure_sections.js";
|
|
7
8
|
export const RESULTS_EXPLORER_URI = "ui://barsom/results-explorer";
|
|
9
|
+
export { FIGURE_SECTION_LABELS, FIGURE_SECTION_ORDER, figureSection } from "../figure_sections.js";
|
|
8
10
|
function labelForFigure(filename) {
|
|
9
11
|
const base = filename.replace(/\.(png|pdf|svg)$/i, "");
|
|
10
12
|
if (base === "combined")
|
|
@@ -34,7 +36,7 @@ function buildAvailableFigures(jobId, summary) {
|
|
|
34
36
|
// `_fetch_figure` server-tool bridge (the default figure also arrives as a
|
|
35
37
|
// base64 image via the tool result). The standalone browser page builds its
|
|
36
38
|
// own relative-URL payload separately (see views/results-explorer/app.ts).
|
|
37
|
-
return figureFiles.map((filename) => {
|
|
39
|
+
return sortFiguresBySection(figureFiles.map((filename) => {
|
|
38
40
|
const base = filename.replace(/\.(png|pdf|svg)$/i, "");
|
|
39
41
|
const featureName = base.startsWith("component_")
|
|
40
42
|
? base.replace(/^component_\d+_/, "").replace(/_/g, " ")
|
|
@@ -44,11 +46,11 @@ function buildAvailableFigures(jobId, summary) {
|
|
|
44
46
|
key: base,
|
|
45
47
|
label: labelForFigure(filename),
|
|
46
48
|
filename,
|
|
47
|
-
kind: base
|
|
49
|
+
kind: figureSection(base),
|
|
48
50
|
featureName,
|
|
49
51
|
caption: getCaptionForImage(filename) || (!isRaster ? "This figure is available for download, but it is not inline-displayable in this first iteration." : undefined),
|
|
50
52
|
};
|
|
51
|
-
});
|
|
53
|
+
}));
|
|
52
54
|
}
|
|
53
55
|
function buildMetrics(summary) {
|
|
54
56
|
const jobType = String(summary.job_type ?? "train_som");
|
|
@@ -164,6 +166,7 @@ async function handleResultsExplorer(job_id) {
|
|
|
164
166
|
jobId: job_id,
|
|
165
167
|
jobStatus: "completed",
|
|
166
168
|
inlineImageAttached,
|
|
169
|
+
uiResourceUri: RESULTS_EXPLORER_URI,
|
|
167
170
|
});
|
|
168
171
|
appendUiDeliveryContent(content, uiDelivery, {
|
|
169
172
|
linkLabel: "Open results explorer",
|
|
@@ -178,7 +181,7 @@ async function handleResultsExplorer(job_id) {
|
|
|
178
181
|
}
|
|
179
182
|
return {
|
|
180
183
|
...structuredTextResult({ ...payload, ui_delivery: uiDelivery }, undefined, content),
|
|
181
|
-
_meta: { ui: { resourceUri: RESULTS_EXPLORER_URI } },
|
|
184
|
+
_meta: { ui: { resourceUri: RESULTS_EXPLORER_URI, visibility: ["app"] } },
|
|
182
185
|
};
|
|
183
186
|
}
|
|
184
187
|
export function registerExploreMapTool(server) {
|
|
@@ -188,7 +191,7 @@ export function registerExploreMapTool(server) {
|
|
|
188
191
|
inputSchema: {
|
|
189
192
|
job_id: z.string().describe("Job ID of a completed map training job"),
|
|
190
193
|
},
|
|
191
|
-
_meta: { ui: { resourceUri: RESULTS_EXPLORER_URI } },
|
|
194
|
+
_meta: { ui: { resourceUri: RESULTS_EXPLORER_URI, visibility: ["app"] } },
|
|
192
195
|
};
|
|
193
196
|
registerAppTool(server, "results_explorer", toolConfig, async (args) => runMcpToolAudit("results_explorer", "default", args, () => handleResultsExplorer(String(args.job_id))));
|
|
194
197
|
// NOTE: this MUST stay a registered tool — the Results Explorer MCP App view
|
package/dist/tools/feedback.js
CHANGED
|
@@ -23,7 +23,7 @@ export function registerFeedbackTool(server) {
|
|
|
23
23
|
|
|
24
24
|
For a substantial issue, prefer feedback_items: submit several focused instances (each max 1400 chars) covering, e.g., symptoms, exact reproduction steps, environment, and concrete asks — they are stored together as one batch so developers see the full picture. Use the single feedback field for a short one-off note.
|
|
25
25
|
|
|
26
|
-
If the API or object storage is unavailable, this tool queues feedback locally (~/.barivia/deferred-feedback) and returns deferred=true with request_id / api_health — call again later to flush. Probe API liveness with account(action=health) (GET /health; does not touch R2).`, {
|
|
26
|
+
If the API or object storage is unavailable, this tool queues feedback locally (~/.barivia/deferred-feedback) and returns deferred=true with request_id / api_health — call again later to flush (idempotent enqueue of the same draft). Probe API liveness with account(action=health) (GET /health; does not touch R2).`, {
|
|
27
27
|
feedback: z.string().max(1400).optional().describe("Single feedback note (max 1400 characters). Use feedback_items instead for a multi-part report."),
|
|
28
28
|
feedback_items: z.array(z.string().max(1400)).max(10).optional().describe("Several feedback instances (each max 1400 characters, up to 10), stored together as one batch. Prefer this for a detailed issue: split it into focused parts (symptoms, reproduction, environment, asks)."),
|
|
29
29
|
}, async ({ feedback, feedback_items }) => {
|
|
@@ -104,5 +104,10 @@ If the API or object storage is unavailable, this tool queues feedback locally (
|
|
|
104
104
|
}
|
|
105
105
|
throw err;
|
|
106
106
|
}
|
|
107
|
+
}, {
|
|
108
|
+
readOnlyHint: false,
|
|
109
|
+
destructiveHint: false,
|
|
110
|
+
idempotentHint: true,
|
|
111
|
+
openWorldHint: true,
|
|
107
112
|
});
|
|
108
113
|
}
|
package/dist/tools/jobs.js
CHANGED
|
@@ -24,13 +24,13 @@ ASYNC POLLING PROTOCOL (action=monitor):
|
|
|
24
24
|
- Optional UI: training_monitor(job_id) for embedded charts; jobs(action=monitor) is the headless equivalent.
|
|
25
25
|
|
|
26
26
|
ASYNC POLLING PROTOCOL (action=status):
|
|
27
|
-
- One-shot check only. If not using monitor, poll every 10-15 seconds manually.
|
|
27
|
+
- One-shot check only. If not using monitor, poll every 10-15 seconds manually — do not give up after one or two polls; large jobs need patience.
|
|
28
28
|
- Progress increases through phases (ordering then convergence for map training); other job types differ.
|
|
29
29
|
- For large grids (40×40+), do not assume failure before 3 minutes on CPU.
|
|
30
30
|
- Map training typical times: 10×10 ~30s | 20×20 ~3–5 min | 40×40 ~15–30 min.
|
|
31
31
|
- completed → results(action=get); failed → read error + optional failure_stage (preprocessing/training/metrics/visualization/upload): memory error → smaller grid/batch and retrain; column missing → datasets(action=preview); NaN on a standard map → train(action=impute) for sparse columns or clean the CSV.
|
|
32
32
|
|
|
33
|
-
action=list: Slim rows by default (label,id,status,job_type,dataset_id,created_at,result_ref,description,tags — no fat params). Use status/job_type/has_results/limit/cursor to page. has_results=true is the results catalog. Pass fields="full" only when you need params/error.
|
|
33
|
+
action=list: Slim rows by default (label,id,status,job_type,dataset_id,created_at,result_ref,description,tags — no fat params). Use status/job_type/has_results/limit/cursor to page; when the response includes next_cursor, call again with cursor=that value. has_results=true is the results catalog. Pass fields="full" only when you need params/error.
|
|
34
34
|
action=update: PATCH label/description/tags (pass at train submit when possible).
|
|
35
35
|
action=compare: metrics table (QE, TE, explained variance, silhouette) for 2+ COMPLETED TRAINING jobs — pick the best run in a sweep. NOT inference(action=compare) (single trained job + second dataset → density-diff heatmap). Only hyperparameter columns that differ across runs are shown (including SIOM knobs gamma/gamma_f/siom_decay/siom_penalty/reset_per_epoch when they vary); pass label="…" at train time for readable rows.
|
|
36
36
|
action=cancel: Not instant — worker checks between phases. Expect up to 30s delay.
|
package/dist/tools/results.js
CHANGED
|
@@ -19,7 +19,8 @@ Temporal state-transition arrows moved to **inference(action=transition_flow)**
|
|
|
19
19
|
ONLY call this after jobs(action=status) returns "completed".
|
|
20
20
|
ESCALATION: If job not found, verify job_id. If "job not complete", poll with jobs(action=status).
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
Job states (for async parents): pending → running → completed | failed | cancelled (finalize_training may run after compute completes — wait until status=completed before get).
|
|
23
|
+
action=get: Returns text summary with quality metrics and inline images; structuredContent includes a slim verification block (status, file_count, has_summary).
|
|
23
24
|
- figures: omit = combined only. Prefer string "all" / "none" (not an array). "all" = all published plots (excludes unpublished cyclic cos/sin expansions unless train used viz_upload_cyclic_components=true). "none" = metrics text only, no images (recommended for sweeps and LLM clients to keep tool payloads small). Array = specific logical names (combined, umatrix, hit_histogram, learning_curve, correlation, divergence_kl, component_1..N) or download filenames. Hosts that stringify arrays are coerced. Request unpublished cyclic planes via results(recolor, figures=[component_N, ...]).
|
|
24
25
|
- include_individual: if true (and figures omitted), inlines every component plane.
|
|
25
26
|
- After recolor or project, use the job_id returned by that operation to get the new artifacts, not the original training job_id.
|
|
@@ -478,6 +479,13 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
|
|
|
478
479
|
summary,
|
|
479
480
|
files,
|
|
480
481
|
figures_inlined: [...inlinedImages],
|
|
482
|
+
verification: {
|
|
483
|
+
status: "completed",
|
|
484
|
+
has_summary: Object.keys(summary).length > 0,
|
|
485
|
+
file_count: files.length,
|
|
486
|
+
figures_inlined_count: inlinedImages.size,
|
|
487
|
+
job_type: jobType2,
|
|
488
|
+
},
|
|
481
489
|
};
|
|
482
490
|
const summaryText = content.filter((c) => c.type === "text").map((c) => c.text).join("\n\n");
|
|
483
491
|
return structuredTextResult(structuredPayload, summaryText, content);
|
package/dist/tools/train.js
CHANGED
|
@@ -15,7 +15,7 @@ export const TRAIN_DESCRIPTION = `Submit a self-organizing map training job. Ret
|
|
|
15
15
|
|
|
16
16
|
Call **guide_barsom_workflow**, **training_guidance**, and/or the **prepare_training** prompt for plan-scoped parameter details, timing heuristics, quality.csv semantics, SIOM/FLooP knobs, and impute constraints — do not guess. Backend: "cpu" | "gpu" | "gpu_graphs" (on CPU-only hosts pass backend=cpu).
|
|
17
17
|
|
|
18
|
-
ASYNC: every action returns a job_id immediately.
|
|
18
|
+
ASYNC: every action returns a job_id immediately. Prefer jobs(action=monitor) to block with snapshots, or poll jobs(action=status) every 10–15s (not faster) — do not abandon after one poll. When completed, call results(action=get). Timing and quality heuristics: **training_guidance**.
|
|
19
19
|
NOT FOR: lifecycle → jobs; scoring → inference.
|
|
20
20
|
Rank transform (map/siom/floop only) is train-only — see training_guidance.`;
|
|
21
21
|
function assertImputeTrainingArgs(args) {
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
|
|
3
3
|
import { runMcpToolAudit } from "../audit.js";
|
|
4
|
+
import { logWarn } from "../logger.js";
|
|
4
5
|
import { apiCall, BARSOM_VIZ_VIEWS, buildStandaloneVizPageUrl, buildVizRelatedUrls, getVizPort, structuredTextResult, } from "../shared.js";
|
|
6
|
+
/** Loose shape check — warn + pass through on failure (never drop a successful monitor payload). */
|
|
7
|
+
const TrainingMonitorStructuredSchema = z
|
|
8
|
+
.object({
|
|
9
|
+
type: z.literal("training-monitor"),
|
|
10
|
+
jobId: z.string(),
|
|
11
|
+
refresh_interval_ms: z.number(),
|
|
12
|
+
})
|
|
13
|
+
.passthrough();
|
|
5
14
|
import { appendUiDeliveryContent, beginInlineFallback, resolveUiDelivery, tryAttachLearningCurve, } from "../ui-delivery.js";
|
|
6
15
|
import { lastEpochTeFromCurves } from "../training_monitor_curve.js";
|
|
7
16
|
import { buildVizStandaloneUrl, resultsExplorerStandaloneLinkText, } from "../viz_links.js";
|
|
@@ -91,10 +100,11 @@ async function enrichWithTrainingLog(job_id, data) {
|
|
|
91
100
|
}
|
|
92
101
|
}
|
|
93
102
|
export async function buildTrainingMonitorResult(job_id, opts) {
|
|
94
|
-
const {
|
|
103
|
+
const { preamble } = opts ?? {};
|
|
95
104
|
let data = (await apiCall("GET", `/v1/jobs/${job_id}`));
|
|
96
105
|
const jobStatus = String(data.status ?? "");
|
|
97
|
-
|
|
106
|
+
// Enrich terminal jobs for curve arrays (App refresh uses job_id only — no public flag).
|
|
107
|
+
if (needsTrainingLogEnrichment(data) || ["completed", "failed", "cancelled"].includes(jobStatus)) {
|
|
98
108
|
data = await enrichWithTrainingLog(job_id, data);
|
|
99
109
|
}
|
|
100
110
|
const port = getVizPort();
|
|
@@ -114,8 +124,8 @@ export async function buildTrainingMonitorResult(job_id, opts) {
|
|
|
114
124
|
timingParts.push(`epoch ${epoch}/${totalEpochs}`);
|
|
115
125
|
const timingNote = timingParts.length > 0 ? ` ${timingParts.join(", ")}.` : "";
|
|
116
126
|
const text = (preamble ? `${preamble}\n\n` : "") +
|
|
117
|
-
`Training monitor (
|
|
118
|
-
`
|
|
127
|
+
`Training monitor (standalone URL + optional App panel; refreshes every ${TRAINING_MONITOR_REFRESH_MS / 1000}s when mounted): job ${job_id} — ${status} (${progress.toFixed(1)}%).${timingNote} ` +
|
|
128
|
+
`For headless progress use jobs(action=status); this tool is optional.`;
|
|
119
129
|
const content = [{ type: "text", text }];
|
|
120
130
|
const terminal = status === "completed" || status === "failed" || status === "cancelled";
|
|
121
131
|
let inlineImageAttached = false;
|
|
@@ -127,6 +137,7 @@ export async function buildTrainingMonitorResult(job_id, opts) {
|
|
|
127
137
|
jobId: job_id,
|
|
128
138
|
jobStatus: status,
|
|
129
139
|
inlineImageAttached,
|
|
140
|
+
uiResourceUri: TRAINING_MONITOR_URI,
|
|
130
141
|
});
|
|
131
142
|
appendUiDeliveryContent(content, uiDelivery, {
|
|
132
143
|
linkLabel: "Open training monitor",
|
|
@@ -142,6 +153,13 @@ export async function buildTrainingMonitorResult(job_id, opts) {
|
|
|
142
153
|
}
|
|
143
154
|
}
|
|
144
155
|
structuredContent.ui_delivery = uiDelivery;
|
|
156
|
+
const parsed = TrainingMonitorStructuredSchema.safeParse(structuredContent);
|
|
157
|
+
if (!parsed.success) {
|
|
158
|
+
logWarn("training_monitor structuredContent validation failed — passing through", {
|
|
159
|
+
job_id,
|
|
160
|
+
issues: parsed.error.issues.slice(0, 5).map((i) => i.message),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
145
163
|
return { structuredContent, content, text };
|
|
146
164
|
}
|
|
147
165
|
/** After terminal + finalize, append results explorer open hint (monitor completion path). */
|
|
@@ -158,24 +176,18 @@ export function appendResultsExplorerCompletionHint(content, job_id) {
|
|
|
158
176
|
export function registerTrainingMonitorTool(server) {
|
|
159
177
|
registerAppTool(server, "training_monitor", {
|
|
160
178
|
title: "Training Monitor",
|
|
161
|
-
description: "Optional
|
|
179
|
+
description: "Optional view of training progress for a job_id. Prefer the standalone localhost URL in the tool result (always surfaced when the viz server is up) — some hosts list MCP Apps without mounting panels. When a host mounts the App, the panel auto-refreshes every 5s: QE/TE curves (≤1000 batch samples per phase) plus a hit-distribution heatmap. Set BARIVIA_VIZ_PORT for a stable port; re-run for a fresh link after proxy restart. Headless: jobs(action=status) has the same scalars — not required to finish training or read results.",
|
|
162
180
|
inputSchema: {
|
|
163
181
|
job_id: z.string().describe("Training job ID to monitor"),
|
|
164
|
-
fetch_training_log: z
|
|
165
|
-
.boolean()
|
|
166
|
-
.optional()
|
|
167
|
-
.describe("Internal: merge completed-job training-log arrays when live progress is empty"),
|
|
168
182
|
},
|
|
169
|
-
_meta: { ui: { resourceUri: TRAINING_MONITOR_URI } },
|
|
183
|
+
_meta: { ui: { resourceUri: TRAINING_MONITOR_URI, visibility: ["model", "app"] } },
|
|
170
184
|
}, async (args) => runMcpToolAudit("training_monitor", "default", args, async () => {
|
|
171
185
|
beginInlineFallback();
|
|
172
|
-
const { job_id
|
|
173
|
-
const { structuredContent, content, text } = await buildTrainingMonitorResult(job_id
|
|
174
|
-
fetch_training_log,
|
|
175
|
-
});
|
|
186
|
+
const { job_id } = args;
|
|
187
|
+
const { structuredContent, content, text } = await buildTrainingMonitorResult(job_id);
|
|
176
188
|
return {
|
|
177
189
|
...structuredTextResult(structuredContent, text, content),
|
|
178
|
-
_meta: { ui: { resourceUri: TRAINING_MONITOR_URI } },
|
|
190
|
+
_meta: { ui: { resourceUri: TRAINING_MONITOR_URI, visibility: ["model", "app"] } },
|
|
179
191
|
};
|
|
180
192
|
}));
|
|
181
193
|
}
|