Replace agent handoff system with planning-based router (#16003)

## Overview

This PR replaces the dynamic agent handoff system with a more
predictable planning-based router that decides upfront how to handle
multi-agent coordination.

## Major Changes

### 🔄 Architecture Shift: Handoffs → Planning

**Removed:**
- `AgentHandoffEntity` and handoff tracking system
- `AgentHandoffService` and `AgentHandoffExecutorService`
- Dynamic agent-to-agent transfers during execution
- Handoff tool generation and description templates

**Added:**
- `AiRouterService` with two strategies: `simple` (single agent) and
`planned` (multi-agent)
- `AgentPlanExecutorService` for executing multi-step plans
- Plan validation (cycle detection, dependency resolution)
- `UnifiedRouterResult` type with discriminated union

### 🤖 New Standard Agents

Added two new specialized agents:
- **Researcher Agent**: Web search, fact-finding, competitive
intelligence
- **Code Agent**: TypeScript function generation for serverless
workflows

### 🏗️ Router Refactoring (Latest)

Split router responsibilities into focused services:
- `AiRouterStrategyDeciderService`: Decides simple vs planned strategy
- `AiRouterPlanGeneratorService`: Generates and validates execution
plans
- `AiRouterService`: Coordinates between services (reduced from 426→275
lines)

### ⚙️ Configuration Improvements

- Added `outputStrategy` to agent definitions (`direct` vs `synthesize`)
- Removed hardcoded special cases for workflow-builder
- Added `plannerModel` field to workspace entity
- Increased `MAX_STEPS` from 10 to 25 for complex workflows

### 📝 Agent Prompt Refinements

Significantly simplified prompts for better clarity:
- Workflow Builder: 51→36 lines
- Helper: 49→28 lines
- Data Manipulator: Enhanced with sorting guidance

### 🔍 Enhanced Debugging

- Plan reasoning and step count in data message parts
- Router debug info with token usage tracking
- Better logging throughout execution pipeline

## Benefits

1. **Simpler Mental Model**: Router decides upfront vs dynamic transfers
2. **Better Predictability**: Users see the plan before execution
3. **Cleaner Architecture**: SRP with focused services
4. **Configuration Over Code**: Agent behavior via config, not hardcoded
logic
5. **Plan Validation**: Catches invalid dependencies and cycles

## Migration Notes

- Database migration removes `agentHandoff` table
- Adds `plannerModel` column to workspace table
- No API breaking changes (agent endpoints unchanged)

## Testing

- Integration tests updated to remove handoff dependencies
- Agent tool test utilities simplified
- Plan validation covered by new logic

## Next Steps (Future PRs)

- Parallel execution of independent plan steps
- Dynamic re-planning based on results
- Plan caching for common routing patterns
- Error recovery strategies in plan executor
This commit is contained in:
Félix Malfait
2025-11-25 12:10:14 +01:00
committed by GitHub
parent 3c0ae49a23
commit e7ebf51e50
177 changed files with 2720 additions and 3222 deletions
@@ -5,7 +5,7 @@ import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/
import {
type ModelId,
ModelProvider,
} from 'src/engine/core-modules/ai/constants/ai-models.const';
} from 'src/engine/metadata-modules/ai-models/constants/ai-models.const';
import { ClientConfigService } from 'src/engine/core-modules/client-config/services/client-config.service';
import { ClientConfigController } from './client-config.controller';
@@ -5,7 +5,7 @@ import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/
import {
ModelId,
ModelProvider,
} from 'src/engine/core-modules/ai/constants/ai-models.const';
} from 'src/engine/metadata-modules/ai-models/constants/ai-models.const';
import { BillingTrialPeriodDTO } from 'src/engine/core-modules/billing/dtos/billing-trial-period.dto';
import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
@@ -3,7 +3,7 @@ import { Test, type TestingModule } from '@nestjs/testing';
import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface';
import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/support.interface';
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai-models/services/ai-model-registry.service';
import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces';
import { ClientConfigService } from 'src/engine/core-modules/client-config/services/client-config.service';
import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service';
@@ -5,12 +5,6 @@ import { isNonEmptyString } from '@sniptt/guards';
import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface';
import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/support.interface';
import {
AI_MODELS,
ModelProvider,
} from 'src/engine/core-modules/ai/constants/ai-models.const';
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
import { convertCentsToBillingCredits } from 'src/engine/core-modules/ai/utils/convert-cents-to-billing-credits.util';
import {
type ClientAIModelConfig,
type ClientConfig,
@@ -18,6 +12,14 @@ import {
import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service';
import { PUBLIC_FEATURE_FLAGS } from 'src/engine/core-modules/feature-flag/constants/public-feature-flag.const';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { convertCentsToBillingCredits } from 'src/engine/metadata-modules/ai-billing/utils/convert-cents-to-billing-credits.util';
import {
AI_MODELS,
DEFAULT_FAST_MODEL,
DEFAULT_SMART_MODEL,
ModelProvider,
} from 'src/engine/metadata-modules/ai-models/constants/ai-models.const';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai-models/services/ai-model-registry.service';
@Injectable()
export class ClientConfigService {
@@ -62,13 +64,42 @@ export class ClientConfigService {
);
if (aiModels.length > 0) {
aiModels.unshift({
modelId: 'auto',
label: 'Auto',
provider: ModelProvider.NONE,
inputCostPer1kTokensInCredits: 0,
outputCostPer1kTokensInCredits: 0,
});
const defaultSpeedModel =
this.aiModelRegistryService.getDefaultSpeedModel();
const defaultSpeedModelConfig = AI_MODELS.find(
(m) => m.modelId === defaultSpeedModel?.modelId,
);
const defaultSpeedModelLabel =
defaultSpeedModelConfig?.label ||
defaultSpeedModel?.modelId ||
'Default';
const defaultPerformanceModel =
this.aiModelRegistryService.getDefaultPerformanceModel();
const defaultPerformanceModelConfig = AI_MODELS.find(
(m) => m.modelId === defaultPerformanceModel?.modelId,
);
const defaultPerformanceModelLabel =
defaultPerformanceModelConfig?.label ||
defaultPerformanceModel?.modelId ||
'Default';
aiModels.unshift(
{
modelId: DEFAULT_SMART_MODEL,
label: `Smart (${defaultPerformanceModelLabel})`,
provider: ModelProvider.NONE,
inputCostPer1kTokensInCredits: 0,
outputCostPer1kTokensInCredits: 0,
},
{
modelId: DEFAULT_FAST_MODEL,
label: `Fast (${defaultSpeedModelLabel})`,
provider: ModelProvider.NONE,
inputCostPer1kTokensInCredits: 0,
outputCostPer1kTokensInCredits: 0,
},
);
}
const clientConfig: ClientConfig = {