@nylorun/agents 0.5.0-beta → 0.6.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,36 @@
1
1
  # @nylorun/agents
2
2
 
3
+ ## 0.6.0-beta
4
+
5
+ ### Minor Changes
6
+
7
+ - c49efed: **Breaking (pre-1.0 minor):** Tenant API connection and executor credentials for Runtime Clients.
8
+
9
+ - `resolveConnection()` — options → environment → Project link; sources never mix.
10
+ - `createClient()` with no arguments uses `resolveConnection`.
11
+ - `connectAgents` application mode: save agents, `PUT /v1/executors` with **derived** tokens (HMAC of application key + Tenant + agent id), connect; tokens are not stored in Project credentials.
12
+ - Re-exports `PROTOCOL_FEATURES`, `ERROR_CODES`, `compareVersions`.
13
+
14
+ - c49efed: **Breaking (pre-1.0 minor):** Replace a single SQLite Runtime per Project or home directory with a **Runtime Host** that serves isolated **Tenants**, selected by `Nylorun-Tenant` and negotiated with `Nylorun-Protocol` (protocol `2`, feature `runtime-tenants`). Vocabulary: Host root + Tenant + Project link.
15
+
16
+ - **core:** `PROTOCOL_VERSION = 2`, `HOST_PROTOCOL`, `TENANT_HEADER`, `PROTOCOL_HEADER`, `newTenantId` / `isTenantId`, `checkCompatibility`; health schema gains `hostId` + `protocol` (`service: "nylorun-runtime"`); Tenant/admin wire schemas; Tenant model routes under `/v1/tenant/*`.
17
+ - **runtime:** Host process + Tenant module; Tenant model routes under `/v1/tenant/*`; `startEphemeralRuntime` for tests/embeds; executors via `PUT /v1/executors`; sandbox prefix `nylorun-<tenant-id>-`.
18
+ - **agents:** `createClient({ url, key, tenant })`; Transport sends Tenant + protocol headers; `/health` compatibility cache; `IncompatibleRuntimeError` with upgrade remedies.
19
+ - **cli:** Host root lifecycle (`runtime up|down|status|logs|restart|run`); Project link (`.nylorun/link.json` + `credentials.json`); `tenant` commands; `runtime status --env` exports `NYLORUN_RUNTIME_URL`, `NYLORUN_SERVER_KEY`, `NYLORUN_TENANT`; removed Project/home SQLite selectors.
20
+ - **studio:** `startStudio({ …, tenant: { id, name } })`; proxy forwards Tenant + protocol headers; UI shows Tenant name/short id.
21
+
22
+ - fd9fd87: Add workflows: compose agents and `tool()` with `Chain`, `Switch`, `Parallel`, `Map`, and `Loop`. A workflow is a registered runnable (`kind: "workflow"`) with the same session API as an agent — `export const agents`, `saveAgent` (saves referenced agents first), `createSession`, `input` (`content` or `data`), `observe({ follow })`, `pending`, `approve`, `cancel`. Slots (`{ run, id?, input? }`) reshape data between nodes. The flow engine (`runFlowDurable`) returns effects only; `harness/src/loop/` and `runDurable` are unchanged.
23
+
24
+ HostEffect gains flow kinds `agent`, `tool` (node), `fn`, and `verify`, each with `path`, `key`, and `iterations`. The Runtime drives agent nodes through the public session contract (linked sessions, shared sandbox via `PutSession.sandbox`), offers `fn` / `verify` again on lease expiry, and routes executor Actions by `(workflowId, key)` with claim-scoped `ctx.sandbox`. Optional `message.manifest` is a turn-only variant of the session pin (turn manifests). Studio shows the manifest tree, live node status, and session links. Examples under `examples/agents/{chain,switch,parallel,map,loop,ship-feature}/`.
25
+
26
+ ### Patch Changes
27
+
28
+ - Pin core to the tested release.
29
+ - Updated dependencies [c49efed]
30
+ - Updated dependencies [c49efed]
31
+ - Updated dependencies [fd9fd87]
32
+ - @nylorun/core@0.5.0-beta
33
+
3
34
  ## 0.5.0-beta
