fix: inject group activation in system prompt

This commit is contained in:
Peter Steinberger
2025-12-23 13:32:07 +00:00
parent 96d57a18ee
commit cba12a1abd
7 changed files with 81 additions and 7 deletions

View File

@@ -267,6 +267,7 @@ export async function runEmbeddedPiAgent(params: {
data: Record<string, unknown>;
}) => void;
enqueue?: typeof enqueueCommand;
extraSystemPrompt?: string;
}): Promise<EmbeddedPiRunResult> {
const enqueue = params.enqueue ?? enqueueCommand;
return enqueue(async () => {
@@ -326,6 +327,7 @@ export async function runEmbeddedPiAgent(params: {
appendPrompt: buildAgentSystemPromptAppend({
workspaceDir: resolvedWorkspace,
defaultThinkLevel: params.thinkLevel,
extraSystemPrompt: params.extraSystemPrompt,
}),
contextFiles,
skills: promptSkills,

View File

@@ -3,13 +3,16 @@ import type { ThinkLevel } from "../auto-reply/thinking.js";
export function buildAgentSystemPromptAppend(params: {
workspaceDir: string;
defaultThinkLevel?: ThinkLevel;
extraSystemPrompt?: string;
}) {
const thinkHint =
params.defaultThinkLevel && params.defaultThinkLevel !== "off"
? `Default thinking level: ${params.defaultThinkLevel}.`
: "Default thinking level: off.";
return [
const extraSystemPrompt = params.extraSystemPrompt?.trim();
const lines = [
"You are Clawd, a personal assistant running inside Clawdis.",
"",
"## Tooling",
@@ -35,6 +38,13 @@ export function buildAgentSystemPromptAppend(params: {
"Never send streaming/partial replies to external messaging surfaces; only final replies should be delivered there.",
"Clawdis handles message transport automatically; respond normally and your reply will be delivered to the current chat.",
"",
];
if (extraSystemPrompt) {
lines.push("## Group Chat Context", extraSystemPrompt, "");
}
lines.push(
"## Heartbeats",
'If you receive a heartbeat poll (a user message containing just "HEARTBEAT"), and there is nothing that needs attention, reply exactly:',
"HEARTBEAT_OK",
@@ -42,7 +52,9 @@ export function buildAgentSystemPromptAppend(params: {
"",
"## Runtime",
thinkHint,
]
);
return lines
.filter(Boolean)
.join("\n");
}

View File

@@ -142,6 +142,49 @@ describe("trigger handling", () => {
});
});
it("injects group activation context into the system prompt", async () => {
await withTempHome(async (home) => {
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
payloads: [{ text: "ok" }],
meta: {
durationMs: 1,
agentMeta: { sessionId: "s", provider: "p", model: "m" },
},
});
const res = await getReplyFromConfig(
{
Body: "hello group",
From: "123@g.us",
To: "+2000",
ChatType: "group",
SenderE164: "+2000",
GroupSubject: "Test Group",
GroupMembers: "Alice (+1), Bob (+2)",
},
{},
{
inbound: {
allowFrom: ["*"],
workspace: join(home, "clawd"),
agent: { provider: "anthropic", model: "claude-opus-4-5" },
session: { store: join(home, "sessions.json") },
groupChat: { requireMention: false },
},
},
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toBe("ok");
expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
const extra =
vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.extraSystemPrompt ??
"";
expect(extra).toContain("Test Group");
expect(extra).toContain("Activation: always-on");
});
});
it("runs a greeting prompt for a bare /new", async () => {
await withTempHome(async (home) => {
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({

View File

@@ -563,6 +563,7 @@ export async function getReplyFromConfig(
}
if (sessionEntry && sessionStore && sessionKey) {
sessionEntry.groupActivation = activationCommand.mode;
sessionEntry.groupActivationNeedsSystemIntro = true;
sessionEntry.updatedAt = Date.now();
sessionStore[sessionKey] = sessionEntry;
await saveSessionStore(storePath, sessionStore);
@@ -648,8 +649,11 @@ export async function getReplyFromConfig(
await startTypingLoop();
const isFirstTurnInSession = isNewSession || !systemSent;
const shouldInjectGroupIntro =
sessionCtx.ChatType === "group" &&
(isFirstTurnInSession || sessionEntry?.groupActivationNeedsSystemIntro);
const groupIntro =
isFirstTurnInSession && sessionCtx.ChatType === "group"
shouldInjectGroupIntro
? (() => {
const activation =
normalizeGroupActivation(sessionEntry?.groupActivation) ??
@@ -713,9 +717,6 @@ export async function getReplyFromConfig(
? "Note: The previous agent run was aborted by the user. Resume carefully or ask for clarification."
: "";
let prefixedBodyBase = baseBodyFinal;
if (groupIntro) {
prefixedBodyBase = `${groupIntro}\n\n${prefixedBodyBase}`;
}
if (abortedHint) {
prefixedBodyBase = `${abortedHint}\n\n${prefixedBodyBase}`;
if (sessionEntry && sessionStore && sessionKey) {
@@ -875,6 +876,7 @@ export async function getReplyFromConfig(
config: cfg,
skillsSnapshot,
prompt: commandBody,
extraSystemPrompt: groupIntro || undefined,
provider,
model,
thinkLevel: resolvedThinkLevel,
@@ -898,6 +900,19 @@ export async function getReplyFromConfig(
: undefined,
});
if (
shouldInjectGroupIntro &&
sessionEntry &&
sessionStore &&
sessionKey &&
sessionEntry.groupActivationNeedsSystemIntro
) {
sessionEntry.groupActivationNeedsSystemIntro = false;
sessionEntry.updatedAt = Date.now();
sessionStore[sessionKey] = sessionEntry;
await saveSessionStore(storePath, sessionStore);
}
const payloadArray = runResult.payloads ?? [];
if (payloadArray.length === 0) return undefined;

View File

@@ -18,6 +18,7 @@ export type SessionEntry = {
thinkingLevel?: string;
verboseLevel?: string;
groupActivation?: "mention" | "always";
groupActivationNeedsSystemIntro?: boolean;
inputTokens?: number;
outputTokens?: number;
totalTokens?: number;