Files
paseo/packages/app/src/utils/workspace-agent-activity.ts
T
Mohamed Boudra a880c9619d feat: background agents for hidden helpers (#8683)
* feat(server): add background agents

An agent can be created background in any workspace. It stays a durable,
addressable agent, but default discovery, workspace status and attention
leave it out. Effective background is agent.background ||
workspace.background.

- Protocol: `create_agent_request.background` now marks the agent itself
  and stays the workspace intent for a workspace the request creates.
  Snapshots carry `background`. The daemon advertises
  `features.backgroundAgents`. Clients advertise the `background_agents`
  capability.
- Daemon: the flag persists in the agent record, so restart and resume
  keep it. AgentDirectory owns visibility. Inclusive reads and
  subscriptions from sources without `background_agents` see only agents
  in background workspaces, because older apps would otherwise open a tab
  for each one.
- Inheritance: omitted on agent-scoped creation, children and new
  workspaces take the caller agent's background.
- MCP: create_agent accepts `background`.
- SDK: placing a background agent in an existing workspace, or under a
  caller, fails fast with "Update the host to use background agents." on
  older hosts.
- CLI: `ls -a` and `ls --background` add a BACKGROUND column.

* feat(app): hide background agents from tabs, track and status

* docs(plugins): point hidden helpers at background agents

* fix: keep background agents hidden across cache reload and connected hosts

* fix: keep remote creation working on older hosts and inherit foreign parents' background
2026-11-04 07:19:21 +07:00

84 lines
2.7 KiB
TypeScript

import type { Agent, WorkspaceDescriptor } from "@/stores/session-store";
import { isWorkspaceRootAgent } from "@/subagents/policies";
import { deriveSidebarStateBucket } from "./sidebar-agent-state";
export interface WorkspaceAgentActivity {
agentId: string;
status: WorkspaceDescriptor["status"];
enteredAt: Date | null;
}
function workspaceAgentStatus(agent: Agent): Agent["status"] {
if (agent.turn.phase === "open") return "running";
return agent.status === "running" ? "idle" : agent.status;
}
export function buildWorkspaceAgentActivityIndex(
agents: ReadonlyMap<string, Agent>,
previous?: ReadonlyMap<string, WorkspaceAgentActivity>,
): Map<string, WorkspaceAgentActivity> {
const activityByWorkspaceId = new Map<string, WorkspaceAgentActivity>();
const latestActivityAtByWorkspaceId = new Map<string, Date>();
for (const agent of agents.values()) {
const parentAgent = agent.parentAgentId ? agents.get(agent.parentAgentId) : undefined;
if (
agent.archivedAt ||
agent.background ||
!agent.workspaceId ||
!isWorkspaceRootAgent(agent, parentAgent)
) {
continue;
}
const enteredAt = agent.attentionTimestamp ?? agent.updatedAt;
const latestActivityAt = latestActivityAtByWorkspaceId.get(agent.workspaceId);
if (latestActivityAt && enteredAt <= latestActivityAt) {
continue;
}
latestActivityAtByWorkspaceId.set(agent.workspaceId, enteredAt);
const status = deriveSidebarStateBucket({
status: workspaceAgentStatus(agent),
pendingPermissionCount: agent.pendingPermissions.length,
requiresAttention: agent.requiresAttention,
attentionReason: agent.attentionReason,
});
activityByWorkspaceId.set(agent.workspaceId, {
agentId: agent.id,
status,
enteredAt,
});
}
for (const [workspaceId, activity] of activityByWorkspaceId) {
const previousActivity = previous?.get(workspaceId);
if (
previousActivity?.agentId === activity.agentId &&
previousActivity.status === activity.status
) {
activityByWorkspaceId.set(workspaceId, previousActivity);
}
}
if (previous && areWorkspaceAgentActivityIndexesIdentical(previous, activityByWorkspaceId)) {
return previous instanceof Map ? previous : new Map(previous);
}
return activityByWorkspaceId;
}
function areWorkspaceAgentActivityIndexesIdentical(
previous: ReadonlyMap<string, WorkspaceAgentActivity>,
next: ReadonlyMap<string, WorkspaceAgentActivity>,
): boolean {
if (previous.size !== next.size) {
return false;
}
for (const [workspaceId, activity] of next) {
if (previous.get(workspaceId) !== activity) {
return false;
}
}
return true;
}