UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

510 lines (509 loc) • 20.8 kB
import { t as createDeferredCore } from "./deferred-D0La5CRk.mjs"; import { t as pruneMapToMaxSize } from "./map-size-BlrH_CnE.mjs"; import { t as formatErrorMessage } from "./errors-O33D01XW.mjs"; import { n as normalizeAgentId } from "./agent-id-D1-Q6vR4.mjs"; import { t as captureOpenClawStateReadContext } from "./openclaw-state-worker-context-DRG_I6tz.mjs"; import "./session-key-C09ow4h0.mjs"; import { d as getActivePluginRegistryVersion } from "./runtime-NsVZTERq.mjs"; import { n as assertAgentDatabaseAdmitted } from "./agent-database-admission-dWNS6Rcs.mjs"; import { v as isUserModelAuthProfileId } from "./profile-usage-stats-BLwxBqRR.mjs"; import { a as getPreparedRuntimeAuthProfileStoreSnapshot } from "./store-Bn_NKkGr.mjs"; import { P as captureUserProfileModelAccountLinksAuthority } from "./user-profiles-internal-DlO1LBbo.mjs"; import { a as listUserProfileAuthLinks, o as listUserProfileAuthLinksAsync } from "./user-model-accounts-sYn17idc.mjs"; import { l as getRuntimeAuthProfileStoreMetadataRevision } from "./runtime-snapshots-kS-hnXTd.mjs"; import { o as getPreparedModelRuntimeAuthMaterializations } from "./prepared-model-runtime-auth-Dn7HF9Vu.mjs"; import "./auth-profiles-DnH-eEC0.mjs"; import { r as PreparedModelRuntimePublicationSupersededError } from "./prepared-model-runtime.errors-5KDV2tDJ.mjs"; import { r as getPublishedPreparedModelCatalogOwnerSnapshot } from "./prepared-model-catalog-CntkE0sm.mjs"; import { t as resolveSwarmConfig } from "./swarm-config-a1kURD5f.mjs"; import { r as getSkillsSnapshotVersion } from "./refresh-state-CRa-OKC_.mjs"; import { n as captureGenerationFacts, r as generationFactsMatch, t as ChatMetadataSnapshotUnavailableError } from "./chat-metadata-facts-BMynAc64.mjs"; import { a as resolveSessionCatalogProfiles, n as prepareChatMetadataModelProjection, o as sessionProjectionKey, r as projectChatSessionMetadata, s as resolveChatAccountSelection, t as hasSessionCatalogContext } from "./chat-metadata-session-projection-CuWYURUs.mjs"; //#region src/gateway/server-methods/chat-metadata-model-list.ts /** Model-list variants are derived facts of the existing chat metadata generation. */ function createChatMetadataModelList(params) { const agents = new Map(params.facts.agents.map((agent) => [agent.agentId, agent])); for (const [key, request] of params.requested) if (!agents.has(request.agentId)) params.requested.delete(key); const projections = /* @__PURE__ */ new Map(); const prepare = (request) => { const key = JSON.stringify([ request.agentId, request.params, request.includeManualSelection ]); const existing = projections.get(key); if (existing) return existing.then((projection) => { if (projection.isCurrent()) return projection; if (projections.get(key) === existing) projections.delete(key); return prepare(request); }); const facts = agents.get(request.agentId); if (!facts?.owner.catalogOwner || !facts.owner.isCurrent()) return Promise.reject(new PreparedModelRuntimePublicationSupersededError("Model catalog changed while preparing this result. Retry the request.")); const { owner, modelCatalog, authStore, authModes } = facts; const catalogOwner = facts.owner.catalogOwner; const pending = import("./models-list-result-DLi9cgeV.mjs").then(({ prepareModelsListResult }) => prepareModelsListResult({ ...request, source: { kind: "published", getConfig: params.context.getRuntimeConfig, owner: { ...owner, catalogOwner, agentId: request.agentId, workspaceDir: catalogOwner.workspaceDir, modelCatalog, authStore, authModes, authMaterializations: getPreparedModelRuntimeAuthMaterializations(owner) } }, preloadedOnly: true })).then((projection) => { params.requested.set(key, { ...request, params: { ...request.params } }); pruneMapToMaxSize(params.requested, params.maxEntries); return projection; }); projections.set(key, pending); pruneMapToMaxSize(projections, params.maxEntries); pending.catch(() => { if (projections.get(key) === pending) projections.delete(key); }); return pending; }; return { prepare, async read(request) { const { requesterProfileId, ...shared } = request; if (requesterProfileId && request.params.view !== "provider-config") { const authority = captureUserProfileModelAccountLinksAuthority(captureOpenClawStateReadContext().admission, requesterProfileId); const links = await listUserProfileAuthLinksAsync(requesterProfileId); if (!authority()) return { isCurrent: () => false, read: () => ({ models: [] }) }; if (links.length > 0) return; const projection = await prepare(shared); return { isCurrent: () => authority() && projection.isCurrent(), read: () => ({ ...projection.read(), accountSelection: { kind: "automatic", label: "Automatic account selection" } }) }; } return prepare(shared); } }; } //#endregion //#region src/gateway/server-methods/chat-metadata-runtime.ts const CHAT_METADATA_CACHE_MAX_ENTRIES = 64; function readPreparedChatMetadata(projection, readParams, config) { readParams.draftAccountSelection?.assertCurrent(); const { agent } = projection; return projectChatSessionMetadata(readParams, { ...projection.read(), ...agent.commands !== void 0 ? { commands: agent.commands } : {}, swarmEnabled: agent.swarmEnabled, accountSelection: resolveChatAccountSelection({ authStore: agent.authStore, sessionEntry: readParams.sessionEntry, requesterProfileId: readParams.requesterProfileId }) }, config); } function createGatewayChatMetadataRuntime(params) { const deps = { getConfig: params.getConfig, getContext: params.getContext, getPreparedOwner: getPublishedPreparedModelCatalogOwnerSnapshot, getPreparedAuthStore: getPreparedRuntimeAuthProfileStoreSnapshot, getAuthStoreRevision: getRuntimeAuthProfileStoreMetadataRevision, getSkillsVersion: getSkillsSnapshotVersion, getPluginRegistryVersion: getActivePluginRegistryVersion, buildCommands: async ({ cfg, agentId }) => { const { buildCommandsListResult } = await import("./commands-list-result-sVHwGr1E.mjs"); return buildCommandsListResult({ cfg, agentId, includeArgs: true, scope: "text" }); }, buildProjection: prepareChatMetadataModelProjection, ...params.deps }; const requestedModelLists = /* @__PURE__ */ new Map(); let current; let lastError; let replacement; let invalidationEpoch = 0; let refreshVersion = 0; let lastSettlement; let lastNotifiedFacts; const notifyChanged = (facts, catalogStatusChanged = false) => { const previous = lastNotifiedFacts; lastNotifiedFacts = facts; params.onChanged?.({ modelCatalogChanged: catalogStatusChanged || !previous || !facts || !generationFactsMatch(previous, facts, "catalog"), authChanged: !previous || !facts || !generationFactsMatch(previous, facts, "auth") }); }; 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?.(); if (!agent.owner.isCurrent()) throw new ChatMetadataSnapshotUnavailableError(`prepared chat metadata owner retired for agent "${agent.agentId}"`); const profiles = resolveSessionCatalogProfiles(sessionEntry, agent.owner.config, agent.agentId); const neutral = !hasSessionCatalogContext(profiles); const defaultProfileId = useRequesterDefaults && !profiles.preferredProfileId && requesterProfileId && listUserProfileAuthLinks(requesterProfileId).length > 0 ? requesterProfileId : void 0; const projections = !profiles.preferredProfileId && 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, agent }; 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); pruneMapToMaxSize(projections, CHAT_METADATA_CACHE_MAX_ENTRIES); return projection; }; const prepareAgent = (generation, agentId) => { const existing = generation.agentsById.get(agentId); if (existing) return existing; const agent = generation.facts.agents.find((candidate) => candidate.agentId === agentId); if (!agent) return; const preparing = trackWork((async () => { let commands; try { commands = (await deps.buildCommands({ cfg: generation.facts.config, agentId })).commands; } catch (error) { params.log.warn(`chat metadata continuing without text commands for ${agentId}: ${formatErrorMessage(error)}`); } return { ...agent, ...commands !== void 0 ? { commands } : {}, swarmEnabled: resolveSwarmConfig(generation.facts.config, agentId).enabled }; })().catch((error) => { if (generation.agentsById.get(agentId) === preparing) generation.agentsById.delete(agentId); throw error; })); generation.agentsById.set(agentId, preparing); pruneMapToMaxSize(generation.agentsById, CHAT_METADATA_CACHE_MAX_ENTRIES); return preparing; }; const runRefresh = async (version) => { if (version !== refreshVersion) return; assertOpen(); try { await params.beforeRefresh?.(); if (version !== refreshVersion) return; const facts = captureGenerationFacts(deps); if (current && generationFactsMatch(current.facts, facts)) return; current = { epoch: invalidationEpoch, facts, modelLists: createChatMetadataModelList({ facts, context: deps.getContext(), requested: requestedModelLists, maxEntries: CHAT_METADATA_CACHE_MAX_ENTRIES }), agentsById: /* @__PURE__ */ new Map(), neutralProjectionByAgentId: /* @__PURE__ */ new Map(), sessionProjectionByKey: /* @__PURE__ */ new Map() }; await Promise.allSettled([...requestedModelLists.values()].map(current.modelLists.prepare)); } catch (error) { if (version !== refreshVersion) return; throw error; } }; const refresh = (options = {}) => { if (stoppedError) return Promise.reject(stoppedError); let facts; if (params.beforeRefresh) { if (pending) { pending.notifyIfUnchanged ||= options.notifyIfUnchanged === true; return pending.promise; } } else { 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)) { if (options.notifyIfUnchanged) notifyChanged(current.facts, true); return Promise.resolve(); } if (pending?.facts && generationFactsMatch(pending.facts, facts)) { pending.notifyIfUnchanged ||= options.notifyIfUnchanged === true; return pending.promise; } if (current || pending) invalidate(true); } const version = ++refreshVersion; const promise = refreshTail.catch(() => {}).then(() => runRefresh(version)); refreshTail = promise; const generationReady = createDeferredCore(); pending = { ...facts ? { facts } : {}, promise, generationReady, notifyIfUnchanged: options.notifyIfUnchanged === true }; promise.then(() => { generationReady.resolve(); if (pending?.promise !== promise) return; const notifyIfUnchanged = pending.notifyIfUnchanged; pending = void 0; if (current?.epoch !== invalidationEpoch) return; lastError = void 0; const committedReplacement = replacement; replacement = void 0; committedReplacement?.resolve(); if (lastSettlement !== current || notifyIfUnchanged) { lastSettlement = current; notifyChanged(current.facts, notifyIfUnchanged); } }, (error) => { generationReady.resolve(); if (pending?.promise !== promise) return; pending = void 0; fail(error); }); return promise; }; const authStoresCurrent = (generation) => generation.facts.agents.every(({ owner, authStoreRevision }) => authStoreRevision === `${deps.getAuthStoreRevision(owner.agentDir)}:${deps.getAuthStoreRevision(owner.inheritedAuthDir)}`); const isCurrentGeneration = (generation) => current === generation && generation.epoch === invalidationEpoch && authStoresCurrent(generation); const readCurrent = async (project) => { for (;;) { assertOpen(); const preparation = pending; if (preparation) { await preparation.generationReady.promise; continue; } let generation = current; const replacementPromise = replacement?.promise; if (!generation && replacementPromise) { await replacementPromise; continue; } 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 && !authStoresCurrent(generation)) { await refresh(); continue; } 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 (isCurrentGeneration(generation) && readProjection.isCurrent()) return readProjection.read(); } catch (error) { if (isCurrentGeneration(generation)) throw error; } } }; const readModelsList = async (request) => { assertAgentDatabaseAdmitted(request.agentId); return readCurrent(async (generation) => { return await generation.modelLists.read(request) ?? { isCurrent: () => true, read: () => void 0 }; }); }; const read = async (readParams) => { assertAgentDatabaseAdmitted(readParams.agentId); const draft = readParams.draftAccountSelection; const isCurrent = readParams.isCurrent; const assertCurrent = isCurrent ? () => { if (!isCurrent()) throw new PreparedModelRuntimePublicationSupersededError("Chat metadata access changed while preparing its metadata. Retry the request."); draft?.assertCurrent(); } : draft?.assertCurrent; const sessionEntry = draft ? { authProfileOverride: draft.authProfileId, authProfileOverrideSource: "user" } : readParams.sessionEntry; return await readCurrent(async (generation) => { const agentId = normalizeAgentId(readParams.agentId); const agent = await prepareAgent(generation, agentId); if (!agent) throw new ChatMetadataSnapshotUnavailableError(`prepared chat metadata is unavailable for agent "${agentId}"`); readParams.assertCurrent?.(); const projection = await projectAgent(generation, agent, sessionEntry, draft?.owner ?? readParams.requesterProfileId, assertCurrent, !readParams.sessionKey && !readParams.sessionEntry); return { isCurrent: projection.isCurrent, read: () => readPreparedChatMetadata(projection, { ...readParams, sessionEntry, requesterProfileId: draft?.owner ?? readParams.requesterProfileId }, deps.getConfig()) }; }); }; const readStartup = async (readParams) => { assertAgentDatabaseAdmitted(readParams.agentId); const profiles = resolveSessionCatalogProfiles(readParams.sessionEntry, deps.getConfig(), readParams.agentId); const hasSessionContext = hasSessionCatalogContext(profiles); const assemble = (neutral, session) => ({ ...readParams.readPolicy === "ready" ? {} : { metadata: readPreparedChatMetadata(session, { ...readParams, requesterProfileId: readParams.readRequesterProfileId?.() }, deps.getConfig()) }, sessionModelCatalog: session.modelCatalog, defaultModelCatalog: neutral.modelCatalog }); const projectStartup = async (generation) => { const agentId = normalizeAgentId(readParams.agentId); const agent = await prepareAgent(generation, agentId); if (!agent) throw new ChatMetadataSnapshotUnavailableError(`prepared chat startup projection is unavailable for agent "${agentId}"`); const readNeutral = await projectAgent(generation, agent); const readSession = hasSessionContext ? await projectAgent(generation, agent, readParams.sessionEntry, readParams.readRequesterProfileId?.()) : readNeutral; return { isCurrent: () => readNeutral.isCurrent() && readSession.isCurrent(), read: () => assemble(readNeutral, readSession) }; }; if (readParams.readPolicy !== "ready" && hasSessionContext) return readCurrent(projectStartup); if (isUserModelAuthProfileId(profiles.preferredProfileId ?? "")) return; const generation = current; if (!generation || replacement || pending || !isCurrentGeneration(generation)) 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 = hasSessionContext ? 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 = (retainNotifiedFacts = false) => { if (stoppedError) return; if (!retainNotifiedFacts) lastNotifiedFacts = void 0; invalidationEpoch += 1; refreshVersion += 1; pending?.generationReady.resolve(); pending = void 0; current = void 0; lastError = void 0; if (!replacement) { replacement = createDeferredCore(); replacement.promise.catch(() => {}); } }; const fail = (error) => { const replacementError = error instanceof Error ? error : new Error(formatErrorMessage(error)); refreshVersion += 1; pending?.generationReady.resolve(); pending = void 0; current = void 0; lastError = replacementError; const failedReplacement = replacement; replacement = void 0; failedReplacement?.reject(replacementError); if (!stoppedError && lastSettlement !== invalidationEpoch) { lastSettlement = invalidationEpoch; notifyChanged(); } }; const stop = async () => { if (!stoppedError) { stoppedError = new ChatMetadataSnapshotUnavailableError("gateway chat metadata runtime is stopped"); invalidationEpoch += 1; fail(stoppedError); requestedModelLists.clear(); } while (activeWork.size > 0) await Promise.allSettled(activeWork); }; return { fail, invalidate, read: (readParams) => trackWork(read(readParams)), readModelsList: (request) => trackWork(readModelsList(request)), readStartup: (startupParams) => trackWork(readStartup(startupParams)), refresh: (options) => trackWork(refresh(options)), stop }; } //#endregion export { createGatewayChatMetadataRuntime };