1use std::path::PathBuf;
22use std::sync::{mpsc::sync_channel, Arc, Mutex};
23
24use harness::{
25 run_events_from_parsed, spawn_streaming, CredentialSpec, Harness, HarnessCapabilities,
26 HarnessError, HarnessInfo, HarnessReadiness, InstallCallback, ParsedLine, ProcessEvent,
27 RunCallback, RunEvent, RunHandle, RunMode, RunRequest, RunTuning, Registry, SessionInfo,
28};
29use serde_json::Value;
30
31const ECHO_ID: &str = "echo";
32
33struct EchoHarness;
36
37impl Harness for EchoHarness {
38 fn info(&self) -> HarnessInfo {
39 HarnessInfo {
40 id: ECHO_ID.to_owned(),
41 display_name: "Echo".to_owned(),
42 description: "A toy harness that echoes the prompt — a template for your own."
43 .to_owned(),
44 requires_install: false,
45 capabilities: HarnessCapabilities {
46 credential_required: false,
47 previews_edits: false,
48 models: Vec::new(),
49 allows_custom_model: false,
50 supports_effort: false,
51 supports_max_turns: false,
52 supports_login: false,
53 },
54 }
55 }
56
57 fn readiness(&self) -> HarnessReadiness {
58 HarnessReadiness {
59 harness_id: ECHO_ID.to_owned(),
60 ready: true,
61 installed: true,
62 version: Some("0.0.0".to_owned()),
63 auth_configured: true,
64 error: None,
65 details: Value::Null,
66 }
67 }
68
69 fn install(&self, _on_event: InstallCallback) -> Result<(), HarnessError> {
70 Ok(()) }
72
73 fn credential(&self) -> CredentialSpec {
74 CredentialSpec {
75 label: "none".to_owned(),
76 keychain_service: String::new(),
77 keychain_account: String::new(),
78 required: false,
79 }
80 }
81
82 fn run(&self, request: RunRequest, on_event: RunCallback) -> Result<RunHandle, HarnessError> {
83 let answer = format!(r#"{{"text":"echo: {}"}}"#, request.prompt.replace('"', "'"));
86 let args = vec![
87 "%s\n".to_owned(),
88 r#"{"type":"init","model":"echo-1"}"#.to_owned(),
89 answer,
90 ];
91 let cwd = request
92 .cwd
93 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
94
95 let parser = Arc::new(Mutex::new(EchoParser::default()));
99 let handle = spawn_streaming(
100 PathBuf::from("printf"),
101 args,
102 Vec::new(),
103 cwd,
104 request.run_id,
105 move |event| {
106 let mut parser = parser.lock().expect("echo parser mutex");
107 for ev in parser.on_process_event(event) {
108 (*on_event)(ev);
109 }
110 },
111 )
112 .map_err(HarnessError::spawn)?;
113 Ok(Box::new(handle))
114 }
115}
116
117#[derive(Default)]
122struct EchoParser {
123 announced: bool,
124}
125
126impl EchoParser {
127 fn on_process_event(&mut self, event: ProcessEvent) -> Vec<RunEvent> {
128 match event {
129 ProcessEvent::Started { run_id } => vec![RunEvent::Started { run_id }],
130 ProcessEvent::Exited {
131 run_id,
132 exit_code,
133 cancelled,
134 } => vec![RunEvent::Exited {
135 run_id,
136 exit_code,
137 cancelled,
138 }],
139 ProcessEvent::Error { run_id, message } => vec![RunEvent::Error { run_id, message }],
140 ProcessEvent::Stderr { .. } => Vec::new(),
141 ProcessEvent::Stdout { run_id, line } => {
142 run_events_from_parsed(&run_id, self.parse_line(&line))
143 }
144 _ => Vec::new(),
146 }
147 }
148
149 fn parse_line(&mut self, line: &str) -> ParsedLine {
150 let value = serde_json::from_str::<Value>(line.trim()).unwrap_or(Value::Null);
151 if !self.announced {
153 if let Some(model) = value.get("model").and_then(Value::as_str) {
154 self.announced = true;
155 return ParsedLine {
156 session: Some(SessionInfo {
157 session_id: None,
158 model: Some(model.to_owned()),
159 }),
160 ..ParsedLine::default()
161 };
162 }
163 }
164 if let Some(text) = value.get("text").and_then(Value::as_str) {
165 return ParsedLine {
166 text: Some(text.to_owned()),
167 ..ParsedLine::default()
168 };
169 }
170 ParsedLine::default()
171 }
172}
173
174fn main() -> Result<(), String> {
175 let reg = Registry::new().register(EchoHarness);
177 let h = reg.by_id(ECHO_ID).expect("registered");
178 println!("harness: {} — {}", h.info().display_name, h.info().description);
179
180 let (tx, rx) = sync_channel::<RunEvent>(64);
181 let on_event: RunCallback = Arc::new(move |ev| {
182 let _ = tx.send(ev);
183 });
184 let _handle = h.run(
185 RunRequest {
186 run_id: "demo".into(),
187 prompt: "hello".into(),
188 cwd: None,
189 mode: RunMode::Ask,
190 tuning: RunTuning::default(),
191 },
192 on_event,
193 )
194 .map_err(|e| e.to_string())?;
195
196 for ev in rx {
198 match ev {
199 RunEvent::Session { model, .. } => println!("[session] model={model:?}"),
200 RunEvent::Text { delta, .. } => println!("[answer] {delta}"),
201 RunEvent::Exited { exit_code, .. } => {
202 println!("[exited] {exit_code:?}");
203 break;
204 }
205 other => println!("{other:?}"),
206 }
207 }
208 Ok(())
209}