mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Show delegated agent activity on parent workspaces
This commit is contained in:
@@ -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`):
|
||||||
|
|||||||
@@ -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",
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user