UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

214 lines (213 loc) 9.68 kB
import { c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js"; import { p as resolveUserPath } from "./utils-CCC-BEJH.js"; import { y as resolveSessionAgentIds } from "./agent-scope-MrLta7Pq.js"; import { f as resolveAgentIdFromSessionKey } from "./session-key-B_NoIfpX.js"; import { r as resolveAgentConfig } from "./agent-scope-config-CgCYpZfK.js"; import { m as triggerInternalHook, n as createInternalHookEvent } from "./internal-hooks-Bq6_9FFu.js"; import { f as filterBootstrapFilesForSession, g as isWorkspaceSetupCompleted, n as DEFAULT_BOOTSTRAP_FILENAME, r as DEFAULT_HEARTBEAT_FILENAME, y as loadWorkspaceBootstrapFiles } from "./workspace-B0bmoo98.js"; import { b as resolveBootstrapTotalMaxChars, g as buildBootstrapContextFiles, v as resolveBootstrapMaxChars } from "./embedded-agent-helpers-7sfpu1OT.js"; import { r as getOrLoadBootstrapFiles } from "./bootstrap-cache-CGyy0R7Z.js"; import { n as shouldIncludeHeartbeatGuidanceForSystemPrompt } from "./heartbeat-system-prompt-CKczFcLs.js"; import path from "node:path"; import fs from "node:fs/promises"; //#region src/agents/bootstrap-hooks.ts /** Runs bootstrap hooks and returns the effective bootstrap file list. */ async function applyBootstrapHookOverrides(params) { const sessionKey = params.sessionKey ?? params.sessionId ?? "unknown"; const agentId = params.agentId ?? (params.sessionKey ? resolveAgentIdFromSessionKey(params.sessionKey) : void 0); const event = createInternalHookEvent("agent", "bootstrap", sessionKey, { workspaceDir: params.workspaceDir, bootstrapFiles: params.files, cfg: params.config, sessionKey: params.sessionKey, sessionId: params.sessionId, agentId }); await triggerInternalHook(event); const updated = event.context.bootstrapFiles; return Array.isArray(updated) ? updated : params.files; } //#endregion //#region src/agents/bootstrap-files.ts /** * Resolves workspace bootstrap files for agent runs and converts them into * bounded context files. */ const CONTINUATION_SCAN_MAX_TAIL_BYTES = 256 * 1024; const FULL_BOOTSTRAP_COMPLETED_CUSTOM_TYPE = "openclaw:bootstrap-context:full"; const BOOTSTRAP_WARNING_DEDUPE_LIMIT = 1024; const seenBootstrapWarnings = /* @__PURE__ */ new Set(); const bootstrapWarningOrder = []; function rememberBootstrapWarning(key) { if (seenBootstrapWarnings.has(key)) return false; if (seenBootstrapWarnings.size >= BOOTSTRAP_WARNING_DEDUPE_LIMIT) { const oldest = bootstrapWarningOrder.shift(); if (oldest) seenBootstrapWarnings.delete(oldest); } seenBootstrapWarnings.add(key); bootstrapWarningOrder.push(key); return true; } /** Resolves the effective bootstrap injection mode for a session agent. */ function resolveContextInjectionMode(config, agentId) { const agentMode = config && agentId ? resolveAgentConfig(config, agentId)?.contextInjection : void 0; if (agentMode === "always" || agentMode === "continuation-skip" || agentMode === "never") return agentMode; return config?.agents?.defaults?.contextInjection ?? "always"; } /** Checks whether the session transcript still has a valid full-bootstrap marker. */ async function hasCompletedBootstrapTurn(sessionFile) { try { const stat = await fs.lstat(sessionFile); if (stat.isSymbolicLink()) return false; const fh = await fs.open(sessionFile, "r"); try { const bytesToRead = Math.min(stat.size, CONTINUATION_SCAN_MAX_TAIL_BYTES); if (bytesToRead <= 0) return false; const start = stat.size - bytesToRead; const buffer = Buffer.allocUnsafe(bytesToRead); const { bytesRead } = await fh.read(buffer, 0, bytesToRead, start); let text = buffer.toString("utf-8", 0, bytesRead); if (start > 0) { const firstNewline = text.indexOf("\n"); if (firstNewline === -1) return false; text = text.slice(firstNewline + 1); } const records = text.split(/\r?\n/u).filter((line) => line.trim().length > 0).slice(-500); let compactedAfterLatestAssistant = false; for (let i = records.length - 1; i >= 0; i--) { const line = records[i]; if (!line) continue; let entry; try { entry = JSON.parse(line); } catch { continue; } const record = entry; if (record?.type === "compaction") { compactedAfterLatestAssistant = true; continue; } if (record?.type === "custom" && record.customType === "openclaw:bootstrap-context:full") return !compactedAfterLatestAssistant; } return false; } finally { await fh.close(); } } catch { return false; } } /** Builds a session-scoped warning sink that dedupes repeated bootstrap warnings. */ function makeBootstrapWarn(params) { const warn = params.warn; if (!warn) return; const workspacePrefix = params.workspaceDir ?? ""; return (message) => { if (!rememberBootstrapWarning(`${workspacePrefix}\u0000${params.sessionLabel}\u0000${message}`)) return; warn(`${message} (sessionKey=${params.sessionLabel})`); }; } function sanitizeBootstrapFiles(files, workspaceDir, warn) { const workspaceRoot = resolveUserPath(workspaceDir); const seenPaths = /* @__PURE__ */ new Set(); const sanitized = []; for (const file of files) { const pathValue = normalizeOptionalString(file.path) ?? ""; if (!pathValue) { warn?.(`skipping bootstrap file "${file.name}" — missing or invalid "path" field (hook may have used "filePath" instead)`); continue; } const resolvedPath = path.isAbsolute(pathValue) ? path.resolve(pathValue) : pathValue.startsWith("~") ? resolveUserPath(pathValue) : path.resolve(workspaceRoot, pathValue); const dedupeKey = path.normalize(path.relative(workspaceRoot, resolvedPath)); if (seenPaths.has(dedupeKey)) continue; seenPaths.add(dedupeKey); sanitized.push({ ...file, path: resolvedPath }); } return sanitized; } function applyContextModeFilter(params) { const contextMode = params.contextMode ?? "full"; const runKind = params.runKind ?? "default"; if (contextMode !== "lightweight") return params.files; if (runKind === "heartbeat") return params.files.filter((file) => file.name === "HEARTBEAT.md"); return []; } function shouldExcludeHeartbeatBootstrapFile(params) { if (!params.config || params.runKind === "heartbeat") return false; const { defaultAgentId, sessionAgentId } = resolveSessionAgentIds({ sessionKey: params.sessionKey ?? params.sessionId, config: params.config, agentId: params.agentId }); if (sessionAgentId !== defaultAgentId) return false; return !shouldIncludeHeartbeatGuidanceForSystemPrompt({ config: params.config, agentId: sessionAgentId, defaultAgentId }); } function filterHeartbeatBootstrapFile(files, excludeHeartbeatBootstrapFile) { if (!excludeHeartbeatBootstrapFile) return files; return files.filter((file) => file.name !== DEFAULT_HEARTBEAT_FILENAME); } function filterCompletedWorkspaceBootstrapFile(files, setupCompleted, workspaceDir) { if (!setupCompleted) return files; const workspaceRoot = resolveUserPath(workspaceDir); const rootBootstrapPath = path.join(workspaceRoot, DEFAULT_BOOTSTRAP_FILENAME); return files.filter((file) => { if (file.name !== "BOOTSTRAP.md") return true; const pathValue = normalizeOptionalString(file.path); if (!pathValue) return true; return (path.isAbsolute(pathValue) ? path.resolve(pathValue) : pathValue.startsWith("~") ? resolveUserPath(pathValue) : path.resolve(workspaceRoot, pathValue)) !== rootBootstrapPath; }); } async function isWorkspaceSetupCompletedForContext(workspaceDir) { try { return await isWorkspaceSetupCompleted(workspaceDir); } catch { return false; } } /** Resolves hook-adjusted, session-filtered bootstrap files for a run. */ async function resolveBootstrapFilesForRun(params) { const excludeHeartbeatBootstrapFile = shouldExcludeHeartbeatBootstrapFile(params); const sessionKey = params.sessionKey ?? params.sessionId; const workspaceSetupCompleted = await isWorkspaceSetupCompletedForContext(params.workspaceDir); return sanitizeBootstrapFiles(filterHeartbeatBootstrapFile(filterCompletedWorkspaceBootstrapFile(await applyBootstrapHookOverrides({ files: applyContextModeFilter({ files: filterCompletedWorkspaceBootstrapFile(filterBootstrapFilesForSession(params.sessionKey ? await getOrLoadBootstrapFiles({ workspaceDir: params.workspaceDir, sessionKey: params.sessionKey }) : await loadWorkspaceBootstrapFiles(params.workspaceDir), sessionKey), workspaceSetupCompleted, params.workspaceDir), contextMode: params.contextMode, runKind: params.runKind }), workspaceDir: params.workspaceDir, config: params.config, sessionKey: params.sessionKey, sessionId: params.sessionId, agentId: params.agentId }), workspaceSetupCompleted, params.workspaceDir), excludeHeartbeatBootstrapFile), params.workspaceDir, params.warn); } /** Resolves both raw bootstrap metadata and bounded context files for a run. */ async function resolveBootstrapContextForRun(params) { const bootstrapFiles = await resolveBootstrapFilesForRun(params); return { bootstrapFiles, contextFiles: buildBootstrapContextForFiles(bootstrapFiles, params) }; } /** Builds bounded context files from already-resolved bootstrap file metadata. */ function buildBootstrapContextForFiles(bootstrapFiles, params) { return buildBootstrapContextFiles(bootstrapFiles, { maxChars: resolveBootstrapMaxChars(params.config, params.agentId), totalMaxChars: resolveBootstrapTotalMaxChars(params.config, params.agentId), warn: params.warn }); } //#endregion export { resolveBootstrapContextForRun as a, makeBootstrapWarn as i, buildBootstrapContextForFiles as n, resolveBootstrapFilesForRun as o, hasCompletedBootstrapTurn as r, resolveContextInjectionMode as s, FULL_BOOTSTRAP_COMPLETED_CUSTOM_TYPE as t };