telltale_vm/effect/handler_trait.rs
1/// VM-level effect handler.
2///
3/// This is the interface between the VM and the host application. Each
4/// choreography can bind a different handler at session open time.
5///
6/// Host-contract rules:
7/// - Methods on this trait are synchronous. Async I/O, transport polling,
8/// storage flushes, and background retries must happen outside callback
9/// execution and feed their results back through canonical ingress.
10/// - Implementations must treat the provided `state` as session-local scratch
11/// for the current callback only. They must not rely on unrelated session
12/// state or mutate VM session metadata through side channels.
13/// - Host-managed session-local mutation should flow through an explicit
14/// ownership capability such as `OwnedSession`, not through ad hoc access to
15/// the session store while callbacks are executing.
16pub trait EffectHandler: Send + Sync {
17 /// Stable identifier for effect-trace attribution.
18 fn handler_identity(&self) -> String {
19 crate::session::DEFAULT_HANDLER_ID.to_string()
20 }
21
22 /// Compute the payload for a send instruction.
23 ///
24 /// Helper hook used by the default `send_decision` implementation and by
25 /// custom runners that want direct payload computation.
26 ///
27 /// # Arguments
28 /// * `role` - The sending role
29 /// * `partner` - The receiving role
30 /// * `label` - The message label
31 /// * `state` - The coroutine's register file (for reading state)
32 ///
33 /// Returns a typed outcome for the callback.
34 fn handle_send(
35 &self,
36 role: &str,
37 partner: &str,
38 label: &str,
39 state: &[Value],
40 ) -> EffectResult<Value>;
41
42 /// Optional fast-path hook for send decision dispatch.
43 ///
44 /// Returning `Some(result)` bypasses `send_decision`.
45 /// Returning `None` keeps canonical behavior unchanged.
46 fn send_decision_fast_path(
47 &self,
48 _fast_path: SendDecisionFastPathInput<'_>,
49 _state: &[Value],
50 _payload: Option<&Value>,
51 ) -> Option<EffectResult<SendDecision>> {
52 None
53 }
54
55 /// Decide how to handle a send, optionally with a precomputed payload.
56 ///
57 /// Middleware can override this to model loss/delay/corruption. The default
58 /// behavior computes a payload via `handle_send` unless one is provided.
59 ///
60 /// Returns a typed outcome for the callback.
61 fn send_decision(&self, input: SendDecisionInput<'_>) -> EffectResult<SendDecision> {
62 if let Some(payload) = input.payload {
63 EffectResult::success(SendDecision::Deliver(payload))
64 } else {
65 self.handle_send(input.role, input.partner, input.label, input.state)
66 .map_success(SendDecision::Deliver)
67 }
68 }
69
70 /// Process a received value.
71 ///
72 /// # Arguments
73 /// * `role` - The receiving role
74 /// * `partner` - The sending role
75 /// * `label` - The message label
76 /// * `state` - The coroutine's register file (mutable for state updates)
77 /// * `payload` - The received value
78 ///
79 /// Returns a typed outcome for the callback.
80 fn handle_recv(
81 &self,
82 role: &str,
83 partner: &str,
84 label: &str,
85 state: &mut Vec<Value>,
86 payload: &Value,
87 ) -> EffectResult<()>;
88
89 /// Choose which branch to take for internal choice (select).
90 ///
91 /// Branch-selection helper for custom runners.
92 ///
93 /// The canonical VM resolves branch labels from received payloads and does
94 /// not call this method in default dispatch paths.
95 ///
96 /// # Arguments
97 /// * `role` - The choosing role
98 /// * `partner` - The partner role
99 /// * `labels` - The available branch labels
100 /// * `state` - The coroutine's register file (for reading state)
101 ///
102 /// Returns a typed outcome for the callback.
103 fn handle_choose(
104 &self,
105 role: &str,
106 partner: &str,
107 labels: &[String],
108 state: &[Value],
109 ) -> EffectResult<String>;
110
111 /// Perform an integration step after a protocol round.
112 ///
113 /// Called after all sends/receives for a tick are complete.
114 ///
115 /// Returns a typed outcome for the callback.
116 fn step(&self, role: &str, state: &mut Vec<Value>) -> EffectResult<()>;
117
118 /// Attempt to acquire a guard layer.
119 ///
120 /// Returning `EffectResult::Blocked` causes the coroutine to block.
121 /// `Success(evidence)` grants the acquire and binds the evidence value.
122 fn handle_acquire(
123 &self,
124 _sid: SessionId,
125 _role: &str,
126 _layer: &str,
127 _state: &[Value],
128 ) -> EffectResult<Value> {
129 EffectResult::success(Value::Unit)
130 }
131
132 /// Release a guard layer using previously acquired evidence.
133 fn handle_release(
134 &self,
135 _sid: SessionId,
136 _role: &str,
137 _layer: &str,
138 _evidence: &Value,
139 _state: &[Value],
140 ) -> EffectResult<()> {
141 EffectResult::success(())
142 }
143
144 /// Topology perturbations injected by the environment for this scheduler tick.
145 ///
146 /// The VM ingests these before selecting coroutines for the round. This is
147 /// a canonical ingress surface for external events; implementations should
148 /// stage async discoveries before this method is called rather than doing
149 /// async work from inside the callback.
150 ///
151 /// Returns a typed outcome for the callback.
152 fn topology_events(&self, _tick: u64) -> EffectResult<Vec<TopologyPerturbation>> {
153 EffectResult::success(Vec::new())
154 }
155
156 /// Optional output-condition metadata for commit gating.
157 ///
158 /// The VM calls this only when a step emits observable events. Returning `None`
159 /// delegates to VM-default metadata.
160 fn output_condition_hint(
161 &self,
162 _sid: SessionId,
163 _role: &str,
164 _state: &[Value],
165 ) -> Option<OutputConditionHint> {
166 None
167 }
168}
169
170impl<T: EffectHandler + ?Sized> EffectHandler for &T {
171 fn handler_identity(&self) -> String {
172 (**self).handler_identity()
173 }
174
175 fn handle_send(
176 &self,
177 role: &str,
178 partner: &str,
179 label: &str,
180 state: &[Value],
181 ) -> EffectResult<Value> {
182 (**self).handle_send(role, partner, label, state)
183 }
184
185 fn send_decision(&self, input: SendDecisionInput<'_>) -> EffectResult<SendDecision> {
186 (**self).send_decision(input)
187 }
188
189 fn send_decision_fast_path(
190 &self,
191 fast_path: SendDecisionFastPathInput<'_>,
192 state: &[Value],
193 payload: Option<&Value>,
194 ) -> Option<EffectResult<SendDecision>> {
195 (**self).send_decision_fast_path(fast_path, state, payload)
196 }
197
198 fn handle_recv(
199 &self,
200 role: &str,
201 partner: &str,
202 label: &str,
203 state: &mut Vec<Value>,
204 payload: &Value,
205 ) -> EffectResult<()> {
206 (**self).handle_recv(role, partner, label, state, payload)
207 }
208
209 fn handle_choose(
210 &self,
211 role: &str,
212 partner: &str,
213 labels: &[String],
214 state: &[Value],
215 ) -> EffectResult<String> {
216 (**self).handle_choose(role, partner, labels, state)
217 }
218
219 fn step(&self, role: &str, state: &mut Vec<Value>) -> EffectResult<()> {
220 (**self).step(role, state)
221 }
222
223 fn handle_acquire(
224 &self,
225 sid: SessionId,
226 role: &str,
227 layer: &str,
228 state: &[Value],
229 ) -> EffectResult<Value> {
230 (**self).handle_acquire(sid, role, layer, state)
231 }
232
233 fn handle_release(
234 &self,
235 sid: SessionId,
236 role: &str,
237 layer: &str,
238 evidence: &Value,
239 state: &[Value],
240 ) -> EffectResult<()> {
241 (**self).handle_release(sid, role, layer, evidence, state)
242 }
243
244 fn topology_events(&self, tick: u64) -> EffectResult<Vec<TopologyPerturbation>> {
245 (**self).topology_events(tick)
246 }
247
248 fn output_condition_hint(
249 &self,
250 sid: SessionId,
251 role: &str,
252 state: &[Value],
253 ) -> Option<OutputConditionHint> {
254 (**self).output_condition_hint(sid, role, state)
255 }
256}