Show delegated agent activity on parent workspaces

This commit is contained in:
Mohamed Boudra
2026-06-03 19:10:05 +07:00
parent 510a299420
commit 76ae8c19b0
3 changed files with 105 additions and 17 deletions

View File

@@ -54,6 +54,12 @@ Closing a tab on a **subagent** (any agent with `parentAgentId`) is **layout-onl
The asymmetry is intentional: a subagent's home is the parent's track, not the tab. Tabs are ephemeral viewing slots; the track is the persistent record of the parent's children. The asymmetry is intentional: a subagent's home is the parent's track, not the tab. Tabs are ephemeral viewing slots; the track is the persistent record of the parent's children.
## Workspace activity
Agent lifecycle status stays literal: a parent agent is `idle` when its own turn is idle, even if a child is running.
Workspace status is an aggregate activity signal. Root agents contribute their normal state bucket to their own workspace. Running subagents contribute `running` to their root parent's workspace, not to the subagent's current `cwd` or worktree. Non-running subagent attention, permission, and error states stay in the parent's subagents track and do not escalate the workspace bucket.
## The subagents track ## The subagents track
The collapsible track above the composer in an agent's pane (`packages/app/src/subagents/track.tsx`). Membership rule (`packages/app/src/subagents/select.ts`): The collapsible track above the composer in an agent's pane (`packages/app/src/subagents/track.tsx`). Membership rule (`packages/app/src/subagents/select.ts`):

View File

@@ -30,11 +30,24 @@ class WorkspaceStatus {
archivedAt: null, archivedAt: null,
}; };
private readonly worktreeWorkspace: PersistedWorkspaceRecord = {
workspaceId: "workspace-worktree",
projectId: this.project.projectId,
cwd: "/workspace/project/.paseo/worktrees/feature",
kind: "worktree",
displayName: "feature",
createdAt: NOW,
updatedAt: NOW,
archivedAt: null,
};
private readonly workspaces = [this.workspace];
private readonly agents: AgentSnapshotPayload[] = []; private readonly agents: AgentSnapshotPayload[] = [];
private readonly directory = new WorkspaceDirectory({ private readonly directory = new WorkspaceDirectory({
logger: createTestLogger(), logger: createTestLogger(),
projectRegistry: { list: async () => [this.project] }, projectRegistry: { list: async () => [this.project] },
workspaceRegistry: { list: async () => [this.workspace] }, workspaceRegistry: { list: async () => this.workspaces },
listAgentPayloads: async () => this.agents, listAgentPayloads: async () => this.agents,
isProviderVisibleToClient: () => true, isProviderVisibleToClient: () => true,
buildWorkspaceDescriptor: async ({ workspace }) => ({ buildWorkspaceDescriptor: async ({ workspace }) => ({
@@ -71,6 +84,20 @@ class WorkspaceStatus {
); );
} }
hasWorktreeWorkspace(): void {
this.workspaces.push(this.worktreeWorkspace);
}
hasDelegatedAgentInWorktree(input: AgentState): void {
this.agents.push(
createAgent({
...input,
cwd: this.worktreeWorkspace.cwd,
labels: { [PARENT_AGENT_ID_LABEL]: "parent-agent" },
}),
);
}
async workspaceStatus(): Promise<WorkspaceDescriptorPayload["status"]> { async workspaceStatus(): Promise<WorkspaceDescriptorPayload["status"]> {
const entries = await this.directory.listFetchEntries({ const entries = await this.directory.listFetchEntries({
type: "fetch_workspaces_request", type: "fetch_workspaces_request",
@@ -78,6 +105,14 @@ class WorkspaceStatus {
}); });
return entries.entries[0]?.status ?? "done"; return entries.entries[0]?.status ?? "done";
} }
async workspaceStatuses(): Promise<Record<string, WorkspaceDescriptorPayload["status"]>> {
const entries = await this.directory.listFetchEntries({
type: "fetch_workspaces_request",
requestId: "workspace-statuses",
});
return Object.fromEntries(entries.entries.map((entry) => [entry.id, entry.status]));
}
} }
interface AgentState { interface AgentState {
@@ -150,4 +185,17 @@ describe("WorkspaceDirectory", () => {
await expect(workspace.workspaceStatus()).resolves.toBe("running"); await expect(workspace.workspaceStatus()).resolves.toBe("running");
}); });
test("running delegated child contributes running to the parent workspace, not its worktree", async () => {
const workspace = new WorkspaceStatus();
workspace.hasWorktreeWorkspace();
workspace.hasRootAgent({ id: "parent-agent", status: "idle" });
workspace.hasDelegatedAgentInWorktree({ id: "child-agent", status: "running" });
await expect(workspace.workspaceStatuses()).resolves.toEqual({
"workspace-1": "running",
"workspace-worktree": "done",
});
});
}); });

