feat(HAI-072): add agent log viewer with persistence, streaming, and UI
- Add agent log persistence layer with JSONL append/read and event emission in core store - Add server-side SSE log streaming endpoint and REST route for fetching logs - Create AgentLogViewer component and useAgentLogs hook for real-time log display - Integrate log viewer into TaskDetailModal - Fix pre-existing build and test errors
This commit is contained in:
@@ -17,6 +17,7 @@ function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
|
||||
getSettings: vi.fn().mockResolvedValue({}),
|
||||
updateSettings: vi.fn(),
|
||||
logEntry: vi.fn().mockResolvedValue(undefined),
|
||||
getAgentLogs: vi.fn().mockResolvedValue([]),
|
||||
...overrides,
|
||||
} as unknown as TaskStore;
|
||||
}
|
||||
@@ -340,4 +341,36 @@ describe("Attachment routes", () => {
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
it("GET /tasks/:id/logs — returns agent logs", async () => {
|
||||
const fakeLogs = [
|
||||
{ timestamp: "2026-01-01T00:00:00Z", taskId: "HAI-001", text: "Hello", type: "text" },
|
||||
{ timestamp: "2026-01-01T00:00:01Z", taskId: "HAI-001", text: "Read", type: "tool" },
|
||||
];
|
||||
(store.getAgentLogs as ReturnType<typeof vi.fn>).mockResolvedValue(fakeLogs);
|
||||
|
||||
const res = await GET(buildApp(), "/api/tasks/HAI-001/logs");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual(fakeLogs);
|
||||
expect(store.getAgentLogs).toHaveBeenCalledWith("HAI-001");
|
||||
});
|
||||
|
||||
it("GET /tasks/:id/logs — returns empty array when no logs", async () => {
|
||||
(store.getAgentLogs as ReturnType<typeof vi.fn>).mockResolvedValue([]);
|
||||
|
||||
const res = await GET(buildApp(), "/api/tasks/HAI-001/logs");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual([]);
|
||||
});
|
||||
|
||||
it("GET /tasks/:id/logs — returns 500 on store error", async () => {
|
||||
(store.getAgentLogs as ReturnType<typeof vi.fn>).mockRejectedValue(new Error("disk error"));
|
||||
|
||||
const res = await GET(buildApp(), "/api/tasks/HAI-001/logs");
|
||||
|
||||
expect(res.status).toBe(500);
|
||||
expect(res.body.error).toBe("disk error");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -174,6 +174,20 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
}
|
||||
});
|
||||
|
||||
// Get historical agent logs for a task
|
||||
router.get("/tasks/:id/logs", async (req, res) => {
|
||||
try {
|
||||
const logs = await store.getAgentLogs(req.params.id);
|
||||
res.json(logs);
|
||||
} catch (err: any) {
|
||||
if (err.code === "ENOENT") {
|
||||
res.status(404).json({ error: `Task ${req.params.id} not found` });
|
||||
} else {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Get single task with prompt content
|
||||
router.get("/tasks/:id", async (req, res) => {
|
||||
try {
|
||||
|
||||
@@ -43,6 +43,35 @@ export function createServer(store: TaskStore, options?: ServerOptions): ReturnT
|
||||
// Rate limiting — stricter limit on SSE connections
|
||||
app.get("/api/events", rateLimit(RATE_LIMITS.sse), createSSE(store));
|
||||
|
||||
// Per-task SSE endpoint for live agent log streaming
|
||||
app.get("/api/tasks/:id/logs/stream", (req, res) => {
|
||||
const taskId = req.params.id;
|
||||
|
||||
res.setHeader("Content-Type", "text/event-stream");
|
||||
res.setHeader("Cache-Control", "no-cache");
|
||||
res.setHeader("Connection", "keep-alive");
|
||||
res.setHeader("X-Accel-Buffering", "no");
|
||||
res.flushHeaders();
|
||||
|
||||
res.write(": connected\n\n");
|
||||
|
||||
const onAgentLog = (entry: { taskId: string; text: string; type: string; timestamp: string }) => {
|
||||
if (entry.taskId !== taskId) return;
|
||||
res.write(`event: agent:log\ndata: ${JSON.stringify(entry)}\n\n`);
|
||||
};
|
||||
|
||||
store.on("agent:log", onAgentLog);
|
||||
|
||||
const heartbeat = setInterval(() => {
|
||||
res.write(": heartbeat\n\n");
|
||||
}, 30_000);
|
||||
|
||||
req.on("close", () => {
|
||||
clearInterval(heartbeat);
|
||||
store.off("agent:log", onAgentLog);
|
||||
});
|
||||
});
|
||||
|
||||
// Rate limiting — mutation endpoints (POST/PUT/PATCH/DELETE)
|
||||
app.use("/api", rateLimit(RATE_LIMITS.api));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user