4
35
 
5
36
  ### Minor Changes
package/README.md CHANGED
@@ -1,6 +1,39 @@
1
1
  # @nylorun/agents
2
2
 
3
- One application install provides definition authoring, a session client, and a connected customer executor. This beta is currently built locally; no packages were published in this pass. Use the workspace or install packed SDK and core artifacts together until publishing is enabled.
3
+ Tenant API client package: definition authoring, a session client, and a
4
+ connected customer executor. Depends only on `@nylorun/core` among Nylorun
5
+ packages. A developer application's production tree should contain only this
6
+ package and `@nylorun/core` from Nylorun. Vocabulary:
7
+ [runtime/src/CONTEXT.md](../runtime/src/CONTEXT.md).
8
+
9
+ ## Application entry (preferred)
10
+
11
+ ```ts
12
+ // src/main.ts
13
+ import { connectAgents } from "@nylorun/agents";
14
+ import { agents } from "../agents/index.js";
15
+
16
+ await connectAgents({ agents }).ready;
17
+ ```
18
+
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 `nylorun dev` and as `node dist/src/main.js`
23
+ (`npm start`). See [MIGRATION.md](../MIGRATION.md#runtime-clients-and-admin-api-breaking-beta)
24
+ for upgrading from `nylorun serve`.
25
+
26
+ ## Connection resolution
27
+
28
+ `resolveConnection` / `createClient()` / `connectAgents({ agents })`:
29
+
30
+ 1. Explicit `{ url, tenant, key }`
31
+ 2. Environment — if any of `NYLORUN_RUNTIME_URL`, `NYLORUN_TENANT`,
32
+ `NYLORUN_SERVER_KEY` or `NYLORUN_EXECUTOR_KEY` is set, all required pieces
33
+ must be present (role `executor` when `NYLORUN_EXECUTOR_KEY` is set)
34
+ 3. Project link — `.nylorun/link.json` + `credentials.json` (application role)
35
+
36
+ Sources never mix. Partial environment fails with `connection_missing`.
4
37
 
5
38
  ```ts
6
39
  import { Agent, createClient, connectAgents, tool } from "@nylorun/agents";
@@ -21,10 +54,11 @@ const assistant = Agent({
21
54
  ],
22
55
  });
23
56
 
24
- // Run these application operations on your trusted backend.
57
+ // Explicit Tenant API client (options or env / link via createClient()).
25
58
  const client = createClient({
26
59
  url: process.env.NYLORUN_RUNTIME_URL,
27
60
  key: process.env.NYLORUN_SERVER_KEY,
61
+ tenant: process.env.NYLORUN_TENANT,
28
62
  });
29
63
  await client.saveAgent(assistant, { implementationVersion: "app-1" });
30
64
  const session = await client.createSession({
@@ -34,21 +68,33 @@ const session = await client.createSession({
34
68
  await session.input("Look up item 123", { idempotencyKey: requestId });
35
69
  const history = await session.history();
36
70
 
37
- // Customer executor process: credential is separately scoped by the Runtime.
71
+ // Executor-only mode when you already hold an executor key:
38
72
  const connection = connectAgents({
39
73
  agents: [assistant],
40
74
  implementationVersion: "app-1",
41
75
  runtime: {
42
76
  url: process.env.NYLORUN_RUNTIME_URL,
43
77
  key: process.env.NYLORUN_EXECUTOR_KEY,
78
+ tenant: process.env.NYLORUN_TENANT,
44
79
  },
45
80
  onError: console.error,
46
81
  });
47
82
  await connection.ready;
48
- // On shutdown:
49
83
  await connection.close();
50
84
  ```
51
85
 
86
+ ```sh
87
+ eval "$(npx nylorun runtime status --env)"
88
+ # → NYLORUN_RUNTIME_URL, NYLORUN_SERVER_KEY, NYLORUN_TENANT
89
+ ```
90
+
91
+ Every request sets `Nylorun-Tenant` and `Nylorun-Protocol`. A `tenant` field in a
92
+ body or query is never read from caller input. Before the first authenticated
93
+ request, `Transport` fetches `/health` once, checks protocol compatibility, and
94
+ throws `IncompatibleRuntimeError` / `incompatible_host` when the Host range or
95
+ required features do not match. Re-exports include `PROTOCOL_FEATURES`,
96
+ `ERROR_CODES` and `compareVersions`.
97
+
52
98
  Load Agent Skills from a local catalog folder with `skills(path)`:
53
99
 
54
100
  ```ts
@@ -80,7 +126,7 @@ const assistant = Agent({
80
126
  type: "streamable-http",
81
127
  url: "https://mcp.example.com/github",
82
128
  },
83
- })
129
+ }),
84
130
  );
85
131
  ```
86
132
 
@@ -108,7 +154,7 @@ The model gets `bash`, `read`, `write`, `edit`, `grep` and `glob` on a Linux mac
108
154
  }))
