Nothing
#' Create a stream controller
#'
#' @description
#' Creates a controller that can cancel an in-progress stream. Pass it to
#' [Chat]'s `$stream()` or `$stream_async()` via the `controller` argument,
#' then call `$cancel()` from anywhere (e.g. a Shiny observer) to stop the
#' stream after the next chunk arrives.
#'
#' The same controller can be reused across multiple streams. Call
#' `$reset()` to clear the cancelled state, or pass it directly to a new
#' `$stream()` call — it will be reset automatically.
#'
#' @section Async cancellation in Shiny:
#'
#' In a Shiny app, use an [ExtendedTask][shiny::ExtendedTask] for
#' non-blocking chat and a `stream_controller()` to wire up a cancel
#' button:
#'
#' ```r
#' controller <- stream_controller()
#'
#' chat_task <- ExtendedTask$new(function(user_query, controller = NULL) {
#' chat <- chat_openai(model = "gpt-5-nano")
#' stream <- chat$stream_async(user_query, controller = controller)
#' shinychat::markdown_stream("response", stream)
#' })
#'
#' observeEvent(input$ask, {
#' controller <<- stream_controller()
#' chat_task$invoke(input$query, controller = controller)
#' })
#'
#' observeEvent(input$cancel, {
#' controller$cancel()
#' })
#' ```
#'
#' @return An `ellmer_stream_controller` object with the following
#' elements:
#'
#' * `$cancel(reason = "cancelled")`: Cancel the stream. The `reason`
#' string is stored on the controller and used as the
#' [AssistantPartialTurn][Turn]'s `reason` property.
#' * `$reset()`: Clear the cancelled state and reason.
#' * `$cancelled`: A logical flag indicating whether the controller
#' has been cancelled.
#' * `$reason`: The cancellation reason string, or `NULL` if not
#' cancelled.
#'
#' @examplesIf rlang::is_interactive()
#' chat <- chat_openai(model = "gpt-5.4-nano")
#'
#' ctrl <- stream_controller()
#' stream <- chat$stream("Write a short story.", controller = ctrl)
#'
#' i <- 0
#' coro::loop(for (chunk in stream) {
#' i <- i + 1
#' if (i > 10) ctrl$cancel()
#' })
#'
#' chat
#'
#' @export
stream_controller <- function() {
StreamController$new()
}
StreamController <- R6::R6Class(
"ellmer_stream_controller",
public = list(
cancel = function(reason = "cancelled") {
check_string(reason)
private$.reason <- reason
private$.cancelled <- TRUE
},
reset = function() {
private$.cancelled <- FALSE
private$.reason <- NULL
},
print = function(...) {
status <- if (private$.cancelled) {
sprintf('cancelled="%s"', private$.reason)
} else {
"active"
}
cat("<ellmer_stream_controller ", status, ">\n", sep = "")
invisible(self)
}
),
active = list(
cancelled = function(value) {
if (!missing(value)) {
cli::cli_abort(
"Use the {.code $cancel()} method to cancel the stream or {.code $reset()} to reset the stream controller."
)
}
private$.cancelled
},
reason = function(value) {
if (!missing(value)) {
cli::cli_abort(
"Use the {.code $cancel()} method to cancel the stream or {.code $reset()} to reset the stream controller."
)
}
private$.reason
}
),
private = list(
.cancelled = FALSE,
.reason = NULL
)
)
as_controller <- function(controller, reset = TRUE, call = caller_env()) {
if (is.null(controller)) {
return(stream_controller())
}
if (!inherits(controller, "ellmer_stream_controller")) {
cli::cli_abort(
"{.arg controller} must be an {.cls ellmer_stream_controller} object created by {.fn stream_controller}.",
call = call
)
}
if (reset && controller$cancelled) {
controller$reset()
}
controller
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.