R/stdlib_agents.R

Defines functions create_skill_architect_agent create_env_agent create_file_agent create_data_agent

Documented in create_data_agent create_env_agent create_file_agent create_skill_architect_agent

#' @title Standard Agent Library: Built-in Specialist Agents
#' @description
#' Factory functions for creating standard library agents with scoped tools
#' and safety guardrails. These agents form the foundation of the multi-agent
#' orchestration system.
#' @name stdlib_agents
NULL

# =============================================================================
# DataAgent: Data Manipulation Specialist (dplyr/tidyr)
# =============================================================================

#' @title Create a DataAgent
#' @description
#' Creates an agent specialized in data manipulation using dplyr and tidyr.
#' The agent can filter, transform, summarize, and reshape data frames in
#' the session environment.
#'
#' @param name Agent name. Default "DataAgent".
#' @param safe_mode If TRUE (default), restricts operations to data manipulation only.
#' @return An Agent object configured for data manipulation.
#' @export
#' @examples
#' \donttest{
#' if (interactive()) {
#'   data_agent <- create_data_agent()
#'   session <- create_shared_session(model = "openai:gpt-4o")
#'   session$set_var("sales", data.frame(
#'     product = c("A", "B", "C"),
#'     revenue = c(100, 200, 150)
#'   ))
#'   result <- data_agent$run(
#'     "Calculate total revenue and find the top product",
#'     session = session,
#'     model = "openai:gpt-4o"
#'   )
#' }
#' }
create_data_agent <- function(name = "DataAgent", safe_mode = TRUE) {
  # Tool: Execute dplyr/tidyr operations
  transform_data_tool <- Tool$new(
    name = "transform_data",
    description = paste0(
      "Execute dplyr/tidyr data transformation on a data frame. ",
      "Supports: filter, select, mutate, arrange, group_by, summarize, ",
      "pivot_longer, pivot_wider, left_join, and more. ",
      "The result is stored back in the session."
    ),
    parameters = z_object(
      input_var = z_string("Name of the input data frame variable"),
      output_var = z_string("Name for the output variable (can be same as input)"),
      operations = z_string(paste0(
        "dplyr/tidyr pipeline code (without the data argument). ",
        "Example: 'filter(x > 10) %>% group_by(category) %>% summarize(total = sum(value))'"
      ))
    ),
    execute = function(args) {
      env <- args$.envir
      if (is.null(env)) {
        return("Error: No session environment available.")
      }

      input_var <- args$input_var
      output_var <- args$output_var
      operations <- args$operations

      # Check input exists
      if (!exists(input_var, envir = env, inherits = TRUE)) {
        return(paste0("Error: Variable '", input_var, "' not found in session."))
      }

      input_data <- get(input_var, envir = env, inherits = TRUE)
      if (!is.data.frame(input_data)) {
        return(paste0("Error: '", input_var, "' is not a data frame."))
      }

      # Check for required packages
      if (!requireNamespace("dplyr", quietly = TRUE)) {
        return("Error: dplyr package is not installed.")
      }

      # Safe mode checks
      if (safe_mode) {
        dangerous <- c(
          "system", "shell", "eval", "parse", "source",
          "write", "save", "download", "url", "file\\."
        )
        for (pattern in dangerous) {
          if (grepl(pattern, operations, ignore.case = TRUE)) {
            return(paste0("Error: Unsafe operation detected: ", pattern))
          }
        }
      }

      # Execute transformation
      result <- tryCatch(
        {
          # Build and execute the pipeline
          code <- paste0("input_data %>% ", operations)
          output <- eval(parse(text = code), envir = list(
            input_data = input_data,
            `%>%` = dplyr::`%>%`
          ), enclos = asNamespace("dplyr"))

          # Store result
          assign(output_var, output, envir = env)

          # Return summary
          paste0(
            "Transformation complete. '", output_var, "' now has ",
            nrow(output), " rows x ", ncol(output), " columns.\n",
            "Columns: ", paste(names(output), collapse = ", ")
          )
        },
        error = function(e) {
          paste0("Error in transformation: ", conditionMessage(e))
        }
      )

      result
    }
  )

  # Tool: Summarize data
  summarize_data_tool <- Tool$new(
    name = "summarize_data",
    description = paste0(
      "Get a statistical summary of a data frame. ",
      "Returns column types, missing values, and basic statistics."
    ),
    parameters = z_object(
      var_name = z_string("Name of the data frame variable to summarize"),
      columns = z_string("Comma-separated column names to focus on (optional, empty for all)")
    ),
    execute = function(args) {
      env <- args$.envir
      if (is.null(env)) {
        return("Error: No session environment available.")
      }

      var_name <- args$var_name
      columns <- args$columns

      if (!exists(var_name, envir = env, inherits = TRUE)) {
        return(paste0("Error: Variable '", var_name, "' not found."))
      }

      df <- get(var_name, envir = env, inherits = TRUE)
      if (!is.data.frame(df)) {
        return(paste0("Error: '", var_name, "' is not a data frame."))
      }

      # Filter columns if specified
      if (!is.null(columns) && nzchar(columns)) {
        cols <- trimws(strsplit(columns, ",")[[1]])
        cols <- cols[cols %in% names(df)]
        if (length(cols) > 0) {
          df <- df[, cols, drop = FALSE]
        }
      }

      # Build summary
      lines <- c(
        paste0("Data frame: ", var_name),
        paste0("Dimensions: ", nrow(df), " rows x ", ncol(df), " columns"),
        ""
      )

      for (col in names(df)) {
        col_data <- df[[col]]
        col_type <- class(col_data)[1]
        n_missing <- sum(is.na(col_data))

        if (is.numeric(col_data)) {
          stats <- sprintf(
            "  %s (%s): min=%.2f, max=%.2f, mean=%.2f, median=%.2f, NA=%d",
            col, col_type,
            min(col_data, na.rm = TRUE),
            max(col_data, na.rm = TRUE),
            mean(col_data, na.rm = TRUE),
            stats::median(col_data, na.rm = TRUE),
            n_missing
          )
        } else if (is.factor(col_data) || is.character(col_data)) {
          n_unique <- length(unique(col_data))
          top_values <- names(sort(table(col_data), decreasing = TRUE))[1:min(3, n_unique)]
          stats <- sprintf(
            "  %s (%s): %d unique values, top: %s, NA=%d",
            col, col_type, n_unique,
            paste(top_values, collapse = ", "),
            n_missing
          )
        } else {
          stats <- sprintf("  %s (%s): NA=%d", col, col_type, n_missing)
        }

        lines <- c(lines, stats)
      }

      paste(lines, collapse = "\n")
    }
  )

  # Tool: Join data frames
  join_data_tool <- Tool$new(
    name = "join_data",
    description = paste0(
      "Join two data frames together. ",
      "Supports left_join, right_join, inner_join, full_join."
    ),
    parameters = z_object(
      left_var = z_string("Name of the left data frame"),
      right_var = z_string("Name of the right data frame"),
      output_var = z_string("Name for the output variable"),
      join_type = z_enum(
        c("left", "right", "inner", "full"),
        description = "Type of join to perform"
      ),
      by_columns = z_string("Comma-separated column names to join by")
    ),
    execute = function(args) {
      env <- args$.envir
      if (is.null(env)) {
        return("Error: No session environment available.")
      }

      if (!requireNamespace("dplyr", quietly = TRUE)) {
        return("Error: dplyr package is not installed.")
      }

      left_var <- args$left_var
      right_var <- args$right_var
      output_var <- args$output_var
      join_type <- args$join_type
      by_columns <- trimws(strsplit(args$by_columns, ",")[[1]])

      # Get data frames
      if (!exists(left_var, envir = env, inherits = TRUE)) {
        return(paste0("Error: '", left_var, "' not found."))
      }
      if (!exists(right_var, envir = env, inherits = TRUE)) {
        return(paste0("Error: '", right_var, "' not found."))
      }

      left_df <- get(left_var, envir = env, inherits = TRUE)
      right_df <- get(right_var, envir = env, inherits = TRUE)

      # Perform join
      result <- tryCatch(
        {
          join_fn <- switch(join_type,
            "left" = dplyr::left_join,
            "right" = dplyr::right_join,
            "inner" = dplyr::inner_join,
            "full" = dplyr::full_join
          )

          output <- join_fn(left_df, right_df, by = by_columns)
          assign(output_var, output, envir = env)

          paste0(
            "Join complete. '", output_var, "' has ",
            nrow(output), " rows x ", ncol(output), " columns."
          )
        },
        error = function(e) {
          paste0("Error in join: ", conditionMessage(e))
        }
      )

      result
    }
  )

  # Tool: List available data
  list_data_tool <- Tool$new(
    name = "list_data",
    description = "List all data frames available in the session environment.",
    parameters = z_empty_object(description = "No parameters required"),
    execute = function(args) {
      env <- args$.envir
      if (is.null(env)) {
        return("Error: No session environment available.")
      }

      vars <- ls(env)
      parent <- parent.env(env)
      if (!identical(parent, emptyenv()) && !identical(parent, baseenv())) {
        vars <- unique(c(vars, ls(parent)))
      }

      if (length(vars) == 0) {
        return("No variables in session.")
      }

      data_frames <- character(0)
      for (v in vars) {
        obj <- get(v, envir = env, inherits = TRUE)
        if (is.data.frame(obj)) {
          data_frames <- c(data_frames, sprintf(
            "- %s: %d rows x %d cols (%s)",
            v, nrow(obj), ncol(obj),
            paste(names(obj)[1:min(5, ncol(obj))], collapse = ", ")
          ))
        }
      }

      if (length(data_frames) == 0) {
        return("No data frames found in session.")
      }

      paste(c("Available data frames:", data_frames), collapse = "\n")
    }
  )

  system_prompt <- paste0(
    "You are a data manipulation specialist using dplyr and tidyr.\n\n",
    "Your expertise includes:\n",
    "- Filtering and selecting data (filter, select, slice)\n",
    "- Transforming columns (mutate, transmute, across)\n",
    "- Grouping and summarizing (group_by, summarize, count)\n",
    "- Reshaping data (pivot_longer, pivot_wider)\n",
    "- Joining data frames (left_join, inner_join, etc.)\n",
    "- Handling missing values and data cleaning\n\n",
    "Guidelines:\n",
    "1. Always check available data first with list_data\n",
    "2. Understand the data structure with summarize_data before transforming\n",
    "3. Use meaningful output variable names\n",
    "4. Chain operations efficiently with %>%\n",
    "5. Explain what each transformation does\n",
    if (safe_mode) "6. You are in safe mode: only data manipulation is allowed\n" else ""
  )

  Agent$new(
    name = name,
    description = paste0(
      "Data manipulation specialist using dplyr/tidyr. Can filter, transform, ",
      "summarize, reshape, and join data frames. Expert in tidyverse workflows."
    ),
    system_prompt = system_prompt,
    tools = list(transform_data_tool, summarize_data_tool, join_data_tool, list_data_tool)
  )
}

