Skip to main content

ff_core/
contracts.rs

1//! Phase 1 function contracts — Args + Result types for each FCALL.
2//!
3//! Each Args struct defines the typed inputs to a Valkey Function.
4//! Each Result enum defines the possible outcomes (success variants + error codes).
5
6use crate::policy::ExecutionPolicy;
7use crate::state::{AttemptType, PublicState, StateVector};
8use crate::types::{
9    AttemptId, AttemptIndex, CancelSource, ExecutionId, LaneId, LeaseEpoch, LeaseId, Namespace,
10    SignalId, SuspensionId, TimestampMs, WaitpointId, WaitpointToken, WorkerId, WorkerInstanceId,
11};
12use serde::{Deserialize, Serialize};
13use std::collections::{BTreeSet, HashMap};
14
15// ─── create_execution ───
16
17#[derive(Clone, Debug, Serialize, Deserialize)]
18pub struct CreateExecutionArgs {
19    pub execution_id: ExecutionId,
20    pub namespace: Namespace,
21    pub lane_id: LaneId,
22    pub execution_kind: String,
23    pub input_payload: Vec<u8>,
24    #[serde(default)]
25    pub payload_encoding: Option<String>,
26    pub priority: i32,
27    pub creator_identity: String,
28    #[serde(default)]
29    pub idempotency_key: Option<String>,
30    #[serde(default)]
31    pub tags: HashMap<String, String>,
32    /// Execution policy (retry, timeout, suspension, routing, etc.).
33    #[serde(default)]
34    pub policy: Option<ExecutionPolicy>,
35    /// If set and in the future, execution starts delayed.
36    #[serde(default)]
37    pub delay_until: Option<TimestampMs>,
38    /// Absolute deadline timestamp (ms). Execution expires if not complete by this time.
39    #[serde(default)]
40    pub execution_deadline_at: Option<TimestampMs>,
41    /// Partition ID (pre-computed).
42    pub partition_id: u16,
43    pub now: TimestampMs,
44}
45
46#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
47pub enum CreateExecutionResult {
48    /// Execution created successfully.
49    Created {
50        execution_id: ExecutionId,
51        public_state: PublicState,
52    },
53    /// Idempotent duplicate — existing execution returned.
54    Duplicate { execution_id: ExecutionId },
55}
56
57// ─── issue_claim_grant ───
58
59#[derive(Clone, Debug, Serialize, Deserialize)]
60pub struct IssueClaimGrantArgs {
61    pub execution_id: ExecutionId,
62    pub lane_id: LaneId,
63    pub worker_id: WorkerId,
64    pub worker_instance_id: WorkerInstanceId,
65    #[serde(default)]
66    pub capability_hash: Option<String>,
67    #[serde(default)]
68    pub route_snapshot_json: Option<String>,
69    #[serde(default)]
70    pub admission_summary: Option<String>,
71    /// Capabilities this worker advertises. Serialized as a sorted,
72    /// comma-separated string to the Lua FCALL (see scheduling.lua
73    /// ff_issue_claim_grant). An empty set matches only executions whose
74    /// `required_capabilities` is also empty.
75    #[serde(default)]
76    pub worker_capabilities: BTreeSet<String>,
77    pub grant_ttl_ms: u64,
78    /// Caller-side timestamp for bookkeeping. NOT passed to the Lua FCALL —
79    /// ff_issue_claim_grant uses `redis.call("TIME")` for grant_expires_at.
80    pub now: TimestampMs,
81}
82
83#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
84pub enum IssueClaimGrantResult {
85    /// Grant issued.
86    Granted { execution_id: ExecutionId },
87}
88
89/// A claim grant issued by the scheduler for a specific execution.
90///
91/// The worker uses this to call `ff_claim_execution` (or
92/// `ff_acquire_lease`), which atomically consumes the grant and
93/// creates the lease.
94///
95/// Shared wire-level type between `ff-scheduler` (issuer) and
96/// `ff-sdk` (consumer, via `FlowFabricWorker::claim_from_grant`).
97/// Lives in `ff-core` so neither crate needs a dep on the other.
98///
99/// **Lane asymmetry with [`ReclaimGrant`]:** `ClaimGrant` does NOT
100/// carry `lane_id`. The issuing scheduler's caller already picked
101/// a lane (that's how admission reached this grant) and passes it
102/// through to `claim_from_grant` as a separate argument. The grant
103/// handle stays narrow to what uniquely identifies the admission
104/// decision. The matching field on [`ReclaimGrant`] is an
105/// intentional divergence — see the note on that type.
106#[derive(Clone, Debug, PartialEq, Eq)]
107pub struct ClaimGrant {
108    /// The execution that was granted.
109    pub execution_id: ExecutionId,
110    /// The partition where this execution lives.
111    pub partition: crate::partition::Partition,
112    /// The Valkey key holding the grant hash (for the worker to
113    /// reference).
114    pub grant_key: String,
115    /// When the grant expires if not consumed.
116    pub expires_at_ms: u64,
117}
118
119/// A reclaim grant issued for a resumed (attempt_interrupted) execution.
120///
121/// Issued by a producer (typically `ff-scheduler` once a Batch-C
122/// reclaim scanner is in place; test fixtures in the interim — no
123/// production Rust caller exists in-tree today). Consumed by
124/// [`FlowFabricWorker::claim_from_reclaim_grant`], which calls
125/// `ff_claim_resumed_execution` atomically: that FCALL validates the
126/// grant, consumes it, and transitions `attempt_interrupted` →
127/// `started` while preserving the existing `attempt_index` +
128/// `attempt_id` (a resumed execution re-uses its attempt; it does
129/// not start a new one).
130///
131/// Mirrors [`ClaimGrant`] for the resume path. Differences:
132///
133///   * [`ClaimGrant`] is issued against a freshly-eligible
134///     execution and `ff_claim_execution` creates a new attempt.
135///   * [`ReclaimGrant`] is issued against an `attempt_interrupted`
136///     execution; `ff_claim_resumed_execution` re-uses the existing
137///     attempt and bumps the lease epoch.
138///
139/// The grant itself is written to the same `claim_grant` Valkey key
140/// that [`ClaimGrant`] uses; the distinction is which Lua FCALL
141/// consumes it (`ff_claim_execution` for new attempts,
142/// `ff_claim_resumed_execution` for resumes).
143///
144/// **Lane asymmetry with [`ClaimGrant`]:** `ReclaimGrant` CARRIES
145/// `lane_id` as a field. The issuing path already knows the lane
146/// (it's read from `exec_core` at grant time); carrying it here
147/// spares the consumer a `HGET exec_core lane_id` round trip on
148/// the hot claim path. The asymmetry is intentional — prefer
149/// one-fewer-HGET on a type that already lives with the resumer's
150/// lifecycle over strict handle symmetry with `ClaimGrant`.
151///
152/// Shared wire-level type between the eventual `ff-scheduler`
153/// producer (Batch-C reclaim scanner — not yet in-tree; test
154/// fixtures construct this type today) and `ff-sdk` (consumer, via
155/// `FlowFabricWorker::claim_from_reclaim_grant`). Lives in
156/// `ff-core` so neither crate needs a dep on the other.
157///
158/// [`FlowFabricWorker::claim_from_reclaim_grant`]: https://docs.rs/ff-sdk
159#[derive(Clone, Debug, PartialEq, Eq)]
160pub struct ReclaimGrant {
161    /// The execution granted for resumption.
162    pub execution_id: ExecutionId,
163    /// The partition the execution lives on.
164    pub partition: crate::partition::Partition,
165    /// Valkey key of the grant hash — same key shape as
166    /// [`ClaimGrant`].
167    pub grant_key: String,
168    /// Monotonic ms when the grant expires; unconsumed grants
169    /// vanish.
170    pub expires_at_ms: u64,
171    /// Lane the execution belongs to. Needed by
172    /// `ff_claim_resumed_execution` for `KEYS[3]` (eligible_zset)
173    /// and `KEYS[9]` (active_index).
174    pub lane_id: LaneId,
175}
176
177// ─── claim_execution ───
178
179#[derive(Clone, Debug, Serialize, Deserialize)]
180pub struct ClaimExecutionArgs {
181    pub execution_id: ExecutionId,
182    pub worker_id: WorkerId,
183    pub worker_instance_id: WorkerInstanceId,
184    pub lane_id: LaneId,
185    pub lease_id: LeaseId,
186    pub lease_ttl_ms: u64,
187    pub attempt_id: AttemptId,
188    /// Expected attempt index (pre-read from exec_core.total_attempt_count).
189    /// Used for KEYS construction — must match what the Lua computes.
190    pub expected_attempt_index: AttemptIndex,
191    /// JSON-encoded attempt policy snapshot.
192    #[serde(default)]
193    pub attempt_policy_json: String,
194    /// Per-attempt timeout in ms.
195    #[serde(default)]
196    pub attempt_timeout_ms: Option<u64>,
197    /// Total execution deadline (absolute timestamp ms).
198    #[serde(default)]
199    pub execution_deadline_at: Option<i64>,
200    pub now: TimestampMs,
201}
202
203#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
204pub struct ClaimedExecution {
205    pub execution_id: ExecutionId,
206    pub lease_id: LeaseId,
207    pub lease_epoch: LeaseEpoch,
208    pub attempt_index: AttemptIndex,
209    pub attempt_id: AttemptId,
210    pub attempt_type: AttemptType,
211    pub lease_expires_at: TimestampMs,
212}
213
214#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
215pub enum ClaimExecutionResult {
216    /// Successfully claimed.
217    Claimed(ClaimedExecution),
218}
219
220// ─── complete_execution ───
221
222#[derive(Clone, Debug, Serialize, Deserialize)]
223pub struct CompleteExecutionArgs {
224    pub execution_id: ExecutionId,
225    pub lease_id: LeaseId,
226    pub lease_epoch: LeaseEpoch,
227    pub attempt_index: AttemptIndex,
228    pub attempt_id: AttemptId,
229    #[serde(default)]
230    pub result_payload: Option<Vec<u8>>,
231    #[serde(default)]
232    pub result_encoding: Option<String>,
233    pub now: TimestampMs,
234}
235
236#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
237pub enum CompleteExecutionResult {
238    /// Execution completed successfully.
239    Completed {
240        execution_id: ExecutionId,
241        public_state: PublicState,
242    },
243}
244
245// ─── renew_lease ───
246
247#[derive(Clone, Debug, Serialize, Deserialize)]
248pub struct RenewLeaseArgs {
249    pub execution_id: ExecutionId,
250    pub attempt_index: AttemptIndex,
251    pub attempt_id: AttemptId,
252    pub lease_id: LeaseId,
253    pub lease_epoch: LeaseEpoch,
254    /// How long to extend the lease (milliseconds).
255    pub lease_ttl_ms: u64,
256    /// Grace period after lease_expires_at before the lease_current key is auto-deleted.
257    #[serde(default = "default_lease_history_grace_ms")]
258    pub lease_history_grace_ms: u64,
259}
260
261fn default_lease_history_grace_ms() -> u64 {
262    60_000
263}
264
265#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
266pub enum RenewLeaseResult {
267    /// Lease renewed.
268    Renewed { expires_at: TimestampMs },
269}
270
271// ─── mark_lease_expired_if_due ───
272
273#[derive(Clone, Debug, Serialize, Deserialize)]
274pub struct MarkLeaseExpiredArgs {
275    pub execution_id: ExecutionId,
276}
277
278#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
279pub enum MarkLeaseExpiredResult {
280    /// Lease was marked as expired.
281    MarkedExpired,
282    /// No action needed (already expired, not yet due, not active, etc.).
283    AlreadySatisfied { reason: String },
284}
285
286// ─── cancel_execution ───
287
288#[derive(Clone, Debug, Serialize, Deserialize)]
289pub struct CancelExecutionArgs {
290    pub execution_id: ExecutionId,
291    pub reason: String,
292    #[serde(default)]
293    pub source: CancelSource,
294    /// Required if not operator_override and execution is active.
295    #[serde(default)]
296    pub lease_id: Option<LeaseId>,
297    #[serde(default)]
298    pub lease_epoch: Option<LeaseEpoch>,
299    /// Required if not operator_override and execution is active.
300    #[serde(default)]
301    pub attempt_id: Option<AttemptId>,
302    pub now: TimestampMs,
303}
304
305#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
306pub enum CancelExecutionResult {
307    /// Execution cancelled.
308    Cancelled {
309        execution_id: ExecutionId,
310        public_state: PublicState,
311    },
312}
313
314// ─── revoke_lease ───
315
316#[derive(Clone, Debug, Serialize, Deserialize)]
317pub struct RevokeLeaseArgs {
318    pub execution_id: ExecutionId,
319    /// If set, only revoke if this matches the current lease. Empty string skips check.
320    #[serde(default)]
321    pub expected_lease_id: Option<String>,
322    /// Worker instance whose lease set to clean up. Read from exec_core before calling.
323    pub worker_instance_id: WorkerInstanceId,
324    pub reason: String,
325}
326
327#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
328pub enum RevokeLeaseResult {
329    /// Lease revoked.
330    Revoked { lease_id: String, lease_epoch: String },
331    /// Already revoked or expired — no action needed.
332    AlreadySatisfied { reason: String },
333}
334
335// ─── delay_execution ───
336
337#[derive(Clone, Debug, Serialize, Deserialize)]
338pub struct DelayExecutionArgs {
339    pub execution_id: ExecutionId,
340    pub lease_id: LeaseId,
341    pub lease_epoch: LeaseEpoch,
342    pub attempt_index: AttemptIndex,
343    pub attempt_id: AttemptId,
344    pub delay_until: TimestampMs,
345    pub now: TimestampMs,
346}
347
348#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
349pub enum DelayExecutionResult {
350    /// Execution delayed.
351    Delayed {
352        execution_id: ExecutionId,
353        public_state: PublicState,
354    },
355}
356
357// ─── move_to_waiting_children ───
358
359#[derive(Clone, Debug, Serialize, Deserialize)]
360pub struct MoveToWaitingChildrenArgs {
361    pub execution_id: ExecutionId,
362    pub lease_id: LeaseId,
363    pub lease_epoch: LeaseEpoch,
364    pub attempt_index: AttemptIndex,
365    pub attempt_id: AttemptId,
366    pub now: TimestampMs,
367}
368
369#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
370pub enum MoveToWaitingChildrenResult {
371    /// Moved to waiting children.
372    Moved {
373        execution_id: ExecutionId,
374        public_state: PublicState,
375    },
376}
377
378// ─── change_priority ───
379
380#[derive(Clone, Debug, Serialize, Deserialize)]
381pub struct ChangePriorityArgs {
382    pub execution_id: ExecutionId,
383    pub new_priority: i32,
384    pub lane_id: LaneId,
385    pub now: TimestampMs,
386}
387
388#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
389pub enum ChangePriorityResult {
390    /// Priority changed and re-scored.
391    Changed { execution_id: ExecutionId },
392}
393
394// ─── update_progress ───
395
396#[derive(Clone, Debug, Serialize, Deserialize)]
397pub struct UpdateProgressArgs {
398    pub execution_id: ExecutionId,
399    pub lease_id: LeaseId,
400    pub lease_epoch: LeaseEpoch,
401    pub attempt_id: AttemptId,
402    #[serde(default)]
403    pub progress_pct: Option<u8>,
404    #[serde(default)]
405    pub progress_message: Option<String>,
406    pub now: TimestampMs,
407}
408
409#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
410pub enum UpdateProgressResult {
411    /// Progress updated.
412    Updated,
413}
414
415// ═══════════════════════════════════════════════════════════════════════
416// Phase 2 contracts: fail, reclaim, expire
417// ═══════════════════════════════════════════════════════════════════════
418
419// ─── fail_execution ───
420
421#[derive(Clone, Debug, Serialize, Deserialize)]
422pub struct FailExecutionArgs {
423    pub execution_id: ExecutionId,
424    pub lease_id: LeaseId,
425    pub lease_epoch: LeaseEpoch,
426    pub attempt_index: AttemptIndex,
427    pub attempt_id: AttemptId,
428    pub failure_reason: String,
429    pub failure_category: String,
430    /// JSON-encoded retry policy (from execution policy). Empty = no retries.
431    #[serde(default)]
432    pub retry_policy_json: String,
433    /// JSON-encoded attempt policy for the next retry attempt.
434    #[serde(default)]
435    pub next_attempt_policy_json: String,
436}
437
438/// Outcome of a fail_execution call.
439#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
440pub enum FailExecutionResult {
441    /// Retry was scheduled — execution is delayed with backoff.
442    RetryScheduled {
443        delay_until: TimestampMs,
444        next_attempt_index: AttemptIndex,
445    },
446    /// No retries left — execution is terminal failed.
447    TerminalFailed,
448}
449
450// ─── issue_reclaim_grant ───
451
452#[derive(Clone, Debug, Serialize, Deserialize)]
453pub struct IssueReclaimGrantArgs {
454    pub execution_id: ExecutionId,
455    pub worker_id: WorkerId,
456    pub worker_instance_id: WorkerInstanceId,
457    pub lane_id: LaneId,
458    #[serde(default)]
459    pub capability_hash: Option<String>,
460    pub grant_ttl_ms: u64,
461    #[serde(default)]
462    pub route_snapshot_json: Option<String>,
463    #[serde(default)]
464    pub admission_summary: Option<String>,
465    /// Caller-side timestamp for bookkeeping. NOT passed to the Lua FCALL —
466    /// ff_issue_reclaim_grant uses `redis.call("TIME")` for grant_expires_at
467    /// (same as ff_issue_claim_grant). Kept for contract symmetry with
468    /// IssueClaimGrantArgs and scheduler audit logging.
469    pub now: TimestampMs,
470}
471
472#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
473pub enum IssueReclaimGrantResult {
474    /// Reclaim grant issued.
475    Granted { expires_at_ms: TimestampMs },
476}
477
478// ─── reclaim_execution ───
479
480#[derive(Clone, Debug, Serialize, Deserialize)]
481pub struct ReclaimExecutionArgs {
482    pub execution_id: ExecutionId,
483    pub worker_id: WorkerId,
484    pub worker_instance_id: WorkerInstanceId,
485    pub lane_id: LaneId,
486    #[serde(default)]
487    pub capability_hash: Option<String>,
488    pub lease_id: LeaseId,
489    pub lease_ttl_ms: u64,
490    pub attempt_id: AttemptId,
491    /// JSON-encoded attempt policy for the reclaim attempt.
492    #[serde(default)]
493    pub attempt_policy_json: String,
494    /// Maximum reclaim count before terminal failure. Default: 100.
495    #[serde(default = "default_max_reclaim_count")]
496    pub max_reclaim_count: u32,
497    /// Old worker instance (for old_worker_leases key construction).
498    pub old_worker_instance_id: WorkerInstanceId,
499    /// Current attempt index (for old_attempt/old_stream_meta key construction).
500    pub current_attempt_index: AttemptIndex,
501}
502
503fn default_max_reclaim_count() -> u32 {
504    100
505}
506
507#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
508pub enum ReclaimExecutionResult {
509    /// Execution reclaimed — new attempt + new lease.
510    Reclaimed {
511        new_attempt_index: AttemptIndex,
512        new_attempt_id: AttemptId,
513        new_lease_id: LeaseId,
514        new_lease_epoch: LeaseEpoch,
515        lease_expires_at: TimestampMs,
516    },
517    /// Max reclaims exceeded — execution moved to terminal.
518    MaxReclaimsExceeded,
519}
520
521// ─── expire_execution ───
522
523#[derive(Clone, Debug, Serialize, Deserialize)]
524pub struct ExpireExecutionArgs {
525    pub execution_id: ExecutionId,
526    /// "attempt_timeout" or "execution_deadline"
527    pub expire_reason: String,
528}
529
530#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
531pub enum ExpireExecutionResult {
532    /// Execution expired.
533    Expired { execution_id: ExecutionId },
534    /// Already terminal — no-op.
535    AlreadyTerminal,
536}
537
538// ═══════════════════════════════════════════════════════════════════════
539// Phase 3 contracts: suspend, signal, resume, waitpoint
540// ═══════════════════════════════════════════════════════════════════════
541
542// ─── suspend_execution ───
543
544#[derive(Clone, Debug, Serialize, Deserialize)]
545pub struct SuspendExecutionArgs {
546    pub execution_id: ExecutionId,
547    pub lease_id: LeaseId,
548    pub lease_epoch: LeaseEpoch,
549    pub attempt_index: AttemptIndex,
550    pub attempt_id: AttemptId,
551    pub suspension_id: SuspensionId,
552    pub waitpoint_id: WaitpointId,
553    pub waitpoint_key: String,
554    pub reason_code: String,
555    pub requested_by: String,
556    pub resume_condition_json: String,
557    pub resume_policy_json: String,
558    #[serde(default)]
559    pub continuation_metadata_pointer: Option<String>,
560    #[serde(default)]
561    pub timeout_at: Option<TimestampMs>,
562    /// true to activate a pending waitpoint, false to create new.
563    #[serde(default)]
564    pub use_pending_waitpoint: bool,
565    /// Timeout behavior: "fail", "cancel", "expire", "auto_resume", "escalate".
566    #[serde(default = "default_timeout_behavior")]
567    pub timeout_behavior: String,
568}
569
570fn default_timeout_behavior() -> String {
571    "fail".to_owned()
572}
573
574#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
575pub enum SuspendExecutionResult {
576    /// Execution suspended, waitpoint active.
577    Suspended {
578        suspension_id: SuspensionId,
579        waitpoint_id: WaitpointId,
580        waitpoint_key: String,
581        /// HMAC-SHA1 token bound to (waitpoint_id, waitpoint_key, created_at).
582        /// Required by signal-delivery callers to authenticate against this
583        /// waitpoint (RFC-004 §Waitpoint Security).
584        waitpoint_token: WaitpointToken,
585    },
586    /// Buffered signals already satisfied the condition — suspension skipped.
587    /// Lease is still held. Token comes from the pending waitpoint record.
588    AlreadySatisfied {
589        suspension_id: SuspensionId,
590        waitpoint_id: WaitpointId,
591        waitpoint_key: String,
592        waitpoint_token: WaitpointToken,
593    },
594}
595
596// ─── resume_execution ───
597
598#[derive(Clone, Debug, Serialize, Deserialize)]
599pub struct ResumeExecutionArgs {
600    pub execution_id: ExecutionId,
601    /// "signal", "operator", "auto_resume"
602    #[serde(default = "default_trigger_type")]
603    pub trigger_type: String,
604    /// Optional delay before becoming eligible (ms).
605    #[serde(default)]
606    pub resume_delay_ms: u64,
607}
608
609fn default_trigger_type() -> String {
610    "signal".to_owned()
611}
612
613#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
614pub enum ResumeExecutionResult {
615    /// Execution resumed to runnable.
616    Resumed { public_state: PublicState },
617}
618
619// ─── create_pending_waitpoint ───
620
621#[derive(Clone, Debug, Serialize, Deserialize)]
622pub struct CreatePendingWaitpointArgs {
623    pub execution_id: ExecutionId,
624    pub lease_id: LeaseId,
625    pub lease_epoch: LeaseEpoch,
626    pub attempt_index: AttemptIndex,
627    pub attempt_id: AttemptId,
628    pub waitpoint_id: WaitpointId,
629    pub waitpoint_key: String,
630    /// Short expiry for the pending waitpoint (ms).
631    pub expires_in_ms: u64,
632}
633
634#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
635pub enum CreatePendingWaitpointResult {
636    /// Pending waitpoint created.
637    Created {
638        waitpoint_id: WaitpointId,
639        waitpoint_key: String,
640        /// HMAC-SHA1 token bound to the pending waitpoint. Required for
641        /// `buffer_signal_for_pending_waitpoint` and carried forward when
642        /// the waitpoint is activated by `suspend_execution`.
643        waitpoint_token: WaitpointToken,
644    },
645}
646
647// ─── close_waitpoint ───
648
649#[derive(Clone, Debug, Serialize, Deserialize)]
650pub struct CloseWaitpointArgs {
651    pub waitpoint_id: WaitpointId,
652    pub reason: String,
653}
654
655#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
656pub enum CloseWaitpointResult {
657    /// Waitpoint closed.
658    Closed,
659}
660
661// ─── deliver_signal ───
662
663#[derive(Clone, Debug, Serialize, Deserialize)]
664pub struct DeliverSignalArgs {
665    pub execution_id: ExecutionId,
666    pub waitpoint_id: WaitpointId,
667    pub signal_id: SignalId,
668    pub signal_name: String,
669    pub signal_category: String,
670    pub source_type: String,
671    pub source_identity: String,
672    #[serde(default)]
673    pub payload: Option<Vec<u8>>,
674    #[serde(default)]
675    pub payload_encoding: Option<String>,
676    #[serde(default)]
677    pub correlation_id: Option<String>,
678    #[serde(default)]
679    pub idempotency_key: Option<String>,
680    pub target_scope: String,
681    #[serde(default)]
682    pub created_at: Option<TimestampMs>,
683    /// Dedup TTL for idempotency key (ms).
684    #[serde(default)]
685    pub dedup_ttl_ms: Option<u64>,
686    /// Resume delay after signal satisfaction (ms).
687    #[serde(default)]
688    pub resume_delay_ms: Option<u64>,
689    /// Max signals per execution (default 10000).
690    #[serde(default)]
691    pub max_signals_per_execution: Option<u64>,
692    /// MAXLEN for the waitpoint signal stream.
693    #[serde(default)]
694    pub signal_maxlen: Option<u64>,
695    /// HMAC-SHA1 token issued when the waitpoint was created. Required for
696    /// signal delivery; missing/tampered/rotated-past-grace tokens are
697    /// rejected with `invalid_token` or `token_expired` (RFC-004).
698    ///
699    /// Defense-in-depth: `WaitpointToken` is a transparent string newtype,
700    /// so an empty string deserializes successfully from JSON. The
701    /// validation boundary is in Lua (`validate_waitpoint_token` returns
702    /// `missing_token` on empty input); this type intentionally does NOT
703    /// pre-reject at the Rust layer so callers get a consistent typed
704    /// error regardless of how they constructed the args.
705    pub waitpoint_token: WaitpointToken,
706    pub now: TimestampMs,
707}
708
709#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
710pub enum DeliverSignalResult {
711    /// Signal accepted with the given effect.
712    Accepted {
713        signal_id: SignalId,
714        effect: String,
715    },
716    /// Duplicate signal (idempotency key matched).
717    Duplicate { existing_signal_id: SignalId },
718}
719
720// ─── buffer_signal_for_pending_waitpoint ───
721
722#[derive(Clone, Debug, Serialize, Deserialize)]
723pub struct BufferSignalArgs {
724    pub execution_id: ExecutionId,
725    pub waitpoint_id: WaitpointId,
726    pub signal_id: SignalId,
727    pub signal_name: String,
728    pub signal_category: String,
729    pub source_type: String,
730    pub source_identity: String,
731    #[serde(default)]
732    pub payload: Option<Vec<u8>>,
733    #[serde(default)]
734    pub payload_encoding: Option<String>,
735    #[serde(default)]
736    pub idempotency_key: Option<String>,
737    pub target_scope: String,
738    /// HMAC-SHA1 token issued when `create_pending_waitpoint` ran. Required
739    /// to authenticate early signals targeting the pending waitpoint.
740    pub waitpoint_token: WaitpointToken,
741    pub now: TimestampMs,
742}
743
744#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
745pub enum BufferSignalResult {
746    /// Signal buffered for pending waitpoint.
747    Buffered { signal_id: SignalId },
748    /// Duplicate signal.
749    Duplicate { existing_signal_id: SignalId },
750}
751
752// ─── list_pending_waitpoints ───
753
754/// One entry in the read-only view of an execution's active waitpoints.
755///
756/// Returned by `Server::list_pending_waitpoints` (and the
757/// `GET /v1/executions/{id}/pending-waitpoints` REST endpoint). The
758/// `waitpoint_token` is the same HMAC-SHA1 credential a suspending worker
759/// receives in `SuspendOutcome::Suspended` — a reviewer that needs to
760/// deliver a signal against this waitpoint must present it in
761/// `DeliverSignalArgs::waitpoint_token`.
762///
763/// Exposing the token here is a deliberate API gap closure: a
764/// human-in-the-loop reviewer has no other path to the token, since only
765/// the suspending worker sees the `SuspendOutcome`. Access is gated by
766/// the same bearer-auth middleware as every other REST endpoint.
767#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
768pub struct PendingWaitpointInfo {
769    pub waitpoint_id: WaitpointId,
770    pub waitpoint_key: String,
771    /// Current waitpoint state: `pending`, `active`, `closed`. Callers
772    /// typically filter to `pending` or `active`.
773    pub state: String,
774    /// HMAC-SHA1 token minted at create time; required by
775    /// `ff_deliver_signal` and `ff_buffer_signal_for_pending_waitpoint`.
776    pub waitpoint_token: WaitpointToken,
777    /// Signal names the resume condition is waiting for. Reviewers that
778    /// need to drive a specific waitpoint — particularly when multiple
779    /// concurrent waitpoints exist on one execution — filter on this to
780    /// pick the right target.
781    ///
782    /// An EMPTY vec means the condition matches any signal (wildcard, per
783    /// `lua/helpers.lua` `initialize_condition`). Callers must not infer
784    /// "no waitpoint" from empty; check `state` / length of the outer
785    /// list for that.
786    #[serde(default)]
787    pub required_signal_names: Vec<String>,
788    /// Timestamp when the waitpoint record was first written.
789    pub created_at: TimestampMs,
790    /// Timestamp when the waitpoint was activated (suspension landed).
791    /// `None` while the waitpoint is still `pending`.
792    #[serde(default, skip_serializing_if = "Option::is_none")]
793    pub activated_at: Option<TimestampMs>,
794    /// Scheduled expiration timestamp. `None` if no timeout configured.
795    #[serde(default, skip_serializing_if = "Option::is_none")]
796    pub expires_at: Option<TimestampMs>,
797}
798
799// ─── expire_suspension ───
800
801#[derive(Clone, Debug, Serialize, Deserialize)]
802pub struct ExpireSuspensionArgs {
803    pub execution_id: ExecutionId,
804}
805
806#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
807pub enum ExpireSuspensionResult {
808    /// Suspension expired with the given behavior applied.
809    Expired { behavior_applied: String },
810    /// Already resolved — no action needed.
811    AlreadySatisfied { reason: String },
812}
813
814// ─── claim_resumed_execution ───
815
816#[derive(Clone, Debug, Serialize, Deserialize)]
817pub struct ClaimResumedExecutionArgs {
818    pub execution_id: ExecutionId,
819    pub worker_id: WorkerId,
820    pub worker_instance_id: WorkerInstanceId,
821    pub lane_id: LaneId,
822    pub lease_id: LeaseId,
823    pub lease_ttl_ms: u64,
824    /// Current attempt index (for KEYS construction — from exec_core).
825    pub current_attempt_index: AttemptIndex,
826    /// Remaining attempt timeout from before suspension (ms). 0 = no timeout.
827    #[serde(default)]
828    pub remaining_attempt_timeout_ms: Option<u64>,
829    pub now: TimestampMs,
830}
831
832#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
833pub struct ClaimedResumedExecution {
834    pub execution_id: ExecutionId,
835    pub lease_id: LeaseId,
836    pub lease_epoch: LeaseEpoch,
837    pub attempt_index: AttemptIndex,
838    pub attempt_id: AttemptId,
839    pub lease_expires_at: TimestampMs,
840}
841
842#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
843pub enum ClaimResumedExecutionResult {
844    /// Successfully claimed resumed execution (same attempt continues).
845    Claimed(ClaimedResumedExecution),
846}
847
848// ═══════════════════════════════════════════════════════════════════════
849// Phase 4 contracts: stream
850// ═══════════════════════════════════════════════════════════════════════
851
852// ─── append_frame ───
853
854#[derive(Clone, Debug, Serialize, Deserialize)]
855pub struct AppendFrameArgs {
856    pub execution_id: ExecutionId,
857    pub attempt_index: AttemptIndex,
858    pub lease_id: LeaseId,
859    pub lease_epoch: LeaseEpoch,
860    pub attempt_id: AttemptId,
861    pub frame_type: String,
862    pub timestamp: TimestampMs,
863    pub payload: Vec<u8>,
864    #[serde(default)]
865    pub encoding: Option<String>,
866    /// Optional structured metadata for the frame (JSON blob).
867    #[serde(default)]
868    pub metadata_json: Option<String>,
869    #[serde(default)]
870    pub correlation_id: Option<String>,
871    #[serde(default)]
872    pub source: Option<String>,
873    /// MAXLEN for the stream. 0 = no trim.
874    #[serde(default)]
875    pub retention_maxlen: Option<u32>,
876    /// Max payload bytes per frame. Default: 65536.
877    #[serde(default)]
878    pub max_payload_bytes: Option<u32>,
879}
880
881#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
882pub enum AppendFrameResult {
883    /// Frame appended successfully.
884    Appended {
885        /// Valkey Stream entry ID (e.g. "1713100800150-0").
886        entry_id: String,
887        /// Total frame count after this append.
888        frame_count: u64,
889    },
890}
891
892// ─── read_attempt_stream / tail_attempt_stream ───
893
894/// Hard cap on the number of frames returned by a single read/tail call.
895///
896/// Single source of truth across the Rust layer (ff-script, ff-server,
897/// ff-sdk). The Lua side in `lua/stream.lua` keeps a matching literal with
898/// an inline reference back here; bump both together if you ever need to
899/// lift the cap.
900pub const STREAM_READ_HARD_CAP: u64 = 10_000;
901
902/// A single frame read from an attempt-scoped stream.
903///
904/// Field set mirrors what `ff_append_frame` writes: `frame_type`, `ts`,
905/// `payload`, `encoding`, `source`, and optionally `correlation_id`. Stored
906/// as an ordered map so field order is deterministic across read calls.
907#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
908pub struct StreamFrame {
909    /// Valkey Stream entry ID, e.g. "1713100800150-0".
910    pub id: String,
911    /// Frame fields in sorted order.
912    pub fields: std::collections::BTreeMap<String, String>,
913}
914
915/// Inputs to `ff_read_attempt_stream` (XRANGE wrapper).
916#[derive(Clone, Debug, Serialize, Deserialize)]
917pub struct ReadFramesArgs {
918    pub execution_id: ExecutionId,
919    pub attempt_index: AttemptIndex,
920    /// XRANGE start ID. Use "-" for earliest.
921    pub from_id: String,
922    /// XRANGE end ID. Use "+" for latest.
923    pub to_id: String,
924    /// XRANGE COUNT limit. MUST be `>= 1`. The REST and SDK layers reject
925    /// `0` at the boundary; the Lua side rejects it too. `STREAM_READ_HARD_CAP`
926    /// is the upper bound.
927    pub count_limit: u64,
928}
929
930/// Result of reading frames from an attempt stream — frames plus terminal
931/// signal so consumers can stop polling without a timeout fallback.
932#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
933pub struct StreamFrames {
934    /// Entries in the requested range (possibly empty).
935    pub frames: Vec<StreamFrame>,
936    /// Timestamp when the upstream writer closed the stream. `None` if the
937    /// stream is still open (or has never been written).
938    #[serde(default, skip_serializing_if = "Option::is_none")]
939    pub closed_at: Option<TimestampMs>,
940    /// Reason from the closing writer. Current values:
941    /// `attempt_success`, `attempt_failure`, `attempt_cancelled`,
942    /// `attempt_interrupted`. `None` iff the stream is still open.
943    #[serde(default, skip_serializing_if = "Option::is_none")]
944    pub closed_reason: Option<String>,
945}
946
947impl StreamFrames {
948    /// Construct an empty open-stream result (no frames, no terminal
949    /// markers). Useful for fast-path peek helpers.
950    pub fn empty_open() -> Self {
951        Self { frames: Vec::new(), closed_at: None, closed_reason: None }
952    }
953
954    /// True iff the producer has closed this stream. Consumers should stop
955    /// polling and drain once this returns true.
956    pub fn is_closed(&self) -> bool {
957        self.closed_at.is_some()
958    }
959}
960
961#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
962pub enum ReadFramesResult {
963    /// Frames returned (possibly empty) plus optional closed markers.
964    Frames(StreamFrames),
965}
966
967// ═══════════════════════════════════════════════════════════════════════
968// Phase 5 contracts: budget, quota, block/unblock
969// ═══════════════════════════════════════════════════════════════════════
970
971// ─── create_budget ───
972
973#[derive(Clone, Debug, Serialize, Deserialize)]
974pub struct CreateBudgetArgs {
975    pub budget_id: crate::types::BudgetId,
976    pub scope_type: String,
977    pub scope_id: String,
978    pub enforcement_mode: String,
979    pub on_hard_limit: String,
980    pub on_soft_limit: String,
981    pub reset_interval_ms: u64,
982    /// Dimension names.
983    pub dimensions: Vec<String>,
984    /// Hard limits per dimension (parallel with dimensions).
985    pub hard_limits: Vec<u64>,
986    /// Soft limits per dimension (parallel with dimensions).
987    pub soft_limits: Vec<u64>,
988    pub now: TimestampMs,
989}
990
991#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
992pub enum CreateBudgetResult {
993    /// Budget created.
994    Created { budget_id: crate::types::BudgetId },
995    /// Already exists (idempotent).
996    AlreadySatisfied { budget_id: crate::types::BudgetId },
997}
998
999// ─── create_quota_policy ───
1000
1001#[derive(Clone, Debug, Serialize, Deserialize)]
1002pub struct CreateQuotaPolicyArgs {
1003    pub quota_policy_id: crate::types::QuotaPolicyId,
1004    pub window_seconds: u64,
1005    pub max_requests_per_window: u64,
1006    pub max_concurrent: u64,
1007    pub now: TimestampMs,
1008}
1009
1010#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1011pub enum CreateQuotaPolicyResult {
1012    /// Quota policy created.
1013    Created { quota_policy_id: crate::types::QuotaPolicyId },
1014    /// Already exists (idempotent).
1015    AlreadySatisfied { quota_policy_id: crate::types::QuotaPolicyId },
1016}
1017
1018// ─── budget_status (read-only) ───
1019
1020/// Operator-facing budget status snapshot (not an FCALL — direct HGETALL reads).
1021#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1022pub struct BudgetStatus {
1023    pub budget_id: String,
1024    pub scope_type: String,
1025    pub scope_id: String,
1026    pub enforcement_mode: String,
1027    /// Current usage per dimension: {dimension_name: current_value}.
1028    pub usage: HashMap<String, u64>,
1029    /// Hard limits per dimension: {dimension_name: limit}.
1030    pub hard_limits: HashMap<String, u64>,
1031    /// Soft limits per dimension: {dimension_name: limit}.
1032    pub soft_limits: HashMap<String, u64>,
1033    pub breach_count: u64,
1034    pub soft_breach_count: u64,
1035    pub last_breach_at: Option<String>,
1036    pub last_breach_dim: Option<String>,
1037    pub next_reset_at: Option<String>,
1038    pub created_at: Option<String>,
1039}
1040
1041// ─── report_usage_and_check ───
1042
1043#[derive(Clone, Debug, Serialize, Deserialize)]
1044pub struct ReportUsageArgs {
1045    /// Dimension names to increment.
1046    pub dimensions: Vec<String>,
1047    /// Increment values (parallel with dimensions).
1048    pub deltas: Vec<u64>,
1049    pub now: TimestampMs,
1050    /// Optional idempotency key to prevent double-counting on retries.
1051    /// Must share the budget's `{b:M}` hash tag for cluster safety.
1052    #[serde(default)]
1053    pub dedup_key: Option<String>,
1054}
1055
1056#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1057pub enum ReportUsageResult {
1058    /// All increments applied, no breach.
1059    Ok,
1060    /// Soft limit breached on a dimension (advisory, increments applied).
1061    SoftBreach {
1062        dimension: String,
1063        current_usage: u64,
1064        soft_limit: u64,
1065    },
1066    /// Hard limit breached (increments NOT applied).
1067    HardBreach {
1068        dimension: String,
1069        current_usage: u64,
1070        hard_limit: u64,
1071    },
1072    /// Dedup key matched — usage already applied in a prior call.
1073    AlreadyApplied,
1074}
1075
1076// ─── reset_budget ───
1077
1078#[derive(Clone, Debug, Serialize, Deserialize)]
1079pub struct ResetBudgetArgs {
1080    pub budget_id: crate::types::BudgetId,
1081    pub now: TimestampMs,
1082}
1083
1084#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1085pub enum ResetBudgetResult {
1086    /// Budget reset successfully.
1087    Reset { next_reset_at: TimestampMs },
1088}
1089
1090// ─── check_admission_and_record ───
1091
1092#[derive(Clone, Debug, Serialize, Deserialize)]
1093pub struct CheckAdmissionArgs {
1094    pub execution_id: ExecutionId,
1095    pub now: TimestampMs,
1096    pub window_seconds: u64,
1097    pub rate_limit: u64,
1098    pub concurrency_cap: u64,
1099    #[serde(default)]
1100    pub jitter_ms: Option<u64>,
1101}
1102
1103#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1104pub enum CheckAdmissionResult {
1105    /// Admitted — execution may proceed.
1106    Admitted,
1107    /// Already admitted in this window (idempotent).
1108    AlreadyAdmitted,
1109    /// Rate limit exceeded.
1110    RateExceeded { retry_after_ms: u64 },
1111    /// Concurrency cap hit.
1112    ConcurrencyExceeded,
1113}
1114
1115// ─── release_admission ───
1116
1117#[derive(Clone, Debug, Serialize, Deserialize)]
1118pub struct ReleaseAdmissionArgs {
1119    pub execution_id: ExecutionId,
1120}
1121
1122#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1123pub enum ReleaseAdmissionResult {
1124    Released,
1125}
1126
1127// ─── block_execution_for_admission ───
1128
1129#[derive(Clone, Debug, Serialize, Deserialize)]
1130pub struct BlockExecutionArgs {
1131    pub execution_id: ExecutionId,
1132    pub blocking_reason: String,
1133    #[serde(default)]
1134    pub blocking_detail: Option<String>,
1135    pub now: TimestampMs,
1136}
1137
1138#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1139pub enum BlockExecutionResult {
1140    /// Execution blocked.
1141    Blocked,
1142}
1143
1144// ─── unblock_execution ───
1145
1146#[derive(Clone, Debug, Serialize, Deserialize)]
1147pub struct UnblockExecutionArgs {
1148    pub execution_id: ExecutionId,
1149    pub now: TimestampMs,
1150    /// Expected blocking reason (prevents stale unblock).
1151    #[serde(default)]
1152    pub expected_blocking_reason: Option<String>,
1153}
1154
1155#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1156pub enum UnblockExecutionResult {
1157    /// Execution unblocked and moved to eligible.
1158    Unblocked,
1159}
1160
1161// ═══════════════════════════════════════════════════════════════════════
1162// Phase 6 contracts: flow coordination and dependencies
1163// ═══════════════════════════════════════════════════════════════════════
1164
1165// ─── create_flow ───
1166
1167#[derive(Clone, Debug, Serialize, Deserialize)]
1168pub struct CreateFlowArgs {
1169    pub flow_id: crate::types::FlowId,
1170    pub flow_kind: String,
1171    pub namespace: Namespace,
1172    pub now: TimestampMs,
1173}
1174
1175#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1176pub enum CreateFlowResult {
1177    /// Flow created successfully.
1178    Created { flow_id: crate::types::FlowId },
1179    /// Flow already exists (idempotent).
1180    AlreadySatisfied { flow_id: crate::types::FlowId },
1181}
1182
1183// ─── add_execution_to_flow ───
1184
1185#[derive(Clone, Debug, Serialize, Deserialize)]
1186pub struct AddExecutionToFlowArgs {
1187    pub flow_id: crate::types::FlowId,
1188    pub execution_id: ExecutionId,
1189    pub now: TimestampMs,
1190}
1191
1192#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1193pub enum AddExecutionToFlowResult {
1194    /// Execution added to flow.
1195    Added {
1196        execution_id: ExecutionId,
1197        new_node_count: u32,
1198    },
1199    /// Already a member (idempotent).
1200    AlreadyMember {
1201        execution_id: ExecutionId,
1202        node_count: u32,
1203    },
1204}
1205
1206// ─── cancel_flow ───
1207
1208#[derive(Clone, Debug, Serialize, Deserialize)]
1209pub struct CancelFlowArgs {
1210    pub flow_id: crate::types::FlowId,
1211    pub reason: String,
1212    pub cancellation_policy: String,
1213    pub now: TimestampMs,
1214}
1215
1216#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1217pub enum CancelFlowResult {
1218    /// Flow cancelled and all member cancellations (if any) have completed
1219    /// synchronously. Used when `cancellation_policy != "cancel_all"`, when
1220    /// the flow has no members, when the caller opted into synchronous
1221    /// dispatch (e.g. `?wait=true`), or when the flow was already in a
1222    /// terminal state (idempotent retry).
1223    ///
1224    /// On the idempotent-retry path `member_execution_ids` may be *capped*
1225    /// at the server (default 1000 entries) to bound response bandwidth on
1226    /// flows with very large membership. The first (non-idempotent) call
1227    /// always returns the full list, so clients that need every member id
1228    /// should persist the initial response.
1229    Cancelled {
1230        cancellation_policy: String,
1231        member_execution_ids: Vec<String>,
1232    },
1233    /// Flow state was flipped to cancelled atomically, but member
1234    /// cancellations are dispatched asynchronously in the background.
1235    /// Clients may poll `GET /v1/executions/{id}/state` for each member
1236    /// execution id to track terminal state.
1237    CancellationScheduled {
1238        cancellation_policy: String,
1239        member_count: u32,
1240        member_execution_ids: Vec<String>,
1241    },
1242}
1243
1244// ─── stage_dependency_edge ───
1245
1246#[derive(Clone, Debug, Serialize, Deserialize)]
1247pub struct StageDependencyEdgeArgs {
1248    pub flow_id: crate::types::FlowId,
1249    pub edge_id: crate::types::EdgeId,
1250    pub upstream_execution_id: ExecutionId,
1251    pub downstream_execution_id: ExecutionId,
1252    #[serde(default = "default_dependency_kind")]
1253    pub dependency_kind: String,
1254    #[serde(default)]
1255    pub data_passing_ref: Option<String>,
1256    pub expected_graph_revision: u64,
1257    pub now: TimestampMs,
1258}
1259
1260fn default_dependency_kind() -> String {
1261    "success_only".to_owned()
1262}
1263
1264#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1265pub enum StageDependencyEdgeResult {
1266    /// Edge staged, new graph revision.
1267    Staged {
1268        edge_id: crate::types::EdgeId,
1269        new_graph_revision: u64,
1270    },
1271}
1272
1273// ─── apply_dependency_to_child ───
1274
1275#[derive(Clone, Debug, Serialize, Deserialize)]
1276pub struct ApplyDependencyToChildArgs {
1277    pub flow_id: crate::types::FlowId,
1278    pub edge_id: crate::types::EdgeId,
1279    /// The child execution that receives the dependency.
1280    pub downstream_execution_id: ExecutionId,
1281    pub upstream_execution_id: ExecutionId,
1282    pub graph_revision: u64,
1283    #[serde(default = "default_dependency_kind")]
1284    pub dependency_kind: String,
1285    #[serde(default)]
1286    pub data_passing_ref: Option<String>,
1287    pub now: TimestampMs,
1288}
1289
1290#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1291pub enum ApplyDependencyToChildResult {
1292    /// Dependency applied, N unsatisfied deps remaining.
1293    Applied { unsatisfied_count: u32 },
1294    /// Already applied (idempotent).
1295    AlreadyApplied,
1296}
1297
1298// ─── resolve_dependency ───
1299
1300#[derive(Clone, Debug, Serialize, Deserialize)]
1301pub struct ResolveDependencyArgs {
1302    pub edge_id: crate::types::EdgeId,
1303    /// "success", "failed", "cancelled", "expired", "skipped"
1304    pub upstream_outcome: String,
1305    pub now: TimestampMs,
1306}
1307
1308#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1309pub enum ResolveDependencyResult {
1310    /// Edge satisfied — downstream may become eligible.
1311    Satisfied,
1312    /// Edge made impossible — downstream becomes skipped.
1313    Impossible,
1314    /// Already resolved (idempotent).
1315    AlreadyResolved,
1316}
1317
1318// ─── promote_blocked_to_eligible ───
1319
1320#[derive(Clone, Debug, Serialize, Deserialize)]
1321pub struct PromoteBlockedToEligibleArgs {
1322    pub execution_id: ExecutionId,
1323    pub now: TimestampMs,
1324}
1325
1326#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1327pub enum PromoteBlockedToEligibleResult {
1328    Promoted,
1329}
1330
1331// ─── evaluate_flow_eligibility ───
1332
1333#[derive(Clone, Debug, Serialize, Deserialize)]
1334pub struct EvaluateFlowEligibilityArgs {
1335    pub execution_id: ExecutionId,
1336}
1337
1338#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1339pub enum EvaluateFlowEligibilityResult {
1340    /// Execution eligibility status.
1341    Status { status: String },
1342}
1343
1344// ─── replay_execution ───
1345
1346#[derive(Clone, Debug, Serialize, Deserialize)]
1347pub struct ReplayExecutionArgs {
1348    pub execution_id: ExecutionId,
1349    pub now: TimestampMs,
1350}
1351
1352#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1353pub enum ReplayExecutionResult {
1354    /// Replayed to runnable.
1355    Replayed { public_state: PublicState },
1356}
1357
1358// ─── get_execution (full read) ───
1359
1360/// Full execution info returned by `Server::get_execution`.
1361#[derive(Clone, Debug, Serialize, Deserialize)]
1362pub struct ExecutionInfo {
1363    pub execution_id: ExecutionId,
1364    pub namespace: String,
1365    pub lane_id: String,
1366    pub priority: i32,
1367    pub execution_kind: String,
1368    pub state_vector: StateVector,
1369    pub public_state: PublicState,
1370    pub created_at: String,
1371    /// TimestampMs (ms since epoch) when the execution's first attempt
1372    /// was started by a worker claim. Empty string until the first
1373    /// claim lands. Serialised as `Option<String>` so pre-claim reads
1374    /// deserialise cleanly even if the field is absent from the wire.
1375    #[serde(default, skip_serializing_if = "Option::is_none")]
1376    pub started_at: Option<String>,
1377    /// TimestampMs when the execution reached a terminal
1378    /// `completed`/`failed`/`cancelled`/`expired` state. Empty /
1379    /// absent while still in flight.
1380    #[serde(default, skip_serializing_if = "Option::is_none")]
1381    pub completed_at: Option<String>,
1382    pub current_attempt_index: u32,
1383    pub flow_id: Option<String>,
1384    pub blocking_detail: String,
1385}
1386
1387// ─── Common sub-types ───
1388
1389/// Summary of state after a mutation, returned by many functions.
1390#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1391pub struct StateSummary {
1392    pub state_vector: StateVector,
1393    pub current_attempt_index: AttemptIndex,
1394}
1395
1396#[cfg(test)]
1397mod tests {
1398    use super::*;
1399    use crate::types::FlowId;
1400
1401    #[test]
1402    fn create_execution_args_serde() {
1403        let config = crate::partition::PartitionConfig::default();
1404        let args = CreateExecutionArgs {
1405            execution_id: ExecutionId::for_flow(&FlowId::new(), &config),
1406            namespace: Namespace::new("test"),
1407            lane_id: LaneId::new("default"),
1408            execution_kind: "llm_call".to_owned(),
1409            input_payload: b"hello".to_vec(),
1410            payload_encoding: Some("json".to_owned()),
1411            priority: 0,
1412            creator_identity: "test-user".to_owned(),
1413            idempotency_key: None,
1414            tags: HashMap::new(),
1415            policy: None,
1416            delay_until: None,
1417            execution_deadline_at: None,
1418            partition_id: 42,
1419            now: TimestampMs::now(),
1420        };
1421        let json = serde_json::to_string(&args).unwrap();
1422        let parsed: CreateExecutionArgs = serde_json::from_str(&json).unwrap();
1423        assert_eq!(args.execution_id, parsed.execution_id);
1424    }
1425
1426    #[test]
1427    fn claim_result_serde() {
1428        let config = crate::partition::PartitionConfig::default();
1429        let result = ClaimExecutionResult::Claimed(ClaimedExecution {
1430            execution_id: ExecutionId::for_flow(&FlowId::new(), &config),
1431            lease_id: LeaseId::new(),
1432            lease_epoch: LeaseEpoch::new(1),
1433            attempt_index: AttemptIndex::new(0),
1434            attempt_id: AttemptId::new(),
1435            attempt_type: AttemptType::Initial,
1436            lease_expires_at: TimestampMs::from_millis(1000),
1437        });
1438        let json = serde_json::to_string(&result).unwrap();
1439        let parsed: ClaimExecutionResult = serde_json::from_str(&json).unwrap();
1440        assert_eq!(result, parsed);
1441    }
1442}
1443
1444// ─── list_executions ───
1445
1446/// Summary of an execution for list views.
1447#[derive(Clone, Debug, Serialize, Deserialize)]
1448pub struct ExecutionSummary {
1449    pub execution_id: ExecutionId,
1450    pub namespace: String,
1451    pub lane_id: String,
1452    pub execution_kind: String,
1453    pub public_state: String,
1454    pub priority: i32,
1455    pub created_at: String,
1456}
1457
1458/// Result of a list_executions query.
1459#[derive(Clone, Debug, Serialize, Deserialize)]
1460pub struct ListExecutionsResult {
1461    pub executions: Vec<ExecutionSummary>,
1462    pub total_returned: usize,
1463}