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
7 changes: 4 additions & 3 deletions DESIGN_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -836,7 +836,7 @@ Tasks can be assigned through multiple strategies:
| **Hierarchical** | Flow down through management chain |
| **Cost-optimized** | Assign to cheapest capable agent |

> **Current state (M3):** Manual, Role-based, and Load-balanced strategies are implemented behind a `TaskAssignmentStrategy` protocol. `TaskAssignmentService` orchestrates assignment with validation and logging. Auction, Hierarchical, and Cost-optimized strategies are planned for M4+.
> **Current state (M4):** All six strategies are implemented behind the `TaskAssignmentStrategy` protocol. Manual, Role-based, Load-balanced, Cost-optimized, and Auction strategies are in the static `STRATEGY_MAP`. Hierarchical requires a `HierarchyResolver` at runtime via `build_strategy_map(hierarchy=...)`.

### 6.5 Agent Execution Loop

Expand Down Expand Up @@ -2393,7 +2393,8 @@ ai-company/
│ │ │ ├── models.py # AssignmentRequest, AssignmentResult, AssignmentCandidate, AgentWorkload
│ │ │ ├── protocol.py # TaskAssignmentStrategy protocol
│ │ │ ├── service.py # TaskAssignmentService (orchestrates strategy + validation)
│ │ │ └── strategies.py # ManualAssignmentStrategy, RoleBasedAssignmentStrategy, LoadBalancedAssignmentStrategy
│ │ │ ├── registry.py # STRATEGY_MAP + build_strategy_map factory
│ │ │ └── strategies.py # All 6 strategy implementations
│ │ ├── decomposition/ # Task decomposition subsystem
│ │ │ ├── __init__.py # Package exports
│ │ │ ├── classifier.py # TaskStructureClassifier (sequential/parallel/mixed)
Expand Down Expand Up @@ -2666,7 +2667,7 @@ These conventions were established during the M0–M2+ review cycle. **Adopted**
| **Pydantic alias for YAML directives** | Adopted (M2.5) | `Field(alias="_remove")` in `TemplateAgentConfig` — YAML uses `_remove: true`, Python accesses `agent.remove`. Keeps the YAML-facing name (underscore prefix signals internal directive) separate from the Python attribute name. | Underscore-prefixed YAML keys signal merge directives vs regular fields. Pydantic alias bridges the naming convention gap cleanly. |
| **Communication foundation** | Adopted (M4) | `MessageBus` protocol with `InMemoryMessageBus` backend (asyncio queues, pull-model `receive()` with shutdown signaling via `asyncio.Event`). `MessageDispatcher` routes to concurrent handlers via `asyncio.TaskGroup` with pre-allocated error collection. `AgentMessenger` per-agent facade auto-fills sender/timestamp/ID; deterministic direct-channel naming `@{sorted_a}:{sorted_b}`. `DeliveryEnvelope` for delivery tracking. `NotBlankStr` validation on all protocol boundary identifiers. | Pull-model avoids callback complexity and enables agents to consume at their own pace. Protocol + backend split enables future persistent/distributed bus implementations. Deterministic DM channel names prevent duplicates. See §5. |
| **Delegation & loop prevention** | Adopted (M4) | `HierarchyResolver` resolves org hierarchy from `Company` at construction (cycle-detected, `MappingProxyType`-frozen). `AuthorityValidator` checks chain-of-command + role permissions. `DelegationGuard` orchestrates five mechanisms (ancestry, depth, dedup, rate limit, circuit breaker) in sequence, short-circuiting on first rejection. `DelegationService` is synchronous (CPU-only); messaging integration deferred. Stateful mechanisms use injectable clock for deterministic testing. Task model extended with `parent_task_id` and `delegation_chain` fields. | Synchronous delegation avoids async complexity for CPU-only validation. Five-mechanism guard provides defence-in-depth against all loop patterns. Injectable clocks enable deterministic testing. See §5.4, §5.5. |
| **Task assignment** | Adopted (M3) | `TaskAssignmentStrategy` protocol with three concrete strategies: Manual (pre-designated), RoleBased (capability scoring via `AgentTaskScorer`), LoadBalanced (workload-aware with score tiebreaker). `TaskAssignmentService` orchestrates with status validation, structured logging, and `STRATEGY_MAP` registry (`MappingProxyType`-wrapped singletons). Inactive agents filtered during scoring. | Pluggable strategies behind a protocol mirror the execution loop and conflict resolution patterns. Reuses `AgentTaskScorer` from routing subsystem. `MappingProxyType` registry matches existing immutability conventions. See §6.4. |
| **Task assignment** | Adopted (M4) | `TaskAssignmentStrategy` protocol with six concrete strategies: Manual (pre-designated), RoleBased (capability scoring via `AgentTaskScorer`), LoadBalanced (workload-aware with score tiebreaker), CostOptimized (cheapest-agent with score tiebreaker), Hierarchical (subordinate delegation via `HierarchyResolver`), Auction (bid = score × availability). `TaskAssignmentService` orchestrates with status validation, structured logging, and `STRATEGY_MAP` registry (`MappingProxyType`-wrapped singletons; five strategies — Hierarchical requires `build_strategy_map(hierarchy=...)`). Inactive agents filtered during scoring. | Pluggable strategies behind a protocol mirror the execution loop and conflict resolution patterns. Reuses `AgentTaskScorer` from routing subsystem. `MappingProxyType` registry matches existing immutability conventions. See §6.4. |
| **Conflict resolution** | Adopted (M4) | `ConflictResolver` protocol with async `resolve()` + sync `build_dissent_records()` split (resolve may call LLM, dissent record is pure construction). Four strategies: `AuthorityResolver` (seniority comparison iterating all N positions, hierarchy proximity tiebreaker via `get_lowest_common_manager`), `DebateResolver` (LLM judge via `JudgeEvaluator` protocol, authority fallback when absent), `HumanEscalationResolver` (stub, returns `ESCALATED_TO_HUMAN`), `HybridResolver` (LLM review + ambiguity escalation/authority fallback). `ConflictResolutionService` follows `DelegationService` pattern (`__slots__`, keyword-only constructor, `MappingProxyType`-wrapped resolver mapping, audit trail). `DissentRecord` preserves losing agent's reasoning. `Conflict.is_cross_department` is a `@computed_field` derived from positions. `HierarchyResolver` extended with `get_lowest_common_manager()` and `get_delegation_depth()`. | Protocol + strategy pattern enables adding new resolution approaches without modifying existing code. Async resolve accommodates LLM calls; sync dissent record avoids unnecessary async overhead. Shared `find_losers` utility prevents code duplication across strategies. See §5.6. |

