diff --git a/internal/runmgr/background_card_test.go b/internal/runmgr/background_card_test.go index fcb11d7..d324d5d 100644 --- a/internal/runmgr/background_card_test.go +++ b/internal/runmgr/background_card_test.go @@ -160,6 +160,10 @@ func TestBackgroundCardLifecycleOnFailure(t *testing.T) { if statusLabel != core.TaskSummaryStatusFailed { t.Fatalf("status kv = %q, want %q", statusLabel, core.TaskSummaryStatusFailed) } + failed, err := c.mgr.st.GetRunByID(card.SourceRunID) + if err != nil || failed.Error == nil || failed.Error.Message != runner.err.Error() { + t.Fatalf("failure details were lost from the execution record: %+v %v", failed.Error, err) + } } func TestBackgroundCardCreateUsesL1SkeletonNotRawTask(t *testing.T) { diff --git a/internal/runmgr/runmgr.go b/internal/runmgr/runmgr.go index 9a8d57b..e786dde 100644 --- a/internal/runmgr/runmgr.go +++ b/internal/runmgr/runmgr.go @@ -1800,6 +1800,9 @@ func (m *Manager) finishChild(rc *runCtx, runner runtime.Runner, emitResult bool child.Status = status child.Phase = core.PhaseDone child.Usage = &usage + if execErr != nil { + child.Error = &core.ErrorPart{Code: "run.subagent_failed", Message: execErr.Error()} + } m.st.UpdateRun(&child) if emitResult { m.emit(child.ConversationID, child.ID, core.EvAgentResult, payload) diff --git a/internal/runmgr/task_projection.go b/internal/runmgr/task_projection.go index d13707b..3bac8a4 100644 --- a/internal/runmgr/task_projection.go +++ b/internal/runmgr/task_projection.go @@ -169,6 +169,7 @@ func (m *Manager) finishTaskProjection(b taskBinding, payload core.AgentResultPa } s.DeliveryState = "available" s.Conclusion = taskResultConclusion(payload) + s.NextStep = "核验已有结果和未决事项,再选择后续工作" s.CheckpointKeys = nil if runErr != nil { s.ExecutionState = "failed" @@ -179,7 +180,11 @@ func (m *Manager) finishTaskProjection(b taskBinding, payload core.AgentResultPa if len(s.Findings) > 0 { s.OutcomeCompleteness = "partial" } - s.Conclusion = "执行失败:" + runErr.Error() + s.NextStep = "核对已有发现和材料;需要继续处理时重新提出请求" + s.Conclusion = "执行失败,任务已停止;请核验已有发现和材料限制" + if strings.Contains(runErr.Error(), "budget_exceeded") { + s.Conclusion = "达到本次处理额度,执行已停止;已有发现已保留" + } } if s.Conclusion == "" { s.Conclusion = "没有可用结果摘要" @@ -212,8 +217,15 @@ func (m *Manager) finishTaskProjection(b taskBinding, payload core.AgentResultPa } } } - s.Phase = core.TaskPhase{ID: s.ExecutionState, Label: "处理结束", ObservedAt: time.Now().UTC()} - s.Focus = "处理结束" + label := "处理结束" + if s.ExecutionState == "failed" { + label = "执行已停止" + } + if s.ExecutionState == "interrupted" { + label = "任务已中断" + } + s.Phase = core.TaskPhase{ID: s.ExecutionState, Label: label, ObservedAt: time.Now().UTC()} + s.Focus = label s.LastActivityAt = time.Now().UTC() s.LastMeaningfulUpdateAt = s.LastActivityAt if e := s.Validate(); e != nil { diff --git a/internal/runmgr/task_projection_test.go b/internal/runmgr/task_projection_test.go index db92679..d3df192 100644 --- a/internal/runmgr/task_projection_test.go +++ b/internal/runmgr/task_projection_test.go @@ -3,12 +3,33 @@ package runmgr import ( "agent-system-backend/internal/cardsvc" core "agent-system-core" + "errors" "os" "path/filepath" "strings" "testing" ) +func TestFailedTaskDoesNotPromiseAutomaticContinuation(t *testing.T) { + c := checkpointEnvironment(t) + if err := c.mgr.mutateTask(c.task, func(s *core.TaskSnapshot) error { + s.NextStep = "输出最终结构化整理结果" + return nil + }); err != nil { + t.Fatal(err) + } + if err := c.mgr.finishTaskProjection(c.task, core.AgentResultPayload{}, errors.New("budget_exceeded: run token budget exhausted")); err != nil { + t.Fatal(err) + } + card, err := c.mgr.st.GetCard(c.run.Owner, c.task.cardID) + if err != nil { + t.Fatal(err) + } + if card.Task.Phase.ID != "failed" || card.Task.Phase.Label != "执行已停止" || strings.Contains(card.Task.NextStep, "输出最终") || strings.Contains(card.Task.Conclusion, "budget_exceeded") { + t.Fatalf("stopped task retained a process promise: %+v", card.Task) + } +} + func TestTaskConclusionSkipsProcessText(t *testing.T) { c, _ := newCardSpawnEnv(t, &gateRunner{parts: []core.Part{{Type: core.PartText, Process: true, Text: "我先检查知识库。"}, {Type: core.PartText, Text: "日期仍待裁定,预算审批尚未完成。"}}}) c.mgr.runner = noopRunner{}