UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

293 lines (292 loc) 13.9 kB
import { u as runSqliteImmediateTransactionSync } from "./node-sqlite-BpQX3W0e.js"; import { a as getNodeSqliteKysely, i as executeSqliteQueryTakeFirstSync, r as executeSqliteQuerySync } from "./kysely-sync-COmh4HWh.js"; import { D as ensureOpenClawAgentStandingIntentsSchema } from "./openclaw-agent-db-maintenance-wTIy-jt-.js"; import { p as openOpenClawAgentDatabase } from "./openclaw-agent-db-CWtDoRbC.js"; import "./sqlite-runtime-BpLlraG0.js"; import { randomUUID } from "node:crypto"; //#region extensions/memory-core/src/standing-intents.ts const DEFAULT_INTENT_COOLDOWN_SECONDS = 86400; const DEFAULT_INTENT_MAX_FIRES = 3; const DEFAULT_INTENT_EXPIRY_MS = 7776e6; const INTENT_MATCH_CANDIDATE_BATCH_SIZE = 32; const INTENT_MATCH_CANDIDATE_LIMIT = 256; const INTENT_INJECTION_MAX_COUNT = 3; const INTENT_INJECTION_MAX_CHARS = 1200; function withStandingIntentDatabase(agentId, callback) { const db = openOpenClawAgentDatabase({ agentId }).db; ensureOpenClawAgentStandingIntentsSchema(db); return callback(db); } function normalizeScopeIdentity(value, field, lowercase = false) { const normalized = value.trim(); if (!normalized) throw new Error(`${field} is unavailable for this creating turn`); return lowercase ? normalized.toLowerCase() : normalized; } function normalizeScopeAccountId(accountId) { return accountId?.trim() || "default"; } function normalizeCreatorSender(value) { const creatorSender = value.trim(); if (!creatorSender || creatorSender.toLowerCase() === "unknown") throw new Error("creating sender is unavailable for this turn"); return creatorSender; } function readKnownCreatorSender(value) { const creatorSender = value?.trim(); return creatorSender && creatorSender.toLowerCase() !== "unknown" ? creatorSender : null; } function encodeStandingIntentChannelScope(params) { const provider = normalizeScopeIdentity(params.provider, "channel identity", true); const identity = params.scope === "channel" ? provider : normalizeScopeIdentity(params.conversationId ?? "", "conversation identity"); return JSON.stringify([ "v1", params.scope, provider, normalizeScopeAccountId(params.accountId), identity ]); } function encodeStandingIntentSenderScope(params) { return JSON.stringify([ "v1", normalizeScopeIdentity(params.provider, "channel identity", true), normalizeScopeAccountId(params.accountId), normalizeScopeIdentity(params.senderId, "sender identity") ]); } function parseStoredChannelScope(value) { if (value === null) return { scope: "anywhere", identity: null }; try { const parsed = JSON.parse(value); if (Array.isArray(parsed) && parsed.length === 5 && parsed[0] === "v1" && (parsed[1] === "channel" || parsed[1] === "conversation") && parsed.slice(2).every((entry) => typeof entry === "string" && entry.length > 0)) return { scope: parsed[1], identity: parsed[4] }; } catch {} return { scope: "anywhere", identity: null }; } function parseStoredSenderScope(value) { if (value === null) return null; try { const parsed = JSON.parse(value); return Array.isArray(parsed) && parsed.length === 4 && parsed[0] === "v1" && parsed.slice(1).every((entry) => typeof entry === "string" && entry.length > 0) ? parsed[3] : null; } catch { return null; } } function rowToIntent(row) { const channelScope = parseStoredChannelScope(row.channel_scope); return { id: row.id, description: row.description, triggerKeywords: parseStoredTriggerKeywords(row.trigger_keywords), triggerEmbedding: row.trigger_embedding, scope: channelScope.scope, channelScope: channelScope.identity, senderScope: parseStoredSenderScope(row.sender_scope), creatorSender: readKnownCreatorSender(row.creator_sender), status: row.status, expiresAt: row.expires_at, maxFires: row.max_fires, fireCount: row.fire_count, cooldownSeconds: row.cooldown_seconds, lastFiredAt: row.last_fired_at, createdAt: row.created_at, sourceSessionId: row.source_session_id }; } function parseStoredTriggerKeywords(value) { try { const parsed = JSON.parse(value); return Array.isArray(parsed) ? parsed.filter((entry) => typeof entry === "string" && Boolean(entry)) : []; } catch { return []; } } function shouldRearm(row, nowMs) { if (row.status !== "fired" || row.last_fired_at === null) return false; return row.last_fired_at + row.cooldown_seconds * 1e3 <= nowMs; } function maintainStandingIntentLifecycle(db, nowMs) { const kysely = getNodeSqliteKysely(db); executeSqliteQuerySync(db, kysely.updateTable("standing_intents").set({ status: "expired" }).where("status", "in", [ "pending", "armed", "fired" ]).where("expires_at", "<=", nowMs)); const fired = executeSqliteQuerySync(db, kysely.selectFrom("standing_intents").selectAll().where("status", "=", "fired").where("expires_at", ">", nowMs).whereRef("fire_count", "<", "max_fires")).rows; for (const row of fired) { if (!shouldRearm(row, nowMs)) continue; executeSqliteQuerySync(db, kysely.updateTable("standing_intents").set({ status: "armed" }).where("id", "=", row.id).where("status", "=", "fired")); } } function createStandingIntent(params) { const nowMs = params.nowMs ?? Date.now(); const row = { id: randomUUID(), description: params.description, trigger_keywords: JSON.stringify(params.triggerKeywords), trigger_embedding: null, channel_scope: params.channelScope ?? null, sender_scope: params.senderScope ?? null, creator_sender: normalizeCreatorSender(params.creatorSender), status: "armed", expires_at: params.expiresAt ?? nowMs + 7776e6, max_fires: params.maxFires ?? 3, fire_count: 0, cooldown_seconds: params.cooldownSeconds ?? 86400, last_fired_at: null, created_at: nowMs, source_session_id: params.sourceSessionId ?? null }; withStandingIntentDatabase(params.agentId, (db) => { runSqliteImmediateTransactionSync(db, () => { const kysely = getNodeSqliteKysely(db); executeSqliteQuerySync(db, kysely.insertInto("standing_intents").values(row)); }); }); return rowToIntent(row); } function listStandingIntents(params) { return withStandingIntentDatabase(params.agentId, (db) => runSqliteImmediateTransactionSync(db, () => { maintainStandingIntentLifecycle(db, params.nowMs ?? Date.now()); let query = getNodeSqliteKysely(db).selectFrom("standing_intents").selectAll(); if (params.status) query = query.where("status", "=", params.status); return executeSqliteQuerySync(db, query.orderBy("created_at", "asc").orderBy("id", "asc")).rows.map(rowToIntent); })); } function sweepStandingIntents(params) { withStandingIntentDatabase(params.agentId, (db) => { runSqliteImmediateTransactionSync(db, () => { maintainStandingIntentLifecycle(db, params.nowMs ?? Date.now()); }); }); } function cancelStandingIntent(params) { return withStandingIntentDatabase(params.agentId, (db) => runSqliteImmediateTransactionSync(db, () => { const kysely = getNodeSqliteKysely(db); if (executeSqliteQuerySync(db, kysely.updateTable("standing_intents").set({ status: "cancelled" }).where("id", "=", params.id).where("status", "in", [ "pending", "armed", "fired" ])).numAffectedRows === 0n) return null; const row = executeSqliteQueryTakeFirstSync(db, kysely.selectFrom("standing_intents").selectAll().where("id", "=", params.id)); return row ? rowToIntent(row) : null; })); } function tokenizeIntentText(text) { return text.toLowerCase().match(/[\p{L}\p{N}_-]+/gu) ?? []; } function buildFtsQuery(promptTokens) { const unique = [...promptTokens]; if (unique.length === 0) return null; return unique.map((token) => `"${token.replaceAll("\"", "\"\"")}"`).join(" OR "); } function triggerMatchesPrompt(row, promptTokens) { return parseStoredTriggerKeywords(row.trigger_keywords).map((keyword) => tokenizeIntentText(keyword)).some((keywordTokens) => keywordTokens.length > 0 && keywordTokens.every((token) => promptTokens.has(token))); } function scopesMatch(row, channelScopes, senderScope) { return (row.channel_scope === null || channelScopes.has(row.channel_scope)) && (row.sender_scope === null || row.sender_scope === senderScope); } function canFire(row, nowMs) { return row.status === "armed" && row.expires_at > nowMs && row.fire_count < row.max_fires && (row.last_fired_at === null || row.last_fired_at + row.cooldown_seconds * 1e3 <= nowMs); } function matchStandingIntents(params) { const promptTokens = new Set(tokenizeIntentText(params.prompt)); const ftsQuery = buildFtsQuery(promptTokens); if (!ftsQuery) return []; const channel = params.channel?.trim() || void 0; const provider = params.provider?.trim().toLowerCase() || void 0; const senderId = params.senderId?.trim() || void 0; const channelScopes = /* @__PURE__ */ new Set(); if (provider) { channelScopes.add(encodeStandingIntentChannelScope({ scope: "channel", provider, accountId: params.accountId })); if (channel) channelScopes.add(encodeStandingIntentChannelScope({ scope: "conversation", provider, accountId: params.accountId, conversationId: channel })); } const storedSenderScope = provider && senderId ? encodeStandingIntentSenderScope({ provider, accountId: params.accountId, senderId }) : void 0; return withStandingIntentDatabase(params.agentId, (db) => runSqliteImmediateTransactionSync(db, () => { const nowMs = params.nowMs ?? Date.now(); maintainStandingIntentLifecycle(db, nowMs); let candidatesQuery = getNodeSqliteKysely(db).selectFrom("standing_intents as intent").innerJoin("standing_intents_fts as fts", "fts.rowid", "intent.intent_key").selectAll("intent").where("fts.trigger_keywords", "match", ftsQuery).where("intent.status", "=", "armed").where("intent.creator_sender", "is not", null).where("intent.expires_at", ">", nowMs).whereRef("intent.fire_count", "<", "intent.max_fires"); candidatesQuery = channelScopes.size > 0 ? candidatesQuery.where((expression) => expression.or([expression("intent.channel_scope", "is", null), ...[...channelScopes].map((scope) => expression("intent.channel_scope", "=", scope))])) : candidatesQuery.where("intent.channel_scope", "is", null); candidatesQuery = storedSenderScope ? candidatesQuery.where((expression) => expression.or([expression("intent.sender_scope", "is", null), expression("intent.sender_scope", "=", storedSenderScope)])) : candidatesQuery.where("intent.sender_scope", "is", null); const kysely = getNodeSqliteKysely(db); const fired = []; let scannedCandidates = 0; let cursor; while (fired.length < INTENT_INJECTION_MAX_COUNT && scannedCandidates < INTENT_MATCH_CANDIDATE_LIMIT) { let pageQuery = candidatesQuery; const currentCursor = cursor; if (currentCursor) pageQuery = pageQuery.where((expression) => expression.or([expression("intent.created_at", ">", currentCursor.createdAt), expression.and([expression("intent.created_at", "=", currentCursor.createdAt), expression("intent.id", ">", currentCursor.id)])])); const candidates = executeSqliteQuerySync(db, pageQuery.orderBy("intent.created_at", "asc").orderBy("intent.id", "asc").limit(Math.min(INTENT_MATCH_CANDIDATE_BATCH_SIZE, INTENT_MATCH_CANDIDATE_LIMIT - scannedCandidates))).rows; if (candidates.length === 0) break; const lastCandidate = candidates.at(-1); scannedCandidates += candidates.length; cursor = lastCandidate ? { createdAt: lastCandidate.created_at, id: lastCandidate.id } : cursor; for (const candidate of candidates) { if (fired.length >= INTENT_INJECTION_MAX_COUNT) break; const current = executeSqliteQueryTakeFirstSync(db, kysely.selectFrom("standing_intents").selectAll().where("id", "=", candidate.id)); if (!current || !readKnownCreatorSender(current.creator_sender) || !canFire(current, nowMs) || !scopesMatch(current, channelScopes, storedSenderScope) || !triggerMatchesPrompt(current, promptTokens)) continue; const nextFireCount = current.fire_count + 1; const firedIntent = rowToIntent({ ...current, fire_count: nextFireCount, last_fired_at: nowMs, status: nextFireCount >= current.max_fires ? "done" : "fired" }); if (!standingIntentsFitContext([...fired, firedIntent])) continue; executeSqliteQuerySync(db, kysely.updateTable("standing_intents").set({ fire_count: nextFireCount, last_fired_at: nowMs, status: nextFireCount >= current.max_fires ? "done" : "fired" }).where("id", "=", current.id).where("status", "=", "armed")); fired.push(firedIntent); } if (candidates.length < INTENT_MATCH_CANDIDATE_BATCH_SIZE) break; } return fired; })); } function renderStandingIntentContext(intents) { return `<standing_intents>\n${intents.map((intent) => { return `Standing intent (created ${new Date(intent.createdAt).toISOString().slice(0, 10)}): ${intent.description}`; }).join("\n")}\n</standing_intents>`; } function standingIntentsFitContext(intents) { return intents.length <= INTENT_INJECTION_MAX_COUNT && renderStandingIntentContext(intents).length <= 1200; } function buildStandingIntentContext(intents) { const included = []; for (const intent of intents.slice(0, INTENT_INJECTION_MAX_COUNT)) { if (!standingIntentsFitContext([...included, intent])) continue; included.push(intent); } return included.length > 0 ? renderStandingIntentContext(included) : void 0; } function isEligibleStandingIntentTurn(ctx) { if (ctx.trigger !== "user" || !ctx.sessionKey && !ctx.sessionId) return false; return ctx.messageProvider?.trim().toLowerCase() === "webchat" || Boolean(ctx.channelId?.trim() || ctx.chatId?.trim()); } //#endregion export { buildStandingIntentContext as a, encodeStandingIntentChannelScope as c, listStandingIntents as d, matchStandingIntents as f, INTENT_INJECTION_MAX_CHARS as i, encodeStandingIntentSenderScope as l, DEFAULT_INTENT_EXPIRY_MS as n, cancelStandingIntent as o, sweepStandingIntents as p, DEFAULT_INTENT_MAX_FIRES as r, createStandingIntent as s, DEFAULT_INTENT_COOLDOWN_SECONDS as t, isEligibleStandingIntentTurn as u };