Reduce Gemini-CLI STDOUT noise (#551)

This commit is contained in:
Solomon
2025-08-21 16:10:26 +01:00
committed by GitHub
parent ed594a3d80
commit ac69b20bba
2 changed files with 65 additions and 0 deletions

View File

@@ -162,6 +162,12 @@ impl StandardCodingAgentExecutor for Gemini {
.format_chunk(Box::new(|partial_line: Option<&str>, chunk: String| {
Self::format_stdout_chunk(&chunk, partial_line.unwrap_or(""))
}))
// Gemini CLI sometimes prints a non-conversational noise
.transform_lines({
Box::new(move |lines: &mut Vec<String>| {
lines.retain(|line| line != "Data collection is disabled.\n");
})
})
.index_provider(entry_index_counter)
.build();

View File

@@ -134,6 +134,16 @@ impl PlainTextBuffer {
&self.lines
}
/// Mutably view lines for in-place transformations.
pub fn lines_mut(&mut self) -> &mut Vec<String> {
&mut self.lines
}
/// Recompute cached total length from current lines.
pub fn recompute_len(&mut self) {
self.total_len = self.lines.iter().map(|s| s.len()).sum();
}
/// Get the current parial line.
pub fn partial_line(&self) -> Option<&str> {
if let Some(last) = self.lines.last()
@@ -167,6 +177,9 @@ pub type MessageBoundaryPredicateFn =
/// Function to create a `NormalizedEntry` from content.
pub type NormalizedEntryProducerFn = Box<dyn Fn(String) -> NormalizedEntry + Send + 'static>;
/// Optional function to transform buffered lines in-place before boundary checks.
pub type LinesTransformFn = Box<dyn FnMut(&mut Vec<String>) + Send + 'static>;
/// High-level plain text log processor with configurable formatting and splitting
pub struct PlainTextLogProcessor {
buffer: PlainTextBuffer,
@@ -174,6 +187,7 @@ pub struct PlainTextLogProcessor {
entry_size_threshold: Option<usize>,
time_gap: Option<Duration>,
format_chunk: Option<FormatChunkFn>,
transform_lines: Option<LinesTransformFn>,
message_boundary_predicate: Option<MessageBoundaryPredicateFn>,
normalized_entry_producer: NormalizedEntryProducerFn,
last_chunk_arrival_time: Instant, // time since last chunk arrived
@@ -217,6 +231,15 @@ impl PlainTextLogProcessor {
// Let the buffer handle text buffering
self.buffer.ingest(formatted_chunk);
if let Some(transform_lines) = self.transform_lines.as_mut() {
transform_lines(self.buffer.lines_mut());
self.buffer.recompute_len();
if self.buffer.is_empty() {
// Nothing left to process after transformation
return vec![];
}
}
let mut patches = Vec::new();
// Check if we have a custom message boundary predicate
@@ -313,6 +336,7 @@ impl PlainTextLogProcessor {
size_threshold: Option<usize>,
time_gap: Option<Duration>,
format_chunk: Option<Box<dyn Fn(Option<&str>, String) -> String + 'static + Send>>,
transform_lines: Option<Box<dyn FnMut(&mut Vec<String>) + 'static + Send>>,
message_boundary_predicate: Option<
Box<dyn Fn(&[String]) -> Option<MessageBoundary> + 'static + Send>,
>,
@@ -330,6 +354,8 @@ impl PlainTextLogProcessor {
format_chunk: format_chunk.map(|f| {
Box::new(f) as Box<dyn Fn(Option<&str>, String) -> String + Send + 'static>
}),
transform_lines: transform_lines
.map(|f| Box::new(f) as Box<dyn FnMut(&mut Vec<String>) + Send + 'static>),
message_boundary_predicate: message_boundary_predicate.map(|p| {
Box::new(p) as Box<dyn Fn(&[String]) -> Option<MessageBoundary> + Send + 'static>
}),
@@ -435,4 +461,37 @@ mod tests {
let patches = processor.process("TOOL: file_read\n".to_string());
assert_eq!(patches.len(), 1);
}
#[test]
fn test_processor_transform_lines_clears_first_line() {
let producer = |content: String| -> NormalizedEntry {
NormalizedEntry {
timestamp: None,
entry_type: NormalizedEntryType::SystemMessage,
content,
metadata: None,
}
};
let mut processor = PlainTextLogProcessor::builder()
.normalized_entry_producer(producer)
.transform_lines(Box::new(|lines: &mut Vec<String>| {
// Drop a specific leading banner line if present
if !lines.is_empty()
&& lines.first().map(|s| s.as_str()) == Some("BANNER LINE TO DROP\n")
{
lines.remove(0);
}
}))
.index_provider(EntryIndexProvider::test_new())
.build();
// Provide a single-line chunk. The transform removes it, leaving nothing to emit.
let patches = processor.process("BANNER LINE TO DROP\n".to_string());
assert_eq!(patches.len(), 0);
// Next, add actual content; should emit one patch with the content
let patches = processor.process("real content\n".to_string());
assert_eq!(patches.len(), 1);
}
}