Skip to contents

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. A procedure you can read in full is one you can audit.

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. That is the whole contract. 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:

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.

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.

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.

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.