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