Files
PaperClipAI/server/src/services/agent-lifecycle-driver.ts
T

69 lines
4.8 KiB
TypeScript

import { and, eq, inArray } from "drizzle-orm";
import { agents, environments, plugins, pluginCompanySettings, type Db } from "@paperclipai/db";
import { instanceSettingsService } from "./instance-settings.js";
import { pluginHasAgentEnvironments, pluginServesAgentEnvironment } from "./plugin-environment-driver.js";
import { heartbeatService } from "./heartbeat.js";
import { listInvalidOrgChainDescendantIds } from "./agent-invokability.js";
import type { PluginWorkerManager } from "./plugin-worker-manager.js";
import type { LifecycleDriver } from "../modules/agent-lifecycle/index.js";
import { agentHarnessVerificationService } from "./agent-harness-verification.js";
import { agentInstanceSecretService } from "./agent-instance-secrets.js";
export function createLifecycleDriver(db: Db, manager: PluginWorkerManager): LifecycleDriver {
const { verify } = agentHarnessVerificationService(db, manager);
const heartbeat = heartbeatService(db, { pluginWorkerManager: manager });
return {
async onReady(agent) {
await heartbeat.resumeQueuedRunsForAgent(agent.companyId, agent.id);
},
async requiredPluginIds(agent) {
const selected = agent.lifecycleRequiredPluginIds;
const installed = await db.select().from(plugins).where(inArray(plugins.status, ["ready", "error", "installed", "upgrade_pending"]));
const disabled = await db.select({ pluginId: pluginCompanySettings.pluginId }).from(pluginCompanySettings)
.where(and(eq(pluginCompanySettings.companyId, agent.companyId), eq(pluginCompanySettings.enabled, false)));
const candidates = installed.filter(plugin => !disabled.some(row => row.pluginId === plugin.id) &&
plugin.manifestJson.agentLifecycle && plugin.manifestJson.capabilities.includes("agents.lifecycle.manage"));
const [row] = await db.select({ environmentId: agents.defaultEnvironmentId }).from(agents).where(eq(agents.id, agent.id));
const environmentId = row?.environmentId ?? (await instanceSettingsService(db).get()).defaultEnvironmentId;
const [environment] = environmentId ? await db.select().from(environments).where(eq(environments.id, environmentId)) : [];
if (selected !== null) {
const added = candidates.filter(plugin => pluginServesAgentEnvironment(plugin, environment) && !selected.includes(plugin.id));
return [...selected, ...added.map(plugin => plugin.id)];
}
return candidates.filter(plugin => !pluginHasAgentEnvironments(plugin) || pluginServesAgentEnvironment(plugin, environment)).map(plugin => plugin.id);
},
async runPlugin(agent, pluginId) {
const [plugin] = await db.select().from(plugins).where(eq(plugins.id, pluginId));
const [settings] = await db.select().from(pluginCompanySettings).where(and(
eq(pluginCompanySettings.pluginId, pluginId), eq(pluginCompanySettings.companyId, agent.companyId)));
if (settings?.enabled === false || !plugin || plugin.status !== "ready" || !plugin.manifestJson.agentLifecycle || !plugin.manifestJson.capabilities.includes("agents.lifecycle.manage")) {
throw Object.assign(new Error("A required lifecycle plugin is unavailable"), { code: "required_plugin_unavailable" });
}
const operationId = agent.lifecycleOperation!.id;
const secrets = agentInstanceSecretService(db);
const instanceSecrets = ["preparing", "resuming"].includes(agent.lifecycleState)
? await secrets.read({ companyId: agent.companyId, agentId: agent.id, pluginId }) : {};
const request = { companyId: agent.companyId, agentId: agent.id, operationId, version: agent.lifecycleVersion, phase: agent.lifecycleState,
instanceSecrets };
const result = await manager.call(plugin.id, "agentLifecycle", request, 30_000);
if (!result || result.operationId !== operationId || result.version !== agent.lifecycleVersion || !["complete", "pending"].includes(result.status)) {
throw Object.assign(new Error("Invalid lifecycle result"), { code: "invalid_lifecycle_result" });
}
if (result.instanceSecrets !== undefined) await secrets.apply(request, pluginId, result.instanceSecrets, agent.lifecycleOperation?.leaseOwner);
return result.status;
},
async runHost(agent) {
if (agent.lifecycleState === "verifying") return verify(agent);
if (["pausing", "terminating", "cleaning_up"].includes(agent.lifecycleState)) {
const ids = agent.lifecycleState === "pausing" ? [agent.id]
: [agent.id, ...listInvalidOrgChainDescendantIds(agent.id,
await db.select().from(agents).where(eq(agents.companyId, agent.companyId)))];
const reason = agent.lifecycleHolds?.includes("company_archived")
? "Cancelled because the company was archived" : "Agent lifecycle stop requested";
if (!await heartbeat.stopInvocationsForAgents(ids, reason)) return "pending";
}
return "complete";
},
};
}