subagents

package
v1.14.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 26, 2026 License: MIT Imports: 11 Imported by: 0

README

Subagent Framework

Complete implementation of Phase 5 from the Alignment Roadmap: isolated subagent execution with parallel dispatch.

Overview

The subagent framework enables Hex to spawn isolated Claude instances for specialized tasks. Each subagent runs with:

  • Isolated context: Separate conversation history and working memory
  • Specialized configuration: Type-specific tools, temperature, and token limits
  • Parallel execution: Multiple subagents can run concurrently
  • Hook integration: SubagentStop event fires on completion

Subagent Types

1. general-purpose
  • Purpose: General tasks (current default behavior)
  • Tools: All tools available
  • Temperature: 1.0
  • Max Tokens: 4096
2. Explore
  • Purpose: Fast codebase exploration and research
  • Tools: Read, Grep, Glob, Bash (read-only)
  • Temperature: 0.7
  • Max Tokens: 8192
  • Use case: "Find all authentication code"
3. Plan
  • Purpose: Design and planning work
  • Tools: Read, Grep, Glob
  • Temperature: 0.6
  • Max Tokens: 6144
  • Use case: "Create implementation plan for feature X"
4. code-reviewer
  • Purpose: Code review and quality checks
  • Tools: Read, Grep, Glob
  • Temperature: 0.3 (consistent, thorough)
  • Max Tokens: 6144
  • Use case: "Review PR #123 for security issues"

Architecture

internal/subagents/
├── types.go          # Type definitions and configuration
├── context.go        # Context isolation (separate conversation history)
├── executor.go       # Isolated execution engine (spawns hex processes)
├── dispatcher.go     # Parallel dispatch coordination
└── subagents_test.go # Comprehensive tests (81.3% coverage)

Usage Examples

Basic Execution
executor := subagents.NewExecutor()

req := &subagents.ExecutionRequest{
    Type:        subagents.TypeExplore,
    Prompt:      "Find all authentication code in src/",
    Description: "Explore auth system",
}

result, err := executor.Execute(ctx, req)
if err != nil {
    return err
}

fmt.Println(result.Output)
Parallel Dispatch
dispatcher := subagents.NewDispatcher(executor)

requests := []*subagents.DispatchRequest{
    {
        ID: "bug1",
        Request: &subagents.ExecutionRequest{
            Type:        subagents.TypeGeneralPurpose,
            Prompt:      "Fix authentication bug",
            Description: "Fix auth bug",
        },
    },
    {
        ID: "bug2",
        Request: &subagents.ExecutionRequest{
            Type:        subagents.TypeGeneralPurpose,
            Prompt:      "Fix payment bug",
            Description: "Fix payment bug",
        },
    },
}

results := dispatcher.DispatchParallel(ctx, requests)

// Process results
for _, r := range results {
    fmt.Printf("Task %s: %v\n", r.ID, r.Result.Success)
}
With Hooks
// Hook engine must implement subagents.HookEngine interface
result, err := executor.ExecuteWithHooks(ctx, req, hookEngine)

// SubagentStop hook fires with:
// - taskDescription
// - subagentType
// - responseLength
// - tokensUsed
// - success
// - executionTime
Using the Enhanced Task Tool
// Legacy mode (backward compatible)
tool := tools.NewTaskTool()

// New framework mode
tool := tools.NewTaskToolWithFramework()

// Execute via tool
result, err := tool.Execute(ctx, map[string]interface{}{
    "prompt":        "Find all auth code",
    "description":   "Explore authentication",
    "subagent_type": "Explore",  // Must be valid type
    "model":         "claude-sonnet-4-5-20250929",  // Optional
})

Context Isolation

Each subagent gets its own IsolatedContext:

// Created automatically by executor
ctx := subagents.NewIsolatedContext("parent-id", subagents.TypeExplore)

// Isolated conversation history
ctx.AddMessage("user", "Hello")
messages := ctx.GetMessages()  // Returns copy, prevents pollution

// Working memory
ctx.SetMemory("key", "value")
value, ok := ctx.GetMemory("key")

The ContextManager handles lifecycle:

  • Creates contexts on execution
  • Cleans up after completion
  • Supports cleanup of old contexts

Hook Integration

The SubagentStop hook fires when a subagent completes:

{
  "hooks": {
    "SubagentStop": {
      "command": "echo 'Subagent ${CLAUDE_SUBAGENT_TYPE} completed in ${CLAUDE_EXECUTION_TIME}s'"
    }
  }
}

Environment variables available:

  • CLAUDE_TASK_DESCRIPTION
  • CLAUDE_SUBAGENT_TYPE
  • CLAUDE_RESPONSE_LENGTH
  • CLAUDE_TOKENS_USED
  • CLAUDE_SUBAGENT_SUCCESS
  • CLAUDE_EXECUTION_TIME
  • CLAUDE_IS_SUBAGENT=true

Advanced Patterns

Batch Processing
// Process 100 tasks in batches of 10
results := dispatcher.DispatchBatch(ctx, requests, 10)
Wait for First Success
// Race multiple approaches, use first success
result, err := dispatcher.WaitForAny(ctx, requests)
With Aggregation
// Aggregate results from multiple subagents
aggregated, err := dispatcher.DispatchWithAggregation(
    ctx,
    requests,
    func(results []*subagents.Result) (interface{}, error) {
        // Combine results
        return combinedAnalysis, nil
    },
)
Statistics
results := dispatcher.DispatchParallel(ctx, requests)
stats := subagents.CalculateStatistics(results)

fmt.Printf("Total: %d, Success: %d, Failed: %d\n",
    stats.Total, stats.Successful, stats.Failed)

Testing

Comprehensive test suite with 81.3% coverage:

go test ./internal/subagents/... -v
go test ./internal/subagents/... -coverprofile=coverage.out
go tool cover -html=coverage.out

Tests cover:

  • All four subagent types
  • Configuration defaults
  • Context isolation and thread safety
  • Parallel dispatch
  • Hook integration
  • Error handling
  • Validation

Implementation Details

Context Isolation
  • Each subagent has separate ConversationHistory
  • Working memory isolated via mutex-protected maps
  • Context cleanup after execution prevents memory leaks
Process Spawning
  • Spawns hex --print subprocess per subagent
  • Inherits environment (API keys, config)
  • Sets HEX_SUBAGENT_TYPE and HEX_SUBAGENT_CONTEXT_ID
  • Captures stdout/stderr
  • Respects timeouts and cancellation
Parallel Coordination
  • Semaphore limits concurrency (default: 10)
  • Goroutines for parallel execution
  • WaitGroup for synchronization
  • Context cancellation propagated

Future Enhancements

Potential improvements:

  1. Token tracking: Parse API response to get actual token usage
  2. Resume support: Allow subagents to resume from checkpoints
  3. Streaming: Add streaming support for long-running subagents
  4. Resource limits: CPU/memory constraints per subagent
  5. Retry logic: Automatic retry on transient failures
  6. Result caching: Cache results for identical requests

Integration Points

Task Tool
  • /Users/harper/Public/src/2389/hex/internal/tools/task_tool.go
  • Enhanced to use framework via NewTaskToolWithFramework()
  • Backward compatible with legacy mode
Hooks Engine
  • /Users/harper/Public/src/2389/hex/internal/hooks/events.go
  • SubagentStop event defined
  • /Users/harper/Public/src/2389/hex/internal/hooks/engine.go
  • FireSubagentStop() method

Performance Characteristics

  • Context creation: ~1μs
  • Subprocess spawn: ~500ms (one-time build) + API call time
  • Parallel dispatch: ~10 concurrent by default
  • Memory: Isolated contexts prevent memory leaks
  • Cleanup: Automatic via defer in executor

Backward Compatibility

The Task tool maintains full backward compatibility:

  • NewTaskTool() uses legacy subprocess implementation
  • NewTaskToolWithFramework() opts into new framework
  • Same API surface for both modes
  • Existing tests pass unchanged

Documentation

Overview

Package subagents provides isolated execution contexts for subagent tasks.

ABOUTME: Context isolation for subagent execution ABOUTME: Ensures subagents run with separate conversation history and working memory

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func IsValid

func IsValid(t string) bool

IsValid checks if a subagent type string is valid

func ValidSubagentTypes

func ValidSubagentTypes() []string

ValidSubagentTypes returns all valid subagent type strings

Types

type Config

type Config struct {
	// Type is the subagent type (general-purpose, Explore, Plan, code-reviewer)
	Type SubagentType

	// Model is the Claude model to use (optional, inherits from parent if empty)
	Model string

	// Timeout is the maximum execution time (0 = use default)
	Timeout time.Duration

	// MaxTokens is the maximum response length (0 = use default)
	MaxTokens int

	// Temperature controls randomness (0.0-1.0, 0 = use default)
	Temperature float64

	// AllowedTools restricts which tools this subagent can use (nil = all tools)
	AllowedTools []string

	// SystemPrompt overrides the default system prompt for this type (optional)
	SystemPrompt string
}

Config holds configuration for a subagent instance

