UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

353 lines (352 loc) 14 kB
import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js"; import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js"; import { t as isPromiseLike } from "./promise-like-D7-l5Fsp.js"; import { t as isPluginJsonValue } from "./host-hook-json-BdcyDerH.js"; import { h as getActivePluginSessionExtensionRegistry, u as getActivePluginRegistry } from "./runtime-BL4wZfTq.js"; import { t as buildPluginAgentTurnPrepareContext } from "./host-hooks-sVMn958c.js"; import { t as normalizeSessionEntrySlotKey } from "./session-entry-slot-keys-DWTKggcI.js"; import "./session-accessor-YsytfDtG.js"; import { a as resolveSessionEntryAccessTarget, c as updateResolvedSessionEntry } from "./session-accessor.entry-CQ2WPpfn.js"; import { randomUUID } from "node:crypto"; //#region src/plugins/host-hook-state.ts const log = createSubsystemLogger("plugins/host-hook-state"); const PROJECTION_FAILED = Symbol("plugin-session-extension-projection-failed"); const MAX_PLUGIN_NEXT_TURN_INJECTION_TEXT_LENGTH = 32768; const MAX_PLUGIN_NEXT_TURN_INJECTION_IDEMPOTENCY_KEY_LENGTH = 512; const MAX_PLUGIN_NEXT_TURN_INJECTIONS_PER_SESSION = 32; function normalizeNamespace(value) { return value.trim(); } function copyJsonValue(value) { return structuredClone(value); } function isPluginNextTurnInjectionPlacement(value) { return value === "prepend_context" || value === "append_context"; } function isPluginNextTurnInjectionRecord(value) { if (!value || typeof value !== "object") return false; const candidate = value; return typeof candidate.id === "string" && typeof candidate.pluginId === "string" && typeof candidate.text === "string" && typeof candidate.createdAt === "number" && Number.isFinite(candidate.createdAt) && isPluginNextTurnInjectionPlacement(candidate.placement) && (candidate.ttlMs === void 0 || typeof candidate.ttlMs === "number" && Number.isFinite(candidate.ttlMs) && candidate.ttlMs >= 0) && (candidate.idempotencyKey === void 0 || typeof candidate.idempotencyKey === "string"); } function isExpired(entry, now) { if (!isPluginNextTurnInjectionRecord(entry)) return true; return typeof entry.ttlMs === "number" && entry.ttlMs >= 0 && now - entry.createdAt > entry.ttlMs; } function isPluginPromptInjectionEnabled(cfg, pluginId) { return (cfg.plugins?.entries?.[pluginId])?.hooks?.allowPromptInjection !== false; } function toPluginNextTurnInjectionRecord(params) { return { id: params.injection.idempotencyKey?.trim() || randomUUID(), pluginId: params.pluginId, pluginName: params.pluginName, text: params.injection.text, idempotencyKey: params.injection.idempotencyKey?.trim() || void 0, placement: params.injection.placement ?? "prepend_context", ttlMs: params.injection.ttlMs, createdAt: params.now, metadata: params.injection.metadata }; } async function enqueuePluginNextTurnInjection(params) { if (typeof params.injection.sessionKey !== "string") return { enqueued: false, id: "", sessionKey: "" }; const sessionKey = params.injection.sessionKey.trim(); if (!sessionKey) return { enqueued: false, id: "", sessionKey }; if (typeof params.injection.text !== "string") return { enqueued: false, id: "", sessionKey }; const text = params.injection.text.trim(); if (!text) return { enqueued: false, id: "", sessionKey }; if (text.length > MAX_PLUGIN_NEXT_TURN_INJECTION_TEXT_LENGTH) return { enqueued: false, id: "", sessionKey }; if (params.injection.metadata !== void 0 && !isPluginJsonValue(params.injection.metadata)) return { enqueued: false, id: "", sessionKey }; if (params.injection.idempotencyKey !== void 0 && (typeof params.injection.idempotencyKey !== "string" || params.injection.idempotencyKey.trim().length === 0 || params.injection.idempotencyKey.length > MAX_PLUGIN_NEXT_TURN_INJECTION_IDEMPOTENCY_KEY_LENGTH)) return { enqueued: false, id: "", sessionKey }; if (params.injection.placement !== void 0 && !isPluginNextTurnInjectionPlacement(params.injection.placement)) return { enqueued: false, id: "", sessionKey }; if (params.injection.ttlMs !== void 0 && (!Number.isFinite(params.injection.ttlMs) || params.injection.ttlMs < 0)) return { enqueued: false, id: "", sessionKey }; const now = params.now ?? Date.now(); const record = toPluginNextTurnInjectionRecord({ pluginId: params.pluginId, pluginName: params.pluginName, injection: { ...params.injection, sessionKey, text }, now }); const scope = { cfg: params.cfg, sessionKey, agentId: params.injection.agentId }; const updated = await updateResolvedSessionEntry(scope, (entry) => { let enqueued = false; let resultId = record.id; const injections = { ...entry.pluginNextTurnInjections }; const rawExisting = injections[params.pluginId]; const existing = (Array.isArray(rawExisting) ? [...rawExisting] : []).filter((candidate) => !isExpired(candidate, now)); const duplicate = record.idempotencyKey ? existing.find((candidate) => candidate.idempotencyKey === record.idempotencyKey) : void 0; if (duplicate) { resultId = duplicate.id; injections[params.pluginId] = existing; entry.pluginNextTurnInjections = injections; return { enqueued, id: resultId }; } if (existing.length >= MAX_PLUGIN_NEXT_TURN_INJECTIONS_PER_SESSION) { injections[params.pluginId] = existing; entry.pluginNextTurnInjections = injections; return { enqueued, id: resultId }; } injections[params.pluginId] = [...existing, record]; entry.pluginNextTurnInjections = injections; entry.updatedAt = now; enqueued = true; return { enqueued, id: resultId }; }); if (!updated.found) return { enqueued: false, id: "", sessionKey }; return { ...updated.result, sessionKey: updated.canonicalKey }; } async function drainPluginNextTurnInjections(params) { const sessionKey = params.sessionKey?.trim(); if (!sessionKey) return []; const scope = { cfg: params.cfg, sessionKey, agentId: params.agentId }; const target = resolveSessionEntryAccessTarget(scope); if (!target.entry) return []; if (!target.entry.pluginNextTurnInjections || Object.keys(target.entry.pluginNextTurnInjections).length === 0) return []; const now = params.now ?? Date.now(); const updated = await updateResolvedSessionEntry(scope, (entry) => { if (!entry?.pluginNextTurnInjections) return []; const activePluginIds = new Set((getActivePluginRegistry()?.plugins ?? []).filter((plugin) => plugin.status === "loaded").map((plugin) => plugin.id)); const drained = []; for (const [pluginId, entries] of Object.entries(entry.pluginNextTurnInjections)) { if (!activePluginIds.has(pluginId) || !isPluginPromptInjectionEnabled(params.cfg, pluginId)) continue; if (!Array.isArray(entries)) continue; const liveEntries = entries.filter((candidate) => !isExpired(candidate, now)); drained.push(...liveEntries); } drained.sort((left, right) => left.createdAt - right.createdAt); delete entry.pluginNextTurnInjections; if (drained.length > 0) entry.updatedAt = now; return drained; }); return updated.found ? updated.result : []; } async function drainPluginNextTurnInjectionContext(params) { const queuedInjections = await drainPluginNextTurnInjections(params); return { queuedInjections, ...buildPluginAgentTurnPrepareContext({ queuedInjections }) }; } function getPluginSessionExtensionStateSync(params) { const pluginId = params.pluginId.trim(); const sessionKey = normalizeOptionalString(params.sessionKey); if (!pluginId || !sessionKey) return; const value = resolveSessionEntryAccessTarget({ cfg: params.cfg, sessionKey, agentId: params.agentId }).entry?.pluginExtensions?.[pluginId]; return value ? copyJsonValue(value) : void 0; } async function patchPluginSessionExtension(params) { const namespace = normalizeNamespace(params.namespace); const pluginId = params.pluginId.trim(); if (!pluginId || !namespace) return { ok: false, error: "pluginId and namespace are required" }; if (params.unset === true && params.value !== void 0) return { ok: false, error: "plugin session extension cannot specify both unset and value" }; if (params.value !== void 0 && !isPluginJsonValue(params.value)) return { ok: false, error: "plugin session extension value must be JSON-compatible" }; if (params.unset !== true && params.value === void 0) return { ok: false, error: "plugin session extension value is required unless unset is true" }; const nextPluginValue = params.value; const registration = (getActivePluginSessionExtensionRegistry()?.sessionExtensions ?? []).find((entry) => entry.pluginId === pluginId && entry.extension.namespace === namespace); if (!registration) return { ok: false, error: `unknown plugin session extension: ${pluginId}/${namespace}` }; const rawSlotKey = normalizeOptionalString(registration.extension.sessionEntrySlotKey); const normalizedSlotKey = rawSlotKey ? normalizeSessionEntrySlotKey(rawSlotKey) : void 0; if (normalizedSlotKey?.ok === false) log.warn(`plugin session extension slot promotion skipped for ${pluginId}/${namespace}: ${normalizedSlotKey.error}`); const slotKey = normalizedSlotKey?.ok === true ? normalizedSlotKey.key : void 0; const updated = await updateResolvedSessionEntry({ cfg: params.cfg, sessionKey: params.sessionKey, agentId: params.agentId }, (entry, context) => { params.assertCurrent?.(); const entryRecord = entry; const pluginExtensions = { ...entry.pluginExtensions }; const pluginState = { ...pluginExtensions[pluginId] }; if (params.unset === true) delete pluginState[namespace]; else pluginState[namespace] = copyJsonValue(nextPluginValue); if (Object.keys(pluginState).length > 0) pluginExtensions[pluginId] = pluginState; else delete pluginExtensions[pluginId]; if (Object.keys(pluginExtensions).length > 0) entry.pluginExtensions = pluginExtensions; else delete entry.pluginExtensions; const storedSlotKeys = { ...entry.pluginExtensionSlotKeys }; const pluginSlotKeys = { ...storedSlotKeys[pluginId] }; const previousSlotKey = normalizeSessionEntrySlotKey(pluginSlotKeys[namespace]); if (previousSlotKey.ok && previousSlotKey.key !== slotKey) delete entryRecord[previousSlotKey.key]; if (slotKey && params.unset !== true) pluginSlotKeys[namespace] = slotKey; else delete pluginSlotKeys[namespace]; if (Object.keys(pluginSlotKeys).length > 0) storedSlotKeys[pluginId] = pluginSlotKeys; else delete storedSlotKeys[pluginId]; if (Object.keys(storedSlotKeys).length > 0) entry.pluginExtensionSlotKeys = storedSlotKeys; else delete entry.pluginExtensionSlotKeys; if (slotKey) { const projected = projectSessionExtensionValueForSlot({ registration, sessionKey: context.canonicalKey, sessionId: entry.sessionId, nextValue: params.unset === true ? void 0 : nextPluginValue }); if (projected === void 0) delete entryRecord[slotKey]; else entryRecord[slotKey] = projected; } entry.updatedAt = Date.now(); return pluginState[namespace]; }); if (!updated.found) return { ok: false, error: `unknown session key: ${params.sessionKey}` }; return { ok: true, key: updated.canonicalKey, value: updated.result }; } /** * Resolve the value that should be mirrored to `SessionEntry[slotKey]` for a * promoted session-extension namespace. Failures are swallowed so a * misbehaving projector cannot block the primary patch from being persisted. */ function projectSessionExtensionValueForSlot(params) { if (params.nextValue === void 0) return; const projected = projectSessionExtensionValue({ pluginId: params.registration.pluginId, namespace: params.registration.extension.namespace, project: params.registration.extension.project, sessionKey: params.sessionKey, sessionId: params.sessionId, state: params.nextValue }); if (projected === PROJECTION_FAILED) return; if (isPromiseLike(projected)) { discardUnexpectedPromiseProjection(projected); return; } if (projected === void 0 || !isPluginJsonValue(projected)) return; return copyJsonValue(projected); } function collectPluginSessionExtensionProjections(params) { const extensions = getActivePluginSessionExtensionRegistry()?.sessionExtensions ?? []; if (extensions.length === 0) return []; const projections = []; for (const registration of extensions) { const state = params.entry.pluginExtensions?.[registration.pluginId]?.[registration.extension.namespace]; if (state === void 0) continue; const projected = projectSessionExtensionValue({ pluginId: registration.pluginId, namespace: registration.extension.namespace, project: registration.extension.project, sessionKey: params.sessionKey, sessionId: params.entry.sessionId, state }); if (projected === PROJECTION_FAILED) continue; if (isPromiseLike(projected)) { discardUnexpectedPromiseProjection(projected); continue; } if (projected !== void 0 && isPluginJsonValue(projected)) projections.push({ pluginId: registration.pluginId, namespace: registration.extension.namespace, value: copyJsonValue(projected) }); } return projections; } function discardUnexpectedPromiseProjection(value) { Promise.resolve(value).catch(() => void 0); } function projectSessionExtensionValue(params) { try { return params.project ? params.project({ sessionKey: params.sessionKey, sessionId: params.sessionId, state: params.state }) : params.state; } catch (error) { log.warn(`plugin session extension projection failed: plugin=${params.pluginId} namespace=${params.namespace} error=${String(error)}`); return PROJECTION_FAILED; } } function projectPluginSessionExtensionsSync(params) { return collectPluginSessionExtensionProjections(params); } //#endregion export { projectPluginSessionExtensionsSync as a, patchPluginSessionExtension as i, enqueuePluginNextTurnInjection as n, getPluginSessionExtensionStateSync as r, drainPluginNextTurnInjectionContext as t };