@nylorun/agents 0.23.0-beta → 0.25.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.
@@ -1,13 +1,14 @@
1
- import { SANDBOX_TOOL_NAMES, approvalRefusal, codeToolRefusal, delegateOf, isBuiltWorkflow, isSandboxToolName, } from "@nylorun/core/define";
1
+ import { SANDBOX_TOOL_NAMES, isSandboxToolName } from "@nylorun/core/define";
2
2
  import { z } from "zod";
3
3
  import { SCOPES_HEADER, SUBJECT_HEADER } from "@nylorun/core/compatibility";
4
4
  import { SessionManifestViewSchema, SessionUsageTotalsSchema, ModelCallsPageSchema, ToolCallsPageSchema, TurnTraceSchema, parseSessionEvent, parseSubjectHeaders, } from "@nylorun/core/contracts";
5
5
  import { SessionsReadClient } from "./reads.js";
6
6
  import { AccessClient } from "./access.js";
7
- import { EnvironmentsClient } from "./environments.js";
7
+ import { AgentInstancesClient } from "./agents.js";
8
+ import { AgentManifestsClient } from "./agent-manifests.js";
8
9
  import { ArtifactsClient } from "./artifacts.js";
9
10
  import { FilesClient } from "./files.js";
10
- import { Transport, env, id, segment } from "./http.js";
11
+ import { Transport, id, segment } from "./http.js";
11
12
  import { observeSSE } from "./sse.js";
