UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

321 lines (320 loc) • 14 kB
import { t as pruneMapToMaxSize } from "./map-size-BlrH_CnE.mjs"; import { t as runInDetachedAsyncContext } from "./detached-async-context-D4YGqfxh.mjs"; import { c as isRecord } from "./record-coerce-TIsquoVR.mjs"; import { t as formatErrorMessage } from "./errors-BvOkmQM5.mjs"; import { t as createSubsystemLogger } from "./subsystem-4NzR43uz.mjs"; import { r as isIncognitoSessionKey } from "./session-key-CmZ4o9-l.mjs"; import "./session-key-DY3MOX4N.mjs"; import { w as runWithGatewayDetachedWorkContinuation } from "./gateway-work-admission-DAV5kkgV.mjs"; import { r as getRuntimeConfig } from "./io.runtime-B134MOMz.mjs"; import "./config-VnHbY_QV.mjs"; import { r as captureOwnedTranscriptWriteAssertion } from "./transcript-write-context-DwwjCPCE.mjs"; import { Q as consumeRunSkillUsage } from "./agent-tools.before-tool-call-CQkX3nOj.mjs"; import { l as isInternalSessionEffectsKey } from "./session-accessor.sqlite-participant-projection-BXRIuuEE.mjs"; import { r as readActiveTranscriptEntryAnchor } from "./session-accessor.sqlite-transcript-anchor-VJSh_9H1.mjs"; import { t as readActiveTranscriptEntryAnchorAsync } from "./session-transcript-anchor-read-D6gfv_oG.mjs"; import { t as canonicalizePath } from "./paths-C-gBEpJw.mjs"; import { t as resolveSkillWorkshopConfig } from "./config-BkIqt6SJ.mjs"; import { t as resolveWorkshopSkillsDir } from "./skills-root-BEm7oxJo.mjs"; import { r as completedTurnMessageAnchor } from "./session-manager-BpvVpTVC.mjs"; import { r as runAgentHarnessAgentEndHook, t as awaitAgentHarnessAgentEndHook } from "./lifecycle-hook-helpers-BOCYz9r1.mjs"; import path from "node:path"; import { AsyncLocalStorage } from "node:async_hooks"; //#region src/agents/skill-workshop-workspace-context.ts const canonicalSkillWorkspace = new AsyncLocalStorage(); function runWithCanonicalSkillWorkspace(canonicalWorkspaceDir, run) { return canonicalWorkspaceDir ? canonicalSkillWorkspace.run(canonicalWorkspaceDir, run) : run(); } function getCanonicalSkillWorkspace() { return canonicalSkillWorkspace.getStore(); } //#endregion //#region src/skills/workshop/experience-review-scheduler.ts /** Model iterations a session accumulates across turns before a review is due. */ const EXPERIENCE_REVIEW_ITERATION_THRESHOLD = 10; const EXPERIENCE_REVIEW_IDLE_MS = 3e4; const EXPERIENCE_REVIEW_RETRY_IDLE_MS = 3e4; const EXPERIENCE_REVIEW_MAX_PENDING = 32; const EXPERIENCE_REVIEW_MAX_COUNTERS = 1024; const EXPERIENCE_REVIEW_BLOCKED_TRIGGERS = /* @__PURE__ */ new Set([ "cron", "heartbeat", "memory", "overflow" ]); const EXPERIENCE_REVIEW_BLOCKED_SESSION_SEGMENTS = /* @__PURE__ */ new Set([ "acp", "cron", "hook", "subagent" ]); const log$2 = createSubsystemLogger("skills/workshop"); function isEligibleContext(ctx) { if (ctx.skillWorkshopAvailable !== true || !ctx.modelProviderId?.trim() || !ctx.modelId?.trim()) return false; const trigger = ctx.foregroundPromptContext.trigger?.trim().toLowerCase(); if (trigger && EXPERIENCE_REVIEW_BLOCKED_TRIGGERS.has(trigger)) return false; const sessionKey = ctx.sessionKey?.trim().toLowerCase(); if (!sessionKey || sessionKey.includes("active-memory") || isInternalSessionEffectsKey(sessionKey)) return false; return !sessionKey.split(":").some((segment) => EXPERIENCE_REVIEW_BLOCKED_SESSION_SEGMENTS.has(segment)); } /** * Provider-reported iterations win; otherwise count assistant messages after the last user * message. A zero report is no report: Codex counts only raw response events, which resumed * threads do not emit. */ function resolveTurnModelIterations(params) { const reported = params.ctx.modelIterations; if (reported !== void 0 && Number.isSafeInteger(reported) && reported > 0) return reported; const { messages } = params.event; let count = 0; for (let index = messages.length - 1; index >= 0; index -= 1) { const message = messages[index]; if (isRecord(message) && message.role === "user") break; if (isRecord(message) && message.role === "assistant") count += 1; } return count; } /** A learned skill the turn read or viewed; its outcome is fresh evidence for that skill. */ function usedWorkshopSkill(params) { if (!params.usedSkills?.length) return false; const root = canonicalizePath(resolveWorkshopSkillsDir(params.config, params.ctx.foregroundPromptContext.agentId)); return params.usedSkills.some((skill) => skill.skillFile?.startsWith(`${root}${path.sep}`)); } /** * Counts model iterations per (agent, session) across turns and queues one background review * once a session has done enough work since its last review or its last own Workshop edit, * or right after a turn that used a learned skill. Reviews wait for a quiet period and run * one at a time. */ function createSkillExperienceReviewScheduler(deps) { const iterationsBySession = /* @__PURE__ */ new Map(); const pendingBySession = /* @__PURE__ */ new Map(); let reviewInFlight = false; const setTimer = deps.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs)); const clearTimer = deps.clearTimer ?? clearTimeout; const arm = (key, pending, delayMs) => { if (pending.timer) clearTimer(pending.timer); const generation = ++pending.generation; const timerCallback = () => { if (pendingBySession.get(key) !== pending || pending.generation !== generation) return; pending.timer = void 0; (async () => { const active = await deps.isSystemActive(); if (pendingBySession.get(key) !== pending || pending.generation !== generation) return; if (active || reviewInFlight) { arm(key, pending, EXPERIENCE_REVIEW_RETRY_IDLE_MS); return; } reviewInFlight = true; try { pendingBySession.delete(key); await deps.runReview(pending.candidate); } finally { reviewInFlight = false; } })().catch((error) => { log$2.warn(`skill experience review failed: ${formatErrorMessage(error)}`); if (pendingBySession.get(key) === pending && pending.generation === generation) pendingBySession.delete(key); }); }; const timer = runInDetachedAsyncContext(() => setTimer(timerCallback, delayMs)); pending.timer = timer; timer.unref?.(); }; const cancel = (key) => { const pending = pendingBySession.get(key); if (pending?.timer) clearTimer(pending.timer); pendingBySession.delete(key); iterationsBySession.delete(key); }; return { schedule(params) { const sessionKey = params.ctx.sessionKey?.trim(); if (!sessionKey || isIncognitoSessionKey(sessionKey) || isIncognitoSessionKey(params.source?.sessionKey)) return; const key = JSON.stringify([params.ctx.foregroundPromptContext.agentId, sessionKey]); const existing = pendingBySession.get(key); if (existing) arm(key, existing, EXPERIENCE_REVIEW_IDLE_MS); if (typeof params.event.error === "string" && params.event.error.trim() !== "") { log$2.debug(`experience review skipped: reason=errored-completion session=${sessionKey}`); return; } if (resolveSkillWorkshopConfig(params.config).autonomous.mode !== "auto") { cancel(key); return; } if (!isEligibleContext(params.ctx)) { log$2.debug(`experience review skipped: reason=ineligible-context session=${sessionKey}`); return; } if (params.workshopMutated) { cancel(key); return; } const iterations = (iterationsBySession.get(key) ?? 0) + resolveTurnModelIterations(params); iterationsBySession.delete(key); if (iterations < EXPERIENCE_REVIEW_ITERATION_THRESHOLD && !usedWorkshopSkill(params)) { iterationsBySession.set(key, iterations); pruneMapToMaxSize(iterationsBySession, EXPERIENCE_REVIEW_MAX_COUNTERS); return; } const workspaceDir = getCanonicalSkillWorkspace() ?? params.ctx.workspaceDir?.trim(); const modelProviderId = params.ctx.modelProviderId?.trim(); const modelId = params.ctx.modelId?.trim(); const { source } = params; if (!workspaceDir || !source || !modelProviderId || !modelId) { log$2.debug(`experience review skipped: reason=missing-context session=${sessionKey}`); return; } if (!existing && pendingBySession.size >= EXPERIENCE_REVIEW_MAX_PENDING) { const oldest = pendingBySession.entries().next().value; if (oldest) { if (oldest[1].timer) clearTimer(oldest[1].timer); pendingBySession.delete(oldest[0]); } } const candidate = { ctx: { runId: params.ctx.runId, workspaceDir, modelProviderId, modelId, authProfileId: params.ctx.authProfileId, foregroundPromptContext: params.ctx.foregroundPromptContext }, config: params.config, source: { ...source }, usedSkills: params.usedSkills ? [...params.usedSkills] : void 0, turnAborted: !params.event.success }; const pending = existing ?? { candidate, generation: 0 }; pending.candidate = candidate; pendingBySession.set(key, pending); arm(key, pending, EXPERIENCE_REVIEW_IDLE_MS); log$2.debug(`experience review scheduled: session=${sessionKey} iterations=${iterations}`); }, clear() { for (const pending of pendingBySession.values()) if (pending.timer) clearTimer(pending.timer); pendingBySession.clear(); iterationsBySession.clear(); } }; } //#endregion //#region src/skills/workshop/experience-review-default.ts const log$1 = createSubsystemLogger("skills/workshop"); const UNUSED_ARCHIVE_INTERVAL_MS = 864e5; const lastUnusedArchiveByAgent = /* @__PURE__ */ new Map(); const defaultScheduler = createSkillExperienceReviewScheduler({ isSystemActive: async () => { const { getActiveEmbeddedRunCount } = await import("./active-run-projections-jslQIQW-.mjs"); return getActiveEmbeddedRunCount() > 0; }, runReview: async (candidate) => { const { getRuntimeConfig } = await import("./config/config.js"); const { prepareSkillExperienceReviewCandidate, runSkillExperienceReview } = await import("./experience-review-CkP_smfc.mjs"); const prepared = await prepareSkillExperienceReviewCandidate(candidate, getRuntimeConfig()); if (prepared) await runSkillExperienceReview(prepared); } }); /** Counts the finished turn toward a background review; never affects the turn's result. */ function scheduleSkillExperienceReview(params) { const runId = params.ctx.runId?.trim(); if (resolveSkillWorkshopConfig(params.config).autonomous.mode !== "auto" || !runId) { defaultScheduler.schedule(params); return; } import("./library-CkeiDZ9F.mjs").then(({ listWorkshopChanges }) => listWorkshopChanges(params.ctx.foregroundPromptContext.agentId, { runId, limit: 1 })).then((changes) => defaultScheduler.schedule({ ...params, workshopMutated: changes.length > 0 })).catch((error) => { log$1.warn(`skill experience review scheduling failed: ${formatErrorMessage(error)}`); }); } /** * Archives an agent's long-unused learned skills, at most once a day per agent. Runs detached * from the finished turn and never affects its result; no scheduler owns this work. */ function scheduleUnusedWorkshopSkillArchive(config, agentId) { if (resolveSkillWorkshopConfig(config).autonomous.mode !== "auto") return; const nowMs = Date.now(); const lastMs = lastUnusedArchiveByAgent.get(agentId); if (lastMs !== void 0 && nowMs - lastMs < UNUSED_ARCHIVE_INTERVAL_MS) return; lastUnusedArchiveByAgent.set(agentId, nowMs); runWithGatewayDetachedWorkContinuation(async () => { const { archiveUnusedWorkshopSkills } = await import("./unused-archive-CJ11eJ7S.mjs"); const archived = await archiveUnusedWorkshopSkills(config, agentId, nowMs); if (archived.length > 0) log$1.info(`archived ${archived.length} unused learned skill(s) for ${agentId}: ${archived.map((change) => change.skillName).join(", ")}`); }, "skills:workshop-unused-archive").catch((error) => { log$1.warn(`unused skill archive failed: ${formatErrorMessage(error)}`); }); } //#endregion //#region src/agents/harness/agent-end-side-effects.ts const log = createSubsystemLogger("agents/harness"); function runCoreAgentEndSideEffects(params, read) { const usedSkills = consumeRunSkillUsage(params.ctx.runId); const foregroundPromptContext = params.ctx.foregroundPromptContext; if (!foregroundPromptContext) return; const config = params.ctx.config ?? getRuntimeConfig(); const schedule = (anchor) => { if (!anchor) return; const ctx = { ...params.ctx, foregroundPromptContext }; scheduleSkillExperienceReview({ event: params.event, ctx, usedSkills, config, source: anchor }); }; const failed = (error) => { log.warn(`skill experience review scheduling failed: ${String(error)}`); }; try { scheduleUnusedWorkshopSkillArchive(config, foregroundPromptContext.agentId); const source = params.skillExperienceReviewSource; if (!source) return; if (read === "worker") { const committed = params[completedTurnMessageAnchor]; const assertCurrent = committed?.assertCurrent ?? captureOwnedTranscriptWriteAssertion(source); assertCurrent(); if (committed) { schedule(committed.anchor); return; } return readActiveTranscriptEntryAnchorAsync(source).then((anchor) => { assertCurrent(); schedule(anchor); }).catch(failed); } schedule(readActiveTranscriptEntryAnchor(source)); } catch (error) { failed(error); } } /** @deprecated Use runAgentEndSideEffectsAsync; retained until the next Plugin SDK major. */ function runAgentEndSideEffects(params) { runCoreAgentEndSideEffects(params, "native"); runAgentHarnessAgentEndHook(params); } /** Keep the turn lease through anchor preparation, then start plugin hooks without waiting. */ async function runAgentEndSideEffectsAsync(params) { await runCoreAgentEndSideEffects(params, "worker"); runAgentHarnessAgentEndHook(params); } /** Runs agent-end side effects and waits for plugin/core completion. */ async function awaitAgentEndSideEffects(params) { await runCoreAgentEndSideEffects(params, "worker"); await awaitAgentHarnessAgentEndHook(params); } //#endregion export { runWithCanonicalSkillWorkspace as i, runAgentEndSideEffects as n, runAgentEndSideEffectsAsync as r, awaitAgentEndSideEffects as t };