From 938d57c8aed098d5bbc60891f8e33f57762ca641 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 23 Jan 2026 14:18:06 +0700 Subject: [PATCH] Update files --- .../src/server/agent/agent-manager.test.ts | 166 ++++++++++++++++++ .../server/src/server/agent/agent-manager.ts | 36 +++- .../src/server/agent/agent-projections.ts | 2 + .../src/server/agent/agent-registry.test.ts | 60 +++++++ .../server/src/server/agent/agent-registry.ts | 6 +- .../src/server/agent/agent-sdk-types.ts | 5 + packages/server/src/server/session.ts | 1 + 7 files changed, 271 insertions(+), 5 deletions(-) diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index 527d4c939..d6b51d5fc 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -202,4 +202,170 @@ describe("AgentManager", () => { const refreshed = manager.getAgent(snapshot.id); expect(refreshed?.runtimeInfo?.model).toBe("gpt-5.2-codex"); }); + + test("listAgents excludes internal agents", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); + const registryPath = join(workdir, "agents.json"); + const registry = new AgentRegistry(registryPath, logger); + let agentCounter = 0; + const manager = new AgentManager({ + clients: { + codex: new TestAgentClient(), + }, + registry, + logger, + idFactory: () => `agent-${agentCounter++}`, + }); + + // Create a normal agent + await manager.createAgent({ + provider: "codex", + cwd: workdir, + title: "Normal Agent", + }); + + // Create an internal agent + await manager.createAgent({ + provider: "codex", + cwd: workdir, + title: "Internal Agent", + internal: true, + }); + + const agents = manager.listAgents(); + expect(agents).toHaveLength(1); + expect(agents[0]?.config.title).toBe("Normal Agent"); + }); + + test("getAgent returns internal agents by ID", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); + const registryPath = join(workdir, "agents.json"); + const registry = new AgentRegistry(registryPath, logger); + const manager = new AgentManager({ + clients: { + codex: new TestAgentClient(), + }, + registry, + logger, + idFactory: () => "internal-agent", + }); + + await manager.createAgent({ + provider: "codex", + cwd: workdir, + title: "Internal Agent", + internal: true, + }); + + const agent = manager.getAgent("internal-agent"); + expect(agent).not.toBeNull(); + expect(agent?.internal).toBe(true); + }); + + test("subscribe does not emit state events for internal agents to global subscribers", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); + const registryPath = join(workdir, "agents.json"); + const registry = new AgentRegistry(registryPath, logger); + let agentCounter = 0; + const manager = new AgentManager({ + clients: { + codex: new TestAgentClient(), + }, + registry, + logger, + idFactory: () => `agent-${agentCounter++}`, + }); + + const receivedEvents: string[] = []; + manager.subscribe((event) => { + if (event.type === "agent_state") { + receivedEvents.push(event.agent.id); + } + }); + + // Create a normal agent - should emit + await manager.createAgent({ + provider: "codex", + cwd: workdir, + title: "Normal Agent", + }); + + // Create an internal agent - should NOT emit to global subscriber + await manager.createAgent({ + provider: "codex", + cwd: workdir, + title: "Internal Agent", + internal: true, + }); + + // Should only have events from the normal agent + expect(receivedEvents.filter((id) => id === "agent-0").length).toBeGreaterThan(0); + expect(receivedEvents.filter((id) => id === "agent-1").length).toBe(0); + }); + + test("subscribe emits state events for internal agents when subscribed by agentId", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); + const registryPath = join(workdir, "agents.json"); + const registry = new AgentRegistry(registryPath, logger); + const manager = new AgentManager({ + clients: { + codex: new TestAgentClient(), + }, + registry, + logger, + idFactory: () => "internal-agent", + }); + + const receivedEvents: string[] = []; + // Subscribe specifically to the internal agent + manager.subscribe( + (event) => { + if (event.type === "agent_state") { + receivedEvents.push(event.agent.id); + } + }, + { agentId: "internal-agent", replayState: false } + ); + + await manager.createAgent({ + provider: "codex", + cwd: workdir, + title: "Internal Agent", + internal: true, + }); + + // Should receive events when subscribed by specific agentId + expect(receivedEvents.filter((id) => id === "internal-agent").length).toBeGreaterThan(0); + }); + + test("onAgentAttention is not called for internal agents", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); + const registryPath = join(workdir, "agents.json"); + const registry = new AgentRegistry(registryPath, logger); + const attentionCalls: string[] = []; + const manager = new AgentManager({ + clients: { + codex: new TestAgentClient(), + }, + registry, + logger, + idFactory: () => "internal-agent", + onAgentAttention: ({ agentId }) => { + attentionCalls.push(agentId); + }, + }); + + const agent = await manager.createAgent({ + provider: "codex", + cwd: workdir, + title: "Internal Agent", + internal: true, + }); + + // Run and complete the agent (which normally triggers attention) + await manager.runAgent(agent.id, "hello"); + + // Should NOT have triggered attention callback for internal agent + expect(attentionCalls).toHaveLength(0); + }); }); diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index 7a9de18c5..bdd6e7910 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -101,6 +101,10 @@ type ManagedAgentBase = { lastError?: string; attention: AttentionState; parentAgentId?: string; + /** + * Internal agents are hidden from listings and don't trigger notifications. + */ + internal?: boolean; }; type ManagedAgentWithSession = ManagedAgentBase & { @@ -230,7 +234,11 @@ export class AgentManager { }); } } else { + // For global subscribers, skip internal agents during replay for (const agent of this.agents.values()) { + if (agent.internal) { + continue; + } callback({ type: "agent_state", agent: { ...agent }, @@ -245,9 +253,11 @@ export class AgentManager { } listAgents(): ManagedAgent[] { - return Array.from(this.agents.values()).map((agent) => ({ - ...agent, - })); + return Array.from(this.agents.values()) + .filter((agent) => !agent.internal) + .map((agent) => ({ + ...agent, + })); } async listPersistedAgents( @@ -811,6 +821,7 @@ export class AgentManager { lastUserMessageAt: null, attention: { requiresAttention: false }, parentAgentId: config.parentAgentId, + internal: config.internal ?? false, } as ActiveManagedAgent; this.agents.set(agentId, managed); @@ -831,7 +842,7 @@ export class AgentManager { private async persistSnapshot( agent: ManagedAgent, - options?: { title?: string | null } + options?: { title?: string | null; internal?: boolean } ): Promise { if (!this.registry) { return; @@ -977,6 +988,11 @@ export class AgentManager { // Track the new status this.previousStatuses.set(agent.id, currentStatus); + // Skip attention tracking for internal agents + if (agent.internal) { + return; + } + // Skip if already requires attention if (agent.attention.requiresAttention) { return; @@ -1054,6 +1070,18 @@ export class AgentManager { ) { continue; } + // Skip internal agents for global subscribers (those without a specific agentId) + if (!subscriber.agentId) { + if (event.type === "agent_state" && event.agent.internal) { + continue; + } + if (event.type === "agent_stream") { + const agent = this.agents.get(event.agentId); + if (agent?.internal) { + continue; + } + } + } subscriber.callback(event); } } diff --git a/packages/server/src/server/agent/agent-projections.ts b/packages/server/src/server/agent/agent-projections.ts index f04b5ce5b..bd4e5c3f1 100644 --- a/packages/server/src/server/agent/agent-projections.ts +++ b/packages/server/src/server/agent/agent-projections.ts @@ -21,6 +21,7 @@ export type { ManagedAgent }; type ProjectionOptions = { title?: string | null; createdAt?: string; + internal?: boolean; }; export function toStoredAgentRecord( @@ -56,6 +57,7 @@ export function toStoredAgentRecord( ? agent.attention.attentionTimestamp.toISOString() : null, parentAgentId: agent.parentAgentId ?? null, + internal: options?.internal, } satisfies StoredAgentRecord; } diff --git a/packages/server/src/server/agent/agent-registry.test.ts b/packages/server/src/server/agent/agent-registry.test.ts index f98d5ba5e..436a665c1 100644 --- a/packages/server/src/server/agent/agent-registry.test.ts +++ b/packages/server/src/server/agent/agent-registry.test.ts @@ -249,4 +249,64 @@ describe("AgentRegistry", () => { const sanitized = readFileSync(filePath, "utf8"); expect(sanitized.includes("GARBAGE-TRAILING")).toBe(false); }); + + test("list returns all agents including internal ones", async () => { + // Create a normal agent + await registry.applySnapshot( + createManagedAgent({ + id: "normal-agent", + cwd: "/tmp/project", + }) + ); + + // Create an internal agent + await registry.applySnapshot( + createManagedAgent({ + id: "internal-agent", + cwd: "/tmp/project", + config: { internal: true }, + }), + { internal: true } + ); + + // Registry should return all agents - filtering is done at the manager level + const records = await registry.list(); + expect(records).toHaveLength(2); + }); + + test("get returns internal agents by ID", async () => { + await registry.applySnapshot( + createManagedAgent({ + id: "internal-agent", + cwd: "/tmp/project", + config: { internal: true }, + }), + { internal: true } + ); + + const record = await registry.get("internal-agent"); + expect(record).not.toBeNull(); + expect(record?.internal).toBe(true); + }); + + test("internal flag is persisted and reloaded", async () => { + await registry.applySnapshot( + createManagedAgent({ + id: "internal-agent", + cwd: "/tmp/project", + config: { internal: true }, + }), + { internal: true } + ); + + // Reload the registry from disk + const reloaded = new AgentRegistry(filePath, logger); + const record = await reloaded.get("internal-agent"); + expect(record?.internal).toBe(true); + + // Registry returns all agents - filtering happens at manager level + const records = await reloaded.list(); + expect(records).toHaveLength(1); + expect(records[0]?.internal).toBe(true); + }); }); diff --git a/packages/server/src/server/agent/agent-registry.ts b/packages/server/src/server/agent/agent-registry.ts index 8e33b2678..f31607b63 100644 --- a/packages/server/src/server/agent/agent-registry.ts +++ b/packages/server/src/server/agent/agent-registry.ts @@ -54,6 +54,7 @@ const STORED_AGENT_SCHEMA = z.object({ attentionReason: z.enum(["finished", "error", "permission"]).nullable().optional(), attentionTimestamp: z.string().nullable().optional(), parentAgentId: z.string().nullable().optional(), + internal: z.boolean().optional(), }); export type SerializableAgentConfig = Pick< @@ -129,15 +130,18 @@ export class AgentRegistry { async applySnapshot( agent: ManagedAgent, - options?: { title?: string | null } + options?: { title?: string | null; internal?: boolean } ): Promise { await this.load(); const existing = this.cache.get(agent.id); const hasTitleOverride = options !== undefined && Object.prototype.hasOwnProperty.call(options, "title"); + const hasInternalOverride = + options !== undefined && Object.prototype.hasOwnProperty.call(options, "internal"); const record = toStoredAgentRecord(agent, { title: hasTitleOverride ? options?.title ?? null : existing?.title ?? null, createdAt: existing?.createdAt, + internal: hasInternalOverride ? options?.internal : (agent.internal ?? existing?.internal), }); this.cache.set(agent.id, record); await this.flush(); diff --git a/packages/server/src/server/agent/agent-sdk-types.ts b/packages/server/src/server/agent/agent-sdk-types.ts index 13d6341db..ecd21fa9c 100644 --- a/packages/server/src/server/agent/agent-sdk-types.ts +++ b/packages/server/src/server/agent/agent-sdk-types.ts @@ -208,6 +208,11 @@ export type AgentSessionConfig = { }; mcpServers?: AgentMetadata; parentAgentId?: string; + /** + * Internal agents are hidden from listings and don't trigger notifications. + * They are used for ephemeral system tasks like commit/PR generation. + */ + internal?: boolean; }; export interface AgentSession { diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index ea7587612..99fd89e59 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -1877,6 +1877,7 @@ export class Session { cwd: agent.cwd, title, parentAgentId: agent.id, + internal: true, }; }