Build Your First Agent

Deep dive into agent development with Agentfield

Build Your First Agent

Understand the architecture and build production-ready agents

This guide takes you beyond the quick start. You'll understand the generated code, add AI-powered reasoners, configure different languages, and learn production patterns.

Prerequisites

  • Completed the Quick Start
  • Agentfield CLI installed
  • Agentfield server running (af server)

Understanding Agent Creation

When you run af init my-agent, the CLI prompts you interactively (unless you use --defaults):

🎯 Creating Agentfield Agent

? Select language:
  ❯ Python
    Go
    TypeScript

? Author name: Jane Smith
? Author email: jane@company.com

✨ Creating project structure...
  ✓ main.py (or main.go / main.ts)
  ✓ reasoners.py (or reasoners.go / reasoners.ts)
  ✓ requirements.txt (or go.mod / package.json)
  ✓ agentfield.yaml
  ✓ README.md

🚀 Agent 'my-agent' created successfully!

Python vs Go vs TypeScript: Choose Your Path

Choose Python if:

  • You need rich AI/ML ecosystem (transformers, langchain, etc.)
  • Rapid prototyping is priority
  • You're comfortable with virtual environments

Trade-off: Deployment requires Python runtime and dependencies

Choose Go if:

  • You want single binary deployment
  • No runtime dependencies in production
  • You need maximum performance

Trade-off: Smaller AI/ML ecosystem compared to Python

Choose TypeScript if:

  • You're building in the Node.js ecosystem
  • You need frontend/backend code sharing
  • You want edge deployment (Vercel, Cloudflare Workers)
  • You prefer strong type safety with modern tooling

Trade-off: Requires Node.js runtime and npm package management

All three languages get the same production features: REST APIs, async execution, identity, and observability.


Generated Project Structure

my-agent/
├── agentfield.yaml      # Agent configuration
├── main.py              # Agent entry point
├── reasoners.py         # AI-powered functions
├── requirements.txt     # Dependencies
└── README.md           # Project documentation

Exploring the Generated Code

The generated agent works immediately with no API keys required.


Enabling AI Features

Want to use AI-powered reasoners? Follow these steps:

Uncomment AI Configuration

In main.py (or main.go / src/agent.ts), uncomment the ai_config section:

app = Agent(
    node_id="my-agent",
    agentfield_server="http://localhost:8080",
    version="1.0.0",
    dev_mode=True,
    ai_config=AIConfig(
        model="gpt-4o",
        temperature=0.2,
    ),
)
app := agent.New(agent.Config{
    NodeID:      "my-agent",
    AgentFieldURL: "http://localhost:8080",
    Version:     "1.0.0",
    ListenAddress: ":0",
    AIConfig: &ai.Config{
        Model:       "gpt-4o",
        APIKey:      os.Getenv("OPENAI_API_KEY"),
        BaseURL:     "https://api.openai.com/v1",
        Temperature: 0.2,
    },
})
const agent = new Agent({
  nodeId: 'my-agent',
  agentFieldUrl: 'http://localhost:8080',
  version: '1.0.0',
  port: 8001,
  devMode: true,
  aiConfig: {
    provider: 'openai',
    model: 'gpt-4o',
    apiKey: process.env.OPENAI_API_KEY,
    temperature: 0.2,
  }
});

Set Your API Key

Either set it in the code or use an environment variable:

export OPENAI_API_KEY=sk-...
# or
export OPENROUTER_API_KEY=sk-or-...

Environment Variables Reference:

VariableRequiredExampleWhen Needed
OPENAI_API_KEYFor AI featuressk-proj-...When calling OpenAI-hosted models
OPENROUTER_API_KEYFor AI featuressk-or-v1-...When using openrouter/... model IDs
AGENT_CALLBACK_URLFor Dockerhttp://host.docker.internal:8001When control plane runs in Docker, agent on host
AGENTFIELD_SERVEROptionalhttp://localhost:8080Default works for local development

Local development with default settings? You only need one AI key to enable AI features. Everything else works out-of-the-box.

For production deployments and advanced configuration, see:

Uncomment AI Reasoners

In reasoners.py (or reasoners.go), uncomment the analyze_sentiment reasoner.

Restart Your Agent

python main.py  # or: go run .

Test AI Reasoner

curl -X POST http://localhost:8080/api/v1/execute/my-agent.demo_analyze_sentiment \
  -H "Content-Type: application/json" \
  -d '{
    "input": {
      "text": "I love building with Agentfield!"
    }
  }'

Response:

