Why: Intent routing needs a registry of workflow definitions, and uninstall was leaving intents and workflows in place. Evidence: go test ./internal/workflow/ ./internal/plugins/ ./internal/app/ passed. Validation rejects duplicate ids, unknown kinds, denied tools, and both step caps. Unregister clears the registry and the intent gate. Impact: Script steps must stay inside the plugin directory. The server holds the registry for later routing. Refs: feature/agent-harness-p4 Co-authored-by: Cursor <cursoragent@cursor.com>
90 lines
2.0 KiB
Go
90 lines
2.0 KiB
Go
package workflow
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
|
|
core "agent-system-core"
|
|
)
|
|
|
|
// Vars 是一次执行里可以填进模板的值。
|
|
type Vars struct {
|
|
Input string
|
|
Slots map[string]string
|
|
Steps map[string]string
|
|
}
|
|
|
|
var placeholder = regexp.MustCompile(`\{\{(input|slots\.[A-Za-z_][A-Za-z0-9_]*|steps\.[A-Za-z_][A-Za-z0-9_]*\.output)\}\}`)
|
|
|
|
// Render 替换 {{input}}、{{slots.<key>}}、{{steps.<id>.output}}。缺值替换成空串。
|
|
func Render(s string, v Vars) string {
|
|
return placeholder.ReplaceAllStringFunc(s, func(token string) string {
|
|
body := token[2 : len(token)-2]
|
|
switch {
|
|
case body == "input":
|
|
return v.Input
|
|
case len(body) > 6 && body[:6] == "slots.":
|
|
return v.Slots[body[6:]]
|
|
case len(body) > 6 && body[:6] == "steps.":
|
|
id := body[6 : len(body)-len(".output")]
|
|
return v.Steps[id]
|
|
default:
|
|
return token
|
|
}
|
|
})
|
|
}
|
|
|
|
// RenderRaw 在 JSON 字符串内部替换占位符,结果仍是合法 JSON。
|
|
func RenderRaw(raw json.RawMessage, v Vars) (json.RawMessage, error) {
|
|
if len(raw) == 0 {
|
|
return nil, nil
|
|
}
|
|
var node any
|
|
if err := json.Unmarshal(raw, &node); err != nil {
|
|
return nil, fmt.Errorf("workflow: 参数不是合法 JSON")
|
|
}
|
|
out, err := json.Marshal(walk(node, v))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func walk(node any, v Vars) any {
|
|
switch x := node.(type) {
|
|
case string:
|
|
return Render(x, v)
|
|
case []any:
|
|
for i := range x {
|
|
x[i] = walk(x[i], v)
|
|
}
|
|
return x
|
|
case map[string]any:
|
|
for k, val := range x {
|
|
x[k] = walk(val, v)
|
|
}
|
|
return x
|
|
default:
|
|
return node
|
|
}
|
|
}
|
|
|
|
// RenderStep 渲染这一步会交给执行器的文本和参数。
|
|
func RenderStep(step core.WorkflowStep, v Vars) (core.WorkflowStep, error) {
|
|
step.Prompt = Render(step.Prompt, v)
|
|
step.Artifact = Render(step.Artifact, v)
|
|
if step.Hitl != nil {
|
|
step.Hitl.Question = Render(step.Hitl.Question, v)
|
|
}
|
|
if step.Card != nil {
|
|
step.Card.Title = Render(step.Card.Title, v)
|
|
}
|
|
args, err := RenderRaw(step.Args, v)
|
|
if err != nil {
|
|
return step, err
|
|
}
|
|
step.Args = args
|
|
return step, nil
|
|
}
|