From 7d4b2835866b1a00ed42b81cab67f8a3bce03390 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 02:31:53 +0000 Subject: [PATCH] feat(agentic): resolve nested flow references Co-Authored-By: Virgil --- pkg/agentic/commands.go | 113 +++++++++++++++++++++++++++--- pkg/agentic/commands_flow_test.go | 39 +++++++++++ 2 files changed, 144 insertions(+), 8 deletions(-) diff --git a/pkg/agentic/commands.go b/pkg/agentic/commands.go index 6cada3f..751622a 100644 --- a/pkg/agentic/commands.go +++ b/pkg/agentic/commands.go @@ -151,8 +151,10 @@ func (s *PrepSubsystem) runFlowCommand(options core.Options, commandLabel string } core.Print(nil, "steps: %d", len(document.Definition.Steps)) - for index, step := range document.Definition.Steps { - core.Print(nil, " %d. %s", index+1, flowStepSummary(step)) + resolvedSteps := s.printFlowSteps(document, "", variables, map[string]bool{document.Source: true}) + output.ResolvedSteps = resolvedSteps + if resolvedSteps != len(document.Definition.Steps) { + core.Print(nil, "resolved steps: %d", resolvedSteps) } return core.Result{Value: output, OK: true} } @@ -871,12 +873,13 @@ func parseIntString(s string) int { } type FlowRunOutput struct { - Success bool `json:"success"` - Source string `json:"source,omitempty"` - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` - Steps int `json:"steps,omitempty"` - Parsed bool `json:"parsed,omitempty"` + Success bool `json:"success"` + Source string `json:"source,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Steps int `json:"steps,omitempty"` + ResolvedSteps int `json:"resolved_steps,omitempty"` + Parsed bool `json:"parsed,omitempty"` } type flowDefinition struct { @@ -1005,6 +1008,100 @@ func flowStepSummary(step flowDefinitionStep) string { } } +func (s *PrepSubsystem) printFlowSteps(document flowRunDocument, indent string, variables map[string]string, visited map[string]bool) int { + total := 0 + for index, step := range document.Definition.Steps { + core.Print(nil, "%s%d. %s", indent, index+1, flowStepSummary(step)) + total++ + + if step.Flow != "" { + resolved := s.resolveFlowReference(document.Source, step.Flow, variables) + if !resolved.OK { + continue + } + + nested, ok := resolved.Value.(flowRunDocument) + if !ok || nested.Source == "" { + continue + } + if visited[nested.Source] { + core.Print(nil, "%s cycle: %s", indent, nested.Source) + continue + } + + core.Print(nil, "%s resolved: %s", indent, nested.Source) + visited[nested.Source] = true + total += s.printFlowSteps(nested, core.Concat(indent, " "), variables, visited) + delete(visited, nested.Source) + } + + if len(step.Parallel) > 0 { + core.Print(nil, "%s parallel:", indent) + for parallelIndex, parallelStep := range step.Parallel { + core.Print(nil, "%s %d. %s", indent, parallelIndex+1, flowStepSummary(parallelStep)) + } + } + } + + return total +} + +func (s *PrepSubsystem) resolveFlowReference(baseSource, reference string, variables map[string]string) core.Result { + trimmedReference := core.Trim(reference) + if trimmedReference == "" { + return core.Result{Value: core.E("agentic.resolveFlowReference", "flow reference is required", nil), OK: false} + } + + candidates := []string{trimmedReference} + + if root := flowRootPath(baseSource); root != "" { + candidate := core.JoinPath(root, trimmedReference) + if candidate != trimmedReference { + candidates = append(candidates, candidate) + } + } + + repoCandidate := core.JoinPath("pkg", "lib", "flow", trimmedReference) + if repoCandidate != trimmedReference { + candidates = append(candidates, repoCandidate) + } + + for _, candidate := range candidates { + result := readFlowDocument(candidate, variables) + if result.OK { + return result + } + + err, ok := result.Value.(error) + if !ok || !core.Contains(err.Error(), "flow not found:") { + return result + } + } + + return core.Result{Value: core.E("agentic.resolveFlowReference", core.Concat("flow not found: ", trimmedReference), nil), OK: false} +} + +func flowRootPath(source string) string { + trimmed := core.Trim(core.Replace(source, "\\", "/")) + if trimmed == "" { + return "" + } + + segments := core.Split(trimmed, "/") + for index := 0; index+2 < len(segments); index++ { + if segments[index] == "pkg" && segments[index+1] == "lib" && segments[index+2] == "flow" { + return core.JoinPath(segments[:index+3]...) + } + } + + dir := core.PathDir(trimmed) + if dir != "" { + return dir + } + + return "" +} + type brainListOutput struct { Count int `json:"count"` Memories []brainListOutputEntry `json:"memories"` diff --git a/pkg/agentic/commands_flow_test.go b/pkg/agentic/commands_flow_test.go index fc99a50..0ad80b1 100644 --- a/pkg/agentic/commands_flow_test.go +++ b/pkg/agentic/commands_flow_test.go @@ -82,6 +82,45 @@ func TestCommandsFlow_CmdRunFlow_Good_RendersVariablesAndDryRun(t *testing.T) { assert.Contains(t, output, "desc: Build core/go") } +func TestCommandsFlow_CmdRunFlow_Good_ResolvesNestedFlowReferences(t *testing.T) { + dir := t.TempDir() + flowRoot := core.JoinPath(dir, "pkg", "lib", "flow") + require.True(t, fs.EnsureDir(core.JoinPath(flowRoot, "verify")).OK) + + rootPath := core.JoinPath(flowRoot, "root.yaml") + require.True(t, fs.Write(rootPath, core.Concat( + "name: Root Flow\n", + "description: Resolve nested flow references\n", + "steps:\n", + " - name: child\n", + " flow: verify/go-qa.yaml\n", + )).OK) + + childPath := core.JoinPath(flowRoot, "verify", "go-qa.yaml") + require.True(t, fs.Write(childPath, core.Concat( + "name: Child Flow\n", + "description: Nested flow body\n", + "steps:\n", + " - name: child-run\n", + " run: echo child\n", + )).OK) + + s := newTestPrep(t) + output := captureStdout(t, func() { + r := s.cmdRunFlow(core.NewOptions(core.Option{Key: "_arg", Value: rootPath})) + require.True(t, r.OK) + + flowOutput, ok := r.Value.(FlowRunOutput) + require.True(t, ok) + assert.True(t, flowOutput.Success) + assert.Equal(t, 1, flowOutput.Steps) + assert.Equal(t, 2, flowOutput.ResolvedSteps) + }) + + assert.Contains(t, output, "resolved:") + assert.Contains(t, output, "child-run: run echo child") +} + func TestCommandsFlow_CmdRunFlow_Bad_MissingPath(t *testing.T) { s := newTestPrep(t)