{
  "execution_id": "exec_20251117_161355_dd2rdzzb",
  "status": "succeeded",
  "result": {
    "confidence": 0.95,
    "key_phrases": ["love building", "Agentfield"],
    "reasoning": "The text expresses strong positive emotion...",
    "sentiment": "positive"
  },
  "duration_ms": 2943
}

Building Custom Reasoners

Basic AI-Powered Reasoner

from pydantic import BaseModel, Field

class CategoryResult(BaseModel):
    category: str = Field(description="tech, business, health, or sports")
    confidence: float = Field(ge=0.0, le=1.0)
    reasoning: str

@reasoners_router.reasoner()
async def categorize_text(text: str) -> dict:
    """Categorize text into predefined categories"""
    result = await reasoners_router.ai(
        system="You are a text categorization expert. Categories: tech, business, health, sports.",
        user=f"Categorize this text: {text}",
        schema=CategoryResult
    )
    return result.model_dump()
type CategoryResult struct {
    Category   string  `json:"category"`
    Confidence float64 `json:"confidence"`
    Reasoning  string  `json:"reasoning"`
}

app.RegisterReasoner("categorize_text", func(ctx context.Context, input map[string]any) (any, error) {
    text, _ := input["text"].(string)

    var result CategoryResult
    err := app.AI(ctx, agent.AIRequest{
        System: "You are a text categorization expert. Categories: tech, business, health, sports.",
        User:   "Categorize this text: " + text,
        Schema: &result,
    })

    return result, err
})
import { z } from 'zod';

const CategoryResultSchema = z.object({
  category: z.enum(['tech', 'business', 'health', 'sports']),
  confidence: z.number().min(0).max(1),
  reasoning: z.string()
});

agent.reasoner('categorize_text', async (ctx) => {
  const { text } = ctx.input;

  const result = await ctx.ai(
    `Categorize this text: ${text}`,
    {
      system: 'You are a text categorization expert. Categories: tech, business, health, sports.',
      schema: CategoryResultSchema
    }
  );

  return result;
}, { description: 'Categorize text into predefined categories' });

Multi-Step Reasoning

@reasoners_router.reasoner()
async def research_topic(topic: str) -> dict:
    """Multi-step research workflow"""

    # Step 1: Generate search queries
    queries = await reasoners_router.ai(
        system="Generate 3 search queries for research.",
        user=f"Topic: {topic}",
        schema=list[str]
    )

    # Step 2: Call search skill (cross-agent communication)
    results = []
    for query in queries:
        result = await app.call(
            "search-agent.web_search",
            query=query
        )
        results.append(result)

    # Step 3: Synthesize findings
    summary = await reasoners_router.ai(
        system="Synthesize research findings into a coherent summary.",
        user=f"Findings: {results}",
        schema=str
    )

    return {
        "topic": topic,
        "queries": queries,
        "summary": summary
    }
app.RegisterReasoner("research_topic", func(ctx context.Context, input map[string]any) (any, error) {
    topic, _ := input["topic"].(string)

    // Step 1: Generate search queries
    var queries []string
    app.AI(ctx, agent.AIRequest{
        System: "Generate 3 search queries for research.",
        User:   "Topic: " + topic,
        Schema: &queries,
    })

    // Step 2: Call search skill
    var results []interface{}
    for _, query := range queries {
        result, _ := app.Call(ctx, "search-agent.web_search", map[string]string{
            "query": query,
        })
        results = append(results, result)
    }

    // Step 3: Synthesize findings
    var summary string
    app.AI(ctx, agent.AIRequest{
        System: "Synthesize research findings.",
        User:   fmt.Sprintf("Findings: %v", results),
        Schema: &summary,
    })

    return map[string]interface{}{
        "topic":   topic,
        "queries": queries,
        "summary": summary,
    }, nil
})
agent.reasoner('research_topic', async (ctx) => {
  const { topic } = ctx.input;

  // Step 1: Generate search queries
  const queries = await ctx.ai(
    `Topic: ${topic}`,
    {
      system: 'Generate 3 search queries for research.',
      schema: z.array(z.string())
    }
  );

  // Step 2: Call search skill (cross-agent communication)
  const results = [];
  for (const query of queries) {
    const result = await ctx.call(
      'search-agent.web_search',
      { query }
    );
    results.push(result);
  }

  // Step 3: Synthesize findings
  const summary = await ctx.ai(
    `Findings: ${JSON.stringify(results)}`,
    {
      system: 'Synthesize research findings into a coherent summary.',
      schema: z.string()
    }
  );

  return {
    topic,
    queries,
    summary
  };
}, { description: 'Multi-step research workflow' });

