feat(FN-3321): add agent self-improvement service and heartbeat evaluation

This merge delivers agent self-improvement (FN-3321) — adding evaluation identity tools, wiring evaluation into the heartbeat loop, implementing a self-improvement service, and providing test coverage. It also expands the dashboard guide view with regression tests, adds org chart full-view mode with

Fusion-Task-Id: FN-3321
This commit is contained in:
Fusion
2026-05-05 03:44:01 -07:00
committed by gsxdsm
parent 02a0c48bad
commit 499417bc8e
14 changed files with 667 additions and 16 deletions

View File

@@ -11,7 +11,7 @@ import { appendFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/p
import { existsSync } from "node:fs";
import { createHash } from "node:crypto";
import { join, relative, resolve } from "node:path";
import type { AgentStore, AgentState, AgentCapability, AgentUpdateInput, TaskDocument, TaskDocumentCreateInput, TaskStore, RunMutationContext, MessageStore, Message, SourceType, Settings, ResearchRun, ResearchRunStatus, TaskCreateInput } from "@fusion/core";
import type { AgentStore, AgentState, AgentCapability, AgentUpdateInput, TaskDocument, TaskDocumentCreateInput, TaskStore, RunMutationContext, MessageStore, Message, SourceType, Settings, ResearchRun, ResearchRunStatus, TaskCreateInput, ReflectionStore } from "@fusion/core";
import { dailyMemoryPath, ensureOpenClawMemoryFiles, getMemoryBackendCapabilities, getProjectMemory, isEphemeralAgent, memoryLongTermPath, resolveMemoryBackend, resolveResearchSettings, resolveTitleSummarizerSettingsModel, scheduleQmdProjectMemoryRefresh, searchProjectMemory, shouldSkipBackgroundQmdRefresh, summarizeTitle } from "@fusion/core";
import { ResearchOrchestrator } from "./research-orchestrator.js";
import { ResearchProviderRegistry } from "./research/provider-registry.js";
@@ -19,6 +19,7 @@ import { ResearchStepRunner } from "./research-step-runner.js";
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
import { Type, type Static } from "@mariozechner/pi-ai";
import type { AgentReflectionService } from "./agent-reflection.js";
import { MAX_INSTRUCTIONS_TEXT_LENGTH, MAX_MEMORY_LENGTH, MAX_SOUL_LENGTH } from "./agent-instructions.js";
import { createLogger } from "./logger.js";
// ── Tool parameter schemas (canonical definitions) ────────────────────────
@@ -55,6 +56,14 @@ export const reflectOnPerformanceParams = Type.Object({
),
});
export const readEvaluationsParams = Type.Object({});
export const updateIdentityParams = Type.Object({
soul: Type.Optional(Type.String({ description: "Updated soul/personality text" })),
instructionsText: Type.Optional(Type.String({ description: "Updated operating instructions" })),
memory: Type.Optional(Type.String({ description: "Updated agent memory text" })),
});
export const listAgentsParams = Type.Object({
role: Type.Optional(
Type.String({ description: "Filter by agent role/capability (e.g., 'executor', 'reviewer', 'qa')" }),
@@ -957,6 +966,162 @@ export function createReflectOnPerformanceTool(
* @param agentStore - AgentStore for agent discovery
* @returns ToolDefinition for the `fn_list_agents` tool
*/
function formatScore(score: number | null | undefined): string {
if (typeof score !== "number" || !Number.isFinite(score)) return "n/a";
return score.toFixed(2);
}
function buildPreview(value: string, limit = 100): string {
const trimmed = value.trim();
return trimmed.length > limit ? `${trimmed.slice(0, limit)}` : trimmed;
}
export function createReadEvaluationsTool(
agentStore: AgentStore,
reflectionStore: ReflectionStore | undefined,
agentId: string,
): ToolDefinition {
return {
name: "fn_read_evaluations",
label: "Read Evaluations",
description: "Read your ratings, recent feedback, and reflection history to support self-improvement.",
parameters: readEvaluationsParams,
execute: async (_id: string, _params: Static<typeof readEvaluationsParams>) => {
const [summary, ratings] = await Promise.all([
agentStore.getRatingSummary(agentId),
agentStore.getRatings(agentId, { limit: 10 }),
]);
const latestReflection = reflectionStore
? await reflectionStore.getLatestReflection(agentId)
: null;
const reflections = reflectionStore
? await reflectionStore.getReflections(agentId, 5)
: [];
const hasRatings = ratings.length > 0 || summary.totalRatings > 0;
const hasReflections = Boolean(latestReflection) || reflections.length > 0;
if (!hasRatings && !hasReflections) {
return {
content: [{ type: "text" as const, text: "No evaluation data available yet." }],
details: {},
};
}
const lines: string[] = [
"Evaluation Summary",
`- Average score: ${formatScore(summary.averageScore)}`,
`- Trend: ${summary.trend}`,
`- Total ratings: ${summary.totalRatings}`,
];
const categoryEntries = Object.entries(summary.categoryAverages ?? {});
if (categoryEntries.length > 0) {
lines.push("", "Category averages:");
for (const [category, score] of categoryEntries) {
lines.push(`- ${category}: ${formatScore(score)}`);
}
}
const commentedRatings = ratings.filter((rating) => rating.comment?.trim());
if (commentedRatings.length > 0) {
lines.push("", "Recent rating comments:");
for (const rating of commentedRatings.slice(0, 5)) {
lines.push(`- [${rating.score}/5] ${rating.comment!.trim()}`);
}
}
if (latestReflection) {
lines.push("", "Latest reflection:", `- Summary: ${latestReflection.summary}`);
if (latestReflection.insights.length > 0) {
lines.push("- Insights:");
latestReflection.insights.forEach((insight) => lines.push(` - ${insight}`));
}
if (latestReflection.suggestedImprovements.length > 0) {
lines.push("- Suggested improvements:");
latestReflection.suggestedImprovements.forEach((item) => lines.push(` - ${item}`));
}
}
if (reflections.length > 0) {
lines.push("", "Recent reflection history:");
for (const reflection of reflections.slice(0, 5)) {
lines.push(`- ${reflection.timestamp}: ${reflection.summary}`);
}
}
return {
content: [{ type: "text" as const, text: lines.join("\n") }],
details: {},
};
},
};
}
export function createUpdateIdentityTool(agentStore: AgentStore, agentId: string): ToolDefinition {
return {
name: "fn_update_identity",
label: "Update Identity",
description: "Update your own soul, instructionsText, or memory fields based on evaluation feedback.",
parameters: updateIdentityParams,
execute: async (_id: string, params: Static<typeof updateIdentityParams>) => {
const updates: AgentUpdateInput = {};
if (params.soul !== undefined) {
const soul = params.soul.trim();
if (soul.length > MAX_SOUL_LENGTH) {
return {
content: [{ type: "text" as const, text: `ERROR: soul exceeds ${MAX_SOUL_LENGTH} character limit` }],
details: {},
};
}
updates.soul = soul;
}
if (params.instructionsText !== undefined) {
const instructionsText = params.instructionsText.trim();
if (instructionsText.length > MAX_INSTRUCTIONS_TEXT_LENGTH) {
return {
content: [{ type: "text" as const, text: `ERROR: instructionsText exceeds ${MAX_INSTRUCTIONS_TEXT_LENGTH} character limit` }],
details: {},
};
}
updates.instructionsText = instructionsText;
}
if (params.memory !== undefined) {
const memory = params.memory.trim();
if (memory.length > MAX_MEMORY_LENGTH) {
return {
content: [{ type: "text" as const, text: `ERROR: memory exceeds ${MAX_MEMORY_LENGTH} character limit` }],
details: {},
};
}
updates.memory = memory;
}
if (Object.keys(updates).length === 0) {
return {
content: [{ type: "text" as const, text: "ERROR: Provide at least one field to update" }],
details: {},
};
}
await agentStore.updateAgent(agentId, updates);
const confirmations = Object.entries(updates).map(([key, value]) => `- ${key}: ${buildPreview(String(value))}`);
return {
content: [{
type: "text" as const,
text: `Updated identity fields:\n${confirmations.join("\n")}`,
}],
details: { updatedFields: Object.keys(updates) },
};
},
};
}
export function createListAgentsTool(agentStore: AgentStore): ToolDefinition {
return {
name: "fn_list_agents",