Skip to contents

An agent is three things: a model (an LLMR::llm_config()), a persona (a system prompt), and machinery around them: memory, tools, and budgets. This vignette builds one of each component. Examples use the open-weight gpt-oss-20b on Groq; set GROQ_API_KEY and LLMRAGENT_RUN_VIGNETTES=true to run them.

library(LLMRagent)
cfg <- LLMR::llm_config("groq", "openai/gpt-oss-20b", temperature = 0.7)

A first agent

ada <- agent(
  "Ada", cfg,
  persona = "You are Ada, a meticulous statistician. Answer in one or two sentences."
)
ada$chat("What is overfitting?")
ada$chat("How would I detect it in practice?")   # remembers the thread
ada$usage()

chat() is stateful: the agent keeps its own memory (last 40 messages by default; see ?memory for summarizing and retrieval policies). reply() is the stateless sibling, used internally by conversations. For long answers, chat(stream = TRUE) prints tokens as they are generated:

ada$chat("Explain cross-validation to a newcomer, in one paragraph.",
         stream = TRUE)

Tools: let the agent call your R functions

Anything you can write as an R function can become a tool. The agent decides when to call it; LLMRagent executes the call and feeds the result back.

lookup_gdp <- LLMR::llm_tool(
  function(country) {
    gdp <- c(chile = 335, uruguay = 81, bolivia = 47)  # USD bn, illustrative
    val <- gdp[tolower(country)]
    if (is.na(val)) "unknown" else paste0("$", val, " billion")
  },
  name = "lookup_gdp",
  description = "Look up a country's GDP in USD billions.",
  parameters = list(country = list(type = "string", description = "Country name"))
)

analyst <- agent("Analyst", cfg, tools = lookup_gdp,
                 persona = "A careful economic analyst. Use tools for any figure.")
analyst$chat("Compare the GDPs of Chile and Uruguay using the lookup tool.")
analyst$trace()   # every model call and tool call, with tokens and timing

Budgets: spend ceilings the agent cannot cross

Budgets are checked before each call; the call that would exceed a limit is refused with a typed error, so a loop cannot spend without you seeing it.

frugal <- agent("Frugal", cfg, budget = budget(max_calls = 2))
frugal$chat("one")
frugal$chat("two")
tryCatch(frugal$chat("three"),
         llmragent_budget_error = function(e) "stopped by budget, as designed")

Structured answers

schema <- list(
  type = "object",
  properties = list(stance = list(type = "string",
                                  enum = list("support", "oppose", "unsure")),
                    reason = list(type = "string")),
  required = list("stance", "reason")
)
ada$ask_structured("Should small samples use t or z intervals?", schema)

Agents calling agents

agent_as_tool() turns an agent into a tool, so another agent can consult it. The supervisor decides for itself when to delegate; each consultation runs on the specialist’s own meter (its usage(), its budget()).

stat <- agent("Stat", cfg,
              persona = "A PhD statistician. Precise about assumptions.")
hist <- agent("Hist", cfg,
              persona = "An economic historian. Institutional context.")

lead <- agent("Lead", cfg,
              persona = "A research lead. Consult specialists, then synthesize.",
              tools = list(agent_as_tool(stat), agent_as_tool(hist)))

lead$chat("Crime fell while policing budgets rose, across many cities.
           What would it take to argue causality?")
stat$usage()   # the consultation showed up here

Pipelines: a fixed sequence of specialists

When the routing is fixed rather than model-chosen, chain agents with agent_pipeline(): each stage transforms the previous stage’s output, and every intermediate product is kept.

run <- agent_pipeline(
  list(
    agent("Extractor", cfg, persona =
      "Extract every factual claim as a numbered list. Nothing else."),
    agent("Checker", cfg, persona =
      "Mark each numbered claim VERIFIABLE or VAGUE, one line each."),
    agent("Editor", cfg, persona =
      "Rewrite the original passage keeping only VERIFIABLE claims.")
  ),
  input = "Our app doubled retention, won three design awards, and users love it."
)
run$output
run$steps      # step, agent, input, output -- the full audit trail

A two-agent conversation

Conversations run over a shared, speaker-attributed transcript: every agent sees the full dialogue each turn, and the transcript comes back as a tidy tibble, ready for text analysis.

rosa <- agent("Rosa", cfg, persona = "A pragmatic city planner. Concrete and brief.")
hugo <- agent("Hugo", cfg, persona = "A skeptical economist. Numbers first. Brief.")

conv <- conversation(
  list(rosa, hugo),
  topic = "Should the city pedestrianize its center?",
  max_turns = 4,
  instruction = "At most three sentences per turn."
)
conv$transcript

From here: vignette("designed-conversations") tours the ready-made study formats (debates, focus groups, interviews, deliberations); vignette("deliberation-experiment") runs a complete factorial study with agent_experiment(); and vignette("super-brain") shows strong-plus-cheap model orchestration with think_harder(). For a per-call audit file of everything an agent did, turn on LLMR::llm_log_enable() before running.