Skip to contents

Deputy Agents implement the ellmer chat methods used by shinychat. Pass an Agent directly to chat_server() to stream responses and tool activity while Deputy checks permissions and tracks usage.

shinychat input -> Agent$stream_async() -> ellmer
                                      \-> AgentResult + hooks + usage

The stream keeps ellmer’s native chunks, so shinychat can render incremental text, tool activity, and attachment-enabled input. After it finishes, agent$last_run() exposes the corresponding AgentResult.

Basic Setup

library(shiny)
library(deputy)
library(shinychat)

workspace <- normalizePath(getwd(), mustWork = TRUE, winslash = "/")

ui <- bslib::page_fluid(
  chat_ui("chat", fill = TRUE, allow_attachments = TRUE)
)

server <- function(input, output, session) {
  chat <- ellmer::chat_openai(
    model = "gpt-5.6-luna",
    system_prompt = "You are a concise data assistant."
  )

  agent <- Agent$new(
    chat = chat,
    tools = tools_data(),
    permissions = permissions_readonly(),
    usage_limits = UsageLimits(
      max_requests = 10,
      max_tool_calls = 12,
      max_cost_usd = 0.50
    ),
    context_policy = ContextPolicy(
      max_tokens = 32000,
      fallback = "error"
    ),
    working_dir = workspace
  )

  chat_server("chat", agent)
}

shinyApp(ui, server)

chat_server() passes both plain text and attachment-enabled ellmer content to the Agent unchanged. It also supplies shinychat history and cancellation around Deputy’s stream.

Compaction and native conversation history can be enabled together. Deputy’s get_turns() supplies the complete selected conversation to shinychat, while get_context_turns() exposes only the compacted model context. Later completed replies remain available when the user switches conversations or restores a branch. Native history requires a shinychat version exporting history_options() and FileConversationStore; Deputy’s transcript contract also works without shinychat. Hosts that previously rejected a non-NULL ContextPolicy(max_tokens) with native history can remove that workaround when using this implementation.

Showing compaction to users

The runnable inst/examples/shiny-chat/ app includes a host-owned compaction notice above the transcript. While summarization runs it says “Summarizing earlier messages…”. After successful installation it says “Earlier messages summarized for the assistant. Your full conversation is still available.” An expandable “View summary” shows the accepted summary as escaped text, alongside an explanation that the assistant uses it with recent messages and that some earlier details may be omitted.

The notice does not add, hide, or replace chat messages. PreCompact and PostCompact hooks update its state; a Stop hook clears the busy indication after an automatic run fails or is interrupted, while retaining any previously accepted summary. Register custom compaction-veto hooks before this notice so their early return prevents the progress indication. Hosts that call agent$compact() directly must also clear progress when that call ends, including errors or vetoes; a manual call does not fire the run’s Stop hook. The example scopes the notice to shinychat’s public conversation ID and clears it through history$on_restore(), including branch navigation. Native history restores the complete transcript rather than the compacted context, so the notice stays hidden until compaction happens again. This presentation belongs to the host; Deputy does not insert UI messages into its conversation record.

What the Agent Adds

Choose a method by the result your application needs:

Method Native return Typical host
chat() final text scripts and consoles
chat_async() promise of final text async applications
chat_structured() / chat_structured_async() typed data structured-output hosts
stream() synchronous ellmer stream terminal hosts
stream_async() asynchronous ellmer stream shinychat
run_sync() AgentResult inspected workflows
run_async() promise of AgentResult subagents

All interfaces enforce the Agent’s permissions, hooks, UsageLimits, workspace path resolution, file checkpoints, automatic context compaction, large-result offloading, and run accounting. Native relative file paths resolve against the Agent’s immutable working_dir; Deputy never changes the process working directory. Custom hosts can pass run_context to any chat, stream, or run method to correlate a request with product-owned session and stage identities.

ContextPolicy() checks the estimated complete context after run initialization and again at model-request boundaries between tool rounds, compacting whenever it crosses the token threshold. Summary streaming is asynchronous and shares the run’s cancellation controller and usage limits. The default fails closed if LLM summary generation fails. Set fallback = "text" only when a deterministic degraded summary is acceptable. summary_fallback_chats explicitly configures separate summary recovery destinations. agent$last_compaction() reports the method, run ID, attempt conditions, and compaction usage.

Large tool results are stored as integrity-checked envelopes and replaced in model context with a preview and deputy://tool-result/... reference. The Agent registers deputy_read_tool_result() so the model can page through the stored result in bounded chunks. Applications can retrieve the original R value with agent$resolve_tool_result(reference).

Hooks and Cancellation

Hooks attach to the Agent as usual:

agent$add_hook(hook_log_tools(verbose = TRUE))

agent$add_hook(HookMatcher(
  event = "PostCompact",
  callback = function(result, context) {
    cli::cli_inform("Compacted with: {result$method}")
  }
))

chat_append() owns the completion promise and surfaces stream errors in the chat UI. shinychat cancellation propagates through ellmer’s stream controller; Deputy still finalizes checkpoints and fires Stop and SessionEnd.

Subagents

LeadAgent delegation uses run_async() internally, so a subagent does not block the Shiny R process while the parent chat is streaming. A custom async tool can use the same contract:

tool_scout <- ellmer::tool(
  fun = coro::async(function(prompt) {
    result <- coro::await(scout$run_async(prompt))
    result$response
  }),
  name = "agent_scout",
  description = "Delegate a literature search.",
  arguments = list(prompt = ellmer::type_string("Self-contained task"))
)
agent$register_tool(tool_scout)

Running the Example

shiny::runApp(system.file("examples/shiny-chat", package = "deputy"))