@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.
@@ -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
- // Headline figures when present, best first: the combined overview (barsom parity),
5
- // then KL/EMD-vs-reference and the coarse/fine volume pair.
6
- const MESH_DEFAULT_FIGURES = ["combined", "KL_ref", "EMD_ref", "plot_vol_coarse_fine"];
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, EMD_stepwise.pdf). |
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 job output is metrics + PNG previews, including a single combined.png overview. action=get inlines the combined headline plus KL_ref/EMD_ref/volume PNGs with captions; pass figures="all" for every PNG, "none" for metrics only, or a list of names.
18
- PDFs ON DEMAND: vector PDFs are not produced by default. Use action=render (format=pdf) once, then action=image to download each <name>.pdf. For an interactive browser use barmesh_results_explorer.
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: download one figure file; render: re-render figures as PDF/SVG on demand"),
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 to inline (default headline panels; 'all'; 'none'; or a list of names)"),
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 one with barmesh_results(action=image, job_id="${job_id}", filename="KL_ref.${fmt}"), or list filenames via barmesh_results(action=get, job_id="${job_id}").`,
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
- const summary = (await apiCall("GET", `/v1/results/${job_id}`));
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 };