Files
AgentCompany/src/main/checkpoint-engine.ts
T
2026-04-04 12:03:50 -04:00

165 lines
6.5 KiB
TypeScript

/**
* Checkpoint Engine
*
* Creates approval checkpoints based on company executionMode.
* In ai_cofounder mode, key orchestration junctures pause for user confirmation.
* In ai_ceo mode, only high-risk actions require approval.
*
* Policy decisions (which types require review, which can be auto-approved)
* are centralized in execution-policy.ts — the single source of truth.
*/
import type { AppDatabase } from "./database";
import type { ApprovalType, ExecutionMode, SectionId } from "@shared/types";
import type { ThreadIntegrationService } from "./thread-integration";
import {
CHECKPOINT_TASK_TYPES,
STRATEGY_DOCUMENT_TYPES,
} from "./execution-policy";
interface CheckpointDeps {
db: AppDatabase;
logger: { info: (...args: unknown[]) => void; error: (...args: unknown[]) => void };
threadIntegration: ThreadIntegrationService;
publishDomainChanged: () => void;
notify: (options: { title: string; body: string; urgency: "critical" | "informational"; navigation?: { section: SectionId; entityId?: string } }) => void;
}
export function getCompanyExecutionMode(db: AppDatabase, companyId: string): ExecutionMode {
return db.getCompanyExecutionMode(companyId) as ExecutionMode;
}
export function createCheckpointEngine(deps: CheckpointDeps) {
const { db, logger, threadIntegration, publishDomainChanged, notify } = deps;
return {
/**
* Called when a task transitions to in_progress for the first time.
* In co-founder mode, creates a plan_review checkpoint for non-code tasks
* so the user can review the task plan before execution begins.
*/
onTaskStarted(taskId: string, companyId: string, agentId: string | null): string | null {
try {
const mode = getCompanyExecutionMode(db, companyId);
if (mode !== "ai_cofounder") return null;
const task = db.getTask(taskId);
const taskType = db.queryAll<{ taskType: string }>(
"SELECT task_type as taskType FROM tasks WHERE id = ? LIMIT 1",
taskId,
)[0]?.taskType;
// Only create plan checkpoints for non-code task types
if (!taskType || !CHECKPOINT_TASK_TYPES.has(taskType)) return null;
const approvalId = db.requestApproval({
companyId,
relatedTaskId: taskId,
requestedByAgentId: agentId,
type: "plan_review",
payloadSummary: `Review plan for: ${task.title}`.slice(0, 280),
impactSummary: `Task "${task.title}" is starting. Review the scope and approach before execution proceeds.`.slice(0, 400),
payloadJson: JSON.stringify({ taskId, taskTitle: task.title, taskType }),
});
threadIntegration.onApprovalCreated(approvalId, companyId, taskId, "plan_review", `Plan review: ${task.title}`);
notify({
title: "Plan Review Needed",
body: `"${task.title}" needs your review before work begins`,
urgency: "informational",
navigation: { section: "approvals", entityId: approvalId },
});
publishDomainChanged();
return approvalId;
} catch (err) {
logger.error(`[checkpoint] onTaskStarted failed: ${err instanceof Error ? err.message : String(err)}`);
return null;
}
},
/**
* Called when a document is created in co-founder mode.
* Creates a draft_review checkpoint so the user can review before
* the document is considered final.
*/
onDocumentDrafted(companyId: string, documentId: string, documentTitle: string, documentType: string, authorAgentId: string | null, taskId: string | null): string | null {
try {
const mode = getCompanyExecutionMode(db, companyId);
if (mode !== "ai_cofounder") return null;
// Strategy-related document types get strategy_review instead
const approvalType: ApprovalType = STRATEGY_DOCUMENT_TYPES.has(documentType)
? "strategy_review"
: "draft_review";
const approvalId = db.requestApproval({
companyId,
relatedTaskId: taskId,
requestedByAgentId: authorAgentId,
type: approvalType,
payloadSummary: `Review ${documentType.replaceAll("_", " ")}: ${documentTitle}`.slice(0, 280),
impactSummary: `New ${documentType.replaceAll("_", " ")} drafted. Please review before it becomes final.`.slice(0, 400),
payloadJson: JSON.stringify({ documentId, documentTitle, documentType }),
});
threadIntegration.onApprovalCreated(approvalId, companyId, taskId, approvalType, `${approvalType}: ${documentTitle}`);
notify({
title: "Draft Review Needed",
body: `"${documentTitle}" needs your review`,
urgency: "informational",
navigation: { section: "approvals", entityId: approvalId },
});
publishDomainChanged();
return approvalId;
} catch (err) {
logger.error(`[checkpoint] onDocumentDrafted failed: ${err instanceof Error ? err.message : String(err)}`);
return null;
}
},
/**
* Checks if a company in ai_cofounder mode has unresolved checkpoints
* that should pause heartbeat for an agent.
*/
shouldPauseHeartbeat(companyId: string, agentId: string): boolean {
try {
const mode = getCompanyExecutionMode(db, companyId);
if (mode !== "ai_cofounder") return false;
// Check for unresolved wait states for THIS agent's threads (not company-wide)
if (threadIntegration.hasUnresolvedWaitStatesForAgent(companyId, agentId)) {
return true;
}
// Check for pending approvals related to this agent's tasks
const pendingApprovals = db.queryAll<{ id: string }>(
`SELECT a.id FROM approvals a
JOIN tasks t ON a.related_task_id = t.id
WHERE a.company_id = ? AND a.state = 'pending'
AND t.assignee_agent_id = ?
AND a.type IN ('plan_review','scope_review','draft_review','strategy_review','deliverable_review','exception_review')
LIMIT 1`,
companyId, agentId,
);
return pendingApprovals.length > 0;
} catch (err) {
logger.error(`[checkpoint] shouldPauseHeartbeat failed: ${err instanceof Error ? err.message : String(err)}`);
return false;
}
},
/**
* Returns execution mode for a company. Convenience accessor.
*/
getExecutionMode(companyId: string): ExecutionMode {
return getCompanyExecutionMode(db, companyId);
},
};
}
export type CheckpointEngineService = ReturnType<typeof createCheckpointEngine>;