View File

@@ -11,7 +11,7 @@ import {
deriveAgentStateBucket, deriveAgentStateBucket,
getWorkspaceStateBucketPriority, getWorkspaceStateBucketPriority,
} from "@getpaseo/protocol/agent-state-bucket"; } from "@getpaseo/protocol/agent-state-bucket";
import { isDelegatedAgent } from "@getpaseo/protocol/agent-labels"; import { getParentAgentIdFromLabels, isDelegatedAgent } from "@getpaseo/protocol/agent-labels";
import { SortablePager } from "./pagination/sortable-pager.js"; import { SortablePager } from "./pagination/sortable-pager.js";
import type { PersistedProjectRecord, PersistedWorkspaceRecord } from "./workspace-registry.js"; import type { PersistedProjectRecord, PersistedWorkspaceRecord } from "./workspace-registry.js";
import { normalizeWorkspaceId } from "./workspace-registry-model.js"; import { normalizeWorkspaceId } from "./workspace-registry-model.js";
@@ -181,18 +181,34 @@ export class WorkspaceDirectory {
}); });
} }
for (const agent of agents) { const activeAgents = agents.filter(
if (agent.archivedAt) { (agent) => !agent.archivedAt && this.deps.isProviderVisibleToClient(agent.provider),
continue; );
} const activeAgentsById = new Map(activeAgents.map((agent) => [agent.id, agent] as const));
if (!this.deps.isProviderVisibleToClient(agent.provider)) {
continue; for (const agent of activeAgents) {
} let workspaceAgent = agent;
let bucket: WorkspaceDescriptorPayload["status"];
if (isDelegatedAgent(agent)) { if (isDelegatedAgent(agent)) {
continue; if (agent.status !== "running") {
continue;
}
const parentAgent = resolveDelegationRootAgent(agent, activeAgentsById);
if (!parentAgent) {
continue;
}
workspaceAgent = parentAgent;
bucket = "running";
} else {
bucket = deriveAgentStateBucket({
status: agent.status,
pendingPermissionCount: agent.pendingPermissions?.length ?? 0,
requiresAttention: agent.requiresAttention,
attentionReason: agent.attentionReason ?? null,
});
} }
const workspaceId = workspaceIdsByDirectory.get(normalizeWorkspaceId(agent.cwd)); const workspaceId = workspaceIdsByDirectory.get(normalizeWorkspaceId(workspaceAgent.cwd));
if (workspaceId === undefined) { if (workspaceId === undefined) {
continue; continue;
} }
@@ -201,12 +217,6 @@ export class WorkspaceDirectory {
continue; continue;
} }
const bucket = deriveAgentStateBucket({
status: agent.status,
pendingPermissionCount: agent.pendingPermissions?.length ?? 0,
requiresAttention: agent.requiresAttention,
attentionReason: agent.attentionReason ?? null,
});
if ( if (
getWorkspaceStateBucketPriority(bucket) < getWorkspaceStateBucketPriority(existing.status) getWorkspaceStateBucketPriority(bucket) < getWorkspaceStateBucketPriority(existing.status)
) { ) {
@@ -336,3 +346,27 @@ export class WorkspaceDirectory {
}; };
} }
} }
function resolveDelegationRootAgent(
agent: AgentSnapshotPayload,
activeAgentsById: ReadonlyMap<string, AgentSnapshotPayload>,
): AgentSnapshotPayload | null {
const seen = new Set<string>([agent.id]);
let current = agent;
while (true) {
const parentAgentId = getParentAgentIdFromLabels(current.labels);
if (!parentAgentId) {
return current;
}
if (seen.has(parentAgentId)) {
return null;
}
const parent = activeAgentsById.get(parentAgentId);
if (!parent) {
return null;
}
seen.add(parentAgentId);
current = parent;
}
}