Expose PyLabRobot liquid handlers as Labwire instruments
Project description
labwire-pylabrobot
Expose a PyLabRobot liquid handler as a Labwire instrument, so an AI agent can read a deck, plan transfers, and move liquid through one protocol, with the units, safety classes, and signed results PyLabRobot does not carry.
PyLabRobot abstracts liquid handling for Python programmers. Labwire describes instruments to AI agents. This bridge composes them; Labwire does not reimplement drivers.
It also exists to answer a harder question than the ophyd bridge could. ophyd devices are signal-shaped, which is the shape Labwire v0.2 was designed around. Liquid handling is not: its commands act on things, and its interesting state is a tree. Everywhere the protocol strained is written down in SPEC-FINDINGS.md, which is the real output of this package.
What changed in v0.3
The deck stopped being a command and became the labwire:deck resource:
read it for typed content plus the index of everything a command parameter
can reference. The invented "plate/A1" grammar is gone; references are
URIs like labwire:deck/source_plate/A1, composed by one protocol-defined
rule, declared with the resource_ref keyword instead of a regex, and
validated by the server against a fresh read before a handler runs. And the
gripper ships: move_plate, move_lid, move_resource at S3, which
now means an operator grant an agent cannot mint, bound to the exact
parameters of one call.
Five-minute quickstart
Nothing here needs hardware, a server, or a browser. From a checkout with
make setup done:
# 1. See what Labwire would serve from a deck
uv run labwire-pylabrobot check \
examples.liquid_handling.rig:build_liquid_handler \
-a examples/liquid_handling/labwire-pylabrobot.yaml
# 2. Watch a serial dilution run through the protocol, with signed evidence
make demo-pylabrobot
# 3. The same dilution, planned by a Claude agent (needs ANTHROPIC_API_KEY)
make demo-pylabrobot-claude
check prints the resolved instrument: every command with its safety class,
every piece of labware with what the annotation file says it holds.
OK: LiquidHandlerChatterboxBackend (lh_deck)
8 channel(s), 9 piece(s) of labware
S2 aspirate
S2 dispense
S1 set_well_volume
S3 move_plate
S0 stop
labwire:deck/tips: tip_rack 8x12 (96 tips)
labwire:deck/source_plate: plate 8x12 (hazard: none)
Serving it is a few lines:
from labwire.bridges.pylabrobot import PyLabRobotInstrument, load_annotations
from labwire.core import InstrumentServer
instrument = PyLabRobotInstrument(handler, load_annotations(Path("labwire-pylabrobot.yaml")))
server = InstrumentServer(instrument, confirmation_token="operator-grant")
async with server.serve_websocket("127.0.0.1", 9520):
...
Addressing
Everything an operation acts on is a URI under the deck resource:
labwire:deck the resource: read it
labwire:deck/source_plate labware standing on the deck
labwire:deck/source_plate/A1 one well of it
labwire:deck/tips/H12 one tip spot
Item URIs compose by the protocol rule (SPEC 10.1): entry URI, slash, an id
from the read result's index, so an agent that can read an index can
construct every legal reference with no grammar to learn. Two things are
deliberately not accepted: PyLabRobot's derived names
(source_plate_well_A1) are refused with the canonical URI, and its range
syntax (plate["A1:H1"]) is not exposed, because JSON arrays already carry
cardinality:
{"wells": ["labwire:deck/source_plate/A1"], "volumes_ul": [50.0]}
Every failure names what would have worked, and the server's own refusal
(-32010) adds the pointer, the expected kind, did_you_mean candidates,
and a ready-to-send read request.
The annotation file
Unlike the ophyd bridge's, this file is optional. ophyd carries no units, so its annotation file exists mostly to supply them; PyLabRobot is consistent by convention (microlitres, microlitres per second, millimetres), so the bridge supplies units from a built-in table. What this file is for is the thing neither library knows, which is what the labware actually holds.
version: 1
instrument:
description: A STARlet running dilutions.
commands:
transfer: {estimated_duration_s: 4.0}
labware:
Cor_96_wellplate_360ul_Fb: {description: A Costar 96-well plate.}
resources:
labwire:deck/acid_stock:
description: 1 M hydrochloric acid.
hazard: corrosive
locked: true # refused outright; enforced
Rules worth knowing:
- Merging is per field, from the labware entry (keyed by PyLabRobot class or model) to the resource entry (keyed by its deck URI), so an override touches only what it names.
- Unknown keys, resources, and commands are errors. An annotation naming a plate that is not on the deck is refused rather than ignored, because a silently dropped hazard annotation is the worst failure this file has.
lockedis enforced. Locking a plate locks all 96 of its wells and refuses every operation touching them.- There is no per-resource
safety_classany more. It was documented in three places as reported-but-not-enforced, and keeping a field that cannot raise a call's class would be keeping a lie: argument-dependent classes are finding F3, still out of scope. Command-levelsafety_classoverrides now genuinely bite, because raising a command to S3 makes it require an operator grant.hazardappears in the deck resource content, where an agent actually reads it.
Mapping
| PyLabRobot | Labwire |
|---|---|
LiquidHandler |
one instrument |
| backend class name | identity.model |
| deck and its labware | the labwire:deck resource (resource/read) |
Well / TipSpot / Plate |
a resource_ref-typed URI in command parameters |
aspirate / dispense / transfer |
commands, safety class S2 |
pick_up_tips / drop_tips / return_tips / discard_tips |
commands, S2 |
move_plate / move_lid / move_resource |
commands, S3: operator grant, not interruptible |
| volume tracker, per well | deck resource content, listed sparsely |
| tip trackers, per channel | deck resource content |
| cumulative volume moved | telemetry channels, in uL |
stop |
command, safety class S0 |
NoTipError / HasTipError |
interlock |
TooLittleLiquidError / TooLittleVolumeError |
validation |
ResourceNotFoundError / NoChannelError |
validation |
ChannelizedError |
first mapped sub-error, all channels in details |
| anything else | hardware_fault |
Safety defaults lean toward friction. Everything that moves or consumes
liquid is S2, so an agent must present an operator confirmation for each
call. Everything that moves labware through space is S3: the failure
mode is a collision, and each call takes a single-use operator grant bound
to its exact parameters, which the demo shows being refused, approved,
used, and then refused on different values. stop is S0 so recovery stays
available while an interlock is tripped. An annotation may raise a class,
never lower it.
LIMITATIONS
Read this before believing anything above.
- No physical hardware, ever. The bridge is exercised against
PyLabRobot's
LiquidHandlerChatterboxBackend, which prints the operations it would have performed. It has never been connected to a Hamilton, a Tecan, an Opentrons, or any other machine, and no claim is made about any vendor instrument. PyLabRobot's simulator backend was removed in favour of a websocket Visualizer that opens a browser, so chatterbox is the only honest hardware-free option. - Command-level S3 is enforced; resource-level hazard is not. Getting
this distinction right matters more than the feature. An S3 command
requires a real operator grant now (finding F4, resolved). But annotating a
resource as hazardous still cannot raise the class of a call that touches
it, because safety classes are per command, not per argument: that is
finding F3, out of scope.
hazardis surfaced to agents in the deck content andlockedis a hard refusal; neither is a gradation. - Gripper destinations are validated against the index, not against physics. A grant authorizes a move the operator saw; nothing here checks reachability, collision clearance, or what a real STARlet's rail geometry permits.
- Enabling tracking is process-wide. The bridge turns on PyLabRobot's tip and volume trackers, because without them a liquid handler silently accepts physically impossible commands. PyLabRobot toggles both through module-level globals rather than per-handler state, so constructing one bridged instrument changes tracking for every liquid handler in the process.
- Volumes are believed, not measured. The trackers know what they have
been told and what they have moved. Nothing looks into a plate. A run starts
with
set_well_volume, and if that is wrong everything after it is wrong. - Cancellation is best-effort and untested on hardware. Each operation is a single await, so a cancel stops the handler and abandons the call rather than interrupting it partway. Against the chatterbox backend operations complete immediately, so a cancel almost always loses the race. An aspiration cannot be un-aspirated in any case (SPEC-FINDINGS F8).
- Gripper moves are not exposed.
move_plate,move_lid, andmove_resourcemove labware through space, where the failure is a collision rather than a bad pipetting step. They deserve their own treatment, and doing them badly is worse than not doing them. - 96-head operations are not exposed, and neither is PyLabRobot's
untyped
backend_kwargspassthrough to vendor firmware. Handing an agent an untyped channel into a vendor backend would undo the point of a typed protocol. - Deck state can go stale.
describe_deckis a snapshot with no push notification and no revision counter, so a second client can move a plate and the first will not know (SPEC-FINDINGS F2).
Licensing
PyLabRobot is a separate MIT-licensed project from the Sculpting Evolution
group at MIT Media Lab. It is an optional dependency here: not vendored,
not forked, not modified. See the repository NOTICE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file labwire_pylabrobot-0.3.0.tar.gz.
File metadata
- Download URL: labwire_pylabrobot-0.3.0.tar.gz
- Upload date:
- Size: 43.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
beab9afe2367a4795f1fa0ee6a4eb8a657173ad96ae67039f604ca68e3eb9b79
|
|
| MD5 |
738eaf837b23ae242d6e66371028e50b
|
|
| BLAKE2b-256 |
5e35174c6162902e3310aae1efa1402e8c2d1b7c47698c1165fde90769cc4e51
|
Provenance
The following attestation bundles were made for labwire_pylabrobot-0.3.0.tar.gz:
Publisher:
release.yml on benchwire/labwire
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
labwire_pylabrobot-0.3.0.tar.gz -
Subject digest:
beab9afe2367a4795f1fa0ee6a4eb8a657173ad96ae67039f604ca68e3eb9b79 - Sigstore transparency entry: 2260407502
- Sigstore integration time:
-
Permalink:
benchwire/labwire@0762ad57b8f190dc77bdbf9fbb6df7ac5d9f679e -
Branch / Tag:
refs/heads/main - Owner: https://github.com/benchwire
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0762ad57b8f190dc77bdbf9fbb6df7ac5d9f679e -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file labwire_pylabrobot-0.3.0-py3-none-any.whl.
File metadata
- Download URL: labwire_pylabrobot-0.3.0-py3-none-any.whl
- Upload date:
- Size: 30.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5c5d80009ba7973dd1d4e1c6f55706af30aea72fd9c5e0f8c299eec239daae76
|
|
| MD5 |
7521584e751d81ab80b327f011a3524e
|
|
| BLAKE2b-256 |
844375896295b03dc2e81ca9094230bbe121ed919fa80f8a673120fd43e168f7
|
Provenance
The following attestation bundles were made for labwire_pylabrobot-0.3.0-py3-none-any.whl:
Publisher:
release.yml on benchwire/labwire
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
labwire_pylabrobot-0.3.0-py3-none-any.whl -
Subject digest:
5c5d80009ba7973dd1d4e1c6f55706af30aea72fd9c5e0f8c299eec239daae76 - Sigstore transparency entry: 2260408111
- Sigstore integration time:
-
Permalink:
benchwire/labwire@0762ad57b8f190dc77bdbf9fbb6df7ac5d9f679e -
Branch / Tag:
refs/heads/main - Owner: https://github.com/benchwire
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0762ad57b8f190dc77bdbf9fbb6df7ac5d9f679e -
Trigger Event:
workflow_dispatch
-
Statement type: