|
|
@@ -0,0 +1,540 @@
|
|
|
+from __future__ import annotations
|
|
|
+
|
|
|
+import json
|
|
|
+from datetime import datetime, timezone
|
|
|
+from typing import Any, Literal
|
|
|
+
|
|
|
+from .models import (
|
|
|
+ DataRef,
|
|
|
+ InputSummary,
|
|
|
+ JourneyEdge,
|
|
|
+ JourneyStep,
|
|
|
+ JourneyView,
|
|
|
+ ProcessView,
|
|
|
+ StepEvidence,
|
|
|
+)
|
|
|
+from .narrator import narrate_contract, task_label
|
|
|
+
|
|
|
+
|
|
|
+def build_journey(script_build_id: int, source: dict[str, Any]) -> JourneyView:
|
|
|
+ snapshot = source["snapshot"]
|
|
|
+ tasks = {str(item["task_id"]): item for item in snapshot.get("tasks") or []}
|
|
|
+ attempts = {str(item["attempt_id"]): item for item in snapshot.get("attempts") or []}
|
|
|
+ validations = {str(item["validation_id"]): item for item in snapshot.get("validations") or []}
|
|
|
+ decisions = {str(item["decision_id"]): item for item in snapshot.get("decisions") or []}
|
|
|
+ contracts = source.get("contracts") or {}
|
|
|
+ messages = source.get("messages") or {}
|
|
|
+ root_trace_id = str(snapshot.get("root_trace_id") or "")
|
|
|
+ planner_calls = _planner_calls(messages.get(root_trace_id) or [])
|
|
|
+ operations = snapshot.get("operations") or []
|
|
|
+ parallel = _parallel_attempts(operations)
|
|
|
+ event_sequences = _event_sequences(source.get("events") or [])
|
|
|
+ steps: list[JourneyStep] = []
|
|
|
+
|
|
|
+ plan_by_task: dict[str, str] = {}
|
|
|
+ execute_by_attempt: dict[str, str] = {}
|
|
|
+ validation_by_id: dict[str, str] = {}
|
|
|
+
|
|
|
+ for task_id, task in tasks.items():
|
|
|
+ if task.get("parent_task_id") is None or task_id not in contracts:
|
|
|
+ continue
|
|
|
+ contract = contracts[task_id]
|
|
|
+ narration = narrate_contract(contract)
|
|
|
+ call = planner_calls.get(task_id)
|
|
|
+ observable = _plain((call or {}).get("assistant_text"))
|
|
|
+ step_id = f"plan:{task_id}"
|
|
|
+ plan_by_task[task_id] = step_id
|
|
|
+ steps.append(
|
|
|
+ JourneyStep(
|
|
|
+ step_id=step_id,
|
|
|
+ step_type="plan",
|
|
|
+ title=f"Planner 规划·{narration.output}",
|
|
|
+ status=str(task.get("status") or "planned"),
|
|
|
+ task_id=task_id,
|
|
|
+ reasoning=observable or narration.reasoning,
|
|
|
+ reasoning_source="observable" if observable else "contract",
|
|
|
+ process=ProcessView(
|
|
|
+ label=narration.action,
|
|
|
+ actor="Planner",
|
|
|
+ tool_name="plan_script_tasks",
|
|
|
+ trace_id=root_trace_id,
|
|
|
+ details={"task_kind": contract.get("task_kind")},
|
|
|
+ ),
|
|
|
+ input_data=_contract_inputs(contract),
|
|
|
+ output_data=[
|
|
|
+ DataRef(label="TaskContract", kind="contract", ref=contract.get("contract_ref"))
|
|
|
+ ],
|
|
|
+ contract=narration,
|
|
|
+ created_at=str(task.get("created_at") or ""),
|
|
|
+ evidence=StepEvidence(
|
|
|
+ contract=contract,
|
|
|
+ tool_calls=[call["tool_call"]] if call and call.get("tool_call") else [],
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ for attempt_id, attempt in attempts.items():
|
|
|
+ task_id = str(attempt.get("task_id") or "")
|
|
|
+ task = tasks.get(task_id) or {}
|
|
|
+ contract = contracts.get(task_id)
|
|
|
+ worker_trace_id = str(attempt.get("worker_trace_id") or "")
|
|
|
+ worker_messages = messages.get(worker_trace_id) or []
|
|
|
+ observable = _assistant_text(worker_messages)
|
|
|
+ narration = narrate_contract(contract) if contract else None
|
|
|
+ tools = _tool_calls(worker_messages)
|
|
|
+ position, total, group_id = parallel.get(attempt_id, (None, None, None))
|
|
|
+ stats = attempt.get("execution_stats") or {}
|
|
|
+ step_id = f"execute:{attempt_id}"
|
|
|
+ execute_by_attempt[attempt_id] = step_id
|
|
|
+ steps.append(
|
|
|
+ JourneyStep(
|
|
|
+ step_id=step_id,
|
|
|
+ step_type="execute",
|
|
|
+ title=f"Worker 执行·{_task_name(task, contract)}",
|
|
|
+ status=str(attempt.get("status") or "unknown"),
|
|
|
+ task_id=task_id,
|
|
|
+ attempt_id=attempt_id,
|
|
|
+ reasoning=observable
|
|
|
+ or (
|
|
|
+ f"执行任务合同:{contract.get('objective')}"
|
|
|
+ if contract
|
|
|
+ else "执行已冻结的任务合同"
|
|
|
+ ),
|
|
|
+ reasoning_source="observable" if observable else "contract",
|
|
|
+ process=ProcessView(
|
|
|
+ label=_worker_process_label(tools),
|
|
|
+ actor="Worker",
|
|
|
+ trace_id=worker_trace_id or None,
|
|
|
+ details={"execution_mode": attempt.get("execution_mode")},
|
|
|
+ ),
|
|
|
+ input_data=_contract_inputs(contract or {}),
|
|
|
+ output_data=_attempt_outputs(attempt),
|
|
|
+ contract=narration,
|
|
|
+ parallel_group_id=group_id,
|
|
|
+ parallel_position=position,
|
|
|
+ parallel_total=total,
|
|
|
+ model=stats.get("primary_model"),
|
|
|
+ tokens=stats.get("total_tokens"),
|
|
|
+ cost=stats.get("total_cost"),
|
|
|
+ duration_ms=attempt.get("duration_ms"),
|
|
|
+ created_at=str(attempt.get("started_at") or attempt.get("created_at") or ""),
|
|
|
+ evidence=StepEvidence(
|
|
|
+ contract=contract,
|
|
|
+ tool_calls=tools,
|
|
|
+ artifact_refs=_artifact_refs(attempt),
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ for validation_id, validation in validations.items():
|
|
|
+ attempt_id = str(validation.get("attempt_id") or "")
|
|
|
+ attempt = attempts.get(attempt_id) or {}
|
|
|
+ task_id = str(validation.get("task_id") or attempt.get("task_id") or "")
|
|
|
+ plan = validation.get("validation_plan") or {}
|
|
|
+ stats = validation.get("execution_stats") or {}
|
|
|
+ reasoning = _validation_reason(validation)
|
|
|
+ task_name = _task_name(tasks.get(task_id) or {}, contracts.get(task_id))
|
|
|
+ step_id = f"validate:{validation_id}"
|
|
|
+ validation_by_id[validation_id] = step_id
|
|
|
+ steps.append(
|
|
|
+ JourneyStep(
|
|
|
+ step_id=step_id,
|
|
|
+ step_type="validate",
|
|
|
+ title=f"Validator 验收·{task_name}",
|
|
|
+ status=str(validation.get("verdict") or validation.get("status") or "unknown"),
|
|
|
+ task_id=task_id,
|
|
|
+ attempt_id=attempt_id or None,
|
|
|
+ validation_id=validation_id,
|
|
|
+ reasoning=reasoning,
|
|
|
+ reasoning_source="validation",
|
|
|
+ process=ProcessView(
|
|
|
+ label=(
|
|
|
+ "确定性规则预检"
|
|
|
+ if plan.get("mode") == "deterministic"
|
|
|
+ else "独立 Validator 语义质检"
|
|
|
+ ),
|
|
|
+ actor="Validator",
|
|
|
+ trace_id=validation.get("validator_trace_id"),
|
|
|
+ details={
|
|
|
+ "mode": plan.get("mode"),
|
|
|
+ "preflight_rule_ids": plan.get("preflight_rule_ids") or [],
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ input_data=_attempt_outputs(attempt),
|
|
|
+ output_data=[
|
|
|
+ DataRef(
|
|
|
+ label=f"Validation·{validation.get('verdict') or validation.get('status')}",
|
|
|
+ kind="validation",
|
|
|
+ ref=validation_id,
|
|
|
+ detail=_plain(validation.get("summary")),
|
|
|
+ )
|
|
|
+ ],
|
|
|
+ model=stats.get("primary_model"),
|
|
|
+ tokens=stats.get("total_tokens"),
|
|
|
+ cost=stats.get("total_cost"),
|
|
|
+ duration_ms=validation.get("duration_ms"),
|
|
|
+ created_at=str(validation.get("started_at") or validation.get("created_at") or ""),
|
|
|
+ evidence=StepEvidence(validation=validation, artifact_refs=_artifact_refs(attempt)),
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ for decision_id, decision in decisions.items():
|
|
|
+ task_id = str(decision.get("task_id") or "")
|
|
|
+ action = str(decision.get("action") or "unknown").upper()
|
|
|
+ validation_id = str(decision.get("validation_id") or "")
|
|
|
+ attempt_id = str(decision.get("attempt_id") or "")
|
|
|
+ steps.append(
|
|
|
+ JourneyStep(
|
|
|
+ step_id=f"decide:{decision_id}",
|
|
|
+ step_type="decide",
|
|
|
+ title=f"Planner 决定·{_action_label(action)}",
|
|
|
+ status=action.lower(),
|
|
|
+ task_id=task_id,
|
|
|
+ attempt_id=attempt_id or None,
|
|
|
+ validation_id=validation_id or None,
|
|
|
+ decision_id=decision_id,
|
|
|
+ reasoning=_plain(decision.get("reason")) or "Planner 根据验收结果选择下一步",
|
|
|
+ reasoning_source="decision",
|
|
|
+ process=ProcessView(
|
|
|
+ label=_action_process(action),
|
|
|
+ actor="Planner",
|
|
|
+ tool_name="decide_script_task",
|
|
|
+ trace_id=root_trace_id,
|
|
|
+ details={
|
|
|
+ "from_status": decision.get("from_status"),
|
|
|
+ "to_status": decision.get("to_status"),
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ input_data=[
|
|
|
+ DataRef(label="Validation", kind="validation", ref=validation_id or None)
|
|
|
+ ],
|
|
|
+ output_data=[
|
|
|
+ DataRef(label=_action_label(action), kind="decision", ref=decision_id)
|
|
|
+ ],
|
|
|
+ created_at=str(decision.get("created_at") or ""),
|
|
|
+ evidence=StepEvidence(decision=decision),
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ steps.sort(key=lambda step: _step_sort_key(step, event_sequences))
|
|
|
+ steps = [step.model_copy(update={"sequence": index}) for index, step in enumerate(steps, 1)]
|
|
|
+ edges = _edges(steps, tasks, plan_by_task, execute_by_attempt, validation_by_id)
|
|
|
+ root_task = next((item for item in tasks.values() if item.get("parent_task_id") is None), {})
|
|
|
+ raw_input = source["input_summary"]
|
|
|
+ return JourneyView(
|
|
|
+ schema_version="script-build-journey/v1",
|
|
|
+ generated_at=datetime.now(timezone.utc).isoformat(),
|
|
|
+ script_build_id=script_build_id,
|
|
|
+ root_trace_id=root_trace_id,
|
|
|
+ status=str(root_task.get("status") or "unknown"),
|
|
|
+ objective=_plain(snapshot.get("root_objective")) or "完成脚本创作与交付",
|
|
|
+ input_summary=InputSummary(
|
|
|
+ topic=_plain(raw_input.get("topic")) or "未记录选题",
|
|
|
+ account_name=(raw_input.get("persona") or {}).get("resolved_account_name")
|
|
|
+ or (raw_input.get("persona") or {}).get("account_name"),
|
|
|
+ persona_point_count=int((raw_input.get("persona") or {}).get("point_count") or 0),
|
|
|
+ strategy_names=[
|
|
|
+ str(item["name"])
|
|
|
+ for item in raw_input.get("strategies") or []
|
|
|
+ if isinstance(item, dict) and item.get("name")
|
|
|
+ ],
|
|
|
+ snapshot_id=str(raw_input.get("input_snapshot_id") or ""),
|
|
|
+ digest=str(raw_input.get("input_digest") or ""),
|
|
|
+ ),
|
|
|
+ steps=steps,
|
|
|
+ edges=edges,
|
|
|
+ warnings=_warnings(steps),
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def _planner_calls(messages: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
|
|
|
+ results = {
|
|
|
+ str(message.get("tool_call_id")): _parse_result(message.get("content"))
|
|
|
+ for message in messages
|
|
|
+ if message.get("role") == "tool" and message.get("tool_call_id")
|
|
|
+ }
|
|
|
+ output: dict[str, dict[str, Any]] = {}
|
|
|
+ last_text = ""
|
|
|
+ for message in sorted(messages, key=lambda value: int(value.get("sequence") or 0)):
|
|
|
+ if message.get("role") != "assistant":
|
|
|
+ continue
|
|
|
+ content = message.get("content")
|
|
|
+ if not isinstance(content, dict):
|
|
|
+ continue
|
|
|
+ if _plain(content.get("text")):
|
|
|
+ last_text = _plain(content.get("text"))
|
|
|
+ for call in content.get("tool_calls") or []:
|
|
|
+ function = call.get("function") or {}
|
|
|
+ if function.get("name") != "plan_script_tasks":
|
|
|
+ continue
|
|
|
+ result = results.get(str(call.get("id"))) or {}
|
|
|
+ for task_id in result.get("task_ids") or []:
|
|
|
+ output[str(task_id)] = {
|
|
|
+ "assistant_text": last_text,
|
|
|
+ "tool_call": {
|
|
|
+ "name": "plan_script_tasks",
|
|
|
+ "arguments": _parse_json(function.get("arguments")),
|
|
|
+ },
|
|
|
+ }
|
|
|
+ return output
|
|
|
+
|
|
|
+
|
|
|
+def _tool_calls(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
|
+ output: list[dict[str, Any]] = []
|
|
|
+ for message in messages:
|
|
|
+ content = message.get("content")
|
|
|
+ if message.get("role") != "assistant" or not isinstance(content, dict):
|
|
|
+ continue
|
|
|
+ for call in content.get("tool_calls") or []:
|
|
|
+ function = call.get("function") or {}
|
|
|
+ output.append(
|
|
|
+ {
|
|
|
+ "name": str(function.get("name") or "unknown"),
|
|
|
+ "arguments": _parse_json(function.get("arguments")),
|
|
|
+ }
|
|
|
+ )
|
|
|
+ return output
|
|
|
+
|
|
|
+
|
|
|
+def _assistant_text(messages: list[dict[str, Any]]) -> str:
|
|
|
+ values = []
|
|
|
+ for message in messages:
|
|
|
+ content = message.get("content")
|
|
|
+ if message.get("role") == "assistant" and isinstance(content, dict):
|
|
|
+ text = _plain(content.get("text"))
|
|
|
+ if text:
|
|
|
+ values.append(text)
|
|
|
+ return " ".join(values[:3])[:1_200]
|
|
|
+
|
|
|
+
|
|
|
+def _contract_inputs(contract: dict[str, Any]) -> list[DataRef]:
|
|
|
+ values = []
|
|
|
+ for item in contract.get("input_decision_refs") or []:
|
|
|
+ if not isinstance(item, dict):
|
|
|
+ continue
|
|
|
+ artifact = item.get("artifact_ref") or {}
|
|
|
+ label = f"已接受的{task_label(str(item.get('expected_task_kind') or '上游成果'))}"
|
|
|
+ values.append(
|
|
|
+ DataRef(
|
|
|
+ label=label,
|
|
|
+ kind=str(artifact.get("kind") or "artifact"),
|
|
|
+ ref=artifact.get("uri"),
|
|
|
+ detail=str(item.get("decision_id") or ""),
|
|
|
+ )
|
|
|
+ )
|
|
|
+ if not values:
|
|
|
+ values.append(DataRef(label="冻结创作输入", kind="input-snapshot"))
|
|
|
+ return values
|
|
|
+
|
|
|
+
|
|
|
+def _attempt_outputs(attempt: dict[str, Any]) -> list[DataRef]:
|
|
|
+ submission = attempt.get("submission") or {}
|
|
|
+ values = []
|
|
|
+ for item in submission.get("artifact_refs") or []:
|
|
|
+ if isinstance(item, dict):
|
|
|
+ values.append(
|
|
|
+ DataRef(
|
|
|
+ label=str(item.get("summary") or item.get("kind") or "Artifact"),
|
|
|
+ kind=str(item.get("kind") or "artifact"),
|
|
|
+ ref=item.get("uri"),
|
|
|
+ )
|
|
|
+ )
|
|
|
+ return values or [DataRef(label="待提交的任务成果", kind="artifact")]
|
|
|
+
|
|
|
+
|
|
|
+def _artifact_refs(attempt: dict[str, Any]) -> list[dict[str, Any]]:
|
|
|
+ submission = attempt.get("submission") or {}
|
|
|
+ return [item for item in submission.get("artifact_refs") or [] if isinstance(item, dict)]
|
|
|
+
|
|
|
+
|
|
|
+def _validation_reason(validation: dict[str, Any]) -> str:
|
|
|
+ summary = _plain(validation.get("summary"))
|
|
|
+ failures = [
|
|
|
+ f"{item.get('criterion_id')}:{_plain(item.get('reason'))}"
|
|
|
+ for item in validation.get("criterion_results") or []
|
|
|
+ if isinstance(item, dict) and str(item.get("verdict")) not in {"passed", "pass"}
|
|
|
+ ]
|
|
|
+ return ";".join(failures) or summary or "Validator 已逐项检查验收标准"
|
|
|
+
|
|
|
+
|
|
|
+def _parallel_attempts(operations: list[dict[str, Any]]) -> dict[str, tuple[int, int, str]]:
|
|
|
+ output: dict[str, tuple[int, int, str]] = {}
|
|
|
+ for operation in operations:
|
|
|
+ attempt_ids = [str(value) for value in operation.get("attempt_ids") or []]
|
|
|
+ if len(attempt_ids) < 2:
|
|
|
+ continue
|
|
|
+ group_id = str(operation.get("operation_id") or "")
|
|
|
+ for index, attempt_id in enumerate(attempt_ids, start=1):
|
|
|
+ output[attempt_id] = (index, len(attempt_ids), group_id)
|
|
|
+ return output
|
|
|
+
|
|
|
+
|
|
|
+def _loop_target(
|
|
|
+ action: str,
|
|
|
+ task_id: str,
|
|
|
+ attempt_id: str,
|
|
|
+ tasks: dict[str, dict[str, Any]],
|
|
|
+ plan_by_task: dict[str, str],
|
|
|
+ execute_by_attempt: dict[str, str],
|
|
|
+) -> str | None:
|
|
|
+ if action == "RETRY":
|
|
|
+ return execute_by_attempt.get(attempt_id)
|
|
|
+ if action == "REVISE":
|
|
|
+ return plan_by_task.get(task_id)
|
|
|
+ if action == "SPLIT":
|
|
|
+ children = tasks.get(task_id, {}).get("child_task_ids") or []
|
|
|
+ return next(
|
|
|
+ (plan_by_task.get(str(value)) for value in children if plan_by_task.get(str(value))),
|
|
|
+ None,
|
|
|
+ )
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
+def _edges(
|
|
|
+ steps: list[JourneyStep],
|
|
|
+ tasks: dict[str, dict[str, Any]],
|
|
|
+ plan_by_task: dict[str, str],
|
|
|
+ execute_by_attempt: dict[str, str],
|
|
|
+ validation_by_id: dict[str, str],
|
|
|
+) -> list[JourneyEdge]:
|
|
|
+ edges: list[JourneyEdge] = []
|
|
|
+ for left, right in zip(steps, steps[1:]):
|
|
|
+ edges.append(
|
|
|
+ JourneyEdge(
|
|
|
+ edge_id=f"sequence:{left.step_id}:{right.step_id}",
|
|
|
+ source=left.step_id,
|
|
|
+ target=right.step_id,
|
|
|
+ relation="sequence",
|
|
|
+ label="然后",
|
|
|
+ )
|
|
|
+ )
|
|
|
+ for step in steps:
|
|
|
+ if step.step_type == "execute" and step.attempt_id:
|
|
|
+ source = plan_by_task.get(step.task_id)
|
|
|
+ if source:
|
|
|
+ edges.append(_edge(source, step.step_id, "creates", "派发执行"))
|
|
|
+ elif step.step_type == "validate" and step.attempt_id:
|
|
|
+ source = execute_by_attempt.get(step.attempt_id)
|
|
|
+ if source:
|
|
|
+ edges.append(_edge(source, step.step_id, "validates", "提交验收"))
|
|
|
+ elif step.step_type == "decide" and step.validation_id:
|
|
|
+ source = validation_by_id.get(step.validation_id)
|
|
|
+ if source:
|
|
|
+ edges.append(_edge(source, step.step_id, "decides", "验收结论"))
|
|
|
+ target = _loop_target(
|
|
|
+ step.status.upper(),
|
|
|
+ step.task_id,
|
|
|
+ step.attempt_id or "",
|
|
|
+ tasks,
|
|
|
+ plan_by_task,
|
|
|
+ execute_by_attempt,
|
|
|
+ )
|
|
|
+ if target:
|
|
|
+ edges.append(_edge(step.step_id, target, "loop", _loop_label(step.status.upper())))
|
|
|
+ return edges
|
|
|
+
|
|
|
+
|
|
|
+def _edge(
|
|
|
+ source: str,
|
|
|
+ target: str,
|
|
|
+ relation: Literal["creates", "validates", "decides", "loop"],
|
|
|
+ label: str,
|
|
|
+) -> JourneyEdge:
|
|
|
+ return JourneyEdge(
|
|
|
+ edge_id=f"{relation}:{source}:{target}",
|
|
|
+ source=source,
|
|
|
+ target=target,
|
|
|
+ relation=relation,
|
|
|
+ label=label,
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def _warnings(steps: list[JourneyStep]) -> list[str]:
|
|
|
+ warnings = []
|
|
|
+ if not steps:
|
|
|
+ warnings.append("当前运行还没有可展示的创作步骤。")
|
|
|
+ if any(step.reasoning_source == "contract" for step in steps):
|
|
|
+ warnings.append("部分“思考”由冻结任务合同转述,不是隐藏思维链。")
|
|
|
+ return warnings
|
|
|
+
|
|
|
+
|
|
|
+def _worker_process_label(tools: list[dict[str, Any]]) -> str:
|
|
|
+ names = [str(item.get("name")) for item in tools if item.get("name")]
|
|
|
+ if not names:
|
|
|
+ return "根据任务合同生成候选成果"
|
|
|
+ shown = "、".join(names[:3])
|
|
|
+ suffix = f"等 {len(names)} 次工具调用" if len(names) > 3 else ""
|
|
|
+ return f"执行 {shown}{suffix}"
|
|
|
+
|
|
|
+
|
|
|
+def _task_name(task: dict[str, Any], contract: dict[str, Any] | None) -> str:
|
|
|
+ if contract:
|
|
|
+ return task_label(str(contract.get("task_kind") or "task"))
|
|
|
+ return _plain((task.get("current_spec") or {}).get("objective"))[:60] or "任务"
|
|
|
+
|
|
|
+
|
|
|
+def _action_label(action: str) -> str:
|
|
|
+ return {
|
|
|
+ "ACCEPT": "接受成果",
|
|
|
+ "RETRY": "重新执行",
|
|
|
+ "REVISE": "修订任务",
|
|
|
+ "SPLIT": "拆分子任务",
|
|
|
+ "BLOCK": "暂停在边界",
|
|
|
+ "CANCEL": "取消任务",
|
|
|
+ }.get(action, action)
|
|
|
+
|
|
|
+
|
|
|
+def _action_process(action: str) -> str:
|
|
|
+ return {
|
|
|
+ "ACCEPT": "接受当前成果,继续规划下一步",
|
|
|
+ "RETRY": "保留任务合同,让 Worker 再执行一次",
|
|
|
+ "REVISE": "修订任务合同后重新执行",
|
|
|
+ "SPLIT": "把当前问题拆成多个子任务",
|
|
|
+ "BLOCK": "保存当前成果,等待下一阶段",
|
|
|
+ }.get(action, "更新任务状态")
|
|
|
+
|
|
|
+
|
|
|
+def _loop_label(action: str) -> str:
|
|
|
+ return {"RETRY": "重试", "REVISE": "修订后返工", "SPLIT": "拆分后继续"}.get(action, "返工")
|
|
|
+
|
|
|
+
|
|
|
+def _event_sequences(events: list[dict[str, Any]]) -> dict[str, int]:
|
|
|
+ result: dict[str, int] = {}
|
|
|
+ for event in events:
|
|
|
+ sequence = int(event.get("sequence") or 0)
|
|
|
+ payload = event.get("payload") or {}
|
|
|
+ ids = list(payload.get("task_ids") or [])
|
|
|
+ ids.extend(
|
|
|
+ payload.get(key)
|
|
|
+ for key in ("task_id", "attempt_id", "validation_id", "decision_id")
|
|
|
+ if payload.get(key)
|
|
|
+ )
|
|
|
+ for entity_id in ids:
|
|
|
+ result.setdefault(str(entity_id), sequence)
|
|
|
+ return result
|
|
|
+
|
|
|
+
|
|
|
+def _step_sort_key(step: JourneyStep, event_sequences: dict[str, int]) -> tuple[int, str, int, str]:
|
|
|
+ priority = {"plan": 0, "execute": 1, "validate": 2, "decide": 3}
|
|
|
+ entity_id = step.decision_id or step.validation_id or step.attempt_id or step.task_id
|
|
|
+ event_sequence = event_sequences.get(entity_id, 2**31)
|
|
|
+ return event_sequence, step.created_at, priority[step.step_type], step.step_id
|
|
|
+
|
|
|
+
|
|
|
+def _parse_result(content: Any) -> dict[str, Any]:
|
|
|
+ if isinstance(content, dict):
|
|
|
+ return _parse_json(content.get("result", content))
|
|
|
+ return _parse_json(content)
|
|
|
+
|
|
|
+
|
|
|
+def _parse_json(value: Any) -> Any:
|
|
|
+ if not isinstance(value, str):
|
|
|
+ return value
|
|
|
+ try:
|
|
|
+ return json.loads(value)
|
|
|
+ except json.JSONDecodeError:
|
|
|
+ return value
|
|
|
+
|
|
|
+
|
|
|
+def _plain(value: Any) -> str:
|
|
|
+ return " ".join(str(value or "").split())
|