feat(learning): 修复任务运行记录排序逻辑处理空attempt_index的情况

当RunRecord的attempt_index为None时,之前的排序逻辑会出现问题。
现在通过在排序键中显式处理None值来解决这个问题,
将None值排在前面,并将其转换为0进行比较。

同时添加了单元测试验证团队运行记录(没有attempt_index)的处理情况。
```
This commit is contained in:
2026-06-15 18:00:59 +08:00
parent 4b0bf65ace
commit beddf12bc0
2 changed files with 54 additions and 1 deletions

View File

@ -462,7 +462,15 @@ class SkillLearningService:
@staticmethod
def _representative_task_text(runs: list[RunRecord], *, fallback: str = "") -> str:
ordered = sorted(runs, key=lambda item: (item.attempt_index, item.started_at, item.run_id))
ordered = sorted(
runs,
key=lambda item: (
item.attempt_index is None,
item.attempt_index if item.attempt_index is not None else 0,
item.started_at,
item.run_id,
),
)
for record in ordered:
text = record.task_text.strip()
if text:

View File

@ -591,6 +591,51 @@ def test_skill_learning_service_uses_original_task_text_for_new_skill_theme(tmp_
assert candidates[0].evidence["task_text"] == "Compare direct production restart with staging rollout"
def test_skill_learning_service_handles_team_runs_without_attempt_index(tmp_path: Path) -> None:
store = SkillSpecStore(tmp_path)
run_store = RunMemoryStore(tmp_path / "memory" / "runs")
learning_store = SkillLearningStore(tmp_path / "memory" / "skills")
service = SkillLearningService(
run_store=run_store,
learning_store=learning_store,
draft_service=DraftService(store),
evidence_selector=EvidenceSelector(run_store),
)
now = datetime.now(timezone.utc).isoformat()
run_store.append_run_record(
RunRecord(
run_id="team-run",
session_id="session-task:team:research",
task_id="task-1",
attempt_index=None,
task_text="Research one product",
started_at=now,
ended_at=now,
success=True,
finish_reason="stop",
)
)
run_store.append_run_record(
RunRecord(
run_id="main-run",
session_id="session-task",
task_id="task-1",
attempt_index=1,
task_text="Compare two products and email the report",
started_at=now,
ended_at=now,
success=True,
finish_reason="stop",
feedback={"acceptance_type": "accept"},
)
)
candidates = service.build_learning_candidates_for_task("task-1", final_accepted_run_id="main-run")
assert [candidate.candidate_id for candidate in candidates] == ["new:task:task-1"]
assert candidates[0].evidence["task_text"] == "Compare two products and email the report"
def test_task_theme_uses_first_sentence_for_chinese_text() -> None:
assert (
SkillLearningService._task_theme(