Make stopped work actionable and retain detailed execution errors

Why: A budget-stopped task retained a promise to generate a final result, while readable card errors need the full diagnosis in the execution record.

Evidence: Continuation-promise and missing-error-record regressions failed on the prior implementation; the full Backend suite now passes.

Impact: Terminal next steps require user action, stopped work has clear labels, and precise subagent errors persist outside the default task summary.

Refs: Independent A04 model assessment; approved R08 and R10
This commit is contained in:
2026-10-13 04:26:38 +00:00
parent ee65604d83
commit 2a64432fc8
4 changed files with 43 additions and 3 deletions
+4
View File
@@ -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) {
+3
View File
@@ -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)
+15 -3
View File
@@ -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 {
+21
View File
@@ -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{}