Files
2026-07-01 17:56:31 +08:00

523 lines
26 KiB
Python

"""Company work-item read service."""
from __future__ import annotations
from collections import Counter, defaultdict
from typing import Any
from opc.layer2_organization.phase import coerce_phase, kanban_column, should_hide_work_item_from_company_kanban
from opc.layer2_organization.work_item_links import task_by_linked_work_item_id
from .context import OfficeServiceContext
from .models import ServiceError, ServiceResult
class WorkItemService:
def __init__(self, context: OfficeServiceContext) -> None:
self.context = context
async def list(
self,
*,
project_id: str,
session_id: str | None = None,
role_id: str | None = None,
status: str | None = None,
limit: int = 100,
kanban_visible_only: bool = False,
) -> ServiceResult:
session_id = str(session_id or "").strip()
engine = await self.context.engine_for_project(project_id)
store = getattr(engine, "store", None)
if not self.context.store_is_ready(store):
raise ServiceError("store_not_ready", "store_not_ready", {"project_id": project_id})
runs = await self._runs(store, project_id, session_id=session_id or None)
tasks = await store.get_tasks(project_id=project_id) if hasattr(store, "get_tasks") else []
hydrate = getattr(store, "hydrate_task_work_item_links", None)
if callable(hydrate):
await hydrate(tasks)
linked_tasks = task_by_linked_work_item_id(tasks)
rows: list[dict[str, Any]] = []
for run in runs:
run_id = str(getattr(run, "run_id", "") or "")
if not run_id:
continue
for item in await store.list_delegation_work_items(run_id):
linked_task = linked_tasks.get(str(getattr(item, "work_item_id", "") or ""))
if session_id and not self._matches_session_scope(session_id, run=run, item=item, linked_task=linked_task):
continue
if kanban_visible_only and not self._is_company_kanban_visible(item):
continue
payload = self._work_item_payload(item, run=run, linked_task=linked_task)
if role_id and payload.get("role_id") != role_id:
continue
if status and payload.get("phase") != status and payload.get("kanban_column") != status:
continue
rows.append(payload)
rows.sort(key=lambda item: str(item.get("updated_at", "")), reverse=True)
return ServiceResult({
"project_id": project_id,
"session_id": session_id,
"work_items": rows[: max(1, int(limit or 100))],
})
async def show(self, *, project_id: str, work_item_id: str, limit: int = 100) -> ServiceResult:
engine = await self.context.engine_for_project(project_id)
store = getattr(engine, "store", None)
if not self.context.store_is_ready(store):
raise ServiceError("store_not_ready", "store_not_ready", {"project_id": project_id})
runs = await self._runs(store, project_id)
tasks = await store.get_tasks(project_id=project_id) if hasattr(store, "get_tasks") else []
hydrate = getattr(store, "hydrate_task_work_item_links", None)
if callable(hydrate):
await hydrate(tasks)
linked_tasks = task_by_linked_work_item_id(tasks)
target = None
target_run = None
for run in runs:
run_id = str(getattr(run, "run_id", "") or "")
for item in await store.list_delegation_work_items(run_id):
if str(getattr(item, "work_item_id", "") or "") == work_item_id:
target = item
target_run = run
break
if target is not None:
break
if target is None:
raise ServiceError("work_item_not_found", "Work item not found", {"work_item_id": work_item_id})
linked_task = linked_tasks.get(work_item_id)
logs = await self.logs(project_id=project_id, work_item_id=work_item_id, limit=limit)
return ServiceResult({
"project_id": project_id,
"work_item": self._work_item_payload(target, run=target_run, linked_task=linked_task),
"logs": logs.payload,
})
async def logs(
self,
*,
project_id: str,
session_id: str | None = None,
work_item_id: str = "",
role_id: str = "",
limit: int = 100,
) -> ServiceResult:
session_id = str(session_id or "").strip()
work_item_id = str(work_item_id or "").strip()
role_id = str(role_id or "").strip()
engine = await self.context.engine_for_project(project_id)
store = getattr(engine, "store", None)
if not self.context.store_is_ready(store):
raise ServiceError("store_not_ready", "store_not_ready", {"project_id": project_id})
runs = await self._runs(store, project_id, session_id=session_id or None)
tasks = await store.get_tasks(project_id=project_id) if hasattr(store, "get_tasks") else []
hydrate = getattr(store, "hydrate_task_work_item_links", None)
if callable(hydrate):
await hydrate(tasks)
linked_tasks = task_by_linked_work_item_id(tasks)
target_items: list[tuple[Any, Any, Any | None]] = []
target_work_item_ids: set[str] = set()
for run in runs:
run_id = str(getattr(run, "run_id", "") or "")
if not run_id:
continue
for item in await store.list_delegation_work_items(run_id):
item_id = str(getattr(item, "work_item_id", "") or "")
if work_item_id and item_id != work_item_id:
continue
if role_id and str(getattr(item, "role_id", "") or "") != role_id:
continue
linked_task = linked_tasks.get(item_id)
target_items.append((item, run, linked_task))
if item_id:
target_work_item_ids.add(item_id)
events: list[dict[str, Any]] = []
target_run_ids = {str(getattr(run, "run_id", "") or "") for run in runs}
for run_id in target_run_ids:
if hasattr(store, "list_delegation_events"):
for event in await store.list_delegation_events(run_id):
event_work_item_id = str(getattr(event, "work_item_id", "") or "")
event_role_id = str(getattr(event, "role_id", "") or "")
if work_item_id and event_work_item_id != work_item_id:
continue
if role_id and event_role_id != role_id and event_work_item_id not in target_work_item_ids:
continue
events.append(self._event_payload(event))
runtime_sessions: list[dict[str, Any]] = []
external_sessions: list[dict[str, Any]] = []
runtime_events: list[dict[str, Any]] = []
runtime_transcript_entries: list[dict[str, Any]] = []
runtime_tool_calls: list[dict[str, Any]] = []
runtime_tool_results: list[dict[str, Any]] = []
runtime_permission_grants: list[dict[str, Any]] = []
transcript: list[Any] = []
handoffs: list[Any] = []
runtime_rows_by_id: dict[str, dict[str, Any]] = {}
runtime_ids: set[str] = set()
external_keys: set[tuple[str, str, str]] = set()
def add_runtime_row(row: Any) -> None:
payload = self._model_payload(row)
runtime_id = str(payload.get("runtime_session_id", "") or "").strip()
if not runtime_id:
return
runtime_rows_by_id[runtime_id] = payload
runtime_ids.add(runtime_id)
def add_runtime_id(runtime_id: Any, *, task: Any = None, role_session: Any = None, source: str = "") -> None:
runtime_id = str(runtime_id or "").strip()
if not runtime_id:
return
runtime_ids.add(runtime_id)
if runtime_id in runtime_rows_by_id:
return
metadata = {"source": source} if source else {}
if role_session is not None:
metadata.update({"source": source or "role_runtime_session", "role_id": str(getattr(role_session, "role_id", "") or "")})
runtime_rows_by_id[runtime_id] = {
"runtime_session_id": runtime_id,
"project_id": project_id,
"session_id": str(getattr(task, "session_id", "") or session_id or ""),
"task_id": str(getattr(task, "id", "") or ""),
"status": str(getattr(role_session, "status", "") or ""),
"metadata": metadata,
"created_at": self._date_value(getattr(role_session, "created_at", None)),
"updated_at": self._date_value(getattr(role_session, "updated_at", None)),
}
def add_external_session(row: Any) -> None:
payload = self._model_payload(row)
key = (
str(payload.get("agent_type", "") or ""),
str(payload.get("session_id", "") or ""),
str(payload.get("task_id", "") or ""),
)
if key in external_keys:
return
external_keys.add(key)
external_sessions.append(payload)
metadata = dict(payload.get("metadata", {}) or {})
add_runtime_id(metadata.get("runtime_session_id"), source="external_session")
add_runtime_id(metadata.get("delegation_role_session_id"), source="external_role_session")
add_runtime_id(payload.get("opc_session_id"), source="external_opc_session")
for item, run, linked_task in target_items:
item_id = str(getattr(item, "work_item_id", "") or "")
if linked_task is not None:
linked_session_id = str(getattr(linked_task, "session_id", "") or "")
if linked_session_id and hasattr(store, "get_session_transcript"):
transcript.extend((await store.get_session_transcript(linked_session_id))[-limit:])
if hasattr(store, "list_runtime_sessions"):
for row in await store.list_runtime_sessions(project_id=project_id, task_id=getattr(linked_task, "id", ""), limit=limit):
add_runtime_row(row)
if linked_session_id:
for row in await store.list_runtime_sessions(project_id=project_id, session_id=linked_session_id, limit=limit):
add_runtime_row(row)
if hasattr(store, "list_external_sessions"):
for row in await store.list_external_sessions(project_id=project_id, task_id=getattr(linked_task, "id", ""), limit=limit):
add_external_session(row)
for runtime_id in self._task_runtime_session_ids(linked_task):
add_runtime_id(runtime_id, task=linked_task, source="task_metadata")
for runtime_id in self._work_item_runtime_session_ids(item):
add_runtime_id(runtime_id, task=linked_task, source="work_item")
if hasattr(store, "get_handoff_records") and item_id:
handoffs.extend(await store.get_handoff_records(project_id=project_id, target_work_item_id=item_id, limit=limit))
if hasattr(store, "list_role_runtime_sessions"):
for run in runs:
run_id = str(getattr(run, "run_id", "") or "")
if not run_id:
continue
try:
role_sessions = await store.list_role_runtime_sessions(run_id, role_id=role_id or None)
except TypeError:
role_sessions = await store.list_role_runtime_sessions(run_id)
if role_id:
role_sessions = [item for item in role_sessions if str(getattr(item, "role_id", "") or "") == role_id]
for role_session in role_sessions:
role_session_id = str(getattr(role_session, "role_session_id", "") or "")
focused = str(getattr(role_session, "focused_work_item_id", "") or "")
related = {focused}
related.update(str(item or "").strip() for item in list(getattr(role_session, "background_work_item_ids", []) or []))
related.update(str(item or "").strip() for item in list(getattr(role_session, "pending_work_item_ids", []) or []))
if work_item_id and work_item_id not in related:
continue
add_runtime_id(role_session_id, role_session=role_session, source="role_runtime_session")
if hasattr(store, "list_external_sessions") and role_session_id:
for row in await store.list_external_sessions(project_id=project_id, opc_session_id=role_session_id, limit=limit):
add_external_session(row)
for runtime_id in sorted(runtime_ids):
if hasattr(store, "list_runtime_events"):
runtime_events.extend(await store.list_runtime_events(runtime_id, limit=limit))
if hasattr(store, "list_runtime_transcript_entries"):
runtime_transcript_entries.extend((await store.list_runtime_transcript_entries(runtime_id))[-limit:])
if hasattr(store, "list_runtime_tool_calls"):
runtime_tool_calls.extend((await store.list_runtime_tool_calls(runtime_id))[-limit:])
if hasattr(store, "list_runtime_tool_results"):
runtime_tool_results.extend((await store.list_runtime_tool_results(runtime_id))[-limit:])
if hasattr(store, "list_runtime_permission_grants"):
runtime_permission_grants.extend((await store.list_runtime_permission_grants(runtime_session_id=runtime_id))[-limit:])
events.sort(key=lambda item: str(item.get("created_at", "")))
runtime_sessions = list(runtime_rows_by_id.values())
runtime_sessions.sort(key=lambda item: str(item.get("updated_at", "")), reverse=True)
return ServiceResult({
"project_id": project_id,
"session_id": session_id,
"work_item_id": work_item_id,
"role_id": role_id,
"work_items": [
self._work_item_payload(item, run=run, linked_task=linked_task)
for item, run, linked_task in target_items
][:limit],
"events": events[-limit:],
"runtime_sessions": runtime_sessions[:limit],
"external_sessions": external_sessions[-limit:],
"runtime_events": runtime_events[-limit:],
"runtime_transcript_entries": runtime_transcript_entries[-limit:],
"runtime_tool_calls": runtime_tool_calls[-limit:],
"runtime_tool_results": runtime_tool_results[-limit:],
"runtime_permission_grants": runtime_permission_grants[-limit:],
"transcript": transcript,
"handoffs": [self._model_payload(item) for item in handoffs],
})
async def status_by_role(self, *, project_id: str, session_id: str | None = None) -> ServiceResult:
listing = await self.list(project_id=project_id, session_id=session_id, limit=10000)
by_role: dict[str, Counter[str]] = defaultdict(Counter)
for item in listing.payload.get("work_items", []):
role_id = str(item.get("role_id") or "unassigned")
by_role[role_id][str(item.get("kanban_column") or item.get("phase") or "unknown")] += 1
return ServiceResult({
"project_id": project_id,
"session_id": str(session_id or "").strip(),
"roles": [
{"role_id": role_id, "counts": dict(counts), "total": sum(counts.values())}
for role_id, counts in sorted(by_role.items())
],
})
async def role_detail(self, *, project_id: str, role_id: str, limit: int = 100) -> ServiceResult:
role_id = str(role_id or "").strip()
if not role_id:
raise ServiceError("role_id_required", "role_id required")
listing = await self.list(project_id=project_id, role_id=role_id, limit=limit)
logs = await self.logs(project_id=project_id, role_id=role_id, limit=limit)
counts: Counter[str] = Counter()
for item in listing.payload.get("work_items", []):
counts[str(item.get("kanban_column") or item.get("phase") or "unknown")] += 1
return ServiceResult({
"project_id": project_id,
"role_id": role_id,
"counts": dict(counts),
"work_items": listing.payload.get("work_items", []),
"logs": logs.payload,
})
async def _runs(self, store: Any, project_id: str, *, session_id: str | None = None) -> list[Any]:
session_id = str(session_id or "").strip()
if hasattr(store, "list_open_delegation_runs"):
runs = await store.list_open_delegation_runs(project_id=project_id)
if session_id:
matched = [run for run in runs if self._session_id_matches(session_id, getattr(run, "session_id", ""))]
if matched:
return list(matched)
elif runs:
return list(runs)
if hasattr(store, "list_delegation_runs"):
if session_id:
for candidate_session_id in self._session_scope_candidates(session_id):
try:
runs = await store.list_delegation_runs(project_id=project_id, session_id=candidate_session_id)
except TypeError:
break
if runs:
return list(runs)
runs = list(await store.list_delegation_runs(project_id=project_id))
if session_id:
return [run for run in runs if self._session_id_matches(session_id, getattr(run, "session_id", ""))]
return runs
return []
@staticmethod
def _is_company_kanban_visible(item: Any) -> bool:
metadata = dict(getattr(item, "metadata", {}) or {})
return bool(
str(getattr(item, "parent_work_item_id", "") or "").strip()
and not bool(metadata.get("attention_work_item", False))
and not should_hide_work_item_from_company_kanban(metadata)
)
@classmethod
def _matches_session_scope(cls, session_id: str, *, run: Any = None, item: Any = None, linked_task: Any = None) -> bool:
session_id = str(session_id or "").strip()
if not session_id:
return True
for candidate in cls._session_candidates(run=run, item=item, linked_task=linked_task):
if cls._session_id_matches(session_id, candidate):
return True
return False
@staticmethod
def _session_scope_candidates(session_id: str) -> list[str]:
session_id = str(session_id or "").strip()
if not session_id:
return []
candidates = [session_id]
if ":" in session_id:
candidates.append(session_id.split(":", 1)[0])
return list(dict.fromkeys(candidates))
@classmethod
def _session_candidates(cls, *, run: Any = None, item: Any = None, linked_task: Any = None) -> list[str]:
values: list[str] = []
def add(value: Any) -> None:
text = str(value or "").strip()
if text:
values.append(text)
def add_from_mapping(mapping: Any) -> None:
if not isinstance(mapping, dict):
return
for key in (
"session_id",
"parent_session_id",
"root_session_id",
"origin_session_id",
"opc_session_id",
"company_runtime_root_session_id",
):
add(mapping.get(key))
if run is not None:
add(getattr(run, "session_id", ""))
add_from_mapping(getattr(run, "metadata", {}) or {})
add_from_mapping(getattr(run, "recovery_pointer", {}) or {})
if item is not None:
add_from_mapping(getattr(item, "metadata", {}) or {})
if linked_task is not None:
add(getattr(linked_task, "session_id", ""))
add(getattr(linked_task, "parent_session_id", ""))
add_from_mapping(getattr(linked_task, "metadata", {}) or {})
return list(dict.fromkeys(values))
@staticmethod
def _session_id_matches(scope_session_id: str, candidate_session_id: Any) -> bool:
scope = str(scope_session_id or "").strip()
candidate = str(candidate_session_id or "").strip()
if not scope or not candidate:
return False
return (
scope == candidate
or candidate.startswith(f"{scope}:")
or scope.startswith(f"{candidate}:")
)
@classmethod
def _task_runtime_session_ids(cls, task: Any) -> list[str]:
metadata = dict(getattr(task, "metadata", {}) or {})
context_snapshot = dict(getattr(task, "context_snapshot", {}) or {})
values = [
(metadata.get("runtime_v2", {}) or {}).get("runtime_session_id") if isinstance(metadata.get("runtime_v2"), dict) else "",
(context_snapshot.get("runtime_resume", {}) or {}).get("runtime_session_id") if isinstance(context_snapshot.get("runtime_resume"), dict) else "",
metadata.get("_permission_bridge_runtime_session_id"),
metadata.get("delegation_role_session_id"),
metadata.get("assigned_role_runtime_id"),
metadata.get("role_runtime_session_id"),
metadata.get("runtime_session_id"),
]
return cls._dedupe_text(values)
@classmethod
def _work_item_runtime_session_ids(cls, item: Any) -> list[str]:
metadata = dict(getattr(item, "metadata", {}) or {})
values = [
getattr(item, "role_runtime_session_id", ""),
getattr(item, "claimed_by_role_runtime_session_id", ""),
metadata.get("assigned_role_runtime_id"),
metadata.get("role_runtime_session_id"),
metadata.get("claimed_by_role_runtime_session_id"),
metadata.get("delegation_role_session_id"),
metadata.get("runtime_session_id"),
]
return cls._dedupe_text(values)
@staticmethod
def _dedupe_text(values: list[Any]) -> list[str]:
result: list[str] = []
seen: set[str] = set()
for value in values:
text = str(value or "").strip()
if not text or text in seen:
continue
seen.add(text)
result.append(text)
return result
def _work_item_payload(self, item: Any, *, run: Any = None, linked_task: Any = None) -> dict[str, Any]:
phase = coerce_phase(getattr(item, "phase", ""))
phase_value = phase.value if hasattr(phase, "value") else str(phase or "")
return {
"work_item_id": str(getattr(item, "work_item_id", "") or ""),
"run_id": str(getattr(item, "run_id", "") or ""),
"project_id": str(getattr(run, "project_id", "") or ""),
"title": str(getattr(item, "title", "") or ""),
"summary": str(getattr(item, "summary", "") or ""),
"role_id": str(getattr(item, "role_id", "") or ""),
"seat_id": str(getattr(item, "seat_id", "") or ""),
"manager_role_id": str(getattr(item, "manager_role_id", "") or ""),
"parent_work_item_id": str(getattr(item, "parent_work_item_id", "") or ""),
"role_runtime_session_id": str(getattr(item, "role_runtime_session_id", "") or ""),
"claimed_by_role_runtime_session_id": str(getattr(item, "claimed_by_role_runtime_session_id", "") or ""),
"phase": phase_value,
"kanban_column": kanban_column(phase),
"deliverable_summary": str(getattr(item, "deliverable_summary", "") or ""),
"blocked_reason": str(getattr(item, "blocked_reason", "") or ""),
"handoff_status": str(getattr(item, "handoff_status", "") or ""),
"metadata": dict(getattr(item, "metadata", {}) or {}),
"runtime_task_id": str(getattr(linked_task, "id", "") or "") if linked_task is not None else "",
"session_id": str(getattr(linked_task, "session_id", "") or "") if linked_task is not None else "",
"runtime_status": (
getattr(getattr(linked_task, "status", None), "value", getattr(linked_task, "status", ""))
if linked_task is not None else ""
),
"created_at": self._date_value(getattr(item, "created_at", None)),
"updated_at": self._date_value(getattr(item, "updated_at", None)),
}
@staticmethod
def _event_payload(event: Any) -> dict[str, Any]:
return {
"event_id": str(getattr(event, "event_id", "") or ""),
"run_id": str(getattr(event, "run_id", "") or ""),
"work_item_id": str(getattr(event, "work_item_id", "") or ""),
"cell_id": str(getattr(event, "cell_id", "") or ""),
"role_id": str(getattr(event, "role_id", "") or ""),
"event_type": str(getattr(event, "event_type", "") or ""),
"payload": dict(getattr(event, "payload", {}) or {}),
"created_at": WorkItemService._date_value(getattr(event, "created_at", None)),
}
@staticmethod
def _model_payload(value: Any) -> dict[str, Any]:
if hasattr(value, "model_dump"):
return value.model_dump()
if hasattr(value, "__dict__"):
return dict(value.__dict__)
if isinstance(value, dict):
return dict(value)
return {"value": value}
@staticmethod
def _date_value(value: Any) -> Any:
if hasattr(value, "isoformat"):
return value.isoformat()
return value