Files
osmedeus/internal/executor/dispatcher.go
T
j3ssie 520977d1ea feat: Add agent-acp step type with ACP protocol support
- Implement agent-acp step type for spawning external ACP agent subprocesses via Agent Communication Protocol
- Add ACPExecutor with validation, field rendering, and subprocess lifecycle management
- Integrate agent-acp field rendering in StepDispatcher (batch and sequential modes)
- Add run_agent() utility function for workflows to execute ACP agents from steps and JS context
- Add osmedeus agent CLI command for interactive agent execution with --agent, --cwd, --timeout, --stdin, and --list flags
- Add /osm/api/agent/chat/completions REST endpoint with OpenAI-compatible chat format and concurrency control
- Support agent selection via: built-in names (claude-code, codex, opencode, gemini) or custom acp_config.command
- Add step-level configuration: cwd, allowed_paths, acp_config (command, args, env, write_enabled)
- Add comprehensive E2E tests for agent-acp workflows (basic, minimal, config, codex variants)
- Add test workflows in test/testdata/workflows/agent-and-llm/
- Update AGENTS.md documentation with agent-acp examples, CLI usage, and API endpoints
2026-02-27 16:18:06 +08:00

1217 lines
35 KiB
Go

package executor
import (
"context"
"fmt"
"regexp"
"github.com/j3ssie/osmedeus/v5/internal/config"
"github.com/j3ssie/osmedeus/v5/internal/core"
"github.com/j3ssie/osmedeus/v5/internal/functions"
"github.com/j3ssie/osmedeus/v5/internal/logger"
"github.com/j3ssie/osmedeus/v5/internal/runner"
"github.com/j3ssie/osmedeus/v5/internal/template"
"github.com/j3ssie/osmedeus/v5/internal/terminal"
"go.uber.org/zap"
)
// functionCallPattern matches function call syntax like functionName(...)
var functionCallPattern = regexp.MustCompile(`\w+\s*\(`)
// StepDispatcher dispatches steps to appropriate executors
type StepDispatcher struct {
registry *PluginRegistry
templateEngine template.TemplateEngine
batchRenderer template.BatchRenderer // For optimized batch rendering
functionRegistry *functions.Registry
dryRun bool
runner runner.Runner
enableBatch bool // Enable batch template rendering
printer *terminal.Printer
// Keep direct references to executors that need special configuration
bashExecutor *BashExecutor
llmExecutor *LLMExecutor
agentExecutor *AgentExecutor
}
// SetDryRun enables or disables dry-run mode for the dispatcher
func (d *StepDispatcher) SetDryRun(dryRun bool) {
d.dryRun = dryRun
}
// SetSilent enables or disables silent mode for executors that support it
func (d *StepDispatcher) SetSilent(silent bool) {
d.llmExecutor.SetSilent(silent)
d.agentExecutor.SetSilent(silent)
}
// SetPrinter sets the terminal printer for user-facing messages
func (d *StepDispatcher) SetPrinter(p *terminal.Printer) {
d.printer = p
}
// SetRunner sets the runner for command execution
func (d *StepDispatcher) SetRunner(r runner.Runner) {
d.runner = r
d.bashExecutor.SetRunner(r)
}
// StepDispatcherConfig holds configuration for the step dispatcher
type StepDispatcherConfig struct {
UseShardedEngine bool // Use sharded template engine for better concurrency
EnableBatch bool // Enable batch template rendering
ShardCount int // Number of shards (default: 16)
ShardCacheSize int // Cache size per shard (default: 64)
}
// DefaultStepDispatcherConfig returns the default configuration
func DefaultStepDispatcherConfig() StepDispatcherConfig {
return StepDispatcherConfig{
UseShardedEngine: true, // Default to sharded engine for better performance
EnableBatch: true, // Default to batch rendering
ShardCount: 16,
ShardCacheSize: 64,
}
}
// NewStepDispatcher creates a new step dispatcher with default configuration
func NewStepDispatcher() *StepDispatcher {
return NewStepDispatcherWithConfig(DefaultStepDispatcherConfig())
}
// NewStepDispatcherWithConfig creates a new step dispatcher with custom configuration
func NewStepDispatcherWithConfig(cfg StepDispatcherConfig) *StepDispatcher {
var engine template.TemplateEngine
var batchRenderer template.BatchRenderer
if cfg.UseShardedEngine {
shardedCfg := template.ShardedEngineConfig{
ShardCount: cfg.ShardCount,
ShardCacheSize: cfg.ShardCacheSize,
EnablePooling: true,
}
shardedEngine := template.NewShardedEngineWithConfig(shardedCfg)
engine = shardedEngine
batchRenderer = shardedEngine
} else {
stdEngine := template.NewEngine()
engine = stdEngine
batchRenderer = stdEngine
}
d := &StepDispatcher{
registry: NewPluginRegistry(),
templateEngine: engine,
batchRenderer: batchRenderer,
functionRegistry: functions.NewRegistry(),
enableBatch: cfg.EnableBatch,
}
// Create executors
d.bashExecutor = NewBashExecutor(engine)
d.llmExecutor = NewLLMExecutor(engine)
d.agentExecutor = NewAgentExecutor(engine, d.functionRegistry)
// Register all built-in plugins
d.registry.Register(d.bashExecutor)
d.registry.Register(NewFunctionExecutor(engine, d.functionRegistry))
d.registry.Register(NewParallelExecutor(d))
d.registry.Register(NewForeachExecutor(d, engine, d.functionRegistry))
d.registry.Register(NewRemoteBashExecutor(engine))
d.registry.Register(NewHTTPExecutor(engine))
d.registry.Register(d.llmExecutor)
d.registry.Register(d.agentExecutor)
d.registry.Register(NewACPExecutor(engine))
return d
}
// RegisterPlugin allows external plugin registration
func (d *StepDispatcher) RegisterPlugin(plugin StepExecutorPlugin) {
d.registry.Register(plugin)
}
// SetConfig passes config to executors that need it
func (d *StepDispatcher) SetConfig(cfg *config.Config) {
d.llmExecutor.SetConfig(cfg)
d.agentExecutor.SetConfig(cfg)
}
// Dispatch dispatches a step to the appropriate executor
func (d *StepDispatcher) Dispatch(ctx context.Context, step *core.Step, execCtx *core.ExecutionContext) (*core.StepResult, error) {
log := logger.Get()
log.Debug("Dispatching step",
zap.String("step_name", step.Name),
zap.String("step_type", string(step.Type)),
zap.Bool("dry_run", d.dryRun),
)
// Render templates in step fields
log.Debug("Rendering step templates")
renderedStep, err := d.renderStep(step, execCtx)
if err != nil {
log.Debug("Template rendering failed", zap.Error(err))
return nil, fmt.Errorf("template rendering failed: %w", err)
}
log.Debug("Step templates rendered",
zap.String("command", renderedStep.Command),
)
// Dispatch based on step type using plugin registry
log.Debug("Dispatching to executor",
zap.String("executor_type", string(step.Type)),
)
plugin, ok := d.registry.Get(step.Type)
if !ok {
return nil, fmt.Errorf("unknown step type: %s", step.Type)
}
log.Debug("Using plugin", zap.String("plugin_name", plugin.Name()))
result, err := plugin.Execute(ctx, renderedStep, execCtx)
if err != nil {
log.Debug("Step execution failed",
zap.String("step", step.Name),
zap.Error(err),
)
return result, err
}
log.Debug("Step execution completed",
zap.String("step", step.Name),
zap.String("status", string(result.Status)),
)
// Process exports
if step.HasExports() {
log.Debug("Processing exports",
zap.Int("export_count", len(step.Exports)),
)
// Merge auto-exports (e.g., from HTTP steps) into vars before evaluating user exports
vars := execCtx.GetVariables()
if result.Exports != nil {
for k, v := range result.Exports {
vars[k] = v
}
}
// Render template variables in export values first, then evaluate if needed
exports := make(map[string]interface{}, len(step.Exports))
for name, expr := range step.Exports {
rendered, err := d.templateEngine.Render(expr, vars)
if err != nil {
log.Warn("Failed to render export value, using original",
zap.String("export", name),
zap.Error(err))
rendered = expr
}
// Only evaluate with JS if the rendered value contains a function call
// Otherwise, use the rendered string directly
if functionCallPattern.MatchString(rendered) {
value, err := d.functionRegistry.Execute(rendered, vars)
if err != nil {
return result, fmt.Errorf("export evaluation failed for %s: %w", name, err)
}
exports[name] = value
} else {
// Use rendered value directly as a string
exports[name] = rendered
}
}
if result.Exports == nil {
result.Exports = make(map[string]interface{})
}
for k, v := range exports {
result.Exports[k] = v
}
log.Debug("Exports processed", zap.Int("total_exports", len(result.Exports)))
}
return result, nil
}
// collectRenderRequests gathers all non-empty template strings from a step
func collectRenderRequests(step *core.Step) []template.RenderRequest {
var requests []template.RenderRequest
add := func(key, tmpl string) {
if tmpl != "" {
requests = append(requests, template.RenderRequest{Key: key, Template: tmpl})
}
}
// String fields
add("Command", step.Command)
add("SpeedArgs", step.SpeedArgs)
add("ConfigArgs", step.ConfigArgs)
add("InputArgs", step.InputArgs)
add("OutputArgs", step.OutputArgs)
add("StdFile", step.StdFile)
add("Function", step.Function)
add("Input", step.Input)
add("VariablePreProcess", step.VariablePreProcess)
add("Log", step.Log)
add("Timeout", string(step.Timeout))
add("Threads", string(step.Threads))
add("URL", step.URL)
add("Method", step.Method)
add("RequestBody", step.RequestBody)
add("StepRunner", string(step.StepRunner))
add("StepRemoteFile", step.StepRemoteFile)
add("HostOutputFile", step.HostOutputFile)
// Slice fields
for i, cmd := range step.Commands {
add(fmt.Sprintf("Commands[%d]", i), cmd)
}
for i, cmd := range step.ParallelCommands {
add(fmt.Sprintf("ParallelCommands[%d]", i), cmd)
}
for i, fn := range step.Functions {
add(fmt.Sprintf("Functions[%d]", i), fn)
}
for i, fn := range step.ParallelFunctions {
add(fmt.Sprintf("ParallelFunctions[%d]", i), fn)
}
for i, input := range step.EmbeddingInput {
add(fmt.Sprintf("EmbeddingInput[%d]", i), input)
}
// Map fields
for k, v := range step.Headers {
add(fmt.Sprintf("Headers[%s]", k), v)
}
// Agent step fields
add("Query", step.Query)
add("SystemPrompt", step.SystemPrompt)
add("StopCondition", step.StopCondition)
add("PlanPrompt", step.PlanPrompt)
add("OnToolStart", step.OnToolStart)
add("OnToolEnd", step.OnToolEnd)
for i, q := range step.Queries {
add(fmt.Sprintf("Queries[%d]", i), q)
}
if step.Memory != nil {
add("Memory.PersistPath", step.Memory.PersistPath)
add("Memory.ResumePath", step.Memory.ResumePath)
}
for i, tool := range step.AgentTools {
add(fmt.Sprintf("AgentTools[%d].Handler", i), tool.Handler)
}
// Sub-agent fields (only top-level; nested sub-agents rendered on spawn)
for i, sa := range step.SubAgents {
add(fmt.Sprintf("SubAgents[%d].SystemPrompt", i), sa.SystemPrompt)
add(fmt.Sprintf("SubAgents[%d].StopCondition", i), sa.StopCondition)
add(fmt.Sprintf("SubAgents[%d].OnToolStart", i), sa.OnToolStart)
add(fmt.Sprintf("SubAgents[%d].OnToolEnd", i), sa.OnToolEnd)
if sa.Memory != nil {
add(fmt.Sprintf("SubAgents[%d].Memory.PersistPath", i), sa.Memory.PersistPath)
add(fmt.Sprintf("SubAgents[%d].Memory.ResumePath", i), sa.Memory.ResumePath)
}
for j, tool := range sa.AgentTools {
add(fmt.Sprintf("SubAgents[%d].AgentTools[%d].Handler", i, j), tool.Handler)
}
}
// Agent-ACP step fields
add("Agent", step.Agent)
add("Cwd", step.Cwd)
for i, p := range step.AllowedPaths {
add(fmt.Sprintf("AllowedPaths[%d]", i), p)
}
if step.ACPConfig != nil {
add("ACPConfig.Command", step.ACPConfig.Command)
for i, arg := range step.ACPConfig.Args {
add(fmt.Sprintf("ACPConfig.Args[%d]", i), arg)
}
for k, v := range step.ACPConfig.Env {
add(fmt.Sprintf("ACPConfig.Env[%s]", k), v)
}
}
// RunnerConfig fields
if step.StepRunnerConfig != nil && step.StepRunnerConfig.RunnerConfig != nil {
cfg := step.StepRunnerConfig.RunnerConfig
add("RunnerConfig.Image", cfg.Image)
add("RunnerConfig.Host", cfg.Host)
add("RunnerConfig.User", cfg.User)
add("RunnerConfig.Password", cfg.Password)
add("RunnerConfig.KeyFile", cfg.KeyFile)
add("RunnerConfig.WorkDir", cfg.WorkDir)
add("RunnerConfig.Network", cfg.Network)
for k, v := range cfg.Env {
add(fmt.Sprintf("RunnerConfig.Env[%s]", k), v)
}
for i, v := range cfg.Volumes {
add(fmt.Sprintf("RunnerConfig.Volumes[%d]", i), v)
}
}
return requests
}
// renderStep renders all template fields in a step
func (d *StepDispatcher) renderStep(step *core.Step, execCtx *core.ExecutionContext) (*core.Step, error) {
vars := execCtx.GetVariables()
// Use batch rendering if enabled and available
if d.enableBatch && d.batchRenderer != nil {
return d.renderStepBatch(step, vars)
}
return d.renderStepSequential(step, vars)
}
// renderStepBatch renders step templates using batch mode for fewer lock acquisitions
func (d *StepDispatcher) renderStepBatch(step *core.Step, vars map[string]any) (*core.Step, error) {
requests := collectRenderRequests(step)
if len(requests) == 0 {
// No templates to render, but still need to handle LLM messages
if len(step.Messages) > 0 {
rendered := *step
if err := d.renderLLMMessages(&rendered, vars); err != nil {
return nil, err
}
return &rendered, nil
}
return step, nil
}
results, err := d.batchRenderer.RenderBatch(requests, vars)
if err != nil {
return nil, err
}
rendered := *step
get := func(key string) string { return results[key] }
// Apply results to string fields
if v := get("Command"); v != "" {
rendered.Command = v
}
if v := get("SpeedArgs"); v != "" {
rendered.SpeedArgs = v
}
if v := get("ConfigArgs"); v != "" {
rendered.ConfigArgs = v
}
if v := get("InputArgs"); v != "" {
rendered.InputArgs = v
}
if v := get("OutputArgs"); v != "" {
rendered.OutputArgs = v
}
if v := get("StdFile"); v != "" {
rendered.StdFile = v
}
if v := get("Function"); v != "" {
rendered.Function = v
}
if v := get("Input"); v != "" {
rendered.Input = v
}
if v := get("VariablePreProcess"); v != "" {
rendered.VariablePreProcess = v
}
if v := get("Log"); v != "" {
rendered.Log = v
}
if v := get("Timeout"); v != "" {
rendered.Timeout = core.StepTimeout(v)
}
if v := get("Threads"); v != "" {
rendered.Threads = core.StepThreads(v)
}
if v := get("URL"); v != "" {
rendered.URL = v
}
if v := get("Method"); v != "" {
rendered.Method = v
}
if v := get("RequestBody"); v != "" {
rendered.RequestBody = v
}
if v := get("StepRunner"); v != "" {
rendered.StepRunner = core.RunnerType(v)
}
if v := get("StepRemoteFile"); v != "" {
rendered.StepRemoteFile = v
}
if v := get("HostOutputFile"); v != "" {
rendered.HostOutputFile = v
}
// Agent step fields
if v := get("Query"); v != "" {
rendered.Query = v
}
if v := get("SystemPrompt"); v != "" {
rendered.SystemPrompt = v
}
if v := get("StopCondition"); v != "" {
rendered.StopCondition = v
}
if v := get("PlanPrompt"); v != "" {
rendered.PlanPrompt = v
}
if v := get("OnToolStart"); v != "" {
rendered.OnToolStart = v
}
if v := get("OnToolEnd"); v != "" {
rendered.OnToolEnd = v
}
// Render Queries slice
if len(step.Queries) > 0 {
rendered.Queries = make([]string, len(step.Queries))
for i := range step.Queries {
rendered.Queries[i] = get(fmt.Sprintf("Queries[%d]", i))
}
}
if step.Memory != nil {
mem := *step.Memory
if v := get("Memory.PersistPath"); v != "" {
mem.PersistPath = v
}
if v := get("Memory.ResumePath"); v != "" {
mem.ResumePath = v
}
rendered.Memory = &mem
}
// Apply results to agent tool handlers
if len(step.AgentTools) > 0 {
renderedTools := make([]core.AgentToolDef, len(step.AgentTools))
copy(renderedTools, step.AgentTools)
for i := range renderedTools {
if v := get(fmt.Sprintf("AgentTools[%d].Handler", i)); v != "" {
renderedTools[i].Handler = v
}
}
rendered.AgentTools = renderedTools
}
// Apply results to sub-agent fields
if len(step.SubAgents) > 0 {
renderedSAs := make([]core.SubAgentDef, len(step.SubAgents))
for i, sa := range step.SubAgents {
renderedSAs[i] = sa.DeepCopy()
if v := get(fmt.Sprintf("SubAgents[%d].SystemPrompt", i)); v != "" {
renderedSAs[i].SystemPrompt = v
}
if v := get(fmt.Sprintf("SubAgents[%d].StopCondition", i)); v != "" {
renderedSAs[i].StopCondition = v
}
if v := get(fmt.Sprintf("SubAgents[%d].OnToolStart", i)); v != "" {
renderedSAs[i].OnToolStart = v
}
if v := get(fmt.Sprintf("SubAgents[%d].OnToolEnd", i)); v != "" {
renderedSAs[i].OnToolEnd = v
}
if sa.Memory != nil {
if renderedSAs[i].Memory == nil {
mem := *sa.Memory
renderedSAs[i].Memory = &mem
}
if v := get(fmt.Sprintf("SubAgents[%d].Memory.PersistPath", i)); v != "" {
renderedSAs[i].Memory.PersistPath = v
}
if v := get(fmt.Sprintf("SubAgents[%d].Memory.ResumePath", i)); v != "" {
renderedSAs[i].Memory.ResumePath = v
}
}
for j := range sa.AgentTools {
if v := get(fmt.Sprintf("SubAgents[%d].AgentTools[%d].Handler", i, j)); v != "" {
renderedSAs[i].AgentTools[j].Handler = v
}
}
}
rendered.SubAgents = renderedSAs
}
// Agent-ACP step fields
if v := get("Agent"); v != "" {
rendered.Agent = v
}
if v := get("Cwd"); v != "" {
rendered.Cwd = v
}
if len(step.AllowedPaths) > 0 {
rendered.AllowedPaths = make([]string, len(step.AllowedPaths))
for i := range step.AllowedPaths {
rendered.AllowedPaths[i] = get(fmt.Sprintf("AllowedPaths[%d]", i))
}
}
if step.ACPConfig != nil {
cfg := *step.ACPConfig
if v := get("ACPConfig.Command"); v != "" {
cfg.Command = v
}
if len(step.ACPConfig.Args) > 0 {
cfg.Args = make([]string, len(step.ACPConfig.Args))
for i := range step.ACPConfig.Args {
cfg.Args[i] = get(fmt.Sprintf("ACPConfig.Args[%d]", i))
}
}
if len(step.ACPConfig.Env) > 0 {
cfg.Env = make(map[string]string, len(step.ACPConfig.Env))
for k := range step.ACPConfig.Env {
cfg.Env[k] = get(fmt.Sprintf("ACPConfig.Env[%s]", k))
}
}
rendered.ACPConfig = &cfg
}
// Apply results to slice fields
if len(step.Commands) > 0 {
rendered.Commands = make([]string, len(step.Commands))
for i := range step.Commands {
rendered.Commands[i] = get(fmt.Sprintf("Commands[%d]", i))
}
}
if len(step.ParallelCommands) > 0 {
rendered.ParallelCommands = make([]string, len(step.ParallelCommands))
for i := range step.ParallelCommands {
rendered.ParallelCommands[i] = get(fmt.Sprintf("ParallelCommands[%d]", i))
}
}
if len(step.Functions) > 0 {
rendered.Functions = make([]string, len(step.Functions))
for i := range step.Functions {
rendered.Functions[i] = get(fmt.Sprintf("Functions[%d]", i))
}
}
if len(step.ParallelFunctions) > 0 {
rendered.ParallelFunctions = make([]string, len(step.ParallelFunctions))
for i := range step.ParallelFunctions {
rendered.ParallelFunctions[i] = get(fmt.Sprintf("ParallelFunctions[%d]", i))
}
}
if len(step.EmbeddingInput) > 0 {
rendered.EmbeddingInput = make([]string, len(step.EmbeddingInput))
for i := range step.EmbeddingInput {
rendered.EmbeddingInput[i] = get(fmt.Sprintf("EmbeddingInput[%d]", i))
}
}
// Apply results to map fields
if len(step.Headers) > 0 {
rendered.Headers = make(map[string]string, len(step.Headers))
for k := range step.Headers {
rendered.Headers[k] = get(fmt.Sprintf("Headers[%s]", k))
}
}
// Apply results to RunnerConfig
if step.StepRunnerConfig != nil && step.StepRunnerConfig.RunnerConfig != nil {
cfg := *step.StepRunnerConfig.RunnerConfig
if v := get("RunnerConfig.Image"); v != "" {
cfg.Image = v
}
if v := get("RunnerConfig.Host"); v != "" {
cfg.Host = v
}
if v := get("RunnerConfig.User"); v != "" {
cfg.User = v
}
if v := get("RunnerConfig.Password"); v != "" {
cfg.Password = v
}
if v := get("RunnerConfig.KeyFile"); v != "" {
cfg.KeyFile = v
}
if v := get("RunnerConfig.WorkDir"); v != "" {
cfg.WorkDir = v
}
if v := get("RunnerConfig.Network"); v != "" {
cfg.Network = v
}
// Apply Env map
if len(step.StepRunnerConfig.Env) > 0 {
cfg.Env = make(map[string]string, len(step.StepRunnerConfig.Env))
for k := range step.StepRunnerConfig.Env {
cfg.Env[k] = get(fmt.Sprintf("RunnerConfig.Env[%s]", k))
}
}
// Apply Volumes slice
if len(step.StepRunnerConfig.Volumes) > 0 {
cfg.Volumes = make([]string, len(step.StepRunnerConfig.Volumes))
for i := range step.StepRunnerConfig.Volumes {
cfg.Volumes[i] = get(fmt.Sprintf("RunnerConfig.Volumes[%d]", i))
}
}
rendered.StepRunnerConfig = &core.StepRunnerConfig{RunnerConfig: &cfg}
}
// LLM messages handled separately (complex nested structure)
if len(step.Messages) > 0 {
if err := d.renderLLMMessages(&rendered, vars); err != nil {
return nil, err
}
}
return &rendered, nil
}
// renderLLMMessages renders LLM message templates (complex nested structure)
func (d *StepDispatcher) renderLLMMessages(rendered *core.Step, vars map[string]any) error {
renderedMessages := make([]core.LLMMessage, len(rendered.Messages))
for i, msg := range rendered.Messages {
renderedMsg := msg
// Render content (can be string or []interface{})
switch content := msg.Content.(type) {
case string:
renderedContent, err := d.templateEngine.Render(content, vars)
if err != nil {
return fmt.Errorf("error rendering message content: %w", err)
}
renderedMsg.Content = renderedContent
case []interface{}:
// Handle multimodal content parts
renderedParts := make([]interface{}, len(content))
for j, part := range content {
if partMap, ok := part.(map[string]interface{}); ok {
renderedPartMap := make(map[string]interface{})
for k, v := range partMap {
renderedPartMap[k] = v
}
// Render text field
if text, ok := partMap["text"].(string); ok {
renderedText, err := d.templateEngine.Render(text, vars)
if err != nil {
return fmt.Errorf("error rendering content part text: %w", err)
}
renderedPartMap["text"] = renderedText
}
// Render image_url.url if present
if imgURL, ok := partMap["image_url"].(map[string]interface{}); ok {
renderedImgURL := make(map[string]interface{})
for k, v := range imgURL {
renderedImgURL[k] = v
}
if url, ok := imgURL["url"].(string); ok {
renderedURL, err := d.templateEngine.Render(url, vars)
if err != nil {
return fmt.Errorf("error rendering image URL: %w", err)
}
renderedImgURL["url"] = renderedURL
}
renderedPartMap["image_url"] = renderedImgURL
}
renderedParts[j] = renderedPartMap
} else {
renderedParts[j] = part
}
}
renderedMsg.Content = renderedParts
}
renderedMessages[i] = renderedMsg
}
rendered.Messages = renderedMessages
return nil
}
// renderStepSequential renders step templates sequentially (original implementation)
func (d *StepDispatcher) renderStepSequential(step *core.Step, vars map[string]any) (*core.Step, error) {
// Create a copy of the step
rendered := *step
// Render command fields
if step.Command != "" {
cmd, err := d.templateEngine.Render(step.Command, vars)
if err != nil {
return nil, err
}
rendered.Command = cmd
}
if len(step.Commands) > 0 {
cmds, err := d.templateEngine.RenderSlice(step.Commands, vars)
if err != nil {
return nil, err
}
rendered.Commands = cmds
}
if len(step.ParallelCommands) > 0 {
cmds, err := d.templateEngine.RenderSlice(step.ParallelCommands, vars)
if err != nil {
return nil, err
}
rendered.ParallelCommands = cmds
}
// Render structured argument fields (for bash/remote-bash steps)
if step.SpeedArgs != "" {
args, err := d.templateEngine.Render(step.SpeedArgs, vars)
if err != nil {
return nil, fmt.Errorf("error rendering speed_args: %w", err)
}
rendered.SpeedArgs = args
}
if step.ConfigArgs != "" {
args, err := d.templateEngine.Render(step.ConfigArgs, vars)
if err != nil {
return nil, fmt.Errorf("error rendering config_args: %w", err)
}
rendered.ConfigArgs = args
}
if step.InputArgs != "" {
args, err := d.templateEngine.Render(step.InputArgs, vars)
if err != nil {
return nil, fmt.Errorf("error rendering input_args: %w", err)
}
rendered.InputArgs = args
}
if step.OutputArgs != "" {
args, err := d.templateEngine.Render(step.OutputArgs, vars)
if err != nil {
return nil, fmt.Errorf("error rendering output_args: %w", err)
}
rendered.OutputArgs = args
}
// Render std_file for stdout/stderr capture
if step.StdFile != "" {
stdFile, err := d.templateEngine.Render(step.StdFile, vars)
if err != nil {
return nil, fmt.Errorf("error rendering std_file: %w", err)
}
rendered.StdFile = stdFile
}
// Render function fields
if step.Function != "" {
fn, err := d.templateEngine.Render(step.Function, vars)
if err != nil {
return nil, err
}
rendered.Function = fn
}
if len(step.Functions) > 0 {
fns, err := d.templateEngine.RenderSlice(step.Functions, vars)
if err != nil {
return nil, err
}
rendered.Functions = fns
}
if len(step.ParallelFunctions) > 0 {
fns, err := d.templateEngine.RenderSlice(step.ParallelFunctions, vars)
if err != nil {
return nil, err
}
rendered.ParallelFunctions = fns
}
// Render foreach fields
if step.Input != "" {
input, err := d.templateEngine.Render(step.Input, vars)
if err != nil {
return nil, err
}
rendered.Input = input
}
if step.VariablePreProcess != "" {
vpp, err := d.templateEngine.Render(step.VariablePreProcess, vars)
if err != nil {
return nil, fmt.Errorf("error rendering variable_pre_process: %w", err)
}
rendered.VariablePreProcess = vpp
}
// Render log message
if step.Log != "" {
log, err := d.templateEngine.Render(step.Log, vars)
if err != nil {
return nil, err
}
rendered.Log = log
}
if step.Timeout != "" {
to, err := d.templateEngine.Render(string(step.Timeout), vars)
if err != nil {
return nil, fmt.Errorf("error rendering timeout: %w", err)
}
rendered.Timeout = core.StepTimeout(to)
}
if step.Threads != "" {
th, err := d.templateEngine.Render(string(step.Threads), vars)
if err != nil {
return nil, fmt.Errorf("error rendering threads: %w", err)
}
rendered.Threads = core.StepThreads(th)
}
// Render HTTP step fields
if step.URL != "" {
url, err := d.templateEngine.Render(step.URL, vars)
if err != nil {
return nil, fmt.Errorf("error rendering url: %w", err)
}
rendered.URL = url
}
if step.Method != "" {
method, err := d.templateEngine.Render(step.Method, vars)
if err != nil {
return nil, fmt.Errorf("error rendering method: %w", err)
}
rendered.Method = method
}
if step.RequestBody != "" {
body, err := d.templateEngine.Render(step.RequestBody, vars)
if err != nil {
return nil, fmt.Errorf("error rendering request_body: %w", err)
}
rendered.RequestBody = body
}
if len(step.Headers) > 0 {
headers, err := d.templateEngine.RenderMap(step.Headers, vars)
if err != nil {
return nil, fmt.Errorf("error rendering headers: %w", err)
}
rendered.Headers = headers
}
// Render step_runner if it contains template variables
if step.StepRunner != "" {
sr, err := d.templateEngine.Render(string(step.StepRunner), vars)
if err != nil {
return nil, fmt.Errorf("error rendering step_runner: %w", err)
}
rendered.StepRunner = core.RunnerType(sr)
}
// Render step_runner_config fields for remote-bash steps
if step.StepRunnerConfig != nil {
renderedConfig := &core.StepRunnerConfig{}
if step.StepRunnerConfig.RunnerConfig != nil {
cfg := *step.StepRunnerConfig.RunnerConfig
// Render string fields that may contain templates
if cfg.Image != "" {
img, err := d.templateEngine.Render(cfg.Image, vars)
if err != nil {
return nil, fmt.Errorf("error rendering step_runner_config.image: %w", err)
}
cfg.Image = img
}
if cfg.Host != "" {
host, err := d.templateEngine.Render(cfg.Host, vars)
if err != nil {
return nil, fmt.Errorf("error rendering step_runner_config.host: %w", err)
}
cfg.Host = host
}
if cfg.User != "" {
user, err := d.templateEngine.Render(cfg.User, vars)
if err != nil {
return nil, fmt.Errorf("error rendering step_runner_config.user: %w", err)
}
cfg.User = user
}
if cfg.Password != "" {
pass, err := d.templateEngine.Render(cfg.Password, vars)
if err != nil {
return nil, fmt.Errorf("error rendering step_runner_config.password: %w", err)
}
cfg.Password = pass
}
if cfg.KeyFile != "" {
keyFile, err := d.templateEngine.Render(cfg.KeyFile, vars)
if err != nil {
return nil, fmt.Errorf("error rendering step_runner_config.key_file: %w", err)
}
cfg.KeyFile = keyFile
}
if cfg.WorkDir != "" {
workDir, err := d.templateEngine.Render(cfg.WorkDir, vars)
if err != nil {
return nil, fmt.Errorf("error rendering step_runner_config.workdir: %w", err)
}
cfg.WorkDir = workDir
}
if cfg.Network != "" {
network, err := d.templateEngine.Render(cfg.Network, vars)
if err != nil {
return nil, fmt.Errorf("error rendering step_runner_config.network: %w", err)
}
cfg.Network = network
}
// Render env map values
if len(cfg.Env) > 0 {
renderedEnv, err := d.templateEngine.RenderMap(cfg.Env, vars)
if err != nil {
return nil, fmt.Errorf("error rendering step_runner_config.env: %w", err)
}
cfg.Env = renderedEnv
}
// Render volumes slice
if len(cfg.Volumes) > 0 {
renderedVols, err := d.templateEngine.RenderSlice(cfg.Volumes, vars)
if err != nil {
return nil, fmt.Errorf("error rendering step_runner_config.volumes: %w", err)
}
cfg.Volumes = renderedVols
}
renderedConfig.RunnerConfig = &cfg
}
rendered.StepRunnerConfig = renderedConfig
}
// Render remote-bash file copy fields
if step.StepRemoteFile != "" {
remoteFile, err := d.templateEngine.Render(step.StepRemoteFile, vars)
if err != nil {
return nil, fmt.Errorf("error rendering step_remote_file: %w", err)
}
rendered.StepRemoteFile = remoteFile
}
if step.HostOutputFile != "" {
hostFile, err := d.templateEngine.Render(step.HostOutputFile, vars)
if err != nil {
return nil, fmt.Errorf("error rendering host_output_file: %w", err)
}
rendered.HostOutputFile = hostFile
}
// Render agent step fields
if step.Query != "" {
q, err := d.templateEngine.Render(step.Query, vars)
if err != nil {
return nil, fmt.Errorf("error rendering query: %w", err)
}
rendered.Query = q
}
if len(step.Queries) > 0 {
qs, err := d.templateEngine.RenderSlice(step.Queries, vars)
if err != nil {
return nil, fmt.Errorf("error rendering queries: %w", err)
}
rendered.Queries = qs
}
if step.SystemPrompt != "" {
sp, err := d.templateEngine.Render(step.SystemPrompt, vars)
if err != nil {
return nil, fmt.Errorf("error rendering system_prompt: %w", err)
}
rendered.SystemPrompt = sp
}
if step.StopCondition != "" {
sc, err := d.templateEngine.Render(step.StopCondition, vars)
if err != nil {
return nil, fmt.Errorf("error rendering stop_condition: %w", err)
}
rendered.StopCondition = sc
}
if step.PlanPrompt != "" {
pp, err := d.templateEngine.Render(step.PlanPrompt, vars)
if err != nil {
return nil, fmt.Errorf("error rendering plan_prompt: %w", err)
}
rendered.PlanPrompt = pp
}
if step.OnToolStart != "" {
ots, err := d.templateEngine.Render(step.OnToolStart, vars)
if err != nil {
return nil, fmt.Errorf("error rendering on_tool_start: %w", err)
}
rendered.OnToolStart = ots
}
if step.OnToolEnd != "" {
ote, err := d.templateEngine.Render(step.OnToolEnd, vars)
if err != nil {
return nil, fmt.Errorf("error rendering on_tool_end: %w", err)
}
rendered.OnToolEnd = ote
}
if step.Memory != nil {
mem := *step.Memory
if mem.PersistPath != "" {
pp, err := d.templateEngine.Render(mem.PersistPath, vars)
if err != nil {
return nil, fmt.Errorf("error rendering memory.persist_path: %w", err)
}
mem.PersistPath = pp
}
if mem.ResumePath != "" {
rp, err := d.templateEngine.Render(mem.ResumePath, vars)
if err != nil {
return nil, fmt.Errorf("error rendering memory.resume_path: %w", err)
}
mem.ResumePath = rp
}
rendered.Memory = &mem
}
// Render agent tool handlers
if len(step.AgentTools) > 0 {
renderedTools := make([]core.AgentToolDef, len(step.AgentTools))
copy(renderedTools, step.AgentTools)
for i, tool := range renderedTools {
if tool.Handler != "" {
h, err := d.templateEngine.Render(tool.Handler, vars)
if err != nil {
return nil, fmt.Errorf("error rendering agent_tools[%d].handler: %w", i, err)
}
renderedTools[i].Handler = h
}
}
rendered.AgentTools = renderedTools
}
// Render sub-agent fields (only top-level; nested sub-agents rendered on spawn)
if len(step.SubAgents) > 0 {
renderedSAs := make([]core.SubAgentDef, len(step.SubAgents))
for i, sa := range step.SubAgents {
renderedSAs[i] = sa.DeepCopy()
if sa.SystemPrompt != "" {
sp, err := d.templateEngine.Render(sa.SystemPrompt, vars)
if err != nil {
return nil, fmt.Errorf("error rendering sub_agents[%d].system_prompt: %w", i, err)
}
renderedSAs[i].SystemPrompt = sp
}
if sa.StopCondition != "" {
sc, err := d.templateEngine.Render(sa.StopCondition, vars)
if err != nil {
return nil, fmt.Errorf("error rendering sub_agents[%d].stop_condition: %w", i, err)
}
renderedSAs[i].StopCondition = sc
}
if sa.OnToolStart != "" {
ots, err := d.templateEngine.Render(sa.OnToolStart, vars)
if err != nil {
return nil, fmt.Errorf("error rendering sub_agents[%d].on_tool_start: %w", i, err)
}
renderedSAs[i].OnToolStart = ots
}
if sa.OnToolEnd != "" {
ote, err := d.templateEngine.Render(sa.OnToolEnd, vars)
if err != nil {
return nil, fmt.Errorf("error rendering sub_agents[%d].on_tool_end: %w", i, err)
}
renderedSAs[i].OnToolEnd = ote
}
if sa.Memory != nil {
if renderedSAs[i].Memory == nil {
mem := *sa.Memory
renderedSAs[i].Memory = &mem
}
if sa.Memory.PersistPath != "" {
pp, err := d.templateEngine.Render(sa.Memory.PersistPath, vars)
if err != nil {
return nil, fmt.Errorf("error rendering sub_agents[%d].memory.persist_path: %w", i, err)
}
renderedSAs[i].Memory.PersistPath = pp
}
if sa.Memory.ResumePath != "" {
rp, err := d.templateEngine.Render(sa.Memory.ResumePath, vars)
if err != nil {
return nil, fmt.Errorf("error rendering sub_agents[%d].memory.resume_path: %w", i, err)
}
renderedSAs[i].Memory.ResumePath = rp
}
}
for j, tool := range sa.AgentTools {
if tool.Handler != "" {
h, err := d.templateEngine.Render(tool.Handler, vars)
if err != nil {
return nil, fmt.Errorf("error rendering sub_agents[%d].agent_tools[%d].handler: %w", i, j, err)
}
renderedSAs[i].AgentTools[j].Handler = h
}
}
}
rendered.SubAgents = renderedSAs
}
// Render agent-acp step fields
if step.Agent != "" {
agent, err := d.templateEngine.Render(step.Agent, vars)
if err != nil {
return nil, fmt.Errorf("error rendering agent: %w", err)
}
rendered.Agent = agent
}
if step.Cwd != "" {
cwd, err := d.templateEngine.Render(step.Cwd, vars)
if err != nil {
return nil, fmt.Errorf("error rendering cwd: %w", err)
}
rendered.Cwd = cwd
}
if len(step.AllowedPaths) > 0 {
paths, err := d.templateEngine.RenderSlice(step.AllowedPaths, vars)
if err != nil {
return nil, fmt.Errorf("error rendering allowed_paths: %w", err)
}
rendered.AllowedPaths = paths
}
if step.ACPConfig != nil {
cfg := *step.ACPConfig
if cfg.Command != "" {
cmd, err := d.templateEngine.Render(cfg.Command, vars)
if err != nil {
return nil, fmt.Errorf("error rendering acp_config.command: %w", err)
}
cfg.Command = cmd
}
if len(cfg.Args) > 0 {
args, err := d.templateEngine.RenderSlice(cfg.Args, vars)
if err != nil {
return nil, fmt.Errorf("error rendering acp_config.args: %w", err)
}
cfg.Args = args
}
if len(cfg.Env) > 0 {
env, err := d.templateEngine.RenderMap(cfg.Env, vars)
if err != nil {
return nil, fmt.Errorf("error rendering acp_config.env: %w", err)
}
cfg.Env = env
}
rendered.ACPConfig = &cfg
}
// Render LLM step fields
if len(step.Messages) > 0 {
if err := d.renderLLMMessages(&rendered, vars); err != nil {
return nil, err
}
}
// Render embedding input
if len(step.EmbeddingInput) > 0 {
embInputs, err := d.templateEngine.RenderSlice(step.EmbeddingInput, vars)
if err != nil {
return nil, fmt.Errorf("error rendering embedding_input: %w", err)
}
rendered.EmbeddingInput = embInputs
}
return &rendered, nil
}
// GetFunctionRegistry returns the function registry
func (d *StepDispatcher) GetFunctionRegistry() *functions.Registry {
return d.functionRegistry
}
// GetTemplateEngine returns the template engine
func (d *StepDispatcher) GetTemplateEngine() template.TemplateEngine {
return d.templateEngine
}