func DefaultConfig

func DefaultConfig(t SubagentType) *Config

DefaultConfig returns the default configuration for a subagent type

type ContextManager

type ContextManager struct {
	// contains filtered or unexported fields
}

ContextManager manages isolated contexts for multiple subagents

func NewContextManager

func NewContextManager() *ContextManager

NewContextManager creates a new context manager

func (*ContextManager) CleanupOldContexts

func (m *ContextManager) CleanupOldContexts(maxAge time.Duration) int

CleanupOldContexts removes contexts older than the specified duration

func (*ContextManager) ContextCount

func (m *ContextManager) ContextCount() int

ContextCount returns the number of active contexts

func (*ContextManager) CreateContext

func (m *ContextManager) CreateContext(parentID string, agentType SubagentType) *IsolatedContext

CreateContext creates a new isolated context

func (*ContextManager) DeleteContext

func (m *ContextManager) DeleteContext(id string) error

DeleteContext removes a context by ID

func (*ContextManager) GetContext

func (m *ContextManager) GetContext(id string) (*IsolatedContext, error)

GetContext retrieves a context by ID

func (*ContextManager) ListContexts

func (m *ContextManager) ListContexts() []string

ListContexts returns all active context IDs

type DispatchRequest

type DispatchRequest struct {
	ID      string // Unique identifier for this dispatch
	Request *ExecutionRequest
}

DispatchRequest represents a single subagent to dispatch

type DispatchResult

type DispatchResult struct {
	ID     string  // Matches the DispatchRequest.ID
	Result *Result // The execution result
	Error  error   // Error if dispatch failed
}

DispatchResult contains the result of a dispatched subagent

type Dispatcher

type Dispatcher struct {
	// Executor is used to run individual subagents
	Executor *Executor

	// MaxConcurrent limits how many subagents can run in parallel
	MaxConcurrent int
}

Dispatcher coordinates parallel execution of multiple subagents

func NewDispatcher

func NewDispatcher(executor *Executor) *Dispatcher

NewDispatcher creates a new parallel dispatcher

func (*Dispatcher) DispatchBatch

func (d *Dispatcher) DispatchBatch(ctx context.Context, requests []*DispatchRequest, batchSize int) []*DispatchResult

DispatchBatch executes subagents in batches of a specified size Useful for processing large numbers of subagents without overwhelming resources

func (*Dispatcher) DispatchParallel

func (d *Dispatcher) DispatchParallel(ctx context.Context, requests []*DispatchRequest) []*DispatchResult

DispatchParallel executes multiple subagents concurrently and returns all results Results are returned in the order they complete, not request order

func (*Dispatcher) DispatchSequential

func (d *Dispatcher) DispatchSequential(ctx context.Context, requests []*DispatchRequest) []*DispatchResult

DispatchSequential executes subagents one at a time in order Useful when order matters or when resources are constrained

func (*Dispatcher) DispatchWithAggregation

func (d *Dispatcher) DispatchWithAggregation(
	ctx context.Context,
	requests []*DispatchRequest,
	aggregator func([]*Result) (interface{}, error),
) (interface{}, error)

DispatchWithAggregation executes subagents and aggregates their results The aggregator function is called with all successful results

func (*Dispatcher) WaitForAny

func (d *Dispatcher) WaitForAny(ctx context.Context, requests []*DispatchRequest) (*DispatchResult, error)

WaitForAny waits for the first subagent to complete successfully Returns as soon as one subagent succeeds, cancelling others

type ExecutionContext

type ExecutionContext struct {
	context.Context
	Isolated *IsolatedContext
}

ExecutionContext wraps a standard context.Context with subagent-specific data

func NewExecutionContext

func NewExecutionContext(ctx context.Context, isolated *IsolatedContext) *ExecutionContext

NewExecutionContext creates a context for subagent execution

type ExecutionRequest

type ExecutionRequest struct {
	// Type is the subagent type to execute
	Type SubagentType

	// Prompt is the task description for the subagent
	Prompt string

	// Description is a human-readable summary of what this subagent will do
	Description string

	// Config overrides default configuration (optional)
	Config *Config

	// Context contains additional context to inject (optional)
	Context map[string]string
}

ExecutionRequest contains all information needed to execute a subagent

func (*ExecutionRequest) Validate

func (r *ExecutionRequest) Validate() error

Validate checks if the execution request is valid

type Executor

type Executor struct {
	// HexBinPath is the path to the hex binary (empty = search PATH)
	HexBinPath string

	// ContextManager manages isolated contexts
	ContextManager *ContextManager

	// DefaultTimeout is the default execution timeout
	DefaultTimeout time.Duration

	// MaxTimeout is the maximum allowed timeout
	MaxTimeout time.Duration
}

