From 73ec4a33b2fcc9393afa9b98ea8161981ea701ef Mon Sep 17 00:00:00 2001 From: ddCat <2695092045@qq.com> Date: Mon, 14 Sep 2026 22:15:56 +0800 Subject: [PATCH] =?UTF-8?q?fix(agent):=20Agent=20-=20=E7=BB=88=E6=AD=A2?= =?UTF-8?q?=E7=94=BB=E5=B8=83=E5=AE=A1=E6=89=B9=E9=A2=84=E8=A7=88=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E5=BE=AA=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/app/cloud_agent_runtime.go | 19 +++++ backend/internal/app/cloud_agent_test.go | 77 +++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/backend/internal/app/cloud_agent_runtime.go b/backend/internal/app/cloud_agent_runtime.go index 8b401edba..a6c1c578a 100644 --- a/backend/internal/app/cloud_agent_runtime.go +++ b/backend/internal/app/cloud_agent_runtime.go @@ -598,6 +598,21 @@ func (s *Service) failCloudAgent(run *model.CloudAgentExecution, state *cloudAge }) } +// failCloudAgentAdmission records deterministic tool admission failures in the +// same checkpoint transaction. Transient repository errors must still escape +// the caller so the scheduler can retry them. +func failCloudAgentAdmission(current *model.CloudAgentExecution, state *cloudAgentRuntime, runID string, err error) error { + message := cloudAgentSafeToolError(err) + current.Status = "failed" + current.FailureMessage = message + state.Approval = nil + state.event(runID, "run_failed", map[string]any{ + "text": message, + "reason": "tool_admission_failed", + }) + return cloudAgentSave(current, state) +} + // Only expose known failure categories; raw provider errors can contain URLs and credentials. func cloudAgentModelFailure(task *model.Task) (string, string) { detail, reason := "模型任务未成功", "model_task_failed" @@ -769,6 +784,10 @@ func (s *Service) advanceCloudAgentTool(run *model.CloudAgentExecution, state *c } else { canvasPlan, err := prepareCloudAgentCanvasMutation(repo, run.UserID, state.Request.CanvasID, call) if err != nil { + var appErr *AppError + if errors.As(err, &appErr) && appErr != nil { + return failCloudAgentAdmission(current, state, run.ID, err) + } return err } preview = canvasPlan.Preview diff --git a/backend/internal/app/cloud_agent_test.go b/backend/internal/app/cloud_agent_test.go index 8d91b981c..7bbf35fcd 100644 --- a/backend/internal/app/cloud_agent_test.go +++ b/backend/internal/app/cloud_agent_test.go @@ -573,3 +573,80 @@ func TestCloudAgentNodeTypesExposeExecutableAllowList(t *testing.T) { } } } + +func TestCloudAgentCanvasApprovalAdmissionFailureTerminatesRun(t *testing.T) { + s, db, _, _ := creationTestService(t) + canvas := model.CanvasProject{ID: "agent-canvas", UserID: "user", Title: "test", PayloadJSON: `{"nodes":[]}`} + if err := db.Create(&canvas).Error; err != nil { + t.Fatal(err) + } + req := agentTestRequest() + req.PermissionMode = "request_approval" + root, err := s.CreateCloudAgentRun("user", req, "") + if err != nil { + t.Fatal(err) + } + doc, err := creationDocument(canvas.PayloadJSON) + if err != nil { + t.Fatal(err) + } + args, err := json.Marshal(map[string]any{ + "snapshotHash": cloudAgentCanvasHash(doc), + "ops": []map[string]any{{"type": "unsupported_canvas_op", "id": "bad-op"}}, + }) + if err != nil { + t.Fatal(err) + } + result, err := json.Marshal(map[string]any{ + "toolCalls": []map[string]any{{ + "id": "write-1", "type": "function", "function": map[string]any{ + "name": "canvas_apply_ops", "arguments": string(args), + }, + }}, + }) + if err != nil { + t.Fatal(err) + } + var task model.Task + if err := db.First(&task, "id = ?", root.ID).Error; err != nil { + t.Fatal(err) + } + if err := db.Model(&model.Task{}).Where("id = ?", root.ID).Updates(map[string]any{ + "status": model.TaskStatusSucceeded, "result_json": string(result), "input_json": publicTaskInputJSON(task.InputJSON), + }).Error; err != nil { + t.Fatal(err) + } + + if err := s.advanceCloudAgentByID("user", root.ID); err != nil { + t.Fatal(err) + } + if err := s.advanceCloudAgentByID("user", root.ID); err != nil { + t.Fatal(err) + } + failed, err := s.repo.CloudAgent("user", root.ID) + if err != nil { + t.Fatal(err) + } + state, err := cloudAgentDecode(failed) + if err != nil { + t.Fatal(err) + } + if failed.Status != "failed" || failed.FailureMessage != "不支持的画布写操作" { + t.Fatalf("admission failure did not terminate run: status=%q message=%q", failed.Status, failed.FailureMessage) + } + if len(state.Events) == 0 || state.Events[len(state.Events)-1].Type != "run_failed" || state.Events[len(state.Events)-1].Payload["reason"] != "tool_admission_failed" { + t.Fatalf("missing admission failure event: %+v", state.Events) + } + eventCount := len(state.Events) + if err := s.advanceCloudAgentByID("user", root.ID); err != nil { + t.Fatal(err) + } + failedAgain, err := s.repo.CloudAgent("user", root.ID) + if err != nil { + t.Fatal(err) + } + stateAgain, err := cloudAgentDecode(failedAgain) + if err != nil || len(stateAgain.Events) != eventCount { + t.Fatalf("terminal run was replayed: err=%v events=%d want=%d", err, len(stateAgain.Events), eventCount) + } +}