Files
ttstd-infinite-canvas/backend/internal/app/cloud_agent_plan.go
ddCat 6892732b06 feat(agent): Agent 工作台 - 计划插话记忆、统一模型选择器并修复摘要与视频入库
将计划清单、运行中插话和个人记忆接入云端 Agent;全站模型选择器统一为左右二级菜单。画布摘要按预算截断不再整轮拒绝,首页视频未知宽高可入库。README 补充贡献者 morrow。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 20:21:25 +08:00

247 lines
8.4 KiB
Go
Raw 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 app
import (
"fmt"
"strings"
)
type cloudAgentPlanItem struct {
ID string `json:"id"`
Title string `json:"title"`
Status string `json:"status"`
}
func cloudAgentPendingPlanItems(plan []cloudAgentPlanItem) []string {
pending := make([]string, 0, len(plan))
for _, item := range plan {
if item.Status != "done" {
pending = append(pending, item.Title)
}
}
return pending
}
func cloudAgentPlanBlock(plan []cloudAgentPlanItem) string {
if len(plan) == 0 {
return ""
}
var b strings.Builder
b.WriteString(cloudAgentPlanBlockMarker)
b.WriteString("当前用户消息是本轮目标。清单只辅助推进仍覆盖在该目标下的步骤;用户已改口时不要回头确认上一轮有没有做完。每完成一项就调用 plan_update 更新它的 status。\n\n")
for _, item := range plan {
mark := "未开始"
switch item.Status {
case "doing":
mark = "进行中"
case "done":
mark = "已完成"
}
b.WriteString(fmt.Sprintf("- [%s] %s. %s\n", mark, item.ID, item.Title))
}
return b.String()
}
const cloudAgentPlanBlockMarker = "\n\n## 本轮待办清单\n\n"
const cloudAgentRuntimeContextMarker = "【运行状态】"
func stripCloudAgentPlanBlock(system string) string {
if i := strings.Index(system, cloudAgentPlanBlockMarker); i >= 0 {
return system[:i]
}
return system
}
func isCloudAgentRuntimeContextMessage(message map[string]any) bool {
if stringField(message, "role") != "user" {
return false
}
return strings.HasPrefix(strings.TrimSpace(stringField(message, "content")), cloudAgentRuntimeContextMarker)
}
func stripCloudAgentRuntimeContext(messages []map[string]any) []map[string]any {
if len(messages) == 0 || !isCloudAgentRuntimeContextMessage(messages[len(messages)-1]) {
return messages
}
return messages[:len(messages)-1]
}
func attachCloudAgentPlan(canonical *canonicalAgentRequest, plan []cloudAgentPlanItem) {
if canonical == nil {
return
}
canonical.SystemPrompt = stripCloudAgentPlanBlock(canonical.SystemPrompt)
canonical.Messages = stripCloudAgentRuntimeContext(canonical.Messages)
block := cloudAgentPlanBlock(plan)
if block == "" {
return
}
canonical.Messages = append(canonical.Messages, map[string]any{
"role": "user",
"content": cloudAgentRuntimeContextMarker + "这不是新的用户指令,只是当前待办状态。当前用户消息仍是本轮目标。\n" + strings.TrimSpace(block),
})
}
func cloudAgentPlanRequiresFirstApproval(state *cloudAgentRuntime, call cloudAgentCall) bool {
if state == nil || state.Approval != nil || len(state.Plan) != 0 || state.Request.PermissionMode != "request_approval" {
return false
}
_, ok := cloudAgentPlanApprovalPreview(call)
return ok
}
func cloudAgentLastMessageIsUserInstruction(messages []map[string]any) bool {
if len(messages) == 0 {
return false
}
last := messages[len(messages)-1]
if stringField(last, "role") != "user" {
return false
}
content := strings.TrimSpace(stringField(last, "content"))
if content == "" {
return false
}
if strings.HasPrefix(content, cloudAgentRuntimeContextMarker) {
return false
}
if strings.HasPrefix(content, "【用户插话】") {
return true
}
if strings.Contains(content, "待办清单") || strings.Contains(content, "现在就调用工具") {
return false
}
return true
}
func cloudAgentUserFirstPrefix(state *cloudAgentRuntime) string {
if state == nil || !cloudAgentLastMessageIsUserInstruction(state.Canonical.Messages) {
return ""
}
last := state.Canonical.Messages[len(state.Canonical.Messages)-1]
userText := truncateRunes(strings.TrimSpace(stringField(last, "content")), 200)
return "用户刚给了新要求:「" + userText + "」。**先按用户的最新要求来做**,做完再继续下面这件事:"
}
func cloudAgentPlanNudgeContent(state *cloudAgentRuntime, pendingTitle string) string {
return cloudAgentUserFirstPrefix(state) + "待办清单里「" + pendingTitle + "」还没完成,这一轮不能算结束。请按这个循环推进:**执行 → 检查结果 → 用 plan_update 更新该项状态 → 做下一项**。现在就调用工具继续。"
}
func cloudAgentPlanRequiredNudgeContent(state *cloudAgentRuntime) string {
return cloudAgentUserFirstPrefix(state) + "你还没有把这次要做的事列成待办清单,服务端无法判断是否做完。请先用 plan_update 把剩余工作一次列全(一项一个可验收的产出,例如「生成镜头2视频」),再按清单逐项推进:每完成一项就更新该项状态,然后做下一项。现在就调用工具。"
}
func cloudAgentPlanApprovalPreview(call cloudAgentCall) (cloudAgentApprovalPreview, bool) {
var args struct {
Items []cloudAgentPlanItem `json:"items"`
}
if err := decodeCloudAgentJSONObject(call.Function.Arguments, &args); err != nil {
return cloudAgentApprovalPreview{}, false
}
items := make([]cloudAgentApprovalPreviewItem, 0, len(args.Items))
for _, entry := range args.Items {
title := strings.TrimSpace(entry.Title)
if title == "" {
continue
}
items = append(items, cloudAgentApprovalPreviewItem{Operation: "plan_step", Summary: truncateRunes(title, 240)})
}
if len(items) < 2 {
return cloudAgentApprovalPreview{}, false
}
return cloudAgentApprovalPreview{
Kind: "plan",
Title: "确认执行计划",
Description: fmt.Sprintf("Agent 把这轮任务拆成 %d 步。确认后才会开始执行;暂不执行则会让它改用别的做法。", len(items)),
Items: items,
}, true
}
func cloudAgentApplyPlanUpdate(state *cloudAgentRuntime, call cloudAgentCall) (any, error) {
var args struct {
Items []cloudAgentPlanItem `json:"items"`
}
if err := decodeCloudAgentJSONObject(call.Function.Arguments, &args); err != nil {
return nil, BadAuthRequest("工具参数必须是只含支持字段的JSON对象")
}
if len(args.Items) > 20 {
return nil, BadAuthRequest("待办清单最多 20 项")
}
seen := map[string]bool{}
for i := range args.Items {
args.Items[i].ID = strings.TrimSpace(args.Items[i].ID)
args.Items[i].Title = strings.TrimSpace(args.Items[i].Title)
if args.Items[i].ID == "" || args.Items[i].Title == "" {
return nil, BadAuthRequest("待办的 id 和 title 不能为空")
}
if seen[args.Items[i].ID] {
return nil, BadAuthRequest("待办 id 重复:" + args.Items[i].ID)
}
seen[args.Items[i].ID] = true
switch args.Items[i].Status {
case "pending", "doing", "done":
default:
args.Items[i].Status = "pending"
}
}
state.Plan = args.Items
return map[string]any{"items": args.Items, "pendingTitles": cloudAgentPendingPlanItems(args.Items)}, nil
}
func cloudAgentAskUser(call cloudAgentCall) (any, error) {
var args struct {
Question string `json:"question"`
Options []struct {
Label string `json:"label"`
Detail string `json:"detail"`
} `json:"options"`
AllowFreeform *bool `json:"allowFreeform"`
}
if err := decodeCloudAgentJSONObject(call.Function.Arguments, &args); err != nil {
return nil, BadAuthRequest("工具参数必须是只含支持字段的JSON对象")
}
question := strings.TrimSpace(args.Question)
if question == "" {
return nil, BadAuthRequest("ask_user 必须给出 question:把要用户拍板的那一个问题一句话写清")
}
options := make([]map[string]any, 0, len(args.Options))
for _, option := range args.Options {
label := strings.TrimSpace(option.Label)
if label == "" {
continue
}
entry := map[string]any{"label": truncateRunes(label, 120)}
if detail := strings.TrimSpace(option.Detail); detail != "" {
entry["detail"] = truncateRunes(detail, 240)
}
options = append(options, entry)
}
if len(options) < 2 {
return nil, BadAuthRequest("ask_user 至少要给 2 个候选项;若你自己能定,直接做完继续,不要问")
}
if len(options) > 6 {
options = options[:6]
}
allowFreeform := true
if args.AllowFreeform != nil {
allowFreeform = *args.AllowFreeform
}
return map[string]any{
"phase": "question",
"question": truncateRunes(question, 400),
"options": options,
"allowFreeform": allowFreeform,
}, nil
}
func skipRemainingCloudAgentCalls(runID string, state *cloudAgentRuntime) {
for index := state.CallIndex + 1; index < len(state.Calls); index++ {
cloudAgentToolResult(runID, state, state.Calls[index], map[string]any{"skipped": true}, BadAuthRequest("本轮已结束(等待用户决定),该调用未执行"))
}
}
func cloudAgentCanonicalWithPlan(state *cloudAgentRuntime) canonicalAgentRequest {
canonical := state.Canonical
attachCloudAgentPlan(&canonical, state.Plan)
return canonical
}