Skip to content
Open
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
166 changes: 166 additions & 0 deletions packages/coding-agent/src/runtime/resource-loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { logger } from "@oh-my-pi/pi-utils";
import { loadCapability } from "../capability";
import { type Rule, ruleCapability } from "../capability/rule";
import { loadPromptTemplates, type PromptTemplate } from "../config/prompt-templates";
import type { Settings } from "../config/settings";
import { TtsrManager } from "../export/ttsr";
import { type CustomCommandsLoadResult, loadCustomCommands } from "../extensibility/custom-commands";
import {
discoverAndLoadExtensions,
type ExtensionFactory,
type LoadExtensionsResult,
loadExtensionFromFactory,
loadExtensions,
} from "../extensibility/extensions";
import { loadSkills, type Skill, type SkillWarning } from "../extensibility/skills";
import { type FileSlashCommand, loadSlashCommands } from "../extensibility/slash-commands";
import { loadProjectContextFiles } from "../system-prompt";
import type { EventBus } from "../utils/event-bus";

export interface RuntimeResourceLoaderOptions {
cwd: string;
agentDir: string;
settings: Settings;
eventBus: EventBus;
}

export class RuntimeResourceLoader {
readonly #cwd: string;
readonly #agentDir: string;
readonly #settings: Settings;
readonly #eventBus: EventBus;

constructor(options: RuntimeResourceLoaderOptions) {
this.#cwd = options.cwd;
this.#agentDir = options.agentDir;
this.#settings = options.settings;
this.#eventBus = options.eventBus;
}

async loadSkills(options: { skills?: Skill[] }): Promise<{ skills: Skill[]; warnings: SkillWarning[] }> {
if (options.skills !== undefined) {
return { skills: options.skills, warnings: [] };
}

const skillsSettings = this.#settings.getGroup("skills");
const disabledExtensionIds = this.#settings.get("disabledExtensions") ?? [];
return await loadSkills({
...skillsSettings,
disabledExtensions: disabledExtensionIds,
cwd: this.#cwd,
});
}

async loadRules(options: {
rules?: Rule[];
injectedTtsrRules: string[];
}): Promise<{ ttsrManager: TtsrManager; rulebookRules: Rule[]; alwaysApplyRules: Rule[] }> {
const ttsrSettings = this.#settings.getGroup("ttsr");
const ttsrManager = new TtsrManager(ttsrSettings);
const rulesResult =
options.rules !== undefined
? { items: options.rules, warnings: undefined }
: await loadCapability<Rule>(ruleCapability.id, { cwd: this.#cwd });
const rulebookRules: Rule[] = [];
const alwaysApplyRules: Rule[] = [];
for (const rule of rulesResult.items) {
const isTtsrRule = rule.condition && rule.condition.length > 0 ? ttsrManager.addRule(rule) : false;
if (isTtsrRule) {
continue;
}
if (rule.alwaysApply === true) {
alwaysApplyRules.push(rule);
continue;
}
if (rule.description) {
rulebookRules.push(rule);
}
}
if (options.injectedTtsrRules.length > 0) {
ttsrManager.restoreInjected(options.injectedTtsrRules);
}
return { ttsrManager, rulebookRules, alwaysApplyRules };
}

async loadContextFiles(options: {
contextFiles?: Array<{ path: string; content: string }>;
}): Promise<Array<{ path: string; content: string; depth?: number }>> {
return options.contextFiles ?? (await loadProjectContextFiles({ cwd: this.#cwd }));
}

async loadPromptTemplates(options: { promptTemplates?: PromptTemplate[] }): Promise<PromptTemplate[]> {
return options.promptTemplates ?? (await loadPromptTemplates({ cwd: this.#cwd, agentDir: this.#agentDir }));
}

async loadSlashCommands(options: { slashCommands?: FileSlashCommand[] }): Promise<FileSlashCommand[]> {
return options.slashCommands ?? (await loadSlashCommands({ cwd: this.#cwd }));
}

async loadCustomCommands(options: { disableExtensionDiscovery?: boolean }): Promise<CustomCommandsLoadResult> {
if (options.disableExtensionDiscovery) {
return { commands: [], errors: [] };
}

const result = await loadCustomCommands({ cwd: this.#cwd, agentDir: this.#agentDir });
for (const { path, error } of result.errors) {
logger.error("Failed to load custom command", { path, error });
}
return result;
}

async loadExtensions(options: {
disableExtensionDiscovery?: boolean;
preloadedExtensions?: LoadExtensionsResult;
additionalExtensionPaths?: string[];
inlineExtensions?: ExtensionFactory[];
}): Promise<LoadExtensionsResult> {
let extensionsResult: LoadExtensionsResult;
if (options.disableExtensionDiscovery) {
const configuredPaths = options.additionalExtensionPaths ?? [];
extensionsResult = await logger.time(
"loadExtensions",
loadExtensions,
configuredPaths,
this.#cwd,
this.#eventBus,
);
for (const { path, error } of extensionsResult.errors) {
logger.error("Failed to load extension", { path, error });
}
} else if (options.preloadedExtensions) {
extensionsResult = options.preloadedExtensions;
} else {
const configuredPaths = [
...(options.additionalExtensionPaths ?? []),
...(this.#settings.get("extensions") ?? []),
];
const disabledExtensionIds = this.#settings.get("disabledExtensions") ?? [];
extensionsResult = await logger.time(
"discoverAndLoadExtensions",
discoverAndLoadExtensions,
configuredPaths,
this.#cwd,
this.#eventBus,
disabledExtensionIds,
);
for (const { path, error } of extensionsResult.errors) {
logger.error("Failed to load extension", { path, error });
}
}

const inlineExtensions = options.inlineExtensions ?? [];
for (let i = 0; i < inlineExtensions.length; i++) {
const factory = inlineExtensions[i];
const loaded = await loadExtensionFromFactory(
factory,
this.#cwd,
this.#eventBus,
extensionsResult.runtime,
`<inline-${i}>`,
);
extensionsResult.extensions.push(loaded);
}

return extensionsResult;
}
}
120 changes: 27 additions & 93 deletions packages/coding-agent/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ import {
import chalk from "chalk";
import { AsyncJobManager, isBackgroundJobSupportEnabled } from "./async";
import { createAutoresearchExtension } from "./autoresearch";
import { loadCapability } from "./capability";
import { type Rule, ruleCapability } from "./capability/rule";
import type { Rule } from "./capability/rule";
import { ModelRegistry } from "./config/model-registry";
import { formatModelString, parseModelPattern, parseModelString, resolveModelRoleValue } from "./config/model-resolver";
import { loadPromptTemplates as loadPromptTemplatesInternal, type PromptTemplate } from "./config/prompt-templates";
Expand All @@ -36,7 +35,6 @@ import { CursorExecHandlers } from "./cursor";
import "./discovery";
import { resolveConfigValue } from "./config/resolve-config-value";
import { initializeWithSettings } from "./discovery";
import { TtsrManager } from "./export/ttsr";
import {
type CustomCommandsLoadResult,
type LoadedCustomCommand,
Expand All @@ -53,8 +51,6 @@ import {
ExtensionToolWrapper,
type ExtensionUIContext,
type LoadExtensionsResult,
loadExtensionFromFactory,
loadExtensions,
type ToolDefinition,
wrapRegisteredTools,
} from "./extensibility/extensions";
Expand Down Expand Up @@ -84,6 +80,7 @@ import {
import { buildMemoryToolDeveloperInstructions, getMemoryRoot, startMemoryStartupTask } from "./memories";
import asyncResultTemplate from "./prompts/tools/async-result.md" with { type: "text" };
import { AgentRegistry, MAIN_AGENT_ID } from "./registry/agent-registry";
import { RuntimeResourceLoader } from "./runtime/resource-loader";
import {
collectEnvSecrets,
deobfuscateSessionContext,
Expand Down Expand Up @@ -668,12 +665,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
if (!options.modelRegistry) {
modelRegistry.refreshInBackground();
}
const skillsSettings = settings.getGroup("skills");
const disabledExtensionIds = settings.get("disabledExtensions") ?? [];
const discoveredSkillsPromise =
options.skills === undefined
? discoverSkills(cwd, agentDir, { ...skillsSettings, disabledExtensions: disabledExtensionIds })
: undefined;
const resourceLoader = new RuntimeResourceLoader({ cwd, agentDir, settings, eventBus });
const discoveredSkillsPromise = options.skills === undefined ? resourceLoader.loadSkills({}) : undefined;

// Initialize provider preferences from settings
const webSearchProvider = settings.get("providers.webSearch");
Expand Down Expand Up @@ -803,49 +796,28 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
let skills: Skill[];
let skillWarnings: SkillWarning[];
if (options.skills !== undefined) {
skills = options.skills;
skillWarnings = [];
const loaded = await resourceLoader.loadSkills({ skills: options.skills });
skills = loaded.skills;
skillWarnings = loaded.warnings;
} else {
const discovered = await logger.time(
"discoverSkills",
() => discoveredSkillsPromise ?? Promise.resolve({ skills: [], warnings: [] }),
() => discoveredSkillsPromise ?? resourceLoader.loadSkills({}),
);
skills = discovered.skills;
skillWarnings = discovered.warnings;
}

// Discover rules and bucket them in one pass to avoid repeated scans over large rule sets.
const { ttsrManager, rulebookRules, alwaysApplyRules } = await logger.time("discoverTtsrRules", async () => {
const ttsrSettings = settings.getGroup("ttsr");
const ttsrManager = new TtsrManager(ttsrSettings);
const rulesResult =
options.rules !== undefined
? { items: options.rules, warnings: undefined }
: await loadCapability<Rule>(ruleCapability.id, { cwd });
const rulebookRules: Rule[] = [];
const alwaysApplyRules: Rule[] = [];
for (const rule of rulesResult.items) {
const isTtsrRule = rule.condition && rule.condition.length > 0 ? ttsrManager.addRule(rule) : false;
if (isTtsrRule) {
continue;
}
if (rule.alwaysApply === true) {
alwaysApplyRules.push(rule);
continue;
}
if (rule.description) {
rulebookRules.push(rule);
}
}
if (existingSession.injectedTtsrRules.length > 0) {
ttsrManager.restoreInjected(existingSession.injectedTtsrRules);
}
return { ttsrManager, rulebookRules, alwaysApplyRules };
});
const { ttsrManager, rulebookRules, alwaysApplyRules } = await logger.time("discoverTtsrRules", () =>
resourceLoader.loadRules({
rules: options.rules,
injectedTtsrRules: existingSession.injectedTtsrRules,
}),
);

const contextFiles = await logger.time(
"discoverContextFiles",
async () => options.contextFiles ?? (await discoverContextFiles(cwd, agentDir)),
const contextFiles = await logger.time("discoverContextFiles", () =>
resourceLoader.loadContextFiles({ contextFiles: options.contextFiles }),
);

let agent: Agent;
Expand Down Expand Up @@ -1102,46 +1074,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
}

// Load extensions (discovers from standard locations + configured paths)
let extensionsResult: LoadExtensionsResult;
if (options.disableExtensionDiscovery) {
const configuredPaths = options.additionalExtensionPaths ?? [];
extensionsResult = await logger.time("loadExtensions", loadExtensions, configuredPaths, cwd, eventBus);
for (const { path, error } of extensionsResult.errors) {
logger.error("Failed to load extension", { path, error });
}
} else if (options.preloadedExtensions) {
extensionsResult = options.preloadedExtensions;
} else {
// Merge CLI extension paths with settings extension paths
const configuredPaths = [...(options.additionalExtensionPaths ?? []), ...(settings.get("extensions") ?? [])];
const disabledExtensionIds = settings.get("disabledExtensions") ?? [];
extensionsResult = await logger.time(
"discoverAndLoadExtensions",
discoverAndLoadExtensions,
configuredPaths,
cwd,
eventBus,
disabledExtensionIds,
);
for (const { path, error } of extensionsResult.errors) {
logger.error("Failed to load extension", { path, error });
}
}

// Load inline extensions from factories
if (inlineExtensions.length > 0) {
for (let i = 0; i < inlineExtensions.length; i++) {
const factory = inlineExtensions[i];
const loaded = await loadExtensionFromFactory(
factory,
cwd,
eventBus,
extensionsResult.runtime,
`<inline-${i}>`,
);
extensionsResult.extensions.push(loaded);
}
}
const extensionsResult = await resourceLoader.loadExtensions({
disableExtensionDiscovery: options.disableExtensionDiscovery,
preloadedExtensions: options.preloadedExtensions,
additionalExtensionPaths: options.additionalExtensionPaths,
inlineExtensions,
});

// Process provider registrations queued during extension loading.
// This must happen before the runner is created so that models registered by
Expand Down Expand Up @@ -1197,13 +1135,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}

// Discover custom commands (TypeScript slash commands)
const customCommandsResult: CustomCommandsLoadResult = options.disableExtensionDiscovery
? { commands: [], errors: [] }
: await logger.time("discoverCustomCommands", loadCustomCommandsInternal, { cwd, agentDir });
if (!options.disableExtensionDiscovery) {
for (const { path, error } of customCommandsResult.errors) {
logger.error("Failed to load custom command", { path, error });
}
}
? await resourceLoader.loadCustomCommands({ disableExtensionDiscovery: true })
: await logger.time("discoverCustomCommands", () => resourceLoader.loadCustomCommands({}));

let extensionRunner: ExtensionRunner | undefined;
if (extensionsResult.extensions.length > 0) {
Expand Down Expand Up @@ -1438,11 +1371,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}

const promptTemplates =
options.promptTemplates ??
(await logger.time("discoverPromptTemplates", discoverPromptTemplates, cwd, agentDir));
(await logger.time("discoverPromptTemplates", () => resourceLoader.loadPromptTemplates({})));
toolSession.promptTemplates = promptTemplates;

const slashCommands =
options.slashCommands ?? (await logger.time("discoverSlashCommands", discoverSlashCommands, cwd));
options.slashCommands ??
(await logger.time("discoverSlashCommands", () => resourceLoader.loadSlashCommands({})));

// Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)
const convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => {
Expand Down