AgenticGoKit is currently in Beta. APIs may change before the stable v1.0 release.
Skip to content

Adding Features to AgenticGoKit

This guide walks through the process of adding new features to AgenticGoKit, from design to implementation to testing and documentation.

🎯 Feature Development Philosophy

AgenticGoKit follows these principles for feature development:

  • User-Centric: Features should solve real user problems
  • API-First: Design public APIs before implementation
  • Backward Compatibility: Maintain compatibility when possible
  • Performance-Aware: Consider performance implications
  • Test-Driven: Write tests alongside code
  • Documentation-Complete: Include comprehensive documentation

📋 Feature Development Process

1. Feature Proposal Phase

Create Feature Request

Start with a GitHub issue using the feature request template:

markdown
## Feature Request: Advanced Agent Chaining

### Problem Statement
Users need to chain multiple agents in complex workflows where the output of one agent becomes the input of the next, with conditional branching and error handling.

### Proposed Solution
Implement an `AgentChain` component that allows:
- Sequential agent execution
- Conditional branching based on agent results
- Error handling and fallback strategies
- State passing between agents

### Use Cases
1. Research workflow: Search → Analyze → Summarize
2. Content workflow: Generate → Review → Publish
3. Data workflow: Extract → Transform → Load

### Success Criteria
- Agents can be chained declaratively
- Conditional logic can be expressed clearly
- Error handling is robust
- Performance is comparable to manual orchestration

Initial Design Discussion

  • Post in GitHub Discussions for community feedback
  • Discuss with core maintainers
  • Consider alternatives and trade-offs
  • Define scope and non-goals

2. Design Phase

API Design Document

Create a detailed design document:

markdown
# Agent Chaining Feature Design

## Public API

### AgentChain Interface
```go
type AgentChain interface {
    // AddStep adds an agent to the chain
    AddStep(step ChainStep) AgentChain
    
    // AddConditionalStep adds a conditional step
    AddConditionalStep(condition Condition, step ChainStep) AgentChain
    
    // Execute runs the entire chain
    Execute(ctx context.Context, input ChainInput) (ChainResult, error)
    
    // SetErrorHandler sets global error handling strategy
    SetErrorHandler(handler ErrorHandler) AgentChain
}

type ChainStep struct {
    Name        string
    Agent       AgentHandler
    InputMapper InputMapper
    Condition   Condition
    OnError     ErrorAction
}

type ChainInput struct {
    Event Event
    State State
}

type ChainResult struct {
    Steps   []StepResult
    FinalState State
    Success bool
}

Core Implementation Structure

  • core/agent_chain.go - Public interface
  • internal/chain/ - Implementation details
  • internal/chain/executor.go - Chain execution logic
  • internal/chain/condition.go - Conditional logic
  • internal/chain/mapper.go - Input/output mapping

Configuration Integration

toml
[agent_chain]
max_steps = 50
step_timeout = "30s"
enable_parallel_execution = true

### 3. Implementation Phase

#### Step 1: Create Public Interface

Start with the public API in `core/`:

```go
// core/agent_chain.go
package core

import (
    "context"
    "time"
)

// AgentChain defines the interface for chaining multiple agents
type AgentChain interface {
    AddStep(step ChainStep) AgentChain
    AddConditionalStep(condition Condition, step ChainStep) AgentChain
    Execute(ctx context.Context, input ChainInput) (ChainResult, error)
    SetErrorHandler(handler ErrorHandler) AgentChain
}

// ChainStep represents a single step in an agent chain
type ChainStep struct {
    Name        string
    Agent       AgentHandler
    InputMapper InputMapper
    Condition   Condition
    OnError     ErrorAction
    Timeout     time.Duration
}

// NewAgentChain creates a new agent chain
func NewAgentChain(name string) AgentChain {
    return internal.NewAgentChain(name)
}

Step 2: Implement Core Logic

Create the implementation in internal/:

go
// internal/chain/agent_chain.go
package chain

import (
    "context"
    "fmt"
    "sync"
    "time"
    
    "github.com/kunalkushwaha/agenticgokit/core"
)