Executor manages the execution of isolated subagent instances

func NewExecutor

func NewExecutor() *Executor

NewExecutor creates a new subagent executor

func (*Executor) Execute

func (e *Executor) Execute(ctx context.Context, req *ExecutionRequest) (*Result, error)

Execute runs a subagent with isolated context and returns the result

func (*Executor) ExecuteWithHooks

func (e *Executor) ExecuteWithHooks(ctx context.Context, req *ExecutionRequest, hookEngine HookEngine) (*Result, error)

ExecuteWithHooks runs a subagent and fires hooks at appropriate times This method integrates with the hooks system

type HookEngine

type HookEngine interface {
	FireSubagentStop(taskDescription, subagentType string, responseLength, tokensUsed int, success bool, executionTime float64) error
}

HookEngine is an interface for firing hooks This allows us to avoid a circular dependency on the hooks package

type IsolatedContext

type IsolatedContext struct {
	// ID is a unique identifier for this context
	ID string

	// ParentID is the ID of the parent context (if any)
	ParentID string

	// Type is the subagent type
	Type SubagentType

	// CreatedAt is when this context was created
	CreatedAt time.Time

	// ConversationHistory stores messages specific to this subagent
	// This is isolated from the parent agent's history
	ConversationHistory []Message

	// WorkingMemory stores ephemeral data for this execution
	WorkingMemory map[string]interface{}
	// contains filtered or unexported fields
}

IsolatedContext represents an isolated execution context for a subagent Each subagent gets its own context that cannot see the parent's history

func NewIsolatedContext

func NewIsolatedContext(parentID string, agentType SubagentType) *IsolatedContext

NewIsolatedContext creates a new isolated context for a subagent

func (*IsolatedContext) AddMessage

func (c *IsolatedContext) AddMessage(role, content string)

AddMessage adds a message to this context's isolated conversation history

func (*IsolatedContext) Clear

func (c *IsolatedContext) Clear()

Clear resets the context (useful for testing or cleanup)

func (*IsolatedContext) GetMemory

func (c *IsolatedContext) GetMemory(key string) (interface{}, bool)

GetMemory retrieves a value from working memory

func (*IsolatedContext) GetMessages

func (c *IsolatedContext) GetMessages() []Message

GetMessages returns a copy of the conversation history This prevents external code from modifying the internal state

func (*IsolatedContext) MessageCount

func (c *IsolatedContext) MessageCount() int

MessageCount returns the number of messages in this context

func (*IsolatedContext) SetMemory

func (c *IsolatedContext) SetMemory(key string, value interface{})

SetMemory stores a value in working memory

type Message

type Message struct {
	Role      string
	Content   string
	Timestamp time.Time
}

Message represents a message in the subagent's isolated conversation

type Result

type Result struct {
	// Success indicates if the subagent completed successfully
	Success bool

	// Output is the subagent's response text
	Output string

	// Error contains error message if Success is false
	Error string

	// Type is the subagent type that was executed
	Type SubagentType

	// Metadata contains execution details (duration, tokens, etc.)
	Metadata map[string]interface{}

	// StartTime is when execution began
	StartTime time.Time

	// EndTime is when execution completed
	EndTime time.Time
}

Result contains the outcome of a subagent execution

func (*Result) Duration

func (r *Result) Duration() time.Duration

Duration returns how long the subagent took to execute

type Statistics

type Statistics struct {
	Total      int
	Successful int
	Failed     int
	Errors     []string
}

Statistics provides metrics about dispatch operations

func CalculateStatistics

func CalculateStatistics(results []*DispatchResult) *Statistics

CalculateStatistics computes statistics from dispatch results

type SubagentType

type SubagentType string

SubagentType represents a predefined subagent type with specialized behavior

const (
	// TypeGeneralPurpose is the default subagent for general tasks
	TypeGeneralPurpose SubagentType = "general-purpose"

	// TypeExplore is optimized for fast codebase exploration and research
	TypeExplore SubagentType = "Explore"

	// TypePlan is specialized for design and planning work
	TypePlan SubagentType = "Plan"

	// TypeCodeReviewer performs code review and quality checks
	TypeCodeReviewer SubagentType = "code-reviewer"
)

type ValidationError

type ValidationError struct {
	Field   string
	Message string
	Details map[string]interface{}
}

ValidationError represents a validation failure

func (*ValidationError) Error

func (e *ValidationError) Error() string

Error implements the error interface

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL