commit 16738f2279a05509e288219ec06bc253c26002b2
Author: Coder Agent <coder@agents.omni>
Date: Mon Apr 20 21:49:33 2026
fix(agent): persist oversized run_bash output to temp files
When run_bash output exceeds the inline limit, keep a bounded head+tail
preview in context and write the full output to a temp file.
The tool result now includes explicit 'Full output saved to:' and
tail/rg follow-up hints so agents can inspect details without blowing
context. Added a regression test that verifies the temp file path is
emitted for large output.
Task-Id: t-823
diff --git a/Omni/Agent/Tools.hs b/Omni/Agent/Tools.hs
index 42464a2b..5f09ed75 100644
--- a/Omni/Agent/Tools.hs
+++ b/Omni/Agent/Tools.hs
@@ -204,6 +204,29 @@ test =
toolResultSuccess tr Test.@=? True
("hello" `Text.isInfixOf` toolResultOutput tr) Test.@=? True
Aeson.Error e -> Test.assertFailure e,
+ Test.unit "runBashTool persists oversized output to temp file" <| do
+ let args =
+ Aeson.object
+ [ "command"
+ .= ("i=1; while [ $i -le 5000 ]; do echo line-$i; i=$((i+1)); done" :: Text)
+ ]
+ result <- Engine.toolExecute runBashTool args
+ case Aeson.fromJSON result of
+ Aeson.Success (tr :: ToolResult) -> do
+ toolResultSuccess tr Test.@=? True
+ ("[OUTPUT TRUNCATED" `Text.isInfixOf` toolResultOutput tr) Test.@=? True
+ let savedPaths =
+ [ Text.strip path
+ | line <- Text.lines (toolResultOutput tr),
+ Just path <- [Text.stripPrefix "Full output saved to: " line],
+ path /= "<failed>"
+ ]
+ case listToMaybe savedPaths of
+ Nothing -> Test.assertFailure "Expected full-output temp file path in run_bash output"
+ Just path -> do
+ exists <- Directory.doesFileExist (Text.unpack path)
+ exists Test.@=? True
+ Aeson.Error e -> Test.assertFailure e,
Test.unit "runBashTool validates cwd exists" <| do
let args =
Aeson.object
@@ -380,6 +403,13 @@ instance Aeson.FromJSON ToolResult where
maxOutputChars :: Int
maxOutputChars = 8000
+-- | For oversized bash output, keep both beginning and end visible.
+runBashPreviewHeadChars :: Int
+runBashPreviewHeadChars = 2800
+
+runBashPreviewTailChars :: Int
+runBashPreviewTailChars = 2800
+
-- | Maximum lines per read_file call
maxReadLinesPerCall :: Int
maxReadLinesPerCall = 400
@@ -398,6 +428,48 @@ truncateOutput output
<> tshow (Text.length output - maxOutputChars)
<> " chars omitted. Use line ranges or more specific searches.]"
+renderRunBashOutput :: Text -> IO Text
+renderRunBashOutput output
+ | Text.length output <= maxOutputChars = pure output
+ | otherwise = do
+ mPath <- persistRunBashOutput output
+ let headChunk = Text.take runBashPreviewHeadChars output
+ tailChunk = Text.takeEnd runBashPreviewTailChars output
+ omittedChars = max 0 (Text.length output - runBashPreviewHeadChars - runBashPreviewTailChars)
+ (pathLine, inspectLine) =
+ case mPath of
+ Nothing ->
+ ( "Full output saved to: <failed>",
+ "Unable to save full output to a temp file; rerun with narrower command output."
+ )
+ Just path ->
+ ( "Full output saved to: " <> path,
+ "Inspect with: tail -n 200 " <> path <> " | rg -n \"error|warn|fail\" " <> path
+ )
+ pure
+ <| Text.intercalate
+ "\n"
+ [ headChunk,
+ "",
+ "[OUTPUT TRUNCATED - " <> tshow omittedChars <> " chars omitted]",
+ pathLine,
+ inspectLine,
+ "",
+ "[... tail ...]",
+ tailChunk
+ ]
+
+persistRunBashOutput :: Text -> IO (Maybe Text)
+persistRunBashOutput output = do
+ tmpDir <- Directory.getTemporaryDirectory
+ let template = "omni-agent-run-bash-XXXXXX.log"
+ result <-
+ try @SomeException <| do
+ (path, h) <- IO.openTempFile tmpDir template
+ (TextIO.hPutStr h output >> IO.hFlush h) `finally` IO.hClose h
+ pure (Text.pack path)
+ pure <| either (const Nothing) Just result
+
mkSuccess :: Text -> Aeson.Value
mkSuccess output = Aeson.toJSON <| ToolResult True (truncateOutput output) Nothing
@@ -1042,7 +1114,7 @@ runBashTool :: Engine.Tool
runBashTool =
Engine.Tool
{ Engine.toolName = "run_bash",
- Engine.toolDescription = "Execute a shell command and return stdout/stderr.",
+ Engine.toolDescription = "Execute a shell command and return stdout/stderr. Large outputs are summarized with a temp file path you can inspect with tail/rg.",
Engine.toolJsonSchema =
Aeson.object
[ "type" .= ("object" :: Text),
@@ -1145,14 +1217,22 @@ executeRunBash v =
}
Just (exitCode, stdoutStr, stderrStr) -> do
let output = Text.pack stdoutStr <> Text.pack stderrStr
+ renderedOutput <- renderRunBashOutput output
case exitCode of
- Exit.ExitSuccess -> pure <| mkSuccess output
+ Exit.ExitSuccess ->
+ pure
+ <| Aeson.toJSON
+ <| ToolResult
+ { toolResultSuccess = True,
+ toolResultOutput = renderedOutput,
+ toolResultError = Nothing
+ }
Exit.ExitFailure code ->
pure
<| Aeson.toJSON
<| ToolResult
{ toolResultSuccess = False,
- toolResultOutput = truncateOutput output,
+ toolResultOutput = renderedOutput,
toolResultError = Just ("Exit code: " <> tshow code)
}
)