@nylorun/agents 0.8.0-beta → 0.10.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 (60) hide show
  1. package/CHANGELOG.md +167 -0
  2. package/README.md +371 -166
  3. package/dist/a2a/handler.d.ts +59 -0
  4. package/dist/a2a/handler.js +178 -0
  5. package/dist/a2a/index.d.ts +8 -0
  6. package/dist/a2a/index.js +7 -0
  7. package/dist/access.d.ts +101 -0
  8. package/dist/access.js +111 -0
  9. package/dist/action-handler.d.ts +64 -0
  10. package/dist/action-handler.js +283 -0
  11. package/dist/ag-ui/handler.d.ts +12 -4
  12. package/dist/ag-ui/handler.js +87 -281
  13. package/dist/ag-ui/index.d.ts +2 -1
  14. package/dist/ag-ui/index.js +2 -1
  15. package/dist/browser.d.ts +97 -0
  16. package/dist/browser.js +277 -0
  17. package/dist/builder.d.ts +20 -0
  18. package/dist/builder.js +26 -0
  19. package/dist/client.d.ts +3 -230
  20. package/dist/client.js +2 -335
  21. package/dist/connection.d.ts +0 -1
  22. package/dist/connection.js +3 -9
  23. package/dist/delivery-token.d.ts +57 -0
  24. package/dist/delivery-token.js +150 -0
  25. package/dist/execute-action.d.ts +6 -0
  26. package/dist/execute-action.js +24 -0
  27. package/dist/http.d.ts +31 -1
  28. package/dist/http.js +74 -14
  29. package/dist/index.d.ts +13 -9
  30. package/dist/index.js +5 -4
  31. package/dist/mcp/index.d.ts +2 -2
  32. package/dist/mcp/index.js +1 -1
  33. package/dist/sandbox/client.d.ts +4 -5
  34. package/dist/sandbox/client.js +4 -8
  35. package/dist/sandbox/index.d.ts +0 -2
  36. package/dist/sandbox/index.js +0 -1
  37. package/dist/served-definitions.d.ts +20 -0
  38. package/dist/served-definitions.js +100 -0
  39. package/dist/session-client.d.ts +282 -0
  40. package/dist/session-client.js +375 -0
  41. package/dist/sse.js +12 -0
  42. package/dist/studio-embed.d.ts +9 -0
  43. package/dist/studio-embed.js +8 -0
  44. package/dist/token-endpoint.d.ts +23 -0
  45. package/dist/token-endpoint.js +30 -0
  46. package/package.json +15 -8
  47. package/dist/ag-ui/history.d.ts +0 -7
  48. package/dist/ag-ui/history.js +0 -60
  49. package/dist/ag-ui/sse.d.ts +0 -9
  50. package/dist/ag-ui/sse.js +0 -7
  51. package/dist/ag-ui/translate.d.ts +0 -38
  52. package/dist/ag-ui/translate.js +0 -222
  53. package/dist/derived-credentials.d.ts +0 -5
  54. package/dist/derived-credentials.js +0 -17
  55. package/dist/executor.d.ts +0 -20
  56. package/dist/executor.js +0 -353
  57. package/dist/mcp/mcp.d.ts +0 -30
  58. package/dist/mcp/mcp.js +0 -80
  59. package/dist/sandbox/sandbox.d.ts +0 -27
  60. package/dist/sandbox/sandbox.js +0 -92
package/README.md CHANGED
@@ -1,51 +1,64 @@
1
1
  # @nylorun/agents
2
2
 
3
- Tenant API client package: definition authoring, a session client, and a
4
- connected customer executor. Depends only on `@nylorun/core` among Nylorun
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
5
5
  packages. A developer application's production tree should contain only this
6
6
  package and `@nylorun/core` from Nylorun. Vocabulary:
7
7
  [runtime/src/CONTEXT.md](../runtime/src/CONTEXT.md).
8
8
 
9
- ## Application entry (preferred)
9
+ ## Application entry (preferred): an Action endpoint
10
10
 
11
11
  ```ts
12
12
  // src/main.ts
13
- import { connectAgents } from "@nylorun/agents";
13
+ import { createServer } from "node:http";
14
+ import { createActionHandler } from "@nylorun/agents";
14
15
  import { agents } from "../agents/index.js";
15
16
 
16
- await connectAgents({ agents }).ready;
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
21
  ```
18
22
 
19
- Application mode saves definitions, registers **derived** executor credentials
20
- (HMAC of the application key + Tenant + agent id), and connects. Restarts and
21
- replicas re-register the same hashes; tokens are never stored in the Project.
22
- The same entry runs under the project's `npm run dev` (`tsx watch`) and as
23
- `node dist/src/main.js` (`npm start`). It finds the Runtime through the three
24
- `NYLORUN_*` variables or the Project link that `npx @nylorun/cli tenant create`
25
- writes; with neither, it fails with `connection_missing` and names those steps. See [MIGRATION.md](../MIGRATION.md#runtime-clients-and-admin-api-breaking-beta)
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, tenant }`
28
+ and it 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 tenant endpoints` shows each endpoint and
31
+ how its deliveries are doing.
32
+
33
+ The URL must be one the Runtime can reach: `localhost` on the local stack
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 three `NYLORUN_*` variables or the Project link
40
+ that `npx @nylorun/cli tenant create` writes; with neither, `register` fails
41
+ with `connection_missing` and names those steps. See [MIGRATION.md](../MIGRATION.md#runtime-clients-and-admin-api-breaking-beta)
26
42
  for upgrading from `nylorun serve`.
27
43
 
28
44
  ## Connection resolution
29
45
 
30
- `resolveConnection` / `createClient()` / `connectAgents({ agents })`:
46
+ `resolveConnection` / `createClient()` / `createActionHandler({ agents })`:
31
47
 
32
48
  1. Explicit `{ url, tenant, key }`
33
- 2. Environment — if any of `NYLORUN_RUNTIME_URL`, `NYLORUN_TENANT`,
34
- `NYLORUN_SERVER_KEY` or `NYLORUN_EXECUTOR_KEY` is set, all required pieces
35
- must be present (role `executor` when `NYLORUN_EXECUTOR_KEY` is set)
36
- 3. Project link — `.nylorun/link.json` + `credentials.json` (application role)
49
+ 2. Environment — if any of `NYLORUN_RUNTIME_URL`, `NYLORUN_TENANT` or
50
+ `NYLORUN_SERVER_KEY` is set, all three must be present
51
+ 3. Project link — `.nylorun/link.json` + `credentials.json` (the application key)
37
52
 
38
53
  Sources never mix. Partial environment fails with `connection_missing`.
39
54
 
40
55
  ```ts
41
- import { Agent, createClient, connectAgents, tool } from "@nylorun/agents";
56
+ import { Agent, createActionHandler, createClient, tool } from "@nylorun/agents";
42
57
  import { z } from "zod";
43
58
 
44
- const assistant = Agent({
45
- id: "assistant",
46
- name: "Assistant",
47
- instructions: "Use the available tools.",
48
- tools: [
59
+ const assistant = Agent({ id: "assistant", name: "Assistant" })
60
+ .instructions("Use the available tools.")
61
+ .tools(
49
62
  tool({
50
63
  name: "lookup",
51
64
  input: z.object({ id: z.string() }),
@@ -53,8 +66,7 @@ const assistant = Agent({
53
66
  return { id, owner: ctx.info };
54
67
  },
55
68
  }),
56
- ],
57
- });
69
+ );
58
70
 
59
71
  // Explicit Tenant API client (options or env / link via createClient()).
60
72
  const client = createClient({
@@ -70,19 +82,22 @@ const session = await client.createSession({
70
82
  await session.input("Look up item 123", { idempotencyKey: requestId });
71
83
  const history = await session.history();
72
84
 
73
- // Executor-only mode when you already hold an executor key:
74
- const connection = connectAgents({
85
+ // A process that only serves Actions holds no key: it reads the Tenant's public keys.
86
+ const actions = createActionHandler({
75
87
  agents: [assistant],
76
88
  implementationVersion: "app-1",
77
89
  runtime: {
78
- url: process.env.NYLORUN_RUNTIME_URL,
79
- key: process.env.NYLORUN_EXECUTOR_KEY,
80
- tenant: process.env.NYLORUN_TENANT,
90
+ url: process.env.NYLORUN_RUNTIME_URL!,
91
+ tenant: process.env.NYLORUN_TENANT!,
81
92
  },
93
+ url: "https://app.example.com/nylorun/actions",
82
94
  onError: console.error,
83
95
  });
84
- await connection.ready;
85
- await connection.close();
96
+ app.post("/nylorun/actions", actions.node);
97
+ // A deploy step with the application key registers the URL once:
98
+ await createActionHandler({ agents: [assistant], client }).register({
99
+ url: "https://app.example.com/nylorun/actions",
100
+ });
86
101
  ```
87
102
 
88
103
  ```sh
@@ -97,68 +112,51 @@ throws `IncompatibleRuntimeError` / `incompatible_host` when the Host range or
97
112
  required features do not match. Re-exports include `PROTOCOL_FEATURES`,
98
113
  `ERROR_CODES` and `compareVersions`.
99
114
 
100
- Load Agent Skills from a local catalog folder with `skills(path)`:
115
+ Load Agent Skills from a local catalog folder with `.skills(path)`:
101
116
 
102
117
  ```ts
103
- import { Agent, skills } from "@nylorun/agents";
104
-
105
- const assistant = Agent({
106
- id: "assistant",
107
- name: "Order assistant",
108
- instructions: "Use lookup_order for orders.",
109
- tools: [lookupOrder],
110
- }).use(skills("./assistant-skills"));
118
+ import { Agent } from "@nylorun/agents";
119
+
120
+ const assistant = Agent({ id: "assistant", name: "Order assistant" })
121
+ .instructions("Use lookup_order for orders.")
122
+ .tools(lookupOrder)
123
+ .skills("./assistant-skills");
111
124
  ```
112
125
 
113
126
  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.
114
127
 
115
- Declare MCP servers with `mcp(...)` (same map shape as agent-plugins `mcpServers` / manifest v3):
128
+ Declare MCP servers with `.mcp(...)` (same map shape as agent-plugins `mcpServers`):
116
129
 
117
130
  ```ts
118
- import { Agent, mcp } from "@nylorun/agents";
119
-
120
- const assistant = Agent({
121
- id: "assistant",
122
- name: "Assistant",
123
- instructions: "Use the available tools.",
124
- }).use(
125
- mcp({
126
- github: {
127
- name: "github",
128
- type: "streamable-http",
129
- url: "https://mcp.example.com/github",
130
- },
131
- }),
132
- );
131
+ import { Agent } from "@nylorun/agents";
132
+
133
+ const assistant = Agent({ id: "assistant", name: "Assistant" })
134
+ .instructions("Use the available tools.")
135
+ .mcp({
136
+ github: { type: "streamable-http", url: "https://mcp.example.com/github" },
137
+ });
133
138
  ```
134
139
 
135
- Each key must equal that server's `name`. Transports follow [Agent Plugins MCP servers](https://agent-plugins.org/plugin-authors/mcp-servers): `stdio`, `streamable-http`, and `sse`. Pass `{ id: "…" }` as the second argument to override the default capability id `"mcp"`.
140
+ 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)`.
136
141
 
137
- Give an agent an isolated computer with `sandbox()`:
142
+ 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:
138
143
 
139
144
  ```ts
140
- import { Agent, sandbox } from "@nylorun/agents";
141
-
142
- const analyst = Agent({
143
- id: "analyst",
144
- instructions: "Analyse the data the user gives you. Use Python.",
145
- }).use(sandbox());
145
+ const session = await client.createSession({
146
+ agentId: "analyst",
147
+ ownerUserId,
148
+ sandbox: {
149
+ network: { allow: ["pypi.org", "files.pythonhosted.org"] }, // no egress unless you list hosts
150
+ resources: { cpus: 2, memory: "2GiB" },
151
+ },
152
+ });
146
153
  ```
147
154
 
148
- The model gets `bash`, `read`, `write`, `edit`, `grep` and `glob` on a Linux machine with a persistent `/workspace`. These tools run in the Runtime, not in your process, so sandbox-only agents need no connected executor. The agent declares what it needs; the Runtime decides where it runs (today an emulated shell in the Runtime process). Every option is optional plain data:
149
-
150
- ```ts
151
- .use(sandbox({
152
- image: "python:3.13", // any OCI image; default python:3.13-slim
153
- network: { preset: "dev", allow: ["api.example.com"] }, // "none" | "dev" (default) | "open"
154
- resources: { cpus: 2, memory: "2GiB" },
155
- idle: "15m", // stop compute when idle; files persist
156
- }))
157
- ```
155
+ 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`.
158
156
 
159
- The `dev` preset allows package registries and code hosts. Private networks, loopback, the host and cloud metadata endpoints are always blocked. Options from the full design that are not in this version (`setup`, `files`, `secrets`, `mount`, `onStart`, …) throw a `SandboxError` that says so.
157
+ `sandbox` takes `false` for none, `{ session }` to share another session's sandbox, or an inline sandbox as above; omit it for the Tenant's default. 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
158
 
161
- Put an agent in another agent's `tools` to let the model delegate to it:
159
+ Add an agent with `.subagents()` to let the model delegate to it:
162
160
 
163
161
  ```ts
164
162
  import { Agent } from "@nylorun/agents";
@@ -168,54 +166,47 @@ const researcher = Agent({
168
166
  id: "researcher",
169
167
  description:
170
168
  "Investigates an order's history. Returns a short summary with the ids it relied on.",
171
- instructions:
172
- "Investigate one question about one order. Be exhaustive, then be brief.",
173
- tools: [searchOrders, readTicket],
174
- outputSchema: z.object({
175
- summary: z.string(),
176
- evidence: z.array(z.string()),
177
- }),
178
- });
169
+ })
170
+ .instructions("Investigate one question about one order. Be exhaustive, then be brief.")
171
+ .tools(searchOrders, readTicket)
172
+ .output(z.object({ summary: z.string(), evidence: z.array(z.string()) }));
179
173
 
