/** * 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 { 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 { 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 = { 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 { return defaultRunner.run(personaSpec, task, options); }