Why: Tool calls stayed invisible until execution finished, and turn, tool, timeout, and output caps stopped long tasks early. Evidence: go test ./internal/runtime/ ./internal/tools/ ./internal/prompts/ ./internal/squadadapter/ ./internal/runmgr/ ./internal/app/ ./internal/settings/ Impact: tool.call.delta and tool.output reach the client; default profile, subagents, and squad no longer cap turns or tool calls; exec timeout defaults to 600s with no hard ceiling. A profile or subagent maxTurns above 0 still applies. Refs: feature/stream-display-overhaul Co-authored-by: Cursor <cursoragent@cursor.com>
190 lines
4.8 KiB
Go
190 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 时用 lightweight;
|
||
// maxTurns 为 0 表示不限制轮次。
|
||
func specFromModule(m core.PromptModule) SubagentSpec {
|
||
spec := SubagentSpec{Tools: m.Tools, Model: m.Model, MaxTurns: m.MaxTurns}
|
||
if spec.Model == "" {
|
||
spec.Model = "lightweight"
|
||
}
|
||
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
|
||
}
|