feat(FN-1183): add agent reflections tab to dashboard
- Add API routes for agent reflections (GET, POST, DELETE) - Create AgentReflectionsTab component with reflection list and form - Integrate reflections tab into AgentDetailView - Add CSS styles for the reflections tab UI - Write tests for API routes and component
This commit is contained in:
@@ -9835,3 +9835,138 @@ describe("GET /api/agents/:id/runs/:runId/logs", () => {
|
||||
expect([404, 500]).toContain(res.status);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Agent Reflection routes", () => {
|
||||
let tempDir: string;
|
||||
let fusionDir: string;
|
||||
let agentId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = mkdtempSync(join(tmpdir(), "kb-routes-reflection-"));
|
||||
fusionDir = join(tempDir, ".fusion");
|
||||
mkdirSync(fusionDir, { recursive: true });
|
||||
|
||||
// Create a real agent in the temp directory
|
||||
const { AgentStore } = await import("@fusion/core");
|
||||
const agentStore = new AgentStore({ rootDir: fusionDir });
|
||||
await agentStore.init();
|
||||
const agent = await agentStore.createAgent({
|
||||
name: "Test Agent",
|
||||
role: "executor",
|
||||
});
|
||||
agentId = agent.id;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function buildApp() {
|
||||
const store = createMockStore({
|
||||
getFusionDir: vi.fn().mockReturnValue(fusionDir),
|
||||
} as any);
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use("/api", createApiRoutes(store));
|
||||
return app;
|
||||
}
|
||||
|
||||
describe("GET /api/agents/:id/reflections", () => {
|
||||
it("returns 200 for valid agent (uses real stores)", async () => {
|
||||
const res = await GET(buildApp(), `/api/agents/${agentId}/reflections`);
|
||||
|
||||
// The route uses real stores, so it should return an empty array (no reflections created yet)
|
||||
expect(res.status).toBe(200);
|
||||
expect(Array.isArray(res.body)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns 404 for non-existent agent", async () => {
|
||||
const res = await GET(buildApp(), "/api/agents/nonexistent-agent/reflections");
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
expect(res.body.error).toContain("not found");
|
||||
});
|
||||
|
||||
it("accepts limit query param", async () => {
|
||||
const res = await GET(buildApp(), `/api/agents/${agentId}/reflections?limit=10`);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(Array.isArray(res.body)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/agents/:id/reflections/latest", () => {
|
||||
it("returns 404 when no reflections exist", async () => {
|
||||
const res = await GET(buildApp(), `/api/agents/${agentId}/reflections/latest`);
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
expect(res.body.error).toContain("No reflections found");
|
||||
});
|
||||
|
||||
it("returns 404 for non-existent agent", async () => {
|
||||
const res = await GET(buildApp(), "/api/agents/nonexistent-agent/reflections/latest");
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
expect(res.body.error).toContain("not found");
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/agents/:id/reflections", () => {
|
||||
it("returns 500 or 503 when reflection service is not available", async () => {
|
||||
const res = await REQUEST(buildApp(), "POST", `/api/agents/${agentId}/reflections`);
|
||||
|
||||
// The reflection service requires the engine to be initialized
|
||||
// Without proper setup, it may return 500 or 503
|
||||
expect([500, 503]).toContain(res.status);
|
||||
});
|
||||
|
||||
it("returns 404 or 500 for non-existent agent", async () => {
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/agents/nonexistent-agent/reflections");
|
||||
|
||||
// The route either returns 404 (agent not found) or 500 (reflection service error)
|
||||
expect([404, 500]).toContain(res.status);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/agents/:id/performance", () => {
|
||||
it("returns 200 with performance summary for valid agent", async () => {
|
||||
const res = await GET(buildApp(), `/api/agents/${agentId}/performance`);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toHaveProperty("agentId");
|
||||
expect(res.body).toHaveProperty("totalTasksCompleted");
|
||||
expect(res.body).toHaveProperty("successRate");
|
||||
});
|
||||
|
||||
it("accepts windowMs query param", async () => {
|
||||
const res = await GET(buildApp(), `/api/agents/${agentId}/performance?windowMs=604800000`);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toHaveProperty("agentId");
|
||||
});
|
||||
|
||||
it("returns 404 for non-existent agent", async () => {
|
||||
const res = await GET(buildApp(), "/api/agents/nonexistent-agent/performance");
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
expect(res.body.error).toContain("not found");
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/agents/:id/reflection-context", () => {
|
||||
it("returns 500 or 503 when reflection service is not available", async () => {
|
||||
const res = await GET(buildApp(), `/api/agents/${agentId}/reflection-context`);
|
||||
|
||||
// The reflection service requires the engine to be initialized
|
||||
// Without proper setup, it may return 500 or 503
|
||||
expect([500, 503]).toContain(res.status);
|
||||
});
|
||||
|
||||
it("returns 404 or 500 for non-existent agent", async () => {
|
||||
const res = await GET(buildApp(), "/api/agents/nonexistent-agent/reflection-context");
|
||||
|
||||
// The route either returns 404 (agent not found) or 500 (reflection service error)
|
||||
expect([404, 500]).toContain(res.status);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9226,6 +9226,234 @@ Output ONLY the prompt text (no markdown, no explanations).`;
|
||||
*/
|
||||
router.get("/agents/:id/employees", getAgentEmployeesHandler);
|
||||
|
||||
// ── Agent Reflection Routes ──────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* GET /api/agents/:id/reflections/latest
|
||||
* Fetch the most recent reflection for an agent.
|
||||
* Must be registered before /agents/:id/reflections to avoid matching "latest" as a limit.
|
||||
* Response 200: AgentReflection | null — The most recent reflection or null
|
||||
* Response 404: { error: "Agent not found" } — When agent doesn't exist
|
||||
* { error: "No reflections found" } — When agent has no reflections
|
||||
*/
|
||||
router.get("/agents/:id/reflections/latest", async (req, res) => {
|
||||
try {
|
||||
const scopedStore = await getScopedStore(req);
|
||||
const { AgentStore, ReflectionStore } = await import("@fusion/core");
|
||||
const agentStore = new AgentStore({ rootDir: scopedStore.getFusionDir() });
|
||||
const reflectionStore = new ReflectionStore({ rootDir: scopedStore.getFusionDir() });
|
||||
await agentStore.init();
|
||||
|
||||
const agentId = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
|
||||
if (!agentId) {
|
||||
throw badRequest("Agent id is required");
|
||||
}
|
||||
|
||||
// Validate the agent exists
|
||||
const agent = await agentStore.getAgent(agentId);
|
||||
if (!agent) {
|
||||
throw notFound("Agent not found");
|
||||
}
|
||||
|
||||
const reflection = await reflectionStore.getLatestReflection(agentId);
|
||||
if (!reflection) {
|
||||
throw notFound("No reflections found");
|
||||
}
|
||||
|
||||
res.json(reflection);
|
||||
} catch (err: any) {
|
||||
if (err instanceof ApiError) {
|
||||
throw err;
|
||||
}
|
||||
rethrowAsApiError(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/agents/:id/reflections
|
||||
* List reflection history for an agent.
|
||||
* Query params: limit (optional, default 50)
|
||||
* Response 200: AgentReflection[] — Array of reflections
|
||||
* Response 404: { error: "Agent not found" } — When agent doesn't exist
|
||||
*/
|
||||
router.get("/agents/:id/reflections", async (req, res) => {
|
||||
try {
|
||||
const scopedStore = await getScopedStore(req);
|
||||
const { AgentStore, ReflectionStore } = await import("@fusion/core");
|
||||
const agentStore = new AgentStore({ rootDir: scopedStore.getFusionDir() });
|
||||
const reflectionStore = new ReflectionStore({ rootDir: scopedStore.getFusionDir() });
|
||||
await agentStore.init();
|
||||
|
||||
const agentId = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
|
||||
if (!agentId) {
|
||||
throw badRequest("Agent id is required");
|
||||
}
|
||||
|
||||
// Validate the agent exists
|
||||
const agent = await agentStore.getAgent(agentId);
|
||||
if (!agent) {
|
||||
throw notFound("Agent not found");
|
||||
}
|
||||
|
||||
// Parse limit from query params (default 50)
|
||||
const limitParam = req.query.limit;
|
||||
const limit = limitParam ? parseInt(String(limitParam), 10) : 50;
|
||||
|
||||
const reflections = await reflectionStore.getReflections(agentId, limit);
|
||||
res.json(reflections);
|
||||
} catch (err: any) {
|
||||
if (err instanceof ApiError) {
|
||||
throw err;
|
||||
}
|
||||
rethrowAsApiError(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/agents/:id/reflections
|
||||
* Trigger a manual reflection for an agent.
|
||||
* Response 201: AgentReflection — The created reflection
|
||||
* Response 404: { error: "Agent not found" } — When agent doesn't exist
|
||||
* Response 500: { error: message } — When reflection generation fails
|
||||
*/
|
||||
router.post("/agents/:id/reflections", async (req, res) => {
|
||||
try {
|
||||
const taskStore = await getScopedStore(req);
|
||||
const { AgentStore, ReflectionStore } = await import("@fusion/core");
|
||||
const { AgentReflectionService } = await import("@fusion/engine");
|
||||
const agentStore = new AgentStore({ rootDir: taskStore.getRootDir() });
|
||||
const reflectionStore = new ReflectionStore({ rootDir: taskStore.getRootDir() });
|
||||
await agentStore.init();
|
||||
|
||||
const agentId = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
|
||||
if (!agentId) {
|
||||
throw badRequest("Agent id is required");
|
||||
}
|
||||
|
||||
// Validate the agent exists
|
||||
const agent = await agentStore.getAgent(agentId);
|
||||
if (!agent) {
|
||||
throw notFound("Agent not found");
|
||||
}
|
||||
|
||||
// Create the reflection service and generate a reflection
|
||||
const reflectionService = new AgentReflectionService({
|
||||
agentStore,
|
||||
taskStore,
|
||||
reflectionStore,
|
||||
rootDir: taskStore.getRootDir(),
|
||||
});
|
||||
|
||||
const reflection = await reflectionService.generateReflection(agentId, "manual");
|
||||
|
||||
res.status(201).json(reflection);
|
||||
} catch (err: any) {
|
||||
if (err instanceof ApiError) {
|
||||
throw err;
|
||||
}
|
||||
rethrowAsApiError(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/agents/:id/performance
|
||||
* Get aggregated performance summary for an agent.
|
||||
* Query params: windowMs (optional, default 7 days)
|
||||
* Response 200: AgentPerformanceSummary
|
||||
* Response 404: { error: "Agent not found" } — When agent doesn't exist
|
||||
*/
|
||||
router.get("/agents/:id/performance", async (req, res) => {
|
||||
try {
|
||||
const scopedStore = await getScopedStore(req);
|
||||
const { AgentStore, ReflectionStore } = await import("@fusion/core");
|
||||
const agentStore = new AgentStore({ rootDir: scopedStore.getFusionDir() });
|
||||
const reflectionStore = new ReflectionStore({ rootDir: scopedStore.getFusionDir() });
|
||||
await agentStore.init();
|
||||
|
||||
const agentId = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
|
||||
if (!agentId) {
|
||||
throw badRequest("Agent id is required");
|
||||
}
|
||||
|
||||
// Validate the agent exists
|
||||
const agent = await agentStore.getAgent(agentId);
|
||||
if (!agent) {
|
||||
throw notFound("Agent not found");
|
||||
}
|
||||
|
||||
// Parse windowMs from query params (default 7 days)
|
||||
const windowMsParam = req.query.windowMs;
|
||||
const windowMs = windowMsParam ? parseInt(String(windowMsParam), 10) : undefined;
|
||||
|
||||
const summary = await reflectionStore.getPerformanceSummary(agentId, { windowMs });
|
||||
res.json(summary);
|
||||
} catch (err: any) {
|
||||
if (err instanceof ApiError) {
|
||||
throw err;
|
||||
}
|
||||
rethrowAsApiError(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/agents/:id/reflection-context
|
||||
* Get raw context for debugging agent reflections.
|
||||
* Response 200: { context: object } — The built reflection context
|
||||
* Response 404: { error: "Agent not found" } — When agent doesn't exist
|
||||
* Response 503: { error: "Reflection service not available" } — When engine not initialized
|
||||
*/
|
||||
router.get("/agents/:id/reflection-context", async (req, res) => {
|
||||
try {
|
||||
const taskStore = await getScopedStore(req);
|
||||
const { AgentStore, ReflectionStore } = await import("@fusion/core");
|
||||
const agentStore = new AgentStore({ rootDir: taskStore.getRootDir() });
|
||||
const reflectionStore = new ReflectionStore({ rootDir: taskStore.getRootDir() });
|
||||
await agentStore.init();
|
||||
|
||||
const agentId = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
|
||||
if (!agentId) {
|
||||
throw badRequest("Agent id is required");
|
||||
}
|
||||
|
||||
// Validate the agent exists
|
||||
const agent = await agentStore.getAgent(agentId);
|
||||
if (!agent) {
|
||||
throw notFound("Agent not found");
|
||||
}
|
||||
|
||||
// Check if AgentReflectionService is available
|
||||
let AgentReflectionService: any;
|
||||
try {
|
||||
const engine = await import("@fusion/engine");
|
||||
AgentReflectionService = engine.AgentReflectionService;
|
||||
} catch {
|
||||
res.status(503).json({ error: "Reflection service not available" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!AgentReflectionService) {
|
||||
res.status(503).json({ error: "Reflection service not available" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the service and build the context
|
||||
const reflectionService = new AgentReflectionService({
|
||||
agentStore,
|
||||
taskStore,
|
||||
reflectionStore,
|
||||
rootDir: taskStore.getRootDir(),
|
||||
});
|
||||
|
||||
const context = await reflectionService.buildReflectionContext(agentId);
|
||||
res.json({ context });
|
||||
} catch (err: any) {
|
||||
if (err instanceof ApiError) {
|
||||
throw err;
|
||||
}
|
||||
rethrowAsApiError(err);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Agent Generation Routes ──────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user