Surface cross-workspace subagents in their workspaces (#2061)
* fix(workspaces): show provider subagent activity Provider subagent lifecycle lives outside managed agent snapshots, so workspace aggregation dropped it after the parent turn finished. Count running provider subagents against the delegation root and emit workspace updates when their status changes. * fix(workspaces): surface cross-workspace subagents
This commit is contained in:
@@ -375,7 +375,16 @@ describe("WorkspaceDirectory", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("running delegated child contributes running to the parent workspace, not its worktree", async () => {
|
||||
test("running same-workspace subagent contributes running to its parent workspace", async () => {
|
||||
const workspace = new WorkspaceStatus();
|
||||
|
||||
workspace.hasRootAgent({ id: "parent-agent", status: "idle" });
|
||||
workspace.hasDelegatedAgent({ id: "child-agent", status: "running" });
|
||||
|
||||
await expect(workspace.workspaceStatus()).resolves.toBe("running");
|
||||
});
|
||||
|
||||
test("running cross-workspace subagent contributes to its own workspace", async () => {
|
||||
const workspace = new WorkspaceStatus();
|
||||
|
||||
workspace.hasWorktreeWorkspace();
|
||||
@@ -383,8 +392,25 @@ describe("WorkspaceDirectory", () => {
|
||||
workspace.hasDelegatedAgentInWorktree({ id: "child-agent", status: "running" });
|
||||
|
||||
await expect(workspace.workspaceStatuses()).resolves.toEqual({
|
||||
"workspace-1": "running",
|
||||
"workspace-worktree": "done",
|
||||
"workspace-1": "done",
|
||||
"workspace-worktree": "running",
|
||||
});
|
||||
});
|
||||
|
||||
test("cross-workspace subagent contributes its full status bucket to its own workspace", async () => {
|
||||
const workspace = new WorkspaceStatus();
|
||||
|
||||
workspace.hasWorktreeWorkspace();
|
||||
workspace.hasRootAgent({ id: "parent-agent", status: "idle" });
|
||||
workspace.hasDelegatedAgentInWorktree({
|
||||
id: "child-agent",
|
||||
status: "idle",
|
||||
pendingPermissionCount: 1,
|
||||
});
|
||||
|
||||
await expect(workspace.workspaceStatuses()).resolves.toEqual({
|
||||
"workspace-1": "done",
|
||||
"workspace-worktree": "needs_input",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
getWorkspaceStateBucketPriority,
|
||||
type WorkspaceStateBucket,
|
||||
} from "@getpaseo/protocol/agent-state-bucket";
|
||||
import { getParentAgentIdFromLabels, isDelegatedAgent } from "@getpaseo/protocol/agent-labels";
|
||||
import { getParentAgentIdFromLabels } from "@getpaseo/protocol/agent-labels";
|
||||
import { SortablePager } from "./pagination/sortable-pager.js";
|
||||
import type { PersistedProjectRecord, PersistedWorkspaceRecord } from "./workspace-registry.js";
|
||||
import { resolveProjectDisplayName } from "./workspace-registry.js";
|
||||
@@ -273,8 +273,9 @@ export class WorkspaceDirectory {
|
||||
|
||||
// Aggregate each agent's state bucket into its owning workspace descriptor,
|
||||
// keeping the highest-priority bucket. A record's owner IS its `workspaceId`;
|
||||
// status never fans out to same-cwd siblings. Delegated agents contribute to
|
||||
// their delegation root's workspace; their own status is ignored unless running.
|
||||
// status never fans out to same-cwd siblings. A subagent in another workspace
|
||||
// is a root for that workspace. Same-workspace descendants contribute only
|
||||
// running activity to the nearest ancestor in that workspace.
|
||||
private applyAgentBucketContributions(params: {
|
||||
activeAgents: AgentSnapshotPayload[];
|
||||
descriptorsByWorkspaceId: Map<string, WorkspaceDescriptorPayload>;
|
||||
@@ -283,26 +284,22 @@ export class WorkspaceDirectory {
|
||||
const activeAgentsById = new Map(activeAgents.map((agent) => [agent.id, agent] as const));
|
||||
|
||||
for (const agent of activeAgents) {
|
||||
let workspaceAgent = agent;
|
||||
let bucket: WorkspaceDescriptorPayload["status"];
|
||||
if (isDelegatedAgent(agent)) {
|
||||
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 workspaceAgent = resolveWorkspaceRootAgent(agent, activeAgentsById);
|
||||
if (!workspaceAgent) {
|
||||
continue;
|
||||
}
|
||||
const isWorkspaceRoot = workspaceAgent.id === agent.id;
|
||||
if (!isWorkspaceRoot && agent.status !== "running") {
|
||||
continue;
|
||||
}
|
||||
const bucket = isWorkspaceRoot
|
||||
? deriveAgentStateBucket({
|
||||
status: agent.status,
|
||||
pendingPermissionCount: agent.pendingPermissions?.length ?? 0,
|
||||
requiresAttention: agent.requiresAttention,
|
||||
attentionReason: agent.attentionReason ?? null,
|
||||
})
|
||||
: "running";
|
||||
|
||||
const workspaceId = workspaceAgent.workspaceId;
|
||||
if (!workspaceId) {
|
||||
@@ -619,7 +616,7 @@ function groupAgentsByWorkspaceId(
|
||||
return byWorkspaceId;
|
||||
}
|
||||
|
||||
function resolveDelegationRootAgent(
|
||||
function resolveWorkspaceRootAgent(
|
||||
agent: AgentSnapshotPayload,
|
||||
activeAgentsById: ReadonlyMap<string, AgentSnapshotPayload>,
|
||||
): AgentSnapshotPayload | null {
|
||||
@@ -638,6 +635,9 @@ function resolveDelegationRootAgent(
|
||||
if (!parent) {
|
||||
return null;
|
||||
}
|
||||
if (parent.workspaceId !== current.workspaceId) {
|
||||
return current;
|
||||
}
|
||||
seen.add(parentAgentId);
|
||||
current = parent;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user