@nylorun/runtime 0.30.0-beta → 0.32.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.
@@ -0,0 +1 @@
1
+ ALTER TABLE "nylorun"."agent_manifests" ADD COLUMN "source" json;
@@ -267,6 +267,13 @@
267
267
  "when": 1792238141315,
268
268
  "tag": "0037_model_usage_cost_split",
269
269
  "breakpoints": true
270
+ },
271
+ {
272
+ "idx": 38,
273
+ "version": "7",
274
+ "when": 1792484344984,
275
+ "tag": "0038_t6_agent_manifest_source",
276
+ "breakpoints": true
270
277
  }
271
278
  ]
272
279
  }
@@ -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 AgentManifestSourceDiff, 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,28 @@ 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
+ }>;
50
+ /**
51
+ * `GET /v1/agent-manifests/{name}/source/diff?from=&to=`: the files added, removed and changed
52
+ * (by SHA-256) from version `from`'s source to version `to`'s; a `404` when either was saved as JSON.
53
+ */
54
+ export declare function diffAgentManifestSources(ctx: TenantContext, name: string, from: number, to: number): Promise<AgentManifestSourceDiff>;
57
55
  /** `GET /v1/agent-manifests/{name}` (the latest) and `.../versions/{n}`. */
58
56
  export declare function getAgentManifest(ctx: TenantContext, name: string, version?: number): Promise<AgentManifestView>;
59
57
  /** `GET /v1/agent-manifests`: each manifest's latest version. */
@@ -21,7 +21,17 @@
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. What a folder may hold is the Tenant's Runtime
31
+ * limits `folderFiles`, `folderBytes` and `folderTextFileBytes` (track T7), which the compiler
32
+ * is given too; `source/diff` compares two versions' sources by path and hash.
24
33
  */
34
+ import { compileAgentFolder, folderTextPaths, } from "@nylorun/core/folder";
25
35
  import { definitionFilesOf, } from "@nylorun/core/contracts";
26
36
  import { hashManifest } from "@nylorun/core/compatibility";
27
37
  import { approvalRefusal, codeToolRefusal, harnessIdOf, loopManifestOf, runtimeOf, sectionGaps, } from "@nylorun/core/define";
@@ -35,7 +45,7 @@ import { readRuntimeLimits } from "./runtime-limits.js";
35
45
  import { effectiveSandboxConfig, readSandboxConfig } from "../sandbox/tenant-config.js";
36
46
  import { actorOf, fileErrors } from "./agents.js";
37
47
  import { moveAgent, planMove } from "./agent-versions.js";
38
- import { definitionFileIssues } from "./definition-files.js";
48
+ import { definitionFileIssues, openDefinitionFile, readDefinitionFile } from "./definition-files.js";
39
49
  import { fail } from "./http.js";
40
50
  /** What a manifest's runtime section asks of an Agent's recipe, with the harness its loop names. */
41
51
  export function manifestRecipeRequest(manifest) {
@@ -73,6 +83,9 @@ function viewOf(row, counts) {
73
83
  savedBy: row.savedBy,
74
84
  createdAt: row.createdAt,
75
85
  agents: counts.get(row.version) ?? 0,
86
+ ...(row.source === undefined
87
+ ? {}
88
+ : { source: { compiler: row.source.compiler, files: row.source.files, bytes: row.source.bytes } }),
76
89
  };
77
90
  }
78
91
  /** Each field that differs between `before` and `after`, by path; arrays compare whole. */
