fix(agents): stop injecting heartbeat system prompt on non-heartbeat runs (#69079)

This commit is contained in:
stainlu
2026-04-20 16:59:30 +08:00
committed by Ayaan Zaidi
parent 151befb90b
commit 06c058b21d
2 changed files with 43 additions and 3 deletions

View File

@@ -0,0 +1,32 @@
import { describe, expect, it } from "vitest";
import { shouldInjectHeartbeatPromptForTrigger } from "./trigger-policy.js";
describe("shouldInjectHeartbeatPromptForTrigger", () => {
it("injects the heartbeat prompt on heartbeat-triggered runs", () => {
expect(shouldInjectHeartbeatPromptForTrigger("heartbeat")).toBe(true);
});
// Regression: the heartbeat system prompt instructs the model to reply
// exactly "HEARTBEAT_OK" when nothing is pending. If that prompt leaks into
// a user-triggered turn, the model can pattern-match the literal HEARTBEAT_OK
// token (which the delivery runtime then suppresses, so the user sees
// silence) or hallucinate a "[object Object]" serialization error as it
// tries to reconcile the heartbeat instruction with a real user message.
// See issue #69079 and its parent #50797.
it.each([
["user"] as const,
["manual"] as const,
["cron"] as const,
["memory"] as const,
["overflow"] as const,
])("does not inject the heartbeat prompt on %s-triggered runs", (trigger) => {
expect(shouldInjectHeartbeatPromptForTrigger(trigger)).toBe(false);
});
it("does not inject the heartbeat prompt when no trigger is supplied", () => {
// Defense-in-depth: if a new call site lands without a trigger, it should
// fall through to the safe default rather than spuriously injecting
// heartbeat instructions.
expect(shouldInjectHeartbeatPromptForTrigger(undefined)).toBe(false);
});
});

View File

@@ -4,13 +4,21 @@ type EmbeddedRunTriggerPolicy = {
injectHeartbeatPrompt: boolean;
};
// The heartbeat system prompt tells the model to reply exactly "HEARTBEAT_OK"
// when nothing needs attention. It is only meaningful on heartbeat-triggered
// runs; injecting it on user/manual/memory/overflow runs confuses smaller
// models into pattern-matching the HEARTBEAT_OK output on real user messages
// (delivery then suppresses the "reply", so the user sees silence) or into
// fabricating "[object Object]" serialization errors as they try to reconcile
// the heartbeat instruction with a non-heartbeat turn. See issue #69079 and
// its parent #50797. Default off; heartbeat trigger explicitly opts in.
const DEFAULT_EMBEDDED_RUN_TRIGGER_POLICY: EmbeddedRunTriggerPolicy = {
injectHeartbeatPrompt: true,
injectHeartbeatPrompt: false,
};
const EMBEDDED_RUN_TRIGGER_POLICY: Partial<Record<EmbeddedRunTrigger, EmbeddedRunTriggerPolicy>> = {
cron: {
injectHeartbeatPrompt: false,
heartbeat: {
injectHeartbeatPrompt: true,
},
};