feat: update heartbeat defaults
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
SettingsManager,
|
||||
type Skill,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
import { resolveHeartbeatPrompt } from "../auto-reply/heartbeat.js";
|
||||
import type { ThinkLevel, VerboseLevel } from "../auto-reply/thinking.js";
|
||||
import { formatToolAggregate } from "../auto-reply/tool-meta.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
@@ -469,6 +470,9 @@ export async function compactEmbeddedPiSession(params: {
|
||||
extraSystemPrompt: params.extraSystemPrompt,
|
||||
ownerNumbers: params.ownerNumbers,
|
||||
reasoningTagHint,
|
||||
heartbeatPrompt: resolveHeartbeatPrompt(
|
||||
params.config?.agent?.heartbeat?.prompt,
|
||||
),
|
||||
runtimeInfo,
|
||||
sandboxInfo,
|
||||
toolNames: tools.map((tool) => tool.name),
|
||||
@@ -765,6 +769,9 @@ export async function runEmbeddedPiAgent(params: {
|
||||
extraSystemPrompt: params.extraSystemPrompt,
|
||||
ownerNumbers: params.ownerNumbers,
|
||||
reasoningTagHint,
|
||||
heartbeatPrompt: resolveHeartbeatPrompt(
|
||||
params.config?.agent?.heartbeat?.prompt,
|
||||
),
|
||||
runtimeInfo,
|
||||
sandboxInfo,
|
||||
toolNames: tools.map((tool) => tool.name),
|
||||
|
||||
@@ -9,6 +9,7 @@ export function buildAgentSystemPromptAppend(params: {
|
||||
toolNames?: string[];
|
||||
userTimezone?: string;
|
||||
userTime?: string;
|
||||
heartbeatPrompt?: string;
|
||||
runtimeInfo?: {
|
||||
host?: string;
|
||||
os?: string;
|
||||
@@ -113,6 +114,10 @@ export function buildAgentSystemPromptAppend(params: {
|
||||
: undefined;
|
||||
const userTimezone = params.userTimezone?.trim();
|
||||
const userTime = params.userTime?.trim();
|
||||
const heartbeatPrompt = params.heartbeatPrompt?.trim();
|
||||
const heartbeatPromptLine = heartbeatPrompt
|
||||
? `Heartbeat prompt: ${heartbeatPrompt}`
|
||||
: "Heartbeat prompt: (configured)";
|
||||
const runtimeInfo = params.runtimeInfo;
|
||||
const runtimeLines: string[] = [];
|
||||
if (runtimeInfo?.host) runtimeLines.push(`Host: ${runtimeInfo.host}`);
|
||||
@@ -207,7 +212,8 @@ export function buildAgentSystemPromptAppend(params: {
|
||||
|
||||
lines.push(
|
||||
"## Heartbeats",
|
||||
'If you receive a heartbeat poll (a user message containing just "HEARTBEAT"), and there is nothing that needs attention, reply exactly:',
|
||||
heartbeatPromptLine,
|
||||
"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",
|
||||
'Clawdbot 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.',
|
||||
|
||||
@@ -23,9 +23,11 @@ describe("ensureAgentWorkspace", () => {
|
||||
|
||||
const identity = path.join(path.resolve(nested), "IDENTITY.md");
|
||||
const user = path.join(path.resolve(nested), "USER.md");
|
||||
const heartbeat = path.join(path.resolve(nested), "HEARTBEAT.md");
|
||||
const bootstrap = path.join(path.resolve(nested), "BOOTSTRAP.md");
|
||||
await expect(fs.stat(identity)).resolves.toBeDefined();
|
||||
await expect(fs.stat(user)).resolves.toBeDefined();
|
||||
await expect(fs.stat(heartbeat)).resolves.toBeDefined();
|
||||
await expect(fs.stat(bootstrap)).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ export const DEFAULT_SOUL_FILENAME = "SOUL.md";
|
||||
export const DEFAULT_TOOLS_FILENAME = "TOOLS.md";
|
||||
export const DEFAULT_IDENTITY_FILENAME = "IDENTITY.md";
|
||||
export const DEFAULT_USER_FILENAME = "USER.md";
|
||||
export const DEFAULT_HEARTBEAT_FILENAME = "HEARTBEAT.md";
|
||||
export const DEFAULT_BOOTSTRAP_FILENAME = "BOOTSTRAP.md";
|
||||
|
||||
const DEFAULT_AGENTS_TEMPLATE = `# AGENTS.md - Clawdbot Workspace
|
||||
@@ -53,6 +54,9 @@ git commit -m "Add agent workspace"
|
||||
- On session start, read today + yesterday if present.
|
||||
- Capture durable facts, preferences, and decisions; avoid secrets.
|
||||
|
||||
## Heartbeats (optional)
|
||||
- HEARTBEAT.md can hold a tiny checklist for heartbeat runs; keep it small.
|
||||
|
||||
## Customize
|
||||
- Add your preferred style, rules, and "memory" here.
|
||||
`;
|
||||
@@ -83,6 +87,12 @@ It does not define which tools exist; Clawdbot provides built-in tools internall
|
||||
Add whatever else you want the assistant to know about your local toolchain.
|
||||
`;
|
||||
|
||||
const DEFAULT_HEARTBEAT_TEMPLATE = `# HEARTBEAT.md - Optional heartbeat notes
|
||||
|
||||
Keep this file small. Leave it empty unless you want a short checklist or reminders
|
||||
to follow during heartbeat runs.
|
||||
`;
|
||||
|
||||
const DEFAULT_BOOTSTRAP_TEMPLATE = `# BOOTSTRAP.md - First Run Ritual (delete after)
|
||||
|
||||
Hello. I was just born.
|
||||
@@ -174,6 +184,7 @@ export type WorkspaceBootstrapFileName =
|
||||
| typeof DEFAULT_TOOLS_FILENAME
|
||||
| typeof DEFAULT_IDENTITY_FILENAME
|
||||
| typeof DEFAULT_USER_FILENAME
|
||||
| typeof DEFAULT_HEARTBEAT_FILENAME
|
||||
| typeof DEFAULT_BOOTSTRAP_FILENAME;
|
||||
|
||||
export type WorkspaceBootstrapFile = {
|
||||
@@ -205,6 +216,7 @@ export async function ensureAgentWorkspace(params?: {
|
||||
toolsPath?: string;
|
||||
identityPath?: string;
|
||||
userPath?: string;
|
||||
heartbeatPath?: string;
|
||||
bootstrapPath?: string;
|
||||
}> {
|
||||
const rawDir = params?.dir?.trim()
|
||||
@@ -220,10 +232,18 @@ export async function ensureAgentWorkspace(params?: {
|
||||
const toolsPath = path.join(dir, DEFAULT_TOOLS_FILENAME);
|
||||
const identityPath = path.join(dir, DEFAULT_IDENTITY_FILENAME);
|
||||
const userPath = path.join(dir, DEFAULT_USER_FILENAME);
|
||||
const heartbeatPath = path.join(dir, DEFAULT_HEARTBEAT_FILENAME);
|
||||
const bootstrapPath = path.join(dir, DEFAULT_BOOTSTRAP_FILENAME);
|
||||
|
||||
const isBrandNewWorkspace = await (async () => {
|
||||
const paths = [agentsPath, soulPath, toolsPath, identityPath, userPath];
|
||||
const paths = [
|
||||
agentsPath,
|
||||
soulPath,
|
||||
toolsPath,
|
||||
identityPath,
|
||||
userPath,
|
||||
heartbeatPath,
|
||||
];
|
||||
const existing = await Promise.all(
|
||||
paths.map(async (p) => {
|
||||
try {
|
||||
@@ -257,6 +277,10 @@ export async function ensureAgentWorkspace(params?: {
|
||||
DEFAULT_USER_FILENAME,
|
||||
DEFAULT_USER_TEMPLATE,
|
||||
);
|
||||
const heartbeatTemplate = await loadTemplate(
|
||||
DEFAULT_HEARTBEAT_FILENAME,
|
||||
DEFAULT_HEARTBEAT_TEMPLATE,
|
||||
);
|
||||
const bootstrapTemplate = await loadTemplate(
|
||||
DEFAULT_BOOTSTRAP_FILENAME,
|
||||
DEFAULT_BOOTSTRAP_TEMPLATE,
|
||||
@@ -267,6 +291,7 @@ export async function ensureAgentWorkspace(params?: {
|
||||
await writeFileIfMissing(toolsPath, toolsTemplate);
|
||||
await writeFileIfMissing(identityPath, identityTemplate);
|
||||
await writeFileIfMissing(userPath, userTemplate);
|
||||
await writeFileIfMissing(heartbeatPath, heartbeatTemplate);
|
||||
if (isBrandNewWorkspace) {
|
||||
await writeFileIfMissing(bootstrapPath, bootstrapTemplate);
|
||||
}
|
||||
@@ -278,6 +303,7 @@ export async function ensureAgentWorkspace(params?: {
|
||||
toolsPath,
|
||||
identityPath,
|
||||
userPath,
|
||||
heartbeatPath,
|
||||
bootstrapPath,
|
||||
};
|
||||
}
|
||||
@@ -311,6 +337,10 @@ export async function loadWorkspaceBootstrapFiles(
|
||||
name: DEFAULT_USER_FILENAME,
|
||||
filePath: path.join(resolvedDir, DEFAULT_USER_FILENAME),
|
||||
},
|
||||
{
|
||||
name: DEFAULT_HEARTBEAT_FILENAME,
|
||||
filePath: path.join(resolvedDir, DEFAULT_HEARTBEAT_FILENAME),
|
||||
},
|
||||
{
|
||||
name: DEFAULT_BOOTSTRAP_FILENAME,
|
||||
filePath: path.join(resolvedDir, DEFAULT_BOOTSTRAP_FILENAME),
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { HEARTBEAT_TOKEN } from "./tokens.js";
|
||||
|
||||
export const HEARTBEAT_PROMPT = "HEARTBEAT";
|
||||
export const HEARTBEAT_PROMPT =
|
||||
"Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.";
|
||||
export const DEFAULT_HEARTBEAT_EVERY = "30m";
|
||||
export const DEFAULT_HEARTBEAT_ACK_MAX_CHARS = 30;
|
||||
|
||||
export function resolveHeartbeatPrompt(raw?: string): string {
|
||||
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
||||
return trimmed || HEARTBEAT_PROMPT;
|
||||
}
|
||||
|
||||
export type StripHeartbeatMode = "heartbeat" | "message";
|
||||
|
||||
function stripTokenAtEdges(raw: string): { text: string; didStrip: boolean } {
|
||||
|
||||
@@ -853,7 +853,7 @@ export type ClawdbotConfig = {
|
||||
typingIntervalSeconds?: number;
|
||||
/** Periodic background heartbeat runs. */
|
||||
heartbeat?: {
|
||||
/** Heartbeat interval (duration string, default unit: minutes). */
|
||||
/** Heartbeat interval (duration string, default unit: minutes; default: 30m). */
|
||||
every?: string;
|
||||
/** Heartbeat model override (provider/model). */
|
||||
model?: string;
|
||||
@@ -869,7 +869,7 @@ export type ClawdbotConfig = {
|
||||
| "none";
|
||||
/** Optional delivery override (E.164 for WhatsApp, chat id for Telegram). */
|
||||
to?: string;
|
||||
/** Override the heartbeat prompt body (default: "HEARTBEAT"). */
|
||||
/** Override the heartbeat prompt body (default: "Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time."). */
|
||||
prompt?: string;
|
||||
/** Max chars allowed after HEARTBEAT_OK before delivery (default: 30). */
|
||||
ackMaxChars?: number;
|
||||
|
||||
@@ -13,8 +13,11 @@ import {
|
||||
} from "./heartbeat-runner.js";
|
||||
|
||||
describe("resolveHeartbeatIntervalMs", () => {
|
||||
it("returns null when unset or invalid", () => {
|
||||
expect(resolveHeartbeatIntervalMs({})).toBeNull();
|
||||
it("returns default when unset", () => {
|
||||
expect(resolveHeartbeatIntervalMs({})).toBe(30 * 60_000);
|
||||
});
|
||||
|
||||
it("returns null when invalid or zero", () => {
|
||||
expect(
|
||||
resolveHeartbeatIntervalMs({ agent: { heartbeat: { every: "0m" } } }),
|
||||
).toBeNull();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
||||
import {
|
||||
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
||||
HEARTBEAT_PROMPT,
|
||||
DEFAULT_HEARTBEAT_EVERY,
|
||||
resolveHeartbeatPrompt as resolveHeartbeatPromptText,
|
||||
stripHeartbeatToken,
|
||||
} from "../auto-reply/heartbeat.js";
|
||||
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
||||
@@ -83,7 +84,8 @@ export function resolveHeartbeatIntervalMs(
|
||||
cfg: ClawdbotConfig,
|
||||
overrideEvery?: string,
|
||||
) {
|
||||
const raw = overrideEvery ?? cfg.agent?.heartbeat?.every;
|
||||
const raw =
|
||||
overrideEvery ?? cfg.agent?.heartbeat?.every ?? DEFAULT_HEARTBEAT_EVERY;
|
||||
if (!raw) return null;
|
||||
const trimmed = String(raw).trim();
|
||||
if (!trimmed) return null;
|
||||
@@ -98,9 +100,7 @@ export function resolveHeartbeatIntervalMs(
|
||||
}
|
||||
|
||||
export function resolveHeartbeatPrompt(cfg: ClawdbotConfig) {
|
||||
const raw = cfg.agent?.heartbeat?.prompt;
|
||||
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
||||
return trimmed || HEARTBEAT_PROMPT;
|
||||
return resolveHeartbeatPromptText(cfg.agent?.heartbeat?.prompt);
|
||||
}
|
||||
|
||||
function resolveHeartbeatAckMaxChars(cfg: ClawdbotConfig) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import {
|
||||
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
||||
HEARTBEAT_PROMPT,
|
||||
resolveHeartbeatPrompt,
|
||||
stripHeartbeatToken,
|
||||
} from "../auto-reply/heartbeat.js";
|
||||
import { dispatchReplyFromConfig } from "../auto-reply/reply/dispatch-from-config.js";
|
||||
@@ -339,7 +340,7 @@ export async function runWebHeartbeatOnce(opts: {
|
||||
|
||||
const replyResult = await replyResolver(
|
||||
{
|
||||
Body: HEARTBEAT_PROMPT,
|
||||
Body: resolveHeartbeatPrompt(cfg.agent?.heartbeat?.prompt),
|
||||
From: to,
|
||||
To: to,
|
||||
MessageSid: sessionId ?? sessionSnapshot.entry?.sessionId,
|
||||
|
||||
Reference in New Issue
Block a user