← Back to task

Commit 16738f22

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)
                             }
             )