# =============================================================================
# FileAgent: File System Specialist (fs/readr)
# =============================================================================

#' @title Create a FileAgent
#' @description
#' Creates an agent specialized in file system operations using fs and readr.
#' The agent can read, write, and manage files with safety guardrails.
#'
#' @param name Agent name. Default "FileAgent".
#' @param allowed_dirs Character vector of allowed directories. Default current dir.
#' @param allowed_extensions Character vector of allowed file extensions.
#' @return An Agent object configured for file operations.
#' @export
#' @examples
#' \donttest{
#' if (interactive()) {
#'   file_agent <- create_file_agent(
#'     allowed_dirs = c("./data", "./output"),
#'     allowed_extensions = c("csv", "json", "txt", "rds")
#'   )
#'   result <- file_agent$run(
#'     "Read the sales.csv file and store it as 'sales_data'",
#'     session = session,
#'     model = "openai:gpt-4o"
#'   )
#' }
#' }
create_file_agent <- function(name = "FileAgent",
                              allowed_dirs = ".",
                              allowed_extensions = c(
                                "csv", "tsv", "txt", "json",
                                "rds", "rda", "xlsx", "xls"
                              )) {
  # Helper to check path safety
  check_path_safety <- function(path, allowed_dirs) {
    # Normalize path
    norm_path <- normalizePath(path, mustWork = FALSE)
    norm_dirs <- sapply(allowed_dirs, function(d) {
      normalizePath(d, mustWork = FALSE)
    })

    # Check if path is within allowed directories
    for (dir in norm_dirs) {
      if (startsWith(norm_path, dir)) {
        return(list(safe = TRUE, reason = NULL))
      }
    }

    list(
      safe = FALSE,
      reason = paste0(
        "Path '", path, "' is outside allowed directories: ",
        paste(allowed_dirs, collapse = ", ")
      )
    )
  }

  # Helper to check extension
  check_extension <- function(path, allowed_extensions) {
    ext <- tools::file_ext(path)
    if (tolower(ext) %in% tolower(allowed_extensions)) {
      return(list(safe = TRUE, reason = NULL))
    }
    list(
      safe = FALSE,
      reason = paste0(
        "Extension '.", ext, "' not allowed. Allowed: ",
        paste0(".", allowed_extensions, collapse = ", ")
      )
    )
  }

  # Tool: Read file
  read_file_tool <- Tool$new(
    name = "read_file",
    description = paste0(
      "Read a file into the session environment. ",
      "Supports CSV, TSV, JSON, RDS, and Excel files. ",
      "Automatically detects format from extension."
    ),
    parameters = z_object(
      path = z_string("Path to the file to read"),
      var_name = z_string("Name for the variable in session"),
      options = z_string("Optional: comma-separated read options (e.g., 'skip=1,na=NA')")
    ),
    execute = function(args) {
      env <- args$.envir
      if (is.null(env)) {
        return("Error: No session environment available.")
      }

      path <- args$path
      var_name <- args$var_name

      # Safety checks
      path_check <- check_path_safety(path, allowed_dirs)
      if (!path_check$safe) {
        return(paste0("Error: ", path_check$reason))
      }

      ext_check <- check_extension(path, allowed_extensions)
      if (!ext_check$safe) {
        return(paste0("Error: ", ext_check$reason))
      }

      if (!file.exists(path)) {
        return(paste0("Error: File '", path, "' does not exist."))
      }

      # Read based on extension
      ext <- tolower(tools::file_ext(path))
      result <- tryCatch(
        {
          data <- switch(ext,
            "csv" = {
              if (requireNamespace("readr", quietly = TRUE)) {
                readr::read_csv(path, show_col_types = FALSE)
              } else {
                utils::read.csv(path, stringsAsFactors = FALSE)
              }
            },
            "tsv" = {
              if (requireNamespace("readr", quietly = TRUE)) {
                readr::read_tsv(path, show_col_types = FALSE)
              } else {
                utils::read.delim(path, stringsAsFactors = FALSE)
              }
            },
            "txt" = readLines(path),
            "json" = jsonlite::fromJSON(path),
            "rds" = readRDS(path),
            "rda" = {
              load(path, envir = env)
              return(paste0("Loaded objects from '", path, "' into session."))
            },
            "xlsx" = ,
            "xls" = {
              if (!requireNamespace("readxl", quietly = TRUE)) {
                return("Error: readxl package required for Excel files.")
              }
              readxl::read_excel(path)
            },
            return(paste0("Error: Unsupported file type: .", ext))
          )

          assign(var_name, data, envir = env)

          if (is.data.frame(data)) {
            paste0(
              "Read '", path, "' into '", var_name, "': ",
              nrow(data), " rows x ", ncol(data), " columns"
            )
          } else {
            paste0("Read '", path, "' into '", var_name, "'")
          }
        },
        error = function(e) {
          paste0("Error reading file: ", conditionMessage(e))
        }
      )

      result
    }
  )

  # Tool: Write file
  write_file_tool <- Tool$new(
    name = "write_file",
    description = paste0(
      "Write data from session to a file. ",
      "Supports CSV, TSV, JSON, and RDS formats."
    ),
    parameters = z_object(
      var_name = z_string("Name of the variable to write"),
      path = z_string("Path for the output file"),
      format = z_enum(
        c("csv", "tsv", "json", "rds"),
        description = "Output format (auto-detected from extension if not specified)"
      )
    ),
    execute = function(args) {
      env <- args$.envir
      if (is.null(env)) {
        return("Error: No session environment available.")
      }

      var_name <- args$var_name
      path <- args$path
      format <- args$format

      # Safety checks
      path_check <- check_path_safety(path, allowed_dirs)
      if (!path_check$safe) {
        return(paste0("Error: ", path_check$reason))
      }

      if (!exists(var_name, envir = env, inherits = TRUE)) {
        return(paste0("Error: Variable '", var_name, "' not found."))
      }

      data <- get(var_name, envir = env, inherits = TRUE)

      # Auto-detect format from extension if not specified
      if (is.null(format) || format == "") {
        format <- tolower(tools::file_ext(path))
      }

      result <- tryCatch(
        {
          switch(format,
            "csv" = {
              if (requireNamespace("readr", quietly = TRUE)) {
                readr::write_csv(data, path)
              } else {
                utils::write.csv(data, path, row.names = FALSE)
              }
            },
            "tsv" = {
              if (requireNamespace("readr", quietly = TRUE)) {
                readr::write_tsv(data, path)
              } else {
                utils::write.table(data, path, sep = "\t", row.names = FALSE)
              }
            },
            "json" = jsonlite::write_json(data, path, auto_unbox = TRUE, pretty = TRUE),
            "rds" = saveRDS(data, path),
            return(paste0("Error: Unsupported format: ", format))
          )

          paste0("Wrote '", var_name, "' to '", path, "'")
        },
        error = function(e) {
          paste0("Error writing file: ", conditionMessage(e))
        }
      )

      result
    }
  )

  # Tool: List files
  list_files_tool <- Tool$new(
    name = "list_files",
    description = "List files in a directory with optional pattern filtering.",
    parameters = z_object(
      path = z_string("Directory path to list"),
      pattern = z_string("Optional: regex pattern to filter files"),
      recursive = z_boolean("Search subdirectories? Default FALSE")
    ),
    execute = function(args) {
      path <- args$path %||% "."
      pattern <- args$pattern
      recursive <- args$recursive %||% FALSE

      # Safety check
      path_check <- check_path_safety(path, allowed_dirs)
      if (!path_check$safe) {
        return(paste0("Error: ", path_check$reason))
      }

      if (!dir.exists(path)) {
        return(paste0("Error: Directory '", path, "' does not exist."))
      }

      files <- list.files(path,
        pattern = pattern, recursive = recursive,
        full.names = TRUE
      )

      if (length(files) == 0) {
        return("No files found matching criteria.")
      }

      # Get file info
      info <- file.info(files)
      summaries <- mapply(function(f, size, mtime) {
        sprintf(
          "- %s (%.1f KB, %s)",
          basename(f),
          size / 1024,
          format(mtime, "%Y-%m-%d %H:%M")
        )
      }, files, info$size, info$mtime, SIMPLIFY = TRUE)

      paste(c(paste0("Files in '", path, "':"), summaries), collapse = "\n")
    }
  )

  # Tool: File info
  file_info_tool <- Tool$new(
    name = "file_info",
    description = "Get detailed information about a file.",
    parameters = z_object(
      path = z_string("Path to the file")
    ),
    execute = function(args) {
      path <- args$path

      path_check <- check_path_safety(path, allowed_dirs)
      if (!path_check$safe) {
        return(paste0("Error: ", path_check$reason))
      }

      if (!file.exists(path)) {
        return(paste0("Error: File '", path, "' does not exist."))
      }

      info <- file.info(path)
      paste0(
        "File: ", basename(path), "\n",
        "Path: ", normalizePath(path), "\n",
        "Size: ", format(info$size, big.mark = ","), " bytes\n",
        "Modified: ", format(info$mtime, "%Y-%m-%d %H:%M:%S"), "\n",
        "Type: ", if (info$isdir) "directory" else tools::file_ext(path)
      )
    }
  )

  system_prompt <- paste0(
    "You are a file system specialist for data files.\n\n",
    "Your capabilities:\n",
    "- Reading various file formats (CSV, TSV, JSON, RDS, Excel)\n",
    "- Writing data to files in different formats\n",
    "- Listing and exploring directory contents\n",
    "- Getting file metadata and information\n\n",
    "Safety constraints:\n",
    "- Allowed directories: ", paste(allowed_dirs, collapse = ", "), "\n",
    "- Allowed extensions: ", paste0(".", allowed_extensions, collapse = ", "), "\n\n",
    "Guidelines:\n",
    "1. Always verify file paths before operations\n",
    "2. Use appropriate formats for the data type\n",
    "3. Report file sizes and row counts after reading\n",
    "4. Suggest appropriate variable names"
  )

  Agent$new(
    name = name,
    description = paste0(
      "File system specialist for data files. Can read CSV, JSON, Excel, RDS files ",
      "and write data to various formats. Operates within allowed directories only."
    ),
    system_prompt = system_prompt,
    tools = list(read_file_tool, write_file_tool, list_files_tool, file_info_tool)
  )
}

