Add built-in agent provisioning foundation

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-07-09 18:48:27 +00:00
1 parent 4d50fa9f29
commit 4d5ad7408d
6 files changed
+799 -1

No files matched your search

@@ -0,0 +1,311 @@
import { randomUUID } from "node:crypto";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import { eq } from "drizzle-orm";
import {
activityLog,
agentConfigRevisions,
agents,
companies,
createDb,
} from "@paperclipai/db";
import {
getEmbeddedPostgresTestSupport,
startEmbeddedPostgresTestDatabase,
} from "./helpers/embedded-postgres.js";
import { HttpError } from "../errors.ts";
import { agentService } from "../services/agents.ts";
import {
builtInAgentService,
deriveBuiltInAgentStatus,
listBuiltInAgentDefinitions,
reconcileBuiltInAgentsOnStartup,
validateBuiltInAgentDefinitions,
} from "../services/built-in-agents.ts";
import { readBuiltInAgentMarker, withBuiltInAgentMarker } from "../services/built-in-agent-metadata.ts";
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
function issuePrefix(id: string) {
return `T${id.replace(/-/g, "").slice(0, 6).toUpperCase()}`;
}
if (!embeddedPostgresSupport.supported) {
console.warn(
`Skipping embedded Postgres built-in agent tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`,
);
}
describeEmbeddedPostgres("built-in agents", () => {
let db!: ReturnType<typeof createDb>;
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
beforeAll(async () => {
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-built-in-agents-");
db = createDb(tempDb.connectionString);
}, 20_000);
afterEach(async () => {
await db.delete(agentConfigRevisions);
await db.delete(activityLog);
await db.delete(agents);
await db.delete(companies);
});
afterAll(async () => {
await tempDb?.cleanup();
});
async function seedCompany() {
const companyId = randomUUID();
await db.insert(companies).values({
id: companyId,
name: "Paperclip",
issuePrefix: issuePrefix(companyId),
requireBoardApprovalForNewAgents: true,
});
return companyId;
}
it("validates the static registry and rejects invalid definitions", () => {
expect(listBuiltInAgentDefinitions().map((definition) => definition.key).sort()).toEqual(["briefs", "learning"]);
expect(() => validateBuiltInAgentDefinitions([
{
key: "briefs",
displayName: "Briefs Agent",
featureKeys: ["briefs"],
shortPurpose: "One",
defaultInstructions: "Do work",
defaultRole: "general",
},
{
key: "briefs",
displayName: "Duplicate",
featureKeys: ["duplicate"],
shortPurpose: "Two",
defaultInstructions: "Do work",
defaultRole: "general",
},
])).toThrow("Duplicate built-in agent key");
expect(() => validateBuiltInAgentDefinitions([
{
key: "Bad Key",
displayName: "Bad",
featureKeys: ["bad"],
shortPurpose: "Bad",
defaultInstructions: "Bad",
defaultRole: "general",
},
])).toThrow("Invalid built-in agent key");
});
it("lazily provisions one agent per company/key and updates the same row on setup", async () => {
const companyId = await seedCompany();
const svc = builtInAgentService(db);
const created = await svc.ensure(companyId, "briefs");
expect(created.status).toBe("needs_setup");
expect(created.agentId).toBeTruthy();
expect(created.agent).toMatchObject({
companyId,
name: "Briefs Agent",
adapterConfig: {},
status: "idle",
});
expect(readBuiltInAgentMarker(created.agent?.metadata)).toEqual({
key: "briefs",
featureKeys: ["briefs"],
});
const configured = await svc.ensure(companyId, "briefs", {
adapterType: "codex_local",
adapterConfig: { model: "gpt-5.4" },
});
expect(configured.status).toBe("ready");
expect(configured.agentId).toBe(created.agentId);
expect(configured.agent).toMatchObject({
adapterType: "codex_local",
adapterConfig: { model: "gpt-5.4" },
});
const rows = await db.select().from(agents).where(eq(agents.companyId, companyId));
expect(rows).toHaveLength(1);
});
it("recovers an orphaned marked row instead of creating a duplicate", async () => {
const companyId = await seedCompany();
const orphanId = randomUUID();
await db.insert(agents).values({
id: orphanId,
companyId,
name: "Old Briefs",
role: "general",
status: "idle",
adapterType: "codex_local",
adapterConfig: { model: "gpt-5.4" },
runtimeConfig: {},
permissions: {},
metadata: withBuiltInAgentMarker({ source: "orphan" }, { key: "briefs", featureKeys: ["briefs"] }),
});
const state = await builtInAgentService(db).ensure(companyId, "briefs");
expect(state.status).toBe("ready");
expect(state.agentId).toBe(orphanId);
const rows = await db.select().from(agents).where(eq(agents.companyId, companyId));
expect(rows).toHaveLength(1);
});
it("derives not_provisioned, needs_setup, ready, and paused states", async () => {
const companyId = await seedCompany();
const builtIns = builtInAgentService(db);
await expect(builtIns.get(companyId, "learning")).resolves.toMatchObject({ status: "not_provisioned" });
const needsSetup = await builtIns.ensure(companyId, "learning");
expect(needsSetup.status).toBe("needs_setup");
expect(deriveBuiltInAgentStatus(needsSetup.agent)).toBe("needs_setup");
const ready = await builtIns.ensure(companyId, "learning", {
adapterType: "claude_local",
adapterConfig: { model: "claude-sonnet-4" },
});
expect(ready.status).toBe("ready");
await agentService(db).pause(ready.agentId!, "manual");
await expect(builtIns.get(companyId, "learning")).resolves.toMatchObject({
status: "paused",
agentId: ready.agentId,
pauseReason: "manual",
});
});
it("blocks deleting a built-in agent", async () => {
const companyId = await seedCompany();
const state = await builtInAgentService(db).ensure(companyId, "briefs");
await expect(agentService(db).remove(state.agentId!)).rejects.toMatchObject({
status: 409,
details: {
code: "built_in_agent_undeletable",
key: "briefs",
},
});
});
it("prevents direct marker add, remove, or mutation", async () => {
const companyId = await seedCompany();
const builtIn = await builtInAgentService(db).ensure(companyId, "briefs");
const normal = await agentService(db).create(companyId, {
name: "Normal",
role: "engineer",
status: "idle",
adapterType: "codex_local",
adapterConfig: { model: "gpt-5.4" },
runtimeConfig: {},
permissions: {},
});
await expect(agentService(db).create(companyId, {
name: "Spoof",
role: "engineer",
status: "idle",
adapterType: "codex_local",
adapterConfig: { model: "gpt-5.4" },
runtimeConfig: {},
permissions: {},
metadata: withBuiltInAgentMarker({}, { key: "briefs", featureKeys: ["briefs"] }),
})).rejects.toMatchObject({ status: 409, details: { code: "built_in_agent_marker_readonly" } });
await expect(agentService(db).update(normal.id, {
metadata: withBuiltInAgentMarker({}, { key: "briefs", featureKeys: ["briefs"] }),
})).rejects.toMatchObject({ status: 409, details: { code: "built_in_agent_marker_readonly" } });
await expect(agentService(db).update(builtIn.agentId!, {
metadata: { other: "metadata" },
})).rejects.toMatchObject({ status: 409, details: { code: "built_in_agent_marker_readonly" } });
await expect(agentService(db).update(builtIn.agentId!, {
metadata: withBuiltInAgentMarker({}, { key: "learning", featureKeys: ["learning"] }),
})).rejects.toMatchObject({ status: 409, details: { code: "built_in_agent_marker_readonly" } });
await expect(agentService(db).update(builtIn.agentId!, {
metadata: withBuiltInAgentMarker({ note: "allowed" }, { key: "briefs", featureKeys: ["briefs"] }),
})).resolves.toMatchObject({
id: builtIn.agentId,
metadata: {
note: "allowed",
paperclipBuiltInAgent: { key: "briefs", featureKeys: ["briefs"] },
},
});
});
it("repairs display/default drift for marked rows during startup reconciliation", async () => {
const companyId = await seedCompany();
const agentId = randomUUID();
await db.insert(agents).values({
id: agentId,
companyId,
name: "Old Name",
role: "engineer",
title: "Old title",
capabilities: "Old purpose",
status: "idle",
adapterType: "codex_local",
adapterConfig: { model: "gpt-5.4" },
runtimeConfig: {},
permissions: {},
metadata: withBuiltInAgentMarker({}, { key: "briefs", featureKeys: ["old-briefs"] }),
});
const result = await reconcileBuiltInAgentsOnStartup(db);
expect(result).toMatchObject({ scanned: 1, reconciled: 1, unknown: 0, duplicates: 0 });
const [row] = await db.select().from(agents).where(eq(agents.id, agentId));
expect(row).toMatchObject({
name: "Briefs Agent",
role: "general",
title: null,
capabilities: "Prepares concise operational briefs for the board and agent company.",
});
expect(readBuiltInAgentMarker(row?.metadata)).toEqual({ key: "briefs", featureKeys: ["briefs"] });
});
it("reports duplicate active instances for a company/key", async () => {
const companyId = await seedCompany();
await db.insert(agents).values([
{
id: randomUUID(),
companyId,
name: "Briefs One",
role: "general",
status: "idle",
adapterType: "codex_local",
adapterConfig: { model: "gpt-5.4" },
runtimeConfig: {},
permissions: {},
metadata: withBuiltInAgentMarker({}, { key: "briefs", featureKeys: ["briefs"] }),
},
{
id: randomUUID(),
companyId,
name: "Briefs Two",
role: "general",
status: "idle",
adapterType: "codex_local",
adapterConfig: { model: "gpt-5.4" },
runtimeConfig: {},
permissions: {},
metadata: withBuiltInAgentMarker({}, { key: "briefs", featureKeys: ["briefs"] }),
},
]);
await expect(builtInAgentService(db).ensure(companyId, "briefs")).rejects.toMatchObject({
status: 409,
details: {
code: "built_in_agent_duplicate_instance",
key: "briefs",
},
} satisfies Partial<HttpError>);
});
});
+14
View File
@@ -42,6 +42,7 @@ import {
environmentCustomImageService,
heartbeatService,
instanceSettingsService,
reconcileBuiltInAgentsOnStartup,
reconcileCloudUpstreamRunsOnStartup,
reconcileCodexLocalManagedHomesOnStartup,
reconcilePersistedRuntimeServicesOnStartup,
@@ -779,6 +780,19 @@ export async function startServer(): Promise<StartedServer> {
logger.error({ err }, "startup reconciliation of codex_local managed homes failed");
});
void reconcileBuiltInAgentsOnStartup(db as any)
.then((result) => {
if (result.reconciled > 0 || result.unknown > 0 || result.duplicates > 0) {
logger.warn(
result,
"startup reconciliation of built-in agents complete",
);
}
})
.catch((err) => {
logger.error({ err }, "startup reconciliation of built-in agents failed");
});
// Force the instance onto the Kubernetes sandbox provider when configured via
// env (PAPERCLIP_EXECUTION_MODE=kubernetes). Runs BEFORE the heartbeat resumes
// queued runs so the policy + managed k8s environments are in place. A bad
+38 -1
View File
@@ -30,6 +30,10 @@ import { syncAgentAdapterEnvBindings } from "./agent-secret-bindings.js";
import { normalizeAgentPermissions } from "./agent-permissions.js";
import { REDACTED_EVENT_VALUE, sanitizeRecord } from "../redaction.js";
import { secretService } from "./secrets.js";
import {
builtInAgentMarkersEqual,
readBuiltInAgentMarker,
} from "./built-in-agent-metadata.js";
function hashToken(token: string) {
return createHash("sha256").update(token).digest("hex");
@@ -65,6 +69,11 @@ interface RevisionMetadata {
interface UpdateAgentOptions {
recordRevision?: RevisionMetadata;
allowBuiltInAgentMetadata?: boolean;
}
interface CreateAgentOptions {
allowBuiltInAgentMetadata?: boolean;
}
interface AgentShortnameRow {
@@ -390,6 +399,21 @@ export function agentService(db: Db) {
});
}
function assertBuiltInAgentMetadataMutationAllowed(
beforeMetadata: unknown,
afterMetadata: unknown,
options?: { allowBuiltInAgentMetadata?: boolean },
) {
if (options?.allowBuiltInAgentMetadata) return;
const beforeMarker = readBuiltInAgentMarker(beforeMetadata);
const afterMarker = readBuiltInAgentMarker(afterMetadata);
if (builtInAgentMarkersEqual(beforeMarker, afterMarker)) return;
throw conflict("Built-in agent marker is managed by Paperclip and cannot be edited directly", {
code: "built_in_agent_marker_readonly",
key: beforeMarker?.key ?? afterMarker?.key ?? null,
});
}
async function updateAgent(
id: string,
data: Partial<typeof agents.$inferInsert>,
@@ -425,6 +449,10 @@ export function agentService(db: Db) {
}
}
if (Object.prototype.hasOwnProperty.call(data, "metadata")) {
assertBuiltInAgentMetadataMutationAllowed(existing.metadata, data.metadata, options);
}
const normalizedPatch = { ...data } as Partial<typeof agents.$inferInsert>;
if (data.permissions !== undefined) {
const role = (data.role ?? existing.role) as string;
@@ -501,7 +529,8 @@ export function agentService(db: Db) {
getById,
create: async (companyId: string, data: Omit<typeof agents.$inferInsert, "companyId">) => {
create: async (companyId: string, data: Omit<typeof agents.$inferInsert, "companyId">, options?: CreateAgentOptions) => {
assertBuiltInAgentMetadataMutationAllowed(null, data.metadata, options);
if (data.reportsTo) {
await ensureManager(companyId, data.reportsTo);
}
@@ -645,6 +674,14 @@ export function agentService(db: Db) {
remove: async (id: string) => {
const existing = await getById(id);
if (!existing) return null;
const builtInMarker = readBuiltInAgentMarker(existing.metadata);
if (builtInMarker) {
throw conflict("Built-in agents cannot be deleted; pause them instead", {
code: "built_in_agent_undeletable",
key: builtInMarker.key,
featureKeys: builtInMarker.featureKeys,
});
}
return db.transaction(async (tx) => {
await tx.update(agents).set({ reportsTo: null }).where(eq(agents.reportsTo, id));
@@ -0,0 +1,45 @@
export const BUILT_IN_AGENT_METADATA_KEY = "paperclipBuiltInAgent";
export interface BuiltInAgentMarker {
key: string;
featureKeys: string[];
}
function isPlainRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function normalizeFeatureKeys(value: unknown): string[] | null {
if (!Array.isArray(value)) return null;
const featureKeys = value.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0);
return featureKeys.length === value.length ? featureKeys : null;
}
export function readBuiltInAgentMarker(metadata: unknown): BuiltInAgentMarker | null {
if (!isPlainRecord(metadata)) return null;
const marker = metadata[BUILT_IN_AGENT_METADATA_KEY];
if (!isPlainRecord(marker)) return null;
const key = marker.key;
const featureKeys = normalizeFeatureKeys(marker.featureKeys);
if (typeof key !== "string" || key.trim().length === 0 || !featureKeys) return null;
return { key, featureKeys };
}
export function withBuiltInAgentMarker(
metadata: Record<string, unknown> | null | undefined,
marker: BuiltInAgentMarker,
): Record<string, unknown> {
return {
...(metadata ?? {}),
[BUILT_IN_AGENT_METADATA_KEY]: {
key: marker.key,
featureKeys: [...marker.featureKeys],
},
};
}
export function builtInAgentMarkersEqual(left: BuiltInAgentMarker | null, right: BuiltInAgentMarker | null) {
if (!left && !right) return true;
if (!left || !right) return false;
return left.key === right.key && JSON.stringify(left.featureKeys) === JSON.stringify(right.featureKeys);
}
+380
View File
@@ -0,0 +1,380 @@
import { and, eq, ne } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import { agents, companies } from "@paperclipai/db";
import type { Agent } from "@paperclipai/shared";
import { conflict, notFound, unprocessable } from "../errors.js";
import { logActivity } from "./activity-log.js";
import { agentService } from "./agents.js";
import {
readBuiltInAgentMarker,
withBuiltInAgentMarker,
} from "./built-in-agent-metadata.js";
export type BuiltInAgentStatus = "not_provisioned" | "needs_setup" | "ready" | "paused";
export interface BuiltInAgentDefinition {
key: string;
displayName: string;
featureKeys: string[];
shortPurpose: string;
defaultInstructions: string;
defaultRole: string;
allowedAdapterTypes?: string[];
defaultBudgetMonthlyCents?: number;
}
export interface BuiltInAgentState {
definition: BuiltInAgentDefinition;
status: BuiltInAgentStatus;
agentId: string | null;
agent: Agent | null;
pauseReason: string | null;
}
export interface BuiltInAgentProvisionInput {
adapterType?: string;
adapterConfig?: Record<string, unknown>;
}
const BUILT_IN_AGENT_KEY_PATTERN = /^[a-z][a-z0-9_-]*$/;
const DEFINITIONS = validateBuiltInAgentDefinitions([
{
key: "briefs",
displayName: "Briefs Agent",
featureKeys: ["briefs"],
shortPurpose: "Prepares concise operational briefs for the board and agent company.",
defaultInstructions:
"You are Paperclip's built-in Briefs agent. Produce concise, sourced operational briefs that help the board understand current company work, risks, and next actions.",
defaultRole: "general",
allowedAdapterTypes: ["codex_local", "claude_local", "gemini_local", "opencode_local", "process"],
defaultBudgetMonthlyCents: 0,
},
{
key: "learning",
displayName: "Learning Agent",
featureKeys: ["learning"],
shortPurpose: "Maintains reusable company learning from completed work and recurring patterns.",
defaultInstructions:
"You are Paperclip's built-in Learning agent. Extract durable lessons from completed work, preserve useful patterns, and keep learning artifacts grounded in source context.",
defaultRole: "general",
allowedAdapterTypes: ["codex_local", "claude_local", "gemini_local", "opencode_local", "process"],
defaultBudgetMonthlyCents: 0,
},
]);
const DEFINITIONS_BY_KEY = new Map(DEFINITIONS.map((definition) => [definition.key, definition]));
function isPlainRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function nonEmptyString(value: unknown) {
return typeof value === "string" && value.trim().length > 0;
}
function uniqueNonEmptyStrings(values: string[]) {
const seen = new Set<string>();
const result: string[] = [];
for (const value of values) {
const normalized = value.trim();
if (!normalized || seen.has(normalized)) continue;
seen.add(normalized);
result.push(normalized);
}
return result;
}
export function validateBuiltInAgentDefinitions(definitions: BuiltInAgentDefinition[]) {
const seenKeys = new Set<string>();
for (const definition of definitions) {
if (!BUILT_IN_AGENT_KEY_PATTERN.test(definition.key)) {
throw new Error(`Invalid built-in agent key: ${definition.key}`);
}
if (seenKeys.has(definition.key)) {
throw new Error(`Duplicate built-in agent key: ${definition.key}`);
}
seenKeys.add(definition.key);
if (!definition.displayName.trim()) {
throw new Error(`Built-in agent ${definition.key} requires a displayName`);
}
if (!definition.shortPurpose.trim()) {
throw new Error(`Built-in agent ${definition.key} requires a shortPurpose`);
}
if (!definition.defaultInstructions.trim()) {
throw new Error(`Built-in agent ${definition.key} requires defaultInstructions`);
}
if (!definition.defaultRole.trim()) {
throw new Error(`Built-in agent ${definition.key} requires a defaultRole`);
}
if (uniqueNonEmptyStrings(definition.featureKeys).length !== definition.featureKeys.length) {
throw new Error(`Built-in agent ${definition.key} featureKeys must be unique non-empty strings`);
}
if (definition.featureKeys.length === 0) {
throw new Error(`Built-in agent ${definition.key} requires at least one featureKey`);
}
if (
definition.allowedAdapterTypes
&& uniqueNonEmptyStrings(definition.allowedAdapterTypes).length !== definition.allowedAdapterTypes.length
) {
throw new Error(`Built-in agent ${definition.key} allowedAdapterTypes must be unique non-empty strings`);
}
if (
definition.defaultBudgetMonthlyCents !== undefined
&& (!Number.isInteger(definition.defaultBudgetMonthlyCents) || definition.defaultBudgetMonthlyCents < 0)
) {
throw new Error(`Built-in agent ${definition.key} defaultBudgetMonthlyCents must be a non-negative integer`);
}
}
return definitions.map((definition) => ({
...definition,
featureKeys: [...definition.featureKeys],
allowedAdapterTypes: definition.allowedAdapterTypes ? [...definition.allowedAdapterTypes] : undefined,
}));
}
export function listBuiltInAgentDefinitions() {
return DEFINITIONS.map((definition) => ({ ...definition, featureKeys: [...definition.featureKeys] }));
}
export function getBuiltInAgentDefinition(key: string) {
return DEFINITIONS_BY_KEY.get(key) ?? null;
}
export function requireBuiltInAgentDefinition(key: string) {
const definition = getBuiltInAgentDefinition(key);
if (!definition) throw notFound(`Built-in agent definition not found: ${key}`);
return definition;
}
function defaultAdapterType(definition: BuiltInAgentDefinition) {
return definition.allowedAdapterTypes?.[0] ?? "process";
}
function assertAdapterAllowed(definition: BuiltInAgentDefinition, adapterType: string) {
if (definition.allowedAdapterTypes && !definition.allowedAdapterTypes.includes(adapterType)) {
throw unprocessable(`Adapter type ${adapterType} is not allowed for built-in agent ${definition.key}`, {
code: "built_in_agent_adapter_not_allowed",
key: definition.key,
allowedAdapterTypes: definition.allowedAdapterTypes,
});
}
}
function hasCompleteAdapterConfig(adapterType: string, adapterConfig: unknown) {
if (!isPlainRecord(adapterConfig)) return false;
if (["process", "command"].includes(adapterType)) {
return nonEmptyString(adapterConfig.command) || nonEmptyString(adapterConfig.script);
}
if (adapterType === "http") {
return nonEmptyString(adapterConfig.url) || nonEmptyString(adapterConfig.endpoint) || nonEmptyString(adapterConfig.webhookUrl);
}
if (adapterType === "openclaw_gateway" || adapterType === "hermes_gateway") {
return nonEmptyString(adapterConfig.baseUrl) || nonEmptyString(adapterConfig.url);
}
return nonEmptyString(adapterConfig.model);
}
export function deriveBuiltInAgentStatus(agent: Pick<Agent, "adapterType" | "adapterConfig" | "status" | "pausedAt"> | null): BuiltInAgentStatus {
if (!agent) return "not_provisioned";
if (agent.status === "paused" || agent.pausedAt) return "paused";
return hasCompleteAdapterConfig(agent.adapterType, agent.adapterConfig) ? "ready" : "needs_setup";
}
function builtInMetadata(definition: BuiltInAgentDefinition, existing?: Record<string, unknown> | null) {
return withBuiltInAgentMarker(existing, {
key: definition.key,
featureKeys: definition.featureKeys,
});
}
function definitionPatch(definition: BuiltInAgentDefinition, input: BuiltInAgentProvisionInput = {}) {
const adapterType = input.adapterType ?? defaultAdapterType(definition);
assertAdapterAllowed(definition, adapterType);
return {
name: definition.displayName,
role: definition.defaultRole,
title: null,
capabilities: definition.shortPurpose,
adapterType,
adapterConfig: input.adapterConfig ?? {},
budgetMonthlyCents: definition.defaultBudgetMonthlyCents ?? 0,
};
}
function rowIsBuiltInAgent(row: typeof agents.$inferSelect, key: string) {
const marker = readBuiltInAgentMarker(row.metadata);
return marker?.key === key;
}
export function builtInAgentService(db: Db) {
const agentSvc = agentService(db);
async function ensureCompany(companyId: string) {
const company = await db
.select({ id: companies.id })
.from(companies)
.where(eq(companies.id, companyId))
.then((rows) => rows[0] ?? null);
if (!company) throw notFound("Company not found");
}
async function findMarkedRows(companyId: string, key: string) {
const rows = await db
.select()
.from(agents)
.where(and(eq(agents.companyId, companyId), ne(agents.status, "terminated")));
return rows
.filter((row) => rowIsBuiltInAgent(row, key))
.sort((left, right) => left.createdAt.getTime() - right.createdAt.getTime() || left.id.localeCompare(right.id));
}
async function findSingleAgent(companyId: string, definition: BuiltInAgentDefinition) {
const markedRows = await findMarkedRows(companyId, definition.key);
if (markedRows.length > 1) {
throw conflict(`Multiple built-in agents found for ${definition.key}`, {
code: "built_in_agent_duplicate_instance",
key: definition.key,
agentIds: markedRows.map((row) => row.id),
});
}
if (markedRows.length === 0) return null;
const agent = await agentSvc.getById(markedRows[0]!.id);
return agent as Agent | null;
}
function state(definition: BuiltInAgentDefinition, agent: Agent | null): BuiltInAgentState {
return {
definition,
status: deriveBuiltInAgentStatus(agent),
agentId: agent?.id ?? null,
agent,
pauseReason: agent?.pauseReason ?? null,
};
}
async function get(companyId: string, key: string) {
const definition = requireBuiltInAgentDefinition(key);
await ensureCompany(companyId);
return state(definition, await findSingleAgent(companyId, definition));
}
async function ensure(companyId: string, key: string, input: BuiltInAgentProvisionInput = {}) {
const definition = requireBuiltInAgentDefinition(key);
await ensureCompany(companyId);
const existing = await findSingleAgent(companyId, definition);
if (existing) {
const patch: Partial<typeof agents.$inferInsert> = {
metadata: builtInMetadata(definition, existing.metadata),
};
if (input.adapterType !== undefined || input.adapterConfig !== undefined) {
const adapterType = input.adapterType ?? existing.adapterType;
assertAdapterAllowed(definition, adapterType);
patch.adapterType = adapterType;
patch.adapterConfig = input.adapterConfig ?? existing.adapterConfig;
}
const updated = await agentSvc.update(existing.id, patch, {
allowBuiltInAgentMetadata: true,
recordRevision: { source: "built-in-agent:ensure" },
});
if (!updated) throw notFound("Built-in agent not found");
return state(definition, updated as Agent);
}
const created = await agentSvc.create(companyId, {
...definitionPatch(definition, input),
status: "idle",
metadata: builtInMetadata(definition),
runtimeConfig: {},
permissions: {},
spentMonthlyCents: 0,
lastHeartbeatAt: null,
}, { allowBuiltInAgentMetadata: true }) as Agent;
await logActivity(db, {
companyId,
actorType: "system",
actorId: "built-in-agents",
action: "built_in_agent.provisioned",
entityType: "agent",
entityId: created.id,
details: {
key: definition.key,
featureKeys: definition.featureKeys,
status: deriveBuiltInAgentStatus(created),
},
});
return state(definition, created);
}
async function list(companyId: string) {
await ensureCompany(companyId);
return Promise.all(DEFINITIONS.map(async (definition) => state(definition, await findSingleAgent(companyId, definition))));
}
async function reconcileDefinitionDefaults(companyId: string, key: string) {
const definition = requireBuiltInAgentDefinition(key);
await ensureCompany(companyId);
const existing = await findSingleAgent(companyId, definition);
if (!existing) return state(definition, null);
const patch = {
name: definition.displayName,
role: definition.defaultRole,
title: null,
capabilities: definition.shortPurpose,
metadata: builtInMetadata(definition, existing.metadata),
};
const updated = await agentSvc.update(existing.id, patch, {
allowBuiltInAgentMetadata: true,
recordRevision: { source: "built-in-agent:reconcile-defaults" },
});
if (!updated) throw notFound("Built-in agent not found");
return state(definition, updated as Agent);
}
return {
definitions: listBuiltInAgentDefinitions,
get,
ensure,
list,
reconcileDefinitionDefaults,
};
}
export async function reconcileBuiltInAgentsOnStartup(db: Db) {
const svc = builtInAgentService(db);
const rows = await db
.select({
companyId: agents.companyId,
metadata: agents.metadata,
status: agents.status,
})
.from(agents)
.where(ne(agents.status, "terminated"));
const seen = new Set<string>();
let scanned = 0;
let reconciled = 0;
let unknown = 0;
let duplicates = 0;
for (const row of rows) {
const marker = readBuiltInAgentMarker(row.metadata);
if (!marker) continue;
scanned += 1;
if (!getBuiltInAgentDefinition(marker.key)) {
unknown += 1;
continue;
}
const instanceKey = `${row.companyId}:${marker.key}`;
if (seen.has(instanceKey)) {
duplicates += 1;
continue;
}
seen.add(instanceKey);
await svc.reconcileDefinitionDefaults(row.companyId, marker.key);
reconciled += 1;
}
return { scanned, reconciled, unknown, duplicates };
}
+11
View File
@@ -4,6 +4,17 @@ export { companySearchService } from "./company-search.js";
export { feedbackService } from "./feedback.js";
export { companySkillService } from "./company-skills.js";
export { agentService, deduplicateAgentName } from "./agents.js";
export {
builtInAgentService,
deriveBuiltInAgentStatus,
getBuiltInAgentDefinition,
listBuiltInAgentDefinitions,
reconcileBuiltInAgentsOnStartup,
validateBuiltInAgentDefinitions,
type BuiltInAgentDefinition,
type BuiltInAgentState,
type BuiltInAgentStatus,
} from "./built-in-agents.js";
export { agentInstructionsService, syncInstructionsBundleConfigFromFilePath } from "./agent-instructions.js";
export { assetService } from "./assets.js";
export { documentService, extractLegacyPlanBody } from "./documents.js";