109
155
  ```
110
156
 
111
- 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`, `scope`, ...) throw a `SandboxError` that says so. See [the sandbox design](../docs/design/sandboxes.md).
157
+ 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. See [the sandbox design](../docs/design/sandboxes.md).
112
158
 
113
159
  Put an agent in another agent's `tools` to let the model delegate to it:
114
160
 
@@ -118,15 +164,21 @@ import { z } from "zod";
118
164
 
119
165
  const researcher = Agent({
120
166
  id: "researcher",
121
- description: "Investigates an order's history. Returns a short summary with the ids it relied on.",
122
- instructions: "Investigate one question about one order. Be exhaustive, then be brief.",
167
+ description:
168
+ "Investigates an order's history. Returns a short summary with the ids it relied on.",
169
+ instructions:
170
+ "Investigate one question about one order. Be exhaustive, then be brief.",
123
171
  tools: [searchOrders, readTicket],
124
- outputSchema: z.object({ summary: z.string(), evidence: z.array(z.string()) }),
172
+ outputSchema: z.object({
173
+ summary: z.string(),
174
+ evidence: z.array(z.string()),
175
+ }),
125
176
  });
126
177
 
127
178
  const support = Agent({
128
179
  id: "support",
129
- instructions: "For anything needing more than two lookups, delegate to researcher with a complete, self-contained task.",
180
+ instructions:
181
+ "For anything needing more than two lookups, delegate to researcher with a complete, self-contained task.",
130
182
  tools: [lookupOrder, refundOrder, researcher],
131
183
  });
