This commit is contained in:
ZacharyZhang-NY committed 2026-03-16 13:07:14 -04:00
1 parent 41eafc7c44
commit 54bcc77ef7
19 files changed
+11420 -64

No files matched your search

+428
View File
@@ -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}` });
});
}
}
+485
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+544 -4
View File
@@ -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
View File
@@ -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