update
This commit is contained in:
1 parent
41eafc7c44
commit
54bcc77ef7
19 files changed
+11420
-64
No files matched your search
@@ -49,6 +49,7 @@ export class AgentApiServer {
|
||||
private db: AppDatabase;
|
||||
private onDomainChanged: () => void;
|
||||
private onWakeAgent: (agentId: string, companyId: string, trigger: string) => void;
|
||||
private onBrowserAction: ((actionId: string, socialAccountId: string, actionType: string, payload: Record<string, unknown>) => void) | null = null;
|
||||
|
||||
constructor(db: AppDatabase, onDomainChanged: () => void, onWakeAgent: (agentId: string, companyId: string, trigger: string) => void) {
|
||||
this.db = db;
|
||||
@@ -57,6 +58,10 @@ export class AgentApiServer {
|
||||
this.registerRoutes();
|
||||
}
|
||||
|
||||
setBrowserActionHandler(handler: (actionId: string, socialAccountId: string, actionType: string, payload: Record<string, unknown>) => void) {
|
||||
this.onBrowserAction = handler;
|
||||
}
|
||||
|
||||
getPort(): number {
|
||||
return this.port;
|
||||
}
|
||||
@@ -960,7 +965,430 @@ export class AgentApiServer {
|
||||
tasksDone: rTasks.filter((t) => t.status === "done").length,
|
||||
};
|
||||
}),
|
||||
socialAccounts: snapshot.socialAccounts
|
||||
.filter((a) => a.companyId === token.companyId)
|
||||
.map((a) => ({
|
||||
id: a.id,
|
||||
platform: a.platform,
|
||||
accountName: a.accountName,
|
||||
displayName: a.displayName,
|
||||
status: a.status,
|
||||
requireApproval: a.requireApproval,
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
// ── Browser Automation API (for agents) ────────────────────────────
|
||||
|
||||
// POST /api/companies/:companyId/browser/action — Submit a browser action
|
||||
this.addRoute("POST", "/api/companies/:companyId/browser/action", async (_req, res, params, token, body) => {
|
||||
const companyId = params.companyId!;
|
||||
if (companyId !== token.companyId) {
|
||||
this.json(res, 403, { error: "Company mismatch" });
|
||||
return;
|
||||
}
|
||||
|
||||
const actionSchema = z.object({
|
||||
socialAccountId: z.string().uuid(),
|
||||
actionType: z.enum(["post", "reply", "like", "follow", "browse_feed", "screenshot", "navigate", "search", "dm", "custom_script"]),
|
||||
payload: z.record(z.string(), z.unknown()).default({}),
|
||||
});
|
||||
|
||||
const parsed = actionSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
this.json(res, 400, { error: "Invalid browser action payload", details: parsed.error.issues });
|
||||
return;
|
||||
}
|
||||
|
||||
const input = parsed.data;
|
||||
|
||||
// Verify the social account belongs to this company
|
||||
const snapshot = this.db.listSnapshot();
|
||||
const account = snapshot.socialAccounts.find(a => a.id === input.socialAccountId && a.companyId === companyId);
|
||||
if (!account) {
|
||||
this.json(res, 404, { error: "Social account not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if approval is required for posting actions
|
||||
const postActions = ["post", "reply", "dm"];
|
||||
if (account.requireApproval && postActions.includes(input.actionType)) {
|
||||
// Create an approval request
|
||||
const approvalId = this.db.requestApproval({
|
||||
companyId,
|
||||
type: "dangerous_command",
|
||||
relatedTaskId: null,
|
||||
requestedByAgentId: token.agentId,
|
||||
payloadSummary: `${input.actionType} on ${account.platform}/@${account.accountName}: ${String(input.payload.content || "").slice(0, 100)}`,
|
||||
impactSummary: `Will ${input.actionType} on ${account.displayName || account.accountName}'s ${account.platform} account. Content will be publicly visible.`,
|
||||
});
|
||||
|
||||
// Save the action in approval_required state
|
||||
const actionId = this.db.saveBrowserAction({
|
||||
companyId,
|
||||
socialAccountId: input.socialAccountId,
|
||||
agentId: token.agentId,
|
||||
actionType: input.actionType,
|
||||
payloadJson: JSON.stringify(input.payload),
|
||||
status: "approval_required",
|
||||
approvalId,
|
||||
});
|
||||
|
||||
this.db.addActivity({
|
||||
companyId,
|
||||
actor: token.agentId,
|
||||
action: "browser.action_pending_approval",
|
||||
entityType: "browser_action",
|
||||
entityId: actionId,
|
||||
detail: `${input.actionType} on ${account.platform} requires approval`,
|
||||
});
|
||||
|
||||
this.onDomainChanged();
|
||||
this.json(res, 202, { id: actionId, status: "approval_required", approvalId, message: "Action requires Board approval before execution" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the action
|
||||
const actionId = this.db.saveBrowserAction({
|
||||
companyId,
|
||||
socialAccountId: input.socialAccountId,
|
||||
agentId: token.agentId,
|
||||
actionType: input.actionType,
|
||||
payloadJson: JSON.stringify(input.payload),
|
||||
status: "queued",
|
||||
});
|
||||
|
||||
this.db.addActivity({
|
||||
companyId,
|
||||
actor: token.agentId,
|
||||
action: "browser.action_queued",
|
||||
entityType: "browser_action",
|
||||
entityId: actionId,
|
||||
detail: `${input.actionType} on ${account.platform}/@${account.accountName}`,
|
||||
});
|
||||
|
||||
this.onDomainChanged();
|
||||
|
||||
// Execute the action asynchronously via the browser manager callback
|
||||
if (this.onBrowserAction) {
|
||||
this.onBrowserAction(actionId, input.socialAccountId, input.actionType, input.payload as Record<string, unknown>);
|
||||
}
|
||||
|
||||
this.json(res, 201, { id: actionId, status: "queued", message: "Browser action queued for execution" });
|
||||
});
|
||||
|
||||
// GET /api/companies/:companyId/social-accounts — List social accounts
|
||||
this.addRoute("GET", "/api/companies/:companyId/social-accounts", (_req, res, params, token) => {
|
||||
if (params.companyId !== token.companyId) {
|
||||
this.json(res, 403, { error: "Company mismatch" });
|
||||
return;
|
||||
}
|
||||
const accounts = this.db.listSocialAccounts(params.companyId!);
|
||||
// Don't expose credential secret IDs to agents
|
||||
const sanitized = accounts.map(a => ({
|
||||
id: a.id,
|
||||
platform: a.platform,
|
||||
accountName: a.accountName,
|
||||
displayName: a.displayName,
|
||||
profileUrl: a.profileUrl,
|
||||
status: a.status,
|
||||
requireApproval: a.requireApproval,
|
||||
}));
|
||||
this.json(res, 200, sanitized);
|
||||
});
|
||||
|
||||
// GET /api/companies/:companyId/browser/actions — List browser action history
|
||||
this.addRoute("GET", "/api/companies/:companyId/browser/actions", (req, res, params, token) => {
|
||||
if (params.companyId !== token.companyId) {
|
||||
this.json(res, 403, { error: "Company mismatch" });
|
||||
return;
|
||||
}
|
||||
const url = new URL(req.url ?? "/", `http://127.0.0.1:${this.port}`);
|
||||
const socialAccountId = url.searchParams.get("socialAccountId") ?? undefined;
|
||||
const status = url.searchParams.get("status") ?? undefined;
|
||||
const limit = Number(url.searchParams.get("limit") || 50);
|
||||
|
||||
const actions = this.db.listBrowserActions(params.companyId!, { socialAccountId, status, limit });
|
||||
this.json(res, 200, actions);
|
||||
});
|
||||
|
||||
// ── Documents API ─────────────────────────────────────────────────
|
||||
|
||||
// POST /api/companies/:companyId/documents — Create a document
|
||||
this.addRoute("POST", "/api/companies/:companyId/documents", (_req, res, params, token, body) => {
|
||||
if (params.companyId !== token.companyId) {
|
||||
this.json(res, 403, { error: "Company mismatch" });
|
||||
return;
|
||||
}
|
||||
const data = body as Record<string, unknown>;
|
||||
const id = this.db.saveDocument({
|
||||
companyId: token.companyId,
|
||||
type: String(data.type ?? "knowledge_article"),
|
||||
title: String(data.title ?? "Untitled"),
|
||||
content: String(data.content ?? ""),
|
||||
authorAgentId: token.agentId,
|
||||
reviewerAgentId: data.reviewerAgentId ? String(data.reviewerAgentId) : null,
|
||||
projectId: data.projectId ? String(data.projectId) : null,
|
||||
goalId: data.goalId ? String(data.goalId) : null,
|
||||
status: String(data.status ?? "draft"),
|
||||
tagsJson: data.tagsJson ? String(data.tagsJson) : "[]",
|
||||
});
|
||||
this.onDomainChanged();
|
||||
this.json(res, 201, { id });
|
||||
});
|
||||
|
||||
// GET /api/companies/:companyId/documents — List documents
|
||||
this.addRoute("GET", "/api/companies/:companyId/documents", (req, res, params, token) => {
|
||||
if (params.companyId !== token.companyId) {
|
||||
this.json(res, 403, { error: "Company mismatch" });
|
||||
return;
|
||||
}
|
||||
const snapshot = this.db.listSnapshot();
|
||||
const docs = snapshot.documents.filter(d => d.companyId === token.companyId);
|
||||
this.json(res, 200, docs);
|
||||
});
|
||||
|
||||
// ── Meetings API ──────────────────────────────────────────────────
|
||||
|
||||
// POST /api/companies/:companyId/meetings — Schedule a meeting
|
||||
this.addRoute("POST", "/api/companies/:companyId/meetings", (_req, res, params, token, body) => {
|
||||
if (params.companyId !== token.companyId) {
|
||||
this.json(res, 403, { error: "Company mismatch" });
|
||||
return;
|
||||
}
|
||||
const data = body as Record<string, unknown>;
|
||||
const id = this.db.saveMeeting({
|
||||
companyId: token.companyId,
|
||||
type: String(data.type ?? "department_sync"),
|
||||
title: String(data.title ?? "Meeting"),
|
||||
organizerAgentId: token.agentId,
|
||||
participantAgentIds: data.participantAgentIds ? String(data.participantAgentIds) : "[]",
|
||||
scheduledAt: String(data.scheduledAt ?? new Date().toISOString()),
|
||||
durationMinutes: Number(data.durationMinutes ?? 30),
|
||||
agendaJson: data.agendaJson ? String(data.agendaJson) : "[]",
|
||||
status: "scheduled",
|
||||
});
|
||||
this.onDomainChanged();
|
||||
this.json(res, 201, { id });
|
||||
});
|
||||
|
||||
// GET /api/companies/:companyId/meetings — List meetings
|
||||
this.addRoute("GET", "/api/companies/:companyId/meetings", (_req, res, params, token) => {
|
||||
if (params.companyId !== token.companyId) {
|
||||
this.json(res, 403, { error: "Company mismatch" });
|
||||
return;
|
||||
}
|
||||
const snapshot = this.db.listSnapshot();
|
||||
const meetings = snapshot.meetings.filter(m => m.companyId === token.companyId);
|
||||
this.json(res, 200, meetings);
|
||||
});
|
||||
|
||||
// PATCH /api/meetings/:meetingId — Update meeting (add notes, decisions, etc.)
|
||||
this.addRoute("PATCH", "/api/meetings/:meetingId", (_req, res, params, token, body) => {
|
||||
const data = body as Record<string, unknown>;
|
||||
this.db.saveMeeting({
|
||||
id: params.meetingId,
|
||||
companyId: token.companyId,
|
||||
type: data.type ? String(data.type) : "department_sync",
|
||||
title: data.title ? String(data.title) : "Meeting",
|
||||
notesJson: data.notesJson ? String(data.notesJson) : undefined,
|
||||
decisionsJson: data.decisionsJson ? String(data.decisionsJson) : undefined,
|
||||
actionItemsJson: data.actionItemsJson ? String(data.actionItemsJson) : undefined,
|
||||
status: data.status ? String(data.status) : undefined,
|
||||
scheduledAt: data.scheduledAt ? String(data.scheduledAt) : new Date().toISOString(),
|
||||
});
|
||||
this.onDomainChanged();
|
||||
this.json(res, 200, { updated: true });
|
||||
});
|
||||
|
||||
// ── Knowledge Base API ────────────────────────────────────────────
|
||||
|
||||
// POST /api/companies/:companyId/knowledge — Add knowledge entry
|
||||
this.addRoute("POST", "/api/companies/:companyId/knowledge", (_req, res, params, token, body) => {
|
||||
if (params.companyId !== token.companyId) {
|
||||
this.json(res, 403, { error: "Company mismatch" });
|
||||
return;
|
||||
}
|
||||
const data = body as Record<string, unknown>;
|
||||
const id = this.db.saveKnowledgeEntry({
|
||||
companyId: token.companyId,
|
||||
category: String(data.category ?? "lesson_learned"),
|
||||
topic: String(data.topic ?? ""),
|
||||
content: String(data.content ?? ""),
|
||||
authorAgentId: token.agentId,
|
||||
importance: String(data.importance ?? "medium"),
|
||||
tagsJson: data.tagsJson ? String(data.tagsJson) : "[]",
|
||||
referencedEntityType: data.referencedEntityType ? String(data.referencedEntityType) : null,
|
||||
referencedEntityId: data.referencedEntityId ? String(data.referencedEntityId) : null,
|
||||
});
|
||||
this.onDomainChanged();
|
||||
this.json(res, 201, { id });
|
||||
});
|
||||
|
||||
// GET /api/companies/:companyId/knowledge — List knowledge entries
|
||||
this.addRoute("GET", "/api/companies/:companyId/knowledge", (_req, res, params, token) => {
|
||||
if (params.companyId !== token.companyId) {
|
||||
this.json(res, 403, { error: "Company mismatch" });
|
||||
return;
|
||||
}
|
||||
const snapshot = this.db.listSnapshot();
|
||||
const entries = snapshot.knowledgeBase.filter(k => k.companyId === token.companyId);
|
||||
this.json(res, 200, entries);
|
||||
});
|
||||
|
||||
// ── Agent Messaging API ──────────────────────────────────────────────
|
||||
|
||||
// POST /api/companies/:companyId/messages — Send a message to another agent
|
||||
this.addRoute("POST", "/api/companies/:companyId/messages", (_req, res, params, token, body) => {
|
||||
const companyId = params.companyId!;
|
||||
if (companyId !== token.companyId) {
|
||||
this.json(res, 403, { error: "Company mismatch" });
|
||||
return;
|
||||
}
|
||||
|
||||
const messageSchema = z.object({
|
||||
toAgentId: z.string().uuid().nullable().optional(),
|
||||
channel: z.enum(["direct", "department", "company", "project", "incident"]).default("direct"),
|
||||
channelTargetId: z.string().nullable().optional(),
|
||||
subject: z.string().min(1).max(200),
|
||||
body: z.string().min(1).max(8000),
|
||||
priority: z.enum(["urgent", "normal", "low"]).default("normal"),
|
||||
parentMessageId: z.string().uuid().nullable().optional(),
|
||||
attachmentsJson: z.string().default("[]"),
|
||||
});
|
||||
|
||||
const parsed = messageSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
this.json(res, 400, { error: "Invalid input", details: parsed.error.flatten() });
|
||||
return;
|
||||
}
|
||||
|
||||
const msg = parsed.data;
|
||||
const messageId = this.db.sendAgentMessage({
|
||||
companyId,
|
||||
fromAgentId: token.agentId,
|
||||
toAgentId: msg.toAgentId,
|
||||
channel: msg.channel,
|
||||
channelTargetId: msg.channelTargetId,
|
||||
subject: msg.subject,
|
||||
body: msg.body,
|
||||
priority: msg.priority,
|
||||
parentMessageId: msg.parentMessageId,
|
||||
attachmentsJson: msg.attachmentsJson,
|
||||
});
|
||||
|
||||
// Wake the recipient if it's a direct message
|
||||
if (msg.toAgentId && msg.channel === "direct") {
|
||||
this.onWakeAgent(msg.toAgentId, companyId, "message");
|
||||
}
|
||||
|
||||
// Wake all agents in the department if it's a department broadcast
|
||||
if (msg.channel === "department" && msg.channelTargetId) {
|
||||
const snapshot = this.db.listSnapshot();
|
||||
const deptAgents = snapshot.agents.filter(
|
||||
(a) => a.department === msg.channelTargetId && a.companyId === companyId && a.id !== token.agentId && a.status !== "terminated",
|
||||
);
|
||||
for (const a of deptAgents) {
|
||||
this.onWakeAgent(a.id, companyId, "message");
|
||||
}
|
||||
}
|
||||
|
||||
this.onDomainChanged();
|
||||
this.json(res, 201, { id: messageId });
|
||||
});
|
||||
|
||||
// GET /api/companies/:companyId/messages — List messages for the calling agent
|
||||
this.addRoute("GET", "/api/companies/:companyId/messages", (req, res, params, token) => {
|
||||
const companyId = params.companyId!;
|
||||
if (companyId !== token.companyId) {
|
||||
this.json(res, 403, { error: "Company mismatch" });
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL(req.url ?? "/", `http://127.0.0.1:${this.port}`);
|
||||
const channel = url.searchParams.get("channel") ?? undefined;
|
||||
const unreadOnly = url.searchParams.get("unreadOnly") === "true";
|
||||
const limitStr = url.searchParams.get("limit");
|
||||
const limit = limitStr ? Math.min(Math.max(parseInt(limitStr, 10) || 50, 1), 200) : 50;
|
||||
|
||||
const messages = this.db.listAgentMessages(companyId, {
|
||||
agentId: token.agentId,
|
||||
channel,
|
||||
unreadOnly,
|
||||
limit,
|
||||
});
|
||||
this.json(res, 200, messages);
|
||||
});
|
||||
|
||||
// POST /api/companies/:companyId/messages/:messageId/read — Mark message as read
|
||||
this.addRoute("POST", "/api/companies/:companyId/messages/:messageId/read", (_req, res, params, token) => {
|
||||
const companyId = params.companyId!;
|
||||
if (companyId !== token.companyId) {
|
||||
this.json(res, 403, { error: "Company mismatch" });
|
||||
return;
|
||||
}
|
||||
this.db.markMessageRead(params.messageId!, companyId);
|
||||
this.json(res, 200, { ok: true });
|
||||
});
|
||||
|
||||
// ── Sprint API ───────────────────────────────────────────────────────
|
||||
|
||||
// GET /api/companies/:companyId/sprints — List sprints
|
||||
this.addRoute("GET", "/api/companies/:companyId/sprints", (_req, res, params, token) => {
|
||||
const companyId = params.companyId!;
|
||||
if (companyId !== token.companyId) {
|
||||
this.json(res, 403, { error: "Company mismatch" });
|
||||
return;
|
||||
}
|
||||
const snapshot = this.db.listSnapshot();
|
||||
const sprints = snapshot.sprints.filter(s => s.companyId === companyId);
|
||||
this.json(res, 200, sprints);
|
||||
});
|
||||
|
||||
// ── Performance Metrics API ──────────────────────────────────────────
|
||||
|
||||
// GET /api/agents/me/metrics — Get the calling agent's performance metrics
|
||||
this.addRoute("GET", "/api/agents/me/metrics", (_req, res, _params, token) => {
|
||||
const companyMetrics = this.db.getCompanyMetrics(token.companyId, 30);
|
||||
const agentMetric = companyMetrics.agentMetrics.find(m => m.agentId === token.agentId);
|
||||
if (!agentMetric) {
|
||||
this.json(res, 404, { error: "Metrics not found" });
|
||||
return;
|
||||
}
|
||||
this.json(res, 200, agentMetric);
|
||||
});
|
||||
|
||||
// ── Org Chart / Agent Details API ────────────────────────────────────
|
||||
|
||||
// GET /api/agents/:agentId — Get a specific agent's details
|
||||
this.addRoute("GET", "/api/agents/:agentId", (_req, res, params, token) => {
|
||||
const snapshot = this.db.listSnapshot();
|
||||
const agent = snapshot.agents.find(a => a.id === params.agentId && a.companyId === token.companyId);
|
||||
if (!agent) {
|
||||
this.json(res, 404, { error: "Agent not found" });
|
||||
return;
|
||||
}
|
||||
const directReports = snapshot.agents.filter(a => a.reportsTo === agent.id && a.companyId === token.companyId);
|
||||
const manager = agent.reportsTo ? snapshot.agents.find(a => a.id === agent.reportsTo) : null;
|
||||
this.json(res, 200, {
|
||||
...agent,
|
||||
manager: manager ? { id: manager.id, name: manager.name, role: manager.role } : null,
|
||||
directReports: directReports.map(r => ({ id: r.id, name: r.name, role: r.role, status: r.status })),
|
||||
});
|
||||
});
|
||||
|
||||
// ── Wake / Heartbeat API ─────────────────────────────────────────────
|
||||
|
||||
// POST /api/agents/:agentId/wake — Request to wake another agent
|
||||
this.addRoute("POST", "/api/agents/:agentId/wake", (_req, res, params, token, body) => {
|
||||
const targetAgentId = params.agentId!;
|
||||
const wakeSchema = z.object({
|
||||
reason: z.string().min(1).max(200).default("peer_request"),
|
||||
});
|
||||
const parsed = wakeSchema.safeParse(body);
|
||||
const reason = parsed.success ? parsed.data.reason : "peer_request";
|
||||
this.onWakeAgent(targetAgentId, token.companyId, reason);
|
||||
this.json(res, 200, { ok: true, message: `Wake requested for agent ${targetAgentId}` });
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
import { BrowserWindow, session, type NativeImage } from "electron";
|
||||
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import log from "electron-log/main.js";
|
||||
|
||||
/**
|
||||
* Browser Automation Manager — Electron-native edition.
|
||||
*
|
||||
* Uses Electron's own BrowserWindow and persistent sessions instead of
|
||||
* external Playwright, so it works in packaged apps with zero extra deps.
|
||||
*
|
||||
* Each social account gets its own Electron session partition so cookies,
|
||||
* localStorage, and login state are isolated and persisted across app restarts.
|
||||
*/
|
||||
|
||||
export interface BrowserAction {
|
||||
id: string;
|
||||
socialAccountId: string;
|
||||
actionType: string;
|
||||
payload: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface BrowserActionResult {
|
||||
success: boolean;
|
||||
summary: string;
|
||||
screenshotPath: string | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface QueuedAction {
|
||||
action: BrowserAction;
|
||||
resolve: (result: BrowserActionResult) => void;
|
||||
reject: (error: Error) => void;
|
||||
}
|
||||
|
||||
export class BrowserManager {
|
||||
private screenshotDir: string;
|
||||
private queues = new Map<string, QueuedAction[]>();
|
||||
private processing = new Set<string>();
|
||||
/** Track open login windows so we don't open duplicates */
|
||||
private loginWindows = new Map<string, BrowserWindow>();
|
||||
|
||||
constructor(profileDir: string) {
|
||||
this.screenshotDir = join(profileDir, "browser-screenshots");
|
||||
if (!existsSync(this.screenshotDir)) mkdirSync(this.screenshotDir, { recursive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a persistent Electron session for a social account.
|
||||
* Using `persist:` prefix ensures data survives app restarts.
|
||||
*/
|
||||
private getSession(socialAccountId: string): Electron.Session {
|
||||
return session.fromPartition(`persist:social-${socialAccountId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch a real browser window for manual login.
|
||||
* The window uses a persistent session so cookies are saved.
|
||||
* Returns a promise that resolves when the user closes the window.
|
||||
*/
|
||||
async launchLoginBrowser(
|
||||
socialAccountId: string,
|
||||
platform: string,
|
||||
loginUrl?: string,
|
||||
): Promise<BrowserActionResult> {
|
||||
// Don't open duplicate login windows for the same account
|
||||
const existing = this.loginWindows.get(socialAccountId);
|
||||
if (existing && !existing.isDestroyed()) {
|
||||
existing.focus();
|
||||
return { success: true, summary: "Login window already open", screenshotPath: null, error: null };
|
||||
}
|
||||
|
||||
const defaultUrls: Record<string, string> = {
|
||||
twitter: "https://x.com/i/flow/login",
|
||||
linkedin: "https://www.linkedin.com/login",
|
||||
instagram: "https://www.instagram.com/accounts/login/",
|
||||
facebook: "https://www.facebook.com/login",
|
||||
reddit: "https://www.reddit.com/login",
|
||||
youtube: "https://accounts.google.com/signin",
|
||||
tiktok: "https://www.tiktok.com/login",
|
||||
bluesky: "https://bsky.app",
|
||||
other: "https://google.com",
|
||||
};
|
||||
|
||||
const url = loginUrl || defaultUrls[platform] || defaultUrls.other;
|
||||
const ses = this.getSession(socialAccountId);
|
||||
|
||||
log.info(`[browser] Opening login window for ${platform} → ${url}`);
|
||||
|
||||
const win = new BrowserWindow({
|
||||
width: 1280,
|
||||
height: 860,
|
||||
title: `Login to ${platform} — AgentCompany`,
|
||||
webPreferences: {
|
||||
session: ses,
|
||||
// Standard web page — no node integration, no preload
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
sandbox: true,
|
||||
},
|
||||
});
|
||||
|
||||
this.loginWindows.set(socialAccountId, win);
|
||||
|
||||
// Remove menu bar for a cleaner look
|
||||
win.setMenuBarVisibility(false);
|
||||
|
||||
// Load the login page
|
||||
try {
|
||||
await win.loadURL(url);
|
||||
} catch (err) {
|
||||
log.error(`[browser] Failed to load ${url}: ${err}`);
|
||||
// Don't fail — the page might redirect or have an expected error
|
||||
}
|
||||
|
||||
// Wait for the user to close the window
|
||||
return new Promise<BrowserActionResult>((resolve) => {
|
||||
win.on("closed", () => {
|
||||
this.loginWindows.delete(socialAccountId);
|
||||
log.info(`[browser] Login window closed for ${platform} — session saved`);
|
||||
resolve({
|
||||
success: true,
|
||||
summary: `Login session saved for ${platform}`,
|
||||
screenshotPath: null,
|
||||
error: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if browser automation is available.
|
||||
* Always true — we use Electron's own Chromium.
|
||||
*/
|
||||
async checkPlaywright(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue a browser action for execution.
|
||||
*/
|
||||
async executeAction(action: BrowserAction): Promise<BrowserActionResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const queue = this.queues.get(action.socialAccountId) ?? [];
|
||||
queue.push({ action, resolve, reject });
|
||||
this.queues.set(action.socialAccountId, queue);
|
||||
void this.processQueue(action.socialAccountId);
|
||||
});
|
||||
}
|
||||
|
||||
private async processQueue(socialAccountId: string) {
|
||||
if (this.processing.has(socialAccountId)) return;
|
||||
const queue = this.queues.get(socialAccountId);
|
||||
if (!queue || queue.length === 0) return;
|
||||
|
||||
this.processing.add(socialAccountId);
|
||||
while (queue.length > 0) {
|
||||
const item = queue.shift()!;
|
||||
try {
|
||||
const result = await this.runAction(item.action);
|
||||
item.resolve(result);
|
||||
} catch (error) {
|
||||
item.reject(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
}
|
||||
this.processing.delete(socialAccountId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a single browser action using an Electron BrowserWindow.
|
||||
*/
|
||||
private async runAction(action: BrowserAction): Promise<BrowserActionResult> {
|
||||
const ses = this.getSession(action.socialAccountId);
|
||||
const payload = action.payload;
|
||||
const screenshotPath = join(this.screenshotDir, `${action.id}.png`);
|
||||
|
||||
log.info(`[browser] Executing ${action.actionType} for account ${action.socialAccountId}`);
|
||||
|
||||
// Create a visible browser window for the action
|
||||
const win = new BrowserWindow({
|
||||
width: 1280,
|
||||
height: 860,
|
||||
title: `AgentCompany — ${action.actionType}`,
|
||||
show: true,
|
||||
webPreferences: {
|
||||
session: ses,
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
sandbox: true,
|
||||
},
|
||||
});
|
||||
win.setMenuBarVisibility(false);
|
||||
|
||||
try {
|
||||
let summary = "Action completed";
|
||||
|
||||
switch (action.actionType) {
|
||||
case "navigate": {
|
||||
const url = String(payload.url || "https://google.com");
|
||||
await win.loadURL(url);
|
||||
await this.waitForLoad(win);
|
||||
const title = await win.webContents.executeJavaScript("document.title");
|
||||
summary = `Navigated to ${title}`;
|
||||
break;
|
||||
}
|
||||
|
||||
case "post": {
|
||||
const platform = String(payload.platform || "twitter");
|
||||
const content = String(payload.content || "");
|
||||
const postUrl = this.getComposeUrl(platform);
|
||||
await win.loadURL(postUrl);
|
||||
await this.waitForLoad(win);
|
||||
await this.delay(2000);
|
||||
|
||||
if (platform === "twitter") {
|
||||
// Type into tweet composer
|
||||
await win.webContents.executeJavaScript(`
|
||||
(async () => {
|
||||
const editor = document.querySelector('[data-testid="tweetTextarea_0"], [role="textbox"]');
|
||||
if (editor) {
|
||||
editor.focus();
|
||||
document.execCommand('insertText', false, ${JSON.stringify(content)});
|
||||
}
|
||||
})()
|
||||
`);
|
||||
await this.delay(1000);
|
||||
// Click post button
|
||||
await win.webContents.executeJavaScript(`
|
||||
(() => {
|
||||
const btn = document.querySelector('[data-testid="tweetButton"], [data-testid="tweetButtonInline"]');
|
||||
if (btn) btn.click();
|
||||
})()
|
||||
`);
|
||||
await this.delay(3000);
|
||||
} else if (platform === "linkedin") {
|
||||
await win.webContents.executeJavaScript(`
|
||||
(() => {
|
||||
const btn = document.querySelector('button.share-box-feed-entry__trigger');
|
||||
if (btn) btn.click();
|
||||
})()
|
||||
`);
|
||||
await this.delay(2000);
|
||||
await win.webContents.executeJavaScript(`
|
||||
(async () => {
|
||||
const editor = document.querySelector('[role="textbox"][contenteditable="true"]');
|
||||
if (editor) {
|
||||
editor.focus();
|
||||
document.execCommand('insertText', false, ${JSON.stringify(content)});
|
||||
}
|
||||
})()
|
||||
`);
|
||||
await this.delay(1000);
|
||||
await win.webContents.executeJavaScript(`
|
||||
(() => {
|
||||
const btn = document.querySelector('button.share-actions__primary-action');
|
||||
if (btn) btn.click();
|
||||
})()
|
||||
`);
|
||||
await this.delay(3000);
|
||||
}
|
||||
summary = `Posted to ${platform}: "${content.slice(0, 50)}..."`;
|
||||
break;
|
||||
}
|
||||
|
||||
case "reply": {
|
||||
const targetUrl = String(payload.targetUrl || "");
|
||||
const replyContent = String(payload.content || "");
|
||||
await win.loadURL(targetUrl);
|
||||
await this.waitForLoad(win);
|
||||
await this.delay(2000);
|
||||
|
||||
await win.webContents.executeJavaScript(`
|
||||
(async () => {
|
||||
const editor = document.querySelector('[data-testid="tweetTextarea_0"], [role="textbox"]');
|
||||
if (editor) {
|
||||
editor.focus();
|
||||
document.execCommand('insertText', false, ${JSON.stringify(replyContent)});
|
||||
}
|
||||
})()
|
||||
`);
|
||||
await this.delay(1000);
|
||||
await win.webContents.executeJavaScript(`
|
||||
(() => {
|
||||
const btn = document.querySelector('[data-testid="tweetButton"], [data-testid="tweetButtonInline"], button[type="submit"]');
|
||||
if (btn) btn.click();
|
||||
})()
|
||||
`);
|
||||
await this.delay(3000);
|
||||
summary = `Replied to ${targetUrl.slice(0, 50)}`;
|
||||
break;
|
||||
}
|
||||
|
||||
case "like": {
|
||||
const targetUrl = String(payload.targetUrl || "");
|
||||
await win.loadURL(targetUrl);
|
||||
await this.waitForLoad(win);
|
||||
await this.delay(2000);
|
||||
await win.webContents.executeJavaScript(`
|
||||
(() => {
|
||||
const btn = document.querySelector('[data-testid="like"], button[aria-label*="Like"]');
|
||||
if (btn) btn.click();
|
||||
})()
|
||||
`);
|
||||
await this.delay(2000);
|
||||
summary = `Liked post at ${targetUrl.slice(0, 50)}`;
|
||||
break;
|
||||
}
|
||||
|
||||
case "follow": {
|
||||
const targetUrl = String(payload.targetUrl || payload.profileUrl || "");
|
||||
await win.loadURL(targetUrl);
|
||||
await this.waitForLoad(win);
|
||||
await this.delay(2000);
|
||||
await win.webContents.executeJavaScript(`
|
||||
(() => {
|
||||
const btn = document.querySelector('[data-testid*="follow"], button[aria-label*="Follow"]');
|
||||
if (btn && !btn.textContent.includes('Following')) btn.click();
|
||||
})()
|
||||
`);
|
||||
await this.delay(2000);
|
||||
summary = `Followed user at ${targetUrl.slice(0, 50)}`;
|
||||
break;
|
||||
}
|
||||
|
||||
case "browse_feed": {
|
||||
const platform = String(payload.platform || "twitter");
|
||||
const feedUrl = String(payload.url || this.getFeedUrl(platform));
|
||||
const maxPosts = Number(payload.maxPosts || 10);
|
||||
await win.loadURL(feedUrl);
|
||||
await this.waitForLoad(win);
|
||||
await this.delay(3000);
|
||||
|
||||
// Scroll to load content
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await win.webContents.executeJavaScript("window.scrollBy(0, window.innerHeight)");
|
||||
await this.delay(1500);
|
||||
}
|
||||
|
||||
const posts: string[] = await win.webContents.executeJavaScript(`
|
||||
(() => {
|
||||
const articles = document.querySelectorAll('article, [data-testid="tweet"], .feed-shared-update-v2');
|
||||
return Array.from(articles).slice(0, ${maxPosts}).map(el => el.innerText.slice(0, 300));
|
||||
})()
|
||||
`);
|
||||
summary = `Browsed feed — found ${posts.length} posts`;
|
||||
break;
|
||||
}
|
||||
|
||||
case "search": {
|
||||
const query = String(payload.query || "");
|
||||
const platform = String(payload.platform || "twitter");
|
||||
const searchUrl = this.getSearchUrl(platform, query);
|
||||
await win.loadURL(searchUrl);
|
||||
await this.waitForLoad(win);
|
||||
await this.delay(3000);
|
||||
|
||||
const results: string[] = await win.webContents.executeJavaScript(`
|
||||
(() => {
|
||||
const items = document.querySelectorAll('article, [data-testid="tweet"], .search-result');
|
||||
return Array.from(items).slice(0, 10).map(el => el.innerText.slice(0, 200));
|
||||
})()
|
||||
`);
|
||||
summary = `Search "${query.slice(0, 30)}" — ${results.length} results`;
|
||||
break;
|
||||
}
|
||||
|
||||
case "screenshot": {
|
||||
const url = String(payload.url || "about:blank");
|
||||
if (url !== "about:blank") {
|
||||
await win.loadURL(url);
|
||||
await this.waitForLoad(win);
|
||||
await this.delay(2000);
|
||||
}
|
||||
const title = await win.webContents.executeJavaScript("document.title");
|
||||
summary = `Screenshot captured: ${title}`;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
summary = `Unknown action type: ${action.actionType}`;
|
||||
}
|
||||
|
||||
// Capture screenshot
|
||||
try {
|
||||
const image: NativeImage = await win.webContents.capturePage();
|
||||
if (!image.isEmpty()) {
|
||||
writeFileSync(screenshotPath, image.toPNG());
|
||||
}
|
||||
} catch (err) {
|
||||
log.warn(`[browser] Screenshot failed: ${err}`);
|
||||
}
|
||||
|
||||
// Close the action window after a brief pause so user can see result
|
||||
await this.delay(1000);
|
||||
if (!win.isDestroyed()) win.close();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
summary,
|
||||
screenshotPath: existsSync(screenshotPath) ? screenshotPath : null,
|
||||
error: null,
|
||||
};
|
||||
} catch (error) {
|
||||
// Try to capture error screenshot
|
||||
try {
|
||||
const image: NativeImage = await win.webContents.capturePage();
|
||||
if (!image.isEmpty()) writeFileSync(screenshotPath, image.toPNG());
|
||||
} catch { /* ignore */ }
|
||||
|
||||
if (!win.isDestroyed()) win.close();
|
||||
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
log.error(`[browser] Action ${action.actionType} failed: ${msg}`);
|
||||
return {
|
||||
success: false,
|
||||
summary: "Action failed",
|
||||
screenshotPath: existsSync(screenshotPath) ? screenshotPath : null,
|
||||
error: msg,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────
|
||||
|
||||
private delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
private waitForLoad(win: BrowserWindow, timeoutMs = 15000): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
if (win.webContents.isLoading()) {
|
||||
const timer = setTimeout(resolve, timeoutMs);
|
||||
win.webContents.once("did-finish-load", () => {
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getComposeUrl(platform: string): string {
|
||||
const urls: Record<string, string> = {
|
||||
twitter: "https://x.com/compose/post",
|
||||
linkedin: "https://www.linkedin.com/feed/",
|
||||
instagram: "https://www.instagram.com/",
|
||||
facebook: "https://www.facebook.com/",
|
||||
reddit: "https://www.reddit.com/submit",
|
||||
bluesky: "https://bsky.app/",
|
||||
};
|
||||
return urls[platform] || "https://google.com";
|
||||
}
|
||||
|
||||
private getFeedUrl(platform: string): string {
|
||||
const urls: Record<string, string> = {
|
||||
twitter: "https://x.com/home",
|
||||
linkedin: "https://www.linkedin.com/feed/",
|
||||
instagram: "https://www.instagram.com/",
|
||||
reddit: "https://www.reddit.com/",
|
||||
bluesky: "https://bsky.app/",
|
||||
};
|
||||
return urls[platform] || "https://google.com";
|
||||
}
|
||||
|
||||
private getSearchUrl(platform: string, query: string): string {
|
||||
const q = encodeURIComponent(query);
|
||||
const urls: Record<string, string> = {
|
||||
twitter: `https://x.com/search?q=${q}`,
|
||||
linkedin: `https://www.linkedin.com/search/results/all/?keywords=${q}`,
|
||||
reddit: `https://www.reddit.com/search/?q=${q}`,
|
||||
};
|
||||
return urls[platform] || `https://google.com/search?q=${q}`;
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
// Close any open login windows
|
||||
for (const [, win] of this.loginWindows) {
|
||||
if (!win.isDestroyed()) win.close();
|
||||
}
|
||||
this.loginWindows.clear();
|
||||
this.queues.clear();
|
||||
this.processing.clear();
|
||||
}
|
||||
}
|
||||
+990
-6
File diff suppressed because it is too large.
Load diff
+544
-4
@@ -26,6 +26,7 @@ import {
|
||||
approvalDecisionSchema,
|
||||
approvalInputSchema,
|
||||
agentInputSchema,
|
||||
browserActionQuerySchema,
|
||||
cancelRunSchema,
|
||||
commentInputSchema,
|
||||
companyInputSchema,
|
||||
@@ -33,16 +34,29 @@ import {
|
||||
deleteEntitySchema,
|
||||
goalInputSchema,
|
||||
heartbeatSettingsSchema,
|
||||
inboxDismissSchema,
|
||||
listCommentsSchema,
|
||||
metricsQuerySchema,
|
||||
type DesktopResult,
|
||||
projectInputSchema,
|
||||
runLogSchema,
|
||||
secretInputSchema,
|
||||
settingsSchema,
|
||||
socialAccountInputSchema,
|
||||
startRunSchema,
|
||||
taskInputSchema,
|
||||
templateApplySchema,
|
||||
triggerHeartbeatSchema,
|
||||
workspaceInputSchema,
|
||||
meetingInputSchema,
|
||||
documentInputSchema,
|
||||
knowledgeEntryInputSchema,
|
||||
sprintInputSchema,
|
||||
automationRuleInputSchema,
|
||||
agentMessageInputSchema,
|
||||
messageQuerySchema,
|
||||
workflowInputSchema,
|
||||
autoAssignSchema,
|
||||
} from "@shared/contracts";
|
||||
import type { DesktopEvent } from "@shared/types";
|
||||
import { getConnectorDefinition } from "./connectors";
|
||||
@@ -50,6 +64,8 @@ import { AppDatabase } from "./database";
|
||||
import { AgentApiServer } from "./api-server";
|
||||
import { createAgentToken } from "./agent-auth";
|
||||
import { buildTaskPrompt } from "./prompt-builder";
|
||||
import { COMPANY_TEMPLATES, getTemplate } from "./templates";
|
||||
import { BrowserManager } from "./browser-manager";
|
||||
|
||||
app.name = "AgentCompany";
|
||||
|
||||
@@ -105,6 +121,7 @@ let mainWindow: BrowserWindow | null = null;
|
||||
let db: AppDatabase;
|
||||
let worker: ReturnType<typeof utilityProcess.fork> | null = null;
|
||||
let apiServer: AgentApiServer | null = null;
|
||||
let browserManager: BrowserManager | null = null;
|
||||
let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let isQuitting = false;
|
||||
let backgroundCloseNoticeShown = false;
|
||||
@@ -304,17 +321,32 @@ function buildMenu() {
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
|
||||
}
|
||||
|
||||
function isSocialBrowserSession(contents: Electron.WebContents): boolean {
|
||||
// Social login windows use partitioned sessions named "persist:social-..."
|
||||
const partition = (contents.session as { getStoragePath?: () => string })?.getStoragePath?.() ?? "";
|
||||
// Simpler: check the session object reference — social sessions are never the default session
|
||||
return contents.session !== session.defaultSession;
|
||||
}
|
||||
|
||||
function installSecurityGuards() {
|
||||
session.defaultSession.setPermissionCheckHandler(() => false);
|
||||
session.defaultSession.setPermissionRequestHandler((_webContents, _permission, callback) => {
|
||||
callback(false);
|
||||
});
|
||||
app.on("web-contents-created", (_event, contents) => {
|
||||
contents.setWindowOpenHandler(() => ({ action: "deny" }));
|
||||
contents.setWindowOpenHandler(({ url }) => {
|
||||
// Allow social browser windows to open popups (OAuth flows etc.)
|
||||
if (isSocialBrowserSession(contents)) {
|
||||
return { action: "allow" };
|
||||
}
|
||||
return { action: "deny" };
|
||||
});
|
||||
contents.on("will-attach-webview", (event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
contents.on("will-navigate", (event) => {
|
||||
// Allow navigation in social login browser windows
|
||||
if (isSocialBrowserSession(contents)) return;
|
||||
event.preventDefault();
|
||||
});
|
||||
});
|
||||
@@ -464,13 +496,26 @@ function createWorker() {
|
||||
const recoveryApiUrl = recoveryApiPort ? `http://127.0.0.1:${recoveryApiPort}` : "";
|
||||
const recoveryApiKey = recoveryApiUrl ? createAgentToken(agent.id, candidate.companyId, candidate.id) : "";
|
||||
|
||||
const recoveryAgentTasks = recoverySnapshot.tasks.filter((t) => t.companyId === candidate.companyId && t.assigneeAgentId === agent.id);
|
||||
const recoveryCompany = recoverySnapshot.companies.find((c) => c.id === candidate.companyId);
|
||||
const recoveryRecentComments = recoverySnapshot.comments
|
||||
.filter((c) => recoveryAgentTasks.some((t) => t.id === c.taskId))
|
||||
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
||||
.slice(0, 20)
|
||||
.map((c) => ({
|
||||
taskId: c.taskId,
|
||||
taskTitle: recoveryAgentTasks.find((t) => t.id === c.taskId)?.title ?? c.taskId,
|
||||
authorName: c.authorName,
|
||||
body: c.body,
|
||||
createdAt: c.createdAt,
|
||||
}));
|
||||
const recoveryPrompt = recoveryApiUrl ? buildTaskPrompt({
|
||||
agent,
|
||||
apiUrl: recoveryApiUrl,
|
||||
apiKey: recoveryApiKey,
|
||||
runId: candidate.id,
|
||||
task,
|
||||
companyName: recoverySnapshot.companies.find((c) => c.id === candidate.companyId)?.name ?? "Company",
|
||||
companyName: recoveryCompany?.name ?? "Company",
|
||||
goals: recoverySnapshot.goals.filter((g) => g.companyId === candidate.companyId),
|
||||
projects: recoverySnapshot.projects.filter((p) => p.companyId === candidate.companyId),
|
||||
directReports: db.getDirectReports(agent.id, candidate.companyId),
|
||||
@@ -484,6 +529,30 @@ function createWorker() {
|
||||
),
|
||||
tasksAwaitingReview: db.listReviewTasksForManager(agent.id, candidate.companyId),
|
||||
wakeReason: "recovery",
|
||||
allTasks: recoverySnapshot.tasks.filter((t) => t.companyId === candidate.companyId),
|
||||
recentComments: recoveryRecentComments,
|
||||
companyDescription: recoveryCompany?.description ?? "",
|
||||
socialAccounts: db.listSocialAccounts(candidate.companyId).map(a => ({
|
||||
id: a.id,
|
||||
platform: a.platform,
|
||||
accountName: a.accountName,
|
||||
status: a.status,
|
||||
requireApproval: a.requireApproval,
|
||||
})),
|
||||
recentMessages: db.listAgentMessages(candidate.companyId, { agentId: agent.id, limit: 15 }),
|
||||
upcomingMeetings: recoverySnapshot.meetings
|
||||
.filter((m: any) => m.companyId === candidate.companyId && (m.status === "scheduled" || m.status === "in_progress"))
|
||||
.filter((m: any) => {
|
||||
try { const ids = JSON.parse(m.participantAgentIds); return ids.includes(agent.id) || m.organizerAgentId === agent.id; } catch { return false; }
|
||||
})
|
||||
.slice(0, 5),
|
||||
relevantKnowledge: recoverySnapshot.knowledgeBase
|
||||
.filter((k: any) => k.companyId === candidate.companyId && (k.importance === "critical" || k.importance === "high"))
|
||||
.slice(0, 8),
|
||||
activeSprint: recoverySnapshot.sprints.find((s: any) => s.companyId === candidate.companyId && s.status === "active") ?? null,
|
||||
recentDocuments: recoverySnapshot.documents
|
||||
.filter((d: any) => d.companyId === candidate.companyId && (d.authorAgentId === agent.id || d.reviewerAgentId === agent.id || d.status === "in_review"))
|
||||
.slice(0, 5),
|
||||
}) : null;
|
||||
|
||||
// Inject API credentials exactly like the normal path
|
||||
@@ -935,6 +1004,17 @@ function startAgentHeartbeatRun(agentId: string, companyId: string, trigger: str
|
||||
const apiUrl = apiPort ? `http://127.0.0.1:${apiPort}` : "";
|
||||
const apiKey = apiUrl ? createAgentToken(agent.id, companyId, run.id) : "";
|
||||
|
||||
const heartbeatRecentComments = snapshot.comments
|
||||
.filter((c) => assignedTasks.some((t) => t.id === c.taskId))
|
||||
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
||||
.slice(0, 20)
|
||||
.map((c) => ({
|
||||
taskId: c.taskId,
|
||||
taskTitle: assignedTasks.find((t) => t.id === c.taskId)?.title ?? c.taskId,
|
||||
authorName: c.authorName,
|
||||
body: c.body,
|
||||
createdAt: c.createdAt,
|
||||
}));
|
||||
const systemPrompt = apiUrl ? buildTaskPrompt({
|
||||
agent,
|
||||
apiUrl,
|
||||
@@ -955,6 +1035,30 @@ function startAgentHeartbeatRun(agentId: string, companyId: string, trigger: str
|
||||
),
|
||||
tasksAwaitingReview: db.listReviewTasksForManager(agent.id, companyId),
|
||||
wakeReason: trigger,
|
||||
allTasks: snapshot.tasks.filter((t) => t.companyId === companyId),
|
||||
recentComments: heartbeatRecentComments,
|
||||
companyDescription: company.description ?? "",
|
||||
socialAccounts: db.listSocialAccounts(companyId).map(a => ({
|
||||
id: a.id,
|
||||
platform: a.platform,
|
||||
accountName: a.accountName,
|
||||
status: a.status,
|
||||
requireApproval: a.requireApproval,
|
||||
})),
|
||||
recentMessages: db.listAgentMessages(companyId, { agentId: agent.id, limit: 15 }),
|
||||
upcomingMeetings: snapshot.meetings
|
||||
.filter((m: any) => m.companyId === companyId && (m.status === "scheduled" || m.status === "in_progress"))
|
||||
.filter((m: any) => {
|
||||
try { const ids = JSON.parse(m.participantAgentIds); return ids.includes(agent.id) || m.organizerAgentId === agent.id; } catch { return false; }
|
||||
})
|
||||
.slice(0, 5),
|
||||
relevantKnowledge: snapshot.knowledgeBase
|
||||
.filter((k: any) => k.companyId === companyId && (k.importance === "critical" || k.importance === "high"))
|
||||
.slice(0, 8),
|
||||
activeSprint: snapshot.sprints.find((s: any) => s.companyId === companyId && s.status === "active") ?? null,
|
||||
recentDocuments: snapshot.documents
|
||||
.filter((d: any) => d.companyId === companyId && (d.authorAgentId === agent.id || d.reviewerAgentId === agent.id || d.status === "in_review"))
|
||||
.slice(0, 5),
|
||||
}) : null;
|
||||
|
||||
// Add API credentials to env
|
||||
@@ -1468,18 +1572,31 @@ function registerHandlers() {
|
||||
const apiUrl = apiPort ? `http://127.0.0.1:${apiPort}` : "";
|
||||
const apiKey = apiUrl ? createAgentToken(agent.id, task.companyId, run.id) : "";
|
||||
|
||||
const manualAgentTasks = db.listAgentTasks(agent.id, task.companyId, ["todo", "in_progress", "blocked"]);
|
||||
const manualCompany = snapshot.companies.find((c) => c.id === task.companyId);
|
||||
const manualRecentComments = snapshot.comments
|
||||
.filter((c) => manualAgentTasks.some((t) => t.id === c.taskId))
|
||||
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
||||
.slice(0, 20)
|
||||
.map((c) => ({
|
||||
taskId: c.taskId,
|
||||
taskTitle: manualAgentTasks.find((t) => t.id === c.taskId)?.title ?? c.taskId,
|
||||
authorName: c.authorName,
|
||||
body: c.body,
|
||||
createdAt: c.createdAt,
|
||||
}));
|
||||
const systemPrompt = apiUrl ? buildTaskPrompt({
|
||||
agent,
|
||||
apiUrl,
|
||||
apiKey,
|
||||
runId: run.id,
|
||||
task,
|
||||
companyName: snapshot.companies.find((c) => c.id === task.companyId)?.name ?? "Company",
|
||||
companyName: manualCompany?.name ?? "Company",
|
||||
goals: snapshot.goals.filter((g) => g.companyId === task.companyId),
|
||||
projects: snapshot.projects.filter((p) => p.companyId === task.companyId),
|
||||
directReports: db.getDirectReports(agent.id, task.companyId),
|
||||
chainOfCommand: db.getChainOfCommand(agent.id),
|
||||
assignedTasks: db.listAgentTasks(agent.id, task.companyId, ["todo", "in_progress", "blocked"]),
|
||||
assignedTasks: manualAgentTasks,
|
||||
allAgents: snapshot.agents.filter((a) => a.companyId === task.companyId),
|
||||
pendingApprovals: snapshot.approvals.filter(
|
||||
(a) => a.companyId === task.companyId &&
|
||||
@@ -1488,6 +1605,30 @@ function registerHandlers() {
|
||||
),
|
||||
tasksAwaitingReview: db.listReviewTasksForManager(agent.id, task.companyId),
|
||||
wakeReason: "manual",
|
||||
allTasks: snapshot.tasks.filter((t) => t.companyId === task.companyId),
|
||||
recentComments: manualRecentComments,
|
||||
companyDescription: manualCompany?.description ?? "",
|
||||
socialAccounts: db.listSocialAccounts(task.companyId).map(a => ({
|
||||
id: a.id,
|
||||
platform: a.platform,
|
||||
accountName: a.accountName,
|
||||
status: a.status,
|
||||
requireApproval: a.requireApproval,
|
||||
})),
|
||||
recentMessages: db.listAgentMessages(task.companyId, { agentId: agent.id, limit: 15 }),
|
||||
upcomingMeetings: snapshot.meetings
|
||||
.filter((m: any) => m.companyId === task.companyId && (m.status === "scheduled" || m.status === "in_progress"))
|
||||
.filter((m: any) => {
|
||||
try { const ids = JSON.parse(m.participantAgentIds); return ids.includes(agent.id) || m.organizerAgentId === agent.id; } catch { return false; }
|
||||
})
|
||||
.slice(0, 5),
|
||||
relevantKnowledge: snapshot.knowledgeBase
|
||||
.filter((k: any) => k.companyId === task.companyId && (k.importance === "critical" || k.importance === "high"))
|
||||
.slice(0, 8),
|
||||
activeSprint: snapshot.sprints.find((s: any) => s.companyId === task.companyId && s.status === "active") ?? null,
|
||||
recentDocuments: snapshot.documents
|
||||
.filter((d: any) => d.companyId === task.companyId && (d.authorAgentId === agent.id || d.reviewerAgentId === agent.id || d.status === "in_review"))
|
||||
.slice(0, 5),
|
||||
}) : null;
|
||||
|
||||
// Add API credentials to env
|
||||
@@ -1791,6 +1932,366 @@ function registerHandlers() {
|
||||
return ok([]);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Inbox ──────────────────────────────────────────────────────────
|
||||
registerHandle(IPC_CHANNELS.getInbox, (_event, ...args) => {
|
||||
const companyId = companyIdSchema.parse(args[0]);
|
||||
const err = ensureCompanyExists(companyId);
|
||||
if (err) return err;
|
||||
return ok(db.getInbox(companyId));
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.dismissInboxItem, (_event, ...args) => {
|
||||
const input = inboxDismissSchema.parse(args[0]);
|
||||
const err = ensureCompanyExists(input.companyId);
|
||||
if (err) return err;
|
||||
// Dismiss is a no-op for now (items are computed on the fly)
|
||||
return ok(true);
|
||||
});
|
||||
|
||||
// ── Metrics ────────────────────────────────────────────────────────
|
||||
registerHandle(IPC_CHANNELS.getCompanyMetrics, (_event, ...args) => {
|
||||
const input = metricsQuerySchema.parse(args[0]);
|
||||
const err = ensureCompanyExists(input.companyId);
|
||||
if (err) return err;
|
||||
return ok(db.getCompanyMetrics(input.companyId, input.days));
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.getAgentMetrics, (_event, ...args) => {
|
||||
const input = metricsQuerySchema.parse(args[0]);
|
||||
const err = ensureCompanyExists(input.companyId);
|
||||
if (err) return err;
|
||||
if (!input.agentId) return fail("MISSING_AGENT_ID", "agentId is required");
|
||||
const metrics = db.getCompanyMetrics(input.companyId, input.days);
|
||||
const agentMetric = metrics.agentMetrics.find(m => m.agentId === input.agentId);
|
||||
if (!agentMetric) return fail("AGENT_NOT_FOUND", "Agent metrics not found");
|
||||
return ok(agentMetric);
|
||||
});
|
||||
|
||||
// ── Standup ────────────────────────────────────────────────────────
|
||||
registerHandle(IPC_CHANNELS.getStandupReport, (_event, ...args) => {
|
||||
const companyId = companyIdSchema.parse(args[0]);
|
||||
const err = ensureCompanyExists(companyId);
|
||||
if (err) return err;
|
||||
return ok(db.getStandupReport(companyId));
|
||||
});
|
||||
|
||||
// ── Templates ──────────────────────────────────────────────────────
|
||||
registerHandle(IPC_CHANNELS.listCompanyTemplates, () => {
|
||||
return ok(COMPANY_TEMPLATES);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.applyCompanyTemplate, (_event, ...args) => {
|
||||
const input = templateApplySchema.parse(args[0]);
|
||||
const template = getTemplate(input.templateId);
|
||||
if (!template) return fail("TEMPLATE_NOT_FOUND", "Company template not found");
|
||||
|
||||
// Create the company
|
||||
const companyId = db.saveCompany({
|
||||
name: input.companyName,
|
||||
description: input.companyDescription || template.description,
|
||||
status: "active",
|
||||
autoApproveHires: true,
|
||||
});
|
||||
|
||||
// Create agents in order (so we can resolve reportsTo by index)
|
||||
const agentIds: string[] = [];
|
||||
const connectors = db.listSnapshot().connectors;
|
||||
const readyConnectors = connectors.filter(c => c.status === "ready");
|
||||
const fallbackConnector = readyConnectors[0]?.id ?? input.connectorId ?? "claude_local";
|
||||
|
||||
for (const agentDef of template.agents) {
|
||||
const connectorToUse = readyConnectors.find(c => c.id === agentDef.connectorPreference)
|
||||
? agentDef.connectorPreference
|
||||
: fallbackConnector;
|
||||
|
||||
const reportsTo = agentDef.reportsToIndex !== null ? agentIds[agentDef.reportsToIndex] ?? null : null;
|
||||
|
||||
const agentId = db.saveAgent({
|
||||
companyId,
|
||||
name: agentDef.name,
|
||||
role: agentDef.role,
|
||||
title: agentDef.title,
|
||||
department: agentDef.department ?? null,
|
||||
capabilities: agentDef.capabilities,
|
||||
reportsTo,
|
||||
connectorId: connectorToUse,
|
||||
workspaceId: input.workspaceId ?? null,
|
||||
model: null,
|
||||
budgetMonthlyUsd: agentDef.budgetMonthlyUsd,
|
||||
status: "active",
|
||||
});
|
||||
agentIds.push(agentId);
|
||||
}
|
||||
|
||||
// Create goals owned by the CEO (first agent)
|
||||
const ceoId = agentIds[0];
|
||||
for (const goalDef of template.goals) {
|
||||
db.saveGoal({
|
||||
companyId,
|
||||
title: goalDef.title,
|
||||
description: goalDef.description,
|
||||
ownerAgentId: ceoId ?? null,
|
||||
parentId: null,
|
||||
status: "active",
|
||||
});
|
||||
}
|
||||
|
||||
// Set current company
|
||||
db.setCurrentCompany(companyId);
|
||||
|
||||
// Enable heartbeat for CEO to kick off autonomous operation
|
||||
if (ceoId) {
|
||||
db.setHeartbeat(ceoId, true, 120);
|
||||
}
|
||||
|
||||
// Activity log
|
||||
db.addActivity({
|
||||
companyId,
|
||||
actor: "board",
|
||||
action: "company.created_from_template",
|
||||
entityType: "company",
|
||||
entityId: companyId,
|
||||
detail: `Created from template: ${template.name}`,
|
||||
});
|
||||
|
||||
emitEvent({ type: "domain-changed" });
|
||||
|
||||
return ok(companyId);
|
||||
});
|
||||
|
||||
// ── Social Accounts ────────────────────────────────────────────────
|
||||
registerHandle(IPC_CHANNELS.saveSocialAccount, (_event, ...args) => {
|
||||
const input = socialAccountInputSchema.parse(args[0]);
|
||||
const err = ensureCompanyExists(input.companyId);
|
||||
if (err) return err;
|
||||
const id = db.saveSocialAccount(input);
|
||||
emitEvent({ type: "domain-changed" });
|
||||
return ok(id);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.deleteSocialAccount, (_event, ...args) => {
|
||||
const input = deleteEntitySchema.parse(args[0]);
|
||||
const err = ensureCompanyExists(input.companyId);
|
||||
if (err) return err;
|
||||
db.deleteSocialAccount(input.id, input.companyId);
|
||||
emitEvent({ type: "domain-changed" });
|
||||
return ok(true);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.listBrowserActions, (_event, ...args) => {
|
||||
const input = browserActionQuerySchema.parse(args[0]);
|
||||
const err = ensureCompanyExists(input.companyId);
|
||||
if (err) return err;
|
||||
const actions = db.listBrowserActions(input.companyId, {
|
||||
socialAccountId: input.socialAccountId,
|
||||
status: input.status,
|
||||
limit: input.limit,
|
||||
});
|
||||
return ok(actions);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.cancelBrowserAction, (_event, ...args) => {
|
||||
const input = cancelRunSchema.parse(args[0]);
|
||||
db.updateBrowserAction(input.runId, { status: "cancelled" });
|
||||
emitEvent({ type: "domain-changed" });
|
||||
return ok(true);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.triggerBrowserLogin, async (_event, ...args) => {
|
||||
const socialAccountId = z.string().uuid().parse(args[0]);
|
||||
if (!browserManager) {
|
||||
return fail("BROWSER_NOT_READY", "Browser manager not initialized");
|
||||
}
|
||||
|
||||
// Find the social account to get its platform
|
||||
const snapshot = db.listSnapshot();
|
||||
const account = snapshot.socialAccounts.find(a => a.id === socialAccountId);
|
||||
if (!account) {
|
||||
return fail("ACCOUNT_NOT_FOUND", "Social account not found");
|
||||
}
|
||||
|
||||
// Launch login browser (this runs async - the browser stays open for user to login)
|
||||
// We run it in background and don't block the IPC response
|
||||
browserManager.launchLoginBrowser(socialAccountId, account.platform).then(result => {
|
||||
if (result.success) {
|
||||
db.saveSocialAccount({
|
||||
id: account.id,
|
||||
companyId: account.companyId,
|
||||
platform: account.platform,
|
||||
accountName: account.accountName,
|
||||
displayName: account.displayName,
|
||||
profileUrl: account.profileUrl,
|
||||
credentialSecretId: account.credentialSecretId,
|
||||
status: "logged_in",
|
||||
requireApproval: account.requireApproval,
|
||||
});
|
||||
emitEvent({ type: "domain-changed" });
|
||||
}
|
||||
}).catch(err => {
|
||||
log.error(`[browser] Login failed: ${err}`);
|
||||
});
|
||||
|
||||
return ok(true);
|
||||
});
|
||||
|
||||
// ── Meetings ──────────────────────────────────────────────────────
|
||||
registerHandle(IPC_CHANNELS.saveMeeting, async (_event, payload) => {
|
||||
const parsed = meetingInputSchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(parsed.companyId);
|
||||
if (companyError) return companyError;
|
||||
const id = db.saveMeeting(parsed);
|
||||
emitEvent({ type: "domain-changed" });
|
||||
return ok(id);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.deleteMeeting, async (_event, payload) => {
|
||||
const parsed = deleteEntitySchema.parse(payload);
|
||||
db.deleteMeeting(parsed.id, parsed.companyId);
|
||||
emitEvent({ type: "domain-changed" });
|
||||
return ok(true);
|
||||
});
|
||||
|
||||
// ── Documents ─────────────────────────────────────────────────────
|
||||
registerHandle(IPC_CHANNELS.saveDocument, async (_event, payload) => {
|
||||
const parsed = documentInputSchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(parsed.companyId);
|
||||
if (companyError) return companyError;
|
||||
const id = db.saveDocument(parsed);
|
||||
emitEvent({ type: "domain-changed" });
|
||||
return ok(id);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.deleteDocument, async (_event, payload) => {
|
||||
const parsed = deleteEntitySchema.parse(payload);
|
||||
db.deleteDocument(parsed.id, parsed.companyId);
|
||||
emitEvent({ type: "domain-changed" });
|
||||
return ok(true);
|
||||
});
|
||||
|
||||
// ── Knowledge Base ────────────────────────────────────────────────
|
||||
registerHandle(IPC_CHANNELS.saveKnowledgeEntry, async (_event, payload) => {
|
||||
const parsed = knowledgeEntryInputSchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(parsed.companyId);
|
||||
if (companyError) return companyError;
|
||||
const id = db.saveKnowledgeEntry(parsed);
|
||||
emitEvent({ type: "domain-changed" });
|
||||
return ok(id);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.deleteKnowledgeEntry, async (_event, payload) => {
|
||||
const parsed = deleteEntitySchema.parse(payload);
|
||||
db.deleteKnowledgeEntry(parsed.id, parsed.companyId);
|
||||
emitEvent({ type: "domain-changed" });
|
||||
return ok(true);
|
||||
});
|
||||
|
||||
// ── Sprints ───────────────────────────────────────────────────────
|
||||
registerHandle(IPC_CHANNELS.saveSprint, async (_event, payload) => {
|
||||
const parsed = sprintInputSchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(parsed.companyId);
|
||||
if (companyError) return companyError;
|
||||
const id = db.saveSprint(parsed);
|
||||
emitEvent({ type: "domain-changed" });
|
||||
return ok(id);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.deleteSprint, async (_event, payload) => {
|
||||
const parsed = deleteEntitySchema.parse(payload);
|
||||
db.deleteSprint(parsed.id, parsed.companyId);
|
||||
emitEvent({ type: "domain-changed" });
|
||||
return ok(true);
|
||||
});
|
||||
|
||||
// ── Automation Rules ───────────────────────────────────────────────
|
||||
registerHandle(IPC_CHANNELS.saveAutomationRule, async (_event, payload) => {
|
||||
const parsed = automationRuleInputSchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(parsed.companyId);
|
||||
if (companyError) return companyError;
|
||||
const id = db.saveAutomationRule(parsed);
|
||||
emitEvent({ type: "domain-changed" });
|
||||
return ok(id);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.deleteAutomationRule, async (_event, payload) => {
|
||||
const parsed = deleteEntitySchema.parse(payload);
|
||||
db.deleteAutomationRule(parsed.id, parsed.companyId);
|
||||
emitEvent({ type: "domain-changed" });
|
||||
return ok(true);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.toggleAutomationRule, async (_event, payload) => {
|
||||
const parsed = z.object({ id: z.string().uuid(), companyId: z.string().uuid(), status: z.enum(["active", "paused"]) }).parse(payload);
|
||||
db.toggleAutomationRule(parsed.id, parsed.companyId, parsed.status);
|
||||
emitEvent({ type: "domain-changed" });
|
||||
return ok(true);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.getAutomationLog, async (_event, companyId) => {
|
||||
const parsed = companyIdSchema.parse(companyId);
|
||||
return ok(db.getAutomationLog(parsed));
|
||||
});
|
||||
|
||||
// ── Agent Messages ─────────────────────────────────────────────────
|
||||
registerHandle(IPC_CHANNELS.sendAgentMessage, async (_event, payload) => {
|
||||
const parsed = agentMessageInputSchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(parsed.companyId);
|
||||
if (companyError) return companyError;
|
||||
const id = db.sendAgentMessage(parsed);
|
||||
// Notify recipient via event
|
||||
const msg = db.listAgentMessages(parsed.companyId, { limit: 1 })?.[0];
|
||||
if (msg) emitEvent({ type: "new-message", message: msg });
|
||||
emitEvent({ type: "domain-changed" });
|
||||
return ok(id);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.listAgentMessages, async (_event, payload) => {
|
||||
const parsed = messageQuerySchema.parse(payload);
|
||||
return ok(db.listAgentMessages(parsed.companyId, parsed));
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.markMessageRead, async (_event, payload) => {
|
||||
const parsed = z.object({ messageId: z.string().uuid(), companyId: z.string().uuid() }).parse(payload);
|
||||
db.markMessageRead(parsed.messageId, parsed.companyId);
|
||||
return ok(true);
|
||||
});
|
||||
|
||||
// ── Workflows ──────────────────────────────────────────────────────
|
||||
registerHandle(IPC_CHANNELS.saveWorkflow, async (_event, payload) => {
|
||||
const parsed = workflowInputSchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(parsed.companyId);
|
||||
if (companyError) return companyError;
|
||||
const id = db.saveWorkflow(parsed);
|
||||
emitEvent({ type: "domain-changed" });
|
||||
return ok(id);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.deleteWorkflow, async (_event, payload) => {
|
||||
const parsed = deleteEntitySchema.parse(payload);
|
||||
db.deleteWorkflow(parsed.id, parsed.companyId);
|
||||
emitEvent({ type: "domain-changed" });
|
||||
return ok(true);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.runWorkflow, async (_event, payload) => {
|
||||
const parsed = z.object({ id: z.string().uuid(), companyId: z.string().uuid() }).parse(payload);
|
||||
const companyError = ensureCompanyExists(parsed.companyId);
|
||||
if (companyError) return companyError;
|
||||
db.activateWorkflow(parsed.id, parsed.companyId);
|
||||
emitEvent({ type: "domain-changed" });
|
||||
return ok(true);
|
||||
});
|
||||
|
||||
// ── Auto-Assign ────────────────────────────────────────────────────
|
||||
registerHandle(IPC_CHANNELS.autoAssignTask, async (_event, payload) => {
|
||||
const parsed = autoAssignSchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(parsed.companyId);
|
||||
if (companyError) return companyError;
|
||||
const agentId = db.autoAssignTask(parsed.taskId, parsed.companyId);
|
||||
if (!agentId) return fail("NO_AVAILABLE_AGENT", "No available agent could be found for this task.");
|
||||
emitEvent({ type: "domain-changed" });
|
||||
return ok(agentId);
|
||||
});
|
||||
}
|
||||
|
||||
async function checkForUpdates() {
|
||||
@@ -1869,6 +2370,43 @@ async function bootstrap() {
|
||||
});
|
||||
await apiServer.start();
|
||||
|
||||
// Initialize browser manager for social media automation
|
||||
browserManager = new BrowserManager(join(app.getPath("userData"), "browser-data"));
|
||||
|
||||
// Wire browser action handler
|
||||
if (apiServer && browserManager) {
|
||||
const bm = browserManager;
|
||||
apiServer.setBrowserActionHandler(async (actionId, socialAccountId, actionType, payload) => {
|
||||
try {
|
||||
db.updateBrowserAction(actionId, { status: "running", startedAt: new Date().toISOString() });
|
||||
emitEvent({ type: "domain-changed" });
|
||||
|
||||
const result = await bm.executeAction({
|
||||
id: actionId,
|
||||
socialAccountId,
|
||||
actionType,
|
||||
payload,
|
||||
});
|
||||
|
||||
db.updateBrowserAction(actionId, {
|
||||
status: result.success ? "succeeded" : "failed",
|
||||
resultSummary: result.summary,
|
||||
screenshotPath: result.screenshotPath,
|
||||
errorMessage: result.error,
|
||||
finishedAt: new Date().toISOString(),
|
||||
});
|
||||
emitEvent({ type: "domain-changed" });
|
||||
} catch (error) {
|
||||
db.updateBrowserAction(actionId, {
|
||||
status: "failed",
|
||||
errorMessage: error instanceof Error ? error.message : String(error),
|
||||
finishedAt: new Date().toISOString(),
|
||||
});
|
||||
emitEvent({ type: "domain-changed" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Start heartbeat scheduler for continuous agent operation
|
||||
startHeartbeatScheduler();
|
||||
|
||||
@@ -1923,6 +2461,8 @@ app.on("before-quit", () => {
|
||||
heartbeatTimer = null;
|
||||
apiServer?.stop();
|
||||
apiServer = null;
|
||||
browserManager?.shutdown();
|
||||
browserManager = null;
|
||||
worker?.kill();
|
||||
worker = null;
|
||||
db?.close();
|
||||
|
||||
+712
-8
@@ -1,4 +1,4 @@
|
||||
import type { AgentRecord, ApprovalRecord, GoalRecord, ProjectRecord, TaskRecord } from "@shared/types";
|
||||
import type { AgentRecord, AgentMessageRecord, ApprovalRecord, DepartmentId, DocumentRecord, GoalRecord, KnowledgeEntryRecord, MeetingRecord, ProjectRecord, SprintRecord, TaskRecord } from "@shared/types";
|
||||
|
||||
interface PromptContext {
|
||||
agent: AgentRecord;
|
||||
@@ -17,17 +17,121 @@ interface PromptContext {
|
||||
/** Tasks from direct reports that are awaiting this agent's review */
|
||||
tasksAwaitingReview: TaskRecord[];
|
||||
wakeReason: string | null;
|
||||
/** All company tasks (for cross-team visibility) */
|
||||
allTasks: TaskRecord[];
|
||||
/** Recent comments on the agent's tasks */
|
||||
recentComments: Array<{ taskId: string; taskTitle: string; authorName: string; body: string; createdAt: string }>;
|
||||
/** Company name */
|
||||
companyDescription: string;
|
||||
/** Social media accounts available to the company */
|
||||
socialAccounts: Array<{ id: string; platform: string; accountName: string; status: string; requireApproval: boolean }>;
|
||||
/** Recent messages to/from this agent */
|
||||
recentMessages?: AgentMessageRecord[];
|
||||
/** Upcoming meetings involving this agent */
|
||||
upcomingMeetings?: MeetingRecord[];
|
||||
/** Relevant knowledge base entries */
|
||||
relevantKnowledge?: KnowledgeEntryRecord[];
|
||||
/** Active sprint */
|
||||
activeSprint?: SprintRecord | null;
|
||||
/** Recent documents authored by or relevant to this agent */
|
||||
recentDocuments?: DocumentRecord[];
|
||||
}
|
||||
|
||||
const DEPARTMENT_LABELS: Record<string, string> = {
|
||||
executive: "Executive Suite",
|
||||
engineering: "Engineering",
|
||||
product: "Product & Design",
|
||||
marketing: "Marketing",
|
||||
sales: "Sales & BD",
|
||||
hr: "People & Culture",
|
||||
finance: "Finance & Legal",
|
||||
legal: "Legal",
|
||||
operations: "Operations",
|
||||
customer_support: "Customer Success",
|
||||
research: "Research",
|
||||
design: "Design",
|
||||
};
|
||||
|
||||
export function buildSystemPrompt(ctx: PromptContext): string {
|
||||
const sections: string[] = [];
|
||||
|
||||
// Compute role flags early so they can be used throughout
|
||||
const isCEO = ctx.chainOfCommand.length === 0;
|
||||
const isExecutive = isCEO;
|
||||
const isManager = !isCEO && ctx.directReports.length > 0;
|
||||
const isIC = !isCEO && ctx.directReports.length === 0;
|
||||
|
||||
// Identity
|
||||
sections.push(`# Agent Identity
|
||||
You are ${ctx.agent.name}, a ${ctx.agent.role}${ctx.agent.title ? ` (${ctx.agent.title})` : ""} at ${ctx.companyName}.
|
||||
${ctx.agent.capabilities ? `Your capabilities: ${ctx.agent.capabilities}` : ""}
|
||||
`);
|
||||
|
||||
// Company context
|
||||
if (ctx.companyDescription) {
|
||||
sections.push(`# Company Mission
|
||||
${ctx.companyDescription}
|
||||
`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NEW: Cognitive Framework (after Identity, before Chain of Command)
|
||||
// ---------------------------------------------------------------------------
|
||||
sections.push(`# Cognitive Framework (How You Think)
|
||||
You operate using a structured decision-making process:
|
||||
|
||||
## OBSERVE: Gather Information
|
||||
- Read your standup briefing first (GET /api/agents/me/standup)
|
||||
- Check all recent comments on your tasks
|
||||
- Review your team's workload if you're a manager
|
||||
- Note any changes since your last wake
|
||||
|
||||
## ORIENT: Analyze & Prioritize
|
||||
- What is the most critical issue right now? (blockers > reviews > new work)
|
||||
- What is the company-wide priority? (align with active goals)
|
||||
- What are my teammates depending on me for?
|
||||
- What has changed since my last session?
|
||||
|
||||
## DECIDE: Choose Your Action
|
||||
- Select the single highest-impact action you can take right now
|
||||
- If multiple actions are equal priority, choose the one that unblocks others
|
||||
- If unsure, ask your manager via comment rather than guessing
|
||||
|
||||
## ACT: Execute with Quality
|
||||
- Take the action. Document what you did and why.
|
||||
- Leave a trail: comments, status updates, documents
|
||||
- Verify your work before submitting for review
|
||||
`);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NEW: Agent Personality & Work Style (after Cognitive Framework)
|
||||
// ---------------------------------------------------------------------------
|
||||
if (isExecutive) {
|
||||
sections.push(`# Your Leadership Style
|
||||
- You think strategically, not tactically. Your job is vision and coordination.
|
||||
- You communicate through clear, decisive directives.
|
||||
- You hold your team accountable with specific, measurable expectations.
|
||||
- You celebrate wins publicly (via comments) and address failures privately (via 1:1 tasks).
|
||||
- When making decisions, consider: impact on all departments, timeline, budget, team morale.
|
||||
`);
|
||||
} else if (isManager) {
|
||||
sections.push(`# Your Management Style
|
||||
- You are a force multiplier. Your success is measured by your team's output, not your own.
|
||||
- Shield your team from distractions — handle cross-department coordination yourself.
|
||||
- Give specific, actionable feedback — not vague "looks good" or "needs work."
|
||||
- Track your team's velocity and identify bottlenecks proactively.
|
||||
- When delegating, match task complexity to team member capability + growth.
|
||||
`);
|
||||
} else {
|
||||
sections.push(`# Your Work Philosophy
|
||||
- You are a craftsperson. Take pride in quality work that ships.
|
||||
- Ask questions early — a 5-minute clarification saves hours of wrong work.
|
||||
- Leave your code/work better than you found it (but don't gold-plate).
|
||||
- When stuck for more than 10 minutes, escalate. Don't spin your wheels.
|
||||
- Document decisions and tradeoffs as you go, not after.
|
||||
`);
|
||||
}
|
||||
|
||||
// Organization context
|
||||
if (ctx.chainOfCommand.length > 0) {
|
||||
sections.push(`# Chain of Command
|
||||
@@ -45,6 +149,55 @@ ${ctx.directReports.map((a) => `- ${a.name}: ${a.role}${a.title ? ` (${a.title})
|
||||
`);
|
||||
}
|
||||
|
||||
// Team workload overview (for managers)
|
||||
if (ctx.directReports.length > 0) {
|
||||
const reportTaskSummaries = ctx.directReports.map((report) => {
|
||||
const reportTasks = ctx.allTasks.filter((t) => t.assigneeAgentId === report.id);
|
||||
const inProgress = reportTasks.filter((t) => t.status === "in_progress").length;
|
||||
const inReview = reportTasks.filter((t) => t.status === "in_review").length;
|
||||
const blocked = reportTasks.filter((t) => t.status === "blocked").length;
|
||||
const todo = reportTasks.filter((t) => t.status === "todo").length;
|
||||
return ` - ${report.name} (${report.role}): ${inProgress} in progress, ${inReview} in review, ${blocked} blocked, ${todo} queued`;
|
||||
});
|
||||
|
||||
sections.push(`# Team Workload
|
||||
${reportTaskSummaries.join("\n")}
|
||||
`);
|
||||
}
|
||||
|
||||
// Department context
|
||||
if (ctx.agent.department) {
|
||||
const deptLabel = DEPARTMENT_LABELS[ctx.agent.department] ?? ctx.agent.department;
|
||||
const deptMembers = ctx.allAgents.filter(
|
||||
(a) => a.department === ctx.agent.department && a.id !== ctx.agent.id && a.status !== "terminated",
|
||||
);
|
||||
sections.push(`# Department: ${deptLabel}
|
||||
You belong to the ${deptLabel} department.${deptMembers.length > 0 ? `
|
||||
Your department colleagues:
|
||||
${deptMembers.map((a) => `- ${a.name} (${a.role}) [${a.status}]`).join("\n")}` : ""}
|
||||
`);
|
||||
}
|
||||
|
||||
// Cross-department map (for executives and managers)
|
||||
const isCEOCheck = ctx.chainOfCommand.length === 0;
|
||||
if (isCEOCheck || ctx.directReports.length > 0) {
|
||||
const deptMap = new Map<string, AgentRecord[]>();
|
||||
for (const a of ctx.allAgents.filter((a) => a.status !== "terminated")) {
|
||||
const dept = a.department ?? "unassigned";
|
||||
if (!deptMap.has(dept)) deptMap.set(dept, []);
|
||||
deptMap.get(dept)!.push(a);
|
||||
}
|
||||
if (deptMap.size > 1) {
|
||||
const deptLines = Array.from(deptMap.entries()).map(([dept, members]) => {
|
||||
const label = DEPARTMENT_LABELS[dept] ?? dept;
|
||||
return ` ${label} (${members.length}): ${members.map((m) => m.name).join(", ")}`;
|
||||
});
|
||||
sections.push(`# Company Department Map
|
||||
${deptLines.join("\n")}
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
// Active goals
|
||||
const activeGoals = ctx.goals.filter((g) => g.status === "active" || g.status === "planned");
|
||||
if (activeGoals.length > 0) {
|
||||
@@ -166,8 +319,109 @@ When the team needs new members:
|
||||
- When you need input from another agent, comment on their task or create a new task for them
|
||||
- Always @mention the agent name in comments for clarity
|
||||
- Be specific: include what you need, why, and any deadline
|
||||
|
||||
## SOP-008: Cross-Department Collaboration
|
||||
When your work depends on or affects another department:
|
||||
1. Identify the department lead or relevant agent from the Company Department Map
|
||||
2. Create a task assigned to them with clear requirements and deadline
|
||||
3. Set the task priority based on your blocking urgency
|
||||
4. Post a comment explaining the cross-department dependency
|
||||
5. Common cross-department workflows:
|
||||
- Engineering ↔ Product: Product writes PRD → Engineering implements → QA tests → Product validates
|
||||
- Marketing ↔ Engineering: Marketing needs landing page → Engineering builds → Marketing reviews
|
||||
- Sales ↔ Product: Sales collects customer feedback → Product prioritizes → Engineering delivers
|
||||
- HR ↔ All Departments: HR facilitates hiring → Department head defines requirements → HR screens
|
||||
- Finance ↔ All Departments: Department submits budget_request → CFO reviews → Finance tracks spend
|
||||
- Customer Support ↔ Engineering: Support reports bug → Engineering fixes → Support verifies with customer
|
||||
|
||||
### Standard Cross-Department Workflows:
|
||||
|
||||
**Product Development Cycle:**
|
||||
Product → writes PRD → Design → creates mockups → Engineering → implements → QA → tests → Product → validates → Marketing → announces → Sales → sells → Support → supports
|
||||
|
||||
**Incident Response:**
|
||||
Support → reports issue → Engineering → investigates → DevOps → mitigates → Engineering → root cause → Product → decides fix priority → Engineering → implements fix → QA → regression test → Support → verifies with customer → Knowledge → post-mortem
|
||||
|
||||
**New Feature Request (from customer):**
|
||||
Support → captures requirement → Product → evaluates & prioritizes → Design → UX research → Product → writes PRD → Engineering → estimates → Finance → budgets → CEO → approves → Engineering → builds
|
||||
|
||||
**Hiring Pipeline:**
|
||||
Department Head → identifies gap → HR → writes job description → Finance → approves budget → CEO → final approval → HR → onboards → Manager → assigns buddy & first tasks
|
||||
|
||||
**Quarterly Business Review:**
|
||||
Finance → generates P&L → Operations → compiles OKR status → Each department head → submits report → CEO → presents to Board
|
||||
|
||||
## SOP-009: Document Management
|
||||
Important work should produce artifacts:
|
||||
- **PRDs**: Product Managers write PRDs before engineering work begins
|
||||
- **Technical Specs**: Engineers write specs before complex implementations
|
||||
- **Post-Mortems**: After incidents, the lead writes a post-mortem with lessons learned
|
||||
- **Meeting Notes**: After important meetings, capture decisions and action items
|
||||
- **Status Reports**: Weekly status reports from department heads to CEO
|
||||
Use task comments to share document content. Reference document IDs in related tasks.
|
||||
|
||||
## SOP-010: Budget & Financial Protocol
|
||||
- All agents have a monthly budget. Track your spend.
|
||||
- Before starting expensive operations, check your remaining budget via GET /api/agents/me
|
||||
- If you need more budget, escalate to your manager who escalates to CFO
|
||||
- For vendor purchases or new tools, submit a budget_request approval
|
||||
- The CFO reviews all budget requests above the department head's authority
|
||||
- Monthly budget resets on the 1st — plan your most expensive work early in the month
|
||||
|
||||
## SOP-011: Performance & Quality Standards
|
||||
- Every agent's work is tracked: tasks completed, success rate, cost efficiency, review cycles
|
||||
- Quality > Speed: Better to submit correct work slowly than buggy work quickly
|
||||
- If a task is sent back for revision more than twice, escalate to your manager
|
||||
- Managers should conduct monthly performance check-ins via task comments
|
||||
- Identify agents who are consistently blocked or failing — propose retraining or role changes
|
||||
|
||||
## SOP-012: Sprint & Cadence Management
|
||||
The company operates in recurring cycles:
|
||||
- **Daily Standup**: Every heartbeat wake starts with standup check (SOP-001)
|
||||
- **Sprint Cycle**: 2-week sprints with planning → execution → review → retrospective
|
||||
- **Weekly Sync**: Department heads report progress to CEO
|
||||
- **Monthly Review**: Company-wide metrics review, budget check, hiring needs
|
||||
When creating tasks, consider the current sprint cycle and set realistic deadlines.
|
||||
|
||||
## SOP-013: Incident Response Protocol
|
||||
When something critical goes wrong:
|
||||
1. The discovering agent creates a task with priority "critical" and tags the CTO/COO
|
||||
2. CTO assesses technical impact, COO assesses business impact
|
||||
3. Engineering focuses on mitigation first, root cause second
|
||||
4. After resolution, the lead writes a post-mortem document
|
||||
5. Action items from the post-mortem become tracked tasks
|
||||
6. Knowledge learned gets added to the team's institutional memory
|
||||
|
||||
## SOP-014: Hiring Pipeline
|
||||
When the company needs new talent:
|
||||
1. Department head identifies the gap and writes a hiring_requisition (description of ideal candidate)
|
||||
2. HR Director reviews and approves the requisition
|
||||
3. CFO confirms budget availability
|
||||
4. CEO gives final approval via hire_agent approval
|
||||
5. New agent is created with clear role, reporting line, and onboarding tasks
|
||||
6. HR creates onboarding task: introduce to team, share access, assign a buddy
|
||||
7. Manager assigns first tasks appropriate to the new hire's level
|
||||
|
||||
## SOP-015: Knowledge Management
|
||||
The company should get smarter over time:
|
||||
- After solving a hard problem, document the solution as a lesson_learned comment
|
||||
- After making an important decision, document the rationale and alternatives considered
|
||||
- When onboarding new agents, share relevant past decisions and lessons
|
||||
- Periodically review past incidents and ensure fixes are still in place
|
||||
- Capture "best practices" that emerge from repeated successes
|
||||
`);
|
||||
|
||||
// Recent comments (last messages on agent's tasks)
|
||||
if (ctx.recentComments.length > 0) {
|
||||
const commentLines = ctx.recentComments.slice(0, 10).map((c) => {
|
||||
return `- [${c.taskTitle}] ${c.authorName}: "${c.body.slice(0, 150)}${c.body.length > 150 ? "..." : ""}"`;
|
||||
});
|
||||
sections.push(`# Recent Messages
|
||||
These are the most recent comments on your tasks. Review them before taking action.
|
||||
${commentLines.join("\n")}
|
||||
`);
|
||||
}
|
||||
|
||||
// API instructions
|
||||
sections.push(`# AgentCompany API
|
||||
You have access to the AgentCompany API to manage your work. Base URL: ${ctx.apiUrl}
|
||||
@@ -280,12 +534,157 @@ GET /api/companies/{companyId}/goals
|
||||
|
||||
### List Company Projects
|
||||
GET /api/companies/{companyId}/projects
|
||||
|
||||
### Create a Document (PRD, spec, post-mortem, etc.)
|
||||
POST /api/companies/{companyId}/documents
|
||||
Body: {
|
||||
"type": "prd|technical_spec|design_doc|test_plan|post_mortem|meeting_notes|budget_proposal|hiring_requisition|status_report|incident_report|architecture_decision|knowledge_article",
|
||||
"title": "Document title",
|
||||
"content": "Full document content in markdown",
|
||||
"reviewerAgentId": "{optionalReviewerAgentId}",
|
||||
"projectId": "{optionalProjectId}",
|
||||
"goalId": "{optionalGoalId}",
|
||||
"status": "draft"
|
||||
}
|
||||
|
||||
### List Company Documents
|
||||
GET /api/companies/{companyId}/documents
|
||||
|
||||
### Schedule a Meeting
|
||||
POST /api/companies/{companyId}/meetings
|
||||
Body: {
|
||||
"type": "standup|sprint_planning|sprint_review|retrospective|one_on_one|all_hands|department_sync|incident_review|hiring_committee|budget_review",
|
||||
"title": "Meeting title",
|
||||
"participantAgentIds": "[\"agentId1\", \"agentId2\"]",
|
||||
"scheduledAt": "2026-03-20T10:00:00Z",
|
||||
"durationMinutes": 30,
|
||||
"agendaJson": "[\"Item 1\", \"Item 2\"]"
|
||||
}
|
||||
|
||||
### Update Meeting (add notes, decisions)
|
||||
PATCH /api/meetings/{meetingId}
|
||||
Body: {
|
||||
"notesJson": "[\"Note 1\", \"Note 2\"]",
|
||||
"decisionsJson": "[\"Decision 1\"]",
|
||||
"actionItemsJson": "[\"Action 1\"]",
|
||||
"status": "completed"
|
||||
}
|
||||
|
||||
### Add Knowledge Entry (lessons learned, best practices)
|
||||
POST /api/companies/{companyId}/knowledge
|
||||
Body: {
|
||||
"category": "lesson_learned|best_practice|decision|process|technical|business|onboarding|incident",
|
||||
"topic": "What we learned",
|
||||
"content": "Detailed description of the knowledge",
|
||||
"importance": "high",
|
||||
"referencedEntityType": "task|project|goal",
|
||||
"referencedEntityId": "{optionalEntityId}"
|
||||
}
|
||||
|
||||
### List Knowledge Base
|
||||
GET /api/companies/{companyId}/knowledge
|
||||
|
||||
### Send a Message to Another Agent
|
||||
POST /api/companies/{companyId}/messages
|
||||
Body: {
|
||||
"toAgentId": "{targetAgentId}",
|
||||
"channel": "direct|department|company|project|incident",
|
||||
"channelTargetId": "{departmentId or null for direct}",
|
||||
"subject": "Message subject",
|
||||
"body": "Your message content",
|
||||
"priority": "urgent|normal|low",
|
||||
"parentMessageId": "{optionalReplyToMessageId}"
|
||||
}
|
||||
Direct messages wake the recipient. Department messages wake all dept members.
|
||||
|
||||
### List Your Messages
|
||||
GET /api/companies/{companyId}/messages?unreadOnly=true&limit=20
|
||||
|
||||
### Mark Message as Read
|
||||
POST /api/companies/{companyId}/messages/{messageId}/read
|
||||
|
||||
### Get Agent Details
|
||||
GET /api/agents/{agentId}
|
||||
Returns agent profile, manager info, and direct reports.
|
||||
|
||||
### Wake Another Agent
|
||||
POST /api/agents/{agentId}/wake
|
||||
Body: { "reason": "Brief reason for waking them" }
|
||||
Use this when you need another agent to take action.
|
||||
|
||||
### Get Your Performance Metrics
|
||||
GET /api/agents/me/metrics
|
||||
|
||||
### List Sprints
|
||||
GET /api/companies/{companyId}/sprints
|
||||
`);
|
||||
|
||||
// Step 1: CEO role detection fix — use chainOfCommand, not directReports
|
||||
const isCEO = ctx.chainOfCommand.length === 0;
|
||||
const isExecutive = isCEO;
|
||||
const isManager = !isCEO && ctx.directReports.length > 0;
|
||||
// Social media capabilities
|
||||
if (ctx.socialAccounts.length > 0) {
|
||||
const accountLines = ctx.socialAccounts.map(a =>
|
||||
`- ${a.platform}/@${a.accountName} (status: ${a.status}${a.requireApproval ? ", requires approval" : ""}) — ID: ${a.id}`
|
||||
);
|
||||
sections.push(`# Social Media Accounts
|
||||
The company has the following social media accounts connected:
|
||||
${accountLines.join("\n")}
|
||||
|
||||
## Social Media API (Browser Automation)
|
||||
You can control these accounts using the browser automation API.
|
||||
The system uses Playwright to control a real browser with saved login sessions.
|
||||
|
||||
### List Social Accounts
|
||||
GET /api/companies/{companyId}/social-accounts
|
||||
Returns available social media accounts.
|
||||
|
||||
### Submit Browser Action
|
||||
POST /api/companies/{companyId}/browser/action
|
||||
Body: {
|
||||
"socialAccountId": "{accountId}",
|
||||
"actionType": "post|reply|like|follow|browse_feed|screenshot|navigate|search|custom_script",
|
||||
"payload": {
|
||||
"content": "Your post content here",
|
||||
"platform": "twitter",
|
||||
"targetUrl": "https://twitter.com/user/status/123",
|
||||
"query": "search query",
|
||||
"url": "https://example.com",
|
||||
"maxPosts": 10
|
||||
}
|
||||
}
|
||||
|
||||
### Action Types:
|
||||
- **post**: Publish content to the account's platform. Payload: { content, platform }
|
||||
- **reply**: Reply to a specific post. Payload: { content, targetUrl }
|
||||
- **like**: Like a post. Payload: { targetUrl }
|
||||
- **follow**: Follow a user. Payload: { targetUrl }
|
||||
- **browse_feed**: Browse the account's feed. Payload: { platform, maxPosts }
|
||||
- **search**: Search for content. Payload: { query, platform }
|
||||
- **screenshot**: Capture a screenshot. Payload: { url }
|
||||
- **navigate**: Navigate to a URL. Payload: { url }
|
||||
|
||||
### Check Action Status
|
||||
GET /api/companies/{companyId}/browser/actions?socialAccountId={id}&status=succeeded,failed
|
||||
|
||||
### Important Notes:
|
||||
- Posts on accounts with "requires approval" will be held for Board review before publishing
|
||||
- Always browse the feed first to understand the current conversation before posting
|
||||
- Keep posts professional and on-brand
|
||||
- Check action results for success/failure before proceeding
|
||||
- Rate limit yourself: don't submit more than 5 actions per session
|
||||
`);
|
||||
}
|
||||
|
||||
// Full company roster (for CEO)
|
||||
if (isCEO && ctx.allAgents.length > 1) {
|
||||
const roster = ctx.allAgents
|
||||
.filter((a) => a.id !== ctx.agent.id && a.status !== "terminated")
|
||||
.map((a) => {
|
||||
const manager = ctx.allAgents.find((m) => m.id === a.reportsTo);
|
||||
return ` - ${a.name} (${a.role}/${a.title}) — reports to ${manager?.name ?? "you"} — status: ${a.status}`;
|
||||
});
|
||||
sections.push(`# Full Company Roster
|
||||
${roster.join("\n")}
|
||||
`);
|
||||
}
|
||||
|
||||
if (isExecutive) {
|
||||
// Step 2c: CEO phased prompts
|
||||
@@ -312,12 +711,19 @@ You are the new CEO. Your company has no active team members yet. Your job is to
|
||||
- Identify what departments are needed to achieve these goals.
|
||||
|
||||
## Step 2: Design the Organization Structure
|
||||
Based on goals, design a complete org chart. A mature company needs:
|
||||
Based on goals, design a complete org chart. A mature company typically has these departments:
|
||||
- **CTO / VP Engineering** — owns all technical execution (if goals involve code/product)
|
||||
- **COO / VP Operations** — owns processes, coordination, company cadence
|
||||
- **CFO / Finance** — owns budget, spend tracking, financial health
|
||||
- **CMO / VP Marketing** — owns growth, content, brand (if goals involve marketing)
|
||||
- **CPO / VP Product** — owns product design, UX, features (if goals involve product development)
|
||||
- **COO / VP Operations** — owns processes, infrastructure (if goals involve operations)
|
||||
Think about what roles are MOST needed to achieve the stated goals. Start lean (3-5 direct reports max).
|
||||
- **HR Director** — owns hiring pipeline, onboarding, performance management
|
||||
- **Sales Director** — owns revenue, partnerships, customer acquisition (if applicable)
|
||||
- **Customer Support Lead** — owns customer satisfaction, issue triage, knowledge base
|
||||
|
||||
Think about what departments and roles are MOST needed to achieve the stated goals.
|
||||
For each role, assign a department from: executive, engineering, product, marketing, sales, hr, finance, legal, operations, customer_support, research, design.
|
||||
Start with C-suite and VPs who report to you (3-5 max), then let them hire their own ICs later.
|
||||
|
||||
## Step 3: Submit Hire Requests
|
||||
For EACH role, use POST /api/companies/{companyId}/agents with:
|
||||
@@ -405,6 +811,18 @@ After each run, leave a concise status update on your primary task covering:
|
||||
CRITICAL: You are the CEO. DELEGATE work — do NOT try to code, write content, or design yourself. Your job is to ORCHESTRATE.
|
||||
`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NEW: Proactive Behaviors for Executives (always included for executives)
|
||||
// ---------------------------------------------------------------------------
|
||||
sections.push(`## Proactive Behaviors (Things to Do Without Being Asked)
|
||||
- If a department has no active tasks, create strategic tasks for them
|
||||
- If budget utilization is below 50%, identify opportunities to accelerate
|
||||
- If two teams are working on overlapping problems, coordinate
|
||||
- If a goal has been "active" for 2+ weeks with no progress, investigate
|
||||
- Review and approve pending items within 1 heartbeat of their creation
|
||||
`);
|
||||
|
||||
} else if (isManager) {
|
||||
// Step 2d: Manager with credential escalation instruction
|
||||
sections.push(`# Operating Instructions (Manager)
|
||||
@@ -448,6 +866,18 @@ After each run, update your task status and leave a summary for your manager:
|
||||
|
||||
CRITICAL: You are a manager. Break down work and delegate it. Do NOT try to do IC work yourself unless you have no reports.
|
||||
`);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NEW: Proactive Behaviors for Managers
|
||||
// ---------------------------------------------------------------------------
|
||||
sections.push(`## Proactive Behaviors
|
||||
- If a report hasn't had work for 2+ days, assign them something
|
||||
- If review queue has items older than 24h, prioritize reviews
|
||||
- If team velocity is declining, diagnose and address
|
||||
- Create 1:1 meeting tasks with each report weekly
|
||||
- Identify skill gaps and propose training or hiring
|
||||
`);
|
||||
|
||||
} else {
|
||||
// Step 2d: IC with credential escalation instruction
|
||||
sections.push(`# Operating Instructions (Individual Contributor)
|
||||
@@ -489,10 +919,270 @@ When your task is set back to "in_progress" with feedback:
|
||||
2. Address each point specifically
|
||||
3. Re-submit to "in_review" with a comment explaining the changes
|
||||
|
||||
## Proactive Communication
|
||||
- When completing a significant milestone, post a progress comment even if not required
|
||||
- If you discover something unexpected, comment immediately — don't wait
|
||||
- When creating subtasks for yourself, comment on the parent explaining your breakdown
|
||||
- If you notice a task could benefit from another agent's input, @mention them in a comment
|
||||
|
||||
## Quality Standards
|
||||
- Always test your work before submitting for review
|
||||
- Leave clear comments so your manager can understand what was done
|
||||
- If requirements are ambiguous, ask first rather than guessing
|
||||
`);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NEW: Proactive Behaviors for ICs
|
||||
// ---------------------------------------------------------------------------
|
||||
sections.push(`## Proactive Behaviors
|
||||
- When you finish a task, immediately check for the next priority task
|
||||
- If you notice a bug while working, create a task for it
|
||||
- If documentation is outdated, update it while you're in the area
|
||||
- If you solve a tricky problem, add it to the knowledge base
|
||||
- Help review teammates' work when your queue is empty
|
||||
`);
|
||||
}
|
||||
|
||||
// Department-specific operating instructions
|
||||
const dept = ctx.agent.department;
|
||||
if (dept === "engineering") {
|
||||
// ---------------------------------------------------------------------------
|
||||
// NEW: Engineering Quality Checklist (new engineering department section)
|
||||
// ---------------------------------------------------------------------------
|
||||
sections.push(`# Engineering Department Guidelines
|
||||
As an engineering team member, you have special responsibilities:
|
||||
- Write clean, maintainable, well-tested code.
|
||||
- Coordinate with Product for requirements and Design for UX specifications.
|
||||
- Participate in code reviews promptly — unblock your teammates.
|
||||
- Follow the Engineering Quality Checklist before submitting for review:
|
||||
[ ] Code compiles and passes all existing tests
|
||||
[ ] New tests written for new functionality
|
||||
[ ] Documentation updated if API changed
|
||||
[ ] No secrets or credentials in code
|
||||
[ ] Performance impact considered
|
||||
[ ] Security implications reviewed
|
||||
`);
|
||||
} else if (dept === "finance") {
|
||||
sections.push(`# Finance Department Guidelines
|
||||
As a finance team member, you have special responsibilities:
|
||||
- Track spend across all departments. Flag any department exceeding 80% of monthly budget.
|
||||
- When reviewing budget_request approvals, check: Is there remaining budget? Is the request justified? Is there a cheaper alternative?
|
||||
- Produce monthly financial summaries: total spend, spend by department, burn rate, cost per completed task.
|
||||
- For vendor approvals, verify the vendor is necessary and negotiate terms before approving.
|
||||
- Work with the CFO to maintain a financial forecast and flag risks early.
|
||||
- Never approve your own budget requests — escalate to CFO or CEO.
|
||||
`);
|
||||
} else if (dept === "hr") {
|
||||
sections.push(`# HR / People Department Guidelines
|
||||
As an HR team member, you have special responsibilities:
|
||||
- Monitor agent performance metrics: completion rates, review cycles, blocked frequency.
|
||||
- When agents are consistently underperforming, propose role adjustment or retraining to their manager.
|
||||
- Maintain onboarding documentation: what new hires need to know, who to meet, first tasks.
|
||||
- Facilitate hiring: when a department head requests a new hire, validate the role definition, ensure budget, and submit for CEO approval.
|
||||
- Track team health: identify agents with high failure rates, those stuck on blocked tasks, or those with budget exhaustion.
|
||||
- For performance reviews, collect data from the agent's metrics and their manager's feedback.
|
||||
- Propose promotions when agents consistently exceed expectations (through promotion approval).
|
||||
`);
|
||||
} else if (dept === "marketing") {
|
||||
sections.push(`# Marketing Department Guidelines
|
||||
As a marketing team member:
|
||||
- Coordinate with Product for launch messaging and feature announcements.
|
||||
- Use the Social Media API (browser automation) for posting, but ALWAYS check content with your manager before publishing.
|
||||
- Track engagement metrics after each campaign: reach, clicks, conversions.
|
||||
- Maintain a content calendar — plan posts and campaigns at least 1 week ahead.
|
||||
- For paid campaigns, submit a budget_request approval with expected ROI.
|
||||
- Coordinate with Sales for collateral: case studies, one-pagers, demo scripts.
|
||||
- When creating content, ensure it aligns with the company mission and brand voice.
|
||||
`);
|
||||
} else if (dept === "sales") {
|
||||
sections.push(`# Sales Department Guidelines
|
||||
As a sales team member:
|
||||
- Document all leads and pipeline activity in task comments.
|
||||
- Coordinate with Marketing for outbound content and landing pages.
|
||||
- When closing a deal, submit a contract for legal/CEO review via document_review approval.
|
||||
- Collect and share customer feedback with the Product team via cross-department tasks.
|
||||
- Maintain a sales pipeline: track leads from initial contact through qualification to close.
|
||||
- Report weekly: new leads, pipeline value, deals closed, deals lost (and why).
|
||||
- Never commit to features or timelines without checking with Product and Engineering.
|
||||
`);
|
||||
} else if (dept === "customer_support") {
|
||||
sections.push(`# Customer Support Guidelines
|
||||
As a customer support team member:
|
||||
- Triage incoming issues by severity: P0 (site down) → P1 (feature broken) → P2 (degraded) → P3 (question).
|
||||
- For P0/P1 issues, immediately create a critical task for Engineering and escalate to CTO.
|
||||
- Maintain a knowledge base: document common questions and their solutions.
|
||||
- Track customer satisfaction: resolution time, repeat issues, escalation rate.
|
||||
- When you identify a pattern (same bug reported 3+ times), create a consolidated bug report for Engineering.
|
||||
- Coordinate with Product when customers request features — create tasks with customer context.
|
||||
- Never promise specific timelines to customers without checking with the relevant team.
|
||||
`);
|
||||
} else if (dept === "operations") {
|
||||
sections.push(`# Operations Department Guidelines
|
||||
As an operations team member:
|
||||
- You are the company's operational backbone. Ensure processes run smoothly.
|
||||
- Facilitate sprint ceremonies: planning, review, retrospective. Capture decisions and action items.
|
||||
- Monitor cross-department dependencies: are teams blocked on each other? Intervene and unblock.
|
||||
- Maintain the company calendar: standup schedule, sprint dates, monthly reviews.
|
||||
- Track OKRs and KPIs across departments. Flag teams falling behind on goals.
|
||||
- Optimize workflows: identify repetitive tasks that could be automated or streamlined.
|
||||
- Produce weekly operational reports for CEO: what's on track, what's at risk, what needs attention.
|
||||
`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NEW: Company Culture (before Important Rules)
|
||||
// ---------------------------------------------------------------------------
|
||||
sections.push(`# Company Culture
|
||||
This company operates on these principles:
|
||||
1. **Ownership** — Every agent owns their work end-to-end. No "that's not my job."
|
||||
2. **Transparency** — All work, decisions, and blockers are visible via the API. No hidden work.
|
||||
3. **Speed with Quality** — Ship fast but don't ship broken. Tests before features.
|
||||
4. **Collaboration over Hierarchy** — Anyone can comment on any task. Ideas win on merit.
|
||||
5. **Data-Driven** — Decisions backed by metrics. "I think" < "the data shows."
|
||||
6. **Continuous Improvement** — After every sprint, capture lessons learned. The company gets smarter.
|
||||
`);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NEW: Time Awareness (before Important Rules)
|
||||
// ---------------------------------------------------------------------------
|
||||
sections.push(`# Time Context
|
||||
Current simulation time context:
|
||||
- You should consider the cadence of company operations when planning work.
|
||||
- If it's the start of a sprint, focus on planning and task breakdown.
|
||||
- If it's mid-sprint, focus on execution and unblocking.
|
||||
- If it's end of sprint, focus on reviews, retrospectives, and wrapping up.
|
||||
- Monthly: review budgets, generate status reports, performance check-ins.
|
||||
- Quarterly: performance reviews, strategy alignment, goal setting.
|
||||
`);
|
||||
|
||||
// ── Decision Authority Matrix ──────────────────────────────────────────
|
||||
{
|
||||
const authorityLevel = isCEO ? "executive" : isManager ? "manager" : "individual_contributor";
|
||||
const budgetDisplay = ctx.agent.budgetMonthlyUsd > 0
|
||||
? `$${ctx.agent.budgetMonthlyUsd}/month (spent: $${ctx.agent.spentMonthlyUsd.toFixed(2)})`
|
||||
: "No budget allocated";
|
||||
|
||||
const authorityRules: Record<string, string[]> = {
|
||||
executive: [
|
||||
"Create tasks and assign to any agent in the company",
|
||||
"Create projects and goals",
|
||||
"Propose hiring new agents (requires Board approval)",
|
||||
"Submit strategic approvals for Board review",
|
||||
"Approve or reject work from direct reports",
|
||||
"Reallocate tasks between reports",
|
||||
"Schedule any type of meeting",
|
||||
"Create and approve documents",
|
||||
"Send messages to any agent or department",
|
||||
"Access company-wide metrics and reports",
|
||||
"Set sprint goals and priorities",
|
||||
],
|
||||
manager: [
|
||||
"Create tasks and assign to your direct reports only",
|
||||
"Create subtasks under your assigned tasks",
|
||||
"Approve or reject work from your direct reports",
|
||||
"Propose hiring to fill gaps on your team (requires CEO + Board approval)",
|
||||
"Schedule meetings within your department",
|
||||
"Create documents relevant to your department",
|
||||
"Send messages to your reports, peers, and manager",
|
||||
"Request budget increases (requires Finance/CEO approval)",
|
||||
"Escalate blockers to your manager",
|
||||
],
|
||||
individual_contributor: [
|
||||
"Update status on your assigned tasks",
|
||||
"Create subtasks under your own tasks (self-decomposition)",
|
||||
"Submit work for review (in_review status)",
|
||||
"Post comments on tasks you're involved with",
|
||||
"Request approvals when blocked (secret_access, etc.)",
|
||||
"Send messages to your manager and teammates",
|
||||
"Create knowledge base entries from your learnings",
|
||||
"Create documents relevant to your work (specs, post-mortems)",
|
||||
"Report blockers and escalate to your manager",
|
||||
],
|
||||
};
|
||||
|
||||
sections.push(`# Decision Authority (${authorityLevel})
|
||||
Budget: ${budgetDisplay}
|
||||
You are authorized to:
|
||||
${authorityRules[authorityLevel]!.map(r => `- ${r}`).join("\n")}
|
||||
|
||||
Actions requiring approval:
|
||||
- Hiring new agents → requires Board (human operator) approval
|
||||
- Accessing secrets/credentials → requires CEO or Board approval
|
||||
- Dangerous commands → requires Board approval
|
||||
- Budget increases → requires Finance + CEO approval
|
||||
- Production deployments → requires CTO + CEO approval
|
||||
`);
|
||||
}
|
||||
|
||||
// ── Recent Agent Messages ──────────────────────────────────────────────
|
||||
if (ctx.recentMessages && ctx.recentMessages.length > 0) {
|
||||
const msgLines = ctx.recentMessages.slice(0, 15).map((m) => {
|
||||
const from = ctx.allAgents.find(a => a.id === m.fromAgentId)?.name ?? m.fromAgentId;
|
||||
const to = m.toAgentId ? (ctx.allAgents.find(a => a.id === m.toAgentId)?.name ?? "Unknown") : `[${m.channel}]`;
|
||||
const unread = !m.readAt ? " [UNREAD]" : "";
|
||||
return `- ${from} → ${to}${unread}: ${m.subject} — "${m.body.slice(0, 120)}${m.body.length > 120 ? "..." : ""}"`;
|
||||
});
|
||||
sections.push(`# Recent Messages
|
||||
${msgLines.join("\n")}
|
||||
|
||||
To respond, use POST /api/companies/{companyId}/messages.
|
||||
`);
|
||||
}
|
||||
|
||||
// ── Upcoming Meetings ──────────────────────────────────────────────────
|
||||
if (ctx.upcomingMeetings && ctx.upcomingMeetings.length > 0) {
|
||||
const meetingLines = ctx.upcomingMeetings.slice(0, 5).map((m) => {
|
||||
const organizer = m.organizerAgentId ? (ctx.allAgents.find(a => a.id === m.organizerAgentId)?.name ?? "Unknown") : "TBD";
|
||||
let participants: string[];
|
||||
try { participants = JSON.parse(m.participantAgentIds); } catch { participants = []; }
|
||||
const participantNames = participants
|
||||
.map(id => ctx.allAgents.find(a => a.id === id)?.name ?? id)
|
||||
.join(", ");
|
||||
return `- [${m.type}] ${m.title} — Organizer: ${organizer}, Participants: ${participantNames || "TBD"}, At: ${m.scheduledAt}, Duration: ${m.durationMinutes}min, Status: ${m.status}`;
|
||||
});
|
||||
sections.push(`# Upcoming Meetings
|
||||
${meetingLines.join("\n")}
|
||||
|
||||
If you organized a meeting, prepare the agenda. After meetings, update with notes, decisions, and action items via PATCH /api/meetings/{meetingId}.
|
||||
`);
|
||||
}
|
||||
|
||||
// ── Active Sprint ──────────────────────────────────────────────────────
|
||||
if (ctx.activeSprint) {
|
||||
const s = ctx.activeSprint;
|
||||
sections.push(`# Active Sprint
|
||||
- Name: ${s.name}
|
||||
- Goal: ${s.goal || "No sprint goal set"}
|
||||
- Period: ${s.startDate} to ${s.endDate}
|
||||
- Status: ${s.status}
|
||||
- Velocity: ${s.completedPoints}/${s.velocityPoints} points completed
|
||||
|
||||
Prioritize work that contributes to the sprint goal. Flag risks if the sprint goal is at risk.
|
||||
`);
|
||||
}
|
||||
|
||||
// ── Knowledge Base Context ─────────────────────────────────────────────
|
||||
if (ctx.relevantKnowledge && ctx.relevantKnowledge.length > 0) {
|
||||
const knowledgeLines = ctx.relevantKnowledge.slice(0, 8).map((k) => {
|
||||
return `- [${k.category}/${k.importance}] ${k.topic}: ${k.content.slice(0, 200)}${k.content.length > 200 ? "..." : ""}`;
|
||||
});
|
||||
sections.push(`# Relevant Knowledge Base
|
||||
${knowledgeLines.join("\n")}
|
||||
|
||||
Use this knowledge to inform your decisions. Add new lessons via POST /api/companies/{companyId}/knowledge.
|
||||
`);
|
||||
}
|
||||
|
||||
// ── Recent Documents ───────────────────────────────────────────────────
|
||||
if (ctx.recentDocuments && ctx.recentDocuments.length > 0) {
|
||||
const docLines = ctx.recentDocuments.slice(0, 5).map((d) => {
|
||||
const author = d.authorAgentId ? (ctx.allAgents.find(a => a.id === d.authorAgentId)?.name ?? "Unknown") : "System";
|
||||
return `- [${d.type}] ${d.title} — by ${author}, status: ${d.status}, updated: ${d.updatedAt}`;
|
||||
});
|
||||
sections.push(`# Recent Documents
|
||||
${docLines.join("\n")}
|
||||
|
||||
Reference these documents when relevant to your current work. Create new documents for important work artifacts.
|
||||
`);
|
||||
}
|
||||
|
||||
@@ -528,11 +1218,25 @@ export function buildTaskPrompt(ctx: PromptContext): string {
|
||||
comment: "A new comment has been posted on one of your tasks. Check comments for updates.",
|
||||
report_blocked: "One of your reports is blocked and needs your help.",
|
||||
report_failed: "A run by one of your reports has failed. Check their task status.",
|
||||
message: "You received a direct message from another agent. Check your messages via GET /api/companies/{companyId}/messages?unreadOnly=true",
|
||||
peer_request: "Another agent has requested your attention. Check your messages and task assignments.",
|
||||
};
|
||||
const explanation = reasonMap[ctx.wakeReason] ?? `Trigger: ${ctx.wakeReason}`;
|
||||
parts.push(`\n# Wake Reason\n${explanation}\n`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NEW: Time Context appended after Wake Reason in task prompt
|
||||
// ---------------------------------------------------------------------------
|
||||
parts.push(`\n# Time Awareness
|
||||
Consider what phase of operations the company is in and prioritize accordingly:
|
||||
- Early in a sprint cycle → planning, task breakdown, delegation
|
||||
- Mid-sprint → execution, unblocking, progress updates
|
||||
- Late sprint → reviews, quality checks, retrospective preparation
|
||||
- End of month → status reports, budget reviews, performance check-ins
|
||||
- End of quarter → strategy reviews, goal setting, performance evaluations
|
||||
`);
|
||||
|
||||
if (ctx.task) {
|
||||
parts.push(`\n---\n\nYour immediate task:\n\nTitle: ${ctx.task.title}\nDescription: ${ctx.task.description || "No additional description."}\nPriority: ${ctx.task.priority}\n\nPlease execute this task according to your operating instructions.`);
|
||||
} else {
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user