@barivia/barmesh-mcp 0.5.1 → 0.5.3
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 +23 -17
- package/dist/index.js +1 -1
- package/dist/job_monitor.js +208 -0
- package/dist/job_status_format.js +24 -2
- package/dist/shared.js +15 -1
- package/dist/tools/barmesh_results_explorer.js +49 -17
- package/dist/tools/cfd.js +2 -2
- package/dist/tools/guide.js +1 -1
- package/dist/tools/jobs.js +35 -10
- package/dist/tools/results.js +105 -14
- package/dist/tools/training_monitor.js +56 -0
- package/dist/views/src/views/barmesh-results-explorer/index.html +33 -29
- package/package.json +1 -1
package/dist/tools/results.js
CHANGED
|
@@ -1,32 +1,44 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import { z } from "zod";
|
|
2
4
|
import { registerAuditedTool } from "../audit.js";
|
|
3
|
-
import { apiCall, apiRawCall, textResult, tryAttachImage, pollUntilComplete, POLL_STAGE_MAX_MS } from "../shared.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import { apiCall, apiRawCall, getWorkspaceRootAsync, sandboxPath, textResult, tryAttachImage, pollUntilComplete, POLL_STAGE_MAX_MS, } from "../shared.js";
|
|
6
|
+
const MESH_DEFAULT_FIGURES = ["combined", "overview_distances", "KL_ref", "EMD_ref", "plot_vol_coarse_fine", "learning_curve"];
|
|
7
|
+
const TEXT_ARTIFACTS = [
|
|
8
|
+
"summary.json",
|
|
9
|
+
"distances.csv",
|
|
10
|
+
"training_metrics.txt",
|
|
11
|
+
"distances_to_ref.txt",
|
|
12
|
+
"emd_stepwise.txt",
|
|
13
|
+
"cfd_metrics.json",
|
|
14
|
+
];
|
|
7
15
|
export function registerResultsTool(server) {
|
|
8
16
|
registerAuditedTool(server, "barmesh_results", `Fetch results of a completed CFD job: distances, convergence reading, and figures.
|
|
9
17
|
|
|
10
18
|
| Action | Use when |
|
|
11
19
|
|--------|----------|
|
|
12
20
|
| get | Read the summary (distances per mesh, stepwise, convergence reading) and inline key figures. |
|
|
13
|
-
| image | Download one figure by filename (e.g. KL_ref.png,
|
|
21
|
+
| image | Download one figure by filename (e.g. KL_ref.png, combined.pdf). |
|
|
14
22
|
| render | Re-render the figures as publication PDFs (or SVG) on demand — PDFs are NOT generated by default. |
|
|
23
|
+
| download | Save figures and metrics to a local folder (headless / agent path). |
|
|
15
24
|
|
|
16
|
-
BEST FOR: After barmesh_jobs(action=status) shows completed.
|
|
17
|
-
FIGURES: Default
|
|
18
|
-
PDFs ON DEMAND: vector PDFs are not produced by default. Use action=render (format=pdf) once, then action=image
|
|
25
|
+
BEST FOR: After barmesh_jobs(action=status) shows completed (and finalize finished if defer_figures was used).
|
|
26
|
+
FIGURES: Default output includes combined.png (component-plane mosaic), overview_distances.png, learning_curve.png, and per-panel PNGs. action=get inlines headline panels; pass figures="all" for every PNG, "none" for metrics only.
|
|
27
|
+
PDFs ON DEMAND: vector PDFs are not produced by default. Use action=render (format=pdf) once, then action=image or action=download.
|
|
28
|
+
If GET /v1/results returns 404 shortly after status=completed, cfd_finalize may still be rendering — poll barmesh_jobs(status) until finalize_job_id completes.
|
|
19
29
|
NOT FOR: Submitting jobs.`, {
|
|
20
|
-
action: z.enum(["get", "image", "render"]).describe("get: summary + figures; image:
|
|
30
|
+
action: z.enum(["get", "image", "render", "download"]).describe("get: summary + figures; image: one file; render: lazy PDF/SVG; download: save to disk"),
|
|
21
31
|
job_id: z.string().describe("Job ID"),
|
|
22
32
|
filename: z.string().optional().describe("For action=image: the figure filename (e.g. KL_ref.png or, after render, KL_ref.pdf)"),
|
|
23
33
|
format: z.enum(["pdf", "png", "svg"]).optional().describe("For action=render: output format to generate (default pdf). PNG previews already exist by default."),
|
|
24
34
|
figures: z
|
|
25
35
|
.union([z.enum(["default", "all", "none"]), z.array(z.string())])
|
|
26
36
|
.optional()
|
|
27
|
-
.describe("For action=get: which figures
|
|
37
|
+
.describe("For action=get/download: which figures (default headline panels; 'all'; 'none'; or a list of base names)"),
|
|
38
|
+
folder: z.string().optional().describe("For action=download: directory to save into (relative to workspace). Files land in a per-job subfolder."),
|
|
39
|
+
include_json: z.boolean().optional().describe("For action=download: also save summary.json and text/CSV artifacts"),
|
|
28
40
|
}, async (args) => {
|
|
29
|
-
const { action, job_id, filename, format, figures } = args;
|
|
41
|
+
const { action, job_id, filename, format, figures, folder, include_json } = args;
|
|
30
42
|
if (action === "render") {
|
|
31
43
|
const fmt = format ?? "pdf";
|
|
32
44
|
const submit = (await apiCall("POST", "/v1/cfd/render", { job_id, format: fmt }));
|
|
@@ -44,7 +56,7 @@ NOT FOR: Submitting jobs.`, {
|
|
|
44
56
|
status: "completed",
|
|
45
57
|
render_job_id: renderId,
|
|
46
58
|
format: fmt,
|
|
47
|
-
suggested_next_step: `Figures rendered as ${fmt}. Download
|
|
59
|
+
suggested_next_step: `Figures rendered as ${fmt}. Download with barmesh_results(action=download, job_id="${job_id}") or fetch one file with action=image.`,
|
|
48
60
|
});
|
|
49
61
|
}
|
|
50
62
|
if (action === "image") {
|
|
@@ -61,7 +73,87 @@ NOT FOR: Submitting jobs.`, {
|
|
|
61
73
|
}
|
|
62
74
|
return textResult(`${filename} is ${contentType}; saved out-of-band. For vector PDFs, open the downloaded file directly.`);
|
|
63
75
|
}
|
|
64
|
-
|
|
76
|
+
if (action === "download") {
|
|
77
|
+
if (!folder)
|
|
78
|
+
throw new Error("barmesh_results(download) requires folder");
|
|
79
|
+
let data;
|
|
80
|
+
try {
|
|
81
|
+
data = (await apiCall("GET", `/v1/results/${job_id}`));
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
85
|
+
if (/404|not found/i.test(msg)) {
|
|
86
|
+
return textResult({
|
|
87
|
+
error: "results_not_ready",
|
|
88
|
+
job_id,
|
|
89
|
+
note: `Results not found yet. If barmesh_jobs(status) shows completed with a finalize_job_id, poll status until finalize completes, then retry download.`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
const summary = (data.summary ?? data);
|
|
95
|
+
const jobLabel = data.label != null && data.label !== "" ? String(data.label) : null;
|
|
96
|
+
const files = summary.files ?? [];
|
|
97
|
+
const isImage = (f) => /\.(png|svg|pdf)$/i.test(f);
|
|
98
|
+
let toDownload;
|
|
99
|
+
if (figures === "all" || figures === undefined) {
|
|
100
|
+
toDownload = include_json ? files : files.filter(isImage);
|
|
101
|
+
}
|
|
102
|
+
else if (Array.isArray(figures)) {
|
|
103
|
+
toDownload = figures.flatMap((key) => {
|
|
104
|
+
if (/\.(png|pdf|svg|csv|txt|json)$/i.test(key))
|
|
105
|
+
return [key];
|
|
106
|
+
return files.filter((f) => f.replace(/\.[^.]+$/, "") === key || f === key);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
else if (figures === "none") {
|
|
110
|
+
toDownload = TEXT_ARTIFACTS.filter((f) => files.includes(f));
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
toDownload = MESH_DEFAULT_FIGURES.flatMap((b) => files.filter((f) => f.startsWith(`${b}.`)));
|
|
114
|
+
}
|
|
115
|
+
if (include_json) {
|
|
116
|
+
for (const t of TEXT_ARTIFACTS) {
|
|
117
|
+
if (files.includes(t) && !toDownload.includes(t))
|
|
118
|
+
toDownload.push(t);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
toDownload = [...new Set(toDownload)];
|
|
122
|
+
let resolvedDir = sandboxPath(folder, await getWorkspaceRootAsync(server));
|
|
123
|
+
const jobSubfolder = `cfd_mesh_convergence_${jobLabel ?? job_id}`.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
124
|
+
resolvedDir = path.join(resolvedDir, jobSubfolder);
|
|
125
|
+
await fs.mkdir(resolvedDir, { recursive: true });
|
|
126
|
+
const saved = [];
|
|
127
|
+
for (const fname of toDownload) {
|
|
128
|
+
try {
|
|
129
|
+
const { data: buf } = await apiRawCall(`/v1/results/${job_id}/image/${fname}`);
|
|
130
|
+
await fs.writeFile(path.join(resolvedDir, fname), buf);
|
|
131
|
+
saved.push(fname);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
/* skip missing */
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const savedDir = path.join(folder, jobSubfolder);
|
|
138
|
+
return textResult(saved.length > 0
|
|
139
|
+
? `Saved ${saved.length} file(s) to ${savedDir}: ${saved.join(", ")}`
|
|
140
|
+
: `No files saved. Check job_id and that the job (and cfd_finalize, if any) is completed.`);
|
|
141
|
+
}
|
|
142
|
+
let summary;
|
|
143
|
+
try {
|
|
144
|
+
summary = (await apiCall("GET", `/v1/results/${job_id}`));
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
148
|
+
if (/404|not found/i.test(msg)) {
|
|
149
|
+
return textResult({
|
|
150
|
+
error: "results_not_ready",
|
|
151
|
+
job_id,
|
|
152
|
+
note: `Results not available yet (HTTP 404). Compute may be done but cfd_finalize is still rendering figures — poll barmesh_jobs(action=status, job_id="${job_id}") until finalize_job_id completes, then retry barmesh_results(action=get).`,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
throw err;
|
|
156
|
+
}
|
|
65
157
|
const content = [{ type: "text", text: JSON.stringify(summary, null, 2) }];
|
|
66
158
|
if (figures === "none")
|
|
67
159
|
return { content };
|
|
@@ -82,7 +174,6 @@ NOT FOR: Submitting jobs.`, {
|
|
|
82
174
|
.filter((f) => f != null);
|
|
83
175
|
}
|
|
84
176
|
else {
|
|
85
|
-
// default headline panels (PNG) when present
|
|
86
177
|
toFetch = MESH_DEFAULT_FIGURES.map((b) => `${b}.png`).filter((f) => imageFiles.includes(f));
|
|
87
178
|
}
|
|
88
179
|
for (const f of toFetch) {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { registerAuditedTool } from "../audit.js";
|
|
3
|
+
import { textResult } from "../shared.js";
|
|
4
|
+
import { DEFAULT_BLOCK_UNTIL_SEC, DEFAULT_POLL_INTERVAL_SEC, formatMonitorText, monitorJob, } from "../job_monitor.js";
|
|
5
|
+
const MONITOR_DESCRIPTION = `Block until a barmesh_mesh_convergence or barmesh_richardson job reaches a terminal state, emitting compact progress snapshots along the way.
|
|
6
|
+
|
|
7
|
+
BEST FOR: After job submit — one call replaces manual barmesh_jobs(status) poll loops (10–20s × several minutes).
|
|
8
|
+
ASYNC PROTOCOL: Server-side poll every ~5s (configurable) until completed/failed/cancelled or block_until timeout. Snapshots include phase, epoch/total, QE/TE, ETA, and ordering_errors tail when live.
|
|
9
|
+
FINALIZE: When defer_figures is used, waits for cfd_finalize and adds a snapshot when figure render starts/completes.
|
|
10
|
+
NOT REQUIRED: barmesh_jobs(action=status) still works for one-shot checks; this tool is the agent-friendly blocking variant (barsom training_monitor parity for headless workflows).
|
|
11
|
+
ESCALATION: On timeout, re-run with a higher block_until_sec; on failed, read failure_stage in the final snapshot.`;
|
|
12
|
+
const monitorSchema = {
|
|
13
|
+
job_id: z.string().describe("Job ID from barmesh_mesh_convergence or barmesh_richardson"),
|
|
14
|
+
block_until_sec: z
|
|
15
|
+
.number()
|
|
16
|
+
.int()
|
|
17
|
+
.min(30)
|
|
18
|
+
.optional()
|
|
19
|
+
.describe(`Max seconds to wait (default ${DEFAULT_BLOCK_UNTIL_SEC}; mesh jobs often need 6–10 min)`),
|
|
20
|
+
poll_interval_sec: z
|
|
21
|
+
.number()
|
|
22
|
+
.int()
|
|
23
|
+
.min(5)
|
|
24
|
+
.optional()
|
|
25
|
+
.describe(`Seconds between status polls (default ${DEFAULT_POLL_INTERVAL_SEC}; do not go below 5)`),
|
|
26
|
+
wait_finalize: z
|
|
27
|
+
.boolean()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("When true (default), wait for cfd_finalize after compute completes before returning"),
|
|
30
|
+
};
|
|
31
|
+
async function runMonitor(args) {
|
|
32
|
+
const block_until_sec = args.block_until_sec ?? DEFAULT_BLOCK_UNTIL_SEC;
|
|
33
|
+
const poll_interval_sec = args.poll_interval_sec ?? DEFAULT_POLL_INTERVAL_SEC;
|
|
34
|
+
const result = await monitorJob(args.job_id, {
|
|
35
|
+
block_until_sec,
|
|
36
|
+
poll_interval_sec,
|
|
37
|
+
wait_finalize: args.wait_finalize,
|
|
38
|
+
});
|
|
39
|
+
const text = formatMonitorText(result, { block_until_sec, poll_interval_sec });
|
|
40
|
+
return textResult({
|
|
41
|
+
...result.data,
|
|
42
|
+
monitor: {
|
|
43
|
+
job_id: result.job_id,
|
|
44
|
+
terminal: result.terminal,
|
|
45
|
+
timed_out: result.timed_out,
|
|
46
|
+
snapshots: result.snapshots,
|
|
47
|
+
status_text: result.status_text,
|
|
48
|
+
suggested_next_step: result.suggested_next_step,
|
|
49
|
+
},
|
|
50
|
+
status_text: text,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export function registerTrainingMonitorTool(server) {
|
|
54
|
+
registerAuditedTool(server, "barmesh_training_monitor", MONITOR_DESCRIPTION, monitorSchema, runMonitor);
|
|
55
|
+
}
|
|
56
|
+
export { runMonitor, MONITOR_DESCRIPTION, monitorSchema };
|