Skip to main content

turnframe_tasks/
task.rs

1//! What a model task is: one narrow question, its context, and the shape of its answer.
2
3use std::fmt;
4
5use serde::Serialize;
6use serde::de::DeserializeOwned;
7use turnframe_provider::request::Message;
8
9/// The kind of a task, which is also the purpose a provider router routes it by.
10pub use turnframe_provider::purpose::ModelPurpose as TaskKind;
11
12/// One narrow question put to a model.
13///
14/// Implementations are pure: they render messages and schemas from their input and
15/// check answers structurally. Everything about calling a model belongs to the engine.
16pub trait ModelTask: Send + Sync {
17    /// Everything the task's context is built from.
18    type Input: Send + Sync;
19    /// The answer, as the schema allows it.
20    type Output: Serialize + DeserializeOwned + Clone + Send + Sync + 'static;
21
22    /// The task kind, which selects the profile and the route.
23    fn kind(&self) -> TaskKind;
24
25    /// The prompt name a prompt source is asked for, such as `understand.segment`.
26    fn prompt_name(&self) -> &str;
27
28    /// The built-in instructions, used when no source supplies the prompt.
29    fn instructions(&self) -> &str;
30
31    /// The answer's JSON Schema for this input, with its closed sets filled in.
32    fn schema(&self, input: &Self::Input) -> serde_json::Value;
33
34    /// The context messages, static parts first.
35    fn render(&self, input: &Self::Input) -> Vec<Message>;
36
37    /// Structural checks the schema cannot express, such as a pointer in range.
38    ///
39    /// # Errors
40    ///
41    /// A [`StructuralError`] naming what is wrong, which a repair round quotes.
42    fn check(&self, _input: &Self::Input, _output: &Self::Output) -> Result<(), StructuralError> {
43        Ok(())
44    }
45
46    /// Whether two answers are the same answer, for voting.
47    fn agree(&self, left: &Self::Output, right: &Self::Output) -> bool {
48        serde_json::to_value(left).ok() == serde_json::to_value(right).ok()
49    }
50}
51
52/// A structural problem with an answer, worded for the repair round that quotes it.
53#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
54#[error("{message}")]
55pub struct StructuralError {
56    /// Stable code, for records and metrics.
57    pub code: &'static str,
58    /// What is wrong, in words the model can act on.
59    pub message: String,
60}
61
62impl StructuralError {
63    /// A structural error with its code and message.
64    #[must_use]
65    pub fn new(code: &'static str, message: impl Into<String>) -> Self {
66        Self {
67            code,
68            message: message.into(),
69        }
70    }
71}
72
73/// Where a task call sits in its turn, as a path: `turn/segment`, `u2/extract`.
74#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
75pub struct TaskId(String);
76
77impl TaskId {
78    /// A top-level identifier.
79    #[must_use]
80    pub fn new(path: impl Into<String>) -> Self {
81        Self(path.into())
82    }
83
84    /// A child identifier: `u2` then `extract` gives `u2/extract`.
85    #[must_use]
86    pub fn child(&self, segment: impl fmt::Display) -> Self {
87        Self(format!("{}/{segment}", self.0))
88    }
89
90    /// The same task with a suffix naming one call of it: `u2/extract#repair1`.
91    #[must_use]
92    pub fn call(&self, suffix: impl fmt::Display) -> String {
93        format!("{}#{suffix}", self.0)
94    }
95
96    /// The path.
97    #[must_use]
98    pub fn as_str(&self) -> &str {
99        &self.0
100    }
101}
102
103impl fmt::Display for TaskId {
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        f.write_str(&self.0)
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn identifiers_read_as_the_graph_they_ran_as() {
115        let unit = TaskId::new("u2");
116        let extract = unit.child("extract");
117        assert_eq!(extract.as_str(), "u2/extract");
118        assert_eq!(extract.call("repair1"), "u2/extract#repair1");
119    }
120}