@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.
- package/CHANGELOG.md +67 -0
- package/README.md +1 -1
- package/dist/api/a2a/card.d.ts +4 -3
- package/dist/api/a2a/card.js +3 -3
- package/dist/api/a2a/routes.js +18 -7
- package/dist/api/ag-ui/routes.js +4 -7
- package/dist/api/components.d.ts +5854 -2718
- package/dist/api/components.js +39 -30
- package/dist/api/http/app.js +9 -4
- package/dist/api/http/body.d.ts +2 -0
- package/dist/api/http/body.js +20 -0
- package/dist/api/http/define.d.ts +4 -0
- package/dist/api/http/define.js +20 -9
- package/dist/api/http/routes/agent-manifests.d.ts +10 -0
- package/dist/api/http/routes/agent-manifests.js +135 -0
- package/dist/api/http/routes/agents.d.ts +11 -0
- package/dist/api/http/routes/agents.js +418 -0
- package/dist/api/http/routes/artifacts.js +2 -2
- package/dist/api/http/routes/coverage.d.ts +4 -4
- package/dist/api/http/routes/coverage.js +12 -6
- package/dist/api/http/routes/files.js +1 -1
- package/dist/api/http/routes/mcp.js +2 -2
- package/dist/api/http/routes/me.d.ts +1 -1
- package/dist/api/http/routes/me.js +1 -2
- package/dist/api/http/routes/memories.d.ts +9 -0
- package/dist/api/http/routes/memories.js +152 -0
- package/dist/api/http/routes/reads.d.ts +1 -137
- package/dist/api/http/routes/reads.js +31 -12
- package/dist/api/http/routes/sessions.js +21 -50
- package/dist/api/http/routes/tenant.js +13 -10
- package/dist/api/http/routes/triggers.d.ts +10 -0
- package/dist/api/http/routes/triggers.js +183 -0
- package/dist/api/openapi.js +69 -47
- package/dist/artifacts/disk.d.ts +10 -10
- package/dist/artifacts/disk.js +27 -27
- package/dist/artifacts/quota.d.ts +1 -1
- package/dist/artifacts/quota.js +2 -2
- package/dist/artifacts/service.js +2 -2
- package/dist/core/flow-host.d.ts +0 -1
- package/dist/gates/egress-credentials.js +1 -1
- package/dist/gates/egress.d.ts +5 -5
- package/dist/gates/egress.js +6 -6
- package/dist/gates/http-tool.js +2 -2
- package/dist/gates/meter.js +22 -5
- package/dist/gates/policy.d.ts +8 -3
- package/dist/gates/policy.js +7 -2
- package/dist/gates/tenant-vaults.d.ts +1 -1
- package/dist/harness/pod.d.ts +2 -0
- package/dist/harness/setup.js +17 -6
- package/dist/harness-api/record.js +11 -0
- package/dist/harness-api/server.js +4 -0
- package/dist/harness-api/start.d.ts +11 -7
- package/dist/harness-api/start.js +49 -5
- package/dist/harness-api/workspace.js +2 -2
- package/dist/host/config-for.d.ts +2 -0
- package/dist/host/config-for.js +1 -0
- package/dist/host/http.d.ts +2 -2
- package/dist/host/http.js +5 -2
- package/dist/host/main.js +1 -0
- package/dist/keys/keys.d.ts +15 -4
- package/dist/keys/keys.js +16 -0
- package/dist/keys/webhooks.d.ts +25 -0
- package/dist/keys/webhooks.js +62 -0
- package/dist/management-openapi.json +313 -133
- package/dist/mcp/connect.d.ts +2 -2
- package/dist/mcp/connect.js +3 -3
- package/dist/mcp/preview.js +7 -7
- package/dist/memory/service.d.ts +97 -0
- package/dist/memory/service.js +335 -0
- package/dist/memory/tool.d.ts +36 -0
- package/dist/memory/tool.js +84 -0
- package/dist/model/pi-model.js +7 -0
- package/dist/openapi.json +13120 -9034
- package/dist/reads/types.d.ts +3 -3
- package/dist/record/agent.d.ts +27 -0
- package/dist/record/agent.js +46 -0
- package/dist/record/index.d.ts +1 -1
- package/dist/record/index.js +1 -1
- package/dist/sandbox/join.js +5 -5
- package/dist/sandbox/placement.d.ts +1 -1
- package/dist/sandbox/placement.js +3 -3
- package/dist/sandbox/pods/lifecycle.d.ts +6 -6
- package/dist/sandbox/pods/lifecycle.js +6 -6
- package/dist/sandbox/pods/reconcile.js +10 -2
- package/dist/sandbox/resolve.d.ts +3 -3
- package/dist/sandbox/resolve.js +7 -7
- package/dist/sandbox/setup.d.ts +3 -1
- package/dist/sandbox/setup.js +14 -4
- package/dist/store/postgres/drizzle/0032_agents.sql +54 -0
- package/dist/store/postgres/drizzle/0033_agent_versions.sql +7 -0
- package/dist/store/postgres/drizzle/0034_protocol_17_session_queue.sql +13 -0
- package/dist/store/postgres/drizzle/0035_t4_triggers.sql +41 -0
- package/dist/store/postgres/drizzle/0036_t5_memories.sql +30 -0
- package/dist/store/postgres/drizzle/0037_model_usage_cost_split.sql +4 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +42 -0
- package/dist/store/postgres/reads.js +44 -87
- package/dist/store/postgres/record-writer.d.ts +4 -4
- package/dist/store/postgres/record-writer.js +9 -9
- package/dist/store/postgres/schema.d.ts +1408 -123
- package/dist/store/postgres/schema.js +194 -38
- package/dist/store/postgres/store.js +329 -79
- package/dist/store/postgres/tenant.js +1 -2
- package/dist/store/types.d.ts +155 -50
- package/dist/store/types.js +0 -1
- package/dist/tenant/advance.js +20 -8
- package/dist/tenant/agent-manifests.d.ts +72 -0
- package/dist/tenant/agent-manifests.js +359 -0
- package/dist/tenant/agent-versions.d.ts +126 -0
- package/dist/tenant/agent-versions.js +389 -0
- package/dist/tenant/agents.d.ts +230 -0
- package/dist/tenant/{environments.js → agents.js} +464 -320
- package/dist/tenant/auth.d.ts +1 -1
- package/dist/tenant/auth.js +3 -3
- package/dist/tenant/commands.d.ts +63 -2
- package/dist/tenant/commands.js +240 -83
- package/dist/tenant/context.d.ts +46 -19
- package/dist/tenant/context.js +6 -2
- package/dist/tenant/effects.js +7 -16
- package/dist/tenant/identity-file.d.ts +6 -6
- package/dist/tenant/identity-file.js +12 -10
- package/dist/tenant/issuers.js +3 -3
- package/dist/tenant/limits.d.ts +0 -3
- package/dist/tenant/limits.js +0 -3
- package/dist/tenant/memories.d.ts +38 -0
- package/dist/tenant/memories.js +119 -0
- package/dist/tenant/reset.js +2 -2
- package/dist/tenant/run-grants.d.ts +6 -0
- package/dist/tenant/run-grants.js +4 -0
- package/dist/tenant/runtime.d.ts +1 -1
- package/dist/tenant/runtime.js +8 -0
- package/dist/tenant/sessions.d.ts +28 -45
- package/dist/tenant/sessions.js +85 -208
- package/dist/tenant/status.js +6 -7
- package/dist/tenant/sweep.js +12 -3
- package/dist/tenant/transcript.d.ts +16 -16
- package/dist/tenant/triggers.d.ts +143 -0
- package/dist/tenant/triggers.js +715 -0
- package/dist/tenant/turn-timing.d.ts +11 -1
- package/dist/tenant/turn-timing.js +3 -1
- package/dist/tenant/types.d.ts +5 -0
- package/dist/tenant/worker.d.ts +2 -0
- package/dist/tenant/worker.js +5 -0
- package/dist/traces/map.d.ts +1 -1
- package/dist/traces/map.js +7 -1
- package/dist/tuning.d.ts +7 -0
- package/dist/tuning.js +19 -0
- package/dist/vault/service.d.ts +13 -8
- package/dist/vault/service.js +61 -17
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +13 -12
- package/dist/api/http/routes/environment-manifests.d.ts +0 -9
- package/dist/api/http/routes/environment-manifests.js +0 -118
- package/dist/api/http/routes/environments.d.ts +0 -11
- package/dist/api/http/routes/environments.js +0 -332
- package/dist/record/environment.d.ts +0 -27
- package/dist/record/environment.js +0 -46
- package/dist/tenant/environment-manifests.d.ts +0 -57
- package/dist/tenant/environment-manifests.js +0 -208
- 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>;
|