26 AI in One Call
This chapter is the shortest path from an installed package to an AI-assisted analysis. It is written for someone who has never used the AI layer and does not yet care how it works. Read it once, run the two commands, and come back for the rest of the part when you want to know what happened.
Everything here is real: no chunk is \dontrun, and the ones that call a model are gated the same way as the rest of the book, so on a machine with no provider they are skipped rather than failed.
26.1 What you need first
The AI layer is optional. One package, installed once:
scletAI() checks this before anything is sent, so a missing package is an
error that names the command rather than a plan that reaches a provider and
fails there.
And one provider key in ~/.Renviron, which R reads at startup (restart afterwards):
sclet never asks for the key and never sees it: aisdk reads it from the environment and the model provider bills the call. A call is also non-deterministic — the same goal can produce a different plan — which is why the layer records what it did rather than trusting what it said.
26.2 The one call
# `conflicted` refuses a `library()` while a name exists in several attached
# packages. The book's setup chunk prefers base's `intersect`/`union`/`setdiff`
# BEFORE the later chapters attach GenomicRanges (via SVP), and a preference
# does not apply retroactively to a package attached after it was set -- so it
# is set again here, where the AI path begins. Same choice, one line.
for (fn in c("intersect", "union", "setdiff")) {
try(conflicted::conflicts_prefer(fn, base::get(fn), .quiet = TRUE), silent = TRUE)
}
set.seed(42)
sce <- scuttle::mockSCE(ncells = 200, ngenes = 80)
sce <- SingleCellExperiment::SingleCellExperiment(
assays = list(counts = SummarizedExperiment::assay(sce, "counts")),
colData = S4Vectors::DataFrame(SummarizedExperiment::colData(sce))
)
sce <- NormalizeData(sce)
sce <- FindVariableFeatures(sce)
sce <- ScaleData(sce)
dim(sce)## [1] 80 200
Now describe what you want. scletAI() is the whole API:
## sclet beginner AI analysis [invalid_plan]
## Goal: cluster the cells and find markers for each cluster
The plan is printed, then the session asks [y/N]. In a script, pass
confirm = "yes" to run without prompting, or confirm = "no" to see the plan
and stop. On a machine with no provider this chunk is skipped by the same switch
as the rest of the AI material, and the chapter still renders.
26.2.1 What came back, and what it means
analysis$status is the one field to read. Three outcomes, all of them normal:
| status | what happened | what to do |
|---|---|---|
completed |
every action ran and was recorded | read analysis$object — the clusters are on it |
dry_run |
the plan was previewed, not executed (you said no, or the session is non-interactive) | run again with confirm = "yes" |
invalid_plan |
the engine refused the plan | read analysis$report$errors; each names what to fix |
The refusal case is not a bug and not a misconfiguration: a language model proposes a plan, the engine checks it against the live object, and a plan that does not survive is refused before anything runs. That is the design — the alternative is an analysis whose steps were never checked. It is also non-deterministic: the same goal on the same object can propose a different plan on a different day, so a refusal is a retry, not a verdict.
## [1] "invalid_plan"
When it completes, the updated SingleCellExperiment is in analysis$object —
the clusters and marker table are on it, exactly as if you had called
FindClusters() and RunDEtest() yourself:
if (identical(analysis$status, "completed")) {
length(unique(as.character(analysis$object$ident)))
} else {
"no clusters: the plan did not complete"
}## [1] "no clusters: the plan did not complete"
26.3 What just happened
scletAI() is one call, but it is not one mechanism. Underneath, in order:
- A plan was composed. A language model was given the object’s recorded state — what analyses exist, what assays are active, what is blocked — and asked for steps. It was never given your expression matrix.
- The plan was validated against the live object. Every action had to be a real registry key, every parameter a real one for it, every dependency satisfiable. An illegal plan is refused here, before anything runs.
- You approved it. Confirmation is a token minted for that exact plan and object state; a plan edited afterwards is refused rather than silently run.
- It executed one action at a time, each result recorded as an evidence node.
- A report was rendered from the ledger and re-checked — which is why the numbers in it can be recomputed rather than retold.
The rest of this part is those five steps in detail, plus the route where you write the plan yourself.
26.4 The three other calls you will meet
scletAI() is the door; these are the rooms, in the order a user meets them.
26.4.1 Ask it something, without running anything
## sclet AI result [copilot]
## Only one recorded analysis exists in the ledger: `preprocess::normalize_logcounts` (status `completed`, method `scuttle::calculateCPM + log1p`, inputs `counts`, scale factor 10000, 200 cells × 80 features), recorded as a `state` record with no parents (root of the recorded chain). A `scaled` assay/layer exists and is the active view with `last_command = "ScaleData"`, but no `ScaleData` record appears in `analyses`, `state_records`, `workflows`, or the timeline (timeline `n_steps = 1`), so scaling is visible in the object but undocumented in the ledger. `features.hvg` is reported (n = 2000, method `scran`, rowData columns mean, total, tech, bio, p.value, FDR), which conflicts with the 80-gene matrix and should be treated as unverified. Not present: reductions (`reductions = []`), graphs, clustering/identity levels (`n_levels = 0`), marker/DE tests, and any `ev:` evidence nodes. Blocked gates: trajectory (`reduction_missing`), differential abundance (`design_role_missing`, needs `ConfirmAIDesignSemantics()`), velocity (`spliced_unspliced_missing`), spatial (no spatial record), multimodal (no altExp). Design semantics are unconfirmed; `Treatment` is only a candidate condition column. The ambient-RNA screen label `low` (low-UMI tail share 0.0718, top-gene detection uniformity 0.7271) is a screen, not a measurement — no contamination estimate exists absent a `run_decontx` evidence node. `execute_analysis` capability is unknown and `available_actions` is empty. No causal or biological claims are supported. Recommended order: reconcile record vs object (scaled layer, HVG provenance), confirm design semantics, run descriptive QC spine on counts/logcounts, then run_pca → find_neighbors (→ optional clustering), then marker tests within clusters and condition contrasts only after design confirmation; optionally run_decontx if ambient contamination is suspected.
## Findings:10 Recommendations:6 Proposed actions:6
AskAI() is read-only: it answers from the recorded state and cannot change the object. It is the right call when you want to understand what you have before deciding what to do.
26.4.2 The same run with the switches visible
## sclet beginner AI analysis [completed]
## Goal: cluster the cells
## Actions: run_pca -> find_neighbors -> find_clusters
## Report self-check: passed
RunAIAnalysis() is the engine scletAI() calls, with its arguments exposed: include to choose which action groups the planner may use, record to decide whether the round is written to the ledger, render_report and verify_before_continue for the self-check. Reach for it when the defaults are not what you want — not first.
26.4.3 Read a finished round back
## <sclet trace graph> sclet-trace-graph/1
## rounds: 0
## nodes: 1 | edges: 0
AnalysisGraph() projects the round onto two tables: which step produced which record, and which step consumed which earlier step’s output. It is how you audit a run you did not write, and how an agent picks up where the last one stopped.
26.5 When it refuses
Refusal is the layer working, not failing. The three you will meet first:
| what you see | what it means |
|---|---|
sclet_ai_missing_dependency |
aisdk is not installed; the message names the command |
sclet_ai_missing_model |
no model could be resolved; supply one, or set a default |
plan_empty / action is not registered |
the model proposed nothing, or something that is not an action — the engine refused rather than guessing |
A provider error (a 401, a rate limit) is the provider’s, and arrives verbatim; sclet does not translate it.
26.6 What to read next
- Driving sclet from an External Agent — the route where you write the plan yourself, and the one most users end up on.
- AI Foundations — what an action declares, and what the registry exposes.
- Evidence Governance — why a report may only claim what the ledger supports.
The short version: scletAI() to start, AskAI() to understand, RunAIAnalysis() when you need a switch, AnalysisGraph() to check.