@rivus/agent 0.14.4 → 0.15.0
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/dist/bootstrap/pi-feishu.d.ts +2 -2
- package/dist/bootstrap/pi-feishu.js +4118 -388
- package/dist/chunks/index.d.ts +117 -1
- package/dist/chunks/pi.js +56 -19
- package/dist/chunks/rivus-daemon-cli.js +452 -144
- package/dist/chunks/rivus-model-management-wire.js +344 -0
- package/dist/chunks/rivus-plugin-testkit.js +1 -1
- package/dist/chunks/{tool-input-digest.js → rivus-tool.js} +67 -67
- package/dist/chunks/src.js +1885 -1687
- package/dist/cli.js +152 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -4
- package/dist/mcp.js +1 -1
- package/dist/pi.d.ts +2 -0
- package/dist/pi.js +1 -1
- package/examples/pi-feishu-deployment.bootstrap.ts +557 -462
- package/package.json +5 -3
- package/skills/runtime-management/SKILL.md +61 -0
|
@@ -1,15 +1,3695 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { d as resolveInvocationAuthority, r as createPiSkillRuntime, s as createToolInputDigest, u as createInvocationAuthority } from "../chunks/rivus-tool.js";
|
|
2
|
+
import { $t as openJsonlRecoveryControl, A as createScheduledAutomation, An as createLangfuseAgentTelemetry, Bn as createFeishuTopicContextResolver, Dn as createPiAgentLoop, Dt as createBackgroundSessionHostTools, Ei as openJsonFeishuSessionStore, En as createPiSessionRegistry, Gi as readPersistenceFile, Ii as createFeishuCardDeliveryReconciler, In as createJsonlAgentEventLog, Ji as mergePiProviderModelDeclaration, Jt as createFeishuPresentationPreparation, K as createRivusMemoryToolDescriptor, Ki as isRecord$4, Lt as createBackgroundSessionService, Nt as openJsonlBackgroundSessionDeliveryStore, Oi as openJsonlFeishuInboxRepository, Ot as createBackgroundSessionSupervisor, Pi as openJsonlFeishuCardDeliveryLedger, Qr as createUuidRunIds, Si as createJsonFetchRequest, Ti as createSessionScheduler, Tn as readAutomationPresentation, Ui as createToolBroker, Un as createConfiguredFeishuTextReplySender, W as createProjectMemoryPromptPreparer, Wi as openJsonlToolOperationLedger, Wn as createConfiguredFeishuCardRolloverRuntime, Xt as createAgentHarnessPooledRuntime, Yi as writeAtomicTextFile, Yt as createFeishuDeploymentEndpoint, Zr as createSystemClock, an as createAgentsMdInstructionsProvider, bn as createJsonFileFeishuCardTargetRegistry, c as createConfiguredFeishuAutomationCardSender, d as createHumanInteractionToolApprovalGateway, en as openJsonlToolOperationLedger$1, f as createHumanInteractionEndpointRegistry, g as loadRivusDeploymentManifest, hr as createLazyFeishuWebSocketEventDispatcher, ht as resolveFeishuDeliveryChatId, ii as createAgentHarness, j as AUTOMATION_SUPPRESSION_PREFIX, jn as resolveLangfuseTelemetryConfig, jt as openJsonlBackgroundSessionRepository, k as openJsonAutomationTickRepository, l as createRoutedHumanInteractionToolApprovalService, mn as createFeishuCotPublisher, mt as createConfiguredFeishuBackgroundSessionDelivery, n as createConfiguredFeishuHumanInteractionPresenter, o as createJsonlHumanInteractionRepository, on as createWorkspaceRootHandle, qi as mergePiProviderBaseUrlOverride, t as createHumanInteractionService, tn as createToolBroker$1, tr as createConfiguredFeishuOpenApiClient, u as createHumanInteractionModelChangeApproval, wt as resolveBackgroundSessionSupervisorIntervalMs, z as openJsonlAgentMemoryService } from "../chunks/src.js";
|
|
3
|
+
import { I as resolveFeishuEndpointCredentials, J as migrateRivusModelManagementConfig, K as installRivusModelManagementCliLauncher, U as runDeploymentProcessEffect, X as loadMergedLocalEnvFile, Y as parseRivusModelManagementHomeConfig, q as installRivusRuntimeManagementSkill } from "../chunks/rivus-daemon-cli.js";
|
|
4
|
+
import { t as createSha256Digest } from "../chunks/sha256-digest.js";
|
|
5
|
+
import { a as toRivusModelManagementSubmission, i as projectRivusModelCliResponse, r as parseRivusModelManagementWireRequest, t as createRivusModelManagementFailure } from "../chunks/rivus-model-management-wire.js";
|
|
3
6
|
import { f as createBackgroundSessionStepSourceMessageId } from "../chunks/background-session-authority.js";
|
|
4
|
-
import { d as createPiSkillRuntime } from "../chunks/tool-input-digest.js";
|
|
5
7
|
import { i as resolvePiSessionToolNames, l as validatePiSkillCommand, n as createPiToolProxyDefinitions, p as createPiSkillReadTools, r as createPiSessionResources, t as createPiToolNameResolver } from "../chunks/pi.js";
|
|
6
|
-
import { Effect } from "effect";
|
|
8
|
+
import { Effect, Either } from "effect";
|
|
7
9
|
import { createHash, randomUUID } from "node:crypto";
|
|
8
|
-
import { mkdir, readFile } from "node:fs/promises";
|
|
9
|
-
import { join, relative } from "node:path";
|
|
10
|
+
import { chmod, mkdir, open, readFile, realpath, rename, stat, unlink } from "node:fs/promises";
|
|
11
|
+
import { delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
12
|
+
import { ModelRuntime, SessionManager, SettingsManager, createAgentSession, createBashToolDefinition, defineTool, estimateTokens } from "@earendil-works/pi-coding-agent";
|
|
10
13
|
import { homedir } from "node:os";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { Type } from "typebox";
|
|
16
|
+
import { createConnection, createServer } from "node:net";
|
|
11
17
|
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
12
|
-
|
|
18
|
+
//#region src/adapters/pi/model-management/pi-model-run-context.ts
|
|
19
|
+
const RIVUS_MODEL_CONTEXT_ENV = "RIVUS_MODEL_CONTEXT";
|
|
20
|
+
const RIVUS_MODEL_SOCKET_ENV = "RIVUS_MODEL_SOCKET";
|
|
21
|
+
const RIVUS_MODEL_CHANGE_TOOL_ID = "rivus.model.change";
|
|
22
|
+
var PiModelRunContextError = class extends Error {
|
|
23
|
+
code;
|
|
24
|
+
name = "PiModelRunContextError";
|
|
25
|
+
constructor(code, message) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.code = code;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Keep the model-management capability bound to a concrete host-owned Run.
|
|
32
|
+
* The reference is the only value that crosses into a child process; all
|
|
33
|
+
* invocation identity stays in this process behind the authority WeakMap.
|
|
34
|
+
*/
|
|
35
|
+
function createPiModelRunContextRegistry(options) {
|
|
36
|
+
validateRegistryOptions(options);
|
|
37
|
+
const entries = /* @__PURE__ */ new Map();
|
|
38
|
+
const toolGrantSet = createToolGrantSet(options);
|
|
39
|
+
const revoke = (reference) => {
|
|
40
|
+
const normalizedReference = reference;
|
|
41
|
+
const entry = entries.get(normalizedReference);
|
|
42
|
+
if (!entry) return false;
|
|
43
|
+
entries.delete(normalizedReference);
|
|
44
|
+
entry.abortSignal.removeEventListener("abort", entry.revokeOnAbort);
|
|
45
|
+
return true;
|
|
46
|
+
};
|
|
47
|
+
const issue = (input) => {
|
|
48
|
+
const invocation = validateAgentLoopInput(input);
|
|
49
|
+
const reference = randomUUID();
|
|
50
|
+
const authority = createInvocationAuthority({
|
|
51
|
+
agentId: options.agentId,
|
|
52
|
+
allowedActorOpenIds: [...invocation.allowedActorOpenIds],
|
|
53
|
+
endpointId: invocation.endpointId,
|
|
54
|
+
instanceId: options.instanceId,
|
|
55
|
+
runId: input.runId,
|
|
56
|
+
sessionKey: input.sessionKey,
|
|
57
|
+
sourceMessageId: invocation.sourceMessageId,
|
|
58
|
+
tenantKey: invocation.tenantKey,
|
|
59
|
+
toolGrantSet
|
|
60
|
+
});
|
|
61
|
+
const context = Object.freeze({
|
|
62
|
+
reference,
|
|
63
|
+
runId: input.runId
|
|
64
|
+
});
|
|
65
|
+
const trustedRun = Object.freeze({
|
|
66
|
+
authority,
|
|
67
|
+
kind: "human"
|
|
68
|
+
});
|
|
69
|
+
const entry = {
|
|
70
|
+
abortSignal: input.abortSignal,
|
|
71
|
+
context,
|
|
72
|
+
revokeOnAbort: () => {
|
|
73
|
+
revoke(reference);
|
|
74
|
+
},
|
|
75
|
+
trustedRun
|
|
76
|
+
};
|
|
77
|
+
entries.set(reference, entry);
|
|
78
|
+
input.abortSignal.addEventListener("abort", entry.revokeOnAbort, { once: true });
|
|
79
|
+
return context;
|
|
80
|
+
};
|
|
81
|
+
return Object.freeze({
|
|
82
|
+
issue,
|
|
83
|
+
resolveRun: (reference) => entries.get(reference)?.trustedRun,
|
|
84
|
+
revoke
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Inject the private transport values at process spawn time. The SDK passes
|
|
89
|
+
* this hook the command and environment separately, so the opaque reference
|
|
90
|
+
* never becomes model prompt content, an argv item, or a JSON request field.
|
|
91
|
+
*/
|
|
92
|
+
function createPiModelManagementBashSpawnHook(options) {
|
|
93
|
+
if (!isAbsolute(options.socketPath)) throw new Error(`${RIVUS_MODEL_SOCKET_ENV} must be an absolute path`);
|
|
94
|
+
if (!isContextReference(options.context.reference)) throw new Error(`${RIVUS_MODEL_CONTEXT_ENV} must be an issued opaque reference`);
|
|
95
|
+
const socketPath = options.socketPath;
|
|
96
|
+
const contextReference = options.context.reference;
|
|
97
|
+
return ({ command, cwd, env }) => ({
|
|
98
|
+
command,
|
|
99
|
+
cwd,
|
|
100
|
+
env: {
|
|
101
|
+
...env,
|
|
102
|
+
[RIVUS_MODEL_CONTEXT_ENV]: contextReference,
|
|
103
|
+
[RIVUS_MODEL_SOCKET_ENV]: socketPath
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function validateRegistryOptions(options) {
|
|
108
|
+
if (!isNonEmptyString(options.agentId) || !isNonEmptyString(options.instanceId)) throw new Error("model Run context registry requires a Host agentId and instanceId");
|
|
109
|
+
if (!isNonEmptyString(options.toolGrantSet.revision)) throw new Error("model Run context registry requires a tool grant revision");
|
|
110
|
+
if (options.toolGrantSet.toolIds.some((toolId) => !isNonEmptyString(toolId))) throw new Error("model Run context registry requires non-empty tool grant ids");
|
|
111
|
+
}
|
|
112
|
+
function createToolGrantSet(options) {
|
|
113
|
+
const toolIds = new Set(options.toolGrantSet.toolIds);
|
|
114
|
+
if (options.modelManagementEnabled === true) toolIds.add(RIVUS_MODEL_CHANGE_TOOL_ID);
|
|
115
|
+
return {
|
|
116
|
+
revision: options.toolGrantSet.revision,
|
|
117
|
+
toolIds: [...toolIds]
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function validateAgentLoopInput(input) {
|
|
121
|
+
if (!isRecord$3(input) || !isAbortSignal(input.abortSignal) || !isNonEmptyString(input.runId) || !isNonEmptyString(input.sessionKey) || typeof input.text !== "string") throw new PiModelRunContextError("invalid-input", "model management requires a concrete AgentLoopInput from the active Host Run");
|
|
122
|
+
if (input.abortSignal.aborted) throw new PiModelRunContextError("run-aborted", "model management cannot issue a context for an aborted Run");
|
|
123
|
+
const invocation = input.invocation;
|
|
124
|
+
if (!isHumanTrustedFeishuInvocation(invocation)) throw new PiModelRunContextError("untrusted-invocation", "model management requires one trusted human Feishu invocation actor");
|
|
125
|
+
return invocation;
|
|
126
|
+
}
|
|
127
|
+
function isHumanTrustedFeishuInvocation(value) {
|
|
128
|
+
if (!isRecord$3(value) || value.kind !== "feishu" || !Array.isArray(value.allowedActorOpenIds) || value.allowedActorOpenIds.length !== 1) return false;
|
|
129
|
+
return value.allowedActorOpenIds.every(isNonEmptyString) && isNonEmptyString(value.endpointId) && isNonEmptyString(value.sourceMessageId) && isNonEmptyString(value.tenantKey);
|
|
130
|
+
}
|
|
131
|
+
function isContextReference(value) {
|
|
132
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
133
|
+
}
|
|
134
|
+
function isAbortSignal(value) {
|
|
135
|
+
return isRecord$3(value) && typeof value.aborted === "boolean" && typeof value.addEventListener === "function" && typeof value.removeEventListener === "function";
|
|
136
|
+
}
|
|
137
|
+
function isNonEmptyString(value) {
|
|
138
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
139
|
+
}
|
|
140
|
+
function isRecord$3(value) {
|
|
141
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
142
|
+
}
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region src/adapters/pi/model-management/pi-managed-session-context.ts
|
|
145
|
+
/** Bind the existing Bash tool to the current Run without adding a model-visible tool. */
|
|
146
|
+
function createPiManagedSessionContext(options) {
|
|
147
|
+
let context;
|
|
148
|
+
const deactivate = () => {
|
|
149
|
+
if (context) options.contexts.revoke(context.reference);
|
|
150
|
+
context = void 0;
|
|
151
|
+
};
|
|
152
|
+
return {
|
|
153
|
+
activate: (input) => {
|
|
154
|
+
deactivate();
|
|
155
|
+
if (input.invocation?.kind === "feishu" && input.invocation.allowedActorOpenIds.length === 1) context = options.contexts.issue(input);
|
|
156
|
+
},
|
|
157
|
+
deactivate,
|
|
158
|
+
tool: defineTool(createBashToolDefinition(options.cwd, { spawnHook: (spawn) => {
|
|
159
|
+
const { RIVUS_MODEL_CONTEXT: _context, RIVUS_MODEL_SOCKET: _socket, ...baseEnv } = spawn.env;
|
|
160
|
+
const prepared = {
|
|
161
|
+
...spawn,
|
|
162
|
+
env: {
|
|
163
|
+
...baseEnv,
|
|
164
|
+
PATH: [
|
|
165
|
+
options.binDirectory,
|
|
166
|
+
dirname(options.nodeExecutable),
|
|
167
|
+
baseEnv.PATH
|
|
168
|
+
].filter(Boolean).join(delimiter),
|
|
169
|
+
RIVUS_MODEL_MANAGEMENT_ENABLED: "true",
|
|
170
|
+
RIVUS_MODEL_SOCKET: options.socketPath
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
return context ? createPiModelManagementBashSpawnHook({
|
|
174
|
+
context,
|
|
175
|
+
socketPath: options.socketPath
|
|
176
|
+
})(prepared) : prepared;
|
|
177
|
+
} }))
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
//#endregion
|
|
181
|
+
//#region src/adapters/pi/model-management/pi-model-resolution.ts
|
|
182
|
+
var PiModelResolutionError = class extends Error {
|
|
183
|
+
name = "PiModelResolutionError";
|
|
184
|
+
code;
|
|
185
|
+
resolutionCode;
|
|
186
|
+
constructor(resolutionCode, message) {
|
|
187
|
+
super(message);
|
|
188
|
+
this.code = resolutionCode === "provider-unknown" ? "model_change_provider_unsupported" : "model_change_target_unsupported";
|
|
189
|
+
this.resolutionCode = resolutionCode;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* Resolve one exact provider/model pair from the installed Pi model runtime.
|
|
194
|
+
*
|
|
195
|
+
* Model aliases and fuzzy matches are intentionally excluded here. Natural-language
|
|
196
|
+
* interpretation belongs to the application/Agent; the activation boundary must bind
|
|
197
|
+
* to the exact model that the SDK can actually construct.
|
|
198
|
+
*/
|
|
199
|
+
function resolvePiModel(options) {
|
|
200
|
+
const provider = options.target.provider.trim();
|
|
201
|
+
const modelId = options.target.model.trim();
|
|
202
|
+
if (!provider || !modelId || provider.includes("/") || modelId.includes("\n")) throw new PiModelResolutionError("invalid-reference", "Pi model target must contain a provider and model id without a provider separator in either component");
|
|
203
|
+
if (options.modelRuntime.getModels(provider).length === 0) {
|
|
204
|
+
if (!options.modelRuntime.getProvider(provider)) throw new PiModelResolutionError("provider-unknown", `Pi provider is not installed: ${provider}`);
|
|
205
|
+
throw new PiModelResolutionError("model-unknown", `Pi provider ${provider} has no known model metadata; configure a declarative model before activation`);
|
|
206
|
+
}
|
|
207
|
+
const model = options.modelRuntime.getModel(provider, modelId);
|
|
208
|
+
if (!model) throw new PiModelResolutionError("model-unknown", `Pi model ${provider}/${modelId} is not present in the installed model catalog`);
|
|
209
|
+
return model;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Ensure metadata for an exact model id that is absent from the installed SDK
|
|
213
|
+
* catalog, then recompose the provider from models.json. Phase 1 only ships the
|
|
214
|
+
* verified DeepSeek declaration required by the management contract; all other
|
|
215
|
+
* unknown ids fail closed and must be supplied by a future explicit catalog path.
|
|
216
|
+
*/
|
|
217
|
+
async function ensurePiModel(options) {
|
|
218
|
+
try {
|
|
219
|
+
return resolvePiModel(options);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (!(error instanceof PiModelResolutionError) || error.resolutionCode !== "model-unknown") throw error;
|
|
222
|
+
}
|
|
223
|
+
const definition = knownPiDeclarativeModel(options.modelRuntime, options.target);
|
|
224
|
+
if (!definition) throw new PiModelResolutionError("model-unknown", `Pi model ${options.target.provider}/${options.target.model} is not present in the installed model catalog and has no approved declarative metadata`);
|
|
225
|
+
await mergePiProviderModelDeclaration({
|
|
226
|
+
filePath: options.modelsPath,
|
|
227
|
+
model: definition,
|
|
228
|
+
provider: options.target.provider
|
|
229
|
+
});
|
|
230
|
+
const refreshError = (await refreshPiModelCatalog(options.modelRuntime, {
|
|
231
|
+
allowNetwork: false,
|
|
232
|
+
provider: options.target.provider,
|
|
233
|
+
...options.signal ? { signal: options.signal } : {}
|
|
234
|
+
})).errors.get(options.target.provider);
|
|
235
|
+
if (refreshError) throw new PiModelResolutionError("model-unknown", `Pi provider ${options.target.provider} could not load declarative model metadata: ${refreshError.message}`);
|
|
236
|
+
return resolvePiModel(options);
|
|
237
|
+
}
|
|
238
|
+
/** Refresh installed SDK metadata, retaining the SDK's persisted declarative catalog. */
|
|
239
|
+
async function refreshPiModelCatalog(modelRuntime, options = {}) {
|
|
240
|
+
const providers = options.provider === void 0 ? void 0 : [options.provider];
|
|
241
|
+
const result = await modelRuntime.refresh({
|
|
242
|
+
allowNetwork: options.allowNetwork ?? false,
|
|
243
|
+
...providers ? { providers } : {},
|
|
244
|
+
...options.signal ? { signal: options.signal } : {}
|
|
245
|
+
});
|
|
246
|
+
const refreshedProviders = providers ?? modelRuntime.getRegisteredProviderIds();
|
|
247
|
+
return Object.freeze({
|
|
248
|
+
errors: result.errors,
|
|
249
|
+
refreshedProviders: Object.freeze([...refreshedProviders])
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
function knownPiDeclarativeModel(modelRuntime, target) {
|
|
253
|
+
if (target.provider !== "deepseek" || target.model !== "deepseek-flash") return void 0;
|
|
254
|
+
const source = modelRuntime.getModel("deepseek", "deepseek-v4-flash");
|
|
255
|
+
if (!source) return void 0;
|
|
256
|
+
return {
|
|
257
|
+
api: source.api,
|
|
258
|
+
baseUrl: source.baseUrl,
|
|
259
|
+
...source.compat ? { compat: source.compat } : {},
|
|
260
|
+
contextWindow: 1e6,
|
|
261
|
+
cost: {
|
|
262
|
+
cacheRead: .006,
|
|
263
|
+
cacheWrite: 0,
|
|
264
|
+
input: .3,
|
|
265
|
+
output: 1.2
|
|
266
|
+
},
|
|
267
|
+
id: target.model,
|
|
268
|
+
input: ["text", "image"],
|
|
269
|
+
maxTokens: 384e3,
|
|
270
|
+
name: "DeepSeek V4.1 Flash",
|
|
271
|
+
reasoning: true,
|
|
272
|
+
...source.samplingParams ? { samplingParams: source.samplingParams } : {},
|
|
273
|
+
...source.thinkingLevelMap ? { thinkingLevelMap: source.thinkingLevelMap } : {}
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region src/adapters/pi/model-management/pi-model-management-grant.ts
|
|
278
|
+
/** Read the Host's current grant and provider binding without exposing credentials. */
|
|
279
|
+
async function readPiModelManagementGrant(options, homeId, baseline) {
|
|
280
|
+
const env = options.envFilePath ? await loadMergedLocalEnvFile(options.envFilePath, options.environmentOverrides) : options.environmentOverrides;
|
|
281
|
+
const configuration = parseRivusModelManagementHomeConfig(env);
|
|
282
|
+
const keyFile = env.PI_API_KEY_FILE?.trim();
|
|
283
|
+
const inlineKey = env.PI_API_KEY?.trim();
|
|
284
|
+
if (keyFile && inlineKey) throw new Error("PI_API_KEY and PI_API_KEY_FILE cannot both be set.");
|
|
285
|
+
const credential = keyFile ? (await readFile(keyFile, "utf8")).trim() : inlineKey || (options.modelAuthPath ? await readPersistenceFile(options.modelAuthPath) ?? "runtime-auth" : "runtime-auth");
|
|
286
|
+
if (!credential) throw new Error("The model provider credential is empty.");
|
|
287
|
+
return {
|
|
288
|
+
bindingRevision: createToolInputDigest({
|
|
289
|
+
adapter: "pi-model-management-v1",
|
|
290
|
+
baseUrl: env.PI_BASE_URL?.trim() || "provider-default",
|
|
291
|
+
credential,
|
|
292
|
+
provider: baseline.provider,
|
|
293
|
+
configuredThinkingLevel: env.PI_THINKING_LEVEL?.trim() || null,
|
|
294
|
+
thinkingLevel: options.thinkingLevel
|
|
295
|
+
}, createSha256Digest),
|
|
296
|
+
budget: {
|
|
297
|
+
maxOutputTokens: 40960,
|
|
298
|
+
maxPaidRequests: 5,
|
|
299
|
+
recoveryReserveOutputTokens: 8192,
|
|
300
|
+
recoveryReservePaidRequests: 1
|
|
301
|
+
},
|
|
302
|
+
enabled: configuration.enabled,
|
|
303
|
+
endpointId: options.endpointId,
|
|
304
|
+
homeId,
|
|
305
|
+
operations: ["set", "rollback"],
|
|
306
|
+
ownerId: configuration.ownerOpenId ?? "disabled",
|
|
307
|
+
provider: baseline.provider,
|
|
308
|
+
revision: 1,
|
|
309
|
+
requireApproval: configuration.requireApproval,
|
|
310
|
+
tenantKey: configuration.tenantKey ?? "disabled",
|
|
311
|
+
timeoutMs: 3e5
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
//#endregion
|
|
315
|
+
//#region src/adapters/pi/model-management/pi-history-compatibility.ts
|
|
316
|
+
var PiHistoryCompatibilityError = class extends Error {
|
|
317
|
+
name = "PiHistoryCompatibilityError";
|
|
318
|
+
code = "model_change_history_incompatible";
|
|
319
|
+
messageIndex;
|
|
320
|
+
constructor(message, messageIndex) {
|
|
321
|
+
super(message);
|
|
322
|
+
this.messageIndex = messageIndex;
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
/**
|
|
326
|
+
* Check the structural parts of a persisted Pi transcript that providers need for
|
|
327
|
+
* the next request. The transcript is never returned or copied; only counts are
|
|
328
|
+
* exposed for diagnostics. This deliberately rejects orphaned tool calls/results
|
|
329
|
+
* instead of deleting or rewriting history during a model change.
|
|
330
|
+
*/
|
|
331
|
+
function inspectPiSessionHistory(sessionManager) {
|
|
332
|
+
const context = sessionManager.buildSessionContext();
|
|
333
|
+
const pendingToolCalls = /* @__PURE__ */ new Map();
|
|
334
|
+
let assistantThinkingBlocks = 0;
|
|
335
|
+
let assistantToolCalls = 0;
|
|
336
|
+
let imageBlocks = 0;
|
|
337
|
+
let toolResults = 0;
|
|
338
|
+
for (const [messageIndex, message] of context.messages.entries()) {
|
|
339
|
+
if (!isRecord$2(message) || typeof message.role !== "string") throw new PiHistoryCompatibilityError("Pi session history contains a malformed message", messageIndex);
|
|
340
|
+
if (message.role === "assistant") {
|
|
341
|
+
const content = message.content;
|
|
342
|
+
if (!Array.isArray(content)) throw new PiHistoryCompatibilityError("Pi assistant history message content must be an array", messageIndex);
|
|
343
|
+
for (const block of content) {
|
|
344
|
+
if (!isRecord$2(block) || typeof block.type !== "string") throw new PiHistoryCompatibilityError("Pi assistant history contains a malformed content block", messageIndex);
|
|
345
|
+
if (block.type === "thinking") {
|
|
346
|
+
if (typeof block.thinking !== "string") throw new PiHistoryCompatibilityError("Pi assistant thinking content is malformed", messageIndex);
|
|
347
|
+
assistantThinkingBlocks += 1;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (block.type === "toolCall") {
|
|
351
|
+
if (typeof block.id !== "string" || !block.id || typeof block.name !== "string" || !block.name) throw new PiHistoryCompatibilityError("Pi assistant tool call is malformed", messageIndex);
|
|
352
|
+
if (!isRecord$2(block.arguments)) throw new PiHistoryCompatibilityError("Pi assistant tool call arguments are malformed", messageIndex);
|
|
353
|
+
pendingToolCalls.set(block.id, block.name);
|
|
354
|
+
assistantToolCalls += 1;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (block.type === "text") {
|
|
358
|
+
if (typeof block.text !== "string") throw new PiHistoryCompatibilityError("Pi assistant text content is malformed", messageIndex);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (block.type === "image") {
|
|
362
|
+
if (typeof block.data !== "string" || typeof block.mimeType !== "string") throw new PiHistoryCompatibilityError("Pi assistant image content is malformed", messageIndex);
|
|
363
|
+
imageBlocks += 1;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
throw new PiHistoryCompatibilityError(`Pi assistant content type is unsupported: ${String(block.type)}`, messageIndex);
|
|
367
|
+
}
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (message.role === "toolResult") {
|
|
371
|
+
const content = message.content;
|
|
372
|
+
if (!Array.isArray(content)) throw new PiHistoryCompatibilityError("Pi tool result content must be an array", messageIndex);
|
|
373
|
+
if (typeof message.toolCallId !== "string" || !pendingToolCalls.has(message.toolCallId)) throw new PiHistoryCompatibilityError("Pi tool result has no preceding assistant tool call", messageIndex);
|
|
374
|
+
if (typeof message.toolName !== "string" || !message.toolName) throw new PiHistoryCompatibilityError("Pi tool result is missing its tool name", messageIndex);
|
|
375
|
+
if (pendingToolCalls.get(message.toolCallId) !== message.toolName) throw new PiHistoryCompatibilityError("Pi tool result tool name does not match its assistant tool call", messageIndex);
|
|
376
|
+
imageBlocks += countImageBlocks(content);
|
|
377
|
+
validatePlainContent(content, messageIndex, "tool result");
|
|
378
|
+
pendingToolCalls.delete(message.toolCallId);
|
|
379
|
+
toolResults += 1;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
if (message.role === "user") {
|
|
383
|
+
if (typeof message.content !== "string") {
|
|
384
|
+
if (!Array.isArray(message.content)) throw new PiHistoryCompatibilityError("Pi user history content must be text or an array", messageIndex);
|
|
385
|
+
imageBlocks += countImageBlocks(message.content);
|
|
386
|
+
validatePlainContent(message.content, messageIndex, "user");
|
|
387
|
+
}
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (message.role === "custom") {
|
|
391
|
+
if (typeof message.customType !== "string" || typeof message.display !== "boolean") throw new PiHistoryCompatibilityError("Pi custom history message metadata is malformed", messageIndex);
|
|
392
|
+
if (typeof message.content === "string") continue;
|
|
393
|
+
if (!Array.isArray(message.content)) throw new PiHistoryCompatibilityError("Pi custom history content must be text or an array", messageIndex);
|
|
394
|
+
imageBlocks += countImageBlocks(message.content);
|
|
395
|
+
validatePlainContent(message.content, messageIndex, "custom");
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (message.role === "bashExecution") {
|
|
399
|
+
if (typeof message.command !== "string" || typeof message.output !== "string" || typeof message.cancelled !== "boolean" || typeof message.truncated !== "boolean" || message.exitCode !== void 0 && message.exitCode !== null && typeof message.exitCode !== "number" || message.fullOutputPath !== void 0 && typeof message.fullOutputPath !== "string" || message.excludeFromContext !== void 0 && typeof message.excludeFromContext !== "boolean") throw new PiHistoryCompatibilityError("Pi bash history message is malformed", messageIndex);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (message.role === "branchSummary") {
|
|
403
|
+
if (typeof message.summary !== "string" || typeof message.fromId !== "string") throw new PiHistoryCompatibilityError("Pi branch summary history message is malformed", messageIndex);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (message.role === "compactionSummary") {
|
|
407
|
+
if (typeof message.summary !== "string" || typeof message.tokensBefore !== "number" || !Number.isFinite(message.tokensBefore) || message.tokensBefore < 0) throw new PiHistoryCompatibilityError("Pi compaction summary history message is malformed", messageIndex);
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
throw new PiHistoryCompatibilityError(`Pi history role is unsupported: ${String(message.role)}`, messageIndex);
|
|
411
|
+
}
|
|
412
|
+
if (pendingToolCalls.size > 0) throw new PiHistoryCompatibilityError("Pi session history contains an assistant tool call without a result", context.messages.length);
|
|
413
|
+
return Object.freeze({
|
|
414
|
+
assistantThinkingBlocks,
|
|
415
|
+
assistantToolCalls,
|
|
416
|
+
compatible: true,
|
|
417
|
+
imageBlocks,
|
|
418
|
+
messageCount: context.messages.length,
|
|
419
|
+
toolResults
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
function countImageBlocks(content) {
|
|
423
|
+
return content.filter((block) => isRecord$2(block) && block.type === "image").length;
|
|
424
|
+
}
|
|
425
|
+
function assertPiSessionHistoryCompatible(sessionManager) {
|
|
426
|
+
return inspectPiSessionHistory(sessionManager);
|
|
427
|
+
}
|
|
428
|
+
function validatePlainContent(content, messageIndex, role) {
|
|
429
|
+
for (const block of content) {
|
|
430
|
+
if (!isRecord$2(block) || typeof block.type !== "string") throw new PiHistoryCompatibilityError(`Pi ${role} history contains a malformed content block`, messageIndex);
|
|
431
|
+
if (block.type === "text") {
|
|
432
|
+
if (typeof block.text !== "string") throw new PiHistoryCompatibilityError(`Pi ${role} text content is malformed`, messageIndex);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (block.type === "image") {
|
|
436
|
+
if (typeof block.data !== "string" || typeof block.mimeType !== "string") throw new PiHistoryCompatibilityError(`Pi ${role} image content is malformed`, messageIndex);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
throw new PiHistoryCompatibilityError(`Pi ${role} content type is unsupported: ${block.type}`, messageIndex);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function isRecord$2(value) {
|
|
443
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
444
|
+
}
|
|
445
|
+
//#endregion
|
|
446
|
+
//#region src/adapters/pi/model-management/pi-model-call-deadline.ts
|
|
447
|
+
var PiModelCallDeadlineExceeded = class extends Error {
|
|
448
|
+
name = "PiModelCallDeadlineExceeded";
|
|
449
|
+
};
|
|
450
|
+
/** Race a provider or SDK promise against the Run deadline without retaining a late result. */
|
|
451
|
+
async function awaitPiModelCall(promise, options = {}) {
|
|
452
|
+
const completion = Promise.resolve(promise);
|
|
453
|
+
const signal = options.signal;
|
|
454
|
+
if (!signal) return completion;
|
|
455
|
+
let settled = false;
|
|
456
|
+
const observed = completion.then((value) => {
|
|
457
|
+
settled = true;
|
|
458
|
+
return value;
|
|
459
|
+
}, (error) => {
|
|
460
|
+
settled = true;
|
|
461
|
+
throw error;
|
|
462
|
+
});
|
|
463
|
+
let removeAbortListener;
|
|
464
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
465
|
+
const handleAbort = () => {
|
|
466
|
+
if (settled) return;
|
|
467
|
+
options.onAbort?.();
|
|
468
|
+
reject(options.isDeadlineExceeded?.() ? new PiModelCallDeadlineExceeded(options.deadlineMessage ?? "Pi model call deadline elapsed") : signal.reason instanceof Error ? signal.reason : new Error(options.abortMessage ?? "Pi model call aborted"));
|
|
469
|
+
};
|
|
470
|
+
if (options.isDeadlineExceeded?.() || signal.aborted) handleAbort();
|
|
471
|
+
else {
|
|
472
|
+
signal.addEventListener("abort", handleAbort, { once: true });
|
|
473
|
+
removeAbortListener = () => signal.removeEventListener("abort", handleAbort);
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
try {
|
|
477
|
+
const value = await Promise.race([observed, aborted]);
|
|
478
|
+
if (options.isDeadlineExceeded?.()) throw new PiModelCallDeadlineExceeded(options.deadlineMessage ?? "Pi model call deadline elapsed");
|
|
479
|
+
return value;
|
|
480
|
+
} finally {
|
|
481
|
+
removeAbortListener?.();
|
|
482
|
+
if (!settled) observed.catch(() => void 0);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
//#endregion
|
|
486
|
+
//#region src/adapters/pi/model-management/pi-model-probe.ts
|
|
487
|
+
const PI_MODEL_PROBE_TOOL_NAME = "rivus_model_management_probe";
|
|
488
|
+
const PI_MODEL_PROBE_TOOL_RESULT = "RIVUS_MODEL_PROBE_TOOL_OK";
|
|
489
|
+
const probeToolParameters = Type.Object({ probe: Type.String() });
|
|
490
|
+
var PiModelProbeError = class extends Error {
|
|
491
|
+
name = "PiModelProbeError";
|
|
492
|
+
code;
|
|
493
|
+
outcome;
|
|
494
|
+
constructor(message, outcome, options, code = "model_change_probe_failed") {
|
|
495
|
+
super(message, options);
|
|
496
|
+
this.code = code;
|
|
497
|
+
this.outcome = outcome;
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
/**
|
|
501
|
+
* Exercise a candidate through the same Pi model runtime used by business runs.
|
|
502
|
+
* The probe builds an in-memory synthetic context, performs an actual streamed
|
|
503
|
+
* tool call, feeds the real fixed tool result into a second streamed request, and
|
|
504
|
+
* then sends a separate follow-up request. It never loads a persisted transcript
|
|
505
|
+
* or exposes business tools.
|
|
506
|
+
*/
|
|
507
|
+
function probePiModel(options) {
|
|
508
|
+
return Effect.gen(function* () {
|
|
509
|
+
const context = {
|
|
510
|
+
messages: [{
|
|
511
|
+
content: "This is an isolated compatibility probe. Call the fixed probe Tool exactly once, then repeat the exact Tool result in your response.",
|
|
512
|
+
role: "user",
|
|
513
|
+
timestamp: Date.now()
|
|
514
|
+
}],
|
|
515
|
+
systemPrompt: "You are running an isolated model compatibility probe. You must call the rivus_model_management_probe Tool exactly once with a short probe value. After receiving its result, repeat its exact result text in your response. Do not call any other Tool.",
|
|
516
|
+
tools: [{
|
|
517
|
+
description: "A fixed no-side-effect probe Tool used only for model compatibility checks.",
|
|
518
|
+
name: PI_MODEL_PROBE_TOOL_NAME,
|
|
519
|
+
parameters: probeToolParameters
|
|
520
|
+
}]
|
|
521
|
+
};
|
|
522
|
+
const transcript = [];
|
|
523
|
+
const initial = yield* streamProbeTurn(options, context, "initial");
|
|
524
|
+
transcript.push(initial.turn);
|
|
525
|
+
const toolCall = findSingleProbeToolCall(initial.message);
|
|
526
|
+
if (!toolCall) return yield* Effect.fail(new PiModelProbeError("candidate did not request the fixed probe Tool in its first streamed response", "known"));
|
|
527
|
+
if (typeof toolCall.arguments.probe !== "string" || toolCall.arguments.probe.length === 0) return yield* Effect.fail(new PiModelProbeError("candidate supplied invalid arguments to the fixed probe Tool", "known"));
|
|
528
|
+
const toolResult = {
|
|
529
|
+
content: [{
|
|
530
|
+
text: `${PI_MODEL_PROBE_TOOL_RESULT}:${randomUUID()}`,
|
|
531
|
+
type: "text"
|
|
532
|
+
}],
|
|
533
|
+
details: { probe: true },
|
|
534
|
+
isError: false,
|
|
535
|
+
role: "toolResult",
|
|
536
|
+
timestamp: Date.now(),
|
|
537
|
+
toolCallId: toolCall.id,
|
|
538
|
+
toolName: PI_MODEL_PROBE_TOOL_NAME
|
|
539
|
+
};
|
|
540
|
+
context.messages.push(initial.message, toolResult);
|
|
541
|
+
const toolResultText = toolResult.content[0].text;
|
|
542
|
+
const afterTool = yield* streamProbeTurn(options, context, "tool-result");
|
|
543
|
+
transcript.push({
|
|
544
|
+
...afterTool.turn,
|
|
545
|
+
toolResult: {
|
|
546
|
+
content: toolResultText,
|
|
547
|
+
toolCallId: toolCall.id,
|
|
548
|
+
toolName: PI_MODEL_PROBE_TOOL_NAME
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
if (!readText(afterTool.message).includes(toolResultText)) return yield* Effect.fail(new PiModelProbeError("candidate did not consume the fixed probe Tool result in its next streamed response", "known"));
|
|
552
|
+
if (findToolCalls(afterTool.message).length > 0) return yield* Effect.fail(new PiModelProbeError("candidate requested an additional Tool during the probe", "known"));
|
|
553
|
+
context.messages.push(afterTool.message, {
|
|
554
|
+
content: "Follow up after the Tool roundtrip. Reply with RIVUS_MODEL_PROBE_FOLLOWUP_OK.",
|
|
555
|
+
role: "user",
|
|
556
|
+
timestamp: Date.now()
|
|
557
|
+
});
|
|
558
|
+
const followup = yield* streamProbeTurn(options, context, "follow-up");
|
|
559
|
+
transcript.push(followup.turn);
|
|
560
|
+
if (findToolCalls(followup.message).length > 0) return yield* Effect.fail(new PiModelProbeError("candidate requested a Tool during the probe follow-up", "known"));
|
|
561
|
+
const followupText = readText(followup.message);
|
|
562
|
+
if (!followupText.includes("RIVUS_MODEL_PROBE_FOLLOWUP_OK")) return yield* Effect.fail(new PiModelProbeError("candidate did not produce the fixed probe follow-up response", "known"));
|
|
563
|
+
const assistantThinkingBlocks = transcript.reduce((count, turn) => count + turn.assistant.content.filter((content) => isRecord$1(content) && content.type === "thinking").length, 0);
|
|
564
|
+
const outputTokens = transcript.reduce((sum, turn) => sum + readOutputTokens(turn.assistant.usage), 0);
|
|
565
|
+
return Object.freeze({
|
|
566
|
+
assistantThinkingBlocks,
|
|
567
|
+
followupText,
|
|
568
|
+
model: Object.freeze({
|
|
569
|
+
id: options.model.id,
|
|
570
|
+
provider: options.model.provider
|
|
571
|
+
}),
|
|
572
|
+
outputTokens,
|
|
573
|
+
passed: true,
|
|
574
|
+
thinkingLevel: options.thinkingLevel,
|
|
575
|
+
toolResultText,
|
|
576
|
+
toolCallId: toolCall.id,
|
|
577
|
+
toolName: PI_MODEL_PROBE_TOOL_NAME,
|
|
578
|
+
transcript: Object.freeze(transcript)
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
function streamProbeTurn(options, context, kind) {
|
|
583
|
+
return streamPaidProbeCall({
|
|
584
|
+
budget: options.budget,
|
|
585
|
+
context,
|
|
586
|
+
description: `Pi model probe ${kind}`,
|
|
587
|
+
kind: "validation",
|
|
588
|
+
model: options.model,
|
|
589
|
+
modelRuntime: options.modelRuntime,
|
|
590
|
+
...options.signal ? { signal: options.signal } : {},
|
|
591
|
+
thinkingLevel: options.thinkingLevel,
|
|
592
|
+
validate: (message) => assertProbeStopReason(message, kind)
|
|
593
|
+
}).pipe(Effect.map(({ message, reservation }) => ({
|
|
594
|
+
message,
|
|
595
|
+
turn: {
|
|
596
|
+
assistant: snapshotAssistant(message),
|
|
597
|
+
kind,
|
|
598
|
+
reservationId: reservation.id
|
|
599
|
+
}
|
|
600
|
+
})));
|
|
601
|
+
}
|
|
602
|
+
/** Run one bounded, synthetic request for activation/recovery canaries. */
|
|
603
|
+
function runPiModelCanary(options) {
|
|
604
|
+
return streamPaidProbeCall({
|
|
605
|
+
budget: options.budget,
|
|
606
|
+
context: {
|
|
607
|
+
messages: [{
|
|
608
|
+
content: "Reply with the exact text RIVUS_MODEL_CANARY_OK.",
|
|
609
|
+
role: "user",
|
|
610
|
+
timestamp: Date.now()
|
|
611
|
+
}],
|
|
612
|
+
systemPrompt: "This is an isolated model canary. Reply with RIVUS_MODEL_CANARY_OK and do not call tools."
|
|
613
|
+
},
|
|
614
|
+
description: `Pi model ${options.kind} canary`,
|
|
615
|
+
kind: options.kind,
|
|
616
|
+
model: options.model,
|
|
617
|
+
modelRuntime: options.modelRuntime,
|
|
618
|
+
...options.signal ? { signal: options.signal } : {},
|
|
619
|
+
thinkingLevel: options.thinkingLevel,
|
|
620
|
+
validate: (message) => {
|
|
621
|
+
assertCanaryStopReason(message);
|
|
622
|
+
if (!readText(message).includes("RIVUS_MODEL_CANARY_OK")) throw new PiModelProbeError("model canary did not produce the fixed response", "known");
|
|
623
|
+
}
|
|
624
|
+
}).pipe(Effect.map(({ message, outputTokens }) => Object.freeze({
|
|
625
|
+
assistant: snapshotAssistant(message),
|
|
626
|
+
outputTokens
|
|
627
|
+
})));
|
|
628
|
+
}
|
|
629
|
+
function streamPaidProbeCall(options) {
|
|
630
|
+
return Effect.gen(function* () {
|
|
631
|
+
const reservation = yield* options.budget.reservePaidCall({ kind: options.kind });
|
|
632
|
+
const callSignal = yield* Effect.sync(() => createPaidCallSignal(options.signal, options.budget.deadlineAt));
|
|
633
|
+
let settled = false;
|
|
634
|
+
const settle = (input) => options.budget.settlePaidCall({
|
|
635
|
+
...input,
|
|
636
|
+
reservation
|
|
637
|
+
}).pipe(Effect.tap(() => Effect.sync(() => {
|
|
638
|
+
settled = true;
|
|
639
|
+
})));
|
|
640
|
+
return yield* Effect.gen(function* () {
|
|
641
|
+
if (callSignal.expired) {
|
|
642
|
+
yield* settle({
|
|
643
|
+
outcome: "known",
|
|
644
|
+
outputTokens: 0
|
|
645
|
+
});
|
|
646
|
+
return yield* Effect.fail(new PiModelProbeError("model change deadline elapsed before the provider request", "known", void 0, "model_change_deadline_exceeded"));
|
|
647
|
+
}
|
|
648
|
+
const message = yield* Effect.tryPromise({
|
|
649
|
+
try: async () => {
|
|
650
|
+
const stream = options.modelRuntime.streamSimple(options.model, options.context, {
|
|
651
|
+
signal: callSignal.signal,
|
|
652
|
+
maxTokens: reservation.maxOutputTokens,
|
|
653
|
+
maxRetries: 0,
|
|
654
|
+
maxRetryDelayMs: 0,
|
|
655
|
+
...options.thinkingLevel === "off" ? {} : { reasoning: options.thinkingLevel }
|
|
656
|
+
});
|
|
657
|
+
return await awaitPiModelCall((async () => {
|
|
658
|
+
for await (const _event of stream) callSignal.signal.throwIfAborted();
|
|
659
|
+
callSignal.signal.throwIfAborted();
|
|
660
|
+
return await stream.result();
|
|
661
|
+
})(), {
|
|
662
|
+
abortMessage: "model probe aborted",
|
|
663
|
+
deadlineMessage: "model change deadline elapsed during provider streaming",
|
|
664
|
+
isDeadlineExceeded: callSignal.isDeadlineExceeded,
|
|
665
|
+
signal: callSignal.signal
|
|
666
|
+
});
|
|
667
|
+
},
|
|
668
|
+
catch: (error) => error
|
|
669
|
+
}).pipe(Effect.catchAll((error) => {
|
|
670
|
+
const deadlineExceeded = callSignal.isDeadlineExceeded() || error instanceof PiModelCallDeadlineExceeded;
|
|
671
|
+
const probeError = new PiModelProbeError(deadlineExceeded ? `${options.description} stream exceeded the model change deadline` : `${options.description} stream failed: ${error instanceof Error ? error.message : String(error)}`, "unknown", { cause: error }, deadlineExceeded ? "model_change_deadline_exceeded" : "model_change_probe_failed");
|
|
672
|
+
return settle({
|
|
673
|
+
outcome: "unknown",
|
|
674
|
+
outputTokens: reservation.maxOutputTokens
|
|
675
|
+
}).pipe(Effect.zipRight(Effect.fail(probeError)));
|
|
676
|
+
}));
|
|
677
|
+
const outputTokens = readOutputTokens(message.usage);
|
|
678
|
+
yield* settle({
|
|
679
|
+
outcome: "known",
|
|
680
|
+
outputTokens
|
|
681
|
+
});
|
|
682
|
+
yield* Effect.try({
|
|
683
|
+
try: () => options.validate(message),
|
|
684
|
+
catch: (error) => error
|
|
685
|
+
});
|
|
686
|
+
return {
|
|
687
|
+
message,
|
|
688
|
+
outputTokens,
|
|
689
|
+
reservation
|
|
690
|
+
};
|
|
691
|
+
}).pipe(Effect.ensuring(Effect.uninterruptible(Effect.suspend(() => settled ? Effect.void : settle({
|
|
692
|
+
outcome: "unknown",
|
|
693
|
+
outputTokens: reservation.maxOutputTokens
|
|
694
|
+
})).pipe(Effect.orDie))), Effect.ensuring(Effect.sync(callSignal.dispose)));
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
function createPaidCallSignal(parent, deadlineAt) {
|
|
698
|
+
const controller = new AbortController();
|
|
699
|
+
const deadlineMs = Date.parse(deadlineAt);
|
|
700
|
+
const remainingMs = deadlineMs - Date.now();
|
|
701
|
+
let deadlineExceeded = !Number.isFinite(deadlineMs) || remainingMs <= 0;
|
|
702
|
+
let timer;
|
|
703
|
+
const onParentAbort = () => controller.abort(parent?.reason);
|
|
704
|
+
if (parent) if (parent.aborted) controller.abort(parent.reason);
|
|
705
|
+
else parent.addEventListener("abort", onParentAbort, { once: true });
|
|
706
|
+
if (remainingMs > 0 && !controller.signal.aborted) timer = setTimeout(() => {
|
|
707
|
+
deadlineExceeded = true;
|
|
708
|
+
controller.abort(/* @__PURE__ */ new Error("model change deadline elapsed"));
|
|
709
|
+
}, Math.min(remainingMs, 2147483647));
|
|
710
|
+
return {
|
|
711
|
+
expired: deadlineExceeded,
|
|
712
|
+
isDeadlineExceeded: () => deadlineExceeded || Number.isFinite(deadlineMs) && Date.now() >= deadlineMs,
|
|
713
|
+
signal: controller.signal,
|
|
714
|
+
dispose: () => {
|
|
715
|
+
if (timer) clearTimeout(timer);
|
|
716
|
+
parent?.removeEventListener("abort", onParentAbort);
|
|
717
|
+
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("model probe cancelled"));
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
function assertProbeStopReason(message, kind) {
|
|
722
|
+
if (message.stopReason === "error" || message.stopReason === "aborted") throw new PiModelProbeError(`candidate streamed an ${message.stopReason} response: ${message.errorMessage ?? "unknown provider error"}`, "known");
|
|
723
|
+
if (message.stopReason === "length" || message.stopReason === "deferred") throw new PiModelProbeError(`candidate response was truncated or deferred during ${kind}`, "known");
|
|
724
|
+
if (kind !== "initial" && message.stopReason !== "stop") throw new PiModelProbeError(`candidate used unsupported stop reason ${message.stopReason} during ${kind}`, "known");
|
|
725
|
+
}
|
|
726
|
+
function assertCanaryStopReason(message) {
|
|
727
|
+
if (message.stopReason === "error" || message.stopReason === "aborted") throw new PiModelProbeError(`model canary streamed an ${message.stopReason} response: ${message.errorMessage ?? "unknown provider error"}`, "known");
|
|
728
|
+
if (message.stopReason === "length" || message.stopReason === "deferred" || message.stopReason !== "stop") throw new PiModelProbeError(`model canary used unsupported stop reason ${message.stopReason}`, "known");
|
|
729
|
+
}
|
|
730
|
+
function findSingleProbeToolCall(message) {
|
|
731
|
+
const calls = findToolCalls(message);
|
|
732
|
+
if (calls.length !== 1 || calls[0]?.name !== "rivus_model_management_probe") return void 0;
|
|
733
|
+
const call = calls[0];
|
|
734
|
+
return {
|
|
735
|
+
arguments: call.arguments,
|
|
736
|
+
id: call.id
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
function findToolCalls(message) {
|
|
740
|
+
return message.content.flatMap((content) => {
|
|
741
|
+
if (!isRecord$1(content) || content.type !== "toolCall") return [];
|
|
742
|
+
return [{
|
|
743
|
+
arguments: isRecord$1(content.arguments) ? content.arguments : {},
|
|
744
|
+
id: typeof content.id === "string" ? content.id : "",
|
|
745
|
+
name: typeof content.name === "string" ? content.name : ""
|
|
746
|
+
}];
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
function readText(message) {
|
|
750
|
+
return message.content.filter((content) => isRecord$1(content) && content.type === "text" && typeof content.text === "string").map((content) => content.text).join("");
|
|
751
|
+
}
|
|
752
|
+
function snapshotAssistant(message) {
|
|
753
|
+
return Object.freeze({
|
|
754
|
+
content: Object.freeze(message.content.map((content) => cloneSerializable(content))),
|
|
755
|
+
model: message.model,
|
|
756
|
+
provider: message.provider,
|
|
757
|
+
stopReason: message.stopReason,
|
|
758
|
+
usage: Object.freeze(cloneSerializable(message.usage))
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
function readOutputTokens(usage) {
|
|
762
|
+
if (isRecord$1(usage) && typeof usage.output === "number" && Number.isSafeInteger(usage.output) && usage.output >= 0) return usage.output;
|
|
763
|
+
return 0;
|
|
764
|
+
}
|
|
765
|
+
function cloneSerializable(value) {
|
|
766
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
767
|
+
if (Array.isArray(value)) return value.map(cloneSerializable);
|
|
768
|
+
if (!isRecord$1(value)) return void 0;
|
|
769
|
+
return Object.fromEntries(Object.entries(value).filter(([, nested]) => typeof nested !== "function" && nested !== void 0).map(([key, nested]) => [key, cloneSerializable(nested)]));
|
|
770
|
+
}
|
|
771
|
+
function isRecord$1(value) {
|
|
772
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
773
|
+
}
|
|
774
|
+
//#endregion
|
|
775
|
+
//#region src/adapters/pi/model-management/pi-model-runtime.ts
|
|
776
|
+
var PiModelRuntimeMutationUnknownError = class extends Error {
|
|
777
|
+
name = "PiModelRuntimeMutationUnknownError";
|
|
778
|
+
outcome = "unknown";
|
|
779
|
+
constructor(message, options) {
|
|
780
|
+
super(message, options);
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
var PiModelTargetCompatibilityError = class extends Error {
|
|
784
|
+
name = "PiModelTargetCompatibilityError";
|
|
785
|
+
code = "model_change_target_unsupported";
|
|
786
|
+
};
|
|
787
|
+
/**
|
|
788
|
+
* Adapter around Pi's live AgentSession instances.
|
|
789
|
+
*
|
|
790
|
+
* The application owns the safe Run boundary. Once that boundary is held, this
|
|
791
|
+
* adapter waits for every cached session, validates its persisted context, and
|
|
792
|
+
* calls AgentSession.setModel so the session manager and conversation history
|
|
793
|
+
* survive the switch. Disposing cached sessions would silently create a new
|
|
794
|
+
* transcript on the next Run and is therefore intentionally unsupported here.
|
|
795
|
+
*/
|
|
796
|
+
function createPiModelRuntime(options) {
|
|
797
|
+
const registries = new Set(options.sessionRegistries ?? []);
|
|
798
|
+
const unsettledMutations = /* @__PURE__ */ new Set();
|
|
799
|
+
let active = options.initial;
|
|
800
|
+
let selectionInitializationStarted = options.initial !== void 0;
|
|
801
|
+
const resolveCandidate = async (context, operation) => {
|
|
802
|
+
assertOperationActive(operation);
|
|
803
|
+
const model = options.modelRuntime.getModel(context.target.provider, context.target.model);
|
|
804
|
+
const resolved = model ? model : options.modelCatalogPath ? await ensurePiModel({
|
|
805
|
+
modelRuntime: options.modelRuntime,
|
|
806
|
+
modelsPath: options.modelCatalogPath,
|
|
807
|
+
target: context.target,
|
|
808
|
+
...operation?.signal ? { signal: operation.signal } : {}
|
|
809
|
+
}) : resolvePiModel({
|
|
810
|
+
modelRuntime: options.modelRuntime,
|
|
811
|
+
target: context.target
|
|
812
|
+
});
|
|
813
|
+
assertOperationActive(operation);
|
|
814
|
+
return Object.freeze({
|
|
815
|
+
binding: Object.freeze({
|
|
816
|
+
bindingRevision: context.bindingRevision,
|
|
817
|
+
model: resolved.id,
|
|
818
|
+
provider: resolved.provider
|
|
819
|
+
}),
|
|
820
|
+
model: resolved
|
|
821
|
+
});
|
|
822
|
+
};
|
|
823
|
+
const listSessions = async () => {
|
|
824
|
+
const handles = await Promise.all([...registries].map((registry) => registry.list()));
|
|
825
|
+
const unique = [];
|
|
826
|
+
const seen = /* @__PURE__ */ new Set();
|
|
827
|
+
for (const handle of handles.flat()) {
|
|
828
|
+
if (seen.has(handle)) continue;
|
|
829
|
+
seen.add(handle);
|
|
830
|
+
unique.push(handle);
|
|
831
|
+
}
|
|
832
|
+
return unique;
|
|
833
|
+
};
|
|
834
|
+
const waitForIdleAndCheckHistory = async (model, operation) => {
|
|
835
|
+
assertOperationActive(operation);
|
|
836
|
+
const sessions = await listSessions();
|
|
837
|
+
let abortPromise;
|
|
838
|
+
const abortSessions = () => {
|
|
839
|
+
if (abortPromise) return abortPromise;
|
|
840
|
+
abortPromise = Promise.all(sessions.map((handle) => Promise.resolve().then(() => handle.session.abort?.()))).then(() => void 0);
|
|
841
|
+
return abortPromise;
|
|
842
|
+
};
|
|
843
|
+
const onAbort = () => {
|
|
844
|
+
abortSessions();
|
|
845
|
+
};
|
|
846
|
+
const signal = operation?.signal;
|
|
847
|
+
if (signal) signal.addEventListener("abort", onAbort, { once: true });
|
|
848
|
+
try {
|
|
849
|
+
if (signal?.aborted) abortSessions();
|
|
850
|
+
await awaitPiModelCall(Promise.all(sessions.map((handle) => Promise.resolve().then(() => handle.session.waitForIdle?.()))), {
|
|
851
|
+
onAbort: abortSessions,
|
|
852
|
+
...signal ? { signal } : {}
|
|
853
|
+
});
|
|
854
|
+
assertOperationActive(operation);
|
|
855
|
+
for (const handle of sessions) {
|
|
856
|
+
assertOperationActive(operation);
|
|
857
|
+
const manager = handle.session.sessionManager;
|
|
858
|
+
if (!manager) continue;
|
|
859
|
+
const context = manager.buildSessionContext();
|
|
860
|
+
const history = assertPiSessionHistoryCompatible(manager);
|
|
861
|
+
if (model) {
|
|
862
|
+
if (history.imageBlocks > 0 && !model.input.includes("image")) throw new PiModelTargetCompatibilityError(`Pi model ${model.provider}/${model.id} does not support ${history.imageBlocks} image history block(s)`);
|
|
863
|
+
if (hasTextInput(context.messages) && !model.input.includes("text")) throw new PiModelTargetCompatibilityError(`Pi model ${model.provider}/${model.id} does not support text history input`);
|
|
864
|
+
const estimatedTokens = context.messages.reduce((total, message) => total + estimateTokens(message), 0);
|
|
865
|
+
if (!Number.isFinite(model.contextWindow) || estimatedTokens > model.contextWindow) throw new PiModelTargetCompatibilityError(`Pi model ${model.provider}/${model.id} context window ${model.contextWindow} is smaller than the estimated ${estimatedTokens}-token session history`);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
assertOperationActive(operation);
|
|
869
|
+
} finally {
|
|
870
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
871
|
+
if (abortPromise) abortPromise.catch(() => void 0);
|
|
872
|
+
}
|
|
873
|
+
return sessions;
|
|
874
|
+
};
|
|
875
|
+
const setSessionsModel = async (sessions, model, thinkingLevel, operation) => {
|
|
876
|
+
assertNoUnsettledMutations();
|
|
877
|
+
for (const handle of sessions) {
|
|
878
|
+
assertOperationActive(operation);
|
|
879
|
+
if (!handle.session.setModel) throw new Error("Pi session does not expose live setModel; refusing to discard its history");
|
|
880
|
+
await awaitModelMutation(Promise.resolve().then(() => {
|
|
881
|
+
assertOperationActive(operation);
|
|
882
|
+
return handle.session.setModel(model);
|
|
883
|
+
}), operation);
|
|
884
|
+
assertOperationActive(operation);
|
|
885
|
+
handle.session.setThinkingLevel?.(thinkingLevel);
|
|
886
|
+
assertOperationActive(operation);
|
|
887
|
+
await awaitModelMutation(Promise.resolve().then(() => {
|
|
888
|
+
assertOperationActive(operation);
|
|
889
|
+
return handle.refreshResources?.();
|
|
890
|
+
}), operation);
|
|
891
|
+
assertOperationActive(operation);
|
|
892
|
+
await awaitModelMutation(Promise.resolve().then(() => {
|
|
893
|
+
assertOperationActive(operation);
|
|
894
|
+
return handle.session.reload?.();
|
|
895
|
+
}), operation);
|
|
896
|
+
assertOperationActive(operation);
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
const trackUnsettledMutation = (promise) => {
|
|
900
|
+
const tracked = promise.then(() => void 0, () => void 0);
|
|
901
|
+
unsettledMutations.add(tracked);
|
|
902
|
+
tracked.then(() => unsettledMutations.delete(tracked), () => unsettledMutations.delete(tracked));
|
|
903
|
+
};
|
|
904
|
+
const awaitModelMutation = async (promise, operation) => {
|
|
905
|
+
let timedOut = false;
|
|
906
|
+
const completion = Promise.resolve(promise);
|
|
907
|
+
try {
|
|
908
|
+
if (!operation?.signal) return completion;
|
|
909
|
+
return await awaitPiModelCall(completion, {
|
|
910
|
+
onAbort: () => {
|
|
911
|
+
timedOut = true;
|
|
912
|
+
trackUnsettledMutation(completion);
|
|
913
|
+
},
|
|
914
|
+
signal: operation.signal
|
|
915
|
+
});
|
|
916
|
+
} catch (error) {
|
|
917
|
+
if (timedOut) throw new PiModelRuntimeMutationUnknownError("Pi model session mutation did not settle before the model change deadline", { cause: error });
|
|
918
|
+
throw error;
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
const assertNoUnsettledMutations = () => {
|
|
922
|
+
if (unsettledMutations.size > 0) throw new PiModelRuntimeMutationUnknownError("A previous Pi model session mutation is still settling; refusing a concurrent mutation");
|
|
923
|
+
};
|
|
924
|
+
const createProbeBudget = (budget) => {
|
|
925
|
+
const settlements = /* @__PURE__ */ new Map();
|
|
926
|
+
return {
|
|
927
|
+
deadlineAt: budget.deadlineAt,
|
|
928
|
+
reservePaidCall: ({ kind, maxOutputTokens }) => budget.reservePaidCall({
|
|
929
|
+
kind,
|
|
930
|
+
...maxOutputTokens === void 0 ? {} : { maxOutputTokens }
|
|
931
|
+
}).pipe(Effect.tap((result) => Effect.sync(() => settlements.set(result.reservation.id, result.settle))), Effect.map((result) => result.reservation)),
|
|
932
|
+
settlePaidCall: ({ outcome, outputTokens, reservation }) => Effect.suspend(() => {
|
|
933
|
+
const settle = settlements.get(reservation.id);
|
|
934
|
+
if (!settle) return Effect.fail(/* @__PURE__ */ new Error(`Pi probe attempted to settle unknown budget reservation: ${reservation.id}`));
|
|
935
|
+
return settle({
|
|
936
|
+
outcome,
|
|
937
|
+
outputTokens
|
|
938
|
+
}).pipe(Effect.tap(() => Effect.sync(() => settlements.delete(reservation.id))), Effect.asVoid);
|
|
939
|
+
})
|
|
940
|
+
};
|
|
941
|
+
};
|
|
942
|
+
const runCanary = (kind, model, budget, operation) => runPiModelCanary({
|
|
943
|
+
budget: createProbeBudget(budget),
|
|
944
|
+
kind,
|
|
945
|
+
model,
|
|
946
|
+
modelRuntime: options.modelRuntime,
|
|
947
|
+
signal: operation.signal,
|
|
948
|
+
thinkingLevel: currentThinkingLevel()
|
|
949
|
+
});
|
|
950
|
+
return {
|
|
951
|
+
activate: (input) => Effect.gen(function* () {
|
|
952
|
+
const operation = yield* Effect.sync(() => createOperationSignal(input.budget.deadlineAt));
|
|
953
|
+
return yield* Effect.gen(function* () {
|
|
954
|
+
yield* Effect.try({
|
|
955
|
+
try: assertNoUnsettledMutations,
|
|
956
|
+
catch: (error) => error
|
|
957
|
+
});
|
|
958
|
+
const candidate = yield* Effect.tryPromise({
|
|
959
|
+
try: () => resolveCandidate(input.context, operation),
|
|
960
|
+
catch: (error) => error
|
|
961
|
+
});
|
|
962
|
+
if (!sameTarget$1(candidate.binding, input.validation.model)) return yield* Effect.fail(/* @__PURE__ */ new Error("Pi activation candidate does not match validated model"));
|
|
963
|
+
const canary = yield* runCanary("activation", candidate.model, input.budget, operation);
|
|
964
|
+
const sessions = yield* Effect.tryPromise({
|
|
965
|
+
try: () => waitForIdleAndCheckHistory(candidate.model, operation),
|
|
966
|
+
catch: (error) => error
|
|
967
|
+
});
|
|
968
|
+
if (input.beforeApply) yield* input.beforeApply();
|
|
969
|
+
yield* Effect.tryPromise({
|
|
970
|
+
try: () => setSessionsModel(sessions, candidate.model, currentThinkingLevel(), operation),
|
|
971
|
+
catch: (error) => error
|
|
972
|
+
});
|
|
973
|
+
active = {
|
|
974
|
+
binding: candidate.binding,
|
|
975
|
+
model: candidate.model,
|
|
976
|
+
thinkingLevel: currentThinkingLevel()
|
|
977
|
+
};
|
|
978
|
+
return Object.freeze({
|
|
979
|
+
evidence: Object.freeze({
|
|
980
|
+
canary: canary.assistant,
|
|
981
|
+
kind: "pi-model-activation-canary-v1",
|
|
982
|
+
model: candidate.binding,
|
|
983
|
+
sessionCount: sessions.length
|
|
984
|
+
}),
|
|
985
|
+
outputTokens: canary.outputTokens,
|
|
986
|
+
switched: true
|
|
987
|
+
});
|
|
988
|
+
}).pipe(Effect.ensuring(Effect.sync(operation.dispose)));
|
|
989
|
+
}),
|
|
990
|
+
current: () => Effect.succeed(active?.binding),
|
|
991
|
+
drain: (context) => Effect.gen(function* () {
|
|
992
|
+
const operation = yield* Effect.sync(() => createOperationSignal(context.deadlineAt));
|
|
993
|
+
yield* Effect.tryPromise({
|
|
994
|
+
try: async () => {
|
|
995
|
+
try {
|
|
996
|
+
const candidate = await resolveCandidate(context, operation);
|
|
997
|
+
await waitForIdleAndCheckHistory(candidate.model, operation);
|
|
998
|
+
} finally {
|
|
999
|
+
operation.dispose();
|
|
1000
|
+
}
|
|
1001
|
+
},
|
|
1002
|
+
catch: (error) => error
|
|
1003
|
+
});
|
|
1004
|
+
}),
|
|
1005
|
+
refreshModelCatalog: (input = {}) => Effect.tryPromise({
|
|
1006
|
+
try: async () => {
|
|
1007
|
+
const operation = createOperationSignal(input.deadlineAt, input.signal);
|
|
1008
|
+
try {
|
|
1009
|
+
assertOperationActive({
|
|
1010
|
+
...input.deadlineAt === void 0 ? {} : { deadlineAt: input.deadlineAt },
|
|
1011
|
+
signal: operation.signal
|
|
1012
|
+
});
|
|
1013
|
+
const result = await options.modelRuntime.refresh({
|
|
1014
|
+
allowNetwork: input.allowNetwork ?? false,
|
|
1015
|
+
...input.provider ? { providers: [input.provider] } : {},
|
|
1016
|
+
signal: operation.signal
|
|
1017
|
+
});
|
|
1018
|
+
if (result.errors.size > 0) throw new Error([...result.errors.entries()].map(([provider, error]) => `${provider}: ${error.message}`).join("; "));
|
|
1019
|
+
assertOperationActive({
|
|
1020
|
+
...input.deadlineAt === void 0 ? {} : { deadlineAt: input.deadlineAt },
|
|
1021
|
+
signal: operation.signal
|
|
1022
|
+
});
|
|
1023
|
+
} finally {
|
|
1024
|
+
operation.dispose();
|
|
1025
|
+
}
|
|
1026
|
+
},
|
|
1027
|
+
catch: (error) => error
|
|
1028
|
+
}),
|
|
1029
|
+
registerSessionRegistry: (registry) => {
|
|
1030
|
+
registries.add(registry);
|
|
1031
|
+
return () => registries.delete(registry);
|
|
1032
|
+
},
|
|
1033
|
+
initializeSelection: (input) => Effect.tryPromise({
|
|
1034
|
+
try: async () => {
|
|
1035
|
+
if (selectionInitializationStarted || active !== void 0) throw new Error("Pi model runtime selection has already been initialized");
|
|
1036
|
+
if (registries.size > 0) throw new Error("Pi model runtime selection must be initialized before any Pi session registry is attached");
|
|
1037
|
+
selectionInitializationStarted = true;
|
|
1038
|
+
try {
|
|
1039
|
+
const context = {
|
|
1040
|
+
baseline: input.binding,
|
|
1041
|
+
bindingRevision: input.binding.bindingRevision,
|
|
1042
|
+
requestId: "startup",
|
|
1043
|
+
target: input.binding
|
|
1044
|
+
};
|
|
1045
|
+
const candidate = await resolveCandidate(context);
|
|
1046
|
+
active = {
|
|
1047
|
+
binding: candidate.binding,
|
|
1048
|
+
model: candidate.model,
|
|
1049
|
+
thinkingLevel: input.thinkingLevel
|
|
1050
|
+
};
|
|
1051
|
+
} catch (error) {
|
|
1052
|
+
selectionInitializationStarted = false;
|
|
1053
|
+
throw error;
|
|
1054
|
+
}
|
|
1055
|
+
},
|
|
1056
|
+
catch: (error) => error
|
|
1057
|
+
}),
|
|
1058
|
+
getSessionOptions: () => Object.freeze({
|
|
1059
|
+
...active?.model ? { model: active.model } : {},
|
|
1060
|
+
...active?.thinkingLevel ? { thinkingLevel: active.thinkingLevel } : {}
|
|
1061
|
+
}),
|
|
1062
|
+
releasePrevious: (input) => Effect.try({
|
|
1063
|
+
try: () => {
|
|
1064
|
+
assertBeforeDeadline$1(input.deadlineAt);
|
|
1065
|
+
},
|
|
1066
|
+
catch: (error) => error
|
|
1067
|
+
}),
|
|
1068
|
+
restore: (input) => Effect.gen(function* () {
|
|
1069
|
+
const baselineContext = {
|
|
1070
|
+
...input.context,
|
|
1071
|
+
target: input.context.baseline
|
|
1072
|
+
};
|
|
1073
|
+
const operation = yield* Effect.sync(() => createOperationSignal(input.budget.deadlineAt));
|
|
1074
|
+
return yield* Effect.gen(function* () {
|
|
1075
|
+
yield* Effect.try({
|
|
1076
|
+
try: assertNoUnsettledMutations,
|
|
1077
|
+
catch: (error) => error
|
|
1078
|
+
});
|
|
1079
|
+
const baseline = yield* Effect.tryPromise({
|
|
1080
|
+
try: () => resolveCandidate(baselineContext, operation),
|
|
1081
|
+
catch: (error) => error
|
|
1082
|
+
});
|
|
1083
|
+
const canary = yield* runCanary("restore", baseline.model, input.budget, operation);
|
|
1084
|
+
const sessions = yield* Effect.tryPromise({
|
|
1085
|
+
try: () => waitForIdleAndCheckHistory(baseline.model, operation),
|
|
1086
|
+
catch: (error) => error
|
|
1087
|
+
});
|
|
1088
|
+
yield* Effect.tryPromise({
|
|
1089
|
+
try: () => setSessionsModel(sessions, baseline.model, currentThinkingLevel(), operation),
|
|
1090
|
+
catch: (error) => error
|
|
1091
|
+
});
|
|
1092
|
+
active = {
|
|
1093
|
+
binding: baseline.binding,
|
|
1094
|
+
model: baseline.model,
|
|
1095
|
+
thinkingLevel: currentThinkingLevel()
|
|
1096
|
+
};
|
|
1097
|
+
return Object.freeze({
|
|
1098
|
+
evidence: Object.freeze({
|
|
1099
|
+
canary: canary.assistant,
|
|
1100
|
+
kind: "pi-model-restore-canary-v1",
|
|
1101
|
+
model: baseline.binding,
|
|
1102
|
+
reason: input.reason,
|
|
1103
|
+
sessionCount: sessions.length
|
|
1104
|
+
}),
|
|
1105
|
+
restored: true
|
|
1106
|
+
});
|
|
1107
|
+
}).pipe(Effect.ensuring(Effect.sync(operation.dispose)));
|
|
1108
|
+
}),
|
|
1109
|
+
validate: (input) => Effect.gen(function* () {
|
|
1110
|
+
const operation = yield* Effect.sync(() => createOperationSignal(input.budget.deadlineAt));
|
|
1111
|
+
return yield* Effect.gen(function* () {
|
|
1112
|
+
const candidate = yield* Effect.tryPromise({
|
|
1113
|
+
try: () => resolveCandidate(input.context, operation),
|
|
1114
|
+
catch: (error) => error
|
|
1115
|
+
});
|
|
1116
|
+
const probe = yield* probePiModel({
|
|
1117
|
+
budget: createProbeBudget(input.budget),
|
|
1118
|
+
model: candidate.model,
|
|
1119
|
+
modelRuntime: options.modelRuntime,
|
|
1120
|
+
signal: operation.signal,
|
|
1121
|
+
thinkingLevel: currentThinkingLevel()
|
|
1122
|
+
});
|
|
1123
|
+
return Object.freeze({
|
|
1124
|
+
evidence: Object.freeze({
|
|
1125
|
+
assistantThinkingBlocks: probe.assistantThinkingBlocks,
|
|
1126
|
+
followupText: probe.followupText,
|
|
1127
|
+
kind: "pi-model-probe-v1",
|
|
1128
|
+
model: probe.model,
|
|
1129
|
+
outputTokens: probe.outputTokens,
|
|
1130
|
+
toolCallId: probe.toolCallId,
|
|
1131
|
+
toolName: probe.toolName,
|
|
1132
|
+
transcript: probe.transcript,
|
|
1133
|
+
toolRoundtrip: true
|
|
1134
|
+
}),
|
|
1135
|
+
model: candidate.binding,
|
|
1136
|
+
outputTokens: probe.outputTokens,
|
|
1137
|
+
passed: true
|
|
1138
|
+
});
|
|
1139
|
+
}).pipe(Effect.ensuring(Effect.sync(operation.dispose)));
|
|
1140
|
+
})
|
|
1141
|
+
};
|
|
1142
|
+
function currentThinkingLevel() {
|
|
1143
|
+
return active?.thinkingLevel ?? "medium";
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
function sameTarget$1(left, right) {
|
|
1147
|
+
return left.bindingRevision === right.bindingRevision && left.provider === right.provider && left.model === right.model;
|
|
1148
|
+
}
|
|
1149
|
+
function hasTextInput(messages) {
|
|
1150
|
+
return messages.some((message) => {
|
|
1151
|
+
if (!isRecord(message) || typeof message.role !== "string") return false;
|
|
1152
|
+
if (message.role === "user" || message.role === "custom" || message.role === "toolResult") return hasTextContent(message.content);
|
|
1153
|
+
if (message.role === "assistant") return Array.isArray(message.content) ? message.content.some((block) => isRecord(block) && (block.type === "text" && typeof block.text === "string" || block.type === "thinking" || block.type === "toolCall")) : false;
|
|
1154
|
+
return message.role === "bashExecution" || message.role === "branchSummary" || message.role === "compactionSummary";
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
function hasTextContent(content) {
|
|
1158
|
+
if (typeof content === "string") return content.length > 0;
|
|
1159
|
+
return Array.isArray(content) && content.some((block) => isRecord(block) && block.type === "text" && typeof block.text === "string");
|
|
1160
|
+
}
|
|
1161
|
+
function isRecord(value) {
|
|
1162
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1163
|
+
}
|
|
1164
|
+
function createOperationSignal(deadlineAt, parent) {
|
|
1165
|
+
const controller = new AbortController();
|
|
1166
|
+
let timer;
|
|
1167
|
+
const onParentAbort = () => controller.abort(parent?.reason);
|
|
1168
|
+
if (parent) if (parent.aborted) onParentAbort();
|
|
1169
|
+
else parent.addEventListener("abort", onParentAbort, { once: true });
|
|
1170
|
+
if (deadlineAt !== void 0) {
|
|
1171
|
+
const remaining = Date.parse(deadlineAt) - Date.now();
|
|
1172
|
+
if (!Number.isFinite(remaining) || remaining <= 0) controller.abort(/* @__PURE__ */ new Error("Pi model management deadline elapsed"));
|
|
1173
|
+
else timer = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("Pi model management deadline elapsed")), remaining);
|
|
1174
|
+
}
|
|
1175
|
+
return {
|
|
1176
|
+
signal: controller.signal,
|
|
1177
|
+
dispose: () => {
|
|
1178
|
+
if (timer) clearTimeout(timer);
|
|
1179
|
+
if (parent) parent.removeEventListener("abort", onParentAbort);
|
|
1180
|
+
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("Pi model operation disposed"));
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
function assertBeforeDeadline$1(deadlineAt) {
|
|
1185
|
+
if (deadlineAt === void 0) return;
|
|
1186
|
+
const deadline = Date.parse(deadlineAt);
|
|
1187
|
+
if (!Number.isFinite(deadline) || Date.now() >= deadline) throw new Error("Pi model management deadline elapsed");
|
|
1188
|
+
}
|
|
1189
|
+
function assertOperationActive(operation) {
|
|
1190
|
+
assertBeforeDeadline$1(operation?.deadlineAt);
|
|
1191
|
+
if (operation?.signal?.aborted) {
|
|
1192
|
+
const reason = operation.signal.reason;
|
|
1193
|
+
throw reason instanceof Error ? reason : /* @__PURE__ */ new Error("Pi model operation aborted");
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
//#endregion
|
|
1197
|
+
//#region src/core/application/deployment/model/model-change-contracts.ts
|
|
1198
|
+
var ModelChangeRequestConflict = class extends Error {
|
|
1199
|
+
name = "ModelChangeRequestConflict";
|
|
1200
|
+
};
|
|
1201
|
+
var ModelChangeRevisionConflict = class extends Error {
|
|
1202
|
+
name = "ModelChangeRevisionConflict";
|
|
1203
|
+
};
|
|
1204
|
+
var ModelChangeBusy = class extends Error {
|
|
1205
|
+
name = "ModelChangeBusy";
|
|
1206
|
+
};
|
|
1207
|
+
var ModelChangeNotFound = class extends Error {
|
|
1208
|
+
name = "ModelChangeNotFound";
|
|
1209
|
+
};
|
|
1210
|
+
var ModelChangeAuthorizationRejected = class extends Error {
|
|
1211
|
+
name = "ModelChangeAuthorizationRejected";
|
|
1212
|
+
};
|
|
1213
|
+
//#endregion
|
|
1214
|
+
//#region src/core/application/deployment/model/model-change-budget.ts
|
|
1215
|
+
var ModelChangeBudgetExceeded = class extends Error {
|
|
1216
|
+
name = "ModelChangeBudgetExceeded";
|
|
1217
|
+
};
|
|
1218
|
+
var ModelChangeBudgetCorrupted = class extends Error {
|
|
1219
|
+
name = "ModelChangeBudgetCorrupted";
|
|
1220
|
+
};
|
|
1221
|
+
function createModelChangeBudget(limits, now) {
|
|
1222
|
+
assertBudgetLimits(limits);
|
|
1223
|
+
assertBeforeDeadline(now, limits.deadlineAt);
|
|
1224
|
+
return Object.freeze({
|
|
1225
|
+
deadlineAt: limits.deadlineAt,
|
|
1226
|
+
identity: limits.identity,
|
|
1227
|
+
maxOutputTokens: limits.maxOutputTokens,
|
|
1228
|
+
maxPaidRequests: limits.maxPaidRequests,
|
|
1229
|
+
outputTokens: 0,
|
|
1230
|
+
paidRequests: 0,
|
|
1231
|
+
recoveryReserveOutputTokens: limits.recoveryReserveOutputTokens,
|
|
1232
|
+
recoveryReservePaidRequests: limits.recoveryReservePaidRequests,
|
|
1233
|
+
reservations: Object.freeze({}),
|
|
1234
|
+
reservedOutputTokens: 0,
|
|
1235
|
+
reservedPaidRequests: 0
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
function createAwaitingModelChangeBudget(deadlineAt) {
|
|
1239
|
+
if (!Number.isFinite(Date.parse(deadlineAt))) throw new ModelChangeBudgetCorrupted("invalid awaiting Model change deadline");
|
|
1240
|
+
return Object.freeze({
|
|
1241
|
+
deadlineAt,
|
|
1242
|
+
identity: "awaiting-approval",
|
|
1243
|
+
maxOutputTokens: 0,
|
|
1244
|
+
maxPaidRequests: 0,
|
|
1245
|
+
outputTokens: 0,
|
|
1246
|
+
paidRequests: 0,
|
|
1247
|
+
recoveryReserveOutputTokens: 0,
|
|
1248
|
+
recoveryReservePaidRequests: 0,
|
|
1249
|
+
reservations: Object.freeze({}),
|
|
1250
|
+
reservedOutputTokens: 0,
|
|
1251
|
+
reservedPaidRequests: 0
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
function carryModelChangeBudgetUsage(budget, usage) {
|
|
1255
|
+
validateModelChangeBudget(budget);
|
|
1256
|
+
if (!Number.isSafeInteger(usage.paidRequests) || usage.paidRequests < 0 || !Number.isSafeInteger(usage.outputTokens) || usage.outputTokens < 0 || usage.paidRequests > budget.maxPaidRequests || usage.outputTokens > budget.maxOutputTokens) throw new ModelChangeBudgetExceeded("existing Model change budget usage is invalid");
|
|
1257
|
+
return Object.freeze({
|
|
1258
|
+
...budget,
|
|
1259
|
+
outputTokens: usage.outputTokens,
|
|
1260
|
+
paidRequests: usage.paidRequests
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
function reserveModelChangeBudget(budget, input) {
|
|
1264
|
+
validateModelChangeBudget(budget);
|
|
1265
|
+
assertBeforeDeadline(input.at, budget.deadlineAt);
|
|
1266
|
+
if (budget.reservations[input.id]) throw new ModelChangeBudgetCorrupted(`duplicate budget reservation: ${input.id}`);
|
|
1267
|
+
if (budget.maxPaidRequests - budget.paidRequests - budget.reservedPaidRequests <= (input.kind === "restore" ? 0 : budget.recoveryReservePaidRequests)) throw new ModelChangeBudgetExceeded("model change paid request budget exhausted");
|
|
1268
|
+
const remaining = budget.maxOutputTokens - budget.outputTokens - budget.reservedOutputTokens;
|
|
1269
|
+
const outputRecoveryReserve = input.kind === "restore" ? 0 : budget.recoveryReserveOutputTokens;
|
|
1270
|
+
if (remaining <= outputRecoveryReserve) throw new ModelChangeBudgetExceeded("model change output token recovery reserve exhausted");
|
|
1271
|
+
const availableOutputTokens = remaining - outputRecoveryReserve;
|
|
1272
|
+
const maxOutputTokens = input.maxOutputTokens ?? availableOutputTokens;
|
|
1273
|
+
if (!Number.isSafeInteger(maxOutputTokens) || maxOutputTokens <= 0 || maxOutputTokens > availableOutputTokens) throw new ModelChangeBudgetExceeded("model change output token budget exhausted");
|
|
1274
|
+
const reservation = Object.freeze({
|
|
1275
|
+
id: input.id,
|
|
1276
|
+
kind: input.kind,
|
|
1277
|
+
maxOutputTokens,
|
|
1278
|
+
paidRequests: 1,
|
|
1279
|
+
reservedAt: input.at
|
|
1280
|
+
});
|
|
1281
|
+
return {
|
|
1282
|
+
budget: Object.freeze({
|
|
1283
|
+
...budget,
|
|
1284
|
+
reservations: Object.freeze({
|
|
1285
|
+
...budget.reservations,
|
|
1286
|
+
[input.id]: reservation
|
|
1287
|
+
}),
|
|
1288
|
+
reservedOutputTokens: budget.reservedOutputTokens + maxOutputTokens,
|
|
1289
|
+
reservedPaidRequests: budget.reservedPaidRequests + 1
|
|
1290
|
+
}),
|
|
1291
|
+
reservation
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
function settleModelChangeBudget(budget, input) {
|
|
1295
|
+
validateModelChangeBudget(budget);
|
|
1296
|
+
const reservation = budget.reservations[input.id];
|
|
1297
|
+
if (!reservation) throw new ModelChangeBudgetCorrupted(`unknown budget reservation: ${input.id}`);
|
|
1298
|
+
if (!Number.isSafeInteger(input.outputTokens) || input.outputTokens < 0) throw new ModelChangeBudgetCorrupted("model change output token usage must be a non-negative integer");
|
|
1299
|
+
if (input.outputTokens > reservation.maxOutputTokens) throw new ModelChangeBudgetExceeded("model change output token budget exceeded by paid call");
|
|
1300
|
+
const { [input.id]: _removed, ...remainingReservations } = budget.reservations;
|
|
1301
|
+
return Object.freeze({
|
|
1302
|
+
...budget,
|
|
1303
|
+
outputTokens: budget.outputTokens + input.outputTokens,
|
|
1304
|
+
paidRequests: budget.paidRequests + reservation.paidRequests,
|
|
1305
|
+
reservations: Object.freeze(remainingReservations),
|
|
1306
|
+
reservedOutputTokens: budget.reservedOutputTokens - reservation.maxOutputTokens,
|
|
1307
|
+
reservedPaidRequests: budget.reservedPaidRequests - reservation.paidRequests
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
/** Unknown provider results consume the reservation and must never be retried. */
|
|
1311
|
+
function settleUnknownModelChangeBudget(budget, input) {
|
|
1312
|
+
const reservation = budget.reservations[input.id];
|
|
1313
|
+
if (!reservation) throw new ModelChangeBudgetCorrupted(`unknown budget reservation: ${input.id}`);
|
|
1314
|
+
return settleModelChangeBudget(budget, {
|
|
1315
|
+
id: input.id,
|
|
1316
|
+
outputTokens: reservation.maxOutputTokens
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
function settleModelChangePaidCall(pending, input) {
|
|
1320
|
+
if (input.outcome === "unknown") return {
|
|
1321
|
+
...pending,
|
|
1322
|
+
budget: settleUnknownModelChangeBudget(pending.budget, { id: input.id }),
|
|
1323
|
+
phase: input.kind === "restore" ? "recovery-required" : "validation-unknown",
|
|
1324
|
+
unknownPaidCallId: input.id
|
|
1325
|
+
};
|
|
1326
|
+
const budget = settleModelChangeBudget(pending.budget, {
|
|
1327
|
+
id: input.id,
|
|
1328
|
+
outputTokens: input.outputTokens
|
|
1329
|
+
});
|
|
1330
|
+
const { unknownPaidCallId: _unknownPaidCallId, ...withoutUnknown } = pending;
|
|
1331
|
+
return {
|
|
1332
|
+
...withoutUnknown,
|
|
1333
|
+
budget
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
function validateModelChangeBudget(budget) {
|
|
1337
|
+
if (typeof budget.identity !== "string" || budget.identity.length === 0 || !Number.isSafeInteger(budget.maxPaidRequests) || budget.maxPaidRequests < 0 || !Number.isSafeInteger(budget.maxOutputTokens) || budget.maxOutputTokens < 0 || !Number.isSafeInteger(budget.recoveryReserveOutputTokens) || budget.recoveryReserveOutputTokens < 0 || budget.recoveryReserveOutputTokens > budget.maxOutputTokens || !Number.isSafeInteger(budget.recoveryReservePaidRequests) || budget.recoveryReservePaidRequests < 0 || budget.recoveryReservePaidRequests > budget.maxPaidRequests || !Number.isSafeInteger(budget.paidRequests) || budget.paidRequests < 0 || !Number.isSafeInteger(budget.outputTokens) || budget.outputTokens < 0 || !Number.isSafeInteger(budget.reservedPaidRequests) || budget.reservedPaidRequests < 0 || !Number.isSafeInteger(budget.reservedOutputTokens) || budget.reservedOutputTokens < 0 || budget.paidRequests + budget.reservedPaidRequests > budget.maxPaidRequests || budget.outputTokens + budget.reservedOutputTokens > budget.maxOutputTokens || !Number.isFinite(Date.parse(budget.deadlineAt))) throw new ModelChangeBudgetCorrupted("invalid model change budget");
|
|
1338
|
+
let paidReservations = 0;
|
|
1339
|
+
let outputReservations = 0;
|
|
1340
|
+
for (const reservation of Object.values(budget.reservations)) {
|
|
1341
|
+
if (reservation.paidRequests !== 1 || reservation.kind !== "activation" && reservation.kind !== "restore" && reservation.kind !== "validation" || !Number.isSafeInteger(reservation.maxOutputTokens) || reservation.maxOutputTokens <= 0 || !Number.isFinite(Date.parse(reservation.reservedAt))) throw new ModelChangeBudgetCorrupted("invalid model change budget reservation");
|
|
1342
|
+
paidReservations += reservation.paidRequests;
|
|
1343
|
+
outputReservations += reservation.maxOutputTokens;
|
|
1344
|
+
}
|
|
1345
|
+
if (paidReservations !== budget.reservedPaidRequests || outputReservations !== budget.reservedOutputTokens) throw new ModelChangeBudgetCorrupted("model change budget reservation totals do not match");
|
|
1346
|
+
}
|
|
1347
|
+
function assertBudgetLimits(limits) {
|
|
1348
|
+
if (typeof limits.identity !== "string" || limits.identity.length === 0 || !Number.isSafeInteger(limits.maxPaidRequests) || limits.maxPaidRequests < 0 || !Number.isSafeInteger(limits.maxOutputTokens) || limits.maxOutputTokens < 0 || !Number.isSafeInteger(limits.recoveryReserveOutputTokens) || limits.recoveryReserveOutputTokens < 0 || limits.recoveryReserveOutputTokens > limits.maxOutputTokens || !Number.isSafeInteger(limits.recoveryReservePaidRequests) || limits.recoveryReservePaidRequests < 0 || limits.recoveryReservePaidRequests > limits.maxPaidRequests || !Number.isFinite(Date.parse(limits.deadlineAt))) throw new ModelChangeBudgetCorrupted("invalid model change budget limits");
|
|
1349
|
+
}
|
|
1350
|
+
function assertBeforeDeadline(now, deadlineAt) {
|
|
1351
|
+
const nowMs = Date.parse(now);
|
|
1352
|
+
const deadlineMs = Date.parse(deadlineAt);
|
|
1353
|
+
if (!Number.isFinite(nowMs) || !Number.isFinite(deadlineMs)) throw new ModelChangeBudgetCorrupted("model change budget timestamps must be ISO timestamps");
|
|
1354
|
+
if (nowMs >= deadlineMs) throw new ModelChangeBudgetExceeded("model change budget deadline has passed");
|
|
1355
|
+
}
|
|
1356
|
+
//#endregion
|
|
1357
|
+
//#region src/adapters/outbound/persistence/deployment/model-state/model-state-codec.ts
|
|
1358
|
+
function encodeModelState(state) {
|
|
1359
|
+
validateModelState(state);
|
|
1360
|
+
const document = {
|
|
1361
|
+
state: structuredClone(state),
|
|
1362
|
+
version: 1
|
|
1363
|
+
};
|
|
1364
|
+
return `${JSON.stringify(document)}\n`;
|
|
1365
|
+
}
|
|
1366
|
+
function decodeModelState(raw) {
|
|
1367
|
+
let parsed;
|
|
1368
|
+
try {
|
|
1369
|
+
parsed = JSON.parse(raw);
|
|
1370
|
+
} catch {
|
|
1371
|
+
throw new Error("Model state is not valid JSON");
|
|
1372
|
+
}
|
|
1373
|
+
if (!isRecord$4(parsed) || parsed.version !== 1 || !isRecord$4(parsed.state)) throw new Error("Unsupported Model state document");
|
|
1374
|
+
return validateModelState(parsed.state);
|
|
1375
|
+
}
|
|
1376
|
+
function validateModelState(value) {
|
|
1377
|
+
if (!isRecord$4(value) || value.schemaVersion !== 1) throw new Error("invalid Model state schema version");
|
|
1378
|
+
if (typeof value.homeId !== "string" || value.homeId.length === 0 || typeof value.ownerId !== "string" || value.ownerId.length === 0 || !Number.isSafeInteger(value.revision) || value.revision < 0 || typeof value.updatedAt !== "string" || !Number.isFinite(Date.parse(value.updatedAt)) || !isRecord$4(value.requests)) throw new Error("invalid Model state metadata");
|
|
1379
|
+
const current = readBinding(value.current, "current");
|
|
1380
|
+
const previous = value.previous === void 0 ? void 0 : readBinding(value.previous, "previous");
|
|
1381
|
+
if (previous && sameModelBinding(previous, current)) throw new Error("Model state previous binding must differ from current");
|
|
1382
|
+
const requests = {};
|
|
1383
|
+
for (const [requestId, receipt] of Object.entries(value.requests)) {
|
|
1384
|
+
if (requestId.length === 0) throw new Error("Model state request id cannot be empty");
|
|
1385
|
+
const restored = readReceipt(receipt);
|
|
1386
|
+
if (restored.requestId !== requestId) throw new Error(`Model state request key mismatch: ${requestId}`);
|
|
1387
|
+
requests[requestId] = restored;
|
|
1388
|
+
}
|
|
1389
|
+
const pending = value.pending === void 0 ? void 0 : readPending(value.pending);
|
|
1390
|
+
if (pending) {
|
|
1391
|
+
const receipt = requests[pending.request.requestId];
|
|
1392
|
+
if (!receipt || receipt.status !== "pending") throw new Error(`Model state pending request is missing its pending receipt: ${pending.request.requestId}`);
|
|
1393
|
+
if (pending.expectedRevision !== value.revision || pending.bindingRevision !== current.bindingRevision) throw new Error("Model state pending revision or binding mismatch");
|
|
1394
|
+
if (pending.ownerId !== void 0 && pending.ownerId !== value.ownerId || pending.principal !== void 0 && (pending.principal.homeId !== value.homeId || pending.principal.ownerId !== value.ownerId)) throw new Error("Model state pending principal does not own this Home");
|
|
1395
|
+
if (receipt.inputDigest !== pending.inputDigest || receipt.operation !== pending.operation || receipt.expectedRevision !== pending.expectedRevision || receipt.bindingRevision !== pending.bindingRevision || receipt.budgetIdentity !== pending.budgetIdentity || !sameReference(receipt.target, pending.target) || !sameBudget(receipt.budget, pending.budget) || !samePrincipal(receipt.principal, pending.principal) || !sameOptionalBinding(receipt.previous, previous) || receipt.phase !== pending.phase || receipt.current === void 0 || !sameModelBinding(receipt.current, current)) throw new Error("Model state pending receipt does not match its durable request");
|
|
1396
|
+
} else if (Object.values(requests).some((receipt) => receipt.status === "pending")) throw new Error("Model state contains a pending receipt without a pending request");
|
|
1397
|
+
const state = {
|
|
1398
|
+
current,
|
|
1399
|
+
homeId: value.homeId,
|
|
1400
|
+
ownerId: value.ownerId,
|
|
1401
|
+
...pending ? { pending } : {},
|
|
1402
|
+
...previous ? { previous } : {},
|
|
1403
|
+
...value.recoveryRequired === void 0 ? {} : { recoveryRequired: readError(value.recoveryRequired) },
|
|
1404
|
+
requests: Object.freeze(requests),
|
|
1405
|
+
revision: value.revision,
|
|
1406
|
+
schemaVersion: 1,
|
|
1407
|
+
updatedAt: value.updatedAt
|
|
1408
|
+
};
|
|
1409
|
+
return structuredClone(state);
|
|
1410
|
+
}
|
|
1411
|
+
function readBinding(value, label) {
|
|
1412
|
+
if (!isRecord$4(value) || typeof value.provider !== "string" || value.provider.length === 0 || typeof value.model !== "string" || value.model.length === 0 || typeof value.bindingRevision !== "string" || value.bindingRevision.length === 0) throw new Error(`invalid Model state ${label} binding`);
|
|
1413
|
+
return Object.freeze({
|
|
1414
|
+
bindingRevision: value.bindingRevision,
|
|
1415
|
+
model: value.model,
|
|
1416
|
+
provider: value.provider
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
function readSource(value) {
|
|
1420
|
+
if (!isRecord$4(value) || value.kind !== "automation" && value.kind !== "human" || typeof value.reference !== "string" || value.reference.length === 0) throw new Error("invalid Model change source");
|
|
1421
|
+
return Object.freeze({
|
|
1422
|
+
kind: value.kind,
|
|
1423
|
+
reference: value.reference
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
function readBudget(value) {
|
|
1427
|
+
if (!isRecord$4(value) || typeof value.deadlineAt !== "string" || typeof value.identity !== "string" || typeof value.maxOutputTokens !== "number" || typeof value.maxPaidRequests !== "number" || typeof value.outputTokens !== "number" || typeof value.paidRequests !== "number" || typeof value.recoveryReserveOutputTokens !== "number" || typeof value.recoveryReservePaidRequests !== "number" || typeof value.reservedOutputTokens !== "number" || typeof value.reservedPaidRequests !== "number" || !isRecord$4(value.reservations)) throw new Error("invalid Model change budget");
|
|
1428
|
+
const reservations = {};
|
|
1429
|
+
for (const [id, reservation] of Object.entries(value.reservations)) {
|
|
1430
|
+
if (!isRecord$4(reservation) || reservation.id !== id || reservation.kind !== "activation" && reservation.kind !== "restore" && reservation.kind !== "validation" || reservation.paidRequests !== 1 || typeof reservation.maxOutputTokens !== "number" || typeof reservation.reservedAt !== "string") throw new Error(`invalid Model change budget reservation: ${id}`);
|
|
1431
|
+
reservations[id] = Object.freeze({
|
|
1432
|
+
id,
|
|
1433
|
+
kind: reservation.kind,
|
|
1434
|
+
maxOutputTokens: reservation.maxOutputTokens,
|
|
1435
|
+
paidRequests: 1,
|
|
1436
|
+
reservedAt: reservation.reservedAt
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
const budget = {
|
|
1440
|
+
deadlineAt: value.deadlineAt,
|
|
1441
|
+
identity: value.identity,
|
|
1442
|
+
maxOutputTokens: value.maxOutputTokens,
|
|
1443
|
+
maxPaidRequests: value.maxPaidRequests,
|
|
1444
|
+
outputTokens: value.outputTokens,
|
|
1445
|
+
paidRequests: value.paidRequests,
|
|
1446
|
+
recoveryReserveOutputTokens: value.recoveryReserveOutputTokens,
|
|
1447
|
+
recoveryReservePaidRequests: value.recoveryReservePaidRequests,
|
|
1448
|
+
reservations: Object.freeze(reservations),
|
|
1449
|
+
reservedOutputTokens: value.reservedOutputTokens,
|
|
1450
|
+
reservedPaidRequests: value.reservedPaidRequests
|
|
1451
|
+
};
|
|
1452
|
+
validateModelChangeBudget(budget);
|
|
1453
|
+
return Object.freeze(budget);
|
|
1454
|
+
}
|
|
1455
|
+
function readError(value) {
|
|
1456
|
+
if (!isRecord$4(value) || typeof value.code !== "string" || typeof value.message !== "string" || value.outcome !== "known" && value.outcome !== "unknown" || value.stage !== "activation" && value.stage !== "boundary" && value.stage !== "commit" && value.stage !== "notification" && value.stage !== "restore" && value.stage !== "validation") throw new Error("invalid Model change error");
|
|
1457
|
+
return Object.freeze({
|
|
1458
|
+
code: value.code,
|
|
1459
|
+
message: value.message,
|
|
1460
|
+
outcome: value.outcome,
|
|
1461
|
+
stage: value.stage
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
function readNotification(value) {
|
|
1465
|
+
if (!isRecord$4(value) || !Number.isSafeInteger(value.attempts) || value.attempts < 0 || value.status !== "pending" && value.status !== "sent" && value.status !== "unknown" || value.lastAttemptAt !== void 0 && (typeof value.lastAttemptAt !== "string" || !Number.isFinite(Date.parse(value.lastAttemptAt)))) throw new Error("invalid Model change notification state");
|
|
1466
|
+
return Object.freeze({
|
|
1467
|
+
attempts: value.attempts,
|
|
1468
|
+
...value.lastAttemptAt === void 0 ? {} : { lastAttemptAt: value.lastAttemptAt },
|
|
1469
|
+
status: value.status
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
function readReceipt(value) {
|
|
1473
|
+
if (!isRecord$4(value) || typeof value.requestId !== "string" || value.requestId.length === 0 || value.operation !== "set" && value.operation !== "rollback" || typeof value.acceptedAt !== "string" || !Number.isFinite(Date.parse(value.acceptedAt)) || typeof value.updatedAt !== "string" || !Number.isFinite(Date.parse(value.updatedAt)) || !Number.isSafeInteger(value.expectedRevision) || value.expectedRevision < 0 || !Number.isSafeInteger(value.revision) || value.revision < 0 || typeof value.inputDigest !== "string" || typeof value.bindingRevision !== "string" || value.bindingRevision.length === 0 || typeof value.budgetIdentity !== "string" || value.budgetIdentity.length === 0 || value.inputDigest.length === 0 || value.status !== "pending" && value.status !== "applied" && value.status !== "failed" && value.status !== "restored" && value.status !== "recovery-required" || !isModelChangePhase(value.phase)) throw new Error("invalid Model change receipt");
|
|
1474
|
+
const budget = readBudget(value.budget);
|
|
1475
|
+
const current = value.current === void 0 ? void 0 : readBinding(value.current, "receipt current");
|
|
1476
|
+
const previous = value.previous === void 0 ? void 0 : readBinding(value.previous, "receipt previous");
|
|
1477
|
+
const target = readTarget(value.target);
|
|
1478
|
+
const error = value.error === void 0 ? void 0 : readError(value.error);
|
|
1479
|
+
const notification = value.notification === void 0 ? void 0 : readNotification(value.notification);
|
|
1480
|
+
const principal = value.principal === void 0 ? void 0 : readPrincipal(value.principal);
|
|
1481
|
+
const authorizationId = value.authorizationId === void 0 ? void 0 : requireString(value.authorizationId, "authorizationId");
|
|
1482
|
+
if (value.operation === "set" && target === void 0) throw new Error("Model set receipt requires a target");
|
|
1483
|
+
if (value.operation === "rollback" && target !== void 0) throw new Error("Model rollback receipt cannot contain a target");
|
|
1484
|
+
if (value.status === "pending") {
|
|
1485
|
+
if (value.phase === "awaiting-approval") {
|
|
1486
|
+
if (authorizationId === void 0 || principal === void 0 || budget.identity !== "awaiting-approval") throw new Error("Model state awaiting receipt is missing its durable approval binding");
|
|
1487
|
+
} else if (authorizationId === void 0 || principal === void 0 || budget.identity === "awaiting-approval") throw new Error("Model state pending receipt is missing trusted authorization state");
|
|
1488
|
+
} else if (authorizationId === void 0 || principal === void 0 || budget.identity === "awaiting-approval") throw new Error("Model state final receipt is missing trusted authorization state");
|
|
1489
|
+
validateReceiptPhase(value.status, value.phase);
|
|
1490
|
+
if (budget.identity !== value.budgetIdentity) throw new Error("Model change receipt budget identity mismatch");
|
|
1491
|
+
return Object.freeze({
|
|
1492
|
+
...modelChangeRecordBase(value.acceptedAt, authorizationId, value.bindingRevision, budget, value.budgetIdentity),
|
|
1493
|
+
...current ? { current } : {},
|
|
1494
|
+
...error ? { error } : {},
|
|
1495
|
+
expectedRevision: value.expectedRevision,
|
|
1496
|
+
inputDigest: value.inputDigest,
|
|
1497
|
+
operation: value.operation,
|
|
1498
|
+
phase: value.phase,
|
|
1499
|
+
...previous ? { previous } : {},
|
|
1500
|
+
...principal ? { principal } : {},
|
|
1501
|
+
requestId: value.requestId,
|
|
1502
|
+
revision: value.revision,
|
|
1503
|
+
status: value.status,
|
|
1504
|
+
...target ? { target } : {},
|
|
1505
|
+
updatedAt: value.updatedAt,
|
|
1506
|
+
...notification ? { notification } : {}
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
function readPending(value) {
|
|
1510
|
+
if (!isRecord$4(value) || typeof value.acceptedAt !== "string" || !Number.isFinite(Date.parse(value.acceptedAt)) || typeof value.bindingRevision !== "string" || value.bindingRevision.length === 0 || typeof value.budgetIdentity !== "string" || value.budgetIdentity.length === 0 || typeof value.expectedRevision !== "number" || !Number.isSafeInteger(value.expectedRevision) || value.expectedRevision < 0 || typeof value.inputDigest !== "string" || value.inputDigest.length === 0 || !isModelChangePhase(value.phase) || typeof value.updatedAt !== "string" || !Number.isFinite(Date.parse(value.updatedAt))) throw new Error("invalid Model change pending record");
|
|
1511
|
+
const request = readSubmission(value.request);
|
|
1512
|
+
if (request.expectedRevision !== value.expectedRevision || request.operation !== value.operation) throw new Error("Model state pending request metadata mismatch");
|
|
1513
|
+
const target = readTarget(value.target);
|
|
1514
|
+
if (!sameReference(target, request.target)) throw new Error("Model state pending target mismatch");
|
|
1515
|
+
const budget = readBudget(value.budget);
|
|
1516
|
+
if (budget.identity !== value.budgetIdentity) throw new Error("Model state pending budget identity mismatch");
|
|
1517
|
+
const principal = value.principal === void 0 ? void 0 : readPrincipal(value.principal);
|
|
1518
|
+
const authorizationId = value.authorizationId === void 0 ? void 0 : requireString(value.authorizationId, "authorizationId");
|
|
1519
|
+
const ownerId = value.ownerId === void 0 ? void 0 : requireString(value.ownerId, "ownerId");
|
|
1520
|
+
if (principal && ownerId !== principal.ownerId) throw new Error("Model state pending owner mismatch");
|
|
1521
|
+
if (value.phase === "awaiting-approval") {
|
|
1522
|
+
if (authorizationId === void 0 || principal === void 0 || budget.identity !== "awaiting-approval") throw new Error("Model state awaiting approval record is missing its durable approval binding");
|
|
1523
|
+
} else if (authorizationId === void 0 || principal === void 0 || budget.identity === "awaiting-approval") throw new Error("Model state accepted request is missing trusted authorization state");
|
|
1524
|
+
if (value.phase === "recovery-required" && value.recoveryAttemptedAt === void 0) throw new Error("Model state recovery-required record is missing its attempt marker");
|
|
1525
|
+
if (value.recoveryAttemptedAt !== void 0 && value.phase !== "recovery-required") throw new Error("Model state recovery marker has an invalid phase");
|
|
1526
|
+
if (value.unknownPaidCallId !== void 0 && value.phase !== "validation-unknown" && value.phase !== "recovery-required") throw new Error("Model state unknown paid call has an invalid phase");
|
|
1527
|
+
return Object.freeze({
|
|
1528
|
+
...modelChangeRecordBase(value.acceptedAt, authorizationId, value.bindingRevision, budget, value.budgetIdentity),
|
|
1529
|
+
expectedRevision: value.expectedRevision,
|
|
1530
|
+
inputDigest: value.inputDigest,
|
|
1531
|
+
operation: value.operation,
|
|
1532
|
+
...ownerId === void 0 ? {} : { ownerId },
|
|
1533
|
+
phase: value.phase,
|
|
1534
|
+
...principal ? { principal } : {},
|
|
1535
|
+
request,
|
|
1536
|
+
...value.recoveryAttemptedAt === void 0 ? {} : { recoveryAttemptedAt: requireTimestamp$1(value.recoveryAttemptedAt, "recoveryAttemptedAt") },
|
|
1537
|
+
...value.unknownPaidCallId === void 0 ? {} : { unknownPaidCallId: requireString(value.unknownPaidCallId, "unknownPaidCallId") },
|
|
1538
|
+
...target ? { target } : {},
|
|
1539
|
+
updatedAt: value.updatedAt
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
function readPrincipal(value) {
|
|
1543
|
+
if (!isRecord$4(value) || typeof value.homeId !== "string" || value.homeId.length === 0 || typeof value.ownerId !== "string" || value.ownerId.length === 0) throw new Error("invalid Model change principal");
|
|
1544
|
+
return Object.freeze({
|
|
1545
|
+
homeId: value.homeId,
|
|
1546
|
+
ownerId: value.ownerId,
|
|
1547
|
+
source: readSource(value.source)
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
function readTarget(value) {
|
|
1551
|
+
if (value === void 0) return void 0;
|
|
1552
|
+
if (!isRecord$4(value) || typeof value.provider !== "string" || value.provider.length === 0 || typeof value.model !== "string" || value.model.length === 0) throw new Error("invalid Model change target");
|
|
1553
|
+
return Object.freeze({
|
|
1554
|
+
model: value.model,
|
|
1555
|
+
provider: value.provider
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
function readSubmission(value) {
|
|
1559
|
+
if (!isRecord$4(value) || typeof value.requestId !== "string" || value.requestId.length === 0 || value.operation !== "set" && value.operation !== "rollback" || !Number.isSafeInteger(value.expectedRevision) || value.expectedRevision < 0) throw new Error("invalid Model change request");
|
|
1560
|
+
const target = readTarget(value.target);
|
|
1561
|
+
if (value.operation === "set" && target === void 0) throw new Error("Model set request requires a target");
|
|
1562
|
+
if (value.operation === "rollback" && target !== void 0) throw new Error("Model rollback request cannot contain a target");
|
|
1563
|
+
return {
|
|
1564
|
+
expectedRevision: value.expectedRevision,
|
|
1565
|
+
operation: value.operation,
|
|
1566
|
+
requestId: value.requestId,
|
|
1567
|
+
...target ? { target } : {}
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
function validateReceiptPhase(status, phase) {
|
|
1571
|
+
if (status === "pending" && (phase === "completed" || phase === "notifying") || status === "applied" && phase !== "notifying" && phase !== "completed" || status === "restored" && phase !== "completed" || status === "failed" && phase !== "completed" && phase !== "validation-unknown" && phase !== "recovery-required" || status === "recovery-required" && phase !== "recovery-required") throw new Error("Model change receipt status and phase mismatch");
|
|
1572
|
+
}
|
|
1573
|
+
function sameReference(left, right) {
|
|
1574
|
+
return left?.model === right?.model && left?.provider === right?.provider;
|
|
1575
|
+
}
|
|
1576
|
+
function sameOptionalBinding(left, right) {
|
|
1577
|
+
if (left === void 0 || right === void 0) return left === right;
|
|
1578
|
+
return sameModelBinding(left, right);
|
|
1579
|
+
}
|
|
1580
|
+
function samePrincipal(left, right) {
|
|
1581
|
+
return left?.homeId === right?.homeId && left?.ownerId === right?.ownerId && left?.source.kind === right?.source.kind && left?.source.reference === right?.source.reference;
|
|
1582
|
+
}
|
|
1583
|
+
function sameBudget(left, right) {
|
|
1584
|
+
if (left.deadlineAt !== right.deadlineAt || left.identity !== right.identity || left.maxOutputTokens !== right.maxOutputTokens || left.maxPaidRequests !== right.maxPaidRequests || left.outputTokens !== right.outputTokens || left.paidRequests !== right.paidRequests || left.recoveryReserveOutputTokens !== right.recoveryReserveOutputTokens || left.recoveryReservePaidRequests !== right.recoveryReservePaidRequests || left.reservedOutputTokens !== right.reservedOutputTokens || left.reservedPaidRequests !== right.reservedPaidRequests) return false;
|
|
1585
|
+
const leftIds = Object.keys(left.reservations);
|
|
1586
|
+
const rightIds = Object.keys(right.reservations);
|
|
1587
|
+
if (leftIds.length !== rightIds.length) return false;
|
|
1588
|
+
return leftIds.every((id) => {
|
|
1589
|
+
const leftReservation = left.reservations[id];
|
|
1590
|
+
const rightReservation = right.reservations[id];
|
|
1591
|
+
return leftReservation !== void 0 && rightReservation !== void 0 && leftReservation.id === rightReservation.id && leftReservation.kind === rightReservation.kind && leftReservation.maxOutputTokens === rightReservation.maxOutputTokens && leftReservation.paidRequests === rightReservation.paidRequests && leftReservation.reservedAt === rightReservation.reservedAt;
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
function requireString(value, label) {
|
|
1595
|
+
if (typeof value !== "string" || value.length === 0) throw new Error(`invalid Model state ${label}`);
|
|
1596
|
+
return value;
|
|
1597
|
+
}
|
|
1598
|
+
function requireTimestamp$1(value, label) {
|
|
1599
|
+
const timestamp = requireString(value, label);
|
|
1600
|
+
if (!Number.isFinite(Date.parse(timestamp))) throw new Error(`invalid Model state ${label}`);
|
|
1601
|
+
return timestamp;
|
|
1602
|
+
}
|
|
1603
|
+
function sameModelBinding(left, right) {
|
|
1604
|
+
return left.provider === right.provider && left.model === right.model && left.bindingRevision === right.bindingRevision;
|
|
1605
|
+
}
|
|
1606
|
+
function isModelChangePhase(value) {
|
|
1607
|
+
return value === "awaiting-approval" || value === "accepted" || value === "waiting-for-boundary" || value === "validating" || value === "activating" || value === "committing" || value === "notifying" || value === "completed" || value === "validation-unknown" || value === "recovery-required";
|
|
1608
|
+
}
|
|
1609
|
+
function modelChangeRecordBase(acceptedAt, authorizationId, bindingRevision, budget, budgetIdentity) {
|
|
1610
|
+
return {
|
|
1611
|
+
acceptedAt,
|
|
1612
|
+
...authorizationId === void 0 ? {} : { authorizationId },
|
|
1613
|
+
bindingRevision,
|
|
1614
|
+
budget,
|
|
1615
|
+
budgetIdentity
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
//#endregion
|
|
1619
|
+
//#region src/adapters/outbound/persistence/deployment/model-state/in-memory-model-state-repository.ts
|
|
1620
|
+
var ModelStateRepositoryConflict = class extends Error {
|
|
1621
|
+
name = "ModelStateRepositoryConflict";
|
|
1622
|
+
};
|
|
1623
|
+
function createInMemoryModelStateRepository(options) {
|
|
1624
|
+
let current = cloneAndValidate(options.initial);
|
|
1625
|
+
const serial = Effect.unsafeMakeSemaphore(1);
|
|
1626
|
+
return {
|
|
1627
|
+
load: () => Effect.sync(() => structuredClone(current)),
|
|
1628
|
+
persist: (input) => serial.withPermits(1)(Effect.uninterruptible(Effect.gen(function* () {
|
|
1629
|
+
const next = yield* Effect.try({
|
|
1630
|
+
try: () => validateNextState(current, input),
|
|
1631
|
+
catch: (error) => error
|
|
1632
|
+
});
|
|
1633
|
+
yield* options.persist?.(structuredClone(next)) ?? Effect.void;
|
|
1634
|
+
current = next;
|
|
1635
|
+
return structuredClone(current);
|
|
1636
|
+
}))),
|
|
1637
|
+
snapshot: () => structuredClone(current)
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
function cloneAndValidate(input) {
|
|
1641
|
+
return validateModelState(structuredClone(input));
|
|
1642
|
+
}
|
|
1643
|
+
function validateNextState(previous, input) {
|
|
1644
|
+
const next = cloneAndValidate(input);
|
|
1645
|
+
if (next.homeId !== previous.homeId || next.ownerId !== previous.ownerId) throw new ModelStateRepositoryConflict("Model state owner or Home cannot change");
|
|
1646
|
+
if (next.revision < previous.revision || next.revision > previous.revision + 1) throw new ModelStateRepositoryConflict("Model state revision must stay or advance by one");
|
|
1647
|
+
if (next.revision === previous.revision && !sameModelBinding(next.current, previous.current)) throw new ModelStateRepositoryConflict("Model state current binding changed without a revision advance");
|
|
1648
|
+
if (next.revision === previous.revision + 1) {
|
|
1649
|
+
if (!next.previous || !sameModelBinding(next.previous, previous.current) || next.pending !== void 0) throw new ModelStateRepositoryConflict("Model state applied revision must preserve the previous binding");
|
|
1650
|
+
}
|
|
1651
|
+
if (next.pending && next.pending.expectedRevision !== next.revision) throw new ModelStateRepositoryConflict("Model state pending request must target the current revision");
|
|
1652
|
+
return next;
|
|
1653
|
+
}
|
|
1654
|
+
//#endregion
|
|
1655
|
+
//#region src/adapters/outbound/persistence/deployment/model-state/json-model-state-repository.ts
|
|
1656
|
+
var ModelStatePersistenceError = class extends Error {
|
|
1657
|
+
operation;
|
|
1658
|
+
cause;
|
|
1659
|
+
name = "ModelStatePersistenceError";
|
|
1660
|
+
constructor(operation, cause) {
|
|
1661
|
+
super(`failed to ${operation} Model state`);
|
|
1662
|
+
this.operation = operation;
|
|
1663
|
+
this.cause = cause;
|
|
1664
|
+
}
|
|
1665
|
+
};
|
|
1666
|
+
function openJsonModelStateRepository(options) {
|
|
1667
|
+
return Effect.tryPromise({
|
|
1668
|
+
try: () => readPersistenceFile(options.filePath),
|
|
1669
|
+
catch: (error) => new ModelStatePersistenceError("load", error)
|
|
1670
|
+
}).pipe(Effect.flatMap((raw) => Effect.gen(function* () {
|
|
1671
|
+
if (raw !== void 0) yield* Effect.tryPromise({
|
|
1672
|
+
try: () => chmod(options.filePath, 384),
|
|
1673
|
+
catch: (error) => new ModelStatePersistenceError("load", error)
|
|
1674
|
+
});
|
|
1675
|
+
return yield* Effect.try({
|
|
1676
|
+
try: () => raw === void 0 ? structuredClone(options.initial) : decodeModelState(raw),
|
|
1677
|
+
catch: (error) => new ModelStatePersistenceError("load", error)
|
|
1678
|
+
});
|
|
1679
|
+
})), Effect.map((initial) => createInMemoryModelStateRepository({
|
|
1680
|
+
initial,
|
|
1681
|
+
persist: (state) => Effect.tryPromise({
|
|
1682
|
+
try: async () => {
|
|
1683
|
+
try {
|
|
1684
|
+
await chmod(options.filePath, 384);
|
|
1685
|
+
} catch (error) {
|
|
1686
|
+
if (!isMissingFileError(error)) throw error;
|
|
1687
|
+
}
|
|
1688
|
+
await writeAtomicTextFile(options.filePath, encodeModelState(state), {
|
|
1689
|
+
durable: true,
|
|
1690
|
+
mode: 384
|
|
1691
|
+
});
|
|
1692
|
+
},
|
|
1693
|
+
catch: (error) => new ModelStatePersistenceError("save", error)
|
|
1694
|
+
})
|
|
1695
|
+
})));
|
|
1696
|
+
}
|
|
1697
|
+
function isMissingFileError(error) {
|
|
1698
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
1699
|
+
}
|
|
1700
|
+
//#endregion
|
|
1701
|
+
//#region src/core/application/deployment/model/model-change-run-boundary.ts
|
|
1702
|
+
/**
|
|
1703
|
+
* Home-wide admission fence shared by every foreground/background runtime.
|
|
1704
|
+
* Waiting Runs are parked outside the active count, so a model-change drain
|
|
1705
|
+
* can close admission and wait only for work that actually started.
|
|
1706
|
+
*/
|
|
1707
|
+
function createModelChangeRunBoundary(options) {
|
|
1708
|
+
let open = true;
|
|
1709
|
+
let active = 0;
|
|
1710
|
+
const runWaiters = /* @__PURE__ */ new Set();
|
|
1711
|
+
let boundaryWaiter;
|
|
1712
|
+
const createLease = () => {
|
|
1713
|
+
let released = false;
|
|
1714
|
+
active += 1;
|
|
1715
|
+
return Object.freeze({ release: () => {
|
|
1716
|
+
if (released) return;
|
|
1717
|
+
released = true;
|
|
1718
|
+
active -= 1;
|
|
1719
|
+
if (active === 0) resolveBoundaryWaiter();
|
|
1720
|
+
} });
|
|
1721
|
+
};
|
|
1722
|
+
const acquireRun = (input = {}) => Effect.suspend(() => {
|
|
1723
|
+
const signal = input.signal;
|
|
1724
|
+
if (signal?.aborted) return Effect.fail(abortError(signal));
|
|
1725
|
+
const remaining = input.deadlineAt === void 0 ? void 0 : remainingUntil(options.clock, input.deadlineAt);
|
|
1726
|
+
if (remaining !== void 0 && remaining <= 0) return Effect.fail(new ModelChangeBoundaryDeadlineError("Run admission deadline has passed"));
|
|
1727
|
+
if (open) return Effect.sync(createLease);
|
|
1728
|
+
const waiting = Effect.async((resume) => {
|
|
1729
|
+
let settled = false;
|
|
1730
|
+
let waiter;
|
|
1731
|
+
let onAbort;
|
|
1732
|
+
const remove = () => {
|
|
1733
|
+
runWaiters.delete(waiter);
|
|
1734
|
+
if (signal && onAbort) signal.removeEventListener("abort", onAbort);
|
|
1735
|
+
};
|
|
1736
|
+
const cleanup = () => {
|
|
1737
|
+
if (settled) return;
|
|
1738
|
+
settled = true;
|
|
1739
|
+
remove();
|
|
1740
|
+
};
|
|
1741
|
+
const resumeWaiter = (effect) => {
|
|
1742
|
+
if (settled) return;
|
|
1743
|
+
settled = true;
|
|
1744
|
+
remove();
|
|
1745
|
+
resume(effect);
|
|
1746
|
+
};
|
|
1747
|
+
onAbort = signal ? () => resumeWaiter(Effect.fail(abortError(signal))) : void 0;
|
|
1748
|
+
waiter = {
|
|
1749
|
+
cleanup,
|
|
1750
|
+
resume: resumeWaiter
|
|
1751
|
+
};
|
|
1752
|
+
runWaiters.add(waiter);
|
|
1753
|
+
if (onAbort && signal) signal.addEventListener("abort", onAbort, { once: true });
|
|
1754
|
+
return Effect.sync(cleanup);
|
|
1755
|
+
});
|
|
1756
|
+
return remaining === void 0 ? waiting : waiting.pipe(Effect.timeoutFail({
|
|
1757
|
+
duration: Math.min(remaining, 2147483647),
|
|
1758
|
+
onTimeout: () => new ModelChangeBoundaryDeadlineError("Run admission deadline has passed")
|
|
1759
|
+
}));
|
|
1760
|
+
});
|
|
1761
|
+
const fence = () => {
|
|
1762
|
+
open = false;
|
|
1763
|
+
};
|
|
1764
|
+
const waitForSafeBoundary = (input) => Effect.suspend(() => {
|
|
1765
|
+
fence();
|
|
1766
|
+
if (boundaryWaiter) return Effect.fail(/* @__PURE__ */ new Error("Model change boundary is already draining"));
|
|
1767
|
+
const remaining = remainingUntil(options.clock, input.deadlineAt);
|
|
1768
|
+
if (remaining <= 0) return Effect.fail(new ModelChangeBoundaryDeadlineError("Model change boundary deadline has passed"));
|
|
1769
|
+
if (active === 0) return Effect.succeed({
|
|
1770
|
+
activeRuns: 0,
|
|
1771
|
+
completedAt: options.clock.now()
|
|
1772
|
+
});
|
|
1773
|
+
return Effect.async((resume) => {
|
|
1774
|
+
let settled = false;
|
|
1775
|
+
const cleanup = () => {
|
|
1776
|
+
if (settled) return;
|
|
1777
|
+
settled = true;
|
|
1778
|
+
if (boundaryWaiter?.cleanup === cleanup) boundaryWaiter = void 0;
|
|
1779
|
+
};
|
|
1780
|
+
const resumeWaiter = (effect) => {
|
|
1781
|
+
if (settled) return;
|
|
1782
|
+
settled = true;
|
|
1783
|
+
if (boundaryWaiter?.cleanup === cleanup) boundaryWaiter = void 0;
|
|
1784
|
+
resume(effect);
|
|
1785
|
+
};
|
|
1786
|
+
boundaryWaiter = {
|
|
1787
|
+
cleanup,
|
|
1788
|
+
resume: resumeWaiter
|
|
1789
|
+
};
|
|
1790
|
+
return Effect.sync(cleanup);
|
|
1791
|
+
}).pipe(Effect.timeoutFail({
|
|
1792
|
+
duration: Math.min(remaining, 2147483647),
|
|
1793
|
+
onTimeout: () => new ModelChangeBoundaryDeadlineError("Active Runs did not drain before the model change deadline")
|
|
1794
|
+
}));
|
|
1795
|
+
});
|
|
1796
|
+
const resumeAfterChange = () => Effect.sync(() => {
|
|
1797
|
+
open = true;
|
|
1798
|
+
const waiting = [...runWaiters];
|
|
1799
|
+
runWaiters.clear();
|
|
1800
|
+
for (const waiter of waiting) waiter.resume(Effect.succeed(createLease()));
|
|
1801
|
+
});
|
|
1802
|
+
function resolveBoundaryWaiter() {
|
|
1803
|
+
const waiter = boundaryWaiter;
|
|
1804
|
+
if (!waiter || active !== 0) return;
|
|
1805
|
+
waiter.resume(Effect.succeed({
|
|
1806
|
+
activeRuns: 0,
|
|
1807
|
+
completedAt: options.clock.now()
|
|
1808
|
+
}));
|
|
1809
|
+
}
|
|
1810
|
+
return Object.freeze({
|
|
1811
|
+
acquireRun,
|
|
1812
|
+
activeRunCount: () => active,
|
|
1813
|
+
admissionOpen: () => open,
|
|
1814
|
+
fence,
|
|
1815
|
+
resumeAfterChange,
|
|
1816
|
+
waitForSafeBoundary
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
var ModelChangeBoundaryDeadlineError = class extends Error {
|
|
1820
|
+
name = "ModelChangeBoundaryDeadlineError";
|
|
1821
|
+
};
|
|
1822
|
+
function remainingUntil(clock, deadlineAt) {
|
|
1823
|
+
const deadline = Date.parse(deadlineAt);
|
|
1824
|
+
const now = Date.parse(clock.now());
|
|
1825
|
+
return Number.isFinite(deadline) && Number.isFinite(now) ? deadline - now : 0;
|
|
1826
|
+
}
|
|
1827
|
+
function abortError(signal) {
|
|
1828
|
+
return signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("Run admission aborted");
|
|
1829
|
+
}
|
|
1830
|
+
//#endregion
|
|
1831
|
+
//#region src/core/application/tool-execution/brokerage/model-change-authorization.ts
|
|
1832
|
+
const TOOL_ID = "rivus.model.change";
|
|
1833
|
+
const TOOL_VERSION = "1";
|
|
1834
|
+
/** Reuses Broker policy, exact approval binding and its durable operation ledger. */
|
|
1835
|
+
function createModelChangeAuthorization(options) {
|
|
1836
|
+
const serial = Effect.unsafeMakeSemaphore(1);
|
|
1837
|
+
const digest = (value) => createToolInputDigest(value, options.digest);
|
|
1838
|
+
const admissions = /* @__PURE__ */ new Map();
|
|
1839
|
+
const broker = createToolBroker({
|
|
1840
|
+
approvals: { consume: (request) => Effect.sync(() => {
|
|
1841
|
+
const admission = admissions.get(request.operationId);
|
|
1842
|
+
return admission !== void 0 && digest(request) === digest(admission.approval);
|
|
1843
|
+
}) },
|
|
1844
|
+
catalog: { snapshot: () => ({ tools: [] }) },
|
|
1845
|
+
digest: options.digest,
|
|
1846
|
+
hostTools: [{
|
|
1847
|
+
createExecutor: () => ({ execute: (_input, context) => {
|
|
1848
|
+
const admission = context.operationId ? admissions.get(context.operationId) : void 0;
|
|
1849
|
+
return admission ? Effect.succeed(admission.result) : Effect.fail(/* @__PURE__ */ new Error("model grant admission is missing"));
|
|
1850
|
+
} }),
|
|
1851
|
+
id: TOOL_ID,
|
|
1852
|
+
idempotency: "required",
|
|
1853
|
+
risk: "host-control",
|
|
1854
|
+
version: TOOL_VERSION
|
|
1855
|
+
}],
|
|
1856
|
+
operations: options.operations,
|
|
1857
|
+
policy: { current: () => options.grant.current().pipe(Effect.map((grant) => ({
|
|
1858
|
+
epoch: grant.revision,
|
|
1859
|
+
revokedToolIds: grant.enabled ? [] : [TOOL_ID]
|
|
1860
|
+
}))) }
|
|
1861
|
+
});
|
|
1862
|
+
const executeBrokerAuthorization = (run, grant, principal, request, authorizationId, deadlineAt) => {
|
|
1863
|
+
const operationId = `model-authorization:${authorizationId}`;
|
|
1864
|
+
const approvalId = `model-grant:${authorizationId}`;
|
|
1865
|
+
const callId = operationId;
|
|
1866
|
+
const payload = {
|
|
1867
|
+
authorizationId,
|
|
1868
|
+
grantRevision: grant.revision,
|
|
1869
|
+
request
|
|
1870
|
+
};
|
|
1871
|
+
const approved = approvedAuthorization(grant, principal, authorizationId, deadlineAt, digest);
|
|
1872
|
+
const brokerAuthority = createModelChangeBrokerAuthority(run.authority, authorizationId);
|
|
1873
|
+
const authority = resolveInvocationAuthority(brokerAuthority);
|
|
1874
|
+
const approval = {
|
|
1875
|
+
agentId: authority.agentId,
|
|
1876
|
+
approvalId,
|
|
1877
|
+
callId,
|
|
1878
|
+
inputDigest: digest(payload),
|
|
1879
|
+
instanceId: authority.instanceId,
|
|
1880
|
+
operationId,
|
|
1881
|
+
risk: "host-control",
|
|
1882
|
+
runId: authority.runId,
|
|
1883
|
+
sessionKey: authority.sessionKey,
|
|
1884
|
+
tenantKey: authority.tenantKey,
|
|
1885
|
+
toolId: TOOL_ID,
|
|
1886
|
+
toolVersion: TOOL_VERSION
|
|
1887
|
+
};
|
|
1888
|
+
admissions.set(operationId, {
|
|
1889
|
+
approval,
|
|
1890
|
+
result: approved
|
|
1891
|
+
});
|
|
1892
|
+
return broker.execute({
|
|
1893
|
+
approvalId,
|
|
1894
|
+
authority: brokerAuthority,
|
|
1895
|
+
callId,
|
|
1896
|
+
input: payload,
|
|
1897
|
+
operationId,
|
|
1898
|
+
toolId: TOOL_ID,
|
|
1899
|
+
version: TOOL_VERSION
|
|
1900
|
+
}).pipe(Effect.ensuring(Effect.sync(() => admissions.delete(operationId))), Effect.map((result) => result));
|
|
1901
|
+
};
|
|
1902
|
+
return {
|
|
1903
|
+
authorize: (input) => serial.withPermits(1)(Effect.gen(function* () {
|
|
1904
|
+
const run = yield* options.resolveRun(input.transport.reference);
|
|
1905
|
+
if (!run || run.kind !== "human") return rejected("trusted_context_invalid", "A trusted human Run is required.");
|
|
1906
|
+
const authority = yield* Effect.try({
|
|
1907
|
+
catch: (error) => error,
|
|
1908
|
+
try: () => resolveInvocationAuthority(run.authority)
|
|
1909
|
+
});
|
|
1910
|
+
const grant = yield* options.grant.current();
|
|
1911
|
+
if (!grant.enabled) return rejected("management_disabled", "Model management is disabled for this Home.");
|
|
1912
|
+
if (input.homeId !== grant.homeId || authority.endpointId !== grant.endpointId || authority.tenantKey !== grant.tenantKey || !authority.allowedActorOpenIds?.includes(grant.ownerId) || authority.allowedActorOpenIds.length !== 1) return rejected("owner_mismatch", "The current conversation is outside the model management grant.");
|
|
1913
|
+
if (!grant.operations.includes(input.request.operation)) return rejected("operation_not_granted", "This model operation is outside the configured grant.");
|
|
1914
|
+
if (input.current.provider !== grant.provider || input.request.target && input.request.target.provider !== grant.provider || input.current.bindingRevision !== grant.bindingRevision) return rejected("binding_changed", "The model provider or binding is outside the current grant.");
|
|
1915
|
+
const principal = {
|
|
1916
|
+
homeId: grant.homeId,
|
|
1917
|
+
ownerId: grant.ownerId,
|
|
1918
|
+
source: {
|
|
1919
|
+
kind: "human",
|
|
1920
|
+
reference: authority.sourceMessageId
|
|
1921
|
+
}
|
|
1922
|
+
};
|
|
1923
|
+
const authorizationId = authorizationIdentity(digest, grant, principal, input.request);
|
|
1924
|
+
if (input.existing?.authorizationId && input.existing.authorizationId !== authorizationId) return rejected("authorization_changed", "The existing model approval binding no longer matches this request.");
|
|
1925
|
+
const deadlineAt = input.existing?.deadlineAt ?? grantDeadline(options, grant);
|
|
1926
|
+
if (grant.requireApproval) {
|
|
1927
|
+
if (!options.approval) return rejected("approval_unavailable", "This model operation requires a durable human approval service.");
|
|
1928
|
+
const approval = yield* options.approval.requestOrGet(createApprovalInput(authorizationId, deadlineAt, grant, principal, input.request, {
|
|
1929
|
+
agentId: authority.agentId,
|
|
1930
|
+
...authority.endpointId ? { endpointId: authority.endpointId } : {},
|
|
1931
|
+
instanceId: authority.instanceId,
|
|
1932
|
+
runId: authority.runId,
|
|
1933
|
+
sessionKey: authority.sessionKey,
|
|
1934
|
+
sourceMessageId: authority.sourceMessageId,
|
|
1935
|
+
tenantKey: authority.tenantKey
|
|
1936
|
+
}));
|
|
1937
|
+
if (approval.status === "pending") return {
|
|
1938
|
+
authorizationId,
|
|
1939
|
+
bindingRevision: grant.bindingRevision,
|
|
1940
|
+
deadlineAt,
|
|
1941
|
+
principal,
|
|
1942
|
+
reason: approval.reason ?? "Model management approval is pending.",
|
|
1943
|
+
status: "awaiting-approval"
|
|
1944
|
+
};
|
|
1945
|
+
if (approval.status === "rejected") return rejected("approval_rejected", approval.reason ?? "Model management approval was rejected.");
|
|
1946
|
+
return yield* executeBrokerAuthorization(run, grant, principal, input.request, authorizationId, deadlineAt);
|
|
1947
|
+
}
|
|
1948
|
+
return yield* executeBrokerAuthorization(run, grant, principal, input.request, authorizationId, deadlineAt);
|
|
1949
|
+
})),
|
|
1950
|
+
revalidate: (input) => Effect.gen(function* () {
|
|
1951
|
+
const grant = yield* options.grant.current();
|
|
1952
|
+
const principal = input.pending.principal;
|
|
1953
|
+
const invalid = invalidPendingGrant(input.homeId, input.current, input.pending, input.authorizationId, grant, principal, digest);
|
|
1954
|
+
if (invalid) return invalid;
|
|
1955
|
+
if (grant.requireApproval) {
|
|
1956
|
+
if (!options.approval) return rejected("approval_unavailable", "Durable model management approval is unavailable.");
|
|
1957
|
+
const approval = yield* options.approval.requestOrGet(createApprovalInput(input.authorizationId, input.pending.budget.deadlineAt, grant, principal, input.pending.request));
|
|
1958
|
+
if (approval.status === "pending") return rejected("approval_pending", "Model management approval is still pending.");
|
|
1959
|
+
if (approval.status === "rejected") return rejected("approval_rejected", approval.reason ?? "Model management approval was rejected.");
|
|
1960
|
+
}
|
|
1961
|
+
return {
|
|
1962
|
+
bindingRevision: grant.bindingRevision,
|
|
1963
|
+
status: "approved"
|
|
1964
|
+
};
|
|
1965
|
+
}),
|
|
1966
|
+
resolvePending: (input) => Effect.gen(function* () {
|
|
1967
|
+
const grant = yield* options.grant.current();
|
|
1968
|
+
const pending = input.pending;
|
|
1969
|
+
const principal = pending.principal;
|
|
1970
|
+
const authorizationId = pending.authorizationId;
|
|
1971
|
+
if (!authorizationId || !principal) return rejected("approval_binding_missing", "The durable approval binding is missing.");
|
|
1972
|
+
const invalid = invalidPendingGrant(input.homeId, input.current, pending, authorizationId, grant, principal, digest, true);
|
|
1973
|
+
if (invalid) return invalid;
|
|
1974
|
+
if (!grant.requireApproval) return yield* executeWorkerBrokerAuthorization(grant, principal, pending.request, authorizationId, pending.budget.deadlineAt);
|
|
1975
|
+
if (!options.approval) return rejected("approval_unavailable", "Durable model management approval is unavailable.");
|
|
1976
|
+
const approval = yield* options.approval.waitForResolution(createApprovalInput(authorizationId, pending.budget.deadlineAt, grant, principal, pending.request));
|
|
1977
|
+
const latestGrant = yield* options.grant.current();
|
|
1978
|
+
const latestInvalid = invalidPendingGrant(input.homeId, input.current, pending, authorizationId, latestGrant, principal, digest, true);
|
|
1979
|
+
if (latestInvalid) return latestInvalid;
|
|
1980
|
+
if (approval.status === "pending") return {
|
|
1981
|
+
authorizationId,
|
|
1982
|
+
bindingRevision: latestGrant.bindingRevision,
|
|
1983
|
+
deadlineAt: pending.budget.deadlineAt,
|
|
1984
|
+
principal,
|
|
1985
|
+
reason: approval.reason ?? "Model management approval is pending.",
|
|
1986
|
+
status: "awaiting-approval"
|
|
1987
|
+
};
|
|
1988
|
+
if (approval.status === "rejected") return rejected("approval_rejected", approval.reason ?? "Model management approval was rejected.");
|
|
1989
|
+
return yield* executeWorkerBrokerAuthorization(latestGrant, principal, pending.request, authorizationId, pending.budget.deadlineAt);
|
|
1990
|
+
})
|
|
1991
|
+
};
|
|
1992
|
+
function executeWorkerBrokerAuthorization(grant, principal, request, authorizationId, deadlineAt) {
|
|
1993
|
+
return Effect.gen(function* () {
|
|
1994
|
+
if (!options.resolveWorkerRun) return rejected("worker_authority_unavailable", "The accepted model worker authority is unavailable.");
|
|
1995
|
+
const worker = yield* options.resolveWorkerRun({
|
|
1996
|
+
authorizationId,
|
|
1997
|
+
grant,
|
|
1998
|
+
principal,
|
|
1999
|
+
request
|
|
2000
|
+
});
|
|
2001
|
+
if (!worker || worker.kind !== "human") return rejected("worker_authority_unavailable", "The accepted model worker authority is unavailable.");
|
|
2002
|
+
return yield* executeBrokerAuthorization(worker, grant, principal, request, authorizationId, deadlineAt);
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
function authorizationIdentity(digest, grant, principal, request) {
|
|
2007
|
+
return digest({
|
|
2008
|
+
grant,
|
|
2009
|
+
principal,
|
|
2010
|
+
request
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
function createModelChangeBrokerAuthority(run, authorizationId) {
|
|
2014
|
+
const authority = resolveInvocationAuthority(run);
|
|
2015
|
+
const operationIdentity = `model-change:${authorizationId}`;
|
|
2016
|
+
return createInvocationAuthority({
|
|
2017
|
+
...authority,
|
|
2018
|
+
instanceId: operationIdentity,
|
|
2019
|
+
runId: operationIdentity,
|
|
2020
|
+
sessionKey: operationIdentity
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
2023
|
+
function approvedAuthorization(grant, principal, authorizationId, deadlineAt, digest) {
|
|
2024
|
+
return {
|
|
2025
|
+
authorizationId,
|
|
2026
|
+
bindingRevision: grant.bindingRevision,
|
|
2027
|
+
budget: {
|
|
2028
|
+
...grant.budget,
|
|
2029
|
+
deadlineAt,
|
|
2030
|
+
identity: digest({
|
|
2031
|
+
grant,
|
|
2032
|
+
principal
|
|
2033
|
+
})
|
|
2034
|
+
},
|
|
2035
|
+
principal,
|
|
2036
|
+
status: "approved"
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
function grantDeadline(options, grant) {
|
|
2040
|
+
const now = Date.parse(options.clock.now());
|
|
2041
|
+
if (!Number.isFinite(now) || !Number.isFinite(grant.timeoutMs) || grant.timeoutMs <= 0) throw new Error("invalid model management grant deadline");
|
|
2042
|
+
return new Date(now + grant.timeoutMs).toISOString();
|
|
2043
|
+
}
|
|
2044
|
+
function createApprovalInput(authorizationId, expiresAt, grant, principal, request, authority) {
|
|
2045
|
+
return {
|
|
2046
|
+
...authority ? { authority } : {},
|
|
2047
|
+
budget: grant.budget,
|
|
2048
|
+
expiresAt,
|
|
2049
|
+
grantRevision: grant.revision,
|
|
2050
|
+
interactionId: `model-change-approval:${authorizationId}`,
|
|
2051
|
+
principal,
|
|
2052
|
+
request
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
function invalidPendingGrant(homeId, current, pending, authorizationId, grant, principal, digest, awaiting = false) {
|
|
2056
|
+
if (!grant.enabled || !principal || principal.homeId !== grant.homeId || homeId !== grant.homeId || principal.ownerId !== grant.ownerId || principal.source.kind !== "human" || current.provider !== grant.provider || current.bindingRevision !== grant.bindingRevision || pending.target?.provider !== grant.provider || !grant.operations.includes(pending.operation) || authorizationId !== authorizationIdentity(digest, grant, principal, pending.request) || !awaiting && pending.budgetIdentity !== digest({
|
|
2057
|
+
grant,
|
|
2058
|
+
principal
|
|
2059
|
+
})) return rejected("authorization_revoked", "The accepted model management grant is no longer valid.");
|
|
2060
|
+
}
|
|
2061
|
+
function rejected(code, reason) {
|
|
2062
|
+
return {
|
|
2063
|
+
code,
|
|
2064
|
+
reason,
|
|
2065
|
+
status: "rejected"
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
//#endregion
|
|
2069
|
+
//#region src/adapters/cli/model/rivus-model-management-socket-server.ts
|
|
2070
|
+
const MAX_FRAME_BYTES = 64 * 1024;
|
|
2071
|
+
function createRivusModelManagementSocketServer(options) {
|
|
2072
|
+
assertAbsoluteSocketPath(options.socketPath);
|
|
2073
|
+
let server;
|
|
2074
|
+
let socketIdentity;
|
|
2075
|
+
let listening = false;
|
|
2076
|
+
let startPromise;
|
|
2077
|
+
return {
|
|
2078
|
+
close: async () => {
|
|
2079
|
+
if (startPromise) await startPromise.catch(() => void 0);
|
|
2080
|
+
const current = server;
|
|
2081
|
+
const identity = socketIdentity;
|
|
2082
|
+
server = void 0;
|
|
2083
|
+
socketIdentity = void 0;
|
|
2084
|
+
if (current && listening) await closeServer(current);
|
|
2085
|
+
listening = false;
|
|
2086
|
+
if (identity) await unlinkOwnedSocket(options.socketPath, identity);
|
|
2087
|
+
},
|
|
2088
|
+
listening: () => listening,
|
|
2089
|
+
start: async () => {
|
|
2090
|
+
if (listening) return;
|
|
2091
|
+
if (startPromise) return startPromise;
|
|
2092
|
+
startPromise = startServer(options, (next, identity) => {
|
|
2093
|
+
server = next;
|
|
2094
|
+
socketIdentity = identity;
|
|
2095
|
+
listening = true;
|
|
2096
|
+
});
|
|
2097
|
+
try {
|
|
2098
|
+
await startPromise;
|
|
2099
|
+
} finally {
|
|
2100
|
+
startPromise = void 0;
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
};
|
|
2104
|
+
}
|
|
2105
|
+
async function startServer(options, onStarted) {
|
|
2106
|
+
await prepareSocketParentDirectory(options.socketPath);
|
|
2107
|
+
const lockPath = `${options.socketPath}.lock`;
|
|
2108
|
+
const lock = await acquireStartLock(lockPath);
|
|
2109
|
+
const next = createServer((socket) => handleConnection(socket, options));
|
|
2110
|
+
let bound = false;
|
|
2111
|
+
try {
|
|
2112
|
+
await prepareSocketPath(options.socketPath);
|
|
2113
|
+
await listen(next, options.socketPath);
|
|
2114
|
+
bound = true;
|
|
2115
|
+
await chmod(options.socketPath, 384);
|
|
2116
|
+
onStarted(next, await readSocketIdentity(options.socketPath));
|
|
2117
|
+
} catch (error) {
|
|
2118
|
+
await closeServer(next).catch(() => void 0);
|
|
2119
|
+
if (bound) await unlinkSocketIfPresent(options.socketPath);
|
|
2120
|
+
throw error;
|
|
2121
|
+
} finally {
|
|
2122
|
+
await releaseStartLock(lock, lockPath);
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
async function handleConnection(socket, options) {
|
|
2126
|
+
let buffer = "";
|
|
2127
|
+
let handled = false;
|
|
2128
|
+
socket.setEncoding("utf8");
|
|
2129
|
+
socket.on("data", (chunk) => {
|
|
2130
|
+
if (handled) return;
|
|
2131
|
+
buffer += chunk;
|
|
2132
|
+
if (Buffer.byteLength(buffer, "utf8") > MAX_FRAME_BYTES) {
|
|
2133
|
+
handled = true;
|
|
2134
|
+
writeResponse(socket, createRivusModelManagementFailure("request_too_large", "model management request is too large"));
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
const newline = buffer.indexOf("\n");
|
|
2138
|
+
if (newline < 0) return;
|
|
2139
|
+
handled = true;
|
|
2140
|
+
handleFrame(buffer.slice(0, newline).trim(), socket, options);
|
|
2141
|
+
});
|
|
2142
|
+
socket.on("end", () => {
|
|
2143
|
+
if (handled || buffer.trim() === "") return;
|
|
2144
|
+
handled = true;
|
|
2145
|
+
handleFrame(buffer.trim(), socket, options);
|
|
2146
|
+
});
|
|
2147
|
+
socket.on("error", () => {
|
|
2148
|
+
handled = true;
|
|
2149
|
+
});
|
|
2150
|
+
}
|
|
2151
|
+
async function handleFrame(frame, socket, options) {
|
|
2152
|
+
let response;
|
|
2153
|
+
let request;
|
|
2154
|
+
try {
|
|
2155
|
+
request = parseRivusModelManagementWireRequest(JSON.parse(frame));
|
|
2156
|
+
} catch (error) {
|
|
2157
|
+
response = createRivusModelManagementFailure("invalid_request", error instanceof Error ? error.message : "invalid model request");
|
|
2158
|
+
writeResponse(socket, response);
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
2161
|
+
try {
|
|
2162
|
+
if (request.operation === "status") response = projectRivusModelCliResponse(await options.handlers.status({
|
|
2163
|
+
...request.requestId ? { requestId: request.requestId } : {},
|
|
2164
|
+
...request.verbose ? { verbose: true } : {}
|
|
2165
|
+
}), request.operation);
|
|
2166
|
+
else response = await handleMutation(request, options);
|
|
2167
|
+
} catch {
|
|
2168
|
+
response = createRivusModelManagementFailure("handler_failed", "the model management request could not be completed");
|
|
2169
|
+
}
|
|
2170
|
+
writeResponse(socket, response);
|
|
2171
|
+
}
|
|
2172
|
+
async function handleMutation(request, options) {
|
|
2173
|
+
if (!request.context || !options.resolveTrustedRun) return createRivusModelManagementFailure("trusted_context_required", "a trusted Run context is required for model changes");
|
|
2174
|
+
let trustedRun;
|
|
2175
|
+
try {
|
|
2176
|
+
trustedRun = await options.resolveTrustedRun.resolve(request.context);
|
|
2177
|
+
} catch {
|
|
2178
|
+
trustedRun = void 0;
|
|
2179
|
+
}
|
|
2180
|
+
if (trustedRun === void 0 || trustedRun === null) return createRivusModelManagementFailure("trusted_context_invalid", "the trusted Run context is invalid or expired");
|
|
2181
|
+
try {
|
|
2182
|
+
return projectRivusModelCliResponse(await options.handlers.handle(toRivusModelManagementSubmission(request), trustedRun), request.operation);
|
|
2183
|
+
} catch {
|
|
2184
|
+
return createRivusModelManagementFailure("handler_failed", "the model management request could not be completed");
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
function writeResponse(socket, response) {
|
|
2188
|
+
try {
|
|
2189
|
+
socket.end(`${JSON.stringify(response)}\n`);
|
|
2190
|
+
} catch {
|
|
2191
|
+
socket.destroy();
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
async function prepareSocketPath(socketPath) {
|
|
2195
|
+
try {
|
|
2196
|
+
const metadata = await stat(socketPath);
|
|
2197
|
+
if (!metadata.isSocket()) throw new Error(`refusing to replace non-socket model path: ${socketPath}`);
|
|
2198
|
+
if (await socketIsLive(socketPath)) throw new Error(`model management socket is already active: ${socketPath}`);
|
|
2199
|
+
const current = await stat(socketPath);
|
|
2200
|
+
if (current.dev !== metadata.dev || current.ino !== metadata.ino) throw new Error(`model management socket changed while starting: ${socketPath}`);
|
|
2201
|
+
await unlink(socketPath);
|
|
2202
|
+
} catch (error) {
|
|
2203
|
+
if (!isMissing(error)) throw error;
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
async function prepareSocketParentDirectory(socketPath) {
|
|
2207
|
+
const parent = dirname(socketPath);
|
|
2208
|
+
await mkdir(parent, {
|
|
2209
|
+
mode: 448,
|
|
2210
|
+
recursive: true
|
|
2211
|
+
});
|
|
2212
|
+
await chmod(parent, 448);
|
|
2213
|
+
}
|
|
2214
|
+
async function socketIsLive(socketPath) {
|
|
2215
|
+
return new Promise((resolve, reject) => {
|
|
2216
|
+
const socket = createConnection(socketPath);
|
|
2217
|
+
let settled = false;
|
|
2218
|
+
const finish = (result, error) => {
|
|
2219
|
+
if (settled) return;
|
|
2220
|
+
settled = true;
|
|
2221
|
+
socket.destroy();
|
|
2222
|
+
if (error) reject(error);
|
|
2223
|
+
else resolve(result);
|
|
2224
|
+
};
|
|
2225
|
+
socket.setTimeout(250, () => finish(false, /* @__PURE__ */ new Error("model management socket liveness could not be verified")));
|
|
2226
|
+
socket.once("connect", () => finish(true));
|
|
2227
|
+
socket.once("error", (error) => {
|
|
2228
|
+
if (error.code === "ECONNREFUSED" || error.code === "ENOENT") finish(false);
|
|
2229
|
+
else finish(false, error);
|
|
2230
|
+
});
|
|
2231
|
+
});
|
|
2232
|
+
}
|
|
2233
|
+
async function acquireStartLock(path) {
|
|
2234
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
2235
|
+
let handle;
|
|
2236
|
+
try {
|
|
2237
|
+
handle = await open(path, "wx", 384);
|
|
2238
|
+
await handle.writeFile(`${process.pid}\n`, "utf8");
|
|
2239
|
+
const metadata = await handle.stat();
|
|
2240
|
+
return {
|
|
2241
|
+
handle,
|
|
2242
|
+
identity: {
|
|
2243
|
+
dev: metadata.dev,
|
|
2244
|
+
ino: metadata.ino
|
|
2245
|
+
}
|
|
2246
|
+
};
|
|
2247
|
+
} catch (error) {
|
|
2248
|
+
await handle?.close().catch(() => void 0);
|
|
2249
|
+
if (!isAlreadyExists(error)) throw new Error("model management socket startup is already in progress", { cause: error });
|
|
2250
|
+
const pid = parseLockOwner(await readPersistenceFile(path));
|
|
2251
|
+
if (pid === void 0) throw new Error("model management socket startup lock owner could not be verified; manual recovery is required");
|
|
2252
|
+
if (processIsAlive(pid)) throw new Error("model management socket startup is already in progress");
|
|
2253
|
+
const reclaimed = `${path}.reclaim-${process.pid}-${randomUUID()}`;
|
|
2254
|
+
try {
|
|
2255
|
+
await rename(path, reclaimed);
|
|
2256
|
+
} catch (reclaimError) {
|
|
2257
|
+
if (isMissing(reclaimError)) continue;
|
|
2258
|
+
throw new Error("model management socket startup lock could not be reclaimed safely", { cause: reclaimError });
|
|
2259
|
+
}
|
|
2260
|
+
await unlink(reclaimed).catch((reclaimError) => {
|
|
2261
|
+
if (!isMissing(reclaimError)) throw reclaimError;
|
|
2262
|
+
});
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
throw new Error("model management socket startup lock changed during stale-owner recovery; manual recovery is required");
|
|
2266
|
+
}
|
|
2267
|
+
async function releaseStartLock(lock, path) {
|
|
2268
|
+
await lock.handle.close();
|
|
2269
|
+
try {
|
|
2270
|
+
const current = await stat(path);
|
|
2271
|
+
if (current.dev !== lock.identity.dev || current.ino !== lock.identity.ino) return;
|
|
2272
|
+
await unlink(path);
|
|
2273
|
+
} catch (error) {
|
|
2274
|
+
if (!isMissing(error)) throw error;
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
async function unlinkSocketIfPresent(socketPath) {
|
|
2278
|
+
try {
|
|
2279
|
+
if ((await stat(socketPath)).isSocket()) await unlink(socketPath);
|
|
2280
|
+
} catch (error) {
|
|
2281
|
+
if (!isMissing(error)) throw error;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
async function unlinkOwnedSocket(socketPath, identity) {
|
|
2285
|
+
try {
|
|
2286
|
+
const current = await readSocketIdentity(socketPath);
|
|
2287
|
+
if (current.dev !== identity.dev || current.ino !== identity.ino) return;
|
|
2288
|
+
await unlink(socketPath);
|
|
2289
|
+
} catch (error) {
|
|
2290
|
+
if (!isMissing(error)) throw error;
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
async function readSocketIdentity(socketPath) {
|
|
2294
|
+
const metadata = await stat(socketPath);
|
|
2295
|
+
return {
|
|
2296
|
+
dev: metadata.dev,
|
|
2297
|
+
ino: metadata.ino
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
function listen(server, socketPath) {
|
|
2301
|
+
return new Promise((resolve, reject) => {
|
|
2302
|
+
const onError = (error) => {
|
|
2303
|
+
server.off("listening", onListening);
|
|
2304
|
+
reject(error);
|
|
2305
|
+
};
|
|
2306
|
+
const onListening = () => {
|
|
2307
|
+
server.off("error", onError);
|
|
2308
|
+
resolve();
|
|
2309
|
+
};
|
|
2310
|
+
server.once("error", onError);
|
|
2311
|
+
server.once("listening", onListening);
|
|
2312
|
+
server.listen(socketPath);
|
|
2313
|
+
});
|
|
2314
|
+
}
|
|
2315
|
+
function closeServer(server) {
|
|
2316
|
+
return new Promise((resolve, reject) => {
|
|
2317
|
+
server.close((error) => error ? reject(error) : resolve());
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
function assertAbsoluteSocketPath(socketPath) {
|
|
2321
|
+
if (!isAbsolute(socketPath)) throw new Error("model socket path must be absolute");
|
|
2322
|
+
}
|
|
2323
|
+
function isMissing(error) {
|
|
2324
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
2325
|
+
}
|
|
2326
|
+
function isAlreadyExists(error) {
|
|
2327
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
|
|
2328
|
+
}
|
|
2329
|
+
function processIsAlive(pid) {
|
|
2330
|
+
try {
|
|
2331
|
+
process.kill(pid, 0);
|
|
2332
|
+
return true;
|
|
2333
|
+
} catch (error) {
|
|
2334
|
+
return typeof error === "object" && error !== null && "code" in error && error.code !== "ESRCH";
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
function parseLockOwner(value) {
|
|
2338
|
+
const normalized = value?.trim();
|
|
2339
|
+
if (!normalized || !/^\d+$/.test(normalized)) return void 0;
|
|
2340
|
+
const pid = Number(normalized);
|
|
2341
|
+
return Number.isSafeInteger(pid) && pid > 0 ? pid : void 0;
|
|
2342
|
+
}
|
|
2343
|
+
//#endregion
|
|
2344
|
+
//#region src/core/application/deployment/model/model-change-recovery.ts
|
|
2345
|
+
const recoveryPhases = /* @__PURE__ */ new Set([
|
|
2346
|
+
"activating",
|
|
2347
|
+
"committing",
|
|
2348
|
+
"validation-unknown",
|
|
2349
|
+
"recovery-required"
|
|
2350
|
+
]);
|
|
2351
|
+
function pendingRequiresModelChangeRecovery(pending) {
|
|
2352
|
+
return recoveryPhases.has(pending.phase) || pending.unknownPaidCallId !== void 0;
|
|
2353
|
+
}
|
|
2354
|
+
//#endregion
|
|
2355
|
+
//#region src/core/application/deployment/model/model-change-projection.ts
|
|
2356
|
+
function stateWithPending(state, pending, receipt) {
|
|
2357
|
+
return {
|
|
2358
|
+
...state,
|
|
2359
|
+
pending,
|
|
2360
|
+
requests: {
|
|
2361
|
+
...state.requests,
|
|
2362
|
+
[receipt.requestId]: receipt
|
|
2363
|
+
},
|
|
2364
|
+
updatedAt: receipt.updatedAt
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
function stateWithReceipt(state, receipt, recovery) {
|
|
2368
|
+
const { pending: _pending, recoveryRequired: _recoveryRequired, ...stateWithoutTransient } = state;
|
|
2369
|
+
return {
|
|
2370
|
+
...stateWithoutTransient,
|
|
2371
|
+
...recovery ? { recoveryRequired: recovery } : {},
|
|
2372
|
+
requests: {
|
|
2373
|
+
...state.requests,
|
|
2374
|
+
[receipt.requestId]: receipt
|
|
2375
|
+
},
|
|
2376
|
+
updatedAt: receipt.updatedAt
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
function pendingReceipt(state, pending, updatedAt) {
|
|
2380
|
+
return {
|
|
2381
|
+
acceptedAt: pending.acceptedAt,
|
|
2382
|
+
...pending.authorizationId ? { authorizationId: pending.authorizationId } : {},
|
|
2383
|
+
bindingRevision: pending.bindingRevision,
|
|
2384
|
+
budget: pending.budget,
|
|
2385
|
+
budgetIdentity: pending.budgetIdentity,
|
|
2386
|
+
current: state.current,
|
|
2387
|
+
expectedRevision: pending.expectedRevision,
|
|
2388
|
+
inputDigest: pending.inputDigest,
|
|
2389
|
+
operation: pending.operation,
|
|
2390
|
+
phase: pending.phase,
|
|
2391
|
+
...state.previous ? { previous: state.previous } : {},
|
|
2392
|
+
...pending.principal ? { principal: pending.principal } : {},
|
|
2393
|
+
requestId: pending.request.requestId,
|
|
2394
|
+
revision: state.revision,
|
|
2395
|
+
status: "pending",
|
|
2396
|
+
...pending.target ? { target: pending.target } : {},
|
|
2397
|
+
updatedAt
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2400
|
+
function failedReceipt(state, pending, error, updatedAt) {
|
|
2401
|
+
return {
|
|
2402
|
+
...pendingReceipt(state, pending, updatedAt),
|
|
2403
|
+
error,
|
|
2404
|
+
phase: error.outcome === "unknown" ? "validation-unknown" : "completed",
|
|
2405
|
+
status: "failed"
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
function restoredReceipt(state, pending, error, updatedAt) {
|
|
2409
|
+
return {
|
|
2410
|
+
...pendingReceipt(state, pending, updatedAt),
|
|
2411
|
+
...error ? { error } : {},
|
|
2412
|
+
phase: "completed",
|
|
2413
|
+
status: "restored"
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2416
|
+
function recoveryReceipt(state, pending, error, updatedAt) {
|
|
2417
|
+
return {
|
|
2418
|
+
...pendingReceipt(state, pending, updatedAt),
|
|
2419
|
+
error,
|
|
2420
|
+
phase: "recovery-required",
|
|
2421
|
+
status: "recovery-required"
|
|
2422
|
+
};
|
|
2423
|
+
}
|
|
2424
|
+
function toStatusView(state, actual, receipt) {
|
|
2425
|
+
return {
|
|
2426
|
+
...actual ? { actual } : {},
|
|
2427
|
+
current: state.current,
|
|
2428
|
+
homeId: state.homeId,
|
|
2429
|
+
...state.pending ? { pending: {
|
|
2430
|
+
phase: state.pending.phase,
|
|
2431
|
+
request: stripTransportContext(state.pending.request),
|
|
2432
|
+
...state.pending.target ? { target: state.pending.target } : {},
|
|
2433
|
+
updatedAt: state.pending.updatedAt
|
|
2434
|
+
} } : {},
|
|
2435
|
+
...state.previous ? { previous: state.previous } : {},
|
|
2436
|
+
...receipt ? { request: toPublicReceipt(receipt) } : {},
|
|
2437
|
+
persisted: state.current,
|
|
2438
|
+
...state.recoveryRequired ? { recoveryRequired: state.recoveryRequired } : {},
|
|
2439
|
+
revision: state.revision,
|
|
2440
|
+
source: "managed"
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
function toPublicReceipt(receipt) {
|
|
2444
|
+
return {
|
|
2445
|
+
acceptedAt: receipt.acceptedAt,
|
|
2446
|
+
budget: {
|
|
2447
|
+
deadlineAt: receipt.budget.deadlineAt,
|
|
2448
|
+
maxOutputTokens: receipt.budget.maxOutputTokens,
|
|
2449
|
+
maxPaidRequests: receipt.budget.maxPaidRequests,
|
|
2450
|
+
outputTokens: receipt.budget.outputTokens,
|
|
2451
|
+
paidRequests: receipt.budget.paidRequests
|
|
2452
|
+
},
|
|
2453
|
+
...receipt.current ? { current: receipt.current } : {},
|
|
2454
|
+
...receipt.error ? { error: receipt.error } : {},
|
|
2455
|
+
expectedRevision: receipt.expectedRevision,
|
|
2456
|
+
operation: receipt.operation,
|
|
2457
|
+
phase: receipt.phase,
|
|
2458
|
+
...receipt.previous ? { previous: receipt.previous } : {},
|
|
2459
|
+
requestId: receipt.requestId,
|
|
2460
|
+
revision: receipt.revision,
|
|
2461
|
+
status: receipt.status,
|
|
2462
|
+
...receipt.target ? { target: receipt.target } : {},
|
|
2463
|
+
updatedAt: receipt.updatedAt
|
|
2464
|
+
};
|
|
2465
|
+
}
|
|
2466
|
+
function resolveTarget(state, submission) {
|
|
2467
|
+
if (submission.operation === "set") return submission.target ? toReference(submission.target) : void 0;
|
|
2468
|
+
return state.previous ? toReference(state.previous) : void 0;
|
|
2469
|
+
}
|
|
2470
|
+
function runtimeContext(state, pending) {
|
|
2471
|
+
if (!pending.target) throw new Error("Model change target is unavailable");
|
|
2472
|
+
return {
|
|
2473
|
+
baseline: state.current,
|
|
2474
|
+
bindingRevision: pending.bindingRevision,
|
|
2475
|
+
requestId: pending.request.requestId,
|
|
2476
|
+
target: pending.target
|
|
2477
|
+
};
|
|
2478
|
+
}
|
|
2479
|
+
function sameSubmission(receipt, submission) {
|
|
2480
|
+
return receipt.requestId === submission.requestId && receipt.operation === submission.operation && receipt.expectedRevision === submission.expectedRevision && receipt.target?.provider === submission.target?.provider && receipt.target?.model === submission.target?.model;
|
|
2481
|
+
}
|
|
2482
|
+
function sameTarget(binding, target) {
|
|
2483
|
+
return binding.provider === target.provider && binding.model === target.model;
|
|
2484
|
+
}
|
|
2485
|
+
function toReference(binding) {
|
|
2486
|
+
return {
|
|
2487
|
+
model: binding.model,
|
|
2488
|
+
provider: binding.provider
|
|
2489
|
+
};
|
|
2490
|
+
}
|
|
2491
|
+
function stripTransportContext(submission) {
|
|
2492
|
+
return {
|
|
2493
|
+
expectedRevision: submission.expectedRevision,
|
|
2494
|
+
operation: submission.operation,
|
|
2495
|
+
requestId: submission.requestId,
|
|
2496
|
+
...submission.target ? { target: toReference(submission.target) } : {}
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
//#endregion
|
|
2500
|
+
//#region src/core/application/deployment/model/model-change-support.ts
|
|
2501
|
+
function loadState(options) {
|
|
2502
|
+
return options.repository.load().pipe(Effect.flatMap((state) => state.homeId === options.homeId ? Effect.succeed(state) : Effect.fail(/* @__PURE__ */ new Error(`Model state belongs to unexpected Home: ${state.homeId}`))));
|
|
2503
|
+
}
|
|
2504
|
+
function persistState(options, state) {
|
|
2505
|
+
return options.repository.persist(state);
|
|
2506
|
+
}
|
|
2507
|
+
function terminalReceiptForPersistence(options, receipt) {
|
|
2508
|
+
return options.notification && receipt.notification === void 0 ? {
|
|
2509
|
+
...receipt,
|
|
2510
|
+
notification: {
|
|
2511
|
+
attempts: 0,
|
|
2512
|
+
status: "pending"
|
|
2513
|
+
}
|
|
2514
|
+
} : receipt;
|
|
2515
|
+
}
|
|
2516
|
+
function requireNow(options) {
|
|
2517
|
+
return requireTimestamp(options.clock.now());
|
|
2518
|
+
}
|
|
2519
|
+
function requireTimestamp(value) {
|
|
2520
|
+
if (!Number.isFinite(Date.parse(value))) throw new Error("Model change clock must return an ISO timestamp");
|
|
2521
|
+
return value;
|
|
2522
|
+
}
|
|
2523
|
+
function modelError(code, message, stage, outcome = "known") {
|
|
2524
|
+
return {
|
|
2525
|
+
code,
|
|
2526
|
+
message,
|
|
2527
|
+
outcome,
|
|
2528
|
+
stage
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
function modelErrorFrom(error, stage) {
|
|
2532
|
+
const outcome = readOutcome(error);
|
|
2533
|
+
const code = readControlledCode(error, stage);
|
|
2534
|
+
return modelError(code, safeStageMessage(stage, outcome, code), stage, outcome);
|
|
2535
|
+
}
|
|
2536
|
+
function readOutcome(error) {
|
|
2537
|
+
if (typeof error === "object" && error !== null && "outcome" in error && (error.outcome === "known" || error.outcome === "unknown")) return error.outcome;
|
|
2538
|
+
return "known";
|
|
2539
|
+
}
|
|
2540
|
+
/** Only application-owned codes may cross the persistence/public projection boundary. */
|
|
2541
|
+
function readControlledCode(error, stage) {
|
|
2542
|
+
if (typeof error !== "object" || error === null) return `model_change_${stage}_failed`;
|
|
2543
|
+
if ("name" in error && error.name === "ModelChangeBudgetExceeded") return "model_change_budget_exhausted";
|
|
2544
|
+
if ("name" in error && error.name === "ModelChangeBoundaryDeadlineError") return "model_change_deadline_exceeded";
|
|
2545
|
+
if ("code" in error && typeof error.code === "string" && CONTROLLED_ERROR_CODES.has(error.code)) return error.code;
|
|
2546
|
+
return `model_change_${stage}_failed`;
|
|
2547
|
+
}
|
|
2548
|
+
const CONTROLLED_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
2549
|
+
"model_change_activation_failed",
|
|
2550
|
+
"model_change_boundary_failed",
|
|
2551
|
+
"model_change_commit_failed",
|
|
2552
|
+
"model_change_deadline_exceeded",
|
|
2553
|
+
"model_change_notification_failed",
|
|
2554
|
+
"model_change_probe_failed",
|
|
2555
|
+
"model_change_restore_failed",
|
|
2556
|
+
"model_change_validation_failed",
|
|
2557
|
+
"model_change_target_unsupported",
|
|
2558
|
+
"model_change_provider_unsupported",
|
|
2559
|
+
"model_change_history_incompatible",
|
|
2560
|
+
"model_change_budget_exhausted"
|
|
2561
|
+
]);
|
|
2562
|
+
function safeStageMessage(stage, outcome, code) {
|
|
2563
|
+
const suffix = outcome === "unknown" ? " The provider result is unknown and must not be retried." : "";
|
|
2564
|
+
const message = {
|
|
2565
|
+
activation: "The model activation failed.",
|
|
2566
|
+
boundary: "The model change could not reach a safe execution boundary.",
|
|
2567
|
+
commit: "The model state could not be committed.",
|
|
2568
|
+
notification: "The model change result could not be delivered.",
|
|
2569
|
+
restore: "The previous model could not be restored.",
|
|
2570
|
+
validation: "The target model did not pass validation."
|
|
2571
|
+
}[stage];
|
|
2572
|
+
return `${{
|
|
2573
|
+
model_change_target_unsupported: "The exact target model has no supported metadata in this runtime.",
|
|
2574
|
+
model_change_provider_unsupported: "The requested provider is unavailable in this runtime.",
|
|
2575
|
+
model_change_history_incompatible: "The existing conversation history is incompatible with the target model.",
|
|
2576
|
+
model_change_budget_exhausted: "The accepted model change budget is exhausted.",
|
|
2577
|
+
model_change_deadline_exceeded: "The accepted model change deadline has elapsed."
|
|
2578
|
+
}[code] ?? message}${suffix}`;
|
|
2579
|
+
}
|
|
2580
|
+
//#endregion
|
|
2581
|
+
//#region src/core/application/deployment/model/model-change-execution.ts
|
|
2582
|
+
function executeModelChangePending(options, initialState, initialPending) {
|
|
2583
|
+
if (!initialPending.target) return Effect.fail(/* @__PURE__ */ new Error("Model change target is unavailable"));
|
|
2584
|
+
let state = initialState;
|
|
2585
|
+
let pending = initialPending;
|
|
2586
|
+
const target = initialPending.target;
|
|
2587
|
+
let paidCallSequence = Object.keys(pending.budget.reservations).length;
|
|
2588
|
+
let boundaryHeld = false;
|
|
2589
|
+
const savePending = (next) => Effect.gen(function* () {
|
|
2590
|
+
const now = requireNow(options);
|
|
2591
|
+
pending = {
|
|
2592
|
+
...next,
|
|
2593
|
+
updatedAt: now
|
|
2594
|
+
};
|
|
2595
|
+
const receipt = pendingReceipt(state, pending, now);
|
|
2596
|
+
state = yield* persistState(options, stateWithPending(state, pending, receipt));
|
|
2597
|
+
return receipt;
|
|
2598
|
+
});
|
|
2599
|
+
const saveFinal = (nextReceipt, recoveryRequired = false) => Effect.gen(function* () {
|
|
2600
|
+
const { pending: _pending, recoveryRequired: _recoveryRequired, ...stateWithoutTransient } = state;
|
|
2601
|
+
const durableReceipt = terminalReceiptForPersistence(options, nextReceipt);
|
|
2602
|
+
state = yield* persistState(options, {
|
|
2603
|
+
...stateWithoutTransient,
|
|
2604
|
+
...recoveryRequired && durableReceipt.error ? { recoveryRequired: durableReceipt.error } : {},
|
|
2605
|
+
requests: {
|
|
2606
|
+
...state.requests,
|
|
2607
|
+
[durableReceipt.requestId]: durableReceipt
|
|
2608
|
+
},
|
|
2609
|
+
updatedAt: durableReceipt.updatedAt
|
|
2610
|
+
});
|
|
2611
|
+
return durableReceipt;
|
|
2612
|
+
});
|
|
2613
|
+
const saveFailed = (error) => Effect.gen(function* () {
|
|
2614
|
+
const receipt = failedReceipt(state, pending, error, requireNow(options));
|
|
2615
|
+
state = yield* persistState(options, stateWithReceipt(state, terminalReceiptForPersistence(options, receipt)));
|
|
2616
|
+
return state.requests[receipt.requestId];
|
|
2617
|
+
});
|
|
2618
|
+
const executionBudget = {
|
|
2619
|
+
deadlineAt: pending.budget.deadlineAt,
|
|
2620
|
+
reservePaidCall: ({ kind, maxOutputTokens }) => Effect.gen(function* () {
|
|
2621
|
+
if (pending.phase === "validation-unknown" || pending.phase === "recovery-required") return yield* Effect.fail(/* @__PURE__ */ new Error("Model change cannot reserve a call after an unknown outcome"));
|
|
2622
|
+
const id = `${pending.request.requestId}:paid:${++paidCallSequence}`;
|
|
2623
|
+
const selected = reserveModelChangeBudget(pending.budget, {
|
|
2624
|
+
at: requireNow(options),
|
|
2625
|
+
id,
|
|
2626
|
+
kind,
|
|
2627
|
+
...maxOutputTokens === void 0 ? {} : { maxOutputTokens }
|
|
2628
|
+
});
|
|
2629
|
+
yield* savePending({
|
|
2630
|
+
...pending,
|
|
2631
|
+
budget: selected.budget
|
|
2632
|
+
});
|
|
2633
|
+
return {
|
|
2634
|
+
reservation: selected.reservation,
|
|
2635
|
+
settle: ({ outcome, outputTokens }) => settlePaidCall(() => pending, savePending, {
|
|
2636
|
+
id,
|
|
2637
|
+
kind,
|
|
2638
|
+
outcome,
|
|
2639
|
+
outputTokens
|
|
2640
|
+
})
|
|
2641
|
+
};
|
|
2642
|
+
})
|
|
2643
|
+
};
|
|
2644
|
+
const revalidateAcceptedBinding = () => Effect.gen(function* () {
|
|
2645
|
+
if (!pending.authorizationId || !pending.principal) return {
|
|
2646
|
+
code: "authorization_incomplete",
|
|
2647
|
+
reason: "The accepted Model change has no trusted authorization binding.",
|
|
2648
|
+
status: "rejected"
|
|
2649
|
+
};
|
|
2650
|
+
const decision = yield* options.authorization.revalidate({
|
|
2651
|
+
authorizationId: pending.authorizationId,
|
|
2652
|
+
current: state.current,
|
|
2653
|
+
homeId: options.homeId,
|
|
2654
|
+
pending
|
|
2655
|
+
});
|
|
2656
|
+
if (decision.status === "rejected") return {
|
|
2657
|
+
code: "authorization_revoked",
|
|
2658
|
+
reason: decision.reason,
|
|
2659
|
+
status: "rejected"
|
|
2660
|
+
};
|
|
2661
|
+
if (decision.bindingRevision !== pending.bindingRevision || state.current.bindingRevision !== pending.bindingRevision || state.revision !== pending.expectedRevision) return {
|
|
2662
|
+
code: "binding_changed",
|
|
2663
|
+
reason: "Model revision or provider binding changed before Model apply",
|
|
2664
|
+
status: "rejected"
|
|
2665
|
+
};
|
|
2666
|
+
return {
|
|
2667
|
+
bindingRevision: decision.bindingRevision,
|
|
2668
|
+
status: "approved"
|
|
2669
|
+
};
|
|
2670
|
+
});
|
|
2671
|
+
const finish = (effect) => Effect.exit(effect).pipe(Effect.flatMap((exit) => Effect.gen(function* () {
|
|
2672
|
+
const requestId = pending.request.requestId;
|
|
2673
|
+
const finalReceipt = state.requests[requestId];
|
|
2674
|
+
const recoveryRequired = state.recoveryRequired !== void 0 || state.pending?.phase === "recovery-required" || finalReceipt?.status === "recovery-required";
|
|
2675
|
+
const durableTerminalState = state.pending === void 0 && finalReceipt !== void 0 && (finalReceipt.status === "applied" || finalReceipt.status === "failed" || finalReceipt.status === "restored");
|
|
2676
|
+
if (boundaryHeld && durableTerminalState && !recoveryRequired) yield* options.boundary.resumeAfterChange({ requestId });
|
|
2677
|
+
if (exit._tag === "Failure") return yield* Effect.failCause(exit.cause);
|
|
2678
|
+
return exit.value;
|
|
2679
|
+
})));
|
|
2680
|
+
return finish(Effect.gen(function* () {
|
|
2681
|
+
yield* savePending({
|
|
2682
|
+
...pending,
|
|
2683
|
+
phase: "waiting-for-boundary"
|
|
2684
|
+
});
|
|
2685
|
+
boundaryHeld = true;
|
|
2686
|
+
const boundary = yield* Effect.either(options.boundary.waitForSafeBoundary({
|
|
2687
|
+
deadlineAt: pending.budget.deadlineAt,
|
|
2688
|
+
requestId: pending.request.requestId
|
|
2689
|
+
}));
|
|
2690
|
+
if (Either.isLeft(boundary)) return yield* saveFailed(modelErrorFrom(boundary.left, "boundary"));
|
|
2691
|
+
const authorization = yield* Effect.either(revalidateAcceptedBinding());
|
|
2692
|
+
if (Either.isLeft(authorization)) return yield* saveFailed(modelErrorFrom(authorization.left, "boundary"));
|
|
2693
|
+
if (authorization.right.status === "rejected") return yield* saveFailed(modelError(authorization.right.code, authorization.right.reason, "boundary"));
|
|
2694
|
+
yield* savePending({
|
|
2695
|
+
...pending,
|
|
2696
|
+
phase: "validating"
|
|
2697
|
+
});
|
|
2698
|
+
const context = runtimeContext(state, pending);
|
|
2699
|
+
const drained = yield* Effect.either(options.runtime.drain({
|
|
2700
|
+
...context,
|
|
2701
|
+
deadlineAt: pending.budget.deadlineAt
|
|
2702
|
+
}));
|
|
2703
|
+
if (Either.isLeft(drained)) return yield* restoreAfterFailure({
|
|
2704
|
+
context,
|
|
2705
|
+
error: modelErrorFrom(drained.left, "activation"),
|
|
2706
|
+
options,
|
|
2707
|
+
saveFinal,
|
|
2708
|
+
savePending,
|
|
2709
|
+
state: () => state,
|
|
2710
|
+
pending: () => pending
|
|
2711
|
+
});
|
|
2712
|
+
const validated = yield* Effect.either(options.runtime.validate({
|
|
2713
|
+
context,
|
|
2714
|
+
budget: executionBudget
|
|
2715
|
+
}));
|
|
2716
|
+
if (Either.isLeft(validated)) return yield* restoreAfterFailure({
|
|
2717
|
+
context,
|
|
2718
|
+
error: modelErrorFrom(validated.left, "validation"),
|
|
2719
|
+
options,
|
|
2720
|
+
saveFinal,
|
|
2721
|
+
savePending,
|
|
2722
|
+
state: () => state,
|
|
2723
|
+
pending: () => pending
|
|
2724
|
+
});
|
|
2725
|
+
if (!validated.right.passed || !sameTarget(validated.right.model, target)) return yield* restoreAfterFailure({
|
|
2726
|
+
context,
|
|
2727
|
+
error: modelError("validation_failed", "Model validation did not prove the requested target", "validation"),
|
|
2728
|
+
options,
|
|
2729
|
+
saveFinal,
|
|
2730
|
+
savePending,
|
|
2731
|
+
state: () => state,
|
|
2732
|
+
pending: () => pending
|
|
2733
|
+
});
|
|
2734
|
+
const activationAuthorization = yield* Effect.either(revalidateAcceptedBinding());
|
|
2735
|
+
if (Either.isLeft(activationAuthorization)) return yield* saveFailed(modelErrorFrom(activationAuthorization.left, "activation"));
|
|
2736
|
+
if (activationAuthorization.right.status === "rejected") return yield* saveFailed(modelError(activationAuthorization.right.code, activationAuthorization.right.reason, "activation"));
|
|
2737
|
+
yield* savePending({
|
|
2738
|
+
...pending,
|
|
2739
|
+
phase: "activating"
|
|
2740
|
+
});
|
|
2741
|
+
const activated = yield* Effect.either(options.runtime.activate({
|
|
2742
|
+
beforeApply: () => Effect.gen(function* () {
|
|
2743
|
+
const guard = yield* revalidateAcceptedBinding();
|
|
2744
|
+
if (guard.status === "rejected") return yield* Effect.fail(new Error(guard.reason));
|
|
2745
|
+
}),
|
|
2746
|
+
context,
|
|
2747
|
+
validation: validated.right,
|
|
2748
|
+
budget: executionBudget
|
|
2749
|
+
}));
|
|
2750
|
+
if (Either.isLeft(activated)) return yield* restoreAfterFailure({
|
|
2751
|
+
context,
|
|
2752
|
+
error: modelErrorFrom(activated.left, "activation"),
|
|
2753
|
+
options,
|
|
2754
|
+
saveFinal,
|
|
2755
|
+
savePending,
|
|
2756
|
+
state: () => state,
|
|
2757
|
+
pending: () => pending
|
|
2758
|
+
});
|
|
2759
|
+
if (Object.keys(pending.budget.reservations).length > 0) return yield* restoreAfterFailure({
|
|
2760
|
+
context,
|
|
2761
|
+
error: modelError("paid_call_unsettled", "Model runtime returned with an unsettled paid call", "activation"),
|
|
2762
|
+
options,
|
|
2763
|
+
saveFinal,
|
|
2764
|
+
savePending,
|
|
2765
|
+
state: () => state,
|
|
2766
|
+
pending: () => pending
|
|
2767
|
+
});
|
|
2768
|
+
yield* savePending({
|
|
2769
|
+
...pending,
|
|
2770
|
+
phase: "committing"
|
|
2771
|
+
});
|
|
2772
|
+
const now = requireNow(options);
|
|
2773
|
+
const applied = {
|
|
2774
|
+
...pendingReceipt(state, pending, now),
|
|
2775
|
+
current: validated.right.model,
|
|
2776
|
+
phase: options.notification ? "notifying" : "completed",
|
|
2777
|
+
previous: state.current,
|
|
2778
|
+
revision: state.revision + 1,
|
|
2779
|
+
status: "applied",
|
|
2780
|
+
...options.notification ? { notification: {
|
|
2781
|
+
attempts: 0,
|
|
2782
|
+
status: "pending"
|
|
2783
|
+
} } : {}
|
|
2784
|
+
};
|
|
2785
|
+
const { pending: _pending, recoveryRequired: _recoveryRequired, ...stateWithoutTransient } = state;
|
|
2786
|
+
const appliedState = {
|
|
2787
|
+
...stateWithoutTransient,
|
|
2788
|
+
current: validated.right.model,
|
|
2789
|
+
previous: state.current,
|
|
2790
|
+
requests: {
|
|
2791
|
+
...state.requests,
|
|
2792
|
+
[pending.request.requestId]: applied
|
|
2793
|
+
},
|
|
2794
|
+
revision: state.revision + 1,
|
|
2795
|
+
updatedAt: now
|
|
2796
|
+
};
|
|
2797
|
+
const committed = yield* Effect.either(persistState(options, appliedState));
|
|
2798
|
+
if (Either.isLeft(committed)) return yield* restoreAfterFailure({
|
|
2799
|
+
context,
|
|
2800
|
+
error: modelErrorFrom(committed.left, "commit"),
|
|
2801
|
+
options,
|
|
2802
|
+
saveFinal,
|
|
2803
|
+
savePending,
|
|
2804
|
+
state: () => state,
|
|
2805
|
+
pending: () => pending
|
|
2806
|
+
});
|
|
2807
|
+
state = committed.right;
|
|
2808
|
+
pending = {
|
|
2809
|
+
...pending,
|
|
2810
|
+
phase: "completed",
|
|
2811
|
+
updatedAt: now
|
|
2812
|
+
};
|
|
2813
|
+
const release = yield* Effect.either(options.runtime.releasePrevious({
|
|
2814
|
+
...context,
|
|
2815
|
+
deadlineAt: pending.budget.deadlineAt
|
|
2816
|
+
}));
|
|
2817
|
+
if (Either.isLeft(release)) {
|
|
2818
|
+
const releaseError = modelErrorFrom(release.left, "activation");
|
|
2819
|
+
const withReleaseError = {
|
|
2820
|
+
...applied,
|
|
2821
|
+
error: releaseError,
|
|
2822
|
+
updatedAt: requireNow(options)
|
|
2823
|
+
};
|
|
2824
|
+
state = yield* persistState(options, {
|
|
2825
|
+
...state,
|
|
2826
|
+
requests: {
|
|
2827
|
+
...state.requests,
|
|
2828
|
+
[pending.request.requestId]: withReleaseError
|
|
2829
|
+
},
|
|
2830
|
+
updatedAt: withReleaseError.updatedAt
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
2833
|
+
if (!options.notification) return state.requests[pending.request.requestId];
|
|
2834
|
+
return state.requests[pending.request.requestId];
|
|
2835
|
+
}));
|
|
2836
|
+
}
|
|
2837
|
+
function restoreAfterFailure(input) {
|
|
2838
|
+
return Effect.gen(function* () {
|
|
2839
|
+
const pending = input.pending();
|
|
2840
|
+
if (pending.recoveryAttemptedAt) {
|
|
2841
|
+
const recovery = recoveryReceipt(input.state(), pending, modelError("recovery_already_attempted", input.error.message, "restore"), requireNow(input.options));
|
|
2842
|
+
return yield* input.saveFinal(recovery, true);
|
|
2843
|
+
}
|
|
2844
|
+
yield* input.savePending({
|
|
2845
|
+
...pending,
|
|
2846
|
+
phase: "recovery-required",
|
|
2847
|
+
recoveryAttemptedAt: requireNow(input.options)
|
|
2848
|
+
});
|
|
2849
|
+
const marked = input.pending();
|
|
2850
|
+
if (marked.unknownPaidCallId) {
|
|
2851
|
+
const { unknownPaidCallId: _unknownPaidCallId, ...withoutUnknown } = marked;
|
|
2852
|
+
yield* input.savePending({ ...withoutUnknown });
|
|
2853
|
+
}
|
|
2854
|
+
const restored = yield* Effect.either(input.options.runtime.restore({
|
|
2855
|
+
budget: executionBudgetForRecovery(input),
|
|
2856
|
+
context: input.context,
|
|
2857
|
+
reason: input.error.message
|
|
2858
|
+
}));
|
|
2859
|
+
if (Either.isRight(restored) && !input.pending().unknownPaidCallId && Object.keys(input.pending().budget.reservations).length === 0) {
|
|
2860
|
+
const receipt = restoredReceipt(input.state(), input.pending(), input.error, requireNow(input.options));
|
|
2861
|
+
return yield* input.saveFinal(receipt);
|
|
2862
|
+
}
|
|
2863
|
+
const recoveryError = Either.isLeft(restored) ? modelErrorFrom(restored.left, "restore") : modelError("recovery_unknown", "Model restore outcome is unknown", "restore", "unknown");
|
|
2864
|
+
const receipt = recoveryReceipt(input.state(), input.pending(), recoveryError, requireNow(input.options));
|
|
2865
|
+
return yield* input.saveFinal(receipt, true);
|
|
2866
|
+
});
|
|
2867
|
+
}
|
|
2868
|
+
function executionBudgetForRecovery(input) {
|
|
2869
|
+
let sequence = Object.keys(input.pending().budget.reservations).length;
|
|
2870
|
+
return {
|
|
2871
|
+
get deadlineAt() {
|
|
2872
|
+
return input.pending().budget.deadlineAt;
|
|
2873
|
+
},
|
|
2874
|
+
reservePaidCall: ({ kind, maxOutputTokens }) => Effect.gen(function* () {
|
|
2875
|
+
if (kind !== "restore") return yield* Effect.fail(/* @__PURE__ */ new Error("recovery may only reserve a restore call"));
|
|
2876
|
+
const pending = input.pending();
|
|
2877
|
+
const id = `${pending.request.requestId}:recovery:${++sequence}`;
|
|
2878
|
+
const selected = reserveModelChangeBudget(pending.budget, {
|
|
2879
|
+
at: requireNow(input.options),
|
|
2880
|
+
id,
|
|
2881
|
+
kind,
|
|
2882
|
+
...maxOutputTokens === void 0 ? {} : { maxOutputTokens }
|
|
2883
|
+
});
|
|
2884
|
+
yield* input.savePending({
|
|
2885
|
+
...pending,
|
|
2886
|
+
budget: selected.budget,
|
|
2887
|
+
phase: "recovery-required"
|
|
2888
|
+
});
|
|
2889
|
+
return {
|
|
2890
|
+
reservation: selected.reservation,
|
|
2891
|
+
settle: ({ outcome, outputTokens }) => settlePaidCall(input.pending, input.savePending, {
|
|
2892
|
+
id,
|
|
2893
|
+
kind,
|
|
2894
|
+
outcome,
|
|
2895
|
+
outputTokens
|
|
2896
|
+
})
|
|
2897
|
+
};
|
|
2898
|
+
})
|
|
2899
|
+
};
|
|
2900
|
+
}
|
|
2901
|
+
function settlePaidCall(getPending, savePending, input) {
|
|
2902
|
+
const nextPending = settleModelChangePaidCall(getPending(), input);
|
|
2903
|
+
return savePending(nextPending).pipe(Effect.map(() => nextPending.budget));
|
|
2904
|
+
}
|
|
2905
|
+
//#endregion
|
|
2906
|
+
//#region src/core/application/deployment/model/model-change-recovery-workflow.ts
|
|
2907
|
+
function recoverModelChangeState(options) {
|
|
2908
|
+
return Effect.gen(function* () {
|
|
2909
|
+
let state = yield* loadState(options);
|
|
2910
|
+
if (state.pending) state = yield* recoverModelChangePending(options, state, state.pending);
|
|
2911
|
+
return state;
|
|
2912
|
+
});
|
|
2913
|
+
}
|
|
2914
|
+
function recoverModelChangePending(options, initialState, initialPending) {
|
|
2915
|
+
let state = initialState;
|
|
2916
|
+
let pending = initialPending;
|
|
2917
|
+
return Effect.gen(function* () {
|
|
2918
|
+
if (pending.phase === "accepted" || pending.phase === "awaiting-approval" || pending.phase === "waiting-for-boundary") {
|
|
2919
|
+
if (Object.keys(pending.budget.reservations).length === 0) {
|
|
2920
|
+
const error = modelError("interrupted_before_verification", "Model change stopped before verification; the persisted current model was retained", "boundary");
|
|
2921
|
+
const receipt = failedReceipt(state, pending, error, requireNow(options));
|
|
2922
|
+
return yield* persistRecoveryReceipt(options, state, receipt);
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
if (pending.recoveryAttemptedAt) {
|
|
2926
|
+
const settled = yield* settlePendingReservations(options, state, pending, () => "recovery-required");
|
|
2927
|
+
state = settled.state;
|
|
2928
|
+
pending = settled.pending;
|
|
2929
|
+
const restoreReservation = settled.restoreReservation;
|
|
2930
|
+
const error = modelError(restoreReservation ? "restore_outcome_unknown" : "recovery_already_attempted", restoreReservation ? "The restore call outcome is unknown; restore was not replayed" : "Model recovery was already attempted", "restore", restoreReservation ? "unknown" : "known");
|
|
2931
|
+
const receipt = recoveryReceipt(state, pending, error, requireNow(options));
|
|
2932
|
+
return yield* persistRecoveryReceipt(options, state, receipt, error);
|
|
2933
|
+
}
|
|
2934
|
+
const settled = yield* settlePendingReservations(options, state, pending, (reservation) => reservation.kind === "restore" ? "recovery-required" : "validation-unknown");
|
|
2935
|
+
state = settled.state;
|
|
2936
|
+
pending = settled.pending;
|
|
2937
|
+
const restoreReservation = settled.restoreReservation;
|
|
2938
|
+
if (pending.unknownPaidCallId) {
|
|
2939
|
+
const { unknownPaidCallId: _unknownPaidCallId, ...withoutUnknown } = pending;
|
|
2940
|
+
pending = {
|
|
2941
|
+
...withoutUnknown,
|
|
2942
|
+
updatedAt: requireNow(options)
|
|
2943
|
+
};
|
|
2944
|
+
state = yield* persistRecoveryPending(options, state, pending);
|
|
2945
|
+
}
|
|
2946
|
+
if (restoreReservation) {
|
|
2947
|
+
const error = modelError("restore_outcome_unknown", "The restore call outcome is unknown; restore was not replayed", "restore", "unknown");
|
|
2948
|
+
const receipt = recoveryReceipt(state, pending, error, requireNow(options));
|
|
2949
|
+
return yield* persistRecoveryReceipt(options, state, receipt, error);
|
|
2950
|
+
}
|
|
2951
|
+
const context = pending.target ? runtimeContext(state, pending) : {
|
|
2952
|
+
baseline: state.current,
|
|
2953
|
+
bindingRevision: pending.bindingRevision,
|
|
2954
|
+
requestId: pending.request.requestId,
|
|
2955
|
+
target: toReference(state.current)
|
|
2956
|
+
};
|
|
2957
|
+
pending = {
|
|
2958
|
+
...pending,
|
|
2959
|
+
phase: "recovery-required",
|
|
2960
|
+
recoveryAttemptedAt: requireNow(options),
|
|
2961
|
+
updatedAt: requireNow(options)
|
|
2962
|
+
};
|
|
2963
|
+
state = yield* persistState(options, stateWithPending(state, pending, pendingReceipt(state, pending, pending.updatedAt)));
|
|
2964
|
+
const restored = yield* Effect.either(options.runtime.restore({
|
|
2965
|
+
budget: executionBudgetForRecovery({
|
|
2966
|
+
options,
|
|
2967
|
+
pending: () => pending,
|
|
2968
|
+
savePending: (next) => Effect.gen(function* () {
|
|
2969
|
+
pending = next;
|
|
2970
|
+
const receipt = pendingReceipt(state, pending, pending.updatedAt);
|
|
2971
|
+
state = yield* persistState(options, stateWithPending(state, pending, receipt));
|
|
2972
|
+
return receipt;
|
|
2973
|
+
})
|
|
2974
|
+
}),
|
|
2975
|
+
context,
|
|
2976
|
+
reason: "recovering an interrupted Model change"
|
|
2977
|
+
}));
|
|
2978
|
+
if (Either.isRight(restored) && !pending.unknownPaidCallId && Object.keys(pending.budget.reservations).length === 0) {
|
|
2979
|
+
const receipt = restoredReceipt(state, pending, void 0, requireNow(options));
|
|
2980
|
+
return yield* persistRecoveryReceipt(options, state, receipt);
|
|
2981
|
+
}
|
|
2982
|
+
const error = Either.isLeft(restored) ? modelErrorFrom(restored.left, "restore") : modelError("recovery_unknown", "interrupted Model restore outcome is unknown", "restore", "unknown");
|
|
2983
|
+
const receipt = recoveryReceipt(state, pending, error, requireNow(options));
|
|
2984
|
+
return yield* persistRecoveryReceipt(options, state, receipt, error);
|
|
2985
|
+
});
|
|
2986
|
+
}
|
|
2987
|
+
function persistRecoveryReceipt(options, state, receipt, error) {
|
|
2988
|
+
return persistState(options, stateWithReceipt(state, terminalReceiptForPersistence(options, receipt), error));
|
|
2989
|
+
}
|
|
2990
|
+
function persistRecoveryPending(options, state, pending) {
|
|
2991
|
+
return persistState(options, stateWithPending(state, pending, pendingReceipt(state, pending, pending.updatedAt)));
|
|
2992
|
+
}
|
|
2993
|
+
function markUnknownReservation(options, pending, reservation, phase) {
|
|
2994
|
+
return {
|
|
2995
|
+
...settleModelChangePaidCall(pending, {
|
|
2996
|
+
id: reservation.id,
|
|
2997
|
+
kind: reservation.kind,
|
|
2998
|
+
outcome: "unknown",
|
|
2999
|
+
outputTokens: reservation.maxOutputTokens
|
|
3000
|
+
}),
|
|
3001
|
+
phase,
|
|
3002
|
+
updatedAt: requireNow(options)
|
|
3003
|
+
};
|
|
3004
|
+
}
|
|
3005
|
+
function settlePendingReservations(options, initialState, initialPending, phaseFor) {
|
|
3006
|
+
let state = initialState;
|
|
3007
|
+
let pending = initialPending;
|
|
3008
|
+
let restoreReservation = false;
|
|
3009
|
+
return Effect.gen(function* () {
|
|
3010
|
+
for (const reservation of Object.values(pending.budget.reservations)) {
|
|
3011
|
+
restoreReservation ||= reservation.kind === "restore";
|
|
3012
|
+
pending = markUnknownReservation(options, pending, reservation, phaseFor(reservation));
|
|
3013
|
+
state = yield* persistRecoveryPending(options, state, pending);
|
|
3014
|
+
}
|
|
3015
|
+
return {
|
|
3016
|
+
pending,
|
|
3017
|
+
restoreReservation,
|
|
3018
|
+
state
|
|
3019
|
+
};
|
|
3020
|
+
});
|
|
3021
|
+
}
|
|
3022
|
+
//#endregion
|
|
3023
|
+
//#region src/core/application/deployment/model/model-change-notification.ts
|
|
3024
|
+
/**
|
|
3025
|
+
* Deliver one durable terminal receipt. Each state update reloads the latest
|
|
3026
|
+
* model state under the coordinator's serial writer and patches only the
|
|
3027
|
+
* target receipt, so a delayed notification cannot restore an old current
|
|
3028
|
+
* binding, revision, or sibling request.
|
|
3029
|
+
*/
|
|
3030
|
+
function notifyModelChangeReceipt(options, requestId, retry, withModelSerial) {
|
|
3031
|
+
const notification = options.notification;
|
|
3032
|
+
if (!notification) return Effect.succeed(void 0);
|
|
3033
|
+
return Effect.gen(function* () {
|
|
3034
|
+
const attempted = yield* withModelSerial(beginNotificationAttempt(options, requestId));
|
|
3035
|
+
if (!attempted) return void 0;
|
|
3036
|
+
return yield* withModelSerial(finishNotificationAttempt(options, requestId, attempted, yield* Effect.either(notification.notify({
|
|
3037
|
+
receipt: toPublicReceipt(attempted),
|
|
3038
|
+
retry
|
|
3039
|
+
}))));
|
|
3040
|
+
});
|
|
3041
|
+
}
|
|
3042
|
+
function beginNotificationAttempt(options, requestId) {
|
|
3043
|
+
return Effect.gen(function* () {
|
|
3044
|
+
const state = yield* loadState(options);
|
|
3045
|
+
const receipt = state.requests[requestId];
|
|
3046
|
+
if (!receipt || receipt.status === "pending" || !receipt.notification || receipt.notification.status === "sent") return;
|
|
3047
|
+
const attemptedAt = requireNow(options);
|
|
3048
|
+
return (yield* persistReceiptPatch(options, state, {
|
|
3049
|
+
...receipt,
|
|
3050
|
+
notification: {
|
|
3051
|
+
attempts: receipt.notification.attempts + 1,
|
|
3052
|
+
lastAttemptAt: attemptedAt,
|
|
3053
|
+
status: "pending"
|
|
3054
|
+
},
|
|
3055
|
+
updatedAt: attemptedAt
|
|
3056
|
+
})).requests[requestId];
|
|
3057
|
+
});
|
|
3058
|
+
}
|
|
3059
|
+
function finishNotificationAttempt(options, requestId, attempted, delivered) {
|
|
3060
|
+
return Effect.gen(function* () {
|
|
3061
|
+
const state = yield* loadState(options);
|
|
3062
|
+
const latest = state.requests[requestId];
|
|
3063
|
+
if (!latest || !latest.notification || latest.notification.status === "sent") return latest;
|
|
3064
|
+
const updatedAt = requireNow(options);
|
|
3065
|
+
return (yield* persistReceiptPatch(options, state, Either.isRight(delivered) ? {
|
|
3066
|
+
...latest,
|
|
3067
|
+
notification: {
|
|
3068
|
+
...latest.notification,
|
|
3069
|
+
status: "sent"
|
|
3070
|
+
},
|
|
3071
|
+
...latest.status === "applied" ? { phase: "completed" } : {},
|
|
3072
|
+
updatedAt
|
|
3073
|
+
} : {
|
|
3074
|
+
...latest,
|
|
3075
|
+
notification: {
|
|
3076
|
+
...latest.notification,
|
|
3077
|
+
attempts: Math.max(latest.notification.attempts, attempted.notification?.attempts ?? 0),
|
|
3078
|
+
status: "unknown"
|
|
3079
|
+
},
|
|
3080
|
+
updatedAt
|
|
3081
|
+
})).requests[requestId];
|
|
3082
|
+
});
|
|
3083
|
+
}
|
|
3084
|
+
function persistReceiptPatch(options, state, receipt) {
|
|
3085
|
+
return persistState(options, {
|
|
3086
|
+
...state,
|
|
3087
|
+
requests: {
|
|
3088
|
+
...state.requests,
|
|
3089
|
+
[receipt.requestId]: receipt
|
|
3090
|
+
},
|
|
3091
|
+
updatedAt: receipt.updatedAt
|
|
3092
|
+
});
|
|
3093
|
+
}
|
|
3094
|
+
//#endregion
|
|
3095
|
+
//#region src/core/application/deployment/model/model-change-coordinator.ts
|
|
3096
|
+
function createModelChangeCoordinator(options) {
|
|
3097
|
+
const serial = Effect.unsafeMakeSemaphore(1);
|
|
3098
|
+
const notificationSerial = Effect.unsafeMakeSemaphore(1);
|
|
3099
|
+
return {
|
|
3100
|
+
accept: (submission, transport) => serial.withPermits(1)(Effect.uninterruptible(acceptRequest(options, submission, transport))).pipe(Effect.flatMap((receipt) => notifyPendingReceipt(options, receipt, serial, notificationSerial))),
|
|
3101
|
+
recover: () => serial.withPermits(1)(Effect.uninterruptible(recoverModelChangeState(options))).pipe(Effect.flatMap(() => retryPendingNotifications(options, serial, notificationSerial))),
|
|
3102
|
+
run: (requestId) => Effect.gen(function* () {
|
|
3103
|
+
const initial = yield* loadState(options);
|
|
3104
|
+
const decision = initial.pending?.request.requestId === requestId && initial.pending.phase === "awaiting-approval" && options.authorization.resolvePending ? yield* options.authorization.resolvePending({
|
|
3105
|
+
current: initial.current,
|
|
3106
|
+
homeId: options.homeId,
|
|
3107
|
+
pending: initial.pending
|
|
3108
|
+
}) : void 0;
|
|
3109
|
+
return yield* notifyPendingReceipt(options, yield* serial.withPermits(1)(Effect.uninterruptible(runRequest(options, requestId, decision))), serial, notificationSerial);
|
|
3110
|
+
}),
|
|
3111
|
+
status: (requestId) => Effect.gen(function* () {
|
|
3112
|
+
const state = yield* loadState(options);
|
|
3113
|
+
const actual = options.runtime.current ? yield* options.runtime.current().pipe(Effect.catchAll(() => Effect.succeed(void 0))) : void 0;
|
|
3114
|
+
const receipt = requestId === void 0 ? void 0 : state.requests[requestId];
|
|
3115
|
+
if (requestId !== void 0 && !receipt) return yield* Effect.fail(new ModelChangeNotFound(`unknown Model change request: ${requestId}`));
|
|
3116
|
+
return toStatusView(state, actual, receipt);
|
|
3117
|
+
})
|
|
3118
|
+
};
|
|
3119
|
+
}
|
|
3120
|
+
function acceptRequest(options, submission, transport) {
|
|
3121
|
+
return Effect.gen(function* () {
|
|
3122
|
+
validateSubmission(submission);
|
|
3123
|
+
let state = yield* loadState(options);
|
|
3124
|
+
const existing = state.requests[submission.requestId];
|
|
3125
|
+
const decision = yield* options.authorization.authorize({
|
|
3126
|
+
current: state.current,
|
|
3127
|
+
homeId: options.homeId,
|
|
3128
|
+
request: submission,
|
|
3129
|
+
transport,
|
|
3130
|
+
...existing && existing.authorizationId ? { existing: {
|
|
3131
|
+
authorizationId: existing.authorizationId,
|
|
3132
|
+
deadlineAt: existing.budget.deadlineAt
|
|
3133
|
+
} } : {}
|
|
3134
|
+
});
|
|
3135
|
+
if (decision.status === "rejected") return yield* Effect.fail(new ModelChangeAuthorizationRejected(decision.reason));
|
|
3136
|
+
if (existing) {
|
|
3137
|
+
if (decision.status === "approved") {
|
|
3138
|
+
assertPrincipal(options, decision.principal);
|
|
3139
|
+
assertExistingPrincipal(existing, decision.principal);
|
|
3140
|
+
if (existing.phase === "awaiting-approval" && state.pending?.request.requestId === submission.requestId) {
|
|
3141
|
+
if (!sameSubmission(existing, submission)) return yield* Effect.fail(new ModelChangeRequestConflict(`request id has different input: ${submission.requestId}`));
|
|
3142
|
+
return yield* upgradeAwaitingApproval(options, state, existing, decision);
|
|
3143
|
+
}
|
|
3144
|
+
const digest = digestFor(options, state, submission, decision, existing.bindingRevision);
|
|
3145
|
+
if (existing.inputDigest !== digest) return yield* Effect.fail(new ModelChangeRequestConflict(`request id has different trusted input: ${submission.requestId}`));
|
|
3146
|
+
} else {
|
|
3147
|
+
if (!decision.principal) return yield* Effect.fail(new ModelChangeAuthorizationRejected("awaiting approval has no trusted principal"));
|
|
3148
|
+
assertPrincipal(options, decision.principal);
|
|
3149
|
+
if (decision.principal.ownerId !== state.ownerId) return yield* Effect.fail(new ModelChangeAuthorizationRejected("authorization owner does not match Model Home"));
|
|
3150
|
+
if (!sameSubmission(existing, submission)) return yield* Effect.fail(new ModelChangeRequestConflict(`request id has different input: ${submission.requestId}`));
|
|
3151
|
+
if (existing.phase !== "awaiting-approval") return yield* Effect.fail(new ModelChangeAuthorizationRejected("Model change authorization is no longer approved"));
|
|
3152
|
+
assertExistingPrincipal(existing, decision.principal);
|
|
3153
|
+
}
|
|
3154
|
+
return existing;
|
|
3155
|
+
}
|
|
3156
|
+
if (state.recoveryRequired) return yield* Effect.fail(new ModelChangeAuthorizationRejected("Model change is blocked by recovery-required state"));
|
|
3157
|
+
if (state.pending) return yield* Effect.fail(new ModelChangeBusy(`Model change already pending: ${state.pending.request.requestId}`));
|
|
3158
|
+
if (submission.expectedRevision !== state.revision) return yield* Effect.fail(new ModelChangeRevisionConflict(`expected Model revision ${submission.expectedRevision}, current revision is ${state.revision}`));
|
|
3159
|
+
const target = resolveTarget(state, submission);
|
|
3160
|
+
const now = requireNow(options);
|
|
3161
|
+
const principal = decision.status === "approved" ? decision.principal : decision.principal;
|
|
3162
|
+
if (principal) {
|
|
3163
|
+
assertPrincipal(options, principal);
|
|
3164
|
+
if (principal.ownerId !== state.ownerId) return yield* Effect.fail(new ModelChangeAuthorizationRejected("authorization owner does not match Model Home"));
|
|
3165
|
+
}
|
|
3166
|
+
const bindingRevision = decision.status === "approved" ? decision.bindingRevision : decision.bindingRevision ?? state.current.bindingRevision;
|
|
3167
|
+
const budget = decision.status === "approved" ? carryModelChangeBudgetUsage(createModelChangeBudget(decision.budget, now), sharedBudgetUsage(state, decision.budget.identity)) : createAwaitingModelChangeBudget(decision.deadlineAt ?? now);
|
|
3168
|
+
const pending = {
|
|
3169
|
+
acceptedAt: now,
|
|
3170
|
+
...decision.authorizationId ? { authorizationId: decision.authorizationId } : {},
|
|
3171
|
+
bindingRevision,
|
|
3172
|
+
budget,
|
|
3173
|
+
budgetIdentity: decision.status === "approved" ? decision.budget.identity : budget.identity,
|
|
3174
|
+
expectedRevision: state.revision,
|
|
3175
|
+
inputDigest: digestFor(options, state, submission, decision, bindingRevision),
|
|
3176
|
+
operation: submission.operation,
|
|
3177
|
+
...principal ? {
|
|
3178
|
+
ownerId: principal.ownerId,
|
|
3179
|
+
principal
|
|
3180
|
+
} : {},
|
|
3181
|
+
phase: decision.status === "awaiting-approval" ? "awaiting-approval" : "accepted",
|
|
3182
|
+
request: stripTransportContext(submission),
|
|
3183
|
+
...target ? { target } : {},
|
|
3184
|
+
updatedAt: now
|
|
3185
|
+
};
|
|
3186
|
+
const receipt = pendingReceipt(state, pending, now);
|
|
3187
|
+
state = yield* persistState(options, stateWithPending(state, pending, receipt));
|
|
3188
|
+
return state.requests[submission.requestId];
|
|
3189
|
+
});
|
|
3190
|
+
}
|
|
3191
|
+
function runRequest(options, requestId, resolvedAwaitingDecision) {
|
|
3192
|
+
return Effect.gen(function* () {
|
|
3193
|
+
let state = yield* loadState(options);
|
|
3194
|
+
let pending = state.pending;
|
|
3195
|
+
if (!pending || pending.request.requestId !== requestId) {
|
|
3196
|
+
const receipt = state.requests[requestId];
|
|
3197
|
+
if (!receipt) return yield* Effect.fail(new ModelChangeNotFound(`unknown Model change request: ${requestId}`));
|
|
3198
|
+
return receipt;
|
|
3199
|
+
}
|
|
3200
|
+
if (pending.phase === "awaiting-approval") {
|
|
3201
|
+
if (!options.authorization.resolvePending) return state.requests[requestId];
|
|
3202
|
+
const decision = resolvedAwaitingDecision ?? (options.authorization.resolvePending ? yield* options.authorization.resolvePending({
|
|
3203
|
+
current: state.current,
|
|
3204
|
+
homeId: options.homeId,
|
|
3205
|
+
pending
|
|
3206
|
+
}) : void 0);
|
|
3207
|
+
if (!decision) return state.requests[requestId];
|
|
3208
|
+
if (decision.status === "awaiting-approval") return state.requests[requestId];
|
|
3209
|
+
if (decision.status === "rejected") {
|
|
3210
|
+
const receipt = failedReceipt(state, pending, modelError("authorization_rejected", decision.reason, "boundary"), requireNow(options));
|
|
3211
|
+
return yield* persistTerminalReceipt(options, state, receipt);
|
|
3212
|
+
}
|
|
3213
|
+
assertPrincipal(options, decision.principal);
|
|
3214
|
+
if (decision.principal.ownerId !== state.ownerId || decision.bindingRevision !== pending.bindingRevision || state.current.bindingRevision !== pending.bindingRevision || state.revision !== pending.expectedRevision) {
|
|
3215
|
+
const receipt = failedReceipt(state, pending, modelError("binding_changed", "Model authorization binding changed before approval was applied", "boundary"), requireNow(options));
|
|
3216
|
+
return yield* persistTerminalReceipt(options, state, receipt);
|
|
3217
|
+
}
|
|
3218
|
+
const promoted = promoteAwaitingPending(options, state, pending, decision);
|
|
3219
|
+
const now = promoted.updatedAt;
|
|
3220
|
+
const receipt = pendingReceipt(state, promoted, now);
|
|
3221
|
+
state = yield* persistState(options, stateWithPending(state, promoted, receipt));
|
|
3222
|
+
pending = promoted;
|
|
3223
|
+
}
|
|
3224
|
+
if (pendingRequiresModelChangeRecovery(pending) || Object.keys(pending.budget.reservations).length > 0) return (yield* recoverModelChangePending(options, state, pending)).requests[requestId];
|
|
3225
|
+
const authorization = yield* revalidateAuthorization(options, state, pending);
|
|
3226
|
+
if (authorization.status === "rejected") {
|
|
3227
|
+
const error = modelError("authorization_revoked", authorization.reason, "boundary");
|
|
3228
|
+
const receipt = failedReceipt(state, pending, error, requireNow(options));
|
|
3229
|
+
return yield* persistTerminalReceipt(options, state, receipt);
|
|
3230
|
+
}
|
|
3231
|
+
if (authorization.bindingRevision !== pending.bindingRevision) {
|
|
3232
|
+
const error = modelError("binding_changed", "provider binding changed before Model change apply", "boundary");
|
|
3233
|
+
const receipt = failedReceipt(state, pending, error, requireNow(options));
|
|
3234
|
+
return yield* persistTerminalReceipt(options, state, receipt);
|
|
3235
|
+
}
|
|
3236
|
+
if (state.revision !== pending.expectedRevision || state.current.bindingRevision !== pending.bindingRevision) {
|
|
3237
|
+
const error = modelError("revision_changed", "Model revision changed before Model change apply", "boundary");
|
|
3238
|
+
const receipt = failedReceipt(state, pending, error, requireNow(options));
|
|
3239
|
+
return yield* persistTerminalReceipt(options, state, receipt);
|
|
3240
|
+
}
|
|
3241
|
+
if (!pending.target) {
|
|
3242
|
+
const error = modelError("target_unavailable", "Model change target is unavailable", "validation");
|
|
3243
|
+
const receipt = failedReceipt(state, pending, error, requireNow(options));
|
|
3244
|
+
return yield* persistTerminalReceipt(options, state, receipt);
|
|
3245
|
+
}
|
|
3246
|
+
return yield* executeModelChangePending(options, state, pending);
|
|
3247
|
+
});
|
|
3248
|
+
}
|
|
3249
|
+
function revalidateAuthorization(options, state, pending) {
|
|
3250
|
+
if (!pending.authorizationId || !pending.principal) return Effect.succeed({
|
|
3251
|
+
bindingRevision: pending.bindingRevision,
|
|
3252
|
+
reason: "Model change authorization is incomplete",
|
|
3253
|
+
status: "rejected"
|
|
3254
|
+
});
|
|
3255
|
+
return options.authorization.revalidate({
|
|
3256
|
+
authorizationId: pending.authorizationId,
|
|
3257
|
+
current: state.current,
|
|
3258
|
+
homeId: options.homeId,
|
|
3259
|
+
pending
|
|
3260
|
+
}).pipe(Effect.map((decision) => decision.status === "approved" ? {
|
|
3261
|
+
bindingRevision: decision.bindingRevision,
|
|
3262
|
+
status: "approved"
|
|
3263
|
+
} : {
|
|
3264
|
+
bindingRevision: pending.bindingRevision,
|
|
3265
|
+
reason: decision.reason,
|
|
3266
|
+
status: "rejected"
|
|
3267
|
+
}));
|
|
3268
|
+
}
|
|
3269
|
+
function upgradeAwaitingApproval(options, state, existing, decision) {
|
|
3270
|
+
return Effect.gen(function* () {
|
|
3271
|
+
const pending = state.pending;
|
|
3272
|
+
if (!pending || pending.request.requestId !== existing.requestId) return yield* Effect.fail(new ModelChangeRequestConflict("awaiting-approval request is no longer pending"));
|
|
3273
|
+
if (decision.principal.ownerId !== state.ownerId) return yield* Effect.fail(new ModelChangeAuthorizationRejected("authorization owner does not match Model Home"));
|
|
3274
|
+
const upgraded = promoteAwaitingPending(options, state, pending, decision);
|
|
3275
|
+
const now = upgraded.updatedAt;
|
|
3276
|
+
return (yield* persistState(options, stateWithPending(state, upgraded, pendingReceipt(state, upgraded, now)))).requests[existing.requestId];
|
|
3277
|
+
});
|
|
3278
|
+
}
|
|
3279
|
+
function promoteAwaitingPending(options, state, pending, decision) {
|
|
3280
|
+
const now = requireNow(options);
|
|
3281
|
+
const budget = carryModelChangeBudgetUsage(createModelChangeBudget(decision.budget, now), sharedBudgetUsage(state, decision.budget.identity));
|
|
3282
|
+
return {
|
|
3283
|
+
...pending,
|
|
3284
|
+
authorizationId: decision.authorizationId,
|
|
3285
|
+
bindingRevision: decision.bindingRevision,
|
|
3286
|
+
budget,
|
|
3287
|
+
budgetIdentity: decision.budget.identity,
|
|
3288
|
+
inputDigest: digestFor(options, state, pending.request, decision, decision.bindingRevision),
|
|
3289
|
+
ownerId: decision.principal.ownerId,
|
|
3290
|
+
phase: "accepted",
|
|
3291
|
+
principal: decision.principal,
|
|
3292
|
+
updatedAt: now
|
|
3293
|
+
};
|
|
3294
|
+
}
|
|
3295
|
+
function sharedBudgetUsage(state, budgetIdentity) {
|
|
3296
|
+
let outputTokens = 0;
|
|
3297
|
+
let paidRequests = 0;
|
|
3298
|
+
for (const receipt of Object.values(state.requests)) {
|
|
3299
|
+
if (receipt.budgetIdentity !== budgetIdentity) continue;
|
|
3300
|
+
outputTokens += receipt.budget.outputTokens + receipt.budget.reservedOutputTokens;
|
|
3301
|
+
paidRequests += receipt.budget.paidRequests + receipt.budget.reservedPaidRequests;
|
|
3302
|
+
}
|
|
3303
|
+
return {
|
|
3304
|
+
outputTokens,
|
|
3305
|
+
paidRequests
|
|
3306
|
+
};
|
|
3307
|
+
}
|
|
3308
|
+
function digestFor(options, state, submission, decision, bindingRevision) {
|
|
3309
|
+
const principal = decision.status === "approved" || decision.status === "awaiting-approval" ? decision.principal : void 0;
|
|
3310
|
+
const source = principal?.source ?? {
|
|
3311
|
+
kind: "human",
|
|
3312
|
+
reference: "awaiting-approval"
|
|
3313
|
+
};
|
|
3314
|
+
return options.identity.digest({
|
|
3315
|
+
...decision.status === "approved" ? { authorizationId: decision.authorizationId } : decision.status === "awaiting-approval" && decision.authorizationId ? { authorizationId: decision.authorizationId } : {},
|
|
3316
|
+
bindingRevision,
|
|
3317
|
+
...decision.status === "approved" ? { budgetIdentity: decision.budget.identity } : {},
|
|
3318
|
+
expectedRevision: submission.expectedRevision,
|
|
3319
|
+
homeId: options.homeId,
|
|
3320
|
+
operation: submission.operation,
|
|
3321
|
+
ownerId: principal?.ownerId ?? state.ownerId,
|
|
3322
|
+
requestId: submission.requestId,
|
|
3323
|
+
source,
|
|
3324
|
+
...submission.target ? { target: toReference(submission.target) } : {}
|
|
3325
|
+
});
|
|
3326
|
+
}
|
|
3327
|
+
function assertPrincipal(options, principal) {
|
|
3328
|
+
if (principal.homeId !== options.homeId) throw new ModelChangeAuthorizationRejected("authorization Home does not match");
|
|
3329
|
+
}
|
|
3330
|
+
function assertExistingPrincipal(existing, principal) {
|
|
3331
|
+
if (existing.principal && existing.principal.ownerId !== principal.ownerId) throw new ModelChangeAuthorizationRejected("request belongs to another owner");
|
|
3332
|
+
if (existing.principal && (existing.principal.source.kind !== principal.source.kind || existing.principal.source.reference !== principal.source.reference)) throw new ModelChangeAuthorizationRejected("request belongs to another source");
|
|
3333
|
+
}
|
|
3334
|
+
function persistTerminalReceipt(options, state, receipt) {
|
|
3335
|
+
return Effect.gen(function* () {
|
|
3336
|
+
const durable = terminalReceiptForPersistence(options, receipt);
|
|
3337
|
+
return (yield* persistState(options, stateWithReceipt(state, durable))).requests[durable.requestId];
|
|
3338
|
+
});
|
|
3339
|
+
}
|
|
3340
|
+
function notifyPendingReceipt(options, receipt, serial, notificationSerial) {
|
|
3341
|
+
if (!options.notification || receipt.status === "pending" || receipt.notification?.status !== "pending") return Effect.succeed(receipt);
|
|
3342
|
+
return notificationSerial.withPermits(1)(notifyModelChangeReceipt(options, receipt.requestId, false, (effect) => serial.withPermits(1)(effect)).pipe(Effect.map((notified) => notified ?? receipt)));
|
|
3343
|
+
}
|
|
3344
|
+
function retryPendingNotifications(options, serial, notificationSerial) {
|
|
3345
|
+
if (!options.notification) return loadState(options);
|
|
3346
|
+
return notificationSerial.withPermits(1)(Effect.gen(function* () {
|
|
3347
|
+
const initial = yield* loadState(options);
|
|
3348
|
+
for (const receipt of Object.values(initial.requests)) {
|
|
3349
|
+
if (receipt.status === "pending" || !receipt.notification || receipt.notification.status === "sent") continue;
|
|
3350
|
+
yield* notifyModelChangeReceipt(options, receipt.requestId, true, (effect) => serial.withPermits(1)(effect));
|
|
3351
|
+
}
|
|
3352
|
+
return yield* loadState(options);
|
|
3353
|
+
}));
|
|
3354
|
+
}
|
|
3355
|
+
function validateSubmission(submission) {
|
|
3356
|
+
if (typeof submission.requestId !== "string" || submission.requestId.length === 0 || submission.operation !== "set" && submission.operation !== "rollback" || !Number.isSafeInteger(submission.expectedRevision) || submission.expectedRevision < 0) throw new Error("invalid Model change submission");
|
|
3357
|
+
if (submission.target !== void 0 && (typeof submission.target.provider !== "string" || submission.target.provider.length === 0 || typeof submission.target.model !== "string" || submission.target.model.length === 0)) throw new Error("invalid Model change target");
|
|
3358
|
+
if (submission.operation === "set" && submission.target === void 0) throw new Error("Model set requires a target");
|
|
3359
|
+
if (submission.operation === "rollback" && submission.target !== void 0) throw new Error("Model rollback does not accept a target");
|
|
3360
|
+
}
|
|
3361
|
+
//#endregion
|
|
3362
|
+
//#region src/bootstrap/deployment/model-management/model-management-control.ts
|
|
3363
|
+
/** Owns the control socket and the accepted-operation worker for one Home. */
|
|
3364
|
+
function createModelManagementControl(options) {
|
|
3365
|
+
const coordinator = createModelChangeCoordinator(options);
|
|
3366
|
+
const workers = /* @__PURE__ */ new Map();
|
|
3367
|
+
const transportReferences = /* @__PURE__ */ new WeakMap();
|
|
3368
|
+
let closing = false;
|
|
3369
|
+
let workerFailed = false;
|
|
3370
|
+
let recovered = false;
|
|
3371
|
+
const server = createRivusModelManagementSocketServer({
|
|
3372
|
+
handlers: {
|
|
3373
|
+
handle: async (input, trustedRun) => {
|
|
3374
|
+
if (closing || workerFailed || !recovered) return createRivusModelManagementFailure("management_unavailable", "Model management requires Host recovery.");
|
|
3375
|
+
const reference = typeof trustedRun === "object" && trustedRun !== null ? transportReferences.get(trustedRun) : void 0;
|
|
3376
|
+
if (!reference) return createRivusModelManagementFailure("trusted_context_invalid", "The trusted Run context is no longer available.");
|
|
3377
|
+
try {
|
|
3378
|
+
const receipt = await runDeploymentProcessEffect(coordinator.accept({
|
|
3379
|
+
expectedRevision: input.expectedRevision,
|
|
3380
|
+
operation: input.operation,
|
|
3381
|
+
requestId: input.requestId,
|
|
3382
|
+
...input.target ? { target: input.target } : {}
|
|
3383
|
+
}, { reference }));
|
|
3384
|
+
if (receipt.status === "pending" && (receipt.phase !== "awaiting-approval" || options.authorization.resolvePending) && !workers.has(receipt.requestId)) {
|
|
3385
|
+
const work = Promise.resolve().then(async () => {
|
|
3386
|
+
try {
|
|
3387
|
+
if ((await runDeploymentProcessEffect(coordinator.run(receipt.requestId))).status === "recovery-required") {
|
|
3388
|
+
workerFailed = true;
|
|
3389
|
+
options.stopBusinessWork();
|
|
3390
|
+
}
|
|
3391
|
+
} catch {
|
|
3392
|
+
workerFailed = true;
|
|
3393
|
+
options.stopBusinessWork();
|
|
3394
|
+
} finally {
|
|
3395
|
+
workers.delete(receipt.requestId);
|
|
3396
|
+
}
|
|
3397
|
+
});
|
|
3398
|
+
workers.set(receipt.requestId, work);
|
|
3399
|
+
}
|
|
3400
|
+
return {
|
|
3401
|
+
...receipt,
|
|
3402
|
+
protocolVersion: 1,
|
|
3403
|
+
runtimeVersion: options.runtimeVersion
|
|
3404
|
+
};
|
|
3405
|
+
} catch (error) {
|
|
3406
|
+
return publicFailure(error);
|
|
3407
|
+
}
|
|
3408
|
+
},
|
|
3409
|
+
status: async ({ requestId }) => {
|
|
3410
|
+
try {
|
|
3411
|
+
const view = await runDeploymentProcessEffect(coordinator.status(requestId));
|
|
3412
|
+
return {
|
|
3413
|
+
...view,
|
|
3414
|
+
...workerFailed ? { error: {
|
|
3415
|
+
code: "management_worker_failed",
|
|
3416
|
+
message: "An accepted operation requires Host recovery."
|
|
3417
|
+
} } : {},
|
|
3418
|
+
protocolVersion: 1,
|
|
3419
|
+
runtimeVersion: options.runtimeVersion,
|
|
3420
|
+
status: workerFailed || view.recoveryRequired ? "recovery-required" : view.request?.status ?? (view.pending ? "pending" : "applied")
|
|
3421
|
+
};
|
|
3422
|
+
} catch (error) {
|
|
3423
|
+
return publicFailure(error);
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
},
|
|
3427
|
+
resolveTrustedRun: { resolve: async (reference) => {
|
|
3428
|
+
if (!await options.resolveTrustedReference(reference)) return void 0;
|
|
3429
|
+
const context = Object.freeze({});
|
|
3430
|
+
transportReferences.set(context, reference);
|
|
3431
|
+
return context;
|
|
3432
|
+
} },
|
|
3433
|
+
socketPath: options.socketPath
|
|
3434
|
+
});
|
|
3435
|
+
return {
|
|
3436
|
+
close: async () => {
|
|
3437
|
+
closing = true;
|
|
3438
|
+
await server.close();
|
|
3439
|
+
await Promise.all(workers.values());
|
|
3440
|
+
},
|
|
3441
|
+
recover: async () => {
|
|
3442
|
+
if (!server.listening()) throw new Error("Acquire the model management socket before recovery.");
|
|
3443
|
+
const state = await runDeploymentProcessEffect(coordinator.recover());
|
|
3444
|
+
if (state.recoveryRequired || state.pending) {
|
|
3445
|
+
workerFailed = true;
|
|
3446
|
+
options.stopBusinessWork();
|
|
3447
|
+
}
|
|
3448
|
+
recovered = true;
|
|
3449
|
+
return state;
|
|
3450
|
+
},
|
|
3451
|
+
start: () => server.start()
|
|
3452
|
+
};
|
|
3453
|
+
}
|
|
3454
|
+
function publicFailure(error) {
|
|
3455
|
+
const name = typeof error === "object" && error !== null && "name" in error ? error.name : void 0;
|
|
3456
|
+
const failures = {
|
|
3457
|
+
ModelChangeAuthorizationRejected: ["authorization_rejected", "The model management request is outside the current authorization."],
|
|
3458
|
+
ModelChangeBusy: ["model_change_busy", "Another model change is pending. Query its status before submitting again."],
|
|
3459
|
+
ModelChangeNotFound: ["request_not_found", "No model change exists with that request ID."],
|
|
3460
|
+
ModelChangeRequestConflict: ["request_conflict", "The request ID is already bound to different input."],
|
|
3461
|
+
ModelChangeRevisionConflict: ["revision_conflict", "The model revision changed. Read the current status."],
|
|
3462
|
+
ModelChangeBudgetExceeded: ["budget_exhausted", "The existing model change budget is exhausted."]
|
|
3463
|
+
};
|
|
3464
|
+
const [code, message] = typeof name === "string" && failures[name] ? failures[name] : ["management_failed", "The model management request could not be completed."];
|
|
3465
|
+
return createRivusModelManagementFailure(code, message);
|
|
3466
|
+
}
|
|
3467
|
+
//#endregion
|
|
3468
|
+
//#region src/bootstrap/deployment/model-management/pi-model-management-deployment.ts
|
|
3469
|
+
/** Assemble one Home's model authority without constructing a business session. */
|
|
3470
|
+
async function openPiModelManagementDeployment(options) {
|
|
3471
|
+
const configuration = parseRivusModelManagementHomeConfig(options.env);
|
|
3472
|
+
const managementDirectory = resolve(options.stateDirectory, "model-management");
|
|
3473
|
+
const statePath = join(managementDirectory, "model-state.json");
|
|
3474
|
+
const markerPath = join(managementDirectory, "legacy-export.json");
|
|
3475
|
+
if (!configuration.enabled && await readPersistenceFile(statePath) === void 0) return void 0;
|
|
3476
|
+
if (options.agentCount !== 1) throw new Error("Model management supports exactly one Agent per Home.");
|
|
3477
|
+
const homeId = createSha256Digest(await realpath(options.stateDirectory));
|
|
3478
|
+
const runtimeVersion = options.runtimeVersion ?? JSON.parse(await readFile(new URL("../package.json", import.meta.resolve("@rivus/agent")), "utf8")).version;
|
|
3479
|
+
const clock = { now: () => (/* @__PURE__ */ new Date()).toISOString() };
|
|
3480
|
+
const boundary = createModelChangeRunBoundary({ clock });
|
|
3481
|
+
boundary.fence();
|
|
3482
|
+
const runtime = createPiModelRuntime({
|
|
3483
|
+
modelCatalogPath: options.modelCatalogPath,
|
|
3484
|
+
modelRuntime: options.modelRuntime
|
|
3485
|
+
});
|
|
3486
|
+
const resolvers = /* @__PURE__ */ new Set();
|
|
3487
|
+
const resolveRun = (reference) => {
|
|
3488
|
+
for (const resolver of resolvers) {
|
|
3489
|
+
const run = resolver(reference);
|
|
3490
|
+
if (run) return run;
|
|
3491
|
+
}
|
|
3492
|
+
};
|
|
3493
|
+
let repository;
|
|
3494
|
+
let authorization;
|
|
3495
|
+
const requireRepository = () => {
|
|
3496
|
+
if (!repository) throw new Error("Model state has not been opened under the control socket.");
|
|
3497
|
+
return repository;
|
|
3498
|
+
};
|
|
3499
|
+
const socketPath = join(managementDirectory, "control.sock");
|
|
3500
|
+
const control = createModelManagementControl({
|
|
3501
|
+
authorization: {
|
|
3502
|
+
authorize: (input) => Effect.suspend(() => authorization ? authorization.authorize(input) : Effect.fail(/* @__PURE__ */ new Error("Model authorization is not ready."))),
|
|
3503
|
+
revalidate: (input) => Effect.suspend(() => authorization ? authorization.revalidate(input) : Effect.fail(/* @__PURE__ */ new Error("Model authorization is not ready."))),
|
|
3504
|
+
resolvePending: (input) => Effect.suspend(() => authorization?.resolvePending ? authorization.resolvePending(input) : Effect.fail(/* @__PURE__ */ new Error("Model authorization is not ready.")))
|
|
3505
|
+
},
|
|
3506
|
+
boundary,
|
|
3507
|
+
clock,
|
|
3508
|
+
homeId,
|
|
3509
|
+
identity: { digest: (input) => createToolInputDigest(input, createSha256Digest) },
|
|
3510
|
+
notification: { notify: ({ receipt }) => Effect.gen(function* () {
|
|
3511
|
+
const source = (yield* requireRepository().load()).requests[receipt.requestId]?.principal?.source;
|
|
3512
|
+
if (!source || source.kind !== "human") return yield* Effect.fail(/* @__PURE__ */ new Error("Model result notification has no trusted human source."));
|
|
3513
|
+
const selected = receipt.current ? `${receipt.current.provider}/${receipt.current.model}` : "unknown";
|
|
3514
|
+
const text = `模型变更 ${receipt.requestId}: ${receipt.status}。当前模型 ${selected},revision ${receipt.revision}。${receipt.error ? `原因:${receipt.error.code}。` : ""}`;
|
|
3515
|
+
yield* options.reply(source.reference, text);
|
|
3516
|
+
return { delivered: true };
|
|
3517
|
+
}) },
|
|
3518
|
+
repository: {
|
|
3519
|
+
load: () => Effect.suspend(() => requireRepository().load()),
|
|
3520
|
+
persist: (state) => Effect.suspend(() => requireRepository().persist(state))
|
|
3521
|
+
},
|
|
3522
|
+
resolveTrustedReference: async (reference) => resolveRun(reference) !== void 0,
|
|
3523
|
+
runtime,
|
|
3524
|
+
runtimeVersion,
|
|
3525
|
+
socketPath,
|
|
3526
|
+
stopBusinessWork: () => boundary.fence()
|
|
3527
|
+
});
|
|
3528
|
+
await control.start();
|
|
3529
|
+
try {
|
|
3530
|
+
const imported = await migrateRivusModelManagementConfig({
|
|
3531
|
+
writeAtomicTextFile,
|
|
3532
|
+
env: options.environmentOverrides,
|
|
3533
|
+
...options.envFilePath ? { envFilePath: options.envFilePath } : {},
|
|
3534
|
+
knownGoodModel: "unused",
|
|
3535
|
+
knownGoodProvider: "unused",
|
|
3536
|
+
managementEnabled: true
|
|
3537
|
+
});
|
|
3538
|
+
if (imported.status !== "enabled") throw new Error("Expected the original model configuration.");
|
|
3539
|
+
const baseUrl = options.env.PI_BASE_URL?.trim();
|
|
3540
|
+
if (baseUrl) {
|
|
3541
|
+
await mergePiProviderBaseUrlOverride({
|
|
3542
|
+
baseUrl,
|
|
3543
|
+
filePath: options.modelCatalogPath,
|
|
3544
|
+
provider: imported.initialModel.provider
|
|
3545
|
+
});
|
|
3546
|
+
await runDeploymentProcessEffect(runtime.refreshModelCatalog({
|
|
3547
|
+
allowNetwork: false,
|
|
3548
|
+
provider: imported.initialModel.provider
|
|
3549
|
+
}));
|
|
3550
|
+
}
|
|
3551
|
+
const grant = await readPiModelManagementGrant(options, homeId, imported.initialModel);
|
|
3552
|
+
const hadState = await readPersistenceFile(statePath) !== void 0;
|
|
3553
|
+
repository = await runDeploymentProcessEffect(openJsonModelStateRepository({
|
|
3554
|
+
filePath: statePath,
|
|
3555
|
+
initial: {
|
|
3556
|
+
current: {
|
|
3557
|
+
...imported.initialModel,
|
|
3558
|
+
bindingRevision: grant.bindingRevision
|
|
3559
|
+
},
|
|
3560
|
+
homeId,
|
|
3561
|
+
ownerId: configuration.ownerOpenId ?? "disabled",
|
|
3562
|
+
requests: {},
|
|
3563
|
+
revision: 0,
|
|
3564
|
+
schemaVersion: 1,
|
|
3565
|
+
updatedAt: clock.now()
|
|
3566
|
+
}
|
|
3567
|
+
}));
|
|
3568
|
+
const state = await runDeploymentProcessEffect(repository.load());
|
|
3569
|
+
if (state.homeId !== homeId || configuration.enabled && state.ownerId !== configuration.ownerOpenId) throw new Error("Persisted model state belongs to another Home or owner.");
|
|
3570
|
+
const marker = await readExportMarker(markerPath);
|
|
3571
|
+
if (!configuration.enabled && marker?.revision === state.revision && !state.pending && !state.recoveryRequired) {
|
|
3572
|
+
await control.close();
|
|
3573
|
+
return {
|
|
3574
|
+
enabled: false,
|
|
3575
|
+
model: imported.initialModel
|
|
3576
|
+
};
|
|
3577
|
+
}
|
|
3578
|
+
if (configuration.enabled && marker && !sameModel(imported.initialModel, state.current)) throw new Error("The legacy model changed after management was disabled. Reconcile the two selections before enabling management.");
|
|
3579
|
+
if (configuration.enabled && state.current.bindingRevision !== grant.bindingRevision) throw new Error("Provider credentials, endpoint, thinking mode or adapter binding changed. Reconcile the persisted model binding before enabling management.");
|
|
3580
|
+
if (!hadState) await runDeploymentProcessEffect(repository.persist(state));
|
|
3581
|
+
const operations = await runDeploymentProcessEffect(openJsonlToolOperationLedger({ filePath: join(managementDirectory, "tool-operations.jsonl") }));
|
|
3582
|
+
authorization = createModelChangeAuthorization({
|
|
3583
|
+
...options.approval ? { approval: options.approval } : {},
|
|
3584
|
+
clock,
|
|
3585
|
+
digest: createSha256Digest,
|
|
3586
|
+
grant: { current: () => Effect.tryPromise({
|
|
3587
|
+
try: () => readPiModelManagementGrant(options, homeId, state.current),
|
|
3588
|
+
catch: (error) => error
|
|
3589
|
+
}) },
|
|
3590
|
+
operations,
|
|
3591
|
+
resolveWorkerRun: ({ grant: currentGrant, principal, request }) => Effect.sync(() => ({
|
|
3592
|
+
authority: createInvocationAuthority({
|
|
3593
|
+
agentId: options.agentId,
|
|
3594
|
+
allowedActorOpenIds: [principal.ownerId],
|
|
3595
|
+
endpointId: currentGrant.endpointId,
|
|
3596
|
+
instanceId: `model-management:${homeId}`,
|
|
3597
|
+
runId: `model-change:${request.requestId}`,
|
|
3598
|
+
sessionKey: `model-management:${homeId}`,
|
|
3599
|
+
sourceMessageId: principal.source.reference,
|
|
3600
|
+
tenantKey: currentGrant.tenantKey,
|
|
3601
|
+
toolGrantSet: {
|
|
3602
|
+
revision: String(currentGrant.revision),
|
|
3603
|
+
toolIds: ["rivus.model.change"]
|
|
3604
|
+
}
|
|
3605
|
+
}),
|
|
3606
|
+
kind: "human"
|
|
3607
|
+
})),
|
|
3608
|
+
resolveRun: (reference) => Effect.sync(() => resolveRun(reference))
|
|
3609
|
+
});
|
|
3610
|
+
await runDeploymentProcessEffect(runtime.initializeSelection({
|
|
3611
|
+
binding: state.current,
|
|
3612
|
+
thinkingLevel: options.thinkingLevel
|
|
3613
|
+
}));
|
|
3614
|
+
const recovered = await control.recover();
|
|
3615
|
+
const recoveryRequired = Boolean(recovered.recoveryRequired || recovered.pending);
|
|
3616
|
+
if (!configuration.enabled && !recoveryRequired) {
|
|
3617
|
+
if (!marker || marker.revision !== recovered.revision) {
|
|
3618
|
+
await migrateRivusModelManagementConfig({
|
|
3619
|
+
writeAtomicTextFile,
|
|
3620
|
+
env: options.environmentOverrides,
|
|
3621
|
+
...options.envFilePath ? { envFilePath: options.envFilePath } : {},
|
|
3622
|
+
knownGoodModel: recovered.current.model,
|
|
3623
|
+
knownGoodProvider: recovered.current.provider,
|
|
3624
|
+
managementEnabled: false
|
|
3625
|
+
});
|
|
3626
|
+
await writeAtomicTextFile(markerPath, JSON.stringify({
|
|
3627
|
+
revision: recovered.revision,
|
|
3628
|
+
version: 1
|
|
3629
|
+
}), {
|
|
3630
|
+
durable: true,
|
|
3631
|
+
mode: 384
|
|
3632
|
+
});
|
|
3633
|
+
}
|
|
3634
|
+
await control.close();
|
|
3635
|
+
return {
|
|
3636
|
+
enabled: false,
|
|
3637
|
+
model: recovered.current
|
|
3638
|
+
};
|
|
3639
|
+
}
|
|
3640
|
+
if (marker && !recoveryRequired) await unlink(markerPath);
|
|
3641
|
+
const skillInstallation = await installRivusRuntimeManagementSkill({
|
|
3642
|
+
writeAtomicTextFile,
|
|
3643
|
+
...options.homeDirectory ? { homeDirectory: options.homeDirectory } : {},
|
|
3644
|
+
sourcePath: fileURLToPath(import.meta.resolve("@rivus/agent/skills/runtime-management/SKILL.md"))
|
|
3645
|
+
});
|
|
3646
|
+
const launcher = await installRivusModelManagementCliLauncher({
|
|
3647
|
+
writeAtomicTextFile,
|
|
3648
|
+
directory: managementDirectory,
|
|
3649
|
+
cliEntryPath: fileURLToPath(new URL("./cli.js", import.meta.resolve("@rivus/agent"))),
|
|
3650
|
+
nodeExecutable: process.execPath
|
|
3651
|
+
});
|
|
3652
|
+
if (!recoveryRequired) await runDeploymentProcessEffect(boundary.resumeAfterChange({ requestId: "startup" }));
|
|
3653
|
+
return {
|
|
3654
|
+
binDirectory: launcher.binDirectory,
|
|
3655
|
+
boundary,
|
|
3656
|
+
close: () => control.close(),
|
|
3657
|
+
enabled: true,
|
|
3658
|
+
recoveryRequired,
|
|
3659
|
+
runtime,
|
|
3660
|
+
socketPath,
|
|
3661
|
+
skillInstallation,
|
|
3662
|
+
createRunContexts: (input) => {
|
|
3663
|
+
const registry = createPiModelRunContextRegistry({
|
|
3664
|
+
...input,
|
|
3665
|
+
modelManagementEnabled: true
|
|
3666
|
+
});
|
|
3667
|
+
const resolver = (reference) => registry.resolveRun(reference);
|
|
3668
|
+
resolvers.add(resolver);
|
|
3669
|
+
return {
|
|
3670
|
+
registry,
|
|
3671
|
+
unregister: () => {
|
|
3672
|
+
resolvers.delete(resolver);
|
|
3673
|
+
}
|
|
3674
|
+
};
|
|
3675
|
+
}
|
|
3676
|
+
};
|
|
3677
|
+
} catch (error) {
|
|
3678
|
+
await control.close();
|
|
3679
|
+
throw error;
|
|
3680
|
+
}
|
|
3681
|
+
}
|
|
3682
|
+
async function readExportMarker(path) {
|
|
3683
|
+
const raw = await readPersistenceFile(path);
|
|
3684
|
+
if (raw === void 0) return void 0;
|
|
3685
|
+
const value = JSON.parse(raw);
|
|
3686
|
+
if (value.version !== 1 || typeof value.revision !== "number" || !Number.isSafeInteger(value.revision) || value.revision < 0) throw new Error("The model configuration export marker is invalid.");
|
|
3687
|
+
return { revision: value.revision };
|
|
3688
|
+
}
|
|
3689
|
+
function sameModel(left, right) {
|
|
3690
|
+
return left.provider === right.provider && left.model === right.model;
|
|
3691
|
+
}
|
|
3692
|
+
//#endregion
|
|
13
3693
|
//#region examples/pi-feishu-deployment.bootstrap.ts
|
|
14
3694
|
const STATE_DIR = process.env.RIVUS_DEPLOYMENT_STATE_DIR?.trim() || ".rivus/deployment";
|
|
15
3695
|
const PI_AGENT_DIR = join(STATE_DIR, "pi-agent");
|
|
@@ -25,9 +3705,13 @@ const THINKING_LEVELS = /* @__PURE__ */ new Set([
|
|
|
25
3705
|
]);
|
|
26
3706
|
async function createRivusDeploymentAdapters(context) {
|
|
27
3707
|
await mkdir(PI_AGENT_DIR, { recursive: true });
|
|
28
|
-
const
|
|
3708
|
+
const hasManagedState = await stat(join(STATE_DIR, "model-management", "model-state.json")).then(() => true, (error) => {
|
|
3709
|
+
if (error.code === "ENOENT") return false;
|
|
3710
|
+
throw error;
|
|
3711
|
+
});
|
|
3712
|
+
const piOptions = await createPiSessionOptions(context, context.env.RIVUS_MODEL_MANAGEMENT_ENABLED?.trim() === "true" || hasManagedState);
|
|
29
3713
|
const telemetryConfig = resolveLangfuseTelemetryConfig(context.env);
|
|
30
|
-
|
|
3714
|
+
let telemetry;
|
|
31
3715
|
const memory = await openJsonlAgentMemoryService({ filePath: join(STATE_DIR, "memory", "agent-memory.jsonl") });
|
|
32
3716
|
const memoryTenantId = context.env.RIVUS_MEMORY_TENANT_ID?.trim() || "local";
|
|
33
3717
|
const request = createJsonFetchRequest();
|
|
@@ -37,395 +3721,428 @@ async function createRivusDeploymentAdapters(context) {
|
|
|
37
3721
|
});
|
|
38
3722
|
const interactionRegistry = createHumanInteractionEndpointRegistry();
|
|
39
3723
|
const manifest = await loadRivusDeploymentManifest(context.manifestPath);
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const config =
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
3724
|
+
const defaultEndpoint = manifest.endpoints.find((endpoint) => endpoint.id === manifest.defaultEndpointId);
|
|
3725
|
+
const management = await openPiModelManagementDeployment({
|
|
3726
|
+
agentId: manifest.defaultAgentId,
|
|
3727
|
+
approval: createHumanInteractionModelChangeApproval({
|
|
3728
|
+
endpointId: manifest.defaultEndpointId,
|
|
3729
|
+
registry: interactionRegistry
|
|
3730
|
+
}),
|
|
3731
|
+
agentCount: manifest.agents.length,
|
|
3732
|
+
endpointId: manifest.defaultEndpointId,
|
|
3733
|
+
env: context.env,
|
|
3734
|
+
environmentOverrides: context.environmentOverrides ?? process.env,
|
|
3735
|
+
...context.envFilePath ? { envFilePath: context.envFilePath } : {},
|
|
3736
|
+
modelAuthPath: PI_AUTH_FILE,
|
|
3737
|
+
modelCatalogPath: PI_MODELS_FILE,
|
|
3738
|
+
modelRuntime: piOptions.modelRuntime,
|
|
3739
|
+
reply: (messageId, text) => {
|
|
3740
|
+
const config = createEndpointConfig(defaultEndpoint, manifest.defaultAgentId, context.env);
|
|
3741
|
+
return createConfiguredFeishuTextReplySender({
|
|
3742
|
+
config,
|
|
3743
|
+
client: createOpenApiClient(config)
|
|
3744
|
+
}).reply(messageId, text);
|
|
3745
|
+
},
|
|
3746
|
+
stateDirectory: STATE_DIR,
|
|
3747
|
+
thinkingLevel: piOptions.thinkingLevel ?? "medium"
|
|
3748
|
+
});
|
|
3749
|
+
try {
|
|
3750
|
+
telemetry = telemetryConfig ? createLangfuseAgentTelemetry(telemetryConfig) : void 0;
|
|
3751
|
+
if (management && !management.enabled) piOptions.model = await ensurePiModel({
|
|
3752
|
+
modelRuntime: piOptions.modelRuntime,
|
|
3753
|
+
modelsPath: PI_MODELS_FILE,
|
|
3754
|
+
target: management.model
|
|
3755
|
+
});
|
|
3756
|
+
if (management?.enabled && management.recoveryRequired) console.error("Model management requires recovery; business Runs are paused. Query rivus model status --json.");
|
|
3757
|
+
if (management?.enabled && management.skillInstallation.status === "conflict") console.error("The runtime-management Skill has local edits; its installed instructions were preserved.");
|
|
3758
|
+
const backgroundSessionsConfig = manifest.backgroundSessions;
|
|
3759
|
+
const sessionRepository = backgroundSessionsConfig?.enabled ? await openJsonlBackgroundSessionRepository({ filePath: join(STATE_DIR, "background-sessions", "sessions.jsonl") }) : void 0;
|
|
3760
|
+
const sessionDeliveries = backgroundSessionsConfig?.enabled ? await openJsonlBackgroundSessionDeliveryStore({ filePath: join(STATE_DIR, "background-sessions", "deliveries.jsonl") }) : void 0;
|
|
3761
|
+
const deliveryClients = /* @__PURE__ */ new Map();
|
|
3762
|
+
const backgroundService = backgroundSessionsConfig?.enabled ? createBackgroundSessionService({
|
|
74
3763
|
clock: { now: () => (/* @__PURE__ */ new Date()).toISOString() },
|
|
75
|
-
config: {
|
|
76
|
-
intervalMs: resolveBackgroundSessionSupervisorIntervalMs(input.config.leaseMs),
|
|
77
|
-
leaseMs: input.config.leaseMs,
|
|
78
|
-
leaseRenewalIntervalMs: input.config.leaseRenewalIntervalMs,
|
|
79
|
-
maxConcurrentSessions: input.config.maxConcurrentSessions,
|
|
80
|
-
maxConsecutiveFailures: input.config.maxConsecutiveFailures,
|
|
81
|
-
retryBackoffMs: input.config.retryBackoffMs,
|
|
82
|
-
sessionLifetimeMs: input.config.sessionLifetimeMs
|
|
83
|
-
},
|
|
84
3764
|
deliveries: sessionDeliveries,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
},
|
|
98
|
-
onError: (error) => {
|
|
99
|
-
console.error("Background session supervisor failed", error);
|
|
100
|
-
},
|
|
101
|
-
repository: sessionRepository,
|
|
102
|
-
runStep: async ({ session, signal, wakeText }) => {
|
|
103
|
-
let runId;
|
|
104
|
-
const invocation = {
|
|
105
|
-
allowedActorOpenIds: session.origin.allowedActorOpenIds,
|
|
106
|
-
...session.origin.conversationId ? { conversationId: session.origin.conversationId } : {},
|
|
107
|
-
endpointId: session.origin.endpointId,
|
|
108
|
-
kind: "background-session",
|
|
109
|
-
...session.origin.memory ? { memory: session.origin.memory } : {},
|
|
110
|
-
sessionId: session.sessionId,
|
|
111
|
-
sourceMessageId: createBackgroundSessionStepSourceMessageId(session.sessionId, session.stepCount + 1),
|
|
112
|
-
tenantKey: session.origin.tenantKey
|
|
113
|
-
};
|
|
114
|
-
const abortPromise = new Promise((_resolve, reject) => {
|
|
115
|
-
signal.addEventListener("abort", () => {
|
|
116
|
-
if (runId) input.cancel({
|
|
117
|
-
agentId: session.authority.agentId,
|
|
118
|
-
reason: "background session step aborted",
|
|
119
|
-
runId,
|
|
120
|
-
sessionKey: session.authority.sessionKey
|
|
121
|
-
});
|
|
122
|
-
reject(/* @__PURE__ */ new Error("background session step aborted"));
|
|
123
|
-
}, { once: true });
|
|
3765
|
+
repository: sessionRepository
|
|
3766
|
+
}) : void 0;
|
|
3767
|
+
const createBackgroundSessionAdapter = (input) => {
|
|
3768
|
+
const resolveDeliverySender = async (endpointId) => {
|
|
3769
|
+
const existing = deliveryClients.get(endpointId);
|
|
3770
|
+
if (existing) return existing;
|
|
3771
|
+
const endpoint = manifest.endpoints.find(({ id }) => id === endpointId);
|
|
3772
|
+
if (!endpoint) throw new Error(`background session delivery endpoint not found: ${endpointId}`);
|
|
3773
|
+
const config = createEndpointConfig(endpoint, endpoint.agentId, context.env);
|
|
3774
|
+
const sender = createConfiguredFeishuBackgroundSessionDelivery({
|
|
3775
|
+
client: createOpenApiClient(config),
|
|
3776
|
+
config
|
|
124
3777
|
});
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
invocation,
|
|
128
|
-
onUpdate: (update) => {
|
|
129
|
-
if (update.event.type === "agent_run_accepted" && !runId) runId = update.event.runId;
|
|
130
|
-
},
|
|
131
|
-
sessionKey: session.authority.sessionKey,
|
|
132
|
-
text: wakeText
|
|
133
|
-
}).then((result) => readStepRunResult(result));
|
|
134
|
-
return Promise.race([runPromise, abortPromise]);
|
|
135
|
-
},
|
|
136
|
-
sleep
|
|
137
|
-
});
|
|
138
|
-
let running = false;
|
|
139
|
-
return {
|
|
140
|
-
running: () => running,
|
|
141
|
-
status: () => supervisor.status(),
|
|
142
|
-
start: async () => {
|
|
143
|
-
await Effect.runPromise(supervisor.recover());
|
|
144
|
-
await Effect.runPromise(supervisor.start());
|
|
145
|
-
running = true;
|
|
146
|
-
},
|
|
147
|
-
stop: async () => {
|
|
148
|
-
await Effect.runPromise(supervisor.stop());
|
|
149
|
-
running = false;
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
};
|
|
153
|
-
return {
|
|
154
|
-
dispose: () => telemetry?.shutdown(),
|
|
155
|
-
createRecoveryControl: () => openJsonlRecoveryControl({
|
|
156
|
-
endpointsDirectory: join(STATE_DIR, "endpoints"),
|
|
157
|
-
instancesDirectory: join(STATE_DIR, "instances")
|
|
158
|
-
}),
|
|
159
|
-
createBackgroundSession: backgroundSessionsConfig?.enabled ? (input) => createBackgroundSessionAdapter(input) : void 0,
|
|
160
|
-
createAutomation: async (input) => {
|
|
161
|
-
const credentials = resolveFeishuEndpointCredentials(input.deliveryEndpoint.credentialRef, context.env);
|
|
162
|
-
const config = {
|
|
163
|
-
agentId: input.definition.agentId,
|
|
164
|
-
feishu: {
|
|
165
|
-
...credentials,
|
|
166
|
-
baseUrl: input.deliveryEndpoint.baseUrl,
|
|
167
|
-
cardStreamLeaseMs: input.deliveryEndpoint.cardStreamLeaseMs,
|
|
168
|
-
streamMinIntervalMs: input.deliveryEndpoint.streamMinIntervalMs
|
|
169
|
-
},
|
|
170
|
-
pi: {}
|
|
3778
|
+
deliveryClients.set(endpointId, sender);
|
|
3779
|
+
return sender;
|
|
171
3780
|
};
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
config
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
bindingId: input.automationId,
|
|
183
|
-
delivery: {
|
|
184
|
-
endpointId: input.definition.delivery.endpointId,
|
|
185
|
-
targetKey: createHash("sha256").update(target).digest("hex"),
|
|
186
|
-
targetType: input.definition.delivery.targetType
|
|
187
|
-
},
|
|
188
|
-
schedule: input.definition.schedule,
|
|
189
|
-
servicePrincipalId: `automation:${input.automationId}`,
|
|
190
|
-
skillGrantRevision: input.definition.runtimeDefinition.skillGrantSet.revision,
|
|
191
|
-
skillIds: input.definition.template.requestedSkillIds,
|
|
192
|
-
templateId: input.definition.templateId,
|
|
193
|
-
timeZone: input.definition.timeZone,
|
|
194
|
-
toolIds: input.definition.template.requestedToolIds
|
|
195
|
-
},
|
|
196
|
-
createInput: input.definition.template.createInput,
|
|
197
|
-
deliver: ({ body, idempotencyKey, presentation }) => Effect.runPromise(sender.send({
|
|
198
|
-
idempotencyKey,
|
|
199
|
-
receiveId: target,
|
|
200
|
-
receiveIdType: input.definition.delivery.targetType,
|
|
201
|
-
markdown: body,
|
|
202
|
-
...presentation === void 0 ? {} : { presentation }
|
|
203
|
-
})),
|
|
204
|
-
onError: () => {
|
|
205
|
-
console.error(`Scheduled Automation ${input.automationId} failed; it will retry`);
|
|
3781
|
+
const supervisor = createBackgroundSessionSupervisor({
|
|
3782
|
+
clock: { now: () => (/* @__PURE__ */ new Date()).toISOString() },
|
|
3783
|
+
config: {
|
|
3784
|
+
intervalMs: resolveBackgroundSessionSupervisorIntervalMs(input.config.leaseMs),
|
|
3785
|
+
leaseMs: input.config.leaseMs,
|
|
3786
|
+
leaseRenewalIntervalMs: input.config.leaseRenewalIntervalMs,
|
|
3787
|
+
maxConcurrentSessions: input.config.maxConcurrentSessions,
|
|
3788
|
+
maxConsecutiveFailures: input.config.maxConsecutiveFailures,
|
|
3789
|
+
retryBackoffMs: input.config.retryBackoffMs,
|
|
3790
|
+
sessionLifetimeMs: input.config.sessionLifetimeMs
|
|
206
3791
|
},
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
3792
|
+
deliveries: sessionDeliveries,
|
|
3793
|
+
deliver: async (delivery) => {
|
|
3794
|
+
const session = await sessionRepository.get(delivery.sessionId);
|
|
3795
|
+
if (!session) throw new Error(`background session not found for delivery: ${delivery.sessionId}`);
|
|
3796
|
+
if (!session.origin.conversationId) throw new Error(`background session has no delivery conversation: ${delivery.sessionId}`);
|
|
3797
|
+
return (await resolveDeliverySender(session.origin.endpointId)).deliver({
|
|
3798
|
+
chatId: resolveFeishuDeliveryChatId(session.origin.conversationId),
|
|
3799
|
+
deliveryId: delivery.deliveryId,
|
|
3800
|
+
displayName: session.displayName,
|
|
3801
|
+
kind: delivery.kind,
|
|
3802
|
+
sessionId: session.sessionId,
|
|
3803
|
+
text: delivery.text
|
|
213
3804
|
});
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
3805
|
+
},
|
|
3806
|
+
onError: (error) => {
|
|
3807
|
+
console.error("Background session supervisor failed", error);
|
|
3808
|
+
},
|
|
3809
|
+
repository: sessionRepository,
|
|
3810
|
+
runStep: async ({ session, signal, wakeText }) => {
|
|
3811
|
+
let runId;
|
|
3812
|
+
const invocation = {
|
|
3813
|
+
allowedActorOpenIds: session.origin.allowedActorOpenIds,
|
|
3814
|
+
...session.origin.conversationId ? { conversationId: session.origin.conversationId } : {},
|
|
3815
|
+
endpointId: session.origin.endpointId,
|
|
3816
|
+
kind: "background-session",
|
|
3817
|
+
...session.origin.memory ? { memory: session.origin.memory } : {},
|
|
3818
|
+
sessionId: session.sessionId,
|
|
3819
|
+
sourceMessageId: createBackgroundSessionStepSourceMessageId(session.sessionId, session.stepCount + 1),
|
|
3820
|
+
tenantKey: session.origin.tenantKey
|
|
218
3821
|
};
|
|
219
|
-
|
|
3822
|
+
const abortPromise = new Promise((_resolve, reject) => {
|
|
3823
|
+
signal.addEventListener("abort", () => {
|
|
3824
|
+
if (runId) input.cancel({
|
|
3825
|
+
agentId: session.authority.agentId,
|
|
3826
|
+
reason: "background session step aborted",
|
|
3827
|
+
runId,
|
|
3828
|
+
sessionKey: session.authority.sessionKey
|
|
3829
|
+
});
|
|
3830
|
+
reject(/* @__PURE__ */ new Error("background session step aborted"));
|
|
3831
|
+
}, { once: true });
|
|
3832
|
+
});
|
|
3833
|
+
const runPromise = input.run({
|
|
3834
|
+
agentId: session.authority.agentId,
|
|
3835
|
+
invocation,
|
|
3836
|
+
onUpdate: (update) => {
|
|
3837
|
+
if (update.event.type === "agent_run_accepted" && !runId) runId = update.event.runId;
|
|
3838
|
+
},
|
|
3839
|
+
sessionKey: session.authority.sessionKey,
|
|
3840
|
+
text: wakeText
|
|
3841
|
+
}).then((result) => readStepRunResult(result));
|
|
3842
|
+
return Promise.race([runPromise, abortPromise]);
|
|
3843
|
+
},
|
|
3844
|
+
sleep
|
|
220
3845
|
});
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const sessionStore = await openJsonFeishuSessionStore({ filePath: join(endpointState, "feishu-session-store.json") });
|
|
230
|
-
const config = {
|
|
231
|
-
agentId: input.agentId,
|
|
232
|
-
feishu: {
|
|
233
|
-
...credentials,
|
|
234
|
-
baseUrl: input.definition.baseUrl,
|
|
235
|
-
cardStreamLeaseMs: input.definition.cardStreamLeaseMs,
|
|
236
|
-
streamMinIntervalMs: input.definition.streamMinIntervalMs
|
|
3846
|
+
let running = false;
|
|
3847
|
+
return {
|
|
3848
|
+
running: () => running,
|
|
3849
|
+
status: () => supervisor.status(),
|
|
3850
|
+
start: async () => {
|
|
3851
|
+
await Effect.runPromise(supervisor.recover());
|
|
3852
|
+
await Effect.runPromise(supervisor.start());
|
|
3853
|
+
running = true;
|
|
237
3854
|
},
|
|
238
|
-
|
|
3855
|
+
stop: async () => {
|
|
3856
|
+
await Effect.runPromise(supervisor.stop());
|
|
3857
|
+
running = false;
|
|
3858
|
+
}
|
|
239
3859
|
};
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
3860
|
+
};
|
|
3861
|
+
return {
|
|
3862
|
+
dispose: async () => {
|
|
3863
|
+
if (management?.enabled) await management.close();
|
|
3864
|
+
await telemetry?.shutdown();
|
|
3865
|
+
},
|
|
3866
|
+
createRecoveryControl: () => openJsonlRecoveryControl({
|
|
3867
|
+
endpointsDirectory: join(STATE_DIR, "endpoints"),
|
|
3868
|
+
instancesDirectory: join(STATE_DIR, "instances")
|
|
3869
|
+
}),
|
|
3870
|
+
createBackgroundSession: backgroundSessionsConfig?.enabled ? (input) => createBackgroundSessionAdapter(input) : void 0,
|
|
3871
|
+
createAutomation: async (input) => {
|
|
3872
|
+
const config = createEndpointConfig(input.deliveryEndpoint, input.definition.agentId, context.env);
|
|
3873
|
+
const sender = createConfiguredFeishuAutomationCardSender({
|
|
3874
|
+
client: createOpenApiClient(config),
|
|
254
3875
|
config
|
|
255
|
-
}),
|
|
256
|
-
repository: createJsonlHumanInteractionRepository({ filePath: join(endpointState, "human-interactions.jsonl") })
|
|
257
|
-
});
|
|
258
|
-
const cardRollover = createConfiguredFeishuCardRolloverRuntime({
|
|
259
|
-
agentName: input.agentId,
|
|
260
|
-
cardTargets,
|
|
261
|
-
client: openApiClient,
|
|
262
|
-
clock: createSystemClock(),
|
|
263
|
-
config,
|
|
264
|
-
ledger: cardLedger,
|
|
265
|
-
onError: (error) => {
|
|
266
|
-
console.error(`Feishu card rollover failed for endpoint ${input.endpointId}`, error);
|
|
267
|
-
},
|
|
268
|
-
...input.definition.progressDisplay === void 0 ? {} : { progressDisplay: input.definition.progressDisplay },
|
|
269
|
-
sleep,
|
|
270
|
-
title: input.agentId
|
|
271
|
-
});
|
|
272
|
-
const publisher = cardRollover.rollover;
|
|
273
|
-
const endpointEvents = createJsonlAgentEventLog({ filePath: join(STATE_DIR, "instances", input.instanceId, "agent-events.jsonl") });
|
|
274
|
-
const endpointInitialEvents = await Effect.runPromise(endpointEvents.readAll());
|
|
275
|
-
await Effect.runPromise(createFeishuCardDeliveryReconciler({
|
|
276
|
-
events: endpointInitialEvents,
|
|
277
|
-
ledger: cardLedger,
|
|
278
|
-
publish: (action) => publisher.publish(action)
|
|
279
|
-
}).reconcile());
|
|
280
|
-
const replies = createConfiguredFeishuTextReplySender({
|
|
281
|
-
client: openApiClient,
|
|
282
|
-
config
|
|
283
|
-
});
|
|
284
|
-
const prepareCardTarget = (run) => cardRollover.prepareRun(run);
|
|
285
|
-
const endpoint = createFeishuDeploymentEndpoint({
|
|
286
|
-
agentId: input.agentId,
|
|
287
|
-
botOpenId,
|
|
288
|
-
cardRollover: cardRollover.transport,
|
|
289
|
-
finalizeRun: (runId) => cardRollover.rollover.releaseRun(runId),
|
|
290
|
-
cancel: input.cancel,
|
|
291
|
-
endpointId: input.endpointId,
|
|
292
|
-
eventDispatcher: createLazyFeishuWebSocketEventDispatcher(() => new Lark.EventDispatcher({})),
|
|
293
|
-
groupPolicy: input.definition.groupPolicy,
|
|
294
|
-
handle: input.handle,
|
|
295
|
-
inboxRepository: inbox,
|
|
296
|
-
initialEvents: endpointInitialEvents,
|
|
297
|
-
interactions,
|
|
298
|
-
maxPendingMessages: 100,
|
|
299
|
-
memoryTenantId,
|
|
300
|
-
...input.projectSpaceId ? { projectSpaceId: input.projectSpaceId } : {},
|
|
301
|
-
onCapacityExceeded: (payload) => replies.reply(payload.event.message.message_id, "Rivus is busy. Please retry in a moment."),
|
|
302
|
-
prepareRun: createFeishuPresentationPreparation({
|
|
303
|
-
...cotPublisher ? { cotPublisher } : {},
|
|
304
|
-
prepareCardTarget,
|
|
305
|
-
reportCotError: (operation, error) => reportCotError(input.endpointId, operation, error)
|
|
306
|
-
}),
|
|
307
|
-
promptContext,
|
|
308
|
-
publish: (action) => publisher.publish(action),
|
|
309
|
-
reply: (messageId, text) => replies.reply(messageId, text),
|
|
310
|
-
sessionStore,
|
|
311
|
-
...input.steer ? { steer: input.steer } : {},
|
|
312
|
-
...cotPublisher ? { publishRunUpdate: (update) => cotPublisher.publish(update).pipe(Effect.catchAll((error) => reportCotError(input.endpointId, "update", error))) } : {},
|
|
313
|
-
sessionNamespace: input.definition.sessionNamespace,
|
|
314
|
-
sleep,
|
|
315
|
-
websocketClient: createLazyFeishuWebSocketClient(credentials, input.definition.baseUrl),
|
|
316
|
-
workerConcurrency: 4
|
|
317
|
-
});
|
|
318
|
-
interactionRegistry.register(input.endpointId, interactions);
|
|
319
|
-
return endpoint;
|
|
320
|
-
},
|
|
321
|
-
createRuntime: async (input) => {
|
|
322
|
-
const instanceState = join(STATE_DIR, "instances", input.instanceId);
|
|
323
|
-
await mkdir(instanceState, { recursive: true });
|
|
324
|
-
const eventLog = createJsonlAgentEventLog({ filePath: join(instanceState, "agent-events.jsonl") });
|
|
325
|
-
const initialEvents = await Effect.runPromise(eventLog.readAll());
|
|
326
|
-
const broker = createToolBroker({
|
|
327
|
-
approvals: createRoutedHumanInteractionToolApprovalService(interactionRegistry),
|
|
328
|
-
catalog: input.catalog,
|
|
329
|
-
hostTools: [...input.definition.memory.tool ? [createRivusMemoryToolDescriptor({ memory })] : [], ...backgroundService ? [...createBackgroundSessionHostTools({
|
|
330
|
-
createSessionId: () => `bg-${randomUUID()}`,
|
|
331
|
-
definition: input.definition,
|
|
332
|
-
service: backgroundService
|
|
333
|
-
})] : []],
|
|
334
|
-
operations: await openJsonlToolOperationLedger({ filePath: join(instanceState, "tool-operations.jsonl") }),
|
|
335
|
-
policy: { current: async () => ({
|
|
336
|
-
epoch: 1,
|
|
337
|
-
revokedToolIds: []
|
|
338
|
-
}) }
|
|
339
|
-
});
|
|
340
|
-
const resolveToolName = createPiToolNameResolver(input.definition.tools);
|
|
341
|
-
const skillRuntime = createPiSkillRuntime(input.definition.skills);
|
|
342
|
-
const workingDirectory = input.projectSpace?.workingDirectory ?? process.cwd();
|
|
343
|
-
const workspaceRoot = input.projectSpace?.root ?? process.cwd();
|
|
344
|
-
const workspaceInstructions = await createAgentsMdInstructionsProvider().resolve({
|
|
345
|
-
maxBytes: 64 * 1024,
|
|
346
|
-
workingDirectory: relative(workspaceRoot, workingDirectory) || ".",
|
|
347
|
-
workspaceRoot: await createWorkspaceRootHandle(workspaceRoot)
|
|
348
|
-
});
|
|
349
|
-
const prepareProjectMemory = input.projectSpace && input.definition.memory.scopes.includes("project") ? createProjectMemoryPromptPreparer({
|
|
350
|
-
agentId: input.agentId,
|
|
351
|
-
memory,
|
|
352
|
-
projectId: input.projectSpace.id
|
|
353
|
-
}) : void 0;
|
|
354
|
-
const sessionRegistry = createPiSessionRegistry({ createSession: async (firstInput) => {
|
|
355
|
-
let activeInput = firstInput;
|
|
356
|
-
const resources = await createPiSessionResources({
|
|
357
|
-
agentDir: PI_AGENT_DIR,
|
|
358
|
-
appendSystemPromptOverride: () => [workspaceInstructions.content, skillRuntime.prompt].filter((content) => content.length > 0),
|
|
359
|
-
cwd: workingDirectory,
|
|
360
|
-
homeDirectory: homedir(),
|
|
361
|
-
...input.projectSpace ? { projectSkillPaths: input.projectSpace.skillPaths } : {},
|
|
362
|
-
systemPromptOverride: () => input.definition.systemPrompt
|
|
363
3876
|
});
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
3877
|
+
const repository = await openJsonAutomationTickRepository({ filePath: join(STATE_DIR, "automations", createHash("sha256").update(input.automationId).digest("hex").slice(0, 16), "ticks.json") });
|
|
3878
|
+
const target = resolveAutomationTarget(input.definition.delivery.targetRef, context.env);
|
|
3879
|
+
return createScheduledAutomation({
|
|
3880
|
+
automationId: input.automationId,
|
|
3881
|
+
binding: {
|
|
3882
|
+
agentId: input.definition.agentId,
|
|
3883
|
+
bindingId: input.automationId,
|
|
3884
|
+
delivery: {
|
|
3885
|
+
endpointId: input.definition.delivery.endpointId,
|
|
3886
|
+
targetKey: createHash("sha256").update(target).digest("hex"),
|
|
3887
|
+
targetType: input.definition.delivery.targetType
|
|
3888
|
+
},
|
|
3889
|
+
schedule: input.definition.schedule,
|
|
3890
|
+
servicePrincipalId: `automation:${input.automationId}`,
|
|
3891
|
+
skillGrantRevision: input.definition.runtimeDefinition.skillGrantSet.revision,
|
|
3892
|
+
skillIds: input.definition.template.requestedSkillIds,
|
|
3893
|
+
templateId: input.definition.templateId,
|
|
3894
|
+
timeZone: input.definition.timeZone,
|
|
3895
|
+
toolIds: input.definition.template.requestedToolIds
|
|
3896
|
+
},
|
|
3897
|
+
createInput: input.definition.template.createInput,
|
|
3898
|
+
deliver: ({ body, idempotencyKey, presentation }) => Effect.runPromise(sender.send({
|
|
3899
|
+
idempotencyKey,
|
|
3900
|
+
receiveId: target,
|
|
3901
|
+
receiveIdType: input.definition.delivery.targetType,
|
|
3902
|
+
markdown: body,
|
|
3903
|
+
...presentation === void 0 ? {} : { presentation }
|
|
3904
|
+
})),
|
|
3905
|
+
onError: () => {
|
|
3906
|
+
console.error(`Scheduled Automation ${input.automationId} failed; it will retry`);
|
|
3907
|
+
},
|
|
3908
|
+
repository,
|
|
3909
|
+
run: async (runInput) => {
|
|
3910
|
+
const result = readAutomationRunResult(await input.run(runInput));
|
|
3911
|
+
const projected = result.body === void 0 ? void 0 : input.definition.template.createPresentation?.({
|
|
3912
|
+
occurrence: runInput.occurrence,
|
|
3913
|
+
text: result.body
|
|
3914
|
+
});
|
|
3915
|
+
const presentation = projected ? readAutomationPresentation(projected) : void 0;
|
|
3916
|
+
return {
|
|
3917
|
+
...result,
|
|
3918
|
+
...presentation === void 0 ? {} : { presentation }
|
|
3919
|
+
};
|
|
3920
|
+
}
|
|
3921
|
+
});
|
|
3922
|
+
},
|
|
3923
|
+
createEndpoint: async (input) => {
|
|
3924
|
+
const credentials = resolveFeishuEndpointCredentials(input.definition.credentialRef, context.env);
|
|
3925
|
+
const botOpenId = await resolveFeishuBotOpenId(credentials, input.definition.baseUrl);
|
|
3926
|
+
const endpointState = join(STATE_DIR, "endpoints", input.endpointId);
|
|
3927
|
+
const cardTargets = createJsonFileFeishuCardTargetRegistry({ filePath: join(endpointState, "feishu-card-targets.json") });
|
|
3928
|
+
const cardLedger = await openJsonlFeishuCardDeliveryLedger({ filePath: join(endpointState, "feishu-card-delivery.jsonl") });
|
|
3929
|
+
const inbox = await openJsonlFeishuInboxRepository({ filePath: join(endpointState, "feishu-inbox.jsonl") });
|
|
3930
|
+
const sessionStore = await openJsonFeishuSessionStore({ filePath: join(endpointState, "feishu-session-store.json") });
|
|
3931
|
+
const config = createEndpointConfig(input.definition, input.agentId, context.env);
|
|
3932
|
+
const openApiClient = createOpenApiClient(config);
|
|
3933
|
+
const promptContext = createFeishuTopicContextResolver({
|
|
3934
|
+
baseUrl: input.definition.baseUrl,
|
|
3935
|
+
client: openApiClient
|
|
3936
|
+
});
|
|
3937
|
+
const cotPublisher = input.definition.experimental?.cotMessages ? createFeishuCotPublisher({
|
|
3938
|
+
baseUrl: resolveExperimentalCotBaseUrl(context.env),
|
|
3939
|
+
client: openApiClient,
|
|
3940
|
+
minIntervalMs: input.definition.streamMinIntervalMs
|
|
3941
|
+
}) : void 0;
|
|
3942
|
+
const interactions = createHumanInteractionService({
|
|
3943
|
+
clock: { now: () => (/* @__PURE__ */ new Date()).toISOString() },
|
|
3944
|
+
presenter: createConfiguredFeishuHumanInteractionPresenter({
|
|
3945
|
+
client: openApiClient,
|
|
3946
|
+
config
|
|
369
3947
|
}),
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
3948
|
+
repository: createJsonlHumanInteractionRepository({ filePath: join(endpointState, "human-interactions.jsonl") })
|
|
3949
|
+
});
|
|
3950
|
+
const cardRollover = createConfiguredFeishuCardRolloverRuntime({
|
|
3951
|
+
agentName: input.agentId,
|
|
3952
|
+
cardTargets,
|
|
3953
|
+
client: openApiClient,
|
|
3954
|
+
clock: createSystemClock(),
|
|
3955
|
+
config,
|
|
3956
|
+
ledger: cardLedger,
|
|
3957
|
+
onError: (error) => {
|
|
3958
|
+
console.error(`Feishu card rollover failed for endpoint ${input.endpointId}`, error);
|
|
3959
|
+
},
|
|
3960
|
+
...input.definition.progressDisplay === void 0 ? {} : { progressDisplay: input.definition.progressDisplay },
|
|
3961
|
+
sleep,
|
|
3962
|
+
title: input.agentId
|
|
3963
|
+
});
|
|
3964
|
+
const publisher = cardRollover.rollover;
|
|
3965
|
+
const endpointEvents = createJsonlAgentEventLog({ filePath: join(STATE_DIR, "instances", input.instanceId, "agent-events.jsonl") });
|
|
3966
|
+
const endpointInitialEvents = await Effect.runPromise(endpointEvents.readAll());
|
|
3967
|
+
await Effect.runPromise(createFeishuCardDeliveryReconciler({
|
|
3968
|
+
events: endpointInitialEvents,
|
|
3969
|
+
ledger: cardLedger,
|
|
3970
|
+
publish: (action) => publisher.publish(action)
|
|
3971
|
+
}).reconcile());
|
|
3972
|
+
const replies = createConfiguredFeishuTextReplySender({
|
|
3973
|
+
client: openApiClient,
|
|
3974
|
+
config
|
|
3975
|
+
});
|
|
3976
|
+
const prepareCardTarget = (run) => cardRollover.prepareRun(run);
|
|
3977
|
+
const endpoint = createFeishuDeploymentEndpoint({
|
|
3978
|
+
agentId: input.agentId,
|
|
3979
|
+
botOpenId,
|
|
3980
|
+
cardRollover: cardRollover.transport,
|
|
3981
|
+
finalizeRun: (runId) => cardRollover.rollover.releaseRun(runId),
|
|
3982
|
+
cancel: input.cancel,
|
|
3983
|
+
endpointId: input.endpointId,
|
|
3984
|
+
eventDispatcher: createLazyFeishuWebSocketEventDispatcher(() => new Lark.EventDispatcher({})),
|
|
3985
|
+
groupPolicy: input.definition.groupPolicy,
|
|
3986
|
+
handle: input.handle,
|
|
3987
|
+
inboxRepository: inbox,
|
|
3988
|
+
initialEvents: endpointInitialEvents,
|
|
3989
|
+
interactions,
|
|
3990
|
+
maxPendingMessages: 100,
|
|
3991
|
+
memoryTenantId,
|
|
3992
|
+
...input.projectSpaceId ? { projectSpaceId: input.projectSpaceId } : {},
|
|
3993
|
+
onCapacityExceeded: (payload) => replies.reply(payload.event.message.message_id, "Rivus is busy. Please retry in a moment."),
|
|
3994
|
+
prepareRun: createFeishuPresentationPreparation({
|
|
3995
|
+
...cotPublisher ? { cotPublisher } : {},
|
|
3996
|
+
prepareCardTarget,
|
|
3997
|
+
reportCotError: (operation, error) => reportCotError(input.endpointId, operation, error)
|
|
379
3998
|
}),
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
...
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
3999
|
+
promptContext,
|
|
4000
|
+
publish: (action) => publisher.publish(action),
|
|
4001
|
+
reply: (messageId, text) => replies.reply(messageId, text),
|
|
4002
|
+
sessionStore,
|
|
4003
|
+
...input.steer ? { steer: input.steer } : {},
|
|
4004
|
+
...cotPublisher ? { publishRunUpdate: (update) => cotPublisher.publish(update).pipe(Effect.catchAll((error) => reportCotError(input.endpointId, "update", error))) } : {},
|
|
4005
|
+
sessionNamespace: input.definition.sessionNamespace,
|
|
4006
|
+
sleep,
|
|
4007
|
+
websocketClient: createLazyFeishuWebSocketClient(credentials, input.definition.baseUrl),
|
|
4008
|
+
workerConcurrency: 4
|
|
4009
|
+
});
|
|
4010
|
+
interactionRegistry.register(input.endpointId, interactions);
|
|
4011
|
+
return endpoint;
|
|
4012
|
+
},
|
|
4013
|
+
createRuntime: async (input) => {
|
|
4014
|
+
const instanceState = join(STATE_DIR, "instances", input.instanceId);
|
|
4015
|
+
await mkdir(instanceState, { recursive: true });
|
|
4016
|
+
const eventLog = createJsonlAgentEventLog({ filePath: join(instanceState, "agent-events.jsonl") });
|
|
4017
|
+
const initialEvents = await Effect.runPromise(eventLog.readAll());
|
|
4018
|
+
const broker = createToolBroker$1({
|
|
4019
|
+
approvals: createRoutedHumanInteractionToolApprovalService(interactionRegistry),
|
|
4020
|
+
catalog: input.catalog,
|
|
4021
|
+
hostTools: [...input.definition.memory.tool ? [createRivusMemoryToolDescriptor({ memory })] : [], ...backgroundService ? [...createBackgroundSessionHostTools({
|
|
4022
|
+
createSessionId: () => `bg-${randomUUID()}`,
|
|
4023
|
+
definition: input.definition,
|
|
4024
|
+
service: backgroundService
|
|
4025
|
+
})] : []],
|
|
4026
|
+
operations: await openJsonlToolOperationLedger$1({ filePath: join(instanceState, "tool-operations.jsonl") }),
|
|
4027
|
+
policy: { current: async () => ({
|
|
4028
|
+
epoch: 1,
|
|
4029
|
+
revokedToolIds: []
|
|
4030
|
+
}) }
|
|
4031
|
+
});
|
|
4032
|
+
const resolveToolName = createPiToolNameResolver(input.definition.tools);
|
|
4033
|
+
const skillRuntime = createPiSkillRuntime(input.definition.skills);
|
|
4034
|
+
const workingDirectory = input.projectSpace?.workingDirectory ?? process.cwd();
|
|
4035
|
+
const runContexts = management?.enabled ? management.createRunContexts({
|
|
4036
|
+
agentId: input.agentId,
|
|
4037
|
+
instanceId: input.instanceId,
|
|
4038
|
+
toolGrantSet: input.definition.toolGrantSet
|
|
4039
|
+
}) : void 0;
|
|
4040
|
+
const workspaceRoot = input.projectSpace?.root ?? process.cwd();
|
|
4041
|
+
const workspaceInstructions = await createAgentsMdInstructionsProvider().resolve({
|
|
4042
|
+
maxBytes: 64 * 1024,
|
|
4043
|
+
workingDirectory: relative(workspaceRoot, workingDirectory) || ".",
|
|
4044
|
+
workspaceRoot: await createWorkspaceRootHandle(workspaceRoot)
|
|
4045
|
+
});
|
|
4046
|
+
const prepareProjectMemory = input.projectSpace && input.definition.memory.scopes.includes("project") ? createProjectMemoryPromptPreparer({
|
|
4047
|
+
agentId: input.agentId,
|
|
4048
|
+
memory,
|
|
4049
|
+
projectId: input.projectSpace.id
|
|
4050
|
+
}) : void 0;
|
|
4051
|
+
const sessionRegistry = createPiSessionRegistry({ createSession: async (firstInput) => {
|
|
4052
|
+
let activeInput = firstInput;
|
|
4053
|
+
const managedContext = management?.enabled && runContexts && input.definition.runtimeToolGrantSet.toolIds.includes("bash") ? createPiManagedSessionContext({
|
|
4054
|
+
binDirectory: management.binDirectory,
|
|
4055
|
+
contexts: runContexts.registry,
|
|
4056
|
+
cwd: workingDirectory,
|
|
4057
|
+
nodeExecutable: process.execPath,
|
|
4058
|
+
socketPath: management.socketPath
|
|
4059
|
+
}) : void 0;
|
|
4060
|
+
const resources = await createPiSessionResources({
|
|
4061
|
+
agentDir: PI_AGENT_DIR,
|
|
4062
|
+
appendSystemPromptOverride: () => [workspaceInstructions.content, skillRuntime.prompt].filter((content) => content.length > 0),
|
|
4063
|
+
cwd: workingDirectory,
|
|
4064
|
+
homeDirectory: homedir(),
|
|
4065
|
+
...input.projectSpace ? { projectSkillPaths: input.projectSpace.skillPaths } : {},
|
|
4066
|
+
systemPromptOverride: () => input.definition.systemPrompt
|
|
4067
|
+
});
|
|
4068
|
+
const customTools = [
|
|
4069
|
+
...managedContext ? [managedContext.tool] : [],
|
|
4070
|
+
...createPiSkillReadTools({
|
|
4071
|
+
cwd: workingDirectory,
|
|
4072
|
+
runtimeToolIds: input.definition.runtimeToolGrantSet.toolIds,
|
|
4073
|
+
skillPaths: resources.skillNames.size > 0 ? resources.skillPaths : []
|
|
4074
|
+
}),
|
|
4075
|
+
...createPiToolProxyDefinitions({
|
|
4076
|
+
agentId: input.agentId,
|
|
4077
|
+
approvals: createHumanInteractionToolApprovalGateway({ registry: interactionRegistry }),
|
|
4078
|
+
broker,
|
|
4079
|
+
getActiveInput: () => activeInput,
|
|
4080
|
+
instanceId: input.instanceId,
|
|
4081
|
+
memoryScopes: input.definition.memory.scopes,
|
|
4082
|
+
toolGrantSet: input.definition.toolGrantSet,
|
|
4083
|
+
tools: input.definition.tools
|
|
4084
|
+
}),
|
|
4085
|
+
...skillRuntime.tool ? [skillRuntime.tool] : []
|
|
4086
|
+
];
|
|
4087
|
+
const activeToolNames = resolvePiSessionToolNames(managedContext ? input.definition.runtimeToolGrantSet.toolIds.filter((toolId) => toolId !== "bash") : input.definition.runtimeToolGrantSet.toolIds, customTools);
|
|
4088
|
+
const result = await createAgentSession(resources.withSessionOptions({
|
|
4089
|
+
...piOptions,
|
|
4090
|
+
...management?.enabled ? management.runtime.getSessionOptions() : {},
|
|
4091
|
+
customTools,
|
|
4092
|
+
excludeTools: [],
|
|
4093
|
+
sessionManager: SessionManager.create(workingDirectory, join(instanceState, "sessions")),
|
|
4094
|
+
tools: [...activeToolNames]
|
|
4095
|
+
}));
|
|
4096
|
+
return {
|
|
4097
|
+
activate: (loopInput) => {
|
|
4098
|
+
activeInput = loopInput;
|
|
4099
|
+
managedContext?.activate(loopInput);
|
|
4100
|
+
},
|
|
4101
|
+
deactivate: () => managedContext?.deactivate(),
|
|
4102
|
+
dispose: () => result.session.dispose(),
|
|
4103
|
+
preparePrompt: async (loopInput) => {
|
|
4104
|
+
validatePiSkillCommand(loopInput.text, resources.skillNames);
|
|
4105
|
+
return prepareProjectMemory ? prepareProjectMemory(loopInput) : loopInput.text;
|
|
4106
|
+
},
|
|
4107
|
+
resolveToolName,
|
|
4108
|
+
refreshResources: () => resources.refresh(),
|
|
4109
|
+
session: result.session
|
|
4110
|
+
};
|
|
4111
|
+
} });
|
|
4112
|
+
const unregisterModels = management?.enabled ? management.runtime.registerSessionRegistry(sessionRegistry) : void 0;
|
|
4113
|
+
const loop = createPiAgentLoop({
|
|
4114
|
+
disposeSessionAfterRun: false,
|
|
4115
|
+
...management?.enabled ? { runBoundary: management.boundary } : {},
|
|
4116
|
+
...telemetry ? { modelContentObserver: telemetry.modelContentObserver } : {},
|
|
4117
|
+
resolveSession: (loopInput) => sessionRegistry.resolve(loopInput)
|
|
4118
|
+
});
|
|
4119
|
+
const scheduler = createSessionScheduler({
|
|
4120
|
+
createRuntime: (sessionKey) => createAgentHarnessPooledRuntime(createAgentHarness({
|
|
4121
|
+
clock: createSystemClock(),
|
|
4122
|
+
eventSinks: telemetry ? [eventLog, telemetry.sink] : [eventLog],
|
|
4123
|
+
initialEvents: eventsForSession(initialEvents, sessionKey),
|
|
4124
|
+
loop,
|
|
4125
|
+
runIds: createUuidRunIds(),
|
|
4126
|
+
...input.binding.kind === "background-session" && backgroundSessionsConfig ? { runTimeoutMs: backgroundSessionsConfig.stepTimeoutMs } : {}
|
|
4127
|
+
})),
|
|
4128
|
+
maxConcurrentSessions: input.binding.kind === "background-session" ? backgroundSessionsConfig?.maxConcurrentSessions ?? 4 : 4,
|
|
4129
|
+
maxQueuedRuns: 32
|
|
4130
|
+
});
|
|
390
4131
|
return {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
},
|
|
399
|
-
resolveToolName,
|
|
400
|
-
session: result.session
|
|
4132
|
+
...scheduler,
|
|
4133
|
+
dispose: async () => {
|
|
4134
|
+
await scheduler.dispose?.();
|
|
4135
|
+
await sessionRegistry.disposeAll();
|
|
4136
|
+
unregisterModels?.();
|
|
4137
|
+
runContexts?.unregister();
|
|
4138
|
+
}
|
|
401
4139
|
};
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const scheduler = createSessionScheduler({
|
|
409
|
-
createRuntime: (sessionKey) => createAgentHarnessPooledRuntime(createAgentHarness({
|
|
410
|
-
clock: createSystemClock(),
|
|
411
|
-
eventSinks: telemetry ? [eventLog, telemetry.sink] : [eventLog],
|
|
412
|
-
initialEvents: eventsForSession(initialEvents, sessionKey),
|
|
413
|
-
loop,
|
|
414
|
-
runIds: createUuidRunIds(),
|
|
415
|
-
...input.binding.kind === "background-session" && backgroundSessionsConfig ? { runTimeoutMs: backgroundSessionsConfig.stepTimeoutMs } : {}
|
|
416
|
-
})),
|
|
417
|
-
maxConcurrentSessions: input.binding.kind === "background-session" ? backgroundSessionsConfig?.maxConcurrentSessions ?? 4 : 4,
|
|
418
|
-
maxQueuedRuns: 32
|
|
419
|
-
});
|
|
420
|
-
return {
|
|
421
|
-
...scheduler,
|
|
422
|
-
dispose: async () => {
|
|
423
|
-
await scheduler.dispose?.();
|
|
424
|
-
await sessionRegistry.disposeAll();
|
|
425
|
-
}
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
};
|
|
4140
|
+
}
|
|
4141
|
+
};
|
|
4142
|
+
} catch (error) {
|
|
4143
|
+
await Promise.allSettled([management?.enabled ? management.close() : Promise.resolve(), telemetry?.shutdown()]);
|
|
4144
|
+
throw error;
|
|
4145
|
+
}
|
|
429
4146
|
}
|
|
430
4147
|
function eventsForSession(events, sessionKey) {
|
|
431
4148
|
const runIds = new Set(events.filter((event) => event.sessionKey === sessionKey).map((event) => event.runId));
|
|
@@ -531,23 +4248,24 @@ async function resolveFeishuBotOpenId(credentials, domain) {
|
|
|
531
4248
|
if (response.code !== 0 || !openId) throw new Error(`Feishu bot identity lookup failed: ${response.msg ?? "response missing bot.open_id"}`);
|
|
532
4249
|
return openId;
|
|
533
4250
|
}
|
|
534
|
-
async function createPiSessionOptions(context) {
|
|
4251
|
+
async function createPiSessionOptions(context, deferModel = false) {
|
|
535
4252
|
const modelReference = optional(context.env.PI_MODEL);
|
|
536
4253
|
const configuredModel = modelReference ? parseModelReference(modelReference) : void 0;
|
|
537
4254
|
const provider = configuredModel?.provider;
|
|
538
4255
|
const apiKey = await resolvePiApiKey(context.env);
|
|
539
4256
|
const baseUrl = optional(context.env.PI_BASE_URL);
|
|
540
4257
|
if ((apiKey || baseUrl) && !provider) throw new Error("PI_API_KEY and PI_BASE_URL require PI_MODEL in provider/model form");
|
|
541
|
-
if (baseUrl) await writeProviderBaseUrlOverride(provider, baseUrl);
|
|
4258
|
+
if (baseUrl && !deferModel) await writeProviderBaseUrlOverride(provider, baseUrl);
|
|
542
4259
|
const modelRuntime = await ModelRuntime.create({
|
|
543
4260
|
allowModelNetwork: false,
|
|
544
4261
|
authPath: PI_AUTH_FILE,
|
|
545
|
-
modelsPath: baseUrl ? PI_MODELS_FILE : null
|
|
4262
|
+
modelsPath: baseUrl || deferModel ? PI_MODELS_FILE : null
|
|
546
4263
|
});
|
|
547
4264
|
if (apiKey) await modelRuntime.setRuntimeApiKey(provider, apiKey);
|
|
548
|
-
const model = configuredModel ? modelRuntime.getModel(configuredModel.provider, configuredModel.modelId) : void 0;
|
|
549
|
-
if (modelReference && !model) throw new Error(`PI_MODEL ${modelReference} was not found in the Pi model runtime`);
|
|
550
|
-
const
|
|
4265
|
+
const model = configuredModel && !deferModel ? modelRuntime.getModel(configuredModel.provider, configuredModel.modelId) : void 0;
|
|
4266
|
+
if (modelReference && !model && !deferModel) throw new Error(`PI_MODEL ${modelReference} was not found in the Pi model runtime`);
|
|
4267
|
+
const settings = deferModel ? SettingsManager.create(process.cwd(), PI_AGENT_DIR, { projectTrusted: false }) : void 0;
|
|
4268
|
+
const thinkingLevel = readThinkingLevel(context.env.PI_THINKING_LEVEL) ?? (configuredModel ? settings?.getModelThinkingLevel(configuredModel.provider, configuredModel.modelId) : void 0) ?? settings?.getDefaultThinkingLevel();
|
|
551
4269
|
return {
|
|
552
4270
|
cwd: process.cwd(),
|
|
553
4271
|
modelRuntime,
|
|
@@ -555,6 +4273,18 @@ async function createPiSessionOptions(context) {
|
|
|
555
4273
|
...thinkingLevel ? { thinkingLevel } : {}
|
|
556
4274
|
};
|
|
557
4275
|
}
|
|
4276
|
+
function createEndpointConfig(definition, agentId, env) {
|
|
4277
|
+
return {
|
|
4278
|
+
agentId,
|
|
4279
|
+
feishu: {
|
|
4280
|
+
...resolveFeishuEndpointCredentials(definition.credentialRef, env),
|
|
4281
|
+
baseUrl: definition.baseUrl,
|
|
4282
|
+
cardStreamLeaseMs: definition.cardStreamLeaseMs,
|
|
4283
|
+
streamMinIntervalMs: definition.streamMinIntervalMs
|
|
4284
|
+
},
|
|
4285
|
+
pi: {}
|
|
4286
|
+
};
|
|
4287
|
+
}
|
|
558
4288
|
async function resolvePiApiKey(env) {
|
|
559
4289
|
const inline = optional(env.PI_API_KEY);
|
|
560
4290
|
const filePath = optional(env.PI_API_KEY_FILE);
|