Skip to main content

arora_behavior/
task.rs

1//! Task runs: concurrent, cancellable behaviors an interpreter hosts.
2//!
3//! A *task run* is a long-running unit of behavior started from a [`Call`] and
4//! tracked by a serializable [`TaskHandle`]. It advances with the interpreter's
5//! normal [`tick`](crate::BehaviorInterpreter::tick) — spawning one is
6//! non-blocking — and everything an outside observer needs to follow or stop it
7//! travels as data on the handle, so it can be driven through the value plane
8//! without knowing how the interpreter hosts the run.
9
10use arora_types::call::Call;
11use arora_types::data::Key;
12use serde::{Deserialize, Serialize};
13use uuid::Uuid;
14
15/// Identity of a live task run.
16///
17/// Unique per run. It is also the namespacing root for the run's keys, which
18/// live under `arora/tasks/<module>/<function>/<run_id>/…`.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
20pub struct TaskId(pub Uuid);
21
22/// A [`TaskId`] travels on the value plane as a bare scalar uuid — its arora
23/// type is the well-known uuid primitive. The distinct Rust newtype conveys
24/// that a uuid *is* a task's identity while carrying no schema of its own.
25impl arora_types::AroraType for TaskId {
26    fn arora_type_id() -> Uuid {
27        *arora_types::ty::UUID_ID
28    }
29
30    fn arora_type() -> arora_types::ty::low::Type {
31        arora_types::ty::PRIMITIVE_TYPES
32            .get(&*arora_types::ty::UUID_ID)
33            .expect("the uuid primitive is always registered")
34            .clone()
35    }
36
37    fn register_types(_registry: &mut arora_types::ty::TypeRegistry) {
38        // A uuid is a well-known primitive, resolved without the registry.
39    }
40}
41
42/// How a run coexists with the runs an interpreter already hosts.
43///
44/// Only [`Concurrent`](Self::Concurrent) is honoured today; the enum is
45/// `#[non_exhaustive]` so conflict-resolution policies that need resource-claims
46/// (preempt, queue, blend, reject) can be added later without a breaking change.
47/// Arbitration, when those land, is the interpreter's — it is the only thing
48/// that sees every run.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50#[non_exhaustive]
51pub enum RunPolicy {
52    /// Run alongside every other run. Overlapping actuation writes are
53    /// last-write-wins — two runs driving the same output will fight.
54    Concurrent,
55}
56
57/// The serializable contract for one run's lifecycle: everything needed to
58/// follow and stop it, as data.
59///
60/// Returned from [`spawn`](crate::BehaviorInterpreter::spawn). The keys are
61/// allocated by the interpreter under the run's [`id`](Self::id), so concurrent
62/// runs demux. A run's *actuation* writes are deliberately not here — those go
63/// to the shared standard keys, where overlapping runs are last-write-wins.
64#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, arora_types::AroraType)]
65#[arora(id = "d1f79433-a751-4fe3-9e6f-561cea873293")]
66pub struct TaskHandle {
67    /// The run's identity.
68    #[arora(id = "35a14602-9fb2-4387-b202-21f0fc8cfb0c")]
69    pub id: TaskId,
70    /// How to stop the run: a [`Call`] the observer issues, which reaches
71    /// [`halt`](crate::BehaviorInterpreter::halt). Carrying it as a `Call` keeps
72    /// "how to stop it" serializable and interpreter-agnostic.
73    #[arora(id = "41e5efbb-959a-4aa0-b078-fc2812ab6181")]
74    pub stop: Call,
75    /// The run's lifecycle status key — a single key an observer watches to
76    /// learn when the run reaches a terminal state and how it ended.
77    #[arora(id = "f1a077ed-4007-4f18-b3be-cc3711058515")]
78    pub status: Key,
79    /// Keys carrying the run's progress feedback, updated as it runs.
80    #[arora(id = "60f2f967-16a3-4631-b27c-92445b583d9c")]
81    pub feedback: Vec<Key>,
82    /// Keys carrying the run's result, written when it terminates.
83    #[arora(id = "d6afb314-2bfd-444a-b041-39d9223ba700")]
84    pub result: Vec<Key>,
85    /// Keys an observer may write to steer a live run (e.g. a moving target).
86    #[arora(id = "53d75772-12bd-4a55-a0a5-696b2bc4f6a9")]
87    pub update: Vec<Key>,
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use arora_types::module::low::TypeRef;
94    use arora_types::ty::low::TypeKind;
95    use arora_types::AroraType;
96
97    #[test]
98    fn task_handle_arora_type_shapes_its_fields() {
99        let (ty, registry) = TaskHandle::arora_type_with_registry();
100        let TypeKind::Structure(structure) = &ty.kind else {
101            panic!("TaskHandle is a structure type");
102        };
103        let field = |id: &str| {
104            structure.fields[&arora_types::Uuid::parse_str(id).unwrap()]
105                .type_ref
106                .clone()
107        };
108        // id: a scalar uuid (TaskId is transparent over uuid).
109        assert!(matches!(
110            field("35a14602-9fb2-4387-b202-21f0fc8cfb0c"),
111            TypeRef::Scalar { id } if id == *arora_types::ty::UUID_ID
112        ));
113        // stop: the nested Call structure, named by Call's id.
114        assert!(matches!(
115            field("41e5efbb-959a-4aa0-b078-fc2812ab6181"),
116            TypeRef::Scalar { id } if id == Call::arora_type_id()
117        ));
118        // status: a scalar string (Key is transparent over string).
119        assert!(matches!(
120            field("f1a077ed-4007-4f18-b3be-cc3711058515"),
121            TypeRef::Scalar { id } if id == *arora_types::ty::STRING_ID
122        ));
123        // feedback / result / update: arrays of string keys.
124        for id in [
125            "60f2f967-16a3-4631-b27c-92445b583d9c",
126            "d6afb314-2bfd-444a-b041-39d9223ba700",
127            "53d75772-12bd-4a55-a0a5-696b2bc4f6a9",
128        ] {
129            assert!(matches!(
130                field(id),
131                TypeRef::Array { id } if id == *arora_types::ty::STRING_ID
132            ));
133        }
134        // The one nested user type — Call — is registered; TaskId and Key are
135        // primitives and so are not.
136        assert!(registry.contains_key(&Call::arora_type_id()));
137    }
138}