180
- const support = Agent({
181
- id: "support",
182
- instructions:
174
+ const support = Agent({ id: "support" })
175
+ .instructions(
183
176
  "For anything needing more than two lookups, delegate to researcher with a complete, self-contained task.",
184
- tools: [lookupOrder, refundOrder, researcher],
185
- });
177
+ )
178
+ .tools(lookupOrder, refundOrder)
179
+ .subagents(researcher);
186
180
  ```
187
181
 
188
- 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 executor you already run for the parent (`connectAgents({ 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).
182
+ 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).
189
183
 
190
- Delegate when the parent should keep the answer. When a specialist should own the rest of the conversation, switch capabilities with a `before("step")` 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.
184
+ 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.
191
185
 
192
- ## Workflows
186
+ 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.
193
187
 
194
- Nest `Chain`, `Switch`, `Parallel`, `Map`, and `Loop` around agents and `tool()` to
195
- build durable flows. A workflow is a definition with `kind: "workflow"`: register
196
- it in `export const agents` (or `saveAgent(workflow)` — referenced agents are
197
- saved first), then `createSession({ agentId })` and `session.input(value)` like
198
- any agent. `input` sends string values as `content` and other JSON as `data`.
199
- `connectAgents({ agents: [workflow] })` saves the tree and connects executors for
200
- the workflow id and every referenced agent. Tool nodes, pure functions (slot
201
- `input`, Switch `on`, Map `over`, Loop `decide`), and Loop `verify` run as Actions
202
- routed by `(workflowId, key)`.
188
+ ## Flow agents
189
+
190
+ An agent's body is either a ReAct loop (the model decides) or a flow (your code
191
+ decides). A flow agent is built from stages, each shaped `.stage(whatRuns, { how })`,
192
+ and is registered, saved and opened as a session like any agent: put it in
193
+ `export const agents`, `saveAgent(flowAgent)`, then `createSession({ agentId })`
194
+ and `session.input(value)`. `input` sends string values as `content` and other JSON
195
+ as `data`. On the wire a flow agent is a workflow manifest v2 (`kind: "workflow"`,
196
+ `workflowSchemaVersion: 2`) that embeds the agents it runs, so one document and one
197
+ manifest hash cover the whole flow; its agents are not listed on their own.
203
198
 
