deepstrike_sdk/runtime/
replay_provider.rs1use std::sync::Mutex;
14
15use async_trait::async_trait;
16use deepstrike_core::context::renderer::RenderedContext;
17use deepstrike_core::runtime::session::ProviderReplay;
18use deepstrike_core::types::message::{Content, ContentPart, Message, ToolCall, ToolSchema};
19use futures::Stream;
20
21use crate::Result;
22use crate::providers::{LLMProvider, ProviderRunState, RuntimePolicy, StreamEvent};
23
24pub struct ReplayProviderOpts {
26 pub tokenizer: Option<Box<dyn Fn(&str) -> u32 + Send + Sync>>,
28 pub wrap: bool,
30}
31
32impl Default for ReplayProviderOpts {
33 fn default() -> Self {
34 Self {
35 tokenizer: None,
36 wrap: false,
37 }
38 }
39}
40
41fn default_tokenizer(text: &str) -> u32 {
42 let len = text.chars().count() as u32;
43 (len + 3) / 4
44}
45
46pub struct ReplayProvider {
48 messages: Vec<Message>,
49 cursor: Mutex<usize>,
50 tokenizer: Box<dyn Fn(&str) -> u32 + Send + Sync>,
51 wrap: bool,
52}
53
54impl ReplayProvider {
55 pub fn new(messages: Vec<Message>) -> Self {
56 Self::with_opts(messages, ReplayProviderOpts::default())
57 }
58
59 pub fn with_opts(messages: Vec<Message>, opts: ReplayProviderOpts) -> Self {
60 Self {
61 messages,
62 cursor: Mutex::new(0),
63 tokenizer: opts
64 .tokenizer
65 .unwrap_or_else(|| Box::new(default_tokenizer)),
66 wrap: opts.wrap,
67 }
68 }
69
70 pub fn consumed(&self) -> usize {
71 *self.cursor.lock().unwrap()
72 }
73
74 pub fn remaining(&self) -> usize {
75 let c = *self.cursor.lock().unwrap();
76 self.messages.len().saturating_sub(c)
77 }
78
79 pub fn reset(&self) {
80 *self.cursor.lock().unwrap() = 0;
81 }
82
83 fn pull(&self) -> Result<Message> {
84 let mut c = self.cursor.lock().unwrap();
85 if *c >= self.messages.len() {
86 if self.wrap && !self.messages.is_empty() {
87 *c = 0;
88 } else {
89 return Err(crate::Error::Other(format!(
90 "ReplayProvider: fixture exhausted (consumed={}, total={})",
91 *c,
92 self.messages.len()
93 )));
94 }
95 }
96 let msg = self.messages[*c].clone();
97 *c += 1;
98 Ok(msg)
99 }
100
101 fn estimate_input_tokens(&self, context: &RenderedContext, tools: &[ToolSchema]) -> u32 {
102 (self.tokenizer)(&render_context_to_text(context, tools))
103 }
104}
105
106fn render_context_to_text(context: &RenderedContext, tools: &[ToolSchema]) -> String {
107 let mut parts: Vec<String> = Vec::new();
108 if !context.system_text.is_empty() {
109 parts.push(context.system_text.clone());
110 }
111 if !context.system_stable.is_empty() {
112 parts.push(context.system_stable.clone());
113 }
114 if !context.system_knowledge.is_empty() {
115 parts.push(context.system_knowledge.clone());
116 }
117 if let Some(turn) = &context.state_turn {
118 if let Some(t) = message_text(turn) {
119 parts.push(t);
120 }
121 }
122 for turn in &context.turns {
123 if let Some(t) = message_text(turn) {
124 parts.push(t);
125 }
126 for tc in &turn.tool_calls {
127 parts.push(format!("{} {}", tc.name, tc.arguments.to_string()));
128 }
129 }
130 for tool in tools {
131 parts.push(format!(
132 "{} {} {}",
133 tool.name, tool.description, tool.parameters
134 ));
135 }
136 parts.join("\n")
137}
138
139fn message_text(m: &Message) -> Option<String> {
140 match &m.content {
141 Content::Text(s) if !s.is_empty() => Some(s.clone()),
142 Content::Parts(parts) => {
143 let joined: String = parts
144 .iter()
145 .filter_map(|p| match p {
146 ContentPart::Text { text } => Some(text.clone()),
147 ContentPart::ToolResult { output, .. } => Some(output.clone()),
148 _ => None,
149 })
150 .collect::<Vec<_>>()
151 .join("\n");
152 if joined.is_empty() {
153 None
154 } else {
155 Some(joined)
156 }
157 }
158 _ => None,
159 }
160}
161
162#[async_trait]
163impl LLMProvider for ReplayProvider {
164 fn runtime_policy(&self) -> RuntimePolicy {
165 RuntimePolicy::default()
166 }
167
168 fn peek_provider_replay(
169 &self,
170 _content: &str,
171 _tool_calls: &[ToolCall],
172 ) -> Option<ProviderReplay> {
173 None
174 }
175
176 fn seed_provider_replay(
177 &self,
178 _content: &str,
179 _tool_calls: &[ToolCall],
180 _replay: &ProviderReplay,
181 ) {
182 }
183
184 async fn stream(
185 &self,
186 context: &RenderedContext,
187 tools: &[ToolSchema],
188 _extensions: Option<&serde_json::Value>,
189 _state: Option<&ProviderRunState>,
190 ) -> Result<Box<dyn Stream<Item = Result<StreamEvent>> + Send + Unpin>> {
191 let msg = self.pull()?;
192 let input_tokens = self.estimate_input_tokens(context, tools);
193 let output_tokens = msg.token_count.unwrap_or_else(|| {
194 let content = message_text(&msg).unwrap_or_default();
195 (self.tokenizer)(&content)
196 });
197
198 let mut events: Vec<Result<StreamEvent>> = Vec::new();
199 events.push(Ok(StreamEvent::Usage {
200 total_tokens: input_tokens + output_tokens,
201 input_tokens,
202 output_tokens,
203 cache_read_input_tokens: 0,
204 cache_creation_input_tokens: 0,
205 cache_read_input_tokens_by_slot: None,
207 stop_reason: None,
209 }));
210 if let Some(text) = message_text(&msg) {
211 if !text.is_empty() {
212 events.push(Ok(StreamEvent::TextDelta { delta: text }));
213 }
214 }
215 for tc in &msg.tool_calls {
216 events.push(Ok(StreamEvent::ToolCall {
217 id: tc.id.to_string(),
218 name: tc.name.to_string(),
219 arguments: tc.arguments.clone(),
220 }));
221 }
222 Ok(Box::new(futures::stream::iter(events)))
223 }
224}