feat(agent): Agent 工作台 - 计划插话记忆、统一模型选择器并修复摘要与视频入库
将计划清单、运行中插话和个人记忆接入云端 Agent;全站模型选择器统一为左右二级菜单。画布摘要按预算截断不再整轮拒绝,首页视频未知宽高可入库。README 补充贡献者 morrow。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user