@@ -237,6 +250,9 @@ export async function saveAgentManifest(ctx, name, body, scope, options = {}) {
237
250
  const { requestId: _, ...manifest } = body;
238
251
  if (manifest.name !== name)
239
252
  fail(400, `The manifest is named ${manifest.name}, and the path names ${name}: they must match.`);
253
+ return saveManifest(ctx, name, manifest, scope, options);
254
+ }
255
+ async function saveManifest(ctx, name, manifest, scope, options, folder) {
240
256
  const hash = agentManifestHash(manifest);
241
257
  const gaps = sectionGaps(manifest);
242
258
  return ctx.store.tx(async (t) => {
@@ -248,12 +264,33 @@ export async function saveAgentManifest(ctx, name, body, scope, options = {}) {
248
264
  : `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
265
  const unchanged = latest?.hash === hash;
250
266
  const to = unchanged ? latest.version : (latest?.version ?? 0) + 1;
267
+ // A folder whose manifest is unchanged keeps the latest version, and with it that version's source.
268
+ const notes = folder === undefined
269
+ ? undefined
270
+ : unchanged && canonical(latest?.source?.entries ?? null) !== canonical(folder.source.entries)
271
+ ? [
272
+ ...folder.diagnostics,
273
+ {
274
+ severity: "note",
275
+ code: "source_not_saved",
276
+ message: "Source files changed that are not part of the agent; the manifest is unchanged, so no version is saved",
277
+ },
278
+ ]
279
+ : folder.diagnostics;
251
280
  const counts = await t.agentsOfManifest(name);
252
281
  const checked = await errorsOf(t, manifest);
253
282
  // The version as it would be saved, for the plan of the Agents it moves.
254
283
  const row = unchanged
255
284
  ? latest
256
- : { name, version: to, body: manifest, hash, savedBy: { actor: actorOf(scope) }, createdAt: new Date().toISOString() };
285
+ : {
286
+ name,
287
+ version: to,
288
+ body: manifest,
289
+ hash,
290
+ savedBy: { actor: actorOf(scope) },
291
+ createdAt: new Date().toISOString(),
292
+ ...(folder === undefined ? {} : { source: folder.source }),
293
+ };
257
294
  if (options.dryRun) {
258
295
  const errors = [...gaps, ...checked.errors];
259
296
  // A save with errors moves nothing; one that changes nothing moves no Agent either.
@@ -268,6 +305,7 @@ export async function saveAgentManifest(ctx, name, body, scope, options = {}) {
268
305
  errors,
269
306
  behind: moves?.behind ?? (await agentsBehind(t, name, to)).length,
270
307
  moves: { now: moves?.now ?? 0, nextStart: moves?.nextStart ?? 0, secrets: moves?.secrets ?? 0 },
308
+ ...(folder === undefined ? {} : { manifest, diagnostics: [...notes], compiler: folder.compiler }),
271
309
  };
272
310
  }
273
311
  if (gaps.length > 0)
@@ -287,8 +325,9 @@ export async function saveAgentManifest(ctx, name, body, scope, options = {}) {
287
325
  });
288
326
  if (checked.errors.length > 0)
289
327
  fail(400, checked.errors.join(" "));
328
+ const folderNotes = notes === undefined ? {} : { diagnostics: [...notes] };
290
329
  if (unchanged)
291
- return { manifest: viewOf(latest, counts), unchanged: true, warnings: warningsOf(ctx, manifest, to, undefined) };
330
+ return { manifest: viewOf(latest, counts), unchanged: true, warnings: warningsOf(ctx, manifest, to, undefined), ...folderNotes };
292
331
  await t.insertAgentManifest(row);
293
332
  // Its files by hash, skills' and runtime files' alike, for the sweep that deletes unused ones.
294
333
  const shas = new Set(definitionFilesOf(loopManifestOf(manifest)));
@@ -296,13 +335,148 @@ export async function saveAgentManifest(ctx, name, body, scope, options = {}) {
296
335
  if ("files" in file)
297
336
  for (const sha of Object.values(file.files))
298
337
  shas.add(sha);
338
+ // A folder's every file too: the version keeps its source.
339
+ for (const entry of folder?.source.entries ?? [])
340
+ shas.add(`sha256:${entry.sha256}`);
299
341
  if (shas.size > 0)
300
342
  await t.insertDefinitionFileUses(manifestFileOwner(name), hash, [...shas]);
301
343
  // Every Agent that tracks the manifest moves to the new version (track T3).
302
344
  const moves = await moveTracking(t, row, true);
303
- return { manifest: viewOf(row, await t.agentsOfManifest(name)), unchanged: false, warnings: warningsOf(ctx, manifest, to, moves) };
345
+ return {
346
+ manifest: viewOf(row, await t.agentsOfManifest(name)),
347
+ unchanged: false,
348
+ warnings: warningsOf(ctx, manifest, to, moves),
349
+ ...folderNotes,
350
+ };
304
351
  });
305
352
  }
353
+ /**
354
+ * `PUT /v1/agent-manifests/{name}` with an agent folder by hash (track T6): reads the files the
355
+ * compiler needs from the definition file store, compiles the folder (`422 folder_invalid` when
356
+ * it does not), then saves or plans the manifest as a JSON save does, keeping the folder as the
357
+ * version's source.
358
+ */
359
+ export async function saveAgentFolder(ctx, name, body, scope, options = {}) {
360
+ const entries = [...body.entries].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
361
+ const limits = folderLimitsOf(await ctx.store.tx((t) => readRuntimeLimits(t)));
362
+ if (entries.length > limits.files)
363
+ fail(413, `An agent folder holds at most ${limits.files} files (the Runtime limit folderFiles); this one holds ${entries.length}`, {
364
+ code: "limit_exceeded",
365
+ details: { limit: "folderFiles", value: limits.files },
366
+ });
367
+ const bytes = entries.reduce((sum, entry) => sum + entry.size, 0);
368
+ if (bytes > limits.totalBytes)
369
+ fail(413, `An agent folder holds at most ${limits.totalBytes} bytes (the Runtime limit folderBytes); this one holds ${bytes}`, {
370
+ code: "limit_exceeded",
371
+ details: { limit: "folderBytes", value: limits.totalBytes },
372
+ });
373
+ // Every file must be held: the version keeps them all as its source.
374
+ const shas = [...new Set(entries.map((entry) => `sha256:${entry.sha256}`))].sort();
375
+ const held = await ctx.store.tx((t) => t.heldDefinitionFiles(shas));
376
+ const missing = entries.filter((entry) => !held.has(`sha256:${entry.sha256}`));
377
+ if (missing.length > 0)
378
+ fail(400, `The folder names ${missing.length} file(s) the Runtime does not hold; upload each with PUT /v1/files/sha256:<hex> first: ${missing
379
+ .slice(0, 20)
380
+ .map((entry) => entry.path)
381
+ .join(", ")}`, { code: "definition_files_missing", details: { missing: missing.map((entry) => `sha256:${entry.sha256}`) } });
382
+ for (const entry of entries)
383
+ if (held.get(`sha256:${entry.sha256}`) !== entry.size)
384
+ fail(400, `${entry.path} is listed with ${entry.size} bytes, and the Runtime holds ${held.get(`sha256:${entry.sha256}`)} for its hash`);
385
+ // The files the compiler reads, each at most the text file limit (it reports a larger one).
386
+ const files = new Map();
387
+ for (const path of folderTextPaths(entries)) {
388
+ const entry = entries.find((item) => item.path === path);
389
+ if (entry.size > limits.textFileBytes)
390
+ continue;
391
+ const bytes = await readDefinitionFile(ctx.blobs, `sha256:${entry.sha256}`);
392
+ if (bytes)
393
+ files.set(path, bytes);
394
+ }
395
+ const compiled = compileAgentFolder({ entries, files, name, limits });
396
+ if (!compiled.manifest) {
397
+ const errors = compiled.diagnostics.filter((item) => item.severity === "error");
398
+ fail(422, `Agent folder ${name} does not compile: ${errors.length} error(s), the first: ${located(errors[0])}`, {
399
+ code: "folder_invalid",
400
+ details: { diagnostics: compiled.diagnostics },
401
+ });
402
+ }
403
+ const source = {
404
+ compiler: compiled.compiler,
405
+ files: entries.length,
406
+ bytes,
407
+ entries: entries.map((entry) => ({ path: entry.path, size: entry.size, sha256: entry.sha256 })),
408
+ };
409
+ return saveManifest(ctx, name, compiled.manifest, scope, options, {
410
+ source,
411
+ diagnostics: compiled.diagnostics,
412
+ compiler: compiled.compiler,
413
+ });
414
+ }
415
+ /** What a folder may hold: the Tenant's Runtime limits, as the compiler takes them. */
416
+ function folderLimitsOf(limits) {
417
+ return { files: limits.folderFiles, totalBytes: limits.folderBytes, textFileBytes: limits.folderTextFileBytes };
418
+ }
419
+ /** A diagnostic as a person reads it: `file:line:column message`. */
420
+ function located(diagnostic) {
421
+ if (!diagnostic)
422
+ return "unknown";
423
+ const where = diagnostic.file
424
+ ? `${diagnostic.file}${diagnostic.line ? `:${diagnostic.line}${diagnostic.column ? `:${diagnostic.column}` : ""}` : ""} `
425
+ : "";
426
+ return `${where}${diagnostic.message}`;
427
+ }
428
+ /** The source of version `version` of manifest `name`, or a `404` for one saved as JSON. */
429
+ async function sourceOf(ctx, name, version) {
430
+ const row = (await ctx.store.tx((t) => t.agentManifest(name, version))) ?? notFound(name, version);
431
+ if (!row.source)
432
+ fail(404, `Version ${version} of agent manifest ${name} was saved as JSON: it has no source.`, { code: "not_found" });
433
+ return row.source;
434
+ }
435
+ /** `GET /v1/agent-manifests/{name}/versions/{v}/source`: the folder a version was compiled from. */
436
+ export async function getAgentManifestSource(ctx, name, version) {
437
+ const source = await sourceOf(ctx, name, version);
438
+ return { name, version, ...source };
439
+ }
440
+ /** `GET .../versions/{v}/source/files/{path}`: one file of a version's source, streamed. */
441
+ export async function readAgentManifestSourceFile(ctx, name, version, path) {
442
+ const source = await sourceOf(ctx, name, version);
443
+ const entry = source.entries.find((item) => item.path === path);
444
+ if (!entry)
445
+ fail(404, `Version ${version} of agent manifest ${name} has no file ${path}.`, { code: "not_found" });
446
+ const body = await openDefinitionFile(ctx.blobs, `sha256:${entry.sha256}`);
447
+ if (!body)
448
+ fail(404, `The Runtime no longer holds ${path} of version ${version} of agent manifest ${name}.`, { code: "not_found" });
449
+ return { body: body, size: entry.size };
450
+ }
451
+ /**
452
+ * `GET /v1/agent-manifests/{name}/source/diff?from=&to=`: the files added, removed and changed
453
+ * (by SHA-256) from version `from`'s source to version `to`'s; a `404` when either was saved as JSON.
454
+ */
455
+ export async function diffAgentManifestSources(ctx, name, from, to) {
456
+ const before = new Map((await sourceOf(ctx, name, from)).entries.map((entry) => [entry.path, entry]));
457
+ const after = (await sourceOf(ctx, name, to)).entries;
458
+ const diff = { name, from, to, added: [], removed: [], changed: [], unchanged: 0 };
459
+ for (const entry of after) {
460
+ const was = before.get(entry.path);
461
+ before.delete(entry.path);
462
+ if (!was)
463
+ diff.added.push(entry);
464
+ else if (was.sha256 === entry.sha256)
465
+ diff.unchanged++;
466
+ else
467
+ diff.changed.push({
468
+ path: entry.path,
469
+ from: { size: was.size, sha256: was.sha256 },
470
+ to: { size: entry.size, sha256: entry.sha256 },
471
+ });
472
+ }
473
+ diff.removed.push(...before.values());
474
+ const byPath = (a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
475
+ diff.added.sort(byPath);
476
+ diff.removed.sort(byPath);
477
+ diff.changed.sort(byPath);
478
+ return diff;
479
+ }
306
480
  /** `GET /v1/agent-manifests/{name}` (the latest) and `.../versions/{n}`. */
307
481
  export async function getAgentManifest(ctx, name, version) {
308
482
  return ctx.store.tx(async (t) => {
@@ -17,10 +17,11 @@ export declare function liveEnvironment(t: Tx, id: string, options?: {
17
17
  * An Agent's state (protocol 16), from its desired state, its compute and its sessions, and
18
18
  * every state that holds with the session behind it (`attention`), highest first. A session waits
19
19
  * on a person while it is paused (an approval or an interaction) or uncertain, and the Agent while
20
- * a move waits for its required secrets (track T3); a session's last turn
21
- * failed while it is `failed`; a turn runs while it is `running`, `runnable` or `waiting`.
20
+ * a move waits for its required secrets (track T3), or a required secret of the version it runs
21
+ * has no value (`unsetSecrets`, track T7: a tool would fail when it is called); a session's last
22
+ * turn failed while it is `failed`; a turn runs while it is `running`, `runnable` or `waiting`.
22
23
  */
23
- export declare function stateOf(agent: Pick<Environment, "desiredState" | "pod"> & Partial<Pick<Environment, "targetVersion" | "appliedVersion" | "version" | "failedVersion">>, sessions: readonly Pick<Session, "id" | "status">[]): {
24
+ export declare function stateOf(agent: Pick<Environment, "desiredState" | "pod"> & Partial<Pick<Environment, "targetVersion" | "appliedVersion" | "version" | "failedVersion">>, sessions: readonly Pick<Session, "id" | "status">[], unsetSecrets?: readonly string[]): {
24
25
  state: AgentState;
25
26
  attention: AgentAttention[];
26
27
  };
@@ -22,7 +22,8 @@
22
22
  * the Agent's id).
23
23
  * - **State on read.** `state` is derived when it is read, the first rule that holds: `paused`
24
24
  * (desired `off`, track T6), `failing` (its compute failed or was lost, or a session's last
25
- * turn failed), `input_required` (a session waits on a person), `working` (a turn runs),
25
+ * turn failed), `input_required` (a session waits on a person, or a required secret is unset),
26
+ * `working` (a turn runs),
26
27
  * `sleeping`; `attention` names the sessions behind each that holds (`stateOf`).
27
28
  * - **Secret slots** (D64): when its manifest declares `secrets`, a secret write sends only the
28
29
  * value, the binding comes from the slot, and a name it does not declare is
@@ -110,16 +111,20 @@ const STATE_RANK = ["paused", "failing", "input_required", "working", "sleeping"
110
111
  * An Agent's state (protocol 16), from its desired state, its compute and its sessions, and
111
112
  * every state that holds with the session behind it (`attention`), highest first. A session waits
112
113
  * on a person while it is paused (an approval or an interaction) or uncertain, and the Agent while
113
- * a move waits for its required secrets (track T3); a session's last turn
114
- * failed while it is `failed`; a turn runs while it is `running`, `runnable` or `waiting`.
114
+ * a move waits for its required secrets (track T3), or a required secret of the version it runs
115
+ * has no value (`unsetSecrets`, track T7: a tool would fail when it is called); a session's last
116
+ * turn failed while it is `failed`; a turn runs while it is `running`, `runnable` or `waiting`.
115
117
  */
116
- export function stateOf(agent, sessions) {
118
+ export function stateOf(agent, sessions, unsetSecrets = []) {
117
119
  const attention = [];
118
120
  if (agent.desiredState === "off")
119
121
  attention.push({ state: "paused", reason: "desired off" });
120
122
  // A move that waits for a person to set its required secrets (track T3).
121
123
  if (agent.targetVersion !== undefined && agent.appliedVersion === agent.version)
122
124
  attention.push({ state: "input_required", reason: `secret_required: version ${agent.targetVersion} waits for its required secrets` });
125
+ // A required secret of the version it runs, unset (track T7).
126
+ for (const name of unsetSecrets)
127
+ attention.push({ state: "input_required", reason: `secret_unset: required secret ${name} unset` });
123
128
  const pod = agent.pod;
124
129
  if (pod && (pod.observed === "failed" || pod.observed === "lost") && pod.desired !== "deleted")
125
130
  attention.push({ state: "failing", reason: `compute ${pod.observed}${pod.reason ? `: ${pod.reason}` : ""}` });
@@ -152,7 +157,7 @@ async function viewOf(ctx, t, environment, owner) {
152
157
  : undefined;
153
158
  const pod = environment.pod;
154
159
  const visible = sessions.filter((session) => owner === undefined || session.ownerUserId === owner);
155
- const { state, attention } = stateOf(environment, visible);
160
+ const { state, attention } = stateOf(environment, visible, await unsetRequiredSecrets(t, environment));
156
161
  return {
157
162
  id: environment.id,
158
163
  manifest: manifestRefOf(environment),
@@ -230,6 +235,22 @@ async function slotsAhead(t, environment) {
230
235
  const target = await t.agentManifest(environment.manifestName, environment.targetVersion);
231
236
  return target ? runtimeOf(target.body).secrets : environment.applied.secrets;
232
237
  }
238
+ /**
239
+ * The required secret slots of the version `environment` runs (its applied runtime) that its vault
240
+ * holds no value for, by name: the Agent's attention (track T7), before a tool fails for want of
241
+ * one. A move waiting on the next version's secrets is its own attention item (`stateOf`).
242
+ */
243
+ async function unsetRequiredSecrets(t, environment) {
244
+ const required = Object.entries(environment.applied.secrets ?? {}).filter(([, slot]) => slot.required === true);
245
+ if (required.length === 0)
246
+ return [];
247
+ const vault = await t.vaultOfEnvironment(environment.id);
248
+ const rows = vault ? await t.credentialsForVault(vault.id) : [];
249
+ return required
250
+ .filter(([name, slot]) => !rows.some((row) => row.name === name && slotHolds(slot, row)))
251
+ .map(([name]) => name)
252
+ .sort();
253
+ }
233
254
  /** Each secret slot `environment` declares (`slotsAhead`), and whether its vault holds a value that fills it. */
234
255
  async function slotsOf(t, environment) {
235
256
  const declared = await slotsAhead(t, environment);
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.32.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.32.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.32.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.28.0-beta",
69
+ "@nylorun/harness": "0.29.4-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",