NylorunDocsBeta
RunAgents

Agents

Make the Agent your sessions run on from a saved manifest, and move it to new versions in place.

import { Agent } from "@nylorun/agents";

const analyst = Agent({ id: "analyst" })
  .instructions("Analyse the data you are given with pandas, and show your numbers.")
  .build();

await client.agentManifests.save({
  agent: analyst,
  runtime: { kind: "virtual", network: { allowedHosts: ["pypi.org", "files.pythonhosted.org"] } },
});

const agentId = `users/${user.id}/analyst`;
await client.agents.put(agentId, { manifest: "analyst", owner: user.id });

const session = await client.createSession({ agent: agentId, ownerUserId: user.id });

An Agent is the live instance of one version of an agent manifest: one sandbox of the manifest's kind, one set of secrets for its shell, MCP servers, and HTTP tools, a disk, its memory, its triggers, and the sessions that run on it. Your app picks the id and decides what it serves: one session, one person (users/<id>/...), or one project (projects/acme). An id is up to 200 characters of /-separated segments of letters, digits, ., _, and -.

The manifest names no Agent, so one manifest makes as many Agents as you need. Making an Agent opens no session. Every session names its Agent with agent when it opens, and keeps it for the session's life.

Make an Agent

put creates the Agent from a saved manifest, or finds the one with that id, in one call:

FieldMeaning
manifestRequired. "analyst" tracks the manifest: its latest version now, and each version saved later. "analyst@3" pins version 3. An unknown name or version is 404 manifest_not_found.
ownerThe owner guard. null clears it; left out, it stays as it is.
labelsKey-value pairs to filter on, at most 32 by default. A put that sends them replaces them.
fromOn create only: { agent, version }, start the disk from another Agent's disk version.

The Runtime resolves the version's runtime section against the Tenant's limits and answers 400 with every problem it finds. The Tenant holds at most 100 Agents by default (409 limit_exceeded; see Kubernetes), and a pod Agent on a Runtime without sandbox pods is 409 agent_unavailable. Request and response shapes are on Create, find, or move an Agent.

On an existing Agent, put changes labels and owner, and moves the Agent to the version manifest names (see Versions). The same put works as get-or-create: with the bare name, a tracking Agent moves to the latest version it can run, and stays where it is when it cannot. A bare name on a pinned Agent makes it track again. Another manifest's name is 409 agent_fixed_field: fork the Agent instead.

Kinds

The kind is the manifest's runtime.kind:

KindBehaviour
virtualAn emulated shell and filesystem in the harness's container, with /workspace kept by the Tenant. Not an isolation boundary. Takes no image, storage, setup, or files; allowedHosts takes exact host names only.
podPreview. A container with its own volume on the Tenant's cluster, after npx nylorun local sandbox enable --context <ctx>. See Kubernetes.
noneNo compute: secrets only, for an agent whose tools are remote MCP servers and HTTP tools. No sandbox tools and no disk. A manifest with no runtime section makes this kind.

With a sandbox (virtual or pod), our harness gives the model bash, read, write, edit, grep, and glob on a persistent /workspace, and save_artifact. A custom harness offers its own. Shell credentials reach pods only: a virtual Agent gets their variables but no header.

npx nylorun doctor sandbox

doctor sandbox reports the sandbox backend in use and the Tenant's limits.

Versions

An Agent runs one version of its manifest at a time, resolvedVersion in its view, and either tracks the manifest or pins a version (tracking). It moves between versions in place, keeping its id, disk, secrets, memory, and sessions:

  • A put that names another version moves it: "analyst@4" pins version 4, "analyst" tracks the latest.
  • A save moves every Agent that tracks the manifest. A pinned Agent stays.
  • Each session's next turn runs the version the Agent runs then. A turn already running ends on its version. A flow session keeps the version it opened on.

Check a move first, then make it:

const plan = await client.agents.plan("projects/acme", { manifest: "analyst@4" });
console.log(plan.applies, plan.changes, plan.missingSecrets, plan.refusals);

