rskit_agent/agent/
stream.rs1use std::pin::Pin;
2
3use async_stream::stream;
4use futures::Stream;
5use rskit_hook::CancellationToken;
6use rskit_llm::types::{Message, Usage};
7
8use super::Agent;
9use crate::runtime::state::RunState;
10use crate::runtime::stop;
11use crate::runtime::turn::{self, TurnOutcome};
12use crate::types::{AgentEvent, StopReason};
13
14impl Agent {
15 pub fn stream(
17 &self,
18 messages: Vec<Message>,
19 ) -> Pin<Box<dyn Stream<Item = AgentEvent> + Send + '_>> {
20 Box::pin(stream! {
21 let mut state = RunState::new(&self.config.system_prompt, messages);
22
23 if let Some(stop_reason) = stop::initial_stop(&self.config) {
24 yield AgentEvent::Complete {
25 result: state.finish(0, stop_reason),
26 };
27 return;
28 }
29
30 let hook_token = CancellationToken::new();
31
32 for turn in 0..self.config.max_turns {
33 if let Some(stop_reason) = stop::wall_clock_stop(&state, &self.config) {
34 yield AgentEvent::Complete {
35 result: state.finish(turn, stop_reason),
36 };
37 return;
38 }
39
40 let usage_before = state.total_usage;
41 yield AgentEvent::TurnStart { turn };
42
43 let outcome = match turn::run_turn(
44 self.provider.as_ref(),
45 &self.config,
46 &mut state,
47 turn,
48 &hook_token,
49 )
50 .await
51 {
52 Ok(outcome) => outcome,
53 Err(error) => {
54 tracing::error!(error = %error, "agent.run.failed");
55 yield AgentEvent::Complete {
56 result: state.finish(turn, StopReason::Aborted),
57 };
58 return;
59 }
60 };
61
62 let usage = usage_delta(usage_before, state.total_usage);
63 let completed_turn = match &outcome {
64 TurnOutcome::Continue => true,
65 TurnOutcome::Stop { turn_count, .. } => *turn_count > turn,
66 };
67
68 if completed_turn {
69 yield AgentEvent::TurnComplete {
70 turn,
71 message: state.last_assistant.clone(),
72 usage,
73 };
74 }
75
76 match outcome {
77 TurnOutcome::Continue => {}
78 TurnOutcome::Stop { turn_count, reason } => {
79 yield AgentEvent::Complete {
80 result: state.finish(turn_count, reason),
81 };
82 return;
83 }
84 }
85 }
86
87 yield AgentEvent::Complete {
88 result: state.finish(self.config.max_turns, StopReason::MaxTurns),
89 };
90 })
91 }
92}
93
94const fn usage_delta(before: Usage, after: Usage) -> Usage {
95 Usage {
96 input_tokens: after.input_tokens.saturating_sub(before.input_tokens),
97 output_tokens: after.output_tokens.saturating_sub(before.output_tokens),
98 cached_tokens: after.cached_tokens.saturating_sub(before.cached_tokens),
99 reasoning_tokens: after
100 .reasoning_tokens
101 .saturating_sub(before.reasoning_tokens),
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use rskit_llm::types::Usage;
108
109 use super::usage_delta;
110
111 #[test]
112 fn usage_delta_reports_per_turn_usage() {
113 let before = Usage {
114 input_tokens: 10,
115 output_tokens: 5,
116 cached_tokens: 2,
117 reasoning_tokens: 1,
118 };
119 let after = Usage {
120 input_tokens: 17,
121 output_tokens: 9,
122 cached_tokens: 3,
123 reasoning_tokens: 6,
124 };
125
126 assert_eq!(
127 usage_delta(before, after),
128 Usage {
129 input_tokens: 7,
130 output_tokens: 4,
131 cached_tokens: 1,
132 reasoning_tokens: 5,
133 }
134 );
135 }
136
137 #[test]
138 fn usage_delta_saturates_if_counters_reset() {
139 let before = Usage {
140 input_tokens: 10,
141 output_tokens: 5,
142 cached_tokens: 2,
143 reasoning_tokens: 1,
144 };
145 let after = Usage {
146 input_tokens: 7,
147 output_tokens: 9,
148 cached_tokens: 1,
149 reasoning_tokens: 6,
150 };
151
152 assert_eq!(
153 usage_delta(before, after),
154 Usage {
155 input_tokens: 0,
156 output_tokens: 4,
157 cached_tokens: 0,
158 reasoning_tokens: 5,
159 }
160 );
161 }
162}