Skip to main content

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}