Files
Agent-System-Backend/internal/workflow/registry.go
weichenandCursor fb5f071761 Run workflow scripts through a capped callback host
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>
2026-10-01 16:18:59 +00:00

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
}