packages feed

seihou-cli-0.6.0.0: src-exe/Seihou/CLI/PromptRun.hs

module Seihou.CLI.PromptRun
  ( handlePromptRun,
  )
where

import Data.Generics.Labels ()
import Data.Map.Strict qualified as Map
import Data.Maybe (fromMaybe)
import Data.Set qualified as Set
import Data.Text qualified as T
import Seihou.CLI.AgentConfig
  ( PendingAgentConfig,
    agentLaunchDeclaration,
    resolveDeclaredAgentConfig,
  )
import Seihou.CLI.AgentLaunch (gatherAgentContext, setupAllowedTools)
import Seihou.CLI.AgentRun (runRenderedAgentPrompt)
import Seihou.CLI.Commands (PromptRunOpts (..))
import Seihou.CLI.PromptRender (renderPromptBody, renderPromptSystemPrompt)
import Seihou.CLI.Shared
  ( deriveNamespace,
    formatVarError,
    logIO,
    toVarNameMap,
    unwrapConfig,
  )
import Seihou.Composition.Instance (primaryInstance)
import Seihou.Composition.Resolve (resolveWithPrompts)
import Seihou.Core.AgentPrompt (validateAgentPrompt)
import Seihou.Core.CommandVar (resolveCommandVars)
import Seihou.Core.Context (resolveContext)
import Seihou.Core.Module (defaultSearchPaths, discoverRunnable)
import Seihou.Core.Types
import Seihou.Effect.ConfigReader
  ( readContextConfig,
    readGlobalConfig,
    readLocalConfig,
    readNamespaceConfig,
  )
import Seihou.Effect.ConfigReaderInterp (runConfigReader)
import Seihou.Effect.ConsoleInterp (runConsole)
import Seihou.Effect.Logger (logError)
import Seihou.Effect.ProcessInterp (runProcessIO)
import Seihou.Prelude
import System.Environment (getEnvironment)
import System.Exit (exitFailure)

