@rivus/agent 0.16.1 → 0.16.6

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