Record, replay and export

gptr logo

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.

Program in natural language, then use ordinary R

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.

Calls inside functions

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.

Recording a designated R script

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.")
top

Then 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.

gptr_config(mode = auto, .scope = "session")

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.

Inspecting and replaying

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:

GPTR_REPLAY=replay Rscript --vanilla analysis.R

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).

R Markdown, Quarto and Jupyter

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:

# The setup chunk of analysis.qmd:
library(gptr)
gptr_doc("analysis.qmd")

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.

Harness calls and coaching calls

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.

Exporting a recorded script

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.

Asking Peter to summarize a completed workflow

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$text

The 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$text

This 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.