@nylorun/runtime 0.27.0-beta → 0.29.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.
Files changed (160) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +1 -1
  3. package/dist/api/a2a/card.d.ts +4 -3
  4. package/dist/api/a2a/card.js +3 -3
  5. package/dist/api/a2a/routes.js +18 -7
  6. package/dist/api/ag-ui/routes.js +4 -7
  7. package/dist/api/components.d.ts +5854 -2718
  8. package/dist/api/components.js +39 -30
  9. package/dist/api/http/app.js +9 -4
  10. package/dist/api/http/body.d.ts +2 -0
  11. package/dist/api/http/body.js +20 -0
  12. package/dist/api/http/define.d.ts +4 -0
  13. package/dist/api/http/define.js +20 -9
  14. package/dist/api/http/routes/agent-manifests.d.ts +10 -0
  15. package/dist/api/http/routes/agent-manifests.js +135 -0
  16. package/dist/api/http/routes/agents.d.ts +11 -0
  17. package/dist/api/http/routes/agents.js +418 -0
  18. package/dist/api/http/routes/artifacts.js +2 -2
  19. package/dist/api/http/routes/coverage.d.ts +4 -4
  20. package/dist/api/http/routes/coverage.js +12 -6
  21. package/dist/api/http/routes/files.js +1 -1
  22. package/dist/api/http/routes/mcp.js +2 -2
  23. package/dist/api/http/routes/me.d.ts +1 -1
  24. package/dist/api/http/routes/me.js +1 -2
  25. package/dist/api/http/routes/memories.d.ts +9 -0
  26. package/dist/api/http/routes/memories.js +152 -0
  27. package/dist/api/http/routes/reads.d.ts +1 -137
  28. package/dist/api/http/routes/reads.js +31 -12
  29. package/dist/api/http/routes/sessions.js +21 -50
  30. package/dist/api/http/routes/tenant.js +13 -10
  31. package/dist/api/http/routes/triggers.d.ts +10 -0
  32. package/dist/api/http/routes/triggers.js +183 -0
  33. package/dist/api/openapi.js +69 -47
  34. package/dist/artifacts/disk.d.ts +10 -10
  35. package/dist/artifacts/disk.js +27 -27
  36. package/dist/artifacts/quota.d.ts +1 -1
  37. package/dist/artifacts/quota.js +2 -2
  38. package/dist/artifacts/service.js +2 -2
  39. package/dist/core/flow-host.d.ts +0 -1
  40. package/dist/gates/egress-credentials.js +1 -1
  41. package/dist/gates/egress.d.ts +5 -5
  42. package/dist/gates/egress.js +6 -6
  43. package/dist/gates/http-tool.js +2 -2
  44. package/dist/gates/meter.js +22 -5
  45. package/dist/gates/policy.d.ts +8 -3
  46. package/dist/gates/policy.js +7 -2
  47. package/dist/gates/tenant-vaults.d.ts +1 -1
  48. package/dist/harness/pod.d.ts +2 -0
  49. package/dist/harness/setup.js +17 -6
  50. package/dist/harness-api/record.js +11 -0
  51. package/dist/harness-api/server.js +4 -0
  52. package/dist/harness-api/start.d.ts +11 -7
  53. package/dist/harness-api/start.js +49 -5
  54. package/dist/harness-api/workspace.js +2 -2
  55. package/dist/host/config-for.d.ts +2 -0
  56. package/dist/host/config-for.js +1 -0
  57. package/dist/host/http.d.ts +2 -2
  58. package/dist/host/http.js +5 -2
  59. package/dist/host/main.js +1 -0
  60. package/dist/keys/keys.d.ts +15 -4
  61. package/dist/keys/keys.js +16 -0
  62. package/dist/keys/webhooks.d.ts +25 -0
  63. package/dist/keys/webhooks.js +62 -0
  64. package/dist/management-openapi.json +313 -133
  65. package/dist/mcp/connect.d.ts +2 -2
  66. package/dist/mcp/connect.js +3 -3
  67. package/dist/mcp/preview.js +7 -7
  68. package/dist/memory/service.d.ts +97 -0
  69. package/dist/memory/service.js +335 -0
  70. package/dist/memory/tool.d.ts +36 -0
  71. package/dist/memory/tool.js +84 -0
  72. package/dist/model/pi-model.js +7 -0
  73. package/dist/openapi.json +13120 -9034
  74. package/dist/reads/types.d.ts +3 -3
  75. package/dist/record/agent.d.ts +27 -0
  76. package/dist/record/agent.js +46 -0
  77. package/dist/record/index.d.ts +1 -1
  78. package/dist/record/index.js +1 -1
  79. package/dist/sandbox/join.js +5 -5
  80. package/dist/sandbox/placement.d.ts +1 -1
  81. package/dist/sandbox/placement.js +3 -3
  82. package/dist/sandbox/pods/lifecycle.d.ts +6 -6
  83. package/dist/sandbox/pods/lifecycle.js +6 -6
  84. package/dist/sandbox/pods/reconcile.js +10 -2
  85. package/dist/sandbox/resolve.d.ts +3 -3
  86. package/dist/sandbox/resolve.js +7 -7
  87. package/dist/sandbox/setup.d.ts +3 -1
  88. package/dist/sandbox/setup.js +14 -4
  89. package/dist/store/postgres/drizzle/0032_agents.sql +54 -0
  90. package/dist/store/postgres/drizzle/0033_agent_versions.sql +7 -0
  91. package/dist/store/postgres/drizzle/0034_protocol_17_session_queue.sql +13 -0
  92. package/dist/store/postgres/drizzle/0035_t4_triggers.sql +41 -0
  93. package/dist/store/postgres/drizzle/0036_t5_memories.sql +30 -0
  94. package/dist/store/postgres/drizzle/0037_model_usage_cost_split.sql +4 -0
  95. package/dist/store/postgres/drizzle/meta/_journal.json +42 -0
  96. package/dist/store/postgres/reads.js +44 -87
  97. package/dist/store/postgres/record-writer.d.ts +4 -4
  98. package/dist/store/postgres/record-writer.js +9 -9
  99. package/dist/store/postgres/schema.d.ts +1408 -123
  100. package/dist/store/postgres/schema.js +194 -38
  101. package/dist/store/postgres/store.js +329 -79
  102. package/dist/store/postgres/tenant.js +1 -2
  103. package/dist/store/types.d.ts +155 -50
  104. package/dist/store/types.js +0 -1
  105. package/dist/tenant/advance.js +20 -8
  106. package/dist/tenant/agent-manifests.d.ts +72 -0
  107. package/dist/tenant/agent-manifests.js +359 -0
  108. package/dist/tenant/agent-versions.d.ts +126 -0
  109. package/dist/tenant/agent-versions.js +389 -0
  110. package/dist/tenant/agents.d.ts +230 -0
  111. package/dist/tenant/{environments.js → agents.js} +464 -320
  112. package/dist/tenant/auth.d.ts +1 -1
  113. package/dist/tenant/auth.js +3 -3
  114. package/dist/tenant/commands.d.ts +63 -2
  115. package/dist/tenant/commands.js +240 -83
  116. package/dist/tenant/context.d.ts +46 -19
  117. package/dist/tenant/context.js +6 -2
  118. package/dist/tenant/effects.js +7 -16
  119. package/dist/tenant/identity-file.d.ts +6 -6
  120. package/dist/tenant/identity-file.js +12 -10
  121. package/dist/tenant/issuers.js +3 -3
  122. package/dist/tenant/limits.d.ts +0 -3
  123. package/dist/tenant/limits.js +0 -3
  124. package/dist/tenant/memories.d.ts +38 -0
  125. package/dist/tenant/memories.js +119 -0
  126. package/dist/tenant/reset.js +2 -2
  127. package/dist/tenant/run-grants.d.ts +6 -0
  128. package/dist/tenant/run-grants.js +4 -0
  129. package/dist/tenant/runtime.d.ts +1 -1
  130. package/dist/tenant/runtime.js +8 -0
  131. package/dist/tenant/sessions.d.ts +28 -45
  132. package/dist/tenant/sessions.js +85 -208
  133. package/dist/tenant/status.js +6 -7
  134. package/dist/tenant/sweep.js +12 -3
  135. package/dist/tenant/transcript.d.ts +16 -16
  136. package/dist/tenant/triggers.d.ts +143 -0
  137. package/dist/tenant/triggers.js +715 -0
  138. package/dist/tenant/turn-timing.d.ts +11 -1
  139. package/dist/tenant/turn-timing.js +3 -1
  140. package/dist/tenant/types.d.ts +5 -0
  141. package/dist/tenant/worker.d.ts +2 -0
  142. package/dist/tenant/worker.js +5 -0
  143. package/dist/traces/map.d.ts +1 -1
  144. package/dist/traces/map.js +7 -1
  145. package/dist/tuning.d.ts +7 -0
  146. package/dist/tuning.js +19 -0
  147. package/dist/vault/service.d.ts +13 -8
  148. package/dist/vault/service.js +61 -17
  149. package/dist/version.d.ts +1 -1
  150. package/dist/version.js +1 -1
  151. package/package.json +13 -12
  152. package/dist/api/http/routes/environment-manifests.d.ts +0 -9
  153. package/dist/api/http/routes/environment-manifests.js +0 -118
  154. package/dist/api/http/routes/environments.d.ts +0 -11
  155. package/dist/api/http/routes/environments.js +0 -332
  156. package/dist/record/environment.d.ts +0 -27
  157. package/dist/record/environment.js +0 -46
  158. package/dist/tenant/environment-manifests.d.ts +0 -57
  159. package/dist/tenant/environment-manifests.js +0 -208
  160. package/dist/tenant/environments.d.ts +0 -172
