feat(agent): Agent 工作台 - 计划插话记忆、统一模型选择器并修复摘要与视频入库

将计划清单、运行中插话和个人记忆接入云端 Agent;全站模型选择器统一为左右二级菜单。画布摘要按预算截断不再整轮拒绝,首页视频未知宽高可入库。README 补充贡献者 morrow。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ddCat
2026-09-17 20:21:25 +08:00
parent b6bae4f957
commit 6892732b06
80 changed files with 7364 additions and 182 deletions

View File

@@ -46,30 +46,37 @@ type cloudAgentApproval struct {
Reason string `json:"reason,omitempty"`
}
type cloudAgentRuntime struct {
Request CloudAgentRequest `json:"request"`
Policy cloudAgentPolicySnapshot `json:"policy"`
ParentID string `json:"parentId,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"`
CreativeAnchor cloudAgentCreativeAnchor `json:"creativeAnchor,omitempty"`
TextHistory []providerTextMessage `json:"textHistory,omitempty"`
Skills []cloudAgentSkill `json:"skills"`
SkillReads map[string]bool `json:"skillReads,omitempty"`
Profile cloudAgentProfileSnapshot `json:"profile"`
ProfileReads map[string]bool `json:"profileReads,omitempty"`
Canonical canonicalAgentRequest `json:"canonical"`
ActiveTaskID string `json:"activeTaskId"`
ActiveTextDraft string `json:"activeTextDraft,omitempty"`
MediaTaskID string `json:"mediaTaskId,omitempty"`
TaskIDs []string `json:"taskIds"`
Step int `json:"step"`
Generations int `json:"generations"`
VideoSeconds int `json:"videoSeconds"`
Calls []cloudAgentCall `json:"calls"`
CallIndex int `json:"callIndex"`
Approval *cloudAgentApproval `json:"approval,omitempty"`
Decisions map[string]string `json:"decisions"`
DecisionSettings map[string]string `json:"decisionSettings,omitempty"`
Events []CloudAgentEvent `json:"events"`
Request CloudAgentRequest `json:"request"`
Policy cloudAgentPolicySnapshot `json:"policy"`
ParentID string `json:"parentId,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"`
CreativeAnchor cloudAgentCreativeAnchor `json:"creativeAnchor,omitempty"`
TextHistory []providerTextMessage `json:"textHistory,omitempty"`
Skills []cloudAgentSkill `json:"skills"`
SkillReads map[string]bool `json:"skillReads,omitempty"`
Profile cloudAgentProfileSnapshot `json:"profile"`
ProfileReads map[string]bool `json:"profileReads,omitempty"`
Canonical canonicalAgentRequest `json:"canonical"`
ActiveTaskID string `json:"activeTaskId"`
ActiveTextDraft string `json:"activeTextDraft,omitempty"`
MediaTaskID string `json:"mediaTaskId,omitempty"`
TaskIDs []string `json:"taskIds"`
Step int `json:"step"`
Generations int `json:"generations"`
VideoSeconds int `json:"videoSeconds"`
Calls []cloudAgentCall `json:"calls"`
CallIndex int `json:"callIndex"`
Approval *cloudAgentApproval `json:"approval,omitempty"`
Decisions map[string]string `json:"decisions"`
DecisionSettings map[string]string `json:"decisionSettings,omitempty"`
ActionNudged bool `json:"actionNudged,omitempty"`
EmptyOutputNudged int `json:"emptyOutputNudged,omitempty"`
StepSnapshotHash string `json:"stepSnapshotHash,omitempty"`
StoryboardTaskID string `json:"storyboardTaskId,omitempty"`
Plan []cloudAgentPlanItem `json:"plan,omitempty"`
PendingInterjections []cloudAgentInterjection `json:"pendingInterjections,omitempty"`
InterjectionIDs []string `json:"interjectionIds,omitempty"`
Events []CloudAgentEvent `json:"events"`
}
func (s *Service) ensureCloudAgentExecution(task *model.Task, initial cloudAgentState) error {
@@ -82,7 +89,10 @@ func (s *Service) ensureCloudAgentExecution(task *model.Task, initial cloudAgent
if err := json.Unmarshal([]byte(task.InputJSON), &input); err != nil {
return err
}
state := cloudAgentRuntime{Request: initial.Request, Policy: initial.Policy, ParentID: initial.ParentID, Fingerprint: initial.Fingerprint, CreativeAnchor: initial.CreativeAnchor, TextHistory: input.TextHistory, Skills: initial.Skills, Profile: initial.Profile, Canonical: input.Requests.Canonical, ActiveTaskID: task.ID, TaskIDs: []string{task.ID}, Step: 1, Decisions: map[string]string{}, Events: []CloudAgentEvent{}}
canonical := input.Requests.Canonical
canonical.SystemPrompt = stripCloudAgentPlanBlock(canonical.SystemPrompt)
canonical.Messages = stripCloudAgentRuntimeContext(canonical.Messages)
state := cloudAgentRuntime{Request: initial.Request, Policy: initial.Policy, ParentID: initial.ParentID, Fingerprint: initial.Fingerprint, CreativeAnchor: initial.CreativeAnchor, TextHistory: input.TextHistory, Skills: initial.Skills, Profile: initial.Profile, Canonical: canonical, ActiveTaskID: task.ID, TaskIDs: []string{task.ID}, Step: 1, Decisions: map[string]string{}, Plan: initial.Plan, Events: []CloudAgentEvent{}}
if len(initial.Skills) > 0 {
state.event(task.ID, "tool_completed", map[string]any{"toolName": "skills_load", "text": fmt.Sprintf("已启用 %d 个技能,正文将按需读取", len(initial.Skills))})
}
@@ -451,9 +461,19 @@ func (s *Service) advanceCloudAgents() {
}
for i := range runs {
s.agentSchedulerCursor = runs[i].ID
if err = s.advanceCloudAgent(&runs[i]); err != nil && !errors.Is(err, repository.ErrCreationConflict) {
log.Printf("agent transition %s: %v", runs[i].ID, err)
if s.terminateStuckCloudAgent(&runs[i]) {
continue
}
err = s.advanceCloudAgent(&runs[i])
if err == nil {
s.clearCloudAgentSchedulerConflict(runs[i].ID)
continue
}
if errors.Is(err, repository.ErrCreationConflict) {
s.noteCloudAgentSchedulerConflict(runs[i].ID)
continue
}
log.Printf("agent transition %s: %v", runs[i].ID, err)
}
}
@@ -523,18 +543,26 @@ func (s *Service) advanceCloudAgent(run *model.CloudAgentExecution) (err error)
if len(calls) == 0 {
calls = result.Legacy
}
if len(result.Text) > 32000 || len(calls) > 8 {
return s.terminateCloudAgent(run, "模型输出超出 Agent 单步限制")
if violation := cloudAgentOutputViolation(result.Text, len(calls)); violation != "" {
return s.correctCloudAgentOutput(run, &state, violation)
}
if err := validateCloudAgentCalls(calls); err != nil {
return s.terminateCloudAgent(run, "模型返回了无效或重复的工具调用")
return s.correctCloudAgentOutput(run, &state, "工具调用无效或重复(callId 不能重复、参数必须是 JSON 对象)")
}
result.ToolCalls = calls
}
if task.Status != model.TaskStatusSucceeded && cloudAgentTruncatedToolArguments(task) {
return s.correctCloudAgentTruncatedCalls(run, &state)
}
if cloudAgentEmptyModelOutput(task) && state.EmptyOutputNudged < cloudAgentMaxEmptyOutputNudges {
return s.correctCloudAgentEmptyOutput(run, &state)
}
return s.repo.MutateCloudAgent(run.UserID, run.ID, run.Revision, func(current *model.CloudAgentExecution, repo *repository.Repository) error {
if task.Status != model.TaskStatusSucceeded {
current.Status = "failed"
text, reason := cloudAgentModelFailure(task)
current.FailureMessage = truncateRunes(text, 1000)
cloudAgentDropInterjections(run.ID, "本轮已结束:"+truncateRunes(text, 120), &state)
state.event(run.ID, "run_failed", map[string]any{"text": text, "reason": reason, "taskId": task.ID})
return cloudAgentSave(current, &state)
}
@@ -549,12 +577,33 @@ func (s *Service) advanceCloudAgent(run *model.CloudAgentExecution) (err error)
}
}
state.ActiveTaskID = ""
if len(calls) > 0 || strings.TrimSpace(result.Text) != "" {
state.EmptyOutputNudged = 0
}
if len(calls) > 0 {
state.ActionNudged = false
state.Canonical.ToolChoice = "auto"
}
state.Calls = calls
state.CallIndex = 0
state.StepSnapshotHash = cloudAgentCaptureStepSnapshotHash(calls)
if len(calls) > 0 {
state.Canonical.Messages = append(state.Canonical.Messages, map[string]any{"role": "assistant", "content": result.Text, "tool_calls": calls})
}
if len(calls) == 0 {
if len(state.PendingInterjections) > 0 {
if !cloudAgentStepBudgetExhausted(&state) {
return cloudAgentSave(current, &state)
}
cloudAgentDropInterjections(run.ID, "本轮已达到模型调用上限", &state)
}
if !cloudAgentStepBudgetExhausted(&state) {
if pending := cloudAgentPendingPlanItems(state.Plan); len(pending) > 0 {
state.Canonical.ToolChoice = "required"
state.Canonical.Messages = append(state.Canonical.Messages, map[string]any{"role": "user", "content": cloudAgentPlanNudgeContent(&state, pending[0])})
return cloudAgentSave(current, &state)
}
}
current.Status = "completed"
}
return cloudAgentSave(current, &state)
@@ -568,8 +617,14 @@ func (s *Service) advanceCloudAgent(run *model.CloudAgentExecution) (err error)
state.SkillReads = nil
state.ProfileReads = nil
}
input := map[string]any{"mode": "text", "prompt": state.Request.Prompt, "agentRequests": map[string]any{"canonical": state.Canonical}, "config": map[string]any{"channelId": state.Request.ChannelID, "channelModelKey": state.Request.ChannelModelKey, "model": firstNonEmpty(state.Request.ChannelModelKey, state.Request.Model)}, "textOptions": map[string]any{"stream": true, "thinking": cloudAgentReasoningEnabled(state.Policy.ReasoningMode)}}
raw, _ := json.Marshal(state.Canonical)
if stepLimit := cloudAgentStepLimit(state.Request); stepLimit > 0 && state.Step >= stepLimit {
return s.failCloudAgent(run, &state, fmt.Sprintf("达到 %d 次模型调用上限,本轮已停止", stepLimit))
}
cloudAgentDrainInterjections(run.ID, &state)
canonical := cloudAgentCanonicalWithPlan(&state)
s.attachCloudAgentLessons(&canonical, run.UserID, cloudAgentLessonTaskText(&state))
input := map[string]any{"mode": "text", "prompt": state.Request.Prompt, "agentRequests": map[string]any{"canonical": canonical}, "config": map[string]any{"channelId": state.Request.ChannelID, "channelModelKey": state.Request.ChannelModelKey, "model": firstNonEmpty(state.Request.ChannelModelKey, state.Request.Model)}, "textOptions": map[string]any{"stream": true, "thinking": cloudAgentReasoningEnabled(state.Policy.ReasoningMode)}}
raw, _ := json.Marshal(canonical)
if len(raw) > 192<<10 {
return s.failCloudAgent(run, &state, "模型上下文超过 192KB 上限")
}
@@ -651,6 +706,8 @@ func (s *Service) terminateCloudAgent(run *model.CloudAgentExecution, message st
func (s *Service) failCloudAgent(run *model.CloudAgentExecution, state *cloudAgentRuntime, message string) error {
return s.repo.MutateCloudAgent(run.UserID, run.ID, run.Revision, func(current *model.CloudAgentExecution, _ *repository.Repository) error {
current.Status = "failed"
current.FailureMessage = truncateRunes(message, 1000)
cloudAgentDropInterjections(run.ID, "本轮已结束:"+truncateRunes(message, 120), state)
state.event(run.ID, "run_failed", map[string]any{"text": message})
return cloudAgentSave(current, state)
})
@@ -797,6 +854,8 @@ func (s *Service) advanceCloudAgentTool(run *model.CloudAgentExecution, state *c
return s.failCloudAgent(run, state, "Agent 工具调用状态无效,本轮已停止")
}
call := state.Calls[state.CallIndex]
call = s.cloudAgentRefreshStepSnapshotHash(run, state, call)
state.Calls[state.CallIndex] = call
if state.Approval != nil && state.Approval.Decision == "" {
return nil
}
@@ -898,6 +957,17 @@ func (s *Service) advanceCloudAgentTool(run *model.CloudAgentExecution, state *c
return cloudAgentSave(current, state)
})
}
if allowed && call.Function.Name == "plan_update" && cloudAgentPlanRequiresFirstApproval(state, call) {
if preview, ok := cloudAgentPlanApprovalPreview(call); ok {
return s.repo.MutateCloudAgent(run.UserID, run.ID, run.Revision, func(current *model.CloudAgentExecution, _ *repository.Repository) error {
approvalID := fmt.Sprintf("%s-%d-%d", run.ID, state.Step, state.CallIndex)
state.Approval = &cloudAgentApproval{ID: approvalID, Call: call, CallHash: cloudAgentApprovalCallHash(call), Preview: preview}
current.Status = "waiting_approval"
state.event(run.ID, "approval_requested", map[string]any{"approvalId": approvalID, "toolName": call.Function.Name, "arguments": json.RawMessage(call.Function.Arguments), "preview": preview, "text": preview.Description})
return cloudAgentSave(current, state)
})
}
}
if allowed && call.Function.Name == "generate_media" && state.Approval != nil && state.Approval.Decision == "approve" {
return s.advanceCloudAgentMedia(run, state, call)
}
@@ -942,6 +1012,17 @@ func (s *Service) advanceCloudAgentTool(run *model.CloudAgentExecution, state *c
default:
result, toolErr = cloudAgentReadTool(repo, run.UserID, state, call)
}
if call.Function.Name == "plan_update" && toolErr == nil {
state.event(run.ID, "plan_updated", map[string]any{"items": state.Plan, "pendingTitles": cloudAgentPendingPlanItems(state.Plan)})
}
if call.Function.Name == "ask_user" && toolErr == nil {
payload, _ := result.(map[string]any)
state.event(run.ID, "user_question", payload)
cloudAgentToolResult(run.ID, state, call, result, nil)
skipRemainingCloudAgentCalls(run.ID, state)
current.Status = "completed"
return cloudAgentSave(current, state)
}
cloudAgentToolResult(run.ID, state, call, result, toolErr)
return cloudAgentSave(current, state)
})