FN-5775: persist AI merge agent output in merger logs

Ensure AI merger runs persist agent output signals so merge activity is visible in task logs.

- Persist merger agent text, thinking, tool start, and tool result output via AgentLogger in AI merge/review sessions.
- Add regression coverage validating persisted merger agent logs and prompt/commit guidance behavior.
- Add a patch changeset for @runfusion/fusion describing AI merge log persistence.

Files changed:
 .changeset/fn-5775-ai-merge-logs.md             |  5 ++
 packages/engine/src/__tests__/merger-ai.test.ts | 62 +++++++++++++++++++++++++
 packages/engine/src/merger-ai.ts                | 40 +++++++++++++++-
 3 files changed, 105 insertions(+), 2 deletions(-)

Fusion-Task-Id: FN-5775

Fusion-Task-Lineage: 7e1b803f-135a-412e-81d9-bad0aa655855
This commit is contained in:
gsxdsm
2026-05-31 08:50:23 -07:00
parent b154844286
commit 93e8a5f78a
3 changed files with 105 additions and 2 deletions

View File

@@ -3,6 +3,25 @@ import { mkdtempSync, rmSync, writeFileSync, readFileSync, existsSync } from "no
import { join } from "node:path";
import { tmpdir } from "node:os";
import { execSync } from "node:child_process";
const createResolvedAgentSessionMock = vi.hoisted(() => vi.fn());
vi.mock("../agent-session-helpers.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../agent-session-helpers.js")>();
return {
...actual,
createResolvedAgentSession: createResolvedAgentSessionMock,
};
});
vi.mock("../pi.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../pi.js")>();
return {
...actual,
promptWithFallback: vi.fn(async (session: { prompt: (prompt: string) => Promise<void> | void }, prompt: string) => {
await session.prompt(prompt);
}),
};
});
import {
runAiMerge,
landSquash,
@@ -179,6 +198,49 @@ describe("runAiMerge", () => {
expect(emitted.some((e) => e.event === "task:merged")).toBe(true);
});
it("persists AI merge agent text/thinking/tool output to agent logs", async () => {
const { dir } = initRepoWithBranch({ branch: "fusion/fn-1" });
const { store } = makeStore(dir, {}, { persistAgentToolOutput: true, persistAgentThinkingLog: true });
createResolvedAgentSessionMock.mockImplementation(async (opts: any) => {
const isReview = String(opts.systemPrompt ?? "").includes(REVIEW_VERDICT_MARKER);
const session = {
async prompt(prompt: string) {
opts.onThinking?.("thinking-delta");
opts.onToolStart?.("read", { path: "feature.txt" });
opts.onToolEnd?.("read", false, "feature work");
opts.onText?.(isReview ? "REVIEW_VERDICT: approve" : "merge-agent-output");
if (!isReview) {
try {
execSync("git merge --squash fusion/fn-1", { cwd: opts.cwd, stdio: "pipe" });
} catch {
execSync("git checkout --theirs . || true", { cwd: opts.cwd, stdio: "pipe", shell: "/bin/bash" } as any);
execSync("git add -A", { cwd: opts.cwd, stdio: "pipe" });
}
execSync("git add -A", { cwd: opts.cwd, stdio: "pipe" });
execSync('git commit -q -m "squash: feature"', { cwd: opts.cwd, stdio: "pipe" });
}
},
dispose: vi.fn(),
getSessionStats: vi.fn(() => ({ tokens: { input: 1, output: 1 } })),
};
return { session };
});
await runAiMerge(store, dir, "FN-1", { manual: true });
const mergerLogCalls = store.appendAgentLog.mock.calls.filter(
([id, _text, _type, _detail, agent]: [string, string, string, string | undefined, string | undefined]) =>
id === "FN-1" && agent === "merger",
);
expect(mergerLogCalls.some(([, text, type]: [string, string, string]) => type === "tool" && text === "read")).toBe(true);
expect(mergerLogCalls.some(([, text, type]: [string, string, string]) => type === "tool_result" && text === "read")).toBe(true);
expect(mergerLogCalls.some(([, _text, type]: [string, string, string]) => type === "thinking")).toBe(true);
expect(mergerLogCalls.some(([, _text, type]: [string, string, string]) => type === "text")).toBe(true);
createResolvedAgentSessionMock.mockReset();
});
it("includes the lineage trailer when the task has a lineageId", async () => {
const { dir } = initRepoWithBranch({ branch: "fusion/fn-1" });
const { store } = makeStore(dir, { lineageId: "lin-abc123" });