@nylorun/agents 0.22.0-beta → 0.24.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 +129 -0
- package/README.md +148 -114
- package/dist/a2a/handler.d.ts +9 -4
- package/dist/a2a/handler.js +9 -0
- package/dist/ag-ui/handler.d.ts +13 -9
- package/dist/ag-ui/handler.js +8 -5
- package/dist/agent-manifests.d.ts +127 -0
- package/dist/agent-manifests.js +208 -0
- package/dist/agents.d.ts +456 -0
- package/dist/agents.js +331 -0
- package/dist/compose.d.ts +23 -0
- package/dist/compose.js +246 -0
- package/dist/files.d.ts +3 -3
- package/dist/files.js +3 -3
- package/dist/index.d.ts +8 -6
- package/dist/index.js +5 -4
- package/dist/plugins/load.d.ts +10 -0
- package/dist/plugins/load.js +25 -14
- package/dist/plugins/plugin.d.ts +1 -1
- package/dist/plugins/plugin.js +1 -1
- package/dist/reads.d.ts +3 -1
- package/dist/session-client.d.ts +78 -48
- package/dist/session-client.js +22 -72
- package/dist/skills/index.d.ts +1 -2
- package/dist/skills/index.js +1 -2
- package/dist/skills/load.d.ts +13 -2
- package/dist/skills/load.js +59 -19
- package/dist/skills/skills.d.ts +2 -2
- package/dist/skills/skills.js +2 -2
- package/package.json +5 -5
- package/dist/environments.d.ts +0 -387
- package/dist/environments.js +0 -299
package/dist/agents.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agents (protocol 16): `client.agents`. An Agent is the live instance of one version of an agent
|
|
3
|
+
* manifest (`client.agentManifests`): one environment of the manifest's runtime kind (`virtual`,
|
|
4
|
+
* `pod` or `none`), one vault of secrets for its shell, MCP servers and HTTP tools, and the
|
|
5
|
+
* sessions that run on it. It is made only from a saved manifest, `name` (tracking) or `name@n`
|
|
6
|
+
* (pinned), and runs one version of it at a time: `put` with another version moves it, and a save
|
|
7
|
+
* moves every Agent that tracks the manifest (track T3). Its manifest, kind, harness and storage
|
|
8
|
+
* are fixed (`409 agent_fixed_field`): `fork` makes a new Agent to change them. It has
|
|
9
|
+
* its own id and outlives its sessions; whether it serves one person or a project is the app's
|
|
10
|
+
* choice, made by its id:
|
|
11
|
+
*
|
|
12
|
+
* - `agents.put("users/alice/triage", { manifest: "triage", owner: "alice" })`: get-or-create
|
|
13
|
+
* in one call. Write its secrets with `agents.secrets.put(id, name, { value })` and open
|
|
14
|
+
* sessions on it with `createSession({ agent: id, ... })`. Creating it opens no session (D71).
|
|
15
|
+
*
|
|
16
|
+
* Ids may hold `/`; they are sent percent-encoded as one path segment. Secret values are
|
|
17
|
+
* write-only: no call reads one back.
|
|
18
|
+
*
|
|
19
|
+
* Triggers (track T4, D74): what its manifest's `runtime.triggers` declares starts its turns with
|
|
20
|
+
* no client connected. `triggers(id)` lists them with their status and next run, `pauseTrigger`,
|
|
21
|
+
* `resumeTrigger` and `runTrigger` change or run one, `rotateWebhookSecret` makes a webhook
|
|
22
|
+
* trigger's signing secret (D75), and `runs(id)` pages what each fire came to.
|
|
23
|
+
*
|
|
24
|
+
* Memory (track T5, D72): an Agent whose manifest sets `harness.memory` keeps Markdown files under
|
|
25
|
+
* `/memories` across sessions. `agents.memories` lists, reads, writes (to seed it), deletes and
|
|
26
|
+
* pages the versions every change leaves.
|
|
27
|
+
*
|
|
28
|
+
* Disk versions (Host feature `environment-disk-versions`): `snapshot(id)` keeps a version of
|
|
29
|
+
* the Agent's disk between turns, `restore(id, version)` puts one back, and
|
|
30
|
+
* `put(id, { manifest, from: { agent, version } })` creates a new Agent from another's version.
|
|
31
|
+
*/
|
|
32
|
+
import { AgentPageSchema, isAgentId, } from "@nylorun/core/contracts";
|
|
33
|
+
import { definitionFileHash } from "./files.js";
|
|
34
|
+
import { id, segment } from "./http.js";
|
|
35
|
+
/**
|
|
36
|
+
* A runtime section's `files` entry by content hash (Q6): `contents` (each file's path under
|
|
37
|
+
* `path` and its bytes or text) hashed, as `entry` for the manifest's `runtime.files` and `bytes`
|
|
38
|
+
* for `agentManifests.save`'s `files`, which uploads them. Each file holds at most 2 GiB.
|
|
39
|
+
*/
|
|
40
|
+
export async function runtimeFiles(path, contents) {
|
|
41
|
+
const files = {};
|
|
42
|
+
const bytes = {};
|
|
43
|
+
for (const [name, content] of Object.entries(contents).sort(([a], [b]) => a.localeCompare(b))) {
|
|
44
|
+
const data = typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
45
|
+
const sha256 = await definitionFileHash(data);
|
|
46
|
+
files[name] = sha256;
|
|
47
|
+
bytes[sha256] = data;
|
|
48
|
+
}
|
|
49
|
+
return { entry: { path, files }, bytes };
|
|
50
|
+
}
|
|
51
|
+
function filterQuery(filter, query = new URLSearchParams()) {
|
|
52
|
+
for (const [key, value] of Object.entries(filter.labels ?? {}))
|
|
53
|
+
query.append("label", `${key}=${value}`);
|
|
54
|
+
if (filter.state !== undefined)
|
|
55
|
+
query.set("state", filter.state);
|
|
56
|
+
if (filter.manifest !== undefined)
|
|
57
|
+
query.set("manifest", filter.manifest);
|
|
58
|
+
if (filter.outdated !== undefined)
|
|
59
|
+
query.set("outdated", String(filter.outdated));
|
|
60
|
+
return query;
|
|
61
|
+
}
|
|
62
|
+
/** An Agent's secrets: `client.agents.secrets`. */
|
|
63
|
+
export class AgentSecretsClient {
|
|
64
|
+
transport;
|
|
65
|
+
path;
|
|
66
|
+
constructor(transport, path) {
|
|
67
|
+
this.transport = transport;
|
|
68
|
+
this.path = path;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Creates or replaces secret `name` (`PUT /v1/agents/{id}/secrets/{name}`): for
|
|
72
|
+
* `environment_secret` and `environment_variable` the name is the variable's. A second
|
|
73
|
+
* credential for a host or a URL is a 409 `credential_conflict`. When the Agent's manifest
|
|
74
|
+
* declares the secret's slot (D64), send only `{ value }` (or `{ headers }`); a name it does
|
|
75
|
+
* not declare is `409 secret_not_in_manifest`.
|
|
76
|
+
*/
|
|
77
|
+
async put(agentId, name, secret, options = {}) {
|
|
78
|
+
return this.transport.json(`${this.path(agentId)}/secrets/${segment(name)}`, "PUT", { requestId: id(), ...secret }, options.signal);
|
|
79
|
+
}
|
|
80
|
+
/** The secrets' names, types and bindings; never a value. */
|
|
81
|
+
async list(agentId, options = {}) {
|
|
82
|
+
const reply = await this.transport.json(`${this.path(agentId)}/secrets`, "GET", undefined, options.signal);
|
|
83
|
+
return reply.secrets;
|
|
84
|
+
}
|
|
85
|
+
async get(agentId, name, options = {}) {
|
|
86
|
+
return this.transport.json(`${this.path(agentId)}/secrets/${segment(name)}`, "GET", undefined, options.signal);
|
|
87
|
+
}
|
|
88
|
+
async delete(agentId, name, options = {}) {
|
|
89
|
+
return this.transport.json(`${this.path(agentId)}/secrets/${segment(name)}`, "DELETE", undefined, options.signal);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* An Agent's memories (track T5, D72): `client.agents.memories`. A path is a memory's path under
|
|
94
|
+
* `/memories` (`conventions/release.md` or `/memories/conventions/release.md`).
|
|
95
|
+
*/
|
|
96
|
+
export class AgentMemoriesClient {
|
|
97
|
+
transport;
|
|
98
|
+
path;
|
|
99
|
+
constructor(transport, path) {
|
|
100
|
+
this.transport = transport;
|
|
101
|
+
this.path = path;
|
|
102
|
+
}
|
|
103
|
+
memoryPath(agentId, path) {
|
|
104
|
+
const relative = path.replace(/^\/+/, "").replace(/^memories\//, "");
|
|
105
|
+
return `${this.path(agentId)}/memories/${segment(relative)}`;
|
|
106
|
+
}
|
|
107
|
+
/** The Agent's memories by path; `pathPrefix` keeps one folder's. */
|
|
108
|
+
async list(agentId, options = {}) {
|
|
109
|
+
const query = new URLSearchParams({ limit: String(options.limit ?? 100) });
|
|
110
|
+
if (options.pathPrefix !== undefined)
|
|
111
|
+
query.set("pathPrefix", options.pathPrefix);
|
|
112
|
+
if (options.cursor !== undefined)
|
|
113
|
+
query.set("cursor", options.cursor);
|
|
114
|
+
return this.transport.json(`${this.path(agentId)}/memories?${query}`, "GET", undefined, options.signal);
|
|
115
|
+
}
|
|
116
|
+
/** One memory with its content. */
|
|
117
|
+
async get(agentId, path, options = {}) {
|
|
118
|
+
return this.transport.json(this.memoryPath(agentId, path), "GET", undefined, options.signal);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Writes the memory whole, creating or replacing it: how an app seeds an Agent's memory. With
|
|
122
|
+
* `contentSha256` the write applies only while the memory still has that content (`""`: only
|
|
123
|
+
* while there is none); otherwise `409 memory_conflict`.
|
|
124
|
+
*/
|
|
125
|
+
async put(agentId, path, content, options = {}) {
|
|
126
|
+
return this.transport.json(this.memoryPath(agentId, path), "PUT", { content, ...(options.contentSha256 === undefined ? {} : { contentSha256: options.contentSha256 }) }, options.signal);
|
|
127
|
+
}
|
|
128
|
+
/** Deletes a memory, or every memory in a folder. */
|
|
129
|
+
async delete(agentId, path, options = {}) {
|
|
130
|
+
return this.transport.json(this.memoryPath(agentId, path), "DELETE", undefined, options.signal);
|
|
131
|
+
}
|
|
132
|
+
/** Every change to the Agent's memories, newest first; `path` keeps one memory's. */
|
|
133
|
+
async versions(agentId, options = {}) {
|
|
134
|
+
const query = new URLSearchParams({ limit: String(options.limit ?? 50) });
|
|
135
|
+
if (options.path !== undefined)
|
|
136
|
+
query.set("path", options.path);
|
|
137
|
+
if (options.cursor !== undefined)
|
|
138
|
+
query.set("cursor", options.cursor);
|
|
139
|
+
return this.transport.json(`${this.path(agentId)}/memory_versions?${query}`, "GET", undefined, options.signal);
|
|
140
|
+
}
|
|
141
|
+
/** One version with the content it left (`null` for a delete). */
|
|
142
|
+
async version(agentId, versionId, options = {}) {
|
|
143
|
+
return this.transport.json(`${this.path(agentId)}/memory_versions/${segment(versionId)}`, "GET", undefined, options.signal);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** The Tenant's Agents: `client.agents`. */
|
|
147
|
+
export class AgentInstancesClient {
|
|
148
|
+
transport;
|
|
149
|
+
constructor(transport) {
|
|
150
|
+
this.transport = transport;
|
|
151
|
+
}
|
|
152
|
+
path(agentId) {
|
|
153
|
+
if (!isAgentId(agentId))
|
|
154
|
+
throw new TypeError(`${agentId} is not an Agent id: up to 200 characters, /-separated segments of letters, digits, '.', '_' and '-'`);
|
|
155
|
+
return `/v1/agents/${segment(agentId)}`;
|
|
156
|
+
}
|
|
157
|
+
/** The Agent's secrets. */
|
|
158
|
+
get secrets() {
|
|
159
|
+
return new AgentSecretsClient(this.transport, (agentId) => this.path(agentId));
|
|
160
|
+
}
|
|
161
|
+
/** The Agent's memories (track T5). */
|
|
162
|
+
get memories() {
|
|
163
|
+
return new AgentMemoriesClient(this.transport, (agentId) => this.path(agentId));
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Creates the Agent, its vault and its compute from a saved agent manifest, or finds the one
|
|
167
|
+
* with this id (`PUT /v1/agents/{id}`): `{ manifest: "name" }` tracks the manifest (its latest
|
|
168
|
+
* version when the Agent is made), `"name@n"` pins version n. On an existing Agent, labels
|
|
169
|
+
* replace, `owner` sets or clears the owner guard, and `manifest` moves it to that version of
|
|
170
|
+
* its manifest (track T3): at once, at its pod's next start for a runtime change (`targetVersion`,
|
|
171
|
+
* `waitingFor: "start"`), or once a required secret the version adds is set. Another manifest,
|
|
172
|
+
* kind, harness or storage is `409 agent_fixed_field` (fork it instead).
|
|
173
|
+
*/
|
|
174
|
+
async put(agentId, input, options = {}) {
|
|
175
|
+
return this.transport.json(this.path(agentId), "PUT", { requestId: id(), ...input }, options.signal);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* What `put` would do to the existing Agent (`PUT /v1/agents/{id}?dryRun=true`, track T3): the
|
|
179
|
+
* version it runs and the one `input.manifest` names, when that applies (`now`, `next_start`,
|
|
180
|
+
* `secrets`, or `refused` with each fixed field), the changes, and the secrets it would wait
|
|
181
|
+
* for or leave unused. Nothing changes.
|
|
182
|
+
*/
|
|
183
|
+
async plan(agentId, input, options = {}) {
|
|
184
|
+
const reply = await this.transport.json(`${this.path(agentId)}?dryRun=true`, "PUT", { requestId: id(), ...input }, options.signal);
|
|
185
|
+
return reply.plan;
|
|
186
|
+
}
|
|
187
|
+
async get(agentId, options = {}) {
|
|
188
|
+
return this.transport.json(this.path(agentId), "GET", undefined, options.signal);
|
|
189
|
+
}
|
|
190
|
+
/** The Agents this client reaches with every label in `labels`, and the `state` or `manifest` given. */
|
|
191
|
+
async list(options = {}) {
|
|
192
|
+
const search = filterQuery(options).toString();
|
|
193
|
+
const reply = await this.transport.json(`/v1/agents${search ? `?${search}` : ""}`, "GET", undefined, options.signal);
|
|
194
|
+
return reply.agents;
|
|
195
|
+
}
|
|
196
|
+
/** One page of `list` (Host feature `session-reads`). */
|
|
197
|
+
async page(options = {}) {
|
|
198
|
+
await this.transport.requireFeature("session-reads", options.signal);
|
|
199
|
+
const query = new URLSearchParams({ limit: String(options.limit ?? 50) });
|
|
200
|
+
if (options.cursor !== undefined)
|
|
201
|
+
query.set("cursor", options.cursor);
|
|
202
|
+
filterQuery(options, query);
|
|
203
|
+
return AgentPageSchema.parse(await this.transport.json(`/v1/agents?${query}`, "GET", undefined, options.signal));
|
|
204
|
+
}
|
|
205
|
+
/** Stops a pod Agent's compute: its volume is kept, and the next turn starts it again. */
|
|
206
|
+
async stop(agentId, options = {}) {
|
|
207
|
+
return this.transport.json(`${this.path(agentId)}/stop`, "POST", {}, options.signal);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Applies a version the Agent waits to apply (`POST /v1/agents/{id}/restart`, track T3): a
|
|
211
|
+
* running pod stops, so the next turn starts it on the version; a stopped one starts at once.
|
|
212
|
+
* `retry` first moves it to the version that failed to start again. Refused during a turn.
|
|
213
|
+
*/
|
|
214
|
+
async restart(agentId, options = {}) {
|
|
215
|
+
return this.transport.json(`${this.path(agentId)}/restart`, "POST", { requestId: id(), ...(options.retry ? { retry: true } : {}) }, options.signal);
|
|
216
|
+
}
|
|
217
|
+
/** Resets a pod Agent: a new pod on a new, empty volume, with its runtime section applied. */
|
|
218
|
+
async reset(agentId, options = {}) {
|
|
219
|
+
return this.transport.json(`${this.path(agentId)}/reset`, "POST", {}, options.signal);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Keeps a version of the Agent's disk (`POST /v1/agents/{id}/snapshot`), between turns;
|
|
223
|
+
* `unchanged` when the disk is as the latest version has it.
|
|
224
|
+
*/
|
|
225
|
+
async snapshot(agentId, options = {}) {
|
|
226
|
+
await this.transport.requireFeature("environment-disk-versions", options.signal);
|
|
227
|
+
return this.transport.json(`${this.path(agentId)}/snapshot`, "POST", {}, options.signal);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Puts disk version `version` back: at once for a virtual Agent (`done`), on a new volume
|
|
231
|
+
* seeded before the next turn for a pod (`pending`).
|
|
232
|
+
*/
|
|
233
|
+
async restore(agentId, version, options = {}) {
|
|
234
|
+
await this.transport.requireFeature("environment-disk-versions", options.signal);
|
|
235
|
+
return this.transport.json(`${this.path(agentId)}/restore`, "POST", { requestId: id(), version }, options.signal);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Forks the Agent into a new one, `options.id` (Q15): how an Agent changes what is fixed for its
|
|
239
|
+
* life (its manifest, kind, harness or storage). It is made from `options.manifest` (default the source's manifest,
|
|
240
|
+
* tracking), its disk starts from `options.disk` (`latest`, the default, snapshots first; a
|
|
241
|
+
* version; or `empty`) and the secrets the new manifest declares are copied
|
|
242
|
+
* (`options.secrets: "none"` copies none). The source keeps what it runs, its disk and its
|
|
243
|
+
* sessions. Answers the new Agent and the plan it ran.
|
|
244
|
+
*/
|
|
245
|
+
async fork(agentId, options) {
|
|
246
|
+
const { signal, ...body } = options;
|
|
247
|
+
return this.transport.json(`${this.path(agentId)}/fork`, "POST", { requestId: id(), ...body }, signal);
|
|
248
|
+
}
|
|
249
|
+
/** What `fork` would do (the disk, the secrets copied and left behind, the changes), making nothing. */
|
|
250
|
+
async planFork(agentId, options) {
|
|
251
|
+
const { signal, ...body } = options;
|
|
252
|
+
const answer = await this.transport.json(`${this.path(agentId)}/fork`, "POST", { ...body, dryRun: true }, signal);
|
|
253
|
+
return answer.plan;
|
|
254
|
+
}
|
|
255
|
+
/** The Agent's disk versions, newest first. */
|
|
256
|
+
async versions(agentId, options = {}) {
|
|
257
|
+
await this.transport.requireFeature("environment-disk-versions", options.signal);
|
|
258
|
+
return this.transport.json(`${this.path(agentId)}/versions`, "GET", undefined, options.signal);
|
|
259
|
+
}
|
|
260
|
+
/** Deletes disk version `version`; `deleted` is false when there was none. */
|
|
261
|
+
async deleteVersion(agentId, version, options = {}) {
|
|
262
|
+
return this.transport.json(`${this.path(agentId)}/versions/${version}`, "DELETE", undefined, options.signal);
|
|
263
|
+
}
|
|
264
|
+
/** Deletes the Agent, its vault and its files; `deleted` is false when there was none. */
|
|
265
|
+
async delete(agentId, options = {}) {
|
|
266
|
+
return this.transport.json(this.path(agentId), "DELETE", undefined, options.signal);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* The Agent's events, from `from` on: its lifecycle, and `agent.egress` for every connection
|
|
270
|
+
* its sandbox opens (with the turns that were running). `types` keeps only those types;
|
|
271
|
+
* `since` and `until` keep events recorded in that window (ISO times or `Date`s).
|
|
272
|
+
*/
|
|
273
|
+
async events(agentId, options = {}) {
|
|
274
|
+
const query = new URLSearchParams();
|
|
275
|
+
if (options.from !== undefined)
|
|
276
|
+
query.set("from", String(options.from));
|
|
277
|
+
if (options.types !== undefined && options.types.length > 0)
|
|
278
|
+
query.set("types", options.types.join(","));
|
|
279
|
+
const iso = (at) => (at instanceof Date ? at.toISOString() : at);
|
|
280
|
+
if (options.since !== undefined)
|
|
281
|
+
query.set("since", iso(options.since));
|
|
282
|
+
if (options.until !== undefined)
|
|
283
|
+
query.set("until", iso(options.until));
|
|
284
|
+
const search = query.toString();
|
|
285
|
+
const reply = await this.transport.json(`${this.path(agentId)}/events${search === "" ? "" : `?${search}`}`, "GET", undefined, options.signal);
|
|
286
|
+
return reply.events;
|
|
287
|
+
}
|
|
288
|
+
/** The Agent's triggers: each one's status (`active`, `paused`, `completed`), next and last run. */
|
|
289
|
+
async triggers(agentId, options = {}) {
|
|
290
|
+
const reply = await this.transport.json(`${this.path(agentId)}/triggers`, "GET", undefined, options.signal);
|
|
291
|
+
return reply.triggers;
|
|
292
|
+
}
|
|
293
|
+
/** Pauses trigger `triggerId`: it stops firing until it is resumed. Answers the Agent's triggers. */
|
|
294
|
+
async pauseTrigger(agentId, triggerId, options = {}) {
|
|
295
|
+
const reply = await this.transport.json(`${this.path(agentId)}/triggers/${segment(triggerId)}/pause`, "POST", {}, options.signal);
|
|
296
|
+
return reply.triggers;
|
|
297
|
+
}
|
|
298
|
+
/** Resumes trigger `triggerId` from its next time after now. Answers the Agent's triggers. */
|
|
299
|
+
async resumeTrigger(agentId, triggerId, options = {}) {
|
|
300
|
+
const reply = await this.transport.json(`${this.path(agentId)}/triggers/${segment(triggerId)}/resume`, "POST", {}, options.signal);
|
|
301
|
+
return reply.triggers;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Runs trigger `triggerId` once now, as if it fired: `idempotencyKey` (default a new one) makes a
|
|
305
|
+
* retried call answer the same run.
|
|
306
|
+
*/
|
|
307
|
+
async runTrigger(agentId, triggerId, options = {}) {
|
|
308
|
+
const response = await this.transport.request(`${this.path(agentId)}/triggers/${segment(triggerId)}/run`, {
|
|
309
|
+
method: "POST",
|
|
310
|
+
headers: { "idempotency-key": options.idempotencyKey ?? id() },
|
|
311
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
312
|
+
});
|
|
313
|
+
return (await response.json()).run;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* A new Standard Webhooks signing secret for webhook trigger `triggerId` (`whsec_…`), shown this
|
|
317
|
+
* once with the trigger's `webhookUrl`; the one before stops working at once.
|
|
318
|
+
*/
|
|
319
|
+
async rotateWebhookSecret(agentId, triggerId, options = {}) {
|
|
320
|
+
return this.transport.json(`${this.path(agentId)}/triggers/${segment(triggerId)}/secret`, "POST", {}, options.signal);
|
|
321
|
+
}
|
|
322
|
+
/** The Agent's runs, newest first; `triggerId` keeps one trigger's. */
|
|
323
|
+
async runs(agentId, options = {}) {
|
|
324
|
+
const query = new URLSearchParams({ limit: String(options.limit ?? 50) });
|
|
325
|
+
if (options.triggerId !== undefined)
|
|
326
|
+
query.set("triggerId", options.triggerId);
|
|
327
|
+
if (options.cursor !== undefined)
|
|
328
|
+
query.set("cursor", options.cursor);
|
|
329
|
+
return this.transport.json(`${this.path(agentId)}/runs?${query}`, "GET", undefined, options.signal);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ComposedManifest } from "./agent-manifests.js";
|
|
2
|
+
/** The file `composeManifest` reads in a folder. */
|
|
3
|
+
export declare const AGENT_MANIFEST_FILE = "agent.manifest.json";
|
|
4
|
+
/** An authoring file that cannot be read or does not validate; `path` names it. */
|
|
5
|
+
export declare class ComposeError extends Error {
|
|
6
|
+
readonly path: string;
|
|
7
|
+
constructor(path: string, message: string);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Composes the agent manifest in `dir` (its `agent.manifest.json`, or `options.file`): the v8
|
|
11
|
+
* document to save, the bytes of every file it names by hash (skills, plugins' skills, runtime
|
|
12
|
+
* files) for `client.agentManifests.save` to upload, its gaps (the harness file's `requires`
|
|
13
|
+
* the runtime misses, and `sectionGaps`, which the Runtime would refuse it for), and the skills
|
|
14
|
+
* and servers skipped. Throws `ComposeError` for a file that cannot be read or does not validate.
|
|
15
|
+
*/
|
|
16
|
+
export declare function composeManifest(dir: string, options?: {
|
|
17
|
+
file?: string;
|
|
18
|
+
}): Promise<ComposedManifest>;
|
|
19
|
+
/**
|
|
20
|
+
* The folders `nylorun apply` composes for `dir`: `dir` itself when it holds an
|
|
21
|
+
* `agent.manifest.json`, else each of its subfolders that does, by name.
|
|
22
|
+
*/
|
|
23
|
+
export declare function manifestFolders(dir: string): Promise<string[]>;
|
package/dist/compose.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Three-file authoring (protocol 16, track T2): `composeManifest(dir)` reads `agent.manifest.json`
|
|
3
|
+
* and the files it names, and makes the one agent manifest v8 document the Runtime saves, with
|
|
4
|
+
* the bytes of every file it names by hash. `nylorun apply` runs it for each folder.
|
|
5
|
+
*
|
|
6
|
+
* - `agent.manifest.json` (`AgentManifestFileSchema`): the manifest's name, description and
|
|
7
|
+
* metadata, with `harness` and `runtime` inline or as paths to the two files below.
|
|
8
|
+
* - `harness.manifest.json` (`HarnessManifestFileSchema`): the harness section, whose `skills`
|
|
9
|
+
* may be a folder of skill folders, `mcpServers` an `mcp.json` (the Agent Plugins format), and
|
|
10
|
+
* `plugins` Agent Plugin folders, folded in as `.plugin()` folds them; `credentials` names the
|
|
11
|
+
* `runtime.secrets` slot each MCP server or HTTP tool authenticates with; `requires` is checked
|
|
12
|
+
* against the runtime section here, since the API never sees it.
|
|
13
|
+
* - `runtime.manifest.json` (`RuntimeManifestFileSchema`): the runtime section, often shared by
|
|
14
|
+
* many agents; a `files` entry `{ path, from }` is a folder beside the file, hashed into
|
|
15
|
+
* `{ path, files }`.
|
|
16
|
+
*
|
|
17
|
+
* Each path is relative to the file that names it. Node only: it reads the files.
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, statSync } from "node:fs";
|
|
20
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
21
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
22
|
+
import { AGENT_MANIFEST_V8_SCHEMA_VERSION, AgentManifestFileSchema, AgentManifestV8Schema, HarnessManifestFileSchema, RuntimeManifestFileSchema, WorkflowHarnessSectionSchema, } from "@nylorun/core/contracts";
|
|
23
|
+
import { sectionGaps } from "@nylorun/core/define";
|
|
24
|
+
import { requirementGaps } from "./agent-manifests.js";
|
|
25
|
+
import { loadMcpServers, loadPlugin } from "./plugins/load.js";
|
|
26
|
+
import { hashFolder, loadSkillsFromDirectory } from "./skills/load.js";
|
|
27
|
+
/** The file `composeManifest` reads in a folder. */
|
|
28
|
+
export const AGENT_MANIFEST_FILE = "agent.manifest.json";
|
|
29
|
+
/** An authoring file that cannot be read or does not validate; `path` names it. */
|
|
30
|
+
export class ComposeError extends Error {
|
|
31
|
+
path;
|
|
32
|
+
constructor(path, message) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.path = path;
|
|
35
|
+
this.name = "ComposeError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** The JSON file at `path`, as `schema` parses it, or a `ComposeError` naming every issue. */
|
|
39
|
+
async function readFileAs(path, schema, where) {
|
|
40
|
+
let text;
|
|
41
|
+
try {
|
|
42
|
+
text = await readFile(path, "utf8");
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
throw new ComposeError(path, `Cannot read ${where}: ${error.code ?? error}`);
|
|
46
|
+
}
|
|
47
|
+
let value;
|
|
48
|
+
try {
|
|
49
|
+
value = JSON.parse(text);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
throw new ComposeError(path, `${where} is not JSON: ${error.message}`);
|
|
53
|
+
}
|
|
54
|
+
return parsedAs(value, schema, path, where);
|
|
55
|
+
}
|
|
56
|
+
function parsedAs(value, schema, path, where) {
|
|
57
|
+
const parsed = schema.safeParse(value);
|
|
58
|
+
if (parsed.success)
|
|
59
|
+
return parsed.data;
|
|
60
|
+
const issues = parsed.error.issues.map((issue) => ` ${issue.path.length ? `${issue.path.map(String).join(".")}: ` : ""}${issue.message}`);
|
|
61
|
+
throw new ComposeError(path, `${where} is not valid:\n${issues.join("\n")}`);
|
|
62
|
+
}
|
|
63
|
+
const isWorkflow = (value) => !!value && typeof value === "object" && value.kind === "workflow";
|
|
64
|
+
/** `$schema` dropped: it is for editors. */
|
|
65
|
+
function withoutSchema(value) {
|
|
66
|
+
const { $schema: _, ...rest } = value;
|
|
67
|
+
return rest;
|
|
68
|
+
}
|
|
69
|
+
/** Reads the runtime section, its `{ path, from }` files hashed into `{ path, files }`. */
|
|
70
|
+
async function runtimeSection(runtime, base, agentFile, files, warnings) {
|
|
71
|
+
const file = typeof runtime === "string" ? resolve(base, runtime) : agentFile;
|
|
72
|
+
const parsed = typeof runtime === "string"
|
|
73
|
+
? await readFileAs(file, RuntimeManifestFileSchema, relativeTo(file))
|
|
74
|
+
: parsedAs(runtime, RuntimeManifestFileSchema, file, `${relativeTo(file)} runtime`);
|
|
75
|
+
const at = dirname(file);
|
|
76
|
+
const entries = [];
|
|
77
|
+
for (const entry of parsed.files ?? []) {
|
|
78
|
+
if (!("from" in entry)) {
|
|
79
|
+
entries.push(entry);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const folder = resolve(at, entry.from);
|
|
83
|
+
if (!existsSync(folder) || !statSync(folder).isDirectory())
|
|
84
|
+
throw new ComposeError(file, `runtime.files: ${entry.from} (for ${entry.path}) is not a folder beside ${relativeTo(file)}`);
|
|
85
|
+
const diagnostics = [];
|
|
86
|
+
const hashed = hashFolder(folder, diagnostics);
|
|
87
|
+
warnings.push(...warningsOf(diagnostics));
|
|
88
|
+
if (Object.keys(hashed.files).length === 0)
|
|
89
|
+
throw new ComposeError(file, `runtime.files: ${entry.from} (for ${entry.path}) holds no files`);
|
|
90
|
+
for (const [sha256, source] of Object.entries(hashed.sources))
|
|
91
|
+
files.set(sha256, source);
|
|
92
|
+
entries.push({ path: entry.path, files: { ...hashed.files } });
|
|
93
|
+
}
|
|
94
|
+
const section = withoutSchema(parsed);
|
|
95
|
+
return { ...section, ...(parsed.files === undefined ? {} : { files: entries }) };
|
|
96
|
+
}
|
|
97
|
+
function warningsOf(diagnostics) {
|
|
98
|
+
return diagnostics.filter((item) => item.severity === "warning").map((item) => item.message);
|
|
99
|
+
}
|
|
100
|
+
/** `path` relative to the working directory, as a person reads it. */
|
|
101
|
+
function relativeTo(path) {
|
|
102
|
+
return relative(process.cwd(), path) || path;
|
|
103
|
+
}
|
|
104
|
+
/** The agent harness section a `harness.manifest.json` describes, its paths resolved. */
|
|
105
|
+
function agentHarness(file, base, where, files, warnings) {
|
|
106
|
+
const skills = {};
|
|
107
|
+
const servers = {};
|
|
108
|
+
const plugins = [];
|
|
109
|
+
const addSkill = (skill, from) => {
|
|
110
|
+
if (skills[skill.name])
|
|
111
|
+
throw new ComposeError(where, `Skill ${skill.name} of ${from} is declared twice`);
|
|
112
|
+
skills[skill.name] = skill;
|
|
113
|
+
};
|
|
114
|
+
const addServer = (key, server, from) => {
|
|
115
|
+
if (servers[key])
|
|
116
|
+
throw new ComposeError(where, `MCP server ${key} of ${from} is declared twice`);
|
|
117
|
+
servers[key] = server;
|
|
118
|
+
};
|
|
119
|
+
// Skills: a folder of skill folders, each hashed as `.skills()` hashes it.
|
|
120
|
+
if (typeof file.skills === "string") {
|
|
121
|
+
const diagnostics = [];
|
|
122
|
+
for (const skill of Object.values(loadSkillsFromDirectory(resolve(base, file.skills), diagnostics))) {
|
|
123
|
+
addSkill({ name: skill.name, description: skill.description, files: { ...skill.files } }, file.skills);
|
|
124
|
+
for (const [sha256, source] of Object.entries(skill.sources))
|
|
125
|
+
files.set(sha256, source);
|
|
126
|
+
}
|
|
127
|
+
warnings.push(...warningsOf(diagnostics));
|
|
128
|
+
}
|
|
129
|
+
else
|
|
130
|
+
for (const skill of Object.values(file.skills ?? {}))
|
|
131
|
+
addSkill(skill, where);
|
|
132
|
+
// MCP servers: an `mcp.json` in the Agent Plugins format.
|
|
133
|
+
if (typeof file.mcpServers === "string") {
|
|
134
|
+
const path = resolve(base, file.mcpServers);
|
|
135
|
+
if (!existsSync(path))
|
|
136
|
+
throw new ComposeError(where, `mcpServers: ${file.mcpServers} does not exist`);
|
|
137
|
+
const diagnostics = [];
|
|
138
|
+
const loaded = loadMcpServers(path, diagnostics, { codePrefix: "harness" });
|
|
139
|
+
const invalid = diagnostics.find((item) => item.code === "harness.mcp-invalid");
|
|
140
|
+
if (invalid)
|
|
141
|
+
throw new ComposeError(path, `mcpServers: ${invalid.message}`);
|
|
142
|
+
warnings.push(...warningsOf(diagnostics));
|
|
143
|
+
for (const [key, server] of Object.entries(loaded))
|
|
144
|
+
addServer(key, server, file.mcpServers);
|
|
145
|
+
}
|
|
146
|
+
else
|
|
147
|
+
for (const [key, server] of Object.entries(file.mcpServers ?? {}))
|
|
148
|
+
addServer(key, server, where);
|
|
149
|
+
// Plugins: a folder is folded in as `.plugin()` folds it, each part labelled with the plugin.
|
|
150
|
+
for (const entry of file.plugins ?? []) {
|
|
151
|
+
if (typeof entry !== "string") {
|
|
152
|
+
plugins.push(entry);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const loaded = loadPlugin(resolve(base, entry));
|
|
156
|
+
warnings.push(...warningsOf(loaded.diagnostics).map((message) => `Plugin '${loaded.name}': ${message}`));
|
|
157
|
+
const label = { name: loaded.name, ...(loaded.version === undefined ? {} : { version: loaded.version }) };
|
|
158
|
+
for (const skill of Object.values(loaded.skills)) {
|
|
159
|
+
addSkill({ name: skill.name, description: skill.description, files: { ...skill.files }, plugin: label }, entry);
|
|
160
|
+
for (const [sha256, source] of Object.entries(skill.sources))
|
|
161
|
+
files.set(sha256, source);
|
|
162
|
+
}
|
|
163
|
+
for (const [key, server] of Object.entries(loaded.mcpServers))
|
|
164
|
+
addServer(key, { ...server, plugin: label }, entry);
|
|
165
|
+
plugins.push(label);
|
|
166
|
+
}
|
|
167
|
+
// Credentials: the slot each server or HTTP tool authenticates with.
|
|
168
|
+
let tools = file.tools;
|
|
169
|
+
for (const [name, slot] of Object.entries(file.credentials ?? {})) {
|
|
170
|
+
const server = servers[name];
|
|
171
|
+
const tool = tools?.find((item) => item.name === name && item.http !== undefined);
|
|
172
|
+
if (!server && !tool)
|
|
173
|
+
throw new ComposeError(where, `credentials names ${name}, which is no MCP server or HTTP tool of the harness`);
|
|
174
|
+
if (server)
|
|
175
|
+
servers[name] = { ...server, credential: slot };
|
|
176
|
+
if (tool)
|
|
177
|
+
tools = tools.map((item) => (item === tool ? { ...item, http: { ...item.http, credential: slot } } : item));
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
...(file.id === undefined ? {} : { id: file.id }),
|
|
181
|
+
...(file.instructions === undefined ? {} : { instructions: file.instructions }),
|
|
182
|
+
...(tools === undefined ? {} : { tools }),
|
|
183
|
+
...(Object.keys(skills).length === 0 ? {} : { skills }),
|
|
184
|
+
...(Object.keys(servers).length === 0 ? {} : { mcpServers: servers }),
|
|
185
|
+
...(plugins.length === 0 ? {} : { plugins }),
|
|
186
|
+
...(file.outputSchema === undefined ? {} : { outputSchema: file.outputSchema }),
|
|
187
|
+
...(file.loop === undefined ? {} : { loop: file.loop }),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Composes the agent manifest in `dir` (its `agent.manifest.json`, or `options.file`): the v8
|
|
192
|
+
* document to save, the bytes of every file it names by hash (skills, plugins' skills, runtime
|
|
193
|
+
* files) for `client.agentManifests.save` to upload, its gaps (the harness file's `requires`
|
|
194
|
+
* the runtime misses, and `sectionGaps`, which the Runtime would refuse it for), and the skills
|
|
195
|
+
* and servers skipped. Throws `ComposeError` for a file that cannot be read or does not validate.
|
|
196
|
+
*/
|
|
197
|
+
export async function composeManifest(dir, options = {}) {
|
|
198
|
+
const agentFile = resolve(dir, options.file ?? AGENT_MANIFEST_FILE);
|
|
199
|
+
const base = dirname(agentFile);
|
|
200
|
+
const authored = await readFileAs(agentFile, AgentManifestFileSchema, relativeTo(agentFile));
|
|
201
|
+
const files = new Map();
|
|
202
|
+
const warnings = [];
|
|
203
|
+
const runtime = authored.runtime === undefined ? undefined : await runtimeSection(authored.runtime, base, agentFile, files, warnings);
|
|
204
|
+
let harness;
|
|
205
|
+
let requires;
|
|
206
|
+
const harnessFile = typeof authored.harness === "string" ? resolve(base, authored.harness) : agentFile;
|
|
207
|
+
const where = relativeTo(harnessFile);
|
|
208
|
+
const raw = typeof authored.harness === "string"
|
|
209
|
+
? await readFileAs(harnessFile, { safeParse: (value) => ({ success: true, data: value }) }, where)
|
|
210
|
+
: authored.harness;
|
|
211
|
+
if (isWorkflow(raw)) {
|
|
212
|
+
// A flow: its agents are embedded, with their skills by hash; nothing to resolve.
|
|
213
|
+
harness = parsedAs(withoutSchema(raw), WorkflowHarnessSectionSchema, harnessFile, where);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
const file = parsedAs(raw, HarnessManifestFileSchema, harnessFile, where);
|
|
217
|
+
requires = file.requires;
|
|
218
|
+
harness = agentHarness(file, dirname(harnessFile), where, files, warnings);
|
|
219
|
+
}
|
|
220
|
+
const manifest = parsedAs({
|
|
221
|
+
manifestSchemaVersion: AGENT_MANIFEST_V8_SCHEMA_VERSION,
|
|
222
|
+
name: authored.name,
|
|
223
|
+
...(authored.description === undefined ? {} : { description: authored.description }),
|
|
224
|
+
...(authored.metadata === undefined ? {} : { metadata: authored.metadata }),
|
|
225
|
+
harness,
|
|
226
|
+
...(runtime === undefined ? {} : { runtime }),
|
|
227
|
+
}, AgentManifestV8Schema, agentFile, `The agent manifest ${authored.name}`);
|
|
228
|
+
const gaps = [...requirementGaps(requires, manifest.runtime).map((gap) => `harness.${gap}`), ...sectionGaps(manifest)];
|
|
229
|
+
return { manifest, files, gaps, warnings };
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* The folders `nylorun apply` composes for `dir`: `dir` itself when it holds an
|
|
233
|
+
* `agent.manifest.json`, else each of its subfolders that does, by name.
|
|
234
|
+
*/
|
|
235
|
+
export async function manifestFolders(dir) {
|
|
236
|
+
const root = resolve(dir);
|
|
237
|
+
if (existsSync(join(root, AGENT_MANIFEST_FILE)))
|
|
238
|
+
return [root];
|
|
239
|
+
const entries = await readdir(root, { withFileTypes: true }).catch((error) => {
|
|
240
|
+
throw new ComposeError(root, `Cannot read ${dir}: ${error.code ?? error}`);
|
|
241
|
+
});
|
|
242
|
+
return entries
|
|
243
|
+
.filter((entry) => entry.isDirectory() && existsSync(join(root, entry.name, AGENT_MANIFEST_FILE)))
|
|
244
|
+
.map((entry) => join(root, entry.name))
|
|
245
|
+
.sort();
|
|
246
|
+
}
|
package/dist/files.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Definition files (track R2 M4): `client.files`. A definition names the files it needs by the
|
|
3
3
|
* SHA-256 of their bytes (a skill's folder, `SkillManifest.files`); the Runtime holds each one
|
|
4
|
-
* once, uploaded with `PUT /v1/files/sha256:<hex>`, and refuses a
|
|
5
|
-
* does not hold (`400 definition_files_missing`). `
|
|
4
|
+
* once, uploaded with `PUT /v1/files/sha256:<hex>`, and refuses a manifest naming a file it
|
|
5
|
+
* does not hold (`400 definition_files_missing`). `agentManifests.save` uploads them itself.
|
|
6
6
|
*/
|
|
7
7
|
import { type DefinitionFileView } from "@nylorun/core/contracts";
|
|
8
8
|
import type { SkillFileSource } from "@nylorun/core/define";
|
|
@@ -17,7 +17,7 @@ export declare class FilesClient {
|
|
|
17
17
|
signal?: AbortSignal;
|
|
18
18
|
}): Promise<boolean>;
|
|
19
19
|
/**
|
|
20
|
-
* Uploads one file, at most
|
|
20
|
+
* Uploads one file, at most 2 GiB. The Runtime checks its bytes hash to `sha256` (default:
|
|
21
21
|
* their hash) and keeps a file it already holds as it is.
|
|
22
22
|
*/
|
|
23
23
|
upload(bytes: Uint8Array, options?: {
|
package/dist/files.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Definition files (track R2 M4): `client.files`. A definition names the files it needs by the
|
|
3
3
|
* SHA-256 of their bytes (a skill's folder, `SkillManifest.files`); the Runtime holds each one
|
|
4
|
-
* once, uploaded with `PUT /v1/files/sha256:<hex>`, and refuses a
|
|
5
|
-
* does not hold (`400 definition_files_missing`). `
|
|
4
|
+
* once, uploaded with `PUT /v1/files/sha256:<hex>`, and refuses a manifest naming a file it
|
|
5
|
+
* does not hold (`400 definition_files_missing`). `agentManifests.save` uploads them itself.
|
|
6
6
|
*/
|
|
7
7
|
import { isDefinitionFileHash, } from "@nylorun/core/contracts";
|
|
8
8
|
import { RuntimeError } from "./http.js";
|
|
@@ -37,7 +37,7 @@ export class FilesClient {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
|
-
* Uploads one file, at most
|
|
40
|
+
* Uploads one file, at most 2 GiB. The Runtime checks its bytes hash to `sha256` (default:
|
|
41
41
|
* their hash) and keeps a file it already holds as it is.
|
|
42
42
|
*/
|
|
43
43
|
async upload(bytes, options = {}) {
|