dsh-labscript 0.2.1 → 0.2.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/CHANGELOG.md +22 -0
- package/README.md +8 -2
- package/lib/tools.js +33 -7
- package/package.json +1 -1
- package/python/bridge.py +4 -4
- package/python/tests/run_tests.py +55 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.3
|
|
4
|
+
|
|
5
|
+
- Fix: labs_status now prints panel values. keys=[...] and raw=true were accepted but the
|
|
6
|
+
rendered summary never showed any global, so the model could only see the key count
|
|
7
|
+
(the tool result content is the render output; the returned object is not in context).
|
|
8
|
+
With keys the selected values are printed (240 chars each, <missing> when absent);
|
|
9
|
+
without keys it prints the count and a hint to pass keys.
|
|
10
|
+
- Tests: labs_status value rendering (values, count-only, missing keys).
|
|
11
|
+
|
|
12
|
+
## 0.2.2
|
|
13
|
+
|
|
14
|
+
- Fix: labs_status and labs_engage report pathMap-mapped paths. `labs_status.labscript`
|
|
15
|
+
and `labs_status.shot_output_folder` used to be raw runmanager paths (e.g. E:\...)
|
|
16
|
+
and were unusable from the agent's machine; they are now what pathMap maps them to.
|
|
17
|
+
This is a field-semantics fix: read them as local paths, do not map again.
|
|
18
|
+
- Fix: tool output renders mapped run folders. The rendered lines of labs_engage,
|
|
19
|
+
labs_wait, labs_data, labs_protocol and the pending-run progress now prefer
|
|
20
|
+
`mapped_run_folder` over the raw `run_folder`.
|
|
21
|
+
- Fix: labs_status now lists the shot output folder, and the status progress object
|
|
22
|
+
reports `mapped_run_folder`.
|
|
23
|
+
- Tests: pathMap exit coverage (status mapping, idempotency, progress mapping).
|
|
24
|
+
|
|
3
25
|
## 0.2.1
|
|
4
26
|
|
|
5
27
|
- Hard limits support a lower bound: limits entries can now use min as well as max
|
package/README.md
CHANGED
|
@@ -97,6 +97,12 @@ Add one row to your DSH profile patch (see examples/config.example.yml):
|
|
|
97
97
|
pythonPaths: [/path/to/my_lab]
|
|
98
98
|
approval: { engage: true, protocolRun: true }
|
|
99
99
|
|
|
100
|
+
`pathMap` rewrites paths reported by runmanager (remote drive letters such as
|
|
101
|
+
`E:/Experiments`) into paths this machine can open (UNC shares). Tool output is
|
|
102
|
+
already mapped: `labs_status`, `labs_engage`, `labs_wait`, `labs_data` and
|
|
103
|
+
`labs_protocol` show local paths, while `run_folder` keeps the raw runmanager
|
|
104
|
+
value next to `mapped_run_folder`.
|
|
105
|
+
|
|
100
106
|
The python interpreter must be able to import numpy and h5py; the real backend
|
|
101
107
|
also needs runmanager (labscript-suite). The plugin never stores credentials:
|
|
102
108
|
zprocess authentication is read by labscript itself from labconfig.
|
|
@@ -119,8 +125,8 @@ is not a physics simulation.
|
|
|
119
125
|
## Development
|
|
120
126
|
|
|
121
127
|
npm install # only runtime dependency: yaml
|
|
122
|
-
npm test # node --test,
|
|
123
|
-
python python/tests/run_tests.py #
|
|
128
|
+
npm test # node --test, 36 tests (fake bridge, no python needed)
|
|
129
|
+
python python/tests/run_tests.py # 15 tests (needs numpy + h5py)
|
|
124
130
|
LABSCRIPT_PYTHON=/path/to/python3 node scripts/demo.mjs # end-to-end mock demo
|
|
125
131
|
|
|
126
132
|
A fresh clone must run `npm install` before `npm test`; the CI workflow does this.
|
package/lib/tools.js
CHANGED
|
@@ -26,7 +26,7 @@ function number(value) {
|
|
|
26
26
|
|
|
27
27
|
function progressLine(run) {
|
|
28
28
|
if (!run) return 'run: none pending'
|
|
29
|
-
const parts = ['run: ' + (run.run_folder || '<submitted>')]
|
|
29
|
+
const parts = ['run: ' + (run.mapped_run_folder || run.run_folder || '<submitted>')]
|
|
30
30
|
if (run.kind) parts.push('kind=' + run.kind)
|
|
31
31
|
if (run.n_h5 !== undefined) parts.push('h5=' + run.n_h5 + '/' + (run.expected_n_shots || '?'))
|
|
32
32
|
else if (run.expected_n_shots) parts.push('expected=' + run.expected_n_shots)
|
|
@@ -35,6 +35,30 @@ function progressLine(run) {
|
|
|
35
35
|
return parts.join(' ')
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// The bridge returns the whole panel; without this the summary alone hides it.
|
|
39
|
+
function globalText(value, limit = 240) {
|
|
40
|
+
let rendered
|
|
41
|
+
try { rendered = JSON.stringify(value) } catch { rendered = String(value) }
|
|
42
|
+
if (rendered === undefined) rendered = String(value)
|
|
43
|
+
return rendered.length > limit ? rendered.slice(0, limit) + '…' : rendered
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function globalLines(args, value) {
|
|
47
|
+
const globals = value.globals || {}
|
|
48
|
+
const keys = Array.isArray(args.keys) ? args.keys : []
|
|
49
|
+
if (keys.length === 0) {
|
|
50
|
+
const count = Object.keys(globals).length
|
|
51
|
+
return [count > 0 ? 'globals: ' + count + ' keys (pass keys=[...] to read values)' : 'globals: none']
|
|
52
|
+
}
|
|
53
|
+
const raw = args.raw === true && value.raw_globals ? value.raw_globals : null
|
|
54
|
+
const out = ['globals:']
|
|
55
|
+
for (const key of keys) {
|
|
56
|
+
const shown = raw ? raw[key] : globals[key]
|
|
57
|
+
out.push(' ' + key + ' = ' + (shown === undefined ? '<missing>' : globalText(shown)))
|
|
58
|
+
}
|
|
59
|
+
return out
|
|
60
|
+
}
|
|
61
|
+
|
|
38
62
|
async function pendingProgress(runtime, exec) {
|
|
39
63
|
const pending = runtime.state.pending
|
|
40
64
|
if (!pending) return null
|
|
@@ -105,7 +129,7 @@ export function registerTools(ctx, runtime) {
|
|
|
105
129
|
|
|
106
130
|
ctx.tools.register({
|
|
107
131
|
name: 'labs_status',
|
|
108
|
-
description: 'Read the labscript runmanager panel: loaded labscript, globals (evaluated; raw expressions with raw=true), n_shots, error_in_globals, run_shots, shot output folder, pending run progress, and the panel snapshot directory. Read-only; call this before any write.',
|
|
132
|
+
description: 'Read the labscript runmanager panel: loaded labscript, globals (evaluated; raw expressions with raw=true), n_shots, error_in_globals, run_shots, shot output folder, pending run progress, and the panel snapshot directory. Read-only; call this before any write. Pass keys=[...] to print the selected panel values (240 chars each; raw=true prints the raw expression). Paths in the output are already mapped for this machine through pathMap; do not map them again.',
|
|
109
133
|
parameters: {
|
|
110
134
|
type: 'object',
|
|
111
135
|
properties: {
|
|
@@ -115,12 +139,14 @@ export function registerTools(ctx, runtime) {
|
|
|
115
139
|
additionalProperties: false,
|
|
116
140
|
},
|
|
117
141
|
isConcurrencySafe: () => true,
|
|
118
|
-
output: looseOutput((
|
|
142
|
+
output: looseOutput((args, value) => text(lines([
|
|
119
143
|
'backend: ' + value.backend + ' plugin: v' + runtime.version,
|
|
120
144
|
'labscript: ' + value.labscript,
|
|
145
|
+
'shot output: ' + value.shot_output_folder,
|
|
121
146
|
'n_shots: ' + value.n_shots + ' error_in_globals: ' + value.error_in_globals + ' run_shots: ' + value.run_shots,
|
|
122
147
|
progressLine(value.last_run),
|
|
123
148
|
'panel history: ' + (value.panel_history ? value.panel_history.dir + ' (' + value.panel_history.count + ' snapshots)' : 'n/a'),
|
|
149
|
+
...globalLines(args, value),
|
|
124
150
|
]))),
|
|
125
151
|
async execute(args, exec) {
|
|
126
152
|
const data = await runtime.runBridge('status', {
|
|
@@ -174,7 +200,7 @@ export function registerTools(ctx, runtime) {
|
|
|
174
200
|
},
|
|
175
201
|
isConcurrencySafe: () => false,
|
|
176
202
|
output: looseOutput((_args, value) => text(lines([
|
|
177
|
-
'submitted run: ' + (value.run_folder || '<run folder pending>'),
|
|
203
|
+
'submitted run: ' + (value.mapped_run_folder || value.run_folder || '<run folder pending>'),
|
|
178
204
|
'n_shots_at_submit: ' + value.n_shots_at_submit + ' expected: ' + value.expected_n_shots,
|
|
179
205
|
'call labs_wait when results are needed.',
|
|
180
206
|
]))),
|
|
@@ -216,7 +242,7 @@ export function registerTools(ctx, runtime) {
|
|
|
216
242
|
},
|
|
217
243
|
isConcurrencySafe: () => true,
|
|
218
244
|
output: looseOutput((_args, value) => text(lines([
|
|
219
|
-
'run: ' + value.run_folder,
|
|
245
|
+
'run: ' + (value.mapped_run_folder || value.run_folder),
|
|
220
246
|
value.aborted ? 'wait cancelled by labs_abort (run aborted)' : '',
|
|
221
247
|
'shots: ' + value.n_h5 + '/' + (value.expected_n_shots || '?') + ' complete: ' + value.complete + (value.timed_out ? ' TIMED OUT' : '') + (value.diagnosis ? ' diagnosis: ' + value.diagnosis : ''),
|
|
222
248
|
value.hint ? 'hint: ' + value.hint : '',
|
|
@@ -326,7 +352,7 @@ export function registerTools(ctx, runtime) {
|
|
|
326
352
|
},
|
|
327
353
|
isConcurrencySafe: () => true,
|
|
328
354
|
output: looseOutput((_args, value) => text(lines([
|
|
329
|
-
'run: ' + value.run_folder,
|
|
355
|
+
'run: ' + (value.mapped_run_folder || value.run_folder),
|
|
330
356
|
'h5: ' + value.n_h5,
|
|
331
357
|
value.scan_table ? 'scan ' + value.scan_table.scan + ': ' + value.scan_table.n_points + ' shots' : '',
|
|
332
358
|
value.globals ? 'globals: ' + Object.keys(value.globals).length + ' keys' : '',
|
|
@@ -366,7 +392,7 @@ export function registerTools(ctx, runtime) {
|
|
|
366
392
|
value.warnings && value.warnings.length ? 'warnings: ' + value.warnings.join('; ') : '',
|
|
367
393
|
value.name_check && value.name_check.checked && value.name_check.missing && value.name_check.missing.length ? 'script key check: MISSING ' + value.name_check.missing.join(', ') : '',
|
|
368
394
|
value.name_check && value.name_check.checked === false ? 'script key check: not checked (' + (value.name_check.reason || 'no script') + ')' : '',
|
|
369
|
-
value.engage ? 'submitted: ' + (value.engage.run_folder || '<run folder pending>') + ' n_shots_at_submit: ' + value.n_shots_at_submit : '',
|
|
395
|
+
value.engage ? 'submitted: ' + (value.engage.mapped_run_folder || value.engage.run_folder || '<run folder pending>') + ' n_shots_at_submit: ' + value.n_shots_at_submit : '',
|
|
370
396
|
value.engage ? 'call labs_wait to finalize this protocol.' : '',
|
|
371
397
|
]))),
|
|
372
398
|
async execute(args, exec) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-labscript",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Control labscript-suite experiments from DeepSeek Harness: runmanager globals and scans, submits and waits, YAML fixed-operation protocols with postprocess write-back, panel snapshots, a built-in mock backend and a read-only doctor.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
package/python/bridge.py
CHANGED
|
@@ -927,8 +927,8 @@ def cmd_status(config, args):
|
|
|
927
927
|
out = {
|
|
928
928
|
"backend": config.get("backend"),
|
|
929
929
|
"version": backend.version() if hasattr(backend, "version") else None,
|
|
930
|
-
"labscript": backend.labscript_file(),
|
|
931
|
-
"shot_output_folder": backend.shot_output_folder(),
|
|
930
|
+
"labscript": map_path(config, backend.labscript_file()),
|
|
931
|
+
"shot_output_folder": map_path(config, backend.shot_output_folder()),
|
|
932
932
|
"globals": jsonable(values),
|
|
933
933
|
"n_shots": backend.n_shots(),
|
|
934
934
|
"error_in_globals": backend.error_in_globals(),
|
|
@@ -1015,7 +1015,7 @@ def cmd_engage(config, args):
|
|
|
1015
1015
|
"mapped_run_folder": map_path(config, run_folder),
|
|
1016
1016
|
"n_shots_at_submit": n_shots,
|
|
1017
1017
|
"expected_n_shots": int(args.get("expected_n_shots") or n_shots),
|
|
1018
|
-
"labscript": backend.labscript_file(),
|
|
1018
|
+
"labscript": map_path(config, backend.labscript_file()),
|
|
1019
1019
|
"submitted_at": now_iso(),
|
|
1020
1020
|
}
|
|
1021
1021
|
append_event(config, {"ts": out["submitted_at"], "tool": "engage", "backend": config.get("backend"), "note": args.get("note"), "run_folder": run_folder, "expected_n_shots": out["expected_n_shots"]})
|
|
@@ -1127,7 +1127,7 @@ def cmd_progress(config, args):
|
|
|
1127
1127
|
expected = int(args.get("expected_n_shots") or 0)
|
|
1128
1128
|
folder = map_path(config, run_folder)
|
|
1129
1129
|
shots = sorted(glob.glob(os.path.join(folder, "**", "*.h5"), recursive=True)) if folder else []
|
|
1130
|
-
return {"run_folder": run_folder, "n_h5": len(shots), "expected_n_shots": expected, "complete": bool(expected and len(shots) >= expected)}
|
|
1130
|
+
return {"run_folder": run_folder, "mapped_run_folder": folder, "n_h5": len(shots), "expected_n_shots": expected, "complete": bool(expected and len(shots) >= expected)}
|
|
1131
1131
|
|
|
1132
1132
|
|
|
1133
1133
|
|
|
@@ -291,6 +291,61 @@ def test_doctor_mock():
|
|
|
291
291
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
292
292
|
|
|
293
293
|
|
|
294
|
+
def test_status_maps_paths_from_runmanager():
|
|
295
|
+
"""labs_status must return paths the agent's machine can open."""
|
|
296
|
+
tmp = tempfile.mkdtemp(prefix="dsh-labscript-py-map-")
|
|
297
|
+
try:
|
|
298
|
+
cfg = make_config(tmp)
|
|
299
|
+
cfg["pathMap"] = [{"remotePrefix": "E:\\Experiments", "localPrefix": "\\\\lab-pc\\Experiments"}]
|
|
300
|
+
|
|
301
|
+
class FakeBackend(object):
|
|
302
|
+
def get_globals(self, raw=False):
|
|
303
|
+
return {}
|
|
304
|
+
|
|
305
|
+
def labscript_file(self):
|
|
306
|
+
return "E:\\Experiments\\2026\\x.py"
|
|
307
|
+
|
|
308
|
+
def shot_output_folder(self):
|
|
309
|
+
return "E:\\Experiments\\2026\\11\\12\\0177"
|
|
310
|
+
|
|
311
|
+
def n_shots(self):
|
|
312
|
+
return 1
|
|
313
|
+
|
|
314
|
+
def error_in_globals(self):
|
|
315
|
+
return False
|
|
316
|
+
|
|
317
|
+
def run_shots(self):
|
|
318
|
+
return True
|
|
319
|
+
|
|
320
|
+
def version(self):
|
|
321
|
+
return "fake"
|
|
322
|
+
|
|
323
|
+
original = bridge.make_backend
|
|
324
|
+
bridge.make_backend = lambda config: FakeBackend()
|
|
325
|
+
try:
|
|
326
|
+
out = bridge.cmd_status(cfg, {})
|
|
327
|
+
finally:
|
|
328
|
+
bridge.make_backend = original
|
|
329
|
+
assert out["labscript"] == "\\\\lab-pc\\Experiments\\2026\\x.py", out["labscript"]
|
|
330
|
+
assert out["shot_output_folder"] == "\\\\lab-pc\\Experiments\\2026\\11\\12\\0177", out["shot_output_folder"]
|
|
331
|
+
finally:
|
|
332
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def test_map_path_is_idempotent():
|
|
336
|
+
cfg = {"pathMap": [{"remotePrefix": "E:\\Experiments", "localPrefix": "\\\\lab-pc\\Experiments"}]}
|
|
337
|
+
once = bridge.map_path(cfg, "E:\\Experiments\\a\\b.h5")
|
|
338
|
+
assert once == "\\\\lab-pc\\Experiments\\a\\b.h5", once
|
|
339
|
+
assert bridge.map_path(cfg, once) == once
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def test_progress_reports_mapped_run_folder():
|
|
343
|
+
cfg = {"pathMap": [{"remotePrefix": "E:\\Experiments", "localPrefix": "\\\\lab-pc\\Experiments"}]}
|
|
344
|
+
out = bridge.cmd_progress(cfg, {"run_folder": "E:\\Experiments\\nope", "expected_n_shots": 1})
|
|
345
|
+
assert out["run_folder"] == "E:\\Experiments\\nope", out
|
|
346
|
+
assert out["mapped_run_folder"] == "\\\\lab-pc\\Experiments\\nope", out
|
|
347
|
+
|
|
348
|
+
|
|
294
349
|
def main():
|
|
295
350
|
tests = [value for name, value in sorted(globals().items()) if name.startswith("test_")]
|
|
296
351
|
failures = 0
|