1pub mod message;
2pub mod prompt;
3
4pub use message::{
5 BaseRenderCache, Message, MessageRole, MessageSequence, Part, PartAttachment, PartKind,
6 PruneState, RenderedPrompt, append_rendered_prompt, messages_are_prompt_resume_safe,
7 render_prompt, render_transcript_prompt, shared_parts,
8};
9pub use prompt::{
10 MAIN_AGENT_INTRO, PromptBuiltin, PromptLayer, PromptSlot, PromptSlotLayer, PromptTemplate,
11 PromptTemplateEntry, PromptTemplateSection, ResolvedPromptLayer, default_prompt_template,
12 resolve_prompt_layers,
13};
14
15use std::collections::HashMap;
16use std::sync::Arc;
17
18use crate::ToolDefinition;
19use crate::llm::types::LlmToolSpec;
20use crate::plugin::{CheckpointKind, PluginMessage, PluginSurfaceEvent};
21use crate::{MessageOrigin, ToolCallRecord};
22
23#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
24#[allow(clippy::large_enum_variant)]
25pub enum SessionEventRecord<ME = ()> {
26 Conversation(ConversationRecord),
27 Tool(ToolEvent),
28 Mode(ME),
29 StateSnapshot(StateSnapshotEvent),
30}
31
32#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
33pub struct ConversationRecord {
34 pub id: String,
35 pub role: MessageRole,
36 pub parts: Arc<Vec<Part>>,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub origin: Option<MessageOrigin>,
39}
40
41#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
42pub struct AcceptedInjectedTurnInput {
43 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub id: Option<String>,
45 pub message: PluginMessage,
46}
47
48impl ConversationRecord {
49 pub fn from_message(message: Message) -> Self {
50 Self {
51 id: message.id,
52 role: message.role,
53 parts: message.parts,
54 origin: message.origin,
55 }
56 }
57
58 pub fn to_message(&self) -> Message {
59 Message {
60 id: self.id.clone(),
61 role: self.role,
62 parts: Arc::clone(&self.parts),
63 origin: self.origin.clone(),
64 }
65 }
66}
67
68#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
69pub enum ToolEvent {
70 Invocation {
71 stable_key: String,
72 record: ToolCallRecord,
73 },
74}
75
76#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
77pub enum StateSnapshotEvent {
78 Lashlang {
79 version: u32,
80 data: String,
81 files: HashMap<String, String>,
82 },
83}
84
85#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
87pub struct TokenUsage {
88 pub input_tokens: i64,
89 pub output_tokens: i64,
90 pub cached_input_tokens: i64,
91 #[serde(default)]
92 pub reasoning_tokens: i64,
93}
94
95impl TokenUsage {
96 pub fn total(&self) -> i64 {
97 self.input_tokens + self.output_tokens + self.reasoning_tokens
98 }
99
100 pub fn add(&mut self, other: &TokenUsage) {
101 self.input_tokens += other.input_tokens;
102 self.output_tokens += other.output_tokens;
103 self.cached_input_tokens += other.cached_input_tokens;
104 self.reasoning_tokens += other.reasoning_tokens;
105 }
106}
107
108#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
109pub struct ErrorEnvelope {
110 pub kind: String,
111 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub code: Option<String>,
113 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub terminal_reason: Option<crate::llm::types::LlmTerminalReason>,
115 pub user_message: String,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
117 pub raw: Option<String>,
118}
119
120#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
121#[serde(tag = "type")]
122#[allow(clippy::large_enum_variant)]
123pub enum SessionEvent {
124 #[serde(rename = "text_delta")]
125 TextDelta { content: String },
126 #[serde(rename = "reasoning_delta")]
131 ReasoningDelta { content: String },
132 #[serde(rename = "tool_call")]
133 ToolCall {
134 #[serde(default, skip_serializing_if = "Option::is_none")]
135 call_id: Option<String>,
136 name: String,
137 args: serde_json::Value,
138 output: crate::ToolCallOutput,
139 duration_ms: u64,
140 },
141 #[serde(rename = "tool_call_start")]
142 ToolCallStart {
143 #[serde(default, skip_serializing_if = "Option::is_none")]
144 call_id: Option<String>,
145 name: String,
146 args: serde_json::Value,
147 },
148 #[serde(rename = "message")]
149 Message { text: String, kind: String },
150 #[serde(rename = "llm_request")]
151 LlmRequest {
152 mode_iteration: usize,
153 message_count: usize,
154 tool_list: String,
155 },
156 #[serde(rename = "llm_response")]
157 LlmResponse {
158 mode_iteration: usize,
159 content: String,
160 duration_ms: u64,
161 },
162 #[serde(rename = "token_usage")]
163 TokenUsage {
164 mode_iteration: usize,
165 usage: TokenUsage,
166 cumulative: TokenUsage,
167 },
168 #[serde(rename = "child_token_usage")]
169 ChildTokenUsage {
170 session_id: String,
171 source: String,
172 model: String,
173 mode_iteration: usize,
174 usage: TokenUsage,
175 cumulative: TokenUsage,
176 },
177 #[serde(rename = "retry_status")]
178 RetryStatus {
179 wait_seconds: u64,
180 attempt: usize,
181 max_attempts: usize,
182 reason: String,
183 #[serde(default, skip_serializing_if = "Option::is_none")]
184 envelope: Option<ErrorEnvelope>,
185 },
186 #[serde(rename = "injected_turn_input_accepted")]
187 InjectedTurnInputAccepted {
188 inputs: Vec<AcceptedInjectedTurnInput>,
189 checkpoint: CheckpointKind,
190 },
191 #[serde(rename = "injected_messages_committed")]
192 InjectedMessagesCommitted {
193 messages: Vec<PluginMessage>,
194 checkpoint: CheckpointKind,
195 },
196 #[serde(rename = "plugin_event")]
197 PluginEvent {
198 plugin_id: String,
199 event: PluginSurfaceEvent,
200 },
201 #[serde(rename = "turn_outcome")]
204 TurnOutcome { outcome: TurnOutcome },
205 #[serde(rename = "done")]
206 Done,
207 #[serde(rename = "error")]
208 Error {
209 message: String,
210 #[serde(default, skip_serializing_if = "Option::is_none")]
211 envelope: Option<ErrorEnvelope>,
212 },
213}
214
215#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
216#[serde(rename_all = "snake_case")]
217pub enum TurnOutcome {
218 Finished(TurnFinish),
219 Handoff { session_id: String },
220 Stopped(TurnStop),
221}
222
223#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
224#[serde(rename_all = "snake_case")]
225pub enum TurnFinish {
226 AssistantMessage {
227 text: String,
228 },
229 SubmittedValue {
230 value: serde_json::Value,
231 },
232 ToolValue {
233 tool_name: String,
234 value: serde_json::Value,
235 },
236}
237
238#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
239#[serde(rename_all = "snake_case")]
240pub enum TurnStop {
241 Cancelled,
242 Incomplete,
243 InvalidInput,
244 MaxTurns,
245 ToolFailure,
246 ProviderError,
247 PluginAbort,
248 RuntimeError,
249 SubmittedError {
250 value: serde_json::Value,
251 },
252 ToolError {
253 tool_name: String,
254 value: serde_json::Value,
255 },
256}
257
258#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
259pub struct PromptRequest {
260 pub question: String,
261 #[serde(default, skip_serializing_if = "Option::is_none")]
262 pub panel: Option<PromptPanel>,
263 #[serde(default, skip_serializing_if = "Vec::is_empty")]
264 pub options: Vec<String>,
265 #[serde(default)]
266 pub selection_mode: PromptSelectionMode,
267 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
268 pub allow_note: bool,
269}
270
271#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
272pub struct PromptPanel {
273 pub title: String,
274 pub markdown: String,
275}
276
277impl PromptRequest {
278 pub fn freeform(question: impl Into<String>) -> Self {
279 Self {
280 question: question.into(),
281 panel: None,
282 options: Vec::new(),
283 selection_mode: PromptSelectionMode::Single,
284 allow_note: false,
285 }
286 }
287
288 pub fn single(question: impl Into<String>, options: Vec<String>) -> Self {
289 Self {
290 question: question.into(),
291 panel: None,
292 options,
293 selection_mode: PromptSelectionMode::Single,
294 allow_note: false,
295 }
296 }
297
298 pub fn multi(question: impl Into<String>, options: Vec<String>) -> Self {
299 Self {
300 question: question.into(),
301 panel: None,
302 options,
303 selection_mode: PromptSelectionMode::Multi,
304 allow_note: false,
305 }
306 }
307
308 pub fn with_optional_note(mut self) -> Self {
309 self.allow_note = !self.is_freeform();
310 self
311 }
312
313 pub fn with_markdown_panel(
314 mut self,
315 title: impl Into<String>,
316 markdown: impl Into<String>,
317 ) -> Self {
318 self.panel = Some(PromptPanel {
319 title: title.into(),
320 markdown: markdown.into(),
321 });
322 self
323 }
324
325 pub fn is_freeform(&self) -> bool {
326 self.options.is_empty()
327 }
328
329 pub fn allows_note(&self) -> bool {
330 self.allow_note && !self.is_freeform()
331 }
332
333 pub fn empty_response(&self) -> PromptResponse {
334 if self.is_freeform() {
335 PromptResponse::Text {
336 text: String::new(),
337 }
338 } else {
339 match self.selection_mode {
340 PromptSelectionMode::Single => PromptResponse::Single {
341 selection: String::new(),
342 note: None,
343 },
344 PromptSelectionMode::Multi => PromptResponse::Multi {
345 selections: Vec::new(),
346 note: None,
347 },
348 }
349 }
350 }
351}
352
353#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
354#[serde(rename_all = "snake_case")]
355pub enum PromptSelectionMode {
356 #[default]
357 Single,
358 Multi,
359}
360
361#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
362#[serde(tag = "kind", rename_all = "snake_case")]
363pub enum PromptResponse {
364 Text {
365 text: String,
366 },
367 Single {
368 selection: String,
369 #[serde(default, skip_serializing_if = "Option::is_none")]
370 note: Option<String>,
371 },
372 Multi {
373 selections: Vec<String>,
374 #[serde(default, skip_serializing_if = "Option::is_none")]
375 note: Option<String>,
376 },
377}
378
379#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
380pub struct TurnTerminationPolicyState {
381 max_steps_final: bool,
382}
383
384impl Default for TurnTerminationPolicyState {
385 fn default() -> Self {
386 Self::new()
387 }
388}
389
390impl TurnTerminationPolicyState {
391 pub fn new() -> Self {
392 Self {
393 max_steps_final: false,
394 }
395 }
396
397 pub fn should_force_exit_after_grace_turn(&self) -> bool {
398 self.max_steps_final
399 }
400
401 pub fn maybe_schedule_turn_limit_final(
402 &mut self,
403 mode_iteration: usize,
404 mode_run_offset: usize,
405 max_turns: Option<usize>,
406 msgs: &mut Vec<Message>,
407 ) {
408 let Some(max) = max_turns else { return };
409 if mode_iteration < mode_run_offset + max {
410 return;
411 }
412 let sys_id = fresh_message_id();
413 msgs.push(Message {
414 id: sys_id.clone(),
415 role: MessageRole::System,
416 parts: Arc::new(vec![Part {
417 id: format!("{}.p0", sys_id),
418 kind: PartKind::Text,
419 content: format!(
420 "Turn limit reached ({max}). You MUST reply in plain prose now containing:\n\
421 1. Summary of what you accomplished\n\
422 2. List of remaining tasks not yet completed\n\
423 3. Recommended next steps\n\
424 Do NOT make any more tool calls and do NOT emit a mode-specific tag."
425 ),
426 attachment: None,
427 tool_call_id: None,
428 tool_name: None,
429 tool_replay: None,
430 prune_state: PruneState::Intact,
431 reasoning_meta: None,
432 response_meta: None,
433 }]),
434 origin: None,
435 });
436 self.max_steps_final = true;
437 }
438}
439
440pub fn make_error_envelope(
441 kind: &str,
442 code: Option<&str>,
443 terminal_reason: Option<crate::llm::types::LlmTerminalReason>,
444 user_message: impl Into<String>,
445 raw: Option<String>,
446) -> ErrorEnvelope {
447 let user_message = user_message.into();
448 ErrorEnvelope {
449 kind: kind.to_string(),
450 code: code.map(str::to_string),
451 terminal_reason,
452 user_message,
453 raw: raw.map(|s| truncate_raw_error(s.trim())),
454 }
455}
456
457pub fn make_error_event(
458 kind: &str,
459 code: Option<&str>,
460 user_message: impl Into<String>,
461 raw: Option<String>,
462) -> SessionEvent {
463 let user_message = user_message.into();
464 SessionEvent::Error {
465 message: user_message.clone(),
466 envelope: Some(make_error_envelope(kind, code, None, user_message, raw)),
467 }
468}
469
470pub fn truncate_raw_error(s: &str) -> String {
471 const MAX_RAW: usize = 4000;
472 if s.len() <= MAX_RAW {
473 return s.to_string();
474 }
475 let keep = MAX_RAW / 2;
476 let omitted = s.len() - MAX_RAW;
477 format!(
478 "{}\n\n... ({omitted} chars omitted) ...\n\n{}",
479 &s[..keep],
480 &s[s.len() - keep..]
481 )
482}
483
484pub fn format_tool_result_content(success: bool, result: &serde_json::Value) -> String {
485 if success {
486 match result {
487 serde_json::Value::String(text) => text.clone(),
488 other => serde_json::to_string(other).unwrap_or_else(|_| "null".to_string()),
489 }
490 } else {
491 match result {
492 serde_json::Value::String(text) => {
493 if text.is_empty() {
494 "[Tool execution failed]".to_string()
495 } else if text.starts_with("[Tool execution failed]") {
496 text.clone()
497 } else {
498 format!("[Tool execution failed]\n{text}")
499 }
500 }
501 other => serde_json::to_string(&serde_json::json!({ "error": other }))
502 .unwrap_or_else(|_| "{\"error\":\"tool execution failed\"}".to_string()),
503 }
504 }
505}
506
507pub fn format_tool_output_content(output: &crate::ToolCallOutput) -> String {
508 match &output.outcome {
509 crate::ToolCallOutcome::Success(value) => {
510 let value = value.to_json_value();
511 match value {
512 serde_json::Value::String(text) => text,
513 other => serde_json::to_string(&other).unwrap_or_else(|_| "null".to_string()),
514 }
515 }
516 crate::ToolCallOutcome::Failure(failure) => {
517 if failure.message.is_empty() {
518 "[Tool execution failed]".to_string()
519 } else {
520 format!("[Tool execution failed]\n{}", failure.message)
521 }
522 }
523 crate::ToolCallOutcome::Cancelled(cancellation) => {
524 if cancellation.message.is_empty() {
525 "[Tool execution cancelled]".to_string()
526 } else {
527 format!("[Tool execution cancelled]\n{}", cancellation.message)
528 }
529 }
530 }
531}
532
533pub fn fresh_message_id() -> String {
534 format!("m{}", uuid::Uuid::new_v4().simple())
535}
536
537pub fn reassign_part_ids(message_id: &str, parts: &mut [Part]) {
538 for (idx, part) in parts.iter_mut().enumerate() {
539 part.id = format!("{message_id}.p{idx}");
540 }
541}
542
543pub fn model_tool_specs_iter<'a>(
544 tools: impl IntoIterator<Item = &'a ToolDefinition>,
545) -> Vec<LlmToolSpec> {
546 tools
547 .into_iter()
548 .map(|tool| {
549 let model_tool = tool.model_tool();
550 LlmToolSpec {
551 name: model_tool.name,
552 description: model_tool.description,
553 input_schema: model_tool.input_schema,
554 output_schema: model_tool.output_schema,
555 input_schema_projections: model_tool.input_schema_projections,
556 output_schema_projections: model_tool.output_schema_projections,
557 }
558 })
559 .collect()
560}
561
562pub fn model_tool_specs(tools: &[ToolDefinition]) -> Vec<LlmToolSpec> {
563 model_tool_specs_iter(tools.iter())
564}