feat: add multi-feature batching for coding agents

Enable the orchestrator to assign 1-3 features per coding agent subprocess,
selected via dependency chain extension + same-category fill. This reduces
cold-start overhead and leverages shared context across related features.

Orchestrator (parallel_orchestrator.py):
- Add batch tracking: _batch_features and _feature_to_primary data structures
- Add build_feature_batches() with dependency chain + category fill algorithm
- Add start_feature_batch() and _spawn_coding_agent_batch() methods
- Update _on_agent_complete() for batch cleanup across all features
- Update stop_feature() with _feature_to_primary lookup
- Update get_ready_features() to exclude all batch feature IDs
- Update main loop to build batches then spawn per available slot

CLI and agent layer:
- Add --feature-ids (comma-separated) and --batch-size CLI args
- Add feature_ids parameter to run_autonomous_agent() with batch prompt selection
- Add get_batch_feature_prompt() with sequential workflow instructions

WebSocket layer (server/websocket.py):
- Add BATCH_CODING_AGENT_START_PATTERN and BATCH_FEATURES_COMPLETE_PATTERN
- Add _handle_batch_agent_start() and _handle_batch_agent_complete() methods
- Add featureIds field to all agent_update messages
- Track current_feature_id updates as agent moves through batch

Frontend (React UI):
- Add featureIds to ActiveAgent and WSAgentUpdateMessage types
- Update KanbanColumn and DependencyGraph agent-feature maps for batch
- Update AgentCard to show "Batch: #X, #Y, #Z" with active feature highlight
- Add "Features per Agent" segmented control (1-3) in SettingsModal

Settings integration (full stack):
- Add batch_size to schemas, settings router, agent router, process manager
- Default batch_size=3, user-configurable 1-3 via settings UI
- batch_size=1 is functionally identical to pre-batching behavior

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Auto
2026-02-01 16:35:07 +02:00
parent e1e5209866
commit 1607fc8175
16 changed files with 654 additions and 82 deletions

View File

