I wrote labwire-pylabrobot, a bridge that exposes a PyLabRobot LiquidHandler as an instrument in Labwire, an open Apache-2.0 protocol for driving hardware from AI agents (discovery, mandatory UCUM units, safety classes with enforced confirmation, signed run manifests, MCP adapter).
The useful part: mapping PyLabRobot broke my spec in ways the first bridge (ophyd) never did, and the failures are documented in SPEC-FINDINGS.md in the repo. The two biggest, both now fixed in v0.3 because this bridge forced them:
-
Well addresses. UCUM made a parameter a volume in uL, but a well reference was just a string. I initially invented an address grammar (“source_plate/A1”) the protocol could not type or validate, exactly the bridge-private convention a protocol is supposed to eliminate. v0.3 added typed resource references: the schema declares the expected kind and the server validates the reference against live deck state at submission.
-
Deck state. Agents cannot plan transfers without the deck, and the protocol had nowhere to put tree-shaped state. The deck is now a first-class discoverable resource (~8 KB projected from PyLabRobot’s ~133 KB serialization), so an agent finds it the same way it finds capabilities, no prompt hints needed.
Still open for critique: every material-moving operation is classed S2 and requires explicit agent confirmation, and gripper moves are exposed at the top tier, which requires an operator grant bound to a digest of the exact parameters, something the agent cannot produce for itself. The bridge uses PyLabRobot as an optional dependency against hardware-free backends only, no vendoring, and the README states it has never driven a physical liquid handler.
Repo: GitHub - benchwire/labwire · GitHub , bridge under packages/bridges/pylabrobot. make demo-pylabrobot runs a serial dilution end to end with signed evidence; make demo-pylabrobot-claude runs the same protocol planned live by an agent.
If a bridge like this is useful to this community, I would rather shape it to fit PyLabRobot’s model than have it drift. If you find anything wrong please let me know!
4 Likes
this is awesome
and thanks for the good feedback! both are good points
2 Likes
Thanks so much Rick. While you are here, I have a question that I think only you can answer for me. What does cancel actually do mid-operation on real machines? My bridge currently abandons the in-flight PLR call, which is a guess. If the safe move is finishing the op and only stopping between steps, I’ll encode that instead.
Also, I’d be happy to PR the bridge into your integrations docs if you want it there.
And if anybody else reading this runs PLR on real hardware and wants an agent driving it through the bridge, I’ll do the setup free for your instrument.
2 Likes
Nice work on the bridge!
Sorry to jump in, the question was for Rick. But I have a Flex here and looked at exactly this recently, so maybe this might help.
OT-3
Stop does try to interrupt the command that is running. ProtocolEngine.request_stop() says it directly: “This will try to interrupt the ongoing command, if there is one. Future commands are canceled. However, by the time this method returns, things may not have settled; the last command may still be running.” It cancels the queue worker task. Physical motion is only really stopped by the emergency-stop, in the lower layers. So stop returning does not mean the motion stopped.
STAR
I think there is no cancel or abort on the Hamilton backends in PyLabRobot. send_command writes the command to USB, then makes a future and waits on it, and a background reader thread matches the reply back by its id. So cancelling that coroutine only stops the waiting. The command is already on the wire.
I have not deliberately cancelled mid-aspirate on the Flex yet. If that is the case you care about most, I can try it here and report back.
Jump in any time, replies like this are the point of the thread.
The STAR detail changes my design. If the command is already on the wire, my bridge abandoning the await means it reports cancelled while the machine keeps moving, which is worse than no cancel at all. I’m changing the mapping so each command declares what cancel actually means on its backend: true abort, stop between steps, or not cancellable. STAR defaults to not cancellable until someone proves otherwise.
Yes on the Flex test if you’re willing. There’s three things I’d want to know. Does physical motion stop, what terminal state the run lands in, and whether what the API reports matches what the machine actually did.
Standing offer applies to that Flex too, whenever you want an agent driving it.
Good, and “not cancellable” as the default sounds right to me.
I will try the cancel test on the Flex and report back what I see. May take a while.
About the agent offer, yes, we can try!
1 Like
Great. Two things to get you running:
-
Send me the snippet you use to construct your LiquidHandler for the Flex, backend and deck included, plus which agent client you use (Claude Desktop, Claude Code, anything that speaks MCP works). I’ll send back a ready-to-run bundle: bridge config wrapping your exact setup, MCP wiring, and a first-session script. Everything small enough to read before you run it.
-
First session stays boring on purpose: discovery, the deck read as a resource, one planned transfer with water so you can watch the confirmation gate do its job. You drive anything physical. I never assume what’s safe on your machine.
Cancel test whenever, no rush, the pilot doesn’t depend on it. I’ll open an issue on the repo to track this if you’re fine working in the open, otherwise we can contact elsewhere, whatever works for you.
1 Like