@nylorun/agents 0.22.0-beta → 0.24.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +129 -0
- package/README.md +148 -114
- package/dist/a2a/handler.d.ts +9 -4
- package/dist/a2a/handler.js +9 -0
- package/dist/ag-ui/handler.d.ts +13 -9
- package/dist/ag-ui/handler.js +8 -5
- package/dist/agent-manifests.d.ts +127 -0
- package/dist/agent-manifests.js +208 -0
- package/dist/agents.d.ts +456 -0
- package/dist/agents.js +331 -0
- package/dist/compose.d.ts +23 -0
- package/dist/compose.js +246 -0
- package/dist/files.d.ts +3 -3
- package/dist/files.js +3 -3
- package/dist/index.d.ts +8 -6
- package/dist/index.js +5 -4
- package/dist/plugins/load.d.ts +10 -0
- package/dist/plugins/load.js +25 -14
- package/dist/plugins/plugin.d.ts +1 -1
- package/dist/plugins/plugin.js +1 -1
- package/dist/reads.d.ts +3 -1
- package/dist/session-client.d.ts +78 -48
- package/dist/session-client.js +22 -72
- package/dist/skills/index.d.ts +1 -2
- package/dist/skills/index.js +1 -2
- package/dist/skills/load.d.ts +13 -2
- package/dist/skills/load.js +59 -19
- package/dist/skills/skills.d.ts +2 -2
- package/dist/skills/skills.js +2 -2
- package/package.json +5 -5
- package/dist/environments.d.ts +0 -387
- package/dist/environments.js +0 -299
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ packages. A developer application's production tree should contain only this
|
|
|
7
7
|
package and `@nylorun/core` from Nylorun. Vocabulary:
|
|
8
8
|
[runtime/server/src/CONTEXT.md](../../runtime/server/src/CONTEXT.md).
|
|
9
9
|
|
|
10
|
-
## Application entry: save your agents
|
|
10
|
+
## Application entry: save your agents' manifests
|
|
11
11
|
|
|
12
12
|
```ts
|
|
13
13
|
// src/main.ts
|
|
@@ -15,16 +15,24 @@ import { createClient } from "@nylorun/agents";
|
|
|
15
15
|
import { agents } from "../agents/index.js";
|
|
16
16
|
|
|
17
17
|
const client = await createClient();
|
|
18
|
-
for (const agent of agents) await client.
|
|
18
|
+
for (const agent of agents) await client.agentManifests.save({ agent, runtime: { kind: "virtual" } });
|
|
19
|
+
// An Agent per person, made from the manifest's latest version; sessions run on it.
|
|
20
|
+
await client.agents.put(`users/${user.id}/support`, { manifest: "support", owner: user.id });
|
|
21
|
+
const session = await client.createSession({
|
|
22
|
+
id: `support-${user.id}`, // ids are the app's: no session is special
|
|
23
|
+
agent: `users/${user.id}/support`,
|
|
24
|
+
ownerUserId: user.id,
|
|
25
|
+
});
|
|
19
26
|
```
|
|
20
27
|
|
|
21
|
-
The Runtime runs each agent entirely from its manifest and the files saved with
|
|
22
|
-
(skills), so a session needs nothing from your process. Your services are reached as tools:
|
|
23
|
-
tools](#http-tools) and remote MCP servers. `
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
`
|
|
28
|
+
The Runtime runs each agent entirely from its saved **agent manifest** and the files saved with
|
|
29
|
+
it (skills), so a session needs nothing from your process. Your services are reached as tools:
|
|
30
|
+
[HTTP tools](#http-tools) and remote MCP servers. `agentManifests.save({ agent, runtime })` makes
|
|
31
|
+
the manifest's harness section from the built agent, names it after the agent's id (or `name`)
|
|
32
|
+
and puts the runtime section beside it ([Agent manifests and Agents](#agent-manifests-and-agents)).
|
|
33
|
+
It refuses, before sending anything, an agent with a tool that would run your code
|
|
34
|
+
(`tool({ run })`) or a flow agent with a tool stage: make it an `http()` tool or serve it from a
|
|
35
|
+
remote MCP server. `tool({ run })` still runs in the local engine (`@nylorun/harness/run`).
|
|
28
36
|
|
|
29
37
|
`createClient()` finds the Runtime through the two `NYLORUN_*` variables or the Project link
|
|
30
38
|
that `npx nylorun start` writes; with neither, it fails with `connection_missing` and names
|
|
@@ -47,7 +55,7 @@ const refundOrder = http({
|
|
|
47
55
|
output: z.object({ refundId: z.string() }), // optional
|
|
48
56
|
url: "https://billing.example.com/refunds",
|
|
49
57
|
method: "POST", // default; or GET, PUT, PATCH, DELETE
|
|
50
|
-
credential: "billing", // optional:
|
|
58
|
+
credential: "billing", // optional: a runtime.secrets slot (bearer or headers) bound to this URL
|
|
51
59
|
timeoutMs: 20_000, // default 60000, at most 300000
|
|
52
60
|
});
|
|
53
61
|
|
|
@@ -122,10 +130,11 @@ rest) is never run twice by the Runtime: a crash fails it to the model as
|
|
|
122
130
|
turn say, that is a new call with a new key: put a business key (an order or
|
|
123
131
|
charge id) in the tool's input schema to refuse it too.
|
|
124
132
|
|
|
125
|
-
`credential` works as for a remote MCP server:
|
|
126
|
-
|
|
127
|
-
`{field}` parameters included
|
|
128
|
-
|
|
133
|
+
`credential` works as for a remote MCP server: it names a `bearer` or `headers`
|
|
134
|
+
slot of the manifest's runtime section bound to the tool's exact URL (as written,
|
|
135
|
+
`{field}` parameters included; the Runtime checks it when the manifest is saved),
|
|
136
|
+
and the vault of the session's Agent must hold a value for it, else the call fails
|
|
137
|
+
before it is sent (`http.credential`). A credential with `via` sends the call there instead, such
|
|
129
138
|
as a gateway, with the session owner in its identity header (a gateway has one
|
|
130
139
|
URL, so a tool with `{field}` parameters fails with `http.credential`), and a `401` is `credential_rejected` to the model, not retried.
|
|
131
140
|
Whether a call waits for a person is not the tool's to say: it is the Tenant's
|
|
@@ -171,7 +180,7 @@ decides, and a call no rule matches runs:
|
|
|
171
180
|
- `allow`: it runs.
|
|
172
181
|
|
|
173
182
|
Sandbox tools never cross the gate: their network side is egress policy. A
|
|
174
|
-
manifest no longer sets `approval` (the Runtime and `
|
|
183
|
+
manifest no longer sets `approval` (the Runtime and `agentManifests.save` refuse it); a
|
|
175
184
|
session pinned to a manifest saved with `approval: "always"` keeps it, as an
|
|
176
185
|
ask, until it ends.
|
|
177
186
|
|
|
@@ -215,16 +224,17 @@ const client = createClient({
|
|
|
215
224
|
url: process.env.NYLORUN_RUNTIME_URL,
|
|
216
225
|
key: process.env.NYLORUN_SERVER_KEY,
|
|
217
226
|
});
|
|
218
|
-
await client.
|
|
227
|
+
await client.agentManifests.save({ agent: assistant });
|
|
228
|
+
await client.agents.put("assistant", { manifest: "assistant" });
|
|
219
229
|
const session = await client.createSession({
|
|
220
|
-
|
|
230
|
+
agent: "assistant",
|
|
221
231
|
ownerUserId: authenticatedUser.id,
|
|
222
232
|
});
|
|
223
233
|
await session.input("Look up item 123", { idempotencyKey: requestId });
|
|
224
234
|
const history = await session.history();
|
|
225
235
|
```
|
|
226
236
|
|
|
227
|
-
Every request sets `Nylorun-Protocol` (
|
|
237
|
+
Every request sets `Nylorun-Protocol` (16) and no `Nylorun-Tenant`: the Runtime
|
|
228
238
|
serves one Tenant. Before the first authenticated
|
|
229
239
|
request, `Transport` fetches `/health` once, checks protocol compatibility, and
|
|
230
240
|
throws `IncompatibleRuntimeError` / `incompatible_host` when the Host range or
|
|
@@ -242,9 +252,9 @@ const assistant = Agent({ id: "assistant", name: "Order assistant" })
|
|
|
242
252
|
.skills("./assistant-skills");
|
|
243
253
|
```
|
|
244
254
|
|
|
245
|
-
Each subdirectory under the catalog must contain a `SKILL.md` with YAML frontmatter (`name`, `description`) per [Agent Skills](https://agentskills.io/home). Every file of a skill's folder, binary included (not `.git/`, `node_modules/`, OS files like `.DS_Store`, or `.env` and `.env.*` files, which hold secrets), is part of the definition: the manifest names each one by path and SHA-256 (`skills.<name>.files`)
|
|
255
|
+
Each subdirectory under the catalog must contain a `SKILL.md` with YAML frontmatter (`name`, `description`) per [Agent Skills](https://agentskills.io/home). Every file of a skill's folder, binary included (not `.git/`, `node_modules/`, OS files like `.DS_Store`, or `.env` and `.env.*` files, which hold secrets), is part of the definition: the manifest names each one by path and SHA-256 (`skills.<name>.files`). A skill's files may hold together at most the Tenant's skill limit (`PUT /v1/tenant/skills`, default 2 GiB); no file and no count of files has a limit of its own, and a manifest's runtime section may set a lower limit for its Agents' sessions (`runtime.limits.skills.totalBytes`). Skills need a sandbox: the runtime section's kind is `virtual` or `pod`, which the Runtime checks when the manifest is saved. `agentManifests.save` uploads the files the Runtime does not hold (`client.files`, `PUT /v1/files/sha256:<hex>`) before it saves the manifest, which the Runtime refuses while it names a file it lacks (`400 definition_files_missing`) or a skill past the Tenant's limit (`413 limit_exceeded`). `read_skill_resource` reads files of up to 10 MiB into the model's context; a larger one is used in the sandbox.
|
|
246
256
|
|
|
247
|
-
The Runtime serves the skills itself, with no call to your process. When
|
|
257
|
+
The Runtime serves the skills itself, with no call to your process. When our harness runs a turn, it adds the skill catalog to the instructions and two tools: `load_skill` returns a skill's `SKILL.md` body and the paths of its other files, and `read_skill_resource` returns a text file. A session with a sandbox also has each skill's files read-only under `/skills/<name>@<hash>/`, so the model can run a skill's scripts with `bash`.
|
|
248
258
|
|
|
249
259
|
Declare MCP servers with `.mcp(...)` (same map shape as agent-plugins `mcpServers`):
|
|
250
260
|
|
|
@@ -262,72 +272,95 @@ Each key names a server; its `name` defaults to the key and, when given, must eq
|
|
|
262
272
|
|
|
263
273
|
Set a server's tools one by one with `tools`, keyed by the server's own tool names, `"*"` for the rest: `{ "*": { enabled: false }, search_issues: { enabled: true }, create_issue: { enabled: true } }` is an allowlist (whether a call waits for a person is the [tool policy](#tool-policy)). When an agent's MCP tools would fill more than a tenth of the model's context window, the Runtime defers them: the model finds them with `tool_search` and runs them with `tool_call`. `deferred: true` or `false`, on a tool or on the server, decides instead.
|
|
264
274
|
|
|
265
|
-
A manifest never carries a server's credential, and its `headers` hold no secret.
|
|
275
|
+
A manifest never carries a server's credential, and its `headers` hold no secret. A server may name the runtime section's secret slot it authenticates with (`credential`: a `bearer` or `headers` slot bound to its URL, checked when the manifest is saved), and you write the credential into each Agent's vault (a token or a header map, or a gateway's key with `via` and an identity header for servers that need each person's sign-in, with `client.agents.secrets.put`); the sessions on that Agent use it: [DEPLOYMENT.md](../../guides/DEPLOYMENT.md#mcp-servers-and-http-tools), which also shows `nylorun tenant mcp inspect` for listing a server's tools first. `admin.coverage({ agentId })` in `@nylorun/admin` says which credential each server and HTTP tool of an Agent's manifest version would get from its vault ([Checking an agent's credentials](../../guides/DEPLOYMENT.md#checking-an-agents-credentials)). The model knows each tool as `server__tool`, with characters outside `[A-Za-z0-9_-]` replaced by `_`; a failed call reaches it with a code (`credential_rejected` on a `401`, `mcp.unreachable`, …), and a result past 32 KiB, or an image, becomes an artifact of the session that it reads with `read_artifact`.
|
|
276
|
+
|
|
277
|
+
## Agent manifests and Agents
|
|
266
278
|
|
|
267
|
-
|
|
279
|
+
An **agent manifest** (v8, protocol 16) is one named, versioned document with two sections: `harness`, what runs inside the agent loop (the harness's `id`, `instructions`, `tools`, `skills`, `mcpServers`, `plugins`, `outputSchema` and the harness's own `loop` settings; or a flow, `kind: "workflow"`), and `runtime`, what the Runtime provides around it (`kind`, `image`, `network`, `setup`, `files`, `limits`, `lifecycle` and its secret slots, `secrets`). The Runtime checks the two against each other when the manifest is saved and refuses every gap at once (`422 manifest_inconsistent`, `details.gaps`): a custom harness off a pod, a `credential` or a skill's `secrets` that is no declared slot of the right type, skills on a runtime without a sandbox. A manifest without `runtime` makes Agents of kind `none`.
|
|
280
|
+
|
|
281
|
+
An **Agent** is the live instance of one version of a manifest: one environment of its runtime's kind, one vault, and the sessions that run on it, with an id you choose (`users/alice/triage`), which outlives them. It is made only from a saved manifest: `manifest: "triage"` tracks the manifest (its latest version when the Agent is made), `"triage@3"` pins version 3; either way it resolves the version once and keeps it. Every session runs on one Agent (`createSession({ agent })`), and no session is special: creating an Agent opens none, and the app picks session ids (D71). Whether an Agent serves one person or a project is your app's choice:
|
|
268
282
|
|
|
269
283
|
```ts
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
const plan = await client.
|
|
282
|
-
await client.
|
|
283
|
-
|
|
284
|
-
// One per person or
|
|
285
|
-
await client.
|
|
286
|
-
await client.
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
await client.environments.put("users/alice/web", {
|
|
290
|
-
kind: "virtual", // the default; "pod" for a container with its own volume, "none" for a vault without compute
|
|
291
|
-
network: { allowedHosts: ["pypi.org", "files.pythonhosted.org"] }, // no egress unless you list hosts
|
|
292
|
-
ownerUserId: "alice", // the owner guard: only alice's sessions may name it
|
|
293
|
-
labels: { team: "data" },
|
|
294
|
-
lifecycle: { deleteAfter: "30d" }, // deleted once unused this long
|
|
295
|
-
});
|
|
296
|
-
await client.environments.secrets.put("users/alice/web", "GH_TOKEN", {
|
|
297
|
-
type: "environment_secret",
|
|
298
|
-
value: process.env.ALICE_GITHUB_TOKEN!,
|
|
299
|
-
allowedHosts: ["api.github.com"],
|
|
300
|
-
});
|
|
301
|
-
const session = await client.createSession({ agentId: "analyst", ownerUserId: "alice", environment: "users/alice/web" });
|
|
284
|
+
// The manifest, from a built agent and the runtime section to run it in.
|
|
285
|
+
const manifest = {
|
|
286
|
+
agent: analyst, // named after its id, "analyst"
|
|
287
|
+
runtime: {
|
|
288
|
+
kind: "pod" as const,
|
|
289
|
+
image: "python:3.13-slim",
|
|
290
|
+
network: { allowedHosts: ["pypi.org", "files.pythonhosted.org", "api.github.com"] },
|
|
291
|
+
setup: [{ command: "pip install pandas" }],
|
|
292
|
+
secrets: { GH_TOKEN: { type: "environment_secret" as const, allowedHosts: ["api.github.com"], required: true } },
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
const plan = await client.agentManifests.plan(manifest); // from → to, changes, warnings, errors, Agents behind
|
|
296
|
+
await client.agentManifests.save(manifest, { ifMatch: plan.from ?? 0 }); // 412 manifest_changed if it moved
|
|
297
|
+
|
|
298
|
+
// One per person: get-or-create by id, write its secrets, open sessions on it.
|
|
299
|
+
await client.agents.put("users/alice/analyst", { manifest: "analyst", owner: "alice", labels: { team: "data" } });
|
|
300
|
+
await client.agents.secrets.put("users/alice/analyst", "GH_TOKEN", { value: process.env.ALICE_GITHUB_TOKEN! });
|
|
301
|
+
const session = await client.createSession({ id: "alice-q3", agent: "users/alice/analyst", ownerUserId: "alice" });
|
|
302
|
+
```
|
|
302
303
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
|
|
304
|
+
`agentManifests.save(input, { ifMatch, requestId })` and `plan(input, { ifMatch })` (a dry run that saves nothing) take a v8 document, what `composeManifest` read, or `{ agent, runtime?, harness?, loop?, name?, description?, metadata? }`: the SDK makes the harness section from the built agent or flow (`harnessSectionOf`; `harness` is the harness's id) and names the manifest after the agent's id. First it uploads every file the manifest names by hash that the Runtime does not hold: the agent's skills, a flow's agents' skills, and the runtime's files (`runtimeFiles(path, contents)` hashes a `files` entry; pass its `bytes` as `files`). `If-Match` is the version you read (`0` for a new manifest). A save that changes nothing answers the latest version, `unchanged`. `get(name, version?)`, `list()`, `versions(name)` (with the Agents made from each) and `delete(name)` (refused while an Agent was made from it: `409 manifest_in_use`) read and remove them.
|
|
305
|
+
|
|
306
|
+
### Three files: `composeManifest`
|
|
307
|
+
|
|
308
|
+
A manifest can also be authored as files, so a runtime is shared by many agents and skills stay folders. `composeManifest(dir)` (Node only) reads `<dir>/agent.manifest.json` and the files it names, each path relative to the file that names it, and returns `{ manifest, files, gaps, warnings }`; `agentManifests.save` takes it as it is and uploads `files`. `npx nylorun apply <dir>` does the same from the command line.
|
|
309
|
+
|
|
310
|
+
```jsonc
|
|
311
|
+
// agents/triage/agent.manifest.json
|
|
312
|
+
{ "manifestSchemaVersion": 8, "name": "triage", "harness": "./harness.manifest.json", "runtime": "../../shared/runtime.manifest.json" }
|
|
313
|
+
|
|
314
|
+
// agents/triage/harness.manifest.json
|
|
315
|
+
{
|
|
316
|
+
"instructions": ["Triage each new issue."],
|
|
317
|
+
"skills": "./skills", // each subfolder with a SKILL.md is a skill
|
|
318
|
+
"mcpServers": "./mcp.json", // the Agent Plugins format
|
|
319
|
+
"plugins": ["../../plugins/github"], // folded in as .plugin() folds it
|
|
320
|
+
"credentials": { "linear": "linear" }, // MCP server or HTTP tool → runtime.secrets slot
|
|
321
|
+
"requires": { "sandbox": "any", "secrets": ["GH_TOKEN"] } // checked here only
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// shared/runtime.manifest.json
|
|
325
|
+
{
|
|
326
|
+
"kind": "pod",
|
|
327
|
+
"files": [{ "path": "/workspace/kit", "from": "./kit" }], // a folder, uploaded by hash
|
|
328
|
+
"secrets": {
|
|
329
|
+
"GH_TOKEN": { "type": "environment_secret", "allowedHosts": ["api.github.com"] },
|
|
330
|
+
"linear": { "type": "bearer", "url": "https://mcp.linear.app/mcp" }
|
|
331
|
+
}
|
|
332
|
+
}
|
|
309
333
|
```
|
|
310
334
|
|
|
311
|
-
|
|
335
|
+
```ts
|
|
336
|
+
import { composeManifest, createClient } from "@nylorun/agents";
|
|
337
|
+
|
|
338
|
+
const composed = await composeManifest("agents/triage");
|
|
339
|
+
if (composed.gaps.length > 0) throw new Error(composed.gaps.join("\n"));
|
|
340
|
+
await client.agentManifests.save(composed);
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
`gaps` names where the runtime misses the harness file's `requires` (`sandbox: "any"` needs kind `virtual` or `pod`, `"pod"` a pod; each of `secrets` a declared slot) and the gaps the Runtime would refuse it for (`sectionGaps`). `requires`, `credentials` and every `$schema` stay out of the document; a `credentials` name that is no server or HTTP tool, or a file that does not validate, throws `ComposeError`. JSON Schemas for editors: `@nylorun/core/schemas/agent-manifest.json`, `harness-manifest.json` and `runtime-manifest.json`.
|
|
344
|
+
|
|
345
|
+
### Agents
|
|
312
346
|
|
|
313
|
-
|
|
347
|
+
With a sandbox (kind `virtual` or `pod`), the model gets `bash`, `read`, `write`, `edit`, `grep` and `glob` on a persistent `/workspace`. These tools run in the Runtime, not in your process. A `virtual` Agent's environment is an emulated shell in the Runtime's harness, which is not a VM boundary and takes no `image`; a `pod` runs `image` with its own volume (`storage`) once the Tenant has sandbox pods (`nylorun local sandbox enable`). Kind `none` has a vault and no sandbox, for agents that use only remote MCP servers and HTTP tools.
|
|
314
348
|
|
|
315
|
-
|
|
349
|
+
An Agent runs one version of its manifest at a time and evolves in place, with its id, disk, vault and sessions: a `put` on an existing one may change its `labels` and `owner` guard and moves it to the version its `manifest` names (`"name"` tracks the latest, `"name@n"` pins), `plan(id, input)` answers what that move would do, and a save of a new version moves every Agent that tracks the manifest. A harness change applies at once (each session's next turn runs it); a pod's runtime change waits for its next start on the same volume (`targetVersion`, `waitingFor: "start"`; `restart(id)` applies it now), and a start that fails goes back to the version it ran (`failedVersion`; `restart(id, { retry: true })` tries again). A version that adds a required secret waits for its value (`waitingFor: "secrets"`). `list({ outdated: true })` lists the Agents still moving. Its manifest's name, kind, harness and storage are fixed (`409 agent_fixed_field`): `fork(id, { id: next, manifest? })` makes a new Agent (default the source's manifest, tracking), with the source's disk (`disk`: `latest`, a version, or `empty`) and the secrets the new manifest declares (`secrets: "none"` copies none), and `planFork` answers what a fork would do. The Runtime checks the runtime section against the Tenant's limits (`GET`/`PUT /v1/tenant/sandbox`: a network ceiling, a resource maximum, the idle timeout; `/v1/tenant/artifacts` and `/v1/tenant/skills` for its `limits`). `lifecycle.idle` stops a pod after that long without a turn (at most the Tenant's); `lifecycle.deleteAfter` deletes the Agent, its vault and its files once nothing has used it that long. Private networks, loopback, the host and cloud metadata endpoints are always blocked.
|
|
316
350
|
|
|
317
|
-
|
|
351
|
+
Secrets are write-only: `client.agents.secrets.put(id, name, secret)` creates or replaces one, and `list`, `get` and `delete` never return a value. When the manifest declares the secret's slot (`runtime.secrets`), send only `{ value }` (or `{ headers }`): the type and binding come from the slot, and a name it does not declare is `409 secret_not_in_manifest`. Typed, an `environment_secret` (`{ value, allowedHosts, inject? }`) shows its name to commands as the sentinel `nylorun_managed`, and egress-gate sets its header on HTTPS requests to `allowedHosts` (pods only); an `environment_variable` (`{ value }`) is a plain variable; `bearer` (`{ url, value, via?, identity? }`) and `headers` (`{ url, headers, via?, identity? }`) are credentials for the MCP servers and HTTP tools at `url`. A second secret for one host, or a second credential for one URL, is `409 credential_conflict` at the write ([DEPLOYMENT.md](../../guides/DEPLOYMENT.md#credentials)).
|
|
318
352
|
|
|
319
|
-
|
|
353
|
+
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. Stop, reset, snapshot and restore wait until no turn runs (`409 agent_busy`). Deleting a session only detaches it. With `owner` set, a session of another owner is `403 agent_owner_mismatch` (a caller acting for a person gets the `404` of a missing Agent), checked when it opens and at every turn start. An Agent's view says what it runs (`manifest`, `resolvedVersion`, `tracking`), its `state` (`working`, `sleeping`, `input_required`, `failing` or `paused`, `AGENT_STATES`) with the sessions behind it (`attention`), its compute (`compute.state`) and its slots. `client.agents.list({ labels, state, manifest })`, `page`, `get(id)`, `events(id, { types, since, until })` (its lifecycle, and `agent.egress` for every connection its sandbox opens, naming the turns that were running), `stop(id)`, `reset(id)` and `delete(id)` manage them, and `npx nylorun agents list | put | rm` does the same from the command line. Acting for a person (`app.as(...)`), changing an Agent or its secrets needs `agents:write`, and saving a manifest `agent-manifests:write`. A trusted issuer's token reaches only the Agents its issuer grants (the identity file's `agents` templates, exact ids or prefixes ending in `/*`), checked at every turn start. The Tenant holds at most `limits.environments` of them (`PUT /v1/tenant/sandbox`, default 100).
|
|
354
|
+
|
|
355
|
+
A pod's disk can start prepared: the runtime's `files` are copied in (artifact versions, or files by hash), then `setup` runs its commands, once per new disk and before its first turn (a failed command fails the pod, and the next turn start runs the setup again). Between turns, `client.agents.snapshot(id)` keeps a version of the disk, `restore(id, version)` puts one back (a pod on a new volume seeded before its next turn), `put(newId, { manifest, from: { agent: id, version: "latest" } })` starts a new Agent from it, and `versions(id)` and `deleteVersion(id, n)` manage them ([DEPLOYMENT.md](../../guides/DEPLOYMENT.md#agents)):
|
|
320
356
|
|
|
321
357
|
```ts
|
|
322
|
-
await client.
|
|
323
|
-
|
|
324
|
-
files: [{ artifactId: repoTarball.artifactId, version: 1, path: "/workspace/app" }],
|
|
325
|
-
setup: [{ command: "npm ci", cwd: "/workspace/app", timeout: "15m" }],
|
|
326
|
-
});
|
|
327
|
-
const { version } = await client.environments.snapshot("projects/acme");
|
|
328
|
-
await client.environments.put("projects/acme-try", { kind: "pod", from: { environment: "projects/acme", version } });
|
|
358
|
+
const { version } = await client.agents.snapshot("projects/acme");
|
|
359
|
+
await client.agents.put("projects/acme-try", { manifest: "acme", from: { agent: "projects/acme", version } });
|
|
329
360
|
```
|
|
330
361
|
|
|
362
|
+
## Subagents
|
|
363
|
+
|
|
331
364
|
Add an agent with `.subagents()` to let the model delegate to it:
|
|
332
365
|
|
|
333
366
|
```ts
|
|
@@ -360,12 +393,13 @@ Delegate when the parent should keep the answer. When a specialist should own th
|
|
|
360
393
|
## Flow agents
|
|
361
394
|
|
|
362
395
|
An agent's body is either a ReAct loop (the model decides) or a flow (its manifest
|
|
363
|
-
decides). A flow agent is built from stages and is
|
|
364
|
-
|
|
365
|
-
`createSession({
|
|
396
|
+
decides). A flow agent is built from stages and is saved and run like any agent: put it in
|
|
397
|
+
`export const agents`, `agentManifests.save({ agent: flowAgent, runtime })`, make an Agent from
|
|
398
|
+
the manifest, then `createSession({ agent })` and `session.input(value)`. `input` sends string values as
|
|
366
399
|
`content` and other JSON as `data`. On the wire a flow agent is a workflow manifest v3
|
|
367
400
|
(`kind: "workflow"`, `workflowSchemaVersion: 3`) that embeds the agents it runs, so one
|
|
368
401
|
document and one manifest hash cover the whole flow; its agents are not listed on their own.
|
|
402
|
+
In an agent manifest it is the harness section, `harness.kind: "workflow"`.
|
|
369
403
|
|
|
370
404
|
A flow passes data between its stages itself: the first stage gets the flow's input, and
|
|
371
405
|
every later stage gets the previous stage's output. So each agent returns, through its
|
|
@@ -435,7 +469,7 @@ object); a mismatch the build can see, such as an agent with no `.output()` befo
|
|
|
435
469
|
is refused (`flow.input-mismatch`), and any other fails the stage when it runs
|
|
436
470
|
(`tool.invalid-input`). The Runtime makes the request as for an agent's HTTP tool, with
|
|
437
471
|
`Nylorun-Agent-Id` set to the flow agent's id and `credential` taken from the
|
|
438
|
-
vault of the flow session's
|
|
472
|
+
vault of the flow session's Agent; its answer is the next stage's input. A failure status, a timeout or
|
|
439
473
|
an answer that does not match `output` fails the stage with its code (`http.status`,
|
|
440
474
|
`http.timeout`, `tool.invalid-output`, …). A Loop body that starts with an HTTP stage
|
|
441
475
|
is retried with the Loop's input, not the feedback.
|
|
@@ -479,13 +513,12 @@ nested flow agent's id in front of its own agents'. Control stages add nothing,
|
|
|
479
513
|
wrapping a step in `.loop()` or moving it between cases keeps its session. An agent
|
|
480
514
|
may appear once per flow; use it again under a new id with `writer.withId("final-writer")`
|
|
481
515
|
(`flow.duplicate-leaf`). A code tool stage (`.pipe(tool({ run }))`) runs only in the local
|
|
482
|
-
engine: the Runtime runs flows from their manifests alone, so `
|
|
483
|
-
agent with one. Use an HTTP stage instead.
|
|
516
|
+
engine: the Runtime runs flows from their manifests alone, so `agentManifests.save` refuses a
|
|
517
|
+
flow agent with one. Use an HTTP stage instead.
|
|
484
518
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
it by naming the same id (see `client.environments`). Call the sandbox
|
|
519
|
+
A flow runs on its Agent: every linked session in the flow runs on the Agent the flow's
|
|
520
|
+
session runs on, with its sandbox and vault; turns within one flow do not block each other.
|
|
521
|
+
Other sessions on the same Agent share its sandbox. Call the sandbox
|
|
489
522
|
built-ins via `session.sandbox` (application).
|
|
490
523
|
Observe with `session.observe({ follow: true })` to merge linked agent streams;
|
|
491
524
|
`pending()` lists waits across the tree. Studio renders the manifest tree and
|
|
@@ -498,9 +531,8 @@ workflow Loop): [runtime/harness/src/CONTEXT.md](../../runtime/harness/src/CONTE
|
|
|
498
531
|
Use `session.observe({ cursor, signal })` for resumable canonical events, `session.inspect()` for waiting/uncertain state, and `approve`, `respond`, or `cancel` with an explicit stable idempotency key. Retry the same semantic command with the same key. `ownerUserId` must come from trusted server authentication. Application credentials are not browser credentials; browser applications need an authorized backend. Definition authoring is browser-bundleable.
|
|
499
532
|
|
|
500
533
|
Connection defaults follow `resolveConnection` (options → environment → Project
|
|
501
|
-
link).
|
|
502
|
-
|
|
503
|
-
definition a session opens with. Model selection and model credentials belong to
|
|
534
|
+
link). Agents do not hash the manifest; a session pins the manifest version its Agent runs
|
|
535
|
+
when it opens, and the digest of the loop manifest made from it. Model selection and model credentials belong to
|
|
504
536
|
the Tenant.
|
|
505
537
|
|
|
506
538
|
The Runtime sends a lost MCP or HTTP tool call again with the same key, then fails it to the
|
|
@@ -514,8 +546,9 @@ local execution is available through `@nylorun/harness/run`.
|
|
|
514
546
|
|
|
515
547
|
The Runtime serves the Tenant's agents to any [AG-UI](https://docs.ag-ui.com)
|
|
516
548
|
client (`@ag-ui/client`'s `HttpAgent`, CopilotKit, a desktop renderer) at
|
|
517
|
-
`/v1/ag-ui/agents/{agentId}` (optional feature `ag-ui-endpoint`)
|
|
518
|
-
thread is one session per person,
|
|
549
|
+
`/v1/ag-ui/agents/{agentId}` (optional feature `ag-ui-endpoint`), where `agentId` is an
|
|
550
|
+
Agent's id (`client.agents`). Each AG-UI thread is one session per person, Agent and thread,
|
|
551
|
+
on that Agent. There are two ways in:
|
|
519
552
|
|
|
520
553
|
- **From your own server**, with `@nylorun/agents/ag-ui` (below): your server
|
|
521
554
|
signs people in and the handler forwards each request to the Runtime acting
|
|
@@ -535,7 +568,7 @@ import support from "./agents/support.js";
|
|
|
535
568
|
|
|
536
569
|
const agui = createAgUiHandler({
|
|
537
570
|
basePath: "/api/agui",
|
|
538
|
-
agents: [support], // nothing else is reachable
|
|
571
|
+
agents: ["support"], // Agent ids: nothing else is reachable
|
|
539
572
|
subject: async (request) => (await getSignedInUser(request))?.id, // undefined → 401
|
|
540
573
|
});
|
|
541
574
|
|
|
@@ -552,9 +585,10 @@ createServer(toNodeListener(agui)).listen(3000);
|
|
|
552
585
|
|
|
553
586
|
`run`, `history`, `reattach` and `cancel` are also on the handler for your own
|
|
554
587
|
routing. The default `client` is `createClient()`; pass `client` to use another
|
|
555
|
-
one, and `session(subject, agentId)` to add the
|
|
556
|
-
|
|
557
|
-
|
|
588
|
+
one, and `session(subject, agentId)` to add the session's `info`. It applies when the thread's
|
|
589
|
+
session is created, on its first run; later runs keep it. The session runs on the Agent the path
|
|
590
|
+
names, with its sandbox and its vault's credentials: to give each person their own keys, serve
|
|
591
|
+
each person's Agent (`users/<id>/support`) and route to it. Whatever the browser sends in
|
|
558
592
|
`forwardedProps.nylorun` is replaced.
|
|
559
593
|
|
|
560
594
|
- Each AG-UI message id is the command's idempotency key: a retried run replays
|
|
@@ -588,7 +622,7 @@ never with a key.
|
|
|
588
622
|
|
|
589
623
|
## A2A
|
|
590
624
|
|
|
591
|
-
`@nylorun/agents/a2a` serves the Tenant's
|
|
625
|
+
`@nylorun/agents/a2a` serves the Tenant's Agents to other agents over
|
|
592
626
|
[A2A](https://a2a-protocol.org) 1.0 (JSON-RPC) from your own server: the
|
|
593
627
|
gateway. Your server authenticates each partner and names the subject its tasks
|
|
594
628
|
belong to. The handler forwards each request to the Runtime, which speaks A2A,
|
|
@@ -597,11 +631,9 @@ so the handler loads no A2A package and parses no message.
|
|
|
597
631
|
```ts
|
|
598
632
|
import { createServer } from "node:http";
|
|
599
633
|
import { createA2aHandler, toNodeListener } from "@nylorun/agents/a2a";
|
|
600
|
-
import support from "./agents/support.js";
|
|
601
|
-
|
|
602
634
|
const a2a = createA2aHandler({
|
|
603
635
|
basePath: "/a2a",
|
|
604
|
-
agents: [support], // nothing else is reachable
|
|
636
|
+
agents: ["support"], // Agent ids: nothing else is reachable
|
|
605
637
|
// undefined → 401; return { subject, agents } to give a partner fewer agents
|
|
606
638
|
subject: (request) => partnerFor(request.headers.get("x-partner-key"))?.subject,
|
|
607
639
|
publicUrl: "https://api.example.com/a2a", // for the card; default: the request's origin
|
|
@@ -628,7 +660,7 @@ What the Runtime answers (Host feature `a2a-endpoint`):
|
|
|
628
660
|
for input, for at most 5 minutes (then it returns `WORKING`); with
|
|
629
661
|
`configuration.returnImmediately` it returns at once and the partner polls
|
|
630
662
|
`GetTask`. `CancelTask` works on a running task.
|
|
631
|
-
- A `contextId` is one session per partner,
|
|
663
|
+
- A `contextId` is one session per partner, Agent and context, on that Agent; a task is one
|
|
632
664
|
turn. A message without `taskId` starts a task (one at a time per context).
|
|
633
665
|
A question from the agent pauses the task as `TASK_STATE_INPUT_REQUIRED`
|
|
634
666
|
with the question as the status message; the partner answers with a message
|
|
@@ -638,8 +670,8 @@ What the Runtime answers (Host feature `a2a-endpoint`):
|
|
|
638
670
|
- Text parts, a single data part, or text and `raw` file parts in (at most
|
|
639
671
|
20 MiB each, stored as artifacts of the context's session); the turn's
|
|
640
672
|
output as the artifact `output` (text, or JSON as a data part). A file by
|
|
641
|
-
`url
|
|
642
|
-
|
|
673
|
+
`url` is `ContentTypeNotSupportedError`; a file of any type is accepted, and
|
|
674
|
+
the agent's harness decides how its model sees it.
|
|
643
675
|
- Clients must send `A2A-Version: 1.0` (header or `?A2A-Version=`).
|
|
644
676
|
- Not yet: `ListTasks`, streaming and `SubscribeToTask`
|
|
645
677
|
(`UnsupportedOperationError`; the card says `streaming: false`), push
|
|
@@ -674,8 +706,10 @@ const { url } = await client.artifacts.link(files[0].artifactId); // no credenti
|
|
|
674
706
|
A session with a sandbox gives the agent `save_artifact`, which saves a file it
|
|
675
707
|
made as an artifact of the session; each new artifact or version appears in the
|
|
676
708
|
session's events as `artifact.created` or `artifact.version.created`. The Tenant's
|
|
677
|
-
limits (`PUT /v1/tenant/artifacts`: 100 MiB per file and 10 GiB in all by default)
|
|
678
|
-
|
|
709
|
+
limits (`PUT /v1/tenant/artifacts`: 100 MiB per file and 10 GiB in all by default),
|
|
710
|
+
and those a manifest's runtime section sets for its Agents' artifacts (`runtime.limits.artifacts`,
|
|
711
|
+
at most the Tenant's), refuse a larger upload with `413 limit_exceeded`, and nothing
|
|
712
|
+
is stored. Acting for
|
|
679
713
|
a person (`as()`, or a trusted issuer's token), a client reaches only the artifacts of that
|
|
680
714
|
person's sessions, and an upload names one of them.
|
|
681
715
|
|
|
@@ -704,11 +738,11 @@ oldest-first behavior and does not require `artifact-reads`.
|
|
|
704
738
|
|
|
705
739
|
### Outputs: folder artifacts
|
|
706
740
|
|
|
707
|
-
At the end of each turn,
|
|
741
|
+
At the end of each turn, our harness saves the files the agent wrote into
|
|
708
742
|
`/workspace/outputs` of its sandbox as a version of the session's **folder**
|
|
709
|
-
artifact `outputs` (kind `folder`): the first
|
|
743
|
+
artifact `outputs` (kind `folder`): the first save creates it, and each later
|
|
710
744
|
turn whose outputs changed adds a version, announced by `artifact.created` or
|
|
711
|
-
`artifact.version.created` with `source: "export"
|
|
745
|
+
`artifact.version.created` with `source: "engine"` (`"export"` before protocol 15). A file that did not change is
|
|
712
746
|
stored once. Read a folder as a tree, one file by path, a diff or a zip:
|
|
713
747
|
|
|
714
748
|
```ts
|
|
@@ -720,8 +754,9 @@ const zip = await client.artifacts.zip(outputs.artifactId); // a streamed Respon
|
|
|
720
754
|
const { url } = await client.artifacts.link(outputs.artifactId, { file: "app/index.html" }); // or the zip without `file`
|
|
721
755
|
```
|
|
722
756
|
|
|
723
|
-
|
|
724
|
-
|
|
757
|
+
A save past a limit (the Runtime limit `folderEntries`, 10,000 files by default;
|
|
758
|
+
1 GiB, our harness's own bound; the per-file limit or the Tenant total) stores
|
|
759
|
+
nothing, and our harness records `artifact.export.skipped`; the turn still
|
|
725
760
|
completes.
|
|
726
761
|
|
|
727
762
|
## Acting for a person (app servers)
|
|
@@ -736,7 +771,7 @@ const app = await createClient(); // the application key, on the server only
|
|
|
736
771
|
|
|
737
772
|
// Per request, after your own sign-in:
|
|
738
773
|
const person = app.as(`app:${user.id}`, { scopes: ["sessions:own"] });
|
|
739
|
-
await person.createSession({
|
|
774
|
+
await person.createSession({ agent: `users/${user.id}/support`, ownerUserId: `app:${user.id}` });
|
|
740
775
|
await person.listSessions(); // only this person's sessions
|
|
741
776
|
```
|
|
742
777
|
|
|
@@ -750,16 +785,15 @@ compatibility check, so calling `as()` per request is cheap.
|
|
|
750
785
|
| Scope | Allows |
|
|
751
786
|
| --- | --- |
|
|
752
787
|
| `sessions:own` | The person's own sessions: create, list, read, stream, message, approve, respond, cancel; and their artifacts |
|
|
753
|
-
| `agents:read` |
|
|
754
|
-
| `agents:write` |
|
|
755
|
-
| `
|
|
788
|
+
| `agents:read` | Reading the Tenant's agent manifests and Agents |
|
|
789
|
+
| `agents:write` | Creating, changing, stopping, resetting and deleting Agents, and writing their secrets (reading them takes `sessions:own` or this) |
|
|
790
|
+
| `agent-manifests:write` | Saving and deleting agent manifests |
|
|
756
791
|
|
|
757
792
|
No scope reaches the sandbox tool routes; call them without `as()`. `tenant:settings` is retired (protocol 8): it grants
|
|
758
793
|
nothing, and no subject reaches the Management API (`/v1/tenant/*`).
|
|
759
|
-
`
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
names it when it opens their sessions
|
|
794
|
+
`environments:write` is refused with a message naming `agents:write` (protocol 16).
|
|
795
|
+
Credentials live in Agents' vaults: the server writes a person's own keys into
|
|
796
|
+
their Agent (made with `owner` set to them) and opens their sessions on it
|
|
763
797
|
([DEPLOYMENT.md](../../guides/DEPLOYMENT.md#credentials)). A subject is 1–200 visible ASCII
|
|
764
798
|
characters (spaces only inside) and `host` is reserved. Your server must drop
|
|
765
799
|
any `Nylorun-*` header its own clients send, and only an application key can act
|
|
@@ -777,7 +811,7 @@ and revoking them is management: `admin.signingKeys.list()`, `.rotate()`
|
|
|
777
811
|
A web page or an app calls the Runtime directly with the JWT your identity
|
|
778
812
|
provider gave the person: list the provider in the Runtime's identity file
|
|
779
813
|
([Trusted issuers](../../guides/DEPLOYMENT.md#trusted-issuers)), and the Runtime takes the
|
|
780
|
-
token's subject, scopes
|
|
814
|
+
token's subject, scopes and Agent grants from it. Nylorun mints no
|
|
781
815
|
token and ships no browser client: use your provider's SDK for sign-in and send
|
|
782
816
|
the token as `Authorization: Bearer <token>` with `Nylorun-Protocol`, through a
|
|
783
817
|
reverse proxy that answers CORS (the Runtime sends no CORS headers). An expired
|
package/dist/a2a/handler.d.ts
CHANGED
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
* The Agent Card comes from the Runtime, with this endpoint's URL and the host's provider and
|
|
8
8
|
* security schemes added.
|
|
9
9
|
*/
|
|
10
|
-
import type
|
|
11
|
-
import { type AgentSource, type AgentsClient } from "../client.js";
|
|
10
|
+
import { type AgentsClient } from "../client.js";
|
|
12
11
|
/** A partner as the host authenticated it: its subject, and optionally fewer agents. */
|
|
13
12
|
export interface A2aCaller {
|
|
14
13
|
/** Owner of the partner's tasks, e.g. `a2a:acme`. Keep it stable for a partner. */
|
|
@@ -30,8 +29,14 @@ export interface A2aCardOptions {
|
|
|
30
29
|
securityRequirements?: readonly unknown[];
|
|
31
30
|
}
|
|
32
31
|
export interface A2aHandlerOptions {
|
|
33
|
-
/**
|
|
34
|
-
|
|
32
|
+
/**
|
|
33
|
+
* The Agents this endpoint serves, by id (`client.agents`, protocol 16), or as
|
|
34
|
+
* `client.agents.get` answers them: a partner's tasks run in sessions on them. Nothing else is
|
|
35
|
+
* reachable.
|
|
36
|
+
*/
|
|
37
|
+
agents: readonly (string | {
|
|
38
|
+
readonly id: string;
|
|
39
|
+
})[];
|
|
35
40
|
/**
|
|
36
41
|
* The partner the request is from, authenticated by the host. `undefined` answers `401`;
|
|
37
42
|
* there is no anonymous mode. Not called for the Agent Card, which is public.
|
package/dist/a2a/handler.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An A2A endpoint over the Tenant's agents, as a web-standard `Request` → `Response` handler
|
|
3
|
+
* (gateway mode). The host authenticates each partner (`subject`); the handler forwards the
|
|
4
|
+
* partner's JSON-RPC request to the Runtime as that subject, and the Runtime speaks A2A (Host
|
|
5
|
+
* feature `a2a-endpoint`). It parses no A2A message and sends no header the partner chose
|
|
6
|
+
* except `A2A-Version` and `A2A-Extensions`, so a partner can never name a subject or scopes.
|
|
7
|
+
* The Agent Card comes from the Runtime, with this endpoint's URL and the host's provider and
|
|
8
|
+
* security schemes added.
|
|
9
|
+
*/
|
|
1
10
|
import { createClient } from "../client.js";
|
|
2
11
|
import { IncompatibleRuntimeError, RuntimeError } from "../http.js";
|
|
3
12
|
/** The Host feature the handler forwards to. */
|