"""Durable Agent OS runtime for user-facing PMFlow AI runs.""" from __future__ import annotations import re from datetime import datetime from typing import Any from sqlalchemy.ext.asyncio import AsyncSession from ...models import ( PMFlowAgentArtifact, PMFlowAgentMemory, PMFlowAgentObservation, PMFlowAgentRun, PMFlowAgentStep, PMFlowAgentVerification, User, ) from ...schemas.pmflow_agent_runtime import ( PMFlowAgentArtifactPacket, PMFlowAgentMemoryPacket, PMFlowAgentObservationPacket, PMFlowAgentRunPacket, PMFlowAgentStepPacket, PMFlowAgentVerificationPacket, customer_safe_pmflow_agent_run_packet, new_pmflow_agent_id, utc_now_iso, ) from ...schemas.rag import RAGAskRequest from ..runtime_audit_context import bind_agent_audit_context PMFLOW_AGENT_OS_POLICY_VERSION = "pmflow-agent-os-v2" class PMFlowAgentRuntimeService: """Own the PMFlow AI Agent Run truth source. QARecord is kept as transcript/history only. The current run state, steps, observations, artifacts, verifications and memories are written to Agent OS tables first, then projected into the customer payload. """ def __init__( self, db: AsyncSession, *, tenant_id: int, delivery_runtime: Any | None = None, environment_service: Any | None = None, specialist_runtime: Any | None = None, agent_loop: Any | None = None, answer_arbitrator: Any | None = None, answer_composer: Any | None = None, answer_critic: Any | None = None, ) -> None: self.db = db self.tenant_id = int(tenant_id or 1) # The product entry now uses a dynamic observe-plan-act-critic-answer # agent runtime. The argument name is kept for constructor compatibility; # when present it must expose run_turn() and is treated as the dynamic # agent delegate, not as a legacy answer override. self.delivery_runtime = delivery_runtime self.environment_service = environment_service self._legacy_constructor_args = { "specialist_runtime": specialist_runtime, "agent_loop": agent_loop, "answer_arbitrator": answer_arbitrator, "answer_composer": answer_composer, "answer_critic": answer_critic, } async def run_space_turn( self, *, request: RAGAskRequest, current_user: User, conversation_context: dict[str, Any] | None, tenant_project_context: dict[str, Any] | None, started_at: float | None = None, event_sink: Any | None = None, ) -> dict[str, Any]: run = PMFlowAgentRun( tenant_id=self.tenant_id, public_run_id=new_pmflow_agent_id("run"), user_id=int(getattr(current_user, "id", 0) or 0) or None, qa_session_id=_safe_int(getattr(request, "session_id", None)), workspace_scope="space", surface="space", presentation_mode="pmflow_ai", source_channel=str(getattr(request, "asked_via", "") or "pmflow")[:40], source_ref=str(getattr(request, "source_chat_id", "") or "")[:160] or None, title=_title_from_question(getattr(request, "question", "")), goal=str(getattr(request, "question", "") or "").strip(), status="active", active_object_refs=[], run_metadata={ "runtime": "pmflow_agent_os", "runtime_contract": "mission_first_multi_agent_operating_runtime", "policy_version": PMFLOW_AGENT_OS_POLICY_VERSION, "conversation_session_id": _safe_int((conversation_context or {}).get("session_id")), }, started_at=datetime.utcnow(), ) self.db.add(run) await self.db.flush() try: run_kwargs = { "request": request, "current_user": current_user, "conversation_context": conversation_context, "tenant_project_context": tenant_project_context, "started_at": started_at, } if event_sink is not None: run_kwargs["event_sink"] = event_sink with bind_agent_audit_context(agent_run_id=run.public_run_id): agent_payload = await self._dynamic_agent_runtime().run_turn(**run_kwargs) answer = str(agent_payload.get("answer") or agent_payload.get("reply_text") or "").strip() agent_run_packet = _dict(agent_payload.get("agent_run_packet")) run.active_object_refs = _agent_payload_object_refs(agent_payload) run.status = _durable_status_from_agent_payload(agent_payload, answer=answer) run.final_answer = answer.strip() or None run.completed_at = datetime.utcnow() run.run_metadata = { **dict(run.run_metadata or {}), "runtime": "pmflow_agent_os", "runtime_contract": "mission_first_multi_agent_operating_runtime", "delegated_runtime": str(agent_payload.get("answer_mode") or agent_payload.get("processing_stage") or "pmflow_ai_operating_runtime"), "delegated_run_id": str(agent_run_packet.get("run_id") or ""), "step_count": len(list(agent_run_packet.get("steps") or [])), "observation_count": len(list(agent_run_packet.get("observations") or [])), "artifact_count": 2 if answer else 1, "verification_count": len(_agent_verifications_from_payload(agent_payload, answer=answer)), "stop_reason": str(agent_run_packet.get("stop_reason") or ""), "iterations": _safe_int(agent_run_packet.get("iterations")), "tool_call_count": _safe_int(agent_run_packet.get("tool_call_count")), } packet = await self._persist_agent_runtime_payload( run=run, agent_payload=agent_payload, answer=answer, ) payload = self._build_customer_payload_from_agent_runtime( request=request, run=run, packet=packet, agent_payload=agent_payload, answer=answer, ) await self.db.flush() return payload except Exception as exc: run.status = "failed" run.last_error = _customer_safe_error_summary(exc) run.completed_at = datetime.utcnow() await self.db.flush() raise def _dynamic_agent_runtime(self) -> Any: if self.delivery_runtime is not None and callable(getattr(self.delivery_runtime, "run_turn", None)): return self.delivery_runtime from ..pmflow_ai_operating_runtime import PMFlowAIOperatingRunService return PMFlowAIOperatingRunService( self.db, tenant_id=self.tenant_id, ) async def _persist_agent_runtime_payload( self, *, run: PMFlowAgentRun, agent_payload: dict[str, Any], answer: str, ) -> PMFlowAgentRunPacket: steps = _steps_from_agent_payload(agent_payload) step_rows_by_public_id: dict[str, PMFlowAgentStep] = {} for step in steps: row = PMFlowAgentStep( tenant_id=self.tenant_id, run_id=int(run.id), public_step_id=step.step_id, step_index=int(step.index), step_kind=step.kind, status=step.status, input_summary=(run.goal or "")[:1000], output_summary=step.summary, tool_name=step.tool_name, duration_ms=step.duration_ms, step_payload=step.model_dump(mode="json", exclude_none=True), completed_at=datetime.utcnow(), ) self.db.add(row) step_rows_by_public_id[step.step_id] = row await self.db.flush() observations = _observations_from_agent_payload(agent_payload, steps=steps) for observation in observations: source_step = step_rows_by_public_id.get(str(observation.step_id or "")) self.db.add( PMFlowAgentObservation( tenant_id=self.tenant_id, run_id=int(run.id), step_id=int(source_step.id) if source_step is not None and getattr(source_step, "id", None) is not None else None, public_observation_id=observation.observation_id, source_kind=observation.source_kind, customer_safe_summary=observation.summary, confidence=observation.confidence, facts=list(observation.facts or []), gaps=list(observation.gaps or []), raw_payload=observation.model_dump(mode="json", exclude_none=True), observed_at=datetime.utcnow(), ) ) artifacts = _artifacts_from_agent_payload(agent_payload, answer=answer) for artifact in artifacts: self.db.add( PMFlowAgentArtifact( tenant_id=self.tenant_id, run_id=int(run.id), step_id=None, public_artifact_id=artifact.artifact_id, artifact_kind=artifact.kind, title=artifact.title, status=artifact.status, payload=dict(artifact.payload or {}), customer_safe_payload=artifact.model_dump(mode="json", exclude_none=True), created_by_step_id=steps[-1].step_id if steps else None, ) ) verifications = _agent_verifications_from_payload(agent_payload, answer=answer) for verification in verifications: self.db.add( PMFlowAgentVerification( tenant_id=self.tenant_id, run_id=int(run.id), step_id=None, public_verification_id=verification.verification_id, target_ref=verification.target_ref, result=verification.result, retry_would_change_state=verification.retry_would_change_state, findings=list(verification.findings or []), verification_payload=verification.model_dump(mode="json", exclude_none=True), verified_at=datetime.utcnow(), ) ) memories = _agent_memories_from_payload(agent_payload) for memory in memories: self.db.add( PMFlowAgentMemory( tenant_id=self.tenant_id, run_id=int(run.id), memory_kind=memory.memory_kind, scope_ref=memory.scope_ref, outcome_signal=memory.outcome_signal, content={"summary": memory.summary}, memory_payload=memory.model_dump(mode="json", exclude_none=True), ) ) return PMFlowAgentRunPacket( run_id=run.public_run_id, status=run.status, goal=run.goal, surface=run.surface or "space", presentation_mode=run.presentation_mode or "pmflow_ai", steps=steps, observations=observations, artifacts=artifacts, verifications=verifications, memories=memories, command_payload=_dict(agent_payload.get("command_payload") or agent_payload.get("answer_canvas")), created_at=utc_now_iso(), completed_at=utc_now_iso(), ) def _build_customer_payload_from_agent_runtime( self, *, request: RAGAskRequest, run: PMFlowAgentRun, packet: PMFlowAgentRunPacket, agent_payload: dict[str, Any], answer: str, ) -> dict[str, Any]: safe_packet = customer_safe_pmflow_agent_run_packet(packet) process_packet = _dict(agent_payload.get("answer_process_packet")) answer_canvas = _dict(agent_payload.get("answer_canvas")) or _answer_canvas(answer=answer, packet=packet) customer_evidence_packet = _dict(agent_payload.get("customer_evidence_packet")) or _customer_evidence_packet( answer=answer, packet=packet, ) deliverable_packet = _dict(agent_payload.get("deliverable_packet")) terminal_state = _dict(agent_payload.get("terminal_answer_state")) payload = { "answer": answer, "reply_text": answer, "final_answer": answer, "run_id": run.public_run_id, "sources": list(agent_payload.get("sources") or []), "sources_used": list(agent_payload.get("sources_used") or agent_payload.get("sources") or []), "thinking_mode": getattr(request, "thinking_mode", None) or "normal", "processing_stage": "pmflow_agent_runtime", "answer_mode": "pmflow_agent_runtime", "grounding_status": str(agent_payload.get("grounding_status") or ("grounded" if packet.observations else "partially_grounded")), "policy_version": PMFLOW_AGENT_OS_POLICY_VERSION, "pmflow_agent_run": safe_packet, "agent_os_environment": { "packet_version": "pmflow_agent_environment.v1", "visibility": "customer", "steps": safe_packet.get("steps") or [], "observations": safe_packet.get("observations") or [], }, "agent_os_run_id": run.public_run_id, "agent_os_steps": safe_packet.get("steps") or [], "agent_os_observations": safe_packet.get("observations") or [], "agent_os_artifacts": safe_packet.get("artifacts") or [], "agent_os_verifications": safe_packet.get("verifications") or [], "agent_os_memories": safe_packet.get("memories") or [], "operating_mission": _dict(agent_payload.get("operating_mission")), "project_world_state_packet": _dict(agent_payload.get("project_world_state_packet")), "operating_run_packet": _dict(agent_payload.get("operating_run_packet")), "agent_os_specialists": {"runtime": "mission_first_operating_runtime"} if _dict(agent_payload.get("operating_mission")) else {}, "agent_os_specialist_assignments": list(agent_payload.get("agent_assignments") or []), "agent_os_work_products": list(agent_payload.get("agent_work_products") or []), "agent_os_agent_council_review": _dict(agent_payload.get("agent_council_review")), "agent_os_operating_plan": _dict(agent_payload.get("operating_plan")), "agent_os_autonomy_gate": _dict(agent_payload.get("autonomy_gate")), "agent_os_operating_cycle": _dict(agent_payload.get("operating_cycle")), "agent_os_consumed_learning_ledger": _dict(agent_payload.get("consumed_learning_ledger")), "agent_os_outcome_observations": list(agent_payload.get("outcome_observations") or []), "agent_os_learning_ledger": _dict(agent_payload.get("learning_ledger")), "agent_os_learning_updates": list(agent_payload.get("learning_updates") or []), "agent_os_next_cycle_triggers": list(agent_payload.get("next_cycle_triggers") or []), "agent_os_prepared_action_packets": _prepared_actions_from_agent_payload(agent_payload), "agent_os_confirmation_requests": _confirmation_requests_from_agent_payload(agent_payload), "agent_os_execution_preparations": list(agent_payload.get("execution_preparations") or []), "agent_os_product_object_projections": list(agent_payload.get("product_object_projections") or []), "agent_os_command_payload": _dict(agent_payload.get("command_payload")), "agent_assignments": list(agent_payload.get("agent_assignments") or []), "agent_work_products": list(agent_payload.get("agent_work_products") or []), "agent_council_review": _dict(agent_payload.get("agent_council_review")), "autonomy_gate": _dict(agent_payload.get("autonomy_gate")), "prepared_action_packets": _prepared_actions_from_agent_payload(agent_payload), "confirmation_requests": _confirmation_requests_from_agent_payload(agent_payload), "execution_preparations": list(agent_payload.get("execution_preparations") or []), "product_object_projections": list(agent_payload.get("product_object_projections") or []), "operating_cycle": _dict(agent_payload.get("operating_cycle")), "consumed_learning_ledger": _dict(agent_payload.get("consumed_learning_ledger")), "outcome_observations": list(agent_payload.get("outcome_observations") or []), "learning_ledger": _dict(agent_payload.get("learning_ledger")), "learning_updates": list(agent_payload.get("learning_updates") or []), "next_cycle_triggers": list(agent_payload.get("next_cycle_triggers") or []), "operating_plan": _dict(agent_payload.get("operating_plan")), "command_payload": _dict(agent_payload.get("command_payload")), "answer_process_packet": process_packet or _answer_process_packet(packet), "customer_evidence_packet": customer_evidence_packet, "answer_canvas": answer_canvas, "deliverable_packet": deliverable_packet, "terminal_answer_state": terminal_state, "turn_presentation_contract": _dict(agent_payload.get("turn_presentation_contract")), "customer_interaction_context_packet": _customer_interaction_context_from_agent_payload(agent_payload), # Kept for customer payload sanitizers and debug-only product gates. # The PMFlow AI workspace does not render these raw packets. "agent_run_packet": _dict(agent_payload.get("agent_run_packet")), "research_workspace": _dict(agent_payload.get("research_workspace")), } return payload def _answer_process_packet(packet: PMFlowAgentRunPacket) -> dict[str, Any]: return { "packet_version": "customer_answer_process.v1", "visibility": "customer", "answer_type": "pmflow_agent_runtime", "route_display": "pmflow_agent_runtime", "steps": [ { "key": step.kind, "label": step.label, "status": step.status, "display_summary": step.summary, "duration_ms": step.duration_ms, } for step in packet.steps ], "total_duration_ms": sum(int(step.duration_ms or 0) for step in packet.steps), } def _steps_from_agent_payload(agent_payload: dict[str, Any]) -> list[PMFlowAgentStepPacket]: agent_run = _dict(agent_payload.get("agent_run_packet")) rows = list(agent_run.get("steps") or []) if not rows: process = _dict(agent_payload.get("answer_process_packet")) rows = list(process.get("steps") or []) steps: list[PMFlowAgentStepPacket] = [] for index, row in enumerate(rows[:32]): if not isinstance(row, dict): continue step_key = str(row.get("key") or row.get("step_id") or f"agent_step_{index + 1}").strip()[:80] selected_tool = str(row.get("selected_tool") or row.get("tool_name") or "").strip()[:120] or None step_type = str(row.get("step_type") or row.get("type") or row.get("key") or "agent_step").strip()[:64] kind = selected_tool if step_type == "tool" and selected_tool else step_type label = str(row.get("label") or _agent_step_label(kind) or f"推进第 {index + 1} 步").strip()[:160] summary = str(row.get("display_summary") or row.get("summary") or row.get("description") or "").strip()[:1000] status = _durable_step_status(row.get("status"), blocking=bool(row.get("blocking"))) steps.append( PMFlowAgentStepPacket( step_id=str(row.get("step_id") or step_key or new_pmflow_agent_id("step"))[:80], index=index, kind=kind or "agent_step", status=status, label=label, summary=summary or label, tool_name=selected_tool, duration_ms=_safe_int(row.get("duration_ms")), ) ) if not steps: steps.append( PMFlowAgentStepPacket( step_id=new_pmflow_agent_id("step"), index=0, kind="agent_answer_runtime", status="completed" if str(agent_payload.get("answer") or "").strip() else "blocked", label="生成回答", summary="已根据本轮客户安全状态组织回答。", ) ) return _dedupe_steps(steps) def _observations_from_agent_payload(agent_payload: dict[str, Any], *, steps: list[PMFlowAgentStepPacket]) -> list[PMFlowAgentObservationPacket]: agent_run = _dict(agent_payload.get("agent_run_packet")) rows = list(agent_run.get("observations") or []) if not rows: workspace = _dict(agent_payload.get("agent_workspace")) rows = list(workspace.get("observations") or []) step_id_by_observation_id = { str(row.get("observation_id")): str(row.get("step_id")) for row in list(agent_run.get("steps") or []) if isinstance(row, dict) and row.get("observation_id") and row.get("step_id") } tool_steps = [step for step in steps if step.tool_name] fallback_step_id = (tool_steps[-1].step_id if tool_steps else (steps[-1].step_id if steps else None)) observations: list[PMFlowAgentObservationPacket] = [] for index, row in enumerate(rows[:48]): if not isinstance(row, dict): continue facts = [dict(item) for item in list(row.get("facts") or [])[:32] if isinstance(item, dict)] observation_id = str(row.get("observation_id") or new_pmflow_agent_id("obs"))[:80] observations.append( PMFlowAgentObservationPacket( observation_id=observation_id, step_id=str(row.get("step_id") or step_id_by_observation_id.get(observation_id) or fallback_step_id or "")[:80] or None, source_kind=str(row.get("source") or row.get("source_kind") or "agent_runtime")[:80], summary=str(row.get("summary") or "")[:1200], confidence=str(row.get("confidence") or "unknown")[:32], facts=facts, gaps=_gaps_from_agent_observation(row, facts=facts), ) ) if not observations: observations.append( PMFlowAgentObservationPacket( step_id=fallback_step_id, source_kind="agent_runtime", summary="本轮没有产生额外工具观察。", confidence="unknown", facts=[], gaps=[], ) ) return _dedupe_observations(observations) def _artifacts_from_agent_payload(agent_payload: dict[str, Any], *, answer: str) -> list[PMFlowAgentArtifactPacket]: artifacts: list[PMFlowAgentArtifactPacket] = [] for projection in list(agent_payload.get("product_object_projections") or [])[:8]: if not isinstance(projection, dict): continue artifacts.append( PMFlowAgentArtifactPacket( kind="product_object_projection", status=str(projection.get("status") or "ready_for_review")[:32], title=str(projection.get("title") or projection.get("object_type") or "经营对象候选")[:240], summary=str(projection.get("summary") or "")[:1000], payload=projection, ) ) for product in list(agent_payload.get("agent_work_products") or [])[:12]: if not isinstance(product, dict): continue artifacts.append( PMFlowAgentArtifactPacket( kind="specialist_work_product", status=str(product.get("status") or "completed")[:32], title=_customer_work_product_title(product), summary=str(product.get("summary") or "")[:1000], payload=product, ) ) deliverable = _dict(agent_payload.get("deliverable_packet")) if deliverable: artifacts.append( PMFlowAgentArtifactPacket( kind="agent_deliverable", status=str(deliverable.get("status") or "completed")[:32], title=str(deliverable.get("title") or "本轮结果")[:240], summary=str(deliverable.get("summary") or deliverable.get("answer_text") or "")[:1000], payload=deliverable, ) ) research_workspace = _dict(agent_payload.get("research_workspace")) if research_workspace and (research_workspace.get("candidate_items") or research_workspace.get("research_goal")): artifacts.append( PMFlowAgentArtifactPacket( kind="research_workspace", status="completed", title=str(research_workspace.get("topic") or "研究工作区")[:240], summary=str(research_workspace.get("research_goal") or research_workspace.get("topic") or "")[:1000], payload=research_workspace, ) ) quality_findings = list(_dict(agent_payload.get("answer_runtime_packet")).get("quality_findings") or []) artifacts.append(_final_answer_artifact(answer, status="blocked" if quality_findings else None)) return _dedupe_artifacts(artifacts) def _agent_verifications_from_payload(agent_payload: dict[str, Any], *, answer: str) -> list[PMFlowAgentVerificationPacket]: agent_run = _dict(agent_payload.get("agent_run_packet")) events = [item for item in list(agent_run.get("event_log") or []) if isinstance(item, dict)] verifications: list[PMFlowAgentVerificationPacket] = [] for event in events[:40]: event_type = str(event.get("event_type") or "").strip() if event_type not in {"critic_review", "runtime_stop"}: continue is_stop = event_type == "runtime_stop" stop_reason = str(event.get("stop_reason") or "").strip() blocked = is_stop and stop_reason not in {"ready_to_answer", "planner_answer"} verifications.append( PMFlowAgentVerificationPacket( target_ref=f"{event_type}:{event.get('iteration', 0)}", result="blocked" if blocked else "passed", retry_would_change_state=bool(event.get("state_changed") or blocked), findings=[ { "type": stop_reason or event_type, "summary": str(event.get("summary") or stop_reason or event_type)[:500], } ], ) ) quality_findings = list(_dict(agent_payload.get("answer_runtime_packet")).get("quality_findings") or []) verifications.append( PMFlowAgentVerificationPacket( target_ref="final_answer", result="passed" if answer.strip() and not quality_findings else "blocked", retry_would_change_state=False, findings=[ {"type": str(item or "quality_finding")[:120], "summary": "回答出口命中质量门禁。"} for item in quality_findings[:8] ], ) ) return _dedupe_verifications(verifications) def _agent_memories_from_payload(agent_payload: dict[str, Any]) -> list[PMFlowAgentMemoryPacket]: learning = _dict(agent_payload.get("learning_ledger")) learning_items = list(learning.get("learning_items") or []) memories: list[PMFlowAgentMemoryPacket] = [] for item in learning_items[:8]: if not isinstance(item, dict): continue memories.append( PMFlowAgentMemoryPacket( memory_kind="operating_learning", scope_ref=str(learning.get("scope_ref") or item.get("scope_ref") or "")[:160] or None, outcome_signal=str(item.get("outcome_status") or item.get("learning_key") or "observed")[:80], summary=str(item.get("summary") or item.get("learning_key") or "")[:1000], ) ) if memories: return _dedupe_memories(memories) workspace = _dict(agent_payload.get("agent_workspace")) deliverable = _dict(agent_payload.get("deliverable_packet")) summary = str(deliverable.get("summary") or workspace.get("current_goal") or "").strip() if not summary: return [] return [ PMFlowAgentMemoryPacket( memory_kind="agent_workspace_snapshot", scope_ref=str(workspace.get("workspace_id") or "")[:160] or None, outcome_signal=str(deliverable.get("status") or workspace.get("status") or "observed")[:80], summary=summary[:1000], ) ] def _gaps_from_agent_observation(row: dict[str, Any], *, facts: list[dict[str, Any]]) -> list[dict[str, Any]]: gaps: list[dict[str, Any]] = [] observation_type = str(row.get("observation_type") or "").strip() if observation_type in {"tool_rejected", "external_search_failed", "external_search_missing_query", "project_world_state_missing_scope"}: gaps.append( { "type": observation_type, "summary": str(row.get("summary") or "本轮观察未完整完成。")[:500], "user_actionable": False, } ) for fact in facts[:16]: fact_type = str(fact.get("type") or "").strip() if fact_type.startswith("missing_") or fact_type in {"tool_rejected", "tool_error", "no_object_candidates"}: gaps.append( { "type": fact_type, "summary": str(fact.get("summary") or fact.get("reason") or row.get("summary") or fact_type)[:500], "user_actionable": bool(fact.get("user_actionable", False)), } ) return gaps[:12] def _agent_payload_object_refs(agent_payload: dict[str, Any]) -> list[str]: refs: list[str] = [] mission = _dict(agent_payload.get("operating_mission")) scope = _dict(mission.get("project_scope")) for item in list(scope.get("object_refs") or []): if isinstance(item, str) and item.strip(): refs.append(item.strip()[:160]) for item in list(mission.get("target_objects") or []): if isinstance(item, dict): object_ref = str(item.get("object_ref") or "").strip() if object_ref: refs.append(object_ref[:160]) for source in ( _dict(agent_payload.get("deliverable_packet")).get("object_refs"), _dict(agent_payload.get("answer_canvas")).get("object_refs"), ): for item in list(source or []): if isinstance(item, str) and item.strip(): refs.append(item.strip()[:160]) elif isinstance(item, dict): object_ref = str(item.get("object_ref") or "").strip() if object_ref: refs.append(object_ref[:160]) workspace = _dict(agent_payload.get("agent_workspace")) for item in list(workspace.get("object_ledger") or [])[:12]: if isinstance(item, dict): object_ref = str(item.get("object_ref") or "").strip() if object_ref: refs.append(object_ref[:160]) return _dedupe([item for item in refs if item])[:12] def _customer_interaction_context_from_agent_payload(agent_payload: dict[str, Any]) -> dict[str, Any]: refs = _agent_payload_object_refs(agent_payload) objects: list[dict[str, Any]] = [] workspace = _dict(agent_payload.get("agent_workspace")) for item in list(workspace.get("object_ledger") or [])[:12]: if not isinstance(item, dict): continue object_ref = str(item.get("object_ref") or "").strip() if not object_ref: continue objects.append( { "object_ref": object_ref[:160], "object_type": str(item.get("object_type") or object_ref.split(":", 1)[0] or "object")[:80], "title": str(item.get("title") or object_ref)[:240], "binding_strength": str(item.get("binding_strength") or "candidate")[:80], } ) if not objects: objects = [ { "object_ref": ref, "object_type": ref.split(":", 1)[0] if ":" in ref else "object", "title": ref, "binding_strength": "derived_from_agent_output", } for ref in refs ] return { "packet_version": "customer_interaction_context.v1", "visibility": "customer", "current_focus_object": objects[0] if objects else None, "last_shown_objects": objects[:12], "recent_objects": objects[:12], } def _prepared_actions_from_agent_payload(agent_payload: dict[str, Any]) -> list[dict[str, Any]]: operating_rows = agent_payload.get("prepared_action_packets") if isinstance(operating_rows, list) and operating_rows: out: list[dict[str, Any]] = [] for index, item in enumerate(operating_rows[:8], start=1): if not isinstance(item, dict): continue title = str(item.get("title") or "").strip() if not _is_customer_visible_action_title(title): continue out.append( { "action_id": str(item.get("prepared_action_id") or f"operating_action_{index}")[:80], "title": title[:120], "summary": _customer_visible_action_detail( item.get("summary") or _dict(item.get("payload")).get("title") or "已准备为待确认动作,确认前不会写入或执行。" ), "href": str(item.get("href") or "")[:300], "target_ref": str(item.get("target_ref") or "")[:160] or None, "status": str(item.get("status") or "prepared_waiting_confirmation")[:80], "confirmation_required": bool(item.get("confirmation_required", True)), "source": "operating_prepared_action", } ) return out canvas = _dict(agent_payload.get("answer_canvas")) rows = canvas.get("action_candidates") if not isinstance(rows, list): rows = [] answer_text = str(agent_payload.get("answer") or agent_payload.get("reply_text") or canvas.get("direct_answer") or "").strip() valid_rows = [ item for item in rows if isinstance(item, dict) and _is_customer_visible_action_title(item.get("title")) and str(item.get("href") or "").strip() ] if len(valid_rows) > 1: mentioned = [ item for item in valid_rows if isinstance(item, dict) and str(item.get("title") or "").strip() and str(item.get("title") or "").strip() in answer_text ] rows = mentioned if mentioned else valid_rows[:1] else: rows = valid_rows out: list[dict[str, Any]] = [] for index, item in enumerate(rows[:8], start=1): if not isinstance(item, dict): continue title = str(item.get("title") or "").strip() row = dict(item) href = str(row.get("href") or "").strip()[:300] if not _is_customer_visible_action_title(title) or not href: continue summary = _customer_visible_action_detail(row.get("reason") or row.get("completion_criteria") or row.get("summary")) out.append( { "action_id": str(row.get("action_id") or f"agent_action_{index}")[:80], "title": title[:120], "summary": summary, "href": href, "target_ref": str(row.get("target_ref") or row.get("object_ref") or "")[:160] or None, "status": "prepared_waiting_confirmation" if bool(row.get("requires_confirmation") or row.get("requires_user_confirm")) else "suggested", "confirmation_required": bool(row.get("requires_confirmation") or row.get("requires_user_confirm")), "source": "deliverable_action_candidate", } ) return out def _is_customer_visible_action_title(value: object) -> bool: text = " ".join(str(value or "").strip().split()) if not text or len(text) > 120: return False lowered = text.lower() internal_terms = ( "goalframe", "top_next_actions", "turnstate", "claim ledger", "boundary ledger", "agent_os", "runtime", "planner", "critic", "workspace", "target_ref", "object_ref", "tool", ) if any(term in lowered for term in internal_terms): return False if re.search(r"\b(project|approval|change_set|task):\d+\b", lowered): return False if ";对象" in text or ";依据" in text or "; object" in lowered: return False latin_count = sum(1 for ch in text if ("a" <= ch.lower() <= "z")) cjk_count = sum(1 for ch in text if "\u4e00" <= ch <= "\u9fff") return not (latin_count > 20 and cjk_count == 0) def _customer_visible_action_detail(value: object) -> str: text = " ".join(str(value or "").strip().split())[:240] if not text: return "" lowered = text.lower() if any(term in lowered for term in ("goalframe", "top_next_actions", "turnstate", "target_ref", "object_ref")): return "" if re.search(r"\b(project|approval|change_set|task):\d+\b", lowered): return "" return text def _customer_work_product_title(product: dict[str, Any]) -> str: labels = { "IdeaSeed": "想法种子", "ResearchNote": "研究记录", "CompetitorMap": "竞品地图", "OpportunityBrief": "机会简报", "MVPOption": "MVP 选项", "TaskDraft": "任务草案", "TechnicalArchitectureAssessment": "技术评估", "QAPlan": "质量与风险计划", "GrowthExperiment": "增长实验", "RevenueSignal": "商业化信号", "LearningUpdate": "学习记录", } product_type = str(product.get("product_type") or "").strip() return labels.get(product_type, "专业 Agent 产物")[:240] def _confirmation_requests_from_agent_payload(agent_payload: dict[str, Any]) -> list[dict[str, Any]]: operating_rows = agent_payload.get("confirmation_requests") if isinstance(operating_rows, list) and operating_rows: out: list[dict[str, Any]] = [] for index, item in enumerate(operating_rows[:8], start=1): if not isinstance(item, dict): continue out.append( { "confirmation_id": str(item.get("confirmation_request_id") or f"operating_confirmation_{index}")[:80], "title": str(item.get("title") or "需要确认的动作")[:240], "summary": str(item.get("confirmation_reason") or item.get("summary") or "")[:500], "status": str(item.get("status") or "waiting_for_user_confirmation")[:80], "requested_for": [str(value)[:120] for value in list(item.get("requested_for") or []) if str(value).strip()][:8], "target_ref": str(item.get("target_ref") or "")[:160], "action_label": str(item.get("action_label") or "")[:120], "action_type": str(item.get("action_type") or "")[:120], "action_payload": dict(item.get("action_payload") or {}) if isinstance(item.get("action_payload"), dict) else {}, "can_execute_after_confirmation": bool(item.get("can_execute_after_confirmation")), } ) return out deliverable = _dict(agent_payload.get("deliverable_packet")) rows = deliverable.get("pending_confirmations") if isinstance(deliverable.get("pending_confirmations"), list) else [] out: list[dict[str, Any]] = [] for index, item in enumerate(rows[:8], start=1): if not isinstance(item, dict): continue out.append( { "confirmation_id": str(item.get("confirmation_id") or f"agent_confirmation_{index}")[:80], "title": str(item.get("title") or item.get("summary") or "需要确认的动作")[:240], "summary": str(item.get("summary") or item.get("description") or "")[:500], "status": str(item.get("status") or "waiting_for_user_confirmation")[:80], "action_label": str(item.get("action_label") or "")[:120], "action_type": str(item.get("action_type") or "")[:120], "action_payload": dict(item.get("action_payload") or {}) if isinstance(item.get("action_payload"), dict) else {}, "can_execute_after_confirmation": bool(item.get("can_execute_after_confirmation")), } ) return out def _durable_status_from_agent_payload(agent_payload: dict[str, Any], *, answer: str) -> str: if not answer.strip(): return "blocked" agent_status = str(_dict(agent_payload.get("agent_run_packet")).get("status") or "").strip() deliverable_status = str(_dict(agent_payload.get("deliverable_packet")).get("status") or "").strip() if agent_status in {"blocked", "waiting_for_user"} or deliverable_status in {"blocked", "needs_customer_input"}: return "blocked" return "completed" def _durable_step_status(value: Any, *, blocking: bool = False) -> str: if blocking: return "blocked" text = str(value or "").strip().lower() if text in {"completed", "done", "passed"}: return "completed" if text in {"active", "running", "pending"}: return "active" if text in {"blocked", "failed"}: return "blocked" if text in {"skipped"}: return "skipped" return "completed" def _agent_step_label(kind: str) -> str: labels = { "understand_goal": "承接目标", "workspace_snapshot": "建立工作区", "planner": "规划下一步", "critic": "检查状态", "answer": "生成回答", "external_search": "读取外部公开信息", "inspect_space_collections": "读取空间状态", "inspect_project_operating_state": "读取项目运行状态", "inspect_pmflow_capability_boundary": "读取能力边界", "inspect_saved_ideas": "读取暂存想法", "inspect_attention_queue": "读取注意力队列", "inspect_object_affordances": "读取对象可用动作", "inspect_project_execution_package": "读取项目执行包", "inspect_source_detail": "读取公开来源详情", "resolve_customer_object": "解析对象锚点", "inspect_object_detail": "读取对象详情", } return labels.get(str(kind or "").strip(), "") def _dict(value: Any) -> dict[str, Any]: if isinstance(value, dict): return dict(value) dump = getattr(value, "model_dump", None) if callable(dump): try: return dump(mode="json", exclude_none=True) except Exception: return {} return {} def _dedupe(rows: list[str]) -> list[str]: out: list[str] = [] seen: set[str] = set() for row in rows: text = str(row or "").strip() key = " ".join(text.lower().split()) if not text or key in seen: continue seen.add(key) out.append(text) return out def _customer_evidence_packet(*, answer: str, packet: PMFlowAgentRunPacket) -> dict[str, Any]: return { "packet_version": "customer_evidence_packet.v1", "visibility": "customer", "answer_type": "pmflow_agent_runtime", "confidence": "high" if packet.observations else "medium", "answer": answer, "facts": [ fact for observation in packet.observations[:8] for fact in list(observation.facts or [])[:6] if isinstance(fact, dict) ][:24], "limitations": [ finding.get("summary") for verification in packet.verifications for finding in list(verification.findings or [])[:4] if isinstance(finding, dict) and str(finding.get("summary") or "").strip() ][:12], } def _answer_canvas(*, answer: str, packet: PMFlowAgentRunPacket) -> dict[str, Any]: return { "packet_version": "answer_canvas.v1", "visibility": "customer", "direct_answer": answer, "next_actions": list(packet.prepared_action_packets or [])[:6], "confirmation_requests": list(packet.confirmation_requests or [])[:6], "process_steps": [step.model_dump(mode="json", exclude_none=True) for step in packet.steps[:12]], } def _final_answer_artifact(answer: str, *, status: str | None = None) -> PMFlowAgentArtifactPacket: return PMFlowAgentArtifactPacket( kind="final_answer", status=status or ("completed" if answer.strip() else "blocked"), title="PMFlow AI 回答", summary=answer[:1000], payload={"answer": answer}, ) def _dedupe_steps(steps: list[PMFlowAgentStepPacket]) -> list[PMFlowAgentStepPacket]: out: list[PMFlowAgentStepPacket] = [] seen: set[str] = set() for step in steps: step_id = str(step.step_id or new_pmflow_agent_id("step"))[:80] if step_id in seen: step_id = f"{step_id[:64]}_{len(out)}" seen.add(step_id) out.append(step.model_copy(update={"step_id": step_id, "index": len(out)})) return out[:32] def _dedupe_observations(observations: list[PMFlowAgentObservationPacket]) -> list[PMFlowAgentObservationPacket]: out: list[PMFlowAgentObservationPacket] = [] seen: set[str] = set() for observation in observations: observation_id = str(observation.observation_id or new_pmflow_agent_id("obs"))[:80] if observation_id in seen: observation_id = f"{observation_id[:64]}_{len(out)}" seen.add(observation_id) out.append(observation.model_copy(update={"observation_id": observation_id})) return out[:48] def _dedupe_artifacts(artifacts: list[PMFlowAgentArtifactPacket]) -> list[PMFlowAgentArtifactPacket]: out: list[PMFlowAgentArtifactPacket] = [] seen: set[str] = set() for artifact in artifacts: key = f"{artifact.kind}:{artifact.title}:{artifact.summary}"[:500] if key in seen: continue seen.add(key) out.append(artifact) return out[:32] def _dedupe_verifications(verifications: list[PMFlowAgentVerificationPacket]) -> list[PMFlowAgentVerificationPacket]: out: list[PMFlowAgentVerificationPacket] = [] seen: set[str] = set() for verification in verifications: verification_id = str(verification.verification_id or new_pmflow_agent_id("ver"))[:80] if verification_id in seen: verification_id = f"{verification_id[:64]}_{len(out)}" seen.add(verification_id) out.append(verification.model_copy(update={"verification_id": verification_id})) return out[:40] def _dedupe_memories(memories: list[PMFlowAgentMemoryPacket]) -> list[PMFlowAgentMemoryPacket]: out: list[PMFlowAgentMemoryPacket] = [] seen: set[str] = set() for memory in memories: key = f"{memory.memory_kind}:{memory.scope_ref}:{memory.summary}"[:500] if key in seen: continue seen.add(key) out.append(memory) return out[:12] def _title_from_question(question: Any) -> str: text = " ".join(str(question or "").strip().split()) return text[:80] or "PMFlow AI Run" def _customer_safe_error_summary(exc: Exception) -> str: return f"{exc.__class__.__name__}: runtime failed"[:500] def _safe_int(value: Any) -> int | None: try: if value is None or value == "": return None return int(value) except Exception: return None __all__ = ["PMFLOW_AGENT_OS_POLICY_VERSION", "PMFlowAgentRuntimeService"]