# Agents (/docs/run/agents)



```ts
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](/docs/run/agent-manifests): one sandbox of the manifest's
kind, one set of [secrets](/docs/run/credentials) for its shell, MCP servers,
and HTTP tools, a disk, its [memory](/docs/run/memory), its
[triggers](/docs/run/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 [#make-an-agent]

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

| Field      | Meaning                                                                                                                                                                                |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `manifest` | Required. `"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`. |
| `owner`    | The [owner guard](#owner-guard). `null` clears it; left out, it stays as it is.                                                                                                        |
| `labels`   | Key-value pairs to filter on, at most 32 by default. A `put` that sends them replaces them.                                                                                            |
| `from`     | On create only: `{ agent, version }`, start the disk from another Agent's [disk version](#disk-versions).                                                                              |

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](/docs/deploy/kubernetes#tenant-limits)), 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](/reference/runtime/agents/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](#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](#fork) the Agent instead.

## Kinds [#kinds]

The kind is the manifest's `runtime.kind`:

| Kind      | Behaviour                                                                                                                                                                                                                   |
| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `virtual` | An 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. |
| `pod`     | Preview. A container with its own volume on the Tenant's cluster, after `npx nylorun local sandbox enable --context <ctx>`. See [Kubernetes](/docs/deploy/kubernetes).                                                      |
| `none`    | No 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](/docs/build/harness) offers its own.
[Shell credentials](/docs/run/credentials#shell-credentials) reach pods only:
a virtual Agent gets their variables but no header.

```sh
npx nylorun doctor sandbox
```

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

## Versions [#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](/docs/run/agent-manifests#save-from-code) 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:

```ts
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:

| `applies`    | When                                                                                                                                                                                                                               |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `none`       | The Agent already runs that version.                                                                                                                                                                                               |
| `now`        | Turns 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_start` | A 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`. |
| `secrets`    | The version adds a required [secret slot](/docs/run/agent-manifests#secret-slots) with no value (`missingSecrets`). It applies once you write the value; until then `waitingFor: "secrets"` and the state is `input_required`.     |
| `refused`    | The 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](/reference/runtime/agents/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](#fork).

```sh
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 [#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:

```ts
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",
});
```

| Option            | Meaning                                                                                                                                                     |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`              | The 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`, `owner` | The 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](/reference/runtime/agents/fork-an-agent).

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

## State [#state]

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

| `state`          | Holds when                                                                                                                                                                |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `failing`        | Its pod failed or was lost, a start on a new version failed, or a session's latest turn failed.                                                                           |
| `input_required` | A 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. |
| `working`        | A turn runs.                                                                                                                                                              |
| `sleeping`       | Nothing to do.                                                                                                                                                            |

`attention` lists every state that holds, highest first, each with its
`reason` and the `sessionId` behind it. A required [secret slot](/docs/run/credentials#secret-slots)
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](/docs/run/triggers) fires. Accept values you do not know in each.

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

## Shared disks and turns [#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 [#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:

| Caller                                                        | Another owner's session gets |
| ------------------------------------------------------------- | ---------------------------- |
| Application key                                               | `403 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 [#access]

An application key reaches every Agent. Acting for a person with
[`client.as()`](/docs/build#serve-your-users), 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 [#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:

```ts
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 [#lifecycle]

| Field                   | Meaning                                                                                                                          |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `lifecycle.idle`        | Stop a pod after this long without a turn. At most the Tenant's `idle` limit (default `15m`), which is also the default.         |
| `lifecycle.deleteAfter` | Delete 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`):

| Call                                          | Does                                                                                                                                                                                                                                                                                                                         |
| --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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](#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](#events).                                                                                                                                                                                                                                                                                           |

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

## Images by digest [#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 [#disk-versions]

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

```ts
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 },
});
```

| Call                             | Does                                                                                                                          |
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `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](#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`](/docs/run/files) 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 [#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](/docs/run/credentials#shell-credentials) 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:

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

```sh
npx nylorun agents events projects/acme --types agent.egress --since 2026-10-15T09:00:00Z
```

## Events [#events]

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

| Events                                                                        | When                                                                                                            |
| ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `agent.created`, `.attached`, `.detached`, `.deleted`, `.forked`              | Made (with the manifest version and `forkedFrom`), a session attached or detached, deleted, forked.             |
| `agent.running`, `.suspended`, `.relaunched`, `.lost`, `.reset`, `.failed`    | Pods: 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_failed`                         | A move waits (`waitingFor`), applies, or a start on the new version went back. See [Versions](#versions).       |
| `agent.egress`                                                                | A connection. See [Egress records](#egress-records).                                                            |
| `agent.run`, `.trigger_paused`, `.trigger_resumed`, `.trigger_secret_rotated` | [Triggers](/docs/run/triggers): a run and what came of it, and changes to a trigger.                            |
| `agent.memory_updated`                                                        | A turn changed the Agent's [memory](/docs/run/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](/docs/run/events).
Request and response shapes are on
[Read an Agent's events](/reference/runtime/agents/read-an-agents-events).

## Next step [#next-step]

<Cards>
  <Card title="Saving manifests" description="Save, check, and version what an Agent runs." href="/docs/run/agent-manifests" />

  <Card title="Secrets and credentials" description="Write an Agent's secrets." href="/docs/run/credentials" />
</Cards>
