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}