Why: Plugin scripts and model-written scripts need the same step runner, without giving the script a way to call interpreters or start another workflow. Evidence: go test ./... passed. Callbacks, denied tools, the 50-call cap, a 1MB line, and cancel-kills-the-process are covered. A model script does not run until the switch is on and the user confirms. Impact: runtime.workflowScripts stays off by default. Plugin script bodies are copied into the launch. Squad entry still has no run_workflow. Refs: feature/agent-harness-p4 Co-authored-by: Cursor <cursoragent@cursor.com>
108 lines
2.8 KiB
Go
108 lines
2.8 KiB
Go
// Package workflow 保存已装载的工作流定义。它只依赖 core,执行器在 runtime。
|
|
package workflow
|
|
|
|
import (
|
|
"encoding/json"
|
|
"sync"
|
|
|
|
core "agent-system-core"
|
|
)
|
|
|
|
// ScriptKey 是脚本正文的索引。同一个相对路径可以出现在不同工作流里。
|
|
func ScriptKey(workflowID, path string) string {
|
|
return workflowID + "\n" + path
|
|
}
|
|
|
|
// Registry 是当前装载集合的快照。Set 整体替换,不增量合并。
|
|
type Registry struct {
|
|
mu sync.RWMutex
|
|
byID map[string]core.WorkflowDef
|
|
byIntent map[string]string
|
|
scripts map[string]string
|
|
}
|
|
|
|
func NewRegistry() *Registry {
|
|
return &Registry{
|
|
byID: map[string]core.WorkflowDef{},
|
|
byIntent: map[string]string{},
|
|
scripts: map[string]string{},
|
|
}
|
|
}
|
|
|
|
// Set 用这一批定义替换注册表。同 ID 或同意图以后出现的为准。
|
|
// scripts 的键是 ScriptKey。定义会被拷贝,调用方之后的修改不影响已注册的副本。
|
|
func (r *Registry) Set(defs []core.WorkflowDef, scripts map[string]string) {
|
|
byID := map[string]core.WorkflowDef{}
|
|
byIntent := map[string]string{}
|
|
for _, def := range defs {
|
|
cloned := cloneDef(def)
|
|
byID[cloned.ID] = cloned
|
|
if cloned.IntentID != "" {
|
|
byIntent[cloned.IntentID] = cloned.ID
|
|
}
|
|
}
|
|
copied := map[string]string{}
|
|
for k, v := range scripts {
|
|
copied[k] = v
|
|
}
|
|
r.mu.Lock()
|
|
r.byID, r.byIntent, r.scripts = byID, byIntent, copied
|
|
r.mu.Unlock()
|
|
}
|
|
|
|
func (r *Registry) Get(id string) (core.WorkflowDef, bool) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
def, ok := r.byID[id]
|
|
if !ok {
|
|
return core.WorkflowDef{}, false
|
|
}
|
|
return cloneDef(def), true
|
|
}
|
|
|
|
func (r *Registry) ByIntent(intentID string) (core.WorkflowDef, bool) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
id, ok := r.byIntent[intentID]
|
|
if !ok {
|
|
return core.WorkflowDef{}, false
|
|
}
|
|
return cloneDef(r.byID[id]), true
|
|
}
|
|
|
|
func (r *Registry) Script(workflowID, path string) (string, bool) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
body, ok := r.scripts[ScriptKey(workflowID, path)]
|
|
return body, ok
|
|
}
|
|
|
|
// SlotQuestion 是启动工作流时还没填上的必填槽位。
|
|
type SlotQuestion struct {
|
|
Key string
|
|
Question string
|
|
}
|
|
|
|
// Launch 是一次工作流 run 启动时的副本。之后插件卸载不能改到它。
|
|
type Launch struct {
|
|
Def core.WorkflowDef
|
|
Slots map[string]string
|
|
Missing []SlotQuestion
|
|
Scripts map[string]string // 键是步骤里的相对路径,值是当时读到的脚本正文
|
|
}
|
|
|
|
// Clone 拷贝定义,调用方之后的修改不影响副本。
|
|
func Clone(def core.WorkflowDef) core.WorkflowDef { return cloneDef(def) }
|
|
|
|
func cloneDef(def core.WorkflowDef) core.WorkflowDef {
|
|
raw, err := json.Marshal(def)
|
|
if err != nil {
|
|
return def
|
|
}
|
|
var out core.WorkflowDef
|
|
if err := json.Unmarshal(raw, &out); err != nil {
|
|
return def
|
|
}
|
|
return out
|
|
}
|