UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

513 lines (512 loc) 20.8 kB
import { a as asOptionalRecord } from "./record-coerce-DItp3I4t.js"; import { n as normalizeAgentId } from "./agent-id-CeT3w4ap.js"; import { m as resolveAgentWorkspaceDir, o as listAgentIds } from "./agent-scope-config-DcbEhP0R.js"; import { t as pruneMapToMaxSize } from "./map-size-CNcWiFKu.js"; import "./session-key-BnWWjqNc.js"; import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js"; import { t as createDeferredCore } from "./deferred-D0La5CRk.js"; import { S as resolveRuntimeConfigCacheKey } from "./runtime-snapshot-BaQikjTR.js"; import { A as resolveCollapsedSessionAuthPinSource, j as resolveSessionAuthProfileOverrideSource } from "./agent-scope-DbtJyKUL.js"; import { f as getActivePluginRegistryVersion } from "./runtime-BL4wZfTq.js"; import { a as parseUserModelAuthProfileId, i as isUserModelAuthProfileId } from "./profile-usage-stats-dkZh2v8y.js"; import { s as readUserModelAccountSummary } from "./user-model-accounts-BMkC9h8s.js"; import { l as getRuntimeAuthProfileStoreSnapshotRevision } from "./runtime-snapshots-CHAErv1O.js"; import { s as getPreparedRuntimeAuthProfileStoreSnapshot } from "./store-F1B2duCT.js"; import { n as getPreparedModelFullCatalogAuth, r as getPreparedModelRuntimeAuthMaterializations } from "./prepared-model-runtime-auth-CnrySjUa.js"; import { c as resolveUserProfileId, p as getUserProfileDisplay } from "./user-profiles-4AB7AmiH.js"; import { i as getPublishedPreparedModelCatalogOwnerSnapshot } from "./prepared-model-catalog-Bt4dYhnY.js"; import "./auth-profiles-BdUEhE7u.js"; import { r as getSkillsSnapshotVersion } from "./refresh-state-DHnXO3IV.js"; import { n as resolveSessionModelRef } from "./session-model-ref-CPZiclLt.js"; import { t as readSessionRuntimeOwnership } from "./session-runtime-ownership-PPRaZqWL.js"; import { t as resolveSwarmConfig } from "./swarm-config-XTe8M2h4.js"; //#region src/gateway/server-methods/chat-account-selection.ts /** The session owns this preference; it is not a receipt for the account that served a turn. */ function resolveChatAccountSelection(params) { const authProfileId = params.sessionEntry?.authProfileOverride?.trim(); if (!authProfileId) return { kind: "automatic", label: "Automatic account selection" }; const source = resolveSessionAuthProfileOverrideSource(params.sessionEntry); if (!isUserModelAuthProfileId(authProfileId)) return { kind: "shared", authProfileId, label: (params.authStore.profiles[authProfileId]?.displayName?.trim() || authProfileId).slice(0, 256), source }; const personal = params.requesterProfileId ? readUserModelAccountSummary({ profileId: params.requesterProfileId, authProfileId }) : void 0; if (personal) return { kind: "personal", authProfileId, label: personal.label, source }; const locator = parseUserModelAuthProfileId(authProfileId); const owner = locator ? resolveUserProfileId(locator.ownerProfileId) : void 0; const displayName = owner ? getUserProfileDisplay(owner).displayName?.trim() : void 0; return { kind: "personal", label: displayName ? `${displayName}'s account`.slice(0, 256) : "Personal account", source }; } //#endregion //#region src/gateway/server-methods/chat-metadata-session-projection.ts async function prepareChatMetadataModelProjection(params) { const { prepareModelsListResult, createGatewayAgentModelCatalogProjector } = await import("./models-list-result-C6qbOlEO.js"); params.assertCurrent?.(); const snapshot = params.facts.modelCatalog; const projector = createGatewayAgentModelCatalogProjector({ cfg: params.facts.owner.config, agentId: params.facts.agentId, snapshot, metadataSnapshot: params.facts.owner.metadataSnapshot, preparedAuthStore: params.facts.authStore, requesterProfileId: params.requesterProfileId, preparedRuntimeAuthModes: params.facts.authModes, preparedRuntimeAuthMaterializations: getPreparedModelRuntimeAuthMaterializations(params.facts.owner), pluginRegistry: params.facts.owner.pluginRegistry, isCurrent: params.facts.owner.isCurrent, observationConfig: params.facts.owner.observationConfig, ...params.preferredProfileId ? { preferredProfileId: params.preferredProfileId } : {}, ...params.pinnedProfileId ? { pinnedProfileId: params.pinnedProfileId } : {} }); const [modelCatalog, readModels] = await Promise.all([projector.projectCatalog(), prepareModelsListResult({ context: params.context, agentId: params.facts.agentId, params: { view: "configured" }, preloadedCatalog: { agentId: params.facts.agentId, config: params.facts.owner.config, snapshot }, preloadedOnly: true, catalogProjector: projector })]); return { modelCatalog, read: () => ({ models: readModels.read().models }), isCurrent: readModels.isCurrent }; } function projectChatSessionMetadata(readParams, metadata, config) { const ownership = readSessionRuntimeOwnership({ ...readParams, config }); if (ownership?.auth !== "native" || !metadata.models) return metadata; const renderedModel = ownership.modelRef ?? resolveSessionModelRef(config, readParams.sessionEntry, readParams.agentId, { allowPluginNormalization: false }); return { ...metadata, models: metadata.models.map((model) => { const row = asOptionalRecord(model); if (row?.provider !== renderedModel.provider || row.id !== renderedModel.model) return model; const { available: _available, unavailableReason: _reason, unavailableUntil: _until, ...native } = row; return native; }) }; } //#endregion //#region src/gateway/server-methods/chat-metadata-runtime.ts const CHAT_METADATA_CACHE_MAX_ENTRIES = 64; function createMetadataReplacement() { const replacement = createDeferredCore(); replacement.promise.catch(() => {}); return replacement; } var ChatMetadataSnapshotUnavailableError = class extends Error { constructor(message = "prepared chat metadata snapshot is unavailable") { super(message); this.name = "ChatMetadataSnapshotUnavailableError"; } }; function captureGenerationFacts(deps) { const config = deps.getConfig(); const agents = listAgentIds(config).map((rawAgentId) => { const agentId = normalizeAgentId(rawAgentId); const owner = deps.getPreparedOwner({ agentId, config }); if (!owner) throw new ChatMetadataSnapshotUnavailableError(`prepared chat metadata owner is unavailable for agent "${agentId}"`); const workspaceDir = owner.workspaceDir ?? resolveAgentWorkspaceDir(config, agentId); const fullModelCatalog = owner.readFullModelCatalog?.(); const fullCatalogAuth = fullModelCatalog ? getPreparedModelFullCatalogAuth(fullModelCatalog) : void 0; if (fullModelCatalog && !fullCatalogAuth) throw new Error("prepared full model catalog omitted its auth generation"); return { agentId, owner, authStore: fullCatalogAuth?.authStore ?? deps.getPreparedAuthStore(owner.agentDir, owner.inheritedAuthDir) ?? { version: 1, profiles: {} }, authModes: fullCatalogAuth?.authModes ?? owner.authModes, authStoreRevision: `${deps.getAuthStoreRevision(owner.agentDir)}:${deps.getAuthStoreRevision(owner.inheritedAuthDir)}`, modelCatalog: fullModelCatalog ?? owner.modelCatalog, skillsVersion: deps.getSkillsVersion(workspaceDir) }; }); return { config, configKey: resolveRuntimeConfigCacheKey(config), pluginRegistryVersion: deps.getPluginRegistryVersion(), agents }; } function generationFactsMatch(left, right) { if (left.configKey !== right.configKey || left.pluginRegistryVersion !== right.pluginRegistryVersion || left.agents.length !== right.agents.length) return false; return left.agents.every((agent, index) => { const candidate = right.agents[index]; return candidate?.agentId === agent.agentId && candidate.owner === agent.owner && candidate.authStoreRevision === agent.authStoreRevision && candidate.modelCatalog === agent.modelCatalog && candidate.skillsVersion === agent.skillsVersion; }); } function resolveSessionProfiles(sessionEntry) { const profileId = sessionEntry?.authProfileOverride?.trim(); if (!profileId) return {}; return { preferredProfileId: profileId, ...resolveCollapsedSessionAuthPinSource(sessionEntry) === "user" ? { pinnedProfileId: profileId } : {} }; } function sessionProjectionKey(agentId, profiles) { return [ normalizeAgentId(agentId), profiles.preferredProfileId ?? "", profiles.pinnedProfileId ?? "" ].join("\0"); } async function defaultBuildCommands(params) { const { buildCommandsListResult } = await import("./commands-list-result-CjzeHmCr.js"); return buildCommandsListResult({ cfg: params.cfg, agentId: params.agentId, includeArgs: true, scope: "text" }); } function createGatewayChatMetadataRuntime(params) { const deps = { getConfig: params.getConfig, getContext: params.getContext, getPreparedOwner: getPublishedPreparedModelCatalogOwnerSnapshot, getPreparedAuthStore: getPreparedRuntimeAuthProfileStoreSnapshot, getAuthStoreRevision: getRuntimeAuthProfileStoreSnapshotRevision, getSkillsVersion: getSkillsSnapshotVersion, getPluginRegistryVersion: getActivePluginRegistryVersion, buildCommands: defaultBuildCommands, buildProjection: prepareChatMetadataModelProjection, ...params.deps }; let current; let lastError; let replacement; let invalidationEpoch = 0; let refreshVersion = 0; let lastSettlement; let refreshTail = Promise.resolve(); let stoppedError; const activeWork = /* @__PURE__ */ new Set(); const trackWork = (work) => { activeWork.add(work); const settled = () => activeWork.delete(work); work.then(settled, settled); return work; }; const assertOpen = () => { if (stoppedError) throw stoppedError; }; let pending; const projectAgent = async (generation, agent, sessionEntry, requesterProfileId, assertCurrent, useRequesterDefaults = false) => { assertOpen(); assertCurrent?.(); const profiles = resolveSessionProfiles(sessionEntry); const neutral = profiles.preferredProfileId === void 0 && profiles.pinnedProfileId === void 0; const defaultProfileId = useRequesterDefaults ? requesterProfileId : void 0; const projections = neutral && defaultProfileId || isUserModelAuthProfileId(profiles.preferredProfileId ?? "") ? /* @__PURE__ */ new Map() : neutral ? generation.neutralProjectionByAgentId : generation.sessionProjectionByKey; const key = neutral ? agent.agentId : sessionProjectionKey(agent.agentId, profiles); const existing = projections.get(key); if (existing) { const prepared = existing.state === "ready" ? existing.projection : await existing.promise; if (prepared.isCurrent()) return prepared; if (projections.get(key) === existing) projections.delete(key); return projectAgent(generation, agent, sessionEntry, requesterProfileId, assertCurrent, useRequesterDefaults); } const projection = deps.buildProjection({ context: deps.getContext(), facts: agent, requesterProfileId: defaultProfileId, ...assertCurrent ? { assertCurrent } : {}, ...profiles }).then((prepared) => { assertCurrent?.(); const preparedProjection = { ...prepared, read: () => { assertCurrent?.(); return { ...prepared.read(), ...agent.commands !== void 0 ? { commands: agent.commands } : {}, swarmEnabled: agent.swarmEnabled, accountSelection: resolveChatAccountSelection({ authStore: agent.authStore, sessionEntry, requesterProfileId }) }; } }; if (generation.epoch === invalidationEpoch && projections.get(key) === entry) projections.set(key, { state: "ready", projection: preparedProjection }); return preparedProjection; }).catch((error) => { if (projections.get(key) === entry) projections.delete(key); throw error; }); trackWork(projection); const entry = { state: "pending", promise: projection }; projections.set(key, entry); if (!neutral) pruneMapToMaxSize(projections, CHAT_METADATA_CACHE_MAX_ENTRIES); return projection; }; const buildGeneration = async (facts, epoch) => { const agents = await Promise.all(facts.agents.map(async (agent) => { let commands; try { commands = (await trackWork(deps.buildCommands({ cfg: facts.config, agentId: agent.agentId }))).commands; } catch (error) { params.log.warn(`chat metadata continuing without text commands for ${agent.agentId}: ${formatErrorMessage(error)}`); } return { ...agent, ...commands !== void 0 ? { commands } : {}, swarmEnabled: resolveSwarmConfig(facts.config, agent.agentId).enabled }; })); const generation = { epoch, facts, agentsById: new Map(agents.map((agent) => [agent.agentId, agent])), neutralProjectionByAgentId: /* @__PURE__ */ new Map(), sessionProjectionByKey: /* @__PURE__ */ new Map() }; if (epoch !== invalidationEpoch) return; await Promise.all(agents.map((agent) => projectAgent(generation, agent))); return generation; }; const runRefresh = async (version) => { assertOpen(); await params.beforeRefresh?.(); if (version !== refreshVersion) return; for (;;) { const epoch = invalidationEpoch; try { const facts = captureGenerationFacts(deps); if (current && generationFactsMatch(current.facts, facts)) return; const generation = await buildGeneration(facts, epoch); if (version !== refreshVersion) return; if (generation && epoch === invalidationEpoch && generationFactsMatch(facts, captureGenerationFacts(deps))) { current = generation; return; } } catch (error) { if (version !== refreshVersion) return; if (epoch === invalidationEpoch) throw error; } } }; const refresh = () => { if (stoppedError) return Promise.reject(stoppedError); const trackRefresh = (promise, facts) => { refreshTail = promise; pending = { ...facts ? { facts } : {}, promise }; promise.then(() => { if (pending?.promise !== promise) return; pending = void 0; if (current?.epoch !== invalidationEpoch) return; lastError = void 0; const committedReplacement = replacement; replacement = void 0; committedReplacement?.resolve(); if (lastSettlement !== current) { lastSettlement = current; params.onChanged?.(); } }, (error) => { if (pending?.promise !== promise) return; pending = void 0; fail(error); }); return promise; }; if (params.beforeRefresh) { if (pending) return pending.promise; const version = ++refreshVersion; return trackRefresh(refreshTail.catch(() => {}).then(() => runRefresh(version))); } let facts; try { facts = captureGenerationFacts(deps); } catch (error) { const refreshError = error instanceof Error ? error : new Error(formatErrorMessage(error)); fail(refreshError); return Promise.reject(refreshError); } if (current && generationFactsMatch(current.facts, facts)) return Promise.resolve(); if (pending?.facts && generationFactsMatch(pending.facts, facts)) return pending.promise; const version = ++refreshVersion; return trackRefresh(refreshTail.catch(() => {}).then(() => runRefresh(version)), facts); }; const readCurrent = async (project) => { for (;;) { assertOpen(); const replacementPromise = replacement?.promise; if (replacementPromise) { await replacementPromise; continue; } const refreshPromise = pending?.promise; if (refreshPromise) { await refreshPromise; continue; } let generation = current; const retryUnavailableOwner = lastError instanceof ChatMetadataSnapshotUnavailableError; if (!generation && (params.refreshOnRead || retryUnavailableOwner)) { await refresh(); generation = current; } if (!generation) { if (lastError) throw lastError; throw new ChatMetadataSnapshotUnavailableError(); } if (params.refreshOnRead) { let latest; try { latest = captureGenerationFacts(deps); } catch { await refresh(); generation = current; } if (latest && generation && !generationFactsMatch(generation.facts, latest)) { await refresh(); generation = current; } } if (!generation) throw new ChatMetadataSnapshotUnavailableError(); if (params.refreshOnRead) { const latest = captureGenerationFacts(deps); if (!generationFactsMatch(generation.facts, latest)) throw new ChatMetadataSnapshotUnavailableError("prepared chat metadata snapshot is stale while its replacement is publishing"); } try { const readProjection = await project(generation); if (current === generation && generation.epoch === invalidationEpoch && readProjection.isCurrent()) return readProjection.read(); } catch (error) { if (current === generation && generation.epoch === invalidationEpoch) throw error; } } }; const read = async (readParams) => { const draft = readParams.draftAccountSelection; const sessionEntry = draft ? { authProfileOverride: draft.authProfileId, authProfileOverrideSource: "user" } : readParams.sessionEntry; return await readCurrent(async (generation) => { const agentId = normalizeAgentId(readParams.agentId); const agent = generation.agentsById.get(agentId); if (!agent) throw new ChatMetadataSnapshotUnavailableError(`prepared chat metadata is unavailable for agent "${agentId}"`); const projection = await projectAgent(generation, agent, sessionEntry, draft?.owner ?? readParams.requesterProfileId, draft?.assertCurrent, !readParams.sessionKey && !readParams.sessionEntry); return { isCurrent: projection.isCurrent, read: () => projectChatSessionMetadata(readParams, projection.read(), deps.getConfig()) }; }); }; const readStartup = async (readParams) => { const profiles = resolveSessionProfiles(readParams.sessionEntry); const assemble = (neutral, session) => ({ ...readParams.readPolicy === "ready" ? {} : { metadata: projectChatSessionMetadata(readParams, session.read(), deps.getConfig()) }, sessionModelCatalog: session.modelCatalog, defaultModelCatalog: neutral.modelCatalog }); const projectStartup = async (generation) => { const agentId = normalizeAgentId(readParams.agentId); const agent = generation.agentsById.get(agentId); if (!agent) throw new ChatMetadataSnapshotUnavailableError(`prepared chat startup projection is unavailable for agent "${agentId}"`); const readNeutral = await projectAgent(generation, agent); const readSession = profiles.preferredProfileId ? await projectAgent(generation, agent, readParams.sessionEntry, readParams.requesterProfileId) : readNeutral; return { isCurrent: () => readNeutral.isCurrent() && readSession.isCurrent(), read: () => assemble(readNeutral, readSession) }; }; if (readParams.readPolicy !== "ready" && profiles.preferredProfileId) return readCurrent(projectStartup); if (isUserModelAuthProfileId(profiles.preferredProfileId ?? "")) return; const generation = current; if (!generation || replacement || pending || generation.epoch !== invalidationEpoch) return; if (params.refreshOnRead) try { if (!generationFactsMatch(generation.facts, captureGenerationFacts(deps))) return; } catch { return; } const agentId = normalizeAgentId(readParams.agentId); const neutral = generation.neutralProjectionByAgentId.get(agentId); const session = profiles.preferredProfileId ? generation.sessionProjectionByKey.get(sessionProjectionKey(agentId, profiles)) : neutral; if (neutral?.state !== "ready" || session?.state !== "ready" || !neutral.projection.isCurrent() || !session.projection.isCurrent()) return; return assemble(neutral.projection, session.projection); }; const invalidate = () => { if (stoppedError) return; invalidationEpoch += 1; current = void 0; lastError = void 0; replacement ??= createMetadataReplacement(); }; const fail = (error) => { const replacementError = error instanceof Error ? error : new Error(formatErrorMessage(error)); refreshVersion += 1; pending = void 0; current = void 0; lastError = replacementError; const failedReplacement = replacement; replacement = void 0; failedReplacement?.reject(replacementError); if (!stoppedError && lastSettlement !== invalidationEpoch) { lastSettlement = invalidationEpoch; params.onChanged?.(); } }; const stop = async () => { if (!stoppedError) { stoppedError = new ChatMetadataSnapshotUnavailableError("gateway chat metadata runtime is stopped"); invalidationEpoch += 1; fail(stoppedError); } while (activeWork.size > 0) await Promise.allSettled(activeWork); }; return { fail, invalidate, read: (readParams) => trackWork(read(readParams)), readStartup: (startupParams) => trackWork(readStartup(startupParams)), refresh: () => trackWork(refresh()), stop }; } //#endregion export { ChatMetadataSnapshotUnavailableError, createGatewayChatMetadataRuntime };