1use std::sync::Arc;
14
15use crate::PromptFingerprint;
16use crate::llm::types::{LlmOutputPart, LlmResponse, LlmToolSpec, ProviderReasoningReplay};
17use crate::sansio::{
18 ChatContextProjector, ContextProjector, ModeProtocol, ProtocolDriverHandle, UnitModeProtocol,
19};
20use crate::session_model::{Message, MessageRole, Part, PartKind, PruneState, shared_parts};
21use crate::{ExecutionMode, PromptContribution, ToolSurface};
22
23#[derive(Clone)]
24pub struct ModeConfig<M: ModeProtocol = UnitModeProtocol> {
25 pub protocol: Arc<dyn ProtocolDriverHandle<M>>,
26 pub projector: Arc<dyn ContextProjector<M>>,
27 pub sync_execution_surface: bool,
28}
29
30impl<M: ModeProtocol> ModeConfig<M> {
31 pub fn chat(protocol: Arc<dyn ProtocolDriverHandle<M>>, sync_execution_surface: bool) -> Self {
32 Self {
33 protocol,
34 projector: Arc::new(ChatContextProjector),
35 sync_execution_surface,
36 }
37 }
38}
39
40#[derive(Clone)]
41pub struct ModePreamble<M: ModeProtocol = UnitModeProtocol> {
42 pub config: ModeConfig<M>,
43 pub tool_specs: Arc<Vec<LlmToolSpec>>,
44 pub tool_names: Arc<Vec<String>>,
45 pub tool_names_fingerprint: PromptFingerprint,
46 pub omitted_tool_count: usize,
47 pub execution_prompt: Arc<str>,
48 pub prompt_contributions: Vec<PromptContribution>,
49}
50
51#[derive(Clone, Debug)]
52pub struct ModeBuildInput {
53 pub mode: ExecutionMode,
54 pub tool_surface: std::sync::Arc<ToolSurface>,
55 pub extra_prompt_contributions: Vec<PromptContribution>,
56}
57
58pub fn normalized_response_parts(llm_response: &LlmResponse) -> Vec<LlmOutputPart> {
63 if llm_response.parts.is_empty() && !llm_response.full_text.is_empty() {
64 vec![LlmOutputPart::Text {
65 text: llm_response.full_text.clone(),
66 response_meta: None,
67 }]
68 } else {
69 llm_response.parts.clone()
70 }
71}
72
73pub fn reasoning_part(
77 asst_id: &str,
78 index: usize,
79 text: String,
80 meta: Option<ProviderReasoningReplay>,
81) -> Part {
82 Part {
83 id: format!("{asst_id}.p{index}"),
84 kind: PartKind::Reasoning,
85 content: text,
86 attachment: None,
87 tool_call_id: None,
88 tool_name: None,
89 tool_replay: None,
90 prune_state: PruneState::Intact,
91 reasoning_meta: meta,
92 response_meta: None,
93 }
94}
95
96pub fn append_assistant_text_part(out: &mut String, next: &str) {
100 if out.is_empty() {
101 out.push_str(next);
102 return;
103 }
104
105 let prev_trailing_newlines = out.chars().rev().take_while(|ch| *ch == '\n').count();
106 let next_leading_newlines = next.chars().take_while(|ch| *ch == '\n').count();
107 let total_boundary_newlines = prev_trailing_newlines + next_leading_newlines;
108 if total_boundary_newlines < 2 {
109 out.push_str(&"\n".repeat(2 - total_boundary_newlines));
110 }
111
112 out.push_str(next);
113}
114
115pub fn turn_limit_exhausted_message(max_turns: usize) -> Message {
118 let id = crate::session_model::fresh_message_id();
119 Message {
120 id: id.clone(),
121 role: MessageRole::System,
122 parts: shared_parts(vec![Part {
123 id: format!("{id}.p0"),
124 kind: PartKind::Error,
125 content: format!("Turn limit reached ({max_turns}) before a final assistant response."),
126 attachment: None,
127 tool_call_id: None,
128 tool_name: None,
129 tool_replay: None,
130 prune_state: PruneState::Intact,
131 reasoning_meta: None,
132 response_meta: None,
133 }]),
134 origin: None,
135 }
136}