feat: enrich system prompt docs guidance

This commit is contained in:
Peter Steinberger
2026-01-18 15:00:14 +00:00
parent f3698e360b
commit e9a08dc507
10 changed files with 119 additions and 5 deletions

View File

@@ -10,15 +10,19 @@ Docs: https://docs.clawd.bot
- Swabble: use the tagged Commander Swift package release. - Swabble: use the tagged Commander Swift package release.
- CLI: add `clawdbot acp client` interactive ACP harness for debugging. - CLI: add `clawdbot acp client` interactive ACP harness for debugging.
- Plugins: route command detection/text chunking helpers through the plugin runtime and drop runtime exports from the SDK. - Plugins: route command detection/text chunking helpers through the plugin runtime and drop runtime exports from the SDK.
- Memory: add native Gemini embeddings provider for memory search. (#1151) — thanks @gumadeiras. - Memory: add native Gemini embeddings provider for memory search. (#1151)
- Media: auto-enable audio understanding when provider keys are configured (OpenAI/Groq/Deepgram). - Agents: add local docs path resolution and include docs/mirror/source/community pointers in the system prompt.
- Docs: add API usage + costs overview. https://docs.clawd.bot/reference/api-usage-costs
### Fixes ### Fixes
- Auth profiles: keep auto-pinned preference while allowing rotation on failover; user pins stay locked. (#1138) — thanks @cheeeee. - Auth profiles: keep auto-pinned preference while allowing rotation on failover; user pins stay locked. (#1138) — thanks @cheeeee.
- macOS: avoid touching launchd in Remote over SSH so quitting the app no longer disables the remote gateway. (#1105) - macOS: avoid touching launchd in Remote over SSH so quitting the app no longer disables the remote gateway. (#1105)
- Memory: index atomically so failed reindex preserves the previous memory database. (#1151) — thanks @gumadeiras. - Memory: index atomically so failed reindex preserves the previous memory database. (#1151)
- Memory: avoid sqlite-vec unique constraint failures when reindexing duplicate chunk ids. (#1151) — thanks @gumadeiras. - Memory: avoid sqlite-vec unique constraint failures when reindexing duplicate chunk ids. (#1151)
## 2026.1.18-5
### Changes
- Dependencies: update core + plugin deps (grammy, vitest, openai, Microsoft agents hosting, etc.).
## 2026.1.18-3 ## 2026.1.18-3

View File

@@ -18,6 +18,7 @@ The prompt is intentionally compact and uses fixed sections:
- **Skills** (when available): tells the model how to load skill instructions on demand. - **Skills** (when available): tells the model how to load skill instructions on demand.
- **Clawdbot Self-Update**: how to run `config.apply` and `update.run`. - **Clawdbot Self-Update**: how to run `config.apply` and `update.run`.
- **Workspace**: working directory (`agents.defaults.workspace`). - **Workspace**: working directory (`agents.defaults.workspace`).
- **Documentation**: local path to Clawdbot docs (repo or npm package) and when to read them.
- **Workspace Files (injected)**: indicates bootstrap files are included below. - **Workspace Files (injected)**: indicates bootstrap files are included below.
- **Sandbox** (when enabled): indicates sandboxed runtime, sandbox paths, and whether elevated exec is available. - **Sandbox** (when enabled): indicates sandboxed runtime, sandbox paths, and whether elevated exec is available.
- **Current Date & Time**: user-local time, timezone, and time format. - **Current Date & Time**: user-local time, timezone, and time format.
@@ -98,3 +99,12 @@ Skills section is omitted.
``` ```
This keeps the base prompt small while still enabling targeted skill usage. This keeps the base prompt small while still enabling targeted skill usage.
## Documentation
When available, the system prompt includes a **Documentation** section that points to the
local Clawdbot docs directory (either `docs/` in the repo workspace or the bundled npm
package docs) and also notes the public mirror, source repo, community Discord, and
ClawdHub (https://clawdhub.com) for skills discovery. The prompt instructs the model to consult local docs first
for Clawdbot behavior, commands, configuration, or architecture, and to run
`clawdbot status` itself when possible (asking the user only when it lacks access).

View File

@@ -6,6 +6,7 @@ import { shouldLogVerbose } from "../globals.js";
import { createSubsystemLogger } from "../logging.js"; import { createSubsystemLogger } from "../logging.js";
import { runCommandWithTimeout } from "../process/exec.js"; import { runCommandWithTimeout } from "../process/exec.js";
import { resolveUserPath } from "../utils.js"; import { resolveUserPath } from "../utils.js";
import { resolveClawdbotDocsPath } from "./docs-path.js";
import { resolveSessionAgentIds } from "./agent-scope.js"; import { resolveSessionAgentIds } from "./agent-scope.js";
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "./bootstrap-files.js"; import { makeBootstrapWarn, resolveBootstrapContextForRun } from "./bootstrap-files.js";
import { resolveCliBackendConfig } from "./cli-backends.js"; import { resolveCliBackendConfig } from "./cli-backends.js";
@@ -83,6 +84,12 @@ export async function runCliAgent(params: {
sessionAgentId === defaultAgentId sessionAgentId === defaultAgentId
? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt) ? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt)
: undefined; : undefined;
const docsPath = await resolveClawdbotDocsPath({
workspaceDir,
argv1: process.argv[1],
cwd: process.cwd(),
moduleUrl: import.meta.url,
});
const systemPrompt = buildSystemPrompt({ const systemPrompt = buildSystemPrompt({
workspaceDir, workspaceDir,
config: params.config, config: params.config,
@@ -90,6 +97,7 @@ export async function runCliAgent(params: {
extraSystemPrompt, extraSystemPrompt,
ownerNumbers: params.ownerNumbers, ownerNumbers: params.ownerNumbers,
heartbeatPrompt, heartbeatPrompt,
docsPath,
tools: [], tools: [],
contextFiles, contextFiles,
modelDisplay, modelDisplay,

View File

@@ -168,6 +168,7 @@ export function buildSystemPrompt(params: {
extraSystemPrompt?: string; extraSystemPrompt?: string;
ownerNumbers?: string[]; ownerNumbers?: string[];
heartbeatPrompt?: string; heartbeatPrompt?: string;
docsPath?: string;
tools: AgentTool[]; tools: AgentTool[];
contextFiles?: EmbeddedContextFile[]; contextFiles?: EmbeddedContextFile[];
modelDisplay: string; modelDisplay: string;
@@ -182,6 +183,7 @@ export function buildSystemPrompt(params: {
ownerNumbers: params.ownerNumbers, ownerNumbers: params.ownerNumbers,
reasoningTagHint: false, reasoningTagHint: false,
heartbeatPrompt: params.heartbeatPrompt, heartbeatPrompt: params.heartbeatPrompt,
docsPath: params.docsPath,
runtimeInfo: { runtimeInfo: {
host: "clawdbot", host: "clawdbot",
os: `${os.type()} ${os.release()}`, os: `${os.type()} ${os.release()}`,

27
src/agents/docs-path.ts Normal file
View File

@@ -0,0 +1,27 @@
import fs from "node:fs";
import path from "node:path";
import { resolveClawdbotPackageRoot } from "../infra/clawdbot-root.js";
export async function resolveClawdbotDocsPath(params: {
workspaceDir?: string;
argv1?: string;
cwd?: string;
moduleUrl?: string;
}): Promise<string | null> {
const workspaceDir = params.workspaceDir?.trim();
if (workspaceDir) {
const workspaceDocs = path.join(workspaceDir, "docs");
if (fs.existsSync(workspaceDocs)) return workspaceDocs;
}
const packageRoot = await resolveClawdbotPackageRoot({
cwd: params.cwd,
argv1: params.argv1,
moduleUrl: params.moduleUrl,
});
if (!packageRoot) return null;
const packageDocs = path.join(packageRoot, "docs");
return fs.existsSync(packageDocs) ? packageDocs : null;
}

View File

@@ -17,6 +17,7 @@ import { resolveUserPath } from "../../utils.js";
import { resolveClawdbotAgentDir } from "../agent-paths.js"; import { resolveClawdbotAgentDir } from "../agent-paths.js";
import { resolveSessionAgentIds } from "../agent-scope.js"; import { resolveSessionAgentIds } from "../agent-scope.js";
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../bootstrap-files.js"; import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../bootstrap-files.js";
import { resolveClawdbotDocsPath } from "../docs-path.js";
import type { ExecElevatedDefaults } from "../bash-tools.js"; import type { ExecElevatedDefaults } from "../bash-tools.js";
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js"; import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js";
import { getApiKeyForModel, resolveModelAuthMode } from "../model-auth.js"; import { getApiKeyForModel, resolveModelAuthMode } from "../model-auth.js";
@@ -250,6 +251,12 @@ export async function compactEmbeddedPiSession(params: {
}); });
const isDefaultAgent = sessionAgentId === defaultAgentId; const isDefaultAgent = sessionAgentId === defaultAgentId;
const promptMode = isSubagentSessionKey(params.sessionKey) ? "minimal" : "full"; const promptMode = isSubagentSessionKey(params.sessionKey) ? "minimal" : "full";
const docsPath = await resolveClawdbotDocsPath({
workspaceDir: effectiveWorkspace,
argv1: process.argv[1],
cwd: process.cwd(),
moduleUrl: import.meta.url,
});
const appendPrompt = buildEmbeddedSystemPrompt({ const appendPrompt = buildEmbeddedSystemPrompt({
workspaceDir: effectiveWorkspace, workspaceDir: effectiveWorkspace,
defaultThinkLevel: params.thinkLevel, defaultThinkLevel: params.thinkLevel,
@@ -261,6 +268,7 @@ export async function compactEmbeddedPiSession(params: {
? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt) ? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt)
: undefined, : undefined,
skillsPrompt, skillsPrompt,
docsPath,
promptMode, promptMode,
runtimeInfo, runtimeInfo,
sandboxInfo, sandboxInfo,

View File

@@ -18,6 +18,7 @@ import { resolveUserPath } from "../../../utils.js";
import { resolveClawdbotAgentDir } from "../../agent-paths.js"; import { resolveClawdbotAgentDir } from "../../agent-paths.js";
import { resolveSessionAgentIds } from "../../agent-scope.js"; import { resolveSessionAgentIds } from "../../agent-scope.js";
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../../bootstrap-files.js"; import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../../bootstrap-files.js";
import { resolveClawdbotDocsPath } from "../../docs-path.js";
import { resolveModelAuthMode } from "../../model-auth.js"; import { resolveModelAuthMode } from "../../model-auth.js";
import { import {
isCloudCodeAssistFormatError, isCloudCodeAssistFormatError,
@@ -216,6 +217,12 @@ export async function runEmbeddedAttempt(
}); });
const isDefaultAgent = sessionAgentId === defaultAgentId; const isDefaultAgent = sessionAgentId === defaultAgentId;
const promptMode = isSubagentSessionKey(params.sessionKey) ? "minimal" : "full"; const promptMode = isSubagentSessionKey(params.sessionKey) ? "minimal" : "full";
const docsPath = await resolveClawdbotDocsPath({
workspaceDir: effectiveWorkspace,
argv1: process.argv[1],
cwd: process.cwd(),
moduleUrl: import.meta.url,
});
const appendPrompt = buildEmbeddedSystemPrompt({ const appendPrompt = buildEmbeddedSystemPrompt({
workspaceDir: effectiveWorkspace, workspaceDir: effectiveWorkspace,
@@ -228,6 +235,7 @@ export async function runEmbeddedAttempt(
? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt) ? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt)
: undefined, : undefined,
skillsPrompt, skillsPrompt,
docsPath,
reactionGuidance, reactionGuidance,
promptMode, promptMode,
runtimeInfo, runtimeInfo,

View File

@@ -15,6 +15,7 @@ export function buildEmbeddedSystemPrompt(params: {
reasoningTagHint: boolean; reasoningTagHint: boolean;
heartbeatPrompt?: string; heartbeatPrompt?: string;
skillsPrompt?: string; skillsPrompt?: string;
docsPath?: string;
reactionGuidance?: { reactionGuidance?: {
level: "minimal" | "extensive"; level: "minimal" | "extensive";
channel: string; channel: string;
@@ -48,6 +49,7 @@ export function buildEmbeddedSystemPrompt(params: {
reasoningTagHint: params.reasoningTagHint, reasoningTagHint: params.reasoningTagHint,
heartbeatPrompt: params.heartbeatPrompt, heartbeatPrompt: params.heartbeatPrompt,
skillsPrompt: params.skillsPrompt, skillsPrompt: params.skillsPrompt,
docsPath: params.docsPath,
reactionGuidance: params.reactionGuidance, reactionGuidance: params.reactionGuidance,
promptMode: params.promptMode, promptMode: params.promptMode,
runtimeInfo: params.runtimeInfo, runtimeInfo: params.runtimeInfo,

View File

@@ -32,12 +32,14 @@ describe("buildAgentSystemPrompt", () => {
"<available_skills>\n <skill>\n <name>demo</name>\n </skill>\n</available_skills>", "<available_skills>\n <skill>\n <name>demo</name>\n </skill>\n</available_skills>",
heartbeatPrompt: "ping", heartbeatPrompt: "ping",
toolNames: ["message", "memory_search"], toolNames: ["message", "memory_search"],
docsPath: "/tmp/clawd/docs",
extraSystemPrompt: "Subagent details", extraSystemPrompt: "Subagent details",
}); });
expect(prompt).not.toContain("## User Identity"); expect(prompt).not.toContain("## User Identity");
expect(prompt).not.toContain("## Skills"); expect(prompt).not.toContain("## Skills");
expect(prompt).not.toContain("## Memory Recall"); expect(prompt).not.toContain("## Memory Recall");
expect(prompt).not.toContain("## Documentation");
expect(prompt).not.toContain("## Reply Tags"); expect(prompt).not.toContain("## Reply Tags");
expect(prompt).not.toContain("## Messaging"); expect(prompt).not.toContain("## Messaging");
expect(prompt).not.toContain("## Silent Replies"); expect(prompt).not.toContain("## Silent Replies");
@@ -86,6 +88,7 @@ describe("buildAgentSystemPrompt", () => {
toolNames: ["Read", "Exec", "process"], toolNames: ["Read", "Exec", "process"],
skillsPrompt: skillsPrompt:
"<available_skills>\n <skill>\n <name>demo</name>\n </skill>\n</available_skills>", "<available_skills>\n <skill>\n <name>demo</name>\n </skill>\n</available_skills>",
docsPath: "/tmp/clawd/docs",
}); });
expect(prompt).toContain("- Read: Read file contents"); expect(prompt).toContain("- Read: Read file contents");
@@ -93,6 +96,21 @@ describe("buildAgentSystemPrompt", () => {
expect(prompt).toContain( expect(prompt).toContain(
"Use `Read` to load the SKILL.md at the location listed for that skill.", "Use `Read` to load the SKILL.md at the location listed for that skill.",
); );
expect(prompt).toContain("Clawdbot docs: /tmp/clawd/docs");
expect(prompt).toContain("read the docs first using `Read`");
});
it("includes docs guidance when docsPath is provided", () => {
const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/clawd",
docsPath: "/tmp/clawd/docs",
});
expect(prompt).toContain("## Documentation");
expect(prompt).toContain("Clawdbot docs: /tmp/clawd/docs");
expect(prompt).toContain(
"When a user asks about Clawdbot behavior, commands, config, or architecture",
);
}); });
it("includes user time when provided (12-hour)", () => { it("includes user time when provided (12-hour)", () => {

View File

@@ -109,6 +109,26 @@ function buildMessagingSection(params: {
]; ];
} }
function buildDocsSection(params: {
docsPath?: string;
isMinimal: boolean;
readToolName: string;
}) {
const docsPath = params.docsPath?.trim();
if (!docsPath || params.isMinimal) return [];
return [
"## Documentation",
`Clawdbot docs: ${docsPath}`,
"Mirror: https://docs.clawd.bot",
"Source: https://github.com/clawdbot/clawdbot",
"Community: https://discord.com/invite/clawd",
"Find new skills: https://clawdhub.com",
"For Clawdbot behavior, commands, config, or architecture: consult local docs first.",
"When diagnosing issues, run `clawdbot status` yourself when possible; only ask the user if you lack access (e.g., sandboxed).",
"",
];
}
export function buildAgentSystemPrompt(params: { export function buildAgentSystemPrompt(params: {
workspaceDir: string; workspaceDir: string;
defaultThinkLevel?: ThinkLevel; defaultThinkLevel?: ThinkLevel;
@@ -125,6 +145,7 @@ export function buildAgentSystemPrompt(params: {
contextFiles?: EmbeddedContextFile[]; contextFiles?: EmbeddedContextFile[];
skillsPrompt?: string; skillsPrompt?: string;
heartbeatPrompt?: string; heartbeatPrompt?: string;
docsPath?: string;
/** Controls which hardcoded sections to include. Defaults to "full". */ /** Controls which hardcoded sections to include. Defaults to "full". */
promptMode?: PromptMode; promptMode?: PromptMode;
runtimeInfo?: { runtimeInfo?: {
@@ -295,6 +316,11 @@ export function buildAgentSystemPrompt(params: {
readToolName, readToolName,
}); });
const memorySection = buildMemorySection({ isMinimal, availableTools }); const memorySection = buildMemorySection({ isMinimal, availableTools });
const docsSection = buildDocsSection({
docsPath: params.docsPath,
isMinimal,
readToolName,
});
// For "none" mode, return just the basic identity line // For "none" mode, return just the basic identity line
if (promptMode === "none") { if (promptMode === "none") {
@@ -371,6 +397,7 @@ export function buildAgentSystemPrompt(params: {
`Your working directory is: ${params.workspaceDir}`, `Your working directory is: ${params.workspaceDir}`,
"Treat this directory as the single global workspace for file operations unless explicitly instructed otherwise.", "Treat this directory as the single global workspace for file operations unless explicitly instructed otherwise.",
"", "",
...docsSection,
params.sandboxInfo?.enabled ? "## Sandbox" : "", params.sandboxInfo?.enabled ? "## Sandbox" : "",
params.sandboxInfo?.enabled params.sandboxInfo?.enabled
? [ ? [