Agent: guard reminder promises behind cron scheduling

This commit is contained in:
Vignesh Natarajan
2026-02-16 13:34:09 -08:00
committed by Vignesh
parent 0cff8bc4e6
commit 5a26d1c622
10 changed files with 222 additions and 7 deletions

View File

@@ -853,6 +853,93 @@ describe("runReplyAgent messaging tool suppression", () => {
});
});
describe("runReplyAgent reminder commitment guard", () => {
function createRun() {
const typing = createMockTypingController();
const sessionCtx = {
Provider: "telegram",
OriginatingTo: "chat",
AccountId: "primary",
MessageSid: "msg",
Surface: "telegram",
} as unknown as TemplateContext;
const resolvedQueue = { mode: "interrupt" } as unknown as QueueSettings;
const followupRun = {
prompt: "hello",
summaryLine: "hello",
enqueuedAt: Date.now(),
run: {
sessionId: "session",
sessionKey: "main",
messageProvider: "telegram",
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp",
config: {},
skillsSnapshot: {},
provider: "anthropic",
model: "claude",
thinkLevel: "low",
verboseLevel: "off",
elevatedLevel: "off",
bashElevated: {
enabled: false,
allowed: false,
defaultLevel: "off",
},
timeoutMs: 1_000,
blockReplyBreak: "message_end",
},
} as unknown as FollowupRun;
return runReplyAgent({
commandBody: "hello",
followupRun,
queueKey: "main",
resolvedQueue,
shouldSteer: false,
shouldFollowup: false,
isActive: false,
isStreaming: false,
typing,
sessionCtx,
sessionKey: "main",
defaultModel: "anthropic/claude-opus-4-5",
resolvedVerboseLevel: "off",
isNewSession: false,
blockStreamingEnabled: false,
resolvedBlockStreamingBreak: "message_end",
shouldInjectGroupIntro: false,
typingMode: "instant",
});
}
it("appends guard note when reminder commitment is not backed by cron.add", async () => {
runEmbeddedPiAgentMock.mockResolvedValueOnce({
payloads: [{ text: "I'll remind you tomorrow morning." }],
meta: {},
successfulCronAdds: 0,
});
const result = await createRun();
expect(result).toMatchObject({
text: "I'll remind you tomorrow morning.\n\nNote: I did not schedule a reminder in this turn, so this will not trigger automatically.",
});
});
it("keeps reminder commitment unchanged when cron.add succeeded", async () => {
runEmbeddedPiAgentMock.mockResolvedValueOnce({
payloads: [{ text: "I'll remind you tomorrow morning." }],
meta: {},
successfulCronAdds: 1,
});
const result = await createRun();
expect(result).toMatchObject({
text: "I'll remind you tomorrow morning.",
});
});
});
describe("runReplyAgent fallback reasoning tags", () => {
type EmbeddedPiAgentParams = {
enforceFinalTag?: boolean;

View File

@@ -42,6 +42,41 @@ import { incrementRunCompactionCount, persistRunSessionUsage } from "./session-r
import { createTypingSignaler } from "./typing-mode.js";
const BLOCK_REPLY_SEND_TIMEOUT_MS = 15_000;
const UNSCHEDULED_REMINDER_NOTE =
"Note: I did not schedule a reminder in this turn, so this will not trigger automatically.";
const REMINDER_COMMITMENT_PATTERNS: RegExp[] = [
/\b(?:i\s*[']?ll|i will)\s+(?:make sure to\s+)?(?:remember|remind|ping|follow up|follow-up|check back|circle back)\b/i,
/\b(?:i\s*[']?ll|i will)\s+(?:set|create|schedule)\s+(?:a\s+)?reminder\b/i,
];
function hasUnbackedReminderCommitment(text: string): boolean {
const normalized = text.toLowerCase();
if (!normalized.trim()) {
return false;
}
if (normalized.includes(UNSCHEDULED_REMINDER_NOTE.toLowerCase())) {
return false;
}
return REMINDER_COMMITMENT_PATTERNS.some((pattern) => pattern.test(text));
}
function appendUnscheduledReminderNote(payloads: ReplyPayload[]): ReplyPayload[] {
let appended = false;
return payloads.map((payload) => {
if (appended || payload.isError || typeof payload.text !== "string") {
return payload;
}
if (!hasUnbackedReminderCommitment(payload.text)) {
return payload;
}
appended = true;
const trimmed = payload.text.trimEnd();
return {
...payload,
text: `${trimmed}\n\n${UNSCHEDULED_REMINDER_NOTE}`,
};
});
}
export async function runReplyAgent(params: {
commandBody: string;
@@ -420,7 +455,19 @@ export async function runReplyAgent(params: {
return finalizeWithFollowup(undefined, queueKey, runFollowupTurn);
}
await signalTypingIfNeeded(replyPayloads, typingSignals);
const successfulCronAdds = runResult.successfulCronAdds ?? 0;
const hasReminderCommitment = replyPayloads.some(
(payload) =>
!payload.isError &&
typeof payload.text === "string" &&
hasUnbackedReminderCommitment(payload.text),
);
const guardedReplyPayloads =
hasReminderCommitment && successfulCronAdds === 0
? appendUnscheduledReminderNote(replyPayloads)
: replyPayloads;
await signalTypingIfNeeded(guardedReplyPayloads, typingSignals);
if (isDiagnosticsEnabled(cfg) && hasNonzeroUsage(usage)) {
const input = usage.input ?? 0;
@@ -488,7 +535,7 @@ export async function runReplyAgent(params: {
}
// If verbose is enabled and this is a new session, prepend a session hint.
let finalPayloads = replyPayloads;
let finalPayloads = guardedReplyPayloads;
const verboseEnabled = resolvedVerboseLevel !== "off";
if (autoCompactionCompleted) {
const count = await incrementRunCompactionCount({