Give plugins an agent's background work count and a background flag on hooks (#9362)

* feat(plugins): give plugins the agent's sidebar indicator and background flag

Plugin UI re-derived agent state from status and attention fields and could
not see background work, so it showed agents as done while a shell they left
running was still live. Agent snapshots now carry the sidebar's own bucket as
indicator/indicatorLabel, computed by the same function the sidebar uses so
the two cannot drift.

Lifecycle hooks and the agent.open.before request now carry `background`, so a
hook can skip a hidden helper agent without a lookup. A hook cannot change it;
an omitted value means unchanged, like metadata.

`finished` attention still means the turn finished; background work is the
indicator's concern.

* fix(plugins): keep session-open hooks that omit background typechecking

A hook that returns its own object instead of spreading the request has no
background field; the result type now treats it as optional, like metadata,
matching the runtime where an omitted value means unchanged.

* refactor(plugins): give agent snapshots the background work count instead of an indicator

* fix(plugins): keep legacy session-open hooks that omit background typechecking
This commit is contained in:
Mohamed Boudra
2026-11-11 15:10:13 +07:00
committed by GitHub
parent 88b46c6781
commit a886bfbcdd
24 changed files with 283 additions and 28 deletions
+17
View File
@@ -370,6 +370,17 @@ describe("plugin agent tools SDK boundary", () => {
...request.mcpServers, company: { type: "http", url: "https://tools.example.com/mcp" },
} };
});
server.addHook("agent.open.before", async ({ request }) => {
const background: boolean = request.background;
if (background) return;
// A hook written before background and metadata returns its own object without them.
return {
agentId: request.agentId, workspaceId: request.workspaceId, provider: request.provider,
cwd: request.cwd, reason: request.reason, purpose: request.purpose, title: request.title,
labels: request.labels, tools: request.tools, env: request.env,
mcpServers: request.mcpServers,
};
});
return () => {};
}
`),
@@ -429,6 +440,12 @@ describe("lifecycle hook SDK types", () => {
// @ts-expect-error A prepare handler must return the whole request or undefined.
server.addHook("agent.create.before", async () => ({ invalid: true }));
server.before("agent.session_open", ({ request }) => request);
server.before("agent.session_open", ({ request }) => ({
agentId: request.agentId, workspaceId: request.workspaceId, provider: request.provider,
cwd: request.cwd, reason: request.reason, purpose: request.purpose, title: request.title,
labels: request.labels, metadata: request.metadata, tools: request.tools, env: request.env,
mcpServers: request.mcpServers,
}));
server.on("agent.turn_ended", () => {});
return () => {};
}
+4
View File
@@ -61,6 +61,10 @@ export interface PluginAgentSnapshot {
readonly workspaceId: string;
readonly provider: string;
readonly status: "initializing" | "idle" | "running" | "error" | "closed";
/** Shells and monitors the agent left running after its turn. 0 when none are live. */
readonly backgroundWork: { readonly count: number };
/** A helper hidden from tabs, lists and attention. Fixed at creation. */
readonly background: boolean;
readonly createdAt: string;
readonly updatedAt: string;
readonly lastActivityAt: string;
+11 -2
View File
@@ -71,6 +71,8 @@ export interface PluginHookAgent {
provider: string;
cwd: string;
title: string | null;
/** A helper hidden from tabs, lists and attention. Fixed at creation. */
readonly background: boolean;
/** Data plugins stored on the agent. Paseo never reads a key. */
metadata: Record<string, JsonValue>;
}
@@ -83,6 +85,8 @@ export interface PluginSessionOpenRequest {
reason: "create" | "resume" | "refresh" | "import";
purpose: "interactive" | "history";
readonly title: string | null;
/** A helper hidden from tabs, lists and attention. Fixed at creation. */
readonly background: boolean;
readonly labels: Readonly<Record<string, string>>;
readonly metadata: Readonly<Record<string, JsonValue>>;
tools: AgentToolDefinition[];
@@ -134,7 +138,9 @@ export interface PluginHookRequests {
/** What a hook may return. An omitted `metadata` stays as it was. */
type PluginHookRequestResults = Omit<PluginHookRequests, "agent.open.before"> & {
"agent.open.before": Omit<PluginSessionOpenRequest, "metadata"> & {
// Read-only fields added after the first SDK stay optional in a result; omitted means unchanged.
"agent.open.before": Omit<PluginSessionOpenRequest, "metadata" | "background"> & {
readonly background?: boolean;
metadata?: Readonly<Record<string, JsonValue>>;
};
};
@@ -184,7 +190,7 @@ export interface PluginLifecycleRegistration {
handler: (
input: { request: PluginBeforeRequests[Name] },
context: PluginHookContext,
) => PluginBeforeRequests[Name] | void | Promise<PluginBeforeRequests[Name] | void>,
) => PluginBeforeResults[Name] | void | Promise<PluginBeforeResults[Name] | void>,
): () => void;
}
@@ -229,6 +235,9 @@ export type PluginLifecycleEvents = {
export type PluginBeforeRequests = {
[Name in keyof typeof legacyRequests]: PluginHookRequests[(typeof legacyRequests)[Name]];
};
type PluginBeforeResults = {
[Name in keyof typeof legacyRequests]: PluginHookRequestResults[(typeof legacyRequests)[Name]];
};
/** @internal SDK adapter for the daemon's hook registry. */
export function createLifecycleRegistration(