1use std::collections::BTreeMap;
2use std::fmt::{Display, Formatter};
3
4use crate::compact::{
5 compact_session, estimate_session_tokens, CompactionConfig, CompactionResult,
6};
7use crate::config::RuntimeFeatureConfig;
8use crate::config::RuntimeHookConfig;
9use crate::hooks::{HookRunResult, HookRunner};
10use crate::permissions::{PermissionOutcome, PermissionPolicy, PermissionPrompter};
11use crate::session::{ContentBlock, ConversationMessage, Session};
12use crate::usage::{TokenUsage, UsageTracker};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct ApiRequest {
16 pub system_prompt: Vec<String>,
17 pub messages: Vec<ConversationMessage>,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum AssistantEvent {
22 TextDelta(String),
23 ToolUse {
24 id: String,
25 name: String,
26 input: String,
27 },
28 Usage(TokenUsage),
29 MessageStop,
30}
31
32pub trait ApiClient {
33 fn stream(&mut self, request: ApiRequest) -> Result<Vec<AssistantEvent>, RuntimeError>;
34}
35
36pub trait ToolExecutor {
37 fn execute(&mut self, tool_name: &str, input: &str) -> Result<String, ToolError>;
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct ToolError {
42 message: String,
43}
44
45impl ToolError {
46 #[must_use]
47 pub fn new(message: impl Into<String>) -> Self {
48 Self {
49 message: message.into(),
50 }
51 }
52}
53
54impl Display for ToolError {
55 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
56 write!(f, "{}", self.message)
57 }
58}
59
60impl std::error::Error for ToolError {}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct RuntimeError {
64 message: String,
65}
66
67impl RuntimeError {
68 #[must_use]
69 pub fn new(message: impl Into<String>) -> Self {
70 Self {
71 message: message.into(),
72 }
73 }
74}
75
76impl Display for RuntimeError {
77 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
78 write!(f, "{}", self.message)
79 }
80}
81
82impl std::error::Error for RuntimeError {}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct TurnSummary {
86 pub assistant_messages: Vec<ConversationMessage>,
87 pub tool_results: Vec<ConversationMessage>,
88 pub iterations: usize,
89 pub usage: TokenUsage,
90}
91
92pub struct ConversationRuntime<C, T> {
93 session: Session,
94 api_client: C,
95 tool_executor: T,
96 permission_policy: PermissionPolicy,
97 system_prompt: Vec<String>,
98 max_iterations: usize,
99 usage_tracker: UsageTracker,
100 hook_runner: HookRunner<RuntimeHookConfig>,
101}
102
103impl<C, T> ConversationRuntime<C, T>
104where
105 C: ApiClient,
106 T: ToolExecutor,
107{
108 #[must_use]
109 pub fn new(
110 session: Session,
111 api_client: C,
112 tool_executor: T,
113 permission_policy: PermissionPolicy,
114 system_prompt: Vec<String>,
115 ) -> Self {
116 Self::new_with_features(
117 session,
118 api_client,
119 tool_executor,
120 permission_policy,
121 system_prompt,
122 &RuntimeFeatureConfig::default(),
123 )
124 }
125
126 #[must_use]
127 pub fn new_with_features(
128 session: Session,
129 api_client: C,
130 tool_executor: T,
131 permission_policy: PermissionPolicy,
132 system_prompt: Vec<String>,
133 feature_config: &RuntimeFeatureConfig,
134 ) -> Self {
135 let usage_tracker = UsageTracker::from_session(&session);
136 Self {
137 session,
138 api_client,
139 tool_executor,
140 permission_policy,
141 system_prompt,
142 max_iterations: 200,
143 usage_tracker,
144 hook_runner: HookRunner::from_feature_config(feature_config),
145 }
146 }
147
148 pub fn update_system_prompt(&mut self, system_prompt: Vec<String>) {
149 self.system_prompt = system_prompt;
150 }
151
152 #[must_use]
153 pub fn with_max_iterations(mut self, max_iterations: usize) -> Self {
154 self.max_iterations = max_iterations;
155 self
156 }
157
158 pub fn run_turn(
159 &mut self,
160 user_input: impl Into<String>,
161 mut prompter: Option<&mut dyn PermissionPrompter>,
162 ) -> Result<TurnSummary, RuntimeError> {
163 self.session
164 .messages
165 .push(ConversationMessage::user_text(user_input.into()));
166
167 let mut assistant_messages = Vec::new();
168 let mut tool_results = Vec::new();
169 let mut iterations = 0;
170
171 loop {
172 iterations += 1;
173 if iterations > self.max_iterations {
174 return Err(RuntimeError::new(
175 "conversation loop exceeded the maximum number of iterations",
176 ));
177 }
178
179 let request = ApiRequest {
180 system_prompt: self.system_prompt.clone(),
181 messages: self.session.messages.clone(),
182 };
183 let events = self.api_client.stream(request)?;
184 let (assistant_message, usage) = build_assistant_message(events)?;
185 if let Some(usage) = usage {
186 self.usage_tracker.record(usage);
187 }
188 let pending_tool_uses = assistant_message
189 .blocks
190 .iter()
191 .filter_map(|block| match block {
192 ContentBlock::ToolUse { id, name, input } => {
193 Some((id.clone(), name.clone(), input.clone()))
194 }
195 _ => None,
196 })
197 .collect::<Vec<_>>();
198
199 self.session.messages.push(assistant_message.clone());
200 assistant_messages.push(assistant_message);
201
202 if pending_tool_uses.is_empty() {
203 break;
204 }
205
206 for (tool_use_id, tool_name, input) in pending_tool_uses {
207 let permission_outcome = if let Some(prompt) = prompter.as_mut() {
208 self.permission_policy
209 .authorize(&tool_name, &input, Some(*prompt))
210 } else {
211 self.permission_policy.authorize(&tool_name, &input, None)
212 };
213
214 let result_message = match permission_outcome {
215 PermissionOutcome::Allow => {
216 let pre_hook_result = self.hook_runner.run_pre_tool_use(&tool_name, &input);
217 if pre_hook_result.is_denied() {
218 let deny_message = format!("PreToolUse hook denied tool `{tool_name}`");
219 ConversationMessage::tool_result(
220 tool_use_id,
221 tool_name,
222 format_hook_message(&pre_hook_result, &deny_message),
223 true,
224 )
225 } else {
226 let (mut output, mut is_error) =
227 match self.tool_executor.execute(&tool_name, &input) {
228 Ok(output) => (output, false),
229 Err(error) => (error.to_string(), true),
230 };
231 output = merge_hook_feedback(pre_hook_result.messages(), output, false);
232
233 let post_hook_result = self
234 .hook_runner
235 .run_post_tool_use(&tool_name, &input, &output, is_error);
236 if post_hook_result.is_denied() {
237 is_error = true;
238 }
239 output = merge_hook_feedback(
240 post_hook_result.messages(),
241 output,
242 post_hook_result.is_denied(),
243 );
244
245 ConversationMessage::tool_result(
246 tool_use_id,
247 tool_name,
248 output,
249 is_error,
250 )
251 }
252 }
253 PermissionOutcome::Deny { reason } => {
254 ConversationMessage::tool_result(tool_use_id, tool_name, reason, true)
255 }
256 };
257 self.session.messages.push(result_message.clone());
258 tool_results.push(result_message);
259 }
260 }
261
262 Ok(TurnSummary {
263 assistant_messages,
264 tool_results,
265 iterations,
266 usage: self.usage_tracker.cumulative_usage(),
267 })
268 }
269
270 #[must_use]
271 pub fn compact(&self, config: CompactionConfig) -> CompactionResult {
272 compact_session(&self.session, config)
273 }
274
275 #[must_use]
276 pub fn estimated_tokens(&self) -> usize {
277 estimate_session_tokens(&self.session)
278 }
279
280 #[must_use]
281 pub fn usage(&self) -> &UsageTracker {
282 &self.usage_tracker
283 }
284
285 #[must_use]
286 pub fn session(&self) -> &Session {
287 &self.session
288 }
289
290 #[must_use]
291 pub fn into_session(self) -> Session {
292 self.session
293 }
294}
295
296fn build_assistant_message(
297 events: Vec<AssistantEvent>,
298) -> Result<(ConversationMessage, Option<TokenUsage>), RuntimeError> {
299 let mut text = String::new();
300 let mut blocks = Vec::new();
301 let mut finished = false;
302 let mut usage = None;
303
304 for event in events {
305 match event {
306 AssistantEvent::TextDelta(delta) => text.push_str(&delta),
307 AssistantEvent::ToolUse { id, name, input } => {
308 flush_text_block(&mut text, &mut blocks);
309 blocks.push(ContentBlock::ToolUse { id, name, input });
310 }
311 AssistantEvent::Usage(value) => usage = Some(value),
312 AssistantEvent::MessageStop => {
313 finished = true;
314 }
315 }
316 }
317
318 flush_text_block(&mut text, &mut blocks);
319
320 if !finished {
321 return Err(RuntimeError::new(
322 "assistant stream ended without a message stop event",
323 ));
324 }
325 if blocks.is_empty() {
326 return Err(RuntimeError::new("assistant stream produced no content"));
327 }
328
329 Ok((
330 ConversationMessage::assistant_with_usage(blocks, usage),
331 usage,
332 ))
333}
334
335fn flush_text_block(text: &mut String, blocks: &mut Vec<ContentBlock>) {
336 if !text.is_empty() {
337 blocks.push(ContentBlock::Text {
338 text: std::mem::take(text),
339 });
340 }
341}
342
343fn format_hook_message(result: &HookRunResult, fallback: &str) -> String {
344 if result.messages().is_empty() {
345 fallback.to_string()
346 } else {
347 result.messages().join("\n")
348 }
349}
350
351fn merge_hook_feedback(messages: &[String], output: String, denied: bool) -> String {
352 if messages.is_empty() {
353 return output;
354 }
355
356 let mut sections = Vec::new();
357 if !output.trim().is_empty() {
358 sections.push(output);
359 }
360 let label = if denied {
361 "Hook feedback (denied)"
362 } else {
363 "Hook feedback"
364 };
365 sections.push(format!("{label}:\n{}", messages.join("\n")));
366 sections.join("\n\n")
367}
368
369type ToolHandler = Box<dyn FnMut(&str) -> Result<String, ToolError>>;
370
371#[derive(Default)]
372pub struct StaticToolExecutor {
373 handlers: BTreeMap<String, ToolHandler>,
374}
375
376impl StaticToolExecutor {
377 #[must_use]
378 pub fn new() -> Self {
379 Self::default()
380 }
381
382 #[must_use]
383 pub fn register(
384 mut self,
385 tool_name: impl Into<String>,
386 handler: impl FnMut(&str) -> Result<String, ToolError> + 'static,
387 ) -> Self {
388 self.handlers.insert(tool_name.into(), Box::new(handler));
389 self
390 }
391}
392
393impl ToolExecutor for StaticToolExecutor {
394 fn execute(&mut self, tool_name: &str, input: &str) -> Result<String, ToolError> {
395 self.handlers
396 .get_mut(tool_name)
397 .ok_or_else(|| ToolError::new(format!("unknown tool: {tool_name}")))?(input)
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use super::{
404 ApiClient, ApiRequest, AssistantEvent, ConversationRuntime, RuntimeError,
405 StaticToolExecutor,
406 };
407 use crate::compact::CompactionConfig;
408 use crate::permissions::{
409 PermissionMode, PermissionPolicy, PermissionPromptDecision, PermissionPrompter,
410 PermissionRequest,
411 };
412 use crate::prompt::{ProjectContext, SystemPromptBuilder};
413 use crate::session::{ContentBlock, MessageRole, Session};
414 use crate::usage::TokenUsage;
415 use std::path::PathBuf;
416
417 struct ScriptedApiClient {
418 call_count: usize,
419 }
420
421 impl ApiClient for ScriptedApiClient {
422 fn stream(&mut self, request: ApiRequest) -> Result<Vec<AssistantEvent>, RuntimeError> {
423 self.call_count += 1;
424 match self.call_count {
425 1 => {
426 assert!(request
427 .messages
428 .iter()
429 .any(|message| message.role == MessageRole::User));
430 Ok(vec![
431 AssistantEvent::TextDelta("Let me calculate that.".to_string()),
432 AssistantEvent::ToolUse {
433 id: "tool-1".to_string(),
434 name: "add".to_string(),
435 input: "2,2".to_string(),
436 },
437 AssistantEvent::Usage(TokenUsage {
438 input_tokens: 20,
439 output_tokens: 6,
440 cache_creation_input_tokens: 1,
441 cache_read_input_tokens: 2,
442 }),
443 AssistantEvent::MessageStop,
444 ])
445 }
446 2 => {
447 let last_message = request
448 .messages
449 .last()
450 .expect("tool result should be present");
451 assert_eq!(last_message.role, MessageRole::Tool);
452 Ok(vec![
453 AssistantEvent::TextDelta("The answer is 4.".to_string()),
454 AssistantEvent::Usage(TokenUsage {
455 input_tokens: 24,
456 output_tokens: 4,
457 cache_creation_input_tokens: 1,
458 cache_read_input_tokens: 3,
459 }),
460 AssistantEvent::MessageStop,
461 ])
462 }
463 _ => Err(RuntimeError::new("unexpected extra API call")),
464 }
465 }
466 }
467
468 struct PromptAllowOnce;
469
470 impl PermissionPrompter for PromptAllowOnce {
471 fn decide(&mut self, request: &PermissionRequest) -> PermissionPromptDecision {
472 assert_eq!(request.tool_name, "add");
473 PermissionPromptDecision::Allow
474 }
475 }
476
477 #[test]
478 fn runs_user_to_tool_to_result_loop_end_to_end_and_tracks_usage() {
479 let api_client = ScriptedApiClient { call_count: 0 };
480 let tool_executor = StaticToolExecutor::new().register("add", |input| {
481 let total = input
482 .split(',')
483 .map(|part| part.parse::<i32>().expect("input must be valid integer"))
484 .sum::<i32>();
485 Ok(total.to_string())
486 });
487 let permission_policy = PermissionPolicy::new(PermissionMode::WorkspaceWrite);
488 let system_prompt = SystemPromptBuilder::new()
489 .with_project_context(ProjectContext {
490 cwd: PathBuf::from("/tmp/project"),
491 current_date: "2026-03-31".to_string(),
492 git_status: None,
493 git_diff: None,
494 instruction_files: Vec::new(),
495 })
496 .with_os("linux", "6.8")
497 .build();
498 let mut runtime = ConversationRuntime::new(
499 Session::new(),
500 api_client,
501 tool_executor,
502 permission_policy,
503 system_prompt,
504 );
505
506 let summary = runtime
507 .run_turn("what is 2 + 2?", Some(&mut PromptAllowOnce))
508 .expect("conversation loop should succeed");
509
510 assert_eq!(summary.iterations, 2);
511 assert_eq!(summary.assistant_messages.len(), 2);
512 assert_eq!(summary.tool_results.len(), 1);
513 assert_eq!(runtime.session().messages.len(), 4);
514 assert_eq!(summary.usage.output_tokens, 10);
515 assert!(matches!(
516 runtime.session().messages[1].blocks[1],
517 ContentBlock::ToolUse { .. }
518 ));
519 assert!(matches!(
520 runtime.session().messages[2].blocks[0],
521 ContentBlock::ToolResult {
522 is_error: false,
523 ..
524 }
525 ));
526 }
527
528 #[test]
529 fn records_denied_tool_results_when_prompt_rejects() {
530 struct RejectPrompter;
531 impl PermissionPrompter for RejectPrompter {
532 fn decide(&mut self, _request: &PermissionRequest) -> PermissionPromptDecision {
533 PermissionPromptDecision::Deny {
534 reason: "not now".to_string(),
535 }
536 }
537 }
538
539 struct SingleCallApiClient;
540 impl ApiClient for SingleCallApiClient {
541 fn stream(&mut self, request: ApiRequest) -> Result<Vec<AssistantEvent>, RuntimeError> {
542 if request
543 .messages
544 .iter()
545 .any(|message| message.role == MessageRole::Tool)
546 {
547 return Ok(vec![
548 AssistantEvent::TextDelta("I could not use the tool.".to_string()),
549 AssistantEvent::MessageStop,
550 ]);
551 }
552 Ok(vec![
553 AssistantEvent::ToolUse {
554 id: "tool-1".to_string(),
555 name: "blocked".to_string(),
556 input: "secret".to_string(),
557 },
558 AssistantEvent::MessageStop,
559 ])
560 }
561 }
562
563 let mut runtime = ConversationRuntime::new(
564 Session::new(),
565 SingleCallApiClient,
566 StaticToolExecutor::new(),
567 PermissionPolicy::new(PermissionMode::WorkspaceWrite),
568 vec!["system".to_string()],
569 );
570
571 let summary = runtime
572 .run_turn("use the tool", Some(&mut RejectPrompter))
573 .expect("conversation should continue after denied tool");
574
575 assert_eq!(summary.tool_results.len(), 1);
576 assert!(matches!(
577 &summary.tool_results[0].blocks[0],
578 ContentBlock::ToolResult { is_error: true, output, .. } if output == "not now"
579 ));
580 }
581
582 #[test]
583 #[cfg(unix)]
584 fn denies_tool_use_when_pre_tool_hook_blocks() {
585 use crate::config::{RuntimeFeatureConfig, RuntimeHookConfig};
586 struct SingleCallApiClient;
587 impl ApiClient for SingleCallApiClient {
588 fn stream(&mut self, request: ApiRequest) -> Result<Vec<AssistantEvent>, RuntimeError> {
589 if request
590 .messages
591 .iter()
592 .any(|message| message.role == MessageRole::Tool)
593 {
594 return Ok(vec![
595 AssistantEvent::TextDelta("blocked".to_string()),
596 AssistantEvent::MessageStop,
597 ]);
598 }
599 Ok(vec![
600 AssistantEvent::ToolUse {
601 id: "tool-1".to_string(),
602 name: "blocked".to_string(),
603 input: r#"{"path":"secret.txt"}"#.to_string(),
604 },
605 AssistantEvent::MessageStop,
606 ])
607 }
608 }
609
610 let deny_config = RuntimeFeatureConfig::default().with_hooks(RuntimeHookConfig::new(
611 vec!["printf 'blocked by hook'; exit 2".to_string()],
612 Vec::new(),
613 ));
614 let mut runtime = ConversationRuntime::new_with_features(
615 Session::new(),
616 SingleCallApiClient,
617 StaticToolExecutor::new().register("blocked", |_input| {
618 panic!("tool should not execute when hook denies")
619 }),
620 PermissionPolicy::new(PermissionMode::DangerFullAccess)
621 .with_tool_requirement("blocked", PermissionMode::WorkspaceWrite),
622 vec!["system".to_string()],
623 &deny_config,
624 );
625
626 let summary = runtime
627 .run_turn("use the tool", None)
628 .expect("conversation should continue after hook denial");
629
630 assert_eq!(summary.tool_results.len(), 1);
631 let ContentBlock::ToolResult {
632 is_error, output, ..
633 } = &summary.tool_results[0].blocks[0]
634 else {
635 panic!("expected tool result block");
636 };
637 assert!(
638 *is_error,
639 "hook denial should produce an error result: {output}"
640 );
641 assert!(
642 output.contains("denied tool") || output.contains("blocked by hook"),
643 "unexpected hook denial output: {output:?}"
644 );
645 }
646
647 #[test]
648 #[cfg(unix)]
649 fn appends_post_tool_hook_feedback_to_tool_result() {
650 use crate::config::{RuntimeFeatureConfig, RuntimeHookConfig};
651 struct TwoCallApiClient {
652 calls: usize,
653 }
654
655 impl ApiClient for TwoCallApiClient {
656 fn stream(&mut self, request: ApiRequest) -> Result<Vec<AssistantEvent>, RuntimeError> {
657 self.calls += 1;
658 match self.calls {
659 1 => Ok(vec![
660 AssistantEvent::ToolUse {
661 id: "tool-1".to_string(),
662 name: "add".to_string(),
663 input: r#"{"lhs":2,"rhs":2}"#.to_string(),
664 },
665 AssistantEvent::MessageStop,
666 ]),
667 2 => {
668 assert!(request
669 .messages
670 .iter()
671 .any(|message| message.role == MessageRole::Tool));
672 Ok(vec![
673 AssistantEvent::TextDelta("done".to_string()),
674 AssistantEvent::MessageStop,
675 ])
676 }
677 _ => Err(RuntimeError::new("unexpected extra API call")),
678 }
679 }
680 }
681
682 let hook_config = RuntimeFeatureConfig::default().with_hooks(RuntimeHookConfig::new(
683 vec!["printf 'pre hook ran'".to_string()],
684 vec!["printf 'post hook ran'".to_string()],
685 ));
686 let mut runtime = ConversationRuntime::new_with_features(
687 Session::new(),
688 TwoCallApiClient { calls: 0 },
689 StaticToolExecutor::new().register("add", |_input| Ok("4".to_string())),
690 PermissionPolicy::new(PermissionMode::DangerFullAccess)
691 .with_tool_requirement("add", PermissionMode::WorkspaceWrite),
692 vec!["system".to_string()],
693 &hook_config,
694 );
695
696 let summary = runtime
697 .run_turn("use add", None)
698 .expect("tool loop succeeds");
699
700 assert_eq!(summary.tool_results.len(), 1);
701 let ContentBlock::ToolResult {
702 is_error, output, ..
703 } = &summary.tool_results[0].blocks[0]
704 else {
705 panic!("expected tool result block");
706 };
707 assert!(
708 !*is_error,
709 "post hook should preserve non-error result: {output:?}"
710 );
711 assert!(
712 output.contains('4'),
713 "tool output missing value: {output:?}"
714 );
715 assert!(
716 output.contains("pre hook ran"),
717 "tool output missing pre hook feedback: {output:?}"
718 );
719 assert!(
720 output.contains("post hook ran"),
721 "tool output missing post hook feedback: {output:?}"
722 );
723 }
724
725 #[test]
726 fn reconstructs_usage_tracker_from_restored_session() {
727 struct SimpleApi;
728 impl ApiClient for SimpleApi {
729 fn stream(
730 &mut self,
731 _request: ApiRequest,
732 ) -> Result<Vec<AssistantEvent>, RuntimeError> {
733 Ok(vec![
734 AssistantEvent::TextDelta("done".to_string()),
735 AssistantEvent::MessageStop,
736 ])
737 }
738 }
739
740 let mut session = Session::new();
741 session
742 .messages
743 .push(crate::session::ConversationMessage::assistant_with_usage(
744 vec![ContentBlock::Text {
745 text: "earlier".to_string(),
746 }],
747 Some(TokenUsage {
748 input_tokens: 11,
749 output_tokens: 7,
750 cache_creation_input_tokens: 2,
751 cache_read_input_tokens: 1,
752 }),
753 ));
754
755 let runtime = ConversationRuntime::new(
756 session,
757 SimpleApi,
758 StaticToolExecutor::new(),
759 PermissionPolicy::new(PermissionMode::DangerFullAccess),
760 vec!["system".to_string()],
761 );
762
763 assert_eq!(runtime.usage().turns(), 1);
764 assert_eq!(runtime.usage().cumulative_usage().total_tokens(), 21);
765 }
766
767 #[test]
768 fn compacts_session_after_turns() {
769 struct SimpleApi;
770 impl ApiClient for SimpleApi {
771 fn stream(
772 &mut self,
773 _request: ApiRequest,
774 ) -> Result<Vec<AssistantEvent>, RuntimeError> {
775 Ok(vec![
776 AssistantEvent::TextDelta("done".to_string()),
777 AssistantEvent::MessageStop,
778 ])
779 }
780 }
781
782 let mut runtime = ConversationRuntime::new(
783 Session::new(),
784 SimpleApi,
785 StaticToolExecutor::new(),
786 PermissionPolicy::new(PermissionMode::DangerFullAccess),
787 vec!["system".to_string()],
788 );
789 runtime.run_turn("a", None).expect("turn a");
790 runtime.run_turn("b", None).expect("turn b");
791 runtime.run_turn("c", None).expect("turn c");
792
793 let result = runtime.compact(CompactionConfig {
794 preserve_recent_messages: 2,
795 max_estimated_tokens: 1,
796 });
797 assert!(result.summary.contains("Conversation summary"));
798 assert_eq!(
799 result.compacted_session.messages[0].role,
800 MessageRole::System
801 );
802 }
803}