const agent = await client.agents.put("projects/acme", { manifest: "analyst@4" });
console.log(agent.resolvedVersion, agent.targetVersion, agent.waitingFor);

plan() changes nothing (PUT /v1/agents/{id}?dryRun=true). Its applies says when the version takes effect:

appliesWhen
noneThe Agent already runs that version.
nowTurns that start from now run it: a harness section change, any change on a virtual Agent or one of kind none, or a pod change to fields other than those below.
next_startA pod's image, resources, network, setup, or files changed: its pod's next start applies them, on the same volume. Until then the view shows targetVersion and waitingFor: "start", and turns run resolvedVersion.
secretsThe version adds a required secret slot with no value (missingSecrets). It applies once you write the value; until then waitingFor: "secrets" and the state is input_required.
refusedThe version changes what is fixed for the Agent's life (refusals).

The plan also lists the changes by section, dormantSecrets (secrets the version does not declare, kept and unused while it runs), and how many sessions take the version at their next turn.

client.agents.restart(id) applies a version a pod waits to start on now: a running pod stops, and the next turn starts it on the new version; a stopped one starts at once. A start that fails (its setup, the pod, or a start that takes too long) goes back to the version the Agent ran, with its disk as it is: the view's failedVersion names it, the Agent gets an agent.start_failed event, and its state is failing. A tracking Agent does not move to that version again on its own; restart(id, { retry: true }) tries it again. Request and response shapes are on Restart an Agent on its pending version.

The manifest's name, runtime.kind, harness.id, and runtime.storage are fixed for the Agent's life. A put that would change one is 409 agent_fixed_field, with each in details.fields; a save leaves a tracking Agent it cannot move on its version. To change them, fork.

npx nylorun agents put projects/acme --manifest analyst@4 --dry-run
npx nylorun agents pin projects/acme 4      # move to version 4 and stay there
npx nylorun agents track projects/acme      # move to the latest, and with each save
npx nylorun agents restart projects/acme    # --retry: the version that failed to start
npx nylorun agents list --outdated          # Agents moving to another version

Fork

A fork makes a new Agent from a manifest version, whose disk starts from the source's and whose secrets are copied inside the Runtime. It is how to change what is fixed for an Agent's life, or to keep a copy. Check the plan first:

const forkPlan = await client.agents.planFork("projects/acme", {
  id: "projects/acme-gpu",
  manifest: "analyst-gpu",
});
console.log(forkPlan.disk, forkPlan.secrets.copied, forkPlan.secrets.missing, forkPlan.changes);

const { agent } = await client.agents.fork("projects/acme", {
  id: "projects/acme-gpu",
  manifest: "analyst-gpu",
});
OptionMeaning
idThe new Agent's id. An id that exists is 409.
manifest"name" (tracking) or "name@n" (pinned). Default: the source's manifest, tracking its latest version.
disk"latest" (default): snapshot the source's disk and start from it, refused with 409 agent_busy while a turn runs. A disk version number, or "empty".
secrets"copy" (default): copy the secrets the new manifest declares, or every secret when it declares none. "none": copy none. Values never leave the Runtime.
labels, ownerThe new Agent's labels and owner guard. Neither is copied from the source.

planFork() makes nothing and answers the plan: the disk it starts from, secrets.copied, secrets.dropped (left behind), secrets.missing (required slots still missing), the changes between what the two run, whether setup runs, how many sessions stay on the source, and how many memories it copies. Memories are copied as they are, each at version 1, with no history. fork() answers the new Agent and the plan it ran.

The source keeps what it runs, its disk, and its sessions, and gets an agent.forked event. The new Agent's view has forkedFrom: { agent, disk }. A session's Agent is fixed, so open new sessions on the new id when you are ready. On a pod, a fork onto other setup commands runs them on the copied disk before its first turn, and a fork pulls the image afresh. Request and response shapes are on Fork an Agent.

npx nylorun agents fork projects/acme projects/acme-gpu --manifest analyst-gpu --dry-run

State

