| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241 |
- import { spawn } from 'child_process'
- import type {
- Ability,
- Step,
- ScriptStep,
- AbilityExecution,
- StepResult,
- ExecutorContext,
- InputValues,
- } from '../types/index.js'
- import { validateInputs } from '../validator/index.js'
- /**
- * Minimal Executor - Script Steps Only
- *
- * Stripped down to prove core concept:
- * - Execute shell commands sequentially
- * - Track step results
- * - Validate exit codes
- *
- * NO: agent steps, skill steps, approval, workflows, context passing
- */
- function generateExecutionId(): string {
- return `exec_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
- }
- function interpolateVariables(text: string, inputs: InputValues): string {
- return text.replace(/\{\{inputs\.(\w+)\}\}/g, (match, name) => {
- const value = inputs[name]
- return value !== undefined ? String(value) : match
- })
- }
- async function runScript(
- command: string,
- options: { cwd?: string; env?: Record<string, string> }
- ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
- return new Promise((resolve) => {
- const proc = spawn('sh', ['-c', command], {
- cwd: options.cwd || process.cwd(),
- env: { ...process.env, ...options.env },
- })
- let stdout = ''
- let stderr = ''
- proc.stdout.on('data', (data) => {
- stdout += data.toString()
- })
- proc.stderr.on('data', (data) => {
- stderr += data.toString()
- })
- proc.on('close', (code) => {
- resolve({ stdout, stderr, exitCode: code ?? 1 })
- })
- proc.on('error', (error) => {
- resolve({ stdout, stderr: error.message, exitCode: 1 })
- })
- })
- }
- async function executeScriptStep(
- step: ScriptStep,
- execution: AbilityExecution,
- ctx: ExecutorContext
- ): Promise<StepResult> {
- const startedAt = Date.now()
- const command = interpolateVariables(step.run, execution.inputs)
- console.log(`[abilities] Executing: ${command}`)
- try {
- const result = await runScript(command, {
- cwd: step.cwd || ctx.cwd,
- env: { ...ctx.env, ...step.env },
- })
- // Validate exit code if specified
- let failed = false
- let error: string | undefined
- if (step.validation?.exit_code !== undefined && result.exitCode !== step.validation.exit_code) {
- failed = true
- error = `Exit code ${result.exitCode}, expected ${step.validation.exit_code}`
- }
- return {
- stepId: step.id,
- status: failed ? 'failed' : 'completed',
- output: result.stdout || result.stderr,
- error,
- startedAt,
- completedAt: Date.now(),
- duration: Date.now() - startedAt,
- }
- } catch (err) {
- return {
- stepId: step.id,
- status: 'failed',
- error: err instanceof Error ? err.message : String(err),
- startedAt,
- completedAt: Date.now(),
- duration: Date.now() - startedAt,
- }
- }
- }
- function buildExecutionOrder(steps: Step[]): Step[] {
- const result: Step[] = []
- const completed = new Set<string>()
- const remaining = [...steps]
- while (remaining.length > 0) {
- const next = remaining.find((step) => {
- if (!step.needs || step.needs.length === 0) return true
- return step.needs.every((dep) => completed.has(dep))
- })
- if (!next) {
- console.error('[abilities] Unable to resolve step order - circular dependency?')
- break
- }
- result.push(next)
- completed.add(next.id)
- remaining.splice(remaining.indexOf(next), 1)
- }
- return result
- }
- export async function executeAbility(
- ability: Ability,
- inputs: InputValues,
- ctx: ExecutorContext
- ): Promise<AbilityExecution> {
- // Validate inputs
- const inputErrors = validateInputs(ability, inputs)
- if (inputErrors.length > 0) {
- return {
- id: generateExecutionId(),
- ability,
- inputs,
- status: 'failed',
- currentStep: null,
- currentStepIndex: -1,
- completedSteps: [],
- pendingSteps: ability.steps,
- startedAt: Date.now(),
- completedAt: Date.now(),
- error: `Input validation failed: ${inputErrors.map((e) => e.message).join(', ')}`,
- }
- }
- // Apply defaults
- const resolvedInputs: InputValues = { ...inputs }
- if (ability.inputs) {
- for (const [name, def] of Object.entries(ability.inputs)) {
- if (resolvedInputs[name] === undefined && def.default !== undefined) {
- resolvedInputs[name] = def.default
- }
- }
- }
- // Build execution order based on dependencies
- const orderedSteps = buildExecutionOrder(ability.steps)
- const execution: AbilityExecution = {
- id: generateExecutionId(),
- ability,
- inputs: resolvedInputs,
- status: 'running',
- currentStep: null,
- currentStepIndex: -1,
- completedSteps: [],
- pendingSteps: [...orderedSteps],
- startedAt: Date.now(),
- }
- // Execute steps sequentially
- for (let i = 0; i < orderedSteps.length; i++) {
- const step = orderedSteps[i]
- execution.currentStep = step
- execution.currentStepIndex = i
- console.log(`[abilities] Step ${i + 1}/${orderedSteps.length}: ${step.id}`)
- const result = await executeScriptStep(step as ScriptStep, execution, ctx)
- execution.completedSteps.push(result)
- execution.pendingSteps = execution.pendingSteps.filter((s) => s.id !== step.id)
- if (result.status === 'failed') {
- execution.status = 'failed'
- execution.error = result.error
- execution.completedAt = Date.now()
- return execution
- }
- }
- execution.status = 'completed'
- execution.currentStep = null
- execution.completedAt = Date.now()
- return execution
- }
- export function formatExecutionResult(execution: AbilityExecution): string {
- const lines: string[] = []
- lines.push(`Ability: ${execution.ability.name}`)
- lines.push(`Status: ${execution.status === 'completed' ? '✅ Complete' : '❌ Failed'}`)
- if (execution.error) {
- lines.push(`Error: ${execution.error}`)
- }
- lines.push('')
- lines.push('Steps:')
- for (const result of execution.completedSteps) {
- const icon = result.status === 'completed' ? '✅' : '❌'
- const duration = result.duration ? ` (${(result.duration / 1000).toFixed(1)}s)` : ''
- lines.push(` ${icon} ${result.stepId}${duration}`)
- if (result.error) {
- lines.push(` Error: ${result.error}`)
- }
- }
- const totalDuration = execution.completedAt
- ? ((execution.completedAt - execution.startedAt) / 1000).toFixed(1)
- : 'N/A'
- lines.push('')
- lines.push(`Duration: ${totalDuration}s`)
- return lines.join('\n')
- }
|