feat(agentic): resolve nested flow references
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
09aa19afde
commit
7d4b283586
2 changed files with 144 additions and 8 deletions
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue