Files
OpenOPC/opc/layer2_organization/custom_runtime.py
T

149 lines
6.3 KiB
Python

"""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
from opc.plugins.office_ui.services.models import ServiceError
normalized_org_id = str(org_id or "").strip()
if not normalized_org_id:
# Isolated org mode must carry a durable org_id; resolving the
# active index here would silently route the run to whichever
# organization is currently loaded.
raise ServiceError(
"org_id_required",
"org_id_required",
{
"project_id": project_id or self.parent.project_id or "default",
"reason": "custom_company_run_requires_durable_org_id",
},
)
org_config, resolved_org_id = self._build_org_config(normalized_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,
active_task_run_registry=getattr(self.parent, "_active_task_run_registry", None),
owns_active_task_run_registry=False,
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()