feat(FN-4056): restore bound reports lookup in heartbeat executor
Restores the bound reports lookup in the heartbeat executor (FN-4056), updating the relevant logic in `agent-heartbeat.ts` with a corresponding test and documentation clarification in `agents.md`. Fusion-Task-Id: FN-4056
This commit is contained in:
@@ -761,6 +761,8 @@ The shipped default `HEARTBEAT_PROCEDURE` (in `packages/engine/src/agent-heartbe
|
||||
|
||||
When the bound task is `executor-class` or `blocked`, the default procedure directs the run to pivot toward coordination levers (in-progress risk scan, stale in-review queue, idle direct reports, strategic memory themes) rather than trying to advance implementation from heartbeat. When the task is `coordination-class`, the heartbeat can engage directly with the bound task.
|
||||
|
||||
The manager-facing reports health block in that prompt is populated from `AgentStore.getAgentsByReportsTo(agent.id)`. Engine code must call that store method with its `AgentStore` instance binding intact because some implementations resolve direct reports through `this.listAgents()`. If the section disappears unexpectedly, look for logs like `Failed to load reports ... Cannot read properties of undefined (reading 'listAgents')`, which indicate an unbound method call regressed.
|
||||
|
||||
This behavior is inherited by new non-ephemeral agents because agent creation seeds a per-agent `HEARTBEAT.md` file from the built-in default. If an agent sets `heartbeatProcedurePath`, that markdown file fully replaces the built-in default at runtime.
|
||||
|
||||
For pre-existing agents, use `POST /api/agents/:id/upgrade-heartbeat-procedure` (also exposed as **Upgrade to Default Heartbeat Procedure** in the agent detail Config tab) to re-seed from the current built-in constant. When the built-in default changes, running this upgrade propagates the new default to existing agents; direct operator edits to an agent’s existing procedure file are preserved unless this upgrade is run (the upgrade overwrites the per-agent file).
|
||||
|
||||
@@ -221,6 +221,25 @@ describe("executeHeartbeat", () => {
|
||||
expect(section).toContain("**stale**");
|
||||
});
|
||||
|
||||
it("buildReportsHealthSection preserves AgentStore method binding for direct-report lookups", async () => {
|
||||
const now = new Date().toISOString();
|
||||
const report = { id: "agent-004", name: "bound-report", state: "active", taskId: "FN-102", reportsTo: "agent-001", lastHeartbeatAt: now, updatedAt: now } as Agent;
|
||||
const store = createStoreWithAgentForExec() as AgentStore & {
|
||||
listAgents: ReturnType<typeof vi.fn>;
|
||||
getAgentsByReportsTo: (agentId: string) => Promise<Agent[]>;
|
||||
};
|
||||
store.listAgents = vi.fn().mockResolvedValue([mockAgent, report]);
|
||||
store.getAgentsByReportsTo = async function (agentId: string) {
|
||||
const agents = await this.listAgents();
|
||||
return agents.filter((candidate: Agent) => candidate.reportsTo === agentId);
|
||||
};
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
|
||||
const section = await (monitor as any).buildReportsHealthSection("agent-001", store);
|
||||
expect(section).toContain("bound-report");
|
||||
expect(store.listAgents).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("executeHeartbeat includes reports health section when agent has reports", async () => {
|
||||
const store = createStoreWithAgentForExec({ taskId: "FN-001" });
|
||||
const now = new Date().toISOString();
|
||||
|
||||
@@ -2430,14 +2430,14 @@ export class HeartbeatMonitor {
|
||||
}
|
||||
|
||||
private async buildReportsHealthSection(agentId: string, agentStore: AgentStore): Promise<string | null> {
|
||||
const getReports = (agentStore as AgentStore & { getAgentsByReportsTo?: (id: string) => Promise<Agent[]> }).getAgentsByReportsTo;
|
||||
if (typeof getReports !== "function") {
|
||||
const storeWithReports = agentStore as AgentStore & { getAgentsByReportsTo?: (id: string) => Promise<Agent[]> };
|
||||
if (typeof storeWithReports.getAgentsByReportsTo !== "function") {
|
||||
return null;
|
||||
}
|
||||
|
||||
let reports: Agent[];
|
||||
try {
|
||||
reports = await getReports(agentId);
|
||||
reports = await storeWithReports.getAgentsByReportsTo(agentId);
|
||||
} catch (err) {
|
||||
heartbeatLog.warn(`Failed to load reports for ${agentId}: ${err instanceof Error ? err.message : String(err)}`);
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user