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.
This commit is contained in:
Mohamed Boudra
2026-11-03 13:38:08 +01:00
parent eb8b5b8f94
commit 129102d5c6
30 changed files with 823 additions and 139 deletions
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import type { AgentSnapshotPayload } from "@getpaseo/protocol/messages";
import { render } from "../../output/index.js";
import { buildAgentLsFetchOptions, listAgents } from "./ls.js";
const daemonTarget = { kind: "endpoint" as const, host: "example.test:12345" };
@@ -130,4 +131,26 @@ describe("runLsCommand", () => {
expect(result.data.map((item) => item.name)).toEqual(["agent 3", "agent 2", "agent 1"]);
});
it("marks background agents in a BACKGROUND column only for inclusive listings", async () => {
const client = createPagingClient();
const fetchAgents: typeof client.fetchAgents = async (options) => {
const page = await client.fetchAgents(options);
return {
...page,
entries: page.entries.map(({ agent }) => ({
agent: agent.title === "agent 2" ? { ...agent, background: true } : agent,
})),
};
};
const connect = async () => ({ ...client, fetchAgents });
const inclusive = await listAgents({ daemonTarget, all: true, global: true }, connect);
const rows = render(inclusive, { noColor: true }).split("\n");
expect(rows[0]).toMatch(/BACKGROUND\s*$/);
expect(rows.slice(1).map((row) => row.trim().split(/\s+/).at(-1))).toEqual(["-", "yes", "-"]);
const ordinary = await listAgents({ daemonTarget, global: true }, connect);
expect(render(ordinary, { noColor: true })).not.toContain("BACKGROUND");
});
});
+13 -2
View File
@@ -15,7 +15,7 @@ export function addLsOptions(cmd: Command): Command {
.description("List agents. By default excludes archived agents.")
.option("-a, --all", "Include archived and background agents")
.option("-g, --global", "List agents across all directories")
.option("--background", "Include background workspace agents")
.option("--background", "Include background agents and agents in background workspaces")
.option(
"--label <key=value>",
"Filter by label (can be used multiple times)",
@@ -35,6 +35,7 @@ export interface AgentListItem {
status: string;
cwd: string;
created: string;
background: boolean;
}
/** Helper to get relative time string */
@@ -89,6 +90,15 @@ export const agentLsSchema: OutputSchema<AgentListItem> = {
],
};
/** Inclusive listings mark background agents so they read apart from visible ones. */
const agentLsInclusiveSchema: OutputSchema<AgentListItem> = {
...agentLsSchema,
columns: [
...agentLsSchema.columns,
{ header: "BACKGROUND", field: (item) => (item.background ? "yes" : "-"), width: 10 },
],
};
/** Transform agent snapshot to AgentListItem */
function toListItem(agent: AgentSnapshotPayload): AgentListItem {
const model = normalizeModelId(agent.runtimeInfo?.model) ?? normalizeModelId(agent.model);
@@ -101,6 +111,7 @@ function toListItem(agent: AgentSnapshotPayload): AgentListItem {
status: agent.status,
cwd: shortenPath(agent.cwd),
created: relativeTime(agent.createdAt),
background: agent.background === true,
};
}
@@ -259,7 +270,7 @@ export async function listAgents(
return {
type: "list",
data: items,
schema: agentLsSchema,
schema: options.all || options.background ? agentLsInclusiveSchema : agentLsSchema,
};
} catch (err) {
await client.close().catch(() => {});