@nylorun/agents 0.7.0-beta → 0.9.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,72 @@
1
1
  # @nylorun/agents
2
2
 
3
+ ## 0.9.0-beta
4
+
5
+ ### Minor Changes
6
+
7
+ - e537a82: **One `Agent` builder: named methods and flow agents.** Every capability has its own method, and deterministic workflows are written on the same builder. ReAct agent manifests are unchanged: the new syntax compiles to exactly what the old syntax produced.
8
+
9
+ - ReAct agents: `.instructions()`, `.tools()`, `.subagents()`, `.mcp()` (a server's `name` defaults to its key; calls merge), `.skills()`, `.plugin()`, `.capability()`, `.sandbox()`, `.beforeTurn()`, `.beforeModel()`, `.afterModel()`, `.afterTurn()`, `.output()`. `capability({ id })` returns a builder with the same methods.
10
+ - Flow agents: `.step()`, `.switch({ ...cases, default }, { on })`, `.parallel()`, `.map()` (runs over its input), `.loop(body, { verify, max | decide })`, plus `.input()`, `.output()`, `.sandbox()`, `flow()` for nested sequences and `.withId()`. Functions receive `{ input, results, flowInput }`, typed from each step's output schema. A flow agent compiles to a workflow manifest (v2, see the flow-manifest-v2 changeset).
11
+ - Build diagnostics: `agent.mixed-body`, `flow.no-model`, `agent.single-value`, `flow.empty`, `loop.max-required`, `loop.invalid-max`, `mcp.duplicate-server`, `delegation.flow-unsupported`.
12
+ - `sandbox()` and `mcp()` move to `@nylorun/core/define` (still exported from `@nylorun/agents`).
13
+ - Deprecated, warning once each: options-form `instructions`/`tools`/`outputSchema` (`NYLORUN_DEP_AGENT_OPTIONS`), `.use(capability)` (`NYLORUN_DEP_USE`), `.before()`/`.after()` (`NYLORUN_DEP_HOOKS`), and `capability({ tools, instructions, … })` (`NYLORUN_DEP_CAPABILITY_OPTIONS`). See MIGRATION.md.
14
+
15
+ - 5278b4e: **Flow agents on workflow manifest v2.** A flow agent compiles to `workflowSchemaVersion: 2` and runs on the new `flow-2` engine; v1 workflows (`Chain`, `Switch`, `Parallel`, `Map`, `Loop`) keep running on `flow-1`, so in-flight runs finish as they began.
16
+
17
+ - **Manifest (core).** Any node may carry `id` and `input`; there are no slots. `chain` and `parallel` are bare collections, a Map is `{ map: { each } }` over its input, and a Loop has `max?` and `decide?`. The header adds `name`, `description`, `metadata`, `inputSchema` and `outputSchema`, and `agents` embeds every agent the flow runs, so one manifest hash covers the whole flow. `Agent.from(json, { nodes, agents })` rebuilds a flow agent. Path and key helpers (`leafPath`, `stageKey`, `forEachFlowNode`, `embeddedAgent`, …) are exported from `@nylorun/core/define`.
18
+ - **Paths (harness).** Linked agent sessions are named by the agent: its id, `[i]` per Map item, and a nested flow agent's id in front. Control stages add nothing, so wrapping a step in a Loop keeps its session. Functions are bound under stage keys (`route:on`, `@1.default.1:input`) and all receive `{ input, results, flowInput }`; `flowInput` works inside nested `flow()`. A Loop without `decide` retries with the verifier's feedback up to `max` and then fails with `loop.exhausted`. Nested flow agents run inline.
19
+ - **Runtime.** Leaves of a v2 workflow resolve from its embedded `agents` (with the workflow's plugin roots, keyed `<agent>/<capability>`) instead of the registry. `loop.iteration` is emitted only for a Loop's own agent turns.
20
+ - **Agents SDK.** `saveAgent(flowAgent)` PUTs one document; application mode registers the flow's executor with its manifest hash. An executor leaves `fn`, `verify` and tool actions of a v2 flow unclaimed when they belong to another manifest hash, because stage keys can shift between deploys; it reports each skipped action once through `onError`.
21
+ - **Studio.** Draws v2 manifests: agents and tools at their session paths, control stages at their stage keys, nested flow agents inline, and lights a Map's agent from its item sessions.
22
+ - Build diagnostics: `flow.duplicate-leaf`, `flow.duplicate-id`, `flow.agent-conflict`, `flow.v1-workflow`, `loop.invalid-verify`, `workflow.flow-agent-child`. Also fixes v1 flows passing an agent step's output to the next step still wrapped in the runtime's turn marker.
23
+
24
+ - 426fd27: **Flow agents as subagents.** `.subagents(flowAgent)` lets a ReAct agent delegate to a flow agent. The flow's workflow manifest v2 is inlined in the delegating tool (`ToolManifest.agent` may be a workflow manifest), so it is saved with the parent and served by the parent's executor, plugin roots included (`<flow>/<agent>/<capability>`).
25
+
26
+ - **Engine (harness).** A call to a flow subagent is one durable `agent` effect (`role: "delegate"`) between the delegation's start and settle points; the flow's output is the tool result, and a failed flow is a failed tool result. Local `run()` refuses flow subagents, which need the Runtime.
27
+ - **Runtime.** The effect starts a linked flow session from the parent's pinned manifest, fresh per call, and settles when the flow's turn ends. Cancelling any session now cascades to the linked sessions it started, not only a workflow's.
28
+ - **Core.** `delegation.flow-unsupported` now only reports a workflow built with `Chain`, `Switch`, `Parallel`, `Map` or `Loop`. New helpers: `flowDelegatesOf`, `flowDelegateManifest`, `isFlowDelegate`; `delegatesOf` lists only the ReAct agents run in-process.
29
+
30
+ ### Patch Changes
31
+
32
+ - Pin core to the tested release.
33
+ - Updated dependencies [e537a82]
34
+ - Updated dependencies [5278b4e]
35
+ - Updated dependencies [426fd27]
36
+ - Updated dependencies [8cda500]
37
+ - @nylorun/core@0.8.0-beta
38
+
39
+ ## 0.8.0-beta
40
+
41
+ ### Minor Changes
42
+
43
+ - a322696: **`@nylorun/agents/ag-ui`: serve agents to AG-UI clients from your own server.** `createAgUiHandler({ basePath, agents, subject })` returns a web-standard `fetch` handler (plus `run`, `history`, `reattach` and `cancel`), and `toNodeListener` adapts it to `node:http` and Express.
44
+
45
+ - One session per person, agent and thread; the AG-UI message id is the idempotency key, so a retried run replays the same turn.
46
+ - Approvals become AG-UI interrupts and resume through `runAgent({ resume })`.
47
+ - History returns a plain AG-UI `Message[]` with the ids the live run used; reattach continues a run from `Last-Event-ID`.
48
+ - Needs a Runtime with `transcript-events`. Adds the `@ag-ui/core` dependency (`~1.0.0`), loaded only by this subpath.
49
+
50
+ `AgentsClient.hostFeatures()` returns the Runtime's protocol features, including optional ones.
51
+
52
+ - 844bff3: **Act for a person: `Nylorun-Subject` and `Nylorun-Scopes`.** An app server that holds the Tenant key can name the person each request is for, and the Runtime enforces it (optional Host feature `subject-headers`).
53
+
54
+ - `client.as(subject, { scopes })` in `@nylorun/agents` sends both headers on every call, event streams included. Scopes: `agents:read`, `agents:write`, `sessions:own`, `vaults:own`, `tenant:settings`; default `["sessions:own"]`.
55
+ - The Runtime limits a subject to the routes its scopes allow (`403 scope_required`) and to its own sessions and vaults: another owner's session, vault or sandbox is the same `404` as a missing one, including `PUT` on its session id (was `409`). Reset, config seed, executors, actions and the sandbox tool routes are open to no subject. Only application keys may send the headers.
56
+ - The AG-UI handler calls the Runtime as each person and requires `subject-headers`; new optional `scopes` option. The host's `session()` parameters can no longer replace a session's id, agent or owner.
57
+ - Core exports `SUBJECT_HEADER`, `SCOPES_HEADER`, `SUBJECT_SCOPES` and `parseSubjectHeaders`. Postgres Tenant schemas migrate to version 2 (an indexed session owner column).
58
+
59
+ Requests without `Nylorun-Subject` are unchanged.
60
+
61
+ ### Patch Changes
62
+
63
+ - 42272f8: The AG-UI handler answers `400` (`invalid_request`) when the Runtime refuses what the client sent, such as a `Last-Event-ID` it cannot read, instead of `502`.
64
+ - Pin core to the tested release.
65
+ - Updated dependencies [a322696]
66
+ - Updated dependencies [844bff3]
67
+ - Updated dependencies [a322696]
68
+ - @nylorun/core@0.7.0-beta
69
+
3
70
  ## 0.7.0-beta
4
71
 
5
72
  ### Minor Changes
package/README.md CHANGED
@@ -41,11 +41,9 @@ Sources never mix. Partial environment fails with `connection_missing`.
41
41
  import { Agent, createClient, connectAgents, tool } from "@nylorun/agents";
42
42
  import { z } from "zod";
43
43
 
44
- const assistant = Agent({
45
- id: "assistant",
46
- name: "Assistant",
47
- instructions: "Use the available tools.",
48
- tools: [
44
+ const assistant = Agent({ id: "assistant", name: "Assistant" })
45
+ .instructions("Use the available tools.")
46
+ .tools(
49
47
  tool({
50
48
  name: "lookup",
51
49
  input: z.object({ id: z.string() }),
@@ -53,8 +51,7 @@ const assistant = Agent({
53
51
  return { id, owner: ctx.info };
54
52
  },
55
53
  }),
56
- ],
57
- });
54
+ );
58
55
 
59
56
  // Explicit Tenant API client (options or env / link via createClient()).
60
57
  const client = createClient({
@@ -97,68 +94,57 @@ throws `IncompatibleRuntimeError` / `incompatible_host` when the Host range or
97
94
  required features do not match. Re-exports include `PROTOCOL_FEATURES`,
98
95
  `ERROR_CODES` and `compareVersions`.
99
96
 
100
- Load Agent Skills from a local catalog folder with `skills(path)`:
97
+ Load Agent Skills from a local catalog folder with `.skills(path)`:
101
98
 
102
99
  ```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"));
100
+ import { Agent } from "@nylorun/agents";
101
+
102
+ const assistant = Agent({ id: "assistant", name: "Order assistant" })
103
+ .instructions("Use lookup_order for orders.")
104
+ .tools(lookupOrder)
105
+ .skills("./assistant-skills");
111
106
  ```
112
107
 
113
108
  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
109
 
115
- Declare MCP servers with `mcp(...)` (same map shape as agent-plugins `mcpServers` / manifest v3):
110
+ Declare MCP servers with `.mcp(...)` (same map shape as agent-plugins `mcpServers`):
116
111
 
117
112
  ```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
- );
113
+ import { Agent } from "@nylorun/agents";
114
+
115
+ const assistant = Agent({ id: "assistant", name: "Assistant" })
116
+ .instructions("Use the available tools.")
117
+ .mcp({
118
+ github: { type: "streamable-http", url: "https://mcp.example.com/github" },
119
+ });
133
120
  ```
134
121
 
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"`.
122
+ 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
123
 
137
- Give an agent an isolated computer with `sandbox()`:
124
+ Give an agent an isolated computer with `.sandbox()`:
138
125
 
139
126
  ```ts
140
- import { Agent, sandbox } from "@nylorun/agents";
127
+ import { Agent } from "@nylorun/agents";
141
128
 
142
- const analyst = Agent({
143
- id: "analyst",
144
- instructions: "Analyse the data the user gives you. Use Python.",
145
- }).use(sandbox());
129
+ const analyst = Agent({ id: "analyst" })
130
+ .instructions("Analyse the data the user gives you. Use Python.")
131
+ .sandbox();
146
132
  ```
147
133
 
148
134
  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
135
 
150
136
  ```ts
151
- .use(sandbox({
137
+ .sandbox({
152
138
  image: "python:3.13", // any OCI image; default python:3.13-slim
153
139
  network: { preset: "dev", allow: ["api.example.com"] }, // "none" | "dev" (default) | "open"
154
140
  resources: { cpus: 2, memory: "2GiB" },
155
141
  idle: "15m", // stop compute when idle; files persist
156
- }))
142
+ })
157
143
  ```
158
144
 
159
145
  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.
160
146
 
161
- Put an agent in another agent's `tools` to let the model delegate to it:
147
+ Add an agent with `.subagents()` to let the model delegate to it:
162
148
 
163
149
  ```ts
164
150
  import { Agent } from "@nylorun/agents";
@@ -168,54 +154,47 @@ const researcher = Agent({
168
154
  id: "researcher",
169
155
  description:
170
156
  "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
- });
157
+ })
158
+ .instructions("Investigate one question about one order. Be exhaustive, then be brief.")
159
+ .tools(searchOrders, readTicket)
160
+ .output(z.object({ summary: z.string(), evidence: z.array(z.string()) }));
179
161
 
180
- const support = Agent({
181
- id: "support",
182
- instructions:
162
+ const support = Agent({ id: "support" })
163
+ .instructions(
183
164
  "For anything needing more than two lookups, delegate to researcher with a complete, self-contained task.",
184
- tools: [lookupOrder, refundOrder, researcher],
185
- });
165
+ )
166
+ .tools(lookupOrder, refundOrder)
167
+ .subagents(researcher);
186
168
  ```
187
169
 
188
170
  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).
189
171
 
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.
172
+ 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 executor. 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.
173
+
174
+ 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.
191
175
 
192
- ## Workflows
176
+ ## Flow agents
193
177
 
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)`.
178
+ An agent's body is either a ReAct loop (the model decides) or a flow (your code
179
+ decides). A flow agent is built from stages, each shaped `.stage(whatRuns, { how })`,
180
+ and is registered, saved and opened as a session like any agent: put it in
181
+ `export const agents`, `saveAgent(flowAgent)`, then `createSession({ agentId })`
182
+ and `session.input(value)`. `input` sends string values as `content` and other JSON
183
+ as `data`. On the wire a flow agent is a workflow manifest v2 (`kind: "workflow"`,
184
+ `workflowSchemaVersion: 2`) that embeds the agents it runs, so one document and one
185
+ manifest hash cover the whole flow; its agents are not listed on their own.
203
186
 
204
187
  ```ts
205
- import { Agent, Chain, Loop, Map, tool } from "@nylorun/agents";
188
+ import { Agent, tool } from "@nylorun/agents";
206
189
  import { z } from "zod";
207
190
 
208
- const planner = Agent({
209
- id: "planner",
210
- instructions: "Return { tasks: string[] }.",
211
- outputSchema: z.object({ tasks: z.array(z.string()) }),
212
- }).build();
191
+ const planner = Agent({ id: "planner" })
192
+ .instructions("Return { tasks: string[] }.")
193
+ .output(z.object({ tasks: z.array(z.string()) }));
213
194
 
214
- const coder = Agent({
215
- id: "coder",
216
- instructions: "Implement one task. Return { summary }.",
217
- outputSchema: z.object({ summary: z.string() }),
218
- }).build();
195
+ const coder = Agent({ id: "coder" })
196
+ .instructions("Implement one task. Return { summary }.")
197
+ .output(z.object({ summary: z.string() }));
219
198
 
220
199
  const openPr = tool({
221
200
  name: "open-pr",
@@ -226,43 +205,51 @@ const openPr = tool({
226
205
  },
227
206
  });
228
207
 
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
- }),
208
+ export const shipFeature = Agent({ id: "ship-feature" })
209
+ .step(planner)
210
+ .map(
211
+ Agent({ id: "code" }).loop(coder, {
212
+ verify: ({ output }) => (output.summary ? { pass: true } : { pass: false, feedback: "Say what you changed." }),
213
+ max: 2,
242
214
  }),
243
- {
244
- run: openPr,
245
- input: ({ value }) => ({
246
- summaries: value.map((item: { summary: string }) => item.summary),
247
- }),
248
- },
249
- ],
250
- });
215
+ { id: "implement", input: ({ input }) => input.tasks },
216
+ )
217
+ .step(openPr, { input: ({ input }) => ({ summaries: input.map((item) => item.summary) }) });
251
218
 