132
184
  ```
@@ -135,11 +187,100 @@ The tool is named after the agent's `id` and takes `{ task: string }`; the agent
135
187
 
136
188
  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.
137
189
 
190
+ ## Workflows
191
+
192
+ Nest `Chain`, `Switch`, `Parallel`, `Map`, and `Loop` around agents and `tool()` to
193
+ build durable flows. A workflow is a definition with `kind: "workflow"`: register
194
+ it in `export const agents` (or `saveAgent(workflow)` — referenced agents are
195
+ saved first), then `createSession({ agentId })` and `session.input(value)` like
196
+ any agent. `input` sends string values as `content` and other JSON as `data`.
197
+ `connectAgents({ agents: [workflow] })` saves the tree and connects executors for
198
+ the workflow id and every referenced agent. Tool nodes, pure functions (slot
199
+ `input`, Switch `on`, Map `over`, Loop `decide`), and Loop `verify` run as Actions
200
+ routed by `(workflowId, key)`.
201
+
202
+ ```ts
203
+ import { Agent, Chain, Loop, Map, tool } from "@nylorun/agents";
204
+ import { z } from "zod";
205
+
206
+ const planner = Agent({
207
+ id: "planner",
208
+ instructions: "Return { tasks: string[] }.",
209
+ outputSchema: z.object({ tasks: z.array(z.string()) }),
210
+ }).build();
211
+
212
+ const coder = Agent({
213
+ id: "coder",
214
+ instructions: "Implement one task. Return { summary }.",
215
+ outputSchema: z.object({ summary: z.string() }),
216
+ }).build();
217
+
218
+ const openPr = tool({
219
+ name: "open-pr",
220
+ input: z.object({ summaries: z.array(z.string()) }),
221
+ async run({ summaries }, ctx) {
222
+ if (!(await ctx.approve("Open the PR?"))) throw new Error("Rejected");
223
+ return { opened: true, count: summaries.length };
224
+ },
225
+ });
226
+
227
+ export const shipFeature = Chain({
228
+ id: "ship-feature",
229
+ steps: [
230
+ planner,
231
+ Map({
232
+ id: "implement",
233
+ over: (plan) => plan.tasks,
234
+ each: Loop({
235
+ id: "code",
236
+ run: coder,
237
+ verify: () => ({ pass: true }),
238
+ decide: ({ output }) => ({ output }),
239
+ }),
240
+ }),
241
+ {
242
+ run: openPr,
243
+ input: ({ value }) => ({
244
+ summaries: value.map((item: { summary: string }) => item.summary),
245
+ }),
246
+ },
247
+ ],
248
+ });
249
+
250
+ export const agents = [shipFeature];
251
+ ```
252
+
253
+ | Primitive | Role |
254
+ | --- | --- |
255
+ | **Chain** | Steps in order; slots reshape with `{ run, id?, input? }` |
256
+ | **Switch** | Pure `on(input) → key`, then the matching case (or `default`) |
257
+ | **Parallel** | Fixed named branches at once; output is an object |
258
+ | **Map** | Pure `over(input) → items`, one child per item; output is an array |
259
+ | **Loop** | `run` → `verify` → `decide` until `{ output }` |
260
+
261
+ Agents inside a workflow keep their own sessions (linked from the workflow
262
+ session) and share one sandbox: declare identical specs on every agent that
263
+ uses one, attach with `createSession({ …, sandbox: { session } })`, call
264
+ built-ins via `session.sandbox` (application) or `ctx.sandbox` (executor).
265
+ Observe with `session.observe({ follow: true })` to merge linked agent streams;
266
+ `pending()` lists waits across the tree. Studio renders the manifest tree and
267
+ live node status. Source examples:
268
+ `examples/agents/{chain,switch,parallel,map,loop,ship-feature}/` (outside the
269
+ default release registry). Host contract:
270
+ [HOST_CONTRACT.md](../harness/HOST_CONTRACT.md). Vocabulary (turn loop vs
271
+ workflow Loop): [harness/src/CONTEXT.md](../harness/src/CONTEXT.md).
272
+
138
273
  Use `session.observe({ cursor, signal })` for resumable canonical events, `session.inspect()` for waiting/uncertain state, and `approve`, `respond`, or `cancel` with an explicit stable idempotency key. Retry the same semantic command with the same key. `ownerUserId` must come from trusted server authentication. Application credentials are not browser credentials; browser applications need an authorized backend. Definition authoring is browser-bundleable.
139
274
 
140
- The Runtime destination is explicit, or defaults from `NYLORUN_RUNTIME_URL`. Application and executor keys default from `NYLORUN_SERVER_KEY` and `NYLORUN_EXECUTOR_KEY`. Implementation version defaults from `NYLORUN_IMPLEMENTATION_VERSION`, then `dev`. A host must provision executor scope for the agent id. Agents do not hash the manifest, and an in-flight action stays claimable after the registered digest changes. Model selection and model credentials belong to the Runtime.
275
+ Connection defaults follow `resolveConnection` (options → environment → Project
276
+ link). Implementation version defaults from `NYLORUN_IMPLEMENTATION_VERSION`,
277
+ then `dev`. Application mode of `connectAgents({ agents })` registers derived
278
+ executor principals; executor-only mode still takes an explicit executor key.
279
+ Agents do not hash the manifest, and an in-flight action stays claimable after
280
+ the registered digest changes. Model selection and model credentials belong to
281
+ the Tenant.
141
282
 
142
- `connectAgents({ agents })` 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.
283
+ 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.
143
284
 
144
285
  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`.