@@ -112,12 +112,25 @@ export function AgentCard({ agent, onShowLogs }: AgentCardProps) {
{/* Feature info */}
<div>
<div className="text-xs text-muted-foreground mb-0.5">
Feature #{agent.featureId}
</div>
<div className="text-sm font-medium truncate" title={agent.featureName}>
{agent.featureName}
</div>
{agent.featureIds && agent.featureIds.length > 1 ? (
<>
<div className="text-xs text-muted-foreground mb-0.5">
Batch: {agent.featureIds.map(id => `#${id}`).join(', ')}
</div>
<div className="text-sm font-bold truncate">
Active: Feature #{agent.featureId}
</div>
</>
) : (
<>
<div className="text-xs text-muted-foreground mb-0.5">
Feature #{agent.featureId}
</div>
<div className="text-sm font-medium truncate" title={agent.featureName}>
{agent.featureName}
</div>
</>
)}
</div>
{/* Thought bubble */}
@@ -195,7 +208,10 @@ export function AgentLogModal({ agent, logs, onClose }: AgentLogModalProps) {
</Badge>
</div>
<p className="text-sm text-muted-foreground">
Feature #{agent.featureId}: {agent.featureName}
{agent.featureIds && agent.featureIds.length > 1
? `Batch: ${agent.featureIds.map(id => `#${id}`).join(', ')}`
: `Feature #${agent.featureId}: ${agent.featureName}`
}
</p>
</div>
</div>

View File

@@ -227,10 +227,14 @@ function DependencyGraphInner({ graphData, onNodeClick, activeAgents = [] }: Dep
}, [])
// Create a map of featureId to agent info for quick lookup
// Maps ALL batch feature IDs to the same agent
const agentByFeatureId = useMemo(() => {
const map = new Map<number, NodeAgentInfo>()
for (const agent of activeAgents) {
map.set(agent.featureId, { name: agent.agentName, state: agent.state })
const ids = agent.featureIds || [agent.featureId]
for (const fid of ids) {
map.set(fid, { name: agent.agentName, state: agent.state })
}
}
return map
}, [activeAgents])

View File

@@ -41,9 +41,14 @@ export function KanbanColumn({
showCreateSpec,
}: KanbanColumnProps) {
// Create a map of feature ID to active agent for quick lookup
const agentByFeatureId = new Map(
activeAgents.map(agent => [agent.featureId, agent])
)
// Maps ALL batch feature IDs to the same agent
const agentByFeatureId = new Map<number, ActiveAgent>()
for (const agent of activeAgents) {
const ids = agent.featureIds || [agent.featureId]
for (const fid of ids) {
agentByFeatureId.set(fid, agent)
}
}
return (
<Card className={`overflow-hidden ${colorMap[color]} py-0`}>

View File

@@ -41,6 +41,12 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
}
}
const handleBatchSizeChange = (size: number) => {
if (!updateSettings.isPending) {
updateSettings.mutate({ batch_size: size })
}
}
const models = modelsData?.models ?? []
const isSaving = updateSettings.isPending
@@ -234,6 +240,30 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
</div>
</div>
{/* Features per Agent */}
<div className="space-y-2">
<Label className="font-medium">Features per Agent</Label>
<p className="text-sm text-muted-foreground">
Number of features assigned to each coding agent
</p>
<div className="flex rounded-lg border overflow-hidden">
{[1, 2, 3].map((size) => (
<button
key={size}
onClick={() => handleBatchSizeChange(size)}
disabled={isSaving}
className={`flex-1 py-2 px-3 text-sm font-medium transition-colors ${
(settings.batch_size ?? 1) === size
? 'bg-primary text-primary-foreground'
: 'bg-background text-foreground hover:bg-muted'
} ${isSaving ? 'opacity-50 cursor-not-allowed' : ''}`}
>
{size}
</button>
))}
</div>
</div>
{/* Update Error */}
{updateSettings.isError && (
<Alert variant="destructive">

View File

@@ -267,6 +267,7 @@ const DEFAULT_SETTINGS: Settings = {
ollama_mode: false,
testing_agent_ratio: 1,
playwright_headless: true,
batch_size: 3,
}
export function useAvailableModels() {

View File

@@ -210,6 +210,7 @@ export function useProjectWebSocket(projectName: string | null) {
agentName: message.agentName,
agentType: message.agentType || 'coding', // Default to coding for backwards compat
featureId: message.featureId,
featureIds: message.featureIds || [message.featureId],
featureName: message.featureName,
state: message.state,
thought: message.thought,
@@ -225,6 +226,7 @@ export function useProjectWebSocket(projectName: string | null) {
agentName: message.agentName,
agentType: message.agentType || 'coding', // Default to coding for backwards compat
featureId: message.featureId,
featureIds: message.featureIds || [message.featureId],
featureName: message.featureName,
state: message.state,
thought: message.thought,

View File

@@ -199,7 +199,8 @@ export interface ActiveAgent {
agentIndex: number // -1 for synthetic completions
agentName: AgentMascot | 'Unknown'
agentType: AgentType // "coding" or "testing"
featureId: number
featureId: number // Current/primary feature (backward compat)
featureIds: number[] // All features in batch
featureName: string
state: AgentState
thought?: string
@@ -270,6 +271,7 @@ export interface WSAgentUpdateMessage {
agentName: AgentMascot | 'Unknown'
agentType: AgentType // "coding" or "testing"
featureId: number
featureIds?: number[] // All features in batch (may be absent for backward compat)
featureName: string
state: AgentState
thought?: string
@@ -530,6 +532,7 @@ export interface Settings {
ollama_mode: boolean
testing_agent_ratio: number // Regression testing agents (0-3)
playwright_headless: boolean
batch_size: number // Features per coding agent batch (1-3)
}
export interface SettingsUpdate {
@@ -537,6 +540,7 @@ export interface SettingsUpdate {
model?: string
testing_agent_ratio?: number
playwright_headless?: boolean
batch_size?: number
}
export interface ProjectSettingsUpdate {