@nylorun/runtime 0.30.0-beta → 0.31.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.
@@ -2569,6 +2569,25 @@ export declare const agentManifests: import("drizzle-orm/pg-core").PgTableWithCo
2569
2569
  }, {}, {
2570
2570
  pgColumnBuilderBrand: 'PgCustomColumnBuilderBrand';
2571
2571
  }>;
2572
+ source: import("drizzle-orm/pg-core").PgColumn<{
2573
+ name: "source";
2574
+ tableName: "agent_manifests";
2575
+ dataType: "custom";
2576
+ columnType: "PgCustomColumn";
2577
+ data: unknown;
2578
+ driverParam: unknown;
2579
+ notNull: false;
2580
+ hasDefault: false;
2581
+ isPrimaryKey: false;
2582
+ isAutoincrement: false;
2583
+ hasRuntimeDefault: false;
2584
+ enumValues: undefined;
2585
+ baseColumn: never;
2586
+ identity: undefined;
2587
+ generated: undefined;
2588
+ }, {}, {
2589
+ pgColumnBuilderBrand: 'PgCustomColumnBuilderBrand';
2590
+ }>;
2572
2591
  };
2573
2592
  dialect: 'pg';
2574
2593
  }>;
@@ -326,6 +326,12 @@ export const agentManifests = nylorun.table("agent_manifests", {
326
326
  /** `{ actor }`: the subject the save acted for, else `application`. */
327
327
  savedBy: jsonText().notNull(),
328
328
  createdAt: textC().notNull(),
329
+ /**
330
+ * The agent folder the version was compiled from (track T6): `{ compiler, files, bytes,
331
+ * entries }`, each entry `{ path, size, sha256 }` of a definition file. Null for a version
332
+ * saved as JSON.
333
+ */
334
+ source: jsonText(),
329
335
  }, (t) => [primaryKey({ name: "agent_manifests_pkey", columns: [t.name, t.version] })]);
330
336
  /**
331
337
  * A vault (D56): the host model's (`id = 'host'`), or one Environment's, created and deleted with
@@ -295,6 +295,7 @@ function agentManifestOf(row) {
295
295
  hash: row.hash,
296
296
  savedBy: row.savedBy,
297
297
  createdAt: row.createdAt,
298
+ ...(row.source === null || row.source === undefined ? {} : { source: row.source }),
298
299
  };
299
300
  }
300
301
  /** A pod patch as column values: ISO times become dates, `null` clears. */
@@ -204,6 +204,20 @@ export interface AgentManifestRow {
204
204
  actor: string;
205
205
  };
206
206
  createdAt: string;
207
+ /** The agent folder the version was compiled from (track T6); none for a version saved as JSON. */
208
+ source?: AgentManifestSourceRecord;
209
+ }
210
+ /** An agent folder as a version keeps it: its compiler, size and files, each a definition file. */
211
+ export interface AgentManifestSourceRecord {
212
+ compiler: string;
213
+ files: number;
214
+ bytes: number;
215
+ /** Sorted by path; `sha256` is lowercase hex, the file `sha256:<hex>` in the definition file store. */
216
+ entries: {
217
+ path: string;
218
+ size: number;
219
+ sha256: string;
220
+ }[];
207
221
  }
208
222
  /** A disk lease (R2d step 3): what holds the disk, since when, and until when unless renewed. */
