feat(FN-2083): merge fusion/fn-2083
This commit is contained in:
@@ -344,4 +344,116 @@ describe("AgentLogger", () => {
|
|||||||
await vi.advanceTimersByTimeAsync(0);
|
await vi.advanceTimersByTimeAsync(0);
|
||||||
expect(store.appendAgentLog).toHaveBeenCalledWith("FN-018", "Bash", "tool_result", undefined, "merger");
|
expect(store.appendAgentLog).toHaveBeenCalledWith("FN-018", "Bash", "tool_result", undefined, "merger");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("persistence failure observability", () => {
|
||||||
|
const warningFor = (warnSpy: ReturnType<typeof vi.spyOn>): string => {
|
||||||
|
return warnSpy.mock.calls.map((call) => call.map((arg) => String(arg)).join(" ")).join("\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
it("text flush failure logs structured warning", async () => {
|
||||||
|
const store = {
|
||||||
|
appendAgentLog: vi.fn().mockRejectedValue(new Error("ENOSPC: no space left on device")),
|
||||||
|
} as unknown as TaskStore;
|
||||||
|
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||||
|
const logger = new AgentLogger({
|
||||||
|
store,
|
||||||
|
taskId: "FN-2083-TEXT",
|
||||||
|
flushSizeBytes: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.onText("some text");
|
||||||
|
await vi.advanceTimersByTimeAsync(0);
|
||||||
|
|
||||||
|
const warning = warningFor(warnSpy);
|
||||||
|
expect(warning).toContain("[agent-logger]");
|
||||||
|
expect(warning).toContain("FN-2083-TEXT");
|
||||||
|
expect(warning).toContain("ENOSPC");
|
||||||
|
|
||||||
|
warnSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("thinking flush failure logs structured warning", async () => {
|
||||||
|
const store = {
|
||||||
|
appendAgentLog: vi.fn().mockRejectedValue(new Error("ENOSPC: no space left on device")),
|
||||||
|
} as unknown as TaskStore;
|
||||||
|
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||||
|
const logger = new AgentLogger({
|
||||||
|
store,
|
||||||
|
taskId: "FN-2083-THINK",
|
||||||
|
flushSizeBytes: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.onThinking("deep thought");
|
||||||
|
await vi.advanceTimersByTimeAsync(0);
|
||||||
|
|
||||||
|
const warning = warningFor(warnSpy);
|
||||||
|
expect(warning).toContain("[agent-logger]");
|
||||||
|
expect(warning).toContain("FN-2083-THINK");
|
||||||
|
expect(warning).toContain("thinking");
|
||||||
|
expect(warning).toContain("ENOSPC");
|
||||||
|
|
||||||
|
warnSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("tool-start persistence failure logs structured warning", async () => {
|
||||||
|
const store = {
|
||||||
|
appendAgentLog: vi.fn().mockRejectedValue(new Error("EACCES: permission denied")),
|
||||||
|
} as unknown as TaskStore;
|
||||||
|
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||||
|
const logger = new AgentLogger({ store, taskId: "FN-2083-TOOL-START" });
|
||||||
|
|
||||||
|
logger.onToolStart("Bash", { command: "ls" });
|
||||||
|
await vi.advanceTimersByTimeAsync(0);
|
||||||
|
|
||||||
|
const warning = warningFor(warnSpy);
|
||||||
|
expect(warning).toContain("[agent-logger]");
|
||||||
|
expect(warning).toContain("FN-2083-TOOL-START");
|
||||||
|
expect(warning).toContain("Bash");
|
||||||
|
expect(warning).toContain("EACCES");
|
||||||
|
|
||||||
|
warnSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("tool-end persistence failure logs structured warning", async () => {
|
||||||
|
const store = {
|
||||||
|
appendAgentLog: vi.fn().mockRejectedValue(new Error("EPERM: operation not permitted")),
|
||||||
|
} as unknown as TaskStore;
|
||||||
|
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||||
|
const logger = new AgentLogger({ store, taskId: "FN-2083-TOOL-END" });
|
||||||
|
|
||||||
|
logger.onToolEnd("Bash", false, "output");
|
||||||
|
await vi.advanceTimersByTimeAsync(0);
|
||||||
|
|
||||||
|
const warning = warningFor(warnSpy);
|
||||||
|
expect(warning).toContain("[agent-logger]");
|
||||||
|
expect(warning).toContain("FN-2083-TOOL-END");
|
||||||
|
expect(warning).toContain("EPERM");
|
||||||
|
|
||||||
|
warnSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flush() propagates text and thinking warnings", async () => {
|
||||||
|
const store = {
|
||||||
|
appendAgentLog: vi.fn().mockRejectedValue(new Error("ENOSPC: no space left on device")),
|
||||||
|
} as unknown as TaskStore;
|
||||||
|
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||||
|
const logger = new AgentLogger({
|
||||||
|
store,
|
||||||
|
taskId: "FN-2083-FLUSH",
|
||||||
|
flushSizeBytes: 1024,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.onText("text");
|
||||||
|
logger.onThinking("thought");
|
||||||
|
await logger.flush();
|
||||||
|
|
||||||
|
const warning = warningFor(warnSpy);
|
||||||
|
expect(warnSpy).toHaveBeenCalledTimes(2);
|
||||||
|
expect(warning).toContain("[agent-logger]");
|
||||||
|
expect(warning).toContain("FN-2083-FLUSH");
|
||||||
|
expect(warning).toContain("ENOSPC");
|
||||||
|
|
||||||
|
warnSpy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import type { TaskStore, AgentRole } from "@fusion/core";
|
import type { TaskStore, AgentRole } from "@fusion/core";
|
||||||
|
import { createLogger } from "./logger.js";
|
||||||
|
|
||||||
/** Default byte threshold before an automatic flush. */
|
/** Default byte threshold before an automatic flush. */
|
||||||
const FLUSH_SIZE_BYTES = 1024;
|
const FLUSH_SIZE_BYTES = 1024;
|
||||||
/** Default timer interval (ms) for periodic flush of small writes. */
|
/** Default timer interval (ms) for periodic flush of small writes. */
|
||||||
const FLUSH_INTERVAL_MS = 500;
|
const FLUSH_INTERVAL_MS = 500;
|
||||||
|
|
||||||
|
const log = createLogger("agent-logger");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Produce a human-readable summary from tool arguments.
|
* Produce a human-readable summary from tool arguments.
|
||||||
* Returns the full argument value without truncation.
|
* Returns the full argument value without truncation.
|
||||||
@@ -147,7 +150,14 @@ export class AgentLogger {
|
|||||||
if (this.thinkingFlushTimer) { clearTimeout(this.thinkingFlushTimer); this.thinkingFlushTimer = null; }
|
if (this.thinkingFlushTimer) { clearTimeout(this.thinkingFlushTimer); this.thinkingFlushTimer = null; }
|
||||||
this.flushThinkingBuffer();
|
this.flushThinkingBuffer();
|
||||||
const detail = summarizeToolArgs(name, args);
|
const detail = summarizeToolArgs(name, args);
|
||||||
this.store.appendAgentLog(this.taskId, name, "tool", detail, this.agent).catch(() => {});
|
this.store.appendAgentLog(this.taskId, name, "tool", detail, this.agent).catch((err) => {
|
||||||
|
log.warn(
|
||||||
|
"Failed to persist tool-start log for task %s (tool: %s): %s",
|
||||||
|
this.taskId,
|
||||||
|
name,
|
||||||
|
err instanceof Error ? err.message : String(err),
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -165,7 +175,15 @@ export class AgentLogger {
|
|||||||
const str = typeof result === "string" ? result : JSON.stringify(result);
|
const str = typeof result === "string" ? result : JSON.stringify(result);
|
||||||
detail = str.length > 500 ? str.slice(0, 500) + "…" : str;
|
detail = str.length > 500 ? str.slice(0, 500) + "…" : str;
|
||||||
}
|
}
|
||||||
this.store.appendAgentLog(this.taskId, name, type, detail, this.agent).catch(() => {});
|
this.store.appendAgentLog(this.taskId, name, type, detail, this.agent).catch((err) => {
|
||||||
|
log.warn(
|
||||||
|
"Failed to persist tool-end log for task %s (tool: %s, type: %s): %s",
|
||||||
|
this.taskId,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
err instanceof Error ? err.message : String(err),
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -185,8 +203,12 @@ export class AgentLogger {
|
|||||||
if (this.textBuffer.length === 0) return Promise.resolve();
|
if (this.textBuffer.length === 0) return Promise.resolve();
|
||||||
const chunk = this.textBuffer;
|
const chunk = this.textBuffer;
|
||||||
this.textBuffer = "";
|
this.textBuffer = "";
|
||||||
return this.store.appendAgentLog(this.taskId, chunk, "text", undefined, this.agent).catch(() => {
|
return this.store.appendAgentLog(this.taskId, chunk, "text", undefined, this.agent).catch((err) => {
|
||||||
/* best-effort persistence */
|
log.warn(
|
||||||
|
"Failed to persist text log for task %s: %s",
|
||||||
|
this.taskId,
|
||||||
|
err instanceof Error ? err.message : String(err),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,8 +216,12 @@ export class AgentLogger {
|
|||||||
if (this.thinkingBuffer.length === 0) return Promise.resolve();
|
if (this.thinkingBuffer.length === 0) return Promise.resolve();
|
||||||
const chunk = this.thinkingBuffer;
|
const chunk = this.thinkingBuffer;
|
||||||
this.thinkingBuffer = "";
|
this.thinkingBuffer = "";
|
||||||
return this.store.appendAgentLog(this.taskId, chunk, "thinking", undefined, this.agent).catch(() => {
|
return this.store.appendAgentLog(this.taskId, chunk, "thinking", undefined, this.agent).catch((err) => {
|
||||||
/* best-effort persistence */
|
log.warn(
|
||||||
|
"Failed to persist thinking log for task %s: %s",
|
||||||
|
this.taskId,
|
||||||
|
err instanceof Error ? err.message : String(err),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user