204
199
  ```ts
205
- import { Agent, Chain, Loop, Map, tool } from "@nylorun/agents";
200
+ import { Agent, tool } from "@nylorun/agents";
206
201
  import { z } from "zod";
207
202
 
208
- const planner = Agent({
209
- id: "planner",
210
- instructions: "Return { tasks: string[] }.",
211
- outputSchema: z.object({ tasks: z.array(z.string()) }),
212
- }).build();
203
+ const planner = Agent({ id: "planner" })
204
+ .instructions("Return { tasks: string[] }.")
205
+ .output(z.object({ tasks: z.array(z.string()) }));
213
206
 
214
- const coder = Agent({
215
- id: "coder",
216
- instructions: "Implement one task. Return { summary }.",
217
- outputSchema: z.object({ summary: z.string() }),
218
- }).build();
207
+ const coder = Agent({ id: "coder" })
208
+ .instructions("Implement one task. Return { summary }.")
209
+ .output(z.object({ summary: z.string() }));
219
210
 
220
211
  const openPr = tool({
221
212
  name: "open-pr",
@@ -226,44 +217,52 @@ const openPr = tool({
226
217
  },
227
218
  });
228
219
 
229
- export const shipFeature = Chain({
230
- id: "ship-feature",
231
- steps: [
232
- planner,
233
- Map({
234
- id: "implement",
235
- over: (plan) => plan.tasks,
236
- each: Loop({
237
- id: "code",
238
- run: coder,
239
- verify: () => ({ pass: true }),
240
- decide: ({ output }) => ({ output }),
241
- }),
220
+ export const shipFeature = Agent({ id: "ship-feature" })
221
+ .step(planner)
222
+ .map(
223
+ Agent({ id: "code" }).loop(coder, {
224
+ verify: ({ output }) => (output.summary ? { pass: true } : { pass: false, feedback: "Say what you changed." }),
225
+ max: 2,
242
226
  }),
243
- {
244
- run: openPr,
245
- input: ({ value }) => ({
246
- summaries: value.map((item: { summary: string }) => item.summary),
247
- }),
248
- },
249
- ],
250
- });
227
+ { id: "implement", input: ({ input }) => input.tasks },
228
+ )
229
+ .step(openPr, { input: ({ input }) => ({ summaries: input.map((item) => item.summary) }) });
251
230
 
252
231
  export const agents = [shipFeature];
253
232
  ```
254
233
 
255
- | Primitive | Role |
234
+ | Stage | Role |
256
235
  | --- | --- |
