@nylorun/runtime 0.50.0-beta → 0.51.1-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 +25 -0
- package/dist/harness/main.js +13 -1
- package/dist/host/stack-config.d.ts +6 -0
- package/dist/host/stack-config.js +1 -0
- package/dist/management-openapi.json +1 -1
- package/dist/openapi.json +1 -1
- package/dist/sandbox/pods/client.d.ts +5 -0
- package/dist/sandbox/pods/lifecycle.d.ts +9 -0
- package/dist/sandbox/pods/lifecycle.js +16 -0
- package/dist/store/postgres/drizzle/0048_sandbox_pod_drained_at.sql +1 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +7 -0
- package/dist/store/postgres/schema.d.ts +17 -0
- package/dist/store/postgres/schema.js +2 -0
- package/dist/store/postgres/store.js +2 -0
- package/dist/store/types.d.ts +5 -0
- package/dist/tenant/runtime.js +14 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +16 -16
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.51.1-beta
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 8451138: Pods on Agent Substrate (preview): a drain is recorded as soon as the moved pod's engine connects again. After suspend before drain, the restored engine redials with its host token (no join), and nothing asked the Runtime to reconcile the pod, so `agent.suspended` with reason `drain` waited for the next turn or idle timer (E13 on kind: not within two minutes). A pod engine's connection now asks for its sandbox's reconcile.
|
|
8
|
+
|
|
9
|
+
## 0.51.0-beta
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 874cc83: Pods on Agent Substrate (preview): suspend before drain, following Substrate's eviction contract. A worker pod that stops (a drain, a GKE node upgrade or scale-down, a WorkerPool rollout, a deleted pod) sends each of its actors SIGTERM and gives them 30 minutes to be suspended; until now the engine exited on that signal, so the actor crashed and went back to its last suspend, losing everything since.
|
|
14
|
+
|
|
15
|
+
- `@nylorun/runtime`: on Substrate (`NYLORUN_SANDBOX_SUBSTRATE=1`, which the sandboxes service's template sets), the engine logs `harness_worker_stopping` on SIGTERM and keeps running, with its runs. The sandboxes service watches worker pods (label `ate.dev/worker-pool`) and, when one turns Terminating, suspends each of the Tenant's actors on it synchronously (eight at a time, trying again for up to 29 minutes), records `drainedAt`, and resumes a sandbox whose request is `Running` on another worker. While it moves, its status reason is `Moving`, and the pod lifecycle waits instead of failing it at the ready deadline. Each drain is recorded once, as `agent.suspended` with reason `drain` (the pod row's `drained_at`, store migration 0048, nullable, run at start). Not covered: a spot reclaim or a node that dies, which still crash and revert. An actor created by an earlier release keeps its engine (#643) until it is reset.
|
|
16
|
+
- `@nylorun/core`: `agent.suspended`'s `reason` may be `drain`.
|
|
17
|
+
- `nylorun`: `local sandbox enable --backend substrate` lets the sandboxes service read pods cluster-wide (`get`, `list`, `watch`), for the worker pods it watches.
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- Pin core to the tested release.
|
|
22
|
+
- Pin harness to the tested release.
|
|
23
|
+
- Updated dependencies [874cc83]
|
|
24
|
+
- Updated dependencies
|
|
25
|
+
- @nylorun/core@0.44.0-beta
|
|
26
|
+
- @nylorun/harness@0.36.4-beta
|
|
27
|
+
|
|
3
28
|
## 0.50.0-beta
|
|
4
29
|
|
|
5
30
|
### Minor Changes
|
package/dist/harness/main.js
CHANGED
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
* pod's join token instead of a harness token (`pod.ts`), runs its disk's setup on a new volume
|
|
15
15
|
* (`setup.ts`), and runs its sandbox's tools in the pod itself (the `local` backend), with egress
|
|
16
16
|
* through egress-gate. SIGTERM (tini forwards
|
|
17
|
-
* it; the pod's grace is 10 s by default) gives its runs back and exits.
|
|
17
|
+
* it; the pod's grace is 10 s by default) gives its runs back and exits. On Agent Substrate
|
|
18
|
+
* (`NYLORUN_SANDBOX_SUBSTRATE=1`), SIGTERM means the worker pod is stopping: the engine logs it and
|
|
19
|
+
* keeps running, so the sandboxes service can suspend the actor within Substrate's 30 minutes.
|
|
18
20
|
*/
|
|
19
21
|
import { existsSync, mkdirSync } from "node:fs";
|
|
20
22
|
import { createServer } from "node:http";
|
|
@@ -106,8 +108,18 @@ export async function runHarness(stack, baseline) {
|
|
|
106
108
|
});
|
|
107
109
|
logger.info("harness_ready", { health: `http://127.0.0.1:${config.healthPort}/health` });
|
|
108
110
|
let stopping = false;
|
|
111
|
+
let workerStopping = false;
|
|
109
112
|
for (const signal of ["SIGINT", "SIGTERM"])
|
|
110
113
|
process.on(signal, () => {
|
|
114
|
+
// On Agent Substrate, SIGTERM means the worker pod is stopping: the actor has 30 minutes to
|
|
115
|
+
// be suspended, and the sandboxes service suspends it. A process that exits can't be, so the
|
|
116
|
+
// engine keeps running, and keeps its runs (Substrate's eviction contract).
|
|
117
|
+
if (signal === "SIGTERM" && config.pod?.substrate) {
|
|
118
|
+
if (!workerStopping)
|
|
119
|
+
logger.info("harness_worker_stopping", { sandboxId: config.pod.sandboxId });
|
|
120
|
+
workerStopping = true;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
111
123
|
if (stopping)
|
|
112
124
|
return;
|
|
113
125
|
stopping = true;
|
|
@@ -169,6 +169,12 @@ export interface PodHostConfig {
|
|
|
169
169
|
* for the hosts whose keys Substrate's egress adds.
|
|
170
170
|
*/
|
|
171
171
|
egressCaFile?: string;
|
|
172
|
+
/**
|
|
173
|
+
* `NYLORUN_SANDBOX_SUBSTRATE=1`: the pod is an Agent Substrate actor. A stopping worker sends
|
|
174
|
+
* it SIGTERM and gives it 30 minutes to be suspended, which the sandboxes service does; the
|
|
175
|
+
* engine stays up instead of exiting (suspend before drain).
|
|
176
|
+
*/
|
|
177
|
+
substrate?: boolean;
|
|
172
178
|
/** Addresses the pod's NetworkPolicy must block, probed before the engine starts. */
|
|
173
179
|
blocked: BlockedAddress[];
|
|
174
180
|
}
|
|
@@ -647,6 +647,7 @@ function parsePodHost(env, url) {
|
|
|
647
647
|
httpUrl: origin,
|
|
648
648
|
...(egress ? { egressProxy: egress.replace(/\/+$/, "") } : {}),
|
|
649
649
|
...(egressCa ? { egressCaFile: egressCa } : {}),
|
|
650
|
+
...(read(env, "NYLORUN_SANDBOX_SUBSTRATE") === "1" ? { substrate: true } : {}),
|
|
650
651
|
blocked: blockedAddresses(env),
|
|
651
652
|
},
|
|
652
653
|
};
|
package/dist/openapi.json
CHANGED
|
@@ -63,6 +63,11 @@ export interface PodStatus {
|
|
|
63
63
|
readonly shutdownTime?: string;
|
|
64
64
|
readonly reason?: string;
|
|
65
65
|
readonly message?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Agent Substrate: when the sandbox was last suspended because its worker stopped. While it
|
|
68
|
+
* moves to another worker, `reason` is `Moving`.
|
|
69
|
+
*/
|
|
70
|
+
readonly drainedAt?: string;
|
|
66
71
|
/** With `?wait=`: whether the state was reached before the timeout. */
|
|
67
72
|
readonly met?: boolean;
|
|
68
73
|
}
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
* (`agent.failed`, saying what the Agent lost); a crash an idle suspend finds is recorded the
|
|
26
26
|
* same way. The next turn start asks for `Running`, and the sandboxes service reverts the
|
|
27
27
|
* sandbox to its last suspend before resuming it.
|
|
28
|
+
* - **Drained** (Agent Substrate): a worker that stops gives its actors 30 minutes to be
|
|
29
|
+
* suspended. The sandboxes service suspends each, records `drainedAt` and resumes it on
|
|
30
|
+
* another worker; a new `drainedAt` is `agent.suspended` (reason `drain`), once, and a pod
|
|
31
|
+
* that is `Moving` waits rather than fail at the ready deadline.
|
|
28
32
|
* - **Lost**: an incarnation that has run (a pod joined) whose Sandbox is gone or whose volume
|
|
29
33
|
* is missing is `lost` (`agent.lost`): its host epoch moves, its Sandbox is deleted, and
|
|
30
34
|
* only a reset brings the Environment back. agent-sandbox recreates a deleted claim empty, so
|
|
@@ -60,6 +64,11 @@ export declare function repositoryOf(reference: string): string;
|
|
|
60
64
|
* the node made up for an image loaded onto it, which no registry serves.
|
|
61
65
|
*/
|
|
62
66
|
export declare function imageDigestOf(imageID: string | undefined, requested: string): string | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* The sandboxes service's reason for a sandbox suspended because its Agent Substrate worker
|
|
69
|
+
* stopped, until it runs on another one.
|
|
70
|
+
*/
|
|
71
|
+
export declare const MOVING = "Moving";
|
|
63
72
|
/** A setup failure's `reason` starts with this (`sandbox/setup.ts`); it is sticky. */
|
|
64
73
|
export declare const SETUP_FAILED = "setup:";
|
|
65
74
|
/** A crash's `reason` (see `crashReasonOf`) starts with this; it stays until a turn starts the pod. */
|
|
@@ -34,6 +34,11 @@ export function imageDigestOf(imageID, requested) {
|
|
|
34
34
|
return undefined;
|
|
35
35
|
return repositoryOf(id) === repositoryOf(requested) ? id : undefined;
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* The sandboxes service's reason for a sandbox suspended because its Agent Substrate worker
|
|
39
|
+
* stopped, until it runs on another one.
|
|
40
|
+
*/
|
|
41
|
+
export const MOVING = "Moving";
|
|
37
42
|
/** A setup failure's `reason` starts with this (`sandbox/setup.ts`); it is sticky. */
|
|
38
43
|
export const SETUP_FAILED = "setup:";
|
|
39
44
|
/** A crash's `reason` (see `crashReasonOf`) starts with this; it stays until a turn starts the pod. */
|
|
@@ -158,6 +163,12 @@ export function decide(facts) {
|
|
|
158
163
|
again(pollMs);
|
|
159
164
|
return done();
|
|
160
165
|
}
|
|
166
|
+
// Agent Substrate: its worker stopped, and the sandboxes service suspended it with everything it
|
|
167
|
+
// held and resumes it on another worker (suspend before drain). Recorded once per drain.
|
|
168
|
+
if (status.drainedAt !== undefined && status.drainedAt !== pod.drainedAt) {
|
|
169
|
+
patch.drainedAt = status.drainedAt;
|
|
170
|
+
events.push(event("agent.suspended", { reason: "drain" }));
|
|
171
|
+
}
|
|
161
172
|
const opId = opIdOf({ k8sName: pod.k8sName, rev });
|
|
162
173
|
if (desired === "suspended" && !status.exists) {
|
|
163
174
|
if (pod.observed !== "suspended")
|
|
@@ -217,6 +228,11 @@ export function decide(facts) {
|
|
|
217
228
|
events.push(event("agent.failed", { reason: crash }));
|
|
218
229
|
return done();
|
|
219
230
|
}
|
|
231
|
+
// On its way to another worker after a drain: neither failed nor starting.
|
|
232
|
+
if (status.reason === MOVING) {
|
|
233
|
+
again(pollMs);
|
|
234
|
+
return done();
|
|
235
|
+
}
|
|
220
236
|
if (now - startedAt >= readyDeadlineMs) {
|
|
221
237
|
const reason = crash ??
|
|
222
238
|
([status.reason, status.message].filter(Boolean).join(": ") ||
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE "nylorun"."agents" ADD COLUMN "drained_at" text;
|
|
@@ -337,6 +337,13 @@
|
|
|
337
337
|
"when": 1794630124577,
|
|
338
338
|
"tag": "0047_model_usage_attempts",
|
|
339
339
|
"breakpoints": true
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
"idx": 48,
|
|
343
|
+
"version": "7",
|
|
344
|
+
"when": 1794635246579,
|
|
345
|
+
"tag": "0048_sandbox_pod_drained_at",
|
|
346
|
+
"breakpoints": true
|
|
340
347
|
}
|
|
341
348
|
]
|
|
342
349
|
}
|
|
@@ -1321,6 +1321,23 @@ export declare const agents: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
|
1321
1321
|
}, {}, {
|
|
1322
1322
|
pgColumnBuilderBrand: 'PgCustomColumnBuilderBrand';
|
|
1323
1323
|
}>;
|
|
1324
|
+
drainedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
1325
|
+
name: "drainedAt";
|
|
1326
|
+
tableName: "agents";
|
|
1327
|
+
dataType: "string";
|
|
1328
|
+
columnType: "PgText";
|
|
1329
|
+
data: string;
|
|
1330
|
+
driverParam: string;
|
|
1331
|
+
notNull: false;
|
|
1332
|
+
hasDefault: false;
|
|
1333
|
+
isPrimaryKey: false;
|
|
1334
|
+
isAutoincrement: false;
|
|
1335
|
+
hasRuntimeDefault: false;
|
|
1336
|
+
enumValues: [string, ...string[]];
|
|
1337
|
+
baseColumn: never;
|
|
1338
|
+
identity: undefined;
|
|
1339
|
+
generated: undefined;
|
|
1340
|
+
}, {}, {}>;
|
|
1324
1341
|
diskOp: import("drizzle-orm/pg-core").PgColumn<{
|
|
1325
1342
|
name: "diskOp";
|
|
1326
1343
|
tableName: "agents";
|
|
@@ -173,6 +173,8 @@ export const agents = nylorun.table("agents", {
|
|
|
173
173
|
imageDigest: textC(),
|
|
174
174
|
/** The engine the image carries (ADR-99): `{ version, digest? }`, from a join of its engine. */
|
|
175
175
|
engineImage: jsonText(),
|
|
176
|
+
/** When its Agent Substrate worker last stopped under it (the sandboxes service's `drainedAt`). */
|
|
177
|
+
drainedAt: text(),
|
|
176
178
|
/**
|
|
177
179
|
* The disk lease (R2d step 3): `{ kind, version?, startedAt, until }` while a snapshot, a
|
|
178
180
|
* restore or a seed copies the disk; turns are refused until it ends or `until` passes.
|
|
@@ -284,6 +284,7 @@ function environmentOf(row) {
|
|
|
284
284
|
...(row.volumeId !== null ? { volumeId: row.volumeId } : {}),
|
|
285
285
|
...(row.imageDigest !== null ? { imageDigest: row.imageDigest } : {}),
|
|
286
286
|
...(row.engineImage ? { engineImage: row.engineImage } : {}),
|
|
287
|
+
...(row.drainedAt !== null ? { drainedAt: row.drainedAt } : {}),
|
|
287
288
|
},
|
|
288
289
|
}
|
|
289
290
|
: {}),
|
|
@@ -319,6 +320,7 @@ function podColumns(patch) {
|
|
|
319
320
|
volumeId: patch.volumeId,
|
|
320
321
|
imageDigest: patch.imageDigest,
|
|
321
322
|
engineImage: patch.engineImage,
|
|
323
|
+
drainedAt: patch.drainedAt,
|
|
322
324
|
};
|
|
323
325
|
return Object.fromEntries(Object.entries(columns).filter(([, value]) => value !== undefined));
|
|
324
326
|
}
|
package/dist/store/types.d.ts
CHANGED
|
@@ -269,6 +269,11 @@ export interface SandboxPodState {
|
|
|
269
269
|
version: string;
|
|
270
270
|
digest?: string;
|
|
271
271
|
};
|
|
272
|
+
/**
|
|
273
|
+
* When the sandbox was last suspended because its Agent Substrate worker stopped (the sandboxes
|
|
274
|
+
* service's `drainedAt`), so each drain is recorded once (`agent.suspended`, reason `drain`).
|
|
275
|
+
*/
|
|
276
|
+
drainedAt?: string;
|
|
272
277
|
}
|
|
273
278
|
/** A change to a pod's columns: `null` clears an optional one. */
|
|
274
279
|
export type SandboxPodPatch = {
|
package/dist/tenant/runtime.js
CHANGED
|
@@ -392,7 +392,20 @@ export class TenantRuntime {
|
|
|
392
392
|
return this.ctx.onSweep(hook);
|
|
393
393
|
}
|
|
394
394
|
attachHarness(channel, peer) {
|
|
395
|
-
|
|
395
|
+
const detach = this.ctx.harness.attach(channel, peer);
|
|
396
|
+
// A pod's engine that (re)connects asks for its sandbox's reconcile: after a drain (Agent
|
|
397
|
+
// Substrate's eviction contract) the restored engine redials with its host token and no join,
|
|
398
|
+
// and this is what lets the Runtime see the move (`drainedAt`, `agent.suspended` reason
|
|
399
|
+
// `drain`) at once rather than at the next turn or idle timer.
|
|
400
|
+
const sandboxId = peer.host?.sandboxId;
|
|
401
|
+
if (sandboxId !== undefined)
|
|
402
|
+
void this.ctx.store
|
|
403
|
+
.tx((t) => t.signalSandbox(sandboxId, { kind: "reconcile" }))
|
|
404
|
+
.catch((error) => this.ctx.config.logger.warn("sandbox reconcile after connect failed", {
|
|
405
|
+
sandboxId,
|
|
406
|
+
error: error instanceof Error ? error.message : String(error),
|
|
407
|
+
}));
|
|
408
|
+
return detach;
|
|
396
409
|
}
|
|
397
410
|
harnessStatus() {
|
|
398
411
|
return harnessStatusOf(this.ctx);
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const RUNTIME_VERSION = "0.
|
|
1
|
+
export declare const RUNTIME_VERSION = "0.51.1-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.51.1-beta";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nylorun/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.51.1-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",
|
|
@@ -61,39 +61,39 @@
|
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@ag-ui/core": "~1.0.4",
|
|
63
63
|
"@asteasolutions/zod-to-openapi": "^9.1.0",
|
|
64
|
-
"@earendil-works/pi-ai": "1.
|
|
65
|
-
"@hono/node-server": "^2.1.
|
|
64
|
+
"@earendil-works/pi-ai": "1.5.2",
|
|
65
|
+
"@hono/node-server": "^2.1.8",
|
|
66
66
|
"@hono/zod-openapi": "^1.6.3",
|
|
67
|
-
"@modelcontextprotocol/sdk": "^1.32.
|
|
68
|
-
"@nylorun/core": "0.
|
|
69
|
-
"@nylorun/harness": "0.36.
|
|
70
|
-
"@restatedev/restate-sdk": "1.
|
|
71
|
-
"@restatedev/restate-sdk-clients": "1.
|
|
72
|
-
"@s2-dev/streamstore": "0.
|
|
67
|
+
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
68
|
+
"@nylorun/core": "0.44.0-beta",
|
|
69
|
+
"@nylorun/harness": "0.36.4-beta",
|
|
70
|
+
"@restatedev/restate-sdk": "1.18.3",
|
|
71
|
+
"@restatedev/restate-sdk-clients": "1.18.3",
|
|
72
|
+
"@s2-dev/streamstore": "0.30.0",
|
|
73
73
|
"aws4fetch": "1.0.20",
|
|
74
74
|
"croner": "^9.1.0",
|
|
75
75
|
"drizzle-orm": "^0.45.4",
|
|
76
|
-
"hono": "^4.13.
|
|
77
|
-
"jose": "^6.2.
|
|
76
|
+
"hono": "^4.13.18",
|
|
77
|
+
"jose": "^6.2.17",
|
|
78
78
|
"just-bash": "3.6.0",
|
|
79
79
|
"pg": "8.23.1",
|
|
80
|
-
"pg-logical-replication": "2.
|
|
80
|
+
"pg-logical-replication": "2.6.0",
|
|
81
81
|
"postgres": "^3.4.9",
|
|
82
|
-
"ws": "^8.22.
|
|
82
|
+
"ws": "^8.22.2",
|
|
83
83
|
"yaml": "^2.9.1",
|
|
84
84
|
"zod": "^4.6.7"
|
|
85
85
|
},
|
|
86
86
|
"devDependencies": {
|
|
87
87
|
"@a2a-js/sdk": "~1.3.0",
|
|
88
88
|
"@ag-ui/client": "1.0.4",
|
|
89
|
-
"@scalar/openapi-parser": "^0.29.
|
|
90
|
-
"@types/node": "^26.6.
|
|
89
|
+
"@scalar/openapi-parser": "^0.29.21",
|
|
90
|
+
"@types/node": "^26.6.6",
|
|
91
91
|
"@types/pg": "^8.23.1",
|
|
92
92
|
"@types/ws": "^8.18.2",
|
|
93
93
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
94
94
|
"drizzle-kit": "^0.31.11",
|
|
95
95
|
"esbuild": "^0.28.2",
|
|
96
96
|
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
97
|
-
"vitest": "^5.0
|
|
97
|
+
"vitest": "^5.1.0"
|
|
98
98
|
}
|
|
99
99
|
}
|