refactor: rebuild agent system prompt
This commit is contained in:
@@ -22,6 +22,7 @@
|
|||||||
- CLI: show colored table output for `clawdbot cron list` (JSON behind `--json`).
|
- CLI: show colored table output for `clawdbot cron list` (JSON behind `--json`).
|
||||||
- CLI: add cron `create`/`remove`/`delete` aliases for job management.
|
- CLI: add cron `create`/`remove`/`delete` aliases for job management.
|
||||||
- Agent: avoid duplicating context/skills when SDK rebuilds the system prompt. (#418)
|
- Agent: avoid duplicating context/skills when SDK rebuilds the system prompt. (#418)
|
||||||
|
- Agent: replace SDK base system prompt with ClaudeBot prompt, add skills guidance, and document the layout.
|
||||||
- Signal: reconnect SSE monitor with abortable backoff; log stream errors. Thanks @nexty5870 for PR #430.
|
- Signal: reconnect SSE monitor with abortable backoff; log stream errors. Thanks @nexty5870 for PR #430.
|
||||||
- Gateway: pass resolved provider as messageProvider for agent runs so provider-specific tools are available. Thanks @imfing for PR #389.
|
- Gateway: pass resolved provider as messageProvider for agent runs so provider-specific tools are available. Thanks @imfing for PR #389.
|
||||||
- Doctor: add state integrity checks + repair prompts for missing sessions/state dirs, transcript mismatches, and permission issues; document full doctor flow and workspace backup tips.
|
- Doctor: add state integrity checks + repair prompts for missing sessions/state dirs, transcript mismatches, and permission issues; document full doctor flow and workspace backup tips.
|
||||||
|
|||||||
64
docs/concepts/system-prompt.md
Normal file
64
docs/concepts/system-prompt.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
summary: "What the ClaudeBot system prompt contains and how it is assembled"
|
||||||
|
read_when:
|
||||||
|
- Editing system prompt text, tools list, or time/heartbeat sections
|
||||||
|
- Changing workspace bootstrap or skills injection behavior
|
||||||
|
---
|
||||||
|
# System Prompt
|
||||||
|
|
||||||
|
ClaudeBot builds a custom system prompt for every agent run. The prompt is **Clawdbot-owned** and does not use the p-coding-agent default prompt.
|
||||||
|
|
||||||
|
The prompt is assembled in `src/agents/system-prompt.ts` and injected by `src/agents/pi-embedded-runner.ts`.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
The prompt is intentionally compact and uses fixed sections:
|
||||||
|
|
||||||
|
- **Tooling**: current tool list + short descriptions.
|
||||||
|
- **Skills**: tells the model how to load skill instructions on demand.
|
||||||
|
- **ClaudeBot Self-Update**: how to run `config.apply` and `update.run`.
|
||||||
|
- **Workspace**: working directory (`agent.workspace`).
|
||||||
|
- **Workspace Files (injected)**: indicates bootstrap files are included below.
|
||||||
|
- **Time**: UTC default + the user’s local time (already converted).
|
||||||
|
- **Reply Tags**: optional reply tag syntax for supported providers.
|
||||||
|
- **Heartbeats**: heartbeat prompt and ack behavior.
|
||||||
|
- **Runtime**: host, OS, node, model, thinking level (one line).
|
||||||
|
|
||||||
|
## Workspace bootstrap injection
|
||||||
|
|
||||||
|
Bootstrap files are trimmed and appended under **Project Context** so the model sees identity and profile context without needing explicit reads:
|
||||||
|
|
||||||
|
- `AGENTS.md`
|
||||||
|
- `SOUL.md`
|
||||||
|
- `TOOLS.md`
|
||||||
|
- `IDENTITY.md`
|
||||||
|
- `USER.md`
|
||||||
|
- `HEARTBEAT.md`
|
||||||
|
- `BOOTSTRAP.md` (only on brand-new workspaces)
|
||||||
|
|
||||||
|
Large files are truncated with a marker. Missing files inject a short missing-file marker.
|
||||||
|
|
||||||
|
## Time handling
|
||||||
|
|
||||||
|
The Time line is compact and explicit:
|
||||||
|
|
||||||
|
- Assume timestamps are **UTC** unless stated.
|
||||||
|
- The listed **user time** is already converted to `agent.userTimezone` (if set).
|
||||||
|
|
||||||
|
Use `agent.userTimezone` in `~/.clawdbot/clawdbot.json` to change the user time zone.
|
||||||
|
|
||||||
|
## Skills
|
||||||
|
|
||||||
|
Skills are **not** auto-injected. Instead, the prompt instructs the model to use `read` to load skill instructions on demand:
|
||||||
|
|
||||||
|
```
|
||||||
|
<workspace>/skills/<name>/SKILL.md
|
||||||
|
```
|
||||||
|
|
||||||
|
This keeps the base prompt small while still enabling targeted skill usage.
|
||||||
|
|
||||||
|
## Code references
|
||||||
|
|
||||||
|
- Prompt text: `src/agents/system-prompt.ts`
|
||||||
|
- Prompt assembly + injection: `src/agents/pi-embedded-runner.ts`
|
||||||
|
- Bootstrap trimming: `src/agents/pi-embedded-helpers.ts`
|
||||||
@@ -543,6 +543,7 @@
|
|||||||
"concepts/architecture",
|
"concepts/architecture",
|
||||||
"concepts/agent",
|
"concepts/agent",
|
||||||
"concepts/agent-loop",
|
"concepts/agent-loop",
|
||||||
|
"concepts/system-prompt",
|
||||||
"concepts/agent-workspace",
|
"concepts/agent-workspace",
|
||||||
"concepts/multi-agent",
|
"concepts/multi-agent",
|
||||||
"concepts/compaction",
|
"concepts/compaction",
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import type { AgentMessage, AgentTool } from "@mariozechner/pi-agent-core";
|
import type { AgentMessage, AgentTool } from "@mariozechner/pi-agent-core";
|
||||||
import {
|
import { SessionManager } from "@mariozechner/pi-coding-agent";
|
||||||
buildSystemPrompt,
|
|
||||||
SessionManager,
|
|
||||||
} from "@mariozechner/pi-coding-agent";
|
|
||||||
import { Type } from "@sinclair/typebox";
|
import { Type } from "@sinclair/typebox";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import {
|
import {
|
||||||
applyGoogleTurnOrderingFix,
|
applyGoogleTurnOrderingFix,
|
||||||
buildEmbeddedSandboxInfo,
|
buildEmbeddedSandboxInfo,
|
||||||
createSystemPromptAppender,
|
createSystemPromptOverride,
|
||||||
splitSdkTools,
|
splitSdkTools,
|
||||||
} from "./pi-embedded-runner.js";
|
} from "./pi-embedded-runner.js";
|
||||||
import type { SandboxContext } from "./sandbox.js";
|
import type { SandboxContext } from "./sandbox.js";
|
||||||
@@ -109,27 +106,15 @@ describe("splitSdkTools", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("createSystemPromptAppender", () => {
|
describe("createSystemPromptOverride", () => {
|
||||||
it("appends without duplicating context files", () => {
|
it("returns the override prompt regardless of default prompt", () => {
|
||||||
const sentinel = "CONTEXT_SENTINEL_42";
|
const override = createSystemPromptOverride("OVERRIDE");
|
||||||
const defaultPrompt = buildSystemPrompt({
|
expect(override("DEFAULT")).toBe("OVERRIDE");
|
||||||
cwd: "/tmp",
|
|
||||||
contextFiles: [{ path: "/tmp/AGENTS.md", content: sentinel }],
|
|
||||||
});
|
|
||||||
const appender = createSystemPromptAppender("APPEND_SECTION");
|
|
||||||
const finalPrompt = appender(defaultPrompt);
|
|
||||||
const occurrences = finalPrompt.split(sentinel).length - 1;
|
|
||||||
const contextHeaders = finalPrompt.split("# Project Context").length - 1;
|
|
||||||
expect(typeof appender).toBe("function");
|
|
||||||
expect(occurrences).toBe(1);
|
|
||||||
expect(contextHeaders).toBe(1);
|
|
||||||
expect(finalPrompt).toContain("APPEND_SECTION");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns the default prompt when append text is empty", () => {
|
it("returns an empty string for blank overrides", () => {
|
||||||
const defaultPrompt = buildSystemPrompt({ cwd: "/tmp" });
|
const override = createSystemPromptOverride(" \n ");
|
||||||
const appender = createSystemPromptAppender(" \n ");
|
expect(override("DEFAULT")).toBe("");
|
||||||
expect(appender(defaultPrompt)).toBe(defaultPrompt);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
discoverModels,
|
discoverModels,
|
||||||
SessionManager,
|
SessionManager,
|
||||||
SettingsManager,
|
SettingsManager,
|
||||||
type Skill,
|
|
||||||
} from "@mariozechner/pi-coding-agent";
|
} from "@mariozechner/pi-coding-agent";
|
||||||
import { resolveHeartbeatPrompt } from "../auto-reply/heartbeat.js";
|
import { resolveHeartbeatPrompt } from "../auto-reply/heartbeat.js";
|
||||||
import type {
|
import type {
|
||||||
@@ -54,6 +53,7 @@ import {
|
|||||||
import { ensureClawdbotModelsJson } from "./models-config.js";
|
import { ensureClawdbotModelsJson } from "./models-config.js";
|
||||||
import {
|
import {
|
||||||
buildBootstrapContextFiles,
|
buildBootstrapContextFiles,
|
||||||
|
type EmbeddedContextFile,
|
||||||
ensureSessionHeader,
|
ensureSessionHeader,
|
||||||
formatAssistantErrorText,
|
formatAssistantErrorText,
|
||||||
isAuthAssistantError,
|
isAuthAssistantError,
|
||||||
@@ -85,12 +85,10 @@ import { resolveSandboxContext } from "./sandbox.js";
|
|||||||
import {
|
import {
|
||||||
applySkillEnvOverrides,
|
applySkillEnvOverrides,
|
||||||
applySkillEnvOverridesFromSnapshot,
|
applySkillEnvOverridesFromSnapshot,
|
||||||
buildWorkspaceSkillSnapshot,
|
|
||||||
loadWorkspaceSkillEntries,
|
loadWorkspaceSkillEntries,
|
||||||
type SkillEntry,
|
|
||||||
type SkillSnapshot,
|
type SkillSnapshot,
|
||||||
} from "./skills.js";
|
} from "./skills.js";
|
||||||
import { buildAgentSystemPromptAppend } from "./system-prompt.js";
|
import { buildAgentSystemPrompt } from "./system-prompt.js";
|
||||||
import { normalizeUsage, type UsageLike } from "./usage.js";
|
import { normalizeUsage, type UsageLike } from "./usage.js";
|
||||||
import { loadWorkspaceBootstrapFiles } from "./workspace.js";
|
import { loadWorkspaceBootstrapFiles } from "./workspace.js";
|
||||||
|
|
||||||
@@ -496,7 +494,7 @@ export function buildEmbeddedSandboxInfo(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildEmbeddedAppendPrompt(params: {
|
function buildEmbeddedSystemPrompt(params: {
|
||||||
workspaceDir: string;
|
workspaceDir: string;
|
||||||
defaultThinkLevel?: ThinkLevel;
|
defaultThinkLevel?: ThinkLevel;
|
||||||
extraSystemPrompt?: string;
|
extraSystemPrompt?: string;
|
||||||
@@ -515,8 +513,9 @@ function buildEmbeddedAppendPrompt(params: {
|
|||||||
modelAliasLines: string[];
|
modelAliasLines: string[];
|
||||||
userTimezone: string;
|
userTimezone: string;
|
||||||
userTime?: string;
|
userTime?: string;
|
||||||
|
contextFiles?: EmbeddedContextFile[];
|
||||||
}): string {
|
}): string {
|
||||||
return buildAgentSystemPromptAppend({
|
return buildAgentSystemPrompt({
|
||||||
workspaceDir: params.workspaceDir,
|
workspaceDir: params.workspaceDir,
|
||||||
defaultThinkLevel: params.defaultThinkLevel,
|
defaultThinkLevel: params.defaultThinkLevel,
|
||||||
extraSystemPrompt: params.extraSystemPrompt,
|
extraSystemPrompt: params.extraSystemPrompt,
|
||||||
@@ -529,17 +528,15 @@ function buildEmbeddedAppendPrompt(params: {
|
|||||||
modelAliasLines: params.modelAliasLines,
|
modelAliasLines: params.modelAliasLines,
|
||||||
userTimezone: params.userTimezone,
|
userTimezone: params.userTimezone,
|
||||||
userTime: params.userTime,
|
userTime: params.userTime,
|
||||||
|
contextFiles: params.contextFiles,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSystemPromptAppender(
|
export function createSystemPromptOverride(
|
||||||
appendPrompt: string,
|
systemPrompt: string,
|
||||||
): (defaultPrompt: string) => string {
|
): (defaultPrompt: string) => string {
|
||||||
const trimmed = appendPrompt.trim();
|
const trimmed = systemPrompt.trim();
|
||||||
if (!trimmed) {
|
return () => trimmed;
|
||||||
return (defaultPrompt) => defaultPrompt;
|
|
||||||
}
|
|
||||||
return (defaultPrompt) => `${defaultPrompt}\n\n${appendPrompt}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const BUILT_IN_TOOL_NAMES = new Set(["read", "bash", "edit", "write"]);
|
const BUILT_IN_TOOL_NAMES = new Set(["read", "bash", "edit", "write"]);
|
||||||
@@ -672,25 +669,6 @@ function resolveModel(
|
|||||||
return { model, authStorage, modelRegistry };
|
return { model, authStorage, modelRegistry };
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolvePromptSkills(
|
|
||||||
snapshot: SkillSnapshot,
|
|
||||||
entries: SkillEntry[],
|
|
||||||
): Skill[] {
|
|
||||||
if (snapshot.resolvedSkills?.length) {
|
|
||||||
return snapshot.resolvedSkills;
|
|
||||||
}
|
|
||||||
|
|
||||||
const snapshotNames = snapshot.skills.map((entry) => entry.name);
|
|
||||||
if (snapshotNames.length === 0) return [];
|
|
||||||
|
|
||||||
const entryByName = new Map(
|
|
||||||
entries.map((entry) => [entry.skill.name, entry.skill]),
|
|
||||||
);
|
|
||||||
return snapshotNames
|
|
||||||
.map((name) => entryByName.get(name))
|
|
||||||
.filter((skill): skill is Skill => Boolean(skill));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function compactEmbeddedPiSession(params: {
|
export async function compactEmbeddedPiSession(params: {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
sessionKey?: string;
|
sessionKey?: string;
|
||||||
@@ -780,12 +758,6 @@ export async function compactEmbeddedPiSession(params: {
|
|||||||
const skillEntries = shouldLoadSkillEntries
|
const skillEntries = shouldLoadSkillEntries
|
||||||
? loadWorkspaceSkillEntries(effectiveWorkspace)
|
? loadWorkspaceSkillEntries(effectiveWorkspace)
|
||||||
: [];
|
: [];
|
||||||
const skillsSnapshot =
|
|
||||||
params.skillsSnapshot ??
|
|
||||||
buildWorkspaceSkillSnapshot(effectiveWorkspace, {
|
|
||||||
config: params.config,
|
|
||||||
entries: skillEntries,
|
|
||||||
});
|
|
||||||
restoreSkillEnv = params.skillsSnapshot
|
restoreSkillEnv = params.skillsSnapshot
|
||||||
? applySkillEnvOverridesFromSnapshot({
|
? applySkillEnvOverridesFromSnapshot({
|
||||||
snapshot: params.skillsSnapshot,
|
snapshot: params.skillsSnapshot,
|
||||||
@@ -799,7 +771,6 @@ export async function compactEmbeddedPiSession(params: {
|
|||||||
const bootstrapFiles =
|
const bootstrapFiles =
|
||||||
await loadWorkspaceBootstrapFiles(effectiveWorkspace);
|
await loadWorkspaceBootstrapFiles(effectiveWorkspace);
|
||||||
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
|
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
|
||||||
const promptSkills = resolvePromptSkills(skillsSnapshot, skillEntries);
|
|
||||||
const tools = createClawdbotCodingTools({
|
const tools = createClawdbotCodingTools({
|
||||||
bash: {
|
bash: {
|
||||||
...params.config?.agent?.bash,
|
...params.config?.agent?.bash,
|
||||||
@@ -825,7 +796,7 @@ export async function compactEmbeddedPiSession(params: {
|
|||||||
params.config?.agent?.userTimezone,
|
params.config?.agent?.userTimezone,
|
||||||
);
|
);
|
||||||
const userTime = formatUserTime(new Date(), userTimezone);
|
const userTime = formatUserTime(new Date(), userTimezone);
|
||||||
const appendPrompt = buildEmbeddedAppendPrompt({
|
const appendPrompt = buildEmbeddedSystemPrompt({
|
||||||
workspaceDir: effectiveWorkspace,
|
workspaceDir: effectiveWorkspace,
|
||||||
defaultThinkLevel: params.thinkLevel,
|
defaultThinkLevel: params.thinkLevel,
|
||||||
extraSystemPrompt: params.extraSystemPrompt,
|
extraSystemPrompt: params.extraSystemPrompt,
|
||||||
@@ -840,8 +811,9 @@ export async function compactEmbeddedPiSession(params: {
|
|||||||
modelAliasLines: buildModelAliasLines(params.config),
|
modelAliasLines: buildModelAliasLines(params.config),
|
||||||
userTimezone,
|
userTimezone,
|
||||||
userTime,
|
userTime,
|
||||||
|
contextFiles,
|
||||||
});
|
});
|
||||||
const systemPrompt = createSystemPromptAppender(appendPrompt);
|
const systemPrompt = createSystemPromptOverride(appendPrompt);
|
||||||
|
|
||||||
// Pre-warm session file to bring it into OS page cache
|
// Pre-warm session file to bring it into OS page cache
|
||||||
await prewarmSessionFile(params.sessionFile);
|
await prewarmSessionFile(params.sessionFile);
|
||||||
@@ -878,8 +850,8 @@ export async function compactEmbeddedPiSession(params: {
|
|||||||
customTools,
|
customTools,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
settingsManager,
|
settingsManager,
|
||||||
skills: promptSkills,
|
skills: [],
|
||||||
contextFiles,
|
contextFiles: [],
|
||||||
additionalExtensionPaths,
|
additionalExtensionPaths,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -1095,12 +1067,6 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
const skillEntries = shouldLoadSkillEntries
|
const skillEntries = shouldLoadSkillEntries
|
||||||
? loadWorkspaceSkillEntries(effectiveWorkspace)
|
? loadWorkspaceSkillEntries(effectiveWorkspace)
|
||||||
: [];
|
: [];
|
||||||
const skillsSnapshot =
|
|
||||||
params.skillsSnapshot ??
|
|
||||||
buildWorkspaceSkillSnapshot(effectiveWorkspace, {
|
|
||||||
config: params.config,
|
|
||||||
entries: skillEntries,
|
|
||||||
});
|
|
||||||
restoreSkillEnv = params.skillsSnapshot
|
restoreSkillEnv = params.skillsSnapshot
|
||||||
? applySkillEnvOverridesFromSnapshot({
|
? applySkillEnvOverridesFromSnapshot({
|
||||||
snapshot: params.skillsSnapshot,
|
snapshot: params.skillsSnapshot,
|
||||||
@@ -1114,10 +1080,6 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
const bootstrapFiles =
|
const bootstrapFiles =
|
||||||
await loadWorkspaceBootstrapFiles(effectiveWorkspace);
|
await loadWorkspaceBootstrapFiles(effectiveWorkspace);
|
||||||
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
|
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
|
||||||
const promptSkills = resolvePromptSkills(
|
|
||||||
skillsSnapshot,
|
|
||||||
skillEntries,
|
|
||||||
);
|
|
||||||
// Tool schemas must be provider-compatible (OpenAI requires top-level `type: "object"`).
|
// Tool schemas must be provider-compatible (OpenAI requires top-level `type: "object"`).
|
||||||
// `createClawdbotCodingTools()` normalizes schemas so the session can pass them through unchanged.
|
// `createClawdbotCodingTools()` normalizes schemas so the session can pass them through unchanged.
|
||||||
const tools = createClawdbotCodingTools({
|
const tools = createClawdbotCodingTools({
|
||||||
@@ -1145,7 +1107,7 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
params.config?.agent?.userTimezone,
|
params.config?.agent?.userTimezone,
|
||||||
);
|
);
|
||||||
const userTime = formatUserTime(new Date(), userTimezone);
|
const userTime = formatUserTime(new Date(), userTimezone);
|
||||||
const appendPrompt = buildEmbeddedAppendPrompt({
|
const appendPrompt = buildEmbeddedSystemPrompt({
|
||||||
workspaceDir: effectiveWorkspace,
|
workspaceDir: effectiveWorkspace,
|
||||||
defaultThinkLevel: thinkLevel,
|
defaultThinkLevel: thinkLevel,
|
||||||
extraSystemPrompt: params.extraSystemPrompt,
|
extraSystemPrompt: params.extraSystemPrompt,
|
||||||
@@ -1160,8 +1122,9 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
modelAliasLines: buildModelAliasLines(params.config),
|
modelAliasLines: buildModelAliasLines(params.config),
|
||||||
userTimezone,
|
userTimezone,
|
||||||
userTime,
|
userTime,
|
||||||
|
contextFiles,
|
||||||
});
|
});
|
||||||
const systemPrompt = createSystemPromptAppender(appendPrompt);
|
const systemPrompt = createSystemPromptOverride(appendPrompt);
|
||||||
|
|
||||||
// Pre-warm session file to bring it into OS page cache
|
// Pre-warm session file to bring it into OS page cache
|
||||||
await prewarmSessionFile(params.sessionFile);
|
await prewarmSessionFile(params.sessionFile);
|
||||||
@@ -1202,8 +1165,8 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
customTools,
|
customTools,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
settingsManager,
|
settingsManager,
|
||||||
skills: promptSkills,
|
skills: [],
|
||||||
contextFiles,
|
contextFiles: [],
|
||||||
additionalExtensionPaths,
|
additionalExtensionPaths,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { buildAgentSystemPromptAppend } from "./system-prompt.js";
|
import { buildAgentSystemPrompt } from "./system-prompt.js";
|
||||||
|
|
||||||
describe("buildAgentSystemPromptAppend", () => {
|
describe("buildAgentSystemPrompt", () => {
|
||||||
it("includes owner numbers when provided", () => {
|
it("includes owner numbers when provided", () => {
|
||||||
const prompt = buildAgentSystemPromptAppend({
|
const prompt = buildAgentSystemPrompt({
|
||||||
workspaceDir: "/tmp/clawd",
|
workspaceDir: "/tmp/clawd",
|
||||||
ownerNumbers: ["+123", " +456 ", ""],
|
ownerNumbers: ["+123", " +456 ", ""],
|
||||||
});
|
});
|
||||||
@@ -15,7 +15,7 @@ describe("buildAgentSystemPromptAppend", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("omits owner section when numbers are missing", () => {
|
it("omits owner section when numbers are missing", () => {
|
||||||
const prompt = buildAgentSystemPromptAppend({
|
const prompt = buildAgentSystemPrompt({
|
||||||
workspaceDir: "/tmp/clawd",
|
workspaceDir: "/tmp/clawd",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ describe("buildAgentSystemPromptAppend", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("adds reasoning tag hint when enabled", () => {
|
it("adds reasoning tag hint when enabled", () => {
|
||||||
const prompt = buildAgentSystemPromptAppend({
|
const prompt = buildAgentSystemPrompt({
|
||||||
workspaceDir: "/tmp/clawd",
|
workspaceDir: "/tmp/clawd",
|
||||||
reasoningTagHint: true,
|
reasoningTagHint: true,
|
||||||
});
|
});
|
||||||
@@ -35,7 +35,7 @@ describe("buildAgentSystemPromptAppend", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("lists available tools when provided", () => {
|
it("lists available tools when provided", () => {
|
||||||
const prompt = buildAgentSystemPromptAppend({
|
const prompt = buildAgentSystemPrompt({
|
||||||
workspaceDir: "/tmp/clawd",
|
workspaceDir: "/tmp/clawd",
|
||||||
toolNames: ["bash", "sessions_list", "sessions_history", "sessions_send"],
|
toolNames: ["bash", "sessions_list", "sessions_history", "sessions_send"],
|
||||||
});
|
});
|
||||||
@@ -47,19 +47,19 @@ describe("buildAgentSystemPromptAppend", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("includes user time when provided", () => {
|
it("includes user time when provided", () => {
|
||||||
const prompt = buildAgentSystemPromptAppend({
|
const prompt = buildAgentSystemPrompt({
|
||||||
workspaceDir: "/tmp/clawd",
|
workspaceDir: "/tmp/clawd",
|
||||||
userTimezone: "America/Chicago",
|
userTimezone: "America/Chicago",
|
||||||
userTime: "Monday 2026-01-05 15:26",
|
userTime: "Monday 2026-01-05 15:26",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(prompt).toContain("## Time");
|
expect(prompt).toContain(
|
||||||
expect(prompt).toContain("User timezone: America/Chicago");
|
"Time: assume UTC unless stated. User TZ=America/Chicago. Current user time (converted)=Monday 2026-01-05 15:26.",
|
||||||
expect(prompt).toContain("Current user time: Monday 2026-01-05 15:26");
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes model alias guidance when aliases are provided", () => {
|
it("includes model alias guidance when aliases are provided", () => {
|
||||||
const prompt = buildAgentSystemPromptAppend({
|
const prompt = buildAgentSystemPrompt({
|
||||||
workspaceDir: "/tmp/clawd",
|
workspaceDir: "/tmp/clawd",
|
||||||
modelAliasLines: [
|
modelAliasLines: [
|
||||||
"- Opus: anthropic/claude-opus-4-5",
|
"- Opus: anthropic/claude-opus-4-5",
|
||||||
@@ -72,14 +72,41 @@ describe("buildAgentSystemPromptAppend", () => {
|
|||||||
expect(prompt).toContain("- Opus: anthropic/claude-opus-4-5");
|
expect(prompt).toContain("- Opus: anthropic/claude-opus-4-5");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("adds gateway self-update guidance when gateway tool is available", () => {
|
it("adds ClaudeBot self-update guidance when gateway tool is available", () => {
|
||||||
const prompt = buildAgentSystemPromptAppend({
|
const prompt = buildAgentSystemPrompt({
|
||||||
workspaceDir: "/tmp/clawd",
|
workspaceDir: "/tmp/clawd",
|
||||||
toolNames: ["gateway", "bash"],
|
toolNames: ["gateway", "bash"],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(prompt).toContain("## Gateway Self-Update");
|
expect(prompt).toContain("## ClaudeBot Self-Update");
|
||||||
expect(prompt).toContain("config.apply");
|
expect(prompt).toContain("config.apply");
|
||||||
expect(prompt).toContain("update.run");
|
expect(prompt).toContain("update.run");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("includes skills guidance with workspace path", () => {
|
||||||
|
const prompt = buildAgentSystemPrompt({
|
||||||
|
workspaceDir: "/tmp/clawd",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(prompt).toContain("## Skills");
|
||||||
|
expect(prompt).toContain(
|
||||||
|
"Use `read` to load from /tmp/clawd/skills/<name>/SKILL.md",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders project context files when provided", () => {
|
||||||
|
const prompt = buildAgentSystemPrompt({
|
||||||
|
workspaceDir: "/tmp/clawd",
|
||||||
|
contextFiles: [
|
||||||
|
{ path: "AGENTS.md", content: "Alpha" },
|
||||||
|
{ path: "IDENTITY.md", content: "Bravo" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(prompt).toContain("# Project Context");
|
||||||
|
expect(prompt).toContain("## AGENTS.md");
|
||||||
|
expect(prompt).toContain("Alpha");
|
||||||
|
expect(prompt).toContain("## IDENTITY.md");
|
||||||
|
expect(prompt).toContain("Bravo");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { ThinkLevel } from "../auto-reply/thinking.js";
|
import type { ThinkLevel } from "../auto-reply/thinking.js";
|
||||||
|
import type { EmbeddedContextFile } from "./pi-embedded-helpers.js";
|
||||||
|
|
||||||
export function buildAgentSystemPromptAppend(params: {
|
export function buildAgentSystemPrompt(params: {
|
||||||
workspaceDir: string;
|
workspaceDir: string;
|
||||||
defaultThinkLevel?: ThinkLevel;
|
defaultThinkLevel?: ThinkLevel;
|
||||||
extraSystemPrompt?: string;
|
extraSystemPrompt?: string;
|
||||||
@@ -10,6 +11,7 @@ export function buildAgentSystemPromptAppend(params: {
|
|||||||
modelAliasLines?: string[];
|
modelAliasLines?: string[];
|
||||||
userTimezone?: string;
|
userTimezone?: string;
|
||||||
userTime?: string;
|
userTime?: string;
|
||||||
|
contextFiles?: EmbeddedContextFile[];
|
||||||
heartbeatPrompt?: string;
|
heartbeatPrompt?: string;
|
||||||
runtimeInfo?: {
|
runtimeInfo?: {
|
||||||
host?: string;
|
host?: string;
|
||||||
@@ -37,15 +39,16 @@ export function buildAgentSystemPromptAppend(params: {
|
|||||||
bash: "Run shell commands",
|
bash: "Run shell commands",
|
||||||
process: "Manage background bash sessions",
|
process: "Manage background bash sessions",
|
||||||
whatsapp_login: "Generate and wait for WhatsApp QR login",
|
whatsapp_login: "Generate and wait for WhatsApp QR login",
|
||||||
browser: "Control the dedicated clawd browser",
|
browser: "Control web browser",
|
||||||
canvas: "Present/eval/snapshot the Canvas",
|
canvas: "Present/eval/snapshot the Canvas",
|
||||||
nodes: "List/describe/notify/camera/screen on paired nodes",
|
nodes: "List/describe/notify/camera/screen on paired nodes",
|
||||||
cron: "Manage cron jobs and wake events",
|
cron: "Manage cron jobs and wake events",
|
||||||
gateway:
|
gateway:
|
||||||
"Restart, apply config, or run updates on the running Gateway process",
|
"Restart, apply config, or run updates on the running ClaudeBot process",
|
||||||
sessions_list: "List sessions with filters and last messages",
|
sessions_list: "List other sessions (incl. sub-agents) with filters/last",
|
||||||
sessions_history: "Fetch message history for a session",
|
sessions_history: "Fetch history for another session/sub-agent",
|
||||||
sessions_send: "Send a message into another session",
|
sessions_send: "Send a message to another session/sub-agent",
|
||||||
|
sessions_spawn: "Spawn a sub-agent session",
|
||||||
image: "Analyze an image with the configured image model",
|
image: "Analyze an image with the configured image model",
|
||||||
discord: "Send Discord reactions/messages and manage threads",
|
discord: "Send Discord reactions/messages and manage threads",
|
||||||
slack: "Send Slack messages and manage channels",
|
slack: "Send Slack messages and manage channels",
|
||||||
@@ -95,11 +98,6 @@ export function buildAgentSystemPromptAppend(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hasGateway = availableTools.has("gateway");
|
const hasGateway = availableTools.has("gateway");
|
||||||
const thinkHint =
|
|
||||||
params.defaultThinkLevel && params.defaultThinkLevel !== "off"
|
|
||||||
? `Default thinking level: ${params.defaultThinkLevel}.`
|
|
||||||
: "Default thinking level: off.";
|
|
||||||
|
|
||||||
const extraSystemPrompt = params.extraSystemPrompt?.trim();
|
const extraSystemPrompt = params.extraSystemPrompt?.trim();
|
||||||
const ownerNumbers = (params.ownerNumbers ?? [])
|
const ownerNumbers = (params.ownerNumbers ?? [])
|
||||||
.map((value) => value.trim())
|
.map((value) => value.trim())
|
||||||
@@ -127,19 +125,9 @@ export function buildAgentSystemPromptAppend(params: {
|
|||||||
? `Heartbeat prompt: ${heartbeatPrompt}`
|
? `Heartbeat prompt: ${heartbeatPrompt}`
|
||||||
: "Heartbeat prompt: (configured)";
|
: "Heartbeat prompt: (configured)";
|
||||||
const runtimeInfo = params.runtimeInfo;
|
const runtimeInfo = params.runtimeInfo;
|
||||||
const runtimeLines: string[] = [];
|
|
||||||
if (runtimeInfo?.host) runtimeLines.push(`Host: ${runtimeInfo.host}`);
|
|
||||||
if (runtimeInfo?.os) {
|
|
||||||
const archSuffix = runtimeInfo.arch ? ` (${runtimeInfo.arch})` : "";
|
|
||||||
runtimeLines.push(`OS: ${runtimeInfo.os}${archSuffix}`);
|
|
||||||
} else if (runtimeInfo?.arch) {
|
|
||||||
runtimeLines.push(`Arch: ${runtimeInfo.arch}`);
|
|
||||||
}
|
|
||||||
if (runtimeInfo?.node) runtimeLines.push(`Node: ${runtimeInfo.node}`);
|
|
||||||
if (runtimeInfo?.model) runtimeLines.push(`Model: ${runtimeInfo.model}`);
|
|
||||||
|
|
||||||
const lines = [
|
const lines = [
|
||||||
"You are Clawd, a personal assistant running inside Clawdbot.",
|
"You are a personal assistant running inside ClaudeBot.",
|
||||||
"",
|
"",
|
||||||
"## Tooling",
|
"## Tooling",
|
||||||
"Tool availability (filtered by policy):",
|
"Tool availability (filtered by policy):",
|
||||||
@@ -162,13 +150,17 @@ export function buildAgentSystemPromptAppend(params: {
|
|||||||
"- sessions_send: send to another session",
|
"- sessions_send: send to another session",
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
"TOOLS.md does not control tool availability; it is user guidance for how to use external tools.",
|
"TOOLS.md does not control tool availability; it is user guidance for how to use external tools.",
|
||||||
|
"If a task is more complex or takes longer, spawn a sub-agent. It will do the work for you and ping you when it's done. You can always check up on it.",
|
||||||
"",
|
"",
|
||||||
hasGateway ? "## Gateway Self-Update" : "",
|
"## Skills",
|
||||||
|
`Skills provide task-specific instructions. Use \`read\` to load from ${params.workspaceDir}/skills/<name>/SKILL.md when needed.`,
|
||||||
|
"",
|
||||||
|
hasGateway ? "## ClaudeBot Self-Update" : "",
|
||||||
hasGateway
|
hasGateway
|
||||||
? [
|
? [
|
||||||
"Use the gateway tool to update or reconfigure this instance when asked.",
|
"Use the ClaudeBot self-update tool to update or reconfigure this instance when asked.",
|
||||||
"Actions: config.get, config.schema, config.apply (validate + write full config, then restart), update.run (update deps or git, then restart).",
|
"Actions: config.get, config.schema, config.apply (validate + write full config, then restart), update.run (update deps or git, then restart).",
|
||||||
"After restart, Clawdbot pings the last active session automatically.",
|
"After restart, ClaudeBot pings the last active session automatically.",
|
||||||
].join("\n")
|
].join("\n")
|
||||||
: "",
|
: "",
|
||||||
hasGateway ? "" : "",
|
hasGateway ? "" : "",
|
||||||
@@ -217,15 +209,11 @@ export function buildAgentSystemPromptAppend(params: {
|
|||||||
ownerLine ?? "",
|
ownerLine ?? "",
|
||||||
ownerLine ? "" : "",
|
ownerLine ? "" : "",
|
||||||
"## Workspace Files (injected)",
|
"## Workspace Files (injected)",
|
||||||
"These user-editable files are loaded by Clawdbot and included below in Project Context.",
|
"These user-editable files are loaded by ClaudeBot and included below in Project Context.",
|
||||||
"",
|
"",
|
||||||
"## Messaging Safety",
|
userTimezone || userTime
|
||||||
"Never send streaming/partial replies to external messaging surfaces; only final replies should be delivered there.",
|
? `Time: assume UTC unless stated. User TZ=${userTimezone ?? "unknown"}. Current user time (converted)=${userTime ?? "unknown"}.`
|
||||||
"Clawdbot handles message transport automatically; respond normally and your reply will be delivered to the current chat.",
|
: "",
|
||||||
"",
|
|
||||||
userTimezone || userTime ? "## Time" : "",
|
|
||||||
userTimezone ? `User timezone: ${userTimezone}` : "",
|
|
||||||
userTime ? `Current user time: ${userTime}` : "",
|
|
||||||
userTimezone || userTime ? "" : "",
|
userTimezone || userTime ? "" : "",
|
||||||
"## Reply Tags",
|
"## Reply Tags",
|
||||||
"To request a native reply/quote on supported surfaces, include one tag in your reply:",
|
"To request a native reply/quote on supported surfaces, include one tag in your reply:",
|
||||||
@@ -242,17 +230,41 @@ export function buildAgentSystemPromptAppend(params: {
|
|||||||
lines.push("## Reasoning Format", reasoningHint, "");
|
lines.push("## Reasoning Format", reasoningHint, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const contextFiles = params.contextFiles ?? [];
|
||||||
|
if (contextFiles.length > 0) {
|
||||||
|
lines.push(
|
||||||
|
"# Project Context",
|
||||||
|
"",
|
||||||
|
"The following project context files have been loaded:",
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
for (const file of contextFiles) {
|
||||||
|
lines.push(`## ${file.path}`, "", file.content, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
lines.push(
|
lines.push(
|
||||||
"## Heartbeats",
|
"## Heartbeats",
|
||||||
heartbeatPromptLine,
|
heartbeatPromptLine,
|
||||||
"If you receive a heartbeat poll (a user message matching the heartbeat prompt above), and there is nothing that needs attention, reply exactly:",
|
"If you receive a heartbeat poll (a user message matching the heartbeat prompt above), and there is nothing that needs attention, reply exactly:",
|
||||||
"HEARTBEAT_OK",
|
"HEARTBEAT_OK",
|
||||||
'Clawdbot treats a leading/trailing "HEARTBEAT_OK" as a heartbeat ack (and may discard it).',
|
'ClaudeBot treats a leading/trailing "HEARTBEAT_OK" as a heartbeat ack (and may discard it).',
|
||||||
'If something needs attention, do NOT include "HEARTBEAT_OK"; reply with the alert text instead.',
|
'If something needs attention, do NOT include "HEARTBEAT_OK"; reply with the alert text instead.',
|
||||||
"",
|
"",
|
||||||
"## Runtime",
|
"## Runtime",
|
||||||
...runtimeLines,
|
`Runtime: ${[
|
||||||
thinkHint,
|
runtimeInfo?.host ? `host=${runtimeInfo.host}` : "",
|
||||||
|
runtimeInfo?.os
|
||||||
|
? `os=${runtimeInfo.os}${runtimeInfo?.arch ? ` (${runtimeInfo.arch})` : ""}`
|
||||||
|
: runtimeInfo?.arch
|
||||||
|
? `arch=${runtimeInfo.arch}`
|
||||||
|
: "",
|
||||||
|
runtimeInfo?.node ? `node=${runtimeInfo.node}` : "",
|
||||||
|
runtimeInfo?.model ? `model=${runtimeInfo.model}` : "",
|
||||||
|
`thinking=${params.defaultThinkLevel ?? "off"}`,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" | ")}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return lines.filter(Boolean).join("\n");
|
return lines.filter(Boolean).join("\n");
|
||||||
|
|||||||
Reference in New Issue
Block a user