Cross-Agent Communication

Call other agents' reasoners and skills using app.call() or ctx.call():

@reasoners_router.reasoner()
async def analyze_with_context(text: str) -> dict:
    """Use multiple agents for comprehensive analysis"""

    # Call sentiment agent
    sentiment = await app.call(
        "sentiment-agent.analyze",
        text=text
    )

    # Call entity extraction agent
    entities = await app.call(
        "nlp-agent.extract_entities",
        text=text
    )

    # Synthesize results
    analysis = await reasoners_router.ai(
        system="Combine sentiment and entity analysis.",
        user=f"Sentiment: {sentiment}, Entities: {entities}",
        schema=dict
    )

    return analysis
app.RegisterReasoner("analyze_with_context", func(ctx context.Context, input map[string]any) (any, error) {
    text, _ := input["text"].(string)

    // Call sentiment agent
    sentiment, _ := app.Call(ctx, "sentiment-agent.analyze", map[string]string{
        "text": text,
    })

    // Call entity extraction agent
    entities, _ := app.Call(ctx, "nlp-agent.extract_entities", map[string]string{
        "text": text,
    })

    // Synthesize results
    var analysis map[string]interface{}
    app.AI(ctx, agent.AIRequest{
        System: "Combine sentiment and entity analysis.",
        User:   fmt.Sprintf("Sentiment: %v, Entities: %v", sentiment, entities),
        Schema: &analysis,
    })

    return analysis, nil
})
agent.reasoner('analyze_with_context', async (ctx) => {
  const { text } = ctx.input;

  // Call sentiment agent
  const sentiment = await ctx.call(
    'sentiment-agent.analyze',
    { text }
  );

  // Call entity extraction agent
  const entities = await ctx.call(
    'nlp-agent.extract_entities',
    { text }
  );

  // Synthesize results
  const analysis = await ctx.ai(
    `Sentiment: ${JSON.stringify(sentiment)}, Entities: ${JSON.stringify(entities)}`,
    {
      system: 'Combine sentiment and entity analysis.',
      schema: z.record(z.any())
    }
  );

  return analysis;
}, { description: 'Use multiple agents for comprehensive analysis' });

The control plane handles:

  • Service discovery
  • Load balancing
  • Workflow tracking
  • Error handling

Memory: Shared State Across Agents

Store and retrieve data across agent executions:

@reasoners_router.reasoner()
async def remember_preference(user_id: str, preference: str) -> dict:
    """Store user preference in shared memory"""

    # Store in memory
    await app.memory.set(
        key=f"user:{user_id}:preference",
        value=preference,
        ttl=86400  # 24 hours
    )

    return {"status": "saved", "user_id": user_id}

@reasoners_router.reasoner()
async def get_preference(user_id: str) -> dict:
    """Retrieve user preference"""

    preference = await app.memory.get(
        key=f"user:{user_id}:preference"
    )

    return {"user_id": user_id, "preference": preference}
app.RegisterReasoner("remember_preference", func(ctx context.Context, input map[string]any) (any, error) {
    userID, _ := input["user_id"].(string)
    preference, _ := input["preference"].(string)

    app.Memory.Set(ctx, agent.MemoryKey{
        Key:   "user:" + userID + ":preference",
        Value: preference,
        TTL:   86400,
    })

    return map[string]string{
        "status":  "saved",
        "user_id": userID,
    }, nil
})
agent.reasoner('remember_preference', async (ctx) => {
  const { user_id, preference } = ctx.input;

  // Store in memory with session scope
  const sessionMem = ctx.memory.session(user_id);
  await sessionMem.set('preference', preference);

  return { status: 'saved', user_id };
}, { description: 'Store user preference in shared memory' });

agent.reasoner('get_preference', async (ctx) => {
  const { user_id } = ctx.input;

  // Retrieve from session memory
  const sessionMem = ctx.memory.session(user_id);
  const preference = await sessionMem.get('preference');

  return { user_id, preference };
}, { description: 'Retrieve user preference' });

Docker Deployment Considerations

Running Control Plane in Docker?

If you're running the Agentfield control plane in Docker but your agent on the host machine, you'll need to set the callback URL:

export AGENT_CALLBACK_URL=http://host.docker.internal:8001

This is required because Docker containers cannot reach localhost on the host. See the Docker Deployment Guide for complete details.


Troubleshooting


Production Deployment


Next Steps