@nylorun/agents 0.8.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,41 @@
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
+
3
39
  ## 0.8.0-beta
4
40
 
5
41
  ### 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.
191
173
 
192
- ## Workflows
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.
193
175
 
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)`.
176
+ ## Flow agents
177
+
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
@@ -0,0 +1,20 @@
1
+ import { Agent as CoreAgent, AgentBuilder as CoreAgentBuilder } from "@nylorun/core/define";
2
+ import type { AgentOptions, ToolSchemaSource } from "@nylorun/core/define";
3
+ import { type SkillsOptions } from "./skills/index.js";
4
+ /**
5
+ * The agent builder with the capabilities that read files: `.skills()` and `.plugin()`.
6
+ * `@nylorun/agents/define` exports the portable builder from core, without them.
7
+ */
8
+ export declare class AgentBuilder<Info = unknown, Schema extends ToolSchemaSource | undefined = undefined, Id extends string = string> extends CoreAgentBuilder<Info, Schema, Id> {
9
+ /** Load an Agent Skills folder. */
10
+ skills(directory: string, options?: SkillsOptions): this;
11
+ /** Attach an Agent Plugin package. */
12
+ plugin(directory: string): this;
13
+ output<S extends ToolSchemaSource>(schema: S): AgentBuilder<Info, S, Id>;
14
+ }
15
+ export declare function Agent<Info = unknown, Schema extends ToolSchemaSource | undefined = undefined, const Id extends string = string>(options: AgentOptions<Schema> & {
16
+ readonly id: Id;
17
+ }): AgentBuilder<Info, Schema, Id>;
18
+ export declare namespace Agent {
19
+ const from: typeof CoreAgent.from;
20
+ }
@@ -0,0 +1,26 @@
1
+ import { Agent as CoreAgent, AgentBuilder as CoreAgentBuilder, } from "@nylorun/core/define";
2
+ import { skills } from "./skills/index.js";
3
+ import { plugin } from "./plugins/plugin.js";
4
+ /**
5
+ * The agent builder with the capabilities that read files: `.skills()` and `.plugin()`.
6
+ * `@nylorun/agents/define` exports the portable builder from core, without them.
7
+ */
8
+ export class AgentBuilder extends CoreAgentBuilder {
9
+ /** Load an Agent Skills folder. */
10
+ skills(directory, options) {
11
+ return this.capability(skills(directory, options));
12
+ }
13
+ /** Attach an Agent Plugin package. */
14
+ plugin(directory) {
15
+ return this.capability(plugin(directory));
16
+ }
17
+ output(schema) {
18
+ return super.output(schema);
19
+ }
20
+ }
21
+ export function Agent(options) {
22
+ return new AgentBuilder(options);
23
+ }
24
+ (function (Agent) {
25
+ Agent.from = CoreAgent.from;
26
+ })(Agent || (Agent = {}));
package/dist/client.js CHANGED
@@ -38,9 +38,15 @@ function declarationsOf(agent) {
38
38
  return [];
39
39
  const found = (binding.declarations ?? []).map((item) => ({ key: item.id, item }));
40
40
  for (const tool of binding.tools ?? []) {
41
- const child = delegateOf(tool)?.agent;
41
+ const delegate = delegateOf(tool);
42
+ const child = delegate?.agent;
42
43
  for (const item of child?.getBinding().declarations ?? [])
43
44
  found.push({ key: `${child.id}/${item.id}`, item });
45
+ // A flow agent used as a tool: its agents' declarations, keyed `<flow>/<agent>/<capability>`.
46
+ const flow = delegate?.workflow;
47
+ for (const leaf of Object.values(flow?.getBinding().agents ?? {}))
48
+ for (const item of leaf.declarations ?? [])
49
+ found.push({ key: `${flow.id}/${leaf.manifest.id}/${item.id}`, item });
44
50
  }
45
51
  return found;
46
52
  }
@@ -100,6 +106,22 @@ export class AgentsClient {
100
106
  return this.transport.json(`/v1/sessions${query}`, "GET", undefined, options.signal);
101
107
  }
102
108
  async saveAgent(agent, options) {
109
+ if (isBuiltWorkflow(agent) && agent.manifest.workflowSchemaVersion === 2) {
110
+ // A v2 workflow embeds its agents: one document, carrying their plugin roots.
111
+ const pluginRoots = {};
112
+ for (const binding of Object.values(agent.getBinding().agents)) {
113
+ const leaf = { id: binding.manifest.id, manifest: binding.manifest, getBinding: () => binding };
114
+ assertNoMiddlewareClosures(leaf);
115
+ for (const [key, root] of Object.entries(pluginRootsOf(leaf) ?? {}))
116
+ pluginRoots[`${leaf.id}/${key}`] = root;
117
+ }
118
+ return this.transport.json(`/v1/agents/${segment(agent.id)}`, "PUT", {
119
+ requestId: options.requestId ?? id(),
120
+ manifest: agent.manifest,
121
+ implementationVersion: options.implementationVersion,
122
+ ...(Object.keys(pluginRoots).length === 0 ? {} : { pluginRoots }),
123
+ });
124
+ }
103
125
  if (isBuiltWorkflow(agent)) {
104
126
  for (const binding of Object.values(agent.getBinding().agents)) {
105
127
  await this.saveAgent({
package/dist/executor.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { AgentManifestSchema, ActionSchema, ActionClaimResponseSchema, } from "@nylorun/core/contracts";
2
- import { agentFrom, isBuiltWorkflow, } from "@nylorun/core/define";
2
+ import { agentFrom, delegateOf, hashManifest, isFlowDelegate, isBuiltWorkflow, } from "@nylorun/core/define";
3
3
  import { AgentsClient, assertNoMiddlewareClosures, } from "./client.js";
4
4
  import { resolveConnection } from "./connection.js";
5
5
  import { deriveExecutorToken } from "./derived-credentials.js";
@@ -12,39 +12,87 @@ function implementationVersionOf(options) {
12
12
  env("NYLORUN_IMPLEMENTATION_VERSION") ??
13
13
  "dev");
14
14
  }
15
+ /** Agents embedded in a v2 workflow: served here, but saved only as part of the workflow. */
16
+ const embeddedAgents = new WeakMap();
15
17
  function buildAgents(options) {
16
18
  const agents = new Map();
17
- const addAgent = (built) => {
19
+ const embedded = new Set();
20
+ /**
21
+ * Agents embedded in another definition (a v2 flow's agents, a flow agent used as a
22
+ * tool) may be reached from more than one place; the same definition is served once.
23
+ */
24
+ const sameAsServed = (id, manifest) => {
25
+ const served = agents.get(id);
26
+ if (!served)
27
+ return false;
28
+ if (hashManifest(served.manifest) === hashManifest(manifest))
29
+ return true;
30
+ throw new Error(`Duplicate connected agent ${id}`);
31
+ };
32
+ const addAgent = (built, shared = false) => {
18
33
  assertNoMiddlewareClosures(built);
19
34
  AgentManifestSchema.parse(built.manifest);
20
- if (agents.has(built.id))
21
- throw new Error(`Duplicate connected agent ${built.id}`);
35
+ if (shared ? sameAsServed(built.id, built.manifest) : agents.has(built.id)) {
36
+ if (!shared)
37
+ throw new Error(`Duplicate connected agent ${built.id}`);
38
+ return;
39
+ }
22
40
  agents.set(built.id, built);
41
+ for (const tool of built.getBinding().tools) {
42
+ const delegate = delegateOf(tool);
43
+ if (!delegate || !isFlowDelegate(delegate))
44
+ continue;
45
+ // A flow agent used as a tool runs in its own linked session: serve its code too.
46
+ if (!delegate.workflow)
47
+ throw new Error(`Flow agent '${delegate.manifest.id}' is used as a tool by '${built.id}' but has no local implementation`);
48
+ addWorkflow(delegate.workflow, true);
49
+ embedded.add(delegate.workflow.id);
50
+ }
51
+ };
52
+ const addWorkflow = (workflow, shared = false) => {
53
+ if (shared ? sameAsServed(workflow.id, workflow.manifest) : agents.has(workflow.id)) {
54
+ if (!shared)
55
+ throw new Error(`Duplicate connected agent ${workflow.id}`);
56
+ return;
57
+ }
58
+ agents.set(workflow.id, workflow);
59
+ const v2 = workflow.manifest.workflowSchemaVersion === 2;
60
+ for (const binding of Object.values(workflow.getBinding().agents)) {
61
+ addAgent(agentFrom(binding.manifest, binding.implementations), v2);
62
+ if (v2)
63
+ embedded.add(binding.manifest.id);
64
+ }
23
65
  };
24
66
  for (const source of options.agents) {
25
67
  if (isBuiltWorkflow(source)) {
26
- if (agents.has(source.id))
27
- throw new Error(`Duplicate connected agent ${source.id}`);
28
- agents.set(source.id, source);
29
- for (const binding of Object.values(source.getBinding().agents))
30
- addAgent(agentFrom(binding.manifest, binding.implementations));
68
+ addWorkflow(source);
31
69
  continue;
32
70
  }
33
71
  const built = source.build?.() ?? source;
34
72
  if (isBuiltWorkflow(built)) {
35
- if (agents.has(built.id))
36
- throw new Error(`Duplicate connected agent ${built.id}`);
37
- agents.set(built.id, built);
38
- for (const binding of Object.values(built.getBinding().agents))
39
- addAgent(agentFrom(binding.manifest, binding.implementations));
73
+ addWorkflow(built);
40
74
  continue;
41
75
  }
42
76
  addAgent(built);
43
77
  }
44
78
  if (!agents.size)
45
79
  throw new Error("connectAgents requires at least one agent");
80
+ // A definition passed in directly is saved on its own, even if another one embeds it.
81
+ for (const source of options.agents)
82
+ embedded.delete(source.id);
83
+ embeddedAgents.set(agents, embedded);
46
84
  return agents;
47
85
  }
86
+ /**
87
+ * The manifest hash an executor serves for a workflow's flow actions. A run is pinned to
88
+ * one workflow manifest, and its stage keys may shift between deploys, so flow actions
89
+ * for another hash are left for the executor that serves it.
90
+ */
91
+ function flowManifestHash(agent) {
92
+ return isBuiltWorkflow(agent) && agent.manifest.workflowSchemaVersion === 2
93
+ ? hashManifest(agent.manifest)
94
+ : undefined;
95
+ }
48
96
  function connectExecutorMode(options, runtime, agents) {
49
97
  const transport = new Transport(runtime, "executor");
50
98
  const version = implementationVersionOf(options);
@@ -65,6 +113,7 @@ function connectExecutorMode(options, runtime, agents) {
65
113
  }
66
114
  catch { }
67
115
  };
116
+ const skipped = new Set();
68
117
  let discovering = false, dirty = false;
69
118
  const discover = async () => {
70
119
  dirty = true;
@@ -86,6 +135,14 @@ function connectExecutorMode(options, runtime, agents) {
86
135
  const agent = agents.get(action.agentId);
87
136
  if (!agent || action.status !== "pending" || active.has(action.actionId))
88
137
  continue;
138
+ const served = flowManifestHash(agent);
139
+ if (served !== undefined && "key" in action && action.manifestHash !== served) {
140
+ if (!skipped.has(action.actionId)) {
141
+ skipped.add(action.actionId);
142
+ report(new Error(`Skipped action ${action.actionId}: it belongs to manifest ${action.manifestHash} of '${action.agentId}', and this executor serves ${served}`));
143
+ }
144
+ continue;
145
+ }
89
146
  const work = new AbortController();
90
147
  active.set(action.actionId, work);
91
148
  let delivered = false;
@@ -264,14 +321,21 @@ function connectApplicationMode(options, application, agents) {
264
321
  });
265
322
  void ready.catch(() => { });
266
323
  const boot = (async () => {
324
+ const embedded = embeddedAgents.get(agents) ?? new Set();
267
325
  for (const agent of agents.values()) {
326
+ if (embedded.has(agent.id))
327
+ continue;
268
328
  await application.saveAgent(agent, { implementationVersion: version });
269
329
  }
270
- const registrations = [...agents.values()].map((agent) => ({
271
- agentId: agent.id,
272
- token: deriveExecutorToken(application.transport.key, application.transport.tenant, agent.id),
273
- implementationVersion: version,
274
- }));
330
+ const registrations = [...agents.values()].map((agent) => {
331
+ const manifestHash = flowManifestHash(agent);
332
+ return {
333
+ agentId: agent.id,
334
+ token: deriveExecutorToken(application.transport.key, application.transport.tenant, agent.id),
335
+ implementationVersion: version,
336
+ ...(manifestHash === undefined ? {} : { manifestHash }),
337
+ };
338
+ });
275
339
  await application.transport.json("/v1/executors", "PUT", {
276
340
  executors: registrations,
277
341
  });
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export { hashManifest, PROTOCOL_VERSION, PROTOCOL_FEATURES, HOST_PROTOCOL, DEFINITION_SCHEMA_VERSION, TENANT_HEADER, PROTOCOL_HEADER, SUBJECT_HEADER, SCOPES_HEADER, TENANT_ID_PATTERN, isTenantId, newTenantId, newPrincipalId, checkCompatibility, compareVersions, ERROR_CODES, } from "@nylorun/core/compatibility";
2
2
  export type { ProtocolFeature, ProtocolRange, Compatibility, ErrorCode, } from "@nylorun/core/compatibility";
3
- export { Agent, AgentBuilder, AgentBuildError, AgentLifecycleError, Loop, Chain, Switch, Parallel, Map, VerdictSchema, isVerdict, withInstructions, withoutTools, WorkflowBuildError, isBuiltWorkflow, capability, tool, defineSchema, ToolError, } from "@nylorun/core/define";
3
+ export { Agent, AgentBuilder } from "./builder.js";
4
+ export { AgentBuildError, AgentLifecycleError, Loop, Chain, Switch, Parallel, Map, VerdictSchema, isVerdict, withInstructions, withoutTools, WorkflowBuildError, isBuiltWorkflow, capability, CapabilityBuilder, flow, tool, defineSchema, ToolError, } from "@nylorun/core/define";
5
+ export type { FlowAgentBuilder, Flow, Named, StageArgs, NestedStageArgs, CapabilityIdentity, } from "@nylorun/core/define";
4
6
  export type { AgentOptions, AgentManifest, BuiltAgent, BuiltWorkflow, LoopOptions, LoopDecideArgs, LoopVerifyArgs, LoopDecision, ChainOptions, SwitchOptions, ParallelOptions, MapOptions, Verdict, WorkflowBinding, WorkflowManifest, ToolDefinition, ToolExecutionContext, ToolOutcome, Patch, Decision, TurnDecision, HookScope, BeforeHook, AfterHook, JsonValue, JsonObject, } from "@nylorun/core/define";
5
7
  export type { LiveEvent, SessionCommand, Action, ActionOutcome, CredentialInfo, CredentialSelection, VaultInfo, SubjectScope, } from "@nylorun/core/contracts";
6
8
  export { SUBJECT_SCOPES } from "@nylorun/core/contracts";
@@ -23,7 +25,7 @@ export type { StdioLaunch } from "./plugins/launch.js";
23
25
  export { skills, loadSkillsFromDirectory, resolveSkillsRoot, parseSkill, SkillsError, formatSkillCatalog, SKILLS_USAGE, } from "./skills/index.js";
24
26
  export type { SkillsCapability, SkillsOptions, SkillDiagnostic, } from "./skills/index.js";
25
27
  export { mcp, McpError } from "./mcp/index.js";
26
- export type { McpCapability, McpOptions } from "./mcp/index.js";
28
+ export type { McpCapability, McpOptions, McpServerSpec } from "./mcp/index.js";
27
29
  export { sandbox, SandboxError } from "./sandbox/index.js";
28
30
  export type { SandboxCapability, SandboxCapabilityOptions, SandboxOptions, ActionSandbox, ActionSandboxToolResult, CreateActionSandboxOptions, } from "./sandbox/index.js";
29
31
  export { createActionSandbox, definitionDeclaresSandbox, isActionSandboxTool, } from "./sandbox/index.js";
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Definition authoring and wire contracts are the only harness runtime imports.
2
2
  export { hashManifest, PROTOCOL_VERSION, PROTOCOL_FEATURES, HOST_PROTOCOL, DEFINITION_SCHEMA_VERSION, TENANT_HEADER, PROTOCOL_HEADER, SUBJECT_HEADER, SCOPES_HEADER, TENANT_ID_PATTERN, isTenantId, newTenantId, newPrincipalId, checkCompatibility, compareVersions, ERROR_CODES, } from "@nylorun/core/compatibility";
3
- export { Agent, AgentBuilder, AgentBuildError, AgentLifecycleError, Loop, Chain, Switch, Parallel, Map, VerdictSchema, isVerdict, withInstructions, withoutTools, WorkflowBuildError, isBuiltWorkflow, capability, tool, defineSchema, ToolError, } from "@nylorun/core/define";
3
+ export { Agent, AgentBuilder } from "./builder.js";
4
+ export { AgentBuildError, AgentLifecycleError, Loop, Chain, Switch, Parallel, Map, VerdictSchema, isVerdict, withInstructions, withoutTools, WorkflowBuildError, isBuiltWorkflow, capability, CapabilityBuilder, flow, tool, defineSchema, ToolError, } from "@nylorun/core/define";
4
5
  export { SUBJECT_SCOPES } from "@nylorun/core/contracts";
5
6
  export { AgentsClient, SessionClient, createClient } from "./client.js";
6
7
  export { connectAgents } from "./executor.js";
@@ -1,2 +1,2 @@
1
- export { mcp, McpError } from "./mcp.js";
2
- export type { McpCapability, McpOptions } from "./mcp.js";
1
+ export { mcp, McpError, normalizeMcpServers } from "@nylorun/core/define";
2
+ export type { McpCapability, McpOptions, McpServerSpec } from "@nylorun/core/define";
package/dist/mcp/index.js CHANGED
@@ -1 +1 @@
1
- export { mcp, McpError } from "./mcp.js";
1
+ export { mcp, McpError, normalizeMcpServers } from "@nylorun/core/define";
@@ -1,4 +1,4 @@
1
- export { sandbox, SandboxError } from "./sandbox.js";
2
- export type { SandboxCapability, SandboxCapabilityOptions, SandboxOptions, } from "./sandbox.js";
1
+ export { sandbox, SandboxError } from "@nylorun/core/define";
2
+ export type { SandboxCapability, SandboxCapabilityOptions, SandboxOptions, } from "@nylorun/core/define";
3
3
  export { createActionSandbox, definitionDeclaresSandbox, isActionSandboxTool, } from "./client.js";
4
4
  export type { ActionSandbox, ActionSandboxToolResult, CreateActionSandboxOptions, } from "./client.js";
@@ -1,2 +1,2 @@
1
- export { sandbox, SandboxError } from "./sandbox.js";
1
+ export { sandbox, SandboxError } from "@nylorun/core/define";
2
2
  export { createActionSandbox, definitionDeclaresSandbox, isActionSandboxTool, } from "./client.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nylorun/agents",
3
- "version": "0.8.0-beta",
3
+ "version": "0.9.0-beta",
4
4
  "description": "Nylorun agent definitions, session client and connected executor",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@ag-ui/core": "~1.0.0",
43
- "@nylorun/core": "0.7.0-beta",
43
+ "@nylorun/core": "0.8.0-beta",
44
44
  "gray-matter": "^4.0.3"
45
45
  },
46
46
  "peerDependencies": {
package/dist/mcp/mcp.d.ts DELETED
@@ -1,30 +0,0 @@
1
- import type { CapabilityDeclaration, McpServerManifest } from "@nylorun/core/define";
2
- export interface McpOptions {
3
- /** Capability id. Defaults to `"mcp"`. */
4
- readonly id?: string;
5
- }
6
- export interface McpCapability extends CapabilityDeclaration {
7
- readonly mcpServers: Readonly<Record<string, McpServerManifest>>;
8
- }
9
- export declare class McpError extends Error {
10
- readonly code: string;
11
- constructor(code: string, message: string);
12
- }
13
- /**
14
- * Declare MCP servers as one capability for `Agent.use`.
15
- *
16
- * The map shape matches agent-plugins `mcpServers` and manifest v3: each key
17
- * must equal that server's `name`. Supported transports are `stdio`,
18
- * `streamable-http`, and `sse` (see https://agent-plugins.org/plugin-authors/mcp-servers).
19
- *
20
- * ```ts
21
- * Agent({...}).use(mcp({
22
- * github: {
23
- * name: "github",
24
- * type: "streamable-http",
25
- * url: "https://mcp.example.com/github",
26
- * },
27
- * }))
28
- * ```
29
- */
30
- export declare function mcp(servers: Readonly<Record<string, McpServerManifest>>, options?: McpOptions): McpCapability;
package/dist/mcp/mcp.js DELETED
@@ -1,80 +0,0 @@
1
- export class McpError extends Error {
2
- code;
3
- constructor(code, message) {
4
- super(message);
5
- this.name = "McpError";
6
- this.code = code;
7
- }
8
- }
9
- /**
10
- * Declare MCP servers as one capability for `Agent.use`.
11
- *
12
- * The map shape matches agent-plugins `mcpServers` and manifest v3: each key
13
- * must equal that server's `name`. Supported transports are `stdio`,
14
- * `streamable-http`, and `sse` (see https://agent-plugins.org/plugin-authors/mcp-servers).
15
- *
16
- * ```ts
17
- * Agent({...}).use(mcp({
18
- * github: {
19
- * name: "github",
20
- * type: "streamable-http",
21
- * url: "https://mcp.example.com/github",
22
- * },
23
- * }))
24
- * ```
25
- */
26
- export function mcp(servers, options = {}) {
27
- const id = options.id ?? "mcp";
28
- if (!isRecord(servers)) {
29
- throw new McpError("mcp.invalid", "mcp() expects a non-empty map of MCP servers");
30
- }
31
- const entries = Object.entries(servers);
32
- if (entries.length === 0) {
33
- throw new McpError("mcp.empty", "mcp() requires at least one MCP server");
34
- }
35
- const mcpServers = {};
36
- for (const [key, server] of entries) {
37
- if (!isMcpServer(server)) {
38
- throw new McpError("mcp.invalid-server", `MCP server '${key}' is not a valid stdio, streamable-http, or sse declaration`);
39
- }
40
- if (server.name !== key) {
41
- throw new McpError("mcp.name-mismatch", `MCP server key '${key}' must equal the server name '${server.name}'`);
42
- }
43
- mcpServers[key] = freezeServer(server);
44
- }
45
- return { id, mcpServers: Object.freeze(mcpServers) };
46
- }
47
- function freezeServer(server) {
48
- if (server.type === "stdio") {
49
- return Object.freeze({
50
- name: server.name,
51
- type: "stdio",
52
- command: server.command,
53
- ...(server.args === undefined ? {} : { args: Object.freeze([...server.args]) }),
54
- ...(server.env === undefined
55
- ? {}
56
- : { env: Object.freeze({ ...server.env }) }),
57
- ...(server.cwd === undefined ? {} : { cwd: server.cwd }),
58
- });
59
- }
60
- return Object.freeze({
61
- name: server.name,
62
- type: server.type,
63
- url: server.url,
64
- ...(server.headers === undefined
65
- ? {}
66
- : { headers: Object.freeze({ ...server.headers }) }),
67
- });
68
- }
69
- function isMcpServer(value) {
70
- if (!isRecord(value) || typeof value.name !== "string" || !value.name)
71
- return false;
72
- if (value.type === "stdio")
73
- return typeof value.command === "string" && value.command.length > 0;
74
- if (value.type === "streamable-http" || value.type === "sse")
75
- return typeof value.url === "string" && value.url.length > 0;
76
- return false;
77
- }
78
- function isRecord(value) {
79
- return !!value && typeof value === "object" && !Array.isArray(value);
80
- }
@@ -1,27 +0,0 @@
1
- import type { CapabilityDeclaration, SandboxManifest, ToolDefinition } from "@nylorun/core/define";
2
- /** What computer the agent needs. The Runtime decides where it runs. */
3
- export type SandboxOptions = SandboxManifest;
4
- export interface SandboxCapabilityOptions {
5
- /** Capability id. Defaults to `"sandbox"`. */
6
- readonly id?: string;
7
- }
8
- export interface SandboxCapability extends CapabilityDeclaration {
9
- readonly sandbox: SandboxManifest;
10
- readonly tools: readonly ToolDefinition[];
11
- }
12
- export declare class SandboxError extends Error {
13
- readonly code: string;
14
- constructor(code: string, message: string);
15
- }
16
- /**
17
- * Give the agent an isolated Linux machine with a persistent `/workspace`.
18
- *
19
- * Adds the built-in `bash`, `read`, `write`, `edit`, `grep` and `glob` tools. They run in the
20
- * Nylorun Runtime, which picks the backend. Agent code declares requirements only.
21
- *
22
- * ```ts
23
- * Agent({ id: "analyst", instructions: "..." }).use(sandbox())
24
- * Agent({ ... }).use(sandbox({ image: "node:24", network: { allow: ["api.github.com"] } }))
25
- * ```
26
- */
27
- export declare function sandbox(options?: SandboxOptions, capability?: SandboxCapabilityOptions): SandboxCapability;
@@ -1,92 +0,0 @@
1
- import { SANDBOX_DEFERRED_FIELDS, SANDBOX_FIELDS, SANDBOX_INSTRUCTIONS, SANDBOX_NETWORK_PRESETS, createSandboxTools, isSandboxHostPattern, parseSandboxDuration, parseSandboxSize, } from "@nylorun/core/define";
2
- export class SandboxError extends Error {
3
- code;
4
- constructor(code, message) {
5
- super(message);
6
- this.name = "SandboxError";
7
- this.code = code;
8
- }
9
- }
10
- /**
11
- * Give the agent an isolated Linux machine with a persistent `/workspace`.
12
- *
13
- * Adds the built-in `bash`, `read`, `write`, `edit`, `grep` and `glob` tools. They run in the
14
- * Nylorun Runtime, which picks the backend. Agent code declares requirements only.
15
- *
16
- * ```ts
17
- * Agent({ id: "analyst", instructions: "..." }).use(sandbox())
18
- * Agent({ ... }).use(sandbox({ image: "node:24", network: { allow: ["api.github.com"] } }))
19
- * ```
20
- */
21
- export function sandbox(options = {}, capability = {}) {
22
- const id = capability.id ?? "sandbox";
23
- if (!isRecord(options))
24
- throw new SandboxError("sandbox.invalid", "sandbox() expects an options object");
25
- for (const key of Object.keys(options)) {
26
- if (SANDBOX_DEFERRED_FIELDS.includes(key))
27
- throw new SandboxError("sandbox.unsupported", `sandbox({ ${key} }) is not supported in this version. ` +
28
- `Supported options: ${SANDBOX_FIELDS.join(", ")}. See docs/design/sandboxes.md for what is planned.`);
29
- if (!SANDBOX_FIELDS.includes(key))
30
- throw new SandboxError("sandbox.unknown-option", `Unknown sandbox option '${key}'. Supported options: ${SANDBOX_FIELDS.join(", ")}`);
31
- }
32
- return Object.freeze({
33
- id,
34
- instructions: Object.freeze([SANDBOX_INSTRUCTIONS]),
35
- tools: createSandboxTools(),
36
- sandbox: freezeOptions(options),
37
- });
38
- }
39
- function freezeOptions(options) {
40
- const result = {};
41
- if (options.image !== undefined) {
42
- if (typeof options.image !== "string" || options.image.trim().length === 0)
43
- throw new SandboxError("sandbox.invalid-image", "sandbox({ image }) must be a non-empty OCI image reference");
44
- result.image = options.image;
45
- }
46
- if (options.network !== undefined) {
47
- if (!isRecord(options.network))
48
- throw new SandboxError("sandbox.invalid-network", "sandbox({ network }) must be an object");
49
- for (const key of Object.keys(options.network))
50
- if (key !== "preset" && key !== "allow")
51
- throw new SandboxError("sandbox.invalid-network", `Unknown network option '${key}'. Use preset and allow.`);
52
- const { preset, allow } = options.network;
53
- if (preset !== undefined && !SANDBOX_NETWORK_PRESETS.includes(preset))
54
- throw new SandboxError("sandbox.invalid-network", `network.preset must be one of ${SANDBOX_NETWORK_PRESETS.join(", ")}`);
55
- if (allow !== undefined) {
56
- if (!Array.isArray(allow))
57
- throw new SandboxError("sandbox.invalid-network", "network.allow must be an array of host names");
58
- for (const host of allow)
59
- if (typeof host !== "string" || !isSandboxHostPattern(host))
60
- throw new SandboxError("sandbox.invalid-network", `network.allow entry '${String(host)}' must be a host name such as api.github.com or *.example.com (no scheme, port or path)`);
61
- }
62
- result.network = Object.freeze({
63
- ...(preset === undefined ? {} : { preset: preset }),
64
- ...(allow === undefined ? {} : { allow: Object.freeze([...allow]) }),
65
- });
66
- }
67
- if (options.resources !== undefined) {
68
- if (!isRecord(options.resources))
69
- throw new SandboxError("sandbox.invalid-resources", "sandbox({ resources }) must be an object");
70
- for (const key of Object.keys(options.resources))
71
- if (key !== "cpus" && key !== "memory")
72
- throw new SandboxError("sandbox.invalid-resources", `resources.${key} is not supported in this version. Use cpus and memory.`);
73
- const { cpus, memory } = options.resources;
74
- if (cpus !== undefined && (!Number.isInteger(cpus) || cpus < 1 || cpus > 64))
75
- throw new SandboxError("sandbox.invalid-resources", "resources.cpus must be an integer from 1 to 64");
76
- if (memory !== undefined && (typeof memory !== "string" || parseSandboxSize(memory) === undefined))
77
- throw new SandboxError("sandbox.invalid-resources", "resources.memory must be a size such as 512MiB or 2GiB");
78
- result.resources = Object.freeze({
79
- ...(cpus === undefined ? {} : { cpus }),
80
- ...(memory === undefined ? {} : { memory }),
81
- });
82
- }
83
- if (options.idle !== undefined) {
84
- if (typeof options.idle !== "string" || parseSandboxDuration(options.idle) === undefined)
85
- throw new SandboxError("sandbox.invalid-idle", "sandbox({ idle }) must be a duration such as 30s, 15m or 1h");
86
- result.idle = options.idle;
87
- }
88
- return Object.freeze(result);
89
- }
90
- function isRecord(value) {
91
- return !!value && typeof value === "object" && !Array.isArray(value);
92
- }