Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

YAML structure appears incorrect: these fields belong at coordination level, not under auto_topology_rules.

Based on CoordinationSectionConfig (see section_config.py), max_concurrency_per_wave, fail_fast, enable_workspace_isolation, and base_branch are top-level fields of the coordination section, not nested under auto_topology_rules. The current indentation suggests they're children of auto_topology_rules.

📝 Suggested fix
 coordination:
   topology: "auto"                    # auto, sas, centralized, decentralized, context_dependent
   auto_topology_rules:
     # sequential tasks -> always single-agent
     sequential_override: "sas"
     # parallel tasks -> select based on domain structure
     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
+  max_concurrency_per_wave: null        # None = unlimited
+  fail_fast: false
+  enable_workspace_isolation: true
+  base_branch: main

Note: If the current indentation is intentional (2-space indent at coordination level), then the YAML is correct but the visual alignment is misleading. Consider adjusting for clarity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/design/engine.md` around lines 793 - 796, The YAML snippet has fields
(max_concurrency_per_wave, fail_fast, enable_workspace_isolation, base_branch)
incorrectly indented under auto_topology_rules; move these keys to the same
indentation level as auto_topology_rules so they become top-level keys of the
coordination section per CoordinationSectionConfig, i.e., detach them from
auto_topology_rules and place them alongside it to match the expected schema
used by the code that reads CoordinationSectionConfig.

```

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
16 changes: 16 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,20 @@
)


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``."""
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