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 documentationExploring 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:
| Variable | Required | Example | When Needed |
|---|---|---|---|
OPENAI_API_KEY | For AI features | sk-proj-... | When calling OpenAI-hosted models |
OPENROUTER_API_KEY | For AI features | sk-or-v1-... | When using openrouter/... model IDs |
AGENT_CALLBACK_URL | For Docker | http://host.docker.internal:8001 | When control plane runs in Docker, agent on host |
AGENTFIELD_SERVER | Optional | http://localhost:8080 | Default 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 analysisapp.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:8001This is required because Docker containers cannot reach localhost on the host. See the Docker Deployment Guide for complete details.