import fs from "node:fs"; import path from "node:path"; export type BootstrapInput = { agentId: string; openclawSessionKey: string; workspace: string; /** Skills XML block extracted from the OpenClaw system prompt, if any */ skillsBlock?: string; /** Subset of OpenClaw context files present in the workspace (for persona/identity) */ workspaceContextFiles?: string[]; }; /** Read a workspace file. Returns the content, or null if missing/unreadable. */ function readWorkspaceFile(workspace: string, filename: string): string | null { try { const content = fs.readFileSync(path.join(workspace, filename), "utf8").trim(); return content || null; } catch { return null; } } /** * Build the one-time bootstrap message injected at the start of a new session. * Workspace context files (SOUL.md, IDENTITY.md, MEMORY.md, USER.md) are read * here and embedded inline — the agent must not need to fetch them separately. * Must NOT be re-injected on every turn. */ export function buildBootstrap(input: BootstrapInput): string { const contextFiles = input.workspaceContextFiles ?? []; const lines = [ `You are operating as a contractor agent inside OpenClaw.`, ``, `## Context`, `- Agent ID: ${input.agentId}`, `- Session key: ${input.openclawSessionKey}`, `- Workspace: ${input.workspace}`, ``, `## Role`, `You receive tasks from OpenClaw users and complete them using your tools.`, `You do not need to manage your own session context — OpenClaw handles session routing.`, `Your responses go directly back to the user through OpenClaw.`, ``, `## Guidelines`, `- Work in the specified workspace directory.`, `- Be concise and action-oriented. Use tools to accomplish tasks rather than describing what you would do.`, `- Each message you receive contains the latest user request. Previous context is in your session memory.`, `- If a task is unclear, ask one focused clarifying question.`, ]; // ── Persona (SOUL.md) ───────────────────────────────────────────────────── // Injected directly so the agent embodies the persona immediately without // needing to read files first. if (contextFiles.includes("SOUL.md")) { const soul = readWorkspaceFile(input.workspace, "SOUL.md"); if (soul) { lines.push(``, `## Soul`, soul); } } // ── Identity (IDENTITY.md) ──────────────────────────────────────────────── if (contextFiles.includes("IDENTITY.md")) { const identity = readWorkspaceFile(input.workspace, "IDENTITY.md"); if (identity) { lines.push(``, `## Identity`, identity); // If the file still looks like the default template, encourage the agent // to fill it in. if (identity.includes("Fill this in during your first conversation")) { lines.push( ``, `_IDENTITY.md is still a template. Pick a name, creature type, and vibe for yourself`, `and update the file at ${input.workspace}/IDENTITY.md._`, ); } } } // ── User profile (USER.md) ──────────────────────────────────────────────── if (contextFiles.includes("USER.md")) { const user = readWorkspaceFile(input.workspace, "USER.md"); if (user) { lines.push(``, `## User Profile`, user); } } // ── Memory index (MEMORY.md) ────────────────────────────────────────────── if (contextFiles.includes("MEMORY.md") || contextFiles.some(f => f.startsWith("memory/"))) { const memoryIndex = readWorkspaceFile(input.workspace, "MEMORY.md"); lines.push(``, `## Memory`); if (memoryIndex) { lines.push(memoryIndex); } lines.push( ``, `Memory files live in ${input.workspace}/memory/. Use the Read tool to fetch individual`, `memories and write new ones to ${input.workspace}/memory/.md.`, ); } // ── Skills ──────────────────────────────────────────────────────────────── if (input.skillsBlock) { lines.push( ``, `## Skills`, `The following skills are available. When a task matches a skill's description:`, `1. Read the skill's SKILL.md using the Read tool (the field is the absolute path).`, `2. Follow the instructions in SKILL.md. Replace \`{baseDir}\` with the directory containing SKILL.md.`, `3. Run scripts using the Bash tool, NOT the \`exec\` tool (you have Bash, not exec).`, ``, input.skillsBlock, ); } return lines.join("\n"); }