Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
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",
]
106 changes: 106 additions & 0 deletions src/ai_company/engine/assignment/registry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Strategy registry and factory for task assignment.

``STRATEGY_MAP`` provides five pre-built strategies as an
immutable mapping. ``build_strategy_map`` is the preferred
factory when a ``HierarchyResolver`` is available (adds the
sixth 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

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

_DEFAULT_SCORER = AgentTaskScorer()

# Excludes HierarchicalAssignmentStrategy — it requires a
# HierarchyResolver at construction. Use
# build_strategy_map(hierarchy=...) to get a complete map
# with all six 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 five strategies as the static
``STRATEGY_MAP``.

Args:
hierarchy: Optional hierarchy resolver for the
hierarchical strategy.
scorer: Optional custom scorer. Defaults to a new
``AgentTaskScorer``.

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

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.

medium

The current implementation creates a new AgentTaskScorer instance every time build_strategy_map is called without a scorer argument. This is inconsistent with the module-level STRATEGY_MAP, which uses a single shared _DEFAULT_SCORER instance.

This can lead to surprising behavior where strategies returned by build_strategy_map() (with no arguments) use a different scorer instance than those in the global STRATEGY_MAP. Reusing the module-level _DEFAULT_SCORER would ensure consistency and avoid creating unnecessary objects.

I suggest changing this line to use the shared scorer. You'll also want to update the docstring on line 77 to reflect this change from "Defaults to a new AgentTaskScorer" to something like "Defaults to the shared module-level AgentTaskScorer instance".

Suggested change
effective_scorer = scorer if scorer is not None else AgentTaskScorer()
effective_scorer = scorer if scorer is not None else _DEFAULT_SCORER


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