Brief agents on their connectors, skills, and the CEO's grant authority; CEO predicate follows role_level
This commit is contained in:
1 parent
3e4c7c81a2
commit
a95859a9f7
3 files changed
+171
-4
No files matched your search
@@ -24,6 +24,8 @@ import {
|
||||
type WorkspaceRow,
|
||||
} from "./task-prompts";
|
||||
import { loadComposioTools } from "./composio/composio-tool-bridge";
|
||||
import { connectorDisplayName } from "./composio/connector-brands";
|
||||
import { ConnectorGrantsService } from "./composio/connector-grants";
|
||||
|
||||
const TASK_RUN_STATE = {
|
||||
QUEUED: "queued",
|
||||
@@ -122,8 +124,13 @@ export class TaskExecutionService {
|
||||
};
|
||||
const chatContext = this.loadChatContext(orgId, isolation);
|
||||
const inboxContext = chatContext ? null : this.loadInboxContext(orgId, agent.id);
|
||||
const isCeo = !agent.manager_agent_id;
|
||||
// The CEO is the c_suite role — the same predicate the grant system
|
||||
// enforces — not "has no manager", which any board hire can match.
|
||||
const isCeo = agent.role_level === "c_suite";
|
||||
const hasReports = this.agentHasReports(orgId, agent.id);
|
||||
const connectorNames = new ConnectorGrantsService(this.db)
|
||||
.toolkitsForAgent(orgId, agent.id)
|
||||
.map(connectorDisplayName);
|
||||
const externalTools = await loadComposioTools(this.db, orgId, agent.id);
|
||||
const result = await runAgentLoop(
|
||||
{
|
||||
@@ -135,7 +142,7 @@ export class TaskExecutionService {
|
||||
orgId,
|
||||
providerId: provider.id,
|
||||
workspaceId: task.workspace_id,
|
||||
instructions: buildAgentInstructions({ agent, companyName: this.orgName(orgId), isCeo, hasReports, externalToolCount: externalTools.length }),
|
||||
instructions: buildAgentInstructions({ agent, companyName: this.orgName(orgId), isCeo, hasReports, connectorNames }),
|
||||
initialUserTurn: buildTaskPrompt({
|
||||
task,
|
||||
agent,
|
||||
|
||||
@@ -76,7 +76,8 @@ export interface AgentInstructionContext {
|
||||
companyName: string;
|
||||
isCeo: boolean;
|
||||
hasReports: boolean;
|
||||
externalToolCount: number;
|
||||
/** Display names of the connected toolkits this agent carries in this run. */
|
||||
connectorNames: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,11 +92,43 @@ export function buildAgentInstructions(input: AgentInstructionContext): string {
|
||||
`You are ${agent.name}, ${agent.title || agent.role} in ${agent.department} at ${input.companyName}. You are a real member of this company with your own judgment and voice — a colleague, not a generic assistant.`,
|
||||
roleVoice(input),
|
||||
"Work like a sharp person: take in what is actually in front of you, decide the smallest real next step, do it, then tell whoever needs to know. Do not narrate busywork.",
|
||||
`Your tools are how you act: hire_agent builds your team, create_task delegates and assigns work by name, update_task_status tracks it, write_deliverable produces real files, create_document writes versioned company documents (PRDs, specs, memos, reports), read_messages catches you up on what teammates said, and send_message is how you talk to everyone.${input.externalToolCount > 0 ? ` You also have ${input.externalToolCount} external tools (composio_* prefixed) for interacting with services like GitHub, Slack, Gmail, etc. — use them when your work involves those platforms.` : ""}`,
|
||||
`Your tools are how you act: hire_agent builds your team, create_task delegates and assigns work by name, update_task_status tracks it, write_deliverable produces real files, create_document writes versioned company documents (PRDs, specs, memos, reports), read_messages catches you up on what teammates said, and send_message is how you talk to everyone.`,
|
||||
capabilityBriefing(input),
|
||||
"Talk like a person on a team, not a status bot: address teammates by @name, say what you genuinely think, ask when you are unsure, and reply when someone asks you something. Before starting work, check your messages with read_messages to see if anyone needs you. Post where the work lives — 'engineering', 'product', or otherwise 'general' — and keep a back-and-forth in one thread by passing the thread_id a previous send_message returned. Hand work to a teammate with /handoff @Name and the file_id write_deliverable gives you.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* What this agent can reach beyond the built-in tools, and how access moves:
|
||||
* the CEO carries everything and hands out grants; everyone else works with
|
||||
* what they were granted and knows where to ask for more.
|
||||
*/
|
||||
function capabilityBriefing(input: AgentInstructionContext): string {
|
||||
const lines: string[] = [];
|
||||
if (input.connectorNames.length > 0) {
|
||||
lines.push(
|
||||
`Connected external services you carry in this run: ${input.connectorNames.join(", ")}. Their tools arrive with the composio_ prefix — use them directly whenever the work touches those platforms instead of describing what you would do.`,
|
||||
);
|
||||
}
|
||||
lines.push(
|
||||
"list_connectors shows every service the company has connected and who holds it; list_skills shows the company's skill library. Check them before planning work that depends on an external platform or a playbook.",
|
||||
);
|
||||
if (input.isCeo) {
|
||||
lines.push(
|
||||
"You are the CEO: you automatically carry every connected service and every company skill. Access for everyone else is yours to manage — when you delegate work that needs an external service or a skill, grant it to that teammate first with grant_connector or grant_skill (match the tool to the job), and take it back with revoke_connector or revoke_skill when the work is done. A teammate without the grant cannot use the tool, so grant before you assign.",
|
||||
);
|
||||
} else if (input.connectorNames.length === 0) {
|
||||
lines.push(
|
||||
"You currently hold no connector grants. If this work needs an external service, message the CEO to grant it to you (they run grant_connector) rather than working around it.",
|
||||
);
|
||||
} else {
|
||||
lines.push(
|
||||
"If the work needs a service you do not carry yet, message the CEO to grant it to you (they run grant_connector).",
|
||||
);
|
||||
}
|
||||
return lines.join(" ");
|
||||
}
|
||||
|
||||
function roleVoice(input: AgentInstructionContext): string {
|
||||
if (input.isCeo) {
|
||||
return "You run this company. Think in outcomes, make the call when it is yours to make, and put the right person on each piece instead of doing everything yourself — then tell the team the why, not just the what.";
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { DatabaseConnection } from "../../src/main/database/connection";
|
||||
import { OrganizationService } from "../../src/main/services/organization-service";
|
||||
import { ProviderService } from "../../src/main/services/provider-service";
|
||||
import { providerAdapters } from "../../src/main/services/provider-adapters";
|
||||
import type { AgentTurnResult, ProviderToolTurnInput } from "../../src/main/services/provider-tool-turn";
|
||||
import { ConnectorGrantsService } from "../../src/main/services/composio/connector-grants";
|
||||
import { TaskExecutionService } from "../../src/main/services/task-execution-service";
|
||||
import { RISK_LEVEL, TASK_PRIORITY, TASK_STATUS } from "../../src/shared/domain";
|
||||
import { buildAgentInstructions, type TaskExecutionAgentRow } from "../../src/main/services/task-prompts";
|
||||
|
||||
function agentRow(overrides: Partial<TaskExecutionAgentRow>): TaskExecutionAgentRow {
|
||||
return {
|
||||
id: "agt_1",
|
||||
name: "Ava",
|
||||
role: "Engineer",
|
||||
department: "Engineering",
|
||||
title: "",
|
||||
role_level: "staff",
|
||||
model_provider_id: "prv_1",
|
||||
model_id: "gpt-5.4",
|
||||
manager_agent_id: "agt_ceo",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("agent instructions", () => {
|
||||
test("the CEO is briefed on its grant authority over connectors and skills", () => {
|
||||
const text = buildAgentInstructions({
|
||||
agent: agentRow({ name: "CEO", role: "CEO", role_level: "c_suite", manager_agent_id: null }),
|
||||
companyName: "OrbitOps",
|
||||
isCeo: true,
|
||||
hasReports: false,
|
||||
connectorNames: ["GitHub", "Slack"],
|
||||
});
|
||||
expect(text).toContain("every connected service and every company skill");
|
||||
expect(text).toContain("grant_connector");
|
||||
expect(text).toContain("grant_skill");
|
||||
expect(text).toContain("grant before you assign");
|
||||
expect(text).toContain("GitHub, Slack");
|
||||
expect(text).toContain("You run this company");
|
||||
});
|
||||
|
||||
test("staff with grants are told exactly which services they carry", () => {
|
||||
const text = buildAgentInstructions({
|
||||
agent: agentRow({}),
|
||||
companyName: "OrbitOps",
|
||||
isCeo: false,
|
||||
hasReports: false,
|
||||
connectorNames: ["Stripe"],
|
||||
});
|
||||
expect(text).toContain("Connected external services you carry in this run: Stripe");
|
||||
expect(text).toContain("composio_ prefix");
|
||||
expect(text).toContain("list_connectors");
|
||||
expect(text).toContain("list_skills");
|
||||
expect(text).not.toContain("You run this company");
|
||||
});
|
||||
|
||||
test("staff without grants are pointed at the CEO for access", () => {
|
||||
const text = buildAgentInstructions({
|
||||
agent: agentRow({}),
|
||||
companyName: "OrbitOps",
|
||||
isCeo: false,
|
||||
hasReports: false,
|
||||
connectorNames: [],
|
||||
});
|
||||
expect(text).toContain("You currently hold no connector grants");
|
||||
expect(text).toContain("message the CEO");
|
||||
expect(text).not.toContain("grant before you assign");
|
||||
});
|
||||
});
|
||||
|
||||
describe("agent instruction wiring", () => {
|
||||
const testVault = {
|
||||
encryptSecret: (secret: string) => Buffer.from(secret, "utf8"),
|
||||
decryptSecret: (ciphertext: Buffer) => ciphertext.toString("utf8"),
|
||||
};
|
||||
|
||||
test("a manager-less staff hire is not the CEO, and carries its granted toolkit by name", async () => {
|
||||
const connection = new DatabaseConnection(join(tmpdir(), `ac-instr-${randomUUID()}.db`));
|
||||
const organization = new OrganizationService(connection.db);
|
||||
let capturedInstructions = "";
|
||||
const fakeToolTurn = async (input: ProviderToolTurnInput): Promise<AgentTurnResult> => {
|
||||
capturedInstructions = input.instructions ?? "";
|
||||
return { text: "done", toolCalls: [], rawResponseJson: JSON.stringify({ id: "resp-1" }) };
|
||||
};
|
||||
const providers = new ProviderService(connection.db, providerAdapters, testVault, undefined, fakeToolTurn);
|
||||
const execution = new TaskExecutionService(connection.db, providers);
|
||||
|
||||
let orgId = "";
|
||||
let agentId = "";
|
||||
let taskId = "";
|
||||
connection.transaction(() => {
|
||||
orgId = organization.createOrganization({
|
||||
organizationName: "OrbitOps", employeeName: "Yang", mission: "m", goal: "g",
|
||||
});
|
||||
const providerId = (connection.db.prepare("select id from providers where org_id = ? and provider_key = 'openai'").get(orgId) as { id: string }).id;
|
||||
providers.saveCredential({ orgId, providerId, secret: "sk-test", model: "gpt-5.4" });
|
||||
agentId = organization.createAgent({
|
||||
orgId, name: "Ada", role: "Engineer", department: "Engineering",
|
||||
modelProviderId: providerId, modelId: "gpt-5.4", budgetCents: 1000, managerAgentId: null,
|
||||
});
|
||||
const now = new Date().toISOString();
|
||||
connection.db.prepare(`
|
||||
insert into connector_accounts(id, org_id, toolkit_slug, toolkit_name, auth_config_id, status, created_at, updated_at)
|
||||
values ('ca_github', ?, 'github', 'GitHub', 'ac_github', 'ACTIVE', ?, ?)
|
||||
`).run(orgId, now, now);
|
||||
new ConnectorGrantsService(connection.db).grant({
|
||||
orgId, agentId, toolkitSlug: "github", grantedByType: "employee", grantedById: null,
|
||||
});
|
||||
taskId = organization.createTask({
|
||||
orgId, title: "Ship it", description: "d", status: TASK_STATUS.IN_PROGRESS,
|
||||
assigneeAgentId: agentId, risk: RISK_LEVEL.LOW, projectId: null, goalId: null,
|
||||
workspaceId: null, priority: TASK_PRIORITY.NORMAL, requiresFilesystem: false,
|
||||
});
|
||||
});
|
||||
|
||||
await execution.runTask({ orgId, taskId });
|
||||
|
||||
expect(capturedInstructions).not.toContain("You run this company");
|
||||
expect(capturedInstructions).toContain("Connected external services you carry in this run: GitHub");
|
||||
expect(capturedInstructions).toContain("list_connectors");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user