203 lines
7.2 KiB
TypeScript
203 lines
7.2 KiB
TypeScript
/**
|
|
* Agent execution runners
|
|
*/
|
|
|
|
import { existsSync, readFileSync } from 'node:fs';
|
|
import { basename, dirname } from 'node:path';
|
|
import { loadCustomAgents, loadAgentPrompt, loadGlobalConfig, loadProjectConfig } from '../infra/config/index.js';
|
|
import { getProvider, type ProviderType, type ProviderCallOptions } from '../infra/providers/index.js';
|
|
import type { AgentResponse, CustomAgentConfig } from '../core/models/index.js';
|
|
import { resolveAgentProviderModel } from '../core/piece/provider-resolution.js';
|
|
import { createLogger } from '../shared/utils/index.js';
|
|
import { loadTemplate } from '../shared/prompts/index.js';
|
|
import type { RunAgentOptions } from './types.js';
|
|
|
|
export type { RunAgentOptions, StreamCallback } from './types.js';
|
|
|
|
const log = createLogger('runner');
|
|
|
|
/**
|
|
* Agent execution runner.
|
|
*
|
|
* Resolves agent configuration (provider, model, prompt) and
|
|
* delegates execution to the appropriate provider.
|
|
*/
|
|
export class AgentRunner {
|
|
private static resolveProviderAndModel(
|
|
cwd: string,
|
|
personaDisplayName: string | undefined,
|
|
options?: RunAgentOptions,
|
|
): { provider: ProviderType; model: string | undefined } {
|
|
const localConfig = loadProjectConfig(cwd);
|
|
const globalConfig = loadGlobalConfig();
|
|
|
|
const resolvedProviderModel = resolveAgentProviderModel({
|
|
personaDisplayName,
|
|
cliProvider: options?.provider,
|
|
cliModel: options?.model,
|
|
stepProvider: options?.stepProvider,
|
|
stepModel: options?.stepModel,
|
|
personaProviders: globalConfig.personaProviders,
|
|
localProvider: localConfig.provider,
|
|
localModel: localConfig.model,
|
|
globalProvider: globalConfig.provider,
|
|
globalModel: globalConfig.model,
|
|
});
|
|
const resolvedProvider = resolvedProviderModel.provider;
|
|
if (!resolvedProvider) {
|
|
throw new Error('No provider configured. Set "provider" in ~/.takt/config.yaml');
|
|
}
|
|
return {
|
|
provider: resolvedProvider,
|
|
model: resolvedProviderModel.model,
|
|
};
|
|
}
|
|
|
|
/** Load persona prompt from file path */
|
|
private static loadPersonaPromptFromPath(personaPath: string): string {
|
|
if (!existsSync(personaPath)) {
|
|
throw new Error(`Persona file not found: ${personaPath}`);
|
|
}
|
|
return readFileSync(personaPath, 'utf-8');
|
|
}
|
|
|
|
/**
|
|
* Get persona name from path or spec.
|
|
* For personas in subdirectories, includes parent dir for pattern matching.
|
|
*/
|
|
private static extractPersonaName(personaSpec: string): string {
|
|
if (!personaSpec.endsWith('.md')) {
|
|
return personaSpec;
|
|
}
|
|
|
|
const name = basename(personaSpec, '.md');
|
|
const dir = basename(dirname(personaSpec));
|
|
|
|
if (dir === 'personas' || dir === '.') {
|
|
return name;
|
|
}
|
|
|
|
return `${dir}/${name}`;
|
|
}
|
|
|
|
/** Build ProviderCallOptions from RunAgentOptions */
|
|
private static buildCallOptions(
|
|
resolvedModel: string | undefined,
|
|
options: RunAgentOptions,
|
|
agentConfig?: CustomAgentConfig,
|
|
): ProviderCallOptions {
|
|
return {
|
|
cwd: options.cwd,
|
|
abortSignal: options.abortSignal,
|
|
sessionId: options.sessionId,
|
|
allowedTools: options.allowedTools ?? agentConfig?.allowedTools,
|
|
mcpServers: options.mcpServers,
|
|
maxTurns: options.maxTurns,
|
|
model: resolvedModel,
|
|
permissionMode: options.permissionMode,
|
|
providerOptions: options.providerOptions,
|
|
onStream: options.onStream,
|
|
onPermissionRequest: options.onPermissionRequest,
|
|
onAskUserQuestion: options.onAskUserQuestion,
|
|
bypassPermissions: options.bypassPermissions,
|
|
outputSchema: options.outputSchema,
|
|
};
|
|
}
|
|
|
|
/** Run a custom agent */
|
|
async runCustom(
|
|
agentConfig: CustomAgentConfig,
|
|
task: string,
|
|
options: RunAgentOptions,
|
|
): Promise<AgentResponse> {
|
|
const resolved = AgentRunner.resolveProviderAndModel(options.cwd, agentConfig.name, options);
|
|
const providerType = resolved.provider;
|
|
const provider = getProvider(providerType);
|
|
|
|
const agent = provider.setup({
|
|
name: agentConfig.name,
|
|
systemPrompt: agentConfig.claudeAgent || agentConfig.claudeSkill
|
|
? undefined
|
|
: loadAgentPrompt(agentConfig, options.cwd),
|
|
claudeAgent: agentConfig.claudeAgent,
|
|
claudeSkill: agentConfig.claudeSkill,
|
|
});
|
|
|
|
return agent.call(task, AgentRunner.buildCallOptions(resolved.model, options, agentConfig));
|
|
}
|
|
|
|
/** Run an agent by name, path, inline prompt string, or no agent at all */
|
|
async run(
|
|
personaSpec: string | undefined,
|
|
task: string,
|
|
options: RunAgentOptions,
|
|
): Promise<AgentResponse> {
|
|
const personaName = personaSpec ? AgentRunner.extractPersonaName(personaSpec) : 'default';
|
|
log.debug('Running agent', {
|
|
personaSpec: personaSpec ?? '(none)',
|
|
personaName,
|
|
provider: options.provider,
|
|
model: options.model,
|
|
hasPersonaPath: !!options.personaPath,
|
|
hasSession: !!options.sessionId,
|
|
permissionMode: options.permissionMode,
|
|
});
|
|
|
|
const resolved = AgentRunner.resolveProviderAndModel(options.cwd, personaName, options);
|
|
const providerType = resolved.provider;
|
|
const provider = getProvider(providerType);
|
|
const callOptions = AgentRunner.buildCallOptions(resolved.model, options);
|
|
|
|
// 1. If personaPath is provided (resolved file exists), load prompt from file
|
|
// and wrap it through the perform_agent_system_prompt template
|
|
if (options.personaPath) {
|
|
const agentDefinition = AgentRunner.loadPersonaPromptFromPath(options.personaPath);
|
|
const language = options.language ?? 'en';
|
|
const templateVars: Record<string, string> = { agentDefinition };
|
|
|
|
if (options.pieceMeta) {
|
|
templateVars.pieceName = options.pieceMeta.pieceName;
|
|
templateVars.pieceDescription = options.pieceMeta.pieceDescription ?? '';
|
|
templateVars.currentMovement = options.pieceMeta.currentMovement;
|
|
templateVars.movementsList = options.pieceMeta.movementsList
|
|
.map((m, i) => `${i + 1}. ${m.name}${m.description ? ` - ${m.description}` : ''}`)
|
|
.join('\n');
|
|
templateVars.currentPosition = options.pieceMeta.currentPosition;
|
|
}
|
|
|
|
const systemPrompt = loadTemplate('perform_agent_system_prompt', language, templateVars);
|
|
const agent = provider.setup({ name: personaName, systemPrompt });
|
|
return agent.call(task, callOptions);
|
|
}
|
|
|
|
// 2. If personaSpec is provided but no personaPath (file not found), try custom agent first,
|
|
// then use the string as inline system prompt
|
|
if (personaSpec) {
|
|
const customAgents = loadCustomAgents();
|
|
const agentConfig = customAgents.get(personaName);
|
|
if (agentConfig) {
|
|
return this.runCustom(agentConfig, task, options);
|
|
}
|
|
|
|
const agent = provider.setup({ name: personaName, systemPrompt: personaSpec });
|
|
return agent.call(task, callOptions);
|
|
}
|
|
|
|
// 3. No persona specified — run with instruction_template only (no system prompt)
|
|
const agent = provider.setup({ name: personaName });
|
|
return agent.call(task, callOptions);
|
|
}
|
|
}
|
|
|
|
// ---- Module-level function facade ----
|
|
|
|
const defaultRunner = new AgentRunner();
|
|
|
|
export async function runAgent(
|
|
personaSpec: string | undefined,
|
|
task: string,
|
|
options: RunAgentOptions,
|
|
): Promise<AgentResponse> {
|
|
return defaultRunner.run(personaSpec, task, options);
|
|
}
|