209
223
  export interface DiskOperation {
@@ -1,28 +1,4 @@
1
- /**
2
- * Agent manifests (protocol 16, track T2): `/v1/agent-manifests/{name}`, named and versioned
3
- * documents with what runs inside an agent's loop (`harness`) and what the Runtime provides
4
- * around it (`runtime`). They replace agent definitions (`PUT /v1/agents/:id` before protocol
5
- * 16) and environment manifests.
6
- *
7
- * - **Versions.** A save whose manifest hashes the same as the latest answers it unchanged; any
8
- * other writes the next version. A version is never changed or deleted alone.
9
- * - **Tracking Agents move.** Saving a version moves every Agent that tracks the manifest to it
10
- * (track T3, `moveAgent`), in the save's transaction: at once, at its pod's next start, or once
11
- * its required secrets are set. A pinned Agent stays, and so does one the version cannot move
12
- * (a fixed field) or whose start failed on it. The plan counts both.
13
- * - **Both sections at once.** The Runtime sees both sections, so it checks them against each
14
- * other (`sectionGaps`): every gap is named in one `422 manifest_inconsistent`, which is what
15
- * `requires` checked when a session opened before protocol 16. Each section's own checks run
16
- * too: the harness's (no code tools, no approvals, HTTP tool timeouts, the skill files) and the
17
- * runtime's (the Tenant's limits, its files).
18
- * - **Plan and drift.** `?dryRun=true` answers what a save would do and saves nothing: the
19
- * changes from the latest by section, every reason the Runtime would refuse it, warnings, the
20
- * Agents it moves and how many stay on older versions. `If-Match: <version>` refuses
21
- * (`412 manifest_changed`) a save whose latest version moved since the client read it; `0` asks
22
- * for a manifest that does not exist yet.
23
- * - **Every client is equal** (Q12): a version records who saved it (`savedBy`).
24
- */
25
- import { type AgentManifestChange, type AgentManifestPlan, type AgentManifestV8, type AgentManifestVersions, type AgentManifestView, type PutAgentManifestRequest, type SaveAgentManifestResponse } from "@nylorun/core/contracts";
1
+ import { type AgentManifestChange, type AgentManifestPlan, type AgentManifestSource, type AgentManifestV8, type AgentManifestVersions, type AgentManifestView, type PutAgentFolderRequest, type PutAgentManifestRequest, type SaveAgentManifestResponse } from "@nylorun/core/contracts";
26
2
  import { type RecipeRequest } from "../sandbox/resolve.js";
27
3
  import type { AuthScope, TenantContext } from "./context.js";
28
4
  /** What a manifest's runtime section asks of an Agent's recipe, with the harness its loop names. */
@@ -54,6 +30,23 @@ export declare function saveAgentManifest(ctx: TenantContext, name: string, body
54
30
  dryRun?: false;
55
31
  ifMatch?: number;
56
32
  }): Promise<SaveAgentManifestResponse>;
33
+ /**
34
+ * `PUT /v1/agent-manifests/{name}` with an agent folder by hash (track T6): reads the files the
35
+ * compiler needs from the definition file store, compiles the folder (`422 folder_invalid` when
36
+ * it does not), then saves or plans the manifest as a JSON save does, keeping the folder as the
37
+ * version's source.
38
+ */
39
+ export declare function saveAgentFolder(ctx: TenantContext, name: string, body: PutAgentFolderRequest, scope: AuthScope, options?: {
40
+ dryRun?: boolean;
41
+ ifMatch?: number;
42
+ }): Promise<AgentManifestPlan | SaveAgentManifestResponse>;
43
+ /** `GET /v1/agent-manifests/{name}/versions/{v}/source`: the folder a version was compiled from. */
44
+ export declare function getAgentManifestSource(ctx: TenantContext, name: string, version: number): Promise<AgentManifestSource>;
45
+ /** `GET .../versions/{v}/source/files/{path}`: one file of a version's source, streamed. */
46
+ export declare function readAgentManifestSourceFile(ctx: TenantContext, name: string, version: number, path: string): Promise<{
47
+ body: ReadableStream<Uint8Array>;
48
+ size: number;
49
+ }>;
57
50
  /** `GET /v1/agent-manifests/{name}` (the latest) and `.../versions/{n}`. */
58
51
  export declare function getAgentManifest(ctx: TenantContext, name: string, version?: number): Promise<AgentManifestView>;
59
52
  /** `GET /v1/agent-manifests`: each manifest's latest version. */
@@ -21,7 +21,15 @@
21
21
  * (`412 manifest_changed`) a save whose latest version moved since the client read it; `0` asks
22
22
  * for a manifest that does not exist yet.
23
23
  * - **Every client is equal** (Q12): a version records who saved it (`savedBy`).
24
+ * - **Agent folders** (track T6, D79): the same route takes a folder by hash
25
+ * (`application/vnd.nylorun.folder+json`). The Runtime reads the files the compiler needs from
26
+ * the definition file store, compiles them with `@nylorun/core/folder` (a folder that does not
27
+ * compile is `422 folder_invalid`, every diagnostic in `details.diagnostics`), and saves the
28
+ * manifest as above, with the folder kept as the version's source (`source`, its files recorded
29
+ * as the version's definition files). A folder whose manifest is unchanged saves no new
30
+ * version, so its source is not saved either.
24
31
  */
32
+ import { compileAgentFolder, folderTextPaths, AGENT_FOLDER_LIMITS, } from "@nylorun/core/folder";
25
33
  import { definitionFilesOf, } from "@nylorun/core/contracts";
26
34
  import { hashManifest } from "@nylorun/core/compatibility";
27
35
  import { approvalRefusal, codeToolRefusal, harnessIdOf, loopManifestOf, runtimeOf, sectionGaps, } from "@nylorun/core/define";
@@ -35,7 +43,7 @@ import { readRuntimeLimits } from "./runtime-limits.js";
35
43
  import { effectiveSandboxConfig, readSandboxConfig } from "../sandbox/tenant-config.js";
36
44
  import { actorOf, fileErrors } from "./agents.js";
37
45
  import { moveAgent, planMove } from "./agent-versions.js";
38
- import { definitionFileIssues } from "./definition-files.js";
46
+ import { definitionFileIssues, openDefinitionFile, readDefinitionFile } from "./definition-files.js";
39
47
  import { fail } from "./http.js";
40
48
  /** What a manifest's runtime section asks of an Agent's recipe, with the harness its loop names. */
41
49
  export function manifestRecipeRequest(manifest) {
@@ -73,6 +81,9 @@ function viewOf(row, counts) {
73
81
  savedBy: row.savedBy,
74
82
  createdAt: row.createdAt,
75
83
  agents: counts.get(row.version) ?? 0,
84
+ ...(row.source === undefined
85
+ ? {}
86
+ : { source: { compiler: row.source.compiler, files: row.source.files, bytes: row.source.bytes } }),
76
87
  };
77
88
  }
78
89
  /** Each field that differs between `before` and `after`, by path; arrays compare whole. */
@@ -237,6 +248,9 @@ export async function saveAgentManifest(ctx, name, body, scope, options = {}) {
237
248
  const { requestId: _, ...manifest } = body;
238
249
  if (manifest.name !== name)
239
250
  fail(400, `The manifest is named ${manifest.name}, and the path names ${name}: they must match.`);
251
+ return saveManifest(ctx, name, manifest, scope, options);
252
+ }
253
+ async function saveManifest(ctx, name, manifest, scope, options, folder) {
240
254
  const hash = agentManifestHash(manifest);
241
255
  const gaps = sectionGaps(manifest);
242
256
  return ctx.store.tx(async (t) => {
@@ -248,12 +262,33 @@ export async function saveAgentManifest(ctx, name, body, scope, options = {}) {
248
262
  : `Agent manifest ${name} is at version ${latest.version}, not ${options.ifMatch}: someone saved it since you read it. Read it again, or save without If-Match to replace it.`, { code: "manifest_changed", details: { latest: latest?.version ?? 0 } });
249
263
  const unchanged = latest?.hash === hash;
250
264
  const to = unchanged ? latest.version : (latest?.version ?? 0) + 1;
265
+ // A folder whose manifest is unchanged keeps the latest version, and with it that version's source.
266
+ const notes = folder === undefined
267
+ ? undefined
268
+ : unchanged && canonical(latest?.source?.entries ?? null) !== canonical(folder.source.entries)
269
+ ? [
270
+ ...folder.diagnostics,
271
+ {
272
+ severity: "note",
273
+ code: "source_not_saved",
274
+ message: "Source files changed that are not part of the agent; the manifest is unchanged, so no version is saved",
275
+ },
276
+ ]
277
+ : folder.diagnostics;
251
278
  const counts = await t.agentsOfManifest(name);
252
279
  const checked = await errorsOf(t, manifest);
253
280
  // The version as it would be saved, for the plan of the Agents it moves.
254
281
  const row = unchanged
255
282
  ? latest
256
- : { name, version: to, body: manifest, hash, savedBy: { actor: actorOf(scope) }, createdAt: new Date().toISOString() };
283
+ : {
284
+ name,
285
+ version: to,
286
+ body: manifest,
287
+ hash,
288
+ savedBy: { actor: actorOf(scope) },
289
+ createdAt: new Date().toISOString(),
290
+ ...(folder === undefined ? {} : { source: folder.source }),
291
+ };
257
292
  if (options.dryRun) {
258
293
  const errors = [...gaps, ...checked.errors];
259
294
  // A save with errors moves nothing; one that changes nothing moves no Agent either.
@@ -268,6 +303,7 @@ export async function saveAgentManifest(ctx, name, body, scope, options = {}) {
268
303
  errors,
269
304
  behind: moves?.behind ?? (await agentsBehind(t, name, to)).length,
270
305
  moves: { now: moves?.now ?? 0, nextStart: moves?.nextStart ?? 0, secrets: moves?.secrets ?? 0 },
306
+ ...(folder === undefined ? {} : { manifest, diagnostics: [...notes], compiler: folder.compiler }),
271
307
  };
272
308
  }
273
309
  if (gaps.length > 0)
@@ -287,8 +323,9 @@ export async function saveAgentManifest(ctx, name, body, scope, options = {}) {
287
323
  });
288
324
  if (checked.errors.length > 0)
289
325
  fail(400, checked.errors.join(" "));
326
+ const folderNotes = notes === undefined ? {} : { diagnostics: [...notes] };
290
327
  if (unchanged)
291
- return { manifest: viewOf(latest, counts), unchanged: true, warnings: warningsOf(ctx, manifest, to, undefined) };
328
+ return { manifest: viewOf(latest, counts), unchanged: true, warnings: warningsOf(ctx, manifest, to, undefined), ...folderNotes };
292
329
  await t.insertAgentManifest(row);
293
330
  // Its files by hash, skills' and runtime files' alike, for the sweep that deletes unused ones.
294
331
  const shas = new Set(definitionFilesOf(loopManifestOf(manifest)));
@@ -296,13 +333,108 @@ export async function saveAgentManifest(ctx, name, body, scope, options = {}) {
296
333
  if ("files" in file)
297
334
  for (const sha of Object.values(file.files))
298
335
  shas.add(sha);
336
+ // A folder's every file too: the version keeps its source.
337
+ for (const entry of folder?.source.entries ?? [])
338
+ shas.add(`sha256:${entry.sha256}`);
299
339
  if (shas.size > 0)
300
340
  await t.insertDefinitionFileUses(manifestFileOwner(name), hash, [...shas]);
301
341
  // Every Agent that tracks the manifest moves to the new version (track T3).
302
342
  const moves = await moveTracking(t, row, true);
303
- return { manifest: viewOf(row, await t.agentsOfManifest(name)), unchanged: false, warnings: warningsOf(ctx, manifest, to, moves) };
343
+ return {
344
+ manifest: viewOf(row, await t.agentsOfManifest(name)),
345
+ unchanged: false,
346
+ warnings: warningsOf(ctx, manifest, to, moves),
347
+ ...folderNotes,
348
+ };
349
+ });
350
+ }
351
+ /**
352
+ * `PUT /v1/agent-manifests/{name}` with an agent folder by hash (track T6): reads the files the
353
+ * compiler needs from the definition file store, compiles the folder (`422 folder_invalid` when
354
+ * it does not), then saves or plans the manifest as a JSON save does, keeping the folder as the
355
+ * version's source.
356
+ */
357
+ export async function saveAgentFolder(ctx, name, body, scope, options = {}) {
358
+ const entries = [...body.entries].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
359
+ if (entries.length > AGENT_FOLDER_LIMITS.files)
360
+ fail(413, `An agent folder holds at most ${AGENT_FOLDER_LIMITS.files} files; this one holds ${entries.length}`, {
361
+ code: "limit_exceeded",
362
+ details: { limit: AGENT_FOLDER_LIMITS.files },
363
+ });
364
+ // Every file must be held: the version keeps them all as its source.
365
+ const shas = [...new Set(entries.map((entry) => `sha256:${entry.sha256}`))].sort();
366
+ const held = await ctx.store.tx((t) => t.heldDefinitionFiles(shas));
367
+ const missing = entries.filter((entry) => !held.has(`sha256:${entry.sha256}`));
368
+ if (missing.length > 0)
369
+ fail(400, `The folder names ${missing.length} file(s) the Runtime does not hold; upload each with PUT /v1/files/sha256:<hex> first: ${missing
370
+ .slice(0, 20)
371
+ .map((entry) => entry.path)
372
+ .join(", ")}`, { code: "definition_files_missing", details: { missing: missing.map((entry) => `sha256:${entry.sha256}`) } });
373
+ for (const entry of entries)
374
+ if (held.get(`sha256:${entry.sha256}`) !== entry.size)
375
+ fail(400, `${entry.path} is listed with ${entry.size} bytes, and the Runtime holds ${held.get(`sha256:${entry.sha256}`)} for its hash`);
376
+ // The files the compiler reads, each at most the text file limit (it reports a larger one).
377
+ const files = new Map();
378
+ for (const path of folderTextPaths(entries)) {
379
+ const entry = entries.find((item) => item.path === path);
380
+ if (entry.size > AGENT_FOLDER_LIMITS.textFileBytes)
381
+ continue;
382
+ const bytes = await readDefinitionFile(ctx.blobs, `sha256:${entry.sha256}`);
383
+ if (bytes)
384
+ files.set(path, bytes);
385
+ }
386
+ const compiled = compileAgentFolder({ entries, files, name });
387
+ if (!compiled.manifest) {
388
+ const errors = compiled.diagnostics.filter((item) => item.severity === "error");
389
+ fail(422, `Agent folder ${name} does not compile: ${errors.length} error(s), the first: ${located(errors[0])}`, {
390
+ code: "folder_invalid",
391
+ details: { diagnostics: compiled.diagnostics },
392
+ });
393
+ }
394
+ const source = {
395
+ compiler: compiled.compiler,
396
+ files: entries.length,
397
+ bytes: entries.reduce((sum, entry) => sum + entry.size, 0),
398
+ entries: entries.map((entry) => ({ path: entry.path, size: entry.size, sha256: entry.sha256 })),
399
+ };
400
+ return saveManifest(ctx, name, compiled.manifest, scope, options, {
401
+ source,
402
+ diagnostics: compiled.diagnostics,
403
+ compiler: compiled.compiler,
304
404
  });
305
405
  }
