Skip to main content

nanocodex_agent/agent/
turn.rs

1use super::*;
2
3/// Completion handle for an accepted turn.
4///
5/// A turn is both a [`Future`] for its final typed result and a [`Stream`] of
6/// optional per-turn events. Result readiness is independent from consuming or
7/// closing that event stream.
8///
9/// Dropping this handle does not cancel the accepted turn. Use [`Self::cancel`]
10/// before dropping it when the work should stop.
11#[must_use = "a turn continues running when dropped; await result(), control it, or explicitly drop it"]
12pub struct Turn {
13    pub(super) control: TurnControl,
14    pub(super) events: AgentEvents,
15    pub(super) result: oneshot::Receiver<Result<TurnResult>>,
16}
17
18impl Turn {
19    /// Returns a cheap cloneable capability targeting this exact turn.
20    #[must_use]
21    pub fn control(&self) -> TurnControl {
22        self.control.clone()
23    }
24
25    /// Injects additional input into this turn at its next safe model boundary.
26    ///
27    /// # Errors
28    ///
29    /// Returns an error for an empty prompt, when this turn is queued or no
30    /// longer active, when its steering queue is full, or if the driver stops.
31    pub async fn steer(&self, prompt: impl Into<Prompt>) -> Result<()> {
32        self.control.steer(prompt).await
33    }
34
35    /// Cancels this exact unfinished turn.
36    ///
37    /// A queued turn is removed before execution and acknowledged immediately;
38    /// its result and terminal event retain their FIFO position behind earlier
39    /// turns. An active turn waits for its model and tool resources to stop
40    /// before cancellation is acknowledged.
41    ///
42    /// # Errors
43    ///
44    /// Returns an error when this turn has already finished or if the driver
45    /// stops.
46    pub async fn cancel(&self) -> Result<()> {
47        self.control.cancel().await
48    }
49
50    /// Waits for and returns the final typed turn result.
51    ///
52    /// This is equivalent to awaiting the turn directly. It does not wait for
53    /// the per-turn event stream to be consumed or closed. Applications that
54    /// need every event should consume the independently returned
55    /// [`AgentEvents`] stream.
56    ///
57    /// # Errors
58    ///
59    /// Returns the model-run failure or an error if the driver stopped early.
60    pub async fn result(self) -> Result<TurnResult> {
61        self.await
62    }
63}
64
65impl Stream for Turn {
66    type Item = AgentEvent;
67
68    fn poll_next(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
69        Pin::new(&mut self.events).poll_next(context)
70    }
71}
72
73impl Future for Turn {
74    type Output = Result<TurnResult>;
75
76    fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
77        Pin::new(&mut self.result)
78            .poll(context)
79            .map(|result| result.map_err(|_| NanocodexError::TurnStopped)?)
80    }
81}
82
83/// Cheap cloneable control capability for one accepted turn.
84#[derive(Clone)]
85pub struct TurnControl {
86    pub(super) key: TurnKey,
87    pub(super) commands: mpsc::Sender<Command>,
88}
89
90impl TurnControl {
91    /// Injects additional input into the targeted turn.
92    ///
93    /// # Errors
94    ///
95    /// Returns an error for an empty prompt, when the turn is not active, when
96    /// its steering queue is full, or if the driver stops.
97    pub async fn steer(&self, prompt: impl Into<Prompt>) -> Result<()> {
98        let prompt = prompt.into();
99        if prompt.instruction.is_empty() {
100            return Err(NanocodexError::InvalidRequest(
101                "steer instruction must not be empty".to_owned(),
102            ));
103        }
104        request_command(&self.commands, |result| Command::Steer {
105            key: self.key,
106            prompt,
107            result,
108        })
109        .await
110    }
111
112    /// Cancels the targeted unfinished turn.
113    ///
114    /// # Errors
115    ///
116    /// Returns an error when the turn has already finished or if the driver
117    /// stops.
118    pub async fn cancel(&self) -> Result<()> {
119        request_command(&self.commands, |result| Command::Cancel {
120            key: self.key,
121            result,
122        })
123        .await
124    }
125}
126
127#[derive(Clone, Copy, Eq, PartialEq)]
128pub(super) struct TurnKey(pub(super) u64);
129
130/// Final result of a completed turn.
131#[derive(Clone)]
132#[non_exhaustive]
133pub struct TurnResult {
134    pub(super) final_message: String,
135    pub(super) usage: TurnUsage,
136    pub(super) checkpoint: Arc<CommittedSession>,
137}
138
139impl TurnResult {
140    /// Returns the final assistant message for this completed turn.
141    #[must_use]
142    pub fn final_message(&self) -> &str {
143        &self.final_message
144    }
145
146    /// Consumes the result and returns its final assistant message.
147    #[must_use]
148    pub fn into_final_message(self) -> String {
149        self.final_message
150    }
151
152    /// Returns exact aggregate token usage for this logical agent turn.
153    #[must_use]
154    pub const fn usage(&self) -> &TurnUsage {
155        &self.usage
156    }
157
158    /// Copies this completed boundary into a serializable, caller-owned session snapshot.
159    ///
160    /// The snapshot contains the complete unredacted model-visible conversation,
161    /// including reasoning payloads and tool inputs and outputs. Applications are
162    /// responsible for protecting and retaining serialized snapshots appropriately.
163    #[must_use]
164    pub fn snapshot(&self) -> SessionSnapshot {
165        self.checkpoint.snapshot()
166    }
167}
168
169impl fmt::Debug for TurnResult {
170    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
171        formatter
172            .debug_struct("TurnResult")
173            .field("final_message", &self.final_message)
174            .finish_non_exhaustive()
175    }
176}
177
178pub(super) enum Command {
179    Prompt {
180        key: TurnKey,
181        prompt: Prompt,
182        thinking: Option<Thinking>,
183        fast_mode: Option<bool>,
184        parent: Option<tracing::Span>,
185        events: EventSink,
186        result: oneshot::Sender<Result<TurnResult>>,
187    },
188    Steer {
189        key: TurnKey,
190        prompt: Prompt,
191        result: oneshot::Sender<Result<()>>,
192    },
193    Cancel {
194        key: TurnKey,
195        result: oneshot::Sender<Result<()>>,
196    },
197    Fork {
198        checkpoint: Option<Arc<CommittedSession>>,
199        result: oneshot::Sender<Result<(Nanocodex, AgentEvents)>>,
200    },
201    Spawn {
202        result: oneshot::Sender<Result<(Nanocodex, AgentEvents)>>,
203    },
204    SetThinking {
205        thinking: Thinking,
206        result: oneshot::Sender<Result<()>>,
207    },
208    SetFastMode {
209        enabled: bool,
210        result: oneshot::Sender<Result<()>>,
211    },
212    Compact {
213        parent: Option<tracing::Span>,
214        result: oneshot::Sender<Result<()>>,
215    },
216    Shutdown,
217}
218
219pub(super) enum QueuedTurn {
220    Pending {
221        key: TurnKey,
222        prompt: Prompt,
223        thinking: Thinking,
224        fast_mode: bool,
225        parent: Option<tracing::Span>,
226        events: EventSink,
227        result: oneshot::Sender<Result<TurnResult>>,
228    },
229    Cancelled {
230        prompt: Prompt,
231        thinking: Thinking,
232        fast_mode: bool,
233        parent: Option<tracing::Span>,
234        events: EventSink,
235        result: oneshot::Sender<Result<TurnResult>>,
236    },
237}