608 lines
27 KiB
Python
608 lines
27 KiB
Python
"""Autonomy gate and prepared action compiler for operating runs."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from ...schemas.pmflow_ai_operating_runtime import (
|
|
PMFlowAIAgentCouncilReview,
|
|
PMFlowAIAgentWorkProduct,
|
|
PMFlowAIAutonomyDecision,
|
|
PMFlowAIConfirmationRequest,
|
|
PMFlowAIExecutionPreparation,
|
|
PMFlowAIOperatingMission,
|
|
PMFlowAIOperatingPlan,
|
|
PMFlowAIPreparedActionPacket,
|
|
PMFlowAIProjectWorldStatePacket,
|
|
clean_text,
|
|
)
|
|
|
|
|
|
PROHIBITED_SIDE_EFFECTS = [
|
|
"write_project_memory",
|
|
"commit_changeset",
|
|
"create_or_update_task",
|
|
"execute_handoff",
|
|
"publish_release",
|
|
"call_external_system",
|
|
]
|
|
|
|
|
|
class PMFlowAIAutonomyPolicyService:
|
|
"""Apply autonomy boundaries after council review.
|
|
|
|
This service consumes action contracts produced by the runtime. It does not
|
|
infer user intent from wording, prompt keys or business keywords.
|
|
"""
|
|
|
|
def apply(
|
|
self,
|
|
*,
|
|
mission: PMFlowAIOperatingMission,
|
|
project_world_state: PMFlowAIProjectWorldStatePacket,
|
|
work_products: list[PMFlowAIAgentWorkProduct],
|
|
council_review: PMFlowAIAgentCouncilReview,
|
|
operating_plan: PMFlowAIOperatingPlan,
|
|
) -> tuple[PMFlowAIAutonomyDecision, PMFlowAIOperatingPlan]:
|
|
prepared_service = PreparedChangeService()
|
|
confirmation_gate = ConfirmationGate()
|
|
execution_service = ExecutionPreparationService()
|
|
prepared_packets = prepared_service.prepare_packets(
|
|
mission=mission,
|
|
project_world_state=project_world_state,
|
|
work_products=work_products,
|
|
operating_plan=operating_plan,
|
|
)
|
|
execution_preparations = execution_service.prepare_handoff_packages(
|
|
mission=mission,
|
|
project_world_state=project_world_state,
|
|
work_products=work_products,
|
|
operating_plan=operating_plan,
|
|
)
|
|
confirmation_requests = confirmation_gate.build_requests(
|
|
mission=mission,
|
|
prepared_packets=prepared_packets,
|
|
execution_preparations=execution_preparations,
|
|
)
|
|
prepared_packets = confirmation_gate.attach_request_ids(
|
|
prepared_packets=prepared_packets,
|
|
confirmation_requests=confirmation_requests,
|
|
)
|
|
execution_preparations = confirmation_gate.attach_execution_request_ids(
|
|
execution_preparations=execution_preparations,
|
|
confirmation_requests=confirmation_requests,
|
|
)
|
|
blocked_actions = _blocked_actions(
|
|
operating_plan=operating_plan,
|
|
council_review=council_review,
|
|
mission=mission,
|
|
)
|
|
safe_suggestions = _safe_suggestions(operating_plan=operating_plan, blocked_actions=blocked_actions)
|
|
policy_findings = _policy_findings(
|
|
mission=mission,
|
|
prepared_packets=prepared_packets,
|
|
confirmation_requests=confirmation_requests,
|
|
execution_preparations=execution_preparations,
|
|
blocked_actions=blocked_actions,
|
|
)
|
|
status = "suggest_only"
|
|
if blocked_actions and not prepared_packets and not execution_preparations:
|
|
status = "blocked"
|
|
elif prepared_packets or execution_preparations or confirmation_requests:
|
|
status = "prepared_waiting_confirmation"
|
|
autonomy_gate = PMFlowAIAutonomyDecision(
|
|
status=status,
|
|
autonomy_level=mission.autonomy_level,
|
|
safe_suggestions=safe_suggestions,
|
|
prepared_action_packets=[item.model_dump(mode="json", exclude_none=True) for item in prepared_packets],
|
|
confirmation_requests=[item.model_dump(mode="json", exclude_none=True) for item in confirmation_requests],
|
|
execution_preparations=[item.model_dump(mode="json", exclude_none=True) for item in execution_preparations],
|
|
blocked_actions=blocked_actions,
|
|
policy_findings=policy_findings,
|
|
prohibited_side_effects=PROHIBITED_SIDE_EFFECTS,
|
|
)
|
|
updated_plan = operating_plan.model_copy(
|
|
update={
|
|
"safe_suggestions": safe_suggestions,
|
|
"prepared_changes": [item.model_dump(mode="json", exclude_none=True) for item in prepared_packets],
|
|
"requires_confirmation": [item.model_dump(mode="json", exclude_none=True) for item in confirmation_requests],
|
|
"blocked_actions": blocked_actions,
|
|
"autonomy_gate": autonomy_gate.model_dump(mode="json", exclude_none=True),
|
|
"prepared_action_packets": [item.model_dump(mode="json", exclude_none=True) for item in prepared_packets],
|
|
"confirmation_requests": [item.model_dump(mode="json", exclude_none=True) for item in confirmation_requests],
|
|
"execution_preparations": [item.model_dump(mode="json", exclude_none=True) for item in execution_preparations],
|
|
}
|
|
)
|
|
return autonomy_gate, updated_plan
|
|
|
|
|
|
class PreparedChangeService:
|
|
"""Build customer-safe prepared action packets without writing state."""
|
|
|
|
def prepare_packets(
|
|
self,
|
|
*,
|
|
mission: PMFlowAIOperatingMission,
|
|
project_world_state: PMFlowAIProjectWorldStatePacket,
|
|
work_products: list[PMFlowAIAgentWorkProduct],
|
|
operating_plan: PMFlowAIOperatingPlan,
|
|
) -> list[PMFlowAIPreparedActionPacket]:
|
|
product_by_agent = {item.agent_id: item for item in work_products}
|
|
packets: list[PMFlowAIPreparedActionPacket] = []
|
|
for action in list(operating_plan.prepared_changes or [])[:12]:
|
|
if not isinstance(action, dict):
|
|
continue
|
|
action_type = clean_text(action.get("action_type"), limit=120)
|
|
source_agent = clean_text(action.get("source_agent"), limit=80)
|
|
family = _family_for_action_type(action_type)
|
|
if not family:
|
|
continue
|
|
packet = PMFlowAIPreparedActionPacket(
|
|
action_family=family,
|
|
title=clean_text(action.get("title"), limit=240),
|
|
target_ref=clean_text(action.get("target_ref") or project_world_state.primary_project_ref, limit=160),
|
|
source_agent=source_agent,
|
|
source_action_type=action_type,
|
|
payload=_prepared_payload(
|
|
action=action,
|
|
family=family,
|
|
mission=mission,
|
|
project_world_state=project_world_state,
|
|
source_product=product_by_agent.get(source_agent),
|
|
),
|
|
evidence_refs=_evidence_refs_for_action(action=action, source_product=product_by_agent.get(source_agent)),
|
|
confirmation_required=True,
|
|
status="prepared_waiting_confirmation",
|
|
prohibited_side_effects=PROHIBITED_SIDE_EFFECTS,
|
|
)
|
|
if packet.title:
|
|
packets.append(packet)
|
|
return _dedupe_packets(packets)
|
|
|
|
|
|
class ConfirmationGate:
|
|
"""Create confirmation requests for every prepared write or handoff."""
|
|
|
|
def build_requests(
|
|
self,
|
|
*,
|
|
mission: PMFlowAIOperatingMission,
|
|
prepared_packets: list[PMFlowAIPreparedActionPacket],
|
|
execution_preparations: list[PMFlowAIExecutionPreparation],
|
|
) -> list[PMFlowAIConfirmationRequest]:
|
|
requests: list[PMFlowAIConfirmationRequest] = []
|
|
for packet in prepared_packets:
|
|
requests.append(
|
|
PMFlowAIConfirmationRequest(
|
|
title=packet.title,
|
|
target_ref=packet.target_ref,
|
|
requested_for=[packet.prepared_action_id],
|
|
confirmation_reason=_confirmation_reason_for_family(packet.action_family),
|
|
required_before=_required_before_for_family(packet.action_family),
|
|
action_label=_action_label_for_family(packet.action_family),
|
|
action_type=_confirmation_action_type(packet.action_family),
|
|
action_payload=_confirmation_action_payload(packet),
|
|
can_execute_after_confirmation=packet.action_family in {"research_recovery", "product_object_projection"},
|
|
status="waiting_for_user_confirmation",
|
|
)
|
|
)
|
|
for item in execution_preparations:
|
|
can_execute = item.readiness == "prepared_waiting_confirmation"
|
|
requests.append(
|
|
PMFlowAIConfirmationRequest(
|
|
title="执行交接",
|
|
target_ref=item.target_ref,
|
|
requested_for=[item.execution_preparation_id],
|
|
confirmation_reason="执行 handoff 会改变项目执行态,只能在用户确认后由执行链路继续处理。",
|
|
required_before=["execute_handoff", "create_or_update_task", "write_project_memory"],
|
|
action_label="创建执行交接" if can_execute else "查看缺口",
|
|
action_type="start_execution_handoff" if can_execute else "review_execution_blockers",
|
|
action_payload=_execution_confirmation_action_payload(item),
|
|
can_execute_after_confirmation=can_execute,
|
|
status="waiting_for_user_confirmation",
|
|
)
|
|
)
|
|
if not requests and mission.autonomy_level in {"execute_after_confirmation", "autonomous_cycle_within_policy"}:
|
|
requests.append(
|
|
PMFlowAIConfirmationRequest(
|
|
title="确认本轮 Operating Plan 的下一步边界",
|
|
target_ref=clean_text(mission.project_scope.object_refs[0] if mission.project_scope.object_refs else "", limit=160),
|
|
requested_for=["operating_plan"],
|
|
confirmation_reason="当前没有可准备写入包,但任何后续写入、执行、发布或外部动作仍必须先确认。",
|
|
required_before=PROHIBITED_SIDE_EFFECTS,
|
|
can_execute_after_confirmation=False,
|
|
status="waiting_for_user_confirmation",
|
|
)
|
|
)
|
|
return requests[:16]
|
|
|
|
def attach_request_ids(
|
|
self,
|
|
*,
|
|
prepared_packets: list[PMFlowAIPreparedActionPacket],
|
|
confirmation_requests: list[PMFlowAIConfirmationRequest],
|
|
) -> list[PMFlowAIPreparedActionPacket]:
|
|
request_by_item = {
|
|
requested_for: request.confirmation_request_id
|
|
for request in confirmation_requests
|
|
for requested_for in request.requested_for
|
|
}
|
|
return [
|
|
item.model_copy(update={"confirmation_request_id": request_by_item.get(item.prepared_action_id)})
|
|
for item in prepared_packets
|
|
]
|
|
|
|
def attach_execution_request_ids(
|
|
self,
|
|
*,
|
|
execution_preparations: list[PMFlowAIExecutionPreparation],
|
|
confirmation_requests: list[PMFlowAIConfirmationRequest],
|
|
) -> list[PMFlowAIExecutionPreparation]:
|
|
request_by_item = {
|
|
requested_for: request.confirmation_request_id
|
|
for request in confirmation_requests
|
|
for requested_for in request.requested_for
|
|
}
|
|
return [
|
|
item.model_copy(update={"required_confirmations": [request_by_item[item.execution_preparation_id]]})
|
|
if item.execution_preparation_id in request_by_item
|
|
else item
|
|
for item in execution_preparations
|
|
]
|
|
|
|
|
|
class ExecutionPreparationService:
|
|
"""Prepare an execution handoff package without starting execution."""
|
|
|
|
def prepare_handoff_packages(
|
|
self,
|
|
*,
|
|
mission: PMFlowAIOperatingMission,
|
|
project_world_state: PMFlowAIProjectWorldStatePacket,
|
|
work_products: list[PMFlowAIAgentWorkProduct],
|
|
operating_plan: PMFlowAIOperatingPlan,
|
|
) -> list[PMFlowAIExecutionPreparation]:
|
|
execution_product = _product_for_agent(work_products=work_products, agent_id="pm_breakdown_agent")
|
|
technical_product = _product_for_agent(work_products=work_products, agent_id="tech_architect_agent")
|
|
if execution_product is None and technical_product is None:
|
|
return []
|
|
target_ref = project_world_state.primary_project_ref or (
|
|
mission.project_scope.object_refs[0] if mission.project_scope.object_refs else ""
|
|
)
|
|
task_candidates = [
|
|
item
|
|
for item in list(operating_plan.priority_order or [])[:5]
|
|
if isinstance(item, dict) and clean_text(item.get("title"), limit=240)
|
|
]
|
|
readiness = "prepared_waiting_confirmation" if task_candidates and project_world_state.status in {"ready", "partial"} else "blocked_by_missing_context"
|
|
blocked_actions: list[dict[str, Any]] = []
|
|
if readiness != "prepared_waiting_confirmation":
|
|
blocked_actions.append(
|
|
{
|
|
"title": "发起执行 handoff",
|
|
"reason": "执行交接需要项目世界状态、任务候选和技术准备度证据。",
|
|
"source": "execution_preparation_gate",
|
|
}
|
|
)
|
|
return [
|
|
PMFlowAIExecutionPreparation(
|
|
target_ref=target_ref,
|
|
readiness=readiness,
|
|
handoff_package={
|
|
"packet_version": "execution_bridge_handoff_preparation.v1",
|
|
"mission_id": mission.mission_id,
|
|
"target_ref": target_ref,
|
|
"task_candidates": task_candidates,
|
|
"technical_readiness": _structured_output(technical_product).get("handoff_readiness"),
|
|
"dependency_gaps": _structured_output(execution_product).get("dependency_gaps", []),
|
|
"execution_capability_graph": {
|
|
"supported_runtimes": ["codex", "cursor", "github_agent", "local_bridge"],
|
|
"required_inputs": ["confirmed_task", "project_memory_snapshot", "acceptance_criteria", "repo_binding"],
|
|
"result_events": ["handoff_created", "execution_package_pulled", "execution_result_reported", "user_acceptance_recorded"],
|
|
},
|
|
"prompt_package_contract": {
|
|
"status": "prepared_contract_only",
|
|
"required_sections": ["mission", "scope", "files_or_modules", "acceptance_tests", "writeback_protocol"],
|
|
"forbidden": ["execute_local_command_without_user_confirmation", "write_project_memory_before_acceptance"],
|
|
},
|
|
"result_writeback_contract": {
|
|
"protocol_version": "execution_result_protocol_v1",
|
|
"expected_fields": ["execution_status", "changed_files", "commands_run", "test_outputs", "blocker_summary"],
|
|
"memory_writeback_policy": "after_user_acceptance_or_review_gate",
|
|
},
|
|
"evidence_refs": _dedupe_texts(
|
|
list(execution_product.evidence_refs if execution_product is not None else [])
|
|
+ list(technical_product.evidence_refs if technical_product is not None else [])
|
|
),
|
|
"execution_boundary": "prepared_package_only_no_handoff_started",
|
|
},
|
|
required_confirmations=[],
|
|
blocked_actions=blocked_actions,
|
|
prohibited_side_effects=PROHIBITED_SIDE_EFFECTS,
|
|
)
|
|
]
|
|
|
|
|
|
def _family_for_action_type(action_type: str) -> str:
|
|
handlers = {
|
|
"prepare_task_change": "task_change",
|
|
"prepare_validation_experiment": "experiment_plan",
|
|
"prepare_requirement_changes": "project_memory_candidate",
|
|
"prepare_product_object": "product_object_projection",
|
|
"continue_research": "research_recovery",
|
|
"prepare_from_project_world_state": "task_change",
|
|
}
|
|
return handlers.get(clean_text(action_type, limit=120), "")
|
|
|
|
|
|
def _prepared_payload(
|
|
*,
|
|
action: dict[str, Any],
|
|
family: str,
|
|
mission: PMFlowAIOperatingMission,
|
|
project_world_state: PMFlowAIProjectWorldStatePacket,
|
|
source_product: PMFlowAIAgentWorkProduct | None,
|
|
) -> dict[str, Any]:
|
|
structured = _structured_output(source_product)
|
|
base = {
|
|
"mission_id": mission.mission_id,
|
|
"target_ref": clean_text(action.get("target_ref") or project_world_state.primary_project_ref, limit=160),
|
|
"title": clean_text(action.get("title"), limit=240),
|
|
"source_product_type": clean_text(source_product.product_type if source_product is not None else "", limit=120),
|
|
"prepared_only": True,
|
|
}
|
|
if family == "task_change":
|
|
return {
|
|
**base,
|
|
"task_change_candidate": {
|
|
"title": base["title"],
|
|
"acceptance_boundary": "must_be_confirmed_before_task_write",
|
|
"priority_order_ref": clean_text(action.get("priority_order_ref"), limit=80),
|
|
"dependency_gaps": structured.get("dependency_gaps", []),
|
|
},
|
|
}
|
|
if family == "experiment_plan":
|
|
return {
|
|
**base,
|
|
"experiment_plan": {
|
|
"hypotheses": structured.get("critical_hypotheses", []),
|
|
"experiments": structured.get("experiments", []),
|
|
"validation_gaps": structured.get("validation_gaps", []),
|
|
},
|
|
}
|
|
if family == "project_memory_candidate":
|
|
return {
|
|
**base,
|
|
"project_memory_candidate": {
|
|
"mvp_scope_read": structured.get("mvp_scope_read", ""),
|
|
"in_scope_candidates": structured.get("in_scope_candidates", []),
|
|
"out_of_scope_now": structured.get("out_of_scope_now", []),
|
|
"write_boundary": "candidate_only_until_user_confirms_project_memory_write",
|
|
},
|
|
}
|
|
if family == "product_object_projection":
|
|
object_type = "OperatingObject"
|
|
if source_product is not None:
|
|
object_type = clean_text(
|
|
structured.get("product_object_type") or structured.get("object_type") or source_product.product_type,
|
|
limit=120,
|
|
) or "OperatingObject"
|
|
return {
|
|
**base,
|
|
"product_object_projection": {
|
|
"object_type": object_type,
|
|
"fields": structured,
|
|
"write_boundary": "candidate_only_until_user_confirms_product_object_projection",
|
|
},
|
|
}
|
|
if family == "research_recovery":
|
|
return {
|
|
**base,
|
|
"research_recovery": {
|
|
"research_question": structured.get("research_question") or mission.original_user_input,
|
|
"source_plan": structured.get("source_plan", []),
|
|
"research_gaps": structured.get("research_gaps", []),
|
|
"market_claims": structured.get("market_claims", []),
|
|
"queries": action.get("research_queries") or [item.get("query") for item in structured.get("source_plan", []) if isinstance(item, dict)],
|
|
"write_boundary": "read_only_research_until_user_confirms_product_object_projection",
|
|
},
|
|
}
|
|
return base
|
|
|
|
|
|
def _confirmation_reason_for_family(action_family: str) -> str:
|
|
reasons = {
|
|
"task_change": "任务变更会影响执行队列,确认前只能作为候选准备包。",
|
|
"experiment_plan": "用户验证实验会改变验证节奏和后续判断,确认前不能写入项目事实。",
|
|
"project_memory_candidate": "Project Memory 候选会改变项目真源,必须确认后才能写入。",
|
|
"product_object_projection": "经营对象候选会进入想法、研究或项目推进链路,确认前只能作为草案展示。",
|
|
"research_recovery": "当前证据还不足以生成机会结论,确认后只继续做只读研究补证,不写项目记忆或创建任务。",
|
|
"execution_handoff_package": "执行交接会触发开发/执行链路,必须确认后才能进入 handoff。",
|
|
}
|
|
return reasons.get(action_family, "该动作可能改变项目状态,必须确认后才能继续。")
|
|
|
|
|
|
def _required_before_for_family(action_family: str) -> list[str]:
|
|
required = {
|
|
"task_change": ["create_or_update_task", "commit_changeset", "write_project_memory"],
|
|
"experiment_plan": ["write_project_memory", "call_external_system"],
|
|
"project_memory_candidate": ["write_project_memory", "commit_changeset"],
|
|
"product_object_projection": ["write_project_memory", "commit_changeset", "create_or_update_task"],
|
|
"research_recovery": ["write_project_memory", "commit_changeset", "create_or_update_task", "execute_handoff"],
|
|
"execution_handoff_package": ["execute_handoff", "create_or_update_task", "write_project_memory"],
|
|
}
|
|
return required.get(action_family, PROHIBITED_SIDE_EFFECTS)
|
|
|
|
|
|
def _action_label_for_family(action_family: str) -> str:
|
|
labels = {
|
|
"research_recovery": "继续补研究",
|
|
"product_object_projection": "保存草案",
|
|
"task_change": "准备任务变更",
|
|
"experiment_plan": "准备验证实验",
|
|
"project_memory_candidate": "准备记忆候选",
|
|
"execution_handoff_package": "准备执行交接",
|
|
}
|
|
return labels.get(action_family, "确认")
|
|
|
|
|
|
def _confirmation_action_type(action_family: str) -> str:
|
|
if action_family == "research_recovery":
|
|
return "continue_research"
|
|
if action_family == "product_object_projection":
|
|
return "save_candidate"
|
|
if action_family == "execution_handoff_package":
|
|
return "start_execution_handoff"
|
|
return "review_prepared_action"
|
|
|
|
|
|
def _confirmation_action_payload(packet: PMFlowAIPreparedActionPacket) -> dict[str, Any]:
|
|
payload = packet.payload if isinstance(packet.payload, dict) else {}
|
|
return {
|
|
"prepared_action_id": packet.prepared_action_id,
|
|
"action_family": packet.action_family,
|
|
"target_ref": packet.target_ref,
|
|
"title": packet.title,
|
|
"payload": payload,
|
|
}
|
|
|
|
|
|
def _execution_confirmation_action_payload(item: PMFlowAIExecutionPreparation) -> dict[str, Any]:
|
|
return {
|
|
"execution_preparation_id": item.execution_preparation_id,
|
|
"action_family": "execution_handoff_package",
|
|
"target_ref": item.target_ref,
|
|
"title": "执行交接",
|
|
"readiness": item.readiness,
|
|
"payload": {
|
|
"handoff_package": item.handoff_package if isinstance(item.handoff_package, dict) else {},
|
|
"blocked_actions": list(item.blocked_actions or []),
|
|
"prohibited_side_effects": list(item.prohibited_side_effects or []),
|
|
},
|
|
}
|
|
|
|
|
|
def _blocked_actions(
|
|
*,
|
|
operating_plan: PMFlowAIOperatingPlan,
|
|
council_review: PMFlowAIAgentCouncilReview,
|
|
mission: PMFlowAIOperatingMission,
|
|
) -> list[dict[str, Any]]:
|
|
rows = [dict(item) for item in list(operating_plan.blocked_actions or []) if isinstance(item, dict)]
|
|
if mission.autonomy_level in {"answer_only", "read_and_diagnose", "propose_plan"}:
|
|
rows.append(
|
|
{
|
|
"title": "准备或执行项目变更",
|
|
"reason": f"本轮 autonomy_level={mission.autonomy_level},只能建议或诊断,不能准备写入/执行包。",
|
|
"source": "autonomy_policy",
|
|
"requires_user_confirm": True,
|
|
}
|
|
)
|
|
for veto in list(council_review.risk_vetoes or []):
|
|
if isinstance(veto, dict):
|
|
rows.append({**veto, "source": "agent_council_risk_veto", "council_status": "blocked"})
|
|
return _dedupe_dicts(rows, key="title")[:16]
|
|
|
|
|
|
def _safe_suggestions(*, operating_plan: PMFlowAIOperatingPlan, blocked_actions: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
blocked_titles = {clean_text(item.get("title"), limit=240).lower() for item in blocked_actions}
|
|
suggestions: list[dict[str, Any]] = []
|
|
for item in list(operating_plan.safe_suggestions or operating_plan.priority_order or [])[:12]:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
title = clean_text(item.get("title"), limit=240)
|
|
if not title or title.lower() in blocked_titles:
|
|
continue
|
|
suggestions.append(
|
|
{
|
|
**item,
|
|
"title": title,
|
|
"autonomy_status": "safe_suggestion",
|
|
"confirmation_required": False,
|
|
"allowed_next_step": "show_to_user",
|
|
}
|
|
)
|
|
return _dedupe_dicts(suggestions, key="title")[:8]
|
|
|
|
|
|
def _policy_findings(
|
|
*,
|
|
mission: PMFlowAIOperatingMission,
|
|
prepared_packets: list[PMFlowAIPreparedActionPacket],
|
|
confirmation_requests: list[PMFlowAIConfirmationRequest],
|
|
execution_preparations: list[PMFlowAIExecutionPreparation],
|
|
blocked_actions: list[dict[str, Any]],
|
|
) -> list[str]:
|
|
findings = [
|
|
"所有 Project Memory 写入、ChangeSet commit、任务变更、执行 handoff、发布和外部动作均被 Autonomy Gate 阻断到确认之后。",
|
|
]
|
|
if prepared_packets:
|
|
findings.append(f"已准备 {len(prepared_packets)} 个候选动作包,全部等待用户确认。")
|
|
if execution_preparations:
|
|
findings.append(f"已准备 {len(execution_preparations)} 个执行交接包,但未发起 handoff。")
|
|
if confirmation_requests:
|
|
findings.append(f"已生成 {len(confirmation_requests)} 个确认请求。")
|
|
if blocked_actions:
|
|
findings.append(f"仍有 {len(blocked_actions)} 个动作被策略或风险阻断。")
|
|
findings.append(f"本轮 autonomy_level={mission.autonomy_level}。")
|
|
return findings[:8]
|
|
|
|
|
|
def _evidence_refs_for_action(*, action: dict[str, Any], source_product: PMFlowAIAgentWorkProduct | None) -> list[str]:
|
|
refs = [clean_text(item, limit=180) for item in list(action.get("evidence_refs") or []) if clean_text(item, limit=180)]
|
|
if not refs and source_product is not None:
|
|
refs = list(source_product.evidence_refs or [])
|
|
return _dedupe_texts(refs)[:12]
|
|
|
|
|
|
def _structured_output(product: PMFlowAIAgentWorkProduct | None) -> dict[str, Any]:
|
|
return dict(product.structured_output or {}) if product is not None and isinstance(product.structured_output, dict) else {}
|
|
|
|
|
|
def _product_for_agent(*, work_products: list[PMFlowAIAgentWorkProduct], agent_id: str) -> PMFlowAIAgentWorkProduct | None:
|
|
return next((item for item in work_products if item.agent_id == agent_id), None)
|
|
|
|
|
|
def _dedupe_packets(rows: list[PMFlowAIPreparedActionPacket]) -> list[PMFlowAIPreparedActionPacket]:
|
|
seen: set[str] = set()
|
|
out: list[PMFlowAIPreparedActionPacket] = []
|
|
for item in rows:
|
|
key = f"{item.action_family}:{item.title}".lower()
|
|
if not item.title or key in seen:
|
|
continue
|
|
seen.add(key)
|
|
out.append(item)
|
|
return out[:12]
|
|
|
|
|
|
def _dedupe_dicts(rows: list[dict[str, Any]], *, key: str) -> list[dict[str, Any]]:
|
|
seen: set[str] = set()
|
|
out: list[dict[str, Any]] = []
|
|
for item in rows:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
value = clean_text(item.get(key), limit=240)
|
|
normalized = value.lower()
|
|
if not value or normalized in seen:
|
|
continue
|
|
seen.add(normalized)
|
|
out.append(item)
|
|
return out
|
|
|
|
|
|
def _dedupe_texts(rows: list[str]) -> list[str]:
|
|
out: list[str] = []
|
|
for item in rows:
|
|
text = clean_text(item, limit=220)
|
|
if text and text not in out:
|
|
out.append(text)
|
|
return out
|
|
|
|
|
|
__all__ = [
|
|
"ConfirmationGate",
|
|
"ExecutionPreparationService",
|
|
"PMFlowAIAutonomyPolicyService",
|
|
"PreparedChangeService",
|
|
]
|