@nylorun/runtime 0.10.0-beta → 0.12.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 +86 -0
- package/README.md +53 -51
- package/dist/adapters/execution/restate.d.ts +130 -0
- package/dist/adapters/execution/restate.js +442 -0
- 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/core/executors.d.ts +4 -1
- package/dist/core/executors.js +20 -1
- package/dist/core/flow-host.d.ts +93 -132
- package/dist/core/flow-host.js +282 -547
- package/dist/core/sandbox-routes.d.ts +13 -4
- package/dist/core/sandbox-routes.js +14 -12
- 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 +2 -1
- package/dist/host/config-for.js +3 -21
- package/dist/host/config.d.ts +0 -8
- package/dist/host/create-host.d.ts +36 -1
- package/dist/host/create-host.js +51 -24
- package/dist/host/execution.d.ts +61 -0
- package/dist/host/execution.js +69 -0
- package/dist/host/http.d.ts +4 -0
- package/dist/host/http.js +6 -0
- package/dist/host/index.d.ts +3 -1
- package/dist/host/index.js +2 -0
- package/dist/host/main.js +106 -47
- package/dist/host/stack-config.d.ts +72 -0
- package/dist/host/stack-config.js +193 -0
- package/dist/index.d.ts +1 -1
- 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/model/pi-model.d.ts +2 -2
- package/dist/model/pi-model.js +5 -5
- package/dist/sandbox/manager.d.ts +26 -8
- package/dist/sandbox/manager.js +95 -47
- package/dist/sandbox/select.d.ts +1 -1
- package/dist/sandbox/select.js +4 -5
- 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 +645 -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/002_session_owner.d.ts +6 -0
- package/dist/store/postgres/migrations/002_session_owner.js +13 -0
- package/dist/store/postgres/migrations/index.d.ts +49 -0
- package/dist/store/postgres/migrations/index.js +76 -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 +797 -0
- package/dist/store/postgres/tenants.d.ts +90 -0
- package/dist/store/postgres/tenants.js +214 -0
- package/dist/store/types.d.ts +442 -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 +32 -0
- package/dist/tenant/auth.js +169 -0
- package/dist/tenant/commands.d.ts +3 -0
- package/dist/tenant/commands.js +393 -0
- package/dist/tenant/context.d.ts +147 -0
- package/dist/tenant/context.js +48 -0
- package/dist/tenant/effects.d.ts +20 -0
- package/dist/tenant/effects.js +564 -0
- package/dist/tenant/envelope.d.ts +0 -3
- package/dist/tenant/envelope.js +2 -34
- package/dist/tenant/ephemeral.d.ts +15 -4
- package/dist/tenant/ephemeral.js +106 -38
- package/dist/tenant/http.d.ts +38 -0
- package/dist/tenant/http.js +53 -0
- package/dist/tenant/live.d.ts +92 -0
- package/dist/tenant/live.js +290 -0
- package/dist/tenant/model-setting.d.ts +16 -0
- package/dist/tenant/model-setting.js +12 -0
- package/dist/tenant/module.d.ts +29 -8
- package/dist/tenant/module.js +165 -158
- package/dist/tenant/paths.d.ts +12 -8
- package/dist/tenant/paths.js +38 -12
- package/dist/tenant/principals.d.ts +12 -13
- package/dist/tenant/principals.js +50 -38
- package/dist/tenant/quarantine-error.d.ts +1 -1
- package/dist/tenant/quarantine-error.js +2 -8
- package/dist/tenant/quarantine.d.ts +0 -8
- package/dist/tenant/quarantine.js +8 -30
- package/dist/tenant/reset.d.ts +7 -6
- package/dist/tenant/reset.js +7 -51
- package/dist/tenant/routes-tenant.d.ts +12 -0
- package/dist/tenant/routes-tenant.js +137 -0
- package/dist/tenant/routes.d.ts +9 -0
- package/dist/tenant/routes.js +151 -0
- package/dist/tenant/runtime.d.ts +59 -58
- package/dist/tenant/runtime.js +243 -2201
- 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 +40 -0
- package/dist/tenant/sessions.js +163 -0
- package/dist/tenant/status.d.ts +13 -4
- package/dist/tenant/status.js +76 -41
- package/dist/tenant/store-memory.d.ts +9 -12
- package/dist/tenant/store-memory.js +79 -68
- 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/transcript.d.ts +104 -0
- package/dist/tenant/transcript.js +58 -0
- package/dist/tenant/types.d.ts +101 -15
- package/dist/tenant/types.js +23 -1
- package/dist/tenant/worker.d.ts +84 -0
- package/dist/tenant/worker.js +189 -0
- package/dist/vault/service.d.ts +44 -31
- package/dist/vault/service.js +454 -360
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +15 -14
- package/dist/adapters/sandbox/microsandbox.d.ts +0 -2
- package/dist/adapters/sandbox/microsandbox.js +0 -238
- package/dist/core/store.d.ts +0 -32
- package/dist/core/store.js +0 -143
- package/dist/launcher/commands.d.ts +0 -24
- package/dist/launcher/commands.js +0 -260
- package/dist/launcher/ensure-config.d.ts +0 -16
- package/dist/launcher/ensure-config.js +0 -79
- package/dist/launcher/errors.d.ts +0 -12
- package/dist/launcher/errors.js +0 -22
- package/dist/launcher/host-config.d.ts +0 -17
- package/dist/launcher/host-config.js +0 -64
- package/dist/launcher/host-state.d.ts +0 -17
- package/dist/launcher/host-state.js +0 -61
- package/dist/launcher/lifecycle.d.ts +0 -66
- package/dist/launcher/lifecycle.js +0 -407
- package/dist/launcher/locks.d.ts +0 -26
- package/dist/launcher/locks.js +0 -117
- package/dist/launcher/logs.d.ts +0 -18
- package/dist/launcher/logs.js +0 -117
- package/dist/launcher/main.d.ts +0 -2
- package/dist/launcher/main.js +0 -66
- package/dist/launcher/output.d.ts +0 -11
- package/dist/launcher/output.js +0 -68
- package/dist/launcher/paths.d.ts +0 -20
- package/dist/launcher/paths.js +0 -36
- package/dist/launcher/probe.d.ts +0 -23
- package/dist/launcher/probe.js +0 -72
- package/dist/launcher/protocol.d.ts +0 -62
- package/dist/launcher/protocol.js +0 -1
- package/dist/launcher/schema-guard.d.ts +0 -6
- package/dist/launcher/schema-guard.js +0 -36
- package/dist/launcher/spawn.d.ts +0 -22
- package/dist/launcher/spawn.js +0 -85
- package/dist/launcher/status.d.ts +0 -6
- package/dist/launcher/status.js +0 -65
- package/dist/tenant/migration.d.ts +0 -16
- package/dist/tenant/migration.js +0 -116
- package/dist/tenant/schema.d.ts +0 -23
- package/dist/tenant/schema.js +0 -151
- package/dist/tenant/store-fs.d.ts +0 -16
- package/dist/tenant/store-fs.js +0 -205
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
import { countActiveFlowWork, deriveAgentEffectSessionId, isFlowEffect, isFlowToolEffect, linkedMessageKey, linkedTurnEnd, } from "../core/flow-host.js";
|
|
2
|
+
import { mayDispatchMore } from "../core/limits.js";
|
|
3
|
+
import { canonical } from "../store/canonical.js";
|
|
4
|
+
import { isOwnershipLost } from "../store/ownership.js";
|
|
5
|
+
import { newStreamIncarnation } from "../streams/types.js";
|
|
6
|
+
import { piModel } from "../model/pi-model.js";
|
|
7
|
+
import { scrub } from "../redact.js";
|
|
8
|
+
import { serversOf } from "../mcp/pool.js";
|
|
9
|
+
import { sandboxCapabilityOf } from "../sandbox/manager.js";
|
|
10
|
+
import { owningSandboxSessionId, sandboxSpecOf } from "../sandbox/share.js";
|
|
11
|
+
import { loadSession, ownedSession, sandboxLookup, sessionOf, } from "./context.js";
|
|
12
|
+
import { fail } from "./http.js";
|
|
13
|
+
import { actionTarget, linkedAgentOutput, manifestFor, mcpToolOf, pinnedTool, } from "./session.js";
|
|
14
|
+
import { command } from "./commands.js";
|
|
15
|
+
import { assistantMessage, toolCompleted, toolIds } from "./transcript.js";
|
|
16
|
+
import { abortKind } from "./worker.js";
|
|
17
|
+
export function invokeModel(ctx, request, signal, model) {
|
|
18
|
+
if (model)
|
|
19
|
+
return model(request, signal);
|
|
20
|
+
if (!ctx.useVaultModel)
|
|
21
|
+
return ctx.modelProvider(request, signal);
|
|
22
|
+
const adapter = piModel({
|
|
23
|
+
root: ctx.config.paths.home,
|
|
24
|
+
readHostModel: () => ctx.vault.readHostModel(),
|
|
25
|
+
writeHostCredential: (credential) => ctx.vault.updateHostCredential(credential),
|
|
26
|
+
});
|
|
27
|
+
return adapter(request.input, {
|
|
28
|
+
request: request.context.request,
|
|
29
|
+
invocationId: String(request.context.invocationId),
|
|
30
|
+
signal,
|
|
31
|
+
reportPreparedCall() { },
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The outcome of a flow `agent` effect once the linked turn it started ended, or undefined
|
|
36
|
+
* while it has not. The linked session may still show an earlier iteration's turn.
|
|
37
|
+
*/
|
|
38
|
+
async function linkedOutcome(t, effect, agent) {
|
|
39
|
+
const end = await linkedTurnEnd(t, effect, agent);
|
|
40
|
+
if (!end || !agent)
|
|
41
|
+
return undefined;
|
|
42
|
+
if (end.status === "completed")
|
|
43
|
+
return { value: linkedAgentOutput(agent, end.output) };
|
|
44
|
+
return {
|
|
45
|
+
value: {
|
|
46
|
+
kind: "failed",
|
|
47
|
+
code: `agent.${end.status}`,
|
|
48
|
+
message: end.error ??
|
|
49
|
+
(end.status === "failed"
|
|
50
|
+
? "Agent turn failed"
|
|
51
|
+
: "Agent turn was cancelled"),
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* What an effect's journal row must match on replay. A delegation's context carries only the
|
|
57
|
+
* parent's tool call id, for its events; rows journaled before it existed still match.
|
|
58
|
+
*/
|
|
59
|
+
function requestIdentity(request) {
|
|
60
|
+
if (request.kind !== "delegation")
|
|
61
|
+
return request;
|
|
62
|
+
const { context: _, ...identity } = request;
|
|
63
|
+
return identity;
|
|
64
|
+
}
|
|
65
|
+
function delegationCallId(request) {
|
|
66
|
+
const callId = request.context?.callId;
|
|
67
|
+
return typeof callId === "string" ? { callId } : {};
|
|
68
|
+
}
|
|
69
|
+
export async function resolveEffect(ctx, request, signal, lease, segment = {}) {
|
|
70
|
+
const { store } = ctx;
|
|
71
|
+
const journaled = await store.tx(async (t) => {
|
|
72
|
+
const s = await ownedSession(t, lease, request.sessionId);
|
|
73
|
+
if (s.status === "cancelled" || s.activeTurnId !== request.turnId)
|
|
74
|
+
throw new Error("Turn cancelled");
|
|
75
|
+
// An aborted advance starts no effect; the advance decides what the abort means.
|
|
76
|
+
signal.throwIfAborted();
|
|
77
|
+
const resolved = (resolution) => ({
|
|
78
|
+
kind: "resolved",
|
|
79
|
+
resolution,
|
|
80
|
+
});
|
|
81
|
+
const existing = await t.get("effects", request.effectId);
|
|
82
|
+
if (existing) {
|
|
83
|
+
if (canonical(requestIdentity(existing.request)) !== canonical(requestIdentity(request)))
|
|
84
|
+
throw new Error("Effect identity request drift");
|
|
85
|
+
if (existing.status === "completed")
|
|
86
|
+
return resolved({ status: "completed", outcome: existing.outcome });
|
|
87
|
+
if (request.kind === "agent" && existing.status === "pending") {
|
|
88
|
+
const agentSessionId = existing.agentSessionId;
|
|
89
|
+
if (agentSessionId) {
|
|
90
|
+
const agent = await t.get("sessions", agentSessionId);
|
|
91
|
+
const outcome = await linkedOutcome(t, existing, agent);
|
|
92
|
+
if (outcome) {
|
|
93
|
+
existing.status = "completed";
|
|
94
|
+
existing.outcome = outcome;
|
|
95
|
+
await t.put("effects", request.effectId, existing);
|
|
96
|
+
return resolved({ status: "completed", outcome });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return resolved({
|
|
101
|
+
status: existing.status === "uncertain" || existing.status === "invoking"
|
|
102
|
+
? "uncertain"
|
|
103
|
+
: "pending",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (request.kind === "agent" ||
|
|
107
|
+
request.kind === "fn" ||
|
|
108
|
+
request.kind === "verify" ||
|
|
109
|
+
isFlowToolEffect(request)) {
|
|
110
|
+
// Handled outside the agent-manifest path below.
|
|
111
|
+
return { kind: "flow" };
|
|
112
|
+
}
|
|
113
|
+
if (request.kind === "delegation") {
|
|
114
|
+
// Lifecycle points of an agent used as a tool: journaled once, so replays never re-emit.
|
|
115
|
+
const outcome = { value: null };
|
|
116
|
+
await t.put("effects", request.effectId, {
|
|
117
|
+
request,
|
|
118
|
+
status: "completed",
|
|
119
|
+
outcome,
|
|
120
|
+
});
|
|
121
|
+
const settled = request.effectId.endsWith(":settled");
|
|
122
|
+
await t.event(s.id, s.activeTurnId, settled ? "delegation.completed" : "delegation.started", {
|
|
123
|
+
agent: request.agent,
|
|
124
|
+
...delegationCallId(request),
|
|
125
|
+
...request.input,
|
|
126
|
+
});
|
|
127
|
+
return resolved({ status: "completed", outcome });
|
|
128
|
+
}
|
|
129
|
+
const agentManifest = manifestFor(s.manifest, request.agent);
|
|
130
|
+
if (!agentManifest)
|
|
131
|
+
throw new Error(`Agent '${request.agent?.id ?? ""}' is not used as a tool`);
|
|
132
|
+
const mcpTool = request.kind === "tool" ? mcpToolOf(s, request) : undefined;
|
|
133
|
+
const sandboxTool = request.kind === "tool" && !mcpTool
|
|
134
|
+
? sandboxCapabilityOf(agentManifest, request.capabilityId, request.toolName)
|
|
135
|
+
: undefined;
|
|
136
|
+
await t.put("effects", request.effectId, {
|
|
137
|
+
request,
|
|
138
|
+
status: request.kind === "model" || mcpTool || sandboxTool
|
|
139
|
+
? "invoking"
|
|
140
|
+
: "pending",
|
|
141
|
+
});
|
|
142
|
+
if (request.kind === "model" || mcpTool || sandboxTool)
|
|
143
|
+
return {
|
|
144
|
+
kind: "invoke",
|
|
145
|
+
invoke: mcpTool ? "mcp" : sandboxTool ? "sandbox" : "model",
|
|
146
|
+
};
|
|
147
|
+
const tool = request.kind === "tool"
|
|
148
|
+
? pinnedTool(agentManifest, request.capabilityId, request.toolName)
|
|
149
|
+
: undefined;
|
|
150
|
+
const base = {
|
|
151
|
+
actionId: request.effectId,
|
|
152
|
+
sessionId: request.sessionId,
|
|
153
|
+
turnId: request.turnId,
|
|
154
|
+
agentId: request.agentId,
|
|
155
|
+
manifestHash: request.manifestHash,
|
|
156
|
+
implementationVersion: s.implementationVersion,
|
|
157
|
+
input: request.input,
|
|
158
|
+
context: request.context,
|
|
159
|
+
status: "pending",
|
|
160
|
+
generation: 0,
|
|
161
|
+
claimId: null,
|
|
162
|
+
leaseExpiresAt: null,
|
|
163
|
+
...(request.agent ? { agent: request.agent } : {}),
|
|
164
|
+
};
|
|
165
|
+
const action = request.kind === "hook"
|
|
166
|
+
? {
|
|
167
|
+
...base,
|
|
168
|
+
kind: "hook",
|
|
169
|
+
hook: {
|
|
170
|
+
at: request.hook.at,
|
|
171
|
+
scope: request.hook.scope,
|
|
172
|
+
capabilityIds: [...request.hook.capabilityIds],
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
: {
|
|
176
|
+
...base,
|
|
177
|
+
kind: "tool",
|
|
178
|
+
capabilityId: request.capabilityId,
|
|
179
|
+
toolName: request.toolName,
|
|
180
|
+
...(tool?.inputSchema ? { inputSchema: tool.inputSchema } : {}),
|
|
181
|
+
...(tool?.outputSchema ? { outputSchema: tool.outputSchema } : {}),
|
|
182
|
+
};
|
|
183
|
+
await t.put("actions", action.actionId, action);
|
|
184
|
+
await t.event(s.id, s.activeTurnId, "action.pending", {
|
|
185
|
+
actionId: action.actionId,
|
|
186
|
+
kind: action.kind,
|
|
187
|
+
...actionTarget(action),
|
|
188
|
+
...(action.kind === "tool" ? toolIds(request.context) : {}),
|
|
189
|
+
input: action.input,
|
|
190
|
+
});
|
|
191
|
+
t.signalWork();
|
|
192
|
+
return resolved({ status: "pending" });
|
|
193
|
+
});
|
|
194
|
+
if (journaled.kind === "resolved")
|
|
195
|
+
return journaled.resolution;
|
|
196
|
+
if (journaled.kind === "flow") {
|
|
197
|
+
if (isFlowEffect(request))
|
|
198
|
+
return resolveNewFlowEffect(ctx, request, signal, lease);
|
|
199
|
+
return { status: "pending" };
|
|
200
|
+
}
|
|
201
|
+
const invoke = journaled.invoke;
|
|
202
|
+
try {
|
|
203
|
+
// The intent is committed; the call itself runs outside any transaction.
|
|
204
|
+
const value = invoke === "mcp"
|
|
205
|
+
? await callMcpTool(ctx, request)
|
|
206
|
+
: invoke === "sandbox"
|
|
207
|
+
? await callSandboxTool(ctx, request, signal)
|
|
208
|
+
: await invokeModel(ctx, request, signal, segment.model);
|
|
209
|
+
return await store.tx(async (t) => {
|
|
210
|
+
const s = await ownedSession(t, lease, request.sessionId);
|
|
211
|
+
// Only a cancel discards an outcome in hand (§10.7). After any other abort (shutdown,
|
|
212
|
+
// deadline) it is recorded, so the next advance replays it instead of calling again.
|
|
213
|
+
if (s.status === "cancelled" ||
|
|
214
|
+
s.activeTurnId !== request.turnId ||
|
|
215
|
+
abortKind(signal) === "cancel")
|
|
216
|
+
throw new Error("Turn cancelled");
|
|
217
|
+
const effect = await t.get("effects", request.effectId);
|
|
218
|
+
effect.status = "completed";
|
|
219
|
+
effect.outcome = { value };
|
|
220
|
+
await t.put("effects", request.effectId, effect);
|
|
221
|
+
const transcript = invoke === "model"
|
|
222
|
+
? assistantMessage(request, value)
|
|
223
|
+
: toolCompleted(request, value);
|
|
224
|
+
if (transcript)
|
|
225
|
+
await t.event(s.id, request.turnId, invoke === "model" ? "message.assistant" : "tool.completed", transcript);
|
|
226
|
+
return { status: "completed", outcome: effect.outcome };
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
// A lost epoch writes nothing: the new owner decides what the effect became.
|
|
231
|
+
if (isOwnershipLost(error))
|
|
232
|
+
throw error;
|
|
233
|
+
await store.tx(async (t) => {
|
|
234
|
+
const s = request.sessionId === lease.sessionId
|
|
235
|
+
? await t.assertEpoch(request.sessionId, lease.epoch)
|
|
236
|
+
: await t.lockSession(request.sessionId);
|
|
237
|
+
const effect = await t.get("effects", request.effectId);
|
|
238
|
+
if (!effect)
|
|
239
|
+
return;
|
|
240
|
+
effect.status = "uncertain";
|
|
241
|
+
effect.error = error instanceof Error ? error.message : String(error);
|
|
242
|
+
await t.put("effects", request.effectId, effect);
|
|
243
|
+
if (s && s.status !== "cancelled" && s.activeTurnId === request.turnId)
|
|
244
|
+
await t.event(s.id, request.turnId, "effect.uncertain", {
|
|
245
|
+
effectId: request.effectId,
|
|
246
|
+
message: effect.error,
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
return { status: "uncertain" };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/** Journal and dispatch a new flow effect (agent / tool node / fn / verify). */
|
|
253
|
+
export async function resolveNewFlowEffect(ctx, request, signal, lease) {
|
|
254
|
+
const { store } = ctx;
|
|
255
|
+
signal.throwIfAborted();
|
|
256
|
+
const step = await store.tx(async (t) => {
|
|
257
|
+
const resolved = (resolution) => ({
|
|
258
|
+
kind: "resolved",
|
|
259
|
+
resolution,
|
|
260
|
+
});
|
|
261
|
+
const workflow = await ownedSession(t, lease, request.sessionId);
|
|
262
|
+
const existing = await t.get("effects", request.effectId);
|
|
263
|
+
if (existing) {
|
|
264
|
+
if (existing.status === "completed")
|
|
265
|
+
return resolved({ status: "completed", outcome: existing.outcome });
|
|
266
|
+
if (existing.status === "queued") {
|
|
267
|
+
// Fall through to dispatch when a concurrency slot is free.
|
|
268
|
+
}
|
|
269
|
+
else if (request.kind === "agent") {
|
|
270
|
+
const agentSessionId = existing.agentSessionId;
|
|
271
|
+
const agent = agentSessionId
|
|
272
|
+
? await t.get("sessions", agentSessionId)
|
|
273
|
+
: undefined;
|
|
274
|
+
const outcome = await linkedOutcome(t, existing, agent);
|
|
275
|
+
if (!outcome)
|
|
276
|
+
return resolved({ status: "pending" });
|
|
277
|
+
existing.status = "completed";
|
|
278
|
+
existing.outcome = outcome;
|
|
279
|
+
await t.put("effects", request.effectId, existing);
|
|
280
|
+
return resolved({ status: "completed", outcome });
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
return resolved({ status: "pending" });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const active = await countActiveFlowWork(t, request.sessionId, request.turnId);
|
|
287
|
+
if (!mayDispatchMore(active, ctx.flowLimits)) {
|
|
288
|
+
if (!existing || existing.status !== "queued")
|
|
289
|
+
await t.put("effects", request.effectId, {
|
|
290
|
+
request,
|
|
291
|
+
status: "queued",
|
|
292
|
+
});
|
|
293
|
+
return resolved({ status: "pending" });
|
|
294
|
+
}
|
|
295
|
+
if (request.kind === "fn" ||
|
|
296
|
+
request.kind === "verify" ||
|
|
297
|
+
isFlowToolEffect(request)) {
|
|
298
|
+
await t.put("effects", request.effectId, {
|
|
299
|
+
request,
|
|
300
|
+
status: "pending",
|
|
301
|
+
});
|
|
302
|
+
const action = request.kind === "fn" || request.kind === "verify"
|
|
303
|
+
? {
|
|
304
|
+
actionId: request.effectId,
|
|
305
|
+
sessionId: request.sessionId,
|
|
306
|
+
turnId: request.turnId,
|
|
307
|
+
agentId: request.agentId,
|
|
308
|
+
manifestHash: request.manifestHash,
|
|
309
|
+
implementationVersion: workflow.implementationVersion,
|
|
310
|
+
input: request.input,
|
|
311
|
+
context: request.context,
|
|
312
|
+
status: "pending",
|
|
313
|
+
generation: 0,
|
|
314
|
+
claimId: null,
|
|
315
|
+
leaseExpiresAt: null,
|
|
316
|
+
kind: request.kind,
|
|
317
|
+
path: request.path,
|
|
318
|
+
key: request.key,
|
|
319
|
+
}
|
|
320
|
+
: {
|
|
321
|
+
actionId: request.effectId,
|
|
322
|
+
sessionId: request.sessionId,
|
|
323
|
+
turnId: request.turnId,
|
|
324
|
+
agentId: request.agentId,
|
|
325
|
+
manifestHash: request.manifestHash,
|
|
326
|
+
implementationVersion: workflow.implementationVersion,
|
|
327
|
+
input: request.input,
|
|
328
|
+
context: request.context,
|
|
329
|
+
status: "pending",
|
|
330
|
+
generation: 0,
|
|
331
|
+
claimId: null,
|
|
332
|
+
leaseExpiresAt: null,
|
|
333
|
+
kind: "tool",
|
|
334
|
+
path: request.path,
|
|
335
|
+
key: request.key,
|
|
336
|
+
};
|
|
337
|
+
await t.put("actions", action.actionId, action);
|
|
338
|
+
if (isFlowToolEffect(request)) {
|
|
339
|
+
await t.event(workflow.id, workflow.activeTurnId, "node.started", {
|
|
340
|
+
path: request.path,
|
|
341
|
+
kind: "tool",
|
|
342
|
+
key: request.key,
|
|
343
|
+
...(request.iterations !== undefined
|
|
344
|
+
? { iterations: request.iterations }
|
|
345
|
+
: {}),
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
await t.event(workflow.id, workflow.activeTurnId, "action.pending", {
|
|
349
|
+
actionId: action.actionId,
|
|
350
|
+
kind: action.kind,
|
|
351
|
+
path: action.path,
|
|
352
|
+
key: action.key,
|
|
353
|
+
input: action.input,
|
|
354
|
+
});
|
|
355
|
+
t.signalWork();
|
|
356
|
+
return resolved({ status: "pending" });
|
|
357
|
+
}
|
|
358
|
+
return { kind: "agent", workflow };
|
|
359
|
+
});
|
|
360
|
+
if (step.kind === "resolved")
|
|
361
|
+
return step.resolution;
|
|
362
|
+
// agent effect: create linked session + message via the public contract path
|
|
363
|
+
const workflow = step.workflow;
|
|
364
|
+
const body = request.input;
|
|
365
|
+
const path = body.path ?? request.path;
|
|
366
|
+
const agentSessionId = deriveAgentEffectSessionId(workflow.id, path, request);
|
|
367
|
+
const iterations = request.iterations ?? "-";
|
|
368
|
+
const n = Number(request.context.n ?? iterations.split(".")[0] ?? 1);
|
|
369
|
+
await store.tx(async (t) => {
|
|
370
|
+
await ownedSession(t, lease, workflow.id);
|
|
371
|
+
await t.put("effects", request.effectId, {
|
|
372
|
+
request,
|
|
373
|
+
status: "pending",
|
|
374
|
+
agentSessionId,
|
|
375
|
+
});
|
|
376
|
+
await t.put("links", agentSessionId, {
|
|
377
|
+
workflowSessionId: workflow.id,
|
|
378
|
+
path,
|
|
379
|
+
effectId: request.effectId,
|
|
380
|
+
turnId: request.turnId,
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
await store.tx(async (t) => {
|
|
384
|
+
// The linked agent (child) session is locked before the workflow (parent).
|
|
385
|
+
const exists = await t.lockSession(agentSessionId);
|
|
386
|
+
await ownedSession(t, lease, workflow.id);
|
|
387
|
+
if (exists)
|
|
388
|
+
return;
|
|
389
|
+
const definition = (await t.get("definitions", body.agentId)) ??
|
|
390
|
+
fail(404, "Definition not found");
|
|
391
|
+
const sandboxOwnerId = sandboxSpecOf(workflow.manifest) || workflow.sandboxOwnerId
|
|
392
|
+
? owningSandboxSessionId(workflow, await sandboxLookup(t, workflow.sandboxOwnerId))
|
|
393
|
+
: undefined;
|
|
394
|
+
const created = {
|
|
395
|
+
id: agentSessionId,
|
|
396
|
+
agentId: body.agentId,
|
|
397
|
+
ownerUserId: workflow.ownerUserId,
|
|
398
|
+
manifest: definition.manifest,
|
|
399
|
+
manifestHash: definition.manifestHash,
|
|
400
|
+
implementationVersion: definition.implementationVersion,
|
|
401
|
+
status: "idle",
|
|
402
|
+
activeTurnId: null,
|
|
403
|
+
creation: {
|
|
404
|
+
requestId: `flow-put-${request.effectId}`,
|
|
405
|
+
agentId: body.agentId,
|
|
406
|
+
ownerUserId: workflow.ownerUserId,
|
|
407
|
+
...(sandboxOwnerId ? { sandbox: { session: sandboxOwnerId } } : {}),
|
|
408
|
+
},
|
|
409
|
+
vaultIds: workflow.vaultIds,
|
|
410
|
+
credentialSelections: workflow.credentialSelections,
|
|
411
|
+
pluginRoots: definition.pluginRoots ?? {},
|
|
412
|
+
...(sandboxOwnerId ? { sandboxOwnerId } : {}),
|
|
413
|
+
streamIncarnation: newStreamIncarnation(),
|
|
414
|
+
};
|
|
415
|
+
await t.put("sessions", agentSessionId, created);
|
|
416
|
+
});
|
|
417
|
+
// Binds this effect to the one linked turn its message opens (`linkedTurnEnd`).
|
|
418
|
+
const idempotencyKey = linkedMessageKey(request);
|
|
419
|
+
const messageInput = body.input;
|
|
420
|
+
const messageCommand = typeof messageInput === "string"
|
|
421
|
+
? {
|
|
422
|
+
type: "message",
|
|
423
|
+
content: messageInput,
|
|
424
|
+
requestId: `flow-${request.effectId}`,
|
|
425
|
+
idempotencyKey,
|
|
426
|
+
...(body.manifest ? { manifest: body.manifest } : {}),
|
|
427
|
+
}
|
|
428
|
+
: {
|
|
429
|
+
type: "message",
|
|
430
|
+
data: messageInput,
|
|
431
|
+
requestId: `flow-${request.effectId}`,
|
|
432
|
+
idempotencyKey,
|
|
433
|
+
...(body.manifest ? { manifest: body.manifest } : {}),
|
|
434
|
+
};
|
|
435
|
+
const accepted = (await command(ctx, agentSessionId, messageCommand, {
|
|
436
|
+
kind: "application",
|
|
437
|
+
principalId: "flow-host",
|
|
438
|
+
}));
|
|
439
|
+
// Workflow events; the linked agent session is only read here, never locked.
|
|
440
|
+
return store.tx(async (t) => {
|
|
441
|
+
const s = await ownedSession(t, lease, request.sessionId);
|
|
442
|
+
const agent = await sessionOf(t, agentSessionId);
|
|
443
|
+
await t.event(s.id, s.activeTurnId, "loop.iteration", {
|
|
444
|
+
path: String(request.context.loopPath ?? path.split("/")[0]),
|
|
445
|
+
n,
|
|
446
|
+
sessionId: agentSessionId,
|
|
447
|
+
turnId: accepted.turnId ?? undefined,
|
|
448
|
+
manifestHash: agent.checkpoint?.manifestHash,
|
|
449
|
+
});
|
|
450
|
+
await t.event(s.id, s.activeTurnId, "node.agent", {
|
|
451
|
+
path,
|
|
452
|
+
iterations,
|
|
453
|
+
sessionId: agentSessionId,
|
|
454
|
+
turnId: accepted.turnId,
|
|
455
|
+
});
|
|
456
|
+
// May already be settled if the agent was fast / replayed.
|
|
457
|
+
const outcome = await linkedOutcome(t, { request, agentSessionId }, agent);
|
|
458
|
+
if (outcome) {
|
|
459
|
+
await t.put("effects", request.effectId, {
|
|
460
|
+
request,
|
|
461
|
+
status: "completed",
|
|
462
|
+
outcome,
|
|
463
|
+
agentSessionId,
|
|
464
|
+
});
|
|
465
|
+
return { status: "completed", outcome };
|
|
466
|
+
}
|
|
467
|
+
return { status: "pending" };
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
/** Discover the session's MCP tools once, or reconnect and refresh diagnostics. */
|
|
471
|
+
export async function prepareMcp(ctx, lease, signal) {
|
|
472
|
+
const { store } = ctx;
|
|
473
|
+
const id = lease.sessionId;
|
|
474
|
+
const s = await loadSession(ctx, id);
|
|
475
|
+
if (serversOf(s.manifest).length === 0)
|
|
476
|
+
return;
|
|
477
|
+
if (!s.mcpSnapshot) {
|
|
478
|
+
const found = await ctx.mcp.discover({
|
|
479
|
+
sessionId: id,
|
|
480
|
+
manifest: s.manifest,
|
|
481
|
+
manifestHash: s.manifestHash,
|
|
482
|
+
pluginRoots: s.pluginRoots ?? {},
|
|
483
|
+
signal,
|
|
484
|
+
});
|
|
485
|
+
await store.tx(async (t) => {
|
|
486
|
+
const current = await ownedSession(t, lease, id);
|
|
487
|
+
if (current.mcpSnapshot)
|
|
488
|
+
return;
|
|
489
|
+
current.mcpSnapshot = found.snapshot;
|
|
490
|
+
current.mcpDiagnostics = found.diagnostics;
|
|
491
|
+
await t.put("sessions", id, current);
|
|
492
|
+
});
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const diagnostics = await ctx.mcp.reconnect({
|
|
496
|
+
sessionId: id,
|
|
497
|
+
manifest: s.manifest,
|
|
498
|
+
pluginRoots: s.pluginRoots ?? {},
|
|
499
|
+
tools: s.mcpSnapshot.mcpTools,
|
|
500
|
+
signal,
|
|
501
|
+
});
|
|
502
|
+
if (diagnostics.length === 0)
|
|
503
|
+
return;
|
|
504
|
+
await store.tx(async (t) => {
|
|
505
|
+
const current = await ownedSession(t, lease, id);
|
|
506
|
+
const prior = [...(current.mcpDiagnostics ?? [])];
|
|
507
|
+
for (const item of diagnostics) {
|
|
508
|
+
const index = prior.findIndex((existing) => existing.capabilityId === item.capabilityId &&
|
|
509
|
+
existing.serverName === item.serverName);
|
|
510
|
+
if (index >= 0)
|
|
511
|
+
prior[index] = item;
|
|
512
|
+
else
|
|
513
|
+
prior.push(item);
|
|
514
|
+
}
|
|
515
|
+
current.mcpDiagnostics = prior;
|
|
516
|
+
await t.put("sessions", id, current);
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
async function callMcpTool(ctx, request) {
|
|
520
|
+
const s = await loadSession(ctx, request.sessionId);
|
|
521
|
+
const tool = mcpToolOf(s, request);
|
|
522
|
+
if (!tool)
|
|
523
|
+
throw new Error(`MCP tool '${request.toolName ?? ""}' is not in the session snapshot`);
|
|
524
|
+
return ctx.mcp.call({
|
|
525
|
+
sessionId: s.id,
|
|
526
|
+
...(tool.agentId === undefined ? {} : { agentId: tool.agentId }),
|
|
527
|
+
capabilityId: tool.capabilityId,
|
|
528
|
+
serverName: tool.serverName,
|
|
529
|
+
serverToolName: tool.serverToolName,
|
|
530
|
+
args: request.input,
|
|
531
|
+
manifest: s.manifest,
|
|
532
|
+
pluginRoots: s.pluginRoots ?? {},
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
async function callSandboxTool(ctx, request, signal) {
|
|
536
|
+
const { s, ownerId } = await ctx.store.tx(async (t) => {
|
|
537
|
+
const s = await sessionOf(t, request.sessionId);
|
|
538
|
+
// Agents used as tools share the session's sandbox; the tree declares one sandbox spec.
|
|
539
|
+
const lookup = await sandboxLookup(t, s.id);
|
|
540
|
+
return { s, ownerId: owningSandboxSessionId(s, lookup) };
|
|
541
|
+
});
|
|
542
|
+
const capability = sandboxCapabilityOf(manifestFor(s.manifest, request.agent), request.capabilityId, request.toolName);
|
|
543
|
+
if (!capability)
|
|
544
|
+
throw new Error(`'${request.toolName ?? ""}' is not a sandbox tool`);
|
|
545
|
+
return ctx.sandbox.run({ id: ownerId, activeTurnId: s.activeTurnId, manifest: s.manifest }, capability, request.toolName, request.input, signal);
|
|
546
|
+
}
|
|
547
|
+
/** Vault authorization for an MCP server request made on behalf of a session. */
|
|
548
|
+
export async function authorize(ctx, sessionId, request) {
|
|
549
|
+
const s = await loadSession(ctx, sessionId);
|
|
550
|
+
const result = await ctx.vault.authorize({
|
|
551
|
+
sessionId,
|
|
552
|
+
vaultIds: s.vaultIds ?? [],
|
|
553
|
+
credentialSelections: s.credentialSelections ?? [],
|
|
554
|
+
url: request.url,
|
|
555
|
+
serverName: request.serverName,
|
|
556
|
+
});
|
|
557
|
+
if (result.status === "authorized") {
|
|
558
|
+
const token = result.headers.authorization.slice("Bearer ".length);
|
|
559
|
+
scrub({ authorization: result.headers.authorization, url: result.url }, [
|
|
560
|
+
token,
|
|
561
|
+
]);
|
|
562
|
+
}
|
|
563
|
+
return result;
|
|
564
|
+
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { type TenantEnvelope } from "@nylorun/core/contracts";
|
|
2
2
|
export declare function parseEnvelope(raw: unknown, expectedId?: string): TenantEnvelope;
|
|
3
|
-
export declare function readEnvelopeFile(path: string, expectedId?: string): TenantEnvelope;
|
|
4
|
-
/** Write envelope via temp sibling then rename (atomic on the same filesystem). */
|
|
5
|
-
export declare function writeEnvelopeFile(envelopePath: string, envelope: TenantEnvelope): void;
|
|
6
3
|
export declare function envelopeNow(input: {
|
|
7
4
|
id: string;
|
|
8
5
|
name: string;
|
package/dist/tenant/envelope.js
CHANGED
|
@@ -1,47 +1,15 @@
|
|
|
1
|
-
import { readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
1
|
import { TenantEnvelopeSchema, } from "@nylorun/core/contracts";
|
|
4
2
|
import { quarantine } from "./quarantine.js";
|
|
5
3
|
export function parseEnvelope(raw, expectedId) {
|
|
6
4
|
const parsed = TenantEnvelopeSchema.safeParse(raw);
|
|
7
5
|
if (!parsed.success) {
|
|
8
|
-
throw quarantine("envelope-invalid", "
|
|
6
|
+
throw quarantine("envelope-invalid", "the Tenant envelope is missing or invalid", expectedId ? { tenantId: expectedId } : {});
|
|
9
7
|
}
|
|
10
8
|
if (expectedId !== undefined && parsed.data.id !== expectedId) {
|
|
11
|
-
throw quarantine("envelope-invalid", `
|
|
9
|
+
throw quarantine("envelope-invalid", `the Tenant envelope id ${parsed.data.id} does not match ${expectedId}`, { tenantId: expectedId });
|
|
12
10
|
}
|
|
13
11
|
return parsed.data;
|
|
14
12
|
}
|
|
15
|
-
export function readEnvelopeFile(path, expectedId) {
|
|
16
|
-
let text;
|
|
17
|
-
try {
|
|
18
|
-
text = readFileSync(path, "utf8");
|
|
19
|
-
}
|
|
20
|
-
catch (error) {
|
|
21
|
-
const code = error.code;
|
|
22
|
-
if (code === "ENOENT") {
|
|
23
|
-
throw quarantine("envelope-invalid", "tenant.json is missing", expectedId ? { tenantId: expectedId } : {});
|
|
24
|
-
}
|
|
25
|
-
throw quarantine("corrupt", "tenant.json could not be read", expectedId ? { tenantId: expectedId } : {});
|
|
26
|
-
}
|
|
27
|
-
let json;
|
|
28
|
-
try {
|
|
29
|
-
json = JSON.parse(text);
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
throw quarantine("envelope-invalid", "tenant.json is not valid JSON", expectedId ? { tenantId: expectedId } : {});
|
|
33
|
-
}
|
|
34
|
-
return parseEnvelope(json, expectedId);
|
|
35
|
-
}
|
|
36
|
-
/** Write envelope via temp sibling then rename (atomic on the same filesystem). */
|
|
37
|
-
export function writeEnvelopeFile(envelopePath, envelope) {
|
|
38
|
-
const validated = parseEnvelope(envelope, envelope.id);
|
|
39
|
-
const tmp = join(envelopePath + `.tmp-${process.pid}-${Date.now()}`);
|
|
40
|
-
writeFileSync(tmp, `${JSON.stringify(validated, null, 2)}\n`, {
|
|
41
|
-
mode: 0o600,
|
|
42
|
-
});
|
|
43
|
-
renameSync(tmp, envelopePath);
|
|
44
|
-
}
|
|
45
13
|
export function envelopeNow(input, now = new Date()) {
|
|
46
14
|
const iso = now.toISOString();
|
|
47
15
|
return {
|
|
@@ -1,19 +1,30 @@
|
|
|
1
|
+
import type { PostgresClient } from "../store/postgres/connect.js";
|
|
1
2
|
import type { Logger, TenantModelConfig } from "./types.js";
|
|
2
3
|
export interface StartEphemeralRuntimeOptions {
|
|
3
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Absolute Host root for `host.json`, the admin key and each Tenant's files (vault key,
|
|
6
|
+
* home, sandboxes). Caller creates any temporary directory. Session data never lands here.
|
|
7
|
+
*/
|
|
4
8
|
hostRoot: string;
|
|
5
9
|
tenantId?: string;
|
|
6
10
|
name?: string;
|
|
7
11
|
applicationKey?: string;
|
|
8
12
|
adminKey?: string;
|
|
9
13
|
principalId?: string;
|
|
14
|
+
/** SHA-256 of a derived Studio key; registers principal `studio`. */
|
|
15
|
+
studioCredentialHash?: string;
|
|
10
16
|
/** Allowlisted baseline for childEnv (e.g. PATH). Never read from ambient here. */
|
|
11
17
|
baseline?: Readonly<Record<string, string>>;
|
|
12
18
|
model?: TenantModelConfig;
|
|
13
|
-
sandboxBackend?: "auto" | "
|
|
19
|
+
sandboxBackend?: "auto" | "virtual";
|
|
14
20
|
/** When true, close() leaves hostRoot on disk. */
|
|
15
21
|
retainRoot?: boolean;
|
|
16
22
|
logger?: Logger;
|
|
23
|
+
/**
|
|
24
|
+
* A Postgres pool: Tenants become schemas in it (`store-pg.ts`) instead of living in memory.
|
|
25
|
+
* The caller ends the pool; the schemas stay.
|
|
26
|
+
*/
|
|
27
|
+
database?: PostgresClient;
|
|
17
28
|
}
|
|
18
29
|
export interface EphemeralRuntime {
|
|
19
30
|
url: string;
|
|
@@ -25,7 +36,7 @@ export interface EphemeralRuntime {
|
|
|
25
36
|
close(): Promise<void>;
|
|
26
37
|
}
|
|
27
38
|
/**
|
|
28
|
-
* Private Host on port 0 with one Tenant (D9 / A19).
|
|
29
|
-
* Exported from `@nylorun/runtime/core`.
|
|
39
|
+
* Private Host on port 0 with one Tenant (D9 / A19). Not durable (see the module comment).
|
|
40
|
+
* Exported from `@nylorun/runtime` and `@nylorun/runtime/core`.
|
|
30
41
|
*/
|
|
31
42
|
export declare function startEphemeralRuntime(options: StartEphemeralRuntimeOptions): Promise<EphemeralRuntime>;
|