foldit_plugin_sdk/plugin.rs
1//! The `Plugin` trait: the contract every plugin implements.
2
3use std::collections::HashMap;
4
5use crate::error::{PluginError, Result};
6use crate::proto::plugin as proto;
7use crate::protocol::{DispatchContext, ParamValue, PollOutcome};
8
9/// Payload variant for [`Plugin::update_assembly`].
10///
11/// Mirrors the proto `UpdateAssemblyRequest.payload` oneof. `Full` is
12/// a fresh assembly snapshot; `Delta` is a delta edit list (decode
13/// via molex's `serialize_edits` / `deserialize_edits` pair).
14#[derive(Debug, Clone, Copy)]
15pub enum AssemblyPayload<'a> {
16 /// Fresh assembly snapshot; replaces the plugin's view.
17 Full(&'a [u8]),
18 /// Delta edit list; applied incrementally on top of the
19 /// plugin's current view.
20 Delta(&'a [u8]),
21}
22
23impl<'a> AssemblyPayload<'a> {
24 /// Borrow the payload bytes regardless of variant.
25 #[must_use]
26 pub fn bytes(&self) -> &'a [u8] {
27 match self {
28 AssemblyPayload::Full(b) | AssemblyPayload::Delta(b) => b,
29 }
30 }
31
32 /// True if the payload is a delta edit list.
33 #[must_use]
34 pub fn is_delta(&self) -> bool {
35 matches!(self, AssemblyPayload::Delta(_))
36 }
37}
38
39/// Worker-side plugin interface.
40///
41/// Both Python and native plugins implement this; the host dispatches
42/// to it through the C ABI or the in-process boundary.
43///
44/// Concurrency: methods take `&self` because plugin state mutation is
45/// done through interior-mutable host runtimes (Python's GIL,
46/// thread-locked C++ FFI). Plugins MUST be `Send` so they can be moved
47/// between worker startup and the request-loop thread.
48pub trait Plugin: Send {
49 /// Start a session with the given canonical Assembly. Returns the
50 /// SessionId chosen by the plugin and the assembly bytes of the
51 /// assembly the plugin actually settled on after any post-Init
52 /// normalization (full-atom pose build, hydrogen fill, terminal O,
53 /// etc.). Plugins with no normalization step return an empty
54 /// `Vec<u8>`; the host then keeps its input assembly.
55 ///
56 /// `assets` carries the puzzle asset files delivered at Init (ligand
57 /// `.params`/conformer bytes, the electron-density map, etc.), each a
58 /// `(name, data)` pair; plugins that don't consume a given asset ignore
59 /// it. `params` is the generic puzzle-config channel (weight-patch +
60 /// objective-filter entries, plus density resolution/grid-spacing
61 /// scalars); plugins that don't consume it ignore it.
62 ///
63 /// # Errors
64 ///
65 /// Returns an error if the plugin can't ingest the assembly or
66 /// allocate a session.
67 fn init(
68 &self,
69 assembly_bytes: &[u8],
70 assets: &[proto::PuzzleAsset],
71 params: &HashMap<String, ParamValue>,
72 ) -> Result<(u64, Vec<u8>)>;
73
74 /// Return the plugin's op + query catalog.
75 ///
76 /// # Errors
77 ///
78 /// Returns an error if the plugin can't produce its registration.
79 fn register(&self) -> Result<proto::PluginRegistration>;
80
81 /// Push an Assembly update to a session. `payload` carries either
82 /// a fresh assembly snapshot (`Full`) or a delta edit list
83 /// (`Delta`); `from_gen`/`to_gen` are the host's broadcast
84 /// generation counters. A plugin whose local gen doesn't match
85 /// `from_gen` should arm a `STALE_GEN` error to return on its
86 /// next dispatch so the host re-syncs.
87 ///
88 /// # Errors
89 ///
90 /// Returns an error if the payload can't be applied or the session
91 /// is unknown.
92 fn update_assembly(
93 &self,
94 session: u64,
95 payload: AssemblyPayload<'_>,
96 from_gen: u64,
97 to_gen: u64,
98 ) -> Result<()>;
99
100 /// Tear down a session. Idempotent.
101 ///
102 /// # Errors
103 ///
104 /// Returns an error if the plugin can't release the session state.
105 fn drop_session(&self, session: u64) -> Result<()>;
106
107 /// Single-shot mutating op. Returns the plugin's working Assembly
108 /// post-op (assembly bytes); the orchestrator copies locked-entity
109 /// slices into canonical state.
110 ///
111 /// # Errors
112 ///
113 /// Default impl returns [`PluginError::Unsupported`]. Implementations
114 /// return an error on op-level failure.
115 fn invoke(
116 &self,
117 _session: u64,
118 _op: &str,
119 _ctx: &DispatchContext,
120 _params: &HashMap<String, ParamValue>,
121 ) -> Result<Vec<u8>> {
122 Err(PluginError::Unsupported)
123 }
124
125 /// Begin a long-running op under the host-assigned `request_id`. The
126 /// plugin keys its stream state on that id.
127 ///
128 /// # Errors
129 ///
130 /// Default impl returns [`PluginError::Unsupported`]. Implementations
131 /// return an error if the stream can't be started.
132 // Arg list is the streaming-dispatch ABI contract (session/op/ctx/params/
133 // request_id); it mirrors the C-ABI and must not be refactored away.
134 #[allow(clippy::too_many_arguments)]
135 fn start_stream(
136 &self,
137 _session: u64,
138 _op: &str,
139 _ctx: &DispatchContext,
140 _params: &HashMap<String, ParamValue>,
141 _request_id: u64,
142 ) -> Result<()> {
143 Err(PluginError::Unsupported)
144 }
145
146 /// Return the latest snapshot for a running stream.
147 ///
148 /// # Errors
149 ///
150 /// Default impl returns [`PluginError::Unsupported`]. Op-level failure
151 /// surfaces as `PollOutcome::Error` rather than `Err`.
152 fn poll_stream(&self, _request_id: u64) -> Result<PollOutcome> {
153 Err(PluginError::Unsupported)
154 }
155
156 /// Push new params to a running stream.
157 ///
158 /// # Errors
159 ///
160 /// Default impl returns [`PluginError::Unsupported`]. Implementations
161 /// return an error if the request id is unknown.
162 fn update_stream(&self, _request_id: u64, _params: &HashMap<String, ParamValue>) -> Result<()> {
163 Err(PluginError::Unsupported)
164 }
165
166 /// Stop a running stream. Idempotent.
167 ///
168 /// # Errors
169 ///
170 /// Default impl returns [`PluginError::Unsupported`]. Implementations
171 /// return an error if cleanup fails.
172 fn cancel_stream(&self, _request_id: u64) -> Result<()> {
173 Err(PluginError::Unsupported)
174 }
175
176 /// Single-shot read query. Returns query-defined opaque bytes.
177 ///
178 /// `assembly` (when non-empty) names a specific composition to
179 /// read/score instead of the session pose; an empty slice means the
180 /// query operates on the live session / its in-flight snapshot.
181 ///
182 /// # Errors
183 ///
184 /// Default impl returns [`PluginError::Unsupported`]. Implementations
185 /// return an error on query failure.
186 // Arg list mirrors the C-ABI `query` contract (session/query/ctx/params/
187 // assembly); it must stay in lockstep with the vtable signature.
188 #[allow(clippy::too_many_arguments)]
189 fn query(
190 &self,
191 _session: u64,
192 _query: &str,
193 _ctx: &DispatchContext,
194 _params: &HashMap<String, ParamValue>,
195 _assembly: &[u8],
196 ) -> Result<Vec<u8>> {
197 Err(PluginError::Unsupported)
198 }
199}