145
286
 
package/dist/client.d.ts CHANGED
@@ -1,11 +1,15 @@
1
- import type { AgentManifest, BuiltAgent, JsonValue } from "@nylorun/core/define";
1
+ import type { AgentManifest, BuiltAgent, BuiltWorkflow, JsonValue, SandboxToolName, WorkflowManifest } from "@nylorun/core/define";
2
2
  import { type CredentialInfo, type CredentialSelection, type SessionCommand, type VaultInfo } from "@nylorun/core/contracts";
3
3
  import { Transport, type Destination } from "./http.js";
4
+ /** Application-principal access to a session's sandbox built-ins (workflows.md §8). */
5
+ export type SessionSandbox = {
6
+ readonly [K in SandboxToolName]: (args: Record<string, unknown>) => Promise<unknown>;
7
+ };
4
8
  export interface AgentSource {
5
9
  readonly id: string;
6
- readonly manifest: AgentManifest;
7
- build?(): BuiltAgent;
8
- getBinding?: BuiltAgent["getBinding"];
10
+ readonly manifest: AgentManifest | WorkflowManifest;
11
+ build?(): BuiltAgent | BuiltWorkflow;
12
+ getBinding?: BuiltAgent["getBinding"] | BuiltWorkflow["getBinding"];
9
13
  }
10
14
  /** Closures stay on the live binding. The published manifest cannot name them. */
11
15
  export declare function assertNoMiddlewareClosures(agent: AgentSource): void;
@@ -15,6 +19,7 @@ export interface SessionView {
15
19
  ownerUserId: string;
16
20
  status: string;
17
21
  activeTurnId: string | null;
22
+ waits?: unknown;
18
23
  [key: string]: unknown;
19
24
  }
20
25
  export declare class AgentsClient {
@@ -33,7 +38,7 @@ export declare class AgentsClient {
33
38
  }): Promise<{
34
39
  sessions: SessionView[];
35
40
  }>;
