78.3 Handoff Routing Fix

Documentation for Sessions

78.3 Handoff Routing Fix

Session: 78 Focus: Capability-Based Handoff Routing Status: Completed


Problem Statement

Browser handoffs were only being routed to agents on the cowork platform. The Chrome extension (platform: external) was never receiving browser handoffs, even though it has browser automation capabilities.

Original Code (Broken)

// apis/backend-ai-api/src/app/infrastructure/services/handoff.service.ts

private async routeHandoff(handoff: HandoffDocument): Promise<{ agentId: string } | null> {
  const requirements: {
    platform?: 'cursor' | 'claude-code' | 'cowork' | 'external';
    capability?: 'canBrowse' | 'canEditFiles' | 'canTestUI';
  } = {};

  if (handoff.toAgentType === 'browser') {
    // ❌ OLD: Restricted to cowork platform only
    requirements.platform = 'cowork';
  } else if (handoff.toAgentType === 'code') {
    requirements.capability = 'canEditFiles';
  }

  // ... routing logic
}

Root Cause

The routing logic used platform-based routing for browser handoffs:


Solution: Capability-Based Routing

Changed browser handoff routing from platform-based to capability-based.

Fixed Code

// apis/backend-ai-api/src/app/infrastructure/services/handoff.service.ts

/**
 * Route handoff to best available agent
 * Session 78: Fixed to route browser handoffs to ANY agent with canBrowse capability,
 * not just 'cowork' platform. This allows Chrome extensions (platform: 'external') 
 * to receive browser tasks.
 */
private async routeHandoff(handoff: HandoffDocument): Promise<{ agentId: string } | null> {
  // Determine capability requirement based on toAgentType
  // Session 78: Removed platform restriction for browser handoffs - use capability-based routing
  const requirements: {
    platform?: 'cursor' | 'claude-code' | 'cowork' | 'external';
    capability?: 'canBrowse' | 'canEditFiles' | 'canTestUI';
  } = {};

  if (handoff.toAgentType === 'browser') {
    // ✅ NEW: Route to ANY agent with canBrowse capability
    // This allows Chrome extensions to receive browser handoffs
    requirements.capability = 'canBrowse';
  } else if (handoff.toAgentType === 'code') {
    requirements.capability = 'canEditFiles';
  }

  // If specific agent requested, try that first
  if (handoff.toAgentId) {
    const agent = await this.agentService.getById(handoff.toAgentId);
    if (agent && agent.status !== 'offline') {
      handoff.assignedTo = agent.agentId;
      await handoff.save();
      return { agentId: agent.agentId };
    }
  }

  // Find best available agent based on capabilities
  const bestAgent = await this.agentService.findBestAgent(handoff.workspace, {
    ...requirements,
    preferIdle: true,
  });

  if (bestAgent) {
    handoff.assignedTo = bestAgent.agentId;
    await handoff.save();
    await this.agentService.incrementHandoffReceived(bestAgent.agentId);
    return { agentId: bestAgent.agentId };
  }

  return null;
}

Agent Service: Default Capabilities Fix

The agent service was also updated to correctly assign default capabilities for the external platform (Chrome extensions).

Fixed Code

// apis/backend-ai-api/src/app/infrastructure/services/agent.service.ts

/**
 * Register or update an agent
 */
async register(input: RegisterAgentInput): Promise<AgentRegistryDocument> {
  const now = new Date();

  // Session 78: Fixed default capabilities for 'external' platform (Chrome extensions)
  // External agents can provide browser capabilities - use their provided caps, not defaults
  const defaultCapabilities: AgentCapabilities = {
    canBrowse: ['cowork', 'external'].includes(input.platform),  // ✅ Added 'external'
    canEditFiles: ['cursor', 'claude-code'].includes(input.platform),
    canExecuteCode: ['cursor', 'claude-code'].includes(input.platform),
    canTestUI: ['cowork', 'external'].includes(input.platform),  // ✅ Added 'external'
    domains: [],
    ...input.capabilities, // Override with provided capabilities
  };

  const agent = await this.agentModel.findOneAndUpdate(
    { agentId: input.agentId },
    {
      $set: {
        instanceId: input.instanceId,
        platform: input.platform,
        workspace: input.workspace,
        sessionNumber: input.sessionNumber,
        status: 'online',
        capabilities: defaultCapabilities,
        metadata: input.metadata || {},
        lastHeartbeat: now,
        socketId: input.socketId,
        disconnectedAt: null,
      },
      // ...
    },
    { upsert: true, new: true },
  );

  return agent;
}

Capability Matrix

Before Session 78

