From c593c7cc81cc8aefd7b4e3c648abb9d224f8dba9 Mon Sep 17 00:00:00 2001 From: Stathis Gennatas Date: Tue, 28 Apr 2026 18:36:30 -0700 Subject: [PATCH 01/10] validate logfile on generate --- r/R/07_Agent.R | 1 + 1 file changed, 1 insertion(+) diff --git a/r/R/07_Agent.R b/r/R/07_Agent.R index 6208dbc..5e3e729 100644 --- a/r/R/07_Agent.R +++ b/r/R/07_Agent.R @@ -511,6 +511,7 @@ method(generate, Agent) <- function( } # Resolve logfile: per-call arg > agent field logfile <- logfile %||% x@logfile + .check_scalar_character(logfile, "logfile") # Check input check_inherits(prompt, "character") update_state <- x@use_memory && commit_to_memory From f52977909836c7f8bbcc3de41d3af2f0454c11dc Mon Sep 17 00:00:00 2001 From: Stathis Gennatas Date: Tue, 28 Apr 2026 18:38:23 -0700 Subject: [PATCH 02/10] validate that logfile everywhere --- r/R/07_Agent.R | 1 + 1 file changed, 1 insertion(+) diff --git a/r/R/07_Agent.R b/r/R/07_Agent.R index 5e3e729..b383ac7 100644 --- a/r/R/07_Agent.R +++ b/r/R/07_Agent.R @@ -78,6 +78,7 @@ Agent <- new_class( tempfile("rtemis_security_log_", fileext = ".jsonl") ) } + .check_scalar_character(logfile, "logfile") new_object( S7_object(), llmconfig = llmconfig, From a39ed7067149e37ea0c953cf667d0baa4d298d26 Mon Sep 17 00:00:00 2001 From: Stathis Gennatas Date: Tue, 28 Apr 2026 20:08:44 -0700 Subject: [PATCH 03/10] added check_optional_scalar_character --- r/R/00_init.R | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/r/R/00_init.R b/r/R/00_init.R index 75d8991..2c761e8 100644 --- a/r/R/00_init.R +++ b/r/R/00_init.R @@ -364,7 +364,7 @@ AIThinking <- new_class( ) # %% utils ---- -# %% .check_scalar_character() ---- +# %% check_scalar_character() ---- #' Check Scalar Character #' #' @param x Object: Object to check. @@ -375,7 +375,7 @@ AIThinking <- new_class( #' @author EDG #' @keywords internal #' @noRd -.check_scalar_character <- function(x, name) { +check_scalar_character <- function(x, name) { if ( !is.character(x) || length(x) != 1L || @@ -388,6 +388,38 @@ AIThinking <- new_class( } +# %% check_optional_scalar_character ---- +#' Check Optional Scalar Character +#' +#' @param x Optional Character: Value to check. +#' @param arg_name Character: Argument name to use in error messages. +#' +#' @return Called for side effects. +#' +#' @author EDG +#' @keywords internal +#' @noRd +#' +#' @examples +#' check_optional_scalar_character(NULL, "my_arg") # Passes +#' check_optional_scalar_character("hello", "my_arg") # Passes +#' # Throw error: +#' try(check_optional_scalar_character(c("hello", "world"), "my_arg")) +#' try(check_optional_scalar_character(123, "my_arg")) +check_optional_scalar_character <- function( + x, + arg_name = deparse(substitute(x)) +) { + check_character(x, allow_null = TRUE, arg_name = arg_name) + if (!is.null(x) && length(x) != 1L) { + cli::cli_abort( + "{.var {arg_name}} must be NULL or a single string." + ) + } + invisible() +} + + # %% .is_named_list() ---- #' Test Named List #' @@ -416,7 +448,7 @@ AIThinking <- new_class( #' @keywords internal #' @noRd .clean_base_url <- function(x) { - .check_scalar_character(x, "base_url") + check_scalar_character(x, "base_url") sub("/+$", "", trimws(x)) } From 9cd926e424ac93f05727ab491b95794a7fcce054 Mon Sep 17 00:00:00 2001 From: Stathis Gennatas Date: Tue, 28 Apr 2026 20:10:14 -0700 Subject: [PATCH 04/10] => check_scalar_character Co-authored-by: Copilot --- r/R/04_Tool.R | 4 ++-- r/R/05_LLMConfig.R | 35 +++++++++++++++++++---------------- r/R/07_Agent.R | 14 ++++++-------- r/R/utils_security.R | 2 +- 4 files changed, 28 insertions(+), 27 deletions(-) diff --git a/r/R/04_Tool.R b/r/R/04_Tool.R index eecbf34..59fba62 100644 --- a/r/R/04_Tool.R +++ b/r/R/04_Tool.R @@ -20,11 +20,11 @@ ToolParameter <- new_class( required = class_logical ), validator = function(self) { - .check_scalar_character(self@name, "name") + check_scalar_character(self@name, "name") if (!nzchar(self@name)) { cli::cli_abort("ToolParameter name cannot be empty.") } - .check_scalar_character(self@description, "description") + check_scalar_character(self@description, "description") check_enum(self@type, .SCHEMA_FIELD_TYPES, arg_name = "type") check_scalar_logical(self@required, "required") NULL diff --git a/r/R/05_LLMConfig.R b/r/R/05_LLMConfig.R index ac0fd3e..9e08830 100644 --- a/r/R/05_LLMConfig.R +++ b/r/R/05_LLMConfig.R @@ -1,6 +1,9 @@ -# References: -# Ollama API: https://docs.ollama.com/ -# Ollama tool calling: https://docs.ollama.com/capabilities/tool-calling +# Ollama References: +# Chat endpoint: https://docs.ollama.com/api/chat +# Thinking: https://docs.ollama.com/capabilities/thinking#enable-thinking-in-api-calls +# Tool calling: https://docs.ollama.com/capabilities/tool-calling#tool-calling +# OpenAI API: https://developers.openai.com/api/reference/overview +# Anthropic API: https://platform.claude.com/docs/en/api/getting-started # %% Constants ---- TEMPERATURE_DEFAULT <- 0.3 @@ -177,20 +180,20 @@ OpenAIConfig <- new_class( enable_thinking = NULL, validate_model = FALSE ) { - .check_scalar_character(model_name, "model_name") - .check_scalar_character(base_url, "base_url") + check_scalar_character(model_name, "model_name") + check_scalar_character(base_url, "base_url") if (!is.null(api_key)) { - .check_scalar_character(api_key, "api_key") + check_scalar_character(api_key, "api_key") } - .check_scalar_character(api_key_env, "api_key_env") + check_scalar_character(api_key_env, "api_key_env") if (!is.null(keychain_service)) { - .check_scalar_character(keychain_service, "keychain_service") + check_scalar_character(keychain_service, "keychain_service") } if (!is.null(organization)) { - .check_scalar_character(organization, "organization") + check_scalar_character(organization, "organization") } if (!is.null(project)) { - .check_scalar_character(project, "project") + check_scalar_character(project, "project") } if (length(timeout) != 1L || is.na(timeout) || timeout <= 0) { cli::cli_abort("{.var timeout} must be a positive numeric scalar.") @@ -288,16 +291,16 @@ AnthropicConfig <- new_class( thinking_budget_tokens = NULL, validate_model = FALSE ) { - .check_scalar_character(model_name, "model_name") - .check_scalar_character(base_url, "base_url") + check_scalar_character(model_name, "model_name") + check_scalar_character(base_url, "base_url") if (!is.null(api_key)) { - .check_scalar_character(api_key, "api_key") + check_scalar_character(api_key, "api_key") } - .check_scalar_character(api_key_env, "api_key_env") + check_scalar_character(api_key_env, "api_key_env") if (!is.null(keychain_service)) { - .check_scalar_character(keychain_service, "keychain_service") + check_scalar_character(keychain_service, "keychain_service") } - .check_scalar_character(anthropic_version, "anthropic_version") + check_scalar_character(anthropic_version, "anthropic_version") if (!is.null(anthropic_beta)) { if ( !is.character(anthropic_beta) || diff --git a/r/R/07_Agent.R b/r/R/07_Agent.R index b383ac7..e536d8b 100644 --- a/r/R/07_Agent.R +++ b/r/R/07_Agent.R @@ -1,11 +1,6 @@ # Agent.R # ::rtemis.llm:: -# 2025 EDG rtemis.org - -# References: -# Chat endpoint: https://docs.ollama.com/api/chat -# Tool calling: https://docs.ollama.com/capabilities/tool-calling#tool-calling -# Thinking: https://docs.ollama.com/capabilities/thinking#enable-thinking-in-api-calls +# 2025- EDG rtemis.org # --- Internal API --------------------------------------------------------------------------------- # %% Agent ---- @@ -78,7 +73,7 @@ Agent <- new_class( tempfile("rtemis_security_log_", fileext = ".jsonl") ) } - .check_scalar_character(logfile, "logfile") + check_scalar_character(logfile, "logfile") new_object( S7_object(), llmconfig = llmconfig, @@ -433,6 +428,9 @@ create_agent <- function( logfile = NULL, verbosity = 1L ) { + check_optional_scalar_character(system_prompt, "system_prompt") + check_scalar_logical(use_memory, "use_memory") + check_optional_scalar_character(logfile, "logfile") agent <- Agent( llmconfig = llmconfig, system_prompt = system_prompt, @@ -512,7 +510,7 @@ method(generate, Agent) <- function( } # Resolve logfile: per-call arg > agent field logfile <- logfile %||% x@logfile - .check_scalar_character(logfile, "logfile") + check_scalar_character(logfile, "logfile") # Check input check_inherits(prompt, "character") update_state <- x@use_memory && commit_to_memory diff --git a/r/R/utils_security.R b/r/R/utils_security.R index 1597595..d6c931d 100644 --- a/r/R/utils_security.R +++ b/r/R/utils_security.R @@ -74,7 +74,7 @@ report_agent_unauthorized_tool <- function( tool_requested, logfile ) { - .check_scalar_character(logfile, "logfile") + check_scalar_character(logfile, "logfile") log_entry <- list( timestamp = Sys.time(), agent_name = agent@name, From e1b67ba4cca0a497646521042cc2e7c3f5c1dff8 Mon Sep 17 00:00:00 2001 From: Stathis Gennatas Date: Wed, 29 Apr 2026 12:45:11 -0700 Subject: [PATCH 05/10] add S7 properties and checks Co-authored-by: Copilot --- r/R/00_init.R | 370 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 314 insertions(+), 56 deletions(-) diff --git a/r/R/00_init.R b/r/R/00_init.R index 2c761e8..5d9de69 100644 --- a/r/R/00_init.R +++ b/r/R/00_init.R @@ -1,3 +1,313 @@ +# %% --- S7 Properties ----------------------------------------------------------------------------- + +# %% Logical ---- +#' Logical scalar S7 property +#' +#' S7 property accepting a single non-NA logical value. +#' +#' @return An S7 property object. +#' @author EDG +#' @noRd +logical_scalar <- new_property( + class_logical, + validator = function(value) { + if (length(value) != 1L || is.na(value)) { + return("must be a logical scalar (TRUE or FALSE)") + } + NULL + } +) + + +# %% Character ---- +#' Non-empty character scalar S7 property +#' +#' S7 property accepting a single non-NA, non-empty (after trimming whitespace) string. +#' +#' @return An S7 property object. +#' @author EDG +#' @noRd +character_scalar <- new_property( + class_character, + validator = function(value) { + if (length(value) != 1L || is.na(value) || !nzchar(trimws(value))) { + return("must be a non-empty character scalar") + } + NULL + } +) + + +#' Optional non-empty character scalar S7 property +#' +#' S7 property accepting `NULL` or a single non-NA, non-empty (after trimming whitespace) string. +#' +#' @return An S7 property object. +#' @author EDG +#' @noRd +optional_character_scalar <- new_property( + class = new_union(class_character, NULL), + default = NULL, + validator = function(value) { + if ( + !is.null(value) && + (length(value) != 1L || is.na(value) || !nzchar(trimws(value))) + ) { + return("must be NULL or a non-empty character scalar") + } + NULL + } +) + + +#' Positive integer scalar S7 property +#' +#' S7 property accepting a single non-NA integer value strictly greater than zero (e.g. `1L`). +#' +#' @return An S7 property object. +#' @author EDG +#' @noRd +pos_integer_scalar <- new_property( + class_integer, + validator = function(value) { + if (length(value) != 1L || is.na(value) || value <= 0L) { + return("must be a positive integer scalar (> 0, e.g. 1L)") + } + NULL + } +) + + +# %% Bounded double scalars ---- +#' Probability scalar S7 property +#' +#' S7 property accepting a single finite double in \eqn{[0, 1]}. +#' +#' @return An S7 property object. +#' @author EDG +#' @noRd +prob_scalar <- new_property( + class_double, + validator = function(value) { + if (length(value) != 1L || is.na(value) || value < 0 || value > 1) { + return("must be a finite double in [0, 1]") + } + NULL + } +) + + +# %% Factory ---- +#' Create a bounded double S7 property +#' +#' Returns a `new_property()` for a double scalar constrained to a given interval. +#' Useful for bounds not covered by the pre-built properties. +#' +#' @param lower Numeric scalar. Lower bound. Default `-Inf`. +#' @param upper Numeric scalar. Upper bound. Default `Inf`. +#' @param lower_open Logical scalar. If `TRUE`, lower bound is exclusive `(lower, ...]`. +#' Default `FALSE`. +#' @param upper_open Logical scalar. If `TRUE`, upper bound is exclusive `[..., upper)`. +#' Default `FALSE`. +#' @param nullable Logical scalar. If `TRUE`, `NULL` is also accepted. Default `FALSE`. +#' +#' @return An S7 property object. +#' @author EDG +#' @export +#' +#' @examples +#' # Learning rate in (0, 1] +#' lr_prop <- bounded_double_property(0, 1, lower_open = TRUE) +bounded_double_property <- function( + lower = -Inf, + upper = Inf, + lower_open = FALSE, + upper_open = FALSE, + nullable = FALSE +) { + lower_sym <- if (lower_open) "(" else "[" + upper_sym <- if (upper_open) ")" else "]" + bound_desc <- paste0( + "must be a finite double in ", + lower_sym, + lower, + ", ", + upper, + upper_sym + ) + + check_lower <- if (lower_open) { + function(v) v > lower + } else { + function(v) v >= lower + } + check_upper <- if (upper_open) { + function(v) v < upper + } else { + function(v) v <= upper + } + + cls <- if (nullable) new_union(class_double, NULL) else class_double + + new_property( + class = cls, + validator = function(value) { + if (is.null(value)) { + return(NULL) + } + if (length(value) != 1L || is.na(value) || !is.finite(value)) { + return(paste0(bound_desc, " (must be a finite scalar)")) + } + if (!check_lower(value) || !check_upper(value)) { + return(bound_desc) + } + NULL + } + ) +} + + +# %% --- Checks ------------------------------------------------------------------------------------ +# %% check_scalar_character() ---- +#' Check Scalar Character +#' +#' @param x Object: Object to check. +#' @param name Character: Argument name to report. +#' +#' @return NULL, invisibly. +#' +#' @author EDG +#' @keywords internal +#' @noRd +check_scalar_character <- function(x, name) { + if ( + !is.character(x) || + length(x) != 1L || + is.na(x) || + !nzchar(trimws(x)) + ) { + cli::cli_abort("{.var {name}} must be a non-empty character scalar.") + } + invisible(NULL) +} + + +# %% check_optional_scalar_character ---- +#' Check Optional Scalar Character +#' +#' @param x Optional Character: Value to check. +#' @param arg_name Character: Argument name to use in error messages. +#' +#' @return Called for side effects. +#' +#' @author EDG +#' @keywords internal +#' @noRd +#' +#' @examples +#' check_optional_scalar_character(NULL, "my_arg") # Passes +#' check_optional_scalar_character("hello", "my_arg") # Passes +#' # Throw error: +#' try(check_optional_scalar_character(c("hello", "world"), "my_arg")) +#' try(check_optional_scalar_character(123, "my_arg")) +check_optional_scalar_character <- function( + x, + arg_name = deparse(substitute(x)) +) { + check_character(x, allow_null = TRUE, arg_name = arg_name) + if (!is.null(x) && length(x) != 1L) { + cli::cli_abort( + "{.var {arg_name}} must be NULL or a single string." + ) + } + invisible() +} + + +# %% check_double_scalar ---- +#' Check double scalar +#' +#' @param x Numeric: Value to check. Must be a single non-NA number (integer inputs are accepted). +#' @param arg_name Character: Argument name to use in error messages. +#' +#' @return Called for side effects. Throws an error if checks fail. +#' +#' @author EDG +#' @export +#' +#' @examples +#' check_double_scalar(3.14) +#' check_double_scalar(1L) +#' # Throw error: +#' try(check_double_scalar(NA_real_)) +#' try(check_double_scalar(c(1.0, 2.0))) +check_double_scalar <- function(x, arg_name = deparse(substitute(x))) { + if (!is.numeric(x)) { + cli::cli_abort("{.var {arg_name}} must be numeric.") + } + if (length(x) != 1L || is.na(x)) { + cli::cli_abort("{.var {arg_name}} must be a single non-NA number.") + } + invisible() +} # /rtemis.core::check_double_scalar + + +# %% check_optional_pos_double_scalar ---- +#' Check optional positive double scalar +#' +#' @param x Optional Numeric: Value to check. Must be `NULL` or a single finite number +#' strictly greater than zero. +#' @param arg_name Character: Argument name to use in error messages. +#' +#' @return Called for side effects. Throws an error if checks fail. +#' +#' @author EDG +#' @noRd +#' +#' @examples +#' check_optional_pos_double_scalar(NULL) +#' check_optional_pos_double_scalar(2.5) +#' # Throw error: +#' try(check_optional_pos_double_scalar(0)) +check_optional_pos_double_scalar <- function( + x, + arg_name = deparse(substitute(x)) +) { + if (is.null(x)) { + return(invisible()) + } + check_pos_double_scalar(x, arg_name = arg_name) + invisible() +} # /rtemis.core::check_optional_pos_double_scalar + + +# %% check_pos_double_scalar ---- +#' Check positive double scalar +#' +#' @param x Numeric: Value to check. Must be a single finite number strictly greater than zero. +#' @param arg_name Character: Argument name to use in error messages. +#' +#' @return Called for side effects. Throws an error if checks fail. +#' +#' @author EDG +#' @export +#' +#' @examples +#' check_pos_double_scalar(0.001) +#' check_pos_double_scalar(100) +#' # Throw error: +#' try(check_pos_double_scalar(0)) +#' try(check_pos_double_scalar(-1)) +#' try(check_pos_double_scalar(Inf)) +check_pos_double_scalar <- function(x, arg_name = deparse(substitute(x))) { + check_double_scalar(x, arg_name = arg_name) + if (!is.finite(x) || x <= 0) { + cli::cli_abort("{.var {arg_name}} must be a finite number greater than 0.") + } + invisible() +} # /rtemis.core::check_pos_double_scalar + + # --- Generics ------------------------------------------------------------------------------------- # %% get_model_name ---- @@ -337,6 +647,8 @@ build_tool_message <- new_generic("build_tool_message", "x") build_response_format <- new_generic("build_response_format", "x") +# %% --- Classes ----------------------------------------------------------------------------------- + # %% AIThinking Class ---- #' @title AIThinking Class #' @@ -351,7 +663,7 @@ build_response_format <- new_generic("build_response_format", "x") AIThinking <- new_class( "AIThinking", properties = list( - content = class_character, + content = character_scalar, metadata = class_list ), constructor = function(content, metadata = list()) { @@ -363,62 +675,8 @@ AIThinking <- new_class( } ) -# %% utils ---- -# %% check_scalar_character() ---- -#' Check Scalar Character -#' -#' @param x Object: Object to check. -#' @param name Character: Argument name to report. -#' -#' @return NULL, invisibly. -#' -#' @author EDG -#' @keywords internal -#' @noRd -check_scalar_character <- function(x, name) { - if ( - !is.character(x) || - length(x) != 1L || - is.na(x) || - !nzchar(trimws(x)) - ) { - cli::cli_abort("{.var {name}} must be a non-empty character scalar.") - } - invisible(NULL) -} - - -# %% check_optional_scalar_character ---- -#' Check Optional Scalar Character -#' -#' @param x Optional Character: Value to check. -#' @param arg_name Character: Argument name to use in error messages. -#' -#' @return Called for side effects. -#' -#' @author EDG -#' @keywords internal -#' @noRd -#' -#' @examples -#' check_optional_scalar_character(NULL, "my_arg") # Passes -#' check_optional_scalar_character("hello", "my_arg") # Passes -#' # Throw error: -#' try(check_optional_scalar_character(c("hello", "world"), "my_arg")) -#' try(check_optional_scalar_character(123, "my_arg")) -check_optional_scalar_character <- function( - x, - arg_name = deparse(substitute(x)) -) { - check_character(x, allow_null = TRUE, arg_name = arg_name) - if (!is.null(x) && length(x) != 1L) { - cli::cli_abort( - "{.var {arg_name}} must be NULL or a single string." - ) - } - invisible() -} +# %% --- Utils ------------------------------------------------------------------------------------- # %% .is_named_list() ---- #' Test Named List From 3c0e016b93a0b69382eaef0fb57d4afaace48b11 Mon Sep 17 00:00:00 2001 From: Stathis Gennatas Date: Wed, 29 Apr 2026 12:46:30 -0700 Subject: [PATCH 06/10] docs Co-authored-by: Copilot --- r/man/bounded_double_property.Rd | 41 ++++++++++++++++++++++++++ r/man/check_double_scalar.Rd | 29 ++++++++++++++++++ r/man/check_pos_double_scalar.Rd | 30 +++++++++++++++++++ r/tests/testthat/test_Agent.R | 2 +- r/tests/testthat/test_AgentState.R | 2 +- r/tests/testthat/test_LLM.R | 2 +- r/tests/testthat/test_anthropic.R | 2 +- r/tests/testthat/test_ollama.R | 2 +- r/tests/testthat/test_openai.R | 2 +- r/tests/testthat/test_tool_wikipedia.R | 2 +- r/tests/testthat/test_tools.R | 3 +- r/tests/testthat/test_validate.R | 2 +- 12 files changed, 110 insertions(+), 9 deletions(-) create mode 100644 r/man/bounded_double_property.Rd create mode 100644 r/man/check_double_scalar.Rd create mode 100644 r/man/check_pos_double_scalar.Rd diff --git a/r/man/bounded_double_property.Rd b/r/man/bounded_double_property.Rd new file mode 100644 index 0000000..c76ebdd --- /dev/null +++ b/r/man/bounded_double_property.Rd @@ -0,0 +1,41 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/00_init.R +\name{bounded_double_property} +\alias{bounded_double_property} +\title{Create a bounded double S7 property} +\usage{ +bounded_double_property( + lower = -Inf, + upper = Inf, + lower_open = FALSE, + upper_open = FALSE, + nullable = FALSE +) +} +\arguments{ +\item{lower}{Numeric scalar. Lower bound. Default \code{-Inf}.} + +\item{upper}{Numeric scalar. Upper bound. Default \code{Inf}.} + +\item{lower_open}{Logical scalar. If \code{TRUE}, lower bound is exclusive \verb{(lower, ...]}. +Default \code{FALSE}.} + +\item{upper_open}{Logical scalar. If \code{TRUE}, upper bound is exclusive \verb{[..., upper)}. +Default \code{FALSE}.} + +\item{nullable}{Logical scalar. If \code{TRUE}, \code{NULL} is also accepted. Default \code{FALSE}.} +} +\value{ +An S7 property object. +} +\description{ +Returns a \code{new_property()} for a double scalar constrained to a given interval. +Useful for bounds not covered by the pre-built properties. +} +\examples{ +# Learning rate in (0, 1] +lr_prop <- bounded_double_property(0, 1, lower_open = TRUE) +} +\author{ +EDG +} diff --git a/r/man/check_double_scalar.Rd b/r/man/check_double_scalar.Rd new file mode 100644 index 0000000..9ef7531 --- /dev/null +++ b/r/man/check_double_scalar.Rd @@ -0,0 +1,29 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/00_init.R +\name{check_double_scalar} +\alias{check_double_scalar} +\title{Check double scalar} +\usage{ +check_double_scalar(x, arg_name = deparse(substitute(x))) +} +\arguments{ +\item{x}{Numeric: Value to check. Must be a single non-NA number (integer inputs are accepted).} + +\item{arg_name}{Character: Argument name to use in error messages.} +} +\value{ +Called for side effects. Throws an error if checks fail. +} +\description{ +Check double scalar +} +\examples{ +check_double_scalar(3.14) +check_double_scalar(1L) +# Throw error: +try(check_double_scalar(NA_real_)) +try(check_double_scalar(c(1.0, 2.0))) +} +\author{ +EDG +} diff --git a/r/man/check_pos_double_scalar.Rd b/r/man/check_pos_double_scalar.Rd new file mode 100644 index 0000000..76e5332 --- /dev/null +++ b/r/man/check_pos_double_scalar.Rd @@ -0,0 +1,30 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/00_init.R +\name{check_pos_double_scalar} +\alias{check_pos_double_scalar} +\title{Check positive double scalar} +\usage{ +check_pos_double_scalar(x, arg_name = deparse(substitute(x))) +} +\arguments{ +\item{x}{Numeric: Value to check. Must be a single finite number strictly greater than zero.} + +\item{arg_name}{Character: Argument name to use in error messages.} +} +\value{ +Called for side effects. Throws an error if checks fail. +} +\description{ +Check positive double scalar +} +\examples{ +check_pos_double_scalar(0.001) +check_pos_double_scalar(100) +# Throw error: +try(check_pos_double_scalar(0)) +try(check_pos_double_scalar(-1)) +try(check_pos_double_scalar(Inf)) +} +\author{ +EDG +} diff --git a/r/tests/testthat/test_Agent.R b/r/tests/testthat/test_Agent.R index 60d263b..0b1b2aa 100644 --- a/r/tests/testthat/test_Agent.R +++ b/r/tests/testthat/test_Agent.R @@ -1,6 +1,6 @@ # test_Agent.R # ::rtemis.llm:: -# 2025 EDG rtemis.org +# 2025- EDG rtemis.org # %% Settings --- model_name <- "qwen3.5:0.8b" diff --git a/r/tests/testthat/test_AgentState.R b/r/tests/testthat/test_AgentState.R index 5959470..9f3328e 100644 --- a/r/tests/testthat/test_AgentState.R +++ b/r/tests/testthat/test_AgentState.R @@ -1,6 +1,6 @@ # test_AgentMemory.R # ::rtemis.llm:: -# 2025 EDG rtemis.org +# 2025- EDG rtemis.org # %% AgentMemory Class ---- test_that("AgentMemory class works", { diff --git a/r/tests/testthat/test_LLM.R b/r/tests/testthat/test_LLM.R index bee12ba..ed390ee 100644 --- a/r/tests/testthat/test_LLM.R +++ b/r/tests/testthat/test_LLM.R @@ -1,6 +1,6 @@ # test_LLM.R # ::rtemis.llm:: -# 2025 EDG rtemis.org +# 2025- EDG rtemis.org # %% Settings ---- model_name <- "qwen3.5:0.8b" diff --git a/r/tests/testthat/test_anthropic.R b/r/tests/testthat/test_anthropic.R index d214f5d..b4f9a28 100644 --- a/r/tests/testthat/test_anthropic.R +++ b/r/tests/testthat/test_anthropic.R @@ -1,6 +1,6 @@ # test_anthropic.R # ::rtemis.llm:: -# 2026 EDG rtemis.org +# 2026- EDG rtemis.org # %% AnthropicConfig ---- test_that("AnthropicConfig class and config_Anthropic work", { diff --git a/r/tests/testthat/test_ollama.R b/r/tests/testthat/test_ollama.R index a63e3f9..ffc8c7b 100644 --- a/r/tests/testthat/test_ollama.R +++ b/r/tests/testthat/test_ollama.R @@ -1,6 +1,6 @@ # test-ollama.R # ::rtemis.llm:: -# 2025 EDG rtemis.org +# 2025- EDG rtemis.org # library(testthat) diff --git a/r/tests/testthat/test_openai.R b/r/tests/testthat/test_openai.R index 5ae20bc..468f943 100644 --- a/r/tests/testthat/test_openai.R +++ b/r/tests/testthat/test_openai.R @@ -1,6 +1,6 @@ # test_openai.R # ::rtemis.llm:: -# 2026 EDG rtemis.org +# 2026- EDG rtemis.org # %% OpenAIConfig ---- test_that("OpenAIConfig class works for local compatible servers", { diff --git a/r/tests/testthat/test_tool_wikipedia.R b/r/tests/testthat/test_tool_wikipedia.R index 18126e3..5f2e73b 100644 --- a/r/tests/testthat/test_tool_wikipedia.R +++ b/r/tests/testthat/test_tool_wikipedia.R @@ -1,6 +1,6 @@ # test_tools_wikipedia.R # ::rtemis.llm:: -# 2025 EDG rtemis.org +# 2025- EDG rtemis.org test_that("query_wikipedia returns expected columns (light run)", { skip_on_cran() diff --git a/r/tests/testthat/test_tools.R b/r/tests/testthat/test_tools.R index 3fe74cb..1355181 100644 --- a/r/tests/testthat/test_tools.R +++ b/r/tests/testthat/test_tools.R @@ -1,6 +1,6 @@ # test-tools.R # ::rtemis.llm:: -# 2025 EDG rtemis.org +# 2025- EDG rtemis.org # %% ToolParameter Class ---- test_that("ToolParameter class works", { @@ -38,6 +38,7 @@ test_that("tool_param() works", { test_that("Tool class works", { tool <- Tool( name = "addition", + function_name = "add_numbers", description = "Performs arithmetic addition of two numbers.", parameters = list( tool_param( diff --git a/r/tests/testthat/test_validate.R b/r/tests/testthat/test_validate.R index fbee5cd..fd2f5a4 100644 --- a/r/tests/testthat/test_validate.R +++ b/r/tests/testthat/test_validate.R @@ -1,6 +1,6 @@ # test_validate.R # ::rtemis.llm:: -# 2025 EDG rtemis.org +# 2025- EDG rtemis.org # %% validate_function ---- # This works normally, but NOT during checks / tests From 554076df96f38c9db53e6c92a20b5adc5005181b Mon Sep 17 00:00:00 2001 From: Stathis Gennatas Date: Wed, 29 Apr 2026 12:47:18 -0700 Subject: [PATCH 07/10] docs --- r/man/bounded_double_property.Rd | 41 -------------------------------- r/man/check_double_scalar.Rd | 29 ---------------------- r/man/check_pos_double_scalar.Rd | 30 ----------------------- 3 files changed, 100 deletions(-) delete mode 100644 r/man/bounded_double_property.Rd delete mode 100644 r/man/check_double_scalar.Rd delete mode 100644 r/man/check_pos_double_scalar.Rd diff --git a/r/man/bounded_double_property.Rd b/r/man/bounded_double_property.Rd deleted file mode 100644 index c76ebdd..0000000 --- a/r/man/bounded_double_property.Rd +++ /dev/null @@ -1,41 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/00_init.R -\name{bounded_double_property} -\alias{bounded_double_property} -\title{Create a bounded double S7 property} -\usage{ -bounded_double_property( - lower = -Inf, - upper = Inf, - lower_open = FALSE, - upper_open = FALSE, - nullable = FALSE -) -} -\arguments{ -\item{lower}{Numeric scalar. Lower bound. Default \code{-Inf}.} - -\item{upper}{Numeric scalar. Upper bound. Default \code{Inf}.} - -\item{lower_open}{Logical scalar. If \code{TRUE}, lower bound is exclusive \verb{(lower, ...]}. -Default \code{FALSE}.} - -\item{upper_open}{Logical scalar. If \code{TRUE}, upper bound is exclusive \verb{[..., upper)}. -Default \code{FALSE}.} - -\item{nullable}{Logical scalar. If \code{TRUE}, \code{NULL} is also accepted. Default \code{FALSE}.} -} -\value{ -An S7 property object. -} -\description{ -Returns a \code{new_property()} for a double scalar constrained to a given interval. -Useful for bounds not covered by the pre-built properties. -} -\examples{ -# Learning rate in (0, 1] -lr_prop <- bounded_double_property(0, 1, lower_open = TRUE) -} -\author{ -EDG -} diff --git a/r/man/check_double_scalar.Rd b/r/man/check_double_scalar.Rd deleted file mode 100644 index 9ef7531..0000000 --- a/r/man/check_double_scalar.Rd +++ /dev/null @@ -1,29 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/00_init.R -\name{check_double_scalar} -\alias{check_double_scalar} -\title{Check double scalar} -\usage{ -check_double_scalar(x, arg_name = deparse(substitute(x))) -} -\arguments{ -\item{x}{Numeric: Value to check. Must be a single non-NA number (integer inputs are accepted).} - -\item{arg_name}{Character: Argument name to use in error messages.} -} -\value{ -Called for side effects. Throws an error if checks fail. -} -\description{ -Check double scalar -} -\examples{ -check_double_scalar(3.14) -check_double_scalar(1L) -# Throw error: -try(check_double_scalar(NA_real_)) -try(check_double_scalar(c(1.0, 2.0))) -} -\author{ -EDG -} diff --git a/r/man/check_pos_double_scalar.Rd b/r/man/check_pos_double_scalar.Rd deleted file mode 100644 index 76e5332..0000000 --- a/r/man/check_pos_double_scalar.Rd +++ /dev/null @@ -1,30 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/00_init.R -\name{check_pos_double_scalar} -\alias{check_pos_double_scalar} -\title{Check positive double scalar} -\usage{ -check_pos_double_scalar(x, arg_name = deparse(substitute(x))) -} -\arguments{ -\item{x}{Numeric: Value to check. Must be a single finite number strictly greater than zero.} - -\item{arg_name}{Character: Argument name to use in error messages.} -} -\value{ -Called for side effects. Throws an error if checks fail. -} -\description{ -Check positive double scalar -} -\examples{ -check_pos_double_scalar(0.001) -check_pos_double_scalar(100) -# Throw error: -try(check_pos_double_scalar(0)) -try(check_pos_double_scalar(-1)) -try(check_pos_double_scalar(Inf)) -} -\author{ -EDG -} From cd1581e149507a60f9212bee05708fb23c95770c Mon Sep 17 00:00:00 2001 From: Stathis Gennatas Date: Wed, 29 Apr 2026 12:47:24 -0700 Subject: [PATCH 08/10] cleanup --- Makefile | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index a3948e5..827440c 100644 --- a/Makefile +++ b/Makefile @@ -4,50 +4,50 @@ format: format-r format-r: - @echo "==> R: Formatting rtemis.draw" + @echo "==> R: Formatting rtemis.llm" cd r && air format . # ── Document ───────────────────────────────────────────────────────────────── document: document-r document-r: format-r - @echo "=> R: Documenting rtemis.draw" + @echo "=> R: Documenting rtemis.llm" cd r && Rscript -e "devtools::document()" # ── Document & Install ─────────────────────────────────────────────────────── install: install-r install-r: document-r - @echo "==> R: Installing rtemis.draw" + @echo "==> R: Installing rtemis.llm" cd r && Rscript -e "devtools::install()" # ── Test ───────────────────────────────────────────────────────────────────── test: test-r test-r: - @echo "==> R: Testing rtemis.draw" + @echo "==> R: Testing rtemis.llm" cd r && Rscript -e "devtools::test(stop_on_failure = TRUE)" # ── URL Check ──────────────────────────────────────────────────────────────── url-check-r: - @echo "==> R: Checking URLs in rtemis.draw" + @echo "==> R: Checking URLs in rtemis.llm" cd r && Rscript -e "urlchecker::url_check()" # ── Check ──────────────────────────────────────────────────────────────────── check: check-r check-r: - @echo "==> R: Checking rtemis.draw" + @echo "==> R: Checking rtemis.llm" cd r && R CMD build . && R CMD check rtemis.llm_*.tar.gz --as-cran && rm rtemis.llm_*.tar.gz # ── Build Site ─────────────────────────────────────────────────────────────── site: site-r site-r: - @echo "==> R: Building pkgdown site for rtemis.draw" + @echo "==> R: Building pkgdown site for rtemis.llm" cd r && Rscript -e "pkgdown::build_site()" # ── Build ──────────────────────────────────────────────────────────────────── build-r: - @echo "==> R: Building rtemis.draw" + @echo "==> R: Building rtemis.llm" cd r && R CMD build . From 49b2dcc986e8a0400308e77b142a5c57798cb323 Mon Sep 17 00:00:00 2001 From: Stathis Gennatas Date: Wed, 29 Apr 2026 12:47:39 -0700 Subject: [PATCH 09/10] do not export internals --- r/R/00_init.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/r/R/00_init.R b/r/R/00_init.R index 5d9de69..60455d5 100644 --- a/r/R/00_init.R +++ b/r/R/00_init.R @@ -113,7 +113,7 @@ prob_scalar <- new_property( #' #' @return An S7 property object. #' @author EDG -#' @export +#' @noRd #' #' @examples #' # Learning rate in (0, 1] @@ -233,7 +233,7 @@ check_optional_scalar_character <- function( #' @return Called for side effects. Throws an error if checks fail. #' #' @author EDG -#' @export +#' @noRd #' #' @examples #' check_double_scalar(3.14) @@ -290,7 +290,7 @@ check_optional_pos_double_scalar <- function( #' @return Called for side effects. Throws an error if checks fail. #' #' @author EDG -#' @export +#' @noRd #' #' @examples #' check_pos_double_scalar(0.001) From 936f61ebab811db44852e0540bde0622c5b855cf Mon Sep 17 00:00:00 2001 From: Stathis Gennatas Date: Wed, 29 Apr 2026 12:47:59 -0700 Subject: [PATCH 10/10] use stricter types and validation --- r/R/01_Schema.R | 12 ++++---- r/R/02_Message.R | 77 ++++++++++++++++++++++++---------------------- r/R/04_Tool.R | 14 ++++----- r/R/05_LLMConfig.R | 28 +++++++++-------- r/R/06_LLM.R | 38 +++++++++++++++++------ r/R/07_Agent.R | 12 ++++---- 6 files changed, 104 insertions(+), 77 deletions(-) diff --git a/r/R/01_Schema.R b/r/R/01_Schema.R index 32f9273..da93ce4 100644 --- a/r/R/01_Schema.R +++ b/r/R/01_Schema.R @@ -28,9 +28,9 @@ Field <- S7::new_class( "Field", properties = list( - name = S7::class_character, - type = S7::class_character, - description = optional(S7::class_character), + name = character_scalar, + type = character_scalar, + description = optional_character_scalar, required = S7::class_logical ), validator = function(self) { @@ -94,9 +94,9 @@ method(print, Field) <- function(x, output_type = NULL, ...) { Schema <- S7::new_class( "Schema", properties = list( - name = optional(S7::class_character), - type = class_character, - description = optional(S7::class_character), + name = optional_character_scalar, + type = character_scalar, + description = optional_character_scalar, fields = class_list ), constructor = function( diff --git a/r/R/02_Message.R b/r/R/02_Message.R index 27066d6..e68e997 100644 --- a/r/R/02_Message.R +++ b/r/R/02_Message.R @@ -32,15 +32,15 @@ TOOL_MESSAGE_ROLE <- "tool" Message <- new_class( "Message", properties = list( - content = class_character, - role = class_character, - name = optional(S7::class_character), + content = character_scalar, + role = character_scalar, + name = optional_character_scalar, timestamp = class_POSIXct, metadata = optional(S7::class_list) ), constructor = function( - content = character(0L), - role = character(0L), + content, + role, name = NULL, metadata = NULL ) { @@ -134,11 +134,12 @@ SystemMessage <- new_class( metadata = NULL ) { new_object( - Message(), - content = content, - role = SYSTEM_MESSAGE_ROLE, - name = name, - metadata = metadata + Message( + content = content, + role = SYSTEM_MESSAGE_ROLE, + name = name, + metadata = metadata + ) ) } ) @@ -183,7 +184,7 @@ InputMessage <- new_class( "InputMessage", parent = Message, properties = list( - image_path = optional(S7::class_character) + image_path = optional_character_scalar ), constructor = function( content, @@ -192,12 +193,13 @@ InputMessage <- new_class( metadata = NULL ) { new_object( - Message(), - content = content, - role = INPUT_MESSAGE_ROLE, - name = name, - image_path = image_path, - metadata = metadata + Message( + content = content, + role = INPUT_MESSAGE_ROLE, + name = name, + metadata = metadata + ), + image_path = image_path ) } ) @@ -269,9 +271,9 @@ LLMMessage <- new_class( "LLMMessage", parent = Message, properties = list( - reasoning = optional(S7::class_character), + reasoning = optional_character_scalar, tool_calls = optional(S7::class_list), - model_name = class_character + model_name = character_scalar ), constructor = function( content, @@ -282,11 +284,12 @@ LLMMessage <- new_class( tool_calls = NULL ) { new_object( - Message(), - content = content, - role = LLM_MESSAGE_ROLE, - name = name, - metadata = metadata, + Message( + content = content, + role = LLM_MESSAGE_ROLE, + name = name, + metadata = metadata + ), model_name = model_name, reasoning = reasoning, tool_calls = tool_calls @@ -492,11 +495,12 @@ AgentMessage <- new_class( metadata = NULL ) { new_object( - Message(), - content = content, - role = AGENT_MESSAGE_ROLE, - name = name, - metadata = metadata + Message( + content = content, + role = AGENT_MESSAGE_ROLE, + name = name, + metadata = metadata + ) ) } ) @@ -574,7 +578,7 @@ ToolMessage <- new_class( "ToolMessage", parent = Message, properties = list( - tool_call_id = optional(S7::class_character) + tool_call_id = optional_character_scalar ), constructor = function( content, @@ -583,12 +587,13 @@ ToolMessage <- new_class( metadata = list() ) { new_object( - Message(), - role = TOOL_MESSAGE_ROLE, - name = name, - content = content, - tool_call_id = tool_call_id, - metadata = metadata + Message( + content = content, + role = TOOL_MESSAGE_ROLE, + name = name, + metadata = metadata + ), + tool_call_id = tool_call_id ) } ) diff --git a/r/R/04_Tool.R b/r/R/04_Tool.R index 59fba62..9e8c248 100644 --- a/r/R/04_Tool.R +++ b/r/R/04_Tool.R @@ -14,10 +14,10 @@ ToolParameter <- new_class( "ToolParameter", properties = list( - name = class_character, - type = class_character, - description = class_character, - required = class_logical + name = character_scalar, + type = character_scalar, + description = character_scalar, + required = logical_scalar ), validator = function(self) { check_scalar_character(self@name, "name") @@ -101,9 +101,9 @@ tool_param <- function( Tool <- new_class( "Tool", properties = list( - name = class_character, - function_name = class_character, - description = class_character, + name = character_scalar, + function_name = character_scalar, + description = character_scalar, parameters = class_list, impl = optional(class_function) ), diff --git a/r/R/05_LLMConfig.R b/r/R/05_LLMConfig.R index 9e08830..e2f10a0 100644 --- a/r/R/05_LLMConfig.R +++ b/r/R/05_LLMConfig.R @@ -38,10 +38,10 @@ ANTHROPIC_THINKING_MIN_BUDGET <- 1024L LLMConfig <- new_class( "LLMConfig", properties = list( - model_name = class_character, + model_name = character_scalar, temperature = class_numeric, - backend = class_character, - base_url = class_character + backend = character_scalar, + base_url = character_scalar ), constructor = function( model_name, @@ -154,11 +154,12 @@ OpenAIConfig <- new_class( "OpenAIConfig", parent = LLMConfig, properties = list( - api_key = optional(S7::class_character), - api_key_env = class_character, - keychain_service = optional(S7::class_character), - organization = optional(S7::class_character), - project = optional(S7::class_character), + temperature = bounded_double_property(0, 2), + api_key = optional_character_scalar, + api_key_env = character_scalar, + keychain_service = optional_character_scalar, + organization = optional_character_scalar, + project = optional_character_scalar, timeout = class_numeric, extra_headers = optional(S7::class_list), extra_body = optional(S7::class_list), @@ -263,11 +264,12 @@ AnthropicConfig <- new_class( "AnthropicConfig", parent = LLMConfig, properties = list( - api_key = optional(S7::class_character), - api_key_env = class_character, - keychain_service = optional(S7::class_character), - anthropic_version = class_character, - anthropic_beta = optional(S7::class_character), + temperature = prob_scalar, + api_key = optional_character_scalar, + api_key_env = character_scalar, + keychain_service = optional_character_scalar, + anthropic_version = character_scalar, + anthropic_beta = optional_character_scalar, max_tokens = class_integer, timeout = class_numeric, extra_headers = optional(S7::class_list), diff --git a/r/R/06_LLM.R b/r/R/06_LLM.R index 4d96b1c..9f5f388 100644 --- a/r/R/06_LLM.R +++ b/r/R/06_LLM.R @@ -17,9 +17,12 @@ LLM <- new_class( "LLM", properties = list( - name = optional(S7::class_character), - system_prompt = class_character - ) + name = optional_character_scalar, + system_prompt = character_scalar + ), + constructor = function(name = NULL, system_prompt) { + new_object(S7_object(), name = name, system_prompt = system_prompt) + } ) # rtemis.llm::LLM @@ -68,7 +71,6 @@ Ollama <- new_class( "Ollama", parent = LLM, properties = list( - name = optional(S7::class_character), config = OllamaConfig, output_schema = optional(Schema) ), @@ -81,9 +83,9 @@ Ollama <- new_class( ollama_check_model(config@model_name) new_object( LLM( + name = name, system_prompt = system_prompt ), - name = name, config = config, output_schema = output_schema ) @@ -147,7 +149,6 @@ OpenAI <- new_class( "OpenAI", parent = LLM, properties = list( - name = optional(S7::class_character), config = OpenAIConfig, output_schema = optional(Schema) ), @@ -159,9 +160,9 @@ OpenAI <- new_class( ) { new_object( LLM( + name = name, system_prompt = system_prompt ), - name = name, config = config, output_schema = output_schema ) @@ -221,7 +222,6 @@ Anthropic <- new_class( "Anthropic", parent = LLM, properties = list( - name = optional(S7::class_character), config = AnthropicConfig, output_schema = optional(Schema) ), @@ -233,9 +233,9 @@ Anthropic <- new_class( ) { new_object( LLM( + name = name, system_prompt = system_prompt ), - name = name, config = config, output_schema = output_schema ) @@ -609,6 +609,10 @@ create_Ollama <- function( think = NULL ) { ollama_check_model(model_name) + check_scalar_character(system_prompt, "system_prompt") + check_optional_pos_double_scalar(temperature, "temperature") + check_optional_scalar_character(name, "name") + check_scalar_character(base_url, "base_url") Ollama( name = name, config = OllamaConfig( @@ -670,6 +674,9 @@ config_OpenAI <- function( enable_thinking = NULL, validate_model = FALSE ) { + check_scalar_character(model_name) + check_optional_pos_double_scalar(temperature, "temperature") + check_scalar_character(base_url, "base_url") OpenAIConfig( model_name = model_name, temperature = temperature, @@ -739,6 +746,11 @@ create_OpenAI <- function( enable_thinking = NULL, validate_model = FALSE ) { + check_scalar_character(model_name) + check_scalar_character(system_prompt, "system_prompt") + check_optional_pos_double_scalar(temperature, "temperature") + check_optional_scalar_character(name, "name") + check_scalar_character(base_url, "base_url") OpenAI( name = name, config = config_OpenAI( @@ -815,6 +827,9 @@ config_Anthropic <- function( thinking_budget_tokens = NULL, validate_model = FALSE ) { + check_scalar_character(model_name) + check_optional_pos_double_scalar(temperature, "temperature") + check_scalar_character(base_url, "base_url") AnthropicConfig( model_name = model_name, temperature = temperature, @@ -887,6 +902,11 @@ create_Anthropic <- function( thinking_budget_tokens = NULL, validate_model = FALSE ) { + check_scalar_character(model_name) + check_scalar_character(system_prompt, "system_prompt") + check_optional_pos_double_scalar(temperature, "temperature") + check_optional_scalar_character(name, "name") + check_scalar_character(base_url, "base_url") Anthropic( name = name, config = config_Anthropic( diff --git a/r/R/07_Agent.R b/r/R/07_Agent.R index e536d8b..a1c9af9 100644 --- a/r/R/07_Agent.R +++ b/r/R/07_Agent.R @@ -34,14 +34,14 @@ Agent <- new_class( properties = list( llmconfig = LLMConfig, state = AgentMemory, - system_prompt = optional(S7::class_character), - use_memory = class_logical, + system_prompt = optional_character_scalar, + use_memory = logical_scalar, tools = optional(S7::class_list), - max_tool_rounds = class_integer, + max_tool_rounds = pos_integer_scalar, output_schema = optional(Schema), - name = optional(S7::class_character), - allow_custom_tools = class_logical, - logfile = S7::class_character + name = optional_character_scalar, + allow_custom_tools = logical_scalar, + logfile = character_scalar ), constructor = function( llmconfig,