Skip to content

Add composable Python skills with sync and async execution - #26

Merged
Jepson2k merged 7 commits into
mainfrom
feat/skill-runtime
Sep 18, 2026
Merged

Jepson2k merged 7 commits into
mainfrom
feat/skill-runtime

Conversation

@Jepson2k

@Jepson2k Jepson2k commented Sep 7, 2026

Copy link
Copy Markdown
Owner

Reusable robot sequences need one Python implementation that works with connected synchronous and asynchronous clients. This adds @skill, a typed sync facade (skill(rbt, ...)) and explicit async entry point (await skill.async_call(rbt, ...)), while keeping connection ownership with the supplied client.

  • Capability checks run before the body. Backend-specific client annotations preserve native API typing.
  • Nested invocations publish lifecycle/progress events with parent ids. Cancellation requests a bounded backend stop and prevents subsequent supplied-client calls from the cancelled invocation.
  • Optional waldoctl.skills entry points discover callable plugins; ordinary imports need no registration. Broken plugins are diagnosed, and duplicate ids exclude all conflicting providers.
  • Documents wrapper interception requirements and cooperative cancellation limits. No automatic recovery motion or power-loss restart.

Validation: 150 tests passed, 4 existing tests skipped; source type checks, all pre-commit hooks and the full GitHub CI matrix passed. Live-controller cancellation and sync execution are exercised by PAROL6 #41 and PAR6 #67; preview and GUI stepping are in Commander #49. All use feat/skill-runtime.

Release dependency: companion backends and Commander target waldoctl v0.14.0; publish that release after this PR merges and before releasing the consumers.

Jepson2k and others added 6 commits September 11, 2026 04:45
A skill's own asyncio.timeout()/wait_for() around a supplied-client call or
a nested skill cancels the running task exactly like an external cancel, so
the per-call guard sent a real STOP to the arm and poisoned the execution on
an ordinary timeout. Cancellation is now authoritative only where it escapes
the root invocation; nested invocations report cancelled and propagate.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A program is written once and runs against a live backend or a preview by
swapping the client, so every wrapper standing in for a client -- the
stepping wrapper, the path preview, a backend's dry run -- has to agree on
which calls queue on the controller, which mint an index the program may
wait on, which cancel the queue, and which read live state a plan cannot
predict. Each wrapper kept its own list, so a new method was stepped,
previewed and waited on by whichever list happened to mention it.

The classification now lives where the method is declared: a @command
marker per method, read back by command_table(). The conformance test
refuses an unclassified public method, and a backend override inherits the
ABC's spec, so the contract belongs to the interface rather than to each
override. Streamed motion and estimate_payload are marked as minting no
index -- they are the commands whose return is not a queue position.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`requires="motion"` became a frozenset of its letters, every letter passed
the identifier pattern, the skill registered, and every invocation then
demanded five one-letter capabilities of the client.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
move_c, move_s, move_p, tool_action and write_io carried
NotImplementedError defaults, which made them read as optional. Both
backends implement all five, so they join the rest of the control
surface as @AbstractMethod.

What does vary by hardware is already declared as a has_* flag on Robot,
so a skill's requirements need only cover those. Requires is one boolean
per flag, replacing the free-form capability set that lived on the
client: a mistyped requirement is a type error at the call site instead
of a MissingCapability at run time, and a backend's support is declared
in one place rather than two.

The check reads client.robot, so a client that names no backend reports
every requirement missing rather than assuming it met.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N8zt66KjkzgZLtQuSfJd9r
isinstance against the DryRunClient protocol resolves members statically,
so a client that forwards attribute access, such as a preview wrapper or
the skill guard, never matches. is_dry_run resolves each public member
with ordinary lookup instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N8zt66KjkzgZLtQuSfJd9r
@Jepson2k
Jepson2k marked this pull request as ready for review September 17, 2026 22:33
@Jepson2k
Jepson2k merged commit 086c292 into main Sep 18, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants