mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-04 07:33:18 +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.
This commit is contained in:
@@ -145,6 +145,7 @@ export const DEFAULT_CLIENT_CAPABILITIES = {
|
||||
[CLIENT_CAPS.workspaceSetupBlocked]: true,
|
||||
[CLIENT_CAPS.workspaceSetupObservation]: true,
|
||||
[CLIENT_CAPS.pluginBinaryPayloads]: true,
|
||||
[CLIENT_CAPS.backgroundAgents]: true,
|
||||
[CLIENT_CAPS.explicitEventSubscriptions]: true,
|
||||
} satisfies Record<Exclude<ClientCapability, typeof CLIENT_CAPS.browserHost>, true>;
|
||||
|
||||
|
||||
@@ -1110,6 +1110,7 @@ test("advertises client capabilities in hello", async () => {
|
||||
workspace_setup_blocked: true,
|
||||
workspace_setup_observation: true,
|
||||
hello_rejection: true,
|
||||
background_agents: true,
|
||||
browser_host: {
|
||||
supportedCommands: ["list_tabs"],
|
||||
hostKind: "desktop app",
|
||||
|
||||
@@ -504,6 +504,7 @@ export interface CreateAgentRequestOptions extends AgentConfigOverrides {
|
||||
git?: GitSetupOptions;
|
||||
worktree?: CreateAgentRequestMessage["worktree"];
|
||||
autoArchive?: CreateAgentRequestMessage["autoArchive"];
|
||||
/** Creates a background agent. A workspace this request creates inherits it. */
|
||||
background?: CreateAgentRequestMessage["background"];
|
||||
/** @deprecated Use createWorkspace({ source, agent, idempotencyKey }); rejected by createAgent. */
|
||||
worktreeName?: string;
|
||||
@@ -3293,13 +3294,24 @@ export class DaemonClient {
|
||||
return this.lastServerInfoMessage?.features?.backgroundWorkspaces === true;
|
||||
}
|
||||
|
||||
// Background on a request that creates its workspace predates background agents: older hosts
|
||||
// apply it to the new workspace. Placing a background agent in an existing workspace, or
|
||||
// under a caller, needs a host that knows background agents.
|
||||
private requireBackgroundCreation(options: CreateAgentRequestOptions): void {
|
||||
if (options.workspaceId === undefined && options.callerAgentId === undefined) {
|
||||
this.requireBackgroundWorkspaces();
|
||||
} else if (this.lastServerInfoMessage?.features?.backgroundAgents !== true) {
|
||||
throw new Error("Update the host to use background agents.");
|
||||
}
|
||||
}
|
||||
|
||||
async createAgent(options: CreateAgentRequestOptions): Promise<AgentSnapshotPayload> {
|
||||
if (options.git !== undefined || options.worktreeName !== undefined) {
|
||||
throw new Error(
|
||||
"Legacy git/worktreeName agent placement was removed. Use createWorkspace({ source, agent, idempotencyKey }) or create a workspace first and pass workspaceId.",
|
||||
);
|
||||
}
|
||||
if (options.background !== undefined) this.requireBackgroundWorkspaces();
|
||||
if (options.background !== undefined) this.requireBackgroundCreation(options);
|
||||
const result = await this.creations.createAgent({
|
||||
...options,
|
||||
config: resolveAgentConfig(options),
|
||||
|
||||
@@ -747,6 +747,88 @@ test("agents.create with internal fails before sending on a host without interna
|
||||
await client.close();
|
||||
});
|
||||
|
||||
async function createWorkspaceHandle(features: Record<string, boolean>) {
|
||||
const { client, ws } = await connectClient(features);
|
||||
const creating = client.workspaces.create({
|
||||
source: { kind: "directory", path: "/repo/sdk" },
|
||||
});
|
||||
const request = parseSentSessionMessage(ws.sent.at(-1));
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "workspace.create.response",
|
||||
payload: {
|
||||
requestId: request.requestId,
|
||||
workspace: createWorkspace({ id: "workspace_open" }),
|
||||
setupTerminalId: null,
|
||||
error: null,
|
||||
},
|
||||
}),
|
||||
);
|
||||
return { client, ws, workspace: await creating };
|
||||
}
|
||||
|
||||
test("a background agent in an existing workspace fails before sending on a host without background agents", async () => {
|
||||
const { client, ws, workspace } = await createWorkspaceHandle({
|
||||
providerUsageList: true,
|
||||
providersSnapshotCwd: true,
|
||||
ownedSubscriptions: true,
|
||||
backgroundWorkspaces: true,
|
||||
});
|
||||
const sentBefore = ws.sent.length;
|
||||
|
||||
await expect(
|
||||
workspace.agents.create({ config: { provider: "codex/gpt-5.4" }, background: true }),
|
||||
).rejects.toThrow("Update the host to use background agents.");
|
||||
expect(ws.sent).toHaveLength(sentBefore);
|
||||
|
||||
const creating = client.agents.create({
|
||||
config: { provider: "codex/gpt-5.4" },
|
||||
cwd: "/repo/sdk",
|
||||
background: true,
|
||||
});
|
||||
expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({
|
||||
type: "create_agent_request",
|
||||
background: true,
|
||||
});
|
||||
expect(parseSentSessionMessage(ws.sent.at(-1))).not.toHaveProperty("workspaceId");
|
||||
creating.catch(() => {});
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test("a background agent in an existing workspace is sent to a host with background agents", async () => {
|
||||
const { client, ws, workspace } = await createWorkspaceHandle({
|
||||
providerUsageList: true,
|
||||
providersSnapshotCwd: true,
|
||||
ownedSubscriptions: true,
|
||||
backgroundWorkspaces: true,
|
||||
backgroundAgents: true,
|
||||
});
|
||||
|
||||
const creating = workspace.agents.create({
|
||||
config: { provider: "codex/gpt-5.4" },
|
||||
background: true,
|
||||
});
|
||||
const request = parseSentSessionMessage(ws.sent.at(-1));
|
||||
expect(request).toMatchObject({
|
||||
type: "create_agent_request",
|
||||
workspaceId: "workspace_open",
|
||||
background: true,
|
||||
});
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "status",
|
||||
payload: {
|
||||
status: "agent_created",
|
||||
requestId: request.requestId,
|
||||
agentId: "agent_background",
|
||||
agent: createAgent({ id: "agent_background", workspaceId: "workspace_open" }),
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect((await creating).id).toBe("agent_background");
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test("agent handles delegate create, send, timeline refetch, archive, and local updates", async () => {
|
||||
const { client, ws } = await connectClient();
|
||||
const createdAgent = createAgent();
|
||||
|
||||
@@ -264,7 +264,11 @@ export interface PaseoAgentCreateOptions {
|
||||
git?: CreateAgentRequestMessage["git"];
|
||||
worktree?: CreateAgentRequestMessage["worktree"];
|
||||
autoArchive?: CreateAgentRequestMessage["autoArchive"];
|
||||
/** Visibility intent for a NEW workspace. Existing targets reject this option. */
|
||||
/**
|
||||
* Creates a background agent: hidden from default lists, tabs, the parent's subagent track
|
||||
* and notifications, and still addressable by id. A workspace this call creates is
|
||||
* background too. With a parent and no value, the parent's own background applies.
|
||||
*/
|
||||
background?: CreateAgentRequestMessage["background"];
|
||||
requestId?: string;
|
||||
labels?: Record<string, string>;
|
||||
|
||||
Reference in New Issue
Block a user