# =============================================================================
# EnvAgent: Environment/Package Management Specialist
# =============================================================================

#' @title Create an EnvAgent
#' @description
#' Creates an agent specialized in R environment and package management.
#' The agent can check, install, and manage R packages with safety controls.
#'
#' @param name Agent name. Default "EnvAgent".
#' @param allow_install Allow package installation. Default FALSE.
#' @param allowed_repos CRAN mirror URLs for installation.
#' @return An Agent object configured for environment management.
#' @export
#' @examples
#' \donttest{
#' if (interactive()) {
#'   env_agent <- create_env_agent(allow_install = TRUE)
#'   result <- env_agent$run(
#'     "Check if tidyverse is installed and load it",
#'     session = session,
#'     model = "openai:gpt-4o"
#'   )
#' }
#' }
create_env_agent <- function(name = "EnvAgent",
                             allow_install = FALSE,
                             allowed_repos = "https://cloud.r-project.org") {
  # Tool: Check package
  check_package_tool <- Tool$new(
    name = "check_package",
    description = "Check if an R package is installed and get its version.",
    parameters = z_object(
      package = z_string("Name of the package to check")
    ),
    execute = function(args) {
      pkg <- args$package

      if (requireNamespace(pkg, quietly = TRUE)) {
        version <- as.character(utils::packageVersion(pkg))
        paste0("Package '", pkg, "' is installed (version ", version, ")")
      } else {
        paste0("Package '", pkg, "' is NOT installed")
      }
    }
  )

  # Tool: Load package
  load_package_tool <- Tool$new(
    name = "load_package",
    description = "Load an R package into the session.",
    parameters = z_object(
      package = z_string("Name of the package to load"),
      quietly = z_boolean("Suppress loading messages? Default TRUE")
    ),
    execute = function(args) {
      pkg <- args$package
      quietly <- args$quietly %||% TRUE

      result <- tryCatch(
        {
          if (quietly) {
            suppressPackageStartupMessages(library(pkg, character.only = TRUE))
          } else {
            library(pkg, character.only = TRUE)
          }
          paste0("Package '", pkg, "' loaded successfully")
        },
        error = function(e) {
          paste0("Error loading '", pkg, "': ", conditionMessage(e))
        }
      )

      result
    }
  )

  # Tool: Install package (conditional)
  install_package_tool <- Tool$new(
    name = "install_package",
    description = if (allow_install) {
      "Install an R package from CRAN."
    } else {
      "Package installation is disabled. Contact administrator to enable."
    },
    parameters = z_object(
      package = z_string("Name of the package to install"),
      dependencies = z_boolean("Install dependencies? Default TRUE")
    ),
    execute = function(args) {
      if (!allow_install) {
        return("Error: Package installation is disabled for safety.")
      }

      pkg <- args$package
      deps <- args$dependencies %||% TRUE

      result <- tryCatch(
        {
          utils::install.packages(
            pkg,
            repos = allowed_repos,
            dependencies = deps,
            quiet = TRUE
          )

          if (requireNamespace(pkg, quietly = TRUE)) {
            paste0("Package '", pkg, "' installed successfully")
          } else {
            paste0("Installation completed but package '", pkg, "' not found")
          }
        },
        error = function(e) {
          paste0("Error installing '", pkg, "': ", conditionMessage(e))
        }
      )

      result
    }
  )

  # Tool: List loaded packages
  list_packages_tool <- Tool$new(
    name = "list_packages",
    description = "List currently loaded packages or search installed packages.",
    parameters = z_object(
      type = z_enum(
        c("loaded", "installed", "search"),
        description = "Type of listing: loaded (attached), installed (all), search (by pattern)"
      ),
      pattern = z_string("Search pattern (for type='search')")
    ),
    execute = function(args) {
      type <- args$type %||% "loaded"
      pattern <- args$pattern

      switch(type,
        "loaded" = {
          pkgs <- search()
          pkgs <- pkgs[grepl("^package:", pkgs)]
          pkgs <- sub("^package:", "", pkgs)
          paste(c("Loaded packages:", paste0("- ", pkgs)), collapse = "\n")
        },
        "installed" = {
          pkgs <- utils::installed.packages()[, "Package"]
          paste0(
            "Installed packages: ", length(pkgs), " total\n",
            "First 20: ", paste(head(pkgs, 20), collapse = ", ")
          )
        },
        "search" = {
          if (is.null(pattern) || !nzchar(pattern)) {
            return("Error: Pattern required for search")
          }
          pkgs <- utils::installed.packages()[, "Package"]
          matches <- pkgs[grepl(pattern, pkgs, ignore.case = TRUE)]
          if (length(matches) == 0) {
            paste0("No packages matching '", pattern, "'")
          } else {
            paste(c(
              paste0("Packages matching '", pattern, "':"),
              paste0("- ", matches)
            ), collapse = "\n")
          }
        }
      )
    }
  )

  # Tool: Session info
  session_info_tool <- Tool$new(
    name = "session_info",
    description = "Get R session information including version and platform.",
    parameters = z_empty_object(description = "No parameters required"),
    execute = function(args) {
      info <- utils::sessionInfo()

      paste0(
        "R Version: ", info$R.version$version.string, "\n",
        "Platform: ", info$platform, "\n",
        "Running under: ", info$running, "\n",
        "Locale: ", paste(info$locale, collapse = ", "), "\n",
        "Base packages: ", paste(info$basePkgs, collapse = ", ")
      )
    }
  )

  # Tool: Check dependencies
  check_deps_tool <- Tool$new(
    name = "check_dependencies",
    description = "Check if all dependencies for a package are available.",
    parameters = z_object(
      package = z_string("Name of the package to check dependencies for")
    ),
    execute = function(args) {
      pkg <- args$package

      if (!requireNamespace(pkg, quietly = TRUE)) {
        return(paste0("Package '", pkg, "' is not installed."))
      }

      # Get dependencies
      deps <- tools::package_dependencies(pkg, recursive = TRUE)[[1]]

      if (is.null(deps) || length(deps) == 0) {
        return(paste0("Package '", pkg, "' has no dependencies."))
      }

      # Check each dependency
      missing <- character(0)
      for (dep in deps) {
        if (!requireNamespace(dep, quietly = TRUE)) {
          missing <- c(missing, dep)
        }
      }

      if (length(missing) == 0) {
        paste0("All ", length(deps), " dependencies for '", pkg, "' are installed.")
      } else {
        paste0(
          "Missing dependencies for '", pkg, "':\n",
          paste0("- ", missing, collapse = "\n")
        )
      }
    }
  )

  system_prompt <- paste0(
    "You are an R environment and package management specialist.\n\n",
    "Your capabilities:\n",
    "- Checking package installation status and versions\n",
    "- Loading packages into the session\n",
    if (allow_install) "- Installing packages from CRAN\n" else "",
    "- Listing loaded and installed packages\n",
    "- Checking package dependencies\n",
    "- Providing session information\n\n",
    "Guidelines:\n",
    "1. Always check if a package is installed before trying to load it\n",
    "2. Suggest alternatives if a package is not available\n",
    "3. Check dependencies before complex operations\n",
    "4. Report version information when relevant\n",
    if (!allow_install) "5. Package installation is DISABLED - suggest manual installation\n" else ""
  )

  Agent$new(
    name = name,
    description = paste0(
      "R environment and package management specialist. Can check, load, ",
      if (allow_install) "and install " else "",
      "packages, verify dependencies, and provide session information."
    ),
    system_prompt = system_prompt,
    tools = list(
      check_package_tool,
      load_package_tool,
      install_package_tool,
      list_packages_tool,
      session_info_tool,
      check_deps_tool
    )
  )
}