257
- | **Chain** | Steps in order; slots reshape with `{ run, id?, input? }` |
258
- | **Switch** | Pure `on(input) → key`, then the matching case (or `default`) |
259
- | **Parallel** | Fixed named branches at once; output is an object |
260
- | **Map** | Pure `over(input) → items`, one child per item; output is an array |
261
- | **Loop** | `run` → `verify` → `decide` until `{ output }` |
262
-
263
- Agents inside a workflow keep their own sessions (linked from the workflow
264
- session) and share one sandbox: declare identical specs on every agent that
265
- uses one, attach with `createSession({ …, sandbox: { session } })`, call
266
- built-ins via `session.sandbox` (application) or `ctx.sandbox` (executor).
236
+ | **`.step(x, { id?, input? })`** | Run one agent, tool or `flow()`; its output is the next stage's input |
237
+ | **`.switch({ ...cases, default? }, { on })`** | `on({ input })` returns a case name; exactly that case runs |
238
+ | **`.parallel(branches)`** | Fixed named branches at once, same input; output is an object |
239
+ | **`.map(each)`** | Run `each` once per item of the input, which must be an array; output is an array |
240
+ | **`.loop(body, { verify, max?, decide? })`** | Run, verify, run again with the feedback until it passes; needs `max` or `decide` |
241
+
242
+ Every function receives one object: `{ input, results, flowInput }`. `input` is
243
+ what the stage received, `results` holds earlier steps' outputs by step id, and
244
+ `flowInput` is the agent's own input. The `input` option computes a stage's input;
245
+ the `id` option names a step. `flow()` builds a sequence with no id for a case,
246
+ branch, map item or loop body that is more than one step. Types flow from each
247
+ step's `.output()` schema to the next stage's `input`.
248
+
249
+ Agents inside a flow keep their own sessions, linked from the flow's session and
250
+ named by the agent: a step's id (the agent's id, or `{ id }`), with `[i]` for each
251
+ Map item and a nested flow agent's id in front of its own agents'. Control stages
252
+ add nothing, so wrapping a step in `.loop()` or moving it between cases keeps its
253
+ session. An agent may appear once per flow; use it again under a new id with
254
+ `.step(writer, { id: "final-writer" })` or `writer.withId("…")`
255
+ (`flow.duplicate-leaf`). Functions are bound under stage keys: a stage's `id`, or
256
+ its position such as `@1.default.1`, plus `:input`, `:on`, `:verify` or `:decide`.
257
+ Name the stages you may reorder.
258
+
259
+ `Chain`, `Switch`, `Parallel`, `Map` and `Loop` still build workflow manifest v1
260
+ directly and run as before; a flow agent can't be a child of them.
261
+
262
+ The agents in a flow share one sandbox: open the flow's session with it,
263
+ `createSession({ …, sandbox: { … } })`, and every agent, tool step and `verify` in the
264
+ flow uses it. Share it with another session with `sandbox: { session }`, and call
265
+ built-ins via `session.sandbox` (application) or `ctx.sandbox` (a tool).
267
266
  Observe with `session.observe({ follow: true })` to merge linked agent streams;
268
267
  `pending()` lists waits across the tree. Studio renders the manifest tree and
269
268
  live node status. Source examples:
@@ -276,23 +275,32 @@ Use `session.observe({ cursor, signal })` for resumable canonical events, `sessi
276
275
 
277
276
  Connection defaults follow `resolveConnection` (options → environment → Project
278
277
  link). Implementation version defaults from `NYLORUN_IMPLEMENTATION_VERSION`,
279
- then `dev`. Application mode of `connectAgents({ agents })` registers derived
280
- executor principals; executor-only mode still takes an explicit executor key.
281
- Agents do not hash the manifest, and an in-flight action stays claimable after
278
+ then `dev`. `register` saves the definitions and registers one endpoint per
279
+ served agent with the application key; serving deliveries needs no key.
280
+ Agents do not hash the manifest, and an in-flight action is still delivered after
282
281
  the registered digest changes. Model selection and model credentials belong to
283
282
  the Tenant.
284
283
 
285
- After registration, `connectAgents` opens authenticated fetch SSE before discovering actions, rediscovers after reconnection, and claims pending actions for a connected agent id. It renews leases while executing, then retries HTTP result delivery with the same recorded outcome and idempotency key. Reconnect delay is bounded at 30 seconds. Notifications confer no execution authority. There is no periodic action-discovery polling. Close aborts the stream, HTTP requests and lease timers and signals running functions; JavaScript cannot forcibly terminate a function that ignores its signal. The host makes expired in-flight actions uncertain instead of automatically repeating external effects.
284
+ 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.
286
285
 
287
- 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 executor 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`.
286
+ 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`.
288
287
 
289
288
  ## AG-UI
290
289
 
291
- `@nylorun/agents/ag-ui` serves the Tenant's agents to any
292
- [AG-UI](https://docs.ag-ui.com) client (`@ag-ui/client`'s `HttpAgent`,
293
- CopilotKit, a desktop renderer) from your own server. Your server signs people
294
- in and names the person each request is for; the handler maps each AG-UI thread
295
- to one session per person, agent and thread, and streams it as AG-UI events.
290
+ The Runtime serves the Tenant's agents to any [AG-UI](https://docs.ag-ui.com)
291
+ client (`@ag-ui/client`'s `HttpAgent`, CopilotKit, a desktop renderer) at
292
+ `/v1/ag-ui/agents/{agentId}` (optional feature `ag-ui-endpoint`). Each AG-UI
293
+ thread is one session per person, agent and thread. There are two ways in:
294
+
295
+ - **From your own server**, with `@nylorun/agents/ag-ui` (below): your server
296
+ signs people in and the handler forwards each request to the Runtime acting
297
+ for that person. The browser never sees the Runtime.
298
+ - **From the page itself**, with `@nylorun/agents/browser`'s `agUi()` and a
299
+ subject token ([In the browser](#in-the-browser)). No chat traffic passes
300
+ through your server.
301
+
302
+ Both reach the same threads: one started through the handler continues from
303
+ the page.
296
304
 
297
305
  ```ts
