@nylorun/runtime 0.9.0-beta → 0.11.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 +81 -0
- package/README.md +110 -37
- package/dist/adapters/execution/restate.d.ts +130 -0
- package/dist/adapters/execution/restate.js +442 -0
- package/dist/adapters/media.d.ts +2 -2
- package/dist/adapters/media.js +2 -2
- package/dist/adapters/observe.d.ts +2 -1
- package/dist/adapters/observe.js +2 -3
- package/dist/adapters/sandbox/virtual.js +1 -1
- package/dist/adapters/streams/s2.d.ts +18 -0
- package/dist/adapters/streams/s2.js +361 -0
- package/dist/assets.d.ts +2 -2
- package/dist/assets.js +5 -3
- package/dist/core/executors.d.ts +15 -3
- package/dist/core/executors.js +40 -5
- package/dist/core/flow-host.d.ts +225 -0
- package/dist/core/flow-host.js +518 -0
- package/dist/core/limits.d.ts +40 -0
- package/dist/core/limits.js +76 -0
- package/dist/core/runtime.d.ts +5 -90
- package/dist/core/runtime.js +3 -1447
- package/dist/core/sandbox-routes.d.ts +40 -0
- package/dist/core/sandbox-routes.js +106 -0
- package/dist/core/turn-manifest.d.ts +50 -0
- package/dist/core/turn-manifest.js +66 -0
- package/dist/execution/memory.d.ts +54 -0
- package/dist/execution/memory.js +217 -0
- package/dist/execution/types.d.ts +125 -0
- package/dist/execution/types.js +52 -0
- package/dist/host/config-for.d.ts +17 -0
- package/dist/host/config-for.js +27 -0
- package/dist/host/config.d.ts +16 -0
- package/dist/host/create-host.d.ts +56 -0
- package/dist/host/create-host.js +420 -0
- package/dist/host/environment.d.ts +13 -0
- package/dist/host/environment.js +53 -0
- package/dist/host/execution.d.ts +61 -0
- package/dist/host/execution.js +69 -0
- package/dist/host/http.d.ts +39 -0
- package/dist/host/http.js +142 -0
- package/dist/host/index.d.ts +8 -0
- package/dist/host/index.js +7 -0
- package/dist/host/logger.d.ts +3 -0
- package/dist/host/logger.js +18 -0
- package/dist/host/main.d.ts +3 -0
- package/dist/host/main.js +201 -0
- package/dist/host/stack-config.d.ts +72 -0
- package/dist/host/stack-config.js +193 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +4 -2
- package/dist/infra/database.d.ts +16 -0
- package/dist/infra/database.js +31 -0
- package/dist/infra/execution.d.ts +39 -0
- package/dist/infra/execution.js +68 -0
- package/dist/infra/index.d.ts +42 -0
- package/dist/infra/index.js +56 -0
- package/dist/infra/readiness.d.ts +33 -0
- package/dist/infra/readiness.js +53 -0
- package/dist/infra/streams.d.ts +17 -0
- package/dist/infra/streams.js +33 -0
- package/dist/infra/workers.d.ts +39 -0
- package/dist/infra/workers.js +31 -0
- package/dist/mcp/connect.d.ts +1 -0
- package/dist/mcp/connect.js +2 -0
- package/dist/mcp/pool.d.ts +3 -1
- package/dist/mcp/pool.js +2 -1
- package/dist/mcp/stdio.d.ts +2 -0
- package/dist/mcp/stdio.js +3 -1
- package/dist/model/auth-store.d.ts +1 -1
- package/dist/model/auth-store.js +2 -2
- package/dist/model/models.d.ts +4 -0
- package/dist/model/models.js +22 -26
- package/dist/model/pi-model.d.ts +2 -2
- package/dist/model/pi-model.js +6 -6
- package/dist/model/settings.d.ts +5 -1
- package/dist/model/settings.js +6 -2
- package/dist/sandbox/manager.d.ts +26 -8
- package/dist/sandbox/manager.js +95 -47
- package/dist/sandbox/select.d.ts +5 -4
- package/dist/sandbox/select.js +18 -13
- package/dist/sandbox/share.d.ts +23 -0
- package/dist/sandbox/share.js +58 -0
- package/dist/sandbox/types.d.ts +2 -2
- package/dist/store/canonical.d.ts +5 -0
- package/dist/store/canonical.js +17 -0
- package/dist/store/cursor.d.ts +9 -0
- package/dist/store/cursor.js +18 -0
- package/dist/store/memory.d.ts +57 -0
- package/dist/store/memory.js +643 -0
- package/dist/store/ownership.d.ts +19 -0
- package/dist/store/ownership.js +33 -0
- package/dist/store/postgres/connect.d.ts +25 -0
- package/dist/store/postgres/connect.js +25 -0
- package/dist/store/postgres/locking.d.ts +16 -0
- package/dist/store/postgres/locking.js +22 -0
- package/dist/store/postgres/migrations/001_initial.d.ts +27 -0
- package/dist/store/postgres/migrations/001_initial.js +196 -0
- package/dist/store/postgres/migrations/index.d.ts +49 -0
- package/dist/store/postgres/migrations/index.js +75 -0
- package/dist/store/postgres/names.d.ts +11 -0
- package/dist/store/postgres/names.js +42 -0
- package/dist/store/postgres/store.d.ts +11 -0
- package/dist/store/postgres/store.js +790 -0
- package/dist/store/postgres/tenants.d.ts +90 -0
- package/dist/store/postgres/tenants.js +207 -0
- package/dist/store/types.d.ts +440 -0
- package/dist/store/types.js +10 -0
- package/dist/streams/basin.d.ts +23 -0
- package/dist/streams/basin.js +51 -0
- package/dist/streams/memory.d.ts +25 -0
- package/dist/streams/memory.js +124 -0
- package/dist/streams/relay.d.ts +37 -0
- package/dist/streams/relay.js +254 -0
- package/dist/streams/types.d.ts +115 -0
- package/dist/streams/types.js +86 -0
- package/dist/tenant/actions.d.ts +153 -0
- package/dist/tenant/actions.js +191 -0
- package/dist/tenant/advance.d.ts +15 -0
- package/dist/tenant/advance.js +439 -0
- package/dist/tenant/auth.d.ts +11 -0
- package/dist/tenant/auth.js +34 -0
- package/dist/tenant/commands.d.ts +3 -0
- package/dist/tenant/commands.js +384 -0
- package/dist/tenant/context.d.ts +140 -0
- package/dist/tenant/context.js +40 -0
- package/dist/tenant/effects.d.ts +20 -0
- package/dist/tenant/effects.js +539 -0
- package/dist/tenant/envelope.d.ts +7 -0
- package/dist/tenant/envelope.js +22 -0
- package/dist/tenant/ephemeral.d.ts +42 -0
- package/dist/tenant/ephemeral.js +224 -0
- package/dist/tenant/http.d.ts +32 -0
- package/dist/tenant/http.js +51 -0
- package/dist/tenant/live.d.ts +92 -0
- package/dist/tenant/live.js +290 -0
- package/dist/tenant/logger.d.ts +9 -0
- package/dist/tenant/logger.js +25 -0
- package/dist/tenant/model-setting.d.ts +16 -0
- package/dist/tenant/model-setting.js +12 -0
- package/dist/tenant/module.d.ts +38 -0
- package/dist/tenant/module.js +319 -0
- package/dist/tenant/paths.d.ts +28 -0
- package/dist/tenant/paths.js +91 -0
- package/dist/tenant/pool.d.ts +10 -0
- package/dist/tenant/pool.js +54 -0
- package/dist/tenant/principals.d.ts +13 -0
- package/dist/tenant/principals.js +37 -0
- package/dist/tenant/quarantine-error.d.ts +6 -0
- package/dist/tenant/quarantine-error.js +9 -0
- package/dist/tenant/quarantine.d.ts +27 -0
- package/dist/tenant/quarantine.js +72 -0
- package/dist/tenant/reset.d.ts +22 -0
- package/dist/tenant/reset.js +81 -0
- package/dist/tenant/routes-tenant.d.ts +12 -0
- package/dist/tenant/routes-tenant.js +126 -0
- package/dist/tenant/routes.d.ts +9 -0
- package/dist/tenant/routes.js +142 -0
- package/dist/tenant/runtime.d.ts +81 -0
- package/dist/tenant/runtime.js +298 -0
- package/dist/tenant/scheduler.d.ts +25 -0
- package/dist/tenant/scheduler.js +84 -0
- package/dist/tenant/session.d.ts +60 -0
- package/dist/tenant/session.js +94 -0
- package/dist/tenant/sessions.d.ts +34 -0
- package/dist/tenant/sessions.js +149 -0
- package/dist/tenant/status.d.ts +37 -0
- package/dist/tenant/status.js +130 -0
- package/dist/tenant/store-memory.d.ts +16 -0
- package/dist/tenant/store-memory.js +111 -0
- package/dist/tenant/store-pg.d.ts +15 -0
- package/dist/tenant/store-pg.js +140 -0
- package/dist/tenant/streams.d.ts +126 -0
- package/dist/tenant/streams.js +419 -0
- package/dist/tenant/sweep.d.ts +15 -0
- package/dist/tenant/sweep.js +122 -0
- package/dist/tenant/types.d.ts +205 -0
- package/dist/tenant/types.js +23 -0
- package/dist/tenant/worker.d.ts +84 -0
- package/dist/tenant/worker.js +189 -0
- package/dist/vault/kek.d.ts +5 -2
- package/dist/vault/kek.js +7 -10
- package/dist/vault/service.d.ts +37 -31
- package/dist/vault/service.js +439 -359
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +14 -11
- package/dist/adapters/sandbox/microsandbox.d.ts +0 -2
- package/dist/adapters/sandbox/microsandbox.js +0 -238
- package/dist/core/main.js +0 -35
- package/dist/core/store.d.ts +0 -30
- package/dist/core/store.js +0 -202
- /package/dist/{core/main.d.ts → host/config.js} +0 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { resolveFlowLimits } from "../core/limits.js";
|
|
3
|
+
import { loadExecutorRegistry } from "../core/executors.js";
|
|
4
|
+
import { scriptedModel, gatewayModel, toolFixtureModel, } from "../core/provider.js";
|
|
5
|
+
import { createKekFile, readVaultKek } from "../vault/kek.js";
|
|
6
|
+
import { VaultService } from "../vault/service.js";
|
|
7
|
+
import { McpPool } from "../mcp/pool.js";
|
|
8
|
+
import { SandboxManager } from "../sandbox/manager.js";
|
|
9
|
+
import { defaultSandboxBackends } from "../sandbox/select.js";
|
|
10
|
+
import { MemoryExecution } from "../execution/memory.js";
|
|
11
|
+
import { QuarantineError } from "./quarantine-error.js";
|
|
12
|
+
import { MemoryStreams } from "../streams/memory.js";
|
|
13
|
+
import { connectedExecutorCount, createLiveHub, endAllStreams, } from "./live.js";
|
|
14
|
+
import { closeStreams, drainOutbox, streamsStatus, wireStreams, } from "./streams.js";
|
|
15
|
+
import { abortAll, abortLocal, createWorkState, drain, waitForIdle, } from "./scheduler.js";
|
|
16
|
+
import { advance } from "./advance.js";
|
|
17
|
+
import { sweep } from "./sweep.js";
|
|
18
|
+
import { TenantWorkers, WORKER_ID, } from "./worker.js";
|
|
19
|
+
import { authorize } from "./effects.js";
|
|
20
|
+
import { handle } from "./routes.js";
|
|
21
|
+
/** Default ownership lease of an advance; the heartbeat renews it every third. */
|
|
22
|
+
const DEFAULT_OWNER_LEASE_MS = 30_000;
|
|
23
|
+
function validateConfig(config) {
|
|
24
|
+
for (const key of ["leaseMs", "ownerLeaseMs", "sweepIntervalMs"]) {
|
|
25
|
+
const value = config[key];
|
|
26
|
+
if (value !== undefined && (!Number.isFinite(value) || value <= 0))
|
|
27
|
+
throw new Error(`${key} must be finite and positive`);
|
|
28
|
+
}
|
|
29
|
+
const flowLimits = resolveFlowLimits({
|
|
30
|
+
flow: config.flow,
|
|
31
|
+
env: config.flowEnv,
|
|
32
|
+
});
|
|
33
|
+
if ((config.model.kind === "fixture" || config.model.kind === "scripted") &&
|
|
34
|
+
config.mode === "shared")
|
|
35
|
+
throw new Error("fixture/scripted models require ephemeral or test mode");
|
|
36
|
+
return flowLimits;
|
|
37
|
+
}
|
|
38
|
+
export class TenantRuntime {
|
|
39
|
+
ctx;
|
|
40
|
+
envelope;
|
|
41
|
+
worker;
|
|
42
|
+
detach;
|
|
43
|
+
closeGraceMs;
|
|
44
|
+
constructor(ctx, envelope,
|
|
45
|
+
/** This Tenant's Worker handlers, as registered with the execution. */
|
|
46
|
+
worker, detach,
|
|
47
|
+
/** How long close waits for running advances: the execution's advance grace period. */
|
|
48
|
+
closeGraceMs) {
|
|
49
|
+
this.ctx = ctx;
|
|
50
|
+
this.envelope = envelope;
|
|
51
|
+
this.worker = worker;
|
|
52
|
+
this.detach = detach;
|
|
53
|
+
this.closeGraceMs = closeGraceMs;
|
|
54
|
+
}
|
|
55
|
+
static async open(config, hooks) {
|
|
56
|
+
const envelope = hooks.envelope;
|
|
57
|
+
const flowLimits = validateConfig(config);
|
|
58
|
+
const paths = config.paths;
|
|
59
|
+
mkdirSync(paths.root, { recursive: true });
|
|
60
|
+
mkdirSync(paths.home, { recursive: true });
|
|
61
|
+
mkdirSync(paths.tmp, { recursive: true });
|
|
62
|
+
mkdirSync(paths.sandboxes, { recursive: true });
|
|
63
|
+
mkdirSync(paths.pluginData, { recursive: true });
|
|
64
|
+
mkdirSync(paths.logs, { recursive: true });
|
|
65
|
+
const store = hooks.store;
|
|
66
|
+
let wired;
|
|
67
|
+
let detach;
|
|
68
|
+
try {
|
|
69
|
+
let kek = readVaultKek({
|
|
70
|
+
vaultKek: hooks.vaultKek,
|
|
71
|
+
vaultKekPath: paths.kek,
|
|
72
|
+
});
|
|
73
|
+
if ((await store.tx((t) => t.countCredentials())) > 0 && !kek) {
|
|
74
|
+
throw new QuarantineError("kek-missing", "Vault key-encryption key is missing for ciphertext in this Tenant", "restore the vault-kek file in the Tenant directory");
|
|
75
|
+
}
|
|
76
|
+
const createKekIfMissing = hooks.createKekIfMissing !== false;
|
|
77
|
+
const ensureKek = () => {
|
|
78
|
+
if (kek)
|
|
79
|
+
return kek;
|
|
80
|
+
if (!createKekIfMissing)
|
|
81
|
+
throw new QuarantineError("kek-missing", "Vault key-encryption key is required", "restore the vault-kek file in the Tenant directory");
|
|
82
|
+
kek = createKekFile(paths.kek);
|
|
83
|
+
return kek;
|
|
84
|
+
};
|
|
85
|
+
const opened = store;
|
|
86
|
+
const registry = await loadExecutorRegistry(opened);
|
|
87
|
+
// A seeded Tenant setting wins over the Host default (A18).
|
|
88
|
+
const seededBackend = await opened.tx((t) => t.getSetting("sandbox.backend"));
|
|
89
|
+
const vault = new VaultService({
|
|
90
|
+
store: opened,
|
|
91
|
+
kek: ensureKek,
|
|
92
|
+
fetch: config.vaultFetch ?? globalThis.fetch,
|
|
93
|
+
});
|
|
94
|
+
// `ctx` is assigned below; these callbacks only run once the Tenant is open.
|
|
95
|
+
let ctx;
|
|
96
|
+
const mcp = new McpPool({
|
|
97
|
+
pluginData: paths.pluginData,
|
|
98
|
+
childEnv: config.childEnv,
|
|
99
|
+
authorize: (sessionId, request) => authorize(ctx, sessionId, request),
|
|
100
|
+
});
|
|
101
|
+
const ephemeral = config.mode === "ephemeral";
|
|
102
|
+
const sandbox = new SandboxManager({
|
|
103
|
+
scope: config.tenantId,
|
|
104
|
+
store: opened,
|
|
105
|
+
backends: config.sandbox.backends ??
|
|
106
|
+
defaultSandboxBackends({ root: paths.sandboxes }),
|
|
107
|
+
preference: seededBackend ?? config.sandbox.backend,
|
|
108
|
+
ephemeral,
|
|
109
|
+
emit: async (sessionId, turnId, type, payload) => {
|
|
110
|
+
if (ctx.closed)
|
|
111
|
+
return;
|
|
112
|
+
try {
|
|
113
|
+
await opened.tx((t) => t.event(sessionId, turnId, type, payload));
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
/* the session is gone (reset) or the store closed */
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
const useVaultModel = config.model.kind === "vault";
|
|
121
|
+
let modelProvider;
|
|
122
|
+
if (hooks.modelProvider)
|
|
123
|
+
modelProvider = hooks.modelProvider;
|
|
124
|
+
else if (config.model.kind === "scripted")
|
|
125
|
+
modelProvider = scriptedModel(config.model.output);
|
|
126
|
+
else if (config.model.kind === "fixture")
|
|
127
|
+
modelProvider = toolFixtureModel();
|
|
128
|
+
else if (config.model.kind === "gateway") {
|
|
129
|
+
const gateway = config.model;
|
|
130
|
+
modelProvider = async (effect, signal) => {
|
|
131
|
+
const secret = await vault.readHostModel();
|
|
132
|
+
const token = typeof secret?.credential.key === "string"
|
|
133
|
+
? secret.credential.key
|
|
134
|
+
: "";
|
|
135
|
+
return gatewayModel({
|
|
136
|
+
url: gateway.url,
|
|
137
|
+
model: gateway.model,
|
|
138
|
+
token,
|
|
139
|
+
})(effect, signal);
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
else
|
|
143
|
+
modelProvider = scriptedModel();
|
|
144
|
+
const live = createLiveHub();
|
|
145
|
+
const local = hooks.execution
|
|
146
|
+
? undefined
|
|
147
|
+
: new MemoryExecution({
|
|
148
|
+
sweepIntervalMs: config.sweepIntervalMs ?? Math.min(config.leaseMs ?? 30_000, 5000),
|
|
149
|
+
onError: (error) => config.logger.error("tenant execution failed", {
|
|
150
|
+
message: error instanceof Error ? error.message : String(error),
|
|
151
|
+
}),
|
|
152
|
+
});
|
|
153
|
+
const { execution, workers } = hooks.execution ?? {
|
|
154
|
+
execution: local,
|
|
155
|
+
workers: new TenantWorkers(),
|
|
156
|
+
};
|
|
157
|
+
const sweepHooks = new Set();
|
|
158
|
+
ctx = {
|
|
159
|
+
config,
|
|
160
|
+
envelope,
|
|
161
|
+
store: opened,
|
|
162
|
+
vault,
|
|
163
|
+
registry,
|
|
164
|
+
mcp,
|
|
165
|
+
sandbox,
|
|
166
|
+
flowLimits,
|
|
167
|
+
modelProvider,
|
|
168
|
+
useVaultModel,
|
|
169
|
+
closing: false,
|
|
170
|
+
closed: false,
|
|
171
|
+
work: createWorkState(),
|
|
172
|
+
live,
|
|
173
|
+
workerId: hooks.workerId ?? WORKER_ID,
|
|
174
|
+
ownerLeaseMs: config.ownerLeaseMs ?? DEFAULT_OWNER_LEASE_MS,
|
|
175
|
+
wake: async (sessionId, wake) => {
|
|
176
|
+
if (ctx.closing || ctx.closed)
|
|
177
|
+
return;
|
|
178
|
+
await execution.wake(config.tenantId, sessionId, wake);
|
|
179
|
+
},
|
|
180
|
+
abortLocal: (sessionId) => abortLocal(ctx, sessionId),
|
|
181
|
+
...(hooks.execution?.stuckInvocations
|
|
182
|
+
? {
|
|
183
|
+
stuckInvocations: () => hooks.execution.stuckInvocations(config.tenantId),
|
|
184
|
+
}
|
|
185
|
+
: {}),
|
|
186
|
+
sweepHooks,
|
|
187
|
+
onSweep: (hook) => {
|
|
188
|
+
sweepHooks.add(hook);
|
|
189
|
+
return () => sweepHooks.delete(hook);
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
wired = await wireStreams(ctx, {
|
|
193
|
+
store: opened,
|
|
194
|
+
streams: hooks.streams ?? new MemoryStreams(),
|
|
195
|
+
ownsStreams: !hooks.streams,
|
|
196
|
+
tenantId: config.tenantId,
|
|
197
|
+
});
|
|
198
|
+
// Outbox rows a lost relay step left behind are appended by the sweep.
|
|
199
|
+
sweepHooks.add(async () => {
|
|
200
|
+
await drainOutbox(ctx);
|
|
201
|
+
});
|
|
202
|
+
// Register the handlers, then arm the sweep: its first pass runs at once and re-wakes
|
|
203
|
+
// sessions a previous process left runnable or running (takeover handles the rest).
|
|
204
|
+
let afterOpen = true;
|
|
205
|
+
const worker = {
|
|
206
|
+
advance: (sessionId, signal) => advance(ctx, sessionId, signal),
|
|
207
|
+
sweep: async () => {
|
|
208
|
+
const first = afterOpen;
|
|
209
|
+
afterOpen = false;
|
|
210
|
+
await sweep(ctx, { afterOpen: first });
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
const unregister = workers.register(config.tenantId, worker);
|
|
214
|
+
detach = async () => {
|
|
215
|
+
unregister();
|
|
216
|
+
await local?.stop();
|
|
217
|
+
};
|
|
218
|
+
if (local)
|
|
219
|
+
await local.start(workers.handlers);
|
|
220
|
+
await execution.armSweep(config.tenantId);
|
|
221
|
+
return new TenantRuntime(ctx, envelope, worker, detach, workers.graceMs);
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
await detach?.().catch(() => undefined);
|
|
225
|
+
await wired?.close().catch(() => undefined);
|
|
226
|
+
await store.close().catch(() => undefined);
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
handle(request, response, url) {
|
|
231
|
+
return handle(this.ctx, request, response, url);
|
|
232
|
+
}
|
|
233
|
+
authorize(sessionId, request) {
|
|
234
|
+
return authorize(this.ctx, sessionId, request);
|
|
235
|
+
}
|
|
236
|
+
async summary() {
|
|
237
|
+
const { store, live } = this.ctx;
|
|
238
|
+
const { counts, outbox } = await store.tx(async (t) => ({
|
|
239
|
+
counts: await t.counts(),
|
|
240
|
+
outbox: await t.outboxStats(),
|
|
241
|
+
}));
|
|
242
|
+
return {
|
|
243
|
+
ready: !this.ctx.closing && !this.ctx.closed,
|
|
244
|
+
runningSessions: counts.runningSessions,
|
|
245
|
+
connectedExecutors: connectedExecutorCount(live),
|
|
246
|
+
pendingActions: counts.pendingActions,
|
|
247
|
+
uncertainEffects: counts.uncertainEffects,
|
|
248
|
+
outboxDepth: outbox.depth,
|
|
249
|
+
relayLagMs: outbox.oldestCreatedAt === null
|
|
250
|
+
? 0
|
|
251
|
+
: Math.max(0, Date.now() - Date.parse(outbox.oldestCreatedAt)),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
drain(activeWork, timeoutMs = 30_000) {
|
|
255
|
+
return drain(this.ctx, activeWork, timeoutMs);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Aborts the advance of `sessionId` if it runs on this process. Cancel calls it after
|
|
259
|
+
* committing `cancelled`; the control stream calls it for cancels made elsewhere.
|
|
260
|
+
*/
|
|
261
|
+
abortLocal(sessionId) {
|
|
262
|
+
this.ctx.abortLocal(sessionId);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* The Durable Streams seam's status for Tenant status and readiness: S2 reachability, the
|
|
266
|
+
* basin, outbox depth and relay lag (`streamsStatus` in `tenant/streams.ts`).
|
|
267
|
+
*/
|
|
268
|
+
streamsStatus() {
|
|
269
|
+
return streamsStatus(this.ctx);
|
|
270
|
+
}
|
|
271
|
+
/** Adds a callback to the Tenant sweep. Returns a function that removes it. */
|
|
272
|
+
onSweep(hook) {
|
|
273
|
+
return this.ctx.onSweep(hook);
|
|
274
|
+
}
|
|
275
|
+
async close() {
|
|
276
|
+
const ctx = this.ctx;
|
|
277
|
+
ctx.closing = true;
|
|
278
|
+
// A shutdown abort: the advances leave their sessions to the next advance, unsettled.
|
|
279
|
+
abortAll(ctx, "shutdown");
|
|
280
|
+
const idleBy = Date.now() + this.closeGraceMs;
|
|
281
|
+
await this.detach();
|
|
282
|
+
await ctx.mcp.close();
|
|
283
|
+
endAllStreams(ctx.live);
|
|
284
|
+
// Bounded: an advance that ignores its abort is abandoned; its lease lapses (§11.4).
|
|
285
|
+
await waitForIdle(ctx, Math.max(0, idleBy - Date.now()));
|
|
286
|
+
await ctx.sandbox.close();
|
|
287
|
+
ctx.closed = true;
|
|
288
|
+
await closeStreams(ctx);
|
|
289
|
+
await ctx.store.close();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Open a Tenant Runtime from an explicit `TenantConfig`. No listen() (A1).
|
|
294
|
+
* Implements `OpenTenantRuntime`.
|
|
295
|
+
*/
|
|
296
|
+
export async function openTenantRuntime(config, hooks) {
|
|
297
|
+
return TenantRuntime.open(config, hooks);
|
|
298
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { TenantContext } from "./context.js";
|
|
2
|
+
import { type AdvanceAbortKind } from "./worker.js";
|
|
3
|
+
export interface WorkState {
|
|
4
|
+
/** Advances running on this process, by session id. */
|
|
5
|
+
readonly running: Map<string, AbortController>;
|
|
6
|
+
}
|
|
7
|
+
export declare function createWorkState(): WorkState;
|
|
8
|
+
/**
|
|
9
|
+
* Abort the advance of `id` if it runs on this process. Cancel calls it after committing
|
|
10
|
+
* `cancelled`, and so does the control stream for cancels made elsewhere.
|
|
11
|
+
*/
|
|
12
|
+
export declare function abortLocal(ctx: TenantContext, id: string, kind?: AdvanceAbortKind): void;
|
|
13
|
+
/** Reset: abort every advance running on this process; their sessions are being cleared. */
|
|
14
|
+
export declare function clearWork(ctx: TenantContext): void;
|
|
15
|
+
/** Stop new advances; cancel active turns if asked; wait for running advances until the timeout. */
|
|
16
|
+
export declare function drain(ctx: TenantContext, activeWork: "drain" | "cancel", timeoutMs: number): Promise<void>;
|
|
17
|
+
/** Abort every advance running on this process; closing the Tenant is a `shutdown`. */
|
|
18
|
+
export declare function abortAll(ctx: TenantContext, kind?: AdvanceAbortKind): void;
|
|
19
|
+
/**
|
|
20
|
+
* Close: wait until no advance runs on this process, for at most `timeoutMs`. Advances still
|
|
21
|
+
* running then (an effect that ignores its abort signal) are abandoned and logged: they have
|
|
22
|
+
* stopped renewing their lease, so it lapses and the next advance takes the session over
|
|
23
|
+
* (§11.4); the epoch fences anything they still try to write. Returns their session ids.
|
|
24
|
+
*/
|
|
25
|
+
export declare function waitForIdle(ctx: TenantContext, timeoutMs: number): Promise<string[]>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local work: the advances running on this process, for `abortLocal`, drain, reset and close.
|
|
3
|
+
*
|
|
4
|
+
* Scheduling itself lives behind `DurableExecution` (`ctx.wake`), the Tenant's handlers in
|
|
5
|
+
* `worker.ts`, `advance.ts` and `sweep.ts`. What stays here is only what one process must
|
|
6
|
+
* know about the advances it runs: their `AbortController`s, keyed by session id.
|
|
7
|
+
*
|
|
8
|
+
* Every abort carries an `AdvanceAbort` saying why (`worker.ts`): a cancel or a reset aborts
|
|
9
|
+
* with `cancel`, closing the Tenant with `shutdown`, which leaves the session for the next
|
|
10
|
+
* advance instead of settling it.
|
|
11
|
+
*/
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
|
+
import { command } from "./commands.js";
|
|
14
|
+
import { AdvanceAbort } from "./worker.js";
|
|
15
|
+
export function createWorkState() {
|
|
16
|
+
return { running: new Map() };
|
|
17
|
+
}
|
|
18
|
+
const MESSAGES = {
|
|
19
|
+
cancel: "Turn cancelled",
|
|
20
|
+
shutdown: "The Tenant is closing",
|
|
21
|
+
deadline: "The advance ran past its deadline",
|
|
22
|
+
"ownership.lost": "Ownership lost",
|
|
23
|
+
};
|
|
24
|
+
function abortWith(controller, kind) {
|
|
25
|
+
if (!controller.signal.aborted)
|
|
26
|
+
controller.abort(new AdvanceAbort(kind, MESSAGES[kind]));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Abort the advance of `id` if it runs on this process. Cancel calls it after committing
|
|
30
|
+
* `cancelled`, and so does the control stream for cancels made elsewhere.
|
|
31
|
+
*/
|
|
32
|
+
export function abortLocal(ctx, id, kind = "cancel") {
|
|
33
|
+
const controller = ctx.work.running.get(id);
|
|
34
|
+
if (controller)
|
|
35
|
+
abortWith(controller, kind);
|
|
36
|
+
}
|
|
37
|
+
/** Reset: abort every advance running on this process; their sessions are being cleared. */
|
|
38
|
+
export function clearWork(ctx) {
|
|
39
|
+
abortAll(ctx, "cancel");
|
|
40
|
+
}
|
|
41
|
+
/** Stop new advances; cancel active turns if asked; wait for running advances until the timeout. */
|
|
42
|
+
export async function drain(ctx, activeWork, timeoutMs) {
|
|
43
|
+
ctx.closing = true;
|
|
44
|
+
if (activeWork === "cancel") {
|
|
45
|
+
const active = await ctx.store.tx((t) => t.sessionsWithStatus(["running", "runnable", "paused"]));
|
|
46
|
+
for (const sess of active)
|
|
47
|
+
if (sess.activeTurnId)
|
|
48
|
+
await command(ctx, sess.id, {
|
|
49
|
+
type: "cancel",
|
|
50
|
+
requestId: randomUUID(),
|
|
51
|
+
idempotencyKey: `drain-cancel-${sess.id}-${sess.activeTurnId}`,
|
|
52
|
+
reason: "tenant drain cancel",
|
|
53
|
+
}, {
|
|
54
|
+
kind: "application",
|
|
55
|
+
principalId: "drain",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const deadline = Date.now() + timeoutMs;
|
|
59
|
+
while (ctx.work.running.size > 0 && Date.now() < deadline)
|
|
60
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
61
|
+
}
|
|
62
|
+
/** Abort every advance running on this process; closing the Tenant is a `shutdown`. */
|
|
63
|
+
export function abortAll(ctx, kind = "shutdown") {
|
|
64
|
+
for (const c of ctx.work.running.values())
|
|
65
|
+
abortWith(c, kind);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Close: wait until no advance runs on this process, for at most `timeoutMs`. Advances still
|
|
69
|
+
* running then (an effect that ignores its abort signal) are abandoned and logged: they have
|
|
70
|
+
* stopped renewing their lease, so it lapses and the next advance takes the session over
|
|
71
|
+
* (§11.4); the epoch fences anything they still try to write. Returns their session ids.
|
|
72
|
+
*/
|
|
73
|
+
export async function waitForIdle(ctx, timeoutMs) {
|
|
74
|
+
const deadline = Date.now() + timeoutMs;
|
|
75
|
+
while (ctx.work.running.size > 0 && Date.now() < deadline)
|
|
76
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
77
|
+
const abandoned = [...ctx.work.running.keys()];
|
|
78
|
+
if (abandoned.length > 0)
|
|
79
|
+
ctx.config.logger.warn("tenant closed with advances still running", {
|
|
80
|
+
sessionIds: abandoned,
|
|
81
|
+
waitedMs: timeoutMs,
|
|
82
|
+
});
|
|
83
|
+
return abandoned;
|
|
84
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers over the session document: turn-manifest pins and variants, state rebasing,
|
|
3
|
+
* the tools of an MCP snapshot, and the event target of an Action. No store access and no I/O.
|
|
4
|
+
*
|
|
5
|
+
* Later waves: stable; the async store conversion (Wave 1 / A) does not change these.
|
|
6
|
+
*/
|
|
7
|
+
import { type DurableSessionTool, type HostEffect } from "@nylorun/harness/run";
|
|
8
|
+
import type { Action } from "@nylorun/core/contracts";
|
|
9
|
+
import { type AgentManifest, type JsonValue } from "@nylorun/core/define";
|
|
10
|
+
import { type TurnManifestStore } from "../core/turn-manifest.js";
|
|
11
|
+
import type { McpSnapshot, McpToolRecord } from "../mcp/snapshot.js";
|
|
12
|
+
import type { Session } from "./context.js";
|
|
13
|
+
export declare function sessionToolsOf(snapshot: McpSnapshot | undefined): readonly DurableSessionTool[] | undefined;
|
|
14
|
+
/** Turn-manifest store backed by the session's `variants` map (no schema change). */
|
|
15
|
+
export declare function variantStore(session: Session): TurnManifestStore;
|
|
16
|
+
/** Manifest this turn's checkpoint is pinned to (session pin or a stored variant). */
|
|
17
|
+
export declare function turnManifestOf(session: Session): AgentManifest;
|
|
18
|
+
/** Wrap a linked agent turn so the Loop learns the turn's manifest (SD-I4 pin stays on session). */
|
|
19
|
+
export declare function linkedAgentOutput(session: Session, output: JsonValue | undefined): JsonValue;
|
|
20
|
+
export declare function rebaseSessionState(session: Session, turnHash: string): void;
|
|
21
|
+
export declare function mcpToolOf(session: Session, request: Pick<HostEffect, "agent" | "capabilityId" | "toolName">): McpToolRecord | undefined;
|
|
22
|
+
/** The manifest of the agent an effect belongs to: the root, or an agent it uses as a tool. */
|
|
23
|
+
export declare function manifestFor(manifest: AgentManifest, agent: HostEffect["agent"]): AgentManifest | undefined;
|
|
24
|
+
export declare function pinnedTool(manifest: AgentManifest, capabilityId?: string, toolName?: string): import("@nylorun/core/define").ToolManifest | undefined;
|
|
25
|
+
/** What an action runs, for events: a tool name, or a hook point and its capabilities. */
|
|
26
|
+
export declare function actionTarget(action: Action): {
|
|
27
|
+
hook: {
|
|
28
|
+
at: "after" | "before";
|
|
29
|
+
scope: "step" | "turn";
|
|
30
|
+
capabilityIds: string[];
|
|
31
|
+
};
|
|
32
|
+
agent?: {
|
|
33
|
+
id: string;
|
|
34
|
+
path: string;
|
|
35
|
+
delegationId?: string | undefined;
|
|
36
|
+
} | undefined;
|
|
37
|
+
} | {
|
|
38
|
+
toolName: string;
|
|
39
|
+
agent?: {
|
|
40
|
+
id: string;
|
|
41
|
+
path: string;
|
|
42
|
+
delegationId?: string | undefined;
|
|
43
|
+
} | undefined;
|
|
44
|
+
} | {
|
|
45
|
+
path: string;
|
|
46
|
+
key: string;
|
|
47
|
+
agent?: {
|
|
48
|
+
id: string;
|
|
49
|
+
path: string;
|
|
50
|
+
delegationId?: string | undefined;
|
|
51
|
+
} | {
|
|
52
|
+
id: string;
|
|
53
|
+
path: string;
|
|
54
|
+
delegationId?: string | undefined;
|
|
55
|
+
} | {
|
|
56
|
+
id: string;
|
|
57
|
+
path: string;
|
|
58
|
+
delegationId?: string | undefined;
|
|
59
|
+
} | undefined;
|
|
60
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers over the session document: turn-manifest pins and variants, state rebasing,
|
|
3
|
+
* the tools of an MCP snapshot, and the event target of an Action. No store access and no I/O.
|
|
4
|
+
*
|
|
5
|
+
* Later waves: stable; the async store conversion (Wave 1 / A) does not change these.
|
|
6
|
+
*/
|
|
7
|
+
import { agentTurnValue, } from "@nylorun/harness/run";
|
|
8
|
+
import { delegateManifest, } from "@nylorun/core/define";
|
|
9
|
+
import { isWorkflowManifest } from "../core/flow-host.js";
|
|
10
|
+
import { allowedManifestHashes, rebaseTurnState, } from "../core/turn-manifest.js";
|
|
11
|
+
export function sessionToolsOf(snapshot) {
|
|
12
|
+
if (!snapshot?.mcpTools.length)
|
|
13
|
+
return undefined;
|
|
14
|
+
return snapshot.mcpTools.map((tool) => ({
|
|
15
|
+
...(tool.agentId === undefined ? {} : { agentId: tool.agentId }),
|
|
16
|
+
capabilityId: tool.capabilityId,
|
|
17
|
+
name: tool.name,
|
|
18
|
+
...(tool.description === undefined
|
|
19
|
+
? {}
|
|
20
|
+
: { description: tool.description }),
|
|
21
|
+
inputSchema: tool.inputSchema,
|
|
22
|
+
...(tool.outputSchema === undefined
|
|
23
|
+
? {}
|
|
24
|
+
: { outputSchema: tool.outputSchema }),
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
/** Turn-manifest store backed by the session's `variants` map (no schema change). */
|
|
28
|
+
export function variantStore(session) {
|
|
29
|
+
return {
|
|
30
|
+
get(hash) {
|
|
31
|
+
return session.variants?.[hash];
|
|
32
|
+
},
|
|
33
|
+
put(hash, manifest) {
|
|
34
|
+
session.variants = { ...(session.variants ?? {}), [hash]: manifest };
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Manifest this turn's checkpoint is pinned to (session pin or a stored variant). */
|
|
39
|
+
export function turnManifestOf(session) {
|
|
40
|
+
const hash = session.checkpoint?.manifestHash;
|
|
41
|
+
if (!hash || hash === session.manifestHash)
|
|
42
|
+
return session.manifest;
|
|
43
|
+
return session.variants?.[hash] ?? session.manifest;
|
|
44
|
+
}
|
|
45
|
+
/** Wrap a linked agent turn so the Loop learns the turn's manifest (SD-I4 pin stays on session). */
|
|
46
|
+
export function linkedAgentOutput(session, output) {
|
|
47
|
+
if (isWorkflowManifest(session.manifest))
|
|
48
|
+
return output ?? null;
|
|
49
|
+
return agentTurnValue(output ?? null, turnManifestOf(session));
|
|
50
|
+
}
|
|
51
|
+
export function rebaseSessionState(session, turnHash) {
|
|
52
|
+
const allowed = allowedManifestHashes({
|
|
53
|
+
pinnedHash: session.manifestHash,
|
|
54
|
+
variantHashes: Object.keys(session.variants ?? {}),
|
|
55
|
+
});
|
|
56
|
+
const next = rebaseTurnState({
|
|
57
|
+
state: session.state,
|
|
58
|
+
turnManifestHash: turnHash,
|
|
59
|
+
isAllowedHash: (hash) => allowed.has(hash),
|
|
60
|
+
});
|
|
61
|
+
if (next !== session.state)
|
|
62
|
+
session.state = next;
|
|
63
|
+
}
|
|
64
|
+
export function mcpToolOf(session, request) {
|
|
65
|
+
return session.mcpSnapshot?.mcpTools.find((tool) => tool.agentId === request.agent?.id &&
|
|
66
|
+
tool.capabilityId === request.capabilityId &&
|
|
67
|
+
tool.name === request.toolName);
|
|
68
|
+
}
|
|
69
|
+
/** The manifest of the agent an effect belongs to: the root, or an agent it uses as a tool. */
|
|
70
|
+
export function manifestFor(manifest, agent) {
|
|
71
|
+
return agent ? delegateManifest(manifest, agent.id) : manifest;
|
|
72
|
+
}
|
|
73
|
+
export function pinnedTool(manifest, capabilityId, toolName) {
|
|
74
|
+
const capability = manifest.capabilities.find((item) => item.id === capabilityId);
|
|
75
|
+
return capability?.tools?.find((tool) => tool.name === toolName);
|
|
76
|
+
}
|
|
77
|
+
/** What an action runs, for events: a tool name, or a hook point and its capabilities. */
|
|
78
|
+
export function actionTarget(action) {
|
|
79
|
+
if (action.kind === "hook")
|
|
80
|
+
return {
|
|
81
|
+
hook: action.hook,
|
|
82
|
+
...(action.agent ? { agent: action.agent } : {}),
|
|
83
|
+
};
|
|
84
|
+
if (action.kind === "tool" && "toolName" in action)
|
|
85
|
+
return {
|
|
86
|
+
toolName: action.toolName,
|
|
87
|
+
...(action.agent ? { agent: action.agent } : {}),
|
|
88
|
+
};
|
|
89
|
+
return {
|
|
90
|
+
path: action.path,
|
|
91
|
+
key: action.key,
|
|
92
|
+
...(action.agent ? { agent: action.agent } : {}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session and definition resources: `PUT/GET /v1/agents`, `PUT /v1/sessions/:id` (create or
|
|
3
|
+
* re-attach vaults), the session list and the session view. Reads use the store's typed
|
|
4
|
+
* queries; `PUT /v1/sessions/:id` runs in one transaction with the vault attachment checks.
|
|
5
|
+
*/
|
|
6
|
+
import { type PutAgentRequest, type PutSessionRequest } from "@nylorun/core/contracts";
|
|
7
|
+
import type { Tx } from "../store/types.js";
|
|
8
|
+
import { type Session, type TenantContext } from "./context.js";
|
|
9
|
+
export declare function listDefinitions(ctx: TenantContext): Promise<{
|
|
10
|
+
agents: {
|
|
11
|
+
agentId: any;
|
|
12
|
+
manifest: any;
|
|
13
|
+
manifestHash: string;
|
|
14
|
+
implementationVersion: string;
|
|
15
|
+
}[];
|
|
16
|
+
}>;
|
|
17
|
+
export declare function putDefinition(ctx: TenantContext, agentId: string, body: PutAgentRequest): Promise<{
|
|
18
|
+
agentId: string;
|
|
19
|
+
manifestHash: string;
|
|
20
|
+
implementationVersion: string;
|
|
21
|
+
}>;
|
|
22
|
+
export declare function listSessions(ctx: TenantContext, agentId: string | null): Promise<{
|
|
23
|
+
sessions: {
|
|
24
|
+
id: string;
|
|
25
|
+
agentId: string;
|
|
26
|
+
ownerUserId: string;
|
|
27
|
+
status: string;
|
|
28
|
+
activeTurnId: string | null;
|
|
29
|
+
}[];
|
|
30
|
+
}>;
|
|
31
|
+
/** Create a session from its definition, or re-attach vaults to an identical one. */
|
|
32
|
+
export declare function putSession(ctx: TenantContext, id: string, body: PutSessionRequest): Promise<Session>;
|
|
33
|
+
/** The session resource returned by `GET`/`PUT /v1/sessions/:id`. */
|
|
34
|
+
export declare function sessionView(t: Tx, s: Session): Promise<unknown>;
|