Agent Basics
Understanding the AgenticGoKit Agent System
This guide covers the fundamental concepts of building agents in AgenticGoKit, from the basic interfaces to advanced multi-agent orchestration patterns.
Core Concepts
AgentHandler Interface
The AgentHandler
is the primary interface for implementing agent logic in AgenticGoKit:
go
type AgentHandler interface {
Run(ctx context.Context, event Event, state State) (AgentResult, error)
}
Key Components:
- Event: Contains the user input and metadata
- State: Thread-safe storage for agent data
- AgentResult: The agent's response and updated state
Multi-Agent Orchestration
AgenticGoKit supports multiple orchestration patterns for coordinating agents:
Collaborative Orchestration
All agents process the same event in parallel (config-driven):
go
runner, _ := core.NewRunnerFromConfig("agentflow.toml")
_ = runner.RegisterAgent("researcher", NewResearchAgent())
_ = runner.RegisterAgent("analyzer", NewAnalysisAgent())
_ = runner.RegisterAgent("validator", NewValidationAgent())
Sequential Orchestration
Agents process events in pipeline order:
go
runner, _ := core.NewRunnerFromConfig("agentflow.toml")
_ = runner.RegisterAgent("collector", NewCollectorAgent())
_ = runner.RegisterAgent("processor", NewProcessorAgent())
_ = runner.RegisterAgent("formatter", NewFormatterAgent())
Loop Orchestration
Single agent repeats until conditions are met:
go
runner, _ := core.NewRunnerFromConfig("agentflow.toml")
_ = runner.RegisterAgent("quality-checker", NewQualityCheckerAgent())
Basic Agent Structure
Every agent follows this pattern:
go
package main
import (
"context"
"fmt"
agentflow "github.com/kunalkushwaha/agenticgokit/core"
)
type MyAgentHandler struct {
llm agentflow.ModelProvider
mcpManager agentflow.MCPManager
name string
}
func NewMyAgent(name string, llm agentflow.ModelProvider, mcp agentflow.MCPManager) *MyAgentHandler {
return &MyAgentHandler{
name: name,
llm: llm,
mcpManager: mcp,
}
}
func (a *MyAgentHandler) Run(ctx context.Context, event agentflow.Event, state agentflow.State) (agentflow.AgentResult, error) {
logger := agentflow.Logger()
logger.Info().Str("agent", a.name).Msg("Processing request")
// 1. Extract input from event
eventData := event.GetData()
message, ok := eventData["message"]
if !ok {
return agentflow.AgentResult{}, fmt.Errorf("no message in event data")
}
// 2. Build system prompt
systemPrompt := "You are a helpful assistant."
// 3. Add available tools to prompt
toolPrompt := ""
if a.mcpManager != nil {
toolPrompt = agentflow.FormatToolsForPrompt(ctx, a.mcpManager)
}
fullPrompt := fmt.Sprintf("%s\n%s\nUser: %s", systemPrompt, toolPrompt, message)
// 4. Call LLM
response, err := a.llm.Generate(ctx, fullPrompt)
if err != nil {
return agentflow.AgentResult{}, fmt.Errorf("LLM call failed: %w", err)
}
// 5. Execute any tool calls
var finalResponse string
if a.mcpManager != nil {
toolResults := agentflow.ParseAndExecuteToolCalls(ctx, a.mcpManager, response)
if len(toolResults) > 0 {
// Synthesize tool results
synthesisPrompt := fmt.Sprintf("Original response: %s\nTool results: %v\nProvide a comprehensive answer:", response, toolResults)
finalResponse, _ = a.llm.Generate(ctx, synthesisPrompt)
} else {
finalResponse = response
}
} else {
finalResponse = response
}
// 6. Update state and return
state.Set("response", finalResponse)
state.Set("processed_by", a.name)
return agentflow.AgentResult{
Result: finalResponse,
State: state,
}, nil
}
Agent Patterns
1. Information Gathering Agent
Specializes in research and data collection:
go
type ResearchAgent struct {
llm agentflow.ModelProvider
mcpManager agentflow.MCPManager
}
func (a *ResearchAgent) Run(ctx context.Context, event agentflow.Event, state agentflow.State) (agentflow.AgentResult, error) {
message := event.GetData()["message"]
systemPrompt := `You are a research agent. Your job is to gather comprehensive information using available tools.
Key behaviors:
- Use search tools for current information
- Use fetch_content for specific URLs
- Gather multiple perspectives
- Organize findings clearly`
// Include tools and generate research-focused response
toolPrompt := agentflow.FormatToolsForPrompt(ctx, a.mcpManager)
prompt := fmt.Sprintf("%s\n%s\nResearch query: %s", systemPrompt, toolPrompt, message)
response, err := a.llm.Generate(ctx, prompt)
if err != nil {
return agentflow.AgentResult{}, err
}
// Execute research tools
toolResults := agentflow.ParseAndExecuteToolCalls(ctx, a.mcpManager, response)
// Compile research findings
if len(toolResults) > 0 {
compilationPrompt := fmt.Sprintf(`Research findings: %v
Please compile these findings into a structured research report with:
1. Key findings
2. Sources
3. Important details
4. Areas for further investigation`, toolResults)
response, _ = a.llm.Generate(ctx, compilationPrompt)
}
state.Set("research_findings", response)
return agentflow.AgentResult{Result: response, State: state}, nil
}
2. Analysis Agent
Processes information and draws insights:
go
type AnalysisAgent struct {
llm agentflow.ModelProvider
}
func (a *AnalysisAgent) Run(ctx context.Context, event agentflow.Event, state agentflow.State) (agentflow.AgentResult, error) {
// Get previous research findings
findings, exists := state.Get("research_findings")
if !exists {
return agentflow.AgentResult{}, fmt.Errorf("no research findings to analyze")
}
message := event.GetData()["message"]
systemPrompt := `You are an analysis agent. Your job is to analyze information and provide insights.
Key behaviors:
- Identify patterns and trends
- Draw meaningful conclusions
- Highlight important implications
- Provide actionable insights`
prompt := fmt.Sprintf(`%s
Original query: %s
Research findings: %s
Please provide a thorough analysis with insights and implications.`, systemPrompt, message, findings)
analysis, err := a.llm.Generate(ctx, prompt)
if err != nil {
return agentflow.AgentResult{}, err
}
state.Set("analysis", analysis)
return agentflow.AgentResult{Result: analysis, State: state}, nil
}
3. Synthesis Agent
Combines multiple inputs into final output:
go
type SynthesisAgent struct {
llm agentflow.ModelProvider
}
func (a *SynthesisAgent) Run(ctx context.Context, event agentflow.Event, state agentflow.State) (agentflow.AgentResult, error) {
// Gather all previous work
research, _ := state.Get("research_findings")
analysis, _ := state.Get("analysis")
message := event.GetData()["message"]
systemPrompt := `You are a synthesis agent. Your job is to create comprehensive, well-structured final responses.
Key behaviors:
- Integrate multiple information sources
- Create coherent, flowing narrative
- Ensure completeness and accuracy
- Provide clear, actionable conclusions`
prompt := fmt.Sprintf(`%s
Original query: %s
Research findings: %s
Analysis: %s
Please synthesize this into a comprehensive, well-structured response that fully addresses the original query.`,
systemPrompt, message, research, analysis)
synthesis, err := a.llm.Generate(ctx, prompt)
if err != nil {
return agentflow.AgentResult{}, err
}
state.Set("final_response", synthesis)
return agentflow.AgentResult{Result: synthesis, State: state}, nil
}
State Management
Using State for Data Flow
State allows agents to share data across the workflow:
go
// Agent 1: Store research data
state.Set("research_data", researchResults)
state.Set("sources", sourceList)
state.SetMeta("research_agent", "agent1")
// Agent 2: Access research data
researchData, exists := state.Get("research_data")
if exists {
// Process the research data
}
// Access metadata
researchAgent, _ := state.GetMeta("research_agent")
State Best Practices
- Use descriptive keys:
"user_preferences"
not"prefs"
- Store structured data: Use structs or maps for complex data
- Set metadata: Track which agent processed what
- Handle missing data: Always check if data exists before using
go
// Good: Structured data storage
type UserProfile struct {
Name string
Preferences []string
Context map[string]interface{}
}
profile := UserProfile{
Name: "John",
Preferences: []string{"technical", "detailed"},
Context: map[string]interface{}{"industry": "software"},
}
state.Set("user_profile", profile)
// Good: Metadata tracking
state.SetMeta("processed_by", "agent1")
state.SetMeta("processing_time", time.Now().Format(time.RFC3339))
state.SetMeta("data_sources", "research,analysis")
Error Handling
Graceful Error Management
go
func (a *MyAgent) Run(ctx context.Context, event agentflow.Event, state agentflow.State) (agentflow.AgentResult, error) {
// Validate inputs
message, ok := event.GetData()["message"]
if !ok {
return agentflow.AgentResult{}, fmt.Errorf("missing required field: message")
}
// Handle LLM errors
response, err := a.llm.Generate(ctx, prompt)
if err != nil {
// Log error for debugging
agentflow.Logger().Error().Err(err).Msg("LLM generation failed")
// Return graceful fallback
fallbackResponse := "I apologize, but I'm having trouble processing your request right now. Please try again."
state.Set("error", err.Error())
state.Set("fallback_used", true)
return agentflow.AgentResult{
Result: fallbackResponse,
State: state,
}, nil // Don't propagate error, handle gracefully
}
// Handle tool execution errors
toolResults := agentflow.ParseAndExecuteToolCalls(ctx, a.mcpManager, response)
if len(toolResults) == 0 && strings.Contains(response, "tool_call") {
// Tool call was attempted but failed
agentflow.Logger().Warn().Msg("Tool calls failed, proceeding without tools")
// Continue with original response
}
return agentflow.AgentResult{Result: response, State: state}, nil
}
Testing Agents
Unit Testing Agent Logic
go
package main
import (
"context"
"testing"
agentflow "github.com/kunalkushwaha/agenticgokit/core"
)
func TestMyAgent(t *testing.T) {
// Setup
mockLLM := &MockModelProvider{}
mockMCP := &MockMCPManager{}
agent := NewMyAgent("test-agent", mockLLM, mockMCP)
// Create test event
eventData := agentflow.EventData{"message": "Hello, world!"}
event := agentflow.NewEvent("test", eventData, nil)
state := agentflow.NewState()
// Execute
result, err := agent.Run(context.Background(), event, state)
// Assert
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if result.Result == "" {
t.Error("Expected non-empty result")
}
// Check state was updated
response, exists := result.State.Get("response")
if !exists {
t.Error("Expected response to be set in state")
}
if response != result.Result {
t.Error("State response should match result")
}
}
// Mock implementations for testing
type MockModelProvider struct{}
func (m *MockModelProvider) Generate(ctx context.Context, prompt string) (string, error) {
return "Mock response", nil
}
func (m *MockModelProvider) Name() string { return "mock" }
type MockMCPManager struct{}
func (m *MockMCPManager) ListTools(ctx context.Context) ([]agentflow.ToolSchema, error) { return nil, nil }
func (m *MockMCPManager) CallTool(ctx context.Context, name string, args map[string]interface{}) (interface{}, error) { return nil, nil }
// ... implement other required methods
Next Steps
- Tool Integration - Learn how to use MCP tools
- LLM Providers - Configure different LLM providers
- Multi-Agent Workflows - Orchestrate multiple agents
- Production Deployment - Scale your agents