---
title: "Workflows: small, checkable, resumable pipelines"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Workflows: small, checkable, resumable pipelines}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include=FALSE}
knitr::opts_chunk$set(
  collapse = TRUE, comment = "#>",
  eval = identical(tolower(Sys.getenv("LLMRAGENT_RUN_VIGNETTES", "false")), "true")
)
```

Most agent work in this package needs no graph. `deliberate()`,
`agent_experiment()`, and the preset pipelines have their own interfaces, and
you should use them. The workflow runtime is for what those do not cover: a
procedure with branches, loops, a human checkpoint in the middle, or a run long
enough that you stop and resume it tomorrow. The runtime is small. A node is a
function of state; an edge is an optional condition; the state is a plain list
you can read. There is no distributed execution and no DSL beyond `add_node` and
`add_edge`. The graph records nodes, edges, state transitions, and checkpoints.

```{r setup}
library(LLMRagent)
cfg <- LLMR::llm_config("groq", "openai/gpt-oss-20b", temperature = 0.3)
```

## State in, state out

A node receives the shared state and returns a new state. Each step receives
the current state and returns the revised state. Here is a two-step procedure
that drafts an answer and then tightens it, with the state carrying the work
from one step to the next:

```{r linear}
wf <- agent_workflow("draft_then_polish") |>
  add_node("draft", function(state) {
    a <- agent("Drafter", cfg)
    state$draft <- a$reply(paste("Draft two sentences on:", state$topic))
    state
  }) |>
  add_node("polish", function(state) {
    a <- agent("Editor", cfg)
    state$final <- a$reply(paste("Tighten this to one sentence:", state$draft))
    state
  }) |>
  add_edge("draft", "polish")

run <- run_workflow(wf, input = list(topic = "why peer review is slow"))
run$state$final
```

`run_workflow()` returns a record of what happened: the sequence of nodes, the
state at each step, and a status. Because every node's state is hashed, the run
is checkable, which is what makes the next two features possible.

## Branches and loops

An edge can carry a condition. The runtime takes the first edge whose condition
holds, so you can branch on what a node produced, or loop until something is good
enough. Loops are bounded by `max_steps`: a runaway procedure stops rather than
spends, raising a typed error you can catch.

```{r loop}
wf2 <- agent_workflow("until_short_enough") |>
  add_node("write", function(state) {
    a <- agent("Writer", cfg)
    state$tries <- (state$tries %||% 0L) + 1L
    state$text <- a$reply(paste0("Write a one-line summary of: ", state$topic,
                                 " (attempt ", state$tries, ")"))
    state
  }) |>
  add_node("done", function(state) state) |>
  add_edge("write", "write", when = function(state) nchar(state$text) > 120 && state$tries < 3) |>
  add_edge("write", "done", when = function(state) nchar(state$text) <= 120 || state$tries >= 3)

run2 <- run_workflow(wf2, input = list(topic = "the history of the printing press"),
                     max_steps = 10)
run2$state$tries   # how many attempts it took
```

## Checkpoints, resume, and fork

Give `run_workflow()` a `checkpoint_dir` and it writes the state after every node
as it goes. If the run fails partway, from a network interruption or a model
timeout, you resume from where it stopped rather than starting over, and
completed nodes do not run again.

```{r resume}
dir <- file.path(tempdir(), "long_run")
run3 <- run_workflow(wf, input = list(topic = "open access publishing"),
                     checkpoint_dir = dir)
# had it failed, resume_workflow(run3, wf) would continue from the last good node
```

`fork_workflow()` branches an existing run at a chosen point, so you can try a
different continuation from a shared prefix without recomputing it -- useful for
asking "what if the editor had been stricter" without paying for the draft twice.

## Replay verification

A run records a state hash at each step. `replay_run()` re-executes the procedure
and compares. The comparison accounts for nondeterminism: a deterministic node
must reproduce its hash exactly, while a model node is checked for design
identity rather than identical text, because a model does not return the same
words twice. A divergence names the first step that differs.

```{r replay}
rp <- replay_run(run, wf, verify = "structural")
rp$steps[, c("node", "replay_match")]
```

Use `verify = "strict"` when the whole procedure is meant to be deterministic and
any difference is a problem. Use the default `"structural"` when model nodes are
involved and you want to confirm the structure of the run without demanding
identical wording. Either way, a tampered checkpoint is caught: the hash will not
match.

## When to reach for this

If `deliberate()` or `agent_pipeline()` already does what you need, use them; the
workflow runtime is not a replacement and `workflow_from_pipeline()` exists only
to show that the engine can express them. Reach for `agent_workflow()` when you
need a branch, a loop, a human gate, or durability across a long run. The added
setup adds checkpointing, resumption, forking, and replay verification:
a pipeline you can leave and return to, and one a reader can verify ran the way
you say it did.
