index.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. import { spawn } from 'child_process'
  2. import type {
  3. Ability,
  4. Step,
  5. ScriptStep,
  6. AbilityExecution,
  7. StepResult,
  8. ExecutorContext,
  9. InputValues,
  10. } from '../types/index.js'
  11. import { validateInputs } from '../validator/index.js'
  12. /**
  13. * Minimal Executor - Script Steps Only
  14. *
  15. * Stripped down to prove core concept:
  16. * - Execute shell commands sequentially
  17. * - Track step results
  18. * - Validate exit codes
  19. *
  20. * NO: agent steps, skill steps, approval, workflows, context passing
  21. */
  22. function generateExecutionId(): string {
  23. return `exec_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
  24. }
  25. function interpolateVariables(text: string, inputs: InputValues): string {
  26. return text.replace(/\{\{inputs\.(\w+)\}\}/g, (match, name) => {
  27. const value = inputs[name]
  28. return value !== undefined ? String(value) : match
  29. })
  30. }
  31. async function runScript(
  32. command: string,
  33. options: { cwd?: string; env?: Record<string, string> }
  34. ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
  35. return new Promise((resolve) => {
  36. const proc = spawn('sh', ['-c', command], {
  37. cwd: options.cwd || process.cwd(),
  38. env: { ...process.env, ...options.env },
  39. })
  40. let stdout = ''
  41. let stderr = ''
  42. proc.stdout.on('data', (data) => {
  43. stdout += data.toString()
  44. })
  45. proc.stderr.on('data', (data) => {
  46. stderr += data.toString()
  47. })
  48. proc.on('close', (code) => {
  49. resolve({ stdout, stderr, exitCode: code ?? 1 })
  50. })
  51. proc.on('error', (error) => {
  52. resolve({ stdout, stderr: error.message, exitCode: 1 })
  53. })
  54. })
  55. }
  56. async function executeScriptStep(
  57. step: ScriptStep,
  58. execution: AbilityExecution,
  59. ctx: ExecutorContext
  60. ): Promise<StepResult> {
  61. const startedAt = Date.now()
  62. const command = interpolateVariables(step.run, execution.inputs)
  63. console.log(`[abilities] Executing: ${command}`)
  64. try {
  65. const result = await runScript(command, {
  66. cwd: step.cwd || ctx.cwd,
  67. env: { ...ctx.env, ...step.env },
  68. })
  69. // Validate exit code if specified
  70. let failed = false
  71. let error: string | undefined
  72. if (step.validation?.exit_code !== undefined && result.exitCode !== step.validation.exit_code) {
  73. failed = true
  74. error = `Exit code ${result.exitCode}, expected ${step.validation.exit_code}`
  75. }
  76. return {
  77. stepId: step.id,
  78. status: failed ? 'failed' : 'completed',
  79. output: result.stdout || result.stderr,
  80. error,
  81. startedAt,
  82. completedAt: Date.now(),
  83. duration: Date.now() - startedAt,
  84. }
  85. } catch (err) {
  86. return {
  87. stepId: step.id,
  88. status: 'failed',
  89. error: err instanceof Error ? err.message : String(err),
  90. startedAt,
  91. completedAt: Date.now(),
  92. duration: Date.now() - startedAt,
  93. }
  94. }
  95. }
  96. function buildExecutionOrder(steps: Step[]): Step[] {
  97. const result: Step[] = []
  98. const completed = new Set<string>()
  99. const remaining = [...steps]
  100. while (remaining.length > 0) {
  101. const next = remaining.find((step) => {
  102. if (!step.needs || step.needs.length === 0) return true
  103. return step.needs.every((dep) => completed.has(dep))
  104. })
  105. if (!next) {
  106. console.error('[abilities] Unable to resolve step order - circular dependency?')
  107. break
  108. }
  109. result.push(next)
  110. completed.add(next.id)
  111. remaining.splice(remaining.indexOf(next), 1)
  112. }
  113. return result
  114. }
  115. export async function executeAbility(
  116. ability: Ability,
  117. inputs: InputValues,
  118. ctx: ExecutorContext
  119. ): Promise<AbilityExecution> {
  120. // Validate inputs
  121. const inputErrors = validateInputs(ability, inputs)
  122. if (inputErrors.length > 0) {
  123. return {
  124. id: generateExecutionId(),
  125. ability,
  126. inputs,
  127. status: 'failed',
  128. currentStep: null,
  129. currentStepIndex: -1,
  130. completedSteps: [],
  131. pendingSteps: ability.steps,
  132. startedAt: Date.now(),
  133. completedAt: Date.now(),
  134. error: `Input validation failed: ${inputErrors.map((e) => e.message).join(', ')}`,
  135. }
  136. }
  137. // Apply defaults
  138. const resolvedInputs: InputValues = { ...inputs }
  139. if (ability.inputs) {
  140. for (const [name, def] of Object.entries(ability.inputs)) {
  141. if (resolvedInputs[name] === undefined && def.default !== undefined) {
  142. resolvedInputs[name] = def.default
  143. }
  144. }
  145. }
  146. // Build execution order based on dependencies
  147. const orderedSteps = buildExecutionOrder(ability.steps)
  148. const execution: AbilityExecution = {
  149. id: generateExecutionId(),
  150. ability,
  151. inputs: resolvedInputs,
  152. status: 'running',
  153. currentStep: null,
  154. currentStepIndex: -1,
  155. completedSteps: [],
  156. pendingSteps: [...orderedSteps],
  157. startedAt: Date.now(),
  158. }
  159. // Execute steps sequentially
  160. for (let i = 0; i < orderedSteps.length; i++) {
  161. const step = orderedSteps[i]
  162. execution.currentStep = step
  163. execution.currentStepIndex = i
  164. console.log(`[abilities] Step ${i + 1}/${orderedSteps.length}: ${step.id}`)
  165. const result = await executeScriptStep(step as ScriptStep, execution, ctx)
  166. execution.completedSteps.push(result)
  167. execution.pendingSteps = execution.pendingSteps.filter((s) => s.id !== step.id)
  168. if (result.status === 'failed') {
  169. execution.status = 'failed'
  170. execution.error = result.error
  171. execution.completedAt = Date.now()
  172. return execution
  173. }
  174. }
  175. execution.status = 'completed'
  176. execution.currentStep = null
  177. execution.completedAt = Date.now()
  178. return execution
  179. }
  180. export function formatExecutionResult(execution: AbilityExecution): string {
  181. const lines: string[] = []
  182. lines.push(`Ability: ${execution.ability.name}`)
  183. lines.push(`Status: ${execution.status === 'completed' ? '✅ Complete' : '❌ Failed'}`)
  184. if (execution.error) {
  185. lines.push(`Error: ${execution.error}`)
  186. }
  187. lines.push('')
  188. lines.push('Steps:')
  189. for (const result of execution.completedSteps) {
  190. const icon = result.status === 'completed' ? '✅' : '❌'
  191. const duration = result.duration ? ` (${(result.duration / 1000).toFixed(1)}s)` : ''
  192. lines.push(` ${icon} ${result.stepId}${duration}`)
  193. if (result.error) {
  194. lines.push(` Error: ${result.error}`)
  195. }
  196. }
  197. const totalDuration = execution.completedAt
  198. ? ((execution.completedAt - execution.startedAt) / 1000).toFixed(1)
  199. : 'N/A'
  200. lines.push('')
  201. lines.push(`Duration: ${totalDuration}s`)
  202. return lines.join('\n')
  203. }