Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,13 @@ curl http://localhost:3000/api/v1/health # backend (via web proxy)

```text
src/ai_company/
api/ # Litestar REST + WebSocket API (controllers, guards, channels, JWT + API key auth, approval gate integration)
api/ # Litestar REST + WebSocket API (controllers, guards, channels, JWT + API key auth, approval gate integration, coordination endpoint)
budget/ # Cost tracking, budget enforcement (pre-flight/in-flight checks, auto-downgrade), billing periods, cost tiers, quota/subscription tracking, CFO cost optimization (anomaly detection, efficiency analysis, downgrade recommendations, approval decisions), spending reports, budget errors (BudgetExhaustedError, DailyLimitExceededError, QuotaExhaustedError)
cli/ # CLI interface (future — thin API wrapper if needed)
communication/ # Message bus, dispatcher, messenger, channels, delegation, loop prevention, conflict resolution, meeting protocol
config/ # YAML company config loading and validation
core/ # Shared domain models, base classes, and resilience config (RetryConfig, RateLimiterConfig)
engine/ # Agent orchestration, execution loops, parallel execution, task decomposition, routing, task assignment, centralized single-writer task state engine (TaskEngine), task lifecycle, recovery, shutdown, workspace isolation, coordination (multi-agent pipeline: TopologyDispatcher protocol, 4 dispatchers — SAS/centralized/decentralized/context-dependent, wave execution, workspace lifecycle integration), coordination error classification, prompt policy validation, checkpoint recovery (checkpoint/, per-turn persistence, heartbeat detection, CheckpointRecoveryStrategy), approval gate (escalation detection, context parking/resume, EscalationInfo/ResumePayload models)
engine/ # Agent orchestration, execution loops, parallel execution, task decomposition, routing, task assignment, centralized single-writer task state engine (TaskEngine), task lifecycle, recovery, shutdown, workspace isolation, coordination (multi-agent pipeline: TopologyDispatcher protocol, 4 dispatchers — SAS/centralized/decentralized/context-dependent, wave execution, workspace lifecycle integration, CoordinationSectionConfig company config bridge, build_coordinator factory), coordination error classification, prompt policy validation, checkpoint recovery (checkpoint/, per-turn persistence, heartbeat detection, CheckpointRecoveryStrategy), approval gate (escalation detection, context parking/resume, EscalationInfo/ResumePayload models)
hr/ # HR engine: hiring, firing, onboarding, offboarding, agent registry, performance tracking (task metrics, collaboration scoring, trend detection), promotion/demotion (criteria evaluation, approval strategies, model mapping)
memory/ # Persistent agent memory (pluggable MemoryBackend protocol), backends/ (Mem0 adapter: backends/mem0/), retrieval pipeline (ranking, injection, context formatting, non-inferable filtering), shared org memory (org/), consolidation/archival (consolidation/)
persistence/ # Operational data persistence — pluggable PersistenceBackend protocol, SQLite initial (see Memory & Persistence design page)
Expand Down Expand Up @@ -151,7 +151,7 @@ web/ # Vue 3 + PrimeVue + Tailwind CSS dashboard
- **Every module** with business logic MUST have: `from ai_company.observability import get_logger` then `logger = get_logger(__name__)`
- **Never** use `import logging` / `logging.getLogger()` / `print()` in application code
- **Variable name**: always `logger` (not `_logger`, not `log`)
- **Event names**: always use constants from the domain-specific module under `ai_company.observability.events` (e.g. `PROVIDER_CALL_START` from `events.provider`, `BUDGET_RECORD_ADDED` from `events.budget`, `CFO_ANOMALY_DETECTED` from `events.cfo`, `CONFLICT_DETECTED` from `events.conflict`, `MEETING_STARTED` from `events.meeting`, `CLASSIFICATION_START` from `events.classification`, `CONSOLIDATION_START` from `events.consolidation`, `ORG_MEMORY_QUERY_START` from `events.org_memory`, `API_REQUEST_STARTED` from `events.api`, `API_ROUTE_NOT_FOUND` from `events.api`, `CODE_RUNNER_EXECUTE_START` from `events.code_runner`, `DOCKER_EXECUTE_START` from `events.docker`, `MCP_INVOKE_START` from `events.mcp`, `SECURITY_EVALUATE_START` from `events.security`, `HR_HIRING_REQUEST_CREATED` from `events.hr`, `PERF_METRIC_RECORDED` from `events.performance`, `TRUST_EVALUATE_START` from `events.trust`, `PROMOTION_EVALUATE_START` from `events.promotion`, `PROMPT_BUILD_START` from `events.prompt`, `MEMORY_RETRIEVAL_START` from `events.memory`, `MEMORY_BACKEND_CONNECTED` from `events.memory`, `MEMORY_ENTRY_STORED` from `events.memory`, `MEMORY_BACKEND_SYSTEM_ERROR` from `events.memory`, `AUTONOMY_ACTION_AUTO_APPROVED` from `events.autonomy`, `TIMEOUT_POLICY_EVALUATED` from `events.timeout`, `PERSISTENCE_AUDIT_ENTRY_SAVED` from `events.persistence`, `TASK_ENGINE_STARTED` from `events.task_engine`, `COORDINATION_STARTED` from `events.coordination`, `COMMUNICATION_DISPATCH_START` from `events.communication`, `COMPANY_STARTED` from `events.company`, `CONFIG_LOADED` from `events.config`, `CORRELATION_ID_CREATED` from `events.correlation`, `DECOMPOSITION_STARTED` from `events.decomposition`, `DELEGATION_STARTED` from `events.delegation`, `EXECUTION_LOOP_START` from `events.execution`, `CHECKPOINT_SAVED` from `events.checkpoint`, `PERSISTENCE_CHECKPOINT_SAVED` from `events.persistence`, `GIT_OPERATION_START` from `events.git`, `PARALLEL_GROUP_START` from `events.parallel`, `PERSONALITY_LOADED` from `events.personality`, `QUOTA_CHECKED` from `events.quota`, `ROLE_ASSIGNED` from `events.role`, `ROUTING_STARTED` from `events.routing`, `SANDBOX_EXECUTE_START` from `events.sandbox`, `TASK_CREATED` from `events.task`, `TASK_ASSIGNMENT_STARTED` from `events.task_assignment`, `TASK_ROUTING_STARTED` from `events.task_routing`, `TEMPLATE_LOADED` from `events.template`, `TOOL_INVOKE_START` from `events.tool`, `TOOL_OUTPUT_WITHHELD` from `events.tool`, `WORKSPACE_CREATED` from `events.workspace`, `APPROVAL_GATE_ESCALATION_DETECTED` from `events.approval_gate`, `APPROVAL_GATE_ESCALATION_FAILED` from `events.approval_gate`, `APPROVAL_GATE_INITIALIZED` from `events.approval_gate`, `APPROVAL_GATE_RISK_CLASSIFIED` from `events.approval_gate`, `APPROVAL_GATE_RISK_CLASSIFY_FAILED` from `events.approval_gate`, `APPROVAL_GATE_CONTEXT_PARKED` from `events.approval_gate`, `APPROVAL_GATE_CONTEXT_PARK_FAILED` from `events.approval_gate`, `APPROVAL_GATE_PARK_TASKLESS` from `events.approval_gate`, `APPROVAL_GATE_RESUME_STARTED` from `events.approval_gate`, `APPROVAL_GATE_CONTEXT_RESUMED` from `events.approval_gate`, `APPROVAL_GATE_RESUME_FAILED` from `events.approval_gate`, `APPROVAL_GATE_RESUME_DELETE_FAILED` from `events.approval_gate`, `APPROVAL_GATE_RESUME_TRIGGERED` from `events.approval_gate`, `APPROVAL_GATE_NO_PARKED_CONTEXT` from `events.approval_gate`, `APPROVAL_GATE_LOOP_WIRING_WARNING` from `events.approval_gate`). Import directly: `from ai_company.observability.events.<domain> import EVENT_CONSTANT`
- **Event names**: always use constants from the domain-specific module under `ai_company.observability.events` (e.g., `PROVIDER_CALL_START` from `events.provider`, `BUDGET_RECORD_ADDED` from `events.budget`, `CFO_ANOMALY_DETECTED` from `events.cfo`, `CONFLICT_DETECTED` from `events.conflict`, `MEETING_STARTED` from `events.meeting`, `CLASSIFICATION_START` from `events.classification`, `CONSOLIDATION_START` from `events.consolidation`, `ORG_MEMORY_QUERY_START` from `events.org_memory`, `API_REQUEST_STARTED` from `events.api`, `API_ROUTE_NOT_FOUND` from `events.api`, `API_COORDINATION_STARTED` from `events.api`, `API_COORDINATION_COMPLETED` from `events.api`, `API_COORDINATION_FAILED` from `events.api`, `API_COORDINATION_AGENT_RESOLVE_FAILED` from `events.api`, `CODE_RUNNER_EXECUTE_START` from `events.code_runner`, `DOCKER_EXECUTE_START` from `events.docker`, `MCP_INVOKE_START` from `events.mcp`, `SECURITY_EVALUATE_START` from `events.security`, `HR_HIRING_REQUEST_CREATED` from `events.hr`, `PERF_METRIC_RECORDED` from `events.performance`, `TRUST_EVALUATE_START` from `events.trust`, `PROMOTION_EVALUATE_START` from `events.promotion`, `PROMPT_BUILD_START` from `events.prompt`, `MEMORY_RETRIEVAL_START` from `events.memory`, `MEMORY_BACKEND_CONNECTED` from `events.memory`, `MEMORY_ENTRY_STORED` from `events.memory`, `MEMORY_BACKEND_SYSTEM_ERROR` from `events.memory`, `AUTONOMY_ACTION_AUTO_APPROVED` from `events.autonomy`, `TIMEOUT_POLICY_EVALUATED` from `events.timeout`, `PERSISTENCE_AUDIT_ENTRY_SAVED` from `events.persistence`, `TASK_ENGINE_STARTED` from `events.task_engine`, `COORDINATION_STARTED` from `events.coordination`, `COORDINATION_FACTORY_BUILT` from `events.coordination`, `COMMUNICATION_DISPATCH_START` from `events.communication`, `COMPANY_STARTED` from `events.company`, `CONFIG_LOADED` from `events.config`, `CORRELATION_ID_CREATED` from `events.correlation`, `DECOMPOSITION_STARTED` from `events.decomposition`, `DELEGATION_STARTED` from `events.delegation`, `EXECUTION_LOOP_START` from `events.execution`, `CHECKPOINT_SAVED` from `events.checkpoint`, `PERSISTENCE_CHECKPOINT_SAVED` from `events.persistence`, `GIT_OPERATION_START` from `events.git`, `PARALLEL_GROUP_START` from `events.parallel`, `PERSONALITY_LOADED` from `events.personality`, `QUOTA_CHECKED` from `events.quota`, `ROLE_ASSIGNED` from `events.role`, `ROUTING_STARTED` from `events.routing`, `SANDBOX_EXECUTE_START` from `events.sandbox`, `TASK_CREATED` from `events.task`, `TASK_ASSIGNMENT_STARTED` from `events.task_assignment`, `TASK_ROUTING_STARTED` from `events.task_routing`, `TEMPLATE_LOADED` from `events.template`, `TOOL_INVOKE_START` from `events.tool`, `TOOL_OUTPUT_WITHHELD` from `events.tool`, `WORKSPACE_CREATED` from `events.workspace`, `APPROVAL_GATE_ESCALATION_DETECTED` from `events.approval_gate`, `APPROVAL_GATE_ESCALATION_FAILED` from `events.approval_gate`, `APPROVAL_GATE_INITIALIZED` from `events.approval_gate`, `APPROVAL_GATE_RISK_CLASSIFIED` from `events.approval_gate`, `APPROVAL_GATE_RISK_CLASSIFY_FAILED` from `events.approval_gate`, `APPROVAL_GATE_CONTEXT_PARKED` from `events.approval_gate`, `APPROVAL_GATE_CONTEXT_PARK_FAILED` from `events.approval_gate`, `APPROVAL_GATE_PARK_TASKLESS` from `events.approval_gate`, `APPROVAL_GATE_RESUME_STARTED` from `events.approval_gate`, `APPROVAL_GATE_CONTEXT_RESUMED` from `events.approval_gate`, `APPROVAL_GATE_RESUME_FAILED` from `events.approval_gate`, `APPROVAL_GATE_RESUME_DELETE_FAILED` from `events.approval_gate`, `APPROVAL_GATE_RESUME_TRIGGERED` from `events.approval_gate`, `APPROVAL_GATE_NO_PARKED_CONTEXT` from `events.approval_gate`, `APPROVAL_GATE_LOOP_WIRING_WARNING` from `events.approval_gate`). Import directly: `from ai_company.observability.events.<domain> import EVENT_CONSTANT`
- **Structured kwargs**: always `logger.info(EVENT, key=value)` — never `logger.info("msg %s", val)`
- **All error paths** must log at WARNING or ERROR with context before raising
- **All state transitions** must log at INFO
Expand Down
7 changes: 7 additions & 0 deletions docs/design/engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,9 @@ All loop implementations satisfy the `ExecutionLoop` runtime-checkable protocol:
composes the execution loop with prompt construction, context management, tool
invocation, and cost tracking into a single `run()` call.

The engine also exposes an optional ``coordinate()`` method that delegates to a
``MultiAgentCoordinator`` when one is configured (see :doc:`coordination`).

**Signature:**

```python
Expand Down Expand Up @@ -787,6 +790,10 @@ coordination:
parallel_default: "centralized"
# mixed tasks -> SAS backbone for sequential phases, delegates parallel sub-tasks
mixed_default: "context_dependent" # hybrid: not a single topology -- engine selects per-phase
max_concurrency_per_wave: null # None = unlimited
fail_fast: false
enable_workspace_isolation: true
base_branch: main
```

The auto-selector uses task structure, artifact count, and (when available from
Expand Down
1 change: 1 addition & 0 deletions docs/design/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,7 @@ future CLI tool are thin clients that call the API -- they contain no business l
| `/api/v1/departments` | Department management |
| `/api/v1/projects` | Project CRUD |
| `/api/v1/tasks` | Task management |
| `POST /api/v1/tasks/{task_id}/coordinate` | Trigger multi-agent coordination |
| `/api/v1/messages` | Communication log |
| `/api/v1/meetings` | Schedule, view meeting outputs |
| `/api/v1/artifacts` | Browse produced artifacts (code, docs, etc.) |
Expand Down
2 changes: 1 addition & 1 deletion docs/roadmap/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The SynthOrg core framework is complete. The following subsystems are built and

- Provider abstraction layer (LiteLLM adapter, routing, resilience)
- Budget and cost management (tracking, enforcement, CFO optimization, quotas)
- Agent engine (execution loops, parallel execution, task decomposition, routing, assignment, recovery, shutdown)
- Agent engine (execution loops, parallel execution, task decomposition, routing, assignment, recovery, shutdown, multi-agent coordination)
- Communication layer (message bus, delegation, loop prevention, conflict resolution, meeting protocol)
- Memory system (pluggable backend protocol, Mem0 adapter, retrieval pipeline, shared org memory, consolidation)
- Security and approval system (rule engine, output scanning, progressive trust, autonomy levels, timeout policies)
Expand Down
8 changes: 8 additions & 0 deletions src/ai_company/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@
from ai_company.communication.bus_protocol import MessageBus # noqa: TC001
from ai_company.config.schema import RootConfig
from ai_company.core.approval import ApprovalItem # noqa: TC001
from ai_company.engine.coordination.service import MultiAgentCoordinator # noqa: TC001
from ai_company.engine.task_engine import TaskEngine # noqa: TC001
from ai_company.hr.registry import AgentRegistryService # noqa: TC001
from ai_company.observability import get_logger
from ai_company.observability.events.api import (
API_APP_SHUTDOWN,
Expand Down Expand Up @@ -355,6 +357,8 @@ def create_app( # noqa: PLR0913
approval_store: ApprovalStore | None = None,
auth_service: AuthService | None = None,
task_engine: TaskEngine | None = None,
coordinator: MultiAgentCoordinator | None = None,
agent_registry: AgentRegistryService | None = None,
) -> Litestar:
"""Create and configure the Litestar application.

Expand All @@ -369,6 +373,8 @@ def create_app( # noqa: PLR0913
approval_store: Approval queue store.
auth_service: Pre-built auth service (for testing).
task_engine: Centralized task state engine.
coordinator: Multi-agent coordinator.
agent_registry: Agent registry service.

Returns:
Configured Litestar application.
Expand Down Expand Up @@ -403,6 +409,8 @@ def create_app( # noqa: PLR0913
approval_store=effective_approval_store,
auth_service=auth_service,
task_engine=task_engine,
coordinator=coordinator,
agent_registry=agent_registry,
startup_time=time.monotonic(),
)

Expand Down
23 changes: 23 additions & 0 deletions src/ai_company/api/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
creates the Litestar ``ChannelsPlugin`` with an in-memory backend.
"""

from typing import TYPE_CHECKING, Any

from litestar.channels import ChannelsPlugin
from litestar.channels.backends.memory import MemoryChannelsBackend

Expand All @@ -24,6 +26,27 @@
)


if TYPE_CHECKING:
from litestar import Request


def get_channels_plugin(
request: Request[Any, Any, Any],
) -> ChannelsPlugin | None:
"""Extract the ``ChannelsPlugin`` from the application, or ``None``.

Args:
request: The incoming Litestar request.

Returns:
The ``ChannelsPlugin`` instance if registered, otherwise ``None``.
"""
for plugin in request.app.plugins:
if isinstance(plugin, ChannelsPlugin):
return plugin
return None
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def create_channels_plugin() -> ChannelsPlugin:
"""Create the channels plugin with in-memory backend.

Expand Down
3 changes: 3 additions & 0 deletions src/ai_company/api/controllers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from ai_company.api.controllers.autonomy import AutonomyController
from ai_company.api.controllers.budget import BudgetController
from ai_company.api.controllers.company import CompanyController
from ai_company.api.controllers.coordination import CoordinationController
from ai_company.api.controllers.departments import DepartmentController
from ai_company.api.controllers.health import HealthController
from ai_company.api.controllers.meetings import MeetingController
Expand All @@ -35,6 +36,7 @@
ApprovalsController,
AutonomyController,
AuthController,
CoordinationController,
)

__all__ = [
Expand All @@ -48,6 +50,7 @@
"BudgetController",
"CompanyController",
"Controller",
"CoordinationController",
"DepartmentController",
"HealthController",
"MeetingController",
Expand Down
20 changes: 10 additions & 10 deletions src/ai_company/api/controllers/approvals.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
from uuid import uuid4

from litestar import Controller, Request, get, post
from litestar.channels import ChannelsPlugin
from litestar.channels import ChannelsPlugin # noqa: TC002
from litestar.datastructures import State # noqa: TC002

from ai_company.api.auth.models import AuthenticatedUser
from ai_company.api.channels import CHANNEL_APPROVALS
from ai_company.api.channels import CHANNEL_APPROVALS, get_channels_plugin
from ai_company.api.dto import (
ApiResponse,
ApproveRequest,
Expand Down Expand Up @@ -44,7 +44,7 @@
logger = get_logger(__name__)


def _get_channels_plugin(
def _require_channels_plugin(
request: Request[Any, Any, Any],
) -> ChannelsPlugin:
"""Extract the ChannelsPlugin from the application.
Expand All @@ -58,12 +58,12 @@ def _get_channels_plugin(
Raises:
RuntimeError: If no ChannelsPlugin is registered on the app.
"""
for plugin in request.app.plugins:
if isinstance(plugin, ChannelsPlugin):
return plugin
msg = "ChannelsPlugin not registered"
logger.error(API_APPROVAL_PUBLISH_FAILED, error=msg)
raise RuntimeError(msg)
plugin = get_channels_plugin(request)
if plugin is None:
msg = "ChannelsPlugin not registered"
logger.error(API_APPROVAL_PUBLISH_FAILED, error=msg)
raise RuntimeError(msg)
return plugin


def _publish_approval_event(
Expand Down Expand Up @@ -93,7 +93,7 @@ def _publish_approval_event(
},
)
try:
channels_plugin = _get_channels_plugin(request)
channels_plugin = _require_channels_plugin(request)
channels_plugin.publish(
event.model_dump_json(),
channels=[CHANNEL_APPROVALS],
Expand Down
Loading
Loading