298
306
  import { createServer } from "node:http";
@@ -318,9 +326,10 @@ createServer(toNodeListener(agui)).listen(3000);
318
326
 
319
327
  `run`, `history`, `reattach` and `cancel` are also on the handler for your own
320
328
  routing. The default `client` is `createClient()`; pass `client` to use another
321
- one, and `session(subject, agentId)` to add the person's `vaultIds` or
322
- `credentialSelections` (keep `info` stable: it is part of the session's
323
- identity).
329
+ one, and `session(subject, agentId)` to add the person's `vaultIds`,
330
+ `credentialSelections` or `info`. They apply when the thread's session is
331
+ created, on its first run; later runs keep them. Whatever the browser sends in
332
+ `forwardedProps.nylorun` is replaced.
324
333
 
325
334
  - Each AG-UI message id is the command's idempotency key: a retried run replays
326
335
  the same turn instead of starting a second one.
@@ -338,9 +347,9 @@ identity).
338
347
  `scopes` defaults to `["sessions:own"]`; add `"vaults:own"` when `session()`
339
348
  attaches the person's vaults. A `subject` the Runtime cannot name (see below)
340
349
  answers `500` (`subject_invalid`).
341
- - The handler needs a Runtime with the optional features `transcript-events`
342
- and `subject-headers` and answers `502` (`runtime_feature_missing`) without
343
- them.
350
+ - The handler needs a Runtime with the optional feature `ag-ui-endpoint` and
351
+ answers `502` (`runtime_feature_missing`) without it. It loads no AG-UI
352
+ package itself: the Runtime does the protocol work.
344
353
 
345
354
  A complete web backend with a test is in
