diff --git a/packages/engine/src/__tests__/persist-thinking-routing.test.ts b/packages/engine/src/__tests__/persist-thinking-routing.test.ts new file mode 100644 index 000000000..f99972e09 --- /dev/null +++ b/packages/engine/src/__tests__/persist-thinking-routing.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it, vi, beforeEach } from "vitest"; +import { isEphemeralAgent, resolvePersistAgentThinkingLog, type GlobalSettings } from "@fusion/core"; +import { AgentLogger } from "../agent-logger.js"; + +vi.mock("../agent-logger.js", () => ({ + AgentLogger: vi.fn(), +})); + +function createLoggerForAgent(agent: { metadata?: Record }, settings: Partial) { + const ephemeral = isEphemeralAgent(agent); + return new AgentLogger({ + persistAgentThinkingLog: resolvePersistAgentThinkingLog(settings, { ephemeral }), + }); +} + +describe("thinking persistence routing", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + const permanentAgent = { metadata: { type: "permanent" } }; + const ephemeralAgent = { metadata: { agentKind: "task-worker" } }; + + it.each([ + { + name: "both off", + settings: { persistAgentThinkingLogPermanent: false, persistAgentThinkingLogEphemeral: false }, + expectedPermanent: false, + expectedEphemeral: false, + }, + { + name: "permanent only", + settings: { persistAgentThinkingLogPermanent: true, persistAgentThinkingLogEphemeral: false }, + expectedPermanent: true, + expectedEphemeral: false, + }, + { + name: "ephemeral only", + settings: { persistAgentThinkingLogPermanent: false, persistAgentThinkingLogEphemeral: true }, + expectedPermanent: false, + expectedEphemeral: true, + }, + { + name: "legacy fallback", + settings: { persistAgentThinkingLog: true }, + expectedPermanent: true, + expectedEphemeral: true, + }, + ])("routes %s", ({ settings, expectedPermanent, expectedEphemeral }) => { + createLoggerForAgent(permanentAgent, settings); + createLoggerForAgent(ephemeralAgent, settings); + + const calls = vi.mocked(AgentLogger).mock.calls; + expect(calls[0]?.[0]).toMatchObject({ persistAgentThinkingLog: expectedPermanent }); + expect(calls[1]?.[0]).toMatchObject({ persistAgentThinkingLog: expectedEphemeral }); + }); +}); diff --git a/packages/engine/src/agent-heartbeat.ts b/packages/engine/src/agent-heartbeat.ts index 422c5f0ea..f8bf1193f 100644 --- a/packages/engine/src/agent-heartbeat.ts +++ b/packages/engine/src/agent-heartbeat.ts @@ -18,7 +18,7 @@ */ import type { AgentStore, AgentHeartbeatRun, HeartbeatInvocationSource, AgentHeartbeatConfig, AgentBudgetStatus, Message, MessageStore, TaskStore, TaskDetail, AgentRole, Agent, InboxTask, RunMutationContext, Settings, AgentConfigRevision, ReflectionStore, ChatStore, ChatRoom, ChatRoomMessage } from "@fusion/core"; -import { ApprovalRequestStore, buildExecutionMemoryInstructions, isEphemeralAgent, hasAgentIdentity, resolveEffectiveAgentPermissionPolicy, canAgentTakeImplementationTask, canAgentTakeImplementationTaskForExplicitRouting } from "@fusion/core"; +import { ApprovalRequestStore, buildExecutionMemoryInstructions, isEphemeralAgent, hasAgentIdentity, resolveEffectiveAgentPermissionPolicy, canAgentTakeImplementationTask, canAgentTakeImplementationTaskForExplicitRouting, resolvePersistAgentThinkingLog } from "@fusion/core"; import type { ToolDefinition } from "@mariozechner/pi-coding-agent"; import { Type, type Static } from "@mariozechner/pi-ai"; import { createHash } from "node:crypto"; @@ -2029,7 +2029,7 @@ export class HeartbeatMonitor { appendLog: (entry) => this.store.appendRunLog(agentId, run.id, entry), agent: agent.role as AgentRole, persistAgentToolOutput: memorySettings?.persistAgentToolOutput, - persistAgentThinkingLog: memorySettings?.persistAgentThinkingLog, + persistAgentThinkingLog: resolvePersistAgentThinkingLog(memorySettings, { ephemeral: isAgentEphemeral }), }); } else if (taskId) { agentLogger = new AgentLogger({ @@ -2038,7 +2038,7 @@ export class HeartbeatMonitor { agent: agent.role as AgentRole, appendLog: (entry) => this.store.appendRunLog(agentId, run.id, entry), persistAgentToolOutput: memorySettings?.persistAgentToolOutput, - persistAgentThinkingLog: memorySettings?.persistAgentThinkingLog, + persistAgentThinkingLog: resolvePersistAgentThinkingLog(memorySettings, { ephemeral: isAgentEphemeral }), }); } diff --git a/packages/engine/src/executor.ts b/packages/engine/src/executor.ts index 842762dc4..7d19389ab 100644 --- a/packages/engine/src/executor.ts +++ b/packages/engine/src/executor.ts @@ -12,6 +12,7 @@ import { getTaskMergeBlocker, isEphemeralAgent, resolveAgentPrompt, + resolvePersistAgentThinkingLog, resolveEffectiveAgentPermissionPolicy, resolveProjectDefaultModel, type RunCommandResult, @@ -3160,7 +3161,8 @@ export class TaskExecutor { taskId: task.id, agent: "executor", persistAgentToolOutput: settings.persistAgentToolOutput, - persistAgentThinkingLog: settings.persistAgentThinkingLog, + // Executor sessions are task-scoped ephemeral workers. + persistAgentThinkingLog: resolvePersistAgentThinkingLog(settings, { ephemeral: true }), onAgentText: (taskId, delta) => { lastAssistantText += delta; stuckDetector?.recordActivity(taskId); @@ -5290,7 +5292,8 @@ ${feedback} taskId: task.id, agent: "executor", persistAgentToolOutput: settings.persistAgentToolOutput, - persistAgentThinkingLog: settings.persistAgentThinkingLog, + // Executor sessions are task-scoped ephemeral workers. + persistAgentThinkingLog: resolvePersistAgentThinkingLog(settings, { ephemeral: true }), onAgentText: this.options.onAgentText, onAgentTool: this.options.onAgentTool, }); @@ -6100,7 +6103,8 @@ and show an appropriate message to the user.\` taskId: task.id, agent: "reviewer", persistAgentToolOutput: settings.persistAgentToolOutput, - persistAgentThinkingLog: settings.persistAgentThinkingLog, + // Review-in-executor sessions are task-scoped ephemeral workers. + persistAgentThinkingLog: resolvePersistAgentThinkingLog(settings, { ephemeral: true }), onAgentText: (taskId, delta) => { this.options.onAgentText?.(taskId, delta); }, diff --git a/packages/engine/src/merger.ts b/packages/engine/src/merger.ts index a1aa360fc..8228f36e2 100644 --- a/packages/engine/src/merger.ts +++ b/packages/engine/src/merger.ts @@ -39,6 +39,7 @@ import { resolveTaskMergeTarget, resolveTitleSummarizerSettingsModel, resolveAgentPrompt, + resolvePersistAgentThinkingLog, summarizeCommitBody, summarizeCommitSubject, summarizeMergeCommit, @@ -1006,7 +1007,8 @@ async function attemptInMergeVerificationFix( taskId, agent: "merger", persistAgentToolOutput: settings.persistAgentToolOutput, - persistAgentThinkingLog: settings.persistAgentThinkingLog, + // Merger agents are task-scoped ephemeral workers. + persistAgentThinkingLog: resolvePersistAgentThinkingLog(settings, { ephemeral: true }), onAgentText: options.onAgentText, onAgentTool: options.onAgentTool, }); @@ -2116,7 +2118,8 @@ async function runAiAgentForAutostashConflict(params: { taskId, agent: "merger", persistAgentToolOutput: settings.persistAgentToolOutput, - persistAgentThinkingLog: settings.persistAgentThinkingLog, + // Merger agents are task-scoped ephemeral workers. + persistAgentThinkingLog: resolvePersistAgentThinkingLog(settings, { ephemeral: true }), onAgentText: options.onAgentText ? (_id: string, delta: string) => options.onAgentText!(delta) : undefined, @@ -2486,7 +2489,8 @@ async function runAiAgentForAutostashHardFail(params: { taskId, agent: "merger", persistAgentToolOutput: settings.persistAgentToolOutput, - persistAgentThinkingLog: settings.persistAgentThinkingLog, + // Merger agents are task-scoped ephemeral workers. + persistAgentThinkingLog: resolvePersistAgentThinkingLog(settings, { ephemeral: true }), onAgentText: options.onAgentText ? (_id: string, delta: string) => options.onAgentText!(delta) : undefined, @@ -4931,7 +4935,8 @@ You are assisting with a paused \`git pull --rebase\`. taskId, agent: "merger", persistAgentToolOutput: settings.persistAgentToolOutput, - persistAgentThinkingLog: settings.persistAgentThinkingLog, + // Merger agents are task-scoped ephemeral workers. + persistAgentThinkingLog: resolvePersistAgentThinkingLog(settings, { ephemeral: true }), onAgentText: options?.onAgentText ? (_id, delta) => options.onAgentText?.(delta) : undefined, @@ -7878,7 +7883,8 @@ async function runAiAgentForCommit(params: AiAgentParams): Promise<{ success: bo taskId, agent: "merger", persistAgentToolOutput: settings.persistAgentToolOutput, - persistAgentThinkingLog: settings.persistAgentThinkingLog, + // Merger agents are task-scoped ephemeral workers. + persistAgentThinkingLog: resolvePersistAgentThinkingLog(settings, { ephemeral: true }), onAgentText: options.onAgentText ? (_id, delta) => options.onAgentText!(delta) : undefined, @@ -8492,7 +8498,8 @@ If issues are found that need attention, describe them clearly and include concr taskId, agent: "merger", persistAgentToolOutput: settings.persistAgentToolOutput, - persistAgentThinkingLog: settings.persistAgentThinkingLog, + // Merger agents are task-scoped ephemeral workers. + persistAgentThinkingLog: resolvePersistAgentThinkingLog(settings, { ephemeral: true }), }); try { diff --git a/packages/engine/src/reviewer.ts b/packages/engine/src/reviewer.ts index f9e34b1e2..92724e9fc 100644 --- a/packages/engine/src/reviewer.ts +++ b/packages/engine/src/reviewer.ts @@ -9,7 +9,7 @@ */ import type { TaskStore, TaskComment, AgentPromptsConfig, Settings } from "@fusion/core"; -import { buildReviewerMemoryInstructions, resolveAgentPrompt } from "@fusion/core"; +import { buildReviewerMemoryInstructions, resolveAgentPrompt, resolvePersistAgentThinkingLog } from "@fusion/core"; import { describeModel, promptWithFallback } from "./pi.js"; import { isContextLimitError } from "./context-limit-detector.js"; import { createResolvedAgentSession, extractRuntimeHint } from "./agent-session-helpers.js"; @@ -350,7 +350,8 @@ export async function reviewStep( ? (_id, delta) => options.onText!(delta) : undefined, persistAgentToolOutput: liveSettings?.persistAgentToolOutput, - persistAgentThinkingLog: liveSettings?.persistAgentThinkingLog, + // Reviewer sessions are task-scoped ephemeral workers. + persistAgentThinkingLog: resolvePersistAgentThinkingLog(liveSettings, { ephemeral: true }), }) : null; diff --git a/packages/engine/src/step-session-executor.ts b/packages/engine/src/step-session-executor.ts index 7ec597695..66a6fdc37 100644 --- a/packages/engine/src/step-session-executor.ts +++ b/packages/engine/src/step-session-executor.ts @@ -18,6 +18,7 @@ import { existsSync } from "node:fs"; import { join } from "node:path"; import type { AgentSession } from "@mariozechner/pi-coding-agent"; import type { AgentStore, MessageStore, PermanentAgentGatingContext, TaskDetail, Settings, TaskStore } from "@fusion/core"; +import { resolvePersistAgentThinkingLog } from "@fusion/core"; import { createResolvedAgentSession, @@ -890,7 +891,8 @@ export class StepSessionExecutor { taskId: taskDetail.id, agent: "executor", persistAgentToolOutput: settings.persistAgentToolOutput, - persistAgentThinkingLog: settings.persistAgentThinkingLog, + // Step-session workers are task-scoped ephemeral agents. + persistAgentThinkingLog: resolvePersistAgentThinkingLog(settings, { ephemeral: true }), }); let session: AgentSession | null = null; diff --git a/packages/engine/src/triage.ts b/packages/engine/src/triage.ts index ba299dce8..8f4a13fb8 100644 --- a/packages/engine/src/triage.ts +++ b/packages/engine/src/triage.ts @@ -9,6 +9,7 @@ import type { import { buildTriageMemoryInstructions, resolveAgentPrompt, + resolvePersistAgentThinkingLog, sortTasksByPriorityThenAgeAndId, } from "@fusion/core"; import type { ImageContent } from "@mariozechner/pi-ai"; @@ -928,7 +929,8 @@ export class TriageProcessor { taskId: task.id, agent: "triage", persistAgentToolOutput: settings.persistAgentToolOutput, - persistAgentThinkingLog: settings.persistAgentThinkingLog, + // Triage runs in a task-scoped ephemeral worker session. + persistAgentThinkingLog: resolvePersistAgentThinkingLog(settings, { ephemeral: true }), onAgentText: (id, delta) => { stuckDetector?.recordActivity(task.id); this.options.onAgentText?.(id, delta);