feat(FN-4062): add global thinking log setting to persist agent reasoning a

Adds a global `thinkingLogEnabled` setting that gates AI thinking log persistence across the engine (executor, reviewer, merger, triage, step-session) and exposes the control in the dashboard Settings modal, with tests verifying settings parity and modal behavior.

Fusion-Task-Id: FN-4062
This commit is contained in:
Fusion
2026-05-11 21:09:28 -07:00
committed by gsxdsm
parent 03b8bdb323
commit 840cd1d1e0
16 changed files with 152 additions and 4 deletions

View File

@@ -269,7 +269,7 @@ describe("AgentLogger", () => {
// ── Thinking buffer/flush ────────────────────────────────────────
it("buffers thinking deltas and flushes on timer", async () => {
it("skips thinking entries by default", async () => {
const store = createMockStore();
const logger = new AgentLogger({
store,
@@ -279,12 +279,30 @@ describe("AgentLogger", () => {
flushIntervalMs: 500,
});
logger.onThinking("thought 1 ");
logger.onThinking("thought 2");
await vi.advanceTimersByTimeAsync(500);
expect(store.appendAgentLog).not.toHaveBeenCalled();
});
it("buffers thinking deltas and flushes on timer when enabled", async () => {
const store = createMockStore();
const logger = new AgentLogger({
store,
taskId: "FN-011A",
agent: "executor",
persistAgentThinkingLog: true,
flushSizeBytes: 1024,
flushIntervalMs: 500,
});
logger.onThinking("thought 1 ");
logger.onThinking("thought 2");
expect(store.appendAgentLog).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(500);
expect(store.appendAgentLog).toHaveBeenCalledWith("FN-011", "thought 1 thought 2", "thinking", undefined, "executor");
expect(store.appendAgentLog).toHaveBeenCalledWith("FN-011A", "thought 1 thought 2", "thinking", undefined, "executor");
});
it("flushes thinking on size threshold", async () => {
@@ -293,6 +311,7 @@ describe("AgentLogger", () => {
store,
taskId: "FN-012",
agent: "triage",
persistAgentThinkingLog: true,
flushSizeBytes: 10,
});
@@ -310,6 +329,7 @@ describe("AgentLogger", () => {
store,
taskId: "FN-013",
agent: "reviewer",
persistAgentThinkingLog: true,
flushSizeBytes: 1024,
});
@@ -324,6 +344,7 @@ describe("AgentLogger", () => {
store,
taskId: "FN-014",
agent: "executor",
persistAgentThinkingLog: true,
flushSizeBytes: 1024,
});
@@ -463,6 +484,7 @@ describe("AgentLogger", () => {
const logger = new AgentLogger({
store,
taskId: "FN-2090-THINKING",
persistAgentThinkingLog: true,
flushSizeBytes: 1,
});

View File

@@ -1890,6 +1890,7 @@ export class HeartbeatMonitor {
appendLog: (entry) => this.store.appendRunLog(agentId, run.id, entry),
agent: agent.role as AgentRole,
persistAgentToolOutput: memorySettings?.persistAgentToolOutput,
persistAgentThinkingLog: memorySettings?.persistAgentThinkingLog,
});
} else if (taskId) {
agentLogger = new AgentLogger({
@@ -1898,6 +1899,7 @@ export class HeartbeatMonitor {
agent: agent.role as AgentRole,
appendLog: (entry) => this.store.appendRunLog(agentId, run.id, entry),
persistAgentToolOutput: memorySettings?.persistAgentToolOutput,
persistAgentThinkingLog: memorySettings?.persistAgentThinkingLog,
});
}

View File

@@ -46,6 +46,8 @@ export function summarizeToolArgs(name: string, args?: Record<string, unknown>):
export interface AgentLoggerOptions {
/** When false, omit `detail` payloads for tool entries while preserving the rows. */
persistAgentToolOutput?: boolean;
/** When true, persist `thinking` rows. Default: false (skip thinking persistence). */
persistAgentThinkingLog?: boolean;
/** The task store used to persist agent log entries (task-store mode). */
store?: TaskStore;
/** The task ID this logger is associated with (task-store mode). */
@@ -110,6 +112,7 @@ export class AgentLogger {
private readonly externalToolCb?: (taskId: string, toolName: string) => void;
private readonly log = createLogger("agent-logger");
private readonly persistAgentToolOutput: boolean;
private readonly persistAgentThinkingLog: boolean;
constructor(options: AgentLoggerOptions) {
this.store = options.store;
@@ -121,6 +124,7 @@ export class AgentLogger {
this.flushSizeBytes = options.flushSizeBytes ?? FLUSH_SIZE_BYTES;
this.flushIntervalMs = options.flushIntervalMs ?? FLUSH_INTERVAL_MS;
this.persistAgentToolOutput = options.persistAgentToolOutput !== false;
this.persistAgentThinkingLog = options.persistAgentThinkingLog === true;
// Bind callbacks so they can be passed directly as function references
this.onText = this.onText.bind(this);
@@ -149,6 +153,9 @@ export class AgentLogger {
* as `type: "thinking"` entries, using the same size/timer pattern as `onText`.
*/
onThinking(delta: string): void {
if (!this.persistAgentThinkingLog) {
return;
}
this.thinkingBuffer += delta;
if (this.thinkingBuffer.length >= this.flushSizeBytes) {
if (this.thinkingFlushTimer) { clearTimeout(this.thinkingFlushTimer); this.thinkingFlushTimer = null; }
@@ -258,6 +265,9 @@ export class AgentLogger {
if (this.thinkingBuffer.length === 0) return Promise.resolve();
const chunk = this.thinkingBuffer;
this.thinkingBuffer = "";
if (!this.persistAgentThinkingLog) {
return Promise.resolve();
}
this.writeEntry(chunk, "thinking", undefined, `Failed to flush thinking buffer for ${this.taskId}`, true);
return this.flushPendingEntries();
}

View File

@@ -3152,6 +3152,7 @@ export class TaskExecutor {
taskId: task.id,
agent: "executor",
persistAgentToolOutput: settings.persistAgentToolOutput,
persistAgentThinkingLog: settings.persistAgentThinkingLog,
onAgentText: (taskId, delta) => {
lastAssistantText += delta;
stuckDetector?.recordActivity(taskId);
@@ -4960,6 +4961,7 @@ ${feedback}
taskId: task.id,
agent: "executor",
persistAgentToolOutput: settings.persistAgentToolOutput,
persistAgentThinkingLog: settings.persistAgentThinkingLog,
onAgentText: this.options.onAgentText,
onAgentTool: this.options.onAgentTool,
});
@@ -5769,6 +5771,7 @@ and show an appropriate message to the user.\`
taskId: task.id,
agent: "reviewer",
persistAgentToolOutput: settings.persistAgentToolOutput,
persistAgentThinkingLog: settings.persistAgentThinkingLog,
onAgentText: (taskId, delta) => {
this.options.onAgentText?.(taskId, delta);
},

View File

@@ -917,6 +917,7 @@ async function attemptInMergeVerificationFix(
taskId,
agent: "merger",
persistAgentToolOutput: settings.persistAgentToolOutput,
persistAgentThinkingLog: settings.persistAgentThinkingLog,
onAgentText: options.onAgentText,
onAgentTool: options.onAgentTool,
});
@@ -2026,6 +2027,7 @@ async function runAiAgentForAutostashConflict(params: {
taskId,
agent: "merger",
persistAgentToolOutput: settings.persistAgentToolOutput,
persistAgentThinkingLog: settings.persistAgentThinkingLog,
onAgentText: options.onAgentText
? (_id: string, delta: string) => options.onAgentText!(delta)
: undefined,
@@ -2395,6 +2397,7 @@ async function runAiAgentForAutostashHardFail(params: {
taskId,
agent: "merger",
persistAgentToolOutput: settings.persistAgentToolOutput,
persistAgentThinkingLog: settings.persistAgentThinkingLog,
onAgentText: options.onAgentText
? (_id: string, delta: string) => options.onAgentText!(delta)
: undefined,
@@ -4442,6 +4445,7 @@ You are assisting with a paused \`git pull --rebase\`.
taskId,
agent: "merger",
persistAgentToolOutput: settings.persistAgentToolOutput,
persistAgentThinkingLog: settings.persistAgentThinkingLog,
onAgentText: options?.onAgentText
? (_id, delta) => options.onAgentText?.(delta)
: undefined,
@@ -7132,6 +7136,7 @@ async function runAiAgentForCommit(params: AiAgentParams): Promise<{ success: bo
taskId,
agent: "merger",
persistAgentToolOutput: settings.persistAgentToolOutput,
persistAgentThinkingLog: settings.persistAgentThinkingLog,
onAgentText: options.onAgentText
? (_id, delta) => options.onAgentText!(delta)
: undefined,
@@ -7737,6 +7742,7 @@ If issues are found that need attention, describe them clearly and include concr
taskId,
agent: "merger",
persistAgentToolOutput: settings.persistAgentToolOutput,
persistAgentThinkingLog: settings.persistAgentThinkingLog,
});
try {

View File

@@ -351,6 +351,7 @@ export async function reviewStep(
? (_id, delta) => options.onText!(delta)
: undefined,
persistAgentToolOutput: liveSettings?.persistAgentToolOutput,
persistAgentThinkingLog: liveSettings?.persistAgentThinkingLog,
})
: null;

View File

@@ -890,6 +890,7 @@ export class StepSessionExecutor {
taskId: taskDetail.id,
agent: "executor",
persistAgentToolOutput: settings.persistAgentToolOutput,
persistAgentThinkingLog: settings.persistAgentThinkingLog,
});
let session: AgentSession | null = null;

View File

@@ -913,6 +913,7 @@ export class TriageProcessor {
taskId: task.id,
agent: "triage",
persistAgentToolOutput: settings.persistAgentToolOutput,
persistAgentThinkingLog: settings.persistAgentThinkingLog,
onAgentText: (id, delta) => {
stuckDetector?.recordActivity(task.id);
this.options.onAgentText?.(id, delta);