346
355
  [examples/src/ag-ui](../examples/README.md#an-agent-in-your-web-app-ag-ui).
@@ -349,8 +358,71 @@ Limitations: assistant text arrives once per model step (no token streaming);
349
358
  no reasoning, state, activity or subagent events, and an agent used as a tool
350
359
  shows only its result; frontend tools in `RunAgentInput.tools` are rejected
351
360
  (`400`); one text part per user message; earlier messages cannot be edited or
352
- regenerated. Browsers always go through your server: never let one call the
353
- Runtime directly.
361
+ regenerated. A browser reaches the Runtime only with a subject token and a
362
+ publishable key, never with a Tenant key.
363
+
364
+ ## A2A
365
+
366
+ `@nylorun/agents/a2a` serves the Tenant's agents to other agents over
367
+ [A2A](https://a2a-protocol.org) 1.0 (JSON-RPC) from your own server: the
368
+ gateway. Your server authenticates each partner and names the subject its tasks
369
+ belong to. The handler forwards each request to the Runtime, which speaks A2A,
370
+ so the handler loads no A2A package and parses no message.
371
+
372
+ ```ts
373
+ import { createServer } from "node:http";
374
+ import { createA2aHandler, toNodeListener } from "@nylorun/agents/a2a";
375
+ import support from "./agents/support.js";
376
+
377
+ const a2a = createA2aHandler({
378
+ basePath: "/a2a",
379
+ agents: [support], // nothing else is reachable
380
+ // undefined → 401; return { subject, agents } to give a partner fewer agents
381
+ subject: (request) => partnerFor(request.headers.get("x-partner-key"))?.subject,
382
+ publicUrl: "https://api.example.com/a2a", // for the card; default: the request's origin
383
+ card: {
384
+ provider: { organization: "Example Inc.", url: "https://example.com" },
385
+ securitySchemes: {
386
+ partnerKey: { apiKeySecurityScheme: { location: "header", name: "X-Partner-Key" } },
387
+ },
388
+ securityRequirements: [{ schemes: { partnerKey: { list: [] } } }],
389
+ },
390
+ });
391
+
392
+ createServer(toNodeListener(a2a)).listen(3000);
393
+ ```
394
+
395
+ | Method and path under `basePath` | Operation |
396
+ | --- | --- |
397
+ | `GET /{agentId}/.well-known/agent-card.json` | The Agent Card: name, description and one skill from the manifest, with this endpoint's URL and your provider and security schemes. Public; `subject` is not called |
398
+ | `POST /{agentId}` | One A2A JSON-RPC request, answered by the Runtime |
399
+
400
+ What the Runtime answers (Host feature `a2a-endpoint`):
401
+
402
+ - `SendMessage` waits until the task completes, fails, is canceled or asks
403
+ for input, for at most 5 minutes (then it returns `WORKING`); with
404
+ `configuration.returnImmediately` it returns at once and the partner polls
405
+ `GetTask`. `CancelTask` works on a running task.
406
+ - A `contextId` is one session per partner, agent and context; a task is one
407
+ turn. A message without `taskId` starts a task (one at a time per context).
408
+ A question from the agent pauses the task as `TASK_STATE_INPUT_REQUIRED`
409
+ with the question as the status message; the partner answers with a message
410
+ on the same `taskId`.
411
+ - The `messageId` is the idempotency key: a retried message returns the same
412
+ task.
413
+ - Text parts, or a single data part, in; the turn's output as the artifact
414
+ `output` (text, or JSON as a data part). Files are
415
+ `ContentTypeNotSupportedError`.
416
+ - Clients must send `A2A-Version: 1.0` (header or `?A2A-Version=`).
417
+ - Not yet: `ListTasks`, streaming and `SubscribeToTask`
418
+ (`UnsupportedOperationError`; the card says `streaming: false`), push
419
+ notifications, the extended card, and approvals. An agent that pauses for an
420
+ approval shows `INPUT_REQUIRED`, refuses a reply, and can be canceled, so do
421
+ not publish agents with approval-gated tools yet.
422
+ - The handler calls the Runtime as the partner's subject with the scope
423
+ `sessions:own` only, so one partner never reaches another's tasks. It sends
424
+ no header the partner chose except `A2A-Version` and `A2A-Extensions`. Without
425
+ `a2a-endpoint` on the Runtime it answers `502` (`runtime_feature_missing`).
354
426
 
355
427
  ## Acting for a person (app servers)
356
428
 
@@ -383,10 +455,143 @@ compatibility check, so calling `as()` per request is cheap.
383
455
  | `agents:write` | Saving agents; listing agents, models and providers |
384
456
  | `tenant:settings` | The Tenant's status, model provider and sandbox settings |
385
457
 
386
- No scope reaches Tenant reset, config seed, executors, actions or the sandbox
458
+ No scope reaches Tenant reset, config seed, Action endpoints, actions or the sandbox
387
459
  tool routes; call those without `as()`. A subject is 1–200 visible ASCII
388
460
  characters (spaces only inside) and `host` is reserved. Your server must drop
389
461
  any `Nylorun-*` header its own clients send, and only an application key can act
390
- for a subject: an executor key that tries is `403`.
462
+ for a subject.
463
+
464
+ ## Minting subject tokens (app servers)
465
+
466
+ To let a person's browser or app call the Runtime directly, your server mints a
467
+ short-lived **subject token** for them instead of carrying their requests
468
+ (optional feature `subject-tokens`). What a token may do comes from the Tenant's
469
+ access policy, not from the caller:
470
+
471
+ ```ts
472
+ import { createClient } from "@nylorun/agents";
473
+
474
+ const app = createClient(); // the Tenant key, on the server only
475
+
476
+ // Once: roles, their agents and limits (or `nylo access policy init`).
477
+ await app.access.putPolicy({
478
+ version: 1,
479
+ roles: {
480
+ user: {
481
+ scopes: ["sessions:own", "agents:read"],
482
+ agents: ["support"],
483
+ limits: { turnsPerHour: 60, concurrentTurns: 2 },
484
+ },
485
+ },
486
+ anon: { scopes: [], agents: [] },
487
+ tokens: { maxTtlSeconds: 600 },
488
+ });
489
+
490
+ // Per signed-in person, from a same-origin POST route:
491
+ const { token, expiresAt } = await app.tokens.create({
492
+ subject: `app:${user.id}`,
493
+ role: "user",
494
+ });
495
+ ```
496
+
497
+ The client sends it as `Authorization: Bearer <token>` with `Nylorun-Tenant`.
498
+ Tokens carry only `sessions:own`, `vaults:own` and `agents:read`, live at most
499
+ 15 minutes, and see only `{ agentId, name, description }` of their role's
500
+ agents. A token that expired or was revoked answers `401` with
501
+ `code: "token_expired"`: fetch a new one and retry. Starting more turns than the
502
+ role allows answers `429` with `code: "limit_exceeded"` and `Retry-After`.
503
+
504
+ | Call | Does |
505
+ | --- | --- |
506
+ | `app.access.revokeSubject(subject)` | Ends every token minted so far for the person and their open event streams |
507
+ | `app.access.signingKeys.rotate()` | Rotates the Tenant's signing keys without signing anyone out (`{ force: true }` for incidents) |
508
+ | `app.access.signingKeys.list()`, `.revoke(kid)`, `app.access.jwks()` | Inspect and retire keys; the public keys |
509
+
510
+ Keep tokens in memory on the client, never in `localStorage`, and serve the
511
+ minting route without CORS. `nylo access …` does the same from the terminal.
512
+
513
+ `createTokenEndpoint` is that route, ready to mount (Next.js, Hono or any
514
+ `Request` → `Response` handler):
515
+
516
+ ```ts
517
+ import { createTokenEndpoint } from "@nylorun/agents";
518
+
519
+ export const POST = createTokenEndpoint({
520
+ client: app,
521
+ role: "user",
522
+ subject: (request) => userFromCookie(request)?.id,
523
+ });
524
+ ```
525
+
526
+ ## In the browser
527
+
528
+ `@nylorun/agents/browser` calls the Runtime from a web page or an app with a
529
+ publishable key and the token route above (optional feature `browser-access`).
530
+ It loads no Node module.
531
+
532
+ ```ts
533
+ import { createBrowserClient } from "@nylorun/agents/browser";
534
+
535
+ const nylo = createBrowserClient({
536
+ url: "https://runtime.example.com",
537
+ publishableKey: "nr_pub_tn_…", // app.access.publishableKeys.create({ name, origins })
538
+ token: () => fetch("/api/nylorun/token", { method: "POST" }).then((r) => r.json()),
539
+ });
540
+
541
+ const session = await nylo.createSession({ agentId: "support" }); // owned by the token's subject
542
+ await session.input("Where is my order?", { idempotencyKey: crypto.randomUUID() });
543
+ for await (const event of session.observe()) console.log(event.type);
544
+ ```
545
+
546
+ For a chat UI, `agUi()` gives `@ag-ui/client`'s `HttpAgent` a `fetch` with the
547
+ key and a current token:
548
+
549
+ ```ts
550
+ import { HttpAgent } from "@ag-ui/client";
551
+
552
+ const { url, fetch } = nylo.agUi("support");
553
+ const agent = new HttpAgent({
554
+ url,
555
+ fetch,
556
+ threadId,
557
+ initialMessages: await nylo.agUiHistory("support", threadId),
558
+ });
559
+ ```
560
+
561
+ When the Runtime ends a run's stream because the token expired or was revoked,
562
+ that `fetch` reattaches from the last event with a new token, so the agent sees
563
+ one run. A person's token may attach their own vaults to a new thread
564
+ (`forwardedProps: { nylorun: { session: { vaultIds } } }`) but not set `info`.
565
+
566
+ The client keeps the token in memory, fetches a new one a minute before it
567
+ expires or when the Runtime answers `401 token_expired`, and never asks for two
568
+ at once. Event streams that the Runtime ends at token expiry or revocation
569
+ reconnect from their last event with a new token. Create a publishable key per
570
+ app with the origins that serve it (`http://localhost:*` for development);
571
+ requests from other origins get the opaque `404`.
572
+
573
+ 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).
574
+
575
+ ## Embedding Studio (desktop apps)
576
+
577
+ `@nylorun/agents/studio-embed` is the contract between Studio and an app that
578
+ shows it in an iframe, such as Babai. It exports the message schema
579
+ (`StudioEmbedMessageSchema`, envelope `{ type: "nylorun.studio", protocol, kind }`),
580
+ the login-token and session schemas, `STUDIO_EMBED_PROTOCOLS` and
581
+ `parseFrameAncestors`.
582
+
583
+ ```ts
584
+ import { StudioEmbedMessageSchema } from "@nylorun/agents/studio-embed";
585
+
586
+ window.addEventListener("message", (event) => {
587
+ if (event.source !== frame.contentWindow || event.origin !== studioOrigin) return;
588
+ const message = StudioEmbedMessageSchema.safeParse(event.data);
589
+ if (!message.success) return;
590
+ if (message.data.kind === "ready") frame.contentWindow!.postMessage(init, studioOrigin);
591
+ });
592
+ ```
391
593
 
392
- The SDK depends only on core within the Nylorun packages; installing it does not install harness. `/ag-ui` adds `@ag-ui/core`; nothing else imports it. Use `/define`, `/client`, `/executor` or `/ag-ui` for focused imports, or the root for convenience. Studio uses `/client`. See [the adopted host contract](../harness/HOST_CONTRACT.md).
594
+ The embedder's backend mints the login token with the admin key
595
+ (`mintStudioLoginToken` in `@nylorun/admin`) and the page passes it to Studio in
596
+ `init`. No key reaches the page. Studio's origin allowlist is
597
+ `NYLORUN_STUDIO_FRAME_ANCESTORS`.