fix(rlm): decode stdout lossily

This commit is contained in:
Hunter Bown
2026-05-21 00:12:59 +08:00
parent ae1bb9dd95
commit 6821f294e2
+44 -18
View File
@@ -288,15 +288,12 @@ impl PythonRuntime {
async fn read_until_ready(&mut self, ready_sentinel: &str) -> Result<(), String> {
loop {
let mut line = String::new();
let n = self
.stdout
.read_line(&mut line)
.await
.map_err(|e| format!("stdout read: {e}"))?;
if n == 0 {
return Err("Python interpreter closed stdout before ready signal".to_string());
}
let line = match self.read_stdout_line_lossy().await? {
Some(line) => line,
None => {
return Err("Python interpreter closed stdout before ready signal".to_string());
}
};
let trimmed = line.trim_end_matches(['\n', '\r']);
if trimmed == ready_sentinel {
return Ok(());
@@ -305,6 +302,20 @@ impl PythonRuntime {
}
}
async fn read_stdout_line_lossy(&mut self) -> Result<Option<String>, String> {
let mut buf = Vec::new();
let n = self
.stdout
.read_until(b'\n', &mut buf)
.await
.map_err(|e| format!("stdout read: {e}"))?;
if n == 0 {
Ok(None)
} else {
Ok(Some(String::from_utf8_lossy(&buf).into_owned()))
}
}
/// Execute a Python code block with no RPC dispatcher. Used for inline
/// `repl` blocks where `llm_query()` should fall back to a sentinel.
pub async fn execute(&mut self, code: &str) -> Result<ReplRound, String> {
@@ -352,15 +363,12 @@ impl PythonRuntime {
let read_loop = async {
loop {
let mut line = String::new();
let n = self
.stdout
.read_line(&mut line)
.await
.map_err(|e| format!("stdout read: {e}"))?;
if n == 0 {
return Err("Python interpreter closed stdout mid-round".to_string());
}
let line = match self.read_stdout_line_lossy().await? {
Some(line) => line,
None => {
return Err("Python interpreter closed stdout mid-round".to_string());
}
};
let trimmed = line.trim_end_matches(['\n', '\r']);
if let Some(rest) = trimmed.strip_prefix(&done_prefix) {
@@ -1079,6 +1087,24 @@ mod tests {
rt.shutdown().await;
}
#[tokio::test]
async fn non_utf8_stdout_decodes_lossy_and_runtime_survives() {
let mut rt = PythonRuntime::new().await.expect("spawn");
let round = rt
.execute(
"import sys\n\
sys.stdout.buffer.write(b'bad:\\xff\\n')\n\
sys.stdout.buffer.flush()\n\
print('after invalid')",
)
.await
.expect("execute");
assert!(round.stdout.contains("bad:\u{fffd}"), "{}", round.stdout);
assert!(round.stdout.contains("after invalid"), "{}", round.stdout);
rt.shutdown().await;
}
#[tokio::test]
async fn variables_persist_across_rounds() {
let mut rt = PythonRuntime::new().await.expect("spawn");