feat(FN-4400): complete Step 3 — add heartbeat prompt template resolver

Fusion-Task-Id: FN-4400
Fusion-Task-Lineage: 0d4f9c48-5b8b-49eb-9cf3-d1d585ffe7fc
This commit is contained in:
Fusion
2026-05-14 03:39:21 -07:00
committed by gsxdsm
parent ae150d169d
commit a47904d968
2 changed files with 71 additions and 2 deletions

View File

@@ -1,9 +1,18 @@
import type { Agent, ProjectSettings } from "@fusion/core";
import { describe, expect, it } from "vitest";
import { resolveHeartbeatScopeDisciplineMode, selectHeartbeatProcedure } from "../heartbeat-procedure-resolver.js";
import {
resolveHeartbeatPromptTemplate,
resolveHeartbeatScopeDisciplineMode,
selectHeartbeatProcedure,
} from "../heartbeat-procedure-resolver.js";
const project = (mode?: ProjectSettings["heartbeatScopeDiscipline"]) => ({ heartbeatScopeDiscipline: mode });
const agent = (mode?: unknown) => ({ runtimeConfig: mode === undefined ? {} : { heartbeatScopeDiscipline: mode } }) as Pick<Agent, "runtimeConfig">;
const projectTemplate = (template?: ProjectSettings["heartbeatPromptTemplate"]) => ({ heartbeatPromptTemplate: template });
const templateAgent = (role: Agent["role"], template?: unknown) => ({
role,
runtimeConfig: template === undefined ? {} : { heartbeatPromptTemplate: template },
}) as Pick<Agent, "runtimeConfig" | "role">;
describe("resolveHeartbeatScopeDisciplineMode", () => {
it.each(["strict", "lite", "off"] as const)("prefers agent override: %s", (mode) => {
@@ -31,6 +40,39 @@ describe("resolveHeartbeatScopeDisciplineMode", () => {
});
});
describe("resolveHeartbeatPromptTemplate", () => {
it("prefers agent override over project and role defaults", () => {
expect(resolveHeartbeatPromptTemplate(projectTemplate("default"), templateAgent("triage", "compact"))).toBe("compact");
});
it("uses project setting when agent override is unset", () => {
expect(resolveHeartbeatPromptTemplate(projectTemplate("compact"), templateAgent("executor"))).toBe("compact");
});
it.each(["triage", "reviewer", "merger", "scheduler", "engineer", "custom"] as const)(
"defaults non-executor role %s to compact",
(role) => {
expect(resolveHeartbeatPromptTemplate(undefined, templateAgent(role))).toBe("compact");
},
);
it("defaults executor role to default", () => {
expect(resolveHeartbeatPromptTemplate(undefined, templateAgent("executor"))).toBe("default");
});
it("falls through invalid agent template to project", () => {
expect(resolveHeartbeatPromptTemplate(projectTemplate("compact"), templateAgent("executor", "invalid"))).toBe("compact");
});
it("falls through invalid project template to role default", () => {
expect(resolveHeartbeatPromptTemplate(projectTemplate("tiny" as never), templateAgent("triage"))).toBe("compact");
});
it("returns default when agent is undefined", () => {
expect(resolveHeartbeatPromptTemplate(projectTemplate("tiny" as never), undefined)).toBe("default");
});
});
describe("selectHeartbeatProcedure", () => {
const procedures = {
task: { strict: "task-strict", lite: "task-lite", off: "task-off" },

View File

@@ -1,11 +1,16 @@
import type { Agent, HeartbeatScopeDisciplineMode, ProjectSettings } from "@fusion/core";
import type { Agent, HeartbeatPromptTemplate, HeartbeatScopeDisciplineMode, ProjectSettings } from "@fusion/core";
const VALID_MODES: readonly HeartbeatScopeDisciplineMode[] = ["strict", "lite", "off"] as const;
const VALID_TEMPLATES: readonly HeartbeatPromptTemplate[] = ["default", "compact"] as const;
function isHeartbeatScopeDisciplineMode(value: unknown): value is HeartbeatScopeDisciplineMode {
return typeof value === "string" && (VALID_MODES as readonly string[]).includes(value);
}
function isHeartbeatPromptTemplate(value: unknown): value is HeartbeatPromptTemplate {
return typeof value === "string" && (VALID_TEMPLATES as readonly string[]).includes(value);
}
export function resolveHeartbeatScopeDisciplineMode(
projectSettings: Pick<ProjectSettings, "heartbeatScopeDiscipline"> | undefined,
agent: Pick<Agent, "runtimeConfig"> | undefined,
@@ -24,6 +29,28 @@ export function resolveHeartbeatScopeDisciplineMode(
return "strict";
}
export function resolveHeartbeatPromptTemplate(
projectSettings: Pick<ProjectSettings, "heartbeatPromptTemplate"> | undefined,
agent: Pick<Agent, "runtimeConfig" | "role"> | undefined,
): HeartbeatPromptTemplate {
const runtimeConfig = agent?.runtimeConfig as Record<string, unknown> | undefined;
const agentTemplate = runtimeConfig?.heartbeatPromptTemplate;
if (isHeartbeatPromptTemplate(agentTemplate)) {
return agentTemplate;
}
const projectTemplate = projectSettings?.heartbeatPromptTemplate;
if (isHeartbeatPromptTemplate(projectTemplate)) {
return projectTemplate;
}
if (!agent) {
return "default";
}
return agent.role === "executor" ? "default" : "compact";
}
export function selectHeartbeatProcedure<T>(
mode: HeartbeatScopeDisciplineMode,
isNoTaskRun: boolean,