type agentChain struct {
    name         string
    steps        []core.ChainStep
    errorHandler core.ErrorHandler
    config       ChainConfig
    mu           sync.RWMutex
}

type ChainConfig struct {
    MaxSteps              int
    StepTimeout           time.Duration
    EnableParallelExecution bool
}

func NewAgentChain(name string) core.AgentChain {
    return &agentChain{
        name:   name,
        steps:  make([]core.ChainStep, 0),
        config: DefaultChainConfig(),
    }
}

func (c *agentChain) AddStep(step core.ChainStep) core.AgentChain {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    c.steps = append(c.steps, step)
    return c
}

func (c *agentChain) Execute(ctx context.Context, input core.ChainInput) (core.ChainResult, error) {
    executor := NewChainExecutor(c.config)
    return executor.Execute(ctx, c.steps, input, c.errorHandler)
}

Step 3: Add Execution Logic

go
// internal/chain/executor.go
package chain

import (
    "context"
    "fmt"
    "time"
    
    "github.com/kunalkushwaha/agenticgokit/core"
)

type ChainExecutor struct {
    config ChainConfig
}

func NewChainExecutor(config ChainConfig) *ChainExecutor {
    return &ChainExecutor{config: config}
}

func (e *ChainExecutor) Execute(ctx context.Context, steps []core.ChainStep, input core.ChainInput, errorHandler core.ErrorHandler) (core.ChainResult, error) {
    result := core.ChainResult{
        Steps:      make([]core.StepResult, 0, len(steps)),
        FinalState: input.State.Clone(),
        Success:    true,
    }
    
    currentState := input.State.Clone()
    
    for i, step := range steps {
        // Check context cancellation
        select {
        case <-ctx.Done():
            return result, ctx.Err()
        default:
        }
        
        // Evaluate condition if present
        if step.Condition != nil && !step.Condition.Evaluate(currentState) {
            continue
        }
        
        // Execute step with timeout
        stepCtx, cancel := context.WithTimeout(ctx, e.getStepTimeout(step))
        stepResult, err := e.executeStep(stepCtx, step, input.Event, currentState)
        cancel()
        
        // Handle step result
        result.Steps = append(result.Steps, stepResult)
        
        if err != nil {
            if errorHandler != nil {
                action := errorHandler.HandleError(err, step, currentState)
                if action == core.ErrorActionStop {
                    result.Success = false
                    return result, fmt.Errorf("chain stopped at step %d: %w", i, err)
                }
                // Continue with ErrorActionContinue
            } else {
                result.Success = false
                return result, fmt.Errorf("step %d failed: %w", i, err)
            }
        } else {
            // Update state with step result
            if stepResult.State != nil {
                currentState = stepResult.State
            }
        }
    }
    
    result.FinalState = currentState
    return result, nil
}

func (e *ChainExecutor) executeStep(ctx context.Context, step core.ChainStep, event core.Event, state core.State) (core.StepResult, error) {
    // Map input if mapper is provided
    stepEvent := event
    stepState := state
    
    if step.InputMapper != nil {
        var err error
        stepEvent, stepState, err = step.InputMapper.Map(event, state)
        if err != nil {
            return core.StepResult{}, fmt.Errorf("input mapping failed: %w", err)
        }
    }
    
    // Execute agent
    agentResult, err := step.Agent.Run(ctx, stepEvent, stepState)
    if err != nil {
        return core.StepResult{
            StepName: step.Name,
            Success:  false,
            Error:    err,
        }, err
    }
    
    return core.StepResult{
        StepName: step.Name,
        Success:  true,
        Data:     agentResult.Data,
        State:    agentResult.State,
    }, nil
}

Step 4: Add Builder Pattern Support

go
// core/agent_chain_builder.go
package core

// AgentChainBuilder provides a fluent interface for building agent chains
type AgentChainBuilder struct {
    chain AgentChain
}

// NewAgentChainBuilder creates a new builder
func NewAgentChainBuilder(name string) *AgentChainBuilder {
    return &AgentChainBuilder{
        chain: NewAgentChain(name),
    }
}