@@ -0,0 +1,359 @@
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 { definitionFilesOf, } from "@nylorun/core/contracts";
26
+ import { hashManifest } from "@nylorun/core/compatibility";
27
+ import { approvalRefusal, codeToolRefusal, harnessIdOf, loopManifestOf, runtimeOf, sectionGaps, } from "@nylorun/core/define";
28
+ import { canonical } from "../store/canonical.js";
29
+ import { resolveRecipe } from "../sandbox/resolve.js";
30
+ import { declaredSandboxes } from "../sandbox/share.js";
31
+ import { httpToolTimeoutIssues } from "../gates/http-tool.js";
32
+ import { limitIssues, limitsInForce, manifestShapeIssues, readTenantLimits } from "./limits.js";
33
+ import { triggerIssues } from "./triggers.js";
34
+ import { readRuntimeLimits } from "./runtime-limits.js";
35
+ import { effectiveSandboxConfig, readSandboxConfig } from "../sandbox/tenant-config.js";
36
+ import { actorOf, fileErrors } from "./agents.js";
37
+ import { moveAgent, planMove } from "./agent-versions.js";
38
+ import { definitionFileIssues } from "./definition-files.js";
39
+ import { fail } from "./http.js";
40
+ /** What a manifest's runtime section asks of an Agent's recipe, with the harness its loop names. */
41
+ export function manifestRecipeRequest(manifest) {
42
+ const runtime = runtimeOf(manifest);
43
+ return {
44
+ ...(runtime.image === undefined ? {} : { image: runtime.image }),
45
+ ...(runtime.resources === undefined ? {} : { resources: runtime.resources }),
46
+ ...(runtime.storage === undefined ? {} : { storage: runtime.storage }),
47
+ ...(runtime.network === undefined ? {} : { network: runtime.network }),
48
+ harness: harnessIdOf(manifest.harness),
49
+ ...(runtime.lifecycle === undefined ? {} : { lifecycle: runtime.lifecycle }),
50
+ ...(runtime.setup === undefined ? {} : { setup: runtime.setup }),
51
+ ...(runtime.files === undefined ? {} : { files: runtime.files }),
52
+ ...(runtime.limits === undefined ? {} : { limits: runtime.limits }),
53
+ ...(runtime.secrets === undefined ? {} : { secrets: runtime.secrets }),
54
+ };
55
+ }
56
+ /** The hash a manifest's version records: equal manifests hash the same. */
57
+ export function agentManifestHash(manifest) {
58
+ return hashManifest(manifest);
59
+ }
60
+ /** The owner a version's definition files are recorded under, for the sweep. */
61
+ export function manifestFileOwner(name) {
62
+ return `agent-manifest:${name}`;
63
+ }
64
+ const notFound = (name, version) => fail(404, version === undefined ? `No agent manifest is named ${name}.` : `Agent manifest ${name} has no version ${version}.`, {
65
+ code: "manifest_not_found",
66
+ });
67
+ function viewOf(row, counts) {
68
+ return {
69
+ name: row.name,
70
+ version: row.version,
71
+ hash: row.hash,
72
+ manifest: row.body,
73
+ savedBy: row.savedBy,
74
+ createdAt: row.createdAt,
75
+ agents: counts.get(row.version) ?? 0,
76
+ };
77
+ }
78
+ /** Each field that differs between `before` and `after`, by path; arrays compare whole. */
79
+ export function manifestChanges(before, after, path = "") {
80
+ const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
81
+ if (object(before) && object(after)) {
82
+ const keys = [...new Set([...Object.keys(before), ...Object.keys(after)])].sort();
83
+ return keys.flatMap((key) => manifestChanges(before[key], after[key], path ? `${path}.${key}` : key));
84
+ }
85
+ if (canonical(before) === canonical(after))
86
+ return [];
87
+ return [
88
+ {
89
+ path,
90
+ ...(before === undefined ? {} : { from: before }),
91
+ ...(after === undefined ? {} : { to: after }),
92
+ },
93
+ ];
94
+ }
95
+ /**
96
+ * The changes from `before` to `after` by section: `manifest` for the top-level fields, else
97
+ * `harness` or `runtime` with the path inside it. A section added or removed whole is one change.
98
+ */
99
+ export function sectionChanges(before, after) {
100
+ const op = (from, to) => from === undefined ? "add" : to === undefined ? "remove" : "replace";
101
+ const changes = [];
102
+ const { harness: beforeHarness, runtime: beforeRuntime, ...beforeTop } = before ?? {};
103
+ const { harness, runtime, ...top } = after;
104
+ for (const change of manifestChanges(beforeTop, top))
105
+ changes.push({ section: "manifest", ...change, op: op(change.from, change.to) });
106
+ for (const [section, from, to] of [
107
+ ["harness", beforeHarness, harness],
108
+ ["runtime", beforeRuntime, runtime],
109
+ ]) {
110
+ if (from === undefined || to === undefined) {
111
+ if (from !== to)
112
+ changes.push({
113
+ section,
114
+ path: "",
115
+ op: op(from, to),
116
+ ...(from === undefined ? {} : { from }),
117
+ ...(to === undefined ? {} : { to }),
118
+ });
119
+ continue;
120
+ }
121
+ for (const change of manifestChanges(from, to))
122
+ changes.push({ section, ...change, op: op(change.from, change.to) });
123
+ }
124
+ return changes;
125
+ }
126
+ /**
127
+ * Each live Agent of manifest `name` on another version than `to`, and whether a save of `to`
128
+ * moves it: it tracks the manifest, is not moving to `to` already, and `to` did not fail to start on it.
129
+ */
130
+ async function agentsBehind(t, name, to) {
131
+ return (await t.listEnvironments())
132
+ .filter((agent) => agent.manifestName === name && agent.pod?.desired !== "deleted" && agent.manifestVersion !== to)
133
+ .map((agent) => ({
134
+ agent,
135
+ moves: agent.manifestRef === "tracking" && agent.targetVersion !== to && agent.failedVersion !== to,
136
+ }));
137
+ }
138
+ /**
139
+ * Plans (or with `apply`, makes) the move of each Agent that tracks manifest `version.name` to
140
+ * `version` (track T3): each under its row lock, in the save's transaction.
141
+ */
142
+ async function moveTracking(t, version, apply) {
143
+ const moves = { now: 0, nextStart: 0, secrets: 0, behind: 0 };
144
+ const now = new Date().toISOString();
145
+ for (const { agent: listed, moves: tracks } of await agentsBehind(t, version.name, version.version)) {
146
+ const agent = tracks && apply ? await t.environment(listed.id, { lock: true }) : listed;
147
+ if (!agent || !tracks) {
148
+ if (agent && agent.targetVersion !== version.version)
149
+ moves.behind++;
150
+ continue;
151
+ }
152
+ const move = await planMove(t, agent, version, "tracking");
153
+ if (move.plan.applies === "refused") {
154
+ moves.behind++;
155
+ continue;
156
+ }
157
+ const applies = apply ? await moveAgent(t, agent, move, now) : move.plan.applies;
158
+ if (applies === "now")
159
+ moves.now++;
160
+ else if (applies === "next_start")
161
+ moves.nextStart++;
162
+ else if (applies === "secrets")
163
+ moves.secrets++;
164
+ }
165
+ return moves;
166
+ }
167
+ /** What a save of `manifest` should tell its saver, though it would be saved. */
168
+ function warningsOf(ctx, manifest, to, moves) {
169
+ const warnings = [];
170
+ const runtime = runtimeOf(manifest);
171
+ if (runtime.image !== undefined && !runtime.image.includes("@sha256:"))
172
+ warnings.push({
173
+ code: "image_tag",
174
+ message: `runtime.image ${runtime.image} is a tag, not a digest: each Agent pins what its first pod pulls, so two Tenants may run different bytes.`,
175
+ });
176
+ if (runtime.kind === "pod" && !ctx.pods)
177
+ warnings.push({
178
+ code: "pods_unavailable",
179
+ message: "runtime.kind pod needs sandbox pods, which this Tenant does not have: an Agent made from it is refused until `nylorun local sandbox enable`.",
180
+ });
181
+ if (moves === undefined)
182
+ return warnings;
183
+ const moved = moves.now + moves.nextStart + moves.secrets;
184
+ if (moved > 0) {
185
+ const parts = [
186
+ ...(moves.now > 0 ? [`${moves.now} at once`] : []),
187
+ ...(moves.nextStart > 0 ? [`${moves.nextStart} at their pod's next start`] : []),
188
+ ...(moves.secrets > 0 ? [`${moves.secrets} once their required secrets are set`] : []),
189
+ ];
190
+ warnings.push({
191
+ code: "agents_move",
192
+ message: `${moved} Agent${moved === 1 ? " tracks" : "s track"} ${manifest.name} and move${moved === 1 ? "s" : ""} to version ${to}: ${parts.join(", ")}.`,
193
+ });
194
+ }
195
+ if (moves.behind > 0)
196
+ warnings.push({
197
+ code: "agents_behind",
198
+ message: `${moves.behind} Agent${moves.behind === 1 ? " stays" : "s stay"} on an older version of ${manifest.name}: pinned, or ${moves.behind === 1 ? "it" : "they"} cannot move to version ${to} (move or fork ${moves.behind === 1 ? "it" : "them"}).`,
199
+ });
200
+ return warnings;
201
+ }
202
+ /**
203
+ * Every reason the Runtime would refuse `manifest` now, apart from the gaps between its sections:
204
+ * its harness section's own checks and its runtime section's (the Tenant's limits, its files).
205
+ */
206
+ async function errorsOf(t, manifest) {
207
+ const loop = loopManifestOf(manifest);
208
+ const errors = [];
209
+ // The Runtime runs agents from their manifests alone (track R2): no tool may need it.
210
+ const code = codeToolRefusal(loop);
211
+ if (code)
212
+ errors.push(code);
213
+ // Tool approvals are the Tenant's tool policy, decided at the Tool Gate (R5 step 2).
214
+ const approval = approvalRefusal(loop);
215
+ if (approval)
216
+ errors.push(approval);
217
+ const declared = declaredSandboxes(loop);
218
+ if (declared.length > 0)
219
+ errors.push(`${declared.join(", ")} declares a sandbox: what an agent runs on is the manifest's runtime section.`);
220
+ const runtimeLimits = await readRuntimeLimits(t);
221
+ errors.push(...httpToolTimeoutIssues(loop, runtimeLimits.httpToolTimeoutMs));
222
+ const runtime = runtimeOf(manifest);
223
+ const config = effectiveSandboxConfig(await readSandboxConfig(t));
224
+ const resolved = resolveRecipe(runtime.kind, manifestRecipeRequest(manifest), config);
225
+ const tenantLimits = await readTenantLimits(t);
226
+ errors.push(...(resolved.kind === "error" ? resolved.errors : []), ...limitIssues(runtime.limits, tenantLimits), ...manifestShapeIssues({ ...runtime, ...(manifest.description === undefined ? {} : { description: manifest.description }) }, runtimeLimits), ...(await fileErrors(t, runtime.files)), ...triggerIssues(runtime.triggers, runtimeLimits));
227
+ // A skill's files count against the manifest's own skill limit, never above the Tenant's.
228
+ const limitBytes = limitsInForce(tenantLimits, runtime.limits).skills.totalBytes;
229
+ const { missing, oversized } = await definitionFileIssues(t, loop, limitBytes);
230
+ if (missing.length > 0)
231
+ errors.push(`The manifest names ${missing.length} file(s) the Runtime does not hold; upload each with PUT /v1/files/<sha256> first: ${missing.join(", ")}`);
232
+ if (oversized.length > 0)
233
+ errors.push(`A skill's files may hold at most ${limitBytes} bytes together: ${oversized.map((skill) => `'${skill.name}' holds ${skill.bytes}`).join(", ")}`);
234
+ return { errors, missing, oversized, limitBytes };
235
+ }
236
+ export async function saveAgentManifest(ctx, name, body, scope, options = {}) {
237
+ const { requestId: _, ...manifest } = body;
238
+ if (manifest.name !== name)
239
+ fail(400, `The manifest is named ${manifest.name}, and the path names ${name}: they must match.`);
240
+ const hash = agentManifestHash(manifest);
241
+ const gaps = sectionGaps(manifest);
242
+ return ctx.store.tx(async (t) => {
243
+ await t.lockAgentManifest(name);
244
+ const latest = await t.agentManifest(name);
245
+ if (options.ifMatch !== undefined && (latest?.version ?? 0) !== options.ifMatch)
246
+ fail(412, latest === undefined
247
+ ? `Agent manifest ${name} does not exist, and If-Match named version ${options.ifMatch}.`
248
+ : `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
+ const unchanged = latest?.hash === hash;
250
+ const to = unchanged ? latest.version : (latest?.version ?? 0) + 1;
251
+ const counts = await t.agentsOfManifest(name);
252
+ const checked = await errorsOf(t, manifest);
253
+ // The version as it would be saved, for the plan of the Agents it moves.
254
+ const row = unchanged
255
+ ? latest
256
+ : { name, version: to, body: manifest, hash, savedBy: { actor: actorOf(scope) }, createdAt: new Date().toISOString() };
257
+ if (options.dryRun) {
258
+ const errors = [...gaps, ...checked.errors];
259
+ // A save with errors moves nothing; one that changes nothing moves no Agent either.
260
+ const moves = errors.length > 0 || unchanged ? undefined : await moveTracking(t, row, false);
261
+ return {
262
+ name,
263
+ from: latest?.version ?? null,
264
+ to,
265
+ unchanged,
266
+ changes: unchanged ? [] : sectionChanges(latest?.body, manifest),
267
+ warnings: warningsOf(ctx, manifest, to, moves),
268
+ errors,
269
+ behind: moves?.behind ?? (await agentsBehind(t, name, to)).length,
270
+ moves: { now: moves?.now ?? 0, nextStart: moves?.nextStart ?? 0, secrets: moves?.secrets ?? 0 },
271
+ };
272
+ }
273
+ if (gaps.length > 0)
274
+ fail(422, `Agent manifest ${name}'s harness and runtime sections do not fit: ${gaps.join("; ")}.`, {
275
+ code: "manifest_inconsistent",
276
+ details: { gaps },
277
+ });
278
+ if (checked.missing.length > 0)
279
+ fail(400, checked.errors.find((error) => error.startsWith("The manifest names")), {
280
+ code: "definition_files_missing",
281
+ details: { missing: checked.missing },
282
+ });
283
+ if (checked.oversized.length > 0)
284
+ fail(413, `This manifest allows a skill's files at most ${checked.limitBytes} bytes together: ${checked.oversized.map((skill) => `'${skill.name}' holds ${skill.bytes}`).join(", ")}`, {
285
+ code: "limit_exceeded",
286
+ details: { limitBytes: checked.limitBytes, skills: checked.oversized },
287
+ });
288
+ if (checked.errors.length > 0)
289
+ fail(400, checked.errors.join(" "));
290
+ if (unchanged)
291
+ return { manifest: viewOf(latest, counts), unchanged: true, warnings: warningsOf(ctx, manifest, to, undefined) };
292
+ await t.insertAgentManifest(row);
293
+ // Its files by hash, skills' and runtime files' alike, for the sweep that deletes unused ones.
294
+ const shas = new Set(definitionFilesOf(loopManifestOf(manifest)));
295
+ for (const file of runtimeOf(manifest).files ?? [])
296
+ if ("files" in file)
297
+ for (const sha of Object.values(file.files))
298
+ shas.add(sha);
299
+ if (shas.size > 0)
300
+ await t.insertDefinitionFileUses(manifestFileOwner(name), hash, [...shas]);
301
+ // Every Agent that tracks the manifest moves to the new version (track T3).
302
+ const moves = await moveTracking(t, row, true);
303
+ return { manifest: viewOf(row, await t.agentsOfManifest(name)), unchanged: false, warnings: warningsOf(ctx, manifest, to, moves) };
304
+ });
305
+ }
306
+ /** `GET /v1/agent-manifests/{name}` (the latest) and `.../versions/{n}`. */
307
+ export async function getAgentManifest(ctx, name, version) {
308
+ return ctx.store.tx(async (t) => {
309
+ const row = (await t.agentManifest(name, version)) ?? notFound(name, version);
310
+ return viewOf(row, await t.agentsOfManifest(name));
311
+ });
312
+ }
313
+ /** `GET /v1/agent-manifests`: each manifest's latest version. */
314
+ export async function listAgentManifests(ctx) {
315
+ return ctx.store.tx(async (t) => {
316
+ const manifests = [];
317
+ for (const row of await t.listAgentManifests())
318
+ manifests.push(viewOf(row, await t.agentsOfManifest(row.name)));
319
+ return { manifests };
320
+ });
321
+ }
322
+ /** `GET /v1/agent-manifests/{name}/versions`: every version, newest first. */
323
+ export async function agentManifestVersions(ctx, name) {
324
+ return ctx.store.tx(async (t) => {
325
+ const rows = await t.agentManifestVersions(name);
326
+ if (rows.length === 0)
327
+ notFound(name);
328
+ const counts = await t.agentsOfManifest(name);
329
+ return {
330
+ name,
331
+ latest: rows[0].version,
332
+ versions: rows.map((row) => ({
333
+ version: row.version,
334
+ hash: row.hash,
335
+ savedBy: row.savedBy,
336
+ createdAt: row.createdAt,
337
+ agents: counts.get(row.version) ?? 0,
338
+ })),
339
+ };
340
+ });
341
+ }
342
+ /**
343
+ * `DELETE /v1/agent-manifests/{name}`: deletes every version, refused while any Agent was made
344
+ * from one (`409 manifest_in_use`): the Agent names the version it runs.
345
+ */
346
+ export async function deleteAgentManifest(ctx, name) {
347
+ return ctx.store.tx(async (t) => {
348
+ await t.lockAgentManifest(name);
349
+ let made = 0;
350
+ for (const n of (await t.agentsOfManifest(name)).values())
351
+ made += n;
352
+ if (made > 0)
353
+ fail(409, `${made} Agent(s) were made from agent manifest ${name}: delete them first, or keep the manifest.`, {
354
+ code: "manifest_in_use",
355
+ details: { agents: made },
356
+ });
357
+ return { name, deleted: (await t.deleteAgentManifest(name)) > 0 };
358
+ });
359
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * An Agent's versions (track T3, D73): which version of its agent manifest it runs, how it moves
3
+ * to another, and which version a session's turn runs.
4
+ *
5
+ * - **One version at a time.** An Agent runs one version of its manifest (`manifestVersion`),
6
+ * both sections together: the loop manifest its harness section makes, and the runtime its
7
+ * compute applies (`applied`). A session pins all of it (`pinOf`) when it opens.
8
+ * - **A move** (`moveAgent`: `PUT /v1/agents/{id}`, and a save for each Agent that tracks the
9
+ * manifest) changes no data: its id, disk, vault and sessions stay. A version whose runtime a
10
+ * pod applies only when it starts (`START_FIELDS`) makes the Agent outdated (`targetVersion`,
11
+ * with the target's runtime in `recipe`) until its pod's next start switches it (`switchTo`);
12
+ * any other applies at once. A version that adds a required secret with no value the version
13
+ * can use waits for it (`resumeMove` after each secret write). Its manifest's name, kind,
14
+ * harness and storage are fixed for its life (`409 agent_fixed_field`).
15
+ * - **The start check** (pods): a start on a new version records what ran before
16
+ * (`startCheck`) until the new pod's harness joins (`passStartCheck`). A failed setup, a failed
17
+ * pod or a start past the Host's `tenant.upgradeStartMs` goes back to it (`revertStart`): the
18
+ * definition, not the data. The version is the Agent's `failedVersion`, which a tracking Agent
19
+ * does not move to again, until a start on a new version succeeds. While a pod waits to start
20
+ * on a new version, it idles at most `tenant.upgradeIdleMs` (`idleMsOf`).
21
+ * - **A turn takes the Agent's version.** A turn's first segment re-pins its session to the
22
+ * version the Agent runs when that changed since the session's last turn (`repinTurn`), before
23
+ * anything of the turn has run: no effect is journaled for it yet. The turn keeps that version
24
+ * through every later segment, approval and rollover. A turn whose message named a variant,
25
+ * and a flow's sessions, keep the version they pinned.
26
+ *
27
+ * Re-pinning at the first segment, not when the message is accepted, keeps a turn that waits for
28
+ * its compute to start on a version the Agent has at that start.
29
+ */
30
+ import type { AgentMovePlan, EnvironmentRecipe } from "@nylorun/core/contracts";
31
+ import { type SandboxManifest } from "@nylorun/core/define";
32
+ import type { AgentManifestRow, Environment, Tx } from "../store/types.js";
33
+ import type { TenantSandboxConfig } from "../sandbox/tenant-config.js";
34
+ import { type Session, type TenantContext } from "./context.js";
35
+ /** What a session pins of the version its Agent runs. */
36
+ export interface SessionPin {
37
+ /** The loop manifest the version's harness section makes (`loopManifestOf`). */
38
+ readonly manifest: any;
39
+ readonly manifestHash: string;
40
+ readonly definition: {
41
+ readonly name: string;
42
+ readonly version: number;
43
+ };
44
+ /** The version's `harness.loop`, when it sets one. */
45
+ readonly loop?: Record<string, unknown>;
46
+ /** The harness that runs its turns: the runtime's, or ours for a flow. */
47
+ readonly harness: string;
48
+ /** The sandbox spec of the runtime the Agent's compute runs (kinds `virtual` and `pod`). */
49
+ readonly sandbox?: SandboxManifest;
50
+ }
51
+ /**
52
+ * The pin of the version Agent `agent` runs: its manifest version's loop manifest, and the
53
+ * harness and sandbox spec of the runtime its compute applies (`applied`). `409 agent_unavailable`
54
+ * when that version is gone.
55
+ */
56
+ export declare function pinOf(t: Tx, agent: Environment): Promise<{
57
+ pin: SessionPin;
58
+ version: AgentManifestRow;
59
+ }>;
60
+ /** Writes `pin` onto `session` (the caller saves it). */
61
+ export declare function applyPin(session: Session, pin: SessionPin): void;
62
+ /**
63
+ * Re-pins `session` (in the segment's owned transaction) to the version its Agent runs, when its
64
+ * active turn is about to run its first segment and nothing of the turn has run yet: the turn
65
+ * then runs that version. Returns whether it changed the session (the caller saves it).
66
+ *
67
+ * Leaves the session as it is for a flow and a flow's linked session, a turn whose message named
68
+ * a variant, a later segment, a turn with any journaled effect, and a version whose harness is
69
+ * not the session's (an Agent's harness is fixed for its life, D73).
70
+ */
71
+ export declare function repinTurn(t: Tx, session: Session): Promise<boolean>;
72
+ /** What a move of `agent` to `target` would do, and the target's runtime resolved. */
73
+ export interface Move {
74
+ plan: AgentMovePlan;
75
+ target: AgentManifestRow;
76
+ recipe: EnvironmentRecipe;
77
+ }
78
+ /**
79
+ * The plan of moving `agent` to version `target` of its manifest, named `tracking` after it:
80
+ * the target's runtime resolved against the Tenant's limits (a `400` when it no longer fits),
81
+ * what it changes, the fixed fields it would change, and the required secrets it would wait for.
82
+ */
83
+ export declare function planMove(t: Tx, agent: Environment, target: AgentManifestRow, tracking: Environment["manifestRef"]): Promise<Move>;
84
+ /** The `409 agent_fixed_field` of a move that would change what is fixed for an Agent's life. */
85
+ export declare function refuseFixed(agentId: string, plan: Pick<AgentMovePlan, "refusals" | "to" | "from">): never;
86
+ /**
87
+ * Moves `agent` (locked, live) per `move` (`planMove`), in the caller's transaction. A move
88
+ * replaces a pending one: the runtime it waited to apply is dropped first. `none` (back to what it
89
+ * runs) only clears the target; `now` switches; `next_start` keeps the target's runtime in
90
+ * `recipe` for the pod's next start (`switchTo`); `secrets` names the target and waits.
91
+ * Returns when it applies.
92
+ */
93
+ export declare function moveAgent(t: Tx, agent: Environment, move: Move, now: string): Promise<AgentMovePlan["applies"]>;
94
+ /**
95
+ * After a secret write: an Agent that waits for its secrets moves on once the target's required
96
+ * slots are filled (`moveAgent` again, to the same target). In the caller's transaction.
97
+ */
98
+ export declare function resumeMove(t: Tx, agent: Environment, now: string): Promise<void>;
99
+ /**
100
+ * At a pod Agent's start (`wantRunning`, `newVolume`): switches `agent` (locked) to the version it
101
+ * moves to when its runtime waits for this start. Applies the target's runtime and version, drops
102
+ * the pinned image digest when the image changed, and records what ran before (`startCheck`)
103
+ * until the new pod's harness joins. Returns whether it switched.
104
+ */
105
+ export declare function switchTo(t: Tx, agent: Environment, now: string): Promise<boolean>;
106
+ /** What an outdated Agent waits for: its secrets, or its pod's next start; `null` when it is not. */
107
+ export declare function waitingFor(agent: Pick<Environment, "targetVersion" | "appliedVersion" | "version">): "secrets" | "start" | null;
108
+ /** How long pod Agent `agent` may idle before it stops: shorter while a start would apply a new version. */
109
+ export declare function idleMsOf(config: TenantSandboxConfig, agent: Environment): number;
110
+ /**
111
+ * The harness of pod Agent `id` joined at host epoch `epoch` (its `hello`): a start on a new
112
+ * version succeeded, setup included. Clears the start check and the failed version.
113
+ */
114
+ export declare function passStartCheck(ctx: TenantContext, id: string, epoch: number): Promise<void>;
115
+ /**
116
+ * Pod Agent `agent` (locked) did not start on the version it switched to (`reason`): back to what
117
+ * it ran before (`startCheck.from`), its definition and not its data, with that version as its
118
+ * `failedVersion`, and its pod asked to run again. A turn waiting for the start runs on the
119
+ * version it went back to. Returns whether there was a start to go back from.
120
+ */
121
+ export declare function revertStart(t: Tx, agent: Environment, reason: string, now: string): Promise<boolean>;
122
+ /**
123
+ * The Tenant sweep: each pod Agent whose start on a new version has taken longer than the Host's
124
+ * `tenant.upgradeStartMs` goes back to the version it ran (`revertStart`).
125
+ */
126
+ export declare function sweepStartChecks(ctx: TenantContext, now?: number): Promise<void>;