diff --git a/.changeset/FN-6963-tool-output-default-off.md b/.changeset/FN-6963-tool-output-default-off.md new file mode 100644 index 0000000000..a6cf0d0936 --- /dev/null +++ b/.changeset/FN-6963-tool-output-default-off.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Saved agent tool-output details now default off to reduce persisted log payloads, while timeline rows remain logged and detailed tool arguments/results stay available via the global `persistAgentToolOutput: true` opt-in. diff --git a/docs/settings-reference.md b/docs/settings-reference.md index e76e85a631..c7676be5c2 100644 --- a/docs/settings-reference.md +++ b/docs/settings-reference.md @@ -98,7 +98,7 @@ Fusion automatically falls back to ntfy's JSON publish format when a notificatio > Mesh lifecycle note: settings sync is executed by the process-level `PeerExchangeService` started by `fn serve`/`fn dashboard`. `InProcessRuntime` does not instantiate settings-sync mesh services per project. | `dashboardCurrentProjectIdByNode` | `Record` | `undefined` | Map of node ID to last-selected project ID. Use key `"local"` for the local node. Persists project context across browser restarts and PWA sessions. | -| `persistAgentToolOutput` | `boolean` | `true` | Controls whether detailed `detail` payloads are persisted for `tool`, `tool_result`, and `tool_error` agent log entries. When disabled, tool timeline rows are still recorded, but verbose payloads are omitted. | +| `persistAgentToolOutput` | `boolean` | `false` | Controls whether detailed `detail` payloads are persisted for `tool`, `tool_result`, and `tool_error` agent log entries. Tool timeline rows are still recorded by default; verbose tool arguments/results require opting in with `persistAgentToolOutput: true`. | | `persistAgentThinkingLogPermanent` | `boolean` | `false` | Controls whether `thinking`/reasoning rows are persisted for permanent (non-ephemeral) agents. | | `persistAgentThinkingLogEphemeral` | `boolean` | `false` | Controls whether `thinking`/reasoning rows are persisted for ephemeral/task-worker/spawned agents. | | `persistAgentThinkingLog` *(deprecated)* | `boolean` | `false` | Legacy fallback alias for thinking-row persistence. When set and a granular key above is still undefined, this legacy value is used for that agent kind. Leaving both granular keys off preserves default-off behavior; assistant text and tool rows are unchanged. | diff --git a/packages/core/src/settings-schema.ts b/packages/core/src/settings-schema.ts index 8c0b929bca..8933340707 100644 --- a/packages/core/src/settings-schema.ts +++ b/packages/core/src/settings-schema.ts @@ -159,7 +159,11 @@ export const DEFAULT_GLOBAL_SETTINGS = { vitestAutoKillEnabled: true, vitestKillThresholdPct: 90, // Agent log persistence controls - persistAgentToolOutput: true, + /* + FNXC:AgentLogs 2026-06-23-00:00: + Verbose tool arguments and results are default-off to reduce persisted log volume and payload exposure. Operators who need saved tool details can explicitly opt in with persistAgentToolOutput: true; tool timeline rows remain logged either way. + */ + persistAgentToolOutput: false, persistAgentThinkingLogPermanent: false, persistAgentThinkingLogEphemeral: false, persistAgentThinkingLog: false, diff --git a/packages/dashboard/app/components/__tests__/SettingsModal.test.tsx b/packages/dashboard/app/components/__tests__/SettingsModal.test.tsx index 2313590734..4df5673871 100644 --- a/packages/dashboard/app/components/__tests__/SettingsModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/SettingsModal.test.tsx @@ -893,8 +893,8 @@ describe("SettingsModal", () => { renderModal({ initialSection: "global-general" }); await waitForSettingsModalReady(); - // persistAgentToolOutput defaults to checked; Star-on-GitHub control absent. - expect(screen.getByRole("checkbox", { name: "Save tool output in agent logs" })).toBeChecked(); + // persistAgentToolOutput defaults to unchecked; Star-on-GitHub control absent. + expect(screen.getByRole("checkbox", { name: "Save tool output in agent logs" })).not.toBeChecked(); expect(screen.queryByRole("checkbox", { name: /Show "Star on GitHub" button in Settings header/i })).toBeNull(); // thinking-log checkboxes default to unchecked. @@ -913,6 +913,22 @@ describe("SettingsModal", () => { expect(screen.getByText(/Projects inherit this value when they do not set a project default tracking repo/i)).toBeInTheDocument(); }); + it("reflects persisted checked value from global settings", async () => { + mockFetchSettings.mockResolvedValue({ + ...defaultSettings, + persistAgentToolOutput: true, + }); + mockFetchSettingsByScope.mockResolvedValue({ + global: { ...defaultSettings, persistAgentToolOutput: true }, + project: {}, + }); + + renderModal({ initialSection: "global-general" }); + await waitForSettingsModalReady(); + + expect(screen.getByRole("checkbox", { name: "Save tool output in agent logs" })).toBeChecked(); + }); + it("reflects persisted unchecked value from global settings", async () => { mockFetchSettings.mockResolvedValue({ ...defaultSettings, @@ -958,7 +974,7 @@ describe("SettingsModal", () => { }); const globalPayload = mockUpdateGlobalSettings.mock.calls[0]?.[0] as Record; - expect(globalPayload.persistAgentToolOutput).toBe(false); + expect(globalPayload.persistAgentToolOutput).toBe(true); if (mockUpdateSettings.mock.calls.length > 0) { const projectPayload = mockUpdateSettings.mock.calls[0]?.[0] as Record; expect(projectPayload.persistAgentToolOutput).toBeUndefined(); diff --git a/packages/dashboard/app/components/settings/sections/GlobalGeneralSection.tsx b/packages/dashboard/app/components/settings/sections/GlobalGeneralSection.tsx index 2c78d2e167..cc1ede8c6b 100644 --- a/packages/dashboard/app/components/settings/sections/GlobalGeneralSection.tsx +++ b/packages/dashboard/app/components/settings/sections/GlobalGeneralSection.tsx @@ -23,7 +23,7 @@ export function GlobalGeneralSection({ scopeBanner, form, setForm, globalTrackin
+ setForm((f) => ({ ...f, persistAgentToolOutput: e.target.checked }))}/>{t("settings.globalGeneral.saveToolOutputInAgentLogs", " Save tool output in agent logs ")} {t("settings.globalGeneral.whenDisabledToolRowsAreStillLoggedBut", " When disabled, tool rows are still logged but detailed tool payloads are omitted. Very large tool payloads may still be clipped even when this stays enabled. ")}
diff --git a/packages/dashboard/src/__tests__/routes-settings.test.ts b/packages/dashboard/src/__tests__/routes-settings.test.ts index dc07d77c17..de6a5711ca 100644 --- a/packages/dashboard/src/__tests__/routes-settings.test.ts +++ b/packages/dashboard/src/__tests__/routes-settings.test.ts @@ -1382,7 +1382,7 @@ describe("GET /settings/scopes", () => { global: { themeMode: "dark", defaultProvider: "anthropic", - persistAgentToolOutput: true, + persistAgentToolOutput: false, persistAgentThinkingLogPermanent: false, persistAgentThinkingLogEphemeral: false, persistAgentThinkingLog: false, @@ -1395,7 +1395,7 @@ describe("GET /settings/scopes", () => { expect(res.status).toBe(200); expect(res.body.global.themeMode).toBe("dark"); expect(res.body.global.defaultProvider).toBe("anthropic"); - expect(res.body.global.persistAgentToolOutput).toBe(true); + expect(res.body.global.persistAgentToolOutput).toBe(false); expect(res.body.global.persistAgentThinkingLogPermanent).toBe(false); expect(res.body.global.persistAgentThinkingLogEphemeral).toBe(false); expect(res.body.global.persistAgentThinkingLog).toBe(false); diff --git a/packages/engine/src/__tests__/agent-logger.test.ts b/packages/engine/src/__tests__/agent-logger.test.ts index 78f3f58f67..4f32677abd 100644 --- a/packages/engine/src/__tests__/agent-logger.test.ts +++ b/packages/engine/src/__tests__/agent-logger.test.ts @@ -141,18 +141,32 @@ describe("AgentLogger", () => { expect(calls.length).toBe(2); // Text flushed first expect(calls[0]).toEqual(["FN-003", "pending text", "text", undefined, undefined]); - // Tool logged second with detail - expect(calls[1]).toEqual(["FN-003", "Bash", "tool", "ls", undefined]); + // Tool logged second without detail by default. + expect(calls[1]).toEqual(["FN-003", "Bash", "tool", undefined, undefined]); }); - it("logs tool detail using summarizeToolArgs", async () => { + it("omits tool detail by default when persistAgentToolOutput is unset", async () => { const store = createMockStore(); const logger = new AgentLogger({ store, taskId: "FN-004" }); logger.onToolStart("Read", { path: "src/index.ts" }); + logger.onToolEnd("Read", false, "ok"); + logger.onToolEnd("Read", true, "err"); await vi.advanceTimersByTimeAsync(0); - expect(store.appendAgentLog).toHaveBeenCalledWith("FN-004", "Read", "tool", "src/index.ts", undefined); + expect(store.appendAgentLog).toHaveBeenNthCalledWith(1, "FN-004", "Read", "tool", undefined, undefined); + expect(store.appendAgentLog).toHaveBeenNthCalledWith(2, "FN-004", "Read", "tool_result", undefined, undefined); + expect(store.appendAgentLog).toHaveBeenNthCalledWith(3, "FN-004", "Read", "tool_error", undefined, undefined); + }); + + it("logs tool detail using summarizeToolArgs when explicitly enabled", async () => { + const store = createMockStore(); + const logger = new AgentLogger({ store, taskId: "FN-004A", persistAgentToolOutput: true }); + + logger.onToolStart("Read", { path: "src/index.ts" }); + await vi.advanceTimersByTimeAsync(0); + + expect(store.appendAgentLog).toHaveBeenCalledWith("FN-004A", "Read", "tool", "src/index.ts", undefined); }); it("omits tool detail when persistAgentToolOutput is disabled", async () => { @@ -264,7 +278,7 @@ describe("AgentLogger", () => { (store.appendAgentLog as ReturnType).mockClear(); logger.onToolStart("Bash", { command: "ls" }); await vi.advanceTimersByTimeAsync(0); - expect(store.appendAgentLog).toHaveBeenCalledWith("FN-010", "Bash", "tool", "ls", "executor"); + expect(store.appendAgentLog).toHaveBeenCalledWith("FN-010", "Bash", "tool", undefined, "executor"); }); // ── Thinking buffer/flush ──────────────────────────────────────── @@ -344,6 +358,7 @@ describe("AgentLogger", () => { store, taskId: "FN-014", agent: "executor", + persistAgentToolOutput: true, persistAgentThinkingLog: true, flushSizeBytes: 1024, }); @@ -365,6 +380,7 @@ describe("AgentLogger", () => { store, taskId: "FN-015", agent: "executor", + persistAgentToolOutput: true, }); logger.onToolEnd("Bash", false, "command output"); @@ -378,6 +394,7 @@ describe("AgentLogger", () => { store, taskId: "FN-016", agent: "executor", + persistAgentToolOutput: true, }); logger.onToolEnd("Read", true, "file not found"); @@ -391,6 +408,7 @@ describe("AgentLogger", () => { store, taskId: "FN-016B", agent: "executor", + persistAgentToolOutput: true, }); const longError = "error:" + "y".repeat(1200); @@ -407,6 +425,7 @@ describe("AgentLogger", () => { store, taskId: "FN-017", agent: "executor", + persistAgentToolOutput: true, }); const longResult = "x".repeat(600); @@ -423,6 +442,7 @@ describe("AgentLogger", () => { store, taskId: "FN-017B", agent: "executor", + persistAgentToolOutput: true, }); const circular: Record = {}; circular.self = circular; diff --git a/packages/engine/src/__tests__/heartbeat-executor.test.ts b/packages/engine/src/__tests__/heartbeat-executor.test.ts index f9183a7ee1..4b3ec31e84 100644 --- a/packages/engine/src/__tests__/heartbeat-executor.test.ts +++ b/packages/engine/src/__tests__/heartbeat-executor.test.ts @@ -3528,8 +3528,8 @@ describe("executeHeartbeat", () => { const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" }); expect(appendAgentLog).toHaveBeenCalledWith("FN-001", "Heartbeat produced visible output", "text", undefined, "executor"); - expect(appendAgentLog).toHaveBeenCalledWith("FN-001", "read", "tool", "README.md", "executor"); - expect(appendAgentLog).toHaveBeenCalledWith("FN-001", "read", "tool_result", "done", "executor"); + expect(appendAgentLog).toHaveBeenCalledWith("FN-001", "read", "tool", undefined, "executor"); + expect(appendAgentLog).toHaveBeenCalledWith("FN-001", "read", "tool_result", undefined, "executor"); expect(result.contextSnapshot?.taskId).toBe("FN-001"); expect(result.stdoutExcerpt).toContain("Heartbeat produced visible output"); }); diff --git a/packages/engine/src/__tests__/merger-merge-details.test.ts b/packages/engine/src/__tests__/merger-merge-details.test.ts index cb065e119b..d241e25d0f 100644 --- a/packages/engine/src/__tests__/merger-merge-details.test.ts +++ b/packages/engine/src/__tests__/merger-merge-details.test.ts @@ -522,7 +522,7 @@ describe("aiMergeTask — agent log persistence", () => { await aiMergeTask(store, "/tmp/root", "FN-050"); - expect(store.appendAgentLog).toHaveBeenCalledWith("FN-050", "Bash", "tool", "git status", "merger"); + expect(store.appendAgentLog).toHaveBeenCalledWith("FN-050", "Bash", "tool", undefined, "merger"); }); it("still fires onAgentText callback alongside logging", async () => { diff --git a/packages/engine/src/__tests__/merger-verification.test.ts b/packages/engine/src/__tests__/merger-verification.test.ts index 6995d962d7..16a6684f31 100644 --- a/packages/engine/src/__tests__/merger-verification.test.ts +++ b/packages/engine/src/__tests__/merger-verification.test.ts @@ -2354,7 +2354,7 @@ describe("aiMergeTask — in-merge verification fix", () => { expect(capturedFixOptions.onToolStart).toBeTypeOf("function"); expect(capturedFixOptions.onToolEnd).toBeTypeOf("function"); - expect(store.appendAgentLog).toHaveBeenCalledWith("FN-050", "Bash", "tool", "vitest run", "merger"); + expect(store.appendAgentLog).toHaveBeenCalledWith("FN-050", "Bash", "tool", undefined, "merger"); const logMessages = (store.logEntry as ReturnType).mock.calls .map((call: any[]) => call[1]) diff --git a/packages/engine/src/__tests__/step-session-executor.test.ts b/packages/engine/src/__tests__/step-session-executor.test.ts index 41453461a7..221aa55d75 100644 --- a/packages/engine/src/__tests__/step-session-executor.test.ts +++ b/packages/engine/src/__tests__/step-session-executor.test.ts @@ -2465,8 +2465,8 @@ describe("StepSessionExecutor", () => { await executor.executeAll(); expect(appendAgentLog).toHaveBeenCalledWith("FN-001", "step output", "text", undefined, "executor"); - expect(appendAgentLog).toHaveBeenCalledWith("FN-001", "read", "tool", "src/foo.ts", "executor"); - expect(appendAgentLog).toHaveBeenCalledWith("FN-001", "read", "tool_result", "ok", "executor"); + expect(appendAgentLog).toHaveBeenCalledWith("FN-001", "read", "tool", undefined, "executor"); + expect(appendAgentLog).toHaveBeenCalledWith("FN-001", "read", "tool_result", undefined, "executor"); }); it("flushes AgentLogger in attempt finally block", async () => { diff --git a/packages/engine/src/__tests__/triage.test.ts b/packages/engine/src/__tests__/triage.test.ts index a8a4a59ca0..6e02f94282 100644 --- a/packages/engine/src/__tests__/triage.test.ts +++ b/packages/engine/src/__tests__/triage.test.ts @@ -3978,7 +3978,7 @@ describe("tool callback behavior (FN-1500)", () => { "FN-TOOL-002", "read", "tool", - "test.txt", + undefined, "triage", ); }); diff --git a/packages/engine/src/agent-logger.ts b/packages/engine/src/agent-logger.ts index 83bff39c46..21aa855351 100644 --- a/packages/engine/src/agent-logger.ts +++ b/packages/engine/src/agent-logger.ts @@ -145,7 +145,7 @@ export function summarizeToolArgs(name: string, args?: Record): * When both are provided, both sinks receive every entry. */ export interface AgentLoggerOptions { - /** When false, omit `detail` payloads for tool entries while preserving the rows. */ + /** When true, persist `detail` payloads for tool entries; default false preserves rows without verbose payloads. */ persistAgentToolOutput?: boolean; /** When true, persist `thinking` rows. Default: false (skip thinking persistence). */ persistAgentThinkingLog?: boolean; @@ -233,7 +233,11 @@ export class AgentLogger { this.externalToolCb = options.onAgentTool; this.flushSizeBytes = options.flushSizeBytes ?? FLUSH_SIZE_BYTES; this.flushIntervalMs = options.flushIntervalMs ?? FLUSH_INTERVAL_MS; - this.persistAgentToolOutput = options.persistAgentToolOutput !== false; + /* + FNXC:AgentLogs 2026-06-23-00:00: + Direct logger construction must match global settings: verbose tool payload persistence is default-off and only explicit persistAgentToolOutput: true saves tool entry detail. Tool/tool_result/tool_error rows still persist so timelines and usage telemetry remain intact. + */ + this.persistAgentToolOutput = options.persistAgentToolOutput === true; this.persistAgentThinkingLog = options.persistAgentThinkingLog === true; this.usageContext = options.usageContext;