// Step adds a simple step to the chain
func (b *AgentChainBuilder) Step(name string, agent AgentHandler) *AgentChainBuilder {
    b.chain.AddStep(ChainStep{
        Name:  name,
        Agent: agent,
    })
    return b
}

// ConditionalStep adds a conditional step
func (b *AgentChainBuilder) ConditionalStep(name string, agent AgentHandler, condition Condition) *AgentChainBuilder {
    b.chain.AddConditionalStep(condition, ChainStep{
        Name:  name,
        Agent: agent,
    })
    return b
}

// WithErrorHandler sets the error handling strategy
func (b *AgentChainBuilder) WithErrorHandler(handler ErrorHandler) *AgentChainBuilder {
    b.chain.SetErrorHandler(handler)
    return b
}

// Build returns the configured chain
func (b *AgentChainBuilder) Build() AgentChain {
    return b.chain
}

4. Testing Phase

Unit Tests

go
// core/agent_chain_test.go
package core

import (
    "context"
    "testing"
    "time"
    
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestAgentChain_Execute(t *testing.T) {
    tests := []struct {
        name     string
        steps    []ChainStep
        input    ChainInput
        expected ChainResult
        wantErr  bool
    }{
        {
            name: "Simple two-step chain",
            steps: []ChainStep{
                {
                    Name:  "step1",
                    Agent: &mockAgent{response: "result1"},
                },
                {
                    Name:  "step2", 
                    Agent: &mockAgent{response: "result2"},
                },
            },
            input: ChainInput{
                Event: NewEvent("test", map[string]interface{}{"query": "test"}),
                State: NewState(),
            },
            expected: ChainResult{
                Success: true,
            },
            wantErr: false,
        },
        {
            name: "Chain with error in middle step",
            steps: []ChainStep{
                {
                    Name:  "step1",
                    Agent: &mockAgent{response: "result1"},
                },
                {
                    Name:  "step2",
                    Agent: &mockAgent{err: fmt.Errorf("step error")},
                },
            },
            input: ChainInput{
                Event: NewEvent("test", map[string]interface{}{"query": "test"}),
                State: NewState(),
            },
            wantErr: true,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            chain := NewAgentChain("test-chain")
            for _, step := range tt.steps {
                chain.AddStep(step)
            }
            
            result, err := chain.Execute(context.Background(), tt.input)
            
            if tt.wantErr {
                assert.Error(t, err)
                return
            }
            
            require.NoError(t, err)
            assert.Equal(t, tt.expected.Success, result.Success)
        })
    }
}

func TestAgentChainBuilder(t *testing.T) {
    agent1 := &mockAgent{response: "result1"}
    agent2 := &mockAgent{response: "result2"}
    
    chain := NewAgentChainBuilder("test-chain").
        Step("step1", agent1).
        Step("step2", agent2).
        WithErrorHandler(&mockErrorHandler{}).
        Build()
    
    assert.NotNil(t, chain)
    
    input := ChainInput{
        Event: NewEvent("test", map[string]interface{}{"query": "test"}),
        State: NewState(),
    }
    
    result, err := chain.Execute(context.Background(), input)
    require.NoError(t, err)
    assert.True(t, result.Success)
    assert.Len(t, result.Steps, 2)
}

// Mock implementations for testing
type mockAgent struct {
    response string
    err      error
}

func (m *mockAgent) Run(ctx context.Context, event Event, state State) (AgentResult, error) {
    if m.err != nil {
        return AgentResult{}, m.err
    }
    
    return AgentResult{
        Data: map[string]interface{}{
            "result": m.response,
        },
        Success: true,
        State:   state,
    }, nil
}

Integration Tests

go
// integration/agent_chain_integration_test.go
//go:build integration
// +build integration

package integration

import (
    "context"
    "testing"
    
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
    "github.com/kunalkushwaha/agenticgokit/core"
)

