thndrs_lib/core/harness/
mod.rs1use std::sync::mpsc::Receiver;
7
8use crate::agent::{RunHandle, ToolExecutionHook, ToolPermissionHook};
9use crate::app::AgentEvent;
10use crate::providers::ProviderMessage;
11use crate::tools::AgentRunConfig;
12
13use thndrs_agent::CancelToken;
14
15#[derive(Debug)]
17pub struct HarnessHandle {
18 pub events: Receiver<AgentEvent>,
20 pub cancel: CancelToken,
22}
23
24impl HarnessHandle {
25 pub fn request_cancel(&self) {
27 self.cancel.cancel();
28 }
29}
30
31#[derive(Debug)]
34pub struct HarnessTurn {
35 handle: RunHandle,
36}
37
38impl HarnessTurn {
39 pub fn new(handle: RunHandle) -> Self {
40 Self { handle }
41 }
42
43 pub fn provider_with_steering(
45 config: AgentRunConfig, messages: Vec<ProviderMessage>, expects_write: bool, steering: Receiver<String>,
46 ) -> Self {
47 Self::new(RunHandle::provider_with_steering(
48 config,
49 messages,
50 expects_write,
51 steering,
52 ))
53 }
54
55 pub fn provider_with_steering_and_permissions(
57 config: AgentRunConfig, messages: Vec<ProviderMessage>, expects_write: bool, steering: Receiver<String>,
58 permission_hook: ToolPermissionHook,
59 ) -> Self {
60 Self::new(
61 RunHandle::provider_with_steering(config, messages, expects_write, steering)
62 .with_permission_hook(permission_hook),
63 )
64 }
65
66 pub fn provider_with_steering_permissions_and_execution(
68 config: AgentRunConfig, messages: Vec<ProviderMessage>, expects_write: bool, steering: Receiver<String>,
69 permission_hook: ToolPermissionHook, execution_hook: ToolExecutionHook,
70 ) -> Self {
71 Self::new(
72 RunHandle::provider_with_steering(config, messages, expects_write, steering)
73 .with_permission_hook(permission_hook)
74 .with_execution_hook(execution_hook),
75 )
76 }
77
78 pub fn start(self) -> HarnessHandle {
80 let cancel = self.handle.cancel.clone();
81 let events = self.handle.spawn();
82 HarnessHandle { events, cancel }
83 }
84
85 #[cfg(test)]
87 pub fn fake(config: AgentRunConfig, prompt: String) -> Self {
88 Self::new(RunHandle::fake(config, prompt))
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use std::path::PathBuf;
95
96 use super::*;
97 use crate::cli::WebSearchMode;
98 use crate::tools::AgentRunConfig;
99
100 #[test]
101 fn fake_turn_starts_and_finishes_without_app() {
102 let config = AgentRunConfig::new(
103 PathBuf::from("."),
104 String::from("fake-agent"),
105 WebSearchMode::DuckDuckGo,
106 );
107 let handle = HarnessTurn::fake(config, String::new()).start();
108 let mut events = Vec::new();
109
110 while let Ok(event) = handle.events.recv() {
111 events.push(event);
112 }
113
114 assert_eq!(events.first(), Some(&AgentEvent::Started));
115 assert_eq!(events.last(), Some(&AgentEvent::Finished));
116 assert!(
117 events
118 .iter()
119 .any(|event| matches!(event, AgentEvent::ReasoningDelta(_)))
120 );
121 assert!(
122 events
123 .iter()
124 .any(|event| matches!(event, AgentEvent::AssistantDelta(_)))
125 );
126 assert!(
127 events
128 .iter()
129 .any(|event| matches!(event, AgentEvent::ToolStarted { .. }))
130 );
131 assert!(
132 events
133 .iter()
134 .any(|event| matches!(event, AgentEvent::ToolFinished { .. }))
135 );
136 }
137}