gptr can keep the code an agent executed beside the prompt that
requested it. In a designated R script, the code goes below the owning
peter() statement between marker comments; in a report or
notebook, it becomes a chunk or cell. A fresh recorded block can run
again as R without another model request. Replaying still executes its R
code, with the effects and input dependencies that code has.
The history keeps code and bounded output, rather than archiving every input object. Provide those objects in the replay environment; working on data already loaded in R does not require an intermediate dataset file. See Execution model for object lifetimes and the separate costs of checkpoints, workers and snapshots.
The examples below are recipes for your configured model. Run recording and file-writing steps deliberately in a temporary project.
Write a prompt for each step. A pipe continues the same session, so the next prompt can use the objects created by the previous one. Keep prompts on separate lines and put short notes where they help explain the next step.
library(gptr)
cars = datasets::mtcars
session = peter("Fit mpg ~ wt in cars with stats::lm(); store fit.") |>
# Reuse the fitted model.
peter("Use fit to predict mpg for every row of cars; store predicted.") |>
peter("Summarize observed mpg minus predicted; store residual_summary.")
graphics::plot(cars$mpg, predicted)
session |> peter("Explain the largest residuals.")Prompts and ordinary R can alternate. A pipe adds turns to the same
mutable session; assigning the session to another name does not make a
branch. Use gptr_fork() for a branch.
peter() is an R function. Give it an environment when
results should outlive a helper call:
rank_cars = function(data, envir) {
peter("Find the three most fuel-efficient cars in data; store top.",
data = force(data), envir = envir)
}
work = list2env(list(data = mtcars))
session = rank_cars(work$data, work)
work$top
summary(session)data = force(data) supplies model context; it does not
create an R binding in work. Here work$data is
prepared before the call, so the R tool can read it. Objects attached by
name must be visible from the evaluation environment.
summary(session) reads recorded turns without a model call;
it is not a narrative or an executable workflow.
A call nested inside a function, loop or another expression does not own a document block. Running that code again may make another model request. Keep recordable prompts at top level and reusable R calculations in functions. System 1 cache hits do not make an arbitrary loop into a recorded workflow.
gptr_doc(path) binds a document and supplies explicit
consent to write blocks into it. gptr_doc() reports the
binding; gptr_doc(FALSE) removes it. It does not create a
workflow from past conversation turns.
Save this as analysis.R in a temporary directory:
library(gptr)
cars = datasets::mtcars
session = peter("Find the three most fuel-efficient cars in cars; store top.")
topThen run it from a separate driver or at the console:
gptr_doc("analysis.R")
work = new.env()
gptr_source("analysis.R", replay = "auto", envir = work)
gptr_blocks("analysis.R")
gptr_doc(FALSE)A missing block makes a model request, and the executed code is
recorded below the call: of an r call that stopped
part-way, the expressions that ran, and code that changed your objects
even where the model passed record = false, which leaves
out only inspection. Only blank lines may separate a call from its
block: after a comment line in between, the call is asked again and
writes a new block, while the old block’s code still runs. The header
identifies the block, model, date and prompt hash; the body hash
identifies its recorded code. ## Decision: retains a
model-supplied note, and #> retains bounded output. A
note is a model explanation, not proof that the code is correct.
## Steer: and ## Follow-up: lines keep what
was sent while the call ran. A replayed session’s history includes them,
and a gptr_steer() or pipe statement that sends one again
is not sent twice. A comment line of the model’s code that reads as one,
or as a block marker, is recorded with one more #, so it
never becomes your steer or ends its block.
The document must be writable, and recording needs consent.
gptr_doc(path) supplies that consent explicitly;
record = "off" prevents automatic recording into discovered
documents. To opt out for one call, including a call in a bound
document, pass .opts = list(record = FALSE). Pass it to
each call in a pipe chain that should not record.
record = TRUE in .opts keeps the usual consent
checks; it does not supply consent. Without a binding, source discovery
can find a call in a document. At an interactive console, agreeing to
transcript recording creates a script under
.gptr/transcripts/; binding an .R file with
gptr_doc() makes that file the transcript
(format = "transcript" only labels the binding). A
transcript holds what passes through gptr: each peter()
call typed at the console and each prompt of the peter()
console as
s_<id> = peter("prompt", <context names>) with
its block, the console’s !, !! and fenced R
lines under # direct R (no model), and slash commands as
comments. A statement also names model = "..." and
mode = "..." when its turn ran on another model or in
another mode than the statement would get without them (after
/model, /mode, the plan menu, or in a console
started as peter(mode = plan)), so
gptr_source(replay = "live") asks those turns with that
model and mode; the others use the model and mode their session has from
the turns before, else the model and mode
settings of the session that re-asks. A named model must be available
there; gptr_source(model =) asks every turn with your own.
A run that was aborted or stopped early (an error,
max_turns, a blocked call or the budget) is recorded too,
with a last ## Run: <status> line, and the next
prompt continues its session. Other code typed at R’s own
> prompt is not recorded, so a transcript whose turns
use objects made there fails on replay; create such inputs with
!code in the peter() console.
A console turn’s block opens with #< comment lines:
what the console sent with the prompt, the head of each
@file and the notes of earlier ! lines.
gptr_source(replay = "live") sends them again instead of
reading the file, so a re-asked turn is the same request even after the
file changed or was removed. Only these lines at the top of the block
count: a line of the model’s code that looks like one is recorded as
##<, so it is never sent as your context. A re-ask does
not send the turn’s ## Steer: and
## Follow-up: lines or the text of an approval answered
n <text>, and the new block does not keep them: put
what the turn needs in its prompt. Each block header and each direct R
header names what that step created (created=). After each
/undo, /redo, /retry and new
! line, gptr reads the transcript from the top and turns
off a direct R line that uses an object last created above it by an
undone turn or by a line that is off (a whole fit = ...
makes fit again and is no use of it): each of its lines
gets #~ under
# direct R (no model; uses undone fit), and the console
says which lines it turned off. A /redo turns them back on.
source() then does not stop at such a line. A comment in
! code that looks like a console line, such as
# /tmp is scratch, is recorded with one more
#, so it goes off with its line, and so is one that looks
like a block marker, so the transcript keeps recording; a block whose
code holds a marker line inside a string is not written, and the console
says so. Your own objects stay in the session: an undo removes what the
turn created and restores what it changed. So a later turn that uses an
object of a line that is off still stops source(), as turn
blocks are never turned off, and a direct R line that read an object the
turn changed (n = nrow(dat) after the turn cut
dat) stays on and replays with the restored value. Names
are read with all.names(): a column or formula term named
like such an object can turn a line off, and a name reached through a
string, as in get("fit"), is not seen.
Recording also needs the agent’s code to run. In the default
manual mode, code that changes objects, such as storing
top above, waits for approval. Nobody can approve under
Rscript, or in rmarkdown::render() even from
the console, so the run stops with gptr_error_permission
and no block is written. For such a run, set auto mode for this R
session first, pass mode = auto to the call, or add
gptr_permissions() rules. Replaying a fresh block needs no
approval.
Main-session recorded code runs as ordinary document code on replay.
Keep its input objects in the document’s evaluation environment, using
peter()’s default caller environment. An arbitrary custom
envir is not restored or applied to the following recorded
chunk. For a function that works in a separate environment, opt out with
.opts = list(record = FALSE) and keep its executable steps
explicitly in the function. Forks and team children have their own
recorded overlay wrappers.
gptr_doc("analysis.R")
listing = gptr_blocks("analysis.R")
listing[, c("id", "status", "model", "date", "tokens", "cost")]
gptr_source("analysis.R", replay = "replay", envir = new.env())
gptr_cache()
gptr_doc(FALSE)gptr_source() executes .R files
only, expression by expression. It can regenerate a stale block
and skip its old code in the same run, which base source()
cannot do. Its return value is a block listing with the action taken for
each block. gptr_blocks() reads R, R Markdown, Quarto and
Jupyter formats; pending writes can be recovered before listing.
| Replay mode | Behavior |
|---|---|
"auto" |
Replay fresh blocks; run missing or stale calls through the selected model. |
"replay" |
Never call a model; a missing or stale record signals
gptr_error_not_recorded. |
"live" |
Ask the model again and rewrite recorded blocks when writing is
allowed, under gptr_source(), knitr or Quarto, an IDE or
Jupyter; under base source() or Rscript a recorded block
replays with a warning. An undone block runs without being rewritten,
and "record" regenerates it. |
"record" |
Regenerate stale blocks; fresh blocks replay. |
A call’s replay = argument wins over process options,
the GPTR_REPLAY environment variable and settings. In any R
front end, use gptr_source("analysis.R", replay = "replay")
to run a recorded script without model calls. From a Unix shell:
This checks that the requested calls can use their records; it does not certify the analysis, freeze package versions, capture external datasets or make file/network operations in the recorded R code safe to repeat. Verify data paths, dependencies, outputs and side effects.
gptr_blocks() marks a block fresh,
stale, user-edited or undone.
Changed prompts or interpolated values can invalidate replay; the
listing alone cannot evaluate runtime interpolation. Blocks are
independent: when a stale turn is regenerated, the later turns of its
session stay fresh and replay, so ask a later turn that depends on it
again, for example with replay = "live" on that call.
Hand-edited code is recognized by its changed hash. Undone code stays
visible as inert comments. Keep the associated answer cache if the
replayed calls need their text or values; the code block and the cache
are different parts of the record.
gptr_cache() reports System 1 and System 2 answer
caches, temporary spill files and pending sidecars.
gptr_cache("prune") removes entries no longer in use or
beyond their retention window; gptr_cache("clear", "s2")
removes System 2 answers. Clearing answers can change what is available
during replay. Pending document sidecars are not deleted by cache
cleanup; apply them with gptr_doc(path, sync = TRUE).
In .Rmd and .qmd, a recorded block becomes
a chunk labeled gptr-<id> after its calling chunk. A
binding belongs to the R process that made it.
rmarkdown::render() knits in the calling R session, so a
binding made there covers the report:
gptr_doc("analysis.Rmd")
rmarkdown::render("analysis.Rmd")
gptr_blocks("analysis.Rmd")
gptr_doc(FALSE)quarto render, quarto::quarto_render() and
RStudio’s Knit and Render buttons run the document in another R process,
which a binding made at the console does not reach: nothing is recorded
and nothing is said. Bind such a document in its own setup chunk, then
render it as usual:
gptr_source() is not a renderer for either format.
Arrange the document’s setup to load gptr, prepare its input data and
select a model or replay mode. Knitting cannot ask for approval, so
allow the agent’s code as described for Rscript above.
Plain R chunks still execute when the report is rendered.
For Jupyter, bind the saved .ipynb path in the R kernel.
Blocks become agent cells. Writes are queued while the notebook is open
to avoid competing with the notebook editor. The queue is kept in the
project’s .gptr/ or, outside one, in
tools::R_user_dir("gptr", "cache"), so after closing the
notebook any R session can apply it and report how many blocks it
wrote:
gptr_doc("analysis.ipynb")
# Run the notebook's `peter()` calls in its R kernel, then save and close the notebook.
# In another R session:
gptr_doc("analysis.ipynb", sync = TRUE)
gptr_blocks("analysis.ipynb")
gptr_doc(FALSE)A notebook edited since a pending block was queued may need conflict resolution. Reopening or listing a file is not proof that pending blocks were applied successfully.
A harness made with gptr_tailor() or
gptr_coach() is called like peter(), and its
top-level calls are recorded the same way. Their header adds
harness=<name>:<hash>, the coaching the call
used, and the block goes stale when that coaching changes. A top-level
statement h = gptr_coach(...), whose harness =
is a name or left out, gets a block with kind=coach. Its
body is one statement that rebuilds the result,
h = gptr_tailor(<start>, coaching = list(...)), or
h = gptr_tailor(<start>) when no new coaching was
kept. One ## Run line per run and a
## Decision: line follow. Replay rebuilds h
from that text with no model, no key and no extra R process. Under
replay = "auto", a stale coach block asks before coaching
again when you are there, and replays otherwise. The block never holds
your data, transcripts, values or check, but the coaching
may mention facts from your data, so read it before you commit the
script. gptr_export() refuses a document with a coach
block. See Harnesses and
coaching.
gptr_export() reads a synchronized .R
history and writes a new .R file without running it or
asking a model. Its default format = "r" turns
peter() calls into comments and keeps the recorded R
executable. With format = "peter", calls remain active and
the previous recorded code becomes comments, so that code is not
executed a second time. Ordinary user code and comments retain their
source order. Choose a new .R destination in an existing
directory, different from the source; existing paths are never replaced.
Export also refuses a source that changes during publication.
For example, save the following as analysis.R. With a
configured model and the usual permission checks, its first three
prompts steer one session; the fourth starts another. The ordinary plot
runs between them, using objects in the same R environment.
cars = datasets::mtcars
session = gptr::peter("Fit mpg ~ wt in cars with stats::lm(); store fit.") |>
# Reuse the fitted model.
gptr::peter("Use fit to predict mpg for every row of cars; store predicted.") |>
gptr::peter("Summarize observed mpg minus predicted; store residual_summary.")
# User code and this comment stay in the exported source.
graphics::plot(cars$mpg, predicted, xlab = "Observed mpg", ylab = "Predicted mpg")
graphics::abline(a = 0, b = 1)
session = gptr::peter("Check predicted length and finite values; store checks.")Record it deliberately, inspect the code and results, then synchronize before exporting:
work = new.env(parent = globalenv())
gptr_doc("analysis.R")
gptr_source("analysis.R", replay = "record", envir = work)
work$residual_summary
work$checks
gptr_doc("analysis.R", sync = TRUE)
gptr_export("analysis.R", "analysis-raw.R")
gptr_export("analysis.R", "analysis-peter.R", format = "peter")
gptr_doc(FALSE)The export operation creates no model request. Running the
format = "peter" export can create new requests; running
the raw export executes its recorded R and ordinary user code. Review
those effects, then validate the raw result in a fresh environment with
the intended inputs. For this example, input preparation is already in
the source. For an analysis that uses a large object loaded earlier,
supply that object in the new environment; exporting does not serialize
the dataset or make an intermediate data file.
In the format = "peter" output, recorded Decision and
Steer notes and bounded #> output follow each operand of
this separate-line pipeline. Other layouts keep feedback after the
complete statement. Notes and output are comments, not assertions that
the analysis is correct. Each block keeps at most twelve output lines
and eighty characters per output line.
Export requires fresh completed records and no pending sidecar. Each
argument of an exported peter() call must be a quoted
string, a number, TRUE, FALSE or
NULL (not NA or a negative number), or after
the prompt an object without an argument name, as in
peter("Fit mpg on wt.", mtcars) or a console turn’s
@fit. Any other argument is refused as a “mixed or
non-literal Peter expression”: a bare name such as
mode = auto or model = opus, or a call such as
.opts = list(max_turns = 4). A pipeline written as one
statement, such as the first session above, is admitted,
and so is a later session |> peter("...") that continues
a session an earlier peter() statement assigned, as the
later turns of a console transcript do. Write the value itself, as in
mode = "auto"; editing an argument this way keeps its block
fresh, so the block needs no new recording. Export also refuses computed
or nested prompts, prompts that interpolate {name} values
(“interpolated inputs need review”), mixed peter()/ordinary
pipelines, shared-line statements, missing, stale or edited blocks and
secret-marked records; it leaves out the block of a console turn that
/undo or /retry undid, and in both formats the
#< lines a turn was sent with, so an exported call is
asked without them. A raw export refuses later ordinary code that reads
a session handle, such as session$value or
session$text, and a function that names one, wherever it is
defined; put needed results in ordinary R objects, or retain the
format = "peter" output. Earlier unrecorded console input
is not reconstructed. Preserved user code can still require gptr or
other packages, read or write files, or call the network. Export is a
source transformation, not verified replay.
Choose the deliverable explicitly. A history document, an ordinary
executable script and a narrative summary have different roles.
gptr_export() transforms an explicitly recorded R document;
it cannot recover arbitrary conversation turns into a complete
workflow.
With your configured model, continue session to request
a narrative:
session |> peter(
"Summarize completed steps, decisions, inputs, outputs and open issues.
Separate proposed work. Name saved files only when verified.",
mode = plan
)
session$textThe answer is model-generated text and adds a turn
to session. Check its claims against the workspace, files
and history before treating it as a record.
gptr_usage(session) shows the request’s usage.
For a history document, designate the target before recording top-level prompts, include ordinary R setup and checks, and inspect the blocks and replay behavior. To assemble one from earlier work, ask the model to draft it and review the draft; earlier unrecorded turns are not automatically recovered into validated blocks.
For an ordinary R script, request a separate, explicit implementation:
session |> peter(
"Write ordinary R for the verified analysis, including inputs and output saves.
Do not call Peter. Identify missing inputs or steps instead of inventing them.",
mode = plan
)
session$textThis returns model-generated output, not an exported or validated script. Save only reviewed code, then run it in a fresh environment with the intended inputs and verify its outputs.
See Typed decisions for typed decisions and Teams and background work for orchestration and SDK control.