feat(FN-1868): merge fusion/fn-1868
This commit is contained in:
@@ -999,7 +999,7 @@ describe("AgentStore", () => {
|
||||
expect(agents[0].name).toBe("Valid");
|
||||
});
|
||||
|
||||
it("filters out system agents when includeSystem is false", async () => {
|
||||
it("filters out ephemeral agents by default", async () => {
|
||||
// Create a normal agent
|
||||
const normal = await store.createAgent({ name: "Normal Agent", role: "executor" });
|
||||
|
||||
@@ -1031,21 +1031,17 @@ describe("AgentStore", () => {
|
||||
metadata: { managedBy: "task-executor" },
|
||||
});
|
||||
|
||||
// Without includeSystem filter, all agents are returned
|
||||
// Without includeEphemeral filter, ephemeral agents are filtered out by default
|
||||
const allAgents = await store.listAgents();
|
||||
expect(allAgents).toHaveLength(5);
|
||||
expect(allAgents).toHaveLength(1);
|
||||
expect(allAgents[0].id).toBe(normal.id);
|
||||
|
||||
// With includeSystem: false, system agents are filtered out
|
||||
const nonSystemAgents = await store.listAgents({ includeSystem: false });
|
||||
expect(nonSystemAgents).toHaveLength(1);
|
||||
expect(nonSystemAgents[0].id).toBe(normal.id);
|
||||
|
||||
// With includeSystem: true, all agents are returned
|
||||
const systemAgents = await store.listAgents({ includeSystem: true });
|
||||
expect(systemAgents).toHaveLength(5);
|
||||
// With includeEphemeral: true, all agents are returned
|
||||
const allIncludingEphemeral = await store.listAgents({ includeEphemeral: true });
|
||||
expect(allIncludingEphemeral).toHaveLength(5);
|
||||
});
|
||||
|
||||
it("includeSystem filter works with state filter", async () => {
|
||||
it("includeEphemeral filter works with state filter", async () => {
|
||||
// Create a normal agent
|
||||
const normal = await store.createAgent({ name: "Normal Agent", role: "executor" });
|
||||
|
||||
@@ -1058,12 +1054,12 @@ describe("AgentStore", () => {
|
||||
await store.recordHeartbeat(taskWorker.id, "ok");
|
||||
await store.updateAgentState(taskWorker.id, "active");
|
||||
|
||||
// Without includeSystem, but with state=active - only returns active non-system agents
|
||||
const activeNonSystem = await store.listAgents({ state: "active", includeSystem: false });
|
||||
expect(activeNonSystem).toHaveLength(0);
|
||||
// Without includeEphemeral filter - only returns active non-ephemeral agents
|
||||
const activeNonEphemeral = await store.listAgents({ state: "active" });
|
||||
expect(activeNonEphemeral).toHaveLength(0);
|
||||
|
||||
// With includeSystem: true, returns all active agents
|
||||
const activeAll = await store.listAgents({ state: "active", includeSystem: true });
|
||||
// With includeEphemeral: true, returns all active agents
|
||||
const activeAll = await store.listAgents({ state: "active", includeEphemeral: true });
|
||||
expect(activeAll).toHaveLength(1);
|
||||
expect(activeAll[0].id).toBe(taskWorker.id);
|
||||
});
|
||||
|
||||
@@ -43,7 +43,7 @@ import type {
|
||||
AgentRatingInput,
|
||||
Task,
|
||||
} from "./types.js";
|
||||
import { AGENT_VALID_TRANSITIONS, agentToConfigSnapshot, diffConfigSnapshots, CheckoutConflictError } from "./types.js";
|
||||
import { AGENT_VALID_TRANSITIONS, agentToConfigSnapshot, diffConfigSnapshots, isEphemeralAgent, CheckoutConflictError } from "./types.js";
|
||||
import type { RunMutationContext } from "./types.js";
|
||||
import type { TaskStore } from "./store.js";
|
||||
import { computeAccessState } from "./agent-permissions.js";
|
||||
@@ -985,27 +985,12 @@ export class AgentStore extends EventEmitter {
|
||||
return agent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an agent is a system-generated ephemeral agent (task-worker or spawned child).
|
||||
* These agents are created at runtime by the engine and should typically be hidden
|
||||
* from the default agents page view.
|
||||
*/
|
||||
private isSystemAgent(agent: Agent): boolean {
|
||||
const metadata = agent.metadata ?? {};
|
||||
return (
|
||||
metadata.agentKind === "task-worker" ||
|
||||
metadata.type === "spawned" ||
|
||||
metadata.taskWorker === true ||
|
||||
metadata.managedBy === "task-executor"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all agents, optionally filtered by state.
|
||||
* @param filter - Optional filter criteria
|
||||
* @returns Array of agents
|
||||
*/
|
||||
async listAgents(filter?: { state?: AgentState; role?: AgentCapability; includeSystem?: boolean }): Promise<Agent[]> {
|
||||
async listAgents(filter?: { state?: AgentState; role?: AgentCapability; includeEphemeral?: boolean }): Promise<Agent[]> {
|
||||
const files = await readdir(this.agentsDir).catch(() => [] as string[]);
|
||||
const agentFiles = files.filter((f) => f.endsWith(".json") && !f.includes("-heartbeats") && !f.includes("-sessions") && !f.includes("-runs") && !f.includes("-revisions"));
|
||||
|
||||
@@ -1019,8 +1004,8 @@ export class AgentStore extends EventEmitter {
|
||||
if (filter?.state && agent.state !== filter.state) continue;
|
||||
if (filter?.role && agent.role !== filter.role) continue;
|
||||
|
||||
// When includeSystem is explicitly false, filter out system agents
|
||||
if (filter?.includeSystem === false && this.isSystemAgent(agent)) continue;
|
||||
// When includeEphemeral is not true, filter out ephemeral agents
|
||||
if (filter?.includeEphemeral !== true && isEphemeralAgent(agent)) continue;
|
||||
|
||||
agents.push(agent);
|
||||
} catch {
|
||||
@@ -1503,7 +1488,7 @@ export class AgentStore extends EventEmitter {
|
||||
* @param filter - Optional filter for listing agents
|
||||
* @returns Root nodes with nested children
|
||||
*/
|
||||
async getOrgTree(filter?: { includeSystem?: boolean }): Promise<OrgTreeNode[]> {
|
||||
async getOrgTree(filter?: { includeEphemeral?: boolean }): Promise<OrgTreeNode[]> {
|
||||
const agents = await this.listAgents(filter);
|
||||
if (agents.length === 0) {
|
||||
return [];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export { COLUMNS, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, isGlobalSettingsKey, isProjectSettingsKey, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, AGENT_PERMISSIONS, agentToConfigSnapshot, diffConfigSnapshots, CheckoutConflictError } 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, isGlobalSettingsKey, isProjectSettingsKey, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, AGENT_PERMISSIONS, agentToConfigSnapshot, diffConfigSnapshots, isEphemeralAgent, CheckoutConflictError } from "./types.js";
|
||||
export type { Column, IssueInfo, IssueState, PrInfo, PrStatus, Task, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, TaskCreateInput, TaskDetail, InboxTask, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, MergeDetails, MergeResult, Settings, GlobalSettings, ProjectSettings, SettingsScope, DaemonTokenSettings, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, 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, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, Mailbox, CheckoutLease, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter } from "./types.js";
|
||||
export { AGENT_VALID_TRANSITIONS } from "./types.js";
|
||||
export {
|
||||
|
||||
@@ -1947,6 +1947,46 @@ export const AGENT_VALID_TRANSITIONS: Record<AgentState, AgentState[]> = {
|
||||
terminated: ["idle", "active", "running"], // Can be restarted or reset
|
||||
};
|
||||
|
||||
/**
|
||||
* Detect if an agent is a runtime-created ephemeral agent (task-worker or spawned child).
|
||||
* These agents are created by the engine for task execution and should typically be
|
||||
* hidden from the default agents page listing.
|
||||
*
|
||||
* Detection heuristics (returns true if ANY match):
|
||||
* - `agent.metadata?.agentKind === "task-worker"` — task-worker agents from InProcessRuntime
|
||||
* - `agent.metadata?.taskWorker === true` — legacy task-worker marker
|
||||
* - `agent.metadata?.managedBy === "task-executor"` — executor-managed agents
|
||||
* - `agent.metadata?.type === "spawned"` — spawned child agents from TaskExecutor
|
||||
* - Legacy fallback: executor role with name starting with "executor-" and no reportsTo
|
||||
*
|
||||
* @param agent - Agent object (partial shape accepted)
|
||||
* @returns true if the agent is an ephemeral/runtime-created agent
|
||||
*/
|
||||
export function isEphemeralAgent(
|
||||
agent: { metadata?: Record<string, unknown> | null; name?: string; role?: string; reportsTo?: string | null },
|
||||
): boolean {
|
||||
const metadata = agent.metadata ?? {};
|
||||
|
||||
// Check explicit metadata markers first
|
||||
if (metadata.agentKind === "task-worker") return true;
|
||||
if (metadata.taskWorker === true) return true;
|
||||
if (metadata.managedBy === "task-executor") return true;
|
||||
if (metadata.type === "spawned") return true;
|
||||
|
||||
// Legacy fallback: executor agents with "executor-" prefix and no manager
|
||||
// These are task workers that were created before metadata was standardized
|
||||
if (
|
||||
agent.role === "executor" &&
|
||||
typeof agent.name === "string" &&
|
||||
agent.name.startsWith("executor-") &&
|
||||
agent.reportsTo == null
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Single heartbeat event recorded for an agent */
|
||||
export interface AgentHeartbeatEvent {
|
||||
/** ISO-8601 timestamp of when the heartbeat was recorded */
|
||||
|
||||
Reference in New Issue
Block a user