36
- saveAgent(agent: AgentSource, options: {
41
+ saveAgent(agent: AgentSource | BuiltWorkflow, options: {
37
42
  implementationVersion: string;
38
43
  requestId?: string;
39
44
  }): Promise<unknown>;
@@ -45,6 +50,10 @@ export declare class AgentsClient {
45
50
  requestId?: string;
46
51
  vaultIds?: readonly string[];
47
52
  credentialSelections?: readonly CredentialSelection[];
53
+ /** Share another session's sandbox (public PutSession.sandbox). */
54
+ sandbox?: {
55
+ session: string;
56
+ };
48
57
  }): Promise<SessionClient>;
49
58
  createVault(options: {
50
59
  name: string;
@@ -118,16 +127,23 @@ export type CommandOptions = {
118
127
  export declare class SessionClient {
119
128
  private readonly transport;
120
129
  readonly id: string;
130
+ readonly sandbox: SessionSandbox;
121
131
  constructor(transport: Transport, id: string);
122
132
  private get path();
123
133
  inspect(signal?: AbortSignal): Promise<SessionView>;
134
+ /** Pending waits / interactions from the session inspect view. */
135
+ pending(signal?: AbortSignal): Promise<{}>;
124
136
  command(command: SessionCommand, signal?: AbortSignal): Promise<{
125
137
  status: "accepted";
126
138
  turnId: string | null;
127
139
  cursor: string | null;
128
140
  requestId: string;
129
141
  }>;
130
- input(content: string, options: CommandOptions): Promise<{
142
+ /**
143
+ * Start a turn. Strings become `message.content`; any other JSON value becomes
144
+ * `message.data` (workflows.md §5).
145
+ */
146
+ input(value: string | JsonValue, options: CommandOptions): Promise<{
131
147
  status: "accepted";
132
148
  turnId: string | null;
133
149
  cursor: string | null;
@@ -162,6 +178,7 @@ export declare class SessionClient {
162
178
  items: {
163
179
  eventId: string;
164
180
  sessionId: string;
181
+ tenantId: string;
165
182
  turnId: string | null;
166
183
  cursor: string;
167
184
  createdAt: string;
@@ -173,17 +190,32 @@ export declare class SessionClient {
173
190
  observe(options?: {
174
191
  cursor?: string;
175
192
  signal?: AbortSignal;
193
+ follow?: boolean;
176
194
  }): AsyncGenerator<{
177
195
  eventId: string;
178
196
  sessionId: string;
197
+ tenantId: string;
179
198
  turnId: string | null;
180
199
  cursor: string;
181
200
  createdAt: string;
182
201
  type: string;
183
202
  payload: unknown;
184
203
  }, void, unknown>;
204
+ private observeOne;
205
+ /**
206
+ * Own session stream plus linked agent sessions announced in `node.agent`
207
+ * events, merged by `createdAt` then arrival order (workflows.md §11).
208
+ */
209
+ private observeFollowing;
185
210
  }
186
- export declare function createClient(destination?: Destination): AgentsClient;
187
- export { RuntimeError } from "./http.js";
188
- export type { Destination } from "./http.js";
211
+ /**
212
+ * Build a Tenant API client.
213
+ * Explicit connection fields keep today's sync Transport resolution (including
214
+ * environment fill-ins). With no connection fields, resolves via
215
+ * `resolveConnection` (environment → project link).
216
+ */
217
+ export declare function createClient(destination: Destination): AgentsClient;
218
+ export declare function createClient(destination?: undefined): Promise<AgentsClient>;
219
+ export { RuntimeError, IncompatibleRuntimeError } from "./http.js";
220
+ export type { Destination, IncompatibleReason } from "./http.js";
189
221
  export type { LiveEvent } from "@nylorun/core/contracts";
package/dist/client.js CHANGED
@@ -1,14 +1,42 @@
1
- import { delegateOf } from "@nylorun/core/define";
1
+ import { SANDBOX_TOOL_NAMES, delegateOf, isBuiltWorkflow, isSandboxToolName, } from "@nylorun/core/define";
2
2
  import { LiveEventSchema, SessionItemsResponseSchema, } from "@nylorun/core/contracts";
3
+ import { resolveConnection } from "./connection.js";
3
4
  import { Transport, id, segment } from "./http.js";
4
5
  import { observeSSE } from "./sse.js";
6
+ function sessionSandboxOf(transport, sessionId) {
7
+ const call = (tool, args) => transport.json(`/v1/sessions/${segment(sessionId)}/sandbox/${segment(tool)}`, "POST", args);
8
+ const sandbox = {};
9
+ for (const tool of SANDBOX_TOOL_NAMES) {
10
+ if (!isSandboxToolName(tool))
11
+ continue;
12
+ sandbox[tool] = (args) => call(tool, args);
13
+ }
14
+ return sandbox;
15
+ }
16
+ function linkedSessionId(event) {
17
+ if (event.type !== "node.agent")
18
+ return undefined;
19
+ const payload = event.payload;
20
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
21
+ return undefined;
22
+ const sessionId = payload.sessionId;
23
+ return typeof sessionId === "string" && sessionId.length > 0
24
+ ? sessionId
25
+ : undefined;
26
+ }
5
27
  const middlewareClosure = "Hosted definitions support declarative capabilities and before/after hooks, not middleware closures";
6
28
  /** The agent's declarations, then those of each agent it uses as a tool, keyed `<child>/<capability>`. */
7
29
  function declarationsOf(agent) {
30
+ if (isBuiltWorkflow(agent) || agent.manifest.kind === "workflow")
31
+ return [];
8
32
  const built = agent.build?.() ?? agent;
33
+ if (isBuiltWorkflow(built))
34
+ return [];
9
35
  const binding = built.getBinding?.();
10
- const found = (binding?.declarations ?? []).map((item) => ({ key: item.id, item }));
11
- for (const tool of binding?.tools ?? []) {
36
+ if (!binding || !("declarations" in binding))
37
+ return [];
38
+ const found = (binding.declarations ?? []).map((item) => ({ key: item.id, item }));
39
+ for (const tool of binding.tools ?? []) {
12
40
  const child = delegateOf(tool)?.agent;
13
41
  for (const item of child?.getBinding().declarations ?? [])
14
42
  found.push({ key: `${child.id}/${item.id}`, item });
@@ -41,7 +69,21 @@ export class AgentsClient {
41
69
  : "";
42
70
  return this.transport.json(`/v1/sessions${query}`, "GET", undefined, options.signal);
43
71
  }
44
- saveAgent(agent, options) {
72
+ async saveAgent(agent, options) {
73
+ if (isBuiltWorkflow(agent)) {
74
+ for (const binding of Object.values(agent.getBinding().agents)) {
75
+ await this.saveAgent({
76
+ id: binding.manifest.id,
77
+ manifest: binding.manifest,
78
+ getBinding: () => binding,
79
+ }, options);
80
+ }
81
+ return this.transport.json(`/v1/agents/${segment(agent.id)}`, "PUT", {
82
+ requestId: options.requestId ?? id(),
83
+ manifest: agent.manifest,
84
+ implementationVersion: options.implementationVersion,
85
+ });
86
+ }
45
87
  assertNoMiddlewareClosures(agent);
46
88
  const pluginRoots = pluginRootsOf(agent);
47
89
  return this.transport.json(`/v1/agents/${segment(agent.id)}`, "PUT", {
@@ -62,6 +104,7 @@ export class AgentsClient {
62
104
  ...(options.credentialSelections
63
105
  ? { credentialSelections: options.credentialSelections }
64
106
  : {}),
107
+ ...(options.sandbox ? { sandbox: options.sandbox } : {}),
65
108
  });
66
109
  return this.session(sessionId);
67
110
  }
@@ -114,9 +157,11 @@ export class AgentsClient {
114
157
  export class SessionClient {
115
158
  transport;
116
159
  id;
160
+ sandbox;
117
161
  constructor(transport, id) {
118
162
  this.transport = transport;
119
163
  this.id = id;
164
+ this.sandbox = sessionSandboxOf(transport, id);
120
165
  }
121
166
  get path() {
122
167
  return `/v1/sessions/${segment(this.id)}`;
@@ -124,16 +169,27 @@ export class SessionClient {
124
169
  inspect(signal) {
125
170
  return this.transport.json(this.path, "GET", undefined, signal);
126
171
  }
172
+ /** Pending waits / interactions from the session inspect view. */
173
+ async pending(signal) {
174
+ const view = await this.inspect(signal);
175
+ return view.waits ?? [];
176
+ }
127
177
  command(command, signal) {
128
178
  return this.transport.json(`${this.path}/commands`, "POST", command, signal);
129
179
  }
130
- input(content, options) {
131
- return this.command({
180
+ /**
181
+ * Start a turn. Strings become `message.content`; any other JSON value becomes
182
+ * `message.data` (workflows.md §5).
183
+ */
184
+ input(value, options) {
185
+ const base = {
132
186
  type: "message",
133
- content,
134
187
  requestId: options.requestId ?? id(),
135
188
  idempotencyKey: options.idempotencyKey,
136
- }, options.signal);
189
+ };
190
+ return this.command(typeof value === "string"
191
+ ? { ...base, content: value }
192
+ : { ...base, data: value }, options.signal);
137
193
  }
138
194
  approve(interactionId, approved, options) {
139
195
  return this.command({
@@ -172,14 +228,92 @@ export class SessionClient {
172
228
  return SessionItemsResponseSchema.parse(await this.transport.json(`${this.path}/items${search ? `?${search}` : ""}`, "GET", undefined, options.signal));
173
229
  }
174
230
  async *observe(options = {}) {
175
- for await (const frame of observeSSE(this.transport, `${this.path}/events`, options)) {
231
+ if (!options.follow) {
232
+ yield* this.observeOne(this.id, options);
233
+ return;
234
+ }
235
+ yield* this.observeFollowing(options);
236
+ }
237
+ async *observeOne(sessionId, options) {
238
+ for await (const frame of observeSSE(this.transport, `/v1/sessions/${segment(sessionId)}/events`, options)) {
176
239
  if (frame.event === "heartbeat" || frame.event === "ready")
177
240
  continue;
178
241
  yield LiveEventSchema.parse(JSON.parse(frame.data));
179
242
  }
180
243
  }
244
+ /**
245
+ * Own session stream plus linked agent sessions announced in `node.agent`
246
+ * events, merged by `createdAt` then arrival order (workflows.md §11).
247
+ */
248
+ async *observeFollowing(options) {
249
+ const signal = options.signal;
250
+ const linked = new Set();
251
+ const queue = [];
252
+ let wait;
253
+ let open = 1;
254
+ let failed;
255
+ const wake = () => {
256
+ wait?.();
257
+ wait = undefined;
258
+ };
259
+ const push = (event) => {
260
+ queue.push(event);
261
+ queue.sort((a, b) => {
262
+ const byTime = a.createdAt.localeCompare(b.createdAt);
263
+ return byTime !== 0 ? byTime : a.cursor.localeCompare(b.cursor);
264
+ });
265
+ wake();
266
+ };
267
+ const pump = async (sessionId, cursor) => {
268
+ try {
269
+ for await (const event of this.observeOne(sessionId, { cursor, signal })) {
270
+ push(event);
271
+ const link = linkedSessionId(event);
272
+ if (link && link !== this.id && !linked.has(link)) {
273
+ linked.add(link);
274
+ open += 1;
275
+ void pump(link).finally(() => {
276
+ open -= 1;
277
+ wake();
278
+ });
279
+ }
280
+ }
281
+ }
282
+ catch (error) {
283
+ if (!signal?.aborted)
284
+ failed = error;
285
+ }
286
+ };
287
+ void pump(this.id, options.cursor).finally(() => {
288
+ open -= 1;
289
+ wake();
290
+ });
291
+ while (!signal?.aborted) {
292
+ if (failed)
293
+ throw failed;
294
+ if (queue.length) {
295
+ yield queue.shift();
296
+ continue;
297
+ }
298
+ if (open <= 0)
299
+ return;
300
+ await new Promise((resolve) => {
301
+ wait = resolve;
302
+ });
303
+ }
304
+ }
181
305
  }
182
306
  export function createClient(destination = {}) {
183
- return new AgentsClient(destination);
307
+ if (destination.url !== undefined ||
308
+ destination.key !== undefined ||
309
+ destination.tenant !== undefined) {
310
+ return new AgentsClient(destination);
311
+ }
312
+ return resolveConnection().then((connection) => new AgentsClient({
313
+ url: connection.url,
314
+ key: connection.key,
315
+ tenant: connection.tenant,
316
+ ...(destination.fetch ? { fetch: destination.fetch } : {}),
317
+ }));
184
318
  }
185
- export { RuntimeError } from "./http.js";
319
+ export { RuntimeError, IncompatibleRuntimeError } from "./http.js";
@@ -0,0 +1,19 @@
1
+ import type { ErrorCode } from "@nylorun/core/compatibility";
2
+ export interface ResolvedConnection {
3
+ url: string;
4
+ tenant: string;
5
+ key: string;
6
+ role: "application" | "executor";
7
+ source: "options" | "environment" | "project-link";
8
+ }
9
+ export declare class ConnectionError extends Error {
10
+ readonly code: ErrorCode;
11
+ constructor(message: string);
12
+ }
13
+ /** Resolve Tenant API connection: options → environment → project link (D§7.1). */
14
+ export declare function resolveConnection(options?: {
15
+ url?: string;
16
+ tenant?: string;
17
+ key?: string;
18
+ cwd?: string;
19
+ }): Promise<ResolvedConnection>;