@nylorun/agents 0.13.0-beta → 0.16.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/README.md +220 -235
  3. package/dist/access.d.ts +6 -97
  4. package/dist/access.js +1 -103
  5. package/dist/ag-ui/handler.d.ts +3 -6
  6. package/dist/ag-ui/handler.js +2 -2
  7. package/dist/artifacts.d.ts +1 -1
  8. package/dist/files.d.ts +31 -0
  9. package/dist/files.js +60 -0
  10. package/dist/http.d.ts +6 -13
  11. package/dist/http.js +4 -16
  12. package/dist/index.d.ts +8 -16
  13. package/dist/index.js +4 -7
  14. package/dist/plugins/load.d.ts +3 -2
  15. package/dist/plugins/load.js +40 -100
  16. package/dist/plugins/plugin.d.ts +6 -1
  17. package/dist/plugins/plugin.js +20 -7
  18. package/dist/reads.d.ts +17 -0
  19. package/dist/reads.js +17 -0
  20. package/dist/sandboxes.d.ts +29 -0
  21. package/dist/sandboxes.js +10 -1
  22. package/dist/session-client.d.ts +94 -112
  23. package/dist/session-client.js +80 -107
  24. package/dist/skills/index.d.ts +1 -1
  25. package/dist/skills/load.d.ts +14 -3
  26. package/dist/skills/load.js +50 -32
  27. package/dist/skills/skills.d.ts +6 -4
  28. package/dist/skills/skills.js +17 -10
  29. package/dist/sse.js +1 -1
  30. package/dist/studio-embed.d.ts +1 -1
  31. package/dist/studio-embed.js +1 -1
  32. package/package.json +3 -7
  33. package/dist/action-handler.d.ts +0 -63
  34. package/dist/action-handler.js +0 -279
  35. package/dist/browser.d.ts +0 -104
  36. package/dist/browser.js +0 -281
  37. package/dist/delivery-token.d.ts +0 -62
  38. package/dist/delivery-token.js +0 -159
  39. package/dist/execute-action.d.ts +0 -16
  40. package/dist/execute-action.js +0 -288
  41. package/dist/plugins/launch.d.ts +0 -17
  42. package/dist/plugins/launch.js +0 -71
  43. package/dist/sandbox/client.d.ts +0 -63
  44. package/dist/sandbox/client.js +0 -29
  45. package/dist/sandbox/index.d.ts +0 -2
  46. package/dist/sandbox/index.js +0 -1
  47. package/dist/served-definitions.d.ts +0 -20
  48. package/dist/served-definitions.js +0 -100
  49. package/dist/token-endpoint.d.ts +0 -23
  50. package/dist/token-endpoint.js +0 -30
package/README.md CHANGED
@@ -1,49 +1,85 @@
1
1
  # @nylorun/agents
2
2
 
3
- Tenant API client package: definition authoring, a session client, and the
4
- Action endpoint that runs your tools (`createActionHandler`). Depends only on `@nylorun/core` among Nylorun
3
+ Runtime API client package: definition authoring and a session client. The Tenant's
4
+ settings, models, vaults, signing keys and application keys are the Management
5
+ API's, through [`@nylorun/admin`](../admin/README.md). Depends only on `@nylorun/core` among Nylorun
5
6
  packages. A developer application's production tree should contain only this
6
7
  package and `@nylorun/core` from Nylorun. Vocabulary:
7
8
  [runtime/src/CONTEXT.md](../runtime/src/CONTEXT.md).
8
9
 
9
- ## Application entry (preferred): an Action endpoint
10
+ ## Application entry: save your agents
10
11
 
11
12
  ```ts
12
13
  // src/main.ts
13
- import { createServer } from "node:http";
14
- import { createActionHandler } from "@nylorun/agents";
14
+ import { createClient } from "@nylorun/agents";
15
15
  import { agents } from "../agents/index.js";
16
16
 
17
- const url = process.env.NYLORUN_ACTIONS_URL ?? "http://localhost:3001/nylorun/actions";
18
- const actions = createActionHandler({ agents, url });
19
- createServer(actions.node).listen(3001); // or actions.fetch in Hono, Next.js, Workers, Bun
20
- await actions.register({ url });
17
+ const client = createClient();
18
+ for (const agent of agents) await client.saveAgent(agent);
21
19
  ```
22
20
 
