## Summary Wave 10 of package code organization (plan: `docs/plans/2026-07-14-001-refactor-package-code-organization-plan.md`), after #2274. ### Peels | New module | Parent | |---|---| | `types/agents.ts` | `types.ts` (permissions, Agent entity, ratings, reflections, heartbeat run types) | | `app/api/missions.ts` | `legacy.ts` (hierarchy, assertions, validation, autopilot) | | `app/api/messaging.ts` | `legacy.ts` (mailbox, approvals, reflections/ratings, budget) | | `app/api/plugins-and-skills.ts` | `legacy.ts` | | `app/api/todo.ts` | `legacy.ts` | | `app/api/insights.ts` | `legacy.ts` | | `app/api/system-panel.ts` | `legacy.ts` | | `task-store/workflow-definitions.ts` | rename of `remaining-ops-8` | Mission interview SSE streams stay in `legacy.ts` until `createResilientEventSource` is shared. Public paths stay stable via re-exports. ### LOC - `types.ts` ~7101 → ~6165 - `legacy.ts` ~10273 → ~8913 ### Shims - `types.ts` → `types/agents.ts` (delete-when: consumers import agents domain directly) - `legacy.ts` → peels above (delete-when: dashboard imports domain modules) - `remaining-ops-8` → `workflow-definitions` (rename complete) ## Test plan - [x] `@fusion/core` typecheck - [x] eslint on peeled dashboard API modules - [x] `pnpm check:line-count` - [x] `agent-permissions` + `agent-permission-policy` tests - [x] `plugin-setup-api` tests - [ ] CI merge gate <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added typed dashboard API support for missions, milestones/features, validation loops, and autopilot. * Added insights browsing and management, including run triggering. * Added messaging/mailboxes and approvals, plus agent reflections, ratings, and budget controls. * Added plugins & skills management and discovery. * Added todo lists and items with reordering. * Added system monitoring controls: rebuilds/restarts, logs, and research finding promotion. * **Improvements** * Centralized agent-related type contracts for safer browser consumption. * Enhanced legacy API compatibility and improved AI session deletion error handling. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
327 lines
12 KiB
TypeScript
327 lines
12 KiB
TypeScript
/**
|
|
* FNXC:CodeOrganization 2026-07-18-14:00:
|
|
* Mailbox, approvals, agent reflections/ratings, and budget client API peeled from legacy.ts.
|
|
*/
|
|
import type {
|
|
Message,
|
|
MessageMetadata,
|
|
MessageType,
|
|
ParticipantType,
|
|
AgentRating,
|
|
AgentRatingSummary,
|
|
AgentRatingInput,
|
|
AgentReflection,
|
|
AgentPerformanceSummary,
|
|
AgentBudgetStatus,
|
|
ApprovalRequestStatus,
|
|
} from "@fusion/core";
|
|
import { api } from "./client.js";
|
|
import { withProjectId } from "./health.js";
|
|
|
|
// ── Messages API ──────────────────────────────────────────────────────────
|
|
|
|
/** Response shape for GET /messages/inbox */
|
|
export interface InboxResponse {
|
|
messages: Message[];
|
|
total: number;
|
|
unreadCount: number;
|
|
}
|
|
|
|
/** Response shape for GET /messages/outbox */
|
|
export interface OutboxResponse {
|
|
messages: Message[];
|
|
total: number;
|
|
}
|
|
|
|
/** Response shape for GET /messages/unread-count */
|
|
export interface UnreadCountResponse {
|
|
unreadCount: number;
|
|
pendingApprovalCount?: number;
|
|
}
|
|
|
|
/** Response shape for POST /messages/read-all */
|
|
export interface MarkAllReadResponse {
|
|
markedAsRead: number;
|
|
}
|
|
|
|
/** Response shape for GET /agents/:id/mailbox */
|
|
export interface AgentMailboxResponse {
|
|
ownerId: string;
|
|
ownerType: ParticipantType;
|
|
unreadCount: number;
|
|
lastMessage?: Message;
|
|
messages: Message[]; // Backward compat alias for inbox
|
|
inbox: Message[];
|
|
outbox: Message[];
|
|
}
|
|
|
|
/** Response shape for GET /agents/mailbox/all */
|
|
export interface AllAgentsMailboxResponse {
|
|
messages: Message[];
|
|
total: number;
|
|
unreadCount: number;
|
|
}
|
|
|
|
/** Input for sending a message via the dashboard */
|
|
export interface SendMessageInput {
|
|
toId: string;
|
|
toType: ParticipantType;
|
|
content: string;
|
|
type: MessageType;
|
|
metadata?: MessageMetadata;
|
|
wakeImmediately?: boolean;
|
|
}
|
|
|
|
export interface ApprovalRequestSummary {
|
|
id: string;
|
|
status: ApprovalRequestStatus;
|
|
actionCategory: string;
|
|
actionSummary: string;
|
|
agentId: string;
|
|
taskId?: string;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
decidedAt?: string;
|
|
decidedBy?: string;
|
|
}
|
|
|
|
export interface ApprovalRequestDetail extends ApprovalRequestSummary {
|
|
requester: {
|
|
actorId: string;
|
|
actorType: "agent" | "user" | "system";
|
|
actorName: string;
|
|
};
|
|
runId?: string;
|
|
requestedAt: string;
|
|
completedAt?: string;
|
|
targetAction: {
|
|
category: string;
|
|
action: string;
|
|
summary: string;
|
|
resourceType: string;
|
|
resourceId: string;
|
|
context?: Record<string, unknown>;
|
|
};
|
|
history: Array<{
|
|
id: string;
|
|
eventType: string;
|
|
actor: {
|
|
actorId: string;
|
|
actorType: "agent" | "user" | "system";
|
|
actorName: string;
|
|
};
|
|
note?: string;
|
|
createdAt: string;
|
|
}>;
|
|
}
|
|
|
|
export interface ApprovalListResponse {
|
|
requests: ApprovalRequestSummary[];
|
|
total: number;
|
|
pendingCount: number;
|
|
}
|
|
|
|
/** Fetch inbox messages for the current user. */
|
|
export function fetchInbox(
|
|
options?: { limit?: number; offset?: number; unreadOnly?: boolean; type?: MessageType },
|
|
projectId?: string,
|
|
): Promise<InboxResponse> {
|
|
const params = new URLSearchParams();
|
|
if (options?.limit !== undefined) params.set("limit", String(options.limit));
|
|
if (options?.offset !== undefined) params.set("offset", String(options.offset));
|
|
if (options?.unreadOnly) params.set("unreadOnly", "true");
|
|
if (options?.type) params.set("type", options.type);
|
|
if (projectId) params.set("projectId", projectId);
|
|
const query = params.size > 0 ? `?${params.toString()}` : "";
|
|
return api<InboxResponse>(`/messages/inbox${query}`);
|
|
}
|
|
|
|
/** Fetch sent messages for the current user. */
|
|
export function fetchOutbox(
|
|
options?: { limit?: number; offset?: number; type?: MessageType },
|
|
projectId?: string,
|
|
): Promise<OutboxResponse> {
|
|
const params = new URLSearchParams();
|
|
if (options?.limit !== undefined) params.set("limit", String(options.limit));
|
|
if (options?.offset !== undefined) params.set("offset", String(options.offset));
|
|
if (options?.type) params.set("type", options.type);
|
|
if (projectId) params.set("projectId", projectId);
|
|
const query = params.size > 0 ? `?${params.toString()}` : "";
|
|
return api<OutboxResponse>(`/messages/outbox${query}`);
|
|
}
|
|
|
|
/** Fetch unread message count (lightweight, for header badge). */
|
|
export function fetchUnreadCount(projectId?: string): Promise<UnreadCountResponse> {
|
|
return api<UnreadCountResponse>(withProjectId("/messages/unread-count", projectId));
|
|
}
|
|
|
|
/** Fetch a single message by ID. */
|
|
export function fetchMessage(id: string, projectId?: string): Promise<Message> {
|
|
return api<Message>(withProjectId(`/messages/${encodeURIComponent(id)}`, projectId));
|
|
}
|
|
|
|
/** Send a new message. */
|
|
export function sendMessage(input: SendMessageInput, projectId?: string): Promise<Message> {
|
|
return api<Message>(withProjectId("/messages", projectId), {
|
|
method: "POST",
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
|
|
/** Materialize an operator-approved task proposal exactly once. */
|
|
export function createProposedTask(id: string, projectId?: string): Promise<{ task: import("@fusion/core").Task; proposal: Message }> {
|
|
return api(withProjectId(`/messages/${encodeURIComponent(id)}/create-proposed-task`, projectId), { method: "POST" });
|
|
}
|
|
|
|
/** Mark a specific message as read. */
|
|
export function markMessageRead(id: string, projectId?: string): Promise<Message> {
|
|
return api<Message>(withProjectId(`/messages/${encodeURIComponent(id)}/read`, projectId), {
|
|
method: "POST",
|
|
});
|
|
}
|
|
|
|
/** Mark all inbox messages as read. */
|
|
export function markAllMessagesRead(projectId?: string): Promise<MarkAllReadResponse> {
|
|
return api<MarkAllReadResponse>(withProjectId("/messages/read-all", projectId), {
|
|
method: "POST",
|
|
});
|
|
}
|
|
|
|
/** Delete a message. */
|
|
export function deleteMessage(id: string, projectId?: string): Promise<void> {
|
|
return api<void>(withProjectId(`/messages/${encodeURIComponent(id)}`, projectId), {
|
|
method: "DELETE",
|
|
});
|
|
}
|
|
|
|
/** Fetch conversation between current user and a specific participant. */
|
|
export function fetchConversation(
|
|
participantId: string,
|
|
participantType: ParticipantType,
|
|
projectId?: string,
|
|
): Promise<Message[]> {
|
|
const path = `/messages/conversation/${encodeURIComponent(participantType)}/${encodeURIComponent(participantId)}`;
|
|
return api<Message[]>(withProjectId(path, projectId));
|
|
}
|
|
|
|
/** Fetch an agent's mailbox (admin read-only view). */
|
|
export function fetchAgentMailbox(agentId: string, projectId?: string): Promise<AgentMailboxResponse> {
|
|
return api<AgentMailboxResponse>(withProjectId(`/agents/${encodeURIComponent(agentId)}/mailbox`, projectId));
|
|
}
|
|
|
|
/** Fetch aggregate mailbox across all agent-to-agent messages (admin read-only view). */
|
|
export function fetchAllAgentMailbox(projectId?: string): Promise<AllAgentsMailboxResponse> {
|
|
return api<AllAgentsMailboxResponse>(withProjectId("/agents/mailbox/all", projectId));
|
|
}
|
|
|
|
export function fetchApprovals(
|
|
options?: { status?: ApprovalRequestStatus; limit?: number; offset?: number },
|
|
projectId?: string,
|
|
): Promise<ApprovalListResponse> {
|
|
const params = new URLSearchParams();
|
|
if (options?.status) params.set("status", options.status);
|
|
if (options?.limit !== undefined) params.set("limit", String(options.limit));
|
|
if (options?.offset !== undefined) params.set("offset", String(options.offset));
|
|
if (projectId) params.set("projectId", projectId);
|
|
const query = params.size > 0 ? `?${params.toString()}` : "";
|
|
return api<ApprovalListResponse>(`/approvals${query}`);
|
|
}
|
|
|
|
export function fetchApprovalDetail(id: string, projectId?: string): Promise<ApprovalRequestDetail> {
|
|
return api<ApprovalRequestDetail>(withProjectId(`/approvals/${encodeURIComponent(id)}`, projectId));
|
|
}
|
|
|
|
export function decideApproval(
|
|
id: string,
|
|
input: { decision: "approve" | "deny"; comment?: string },
|
|
projectId?: string,
|
|
): Promise<ApprovalRequestDetail> {
|
|
return api<ApprovalRequestDetail>(withProjectId(`/approvals/${encodeURIComponent(id)}/decision`, projectId), {
|
|
method: "POST",
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
|
|
/** Fetch reflection history for an agent. */
|
|
export function fetchAgentReflections(agentId: string, limit?: number, projectId?: string): Promise<AgentReflection[]> {
|
|
const params = new URLSearchParams();
|
|
if (limit !== undefined) params.set("limit", String(limit));
|
|
if (projectId) params.set("projectId", projectId);
|
|
const query = params.size > 0 ? `?${params.toString()}` : "";
|
|
return api<AgentReflection[]>(`/agents/${encodeURIComponent(agentId)}/reflections${query}`);
|
|
}
|
|
|
|
/** Fetch the most recent reflection for an agent. */
|
|
export function fetchAgentReflection(agentId: string, projectId?: string): Promise<AgentReflection> {
|
|
return api<AgentReflection>(withProjectId(`/agents/${encodeURIComponent(agentId)}/reflections/latest`, projectId));
|
|
}
|
|
|
|
/** Trigger a manual reflection for an agent. */
|
|
export function triggerAgentReflection(agentId: string, projectId?: string): Promise<AgentReflection | null> {
|
|
return api<AgentReflection | null>(withProjectId(`/agents/${encodeURIComponent(agentId)}/reflections`, projectId), {
|
|
method: "POST",
|
|
});
|
|
}
|
|
|
|
/** Fetch aggregated performance summary for an agent. */
|
|
export function fetchAgentPerformance(agentId: string, windowMs?: number, projectId?: string): Promise<AgentPerformanceSummary> {
|
|
const params = new URLSearchParams();
|
|
if (windowMs !== undefined) params.set("windowMs", String(windowMs));
|
|
if (projectId) params.set("projectId", projectId);
|
|
const query = params.size > 0 ? `?${params.toString()}` : "";
|
|
return api<AgentPerformanceSummary>(`/agents/${encodeURIComponent(agentId)}/performance${query}`);
|
|
}
|
|
|
|
/** Fetch ratings for an agent */
|
|
export function fetchAgentRatings(
|
|
agentId: string,
|
|
options?: { limit?: number; category?: string },
|
|
projectId?: string,
|
|
): Promise<AgentRating[]> {
|
|
const params = new URLSearchParams();
|
|
if (options?.limit !== undefined) params.set("limit", String(options.limit));
|
|
if (options?.category) params.set("category", options.category);
|
|
if (projectId) params.set("projectId", projectId);
|
|
const query = params.size > 0 ? `?${params.toString()}` : "";
|
|
return api<AgentRating[]>(`/agents/${encodeURIComponent(agentId)}/ratings${query}`);
|
|
}
|
|
|
|
/** Add a rating for an agent */
|
|
export function addAgentRating(
|
|
agentId: string,
|
|
input: AgentRatingInput,
|
|
projectId?: string,
|
|
): Promise<AgentRating> {
|
|
return api<AgentRating>(withProjectId(`/agents/${encodeURIComponent(agentId)}/ratings`, projectId), {
|
|
method: "POST",
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
|
|
/** Fetch rating summary for an agent */
|
|
export function fetchAgentRatingSummary(agentId: string, projectId?: string): Promise<AgentRatingSummary> {
|
|
return api<AgentRatingSummary>(withProjectId(`/agents/${encodeURIComponent(agentId)}/ratings/summary`, projectId));
|
|
}
|
|
|
|
/** Delete a specific rating */
|
|
export function deleteAgentRating(agentId: string, ratingId: string, projectId?: string): Promise<void> {
|
|
return api<void>(withProjectId(`/agents/${encodeURIComponent(agentId)}/ratings/${encodeURIComponent(ratingId)}`, projectId), {
|
|
method: "DELETE",
|
|
});
|
|
}
|
|
|
|
// ── Agent Budget API ──────────────────────────────────────────────────────
|
|
|
|
/** Fetch budget status for an agent */
|
|
export function fetchAgentBudgetStatus(agentId: string, projectId?: string): Promise<AgentBudgetStatus> {
|
|
return api<AgentBudgetStatus>(withProjectId(`/agents/${encodeURIComponent(agentId)}/budget`, projectId));
|
|
}
|
|
|
|
/** Reset budget usage for an agent */
|
|
export function resetAgentBudget(agentId: string, projectId?: string): Promise<void> {
|
|
return api<void>(withProjectId(`/agents/${encodeURIComponent(agentId)}/budget/reset`, projectId), {
|
|
method: "POST",
|
|
});
|
|
}
|
|
|