test_main_agent_decision_projection.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690
  1. from __future__ import annotations
  2. from app.main_agent_decision_projection import (
  3. MainAgentDecisionProjector,
  4. parse_goal_items,
  5. parse_script_direction,
  6. )
  7. from app.runtime_event_index import RuntimeEventIndex
  8. from app.runtime_tool_catalog import business_tool_label
  9. DIRECTION = """## 选题价值主张
  10. 用真实事件解释行业变化。
  11. ## 账号价值主张
  12. 保持专业深度与信息密度。
  13. ## 创作总目标
  14. ### 目标 1
  15. - 目标语句:拆解机制并让读者看懂因果链。
  16. - 通过标准:结构清楚。
  17. ### 目标 2
  18. - 目标语句:用可核验数据支撑关键结论。
  19. ## 领域评估维度
  20. 1. 数据可信度
  21. 2. 适用边界
  22. """
  23. def _event(
  24. event_id: int,
  25. event_seq: int,
  26. event_type: str,
  27. event_name: str,
  28. **extra,
  29. ):
  30. return {
  31. "id": event_id,
  32. "event_seq": event_seq,
  33. "event_type": event_type,
  34. "event_name": event_name,
  35. "status": "ok",
  36. "ended_at": f"2026-07-14T10:00:{event_seq:02d}",
  37. **extra,
  38. }
  39. def _main_scope(event_id: int = 1):
  40. return _event(
  41. event_id,
  42. 1,
  43. "agent_scope",
  44. "main",
  45. agent_role="main",
  46. agent_depth=0,
  47. scope_event_id=event_id,
  48. parent_event_id=None,
  49. )
  50. def _main_tool(event_id: int, event_seq: int, name: str, **extra):
  51. return _event(
  52. event_id,
  53. event_seq,
  54. "tool_call",
  55. name,
  56. agent_role="main",
  57. agent_depth=0,
  58. scope_event_id=1,
  59. parent_event_id=1,
  60. **extra,
  61. )
  62. def test_runtime_index_requires_real_main_scope_for_direct_reads():
  63. events = [
  64. _main_scope(),
  65. _main_tool(2, 2, "get_topic_detail"),
  66. _event(
  67. 3,
  68. 3,
  69. "tool_call",
  70. "get_topic_detail",
  71. agent_role="script_evaluator",
  72. agent_depth=1,
  73. scope_event_id=30,
  74. parent_event_id=30,
  75. ),
  76. _event(
  77. 4,
  78. 4,
  79. "tool_call",
  80. "get_topic_detail",
  81. agent_role="main",
  82. agent_depth=0,
  83. scope_event_id=999,
  84. parent_event_id=999,
  85. ),
  86. _main_tool(5, 5, "get_domain_info", status="error"),
  87. ]
  88. index = RuntimeEventIndex(events)
  89. assert index.main_scope_ids == {1}
  90. assert [event["id"] for event in index.main_direct_reads()] == [2]
  91. assert [event["id"] for event in index.main_direct_reads(successful=False)] == [5]
  92. assert [event["id"] for event in index.events_for_actor("main")] == [1, 2, 4, 5]
  93. assert [event["id"] for event in index.events_for_actor("overall-evaluator")] == [3]
  94. assert business_tool_label("get_topic_detail") == "选题"
  95. def test_planning_analysis_uses_last_real_main_thought_before_direction_save():
  96. events = [
  97. _main_scope(),
  98. _main_tool(2, 2, "think_and_plan", inputData={
  99. "thought_number": 1,
  100. "thought_summary": "先形成旧规划",
  101. "thought": "旧思考",
  102. "plan": "旧计划",
  103. "action": "旧下一步",
  104. }),
  105. _main_tool(3, 3, "think_and_plan", inputData={
  106. "thought_number": 2,
  107. "thought_summary": "锁定创作目标与首轮骨架",
  108. "thought": "完整分析账号结构与领域下限。",
  109. "plan": "保存目标,然后开启首轮。",
  110. "action": "写入创作总目标",
  111. }),
  112. _main_tool(4, 4, "save_script_direction", inputData={"script_direction": DIRECTION}),
  113. _main_tool(5, 5, "think_and_plan", inputData={
  114. "thought_summary": "保存后的其他规划",
  115. "thought": "不应成为开场规划",
  116. "plan": "后续计划",
  117. "action": "继续",
  118. }),
  119. ]
  120. planning = MainAgentDecisionProjector().project(
  121. RuntimeEventIndex(events),
  122. script_direction=DIRECTION,
  123. rounds=[],
  124. )["planningAnalysis"]
  125. assert planning == {
  126. "stage": "planning-analysis",
  127. "summary": "锁定创作目标与首轮骨架",
  128. "thought": "完整分析账号结构与领域下限。",
  129. "plan": "保存目标,然后开启首轮。",
  130. "action": "写入创作总目标",
  131. "eventRef": "event:3",
  132. "technicalRefs": ["event:3"],
  133. "completeness": "complete",
  134. }
  135. def test_begin_round_prefers_success_output_then_falls_back_to_round_marker():
  136. events = [
  137. _main_scope(),
  138. _main_tool(
  139. 2,
  140. 2,
  141. "begin_round",
  142. round_index=0,
  143. output_preview='{"success": true, "round_index": 1}',
  144. ),
  145. _main_tool(
  146. 3,
  147. 3,
  148. "begin_round",
  149. round_index=1,
  150. output_preview='{"success": true}',
  151. ),
  152. _event(
  153. 4,
  154. 4,
  155. "round_begin",
  156. "2",
  157. agent_role="main",
  158. agent_depth=0,
  159. scope_event_id=1,
  160. parent_event_id=1,
  161. payload={"round_index": 2, "goal": "第二轮"},
  162. ),
  163. ]
  164. index = RuntimeEventIndex(events)
  165. assert index.resolved_begin_round(events[1]) == 1
  166. assert index.resolved_begin_round(events[2]) == 2
  167. assert {key: [event["id"] for event in value] for key, value in index.successful_begin_rounds().items()} == {1: [2], 2: [3]}
  168. def test_evaluator_association_uses_hierarchy_and_structured_round_not_report_text():
  169. events = [
  170. _main_scope(),
  171. _event(
  172. 10,
  173. 2,
  174. "agent_invoke",
  175. "script_evaluator",
  176. agent_role="script_evaluator",
  177. agent_depth=1,
  178. scope_event_id=10,
  179. parent_event_id=1,
  180. round_index=1,
  181. agentOutputData={"summary": "## 评估报告 round=99\n### 整体结论:部分通过"},
  182. ),
  183. _event(
  184. 11,
  185. 3,
  186. "agent_invoke",
  187. "script_evaluator",
  188. agent_role="script_evaluator",
  189. agent_depth=1,
  190. scope_event_id=11,
  191. parent_event_id=None,
  192. round_index=1,
  193. agentOutputData={"summary": "### 整体结论:通过"},
  194. ),
  195. ]
  196. associations = RuntimeEventIndex(events).agent_returns(
  197. "script_evaluator", round_index=1
  198. )
  199. assert [(event["id"], relation) for event, relation in associations] == [
  200. (10, "returned-report"),
  201. (11, "runtime-associated"),
  202. ]
  203. assert RuntimeEventIndex(events).agent_returns("script_evaluator", round_index=99) == []
  204. def test_direction_parser_selects_real_goals_instead_of_value_proposition():
  205. parsed = parse_script_direction(DIRECTION)
  206. assert parsed["goals"] == [
  207. "拆解机制并让读者看懂因果链。",
  208. "用可核验数据支撑关键结论。",
  209. ]
  210. assert parsed["constraints"] == ["数据可信度", "适用边界", "结构清楚。"]
  211. assert parsed["valueJudgements"][0]["label"] == "选题价值主张"
  212. assert parsed["goals"][0] != parsed["valueJudgements"][0]["value"]
  213. assert parsed["raw"] == DIRECTION.strip()
  214. def test_full_projection_preserves_db_truth_and_three_honest_decision_chains():
  215. first_paths = [
  216. {
  217. "path_index": 1,
  218. "path_type": "内容",
  219. "action": "新增",
  220. "target": "机制拆解段",
  221. "method": "案例对照",
  222. "emphasis": "结构可读性",
  223. },
  224. {
  225. "path_index": 2,
  226. "path_type": "领域信息",
  227. "action": "核验",
  228. "target": "行业数据",
  229. "method": "交叉验证",
  230. },
  231. ]
  232. second_paths = [
  233. {"path_index": 1, "path_type": "内容", "action": "补齐", "target": "结尾"}
  234. ]
  235. events = [
  236. _main_scope(),
  237. _main_tool(2, 2, "get_topic_detail", output_preview='{"topic":{"result":"解释真实机制"},"points":[{"point_result":"测试选题"}]}'),
  238. _main_tool(3, 3, "get_account_script_section_patterns", output_preview='{"\u5206\u6bb5\u89c4\u5f8b\u6458\u8981":"成品展示到机制拆解的四段结构"}'),
  239. _main_tool(4, 4, "get_account_script_persona_points", output_preview='{"returned_count":82,"records":[{"\u5217":"形式"},{"\u5217":"作用"}]}'),
  240. _event(
  241. 40,
  242. 5,
  243. "tool_call",
  244. "get_topic_detail",
  245. agent_role="script_evaluator",
  246. agent_depth=1,
  247. scope_event_id=39,
  248. parent_event_id=39,
  249. output_preview='{"count":1}',
  250. ),
  251. _main_tool(
  252. 5,
  253. 6,
  254. "save_script_direction",
  255. inputData={"script_direction": DIRECTION},
  256. output_preview='{"success": true}',
  257. ),
  258. _main_tool(
  259. 6,
  260. 7,
  261. "begin_round",
  262. round_index=0,
  263. inputData={"goal": "搭建机制拆解主结构"},
  264. output_preview='{"success": true, "round_index": 1}',
  265. ),
  266. _event(
  267. 7,
  268. 8,
  269. "round_begin",
  270. "1",
  271. agent_role="main",
  272. agent_depth=0,
  273. scope_event_id=1,
  274. parent_event_id=1,
  275. payload={"round_index": 1},
  276. ),
  277. _main_tool(8, 9, "get_script_snapshot", round_index=1, output_preview='{"count":0}'),
  278. _main_tool(
  279. 9,
  280. 10,
  281. "record_multipath_plan",
  282. round_index=1,
  283. inputData={"paths": first_paths, "mode": "竞争", "note": "一路搭结构,一路核验数据。"},
  284. output_preview='{"success": true, "round_index": 1, "path_count": 2}',
  285. ),
  286. _main_tool(
  287. 90,
  288. 11,
  289. "record_multipath_plan",
  290. round_index=1,
  291. inputData={"paths": [], "mode": "竞争"},
  292. status="error",
  293. output_preview='{"success": false, "error": "paths required"}',
  294. ),
  295. _event(
  296. 10,
  297. 12,
  298. "agent_invoke",
  299. "script_evaluator",
  300. agent_role="script_evaluator",
  301. agent_depth=1,
  302. scope_event_id=10,
  303. parent_event_id=1,
  304. round_index=1,
  305. agentOutputData={
  306. "summary": "## 评估报告 round=7\n### 整体结论:部分通过\n引领目标:补齐结尾\n遗留问题:缺少行动建议"
  307. },
  308. ),
  309. _main_tool(
  310. 11,
  311. 13,
  312. "begin_round",
  313. round_index=1,
  314. inputData={"goal": "补齐结尾。\n具体任务:增加行动建议"},
  315. output_preview='{"success": true, "round_index": 2}',
  316. ),
  317. _event(
  318. 12,
  319. 14,
  320. "round_begin",
  321. "2",
  322. agent_role="main",
  323. agent_depth=0,
  324. scope_event_id=1,
  325. parent_event_id=1,
  326. payload={"round_index": 2},
  327. ),
  328. _main_tool(
  329. 13,
  330. 15,
  331. "record_multipath_plan",
  332. round_index=2,
  333. inputData={"paths": second_paths, "mode": "分工", "note": "任务已收敛,不需要重复探索。"},
  334. output_preview='{"success": true, "round_index": 2, "path_count": 1}',
  335. ),
  336. ]
  337. rounds = [
  338. {
  339. "round_index": 1,
  340. "goal": "搭建机制拆解主结构",
  341. "multipath_plan": first_paths,
  342. "race_or_divide": "竞争",
  343. "plan_note": "一路搭结构,一路核验数据。",
  344. },
  345. {
  346. "round_index": 2,
  347. "goal": "补齐结尾。\n具体任务:增加行动建议",
  348. "multipath_plan": second_paths,
  349. "race_or_divide": "分工",
  350. "plan_note": "任务已收敛,不需要重复探索。",
  351. },
  352. ]
  353. projection = MainAgentDecisionProjector().project(
  354. RuntimeEventIndex(events),
  355. script_direction=DIRECTION,
  356. rounds=rounds,
  357. multipath_decisions=[
  358. {
  359. "id": 501,
  360. "round_index": 1,
  361. "decision": "采用结构方案,保留数据核验结果。",
  362. "reasoning": "两路结果可组合。",
  363. "created_at": "2026-07-14T10:00:11",
  364. }
  365. ],
  366. )
  367. objective = projection["objective"]
  368. assert objective["decisionItems"][0] == "拆解机制并让读者看懂因果链。"
  369. assert [item["label"] for item in objective["inputs"]] == [
  370. "选题",
  371. "账号段落模式",
  372. "账号人设",
  373. ]
  374. assert [item["summary"] for item in objective["inputs"]] == [
  375. "测试选题:解释真实机制",
  376. "成品展示到机制拆解的四段结构",
  377. "返回 82 个账号表达特征,主要涉及形式、作用",
  378. ]
  379. assert 40 not in [int(item["detailRef"].split(":")[1]) for item in objective["inputs"]]
  380. round_two = projection["roundGoalsByRound"][2]
  381. assert [item["relation"] for item in round_two["inputs"][:2]] == [
  382. "returned-report",
  383. "persisted-record",
  384. ]
  385. assert round_two["inputs"][0]["summary"] == "部分通过"
  386. assert "下一步:补齐结尾" in round_two["constraints"]
  387. assert round_two["revisionRefs"] == ["event:11"]
  388. first_plan = projection["implementationPlansByRound"][1]
  389. assert first_plan["decisionItems"] == [
  390. "方案 1 · 新增:机制拆解段",
  391. "方案 2 · 核验:行业数据",
  392. ]
  393. assert first_plan["inputs"][1]["label"] == "当前主脚本"
  394. assert first_plan["currentRevisionRef"] == "event:9"
  395. assert first_plan["technicalRefs"] == ["event:90"]
  396. assert first_plan["routeItems"][0] == {
  397. "pathIndex": 1,
  398. "pathType": "内容",
  399. "action": "新增",
  400. "target": "机制拆解段",
  401. "method": "案例对照",
  402. "emphasis": "结构可读性",
  403. }
  404. second_plan = projection["implementationPlansByRound"][2]
  405. assert second_plan["displayMode"] == "单路"
  406. assert second_plan["rawMode"] == "分工"
  407. assert projection["objective"]["sourceDocument"] == DIRECTION.strip()
  408. first_goal = projection["roundGoalsByRound"][1]
  409. assert first_goal["decisionItems"] == ["搭建机制拆解主结构"]
  410. assert first_goal["sourceDocument"] == "搭建机制拆解主结构"
  411. assert "goalSections" not in first_goal
  412. def test_round_goal_inputs_keep_complete_business_text():
  413. long_goal = "完整创作目标需要保留全部约束和判断依据。" * 24
  414. long_report = "上一轮整体评审的完整结论需要原样传给下一轮。" * 20
  415. long_decision = "上一轮主 Agent 的完整多路决策需要原样保留。" * 20
  416. direction = f"## 创作总目标\n- 目标语句:{long_goal}"
  417. events = [
  418. _main_scope(),
  419. _main_tool(
  420. 2,
  421. 2,
  422. "save_script_direction",
  423. inputData={"script_direction": direction},
  424. output_preview='{"success": true}',
  425. ),
  426. _main_tool(
  427. 3,
  428. 3,
  429. "begin_round",
  430. round_index=0,
  431. output_preview='{"success": true, "round_index": 1}',
  432. ),
  433. _event(
  434. 4,
  435. 4,
  436. "agent_invoke",
  437. "script_evaluator",
  438. agent_role="script_evaluator",
  439. agent_depth=1,
  440. scope_event_id=4,
  441. parent_event_id=1,
  442. round_index=1,
  443. agentOutputData={"summary": f"### 整体结论:{long_report}"},
  444. ),
  445. _main_tool(
  446. 5,
  447. 5,
  448. "begin_round",
  449. round_index=1,
  450. output_preview='{"success": true, "round_index": 2}',
  451. ),
  452. ]
  453. projection = MainAgentDecisionProjector().project(
  454. RuntimeEventIndex(events),
  455. script_direction=direction,
  456. rounds=[
  457. {"round_index": 1, "goal": "第一轮", "multipath_plan": []},
  458. {"round_index": 2, "goal": "第二轮", "multipath_plan": []},
  459. ],
  460. multipath_decisions=[
  461. {
  462. "id": 88,
  463. "round_index": 1,
  464. "decision": long_decision,
  465. "created_at": "2026-07-14T10:00:04",
  466. }
  467. ],
  468. )
  469. first_round_input = projection["roundGoalsByRound"][1]["inputs"][0]["summary"]
  470. second_round_inputs = projection["roundGoalsByRound"][2]["inputs"]
  471. assert first_round_input == "\n\n".join(projection["objective"]["decisionItems"])
  472. assert len(first_round_input) > 220
  473. assert second_round_inputs[0]["summary"] == long_report
  474. assert second_round_inputs[1]["summary"] == long_decision
  475. assert all(not str(item["summary"]).endswith("…") for item in second_round_inputs[:2])
  476. def test_db_only_projection_degrades_without_inventing_runtime_evidence():
  477. projection = MainAgentDecisionProjector().project(
  478. RuntimeEventIndex([]),
  479. script_direction="做一个清晰的行业解释。",
  480. rounds=[
  481. {
  482. "round_index": 1,
  483. "goal": "搭建结构",
  484. "multipath_plan": [{"path_index": 1, "target": "开场"}],
  485. "race_or_divide": "分工",
  486. "plan_note": None,
  487. }
  488. ],
  489. )
  490. assert projection["objective"]["completeness"] == "partial"
  491. assert projection["objective"]["inputs"] == []
  492. assert projection["roundGoalsByRound"][1]["completeness"] == "partial"
  493. assert projection["implementationPlansByRound"][1]["completeness"] == "partial"
  494. assert projection["implementationPlansByRound"][1]["explicitReasoning"] is None
  495. def test_multiple_saves_and_plan_versions_preserve_history_and_separate_failures():
  496. old_direction = "## 创作总目标\n- 目标语句:旧目标"
  497. current_direction = "## 创作总目标\n- 目标语句:新目标"
  498. old_paths = [{"path_index": 1, "action": "新增", "target": "开场"}]
  499. current_paths = [{"path_index": 1, "action": "改写", "target": "开场"}]
  500. events = [
  501. _main_scope(),
  502. _main_tool(2, 2, "save_script_direction", inputData={"script_direction": old_direction}, output_preview='{"success":true}'),
  503. _main_tool(3, 3, "save_script_direction", inputData={"script_direction": current_direction}, output_preview='{"success":true}'),
  504. _main_tool(4, 4, "save_script_direction", status="error", inputData={"script_direction": "失败版"}, output_preview='{"success":false,"error":"write failed"}'),
  505. _main_tool(5, 5, "begin_round", round_index=0, output_preview='{"success":true,"round_index":1}'),
  506. _main_tool(6, 6, "record_multipath_plan", round_index=1, inputData={"paths": old_paths, "mode": "竞争", "note": "初版"}, output_preview='{"success":true,"round_index":1}'),
  507. _main_tool(7, 7, "record_multipath_plan", round_index=1, inputData={"paths": current_paths, "mode": "分工", "note": "修订版"}, output_preview='{"success":true,"round_index":1}'),
  508. ]
  509. projection = MainAgentDecisionProjector().project(
  510. RuntimeEventIndex(events),
  511. script_direction=current_direction,
  512. rounds=[{
  513. "round_index": 1,
  514. "goal": "修正开场",
  515. "multipath_plan": current_paths,
  516. "race_or_divide": "分工",
  517. "plan_note": "修订版",
  518. }],
  519. )
  520. assert projection["objective"]["revisionRefs"] == ["event:2", "event:3"]
  521. assert projection["objective"]["currentRevisionRef"] == "event:3"
  522. assert projection["objective"]["technicalRefs"] == ["event:4"]
  523. plan = projection["implementationPlansByRound"][1]
  524. assert plan["revisionRefs"] == ["event:6", "event:7"]
  525. assert [item["current"] for item in plan["revisions"]] == [False, True]
  526. assert plan["currentRevisionRef"] == "event:7"
  527. def test_goal_parser_keeps_the_saved_goal_as_one_item():
  528. goal = "本轮要补齐结构\n① 增加开场\n② 补齐数据"
  529. assert parse_goal_items(goal) == [goal]
  530. def test_plan_revision_matches_business_alias_without_changing_raw_db_mode():
  531. paths = [{"path_index": 1, "action": "改写", "target": "开场"}]
  532. events = [
  533. _main_scope(),
  534. _main_tool(2, 2, "begin_round", round_index=0, output_preview='{"success":true,"round_index":1}'),
  535. _main_tool(
  536. 3,
  537. 3,
  538. "record_multipath_plan",
  539. round_index=1,
  540. inputData={"paths": paths, "mode": "竞争", "note": "比较两路表达"},
  541. output_preview='{"success":true,"round_index":1}',
  542. ),
  543. ]
  544. plan = MainAgentDecisionProjector().project(
  545. RuntimeEventIndex(events),
  546. script_direction=None,
  547. rounds=[{
  548. "round_index": 1,
  549. "goal": "修正开场",
  550. "multipath_plan": paths,
  551. "race_or_divide": "赛马",
  552. "plan_note": "比较两路表达",
  553. }],
  554. )["implementationPlansByRound"][1]
  555. assert plan["rawMode"] == "赛马"
  556. assert plan["currentRevisionRef"] == "event:3"
  557. def test_failed_begin_round_does_not_reuse_its_old_round_context():
  558. events = [
  559. _main_scope(),
  560. _main_tool(
  561. 2,
  562. 2,
  563. "begin_round",
  564. round_index=1,
  565. status="error",
  566. output_preview='{"success":false,"error":"write failed"}',
  567. ),
  568. ]
  569. projection = MainAgentDecisionProjector().project(
  570. RuntimeEventIndex(events),
  571. script_direction="创作目标",
  572. rounds=[
  573. {"round_index": 1, "goal": "第一轮", "multipath_plan": []},
  574. {"round_index": 2, "goal": "第二轮", "multipath_plan": []},
  575. ],
  576. )
  577. assert projection["roundGoalsByRound"][1]["technicalRefs"] == []
  578. assert projection["roundGoalsByRound"][2]["technicalRefs"] == []
  579. assert projection["unassigned"][0]["eventId"] == 2
  580. assert projection["unassigned"][0]["association"] == "failed-main-decision-call-unassigned"
  581. def test_round_goal_keeps_report_problems_and_rejects_late_db_decisions():
  582. events = [
  583. _main_scope(),
  584. _event(
  585. 10,
  586. 2,
  587. "agent_invoke",
  588. "script_evaluator",
  589. agent_role="script_evaluator",
  590. agent_depth=1,
  591. scope_event_id=10,
  592. parent_event_id=1,
  593. round_index=1,
  594. agentOutputData={
  595. "summary": """### 整体结论:部分通过
  596. **问题详情**:
  597. - **paragraph_id=9**:收尾段落缺失。
  598. **下一步引领目标**:
  599. **补齐收尾并形成闭环。**
  600. """
  601. },
  602. ),
  603. _main_tool(
  604. 11,
  605. 5,
  606. "begin_round",
  607. round_index=1,
  608. started_at="2026-07-14T10:00:05",
  609. output_preview='{"success":true,"round_index":2}',
  610. ),
  611. ]
  612. projection = MainAgentDecisionProjector().project(
  613. RuntimeEventIndex(events),
  614. script_direction="创作目标",
  615. rounds=[
  616. {"round_index": 1, "goal": "第一轮", "multipath_plan": []},
  617. {"round_index": 2, "goal": "补齐收尾", "multipath_plan": []},
  618. ],
  619. multipath_decisions=[
  620. {
  621. "id": 71,
  622. "round_index": 1,
  623. "decision": "采用方案 1",
  624. "created_at": "2026-07-14T10:00:06",
  625. }
  626. ],
  627. )
  628. goal = projection["roundGoalsByRound"][2]
  629. assert "问题:收尾段落缺失。" in goal["constraints"]
  630. assert "下一步:补齐收尾并形成闭环。" in goal["constraints"]
  631. assert not any(value.endswith("---") for value in goal["constraints"])
  632. assert not any(item["label"] == "上一轮主 Agent 多路决策" for item in goal["inputs"])
  633. assert "multipath-decision:71" in goal["technicalRefs"]