252
219
  export const agents = [shipFeature];
253
220
  ```
254
221
 
255
- | Primitive | Role |
222
+ | Stage | Role |
256
223
  | --- | --- |
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
224
+ | **`.step(x, { id?, input? })`** | Run one agent, tool or `flow()`; its output is the next stage's input |
225
+ | **`.switch({ ...cases, default? }, { on })`** | `on({ input })` returns a case name; exactly that case runs |
226
+ | **`.parallel(branches)`** | Fixed named branches at once, same input; output is an object |
227
+ | **`.map(each)`** | Run `each` once per item of the input, which must be an array; output is an array |
228
+ | **`.loop(body, { verify, max?, decide? })`** | Run, verify, run again with the feedback until it passes; needs `max` or `decide` |
229
+
230
+ Every function receives one object: `{ input, results, flowInput }`. `input` is
231
+ what the stage received, `results` holds earlier steps' outputs by step id, and
232
+ `flowInput` is the agent's own input. The `input` option computes a stage's input;
233
+ the `id` option names a step. `flow()` builds a sequence with no id for a case,
234
+ branch, map item or loop body that is more than one step. Types flow from each
235
+ step's `.output()` schema to the next stage's `input`.
236
+
237
+ Agents inside a flow keep their own sessions, linked from the flow's session and
238
+ named by the agent: a step's id (the agent's id, or `{ id }`), with `[i]` for each
239
+ Map item and a nested flow agent's id in front of its own agents'. Control stages
240
+ add nothing, so wrapping a step in `.loop()` or moving it between cases keeps its
241
+ session. An agent may appear once per flow; use it again under a new id with
242
+ `.step(writer, { id: "final-writer" })` or `writer.withId("…")`
243
+ (`flow.duplicate-leaf`). Functions are bound under stage keys: a stage's `id`, or
244
+ its position such as `@1.default.1`, plus `:input`, `:on`, `:verify` or `:decide`.
245
+ Name the stages you may reorder.
246
+
247
+ `Chain`, `Switch`, `Parallel`, `Map` and `Loop` still build workflow manifest v1
248
+ directly and run as before; a flow agent can't be a child of them.
249
+
250
+ The agents in a flow share one sandbox: declare it with `.sandbox(spec)` on the
251
+ flow agent and give the agents that use it `.sandbox()` with no options (an agent
252
+ that declares its own spec must match), attach with `createSession({ …, sandbox: { session } })`, call
266
253
  built-ins via `session.sandbox` (application) or `ctx.sandbox` (executor).
267
254
  Observe with `session.observe({ follow: true })` to merge linked agent streams;
268
255
  `pending()` lists waits across the tree. Studio renders the manifest tree and
@@ -286,4 +273,107 @@ After registration, `connectAgents` opens authenticated fetch SSE before discove
286
273
 
287
274
  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`.
