use goose_provider_types::conversation::message::Message;
use goose_provider_types::errors::ProviderError;
use super::super::{finalize_usage, thinking_output::ThinkingOutputFilter, StreamSender};
use super::inference_engine::{
generation_loop, prepare_generation, GenerationContext, StopSuffixTrimmer, TokenAction,
};
use crate::tool_emulation::{message_for_emulator_action, EmulatorAction, StreamingEmulatorParser};
fn send_emulator_action(
action: &EmulatorAction,
message_id: &str,
tx: &StreamSender,
) -> Result<bool, ()> {
let (message, is_tool) = message_for_emulator_action(action, message_id);
tx.blocking_send(Ok((Some(message), None)))
.map_err(|_| ())?;
Ok(is_tool)
}
pub(super) fn generate_with_emulated_tools(
ctx: &mut GenerationContext<'_>,
code_mode_enabled: bool,
oai_messages_json: &str,
) -> Result<(), ProviderError> {
let prepared = prepare_generation(ctx, oai_messages_json, None, None)?;
let template_result = prepared.template_result;
let mut llama_ctx = prepared.llama_ctx;
let prompt_token_count = prepared.prompt_token_count;
let effective_ctx = prepared.effective_ctx;
let message_id = ctx.message_id;
let tx = ctx.tx;
let mut emulator_parser = StreamingEmulatorParser::new(code_mode_enabled);
let mut output_filter = ThinkingOutputFilter::new(
ctx.settings.enable_thinking,
&template_result.generation_prompt,
);
let mut stop_trimmer = StopSuffixTrimmer::new(&template_result.additional_stops);
let mut generated_text = String::new();
let mut tool_call_emitted = false;
let mut send_failed = false;
let mut stop_string_emitted = false;
let output_token_count = generation_loop(
&ctx.loaded.model,
&mut llama_ctx,
ctx.settings,
prompt_token_count,
effective_ctx,
|piece| {
generated_text.push_str(piece);
let filtered = output_filter.push_text(piece);
let (content, stop_seen) = stop_trimmer.push(&filtered.content);
let actions = emulator_parser.process_chunk(&content);
for action in actions {
match send_emulator_action(&action, message_id, tx) {
Ok(is_tool) => {
if is_tool {
tool_call_emitted = true;
}
}
Err(_) => {
send_failed = true;
return Ok(TokenAction::Stop);
}
}
}
if tool_call_emitted {
Ok(TokenAction::Stop)
} else if stop_seen
|| template_result
.additional_stops
.iter()
.any(|stop| generated_text.ends_with(stop))
{
stop_string_emitted = true;
Ok(TokenAction::Stop)
} else {
Ok(TokenAction::Continue)
}
},
)?;
if !send_failed {
let filtered = output_filter.finish();
if !filtered.thinking.is_empty() {
let mut message = Message::assistant().with_thinking(filtered.thinking, "");
message.id = Some(message_id.to_string());
send_failed = tx.blocking_send(Ok((Some(message), None))).is_err();
}
if !send_failed {
let content = if stop_string_emitted {
String::new()
} else {
let (content, stop_seen) = stop_trimmer.push(&filtered.content);
let mut content = content;
if !stop_seen {
content.push_str(&stop_trimmer.finish());
}
content
};
for action in emulator_parser.process_chunk(&content) {
if send_emulator_action(&action, message_id, tx).is_err() {
send_failed = true;
break;
}
}
}
}
if !send_failed {
for action in emulator_parser.flush() {
if send_emulator_action(&action, message_id, tx).is_err() {
break;
}
}
}
let provider_usage = finalize_usage(
ctx.log,
std::mem::take(&mut ctx.model_name),
"emulator",
prompt_token_count,
output_token_count,
None,
);
let _ = ctx.tx.blocking_send(Ok((None, Some(provider_usage))));
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn trim_chunks(chunks: &[&str], stops: &[String]) -> (String, bool) {
let mut trimmer = StopSuffixTrimmer::new(stops);
let mut output = String::new();
let mut stopped = false;
for chunk in chunks {
let (content, stop_seen) = trimmer.push(chunk);
output.push_str(&content);
if stop_seen {
stopped = true;
break;
}
}
if !stopped {
output.push_str(&trimmer.finish());
}
(output, stopped)
}
fn parse_with_seeded_thinking(
chunks: &[&str],
code_mode: bool,
) -> (String, Vec<EmulatorAction>) {
let mut output_filter = ThinkingOutputFilter::new(true, "<|assistant|><think>\n");
let mut parser = StreamingEmulatorParser::new(code_mode);
let mut thinking = String::new();
let mut actions = Vec::new();
for chunk in chunks {
let filtered = output_filter.push_text(chunk);
thinking.push_str(&filtered.thinking);
actions.extend(parser.process_chunk(&filtered.content));
}
let filtered = output_filter.finish();
thinking.push_str(&filtered.thinking);
actions.extend(parser.process_chunk(&filtered.content));
actions.extend(parser.flush());
(thinking, actions)
}
#[test]
fn stop_suffix_trimmer_strips_split_stop() {
let stops = vec!["<|eom_id|>".to_string()];
let (content, stopped) = trim_chunks(&["The answer", "<|e", "om_id|>"], &stops);
assert!(stopped);
assert_eq!(content, "The answer");
}
#[test]
fn stop_suffix_trimmer_flushes_partial_non_stop() {
let stops = vec!["<|eom_id|>".to_string()];
let (content, stopped) = trim_chunks(&["Use the <", " symbol"], &stops);
assert!(!stopped);
assert_eq!(content, "Use the < symbol");
}
#[test]
fn thinking_seeded_from_generation_prompt_is_not_emulated_text() {
let (thinking, actions) =
parse_with_seeded_thinking(&["reasoning\n$ echo hidden\n</think>The answer."], false);
assert_eq!(thinking.trim(), "reasoning\n$ echo hidden");
assert!(actions
.iter()
.all(|action| matches!(action, EmulatorAction::Text(_))));
let text: String = actions
.iter()
.filter_map(|action| match action {
EmulatorAction::Text(text) => Some(text.as_str()),
_ => None,
})
.collect();
assert_eq!(text.trim(), "The answer.");
}
}