Build a durable Agent

This tutorial builds an Agent that remembers what happened earlier in its Session. It keeps a live object from init in memory and a log file on the Computer’s disk, then continues the conversation across several Turns, an idle period and an interruption. It needs no model provider.

You need a project created with helmr init and the CLI configured (see Quickstart). The examples use the demo Project’s staging Environment. For the guarantees behind each step, see Durability.

Define the Agent

Create agents/assistant.ts:

import { agent, template, image } from "@helmr/sdk"
import { appendFile, readFile } from "node:fs/promises"

const LOG = "/workspace/log.txt"

export const assistantComputer = template({
  id: "assistant",
  image: image("assistant").from("node:24-bookworm-slim"),
  resources: { cpu: 1, memory: "1GiB" },
})

export const assistant = agent({
  id: "assistant",
  template: assistantComputer,
  closeAfterIdle: "1d",
  hibernateAfterIdle: "5m",
  maxTurnDuration: "10m",

  async init(ctx) {
    // Runs once per Session process, before its first Turn.
    return { turns: 0, startedAs: ctx.recovery.kind }
  },

  async turn(turn, ctx) {
    const text = turn.input.map(part => part.text).join("").trim()
    if (!text) throw new Error("Send some text")

    ctx.initResult.turns += 1
    await appendFile(LOG, `${turn.sequence}: ${text}\n`, { flush: true })
    const lines = (await readFile(LOG, "utf8")).trimEnd().split("\n").length

    await turn.output.write(`Recorded "${text}"`)
    await turn.respond(`Turn ${ctx.initResult.turns} in this process. Log entries: ${lines}.`)
    return { turnsInProcess: ctx.initResult.turns, logEntries: lines, startedAs: ctx.initResult.startedAs }
  },
})

The Agent keeps two kinds of state:

  • ctx.initResult is the value init returned. It is an ordinary live object; it could just as well hold an open client or a child process. It lasts as long as the Session’s process.
  • /workspace/log.txt is a file on the Computer’s disk. Helmr saves the disk periodically while the Computer runs and after a Turn’s handler returns successfully. The Turn becomes completed when its result and response are committed; its disk save finishes in the background.

closeAfterIdle closes the Session after a day without work, hibernateAfterIdle hibernates its Computer after five idle minutes, and maxTurnDuration limits each Turn. See Agents.

Deploy and start

helmr deploy . --project demo --env staging
helmr session create assistant --project demo --env staging \
  --text "first" --key user:ada \
  --idempotency-key tutorial:assistant:1

Save the session_id from the receipt to enqueue more input. The Session key user:ada lets you find this conversation later; creating another Session with that key conflicts.

Wait for the Turn:

helmr session turn wait TURN_ID --session SESSION_ID --timeout 10m \
  --project demo --env staging

The response reads Turn 1 in this process. Log entries: 1.

Continue the Session

helmr session enqueue SESSION_ID --project demo --env staging \
  --text "second" --idempotency-key tutorial:assistant:2

Wait for the new turn_id. The response now reads Turn 2 in this process. Log entries: 2. The counter in initResult kept its value because init did not run again.

Leave the Session alone for more than five minutes, then enqueue third. After five idle minutes Helmr hibernates its Computer with memory, processes and disk, and the next input resumes it where it stopped. The counter continues at 3 and startedAs is still initial.

Follow progress at any time with:

helmr session events SESSION_ID --project demo --env staging

When the process is lost

If the Session’s process is lost while a Turn runs, that Turn ends interrupted and queued input runs on a new process. Helmr does not replay the interrupted input. After three consecutive process failures the Session is held; find the hold and resume:

helmr session get SESSION_ID --project demo --env staging
helmr session resume SESSION_ID --hold HOLD_ID --project demo --env staging

On the next input Helmr starts a new process on the same Computer and runs init again with ctx.recovery.kind set to reconstructed. In this Agent the counter starts again at 1, while the log file still has every entry the disk kept. That is the rule to design for: keep anything that must survive reconstruction on disk or in your own store, and make external effects safe to repeat. Losing the worker of a running Computer restores the latest eligible saved disk, which can predate completed requests. Helmr explains any gap and requires acknowledgement before continuing. See Durability.

Interrupt and resume

You can stop work yourself the same way:

helmr session interrupt SESSION_ID --project demo --env staging

Interrupt stops the running Turn, keeps queued Turns waiting and holds the Session. The receipt includes hold_id; resume with it when you want queued work to continue:

helmr session resume SESSION_ID --hold HOLD_ID --project demo --env staging

Resuming lets queued and new Turns run. It does not rerun the interrupted input; send it again if you still want it. See Sessions.

Finish

helmr session close SESSION_ID --project demo --env staging

Close finishes work already accepted and then ends the Session. It does not release holds; resume first if the Session is held.

Next steps

  • Start a model client or coding-agent process in init and reuse it in every Turn; see Agents.
  • Ask a person for input with turn.ask; see Answer questions.
  • Set up the Computer’s tools and files once with prepare; see Prepare a Computer.