@nylorun/runtime 0.14.0-beta → 0.15.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 +104 -0
- package/README.md +75 -33
- package/dist/adapters/execution/restate.js +2 -2
- package/dist/adapters/replication/pgoutput.d.ts +5 -0
- package/dist/adapters/replication/pgoutput.js +4 -3
- package/dist/admin-openapi.json +184 -685
- package/dist/api/components.d.ts +308 -68
- package/dist/api/components.js +4 -5
- package/dist/api/gate/keys.d.ts +14 -0
- package/dist/api/gate/keys.js +59 -0
- package/dist/api/gate/routes.d.ts +34 -0
- package/dist/api/gate/routes.js +235 -0
- package/dist/api/http/define.js +0 -8
- package/dist/api/http/routes/access.js +25 -18
- package/dist/api/http/routes/tenant.d.ts +2 -1
- package/dist/api/http/routes/tenant.js +77 -5
- package/dist/api/http/routes/vaults.js +2 -2
- package/dist/api/openapi.js +4 -5
- package/dist/gates/contract.d.ts +51 -0
- package/dist/gates/contract.js +37 -0
- package/dist/gates/handler.d.ts +21 -0
- package/dist/gates/handler.js +55 -0
- package/dist/gates/http-client.d.ts +12 -0
- package/dist/gates/http-client.js +160 -0
- package/dist/gates/in-process.d.ts +32 -0
- package/dist/gates/in-process.js +42 -0
- package/dist/gates/inflight.d.ts +39 -0
- package/dist/gates/inflight.js +70 -0
- package/dist/gates/mcp-handler.d.ts +35 -0
- package/dist/gates/mcp-handler.js +142 -0
- package/dist/gates/meter.d.ts +24 -0
- package/dist/gates/meter.js +165 -0
- package/dist/gates/model-gate.d.ts +45 -0
- package/dist/gates/model-gate.js +1 -0
- package/dist/gates/tenant-vaults.d.ts +64 -0
- package/dist/gates/tenant-vaults.js +94 -0
- package/dist/gates/tool-calls.d.ts +51 -0
- package/dist/gates/tool-calls.js +64 -0
- package/dist/gates/tool-client.d.ts +12 -0
- package/dist/gates/tool-client.js +201 -0
- package/dist/gates/tool-contract.d.ts +116 -0
- package/dist/gates/tool-contract.js +64 -0
- package/dist/gates/tool-gate.d.ts +45 -0
- package/dist/gates/tool-gate.js +10 -0
- package/dist/host/admin-api.d.ts +7 -6
- package/dist/host/admin-api.js +9 -113
- package/dist/host/app.d.ts +1 -1
- package/dist/host/app.js +20 -24
- package/dist/host/config-for.d.ts +1 -1
- package/dist/host/config-for.js +2 -2
- package/dist/host/create-host.d.ts +5 -5
- package/dist/host/create-host.js +5 -15
- package/dist/host/execution.d.ts +20 -18
- package/dist/host/execution.js +2 -2
- package/dist/host/gates.d.ts +58 -0
- package/dist/host/gates.js +127 -0
- package/dist/host/http.d.ts +6 -1
- package/dist/host/http.js +17 -0
- package/dist/host/index.d.ts +1 -1
- package/dist/host/main.js +151 -71
- package/dist/host/stack-config.d.ts +71 -19
- package/dist/host/stack-config.js +192 -18
- package/dist/infra/database.d.ts +2 -2
- package/dist/infra/execution.d.ts +5 -5
- package/dist/infra/execution.js +8 -6
- package/dist/infra/index.d.ts +1 -1
- package/dist/infra/workers.d.ts +17 -17
- package/dist/infra/workers.js +9 -9
- package/dist/keys/client.d.ts +9 -0
- package/dist/keys/client.js +70 -0
- package/dist/keys/contract.d.ts +24 -0
- package/dist/keys/contract.js +12 -0
- package/dist/keys/keys.d.ts +41 -0
- package/dist/keys/keys.js +50 -0
- package/dist/mcp/connect.d.ts +43 -3
- package/dist/mcp/connect.js +12 -5
- package/dist/mcp/pool.d.ts +21 -3
- package/dist/mcp/pool.js +15 -2
- package/dist/openapi.json +887 -694
- package/dist/record/append.d.ts +51 -0
- package/dist/record/append.js +20 -0
- package/dist/{store/event.js → record/envelope.js} +1 -1
- package/dist/record/index.d.ts +11 -0
- package/dist/record/index.js +11 -0
- package/dist/store/postgres/connect.d.ts +15 -12
- package/dist/store/postgres/connect.js +24 -12
- package/dist/store/postgres/db.d.ts +16 -0
- package/dist/store/postgres/db.js +28 -0
- package/dist/store/postgres/drizzle/0000_foundation.sql +22 -0
- package/dist/store/postgres/drizzle/0001_baseline.sql +255 -0
- package/dist/store/postgres/drizzle/0002_stream_relay.sql +4 -0
- package/dist/store/postgres/drizzle/0003_tool_crossings.sql +9 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +34 -0
- package/dist/store/postgres/migrate.d.ts +68 -0
- package/dist/store/postgres/migrate.js +183 -0
- package/dist/store/postgres/record-writer.d.ts +4 -0
- package/dist/store/postgres/record-writer.js +34 -0
- package/dist/store/postgres/record.d.ts +2 -8
- package/dist/store/postgres/record.js +50 -54
- package/dist/store/postgres/schema.d.ts +3096 -0
- package/dist/store/postgres/schema.js +363 -0
- package/dist/store/postgres/store.d.ts +2 -4
- package/dist/store/postgres/store.js +481 -567
- package/dist/store/postgres/tenant.d.ts +56 -0
- package/dist/store/postgres/tenant.js +154 -0
- package/dist/store/types.d.ts +43 -87
- package/dist/store/types.js +0 -1
- package/dist/streams/control.d.ts +1 -1
- package/dist/streams/control.js +6 -2
- package/dist/streams/relay/types.d.ts +2 -1
- package/dist/streams/relay/types.js +2 -1
- package/dist/streams/types.d.ts +5 -0
- package/dist/tenant/advance.d.ts +7 -2
- package/dist/tenant/advance.js +95 -16
- package/dist/tenant/cause.d.ts +25 -0
- package/dist/tenant/cause.js +45 -0
- package/dist/tenant/commands.js +4 -2
- package/dist/tenant/context.d.ts +26 -3
- package/dist/tenant/delivery-token.js +17 -18
- package/dist/tenant/delivery.d.ts +1 -1
- package/dist/tenant/delivery.js +18 -13
- package/dist/tenant/effects.d.ts +15 -1
- package/dist/tenant/effects.js +93 -17
- package/dist/tenant/ephemeral.d.ts +16 -7
- package/dist/tenant/ephemeral.js +45 -111
- package/dist/tenant/history.d.ts +64 -0
- package/dist/tenant/history.js +120 -0
- package/dist/tenant/model-setting.d.ts +2 -2
- package/dist/tenant/module.d.ts +14 -28
- package/dist/tenant/module.js +131 -271
- package/dist/tenant/paths.d.ts +11 -16
- package/dist/tenant/paths.js +24 -68
- package/dist/tenant/pool.d.ts +1 -1
- package/dist/tenant/pool.js +1 -1
- package/dist/tenant/principals.d.ts +23 -11
- package/dist/tenant/principals.js +61 -49
- package/dist/tenant/runtime.d.ts +19 -1
- package/dist/tenant/runtime.js +40 -26
- package/dist/tenant/scheduler.d.ts +4 -2
- package/dist/tenant/scheduler.js +4 -4
- package/dist/tenant/session-streams.d.ts +2 -0
- package/dist/tenant/session-streams.js +8 -3
- package/dist/tenant/status.d.ts +3 -0
- package/dist/tenant/status.js +1 -1
- package/dist/tenant/store-pg.d.ts +9 -6
- package/dist/tenant/store-pg.js +60 -125
- package/dist/tenant/streams.d.ts +9 -25
- package/dist/tenant/streams.js +7 -22
- package/dist/tenant/tokens.js +16 -14
- package/dist/tenant/types.d.ts +41 -92
- package/dist/tenant/types.js +4 -15
- package/dist/tenant/worker.js +4 -4
- package/dist/vault/host-model.d.ts +31 -0
- package/dist/vault/host-model.js +57 -0
- package/dist/vault/service.d.ts +26 -15
- package/dist/vault/service.js +70 -109
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +9 -5
- package/dist/store/memory.d.ts +0 -69
- package/dist/store/memory.js +0 -810
- package/dist/store/postgres/migrations/001_initial.d.ts +0 -27
- package/dist/store/postgres/migrations/001_initial.js +0 -196
- package/dist/store/postgres/migrations/002_session_owner.d.ts +0 -6
- package/dist/store/postgres/migrations/002_session_owner.js +0 -13
- package/dist/store/postgres/migrations/003_subject_tokens.d.ts +0 -8
- package/dist/store/postgres/migrations/003_subject_tokens.js +0 -42
- package/dist/store/postgres/migrations/004_publishable_keys.d.ts +0 -6
- package/dist/store/postgres/migrations/004_publishable_keys.js +0 -18
- package/dist/store/postgres/migrations/005_action_endpoints.d.ts +0 -6
- package/dist/store/postgres/migrations/005_action_endpoints.js +0 -32
- package/dist/store/postgres/migrations/006_drop_executors.d.ts +0 -11
- package/dist/store/postgres/migrations/006_drop_executors.js +0 -32
- package/dist/store/postgres/migrations/007_event_time.d.ts +0 -7
- package/dist/store/postgres/migrations/007_event_time.js +0 -14
- package/dist/store/postgres/migrations/008_durable_streams.d.ts +0 -13
- package/dist/store/postgres/migrations/008_durable_streams.js +0 -35
- package/dist/store/postgres/migrations/index.d.ts +0 -53
- package/dist/store/postgres/migrations/index.js +0 -96
- package/dist/store/postgres/migrations/shared/index.d.ts +0 -31
- package/dist/store/postgres/migrations/shared/index.js +0 -92
- package/dist/store/postgres/names.d.ts +0 -11
- package/dist/store/postgres/names.js +0 -42
- package/dist/store/postgres/tenants.d.ts +0 -90
- package/dist/store/postgres/tenants.js +0 -228
- package/dist/streams/relay/memory.d.ts +0 -69
- package/dist/streams/relay/memory.js +0 -176
- package/dist/tenant/envelope.d.ts +0 -7
- package/dist/tenant/envelope.js +0 -22
- package/dist/tenant/quarantine-error.d.ts +0 -6
- package/dist/tenant/quarantine-error.js +0 -9
- package/dist/tenant/quarantine.d.ts +0 -27
- package/dist/tenant/quarantine.js +0 -72
- package/dist/tenant/store-memory.d.ts +0 -16
- package/dist/tenant/store-memory.js +0 -111
- /package/dist/{store → record}/cursor.d.ts +0 -0
- /package/dist/{store → record}/cursor.js +0 -0
- /package/dist/{store/event.d.ts → record/envelope.d.ts} +0 -0
package/dist/tenant/advance.js
CHANGED
|
@@ -34,8 +34,9 @@ import { runDurable, runFlowDurable, } from "@nylorun/harness/run";
|
|
|
34
34
|
import { aggregateWaits, cancelSiblingWork, isWorkflowManifest, wakeLinkedWorkflow, } from "../core/flow-host.js";
|
|
35
35
|
import { isOwnershipLost, ownedTx } from "../store/ownership.js";
|
|
36
36
|
import { linkedAgentOutput, rebaseSessionState, sessionToolsOf, turnManifestOf, } from "./session.js";
|
|
37
|
-
import { prepareMcp, resolveEffect } from "./effects.js";
|
|
37
|
+
import { isRemoteMcpEffect, prepareMcp, recoversMcpCalls, recoversModelCalls, resolveEffect, } from "./effects.js";
|
|
38
38
|
import { slimModelEffects } from "./slim.js";
|
|
39
|
+
import { checkParity, foldTranscript, leanState, transcriptOf, transcriptUpdates, withTranscript, } from "./history.js";
|
|
39
40
|
import { command } from "./commands.js";
|
|
40
41
|
import { usesFixtureModel } from "./model-setting.js";
|
|
41
42
|
import { toolFixtureModel } from "../core/provider.js";
|
|
@@ -86,6 +87,7 @@ export async function advance(ctx, id, signal) {
|
|
|
86
87
|
else
|
|
87
88
|
signal.addEventListener("abort", forward, { once: true });
|
|
88
89
|
ctx.work.running.set(id, controller);
|
|
90
|
+
ctx.work.runningTurns.set(id, taken.session.activeTurnId ?? null);
|
|
89
91
|
const heartbeat = startHeartbeat(ctx, lease, controller);
|
|
90
92
|
let result = DONE;
|
|
91
93
|
try {
|
|
@@ -111,8 +113,10 @@ export async function advance(ctx, id, signal) {
|
|
|
111
113
|
finally {
|
|
112
114
|
heartbeat.stop();
|
|
113
115
|
signal.removeEventListener("abort", forward);
|
|
114
|
-
if (ctx.work.running.get(id) === controller)
|
|
116
|
+
if (ctx.work.running.get(id) === controller) {
|
|
115
117
|
ctx.work.running.delete(id);
|
|
118
|
+
ctx.work.runningTurns.delete(id);
|
|
119
|
+
}
|
|
116
120
|
// Best effort: a release that fails leaves a lease that simply expires.
|
|
117
121
|
await ctx.store
|
|
118
122
|
.tx((t) => t.releaseOwnership(id, lease.owner, lease.epoch))
|
|
@@ -147,7 +151,10 @@ async function takeOwnership(ctx, id) {
|
|
|
147
151
|
};
|
|
148
152
|
const s = await t.assertEpoch(id, taken.epoch);
|
|
149
153
|
if (taken.takeover)
|
|
150
|
-
await takeOver(t, s
|
|
154
|
+
await takeOver(t, s, {
|
|
155
|
+
recoversModelCalls: recoversModelCalls(ctx),
|
|
156
|
+
recoversMcpCalls: recoversMcpCalls(ctx),
|
|
157
|
+
});
|
|
151
158
|
if (!s.checkpoint || !["running", "runnable"].includes(s.status)) {
|
|
152
159
|
await t.releaseOwnership(id, lease.owner, lease.epoch);
|
|
153
160
|
return { status: "idle" };
|
|
@@ -159,13 +166,20 @@ async function takeOwnership(ctx, id) {
|
|
|
159
166
|
}
|
|
160
167
|
/**
|
|
161
168
|
* Takeover (§10.5 step 2, §11.4), inside the transaction that took ownership: the dead
|
|
162
|
-
* owner's `invoking` effects become `uncertain` and are never invoked again
|
|
169
|
+
* owner's `invoking` effects become `uncertain` and are never invoked again, except model
|
|
170
|
+
* calls when the gate recovers them (P1.2) and remote MCP calls when the Tool Gate does (F4.1
|
|
171
|
+
* G3): those stay `invoking`, and the replay re-sends them. When one belongs
|
|
163
172
|
* to the active turn, the session becomes `uncertain` too, with an `effect.uncertain` event.
|
|
164
173
|
* Mutates and writes `s`. Returns the effect ids it marked.
|
|
165
174
|
*/
|
|
166
|
-
export async function takeOver(t, s) {
|
|
175
|
+
export async function takeOver(t, s, options = {}) {
|
|
167
176
|
const marked = [];
|
|
168
177
|
for (const effect of await t.invokingEffects(s.id)) {
|
|
178
|
+
// Still running at the gate, or finished there: the replay re-sends it (P1.2).
|
|
179
|
+
if (options.recoversModelCalls && effect.request.kind === "model")
|
|
180
|
+
continue;
|
|
181
|
+
if (options.recoversMcpCalls && isRemoteMcpEffect(s, effect.request))
|
|
182
|
+
continue;
|
|
169
183
|
effect.status = "uncertain";
|
|
170
184
|
effect.error ??= "The Worker running this effect stopped before its outcome was recorded";
|
|
171
185
|
await t.put("effects", effect.request.effectId, effect);
|
|
@@ -238,12 +252,24 @@ async function runSegment(ctx, lease, started, signal) {
|
|
|
238
252
|
const { current, fixtureModel } = await ownedTx(ctx.store, id, lease.epoch, async (t, current) => {
|
|
239
253
|
if (!isWorkflowManifest(current.manifest) && current.checkpoint) {
|
|
240
254
|
rebaseSessionState(current, current.checkpoint.manifestHash);
|
|
255
|
+
await adoptStoredTranscript(t, current);
|
|
241
256
|
const cp = current.checkpoint;
|
|
242
257
|
current.checkpoint = { ...cp, state: current.state };
|
|
243
258
|
await t.put("sessions", id, current);
|
|
244
259
|
}
|
|
245
260
|
return { current, fixtureModel: await usesFixtureModel(t) };
|
|
246
261
|
});
|
|
262
|
+
// One history (P0.3): the engine resumes from the transcript folded from the record.
|
|
263
|
+
let startTranscript = [];
|
|
264
|
+
let engineCheckpoint = current.checkpoint;
|
|
265
|
+
if (!isWorkflowManifest(current.manifest) && current.checkpoint) {
|
|
266
|
+
const cp = current.checkpoint;
|
|
267
|
+
if (cp.state) {
|
|
268
|
+
startTranscript = await foldSession(ctx, current);
|
|
269
|
+
checkParity(id, startTranscript, transcriptOf(cp.state));
|
|
270
|
+
engineCheckpoint = { ...cp, state: withTranscript(cp.state, startTranscript) };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
247
273
|
const segment = fixtureModel ? { model: fixture } : {};
|
|
248
274
|
const host = {
|
|
249
275
|
resolveEffect: (e) => resolveEffect(ctx, e, signal, lease, segment),
|
|
@@ -258,7 +284,7 @@ async function runSegment(ctx, lease, started, signal) {
|
|
|
258
284
|
})
|
|
259
285
|
: await runDurable({
|
|
260
286
|
manifest: turnManifestOf(current),
|
|
261
|
-
checkpoint:
|
|
287
|
+
checkpoint: engineCheckpoint,
|
|
262
288
|
signal,
|
|
263
289
|
sessionTools: sessionToolsOf(current.mcpSnapshot),
|
|
264
290
|
host,
|
|
@@ -273,8 +299,15 @@ async function runSegment(ctx, lease, started, signal) {
|
|
|
273
299
|
if (signal.reason instanceof AdvanceDeadlineError &&
|
|
274
300
|
(result.status === "cancelled" || result.status === "failed"))
|
|
275
301
|
await settleFailure(ctx, lease, started, signal.reason);
|
|
276
|
-
else
|
|
277
|
-
|
|
302
|
+
else {
|
|
303
|
+
const state = !isWorkflowManifest(current.manifest) &&
|
|
304
|
+
(result.status === "yielded" || result.status === "completed" || result.status === "paused") &&
|
|
305
|
+
"result" in result
|
|
306
|
+
? result.result?.state
|
|
307
|
+
: undefined;
|
|
308
|
+
const updates = state ? transcriptUpdates(startTranscript, transcriptOf(state)) : [];
|
|
309
|
+
await settle(ctx, lease, started, result, updates);
|
|
310
|
+
}
|
|
278
311
|
}
|
|
279
312
|
catch (error) {
|
|
280
313
|
if (isOwnershipLost(error) || error instanceof SegmentStopped)
|
|
@@ -312,7 +345,7 @@ async function wakeWorkflowOf(ctx, t, agent, turnId, type, payload) {
|
|
|
312
345
|
});
|
|
313
346
|
}
|
|
314
347
|
/** Record a segment result, then cancel failed siblings and wake a linked workflow. */
|
|
315
|
-
async function settle(ctx, lease, s, result) {
|
|
348
|
+
async function settle(ctx, lease, s, result, updates = []) {
|
|
316
349
|
const id = lease.sessionId;
|
|
317
350
|
const siblingCancelIds = await ownedTx(ctx.store, id, lease.epoch, async (t, current) => {
|
|
318
351
|
const siblings = [];
|
|
@@ -348,7 +381,8 @@ async function settle(ctx, lease, s, result) {
|
|
|
348
381
|
if (result.status === "yielded") {
|
|
349
382
|
// The turn goes on in a new segment: same turn, next checkpoint, woken right away.
|
|
350
383
|
const finished = result.checkpoint;
|
|
351
|
-
|
|
384
|
+
await recordTranscript(t, id, s.activeTurnId, current, updates);
|
|
385
|
+
current.state = leanState(result.result.state);
|
|
352
386
|
current.checkpoint = {
|
|
353
387
|
...finished,
|
|
354
388
|
segment: finished.segment + 1,
|
|
@@ -357,7 +391,6 @@ async function settle(ctx, lease, s, result) {
|
|
|
357
391
|
};
|
|
358
392
|
current.status = "runnable";
|
|
359
393
|
current.waits = undefined;
|
|
360
|
-
await t.put("checkpoints", JSON.stringify([id, s.activeTurnId, finished.segment]), { checkpoint: finished, status: "yielded" });
|
|
361
394
|
await slimModelEffects(t, id, s.activeTurnId);
|
|
362
395
|
await t.put("sessions", id, current);
|
|
363
396
|
const segment = finished.segment + 1;
|
|
@@ -369,13 +402,14 @@ async function settle(ctx, lease, s, result) {
|
|
|
369
402
|
}
|
|
370
403
|
const flow = isWorkflowManifest(current.manifest);
|
|
371
404
|
if (!flow) {
|
|
405
|
+
if (result.status !== "failed")
|
|
406
|
+
await recordTranscript(t, id, s.activeTurnId, current, updates);
|
|
372
407
|
current.state =
|
|
373
408
|
result.status === "failed"
|
|
374
409
|
? current.turnStartState
|
|
375
|
-
: result.result.state;
|
|
410
|
+
: leanState(result.result.state);
|
|
376
411
|
}
|
|
377
|
-
current.checkpoint = result.checkpoint;
|
|
378
|
-
await t.put("checkpoints", JSON.stringify([id, s.activeTurnId, result.checkpoint.segment]), { checkpoint: result.checkpoint, status: result.status });
|
|
412
|
+
current.checkpoint = leanCheckpoint(result.checkpoint);
|
|
379
413
|
current.status = result.status;
|
|
380
414
|
current.waits =
|
|
381
415
|
result.status === "paused" ? result.result.pending : undefined;
|
|
@@ -456,8 +490,6 @@ async function settleFailure(ctx, lease, s, error) {
|
|
|
456
490
|
return;
|
|
457
491
|
current.status = "failed";
|
|
458
492
|
current.state = current.turnStartState;
|
|
459
|
-
if (current.checkpoint)
|
|
460
|
-
await t.put("checkpoints", JSON.stringify([id, s.activeTurnId, current.checkpoint.segment]), { checkpoint: current.checkpoint, status: "failed" });
|
|
461
493
|
current.error = error instanceof Error ? error.message : String(error);
|
|
462
494
|
const payload = { message: current.error };
|
|
463
495
|
await t.event(id, current.activeTurnId, "turn.failed", payload);
|
|
@@ -469,3 +501,50 @@ async function settleFailure(ctx, lease, s, error) {
|
|
|
469
501
|
await wakeWorkflowOf(ctx, t, current, s.activeTurnId, "turn.failed", payload);
|
|
470
502
|
});
|
|
471
503
|
}
|
|
504
|
+
/** `checkpoint` with its engine state stored lean (`history.ts`). */
|
|
505
|
+
function leanCheckpoint(checkpoint) {
|
|
506
|
+
const state = checkpoint?.state;
|
|
507
|
+
if (!checkpoint || !state)
|
|
508
|
+
return checkpoint;
|
|
509
|
+
return { ...checkpoint, state: leanState(state) };
|
|
510
|
+
}
|
|
511
|
+
/** Appends a segment's transcript edits; a snapshot moves `history.snapshot`. */
|
|
512
|
+
async function recordTranscript(t, id, turnId, s, updates) {
|
|
513
|
+
for (const update of updates) {
|
|
514
|
+
const event = await t.event(id, turnId, "transcript.updated", update);
|
|
515
|
+
if (update.keep === 0)
|
|
516
|
+
s.history = { from: s.history?.from ?? event.seq, snapshot: event.seq };
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* A session written before transcripts were recorded keeps its transcript on its row: record
|
|
521
|
+
* it once (the turn's starting transcript, then the turn so far), and store the row lean.
|
|
522
|
+
*/
|
|
523
|
+
async function adoptStoredTranscript(t, s) {
|
|
524
|
+
if (s.history)
|
|
525
|
+
return;
|
|
526
|
+
const now = transcriptOf(s.state);
|
|
527
|
+
const start = s.activeTurnId ? transcriptOf(s.turnStartState) : now;
|
|
528
|
+
if (now.length === 0 && start.length === 0)
|
|
529
|
+
return;
|
|
530
|
+
let first;
|
|
531
|
+
const write = async (turnId, updates) => {
|
|
532
|
+
for (const update of updates) {
|
|
533
|
+
const event = await t.event(s.id, turnId, "transcript.updated", update);
|
|
534
|
+
first ??= event.seq;
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
await write(null, transcriptUpdates([], start));
|
|
538
|
+
if (s.activeTurnId)
|
|
539
|
+
await write(s.activeTurnId, transcriptUpdates(start, now));
|
|
540
|
+
s.history = { from: first ?? 0, snapshot: first ?? 0 };
|
|
541
|
+
s.state = leanState(s.state);
|
|
542
|
+
s.turnStartState = leanState(s.turnStartState);
|
|
543
|
+
}
|
|
544
|
+
/** The session's transcript, folded from its record from `history.from`. */
|
|
545
|
+
async function foldSession(ctx, s) {
|
|
546
|
+
const rows = await ctx.store
|
|
547
|
+
.record()
|
|
548
|
+
.readRange(ctx.store.tenantId, s.id, s.history?.from ?? 0, Number.MAX_SAFE_INTEGER);
|
|
549
|
+
return foldTranscript(rows.map((row) => row.body));
|
|
550
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why the Host's Tenant could not be opened (tenancy.md §5). A cause is about the Tenant's
|
|
3
|
+
* own data or files (its database is newer than this Runtime, its vault key is missing, the
|
|
4
|
+
* database has an old layout): the Host fails readiness, names the cause in
|
|
5
|
+
* `/v1/admin/status` and the logs, and answers Tenant requests with the opaque 404 until it is
|
|
6
|
+
* repaired and the Host restarted. A failure outside the Tenant (Postgres unreachable) is not
|
|
7
|
+
* a cause: it is `TenantUnavailableError`, and the next request tries again.
|
|
8
|
+
*/
|
|
9
|
+
import type { TenantCause, TenantEnvelope } from "@nylorun/core/contracts";
|
|
10
|
+
export type { TenantCause };
|
|
11
|
+
/** Thrown by migrations, the Tenant bootstrap and the Tenant Runtime when the Tenant cannot open. */
|
|
12
|
+
export declare class TenantOpenError extends Error {
|
|
13
|
+
readonly code: TenantCause["code"];
|
|
14
|
+
readonly repair: string;
|
|
15
|
+
/** The Tenant, when it was read before opening failed (`/v1/admin/status` names it). */
|
|
16
|
+
envelope?: TenantEnvelope;
|
|
17
|
+
constructor(fields: TenantCause);
|
|
18
|
+
toCause(): TenantCause;
|
|
19
|
+
}
|
|
20
|
+
/** A `TenantOpenError` with the standard repair for `code`. */
|
|
21
|
+
export declare function openError(code: TenantCause["code"], message: string): TenantOpenError;
|
|
22
|
+
/** The cause an error carries, when it is a `TenantOpenError`. */
|
|
23
|
+
export declare function causeOf(error: unknown): TenantCause | undefined;
|
|
24
|
+
/** What an operator does about each cause. */
|
|
25
|
+
export declare function repairFor(code: TenantCause["code"]): string;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Thrown by migrations, the Tenant bootstrap and the Tenant Runtime when the Tenant cannot open. */
|
|
2
|
+
export class TenantOpenError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
repair;
|
|
5
|
+
/** The Tenant, when it was read before opening failed (`/v1/admin/status` names it). */
|
|
6
|
+
envelope;
|
|
7
|
+
constructor(fields) {
|
|
8
|
+
super(fields.message);
|
|
9
|
+
this.name = "TenantOpenError";
|
|
10
|
+
this.code = fields.code;
|
|
11
|
+
this.repair = fields.repair;
|
|
12
|
+
}
|
|
13
|
+
toCause() {
|
|
14
|
+
return { code: this.code, message: this.message, repair: this.repair };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** A `TenantOpenError` with the standard repair for `code`. */
|
|
18
|
+
export function openError(code, message) {
|
|
19
|
+
return new TenantOpenError({ code, message, repair: repairFor(code) });
|
|
20
|
+
}
|
|
21
|
+
/** The cause an error carries, when it is a `TenantOpenError`. */
|
|
22
|
+
export function causeOf(error) {
|
|
23
|
+
return error instanceof TenantOpenError ? error.toCause() : undefined;
|
|
24
|
+
}
|
|
25
|
+
/** What an operator does about each cause. */
|
|
26
|
+
export function repairFor(code) {
|
|
27
|
+
switch (code) {
|
|
28
|
+
case "kek-missing":
|
|
29
|
+
return "restore the vault key (vault-kek in the Host root's keys/ directory) from a backup, then restart the Runtime and its gateway: the Tenant's ciphertext cannot be opened without it";
|
|
30
|
+
case "corrupt":
|
|
31
|
+
return "restore the database from a backup, then restart the Runtime";
|
|
32
|
+
case "schema-too-new":
|
|
33
|
+
return "the database was migrated by a newer Runtime: run that version or newer";
|
|
34
|
+
case "migration-failed":
|
|
35
|
+
return "inspect the Runtime log for the failed migration, fix it, then restart the Runtime";
|
|
36
|
+
case "envelope-invalid":
|
|
37
|
+
return "the nylorun.tenant row is missing or invalid: restore the database from a backup";
|
|
38
|
+
case "open-timeout":
|
|
39
|
+
return "opening the Tenant timed out: inspect the Runtime log and Postgres, then restart the Runtime";
|
|
40
|
+
case "open-failed":
|
|
41
|
+
return "inspect the Runtime log, repair what it names, then restart the Runtime";
|
|
42
|
+
case "database-layout-old":
|
|
43
|
+
return "this release starts fresh: point the Runtime at a new database (a new stack: `nylorun start`); the old database is left as it is";
|
|
44
|
+
}
|
|
45
|
+
}
|
package/dist/tenant/commands.js
CHANGED
|
@@ -153,7 +153,7 @@ export async function command(ctx, id, input, scope) {
|
|
|
153
153
|
});
|
|
154
154
|
await slimModelEffects(t, id, cancelledTurnId);
|
|
155
155
|
// The process running the advance aborts it on `session.cancel` (tenant/control).
|
|
156
|
-
t.afterCommit(() => signalSessionCancel(ctx, id));
|
|
156
|
+
t.afterCommit(() => signalSessionCancel(ctx, id, cancelledTurnId));
|
|
157
157
|
s.activeTurnId = null;
|
|
158
158
|
if (cancelledTurnId !== null)
|
|
159
159
|
s.lastTurnId = cancelledTurnId;
|
|
@@ -182,6 +182,9 @@ export async function command(ctx, id, input, scope) {
|
|
|
182
182
|
if (scope.kind === "token" && scope.limits)
|
|
183
183
|
await chargeTurn(t, scope.subject, scope.limits);
|
|
184
184
|
s.turnStartState = s.state;
|
|
185
|
+
// A new turn folds from the latest snapshot: a cancel or failure only reverts this turn.
|
|
186
|
+
if (s.history?.snapshot !== undefined)
|
|
187
|
+
s.history = { ...s.history, from: s.history.snapshot };
|
|
185
188
|
s.activeTurnId = randomUUID();
|
|
186
189
|
if (isWorkflowManifest(s.manifest)) {
|
|
187
190
|
const input = "content" in command ? command.content : command.data;
|
|
@@ -263,7 +266,6 @@ export async function command(ctx, id, input, scope) {
|
|
|
263
266
|
segment: (s.checkpoint?.segment ?? 0) + 1,
|
|
264
267
|
});
|
|
265
268
|
}
|
|
266
|
-
await t.put("checkpoints", JSON.stringify([id, s.activeTurnId, s.checkpoint.segment]), { checkpoint: s.checkpoint, status: "runnable" });
|
|
267
269
|
s.status = "runnable";
|
|
268
270
|
s.waits = undefined;
|
|
269
271
|
const commandWake = {
|
package/dist/tenant/context.d.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* `wake` goes to `DurableExecution.wake`, which calls the Tenant's `advance` (`advance.ts`)
|
|
14
14
|
* under ownership (§10.6); `abortLocal` aborts an advance running on this process.
|
|
15
15
|
*/
|
|
16
|
+
import type { SessionHistory } from "./history.js";
|
|
16
17
|
import type { RoleLimits, SubjectScope, TenantEnvelope } from "@nylorun/core/contracts";
|
|
17
18
|
import type { DurableCheckpoint, FlowCheckpoint } from "@nylorun/harness/run";
|
|
18
19
|
import type { AgentManifest, JsonValue, SandboxManifest } from "@nylorun/core/define";
|
|
@@ -20,6 +21,9 @@ import type { CredentialSelection } from "@nylorun/core/contracts";
|
|
|
20
21
|
import type { SessionStore, Tx } from "../store/types.js";
|
|
21
22
|
import type { FlowLimits } from "../core/limits.js";
|
|
22
23
|
import type { ModelProvider } from "../core/provider.js";
|
|
24
|
+
import type { ModelGate } from "../gates/model-gate.js";
|
|
25
|
+
import type { ToolGate } from "../gates/tool-gate.js";
|
|
26
|
+
import type { Keys } from "../keys/keys.js";
|
|
23
27
|
import type { VaultService } from "../vault/service.js";
|
|
24
28
|
import type { McpPool } from "../mcp/pool.js";
|
|
25
29
|
import type { McpDiagnostic, McpSnapshot } from "../mcp/snapshot.js";
|
|
@@ -43,8 +47,11 @@ export interface Session {
|
|
|
43
47
|
status: string;
|
|
44
48
|
activeTurnId: string | null;
|
|
45
49
|
checkpoint?: DurableCheckpoint | FlowCheckpoint;
|
|
50
|
+
/** The engine state; its transcript is folded from the record (`history.ts`), not stored. */
|
|
46
51
|
state?: any;
|
|
47
52
|
turnStartState?: any;
|
|
53
|
+
/** Where the transcript's fold starts in the session's record (`history.ts`). */
|
|
54
|
+
history?: SessionHistory;
|
|
48
55
|
waits?: unknown;
|
|
49
56
|
error?: string;
|
|
50
57
|
/** Last completed turn output (for linked agent → workflow settle). */
|
|
@@ -135,8 +142,21 @@ export interface TenantContext {
|
|
|
135
142
|
readonly sandbox: SandboxManager;
|
|
136
143
|
readonly flowLimits: FlowLimits;
|
|
137
144
|
readonly modelProvider: ModelProvider;
|
|
138
|
-
/** True when the model comes from the Tenant vault selection
|
|
145
|
+
/** True when the model comes from the Tenant vault selection, served by `modelGate`. */
|
|
139
146
|
readonly useVaultModel: boolean;
|
|
147
|
+
/** Serves vault-backed model calls (blueprint §15): in this process, or the gates service. */
|
|
148
|
+
readonly modelGate: ModelGate;
|
|
149
|
+
/**
|
|
150
|
+
* Serves remote MCP servers and Action deliveries (blueprint §12, F4.1): in this process, or
|
|
151
|
+
* the gates service.
|
|
152
|
+
*/
|
|
153
|
+
readonly toolGate: ToolGate;
|
|
154
|
+
/**
|
|
155
|
+
* Vault writes that touch a secret, and all token signing (blueprint §14, F4.2): in this
|
|
156
|
+
* process, or the gateway's keys service. Call it instead of `vault`'s sealing methods and
|
|
157
|
+
* `signingKeys`' private key.
|
|
158
|
+
*/
|
|
159
|
+
readonly keys: Keys;
|
|
140
160
|
/** Set by drain/close; reset clears it again. Stops new advances. */
|
|
141
161
|
closing: boolean;
|
|
142
162
|
/** Set once close has finished releasing resources. */
|
|
@@ -157,8 +177,11 @@ export interface TenantContext {
|
|
|
157
177
|
* re-wakes anything left runnable.
|
|
158
178
|
*/
|
|
159
179
|
wake(sessionId: string, wake: Wake): Promise<void>;
|
|
160
|
-
/**
|
|
161
|
-
|
|
180
|
+
/**
|
|
181
|
+
* Seam: abort the advance of a session running in this process, if any. With `turnId`, only
|
|
182
|
+
* an advance of that turn (a cancel signal names the turn it cancelled).
|
|
183
|
+
*/
|
|
184
|
+
abortLocal(sessionId: string, turnId?: string): void;
|
|
162
185
|
/**
|
|
163
186
|
* Seam: deliver an Action to its agent's Action endpoint (`DurableExecution.deliver`). Call it
|
|
164
187
|
* from `t.afterCommit`. Dropped while the Tenant is closing; the sweep re-sends it.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* (`signing-keys.ts`), so a rotation never revokes a live token.
|
|
11
11
|
*/
|
|
12
12
|
import { createHash, randomUUID } from "node:crypto";
|
|
13
|
-
import { errors, jwtVerify
|
|
13
|
+
import { errors, jwtVerify } from "jose";
|
|
14
14
|
import { DELIVERY_TOKEN_MAX_TTL_SECONDS, DELIVERY_TOKEN_TYPE, subjectTokenIssuer, } from "@nylorun/core/contracts";
|
|
15
15
|
import { failOpaque, HttpError } from "./http.js";
|
|
16
16
|
import { CLOCK_TOLERANCE_SECONDS, tokenKeyId } from "./jwt.js";
|
|
@@ -23,27 +23,26 @@ export function bodyHash(body) {
|
|
|
23
23
|
* is capped at `DELIVERY_TOKEN_MAX_TTL_SECONDS`.
|
|
24
24
|
*/
|
|
25
25
|
export async function mintDeliveryToken(ctx, options) {
|
|
26
|
-
const kek = ctx.signingKeys.kek();
|
|
27
|
-
const row = await ctx.store.tx((t) => ctx.signingKeys.ensure(t, kek));
|
|
28
|
-
const signer = await ctx.signingKeys.privateKey(row, kek);
|
|
29
26
|
const ttl = Math.max(1, Math.min(Math.floor(options.ttlSeconds), DELIVERY_TOKEN_MAX_TTL_SECONDS));
|
|
30
27
|
const iat = Math.floor(Date.now() / 1000);
|
|
31
28
|
const tokenId = randomUUID();
|
|
32
29
|
const delivery = options.for;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
30
|
+
// Signed by the keys service (F4.2): this process never holds the private key.
|
|
31
|
+
const { token, keyId } = await ctx.keys.sign({
|
|
32
|
+
typ: DELIVERY_TOKEN_TYPE,
|
|
33
|
+
claims: {
|
|
34
|
+
agt: delivery.agentId,
|
|
35
|
+
gen: delivery.kind === "action" ? delivery.generation : 0,
|
|
36
|
+
bdy: bodyHash(options.body),
|
|
37
|
+
iss: subjectTokenIssuer(ctx.config.tenantId),
|
|
38
|
+
aud: options.audience,
|
|
39
|
+
sub: delivery.kind === "action" ? delivery.actionId : "ping",
|
|
40
|
+
iat,
|
|
41
|
+
exp: iat + ttl,
|
|
42
|
+
jti: tokenId,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
return { token, expiresAt: (iat + ttl) * 1000, tokenId, keyId };
|
|
47
46
|
}
|
|
48
47
|
/**
|
|
49
48
|
* Verifies a delivery token presented as a bearer. Answers `401 token_expired` for an expired
|
|
@@ -3,7 +3,7 @@ import type { DeliverResult } from "../execution/types.js";
|
|
|
3
3
|
import type { Tx } from "../store/types.js";
|
|
4
4
|
import { type Session, type TenantContext } from "./context.js";
|
|
5
5
|
import { type DeliveryScope } from "./delivery-token.js";
|
|
6
|
-
import {
|
|
6
|
+
import type { OutboundResult } from "./outbound.js";
|
|
7
7
|
/**
|
|
8
8
|
* Offers an Action that just became `pending` to its agent's endpoint, after the transaction
|
|
9
9
|
* commits. Without an endpoint it waits for the agent's next registration (`PUT /v1/endpoints`).
|
package/dist/tenant/delivery.js
CHANGED
|
@@ -27,7 +27,6 @@ import { acceptedOutcome, recordActionOutcome } from "./commands.js";
|
|
|
27
27
|
import { sandboxLookup } from "./context.js";
|
|
28
28
|
import { mintDeliveryToken } from "./delivery-token.js";
|
|
29
29
|
import { fail } from "./http.js";
|
|
30
|
-
import { post } from "./outbound.js";
|
|
31
30
|
const DONE = { status: "done" };
|
|
32
31
|
/** Extra time past the endpoint's timeout before an unanswered delivery counts as lost. */
|
|
33
32
|
const DEADLINE_GRACE_MS = 5_000;
|
|
@@ -115,14 +114,17 @@ export async function deliverAction(ctx, actionId, signal) {
|
|
|
115
114
|
const inFlight = track(ctx, action.sessionId, cancel);
|
|
116
115
|
let result;
|
|
117
116
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
117
|
+
// Through the Tool Gate (F4.1): the gates service POSTs it under its own address policy.
|
|
118
|
+
result = await ctx.toolGate.post({
|
|
119
|
+
url: endpoint.url,
|
|
120
|
+
body,
|
|
121
|
+
headers: {
|
|
122
|
+
[SIGNATURE_HEADER]: minted.token,
|
|
123
|
+
[PROTOCOL_HEADER]: String(PROTOCOL_VERSION),
|
|
124
|
+
"idempotency-key": actionId,
|
|
125
|
+
},
|
|
126
|
+
timeoutMs: endpoint.timeoutMs,
|
|
127
|
+
}, AbortSignal.any([signal, cancel.signal, AbortSignal.timeout(endpoint.timeoutMs)]));
|
|
126
128
|
}
|
|
127
129
|
finally {
|
|
128
130
|
inFlight();
|
|
@@ -370,10 +372,13 @@ export async function pingEndpoint(ctx, agentId) {
|
|
|
370
372
|
body,
|
|
371
373
|
ttlSeconds: TOKEN_SLACK_SECONDS,
|
|
372
374
|
});
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
375
|
+
const timeoutMs = Math.min(endpoint.timeoutMs, 10_000);
|
|
376
|
+
const result = await ctx.toolGate.post({
|
|
377
|
+
url: endpoint.url,
|
|
378
|
+
body,
|
|
379
|
+
headers: { [SIGNATURE_HEADER]: minted.token, [PROTOCOL_HEADER]: String(PROTOCOL_VERSION) },
|
|
380
|
+
timeoutMs,
|
|
381
|
+
}, AbortSignal.timeout(timeoutMs));
|
|
377
382
|
const at = new Date().toISOString();
|
|
378
383
|
const refuse = async (code, message) => {
|
|
379
384
|
await ctx.store.tx((t) => t.recordEndpointHealth(agentId, { kind: "failure", at, code, message }));
|
package/dist/tenant/effects.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EffectResolution, HostEffect } from "@nylorun/harness/run";
|
|
2
2
|
import type { AuthorizeResult } from "../vault/service.js";
|
|
3
|
-
import { type Lease, type TenantContext } from "./context.js";
|
|
3
|
+
import { type Lease, type Session, type TenantContext } from "./context.js";
|
|
4
4
|
import type { ModelProvider } from "../core/provider.js";
|
|
5
5
|
/** What one advance's segment decides for all its effects. */
|
|
6
6
|
export interface SegmentOptions {
|
|
@@ -13,6 +13,20 @@ export interface SegmentOptions {
|
|
|
13
13
|
* advance decides what the abort means.
|
|
14
14
|
*/
|
|
15
15
|
export declare function invokeModel(ctx: TenantContext, request: HostEffect, signal: AbortSignal, model?: ModelProvider): Promise<unknown>;
|
|
16
|
+
/**
|
|
17
|
+
* True when this Tenant's remote MCP calls outlive the process that sent them (the gates
|
|
18
|
+
* service, F4.1 G3): after a takeover or a shutdown, the journaled call is re-sent and joins
|
|
19
|
+
* the running call or gets its answer, instead of becoming `uncertain`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function recoversMcpCalls(ctx: TenantContext): boolean;
|
|
22
|
+
/** True when `request` calls a tool of a remote (`streamable-http` or `sse`) MCP server. */
|
|
23
|
+
export declare function isRemoteMcpEffect(s: Session, request: HostEffect): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* True when this Tenant's vault-backed model calls outlive the process that sent them (the
|
|
26
|
+
* gates service, P1.2): after a takeover or a shutdown, the journaled call is re-sent and joins
|
|
27
|
+
* the running call or gets its outcome, instead of becoming `uncertain`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function recoversModelCalls(ctx: TenantContext): boolean;
|
|
16
30
|
export declare function resolveEffect(ctx: TenantContext, request: HostEffect, signal: AbortSignal, lease: Lease, segment?: SegmentOptions): Promise<EffectResolution>;
|
|
17
31
|
/** Journal and dispatch a new flow effect (agent / tool node / fn / verify). */
|
|
18
32
|
export declare function resolveNewFlowEffect(ctx: TenantContext, request: HostEffect, signal: AbortSignal, lease: Lease): Promise<EffectResolution>;
|