Files
AgentCompany/src/main/ipc-core-operations-handlers.ts
T
2026-03-18 16:13:17 -04:00

235 lines
8.8 KiB
TypeScript

import {
IPC_CHANNELS,
approvalDecisionSchema,
approvalInputSchema,
cancelRunSchema,
commentInputSchema,
heartbeatSettingsSchema,
listCommentsSchema,
runLogChunkSchema,
runLogSchema,
startRunSchema,
triggerHeartbeatSchema,
} from "@shared/contracts";
import { resolveApprovalDecision, shouldAutoApproveApproval } from "./approval-policy";
import { fail, ok, type RegisterCoreHandlersDependencies } from "./ipc-core-common";
export function registerCoreOperationHandlers({
registerHandle,
db,
ensureCompanyExists,
publishDomainChanged,
emitEvent,
wakeAgentIfPossible,
handleApprovalResolved,
dispatchAutomation,
queueTaskRun,
getWorkerProcess,
}: Pick<
RegisterCoreHandlersDependencies,
| "registerHandle"
| "db"
| "ensureCompanyExists"
| "publishDomainChanged"
| "emitEvent"
| "wakeAgentIfPossible"
| "handleApprovalResolved"
| "dispatchAutomation"
| "queueTaskRun"
| "getWorkerProcess"
>) {
registerHandle(IPC_CHANNELS.requestApproval, async (_event, payload) => {
const parsed = approvalInputSchema.parse(payload);
const companyError = ensureCompanyExists(parsed.companyId);
if (companyError) return companyError;
const invalidTask = parsed.relatedTaskId && !db.belongsToCompany("tasks", parsed.relatedTaskId, parsed.companyId);
const invalidAgent = parsed.requestedByAgentId && !db.belongsToCompany("agents", parsed.requestedByAgentId, parsed.companyId);
const invalidRelatedAgent = parsed.relatedAgentId && !db.belongsToCompany("agents", parsed.relatedAgentId, parsed.companyId);
if (invalidTask || invalidAgent || invalidRelatedAgent) {
return fail("INVALID_COMPANY_REFERENCE", "The approval references data outside the current company.");
}
let id: string;
try {
id = db.requestApproval(parsed);
} catch (error) {
return fail("APPROVAL_INVALID", error instanceof Error ? error.message : "Approval could not be created.");
}
db.addActivity({
companyId: parsed.companyId,
actor: parsed.requestedByAgentId ?? "board",
action: "approval.created",
entityType: "approval",
entityId: id,
detail: parsed.payloadSummary,
});
if (shouldAutoApproveApproval(db, parsed.companyId, parsed.type)) {
resolveApprovalDecision({
db,
handleApprovalResolved,
companyId: parsed.companyId,
approvalId: id,
state: "approved",
decisionNote: "Auto-approved by company policy",
detail: "Auto-approved by company policy",
});
publishDomainChanged();
return ok(id);
}
// Approval was NOT auto-approved — wake decision-makers to review it
dispatchAutomation("approval_created", {
companyId: parsed.companyId,
agentId: parsed.requestedByAgentId ?? undefined,
taskId: parsed.relatedTaskId ?? undefined,
});
publishDomainChanged();
return ok(id);
});
registerHandle(IPC_CHANNELS.decideApproval, async (_event, payload) => {
const parsed = approvalDecisionSchema.parse(payload);
const companyError = ensureCompanyExists(parsed.companyId);
if (companyError) return companyError;
if (!db.belongsToCompany("approvals", parsed.approvalId, parsed.companyId)) {
return fail("APPROVAL_NOT_FOUND", "Approval not found in the current company.");
}
const approval = db.getApprovalRecord(parsed.approvalId);
if (!approval || approval.companyId !== parsed.companyId) {
return fail("APPROVAL_NOT_FOUND", "Approval not found in the current company.");
}
resolveApprovalDecision({
db,
handleApprovalResolved,
companyId: approval.companyId,
approvalId: approval.id,
state: parsed.state,
decisionNote: parsed.decisionNote,
detail: parsed.decisionNote,
});
publishDomainChanged();
return ok(true);
});
registerHandle(IPC_CHANNELS.startTaskRun, async (_event, payload) => {
const parsed = startRunSchema.parse(payload);
const companyError = ensureCompanyExists(parsed.companyId);
if (companyError) return companyError;
if (!db.belongsToCompany("tasks", parsed.taskId, parsed.companyId)) {
return fail("TASK_NOT_FOUND", "Task not found in the current company.");
}
return queueTaskRun(parsed.taskId, "manual", "board");
});
registerHandle(IPC_CHANNELS.cancelRun, (_event, payload) => {
const parsed = cancelRunSchema.parse(payload);
const companyError = ensureCompanyExists(parsed.companyId);
if (companyError) return companyError;
if (!db.belongsToCompany("runs", parsed.runId, parsed.companyId)) {
return fail("RUN_NOT_FOUND", "Run not found in the current company.");
}
getWorkerProcess()?.postMessage({ type: "cancel-run", runId: parsed.runId });
return ok(true);
});
registerHandle(IPC_CHANNELS.getRunLog, (_event, payload) => {
const parsed = runLogSchema.parse(payload);
const companyError = ensureCompanyExists(parsed.companyId);
if (companyError) return companyError;
if (!db.belongsToCompany("runs", parsed.runId, parsed.companyId)) {
return fail("RUN_NOT_FOUND", "Run not found in the current company.");
}
return ok(db.readRunLog(parsed.runId));
});
registerHandle(IPC_CHANNELS.getRunLogChunk, (_event, payload) => {
const parsed = runLogChunkSchema.parse(payload);
const companyError = ensureCompanyExists(parsed.companyId);
if (companyError) return companyError;
if (!db.belongsToCompany("runs", parsed.runId, parsed.companyId)) {
return fail("RUN_NOT_FOUND", "Run not found in the current company.");
}
return ok(db.readRunLogChunk(parsed.runId, parsed.offset, parsed.limit));
});
registerHandle(IPC_CHANNELS.addComment, async (_event, payload) => {
const parsed = commentInputSchema.parse(payload);
const companyError = ensureCompanyExists(parsed.companyId);
if (companyError) return companyError;
const comment = db.addComment({
companyId: parsed.companyId,
taskId: parsed.taskId,
authorAgentId: parsed.authorAgentId ?? null,
authorName: parsed.authorName,
body: parsed.body,
});
db.addActivity({
companyId: parsed.companyId,
actor: parsed.authorAgentId ?? "board",
action: "comment.created",
entityType: "task",
entityId: parsed.taskId,
detail: parsed.body.slice(0, 100),
});
emitEvent({ type: "agent-message", taskId: parsed.taskId, comment });
publishDomainChanged();
try {
const task = db.getTask(parsed.taskId);
if (task.assigneeAgentId) {
wakeAgentIfPossible(task.assigneeAgentId, task.companyId, "comment");
}
dispatchAutomation("comment_posted", {
companyId: parsed.companyId,
taskId: parsed.taskId,
agentId: parsed.authorAgentId ?? undefined,
});
} catch {
// Task may have been deleted between comment creation and wake evaluation.
}
return ok(comment.id);
});
registerHandle(IPC_CHANNELS.listComments, async (_event, payload) => {
const parsed = listCommentsSchema.parse(payload);
const companyError = ensureCompanyExists(parsed.companyId);
if (companyError) return companyError;
if (!db.belongsToCompany("tasks", parsed.taskId, parsed.companyId)) {
return fail("TASK_NOT_FOUND", "Task not found in the current company.");
}
return ok(db.listComments(parsed.taskId));
});
registerHandle(IPC_CHANNELS.setHeartbeat, async (_event, payload) => {
const parsed = heartbeatSettingsSchema.parse(payload);
const companyError = ensureCompanyExists(parsed.companyId);
if (companyError) return companyError;
if (!db.belongsToCompany("agents", parsed.agentId, parsed.companyId)) {
return fail("AGENT_NOT_FOUND", "Agent not found in the current company.");
}
db.setHeartbeat(parsed.agentId, parsed.enabled, parsed.intervalSec);
db.addActivity({
companyId: parsed.companyId,
actor: "board",
action: parsed.enabled ? "heartbeat.enabled" : "heartbeat.disabled",
entityType: "agent",
entityId: parsed.agentId,
detail: `Interval: ${parsed.intervalSec}s`,
});
emitEvent({ type: "heartbeat-status", agentId: parsed.agentId, enabled: parsed.enabled, nextWakeAt: null });
publishDomainChanged();
return ok(true);
});
registerHandle(IPC_CHANNELS.triggerHeartbeat, async (_event, payload) => {
const parsed = triggerHeartbeatSchema.parse(payload);
const companyError = ensureCompanyExists(parsed.companyId);
if (companyError) return companyError;
if (!db.belongsToCompany("agents", parsed.agentId, parsed.companyId)) {
return fail("AGENT_NOT_FOUND", "Agent not found in the current company.");
}
const runId = wakeAgentIfPossible(parsed.agentId, parsed.companyId, parsed.trigger);
if (!runId) {
return fail("HEARTBEAT_SKIPPED", "Agent cannot be woken right now (check connector status, budget, or active runs).");
}
return ok(runId);
});
}