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
@@ -3,7 +3,9 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AiModule } from 'src/engine/core-modules/ai/ai.module';
import { AiBillingModule } from 'src/engine/metadata-modules/ai-billing/ai-billing.module';
import { AiModelsModule } from 'src/engine/metadata-modules/ai-models/ai-models.module';
import { AiToolsModule } from 'src/engine/metadata-modules/ai-tools/ai-tools.module';
import { BillingResolver } from 'src/engine/core-modules/billing/billing.resolver';
import { BillingSyncCustomerDataCommand } from 'src/engine/core-modules/billing/commands/billing-sync-customer-data.command';
import { BillingSyncPlansDataCommand } from 'src/engine/core-modules/billing/commands/billing-sync-plans-data.command';
@@ -42,7 +44,9 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
StripeModule,
MessageQueueModule,
PermissionsModule,
AiModule,
AiBillingModule,
AiModelsModule,
AiToolsModule,
WorkspaceDomainsModule,
TypeOrmModule.forFeature([
BillingSubscriptionEntity,
@@ -71,6 +71,7 @@ export class BillingUsageService {
eventName: billingEvents[0].eventName,
value: billingEvents[0].value,
stripeCustomerId: workspaceStripeCustomer.stripeCustomerId,
dimensions: billingEvents[0].dimensions,
});
} catch (error) {
throw new BillingException(
@@ -5,6 +5,7 @@ import { Injectable, Logger } from '@nestjs/common';
import type Stripe from 'stripe';
import { type BillingMeterEventName } from 'src/engine/core-modules/billing/enums/billing-meter-event-names';
import { type BillingDimensions } from 'src/engine/core-modules/billing/types/billing-dimensions.type';
import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@@ -29,17 +30,33 @@ export class StripeBillingMeterEventService {
eventName,
value,
stripeCustomerId,
dimensions,
}: {
eventName: BillingMeterEventName;
value: number;
stripeCustomerId: string;
dimensions?: BillingDimensions;
}) {
const payload: Record<string, string> = {
value: value.toString(),
stripe_customer_id: stripeCustomerId,
};
if (dimensions) {
payload.execution_type = dimensions.execution_type;
if (dimensions.resource_id !== undefined) {
payload.resource_id = dimensions.resource_id || 'none';
}
if (dimensions.execution_context_1 !== undefined) {
payload.execution_context_1 = dimensions.execution_context_1 || 'none';
}
}
await this.stripe.billing.meterEvents.create({
event_name: eventName,
payload: {
value: value.toString(),
stripe_customer_id: stripeCustomerId,
},
payload,
});
}
@@ -0,0 +1,12 @@
/* @license Enterprise */
export type BillingExecutionType =
| 'workflow_execution'
| 'code_execution'
| 'ai_token';
export type BillingDimensions = {
execution_type: BillingExecutionType;
resource_id?: string | null;
execution_context_1?: string | null;
};
@@ -3,8 +3,10 @@
import { type NonNegative } from 'type-fest';
import { type BillingMeterEventName } from 'src/engine/core-modules/billing/enums/billing-meter-event-names';
import { type BillingDimensions } from 'src/engine/core-modules/billing/types/billing-dimensions.type';
export type BillingUsageEvent = {
eventName: BillingMeterEventName;
value: NonNegative<number>;
dimensions?: BillingDimensions;
};