1use async_trait::async_trait;
2use tokio::sync::mpsc;
3
4use super::event::{HarnessInternalEvent, NativeHarnessError, NativeTurnInput};
5use super::tools::{MockToolRuntime, ToolInvocation, ToolRuntime};
6
7#[async_trait]
8pub trait NativeHarness: Send + Sync {
9 async fn run_turn(
10 &self,
11 input: NativeTurnInput,
12 ) -> Result<mpsc::Receiver<Result<HarnessInternalEvent, NativeHarnessError>>, NativeHarnessError>;
13}
14
15#[derive(Debug, Clone)]
16pub struct FakeNativeHarness {
17 events: Vec<HarnessInternalEvent>,
18}
19
20impl FakeNativeHarness {
21 pub fn new(events: Vec<HarnessInternalEvent>) -> Self {
22 Self { events }
23 }
24}
25
26#[async_trait]
27impl NativeHarness for FakeNativeHarness {
28 async fn run_turn(
29 &self,
30 _input: NativeTurnInput,
31 ) -> Result<mpsc::Receiver<Result<HarnessInternalEvent, NativeHarnessError>>, NativeHarnessError>
32 {
33 let (tx, rx) = mpsc::channel(self.events.len().max(1));
34 for ev in self.events.clone() {
35 tx.send(Ok(ev))
36 .await
37 .map_err(|_| NativeHarnessError::ChannelClosed)?;
38 }
39 drop(tx);
40 Ok(rx)
41 }
42}
43
44pub struct ToolCapableFakeHarness<R = MockToolRuntime> {
45 runtime: R,
46}
47
48impl ToolCapableFakeHarness<MockToolRuntime> {
49 pub fn mock() -> Self {
50 Self {
51 runtime: MockToolRuntime::new().with_file("README.md", "mock readme"),
52 }
53 }
54}
55
56impl<R> ToolCapableFakeHarness<R> {
57 pub fn new(runtime: R) -> Self {
58 Self { runtime }
59 }
60}
61
62#[async_trait]
63impl<R> NativeHarness for ToolCapableFakeHarness<R>
64where
65 R: ToolRuntime + Send + Sync,
66{
67 async fn run_turn(
68 &self,
69 input: NativeTurnInput,
70 ) -> Result<mpsc::Receiver<Result<HarnessInternalEvent, NativeHarnessError>>, NativeHarnessError>
71 {
72 let (tx, rx) = mpsc::channel(8);
73 let tool_input = tool_invocation_from_prompt(&input.prompt_text);
74 let tool_name = tool_input.name.clone();
75 let tool_id = tool_input.id.clone();
76 let raw_input = tool_input.input.clone();
77 tx.send(Ok(HarnessInternalEvent::AssistantTextChunk {
78 msg_id: "msg_native_1".into(),
79 delta: format!("native harness executing tool: {tool_name}"),
80 }))
81 .await
82 .map_err(|_| NativeHarnessError::ChannelClosed)?;
83 tx.send(Ok(HarnessInternalEvent::ToolCall {
84 id: tool_id.clone(),
85 name: tool_name,
86 input: raw_input,
87 }))
88 .await
89 .map_err(|_| NativeHarnessError::ChannelClosed)?;
90
91 let outcome = self.runtime.invoke(tool_input).await.map_err(|e| {
92 NativeHarnessError::Failed(format!("tool runtime invocation failed: {e}"))
93 })?;
94 tx.send(Ok(HarnessInternalEvent::ToolResult {
95 id: tool_id,
96 output: outcome.output.map_err(|failure| failure.to_string()),
97 }))
98 .await
99 .map_err(|_| NativeHarnessError::ChannelClosed)?;
100 tx.send(Ok(HarnessInternalEvent::TurnEnd {
101 stop_reason: "end_turn".into(),
102 usage: None,
103 final_messages: vec![],
107 }))
108 .await
109 .map_err(|_| NativeHarnessError::ChannelClosed)?;
110 drop(tx);
111 Ok(rx)
112 }
113}
114
115fn tool_invocation_from_prompt(prompt: &str) -> ToolInvocation {
116 let trimmed = prompt.trim();
117 if let Some(path) = trimmed.strip_prefix("read ") {
118 ToolInvocation {
119 id: "tc_read_1".into(),
120 name: "read".into(),
121 input: serde_json::json!({"path": path.trim()}),
122 }
123 } else if let Some(rest) = trimmed.strip_prefix("write ") {
124 let (path, content) = rest.split_once(' ').unwrap_or((rest, ""));
125 ToolInvocation {
126 id: "tc_write_1".into(),
127 name: "write".into(),
128 input: serde_json::json!({"path": path.trim(), "content": content}),
129 }
130 } else {
131 ToolInvocation {
132 id: "tc_bash_1".into(),
133 name: "bash".into(),
134 input: serde_json::json!({"command": trimmed}),
135 }
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use crate::HarnessInternalEvent;
143
144 #[tokio::test]
145 async fn tool_capable_fake_harness_emits_tool_lifecycle() {
146 let harness = ToolCapableFakeHarness::mock();
147 let mut rx = harness
148 .run_turn(NativeTurnInput {
149 prompt_text: "read README.md".into(),
150 system_prompt: None,
151 attachments: vec![],
152 cancel_token: None,
153 prior_messages: vec![],
154 context_path: None,
155 })
156 .await
157 .unwrap();
158
159 assert!(matches!(
160 rx.recv().await.unwrap().unwrap(),
161 HarnessInternalEvent::AssistantTextChunk { .. }
162 ));
163 assert!(matches!(
164 rx.recv().await.unwrap().unwrap(),
165 HarnessInternalEvent::ToolCall { ref name, .. } if name == "read"
166 ));
167 assert!(matches!(
168 rx.recv().await.unwrap().unwrap(),
169 HarnessInternalEvent::ToolResult { .. }
170 ));
171 assert!(matches!(
172 rx.recv().await.unwrap().unwrap(),
173 HarnessInternalEvent::TurnEnd { .. }
174 ));
175 assert!(rx.recv().await.is_none());
176 }
177}