Initial commit

This commit is contained in:
LZH-YS1998
2026-07-01 17:56:31 +08:00
commit d78931979d
731 changed files with 311088 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,212 @@
"""Helpers for soft-topology collaboration and ownership-contract enforcement."""
from __future__ import annotations
from pathlib import Path
from typing import Any
from opc.core.models import Task
_COLLABORATION_TOOLS = {
"send_dm",
"broadcast_issue",
"start_meeting",
"ask_peer_and_wait",
}
_WRITE_TOOLS = {
"file_write",
"file_edit",
"apply_patch",
"shell_exec",
}
def _normalized_role_list(values: list[Any] | None) -> list[str]:
seen: set[str] = set()
result: list[str] = []
for item in values or []:
role_id = str(item or "").strip()
if not role_id or role_id in seen:
continue
seen.add(role_id)
result.append(role_id)
return result
def collect_dynamic_contact_roles(task: Task | None) -> list[str]:
if task is None:
return []
metadata = dict(getattr(task, "metadata", {}) or {})
ownership_contract = dict(metadata.get("ownership_contract", {}) or {})
work_item_gate = dict(metadata.get("work_item_gate", {}) or {})
dynamic = []
dynamic.extend(_normalized_role_list(ownership_contract.get("allowed_collaboration_targets")))
dynamic.extend(_normalized_role_list(ownership_contract.get("downstream_consumer")))
dynamic.extend(_normalized_role_list(metadata.get("dynamic_allowed_contact_roles")))
reviewer_role = str(work_item_gate.get("reviewer_role", "") or metadata.get("manager_role_id", "") or "").strip()
if reviewer_role:
dynamic.append(reviewer_role)
for handoff in list(metadata.get("handoff_log", []) or []):
if isinstance(handoff, dict):
dynamic.append(str(handoff.get("to", "")).strip())
dynamic.append(str(handoff.get("from", "")).strip())
return _normalized_role_list(dynamic)
def effective_contact_roles(
role_id: str,
*,
task: Task | None = None,
org_engine: Any | None = None,
) -> list[str]:
static_roles: list[str] = []
if org_engine is not None and hasattr(org_engine, "get_allowed_contact_roles"):
getter = getattr(org_engine, "get_allowed_contact_roles")
try:
static_roles = list(getter(role_id, task=task) or [])
except TypeError:
static_roles = list(getter(role_id) or [])
return _normalized_role_list([*static_roles, *collect_dynamic_contact_roles(task)])
def _candidate_paths(tool_name: str, arguments: dict[str, Any]) -> list[Path]:
if tool_name in {"file_write", "file_edit", "file_read", "grep", "glob", "file_search", "list_dir"}:
path = str(arguments.get("path", "") or arguments.get("directory", "") or "").strip()
return [Path(path)] if path else []
if tool_name == "apply_patch":
patch = str(arguments.get("patch", "") or "").splitlines()
paths: list[Path] = []
prefixes = ("*** Add File: ", "*** Update File: ", "*** Delete File: ")
for line in patch:
for prefix in prefixes:
if line.startswith(prefix):
raw = line[len(prefix):].strip()
if raw:
paths.append(Path(raw))
return paths
if tool_name == "shell_exec":
cwd = str(arguments.get("working_directory", "") or "").strip()
return [Path(cwd)] if cwd else []
return []
def _resolved_write_roots(task: Task | None) -> list[Path]:
if task is None:
return []
metadata = dict(getattr(task, "metadata", {}) or {})
ownership_contract = dict(metadata.get("ownership_contract", {}) or {})
scope = str(ownership_contract.get("write_scope", "") or "").strip()
roots: list[Path] = []
if scope and scope not in {"assigned_workspace", "read_only"}:
roots.append(Path(scope))
output_dir = str(metadata.get("target_output_dir", "") or "").strip()
if output_dir:
roots.append(Path(output_dir))
return [path.resolve() for path in roots if str(path).strip()]
def _path_within(candidate: Path, root: Path) -> bool:
try:
return candidate.resolve().is_relative_to(root.resolve())
except AttributeError:
resolved = str(candidate.resolve())
root_value = str(root.resolve())
return resolved == root_value or resolved.startswith(root_value.rstrip("/") + "/")
except FileNotFoundError:
resolved = candidate.expanduser().resolve(strict=False)
root_resolved = root.expanduser().resolve(strict=False)
try:
return resolved.is_relative_to(root_resolved)
except AttributeError:
resolved_value = str(resolved)
root_value = str(root_resolved)
return resolved_value == root_value or resolved_value.startswith(root_value.rstrip("/") + "/")
def ownership_guard_violation(
*,
task: Task | None,
tool_name: str,
arguments: dict[str, Any],
org_engine: Any | None = None,
) -> str | None:
if task is None:
return None
metadata = dict(getattr(task, "metadata", {}) or {})
if str(metadata.get("execution_mode", "") or "").strip() != "company_mode":
return None
ownership_contract = dict(metadata.get("ownership_contract", {}) or {})
if not ownership_contract:
return None
if tool_name in _WRITE_TOOLS:
write_scope = str(ownership_contract.get("write_scope", "") or "").strip()
if write_scope == "read_only":
return (
"Ownership contract blocks write-side effects for this work item. "
"This work item is read_only and must not modify files or run mutating shell commands."
)
roots = _resolved_write_roots(task)
candidate_paths = _candidate_paths(tool_name, arguments)
if roots and candidate_paths:
out_of_scope = [str(path) for path in candidate_paths if not any(_path_within(path, root) for root in roots)]
if out_of_scope:
return (
"Ownership contract blocks writes outside the assigned workspace. "
f"Out-of-scope target(s): {', '.join(out_of_scope[:4])}."
)
if tool_name in _COLLABORATION_TOOLS:
# TODO(role-identity): route through a central work-item role accessor.
from_role = str(getattr(task, "assigned_to", "") or metadata.get("work_item_role_id", "") or "").strip()
allowed = set(effective_contact_roles(from_role, task=task, org_engine=org_engine))
recipients: list[str] = []
if tool_name == "broadcast_issue":
recipients = _normalized_role_list(arguments.get("to_agents"))
elif tool_name == "start_meeting":
recipients = _normalized_role_list(arguments.get("participants"))
else:
recipients = _normalized_role_list([arguments.get("to_agent")])
invalid = [recipient for recipient in recipients if recipient and recipient not in allowed]
if invalid:
return (
"Ownership contract / collaboration topology blocks this contact target. "
f"Recipient(s) not allowed for the current work item: {', '.join(invalid)}."
)
return None
def render_ownership_contract(task: Task | None) -> str:
"""Render the boundary-enforcing part of the ownership contract.
Only three fields are rendered here: write scope, allowed
collaboration targets, and downstream consumers. The
``summary`` and ``expected_artifacts`` fields are intentionally
NOT rendered — ``summary`` duplicates the work-item identity's
``your_responsibility``, and ``expected_artifacts`` duplicates
the work-item identity's ``deliverables``. Both already appear in
the work-item identity block that every role receives, so repeating
them here would be redundant.
Returns "" if the task carries no contract or if none of the
boundary-enforcing fields are populated.
"""
if task is None:
return ""
ownership_contract = dict(getattr(task, "metadata", {}).get("ownership_contract", {}) or {})
if not ownership_contract:
return ""
lines: list[str] = []
write_scope = str(ownership_contract.get("write_scope", "") or "").strip()
if write_scope:
lines.append(f"Write scope: {write_scope}")
allowed_collaboration_targets = _normalized_role_list(ownership_contract.get("allowed_collaboration_targets"))
if allowed_collaboration_targets:
lines.append("Allowed collaboration targets:")
lines.extend(f"- {item}" for item in allowed_collaboration_targets[:12])
downstream = _normalized_role_list(ownership_contract.get("downstream_consumer"))
if downstream:
lines.append("Downstream consumers:")
lines.extend(f"- {item}" for item in downstream[:8])
if not lines:
return ""
return "## Ownership Contract\n" + "\n".join(lines)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,345 @@
"""Built-in company runtime profiles and helpers."""
from __future__ import annotations
from opc.core.config import (
ArtifactPolicyConfig,
CommunicationPolicyConfig,
GateHarnessPolicyConfig,
HandoffPolicyConfig,
MemoryPolicyConfig,
ReviewPolicyConfig,
RoleConfig,
RoleRuntimePolicyConfig,
RuntimePolicyConfig,
)
from opc.core.models import CompanyProfile
from opc.layer2_organization.data_acquisition_policy import ACQUISITION_SPECIALIST_ROLE_ID
_BROWSER_RESEARCH_TOOLS = [
"browser_navigate",
"browser_navigate_back",
"browser_snapshot",
"browser_wait_for",
"browser_scroll",
"browser_take_screenshot",
]
_BROWSER_EXECUTION_TOOLS = [
"browser_navigate",
"browser_navigate_back",
"browser_click",
"browser_snapshot",
"browser_type",
"browser_wait_for",
"browser_scroll",
"browser_select_option",
"browser_take_screenshot",
"browser_close",
]
_CORPORATE_COORDINATION_TOOLS = [
"file_read",
"file_search",
"list_dir",
"todo_write",
"todo_read",
]
_CORPORATE_BOOTSTRAP_TOOLS = [
*_CORPORATE_COORDINATION_TOOLS,
"shell_exec",
"file_write",
"file_edit",
]
_CORPORATE_WEB_COORDINATION_TOOLS = [
*_CORPORATE_COORDINATION_TOOLS,
"web_search",
"web_fetch",
]
_CORPORATE_EXECUTION_TOOLS = [
"shell_exec",
"file_read",
"file_write",
"file_edit",
"file_search",
"list_dir",
"web_search",
"web_fetch",
"todo_write",
"todo_read",
*_BROWSER_EXECUTION_TOOLS,
]
_CORPORATE_QA_TOOLS = [
"file_read",
"file_write",
"file_search",
"list_dir",
"shell_exec",
"browser_navigate",
"browser_navigate_back",
"browser_snapshot",
"browser_wait_for",
"browser_scroll",
"browser_select_option",
"browser_take_screenshot",
]
_CORPORATE_ENV_TOOLS = [
"shell_exec",
"file_read",
"file_write",
"file_edit",
"file_search",
"list_dir",
"web_search",
"web_fetch",
"todo_write",
"todo_read",
]
_CORPORATE_DATA_ACQUISITION_TOOLS = [
"shell_exec",
"file_read",
"file_write",
"file_search",
"list_dir",
"web_search",
"web_fetch",
"todo_write",
"todo_read",
*_BROWSER_EXECUTION_TOOLS,
]
def get_company_profile_descriptions() -> dict[str, str]:
return {
CompanyProfile.CORPORATE.value: (
"A hierarchical corporate runtime with CEO, C-suite executives (CTO/CMO/COO), and specialized workers. Execution is driven by work items and role queues."
),
CompanyProfile.CUSTOM.value: (
"A user-defined company runtime. Roles and work items drive execution."
),
}
def get_builtin_runtime_policies() -> dict[str, RuntimePolicyConfig]:
return {
CompanyProfile.CORPORATE.value: RuntimePolicyConfig(
communication=CommunicationPolicyConfig(
default_mode="dm",
blocking_default=False,
meeting_required_for=["architecture", "cross_team_conflict"],
allow_broadcast=True,
),
memory=MemoryPolicyConfig(
include_role_memory=True,
include_project_memory=False,
include_decision_log=True,
include_artifact_index=True,
recent_history_lines=12,
),
handoff=HandoffPolicyConfig(
require_structured_handoff=True,
require_ack=False,
include_risks=True,
include_open_questions=True,
),
artifact=ArtifactPolicyConfig(
enforce_contract=False,
require_artifact_index=True,
required_kinds=[],
),
review=ReviewPolicyConfig(
enable_work_item_gates=False,
strict_gate_inference=False,
require_reviewer_role=True,
allow_human_override=True,
),
gate_harness=GateHarnessPolicyConfig(
decision_mode="agent_first",
default_degrade_policy="allow",
allow_pass_with_constraints=True,
),
),
CompanyProfile.CUSTOM.value: RuntimePolicyConfig(
gate_harness=GateHarnessPolicyConfig(
decision_mode="agent_first",
default_degrade_policy="allow",
allow_pass_with_constraints=True,
),
),
}
def _apply_configured_role_overrides(
builtin_roles: list[RoleConfig],
configured_roles: list[RoleConfig] | None = None,
) -> list[RoleConfig]:
"""Overlay explicit org_config role fields onto builtin role presets."""
if not configured_roles:
return builtin_roles
configured_by_id = {role.id: role for role in configured_roles}
merged: list[RoleConfig] = []
for builtin_role in builtin_roles:
configured_role = configured_by_id.get(builtin_role.id)
if configured_role is None:
merged.append(builtin_role)
continue
update_fields = {
field_name: getattr(configured_role, field_name)
for field_name in configured_role.model_fields_set
if field_name != "id"
}
if not update_fields:
merged.append(builtin_role)
continue
merged.append(builtin_role.model_copy(update=update_fields, deep=True))
return merged
def get_builtin_roles(
profile: str,
configured_roles: list[RoleConfig] | None = None,
) -> list[RoleConfig]:
# Corporate roles (also fallback for custom and unknown profiles)
return _apply_configured_role_overrides([
RoleConfig(
id="ceo",
name="CEO",
icon="leader",
responsibility="Strategic intake, high-level routing, final aggregation and delivery to the owner.",
can_spawn=["cto", "cmo", "coo"],
tools=list(_CORPORATE_BOOTSTRAP_TOOLS),
prompt_refs=["Route tasks to the appropriate C-suite executive. Aggregate final results."],
),
RoleConfig(
id="cto",
name="CTO",
icon="code",
responsibility="Technical planning, architecture decisions, code review, and engineering oversight.",
reports_to="ceo",
can_spawn=["senior_engineer", "devops_engineer"],
tools=[*_CORPORATE_WEB_COORDINATION_TOOLS, "shell_exec"],
prompt_refs=["Focus on technical feasibility, architecture quality, and engineering best practices."],
),
RoleConfig(
id="cmo",
name="CMO",
icon="marketing",
responsibility="Marketing strategy, content planning, UX review, and brand oversight.",
reports_to="ceo",
can_spawn=["content_specialist", "designer"],
tools=[*_CORPORATE_WEB_COORDINATION_TOOLS, *_BROWSER_RESEARCH_TOOLS],
prompt_refs=["Optimize for audience fit, brand consistency, and content quality."],
),
RoleConfig(
id="coo",
name="COO",
icon="strategy",
responsibility="Operations coordination, process management, cross-team alignment, and quality assurance.",
reports_to="ceo",
can_spawn=[ACQUISITION_SPECIALIST_ROLE_ID, "qa_analyst"],
tools=[*_CORPORATE_WEB_COORDINATION_TOOLS, *_BROWSER_RESEARCH_TOOLS],
prompt_refs=["Ensure operational efficiency, process compliance, and delivery quality."],
),
RoleConfig(
id=ACQUISITION_SPECIALIST_ROLE_ID,
name="Acquisition Specialist",
icon="target",
responsibility="Discover, verify, prepare, and report task-critical external inputs inside the shared workspace.",
reports_to="coo",
preferred_external_agent="claude_code",
tools=list(_CORPORATE_DATA_ACQUISITION_TOOLS),
prompt_refs=[
"Run data acquisition in four phases: Discover, Verify, Prepare, Report.",
"For media tasks, HTML snapshots and URL lists never count as acquired binary assets.",
"Use standard CLI download tools through shell_exec instead of ad hoc inline network scripts.",
],
),
RoleConfig(
id="senior_engineer",
name="Senior Engineer",
icon="terminal",
responsibility="Code implementation, system development, and technical execution.",
reports_to="cto",
preferred_external_agent="codex",
runtime_policy=RoleRuntimePolicyConfig(execution_strategy="auto"),
tools=list(_CORPORATE_EXECUTION_TOOLS),
prompt_refs=["Write clean, tested code. Leave clear documentation for reviewers."],
),
RoleConfig(
id="devops_engineer",
name="DevOps Engineer",
icon="settings",
responsibility="Infrastructure, deployment, CI/CD, monitoring, and operational hardening.",
reports_to="cto",
preferred_external_agent="cursor",
tools=list(_CORPORATE_EXECUTION_TOOLS),
prompt_refs=["Prioritize operational safety, observability, and deployment readiness."],
),
RoleConfig(
id="content_specialist",
name="Content Specialist",
icon="writing",
responsibility="Documentation, copywriting, presentations, and user-facing writing.",
reports_to="cmo",
tools=list(_CORPORATE_EXECUTION_TOOLS),
prompt_refs=["Write clearly for the target audience. Polish deliverables."],
),
RoleConfig(
id="designer",
name="Designer",
icon="design",
responsibility="Visual design, UX artifacts, wireframes, and design system work.",
reports_to="cmo",
tools=list(_CORPORATE_EXECUTION_TOOLS),
prompt_refs=["Focus on usability, visual consistency, and design quality."],
),
RoleConfig(
id="qa_analyst",
name="QA Analyst",
icon="bug",
responsibility="Testing, security review, compliance checks, and acceptance validation.",
reports_to="coo",
tools=list(_CORPORATE_QA_TOOLS),
prompt_refs=["Test rigorously. Reject unclear or unsafe outputs."],
),
RoleConfig(
id="env_engineer",
name="Environment Engineer",
icon="database",
responsibility=(
"Probe the host environment, install required tools and dependencies, "
"prepare the assigned target_output_dir and base workspace directories, "
"configure runtime environments (conda/venv/docker/system packages), "
"and produce a verified environment manifest for downstream execution work items. "
"Supports any toolchain: video editing (FFmpeg, DaVinci), 3D engines (Unity, Unreal, Blender, Godot), "
"audio processing (FMOD, Wwise, SoX), ML/AI frameworks (PyTorch, TensorFlow), "
"game development SDKs, design tools, and any other software the task requires."
),
reports_to="cto",
skill_refs=["env_provisioning"],
runtime_policy=RoleRuntimePolicyConfig(
execution_strategy="native",
default_turn_type="setup",
shell_timeout_override=1800,
),
tools=list(_CORPORATE_ENV_TOOLS),
prompt_refs=[
"Always probe what is already installed before attempting installation.",
"Prepare the assigned target_output_dir before downstream work items run, including any missing parent directories and baseline workspace folders.",
"Produce a structured environment_manifest JSON as your final artifact.",
"Include verification commands that downstream work items can use to validate the environment.",
"Prefer system package managers (apt, brew, dnf) for system tools, pip/conda/uv for Python packages.",
"When GPU is needed, check CUDA/ROCm availability and driver versions.",
"For complex environments, create isolated envs (conda/venv) rather than polluting the host.",
],
),
], configured_roles)
+132
View File
@@ -0,0 +1,132 @@
"""Custom/org runtime entrypoint.
Custom mode runs with company-mode semantics, but with a user-selected
organization config. The runner owns the custom isolation boundary:
- company mode's engine/config objects are not mutated;
- the custom runtime uses its own OPCEngine instance and company executor;
- the runtime is rebound to the caller's store so UI session/task/transcript
state remains in the same project context as the chat that started it.
"""
from __future__ import annotations
import copy
import uuid
from typing import Any, TYPE_CHECKING
from opc.core.config import OPCConfig
from opc.core.models import UserMessage
from opc.core.org_config import (
apply_org_config_payload_to_config,
load_org_config_payload,
validate_runnable_org_config,
)
if TYPE_CHECKING:
from opc.engine import OPCEngine
class CustomRuntimeRunner:
"""Run custom/org turns through an isolated company-runtime engine."""
def __init__(self, parent: OPCEngine) -> None:
self.parent = parent
def _build_org_config(self, organization_id: str | None) -> tuple[OPCConfig, str | None]:
config_dir = self.parent.opc_home / "config"
payload, source_path = load_org_config_payload(config_dir, organization_id)
try:
base_config = OPCConfig.load(config_dir) if config_dir.exists() else copy.deepcopy(self.parent.config)
except Exception:
base_config = copy.deepcopy(self.parent.config)
loaded_config = apply_org_config_payload_to_config(
base_config,
payload,
source_path=source_path,
)
resolved_org_id = str(getattr(loaded_config.org, "organization_id", "") or organization_id or "").strip() or None
validate_runnable_org_config(loaded_config, organization_id=resolved_org_id or "")
return loaded_config, resolved_org_id
async def process_message(
self,
content: str,
*,
project_id: str | None,
session_id: str | None,
org_id: str | None,
preferred_agent: str | None,
domains: list[str] | None,
origin_task_id: str | None,
attachment_refs: list[dict[str, Any]] | None,
message_metadata: dict[str, Any] | None,
) -> str:
from opc.engine import OPCEngine
from opc.layer2_organization.phase_hooks import unregister_dispatcher_wake
org_config, resolved_org_id = self._build_org_config(org_id)
normalized_project_id = str(project_id or self.parent.project_id or "default").strip() or "default"
shared_store = getattr(self.parent, "store", None)
runtime = OPCEngine(
config=org_config,
opc_home=self.parent.opc_home,
project_id=normalized_project_id,
store=shared_store,
owns_store=shared_store is None,
run_startup_reconcile=shared_store is None,
on_progress=self.parent.on_progress,
on_runtime_event=self.parent.on_runtime_event,
on_escalation=self.parent.on_escalation,
)
runtime.on_company_runtime_children = self.parent.on_company_runtime_children
await runtime.initialize()
company_executor = getattr(runtime, "company_executor", None)
if company_executor is not None:
callback_factory = getattr(self.parent, "on_company_kanban_callback_factory", None)
if callable(callback_factory):
company_executor.on_kanban_changed = callback_factory(runtime)
else:
parent_executor = getattr(self.parent, "company_executor", None)
company_executor.on_kanban_changed = getattr(parent_executor, "on_kanban_changed", None)
try:
normalized_attachment_refs = runtime._normalize_attachment_refs(attachment_refs)
metadata = {
"mode": "company",
"exec_mode": "org",
"org_id": resolved_org_id,
"organization_id": resolved_org_id,
"organization_name": str(getattr(org_config.org, "organization_name", "") or "").strip(),
"organization_config_file": str(getattr(org_config.org, "organization_config_file", "") or "").strip(),
"preferred_agent": preferred_agent,
"domains": domains or [],
"company_profile": "custom",
"origin_task_id": origin_task_id,
"attachment_refs": normalized_attachment_refs,
}
if message_metadata:
metadata.update(dict(message_metadata))
metadata["mode"] = "company"
metadata["exec_mode"] = "org"
metadata["company_profile"] = "custom"
metadata["org_id"] = resolved_org_id
metadata["organization_id"] = resolved_org_id
message = UserMessage(
channel="cli",
user_id="owner",
content=content,
attachments=normalized_attachment_refs,
session_id=session_id or str(uuid.uuid4()),
project_context=normalized_project_id,
metadata=metadata,
)
response = await runtime.message_bus.process_single(message)
return response.content if response else "No response generated."
finally:
company_executor = getattr(runtime, "company_executor", None)
wake = getattr(company_executor, "_signal_dispatcher_wake", None)
if wake is not None:
unregister_dispatcher_wake(wake)
await runtime.shutdown()
@@ -0,0 +1,406 @@
"""Shared policy helpers for data acquisition work-item projections."""
from __future__ import annotations
import json
import re
import shlex
from pathlib import Path
from typing import Any
from opc.layer2_organization.work_item_identity import projection_id_for_task
DATA_ACQUISITION_PROJECTION_ID = "data_acquisition"
ACQUISITION_SPECIALIST_ROLE_ID = "acquisition_specialist"
ACQUISITION_SHELL_PREFIXES = {"curl", "wget", "yt-dlp", "aria2c", "ffmpeg"}
MEDIA_BINARY_ASSET_KEYWORDS = (
"video",
"trailer",
"footage",
"clip",
"素材",
"片段",
"audio",
"music",
"subtitle",
"srt",
"bilibili",
"youtube",
"mp4",
"wav",
)
DEFAULT_SOURCE_CANDIDATES_RELATIVE_PATH = "work/source_candidates.json"
DEFAULT_DOWNLOAD_MANIFEST_RELATIVE_PATH = "work/download_manifest.json"
DEFAULT_ACQUISITION_EXECUTION_RECORD_RELATIVE_PATH = "deliverables/acquisition_execution_record.md"
MEDIA_ASSET_SUBDIRS = ("trailers", "audio", "subtitles")
_SHELL_CONTROL_TOKENS = {"&&", "||", ";", "|", "&", ">", ">>", "<", "<<"}
_HTTP_URL_RE = re.compile(r"^https?://", re.IGNORECASE)
def task_projection_id(task: Any | None) -> str:
if task is None:
return ""
return projection_id_for_task(task).lower()
def task_role_id(task: Any | None) -> str:
if task is None:
return ""
metadata = getattr(task, "metadata", {}) or {}
assigned = str(getattr(task, "assigned_to", "") or "").strip().lower()
if assigned:
return assigned
# TODO(role-identity): route through a central work-item role accessor
# when assigned_to is empty (e.g. provisioning subtasks).
return str(metadata.get("work_item_role_id", "") or "").strip().lower()
def is_acquisition_specialist_projection(
*,
task: Any | None = None,
projection_id: str = "",
role_id: str = "",
) -> bool:
resolved_projection = str(projection_id or task_projection_id(task) or "").strip().lower()
resolved_role = str(role_id or task_role_id(task) or "").strip().lower()
return resolved_projection == DATA_ACQUISITION_PROJECTION_ID and resolved_role == ACQUISITION_SPECIALIST_ROLE_ID
def workspace_root_for_task(task: Any | None) -> Path | None:
if task is None:
return None
metadata = getattr(task, "metadata", {}) or {}
manifest = dict(metadata.get("workspace_manifest", {}) or {})
root = str(manifest.get("root_path", "") or metadata.get("target_output_dir", "") or "").strip()
if not root:
return None
try:
return Path(root).resolve()
except Exception:
return None
def reserved_path_for_task(task: Any | None, key: str) -> Path | None:
if task is None:
return None
manifest = dict(getattr(task, "metadata", {}).get("workspace_manifest", {}) or {})
reserved = dict(manifest.get("reserved_paths", {}) or {})
raw = str(reserved.get(key, "") or "").strip()
if not raw:
return None
try:
return Path(raw).resolve()
except Exception:
return None
def default_source_candidates_path(task: Any | None) -> str:
work_dir = reserved_path_for_task(task, "work")
if work_dir is not None:
return str((work_dir / "source_candidates.json").resolve())
root = workspace_root_for_task(task)
if root is not None:
return str((root / DEFAULT_SOURCE_CANDIDATES_RELATIVE_PATH).resolve())
return DEFAULT_SOURCE_CANDIDATES_RELATIVE_PATH
def default_download_manifest_path(task: Any | None) -> str:
work_dir = reserved_path_for_task(task, "work")
if work_dir is not None:
return str((work_dir / "download_manifest.json").resolve())
root = workspace_root_for_task(task)
if root is not None:
return str((root / DEFAULT_DOWNLOAD_MANIFEST_RELATIVE_PATH).resolve())
return DEFAULT_DOWNLOAD_MANIFEST_RELATIVE_PATH
def default_execution_record_path(task: Any | None) -> str:
deliverables_dir = reserved_path_for_task(task, "deliverables")
if deliverables_dir is not None:
return str((deliverables_dir / "acquisition_execution_record.md").resolve())
root = workspace_root_for_task(task)
if root is not None:
return str((root / DEFAULT_ACQUISITION_EXECUTION_RECORD_RELATIVE_PATH).resolve())
return DEFAULT_ACQUISITION_EXECUTION_RECORD_RELATIVE_PATH
def _normalize_item_text(value: Any) -> str:
if isinstance(value, dict):
try:
return json.dumps(value, ensure_ascii=False, sort_keys=True)
except TypeError:
return str(value).strip()
if isinstance(value, (list, tuple, set)):
return " ".join(_normalize_item_text(item) for item in value if _normalize_item_text(item))
return str(value or "").strip()
def requires_binary_asset_acquisition(task: Any | None, report: dict[str, Any] | None = None) -> bool:
texts: list[str] = []
if task is not None:
metadata = getattr(task, "metadata", {}) or {}
texts.extend([
str(getattr(task, "title", "") or ""),
str(getattr(task, "description", "") or ""),
str(metadata.get("original_message", "") or ""),
])
report_payload = dict(report or {})
for key in ("required_inputs", "present_inputs", "missing_inputs", "attempted_sources", "notes", "blocked_reasons"):
raw = report_payload.get(key, [])
if isinstance(raw, list):
texts.extend(_normalize_item_text(item) for item in raw)
elif raw not in (None, "", [], {}):
texts.append(_normalize_item_text(raw))
combined = "\n".join(text.lower() for text in texts if text).strip()
if not combined:
return False
return any(keyword in combined for keyword in MEDIA_BINARY_ASSET_KEYWORDS)
def load_json_file(path: str) -> Any:
raw = str(path or "").strip()
if not raw:
return None
try:
return json.loads(Path(raw).read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return None
def _manifest_entries(value: Any) -> list[dict[str, Any]]:
if isinstance(value, list):
return [dict(item) for item in value if isinstance(item, dict)]
if isinstance(value, dict):
for key in ("entries", "items", "downloads", "download_manifest"):
nested = value.get(key)
if isinstance(nested, list):
return [dict(item) for item in nested if isinstance(item, dict)]
return []
def load_download_manifest_entries(path: str) -> list[dict[str, Any]]:
return _manifest_entries(load_json_file(path))
def _path_within(candidate: Path, root: Path) -> bool:
try:
candidate.relative_to(root)
return True
except ValueError:
return False
def has_downloaded_binary_asset(
*,
task: Any | None,
report: dict[str, Any],
download_manifest_path: str,
designated_input_dir: str,
) -> bool:
manifest_entries = load_download_manifest_entries(download_manifest_path)
if not manifest_entries:
return False
input_root = None
if designated_input_dir:
try:
input_root = Path(designated_input_dir).resolve()
except Exception:
input_root = None
for entry in manifest_entries:
status = str(entry.get("status", "") or "").strip().lower()
if status != "downloaded":
continue
local_path = str(entry.get("local_path", "") or "").strip()
if not local_path:
continue
try:
candidate = Path(local_path).resolve()
except Exception:
continue
media_kind = str(entry.get("media_kind", "") or "").strip().lower()
if input_root is not None and _path_within(candidate, input_root):
relative_parts = candidate.relative_to(input_root).parts
if relative_parts and relative_parts[0] in MEDIA_ASSET_SUBDIRS:
return True
if media_kind in {"video", "audio", "subtitle"}:
return True
elif workspace_root_for_task(task) is not None and _path_within(candidate, workspace_root_for_task(task) or Path(".")):
if media_kind in {"video", "audio", "subtitle"}:
return True
return False
def split_shell_command_segments(command: str) -> list[list[str]]:
text = str(command or "").replace("\r\n", "\n").replace("\n", " ; ").strip()
if not text:
return []
try:
lexer = shlex.shlex(text, posix=True, punctuation_chars=";&|<>")
lexer.whitespace_split = True
lexer.commenters = ""
tokens = list(lexer)
except ValueError:
try:
tokens = shlex.split(text)
except ValueError:
tokens = text.split()
segments: list[list[str]] = []
current: list[str] = []
for token in tokens:
if token in _SHELL_CONTROL_TOKENS:
if current:
segments.append(current)
current = []
continue
current.append(token)
if current:
segments.append(current)
return segments
def _resolve_output_path(token: str, *, working_directory: str, target_output_dir: str) -> Path | None:
raw = str(token or "").strip()
if not raw or raw == "-":
return None
try:
candidate = Path(raw)
if candidate.is_absolute():
return candidate.resolve()
base = Path(working_directory or target_output_dir or ".").resolve()
return (base / candidate).resolve()
except Exception:
return None
def _extract_output_targets(
prefix: str,
tokens: list[str],
*,
working_directory: str,
target_output_dir: str,
) -> list[Path]:
lower_tokens = [token.lower() for token in tokens]
targets: list[Path] = []
def _append(token: str) -> None:
resolved = _resolve_output_path(
token,
working_directory=working_directory,
target_output_dir=target_output_dir,
)
if resolved is not None:
targets.append(resolved)
if prefix == "curl":
for index, token in enumerate(lower_tokens[:-1]):
if token in {"-o", "--output"}:
_append(tokens[index + 1])
if any(token in {"-o", "--output", "-O", "--remote-name"} for token in lower_tokens) and not targets:
_append(working_directory or target_output_dir)
return targets
if prefix == "wget":
directory_prefix = ""
file_name = ""
for index, token in enumerate(lower_tokens[:-1]):
if token in {"-O", "--output-document"}:
file_name = tokens[index + 1]
elif token in {"-P", "--directory-prefix"}:
directory_prefix = tokens[index + 1]
if file_name:
_append(str(Path(directory_prefix) / file_name) if directory_prefix else file_name)
elif directory_prefix:
_append(directory_prefix)
else:
_append(working_directory or target_output_dir)
return targets
if prefix == "yt-dlp":
directory_prefix = ""
output_template = ""
for index, token in enumerate(lower_tokens[:-1]):
if token in {"-o", "--output"}:
output_template = tokens[index + 1]
elif token in {"-P", "--paths"}:
directory_prefix = tokens[index + 1]
if output_template:
_append(str(Path(directory_prefix) / output_template) if directory_prefix else output_template)
elif directory_prefix:
_append(directory_prefix)
else:
_append(working_directory or target_output_dir)
return targets
if prefix == "aria2c":
directory_prefix = ""
file_name = ""
for index, token in enumerate(lower_tokens[:-1]):
if token in {"-d", "--dir"}:
directory_prefix = tokens[index + 1]
elif token in {"-o", "--out"}:
file_name = tokens[index + 1]
if file_name:
_append(str(Path(directory_prefix) / file_name) if directory_prefix else file_name)
elif directory_prefix:
_append(directory_prefix)
else:
_append(working_directory or target_output_dir)
return targets
if prefix == "ffmpeg":
for token in reversed(tokens[1:]):
if token.startswith("-"):
continue
if _HTTP_URL_RE.match(token):
continue
_append(token)
break
return targets
return targets
def is_projection_scoped_acquisition_shell_command(
*,
command: str,
task: Any | None = None,
projection_id: str = "",
role_id: str = "",
working_directory: str = "",
target_output_dir: str = "",
) -> bool:
if not is_acquisition_specialist_projection(task=task, projection_id=projection_id, role_id=role_id):
return False
root = str(target_output_dir or (workspace_root_for_task(task) or "")).strip()
if not root:
return False
try:
workspace_root = Path(root).resolve()
except Exception:
return False
segments = split_shell_command_segments(command)
if len(segments) != 1:
return False
tokens = segments[0]
if not tokens:
return False
prefix = str(tokens[0] or "").strip().lower()
if prefix not in ACQUISITION_SHELL_PREFIXES:
return False
urls = [token for token in tokens if "://" in token]
if urls and any(not _HTTP_URL_RE.match(url) for url in urls):
return False
targets = _extract_output_targets(
prefix,
tokens,
working_directory=working_directory,
target_output_dir=str(workspace_root),
)
if not targets:
try:
cwd = Path(working_directory or workspace_root).resolve()
except Exception:
return False
return _path_within(cwd, workspace_root)
return all(_path_within(target, workspace_root) for target in targets)
+130
View File
@@ -0,0 +1,130 @@
"""Escalation engine — handles human-in-the-loop decision points."""
from __future__ import annotations
import asyncio
import uuid
from typing import Any, Callable, Coroutine, Optional
from loguru import logger
from opc.core.models import EscalationType, OPCEvent, Task
from opc.core.events import EventBus
UserReplyCallback = Callable[[str, list[dict]], Coroutine[Any, Any, Optional[str]]]
class EscalationEngine:
"""Manages escalation to the human owner for decisions, info, and risk warnings."""
def __init__(
self,
event_bus: EventBus,
timeout_seconds: int = 300,
user_reply_callback: UserReplyCallback | None = None,
) -> None:
self.event_bus = event_bus
self.timeout_seconds = timeout_seconds
self.user_reply_callback = user_reply_callback
self._pending: dict[str, asyncio.Event] = {}
self._replies: dict[str, str] = {}
async def escalate(
self,
task: Task,
escalation_type: EscalationType,
message: str,
options: list[dict[str, str]] | None = None,
default_action: str | None = None,
) -> str | None:
"""Escalate to the user and wait for a reply.
Returns the user's reply or the default action on timeout.
"""
# Use a unique escalation id per prompt so repeated approvals for the
# same task do not alias to older UI cards or stale pending state.
escalation_id = f"esc_{task.id}_{uuid.uuid4().hex}"
await self.event_bus.publish(OPCEvent(
event_type="escalation_created",
payload={
"escalation_id": escalation_id,
"task_id": task.id,
"type": escalation_type.value,
"message": message,
"options": options or [],
"default_action": default_action,
},
))
logger.info(f"Escalation [{escalation_type.value}] for task {task.id}: {message}")
if self.user_reply_callback:
try:
reply = await asyncio.wait_for(
self.user_reply_callback(message, options or []),
timeout=self.timeout_seconds,
)
if reply is not None:
await self.event_bus.publish(OPCEvent(
event_type="escalation_resolved",
payload={"escalation_id": escalation_id, "reply": reply},
))
return reply
except asyncio.TimeoutError:
logger.warning(f"Escalation {escalation_id} timed out, using default: {default_action}")
await self.event_bus.publish(OPCEvent(
event_type="escalation_timeout",
payload={"escalation_id": escalation_id, "default_action": default_action},
))
return default_action
except Exception as e:
logger.error(f"Escalation callback error: {e}")
return default_action
async def escalate_info_needed(self, task: Task, info_description: str) -> str | None:
return await self.escalate(
task=task,
escalation_type=EscalationType.INFO_NEEDED,
message=f"[INFO NEEDED] Task: {task.title}\nMissing: {info_description}\nPlease provide to continue.",
)
async def escalate_decision(
self,
task: Task,
question: str,
options: list[dict[str, str]],
default_action: str | None = None,
) -> str | None:
metadata = dict(getattr(task, "metadata", {}) or {})
execution_mode = str(metadata.get("execution_mode", "") or "").strip()
mode = str(metadata.get("mode", "") or "").strip()
runtime_kind = str(metadata.get("runtime_kind", "") or "").strip()
is_task_mode = (
execution_mode == "task_mode"
or mode == "task"
or runtime_kind == "task_mode_agent_turn"
)
task_label = (
str(metadata.get("original_message") or getattr(task, "description", "") or task.title).strip()
if is_task_mode
else task.title
)
return await self.escalate(
task=task,
escalation_type=EscalationType.DECISION_NEEDED,
message=f"[DECISION NEEDED] Task: {task_label}\n{question}",
options=options,
default_action=default_action,
)
async def escalate_risk(self, task: Task, risk_description: str) -> str | None:
return await self.escalate(
task=task,
escalation_type=EscalationType.RISK_WARNING,
message=f"[RISK WARNING] {risk_description}",
options=[{"id": "proceed", "label": "Proceed"}, {"id": "abort", "label": "Abort"}],
default_action="abort",
)
+437
View File
@@ -0,0 +1,437 @@
"""LLM-judged gate harness for company-mode work items.
This module used to encode ~700 lines of hard-coded "if blocker_type ==
X then action = Y" rules. That approach kept misclassifying real
deliveries (e.g. reading `ready_for_final_release: false` as a hard
release blocker even when the same report explicitly said
`ready_for_downstream_execution: true`) and could not generalize across
the variety of user tasks the runtime is supposed to handle.
The current design is intentionally minimal:
1. Build a *descriptive* evidence packet for the completed work item. The
packet only describes what happened — it makes no policy decisions.
It contains: the work item's original requirements, the upstream context
the agent saw, the agent's actual output (result + summary + artifact
index + work_item_summary_for_downstream), the agent's own self-reported risks and
blockers, and a small number of cheap objective signals (task status,
dependency health, prior rework history).
2. Hand the packet to an LLM judge with a tightly scoped prompt. The
judge compares "what was asked" vs "what was produced" and returns
one of exactly three actions: `pass`, `rework_same_work_item`, or
`escalate`. The reason text is fed back to the original agent
session via `gate_harness_rework_feedback` so the same agent can fix
it on the next turn.
3. Apply a single safety net: a stagnation cap. If the same blocker
fingerprint has caused N reworks in a row without converging,
upgrade to `escalate` so the user gets a chance to break the loop.
This is the only deterministic decision the harness still makes.
If the LLM is unavailable, evaluate() returns `pass` with a logged
warning rather than blocking — the runtime is designed to trust agent
self-reports when there is no second opinion available.
"""
from __future__ import annotations
import hashlib
import json
import logging
from dataclasses import dataclass, field
from typing import Any, Awaitable, Callable
from opc.core.config import GateHarnessPolicyConfig
from opc.core.models import Task
from opc.layer2_organization.work_item_identity import projection_id_for_task
from opc.llm.retry import LLMRetryError, call_llm_json_with_retry
logger = logging.getLogger(__name__)
# Callback signature for spawning a fresh judge session.
#
# The runner is expected to:
# 1. Inspect `source_task.assigned_external_agent` (or fall back to a
# native model) to pick the SAME agent type that produced the
# work item being judged. The point is consistency — codex-produced
# work is judged by codex, claude_code-produced work by
# claude_code, native-produced work by the native LLM.
# 2. Open a NEW session for the judge (not a resume of the source
# agent's session) so the judge starts with a clean context and
# cannot be biased by the agent's prior reasoning trace.
# 3. Send `system_prompt` as the system message and the JSON-encoded
# packet as the user message.
# 4. Return the raw text the judge produced.
#
# gate_harness then parses that text as JSON. If the runner returns
# anything that doesn't parse, the harness falls back to `pass` with a
# logged warning rather than blocking the runtime.
JudgeRunner = Callable[
["GateEvidencePacket", str, Task],
Awaitable[str],
]
GATE_HARNESS_AGENT_PROMPT = """\
You are the gate-harness judge for one completed work item of an AI company runtime.
You receive an evidence packet describing what the work item was asked
to produce, what context the agent had, and what the agent actually
produced. Decide whether the output meets the work item's requirements.
Return STRICT JSON with exactly these two fields and no extras:
{
"action": "pass" | "rework_same_work_item" | "escalate",
"reason": "<your explanation; if action is rework_same_work_item, include the concrete actionable steps the agent should take>"
}
Action semantics:
- `pass` — the deliverables meet the stated requirements.
- `rework_same_work_item` — the same agent can fix the gap; your `reason`
must spell out what is wrong and what to do about it.
- `escalate` — the gap needs a human decision, OR the same problem
has already caused multiple unsuccessful reworks.
Return JSON only — no markdown fences, no commentary.
"""
@dataclass
class GateEvidencePacket:
"""Minimal snapshot fed to the LLM judge: 5 fields only.
- requirements: what the work item was asked to produce (task.title +
task.description — the "task brief" that was given to the agent)
- output: what the agent actually produced (task.result raw stdout)
- prior_rework_count / prior_rework_feedback: history of previous
gate judge rework cycles for this work item
"""
projection_id: str
requirements: str
output: str
prior_rework_count: int = 0
prior_rework_feedback: str = ""
def to_dict(self) -> dict[str, Any]:
return {
"projection_id": self.projection_id,
"requirements": self.requirements,
"output": self.output,
"prior_rework_count": self.prior_rework_count,
"prior_rework_feedback": self.prior_rework_feedback,
}
@dataclass
class GateHarnessDecision:
"""Projection-only decision returned to company_mode."""
action: str
summary: str
target_projection_id: str = ""
target_projection_ids: list[str] = field(default_factory=list)
notes: list[str] = field(default_factory=list)
constraints: list[str] = field(default_factory=list)
blockers: list[str] = field(default_factory=list)
blocker_types: list[str] = field(default_factory=list)
residual_risks: list[str] = field(default_factory=list)
source: str = "llm_judge"
blocker_fingerprint: str = ""
def __post_init__(self) -> None:
projection_id = str(self.target_projection_id or "").strip()
projection_ids = [
str(item).strip()
for item in list(self.target_projection_ids or [])
if str(item).strip()
]
if not projection_ids and projection_id:
projection_ids = [projection_id]
self.target_projection_id = projection_id or (projection_ids[0] if projection_ids else "")
self.target_projection_ids = projection_ids
def to_dict(self) -> dict[str, Any]:
return {
"action": self.action,
"summary": self.summary,
"target_projection_id": self.target_projection_id,
"target_projection_ids": list(self.target_projection_ids),
"notes": list(self.notes),
"constraints": list(self.constraints),
"blockers": list(self.blockers),
"blocker_types": list(self.blocker_types),
"residual_risks": list(self.residual_risks),
"source": self.source,
"blocker_fingerprint": self.blocker_fingerprint,
}
class GateHarness:
"""Evaluates one completed work item using an LLM judge."""
ALLOWED_ACTIONS = ("pass", "rework_same_work_item", "escalate")
def __init__(
self,
*,
policy: GateHarnessPolicyConfig | dict[str, Any] | None = None,
llm: Any | None = None,
org_engine: Any | None = None,
judge_runner: JudgeRunner | None = None,
) -> None:
if isinstance(policy, GateHarnessPolicyConfig):
self.policy = policy
else:
self.policy = GateHarnessPolicyConfig.model_validate(dict(policy or {}))
self.llm = llm
self.org_engine = org_engine
# Preferred path: company_mode wires a runner that spawns a
# fresh session of the SAME external agent type that produced
# the work item being judged (codex → codex, claude_code →
# claude_code, native → native llm). When `judge_runner` is
# set, `_invoke_judge` uses it. Otherwise we fall back to
# `self.llm.simple_chat` so the harness still works in tests
# and in setups that have not yet wired the per-agent dispatch.
self.judge_runner = judge_runner
# ------------------------------------------------------------------
# Public entrypoint
# ------------------------------------------------------------------
async def evaluate(
self,
task: Task,
task_by_projection_id: dict[str, Task],
) -> tuple[GateEvidencePacket, GateHarnessDecision]:
packet = self.build_evidence_packet(task, task_by_projection_id)
decision = await self._invoke_judge(packet, source_task=task)
decision = self._apply_stagnation_cap(task, decision)
return packet, decision
# ------------------------------------------------------------------
# Evidence packet construction (Class B: descriptive only)
# ------------------------------------------------------------------
def build_evidence_packet(
self,
task: Task,
task_by_projection_id: dict[str, Task],
) -> GateEvidencePacket:
metadata = dict(task.metadata or {})
projection_id = projection_id_for_task(task)
# Requirements = the task brief the agent received.
requirements = f"{task.title or ''}\n\n{task.description or ''}".strip()
# Output = agent's raw stdout result.
if isinstance(task.result, dict):
output = str(task.result.get("content", "") or "")
elif task.result is not None:
output = str(task.result)
else:
output = ""
return GateEvidencePacket(
projection_id=projection_id,
requirements=requirements,
output=output,
prior_rework_count=int(metadata.get("gate_harness_rework_count", 0) or 0),
prior_rework_feedback=str(metadata.get("gate_harness_rework_feedback", "") or "").strip(),
)
# ------------------------------------------------------------------
# LLM judge
# ------------------------------------------------------------------
async def _invoke_judge(
self,
packet: GateEvidencePacket,
*,
source_task: Task,
) -> GateHarnessDecision:
# Preferred: company_mode-supplied runner that spawns a fresh
# session of the SAME external agent type that produced this
# work item (codex → codex, etc.). Fallback: native simple_chat.
# Strategy: the judge_runner path spawns a fresh agent session to
# produce the JSON. Per system design, errors inside a running
# agent are handled by the agent itself, so we keep that path
# single-shot. Only the simple_chat fallback is wrapped in the
# retry helper, because there is no agent to self-correct.
if self.judge_runner is not None:
try:
raw = await self.judge_runner(packet, GATE_HARNESS_AGENT_PROMPT, source_task)
except Exception as exc: # pragma: no cover - runner transport errors
logger.warning(
"[gate_harness] Judge runner failed for work item `%s`: %s; defaulting to pass.",
packet.projection_id,
exc,
)
return self._fallback_pass(packet, reason=f"Judge runner failed: {exc}")
try:
data = json.loads(self._strip_markdown_fences(raw))
except Exception:
logger.warning(
"[gate_harness] Judge runner returned non-JSON for work item `%s`; defaulting to pass. Raw=%r",
packet.projection_id,
str(raw)[:200],
)
return self._fallback_pass(packet, reason="Judge runner returned non-JSON output.")
if not isinstance(data, dict):
return self._fallback_pass(packet, reason="Judge runner returned non-object output.")
action = str(data.get("action", "") or "").strip()
if action not in self.ALLOWED_ACTIONS:
logger.warning(
"[gate_harness] Judge runner returned unknown action `%s` for work item `%s`; defaulting to pass.",
action,
packet.projection_id,
)
return self._fallback_pass(packet, reason=f"Judge runner returned unrecognized action `{action}`.")
elif self.llm is not None:
allowed_actions = self.ALLOWED_ACTIONS
def _validate_judge_response(parsed: Any) -> str | None:
if not isinstance(parsed, dict):
return "Top-level response must be a JSON object."
act = str(parsed.get("action", "") or "").strip()
if act not in allowed_actions:
return (
f"Unknown action `{act}`. Choose one of: "
f"{', '.join(sorted(allowed_actions))}."
)
return None
try:
data = await call_llm_json_with_retry(
self.llm,
system=GATE_HARNESS_AGENT_PROMPT,
payload=packet.to_dict(),
task_type="quick_tasks",
validator=_validate_judge_response,
label=f"gate_harness:{packet.projection_id}",
)
except LLMRetryError as exc:
logger.warning(
"[gate_harness] simple_chat judge fallback failed for work item `%s` after retries: %s; defaulting to pass.",
packet.projection_id,
exc.last_error,
)
return self._fallback_pass(
packet,
reason=f"LLM judge failed after retries: {exc.last_error}",
)
action = str(data.get("action", "") or "").strip()
else:
logger.warning(
"[gate_harness] No judge_runner and no llm available for work item `%s`; defaulting to pass.",
packet.projection_id,
)
return self._fallback_pass(packet, reason="Judge unavailable; trusting agent self-report.")
reason = str(data.get("reason", "") or "").strip() or "(no reason provided)"
# `reason` is the canonical text fed back to the agent's session
# via `gate_harness_rework_feedback` on the next turn. The judge
# is instructed to put any "what to fix and how" content directly
# in `reason` when the action is rework_same_work_item.
return GateHarnessDecision(
action=action,
summary=reason,
target_projection_id=packet.projection_id if action == "rework_same_work_item" else "",
target_projection_ids=[packet.projection_id] if action == "rework_same_work_item" else [],
source="llm_judge",
blocker_fingerprint=self._fingerprint(action, reason),
)
def _fallback_pass(self, packet: GateEvidencePacket, *, reason: str) -> GateHarnessDecision:
return GateHarnessDecision(
action="pass",
summary=reason,
source="llm_judge_fallback",
)
# ------------------------------------------------------------------
# Stagnation cap (Class C: the only deterministic decision left)
# ------------------------------------------------------------------
def _apply_stagnation_cap(
self,
task: Task,
decision: GateHarnessDecision,
) -> GateHarnessDecision:
"""Escalate only when the SAME problem keeps repeating.
Total rework count is irrelevant — a work item that gets reworked
10 times for 10 different reasons is healthy iteration (e.g.
user keeps asking for changes). What indicates stagnation is
N consecutive reworks whose fingerprints match: the judge
keeps saying the same thing, meaning the agent is unable to
fix the problem.
We count consecutive trailing rework entries in history that
share the current decision's fingerprint. If that streak
reaches the threshold, we escalate.
"""
if decision.action != "rework_same_work_item":
return decision
threshold = max(2, int(getattr(self.policy, "stagnation_threshold", 3) or 3))
current_fp = decision.blocker_fingerprint
if not current_fp:
return decision
history = [
dict(item)
for item in list(task.metadata.get("gate_harness_history", []) or [])
if isinstance(item, dict)
]
# Count consecutive trailing reworks with the same fingerprint.
consecutive = 0
for item in reversed(history):
if (
str(item.get("action", "") or "") == "rework_same_work_item"
and str(item.get("blocker_fingerprint", "") or "") == current_fp
):
consecutive += 1
else:
break
if consecutive + 1 < threshold:
return decision
upgraded_summary = (
f"{decision.summary}\n\n"
f"[stagnation] The same problem has been flagged {consecutive + 1} "
f"consecutive times without progress. Escalating to user."
)
return GateHarnessDecision(
action="escalate",
summary=upgraded_summary,
target_projection_id=decision.target_projection_id,
target_projection_ids=list(decision.target_projection_ids),
source=f"{decision.source}+stagnation_cap",
blocker_fingerprint=decision.blocker_fingerprint,
)
# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
@staticmethod
def _fingerprint(action: str, reason: str) -> str:
if not action and not reason:
return ""
joined = f"{action}||{reason.strip()[:240]}"
return hashlib.sha1(joined.encode("utf-8", errors="replace")).hexdigest()[:16]
@staticmethod
def _strip_markdown_fences(text: str) -> str:
value = str(text or "").strip()
if value.startswith("```"):
value = value.split("\n", 1)[1] if "\n" in value else value[3:]
if value.endswith("```"):
value = value[:-3]
return value.strip()
+102
View File
@@ -0,0 +1,102 @@
"""Goal hierarchy manager for persistent company-mode organizations."""
from __future__ import annotations
from typing import Any
from loguru import logger
from opc.core.models import Goal, GoalLevel, GoalStatus
from opc.database.store import OPCStore
class GoalManager:
"""Manages hierarchical goals within an organization."""
def __init__(self, store: OPCStore) -> None:
self.store = store
async def create_goal(
self,
org_id: str,
title: str,
description: str = "",
level: GoalLevel = GoalLevel.TASK,
parent_id: str | None = None,
owner_agent_id: str | None = None,
priority: int = 5,
metadata: dict[str, Any] | None = None,
) -> Goal:
goal = Goal(
org_id=org_id,
parent_id=parent_id,
owner_agent_id=owner_agent_id,
level=level,
title=title,
description=description,
priority=priority,
metadata=dict(metadata or {}),
)
await self.store.save_goal(goal)
logger.info("Created goal {} ({}) in org {}", goal.goal_id, title, org_id)
return goal
async def get_goal(self, goal_id: str) -> Goal | None:
return await self.store.get_goal(goal_id)
async def get_goal_tree(self, org_id: str) -> list[dict[str, Any]]:
"""Return hierarchical goal tree for an organization."""
all_goals = await self.store.get_goal_tree(org_id)
by_parent: dict[str | None, list[Goal]] = {}
for goal in all_goals:
by_parent.setdefault(goal.parent_id, []).append(goal)
def _build(parent_id: str | None) -> list[dict[str, Any]]:
children = by_parent.get(parent_id, [])
return [
{
"goal": goal,
"children": _build(goal.goal_id),
}
for goal in children
]
return _build(None)
async def get_goal_chain(self, goal_id: str) -> list[Goal]:
"""Walk from a goal up to the root, returning [leaf, ..., root]."""
chain: list[Goal] = []
current_id: str | None = goal_id
seen: set[str] = set()
while current_id and current_id not in seen:
seen.add(current_id)
goal = await self.store.get_goal(current_id)
if not goal:
break
chain.append(goal)
current_id = goal.parent_id
return chain
async def link_task_to_goal(self, task_id: str, goal_id: str) -> None:
"""Set a task's goal_id field."""
task = await self.store.get_task(task_id)
if task:
task.goal_id = goal_id
await self.store.save_task(task)
logger.debug("Linked task {} to goal {}", task_id, goal_id)
async def update_goal_status(self, goal_id: str, status: GoalStatus) -> None:
goal = await self.store.get_goal(goal_id)
if goal:
goal.status = status
await self.store.save_goal(goal)
logger.info("Goal {} status -> {}", goal_id, status.value)
async def list_root_goals(self, org_id: str) -> list[Goal]:
return await self.store.list_goals(org_id, parent_id=None)
async def list_children(self, goal_id: str) -> list[Goal]:
goal = await self.store.get_goal(goal_id)
if not goal:
return []
return await self.store.list_goals(goal.org_id, parent_id=goal_id)
+185
View File
@@ -0,0 +1,185 @@
"""Heartbeat scheduler for company-mode agent autonomy.
Periodically checks heartbeat-enabled agents and wakes them to process
pending tasks. Runs as a background ``asyncio.Task`` within the same
process — no separate service needed.
"""
from __future__ import annotations
import asyncio
import uuid
from datetime import datetime, timedelta
from typing import Any, Callable, Coroutine
from loguru import logger
class HeartbeatScheduler:
"""In-process heartbeat loop that periodically activates company-mode agents."""
def __init__(
self,
store: Any,
org_engine: Any,
execute_task_fn: Callable[..., Coroutine[Any, Any, Any]],
checkout_and_run_fn: Callable[..., Coroutine[Any, Any, Any]] | None = None,
interval_sec: int = 30,
max_concurrent_runs: int = 1,
communication: Any | None = None,
) -> None:
self.store = store
self.org_engine = org_engine
self.execute_task_fn = execute_task_fn
self.checkout_and_run_fn = checkout_and_run_fn
self.interval_sec = interval_sec
self.max_concurrent_runs = max_concurrent_runs
self.communication = communication
self._running = False
self._task: asyncio.Task[None] | None = None
self._active_runs: dict[str, asyncio.Task[Any]] = {}
self._wakeup_event = asyncio.Event()
# -- lifecycle ---------------------------------------------------------
async def start(self) -> None:
if self._running:
return
self._running = True
self._task = asyncio.create_task(self._tick_loop())
logger.info("HeartbeatScheduler started (interval={}s)", self.interval_sec)
async def stop(self) -> None:
self._running = False
self._wakeup_event.set()
if self._task:
self._task.cancel()
try:
await self._task
except asyncio.CancelledError:
pass
self._task = None
for task in list(self._active_runs.values()):
task.cancel()
self._active_runs.clear()
logger.info("HeartbeatScheduler stopped")
# -- on-demand wakeup --------------------------------------------------
async def wakeup(self, agent_id: str, reason: str = "on_demand") -> None:
"""Immediately wake a specific agent outside the normal tick cycle."""
logger.info("Wakeup requested for agent={} reason={}", agent_id, reason)
if agent_id in self._active_runs and not self._active_runs[agent_id].done():
logger.debug("Agent {} already has an active run, skipping wakeup", agent_id)
return
self._active_runs[agent_id] = asyncio.create_task(
self._run_agent_heartbeat(agent_id)
)
# -- main loop ---------------------------------------------------------
async def _tick_loop(self) -> None:
while self._running:
try:
await self._tick()
except asyncio.CancelledError:
break
except Exception:
logger.exception("HeartbeatScheduler tick error")
try:
await asyncio.wait_for(
self._wakeup_event.wait(),
timeout=self.interval_sec,
)
self._wakeup_event.clear()
except asyncio.TimeoutError:
pass
async def _resolve_stale_waits(self) -> None:
"""Resolve stale peer waits and auto-simulate meetings when all work is stalled."""
if not self.communication:
return
from opc.core.models import TaskStatus
try:
waiting_tasks = await self.store.get_tasks(status=TaskStatus.AWAITING_PEER)
if not waiting_tasks:
return
resumed = await self.communication.refresh_waiting_tasks(waiting_tasks)
for task in resumed:
logger.info("Heartbeat resolved peer wait for task={}", task.id)
still_waiting = [t for t in waiting_tasks if t.status == TaskStatus.AWAITING_PEER]
if not still_waiting:
return
project_ids = {t.project_id for t in still_waiting}
for pid in project_ids:
all_project_tasks = await self.store.get_tasks(project_id=pid)
has_runnable = any(
t.status in {TaskStatus.PENDING, TaskStatus.RUNNING}
for t in all_project_tasks
)
if has_runnable:
continue
project_waiting = [t for t in still_waiting if t.project_id == pid]
resolved = await self.communication.auto_resolve_stale_meetings(project_waiting)
for room_id in resolved:
logger.info("Heartbeat auto-simulated meeting={} (project {} fully stalled)", room_id, pid)
except Exception:
logger.exception("Heartbeat _resolve_stale_waits error")
async def _tick(self) -> None:
self._cleanup_done_runs()
await self._resolve_stale_waits()
agents = self.org_engine.list_agents()
now = datetime.now()
for agent in agents:
if not getattr(agent, "heartbeat_enabled", False):
continue
if agent.role_id in self._active_runs and not self._active_runs[agent.role_id].done():
continue
if len(self._active_runs) >= self.max_concurrent_runs:
break
interval = getattr(agent, "heartbeat_interval_sec", 300)
last_hb = getattr(agent, "last_heartbeat_at", None)
if last_hb and (now - last_hb) < timedelta(seconds=interval):
continue
logger.debug("Heartbeat tick: scheduling agent={}", agent.role_id)
self._active_runs[agent.role_id] = asyncio.create_task(
self._run_agent_heartbeat(agent.role_id)
)
async def _run_agent_heartbeat(self, agent_id: str) -> None:
"""Single heartbeat cycle: find a pending task, check it out, execute."""
from opc.core.models import TaskStatus
try:
tasks = await self.store.get_tasks(status=TaskStatus.PENDING)
candidate = None
for task in tasks:
if task.assigned_to == agent_id or not task.assigned_to:
candidate = task
break
if not candidate:
return
claimed = await self.store.checkout_task(candidate.id, agent_id)
if not claimed:
logger.debug("Agent {} failed to checkout task {}", agent_id, candidate.id)
return
logger.info("Agent {} executing task {} via heartbeat", agent_id, candidate.title)
if self.checkout_and_run_fn:
await self.checkout_and_run_fn(candidate, agent_id)
else:
await self.execute_task_fn(candidate)
except Exception:
logger.exception("Heartbeat run failed for agent={}", agent_id)
def _cleanup_done_runs(self) -> None:
done = [k for k, v in self._active_runs.items() if v.done()]
for k in done:
task = self._active_runs.pop(k)
if task.exception():
logger.warning("Heartbeat run for {} ended with error: {}", k, task.exception())
@@ -0,0 +1,623 @@
"""Company-mode metadata ownership rules.
This module is the executable owner matrix for WorkItem/runtime Task
metadata. In company mode, DelegationWorkItem owns business/collaboration
state; runtime Task owns execution/session/audit state. Task may carry a
small execution-copy envelope, but those copies are not scheduling facts.
"""
from __future__ import annotations
import copy
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Iterable, Mapping
from opc.core.models import DelegationWorkItem, Task
from opc.layer2_organization.work_item_links import linked_work_item_id_for_task
class MetadataOwner(str, Enum):
WORK_ITEM = "work_item"
RUNTIME_TASK = "runtime_task"
EXECUTION_COPY = "execution_copy"
@dataclass(frozen=True)
class MetadataFieldSpec:
key: str
owner: MetadataOwner
allowed_locations: tuple[str, ...] = ("work_item",)
legacy_fallback: bool = False
migration_policy: str = ""
description: str = ""
@property
def allows_task_execution_copy(self) -> bool:
return "task_execution_copy" in self.allowed_locations
@property
def allows_task_legacy_read(self) -> bool:
return "task_legacy_read" in self.allowed_locations or self.legacy_fallback
@dataclass(frozen=True)
class MetadataOwnershipIssue:
code: str
severity: str
key: str
owner: str
work_item_id: str = ""
runtime_task_id: str = ""
message: str = ""
details: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class MetadataOwnershipMigrationChange:
work_item_id: str
runtime_task_id: str
updates: dict[str, Any]
conflicts: dict[str, dict[str, Any]] = field(default_factory=dict)
@dataclass(frozen=True)
class MetadataOwnershipMigrationReport:
dry_run: bool
scanned_work_items: int
changed_work_items: int
changes: tuple[MetadataOwnershipMigrationChange, ...]
issues: tuple[MetadataOwnershipIssue, ...] = ()
def _spec(
key: str,
owner: MetadataOwner,
*,
allowed_locations: Iterable[str] | None = None,
legacy_fallback: bool = False,
migration_policy: str = "",
description: str = "",
) -> MetadataFieldSpec:
if allowed_locations is None:
allowed_locations = ("work_item",) if owner == MetadataOwner.WORK_ITEM else ("task",)
return MetadataFieldSpec(
key=key,
owner=owner,
allowed_locations=tuple(allowed_locations),
legacy_fallback=legacy_fallback,
migration_policy=migration_policy,
description=description,
)
_WORK_ITEM_FIELDS: tuple[MetadataFieldSpec, ...] = (
_spec("work_kind", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("current_turn_mode", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("dependency_work_item_ids", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("waiting_on_work_item_ids", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("delegated_children_pending", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("handoff_context", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), legacy_fallback=True, migration_policy="backfill_if_missing"),
_spec("context_preview", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), legacy_fallback=True, migration_policy="backfill_if_missing"),
_spec("prompt_contract", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), migration_policy="work_item_wins"),
_spec("review_target_prompt_contract", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), migration_policy="work_item_wins"),
_spec("report_target_prompt_contract", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), migration_policy="work_item_wins"),
_spec("prompt_contract_blocker", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), migration_policy="work_item_wins"),
_spec("manager_mutation_revision", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("manager_mutation_id", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("manager_mutation_action", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("manager_mutation_reason", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("manager_mutation_at", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("manager_mutation_by_role_id", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("manager_mutation_by_seat_id", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("manager_mutation_user_input", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("latest_user_directive", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("progress_log", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_legacy_read"), legacy_fallback=True, migration_policy="backfill_if_missing"),
_spec("work_item_role_name", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), legacy_fallback=True, migration_policy="backfill_if_missing"),
_spec("employee_assignment", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="backfill_if_missing"),
_spec("employee_prompt_context", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), legacy_fallback=True, migration_policy="backfill_if_missing"),
_spec("employee_delta_context", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), legacy_fallback=True, migration_policy="backfill_if_missing"),
_spec("completion_report", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), legacy_fallback=True, migration_policy="backfill_if_missing"),
_spec("deliverable_summary", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), legacy_fallback=True, migration_policy="backfill_if_missing"),
_spec("work_item_summary", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), legacy_fallback=True, migration_policy="backfill_if_missing"),
_spec("work_item_summary_for_downstream", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="backfill_if_missing"),
_spec("work_item_artifact_index", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), legacy_fallback=True, migration_policy="backfill_if_missing"),
_spec("verification_status", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), legacy_fallback=True, migration_policy="backfill_if_missing"),
_spec("verification_evidence", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), legacy_fallback=True, migration_policy="backfill_if_missing"),
_spec("verification", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), legacy_fallback=True, migration_policy="backfill_if_missing"),
_spec("structured_review_verdict", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="backfill_if_missing"),
_spec("review_owner_role_id", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("review_owner_seat_id", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("review_attempt", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("review_attempt_count", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("review_target_work_item_id", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("review_target_worker_task_id", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("review_target_worker_role_id", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("review_target_worker_seat_id", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("review_completion_report", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("review_target_title", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("review_target_description", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("review_evidence", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("rework_feedback", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("review_feedback_version", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("review_rework_count", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("review_retry_hint", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("review_retry_of_attempt", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("review_retry_reason", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("report_attempt", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("report_attempt_count", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("report_target_work_item_id", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("report_target_worker_task_id", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("report_target_worker_role_id", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("report_target_worker_seat_id", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("report_target_title", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("report_target_description", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("report_source_summary", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("report_source_result_content", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("report_source_evidence", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("follow_up_actions", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), legacy_fallback=True, migration_policy="backfill_if_missing"),
_spec("follow_up_action", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("follow_up_reason", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("follow_up_dedupe_key", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("synthesis_turn_started", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("synthesis_ready_at", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("synthesis_source_work_item_ids", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("synthesis_reports_to_role_id", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("synthesis_reports_to_seat_id", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
_spec("delivery_package", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), legacy_fallback=True, migration_policy="backfill_if_missing"),
_spec("downstream_assignments", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), legacy_fallback=True, migration_policy="backfill_if_missing"),
_spec("open_questions", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), legacy_fallback=True, migration_policy="backfill_if_missing"),
_spec("assumptions", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), legacy_fallback=True, migration_policy="backfill_if_missing"),
_spec("decisions", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), legacy_fallback=True, migration_policy="backfill_if_missing"),
_spec("risks", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), legacy_fallback=True, migration_policy="backfill_if_missing"),
_spec("self_evolution_work_item", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), migration_policy="work_item_wins"),
_spec("self_evolution_root", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), migration_policy="work_item_wins"),
_spec("self_evolution_checkpoint_id", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), migration_policy="work_item_wins"),
_spec("self_evolution_human_action", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), migration_policy="work_item_wins"),
_spec("self_evolution_human_feedback", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), migration_policy="work_item_wins"),
_spec("self_evolution_delivery_task_id", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), migration_policy="work_item_wins"),
_spec("self_evolution_delivery_projection_id", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), migration_policy="work_item_wins"),
_spec("self_evolution_delivery_summary", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), migration_policy="work_item_wins"),
_spec("self_evolution_patch_max_retries", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), migration_policy="work_item_wins"),
_spec("self_evolution_recorded", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), migration_policy="work_item_wins"),
_spec("self_evolution_patch", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), migration_policy="work_item_wins"),
_spec("self_evolution_completed_at", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), migration_policy="work_item_wins"),
_spec("self_evolution_error", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), migration_policy="work_item_wins"),
)
_RUNTIME_TASK_FIELDS: tuple[MetadataFieldSpec, ...] = (
_spec("runtime_v2", MetadataOwner.RUNTIME_TASK, allowed_locations=("task",), migration_policy="task_only"),
_spec("runtime_verification", MetadataOwner.RUNTIME_TASK, allowed_locations=("task",), migration_policy="task_only"),
_spec("runtime_verification_evidence", MetadataOwner.RUNTIME_TASK, allowed_locations=("task",), migration_policy="task_only"),
_spec("runtime_verification_status", MetadataOwner.RUNTIME_TASK, allowed_locations=("task",), migration_policy="task_only"),
_spec("member_session_state", MetadataOwner.RUNTIME_TASK, allowed_locations=("task",), migration_policy="task_only"),
_spec("external_resume_session_id", MetadataOwner.RUNTIME_TASK, allowed_locations=("task",), migration_policy="task_only"),
_spec("external_resume_agent_type", MetadataOwner.RUNTIME_TASK, allowed_locations=("task",), migration_policy="task_only"),
_spec("working_memory", MetadataOwner.RUNTIME_TASK, allowed_locations=("task",), migration_policy="task_only"),
_spec("interrupted_recovery", MetadataOwner.RUNTIME_TASK, allowed_locations=("task",), migration_policy="task_only"),
_spec("last_stop_reason", MetadataOwner.RUNTIME_TASK, allowed_locations=("task",), migration_policy="task_only"),
_spec("peer_wait", MetadataOwner.RUNTIME_TASK, allowed_locations=("task",), migration_policy="task_only"),
_spec("comms_cross_role_history", MetadataOwner.RUNTIME_TASK, allowed_locations=("task",), migration_policy="task_only"),
_spec("comms_last_blocked_reactivation_at", MetadataOwner.RUNTIME_TASK, allowed_locations=("task",), migration_policy="task_only"),
_spec("comms_last_blocked_reactivation_key", MetadataOwner.RUNTIME_TASK, allowed_locations=("task",), migration_policy="task_only"),
_spec("runtime_control_state", MetadataOwner.RUNTIME_TASK, allowed_locations=("task",), migration_policy="task_only"),
_spec("automated_verification_results", MetadataOwner.RUNTIME_TASK, allowed_locations=("task",), migration_policy="task_only"),
_spec("runtime_session_team_instance_id", MetadataOwner.RUNTIME_TASK, allowed_locations=("task",), migration_policy="task_only"),
_spec("runtime_session_team_id", MetadataOwner.RUNTIME_TASK, allowed_locations=("task",), migration_policy="task_only"),
_spec("runtime_session_seat_id", MetadataOwner.RUNTIME_TASK, allowed_locations=("task",), migration_policy="task_only"),
)
_EXECUTION_COPY_FIELDS: tuple[MetadataFieldSpec, ...] = (
_spec("mode", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("execution_mode", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("execution_model", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("runtime_model", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("original_message", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("company_profile", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("delegation_playbook", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("runtime_topology", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("organization_id", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("org_id", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("delegation_run_id", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("delegation_cell_id", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("delegation_team_instance_id", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("delegation_team_id", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("delegation_seat_id", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("delegation_role_session_id", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("work_item_role_id", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("seat_manager_role_id", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("manager_role_id", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("manager_seat_id", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("managed_team_id", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("seat_contact_role_ids", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("allowed_delegate_role_ids", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("force_native_execution", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("preferred_external_agent", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("selected_execution_agent", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("execution_agent_locked", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("selected_execution_agent_source", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("work_item_execution_strategy", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("adaptive", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("execution_task_ids", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("parent_session_id", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("work_item_batch_id", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("target_output_dir", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("output_root", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("workspace_root", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("comms_workspace_root", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("comms_root", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("user_visible", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("authoritative_output", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("review_owner_kind", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("requires_user_feedback", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("feedback_scope", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("review_task", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("review_execution_work_item", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("report_execution_work_item", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
_spec("skip_work_item_sync", MetadataOwner.EXECUTION_COPY, allowed_locations=("task_execution_copy",), migration_policy="copy_from_runtime_context"),
)
METADATA_FIELD_SPECS: dict[str, MetadataFieldSpec] = {
spec.key: spec
for spec in (
*_WORK_ITEM_FIELDS,
*_RUNTIME_TASK_FIELDS,
*_EXECUTION_COPY_FIELDS,
)
}
WORK_ITEM_OWNED_KEYS: frozenset[str] = frozenset(
key for key, spec in METADATA_FIELD_SPECS.items() if spec.owner == MetadataOwner.WORK_ITEM
)
RUNTIME_TASK_OWNED_KEYS: frozenset[str] = frozenset(
key for key, spec in METADATA_FIELD_SPECS.items() if spec.owner == MetadataOwner.RUNTIME_TASK
)
EXECUTION_COPY_KEYS: frozenset[str] = frozenset(
key
for key, spec in METADATA_FIELD_SPECS.items()
if spec.owner == MetadataOwner.EXECUTION_COPY or spec.allows_task_execution_copy
)
LEGACY_READONLY_KEYS: frozenset[str] = frozenset()
def metadata_owner_for_key(key: str) -> MetadataOwner | None:
spec = METADATA_FIELD_SPECS.get(str(key or "").strip())
return spec.owner if spec is not None else None
def metadata_spec_for_key(key: str) -> MetadataFieldSpec | None:
return METADATA_FIELD_SPECS.get(str(key or "").strip())
def is_work_item_owned_key(key: str) -> bool:
return metadata_owner_for_key(key) == MetadataOwner.WORK_ITEM
def is_runtime_task_owned_key(key: str) -> bool:
return metadata_owner_for_key(key) == MetadataOwner.RUNTIME_TASK
def is_execution_copy_key(key: str) -> bool:
return str(key or "").strip() in EXECUTION_COPY_KEYS
def supports_legacy_task_fallback(key: str) -> bool:
spec = metadata_spec_for_key(key)
return bool(spec and spec.legacy_fallback)
def _has_value(value: Any) -> bool:
return value not in (None, "", [], {})
def copy_work_item_execution_metadata(
work_item: DelegationWorkItem | None,
*,
keys: Iterable[str] | None = None,
) -> dict[str, Any]:
"""Return WorkItem-owned values allowed on Task as execution copies."""
if work_item is None:
return {}
metadata = dict(getattr(work_item, "metadata", {}) or {})
selected = list(keys) if keys is not None else sorted(EXECUTION_COPY_KEYS)
copied: dict[str, Any] = {}
for key in selected:
spec = metadata_spec_for_key(key)
if spec is None or not spec.allows_task_execution_copy:
continue
if key not in metadata:
continue
value = metadata.get(key)
if not _has_value(value):
continue
copied[key] = copy.deepcopy(value)
return copied
def build_work_item_owner_execution_copy(work_item: DelegationWorkItem | None) -> dict[str, Any]:
"""Build the runtime Task owner envelope from the WorkItem facts."""
if work_item is None:
return {}
metadata = dict(getattr(work_item, "metadata", {}) or {})
payload = {
"delegation_run_id": str(getattr(work_item, "run_id", "") or "").strip(),
"delegation_cell_id": str(getattr(work_item, "cell_id", "") or "").strip(),
"delegation_team_instance_id": str(getattr(work_item, "team_instance_id", "") or "").strip(),
"delegation_team_id": str(
getattr(work_item, "team_id", "")
or metadata.get("team_id", "")
or getattr(work_item, "cell_id", "")
or ""
).strip(),
"delegation_seat_id": str(
getattr(work_item, "seat_id", "")
or metadata.get("seat_id", "")
or ""
).strip(),
"delegation_role_session_id": str(
getattr(work_item, "role_runtime_session_id", "")
or metadata.get("assigned_role_runtime_id", "")
or ""
).strip(),
"work_item_role_id": str(getattr(work_item, "role_id", "") or metadata.get("role_id", "") or "").strip(),
"work_kind": str(metadata.get("work_kind", "") or getattr(work_item, "kind", "") or "").strip().lower(),
"manager_role_id": str(getattr(work_item, "manager_role_id", "") or metadata.get("manager_role_id", "") or "").strip(),
"manager_seat_id": str(getattr(work_item, "manager_seat_id", "") or metadata.get("manager_seat_id", "") or "").strip(),
"work_item_batch_id": str(getattr(work_item, "batch_id", "") or "").strip(),
}
return {
key: copy.deepcopy(value)
for key, value in payload.items()
if key in EXECUTION_COPY_KEYS and _has_value(value)
}
def strip_disallowed_work_item_metadata_from_runtime_task(task: Task) -> list[str]:
"""Remove WorkItem-owned fields that are not valid Task execution copies."""
metadata = dict(getattr(task, "metadata", {}) or {})
removed: list[str] = []
for key in sorted(WORK_ITEM_OWNED_KEYS):
if key in EXECUTION_COPY_KEYS:
continue
if key in metadata:
metadata.pop(key, None)
removed.append(key)
if removed:
task.metadata = metadata
return removed
def filter_work_item_owned_metadata(updates: Mapping[str, Any]) -> dict[str, Any]:
return {
str(key): copy.deepcopy(value)
for key, value in dict(updates or {}).items()
if is_work_item_owned_key(str(key)) and _has_value(value)
}
def filter_runtime_task_owned_metadata(updates: Mapping[str, Any]) -> dict[str, Any]:
return {
str(key): copy.deepcopy(value)
for key, value in dict(updates or {}).items()
if is_runtime_task_owned_key(str(key)) and _has_value(value)
}
def validate_metadata_ownership(
work_item: DelegationWorkItem | None,
task: Task | None,
) -> list[MetadataOwnershipIssue]:
"""Read-only diagnostics for WorkItem/runtime Task metadata drift."""
if work_item is None and task is None:
return []
item_metadata = dict(getattr(work_item, "metadata", {}) or {}) if work_item is not None else {}
task_metadata = dict(getattr(task, "metadata", {}) or {}) if task is not None else {}
work_item_id = str(getattr(work_item, "work_item_id", "") or "").strip() or linked_work_item_id_for_task(task)
runtime_task_id = str(getattr(task, "id", "") or "").strip()
issues: list[MetadataOwnershipIssue] = []
for key in sorted(WORK_ITEM_OWNED_KEYS):
if key not in task_metadata:
continue
task_value = task_metadata.get(key)
if not _has_value(task_value):
continue
work_item_has_value = key in item_metadata and _has_value(item_metadata.get(key))
if not work_item_has_value:
issues.append(
MetadataOwnershipIssue(
code="metadata_ownership_violation",
severity="warning",
key=key,
owner=MetadataOwner.WORK_ITEM.value,
work_item_id=work_item_id,
runtime_task_id=runtime_task_id,
message=f"WorkItem-owned metadata `{key}` is present on runtime Task but missing on WorkItem.",
)
)
continue
if item_metadata.get(key) != task_value and not is_execution_copy_key(key):
issues.append(
MetadataOwnershipIssue(
code="metadata_ownership_conflict",
severity="warning",
key=key,
owner=MetadataOwner.WORK_ITEM.value,
work_item_id=work_item_id,
runtime_task_id=runtime_task_id,
message=f"WorkItem-owned metadata `{key}` differs between WorkItem and runtime Task; WorkItem wins.",
details={"work_item_value": item_metadata.get(key), "task_value": task_value},
)
)
for key in sorted(RUNTIME_TASK_OWNED_KEYS):
if key in item_metadata and _has_value(item_metadata.get(key)):
issues.append(
MetadataOwnershipIssue(
code="metadata_ownership_violation",
severity="warning",
key=key,
owner=MetadataOwner.RUNTIME_TASK.value,
work_item_id=work_item_id,
runtime_task_id=runtime_task_id,
message=f"Runtime Task-owned metadata `{key}` should not be stored on WorkItem.",
)
)
return issues
async def update_work_item_owned_metadata(
store: Any,
work_item_id: str,
updates: Mapping[str, Any],
) -> dict[str, Any]:
"""Persist only WorkItem-owned metadata updates through the store."""
filtered = filter_work_item_owned_metadata(updates)
if not filtered or store is None or not hasattr(store, "update_delegation_work_item"):
return {}
await store.update_delegation_work_item(work_item_id, metadata_updates=filtered)
return filtered
async def append_work_item_progress(
store: Any,
work_item_id: str,
message: str,
*,
limit: int = 20,
dedupe: bool = False,
) -> list[Any]:
"""Append user-visible progress to the WorkItem-owned progress log."""
wid = str(work_item_id or "").strip()
note = str(message or "").strip()
if not wid or not note or store is None or not hasattr(store, "get_delegation_work_item"):
return []
try:
work_item = await store.get_delegation_work_item(wid)
except Exception:
return []
if work_item is None:
return []
metadata = dict(getattr(work_item, "metadata", {}) or {})
progress = list(metadata.get("progress_log", []) or [])
if not dedupe or note not in progress:
progress.append(note)
progress = progress[-max(1, int(limit or 20)) :]
await update_work_item_owned_metadata(store, wid, {"progress_log": progress})
return progress
async def sync_work_item_current_turn_mode(
store: Any,
work_item_id: str,
current_turn_mode: str,
) -> bool:
"""Persist the WorkItem-owned current turn mode from runtime evaluation."""
wid = str(work_item_id or "").strip()
mode = str(current_turn_mode or "").strip()
if not wid or not mode:
return False
return bool(await update_work_item_owned_metadata(store, wid, {"current_turn_mode": mode}))
def update_runtime_task_owned_metadata(task: Task, updates: Mapping[str, Any]) -> dict[str, Any]:
"""Apply only runtime Task-owned metadata updates to an in-memory Task."""
filtered = filter_runtime_task_owned_metadata(updates)
if not filtered:
return {}
task.metadata = {**dict(task.metadata or {}), **filtered}
return filtered
async def migrate_work_item_owned_metadata_from_linked_tasks(
store: Any,
*,
run_id: str | None = None,
work_item_ids: Iterable[str] | None = None,
dry_run: bool = True,
) -> MetadataOwnershipMigrationReport:
"""Explicit maintenance backfill from legacy Task metadata to WorkItem.
This is intentionally not called from hot read paths. WorkItem wins on
conflicts; only missing WorkItem-owned keys are backfilled.
"""
if store is None:
return MetadataOwnershipMigrationReport(True, 0, 0, ())
items: list[DelegationWorkItem] = []
if work_item_ids is not None:
getter = getattr(store, "get_delegation_work_item", None)
if callable(getter):
for raw_id in work_item_ids:
wid = str(raw_id or "").strip()
if not wid:
continue
item = await getter(wid)
if item is not None:
items.append(item)
elif run_id:
lister = getattr(store, "list_delegation_work_items", None)
if callable(lister):
items = list(await lister(str(run_id).strip()))
get_runtime_task = getattr(store, "get_runtime_task_for_work_item", None)
changes: list[MetadataOwnershipMigrationChange] = []
issues: list[MetadataOwnershipIssue] = []
for item in items:
wid = str(getattr(item, "work_item_id", "") or "").strip()
if not wid or not callable(get_runtime_task):
continue
task = await get_runtime_task(wid)
if task is None:
continue
item_metadata = dict(item.metadata or {})
task_metadata = dict(task.metadata or {})
updates: dict[str, Any] = {}
conflicts: dict[str, dict[str, Any]] = {}
for key in sorted(WORK_ITEM_OWNED_KEYS):
if key not in task_metadata or not _has_value(task_metadata.get(key)):
continue
if key not in item_metadata or not _has_value(item_metadata.get(key)):
spec = metadata_spec_for_key(key)
if spec is not None and spec.legacy_fallback:
updates[key] = copy.deepcopy(task_metadata.get(key))
continue
if item_metadata.get(key) != task_metadata.get(key):
conflicts[key] = {
"work_item_value": copy.deepcopy(item_metadata.get(key)),
"task_value": copy.deepcopy(task_metadata.get(key)),
}
if conflicts:
for key in sorted(conflicts):
issues.append(
MetadataOwnershipIssue(
code="metadata_ownership_conflict",
severity="warning",
key=key,
owner=MetadataOwner.WORK_ITEM.value,
work_item_id=wid,
runtime_task_id=str(getattr(task, "id", "") or "").strip(),
message=f"WorkItem-owned metadata `{key}` conflicts during migration; WorkItem wins.",
details=conflicts[key],
)
)
if not updates:
continue
changes.append(
MetadataOwnershipMigrationChange(
work_item_id=wid,
runtime_task_id=str(getattr(task, "id", "") or "").strip(),
updates=updates,
conflicts=conflicts,
)
)
if not dry_run and hasattr(store, "update_delegation_work_item"):
await store.update_delegation_work_item(wid, metadata_updates=updates)
return MetadataOwnershipMigrationReport(
dry_run=bool(dry_run),
scanned_work_items=len(items),
changed_work_items=len(changes),
changes=tuple(changes),
issues=tuple(issues),
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,653 @@
"""Org-driven company work-item runtime planning."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
@dataclass
class WorkItemDependencySpec:
"""Dependency from one projected work item to another."""
projection_id: str
dependency_projection_id: str
dependency_class: str = "hard"
metadata: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return {
"projection_id": self.projection_id,
"dependency_projection_id": self.dependency_projection_id,
"dependency_class": self.dependency_class,
"metadata": dict(self.metadata),
}
@classmethod
def from_dict(cls, data: dict[str, Any] | None) -> "WorkItemDependencySpec":
payload = dict(data or {})
return cls(
projection_id=str(payload.get("projection_id", "") or "").strip(),
dependency_projection_id=str(payload.get("dependency_projection_id", "") or "").strip(),
dependency_class=str(payload.get("dependency_class", "") or "hard").strip() or "hard",
metadata=dict(payload.get("metadata", {}) or {}),
)
@dataclass
class WorkItemGatePolicy:
"""Projection-first gate policy for company work items."""
gate_type: str = "review"
instructions: str = ""
reviewer_role: str | None = None
requires_human: bool = False
on_reject: str = "halt"
rework_projection_id: str | None = None
max_retries: int = 1
metadata: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
self.metadata = dict(self.metadata or {})
rework_projection_id = str(
self.metadata.get("rework_projection_id")
or self.rework_projection_id
or ""
).strip()
self.rework_projection_id = rework_projection_id or None
self.gate_type = str(self.gate_type or "review").strip().lower() or "review"
self.on_reject = str(self.on_reject or "halt").strip().lower() or "halt"
if rework_projection_id:
self.metadata["rework_projection_id"] = rework_projection_id
def to_dict(self) -> dict[str, Any]:
return {
"type": self.gate_type,
"instructions": self.instructions,
"reviewer_role": self.reviewer_role,
"requires_human": bool(self.requires_human),
"on_reject": self.on_reject,
"rework_projection_id": self.rework_projection_id,
"max_retries": int(self.max_retries),
"metadata": dict(self.metadata),
}
@classmethod
def from_dict(cls, data: dict[str, Any] | None) -> "WorkItemGatePolicy | None":
if not data:
return None
payload = dict(data or {})
metadata = dict(payload.get("metadata", {}) or {})
rework_projection_id = str(
payload.get("rework_projection_id")
or metadata.get("rework_projection_id")
or ""
).strip()
return cls(
gate_type=str(payload.get("type", "") or payload.get("gate_type", "") or "review"),
instructions=str(payload.get("instructions", "") or ""),
reviewer_role=payload.get("reviewer_role"),
requires_human=bool(payload.get("requires_human", False)),
on_reject=str(payload.get("on_reject", "") or "halt"),
rework_projection_id=rework_projection_id or None,
max_retries=int(payload.get("max_retries", 1) or 1),
metadata=metadata,
)
@dataclass
class WorkItemReviewPolicy:
"""Review owner policy for a projected work item."""
review_owner_role_id: str = ""
review_level: str = "manager"
max_reworks: int = 10
metadata: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return {
"review_owner_role_id": self.review_owner_role_id,
"review_level": self.review_level,
"max_reworks": int(self.max_reworks),
"metadata": dict(self.metadata),
}
@classmethod
def from_dict(cls, data: dict[str, Any] | None) -> "WorkItemReviewPolicy":
payload = dict(data or {})
return cls(
review_owner_role_id=str(payload.get("review_owner_role_id", "") or "").strip(),
review_level=str(payload.get("review_level", "") or "manager").strip() or "manager",
max_reworks=int(payload.get("max_reworks", 10) or 10),
metadata=dict(payload.get("metadata", {}) or {}),
)
@dataclass
class WorkItemDeliveryPolicy:
"""Delivery policy for a projected company work item."""
user_visible: bool = False
authoritative_output: bool = False
requires_user_feedback: bool = False
metadata: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return {
"user_visible": bool(self.user_visible),
"authoritative_output": bool(self.authoritative_output),
"requires_user_feedback": bool(self.requires_user_feedback),
"metadata": dict(self.metadata),
}
@classmethod
def from_dict(cls, data: dict[str, Any] | None) -> "WorkItemDeliveryPolicy":
payload = dict(data or {})
return cls(
user_visible=bool(payload.get("user_visible", False)),
authoritative_output=bool(payload.get("authoritative_output", False)),
requires_user_feedback=bool(payload.get("requires_user_feedback", False)),
metadata=dict(payload.get("metadata", {}) or {}),
)
@dataclass
class WorkItemProjectionSpec:
"""Projection-first spec consumed by the company work-item runtime."""
projection_id: str
turn_type: str
role_id: str
title: str
summary: str = ""
dependency_projection_ids: list[str] = field(default_factory=list)
dependency_classes: dict[str, str] = field(default_factory=dict)
team_id: str = ""
seat_id: str = ""
manager_role_id: str = ""
manager_seat_id: str = ""
execution_strategy: str = "auto"
preferred_external_agent: str | None = None
parallel_group: str | None = None
prompt_refs: list[str] = field(default_factory=list)
skill_refs: list[str] = field(default_factory=list)
handoff_template_ref: str | None = None
memory_policy_ref: str | None = None
artifact_contract_ref: str | None = None
allowed_delegate_role_ids: list[str] = field(default_factory=list)
contact_role_ids: list[str] = field(default_factory=list)
gate_policy: WorkItemGatePolicy | None = None
review_policy: WorkItemReviewPolicy = field(default_factory=WorkItemReviewPolicy)
delivery_policy: WorkItemDeliveryPolicy = field(default_factory=WorkItemDeliveryPolicy)
metadata: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return {
"projection_id": self.projection_id,
"turn_type": self.turn_type,
"role_id": self.role_id,
"title": self.title,
"summary": self.summary,
"dependency_projection_ids": list(self.dependency_projection_ids),
"dependency_classes": dict(self.dependency_classes),
"team_id": self.team_id,
"seat_id": self.seat_id,
"manager_role_id": self.manager_role_id,
"manager_seat_id": self.manager_seat_id,
"execution_strategy": self.execution_strategy,
"preferred_external_agent": self.preferred_external_agent,
"parallel_group": self.parallel_group,
"prompt_refs": list(self.prompt_refs),
"skill_refs": list(self.skill_refs),
"handoff_template_ref": self.handoff_template_ref,
"memory_policy_ref": self.memory_policy_ref,
"artifact_contract_ref": self.artifact_contract_ref,
"allowed_delegate_role_ids": list(self.allowed_delegate_role_ids),
"contact_role_ids": list(self.contact_role_ids),
"gate_policy": self.gate_policy.to_dict() if self.gate_policy else None,
"review_policy": self.review_policy.to_dict(),
"delivery_policy": self.delivery_policy.to_dict(),
"metadata": dict(self.metadata),
}
@classmethod
def from_dict(cls, data: dict[str, Any] | None) -> "WorkItemProjectionSpec":
payload = dict(data or {})
return cls(
projection_id=str(payload.get("projection_id", "") or "").strip(),
turn_type=str(payload.get("turn_type", "") or "execute").strip().lower() or "execute",
role_id=str(payload.get("role_id", "") or "").strip(),
title=str(payload.get("title", "") or payload.get("projection_id", "") or "Work Item").strip(),
summary=str(payload.get("summary", "") or "").strip(),
dependency_projection_ids=_clean_list(payload.get("dependency_projection_ids", [])),
dependency_classes={
str(key).strip(): str(value).strip()
for key, value in dict(payload.get("dependency_classes", {}) or {}).items()
if str(key).strip() and str(value).strip()
},
team_id=str(payload.get("team_id", "") or "").strip(),
seat_id=str(payload.get("seat_id", "") or "").strip(),
manager_role_id=str(payload.get("manager_role_id", "") or "").strip(),
manager_seat_id=str(payload.get("manager_seat_id", "") or "").strip(),
execution_strategy=str(payload.get("execution_strategy", "") or "auto").strip() or "auto",
preferred_external_agent=payload.get("preferred_external_agent"),
parallel_group=payload.get("parallel_group"),
prompt_refs=_clean_list(payload.get("prompt_refs", [])),
skill_refs=_clean_list(payload.get("skill_refs", [])),
handoff_template_ref=payload.get("handoff_template_ref"),
memory_policy_ref=payload.get("memory_policy_ref"),
artifact_contract_ref=payload.get("artifact_contract_ref"),
allowed_delegate_role_ids=_clean_list(payload.get("allowed_delegate_role_ids", [])),
contact_role_ids=_clean_list(payload.get("contact_role_ids", [])),
gate_policy=WorkItemGatePolicy.from_dict(payload.get("gate_policy")),
review_policy=WorkItemReviewPolicy.from_dict(payload.get("review_policy")),
delivery_policy=WorkItemDeliveryPolicy.from_dict(payload.get("delivery_policy")),
metadata=dict(payload.get("metadata", {}) or {}),
)
@dataclass
class CompanyWorkItemRuntimePlan:
"""Company mode plan expressed only as projected work items."""
profile: str = "corporate"
final_decider_role_id: str = ""
top_level_role_ids: list[str] = field(default_factory=list)
root_projection_id: str = ""
projections: list[WorkItemProjectionSpec] = field(default_factory=list)
dependencies: list[WorkItemDependencySpec] = field(default_factory=list)
collaboration_links: list[dict[str, Any]] = field(default_factory=list)
metadata: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return {
"profile": self.profile,
"runtime_model": "multi_team_org",
"work_item_driven": True,
"final_decider_role_id": self.final_decider_role_id,
"top_level_role_ids": list(self.top_level_role_ids),
"root_projection_id": self.root_projection_id,
"projections": [projection.to_dict() for projection in self.projections],
"dependencies": [dependency.to_dict() for dependency in self.dependencies],
"collaboration_links": [dict(link) for link in self.collaboration_links],
"metadata": dict(self.metadata),
}
@classmethod
def from_dict(cls, data: dict[str, Any] | None) -> "CompanyWorkItemRuntimePlan":
payload = dict(data or {})
projections = [
WorkItemProjectionSpec.from_dict(item)
for item in list(payload.get("projections", []) or payload.get("seeds", []) or [])
if isinstance(item, dict)
]
return cls(
profile=str(payload.get("profile", "") or "corporate").strip() or "corporate",
final_decider_role_id=str(payload.get("final_decider_role_id", "") or "").strip(),
top_level_role_ids=_clean_list(payload.get("top_level_role_ids", [])),
root_projection_id=str(payload.get("root_projection_id", "") or "").strip(),
projections=projections,
dependencies=[
WorkItemDependencySpec.from_dict(item)
for item in list(payload.get("dependencies", []) or [])
if isinstance(item, dict)
],
collaboration_links=[dict(item) for item in list(payload.get("collaboration_links", []) or []) if isinstance(item, dict)],
metadata=dict(payload.get("metadata", {}) or {}),
)
def projection_by_id(self) -> dict[str, WorkItemProjectionSpec]:
return {spec.projection_id: spec for spec in self.projections if spec.projection_id}
def projection_order_map(self) -> dict[str, int]:
return {
spec.projection_id: index
for index, spec in enumerate(self.projections)
if spec.projection_id
}
def dependencies_for(self, projection_id: str) -> list[str]:
spec = self.projection_by_id().get(str(projection_id or "").strip())
return list(spec.dependency_projection_ids) if spec is not None else []
def dependent_projection_ids(self, source_projection_id: str) -> list[str]:
source = str(source_projection_id or "").strip()
if not source:
return []
return [
spec.projection_id
for spec in self.projections
if source in {str(item).strip() for item in list(spec.dependency_projection_ids or [])}
and spec.projection_id
]
def serialize_company_work_item_plan(plan: CompanyWorkItemRuntimePlan | None) -> dict[str, Any]:
return plan.to_dict() if plan is not None else {}
def deserialize_company_work_item_plan(data: dict[str, Any] | None) -> CompanyWorkItemRuntimePlan:
return CompanyWorkItemRuntimePlan.from_dict(data)
@dataclass
class OrgWorkItemSeed:
"""A projection-first template for a work item owned by an org seat."""
projection_id: str
turn_type: str
role_id: str
team_id: str
seat_id: str
manager_role_id: str
title: str
summary: str
skill_refs: list[str] = field(default_factory=list)
prompt_refs: list[str] = field(default_factory=list)
allowed_delegate_role_ids: list[str] = field(default_factory=list)
review_owner_role_id: str = ""
dependency_work_item_ids: list[str] = field(default_factory=list)
metadata: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return {
"projection_id": self.projection_id,
"turn_type": self.turn_type,
"role_id": self.role_id,
"team_id": self.team_id,
"seat_id": self.seat_id,
"manager_role_id": self.manager_role_id,
"title": self.title,
"summary": self.summary,
"skill_refs": list(self.skill_refs),
"prompt_refs": list(self.prompt_refs),
"allowed_delegate_role_ids": list(self.allowed_delegate_role_ids),
"review_owner_role_id": self.review_owner_role_id,
"dependency_work_item_ids": list(self.dependency_work_item_ids),
"metadata": dict(self.metadata),
}
@dataclass
class OrgWorkItemRuntimeBlueprint:
"""The custom org collaboration plan consumed by the work-item runtime."""
profile: str = "custom"
final_decider_role_id: str = ""
top_level_role_ids: list[str] = field(default_factory=list)
root_projection_id: str = ""
seeds: list[OrgWorkItemSeed] = field(default_factory=list)
collaboration_links: list[dict[str, Any]] = field(default_factory=list)
metadata: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return {
"profile": self.profile,
"runtime_model": "multi_team_org",
"work_item_driven": True,
"final_decider_role_id": self.final_decider_role_id,
"top_level_role_ids": list(self.top_level_role_ids),
"root_projection_id": self.root_projection_id,
"seeds": [seed.to_dict() for seed in self.seeds],
"collaboration_links": [dict(link) for link in self.collaboration_links],
"metadata": dict(self.metadata),
}
def _clean_list(values: Any) -> list[str]:
return [str(item).strip() for item in list(values or []) if str(item).strip()]
def _projection_id(*parts: str) -> str:
return "::".join(str(part or "").strip().replace(" ", "_") for part in parts if str(part or "").strip())
def _role_review_owner(agent: Any, manager_role_id: str, final_decider_role_id: str) -> str:
policy = getattr(agent, "runtime_policy", {}) or {}
if isinstance(policy, dict):
review_role = str(policy.get("review_role", "") or "").strip()
if review_role:
return review_role
return manager_role_id or final_decider_role_id
def build_custom_org_work_item_blueprint(
org_engine: Any,
*,
runtime_topology: dict[str, Any],
original_request: str = "",
runtime_policy: dict[str, Any] | None = None,
) -> OrgWorkItemRuntimeBlueprint:
"""Build the custom runtime collaboration blueprint from org structure.
This function deliberately consumes only org topology and runtime policy.
Custom mode is org-first.
"""
final_decider_role_id = str(runtime_topology.get("final_decider_role_id", "") or "").strip()
top_level_role_ids = _clean_list(runtime_topology.get("top_level_role_ids", []))
if not final_decider_role_id:
if len(top_level_role_ids) == 1:
final_decider_role_id = top_level_role_ids[0]
else:
raise ValueError("custom org runtime requires final_decider_role_id when multiple top-level roles exist")
seats = [dict(item) for item in list(runtime_topology.get("seats", []) or []) if isinstance(item, dict)]
teams = [dict(item) for item in list(runtime_topology.get("teams", []) or []) if isinstance(item, dict)]
team_by_id = {str(team.get("team_id", "") or "").strip(): team for team in teams if str(team.get("team_id", "") or "").strip()}
seeds: list[OrgWorkItemSeed] = []
links: list[dict[str, Any]] = []
root_projection_id = _projection_id("custom", "intake", final_decider_role_id)
seen_projection_ids: set[str] = set()
for seat in seats:
role_id = str(seat.get("role_id", "") or "").strip()
seat_id = str(seat.get("seat_id", "") or "").strip()
team_id = str(seat.get("team_id", "") or "").strip()
if not role_id or not seat_id:
continue
agent = org_engine.get_agent(role_id) if hasattr(org_engine, "get_agent") else None
manager_role_id = str(seat.get("manager_role_id", "") or getattr(agent, "reports_to", "") or "").strip()
if manager_role_id == "owner":
manager_role_id = ""
allowed_delegate_role_ids = _clean_list(
seat.get("allowed_delegate_role_ids")
or (org_engine.get_allowed_downstream_roles(role_id) if hasattr(org_engine, "get_allowed_downstream_roles") else [])
)
contact_role_ids = _clean_list(seat.get("contact_role_ids", []))
is_final_decider = role_id == final_decider_role_id
is_manager = bool(allowed_delegate_role_ids or str(seat.get("managed_team_id", "") or "").strip() or seat.get("is_team_lead"))
turn_type = "intake" if is_final_decider else ("dispatch" if is_manager else "execute")
projection_id = root_projection_id if is_final_decider else _projection_id("custom", turn_type, seat_id)
if projection_id in seen_projection_ids:
projection_id = _projection_id(projection_id, role_id)
seen_projection_ids.add(projection_id)
role_name = str(getattr(agent, "name", "") or role_id).strip()
responsibility = str(getattr(agent, "responsibility", "") or "").strip()
seeds.append(
OrgWorkItemSeed(
projection_id=projection_id,
turn_type=turn_type,
role_id=role_id,
team_id=team_id,
seat_id=seat_id,
manager_role_id=manager_role_id,
title=f"{role_name} {turn_type.title()}",
summary=responsibility or original_request or f"{role_name} work item",
skill_refs=list(getattr(agent, "skill_refs", []) or []),
prompt_refs=list(getattr(agent, "prompt_refs", []) or []),
allowed_delegate_role_ids=allowed_delegate_role_ids,
review_owner_role_id=_role_review_owner(agent, manager_role_id, final_decider_role_id),
metadata={
"source": "custom_org_work_item_runtime",
"role_name": role_name,
"responsibility": responsibility,
"team_id": team_id,
"team_name": str((team_by_id.get(team_id, {}) or {}).get("metadata", {}).get("lead_name", "") or ""),
"contact_role_ids": contact_role_ids,
"managed_team_id": str(seat.get("managed_team_id", "") or "").strip(),
"preferred_external_agent": str(seat.get("preferred_external_agent", "") or getattr(agent, "preferred_external_agent", "") or "").strip(),
"selected_execution_agent": str(seat.get("selected_execution_agent", "") or "").strip(),
"runtime_policy": dict(getattr(agent, "runtime_policy", {}) or {}),
},
)
)
for delegate_role_id in allowed_delegate_role_ids:
links.append({
"source_role_id": role_id,
"target_role_id": delegate_role_id,
"link_type": "delegates_to",
"source_projection_id": projection_id,
})
if manager_role_id:
links.append({
"source_role_id": role_id,
"target_role_id": manager_role_id,
"link_type": "reports_to",
"source_projection_id": projection_id,
})
if root_projection_id not in {seed.projection_id for seed in seeds}:
final_agent = org_engine.get_agent(final_decider_role_id) if hasattr(org_engine, "get_agent") else None
seeds.insert(
0,
OrgWorkItemSeed(
projection_id=root_projection_id,
turn_type="intake",
role_id=final_decider_role_id,
team_id=f"team::{final_decider_role_id}",
seat_id=f"seat::team::{final_decider_role_id}::{final_decider_role_id}",
manager_role_id="",
title=f"{getattr(final_agent, 'name', final_decider_role_id)} Intake",
summary=original_request or "Custom organization intake",
skill_refs=list(getattr(final_agent, "skill_refs", []) or []),
prompt_refs=list(getattr(final_agent, "prompt_refs", []) or []),
allowed_delegate_role_ids=_clean_list(
org_engine.get_allowed_downstream_roles(final_decider_role_id)
if hasattr(org_engine, "get_allowed_downstream_roles")
else []
),
review_owner_role_id=final_decider_role_id,
metadata={"source": "custom_org_work_item_runtime", "fallback_root": True},
),
)
return OrgWorkItemRuntimeBlueprint(
final_decider_role_id=final_decider_role_id,
top_level_role_ids=top_level_role_ids,
root_projection_id=root_projection_id,
seeds=seeds,
collaboration_links=links,
metadata={
"source": "custom_org_work_item_runtime",
"team_count": len(teams),
"seat_count": len(seats),
"runtime_policy": dict(runtime_policy or {}),
},
)
def build_company_work_item_runtime_plan(
org_engine: Any,
*,
profile: str = "corporate",
runtime_topology: dict[str, Any],
original_request: str = "",
runtime_policy: dict[str, Any] | None = None,
) -> CompanyWorkItemRuntimePlan:
"""Build the company runtime plan from org topology, not a fixed step list."""
normalized_profile = str(profile or "corporate").strip() or "corporate"
blueprint = build_custom_org_work_item_blueprint(
org_engine,
runtime_topology=runtime_topology,
original_request=original_request,
runtime_policy=runtime_policy,
)
root_projection_id = blueprint.root_projection_id.replace("custom::", f"{normalized_profile}::", 1)
projections: list[WorkItemProjectionSpec] = []
dependencies: list[WorkItemDependencySpec] = []
for seed in blueprint.seeds:
projection_id = seed.projection_id.replace("custom::", f"{normalized_profile}::", 1)
dependency_projection_ids = [
item.replace("custom::", f"{normalized_profile}::", 1)
for item in list(seed.dependency_work_item_ids or [])
]
if projection_id != root_projection_id and root_projection_id and root_projection_id not in dependency_projection_ids:
dependency_projection_ids.insert(0, root_projection_id)
for dependency_projection_id in dependency_projection_ids:
dependencies.append(
WorkItemDependencySpec(
projection_id=projection_id,
dependency_projection_id=dependency_projection_id,
dependency_class="hard",
)
)
metadata = {
**dict(seed.metadata or {}),
"source": "company_work_item_runtime_plan",
"seed_source": dict(seed.metadata or {}).get("source", ""),
"work_kind": seed.turn_type,
"delegation_turn_kind": seed.turn_type,
"allowed_delegate_role_ids": list(seed.allowed_delegate_role_ids),
"dependency_projection_ids": list(dependency_projection_ids),
"runtime_policy": dict(runtime_policy or {}),
}
is_root = projection_id == root_projection_id
projections.append(
WorkItemProjectionSpec(
projection_id=projection_id,
turn_type=seed.turn_type,
role_id=seed.role_id,
title=seed.title,
summary=seed.summary,
dependency_projection_ids=dependency_projection_ids,
team_id=seed.team_id,
seat_id=seed.seat_id,
manager_role_id=seed.manager_role_id,
prompt_refs=list(seed.prompt_refs),
skill_refs=list(seed.skill_refs),
allowed_delegate_role_ids=list(seed.allowed_delegate_role_ids),
contact_role_ids=_clean_list(seed.metadata.get("contact_role_ids", [])),
preferred_external_agent=str(seed.metadata.get("preferred_external_agent", "") or "").strip() or None,
gate_policy=(
WorkItemGatePolicy(
gate_type="review",
reviewer_role=seed.review_owner_role_id or None,
on_reject="rework",
rework_projection_id=projection_id,
metadata={"source": "work_item_review_policy"},
)
if seed.review_owner_role_id and not is_root
else None
),
review_policy=WorkItemReviewPolicy(
review_owner_role_id=seed.review_owner_role_id,
review_level="manager" if seed.review_owner_role_id else "human",
),
delivery_policy=WorkItemDeliveryPolicy(
user_visible=is_root,
authoritative_output=is_root,
requires_user_feedback=is_root,
),
metadata=metadata,
)
)
return CompanyWorkItemRuntimePlan(
profile=normalized_profile,
final_decider_role_id=blueprint.final_decider_role_id,
top_level_role_ids=list(blueprint.top_level_role_ids),
root_projection_id=root_projection_id,
projections=projections,
dependencies=dependencies,
collaboration_links=[dict(link) for link in blueprint.collaboration_links],
metadata={
**dict(blueprint.metadata or {}),
"source": "company_work_item_runtime_plan",
"runtime_model": "multi_team_org",
"work_item_driven": True,
"runtime_policy": dict(runtime_policy or {}),
},
)
+197
View File
@@ -0,0 +1,197 @@
"""Helpers for company runtime output-root contracts.
The runtime may receive a user-requested absolute output path that is not
writable inside an external-agent sandbox. These helpers keep the writable
canonical output root separate from requested path aliases.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any
OUTPUT_CONTRACT_KEYS: tuple[str, ...] = (
"workspace_root",
"output_root",
"target_output_dir",
"requested_output_root",
"output_root_aliases",
"output_root_alias_map",
)
PATH_BLOCKER_TOKENS: tuple[str, ...] = (
"operation not permitted",
"permission denied",
"sandbox",
"not writable",
"read-only",
"readonly",
"missing required path",
"required root",
"required path",
)
def normalize_path_text(value: Any) -> str:
raw = str(value or "").strip()
if not raw:
return ""
try:
return str(Path(raw).expanduser().resolve(strict=False))
except Exception:
return raw
def is_path_relative_to(path: str | Path, root: str | Path) -> bool:
try:
Path(path).expanduser().resolve(strict=False).relative_to(
Path(root).expanduser().resolve(strict=False)
)
return True
except Exception:
return False
def output_contract_metadata(source: dict[str, Any] | None) -> dict[str, Any]:
data = dict(source or {})
output_root = normalize_path_text(data.get("output_root") or data.get("target_output_dir"))
target_output_dir = normalize_path_text(data.get("target_output_dir") or output_root)
workspace_root = normalize_path_text(data.get("workspace_root") or data.get("comms_workspace_root"))
raw_requested_output_root = str(data.get("requested_output_root") or "").strip()
requested_output_root = normalize_path_text(raw_requested_output_root)
aliases: list[str] = []
for item in list(data.get("output_root_aliases", []) or []):
raw_alias = str(item or "").strip()
normalized_alias = normalize_path_text(raw_alias)
if normalized_alias:
aliases.append(normalized_alias)
if raw_alias and raw_alias != normalized_alias:
aliases.append(raw_alias)
if requested_output_root and requested_output_root not in aliases and requested_output_root != output_root:
aliases.append(requested_output_root)
if raw_requested_output_root and raw_requested_output_root not in aliases and raw_requested_output_root != output_root:
aliases.append(raw_requested_output_root)
alias_map: dict[str, str] = {}
raw_map = data.get("output_root_alias_map", {})
if isinstance(raw_map, dict):
for key, value in raw_map.items():
raw_alias = str(key or "").strip()
alias = normalize_path_text(key)
target = normalize_path_text(value)
if raw_alias and target:
alias_map[raw_alias] = target
if alias and target:
alias_map[alias] = target
if output_root:
for alias in aliases:
if alias and alias != output_root:
alias_map.setdefault(alias, output_root)
contract: dict[str, Any] = {}
if workspace_root:
contract["workspace_root"] = workspace_root
if output_root:
contract["output_root"] = output_root
contract["target_output_dir"] = target_output_dir or output_root
elif target_output_dir:
contract["target_output_dir"] = target_output_dir
if requested_output_root:
contract["requested_output_root"] = requested_output_root
if aliases:
contract["output_root_aliases"] = list(dict.fromkeys(aliases))
if alias_map:
contract["output_root_alias_map"] = alias_map
return contract
def output_alias_map(source: dict[str, Any] | None) -> dict[str, str]:
data = dict(source or {})
contract = output_contract_metadata(data)
aliases = dict(contract.get("output_root_alias_map", {}) or {})
output_root = str(contract.get("output_root") or contract.get("target_output_dir") or "").strip()
if not output_root:
return aliases
raw_requested = str(data.get("requested_output_root") or "").strip()
if raw_requested and raw_requested != output_root:
aliases.setdefault(raw_requested, output_root)
raw_aliases = data.get("output_root_aliases", []) or []
if isinstance(raw_aliases, (list, tuple, set)):
for item in raw_aliases:
raw_alias = str(item or "").strip()
if raw_alias and raw_alias != output_root:
aliases.setdefault(raw_alias, output_root)
raw_map = data.get("output_root_alias_map", {})
if isinstance(raw_map, dict):
for key, value in raw_map.items():
raw_alias = str(key or "").strip()
raw_target = str(value or "").strip()
target = normalize_path_text(raw_target) or output_root
if raw_alias and target:
aliases.setdefault(raw_alias, target)
return aliases
def replace_output_aliases(value: Any, source: dict[str, Any] | None) -> Any:
aliases = output_alias_map(source)
if not aliases:
return value
if isinstance(value, str):
updated = value
for alias, target in sorted(aliases.items(), key=lambda item: len(item[0]), reverse=True):
if alias and target and alias != target:
updated = updated.replace(alias, target)
return updated
if isinstance(value, list):
return [replace_output_aliases(item, source) for item in value]
if isinstance(value, tuple):
return tuple(replace_output_aliases(item, source) for item in value)
if isinstance(value, dict):
return {
key: replace_output_aliases(item, source)
for key, item in value.items()
}
return value
def text_has_path_blocker(value: Any) -> bool:
text = str(value or "").strip().lower()
if not text:
return False
return any(token in text for token in PATH_BLOCKER_TOKENS)
def render_output_contract_context(
source: dict[str, Any] | None,
*,
heading: str = "## Runtime Output Contract",
include_workspace_root: bool = True,
) -> str:
contract = output_contract_metadata(source)
output_root = str(contract.get("output_root", "") or "").strip()
workspace_root = str(contract.get("workspace_root", "") or "").strip()
requested = str(contract.get("requested_output_root", "") or "").strip()
alias_map = dict(contract.get("output_root_alias_map", {}) or {})
visible_workspace_root = workspace_root if include_workspace_root else ""
if not any([output_root, visible_workspace_root, requested, alias_map]):
return ""
lines: list[str] = [heading]
if output_root:
lines.append(f"Canonical writable output root: {output_root}")
if visible_workspace_root:
lines.append(f"Workspace root: {visible_workspace_root}")
if requested and requested != output_root:
lines.append(f"Requested output alias: {requested}")
if alias_map:
lines.append("Path aliases:")
for alias, target in list(alias_map.items())[:6]:
lines.append(f"- {alias} -> {target}")
lines.append("When old instructions mention an alias path, write and verify against the canonical output root.")
return "\n".join(lines).strip()
+503
View File
@@ -0,0 +1,503 @@
"""Unified phase model for delegation work items.
A `Phase` is the single authoritative state of a delegation work item. It
replaces the previous mixture of `status` + 5 metadata sub-state fields
(activation_state / lifecycle_state / review_state / manager_release_state /
review_execution_state) which were tangled and could disagree with each other.
Design:
- One enum value per concrete situation a card can be in.
- Pure-function projections derive everything else (kanban column, owner,
TaskStatus, runnability, verdict).
- A static transition table is enforced at every write.
- A **phase-transition hook** mechanism lets other layers (task.status,
role_session.status, dispatcher wake signal, ...) subscribe to phase
changes and synchronise themselves in one place. This eliminates the
bug class where code forgot to update a dependent layer after
transitioning a phase, leaving task/session state desynchronised from
work-item phase.
"""
from __future__ import annotations
from typing import Any, Awaitable, Callable, Mapping, Optional
from loguru import logger
from opc.core.models import Phase, TaskStatus
from opc.layer2_organization.work_item_identity import work_item_turn_type_from_metadata
__all__ = [
"Phase",
"TODO_PHASES",
"IN_PROGRESS_PHASES",
"IN_REVIEW_PHASES",
"DONE_PHASES",
"TERMINAL_PHASES",
"RUNNABLE_PHASES",
"WAITING_EXTERNAL_PHASES",
"ALLOWED_TRANSITIONS",
"InvalidPhaseTransition",
"validate_transition",
"kanban_column",
"is_runnable",
"is_terminal",
"is_waiting_external",
"effective_owner",
"verdict",
"task_status_for_phase",
"phase_for_task_status",
"coerce_phase",
"is_review_execution_work_item_metadata",
"should_hide_work_item_from_company_kanban",
"is_resumable_after_claim_release",
"is_orphaned",
"is_dispatchable",
"PhaseTransitionHook",
"register_phase_transition_hook",
"clear_phase_transition_hooks",
"on_phase_transition",
]
# ── Phase transition hook mechanism (D2) ─────────────────────────────────
#
# Other layers (task.status sync, role_session.status sync, dispatcher wake)
# subscribe by calling register_phase_transition_hook. The store fires
# on_phase_transition after every successful work-item write. Each hook
# is invoked with (previous_phase, target_phase, item, store=...). One
# hook raising never prevents the others from firing nor prevents the
# write itself from succeeding; the goal is "best-effort cross-layer
# convergence", not transactional consistency. (The work-item write IS
# the source of truth — hooks merely propagate it.)
PhaseTransitionHook = Callable[..., Awaitable[None]]
_PHASE_TRANSITION_HOOKS: list[PhaseTransitionHook] = []
def register_phase_transition_hook(hook: PhaseTransitionHook) -> None:
"""Register a hook to fire after every work-item phase write.
Hook signature: ``async def hook(previous: Phase | None, target: Phase,
item: DelegationWorkItem, *, store: OPCStore) -> None``.
Hooks are invoked in registration order. Re-registering the same
callable is a no-op (idempotent — important for module-level
registration that may run twice under reload/import re-entry).
"""
if hook not in _PHASE_TRANSITION_HOOKS:
_PHASE_TRANSITION_HOOKS.append(hook)
def clear_phase_transition_hooks() -> None:
"""Remove all registered hooks. Test-only helper."""
_PHASE_TRANSITION_HOOKS.clear()
async def on_phase_transition(
previous: Phase | None,
target: Phase,
item: Any,
*,
store: Any,
) -> None:
"""Fire all registered phase-transition hooks.
Called by the store after a successful work-item write commits. Each
hook gets the same arguments; failures in one hook are logged but do
not affect other hooks or the upstream write.
"""
for hook in list(_PHASE_TRANSITION_HOOKS):
try:
await hook(previous, target, item, store=store)
except Exception:
logger.opt(exception=True).warning(
f"phase transition hook {getattr(hook, '__name__', repr(hook))} "
f"failed for {previous} -> {target} on work_item "
f"{getattr(item, 'work_item_id', '?')}"
)
def _normalized_text(value: Any) -> str:
return str(value or "").strip().lower()
def is_review_execution_work_item_metadata(metadata: Mapping[str, Any] | None) -> bool:
"""True when the work item is a hidden auxiliary review card."""
data = dict(metadata or {})
if bool(data.get("review_execution_work_item", False)):
return True
work_kind = _normalized_text(data.get("work_kind") or work_item_turn_type_from_metadata(data, fallback=""))
return work_kind == "review" and bool(str(data.get("review_target_work_item_id", "") or "").strip())
def is_report_execution_work_item_metadata(metadata: Mapping[str, Any] | None) -> bool:
"""True when the work item is the hidden auxiliary report-generation card.
Spawned by ``_apply_done_transition`` after a worker finishes its execute
turn but before review begins, so the worker can produce a structured
handoff report on its own session before the reviewer sees anything.
"""
data = dict(metadata or {})
if bool(data.get("report_execution_work_item", False)):
return True
work_kind = _normalized_text(data.get("work_kind") or work_item_turn_type_from_metadata(data, fallback=""))
return work_kind == "report" and bool(str(data.get("report_target_work_item_id", "") or "").strip())
def should_hide_work_item_from_company_kanban(metadata: Mapping[str, Any] | None) -> bool:
"""True when the kanban UI should not display this work item."""
data = dict(metadata or {})
return bool(data.get("hidden_from_company_kanban", False))
# ── Set views over phases ────────────────────────────────────────────────
TODO_PHASES: frozenset[Phase] = frozenset({
Phase.QUEUED,
Phase.READY,
Phase.READY_FOR_REWORK,
Phase.WAITING_DEPENDENCIES,
})
IN_PROGRESS_PHASES: frozenset[Phase] = frozenset({
Phase.RUNNING,
Phase.WAITING_FOR_PEER,
Phase.WAITING_FOR_CHILDREN,
Phase.PAUSED,
Phase.NEEDS_ATTENTION,
})
IN_REVIEW_PHASES: frozenset[Phase] = frozenset({
Phase.AWAITING_MANAGER_REVIEW,
Phase.AWAITING_HUMAN,
})
DONE_PHASES: frozenset[Phase] = frozenset({
Phase.APPROVED,
Phase.FAILED,
Phase.CANCELLED,
})
TERMINAL_PHASES: frozenset[Phase] = DONE_PHASES
RUNNABLE_PHASES: frozenset[Phase] = frozenset({
Phase.READY,
Phase.READY_FOR_REWORK,
})
WAITING_EXTERNAL_PHASES: frozenset[Phase] = frozenset({
Phase.WAITING_FOR_PEER,
Phase.WAITING_FOR_CHILDREN,
Phase.WAITING_DEPENDENCIES,
Phase.NEEDS_ATTENTION,
})
# ── Allowed transitions (state machine) ──────────────────────────────────
# Two universal exits every non-terminal phase must have:
# - FAILED: abort-on-error path. Any exception in the agent runtime
# must be able to land here from any sub-state.
# - CANCELLED: user/system cancel path. Same reason.
# These are baked into the table below; the invariant tests in
# test_phase_state_machine_invariants.py guarantee they stay there.
_UNIVERSAL_EXITS: frozenset[Phase] = frozenset({Phase.FAILED, Phase.CANCELLED})
# Transitions that release a stale claim back to the dispatcher queue,
# enabling crash-recovery (Bug C). Whenever an in-flight card's runtime
# session dies, the sweeper transitions the card back to READY (or
# READY_FOR_REWORK if appropriate), which the dispatcher then re-claims.
# This must be wired into every in-flight phase.
_RECOVERY_EXITS: frozenset[Phase] = frozenset({Phase.READY})
ALLOWED_TRANSITIONS: dict[Phase, frozenset[Phase]] = {
# todo
Phase.QUEUED: frozenset({
Phase.READY,
Phase.WAITING_DEPENDENCIES,
}) | _UNIVERSAL_EXITS,
Phase.WAITING_DEPENDENCIES: frozenset({
Phase.READY,
}) | _UNIVERSAL_EXITS,
Phase.READY: frozenset({
Phase.RUNNING,
Phase.WAITING_DEPENDENCIES,
}) | _UNIVERSAL_EXITS,
Phase.READY_FOR_REWORK: frozenset({
Phase.RUNNING,
}) | _UNIVERSAL_EXITS,
# in_progress
# RUNNING → APPROVED is the self-completion path used by review
# work items (which do not themselves need external review). It is
# also a legal escape for any future "auto-approved" work kind.
Phase.RUNNING: frozenset({
Phase.WAITING_FOR_PEER,
Phase.WAITING_FOR_CHILDREN,
Phase.PAUSED,
Phase.NEEDS_ATTENTION,
Phase.AWAITING_MANAGER_REVIEW,
Phase.AWAITING_HUMAN,
Phase.APPROVED,
}) | _UNIVERSAL_EXITS | _RECOVERY_EXITS,
Phase.WAITING_FOR_PEER: frozenset({
Phase.RUNNING,
}) | _UNIVERSAL_EXITS | _RECOVERY_EXITS,
Phase.WAITING_FOR_CHILDREN: frozenset({
Phase.RUNNING,
}) | _UNIVERSAL_EXITS | _RECOVERY_EXITS,
Phase.PAUSED: frozenset({
Phase.RUNNING,
}) | _UNIVERSAL_EXITS | _RECOVERY_EXITS,
Phase.NEEDS_ATTENTION: frozenset({
Phase.RUNNING,
}) | _UNIVERSAL_EXITS | _RECOVERY_EXITS,
# in_review
Phase.AWAITING_MANAGER_REVIEW: frozenset({
Phase.APPROVED,
Phase.READY_FOR_REWORK,
# Escalation path: when the manager-review/rework loop exceeds its
# retry budget, the runtime escalates the card to a human decider
# instead of bouncing it back to the worker yet again.
Phase.AWAITING_HUMAN,
}) | _UNIVERSAL_EXITS | _RECOVERY_EXITS,
Phase.AWAITING_HUMAN: frozenset({
Phase.APPROVED,
Phase.READY_FOR_REWORK,
}) | _UNIVERSAL_EXITS | _RECOVERY_EXITS,
# terminal — no outgoing edges
Phase.APPROVED: frozenset(),
Phase.FAILED: frozenset(),
Phase.CANCELLED: frozenset(),
}
class InvalidPhaseTransition(ValueError):
"""Raised when a write attempts a transition not in ALLOWED_TRANSITIONS."""
def validate_transition(previous: Phase | None, target: Phase) -> None:
"""Raise InvalidPhaseTransition when target is not reachable from previous.
Initial creation (previous is None) and idempotent writes (previous == target)
are always allowed.
"""
if previous is None or previous == target:
return
allowed = ALLOWED_TRANSITIONS.get(previous, frozenset())
if target not in allowed:
raise InvalidPhaseTransition(
f"invalid phase transition: {previous.value} -> {target.value}"
)
# ── Pure-function projections ────────────────────────────────────────────
_PHASE_TO_COLUMN: dict[Phase, str] = {
**{p: "todo" for p in TODO_PHASES},
**{p: "in_progress" for p in IN_PROGRESS_PHASES},
**{p: "in_review" for p in IN_REVIEW_PHASES},
**{p: "done" for p in DONE_PHASES},
}
def kanban_column(phase: Phase) -> str:
"""Project Phase to one of the four kanban columns."""
return _PHASE_TO_COLUMN[phase]
def is_runnable(phase: Phase) -> bool:
"""Whether the dispatcher should claim+spawn this card on its next tick."""
return phase in RUNNABLE_PHASES
# Phases whose runtime claim, when released as stale (process restart, crashed
# session), allow the dispatcher to re-pick the card. Without this set, a card
# that was actively running when the process died becomes a zombie: phase still
# says RUNNING / WAITING_FOR_*, but no session is alive to make progress.
_RESUMABLE_AFTER_STALE_CLAIM: frozenset[Phase] = frozenset({
Phase.RUNNING,
Phase.WAITING_FOR_PEER,
Phase.WAITING_FOR_CHILDREN,
Phase.PAUSED,
Phase.NEEDS_ATTENTION,
Phase.AWAITING_MANAGER_REVIEW,
Phase.AWAITING_HUMAN,
})
def is_resumable_after_claim_release(phase: Phase) -> bool:
"""True iff a stale claim on this phase can be released and the card
re-picked up by the dispatcher (rather than left as a zombie).
Used by the periodic stale-claim sweeper. Every in-flight phase must
return True here — the invariant test in
test_phase_state_machine_invariants.py enforces this.
"""
return phase in _RESUMABLE_AFTER_STALE_CLAIM
def is_orphaned(item: Any) -> bool:
"""A work item is orphaned when its phase says 'in flight' but no
runtime session currently holds a claim on it.
Typical scenario: the process that owned the claim died (restart,
crash). On startup the stale-claim sweeper clears the claim
metadata; this function then lets the dispatcher re-pick the card
on the next tick, eliminating zombie work items (Bug C).
"""
if not is_resumable_after_claim_release(item.phase):
return False
claim = str(getattr(item, "claimed_by_role_runtime_session_id", "") or "").strip()
return not claim
def is_dispatchable(item: Any) -> bool:
"""Combined check used by the dispatcher: pick this card on next tick?
True for fresh-runnable phases (READY/READY_FOR_REWORK) and for
orphaned in-flight cards (RUNNING / WAITING_FOR_* / PAUSED / etc.
whose previous claim died and was swept).
Fix 5 PR3: a work item stamped with ``metadata.queued_behind_session``
is waiting behind another task on its role's serial queue — the
dispatcher must skip it until the session that holds the queue
dequeues it (``clear_session_focus_on_terminal_hook`` does the
dequeue on terminal transitions and drops the stamp). Without this
check the dispatcher would claim both the focused item and the queued
item into the same session on the same tick, defeating the queue.
"""
metadata = getattr(item, "metadata", {}) or {}
if isinstance(metadata, dict) and str(metadata.get("queued_behind_session", "") or "").strip():
return False
if isinstance(metadata, dict) and str(metadata.get("dispatch_hold", "") or "").strip():
return False
return is_runnable(item.phase) or is_orphaned(item)
def is_terminal(phase: Phase) -> bool:
return phase in TERMINAL_PHASES
def is_waiting_external(phase: Phase) -> bool:
"""True when the card is suspended waiting for an external event/actor."""
return phase in WAITING_EXTERNAL_PHASES
def effective_owner(phase: Phase, item: Any) -> tuple[str, str]:
"""Return (role_id, seat_id) of the actor currently responsible for the card.
In `in_review` phases the owner swaps from worker to manager so the card
appears in the reviewer's swimlane. In all other phases the worker stays
as owner. `item` may be a DelegationWorkItem or a mapping with the same
field names.
"""
def _field(name: str) -> str:
if isinstance(item, Mapping):
return str(item.get(name, "") or "").strip()
return str(getattr(item, name, "") or "").strip()
if phase in IN_REVIEW_PHASES:
return _field("manager_role_id") or _field("role_id"), \
_field("manager_seat_id") or _field("seat_id")
return _field("role_id"), _field("seat_id")
_PHASE_TO_VERDICT: dict[Phase, str] = {
Phase.APPROVED: "approve",
Phase.READY_FOR_REWORK: "rework",
Phase.FAILED: "fail",
Phase.CANCELLED: "cancel",
}
def verdict(phase: Phase) -> Optional[str]:
"""Return the verdict label for terminal/rework phases, else None."""
return _PHASE_TO_VERDICT.get(phase)
# Company-mode boundary note:
# DelegationWorkItem.phase is the business state source of truth. Task.status is
# only the runtime/job projection needed by existing execution, session, and
# tool infrastructure.
_PHASE_TO_TASK_STATUS: dict[Phase, TaskStatus] = {
# todo column
Phase.QUEUED: TaskStatus.PENDING,
Phase.READY: TaskStatus.PENDING,
Phase.READY_FOR_REWORK: TaskStatus.PENDING,
Phase.WAITING_DEPENDENCIES: TaskStatus.BLOCKED,
# in_progress column
Phase.RUNNING: TaskStatus.RUNNING,
Phase.WAITING_FOR_PEER: TaskStatus.AWAITING_PEER,
Phase.WAITING_FOR_CHILDREN: TaskStatus.BLOCKED,
Phase.PAUSED: TaskStatus.BLOCKED,
Phase.NEEDS_ATTENTION: TaskStatus.BLOCKED,
# in_review column
Phase.AWAITING_MANAGER_REVIEW: TaskStatus.AWAITING_MANAGER_REVIEW,
Phase.AWAITING_HUMAN: TaskStatus.AWAITING_HUMAN,
# done column
Phase.APPROVED: TaskStatus.DONE,
Phase.FAILED: TaskStatus.FAILED,
Phase.CANCELLED: TaskStatus.CANCELLED,
}
def task_status_for_phase(phase: Phase) -> TaskStatus:
"""Project Phase to the corresponding runtime TaskStatus.
Used when syncing a work item's phase back to its associated tasks row.
"""
return _PHASE_TO_TASK_STATUS[phase]
# ── TaskStatus → Phase reverse mapping (for runtime → work-item sync) ────
def phase_for_task_status(
status: TaskStatus | str,
*,
has_pending_children: bool = False,
) -> Phase:
"""Map a runtime TaskStatus to a Phase.
`has_pending_children` lets the caller distinguish two BLOCKED sub-cases:
BLOCKED-with-children-pending → WAITING_FOR_CHILDREN, otherwise PAUSED.
All other TaskStatus values map 1-to-1.
"""
s = TaskStatus(status) if not isinstance(status, TaskStatus) else status
if s == TaskStatus.RUNNING:
return Phase.RUNNING
if s == TaskStatus.AWAITING_PEER:
return Phase.WAITING_FOR_PEER
if s == TaskStatus.BLOCKED:
return Phase.WAITING_FOR_CHILDREN if has_pending_children else Phase.PAUSED
if s in (TaskStatus.AWAITING_MANAGER_REVIEW, TaskStatus.AWAITING_REVIEW):
return Phase.AWAITING_MANAGER_REVIEW
if s == TaskStatus.AWAITING_HUMAN:
return Phase.AWAITING_HUMAN
if s == TaskStatus.DONE:
return Phase.APPROVED
if s == TaskStatus.FAILED:
return Phase.FAILED
if s == TaskStatus.CANCELLED:
return Phase.CANCELLED
# PENDING / IDLE → READY (caller can override to QUEUED if not released)
return Phase.READY
# ── Convenience parsers ─────────────────────────────────────────────────
def coerce_phase(value: Any) -> Phase:
"""Parse a phase value from a string/Phase, raising ValueError on garbage."""
if isinstance(value, Phase):
return value
if isinstance(value, str):
try:
return Phase(value.strip().lower())
except ValueError as exc:
raise ValueError(f"unknown phase value: {value!r}") from exc
raise TypeError(f"phase must be Phase or str, got {type(value).__name__}")
+811
View File
@@ -0,0 +1,811 @@
"""Phase-transition hooks.
Each hook subscribes to `on_phase_transition` from `phase.py` and is
responsible for ONE downstream layer:
sync_task_status_hook → tasks.status follows work_item.phase
signal_dispatcher_hook → kick the dispatcher loop awake when a
transition opens new dispatchable work
refresh_dependents_hook → propagate child terminal/escalation
transitions up the dep graph so stuck
leaders wake (Fix 3)
Phase B note: the old ``wake_parent_on_resume_hook`` /
``sync_member_session_hook`` / ``_REENQUEUE_WORK_ITEM_HOOKS`` /
``_RUNTIME_RECONCILER_HOOKS`` machinery has been removed. Parent wake
and rework dispatch are now handled by the dispatcher's per-tick
rehydrate pass (``CompanyMode._execute_multi_team_org`` unparks stale
member sessions + re-enqueues runnable work items from the DB every
iteration). Keeping the DB as the single source of truth and having
the dispatcher converge on each tick replaces three layers of
cross-layer sync hooks — much less to break.
"""
from __future__ import annotations
from typing import Any
from loguru import logger
from opc.core.models import Phase, TaskStatus
from opc.layer2_organization.phase import (
DONE_PHASES,
RUNNABLE_PHASES,
register_phase_transition_hook,
task_status_for_phase,
)
from opc.layer2_organization.work_item_transition import refresh_dependents_for_run
# Module-level singleton for dispatcher signalling. CompanyMode populates
# this in its constructor with a callable that sets the wake event. We
# keep it as a list (rather than a single callable) so multiple engines
# can coexist (rare, but happens in tests).
_DISPATCHER_WAKE_HOOKS: list[Any] = []
def register_dispatcher_wake(callback: Any) -> None:
"""CompanyMode calls this to register its `_signal_dispatcher_wake`
so the phase-transition hook can fire it without an import cycle."""
if callback not in _DISPATCHER_WAKE_HOOKS:
_DISPATCHER_WAKE_HOOKS.append(callback)
def unregister_dispatcher_wake(callback: Any) -> None:
try:
_DISPATCHER_WAKE_HOOKS.remove(callback)
except ValueError:
pass
# ── Hook 1: task.status follows work_item.phase ──────────────────────────
async def sync_task_status_hook(
previous: Phase | None,
target: Phase,
item: Any,
*,
store: Any,
) -> None:
"""Whenever a work-item phase changes, project the new TaskStatus and
persist it on the linked task. Without this, task.status drifts from
work_item.phase (the bug behind the app04 deadlock — task remained
BLOCKED even after work_item moved WAITING_FOR_CHILDREN → RUNNING).
Company-mode boundary: this hook keeps the internal runtime Task aligned
with the WorkItem; it does not make Task a second business-state owner.
"""
# Even idempotent work-item saves should project the phase back onto
# the linked task. This repairs task/work_item drift when an older
# task row is still awaiting review after the work item already
# reached a terminal phase.
if not hasattr(store, "get_task") or not hasattr(store, "save_task"):
return
try:
task = None
get_runtime_task = getattr(store, "get_runtime_task_for_work_item", None)
if callable(get_runtime_task):
task = await get_runtime_task(str(getattr(item, "work_item_id", "") or "").strip())
except Exception:
return
if task is None:
return
desired_status = task_status_for_phase(target)
if task.status == desired_status:
return
task.status = desired_status
try:
await store.save_task(task)
except Exception:
logger.opt(exception=True).debug("sync_task_status_hook: save_task failed")
# ── Hook 2: kick the dispatcher when a transition opens dispatchable work ─
_DISPATCHER_WAKE_TARGETS = RUNNABLE_PHASES | DONE_PHASES
async def signal_dispatcher_hook(
previous: Phase | None,
target: Phase,
item: Any,
*,
store: Any,
) -> None:
"""Fire the dispatcher's wake event whenever a phase transition
opens the door to new work or unblocks downstream dependents.
Specifically: any transition to RUNNABLE_PHASES (READY /
READY_FOR_REWORK) or DONE_PHASES (children-done propagation) should
immediately ping the loop. Without this, the dispatcher only sees
the change on its next periodic tick (slower UX).
"""
if previous == target:
return
if target not in _DISPATCHER_WAKE_TARGETS and target != Phase.RUNNING:
# Allow RUNNING because parent-unblock transitions to RUNNING and
# the dispatcher needs to re-pick the parent.
return
for cb in list(_DISPATCHER_WAKE_HOOKS):
try:
cb()
except Exception:
logger.opt(exception=True).debug("dispatcher wake callback raised")
# ── Hook 3: propagate child terminal/escalation to parent dep frontier ──
# Which transitions can unblock or re-evaluate a parent.
# APPROVED — may make parent's all_approved true → WAITING_FOR_CHILDREN → RUNNING.
# FAILED / CANCELLED — parent should see the child exited; same refresh pass
# rewrites waiting_on_work_item_ids and lets higher-level policy decide next.
# AWAITING_HUMAN — a human needs to act on this child; the frontier refresh
# keeps parent metadata consistent (e.g. waiting_on_work_item_ids). When the
# human subsequently approves (AWAITING_HUMAN → APPROVED), that transition
# also fires this hook and finally unblocks the parent.
# READY_FOR_REWORK — child was sent back to worker by reviewer. Parent's
# waiting_on_work_item_ids is stale in the opposite direction (the child is
# no longer "done" from parent's perspective) and parent's claim may be
# holding the parent's session hostage. Refresh keeps the frontier accurate
# and triggers claim release on wake (see clear_claim_on_wake in
# work_item_transition.refresh_dependents_for_run).
_DEPENDENT_REFRESH_TARGETS: frozenset[Phase] = DONE_PHASES | frozenset({
Phase.AWAITING_HUMAN,
Phase.READY_FOR_REWORK,
})
async def refresh_dependents_hook(
previous: Phase | None,
target: Phase,
item: Any,
*,
store: Any,
) -> None:
"""Fix 3 — single entry point for parent dep-frontier updates.
Before this hook existed, ``_refresh_delegation_dependents`` was
only called on the APPROVED-verdict branch of
``_finalize_review_work_item``. A child escalating to AWAITING_HUMAN
(max_review_reworks exceeded) or being CANCELLED never triggered
the refresh, so its parent's ``waiting_on_work_item_ids`` and claim
state drifted from reality. new16/app12 reproduced this: cto parent
``cdb248d8`` sat in WAITING_FOR_CHILDREN for 13+ minutes with the
claim held by an idle session, neither runnable nor orphaned.
Re-entrancy is handled inside ``refresh_dependents_for_run`` via a
ContextVar, so transitive updates (parent → RUNNING → some ancestor)
don't re-walk the same run.
"""
if previous == target:
return
if target not in _DEPENDENT_REFRESH_TARGETS:
return
run_id = str(getattr(item, "run_id", "") or "").strip()
if not run_id:
return
try:
await refresh_dependents_for_run(
store,
run_id=run_id,
source_work_item_id=str(getattr(item, "work_item_id", "") or "").strip() or None,
source_role_id=str(getattr(item, "role_id", "") or "").strip() or None,
source_cell_id=str(getattr(item, "cell_id", "") or "").strip() or None,
)
except Exception:
logger.opt(exception=True).debug(
"refresh_dependents_hook: refresh_dependents_for_run raised"
)
# ── Hook 4: clear stale role-session focus when work items terminate ─────
# Firing scope — any transition into a terminal DB phase. We deliberately
# do NOT include AWAITING_HUMAN: a human still needs to act on those, and
# the session that escalated is legitimately still "focused" on the item
# until the human resolves it.
_FOCUS_CLEAR_TARGETS: frozenset[Phase] = DONE_PHASES
async def clear_session_focus_on_terminal_hook(
previous: Phase | None,
target: Phase,
item: Any,
*,
store: Any,
) -> None:
"""Null out ``focused_work_item_id`` on any role_runtime_session still
pointing at ``item`` once it reaches a terminal phase. Also demotes
``status='blocked'`` → ``'idle'`` for those sessions so the UI and
dispatcher rehydrate pass no longer see a "CTO blocked on X" row
after X has been approved / failed / cancelled.
Why a hook and not a routine cleanup in ``complete_claim``:
``complete_claim`` only runs on the claim-release path. Work items
transitioning via review verdict, migration, or direct store writes
bypass it, leaving session.focused_work_item_id stale. new16/app13
reproduced the symptom on two leader sessions (cto / coo) that kept
``status=blocked`` + focus on APPROVED work items for ~30 minutes.
Not functionally blocking — the dispatcher keys off work_item.phase,
not session focus — but the UI mislead users and the per-tick
rehydrate pass did dead work on the stale rows.
Fix 5 PR3: when the serial-queue flag is on, after clearing focus
we reconcile the role's pending queue and clear the next valid
``queued_behind_session`` stamp so the dispatcher picks it up.
"""
if previous == target:
return
if target not in _FOCUS_CLEAR_TARGETS:
return
wid = str(getattr(item, "work_item_id", "") or "").strip()
if not wid:
return
run_id = str(getattr(item, "run_id", "") or "").strip()
if not run_id:
return
if not hasattr(store, "list_role_runtime_sessions") or not hasattr(
store, "update_delegation_role_session"
):
return
try:
sessions = await store.list_role_runtime_sessions(run_id)
except Exception:
logger.opt(exception=True).debug(
"clear_session_focus_on_terminal_hook: list_role_runtime_sessions failed "
f"run_id={run_id}"
)
return
queue_enabled = bool(getattr(store, "role_serial_queue_enabled", False))
affected_role_session_ids: set[str] = set()
item_role_session_id = str(
getattr(item, "role_runtime_session_id", "") or ""
).strip()
if item_role_session_id:
affected_role_session_ids.add(item_role_session_id)
for session in sessions:
if str(getattr(session, "focused_work_item_id", "") or "") != wid:
continue
sid = str(getattr(session, "role_session_id", "") or "").strip()
if sid:
affected_role_session_ids.add(sid)
current_status = str(getattr(session, "status", "") or "").strip().lower()
status_override = "idle" if current_status == "blocked" else None
try:
await store.update_delegation_role_session(
session.role_session_id,
focused_work_item_id="",
status=status_override,
)
except Exception:
logger.opt(exception=True).debug(
"clear_session_focus_on_terminal_hook: update_delegation_role_session "
f"failed session_id={session.role_session_id}"
)
continue
if queue_enabled and affected_role_session_ids:
# PR3 originally promoted only when the terminal item was still
# focused by the session. In practice the focus may already have
# been cleared by a different path while the queued marker remains
# on the next card. Reconcile the assigned role session either way.
await reconcile_role_serial_queues(
store,
run_id,
role_session_ids=affected_role_session_ids,
)
def _work_item_id(item: Any) -> str:
return str(getattr(item, "work_item_id", "") or "").strip()
def _work_item_role_session_id(item: Any) -> str:
return str(getattr(item, "role_runtime_session_id", "") or "").strip()
def _queued_behind_session(item: Any) -> str:
metadata = getattr(item, "metadata", {}) or {}
if not isinstance(metadata, dict):
return ""
return str(metadata.get("queued_behind_session", "") or "").strip()
def _queue_candidate(item: Any) -> bool:
if getattr(item, "phase", None) not in RUNNABLE_PHASES:
return False
claimed = str(
getattr(item, "claimed_by_role_runtime_session_id", "") or ""
).strip()
return not claimed
async def _clear_queued_marker(
item: Any,
store: Any,
*,
expected_session_id: str | None = None,
) -> bool:
metadata = dict(getattr(item, "metadata", {}) or {})
marker = str(metadata.get("queued_behind_session", "") or "").strip()
if not marker:
return False
if expected_session_id is not None and marker != str(expected_session_id or "").strip():
return False
metadata.pop("queued_behind_session", None)
item.metadata = metadata
try:
await store.save_delegation_work_item(item)
except Exception:
logger.opt(exception=True).debug(
f"clear queued_behind_session failed wid={_work_item_id(item)}"
)
return False
return True
async def _save_role_session(store: Any, session: Any) -> bool:
try:
await store.save_delegation_role_session(session)
return True
except Exception:
logger.opt(exception=True).debug(
"save_delegation_role_session failed during serial queue reconcile "
f"sid={getattr(session, 'role_session_id', '')}"
)
return False
def _active_focus_id(session: Any, work_item_by_id: dict[str, Any]) -> str:
focused = str(getattr(session, "focused_work_item_id", "") or "").strip()
if not focused:
return ""
focused_item = work_item_by_id.get(focused)
if focused_item is None:
return ""
if getattr(focused_item, "phase", None) in DONE_PHASES:
return ""
return focused
async def reconcile_role_serial_queues(
store: Any,
run_id: str,
*,
role_session_ids: set[str] | list[str] | tuple[str, ...] | None = None,
) -> dict[str, Any]:
"""Repair derived serial-queue state for a run.
``pending_work_item_ids`` and ``focused_work_item_id`` are the source of
truth. ``metadata.queued_behind_session`` is only a dispatcher filter, so
it must be cleared when it no longer matches the role session's queue.
"""
result: dict[str, Any] = {
"run_id": str(run_id or "").strip(),
"cleared_markers": [],
"pruned_pending_ids": [],
"promoted_work_item_ids": [],
"cleared_focus_session_ids": [],
}
if not bool(getattr(store, "role_serial_queue_enabled", False)):
return result
rid = result["run_id"]
if not rid:
return result
required = (
"list_role_runtime_sessions",
"list_delegation_work_items",
"save_delegation_role_session",
"save_delegation_work_item",
)
if any(not hasattr(store, name) for name in required):
return result
wanted_sessions = {
str(item).strip()
for item in list(role_session_ids or [])
if str(item).strip()
}
try:
all_sessions = await store.list_role_runtime_sessions(rid)
work_items = await store.list_delegation_work_items(rid)
except Exception:
logger.opt(exception=True).debug(
f"reconcile_role_serial_queues: load failed run_id={rid}"
)
return result
sessions = list(all_sessions)
if wanted_sessions:
sessions = [
session for session in sessions
if str(getattr(session, "role_session_id", "") or "").strip()
in wanted_sessions
]
session_by_id = {
str(getattr(session, "role_session_id", "") or "").strip(): session
for session in sessions
if str(getattr(session, "role_session_id", "") or "").strip()
}
all_session_ids = {
str(getattr(session, "role_session_id", "") or "").strip()
for session in all_sessions
if str(getattr(session, "role_session_id", "") or "").strip()
}
work_item_by_id = {
_work_item_id(item): item
for item in work_items
if _work_item_id(item)
}
pending_after_by_session: dict[str, list[str]] = {}
blocker_by_session: dict[str, str] = {}
for session in sessions:
sid = str(getattr(session, "role_session_id", "") or "").strip()
if not sid:
continue
original_pending = [
str(item).strip()
for item in list(getattr(session, "pending_work_item_ids", []) or [])
if str(item).strip()
]
original_focus = str(getattr(session, "focused_work_item_id", "") or "").strip()
active_focus = _active_focus_id(session, work_item_by_id)
session_changed = False
if original_focus and not active_focus:
session.focused_work_item_id = ""
session.status = "idle"
session_changed = True
result["cleared_focus_session_ids"].append(sid)
elif active_focus and str(getattr(session, "status", "") or "").strip().lower() == "idle":
session.status = "running"
session_changed = True
clean_pending: list[str] = []
seen_pending: set[str] = set()
for pending_id in original_pending:
if pending_id in seen_pending:
result["pruned_pending_ids"].append(pending_id)
continue
seen_pending.add(pending_id)
pending_item = work_item_by_id.get(pending_id)
if pending_item is None or not _queue_candidate(pending_item):
result["pruned_pending_ids"].append(pending_id)
if pending_item is not None and await _clear_queued_marker(
pending_item,
store,
expected_session_id=sid,
):
result["cleared_markers"].append(pending_id)
continue
clean_pending.append(pending_id)
front_unqueued = ""
if not active_focus:
for candidate in work_items:
candidate_id = _work_item_id(candidate)
if not candidate_id or candidate_id in clean_pending:
continue
if _work_item_role_session_id(candidate) != sid:
continue
if not _queue_candidate(candidate):
continue
if _queued_behind_session(candidate):
continue
front_unqueued = candidate_id
break
promoted = ""
if not active_focus and not front_unqueued and clean_pending:
promoted = clean_pending.pop(0)
promoted_item = work_item_by_id.get(promoted)
if promoted_item is not None and await _clear_queued_marker(
promoted_item,
store,
expected_session_id=sid,
):
result["cleared_markers"].append(promoted)
result["promoted_work_item_ids"].append(promoted)
if clean_pending != original_pending:
session.pending_work_item_ids = clean_pending
session_changed = True
if not active_focus and not front_unqueued and not promoted:
if str(getattr(session, "status", "") or "").strip().lower() != "idle":
session.status = "idle"
session.focused_work_item_id = ""
session_changed = True
if session_changed:
await _save_role_session(store, session)
pending_after_by_session[sid] = list(clean_pending)
blocker_by_session[sid] = active_focus or front_unqueued or promoted
for item in work_items:
item_id = _work_item_id(item)
marker = _queued_behind_session(item)
if not item_id or not marker:
continue
if wanted_sessions and marker not in wanted_sessions:
continue
valid = False
if marker in all_session_ids:
pending = pending_after_by_session.get(marker)
if pending is None and not wanted_sessions:
session = session_by_id.get(marker)
pending = list(getattr(session, "pending_work_item_ids", []) or []) if session is not None else []
blocker = blocker_by_session.get(marker, "")
valid = item_id in list(pending or []) and bool(blocker) and blocker != item_id
if valid:
continue
if await _clear_queued_marker(item, store, expected_session_id=marker):
result["cleared_markers"].append(item_id)
if (
result["cleared_markers"]
or result["pruned_pending_ids"]
or result["promoted_work_item_ids"]
or result["cleared_focus_session_ids"]
):
logger.debug(f"serial queue reconciled: {result}")
if hasattr(store, "save_runtime_event"):
try:
await store.save_runtime_event(
rid,
"serial_queue_reconciled",
result,
)
except Exception:
logger.opt(exception=True).debug(
f"serial_queue_reconciled event emit failed run_id={rid}"
)
if result["promoted_work_item_ids"]:
for cb in list(_DISPATCHER_WAKE_HOOKS):
try:
cb()
except Exception:
logger.opt(exception=True).debug("dispatcher wake callback raised")
return result
async def _promote_next_pending_for_session(session: Any, store: Any) -> None:
"""Compatibility helper that reconciles one session's serial queue.
The reconciler prunes dead entries before promoting the next runnable
item, so this wrapper is safer than blindly popping the FIFO head.
"""
sid = str(getattr(session, "role_session_id", "") or "").strip()
if not sid:
return
run_id = str(getattr(session, "run_id", "") or "").strip()
if not run_id:
return
await reconcile_role_serial_queues(store, run_id, role_session_ids={sid})
# ── Hook 5 (Fix 5 PR3): enqueue runnable work for busy sessions ──────────
_RUNNABLE_ENQUEUE_TARGETS: frozenset[Phase] = RUNNABLE_PHASES
async def enqueue_session_work_on_runnable_hook(
previous: Phase | None,
target: Phase,
item: Any,
*,
store: Any,
) -> None:
"""When a work item becomes runnable for a role whose session is
already focused on something else, append it to the session's pending
queue and stamp ``metadata.queued_behind_session`` so the dispatcher
skips it until its turn.
Gated by ``store.role_serial_queue_enabled``. The flag remains available
for explicit compatibility tests, but company mode enables serial role
queues by default.
Design notes:
* The enqueue is idempotent. If the work item is already in the
queue, ``enqueue_pending_work_item`` returns False and this hook
skips the metadata stamp. That handles the case where the hook
fires twice on the same item (e.g. READY → RUNNING → READY_FOR_REWORK
cycling) without double-enqueuing.
* We deliberately leave the work item's phase alone — it stays
READY/READY_FOR_REWORK so the dispatcher still considers it, and
the claim filter (PR3.4) uses the ``queued_behind_session`` stamp
to decide whether to skip.
* No work is enqueued when the session's own focus IS this work item
(transient race during a claim — the session just grabbed the item
and its phase transitioned to RUNNING then back). That's handled by
the focus check: if ``session.focused_work_item_id == wid`` the
session is NOT busy with a different item, so we don't enqueue.
"""
if previous == target:
return
if not bool(getattr(store, "role_serial_queue_enabled", False)):
return
if target not in _RUNNABLE_ENQUEUE_TARGETS:
return
wid = str(getattr(item, "work_item_id", "") or "").strip()
if not wid:
return
sid = str(getattr(item, "role_runtime_session_id", "") or "").strip()
if not sid:
# No session stamped on the work item yet — the engine's bootstrap
# will stamp it before dispatch. Skip; we'll see the next
# transition once the session id is available.
return
# Is the session busy with a different work item?
try:
session = await store.get_delegation_role_session(sid)
except Exception:
logger.opt(exception=True).debug(
f"enqueue_session_work_on_runnable_hook: get_delegation_role_session "
f"failed sid={sid}"
)
return
if session is None:
return
focused = str(getattr(session, "focused_work_item_id", "") or "").strip()
if not focused or focused == wid:
# Session is idle or already holding this exact work item — let
# the normal claim path proceed without queueing.
return
# Append to queue, stamp metadata so the claim filter skips this wid.
try:
enqueued = await store.enqueue_pending_work_item(sid, wid)
except Exception:
logger.opt(exception=True).debug(
f"enqueue_session_work_on_runnable_hook: enqueue failed "
f"sid={sid} wid={wid}"
)
return
if not enqueued:
return # already in the queue — nothing more to do
metadata = dict(getattr(item, "metadata", {}) or {})
if metadata.get("queued_behind_session") == sid:
return
metadata["queued_behind_session"] = sid
try:
await store.update_delegation_work_item(
wid, metadata_updates={"queued_behind_session": sid}
)
except Exception:
logger.opt(exception=True).debug(
f"enqueue_session_work_on_runnable_hook: update_delegation_work_item "
f"failed wid={wid}"
)
# Fix 5 PR7 observability: emit a runtime event when the queue
# crosses the attention threshold. Fires at the *crossing* so we
# don't spam on every subsequent enqueue — dedup handled by checking
# that the queue length equals the threshold exactly after this
# enqueue (previous state was threshold-1 or lower, now we're at
# threshold). ``STUCK_QUEUE_DEPTH_THRESHOLD`` is conservative;
# operators can watch for the event and investigate before a true
# pile-up develops.
try:
refreshed = await store.get_delegation_role_session(sid)
except Exception:
refreshed = None
if refreshed is not None:
depth = len(list(getattr(refreshed, "pending_work_item_ids", []) or []))
if depth == STUCK_QUEUE_DEPTH_THRESHOLD and hasattr(store, "save_runtime_event"):
try:
await store.save_runtime_event(
sid,
"stuck_session_queue_depth",
{
"role_session_id": sid,
"role_id": str(getattr(refreshed, "role_id", "") or ""),
"run_id": str(getattr(refreshed, "run_id", "") or ""),
"focused_work_item_id": str(
getattr(refreshed, "focused_work_item_id", "") or ""
),
"queue_depth": depth,
"threshold": STUCK_QUEUE_DEPTH_THRESHOLD,
"triggered_by_work_item_id": wid,
},
)
except Exception:
logger.opt(exception=True).debug(
f"PR7 stuck-queue event emit failed sid={sid}"
)
# ── Fix 5 PR7 thresholds ─────────────────────────────────────────────────
# Queue-depth threshold: when a role's pending queue first reaches this,
# we emit ``stuck_session_queue_depth``. Chosen conservatively — with the
# serial queue on, a handful of pending items is normal during a delegation
# wave; 5 in flight to one role means upstream is producing faster than
# the role can drain and ops should look.
STUCK_QUEUE_DEPTH_THRESHOLD = 5
# Stuck-focus threshold (minutes): when a session has held the same
# ``focused_work_item_id`` for longer than this without a phase transition,
# ``check_stuck_focused_sessions`` emits a ``stuck_session_focused`` event.
# This is a poll-style check; call it periodically from a dispatcher tick
# or from an ops CLI (not auto-fired by the phase hooks).
STUCK_FOCUS_MINUTES = 10
async def check_stuck_focused_sessions(
store: Any,
*,
run_id: str | None = None,
threshold_minutes: int = STUCK_FOCUS_MINUTES,
) -> list[dict[str, Any]]:
"""Scan role_runtime_sessions whose ``focused_work_item_id`` has been
set for longer than ``threshold_minutes`` and emit one
``stuck_session_focused`` runtime event per offender.
Returns a list of the emitted event payloads for ops visibility.
Safe to call on every tick — events are cheap inserts and the dedup
is done by the consumer (downstream dashboards filter by last-seen-time).
"""
from datetime import datetime, timedelta
if not hasattr(store, "list_role_runtime_sessions"):
return []
if run_id:
sessions = await store.list_role_runtime_sessions(run_id)
else:
# Walk every known run. The store exposes per-run listing only,
# so gather run_ids from delegation_runs first.
if not hasattr(store, "list_delegation_runs"):
return []
try:
runs = await store.list_delegation_runs()
except Exception:
return []
sessions = []
for run in runs:
try:
sessions.extend(await store.list_role_runtime_sessions(run.run_id))
except Exception:
continue
threshold = timedelta(minutes=max(0, int(threshold_minutes)))
now = datetime.now()
emitted: list[dict[str, Any]] = []
for session in sessions:
focused = str(getattr(session, "focused_work_item_id", "") or "").strip()
if not focused:
continue
updated_at = getattr(session, "updated_at", None)
if not isinstance(updated_at, datetime):
continue
if now - updated_at < threshold:
continue
payload = {
"role_session_id": session.role_session_id,
"role_id": getattr(session, "role_id", ""),
"run_id": getattr(session, "run_id", ""),
"focused_work_item_id": focused,
"focused_for_minutes": int((now - updated_at).total_seconds() // 60),
"threshold_minutes": int(threshold_minutes),
"queue_depth": len(list(getattr(session, "pending_work_item_ids", []) or [])),
}
if hasattr(store, "save_runtime_event"):
try:
await store.save_runtime_event(
session.role_session_id,
"stuck_session_focused",
payload,
)
except Exception:
logger.opt(exception=True).debug(
f"PR7 stuck-focus event emit failed "
f"sid={session.role_session_id}"
)
emitted.append(payload)
return emitted
# ── Module-level registration ────────────────────────────────────────────
register_phase_transition_hook(sync_task_status_hook)
register_phase_transition_hook(signal_dispatcher_hook)
register_phase_transition_hook(refresh_dependents_hook)
register_phase_transition_hook(clear_session_focus_on_terminal_hook)
# Fix 5 PR3 — gated internally on ``store.role_serial_queue_enabled``,
# always-registered so flipping the flag at runtime takes effect without
# re-wiring the hook list.
register_phase_transition_hook(enqueue_session_work_on_runnable_hook)
+318
View File
@@ -0,0 +1,318 @@
"""Canonical prompt contract for company-mode WorkItems.
The renderer consumes this contract directly. Legacy fields such as
``summary``/``brief`` remain useful for UI and audit, but prompt assembly
should not re-derive its own packet from them after a contract exists.
"""
from __future__ import annotations
import copy
from typing import Any
PROMPT_CONTRACT_VERSION = 2
def normalize_prompt_text_list(value: Any) -> list[str]:
if value is None:
return []
if isinstance(value, (list, tuple, set)):
items: list[str] = []
for item in value:
if isinstance(item, dict):
rendered = str(
item.get("value", "")
or item.get("input", "")
or item.get("raw", "")
or item.get("work_item_id", "")
or ""
).strip()
else:
rendered = str(item).strip()
if rendered:
items.append(rendered)
return items
rendered = str(value).strip()
return [rendered] if rendered else []
def normalize_dependency_specs(value: Any) -> list[Any]:
if value is None:
return []
if isinstance(value, (list, tuple, set)):
return [copy.deepcopy(item) for item in value if str(item).strip() or isinstance(item, dict)]
if isinstance(value, dict):
return [copy.deepcopy(value)]
rendered = str(value).strip()
return [rendered] if rendered else []
def normalize_prompt_contract(value: Any) -> dict[str, Any]:
payload = copy.deepcopy(value) if isinstance(value, dict) else {}
assignment = dict(payload.get("assignment_context", {}) or {})
try:
version = int(payload.get("version") or PROMPT_CONTRACT_VERSION)
except (TypeError, ValueError):
version = PROMPT_CONTRACT_VERSION
normalized = {
"version": version,
"task_brief": str(payload.get("task_brief", "") or "").strip(),
"assignment_context": {
"upstream_intent_summary": str(assignment.get("upstream_intent_summary", "") or "").strip(),
"manager_planning_handoff": str(assignment.get("manager_planning_handoff", "") or "").strip(),
"manager_outcome_dispatch": bool(assignment.get("manager_outcome_dispatch", False)),
"owned_outcome_kind": str(assignment.get("owned_outcome_kind", "") or "execute").strip() or "execute",
"scope_key": str(assignment.get("scope_key", "") or "").strip(),
"deliverables": normalize_prompt_text_list(assignment.get("deliverables", [])),
"acceptance_criteria": normalize_prompt_text_list(assignment.get("acceptance_criteria", [])),
"dependency_specs": normalize_dependency_specs(assignment.get("dependency_specs", [])),
"coordination_notes": str(assignment.get("coordination_notes", "") or "").strip(),
"delegation_rationale": str(assignment.get("delegation_rationale", "") or "").strip(),
"non_overlap_guard": str(assignment.get("non_overlap_guard", "") or "").strip(),
},
"turn_profiles": copy.deepcopy(dict(payload.get("turn_profiles", {}) or {})),
}
if payload.get("target_contract"):
normalized["target_contract"] = normalize_prompt_contract(payload.get("target_contract"))
source = dict(payload.get("source", {}) or {})
if source:
normalized["source"] = copy.deepcopy(source)
return normalized
def has_prompt_contract(value: Any) -> bool:
contract = normalize_prompt_contract(value)
return bool(contract.get("task_brief") or any(_assignment_has_content(contract)))
def _assignment_has_content(contract: dict[str, Any]) -> list[Any]:
assignment = dict(contract.get("assignment_context", {}) or {})
return [
assignment.get("upstream_intent_summary"),
assignment.get("manager_planning_handoff"),
assignment.get("manager_outcome_dispatch"),
assignment.get("scope_key"),
assignment.get("deliverables"),
assignment.get("acceptance_criteria"),
assignment.get("dependency_specs"),
assignment.get("coordination_notes"),
assignment.get("delegation_rationale"),
assignment.get("non_overlap_guard"),
]
def make_prompt_contract(
*,
task_brief: str,
upstream_intent_summary: str = "",
manager_planning_handoff: str = "",
manager_outcome_dispatch: bool = False,
owned_outcome_kind: str = "execute",
scope_key: str = "",
deliverables: Any = None,
acceptance_criteria: Any = None,
dependency_specs: Any = None,
coordination_notes: str = "",
delegation_rationale: str = "",
non_overlap_guard: str = "",
turn_profiles: dict[str, Any] | None = None,
target_contract: dict[str, Any] | None = None,
source: dict[str, Any] | None = None,
) -> dict[str, Any]:
payload: dict[str, Any] = {
"version": PROMPT_CONTRACT_VERSION,
"task_brief": str(task_brief or "").strip(),
"assignment_context": {
"upstream_intent_summary": str(upstream_intent_summary or "").strip(),
"manager_planning_handoff": str(manager_planning_handoff or "").strip(),
"manager_outcome_dispatch": bool(manager_outcome_dispatch),
"owned_outcome_kind": str(owned_outcome_kind or "execute").strip() or "execute",
"scope_key": str(scope_key or "").strip(),
"deliverables": normalize_prompt_text_list(deliverables),
"acceptance_criteria": normalize_prompt_text_list(acceptance_criteria),
"dependency_specs": normalize_dependency_specs(dependency_specs),
"coordination_notes": str(coordination_notes or "").strip(),
"delegation_rationale": str(delegation_rationale or "").strip(),
"non_overlap_guard": str(non_overlap_guard or "").strip(),
},
"turn_profiles": copy.deepcopy(dict(turn_profiles or {})),
}
if target_contract:
payload["target_contract"] = normalize_prompt_contract(target_contract)
if source:
payload["source"] = copy.deepcopy(dict(source))
return normalize_prompt_contract(payload)
def make_prompt_contract_blocker(reason: str) -> dict[str, Any]:
return make_prompt_contract(
task_brief="SYSTEM BLOCKER: prompt_contract is missing or incomplete for this WorkItem.",
deliverables=["Do not execute the original task until a valid prompt_contract exists."],
acceptance_criteria=[str(reason or "Missing prompt contract.").strip()],
source={"kind": "prompt_contract_blocker"},
)
def prompt_contract_from_delegate_item(
item: dict[str, Any],
*,
task_brief: str,
upstream_intent_summary: str = "",
manager_planning_handoff: str = "",
manager_outcome_dispatch: bool = False,
owned_outcome_kind: str = "execute",
scope_key: str = "",
dependency_specs: Any = None,
) -> dict[str, Any]:
return make_prompt_contract(
task_brief=task_brief,
upstream_intent_summary=upstream_intent_summary,
manager_planning_handoff=manager_planning_handoff,
manager_outcome_dispatch=manager_outcome_dispatch,
owned_outcome_kind=owned_outcome_kind,
scope_key=scope_key,
deliverables=item.get("deliverables", item.get("outputs", [])),
acceptance_criteria=item.get("acceptance_criteria", item.get("done_when", [])),
dependency_specs=dependency_specs,
coordination_notes=str(item.get("coordination_notes", "") or "").strip(),
delegation_rationale=str(item.get("delegation_rationale", "") or "").strip(),
non_overlap_guard=str(item.get("non_overlap_guard", "") or "").strip(),
source={"kind": "delegate_work"},
)
def prompt_contract_from_work_item(
work_item: Any,
*,
task_metadata: dict[str, Any] | None = None,
task_description: str = "",
) -> dict[str, Any]:
"""Build a one-time compatibility contract outside the renderer."""
metadata = dict(getattr(work_item, "metadata", {}) or {})
task_metadata = dict(task_metadata or {})
existing = metadata.get("prompt_contract") or task_metadata.get("prompt_contract")
if has_prompt_contract(existing):
return normalize_prompt_contract(existing)
legacy_assignment = dict(metadata.get("prompt_assignment", {}) or task_metadata.get("prompt_assignment", {}) or {})
task_brief = str(
legacy_assignment.get("task_brief", "")
or legacy_assignment.get("primary_task_brief", "")
or metadata.get("brief", "")
or getattr(work_item, "summary", "")
or task_description
or getattr(work_item, "title", "")
or ""
).strip()
if not task_brief:
return make_prompt_contract_blocker(
f"WorkItem `{str(getattr(work_item, 'work_item_id', '') or '').strip()}` has no task_brief."
)
playbook = dict(metadata.get("delegation_playbook", {}) or task_metadata.get("delegation_playbook", {}) or {})
return make_prompt_contract(
task_brief=task_brief,
upstream_intent_summary=str(
legacy_assignment.get("upstream_intent_summary", "")
or metadata.get("upstream_intent_summary", "")
or task_metadata.get("global_intent_summary", "")
or playbook.get("global_intent_summary", "")
or playbook.get("intent_summary", "")
or ""
).strip(),
manager_planning_handoff=str(
legacy_assignment.get("manager_planning_handoff", "")
or metadata.get("manager_planning_handoff", "")
or metadata.get("planning_context", "")
or ""
).strip(),
manager_outcome_dispatch=bool(
legacy_assignment.get("manager_outcome_dispatch", metadata.get("manager_outcome_dispatch", False))
),
owned_outcome_kind=str(
legacy_assignment.get("owned_outcome_kind", metadata.get("owned_outcome_kind", getattr(work_item, "kind", "execute")))
or "execute"
).strip(),
scope_key=str(legacy_assignment.get("scope_key", metadata.get("scope_key", "")) or "").strip(),
deliverables=legacy_assignment.get("deliverables", legacy_assignment.get("outputs", metadata.get("deliverables", metadata.get("outputs", [])))),
acceptance_criteria=legacy_assignment.get(
"acceptance_criteria",
legacy_assignment.get("done_when", metadata.get("acceptance_criteria", metadata.get("done_when", []))),
),
dependency_specs=legacy_assignment.get(
"dependency_specs",
legacy_assignment.get("dependency_work_item_ids", metadata.get("dependency_specs", metadata.get("dependency_work_item_ids", []))),
),
coordination_notes=str(legacy_assignment.get("coordination_notes", metadata.get("coordination_notes", "")) or "").strip(),
delegation_rationale=str(legacy_assignment.get("delegation_rationale", metadata.get("delegation_rationale", "")) or "").strip(),
non_overlap_guard=str(legacy_assignment.get("non_overlap_guard", metadata.get("non_overlap_guard", "")) or "").strip(),
source={"kind": "normalized_legacy_work_item"},
)
def render_assignment_context_from_contract(
contract: dict[str, Any],
*,
include_dispatch_fields: bool = False,
) -> str:
contract = normalize_prompt_contract(contract)
assignment = dict(contract.get("assignment_context", {}) or {})
lines: list[str] = ["## Work Item Assignment Context"]
if include_dispatch_fields and assignment.get("manager_outcome_dispatch"):
owned_kind = str(assignment.get("owned_outcome_kind") or "execute").strip()
lines.append(
"Manager outcome turn: you own the final outcome, but this turn "
"starts with delegation to direct reports before local integration. "
f"Owned outcome kind: {owned_kind}. Do not execute the production "
"work yourself in this dispatch turn unless no downstream seat is a fit."
)
if assignment.get("upstream_intent_summary"):
lines.extend(["", "### Upstream Intent Summary", str(assignment["upstream_intent_summary"])])
if include_dispatch_fields and assignment.get("manager_planning_handoff"):
lines.extend(["", "### Manager Planning Handoff", str(assignment["manager_planning_handoff"])])
if assignment.get("scope_key"):
lines.extend(["", "### Scope Key", str(assignment["scope_key"])])
for title, key in (
("Deliverables", "deliverables"),
("Acceptance Criteria", "acceptance_criteria"),
("Dependencies", "dependency_specs"),
):
items = normalize_prompt_text_list(assignment.get(key, []))
if items:
lines.extend(["", f"### {title}"])
lines.extend(f"- {item}" for item in items)
for title, key in (
("Coordination Notes", "coordination_notes"),
("Delegation Rationale", "delegation_rationale"),
("Boundaries / Non-overlap Guard", "non_overlap_guard"),
):
value = str(assignment.get(key, "") or "").strip()
if value:
lines.extend(["", f"### {title}", value])
if len(lines) == 1:
return ""
return "\n".join(lines).strip()
def render_target_prompt_contract(contract: dict[str, Any], *, heading: str = "### Target Work Item Contract") -> str:
contract = normalize_prompt_contract(contract)
lines: list[str] = [heading]
task_brief = str(contract.get("task_brief", "") or "").strip()
if task_brief:
lines.extend(["", "#### Task Brief", task_brief])
assignment = render_assignment_context_from_contract(contract, include_dispatch_fields=True)
if assignment:
assignment = assignment.replace("## Work Item Assignment Context", "#### Assignment Context", 1)
lines.extend(["", assignment])
if len(lines) == 1:
return ""
return "\n".join(lines).strip()
def is_report_prompt_turn(metadata: dict[str, Any] | None) -> bool:
payload = dict(metadata or {})
return bool(
payload.get("report_execution_work_item")
or str(payload.get("current_turn_mode", "") or "").strip() == "report_required"
or str(payload.get("work_item_turn_type", "") or "").strip() == "report"
or str(payload.get("work_kind", "") or "").strip() == "report"
)
@@ -0,0 +1,137 @@
"""Background sweeper that re-opens DONE tasks when new actionable mail arrives.
The company-mode end-of-turn hook (``_reactivate_for_unread_mail``) already
catches the common case where mail arrived *before* a task finished, but it
only runs at task-completion boundaries. When a DONE task's role receives a
blocking/actionable DM afterwards, nothing spontaneously wakes the role — in
previous versions of OPC the gap was hidden by a main-LLM "impersonation
reply" fallback that let the sender unblock but never involved the role's own
agent.
This sweeper closes the gap without introducing new abstractions: every few
seconds it re-scans DONE tasks for the active project and calls the existing
``reactivate_fn`` (which reuses all the guard logic in
``CompanyWorkItemExecutor._reactivate_for_unread_mail`` — fingerprint check,
depth cap, cross-role ping-pong detection). The scheduler then picks the task
up naturally and the external_broker's session-resume path hands the agent
back a fully contextualized codex/claude_code session.
"""
from __future__ import annotations
import asyncio
from typing import Any, Awaitable, Callable
from loguru import logger
from opc.core.models import Task, TaskStatus
class CommsReactivationSweeper:
"""Periodic scan that re-opens DONE tasks whose role received new mail."""
def __init__(
self,
*,
store: Any,
project_id_getter: Callable[[], str | None],
reactivate_fn: Callable[[Task], Awaitable[bool]],
interval_sec: float = 10.0,
) -> None:
self.store = store
self.project_id_getter = project_id_getter
self.reactivate_fn = reactivate_fn
self.interval_sec = max(1.0, float(interval_sec))
self._running = False
self._task: asyncio.Task[None] | None = None
async def start(self) -> None:
if self._running:
return
if self.store is None:
logger.debug("CommsReactivationSweeper: no store, skipping start")
return
self._running = True
self._task = asyncio.create_task(self._tick_loop())
logger.info(
"CommsReactivationSweeper started (interval={}s)", self.interval_sec
)
async def stop(self) -> None:
self._running = False
if self._task is None:
return
self._task.cancel()
try:
await self._task
except asyncio.CancelledError:
pass
except Exception:
logger.exception("CommsReactivationSweeper stop error")
self._task = None
logger.info("CommsReactivationSweeper stopped")
async def _tick_loop(self) -> None:
while self._running:
try:
await self._tick()
except asyncio.CancelledError:
break
except Exception:
logger.exception("CommsReactivationSweeper tick error")
try:
await asyncio.sleep(self.interval_sec)
except asyncio.CancelledError:
break
async def _tick(self) -> None:
project_id = (self.project_id_getter() or "").strip()
if not project_id:
return
# Guard against a store that has been closed (e.g. during project
# switch, where engine rebinds ``self.store`` on the sweeper but a
# tick already in flight may still reference the previous handle)
# or a store that is reattaching its sqlite connection.
if getattr(self.store, "_db", None) is None:
return
try:
done_tasks = await self.store.get_tasks(
project_id=project_id,
status=TaskStatus.DONE,
)
except AssertionError:
# OPCStore raises AssertionError when queried while ``_db`` is
# None (closed). Treat as a transient no-op; the next tick will
# see the refreshed store.
return
except Exception:
logger.exception(
"CommsReactivationSweeper: failed to list DONE tasks for project={}",
project_id,
)
return
if not done_tasks:
return
reactivated_count = 0
for task in done_tasks:
try:
reactivated = await self.reactivate_fn(task)
except Exception:
logger.exception(
"CommsReactivationSweeper: reactivate_fn raised for task={}",
getattr(task, "id", ""),
)
continue
if reactivated:
reactivated_count += 1
logger.info(
"[comms_reactivation] sweep reactivated task={} role={}",
getattr(task, "id", ""),
str(getattr(task, "assigned_to", "") or "").strip(),
)
if reactivated_count:
logger.debug(
"CommsReactivationSweeper: tick reactivated {} task(s) for project={}",
reactivated_count,
project_id,
)
File diff suppressed because it is too large Load Diff
+488
View File
@@ -0,0 +1,488 @@
"""Runtime company reorganization orchestration."""
from __future__ import annotations
from datetime import datetime
from typing import Any, Callable, Coroutine
from opc.core.models import (
ApprovalAction,
OrgSnapshot,
ReorgChangeSet,
ReorgEventKind,
ReorgEventRecord,
ReorgMigrationPlan,
ReorgProposal,
ReorgProposalStatus,
ReorgRiskLevel,
ReorgRoleChange,
ReorgScope,
ReorgTaskAdjustment,
Task,
TaskStatus,
)
from opc.database.store import OPCStore
from opc.layer2_organization.approval import ApprovalEngine
from opc.layer2_organization.communication import CommunicationManager
from opc.layer2_organization.org_engine import OrgEngine
from opc.layer2_organization.work_item_identity import work_item_identity_payload_for_task
class ReorgManager:
"""Coordinates proposal, approval, application, and migration of runtime org changes."""
ACTIVE_TASK_STATUSES = {
TaskStatus.PENDING,
TaskStatus.BLOCKED,
TaskStatus.AWAITING_PEER,
TaskStatus.AWAITING_MANAGER_REVIEW,
TaskStatus.AWAITING_HUMAN,
TaskStatus.AWAITING_REVIEW,
}
def __init__(
self,
store: OPCStore,
org_engine: OrgEngine,
approval_engine: ApprovalEngine | None,
communication: CommunicationManager | None,
progress_callback: Callable[[str], Coroutine[Any, Any, None]] | None = None,
) -> None:
self.store = store
self.org_engine = org_engine
self.approval_engine = approval_engine
self.communication = communication
self.progress_callback = progress_callback
async def _emit_progress(self, message: str) -> None:
if self.progress_callback:
await self.progress_callback(message)
async def build_org_snapshot(self, project_id: str) -> OrgSnapshot:
tasks = await self.store.get_tasks(project_id=project_id)
active_tasks = [
{
"task_id": task.id,
"title": task.title,
"status": task.status.value,
"assigned_to": task.assigned_to,
**work_item_identity_payload_for_task(task),
"org_version": task.metadata.get("org_version", self.org_engine.current_org_version()),
"runtime_topology_version": task.metadata.get("runtime_topology_version", self.org_engine.current_runtime_topology_version()),
}
for task in tasks
if task.status in self.ACTIVE_TASK_STATUSES | {TaskStatus.RUNNING}
]
return self.org_engine.snapshot_org(project_id=project_id, active_tasks=active_tasks)
async def propose_reorg(
self,
*,
project_id: str,
summary: str,
rationale: str = "",
title: str = "",
initiated_by: str = "owner",
source_role_id: str = "",
changeset: ReorgChangeSet | dict[str, Any] | None = None,
scope: ReorgScope | None = None,
session_id: str | None = None,
task_id: str | None = None,
metadata: dict[str, Any] | None = None,
) -> ReorgProposal:
if isinstance(changeset, dict):
changeset = ReorgChangeSet(**changeset)
changeset = self._normalize_changeset(changeset or ReorgChangeSet())
scope = scope or self._infer_scope(changeset)
risk_level = self._classify_risk(scope, changeset)
snapshot = await self.build_org_snapshot(project_id)
await self.store.save_org_snapshot(snapshot)
migration_plan = await self._build_migration_plan(
project_id=project_id,
changeset=changeset,
snapshot=snapshot,
target_org_version=snapshot.org_version + (1 if scope == ReorgScope.ORG_MUTATION else 0),
)
proposal = ReorgProposal(
project_id=project_id,
session_id=session_id,
task_id=task_id,
initiated_by=initiated_by,
source_role_id=source_role_id,
scope=scope,
risk_level=risk_level,
status=ReorgProposalStatus.PROPOSED,
title=title or summary[:120],
summary=summary,
rationale=rationale or summary,
user_confirmation_required=(scope != ReorgScope.TASK_ADJUSTMENT or risk_level != ReorgRiskLevel.LOW),
old_org_version=snapshot.org_version,
new_org_version=migration_plan.metadata.get("target_org_version", snapshot.org_version),
old_runtime_topology_version=snapshot.runtime_topology_version,
new_runtime_topology_version=snapshot.runtime_topology_version,
changeset=changeset,
migration_plan=migration_plan,
impact_summary={
"affected_tasks": len(migration_plan.affected_task_ids),
"affected_checkpoints": len(migration_plan.affected_checkpoint_ids),
"role_mapping": migration_plan.role_mapping,
},
metadata=dict(metadata or {}),
)
await self.store.save_reorg_proposal(proposal)
await self.store.record_reorg_event(
ReorgEventRecord(
proposal_id=proposal.proposal_id,
project_id=project_id,
event_kind=ReorgEventKind.PROPOSED,
summary=proposal.summary,
details={
"scope": proposal.scope.value,
"risk_level": proposal.risk_level.value,
"changeset": proposal.changeset.__dict__,
},
)
)
return proposal
async def request_reorg_approval(self, proposal_id: str) -> tuple[bool, ReorgProposal]:
proposal = await self._require_proposal(proposal_id)
if not proposal.user_confirmation_required:
proposal.status = ReorgProposalStatus.APPROVED
proposal.updated_at = datetime.now()
await self.store.save_reorg_proposal(proposal)
return True, proposal
if not self.approval_engine:
raise RuntimeError("Approval engine is unavailable")
approval_task = Task(
id=proposal.task_id or proposal.proposal_id,
session_id=proposal.session_id,
project_id=proposal.project_id,
title=proposal.title,
description=proposal.summary,
assigned_to=proposal.source_role_id or "coordinator",
metadata={
"reorg_proposal_id": proposal.proposal_id,
"org_version": proposal.old_org_version,
"runtime_topology_version": proposal.old_runtime_topology_version,
},
)
approved, decision = await self.approval_engine.authorize_work_item_action(
task=approval_task,
work_item_title=f"reorg:{proposal.title or proposal.proposal_id}",
metadata={
"role_id": proposal.source_role_id or "owner",
"company_profile": self.org_engine.get_company_profile(),
"gate_type": "company_reorg",
"proposal_id": proposal.proposal_id,
"scope": proposal.scope.value,
"risk_level": proposal.risk_level.value,
},
on_progress=self.progress_callback,
force_human=True,
)
proposal.approval_notes = decision.rationale
proposal.status = ReorgProposalStatus.APPROVED if approved else ReorgProposalStatus.DENIED
proposal.updated_at = datetime.now()
await self.store.save_reorg_proposal(proposal)
await self.store.record_reorg_event(
ReorgEventRecord(
proposal_id=proposal.proposal_id,
project_id=proposal.project_id,
event_kind=ReorgEventKind.APPROVED if approved else ReorgEventKind.DENIED,
summary=decision.rationale,
details={
"approval_action": decision.action.value,
"risk_level": decision.risk_level.value,
"metadata": decision.metadata,
},
)
)
return approved, proposal
async def set_reorg_approval(self, proposal_id: str, approved: bool, notes: str = "") -> ReorgProposal:
proposal = await self._require_proposal(proposal_id)
proposal.status = ReorgProposalStatus.APPROVED if approved else ReorgProposalStatus.DENIED
proposal.approval_notes = notes or proposal.approval_notes
proposal.updated_at = datetime.now()
await self.store.save_reorg_proposal(proposal)
await self.store.record_reorg_event(
ReorgEventRecord(
proposal_id=proposal.proposal_id,
project_id=proposal.project_id,
event_kind=ReorgEventKind.APPROVED if approved else ReorgEventKind.DENIED,
summary=notes or proposal.summary,
details={"status": proposal.status.value},
)
)
return proposal
async def apply_reorg(self, proposal_id: str) -> dict[str, Any]:
proposal = await self._require_proposal(proposal_id)
if proposal.user_confirmation_required and proposal.status != ReorgProposalStatus.APPROVED:
raise ValueError("Proposal must be approved before apply.")
before_snapshot = await self.build_org_snapshot(proposal.project_id)
await self.store.save_org_snapshot(before_snapshot)
proposal.migration_plan.rollback_snapshot_id = before_snapshot.snapshot_id
change_result = self.org_engine.apply_changeset(
proposal.changeset,
persist=True,
) if proposal.scope == ReorgScope.ORG_MUTATION or proposal.changeset.role_changes else {
"old_org_version": self.org_engine.current_org_version(),
"new_org_version": self.org_engine.current_org_version(),
"role_mapping": {},
}
migration_summary = await self._migrate_active_state(proposal, change_result)
proposal.status = ReorgProposalStatus.APPLIED
proposal.old_org_version = change_result["old_org_version"]
proposal.new_org_version = change_result["new_org_version"]
proposal.migration_plan.role_mapping = dict(change_result.get("role_mapping", {}))
proposal.migration_plan.metadata["migration_summary"] = migration_summary
proposal.updated_at = datetime.now()
await self.store.save_reorg_proposal(proposal)
after_snapshot = await self.build_org_snapshot(proposal.project_id)
await self.store.save_org_snapshot(after_snapshot)
await self.store.record_reorg_event(
ReorgEventRecord(
proposal_id=proposal.proposal_id,
project_id=proposal.project_id,
event_kind=ReorgEventKind.APPLIED,
summary=proposal.summary,
details={
"change_result": change_result,
"migration_summary": migration_summary,
"snapshot_id": after_snapshot.snapshot_id,
},
)
)
return {
"proposal_id": proposal.proposal_id,
"status": proposal.status.value,
"migration_summary": migration_summary,
"change_result": change_result,
"snapshot_id": after_snapshot.snapshot_id,
}
async def suggest_task_adjustment(
self,
*,
project_id: str,
source_role_id: str,
summary: str,
changeset: ReorgChangeSet | dict[str, Any],
session_id: str | None = None,
task_id: str | None = None,
) -> dict[str, Any]:
proposal = await self.propose_reorg(
project_id=project_id,
summary=summary,
rationale=summary,
initiated_by=source_role_id,
source_role_id=source_role_id,
changeset=changeset,
scope=ReorgScope.TASK_ADJUSTMENT,
session_id=session_id,
task_id=task_id,
metadata={"auto_apply_candidate": True},
)
if proposal.risk_level == ReorgRiskLevel.LOW and self._is_top_level_role(source_role_id):
await self.set_reorg_approval(proposal.proposal_id, approved=True, notes="Auto-approved low-risk task adjustment.")
result = await self.apply_reorg(proposal.proposal_id)
await self.store.record_reorg_event(
ReorgEventRecord(
proposal_id=proposal.proposal_id,
project_id=proposal.project_id,
event_kind=ReorgEventKind.AUTO_TASK_ADJUSTED,
summary=summary,
details=result,
)
)
return {"proposal": proposal, "auto_applied": True, "result": result}
return {"proposal": proposal, "auto_applied": False}
async def _build_migration_plan(
self,
*,
project_id: str,
changeset: ReorgChangeSet,
snapshot: OrgSnapshot,
target_org_version: int,
) -> ReorgMigrationPlan:
tasks = await self.store.get_tasks(project_id=project_id)
checkpoints = await self.store.get_pending_checkpoints(project_id=project_id)
affected_tasks = [task.id for task in tasks if task.status in self.ACTIVE_TASK_STATUSES]
role_mapping: dict[str, str] = {}
for change in changeset.role_changes:
if change.action == "replace" and change.replacement_role_id:
role_mapping[change.role_id] = change.replacement_role_id
elif change.action == "remove":
role_mapping[change.role_id] = ""
warnings: list[str] = []
if any(task.status == TaskStatus.RUNNING for task in tasks):
warnings.append("Running tasks are not force-migrated and will continue until their current iteration completes.")
return ReorgMigrationPlan(
affected_task_ids=affected_tasks,
affected_checkpoint_ids=[checkpoint.checkpoint_id for checkpoint in checkpoints],
affected_handoff_ids=[],
role_mapping=role_mapping,
invalidated_waits=[],
migration_notes=[
f"Snapshot org_version={snapshot.org_version}.",
f"Target org_version={target_org_version}.",
],
compatibility_warnings=warnings,
metadata={
"target_org_version": target_org_version,
},
)
async def _migrate_active_state(self, proposal: ReorgProposal, change_result: dict[str, Any]) -> dict[str, Any]:
tasks = await self.store.get_tasks(project_id=proposal.project_id)
checkpoints = await self.store.get_pending_checkpoints(project_id=proposal.project_id)
migrated_task_ids: list[str] = []
migrated_checkpoint_ids: list[str] = []
role_mapping = dict(change_result.get("role_mapping", {}))
target_org_version = change_result.get("new_org_version", self.org_engine.current_org_version())
for task in tasks:
if task.status == TaskStatus.RUNNING:
task.metadata = dict(task.metadata)
task.metadata["migration_status"] = "pending_running_completion"
task.metadata["reorg_proposal_id"] = proposal.proposal_id
await self.store.save_task(task)
continue
if task.status not in self.ACTIVE_TASK_STATUSES:
continue
task.metadata = dict(task.metadata)
task.context_snapshot = dict(task.context_snapshot)
current_role = task.assigned_to or str(task.metadata.get("work_item_role_id", ""))
new_role = role_mapping.get(current_role, current_role)
if current_role and new_role and new_role != current_role:
task.assigned_to = new_role
task.metadata["work_item_role_id"] = new_role
elif current_role and new_role == "" and task.status in self.ACTIVE_TASK_STATUSES:
task.status = TaskStatus.CANCELLED
self._apply_task_adjustments(task, proposal.changeset)
peer_wait = dict(task.metadata.get("peer_wait", {}))
if peer_wait:
waiting_on = list(peer_wait.get("waiting_on_agents", []))
peer_wait["waiting_on_agents"] = [
role_mapping.get(agent_id, agent_id)
for agent_id in waiting_on
if role_mapping.get(agent_id, agent_id)
]
task.metadata["peer_wait"] = peer_wait
active_meeting = dict(task.context_snapshot.get("active_meeting", {}))
if active_meeting:
participants = list(active_meeting.get("participants", []))
if participants:
active_meeting["participants"] = [
role_mapping.get(agent_id, agent_id)
for agent_id in participants
if role_mapping.get(agent_id, agent_id)
]
task.context_snapshot["active_meeting"] = active_meeting
task.metadata["org_version"] = target_org_version
task.metadata["reorg_proposal_id"] = proposal.proposal_id
task.metadata["migration_status"] = "migrated"
task.metadata["superseded_by_reorg"] = proposal.proposal_id
task.context_snapshot["migration_reason"] = proposal.summary
task.context_snapshot["migration_role_mapping"] = role_mapping
task.context_snapshot["migration_handoff"] = {
"proposal_id": proposal.proposal_id,
"reason": proposal.summary,
"previous_role": current_role,
"current_role": task.assigned_to,
}
await self.store.save_task(task)
migrated_task_ids.append(task.id)
for checkpoint in checkpoints:
checkpoint.payload = dict(checkpoint.payload)
checkpoint.payload["org_version"] = target_org_version
checkpoint.payload["reorg_proposal_id"] = proposal.proposal_id
await self.store.save_execution_checkpoint(checkpoint)
migrated_checkpoint_ids.append(checkpoint.checkpoint_id)
proposal.migration_plan.affected_task_ids = migrated_task_ids
proposal.migration_plan.affected_checkpoint_ids = migrated_checkpoint_ids
proposal.migration_plan.metadata["target_org_version"] = target_org_version
await self._emit_progress(
f"[Reorg] Applied proposal {proposal.proposal_id}: migrated {len(migrated_task_ids)} tasks and {len(migrated_checkpoint_ids)} checkpoints."
)
return {
"migrated_task_ids": migrated_task_ids,
"migrated_checkpoint_ids": migrated_checkpoint_ids,
"target_org_version": target_org_version,
}
def _apply_task_adjustments(self, task: Task, changeset: ReorgChangeSet) -> None:
if not changeset.task_adjustments:
return
for adjustment in changeset.task_adjustments:
if adjustment.task_id and adjustment.task_id != task.id:
continue
if adjustment.action == "reassign" and adjustment.new_role_id:
task.assigned_to = adjustment.new_role_id
task.metadata["work_item_role_id"] = adjustment.new_role_id
elif adjustment.action == "reprioritize" and adjustment.priority is not None:
task.priority = adjustment.priority
elif adjustment.action == "update_description" and adjustment.description_append.strip():
addition = adjustment.description_append.strip()
if addition not in task.description:
task.description = f"{task.description}\n\nAdjustment note:\n{addition}".strip()
elif adjustment.action == "append_acceptance_criteria" and adjustment.acceptance_criteria:
criteria = list(task.metadata.get("acceptance_criteria", []))
for item in adjustment.acceptance_criteria:
if item not in criteria:
criteria.append(item)
task.metadata["acceptance_criteria"] = criteria
elif adjustment.action == "request_review":
task.metadata["force_additional_review"] = True
def _infer_scope(self, changeset: ReorgChangeSet) -> ReorgScope:
if changeset.role_changes:
return ReorgScope.ORG_MUTATION
return ReorgScope.TASK_ADJUSTMENT
def _classify_risk(self, scope: ReorgScope, changeset: ReorgChangeSet) -> ReorgRiskLevel:
if scope == ReorgScope.ORG_MUTATION:
return ReorgRiskLevel.HIGH
for adjustment in changeset.task_adjustments:
if adjustment.action not in {"reassign", "reprioritize", "update_description", "append_acceptance_criteria", "request_review"}:
return ReorgRiskLevel.MEDIUM
return ReorgRiskLevel.LOW
def _is_top_level_role(self, role_id: str) -> bool:
agent = self.org_engine.get_agent(role_id)
if not agent:
return role_id in {"owner", "coordinator"}
return agent.reports_to == "owner"
def _normalize_changeset(self, changeset: ReorgChangeSet) -> ReorgChangeSet:
role_changes = [
item if isinstance(item, ReorgRoleChange) else ReorgRoleChange(**item)
for item in changeset.role_changes
]
task_adjustments = [
item if isinstance(item, ReorgTaskAdjustment) else ReorgTaskAdjustment(**item)
for item in changeset.task_adjustments
]
return ReorgChangeSet(
role_changes=role_changes,
task_adjustments=task_adjustments,
metadata=dict(changeset.metadata),
)
async def _require_proposal(self, proposal_id: str) -> ReorgProposal:
proposal = await self.store.get_reorg_proposal(proposal_id)
if not proposal:
raise ValueError(f"Unknown reorg proposal `{proposal_id}`.")
return proposal
+156
View File
@@ -0,0 +1,156 @@
"""Seat-scoped execution adapter for actor-runtime company mode."""
from __future__ import annotations
from typing import Any, Protocol
from opc.core.models import CompanyMemberSession, Task, TaskResult
from opc.layer2_organization.session_scoping import (
external_resume_allowed_for_scope,
task_session_scope_id,
)
class SeatExecutor(Protocol):
"""Common execution contract for seat-backed turns."""
async def prepare_seat(
self,
task: Task,
*,
member_session: CompanyMemberSession | None = None,
role: Any | None = None,
) -> None: ...
async def run_turn(
self,
task: Task,
*,
member_session: CompanyMemberSession | None = None,
) -> TaskResult: ...
async def checkpoint(
self,
task: Task,
*,
member_session: CompanyMemberSession | None = None,
) -> dict[str, Any]: ...
async def interrupt(
self,
task: Task,
*,
member_session: CompanyMemberSession | None = None,
) -> None: ...
async def shutdown(
self,
*,
member_session: CompanyMemberSession | None = None,
) -> None: ...
class EngineSeatExecutor:
"""Seat executor backed by the existing OPC engine task runners."""
def __init__(self, host: Any) -> None:
self.host = host
async def prepare_seat(
self,
task: Task,
*,
member_session: CompanyMemberSession | None = None,
role: Any | None = None,
) -> None:
_ = role
if member_session is None:
return
adapter_state = dict(member_session.adapter_session_state or {})
if not adapter_state:
return
task.metadata = dict(task.metadata or {})
task.context_snapshot = dict(task.context_snapshot or {})
resume_scope_id = str(
adapter_state.get("external_resume_session_scope_id", "")
or adapter_state.get("session_scope_id", "")
or ""
).strip()
assigned_agent = str(task.assigned_external_agent or "").strip()
state_agent = str(
adapter_state.get("external_resume_agent_type")
or adapter_state.get("selected_execution_agent")
or ""
).strip()
if assigned_agent:
agent_entry = adapter_state.get(assigned_agent)
if isinstance(agent_entry, dict):
adapter_state = {**adapter_state, **dict(agent_entry)}
entry_token = str(
agent_entry.get("external_resume_session_id")
or agent_entry.get("resume_session_id")
or agent_entry.get("provider_session_id")
or ""
).strip()
if entry_token:
adapter_state["external_resume_session_id"] = entry_token
state_agent = assigned_agent
if not external_resume_allowed_for_scope(task, resume_scope_id=resume_scope_id):
adapter_state.pop("external_resume_session_id", None)
adapter_state.pop("external_resume_session_scope_id", None)
adapter_state.pop("external_resume_agent_type", None)
external_resume_session_id = str(adapter_state.get("external_resume_session_id", "") or "").strip()
if external_resume_session_id and assigned_agent and state_agent == assigned_agent:
task.metadata["external_resume_session_id"] = external_resume_session_id
task.metadata["external_resume_session_scope_id"] = (
resume_scope_id or task_session_scope_id(task)
)
task.metadata["external_resume_agent_type"] = assigned_agent
else:
task.metadata.pop("external_resume_session_id", None)
task.metadata.pop("external_resume_session_scope_id", None)
task.metadata.pop("external_resume_agent_type", None)
if adapter_state:
task.context_snapshot["seat_adapter_session_state"] = dict(adapter_state)
async def run_turn(
self,
task: Task,
*,
member_session: CompanyMemberSession | None = None,
) -> TaskResult:
_ = member_session
return await self.host._execute_task(task)
async def checkpoint(
self,
task: Task,
*,
member_session: CompanyMemberSession | None = None,
) -> dict[str, Any]:
_ = member_session
return {
"task_id": str(task.id or "").strip(),
"seat_id": str((task.metadata or {}).get("delegation_seat_id", "") or "").strip(),
"role_session_id": str((task.metadata or {}).get("delegation_role_session_id", "") or "").strip(),
"assigned_external_agent": str(task.assigned_external_agent or "").strip(),
"external_resume_session_id": str((task.metadata or {}).get("external_resume_session_id", "") or "").strip(),
}
async def interrupt(
self,
task: Task,
*,
member_session: CompanyMemberSession | None = None,
) -> None:
_ = member_session
if hasattr(self.host, "_active_task_runs"):
self.host._active_task_runs.discard(task.id)
async def shutdown(
self,
*,
member_session: CompanyMemberSession | None = None,
) -> None:
_ = member_session
return None
+225
View File
@@ -0,0 +1,225 @@
"""Secretary service for long-term policy capture and lightweight governance."""
from __future__ import annotations
import json
import uuid
from typing import Any
from loguru import logger
from opc.database.store import OPCStore
from opc.layer5_memory.memory_manager import MemoryManager
from opc.layer5_memory.preference import PreferenceManager
from opc.layer5_memory.secretary_policy import SecretaryPolicyManager
from opc.layer5_memory.skill_importer import ExternalSkillImporter, SkillImportError
from opc.layer5_memory.skill_library import SkillLibrary
from opc.llm.provider import LLMProvider
from opc.llm.retry import LLMRetryError, call_llm_json_with_retry
class SecretaryService:
"""Direct secretary interface with long-term memory and policy updates."""
def __init__(
self,
llm: LLMProvider,
store: OPCStore,
memory: MemoryManager,
preferences: PreferenceManager,
skills: SkillLibrary,
policies: SecretaryPolicyManager,
) -> None:
self.llm = llm
self.store = store
self.memory = memory
self.preferences = preferences
self.skills = skills
self.policies = policies
self.skill_importer = ExternalSkillImporter(skill_library=skills, policies=policies)
async def handle_message(
self,
content: str,
*,
project_id: str | None = None,
session_id: str | None = None,
) -> dict[str, Any]:
secretary_session_id = session_id or str(uuid.uuid4())
await self.memory.ensure_session(
secretary_session_id,
project_id=project_id or "default",
title=(content[:120] or "Secretary Session").strip(),
mode="primary",
metadata={"interface": "secretary"},
)
await self.memory.record_user_turn(secretary_session_id, content, project_id=project_id or "default")
prompt = await self._build_prompt(content, project_id=project_id, session_id=secretary_session_id)
raw_fallback_text = ""
try:
parsed = await call_llm_json_with_retry(
self.llm,
system=self._system_prompt(),
payload=prompt,
task_type="quick_tasks",
label="secretary",
)
except LLMRetryError as exc:
logger.warning(
f"Secretary LLM returned invalid JSON after retries: {exc}; "
"falling back to plain-text echo."
)
raw_fallback_text = str(exc.last_raw or "").strip()
parsed = {"response": raw_fallback_text, "actions": []}
applied_updates: list[str] = []
applied_actions = await self._apply_actions(parsed.get("actions", []), project_id=project_id)
reply = str(parsed.get("response", "")).strip() or raw_fallback_text
if applied_updates:
reply += "\n\nApplied secretary updates:\n" + "\n".join(f"- {item}" for item in applied_updates)
if applied_actions:
reply += "\n\nApplied secretary actions:\n" + "\n".join(f"- {item}" for item in applied_actions)
await self.memory.record_assistant_turn(
secretary_session_id,
reply,
project_id=project_id or "default",
metadata={"kind": "secretary_reply"},
)
return {
"response": reply,
"session_id": secretary_session_id,
"applied_updates": applied_updates,
"applied_actions": applied_actions,
}
async def list_sessions(self, project_id: str | None, limit: int = 20) -> list[Any]:
sessions = await self.store.list_sessions(project_id=project_id or "default", parent_session_id=None, limit=limit * 3)
return [item for item in sessions if item.metadata.get("interface") == "secretary"][:limit]
def describe_policies(self, project_id: str | None = None) -> str:
return self.policies.summarize_policies(project_id=project_id)
async def _build_prompt(self, content: str, project_id: str | None, session_id: str) -> str:
policy_summary = self.policies.summarize_policies(project_id=project_id)
project_knowledge = await self.memory.build_project_knowledge_context(project_id=project_id)
session_history = await self.memory.build_session_prompt_context(
session_id,
include_latest_user_turn=False,
)
recent_events = await self.store.get_events(limit=12)
event_lines: list[str] = []
for event in reversed(recent_events[-8:]):
payload = str(event.get("payload", ""))
event_lines.append(f"- {event.get('event_type', '')}: {payload}")
skill_names = [skill.name for skill in self.skills.list_skills()]
current_preferences = self.preferences.load_merged(project_id=project_id)
context = {
"project_id": project_id or "default",
"user_message": content,
"current_secretary_policies": policy_summary,
"current_preferences": {
"communication_style": current_preferences.get("communication_style", ""),
"preferred_language": current_preferences.get("preferred_language", ""),
"decision_preferences": current_preferences.get("decision_preferences", {}),
},
"project_knowledge": project_knowledge,
"secretary_session_history": session_history,
"recent_structured_events": event_lines,
"available_skill_names": skill_names[:80],
}
return json.dumps(context, ensure_ascii=False)
def _system_prompt(self) -> str:
prompt = (
"You are the long-term secretary of the OPC system.\n"
"Your job is to answer as a practical assistant. Durable memory and policy updates are handled by agents through the memory skill, not by the secretary.\n"
"Important constraints:\n"
"- Do not create memory notes, authorization rules, workspace guardrails, skill injection rules, or preferences.\n"
"- Use actions only for explicit skill imports.\n"
"- Return strict JSON only.\n\n"
"JSON schema:\n"
"{\n"
' "response": "assistant reply",\n'
' "actions": [\n'
" {\n"
' "kind": "import_skill",\n'
' "scope": "project",\n'
' "source": "clawhub" | "path",\n'
' "query": "natural language search terms or exact slug",\n'
' "slug": "exact-skill-slug-if-known",\n'
' "path": "/absolute/path/to/downloaded/skill/folder",\n'
' "domains": ["coding"],\n'
' "enable": true,\n'
' "rationale": "why this import is needed"\n'
" }\n"
" ]\n"
"}"
)
return prompt
def _parse_response(self, raw: str) -> dict[str, Any]:
text = raw.strip()
if text.startswith("```"):
parts = text.split("\n", 1)
text = parts[1] if len(parts) == 2 else text[3:]
if text.endswith("```"):
text = text[:-3]
text = text.strip()
try:
data = json.loads(text)
if isinstance(data, dict):
return data
except Exception as e:
logger.debug(f"Secretary JSON parse failed: {e}")
return {"response": raw.strip(), "actions": []}
async def _apply_updates(self, updates: list[Any], project_id: str | None) -> list[str]:
_ = (updates, project_id)
return []
async def _apply_actions(self, actions: list[Any], project_id: str | None) -> list[str]:
applied: list[str] = []
for action in actions:
if not isinstance(action, dict):
continue
kind = str(action.get("kind", "")).strip()
if kind == "update_preferences":
applied.append("skipped preference update because secretary memory writes are disabled")
continue
if kind != "import_skill":
continue
if not project_id:
applied.append("skipped skill import because the secretary needs a project context")
continue
source = str(action.get("source", "clawhub")).strip().lower() or "clawhub"
query = str(action.get("query", "")).strip()
slug = str(action.get("slug", "")).strip()
path = str(action.get("path", "")).strip()
if source == "clawhub" and not query and not slug:
applied.append("skipped skill import because no skill query or slug was provided")
continue
if source in {"path", "directory", "local"} and not path:
applied.append("skipped skill import because no local skill path was provided")
continue
domains = [str(item).strip() for item in action.get("domains", []) if str(item).strip()]
enable = bool(action.get("enable", True))
try:
result = await self.skill_importer.import_skill(
project_id=project_id,
source=source,
query=query,
slug=slug,
path=path,
domains=domains,
enable=enable,
)
summary = f"imported skill `{result.skill_name}` and made it available in project `{project_id}`"
if result.enabled_domains:
summary += f"; auto-injected for {', '.join(result.enabled_domains)}"
applied.append(summary)
except SkillImportError as exc:
applied.append(f"skill import failed: {exc}")
return applied
+156
View File
@@ -0,0 +1,156 @@
"""Shared helpers for company-mode session scoping and continuity guards.
Phase A (role-instance model): the session / queue key is keyed by
``(session_scope, role_id)`` — *not* by seat. A role that appears as a
member in multiple teams (e.g. CMO is both CEO's subordinate and the
leader of her own team) has **one** session and **one** queue. The seat
id stays as organizational metadata but no longer affects identity.
If a later refactor needs to support multiple parallel instances of the
same role (e.g. two CMOs in parallel branches of a run), pass
``team_instance_id`` to disambiguate — it is appended to the key when
present.
"""
from __future__ import annotations
from opc.core.models import Task
from opc.layer2_organization.work_item_runtime import is_work_item_runtime_metadata
def task_session_scope_id(task: Task) -> str:
"""Return the top-level session scope for a company-mode task."""
metadata = dict(getattr(task, "metadata", {}) or {})
return str(
getattr(task, "parent_session_id", "")
or metadata.get("parent_session_id", "")
or getattr(task, "session_id", "")
or metadata.get("session_id", "")
or ""
).strip()
def scoped_member_session_id(
*,
project_id: str,
session_scope_id: str,
role_id: str,
employee_id: str,
team_instance_id: str = "",
explicit_id: str = "",
) -> str:
"""Build a role-instance member session id.
One per ``(project, session_scope, [team_instance], role, employee)``.
Previously this was keyed by seat; that is gone in the role-instance
model. Same role → same session, across upward/downward work.
``team_instance_id`` is optional — included when multiple concurrent
instances of the same role exist in a single run. For standard
single-branch company mode it's left blank.
"""
explicit = str(explicit_id or "").strip()
if explicit:
return explicit
project = str(project_id or "default").strip() or "default"
scope = str(session_scope_id or "").strip()
role = str(role_id or "unknown").strip() or "unknown"
employee = str(employee_id or "default").strip() or "default"
team_instance = str(team_instance_id or "").strip()
scoped_prefix = f"{project}::{scope}" if scope else project
parts: list[str] = [scoped_prefix]
if team_instance:
parts.append(team_instance)
parts.append(role)
parts.append(employee)
return "role-session::" + "::".join(parts)
def scoped_queue_key(
*,
session_scope_id: str,
role_id: str = "",
team_instance_id: str = "",
seat_id: str = "", # deprecated, ignored — kept for arg compat while callers migrate
) -> str:
"""Build the per-role dispatch queue key.
Role-scoped (not seat-scoped). ``team_instance_id`` is only appended
when present (future-proof for multi-branch). ``seat_id`` is ignored.
"""
role = str(role_id or "").strip()
scope = str(session_scope_id or "").strip()
team_instance = str(team_instance_id or "").strip()
if not role:
return ""
parts: list[str] = []
if scope:
parts.append(scope)
if team_instance:
parts.append(team_instance)
parts.append(role)
return "::".join(parts)
def role_home_team_instance_id(
role_id: str,
seats: list[dict] | None,
) -> str:
"""Return the ``team_instance_id`` where ``role_id`` is the leader.
Convention: the leader seat of a role has ``team_id == f"team::{role_id}"``.
For leaf roles (no own team), returns the team_instance of any seat
that lists this role. Returns empty string if no seat matches.
"""
role = str(role_id or "").strip()
if not role:
return ""
seat_list = [dict(seat) for seat in (seats or []) if isinstance(seat, dict)]
# Prefer the leader seat (role's own team).
for seat in seat_list:
if str(seat.get("role_id", "") or "").strip() != role:
continue
if str(seat.get("team_id", "") or "").strip() == f"team::{role}":
ti = str(seat.get("team_instance_id", "") or "").strip()
if ti:
return ti
# Fallback: first seat listing this role.
for seat in seat_list:
if str(seat.get("role_id", "") or "").strip() != role:
continue
ti = str(seat.get("team_instance_id", "") or "").strip()
if ti:
return ti
return ""
def is_top_level_company_session(task: Task) -> bool:
"""Return whether the task belongs to a top-level actor-runtime session."""
metadata = dict(getattr(task, "metadata", {}) or {})
if not is_work_item_runtime_metadata(metadata):
return False
session_id = str(
getattr(task, "session_id", "")
or metadata.get("session_id", "")
or ""
).strip()
parent_session_id = str(
getattr(task, "parent_session_id", "")
or metadata.get("parent_session_id", "")
or ""
).strip()
return bool(session_id and parent_session_id and session_id == parent_session_id)
def external_resume_allowed_for_scope(task: Task, *, resume_scope_id: str = "") -> bool:
"""Only allow external session continuation when the scope matches."""
metadata = dict(getattr(task, "metadata", {}) or {})
if bool(metadata.get("allow_external_resume_on_top_level_session", False)):
return True
current_scope = task_session_scope_id(task)
if not current_scope:
return True
resume_scope = str(resume_scope_id or "").strip()
if resume_scope:
return resume_scope == current_scope
return not is_top_level_company_session(task)
+708
View File
@@ -0,0 +1,708 @@
"""Local talent-market helpers for importing and hiring agency agents."""
from __future__ import annotations
import re
from pathlib import Path
from typing import Any
import yaml
from yaml import YAMLError
from opc.core.config import EmployeeConfig, OPCConfig, TalentTemplateConfig
_FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n?", re.DOTALL)
_CATEGORY_DESCRIPTIONS: dict[str, str] = {
"academic": "Academic research and scholarly analysis across humanities and social science disciplines.",
"design": "Visual design, brand systems, UX thinking, and creative asset direction.",
"engineering": "Software engineering, architecture, implementation, integration, and technical delivery.",
"examples": "Reference or demo-style prompts that illustrate processes rather than specialized staffing.",
"finance": "Finance, investment research, financial modeling, valuation, accounting, tax, and portfolio analysis.",
"general": "General-purpose execution support for broad, lightweight, or fallback work across roles.",
"game-development": "Game systems, content, technical art, narrative, and interactive development.",
"marketing": "Audience growth, messaging, campaigns, content strategy, and go-to-market execution.",
"paid-media": "Performance marketing, media buying, attribution, and campaign optimization.",
"product": "Product strategy, prioritization, discovery, user insight, and roadmap decisions.",
"project-management": "Planning, delivery coordination, process management, and operational tracking.",
"sales": "Pipeline development, customer discovery, solution positioning, and deal support.",
"spatial-computing": "XR, visionOS, 3D interfaces, immersive experiences, and spatial product work.",
"specialized": "Specialized domain experts for niche industries, compliance, process design, and custom operations.",
"strategy": "High-level planning, operating models, and cross-functional execution playbooks.",
"support": "Operational support, reporting, compliance, maintenance, and service continuity.",
"testing": "QA, validation, benchmarking, audit, and evidence-based quality checks.",
}
_CATEGORY_PREFIXES = tuple(sorted(_CATEGORY_DESCRIPTIONS.keys(), key=len, reverse=True))
_IGNORED_TEMPLATE_STEMS = {
"readme",
"integration-readme",
"integrations-readme",
"pull_request_template",
"issue_template",
"contributing",
"changelog",
"license",
}
_NON_TALENT_RECURSIVE_DIRS = {"integrations", "scripts"}
def _slugify(value: str) -> str:
slug = re.sub(r"[^A-Za-z0-9._:-]+", "-", value.strip().lower()).strip("-")
return slug or "talent"
def _tokenize_text(text: str) -> list[str]:
return re.findall(r"[a-z0-9][a-z0-9+-]{2,}", text.lower())
def _extract_frontmatter(text: str) -> tuple[dict[str, Any], str]:
match = _FRONTMATTER_RE.match(text)
if not match:
return {}, text.strip()
frontmatter_text = match.group(1)
try:
frontmatter = yaml.safe_load(frontmatter_text) or {}
except YAMLError:
frontmatter = _parse_relaxed_frontmatter(frontmatter_text)
body = text[match.end():].strip()
return frontmatter, body
def _parse_relaxed_frontmatter(frontmatter_text: str) -> dict[str, Any]:
"""Parse simple frontmatter that looks YAML-like but isn't strictly valid YAML."""
parsed: dict[str, Any] = {}
active_key: str | None = None
for raw_line in frontmatter_text.splitlines():
line = raw_line.rstrip()
stripped = line.strip()
if not stripped or stripped.startswith("#"):
continue
if stripped.startswith("- ") and active_key:
current = parsed.setdefault(active_key, [])
if not isinstance(current, list):
current = [str(current)]
current.append(stripped[2:].strip())
parsed[active_key] = current
continue
if ":" not in line:
continue
key, value = line.split(":", 1)
active_key = key.strip()
parsed[active_key] = _coerce_frontmatter_value(value.strip())
return parsed
def _coerce_frontmatter_value(value: str) -> Any:
if not value:
return ""
if value.startswith(('"', "'", "[", "{")):
try:
return yaml.safe_load(value)
except YAMLError:
return value.strip().strip("\"'")
lowered = value.lower()
if lowered in {"true", "false"}:
return lowered == "true"
return value.strip().strip("\"'")
def _infer_category_from_stem(stem: str) -> str | None:
normalized = stem.strip().lower()
for category in _CATEGORY_PREFIXES:
if normalized == category or normalized.startswith(f"{category}-"):
return category
return None
def _should_ignore_template_path(path: Path) -> bool:
filename = path.name.strip().lower()
stem = path.stem.strip().lower().lstrip(".")
if filename.startswith("."):
return True
if stem in _IGNORED_TEMPLATE_STEMS:
return True
if stem.endswith("-readme") or stem.endswith("_readme"):
return True
return False
def _has_named_frontmatter_template(path: Path) -> bool:
try:
frontmatter, _ = _extract_frontmatter(path.read_text(encoding="utf-8"))
except OSError:
return False
return bool(str(frontmatter.get("name", "")).strip())
def _looks_like_prompt_path(ref: str) -> bool:
value = str(ref or "").strip()
if not value:
return False
if "\n" in value:
return False
if len(value) > 260:
return False
if value.startswith(("You ", "Act ", "Focus ", "Review ", "Write ")):
return False
if " " in value and "/" not in value and "\\" not in value:
return False
return True
def resolve_prompt_refs(refs: list[str], opc_home: Path) -> list[str]:
resolved: list[str] = []
for ref in refs:
value = str(ref or "").strip()
if not value:
continue
if _looks_like_prompt_path(value):
try:
path = Path(value)
if not path.is_absolute():
path = opc_home / path
if path.exists() and path.is_file():
resolved.append(path.read_text(encoding="utf-8").strip())
continue
except OSError:
pass
resolved.append(value)
return [item for item in resolved if item]
def _is_placeholder_employee(employee: EmployeeConfig) -> bool:
metadata = dict(employee.metadata or {})
return bool(metadata.get("is_default_employee") or metadata.get("is_fallback_employee"))
class HireError(Exception):
"""Base class for recoverable failures in TalentMarket.hire_template."""
class RoleAlreadyHiredError(HireError):
"""Raised when a role already has a non-placeholder employee."""
def __init__(self, role_id: str, existing_employee_id: str) -> None:
self.role_id = role_id
self.existing_employee_id = existing_employee_id
super().__init__(
f"Role '{role_id}' already has employee '{existing_employee_id}'.",
)
class TalentMarket:
"""Imports talent templates from local markdown files and hires employees."""
def __init__(self, opc_home: Path, config: OPCConfig) -> None:
self.opc_home = opc_home
self.config = config
def list_templates(self) -> list[TalentTemplateConfig]:
return self.list_available_templates()
def list_employees(self) -> list[EmployeeConfig]:
return sorted(self.config.org.employees, key=lambda item: (item.role_id, item.name.lower()))
def get_template(self, template_id: str) -> TalentTemplateConfig | None:
normalized = str(template_id or "").strip()
if not normalized:
return None
return next((item for item in self.list_available_templates() if item.id == normalized), None)
def list_available_templates(self) -> list[TalentTemplateConfig]:
"""Return every template the talent market can hire from the talent catalog."""
templates_by_id: dict[str, TalentTemplateConfig] = {}
for template in self.scan_local_talent():
templates_by_id[template.id] = template
try:
from opc.market.talent_presets import get_all_talent_presets
for raw in get_all_talent_presets():
template = self._template_from_preset(raw)
templates_by_id.setdefault(template.id, template)
except Exception:
pass
return sorted(templates_by_id.values(), key=lambda item: (item.category, item.name.lower()))
def ensure_template_available(self, template_id: str) -> TalentTemplateConfig | None:
"""Resolve a template from the talent catalog without mutating org config."""
normalized = str(template_id or "").strip()
if not normalized:
return None
return self.get_template(normalized)
def build_employee_id(self, *, role_id: str, template_id: str) -> str:
_ = role_id
return str(template_id or "").strip()
@staticmethod
def _attach_employee_to_role(employee: EmployeeConfig, role_id: str) -> EmployeeConfig:
normalized_role_id = str(role_id or "").strip()
if not normalized_role_id:
return employee
metadata = dict(employee.metadata or {})
for key in ("home_role_ids", "staffed_role_ids"):
values = [
str(item).strip()
for item in list(metadata.get(key, []) or [])
if str(item).strip()
]
if normalized_role_id not in values:
values.append(normalized_role_id)
metadata[key] = values
metadata.setdefault("home_role_id", str(employee.role_id or normalized_role_id).strip() or normalized_role_id)
employee.metadata = metadata
return employee
def _remove_placeholders_for_role(self, role_id: str) -> None:
normalized_role_id = str(role_id or "").strip()
if not normalized_role_id:
return
self.config.org.employees = [
item
for item in self.config.org.employees
if not (item.role_id == normalized_role_id and _is_placeholder_employee(item))
]
def build_candidate_summary(self, template: TalentTemplateConfig) -> dict[str, Any]:
return {
"template_id": template.id,
"name": template.name,
"description": template.description,
"category": template.category,
"category_description": self.describe_category(template.category),
}
def describe_category(self, category: str) -> str:
normalized = (category or "").strip().lower()
if normalized in _CATEGORY_DESCRIPTIONS:
return _CATEGORY_DESCRIPTIONS[normalized]
return f"{normalized.replace('-', ' ').strip().title()} related talent and execution support."
def list_category_catalog(self) -> list[dict[str, Any]]:
counts: dict[str, int] = {}
for template in self.list_available_templates():
category = (template.category or "general").strip().lower() or "general"
counts[category] = counts.get(category, 0) + 1
catalog = [
{
"category": category,
"description": self.describe_category(category),
"template_count": count,
}
for category, count in counts.items()
]
return sorted(catalog, key=lambda item: (item["category"] != "engineering", item["category"]))
def list_templates_by_categories(self, *, categories: list[str]) -> list[TalentTemplateConfig]:
selected = {(category or "").strip().lower() for category in categories if str(category).strip()}
if not selected:
return []
return [
template for template in self.list_available_templates()
if (template.category or "general").strip().lower() in selected
]
def shortlist_templates_by_categories(
self,
*,
categories: list[str],
role_descriptions: list[str],
limit: int = 6,
) -> list[TalentTemplateConfig]:
selected = {(category or "").strip().lower() for category in categories if str(category).strip()}
templates = [
template for template in self.list_available_templates()
if not selected or (template.category or "general").strip().lower() in selected
]
if not templates:
return []
need_tokens = set(_tokenize_text(" ".join(role_descriptions)))
scored: list[tuple[float, TalentTemplateConfig]] = []
for template in templates:
template_tokens = set(_tokenize_text(f"{template.name} {template.description}"))
score = float(len(need_tokens & template_tokens))
if template.description:
score += 0.25
scored.append((score, template))
scored.sort(key=lambda item: (-item[0], item[1].name.lower()))
return [template for _, template in scored[:limit]]
def search_templates_for_need(
self,
*,
role_id: str,
domains: list[str],
role_descriptions: list[str],
limit: int = 5,
) -> list[TalentTemplateConfig]:
scored: list[tuple[float, TalentTemplateConfig]] = []
role_text = " ".join(role_descriptions).lower()
need_tokens = {
role_id.lower(),
*[domain.lower() for domain in domains],
*re.findall(r"[a-z0-9][a-z0-9+-]{2,}", role_text),
}
for template in self.list_available_templates():
template_tokens = {
template.category.lower(),
template.name.lower(),
*[domain.lower() for domain in template.domains],
*[tag.lower() for tag in template.tags],
*re.findall(r"[a-z0-9][a-z0-9+-]{2,}", template.description.lower()),
}
score = 0.0
if role_id.lower() in template_tokens:
score += 6.0
score += float(len(need_tokens & template_tokens))
if template.preferred_external_agent:
score += 0.5
if score > 0:
scored.append((score, template))
scored.sort(key=lambda item: (-item[0], item[1].name.lower()))
return [template for _, template in scored[:limit]]
def import_from_repo(self, repo_path: Path) -> list[TalentTemplateConfig]:
repo_root = repo_path.expanduser().resolve()
imported: list[TalentTemplateConfig] = []
for category_dir in sorted(repo_root.iterdir()):
if not category_dir.is_dir():
continue
for markdown_path in self._iter_repo_template_paths(category_dir):
template = self._parse_template(markdown_path, repo_root)
if template is None:
continue
self._write_prompt(template.id, markdown_path)
imported.append(template)
unique = {template.id: template for template in imported if template.id}
return sorted(unique.values(), key=lambda item: (item.category, item.name.lower()))
def _iter_repo_template_paths(self, category_dir: Path) -> list[Path]:
top_level = category_dir.name.strip().lower()
paths: list[Path] = []
for markdown_path in sorted(category_dir.rglob("*.md")):
if _should_ignore_template_path(markdown_path):
continue
if markdown_path.parent == category_dir:
paths.append(markdown_path)
continue
if top_level in _NON_TALENT_RECURSIVE_DIRS:
continue
if _has_named_frontmatter_template(markdown_path):
paths.append(markdown_path)
return paths
def _resolve_template(self, template_id: str) -> TalentTemplateConfig | None:
return self.get_template(template_id)
def hire_template(
self,
template_id: str,
role_id: str,
*,
employee_name: str | None = None,
employee_id: str | None = None,
) -> EmployeeConfig:
template = self.ensure_template_available(template_id)
if template is None:
raise ValueError(f"Unknown talent template `{template_id}`.")
employee_name = (employee_name or template.name).strip()
if not employee_name:
raise ValueError("Employee name cannot be empty.")
displaced_ids = {
item.employee_id
for item in self.config.org.employees
if item.role_id == role_id and _is_placeholder_employee(item)
}
chosen_id = self.build_employee_id(role_id=role_id, template_id=template.id)
if any(
item.employee_id == chosen_id and item.employee_id not in displaced_ids
for item in self.config.org.employees
):
existing = next(item for item in self.config.org.employees if item.employee_id == chosen_id)
if not _is_placeholder_employee(existing):
self._attach_employee_to_role(existing, role_id)
self._remove_placeholders_for_role(role_id)
return existing
raise ValueError(f"Employee `{chosen_id}` already exists.")
new_employee = EmployeeConfig(
employee_id=chosen_id,
template_id=template.id,
name=employee_name,
role_id=role_id,
description=template.description,
category=template.category,
domains=[],
tags=list(template.tags),
prompt_refs=[template.prompt_ref] if template.prompt_ref else [],
skill_refs=[],
preferred_external_agent=template.preferred_external_agent,
metadata={
"source_repo": template.source_repo,
"source_path": template.source_path,
"source_revision": template.source_revision,
"talent_template_name": template.name,
"talent_template_category": template.category,
"home_role_id": role_id,
"home_role_ids": [role_id],
"staffed_role_ids": [role_id],
},
)
self.config.org.employees = [
item for item in self.config.org.employees if item.employee_id not in displaced_ids
] + [
new_employee,
]
return new_employee
def ensure_hire_template(
self,
template_id: str,
role_id: str,
*,
employee_name: str | None = None,
employee_id: str | None = None,
) -> EmployeeConfig:
template = self.ensure_template_available(template_id)
if template is None:
raise ValueError(f"Unknown talent template `{template_id}`.")
_ = employee_id
desired_id = self.build_employee_id(role_id=role_id, template_id=template.id)
existing = next((item for item in self.config.org.employees if item.employee_id == desired_id), None)
if existing and not _is_placeholder_employee(existing):
self._attach_employee_to_role(existing, role_id)
self._remove_placeholders_for_role(role_id)
return existing
resolved_name = (employee_name or template.name).strip()
if not resolved_name:
raise ValueError("Employee name cannot be empty.")
displaced_ids = {
item.employee_id
for item in self.config.org.employees
if item.role_id == role_id
and _is_placeholder_employee(item)
}
if existing is not None:
displaced_ids.add(existing.employee_id)
new_employee = EmployeeConfig(
employee_id=desired_id,
template_id=template.id,
name=resolved_name,
role_id=role_id,
description=template.description,
category=template.category,
domains=[],
tags=list(template.tags),
prompt_refs=[template.prompt_ref] if template.prompt_ref else [],
skill_refs=[],
preferred_external_agent=template.preferred_external_agent,
metadata={
"source_repo": template.source_repo,
"source_path": template.source_path,
"source_revision": template.source_revision,
"talent_template_name": template.name,
"talent_template_category": template.category,
"home_role_id": role_id,
"home_role_ids": [role_id],
"staffed_role_ids": [role_id],
},
)
self.config.org.employees = [
item for item in self.config.org.employees if item.employee_id not in displaced_ids
] + [
new_employee,
]
return new_employee
def scan_local_talent(self) -> list[TalentTemplateConfig]:
"""Scan ``prompts/talent/*.md`` and return the local talent catalog."""
talent_dir = self.opc_home / "prompts" / "talent"
if not talent_dir.is_dir():
return []
found: dict[str, TalentTemplateConfig] = {}
for md_path in sorted(talent_dir.glob("*.md")):
tpl = self._parse_template(md_path, talent_dir)
if tpl:
found[tpl.id] = tpl
return sorted(found.values(), key=lambda item: (item.category, item.name.lower()))
def _template_from_preset(self, data: dict[str, Any]) -> TalentTemplateConfig:
template_id = str(data.get("id") or data.get("template_id") or "").strip()
name = str(data.get("name") or template_id).strip() or template_id
return TalentTemplateConfig(
id=template_id,
name=name,
description=str(data.get("description", "") or ""),
category=str(data.get("category", "") or ""),
domains=[
str(item).strip()
for item in list(data.get("domains", []) or [])
if str(item).strip()
],
tags=[
str(item).strip()
for item in list(data.get("tags", []) or [])
if str(item).strip()
],
prompt_ref=str(data.get("prompt_ref", "") or ""),
preferred_external_agent=data.get("preferred_external_agent"),
source_repo="builtin",
source_path=str(data.get("source_path", "") or ""),
source_revision=str(data.get("source_revision", "") or "builtin"),
metadata={
key: value
for key, value in data.items()
if key not in {"id", "template_id", "name", "description", "category", "domains", "tags", "prompt_ref", "preferred_external_agent", "source_path", "source_revision"}
},
)
def import_local_templates(self, template_ids: list[str]) -> list[TalentTemplateConfig]:
"""Resolve selected templates from the local talent directory."""
available = {t.id: t for t in self.scan_local_talent()}
imported: list[TalentTemplateConfig] = []
for tid in template_ids:
tpl = available.get(tid)
if tpl:
imported.append(tpl)
return sorted(imported, key=lambda t: (t.category, t.name.lower()))
def _parse_template(self, path: Path, repo_root: Path) -> TalentTemplateConfig | None:
if _should_ignore_template_path(path):
return None
text = path.read_text(encoding="utf-8")
frontmatter, body = _extract_frontmatter(text)
# If no frontmatter, derive name from first H1 heading or filename
if not frontmatter or not frontmatter.get("name"):
h1_match = re.match(r"^#\s+(.+)", body or text, re.MULTILINE)
derived_name = h1_match.group(1).strip() if h1_match else path.stem.replace("-", " ").title()
if not derived_name:
return None
frontmatter = dict(frontmatter) if frontmatter else {}
frontmatter["name"] = derived_name
category = self._resolve_template_category(path, repo_root, frontmatter)
rel_path = str(path.relative_to(repo_root))
name = str(frontmatter.get("name", path.stem)).strip() or path.stem
description = str(frontmatter.get("description", "")).strip()
template_id = self._build_template_id(path, repo_root, frontmatter, category)
domains = [
str(item).strip()
for item in list(frontmatter.get("domains", []) or [])
if str(item).strip()
]
raw_tags = [
str(item).strip()
for item in list(frontmatter.get("tags", []) or [])
if str(item).strip()
]
tags = raw_tags or self._infer_tags(category, name, rel_path)
if repo_root.name.strip().lower() == "talent" and path.parent == repo_root:
prompt_ref = f"prompts/talent/{path.name}"
else:
prompt_ref = f"prompts/talent/{template_id}.md"
preferred_external_agent = self._infer_preferred_external_agent(body, category)
metadata = {
key: value
for key, value in frontmatter.items()
if key not in {"id", "name", "description", "domains", "tags", "category"}
}
return TalentTemplateConfig(
id=template_id,
name=name,
description=description,
category=category,
domains=domains,
tags=tags,
prompt_ref=prompt_ref,
preferred_external_agent=preferred_external_agent,
source_repo=str(repo_root),
source_path=rel_path,
source_revision="local",
metadata=metadata,
)
def _resolve_template_category(self, path: Path, repo_root: Path, frontmatter: dict[str, Any]) -> str:
explicit = str(frontmatter.get("category", "")).strip().lower()
if explicit:
return explicit
parent_category = path.parent.name.strip().lower()
repo_root_name = repo_root.name.strip().lower()
try:
first_part = path.relative_to(repo_root).parts[0].strip().lower()
except (IndexError, ValueError):
first_part = ""
if first_part in _CATEGORY_DESCRIPTIONS:
return first_part
if parent_category and parent_category != repo_root_name:
return parent_category
inferred = _infer_category_from_stem(path.stem)
if inferred:
return inferred
if repo_root_name == "talent":
return "general"
return parent_category or "general"
def _build_template_id(
self,
path: Path,
repo_root: Path,
frontmatter: dict[str, Any],
category: str,
) -> str:
explicit = frontmatter.get("id")
if explicit:
return _slugify(str(explicit))
if path.parent.name.strip().lower() == repo_root.name.strip().lower():
inferred = _infer_category_from_stem(path.stem)
if inferred == category:
return _slugify(path.stem)
return _slugify(f"{category}-{path.stem}")
def _write_prompt(self, template_id: str, source_path: Path) -> None:
text = source_path.read_text(encoding="utf-8")
prompt_dir = self.opc_home / "prompts" / "talent"
prompt_dir.mkdir(parents=True, exist_ok=True)
prompt_path = prompt_dir / f"{template_id}.md"
prompt_path.write_text(text.rstrip() + "\n", encoding="utf-8")
def _infer_domains(
self,
category: str,
name: str,
description: str,
body: str,
rel_path: str,
) -> list[str]:
_ = (category, name, description, body, rel_path)
return []
def _infer_tags(self, category: str, name: str, rel_path: str) -> list[str]:
parts = [category.lower(), *re.findall(r"[a-z0-9][a-z0-9+-]{2,}", f"{name} {rel_path}".lower())]
tags: list[str] = []
for part in parts:
if part not in tags:
tags.append(part)
return tags[:10]
def _infer_preferred_external_agent(self, body: str, category: str) -> str | None:
text = f"{category}\n{body}".lower()
if "opencode" in text or "open code" in text:
return "opencode"
if any(token in text for token in ("code", "engineering", "terminal", "implementation", "repository")):
return "codex"
if any(token in text for token in ("infrastructure", "deployment", "operations", "visionos", "cursor")):
return "cursor"
return None
+143
View File
@@ -0,0 +1,143 @@
"""Task graph scheduler — DAG-based task dependency management and parallel execution."""
from __future__ import annotations
import asyncio
from typing import Any, Callable, Coroutine
from loguru import logger
from opc.core.models import Task, TaskStatus, OPCEvent
from opc.core.events import EventBus
from opc.database.store import OPCStore
from opc.layer2_organization.work_item_identity import work_item_projection_id_from_metadata
class TaskGraphScheduler:
"""Manages task dependencies as a DAG and schedules execution.
Tasks with no unmet dependencies are marked RUNNABLE.
Independent tasks can run in parallel; dependent tasks wait.
"""
def __init__(self, store: OPCStore, event_bus: EventBus) -> None:
self.store = store
self.event_bus = event_bus
async def create_tasks(self, task_dicts: list[dict[str, Any]], parent_id: str | None = None) -> list[Task]:
"""Create tasks from dispatch plan and save to store."""
tasks: list[Task] = []
id_map: dict[int, str] = {}
logical_id_map: dict[str, str] = {}
for i, td in enumerate(task_dicts):
metadata = td.get("metadata", {})
task = Task(
session_id=td.get("session_id"),
parent_session_id=td.get("parent_session_id"),
title=td.get("title", ""),
description=td.get("description", ""),
assigned_to=td.get("assigned_to", ""),
tags=td.get("tags", []),
priority=td.get("priority", 5),
project_id=td.get("project_id", "default"),
parent_id=parent_id,
assigned_external_agent=td.get("assigned_external_agent"),
metadata=metadata,
)
id_map[i] = task.id
logical_key = td.get("task_key") or work_item_projection_id_from_metadata(metadata) or metadata.get("task_key")
if logical_key:
logical_id_map[str(logical_key)] = task.id
tasks.append(task)
for i, td in enumerate(task_dicts):
dep_indices = td.get("dependencies", [])
deps: list[str] = []
for dep in dep_indices:
if isinstance(dep, int) and dep in id_map:
deps.append(id_map[dep])
elif isinstance(dep, str) and dep in logical_id_map:
deps.append(logical_id_map[dep])
elif isinstance(dep, str):
deps.append(dep)
tasks[i].dependencies = deps
for task in tasks:
await self.store.save_task(task)
await self.event_bus.publish(OPCEvent(
event_type="task_created",
payload={"task_id": task.id, "title": task.title},
))
return tasks
def get_runnable(self, tasks: list[Task]) -> list[Task]:
"""Return tasks whose dependencies are all DONE."""
done_ids = {t.id for t in tasks if t.status == TaskStatus.DONE}
runnable: list[Task] = []
for task in tasks:
if task.status != TaskStatus.PENDING:
continue
if all(dep in done_ids for dep in task.dependencies):
runnable.append(task)
return runnable
async def execute_graph(
self,
tasks: list[Task],
executor: Callable[[Task], Coroutine[Any, Any, Any]],
) -> list[Task]:
"""Execute a task graph, respecting dependencies.
Runs independent tasks in parallel, waits for dependent tasks.
"""
remaining = set(t.id for t in tasks if t.status == TaskStatus.PENDING)
task_map = {t.id: t for t in tasks}
while remaining:
current_tasks = [task_map[tid] for tid in remaining]
runnable = self.get_runnable(current_tasks + [t for t in tasks if t.status == TaskStatus.DONE])
if not runnable:
failed = [task_map[tid] for tid in remaining]
blocked_ids = [t.id for t in failed]
logger.warning(f"No runnable tasks found. Blocked: {blocked_ids}")
for t in failed:
t.status = TaskStatus.BLOCKED
await self.store.save_task(t)
break
logger.info(f"Running {len(runnable)} tasks in parallel")
async def _run_one(task: Task) -> None:
try:
task.status = TaskStatus.RUNNING
await self.store.save_task(task)
await self.event_bus.publish(OPCEvent(
event_type="task_status_changed",
payload={"task_id": task.id, "status": "running"},
))
await executor(task)
except Exception as e:
logger.error(f"Task {task.id} failed: {e}")
task.status = TaskStatus.FAILED
await self.store.save_task(task)
finally:
# Notify frontend of final task status (DONE/FAILED/etc.)
await self.event_bus.publish(OPCEvent(
event_type="task_status_changed",
payload={"task_id": task.id, "status": task.status.value},
))
await asyncio.gather(*[_run_one(t) for t in runnable])
for t in runnable:
remaining.discard(t.id)
return tasks
async def get_all_project_tasks(self, project_id: str) -> list[Task]:
return await self.store.get_tasks(project_id=project_id)
+138
View File
@@ -0,0 +1,138 @@
"""Stateless turn-mode classifier for role-instance dispatch.
A role's session can be called into action for several different kinds
of turn on the SAME work item. ``infer_turn_mode`` looks at the work
item's state (phase + metadata) and the queue entry type to return
one of five canonical modes. The prompt / context assembly branches
on this value so each mode gets the right context block injected.
EXECUTE — do the work yourself (leaf role, no children)
DELEGATE — break into subtasks (manager role, no children yet)
REVIEW — evaluate a subordinate's deliverable and emit a verdict
INTEGRATE — parent resumes after all children APPROVED; produce the
rolled-up deliverable for upstream review
REWORK — reviewer rejected your prior turn; address the feedback
REPORT — worker DONE; resume the same session under a dedicated
prompt to produce a structured handoff for the reviewer
The classifier is pure: given the same work item + queue entry kind,
it always returns the same mode. It does **not** load the store; all
state must be present on the work item (phase + metadata). This is a
deliberate tradeoff so the mode can be recomputed cheaply at any
point in the dispatcher or context-assembly path.
"""
from __future__ import annotations
from enum import Enum
from typing import Any, Mapping
from opc.core.models import Phase
class TurnMode(str, Enum):
EXECUTE = "execute"
DELEGATE = "delegate"
REVIEW = "review"
INTEGRATE = "integrate"
REWORK = "rework"
REPORT = "report"
def _as_phase(value: Any) -> Phase | None:
if isinstance(value, Phase):
return value
if isinstance(value, str):
try:
return Phase(value.strip().lower())
except Exception:
return None
return None
def _as_mapping(value: Any) -> dict[str, Any]:
if isinstance(value, Mapping):
return dict(value)
return {}
def infer_turn_mode(
work_item: Any,
*,
is_review_entry: bool = False,
) -> TurnMode:
"""Classify the turn the agent is about to run.
``work_item`` is the DelegationWorkItem (or any object exposing
the same ``phase`` / ``kind`` / ``metadata`` attributes).
``is_review_entry`` should be True when the dispatcher popped a
``review-work-item::`` queue entry — those are always reviews,
even if the underlying work_item metadata is ambiguous.
"""
metadata = _as_mapping(getattr(work_item, "metadata", None))
kind = str(getattr(work_item, "kind", "") or "").strip().lower()
phase = _as_phase(getattr(work_item, "phase", None))
# Priority 0: report turn. The hidden auxiliary card spawned after
# a worker DONE so the same session can produce a structured
# handoff before the reviewer is invoked. Detected purely from the
# work item's metadata flag or kind.
if (
bool(metadata.get("report_execution_work_item", False))
or kind == "report"
):
return TurnMode.REPORT
# Priority 1: review turn. Either the queue entry tag says so,
# the work item is explicitly marked as the hidden review card,
# or kind == "review".
if (
is_review_entry
or bool(metadata.get("review_execution_work_item", False))
or kind == "review"
):
return TurnMode.REVIEW
# Priority 2: rework. Phase READY_FOR_REWORK is the canonical
# signal, but the dispatcher flips the work item to RUNNING
# before the prompt is built — by the time the agent runs we
# may only see RUNNING. Fall back to the metadata trail the
# reviewer leaves: ``rework_feedback`` is set on rejection and
# cleared on approval, and ``review_rework_count`` increments
# on each rejection. Either signal means "the previous turn
# was rejected", so render this as REWORK.
if phase == Phase.READY_FOR_REWORK:
return TurnMode.REWORK
rework_feedback = str(metadata.get("rework_feedback", "") or "").strip()
rework_count = int(metadata.get("review_rework_count", 0) or 0)
if rework_feedback or rework_count > 0:
return TurnMode.REWORK
# Priority 3: integrate. The parent has dependency_work_item_ids
# (it delegated previously) AND is currently runnable (RUNNING /
# READY). That can only mean children have completed and the
# parent is being dispatched for its integration turn. The
# metadata.frontier == "resumed" flag is also set by the wake
# edge when present.
dependency_ids = [
str(x).strip()
for x in list(metadata.get("dependency_work_item_ids", []) or [])
if str(x).strip()
]
frontier = str(metadata.get("frontier", "") or "").strip().lower()
if dependency_ids and (
phase in {Phase.RUNNING, Phase.READY} or frontier == "resumed"
):
return TurnMode.INTEGRATE
# Priority 4: delegate. Manager role with nothing spawned yet.
allowed_delegate_role_ids = [
str(x).strip()
for x in list(metadata.get("allowed_delegate_role_ids", []) or [])
if str(x).strip()
]
if allowed_delegate_role_ids and not dependency_ids:
return TurnMode.DELEGATE
# Default: worker executing their own work item.
return TurnMode.EXECUTE
@@ -0,0 +1,100 @@
"""Owner-aware read adapter over work_item / task metadata.
Company-mode historically embedded ~150+ context keys on ``Task.metadata``
and now stores WorkItem-owned fields on ``DelegationWorkItem.metadata``.
This adapter inverts old task-first reads: WorkItem-owned
keys are read from ``work_item.metadata`` first, and task-side fallback is
only allowed for keys explicitly marked as legacy fallback in
``metadata_ownership``. When no work item is in scope, it degrades to the
task-mode behavior and serves task metadata directly.
This module exposes a thin synchronous view that prefers work_item.metadata
and degrades to task.metadata. It's used by:
* ``opc.plugins.office_ui.snapshot_builder.work_item_to_kanban`` — the
kanban renderer previously read ``linked_task.metadata`` for
``progress_log`` / ``work_item_role_name`` / ``employee_prompt_context``
/ ``employee_delta_context``. This path switches it to the view
so it transparently prefers the work_item-side mirror once the mirror
starts dual-writing.
* ``opc.layer1_perception.context_assembler`` and
``opc.layer3_agent.external_broker`` — This path migrates those
consumers the same way. Because the view degrades cleanly to task.metadata
when no work_item is linked (task-mode path), task-mode callers
continue to function without any mode branching.
**Design constraints (things the view is explicitly NOT):**
* **No async / no I/O.** Construct from an in-scope work_item + task pair.
The snapshot builder and context assembler already have both; adding
async store reads would double the round-trips per render.
* **No cache.** Two dict reads is fast enough — measure before optimising.
* **No ``.set()`` writer.** Writes still go through the store via
``update_delegation_work_item`` / ``save_task``. The view is a read
surface only.
* **Defensive copies for list/dict.** Callers must not be able to
accidentally mutate the source dict through the view.
"""
from __future__ import annotations
from typing import Any
from opc.layer2_organization.metadata_ownership import supports_legacy_task_fallback
class WorkItemContextView:
"""Read-only view preferring WorkItem-owned metadata.
Constructor snapshots both dicts at construction. In company mode, task
fallback is a legacy compatibility path controlled by the owner matrix.
In task mode (no WorkItem), all task metadata remains visible.
"""
__slots__ = ("_wi_meta", "_task_meta", "_has_work_item")
def __init__(self, work_item: Any = None, task: Any = None):
wi_meta = getattr(work_item, "metadata", None) if work_item is not None else None
task_meta = getattr(task, "metadata", None) if task is not None else None
self._wi_meta: dict = dict(wi_meta) if isinstance(wi_meta, dict) else {}
self._task_meta: dict = dict(task_meta) if isinstance(task_meta, dict) else {}
self._has_work_item = isinstance(wi_meta, dict)
def get(self, key: str, default: Any = None) -> Any:
"""Return metadata according to the owner matrix.
Returns ``None`` if the key exists but is None on the work_item side;
this preserves dict.get semantics and prevents accidental fallback
over an explicit WorkItem value.
"""
if key in self._wi_meta:
return self._wi_meta[key]
if self._has_work_item and not supports_legacy_task_fallback(key):
return default
return self._task_meta.get(key, default)
def get_list(self, key: str) -> list:
"""Return a fresh list copy. Empty list for missing / non-list keys."""
v = self.get(key, None)
if isinstance(v, (list, tuple)):
return list(v)
return []
def get_dict(self, key: str) -> dict:
"""Return a fresh dict copy. Empty dict for missing / non-dict keys."""
v = self.get(key, None)
if isinstance(v, dict):
return dict(v)
return {}
def has(self, key: str) -> bool:
"""True iff the key is visible through this owner-aware view."""
return key in self._wi_meta or (
key in self._task_meta
and (not self._has_work_item or supports_legacy_task_fallback(key))
)
def __repr__(self) -> str:
return (
"WorkItemContextView("
f"wi_keys={len(self._wi_meta)}, task_keys={len(self._task_meta)})"
)
@@ -0,0 +1,343 @@
"""Helpers for company work-item projection identity metadata."""
from __future__ import annotations
from typing import Any, Mapping
WORK_ITEM_PROJECTION_ID_KEY = "work_item_projection_id"
WORK_ITEM_TURN_TYPE_KEY = "work_item_turn_type"
GATE_REWORK_PROJECTION_ID_KEY = "rework_projection_id"
GATE_TARGET_PROJECTION_ID_KEY = "target_projection_id"
GATE_TARGET_PROJECTION_IDS_KEY = "target_projection_ids"
CANONICAL_WORK_ITEM_TURN_TYPES: frozenset[str] = frozenset(
{
"intake",
"dispatch",
"plan",
"setup",
"execute",
"review",
"report",
"followup",
"monitor",
"aggregate",
"deliver",
"self_evolution",
}
)
_TURN_TYPE_ALIASES: dict[str, str] = {
"delegate": "dispatch",
"delegation": "dispatch",
"delivery": "deliver",
"follow-up": "followup",
"follow_up": "followup",
"synthesis": "aggregate",
"synthesize": "aggregate",
"self-evolution": "self_evolution",
"self evolution": "self_evolution",
}
def _clean(value: Any) -> str:
return str(value or "").strip()
def normalize_work_item_turn_type(value: Any, *, fallback: str = "") -> str:
"""Normalize runtime/work-item turn-kind aliases to canonical names."""
normalized = _clean(value).lower() or _clean(fallback).lower()
return _TURN_TYPE_ALIASES.get(normalized, normalized)
def canonical_work_item_turn_type_for_kind(value: Any, *, fallback: str = "execute") -> str:
"""Map a WorkItem/runtime business kind to the canonical runtime turn type."""
normalized = normalize_work_item_turn_type(value, fallback="")
if normalized in CANONICAL_WORK_ITEM_TURN_TYPES:
return normalized
fallback_normalized = normalize_work_item_turn_type(fallback, fallback="")
if fallback_normalized in CANONICAL_WORK_ITEM_TURN_TYPES:
return fallback_normalized
return ""
def work_item_projection_id_from_metadata(
metadata: Mapping[str, Any] | None,
*,
fallback: str = "",
) -> str:
"""Read the canonical projected work-item task identity."""
if not metadata:
return _clean(fallback)
value = _clean(metadata.get(WORK_ITEM_PROJECTION_ID_KEY))
if value:
return value
return _clean(fallback)
def work_item_turn_type_from_metadata(
metadata: Mapping[str, Any] | None,
*,
fallback: str = "execute",
) -> str:
"""Read the canonical company work-item turn type."""
if not metadata:
return _clean(fallback).lower()
for key in (
WORK_ITEM_TURN_TYPE_KEY,
"work_kind",
"delegation_turn_kind",
):
value = normalize_work_item_turn_type(metadata.get(key))
if value:
return value
return normalize_work_item_turn_type(fallback)
def projection_id_for_task(task: Any) -> str:
"""Return the work-item projection identity for a projected Task."""
metadata = dict(getattr(task, "metadata", {}) or {})
return work_item_projection_id_from_metadata(
metadata,
fallback=_clean(getattr(task, "id", "")),
)
def turn_type_for_task(task: Any, *, fallback: str = "execute") -> str:
"""Return the work-item turn type for a projected Task."""
metadata = dict(getattr(task, "metadata", {}) or {})
return work_item_turn_type_from_metadata(metadata, fallback=fallback)
def projection_id_for_work_item(item: Any) -> str:
"""Return the projection identity for a DelegationWorkItem-like object."""
explicit_projection = _clean(getattr(item, "projection_id", ""))
if explicit_projection:
return explicit_projection
metadata = dict(getattr(item, "metadata", {}) or {})
return work_item_projection_id_from_metadata(
metadata,
fallback=(
_clean(getattr(item, "projection_id", ""))
or _clean(getattr(item, "work_item_id", ""))
),
)
def turn_type_for_work_item(item: Any, *, fallback: str = "execute") -> str:
"""Return the turn type for a DelegationWorkItem-like object."""
metadata = dict(getattr(item, "metadata", {}) or {})
return work_item_turn_type_from_metadata(
metadata,
fallback=_clean(getattr(item, "kind", "")) or fallback,
)
def canonical_turn_type_for_work_item(item: Any, *, fallback: str = "execute") -> str:
"""Return a canonical turn type for a WorkItem-like object or metadata."""
if isinstance(item, Mapping):
return work_item_turn_type_from_metadata(item, fallback=fallback)
return turn_type_for_work_item(item, fallback=fallback)
def _turn_type_for_value(value: Any, *, fallback: str = "") -> str:
if isinstance(value, Mapping):
return work_item_turn_type_from_metadata(value, fallback=fallback)
if hasattr(value, "metadata"):
return canonical_turn_type_for_work_item(value, fallback=fallback or "execute")
return canonical_work_item_turn_type_for_kind(value, fallback=fallback)
def is_delivery_turn(value_or_metadata: Any) -> bool:
"""Return True for final delivery turns, including legacy ``delivery`` alias."""
return _turn_type_for_value(value_or_metadata, fallback="") == "deliver"
def is_manager_reviewable_turn(value_or_metadata: Any) -> bool:
"""Return True when a finished WorkItem should enter manager review flow."""
turn_type = _turn_type_for_value(value_or_metadata, fallback="")
if not turn_type:
return False
return turn_type not in {"intake", "plan", "dispatch", "aggregate", "deliver", "self_evolution"}
def mark_work_item_projection(
metadata: Mapping[str, Any] | None = None,
*,
projection_id: str = "",
turn_type: str = "",
) -> dict[str, Any]:
"""Return metadata with canonical work-item projection keys only."""
result = dict(metadata or {})
projection = _clean(projection_id) or work_item_projection_id_from_metadata(result)
turn = normalize_work_item_turn_type(turn_type) or work_item_turn_type_from_metadata(result)
if projection:
result[WORK_ITEM_PROJECTION_ID_KEY] = projection
if turn:
result[WORK_ITEM_TURN_TYPE_KEY] = turn
return result
def mark_projected_work_item_task(
metadata: Mapping[str, Any] | None = None,
*,
projection_id: str = "",
turn_type: str = "",
) -> dict[str, Any]:
"""Return projected task/work-item metadata with canonical identity keys."""
return mark_work_item_projection(
metadata,
projection_id=projection_id,
turn_type=turn_type,
)
def work_item_identity_payload(
*,
projection_id: str = "",
turn_type: str = "",
source: Mapping[str, Any] | None = None,
include_empty: bool = False,
) -> dict[str, str]:
"""Build a canonical event/checkpoint/ws payload identity fragment."""
source_meta = dict(source or {})
projection = _clean(projection_id) or work_item_projection_id_from_metadata(source_meta, fallback="")
turn = normalize_work_item_turn_type(turn_type) or work_item_turn_type_from_metadata(source_meta, fallback="")
payload: dict[str, str] = {}
if projection or include_empty:
payload[WORK_ITEM_PROJECTION_ID_KEY] = projection
if turn or include_empty:
payload[WORK_ITEM_TURN_TYPE_KEY] = turn
return payload
def work_item_identity_payload_from_metadata(
metadata: Mapping[str, Any] | None,
*,
projection_id_fallback: str = "",
turn_type_fallback: str = "",
include_empty: bool = False,
) -> dict[str, str]:
"""Build a canonical payload identity fragment from metadata."""
source_meta = dict(metadata or {})
return work_item_identity_payload(
projection_id=work_item_projection_id_from_metadata(
source_meta,
fallback=projection_id_fallback,
),
turn_type=work_item_turn_type_from_metadata(
source_meta,
fallback=turn_type_fallback,
),
include_empty=include_empty,
)
def work_item_identity_payload_for_task(
task: Any,
*,
fallback_turn_type: str = "",
include_empty: bool = False,
) -> dict[str, str]:
"""Build a canonical payload identity fragment for a Task-like object."""
if task is None:
return work_item_identity_payload(
turn_type=fallback_turn_type,
include_empty=include_empty,
)
return work_item_identity_payload(
projection_id=projection_id_for_task(task),
turn_type=turn_type_for_task(task, fallback=fallback_turn_type),
include_empty=include_empty,
)
def migrate_work_item_projection_metadata(
metadata: Mapping[str, Any] | None,
*,
projection_id_fallback: str = "",
turn_type_fallback: str = "",
) -> tuple[dict[str, Any], bool]:
"""Normalize canonical projection metadata from canonical inputs only."""
before = dict(metadata or {})
result = dict(before)
projection = work_item_projection_id_from_metadata(
result,
fallback=projection_id_fallback,
)
turn = work_item_turn_type_from_metadata(
result,
fallback=turn_type_fallback or "execute",
)
if projection and not _clean(result.get(WORK_ITEM_PROJECTION_ID_KEY)):
result[WORK_ITEM_PROJECTION_ID_KEY] = projection
if turn and not _clean(result.get(WORK_ITEM_TURN_TYPE_KEY)):
result[WORK_ITEM_TURN_TYPE_KEY] = turn
return result, result != before
def rework_projection_id_for_gate(gate: Any, *, fallback: str = "") -> str:
"""Return the gate rework target as a work-item projection identity."""
metadata = dict(getattr(gate, "metadata", {}) or {})
return _clean(
metadata.get(GATE_REWORK_PROJECTION_ID_KEY)
or getattr(gate, "rework_projection_id", "")
or fallback
)
def mark_gate_rework_projection(gate: Any, projection_id: str) -> Any:
"""Attach projection-only gate rework identity."""
projection = _clean(projection_id)
metadata = dict(getattr(gate, "metadata", {}) or {})
if projection:
metadata[GATE_REWORK_PROJECTION_ID_KEY] = projection
setattr(gate, "metadata", metadata)
if hasattr(gate, "rework_projection_id"):
setattr(gate, "rework_projection_id", projection or None)
return gate
def target_projection_id_for_decision(decision: Any, *, fallback: str = "") -> str:
"""Return a gate-harness target as a work-item projection identity."""
return _clean(
getattr(decision, GATE_TARGET_PROJECTION_ID_KEY, "")
or fallback
)
def target_projection_ids_for_decision(decision: Any) -> list[str]:
"""Return all gate-harness targets as work-item projection identities."""
raw_ids = list(getattr(decision, GATE_TARGET_PROJECTION_IDS_KEY, []) or [])
if not raw_ids:
single = target_projection_id_for_decision(decision)
raw_ids = [single] if single else []
result: list[str] = []
seen: set[str] = set()
for item in raw_ids:
value = _clean(item)
if value and value not in seen:
seen.add(value)
result.append(value)
return result
def gate_rework_payload(
*,
rework_projection_id: str = "",
target_projection_id: str = "",
review_projection_id: str = "",
) -> dict[str, Any]:
"""Build projection-only gate/rework payload metadata."""
rework = _clean(rework_projection_id)
target = _clean(target_projection_id)
review = _clean(review_projection_id)
payload: dict[str, Any] = {}
if rework:
payload[GATE_REWORK_PROJECTION_ID_KEY] = rework
if target:
payload[GATE_TARGET_PROJECTION_ID_KEY] = target
if review:
payload["review_projection_id"] = review
return payload
@@ -0,0 +1,31 @@
"""Runtime link helpers for company WorkItem <-> Task relations."""
from __future__ import annotations
from typing import Any, Iterable
def linked_work_item_id_for_task(task: Any | None) -> str:
"""Return the WorkItem id linked to a runtime Task.
The structured link table hydrates ``Task.linked_work_item_id``. Runtime
code should not use legacy Task metadata as a mapping source.
"""
if task is None:
return ""
return str(getattr(task, "linked_work_item_id", "") or "").strip()
def set_linked_work_item_id(task: Any | None, work_item_id: str) -> None:
if task is None:
return
setattr(task, "linked_work_item_id", str(work_item_id or "").strip())
def task_by_linked_work_item_id(tasks: Iterable[Any]) -> dict[str, Any]:
mapping: dict[str, Any] = {}
for task in tasks:
work_item_id = linked_work_item_id_for_task(task)
if work_item_id:
mapping[work_item_id] = task
return mapping
@@ -0,0 +1,60 @@
"""Helpers for identifying company work-item runtime metadata."""
from __future__ import annotations
from typing import Any, Mapping
WORK_ITEM_RUNTIME_KEY = "work_item_runtime"
WORK_ITEM_RUNTIME_VERSION_KEY = "work_item_runtime_version"
def is_work_item_runtime_metadata(metadata: Mapping[str, Any] | None) -> bool:
"""Return whether metadata belongs to the company work-item runtime."""
if not metadata:
return False
return bool(metadata.get(WORK_ITEM_RUNTIME_KEY, False))
def work_item_runtime_version(
metadata: Mapping[str, Any] | None,
*,
default: int = 1,
) -> int:
"""Read the canonical work-item runtime version."""
if not metadata:
return int(default)
raw = metadata.get(WORK_ITEM_RUNTIME_VERSION_KEY, default)
try:
version = int(raw)
except (TypeError, ValueError):
version = int(default)
return version if version > 0 else int(default)
def mark_work_item_runtime(
metadata: Mapping[str, Any] | None = None,
*,
version: int = 1,
) -> dict[str, Any]:
"""Return metadata marked for the company work-item runtime."""
result = dict(metadata or {})
result[WORK_ITEM_RUNTIME_KEY] = True
result[WORK_ITEM_RUNTIME_VERSION_KEY] = work_item_runtime_version(result, default=version)
return result
def migrate_work_item_runtime_metadata(
metadata: Mapping[str, Any] | None,
*,
default_version: int = 1,
) -> tuple[dict[str, Any], bool]:
"""Normalize canonical work-item runtime metadata."""
before = dict(metadata or {})
result = dict(before)
if result.get(WORK_ITEM_RUNTIME_KEY, False):
result[WORK_ITEM_RUNTIME_VERSION_KEY] = work_item_runtime_version(
result,
default=default_version,
)
return result, result != before
@@ -0,0 +1,529 @@
"""Diagnostics for company WorkItem runtime Task projections."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Iterable, Mapping
from opc.core.models import DelegationWorkItem, Phase, Task
from opc.layer2_organization.phase import (
DONE_PHASES,
is_report_execution_work_item_metadata,
is_review_execution_work_item_metadata,
should_hide_work_item_from_company_kanban,
)
from opc.layer2_organization.metadata_ownership import validate_metadata_ownership
from opc.layer2_organization.work_item_identity import (
WORK_ITEM_TURN_TYPE_KEY,
canonical_work_item_turn_type_for_kind,
normalize_work_item_turn_type,
projection_id_for_work_item,
work_item_projection_id_from_metadata,
)
from opc.layer2_organization.work_item_links import linked_work_item_id_for_task
from opc.layer2_organization.work_item_runtime import is_work_item_runtime_metadata
WORK_ITEM_RUNTIME_INVARIANT_EVENT_TYPE = "work_item_runtime_invariant_violation"
_RUNTIME_MODELS = {"multi_team_org"}
_AUXILIARY_TURN_KINDS = {"review", "report", "followup", "follow_up", "delivery", "deliver"}
_HIDDEN_RUNNABLE_TURN_KINDS = _AUXILIARY_TURN_KINDS | {"aggregate", "synthesize"}
@dataclass(frozen=True)
class WorkItemRuntimeInvariantIssue:
code: str
severity: str
run_id: str = ""
work_item_id: str = ""
runtime_task_id: str = ""
projection_id: str = ""
message: str = ""
details: dict[str, Any] = field(default_factory=dict)
def fingerprint(self) -> tuple[str, str, str, str]:
return (
str(self.code or "").strip(),
str(self.work_item_id or "").strip(),
str(self.runtime_task_id or "").strip(),
str(self.projection_id or "").strip(),
)
def to_event_payload(self) -> dict[str, Any]:
payload = {
"code": self.code,
"severity": self.severity,
"run_id": self.run_id,
"work_item_id": self.work_item_id,
"runtime_task_id": self.runtime_task_id,
"projection_id": self.projection_id,
"message": self.message,
}
if self.details:
payload["details"] = dict(self.details)
return payload
def _clean(value: Any) -> str:
return str(value or "").strip()
def _lower(value: Any) -> str:
return _clean(value).lower()
def _phase_value(value: Any) -> str:
if isinstance(value, Phase):
return value.value
return _lower(value)
def _is_done_phase(value: Any) -> bool:
if isinstance(value, Phase):
return value in DONE_PHASES
normalized = _lower(value)
return normalized in {phase.value for phase in DONE_PHASES}
def _runtime_model(metadata: Mapping[str, Any]) -> str:
return _lower(metadata.get("runtime_model") or metadata.get("execution_model"))
def _turn_kind(metadata: Mapping[str, Any], *, fallback: str = "") -> str:
turn = normalize_work_item_turn_type(metadata.get(WORK_ITEM_TURN_TYPE_KEY), fallback="")
if turn:
return turn
for key in ("work_kind", "delegation_turn_kind"):
owner_kind = canonical_work_item_turn_type_for_kind(metadata.get(key), fallback="")
if owner_kind:
return owner_kind
return canonical_work_item_turn_type_for_kind(fallback, fallback="")
def _task_projection_id(task: Task | None) -> str:
if task is None:
return ""
return work_item_projection_id_from_metadata(dict(task.metadata or {}), fallback=_clean(getattr(task, "id", "")))
def _issue(
code: str,
severity: str,
*,
task: Task | None = None,
work_item: DelegationWorkItem | None = None,
work_item_id: str = "",
projection_id: str = "",
message: str,
details: Mapping[str, Any] | None = None,
) -> WorkItemRuntimeInvariantIssue:
task_metadata = dict(getattr(task, "metadata", {}) or {})
item_metadata = dict(getattr(work_item, "metadata", {}) or {})
wid = (
_clean(work_item_id)
or _clean(getattr(work_item, "work_item_id", ""))
or linked_work_item_id_for_task(task)
)
projection = _clean(projection_id) or _task_projection_id(task)
if not projection and work_item is not None:
projection = projection_id_for_work_item(work_item)
return WorkItemRuntimeInvariantIssue(
code=code,
severity=severity,
run_id=(
_clean(getattr(work_item, "run_id", ""))
or _clean(item_metadata.get("delegation_run_id"))
or _clean(task_metadata.get("delegation_run_id"))
),
work_item_id=wid,
runtime_task_id=_clean(getattr(task, "id", "")),
projection_id=projection,
message=message,
details=dict(details or {}),
)
def is_company_runtime_projection_task(task: Task | None) -> bool:
"""Return true for company WorkItem runtime projection Tasks.
The predicate intentionally does not read legacy owner metadata. A task is
a projection only when it carries the canonical runtime marker and the
canonical projection identity.
"""
if task is None:
return False
metadata = dict(getattr(task, "metadata", {}) or {})
if not is_work_item_runtime_metadata(metadata):
return False
if bool(metadata.get("synthetic_inbox_turn") or metadata.get("synthetic_company_inbox")):
return False
runtime_model = _runtime_model(metadata)
if runtime_model and runtime_model not in _RUNTIME_MODELS:
return False
return bool(work_item_projection_id_from_metadata(metadata, fallback=""))
def _expected_work_kind(work_item: DelegationWorkItem | None) -> str:
if work_item is None:
return ""
metadata = dict(work_item.metadata or {})
return _turn_kind(metadata, fallback=_clean(getattr(work_item, "kind", "")) or "execute") or "execute"
def _expected_role_id(work_item: DelegationWorkItem | None) -> str:
return _clean(getattr(work_item, "role_id", "")) if work_item is not None else ""
def _expected_seat_id(work_item: DelegationWorkItem | None) -> str:
if work_item is None:
return ""
return _clean(getattr(work_item, "seat_id", "")) or _clean(dict(work_item.metadata or {}).get("seat_id"))
def _expected_turn_mode(work_item: DelegationWorkItem | None) -> str:
if work_item is None:
return ""
return _clean(dict(work_item.metadata or {}).get("current_turn_mode"))
def validate_work_item_runtime_projection(
task: Task,
work_item: DelegationWorkItem | None,
*,
work_item_by_id: Mapping[str, DelegationWorkItem] | None = None,
) -> list[WorkItemRuntimeInvariantIssue]:
"""Validate that a projected runtime Task is backed by a WorkItem link.
This function is read-only. It reports mismatches; it never repairs links
and never uses legacy Task metadata as an owner source.
"""
if not is_company_runtime_projection_task(task):
return []
task_metadata = dict(getattr(task, "metadata", {}) or {})
item_metadata = dict(getattr(work_item, "metadata", {}) or {}) if work_item is not None else {}
linked_work_item_id = linked_work_item_id_for_task(task)
projection_id = _task_projection_id(task)
issues: list[WorkItemRuntimeInvariantIssue] = []
if not linked_work_item_id:
issues.append(
_issue(
"missing_link",
"error",
task=task,
work_item=work_item,
projection_id=projection_id,
message="Company runtime projection task is not hydrated from work_item_runtime_links.",
)
)
if work_item is None:
issues.append(
_issue(
"missing_work_item",
"error",
task=task,
work_item_id=linked_work_item_id,
projection_id=projection_id,
message="Company runtime projection task link does not resolve to a WorkItem.",
)
)
return issues
expected_work_item_id = _clean(getattr(work_item, "work_item_id", ""))
if linked_work_item_id and expected_work_item_id and linked_work_item_id != expected_work_item_id:
issues.append(
_issue(
"link_work_item_mismatch",
"error",
task=task,
work_item=work_item,
work_item_id=linked_work_item_id,
projection_id=projection_id,
message="Runtime task link points at a different WorkItem than the supplied WorkItem.",
details={"expected_work_item_id": expected_work_item_id, "linked_work_item_id": linked_work_item_id},
)
)
expected_projection_id = projection_id_for_work_item(work_item)
if projection_id and expected_projection_id and projection_id != expected_projection_id:
issues.append(
_issue(
"projection_mismatch",
"warning",
task=task,
work_item=work_item,
projection_id=projection_id,
message="Runtime task projection id differs from the WorkItem projection id.",
details={"expected_projection_id": expected_projection_id, "task_projection_id": projection_id},
)
)
expected_kind = _expected_work_kind(work_item)
task_kind = _turn_kind(task_metadata, fallback=_lower(task_metadata.get("work_kind")) or expected_kind)
if expected_kind and task_kind and task_kind != expected_kind:
issues.append(
_issue(
"work_kind_mismatch",
"error",
task=task,
work_item=work_item,
projection_id=projection_id,
message="Runtime task turn kind differs from the WorkItem turn kind.",
details={"expected_work_kind": expected_kind, "task_work_kind": task_kind},
)
)
expected_role_id = _expected_role_id(work_item)
task_role_id = _clean(getattr(task, "assigned_to", "")) or _clean(task_metadata.get("work_item_role_id"))
if expected_role_id and task_role_id and task_role_id != expected_role_id:
issues.append(
_issue(
"owner_role_mismatch",
"error",
task=task,
work_item=work_item,
projection_id=projection_id,
message="Runtime task owner role differs from the WorkItem role.",
details={"expected_role_id": expected_role_id, "task_role_id": task_role_id},
)
)
expected_seat_id = _expected_seat_id(work_item)
task_seat_id = _clean(task_metadata.get("delegation_seat_id") or task_metadata.get("seat_id"))
if expected_seat_id and task_seat_id and task_seat_id != expected_seat_id:
issues.append(
_issue(
"owner_seat_mismatch",
"error",
task=task,
work_item=work_item,
projection_id=projection_id,
message="Runtime task owner seat differs from the WorkItem seat.",
details={"expected_seat_id": expected_seat_id, "task_seat_id": task_seat_id},
)
)
expected_turn_mode = _expected_turn_mode(work_item)
task_turn_mode = _clean(task_metadata.get("current_turn_mode"))
if expected_turn_mode:
if task_turn_mode and task_turn_mode != expected_turn_mode:
issues.append(
_issue(
"turn_mode_mismatch",
"error",
task=task,
work_item=work_item,
projection_id=projection_id,
message="Runtime task current_turn_mode differs from the WorkItem mode.",
details={"expected_turn_mode": expected_turn_mode, "task_turn_mode": task_turn_mode},
)
)
elif not task_turn_mode and expected_kind in _AUXILIARY_TURN_KINDS:
issues.append(
_issue(
"turn_mode_missing",
"warning",
task=task,
work_item=work_item,
projection_id=projection_id,
message="Auxiliary runtime task is missing the WorkItem current_turn_mode execution copy.",
details={"expected_turn_mode": expected_turn_mode, "work_kind": expected_kind},
)
)
has_work_item_context = work_item_by_id is not None
work_item_by_id = dict(work_item_by_id or {})
if is_review_execution_work_item_metadata(item_metadata):
target_id = _clean(item_metadata.get("review_target_work_item_id"))
target = work_item_by_id.get(target_id) if target_id else None
if not target_id:
issues.append(
_issue(
"review_target_missing",
"error",
task=task,
work_item=work_item,
projection_id=projection_id,
message="Review runtime WorkItem is missing review_target_work_item_id.",
)
)
elif has_work_item_context and target is None:
issues.append(
_issue(
"review_target_unresolved",
"error",
task=task,
work_item=work_item,
projection_id=projection_id,
message="Review runtime WorkItem target does not exist in the active WorkItem set.",
details={"review_target_work_item_id": target_id},
)
)
elif target is not None:
target_metadata = dict(target.metadata or {})
expected_review_role = (
_clean(target_metadata.get("review_owner_role_id"))
or _clean(getattr(target, "manager_role_id", ""))
)
expected_review_seat = (
_clean(target_metadata.get("review_owner_seat_id"))
or _clean(getattr(target, "manager_seat_id", ""))
)
if expected_review_role and expected_review_role != expected_role_id:
issues.append(
_issue(
"review_owner_mismatch",
"error",
task=task,
work_item=work_item,
projection_id=projection_id,
message="Review WorkItem owner role does not match the target WorkItem review owner.",
details={"expected_review_role_id": expected_review_role, "review_work_item_role_id": expected_role_id},
)
)
if expected_review_seat and expected_review_seat != expected_seat_id:
issues.append(
_issue(
"review_owner_mismatch",
"error",
task=task,
work_item=work_item,
projection_id=projection_id,
message="Review WorkItem owner seat does not match the target WorkItem review owner.",
details={"expected_review_seat_id": expected_review_seat, "review_work_item_seat_id": expected_seat_id},
)
)
review_meta_role = _clean(item_metadata.get("review_owner_role_id"))
review_meta_seat = _clean(item_metadata.get("review_owner_seat_id"))
if expected_review_role and review_meta_role and review_meta_role != expected_review_role:
issues.append(
_issue(
"review_owner_metadata_mismatch",
"warning",
task=task,
work_item=work_item,
projection_id=projection_id,
message="Review WorkItem metadata owner role differs from the target review owner.",
details={"expected_review_role_id": expected_review_role, "metadata_review_owner_role_id": review_meta_role},
)
)
if expected_review_seat and review_meta_seat and review_meta_seat != expected_review_seat:
issues.append(
_issue(
"review_owner_metadata_mismatch",
"warning",
task=task,
work_item=work_item,
projection_id=projection_id,
message="Review WorkItem metadata owner seat differs from the target review owner.",
details={"expected_review_seat_id": expected_review_seat, "metadata_review_owner_seat_id": review_meta_seat},
)
)
if is_report_execution_work_item_metadata(item_metadata):
target_id = _clean(item_metadata.get("report_target_work_item_id"))
if not target_id:
issues.append(
_issue(
"report_target_missing",
"error",
task=task,
work_item=work_item,
projection_id=projection_id,
message="Report runtime WorkItem is missing report_target_work_item_id.",
)
)
elif has_work_item_context and target_id not in work_item_by_id:
issues.append(
_issue(
"report_target_unresolved",
"error",
task=task,
work_item=work_item,
projection_id=projection_id,
message="Report runtime WorkItem target does not exist in the active WorkItem set.",
details={"report_target_work_item_id": target_id},
)
)
for ownership_issue in validate_metadata_ownership(work_item, task):
issues.append(
_issue(
ownership_issue.code,
ownership_issue.severity,
task=task,
work_item=work_item,
projection_id=projection_id,
message=ownership_issue.message,
details={
"metadata_key": ownership_issue.key,
"owner": ownership_issue.owner,
**dict(ownership_issue.details or {}),
},
)
)
return issues
def _work_item_expects_runtime_projection(work_item: DelegationWorkItem) -> bool:
metadata = dict(work_item.metadata or {})
if not is_work_item_runtime_metadata(metadata):
return False
runtime_model = _runtime_model(metadata)
if runtime_model and runtime_model not in _RUNTIME_MODELS:
return False
if _is_done_phase(getattr(work_item, "phase", "")):
return False
kind = _expected_work_kind(work_item)
if should_hide_work_item_from_company_kanban(metadata) and kind not in _HIDDEN_RUNNABLE_TURN_KINDS:
return False
return bool(projection_id_for_work_item(work_item))
def diagnose_work_item_runtime_projections(
tasks: Iterable[Task],
work_items: Iterable[DelegationWorkItem],
) -> list[WorkItemRuntimeInvariantIssue]:
"""Return projection invariant issues without mutating Tasks or the DB."""
task_list = list(tasks or [])
work_item_list = list(work_items or [])
work_item_by_id = {
_clean(getattr(item, "work_item_id", "")): item
for item in work_item_list
if _clean(getattr(item, "work_item_id", ""))
}
issues: list[WorkItemRuntimeInvariantIssue] = []
linked_work_item_ids: set[str] = set()
for task in task_list:
if not is_company_runtime_projection_task(task):
continue
linked_work_item_id = linked_work_item_id_for_task(task)
if linked_work_item_id:
linked_work_item_ids.add(linked_work_item_id)
issues.extend(
validate_work_item_runtime_projection(
task,
work_item_by_id.get(linked_work_item_id) if linked_work_item_id else None,
work_item_by_id=work_item_by_id,
)
)
for work_item in work_item_list:
wid = _clean(getattr(work_item, "work_item_id", ""))
if not wid or wid in linked_work_item_ids:
continue
if not _work_item_expects_runtime_projection(work_item):
continue
issues.append(
_issue(
"work_item_missing_runtime_task",
"error",
work_item=work_item,
projection_id=projection_id_for_work_item(work_item),
message="Non-terminal company WorkItem has no linked runtime Task projection after materialization.",
details={"phase": _phase_value(getattr(work_item, "phase", "")), "work_kind": _expected_work_kind(work_item)},
)
)
return issues
@@ -0,0 +1,739 @@
"""Single authoritative entry point for WorkItem phase changes.
This module exposes ``transition_work_item`` the only function in the
codebase that should mutate a ``DelegationWorkItem.phase``. Its purpose is
to centralise the "phase is the single source of truth" invariant that
``phase.py`` always claimed but code never enforced.
Everything downstream of phase (``Task.status``, ``DelegationRoleSession
.status`` (DB), ``CompanyMemberSession.status`` (memory), UI kanban
column) is synced via the registered phase-transition hooks, so callers
only need to think about "what phase should this card be in now".
Direct writes like ``task.status = TaskStatus.CANCELLED`` or
``session.status = "idle"`` bypass the hook chain and guarantee cross-layer
state desync the exact pattern that produced the parent-resume and
stop-cascade bugs in new11 app05. See ``plans/task-key-proud-blum.md``.
This module also exposes ``refresh_dependents_for_run`` the dependency
frontier pass that propagates child completion (or terminal state) to
parent work items. Lifted out of CompanyMode so a phase-transition hook
can invoke it on any terminal/escalation transition without an import
cycle. See Fix 3 in ``memory/company-mode-stuck-bugs.md``.
"""
from __future__ import annotations
from contextvars import ContextVar
from datetime import datetime
from typing import Any
from loguru import logger
from opc.core.models import DelegationEvent, DelegationWorkItem, Phase, Task, TaskStatus
from opc.layer2_organization.phase import (
DONE_PHASES,
InvalidPhaseTransition,
coerce_phase,
phase_for_task_status,
task_status_for_phase,
validate_transition,
)
from opc.layer2_organization.work_item_links import linked_work_item_id_for_task
from opc.layer2_organization.work_item_identity import work_item_identity_payload
from opc.layer2_organization.work_item_runtime import is_work_item_runtime_metadata
async def transition_work_item(
store: Any,
work_item_id: str,
*,
target_phase: Phase | str,
reason: str,
summary: str | None = None,
metadata_updates: dict[str, Any] | None = None,
release_claim: bool = False,
) -> DelegationWorkItem | None:
"""Transition a work item to ``target_phase``.
The function wraps ``store.update_delegation_work_item(phase=...)``
which already validates the transition against the state-machine table
(``ALLOWED_TRANSITIONS`` in phase.py) and fires the full
``on_phase_transition`` hook chain. This wrapper adds:
- A mandatory ``reason`` string stamped into metadata for audit
- An optional ``release_claim`` flag that clears
``claimed_by_role_runtime_session_id`` / ``claimed_by_seat_id`` so
``is_orphaned`` becomes True and the dispatcher re-picks the card
Args:
store: OPCStore instance; must expose ``update_delegation_work_item``.
work_item_id: Target work item id.
target_phase: ``Phase`` enum or string (coerced via ``coerce_phase``).
reason: Short human-readable reason; recorded in metadata for audit.
summary: Optional summary string persisted on the work item.
metadata_updates: Extra metadata keys to merge onto the work item.
release_claim: When True, clears the current claim so the dispatcher
can re-acquire. Useful for cancel / timeout / forced-release paths.
Returns:
The updated ``DelegationWorkItem``, or ``None`` when the store lacks
the required API or the work item does not exist.
"""
if not store or not hasattr(store, "update_delegation_work_item"):
logger.warning(
"transition_work_item: store lacks update_delegation_work_item"
)
return None
phase = coerce_phase(target_phase)
merged: dict[str, Any] = dict(metadata_updates or {})
reason_clean = str(reason or "").strip()
if reason_clean:
merged["last_transition_reason"] = reason_clean
kwargs: dict[str, Any] = {
"phase": phase,
"metadata_updates": merged,
}
if summary is not None:
kwargs["summary"] = summary
# Phase transition first, claim release second (when requested). The
# old rationale for this ordering was "sync_member_session_hook reads
# item.claimed_by_role_runtime_session_id"; Phase B removed that hook
# and moved the unpark to the dispatcher's per-tick rehydrate pass,
# but the two-step ordering is kept so downstream listeners that
# still inspect the claim (audit logs, kanban projections) see a
# consistent before/after.
try:
result = await store.update_delegation_work_item(work_item_id, **kwargs)
except Exception:
logger.opt(exception=True).warning(
f"transition_work_item failed wid={work_item_id} "
f"target={phase.value} reason={reason_clean}"
)
raise
if release_claim and result is not None:
try:
result = await store.update_delegation_work_item(
work_item_id,
claimed_by_role_runtime_session_id="",
claimed_by_seat_id="",
)
except Exception:
logger.opt(exception=True).warning(
f"transition_work_item: claim release failed wid={work_item_id}"
)
return result
def _fallback_status_for(
target_status_or_phase: TaskStatus | Phase | str,
task: Task,
) -> TaskStatus | None:
"""Pre-compute the TaskStatus to assign locally when the work-item
transition cannot happen (no linked work_item, no store). Mirrors the
projection the live hook would apply, so task-mode callers see the
same local result whether or not a work_item exists.
"""
try:
if isinstance(target_status_or_phase, TaskStatus):
return target_status_or_phase
if isinstance(target_status_or_phase, Phase):
return task_status_for_phase(target_status_or_phase)
if isinstance(target_status_or_phase, str):
raw = target_status_or_phase.strip().lower()
try:
return task_status_for_phase(Phase(raw))
except ValueError:
try:
return TaskStatus(raw)
except ValueError:
return None
except Exception:
return None
return None
async def transition_work_item_from_task(
store: Any,
task: Task,
*,
target_status_or_phase: TaskStatus | Phase | str,
reason: str,
summary: str | None = None,
metadata_updates: dict[str, Any] | None = None,
release_claim: bool = False,
require_work_item: bool = False,
) -> bool:
"""Task bridge helper: transition a work item when the caller holds a Task.
Designed as the replacement for direct ``task.status = ...`` writes in
company-mode code. Resolves the linked work item via the hydrated runtime
link table id (falling back to legacy metadata for old rows), coerces the
desired state into a Phase, and delegates to ``transition_work_item``.
``target_status_or_phase`` may be:
* a ``Phase`` (or phase-string) used verbatim.
* a ``TaskStatus`` (or status-string) projected via
``phase_for_task_status``. BLOCKED disambiguation uses
``task.metadata['delegation_pending_work_item_ids']`` to distinguish
``WAITING_FOR_CHILDREN`` (has pending children) from ``PAUSED``.
This preserves the old task-status projection semantics.
Forward-invalid transitions (e.g. a late async callback arriving after
the work item was already moved by a reviewer) are silently preserved
rather than raising. We log at DEBUG so the race is observable without
being noisy.
**Local task.status sync**: after the work item transition, the registered
``sync_task_status_hook`` updates the DB task.status. The caller still
holds a local ``task`` object whose ``status`` is now stale and any
subsequent ``save_task(task)`` would overwrite the hook's DB update with
the stale value (race). To avoid that, we eagerly project the target
Phase back to a TaskStatus and assign it to the local ``task.status``
in-memory. This is NOT a direct DB write; it keeps the caller's in-memory
view consistent with what the DB now holds.
**task-mode fallback**: when there's no linked work item (task-mode
path or pre-materialization), the helper returns ``False`` but still
syncs the local ``task.status`` to the caller's intended value. This
lets company-mode call sites be migrated to this helper without each
one needing its own ``task.status = ...`` fallback the task-mode
execution path just sees the local mutation and a subsequent
``save_task(task)`` by the caller persists it.
``require_work_item=True`` is for company-mode runtime call sites where
falling back to a local Task.status write would reintroduce drift. In
that mode missing store/link returns ``False`` without mutating local
status.
Returns ``True`` when a work-item transition was issued (including the
silent-degrade no-op case). Returns ``False`` when there is no linked
work item; the local ``task.status`` is only synced when
``require_work_item`` is false.
"""
# Pre-resolve the fallback status so task-mode / pre-materialization
# paths still end up with a synced local task.status before we bail.
fallback_status = _fallback_status_for(target_status_or_phase, task)
if not store or not hasattr(store, "update_delegation_work_item"):
if fallback_status is not None and not require_work_item:
task.status = fallback_status
return False
work_item_id = linked_work_item_id_for_task(task)
if not work_item_id and hasattr(store, "get_work_item_for_runtime_task"):
try:
linked_item = await store.get_work_item_for_runtime_task(task.id)
except Exception:
linked_item = None
work_item_id = str(getattr(linked_item, "work_item_id", "") or "").strip()
if not work_item_id:
if fallback_status is not None and not require_work_item:
task.status = fallback_status
return False
# Coerce target to Phase, applying BLOCKED → WAITING_FOR_CHILDREN/PAUSED
# disambiguation from task metadata.
if isinstance(target_status_or_phase, Phase):
target_phase: Phase = target_status_or_phase
elif isinstance(target_status_or_phase, TaskStatus):
has_pending_children = bool(
(task.metadata or {}).get("delegation_pending_work_item_ids") or []
)
target_phase = phase_for_task_status(
target_status_or_phase,
has_pending_children=has_pending_children,
)
elif isinstance(target_status_or_phase, str):
raw = target_status_or_phase.strip().lower()
try:
target_phase = Phase(raw)
except ValueError:
try:
ts = TaskStatus(raw)
except ValueError as exc:
raise ValueError(
f"transition_work_item_from_task: target_status_or_phase {target_status_or_phase!r} "
"is neither a valid Phase nor TaskStatus"
) from exc
has_pending_children = bool(
(task.metadata or {}).get("delegation_pending_work_item_ids") or []
)
target_phase = phase_for_task_status(
ts, has_pending_children=has_pending_children
)
else:
raise TypeError(
"transition_work_item_from_task: target_status_or_phase must be "
f"Phase | TaskStatus | str, got {type(target_status_or_phase).__name__}"
)
# Silent-degrade guard against late async races (see docstring). Look up
# the persisted phase and preserve it if the desired transition is not
# in ALLOWED_TRANSITIONS. This keeps shared role-session callbacks
# crash-free when a late writer observes stale task state.
persisted_phase: Phase | None = None
if hasattr(store, "get_delegation_work_item"):
try:
persisted_item = await store.get_delegation_work_item(work_item_id)
except Exception:
persisted_item = None
if persisted_item is not None:
persisted_phase = getattr(persisted_item, "phase", None)
if target_phase != persisted_phase and persisted_phase is not None:
try:
validate_transition(persisted_phase, target_phase)
except InvalidPhaseTransition:
logger.debug(
"transition_work_item_from_task: preserving persisted phase "
f"{persisted_phase.value} for work_item={work_item_id} "
f"(projected {target_phase.value} would be an invalid transition)"
)
return True
# Always stamp the task-id / task-status back-reference so audit and
# reverse lookup stay consistent. Callers can layer extra metadata on top.
back_ref: dict[str, Any] = {
"task_id": task.id,
"task_status": (
target_status_or_phase.value
if isinstance(target_status_or_phase, (TaskStatus, Phase))
else str(target_status_or_phase)
),
}
if metadata_updates:
back_ref.update(metadata_updates)
try:
await transition_work_item(
store,
work_item_id,
target_phase=target_phase,
reason=reason,
summary=summary,
metadata_updates=back_ref,
release_claim=release_claim,
)
except InvalidPhaseTransition:
# Defensive: state-machine validation at the store layer can also
# raise. Degrade the same way the pre-check does, for the race
# where persisted_phase changed between our lookup and the write.
logger.debug(
f"transition_work_item_from_task: store-layer rejected "
f"{persisted_phase}{target_phase.value} for wid={work_item_id} "
"(concurrent writer); degrading to no-op."
)
return True
# Sync local task.status to match the target phase so any subsequent
# save_task(task) by the caller doesn't race with the hook's DB update.
# The assignment goes through task_status_for_phase() — not a literal
# TaskStatus.CANCELLED/FAILED — so the DirectStatusWriteLintTest regex
# doesn't flag it as a bypass.
try:
task.status = task_status_for_phase(target_phase)
except Exception:
logger.opt(exception=True).debug(
"transition_work_item_from_task: local status sync failed"
)
return True
async def apply_task_status_transition(
store: Any,
task: Task,
*,
target_status_or_phase: TaskStatus | Phase | str,
reason: str,
summary: str | None = None,
metadata_updates: dict[str, Any] | None = None,
release_claim: bool = False,
save_plain_task: bool = True,
raise_on_missing_work_item: bool = True,
) -> bool:
"""Apply a task status intent through the right source of truth.
Company WorkItem runtime tasks must transition their linked WorkItem phase;
plain task-mode tasks keep the legacy Task.status behavior through the
fallback branch in ``transition_work_item_from_task``.
"""
metadata = dict(getattr(task, "metadata", {}) or {})
company_runtime = bool(
linked_work_item_id_for_task(task)
or is_work_item_runtime_metadata(metadata)
)
transitioned = await transition_work_item_from_task(
store,
task,
target_status_or_phase=target_status_or_phase,
reason=reason,
summary=summary,
metadata_updates=metadata_updates,
release_claim=release_claim,
require_work_item=company_runtime,
)
if company_runtime:
if not transitioned and raise_on_missing_work_item:
target = (
target_status_or_phase.value
if isinstance(target_status_or_phase, (TaskStatus, Phase))
else str(target_status_or_phase)
)
raise RuntimeError(
"company runtime task cannot transition without a linked WorkItem: "
f"task={getattr(task, 'id', '')} target={target}"
)
return transitioned
if save_plain_task and store and hasattr(store, "save_task"):
await store.save_task(task)
return transitioned
# Re-entrancy guard: refresh_dependents_for_run writes to work items, which
# fires phase-transition hooks, which can re-call refresh. The outer call
# already walks every item in the run, so inner calls on the same run_id
# are redundant — silently skip them. Module-level ContextVar because the
# dispatcher runs async tasks, and we want per-task isolation.
_REFRESH_IN_FLIGHT: ContextVar[frozenset[str]] = ContextVar(
"refresh_dependents_in_flight", default=frozenset()
)
_SYNTHESIS_SKIP_KINDS: frozenset[str] = frozenset({
"aggregate",
"deliver",
"delivery",
"intake",
"review",
"synthesis",
"synthesize",
})
def _work_item_id(item: DelegationWorkItem | Any | None) -> str:
return str(getattr(item, "work_item_id", "") or "").strip()
def _dependency_replacement_ids(item: DelegationWorkItem | Any | None) -> list[str]:
metadata = dict(getattr(item, "metadata", {}) or {}) if item is not None else {}
raw = (
metadata.get("replacement_dependency_work_item_ids")
or metadata.get("replacement_work_item_ids")
or metadata.get("superseded_by_work_item_ids")
or []
)
if isinstance(raw, str):
raw = [raw]
try:
values = list(raw or [])
except TypeError:
values = [raw]
return list(dict.fromkeys(str(value).strip() for value in values if str(value).strip()))
def is_prunable_dependency_work_item(item: DelegationWorkItem | Any | None) -> bool:
"""True when a dependency target is obsolete rather than merely failed.
A normal CANCELLED/FAILED dependency is still meaningful and should keep
the parent from silently succeeding. Manager-deleted or hidden cancelled
cards are different: they are explicit graph edits, so stale references to
them must be removed or replaced whenever the dependency frontier refreshes.
"""
if item is None:
return False
metadata = dict(getattr(item, "metadata", {}) or {})
if bool(metadata.get("deleted_by_manager_tool", False)):
return True
upstream_visibility = str(metadata.get("upstream_visibility", "") or "").strip().lower()
return (
getattr(item, "phase", None) == Phase.CANCELLED
and bool(metadata.get("hidden_from_company_kanban", False))
and upstream_visibility == "hidden"
)
def normalize_dependency_work_item_ids(
raw_dependency_ids: list[str] | tuple[str, ...] | set[str],
work_item_by_id: dict[str, DelegationWorkItem | Any],
*,
owner_work_item_id: str = "",
) -> tuple[list[str], list[str]]:
"""Drop or replace stale dependency ids while preserving hard failures.
Returns ``(active_ids, pruned_ids)``. Replacement ids come from metadata on
the obsolete dependency target, and are themselves validated against the
current run graph so a deleted replacement cannot resurrect another stale
edge.
"""
owner_id = str(owner_work_item_id or "").strip()
active: list[str] = []
pruned: list[str] = []
def append_active(candidate_id: str) -> None:
candidate = str(candidate_id or "").strip()
if not candidate or candidate == owner_id:
if candidate:
pruned.append(candidate)
return
item = work_item_by_id.get(candidate)
if is_prunable_dependency_work_item(item):
pruned.append(candidate)
return
active.append(candidate)
for raw_id in list(raw_dependency_ids or []):
dep_id = str(raw_id or "").strip()
if not dep_id:
continue
item = work_item_by_id.get(dep_id)
if is_prunable_dependency_work_item(item):
pruned.append(dep_id)
for replacement_id in _dependency_replacement_ids(item):
append_active(replacement_id)
continue
append_active(dep_id)
return (
list(dict.fromkeys(active)),
list(dict.fromkeys(pruned)),
)
def _work_item_kind(item: DelegationWorkItem, metadata: dict[str, Any]) -> str:
return str(
metadata.get("work_kind")
or metadata.get("delegation_turn_kind")
or item.kind
or ""
).strip().lower()
def _should_enter_synthesis_turn(
item: DelegationWorkItem,
metadata: dict[str, Any],
dependency_ids: list[str],
) -> bool:
if item.phase != Phase.WAITING_FOR_CHILDREN:
return False
if not dependency_ids:
return False
if bool(metadata.get("synthesis_turn_started", False)):
return False
if _work_item_kind(item, metadata) in _SYNTHESIS_SKIP_KINDS:
return False
if not (
bool(metadata.get("delegated_children_pending", False))
or str(metadata.get("frontier", "") or "").strip() == "waiting_for_children"
or str(metadata.get("last_delegated_by_seat_id", "") or "").strip()
):
return False
return True
def _synthesis_turn_summary(item: DelegationWorkItem, dependency_ids: list[str]) -> str:
title = str(item.title or "delegated work").strip()
child_count = len(dependency_ids)
manager_label = str(item.manager_role_id or "the upstream owner").strip()
return (
f"Synthesize the {child_count} approved child work item"
f"{'' if child_count == 1 else 's'} for `{title}` and prepare the "
f"handoff for {manager_label}. Include what was completed, evidence, "
"remaining risks, and any decision needed from the upper role."
)
async def refresh_dependents_for_run(
store: Any,
*,
run_id: str,
source_work_item_id: str | None = None,
source_task_id: str | None = None,
source_role_id: str | None = None,
source_cell_id: str | None = None,
) -> bool:
"""Walk all work items in ``run_id`` and propagate dependency state
to parent phases.
**What this does in one pass**:
- ``WAITING_DEPENDENCIES READY`` (or ``READY_FOR_REWORK`` when the
item carries an outstanding rework_feedback) when all deps approved.
- ``WAITING_FOR_CHILDREN READY`` as a synthesis turn when delegated
children are all approved; otherwise ``WAITING_FOR_CHILDREN RUNNING``.
Both paths release the parent's stale claim so the dispatcher can
re-pick it cleanly.
- Reverse direction: a RUNNING item whose deps regress (new dep
appeared) goes to ``WAITING_FOR_CHILDREN``; a READY item to
``WAITING_DEPENDENCIES``.
**Who calls this**:
1. ``CompanyMode._refresh_delegation_dependents`` preserves the
explicit call from APPROVED-verdict paths (belt-and-suspenders).
2. ``phase_hooks.refresh_dependents_hook`` fires on every terminal
transition (APPROVED / FAILED / CANCELLED) and on AWAITING_HUMAN,
so the frontier refreshes automatically. Without the hook, a
child escalating to AWAITING_HUMAN (or a human-approved
AWAITING_HUMAN APPROVED click) would never unblock its parent.
Returns True when any parent was mutated (for cheap change detection
in callers that want to emit a downstream event).
"""
if not store or not run_id:
return False
if not hasattr(store, "list_delegation_work_items") or not hasattr(
store, "update_delegation_work_item"
):
return False
in_flight = _REFRESH_IN_FLIGHT.get()
if run_id in in_flight:
return False
token = _REFRESH_IN_FLIGHT.set(in_flight | {run_id})
try:
try:
work_items = await store.list_delegation_work_items(run_id)
except Exception:
logger.opt(exception=True).debug(
f"refresh_dependents_for_run: list_delegation_work_items failed run={run_id}"
)
return False
work_item_by_id = {item.work_item_id: item for item in work_items}
changed = False
for work_item in work_items:
metadata = dict(work_item.metadata or {})
raw_dependency_ids = [
str(item).strip()
for item in list(metadata.get("dependency_work_item_ids", []) or [])
if str(item).strip()
]
if not raw_dependency_ids:
continue
dependency_ids, pruned_dependency_ids = normalize_dependency_work_item_ids(
raw_dependency_ids,
work_item_by_id,
owner_work_item_id=work_item.work_item_id,
)
dependency_phases = {
dep_id: (work_item_by_id[dep_id].phase if dep_id in work_item_by_id else None)
for dep_id in dependency_ids
}
all_approved = all(p == Phase.APPROVED for p in dependency_phases.values())
target_phase = work_item.phase
metadata_updates: dict[str, Any] = {}
summary_update: str | None = None
entered_synthesis_turn = False
if dependency_ids != raw_dependency_ids:
metadata_updates["dependency_work_item_ids"] = list(dependency_ids)
metadata_updates["dependency_pruned_at"] = datetime.now().isoformat()
if pruned_dependency_ids:
previous_pruned = [
str(item).strip()
for item in list(metadata.get("pruned_dependency_work_item_ids", []) or [])
if str(item).strip()
]
metadata_updates["pruned_dependency_work_item_ids"] = list(
dict.fromkeys([*previous_pruned, *pruned_dependency_ids])
)
if all_approved:
if _should_enter_synthesis_turn(work_item, metadata, dependency_ids):
entered_synthesis_turn = True
target_phase = Phase.READY
summary_update = _synthesis_turn_summary(work_item, dependency_ids)
previous_kind = _work_item_kind(work_item, metadata)
metadata_updates.update(
{
"pre_synthesis_work_kind": previous_kind,
"work_kind": "synthesize",
"delegation_turn_kind": "synthesize",
**work_item_identity_payload(
projection_id=str(work_item.projection_id or work_item.work_item_id or ""),
turn_type="aggregate",
),
"current_turn_mode": "synthesize_required",
"synthesis_turn_started": True,
"synthesis_ready_at": datetime.now().isoformat(),
"synthesis_source_work_item_ids": list(dependency_ids),
"synthesis_reports_to_role_id": str(work_item.manager_role_id or "").strip(),
"synthesis_reports_to_seat_id": str(work_item.manager_seat_id or "").strip(),
"frontier": "synthesis_ready",
"needs_manager_attention": False,
}
)
elif work_item.phase == Phase.WAITING_DEPENDENCIES:
target_phase = (
Phase.READY_FOR_REWORK
if str(metadata.get("rework_feedback", "") or "").strip()
else Phase.READY
)
elif work_item.phase == Phase.WAITING_FOR_CHILDREN:
target_phase = Phase.RUNNING
if _work_item_kind(work_item, metadata) in {"deliver", "delivery"}:
metadata_updates.update(
{
"work_kind": "delivery",
"delegation_turn_kind": "delivery",
**work_item_identity_payload(
projection_id=str(work_item.projection_id or work_item.work_item_id or ""),
turn_type="deliver",
),
"current_turn_mode": "deliver_required",
"delivery_turn_ready_at": datetime.now().isoformat(),
}
)
metadata_updates["waiting_on_work_item_ids"] = []
if metadata.get("delegated_children_pending"):
metadata_updates["delegated_children_pending"] = False
if str(metadata.get("frontier", "") or "") == "waiting_for_children" and not entered_synthesis_turn:
metadata_updates["frontier"] = "resumed"
else:
if work_item.phase == Phase.READY:
target_phase = Phase.WAITING_DEPENDENCIES
elif work_item.phase == Phase.RUNNING:
target_phase = Phase.WAITING_FOR_CHILDREN
metadata_updates["waiting_on_work_item_ids"] = dependency_ids
# Clear the parent claim whenever the parent truly leaves
# WAITING_FOR_CHILDREN toward a non-terminal phase. The old
# condition ("only when all children approved AND target is
# RUNNING") left a gap: when a child went READY_FOR_REWORK,
# the refresh now fires (per _DEPENDENT_REFRESH_TARGETS) but
# the parent stayed in WAITING_FOR_CHILDREN with a stale claim,
# so the dispatcher couldn't re-pick it even though the child
# was back on the worker's queue.
# We exclude DONE_PHASES because for terminal parents the
# claim is a historical audit record of "last executor".
clear_claim_on_wake = (
work_item.phase == Phase.WAITING_FOR_CHILDREN
and target_phase != work_item.phase
and target_phase not in DONE_PHASES
)
if target_phase != work_item.phase or metadata_updates or clear_claim_on_wake:
try:
await store.update_delegation_work_item(
work_item.work_item_id,
phase=target_phase if target_phase != work_item.phase else None,
blocked_reason="" if all_approved else None,
metadata_updates=metadata_updates or None,
summary=summary_update,
claimed_by_role_runtime_session_id="" if clear_claim_on_wake else None,
claimed_by_seat_id="" if clear_claim_on_wake else None,
)
changed = True
except Exception:
logger.opt(exception=True).debug(
"refresh_dependents_for_run: update_delegation_work_item failed "
f"wid={work_item.work_item_id}"
)
if changed and hasattr(store, "save_delegation_event"):
try:
await store.save_delegation_event(
DelegationEvent(
run_id=run_id,
work_item_id=source_work_item_id or None,
cell_id=source_cell_id or None,
role_id=source_role_id or None,
event_type="dependency_frontier_refreshed",
payload={
"source_task_id": source_task_id,
"source_work_item_id": source_work_item_id,
},
)
)
except Exception:
logger.opt(exception=True).debug(
"refresh_dependents_for_run: event persistence failed"
)
return changed
finally:
_REFRESH_IN_FLIGHT.reset(token)