Files
AgentCompany/src/main/prompt-builder/index.ts
T
2026-04-22 13:19:43 -04:00

104 lines
4.3 KiB
TypeScript

import type { AgentRecord, ApprovalRecord, DocumentRecord, ExecutionMode, GoalRecord, KnowledgeEntryRecord, MeetingRecord, ProjectRecord, SprintRecord, TaskRecord } from "@shared/types";
import type { AppLocale } from "@shared/locale";
import type { ProjectedSkill } from "../skill-types";
import { buildSystemPrompt } from "./system-prompt";
import { renderWakeReason } from "./wake-reasons";
import { getSystemMessages } from "./messages";
export { buildSystemPrompt } from "./system-prompt";
export interface PromptContext {
agent: AgentRecord;
apiUrl: string;
apiKey: string;
runId: string;
task: TaskRecord | null;
companyName: string;
locale?: AppLocale;
/** Company execution mode: ai_ceo (fully autonomous) or ai_cofounder (human-reviewed) */
executionMode?: ExecutionMode;
goals: GoalRecord[];
projects: ProjectRecord[];
directReports: AgentRecord[];
chainOfCommand: AgentRecord[];
assignedTasks: TaskRecord[];
allAgents: AgentRecord[];
pendingApprovals: ApprovalRecord[];
tasksAwaitingReview: TaskRecord[];
wakeReason: string | null;
allTasks: TaskRecord[];
recentComments: Array<{ taskId: string; taskTitle: string; authorName: string; body: string; createdAt: string }>;
companyDescription: string;
socialAccounts: Array<{ id: string; platform: string; accountName: string; status: string; requireApproval: boolean }>;
upcomingMeetings?: MeetingRecord[];
relevantKnowledge?: KnowledgeEntryRecord[];
activeSprint?: SprintRecord | null;
recentDocuments?: DocumentRecord[];
allConnectors?: Array<{ id: string; label: string; status: string; enabledModels?: string[] }>;
/** D-07: Handoff markdown from a rotated session, injected as context for the agent */
sessionHandoff?: string | null;
/** Skills visible to this agent via pi's progressive-disclosure model. */
skills?: ProjectedSkill[];
/** All company documents for attachment content resolution in messages */
companyDocuments?: DocumentRecord[];
/** Company-wide skill catalog for capability-aware delegation. */
companySkillCatalog?: Array<{ name: string; slug: string; description: string | null }>;
/** Company workspaces visible to the current run. */
companyWorkspaces?: Array<{ name: string; localPath: string }>;
/**
* Pending-skip context from the heartbeat scheduler. Present only
* when the scheduler most recently refused to dispatch this agent
* (e.g. worker not ready, cooldown). Surfacing it into the system
* prompt is the reader half of the scheduler-side write — without
* it, a multi-day skip streak would be invisible to the agent even
* though `runtime_state_json.heartbeatSkip` tracks it precisely.
*/
heartbeatSkip?: {
reason: string;
streak: number;
since: string;
lastTickAt: string;
} | null;
}
export function buildTaskPrompt(ctx: PromptContext): string {
const m = getSystemMessages();
const systemPrompt = buildSystemPrompt(ctx);
const parts: string[] = [systemPrompt];
// Volatile per-turn context lives AFTER the cacheable system prefix so the
// Anthropic prompt cache hits across heartbeats. Putting the wall-clock
// time inside the system prompt would invalidate the cache on every call.
const now = new Date();
const dateStr = now.toISOString().split("T")[0];
const timeStr = now.toISOString().split("T")[1]?.slice(0, 5) ?? "00:00";
const dayOfWeek = m.dayNames[now.getUTCDay()] ?? "";
parts.push(`\n${m.currentDateSection(dayOfWeek, dateStr, timeStr)}`);
if (ctx.wakeReason) {
const explanation = renderWakeReason(ctx.wakeReason, {
companyId: ctx.agent.companyId,
agentId: ctx.agent.id,
});
parts.push(`\n${m.wakeReasonHeading}\n${explanation}\n`);
}
if (ctx.heartbeatSkip) {
parts.push(`\n${m.heartbeatSkipSection(ctx.heartbeatSkip)}`);
}
parts.push(`\n${m.timeAwarenessSection}`);
// Taskless runs used to emit a "respond with a brief status summary"
// nudge here. That text fought the wake-reason block above and caused
// communication wakes to degrade into self-narration. The wake-reason
// section already tells the agent what to do; the initial user turn
// (assembled at dispatch) supplies the concrete action cue — no fallback
// text is needed for the system prompt anymore.
if (ctx.task) {
parts.push(m.immediateTaskSection(ctx.task.title, ctx.task.description, ctx.task.priority));
}
return parts.join("");
}