Files
paseo/plugin-examples/lifecycle-actions/index.server.ts
T
Mohamed Boudra a886bfbcdd 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
2026-11-11 15:10:13 +07:00

98 lines
2.7 KiB
TypeScript

import type { PluginServerContext } from "@getpaseo/plugin/server";
import { latestOutputText, shellCommand } from "./server/inspect";
export default function contribute(server: PluginServerContext) {
server.addHook("workspace.create.before", async ({ request }) => {
if (request.title === "Isolated lifecycle example" && request.source.kind === "directory") {
return {
...request,
source: {
kind: "worktree",
cwd: request.source.path,
action: "branch-off",
branchName: "lifecycle-example",
},
};
}
return request;
});
server.addHook("agent.turn.after", async (event, context) => {
// A background agent reports to the agent that created it, which decides whether to retry.
if (event.outcome.kind === "canceled" || event.agent.background) {
return;
}
const agent = context.paseo.agents.ref(event.agent.id);
const page = await agent.timeline.refetch({ direction: "tail", limit: 20 });
const text = latestOutputText(page.entries.map(({ item }) => item));
if (/out of credits/i.test(text)) {
console.log("Sending a follow-up after out of credits", event.agent.id);
await agent.send("Try again.");
}
});
server.addHook("agent.permission.before", async (event, context) => {
const command = shellCommand(event.request);
if (command === null) {
return;
}
const agent = context.paseo.agents.ref(event.agent.id);
if (/\brm\s+-rf\b/.test(command)) {
await agent.respondToPermission({
requestId: event.request.id,
response: {
behavior: "deny",
message: "Recursive deletion is blocked by the example plugin.",
},
});
return;
}
if (command.trim() === "git status") {
await agent.respondToPermission({
requestId: event.request.id,
response: { behavior: "allow" },
});
}
});
server.addHook("agent.create.before", async ({ request }) => {
return {
...request,
env: {
...request.env,
PASEO_HOOK_CREATE_EXAMPLE: "created",
},
};
});
server.addHook("agent.create.before", async ({ request }) => {
if (request.config.provider === "codex") {
return {
...request,
config: {
...request.config,
provider: "claude",
model: "haiku",
modeId: "default",
providerOptions: undefined,
},
};
}
return request;
});
server.addHook("agent.open.before", async ({ request }) => {
return {
...request,
env: {
...request.env,
PASEO_HOOK_OPEN_EXAMPLE: "opened",
},
};
});
return () => {};
}