library(here) # manage file paths
library(socviz) # data and some useful functions
library(tidyverse) # your friend and mine
library(btw) # describe R things to LLMsModern Plain Text Social Science
Week 12
Duke University
November 2026
btwOn its own, the model cannot:
When a chat app seems to do these things, the work is being done outside the model.
You send a JSON request:
And you get JSON back:
Note the usage field. Tokens are the unit of both cost and capacity.
Ask a bare model, with no tools, to multiply two eight-digit numbers.
It will produce a number. The number will have the right number of digits, and the right first few digits. It will look entirely plausible.
[1] "1,711,835,094,393,372"
One bare model answered 1,711,067,239,026,172, which is close but wrong.
%>%, gather()/spread(), aes_string(), size for line widthsThe same underlying model behaves very differently in each, because the harness decides what the model sees and what it can do.
ellmer) or on a server (claude.ai).| Component | Provided by |
|---|---|
| Predicting the next token | Model |
| System prompt | Harness |
| Re-sending earlier turns | Harness |
Reading CLAUDE.md / AGENTS.md |
Harness |
| Describing the available tools | Harness |
| Actually running a tool | Harness |
| Asking you for permission | Harness |
Most of the differences you notice day to day are harness differences.
This course’s repository has an AGENTS.md file. Part of it:
# Constraints
- Some datasets used may be very large. Do NOT attempt to ingest them in
order to inspect them. Use the available local tools (R and its packages;
the package APIs that allow the data to be queried, your plugins and
skills, etc). You may extract very small samples (<1000 rows) from data
files if it's necessary.
- The user will handle git and other version control matters.The harness pastes this into the context at the start of every session, which is how “project memory” works.
read_file with path = 'R/clean.R'”.git status.sequenceDiagram participant U as You participant H as Harness participant M as Model U->>H: "Plot donors by year for each country" H->>M: prompt + tool definitions M->>H: tool request: describe organdata H->>M: tool result: columns, types, ranges M->>H: tool request: run this ggplot code H->>M: tool result: error, no column 'donor' M->>H: tool request: run the corrected code H->>M: tool result: success M->>H: final answer H->>U: "Here's the plot ..."
An agent is an LLM, in a harness, that calls tools repeatedly in a loop.
The loop: read, act, read again to check what happened, decide what to do next.
Here’s a calculator. It evaluates an expression in an environment that contains only arithmetic.
calculator <- function(code) {
math_env <- new.env(parent = emptyenv())
ops <- c("+", "-", "*", "/", "^", "(", "sqrt", "exp", "log")
walk(ops, \(op) assign(op, get(op, envir = baseenv()), envir = math_env))
eval(parse(text = code), envir = math_env)
}
calculator("20598162 * 83106206") |>
format(big.mark = ",", scientific = FALSE)[1] "1,711,835,094,393,372"
And because of how it’s built, it can’t do anything else:
Give the model this tool, describe it as “use this for any arithmetic, since you can’t reliably compute it yourself”, and it gets the multiplication right.
Here is one of the tools in the btw package, as the model receives it:
[1] "Get help page from package."
package_name
"The exact name of the package, e.g. 'shiny'. Can be an empty string..."
topic
"The topic_id or alias of the help page, e.g. 'withProgress' or 'inc..."
_intent
"The intent of the tool call that describes why you called this tool..."
That text is all the model knows about this tool, so the descriptions have to be written carefully.
dplyr you actually have installed.Of the four, verification matters most: a model that can check its own work is much more reliable than one that can’t.
air format .air”) is a request. A hook that runs air after every edit is a guarantee.Only 1, 2, and 6 are strictly essential. Claude Code’s built-in tools are essentially these, plus web search and fetch, and the ability to start sub-agents.
| Subscription | API | |
|---|---|---|
| You pay | A flat monthly fee | Per token, input and output |
| Limits | Usage caps that reset over time windows | Rate limits, plus spend limits you set |
| Credentials | Log in with your account | An API key, e.g. ANTHROPIC_API_KEY |
| Harness | Anthropic’s: claude.ai, Desktop, Claude Code | Anything: Claude Code, ellmer, your own |
| Good for | Interactive work | Programs, pipelines, batch jobs, apps |
ellmer::chat_anthropic() and btw::btw_client().~/.Renviron, never in a script.Bring the LLM to R
ellmer in your R session. R is the harness.Bring R to the LLM
This needs an API key, so we won’t run it today. The whole harness is about a dozen lines.
library(ellmer)
chat <- chat_anthropic() # needs ANTHROPIC_API_KEY
chat$register_tool(tool(
calculator,
description = "Evaluate a mathematical expression. Use this for any
arithmetic, since you can't reliably compute it yourself.",
arguments = list(code = type_string("An expression, e.g. '1 + 2 * 3'."))
))
chat$chat("What's 20598162 * 83106206?")
#> ◯ [tool call] calculator(code = "20598162 * 83106206")
#> ● #> 1711835094393372
#> 20598162 × 83106206 = 1,711,835,094,393,372.btw package“By the way, here’s some context.” A toolkit for describing R things to LLMs.
Three ways to use it:
btw() at the console: copies a description to the clipboard, for pasting into any chat.btw_mcp_server(): serves R tools to Claude Code, Claude Desktop, etc.btw_client(): an ellmer chat with btw tools. Needs an API key.Modes 1 and 2 work with a subscription.
btw() at the console## Context
organ_small
```json
{"n_cols":4,"n_rows":238,"groups":[],"class":["tbl_df","tbl","data.frame"],"columns":{"year":{"variable":"year","type":"Date","min":"1991-01-01","max":"2002-01-01","median":"1996-07-02","n_unique":12},"country":{"variable":"country","type":"character","min":5,"max":14,"empty":0,"n_unique":17,"values":["Australia","Austria","Belgium","Canada","Denmark","Finland","France","Germany","Ireland","Italy"]},"consent_law":{"variable":"consent_law","type":"character","min":8,"max":8,"empty":0,"n_unique":2,"values":["Informed","Presumed"]},"donors":{"variable":"donors","type":"numeric","mean":16.4848,"sd":5.1108,"p0":5.2,"p25":13,"p50":15.1,"p75":19.6,"p100":33.9}}}
```
Interactively, btw(organ_small) puts this on your clipboard, ready to paste into claude.ai.
btw() at the consoleYou can mix objects, documentation, and your question:
dplyr.btw as an MCP serverTell Claude Code about it, once, from the terminal:
Check that it’s connected:
Now, in every Claude Code session, the model can call R’s help system, inspect data frames, check installed packages, and search CRAN.
# A tibble: 12 × 2
group tools
<chr> <int>
1 agent 1
2 cran 3
3 docs 5
4 env 2
5 files 6
6 git 7
7 github 1
8 ide 1
9 pkg 5
10 sessioninfo 3
11 skills 1
12 web 1
docs, env, sessioninfo, and cran are a good start. Add pkg if you write packages.btw_tool_run_r is opt-in. Name it explicitly to let the model run R code.Ask a model about purrr::in_parallel(), added in mid-2025. Without tools, it may not know it exists, or may confidently invent arguments. With btw, it calls a tool instead:
#### Usage
``` R
in_parallel(.f, ...)
```
#### Arguments
##### `.f`
A fresh formula or function. "Fresh" here means that they should be
declared in the call to `in_parallel()`.
##### `...`
Named arguments to declare in the environment of the function.
env tools describe your data frames, and run_r runs in your session.list_r_sessions and select_r_session to find it./mcp.In your R console:
In Claude Code:
| Step | Tool call | Why it matters |
|---|---|---|
| 1 | select_r_session |
Attaches to your console |
| 2 | btw_tool_env_describe_data_frame |
Real column names, types, ranges |
| 3 | btw_tool_docs_help_page (ggplot2) |
Current arguments, e.g. linewidth not size |
| 4 | btw_tool_run_r |
Runs the code; sees warnings and errors |
| 5 | btw_tool_run_r (again) |
Fixes the problem and re-checks |
| 6 | Final answer | Code that has actually been run |
The plot appears in your session’s plot pane, because the code ran in your session.
pkg group, the model can document(), test(), and check() in the loop.btw command-line toolSome harnesses can’t use MCP, or you want to keep tool definitions out of the context. btw also ships a CLI.
Any agent with a shell tool can call it. The installer offers a skill telling the agent the CLI exists.
CLAUDE.md or AGENTS.md at the root of a project. Standing instructions: style rules, project layout, things to avoid.btw_skill_install_package() installs them; btw_skill_install_project() does it for all of a project’s dependencies.Hadley Wickham asked two coding agents in auto mode to install a tool via curl ... | sh, i.e. run a script from the internet without reading it.
The model’s judgment is not a security boundary.
.Renviron for keys, not scripts.duckdb, arrow, dbplyr), not read it.targets pipelines.btw: https://posit-dev.github.io/btw/ellmer: https://ellmer.tidyverse.org