# Null-coalescing operator (if not already defined)
if (!exists("%||%")) {
  `%||%` <- function(x, y) if (is.null(x)) y else x
}
# =============================================================================
# SkillArchitect: Advanced Skill Creator Specialist
# =============================================================================

#' @title Create a SkillArchitect Agent
#' @description
#' Creates an advanced agent specialized in creating, testing, and refining new skills.
#' It follows a rigorous "Ingest -> Design -> Implement -> Verify" workflow.
#'
#' @param name Agent name. Default "SkillArchitect".
#' @param registry Optional SkillRegistry object (defaults to creating one from inst/skills).
#' @param model The model object to use for verification (spawning a tester agent).
#' @return An Agent object configured for skill architecture.
#' @export
create_skill_architect_agent <- function(name = "SkillArchitect", registry = NULL, model = NULL) {
  # Initialize registry if not provided
  if (is.null(registry)) {
    registry <- create_auto_skill_registry(project_dir = getwd(), recursive = TRUE)
  }

  # Ensure the skill-creator skill is available
  if (is.null(registry$get_skill("skill-creator"))) {
    rlang::abort("The 'skill-creator' skill is required but not found in the registry.")
  }

  # Get standard skill creation tools
  skill_tools <- create_skill_tools(registry)

  # Get Skill Forge tools (Analysis & Verification)
  # Requires model for the verification loop
  forge_tools <- list()
  if (!is.null(model)) {
    # Skill-forge authoring/verification tools live in the optional companion
    # package aisdk.skills; continue without them when it is not installed.
    if (.companion_pkg_available("skills")) {
      forge_fn <- .companion_pkg_get("skills", "create_skill_forge_tools")
      forge_tools <- forge_fn(registry, model)
    } else {
      rlang::warn(paste0(
        "Skill-forge verification tools require the '",
        .companion_pkg_name("skills"),
        "' package (", .companion_install_hint("skills"),
        "); continuing without them."
      ))
    }
  } else {
    rlang::warn("No 'model' provided to SkillArchitect. Verification tools will be disabled.")
  }

  all_tools <- c(skill_tools, forge_tools)

  system_prompt <- paste0(
    "You are the Skill Architect, a senior AI engineer responsible for extending this system's capabilities.\n",
    "Your mission is to build robust, reusable 'Skills' that enable other agents to perform complex tasks.\n\n",
    "## Core Principles\n",
    "1. **Conciseness**: Don't maximize token usage. Be efficient.\n",
    "2. **Progressive Disclosure**: Detailed docs go into 'references/', core workflow in 'SKILL.md'.\n",
    "3. **Verification**: Never assume it works. Always VERIFY with a test run.\n\n",
    "## The Workflow\n",
    "1. **Ingest**: Learn the domain. Use `analyze_r_package` if wrapping a package.\n",
    "2. **Design**: Plan the skill structure. What inputs? What outputs? Draft the `SKILL.md` in your mind.\n",
    "3. **Implement**: Use the 'skill-creator' workflow and tools:\n",
    "   - `create_structure.R`\n",
    "   - `write_skill_md.R`\n",
    "   - `write_script.R`\n",
    "4. **Verify**: Use `verify_skill` to spawn a tester. If it fails, iterate and fix.\n\n",
    "## Skill Structure\n",
    "- `SKILL.md`: YAML metadata + High-level instructions (Imperative mood).\n",
    "- `scripts/`: Self-contained R functions. Use `args` list for inputs.\n",
    "- `references/`: (Optional) Large documentation or tables.\n\n",
    "You have access to powerful tools. Use them wisely to build the next generation of capabilities."
  )

  Agent$new(
    name = name,
    description = "Senior Skill Architect. Designs, implements, and verifies new system capabilities.",
    system_prompt = system_prompt,
    tools = all_tools
  )
}

Try the aisdk package in your browser

Any scripts or data that you put into this service are public.

aisdk documentation built on May 29, 2026, 9:07 a.m.