feat(FN-3175): document global tool output persistence setting in settings
Added documentation for the global tool output persistence setting to the settings reference guide. Fusion-Task-Id: FN-3175
This commit is contained in:
@@ -55,6 +55,8 @@ describe("settings key parity", () => {
|
||||
expect(isGlobalSettingsKey("researchGlobalDefaults")).toBe(true);
|
||||
expect(isProjectSettingsKey("themeMode")).toBe(false);
|
||||
expect(isGlobalSettingsKey("remoteAccess")).toBe(true);
|
||||
expect(isGlobalSettingsKey("persistAgentToolOutput")).toBe(true);
|
||||
expect(isProjectSettingsKey("persistAgentToolOutput")).toBe(false);
|
||||
expect(isGlobalSettingsKey("researchSettings")).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -67,6 +67,8 @@ export const DEFAULT_GLOBAL_SETTINGS = {
|
||||
// Dashboard TUI memory guard
|
||||
vitestAutoKillEnabled: true,
|
||||
vitestKillThresholdPct: 90,
|
||||
// Agent log persistence controls
|
||||
persistAgentToolOutput: true,
|
||||
researchGlobalDefaults: {
|
||||
searchProvider: undefined,
|
||||
synthesisProvider: undefined,
|
||||
|
||||
@@ -1441,6 +1441,11 @@ export interface GlobalSettings {
|
||||
* triggers a vitest auto-kill. Clamped to [50, 99] in the UI.
|
||||
* Default: 90. */
|
||||
vitestKillThresholdPct?: number;
|
||||
/** When true (default), persist detailed tool argument/result payloads in
|
||||
* task agent logs (`agent.log`) for `tool`, `tool_result`, and
|
||||
* `tool_error` entries. When false, tool timeline rows are still stored,
|
||||
* but their verbose `detail` payload is omitted to reduce log size/noise. */
|
||||
persistAgentToolOutput?: boolean;
|
||||
/** Research defaults shared across all projects.
|
||||
* Project settings may override these via `researchSettings`. */
|
||||
researchGlobalDefaults?: ResearchGlobalDefaults;
|
||||
|
||||
@@ -1846,6 +1846,22 @@ export function SettingsModal({
|
||||
</small>
|
||||
</div>
|
||||
<CliBinaryPanel />
|
||||
<div className="form-group">
|
||||
<label htmlFor="persistAgentToolOutput" className="checkbox-label">
|
||||
<input
|
||||
id="persistAgentToolOutput"
|
||||
type="checkbox"
|
||||
checked={form.persistAgentToolOutput !== false}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, persistAgentToolOutput: e.target.checked }))
|
||||
}
|
||||
/>
|
||||
Save tool output in agent logs
|
||||
</label>
|
||||
<div className="settings-field-help">
|
||||
When disabled, tool rows are still logged but detailed tool payloads are omitted.
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="fnBinaryCheckEnabled" className="checkbox-label">
|
||||
<input
|
||||
|
||||
@@ -241,6 +241,8 @@ describe("AgentLogViewer", () => {
|
||||
it("does not render detail toggle when detail is absent", () => {
|
||||
const entries = [
|
||||
makeEntry({ text: "Bash", type: "tool" }),
|
||||
makeEntry({ text: "Bash", type: "tool_result" }),
|
||||
makeEntry({ text: "Bash", type: "tool_error" }),
|
||||
];
|
||||
render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
expect(screen.queryByTestId("tool-detail-toggle")).toBeNull();
|
||||
|
||||
@@ -416,6 +416,50 @@ describe("SettingsModal", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Global General", () => {
|
||||
it("defaults persistAgentToolOutput checkbox to checked", async () => {
|
||||
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,
|
||||
persistAgentToolOutput: false,
|
||||
});
|
||||
mockFetchSettingsByScope.mockResolvedValue({
|
||||
global: { ...defaultSettings, persistAgentToolOutput: false },
|
||||
project: {},
|
||||
});
|
||||
|
||||
renderModal({ initialSection: "global-general" });
|
||||
await waitForSettingsModalReady();
|
||||
|
||||
expect(screen.getByRole("checkbox", { name: "Save tool output in agent logs" })).not.toBeChecked();
|
||||
});
|
||||
|
||||
it("saves persistAgentToolOutput only via global settings payload", async () => {
|
||||
renderModal({ initialSection: "global-general" });
|
||||
await waitForSettingsModalReady();
|
||||
|
||||
await userEvent.click(screen.getByRole("checkbox", { name: "Save tool output in agent logs" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Save" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateGlobalSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const globalPayload = mockUpdateGlobalSettings.mock.calls[0]?.[0] as Record<string, unknown>;
|
||||
expect(globalPayload.persistAgentToolOutput).toBe(false);
|
||||
if (mockUpdateSettings.mock.calls.length > 0) {
|
||||
const projectPayload = mockUpdateSettings.mock.calls[0]?.[0] as Record<string, unknown>;
|
||||
expect(projectPayload.persistAgentToolOutput).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Appearance", () => {
|
||||
it("renders dashboard font size options with saved value", async () => {
|
||||
const onDashboardFontScaleChange = vi.fn();
|
||||
|
||||
@@ -15066,6 +15066,23 @@ describe("PUT /settings/global", () => {
|
||||
expect(store.updateGlobalSettings).toHaveBeenCalledWith({ themeMode: "light" });
|
||||
});
|
||||
|
||||
it("accepts persistAgentToolOutput in global updates", async () => {
|
||||
const updatedMerged = { persistAgentToolOutput: false };
|
||||
(store.updateGlobalSettings as ReturnType<typeof vi.fn>).mockResolvedValue(updatedMerged);
|
||||
|
||||
const res = await REQUEST(
|
||||
buildApp(),
|
||||
"PUT",
|
||||
"/api/settings/global",
|
||||
JSON.stringify({ persistAgentToolOutput: false }),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(store.updateGlobalSettings).toHaveBeenCalledWith({ persistAgentToolOutput: false });
|
||||
expect(res.body.persistAgentToolOutput).toBe(false);
|
||||
});
|
||||
|
||||
it("returns 500 on update error", async () => {
|
||||
(store.updateGlobalSettings as ReturnType<typeof vi.fn>).mockRejectedValue(new Error("Write failed"));
|
||||
|
||||
@@ -15200,7 +15217,7 @@ describe("GET /settings/scopes", () => {
|
||||
|
||||
it("returns settings separated by scope", async () => {
|
||||
(store.getSettingsByScope as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
global: { themeMode: "dark", defaultProvider: "anthropic" },
|
||||
global: { themeMode: "dark", defaultProvider: "anthropic", persistAgentToolOutput: true },
|
||||
project: { maxConcurrent: 4, autoMerge: false },
|
||||
});
|
||||
|
||||
@@ -15209,8 +15226,10 @@ 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.project.maxConcurrent).toBe(4);
|
||||
expect(res.body.project.autoMerge).toBe(false);
|
||||
expect(res.body.project.persistAgentToolOutput).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns exact response envelope shape with only global and project keys", async () => {
|
||||
|
||||
@@ -155,6 +155,24 @@ describe("AgentLogger", () => {
|
||||
expect(store.appendAgentLog).toHaveBeenCalledWith("FN-004", "Read", "tool", "src/index.ts", undefined);
|
||||
});
|
||||
|
||||
it("omits tool detail when persistAgentToolOutput is disabled", async () => {
|
||||
const store = createMockStore();
|
||||
const logger = new AgentLogger({
|
||||
store,
|
||||
taskId: "FN-004B",
|
||||
persistAgentToolOutput: false,
|
||||
});
|
||||
|
||||
logger.onToolStart("Read", { path: "src/index.ts" });
|
||||
logger.onToolEnd("Read", false, "ok");
|
||||
logger.onToolEnd("Read", true, "err");
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
expect(store.appendAgentLog).toHaveBeenNthCalledWith(1, "FN-004B", "Read", "tool", undefined, undefined);
|
||||
expect(store.appendAgentLog).toHaveBeenNthCalledWith(2, "FN-004B", "Read", "tool_result", undefined, undefined);
|
||||
expect(store.appendAgentLog).toHaveBeenNthCalledWith(3, "FN-004B", "Read", "tool_error", undefined, undefined);
|
||||
});
|
||||
|
||||
it("logs tool with undefined detail for unknown args", async () => {
|
||||
const store = createMockStore();
|
||||
const logger = new AgentLogger({ store, taskId: "FN-005" });
|
||||
|
||||
@@ -1417,6 +1417,7 @@ export class HeartbeatMonitor {
|
||||
agentLogger = new AgentLogger({
|
||||
appendLog: (entry) => this.store.appendRunLog(agentId, run.id, entry),
|
||||
agent: agent.role as AgentRole,
|
||||
persistAgentToolOutput: memorySettings?.persistAgentToolOutput,
|
||||
});
|
||||
} else if (taskId) {
|
||||
agentLogger = new AgentLogger({
|
||||
@@ -1424,6 +1425,7 @@ export class HeartbeatMonitor {
|
||||
taskId,
|
||||
agent: agent.role as AgentRole,
|
||||
appendLog: (entry) => this.store.appendRunLog(agentId, run.id, entry),
|
||||
persistAgentToolOutput: memorySettings?.persistAgentToolOutput,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,8 @@ export function summarizeToolArgs(name: string, args?: Record<string, unknown>):
|
||||
* When both are provided, both sinks receive every entry.
|
||||
*/
|
||||
export interface AgentLoggerOptions {
|
||||
/** When false, omit `detail` payloads for tool entries while preserving the rows. */
|
||||
persistAgentToolOutput?: 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). */
|
||||
@@ -107,6 +109,7 @@ export class AgentLogger {
|
||||
private readonly externalTextCb?: (taskId: string, delta: string) => void;
|
||||
private readonly externalToolCb?: (taskId: string, toolName: string) => void;
|
||||
private readonly log = createLogger("agent-logger");
|
||||
private readonly persistAgentToolOutput: boolean;
|
||||
|
||||
constructor(options: AgentLoggerOptions) {
|
||||
this.store = options.store;
|
||||
@@ -117,6 +120,7 @@ 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;
|
||||
|
||||
// Bind callbacks so they can be passed directly as function references
|
||||
this.onText = this.onText.bind(this);
|
||||
@@ -208,12 +212,14 @@ export class AgentLogger {
|
||||
* @param storeWarnMsg - Warning message prefix used when the task-store write fails.
|
||||
*/
|
||||
private writeEntry(text: string, type: AgentLogEntry["type"], detail: string | undefined, _storeWarnMsg: string, immediate = false): void {
|
||||
const isToolEntry = type === "tool" || type === "tool_result" || type === "tool_error";
|
||||
const includeDetail = !isToolEntry || this.persistAgentToolOutput;
|
||||
const entry: AgentLogEntry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
taskId: this.taskId,
|
||||
text,
|
||||
type,
|
||||
...(detail !== undefined && { detail }),
|
||||
...(detail !== undefined && includeDetail && { detail }),
|
||||
...(this.agent !== undefined && { agent: this.agent }),
|
||||
};
|
||||
|
||||
|
||||
@@ -2679,6 +2679,7 @@ export class TaskExecutor {
|
||||
store: this.store,
|
||||
taskId: task.id,
|
||||
agent: "executor",
|
||||
persistAgentToolOutput: settings.persistAgentToolOutput,
|
||||
onAgentText: (taskId, delta) => {
|
||||
lastAssistantText += delta;
|
||||
stuckDetector?.recordActivity(taskId);
|
||||
@@ -4762,6 +4763,7 @@ and show an appropriate message to the user.\`
|
||||
store: this.store,
|
||||
taskId: task.id,
|
||||
agent: "reviewer",
|
||||
persistAgentToolOutput: settings.persistAgentToolOutput,
|
||||
onAgentText: (taskId, delta) => {
|
||||
this.options.onAgentText?.(taskId, delta);
|
||||
},
|
||||
|
||||
@@ -896,6 +896,7 @@ async function attemptInMergeVerificationFix(
|
||||
store,
|
||||
taskId,
|
||||
agent: "merger",
|
||||
persistAgentToolOutput: settings.persistAgentToolOutput,
|
||||
onAgentText: options.onAgentText,
|
||||
onAgentTool: options.onAgentTool,
|
||||
});
|
||||
@@ -2275,6 +2276,7 @@ You are assisting with a paused \`git pull --rebase\`.
|
||||
store,
|
||||
taskId,
|
||||
agent: "merger",
|
||||
persistAgentToolOutput: settings.persistAgentToolOutput,
|
||||
onAgentText: options?.onAgentText
|
||||
? (_id, delta) => options.onAgentText?.(delta)
|
||||
: undefined,
|
||||
@@ -4707,6 +4709,7 @@ async function runAiAgentForCommit(params: AiAgentParams): Promise<{ success: bo
|
||||
store,
|
||||
taskId,
|
||||
agent: "merger",
|
||||
persistAgentToolOutput: settings.persistAgentToolOutput,
|
||||
onAgentText: options.onAgentText
|
||||
? (_id, delta) => options.onAgentText!(delta)
|
||||
: undefined,
|
||||
@@ -5298,6 +5301,7 @@ If issues are found that need attention, describe them clearly and include concr
|
||||
store,
|
||||
taskId,
|
||||
agent: "merger",
|
||||
persistAgentToolOutput: settings.persistAgentToolOutput,
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
@@ -343,6 +343,7 @@ export async function reviewStep(
|
||||
onAgentText: options.onText
|
||||
? (_id, delta) => options.onText!(delta)
|
||||
: undefined,
|
||||
persistAgentToolOutput: liveSettings?.persistAgentToolOutput,
|
||||
})
|
||||
: null;
|
||||
|
||||
|
||||
@@ -911,6 +911,7 @@ export class StepSessionExecutor {
|
||||
store: this.store,
|
||||
taskId: taskDetail.id,
|
||||
agent: "executor",
|
||||
persistAgentToolOutput: settings.persistAgentToolOutput,
|
||||
});
|
||||
let session: AgentSession | null = null;
|
||||
|
||||
|
||||
@@ -888,6 +888,7 @@ export class TriageProcessor {
|
||||
store: this.store,
|
||||
taskId: task.id,
|
||||
agent: "triage",
|
||||
persistAgentToolOutput: settings.persistAgentToolOutput,
|
||||
onAgentText: (id, delta) => {
|
||||
stuckDetector?.recordActivity(task.id);
|
||||
this.options.onAgentText?.(id, delta);
|
||||
|
||||
Reference in New Issue
Block a user