Files
PaperClipAI/server/src/services/agent-lifecycle.ts
T
2026-10-31 09:07:52 -05:00

100 lines
5.9 KiB
TypeScript

import { notifyRunDispatch } from "./run-dispatch-notifications.js";
import { notifyNativeReviewResponse } from "./native-review-response-notifications.js";
import { isAgentStatusInvokable } from "@paperclipai/shared";
import { notifyNativeResponsePresentation } from "./native-response-presentation-notifications.js";
import { notifyChatInboundWakeNotices } from "./chat-inbound-wake-notice-notifications.js";
import { logger } from "../middleware/logger.js";
import { companies, type Db } from "@paperclipai/db";
import { eq } from "drizzle-orm";
import * as lifecycle from "../modules/agent-lifecycle/index.js";
import { withAccountingTransaction } from "./accounting-transaction.js";
import { budgetServiceInTransaction, policyBlocks, type BudgetServiceHooks } from "./budgets.js";
import { recordAgentStatusEvent, recordResourceCreationEvent } from "./resource-lifecycle-events.js";
import { clearPrimaryAgent } from "./primary-agent.js";
import { deleteAgentDependencies } from "./agent-deletion.js";
import { prepareAgentConfiguration, completeAgentConfiguration } from "./agent-configuration.js";
import { prepareAgentHire, initializeAgentHire, prepareAgentHireApproval, completeAgentHireApproval } from "./agent-hiring.js";
import { DELIVERY_QUEUES, notifyDeliveryWork } from "./delivery-work-notifications.js";
import { fenceActiveCompanyAgentStopsInTransaction as fenceActiveCompanyStops } from "../modules/agent-lifecycle/index.js";
import { applyAgentPolicyHoldsInTransaction } from "../modules/agent-lifecycle/index.js";
import { trackIdleWork } from "./task-admission.js";
import { environmentService } from "./environments.js";
import { instanceSettingsService } from "./instance-settings.js";
import { pluginEnvironmentRequiresAgent } from "./plugin-environment-driver.js";
export { scheduleAgentLifecycle, processAgentLifecycle } from "../modules/agent-lifecycle/index.js";
export type { LifecycleDriver, LifecycleAgent } from "../modules/agent-lifecycle/index.js";
export function createAgentLifecycleEffects(_hooks: BudgetServiceHooks = {}): lifecycle.LifecycleEffects {
return {
async setupRequiresPreparation(db, companyId, environmentId) {
const id = environmentId ?? (await instanceSettingsService(db).get()).defaultEnvironmentId;
const environment = id ? await environmentService(db).getById(id) : null;
if (!environment || !await pluginEnvironmentRequiresAgent(db, environment)) return false;
const [company] = await db.select({ requiresApproval: companies.requireBoardApprovalForNewAgents }).from(companies).where(eq(companies.id, companyId));
if (!company || company.requiresApproval) return false;
const bound = await environmentService(db).listBoundCompanyIds(environment.id);
if (bound.length && !bound.includes(companyId)) throw new lifecycle.AgentLifecycleConflict("The environment belongs to another company");
return true;
},
notifyWork: tx => notifyDeliveryWork(tx, DELIVERY_QUEUES.agentLifecycle),
async beforeStatusChange(tx, companyId, agentId, before, after) {
if (before !== after) {
await notifyRunDispatch(tx, { companyId });
await notifyDeliveryWork(tx, DELIVERY_QUEUES.agentTimers);
}
if (!isAgentStatusInvokable(before) && isAgentStatusInvokable(after)) {
await notifyNativeResponsePresentation(tx, { companyId, agentId });
await notifyNativeReviewResponse(tx, { companyId, agentId });
await notifyChatInboundWakeNotices(tx, { companyId });
}
},
reportFailure(context, error) {
const code = lifecycle.lifecycleFailureCode(error);
logger.warn({ ...context, failureCode: code ?? "unclassified" }, "Agent lifecycle operation failed");
},
transaction: withAccountingTransaction,
deleteDependencies: deleteAgentDependencies,
policyBlocks,
setAgentBudget: (db, publications, companyId, agentId, amount, userId, isActive) =>
budgetServiceInTransaction(db, publications).upsertPolicy(companyId, {
scopeType: "agent", scopeId: agentId, amount, isActive, windowKind: "calendar_month_utc",
}, userId),
recordCreation: (db, companyId, agentId) => recordResourceCreationEvent(db, companyId, "agent", agentId),
recordStatus: recordAgentStatusEvent,
clearPrimary: clearPrimaryAgent,
prepareConfiguration: prepareAgentConfiguration,
completeConfiguration: completeAgentConfiguration,
prepareHire: prepareAgentHire,
initializeHire: initializeAgentHire,
prepareHireApproval: prepareAgentHireApproval,
completeHireApproval: completeAgentHireApproval,
};
}
export function createAgentLifecycle(db: Db, hooks: BudgetServiceHooks = {}) {
return lifecycle.createAgentLifecycle(db, createAgentLifecycleEffects(hooks));
}
function trackedDriver(driver: lifecycle.LifecycleDriver): lifecycle.LifecycleDriver {
return { ...driver, runHost: agent => trackIdleWork(driver.runHost(agent)),
runPlugin: (agent, pluginId) => trackIdleWork(driver.runPlugin(agent, pluginId)) };
}
export function configureAgentLifecycle(db: Db, driver: lifecycle.LifecycleDriver, canRun = () => true) {
return lifecycle.configureAgentLifecycle(db, createAgentLifecycleEffects(), trackedDriver(driver), canRun);
}
export function startAgentLifecycle(db: Db, driver: lifecycle.LifecycleDriver, canRun: () => boolean) {
return lifecycle.startAgentLifecycle(db, createAgentLifecycleEffects(), trackedDriver(driver), canRun);
}
/** Budget evaluation owns the transaction and supplies the aggregate agent decision. */
export function applyAgentBudgetHoldInTransaction(tx: Db, companyId: string, agentId: string, blocked: boolean) {
return applyAgentPolicyHoldsInTransaction(tx, createAgentLifecycleEffects(), companyId, agentId, { budget: blocked });
}
export function fenceActiveCompanyAgentStopsInTransaction(tx: Db, companyId: string, status: "paused" | "archived") {
return fenceActiveCompanyStops(tx, createAgentLifecycleEffects(), companyId, status);
}