The view's state is the first of these that holds: paused, failing, input_required, working, or sleeping.

stateHolds when
failingIts pod failed or was lost, a start on a new version failed, or a session's latest turn failed.
input_requiredA session waits on a person (an approval, an answer, or an uncertain call), a move waits for a required secret, or a required secret of the version it runs has no value.
workingA turn runs.
sleepingNothing to do.

attention lists every state that holds, highest first, each with its reason and the sessionId behind it. A required secret slot with no value holds input_required until you write it, since a tool that uses it would fail when called: { state: "input_required", reason: "secret_unset: required secret ORDERS_TOKEN unset" }. compute.state is its sandbox's: ready, creating, running, or stopped. nextWakeAt is when its next trigger fires. Accept values you do not know in each.

const waiting = await client.agents.list({ state: "input_required" });
for (const agent of waiting) console.log(agent.id, agent.attention);

Shared disks and turns

Sessions on one Agent share its /workspace and run their turns at the same time, one turn per session. Which sessions work on one disk, and when, is your app's call: give work that must not share files its own Agent, or a fork. A flow's child sessions run on its Agent. Sandbox commands of different sessions run at the same time; each session's commands keep their order. A skill's files sit in a folder per version, /skills/<name>@<hash>/, so agents with different versions of one skill share an Agent.

Stop, restart, reset, snapshot, restore, and delete wait for the Agent's turns: while any turn runs they answer 409 agent_busy, as does a turn start while a snapshot or restore copies the disk. Retry after Retry-After.

Deleting a session only detaches it from its Agent. Deleting the Agent leaves its sessions' history readable and refuses their next turn (409 agent_unavailable) until an Agent with that id exists again.

Owner guard

With owner set, only sessions with that owner may run on the Agent. It is checked when a session opens and at every turn start:

