Initial commit
This commit is contained in:
@@ -0,0 +1,885 @@
|
||||
"""Read-model builders for the CLI board."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time as _time
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from opc.layer2_organization.phase import (
|
||||
Phase,
|
||||
coerce_phase,
|
||||
kanban_column,
|
||||
should_hide_work_item_from_company_kanban,
|
||||
)
|
||||
from opc.layer2_organization.work_item_context_view import WorkItemContextView
|
||||
from opc.layer2_organization.work_item_links import task_by_linked_work_item_id
|
||||
from opc.presentation.kanban import (
|
||||
COMPANY_KANBAN_COLUMNS,
|
||||
build_base_task_payload,
|
||||
datetime_to_timestamp,
|
||||
)
|
||||
|
||||
from ..state.models import (
|
||||
BoardAlert,
|
||||
BoardMetrics,
|
||||
BoardSnapshot,
|
||||
BoardTaskView,
|
||||
LinkedExecutionView,
|
||||
OrgEmployeeView,
|
||||
OrgRoleView,
|
||||
OrgSnapshotView,
|
||||
PendingCheckpointView,
|
||||
PipelineSnapshot,
|
||||
PipelineWorkItemView,
|
||||
SessionMessageView,
|
||||
SessionSummaryView,
|
||||
TaskDetailView,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .engine_facade import EngineFacade
|
||||
|
||||
|
||||
class BoardRepository:
|
||||
"""Builds CLI-specific board and detail snapshots from the main OPC store."""
|
||||
|
||||
def __init__(self, facade: "EngineFacade", project_id: str | None = None) -> None:
|
||||
self.facade = facade
|
||||
self.project_id = project_id
|
||||
|
||||
async def load_snapshot(self) -> BoardSnapshot:
|
||||
engine = await self.facade.ensure_ready()
|
||||
if not engine.store:
|
||||
return BoardSnapshot(project_id=self._project_id)
|
||||
|
||||
tasks = await engine.store.get_tasks(project_id=self._project_id)
|
||||
checkpoints = await engine.store.get_pending_checkpoints(project_id=self._project_id)
|
||||
checkpoint_by_session = self._checkpoint_by_session(checkpoints)
|
||||
await self._enrich_checkpoint_payloads(checkpoint_by_session)
|
||||
|
||||
company_snapshot = await self._maybe_build_company_snapshot(
|
||||
engine,
|
||||
tasks=tasks,
|
||||
checkpoint_by_session=checkpoint_by_session,
|
||||
checkpoint_count=len(checkpoints),
|
||||
)
|
||||
if company_snapshot is not None:
|
||||
return company_snapshot
|
||||
|
||||
visible_tasks, hidden_by_origin, hidden_count = self._split_visible_tasks(tasks)
|
||||
|
||||
task_views: list[BoardTaskView] = []
|
||||
for display_num, task in enumerate(visible_tasks, start=1):
|
||||
task_views.append(
|
||||
self._task_to_view(
|
||||
task,
|
||||
checkpoint=checkpoint_by_session.get(getattr(task, "session_id", None)),
|
||||
linked_tasks=hidden_by_origin.get(getattr(task, "id", ""), []),
|
||||
display_num=display_num,
|
||||
)
|
||||
)
|
||||
|
||||
session_summaries = [self._session_summary_view(task) for task in task_views]
|
||||
alerts = self._alerts_from_tasks(task_views)
|
||||
return BoardSnapshot(
|
||||
project_id=self._project_id,
|
||||
tasks=task_views,
|
||||
hidden_task_count=hidden_count,
|
||||
pending_checkpoint_count=len(checkpoints),
|
||||
session_summaries=session_summaries,
|
||||
alerts=alerts,
|
||||
metrics=self._build_metrics(task_views, hidden_count=hidden_count, pending_checkpoint_count=len(checkpoints)),
|
||||
)
|
||||
|
||||
async def _maybe_build_company_snapshot(
|
||||
self,
|
||||
engine: Any,
|
||||
*,
|
||||
tasks: list[Any],
|
||||
checkpoint_by_session: dict[str | None, PendingCheckpointView],
|
||||
checkpoint_count: int,
|
||||
) -> BoardSnapshot | None:
|
||||
"""Return a company-mode snapshot if an active delegation run exists.
|
||||
|
||||
Cards are sourced from `DelegationWorkItem`. Runtime `Task` objects are
|
||||
kept only as audit references (`runtime_task_id` / `session_id`).
|
||||
"""
|
||||
store = engine.store
|
||||
list_runs = getattr(store, "list_open_delegation_runs", None)
|
||||
list_items = getattr(store, "list_delegation_work_items", None)
|
||||
if list_runs is None or list_items is None:
|
||||
return None
|
||||
try:
|
||||
open_runs = await list_runs(project_id=self._project_id)
|
||||
except TypeError:
|
||||
open_runs = await list_runs()
|
||||
if not open_runs:
|
||||
return None
|
||||
active_run = open_runs[0]
|
||||
run_id = str(getattr(active_run, "run_id", "") or "").strip()
|
||||
if not run_id:
|
||||
return None
|
||||
work_items = await list_items(run_id)
|
||||
if not work_items:
|
||||
return None
|
||||
|
||||
hydrate_links = getattr(store, "hydrate_task_work_item_links", None)
|
||||
if callable(hydrate_links):
|
||||
await hydrate_links(tasks)
|
||||
task_by_work_item_id = task_by_linked_work_item_id(tasks)
|
||||
|
||||
visible_items: list[Any] = []
|
||||
hidden_count = 0
|
||||
for item in work_items:
|
||||
metadata = dict(getattr(item, "metadata", {}) or {})
|
||||
if not str(getattr(item, "parent_work_item_id", "") or "").strip():
|
||||
# Skip the synthetic root work item — the kanban shows leaf delegations.
|
||||
hidden_count += 1
|
||||
continue
|
||||
if bool(metadata.get("attention_work_item", False)):
|
||||
hidden_count += 1
|
||||
continue
|
||||
if should_hide_work_item_from_company_kanban(metadata):
|
||||
hidden_count += 1
|
||||
continue
|
||||
visible_items.append(item)
|
||||
|
||||
task_views: list[BoardTaskView] = []
|
||||
for display_num, item in enumerate(visible_items, start=1):
|
||||
linked_task = task_by_work_item_id.get(str(getattr(item, "work_item_id", "") or "").strip())
|
||||
checkpoint = None
|
||||
linked_session_id = getattr(linked_task, "session_id", None) if linked_task is not None else None
|
||||
if linked_session_id:
|
||||
checkpoint = checkpoint_by_session.get(linked_session_id)
|
||||
task_views.append(
|
||||
self._work_item_to_view(
|
||||
item,
|
||||
linked_task=linked_task,
|
||||
checkpoint=checkpoint,
|
||||
display_num=display_num,
|
||||
)
|
||||
)
|
||||
|
||||
session_summaries = [self._session_summary_view(task) for task in task_views]
|
||||
alerts = self._alerts_from_tasks(task_views)
|
||||
column_order = [column.column_id for column in COMPANY_KANBAN_COLUMNS]
|
||||
return BoardSnapshot(
|
||||
project_id=self._project_id,
|
||||
tasks=task_views,
|
||||
hidden_task_count=hidden_count,
|
||||
pending_checkpoint_count=checkpoint_count,
|
||||
session_summaries=session_summaries,
|
||||
alerts=alerts,
|
||||
metrics=self._build_metrics(
|
||||
task_views,
|
||||
hidden_count=hidden_count,
|
||||
pending_checkpoint_count=checkpoint_count,
|
||||
),
|
||||
mode="company",
|
||||
column_order=column_order,
|
||||
)
|
||||
|
||||
async def load_task_detail(self, task_id: str) -> TaskDetailView | None:
|
||||
engine = await self.facade.ensure_ready()
|
||||
if not engine.store:
|
||||
return None
|
||||
|
||||
tasks = await engine.store.get_tasks(project_id=self._project_id)
|
||||
checkpoints = await engine.store.get_pending_checkpoints(project_id=self._project_id)
|
||||
checkpoint_by_session = self._checkpoint_by_session(checkpoints)
|
||||
await self._enrich_checkpoint_payloads(checkpoint_by_session)
|
||||
|
||||
# In company mode the kanban card_id is a DelegationWorkItem.work_item_id,
|
||||
# not a Task.id. Try the work-item path first; fall back to the runtime
|
||||
# Task path so standard mode (and detail links by Task.id) still works.
|
||||
work_item_detail = await self._load_work_item_detail(
|
||||
engine,
|
||||
work_item_id=task_id,
|
||||
tasks=tasks,
|
||||
checkpoint_by_session=checkpoint_by_session,
|
||||
)
|
||||
if work_item_detail is not None:
|
||||
return work_item_detail
|
||||
|
||||
visible_tasks, _, _ = self._split_visible_tasks(tasks)
|
||||
target = next((task for task in tasks if getattr(task, "id", "") == task_id), None)
|
||||
if target is None:
|
||||
return None
|
||||
|
||||
pending = checkpoint_by_session.get(getattr(target, "session_id", None))
|
||||
linked: list[Any] = []
|
||||
seen_ids: set[str] = set()
|
||||
for task in tasks:
|
||||
origin_task_id = self._origin_task_id(task)
|
||||
if origin_task_id == task_id and getattr(task, "id", "") != task_id:
|
||||
if task.id not in seen_ids:
|
||||
linked.append(task)
|
||||
seen_ids.add(task.id)
|
||||
continue
|
||||
if getattr(task, "parent_session_id", None) and getattr(task, "parent_session_id", None) == getattr(target, "session_id", None):
|
||||
if task.id not in seen_ids and task.id != task_id:
|
||||
linked.append(task)
|
||||
seen_ids.add(task.id)
|
||||
|
||||
transcript: list[SessionMessageView] = []
|
||||
if getattr(target, "session_id", None):
|
||||
raw_transcript = await engine.store.get_session_transcript(target.session_id)
|
||||
transcript = [msg for msg in (self._transcript_item_to_view(item) for item in raw_transcript) if msg is not None]
|
||||
|
||||
display_num = next(
|
||||
(index for index, task in enumerate(visible_tasks, start=1) if getattr(task, "id", "") == task_id),
|
||||
0,
|
||||
)
|
||||
task_view = self._task_to_view(target, checkpoint=pending, linked_tasks=linked, display_num=display_num)
|
||||
linked_views = [self._linked_execution_view(task) for task in linked]
|
||||
result = getattr(target, "result", None) or {}
|
||||
result_content = result.get("content") if isinstance(result, dict) else None
|
||||
artifacts = result.get("artifacts", []) if isinstance(result, dict) else []
|
||||
metadata = getattr(target, "metadata", {}) if isinstance(getattr(target, "metadata", {}), dict) else {}
|
||||
context_preview = (
|
||||
metadata.get("handoff_context")
|
||||
or metadata.get("context_preview")
|
||||
or metadata.get("secretary_context")
|
||||
or None
|
||||
)
|
||||
return TaskDetailView(
|
||||
task=task_view,
|
||||
transcript=transcript,
|
||||
linked_executions=linked_views,
|
||||
progress_entries=list(metadata.get("progress_log", []) or []),
|
||||
pending_checkpoint=pending,
|
||||
result_content=result_content,
|
||||
artifacts=artifacts if isinstance(artifacts, list) else [artifacts],
|
||||
context_preview=str(context_preview).strip() if context_preview else None,
|
||||
)
|
||||
|
||||
async def _load_work_item_detail(
|
||||
self,
|
||||
engine: Any,
|
||||
*,
|
||||
work_item_id: str,
|
||||
tasks: list[Any],
|
||||
checkpoint_by_session: dict[str | None, PendingCheckpointView],
|
||||
) -> TaskDetailView | None:
|
||||
"""Build a detail view keyed by DelegationWorkItem.work_item_id.
|
||||
|
||||
Returns None when the id is not a known work item (caller falls back
|
||||
to the Task-id path).
|
||||
"""
|
||||
store = engine.store
|
||||
list_runs = getattr(store, "list_open_delegation_runs", None)
|
||||
list_items = getattr(store, "list_delegation_work_items", None)
|
||||
if list_runs is None or list_items is None or not work_item_id:
|
||||
return None
|
||||
try:
|
||||
open_runs = await list_runs(project_id=self._project_id)
|
||||
except TypeError:
|
||||
open_runs = await list_runs()
|
||||
if not open_runs:
|
||||
return None
|
||||
target_item: Any | None = None
|
||||
for run in open_runs:
|
||||
run_id = str(getattr(run, "run_id", "") or "").strip()
|
||||
if not run_id:
|
||||
continue
|
||||
for item in await list_items(run_id):
|
||||
if str(getattr(item, "work_item_id", "") or "").strip() == work_item_id:
|
||||
target_item = item
|
||||
break
|
||||
if target_item is not None:
|
||||
break
|
||||
if target_item is None:
|
||||
return None
|
||||
|
||||
task_by_work_item_id = task_by_linked_work_item_id(tasks)
|
||||
linked_task = task_by_work_item_id.get(work_item_id)
|
||||
|
||||
linked_session_id = str(getattr(linked_task, "session_id", "") or "").strip() if linked_task is not None else ""
|
||||
checkpoint = checkpoint_by_session.get(linked_session_id) if linked_session_id else None
|
||||
|
||||
transcript: list[SessionMessageView] = []
|
||||
if linked_session_id:
|
||||
raw_transcript = await engine.store.get_session_transcript(linked_session_id)
|
||||
transcript = [
|
||||
msg
|
||||
for msg in (self._transcript_item_to_view(item) for item in raw_transcript)
|
||||
if msg is not None
|
||||
]
|
||||
|
||||
task_view = self._work_item_to_view(
|
||||
target_item,
|
||||
linked_task=linked_task,
|
||||
checkpoint=checkpoint,
|
||||
display_num=0,
|
||||
)
|
||||
result = getattr(linked_task, "result", None) or {} if linked_task is not None else {}
|
||||
result_content = result.get("content") if isinstance(result, dict) else None
|
||||
artifacts = result.get("artifacts", []) if isinstance(result, dict) else []
|
||||
# Prefer work-item handoff context; fall back to runtime Task metadata.
|
||||
linked_metadata = (
|
||||
getattr(linked_task, "metadata", None) or {}
|
||||
if linked_task is not None
|
||||
else {}
|
||||
)
|
||||
view = WorkItemContextView(target_item, linked_task)
|
||||
context_preview = (
|
||||
view.get("handoff_context")
|
||||
or view.get("context_preview")
|
||||
or (linked_metadata.get("secretary_context") if isinstance(linked_metadata, dict) else None)
|
||||
or None
|
||||
)
|
||||
progress_log = view.get_list("progress_log")
|
||||
return TaskDetailView(
|
||||
task=task_view,
|
||||
transcript=transcript,
|
||||
linked_executions=[self._linked_execution_view(linked_task)] if linked_task is not None else [],
|
||||
progress_entries=progress_log,
|
||||
pending_checkpoint=checkpoint,
|
||||
result_content=result_content,
|
||||
artifacts=artifacts if isinstance(artifacts, list) else [artifacts],
|
||||
context_preview=str(context_preview).strip() if context_preview else None,
|
||||
)
|
||||
|
||||
async def load_pipeline_state(
|
||||
self,
|
||||
parent_task_id: str,
|
||||
runtime_lookup: dict[str, Any] | None = None,
|
||||
) -> PipelineSnapshot | None:
|
||||
"""Build the terminal Office-style work-item pipeline for a run."""
|
||||
engine = await self.facade.ensure_ready()
|
||||
store = getattr(engine, "store", None)
|
||||
if not store:
|
||||
return None
|
||||
list_runs = getattr(store, "list_open_delegation_runs", None)
|
||||
list_items = getattr(store, "list_delegation_work_items", None)
|
||||
if list_runs is None or list_items is None:
|
||||
return None
|
||||
try:
|
||||
runs = list(await list_runs(project_id=self._project_id))
|
||||
except TypeError:
|
||||
runs = list(await list_runs())
|
||||
if not runs:
|
||||
return None
|
||||
|
||||
tasks = await store.get_tasks(project_id=self._project_id) if hasattr(store, "get_tasks") else []
|
||||
hydrate_links = getattr(store, "hydrate_task_work_item_links", None)
|
||||
if callable(hydrate_links):
|
||||
await hydrate_links(tasks)
|
||||
linked_tasks = task_by_linked_work_item_id(tasks)
|
||||
|
||||
selected_run = None
|
||||
selected_items: list[Any] = []
|
||||
target = str(parent_task_id or "").strip()
|
||||
for run in runs:
|
||||
run_id = str(getattr(run, "run_id", "") or "").strip()
|
||||
items = list(await list_items(run_id)) if run_id else []
|
||||
run_keys = {
|
||||
run_id,
|
||||
str(getattr(run, "parent_task_id", "") or "").strip(),
|
||||
str(getattr(run, "task_id", "") or "").strip(),
|
||||
str(getattr(run, "parent_session_id", "") or "").strip(),
|
||||
}
|
||||
item_keys = {str(getattr(item, "work_item_id", "") or "").strip() for item in items}
|
||||
if not target or target in run_keys or target in item_keys:
|
||||
selected_run = run
|
||||
selected_items = items
|
||||
break
|
||||
if selected_run is None:
|
||||
selected_run = runs[0]
|
||||
run_id = str(getattr(selected_run, "run_id", "") or "").strip()
|
||||
selected_items = list(await list_items(run_id)) if run_id else []
|
||||
if not selected_items:
|
||||
return None
|
||||
|
||||
work_items: list[PipelineWorkItemView] = []
|
||||
start_ts = 0.0
|
||||
end_ts = 0.0
|
||||
for item in selected_items:
|
||||
work_item_id = str(getattr(item, "work_item_id", "") or "").strip()
|
||||
linked_task = linked_tasks.get(work_item_id)
|
||||
linked_task_id = str(getattr(linked_task, "id", "") or "").strip() if linked_task is not None else ""
|
||||
metadata = dict(getattr(item, "metadata", {}) or {})
|
||||
linked_metadata = dict(getattr(linked_task, "metadata", {}) or {}) if linked_task is not None else {}
|
||||
phase = coerce_phase(getattr(item, "phase", Phase.READY))
|
||||
created_at = datetime_to_timestamp(getattr(item, "created_at", None))
|
||||
updated_at = datetime_to_timestamp(getattr(item, "updated_at", None) or getattr(item, "created_at", None))
|
||||
if created_at and (not start_ts or created_at < start_ts):
|
||||
start_ts = created_at
|
||||
if updated_at and updated_at > end_ts:
|
||||
end_ts = updated_at
|
||||
runtime = None
|
||||
if runtime_lookup:
|
||||
runtime = runtime_lookup.get(linked_task_id) or runtime_lookup.get(work_item_id)
|
||||
work_items.append(PipelineWorkItemView(
|
||||
projection_id=work_item_id,
|
||||
title=str(getattr(item, "title", "") or work_item_id),
|
||||
role_id=str(getattr(item, "role_id", "") or ""),
|
||||
status=phase.value,
|
||||
assigned_to=str(getattr(linked_task, "assigned_to", "") or getattr(item, "role_id", "") or ""),
|
||||
task_id=linked_task_id or None,
|
||||
runtime_task_id=linked_task_id or None,
|
||||
execution_turn_id=linked_task_id or None,
|
||||
session_id=str(getattr(linked_task, "session_id", "") or "") or None,
|
||||
elapsed_sec=max(0.0, (updated_at or _time.time()) - created_at) if created_at else 0.0,
|
||||
current_tool=getattr(runtime, "current_tool", None) if runtime is not None else None,
|
||||
tool_elapsed_ms=int(getattr(runtime, "tool_elapsed_ms", 0) or 0) if runtime is not None else 0,
|
||||
last_tool_summary=str(getattr(runtime, "last_tool_summary", "") or "") if runtime is not None else "",
|
||||
context_remaining_pct=int(getattr(runtime, "context_remaining_pct", 0) or 0) if runtime is not None else 0,
|
||||
turn_cost_usd=float(getattr(runtime, "turn_cost_usd", 0.0) or 0.0) if runtime is not None else 0.0,
|
||||
has_gate=bool(metadata.get("gate_type") or linked_metadata.get("checkpoint_hint") or (getattr(runtime, "pending_permission_count", 0) if runtime is not None else 0)),
|
||||
gate_type=str(metadata.get("gate_type") or linked_metadata.get("checkpoint_type") or "") or None,
|
||||
dependencies=[
|
||||
str(dep).strip()
|
||||
for dep in list(metadata.get("dependency_work_item_ids", []) or [])
|
||||
if str(dep).strip()
|
||||
],
|
||||
parallel_group=str(metadata.get("parallel_group", "") or "") or None,
|
||||
))
|
||||
|
||||
done_count = sum(1 for item in work_items if item.status in {"done", "reviewed", "delivered"})
|
||||
parent_title = (
|
||||
str(getattr(selected_run, "title", "") or "")
|
||||
or str(getattr(selected_run, "summary", "") or "")
|
||||
or str(getattr(selected_run, "run_id", "") or "")
|
||||
)
|
||||
return PipelineSnapshot(
|
||||
parent_task_id=str(getattr(selected_run, "parent_task_id", "") or getattr(selected_run, "task_id", "") or target),
|
||||
parent_title=parent_title,
|
||||
profile=str(getattr(selected_run, "profile", "") or getattr(selected_run, "company_profile", "") or ""),
|
||||
work_items=work_items,
|
||||
done_count=done_count,
|
||||
total_count=len(work_items),
|
||||
elapsed_sec=max(0.0, (end_ts or _time.time()) - start_ts) if start_ts else 0.0,
|
||||
)
|
||||
|
||||
async def load_org_snapshot(self) -> OrgSnapshotView | None:
|
||||
"""Build a read-only view of the current org structure."""
|
||||
engine = await self.facade.ensure_ready()
|
||||
if not engine.org_engine:
|
||||
return None
|
||||
|
||||
org = engine.org_engine
|
||||
|
||||
# Roles as tree
|
||||
all_agents = org.list_agents()
|
||||
all_employees = org.list_employees()
|
||||
emp_count_by_role: dict[str, int] = {}
|
||||
for emp in all_employees:
|
||||
emp_count_by_role[emp.role_id] = emp_count_by_role.get(emp.role_id, 0) + 1
|
||||
|
||||
try:
|
||||
tree_raw = org.get_org_tree()
|
||||
except Exception:
|
||||
tree_raw = []
|
||||
|
||||
def _build_role_tree(nodes: list[dict]) -> list[OrgRoleView]:
|
||||
result: list[OrgRoleView] = []
|
||||
for node in nodes:
|
||||
agent = node.get("agent")
|
||||
if agent is None:
|
||||
continue
|
||||
role_id = getattr(agent, "role_id", "")
|
||||
result.append(OrgRoleView(
|
||||
role_id=role_id,
|
||||
name=getattr(agent, "name", role_id),
|
||||
responsibility=getattr(agent, "responsibility", ""),
|
||||
reports_to=getattr(agent, "reports_to", "owner"),
|
||||
employee_count=emp_count_by_role.get(role_id, 0),
|
||||
children=_build_role_tree(node.get("reports", [])),
|
||||
))
|
||||
return result
|
||||
|
||||
role_tree = _build_role_tree(tree_raw)
|
||||
|
||||
# Employees
|
||||
employee_views = [
|
||||
OrgEmployeeView(
|
||||
employee_id=emp.employee_id,
|
||||
name=emp.name,
|
||||
role_id=emp.role_id,
|
||||
category=emp.category,
|
||||
domains=list(emp.domains),
|
||||
seniority=emp.seniority,
|
||||
)
|
||||
for emp in all_employees
|
||||
]
|
||||
|
||||
profile = org.get_company_profile()
|
||||
|
||||
return OrgSnapshotView(
|
||||
role_tree=role_tree,
|
||||
employees=employee_views,
|
||||
company_profile=profile,
|
||||
role_count=len(all_agents),
|
||||
employee_count=len(all_employees),
|
||||
)
|
||||
|
||||
@property
|
||||
def _project_id(self) -> str:
|
||||
return self.project_id or self.facade.project_id or "default"
|
||||
|
||||
@staticmethod
|
||||
def _origin_task_id(task: Any) -> str | None:
|
||||
metadata = getattr(task, "metadata", None)
|
||||
if not isinstance(metadata, dict):
|
||||
return None
|
||||
origin_task_id = str(metadata.get("origin_task_id", "") or "").strip()
|
||||
return origin_task_id or None
|
||||
|
||||
def _split_visible_tasks(self, tasks: list[Any]) -> tuple[list[Any], dict[str, list[Any]], int]:
|
||||
visible_tasks: list[Any] = []
|
||||
hidden_by_origin: dict[str, list[Any]] = defaultdict(list)
|
||||
hidden_count = 0
|
||||
for task in tasks:
|
||||
origin_task_id = self._origin_task_id(task)
|
||||
if origin_task_id and origin_task_id != getattr(task, "id", ""):
|
||||
hidden_by_origin[origin_task_id].append(task)
|
||||
hidden_count += 1
|
||||
continue
|
||||
visible_tasks.append(task)
|
||||
return visible_tasks, hidden_by_origin, hidden_count
|
||||
|
||||
@staticmethod
|
||||
def _checkpoint_prompt(checkpoint: Any) -> tuple[str, str]:
|
||||
payload = getattr(checkpoint, "payload", {}) or {}
|
||||
prompt = (
|
||||
payload.get("prompt")
|
||||
or payload.get("message")
|
||||
or payload.get("summary")
|
||||
or payload.get("original_message")
|
||||
or ""
|
||||
)
|
||||
prompt_text = str(prompt).strip()
|
||||
if not prompt_text:
|
||||
prompt_text = json.dumps(payload, ensure_ascii=False, indent=2)[:600]
|
||||
|
||||
summary = (
|
||||
payload.get("feedback_scope")
|
||||
or payload.get("work_item_projection_title")
|
||||
or payload.get("summary")
|
||||
or payload.get("title")
|
||||
or getattr(checkpoint, "checkpoint_type", "pending")
|
||||
)
|
||||
return str(summary).strip(), prompt_text
|
||||
|
||||
def _checkpoint_view(self, checkpoint: Any) -> PendingCheckpointView:
|
||||
summary, prompt = self._checkpoint_prompt(checkpoint)
|
||||
return PendingCheckpointView(
|
||||
checkpoint_id=str(getattr(checkpoint, "checkpoint_id", "") or ""),
|
||||
checkpoint_type=str(getattr(checkpoint, "checkpoint_type", "") or ""),
|
||||
status=str(getattr(checkpoint, "status", "") or ""),
|
||||
session_id=getattr(checkpoint, "session_id", None),
|
||||
task_id=getattr(checkpoint, "task_id", None),
|
||||
summary=summary,
|
||||
prompt=prompt,
|
||||
payload=dict(getattr(checkpoint, "payload", {}) or {}),
|
||||
)
|
||||
|
||||
def _checkpoint_by_session(self, checkpoints: list[Any]) -> dict[str | None, PendingCheckpointView]:
|
||||
result: dict[str | None, PendingCheckpointView] = {}
|
||||
for checkpoint in checkpoints:
|
||||
session_id = getattr(checkpoint, "session_id", None)
|
||||
if session_id not in result:
|
||||
result[session_id] = self._checkpoint_view(checkpoint)
|
||||
return result
|
||||
|
||||
async def _enrich_checkpoint_payloads(
|
||||
self,
|
||||
checkpoint_map: dict[str | None, PendingCheckpointView],
|
||||
) -> None:
|
||||
"""Enrich reorg checkpoint payloads with full proposal data from store."""
|
||||
engine = await self.facade.ensure_ready()
|
||||
if not engine.store:
|
||||
return
|
||||
for view in checkpoint_map.values():
|
||||
if view.checkpoint_type != "company_reorg_pending":
|
||||
continue
|
||||
proposal_id = str(view.payload.get("proposal_id", "") or "").strip()
|
||||
if not proposal_id:
|
||||
continue
|
||||
try:
|
||||
proposal = await engine.store.get_reorg_proposal(proposal_id)
|
||||
except Exception:
|
||||
continue
|
||||
if proposal is None:
|
||||
continue
|
||||
view.payload["title"] = proposal.title
|
||||
view.payload["scope"] = proposal.scope.value if hasattr(proposal.scope, "value") else str(proposal.scope)
|
||||
view.payload["risk_level"] = proposal.risk_level.value if hasattr(proposal.risk_level, "value") else str(proposal.risk_level)
|
||||
view.payload["summary"] = proposal.summary
|
||||
view.payload["rationale"] = proposal.rationale
|
||||
view.payload["impact_summary"] = dict(proposal.impact_summary) if proposal.impact_summary else {}
|
||||
changeset = proposal.changeset
|
||||
if changeset:
|
||||
view.payload["role_changes"] = [
|
||||
{"action": rc.action, "role_id": rc.role_id, "replacement_role_id": getattr(rc, "replacement_role_id", ""), "reason": getattr(rc, "reason", "")}
|
||||
for rc in (changeset.role_changes if hasattr(changeset, "role_changes") else [])
|
||||
]
|
||||
|
||||
def _work_item_to_view(
|
||||
self,
|
||||
item: Any,
|
||||
*,
|
||||
linked_task: Any | None,
|
||||
checkpoint: PendingCheckpointView | None,
|
||||
display_num: int = 0,
|
||||
) -> BoardTaskView:
|
||||
"""Build a BoardTaskView from a DelegationWorkItem (company-mode card).
|
||||
|
||||
The card identity is the work_item_id; runtime Task / session are
|
||||
kept only as audit references via runtime_task_id / session_id.
|
||||
"""
|
||||
normalized_metadata = dict(getattr(item, "metadata", {}) or {})
|
||||
phase = coerce_phase(getattr(item, "phase", Phase.READY))
|
||||
column_id = kanban_column(phase).replace("_", "-")
|
||||
canonical_status = phase.value
|
||||
work_item_id = str(getattr(item, "work_item_id", "") or "").strip()
|
||||
role_id = str(getattr(item, "role_id", "") or "").strip()
|
||||
title = str(getattr(item, "title", "") or "").strip()
|
||||
summary = str(getattr(item, "summary", "") or "").strip()
|
||||
kind = str(getattr(item, "kind", "") or "").strip()
|
||||
dependencies = [
|
||||
str(dep).strip()
|
||||
for dep in list(normalized_metadata.get("dependency_work_item_ids", []) or [])
|
||||
if str(dep).strip()
|
||||
]
|
||||
created_at = datetime_to_timestamp(getattr(item, "created_at", None))
|
||||
updated_at = datetime_to_timestamp(getattr(item, "updated_at", None) or getattr(item, "created_at", None))
|
||||
|
||||
# Audit/back-references (NOT used to drive lifecycle on the card).
|
||||
linked_task_id = str(getattr(linked_task, "id", "") or "").strip() if linked_task is not None else ""
|
||||
linked_session_id = str(getattr(linked_task, "session_id", "") or "").strip() if linked_task is not None else ""
|
||||
result = getattr(linked_task, "result", None) or {} if linked_task is not None else {}
|
||||
result_content = result.get("content") if isinstance(result, dict) else None
|
||||
artifacts = result.get("artifacts", []) if isinstance(result, dict) else []
|
||||
# Merge work-item metadata first, then linked-task metadata so the card
|
||||
# surfaces work-item-truth (status/dependencies) plus runtime telemetry.
|
||||
merged_metadata: dict[str, Any] = dict(normalized_metadata)
|
||||
if linked_task is not None:
|
||||
linked_meta = getattr(linked_task, "metadata", None) or {}
|
||||
if isinstance(linked_meta, dict):
|
||||
for key, value in linked_meta.items():
|
||||
merged_metadata.setdefault(key, value)
|
||||
|
||||
return BoardTaskView(
|
||||
task_id=work_item_id,
|
||||
title=title,
|
||||
description=summary,
|
||||
status=canonical_status,
|
||||
column_id=column_id,
|
||||
priority=None,
|
||||
assignee_ids=[role_id] if role_id else [],
|
||||
assigned_to=role_id,
|
||||
tags=[kind] if kind else [],
|
||||
session_id=linked_session_id or None,
|
||||
created_at=created_at,
|
||||
updated_at=updated_at,
|
||||
metadata=merged_metadata,
|
||||
pending_checkpoint=checkpoint,
|
||||
linked_task_count=1 if linked_task is not None else 0,
|
||||
result_content=result_content,
|
||||
artifacts=artifacts if isinstance(artifacts, list) else [artifacts],
|
||||
origin_task_id=None,
|
||||
display_id=f"OPC-{display_num}" if display_num else "",
|
||||
dependencies=dependencies,
|
||||
work_item_id=work_item_id,
|
||||
runtime_task_id=linked_task_id or None,
|
||||
execution_turn_id=linked_task_id or None,
|
||||
)
|
||||
|
||||
def _task_to_view(
|
||||
self,
|
||||
task: Any,
|
||||
*,
|
||||
checkpoint: PendingCheckpointView | None,
|
||||
linked_tasks: list[Any],
|
||||
display_num: int = 0,
|
||||
) -> BoardTaskView:
|
||||
payload = build_base_task_payload(task, display_num)
|
||||
result = getattr(task, "result", None) or {}
|
||||
result_content = result.get("content") if isinstance(result, dict) else None
|
||||
artifacts = result.get("artifacts", []) if isinstance(result, dict) else []
|
||||
metadata = getattr(task, "metadata", {}) if isinstance(getattr(task, "metadata", {}), dict) else {}
|
||||
return BoardTaskView(
|
||||
task_id=payload["task_id"],
|
||||
title=payload["title"],
|
||||
description=payload["description"],
|
||||
status=payload["status"],
|
||||
column_id=payload["column_id"],
|
||||
priority=payload["priority"],
|
||||
assignee_ids=payload["assignee_ids"],
|
||||
assigned_to=str(getattr(task, "assigned_to", "") or ""),
|
||||
tags=payload["tags"],
|
||||
session_id=payload["session_id"],
|
||||
created_at=float(payload["created_at"]),
|
||||
updated_at=float(payload["updated_at"]),
|
||||
metadata=metadata,
|
||||
pending_checkpoint=checkpoint,
|
||||
linked_task_count=len(linked_tasks),
|
||||
result_content=result_content,
|
||||
artifacts=artifacts if isinstance(artifacts, list) else [artifacts],
|
||||
origin_task_id=self._origin_task_id(task),
|
||||
display_id=str(payload.get("display_id", "") or ""),
|
||||
dependencies=list(payload.get("dependencies", []) or []),
|
||||
runtime_task_id=payload["task_id"],
|
||||
execution_turn_id=payload["task_id"],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _session_summary_view(task: BoardTaskView) -> SessionSummaryView:
|
||||
return SessionSummaryView(
|
||||
task_id=task.task_id,
|
||||
title=task.title,
|
||||
status=task.status,
|
||||
column_id=task.column_id,
|
||||
session_id=task.session_id,
|
||||
updated_at=float(task.updated_at),
|
||||
created_at=float(task.created_at),
|
||||
assigned_to=task.assigned_to,
|
||||
priority=task.priority,
|
||||
pending_checkpoint=task.pending_checkpoint is not None,
|
||||
linked_task_count=task.linked_task_count,
|
||||
tags=list(task.tags),
|
||||
runtime_task_id=task.runtime_task_id,
|
||||
execution_turn_id=task.execution_turn_id or task.runtime_task_id,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _alerts_from_tasks(tasks: list[BoardTaskView]) -> list[BoardAlert]:
|
||||
alerts: list[BoardAlert] = []
|
||||
for task in tasks:
|
||||
item_label = "Work item" if task.work_item_id else "Task"
|
||||
if task.pending_checkpoint:
|
||||
alerts.append(
|
||||
BoardAlert(
|
||||
alert_id=f"checkpoint:{task.task_id}",
|
||||
level="warn",
|
||||
title="Checkpoint pending",
|
||||
message=f"{task.title} is waiting for human feedback.",
|
||||
task_id=task.task_id,
|
||||
created_at=float(task.updated_at or task.created_at),
|
||||
)
|
||||
)
|
||||
if task.status in {"blocked", "awaiting_peer", "awaiting_review"}:
|
||||
alerts.append(
|
||||
BoardAlert(
|
||||
alert_id=f"blocked:{task.task_id}",
|
||||
level="warn",
|
||||
title=f"{item_label} paused",
|
||||
message=f"{task.title} is paused in `{task.status}`.",
|
||||
task_id=task.task_id,
|
||||
created_at=float(task.updated_at or task.created_at),
|
||||
)
|
||||
)
|
||||
if task.status in {"failed", "cancelled"}:
|
||||
alerts.append(
|
||||
BoardAlert(
|
||||
alert_id=f"terminal:{task.task_id}",
|
||||
level="error",
|
||||
title=f"{item_label} ended abnormally",
|
||||
message=f"{task.title} finished with status `{task.status}`.",
|
||||
task_id=task.task_id,
|
||||
created_at=float(task.updated_at or task.created_at),
|
||||
)
|
||||
)
|
||||
level_weight = {"error": 0, "warn": 1, "info": 2}
|
||||
alerts.sort(key=lambda alert: (level_weight.get(alert.level, 99), -float(alert.created_at)))
|
||||
return alerts[:12]
|
||||
|
||||
@staticmethod
|
||||
def _build_metrics(
|
||||
tasks: list[BoardTaskView],
|
||||
*,
|
||||
hidden_count: int,
|
||||
pending_checkpoint_count: int,
|
||||
) -> BoardMetrics:
|
||||
todo_count = sum(1 for task in tasks if task.column_id == "todo")
|
||||
in_progress_count = sum(1 for task in tasks if task.column_id == "in-progress")
|
||||
in_review_count = sum(1 for task in tasks if task.column_id == "in-review")
|
||||
done_count = sum(1 for task in tasks if task.column_id == "done")
|
||||
blocked_count = sum(1 for task in tasks if task.status in {"blocked", "awaiting_peer", "awaiting_review"})
|
||||
failed_count = sum(1 for task in tasks if task.status in {"failed", "cancelled"})
|
||||
active_session_count = sum(1 for task in tasks if task.session_id)
|
||||
running_count = sum(
|
||||
1
|
||||
for task in tasks
|
||||
if task.status in {"running", "idle", "blocked", "awaiting_peer", "awaiting_review"}
|
||||
)
|
||||
return BoardMetrics(
|
||||
total_tasks=len(tasks) + hidden_count,
|
||||
visible_tasks=len(tasks),
|
||||
filtered_tasks=len(tasks),
|
||||
hidden_task_count=hidden_count,
|
||||
todo_count=todo_count,
|
||||
in_progress_count=in_progress_count,
|
||||
in_review_count=in_review_count,
|
||||
done_count=done_count,
|
||||
running_count=running_count,
|
||||
blocked_count=blocked_count,
|
||||
failed_count=failed_count,
|
||||
pending_checkpoint_count=pending_checkpoint_count,
|
||||
active_session_count=active_session_count,
|
||||
alert_count=len(BoardRepository._alerts_from_tasks(tasks)),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _linked_execution_view(task: Any) -> LinkedExecutionView:
|
||||
payload = build_base_task_payload(task, 0)
|
||||
return LinkedExecutionView(
|
||||
task_id=payload["task_id"],
|
||||
title=payload["title"],
|
||||
status=payload["status"],
|
||||
assigned_to=str(getattr(task, "assigned_to", "") or ""),
|
||||
session_id=payload["session_id"],
|
||||
created_at=float(payload["created_at"]),
|
||||
updated_at=float(payload["updated_at"]),
|
||||
runtime_task_id=payload["task_id"],
|
||||
execution_turn_id=payload["task_id"],
|
||||
metadata=dict(getattr(task, "metadata", {}) or {}),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _render_parts(parts: list[Any]) -> str:
|
||||
lines: list[str] = []
|
||||
for part in parts:
|
||||
part_type = getattr(part, "part_type", "")
|
||||
payload = getattr(part, "payload", {}) if isinstance(getattr(part, "payload", {}), dict) else {}
|
||||
if part_type == "text":
|
||||
text = payload.get("text", "")
|
||||
if text:
|
||||
lines.append(str(text))
|
||||
elif part_type in {"subtask_result", "task_result"}:
|
||||
title = payload.get("task_title", "Task")
|
||||
summary = payload.get("summary", "")
|
||||
lines.append(f"{title}: {summary}".strip(": "))
|
||||
return "\n".join(lines).strip()
|
||||
|
||||
def _transcript_item_to_view(self, item: dict[str, Any]) -> SessionMessageView | None:
|
||||
message = item.get("message")
|
||||
if not message or getattr(message, "summary_flag", False):
|
||||
return None
|
||||
|
||||
content = self._render_parts(item.get("parts", []))
|
||||
if not content:
|
||||
return None
|
||||
|
||||
role = str(getattr(message, "role", "") or "").strip().lower()
|
||||
agent_id = str(getattr(message, "agent_id", "") or "").strip()
|
||||
sender_name = {
|
||||
"user": "You",
|
||||
"assistant": "OPC",
|
||||
"system": "System",
|
||||
"subagent": agent_id.replace("_", " ").replace("-", " ").title() if agent_id else "Subagent",
|
||||
}.get(role, agent_id or role.title() or "OPC")
|
||||
created_at = getattr(message, "created_at", None)
|
||||
timestamp = created_at.timestamp() if hasattr(created_at, "timestamp") else 0.0
|
||||
return SessionMessageView(
|
||||
message_id=str(getattr(message, "message_id", "") or ""),
|
||||
role=role or "assistant",
|
||||
sender_name=sender_name,
|
||||
content=content,
|
||||
created_at=timestamp,
|
||||
metadata={"agent_id": agent_id} if agent_id else {},
|
||||
)
|
||||
Reference in New Issue
Block a user