Files
Agent-System-Backend/internal/prompts/parse.go
T
weichenandCursor 46ce1b2c8e Let subagents run in the background and be reviewed
Why: A parent turn should be able to finish while children keep working, and a review should keep only findings that a second pass confirms. Users also need to override a profile's tools, model, and turn cap without adding dispatch tools.
Evidence: go test ./... passed. New cases cover overlay rejection, role mapping, queued notices, and dropping FAIL or unparsed findings.
Impact: Notices live only in memory, and each conversation allows three background children. Squad writer steps now use the writer profile.
Refs: feature/agent-harness-p3b
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-01 15:29:01 +00:00

192 lines
4.8 KiB
Go

// Package prompts 实现提示词库(prompt-library):内置库 embed 加载、用户覆盖链、
// 触发匹配与 system 组装。提示词是数据不是代码——文案变更走库文件 diff。
package prompts
import (
"fmt"
"strconv"
"strings"
core "agent-system-core"
)
// ParseModuleFile 解析一个库文件(frontmatter + 正文)。
// frontmatter 语法(刻意最小,不引 YAML 依赖):
//
// id: task.doc-minutes
// layer: task
// version: 1
// title: 会议纪要
// description: 依据上传文档生成完整会议纪要
// priority: 60
// requiresAttachments: true
// outputContract: |
// 多行契约文本(缩进块,支持)
// triggers:
// - kind: intent
// keywords: 纪要, 会议记录
// - kind: command
// command: /minutes
func ParseModuleFile(name, content string) (core.PromptModule, error) {
m := core.PromptModule{Source: string(core.SourceBuiltin)}
if !strings.HasPrefix(content, "---\n") {
return m, fmt.Errorf("%s: missing frontmatter", name)
}
end := strings.Index(content[4:], "\n---\n")
if end < 0 {
return m, fmt.Errorf("%s: unterminated frontmatter", name)
}
fm := content[4 : 4+end]
m.Body = strings.TrimLeft(content[4+end+5:], "\n")
lines := strings.Split(fm, "\n")
var trigger *core.PromptTrigger
blockKey := "" // 当前块标量(outputContract: |)归属的键
for i := 0; i < len(lines); i++ {
line := lines[i]
if strings.TrimSpace(line) == "" {
continue
}
if blockKey != "" {
// 块标量体:任何非空且带缩进的行并入;遇到顶格键结束块。
if line[0] == ' ' || line[0] == '\t' {
appendBlock(&m, blockKey, strings.TrimSpace(line))
continue
}
blockKey = ""
}
indented := line[0] == ' ' || line[0] == '\t'
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "- ") {
if indented {
return m, fmt.Errorf("%s: nested list unsupported: %q", name, trimmed)
}
k, v := splitKV(strings.TrimPrefix(trimmed, "- "))
if k != "kind" {
return m, fmt.Errorf("%s: list item outside triggers: %q", name, trimmed)
}
m.Triggers = append(m.Triggers, core.PromptTrigger{Kind: v})
trigger = &m.Triggers[len(m.Triggers)-1]
continue
}
k, v := splitKV(trimmed)
if indented && trigger != nil {
switch k {
case "keywords":
trigger.Keywords = splitList(v)
case "command":
trigger.Command = v
default:
return m, fmt.Errorf("%s: unknown trigger field %q", name, k)
}
continue
}
if indented {
return m, fmt.Errorf("%s: unexpected indented line %q", name, trimmed)
}
if v == "|" {
blockKey = k
continue
}
switch k {
case "id":
m.ID = v
case "layer":
m.Layer = v
case "version":
m.Version = atoi(v)
case "title":
m.Title = v
case "description":
m.Description = v
case "priority":
m.Priority = atoi(v)
case "requiresAttachments":
m.RequiresAttachments = v == "true"
case "outputContract":
m.OutputContract = v
case "triggers":
// 值留空;列表项由后续 "- kind:" 行提供。
case "tools":
m.Tools = splitList(v)
case "model":
m.Model = v
case "maxTurns":
m.MaxTurns = atoi(v)
default:
return m, fmt.Errorf("%s: unknown field %q", name, k)
}
}
if m.ID == "" || m.Layer == "" || m.Body == "" {
return m, fmt.Errorf("%s: id/layer/body required", name)
}
if m.Version == 0 {
m.Version = 1
}
return m, nil
}
// SubagentSpec 是子代理 profile。空字段表示沿用内置。
type SubagentSpec struct {
Tools []string
Model string
MaxTurns int
}
// specFromModule 由模块字段派生 profile。未写的 model / maxTurns 用默认值。
func specFromModule(m core.PromptModule) SubagentSpec {
spec := SubagentSpec{Tools: m.Tools, Model: m.Model, MaxTurns: m.MaxTurns}
if spec.Model == "" {
spec.Model = "lightweight"
}
if spec.MaxTurns <= 0 {
spec.MaxTurns = 8
}
return spec
}
// ParseSubagentSpec 从库文件 frontmatter 取出 tools、model、maxTurns。
// 标准模块字段可以同时出现。其他未知键报错。
func ParseSubagentSpec(content string) (SubagentSpec, error) {
m, err := ParseModuleFile("subagent", content)
if err != nil {
return SubagentSpec{}, err
}
return specFromModule(m), nil
}
func appendBlock(m *core.PromptModule, key, line string) {
switch key {
case "outputContract":
if m.OutputContract == "" {
m.OutputContract = line
} else {
m.OutputContract += "\n" + line
}
}
}
// splitKV 拆 "key: value"。
func splitKV(s string) (string, string) {
i := strings.Index(s, ":")
if i < 0 {
return s, ""
}
return strings.TrimSpace(s[:i]), strings.TrimSpace(s[i+1:])
}
func splitList(s string) []string {
var out []string
for _, p := range strings.Split(s, ",") {
if p = strings.TrimSpace(p); p != "" {
out = append(out, p)
}
}
return out
}
func atoi(s string) int {
n, _ := strconv.Atoi(strings.TrimSpace(s))
return n
}