func TestAgentChain_RealWorkflow(t *testing.T) {
    // Setup real agents
    searchAgent := &SearchAgent{
        provider: newTestSearchProvider(),
    }
    
    analysisAgent := &AnalysisAgent{
        llm: newTestLLMProvider(),
    }
    
    summaryAgent := &SummaryAgent{
        llm: newTestLLMProvider(),
    }
    
    // Create chain
    chain := core.NewAgentChainBuilder("research-workflow").
        Step("search", searchAgent).
        Step("analyze", analysisAgent).
        Step("summarize", summaryAgent).
        Build()
    
    // Execute chain
    input := core.ChainInput{
        Event: core.NewEvent("research", map[string]interface{}{
            "topic": "artificial intelligence trends 2024",
        }),
        State: core.NewState(),
    }
    
    result, err := chain.Execute(context.Background(), input)
    require.NoError(t, err)
    
    assert.True(t, result.Success)
    assert.Len(t, result.Steps, 3)
    assert.Contains(t, result.FinalState.GetString("summary"), "artificial intelligence")
}

5. Documentation Phase

API Documentation

markdown
# Agent Chaining

Agent chaining allows you to create sophisticated workflows by connecting multiple agents in sequence, with conditional logic and error handling.

## Basic Usage

```go
import "github.com/kunalkushwaha/agenticgokit/core"

// Create agents
searchAgent := &SearchAgent{}
analysisAgent := &AnalysisAgent{}
summaryAgent := &SummaryAgent{}

// Build chain
chain := core.NewAgentChainBuilder("research-workflow").
    Step("search", searchAgent).
    Step("analyze", analysisAgent).
    Step("summarize", summaryAgent).
    Build()

// Execute chain
input := core.ChainInput{
    Event: core.NewEvent("research", map[string]interface{}{
        "topic": "AI trends",
    }),
    State: core.NewState(),
}

result, err := chain.Execute(context.Background(), input)
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Final result: %v\n", result.FinalState)

Advanced Features

Conditional Steps

go
chain := core.NewAgentChainBuilder("conditional-workflow").
    Step("initial", initialAgent).
    ConditionalStep("optional", optionalAgent, 
        core.StateCondition("need_extra_processing", true)).
    Step("final", finalAgent).
    Build()

Error Handling

go
errorHandler := &core.ContinueOnErrorHandler{
    MaxErrors: 2,
    LogErrors: true,
}

chain := core.NewAgentChainBuilder("resilient-workflow").
    Step("step1", agent1).
    Step("step2", agent2).
    WithErrorHandler(errorHandler).
    Build()

#### User Guide Update

Add section to `docs/guides/AgentBasics.md`:

```markdown
## Agent Chaining

For complex workflows requiring multiple agents, use AgentChain:

### Creating a Chain

```go
chain := core.NewAgentChainBuilder("my-workflow").
    Step("search", searchAgent).
    Step("analyze", analysisAgent).
    Step("summarize", summaryAgent).
    Build()

Executing the Chain

go
result, err := chain.Execute(ctx, core.ChainInput{
    Event: event,
    State: state,
})

The chain will execute each step in sequence, passing state between steps.


### 6. Configuration Integration

#### Add Configuration Support

```go
// core/config.go - Add to existing config

type Config struct {
    // ... existing fields ...
    
    AgentChain AgentChainConfig `toml:"agent_chain"`
}

type AgentChainConfig struct {
    MaxSteps              int           `toml:"max_steps"`
    StepTimeout           time.Duration `toml:"step_timeout"`
    EnableParallelSteps   bool          `toml:"enable_parallel_steps"`
    DefaultErrorStrategy  string        `toml:"default_error_strategy"`
}

func (c AgentChainConfig) Validate() error {
    if c.MaxSteps < 1 {
        return fmt.Errorf("max_steps must be at least 1")
    }
    if c.StepTimeout < time.Second {
        return fmt.Errorf("step_timeout must be at least 1 second")
    }
    return nil
}

Update Default Configuration

toml
# Default agentflow.toml additions
[agent_chain]
max_steps = 50
step_timeout = "30s"
enable_parallel_steps = false
default_error_strategy = "stop"

7. CLI Integration

