@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.
- package/CHANGELOG.md +59 -0
- package/dist/api/components.d.ts +229 -0
- package/dist/api/components.js +4 -1
- package/dist/api/http/routes/agent-manifests.d.ts +3 -1
- package/dist/api/http/routes/agent-manifests.js +89 -10
- package/dist/api/openapi.js +3 -0
- package/dist/host/http.js +3 -1
- package/dist/management-openapi.json +34 -5
- package/dist/openapi.json +951 -39
- package/dist/store/postgres/drizzle/0038_t6_agent_manifest_source.sql +1 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +7 -0
- package/dist/store/postgres/schema.d.ts +19 -0
- package/dist/store/postgres/schema.js +6 -0
- package/dist/store/postgres/store.js +1 -0
- package/dist/store/types.d.ts +14 -0
- package/dist/tenant/agent-manifests.d.ts +23 -25
- package/dist/tenant/agent-manifests.js +178 -4
- package/dist/tenant/agents.d.ts +4 -3
- package/dist/tenant/agents.js +26 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
|
@@ -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. */
|
package/dist/store/types.d.ts
CHANGED
|
@@ -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
|
-
: {
|
|
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 {
|
|
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) => {
|
package/dist/tenant/agents.d.ts
CHANGED
|
@@ -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)
|
|
21
|
-
*
|
|
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
|
};
|
package/dist/tenant/agents.js
CHANGED
|
@@ -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
|
|
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)
|
|
114
|
-
*
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
69
|
-
"@nylorun/harness": "0.29.
|
|
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",
|