288
275
 
289
- The SDK depends only on core within the Nylorun packages; installing it does not install harness. Use `/define`, `/client`, or `/executor` for focused imports, or the root for convenience. Studio uses `/client`. See [the adopted host contract](../harness/HOST_CONTRACT.md).
276
+ ## AG-UI
277
+
278
+ `@nylorun/agents/ag-ui` serves the Tenant's agents to any
279
+ [AG-UI](https://docs.ag-ui.com) client (`@ag-ui/client`'s `HttpAgent`,
280
+ CopilotKit, a desktop renderer) from your own server. Your server signs people
281
+ in and names the person each request is for; the handler maps each AG-UI thread
282
+ to one session per person, agent and thread, and streams it as AG-UI events.
283
+
284
+ ```ts
285
+ import { createServer } from "node:http";
286
+ import { createAgUiHandler, toNodeListener } from "@nylorun/agents/ag-ui";
287
+ import support from "./agents/support.js";
288
+
289
+ const agui = createAgUiHandler({
290
+ basePath: "/api/agui",
291
+ agents: [support], // nothing else is reachable
292
+ subject: async (request) => (await getSignedInUser(request))?.id, // undefined → 401
293
+ });
294
+
295
+ createServer(toNodeListener(agui)).listen(3000);
296
+ // Next.js, Hono, Bun, Deno, Workers: export or mount `agui.fetch` directly.
297
+ ```
298
+
299
+ | Method and path under `basePath` | Operation |
300
+ | --- | --- |
301
+ | `POST /{agentId}` | Run: an AG-UI `RunAgentInput` in, server-sent events out |
302
+ | `GET /{agentId}/threads/{threadId}/messages` | History as a plain AG-UI `Message[]`, usable as `HttpAgent`'s `initialMessages` |
303
+ | `GET /{agentId}/threads/{threadId}/events` | Reattach: the rest of a run after a dropped connection, from `Last-Event-ID` (or `?cursor=`); `204` when nothing is left |
304
+ | `POST /{agentId}/threads/{threadId}/cancel` | Cancel the running turn |
305
+
306
+ `run`, `history`, `reattach` and `cancel` are also on the handler for your own
307
+ routing. The default `client` is `createClient()`; pass `client` to use another
308
+ one, and `session(subject, agentId)` to add the person's `vaultIds` or
309
+ `credentialSelections` (keep `info` stable: it is part of the session's
310
+ identity).
311
+
312
+ - Each AG-UI message id is the command's idempotency key: a retried run replays
313
+ the same turn instead of starting a second one.
314
+ - An approval ends the run with an AG-UI interrupt (`reason: "tool_approval"`,
315
+ `toolCallId` the model's call id). Resume with
316
+ `runAgent({ resume: [{ interruptId, status: "resolved", payload: { approved: true } }] })`.
317
+ Other interactions answer with `payload` as the response.
318
+ - Every event that ends a group carries the Runtime cursor as its SSE `id`.
319
+ `HttpAgent` does not reconnect by itself; call the reattach route with the
320
+ last id you received.
321
+ - Closing the connection stops reading; it never cancels the turn.
322
+ - A busy or paused thread ends the run with `RUN_ERROR` code `session_busy`.
323
+ - The handler calls the Runtime as each person (`client.as(subject, { scopes })`,
324
+ below), so the Runtime itself keeps one person out of another's threads.
325
+ `scopes` defaults to `["sessions:own"]`; add `"vaults:own"` when `session()`
326
+ attaches the person's vaults. A `subject` the Runtime cannot name (see below)
327
+ answers `500` (`subject_invalid`).
328
+ - The handler needs a Runtime with the optional features `transcript-events`
329
+ and `subject-headers` and answers `502` (`runtime_feature_missing`) without
330
+ them.
331
+
332
+ A complete web backend with a test is in
333
+ [examples/src/ag-ui](../examples/README.md#an-agent-in-your-web-app-ag-ui).
334
+
335
+ Limitations: assistant text arrives once per model step (no token streaming);
336
+ no reasoning, state, activity or subagent events, and an agent used as a tool
337
+ shows only its result; frontend tools in `RunAgentInput.tools` are rejected
338
+ (`400`); one text part per user message; earlier messages cannot be edited or
339
+ regenerated. Browsers always go through your server: never let one call the
340
+ Runtime directly.
341
+
342
+ ## Acting for a person (app servers)
343
+
344
+ A server that signs people in and calls the Runtime for them (an "app server")
345
+ keeps the Tenant key to itself and names the person on each call:
346
+
347
+ ```ts
348
+ import { createClient } from "@nylorun/agents";
349
+
350
+ const app = createClient(); // the Tenant key, on the server only
351
+
352
+ // Per request, after your own sign-in:
353
+ const person = app.as(`app:${user.id}`, { scopes: ["sessions:own", "vaults:own"] });
354
+ await person.createSession({ agentId: "support", ownerUserId: `app:${user.id}` });
355
+ await person.listSessions(); // only this person's sessions
356
+ ```
357
+
358
+ `as()` sends `Nylorun-Subject` and `Nylorun-Scopes` on every call, event
359
+ streams included, and the Runtime (optional feature `subject-headers`)
360
+ enforces both: another person's sessions and vaults answer the same `404` as
361
+ missing ones, and a route outside the scopes answers `403` (`scope_required`).
362
+ Nothing is minted, cached or refreshed, and the copy shares the client's
363
+ compatibility check, so calling `as()` per request is cheap.
364
+
365
+ | Scope | Allows |
366
+ | --- | --- |
367
+ | `sessions:own` | The person's own sessions: create, list, read, stream, message, approve, respond, cancel |
368
+ | `vaults:own` | The person's own vaults and credentials |
369
+ | `agents:read` | Listing the Tenant's agents |
370
+ | `agents:write` | Saving agents; listing agents, models and providers |
371
+ | `tenant:settings` | The Tenant's status, model provider and sandbox settings |
372
+
373
+ No scope reaches Tenant reset, config seed, executors, actions or the sandbox
374
+ tool routes; call those without `as()`. A subject is 1–200 visible ASCII
375
+ characters (spaces only inside) and `host` is reserved. Your server must drop
376
+ any `Nylorun-*` header its own clients send, and only an application key can act
377
+ for a subject: an executor key that tries is `403`.
378
+
379
+ 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).
@@ -0,0 +1,42 @@
1
+ import type { BuiltWorkflow } from "@nylorun/core/define";
2
+ import { type SubjectScope } from "@nylorun/core/contracts";
3
+ import { type AgentSource, type AgentsClient } from "../client.js";
4
+ /** Per-session parameters the host may add, such as the person's vault for MCP servers. */
5
+ export interface AgUiSessionOptions {
6
+ /** Part of the session's identity: keep it stable for a subject and agent, or runs get 409. */
7
+ info?: Record<string, unknown>;
8
+ vaultIds?: readonly string[];
9
+ credentialSelections?: Parameters<AgentsClient["createSession"]>[0]["credentialSelections"];
10
+ }
11
+ export interface AgUiHandlerOptions {
12
+ /** Agents this endpoint may run: built agents, workflows or their ids. Nothing else is reachable. */
13
+ agents: readonly (AgentSource | BuiltWorkflow | string)[];
14
+ /** The signed-in person the request is for. `undefined` answers 401; there is no anonymous mode. */
15
+ subject(request: Request): string | undefined | Promise<string | undefined>;
16
+ /** Where `fetch` is mounted, e.g. `/api/agui`. Default `/`. */
17
+ basePath?: string;
18
+ /** Application client. Default: `createClient()`, from the environment or the Project link. */
19
+ client?: AgentsClient | Promise<AgentsClient>;
20
+ /**
21
+ * What each subject may do through this endpoint. Default `["sessions:own"]`; add
22
+ * `vaults:own` if `session()` attaches the person's vaults.
23
+ */
24
+ scopes?: readonly SubjectScope[];
25
+ /** Optional per-session parameters, e.g. the person's vault for connected accounts. */
26
+ session?(subject: string, agentId: string): AgUiSessionOptions | Promise<AgUiSessionOptions>;
27
+ }
28
+ export interface AgUiHandler {
29
+ /** One entry point that routes by method and path under `basePath`. Bound. */
30
+ readonly fetch: (request: Request) => Promise<Response>;
31
+ /** `POST`: an AG-UI `RunAgentInput` in, the run's events out as server-sent events. */
32
+ run(request: Request, agentId: string): Promise<Response>;
33
+ /** `GET`: the thread's messages as a plain AG-UI `Message[]`. */
34
+ history(request: Request, agentId: string, threadId: string): Promise<Response>;
35
+ /** `GET`: the rest of a run after a dropped connection, from `Last-Event-ID` or `?cursor=`. */
36
+ reattach(request: Request, agentId: string, threadId: string): Promise<Response>;
37
+ /** `POST`: cancels the thread's running turn. */
38
+ cancel(request: Request, agentId: string, threadId: string): Promise<Response>;
39
+ }
40
+ /** A session per subject, agent and thread; one subject can never name another's session. */
41
+ export declare function sessionIdFor(subject: string, agentId: string, threadId: string): string;
42
+ export declare function createAgUiHandler(options: AgUiHandlerOptions): AgUiHandler;