Add CLI Commands

go
// cmd/agentcli/cmd/chain.go
package cmd

import (
    "github.com/spf13/cobra"
)

var chainCmd = &cobra.Command{
    Use:   "chain",
    Short: "Manage agent chains",
    Long:  "Commands for creating and managing agent chains",
}

var chainCreateCmd = &cobra.Command{
    Use:   "create <name>",
    Short: "Create a new agent chain",
    Args:  cobra.ExactArgs(1),
    RunE:  runChainCreate,
}

var chainRunCmd = &cobra.Command{
    Use:   "run <chain-name>",
    Short: "Execute an agent chain",
    Args:  cobra.ExactArgs(1),
    RunE:  runChainRun,
}

func init() {
    chainCmd.AddCommand(chainCreateCmd)
    chainCmd.AddCommand(chainRunCmd)
    rootCmd.AddCommand(chainCmd)
}

🔄 Feature Integration Checklist

Pre-Implementation

  • [ ] Feature request created and discussed
  • [ ] Design document written and reviewed
  • [ ] API design approved by maintainers
  • [ ] Breaking changes identified and documented

Implementation

  • [ ] Public API implemented in core/
  • [ ] Implementation details in internal/
  • [ ] Configuration integration added
  • [ ] Error handling implemented
  • [ ] Performance considerations addressed

Testing

  • [ ] Unit tests written and passing
  • [ ] Integration tests written and passing
  • [ ] Benchmarks created for performance-critical paths
  • [ ] Mock implementations created for testing

Documentation

  • [ ] API documentation written
  • [ ] User guide updated
  • [ ] Examples created and tested
  • [ ] Migration guide written (if breaking changes)

CLI Integration

  • [ ] CLI commands added (if applicable)
  • [ ] Help text and examples provided
  • [ ] Shell completion updated

Quality Assurance

  • [ ] Code review completed
  • [ ] Security review completed (if applicable)
  • [ ] Performance testing completed
  • [ ] Manual testing completed

Release Preparation

  • [ ] CHANGELOG.md updated
  • [ ] Version compatibility documented
  • [ ] Release notes drafted
  • [ ] Deprecation notices added (if applicable)

📚 Feature Examples

Simple Feature: Add Timeout to Agent Execution

go
// 1. Extend AgentHandler interface
type AgentHandler interface {
    Run(ctx context.Context, event Event, state State) (AgentResult, error)
    GetTimeout() time.Duration // New method
}

// 2. Update Runner to use timeout
func (r *Runner) executeAgent(ctx context.Context, agent AgentHandler, event Event, state State) (AgentResult, error) {
    timeout := agent.GetTimeout()
    if timeout > 0 {
        var cancel context.CancelFunc
        ctx, cancel = context.WithTimeout(ctx, timeout)
        defer cancel()
    }
    
    return agent.Run(ctx, event, state)
}

// 3. Provide default implementation
type BaseAgent struct {
    timeout time.Duration
}

func (a *BaseAgent) GetTimeout() time.Duration {
    if a.timeout > 0 {
        return a.timeout
    }
    return 30 * time.Second // default
}

Complex Feature: Agent Middleware System

go
// 1. Define middleware interface
type Middleware interface {
    Handle(next AgentHandler) AgentHandler
}

// 2. Create middleware chain
type MiddlewareChain struct {
    middlewares []Middleware
}

func (c *MiddlewareChain) Then(handler AgentHandler) AgentHandler {
    for i := len(c.middlewares) - 1; i >= 0; i-- {
        handler = c.middlewares[i].Handle(handler)
    }
    return handler
}

// 3. Integrate with Runner
func (r *Runner) RegisterAgentWithMiddleware(name string, handler AgentHandler, middlewares ...Middleware) {
    chain := &MiddlewareChain{middlewares: middlewares}
    wrappedHandler := chain.Then(handler)
    r.RegisterAgent(name, wrappedHandler)
}

This comprehensive guide provides the framework for adding any feature to AgenticGoKit while maintaining code quality, performance, and user experience standards.

Released under the Apache 2.0 License.