Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ tasks:
desc: Build the application binary
cmds:
- go build -ldflags "{{.LDFLAGS}}" -o {{.BUILD_DIR}}/{{.BINARY_NAME}} {{.MAIN_PKG}}
- mkdir -p {{.HOME}}/bin
- ln -sf {{.USER_WORKING_DIR}}/{{.BUILD_DIR}}/{{.BINARY_NAME}} {{.HOME}}/bin/{{.BINARY_NAME}}
sources:
- "{{.GO_SOURCES}}"
Expand Down
19 changes: 19 additions & 0 deletions cagent-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,25 @@
"$ref": "#/definitions/ProviderConfig"
}
},
"workflow": {
"type": "object",
"properties": {
"steps": {
"type": "array",
"description": "List of workflow steps",
"items": {
"$ref": "#/definitions/StepConfig"
}
},
"max_loop_iterations": {
"type": "integer",
"description": "Maximum number of times a step can be re-executed due to a conditional back-edge (loop). Default: 100",
"minimum": 0
}
},
"additionalProperties": false,
"description": "Workflow configuration for sequential, conditional, and parallel step execution"
},
"agents": {
"type": "object",
"description": "Map of agent configurations",
Expand Down
77 changes: 77 additions & 0 deletions cmd/root/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import (
"github.com/docker/cagent/pkg/teamloader"
"github.com/docker/cagent/pkg/telemetry"
"github.com/docker/cagent/pkg/tui/styles"
"github.com/docker/cagent/pkg/workflow"
"github.com/docker/cagent/pkg/workflowrun"
)

type runExecFlags struct {
Expand Down Expand Up @@ -50,6 +52,9 @@ type runExecFlags struct {

// Run only
hideToolResults bool

// Workflow: set when config has workflow; exec mode runs workflow instead of single agent
workflowConfig *workflow.Config
}

func newRunCmd() *cobra.Command {
Expand Down Expand Up @@ -219,6 +224,8 @@ func (f *runExecFlags) runOrExec(ctx context.Context, out *cli.Printer, args []s
return err
}

f.workflowConfig = loadResult.Workflow

rt, sess, err = f.createLocalRuntimeAndSession(ctx, loadResult)
if err != nil {
return err
Expand Down Expand Up @@ -399,6 +406,10 @@ func (f *runExecFlags) handleExecMode(ctx context.Context, out *cli.Printer, rt
execArgs = append(execArgs, "Please proceed.")
}

if f.workflowConfig != nil {
return f.runExecWorkflow(ctx, out, rt, sess, execArgs[1])
}

err := cli.Run(ctx, out, cli.Config{
AppName: AppName,
AttachmentPath: f.attachmentPath,
Expand All @@ -412,6 +423,72 @@ func (f *runExecFlags) handleExecMode(ctx context.Context, out *cli.Printer, rt
return err
}

// runExecWorkflow runs the workflow executor and prints events to out (exec mode only).
func (f *runExecFlags) runExecWorkflow(ctx context.Context, out *cli.Printer, rt runtime.Runtime, sess *session.Session, userMessage string) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()

sess.AddMessage(cli.PrepareUserMessage(ctx, rt, userMessage, f.attachmentPath))
sess.SendUserMessage = true

exec := workflowrun.NewLocalExecutor(rt)
events := make(chan workflowrun.Event, 128)
go func() {
defer close(events)
if _, err := exec.Run(ctx, f.workflowConfig, sess, events); err != nil {
events <- runtime.Error(err.Error())
}
}()

var lastErr error
firstAgent := true
lastAgentName := ""
for event := range events {
if errEvent, ok := event.(*runtime.ErrorEvent); ok {
lastErr = fmt.Errorf("%s", errEvent.Error)
out.PrintError(lastErr)
continue
}
var agentName string
if re, ok := event.(runtime.Event); ok {
agentName = re.GetAgentName()
}
if agentName != "" && (firstAgent || agentName != lastAgentName) {
if !firstAgent {
out.Println()
}
out.PrintAgentName(agentName)
firstAgent = false
lastAgentName = agentName
}
switch e := event.(type) {
case *runtime.AgentChoiceEvent:
out.Print(e.Content)
case *runtime.AgentChoiceReasoningEvent:
out.Print(e.Content)
case *runtime.ToolCallConfirmationEvent:
if !f.autoApprove {
rt.Resume(ctx, runtime.ResumeReject(""))
} else {
rt.Resume(ctx, runtime.ResumeApprove())
}
case *runtime.ToolCallEvent:
if !f.hideToolCalls {
out.PrintToolCall(e.ToolCall)
}
case *runtime.ToolCallResponseEvent:
if !f.hideToolCalls {
out.PrintToolCallResponse(e.ToolCall, e.Response)
}
}
}

if lastErr != nil {
return RuntimeError{Err: lastErr}
}
return nil
}

func readInitialMessage(args []string) (*string, error) {
if len(args) < 2 {
return nil, nil
Expand Down
Loading