---
title: "Generation as a stream of R data"
format: html
engine: knitr
vignette: >
  %\VignetteIndexEntry{Generation as a stream of R data}
  %\VignetteEngine{quarto::html}
  %\VignetteEncoding{UTF-8}
---

Token streaming is a development feature after relm 0.3.0. It adds `on_token`
to asynchronous generation. The worker uses the loaded model while R receives
small, ordered batches as ordinary data frames. The final promise still resolves
to the same named character vector and seed. These examples require a local
model and are shown without running or downloading one during document rendering.

## Collect text and inspect tokens

```{r}
#| eval: false
library(relm)
m <- llm("/path/to/your/model.gguf")
chunks <- list()
pending <- llm_generate(m, "Explain a confidence interval in two sentences.",
  seed = 17, async = TRUE,
  on_token = function(batch) {
    chunks[[length(chunks) + 1L]] <<- batch
    cat(paste0(batch$text[batch$event == "text"], collapse = ""))
  })
observed <- promises::then(pending, function(value) {
  events <- do.call(rbind, chunks)
  reconstructed <- paste0(events$text[events$event == "text"], collapse = "")
  stopifnot(identical(reconstructed, unname(as.character(value))))
  print(head(events[events$event == "token", c("token_pos", "token_id", "elapsed")]))
}, function(error) {
  message(conditionMessage(error))
})
```

Run each example after the preceding promise has settled; close `m` after the
last call. RStudio's console stays available while the worker generates and the event loop
delivers callbacks. A callback runs on R's main thread: keep it short. Slow
callbacks or storage can block R and slow the producer. `llm_cancel(m)` requests
cooperative cancellation; it cannot interrupt an arbitrary C call immediately.

Rows have `event_id`, `event`, `prompt_id`, `token_pos`, `token_id`, `text`,
`elapsed`, `finish_reason` and `validated`. Indices are 1-based. A `token` row
records a sampled non-EOG token; `text` rows contain committed text fragments;
`prompt_end` marks successful completion of that prompt. Tokens are not words,
characters or necessarily standalone UTF-8 strings. Text can be buffered to
preserve decoder/stop semantics; long stop sequences can delay display.
Concatenating text rows reproduces the final
successful result, even when token boundaries split characters or stop strings.

With `schema`, token/text rows are provisional (`validated = FALSE`); only that
prompt's successful end is independently schema-validated. A later failure can
still reject the entire call. Already delivered data is not rolled back, and
schema validity does not establish factual accuracy.

## Keep a rolling window instead of retaining everything

```{r}
#| eval: false
window <- NULL
pending <- llm_generate(m, "Count from 1 to 100.", seed = 17, async = TRUE,
  on_token = function(batch) {
    tokens <- batch[batch$event == "token", , drop = FALSE]
    if (!nrow(tokens)) return(invisible(NULL))
    window <<- tail(rbind(window, tokens), 128L)
    seconds <- diff(range(window$elapsed))
    if (seconds > 0) {
      message(round((nrow(window) - 1L) / seconds, 1), " tokens/s in this window")
    }
  })
```

This measures token production under the current workload, including any
backpressure; it is not an unconstrained engine benchmark. Compare production
`elapsed` with local receipt time when delivery lag matters. The native queue
holds at most 256 events/256 KiB text and each callback at most 64 rows/64 KiB.
Those are payload limits, not a whole-process RSS limit. A caller that stores
every batch, as in the first example, owns that additional memory.

## Write an event CSV

```{r}
#| eval: false
con <- file("tokens.csv", open = "wb")
pending <- llm_generate(m, "Explain bootstrap resampling.", seed = 17,
  async = TRUE, on_token = con)
observed <- promises::then(pending, function(value) {
  close(con) # The caller opens and closes the connection.
  print(value)
}, function(error) {
  close(con)
  message(conditionMessage(error))
})
```

Use an empty, seekable regular file opened in binary write mode. Text/raw
connections, sockets, pipes and append destinations are outside this interface.
Do not write or seek the connection while generation owns its stream. Closing
it early stops delivery and rejects the call; errors can leave a partial file.
The promise establishes successful completion, not the presence of one end row.

Read the file with fixed column types and preserve literal `"NA"` text:

```{r}
#| eval: false
events <- read.csv("tokens.csv", fileEncoding = "UTF-8", check.names = FALSE,
  na.strings = character(),
  colClasses = c("integer", "character", "integer", "integer", "integer",
    "character", "numeric", "character", "logical"))
```

The CSV writer preserves the original text bytes. Base R's reader normalizes
embedded CRLF line endings to LF even inside quotes. If those exact line endings
matter, retain callback text or use a CSV reader that preserves quoted CRLF;
do not infer byte identity from this convenience reader.

`on_progress` remains a coalesced progress summary. `on_token` supplies ordered
data without coalescing on successful calls. During every token callback the
model is reserved, so other native operations report busy. Final `on_progress`
runs after ownership is released. Streaming does not expose live activations or
permit changing interventions while a worker is using the model.
