deepstrike_sdk/runtime/
replay_provider.rs1use std::sync::Mutex;
14
15use async_trait::async_trait;
16use deepstrike_core::context::renderer::InternalRenderedContext;
17use deepstrike_core::runtime::session::ProviderReplay;
18use deepstrike_core::types::message::{Content, ContentPart, CoreMessage, 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<CoreMessage>,
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<CoreMessage>) -> Self {
56 Self::with_opts(messages, ReplayProviderOpts::default())
57 }
58
59 pub fn with_opts(messages: Vec<CoreMessage>, 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<CoreMessage> {
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(
102 &self,
103 context: &InternalRenderedContext,
104 tools: &[ToolSchema],
105 ) -> u32 {
106 (self.tokenizer)(&render_context_to_text(context, tools))
107 }
108}
109
110fn render_context_to_text(context: &InternalRenderedContext, tools: &[ToolSchema]) -> String {
111 let mut parts: Vec<String> = Vec::new();
112 if !context.system_text.is_empty() {
113 parts.push(context.system_text.clone());
114 }
115 if !context.system_stable.is_empty() {
116 parts.push(context.system_stable.clone());
117 }
118 if !context.system_knowledge.is_empty() {
119 parts.push(context.system_knowledge.clone());
120 }
121 if let Some(turn) = &context.state_turn {
122 if let Some(t) = message_text(turn) {
123 parts.push(t);
124 }
125 }
126 for turn in &context.turns {
127 if let Some(t) = message_text(turn) {
128 parts.push(t);
129 }
130 for tc in &turn.tool_calls {
131 parts.push(format!("{} {}", tc.name, tc.arguments.to_string()));
132 }
133 }
134 for tool in tools {
135 parts.push(format!(
136 "{} {} {}",
137 tool.name, tool.description, tool.parameters
138 ));
139 }
140 parts.join("\n")
141}
142
143fn message_text(m: &CoreMessage) -> Option<String> {
144 match &m.content {
145 Content::Text(s) if !s.is_empty() => Some(s.clone()),
146 Content::Parts(parts) => {
147 let joined: String = parts
148 .iter()
149 .filter_map(|p| match p {
150 ContentPart::Text { text } => Some(text.clone()),
151 ContentPart::ToolResult { output, .. } => Some(output.clone()),
152 _ => None,
153 })
154 .collect::<Vec<_>>()
155 .join("\n");
156 if joined.is_empty() {
157 None
158 } else {
159 Some(joined)
160 }
161 }
162 _ => None,
163 }
164}
165
166#[async_trait]
167impl LLMProvider for ReplayProvider {
168 fn runtime_policy(&self) -> RuntimePolicy {
169 RuntimePolicy::default()
170 }
171
172 fn peek_provider_replay(
173 &self,
174 _content: &str,
175 _tool_calls: &[ToolCall],
176 ) -> Option<ProviderReplay> {
177 None
178 }
179
180 fn seed_provider_replay(
181 &self,
182 _content: &str,
183 _tool_calls: &[ToolCall],
184 _replay: &ProviderReplay,
185 ) {
186 }
187
188 async fn stream(
189 &self,
190 context: &InternalRenderedContext,
191 tools: &[ToolSchema],
192 _extensions: Option<&serde_json::Value>,
193 _state: Option<&ProviderRunState>,
194 ) -> Result<Box<dyn Stream<Item = Result<StreamEvent>> + Send + Unpin>> {
195 let msg = self.pull()?;
196 let input_tokens = self.estimate_input_tokens(context, tools);
197 let content = message_text(&msg).unwrap_or_default();
198 let output_tokens = (self.tokenizer)(&content);
199
200 let mut events: Vec<Result<StreamEvent>> = Vec::new();
201 events.push(Ok(StreamEvent::Usage {
202 total_tokens: input_tokens + output_tokens,
203 input_tokens,
204 output_tokens,
205 cache_read_input_tokens: 0,
206 cache_creation_input_tokens: 0,
207 cache_read_input_tokens_by_slot: None,
209 stop_reason: None,
211 }));
212 if let Some(text) = message_text(&msg) {
213 if !text.is_empty() {
214 events.push(Ok(StreamEvent::TextDelta { delta: text }));
215 }
216 }
217 for tc in &msg.tool_calls {
218 events.push(Ok(StreamEvent::ToolCall {
219 id: tc.id.to_string(),
220 name: tc.name.to_string(),
221 arguments: tc.arguments.clone(),
222 }));
223 }
224 Ok(Box::new(futures::stream::iter(events)))
225 }
226}