handlePromptRun :: PendingAgentConfig -> PromptRunOpts -> IO ()
handlePromptRun pending opts = do
  let level = if opts ^. #verbose then LogVerbose else LogNormal

  searchPaths <- defaultSearchPaths
  runnableResult <- discoverRunnable searchPaths (opts ^. #name)
  (prompt, promptDir) <- case runnableResult of
    Right (RunnableAgentPrompt p dir) -> pure (p, dir)
    Right (RunnableModule _ _) ->
      exitErr level $
        "'"
          <> opts ^. #name . #unModuleName
          <> "' is a module, not a prompt. Did you mean 'seihou run "
          <> opts ^. #name . #unModuleName
          <> "'?"
    Right (RunnableRecipe _ _) ->
      exitErr level $
        "'"
          <> opts ^. #name . #unModuleName
          <> "' is a recipe, not a prompt. Did you mean 'seihou run "
          <> opts ^. #name . #unModuleName
          <> "'?"
    Right (RunnableBlueprint _ _) ->
      exitErr level $
        "'"
          <> opts ^. #name . #unModuleName
          <> "' is a blueprint, not a prompt. Did you mean 'seihou agent run "
          <> opts ^. #name . #unModuleName
          <> "'?"
    Left err -> exitErr level (renderModuleLoadError err)

  validation <- validateAgentPrompt promptDir prompt
  case validation of
    Left err -> exitErr level (renderModuleLoadError err)
    Right _ -> pure ()

  -- Finish provider/model/effort resolution now that the prompt is loaded, so
  -- an unusable declaration is reported alongside the prompt's other errors.
  modelConfig <-
    resolveDeclaredAgentConfig
      level
      ("prompt '" <> prompt ^. #name . #unModuleName <> "'")
      pending
      (agentLaunchDeclaration (prompt ^. #launch))

  let placeholderModule =
        Module
          { name = prompt ^. #name,
            version = prompt ^. #version,
            description = prompt ^. #description,
            vars = relaxCommandVarDecls (prompt ^. #commandVars) (prompt ^. #vars),
            exports = [],
            prompts = prompt ^. #prompts,
            steps = [],
            commands = [],
            dependencies = [],
            removal = Nothing,
            migrations = []
          }
      placeholderInst = primaryInstance (prompt ^. #name)
      placeholderTriple = (placeholderInst, placeholderModule, promptDir)

  envPairs <- getEnvironment
  let cliOverrides = Map.fromList [(VarName k, v) | (k, v) <- opts ^. #vars]
      envVars = Map.fromList [(T.pack k, T.pack v) | (k, v) <- envPairs]
      namespace = fromMaybe (deriveNamespace (prompt ^. #name)) (opts ^. #namespace)
  context <- resolveContext (opts ^. #context) envVars
  let contextName = fromMaybe "" context

  resolveResult <- runEff $ runConfigReader $ runConsole $ do
    localCfg <- readLocalConfig >>= unwrapConfig level
    nsCfg <- readNamespaceConfig namespace >>= unwrapConfig level
    ctxCfg <- readContextConfig contextName >>= unwrapConfig level
    gCfg <- readGlobalConfig >>= unwrapConfig level
    resolveWithPrompts
      [placeholderTriple]
      cliOverrides
      envVars
      namespace
      contextName
      (toVarNameMap localCfg)
      (toVarNameMap nsCfg)
      (toVarNameMap ctxCfg)
      (toVarNameMap gCfg)

  resolvedNormal <- case resolveResult of
    Left errs -> do
      logIO level $ do
        logError "Error resolving prompt variables:"
        mapM_ (logError . ("  " <>) . formatVarError) errs
      exitFailure
    Right r -> pure (Map.findWithDefault Map.empty placeholderInst r)

  commandResult <- runEff $ runProcessIO $ resolveCommandVars (prompt ^. #vars) (prompt ^. #commandVars) resolvedNormal
  resolved <- case commandResult of
    Left errs -> do
      logIO level $ do
        logError "Error resolving command variables:"
        mapM_ (logError . ("  " <>) . formatVarError) errs
      exitFailure
    Right r -> pure r

  let renderedPrompt = renderPromptBody resolved (prompt ^. #prompt)
  ctx <- gatherAgentContext
  let systemPrompt = renderPromptSystemPrompt ctx prompt resolved renderedPrompt (opts ^. #prompt)

  _ <-
    runRenderedAgentPrompt
      (opts ^. #debug)
      modelConfig
      setupAllowedTools
      Nothing
      systemPrompt
      (opts ^. #prompt)
  pure ()

relaxCommandVarDecls :: [CommandVar] -> [VarDecl] -> [VarDecl]
relaxCommandVarDecls commandVars =
  map relaxOne
  where
    commandNames = Set.fromList (map (^. #name) commandVars)
    relaxOne decl
      | Set.member (decl ^. #name) commandNames = decl & #required .~ False
      | otherwise = decl

exitErr :: LogLevel -> Text -> IO a
exitErr level msg = do
  logIO level (logError msg)
  exitFailure

renderModuleLoadError :: ModuleLoadError -> Text
renderModuleLoadError = \case
  ModuleNotFound name searched ->
    "Prompt '"
      <> name ^. #unModuleName
      <> "' not found. Searched in:\n"
      <> T.intercalate "\n" (map (("  " <>) . T.pack) searched)
  DhallEvalError name msg ->
    "Failed to evaluate '" <> name ^. #unModuleName <> "': " <> msg
  DhallDecodeError name msg ->
    "Failed to decode '" <> name ^. #unModuleName <> "': " <> msg
  ValidationError name msgs ->
    "Validation failed for '"
      <> name ^. #unModuleName
      <> "':\n"
      <> T.intercalate "\n" (map ("  " <>) msgs)
  CircularDependency names ->
    "Circular dependency detected: "
      <> T.intercalate " -> " (map (^. #unModuleName) names)
  MissingSourceFile name path ->
    "Missing source file in '"
      <> name ^. #unModuleName
      <> "': "
      <> T.pack path
  RegistryEvalError path msg ->
    "Failed to evaluate registry at '" <> path <> "': " <> msg