@nylorun/runtime 0.46.2-beta → 0.47.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 +30 -0
- package/dist/api/a2a/routes.js +69 -13
- package/dist/api/a2a/tasks.d.ts +3 -3
- package/dist/api/a2a/tasks.js +4 -4
- package/dist/api/components.d.ts +30 -0
- package/dist/api/http/routes/tenant.js +16 -5
- package/dist/management-openapi.json +120 -5
- package/dist/openapi.json +1 -1
- package/dist/sandbox/pods/lifecycle.d.ts +12 -0
- package/dist/sandbox/pods/lifecycle.js +32 -2
- package/dist/sandbox/tenant-config.d.ts +6 -2
- package/dist/sandbox/tenant-config.js +3 -2
- package/dist/tenant/agents.js +5 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.47.1-beta
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 3a8a080: Pods on Agent Substrate (preview): a crash that an idle suspend found now makes the Agent `failing`, and its `attention` names the crash (`compute crashed: Crashed: …`). Before, the crash appeared only in the Agent's events, and the Agent showed as sleeping. The entry stays until a turn starts the pod again, which reverts the sandbox to its last suspend.
|
|
8
|
+
|
|
9
|
+
## 0.47.0-beta
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- fe118db: The Tenant's sandbox settings round-trip, and `engine.images` comes back (#588).
|
|
14
|
+
|
|
15
|
+
- **Breaking (`@nylorun/runtime`, `@nylorun/core`, `@nylorun/admin`): `config` is the settings as stored.** `GET` and `PUT /v1/tenant/sandbox` (`admin.settings.sandbox`) answer `config` as the Tenant set it, with no defaults, in exactly the shape `PUT` takes (`limits.resources.memory: "8GiB"`), so `put(get().config)` changes nothing and saves no default as a setting. The settings in force, every default filled in and sizes in MiB (`memoryMiB`), are `effective`, which is what `config` was. Before, `PUT` refused what `GET` answered (`unrecognized_keys "memoryMiB"`). Read the settings in force from `effective` (`view.effective.limits.resources.memoryMiB`), and build a `PUT` body from `view.config`. `TenantSandboxViewSchema` has `config: TenantEnvironmentsConfigSchema` and `effective: EffectiveSandboxConfigSchema`.
|
|
16
|
+
- **`engine.images` is answered.** A `PUT` with `engine.images` was stored and used, but left out of its answer and of the next `GET`. Both now carry it, in `config` as set and in `effective` (`[]` when unset).
|
|
17
|
+
- `@nylorun/studio`: Edit compute settings puts the stored settings with only the fields it changed, so it no longer erases `engine.images` set through the API, keeps `placement`, and saves no default as a setting; clearing idle, stop grace or most Agents puts back the default. The Compute tab lists the engine images.
|
|
18
|
+
- `nylorun`: `nylorun tenant settings sandbox put` takes what `get` printed and puts back its `config`. `nylorun doctor sandbox` reads the limits in force from `effective`, or from `config` on an older Runtime.
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- fe118db: An A2A `SendMessage` with `returnImmediately` to a busy context answers `TASK_STATE_SUBMITTED` (#586), and `CancelTask` on a queued task answers `TASK_STATE_CANCELED` (#585), instead of `TaskNotFoundError` while the session's stream still lags behind the record. Both replies now wait, up to 5 s, for the stream to hold the request's own write, and a queued task's `GetTask` answers `SUBMITTED` from its queue row until then.
|
|
23
|
+
- 71c6741: Pods on Agent Substrate (preview): a crashed sandbox is now recorded in the Agent's events. A running pod whose sandbox crashed is `failed` at once, rather than after the 5-minute ready deadline. A crash that an idle suspend finds is also recorded, where before the Agent showed only `agent.suspended`. In both cases `agent.failed` gives Substrate's crash message and says that what the Agent wrote since its last suspend is lost. The next turn reverts the sandbox to that suspend, as before. The sandboxes service now reports the crash's own message in place of the whole crash record.
|
|
24
|
+
- fe118db: A message's `removeTools` turns a Runtime plugin's tool off by its own name too: `memory` now does what `nylorun__memory` does, so the name events and `check --explain` showed no longer leaves the tool on without a word (#589). `nylorun check --explain` lists a plugin's tools by the names the model reads (`nylorun__memory`, `nylorun__update_plan`), the ones `removeTools` and tool policy take, and its memory note says the index is pinned again at a turn after a write from elsewhere (#590, #591).
|
|
25
|
+
- Pin core to the tested release.
|
|
26
|
+
- Pin harness to the tested release.
|
|
27
|
+
- Updated dependencies [fe118db]
|
|
28
|
+
- Updated dependencies [fe118db]
|
|
29
|
+
- Updated dependencies
|
|
30
|
+
- @nylorun/core@0.41.0-beta
|
|
31
|
+
- @nylorun/harness@0.36.1-beta
|
|
32
|
+
|
|
3
33
|
## 0.46.2-beta
|
|
4
34
|
|
|
5
35
|
### Patch Changes
|
package/dist/api/a2a/routes.js
CHANGED
|
@@ -44,7 +44,7 @@ import { A2aError, a2aFail, checkVersion, jsonRpcError, jsonRpcResult, parseCrea
|
|
|
44
44
|
import { configView, kickPushes, pushEnabled, pushPolicy, savePushConfig, } from "./push.js";
|
|
45
45
|
import { ENDED, SETTLED_EVENTS, buildTask, callerTraceId, contextSessionId, droppedTask, endsQueued, newContextId, parseTaskId, pendingInteraction, pendingInteractions, submittedTask, taskIdOf, } from "./tasks.js";
|
|
46
46
|
import { currentModelDescriptor } from "../../model/descriptor.js";
|
|
47
|
-
import { decodeCursor } from "../../record/cursor.js";
|
|
47
|
+
import { decodeCursor, encodeCursor } from "../../record/cursor.js";
|
|
48
48
|
import { accessOf } from "../../tenant/auth.js";
|
|
49
49
|
import { cancelQueuedMessage, command } from "../../tenant/commands.js";
|
|
50
50
|
import { loadSession, reachesAgent, } from "../../tenant/context.js";
|
|
@@ -218,7 +218,8 @@ async function indexedTask(caller, row, session, options = {}) {
|
|
|
218
218
|
* index keeps the queued messages each turn took) the task is that turn's, under the id the
|
|
219
219
|
* caller knows. A queued message that failed or was cancelled before a turn took it (Host feature
|
|
220
220
|
* `queue-recovery`) is `FAILED` or `CANCELED`, from the event that ended it. Only a message still
|
|
221
|
-
* in the queue, or no longer in it and taken by no turn, is looked for in the session's history
|
|
221
|
+
* in the queue, or no longer in it and taken by no turn, is looked for in the session's history;
|
|
222
|
+
* one still in the queue is `SUBMITTED` from its row while the stream lags behind the record.
|
|
222
223
|
*/
|
|
223
224
|
async function ownedTask(caller, taskId, historyLength) {
|
|
224
225
|
const ids = parseTaskId(taskId) ?? taskNotFound(taskId);
|
|
@@ -229,8 +230,9 @@ async function ownedTask(caller, taskId, historyLength) {
|
|
|
229
230
|
if (turn)
|
|
230
231
|
return { turn };
|
|
231
232
|
// Still queued, or taken: the queue is read first, so a message taken meanwhile is found.
|
|
232
|
-
|
|
233
|
-
|
|
233
|
+
const row = (await t.queuedMessages(sessionId)).find((row) => row.id === ids.turnId);
|
|
234
|
+
if (row)
|
|
235
|
+
return { row };
|
|
234
236
|
const taken = await t.turnOfQueuedMessage(sessionId, ids.turnId);
|
|
235
237
|
return taken ? { turn: taken } : {};
|
|
236
238
|
});
|
|
@@ -242,8 +244,10 @@ async function ownedTask(caller, taskId, historyLength) {
|
|
|
242
244
|
const owned = { session, sessionId, contextId: ids.contextId, turnId: ids.turnId, events: [] };
|
|
243
245
|
if (dropped)
|
|
244
246
|
return { ...owned, task: droppedTask(ids.contextId, taskId, dropped), dropped };
|
|
245
|
-
if (
|
|
246
|
-
|
|
247
|
+
if (found.row) {
|
|
248
|
+
const since = queued?.time ?? found.row.createdAt;
|
|
249
|
+
return { ...owned, task: submittedTask(ids.contextId, taskId, since), queued: queued ?? found.row };
|
|
250
|
+
}
|
|
247
251
|
}
|
|
248
252
|
const row = found.turn ?? taskNotFound(taskId);
|
|
249
253
|
const built = (await indexedTask(caller, { ...row, a2aContextId: ids.contextId }, session, {
|
|
@@ -361,7 +365,10 @@ async function sendMessage(caller, params, signal) {
|
|
|
361
365
|
nextSeq: decodeCursor(sessionId, accepted.cursor),
|
|
362
366
|
...(accepted.turnId !== undefined ? { traceId: await sendTraceId(caller, sessionId, accepted.turnId) } : {}),
|
|
363
367
|
});
|
|
364
|
-
|
|
368
|
+
// The task is read from the stream: it must hold the message this request wrote.
|
|
369
|
+
if (send.returnImmediately)
|
|
370
|
+
await caughtUp(caller.ctx, sessionId, accepted.cursor, signal);
|
|
371
|
+
else
|
|
365
372
|
await settled(caller.ctx, sessionId, accepted, signal);
|
|
366
373
|
const { task } = await ownedTask(caller, taskId, send.historyLength);
|
|
367
374
|
return { task };
|
|
@@ -509,17 +516,30 @@ function replyTarget(owned) {
|
|
|
509
516
|
/**
|
|
510
517
|
* `CancelTask`. A queued task (`SUBMITTED`) leaves its context's queue and is `CANCELED`
|
|
511
518
|
* (`queue.cancelled`, pushed), whether a task runs in the context or not (Host feature
|
|
512
|
-
* `queue-recovery`); one a turn took meanwhile is cancelled as that turn.
|
|
513
|
-
*
|
|
519
|
+
* `queue-recovery`); one a turn took meanwhile is cancelled as that turn. The answer waits for
|
|
520
|
+
* the stream to hold the `queue.cancelled` it wrote (`caughtUp`). A running or waiting task's turn
|
|
521
|
+
* is cancelled. An ended task is not cancelable, unless it is `CANCELED` already.
|
|
514
522
|
*/
|
|
515
523
|
async function cancelTask(caller, taskId, again = true) {
|
|
516
524
|
const owned = await ownedTask(caller, taskId);
|
|
517
525
|
if (owned.queued) {
|
|
518
526
|
const cancelled = await cancelQueuedMessage(caller.ctx, owned.sessionId, owned.turnId, caller.scope, "Canceled by the A2A client");
|
|
519
527
|
// Gone from the queue meanwhile: a turn took it, or it failed. Answer as the task is now.
|
|
520
|
-
if (!cancelled
|
|
521
|
-
return cancelTask(caller, taskId, false);
|
|
522
|
-
|
|
528
|
+
if (!cancelled)
|
|
529
|
+
return again ? cancelTask(caller, taskId, false) : (await ownedTask(caller, taskId)).task;
|
|
530
|
+
await caughtUp(caller.ctx, owned.sessionId, cancelled.cursor);
|
|
531
|
+
const after = await ownedTask(caller, taskId).catch((error) => {
|
|
532
|
+
if (error instanceof A2aError && error.kind === "taskNotFound")
|
|
533
|
+
return undefined;
|
|
534
|
+
throw error;
|
|
535
|
+
});
|
|
536
|
+
// The stream still lags behind `queue.cancelled`: the cancel committed, so it is CANCELED.
|
|
537
|
+
return (after?.task ?? {
|
|
538
|
+
id: taskId,
|
|
539
|
+
contextId: owned.contextId,
|
|
540
|
+
status: { state: "TASK_STATE_CANCELED", timestamp: new Date().toISOString() },
|
|
541
|
+
history: [],
|
|
542
|
+
});
|
|
523
543
|
}
|
|
524
544
|
if (owned.session.activeTurnId !== owned.turnId) {
|
|
525
545
|
if (owned.task.status.state === "TASK_STATE_CANCELED")
|
|
@@ -577,7 +597,9 @@ async function createPushConfig(caller, params) {
|
|
|
577
597
|
requirePush(caller);
|
|
578
598
|
const { taskId, config } = parseCreatePushParams(params);
|
|
579
599
|
const owned = await ownedTask(caller, taskId);
|
|
580
|
-
|
|
600
|
+
// A queued task the stream has not caught up with is followed from the session's start.
|
|
601
|
+
const queued = owned.queued && "type" in owned.queued ? owned.queued : undefined;
|
|
602
|
+
const last = owned.dropped ?? queued ?? owned.events.at(-1);
|
|
581
603
|
const ended = owned.dropped !== undefined || owned.events.some((event) => ENDED.has(event.type));
|
|
582
604
|
const configId = config.id ?? createHash("sha256").update(`${config.url}\u0000${config.token ?? ""}`).digest("hex").slice(0, 16);
|
|
583
605
|
return registerPush(caller, { ...config, id: configId }, {
|
|
@@ -622,6 +644,40 @@ async function deletePushConfig(caller, params) {
|
|
|
622
644
|
await caller.ctx.store.tx((t) => t.deletePushConfig(owned.sessionId, owned.task.id, ref.id));
|
|
623
645
|
return {};
|
|
624
646
|
}
|
|
647
|
+
/** How long a reply waits for the session's stream to hold the request's own write. */
|
|
648
|
+
const OWN_WRITE_WAIT_MS = 5_000;
|
|
649
|
+
/**
|
|
650
|
+
* Waits until the session's stream holds the event at `cursor`, which this request wrote: the
|
|
651
|
+
* relay copies it from the record a little after the commit, and replies are read from the
|
|
652
|
+
* stream. Ends after `OWN_WRITE_WAIT_MS`, when the caller hangs up, or when the feed ends; the
|
|
653
|
+
* reply then says what the stream and the store say.
|
|
654
|
+
*/
|
|
655
|
+
async function caughtUp(ctx, sessionId, cursor, signal) {
|
|
656
|
+
const seq = decodeCursor(sessionId, cursor);
|
|
657
|
+
const stop = new AbortController();
|
|
658
|
+
const timer = setTimeout(() => stop.abort(), OWN_WRITE_WAIT_MS);
|
|
659
|
+
timer.unref();
|
|
660
|
+
const hangup = () => stop.abort();
|
|
661
|
+
signal?.addEventListener("abort", hangup, { once: true });
|
|
662
|
+
if (signal?.aborted)
|
|
663
|
+
stop.abort();
|
|
664
|
+
try {
|
|
665
|
+
// The feed yields what follows a cursor: start from the event before.
|
|
666
|
+
const before = seq > 0 ? encodeCursor(sessionId, seq - 1) : undefined;
|
|
667
|
+
for await (const event of observeSession(ctx, sessionId, before, { signal: stop.signal }))
|
|
668
|
+
if (event.seq >= seq)
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
catch (error) {
|
|
672
|
+
// A session or streams gone: the reply says so.
|
|
673
|
+
if (!(error instanceof HttpError))
|
|
674
|
+
throw error;
|
|
675
|
+
}
|
|
676
|
+
finally {
|
|
677
|
+
clearTimeout(timer);
|
|
678
|
+
signal?.removeEventListener("abort", hangup);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
625
681
|
/**
|
|
626
682
|
* Waits until the task's turn ends, pauses or has an uncertain effect, the caller hangs up, or
|
|
627
683
|
* the Tenant's Runtime limit `a2aWaitMs` passes: after it, the task is returned as it is
|
package/dist/api/a2a/tasks.d.ts
CHANGED
|
@@ -121,10 +121,10 @@ export interface BuildTaskInput {
|
|
|
121
121
|
*/
|
|
122
122
|
export declare function buildTask(input: BuildTaskInput): Task | undefined;
|
|
123
123
|
/**
|
|
124
|
-
* A queued message's task while it waits (profile v2 §1): `SUBMITTED
|
|
125
|
-
* event, until a turn takes it.
|
|
124
|
+
* A queued message's task while it waits (profile v2 §1): `SUBMITTED` since `queuedAt` (its
|
|
125
|
+
* `message.queued` event's time, or its queue row's), until a turn takes it.
|
|
126
126
|
*/
|
|
127
|
-
export declare function submittedTask(contextId: string, taskId: string,
|
|
127
|
+
export declare function submittedTask(contextId: string, taskId: string, queuedAt: string): Task;
|
|
128
128
|
/**
|
|
129
129
|
* A queued message's task that ended without a turn (Host feature `queue-recovery`), from the
|
|
130
130
|
* `queue.failed` or `queue.cancelled` event that names it: `FAILED`, saying why it never started,
|
package/dist/api/a2a/tasks.js
CHANGED
|
@@ -488,14 +488,14 @@ export function buildTask(input) {
|
|
|
488
488
|
};
|
|
489
489
|
}
|
|
490
490
|
/**
|
|
491
|
-
* A queued message's task while it waits (profile v2 §1): `SUBMITTED
|
|
492
|
-
* event, until a turn takes it.
|
|
491
|
+
* A queued message's task while it waits (profile v2 §1): `SUBMITTED` since `queuedAt` (its
|
|
492
|
+
* `message.queued` event's time, or its queue row's), until a turn takes it.
|
|
493
493
|
*/
|
|
494
|
-
export function submittedTask(contextId, taskId,
|
|
494
|
+
export function submittedTask(contextId, taskId, queuedAt) {
|
|
495
495
|
return {
|
|
496
496
|
id: taskId,
|
|
497
497
|
contextId,
|
|
498
|
-
status: { state: "TASK_STATE_SUBMITTED", timestamp:
|
|
498
|
+
status: { state: "TASK_STATE_SUBMITTED", timestamp: queuedAt },
|
|
499
499
|
history: [],
|
|
500
500
|
};
|
|
501
501
|
}
|
package/dist/api/components.d.ts
CHANGED
|
@@ -15896,6 +15896,33 @@ export declare const TenantSandboxView: z.ZodObject<{
|
|
|
15896
15896
|
}, z.core.$strict>>;
|
|
15897
15897
|
defaultImage: z.ZodString;
|
|
15898
15898
|
config: z.ZodObject<{
|
|
15899
|
+
limits: z.ZodOptional<z.ZodObject<{
|
|
15900
|
+
network: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
15901
|
+
resources: z.ZodOptional<z.ZodObject<{
|
|
15902
|
+
cpus: z.ZodOptional<z.ZodNumber>;
|
|
15903
|
+
memory: z.ZodOptional<z.ZodString>;
|
|
15904
|
+
}, z.core.$strict>>;
|
|
15905
|
+
defaultResources: z.ZodOptional<z.ZodObject<{
|
|
15906
|
+
cpus: z.ZodOptional<z.ZodNumber>;
|
|
15907
|
+
memory: z.ZodOptional<z.ZodString>;
|
|
15908
|
+
}, z.core.$strict>>;
|
|
15909
|
+
idle: z.ZodOptional<z.ZodString>;
|
|
15910
|
+
environments: z.ZodOptional<z.ZodNumber>;
|
|
15911
|
+
}, z.core.$strict>>;
|
|
15912
|
+
lifecycle: z.ZodOptional<z.ZodObject<{
|
|
15913
|
+
stopGrace: z.ZodOptional<z.ZodString>;
|
|
15914
|
+
}, z.core.$strict>>;
|
|
15915
|
+
engine: z.ZodOptional<z.ZodObject<{
|
|
15916
|
+
images: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
15917
|
+
}, z.core.$strict>>;
|
|
15918
|
+
placement: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
15919
|
+
hosts: z.ZodArray<z.ZodEnum<{
|
|
15920
|
+
"harness-container": "harness-container";
|
|
15921
|
+
sandbox: "sandbox";
|
|
15922
|
+
}>>;
|
|
15923
|
+
}, z.core.$strict>>>;
|
|
15924
|
+
}, z.core.$strict>;
|
|
15925
|
+
effective: z.ZodObject<{
|
|
15899
15926
|
limits: z.ZodObject<{
|
|
15900
15927
|
network: z.ZodArray<z.ZodString>;
|
|
15901
15928
|
resources: z.ZodObject<{
|
|
@@ -15918,6 +15945,9 @@ export declare const TenantSandboxView: z.ZodObject<{
|
|
|
15918
15945
|
sandbox: "sandbox";
|
|
15919
15946
|
}>>;
|
|
15920
15947
|
}, z.core.$strict>>>;
|
|
15948
|
+
engine: z.ZodOptional<z.ZodObject<{
|
|
15949
|
+
images: z.ZodArray<z.ZodString>;
|
|
15950
|
+
}, z.core.$strict>>;
|
|
15921
15951
|
}, z.core.$strict>;
|
|
15922
15952
|
cluster: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
15923
15953
|
namespace: z.ZodString;
|
|
@@ -50,15 +50,21 @@ const budgetView = (row) => ({
|
|
|
50
50
|
...(row.limitUsd !== null ? { limitUsd: row.limitUsd } : {}),
|
|
51
51
|
...(row.limitTokens !== null ? { limitTokens: row.limitTokens } : {}),
|
|
52
52
|
});
|
|
53
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* `GET /v1/tenant/sandbox`: the backend report, the settings as stored (`config`, what `PUT`
|
|
55
|
+
* takes, so a client can put it back changed) and the configuration in force (`effective`).
|
|
56
|
+
*/
|
|
54
57
|
async function sandboxView(ctx) {
|
|
55
58
|
const config = await ctx.store.tx((t) => readSandboxConfig(t));
|
|
56
59
|
return {
|
|
57
60
|
...(await ctx.sandbox.report()),
|
|
58
|
-
config
|
|
61
|
+
config,
|
|
62
|
+
effective: effectiveSandboxConfig(config),
|
|
59
63
|
cluster: await clusterOf(ctx),
|
|
60
64
|
};
|
|
61
65
|
}
|
|
66
|
+
/** What the sandbox routes answer, for their descriptions. */
|
|
67
|
+
const SANDBOX_VIEW_TEXT = "`config` is the Tenant's settings as stored: only what it set, in exactly the shape `PUT` takes, so a client changes one setting by putting `config` back with that change. `effective` is the configuration in force: `config` with every default filled in, sizes in MiB (`memoryMiB`).";
|
|
62
68
|
/** The Tenant's cluster for pod sandboxes (F7.2), from the sandboxes service; null without one. */
|
|
63
69
|
async function clusterOf(ctx) {
|
|
64
70
|
if (!ctx.pods)
|
|
@@ -224,16 +230,21 @@ export function tenantRoutes(api) {
|
|
|
224
230
|
path: "/v1/tenant/sandbox",
|
|
225
231
|
tags: ["Tenant"],
|
|
226
232
|
summary: "Get the Tenant's sandbox backend and Agent compute settings",
|
|
227
|
-
|
|
233
|
+
description: SANDBOX_VIEW_TEXT,
|
|
234
|
+
responses: {
|
|
235
|
+
200: json(TenantSandboxView, "The backend in use, the settings as stored and the configuration in force"),
|
|
236
|
+
},
|
|
228
237
|
}, async (c) => jsonResponse(200, await sandboxView(c.env.tenant)));
|
|
229
238
|
tenantRoute(api, MANAGEMENT, {
|
|
230
239
|
method: "put",
|
|
231
240
|
path: "/v1/tenant/sandbox",
|
|
232
241
|
tags: ["Tenant"],
|
|
233
242
|
summary: "Set the Tenant's Agent compute settings",
|
|
234
|
-
description:
|
|
243
|
+
description: `Replaces the Tenant's settings (Tenant setting \`environments.config\`): the limits every Agent's runtime must fit, the pods' stop grace, the images that carry the engine (\`engine.images\`) and where harnesses may run. A setting left out takes its default. Answers like \`GET\`: ${SANDBOX_VIEW_TEXT}`,
|
|
235
244
|
request: { body: body(PutTenantEnvironmentsRequest) },
|
|
236
|
-
responses: {
|
|
245
|
+
responses: {
|
|
246
|
+
200: json(TenantSandboxView, "The settings now stored and the configuration now in force"),
|
|
247
|
+
},
|
|
237
248
|
}, async (c) => {
|
|
238
249
|
const ctx = c.env.tenant;
|
|
239
250
|
const { requestId: _requestId, ...config } = PutTenantEnvironmentsRequestSchema.parse(await readJson(c));
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.2.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Nylorun Management API",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.47.1-beta",
|
|
6
6
|
"x-nylorun-protocol": {
|
|
7
7
|
"min": 16,
|
|
8
8
|
"max": 18,
|
|
@@ -2144,6 +2144,104 @@
|
|
|
2144
2144
|
"type": "string"
|
|
2145
2145
|
},
|
|
2146
2146
|
"config": {
|
|
2147
|
+
"type": "object",
|
|
2148
|
+
"properties": {
|
|
2149
|
+
"limits": {
|
|
2150
|
+
"type": "object",
|
|
2151
|
+
"properties": {
|
|
2152
|
+
"network": {
|
|
2153
|
+
"type": "array",
|
|
2154
|
+
"items": {
|
|
2155
|
+
"type": "string"
|
|
2156
|
+
}
|
|
2157
|
+
},
|
|
2158
|
+
"resources": {
|
|
2159
|
+
"type": "object",
|
|
2160
|
+
"properties": {
|
|
2161
|
+
"cpus": {
|
|
2162
|
+
"type": "integer",
|
|
2163
|
+
"minimum": 1
|
|
2164
|
+
},
|
|
2165
|
+
"memory": {
|
|
2166
|
+
"type": "string"
|
|
2167
|
+
}
|
|
2168
|
+
},
|
|
2169
|
+
"additionalProperties": false
|
|
2170
|
+
},
|
|
2171
|
+
"defaultResources": {
|
|
2172
|
+
"type": "object",
|
|
2173
|
+
"properties": {
|
|
2174
|
+
"cpus": {
|
|
2175
|
+
"type": "integer",
|
|
2176
|
+
"minimum": 1
|
|
2177
|
+
},
|
|
2178
|
+
"memory": {
|
|
2179
|
+
"type": "string"
|
|
2180
|
+
}
|
|
2181
|
+
},
|
|
2182
|
+
"additionalProperties": false
|
|
2183
|
+
},
|
|
2184
|
+
"idle": {
|
|
2185
|
+
"type": "string"
|
|
2186
|
+
},
|
|
2187
|
+
"environments": {
|
|
2188
|
+
"type": "integer",
|
|
2189
|
+
"minimum": 0
|
|
2190
|
+
}
|
|
2191
|
+
},
|
|
2192
|
+
"additionalProperties": false
|
|
2193
|
+
},
|
|
2194
|
+
"lifecycle": {
|
|
2195
|
+
"type": "object",
|
|
2196
|
+
"properties": {
|
|
2197
|
+
"stopGrace": {
|
|
2198
|
+
"type": "string"
|
|
2199
|
+
}
|
|
2200
|
+
},
|
|
2201
|
+
"additionalProperties": false
|
|
2202
|
+
},
|
|
2203
|
+
"engine": {
|
|
2204
|
+
"type": "object",
|
|
2205
|
+
"properties": {
|
|
2206
|
+
"images": {
|
|
2207
|
+
"type": "array",
|
|
2208
|
+
"items": {
|
|
2209
|
+
"type": "string",
|
|
2210
|
+
"minLength": 1,
|
|
2211
|
+
"maxLength": 256,
|
|
2212
|
+
"pattern": "^[a-z0-9]([a-z0-9._:/-]*[a-z0-9])?$"
|
|
2213
|
+
},
|
|
2214
|
+
"maxItems": 32
|
|
2215
|
+
}
|
|
2216
|
+
},
|
|
2217
|
+
"additionalProperties": false
|
|
2218
|
+
},
|
|
2219
|
+
"placement": {
|
|
2220
|
+
"type": "object",
|
|
2221
|
+
"additionalProperties": {
|
|
2222
|
+
"type": "object",
|
|
2223
|
+
"properties": {
|
|
2224
|
+
"hosts": {
|
|
2225
|
+
"type": "array",
|
|
2226
|
+
"items": {
|
|
2227
|
+
"type": "string",
|
|
2228
|
+
"enum": [
|
|
2229
|
+
"harness-container",
|
|
2230
|
+
"sandbox"
|
|
2231
|
+
]
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
},
|
|
2235
|
+
"required": [
|
|
2236
|
+
"hosts"
|
|
2237
|
+
],
|
|
2238
|
+
"additionalProperties": false
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
},
|
|
2242
|
+
"additionalProperties": false
|
|
2243
|
+
},
|
|
2244
|
+
"effective": {
|
|
2147
2245
|
"type": "object",
|
|
2148
2246
|
"properties": {
|
|
2149
2247
|
"limits": {
|
|
@@ -2236,6 +2334,21 @@
|
|
|
2236
2334
|
],
|
|
2237
2335
|
"additionalProperties": false
|
|
2238
2336
|
}
|
|
2337
|
+
},
|
|
2338
|
+
"engine": {
|
|
2339
|
+
"type": "object",
|
|
2340
|
+
"properties": {
|
|
2341
|
+
"images": {
|
|
2342
|
+
"type": "array",
|
|
2343
|
+
"items": {
|
|
2344
|
+
"type": "string"
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
},
|
|
2348
|
+
"required": [
|
|
2349
|
+
"images"
|
|
2350
|
+
],
|
|
2351
|
+
"additionalProperties": false
|
|
2239
2352
|
}
|
|
2240
2353
|
},
|
|
2241
2354
|
"required": [
|
|
@@ -2300,7 +2413,8 @@
|
|
|
2300
2413
|
"reason",
|
|
2301
2414
|
"probes",
|
|
2302
2415
|
"defaultImage",
|
|
2303
|
-
"config"
|
|
2416
|
+
"config",
|
|
2417
|
+
"effective"
|
|
2304
2418
|
],
|
|
2305
2419
|
"additionalProperties": false
|
|
2306
2420
|
},
|
|
@@ -6841,6 +6955,7 @@
|
|
|
6841
6955
|
"Settings"
|
|
6842
6956
|
],
|
|
6843
6957
|
"summary": "Get the Tenant's sandbox backend and Agent compute settings",
|
|
6958
|
+
"description": "`config` is the Tenant's settings as stored: only what it set, in exactly the shape `PUT` takes, so a client changes one setting by putting `config` back with that change. `effective` is the configuration in force: `config` with every default filled in, sizes in MiB (`memoryMiB`).",
|
|
6844
6959
|
"security": [
|
|
6845
6960
|
{
|
|
6846
6961
|
"managementKey": []
|
|
@@ -6864,7 +6979,7 @@
|
|
|
6864
6979
|
],
|
|
6865
6980
|
"responses": {
|
|
6866
6981
|
"200": {
|
|
6867
|
-
"description": "The backend in use and the configuration in force",
|
|
6982
|
+
"description": "The backend in use, the settings as stored and the configuration in force",
|
|
6868
6983
|
"content": {
|
|
6869
6984
|
"application/json": {
|
|
6870
6985
|
"schema": {
|
|
@@ -6940,7 +7055,7 @@
|
|
|
6940
7055
|
"Settings"
|
|
6941
7056
|
],
|
|
6942
7057
|
"summary": "Set the Tenant's Agent compute settings",
|
|
6943
|
-
"description": "
|
|
7058
|
+
"description": "Replaces the Tenant's settings (Tenant setting `environments.config`): the limits every Agent's runtime must fit, the pods' stop grace, the images that carry the engine (`engine.images`) and where harnesses may run. A setting left out takes its default. Answers like `GET`: `config` is the Tenant's settings as stored: only what it set, in exactly the shape `PUT` takes, so a client changes one setting by putting `config` back with that change. `effective` is the configuration in force: `config` with every default filled in, sizes in MiB (`memoryMiB`).",
|
|
6944
7059
|
"security": [
|
|
6945
7060
|
{
|
|
6946
7061
|
"managementKey": []
|
|
@@ -6974,7 +7089,7 @@
|
|
|
6974
7089
|
},
|
|
6975
7090
|
"responses": {
|
|
6976
7091
|
"200": {
|
|
6977
|
-
"description": "The configuration now in force",
|
|
7092
|
+
"description": "The settings now stored and the configuration now in force",
|
|
6978
7093
|
"content": {
|
|
6979
7094
|
"application/json": {
|
|
6980
7095
|
"schema": {
|
package/dist/openapi.json
CHANGED
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
* ran on for days. A timer that fires during a turn looks again one idle period later.
|
|
22
22
|
* Becoming ready moves `lastActiveAt` to that moment, so a slow start doesn't use up the
|
|
23
23
|
* idle period before the pod can take a turn.
|
|
24
|
+
* - **Crashed** (Agent Substrate): a running pod whose sandbox crashed is `failed` at once
|
|
25
|
+
* (`agent.failed`, saying what the Agent lost); a crash an idle suspend finds is recorded the
|
|
26
|
+
* same way. The next turn start asks for `Running`, and the sandboxes service reverts the
|
|
27
|
+
* sandbox to its last suspend before resuming it.
|
|
24
28
|
* - **Lost**: an incarnation that has run (a pod joined) whose Sandbox is gone or whose volume
|
|
25
29
|
* is missing is `lost` (`agent.lost`): its host epoch moves, its Sandbox is deleted, and
|
|
26
30
|
* only a reset brings the Environment back. agent-sandbox recreates a deleted claim empty, so
|
|
@@ -58,6 +62,14 @@ export declare function repositoryOf(reference: string): string;
|
|
|
58
62
|
export declare function imageDigestOf(imageID: string | undefined, requested: string): string | undefined;
|
|
59
63
|
/** A setup failure's `reason` starts with this (`sandbox/setup.ts`); it is sticky. */
|
|
60
64
|
export declare const SETUP_FAILED = "setup:";
|
|
65
|
+
/** A crash's `reason` (see `crashReasonOf`) starts with this; it stays until a turn starts the pod. */
|
|
66
|
+
export declare const CRASHED = "Crashed: ";
|
|
67
|
+
/**
|
|
68
|
+
* A sandbox that crashed (Agent Substrate: its worker went away, or a wake failed) lost what its
|
|
69
|
+
* Agent wrote since its last suspend; the next start reverts it to that suspend. The reason
|
|
70
|
+
* `agent.failed` records, or undefined when the sandbox has not crashed.
|
|
71
|
+
*/
|
|
72
|
+
export declare function crashReasonOf(status: PodStatus): string | undefined;
|
|
61
73
|
/** The lifecycle settings (D36), read each time a decision is made. */
|
|
62
74
|
export interface PodLifecycleConfig {
|
|
63
75
|
readonly idleMs: number;
|
|
@@ -36,6 +36,19 @@ export function imageDigestOf(imageID, requested) {
|
|
|
36
36
|
}
|
|
37
37
|
/** A setup failure's `reason` starts with this (`sandbox/setup.ts`); it is sticky. */
|
|
38
38
|
export const SETUP_FAILED = "setup:";
|
|
39
|
+
/** A crash's `reason` (see `crashReasonOf`) starts with this; it stays until a turn starts the pod. */
|
|
40
|
+
export const CRASHED = "Crashed: ";
|
|
41
|
+
/**
|
|
42
|
+
* A sandbox that crashed (Agent Substrate: its worker went away, or a wake failed) lost what its
|
|
43
|
+
* Agent wrote since its last suspend; the next start reverts it to that suspend. The reason
|
|
44
|
+
* `agent.failed` records, or undefined when the sandbox has not crashed.
|
|
45
|
+
*/
|
|
46
|
+
export function crashReasonOf(status) {
|
|
47
|
+
if (status.reason !== "Crashed")
|
|
48
|
+
return undefined;
|
|
49
|
+
const what = status.message?.trim() || "the sandbox stopped";
|
|
50
|
+
return `${CRASHED}${what}. What the Agent wrote since its last suspend is lost; its next turn starts it from that suspend.`;
|
|
51
|
+
}
|
|
39
52
|
/** The service's operation id of the row's current spec. */
|
|
40
53
|
export function opIdOf(pod) {
|
|
41
54
|
return `${pod.k8sName}.r${pod.rev}`;
|
|
@@ -195,9 +208,19 @@ export function decide(facts) {
|
|
|
195
208
|
const startedAt = Date.parse(pod.startedAt ?? row.updatedAt);
|
|
196
209
|
if (pod.observed === "failed")
|
|
197
210
|
return done();
|
|
211
|
+
// A pod that was running and crashed will not come back by itself: failed now, not at the
|
|
212
|
+
// ready deadline. One still starting is left to its wake, which reverts a crashed sandbox.
|
|
213
|
+
const crash = crashReasonOf(status);
|
|
214
|
+
if (crash !== undefined && pod.observed === "running") {
|
|
215
|
+
patch.observed = "failed";
|
|
216
|
+
patch.reason = crash;
|
|
217
|
+
events.push(event("agent.failed", { reason: crash }));
|
|
218
|
+
return done();
|
|
219
|
+
}
|
|
198
220
|
if (now - startedAt >= readyDeadlineMs) {
|
|
199
|
-
const reason =
|
|
200
|
-
|
|
221
|
+
const reason = crash ??
|
|
222
|
+
([status.reason, status.message].filter(Boolean).join(": ") ||
|
|
223
|
+
`The pod was not ready within ${Math.round(readyDeadlineMs / 1000)} s`);
|
|
201
224
|
patch.observed = "failed";
|
|
202
225
|
patch.reason = reason;
|
|
203
226
|
events.push(event("agent.failed", { reason }));
|
|
@@ -212,6 +235,13 @@ export function decide(facts) {
|
|
|
212
235
|
if (status.suspended || !status.podPhase) {
|
|
213
236
|
if (pod.observed !== "suspended")
|
|
214
237
|
patch.observed = "suspended";
|
|
238
|
+
// A suspend can find the sandbox already crashed (the idle timer fired before anything saw
|
|
239
|
+
// the crash): record the crash once, so its owner learns what it lost.
|
|
240
|
+
const crash = crashReasonOf(status);
|
|
241
|
+
if (crash !== undefined && pod.reason !== crash) {
|
|
242
|
+
patch.reason = crash;
|
|
243
|
+
events.push(event("agent.failed", { reason: crash }));
|
|
244
|
+
}
|
|
215
245
|
return done();
|
|
216
246
|
}
|
|
217
247
|
again(pollMs);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A Tenant's Environment settings, stored as Tenant setting `environments.config`: the limits
|
|
3
|
-
* every Environment's recipe must fit, the pods' stop grace
|
|
4
|
-
* fields take the defaults below.
|
|
3
|
+
* every Environment's recipe must fit, the pods' stop grace, the images that carry the engine and
|
|
4
|
+
* the harness placement. Unset fields take the defaults below.
|
|
5
5
|
*/
|
|
6
6
|
import { type PlacementHost, type TenantEnvironmentsConfig } from "@nylorun/core/contracts";
|
|
7
7
|
import { DEFAULT_SANDBOX_IDLE } from "./policy.js";
|
|
@@ -36,6 +36,10 @@ export interface EffectiveSandboxConfig {
|
|
|
36
36
|
readonly placement: Readonly<Record<string, {
|
|
37
37
|
readonly hosts: readonly PlacementHost[];
|
|
38
38
|
}>>;
|
|
39
|
+
/** Pods: the image repositories that carry this Runtime's engine (ADR-99). */
|
|
40
|
+
readonly engine: {
|
|
41
|
+
readonly images: readonly string[];
|
|
42
|
+
};
|
|
39
43
|
}
|
|
40
44
|
interface SettingsReader {
|
|
41
45
|
getSetting(key: string): Promise<string | undefined>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A Tenant's Environment settings, stored as Tenant setting `environments.config`: the limits
|
|
3
|
-
* every Environment's recipe must fit, the pods' stop grace
|
|
4
|
-
* fields take the defaults below.
|
|
3
|
+
* every Environment's recipe must fit, the pods' stop grace, the images that carry the engine and
|
|
4
|
+
* the harness placement. Unset fields take the defaults below.
|
|
5
5
|
*/
|
|
6
6
|
import { TenantEnvironmentsConfigSchema, } from "@nylorun/core/contracts";
|
|
7
7
|
import { parseSandboxSize } from "@nylorun/core/define";
|
|
@@ -42,6 +42,7 @@ export function effectiveSandboxConfig(config) {
|
|
|
42
42
|
},
|
|
43
43
|
lifecycle: { stopGrace: config.lifecycle?.stopGrace ?? "10s" },
|
|
44
44
|
placement: config.placement ?? { "*": { hosts: ["harness-container", "sandbox"] } },
|
|
45
|
+
engine: { images: config.engine?.images ?? [] },
|
|
45
46
|
};
|
|
46
47
|
}
|
|
47
48
|
/** The stored configuration, or `{}` when the Tenant has none. */
|
package/dist/tenant/agents.js
CHANGED
|
@@ -55,7 +55,7 @@ import { sandboxWorkspaceKey } from "../sandbox/records.js";
|
|
|
55
55
|
import { effectiveSandboxConfig, readSandboxConfig } from "../sandbox/tenant-config.js";
|
|
56
56
|
import { podName } from "../sandbox/pods/name.js";
|
|
57
57
|
import { podLifecycleConfig } from "../sandbox/pods/spec.js";
|
|
58
|
-
import { SETUP_FAILED } from "../sandbox/pods/lifecycle.js";
|
|
58
|
+
import { CRASHED, SETUP_FAILED } from "../sandbox/pods/lifecycle.js";
|
|
59
59
|
import { requirePods } from "../sandbox/placement.js";
|
|
60
60
|
import { deleteDiskArtifact, forkSeedOf, runPendingSeed, snapshotEnvironment } from "../artifacts/disk.js";
|
|
61
61
|
import { manifestChanges, manifestRecipeRequest } from "./agent-manifests.js";
|
|
@@ -128,6 +128,10 @@ export function stateOf(agent, sessions, unsetSecrets = []) {
|
|
|
128
128
|
const pod = agent.pod;
|
|
129
129
|
if (pod && (pod.observed === "failed" || pod.observed === "lost") && pod.desired !== "deleted")
|
|
130
130
|
attention.push({ state: "failing", reason: `compute ${pod.observed}${pod.reason ? `: ${pod.reason}` : ""}` });
|
|
131
|
+
// A crash an idle suspend found leaves the pod suspended, not failed: what the Agent wrote since
|
|
132
|
+
// its last suspend is gone all the same, so it is named until a turn starts the pod again.
|
|
133
|
+
else if (pod && pod.reason?.startsWith(CRASHED) && pod.desired !== "deleted")
|
|
134
|
+
attention.push({ state: "failing", reason: `compute crashed: ${pod.reason}` });
|
|
131
135
|
// A start on a new version failed and went back (track T3): until a start on a new one succeeds.
|
|
132
136
|
if (agent.failedVersion !== undefined)
|
|
133
137
|
attention.push({ state: "failing", reason: `start_failed: version ${agent.failedVersion} did not start` });
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const RUNTIME_VERSION = "0.
|
|
1
|
+
export declare const RUNTIME_VERSION = "0.47.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.47.1-beta";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nylorun/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.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",
|
|
@@ -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.36.
|
|
68
|
+
"@nylorun/core": "0.41.0-beta",
|
|
69
|
+
"@nylorun/harness": "0.36.1-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",
|