3  A trusted mini-agent template in Shiny

Web applications are a great way to implement trusted mini-agents because the developer has complete control of both the frontend and backend. We can optimize the human user experience of reviewing LLM-generated tool inputs, control all the tools, and cleanly separate trusted results from untrusted chat output. This chapter proposes a template for a trusted mini-agent as a Shiny app in R. Users can begin with this template and add their own ellmer tools to create a trusted mini-agent that fits their needs. The next chapter covers tools.

3.1 Prerequisites

At minimum, we use these R packages to implement Shiny-based trusted mini-agents.

Package Role
ellmer LLM chat client with tool registration and streaming.
shinychat Drop-in chat UI for Shiny that binds to an ellmer chat object.
bslib Bootstrap-based interface elements such as page_sidebar and theming.

3.2 The template app

The app has a simple interface: a sidebar with the untrusted AI chat, a bslib card where humans review LLM-generated tool inputs, and a bslib card for trusted results.

Screenshot of the trusted mini-agent template Shiny app. The left sidebar shows an AI chat interface labeled “do not trust any AI output here!” with a simple ping/pong exchange. The main area has two empty placeholder cards: one labeled “Humans review LLM-generated tool inputs here” and another labeled “Trusted results go here,” illustrating the three-region layout of trust zones.

It is critical to create separate regions of trust and skepticism in the interface. Users need to know exactly which parts to trust, which parts to review, and which parts to never trust. We avoid injecting trusted results into the chat interface because it creates ambiguity about what to trust, which increases the risk that users will miss hallucinations.

3.3 Implementation

We separately consider the ellmer chat object, Shiny UI, and Shiny server function.

3.3.1 Chat object

In ellmer, the chat object brokers communication with the LLM, and it facilitates the registration of tools. Crucially for trusted mini-agents, a chat object supports no tools by default, so each tool must be explicitly registered (see the next chapter).

For convenience, we create a separate constructor for the ellmer chat object. This compartmentalization will be useful when we add tools and complicated system prompts.

We use chat_anthropic() in this example, but any chat object will do.

new_chat <- function() {
  ellmer::chat_anthropic(
    system_prompt = "You are a friendly, concise assistant."
  )
}

3.3.2 Shiny UI

In a trusted mini-agent, the user interface should have separate components for the chat, human oversight, and trusted results. That way, it is clear to the user which parts of the interface to trust, which parts to review, and which parts to always view with strong skepticism.

ui <- bslib::page_sidebar(
  title = "Trusted mini-agent template",
  theme = bslib::bs_theme(bootswatch = "cosmo"),
  sidebar = bslib::sidebar(
    title = "AI chat (do not trust any AI output here!)",
    shinychat::chat_ui("chat")
  ),
  bslib::layout_columns(
    bslib::card("Humans review LLM-generated tool inputs here."),
    bslib::card("Trusted results go here.")
  )
)

3.3.3 Shiny server function

In the Shiny server, we have a reactive expression that watches for user prompts from the shinychat interface and streams the LLM’s reply back into the UI token-by-token.

server <- function(input, output, session) {
  chat <- new_chat()
  shiny::observeEvent(input$chat_user_input, {
    stream <- chat$stream_async(input$chat_user_input, stream = "content")
    shinychat::chat_append("chat", stream)
  })
}

3.3.4 Full app code

This app.R script is a blank template to help you begin implementing your own trusted mini-agent.

app.R
new_chat <- function() {
  ellmer::chat_anthropic(
    system_prompt = "You are a friendly, concise assistant."
  )
}

ui <- bslib::page_sidebar(
  title = "Trusted mini-agent template",
  theme = bslib::bs_theme(bootswatch = "cosmo"),
  sidebar = bslib::sidebar(
    title = "AI chat (do not trust any AI output here!)",
    shinychat::chat_ui("chat")
  ),
  bslib::layout_columns(
    bslib::card("Humans review LLM-generated tool inputs here."),
    bslib::card("Trusted results go here.")
  )
)

server <- function(input, output, session) {
  chat <- new_chat()
  shiny::observeEvent(input$chat_user_input, {
    stream <- chat$stream_async(input$chat_user_input, stream = "content")
    shinychat::chat_append("chat", stream)
  })
}

shiny::shinyApp(ui, server)