CallerAnother owner's session gets
Application key403 agent_owner_mismatch
Acting for a person (client.as(), a trusted issuer's token)The 404 of a missing Agent

Set the guard on every Agent that holds a person's own token. Every session on an Agent gets all its secrets.

Access

An application key reaches every Agent. Acting for a person with client.as(), reading an Agent takes sessions:own, agents:read, or agents:write, and making, moving, forking, stopping, restarting, resetting, or deleting one, or writing its secrets or memories, takes agents:write. The view then lists only that person's sessions.

A trusted issuer's token reaches only the ids its issuer grants in the identity file, such as agents: ["users/{sub}/*"]. A token that no longer reaches a session's Agent gets 403 agent_not_granted at the turn start.

One Agent per session

For a session that needs a sandbox and secrets of its own, make an Agent named after the session, open the session on it, and delete it when you are done:

import { randomUUID } from "node:crypto";

const sessionId = randomUUID();
const scratch = `sessions/${sessionId}`;

await client.agents.put(scratch, { manifest: "analyst", owner: user.id });
// The manifest declares the secret slot "linear".
await client.agents.secrets.put(scratch, "linear", { value: process.env.LINEAR_TOKEN! });
const session = await client.createSession({ id: sessionId, agent: scratch, ownerUserId: user.id });

// ... run turns ...

await client.agents.delete(scratch);

delete removes the Agent with its secrets, disk, disk versions, and memories.

Lifecycle

FieldMeaning
lifecycle.idleStop a pod after this long without a turn. At most the Tenant's idle limit (default 15m), which is also the default.
lifecycle.deleteAfterDelete the Agent, its secrets, and its disk once unused this long: no turn, change, secret write, stop, or reset. Default never.

Both are fields of the manifest's runtime section. client.agents is the Agent calls of the client (AgentInstancesClient; the whole client is AgentsClient):

CallDoes
get(id)The view: manifest, resolvedVersion, manifestHash, tracking, targetVersion, waitingFor, failedVersion, kind, harness, owner, labels, state, attention, nextWakeAt, compute, runtime (what it runs, resolved), limits, slots, disk, forkedFrom, lastUsedAt, and the sessions on it.
list({ labels, state, manifest, outdated })Every Agent with all those labels, in that state, made from that manifest, or moving to another version.
page({ ..., limit, cursor })The same, a page at a time.
stop(id)Pods: stop compute and keep the volume. The next turn starts it again.
restart(id, { retry })Apply a version it waits to start on. See Versions.
reset(id)Pods: a new pod on a new, empty volume, with its setup and input files. The way out of agent_lost.
delete(id)Delete the Agent, its secrets, its disk, its disk versions, and its memories.
events(id, { from, types, since, until })Its events. See Events.

npx nylorun agents list, get <id>, and rm <id> list, read, and delete Agents from a terminal.

Images by digest

The first time a pod of an Agent is ready, the Runtime records the image it pulled by digest, compute.pod.imageDigest in the view. Every later pod of that Agent, after a stop, a relaunch, or a reset, runs that digest: a tag that moves never changes what an Agent runs. A version that changes image pulls the new one at the pod's next start, and a fork pulls the tag afresh. An image loaded onto the node without a registry pins nothing.

Disk versions

Keep a version of an Agent's disk between turns, and put one back:

const { version } = await client.agents.snapshot("projects/acme");

await client.agents.restore("projects/acme", version);

await client.agents.put("projects/acme-try", {
  manifest: "analyst",
  from: { agent: "projects/acme", version },
});
CallDoes
snapshot(id)Keep /workspace as a new version. An unchanged disk makes none (unchanged: true).
restore(id, version)Put a version (or "latest") back: a virtual Agent at once (done), a pod on a new volume before its next turn (pending).
put(newId, { manifest, from })Create an Agent whose disk starts as another's version. Its secrets start empty. To copy secrets too, fork.
versions(id)The versions, newest first, and the folder artifact that holds them.
deleteVersion(id, n)Delete one version.

A snapshot or restore during a turn is 409 agent_busy. A version holds files with their modes and times, links, and empty directories, at most 200,000 entries and 10 GiB by default, within the Tenant's artifact limits; a manifest's runtime.limits.disk may set less. Read a version's tree, files, and diffs through client.artifacts with the artifactId from versions(id). A pod whose volume is lost and that has a version starts again from the latest one. Kind none has no disk.

Egress records

Each connection a pod opens to the internet, allowed or refused, is one agent.egress event on the Agent's own events. It carries the host, port, outcome (allowed or refused), the reason for a refusal, bytes up and down, the duration, mode (credential for a host a shell credential is bound to, else tunnel), and runningTurns: the { sessionId, turnId } of each turn running when it opened, at most 16. It never carries a header value, a path, or a body. Session logs and turn traces hold no egress.

A connection is the Agent's: with several turns running, it names them all. To see the connections around one turn, read the Agent's egress events in the turn's time window:

const connections = await client.agents.events("projects/acme", {
  types: ["agent.egress"],
  since: "2026-10-15T09:00:00Z",
  until: "2026-10-15T09:05:00Z",
});
npx nylorun agents events projects/acme --types agent.egress --since 2026-10-15T09:00:00Z

Events

client.agents.events(id) reads the Agent's events (AGENT_EVENT_TYPES):

EventsWhen
agent.created, .attached, .detached, .deleted, .forkedMade (with the manifest version and forkedFrom), a session attached or detached, deleted, forked.
agent.running, .suspended, .relaunched, .lost, .reset, .failedPods: the pod's life. A failed setup command is an agent.failed with its exit code and the end of its output.
agent.outdated, .version_changed, .start_failedA move waits (waitingFor), applies, or a start on the new version went back. See Versions.
agent.egressA connection. See Egress records.
agent.run, .trigger_paused, .trigger_resumed, .trigger_secret_rotatedTriggers: a run and what came of it, and changes to a trigger.
agent.memory_updatedA turn changed the Agent's memory.

types keeps only those types; since and until keep the events recorded in that window. A session's own log records agent.attached, environment.state, and environment.exec. See Events. Request and response shapes are on Read an Agent's events.

Next step

On this page