Keep delegated agents out of workspace alerts (#1293)

* Keep delegated agents out of workspace alerts

* Share delegated agent label parsing
This commit is contained in:
Mohamed Boudra
2026-06-02 20:55:19 +08:00
committed by GitHub
parent 4f3116d28a
commit 14af053c66
7 changed files with 233 additions and 7 deletions
+2 -6
View File
@@ -1,6 +1,6 @@
import type { AgentSnapshotPayload } from "@getpaseo/protocol/messages";
import type { AgentPermissionRequest } from "@getpaseo/protocol/agent-types";
import { PARENT_AGENT_ID_LABEL } from "@getpaseo/protocol/agent-labels";
import { getParentAgentIdFromLabels } from "@getpaseo/protocol/agent-labels";
export function derivePendingPermissionKey(
agentId: string,
@@ -26,11 +26,7 @@ export function normalizeAgentSnapshot(snapshot: AgentSnapshotPayload, serverId:
? new Date(snapshot.attentionTimestamp)
: null;
const archivedAt = snapshot.archivedAt ? new Date(snapshot.archivedAt) : null;
const parentAgentLabel = snapshot.labels?.[PARENT_AGENT_ID_LABEL];
const parentAgentId =
typeof parentAgentLabel === "string" && parentAgentLabel.trim().length > 0
? parentAgentLabel.trim()
: null;
const parentAgentId = getParentAgentIdFromLabels(snapshot.labels);
return {
serverId,
@@ -0,0 +1,21 @@
import { describe, expect, test } from "vitest";
import {
getParentAgentIdFromLabels,
isDelegatedAgent,
PARENT_AGENT_ID_LABEL,
} from "./agent-labels.js";
describe("agent label policy", () => {
test("treats a non-empty parent agent label as delegation", () => {
const labels = { [PARENT_AGENT_ID_LABEL]: " parent-agent \n" };
expect(getParentAgentIdFromLabels(labels)).toBe("parent-agent");
expect(isDelegatedAgent({ labels })).toBe(true);
});
test("ignores missing, empty, and non-string parent agent labels", () => {
expect(isDelegatedAgent({ labels: {} })).toBe(false);
expect(isDelegatedAgent({ labels: { [PARENT_AGENT_ID_LABEL]: " " } })).toBe(false);
expect(isDelegatedAgent({ labels: { [PARENT_AGENT_ID_LABEL]: 42 } })).toBe(false);
});
});
+15
View File
@@ -1 +1,16 @@
export const PARENT_AGENT_ID_LABEL = "paseo.parent-agent-id";
export interface AgentLabelSource {
labels?: Record<string, unknown> | null;
}
export function getParentAgentIdFromLabels(labels: Record<string, unknown> | null | undefined) {
const parentAgentId = labels?.[PARENT_AGENT_ID_LABEL];
return typeof parentAgentId === "string" && parentAgentId.trim().length > 0
? parentAgentId.trim()
: null;
}
export function isDelegatedAgent(agent: AgentLabelSource): boolean {
return getParentAgentIdFromLabels(agent.labels) !== null;
}
@@ -3892,6 +3892,39 @@ test("onAgentAttention is not called for internal agents", async () => {
expect(attentionCalls).toHaveLength(0);
});
test("onAgentAttention is not called for delegated child agents", async () => {
const childAgentId = "00000000-0000-4000-8000-000000000112";
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
const storagePath = join(workdir, "agents");
const storage = new AgentStorage(storagePath, logger);
const attentionCalls: string[] = [];
const manager = new AgentManager({
clients: {
codex: new TestAgentClient(),
},
registry: storage,
logger,
idFactory: () => childAgentId,
onAgentAttention: ({ agentId }) => {
attentionCalls.push(agentId);
},
});
const agent = await manager.createAgent(
{
provider: "codex",
cwd: workdir,
title: "Delegated Child Agent",
},
undefined,
{ labels: { [PARENT_AGENT_ID_LABEL]: "parent-agent" } },
);
await manager.runAgent(agent.id, "hello");
expect(attentionCalls).toEqual([]);
});
test("clearAgentAttention on errored agent stays cleared until a new error transition", async () => {
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-attention-error-"));
const storagePath = join(workdir, "agents");
@@ -5,7 +5,7 @@ import {
AGENT_LIFECYCLE_STATUSES,
type AgentLifecycleStatus,
} from "@getpaseo/protocol/agent-lifecycle";
import { PARENT_AGENT_ID_LABEL } from "@getpaseo/protocol/agent-labels";
import { isDelegatedAgent, PARENT_AGENT_ID_LABEL } from "@getpaseo/protocol/agent-labels";
import type { Logger } from "pino";
import { z } from "zod";
import type { TerminalManager } from "../../terminal/terminal-manager.js";
@@ -3368,6 +3368,10 @@ export class AgentManager {
agent: ManagedAgent,
reason: "finished" | "error" | "permission",
): void {
if (isDelegatedAgent(agent)) {
return;
}
this.onAgentAttention?.({
agentId: agent.id,
provider: agent.provider,
@@ -0,0 +1,153 @@
import { describe, expect, test } from "vitest";
import { PARENT_AGENT_ID_LABEL } from "@getpaseo/protocol/agent-labels";
import { createTestLogger } from "../test-utils/test-logger.js";
import type { AgentSnapshotPayload, WorkspaceDescriptorPayload } from "./messages.js";
import { WorkspaceDirectory } from "./workspace-directory.js";
import type { PersistedProjectRecord, PersistedWorkspaceRecord } from "./workspace-registry.js";
const NOW = "2026-03-01T12:00:00.000Z";
class WorkspaceStatus {
private readonly project: PersistedProjectRecord = {
projectId: "project-1",
rootPath: "/workspace/project",
kind: "git",
displayName: "project",
customName: null,
createdAt: NOW,
updatedAt: NOW,
archivedAt: null,
};
private readonly workspace: PersistedWorkspaceRecord = {
workspaceId: "workspace-1",
projectId: this.project.projectId,
cwd: this.project.rootPath,
kind: "local_checkout",
displayName: "main",
createdAt: NOW,
updatedAt: NOW,
archivedAt: null,
};
private readonly agents: AgentSnapshotPayload[] = [];
private readonly directory = new WorkspaceDirectory({
logger: createTestLogger(),
projectRegistry: { list: async () => [this.project] },
workspaceRegistry: { list: async () => [this.workspace] },
listAgentPayloads: async () => this.agents,
isProviderVisibleToClient: () => true,
buildWorkspaceDescriptor: async ({ workspace }) => ({
id: workspace.workspaceId,
projectId: workspace.projectId,
projectDisplayName: "project",
projectCustomName: null,
projectRootPath: this.project.rootPath,
workspaceDirectory: workspace.cwd,
projectKind: "git",
workspaceKind: workspace.kind,
name: workspace.displayName,
archivingAt: null,
status: "done",
activityAt: null,
diffStat: null,
scripts: [],
gitRuntime: null,
githubRuntime: null,
}),
});
hasRootAgent(input: AgentState): void {
this.agents.push(createAgent({ ...input, cwd: this.workspace.cwd }));
}
hasDelegatedAgent(input: AgentState): void {
this.agents.push(
createAgent({
...input,
cwd: this.workspace.cwd,
labels: { [PARENT_AGENT_ID_LABEL]: "parent-agent" },
}),
);
}
async workspaceStatus(): Promise<WorkspaceDescriptorPayload["status"]> {
const entries = await this.directory.listFetchEntries({
type: "fetch_workspaces_request",
requestId: "workspace-status",
});
return entries.entries[0]?.status ?? "done";
}
}
interface AgentState {
id: string;
status: AgentSnapshotPayload["status"];
pendingPermissionCount?: number;
requiresAttention?: boolean;
attentionReason?: AgentSnapshotPayload["attentionReason"];
}
function createAgent(input: AgentState & { cwd: string; labels?: Record<string, string> }) {
const pendingPermissionCount = input.pendingPermissionCount ?? 0;
return {
id: input.id,
provider: "codex",
cwd: input.cwd,
model: null,
thinkingOptionId: null,
effectiveThinkingOptionId: null,
createdAt: NOW,
updatedAt: NOW,
lastUserMessageAt: null,
status: input.status,
capabilities: {
supportsStreaming: true,
supportsSessionPersistence: true,
supportsDynamicModes: true,
supportsMcpServers: true,
supportsReasoningStream: true,
supportsToolInvocations: true,
},
currentModeId: null,
availableModes: [],
pendingPermissions: Array.from({ length: pendingPermissionCount }, (_, index) => ({
id: `permission-${input.id}-${index}`,
provider: "codex",
name: "tool",
kind: "tool" as const,
})),
persistence: null,
runtimeInfo: {
provider: "codex",
sessionId: null,
},
title: null,
labels: input.labels ?? {},
requiresAttention: input.requiresAttention ?? false,
attentionReason: input.attentionReason ?? null,
attentionTimestamp: null,
archivedAt: null,
} satisfies AgentSnapshotPayload;
}
describe("WorkspaceDirectory", () => {
test("uses root agent activity, not delegated child activity, for workspace status", async () => {
const workspace = new WorkspaceStatus();
workspace.hasRootAgent({ id: "root-agent", status: "running" });
workspace.hasDelegatedAgent({
id: "child-needs-input",
status: "idle",
pendingPermissionCount: 1,
});
workspace.hasDelegatedAgent({
id: "child-error",
status: "error",
requiresAttention: true,
attentionReason: "error",
});
await expect(workspace.workspaceStatus()).resolves.toBe("running");
});
});
@@ -11,6 +11,7 @@ import {
deriveAgentStateBucket,
getWorkspaceStateBucketPriority,
} from "@getpaseo/protocol/agent-state-bucket";
import { isDelegatedAgent } from "@getpaseo/protocol/agent-labels";
import { SortablePager } from "./pagination/sortable-pager.js";
import type { PersistedProjectRecord, PersistedWorkspaceRecord } from "./workspace-registry.js";
import { normalizeWorkspaceId } from "./workspace-registry-model.js";
@@ -187,6 +188,9 @@ export class WorkspaceDirectory {
if (!this.deps.isProviderVisibleToClient(agent.provider)) {
continue;
}
if (isDelegatedAgent(agent)) {
continue;
}
const workspaceId = workspaceIdsByDirectory.get(normalizeWorkspaceId(agent.cwd));
if (workspaceId === undefined) {