mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 20:03:05 +00:00
Add SDK for programmatic AgentSession usage
- Add src/core/sdk.ts with createAgentSession() factory and discovery functions - Update loaders to accept cwd/agentDir parameters (skills, hooks, custom-tools, slash-commands, system-prompt) - Export SDK from package index Addresses #272
This commit is contained in:
parent
11e743373d
commit
5482bf3e14
8 changed files with 737 additions and 40 deletions
|
|
@ -303,7 +303,7 @@ function discoverToolsInDir(dir: string): string[] {
|
|||
|
||||
/**
|
||||
* Discover and load tools from standard locations:
|
||||
* 1. ~/.pi/agent/tools/*.ts (global)
|
||||
* 1. agentDir/tools/*.ts (global)
|
||||
* 2. cwd/.pi/tools/*.ts (project-local)
|
||||
*
|
||||
* Plus any explicitly configured paths from settings or CLI.
|
||||
|
|
@ -311,12 +311,15 @@ function discoverToolsInDir(dir: string): string[] {
|
|||
* @param configuredPaths - Explicit paths from settings.json and CLI --tool flags
|
||||
* @param cwd - Current working directory
|
||||
* @param builtInToolNames - Names of built-in tools to check for conflicts
|
||||
* @param agentDir - Agent config directory. Default: from getAgentDir()
|
||||
*/
|
||||
export async function discoverAndLoadCustomTools(
|
||||
configuredPaths: string[],
|
||||
cwd: string,
|
||||
builtInToolNames: string[],
|
||||
agentDir?: string,
|
||||
): Promise<CustomToolsLoadResult> {
|
||||
const resolvedAgentDir = agentDir ?? getAgentDir();
|
||||
const allPaths: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
|
|
@ -331,8 +334,8 @@ export async function discoverAndLoadCustomTools(
|
|||
}
|
||||
};
|
||||
|
||||
// 1. Global tools: ~/.pi/agent/tools/
|
||||
const globalToolsDir = path.join(getAgentDir(), "tools");
|
||||
// 1. Global tools: agentDir/tools/
|
||||
const globalToolsDir = path.join(resolvedAgentDir, "tools");
|
||||
addPaths(discoverToolsInDir(globalToolsDir));
|
||||
|
||||
// 2. Project-local tools: cwd/.pi/tools/
|
||||
|
|
|
|||
|
|
@ -215,17 +215,28 @@ function discoverHooksInDir(dir: string): string[] {
|
|||
}
|
||||
}
|
||||
|
||||
export interface DiscoverAndLoadHooksOptions {
|
||||
/** Explicit paths from settings.json or CLI */
|
||||
configuredPaths?: string[];
|
||||
/** Current working directory */
|
||||
cwd?: string;
|
||||
/** Agent config directory. Default: from getAgentDir() */
|
||||
agentDir?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover and load hooks from standard locations:
|
||||
* 1. ~/.pi/agent/hooks/*.ts (global)
|
||||
* 1. agentDir/hooks/*.ts (global)
|
||||
* 2. cwd/.pi/hooks/*.ts (project-local)
|
||||
*
|
||||
* Plus any explicitly configured paths from settings.
|
||||
*
|
||||
* @param configuredPaths - Explicit paths from settings.json
|
||||
* @param cwd - Current working directory
|
||||
*/
|
||||
export async function discoverAndLoadHooks(configuredPaths: string[], cwd: string): Promise<LoadHooksResult> {
|
||||
export async function discoverAndLoadHooks(
|
||||
configuredPaths: string[],
|
||||
cwd: string,
|
||||
agentDir?: string,
|
||||
): Promise<LoadHooksResult> {
|
||||
const resolvedAgentDir = agentDir ?? getAgentDir();
|
||||
const allPaths: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
|
|
@ -240,8 +251,8 @@ export async function discoverAndLoadHooks(configuredPaths: string[], cwd: strin
|
|||
}
|
||||
};
|
||||
|
||||
// 1. Global hooks: ~/.pi/agent/hooks/
|
||||
const globalHooksDir = path.join(getAgentDir(), "hooks");
|
||||
// 1. Global hooks: agentDir/hooks/
|
||||
const globalHooksDir = path.join(resolvedAgentDir, "hooks");
|
||||
addPaths(discoverHooksInDir(globalHooksDir));
|
||||
|
||||
// 2. Project-local hooks: cwd/.pi/hooks/
|
||||
|
|
|
|||
593
packages/coding-agent/src/core/sdk.ts
Normal file
593
packages/coding-agent/src/core/sdk.ts
Normal file
|
|
@ -0,0 +1,593 @@
|
|||
/**
|
||||
* SDK for programmatic usage of AgentSession.
|
||||
*
|
||||
* Provides a factory function and discovery helpers that allow full control
|
||||
* over agent configuration, or sensible defaults that match CLI behavior.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Minimal - everything auto-discovered
|
||||
* const session = await createAgentSession();
|
||||
*
|
||||
* // With custom hooks
|
||||
* const session = await createAgentSession({
|
||||
* hooks: [
|
||||
* ...await discoverHooks(),
|
||||
* { factory: myHookFactory },
|
||||
* ],
|
||||
* });
|
||||
*
|
||||
* // Full control
|
||||
* const session = await createAgentSession({
|
||||
* model: myModel,
|
||||
* getApiKey: async () => process.env.MY_KEY,
|
||||
* tools: [readTool, bashTool],
|
||||
* hooks: [],
|
||||
* skills: [],
|
||||
* sessionFile: false,
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { Agent, ProviderTransport, type ThinkingLevel } from "@mariozechner/pi-agent-core";
|
||||
import type { Model } from "@mariozechner/pi-ai";
|
||||
import { getAgentDir } from "../config.js";
|
||||
import { AgentSession } from "./agent-session.js";
|
||||
import { discoverAndLoadCustomTools, type LoadedCustomTool } from "./custom-tools/index.js";
|
||||
import type { CustomAgentTool } from "./custom-tools/types.js";
|
||||
import { discoverAndLoadHooks, HookRunner, type LoadedHook, wrapToolsWithHooks } from "./hooks/index.js";
|
||||
import type { HookFactory } from "./hooks/types.js";
|
||||
import { messageTransformer } from "./messages.js";
|
||||
import {
|
||||
findModel as findModelInternal,
|
||||
getApiKeyForModel,
|
||||
getAvailableModels,
|
||||
loadAndMergeModels,
|
||||
} from "./model-config.js";
|
||||
import { SessionManager } from "./session-manager.js";
|
||||
import { type Settings, SettingsManager, type SkillsSettings } from "./settings-manager.js";
|
||||
import { loadSkills as loadSkillsInternal, type Skill } from "./skills.js";
|
||||
import { type FileSlashCommand, loadSlashCommands as loadSlashCommandsInternal } from "./slash-commands.js";
|
||||
import {
|
||||
buildSystemPrompt as buildSystemPromptInternal,
|
||||
loadProjectContextFiles as loadContextFilesInternal,
|
||||
} from "./system-prompt.js";
|
||||
import {
|
||||
allTools,
|
||||
bashTool,
|
||||
codingTools,
|
||||
editTool,
|
||||
findTool,
|
||||
grepTool,
|
||||
lsTool,
|
||||
readOnlyTools,
|
||||
readTool,
|
||||
type Tool,
|
||||
writeTool,
|
||||
} from "./tools/index.js";
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export interface CreateAgentSessionOptions {
|
||||
// === Environment ===
|
||||
/** Working directory for project-local discovery. Default: process.cwd() */
|
||||
cwd?: string;
|
||||
/** Global config directory. Default: ~/.pi/agent */
|
||||
agentDir?: string;
|
||||
|
||||
// === Model & Thinking ===
|
||||
/** Model to use. Default: from settings, else first available */
|
||||
model?: Model<any>;
|
||||
/** Thinking level. Default: from settings, else 'off' (clamped to model capabilities) */
|
||||
thinkingLevel?: ThinkingLevel;
|
||||
|
||||
// === API Key ===
|
||||
/** API key resolver. Default: defaultGetApiKey() */
|
||||
getApiKey?: (model: Model<any>) => Promise<string | undefined>;
|
||||
|
||||
// === System Prompt ===
|
||||
/** System prompt. String replaces default, function receives default and returns final. */
|
||||
systemPrompt?: string | ((defaultPrompt: string) => string);
|
||||
|
||||
// === Tools ===
|
||||
/** Built-in tools to use. Default: codingTools [read, bash, edit, write] */
|
||||
tools?: Tool[];
|
||||
/** Custom tools. Default: discovered from cwd/.pi/tools/ + agentDir/tools/ */
|
||||
customTools?: Array<{ path?: string; tool: CustomAgentTool }>;
|
||||
|
||||
// === Hooks ===
|
||||
/** Hooks. Default: discovered from cwd/.pi/hooks/ + agentDir/hooks/ */
|
||||
hooks?: Array<{ path?: string; factory: HookFactory }>;
|
||||
|
||||
// === Context ===
|
||||
/** Skills. Default: discovered from multiple locations */
|
||||
skills?: Skill[];
|
||||
/** Context files (AGENTS.md content). Default: discovered walking up from cwd */
|
||||
contextFiles?: Array<{ path: string; content: string }>;
|
||||
/** Slash commands. Default: discovered from cwd/.pi/commands/ + agentDir/commands/ */
|
||||
slashCommands?: FileSlashCommand[];
|
||||
|
||||
// === Session ===
|
||||
/** Session file path, or false to disable persistence. Default: auto in agentDir/sessions/ */
|
||||
sessionFile?: string | false;
|
||||
|
||||
// === Settings ===
|
||||
/** Settings overrides (merged with agentDir/settings.json) */
|
||||
settings?: Partial<Settings>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Re-exports
|
||||
// ============================================================================
|
||||
|
||||
export type { CustomAgentTool } from "./custom-tools/types.js";
|
||||
export type { HookAPI, HookFactory } from "./hooks/types.js";
|
||||
export type { Settings, SkillsSettings } from "./settings-manager.js";
|
||||
export type { Skill } from "./skills.js";
|
||||
export type { FileSlashCommand } from "./slash-commands.js";
|
||||
export type { Tool } from "./tools/index.js";
|
||||
|
||||
export {
|
||||
readTool,
|
||||
bashTool,
|
||||
editTool,
|
||||
writeTool,
|
||||
grepTool,
|
||||
findTool,
|
||||
lsTool,
|
||||
codingTools,
|
||||
readOnlyTools,
|
||||
allTools as allBuiltInTools,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
function getDefaultAgentDir(): string {
|
||||
return getAgentDir();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Discovery Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get all models (built-in + custom from models.json).
|
||||
* Note: Uses default agentDir for models.json location.
|
||||
*/
|
||||
export function discoverModels(): Model<any>[] {
|
||||
const { models, error } = loadAndMergeModels();
|
||||
if (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
return models;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get models that have valid API keys available.
|
||||
* Note: Uses default agentDir for models.json and oauth.json location.
|
||||
*/
|
||||
export async function discoverAvailableModels(): Promise<Model<any>[]> {
|
||||
const { models, error } = await getAvailableModels();
|
||||
if (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
return models;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a model by provider and ID.
|
||||
* Note: Uses default agentDir for models.json location.
|
||||
* @returns The model, or null if not found
|
||||
*/
|
||||
export function findModel(provider: string, modelId: string): Model<any> | null {
|
||||
const { model, error } = findModelInternal(provider, modelId);
|
||||
if (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover hooks from cwd and agentDir.
|
||||
*/
|
||||
export async function discoverHooks(
|
||||
cwd?: string,
|
||||
agentDir?: string,
|
||||
): Promise<Array<{ path: string; factory: HookFactory }>> {
|
||||
const resolvedCwd = cwd ?? process.cwd();
|
||||
const resolvedAgentDir = agentDir ?? getDefaultAgentDir();
|
||||
|
||||
const { hooks, errors } = await discoverAndLoadHooks([], resolvedCwd, resolvedAgentDir);
|
||||
|
||||
// Log errors but don't fail
|
||||
for (const { path, error } of errors) {
|
||||
console.error(`Failed to load hook "${path}": ${error}`);
|
||||
}
|
||||
|
||||
return hooks.map((h) => ({
|
||||
path: h.path,
|
||||
factory: createFactoryFromLoadedHook(h),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover custom tools from cwd and agentDir.
|
||||
*/
|
||||
export async function discoverCustomTools(
|
||||
cwd?: string,
|
||||
agentDir?: string,
|
||||
): Promise<Array<{ path: string; tool: CustomAgentTool }>> {
|
||||
const resolvedCwd = cwd ?? process.cwd();
|
||||
const resolvedAgentDir = agentDir ?? getDefaultAgentDir();
|
||||
|
||||
const { tools, errors } = await discoverAndLoadCustomTools([], resolvedCwd, Object.keys(allTools), resolvedAgentDir);
|
||||
|
||||
// Log errors but don't fail
|
||||
for (const { path, error } of errors) {
|
||||
console.error(`Failed to load custom tool "${path}": ${error}`);
|
||||
}
|
||||
|
||||
return tools.map((t) => ({
|
||||
path: t.path,
|
||||
tool: t.tool,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover skills from cwd and agentDir.
|
||||
*/
|
||||
export function discoverSkills(cwd?: string, agentDir?: string, settings?: SkillsSettings): Skill[] {
|
||||
const { skills } = loadSkillsInternal({
|
||||
...settings,
|
||||
cwd: cwd ?? process.cwd(),
|
||||
agentDir: agentDir ?? getDefaultAgentDir(),
|
||||
});
|
||||
return skills;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover context files (AGENTS.md) walking up from cwd.
|
||||
*/
|
||||
export function discoverContextFiles(cwd?: string, agentDir?: string): Array<{ path: string; content: string }> {
|
||||
return loadContextFilesInternal({
|
||||
cwd: cwd ?? process.cwd(),
|
||||
agentDir: agentDir ?? getDefaultAgentDir(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover slash commands from cwd and agentDir.
|
||||
*/
|
||||
export function discoverSlashCommands(cwd?: string, agentDir?: string): FileSlashCommand[] {
|
||||
return loadSlashCommandsInternal({
|
||||
cwd: cwd ?? process.cwd(),
|
||||
agentDir: agentDir ?? getDefaultAgentDir(),
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// API Key Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Create the default API key resolver.
|
||||
* Checks custom providers (models.json), OAuth, and environment variables.
|
||||
* Note: Uses default agentDir for models.json and oauth.json location.
|
||||
*/
|
||||
export function defaultGetApiKey(): (model: Model<any>) => Promise<string | undefined> {
|
||||
return getApiKeyForModel;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// System Prompt
|
||||
// ============================================================================
|
||||
|
||||
export interface BuildSystemPromptOptions {
|
||||
tools?: Tool[];
|
||||
skills?: Skill[];
|
||||
contextFiles?: Array<{ path: string; content: string }>;
|
||||
cwd?: string;
|
||||
appendPrompt?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the default system prompt.
|
||||
*/
|
||||
export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): string {
|
||||
return buildSystemPromptInternal({
|
||||
cwd: options.cwd,
|
||||
skills: options.skills,
|
||||
contextFiles: options.contextFiles,
|
||||
appendSystemPrompt: options.appendPrompt,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Settings
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Load settings from agentDir/settings.json.
|
||||
*/
|
||||
export function loadSettings(agentDir?: string): Settings {
|
||||
const manager = new SettingsManager(agentDir ?? getDefaultAgentDir());
|
||||
return {
|
||||
defaultProvider: manager.getDefaultProvider(),
|
||||
defaultModel: manager.getDefaultModel(),
|
||||
defaultThinkingLevel: manager.getDefaultThinkingLevel(),
|
||||
queueMode: manager.getQueueMode(),
|
||||
theme: manager.getTheme(),
|
||||
compaction: manager.getCompactionSettings(),
|
||||
retry: manager.getRetrySettings(),
|
||||
hideThinkingBlock: manager.getHideThinkingBlock(),
|
||||
shellPath: manager.getShellPath(),
|
||||
collapseChangelog: manager.getCollapseChangelog(),
|
||||
hooks: manager.getHookPaths(),
|
||||
hookTimeout: manager.getHookTimeout(),
|
||||
customTools: manager.getCustomToolPaths(),
|
||||
skills: manager.getSkillsSettings(),
|
||||
terminal: { showImages: manager.getShowImages() },
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Internal Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Create a HookFactory from a LoadedHook.
|
||||
* This allows mixing discovered hooks with inline hooks.
|
||||
*/
|
||||
function createFactoryFromLoadedHook(loaded: LoadedHook): HookFactory {
|
||||
return (api) => {
|
||||
for (const [eventType, handlers] of loaded.handlers) {
|
||||
for (const handler of handlers) {
|
||||
api.on(eventType as any, handler as any);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert hook definitions to LoadedHooks for the HookRunner.
|
||||
*/
|
||||
function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; factory: HookFactory }>): LoadedHook[] {
|
||||
return definitions.map((def) => {
|
||||
const handlers = new Map<string, Array<(...args: unknown[]) => Promise<unknown>>>();
|
||||
let sendHandler: (text: string, attachments?: any[]) => void = () => {};
|
||||
|
||||
const api = {
|
||||
on: (event: string, handler: (...args: unknown[]) => Promise<unknown>) => {
|
||||
const list = handlers.get(event) ?? [];
|
||||
list.push(handler);
|
||||
handlers.set(event, list);
|
||||
},
|
||||
send: (text: string, attachments?: any[]) => {
|
||||
sendHandler(text, attachments);
|
||||
},
|
||||
};
|
||||
|
||||
def.factory(api as any);
|
||||
|
||||
return {
|
||||
path: def.path ?? "<inline>",
|
||||
resolvedPath: def.path ?? "<inline>",
|
||||
handlers,
|
||||
setSendHandler: (handler: (text: string, attachments?: any[]) => void) => {
|
||||
sendHandler = handler;
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Factory
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Create an AgentSession with the specified options.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Minimal - uses defaults
|
||||
* const session = await createAgentSession();
|
||||
*
|
||||
* // With explicit model
|
||||
* const session = await createAgentSession({
|
||||
* model: findModel('anthropic', 'claude-sonnet-4-20250514'),
|
||||
* thinkingLevel: 'high',
|
||||
* });
|
||||
*
|
||||
* // Full control
|
||||
* const session = await createAgentSession({
|
||||
* model: myModel,
|
||||
* getApiKey: async () => process.env.MY_KEY,
|
||||
* systemPrompt: 'You are helpful.',
|
||||
* tools: [readTool, bashTool],
|
||||
* hooks: [],
|
||||
* skills: [],
|
||||
* sessionFile: false,
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<AgentSession> {
|
||||
const cwd = options.cwd ?? process.cwd();
|
||||
const agentDir = options.agentDir ?? getDefaultAgentDir();
|
||||
|
||||
// === Settings ===
|
||||
const settingsManager = new SettingsManager(agentDir);
|
||||
|
||||
// === Model Resolution ===
|
||||
let model = options.model;
|
||||
if (!model) {
|
||||
// Try settings default
|
||||
const defaultProvider = settingsManager.getDefaultProvider();
|
||||
const defaultModelId = settingsManager.getDefaultModel();
|
||||
if (defaultProvider && defaultModelId) {
|
||||
model = findModel(defaultProvider, defaultModelId) ?? undefined;
|
||||
// Verify it has an API key
|
||||
if (model) {
|
||||
const key = await getApiKeyForModel(model);
|
||||
if (!key) {
|
||||
model = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to first available
|
||||
if (!model) {
|
||||
const available = await discoverAvailableModels();
|
||||
if (available.length === 0) {
|
||||
throw new Error(
|
||||
"No models available. Set an API key environment variable " +
|
||||
"(ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.) or provide a model explicitly.",
|
||||
);
|
||||
}
|
||||
model = available[0];
|
||||
}
|
||||
}
|
||||
|
||||
// === Thinking Level Resolution ===
|
||||
let thinkingLevel = options.thinkingLevel;
|
||||
if (thinkingLevel === undefined) {
|
||||
thinkingLevel = settingsManager.getDefaultThinkingLevel() ?? "off";
|
||||
}
|
||||
// Clamp to model capabilities
|
||||
if (!model.reasoning) {
|
||||
thinkingLevel = "off";
|
||||
}
|
||||
|
||||
// === API Key Resolver ===
|
||||
const getApiKey = options.getApiKey ?? defaultGetApiKey();
|
||||
|
||||
// === Skills ===
|
||||
const skills = options.skills ?? discoverSkills(cwd, agentDir, settingsManager.getSkillsSettings());
|
||||
|
||||
// === Context Files ===
|
||||
const contextFiles = options.contextFiles ?? discoverContextFiles(cwd, agentDir);
|
||||
|
||||
// === Tools ===
|
||||
const builtInTools = options.tools ?? codingTools;
|
||||
|
||||
// === Custom Tools ===
|
||||
let customToolsResult: { tools: LoadedCustomTool[]; setUIContext: (ctx: any, hasUI: boolean) => void };
|
||||
if (options.customTools !== undefined) {
|
||||
// Use provided custom tools
|
||||
const loadedTools: LoadedCustomTool[] = options.customTools.map((ct) => ({
|
||||
path: ct.path ?? "<inline>",
|
||||
resolvedPath: ct.path ?? "<inline>",
|
||||
tool: ct.tool,
|
||||
}));
|
||||
customToolsResult = {
|
||||
tools: loadedTools,
|
||||
setUIContext: () => {},
|
||||
};
|
||||
} else {
|
||||
// Discover custom tools
|
||||
const result = await discoverAndLoadCustomTools(
|
||||
settingsManager.getCustomToolPaths(),
|
||||
cwd,
|
||||
Object.keys(allTools),
|
||||
agentDir,
|
||||
);
|
||||
for (const { path, error } of result.errors) {
|
||||
console.error(`Failed to load custom tool "${path}": ${error}`);
|
||||
}
|
||||
customToolsResult = result;
|
||||
}
|
||||
|
||||
// === Hooks ===
|
||||
let hookRunner: HookRunner | null = null;
|
||||
if (options.hooks !== undefined) {
|
||||
if (options.hooks.length > 0) {
|
||||
const loadedHooks = createLoadedHooksFromDefinitions(options.hooks);
|
||||
hookRunner = new HookRunner(loadedHooks, cwd, settingsManager.getHookTimeout());
|
||||
}
|
||||
} else {
|
||||
// Discover hooks
|
||||
const { hooks, errors } = await discoverAndLoadHooks(settingsManager.getHookPaths(), cwd, agentDir);
|
||||
for (const { path, error } of errors) {
|
||||
console.error(`Failed to load hook "${path}": ${error}`);
|
||||
}
|
||||
if (hooks.length > 0) {
|
||||
hookRunner = new HookRunner(hooks, cwd, settingsManager.getHookTimeout());
|
||||
}
|
||||
}
|
||||
|
||||
// === Combine and wrap tools ===
|
||||
let allToolsArray: Tool[] = [...builtInTools, ...customToolsResult.tools.map((lt) => lt.tool as unknown as Tool)];
|
||||
if (hookRunner) {
|
||||
allToolsArray = wrapToolsWithHooks(allToolsArray, hookRunner) as Tool[];
|
||||
}
|
||||
|
||||
// === System Prompt ===
|
||||
let systemPrompt: string;
|
||||
const defaultPrompt = buildSystemPromptInternal({
|
||||
cwd,
|
||||
agentDir,
|
||||
skills,
|
||||
contextFiles,
|
||||
});
|
||||
|
||||
if (options.systemPrompt === undefined) {
|
||||
systemPrompt = defaultPrompt;
|
||||
} else if (typeof options.systemPrompt === "string") {
|
||||
systemPrompt = options.systemPrompt;
|
||||
} else {
|
||||
systemPrompt = options.systemPrompt(defaultPrompt);
|
||||
}
|
||||
|
||||
// === Slash Commands ===
|
||||
const slashCommands = options.slashCommands ?? discoverSlashCommands(cwd, agentDir);
|
||||
|
||||
// === Session Manager ===
|
||||
const sessionManager = new SessionManager(false, undefined);
|
||||
if (options.sessionFile === false) {
|
||||
sessionManager.disable();
|
||||
} else if (typeof options.sessionFile === "string") {
|
||||
sessionManager.setSessionFile(options.sessionFile);
|
||||
}
|
||||
// If undefined, SessionManager uses auto-detection based on cwd
|
||||
|
||||
// === Create Agent ===
|
||||
const agent = new Agent({
|
||||
initialState: {
|
||||
systemPrompt,
|
||||
model,
|
||||
thinkingLevel,
|
||||
tools: allToolsArray,
|
||||
},
|
||||
messageTransformer,
|
||||
queueMode: settingsManager.getQueueMode(),
|
||||
transport: new ProviderTransport({
|
||||
getApiKey: async () => {
|
||||
const currentModel = agent.state.model;
|
||||
if (!currentModel) {
|
||||
throw new Error("No model selected");
|
||||
}
|
||||
const key = await getApiKey(currentModel);
|
||||
if (!key) {
|
||||
throw new Error(`No API key found for provider "${currentModel.provider}"`);
|
||||
}
|
||||
return key;
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// === Create Session ===
|
||||
const session = new AgentSession({
|
||||
agent,
|
||||
sessionManager,
|
||||
settingsManager,
|
||||
fileCommands: slashCommands,
|
||||
hookRunner,
|
||||
customTools: customToolsResult.tools,
|
||||
skillsSettings: settingsManager.getSkillsSettings(),
|
||||
});
|
||||
|
||||
return session;
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import { existsSync, readdirSync, readFileSync } from "fs";
|
|||
import { minimatch } from "minimatch";
|
||||
import { homedir } from "os";
|
||||
import { basename, dirname, join, resolve } from "path";
|
||||
import { CONFIG_DIR_NAME } from "../config.js";
|
||||
import { CONFIG_DIR_NAME, getAgentDir } from "../config.js";
|
||||
import type { SkillsSettings } from "./settings-manager.js";
|
||||
|
||||
/**
|
||||
|
|
@ -313,12 +313,21 @@ function escapeXml(str: string): string {
|
|||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
export interface LoadSkillsOptions extends SkillsSettings {
|
||||
/** Working directory for project-local skills. Default: process.cwd() */
|
||||
cwd?: string;
|
||||
/** Agent config directory for global skills. Default: ~/.pi/agent */
|
||||
agentDir?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load skills from all configured locations.
|
||||
* Returns skills and any validation warnings.
|
||||
*/
|
||||
export function loadSkills(options: SkillsSettings = {}): LoadSkillsResult {
|
||||
export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {
|
||||
const {
|
||||
cwd = process.cwd(),
|
||||
agentDir,
|
||||
enableCodexUser = true,
|
||||
enableClaudeUser = true,
|
||||
enableClaudeProject = true,
|
||||
|
|
@ -329,6 +338,9 @@ export function loadSkills(options: SkillsSettings = {}): LoadSkillsResult {
|
|||
includeSkills = [],
|
||||
} = options;
|
||||
|
||||
// Resolve agentDir - if not provided, use default from config
|
||||
const resolvedAgentDir = agentDir ?? getAgentDir();
|
||||
|
||||
const skillMap = new Map<string, Skill>();
|
||||
const allWarnings: SkillWarning[] = [];
|
||||
const collisionWarnings: SkillWarning[] = [];
|
||||
|
|
@ -375,13 +387,13 @@ export function loadSkills(options: SkillsSettings = {}): LoadSkillsResult {
|
|||
addSkills(loadSkillsFromDirInternal(join(homedir(), ".claude", "skills"), "claude-user", "claude"));
|
||||
}
|
||||
if (enableClaudeProject) {
|
||||
addSkills(loadSkillsFromDirInternal(resolve(process.cwd(), ".claude", "skills"), "claude-project", "claude"));
|
||||
addSkills(loadSkillsFromDirInternal(resolve(cwd, ".claude", "skills"), "claude-project", "claude"));
|
||||
}
|
||||
if (enablePiUser) {
|
||||
addSkills(loadSkillsFromDirInternal(join(homedir(), CONFIG_DIR_NAME, "agent", "skills"), "user", "recursive"));
|
||||
addSkills(loadSkillsFromDirInternal(join(resolvedAgentDir, "skills"), "user", "recursive"));
|
||||
}
|
||||
if (enablePiProject) {
|
||||
addSkills(loadSkillsFromDirInternal(resolve(process.cwd(), CONFIG_DIR_NAME, "skills"), "project", "recursive"));
|
||||
addSkills(loadSkillsFromDirInternal(resolve(cwd, CONFIG_DIR_NAME, "skills"), "project", "recursive"));
|
||||
}
|
||||
for (const customDir of customDirectories) {
|
||||
addSkills(loadSkillsFromDirInternal(customDir.replace(/^~(?=$|[\\/])/, homedir()), "custom", "recursive"));
|
||||
|
|
|
|||
|
|
@ -165,20 +165,31 @@ function loadCommandsFromDir(dir: string, source: "user" | "project", subdir: st
|
|||
return commands;
|
||||
}
|
||||
|
||||
export interface LoadSlashCommandsOptions {
|
||||
/** Working directory for project-local commands. Default: process.cwd() */
|
||||
cwd?: string;
|
||||
/** Agent config directory for global commands. Default: from getCommandsDir() */
|
||||
agentDir?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all custom slash commands from:
|
||||
* 1. Global: ~/{CONFIG_DIR_NAME}/agent/commands/
|
||||
* 2. Project: ./{CONFIG_DIR_NAME}/commands/
|
||||
* 1. Global: agentDir/commands/
|
||||
* 2. Project: cwd/{CONFIG_DIR_NAME}/commands/
|
||||
*/
|
||||
export function loadSlashCommands(): FileSlashCommand[] {
|
||||
export function loadSlashCommands(options: LoadSlashCommandsOptions = {}): FileSlashCommand[] {
|
||||
const resolvedCwd = options.cwd ?? process.cwd();
|
||||
const resolvedAgentDir = options.agentDir ?? getCommandsDir();
|
||||
|
||||
const commands: FileSlashCommand[] = [];
|
||||
|
||||
// 1. Load global commands from ~/{CONFIG_DIR_NAME}/agent/commands/
|
||||
const globalCommandsDir = getCommandsDir();
|
||||
// 1. Load global commands from agentDir/commands/
|
||||
// Note: if agentDir is provided, it should be the agent dir, not the commands dir
|
||||
const globalCommandsDir = options.agentDir ? join(options.agentDir, "commands") : resolvedAgentDir;
|
||||
commands.push(...loadCommandsFromDir(globalCommandsDir, "user"));
|
||||
|
||||
// 2. Load project commands from ./{CONFIG_DIR_NAME}/commands/
|
||||
const projectCommandsDir = resolve(process.cwd(), CONFIG_DIR_NAME, "commands");
|
||||
// 2. Load project commands from cwd/{CONFIG_DIR_NAME}/commands/
|
||||
const projectCommandsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, "commands");
|
||||
commands.push(...loadCommandsFromDir(projectCommandsDir, "project"));
|
||||
|
||||
return commands;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { existsSync, readFileSync } from "fs";
|
|||
import { join, resolve } from "path";
|
||||
import { getAgentDir, getDocsPath, getReadmePath } from "../config.js";
|
||||
import type { SkillsSettings } from "./settings-manager.js";
|
||||
import { formatSkillsForPrompt, loadSkills } from "./skills.js";
|
||||
import { formatSkillsForPrompt, loadSkills, type Skill } from "./skills.js";
|
||||
import type { ToolName } from "./tools/index.js";
|
||||
|
||||
/** Tool descriptions for system prompt */
|
||||
|
|
@ -58,29 +58,39 @@ function loadContextFileFromDir(dir: string): { path: string; content: string }
|
|||
return null;
|
||||
}
|
||||
|
||||
export interface LoadContextFilesOptions {
|
||||
/** Working directory to start walking up from. Default: process.cwd() */
|
||||
cwd?: string;
|
||||
/** Agent config directory for global context. Default: from getAgentDir() */
|
||||
agentDir?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all project context files in order:
|
||||
* 1. Global: ~/{CONFIG_DIR_NAME}/agent/AGENTS.md or CLAUDE.md
|
||||
* 1. Global: agentDir/AGENTS.md or CLAUDE.md
|
||||
* 2. Parent directories (top-most first) down to cwd
|
||||
* Each returns {path, content} for separate messages
|
||||
*/
|
||||
export function loadProjectContextFiles(): Array<{ path: string; content: string }> {
|
||||
export function loadProjectContextFiles(
|
||||
options: LoadContextFilesOptions = {},
|
||||
): Array<{ path: string; content: string }> {
|
||||
const resolvedCwd = options.cwd ?? process.cwd();
|
||||
const resolvedAgentDir = options.agentDir ?? getAgentDir();
|
||||
|
||||
const contextFiles: Array<{ path: string; content: string }> = [];
|
||||
const seenPaths = new Set<string>();
|
||||
|
||||
// 1. Load global context from ~/{CONFIG_DIR_NAME}/agent/
|
||||
const globalContextDir = getAgentDir();
|
||||
const globalContext = loadContextFileFromDir(globalContextDir);
|
||||
// 1. Load global context from agentDir
|
||||
const globalContext = loadContextFileFromDir(resolvedAgentDir);
|
||||
if (globalContext) {
|
||||
contextFiles.push(globalContext);
|
||||
seenPaths.add(globalContext.path);
|
||||
}
|
||||
|
||||
// 2. Walk up from cwd to root, collecting all context files
|
||||
const cwd = process.cwd();
|
||||
const ancestorContextFiles: Array<{ path: string; content: string }> = [];
|
||||
|
||||
let currentDir = cwd;
|
||||
let currentDir = resolvedCwd;
|
||||
const root = resolve("/");
|
||||
|
||||
while (true) {
|
||||
|
|
@ -107,15 +117,37 @@ export function loadProjectContextFiles(): Array<{ path: string; content: string
|
|||
}
|
||||
|
||||
export interface BuildSystemPromptOptions {
|
||||
/** Custom system prompt (replaces default). */
|
||||
customPrompt?: string;
|
||||
/** Tools to include in prompt. Default: [read, bash, edit, write] */
|
||||
selectedTools?: ToolName[];
|
||||
/** Text to append to system prompt. */
|
||||
appendSystemPrompt?: string;
|
||||
/** Skills settings for discovery. */
|
||||
skillsSettings?: SkillsSettings;
|
||||
/** Working directory. Default: process.cwd() */
|
||||
cwd?: string;
|
||||
/** Agent config directory. Default: from getAgentDir() */
|
||||
agentDir?: string;
|
||||
/** Pre-loaded context files (skips discovery if provided). */
|
||||
contextFiles?: Array<{ path: string; content: string }>;
|
||||
/** Pre-loaded skills (skips discovery if provided). */
|
||||
skills?: Skill[];
|
||||
}
|
||||
|
||||
/** Build the system prompt with tools, guidelines, and context */
|
||||
export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): string {
|
||||
const { customPrompt, selectedTools, appendSystemPrompt, skillsSettings } = options;
|
||||
const {
|
||||
customPrompt,
|
||||
selectedTools,
|
||||
appendSystemPrompt,
|
||||
skillsSettings,
|
||||
cwd,
|
||||
agentDir,
|
||||
contextFiles: providedContextFiles,
|
||||
skills: providedSkills,
|
||||
} = options;
|
||||
const resolvedCwd = cwd ?? process.cwd();
|
||||
const resolvedCustomPrompt = resolvePromptInput(customPrompt, "system prompt");
|
||||
const resolvedAppendPrompt = resolvePromptInput(appendSystemPrompt, "append system prompt");
|
||||
|
||||
|
|
@ -133,6 +165,14 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|||
|
||||
const appendSection = resolvedAppendPrompt ? `\n\n${resolvedAppendPrompt}` : "";
|
||||
|
||||
// Resolve context files: use provided or discover
|
||||
const contextFiles = providedContextFiles ?? loadProjectContextFiles({ cwd: resolvedCwd, agentDir });
|
||||
|
||||
// Resolve skills: use provided or discover
|
||||
const skills =
|
||||
providedSkills ??
|
||||
(skillsSettings?.enabled !== false ? loadSkills({ ...skillsSettings, cwd: resolvedCwd, agentDir }).skills : []);
|
||||
|
||||
if (resolvedCustomPrompt) {
|
||||
let prompt = resolvedCustomPrompt;
|
||||
|
||||
|
|
@ -141,7 +181,6 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|||
}
|
||||
|
||||
// Append project context files
|
||||
const contextFiles = loadProjectContextFiles();
|
||||
if (contextFiles.length > 0) {
|
||||
prompt += "\n\n# Project Context\n\n";
|
||||
prompt += "The following project context files have been loaded:\n\n";
|
||||
|
|
@ -152,14 +191,13 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|||
|
||||
// Append skills section (only if read tool is available)
|
||||
const customPromptHasRead = !selectedTools || selectedTools.includes("read");
|
||||
if (skillsSettings?.enabled !== false && customPromptHasRead) {
|
||||
const { skills } = loadSkills(skillsSettings ?? {});
|
||||
if (customPromptHasRead && skills.length > 0) {
|
||||
prompt += formatSkillsForPrompt(skills);
|
||||
}
|
||||
|
||||
// Add date/time and working directory last
|
||||
prompt += `\nCurrent date and time: ${dateTime}`;
|
||||
prompt += `\nCurrent working directory: ${process.cwd()}`;
|
||||
prompt += `\nCurrent working directory: ${resolvedCwd}`;
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
|
@ -248,7 +286,6 @@ Documentation:
|
|||
}
|
||||
|
||||
// Append project context files
|
||||
const contextFiles = loadProjectContextFiles();
|
||||
if (contextFiles.length > 0) {
|
||||
prompt += "\n\n# Project Context\n\n";
|
||||
prompt += "The following project context files have been loaded:\n\n";
|
||||
|
|
@ -258,14 +295,13 @@ Documentation:
|
|||
}
|
||||
|
||||
// Append skills section (only if read tool is available)
|
||||
if (skillsSettings?.enabled !== false && hasRead) {
|
||||
const { skills } = loadSkills(skillsSettings ?? {});
|
||||
if (hasRead && skills.length > 0) {
|
||||
prompt += formatSkillsForPrompt(skills);
|
||||
}
|
||||
|
||||
// Add date/time and working directory last
|
||||
prompt += `\nCurrent date and time: ${dateTime}`;
|
||||
prompt += `\nCurrent working directory: ${process.cwd()}`;
|
||||
prompt += `\nCurrent working directory: ${resolvedCwd}`;
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import type { AgentTool } from "@mariozechner/pi-ai";
|
||||
|
||||
export { type BashToolDetails, bashTool } from "./bash.js";
|
||||
export { editTool } from "./edit.js";
|
||||
export { type FindToolDetails, findTool } from "./find.js";
|
||||
|
|
@ -15,8 +17,14 @@ import { lsTool } from "./ls.js";
|
|||
import { readTool } from "./read.js";
|
||||
import { writeTool } from "./write.js";
|
||||
|
||||
/** Tool type (AgentTool from pi-ai) */
|
||||
export type Tool = AgentTool<any>;
|
||||
|
||||
// Default tools for full access mode
|
||||
export const codingTools = [readTool, bashTool, editTool, writeTool];
|
||||
export const codingTools: Tool[] = [readTool, bashTool, editTool, writeTool];
|
||||
|
||||
// Read-only tools for exploration without modification
|
||||
export const readOnlyTools: Tool[] = [readTool, grepTool, findTool, lsTool];
|
||||
|
||||
// All available tools (including read-only exploration tools)
|
||||
export const allTools = {
|
||||
|
|
|
|||
|
|
@ -83,6 +83,29 @@ export {
|
|||
type OAuthPrompt,
|
||||
type OAuthProvider,
|
||||
} from "./core/oauth/index.js";
|
||||
// SDK for programmatic usage
|
||||
export {
|
||||
allBuiltInTools,
|
||||
type BuildSystemPromptOptions,
|
||||
buildSystemPrompt,
|
||||
type CreateAgentSessionOptions,
|
||||
// Factory
|
||||
createAgentSession,
|
||||
// Helpers
|
||||
defaultGetApiKey,
|
||||
discoverAvailableModels,
|
||||
discoverContextFiles,
|
||||
discoverCustomTools,
|
||||
discoverHooks,
|
||||
// Discovery
|
||||
discoverModels,
|
||||
discoverSkills,
|
||||
discoverSlashCommands,
|
||||
findModel as findModelByProviderAndId,
|
||||
loadSettings,
|
||||
// Tools
|
||||
readOnlyTools,
|
||||
} from "./core/sdk.js";
|
||||
export {
|
||||
type CompactionEntry,
|
||||
createSummaryMessage,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue