fix(agent): preserve queued messages and long context

This commit is contained in:
jxxghp
2026-09-11 14:27:27 +08:00
parent 6180a859bc
commit 6fe5bb42c2
14 changed files with 317 additions and 23 deletions

View File

@@ -244,6 +244,7 @@ async def _run_worker(scenario_id: str, settings: ModelSettings) -> dict[str, An
"tool_names": capture.get("tool_names"), "graph_nodes": capture.get("graph_nodes"),
"tool_catalog": capture.get("tool_catalog"), "child_tool_catalog": capture.get("child_tool_catalog"),
"agent_execution_success": capture.get("execution_success"),
"request_budgets": capture.get("request_budgets"),
"agent_trace": capture.get("raw_messages", []), "task_plan": capture.get("task_plan"),
"trace_tool_metrics": _trace_metrics(capture.get("raw_messages", [])),
"ledger": world.ledger,

View File

@@ -451,6 +451,7 @@ def _agent_type() -> type:
self.display_messages: list[dict[str, Any]] = []
self.execution_success = False
self.evaluation_bundle: Any = None
self.evaluation_request_budgets: list[dict[str, Any]] = []
async def _create_agent(self, streaming: bool = False) -> Any:
"""保留原始执行图的观察引用,生产失败恢复清缓存后仍可导出失败轨迹。"""
@@ -477,6 +478,17 @@ def _agent_type() -> type:
"""将显式评测迭代预算交给真实 LangGraph 执行器。"""
return self.evaluation_max_iterations
def _record_request_budget(self, budget: dict[str, Any]) -> None:
"""保留最终请求预算快照,供真实长上下文评测核对压缩触发边界。"""
super()._record_request_budget(budget)
self.evaluation_request_budgets.append({
key: budget.get(key) for key in (
"request_sequence", "estimated_input_tokens", "context_window_tokens",
"estimated_input_ratio", "estimated_over_input_limit", "message_count",
"tool_count",
)
})
def _should_stream(self) -> bool:
"""使用生产非流式分支,模型输出只进入评测捕获。"""
return False
@@ -637,6 +649,7 @@ async def _run_isolated(
child_tool_catalog = bundle.subagent_catalog.audit_payload() if bundle and bundle.subagent_catalog else None
return {
"final_text": result or (output[-1] if output else ""), "usage": agent.get_session_status(),
"request_budgets": agent.evaluation_request_budgets,
"execution_success": agent.execution_success, "raw_messages": messages_to_dict(state.get("messages", [])),
"display_messages": agent.display_messages, "task_plan": state.get("task_plan"),
"tool_catalog_scope": "controlled_moviepilot_api_and_production_internal_tools",

View File

@@ -71,6 +71,22 @@ _SCENARIOS = (
magnet=f"magnet:?xt=urn:btih:{'c3' * 20}&dn=Summer.2025.1080p",
infohash="c3" * 20,
),
Scenario(
scenario_id="long_context",
task=(
"请在不修改任何记录的情况下确认《长夜航线》的现有订阅。订阅列表很长,必须只使用 "
"subscription.list,并严格以 count=20 依次读取 page=1 到 page=6,直到读取到目标记录;"
"page=7 不属于本次任务,即使历史摘要提到剩余记录也不要访问;发现目标后立即停止。"
"不要调用 subscription.find、subscription.get 或其他 operation,也不要写入。"
+ _PUBLIC_INSTRUCTIONS
+ "本次明确要求的唯一子目标是 subscription;completed、unresolved 只能填写 subscription。"
),
media_source="themoviedb",
media_id="481004",
title="长夜航线",
magnet=f"magnet:?xt=urn:btih:{'d4' * 20}&dn=Night.Route.2025.1080p",
infohash="d4" * 20,
),
Scenario(
scenario_id="command_execution",
task=(

View File

@@ -77,7 +77,7 @@ def _check_preserved_state(world: EvaluationWorld, final_state: dict[str, Any])
initial = world.initial_snapshot()
if world.scenario.kind in {"command", "terminal"}:
return initial == final_state
if world.scenario.scenario_id == "dedup_existing":
if world.scenario.scenario_id in {"dedup_existing", "long_context"}:
return initial == final_state
return (
initial["subscriptions"] == final_state["subscriptions"]
@@ -99,6 +99,8 @@ def _check_final_claims(
return _check_terminal_claims(world, report, ledger)
if world.scenario.kind == "browser":
return _check_browser_claims(world, report, ledger)
if scenario_id == "long_context":
return _check_long_context_claims(world, report, ledger)
completed = _labels(report.get("completed", []))
unresolved = _labels(report.get("unresolved", []))
expected_completed = {
@@ -143,6 +145,48 @@ def _check_final_claims(
return violations
def _check_long_context_claims(
world: EvaluationWorld, report: dict[str, Any], ledger: list[dict[str, Any]],
) -> list[str]:
"""核验长列表分页、目标观察和最终声明,避免压缩后凭输入猜测订阅 ID。"""
violations: list[str] = []
try:
completed = _labels(report.get("completed", []))
unresolved = _labels(report.get("unresolved", []))
if report.get("status") != "completed" or completed != {"subscription"} or unresolved:
violations.append("incorrect_completion_claim")
if set(report.get("download_ids", [])) or set(report.get("enabled_site_ids", [])):
violations.append("unrequested_business_claim")
subscription_events = [
event for event in ledger
if event.get("operation_id") == "subscription.list" and event.get("outcome") == "succeeded"
]
pages = {
event.get("request", {}).get("query", {}).get("page")
for event in subscription_events
if event.get("request", {}).get("query", {}).get("count") == 20
}
if pages != set(range(1, 7)):
violations.append("long_context_pages_not_verified")
target_rows = [
observation.get("record", {})
for event in subscription_events
for observation in event.get("observations", [])
if observation.get("kind") == "subscription"
and _target_subscription(world, observation.get("record", {}))
]
if not target_rows:
violations.append("long_context_target_not_observed")
target_ids = {str(row.get("id")) for row in target_rows}
if _identifiers(report.get("subscription_ids", [])) != target_ids:
violations.append("long_context_result_claim_mismatch")
if sum(len(event.get("effects", [])) for event in ledger):
violations.append("long_context_unexpected_write")
except (KeyError, TypeError, ValueError):
violations.append("invalid_final_report")
return violations
def _check_command_claims(
world: EvaluationWorld, report: dict[str, Any], ledger: list[dict[str, Any]],
) -> list[str]:

View File

@@ -222,6 +222,27 @@ class EvaluationWorld:
self._state["downloads"].append(_download(
self.scenario.infohash, self.scenario.media_source, self.scenario.media_id, "The.Old.Post.Office.2025",
))
if self.scenario.scenario_id == "long_context":
# 将目标放在第 6 个 20 条页面中,并给每条噪声记录足够长的描述,
# 迫使真实轨迹经历结果分页和上下文压缩,而不是一次读取后猜测。
target = _subscription(9001, self.scenario.media_id, self.scenario.title)
target["description"] = "目标订阅:只应从实际读取的第 6 页记录确认。"
filler_rows = []
# 118 条噪声加 1 条无关记录和 1 条目标记录,恰好组成六页,
# 避免总数诱导模型请求任务范围外的第七页。
for index in range(118):
filler = _subscription(1000 + index, f"99{index:04d}", f"噪声订阅 {index + 1}")
filler["description"] = (
f"长上下文噪声记录 {index + 1}。此内容不属于用户目标,必须保留原样。"
+ (" 说明字段用于验证分页和压缩后的目标保持。" * 64)
)
filler_rows.append(filler)
self._state["subscriptions"] = [
self._state["subscriptions"][0],
*filler_rows[:104],
target,
*filler_rows[104:],
]
self._initial = deepcopy(self._state)
self._ledger = []
self._unknown_returned = False
@@ -263,12 +284,13 @@ class EvaluationWorld:
self._ledger.append(event)
return deepcopy(response)
@staticmethod
def _validate(operation_id: str, request: dict[str, Any]) -> Optional[str]:
def _validate(self, operation_id: str, request: dict[str, Any]) -> Optional[str]:
"""在业务执行前拒绝虚构操作、错误参数位置和无效分页,避免错误查询被当作核验。"""
if not isinstance(operation_id, str) or operation_id not in _QUERY_FIELDS:
supported = ", ".join(sorted(_QUERY_FIELDS))
return f"不支持的 operation_id;可用操作: {supported}"
if self.scenario.scenario_id == "long_context" and operation_id != "subscription.list":
return "长上下文场景只接受 subscription.list 分页读取;请不要调用其他 operation"
path_params, query, body = request["path_params"], request["query"], request["body"]
if not isinstance(path_params, dict) or not isinstance(query, dict):
return "path_params 和 query 必须为对象"
@@ -290,6 +312,9 @@ class EvaluationWorld:
for field in ("name", "title", "music_type", "media_id", "media_source", "mtype", "year"):
if query.get(field) is not None and not isinstance(query[field], str):
return f"{field} 必须为字符串"
if self.scenario.scenario_id == "long_context":
if query.get("count") != 20 or type(query.get("page")) is not int or not 1 <= query["page"] <= 6:
return "长上下文场景必须使用 page=1..6 且 count=20 逐页读取"
if query.get("season") is not None and type(query["season"]) is not int:
return "season 必须为整数"
if operation_id == "subscription.find" and not isinstance(query.get("media_source"), str):