mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-07 08:54:42 +00:00
* 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
84 lines
2.7 KiB
TypeScript
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;
|
|
}
|