Files
weichenandCursor cd74434b4f Stream tool progress and drop hard agent limits
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>
2026-10-02 02:17:02 +00:00

190 lines
4.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}