use std::io::{BufRead, BufReader, Read, Write};
use std::net::TcpStream;
use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
use sharpebench_protocol::{Decision, MarketObservation};
use crate::agent::Agent;
const MAX_AGENT_RESPONSE: u64 = 8 * 1024 * 1024;
pub struct ExternalAgent {
child: Child,
stdin: ChildStdin,
stdout: BufReader<ChildStdout>,
}
impl ExternalAgent {
pub fn spawn(program: &str, args: &[&str]) -> std::io::Result<Self> {
let mut child = Command::new(program)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
let stdin = child
.stdin
.take()
.ok_or_else(|| std::io::Error::other("no stdin"))?;
let stdout = child
.stdout
.take()
.ok_or_else(|| std::io::Error::other("no stdout"))?;
Ok(Self {
child,
stdin,
stdout: BufReader::new(stdout),
})
}
fn hold() -> Decision {
Decision {
orders: Vec::new(),
reasoning: "external agent error → hold".to_string(),
}
}
}
impl Agent for ExternalAgent {
fn decide(&mut self, obs: &MarketObservation) -> Decision {
let Ok(line) = serde_json::to_string(obs) else {
return Self::hold();
};
if writeln!(self.stdin, "{line}").is_err() || self.stdin.flush().is_err() {
return Self::hold();
}
let mut resp = String::new();
match self.stdout.read_line(&mut resp) {
Ok(0) | Err(_) => Self::hold(),
Ok(_) => serde_json::from_str(&resp).unwrap_or_else(|_| Self::hold()),
}
}
}
impl Drop for ExternalAgent {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
pub struct HttpAgent {
host: String,
port: u16,
}
impl HttpAgent {
pub fn new(addr: impl Into<String>) -> Self {
let addr = addr.into();
match addr.rsplit_once(':') {
Some((h, p)) => Self {
host: h.to_string(),
port: p.parse().unwrap_or(80),
},
None => Self {
host: addr,
port: 80,
},
}
}
fn decide_checked(&self, obs: &MarketObservation) -> std::io::Result<Decision> {
let body = serde_json::to_string(obs).map_err(std::io::Error::other)?;
let mut stream = TcpStream::connect((self.host.as_str(), self.port))?;
let timeout = std::time::Duration::from_secs(30);
stream.set_read_timeout(Some(timeout))?;
stream.set_write_timeout(Some(timeout))?;
let req = format!(
"POST /decide HTTP/1.1\r\nHost: {}\r\nContent-Type: application/json\r\n\
Content-Length: {}\r\nConnection: close\r\n\r\n{}",
self.host,
body.len(),
body
);
stream.write_all(req.as_bytes())?;
stream.flush()?;
let mut raw = String::new();
(&stream)
.take(MAX_AGENT_RESPONSE)
.read_to_string(&mut raw)?;
let json = raw
.split_once("\r\n\r\n")
.map(|(_, b)| b)
.ok_or_else(|| std::io::Error::other("malformed HTTP response"))?;
serde_json::from_str(json).map_err(std::io::Error::other)
}
}
impl Agent for HttpAgent {
fn decide(&mut self, obs: &MarketObservation) -> Decision {
self.decide_checked(obs).unwrap_or_else(|_| Decision {
orders: Vec::new(),
reasoning: "http agent error → hold".to_string(),
})
}
}