1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
"""Unified plugin interface for the foldit plugin SDK.
Plugins implement :class:`PluginInterface` to expose ops + queries to the
foldit orchestrator via the plugin protocol (see ``proto/plugin.proto``).
Each plugin runs in its own worker process; the worker host dispatches lifecycle endpoints
(Init, UpdateAssembly, Drop), op dispatch (Invoke, StartStream,
PollStream, UpdateStream, CancelStream), and query dispatch (Query) to
methods on the plugin instance.
Ops vs queries
--------------
Two categories of plugin-exposed work, with different semantics:
- **Ops** mutate state. Take entity locks. Return assembly bytes
(orchestrator copies locked-entity slices into canonical state).
Examples: predict, design, wiggle, mutate. Implemented in
:meth:`invoke` (single-shot) or :meth:`start_stream` (long-running).
- **Queries** read state. No locks. Return query-defined opaque bytes.
Examples: score breakdown, rama colors, sequence_design candidates.
Implemented in :meth:`query` (single-shot only).
Lifecycle a plugin sees:
1. Worker process spawns; imports the plugin module; instantiates the
``Plugin`` class with config.
2. Worker calls :meth:`init` with the canonical Assembly bytes; plugin
returns a SessionId.
3. Worker calls :meth:`register` to retrieve the plugin's op + query
catalog (``PluginRegistration``); orchestrator caches it.
4. Per user action, worker calls :meth:`invoke` (op, single-shot) or
:meth:`start_stream` / :meth:`poll_stream` / :meth:`update_stream` /
:meth:`cancel_stream` (op, long-running) or :meth:`query`
(read).
5. On orchestrator-driven Assembly changes, worker calls
:meth:`update_assembly`. Plugin treats incoming Assembly as latest
authority.
6. On session teardown, worker calls :meth:`drop`.
Bound-type surface
-------------------
The dispatch context and poll outcome are native types compiled into the
SDK's pyo3 extension, not Python dataclasses:
- :meth:`invoke` / :meth:`query` / :meth:`start_stream` receive a
:class:`~foldit_plugin_sdk.DispatchContext` (read-only; ``focused_entity_id``,
``selection``, ``designable``) plus the flattened native-Python params dict.
- :meth:`poll_stream` returns a :class:`~foldit_plugin_sdk.PollOutcome`, which
the plugin builds via its static factories
(``PollOutcome.pending`` / ``.checkpoint`` / ``.cancelled`` / ``.final_`` /
``.error``), optionally attaching a
:class:`~foldit_plugin_sdk.ScoreReport`.
Streaming model
---------------
Streams are *polling-based*, not push. ``start_stream`` kicks off the
work (typically in a background thread) under the host-assigned
``request_id``. The plugin tracks per-request_id state in an internal
dict. ``poll_stream`` returns the latest snapshot from that dict.
Plugins MUST coalesce: between polls (and between successive
``UpdateStream`` calls) only the latest snapshot / latest params
survives. No frame queue, no client-side cursor. This is critical for
interactive-drag patterns where high-frequency mouse events outpace the
plugin's apply rate.
See ``poll_stream`` for the return-shape contract.
"""
# plugin_pb2 is generated by `just generate-proto`; lives in the sibling
# `proto/` subpackage.
# type: ignore[import-not-found]
# The dispatch context and poll outcome are compiled into the SDK's
# pyo3 extension and only resolve at runtime, post-maturin. Importing
# them under TYPE_CHECKING keeps the annotations honest without
# requiring the built extension at module-import time.
"""Construct a :class:`plugin_pb2.ParamValue` from a native Python value.
Supported: int, float, bool, str, and 3-tuples / lists for Vec3.
Plugins building :class:`plugin_pb2.ParamSpec` defaults use this.
"""
=
=
=
=
=
=
=
=
return
"""Base class all foldit plugins implement.
Required overrides:
- :meth:`__init__` receives plugin-private config dict.
- :meth:`register` returns the plugin's op catalog as a
:class:`plugin_pb2.PluginRegistration`.
- :meth:`init` starts a session given an Assembly.
- :meth:`update_assembly` replaces the working Assembly.
- :meth:`drop` tears down a session.
Optional overrides (raise NotImplementedError by default; plugins
declare which ops they support via :meth:`register`):
- :meth:`invoke` single-shot ops.
- :meth:`start_stream` / :meth:`poll_stream` / :meth:`update_stream`
/ :meth:`cancel_stream` streaming ops.
"""
"""Initialize the plugin with its host-process config.
Per ``PLUGIN_PROTOCOL.md``: no init params on the wire; config is
plugin-private. The host process passes whatever the plugin's
spawn descriptor carries.
"""
...
# Lifecycle: required
"""Return this plugin's :class:`plugin_pb2.PluginRegistration`.
Called by the worker host after :meth:`init` succeeds; the
orchestrator caches the resulting catalog and routes ops by
``PluginOp.id``.
"""
...
"""Start a session. Returns a SessionId (uint64), the plugin's choice;
commonly ``1`` for single-session plugins.
``assembly_bytes`` is the canonical Assembly (assembly wire format) as
of the moment of session start. Plugins decode this into whatever
internal working state they need (loaded model + cached
embeddings for ML, ``RISession`` for Rosetta, etc.).
"""
...
# Payload-kind tags matching `FolditPluginAssemblyPayloadKind` on
# the Rust ABI side. Used as the `payload_kind` arg to
# :meth:`update_assembly`.
: = 0
: = 1
"""Push an Assembly update.
``payload_kind`` is :attr:`PAYLOAD_KIND_FULL` (fresh assembly
snapshot) or :attr:`PAYLOAD_KIND_DELTA` (delta edit list;
decode via ``molex.deserialize_delta``). ``from_gen`` / ``to_gen``
are the host's broadcast generation counters; a plugin whose
local gen does not match ``from_gen`` must arm a ``STALE_GEN``
error to return on its next dispatch so the host re-syncs.
Plugins MUST treat the incoming Assembly (full, or the state
after applying the delta) as latest authority and discard any
internal state inconsistent with it.
"""
...
"""Tear down a session. Free any resources keyed to this session.
Idempotent: calling twice with the same SessionId is a no-op."""
...
# Op dispatch: single-shot mutation
"""Single-shot mutating op (kind=INVOKE).
Ops mutate the plugin's working assembly. ``op`` matches a
registered :class:`plugin_pb2.PluginOp` id with
``kind=OP_KIND_INVOKE``. ``context`` is the bound
:class:`~foldit_plugin_sdk.DispatchContext` carrying the focus +
selection captured by the orchestrator at trigger time. ``params``
is the flattened native-Python dict of typed params.
Returns the plugin's working assembly bytes post-op (assembly
wire format). The orchestrator copies locked-entity slices from the
returned assembly into canonical state.
Ops with no state mutation should be queries, not invokes; see
:meth:`query`.
"""
# Query dispatch: read state without mutation
"""Single-shot read query.
Queries READ state; no entity mutation, no entity locks.
``query`` matches a registered :class:`plugin_pb2.PluginQuery` id.
``context`` and ``params`` mirror :meth:`invoke`.
Returns query-defined opaque bytes. The plugin and the consuming
Tier 2 panel agree on the encoding (e.g. UTF-8 JSON for
sequence-design candidates, packed float arrays for rama colors).
Concurrent queries on the same entity are safe; queries racing
against ops read what's there at execution time.
"""
# Generic dispatch: streaming
"""Begin a long-running op (kind=STREAM) under the host-assigned
``request_id`` (uint64). The plugin keys its per-stream state on
that id; it does not choose its own.
The plugin starts the actual work (typically in a background
thread) and tracks per-``request_id`` state in an internal dict.
Subsequent :meth:`poll_stream` calls read that state and return
the latest snapshot.
Plugins MUST coalesce: only the latest snapshot survives between
polls. See module docstring for the streaming model.
"""
"""Return the latest snapshot for a running stream as a bound
:class:`~foldit_plugin_sdk.PollOutcome`, built via its static
factories:
- ``PollOutcome.pending(...)`` op still running. Caller should
continue polling.
- ``PollOutcome.checkpoint(...)`` accepted intermediate the host
commits while the stream keeps running. Polling continues.
- ``PollOutcome.cancelled(...)`` op stopped at host request,
returning a usable working pose. No further polls.
- ``PollOutcome.final_(...)`` op finished successfully. The
orchestrator promotes the assembly into canonical state. No
further polls.
- ``PollOutcome.error(...)`` op failed. No further polls.
"""
"""Push new params to a running stream.
Used for pull-target updates, rama-drag ticks, etc. ``params``
carries only the fields the plugin's metadata declared as
updatable; values for non-updatable params are silently ignored.
"""
"""Stop a running stream. Idempotent: cancelling an already-finished
stream is a no-op."""
"""Find a class implementing :class:`PluginInterface` in ``module``.
Used by the worker host after dynamically importing the plugin module.
The plugin is expected to expose exactly one ``PluginInterface``
subclass (conventionally named ``Plugin``).
Raises :class:`ValueError` if no subclass found.
"""
=
return