Actors, Sessions and Turns
actor({ id, run, idleTimeout?, queue?, maxDuration?, ttl?, retry? }) defines
execution. Inputs, messages, outputs and results are application-defined JSON.
Validate with Zod or another library in your code where needed; Actor definitions
do not contain schemas or automatically infer external client payload types.
import { actor } from "@helmr/sdk"
export const reviewer = actor({
id: "reviewer",
async run(session) {
for (;;) {
const turn = await session.receive()
if (turn === null) return
await turn.output.write({ received: turn.input })
await turn.complete()
}
},
})
actor.start({ workspace, key?, idempotencyKey?, run?, signal? }) returns
{ session, run }. Start does not accept initial input; enqueue work afterwards.
One Run can process multiple Turns. A Run is execution, a Turn is one queued unit
of work, and the Session is their stable interaction address.
Inside an Actor
session.receive({ timeout?, idleTimeout?, metadata?, tags? })returns a Turn, ornullafter a closing Session has drained. Timeout rejects; it is not a successful empty receive. The Actor idle timeout controls the warm wait before managed suspension, not the Session lifetime.- A Turn has
id,sequence,input,source,createdAt, andsignal. await turn.onMessage(handler)registers one sequential handler and acknowledges readiness. Each callback receives{ id, data }; application fields such asdata.typeare entirely application-defined. Await readiness before publishing a question or approval request. ThrowMessageRejectedfor a known rejection; an unexpected handler failure produces an unknown message outcome.turn.output.write(value, { idempotencyKey? })appends durable output associated with the Turn.turn.output.pipe(iterable)consumes a finite stream. Neither operation completes the Turn.session.output.write/pipepublishes explicit Session-level output outside Turn ownership.await turn.complete(result?)orawait turn.fail(error)explicitly settles the Turn after started output and admitted callbacks drain. No-result completion omits the result; completing withnullrecords JSON null. Returning fromrun, requesting the next Turn, or reaching stream EOF is not implicit completion.
Keep post-processing and deterministic tests inside the Turn before completion.
Use turn.signal for Turn-owned cancellation, and retain native provider SDK
behavior in application code. Helmr does not supply an onTurn shortcut, an
agent adapter requirement, or a dedicated ask/approval primitive.
Outside an Actor
Use client.actors.start(declaredId, request) or client.sessions.ref(id):
const session = client.sessions.ref(sessionId)
const turn = await session.enqueue({ issue: "APP-42" }, {
idempotencyKey: "linear:event-123",
})
// In a later request, once this Turn is active:
await turn.send({ type: "update_constraints", allowDependencyChanges: false })
const state = await turn.retrieve()
const page = await session.events.list({ after: 0, limit: 100 })
enqueue returns a TurnRef and always creates FIFO work. send returns
{ kind: "enqueued", turn } or { kind: "messaged", turn, message }: it sends
to the active Turn before settlement, even before its handler is registered, or
joins the FIFO queue when idle. Accepted messages retain that exact target and
are delivered when its handler is ready; they never fall back to a different Turn.
session.turn(id).send(data) always targets that exact active Turn and returns
a message receipt, not proof of application or provider handling. Held Sessions
reject send; enqueue can still add work to an open held Session.
events.list({ after?, limit? }) returns records, nextAfter, hasMore, and
retainedAfter. All output and lifecycle events share one sequence. Records carry
kind, data, nullable turnId, and nullable Run provenance. Keep a cursor per
consumer, and deduplicate remote effects using event IDs. A finite page is not a
completion signal: inspect terminal Turn events or turn.retrieve().
turn.complete(result?) and turn.fail(error) commit the logical outcome without
capturing the Computer. Their terminal records and Turn views do not attach a
disk version. Disk preservation and restoration follow the Computer lifecycle;
local files can be newer than the last saved disk even after a Turn completes.
turn.interrupt() returns a stop receipt and hold identity. Acceptance may precede
physical convergence. Queued work stays retained. session.resume({ holdId })
releases that exact converged hold; it never replays the interrupted Turn.
session.cancel() rejects new input, records queued Turns as cancelled, and
requests interruption of active work. It returns an acceptance receipt; poll
session.retrieve() for status: "closed" before deleting its Workspace.
cancelRequestedAt remains visible on the Session. An active Turn uses the normal
interrupted outcome after its stop and capture complete. Previously completed
Turns and output remain unchanged.
await session.cancel({ idempotencyKey: "stop-review" })
const state = await session.retrieve()
// An accepted request does not prove physical termination.
if (state.status === "closed") {
await client.workspaces.ref(state.workspaceId).delete()
}
Cancellation can escalate a Session already closing. It cannot be undone with
resume(). Closure waits until the old execution and its owned work are physically
excluded before releasing Workspace ownership. Cancellation also works through a
runtime Session reference inside an Actor or Task.
session.close() rejects new ordinary admission and drains accepted FIFO work.
An explicit interruption still requires resume({ holdId }); resuming an empty
Session leaves it idle until new input arrives. Session statuses are open,
closing, closed, and failed.
Helmr automatically restores the last committed Computer version after execution
loss, once the old writer is physically excluded. Unpublished files can be lost.
Completed Turn results remain; the interrupted execution’s active Turn fails and
its memory and uncertain callbacks are not replayed. Never-started queued Turns
retain their identity and order and can start in a new Actor Run. A
session.execution_lost event records the loss and selected saved version.
Repeated infrastructure loss or preparation exhaustion terminates the Session;
known Actor application or initialization failure also terminates it. Restoring
an environment does not prove whether an external API side effect happened.
Message acceptance does not wait for handler registration. During a managed Token
or child wait, messages remain pending until the original wait resumes; they do
not unblock that wait. Settlement rejects new sends with turn_settling and emits
rejections for accepted messages whose handlers never started. Acceptance is not
proof of application or provider handling. Explicit interruption requires resume; infrastructure recovery is automatic.