Skip to content

Commit 98c7ac0

Browse files
committed
fix(agents): preserve heartbeat hook ordering
1 parent d08e333 commit 98c7ac0

2 files changed

Lines changed: 60 additions & 20 deletions

File tree

src/agents/harness/prompt-compaction-hook-helpers.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,47 @@ describe("resolveAgentHarnessBeforePromptBuildResult", () => {
9090
expect(result.developerInstructions).toBe("base instructions");
9191
});
9292

93+
it("runs heartbeat contributions before other prompt-build hooks", async () => {
94+
const calls: string[] = [];
95+
initializeGlobalHookRunner(
96+
createMockPluginRegistry([
97+
{
98+
hookName: "heartbeat_prompt_contribution",
99+
handler: () => {
100+
calls.push("heartbeat");
101+
return { prependContext: "heartbeat context" };
102+
},
103+
},
104+
{
105+
hookName: "before_prompt_build",
106+
handler: () => {
107+
calls.push("before_prompt_build");
108+
return { prependContext: "prompt context" };
109+
},
110+
},
111+
{
112+
hookName: "before_agent_start",
113+
handler: () => {
114+
calls.push("before_agent_start");
115+
return { prependContext: "agent-start context" };
116+
},
117+
},
118+
]),
119+
);
120+
121+
const result = await resolveAgentHarnessBeforePromptBuildResult({
122+
prompt: "hello",
123+
developerInstructions: "base instructions",
124+
messages: [],
125+
ctx: { trigger: "heartbeat", agentId: "agent-1", sessionKey: "session-1" },
126+
});
127+
128+
expect(calls).toEqual(["heartbeat", "before_prompt_build", "before_agent_start"]);
129+
expect(result.prompt).toBe(
130+
"heartbeat context\n\nprompt context\n\nagent-start context\n\nhello",
131+
);
132+
});
133+
93134
it("skips heartbeat_prompt_contribution off a heartbeat turn", async () => {
94135
const handler = vi.fn(() => ({ prependContext: "should not appear" }));
95136
initializeGlobalHookRunner(

src/agents/harness/prompt-compaction-hook-helpers.ts

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,25 @@ export async function resolveAgentHarnessBeforePromptBuildResult(params: {
6060
messages: params.messages,
6161
};
6262

63+
// Match the embedded runner's lifecycle order: heartbeat contributions are
64+
// collected before prompt-build hooks so hook side effects stay deterministic.
65+
const heartbeatResult =
66+
hasHeartbeatContribution && hookRunner
67+
? await hookRunner
68+
.runHeartbeatPromptContribution(
69+
{
70+
sessionKey: params.ctx.sessionKey,
71+
agentId: params.ctx.agentId,
72+
heartbeatName: "heartbeat",
73+
},
74+
hookCtx,
75+
)
76+
.catch((error: unknown) => {
77+
log.warn(`heartbeat_prompt_contribution hook failed: ${String(error)}`);
78+
return undefined;
79+
})
80+
: undefined;
81+
6382
// Support the newer before_prompt_build hook plus the deprecated
6483
// before_agent_start hook during the prompt-build migration window.
6584
const promptBuildResult = hookRunner?.hasHooks("before_prompt_build")
@@ -81,26 +100,6 @@ export async function resolveAgentHarnessBeforePromptBuildResult(params: {
81100
})
82101
: undefined;
83102

84-
// heartbeat_prompt_contribution runs only on heartbeat turns. Mirrors the
85-
// embedded runner (resolvePromptBuildHookResult): the contribution is merged
86-
// ahead of the before_prompt_build / before_agent_start contributions.
87-
const heartbeatResult =
88-
hasHeartbeatContribution && hookRunner
89-
? await hookRunner
90-
.runHeartbeatPromptContribution(
91-
{
92-
sessionKey: params.ctx.sessionKey,
93-
agentId: params.ctx.agentId,
94-
heartbeatName: "heartbeat",
95-
},
96-
hookCtx,
97-
)
98-
.catch((error: unknown) => {
99-
log.warn(`heartbeat_prompt_contribution hook failed: ${String(error)}`);
100-
return undefined;
101-
})
102-
: undefined;
103-
104103
const systemPrompt = resolvePromptBuildSystemPrompt({
105104
developerInstructions: params.developerInstructions,
106105
promptBuildResult,

0 commit comments

Comments
 (0)