23
- The Runtime delivers each tool call, hook and workflow function of these agents
24
- to `url`, signed with a short-lived **delivery token** that the handler checks
25
- (Tenant, URL, Action, generation and body) before any code runs. `register`
26
- saves the definitions, registers the URL and pings it through the Runtime. A
27
- process that only serves Actions needs no key: pass `runtime: { url }` and it
28
- reads the Tenant's public keys. Mark a long tool
29
- `tool({ …, background: true })`: the handler answers at once, heartbeats and
30
- posts the result. `npx @nylorun/cli endpoints` shows each endpoint and how its
31
- deliveries are doing.
32
-
33
- The URL must be one the Runtime can reach: `localhost` on a local Tenant
34
- (its Runtime runs in Docker and maps `localhost` to this machine), a public URL
35
- in production, or a tunnel (ngrok, Cloudflare Tunnel) for a remote Runtime.
36
-
37
- `connectAgents` and executors were removed in protocol 3: mount
38
- `createActionHandler` instead (see [MIGRATION.md](../MIGRATION.md)). The handler
39
- finds the Runtime through the two `NYLORUN_*` variables or the Project link
40
- that `npx nylorun start` writes; with neither, `register` fails with
41
- `connection_missing` and names those steps. See [MIGRATION.md](../MIGRATION.md#runtime-clients-and-admin-api-breaking-beta)
42
- for upgrading from `nylorun serve`.
21
+ The Runtime runs each agent from its manifest and the files saved with it (skills), and
22
+ never calls your code during a session. Your services are reached as tools: [HTTP
23
+ tools](#http-tools) and remote MCP servers. `saveAgent` refuses, before sending anything, an
24
+ agent with a tool that would run your code (`tool({ run })`) or a flow agent with a tool
25
+ stage: make it an `http()` tool or serve it from a remote MCP server. `tool({ run })` still
26
+ runs in the local engine (`@nylorun/harness/run`). `implementationVersion` defaults to
27
+ `NYLORUN_IMPLEMENTATION_VERSION`, else `dev`.
28
+
29
+ `createClient()` finds the Runtime through the two `NYLORUN_*` variables or the Project link
30
+ that `npx nylorun start` writes; with neither, it fails with `connection_missing` and names
31
+ those steps. Action endpoints (`createActionHandler`), and before them `connectAgents` and
32
+ executors, were removed; see [MIGRATION.md](../MIGRATION.md#action-endpoints-are-removed).
33
+
34
+ ## HTTP tools
35
+
36
+ A tool can be one HTTP request to your service, made by the Runtime itself: no code of
37
+ yours runs during the session.
38
+
39
+ ```ts
40
+ import { Agent, http } from "@nylorun/agents";
41
+ import { z } from "zod";
42
+
43
+ const refundOrder = http({
44
+ name: "refund_order",
45
+ description: "Refund an order.",
46
+ input: z.object({ orderId: z.string(), amount: z.number() }), // or a JSON Schema object
47
+ output: z.object({ refundId: z.string() }), // optional
48
+ url: "https://billing.example.com/refunds",
49
+ method: "POST", // default; or PUT, PATCH
50
+ credential: "billing", // optional: a vault credential bound to this URL
51
+ timeoutMs: 20_000, // default 60000, at most 300000
52
+ approval: "always", // optional: each call waits for session.approve()
53
+ });
54
+
55
+ export const support = Agent({ id: "support" }).tools(refundOrder);
56
+ ```
57
+
58
+ The tool goes into the manifest as `http` (and `approval`), and the Runtime's
59
+ Tool Gate sends the model's input as the JSON body, under the Host's address
60
+ policy (`NYLORUN_ENDPOINT_*`), with `content-type: application/json`,
61
+ `Nylorun-Session-Id`, `Nylorun-Turn-Id`, `Nylorun-Agent-Id` and an
62
+ `Idempotency-Key` that is the same when the Runtime re-sends the call after a
63
+ restart, so your service can drop duplicates. A `2xx` JSON answer is the output
64
+ (text when it is not JSON and the tool has no `output`), checked against
65
+ `output`. Any other status (with the start of its body), a timeout, a refused
66
+ address or a mismatched output is a tool error the model sees. A call whose
67
+ answer was lost with the gateway is `uncertain` and never sent again.
68
+
69
+ `credential` works as for a remote MCP server: the session's attached vaults
70
+ must hold a credential bound to the tool's exact URL (a `credentialSelections`
71
+ entry whose `serverName` is the `credential` picks one when several are), else
72
+ the operator's credential resolver is asked; without one the call fails before
73
+ it is sent. `approval: "always"` pauses the turn for `session.approve()`; a
74
+ denied call never runs and the model sees the denial. A remote MCP server takes
75
+ `approval: "always"` too, for every one of its tools:
76
+ `.mcp({ shop: { type: "streamable-http", url, approval: "always" } })`.
77
+ An HTTP tool is also a flow stage (see [Flow agents](#flow-agents)), and runs
78
+ only on the Runtime (a local `run()` reports `http.runtime-only`).
43
79
 
44
80
  ## Connection resolution
45
81
 
46
- `resolveConnection` / `createClient()` / `createActionHandler({ agents })`:
82
+ `resolveConnection` / `createClient()`:
47
83
 
48
84
  1. Explicit `{ url, key }`
49
85
  2. Environment — if `NYLORUN_RUNTIME_URL` or `NYLORUN_SERVER_KEY` is set, both
@@ -59,22 +95,20 @@ Runtime serves one Tenant, so nothing names it: the `tenant` option is gone
59
95
  is ignored.
60
96
 
61
97
  ```ts
62
- import { Agent, createActionHandler, createClient, tool } from "@nylorun/agents";
98
+ import { Agent, createClient, http } from "@nylorun/agents";
63
99
  import { z } from "zod";
64
100
 
65
101
  const assistant = Agent({ id: "assistant", name: "Assistant" })
66
102
  .instructions("Use the available tools.")
67
103
  .tools(
68
- tool({
104
+ http({
69
105
  name: "lookup",
70
106
  input: z.object({ id: z.string() }),
71
- async run({ id }, ctx) {
72
- return { id, owner: ctx.info };
73
- },
107
+ url: "https://items.example.com/lookup",
74
108
  }),
75
109
  );
76
110
 
77
- // Explicit Tenant API client (options or env / link via createClient()).
111
+ // Explicit Runtime API client (options or env / link via createClient()).
78
112
  const client = createClient({
79
113
  url: process.env.NYLORUN_RUNTIME_URL,
80
114
  key: process.env.NYLORUN_SERVER_KEY,
@@ -86,20 +120,6 @@ const session = await client.createSession({
86
120
  });
87
121
  await session.input("Look up item 123", { idempotencyKey: requestId });
88
122
  const history = await session.history();
89
-
90
- // A process that only serves Actions holds no key: it reads the Tenant's public keys.
91
- const actions = createActionHandler({
92
- agents: [assistant],
93
- implementationVersion: "app-1",
94
- runtime: { url: process.env.NYLORUN_RUNTIME_URL! },
95
- url: "https://app.example.com/nylorun/actions",
96
- onError: console.error,
97
- });
98
- app.post("/nylorun/actions", actions.node);
99
- // A deploy step with the application key registers the URL once:
100
- await createActionHandler({ agents: [assistant], client }).register({
101
- url: "https://app.example.com/nylorun/actions",
102
- });
103
123
  ```
104
124
 
105
125
  ```sh
@@ -125,7 +145,9 @@ const assistant = Agent({ id: "assistant", name: "Order assistant" })
125
145
  .skills("./assistant-skills");
126
146
  ```
127
147
 
128
- Each subdirectory under the catalog must contain a `SKILL.md` with YAML frontmatter (`name`, `description`) per [Agent Skills](https://agentskills.io/home). Supporting files (for example `references/`) are available through `read_skill_resource` after `load_skill`. The helper sets both the manifest skill catalog and the on-disk skill records so you do not duplicate content.
148
+ 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`), at most 500 files of 10 MiB each. `saveAgent` uploads the files the Runtime does not hold (`client.files`, `PUT /v1/files/sha256:<hex>`) before it puts the definition, which the Runtime refuses while it names a file it lacks (`400 definition_files_missing`).
149
+
150
+ The Runtime serves the skills itself, with no call to your process: `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>/`, so the model can run a skill's scripts with `bash`. Run without a Runtime, the skill tools only say so.
129
151
 
130
152
  Declare MCP servers with `.mcp(...)` (same map shape as agent-plugins `mcpServers`):
131
153
 
@@ -139,7 +161,7 @@ const assistant = Agent({ id: "assistant", name: "Assistant" })
139
161
  });
140
162
  ```
141
163
 
142
- Each key names a server; its `name` defaults to the key and, when given, must equal it. Repeated `.mcp()` calls add servers to the same capability. Transports follow [Agent Plugins MCP servers](https://agent-plugins.org/plugin-authors/mcp-servers): `stdio`, `streamable-http`, and `sse`. Attach an Agent Plugin package with `.plugin(path)`.
164
+ Each key names a server; its `name` defaults to the key and, when given, must equal it. Repeated `.mcp()` calls add servers to the same capability. Nylorun accepts remote servers only: `streamable-http` and `sse` ([Agent Plugins MCP servers](https://agent-plugins.org/plugin-authors/mcp-servers)); a `stdio` server is refused, so run it behind an HTTP transport and declare its URL. Attach an Agent Plugin package with `.plugin(path)`: its skills and remote MCP servers join the agent, and a stdio server in its `mcp.json` throws.
143
165
 
144
166
  Give a session a sandbox when you open it. The agent declares nothing, so the same agent runs with or without one, in any Tenant:
145
167
 
@@ -154,7 +176,7 @@ const session = await client.createSession({
154
176
  });
155
177
  ```
156
178
 
157
- The model gets `bash`, `read`, `write`, `edit`, `grep` and `glob` on a persistent `/workspace`. These tools run in the Runtime, not in your process, so sandbox-only agents need no Action endpoint. The Runtime decides where the sandbox runs: today an emulated shell in the Runtime process, which is not a VM boundary and takes no `image`.
179
+ The model gets `bash`, `read`, `write`, `edit`, `grep` and `glob` on a persistent `/workspace`. These tools run in the Runtime, not in your process. The Runtime decides where the sandbox runs: today an emulated shell in the Runtime process, which is not a VM boundary and takes no `image`.
158
180
 
159
181
  `sandbox` takes `false` for none, `{ id }` to attach a sandbox resource (below), or an inline sandbox as above; omit it for the Tenant's default. `{ session }`, sharing another session's sandbox, still works and is deprecated. The Runtime checks it against the Tenant's limits (`GET`/`PUT /v1/tenant/sandbox`: a network ceiling, a resource maximum, the idle timeout) and answers `400` with every problem it finds. A caller acting for a user (`app.as(...)`) can't define one inline; it gets the Tenant's default or `false`. Private networks, loopback, the host and cloud metadata endpoints are always blocked.
160
182
 
@@ -171,7 +193,7 @@ await client.sandboxes.ensure("team-a/proj-42", { labels: { project: "acme" } })
171
193
  await client.createSession({ agentId: "analyst", ownerUserId, sandbox: { id: "team-a/proj-42" } });
172
194
  ```
173
195
 
174
- Sessions attached to one sandbox share its `/workspace`, and one turn runs in it at a time: a second session's turn is refused with `409 sandbox_busy` until the first ends. Deleting a session only detaches it. A spec is fixed once the sandbox exists; `labels` can change. `client.sandboxes.list({ labels })`, `get(id)` and `delete(id)` manage them, and `npx nylorun sandbox ls | rm` does the same on a local Tenant. A subject token reaches only the sandboxes it is minted for, checked at every turn start: `client.tokens.create({ subject, role, sandboxes: ["team-a/*", "user-42"] })` takes exact ids or prefixes ending in `/*`. Creating and deleting through a subject token needs a role with `sandboxes:write`. The Tenant holds at most `limits.sandboxes` of them (`PUT /v1/tenant/sandbox`, default 100).
196
+ Sessions attached to one sandbox share its `/workspace`, and one turn runs in it at a time: a second session's turn is refused with `409 sandbox_busy` until the first ends. Deleting a session only detaches it. A spec is fixed once the sandbox exists; `labels` can change. `client.sandboxes.list({ labels })`, `get(id)` and `delete(id)` manage them, and `npx nylorun sandbox ls | rm` does the same on a local Tenant. A trusted issuer's token reaches only the sandboxes its issuer grants (the identity file's `sandboxes` templates, exact ids or prefixes ending in `/*`), checked at every turn start; creating and deleting through one needs `sandboxes:write`. The Tenant holds at most `limits.sandboxes` of them (`PUT /v1/tenant/sandbox`, default 100).
175
197
 
176
198
  Add an agent with `.subagents()` to let the model delegate to it:
177
199
 
@@ -196,35 +218,46 @@ const support = Agent({ id: "support" })
196
218
  .subagents(researcher);
197
219
  ```
198
220
 
199
- The tool is named after the agent's `id` and takes `{ task: string }`; the agent's `description` (required) is what the parent's model reads to decide when to delegate. The child starts with a fresh context: it sees its own instructions and the task, nothing of the parent's conversation, and only its final text (or `outputSchema` result) comes back. It keeps its own tools, hooks, skills and MCP servers, served by the Action endpoint you already mount for the parent (`createActionHandler({ agents: [support] })` serves both), shares the session's sandbox, and starts with empty `ctx.state`. Tools can read `ctx.agent` (`{ id, path, delegationId }`). Several delegation calls in one model response run in parallel. An empty answer, a failure (with the child's last text marked as evidence) or a cancelled child reaches the parent's model as a failed tool result, never as success. The Runtime emits `delegation.started` and `delegation.completed`; `session.history({ agent })` filters by `delegationId` (one child invocation) or by path such as `support/researcher` (every concurrent child that shares that path).
221
+ The tool is named after the agent's `id` and takes `{ task: string }`; the agent's `description` (required) is what the parent's model reads to decide when to delegate. The child starts with a fresh context: it sees its own instructions and the task, nothing of the parent's conversation, and only its final text (or `outputSchema` result) comes back. It keeps its own tools, skills and MCP servers, saved with the parent, shares the session's sandbox, and starts with empty `ctx.state`. Tools can read `ctx.agent` (`{ id, path, delegationId }`). Several delegation calls in one model response run in parallel. An empty answer, a failure (with the child's last text marked as evidence) or a cancelled child reaches the parent's model as a failed tool result, never as success. The Runtime emits `delegation.started` and `delegation.completed`; `session.history({ agent })` filters by `delegationId` (one child invocation) or by path such as `support/researcher` (every concurrent child that shares that path).
200
222
 
201
- A flow agent can be a subagent too: `.subagents(researchFlow)` with a `description` like any other. Its manifest is inlined in the parent's, so it is saved with the parent and served by the parent's Action endpoint. When the model calls it, the flow runs on the Runtime in its own linked session (a fresh one per call, linked from the parent's with a `node.agent` event at `support/<flow id>`), its agents in theirs, and the flow's output comes back as the tool result; a failed flow is a failed tool result. Cancelling the parent cancels the flow. Flow agents as subagents run on the Runtime only, not in a local `run()`. An approval asked for inside the flow waits on the flow's own session.
223
+ A flow agent can be a subagent too: `.subagents(researchFlow)` with a `description` like any other. Its manifest is inlined in the parent's, so it is saved with the parent. When the model calls it, the flow runs on the Runtime in its own linked session (a fresh one per call, linked from the parent's with a `node.agent` event at `support/<flow id>`), its agents in theirs, and the flow's output comes back as the tool result; a failed flow is a failed tool result. Cancelling the parent cancels the flow. Flow agents as subagents run on the Runtime only, not in a local `run()`. An approval asked for inside the flow waits on the flow's own session.
202
224
 
203
- Delegate when the parent should keep the answer. When a specialist should own the rest of the conversation, switch capabilities with a `.beforeModel()` patch instead. This version is one level deep and non-interactive: a delegated agent cannot use agents as tools, its tools cannot declare `approval` (keep those on the parent), and `ctx.ask`, `ctx.approve`, `ctx.sleep` or `ctx.waitFor` inside it fail with `delegation.interaction-unsupported`. Delegation is not an approval boundary; approvals live on tools.
225
+ Delegate when the parent should keep the answer. When a specialist should own the rest of the conversation, send the next message with a turn manifest that has the specialist's instructions instead. This version is one level deep and non-interactive: a delegated agent cannot use agents as tools, its tools cannot declare `approval` (keep those on the parent), and `ctx.ask`, `ctx.approve`, `ctx.sleep` or `ctx.waitFor` inside it fail with `delegation.interaction-unsupported`. Delegation is not an approval boundary; approvals live on tools.
204
226
 
205
227
  ## Flow agents
206
228
 
207
- An agent's body is either a ReAct loop (the model decides) or a flow (your code
208
- decides). A flow agent is built from stages, each shaped `.stage(whatRuns, { how })`,
209
- and is registered, saved and opened as a session like any agent: put it in
210
- `export const agents`, `saveAgent(flowAgent)`, then `createSession({ agentId })`
211
- and `session.input(value)`. `input` sends string values as `content` and other JSON
212
- as `data`. On the wire a flow agent is a workflow manifest v2 (`kind: "workflow"`,
213
- `workflowSchemaVersion: 2`) that embeds the agents it runs, so one document and one
214
- manifest hash cover the whole flow; its agents are not listed on their own.
229
+ An agent's body is either a ReAct loop (the model decides) or a flow (its manifest
230
+ decides). A flow agent is built from stages and is registered, saved and opened as a
231
+ session like any agent: put it in `export const agents`, `saveAgent(flowAgent)`, then
232
+ `createSession({ agentId })` and `session.input(value)`. `input` sends string values as
233
+ `content` and other JSON as `data`. On the wire a flow agent is a workflow manifest v3
234
+ (`kind: "workflow"`, `workflowSchemaVersion: 3`) that embeds the agents it runs, so one
235
+ document and one manifest hash cover the whole flow; its agents are not listed on their own.
236
+
237
+ A flow runs no code of yours: the first stage gets the flow's input, and every later stage
238
+ gets the previous stage's output. So each agent returns, through its `.output()` schema,
239
+ what the next stage needs.
215
240
 
216
241
  ```ts
217
- import { Agent, tool } from "@nylorun/agents";
242
+ import { Agent, VerdictSchema, tool } from "@nylorun/agents";
218
243
  import { z } from "zod";
219
244
 
220
245
  const planner = Agent({ id: "planner" })
221
- .instructions("Return { tasks: string[] }.")
222
- .output(z.object({ tasks: z.array(z.string()) }));
246
+ .instructions("Return { items: string[] }, one task each.")
247
+ .output(z.object({ items: z.array(z.string()) }));
223
248
 
224
249
  const coder = Agent({ id: "coder" })
225
250
  .instructions("Implement one task. Return { summary }.")
226
251
  .output(z.object({ summary: z.string() }));
227
252
 
253
+ const reviewer = Agent({ id: "reviewer" })
254
+ .instructions("Pass the response when its summary says what changed.")
255
+ .output(VerdictSchema);
256
+
257
+ const prWriter = Agent({ id: "pr-writer" })
258
+ .instructions("Return the summaries you are given as { summaries }.")
259
+ .output(z.object({ summaries: z.array(z.string()) }));
260
+
228
261
  const openPr = tool({
229
262
  name: "open-pr",
230
263
  input: z.object({ summaries: z.array(z.string()) }),
@@ -235,52 +268,88 @@ const openPr = tool({
235
268
  });
236
269
 
237
270
  export const shipFeature = Agent({ id: "ship-feature" })
238
- .step(planner)
239
- .map(
240
- Agent({ id: "code" }).loop(coder, {
241
- verify: ({ output }) => (output.summary ? { pass: true } : { pass: false, feedback: "Say what you changed." }),
242
- max: 2,
243
- }),
244
- { id: "implement", input: ({ input }) => input.tasks },
245
- )
246
- .step(openPr, { input: ({ input }) => ({ summaries: input.map((item) => item.summary) }) });
271
+ .pipe(planner)
272
+ .map(Agent({ id: "code" }).loop(coder, { verify: reviewer, max: 2 }), { id: "implement" })
273
+ .pipe(prWriter, openPr);
247
274
 
248
275
  export const agents = [shipFeature];
249
276
  ```
250
277
 
251
278
  | Stage | Role |
252
279
  | --- | --- |
253
- | **`.step(x, { id?, input? })`** | Run one agent, tool or `flow()`; its output is the next stage's input |
254
- | **`.switch({ ...cases, default? }, { on })`** | `on({ input })` returns a case name; exactly that case runs |
280
+ | **`.pipe(a, b, …)`** | Run agents, tools or `flow()`s in order; each one's output is the next one's input |
281
+ | **`.switch({ ...cases, default? })`** | Run the case the previous output names: a string, or its `route` field |
255
282
  | **`.parallel(branches)`** | Fixed named branches at once, same input; output is an object |
256
- | **`.map(each)`** | Run `each` once per item of the input, which must be an array; output is an array |
257
- | **`.loop(body, { verify, max?, decide? })`** | Run, verify, run again with the feedback until it passes; needs `max` or `decide` |
283
+ | **`.map(each)`** | Run `each` once per item of the previous output: an array, or its `items`; output is an array |
284
+ | **`.loop(body, { verify, max })`** | Run `body`, ask `verify` (an agent, or `http({ url })`), run again with its feedback until it passes, at most `max` times |
285
+
286
+ Every stage takes `{ id }` to name it (`.loop` takes it beside `verify` and `max`).
287
+ `flow()` builds a sequence with no id for a case, branch, map item or loop body that
288
+ is more than one step. A verifier agent gets `{ task, response, iteration }` and returns
289
+ a verdict, `{ pass, feedback? }` (`VerdictSchema`), with feedback when it fails; the
290
+ verdict is recorded as a `loop.verified` event. An agent after the first stage also
291
+ sees the flow's input, as the original request, before its own input. Types flow from
292
+ each stage's `.output()` schema to the flow agent's output. `.step(x, { id })` is a
293
+ deprecated alias for `.pipe(x.withId(id))`.
294
+
295
+ An [HTTP tool](#http-tools) is a stage too: in `.pipe()`, a case, a Map item or a Loop
296
+ body. Its input is the previous output, which must match the tool's `input` (an
297
+ object); a mismatch the build can see, such as an agent with no `.output()` before it,
298
+ is refused (`flow.input-mismatch`), and any other fails the stage when it runs
299
+ (`tool.invalid-input`). The Runtime makes the request as for an agent's HTTP tool, with
300
+ `Nylorun-Agent-Id` set to the flow agent's id and `credential` taken from the flow
301
+ session's vaults; its answer is the next stage's input. A failure status, a timeout or
302
+ an answer that does not match `output` fails the stage with its code (`http.status`,
303
+ `http.timeout`, `tool.invalid-output`, …). A Loop body that starts with an HTTP stage
304
+ is retried with the Loop's input, not the feedback. `approval: "always"` is not
305
+ supported on a flow stage yet (`flow.approval-unsupported`).
306
+
307
+ A Loop may be judged by your service instead of an agent: `http({ url, method?,
308
+ credential?, timeoutMs? })`, with no name or input, is an HTTP verifier. The Runtime
309
+ sends `{ input, output, iteration }` (the Loop's input, the attempt's output, its
310
+ number) and reads a verdict, `{ pass, feedback? }`, from the answer; anything else, or a
311
+ failed request, fails the Loop (`loop.verify-failed`). Its verdicts are recorded as
312
+ `loop.verified`, as an agent's are.
258
313
 
259
- Every function receives one object: `{ input, results, flowInput }`. `input` is
260
- what the stage received, `results` holds earlier steps' outputs by step id, and
261
- `flowInput` is the agent's own input. The `input` option computes a stage's input;
262
- the `id` option names a step. `flow()` builds a sequence with no id for a case,
263
- branch, map item or loop body that is more than one step. Types flow from each
264
- step's `.output()` schema to the next stage's `input`.
314
+ ```ts
315
+ import { Agent, http } from "@nylorun/agents";
316
+ import { z } from "zod";
317
+
318
+ const order = Agent({ id: "order" })
319
+ .instructions("Name the order to refund as { orderId }.")
320
+ .output(z.object({ orderId: z.string() }));
321
+
322
+ const refund = http({
323
+ name: "refund",
324
+ input: z.object({ orderId: z.string() }),
325
+ output: z.object({ refundId: z.string() }),
326
+ url: "https://billing.example.com/refunds",
327
+ credential: "billing",
328
+ });
329
+
330
+ const fixer = Agent({ id: "fixer" }).instructions("Fix the failing test.");
331
+
332
+ export const refunds = Agent({ id: "refunds" }).pipe(order, refund);
333
+ export const fixTests = Agent({ id: "fix-tests" }).loop(fixer, {
334
+ verify: http({ url: "https://ci.example.com/verify", credential: "ci" }),
335
+ max: 3,
336
+ });
337
+ ```
265
338
 
266
339
  Agents inside a flow keep their own sessions, linked from the flow's session and
267
- named by the agent: a step's id (the agent's id, or `{ id }`), with `[i]` for each
268
- Map item and a nested flow agent's id in front of its own agents'. Control stages
269
- add nothing, so wrapping a step in `.loop()` or moving it between cases keeps its
270
- session. An agent may appear once per flow; use it again under a new id with
271
- `.step(writer, { id: "final-writer" })` or `writer.withId("…")`
272
- (`flow.duplicate-leaf`). Functions are bound under stage keys: a stage's `id`, or
273
- its position such as `@1.default.1`, plus `:input`, `:on`, `:verify` or `:decide`.
274
- Name the stages you may reorder.
275
-
276
- `Chain`, `Switch`, `Parallel`, `Map` and `Loop` still build workflow manifest v1
277
- directly and run as before; a flow agent can't be a child of them.
340
+ named by the agent: its id (or `.withId("…")`), with `[i]` for each Map item and a
341
+ nested flow agent's id in front of its own agents'. Control stages add nothing, so
342
+ wrapping a step in `.loop()` or moving it between cases keeps its session. An agent
343
+ may appear once per flow; use it again under a new id with `writer.withId("final-writer")`
344
+ (`flow.duplicate-leaf`). A code tool stage (`.pipe(tool({ run }))`) runs only in the local
345
+ engine: the Runtime runs no code of yours, so `saveAgent` refuses a flow agent with one. Use
346
+ an HTTP stage instead.
278
347
 
279
348
  The agents in a flow share one sandbox: open the flow's session with it,
280
- `createSession({ …, sandbox: { … } })`, and every agent, tool step and `verify` in the
281
- flow uses it. Share it with other sessions by opening them all on one sandbox resource
349
+ `createSession({ …, sandbox: { … } })`, and every agent in the flow uses it. Share it
350
+ with other sessions by opening them all on one sandbox resource
282
351
  (`sandbox: { id }`, see `client.sandboxes`), and call
283
- built-ins via `session.sandbox` (application) or `ctx.sandbox` (a tool).
352
+ built-ins via `session.sandbox` (application).
284
353
  Observe with `session.observe({ follow: true })` to merge linked agent streams;
285
354
  `pending()` lists waits across the tree. Studio renders the manifest tree and
286
355
  live node status. Source examples:
@@ -293,15 +362,15 @@ Use `session.observe({ cursor, signal })` for resumable canonical events, `sessi
293
362
 
294
363
  Connection defaults follow `resolveConnection` (options → environment → Project
295
364
  link). Implementation version defaults from `NYLORUN_IMPLEMENTATION_VERSION`,
296
- then `dev`. `register` saves the definitions and registers one endpoint per
297
- served agent with the application key; serving deliveries needs no key.
298
- Agents do not hash the manifest, and an in-flight action is still delivered after
299
- the registered digest changes. Model selection and model credentials belong to
365
+ then `dev`. Agents do not hash the manifest; the Runtime pins the digest of the
366
+ definition a session opens with. Model selection and model credentials belong to
300
367
  the Tenant.
301
368
 
302
- After registration, the Runtime POSTs each Action to the endpoint with a delivery token signed by the Tenant's key; the handler verifies it before any code runs and answers with the outcome. A background tool answers `202`, heartbeats with the newest token and posts its result. A cancel aborts the request, which reaches the tool as `ctx.signal`; JavaScript cannot forcibly terminate a function that ignores its signal. The host makes lost in-flight deliveries uncertain instead of automatically repeating external effects.
303
-
304
- Tools receive state, info, identity, resume, signal, approval/response helpers and memoized `step`. Step outcomes survive a persisted wait result; they do not establish exactly-once external effects after an unacknowledged crash. `sleep` and `waitFor` currently return inspectable deferred outcomes; automatic timer/event wakeups remain runtime implementation work. Remote `onModelCall` convenience and progress-event transport are not supplied in this pass. Arbitrary middleware closures are rejected for durable definitions; use `before`/`after` hooks. The Action endpoint runs every capability registered at a hook point in one action. Agent definitions have no `.run()`; explicit local execution is available through `@nylorun/harness/run`.
369
+ The Runtime makes a lost HTTP tool call `uncertain` instead of repeating its external effect.
370
+ In the local engine (`@nylorun/harness/run`), `tool({ run })` tools receive state, info,
371
+ identity, resume, signal, approval/response helpers and memoized `step`. Arbitrary middleware
372
+ closures are rejected for durable definitions. Agent definitions have no `.run()`; explicit
373
+ local execution is available through `@nylorun/harness/run`.
305
374
 
306
375
  ## AG-UI
307
376
 
@@ -313,9 +382,10 @@ thread is one session per person, agent and thread. There are two ways in:
313
382
  - **From your own server**, with `@nylorun/agents/ag-ui` (below): your server
314
383
  signs people in and the handler forwards each request to the Runtime acting
315
384
  for that person. The browser never sees the Runtime.
316
- - **From the page itself**, with `@nylorun/agents/browser`'s `agUi()` and a
317
- subject token ([In the browser](#in-the-browser)). No chat traffic passes
318
- through your server.
385
+ - **From the page itself**, with `@ag-ui/client`'s `HttpAgent` and a `fetch`
386
+ that sends your identity provider's token
387
+ ([In the browser](#in-the-browser)). No chat traffic passes through your
388
+ server.
319
389
 
320
390
  Both reach the same threads: one started through the handler continues from
321
391
  the page.
@@ -362,8 +432,7 @@ created, on its first run; later runs keep them. Whatever the browser sends in
362
432
  - A busy or paused thread ends the run with `RUN_ERROR` code `session_busy`.
363
433
  - The handler calls the Runtime as each person (`client.as(subject, { scopes })`,
364
434
  below), so the Runtime itself keeps one person out of another's threads.
365
- `scopes` defaults to `["sessions:own"]`; add `"vaults:own"` when `session()`
366
- attaches the person's vaults. A `subject` the Runtime cannot name (see below)
435
+ `scopes` defaults to `["sessions:own"]`. A `subject` the Runtime cannot name (see below)
367
436
  answers `500` (`subject_invalid`).
368
437
  - The handler needs a Runtime with the optional feature `ag-ui-endpoint` and
369
438
  answers `502` (`runtime_feature_missing`) without it. It loads no AG-UI
@@ -376,8 +445,8 @@ Limitations: assistant text arrives once per model step (no token streaming);
376
445
  no reasoning, state, activity or subagent events, and an agent used as a tool
377
446
  shows only its result; frontend tools in `RunAgentInput.tools` are rejected
378
447
  (`400`); one text part per user message; earlier messages cannot be edited or
379
- regenerated. A browser reaches the Runtime only with a subject token and a
380
- publishable key, never with a Tenant key.
448
+ regenerated. A browser reaches the Runtime only with a trusted issuer's token,
449
+ never with a key.
381
450
 
382
451
  ## A2A
383
452
 
@@ -467,7 +536,7 @@ made as an artifact of the session; each new artifact or version appears in the
467
536
  session's events as `artifact.created` or `artifact.version.created`. The Tenant's
468
537
  limits (`PUT /v1/tenant/artifacts`: 100 MiB per file and 10 GiB in all by default)
469
538
  refuse a larger upload with `413 limit_exceeded`, and nothing is stored. Acting for
470
- a person (`as()`, a subject token), a client reaches only the artifacts of that
539
+ a person (`as()`, or a trusted issuer's token), a client reaches only the artifacts of that
471
540
  person's sessions, and an upload names one of them.
472
541
 
473
542
  ### Outputs: folder artifacts
@@ -495,155 +564,69 @@ completes.
495
564
  ## Acting for a person (app servers)
496
565
 
497
566
  A server that signs people in and calls the Runtime for them (an "app server")
498
- keeps the Tenant key to itself and names the person on each call:
567
+ keeps its application key to itself and names the person on each call:
499
568
 
500
569
  ```ts
501
570
  import { createClient } from "@nylorun/agents";
502
571
 
503
- const app = createClient(); // the Tenant key, on the server only
572
+ const app = createClient(); // the application key, on the server only
504
573
 
505
574
  // Per request, after your own sign-in:
506
- const person = app.as(`app:${user.id}`, { scopes: ["sessions:own", "vaults:own"] });
575
+ const person = app.as(`app:${user.id}`, { scopes: ["sessions:own"] });
507
576
  await person.createSession({ agentId: "support", ownerUserId: `app:${user.id}` });
508
577
  await person.listSessions(); // only this person's sessions
509
578
  ```
510
579
 
511
580
  `as()` sends `Nylorun-Subject` and `Nylorun-Scopes` on every call, event
512
581
  streams included, and the Runtime (optional feature `subject-headers`)
513
- enforces both: another person's sessions and vaults answer the same `404` as
514
- missing ones, and a route outside the scopes answers `403` (`scope_required`).
582
+ enforces both: another person's sessions answer the same `404` as missing
583
+ ones, and a route outside the scopes answers `403` (`scope_required`).
515
584
  Nothing is minted, cached or refreshed, and the copy shares the client's
516
585
  compatibility check, so calling `as()` per request is cheap.
517
586
 
518
587
  | Scope | Allows |
519
588
  | --- | --- |
520
589
  | `sessions:own` | The person's own sessions: create, list, read, stream, message, approve, respond, cancel; and their artifacts |
521
- | `vaults:own` | The person's own vaults and credentials |
522
590
  | `agents:read` | Listing the Tenant's agents |
523
- | `agents:write` | Saving agents; listing agents, models and providers |
524
- | `tenant:settings` | The Tenant's status, model provider and sandbox settings |
525
-
526
- No scope reaches Tenant reset, config seed, Action endpoints, actions or the sandbox
527
- tool routes; call those without `as()`. A subject is 1–200 visible ASCII
591
+ | `agents:write` | Saving agents; listing agents |
592
+
593
+ No scope reaches the sandbox tool routes; call them without `as()`. `tenant:settings` is retired (protocol 8): it grants
594
+ nothing, and no subject reaches the Management API (`/v1/tenant/*`). Vaults are
595
+ the installation's, created through the Management API with a management key
596
+ (`admin.vaults.create({ scope: "installation", … })` in
597
+ [`@nylorun/admin`](../admin/README.md)); a session attaches them with
598
+ `vaultIds`, and a person's own credentials come from your credential resolver
599
+ ([DEPLOYMENT.md](../DEPLOYMENT.md#credentials)). A subject is 1–200 visible ASCII
528
600
  characters (spaces only inside) and `host` is reserved. Your server must drop
529
601
  any `Nylorun-*` header its own clients send, and only an application key can act
530
602
  for a subject.
531
603
 
532
- ## Minting subject tokens (app servers)
533
-
534
- To let a person's browser or app call the Runtime directly, your server mints a
535
- short-lived **subject token** for them instead of carrying their requests
536
- (optional feature `subject-tokens`). What a token may do comes from the Tenant's
537
- access policy, not from the caller:
538
-
539
- ```ts
540
- import { createClient } from "@nylorun/agents";
541
-
542
- const app = createClient(); // the Tenant key, on the server only
543
-
544
- // Once: roles, their agents and limits (or `nylo access policy init`).
545
- await app.access.putPolicy({
546
- version: 1,
547
- roles: {
548
- user: {
549
- scopes: ["sessions:own", "agents:read"],
550
- agents: ["support"],
551
- limits: { turnsPerHour: 60, concurrentTurns: 2 },
552
- },
553
- },
554
- anon: { scopes: [], agents: [] },
555
- tokens: { maxTtlSeconds: 600 },
556
- });
557
-
558
- // Per signed-in person, from a same-origin POST route:
559
- const { token, expiresAt } = await app.tokens.create({
560
- subject: `app:${user.id}`,
561
- role: "user",
562
- });
563
- ```
564
-
565
- The client sends it as `Authorization: Bearer <token>`.
566
- Tokens carry only `sessions:own`, `vaults:own` and `agents:read`, live at most
567
- 15 minutes, and see only `{ agentId, name, description }` of their role's
568
- agents. A token that expired or was revoked answers `401` with
569
- `code: "token_expired"`: fetch a new one and retry. Starting more turns than the
570
- role allows answers `429` with `code: "limit_exceeded"` and `Retry-After`.
571
-
572
- | Call | Does |
573
- | --- | --- |
574
- | `app.access.revokeSubject(subject)` | Ends every token minted so far for the person and their open event streams |
575
- | `app.access.signingKeys.rotate()` | Rotates the Tenant's signing keys without signing anyone out (`{ force: true }` for incidents) |
576
- | `app.access.signingKeys.list()`, `.revoke(kid)`, `app.access.jwks()` | Inspect and retire keys; the public keys |
577
-
578
- Keep tokens in memory on the client, never in `localStorage`, and serve the
579
- minting route without CORS. `nylo access …` does the same from the terminal.
580
-
581
- `createTokenEndpoint` is that route, ready to mount (Next.js, Hono or any
582
- `Request` → `Response` handler):
583
-
584
- ```ts
585
- import { createTokenEndpoint } from "@nylorun/agents";
586
-
587
- export const POST = createTokenEndpoint({
588
- client: app,
589
- role: "user",
590
- subject: (request) => userFromCookie(request)?.id,
591
- });
592
- ```
604
+ The Tenant's signing keys sign capability links and run tokens.
605
+ `app.access.jwks()` reads the public keys (`GET /v1/access/jwks`, no key
606
+ needed). Listing, rotating
607
+ and revoking them is management: `admin.signingKeys.list()`, `.rotate()`
608
+ (`{ force: true }` for incidents) and `.revoke(kid)` in `@nylorun/admin`, or
609
+ `nylo access signing-keys …` from the terminal.
593
610
 
594
611
  ## In the browser
595
612
 
596
- `@nylorun/agents/browser` calls the Runtime from a web page or an app with a
597
- publishable key and the token route above (optional feature `browser-access`).
598
- It loads no Node module.
599
-
600
- ```ts
601
- import { createBrowserClient } from "@nylorun/agents/browser";
602
-
603
- const nylo = createBrowserClient({
604
- url: "https://runtime.example.com",
605
- publishableKey: "nr_pub_tn_…", // app.access.publishableKeys.create({ name, origins })
606
- token: () => fetch("/api/nylorun/token", { method: "POST" }).then((r) => r.json()),
607
- });
608
-
609
- const session = await nylo.createSession({ agentId: "support" }); // owned by the token's subject
610
- await session.input("Where is my order?", { idempotencyKey: crypto.randomUUID() });
611
- for await (const event of session.observe()) console.log(event.type);
612
- ```
613
-
614
- For a chat UI, `agUi()` gives `@ag-ui/client`'s `HttpAgent` a `fetch` with the
615
- key and a current token:
616
-
617
- ```ts
618
- import { HttpAgent } from "@ag-ui/client";
619
-
620
- const { url, fetch } = nylo.agUi("support");
621
- const agent = new HttpAgent({
622
- url,
623
- fetch,
624
- threadId,
625
- initialMessages: await nylo.agUiHistory("support", threadId),
626
- });
627
- ```
628
-
629
- When the Runtime ends a run's stream because the token expired or was revoked,
630
- that `fetch` reattaches from the last event with a new token, so the agent sees
631
- one run. A person's token may attach their own vaults to a new thread
632
- (`forwardedProps: { nylorun: { session: { vaultIds } } }`) but not set `info`.
633
-
634
- The client keeps the token in memory, fetches a new one a minute before it
635
- expires or when the Runtime answers `401 token_expired`, and never asks for two
636
- at once. Event streams that the Runtime ends at token expiry or revocation
637
- reconnect from their last event with a new token. Create a publishable key per
638
- app with the origins that serve it (`http://localhost:*` for development);
639
- requests from other origins get the opaque `404`.
613
+ A web page or an app calls the Runtime directly with the JWT your identity
614
+ provider gave the person: list the provider in the Runtime's identity file
615
+ ([Trusted issuers](../DEPLOYMENT.md#trusted-issuers)), and the Runtime takes the
616
+ token's subject, scopes, agents and sandbox grants from it. Nylorun mints no
617
+ token and ships no browser client: use your provider's SDK for sign-in and send
618
+ the token as `Authorization: Bearer <token>` with `Nylorun-Protocol`, through a
619
+ reverse proxy that answers CORS (the Runtime sends no CORS headers). An expired
620
+ token answers `401` with `code: "token_expired"`; `GET /v1/me` shows what a token
621
+ renders to. For a chat UI, give `@ag-ui/client`'s `HttpAgent` a `fetch` that adds
622
+ those headers.
640
623
 
641
- The SDK depends only on core within the Nylorun packages; installing it does not install harness or any AG-UI or A2A package. Use `/define`, `/client`, `/ag-ui`, `/a2a` or `/browser` for focused imports, or the root for convenience. Studio uses `/client`. See [the adopted host contract](../harness/HOST_CONTRACT.md).
624
+ The SDK depends only on core within the Nylorun packages; installing it does not install harness or any AG-UI or A2A package. Use `/define`, `/client`, `/ag-ui` or `/a2a` for focused imports, or the root for convenience. Studio uses `/client`. See [the adopted host contract](../harness/HOST_CONTRACT.md).
642
625
 
643
626
  ## Embedding Studio (desktop apps)
644
627
 
645
628
  `@nylorun/agents/studio-embed` is the contract between Studio and an app that
646
- shows it in an iframe, such as Babai. It exports the message schema
629
+ shows it in an iframe. It exports the message schema
647
630
  (`StudioEmbedMessageSchema`, envelope `{ type: "nylorun.studio", protocol, kind }`),
648
631
  the login-token and session schemas, `STUDIO_EMBED_PROTOCOLS` and
649
632
  `parseFrameAncestors`.
@@ -662,4 +645,6 @@ window.addEventListener("message", (event) => {
662
645
  The embedder's backend mints the login token with the admin key
663
646
  (`mintStudioLoginToken` in `@nylorun/admin`) and the page passes it to Studio in
664
647
  `init`. No key reaches the page. Studio's origin allowlist is
665
- `NYLORUN_STUDIO_FRAME_ANCESTORS`.
648
+ `NYLORUN_STUDIO_FRAME_ANCESTORS`, empty by default: list your app's origins with
649
+ `nylorun start --studio-embed-origin <origin>`. The message `open.session`
650
+ (`{ sessionId }`) asks the embedding app to open that session in its own UI.