feat(agentic): resolve nested flow references

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 02:31:53 +00:00
parent 09aa19afde
commit 7d4b283586
2 changed files with 144 additions and 8 deletions

View file

@ -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"`

View file

@ -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)