Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/agents/definitions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ export {
export type { FetchContextParams } from './contextSteps.js';
export {
clearDefinitionCache,
getKnownAgentTypes,
getBuiltinAgentTypes,
invalidateDefinitionCache,
isBuiltinAgentType,
isPMFocusedAgent,
loadAgentDefinition,
loadAllAgentDefinitions,
loadBuiltinDefinition,
resolveAgentDefinition,
resolveAllAgentDefinitions,
resolveKnownAgentTypes,
Expand Down
44 changes: 17 additions & 27 deletions src/agents/definitions/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ const cache = new Map<string, AgentDefinition>();
let knownTypes: string[] | null = null;

/**
* Load and validate a single agent definition from YAML.
* Load and validate a single built-in agent definition from YAML.
* Results are cached after first load.
*
* @deprecated Use `resolveAgentDefinition()` instead (checks cache → DB → YAML).
* Use this in sync contexts that genuinely need YAML-only access (seed scripts,
* reset operations, internal fallbacks). For general use prefer `resolveAgentDefinition()`
* which checks the in-memory cache, then the database, then falls back to YAML.
*/
export function loadAgentDefinition(agentType: string): AgentDefinition {
export function loadBuiltinDefinition(agentType: string): AgentDefinition {
const cached = cache.get(agentType);
if (cached) return cached;

Expand All @@ -47,25 +49,13 @@ export function loadAgentDefinition(agentType: string): AgentDefinition {
}

/**
* Load all agent definitions discovered from YAML files in the definitions directory.
* Return the list of built-in agent types (derived from YAML filenames).
*
* @deprecated Use `resolveAllAgentDefinitions()` instead (checks DB with YAML fallback).
* Use this in sync contexts that genuinely need YAML-only type enumeration.
* For general use prefer `resolveKnownAgentTypes()` which also includes
* custom types stored only in the database.
*/
export function loadAllAgentDefinitions(): Map<string, AgentDefinition> {
const types = getKnownAgentTypes();
const result = new Map<string, AgentDefinition>();
for (const agentType of types) {
result.set(agentType, loadAgentDefinition(agentType));
}
return result;
}

/**
* Return the list of known agent types (derived from YAML filenames).
*
* @deprecated Use `resolveKnownAgentTypes()` instead (returns types from both DB and YAML).
*/
export function getKnownAgentTypes(): string[] {
export function getBuiltinAgentTypes(): string[] {
if (knownTypes) return knownTypes;

const entries = readdirSync(__dirname);
Expand All @@ -86,11 +76,11 @@ export function clearDefinitionCache(): void {

/**
* Returns true if the given agentType has a backing YAML file (i.e. is a built-in type).
* Wraps `getKnownAgentTypes().includes()` to avoid repeated deprecated-function calls at each
* call site.
* Wraps `getBuiltinAgentTypes().includes()` to avoid repeated deprecated-function calls at
* each call site.
*/
export function isBuiltinAgentType(agentType: string): boolean {
return getKnownAgentTypes().includes(agentType);
return getBuiltinAgentTypes().includes(agentType);
}

// ============================================================================
Expand Down Expand Up @@ -123,7 +113,7 @@ export async function resolveAgentDefinition(agentType: string): Promise<AgentDe
}

// 3. YAML fallback
return loadAgentDefinition(agentType);
return loadBuiltinDefinition(agentType);
}

/**
Expand All @@ -133,7 +123,7 @@ export async function resolveAgentDefinition(agentType: string): Promise<AgentDe
* Returns a `Map<agentType, AgentDefinition>` covering all known agent types.
*/
export async function resolveAllAgentDefinitions(): Promise<Map<string, AgentDefinition>> {
const yamlTypes = getKnownAgentTypes();
const yamlTypes = getBuiltinAgentTypes();
const result = new Map<string, AgentDefinition>();

// Fetch all DB entries first
Expand All @@ -155,7 +145,7 @@ export async function resolveAllAgentDefinitions(): Promise<Map<string, AgentDef
// Fill missing types from YAML
for (const agentType of yamlTypes) {
if (!result.has(agentType)) {
result.set(agentType, loadAgentDefinition(agentType));
result.set(agentType, loadBuiltinDefinition(agentType));
}
}

Expand All @@ -166,7 +156,7 @@ export async function resolveAllAgentDefinitions(): Promise<Map<string, AgentDef
* Return all known agent types, combining DB-registered types with YAML-discovered types.
*/
export async function resolveKnownAgentTypes(): Promise<string[]> {
const yamlTypes = new Set(getKnownAgentTypes());
const yamlTypes = new Set(getBuiltinAgentTypes());

try {
const { listAgentDefinitions } = await import(
Expand Down
11 changes: 5 additions & 6 deletions src/agents/prompts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { Eta } from 'eta';

import { resolveKnownAgentTypes } from '../definitions/index.js';
import { loadAgentDefinition } from '../definitions/loader.js';
import { resolveAgentDefinition, resolveKnownAgentTypes } from '../definitions/index.js';

const __dirname = dirname(fileURLToPath(import.meta.url));
const templatesDir = join(__dirname, 'templates');
Expand Down Expand Up @@ -236,13 +235,13 @@ export function renderInlineTaskPrompt(
}

/**
* Returns the YAML-defined taskPrompt for an agent type (the factory default).
* Does not require initPrompts() — reads directly from YAML.
* Returns the taskPrompt for an agent type (the factory default).
* Checks the database (with YAML fallback) via `resolveAgentDefinition()`.
* Returns null if the agent type is unknown or has no taskPrompt defined.
*/
export function getDefaultTaskPrompt(agentType: string): string | null {
export async function getDefaultTaskPrompt(agentType: string): Promise<string | null> {
try {
const definition = loadAgentDefinition(agentType);
const definition = await resolveAgentDefinition(agentType);
return definition.prompts.taskPrompt ?? null;
} catch {
return null;
Expand Down
4 changes: 2 additions & 2 deletions src/api/routers/agentConfigs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,8 @@ export const agentConfigsRouter = router({
// No .eta template on disk — skip gracefully
}

// 4. YAML-defined task prompt (factory default)
const defaultTaskPrompt = getDefaultTaskPrompt(input.agentType);
// 4. Task prompt factory default (checks DB then YAML fallback)
const defaultTaskPrompt = await getDefaultTaskPrompt(input.agentType);

return {
projectSystemPrompt,
Expand Down
32 changes: 16 additions & 16 deletions src/api/routers/agentDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { TRPCError } from '@trpc/server';
import { z } from 'zod';
import { CAPABILITIES } from '../../agents/capabilities/index.js';
import {
getKnownAgentTypes,
getBuiltinAgentTypes,
invalidateDefinitionCache,
isBuiltinAgentType,
loadAgentDefinition,
loadBuiltinDefinition,
resolveAgentDefinition,
resolveKnownAgentTypes,
} from '../../agents/definitions/loader.js';
Expand Down Expand Up @@ -60,10 +60,10 @@ export const agentDefinitionsRouter = router({
* resolveAllAgentDefinitions() which would issue its own redundant listAgentDefinitions() call.
*/
list: superAdminProcedure.query(async () => {
// Intentional: getKnownAgentTypes() (deprecated) is used here to enumerate YAML types
// for the merge loop below. resolveKnownAgentTypes() also hits the DB, which we already
// cover via listAgentDefinitions(); calling both would be redundant.
const yamlTypes = getKnownAgentTypes();
// getBuiltinAgentTypes() enumerates YAML types for the merge loop below.
// resolveKnownAgentTypes() also hits the DB, which we already cover via
// listAgentDefinitions(); calling both would be redundant.
const yamlTypes = getBuiltinAgentTypes();
const result: Array<{ agentType: string; definition: AgentDefinition; isBuiltin: boolean }> =
[];

Expand All @@ -84,15 +84,15 @@ export const agentDefinitionsRouter = router({
seen.add(row.agentType);
}

// Fill in YAML-only types not present in DB
// Intentional: loadAgentDefinition() (deprecated) is used here because this is a
// synchronous fallback path — we already have the YAML type list and just need the
// raw definition content; the async resolveAgentDefinition() would add DB round-trips.
// Fill in YAML-only types not present in DB.
// loadBuiltinDefinition() is used here because this is a synchronous fallback path —
// we already have the YAML type list and just need the raw definition content;
// the async resolveAgentDefinition() would add unnecessary DB round-trips.
for (const agentType of yamlTypes) {
if (!seen.has(agentType)) {
result.push({
agentType,
definition: loadAgentDefinition(agentType),
definition: loadBuiltinDefinition(agentType),
isBuiltin: true, // YAML-only types are always builtin
});
}
Expand Down Expand Up @@ -232,11 +232,11 @@ export const agentDefinitionsRouter = router({
}

// Re-read the YAML (bypass cache).
// Intentional: loadAgentDefinition() (deprecated) is used here because this endpoint
// explicitly needs the raw YAML definition — the purpose of reset is to bypass any DB
// override and restore the hard-coded YAML defaults.
// loadBuiltinDefinition() is used here because this endpoint explicitly needs the
// raw YAML definition — the purpose of reset is to bypass any DB override and
// restore the hard-coded YAML defaults.
invalidateDefinitionCache();
const yamlDefinition = loadAgentDefinition(input.agentType);
const yamlDefinition = loadBuiltinDefinition(input.agentType);
await upsertAgentDefinition(input.agentType, yamlDefinition, true);
invalidateDefinitionCache();
return { agentType: input.agentType };
Expand Down Expand Up @@ -311,7 +311,7 @@ export const agentDefinitionsRouter = router({
// Load YAML defaults and use its prompts section
let yamlDefault: AgentDefinition;
try {
yamlDefault = loadAgentDefinition(input.agentType);
yamlDefault = loadBuiltinDefinition(input.agentType);
} catch {
throw new TRPCError({
code: 'NOT_FOUND',
Expand Down
6 changes: 3 additions & 3 deletions src/api/routers/agentTriggerConfigs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TRPCError } from '@trpc/server';
import { z } from 'zod';
import { getKnownAgentTypes, loadAgentDefinition } from '../../agents/definitions/loader.js';
import { getBuiltinAgentTypes, loadBuiltinDefinition } from '../../agents/definitions/loader.js';
import type {
AgentDefinition,
SupportedTrigger,
Expand Down Expand Up @@ -220,7 +220,7 @@ export const agentTriggerConfigsRouter = router({
const enabledAgentTypes = new Set(projectAgentConfigs.map((c) => c.agentType));

// Build a combined list of definitions (DB + YAML)
const yamlTypes = getKnownAgentTypes();
const yamlTypes = getBuiltinAgentTypes();
const definitions: Array<{ agentType: string; definition: AgentDefinition }> = [];
const seen = new Set<string>();

Expand All @@ -234,7 +234,7 @@ export const agentTriggerConfigsRouter = router({
for (const agentType of yamlTypes) {
if (!seen.has(agentType)) {
try {
definitions.push({ agentType, definition: loadAgentDefinition(agentType) });
definitions.push({ agentType, definition: loadBuiltinDefinition(agentType) });
} catch (err) {
logger.warn('Failed to load agent definition from YAML', { agentType, error: err });
}
Expand Down
6 changes: 3 additions & 3 deletions src/db/seeds/seedAgentDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@
* npx tsx src/db/seeds/seedAgentDefinitions.ts
*/

import { getKnownAgentTypes, loadAgentDefinition } from '../../agents/definitions/loader.js';
import { getBuiltinAgentTypes, loadBuiltinDefinition } from '../../agents/definitions/loader.js';
import { readTemplateFileSync } from '../../agents/prompts/index.js';
import { upsertAgentDefinition } from '../repositories/agentDefinitionsRepository.js';

export async function seedAgentDefinitions(): Promise<void> {
const agentTypes = getKnownAgentTypes();
const agentTypes = getBuiltinAgentTypes();

console.log(`Seeding ${agentTypes.length} agent definitions...`);

for (const agentType of agentTypes) {
const definition = loadAgentDefinition(agentType);
const definition = loadBuiltinDefinition(agentType);
const systemPrompt = readTemplateFileSync(agentType);
const enriched = systemPrompt
? { ...definition, prompts: { ...definition.prompts, systemPrompt } }
Expand Down
24 changes: 12 additions & 12 deletions tests/unit/agents/definitions/async-resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
clearDefinitionCache,
invalidateDefinitionCache,
loadAgentDefinition,
loadBuiltinDefinition,
resolveAgentDefinition,
resolveAllAgentDefinitions,
resolveKnownAgentTypes,
Expand Down Expand Up @@ -51,7 +51,7 @@ describe('resolveAgentDefinition', () => {

it('returns from in-memory cache when already loaded', async () => {
// Prime the cache via the sync loader
const fromYaml = loadAgentDefinition('implementation');
const fromYaml = loadBuiltinDefinition('implementation');
const { getAgentDefinition } = await getDbMocks();

// resolveAgentDefinition should return the cached value without hitting DB
Expand All @@ -62,7 +62,7 @@ describe('resolveAgentDefinition', () => {

it('fetches from DB when cache is empty and DB has the definition', async () => {
const { getAgentDefinition } = await getDbMocks();
const dbDef = loadAgentDefinition('planning');
const dbDef = loadBuiltinDefinition('planning');
clearDefinitionCache();

getAgentDefinition.mockResolvedValue(dbDef);
Expand All @@ -77,7 +77,7 @@ describe('resolveAgentDefinition', () => {
getAgentDefinition.mockResolvedValue(null);

const result = await resolveAgentDefinition('splitting');
const expected = loadAgentDefinition('splitting');
const expected = loadBuiltinDefinition('splitting');
expect(result).toEqual(expected);
});

Expand All @@ -86,13 +86,13 @@ describe('resolveAgentDefinition', () => {
getAgentDefinition.mockRejectedValue(new Error('DB connection failed'));

const result = await resolveAgentDefinition('review');
const expected = loadAgentDefinition('review');
const expected = loadBuiltinDefinition('review');
expect(result).toEqual(expected);
});

it('caches DB result so subsequent calls skip DB', async () => {
const { getAgentDefinition } = await getDbMocks();
const dbDef = loadAgentDefinition('debug');
const dbDef = loadBuiltinDefinition('debug');
clearDefinitionCache();
getAgentDefinition.mockResolvedValue(dbDef);

Expand Down Expand Up @@ -127,7 +127,7 @@ describe('resolveAllAgentDefinitions', () => {

it('prefers DB definitions over YAML when present in DB', async () => {
const { listAgentDefinitions } = await getDbMocks();
const dbDef = loadAgentDefinition('implementation');
const dbDef = loadBuiltinDefinition('implementation');
clearDefinitionCache();

// Simulate DB having only "implementation"
Expand Down Expand Up @@ -172,7 +172,7 @@ describe('resolveKnownAgentTypes', () => {

it('merges DB-only types with YAML types', async () => {
const { listAgentDefinitions } = await getDbMocks();
const customDef = loadAgentDefinition('implementation');
const customDef = loadBuiltinDefinition('implementation');
clearDefinitionCache();

listAgentDefinitions.mockResolvedValue([
Expand Down Expand Up @@ -213,8 +213,8 @@ describe('invalidateDefinitionCache', () => {

it('clears the in-memory cache so next resolve hits DB', async () => {
const { getAgentDefinition } = await getDbMocks();
const dbDef = loadAgentDefinition('planning');
// Clear cache after priming via loadAgentDefinition so resolveAgentDefinition hits DB
const dbDef = loadBuiltinDefinition('planning');
// Clear cache after priming via loadBuiltinDefinition so resolveAgentDefinition hits DB
clearDefinitionCache();
getAgentDefinition.mockResolvedValue(dbDef);

Expand All @@ -229,10 +229,10 @@ describe('invalidateDefinitionCache', () => {
});

it('behaves identically to clearDefinitionCache for the sync path', () => {
loadAgentDefinition('splitting'); // prime cache
loadBuiltinDefinition('splitting'); // prime cache
invalidateDefinitionCache();
// Sync load still works (reads fresh from YAML)
expect(() => loadAgentDefinition('splitting')).not.toThrow();
expect(() => loadBuiltinDefinition('splitting')).not.toThrow();
});
});

Expand Down
Loading
Loading