@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/effects.js
CHANGED
|
@@ -3,9 +3,8 @@ import { countActiveFlowWork, deriveAgentEffectSessionId, isFlowEffect, isFlowTo
|
|
|
3
3
|
import { mayDispatchMore } from "../core/limits.js";
|
|
4
4
|
import { canonical } from "../store/canonical.js";
|
|
5
5
|
import { isOwnershipLost } from "../store/ownership.js";
|
|
6
|
-
import { piModel } from "../model/pi-model.js";
|
|
7
6
|
import { scrub } from "../redact.js";
|
|
8
|
-
import { serversOf } from "../mcp/pool.js";
|
|
7
|
+
import { findServer, serversOf } from "../mcp/pool.js";
|
|
9
8
|
import { sandboxCapabilityOf } from "../sandbox/manager.js";
|
|
10
9
|
import { owningSandboxSessionId, sandboxSpecOf, sessionSandboxSpec, } from "../sandbox/share.js";
|
|
11
10
|
import { withSandboxCapability } from "../sandbox/session-sandbox.js";
|
|
@@ -28,22 +27,24 @@ export async function invokeModel(ctx, request, signal, model) {
|
|
|
28
27
|
return await model(request, signal);
|
|
29
28
|
if (!ctx.useVaultModel)
|
|
30
29
|
return await ctx.modelProvider(request, signal);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return await adapter(request.input, {
|
|
38
|
-
request: request.context.request,
|
|
30
|
+
return await ctx.modelGate.call({
|
|
31
|
+
tenantId: ctx.config.tenantId,
|
|
32
|
+
sessionId: request.sessionId,
|
|
33
|
+
turnId: request.turnId,
|
|
34
|
+
agentId: request.agentId,
|
|
35
|
+
effectId: request.effectId,
|
|
39
36
|
invocationId: String(request.context.invocationId),
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
});
|
|
37
|
+
call: request.input,
|
|
38
|
+
}, signal);
|
|
43
39
|
}
|
|
44
40
|
catch (error) {
|
|
45
|
-
if (signal.aborted)
|
|
41
|
+
if (signal.aborted) {
|
|
42
|
+
// A call that outlives this process stops only when told to: on a user cancel. After a
|
|
43
|
+
// shutdown or a lost lease the next owner re-sends it and picks up its outcome (P1.2).
|
|
44
|
+
if (!model && ctx.useVaultModel && abortKind(signal) === "cancel" && ctx.modelGate.cancel)
|
|
45
|
+
await ctx.modelGate.cancel({ tenantId: ctx.config.tenantId, effectId: request.effectId });
|
|
46
46
|
throw error;
|
|
47
|
+
}
|
|
47
48
|
return classifyThrown(error);
|
|
48
49
|
}
|
|
49
50
|
}
|
|
@@ -68,6 +69,32 @@ async function linkedOutcome(t, effect, agent) {
|
|
|
68
69
|
},
|
|
69
70
|
};
|
|
70
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* True when this Tenant's remote MCP calls outlive the process that sent them (the gates
|
|
74
|
+
* service, F4.1 G3): after a takeover or a shutdown, the journaled call is re-sent and joins
|
|
75
|
+
* the running call or gets its answer, instead of becoming `uncertain`.
|
|
76
|
+
*/
|
|
77
|
+
export function recoversMcpCalls(ctx) {
|
|
78
|
+
return ctx.toolGate.recovers === true;
|
|
79
|
+
}
|
|
80
|
+
/** True when `request` calls a tool of a remote (`streamable-http` or `sse`) MCP server. */
|
|
81
|
+
export function isRemoteMcpEffect(s, request) {
|
|
82
|
+
if (request.kind !== "tool")
|
|
83
|
+
return false;
|
|
84
|
+
const tool = mcpToolOf(s, request);
|
|
85
|
+
if (!tool)
|
|
86
|
+
return false;
|
|
87
|
+
const declared = findServer(s.manifest, tool.agentId, tool.capabilityId, tool.serverName);
|
|
88
|
+
return declared !== undefined && declared.server.type !== "stdio";
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* True when this Tenant's vault-backed model calls outlive the process that sent them (the
|
|
92
|
+
* gates service, P1.2): after a takeover or a shutdown, the journaled call is re-sent and joins
|
|
93
|
+
* the running call or gets its outcome, instead of becoming `uncertain`.
|
|
94
|
+
*/
|
|
95
|
+
export function recoversModelCalls(ctx) {
|
|
96
|
+
return ctx.useVaultModel && ctx.modelGate.recovers === true;
|
|
97
|
+
}
|
|
71
98
|
/**
|
|
72
99
|
* What an effect's journal row must match on replay. A delegation's context carries only the
|
|
73
100
|
* parent's tool call id, for its events; rows journaled before it existed still match.
|
|
@@ -100,6 +127,22 @@ export async function resolveEffect(ctx, request, signal, lease, segment = {}) {
|
|
|
100
127
|
throw new Error("Effect identity request drift");
|
|
101
128
|
if (existing.status === "completed")
|
|
102
129
|
return resolved({ status: "completed", outcome: existing.outcome });
|
|
130
|
+
// A model call its previous owner left running at the gate: re-send it, same key and
|
|
131
|
+
// request, to join it or collect its outcome.
|
|
132
|
+
if (existing.status === "invoking" &&
|
|
133
|
+
request.kind === "model" &&
|
|
134
|
+
recoversModelCalls(ctx))
|
|
135
|
+
return { kind: "invoke", invoke: "model", journaled: existing.request };
|
|
136
|
+
// A remote MCP call its previous owner left running at the gate: the same, by effect id.
|
|
137
|
+
if (existing.status === "invoking" &&
|
|
138
|
+
recoversMcpCalls(ctx) &&
|
|
139
|
+
isRemoteMcpEffect(s, existing.request))
|
|
140
|
+
return {
|
|
141
|
+
kind: "invoke",
|
|
142
|
+
invoke: "mcp",
|
|
143
|
+
journaled: existing.request,
|
|
144
|
+
remote: true,
|
|
145
|
+
};
|
|
103
146
|
if (request.kind === "agent" && existing.status === "pending") {
|
|
104
147
|
const agentSessionId = existing.agentSessionId;
|
|
105
148
|
if (agentSessionId) {
|
|
@@ -159,6 +202,7 @@ export async function resolveEffect(ctx, request, signal, lease, segment = {}) {
|
|
|
159
202
|
return {
|
|
160
203
|
kind: "invoke",
|
|
161
204
|
invoke: mcpTool ? "mcp" : sandboxTool ? "sandbox" : "model",
|
|
205
|
+
...(mcpTool && isRemoteMcpEffect(s, request) ? { remote: true } : {}),
|
|
162
206
|
};
|
|
163
207
|
const tool = request.kind === "tool"
|
|
164
208
|
? pinnedTool(agentManifest, request.capabilityId, request.toolName)
|
|
@@ -216,10 +260,10 @@ export async function resolveEffect(ctx, request, signal, lease, segment = {}) {
|
|
|
216
260
|
try {
|
|
217
261
|
// The intent is committed; the call itself runs outside any transaction.
|
|
218
262
|
const value = invoke === "mcp"
|
|
219
|
-
? await callMcpTool(ctx, request)
|
|
263
|
+
? await callMcpTool(ctx, journaled.journaled ?? request, abortOn(signal, journaled.remote && recoversMcpCalls(ctx) ? ["cancel", "shutdown"] : ["cancel"]))
|
|
220
264
|
: invoke === "sandbox"
|
|
221
265
|
? await callSandboxTool(ctx, request, signal)
|
|
222
|
-
: await invokeModel(ctx, request, signal, segment.model);
|
|
266
|
+
: await invokeModel(ctx, journaled.journaled ?? request, signal, segment.model);
|
|
223
267
|
return await store.tx(async (t) => {
|
|
224
268
|
const s = await ownedSession(t, lease, request.sessionId);
|
|
225
269
|
// Only a cancel discards an outcome in hand (§10.7). After any other abort (shutdown,
|
|
@@ -256,6 +300,18 @@ export async function resolveEffect(ctx, request, signal, lease, segment = {}) {
|
|
|
256
300
|
// A lost epoch writes nothing: the new owner decides what the effect became.
|
|
257
301
|
if (isOwnershipLost(error))
|
|
258
302
|
throw error;
|
|
303
|
+
// A shutdown leaves a model call running at the gate, still `invoking`: the next advance
|
|
304
|
+
// re-sends it (P1.2). The segment stops for the shutdown as usual.
|
|
305
|
+
if (invoke === "model" && recoversModelCalls(ctx) && abortKind(signal) === "shutdown")
|
|
306
|
+
throw error;
|
|
307
|
+
// The same for a remote MCP call at the Tool Gate (G3). A user cancel stops it there: a
|
|
308
|
+
// keyed call outlives the request that sent it.
|
|
309
|
+
if (invoke === "mcp" && journaled.remote && recoversMcpCalls(ctx)) {
|
|
310
|
+
if (abortKind(signal) === "shutdown")
|
|
311
|
+
throw error;
|
|
312
|
+
if (abortKind(signal) === "cancel" && ctx.toolGate.cancel)
|
|
313
|
+
await ctx.toolGate.cancel({ tenantId: ctx.config.tenantId, effectId: request.effectId });
|
|
314
|
+
}
|
|
259
315
|
await store.tx(async (t) => {
|
|
260
316
|
const s = request.sessionId === lease.sessionId
|
|
261
317
|
? await t.assertEpoch(request.sessionId, lease.epoch)
|
|
@@ -544,7 +600,25 @@ export async function prepareMcp(ctx, lease, signal) {
|
|
|
544
600
|
await t.put("sessions", id, current);
|
|
545
601
|
});
|
|
546
602
|
}
|
|
547
|
-
|
|
603
|
+
/**
|
|
604
|
+
* A signal that follows `signal` only for the abort kinds in `kinds`. An MCP call stops on a
|
|
605
|
+
* cancel; at the Tool Gate it also stops waiting on a shutdown, since the gate keeps the call
|
|
606
|
+
* for the next owner. Otherwise a shutdown or a deadline lets the call finish and records it,
|
|
607
|
+
* so the next advance replays it.
|
|
608
|
+
*/
|
|
609
|
+
function abortOn(signal, kinds) {
|
|
610
|
+
const controller = new AbortController();
|
|
611
|
+
const follow = () => {
|
|
612
|
+
if (kinds.includes(abortKind(signal)))
|
|
613
|
+
controller.abort(signal.reason);
|
|
614
|
+
};
|
|
615
|
+
if (signal.aborted)
|
|
616
|
+
follow();
|
|
617
|
+
else
|
|
618
|
+
signal.addEventListener("abort", follow, { once: true });
|
|
619
|
+
return controller.signal;
|
|
620
|
+
}
|
|
621
|
+
async function callMcpTool(ctx, request, signal) {
|
|
548
622
|
const s = await loadSession(ctx, request.sessionId);
|
|
549
623
|
const tool = mcpToolOf(s, request);
|
|
550
624
|
if (!tool)
|
|
@@ -558,6 +632,8 @@ async function callMcpTool(ctx, request) {
|
|
|
558
632
|
args: request.input,
|
|
559
633
|
manifest: s.manifest,
|
|
560
634
|
pluginRoots: s.pluginRoots ?? {},
|
|
635
|
+
effectId: request.effectId,
|
|
636
|
+
signal,
|
|
561
637
|
});
|
|
562
638
|
}
|
|
563
639
|
/**
|
|
@@ -1,17 +1,23 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type PostgresClient } from "../store/postgres/connect.js";
|
|
2
2
|
import type { Logger, TenantModelConfig } from "./types.js";
|
|
3
3
|
export interface StartEphemeralRuntimeOptions {
|
|
4
4
|
/**
|
|
5
|
-
* Absolute Host root for `host.json`, the admin key and
|
|
5
|
+
* Absolute Host root for `host.json`, the admin key and the Tenant's files (vault key,
|
|
6
6
|
* home, sandboxes). Caller creates any temporary directory. Session data never lands here.
|
|
7
7
|
*/
|
|
8
8
|
hostRoot: string;
|
|
9
|
+
/** The Tenant's id when the database holds no Tenant yet. Default: a new id. */
|
|
9
10
|
tenantId?: string;
|
|
11
|
+
/** The Tenant's name when the database holds no Tenant yet. Default `ephemeral`. */
|
|
10
12
|
name?: string;
|
|
13
|
+
/** An application key for an application principal of the Tenant. Default: a new key. */
|
|
11
14
|
applicationKey?: string;
|
|
12
15
|
adminKey?: string;
|
|
13
16
|
principalId?: string;
|
|
14
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* SHA-256 of the Studio key; registers principal `studio` with it. Default: the key the
|
|
19
|
+
* admin key derives (`deriveStudioToken` in `@nylorun/admin`).
|
|
20
|
+
*/
|
|
15
21
|
studioCredentialHash?: string;
|
|
16
22
|
/** Allowlisted baseline for childEnv (e.g. PATH). Never read from ambient here. */
|
|
17
23
|
baseline?: Readonly<Record<string, string>>;
|
|
@@ -25,10 +31,12 @@ export interface StartEphemeralRuntimeOptions {
|
|
|
25
31
|
operatorListener?: boolean;
|
|
26
32
|
logger?: Logger;
|
|
27
33
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
34
|
+
* The Postgres database of the Tenant (one Tenant per database): a pool, which the caller
|
|
35
|
+
* ends, or a URL, for which the Runtime opens a pool and ends it on `close()`. A database
|
|
36
|
+
* that already holds a Tenant serves that one. The data stays after `close()`; a test drops
|
|
37
|
+
* its database.
|
|
30
38
|
*/
|
|
31
|
-
database
|
|
39
|
+
database: PostgresClient | string;
|
|
32
40
|
}
|
|
33
41
|
export interface EphemeralRuntime {
|
|
34
42
|
url: string;
|
|
@@ -42,7 +50,8 @@ export interface EphemeralRuntime {
|
|
|
42
50
|
close(): Promise<void>;
|
|
43
51
|
}
|
|
44
52
|
/**
|
|
45
|
-
* Private Host on port 0
|
|
53
|
+
* Private Host on port 0 serving one Tenant (D9 / A19). Durable only in its database (see the
|
|
54
|
+
* module comment).
|
|
46
55
|
* Exported from `@nylorun/runtime` and `@nylorun/runtime/core`.
|
|
47
56
|
*/
|
|
48
57
|
export declare function startEphemeralRuntime(options: StartEphemeralRuntimeOptions): Promise<EphemeralRuntime>;
|
package/dist/tenant/ephemeral.js
CHANGED
|
@@ -1,29 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `startEphemeralRuntime`: a private, in-process Host on port 0
|
|
3
|
-
* embeds that need the Runtime's HTTP API without
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* `startEphemeralRuntime`: a private, in-process Host on port 0 serving one Tenant, for tests
|
|
3
|
+
* and embeds that need the Runtime's HTTP API without Restate and S2. Its Tenant is the one
|
|
4
|
+
* the Postgres database the caller passes holds (`store/postgres/tenant.ts`), created there on
|
|
5
|
+
* first start through the same path as a Host's; there is no in-memory Session Store. The rest
|
|
6
|
+
* is not durable: Durable Streams are in memory and gone after `close()`, and scheduling is
|
|
7
|
+
* the in-process execution the Tenant starts for itself.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* (`model-setting.ts`), and deleted afterwards.
|
|
9
|
+
* The smoke checks do not use it: they reset the Tenant of a temporary Docker stack and seed
|
|
10
|
+
* the Tenant-level fixture model (`model-setting.ts`, scripts/lib/stack-tenant.mjs).
|
|
11
11
|
*/
|
|
12
12
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
13
13
|
import { randomBytes } from "node:crypto";
|
|
14
14
|
import { createRequire } from "node:module";
|
|
15
15
|
import { newTenantId } from "@nylorun/core/compatibility";
|
|
16
|
-
import {
|
|
16
|
+
import { mintBearerToken } from "../core/bearer.js";
|
|
17
17
|
import { createHost } from "../host/create-host.js";
|
|
18
18
|
import { createKekFile } from "../vault/kek.js";
|
|
19
|
-
import { MemorySessionStore } from "../store/memory.js";
|
|
20
19
|
import { createTenantModule } from "./module.js";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
20
|
+
import { createPostgresTenantOpener } from "./store-pg.js";
|
|
21
|
+
import { createPostgresClient, } from "../store/postgres/connect.js";
|
|
23
22
|
import { MemoryStreams } from "../streams/memory.js";
|
|
24
23
|
import { createTenantLogger } from "./logger.js";
|
|
25
24
|
import { hostPaths, tenantPaths } from "./paths.js";
|
|
26
|
-
import {
|
|
25
|
+
import { hostPrincipals } from "./principals.js";
|
|
27
26
|
import { openTenantRuntime } from "./runtime.js";
|
|
28
27
|
const nodeRequire = createRequire(import.meta.url);
|
|
29
28
|
function coreVersion() {
|
|
@@ -38,70 +37,8 @@ function newHostId() {
|
|
|
38
37
|
return `host_${newTenantId().slice(3)}`;
|
|
39
38
|
}
|
|
40
39
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*/
|
|
44
|
-
function memoryTenants(options) {
|
|
45
|
-
const stores = new Map();
|
|
46
|
-
const inner = createMemoryTenantStore({
|
|
47
|
-
configFor: options.configFor,
|
|
48
|
-
logger: options.logger,
|
|
49
|
-
openRuntime: async (config) => {
|
|
50
|
-
const store = stores.get(config.tenantId);
|
|
51
|
-
if (!store)
|
|
52
|
-
throw new Error(`Tenant ${config.tenantId} has no Session Store`);
|
|
53
|
-
return openTenantRuntime(config, {
|
|
54
|
-
createKekIfMissing: true,
|
|
55
|
-
streams: options.streams,
|
|
56
|
-
store: kept(store),
|
|
57
|
-
envelope: await inner.readEnvelope(config.tenantId),
|
|
58
|
-
});
|
|
59
|
-
},
|
|
60
|
-
});
|
|
61
|
-
const store = {
|
|
62
|
-
...inner,
|
|
63
|
-
async create(envelope, bootstrap) {
|
|
64
|
-
const created = await inner.create(envelope, bootstrap);
|
|
65
|
-
if (created === "created") {
|
|
66
|
-
const sessions = new MemorySessionStore({ tenantId: envelope.id });
|
|
67
|
-
await sessions.tx((t) => bootstrapPrincipal(t, bootstrap));
|
|
68
|
-
stores.set(envelope.id, sessions);
|
|
69
|
-
}
|
|
70
|
-
return created;
|
|
71
|
-
},
|
|
72
|
-
async trash(id, now) {
|
|
73
|
-
await inner.trash(id, now);
|
|
74
|
-
await stores.get(id)?.close();
|
|
75
|
-
stores.delete(id);
|
|
76
|
-
},
|
|
77
|
-
async removePartial(id) {
|
|
78
|
-
await inner.removePartial(id);
|
|
79
|
-
await stores.get(id)?.close();
|
|
80
|
-
stores.delete(id);
|
|
81
|
-
},
|
|
82
|
-
};
|
|
83
|
-
return {
|
|
84
|
-
store,
|
|
85
|
-
async close() {
|
|
86
|
-
for (const sessions of stores.values())
|
|
87
|
-
await sessions.close();
|
|
88
|
-
stores.clear();
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
/** The Tenant closes its store on close; a memory store must outlive that for a re-open. */
|
|
93
|
-
function kept(store) {
|
|
94
|
-
return {
|
|
95
|
-
tenantId: store.tenantId,
|
|
96
|
-
tx: (fn) => store.tx(fn),
|
|
97
|
-
onCommit: (listener) => store.onCommit(listener),
|
|
98
|
-
record: () => store.record(),
|
|
99
|
-
health: () => store.health(),
|
|
100
|
-
close: async () => { },
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Private Host on port 0 with one Tenant (D9 / A19). Not durable (see the module comment).
|
|
40
|
+
* Private Host on port 0 serving one Tenant (D9 / A19). Durable only in its database (see the
|
|
41
|
+
* module comment).
|
|
105
42
|
* Exported from `@nylorun/runtime` and `@nylorun/runtime/core`.
|
|
106
43
|
*/
|
|
107
44
|
export async function startEphemeralRuntime(options) {
|
|
@@ -109,8 +46,6 @@ export async function startEphemeralRuntime(options) {
|
|
|
109
46
|
const paths = hostPaths(hostRoot);
|
|
110
47
|
mkdirSync(paths.home, { recursive: true });
|
|
111
48
|
mkdirSync(paths.tmp, { recursive: true });
|
|
112
|
-
mkdirSync(paths.tenants, { recursive: true });
|
|
113
|
-
mkdirSync(paths.trash, { recursive: true });
|
|
114
49
|
const hostId = newHostId();
|
|
115
50
|
const adminKey = options.adminKey ?? randomBytes(32).toString("hex");
|
|
116
51
|
const hostConfig = {
|
|
@@ -132,7 +67,7 @@ export async function startEphemeralRuntime(options) {
|
|
|
132
67
|
const baseline = options.baseline ?? {};
|
|
133
68
|
const model = options.model ?? { kind: "scripted", output: "ok" };
|
|
134
69
|
const configFor = (tenantId) => {
|
|
135
|
-
const tenant = tenantPaths(hostRoot
|
|
70
|
+
const tenant = tenantPaths(hostRoot);
|
|
136
71
|
// A seeded `sandbox.backend` setting overrides this when the Tenant opens.
|
|
137
72
|
const sandboxBackend = options.sandboxBackend ?? "virtual";
|
|
138
73
|
return {
|
|
@@ -153,13 +88,26 @@ export async function startEphemeralRuntime(options) {
|
|
|
153
88
|
};
|
|
154
89
|
};
|
|
155
90
|
const streams = new MemoryStreams();
|
|
156
|
-
const
|
|
157
|
-
?
|
|
158
|
-
:
|
|
159
|
-
const
|
|
160
|
-
|
|
91
|
+
const ownPool = typeof options.database === "string"
|
|
92
|
+
? createPostgresClient(options.database)
|
|
93
|
+
: undefined;
|
|
94
|
+
const applicationKey = options.applicationKey ?? mintBearerToken();
|
|
95
|
+
const principalId = options.principalId ?? `principal_${randomBytes(8).toString("hex")}`;
|
|
96
|
+
const module = createTenantModule({
|
|
97
|
+
open: createPostgresTenantOpener({
|
|
161
98
|
hostRoot,
|
|
162
|
-
sql: options.database,
|
|
99
|
+
sql: ownPool ?? options.database,
|
|
100
|
+
create: {
|
|
101
|
+
tenantId: options.tenantId ?? newTenantId(),
|
|
102
|
+
name: options.name ?? "ephemeral",
|
|
103
|
+
principals: hostPrincipals({
|
|
104
|
+
adminKey,
|
|
105
|
+
application: { principalId, key: applicationKey },
|
|
106
|
+
...(options.studioCredentialHash
|
|
107
|
+
? { studioCredentialHash: options.studioCredentialHash }
|
|
108
|
+
: {}),
|
|
109
|
+
}),
|
|
110
|
+
},
|
|
163
111
|
configFor,
|
|
164
112
|
logger,
|
|
165
113
|
openRuntime: (config, opened) => openTenantRuntime(config, {
|
|
@@ -167,45 +115,24 @@ export async function startEphemeralRuntime(options) {
|
|
|
167
115
|
streams,
|
|
168
116
|
...opened,
|
|
169
117
|
}),
|
|
170
|
-
})
|
|
171
|
-
: memory.store;
|
|
172
|
-
const module = createTenantModule({
|
|
173
|
-
store,
|
|
118
|
+
}),
|
|
174
119
|
logger,
|
|
175
|
-
onDeleted: (tenantId) => streams.deleteTenant(tenantId),
|
|
176
120
|
});
|
|
177
121
|
let host;
|
|
178
122
|
/** Closes what was opened, in reverse order; `close()` and a failed start share it. */
|
|
179
123
|
const release = async () => {
|
|
180
124
|
await host?.close();
|
|
181
125
|
await module.close();
|
|
182
|
-
await memory?.close();
|
|
183
126
|
await streams.close();
|
|
127
|
+
await ownPool?.end({ timeout: 5 });
|
|
184
128
|
if (!options.retainRoot)
|
|
185
129
|
rmSync(hostRoot, { recursive: true, force: true });
|
|
186
130
|
};
|
|
187
131
|
let tenantId;
|
|
188
|
-
let applicationKey;
|
|
189
|
-
let principalId;
|
|
190
132
|
try {
|
|
191
|
-
await module.start();
|
|
192
|
-
tenantId = options.tenantId ?? newTenantId();
|
|
193
|
-
applicationKey = options.applicationKey ?? mintBearerToken();
|
|
194
|
-
principalId =
|
|
195
|
-
options.principalId ?? `principal_${randomBytes(8).toString("hex")}`;
|
|
196
|
-
const credentialHash = hashToken(applicationKey);
|
|
197
|
-
await module.create({
|
|
198
|
-
tenantId,
|
|
199
|
-
name: options.name ?? "ephemeral",
|
|
200
|
-
principalId,
|
|
201
|
-
credentialHash,
|
|
202
|
-
idempotencyKey: `ephemeral-${tenantId}`,
|
|
203
|
-
...(options.studioCredentialHash
|
|
204
|
-
? { studioCredentialHash: options.studioCredentialHash }
|
|
205
|
-
: {}),
|
|
206
|
-
});
|
|
207
133
|
// KEK for first vault write; openTenantRuntime also creates when hooks allow.
|
|
208
|
-
const kekPath = tenantPaths(hostRoot
|
|
134
|
+
const kekPath = tenantPaths(hostRoot).kek;
|
|
135
|
+
mkdirSync(tenantPaths(hostRoot).root, { recursive: true, mode: 0o700 });
|
|
209
136
|
if (!existsSync(kekPath))
|
|
210
137
|
createKekFile(kekPath);
|
|
211
138
|
host = createHost({
|
|
@@ -220,7 +147,14 @@ export async function startEphemeralRuntime(options) {
|
|
|
220
147
|
? { operator: { host: "127.0.0.1", port: 0 } }
|
|
221
148
|
: {}),
|
|
222
149
|
});
|
|
150
|
+
// Opens the Tenant: creates it in the database first when it holds none.
|
|
223
151
|
await host.listen();
|
|
152
|
+
const tenant = module.tenant();
|
|
153
|
+
if (tenant.state !== "open" || !tenant.id)
|
|
154
|
+
throw new Error(tenant.cause
|
|
155
|
+
? `The Tenant could not be opened (${tenant.cause.code}): ${tenant.cause.message}`
|
|
156
|
+
: "The Tenant could not be opened: its database is unavailable");
|
|
157
|
+
tenantId = tenant.id;
|
|
224
158
|
}
|
|
225
159
|
catch (error) {
|
|
226
160
|
await release().catch((cleanup) => logger.warn("ephemeral runtime cleanup failed", {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One history (blueprint P0.3): the own loop's model-facing transcript is a fold of the
|
|
3
|
+
* session's record, not a copy on the session row.
|
|
4
|
+
*
|
|
5
|
+
* - **Write.** After a segment that keeps the engine's state (a rollover, or a settle that is
|
|
6
|
+
* not a failure), the Runtime appends `transcript.updated` events: an edit from the
|
|
7
|
+
* transcript the segment started with to the one the engine returned (`transcriptUpdates`).
|
|
8
|
+
* Normal steps only append; compaction replaces the prefix, so its edit is a snapshot.
|
|
9
|
+
* - **Fold.** `foldTranscript` applies the edits in order. `turn.cancelled` and `turn.failed`
|
|
10
|
+
* restore the transcript from before that turn's first edit, as cancel and failure restore
|
|
11
|
+
* `turnStartState`.
|
|
12
|
+
* - **Lean rows.** The session row stores the engine state with an empty transcript
|
|
13
|
+
* (`leanState`). `runSegment` folds the record from `Session.history.from` and hands the
|
|
14
|
+
* engine the full transcript (`withTranscript`).
|
|
15
|
+
* - **Shadow mode** (on in the runtime's tests, `test/setup/transcript-shadow.ts`) keeps the
|
|
16
|
+
* transcript on the row too and checks the fold against it at every segment start.
|
|
17
|
+
*/
|
|
18
|
+
import type { LiveEvent } from "@nylorun/core/contracts";
|
|
19
|
+
/** The payload of one `transcript.updated` event. */
|
|
20
|
+
export interface TranscriptUpdate {
|
|
21
|
+
keep: number;
|
|
22
|
+
entries: unknown[];
|
|
23
|
+
length: number;
|
|
24
|
+
}
|
|
25
|
+
/** Where a session's fold starts. */
|
|
26
|
+
export interface SessionHistory {
|
|
27
|
+
/** The fold reads the record from this seq: a snapshot at or before the turn's start. */
|
|
28
|
+
from: number;
|
|
29
|
+
/** The latest snapshot (`keep: 0`), which `from` moves to when the next turn starts. */
|
|
30
|
+
snapshot?: number;
|
|
31
|
+
}
|
|
32
|
+
/** An edit's entries are split so each event stays well under S2's 1 MiB record limit. */
|
|
33
|
+
export declare const CHUNK_BYTES: number;
|
|
34
|
+
/** Shadow mode: keep the transcript on the session row and check the fold against it. */
|
|
35
|
+
export declare function transcriptShadow(): boolean;
|
|
36
|
+
/** Tests switch shadow mode off to exercise lean rows. Returns the previous setting. */
|
|
37
|
+
export declare function setTranscriptShadow(on: boolean): boolean;
|
|
38
|
+
export declare class TranscriptFoldError extends Error {
|
|
39
|
+
constructor(message: string);
|
|
40
|
+
}
|
|
41
|
+
/** The fold and the transcript on the row differ (shadow mode only). */
|
|
42
|
+
export declare class TranscriptParityError extends Error {
|
|
43
|
+
constructor(sessionId: string, at: number, folded: number, stored: number);
|
|
44
|
+
}
|
|
45
|
+
/** The engine state's transcript, or none. */
|
|
46
|
+
export declare function transcriptOf(state: unknown): unknown[];
|
|
47
|
+
/**
|
|
48
|
+
* The edits from `before` to `after`, split into chunks of at most `CHUNK_BYTES` of entries.
|
|
49
|
+
* Empty when nothing changed.
|
|
50
|
+
*/
|
|
51
|
+
export declare function transcriptUpdates(before: readonly unknown[], after: readonly unknown[], chunkBytes?: number): TranscriptUpdate[];
|
|
52
|
+
/** Applies one edit. Throws `TranscriptFoldError` when it does not fit the transcript. */
|
|
53
|
+
export declare function applyUpdate(transcript: readonly unknown[], update: TranscriptUpdate): unknown[];
|
|
54
|
+
/**
|
|
55
|
+
* Folds a session's events, from the start of its record or from a snapshot, into the
|
|
56
|
+
* transcript the engine resumes from.
|
|
57
|
+
*/
|
|
58
|
+
export declare function foldTranscript(events: Iterable<Pick<LiveEvent, "type" | "turnId" | "payload">>): unknown[];
|
|
59
|
+
/** `state` with an empty transcript (what the session row stores outside shadow mode). */
|
|
60
|
+
export declare function leanState<T>(state: T): T;
|
|
61
|
+
/** `state` with `transcript` (what the engine runs on). */
|
|
62
|
+
export declare function withTranscript<T>(state: T, transcript: readonly unknown[]): T;
|
|
63
|
+
/** In shadow mode, checks the fold against the transcript the row kept. */
|
|
64
|
+
export declare function checkParity(sessionId: string, folded: readonly unknown[], stored: readonly unknown[]): void;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/** An edit's entries are split so each event stays well under S2's 1 MiB record limit. */
|
|
2
|
+
export const CHUNK_BYTES = 256 * 1024;
|
|
3
|
+
let shadow = false;
|
|
4
|
+
/** Shadow mode: keep the transcript on the session row and check the fold against it. */
|
|
5
|
+
export function transcriptShadow() {
|
|
6
|
+
return shadow;
|
|
7
|
+
}
|
|
8
|
+
/** Tests switch shadow mode off to exercise lean rows. Returns the previous setting. */
|
|
9
|
+
export function setTranscriptShadow(on) {
|
|
10
|
+
const previous = shadow;
|
|
11
|
+
shadow = on;
|
|
12
|
+
return previous;
|
|
13
|
+
}
|
|
14
|
+
export class TranscriptFoldError extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "TranscriptFoldError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** The fold and the transcript on the row differ (shadow mode only). */
|
|
21
|
+
export class TranscriptParityError extends Error {
|
|
22
|
+
constructor(sessionId, at, folded, stored) {
|
|
23
|
+
super(`Session ${sessionId}: the folded transcript differs from the stored one at entry ${at}` +
|
|
24
|
+
` (folded ${folded} entries, stored ${stored})`);
|
|
25
|
+
this.name = "TranscriptParityError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
29
|
+
/** The engine state's transcript, or none. */
|
|
30
|
+
export function transcriptOf(state) {
|
|
31
|
+
const transcript = state?.transcript;
|
|
32
|
+
return Array.isArray(transcript) ? transcript : [];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The edits from `before` to `after`, split into chunks of at most `CHUNK_BYTES` of entries.
|
|
36
|
+
* Empty when nothing changed.
|
|
37
|
+
*/
|
|
38
|
+
export function transcriptUpdates(before, after, chunkBytes = CHUNK_BYTES) {
|
|
39
|
+
let keep = 0;
|
|
40
|
+
while (keep < before.length && keep < after.length && same(before[keep], after[keep]))
|
|
41
|
+
keep += 1;
|
|
42
|
+
if (keep === before.length && keep === after.length)
|
|
43
|
+
return [];
|
|
44
|
+
const added = after.slice(keep);
|
|
45
|
+
if (added.length === 0)
|
|
46
|
+
return [{ keep, entries: [], length: keep }];
|
|
47
|
+
const updates = [];
|
|
48
|
+
let chunk = [];
|
|
49
|
+
let bytes = 0;
|
|
50
|
+
let base = keep;
|
|
51
|
+
const flush = () => {
|
|
52
|
+
updates.push({ keep: base, entries: chunk, length: base + chunk.length });
|
|
53
|
+
base += chunk.length;
|
|
54
|
+
chunk = [];
|
|
55
|
+
bytes = 0;
|
|
56
|
+
};
|
|
57
|
+
for (const entry of added) {
|
|
58
|
+
const size = JSON.stringify(entry).length;
|
|
59
|
+
if (chunk.length > 0 && bytes + size > chunkBytes)
|
|
60
|
+
flush();
|
|
61
|
+
chunk.push(entry);
|
|
62
|
+
bytes += size;
|
|
63
|
+
}
|
|
64
|
+
flush();
|
|
65
|
+
return updates;
|
|
66
|
+
}
|
|
67
|
+
/** Applies one edit. Throws `TranscriptFoldError` when it does not fit the transcript. */
|
|
68
|
+
export function applyUpdate(transcript, update) {
|
|
69
|
+
if (update.keep > transcript.length)
|
|
70
|
+
throw new TranscriptFoldError(`transcript.updated keeps ${update.keep} entries of ${transcript.length}`);
|
|
71
|
+
const next = [...transcript.slice(0, update.keep), ...update.entries];
|
|
72
|
+
if (next.length !== update.length)
|
|
73
|
+
throw new TranscriptFoldError(`transcript.updated should give ${update.length} entries, not ${next.length}`);
|
|
74
|
+
return next;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Folds a session's events, from the start of its record or from a snapshot, into the
|
|
78
|
+
* transcript the engine resumes from.
|
|
79
|
+
*/
|
|
80
|
+
export function foldTranscript(events) {
|
|
81
|
+
let transcript = [];
|
|
82
|
+
// The transcript before the current turn's first edit: what a cancel or failure restores.
|
|
83
|
+
let turn;
|
|
84
|
+
for (const event of events) {
|
|
85
|
+
if (event.type === "transcript.updated") {
|
|
86
|
+
if (!turn || turn.id !== event.turnId)
|
|
87
|
+
turn = { id: event.turnId, before: transcript };
|
|
88
|
+
transcript = applyUpdate(transcript, event.payload);
|
|
89
|
+
}
|
|
90
|
+
else if (event.type === "turn.cancelled" || event.type === "turn.failed") {
|
|
91
|
+
if (turn && turn.id === event.turnId)
|
|
92
|
+
transcript = turn.before;
|
|
93
|
+
turn = undefined;
|
|
94
|
+
}
|
|
95
|
+
else if (event.type === "turn.completed")
|
|
96
|
+
turn = undefined;
|
|
97
|
+
}
|
|
98
|
+
return transcript;
|
|
99
|
+
}
|
|
100
|
+
/** `state` with an empty transcript (what the session row stores outside shadow mode). */
|
|
101
|
+
export function leanState(state) {
|
|
102
|
+
if (shadow || !state || transcriptOf(state).length === 0)
|
|
103
|
+
return state;
|
|
104
|
+
return { ...state, transcript: [] };
|
|
105
|
+
}
|
|
106
|
+
/** `state` with `transcript` (what the engine runs on). */
|
|
107
|
+
export function withTranscript(state, transcript) {
|
|
108
|
+
if (!state)
|
|
109
|
+
return state;
|
|
110
|
+
return { ...state, transcript: [...transcript] };
|
|
111
|
+
}
|
|
112
|
+
/** In shadow mode, checks the fold against the transcript the row kept. */
|
|
113
|
+
export function checkParity(sessionId, folded, stored) {
|
|
114
|
+
if (!shadow)
|
|
115
|
+
return;
|
|
116
|
+
const length = Math.max(folded.length, stored.length);
|
|
117
|
+
for (let i = 0; i < length; i += 1)
|
|
118
|
+
if (!same(folded[i], stored[i]))
|
|
119
|
+
throw new TranscriptParityError(sessionId, i, folded.length, stored.length);
|
|
120
|
+
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* The Tenant-level fixture model: Tenant setting `model.fixture`, seeded with
|
|
3
3
|
* `fixtureModel: true` (`PUT /v1/tenant/config/seed`, Host feature `tenant-fixture-model`).
|
|
4
4
|
* A Tenant with it answers every model call with the Runtime's deterministic fixture model
|
|
5
|
-
* (`core/provider.ts` `toolFixtureModel`) instead of its configured model
|
|
6
|
-
*
|
|
5
|
+
* (`core/provider.ts` `toolFixtureModel`) instead of its configured model. Smoke checks seed
|
|
6
|
+
* it (scripts/lib/stack-tenant.mjs).
|
|
7
7
|
*
|
|
8
8
|
* The setting is read once per advance, in a transaction the advance makes anyway, so a Tenant
|
|
9
9
|
* seeded on another node takes it at its next advance.
|