---
title: "System 1 decisions"
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{System 1 decisions}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
---
Generative models (System 2) produce text and code. Typed decision models (System 1) answer
with a logical value, a choice, or a numeric score, together with their probabilities. gptr
returns R vectors that can be used in comparisons, filters and control flow. A batch returns
one answer per input.
The executed examples use `judge`, a fake classifier with synthetic probabilities. Its setup
is hidden to keep the examples short. The outputs show the interface, not model accuracy or
calibration.
``` r
library(gptr)
```
## Logical decisions and probabilities
``` r
abstracts = c(
trial = "We randomized 200 adults to drug A or placebo for 12 weeks.",
cohort = "We followed 5000 nurses for 20 years and recorded incident diabetes.",
pilot = "A pilot study of 30 patients; the allocation method is not reported."
)
is_rct = peter("Is this a randomized trial?", abstracts, model = judge)
is_rct
#> trial cohort pilot
#> TRUE (p=0.94) FALSE (p=0.07) TRUE (p=0.55)
#> judge-s1-1.0 . calibration unknown . 2026-10-15
gptr_prob(is_rct)
#> trial cohort pilot
#> 0.94 0.07 0.55
gptr_prob(is_rct, "probabilities")
#> FALSE TRUE
#> trial 0.06 0.94
#> cohort 0.93 0.07
#> pilot 0.45 0.55
```
The `gptr_decision` result is logical. Its value is `TRUE` when P(yes) is at least
`threshold` (0.5 by default). `gptr_prob(x)` returns P(yes), while
`gptr_prob(x, "confidence")` returns `abs(2 * P(yes) - 1)`; confidence is different from
the probability of a positive answer.
## Structured inputs and batch shape
A data frame supplies one record per row. An atomic vector or an unnamed list supplies one
state per element. A fully named list supplies one structured state; `I(x)` also requests one
state for the whole object. Multiple inputs must have compatible batch sizes: a single state
can accompany every item in a batch.
``` r
studies = data.frame(
abstract = unname(abstracts),
sample_size = c(200L, 5000L, 30L),
row.names = names(abstracts)
)
peter("Was allocation randomized?", studies, model = judge)
#> trial cohort pilot
#> TRUE (p=0.94) FALSE (p=0.07) TRUE (p=0.55)
#> judge-s1-1.0 . calibration unknown . 2026-10-15
record = list(abstract = abstracts[["trial"]], sample_size = 200L,
allocation = "randomized")
length(peter("Was allocation randomized?", record, model = judge))
#> [1] 1
```
System 1 sends the input states to the selected provider, rather than only an object name.
Small structured values can be serialized in full; large or unsupported values are described
or truncated. For a large object already loaded in R, derive the small text or structured
records the decision needs locally before calling. Check what information the question needs
before sending sensitive data; see `?gptr_egress`, `?gptr_security` and
[Execution model](execution-model.html) for how inline R computation differs from these inputs.
## Uncertainty in filters, functions and loops
`min_confidence` turns answers below the requested confidence into `NA` by default. Unknown
confidence is also uncertain when the minimum is positive. For yes/no decisions,
`min_confidence = 0.8` requires P(yes) at least 0.9 or at most 0.1.
``` r
screened = peter("Is this a randomized trial?", abstracts, model = judge,
min_confidence = 0.8)
screened
#> trial cohort pilot
#> TRUE (p=0.94) FALSE (p=0.07) NA (p=0.55)
#> judge-s1-1.0 . calibration unknown . 2026-10-15
names(abstracts)[!is.na(screened) & screened]
#> [1] "trial"
names(abstracts)[is.na(screened)]
#> [1] "pilot"
```
Use `isTRUE()` for a scalar decision in `if` or `while` when abstention is possible. Decide
separately what to do with an uncertain result; treating `NA` as exclusion is a workflow
choice, not a negative model answer.
``` r
is_trial = function(text) {
peter("Is this a randomized trial?", text, model = judge, min_confidence = 0.8)
}
included = character()
review = character()
for (id in names(abstracts)) {
decision = is_trial(abstracts[[id]])
if (isTRUE(decision)) included = c(included, id)
if (is.na(decision)) review = c(review, id)
}
list(included = included, review = review)
#> $included
#> [1] "trial"
#>
#> $review
#> [1] "pilot"
```
The same function can live in an ordinary R file. A call inside a function or loop does not
own a recorded document block. A matching System 1 cache entry can avoid another request;
that is distinct from document replay. See [Script as history](script-as-history.html).
`uncertain = "stop"` signals `gptr_error_s1_uncertain`. `TRUE` or `FALSE` supply a yes/no
fallback; a function `function(state, answer)` can return one replacement of the answer's
type. Such a function may make another model request if you explicitly implement that
escalation, with its own egress, permissions and usage.
## Choices and fractional scores
``` r
samples = c(s1 = "Biopsy of the right lower lobe",
s2 = "Hepatocytes from a resected liver segment",
s3 = "Cortex from an epilepsy resection")
tissue = peter("Which tissue is this?", samples, model = judge,
choices = c("liver", "lung", "brain", "other"))
tissue
#> s1 s2 s3
#> lung (p=0.91) liver (p=0.88) brain (p=0.80)
#> judge-s1-1.0 . calibration unknown . 2026-10-15
gptr_prob(tissue)
#> s1 s2 s3
#> 0.91 0.88 0.80
gptr_prob(tissue, "probabilities")
#> liver lung brain other
#> s1 0.03 0.91 0.03 0.03
#> s2 0.88 0.04 0.03 0.05
#> s3 0.05 0.05 0.80 0.10
severity = peter("Rate this event's severity.",
c(a = "Mild headache", b = "Anaphylaxis requiring adrenaline"),
model = judge, levels = c("mild", "moderate", "severe"))
severity
#> a b
#> 0.20 (conf 0.85) 1.88 (conf 0.90)
#> judge-s1-1.0 . calibration unknown . 2026-10-15
gptr_prob(severity, "probabilities")
#> mild moderate severe
#> a 0.85 0.10 0.05
#> b 0.02 0.08 0.90
```
`gptr_choice` is character; `gptr_prob(tissue)` gives the probability of the selected label.
`gptr_score` is numeric, the expected **zero-based** level: here mild = 0, moderate = 1,
severe = 2. The example scores are 0.20 and 1.88, not rounded labels. For scores,
`gptr_prob(x)` gives confidence; use `"probabilities"` to inspect the distribution over
levels. Choice and score confidence are provider-defined measures of concentration, not
established probabilities that the answer is correct.
## Provenance and caching
``` r
meta = attr(is_rct, "meta")
meta[c("model", "engine", "calibrated", "execution", "locality", "cached")]
#> $model
#> [1] "judge-s1-1.0"
#>
#> $engine
#> [1] "fake"
#>
#> $calibrated
#> [1] NA
#>
#> $execution
#> [1] "native"
#>
#> $locality
#> [1] "local"
#>
#> $cached
#> [1] FALSE FALSE FALSE
```
The metadata also includes usage, request IDs and, when available, a model digest, server
version and calibration provenance. `calibrated = NA` means unknown; native decision models
do not become calibrated merely by returning probabilities. Emulated answers have
`calibrated = FALSE`. Slicing a typed result keeps its probabilities aligned.
Matching answers are cached per input and question. With a `.gptr/` workspace the System 1
cache is durable; without one it lives under the session's temporary workspace. Cache
identity also depends on the model and answer options. Changed model identity, live mode or
a missing entry can require a request. A mutable local model without verified immutable
identity cannot promise durable reuse. Use `gptr_cache()` to inspect the cache.
## System 1 routes or judges System 2
``` r
tasks = c("Rename a column.", "Choose a mixed model for repeated measurements.")
for (task in tasks) {
hard = peter("Does this task need the stronger model?", task, model = judge)
session = peter(task, model = if (isTRUE(hard)) strong else fast)
print(session$text)
}
#> [1] "A quick answer from the fast model."
#> [1] "A careful answer from the strong model."
session = peter("Summarize mtcars in one sentence.", model = summary_model)
session |> peter("Did the analysis succeed?", model = judge)
#> [1] TRUE (p=0.94)
#> judge-s1-1.0 . calibration unknown . 2026-10-15
session$turns
#> [1] 1
```
The piped classifier receives a bounded description of the session's status, designated
value and last answer. It adds no agent turn. Judging the answer text is not verification of
the calculations or saved artifacts; validate those in R.
## Native Jev, local Ollama and explicit emulation
The following examples can contact real providers and are not executed here. Jev reads
`TYPESAFE_API_KEY`; `gptr_env()` also recognizes `jev-key`, `JEV_KEY`, `JEV_API_KEY` and
`TYPESAFE_KEY` aliases in a key file.
``` r
gptr_env("~/keys/jev.env")
peter("Is this a randomized trial?", abstracts, model = jev)
# Prepare an installed native decision model on your local Ollama server first.
peter("Is this a randomized trial?", abstracts, model = "ollama/clef-flash")
```
Native Clef and Clef Flash use Ollama's System One API and require server 0.35.1 or later.
Their local-only preflight checks the endpoint and discovered model evidence; an unavailable
or unsuitable model fails instead of silently falling back to a cloud model. Ollama chat
models use a different adapter and return agent sessions.
Emulation uses structured output from a generative model. It must be enabled explicitly in
settings; writing an `emulate:` model name alone does not enable it.
``` r
gptr_config(system1 = "emulate:anthropic/claude-haiku-4-5", .scope = "session")
peter("Is this a randomized trial?", abstracts,
model = "emulate:anthropic/claude-haiku-4-5")
```
Emulation is a model request, is marked uncalibrated, and does not create native decision
capabilities for a chat model. Select a native model when the workflow requires native
probabilities, and assess calibration on the data relevant to that workflow.