---
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ AI Company lets you spin up a virtual organization staffed entirely by AI agents
- **Hierarchical Delegation** - Chain-of-command task delegation with five-mechanism loop prevention
- **Conflict Resolution** - Pluggable strategies for resolving agent disagreements (authority, debate, human escalation, hybrid) with dissent audit trail
- **Task Decomposition & Routing** - DAG-based and LLM-based subtask decomposition, structure classification, and agent-task scoring
- **Task Assignment** - Pluggable strategies (manual, role-based, load-balanced) for matching tasks to capable agents
- **Task Assignment** - Pluggable strategies (manual, role-based, load-balanced, cost-optimized, hierarchical, auction) for matching tasks to capable agents
- **Workspace Isolation** - Git worktree-based concurrent workspace isolation with sequential merge and conflict escalation
- **Configurable Autonomy** - From fully autonomous to human-approves-everything, with a Security Ops agent in between
- **Persistent Memory** - Agents remember past decisions, code, relationships (memory layer TBD)
Expand Down
8 changes: 4 additions & 4 deletions src/ai_company/core/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class Task(BaseModel):
type: Classification of the task's work type.
priority: Task urgency and importance level.
project: Project ID this task belongs to.
created_by: Agent ID of the task creator.
created_by: Agent name of the task creator.
assigned_to: Agent ID of the assignee (``None`` if unassigned).
reviewers: Agent IDs of designated reviewers.
dependencies: IDs of tasks this task depends on.
Expand All @@ -69,7 +69,7 @@ class Task(BaseModel):
status: Current lifecycle status.
parent_task_id: Parent task ID when created via delegation
(``None`` for root tasks).
delegation_chain: Ordered agent IDs of delegators (root first).
delegation_chain: Ordered agent names of delegators (root first).
task_structure: Classification of how subtasks relate to each
other (``None`` when not yet classified).
coordination_topology: Coordination topology for multi-agent
Expand All @@ -92,7 +92,7 @@ class Task(BaseModel):
description="Project ID this task belongs to",
)
created_by: NotBlankStr = Field(
description="Agent ID of the task creator",
description="Agent name of the task creator",
)
assigned_to: NotBlankStr | None = Field(
default=None,
Expand Down Expand Up @@ -142,7 +142,7 @@ class Task(BaseModel):
)
delegation_chain: tuple[NotBlankStr, ...] = Field(
default=(),
description="Ordered agent IDs of delegators (root first)",
description="Ordered agent names of delegators (root first)",
)
task_structure: TaskStructure | None = Field(
default=None,
Expand Down
14 changes: 14 additions & 0 deletions src/ai_company/engine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,25 @@
from ai_company.engine.agent_engine import AgentEngine
from ai_company.engine.assignment import (
STRATEGY_MAP,
STRATEGY_NAME_AUCTION,
STRATEGY_NAME_COST_OPTIMIZED,
STRATEGY_NAME_HIERARCHICAL,
STRATEGY_NAME_LOAD_BALANCED,
STRATEGY_NAME_MANUAL,
STRATEGY_NAME_ROLE_BASED,
AgentWorkload,
AssignmentCandidate,
AssignmentRequest,
AssignmentResult,
AuctionAssignmentStrategy,
CostOptimizedAssignmentStrategy,
HierarchicalAssignmentStrategy,
LoadBalancedAssignmentStrategy,
ManualAssignmentStrategy,
RoleBasedAssignmentStrategy,
TaskAssignmentService,
TaskAssignmentStrategy,
build_strategy_map,
)
from ai_company.engine.context import (
DEFAULT_MAX_TURNS,
Expand Down Expand Up @@ -135,6 +142,9 @@
__all__ = [
"DEFAULT_MAX_TURNS",
"STRATEGY_MAP",
"STRATEGY_NAME_AUCTION",
"STRATEGY_NAME_COST_OPTIMIZED",
"STRATEGY_NAME_HIERARCHICAL",
"STRATEGY_NAME_LOAD_BALANCED",
"STRATEGY_NAME_MANUAL",
"STRATEGY_NAME_ROLE_BASED",
Expand All @@ -150,11 +160,13 @@
"AssignmentCandidate",
"AssignmentRequest",
"AssignmentResult",
"AuctionAssignmentStrategy",
"AutoTopologyConfig",
"BudgetChecker",
"BudgetExhaustedError",
"CleanupCallback",
"CooperativeTimeoutStrategy",
"CostOptimizedAssignmentStrategy",
"DecompositionContext",
"DecompositionCycleError",
"DecompositionDepthError",
Expand All @@ -171,6 +183,7 @@
"ExecutionResult",
"ExecutionStateError",
"FailAndReassignStrategy",
"HierarchicalAssignmentStrategy",
"InMemoryResourceLock",
"LlmDecompositionConfig",
"LlmDecompositionStrategy",
Expand Down Expand Up @@ -238,5 +251,6 @@
"WorkspaceRequest",
"WorkspaceSetupError",
"add_token_usage",
"build_strategy_map",
"build_system_prompt",
]
21 changes: 19 additions & 2 deletions src/ai_company/engine/assignment/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""Task assignment engine.

Assigns tasks to agents using pluggable strategies: manual
designation, role-based scoring, or load-balanced selection.
designation, role-based scoring, load-balanced selection,
cost-optimized selection, hierarchical delegation, or auction.
"""

from ai_company.engine.assignment.models import (
Expand All @@ -11,29 +12,45 @@
AssignmentResult,
)
from ai_company.engine.assignment.protocol import TaskAssignmentStrategy
from ai_company.engine.assignment.registry import (
STRATEGY_MAP,
build_strategy_map,
)
from ai_company.engine.assignment.service import TaskAssignmentService
from ai_company.engine.assignment.strategies import (
STRATEGY_MAP,
STRATEGY_NAME_AUCTION,
STRATEGY_NAME_COST_OPTIMIZED,
STRATEGY_NAME_HIERARCHICAL,
STRATEGY_NAME_LOAD_BALANCED,
STRATEGY_NAME_MANUAL,
STRATEGY_NAME_ROLE_BASED,
AuctionAssignmentStrategy,
CostOptimizedAssignmentStrategy,
HierarchicalAssignmentStrategy,
LoadBalancedAssignmentStrategy,
ManualAssignmentStrategy,
RoleBasedAssignmentStrategy,
)

__all__ = [
"STRATEGY_MAP",
"STRATEGY_NAME_AUCTION",
"STRATEGY_NAME_COST_OPTIMIZED",
"STRATEGY_NAME_HIERARCHICAL",
"STRATEGY_NAME_LOAD_BALANCED",
"STRATEGY_NAME_MANUAL",
"STRATEGY_NAME_ROLE_BASED",
"AgentWorkload",
"AssignmentCandidate",
"AssignmentRequest",
"AssignmentResult",
"AuctionAssignmentStrategy",
"CostOptimizedAssignmentStrategy",
"HierarchicalAssignmentStrategy",
"LoadBalancedAssignmentStrategy",
"ManualAssignmentStrategy",
"RoleBasedAssignmentStrategy",
"TaskAssignmentService",
"TaskAssignmentStrategy",
"build_strategy_map",
]
116 changes: 116 additions & 0 deletions src/ai_company/engine/assignment/registry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""Strategy registry and factory for task assignment.

``STRATEGY_MAP`` provides all pre-built strategies except
``HierarchicalAssignmentStrategy`` as an immutable mapping.
``build_strategy_map`` is the preferred factory when a
``HierarchyResolver`` is available (adds the hierarchical
strategy) or a custom ``AgentTaskScorer`` is needed.
"""

from types import MappingProxyType
from typing import TYPE_CHECKING

from ai_company.engine.assignment.strategies import (
STRATEGY_NAME_AUCTION,
STRATEGY_NAME_COST_OPTIMIZED,
STRATEGY_NAME_HIERARCHICAL,
STRATEGY_NAME_LOAD_BALANCED,
STRATEGY_NAME_MANUAL,
STRATEGY_NAME_ROLE_BASED,
AuctionAssignmentStrategy,
CostOptimizedAssignmentStrategy,
HierarchicalAssignmentStrategy,
LoadBalancedAssignmentStrategy,
ManualAssignmentStrategy,
RoleBasedAssignmentStrategy,
)
from ai_company.engine.routing.scorer import AgentTaskScorer
from ai_company.observability import get_logger

if TYPE_CHECKING:
from ai_company.communication.delegation.hierarchy import (
HierarchyResolver,
)
from ai_company.engine.assignment.protocol import (
TaskAssignmentStrategy,
)

logger = get_logger(__name__)

_DEFAULT_SCORER = AgentTaskScorer()

# Excludes HierarchicalAssignmentStrategy — it requires a
# HierarchyResolver at construction. Use
# build_strategy_map(hierarchy=...) to get a complete map
# that includes all strategies.
STRATEGY_MAP: MappingProxyType[str, TaskAssignmentStrategy] = MappingProxyType(
{
STRATEGY_NAME_MANUAL: ManualAssignmentStrategy(),
STRATEGY_NAME_ROLE_BASED: RoleBasedAssignmentStrategy(
_DEFAULT_SCORER,
),
STRATEGY_NAME_LOAD_BALANCED: LoadBalancedAssignmentStrategy(
_DEFAULT_SCORER,
),
STRATEGY_NAME_COST_OPTIMIZED: CostOptimizedAssignmentStrategy(
_DEFAULT_SCORER,
),
STRATEGY_NAME_AUCTION: AuctionAssignmentStrategy(
_DEFAULT_SCORER,
),
},
)


def build_strategy_map(
*,
hierarchy: HierarchyResolver | None = None,
scorer: AgentTaskScorer | None = None,
) -> MappingProxyType[str, TaskAssignmentStrategy]:
"""Build a strategy map, optionally including hierarchical.

When ``hierarchy`` is provided, includes the
``HierarchicalAssignmentStrategy`` in the returned map.
Otherwise, returns the same strategies as the static
``STRATEGY_MAP``.

Args:
hierarchy: Optional hierarchy resolver for the
hierarchical strategy.
scorer: Optional custom scorer. Defaults to the
shared module-level ``AgentTaskScorer`` instance.

Returns:
Immutable mapping of strategy names to instances.
"""
effective_scorer = scorer if scorer is not None else _DEFAULT_SCORER

logger.debug(
"task_assignment.registry.build",
has_hierarchy=hierarchy is not None,
custom_scorer=scorer is not None,
)

strategies: dict[str, TaskAssignmentStrategy] = {
STRATEGY_NAME_MANUAL: ManualAssignmentStrategy(),
STRATEGY_NAME_ROLE_BASED: RoleBasedAssignmentStrategy(
effective_scorer,
),
STRATEGY_NAME_LOAD_BALANCED: LoadBalancedAssignmentStrategy(
effective_scorer,
),
STRATEGY_NAME_COST_OPTIMIZED: CostOptimizedAssignmentStrategy(
effective_scorer,
),
STRATEGY_NAME_AUCTION: AuctionAssignmentStrategy(
effective_scorer,
),
}

if hierarchy is not None:
strategies[STRATEGY_NAME_HIERARCHICAL] = HierarchicalAssignmentStrategy(
effective_scorer,
hierarchy,
)

return MappingProxyType(strategies)
Loading
Loading