Skip to main content

basis_tasks/
spec.rs

1//! `RunSpec`: one task's worth of spawn request.
2//!
3//! Built the way `basis::RunSpec` is — `new` plus `with_*` methods that
4//! return new values — because this is the same kind of thing one layer up:
5//! `basis::RunSpec` is a run's per-turn intent against an already-open
6//! [`basis::Workspace`]; this is what a *durable* task additionally records
7//! so a later attach, in a process that may not be this one, can open that
8//! workspace itself and mint the run.
9
10use std::time::Duration;
11
12use basis::{Effort, SystemPrompt};
13
14use crate::{approve::Approve, handle::TaskHandle};
15
16/// The default deadline an unattended task is given when nothing else names
17/// one: 30 minutes. A spawned task may never be waited on by an attentive
18/// caller, so — unlike an attended one-shot, which is unbounded unless asked
19/// — it always gets a finite service bound (`with_deadline` narrows it).
20pub const DEFAULT_DEADLINE: Duration = Duration::from_secs(30 * 60);
21
22/// Which conversation a spawned task picks up, if any.
23#[derive(Debug, Clone, Default, PartialEq, Eq)]
24pub enum Continuation {
25    /// Opens a new conversation. The ordinary case.
26    #[default]
27    New,
28    /// The conversation this workspace was last worked in — what a bare
29    /// `--continue` resolves against.
30    Latest,
31    /// A specific conversation, by the task that opened or last continued
32    /// it — what `--continue --session <ID>` resolves against.
33    Named(TaskHandle),
34}
35
36/// One task's worth of spawn request: `basis::RunSpec`'s per-run intent,
37/// plus the workspace-level overrides a [`basis::Workspace`] normally fixes
38/// once, and the facts a task that may run unattended, in another process,
39/// additionally needs recorded.
40#[derive(Debug, Clone, PartialEq)]
41pub struct RunSpec {
42    pub(crate) prompt: String,
43    pub(crate) provider: Option<String>,
44    pub(crate) base_url: Option<String>,
45    pub(crate) model: Option<String>,
46    pub(crate) shell: bool,
47    pub(crate) system_prompt: Option<SystemPrompt>,
48    pub(crate) effort: Option<Effort>,
49    pub(crate) approve: Approve,
50    pub(crate) deadline: Option<Duration>,
51    pub(crate) tool_budget: Option<usize>,
52    pub(crate) token_budget: Option<u64>,
53    pub(crate) detached: bool,
54    pub(crate) continuation: Continuation,
55}
56
57impl RunSpec {
58    pub fn new(prompt: impl Into<String>) -> Self {
59        Self {
60            prompt: prompt.into(),
61            provider: None,
62            base_url: None,
63            model: None,
64            shell: true,
65            system_prompt: None,
66            effort: None,
67            approve: Approve::default(),
68            deadline: None,
69            tool_budget: None,
70            token_budget: None,
71            detached: false,
72            continuation: Continuation::default(),
73        }
74    }
75
76    pub fn with_provider(self, provider: impl Into<String>) -> Self {
77        Self {
78            provider: Some(provider.into()),
79            ..self
80        }
81    }
82
83    pub fn with_base_url(self, base_url: impl Into<String>) -> Self {
84        Self {
85            base_url: Some(base_url.into()),
86            ..self
87        }
88    }
89
90    pub fn with_model(self, model: impl Into<String>) -> Self {
91        Self {
92            model: Some(model.into()),
93            ..self
94        }
95    }
96
97    /// Refuses the task shell access — sugar over the workspace's own
98    /// `ShellAccess::from_flag`.
99    pub fn without_shell(self) -> Self {
100        Self {
101            shell: false,
102            ..self
103        }
104    }
105
106    pub fn with_system_prompt(self, system_prompt: SystemPrompt) -> Self {
107        Self {
108            system_prompt: Some(system_prompt),
109            ..self
110        }
111    }
112
113    pub fn with_effort(self, effort: Effort) -> Self {
114        Self {
115            effort: Some(effort),
116            ..self
117        }
118    }
119
120    /// Every consequential call this task's turns make is put to this.
121    pub fn with_approve(self, approve: Approve) -> Self {
122        Self { approve, ..self }
123    }
124
125    /// Gives up on the task after `deadline`, counted from spawn. Defaults to
126    /// [`DEFAULT_DEADLINE`] when never set — an unattended task always gets a
127    /// finite service bound.
128    pub fn with_deadline(self, deadline: Duration) -> Self {
129        Self {
130            deadline: Some(deadline),
131            ..self
132        }
133    }
134
135    pub fn with_tool_budget(self, tool_budget: usize) -> Self {
136        Self {
137            tool_budget: Some(tool_budget),
138            ..self
139        }
140    }
141
142    pub fn with_token_budget(self, token_budget: u64) -> Self {
143        Self {
144            token_budget: Some(token_budget),
145            ..self
146        }
147    }
148
149    /// Spawns outside the calling task's ownership tree even when this
150    /// process is itself executing one — see [`crate::current_task`]. A
151    /// detached task inherits no scope: nothing cancels it downward, and
152    /// nothing waits for it before its would-be parent settles.
153    pub fn detached(self) -> Self {
154        Self {
155            detached: true,
156            ..self
157        }
158    }
159
160    /// Picks up an existing conversation instead of opening a new one.
161    pub fn continuing(self, continuation: Continuation) -> Self {
162        Self {
163            continuation,
164            ..self
165        }
166    }
167}