Platform canBrowse canEditFiles canExecuteCode canTestUI
cursor ❌ ✅ ✅ ❌
claude-code ❌ ✅ ✅ ❌
cowork ✅ ❌ ❌ ✅
external ❌ ❌ ❌ ❌

After Session 78

Platform canBrowse canEditFiles canExecuteCode canTestUI
cursor ❌ ✅ ✅ ❌
claude-code ❌ ✅ ✅ ❌
cowork ✅ ❌ ❌ ✅
external ✅ ❌ ❌ ✅

Handoff Routing Flow

Before (Broken)

                    ┌─────────────────────────────────┐
                    │ Create Handoff                  │
                    │ toAgentType: 'browser'          │
                    └─────────────────────────────────┘
                                  │
                                  ▼
                    ┌─────────────────────────────────┐
                    │ routeHandoff()                  │
                    │ requirements.platform = 'cowork'│
                    └─────────────────────────────────┘
                                  │
                                  ▼
                    ┌─────────────────────────────────┐
                    │ findBestAgent()                 │
                    │ query: { platform: 'cowork' }   │
                    └─────────────────────────────────┘
                                  │
                         ┌────────┴────────┐
                         │                 │
                         ▼                 ▼
                    ┌─────────┐       ┌─────────────┐
                    │ Cowork  │       │ Extension   │
                    │ ✅ Match │       │ ❌ No Match  │
                    └─────────┘       └─────────────┘

After (Fixed)

                    ┌─────────────────────────────────┐
                    │ Create Handoff                  │
                    │ toAgentType: 'browser'          │
                    └─────────────────────────────────┘
                                  │
                                  ▼
                    ┌─────────────────────────────────────────┐
                    │ routeHandoff()                          │
                    │ requirements.capability = 'canBrowse'   │
                    └─────────────────────────────────────────┘
                                  │
                                  ▼
                    ┌─────────────────────────────────────────┐
                    │ findBestAgent()                         │
                    │ query: { 'capabilities.canBrowse': true }│
                    └─────────────────────────────────────────┘
                                  │
                         ┌────────┴────────┐
                         │                 │
                         ▼                 ▼
                    ┌─────────┐       ┌─────────────┐
                    │ Cowork  │       │ Extension   │
                    │ ✅ Match │       │ ✅ Match    │
                    └─────────┘       └─────────────┘

Agent Discovery Logic

The findBestAgent method in AgentService now correctly queries by capability:

async findBestAgent(
  workspace: string,
  requirements: {
    platform?: AgentPlatform;
    capability?: keyof AgentCapabilities;
    domain?: string;
    preferIdle?: boolean;
  },
): Promise<AgentRegistryDocument | null> {
  const query: any = {
    workspace,
    status: { $in: ['online', 'busy', 'idle'] },
  };

  if (requirements.platform) {
    query.platform = requirements.platform;
  }

  if (requirements.capability) {
    // ✅ Query by capability, not platform
    query[`capabilities.${requirements.capability}`] = true;
  }

  if (requirements.domain) {
    query['capabilities.domains'] = requirements.domain;
  }

  // Sort by idle status first, then by response time
  const agents = await this.agentModel.find(query).sort({
    status: requirements.preferIdle ? 1 : -1,
    averageResponseTimeMs: 1,
  });

  return agents[0] || null;
}

Testing

Verification Steps

  1. Register Chrome Extension:

    • Extension connects with platform: 'external'
    • Check MongoDB: agent should have canBrowse: true
  2. Create Browser Handoff:

    • Create handoff with toAgentType: 'browser'
    • Verify extension receives the handoff notification
  3. Multi-Agent Scenario:

    • Have both Cowork and Extension online
    • Create browser handoff
    • Should route to whichever is idle (or has faster response time)

MongoDB Query to Verify

// Check agent capabilities
db.agentregistries.find({
  platform: 'external',
  'capabilities.canBrowse': true
})

// Check pending browser handoffs
db.handoffs.find({
  toAgentType: 'browser',
  status: 'pending'
})

Key Takeaways

  1. Capability > Platform: Route by what agents can do, not what platform they run on
  2. Extensibility: New platforms with browser capabilities automatically receive browser handoffs
  3. Consistency: Both browser and code handoffs now use capability-based routing
  4. Future-Proof: Adding new capabilities (e.g., canAnalyze, canDeploy) follows the same pattern

Related Code

File Change
handoff.service.ts Removed platform = 'cowork' restriction
agent.service.ts Added external to canBrowse and canTestUI defaults
mcp-connector.js Extension registers with canBrowse: true capability