12
13
  function sessionSandboxOf(transport, sessionId) {
13
14
  const call = (tool, args) => transport.json(`/v1/sessions/${segment(sessionId)}/sandbox/${segment(tool)}`, "POST", args);
@@ -30,33 +31,6 @@ function linkedSessionId(event) {
30
31
  ? sessionId
31
32
  : undefined;
32
33
  }
33
- /** The agent's declarations, then those of each agent it uses as a tool. */
34
- function declarationsOf(agent) {
35
- if (isBuiltWorkflow(agent) || agent.manifest.kind === "workflow")
36
- return [];
37
- const built = agent.build?.() ?? agent;
38
- if (isBuiltWorkflow(built))
39
- return [];
40
- const binding = built.getBinding?.();
41
- if (!binding || !("parts" in binding))
42
- return [];
43
- const found = [...(binding.parts ?? [])];
44
- for (const tool of binding.tools ?? []) {
45
- const delegate = delegateOf(tool);
46
- found.push(...(delegate?.agent?.getBinding().parts ?? []));
47
- // A flow agent used as a tool: its agents' declarations.
48
- for (const leaf of Object.values(delegate?.workflow?.getBinding().agents ?? {}))
49
- found.push(...(leaf.parts ?? []));
50
- }
51
- return found;
52
- }
53
- /** The bytes of every skill file the agent's definition names, by `sha256:<hex>`. */
54
- function skillFilesOf(agent, into = new Map()) {
55
- for (const item of declarationsOf(agent))
56
- for (const [sha256, source] of Object.entries(item.skillFiles ?? {}))
57
- into.set(sha256, source);
58
- return into;
59
- }
60
34
  export class AgentsClient {
61
35
  transport;
62
36
  /** The subject this client acts for (`as`), or `undefined` for the principal itself. */
@@ -92,12 +66,19 @@ export class AgentsClient {
92
66
  return new AccessClient(this.transport);
93
67
  }
94
68
  /**
95
- * Environments (protocol 11): `put` one by id (create, find or change), write its `secrets`,
96
- * `list` them by label, `stop`, `reset` or `delete` one, or give a session its own with
97
- * `forSession`.
69
+ * Agent manifests (protocol 16): `save` a manifest's next version (a v8 document, what
70
+ * `composeManifest` read, or a built agent with its runtime section), `plan` a save, read
71
+ * their versions.
98
72
  */
99
- get environments() {
100
- return new EnvironmentsClient(this.transport, (options) => this.createSession(options));
73
+ get agentManifests() {
74
+ return new AgentManifestsClient(this.transport);
75
+ }
76
+ /**
77
+ * Agents (protocol 16): `put` one by id from a saved manifest (create or find), write its
78
+ * `secrets`, `list` them, `stop`, `reset`, `fork` or `delete` one. Sessions run on them.
79
+ */
80
+ get agents() {
81
+ return new AgentInstancesClient(this.transport);
101
82
  }
102
83
  /** File artifacts: upload, list, download with Range, capability links (protocol 6). */
103
84
  get artifacts() {
@@ -114,54 +95,19 @@ export class AgentsClient {
114
95
  get sessions() {
115
96
  return new SessionsReadClient(this.transport);
116
97
  }
117
- listAgents(options = {}) {
118
- return this.transport.json("/v1/agents", "GET", undefined, options.signal);
119
- }
120
98
  listSessions(options = {}) {
121
99
  const query = options.agentId
122
100
  ? `?agentId=${encodeURIComponent(options.agentId)}`
123
101
  : "";
124
102
  return this.transport.json(`/v1/sessions${query}`, "GET", undefined, options.signal);
125
103
  }
126
- /**
127
- * Registers the agent's definition (`PUT /v1/agents/{id}`), first uploading the skill files
128
- * it names that the Runtime does not hold. A definition with a tool that runs your code
129
- * (`tool({ run })`, a flow's tool stage) is refused before anything is sent: the Runtime runs
130
- * agents from their manifests alone. `implementationVersion` defaults to
131
- * `NYLORUN_IMPLEMENTATION_VERSION`, else `dev`. `dryRun` (protocol 14) runs every check and
132
- * saves nothing; the skill files are still uploaded, since they are stored by content.
133
- */
134
- async saveAgent(agent, options = {}) {
135
- const files = new Map();
136
- if (isBuiltWorkflow(agent)) {
137
- // A flow agent embeds its agents: one document, carrying their files.
138
- for (const binding of Object.values(agent.getBinding().agents)) {
139
- const leaf = { id: binding.manifest.id, manifest: binding.manifest, getBinding: () => binding };
140
- skillFilesOf(leaf, files);
141
- }
142
- }
143
- else {
144
- skillFilesOf(agent, files);
145
- }
146
- const refusal = codeToolRefusal(agent.manifest) ??
147
- approvalRefusal(agent.manifest);
148
- if (refusal)
149
- throw new Error(refusal);
150
- await this.files.ensure(files);
151
- return this.transport.json(`/v1/agents/${segment(agent.id)}${options.dryRun ? "?dryRun=true" : ""}`, "PUT", {
152
- requestId: options.requestId ?? id(),
153
- manifest: agent.manifest,
154
- implementationVersion: options.implementationVersion ?? env("NYLORUN_IMPLEMENTATION_VERSION") ?? "dev",
155
- });
156
- }
157
104
  async createSession(options) {
158
105
  const sessionId = options.id ?? id();
159
106
  await this.transport.json(`/v1/sessions/${segment(sessionId)}`, "PUT", {
160
107
  requestId: options.requestId ?? id(),
161
- agentId: options.agentId,
108
+ agent: options.agent,
162
109
  ownerUserId: options.ownerUserId,
163
110
  ...(options.info ? { info: options.info } : {}),
164
- ...(options.environment === undefined ? {} : { environment: options.environment }),
165
111
  });
166
112
  return this.session(sessionId);
167
113
  }
@@ -189,6 +135,7 @@ export class SessionClient {
189
135
  const view = await this.inspect(signal);
190
136
  return view.waits ?? [];
191
137
  }
138
+ /** A message answers `queued` when it waits for the running turn (protocol 17). */
192
139
  command(command, signal) {
193
140
  return this.transport.json(`${this.path}/commands`, "POST", command, signal);
194
141
  }
@@ -201,6 +148,7 @@ export class SessionClient {
201
148
  type: "message",
202
149
  requestId: options.requestId ?? id(),
203
150
  idempotencyKey: options.idempotencyKey,
151
+ ...(options.delivery ? { delivery: options.delivery } : {}),
204
152
  };
205
153
  return this.command(typeof value === "string"
206
154
  ? { ...base, content: value }
@@ -217,6 +165,7 @@ export class SessionClient {
217
165
  requestId: options.requestId ?? id(),
218
166
  idempotencyKey: options.idempotencyKey,
219
167
  parts: [...parts],
168
+ ...(options.delivery ? { delivery: options.delivery } : {}),
220
169
  }, options.signal);
221
170
  }
222
171
  approve(interactionId, approved, options) {
@@ -266,7 +215,7 @@ export class SessionClient {
266
215
  return ModelCallsPageSchema.parse(await this.transport.json(`${this.path}/calls/model?${query}`, "GET", undefined, options.signal));
267
216
  }
268
217
  /**
269
- * The session's tool calls (R5, Host feature `tool-calls`), in the order they were first sent:
218
+ * The session's tool calls (Host feature `tool-calls`), in the order they were first sent:
270
219
  * each one's tool, input, status and sends. `attempts` above 1: a crash re-sent it.
271
220
  */
272
221
  async toolCalls(options = {}) {
@@ -279,7 +228,7 @@ export class SessionClient {
279
228
  return ToolCallsPageSchema.parse(await this.transport.json(`${this.path}/calls/tool?${query}`, "GET", undefined, options.signal));
280
229
  }
281
230
  /**
282
- * A turn's trace (R7, Host feature `traces`): its spans as OTLP/HTTP JSON (`resourceSpans`), as
231
+ * A turn's trace (Host feature `traces`): its spans as OTLP/HTTP JSON (`resourceSpans`), as
283
232
  * the Runtime posts them to an OTLP collector. Post it to a collector's `/v1/traces` to backfill
284
233
  * one, or read it without one.
285
234
  */
@@ -385,4 +334,5 @@ export class SessionClient {
385
334
  }
386
335
  export { RuntimeError, IncompatibleRuntimeError } from "./http.js";
387
336
  export { AccessClient } from "./access.js";
388
- export { EnvironmentManifestsClient, EnvironmentSecretsClient, EnvironmentsClient, environmentFiles, environmentManifest, } from "./environments.js";
337
+ export { AgentInstancesClient, AgentMemoriesClient, AgentSecretsClient, runtimeFiles } from "./agents.js";
338
+ export { AgentManifestsClient, ManifestGapsError, agentManifest, requirementGaps } from "./agent-manifests.js";
@@ -1,2 +1,2 @@
1
- export { loadSkillsFromDirectory, resolveSkillsRoot, parseSkill, SkillsError, } from "./load.js";
1
+ export { hashFolder, loadSkillsFromDirectory, resolveSkillsRoot, parseSkill, SkillsError, } from "./load.js";
2
2
  export type { LoadedSkill, SkillDiagnostic } from "./load.js";
@@ -1 +1 @@
1
- export { loadSkillsFromDirectory, resolveSkillsRoot, parseSkill, SkillsError, } from "./load.js";
1
+ export { hashFolder, loadSkillsFromDirectory, resolveSkillsRoot, parseSkill, SkillsError, } from "./load.js";
@@ -14,6 +14,11 @@ export declare class SkillsError extends Error {
14
14
  export interface LoadedSkill {
15
15
  readonly name: string;
16
16
  readonly description: string;
17
+ /**
18
+ * The `runtime.secrets` slots its scripts read (`environment_secret` or `environment_variable`),
19
+ * from its SKILL.md's frontmatter `secrets`: sorted, each once.
20
+ */
21
+ readonly secrets?: readonly string[];
17
22
  /** Every file of the folder by its `/`-separated path, as `sha256:<hex>`. */
18
23
  readonly files: Readonly<Record<string, string>>;
19
24
  /** Each file's bytes, by `sha256:<hex>`, for the client to upload. */
@@ -32,8 +37,21 @@ export declare function loadSkillsFromDirectory(directory: string, diagnostics:
32
37
  readonly boundary?: string;
33
38
  readonly codePrefix?: string;
34
39
  }): Readonly<Record<string, LoadedSkill>>;
35
- export declare function parseSkill(text: string, directoryName: string): {
40
+ /** A SKILL.md's frontmatter and instructions: `secrets` lists the secret slots its scripts read. */
41
+ export interface ParsedSkill {
36
42
  name: string;
37
43
  description: string;
44
+ secrets?: string[];
38
45
  instructions: string;
39
- } | undefined;
46
+ }
47
+ export declare function parseSkill(text: string, directoryName: string): ParsedSkill | undefined;
48
+ /**
49
+ * Every file of `directory` (a runtime section's `files` entry with `from`), hashed as a skill's
50
+ * folder is: each by its `/`-separated path, with where to read its bytes. Version control, OS
51
+ * and `.env` files are left out; a symbolic link outside `boundary` (default the folder) is
52
+ * skipped with a diagnostic.
53
+ */
54
+ export declare function hashFolder(directory: string, diagnostics: SkillDiagnostic[], options?: {
55
+ readonly boundary?: string;
56
+ readonly codePrefix?: string;
57
+ }): Pick<LoadedSkill, "files" | "sources">;
@@ -3,6 +3,7 @@ import { closeSync, existsSync, openSync, readdirSync, readFileSync, readSync, r
3
3
  import { open } from "node:fs/promises";
4
4
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { SKILL_ENTRY, skillFilePathIssue, } from "@nylorun/core/define";
6
+ import { ENVIRONMENT_NAME_PATTERN } from "@nylorun/core/contracts";
6
7
  import matter from "gray-matter";
7
8
  const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
8
9
  /** Version control and operating system files, never part of a skill. */
@@ -71,12 +72,12 @@ export function loadSkillsFromDirectory(directory, diagnostics, options = {}) {
71
72
  });
72
73
  continue;
73
74
  }
74
- const parsed = parseSkill(readFileSync(skillReal, "utf8"), entry.name);
75
- if (!parsed) {
75
+ const parsed = readSkill(readFileSync(skillReal, "utf8"), entry.name);
76
+ if (typeof parsed === "string") {
76
77
  diagnostics.push({
77
78
  severity: "warning",
78
79
  code: `${prefix}.skill-skipped`,
79
- message: `Skipped skill '${entry.name}' because SKILL.md is not a valid Agent Skill`,
80
+ message: `Skipped skill '${entry.name}' because SKILL.md ${parsed}`,
80
81
  path: skillFile,
81
82
  });
82
83
  continue;
@@ -93,36 +94,61 @@ export function loadSkillsFromDirectory(directory, diagnostics, options = {}) {
93
94
  skills[parsed.name] = {
94
95
  name: parsed.name,
95
96
  description: parsed.description,
96
- ...readFiles(parsed.name, boundary, directoryReal, diagnostics, prefix),
97
+ ...(parsed.secrets === undefined ? {} : { secrets: parsed.secrets }),
98
+ ...readFiles(`skill '${parsed.name}'`, boundary, directoryReal, diagnostics, prefix),
97
99
  };
98
100
  }
99
101
  return skills;
100
102
  }
101
103
  export function parseSkill(text, directoryName) {
104
+ const parsed = readSkill(text, directoryName);
105
+ return typeof parsed === "string" ? undefined : parsed;
106
+ }
107
+ const INVALID_SKILL = "is not a valid Agent Skill";
108
+ /** The skill a SKILL.md declares, or why it declares none. */
109
+ function readSkill(text, directoryName) {
102
110
  let parsed;
103
111
  try {
104
112
  parsed = matter(text);
105
113
  }
106
114
  catch {
107
- return undefined;
115
+ return INVALID_SKILL;
108
116
  }
109
117
  const name = asTrimmedString(parsed.data.name);
110
118
  const description = asTrimmedString(parsed.data.description);
111
119
  if (!name || !description || description.length > 1024)
112
- return undefined;
120
+ return INVALID_SKILL;
113
121
  if (name.length > 64 || !SKILL_NAME.test(name) || name !== directoryName)
114
- return undefined;
122
+ return INVALID_SKILL;
123
+ const secrets = parsed.data.secrets;
124
+ if (secrets !== undefined &&
125
+ (!Array.isArray(secrets) ||
126
+ !secrets.every((item) => typeof item === "string" && ENVIRONMENT_NAME_PATTERN.test(item))))
127
+ return "has secrets that are not a list of environment variable names";
115
128
  return {
116
129
  name,
117
130
  description,
131
+ ...(secrets === undefined || secrets.length === 0
132
+ ? {}
133
+ : { secrets: [...new Set(secrets)].sort() }),
118
134
  instructions: parsed.content.replace(/^\r?\n/, ""),
119
135
  };
120
136
  }
121
137
  function asTrimmedString(value) {
122
138
  return typeof value === "string" ? value.trim() : "";
123
139
  }
124
- /** Every file of the skill's folder, hashed, and where to read each one's bytes. */
125
- function readFiles(name, boundary, directory, diagnostics, prefix) {
140
+ /**
141
+ * Every file of `directory` (a runtime section's `files` entry with `from`), hashed as a skill's
142
+ * folder is: each by its `/`-separated path, with where to read its bytes. Version control, OS
143
+ * and `.env` files are left out; a symbolic link outside `boundary` (default the folder) is
144
+ * skipped with a diagnostic.
145
+ */
146
+ export function hashFolder(directory, diagnostics, options = {}) {
147
+ const root = resolveSkillsRoot(directory);
148
+ return readFiles(`folder ${directory}`, options.boundary ?? root, root, diagnostics, options.codePrefix ?? "files");
149
+ }
150
+ /** Every file of the folder, hashed, and where to read each one's bytes. */
151
+ function readFiles(label, boundary, directory, diagnostics, prefix) {
126
152
  const files = {};
127
153
  const sources = {};
128
154
  const visited = new Set();
@@ -138,7 +164,7 @@ function readFiles(name, boundary, directory, diagnostics, prefix) {
138
164
  diagnostics.push({
139
165
  severity: "warning",
140
166
  code: `${prefix}.resource-denied`,
141
- message: `Denied a file of skill '${name}' outside the package: ${entry.name}`,
167
+ message: `Denied a file of ${label} outside the package: ${entry.name}`,
142
168
  path: full,
143
169
  });
144
170
  continue;
@@ -154,7 +180,7 @@ function readFiles(name, boundary, directory, diagnostics, prefix) {
154
180
  continue;
155
181
  const issue = skillFilePathIssue(path);
156
182
  if (issue)
157
- throw new SkillsError(`${prefix}.invalid-path`, `Skill '${name}' has a file whose path ${issue}: ${path}`);
183
+ throw new SkillsError(`${prefix}.invalid-path`, `${label[0].toUpperCase()}${label.slice(1)} has a file whose path ${issue}: ${path}`);
158
184
  const hash = hashFile(real);
159
185
  files[path] = hash;
160
186
  sources[hash] = { size: stats.size, read: () => readWhole(real, stats.size) };
@@ -18,13 +18,15 @@ export interface SkillsPart extends PartDeclaration {
18
18
  * ```
19
19
  *
20
20
  * Sets `skills`: each skill's name, description and files (every file of its folder, binary
21
- * included, by content hash). The client uploads the files the Runtime lacks when it registers
22
- * the agent; the Runtime adds the skill catalog and serves `load_skill` / `read_skill_resource`
23
- * from them.
21
+ * included, by content hash). A SKILL.md's frontmatter `secrets` (environment variable names)
22
+ * become the skill's `secrets` in the harness section when the agent's manifest is saved. The
23
+ * client uploads the files the Runtime lacks when it saves the agent's manifest; the Runtime adds
24
+ * the skill catalog and serves `load_skill` / `read_skill_resource` from them.
24
25
  */
25
26
  export declare function skills(directory: string): SkillsPart;
26
27
  /**
27
- * The manifest's skills and the bytes behind their files. A plugin's skills carry its label, so
28
- * a tool call can be traced to the plugin that brought the skill.
28
+ * The manifest's skills, the bytes behind their files and the secret slots their SKILL.md lists
29
+ * (`secrets`, the harness section's when the agent is saved). A plugin's skills carry its label,
30
+ * so a tool call can be traced to the plugin that brought the skill.
29
31
  */
30
- export declare function skillsDeclaration(listed: readonly LoadedSkill[], plugin?: PluginLabel): Required<Pick<PartDeclaration, "skills" | "skillFiles">>;
32
+ export declare function skillsDeclaration(listed: readonly LoadedSkill[], plugin?: PluginLabel): Required<Pick<PartDeclaration, "skills" | "skillFiles">> & Pick<PartDeclaration, "skillSecrets">;
@@ -13,9 +13,10 @@ import { loadSkillsFromDirectory, resolveSkillsRoot, } from "./load.js";
13
13
  * ```
14
14
  *
15
15
  * Sets `skills`: each skill's name, description and files (every file of its folder, binary
16
- * included, by content hash). The client uploads the files the Runtime lacks when it registers
17
- * the agent; the Runtime adds the skill catalog and serves `load_skill` / `read_skill_resource`
18
- * from them.
16
+ * included, by content hash). A SKILL.md's frontmatter `secrets` (environment variable names)
17
+ * become the skill's `secrets` in the harness section when the agent's manifest is saved. The
18
+ * client uploads the files the Runtime lacks when it saves the agent's manifest; the Runtime adds
19
+ * the skill catalog and serves `load_skill` / `read_skill_resource` from them.
19
20
  */
20
21
  export function skills(directory) {
21
22
  const root = resolveSkillsRoot(directory);
@@ -29,10 +30,12 @@ export function skills(directory) {
29
30
  return { id, root, ...skillsDeclaration(listed), diagnostics };
30
31
  }
31
32
  /**
32
- * The manifest's skills and the bytes behind their files. A plugin's skills carry its label, so
33
- * a tool call can be traced to the plugin that brought the skill.
33
+ * The manifest's skills, the bytes behind their files and the secret slots their SKILL.md lists
34
+ * (`secrets`, the harness section's when the agent is saved). A plugin's skills carry its label,
35
+ * so a tool call can be traced to the plugin that brought the skill.
34
36
  */
35
37
  export function skillsDeclaration(listed, plugin) {
38
+ const secrets = listed.filter((skill) => skill.secrets !== undefined).map((skill) => [skill.name, skill.secrets]);
36
39
  return {
37
40
  skills: Object.fromEntries(listed.map((skill) => [
38
41
  skill.name,
@@ -44,5 +47,6 @@ export function skillsDeclaration(listed, plugin) {
44
47
  },
45
48
  ])),
46
49
  skillFiles: Object.assign({}, ...listed.map((skill) => skill.sources)),
50
+ ...(secrets.length === 0 ? {} : { skillSecrets: Object.fromEntries(secrets) }),
47
51
  };
48
52
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nylorun/agents",
3
- "version": "0.23.0-beta",
3
+ "version": "0.25.0-beta",
4
4
  "description": "Nylorun agent definitions and session client",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -43,16 +43,16 @@
43
43
  "test": "vitest run --passWithNoTests"
44
44
  },
45
45
  "dependencies": {
46
- "@nylorun/core": "0.23.0-beta",
46
+ "@nylorun/core": "0.25.0-beta",
47
47
  "gray-matter": "^4.0.3"
48
48
  },
49
49
  "peerDependencies": {
50
- "zod": "^4.6.5"
50
+ "zod": "^4.6.7"
51
51
  },
52
52
  "devDependencies": {
53
- "@types/node": "^26.6.4",
53
+ "@types/node": "^26.6.5",
54
54
  "typescript": "npm:@typescript/typescript6@^6.0.2",
55
- "vitest": "^5.0.2"
55
+ "vitest": "^5.0.4"
56
56
  },
57
57
  "engines": {
58
58
  "node": ">=24"