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 @@
+170
View File
@@ -0,0 +1,170 @@
"""Attachment content helpers for extraction and multimodal routing."""
from __future__ import annotations
from io import BytesIO
from pathlib import Path
from typing import Iterable
TEXT_EXTENSIONS = {
".txt", ".md", ".csv", ".json", ".yaml", ".yml",
".py", ".js", ".ts", ".tsx", ".jsx", ".html", ".css",
".xml", ".toml", ".ini", ".cfg", ".log", ".sql",
}
OFFICE_EXTENSIONS = {".docx", ".xlsx", ".pptx"}
class _PreviewAccumulator:
def __init__(self, max_chars: int) -> None:
self.max_chars = max(0, max_chars)
self.parts: list[str] = []
self.used = 0
def add(self, text: str) -> bool:
normalized = str(text or "").strip()
if not normalized:
return False
if self.used >= self.max_chars:
return True
available = self.max_chars - self.used
if len(normalized) > available:
normalized = normalized[:available].rstrip()
if not normalized:
return True
self.parts.append(normalized)
self.used += len(normalized) + 1
return self.used >= self.max_chars
def render(self) -> str:
return "\n".join(self.parts).strip()
def attachment_suffix(filename: str) -> str:
return Path(filename).suffix.lower()
def is_text_like_attachment(filename: str, mime_type: str) -> bool:
if mime_type.startswith("text/"):
return True
return attachment_suffix(filename) in TEXT_EXTENSIONS
def can_extract_text(filename: str, mime_type: str) -> bool:
return is_text_like_attachment(filename, mime_type) or attachment_suffix(filename) in OFFICE_EXTENSIONS
def extract_attachment_text(
filename: str,
mime_type: str,
raw: bytes,
*,
max_chars: int = 4000,
) -> str:
suffix = attachment_suffix(filename)
if is_text_like_attachment(filename, mime_type):
return _clip_text(raw.decode("utf-8", errors="replace").strip(), max_chars)
if suffix == ".docx":
return _extract_docx_text(raw, max_chars=max_chars)
if suffix == ".xlsx":
return _extract_xlsx_text(raw, max_chars=max_chars)
if suffix == ".pptx":
return _extract_pptx_text(raw, max_chars=max_chars)
return ""
def _clip_text(text: str, max_chars: int) -> str:
text = str(text or "").strip()
if len(text) <= max_chars:
return text
return f"{text[:max_chars].rstrip()}\n...[truncated]"
def _extract_docx_text(raw: bytes, *, max_chars: int) -> str:
from docx import Document
acc = _PreviewAccumulator(max_chars)
doc = Document(BytesIO(raw))
for para in doc.paragraphs:
if acc.add(para.text):
return _clip_text(acc.render(), max_chars)
for table in doc.tables:
for row in table.rows:
cells = [cell.text.strip() for cell in row.cells]
line = " | ".join(cell for cell in cells if cell)
if acc.add(line):
return _clip_text(acc.render(), max_chars)
return _clip_text(acc.render(), max_chars)
def _extract_xlsx_text(raw: bytes, *, max_chars: int) -> str:
from openpyxl import load_workbook
acc = _PreviewAccumulator(max_chars)
workbook = load_workbook(BytesIO(raw), read_only=True, data_only=True)
try:
for sheet in workbook.worksheets[:5]:
if acc.add(f"# Sheet: {sheet.title}"):
break
row_count = 0
for row in sheet.iter_rows(values_only=True):
values = [_normalize_excel_cell(value) for value in row[:16]]
if not any(values):
continue
row_count += 1
if acc.add("\t".join(values)):
return _clip_text(acc.render(), max_chars)
if row_count >= 80:
break
finally:
workbook.close()
return _clip_text(acc.render(), max_chars)
def _normalize_excel_cell(value: object) -> str:
if value is None:
return ""
if isinstance(value, float):
text = f"{value:.6f}".rstrip("0").rstrip(".")
return text or "0"
return str(value).strip()
def _extract_pptx_text(raw: bytes, *, max_chars: int) -> str:
from pptx import Presentation
acc = _PreviewAccumulator(max_chars)
presentation = Presentation(BytesIO(raw))
for index, slide in enumerate(list(presentation.slides)[:20], start=1):
if acc.add(f"# Slide {index}"):
break
for text in _iter_slide_text(slide.shapes):
if acc.add(text):
return _clip_text(acc.render(), max_chars)
return _clip_text(acc.render(), max_chars)
def _iter_slide_text(shapes: Iterable[object]) -> Iterable[str]:
for shape in shapes:
text = getattr(shape, "text", "")
if isinstance(text, str) and text.strip():
yield text
table = getattr(shape, "table", None)
if table is not None:
for row in table.rows:
cells = [cell.text.strip() for cell in row.cells]
line = " | ".join(cell for cell in cells if cell)
if line:
yield line
subshapes = getattr(shape, "shapes", None)
if subshapes is not None:
yield from _iter_slide_text(subshapes)
+257
View File
@@ -0,0 +1,257 @@
"""Attachment storage layer — disk-based file storage with lightweight references.
Files are stored on disk under `{opc_home}/projects/{project_id}/attachments/{id}/`.
Only lightweight AttachmentRef objects (no binary content) flow through the engine
pipeline, databases, and WebSocket messages. Base64 encoding is performed lazily
at the two endpoints: ingestion (decode → disk) and LLM call (disk → encode).
"""
from __future__ import annotations
import base64
import mimetypes
import os
import shutil
import uuid
from dataclasses import dataclass
from pathlib import Path
from typing import Any
ALLOWED_MIME_PREFIXES = (
"image/",
"text/",
"application/pdf",
"application/json",
"application/x-yaml",
"application/yaml",
)
ALLOWED_EXTENSIONS = {
".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".tiff",
".txt", ".md", ".pdf", ".csv", ".json", ".yaml", ".yml",
".py", ".js", ".ts", ".tsx", ".jsx", ".html", ".css",
".java", ".c", ".cpp", ".h", ".go", ".rs", ".rb", ".sh",
".xml", ".toml", ".ini", ".cfg", ".log",
".docx", ".xlsx", ".pptx",
".mp4", ".mpeg", ".mpg", ".mov", ".webm",
}
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10 MB per file
MAX_TOTAL_SIZE = 20 * 1024 * 1024 # 20 MB per message
ALLOWED_MIME_TYPES = {
"video/mp4",
"video/mpeg",
"video/quicktime",
"video/webm",
}
_MIME_EXTENSION_OVERRIDES = {
"image/jpeg": ".jpg",
"image/png": ".png",
"image/gif": ".gif",
"image/webp": ".webp",
"image/svg+xml": ".svg",
"image/bmp": ".bmp",
"image/tiff": ".tiff",
"application/pdf": ".pdf",
"text/plain": ".txt",
"text/markdown": ".md",
"application/json": ".json",
"application/x-yaml": ".yaml",
"application/yaml": ".yaml",
"video/mp4": ".mp4",
"video/mpeg": ".mpeg",
"video/quicktime": ".mov",
"video/webm": ".webm",
}
@dataclass
class AttachmentRef:
"""Lightweight reference to a stored attachment — safe for JSON / metadata."""
attachment_id: str
filename: str
mime_type: str
size_bytes: int
disk_path: str # relative to opc_home
@property
def is_image(self) -> bool:
return self.mime_type.startswith("image/")
def to_dict(self) -> dict[str, Any]:
return {
"attachment_id": self.attachment_id,
"filename": self.filename,
"mime_type": self.mime_type,
"size_bytes": self.size_bytes,
"disk_path": self.disk_path,
}
@classmethod
def from_dict(cls, d: dict[str, Any]) -> AttachmentRef:
return cls(
attachment_id=d["attachment_id"],
filename=d["filename"],
mime_type=d.get("mime_type", "application/octet-stream"),
size_bytes=d.get("size_bytes", 0),
disk_path=d.get("disk_path", ""),
)
def _sanitize_filename(name: str) -> str:
"""Strip path components and dangerous characters from a filename."""
name = os.path.basename(name)
name = name.replace("..", "").replace("/", "").replace("\\", "")
return name or "upload"
def _check_mime(filename: str, mime: str) -> bool:
ext = os.path.splitext(filename)[1].lower()
if ext in ALLOWED_EXTENSIONS:
return True
if mime in ALLOWED_MIME_TYPES:
return True
for prefix in ALLOWED_MIME_PREFIXES:
if mime.startswith(prefix):
return True
return False
def _normalize_mime(mime_type: str | None) -> str:
return str(mime_type or "").strip().lower()
def _split_data_url(payload: str) -> tuple[str, str | None]:
raw = str(payload or "").strip()
if not raw.startswith("data:"):
return raw, None
header, sep, encoded = raw.partition(",")
if not sep:
return raw, None
mime = header[5:].split(";", 1)[0].strip().lower()
return encoded, mime or None
def _extension_for_mime(mime_type: str) -> str:
if not mime_type:
return ""
override = _MIME_EXTENSION_OVERRIDES.get(mime_type)
if override:
return override
guessed = mimetypes.guess_extension(mime_type, strict=False) or ""
return guessed.lower()
def _ensure_filename_extension(filename: str, mime_type: str) -> str:
if os.path.splitext(filename)[1]:
return filename
extension = _extension_for_mime(mime_type)
if extension:
return f"{filename}{extension}"
return filename
class AttachmentStore:
"""Manages attachment lifecycle on disk."""
def __init__(self, opc_home: Path, project_id: str) -> None:
self.opc_home = opc_home
self.project_id = project_id
self.base_dir = opc_home / "projects" / project_id / "attachments"
def _ensure_dir(self, attachment_id: str) -> Path:
d = self.base_dir / attachment_id
d.mkdir(parents=True, exist_ok=True)
return d
async def save_from_base64(
self,
filename: str,
b64_data: str,
mime_type: str | None = None,
) -> AttachmentRef:
"""Decode base64 data and persist to disk. Returns a lightweight ref."""
filename = _sanitize_filename(filename)
b64_payload, inferred_mime = _split_data_url(b64_data)
mime = _normalize_mime(mime_type) or inferred_mime or ""
if not mime:
guessed_mime, _ = mimetypes.guess_type(filename)
mime = guessed_mime or "application/octet-stream"
filename = _ensure_filename_extension(filename, mime)
if mime == "application/octet-stream":
guessed_mime, _ = mimetypes.guess_type(filename)
mime = guessed_mime or mime
try:
raw = base64.b64decode(b64_payload)
except Exception as exc:
raise ValueError(f"Invalid base64 data for {filename}: {exc}") from exc
size = len(raw)
if size > MAX_FILE_SIZE:
raise ValueError(f"File too large: {size} bytes (limit {MAX_FILE_SIZE})")
if not _check_mime(filename, mime):
raise ValueError(f"Unsupported file type: {filename} ({mime})")
aid = uuid.uuid4().hex[:16]
dest_dir = self._ensure_dir(aid)
dest = dest_dir / filename
dest.write_bytes(raw)
rel = dest.relative_to(self.opc_home)
return AttachmentRef(
attachment_id=aid,
filename=filename,
mime_type=mime,
size_bytes=size,
disk_path=str(rel),
)
async def save_from_path(self, file_path: Path) -> AttachmentRef:
"""Copy a local file into the attachment store. Used by CLI."""
file_path = file_path.expanduser().resolve()
if not file_path.is_file():
raise FileNotFoundError(f"File not found: {file_path}")
size = file_path.stat().st_size
if size > MAX_FILE_SIZE:
raise ValueError(f"File too large: {size} bytes (limit {MAX_FILE_SIZE})")
filename = _sanitize_filename(file_path.name)
mime, _ = mimetypes.guess_type(filename)
mime = mime or "application/octet-stream"
if not _check_mime(filename, mime):
raise ValueError(f"Unsupported file type: {filename} ({mime})")
aid = uuid.uuid4().hex[:16]
dest_dir = self._ensure_dir(aid)
dest = dest_dir / filename
shutil.copy2(str(file_path), str(dest))
rel = dest.relative_to(self.opc_home)
return AttachmentRef(
attachment_id=aid,
filename=filename,
mime_type=mime,
size_bytes=size,
disk_path=str(rel),
)
def resolve_abs_path(self, ref: AttachmentRef) -> Path:
"""Return the absolute path on disk for reading."""
resolved = (self.opc_home / ref.disk_path).resolve()
if not str(resolved).startswith(str(self.base_dir.resolve())):
raise ValueError(f"Path traversal detected: {ref.disk_path}")
return resolved
def read_bytes(self, ref: AttachmentRef) -> bytes:
"""Read raw file content from disk."""
return self.resolve_abs_path(ref).read_bytes()
def read_base64(self, ref: AttachmentRef) -> str:
"""Read file and return as base64 string — called only at LLM call time."""
return base64.b64encode(self.read_bytes(ref)).decode("ascii")
def resolve_http_path(self, ref: AttachmentRef) -> str:
"""Return the HTTP-accessible path for frontend display."""
return f"/api/attachments/{ref.attachment_id}/{ref.filename}"
+329
View File
@@ -0,0 +1,329 @@
"""Canonical company-mode collaboration tool names."""
from __future__ import annotations
from typing import Any
def company_collaboration_enabled(execution_mode: str | None) -> bool:
"""Return whether company collaboration capabilities should be exposed."""
return str(execution_mode or "").strip() == "company_mode"
def company_collaboration_enabled_for_task(task: object | None) -> bool:
"""Return whether collaboration capabilities are enabled for a task-like object."""
if task is None:
return False
metadata = dict(getattr(task, "metadata", {}) or {})
return company_collaboration_enabled(str(metadata.get("execution_mode", "") or ""))
COLLAB_PROFILE_DISABLED = "disabled"
COLLAB_PROFILE_WORKER_DEFAULT = "worker_default"
COLLAB_PROFILE_WORKER_EXECUTE_REVIEW = "worker_execute_review"
COLLAB_PROFILE_MANAGER_DEFAULT = "manager_default"
COLLAB_PROFILE_COORDINATOR_DEFAULT = "coordinator_default"
COLLAB_PROFILE_DEBUG_ADMIN = "debug_admin"
WORKER_DEFAULT_TOOL_NAMES: tuple[str, ...] = (
"inbox",
"send_dm",
"ask_peer_and_wait",
"reply_message",
)
WORKER_EXECUTE_REVIEW_TOOL_NAMES: tuple[str, ...] = WORKER_DEFAULT_TOOL_NAMES
MANAGER_DEFAULT_TOOL_NAMES: tuple[str, ...] = (
*WORKER_DEFAULT_TOOL_NAMES,
"delegate_work",
"modify_work_item",
"delete_work_item",
"manager_board_read",
"broadcast_issue",
"start_meeting",
)
COORDINATOR_DEFAULT_TOOL_NAMES: tuple[str, ...] = (
*MANAGER_DEFAULT_TOOL_NAMES,
"propose_task_adjustment",
"route_work",
)
DEBUG_ADMIN_TOOL_NAMES: tuple[str, ...] = (
"read_inbox",
"read_meeting",
"list_colleagues",
)
MEETING_RESPONSE_TOOL_NAMES: tuple[str, ...] = ("respond_meeting",)
HUMAN_REVIEW_TOOL_NAMES: tuple[str, ...] = ("close_human_review",)
COMPANY_COLLABORATION_TOOL_NAMES: tuple[str, ...] = (
*COORDINATOR_DEFAULT_TOOL_NAMES,
*MEETING_RESPONSE_TOOL_NAMES,
*HUMAN_REVIEW_TOOL_NAMES,
)
COMPANY_DEBUG_TOOL_NAMES: tuple[str, ...] = DEBUG_ADMIN_TOOL_NAMES
COMPANY_ALL_COLLABORATION_TOOL_NAMES: tuple[str, ...] = tuple(
dict.fromkeys(
[
*COMPANY_COLLABORATION_TOOL_NAMES,
*COMPANY_DEBUG_TOOL_NAMES,
]
)
)
COMPANY_APPROVAL_EXEMPT_TOOL_NAMES: tuple[str, ...] = (
*COMPANY_ALL_COLLABORATION_TOOL_NAMES,
)
MULTI_TEAM_COORDINATION_TURN_MODES: frozenset[str] = frozenset(
{
"dispatch_required",
"monitor_children",
"synthesize_required",
"deliver_required",
}
)
# Kanban-push review turn: a dedicated review Task is the only thing running
# in the seat. The manager emits a structured verdict as part of the turn
# output; the runtime (``_finalize_review_work_item``) auto-applies it to
# the child work item. The restricted toolset keeps the turn focused on
# judgement — no delegation / messaging / dispatch side-channels.
REVIEW_EXECUTE_TURN_MODE: str = "review_execute"
REVIEW_EXECUTE_TOOL_NAMES: frozenset[str] = frozenset(
{
"manager_board_read",
}
)
MULTI_TEAM_COORDINATOR_LEGACY_TOOL_NAMES: frozenset[str] = frozenset(
{
"propose_task_adjustment",
"route_work",
}
)
def _task_metadata(task: object | None) -> dict[str, Any]:
return dict(getattr(task, "metadata", {}) or {}) if task is not None else {}
def _task_context_snapshot(task: object | None) -> dict[str, Any]:
return dict(getattr(task, "context_snapshot", {}) or {}) if task is not None else {}
def _work_item_turn_type(metadata: dict[str, Any]) -> str:
for key in ("work_item_turn_type", "work_item_turn_type", "work_kind", "delegation_turn_kind"):
value = str(metadata.get(key, "") or "").strip().lower()
if value:
return value
return ""
def _runtime_state_dict(runtime_state: Any | None) -> dict[str, Any]:
return dict(runtime_state or {}) if isinstance(runtime_state, dict) else {}
def _role_type_hint(role_cfg: Any | None, runtime_state: dict[str, Any]) -> str:
if role_cfg is not None:
runtime_policy = getattr(role_cfg, "runtime_policy", None)
hinted = str(
getattr(runtime_policy, "role_type", "")
or (runtime_policy.get("role_type", "") if isinstance(runtime_policy, dict) else "")
or getattr(role_cfg, "role_type", "")
or ""
).strip().lower()
if hinted:
return hinted
return str(runtime_state.get("role_type", "") or "").strip().lower()
def _can_spawn_hint(role_cfg: Any | None, runtime_state: dict[str, Any]) -> bool:
if role_cfg is not None:
can_spawn = [str(item).strip() for item in list(getattr(role_cfg, "can_spawn", []) or []) if str(item).strip()]
if can_spawn:
return True
return bool(
[
str(item).strip()
for item in list(runtime_state.get("can_spawn", []) or [])
if str(item).strip()
]
)
def _is_execute_or_review_work_item(task: object | None) -> bool:
metadata = _task_metadata(task)
if not company_collaboration_enabled(str(metadata.get("execution_mode", "") or "")):
return False
turn_type = _work_item_turn_type(metadata)
return turn_type in {"execute", "review"}
def _has_active_meeting(task: object | None, runtime_state: dict[str, Any]) -> bool:
metadata = _task_metadata(task)
peer_wait = dict(metadata.get("peer_wait", {}) or {})
if str(peer_wait.get("kind", "") or "").strip().lower() == "meeting":
return True
context_snapshot = _task_context_snapshot(task)
for bucket in (
context_snapshot.get("company_member_inbox", []),
context_snapshot.get("company_member_protocol_backlog", []),
context_snapshot.get("company_member_notification_backlog", []),
context_snapshot.get("broker_pending_inbox", []),
):
for item in list(bucket or []):
if isinstance(item, dict) and (
str(item.get("meeting_room_id", "") or "").strip()
or str(dict(item.get("metadata", {}) or {}).get("meeting_room_id", "") or "").strip()
):
return True
latest_company_notification = dict(context_snapshot.get("latest_company_notification", {}) or {})
if str(latest_company_notification.get("meeting_room_id", "") or "").strip():
return True
if str(runtime_state.get("meeting_room_id", "") or "").strip():
return True
if bool(runtime_state.get("active_meeting", False)):
return True
return False
def _human_review_close_allowed(task: object | None, runtime_state: dict[str, Any]) -> bool:
metadata = _task_metadata(task)
context_snapshot = _task_context_snapshot(task)
return any(
bool(source.get("human_review_close_allowed", False))
or str(source.get("human_review_checkpoint_type", "") or "").strip() == "company_delivery_feedback"
for source in (metadata, context_snapshot, runtime_state)
)
def resolve_company_turn_mode(
task: object | None,
runtime_state: dict[str, Any] | None = None,
) -> str:
metadata = _task_metadata(task)
context_snapshot = _task_context_snapshot(task)
state = _runtime_state_dict(runtime_state)
runtime_model = str(
metadata.get("runtime_model", "")
or context_snapshot.get("runtime_model", "")
or state.get("runtime_model", "")
or ""
).strip()
if runtime_model != "multi_team_org":
return ""
for candidate in (
state.get("current_turn_mode"),
dict(state.get("manager_digest", {}) or {}).get("current_turn_mode"),
metadata.get("current_turn_mode"),
context_snapshot.get("current_turn_mode"),
dict(metadata.get("member_session_state", {}) or {}).get("current_turn_mode"),
dict(context_snapshot.get("member_session", {}) or {}).get("current_turn_mode"),
dict(metadata.get("resident_assignment", {}) or {}).get("metadata", {}).get("current_turn_mode"),
dict(context_snapshot.get("resident_assignment", {}) or {}).get("metadata", {}).get("current_turn_mode"),
dict(context_snapshot.get("manager_digest", {}) or {}).get("current_turn_mode"),
):
value = str(candidate or "").strip()
if value:
return value
return ""
def resolve_collaboration_profile(
task: object | None,
role: str = "",
seat: str = "",
runtime_state: dict[str, Any] | None = None,
*,
role_cfg: Any | None = None,
debug_admin: bool = False,
) -> str:
"""Resolve the collaboration capability profile for a task/seat."""
metadata = _task_metadata(task)
state = _runtime_state_dict(runtime_state)
if not company_collaboration_enabled_for_task(task):
return COLLAB_PROFILE_DISABLED
if debug_admin or bool(metadata.get("collaboration_debug_admin", False)) or bool(state.get("debug_admin", False)):
return COLLAB_PROFILE_DEBUG_ADMIN
managed_team_id = str(
metadata.get("managed_team_id", "")
or state.get("managed_team_id", "")
or dict(metadata.get("member_session_state", {}) or {}).get("metadata", {}).get("managed_team_id", "")
or ""
).strip()
role_type = _role_type_hint(role_cfg, state)
can_spawn = _can_spawn_hint(role_cfg, state)
manager_board_summary = (
dict(state.get("manager_board_summary", {}) or {})
or dict(_task_context_snapshot(task).get("manager_board_summary", {}) or {})
)
work_item_turn_type = _work_item_turn_type(metadata)
if role_type == "coordinator" or can_spawn:
return COLLAB_PROFILE_COORDINATOR_DEFAULT
if managed_team_id or manager_board_summary or work_item_turn_type in {"intake", "plan", "dispatch", "monitor", "aggregate", "deliver"}:
return COLLAB_PROFILE_MANAGER_DEFAULT
if _is_execute_or_review_work_item(task):
return COLLAB_PROFILE_WORKER_EXECUTE_REVIEW
return COLLAB_PROFILE_WORKER_DEFAULT
def resolve_allowed_collaboration_tools(
profile: str,
task: object | None = None,
runtime_state: dict[str, Any] | None = None,
) -> set[str]:
"""Return the collaboration tools visible for the resolved profile."""
state = _runtime_state_dict(runtime_state)
if profile == COLLAB_PROFILE_DISABLED:
return set()
if profile == COLLAB_PROFILE_DEBUG_ADMIN:
return set(DEBUG_ADMIN_TOOL_NAMES)
if profile == COLLAB_PROFILE_WORKER_EXECUTE_REVIEW:
allowed = set(WORKER_EXECUTE_REVIEW_TOOL_NAMES)
elif profile == COLLAB_PROFILE_MANAGER_DEFAULT:
allowed = set(MANAGER_DEFAULT_TOOL_NAMES)
elif profile == COLLAB_PROFILE_COORDINATOR_DEFAULT:
allowed = set(COORDINATOR_DEFAULT_TOOL_NAMES)
else:
allowed = set(WORKER_DEFAULT_TOOL_NAMES)
turn_mode = resolve_company_turn_mode(task, runtime_state=state)
if turn_mode == REVIEW_EXECUTE_TURN_MODE and str(_task_metadata(task).get("runtime_model", "") or "").strip() == "multi_team_org":
return set(REVIEW_EXECUTE_TOOL_NAMES)
if turn_mode in MULTI_TEAM_COORDINATION_TURN_MODES:
allowed.discard("ask_peer_and_wait")
if turn_mode and str(_task_metadata(task).get("runtime_model", "") or "").strip() == "multi_team_org":
allowed.difference_update(MULTI_TEAM_COORDINATOR_LEGACY_TOOL_NAMES)
if _has_active_meeting(task, state):
allowed.update(MEETING_RESPONSE_TOOL_NAMES)
if _human_review_close_allowed(task, state):
allowed.update(HUMAN_REVIEW_TOOL_NAMES)
return allowed
def resolve_task_collaboration_tools(
task: object | None,
*,
role: str = "",
seat: str = "",
runtime_state: dict[str, Any] | None = None,
role_cfg: Any | None = None,
debug_admin: bool = False,
) -> tuple[str, set[str]]:
profile = resolve_collaboration_profile(
task,
role=role,
seat=seat,
runtime_state=runtime_state,
role_cfg=role_cfg,
debug_admin=debug_admin,
)
return profile, resolve_allowed_collaboration_tools(profile, task=task, runtime_state=runtime_state)
+1857
View File
File diff suppressed because it is too large Load Diff
+273
View File
@@ -0,0 +1,273 @@
"""Persistent company employee registry helpers.
Employees are runtime/company assets, while organization configs describe the
role graph and policies. This module keeps employee records outside org yaml
and normalizes template-backed employees to a canonical template id.
"""
from __future__ import annotations
import json
import re
from pathlib import Path
from typing import Any
import yaml
from opc.core.config import EmployeeConfig, validate_organization_id
EMPLOYEE_REGISTRY_SCHEMA_VERSION = 1
EMPLOYEE_REGISTRY_KIND = "company_employee"
_COUNT_KEYS = {
"successes",
"partial_successes",
"failures",
"reflection_count",
}
def _slugify(value: str) -> str:
slug = re.sub(r"[^A-Za-z0-9._-]+", "-", value.strip().lower()).strip("-")
return slug or "employee"
def employee_registry_dir(opc_home: Path, organization_id: Any) -> Path:
org_id = validate_organization_id(organization_id)
return Path(opc_home) / "company_state" / org_id / "employees"
def employee_registry_path(opc_home: Path, organization_id: Any, employee_id: str) -> Path:
filename = f"{_slugify(employee_id)}.yaml"
return employee_registry_dir(opc_home, organization_id) / filename
def is_placeholder_employee(employee: EmployeeConfig | dict[str, Any]) -> bool:
metadata = dict(employee.metadata if isinstance(employee, EmployeeConfig) else employee.get("metadata") or {})
return bool(metadata.get("is_default_employee") or metadata.get("is_fallback_employee"))
def _employee_from_payload(raw: Any) -> EmployeeConfig | None:
if isinstance(raw, EmployeeConfig):
return raw.model_copy(deep=True)
if not isinstance(raw, dict):
return None
payload = raw.get("employee") if isinstance(raw.get("employee"), dict) else raw
try:
return EmployeeConfig.model_validate(payload)
except Exception:
return None
def _append_unique(items: list[Any], additions: list[Any]) -> list[Any]:
result = list(items or [])
for item in list(additions or []):
if item not in result:
result.append(item)
return result
def _merge_metadata(base: dict[str, Any], incoming: dict[str, Any]) -> dict[str, Any]:
merged = dict(base or {})
for key, value in dict(incoming or {}).items():
if key == "legacy_employee_ids":
merged[key] = _append_unique(list(merged.get(key, []) or []), list(value or []))
elif key in {"home_role_ids", "staffed_role_ids"}:
merged[key] = _append_unique(list(merged.get(key, []) or []), list(value or []))
elif key not in merged or _is_empty_value(merged.get(key)):
merged[key] = value
elif isinstance(merged.get(key), dict) and isinstance(value, dict):
nested = dict(merged[key])
nested.update(value)
merged[key] = nested
return merged
def _canonicalize_employee(employee: EmployeeConfig) -> tuple[EmployeeConfig, dict[str, str]]:
if is_placeholder_employee(employee):
return employee.model_copy(deep=True), {}
old_id = str(employee.employee_id or "").strip()
template_id = str(employee.template_id or "").strip()
canonical_id = template_id or old_id
metadata = dict(employee.metadata or {})
aliases: dict[str, str] = {}
legacy_ids = [str(item).strip() for item in list(metadata.get("legacy_employee_ids", []) or []) if str(item).strip()]
if old_id and old_id != canonical_id and old_id not in legacy_ids:
legacy_ids.append(old_id)
if legacy_ids:
metadata["legacy_employee_ids"] = legacy_ids
for legacy_id in legacy_ids:
aliases[legacy_id] = canonical_id
role_id = str(employee.role_id or "").strip()
if role_id:
metadata.setdefault("home_role_id", role_id)
metadata["home_role_ids"] = _append_unique(list(metadata.get("home_role_ids", []) or []), [role_id])
metadata["staffed_role_ids"] = _append_unique(list(metadata.get("staffed_role_ids", []) or []), [role_id])
if template_id:
metadata.setdefault("canonical_employee_id", canonical_id)
return employee.model_copy(update={"employee_id": canonical_id, "metadata": metadata}), aliases
def _merge_employee(base: EmployeeConfig, incoming: EmployeeConfig) -> EmployeeConfig:
merged = base.model_dump()
other = incoming.model_dump()
for field in ("domains", "tags", "prompt_refs", "skill_refs"):
merged[field] = _append_unique(list(merged.get(field, []) or []), list(other.get(field, []) or []))
for field in ("name", "template_id", "description", "category", "preferred_external_agent", "seniority", "status"):
if not merged.get(field) and other.get(field):
merged[field] = other[field]
elif field == "description" and len(str(other.get(field, ""))) > len(str(merged.get(field, ""))):
merged[field] = other[field]
if not merged.get("role_id") and other.get("role_id"):
merged["role_id"] = other["role_id"]
merged["metadata"] = _merge_metadata(dict(merged.get("metadata", {}) or {}), dict(other.get("metadata", {}) or {}))
return EmployeeConfig.model_validate(merged)
def normalize_employee_records(employees: list[Any]) -> tuple[list[EmployeeConfig], dict[str, str]]:
real_by_id: dict[str, EmployeeConfig] = {}
placeholders: list[EmployeeConfig] = []
aliases: dict[str, str] = {}
for raw in list(employees or []):
employee = _employee_from_payload(raw)
if employee is None:
continue
if is_placeholder_employee(employee):
placeholders.append(employee)
continue
canonical, employee_aliases = _canonicalize_employee(employee)
aliases.update(employee_aliases)
existing = real_by_id.get(canonical.employee_id)
real_by_id[canonical.employee_id] = _merge_employee(existing, canonical) if existing else canonical
real = sorted(real_by_id.values(), key=lambda item: (item.category, item.name.lower(), item.employee_id))
return [*real, *placeholders], aliases
def load_employee_registry(opc_home: Path, organization_id: Any) -> list[EmployeeConfig]:
directory = employee_registry_dir(opc_home, organization_id)
if not directory.is_dir():
return []
employees: list[EmployeeConfig] = []
for path in sorted(directory.glob("*.yaml")):
try:
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
except Exception:
continue
employee = _employee_from_payload(data)
if employee is not None:
employees.append(employee)
return employees
def load_company_employees(
opc_home: Path,
organization_id: Any,
legacy_employees: list[Any],
) -> list[EmployeeConfig]:
registry_employees = load_employee_registry(opc_home, organization_id)
employees, aliases = normalize_employee_records([*registry_employees, *list(legacy_employees or [])])
migrate_evolution_employee_ids(opc_home, aliases)
return employees
def write_employee_registry(
opc_home: Path,
organization_id: Any,
employees: list[Any],
) -> tuple[list[EmployeeConfig], dict[str, str]]:
normalized, aliases = normalize_employee_records(employees)
real_employees = [employee for employee in normalized if not is_placeholder_employee(employee)]
directory = employee_registry_dir(opc_home, organization_id)
directory.mkdir(parents=True, exist_ok=True)
expected_paths: set[Path] = set()
for employee in real_employees:
path = employee_registry_path(opc_home, organization_id, employee.employee_id)
expected_paths.add(path)
payload = {
"schema_version": EMPLOYEE_REGISTRY_SCHEMA_VERSION,
"kind": EMPLOYEE_REGISTRY_KIND,
"organization_id": validate_organization_id(organization_id),
"employee": employee.model_dump(),
}
_atomic_write_text(
path,
yaml.dump(payload, default_flow_style=False, sort_keys=False, allow_unicode=True),
)
for path in directory.glob("*.yaml"):
if path not in expected_paths:
try:
path.unlink()
except OSError:
pass
migrate_evolution_employee_ids(opc_home, aliases)
return normalized, aliases
def migrate_evolution_employee_ids(opc_home: Path, aliases: dict[str, str]) -> None:
canonical_aliases = {
str(old).strip(): str(new).strip()
for old, new in dict(aliases or {}).items()
if str(old).strip() and str(new).strip() and str(old).strip() != str(new).strip()
}
if not canonical_aliases:
return
paths: list[Path] = [Path(opc_home) / "evolution" / "employees.json"]
projects_dir = Path(opc_home) / "projects"
if projects_dir.is_dir():
paths.extend(sorted(projects_dir.glob("*/employee_evolution.json")))
for path in paths:
if not path.exists():
continue
try:
profile = json.loads(path.read_text(encoding="utf-8"))
except Exception:
continue
if not isinstance(profile, dict):
continue
employees = profile.get("employees")
if not isinstance(employees, dict):
continue
changed = False
for legacy_id, canonical_id in canonical_aliases.items():
if legacy_id not in employees:
continue
legacy_record = employees.pop(legacy_id)
current = employees.get(canonical_id)
employees[canonical_id] = _merge_evolution_records(current, legacy_record)
changed = True
if changed:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(profile, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
def _merge_evolution_records(base: Any, incoming: Any) -> Any:
if isinstance(base, dict) and isinstance(incoming, dict):
merged = dict(base)
for key, value in incoming.items():
if key in _COUNT_KEYS and isinstance(value, (int, float)):
merged[key] = int(merged.get(key, 0) or 0) + int(value)
elif isinstance(value, list):
merged[key] = _append_unique(list(merged.get(key, []) or []), value)
elif isinstance(value, dict):
merged[key] = _merge_evolution_records(merged.get(key, {}), value)
elif key not in merged or _is_empty_value(merged.get(key)):
merged[key] = value
return merged
if isinstance(base, list) and isinstance(incoming, list):
return _append_unique(base, incoming)
return base if not _is_empty_value(base) else incoming
def _is_empty_value(value: Any) -> bool:
return value is None or value == "" or value == [] or value == {}
def _atomic_write_text(path: Path, content: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(path.suffix + ".tmp")
tmp.write_text(content, encoding="utf-8")
tmp.replace(path)
+50
View File
@@ -0,0 +1,50 @@
"""In-process event bus for OPC system."""
from __future__ import annotations
import asyncio
from collections import defaultdict
from typing import Any, Callable, Coroutine
from opc.core.models import OPCEvent
Listener = Callable[[OPCEvent], Coroutine[Any, Any, None]]
class EventBus:
"""Simple async pub/sub event bus for inter-layer communication."""
def __init__(self) -> None:
self._listeners: dict[str, list[Listener]] = defaultdict(list)
self._global_listeners: list[Listener] = []
self._history: list[OPCEvent] = []
self._lock: asyncio.Lock | None = None
def _get_lock(self) -> asyncio.Lock:
if self._lock is None:
self._lock = asyncio.Lock()
return self._lock
def subscribe(self, event_type: str, listener: Listener) -> None:
self._listeners[event_type].append(listener)
def subscribe_all(self, listener: Listener) -> None:
self._global_listeners.append(listener)
async def publish(self, event: OPCEvent) -> None:
async with self._get_lock():
self._history.append(event)
# Snapshot listener lists under lock to avoid mutation during iteration
typed = list(self._listeners.get(event.event_type, []))
globl = list(self._global_listeners)
# Execute listeners outside lock to avoid holding it during async work
tasks = [fn(event) for fn in typed + globl]
if tasks:
await asyncio.gather(*tasks, return_exceptions=True)
def get_history(self, event_type: str | None = None, limit: int = 50) -> list[OPCEvent]:
events = self._history
if event_type:
events = [e for e in events if e.event_type == event_type]
return events[-limit:]
+1370
View File
File diff suppressed because it is too large Load Diff
+269
View File
@@ -0,0 +1,269 @@
"""Dedicated storage helpers for user-defined org architectures."""
from __future__ import annotations
from pathlib import Path
from typing import Any
import yaml
from opc.core.config import (
COMPANY_ORG_KIND,
COMPANY_ORG_SCHEMA_VERSION,
DEFAULT_ORGANIZATION_ID,
OPCConfig,
_atomic_write_text,
_company_org_payload_to_org_mapping,
_read_yaml_file,
build_company_org_payload_from_config,
slugify_organization_name,
validate_organization_id,
)
ORG_INDEX_FILENAME = "org_index.yaml"
ORG_CONFIGS_DIRNAME = "company_orgs"
ORG_CONFIG_KIND = COMPANY_ORG_KIND
ORG_CONFIG_SCHEMA_VERSION = COMPANY_ORG_SCHEMA_VERSION
RESERVED_ORG_CONFIG_IDS = frozenset({DEFAULT_ORGANIZATION_ID})
class RunnableOrgConfigError(ValueError):
"""Raised when a saved custom org cannot be safely activated or run."""
def org_index_path(config_dir: Path) -> Path:
return Path(config_dir) / ORG_INDEX_FILENAME
def org_configs_dir(config_dir: Path) -> Path:
return Path(config_dir) / ORG_CONFIGS_DIRNAME
def is_reserved_org_config_id(value: Any) -> bool:
try:
org_id = validate_organization_id(value)
except ValueError:
return False
return org_id in RESERVED_ORG_CONFIG_IDS
def validate_saved_org_id(value: Any) -> str:
org_id = validate_organization_id(value)
if org_id in RESERVED_ORG_CONFIG_IDS:
raise ValueError(f"Reserved organization_id for built-in company profile: {org_id!r}")
return org_id
def org_config_filename(organization_id: Any) -> str:
return f"org_{validate_saved_org_id(organization_id)}_config.yaml"
def organization_id_from_org_config_filename(path: Path) -> str | None:
name = Path(path).name
prefix = "org_"
suffix = "_config.yaml"
if not name.startswith(prefix) or not name.endswith(suffix):
return None
candidate = name[len(prefix):-len(suffix)]
try:
return validate_saved_org_id(candidate)
except ValueError:
return None
def org_config_path(config_dir: Path, organization_id: Any) -> Path:
return org_configs_dir(config_dir) / org_config_filename(organization_id)
def org_config_relative_path(organization_id: Any) -> str:
return f"{ORG_CONFIGS_DIRNAME}/{org_config_filename(organization_id)}"
def read_org_index(config_dir: Path) -> str | None:
path = org_index_path(config_dir)
if not path.exists():
return None
data = _read_yaml_file(path)
active_id = data.get("active_organization_id")
if not active_id:
return None
org_id = validate_organization_id(active_id)
return None if org_id in RESERVED_ORG_CONFIG_IDS else org_id
def write_org_index(config_dir: Path, organization_id: Any) -> None:
org_id = validate_saved_org_id(organization_id)
_atomic_write_text(
org_index_path(config_dir),
yaml.dump(
{
"schema_version": 1,
"active_organization_id": org_id,
},
default_flow_style=False,
sort_keys=False,
allow_unicode=True,
),
)
def list_org_config_paths(config_dir: Path) -> list[Path]:
org_dir = org_configs_dir(config_dir)
if not org_dir.is_dir():
return []
return sorted(path for path in org_dir.glob("org_*_config.yaml") if organization_id_from_org_config_filename(path))
def allocate_org_config_id(config_dir: Path, organization_name: Any, *, preferred_id: Any = "") -> str:
base = str(preferred_id or "").strip()
try:
candidate = validate_saved_org_id(base) if base else ""
except ValueError:
candidate = ""
if not candidate:
candidate = slugify_organization_name(organization_name)
existing = {
org_id
for path in list_org_config_paths(config_dir)
for org_id in [organization_id_from_org_config_filename(path)]
if org_id
}
existing.update(RESERVED_ORG_CONFIG_IDS)
if candidate not in existing:
return candidate
suffix = 2
while True:
tail = f"_{suffix}"
stem = candidate[: max(1, 64 - len(tail))].rstrip("_-") or "org"
next_id = f"{stem}{tail}"
if next_id not in existing:
return next_id
suffix += 1
def validate_org_config_payload(path: Path, data: dict[str, Any]) -> dict[str, Any]:
schema_version = int(data.get("schema_version", 1) or 1)
if schema_version > ORG_CONFIG_SCHEMA_VERSION:
raise ValueError(
f"{path.name} schema_version {schema_version} is not supported by this version of OpenOPC"
)
kind = str(data.get("kind", "") or "").strip()
if schema_version >= ORG_CONFIG_SCHEMA_VERSION and kind and kind != ORG_CONFIG_KIND:
raise ValueError(f"Unsupported org architecture kind in {path.name}: {kind}")
data["schema_version"] = schema_version
return data
def build_org_config_payload_from_config(
config: OPCConfig,
*,
organization_id: str | None = None,
organization_name: str | None = None,
) -> dict[str, Any]:
payload = build_company_org_payload_from_config(
config,
organization_id=organization_id,
organization_name=organization_name,
force_profile="custom",
)
org_id = validate_saved_org_id(payload.get("organization_id"))
payload["metadata"] = {
**dict(payload.get("metadata", {}) or {}),
"source": "org_mode",
"organization_config_file": org_config_relative_path(org_id),
}
return payload
def apply_org_config_payload_to_config(
base_config: OPCConfig,
data: dict[str, Any],
*,
source_path: Path | None = None,
) -> OPCConfig:
merged = base_config.model_dump()
org_mapping = _company_org_payload_to_org_mapping(data, source_path=source_path)
org_mapping["organization_id"] = validate_saved_org_id(org_mapping.get("organization_id"))
org_mapping["company_profile"] = "custom"
if org_mapping.get("organization_id"):
org_mapping["organization_config_file"] = org_config_relative_path(org_mapping["organization_id"])
merged["org"] = org_mapping
config = OPCConfig.model_validate(merged)
config.org.talent_templates = []
if source_path is not None:
try:
from opc.core.employee_registry import load_company_employees
config_dir = Path(source_path).parent.parent
config.org.employees = load_company_employees(
config_dir.parent,
config.org.organization_id,
list(config.org.employees),
)
except Exception:
pass
return config
def validate_runnable_org_config(config: OPCConfig, *, organization_id: Any = "") -> None:
"""Reject custom orgs that would silently fall back to corporate builtin roles.
Corporate can still fall back to builtin roles for legacy configs.
Saved custom orgs must carry explicit roles before activation or execution.
"""
org = config.org
profile = str(getattr(org, "company_profile", "") or "").strip().lower()
if profile != "custom":
return
org_id = validate_saved_org_id(organization_id or getattr(org, "organization_id", ""))
roles = [
role
for role in list(getattr(org, "roles", []) or [])
if str(getattr(role, "id", "") or "").strip()
]
if roles:
return
raise RunnableOrgConfigError(
f"Custom organization `{org_id}` has no roles. "
"Refusing to activate or run it with corporate fallback roles."
)
def write_org_config_payload(config_dir: Path, organization_id: Any, payload: dict[str, Any]) -> Path:
org_id = validate_saved_org_id(organization_id)
path = org_config_path(config_dir, org_id)
payload = dict(payload)
raw_employees = list(payload.get("employees", []) or [])
if raw_employees:
from opc.core.employee_registry import load_employee_registry, write_employee_registry
opc_home = Path(config_dir).parent
existing = load_employee_registry(opc_home, org_id)
write_employee_registry(opc_home, org_id, [*existing, *raw_employees])
payload["employees"] = []
payload["talent_templates"] = []
payload["organization_id"] = org_id
payload.setdefault("schema_version", ORG_CONFIG_SCHEMA_VERSION)
payload.setdefault("kind", ORG_CONFIG_KIND)
payload["metadata"] = {
**dict(payload.get("metadata", {}) or {}),
"organization_config_file": org_config_relative_path(org_id),
}
_atomic_write_text(
path,
yaml.dump(payload, default_flow_style=False, sort_keys=False, allow_unicode=True),
)
return path
def load_org_config_payload(config_dir: Path, organization_id: Any | None = None) -> tuple[dict[str, Any], Path]:
config_dir = Path(config_dir)
org_id = validate_saved_org_id(organization_id) if organization_id else read_org_index(config_dir)
if not org_id:
raise FileNotFoundError("No active org architecture is selected.")
path = org_config_path(config_dir, org_id)
if not path.exists():
raise FileNotFoundError(f"Org architecture config does not exist: {path}")
return validate_org_config_payload(path, _read_yaml_file(path)), path
+40
View File
@@ -0,0 +1,40 @@
"""Windows-specific SSL environment helpers."""
from __future__ import annotations
import os
_REMOVED_SSLKEYLOGFILE: str | None = None
def sanitize_windows_sslkeylogfile() -> str | None:
"""Eagerly remove SSLKEYLOGFILE on Windows and remember it for one warning."""
global _REMOVED_SSLKEYLOGFILE
if os.name != "nt":
return None
removed = os.environ.pop("SSLKEYLOGFILE", None)
if removed and not _REMOVED_SSLKEYLOGFILE:
_REMOVED_SSLKEYLOGFILE = removed
return removed
def pop_windows_sslkeylogfile() -> str | None:
"""Remove SSLKEYLOGFILE on Windows to avoid aiohttp/OpenSSL import crashes."""
global _REMOVED_SSLKEYLOGFILE
if os.name != "nt":
return None
removed = os.environ.pop("SSLKEYLOGFILE", None)
if removed:
_REMOVED_SSLKEYLOGFILE = None
return removed
remembered = _REMOVED_SSLKEYLOGFILE
_REMOVED_SSLKEYLOGFILE = None
return remembered
def format_windows_sslkeylog_warning(command_label: str, keylog_path: str) -> str:
"""Render a consistent warning when SSLKEYLOGFILE must be ignored on Windows."""
return (
f"Warning: ignoring SSLKEYLOGFILE for `{command_label}` on Windows "
f"({keylog_path}) because it can crash aiohttp/OpenSSL."
)
+180
View File
@@ -0,0 +1,180 @@
"""Utilities for normalizing worker-facing message envelopes."""
from __future__ import annotations
from typing import Any
MESSAGE_CLASSES = {"chat", "protocol", "notification"}
# Protocol types that messages may carry. Ten previously-declared values
# (permission_*, plan_approval_*, dependency_*, approval_reply, cross_team_*)
# were never actually written by any code path and have been removed along
# with the matching CommsSemanticType enum values. See
# plans/task-cleanup-dead-comms.md.
_PROTOCOL_TYPES = {
"approval_request",
"shutdown_request",
"shutdown_response",
"ack",
}
_NOTIFICATION_KINDS = {
"idle",
"task_complete",
"blocked",
"handoff_ready",
"completion",
"status_digest",
"permission_needed",
"error",
}
_SEMANTIC_PROTOCOL_MAP = {
"approval_request": "approval_request",
}
_SEMANTIC_NOTIFICATION_MAP = {
"idle_notification": "idle",
"handoff_ready": "handoff_ready",
"work_item_result": "task_complete",
"blocker": "blocked",
"completion": "completion",
"status_digest": "status_digest",
}
def _clean_text(value: Any) -> str:
return str(value or "").strip()
def _clean_choice(value: Any, allowed: set[str]) -> str:
normalized = _clean_text(value).lower()
return normalized if normalized in allowed else ""
def _coerce_bool(value: Any) -> bool | None:
if isinstance(value, bool):
return value
if isinstance(value, str):
normalized = value.strip().lower()
if normalized in {"true", "1", "yes", "y", "on"}:
return True
if normalized in {"false", "0", "no", "n", "off"}:
return False
return None
def normalize_worker_envelope_metadata(
metadata: dict[str, Any] | None = None,
*,
msg_type: str = "",
semantic_type: str = "",
transport_kind: str = "",
from_agent: str = "",
reply_needed: bool = False,
worker_id: str = "",
task_id: str = "",
projection_id: str = "",
session_id: str = "",
) -> dict[str, Any]:
"""Return additive worker-envelope metadata for chat/protocol/notification routing."""
merged = dict(metadata or {})
protocol_type = _clean_choice(merged.get("protocol_type"), _PROTOCOL_TYPES)
if not protocol_type:
protocol_type = _SEMANTIC_PROTOCOL_MAP.get(_clean_text(semantic_type).lower(), "")
if not protocol_type and _clean_text(msg_type).lower() == "ack":
protocol_type = "ack"
notification_kind = _clean_choice(merged.get("notification_kind"), _NOTIFICATION_KINDS)
if not notification_kind:
notification_kind = _SEMANTIC_NOTIFICATION_MAP.get(_clean_text(semantic_type).lower(), "")
resident_status = _clean_text(merged.get("resident_status")).lower()
if not notification_kind and resident_status in {"idle", "blocked"}:
notification_kind = resident_status
if not notification_kind and _clean_text(merged.get("status")).lower() in {"failed", "error"}:
notification_kind = "error"
message_class = _clean_choice(merged.get("message_class"), MESSAGE_CLASSES)
if not message_class:
if protocol_type:
message_class = "protocol"
elif notification_kind:
message_class = "notification"
else:
message_class = "chat"
actionable = _coerce_bool(merged.get("actionable"))
if actionable is None:
actionable = message_class != "notification"
resolved_worker_id = (
_clean_text(merged.get("worker_id"))
or _clean_text(worker_id)
or _clean_text(merged.get("member_session_id"))
or _clean_text(merged.get("runtime_session_id"))
or _clean_text(from_agent)
)
origin_task_id = _clean_text(merged.get("origin_task_id")) or _clean_text(task_id)
origin_projection_id = _clean_text(merged.get("origin_projection_id")) or _clean_text(projection_id)
origin_session_id = _clean_text(merged.get("origin_session_id")) or _clean_text(session_id)
merged.update(
{
"message_class": message_class,
"protocol_type": protocol_type or None,
"notification_kind": notification_kind or None,
"actionable": bool(actionable),
"worker_id": resolved_worker_id,
"origin_task_id": origin_task_id or None,
"origin_projection_id": origin_projection_id or None,
"origin_session_id": origin_session_id or None,
}
)
if reply_needed and message_class == "notification":
merged["actionable"] = True
if _clean_text(transport_kind):
merged.setdefault("transport_kind", _clean_text(transport_kind).lower())
if _clean_text(semantic_type):
merged.setdefault("semantic_type", _clean_text(semantic_type).lower())
return merged
def envelope_fields_from_message(message: dict[str, Any]) -> dict[str, Any]:
metadata = normalize_worker_envelope_metadata(
dict(message.get("metadata", {}) or {}),
msg_type=_clean_text(message.get("msg_type")),
semantic_type=_clean_text(message.get("semantic_type")),
transport_kind=_clean_text(message.get("transport_kind")),
from_agent=_clean_text(message.get("from_agent") or message.get("from")),
reply_needed=bool(message.get("reply_needed")),
worker_id=_clean_text(message.get("worker_id")),
task_id=_clean_text(message.get("origin_task_id") or message.get("task_id")),
projection_id=_clean_text(message.get("origin_projection_id") or message.get("projection_id")),
session_id=_clean_text(message.get("origin_session_id") or message.get("session_id")),
)
return {
"message_class": metadata.get("message_class", "chat"),
"protocol_type": metadata.get("protocol_type"),
"notification_kind": metadata.get("notification_kind"),
"actionable": bool(metadata.get("actionable", True)),
"worker_id": metadata.get("worker_id"),
"origin_task_id": metadata.get("origin_task_id"),
"origin_projection_id": metadata.get("origin_projection_id"),
"origin_session_id": metadata.get("origin_session_id"),
"metadata": metadata,
}
def classify_worker_message(message: dict[str, Any]) -> dict[str, Any]:
"""Return a shallow copy with normalized worker-envelope fields."""
merged = dict(message)
envelope = envelope_fields_from_message(message)
merged.update({k: v for k, v in envelope.items() if k != "metadata"})
merged["metadata"] = envelope["metadata"]
return merged
def worker_message_is_actionable(message: dict[str, Any]) -> bool:
envelope = envelope_fields_from_message(message)
return bool(envelope.get("actionable", True)) and envelope.get("message_class") in {"chat", "protocol"}