feat(FN-1184): add agent rating persistence and summary APIs

- Add AgentRating, AgentRatingSummary, and AgentRatingInput types and re-export them from @fusion/core
- Bump core schema to v14 with an agentRatings table plus agentId/createdAt indexes
- Implement AgentStore rating methods for add/list/filter/summary/delete with score validation and rating:added events
- Extend database and agent-store tests to cover migration, rating CRUD behavior, filtering, limits, and trend calculation
This commit is contained in:
gsxdsm
2026-04-08 05:20:29 -07:00
parent 8e8fc07dc1
commit 844d43c017
6 changed files with 438 additions and 12 deletions

View File

@@ -1311,6 +1311,196 @@ describe("AgentStore", () => {
}); });
}); });
describe("rating methods", () => {
const addSequencedRatings = async (
agentId: string,
scores: number[],
inputOverrides?: Partial<{ category: string; comment: string; runId: string; taskId: string; raterId: string }>,
) => {
vi.useFakeTimers();
const base = new Date("2026-01-01T00:00:00.000Z").getTime();
try {
const ratings = [];
for (let i = 0; i < scores.length; i++) {
vi.setSystemTime(new Date(base + i * 1000));
ratings.push(
await store.addRating(agentId, {
raterType: "user",
score: scores[i],
...inputOverrides,
}),
);
}
return ratings;
} finally {
vi.useRealTimers();
}
};
it("addRating creates a rating and emits rating:added", async () => {
const agent = await store.createAgent({ name: "Rated Agent", role: "executor" });
const handler = vi.fn();
store.on("rating:added", handler);
const rating = await store.addRating(agent.id, {
raterType: "user",
score: 5,
comment: "Great run",
});
expect(rating.id).toMatch(/^rating-[a-f0-9]{8}$/);
expect(rating.agentId).toBe(agent.id);
expect(rating.raterType).toBe("user");
expect(rating.score).toBe(5);
expect(rating.comment).toBe("Great run");
expect(new Date(rating.createdAt).getTime()).not.toBeNaN();
expect(handler).toHaveBeenCalledOnce();
expect(handler).toHaveBeenCalledWith(rating);
});
it("addRating rejects scores outside 1..5", async () => {
const agent = await store.createAgent({ name: "Validator", role: "reviewer" });
await expect(
store.addRating(agent.id, { raterType: "system", score: 0 }),
).rejects.toThrow("Rating score must be between 1 and 5");
await expect(
store.addRating(agent.id, { raterType: "system", score: 6 }),
).rejects.toThrow("Rating score must be between 1 and 5");
});
it("addRating stores all optional fields", async () => {
const agent = await store.createAgent({ name: "Optional Fields", role: "executor" });
const rating = await store.addRating(agent.id, {
raterType: "agent",
raterId: "agent-rater",
score: 4,
category: "quality",
comment: "Strong implementation",
runId: "run-123",
taskId: "FN-1000",
});
expect(rating.raterId).toBe("agent-rater");
expect(rating.category).toBe("quality");
expect(rating.comment).toBe("Strong implementation");
expect(rating.runId).toBe("run-123");
expect(rating.taskId).toBe("FN-1000");
});
it("getRatings returns ratings ordered by createdAt desc", async () => {
const agent = await store.createAgent({ name: "Order Agent", role: "executor" });
const created = await addSequencedRatings(agent.id, [2, 3, 5]);
const ratings = await store.getRatings(agent.id);
expect(ratings.map((rating) => rating.id)).toEqual([
created[2].id,
created[1].id,
created[0].id,
]);
});
it("getRatings applies category filter", async () => {
const agent = await store.createAgent({ name: "Category Agent", role: "executor" });
await store.addRating(agent.id, { raterType: "user", score: 4, category: "quality" });
await store.addRating(agent.id, { raterType: "user", score: 2, category: "speed" });
await store.addRating(agent.id, { raterType: "user", score: 5, category: "quality" });
const ratings = await store.getRatings(agent.id, { category: "quality" });
expect(ratings).toHaveLength(2);
expect(ratings.every((rating) => rating.category === "quality")).toBe(true);
});
it("getRatings respects the limit option", async () => {
const agent = await store.createAgent({ name: "Limit Agent", role: "executor" });
await addSequencedRatings(agent.id, [1, 2, 3, 4]);
const ratings = await store.getRatings(agent.id, { limit: 2 });
expect(ratings).toHaveLength(2);
expect(ratings[0].score).toBe(4);
expect(ratings[1].score).toBe(3);
});
it("getRatingSummary returns an empty summary when no ratings exist", async () => {
const agent = await store.createAgent({ name: "Empty Summary", role: "executor" });
const summary = await store.getRatingSummary(agent.id);
expect(summary).toEqual({
agentId: agent.id,
averageScore: 0,
totalRatings: 0,
categoryAverages: {},
recentRatings: [],
trend: "insufficient-data",
});
});
it("getRatingSummary computes averages and categoryAverages", async () => {
const agent = await store.createAgent({ name: "Summary Agent", role: "executor" });
await addSequencedRatings(agent.id, [5], { category: "quality" });
await addSequencedRatings(agent.id, [3], { category: "quality" });
await addSequencedRatings(agent.id, [4], { category: "speed" });
await addSequencedRatings(agent.id, [2]);
const summary = await store.getRatingSummary(agent.id);
expect(summary.averageScore).toBe(3.5);
expect(summary.totalRatings).toBe(4);
expect(summary.categoryAverages).toEqual({
quality: 4,
speed: 4,
});
expect(summary.recentRatings).toHaveLength(4);
expect(summary.trend).toBe("insufficient-data");
});
it("getRatingSummary trend is improving when recent average is higher", async () => {
const agent = await store.createAgent({ name: "Improving Agent", role: "executor" });
await addSequencedRatings(agent.id, [1, 1, 2, 2, 2, 4, 4, 5, 5, 5]);
const summary = await store.getRatingSummary(agent.id);
expect(summary.trend).toBe("improving");
});
it("getRatingSummary trend is declining when recent average is lower", async () => {
const agent = await store.createAgent({ name: "Declining Agent", role: "executor" });
await addSequencedRatings(agent.id, [5, 5, 4, 4, 4, 2, 2, 1, 1, 1]);
const summary = await store.getRatingSummary(agent.id);
expect(summary.trend).toBe("declining");
});
it("getRatingSummary trend is stable when windows are approximately equal", async () => {
const agent = await store.createAgent({ name: "Stable Agent", role: "executor" });
await addSequencedRatings(agent.id, [3, 3, 3, 3, 3, 3, 3, 3, 3, 3]);
const summary = await store.getRatingSummary(agent.id);
expect(summary.trend).toBe("stable");
});
it("deleteRating removes the rating", async () => {
const agent = await store.createAgent({ name: "Delete Agent", role: "executor" });
const first = await store.addRating(agent.id, { raterType: "user", score: 4 });
await store.addRating(agent.id, { raterType: "user", score: 5 });
await store.deleteRating(first.id);
const ratings = await store.getRatings(agent.id);
expect(ratings).toHaveLength(1);
expect(ratings[0].id).not.toBe(first.id);
});
});
// ── heartbeat lifecycle via updateAgentState ────────────────────── // ── heartbeat lifecycle via updateAgentState ──────────────────────
describe("heartbeat lifecycle via updateAgentState", () => { describe("heartbeat lifecycle via updateAgentState", () => {

View File

@@ -35,9 +35,13 @@ import type {
AgentAccessState, AgentAccessState,
OrgTreeNode, OrgTreeNode,
InstructionsBundleConfig, InstructionsBundleConfig,
AgentRating,
AgentRatingSummary,
AgentRatingInput,
} from "./types.js"; } from "./types.js";
import { AGENT_VALID_TRANSITIONS, agentToConfigSnapshot, diffConfigSnapshots } from "./types.js"; import { AGENT_VALID_TRANSITIONS, agentToConfigSnapshot, diffConfigSnapshots } from "./types.js";
import { computeAccessState } from "./agent-permissions.js"; import { computeAccessState } from "./agent-permissions.js";
import { Database } from "./db.js";
/** Events emitted by AgentStore */ /** Events emitted by AgentStore */
export interface AgentStoreEvents { export interface AgentStoreEvents {
@@ -55,6 +59,8 @@ export interface AgentStoreEvents {
"agent:configRevision": (agentId: string, revision: AgentConfigRevision) => void; "agent:configRevision": (agentId: string, revision: AgentConfigRevision) => void;
/** Emitted when a task is assigned to an agent (taskId is non-empty) */ /** Emitted when a task is assigned to an agent (taskId is non-empty) */
"agent:assigned": (agent: Agent, taskId: string) => void; "agent:assigned": (agent: Agent, taskId: string) => void;
/** Emitted when a rating is added */
"rating:added": (rating: AgentRating) => void;
} }
type TypedEventEmitter<Events extends Record<string, unknown[]>> = { type TypedEventEmitter<Events extends Record<string, unknown[]>> = {
@@ -108,6 +114,7 @@ export class AgentStore extends EventEmitter {
private rootDir: string; private rootDir: string;
private agentsDir: string; private agentsDir: string;
private locks: Map<string, AgentLock> = new Map(); private locks: Map<string, AgentLock> = new Map();
private _db: Database | null = null;
constructor(options: AgentStoreOptions = {}) { constructor(options: AgentStoreOptions = {}) {
super(); super();
@@ -115,11 +122,20 @@ export class AgentStore extends EventEmitter {
this.agentsDir = join(this.rootDir, "agents"); this.agentsDir = join(this.rootDir, "agents");
} }
private get db(): Database {
if (!this._db) {
this._db = new Database(this.rootDir);
this._db.init();
}
return this._db;
}
/** /**
* Initialize the store by creating necessary directories. * Initialize the store by creating necessary directories.
* Should be called before other operations. * Should be called before other operations.
*/ */
async init(): Promise<void> { async init(): Promise<void> {
const _ = this.db;
await mkdir(this.agentsDir, { recursive: true }); await mkdir(this.agentsDir, { recursive: true });
} }
@@ -219,6 +235,146 @@ export class AgentStore extends EventEmitter {
}; };
} }
private mapRatingRow(row: any): AgentRating {
return {
id: row.id,
agentId: row.agentId,
raterType: row.raterType,
raterId: row.raterId ?? undefined,
score: row.score,
category: row.category ?? undefined,
comment: row.comment ?? undefined,
runId: row.runId ?? undefined,
taskId: row.taskId ?? undefined,
createdAt: row.createdAt,
};
}
async addRating(agentId: string, input: AgentRatingInput): Promise<AgentRating> {
if (input.score < 1 || input.score > 5) {
throw new Error("Rating score must be between 1 and 5");
}
const rating: AgentRating = {
id: `rating-${randomUUID().slice(0, 8)}`,
agentId,
raterType: input.raterType,
raterId: input.raterId,
score: input.score,
category: input.category,
comment: input.comment,
runId: input.runId,
taskId: input.taskId,
createdAt: new Date().toISOString(),
};
this.db.prepare(`
INSERT INTO agentRatings (id, agentId, raterType, raterId, score, category, comment, runId, taskId, createdAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
rating.id,
rating.agentId,
rating.raterType,
rating.raterId ?? null,
rating.score,
rating.category ?? null,
rating.comment ?? null,
rating.runId ?? null,
rating.taskId ?? null,
rating.createdAt,
);
this.db.bumpLastModified();
this.emit("rating:added", rating);
return rating;
}
async getRatings(agentId: string, options?: { limit?: number; category?: string }): Promise<AgentRating[]> {
const params: Array<string | number> = [agentId];
let query = "SELECT * FROM agentRatings WHERE agentId = ?";
if (options?.category !== undefined) {
query += " AND category = ?";
params.push(options.category);
}
query += " ORDER BY createdAt DESC";
if (options?.limit !== undefined) {
query += " LIMIT ?";
params.push(options.limit);
}
const rows = this.db.prepare(query).all(...params);
return rows.map((row) => this.mapRatingRow(row));
}
async getRatingSummary(agentId: string): Promise<AgentRatingSummary> {
const ratings = await this.getRatings(agentId);
if (ratings.length === 0) {
return {
agentId,
averageScore: 0,
totalRatings: 0,
categoryAverages: {},
recentRatings: [],
trend: "insufficient-data",
};
}
const averageScore = Math.round((ratings.reduce((sum, rating) => sum + rating.score, 0) / ratings.length) * 100) / 100;
const categoryBuckets = new Map<string, { total: number; count: number }>();
for (const rating of ratings) {
if (rating.category === undefined) {
continue;
}
const existing = categoryBuckets.get(rating.category) ?? { total: 0, count: 0 };
existing.total += rating.score;
existing.count += 1;
categoryBuckets.set(rating.category, existing);
}
const categoryAverages: Record<string, number> = {};
for (const [category, bucket] of categoryBuckets) {
categoryAverages[category] = Math.round((bucket.total / bucket.count) * 100) / 100;
}
const recentRatings = ratings.slice(0, 10);
let trend: AgentRatingSummary["trend"] = "insufficient-data";
if (ratings.length >= 10) {
const recentWindow = ratings.slice(0, 5);
const previousWindow = ratings.slice(5, 10);
const recentAvg = recentWindow.reduce((sum, rating) => sum + rating.score, 0) / recentWindow.length;
const previousAvg = previousWindow.reduce((sum, rating) => sum + rating.score, 0) / previousWindow.length;
if (Math.abs(recentAvg - previousAvg) <= 0.01) {
trend = "stable";
} else if (recentAvg > previousAvg) {
trend = "improving";
} else {
trend = "declining";
}
}
return {
agentId,
averageScore,
totalRatings: ratings.length,
categoryAverages,
recentRatings,
trend,
};
}
async deleteRating(ratingId: string): Promise<void> {
this.db.prepare("DELETE FROM agentRatings WHERE id = ?").run(ratingId);
this.db.bumpLastModified();
}
/** /**
* Get the managed instructions directory path for an agent. * Get the managed instructions directory path for an agent.
* Does not create the directory. * Does not create the directory.

View File

@@ -71,6 +71,7 @@ describe("Database", () => {
expect(tableNames).toContain("mission_features"); expect(tableNames).toContain("mission_features");
expect(tableNames).toContain("ai_sessions"); expect(tableNames).toContain("ai_sessions");
expect(tableNames).toContain("messages"); expect(tableNames).toContain("messages");
expect(tableNames).toContain("agentRatings");
}); });
it("creates all expected indexes", () => { it("creates all expected indexes", () => {
@@ -90,10 +91,12 @@ describe("Database", () => {
expect(indexNames).toContain("idxMessagesCreatedAt"); expect(indexNames).toContain("idxMessagesCreatedAt");
expect(indexNames).toContain("idxMessagesFrom"); expect(indexNames).toContain("idxMessagesFrom");
expect(indexNames).toContain("idxMessagesTo"); expect(indexNames).toContain("idxMessagesTo");
expect(indexNames).toContain("idxAgentRatingsAgentId");
expect(indexNames).toContain("idxAgentRatingsCreatedAt");
}); });
it("seeds schema version", () => { it("seeds schema version", () => {
expect(db.getSchemaVersion()).toBe(13); expect(db.getSchemaVersion()).toBe(14);
}); });
it("seeds lastModified", () => { it("seeds lastModified", () => {
@@ -116,7 +119,7 @@ describe("Database", () => {
it("is idempotent - calling init() twice does not fail", () => { it("is idempotent - calling init() twice does not fail", () => {
expect(() => db.init()).not.toThrow(); expect(() => db.init()).not.toThrow();
expect(db.getSchemaVersion()).toBe(13); expect(db.getSchemaVersion()).toBe(14);
}); });
it("does not overwrite existing config on re-init", () => { it("does not overwrite existing config on re-init", () => {
@@ -723,7 +726,7 @@ describe("schema migrations", () => {
db.init(); db.init();
// Verify version bumped to 5 (includes v1→v2, v2→v3, v3→v4, and v4→v5 migrations) // Verify version bumped to 5 (includes v1→v2, v2→v3, v3→v4, and v4→v5 migrations)
expect(db.getSchemaVersion()).toBe(13); expect(db.getSchemaVersion()).toBe(14);
// Verify new columns exist and existing data is intact // Verify new columns exist and existing data is intact
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>; const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
@@ -748,11 +751,35 @@ describe("schema migrations", () => {
const db = new Database(kbDir); const db = new Database(kbDir);
db.init(); db.init();
expect(db.getSchemaVersion()).toBe(13); expect(db.getSchemaVersion()).toBe(14);
// Re-init should not fail // Re-init should not fail
db.init(); db.init();
expect(db.getSchemaVersion()).toBe(13); expect(db.getSchemaVersion()).toBe(14);
db.close();
});
it("applies migration 14 by creating agentRatings table and indexes", () => {
tmpDir = makeTmpDir();
const kbDir = join(tmpDir, ".fusion");
const db = new Database(kbDir);
db.exec("CREATE TABLE IF NOT EXISTS __meta (key TEXT PRIMARY KEY, value TEXT)");
db.exec("INSERT INTO __meta (key, value) VALUES ('schemaVersion', '13')");
db.exec("INSERT INTO __meta (key, value) VALUES ('lastModified', '1000')");
db.init();
expect(db.getSchemaVersion()).toBe(14);
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'agentRatings'").all() as Array<{ name: string }>;
expect(tables).toEqual([{ name: "agentRatings" }]);
const indexes = db.prepare("SELECT name FROM sqlite_master WHERE type='index' AND tbl_name = 'agentRatings' ORDER BY name").all() as Array<{ name: string }>;
const indexNames = indexes.map((index) => index.name);
expect(indexNames).toContain("idxAgentRatingsAgentId");
expect(indexNames).toContain("idxAgentRatingsCreatedAt");
db.close(); db.close();
}); });
@@ -847,7 +874,7 @@ describe("schema migrations", () => {
db.init(); db.init();
// Verify version bumped to 5 // Verify version bumped to 5
expect(db.getSchemaVersion()).toBe(13); expect(db.getSchemaVersion()).toBe(14);
// Verify new columns exist and existing data is intact // Verify new columns exist and existing data is intact
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>; const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
@@ -1057,7 +1084,7 @@ describe("createDatabase factory", () => {
const db = createDatabase(kbDir); const db = createDatabase(kbDir);
db.init(); db.init();
expect(db.getSchemaVersion()).toBe(13); expect(db.getSchemaVersion()).toBe(14);
expect(db.getLastModified()).toBeGreaterThan(0); expect(db.getLastModified()).toBeGreaterThan(0);
db.close(); db.close();

View File

@@ -59,7 +59,7 @@ export function fromJson<T>(json: string | null | undefined): T | undefined {
// ── Schema Definition ──────────────────────────────────────────────── // ── Schema Definition ────────────────────────────────────────────────
const SCHEMA_VERSION = 13; const SCHEMA_VERSION = 14;
function normalizeTaskComments( function normalizeTaskComments(
steeringComments: SteeringComment[] | undefined, steeringComments: SteeringComment[] | undefined,
@@ -451,9 +451,6 @@ export class Database {
}); });
} }
// Future migrations go here:
// if (version < 14) { this.applyMigration(14, () => { ... }); }
if (version < 10) { if (version < 10) {
this.applyMigration(10, () => { this.applyMigration(10, () => {
this.addColumnIfMissing("missions", "autopilotEnabled", "INTEGER DEFAULT 0"); this.addColumnIfMissing("missions", "autopilotEnabled", "INTEGER DEFAULT 0");
@@ -498,6 +495,27 @@ export class Database {
this.db.exec(`CREATE INDEX IF NOT EXISTS idxTasksAssignedAgentId ON tasks(assignedAgentId)`); this.db.exec(`CREATE INDEX IF NOT EXISTS idxTasksAssignedAgentId ON tasks(assignedAgentId)`);
}); });
} }
if (version < 14) {
this.applyMigration(14, () => {
this.db.exec(`
CREATE TABLE IF NOT EXISTS agentRatings (
id TEXT PRIMARY KEY,
agentId TEXT NOT NULL,
raterType TEXT NOT NULL,
raterId TEXT,
score INTEGER NOT NULL CHECK(score BETWEEN 1 AND 5),
category TEXT,
comment TEXT,
runId TEXT,
taskId TEXT,
createdAt TEXT NOT NULL
)
`);
this.db.exec(`CREATE INDEX IF NOT EXISTS idxAgentRatingsAgentId ON agentRatings(agentId)`);
this.db.exec(`CREATE INDEX IF NOT EXISTS idxAgentRatingsCreatedAt ON agentRatings(createdAt)`);
});
}
} }
/** /**

View File

@@ -1,5 +1,5 @@
export { COLUMNS, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, AGENT_PERMISSIONS, agentToConfigSnapshot, diffConfigSnapshots } from "./types.js"; export { COLUMNS, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, AGENT_PERMISSIONS, agentToConfigSnapshot, diffConfigSnapshots } from "./types.js";
export type { Column, IssueInfo, IssueState, PrInfo, PrStatus, Task, TaskAttachment, TaskComment, TaskCommentInput, TaskCreateInput, TaskDetail, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, MergeDetails, MergeResult, Settings, GlobalSettings, ProjectSettings, SettingsScope, TaskStep, StepStatus, TaskLogEntry, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, HeartbeatInvocationSource, AgentTaskSession, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, Mailbox } from "./types.js"; export type { Column, IssueInfo, IssueState, PrInfo, PrStatus, Task, TaskAttachment, TaskComment, TaskCommentInput, TaskCreateInput, TaskDetail, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, MergeDetails, MergeResult, Settings, GlobalSettings, ProjectSettings, SettingsScope, TaskStep, StepStatus, TaskLogEntry, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, Mailbox } from "./types.js";
export { AGENT_VALID_TRANSITIONS } from "./types.js"; export { AGENT_VALID_TRANSITIONS } from "./types.js";
export { export {
BUILTIN_AGENT_PROMPTS, BUILTIN_AGENT_PROMPTS,

View File

@@ -1728,6 +1728,41 @@ export interface AgentTaskSession {
updatedAt: string; updatedAt: string;
} }
/** A single performance rating for an agent */
export interface AgentRating {
id: string;
agentId: string;
raterType: "user" | "agent" | "system";
raterId?: string;
score: number;
category?: string;
comment?: string;
runId?: string;
taskId?: string;
createdAt: string;
}
/** Aggregated rating statistics for an agent */
export interface AgentRatingSummary {
agentId: string;
averageScore: number;
totalRatings: number;
categoryAverages: Record<string, number>;
recentRatings: AgentRating[];
trend: "improving" | "declining" | "stable" | "insufficient-data";
}
/** Input payload for creating an agent rating */
export interface AgentRatingInput {
raterType: "user" | "agent" | "system";
raterId?: string;
score: number;
category?: string;
comment?: string;
runId?: string;
taskId?: string;
}
/** Trackable configuration fields for revision history. /** Trackable configuration fields for revision history.
* Excludes budget-related items, state, taskId, token counts, and timestamps. */ * Excludes budget-related items, state, taskId, token counts, and timestamps. */
export interface AgentConfigSnapshot { export interface AgentConfigSnapshot {