406
+ /** A diagnostic as a person reads it: `file:line:column message`. */
407
+ function located(diagnostic) {
408
+ if (!diagnostic)
409
+ return "unknown";
410
+ const where = diagnostic.file
411
+ ? `${diagnostic.file}${diagnostic.line ? `:${diagnostic.line}${diagnostic.column ? `:${diagnostic.column}` : ""}` : ""} `
412
+ : "";
413
+ return `${where}${diagnostic.message}`;
414
+ }
415
+ /** The source of version `version` of manifest `name`, or a `404` for one saved as JSON. */
416
+ async function sourceOf(ctx, name, version) {
417
+ const row = (await ctx.store.tx((t) => t.agentManifest(name, version))) ?? notFound(name, version);
418
+ if (!row.source)
419
+ fail(404, `Version ${version} of agent manifest ${name} was saved as JSON: it has no source.`, { code: "not_found" });
420
+ return row.source;
421
+ }
422
+ /** `GET /v1/agent-manifests/{name}/versions/{v}/source`: the folder a version was compiled from. */
423
+ export async function getAgentManifestSource(ctx, name, version) {
424
+ const source = await sourceOf(ctx, name, version);
425
+ return { name, version, ...source };
426
+ }
427
+ /** `GET .../versions/{v}/source/files/{path}`: one file of a version's source, streamed. */
428
+ export async function readAgentManifestSourceFile(ctx, name, version, path) {
429
+ const source = await sourceOf(ctx, name, version);
430
+ const entry = source.entries.find((item) => item.path === path);
431
+ if (!entry)
432
+ fail(404, `Version ${version} of agent manifest ${name} has no file ${path}.`, { code: "not_found" });
433
+ const body = await openDefinitionFile(ctx.blobs, `sha256:${entry.sha256}`);
434
+ if (!body)
435
+ fail(404, `The Runtime no longer holds ${path} of version ${version} of agent manifest ${name}.`, { code: "not_found" });
436
+ return { body: body, size: entry.size };
437
+ }
306
438
  /** `GET /v1/agent-manifests/{name}` (the latest) and `.../versions/{n}`. */
307
439
  export async function getAgentManifest(ctx, name, version) {
308
440
  return ctx.store.tx(async (t) => {
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const RUNTIME_VERSION = "0.30.0-beta";
1
+ export declare const RUNTIME_VERSION = "0.31.0-beta";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Kept in step with package.json by runtime/server/scripts/check-package.mjs, which runs in npm run check.
2
2
  // release:prepare also rewrites this when it bumps @nylorun/runtime.
3
- export const RUNTIME_VERSION = "0.30.0-beta";
3
+ export const RUNTIME_VERSION = "0.31.0-beta";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nylorun/runtime",
3
- "version": "0.30.0-beta",
3
+ "version": "0.31.0-beta",
4
4
  "description": "Nylorun Runtime: the agent execution Host on Postgres, Restate and S2, and model providers.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -65,8 +65,8 @@
65
65
  "@hono/node-server": "^2.1.5",
66
66
  "@hono/zod-openapi": "^1.6.3",
67
67
  "@modelcontextprotocol/sdk": "^1.32.1",
68
- "@nylorun/core": "0.26.0-beta",
69
- "@nylorun/harness": "0.29.2-beta",
68
+ "@nylorun/core": "0.27.0-beta",
69
+ "@nylorun/harness": "0.29.3-beta",
70
70
  "@restatedev/restate-sdk": "1.17.3",
71
71
  "@restatedev/restate-sdk-clients": "1.17.3",
72
72
  "@s2-dev/streamstore": "0.28.0",