@barivia/barsom-mcp 0.19.0 → 0.20.0
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 -1
- package/dist/job_monitor.js +230 -0
- package/dist/shared.js +1 -1
- package/dist/tools/jobs.js +34 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -180,6 +180,10 @@ The right viewer depends on **(MCP App support)** **and** **(can the human reach
|
|
|
180
180
|
|
|
181
181
|
### Migration notes
|
|
182
182
|
|
|
183
|
+
- **Features (0.20.0, non-breaking):**
|
|
184
|
+
- **FLooP-SIOM maps are now projectable.** `inference(action=project_columns)`, `inference(action=predict)`, and `datasets(action=add_expression, project_onto_job=<floop_job>)` work on FLooP-SIOM (free/chain) maps — values render onto the FLooP Voronoi layout instead of failing. Grid-only ops (`inference(action=transition_flow | impute_column | render_variant)`) return a clear `unsupported_topology_for_inference` message; use a fixed-grid SOM/SIOM for those. No client change required.
|
|
185
|
+
- **`datasets(action=reduce_spectral)` column selectors.** New optional `columns_range` (`[first, last]`, the server expands the inclusive numeric block — preferred for wide spectra over sending thousands of names) and `columns_except` (drop non-frequency columns like id/label/target from the block, or use alone for "all numeric except these"). `columns_except` composes with `columns_block` or `columns_range`; `columns_block` and `columns_range` are mutually exclusive. Existing `columns_block` callers are unaffected.
|
|
186
|
+
- Minimum dataset size for train/analyze relaxed from 50 to **10 rows** (10–49 allowed with a small-N stability caveat).
|
|
183
187
|
- **Surface cleanup (0.19.0, breaking):**
|
|
184
188
|
- `training_prep` and `submit_prepared_training` **removed**. The interactive prep UI is retired; for pre-train help use the `prepare_training` prompt (narrative checklist) or `training_guidance` (presets + resolved per-column normalization), then submit directly with `train(action=map | siom_map | impute | floop_siom)`.
|
|
185
189
|
- The unused `data-preview` and `map-explorer` view scaffolds were deleted (they were never wired to a tool). The two shipped MCP App UIs are now `training_monitor` and `results_explorer`, both enhanced (auto-resize, earlier render, and silent model-context sync of what the user is viewing).
|
|
@@ -227,7 +231,7 @@ MCP Client (Cursor/Claude) ←stdio→ @barivia/barsom-mcp ←HTTPS→ api.bariv
|
|
|
227
231
|
| `401` / invalid key | `BARIVIA_API_KEY` in MCP config; check your email or manage your key in the [account dashboard](https://barivia.se/dashboard). Error text includes a **request id** for support. |
|
|
228
232
|
| Request timed out | Raise `BARIVIA_FETCH_TIMEOUT_MS` (e.g. `120000`). Large uploads already use an extended timeout; `datasets(analyze)` is async (auto-polled). Timeouts are not auto-retried, so re-running a timed-out upload is safe — an idempotency key reconciles it to the original dataset instead of duplicating. |
|
|
229
233
|
| `Path must be within the workspace` / upload can’t find file | Set `BARIVIA_WORKSPACE_ROOT` to your project directory, or use an absolute path / `file:///...` URI. |
|
|
230
|
-
| Job stuck “running” | Poll `jobs(action=status)` every 10–15s; large grids or FLooP-SIOM can take several minutes—not an MCP error. Staged datasets enqueue **`prepare_training_matrix`** (or impute prepare) first — train submit returns `prepare_job_id`; MCP auto-polls prepare when present. CFD mesh submit may return `prepare_job_id` for **`
|
|
234
|
+
| Job stuck “running” | Poll `jobs(action=status)` every 10–15s; large grids or FLooP-SIOM can take several minutes—not an MCP error. Staged datasets enqueue **`prepare_training_matrix`** (or impute prepare) first — train submit returns `prepare_job_id`; MCP auto-polls prepare when present. CFD mesh submit may return `prepare_job_id` for **`prepare_training_matrix`** (barmesh auto-polls). |
|
|
231
235
|
| `429` | Rate limit—wait and retry. |
|
|
232
236
|
| Standalone `…/viz/…` page stuck at "running 0%" or "Loading…" | The link is stale: the viz port is assigned per MCP session and changes when the proxy restarts. Re-run `training_monitor` / `results_explorer` for a fresh URL, or set `BARIVIA_VIZ_PORT` for a persistent port. Confirm with `GET /api/health?job_id=<id>`. |
|
|
233
237
|
| Embedded results explorer: switching figures shows nothing | Fixed in `0.18.0` — earlier versions pointed embedded figures at a localhost URL the webview can't load. Update the package; the embedded view now lazy-loads figures over the MCP App bridge. |
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side job monitor for barsom async training workflows.
|
|
3
|
+
* Polls GET /v1/jobs/:id until terminal (or block_until timeout) and emits
|
|
4
|
+
* compact throttled snapshots so agents avoid manual status poll loops.
|
|
5
|
+
*/
|
|
6
|
+
import { apiCall, textResult } from "./shared.js";
|
|
7
|
+
import { formatJobStatusText } from "./job_status_format.js";
|
|
8
|
+
import { pollFinalizeIfPresent, refreshJobAfterFinalize } from "./train_finalize.js";
|
|
9
|
+
export const DEFAULT_BLOCK_UNTIL_SEC = 900;
|
|
10
|
+
export const DEFAULT_POLL_INTERVAL_SEC = 5;
|
|
11
|
+
export const MIN_POLL_INTERVAL_SEC = 5;
|
|
12
|
+
export const HEARTBEAT_POLLS = 1;
|
|
13
|
+
function sleep(ms) {
|
|
14
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
15
|
+
}
|
|
16
|
+
function num(data, key) {
|
|
17
|
+
const v = data[key];
|
|
18
|
+
if (v == null || Number.isNaN(Number(v)))
|
|
19
|
+
return undefined;
|
|
20
|
+
return Number(v);
|
|
21
|
+
}
|
|
22
|
+
function str(data, key) {
|
|
23
|
+
const v = data[key];
|
|
24
|
+
if (v == null || String(v) === "")
|
|
25
|
+
return undefined;
|
|
26
|
+
return String(v);
|
|
27
|
+
}
|
|
28
|
+
function tailOrderingErrors(data, n = 4) {
|
|
29
|
+
const raw = data.ordering_errors;
|
|
30
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
31
|
+
return undefined;
|
|
32
|
+
return raw.slice(-n).map((x) => Number(x)).filter((x) => !Number.isNaN(x));
|
|
33
|
+
}
|
|
34
|
+
export function snapshotFromJob(data, elapsedSec, note) {
|
|
35
|
+
const status = String(data.status ?? "unknown");
|
|
36
|
+
const progress_pct = (data.progress ?? 0) * 100;
|
|
37
|
+
const snap = {
|
|
38
|
+
elapsed_sec: elapsedSec,
|
|
39
|
+
status,
|
|
40
|
+
progress_pct: Math.round(progress_pct * 10) / 10,
|
|
41
|
+
};
|
|
42
|
+
const phase = str(data, "progress_phase");
|
|
43
|
+
if (phase)
|
|
44
|
+
snap.phase = phase;
|
|
45
|
+
const epoch = num(data, "epoch");
|
|
46
|
+
const total = num(data, "total_epochs");
|
|
47
|
+
if (epoch != null)
|
|
48
|
+
snap.epoch = epoch;
|
|
49
|
+
if (total != null)
|
|
50
|
+
snap.total_epochs = total;
|
|
51
|
+
const qe = num(data, "quantization_error");
|
|
52
|
+
const te = num(data, "topographic_error");
|
|
53
|
+
if (qe != null)
|
|
54
|
+
snap.qe = Math.round(qe * 10_000) / 10_000;
|
|
55
|
+
if (te != null)
|
|
56
|
+
snap.te = Math.round(te * 10_000) / 10_000;
|
|
57
|
+
const eta = num(data, "training_eta_sec");
|
|
58
|
+
if (eta != null && eta > 0)
|
|
59
|
+
snap.eta_sec = Math.round(eta);
|
|
60
|
+
const tail = tailOrderingErrors(data);
|
|
61
|
+
if (tail && tail.length > 0)
|
|
62
|
+
snap.ordering_errors_tail = tail;
|
|
63
|
+
if (note)
|
|
64
|
+
snap.note = note;
|
|
65
|
+
return snap;
|
|
66
|
+
}
|
|
67
|
+
/** True when a new snapshot is worth recording (phase/epoch/progress/status change). */
|
|
68
|
+
export function shouldRecordSnapshot(prev, next) {
|
|
69
|
+
if (!prev)
|
|
70
|
+
return true;
|
|
71
|
+
if (prev.status !== next.status)
|
|
72
|
+
return true;
|
|
73
|
+
if (next.note)
|
|
74
|
+
return true;
|
|
75
|
+
if (prev.phase !== next.phase)
|
|
76
|
+
return true;
|
|
77
|
+
if (prev.epoch !== next.epoch)
|
|
78
|
+
return true;
|
|
79
|
+
if (Math.abs(prev.progress_pct - next.progress_pct) >= 1)
|
|
80
|
+
return true;
|
|
81
|
+
if (prev.qe !== next.qe || prev.te !== next.te)
|
|
82
|
+
return true;
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
export function formatSnapshotLine(s) {
|
|
86
|
+
const parts = [`[+${s.elapsed_sec}s] ${s.status} ${s.progress_pct.toFixed(1)}%`];
|
|
87
|
+
if (s.phase)
|
|
88
|
+
parts.push(`phase ${s.phase}`);
|
|
89
|
+
if (s.epoch != null && s.total_epochs != null)
|
|
90
|
+
parts.push(`epoch ${s.epoch}/${s.total_epochs}`);
|
|
91
|
+
if (s.qe != null)
|
|
92
|
+
parts.push(`QE ${s.qe.toFixed(4)}`);
|
|
93
|
+
if (s.te != null)
|
|
94
|
+
parts.push(`TE ${s.te.toFixed(4)}`);
|
|
95
|
+
if (s.eta_sec != null)
|
|
96
|
+
parts.push(`ETA ~${s.eta_sec}s`);
|
|
97
|
+
if (s.ordering_errors_tail?.length) {
|
|
98
|
+
parts.push(`ordering_errors tail [${s.ordering_errors_tail.map((x) => x.toFixed(4)).join(", ")}]`);
|
|
99
|
+
}
|
|
100
|
+
if (s.note)
|
|
101
|
+
parts.push(s.note);
|
|
102
|
+
return parts.join(" | ");
|
|
103
|
+
}
|
|
104
|
+
export function formatMonitorText(result, opts) {
|
|
105
|
+
const lines = [
|
|
106
|
+
`Job ${result.job_id} monitor (block_until=${opts.block_until_sec}s, poll=${opts.poll_interval_sec}s):`,
|
|
107
|
+
];
|
|
108
|
+
for (const s of result.snapshots)
|
|
109
|
+
lines.push(formatSnapshotLine(s));
|
|
110
|
+
lines.push("");
|
|
111
|
+
if (result.timed_out) {
|
|
112
|
+
lines.push(`Timed out before terminal state. Last status: ${result.status_text}`);
|
|
113
|
+
lines.push(`Re-run jobs(action=monitor, job_id="${result.job_id}") or training_monitor(job_id="${result.job_id}").`);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
lines.push(`Terminal: ${result.status_text}`);
|
|
117
|
+
}
|
|
118
|
+
lines.push(result.suggested_next_step);
|
|
119
|
+
return lines.join("\n");
|
|
120
|
+
}
|
|
121
|
+
function suggestedNextStep(job_id, data) {
|
|
122
|
+
const status = String(data.status ?? "");
|
|
123
|
+
if (status === "completed") {
|
|
124
|
+
const finalizeId = str(data, "finalize_job_id");
|
|
125
|
+
if (finalizeId) {
|
|
126
|
+
return `Compute done; finalize_training may still be running. Re-run jobs(action=monitor, job_id="${job_id}") with wait_finalize=true, then results(action=get, job_id="${job_id}").`;
|
|
127
|
+
}
|
|
128
|
+
return `Next: results(action=get, job_id="${job_id}") or results_explorer(job_id="${job_id}").`;
|
|
129
|
+
}
|
|
130
|
+
if (status === "failed") {
|
|
131
|
+
const stage = str(data, "failure_stage");
|
|
132
|
+
return `Job failed${stage ? ` at ${stage}` : ""}. Read the error above before retrying.`;
|
|
133
|
+
}
|
|
134
|
+
if (status === "cancelled")
|
|
135
|
+
return `Job cancelled. Confirm with jobs(action=status, job_id="${job_id}").`;
|
|
136
|
+
return `Still running — re-run jobs(action=monitor, job_id="${job_id}") to continue waiting.`;
|
|
137
|
+
}
|
|
138
|
+
async function fetchTrainingLogHint(job_id, data) {
|
|
139
|
+
const status = String(data.status ?? "");
|
|
140
|
+
if (status !== "completed" && status !== "failed")
|
|
141
|
+
return undefined;
|
|
142
|
+
try {
|
|
143
|
+
await apiCall("GET", `/v1/results/${job_id}/training-log`);
|
|
144
|
+
return `Training log available at GET /v1/results/${job_id}/training-log.`;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export async function monitorJob(job_id, options = {}) {
|
|
151
|
+
const block_until_sec = Math.max(30, options.block_until_sec ?? DEFAULT_BLOCK_UNTIL_SEC);
|
|
152
|
+
const poll_interval_sec = Math.max(MIN_POLL_INTERVAL_SEC, options.poll_interval_sec ?? DEFAULT_POLL_INTERVAL_SEC);
|
|
153
|
+
const wait_finalize = options.wait_finalize !== false;
|
|
154
|
+
const blockMs = block_until_sec * 1000;
|
|
155
|
+
const pollMs = poll_interval_sec * 1000;
|
|
156
|
+
const start = Date.now();
|
|
157
|
+
const snapshots = [];
|
|
158
|
+
let lastSnap = null;
|
|
159
|
+
let data = {};
|
|
160
|
+
let heartbeat = 0;
|
|
161
|
+
while (Date.now() - start < blockMs) {
|
|
162
|
+
data = (await apiCall("GET", `/v1/jobs/${job_id}`));
|
|
163
|
+
const elapsedSec = Math.round((Date.now() - start) / 1000);
|
|
164
|
+
const snap = snapshotFromJob(data, elapsedSec);
|
|
165
|
+
heartbeat += 1;
|
|
166
|
+
const heartbeatDue = heartbeat >= HEARTBEAT_POLLS;
|
|
167
|
+
if (shouldRecordSnapshot(lastSnap, snap) || heartbeatDue) {
|
|
168
|
+
snapshots.push(snap);
|
|
169
|
+
lastSnap = snap;
|
|
170
|
+
heartbeat = 0;
|
|
171
|
+
}
|
|
172
|
+
const status = String(data.status ?? "");
|
|
173
|
+
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
await sleep(pollMs);
|
|
177
|
+
}
|
|
178
|
+
const status = String(data.status ?? "");
|
|
179
|
+
const terminal = status === "completed" || status === "failed" || status === "cancelled";
|
|
180
|
+
const timed_out = !terminal;
|
|
181
|
+
if (terminal && status === "completed" && wait_finalize && data.finalize_job_id) {
|
|
182
|
+
const finalizeId = String(data.finalize_job_id);
|
|
183
|
+
const elapsedSec = Math.round((Date.now() - start) / 1000);
|
|
184
|
+
snapshots.push(snapshotFromJob(data, elapsedSec, `finalize_training ${finalizeId} started — waiting for figure render`));
|
|
185
|
+
try {
|
|
186
|
+
const { note } = await pollFinalizeIfPresent(job_id, data, Math.max(0, blockMs - (Date.now() - start)));
|
|
187
|
+
data = await refreshJobAfterFinalize(job_id);
|
|
188
|
+
snapshots.push(snapshotFromJob(data, Math.round((Date.now() - start) / 1000), note ?? `finalize_training ${finalizeId} completed`));
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
snapshots.push(snapshotFromJob(data, Math.round((Date.now() - start) / 1000), `finalize_training failed: ${err.message}`));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const statusText = formatJobStatusText(job_id, data);
|
|
195
|
+
const logHint = terminal ? await fetchTrainingLogHint(job_id, data) : undefined;
|
|
196
|
+
let suggested = suggestedNextStep(job_id, data);
|
|
197
|
+
if (logHint)
|
|
198
|
+
suggested += ` ${logHint}`;
|
|
199
|
+
return {
|
|
200
|
+
job_id,
|
|
201
|
+
terminal,
|
|
202
|
+
timed_out,
|
|
203
|
+
snapshots,
|
|
204
|
+
status_text: statusText,
|
|
205
|
+
data,
|
|
206
|
+
suggested_next_step: suggested,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
export async function runJobMonitor(args) {
|
|
210
|
+
const block_until_sec = args.block_until_sec ?? DEFAULT_BLOCK_UNTIL_SEC;
|
|
211
|
+
const poll_interval_sec = args.poll_interval_sec ?? DEFAULT_POLL_INTERVAL_SEC;
|
|
212
|
+
const result = await monitorJob(args.job_id, {
|
|
213
|
+
block_until_sec,
|
|
214
|
+
poll_interval_sec,
|
|
215
|
+
wait_finalize: args.wait_finalize,
|
|
216
|
+
});
|
|
217
|
+
const text = formatMonitorText(result, { block_until_sec, poll_interval_sec });
|
|
218
|
+
return textResult({
|
|
219
|
+
...result.data,
|
|
220
|
+
monitor: {
|
|
221
|
+
job_id: result.job_id,
|
|
222
|
+
terminal: result.terminal,
|
|
223
|
+
timed_out: result.timed_out,
|
|
224
|
+
snapshots: result.snapshots,
|
|
225
|
+
status_text: result.status_text,
|
|
226
|
+
suggested_next_step: result.suggested_next_step,
|
|
227
|
+
},
|
|
228
|
+
status_text: text,
|
|
229
|
+
});
|
|
230
|
+
}
|
package/dist/shared.js
CHANGED
|
@@ -26,7 +26,7 @@ export const RETRYABLE_STATUS = new Set([502, 503, 504]);
|
|
|
26
26
|
* X-Barsom-Client-Version so the server can annotate tool guidance with the
|
|
27
27
|
* wrapper version each action requires. Keep in sync with package.json on bump.
|
|
28
28
|
*/
|
|
29
|
-
export const CLIENT_VERSION = "0.
|
|
29
|
+
export const CLIENT_VERSION = "0.20.0";
|
|
30
30
|
/** User-facing links; keep aligned with barivia.se / api.barivia.se. */
|
|
31
31
|
export const PUBLIC_SITE_ORIGIN = "https://barivia.se";
|
|
32
32
|
/** Self-serve account dashboard (manage plan, billing, and API keys). */
|
package/dist/tools/jobs.js
CHANGED
|
@@ -3,11 +3,13 @@ import { registerAuditedTool } from "../audit.js";
|
|
|
3
3
|
import { apiCall, textResult } from "../shared.js";
|
|
4
4
|
import { formatJobCancelText, formatJobStatusText } from "../job_status_format.js";
|
|
5
5
|
import { pollFinalizeIfPresent, refreshJobAfterFinalize } from "../train_finalize.js";
|
|
6
|
+
import { DEFAULT_BLOCK_UNTIL_SEC, DEFAULT_POLL_INTERVAL_SEC, runJobMonitor } from "../job_monitor.js";
|
|
6
7
|
export const JOBS_DESCRIPTION_BASE = `Manage and inspect jobs (lifecycle only).
|
|
7
8
|
|
|
8
9
|
| Action | Use when |
|
|
9
10
|
|--------|----------|
|
|
10
|
-
|
|
|
11
|
+
| monitor | After async submit — blocks server-side with 5s snapshots until terminal (preferred for agents) |
|
|
12
|
+
| status | One-shot progress check (manual poll every 10–15s if not using monitor) |
|
|
11
13
|
| list | Finding job IDs / reviewing pipeline state (optionally filter by dataset_id) |
|
|
12
14
|
| compare | Picking the best TRAINING run from a set (metrics table). NOT inference(compare), which diffs one dataset's distribution against the training set. |
|
|
13
15
|
| cancel | Stopping a running or pending job to free the worker |
|
|
@@ -15,8 +17,13 @@ export const JOBS_DESCRIPTION_BASE = `Manage and inspect jobs (lifecycle only).
|
|
|
15
17
|
|
|
16
18
|
To SUBMIT training, use the **train** tool (train(action=map | siom_map | impute | floop_siom)); to score data or run post-training map operations (predict, batch_predict, compare, project_columns, impute_column, transition_flow), use **inference**.
|
|
17
19
|
|
|
20
|
+
ASYNC POLLING PROTOCOL (action=monitor):
|
|
21
|
+
- Preferred after train/inference submit: one call blocks until completed/failed or block_until timeout (default ${DEFAULT_BLOCK_UNTIL_SEC}s, poll every ${DEFAULT_POLL_INTERVAL_SEC}s).
|
|
22
|
+
- Snapshots include phase, epoch/total, QE/TE, ETA, ordering_errors tail when live; waits for finalize_training by default.
|
|
23
|
+
- Optional UI: training_monitor(job_id) for embedded charts; jobs(action=monitor) is the headless equivalent.
|
|
24
|
+
|
|
18
25
|
ASYNC POLLING PROTOCOL (action=status):
|
|
19
|
-
-
|
|
26
|
+
- One-shot check only. If not using monitor, poll every 10-15 seconds manually.
|
|
20
27
|
- Progress increases through phases (ordering then convergence for map training); other job types differ.
|
|
21
28
|
- For large grids (40×40+), do not assume failure before 3 minutes on CPU.
|
|
22
29
|
- Map training typical times: 10×10 ~30s | 20×20 ~3–5 min | 40×40 ~15–30 min.
|
|
@@ -137,12 +144,28 @@ export function renderCompareTable(comparisons) {
|
|
|
137
144
|
export function registerJobsTool(server, description) {
|
|
138
145
|
registerAuditedTool(server, "jobs", description, {
|
|
139
146
|
action: z
|
|
140
|
-
.enum(["status", "list", "compare", "cancel", "delete"])
|
|
141
|
-
.describe("status: check
|
|
147
|
+
.enum(["status", "monitor", "list", "compare", "cancel", "delete"])
|
|
148
|
+
.describe("status: one-shot check; monitor: block until terminal with snapshots; list/compare/cancel/delete as labeled. (Training via train; scoring via inference.)"),
|
|
142
149
|
job_id: z
|
|
143
150
|
.string()
|
|
144
151
|
.optional()
|
|
145
|
-
.describe("Job ID — required for action=status, cancel, delete."),
|
|
152
|
+
.describe("Job ID — required for action=status, monitor, cancel, delete."),
|
|
153
|
+
block_until_sec: z
|
|
154
|
+
.number()
|
|
155
|
+
.int()
|
|
156
|
+
.min(30)
|
|
157
|
+
.optional()
|
|
158
|
+
.describe(`action=monitor only: max wait seconds (default ${DEFAULT_BLOCK_UNTIL_SEC})`),
|
|
159
|
+
poll_interval_sec: z
|
|
160
|
+
.number()
|
|
161
|
+
.int()
|
|
162
|
+
.min(5)
|
|
163
|
+
.optional()
|
|
164
|
+
.describe(`action=monitor only: poll interval seconds (default ${DEFAULT_POLL_INTERVAL_SEC})`),
|
|
165
|
+
wait_finalize: z
|
|
166
|
+
.boolean()
|
|
167
|
+
.optional()
|
|
168
|
+
.describe("action=monitor only: wait for finalize_training (default true)"),
|
|
146
169
|
job_ids: z
|
|
147
170
|
.array(z.string())
|
|
148
171
|
.optional()
|
|
@@ -152,8 +175,13 @@ export function registerJobsTool(server, description) {
|
|
|
152
175
|
.optional()
|
|
153
176
|
.describe("action=list: filter jobs by this dataset ID."),
|
|
154
177
|
}, async (args) => {
|
|
155
|
-
const { action, job_id, job_ids, dataset_id } = args;
|
|
178
|
+
const { action, job_id, job_ids, dataset_id, block_until_sec, poll_interval_sec, wait_finalize } = args;
|
|
156
179
|
// ---- Lifecycle ----
|
|
180
|
+
if (action === "monitor") {
|
|
181
|
+
if (!job_id)
|
|
182
|
+
throw new Error("jobs(monitor) requires job_id");
|
|
183
|
+
return runJobMonitor({ job_id, block_until_sec, poll_interval_sec, wait_finalize });
|
|
184
|
+
}
|
|
157
185
|
if (action === "status") {
|
|
158
186
|
if (!job_id)
|
|
159
187
|
throw new Error("jobs(status) requires job_id");
|