Why: The composed reply copied the registered artifact id and file size out of the tool summary. Evidence: go test ./internal/runtime/ -run 'TestComposeUserPromptOmitsArtifactIDs|TestComposeReplyDropsInternalRefs' Impact: Those tokens are removed from the compose input and from the reply before it is shown. Refs: feature/final-reply-compose Co-authored-by: Cursor <cursoragent@cursor.com>
139 lines
4.9 KiB
Go
139 lines
4.9 KiB
Go
package runtime
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
agentllm "agent-native/runtime/llm"
|
|
core "agent-system-core"
|
|
)
|
|
|
|
// 工件编号和字节数只属于系统记录。收尾模型见过工具摘要后会把它们抄进回复。
|
|
var internalRef = regexp.MustCompile(`art_[0-9a-f]{8,}|\d[\d,]*\s*字节`)
|
|
|
|
const composeFallbackPrompt = `你负责写给用户的最终回复。用户只看这一段,看不到上面的工作过程。
|
|
|
|
规则:
|
|
- 只输出给用户看的一段话。不要写思考过程,不要使用 think 标签,不要写「根据规则」「所以我应该」。
|
|
- 直接回应用户的原始请求。先写结果和结论,再写交付了什么、要点是什么、还可以怎么调整。
|
|
- 不要复述过程,不要提内部检查、lint、重试、验证器、评分、文件路径、字节数或工件编号。
|
|
- 检查结论只用来判断有没有没做完的事。文件已经按用户的要求生成时,不要把评分或版式意见告诉用户。
|
|
- 只有用户的请求确实没完成时才说明,用用户能懂的话,不要引用检查原文。
|
|
- 不要编造材料里没有的内容。使用与用户相同的语言。`
|
|
|
|
func (l *LLMRunner) composePrompt(owner string) string {
|
|
if l.Prompts != nil {
|
|
if body, ok, err := l.Prompts.ModuleBody(owner, "aux.compose.final"); err == nil && ok && strings.TrimSpace(body) != "" {
|
|
return body
|
|
}
|
|
}
|
|
return composeFallbackPrompt
|
|
}
|
|
|
|
// composeFinal 在工作循环结束后,用主模型单独写一段直接回答用户的回复。
|
|
// 失败或空结果时恢复草稿,不影响这一轮已经成功结束。
|
|
func (st *directRun) composeFinal(stream agentllm.StreamFunc, model, notice string, opts agentllm.StreamOptions) bool {
|
|
_ = st.rc.Emit(core.EvRunPhase, core.RunPhasePayload{Phase: core.PhaseFinalizing})
|
|
draftIdx := st.demoteLastText()
|
|
draft := ""
|
|
if draftIdx >= 0 {
|
|
draft = st.finalParts[draftIdx].Text
|
|
}
|
|
msgs := []agentllm.Message{
|
|
{Role: agentllm.RoleSystem, Content: st.l.composePrompt(st.rc.Profile().Owner)},
|
|
{Role: agentllm.RoleUser, Content: st.composeUserPrompt(draft, notice)},
|
|
}
|
|
var b strings.Builder
|
|
failed := false
|
|
for ev := range stream(st.rc.Context(), model, msgs, nil, opts) {
|
|
switch ev.Kind {
|
|
case agentllm.KindTextDelta:
|
|
b.WriteString(ev.Text)
|
|
case agentllm.KindError:
|
|
failed = true
|
|
case agentllm.KindMessageEnd:
|
|
st.usageTotal.PromptTokens += ev.Usage.PromptTokens
|
|
st.usageTotal.CompletionTokens += ev.Usage.CompletionTokens
|
|
st.usageTotal.TotalTokens += ev.Usage.TotalTokens
|
|
if b.Len() == 0 {
|
|
b.WriteString(ev.Message.Content)
|
|
}
|
|
}
|
|
}
|
|
// 有的模型把推理放进 think 再写正文。推理不发给用户;只发出剥离后的正文。
|
|
body, _ := splitThinkBlock(b.String())
|
|
body = scrubInternal(body)
|
|
if failed || body == "" || strings.Contains(body, "<think>") {
|
|
if draftIdx >= 0 && draftIdx < len(st.finalParts) && st.finalParts[draftIdx].Type == core.PartText {
|
|
st.finalParts[draftIdx].Process = false
|
|
}
|
|
return false
|
|
}
|
|
_ = st.rc.Emit(core.EvMessageDelta, core.MessageDeltaPayload{Index: st.textIdx, Text: body})
|
|
st.finalParts = append(st.finalParts, core.Part{Type: core.PartText, Text: body})
|
|
st.textIdx++
|
|
return true
|
|
}
|
|
|
|
func (st *directRun) composeUserPrompt(draft, notice string) string {
|
|
var b strings.Builder
|
|
b.WriteString("用户的原始请求:\n")
|
|
if user := strings.TrimSpace(currentUserText(st.rc)); user == "" {
|
|
b.WriteString("(无)\n")
|
|
} else {
|
|
b.WriteString(user)
|
|
b.WriteString("\n")
|
|
}
|
|
b.WriteString("\n工作过程中的草稿回复(不要照抄过程,把它整理成给用户的回复):\n")
|
|
if strings.TrimSpace(draft) == "" {
|
|
b.WriteString("(无)\n")
|
|
} else {
|
|
b.WriteString(scrubInternal(draft))
|
|
b.WriteString("\n")
|
|
}
|
|
b.WriteString("\n工具过程:\n")
|
|
if len(st.evidence) == 0 {
|
|
b.WriteString("(无)\n")
|
|
}
|
|
for i, ev := range st.evidence {
|
|
fmt.Fprintf(&b, "#%d %s %s %s\n", i+1, ev.Name, ev.Status, scrubInternal(ev.Summary))
|
|
}
|
|
b.WriteString("\n交付物:\n")
|
|
names := 0
|
|
for _, ref := range st.deliverables {
|
|
if ref == nil || strings.TrimSpace(ref.Name) == "" {
|
|
continue
|
|
}
|
|
fmt.Fprintf(&b, "- %s\n", ref.Name)
|
|
names++
|
|
}
|
|
if names == 0 {
|
|
b.WriteString("(无)\n")
|
|
}
|
|
b.WriteString("\n检查结论:\n")
|
|
if len(st.checks) == 0 {
|
|
b.WriteString("(无)\n")
|
|
}
|
|
for _, c := range st.checks {
|
|
if c.Passed {
|
|
fmt.Fprintf(&b, "- 通过:%s\n", scrubInternal(c.Reason))
|
|
continue
|
|
}
|
|
fmt.Fprintf(&b, "- 未通过:%s\n", scrubInternal(c.Reason))
|
|
}
|
|
if strings.TrimSpace(notice) != "" {
|
|
b.WriteString("\n仍未完成(必须在回复里如实说明,不要假装已经完成):\n")
|
|
b.WriteString(scrubInternal(notice))
|
|
b.WriteString("\n")
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func scrubInternal(s string) string {
|
|
s = internalRef.ReplaceAllString(s, "")
|
|
s = strings.ReplaceAll(s, "()", "")
|
|
s = strings.ReplaceAll(s, "()", "")
|
|
return strings.TrimSpace(s)
|
|
}
|