Agora is built on horizontal composable primitives, not vertical use-case agents. This means:
- ✅ Horizontal = Reusable building blocks (role, swarm, bulk, pipeline)
- ❌ Vertical = Single-purpose specialists (GTM expert, website analyzer, etc.)
Users compose primitives via YAML to create custom agents. Agora provides the building blocks, users build the agents.
┌─────────────────────────────────────────────────────────┐
│ YAML Agent Configurations (api/_lib/agents/*/config.yaml) │
│ - Users define agents by composing primitives │
│ - type: role | swarm | bulk | pipeline │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Agent Loader (api/_lib/utils/agent_loader.py) │
│ - Reads YAML configs │
│ - Instantiates primitives based on type │
│ - Returns configured primitive instance │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Primitive System (api/_lib/primitives/) │
│ - BasePrimitive (abstract interface) │
│ - RolePrimitive (simple LLM call) │
│ - SwarmPrimitive (parallel decomposition) │
│ - BulkPrimitive (batch processing) │
│ - PipelinePrimitive (sequential steps) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ LLM Execution (api/_lib/utils/openai_client.py) │
│ - llm_call_messages() - Non-streaming │
│ - llm_call_streaming() - SSE streaming │
└─────────────────────────────────────────────────────────┘
Purpose: Simple LLM call with configurable system prompt
Use Cases:
- Basic chat agents
- Specialized personas (academic writer, code reviewer)
- Single-step transformations
YAML Config Example:
name: "Academic Writer"
type: role
description: "Writes academic-style content"
model: "gpt-4o-mini"
temperature: 0.7
max_tokens: 2000
system_prompt: |
You are an expert academic writer. Write in formal,
scholarly style with proper citations and structure.Implementation: api/_lib/primitives/role.py
How it works:
- Builds message array with system prompt + context + query
- Calls LLM
- Returns response
Purpose: Parallel task decomposition and synthesis
Use Cases:
- Multi-perspective research
- Complex queries requiring multiple angles
- Divide-and-conquer problems
YAML Config Example:
name: "Research Swarm"
type: swarm
description: "Multi-agent parallel research"
model: "gpt-4o-mini"
max_parallel: 3
temperature: 0.5
max_tokens: 2000
system_prompt: |
You are a research coordinator. Synthesize findings
from multiple research specialists into a coherent answer.
sub_agent_prompt: |
You are a focused research specialist. Answer the
given question accurately and concisely.Implementation: api/_lib/primitives/swarm.py
How it works:
- Decompose: LLM breaks query into N independent sub-tasks
- Execute: Run sub-tasks in parallel (asyncio.gather)
- Synthesize: LLM combines results into final answer
Flow Diagram:
Query: "Research climate change impact on agriculture"
↓
[Decompose]
/ | \
v v v
[Sub1] [Sub2] [Sub3] ← Parallel execution
↓ ↓ ↓
[R1] [R2] [R3]
\ | /
[Synthesize]
↓
Final Answer
Purpose: Process lists/batches item-by-item with structured output
Use Cases:
- "Analyze these 10 companies"
- "Summarize each of these articles"
- "Score these resumes"
YAML Config Example:
name: "Batch Analyzer"
type: bulk
description: "Process multiple items in sequence"
model: "gpt-4o-mini"
temperature: 0.5
max_tokens: 500 # Per item
system_prompt: |
You are a data analyst. Process each item systematically
and provide structured output.
item_prompt_template: |
Analyze this item:
{{item}}
Provide: name, summary, score (1-10), key_insightsImplementation: api/_lib/primitives/bulk.py
How it works:
- Extract list items from query (LLM or regex)
- Process each item sequentially
- Format results as structured table/JSON
- Return aggregated output
Flow Diagram:
Query: "Analyze: CompanyA, CompanyB, CompanyC"
↓
[Extract Items]
["CompanyA", "CompanyB", "CompanyC"]
↓
[Process Each]
Item 1 → [LLM] → Result 1
Item 2 → [LLM] → Result 2
Item 3 → [LLM] → Result 3
↓
[Format Results]
| Name | Score | Summary |
|------|-------|---------|
| A | 8/10 | ... |
| B | 6/10 | ... |
| C | 9/10 | ... |
Purpose: Sequential multi-step workflows where each step feeds the next
Use Cases:
- Research → Draft → Review → Publish
- Data extraction → Analysis → Visualization
- Multi-stage reasoning (CoT)
YAML Config Example:
name: "Research Pipeline"
type: pipeline
description: "Multi-step research and writing workflow"
model: "gpt-4o-mini"
steps:
- name: "research"
prompt: "Research this topic thoroughly: {{query}}"
max_tokens: 1500
- name: "outline"
prompt: "Create outline based on research: {{research}}"
max_tokens: 500
- name: "draft"
prompt: "Write full article from outline: {{outline}}"
max_tokens: 2000
- name: "review"
prompt: "Review and improve: {{draft}}"
max_tokens: 2000Implementation: api/_lib/primitives/pipeline.py
How it works:
- Execute steps sequentially
- Each step receives outputs from previous steps as variables
- Return final step output (or all intermediate outputs)
Flow Diagram:
Query: "Write about quantum computing"
↓
[Step 1: Research]
{{query}} → LLM → research_output
↓
[Step 2: Outline]
{{research}} → LLM → outline_output
↓
[Step 3: Draft]
{{outline}} → LLM → draft_output
↓
[Step 4: Review]
{{draft}} → LLM → final_output
↓
Response
File: api/_lib/utils/agent_loader.py
Purpose: Dynamically instantiate primitives from YAML configs
Primitive Registry:
PRIMITIVE_REGISTRY = {
"role": RolePrimitive,
"swarm": SwarmPrimitive,
"bulk": BulkPrimitive,
"pipeline": PipelinePrimitive,
}Loading Process:
def create_agent_from_config(agent_name: str) -> BasePrimitive:
# 1. Load config.yaml
config = load_agent_config(agent_name)
# 2. Load prompt.txt if exists
if prompt_path.exists():
config["system_prompt"] = prompt_path.read_text()
# 3. Get primitive type from config
primitive_type = config.get("type", "role") # Default to role
# 4. Instantiate primitive
primitive_class = PRIMITIVE_REGISTRY[primitive_type]
return primitive_class(config)Users create agents by:
- Create agent directory:
api/_lib/agents/my_agent/ - Write config.yaml:
name: "My Custom Agent" type: bulk # Choose primitive type description: "My custom agent description" model: "gpt-4o-mini" # ... primitive-specific config
- Write prompt.txt (optional):
You are a specialized assistant for... - Deploy: Agent automatically available via
/api/agentsendpoint
To add a new primitive type:
- Create primitive file:
api/_lib/primitives/my_primitive.py - Extend BasePrimitive:
from .base import BasePrimitive class MyPrimitive(BasePrimitive): async def run(self, query, context, stream=False, **kwargs): # Implement primitive logic pass
- Register primitive:
# In agent_loader.py PRIMITIVE_REGISTRY = { "role": RolePrimitive, "swarm": SwarmPrimitive, "bulk": BulkPrimitive, "pipeline": PipelinePrimitive, "my_primitive": MyPrimitive, # Add here }
- Document: Update this file with primitive description
Primitives are building blocks, not endpoints. Users combine them to create custom agents.
Agents are defined via YAML, not code. This makes them:
- Easy to create
- Version-controllable
- Shareable across teams
- Primitives = How to execute (implementation)
- Agents = What to execute (configuration)
- Agent Loader = Wiring layer
Adding new primitives doesn't break existing agents. New primitives are added to registry, old agents keep working.
api/_lib/agents/
├── gtm_expert/ ← Single-purpose specialist
├── website_analyzer/ ← Single-purpose specialist
├── prompt_optimizer/ ← Single-purpose specialist
└── ocr_pdf/ ← Single-purpose specialist
Problems:
- Each agent is a one-off
- No reusability
- Requires code changes to add agents
- Not composable
api/_lib/primitives/
├── role.py ← Reusable building block
├── swarm.py ← Reusable building block
├── bulk.py ← Reusable building block
└── pipeline.py ← Reusable building block
api/_lib/agents/
├── academic_writer/config.yaml (type: role)
├── research_team/config.yaml (type: swarm)
├── company_analyzer/config.yaml (type: bulk)
└── content_pipeline/config.yaml (type: pipeline)
Benefits:
- Primitives are reused across agents
- Users create agents via YAML (no code)
- New primitives benefit all users
- Composable and extensible
Stateful agents with long-term memory
Use Cases:
- Conversational agents that remember context
- Personalized assistants
- Multi-session workflows
Dynamic agent selection based on query classification
Use Cases:
- Auto-routing to best primitive
- Multi-agent orchestration
- Query triaging
Model Context Protocol integration for tool use
Use Cases:
- File system access
- Database queries
- External API calls
List all available agents (auto-discovered from api/_lib/agents/ directory)
Response:
{
"agents": [
{"name": "academic_writer", "type": "role", "description": "..."},
{"name": "research_team", "type": "swarm", "description": "..."},
{"name": "company_analyzer", "type": "bulk", "description": "..."}
],
"count": 3
}Execute an agent (non-streaming)
Request:
{
"query": "Research quantum computing applications",
"agent_name": "research_team"
}Response:
{
"response": "...",
"agent_used": "research_team",
"tokens_used": 1523
}Execute an agent (SSE streaming)
Query Params: ?query=...&agent_name=research_team
Response: Server-Sent Events
Each primitive should have:
- Unit tests: Test primitive logic in isolation
- Integration tests: Test with agent loader
- E2E tests: Test via API endpoints
Example Test Structure:
tests/
├── primitives/
│ ├── test_role.py
│ ├── test_swarm.py
│ ├── test_bulk.py
│ └── test_pipeline.py
├── agents/
│ └── test_agent_loader.py
└── e2e/
└── test_chat_api.py
Agora's architecture is fundamentally about composability:
- Primitives = Horizontal building blocks
- Agents = YAML configurations of primitives
- Agent Loader = Dynamic instantiation system
Users create custom agents by composing primitives, not by writing code. Developers create new primitives to expand the platform's capabilities.
This architecture enables:
- ✅ Easy agent creation (YAML only)
- ✅ Reusable components (primitives)
- ✅ Extensible platform (new primitives)
- ✅ Clear separation of concerns (primitive vs config)
- ✅ Composable workflows (pipeline, swarm)
The goal: Make building AI agents as simple as writing a YAML file.