Overview
This guide outlines extensible patterns for customizing Shannon while maintaining upgrade compatibility and clean separation of concerns.
Templates System 1 - Pre-built workflows with low overhead
Tools Add capabilities via MCP, OpenAPI, or Python
Vendor Adapters Domain-specific integrations without core changes
Synthesis Templates Customize research output formatting
Extension Methods Comparison
Extension Type Complexity Code Changes Use Case Templates Low YAML only Repeatable workflows MCP/OpenAPI Tools Low Config only External APIs Built-in Tools Medium Python only Custom logic Vendor Adapters Medium Python + Config Domain-specific integrations Decomposition High Go + Python Custom planning logic
For most use cases, Templates and Vendor Adapters provide the best balance of power and simplicity.
Extend Decomposition (System 2)
For custom planning and reasoning logic
The orchestrator calls the LLM service endpoint /agent/decompose for planning.
When to Use
Custom task decomposition strategies
Domain-specific planning heuristics
Pre/post-processing of LLM requests
Integration with external planning systems
Implementation Options
Lightweight (Go)
Full Custom (Python)
Best for: Pre/post-processing LLM requestsAdd heuristics to go/orchestrator/internal/activities/decompose.go: // Example: Pre-process query before decomposition
func PreprocessQuery ( query string ) string {
// Add domain-specific context
// Normalize input format
// Inject additional constraints
return enhancedQuery
}
// Example: Post-process decomposition response
func PostprocessDecomposition ( resp * DecompositionResponse ) {
// Validate subtasks
// Add fallback steps
// Optimize execution order
}
Best for: Complete custom planning logicAdd new endpoint in python/llm-service/llm_service/api/agent.py: @router.post ( "/agent/decompose/custom" )
async def custom_decompose ( request : DecomposeRequest):
"""Custom decomposition logic for domain-specific tasks."""
# Your custom planning logic
subtasks = custom_planning_algorithm(request.query)
# Return compatible response
return DecompositionResponse(
subtasks = subtasks,
pattern = "custom" ,
complexity = calculate_complexity(subtasks)
)
Route via feature flag or context key: // In orchestrator
if ctx . CustomDecomposition {
endpoint = "/agent/decompose/custom"
} else {
endpoint = "/agent/decompose"
}
Keep response schema compatible with DecompositionResponse to avoid breaking orchestrator workflows.
Add/Customize Templates (System 1)
For repeatable workflows with low overhead
When to Use
Predefined workflows (data analysis, code review, etc.)
Quick task execution without AI planning
Common patterns used frequently
Performance-critical paths
Creating Templates
Create Template File
Place templates in your own directory: # templates/custom/research-workflow.yaml
name : "research_workflow"
description : "Multi-stage research and analysis workflow"
version : "1.0.0"
extends : [] # Optional: inherit from other templates
defaults :
model_tier : "medium"
budget_agent_max : 5000
require_approval : false
nodes :
- id : "search"
type : "simple"
strategy : "react"
tools_allowlist : [ "web_search" , "calculator" ]
budget_max : 1000
depends_on : []
- id : "analyze"
type : "cognitive"
strategy : "chain_of_thought"
tools_allowlist : [ "python_executor" , "calculator" ]
budget_max : 2000
depends_on : [ "search" ]
- id : "synthesize"
type : "simple"
strategy : "react"
tools_allowlist : [ "web_search" ]
budget_max : 1000
depends_on : [ "analyze" ]
edges :
- from : "search"
to : "analyze"
- from : "analyze"
to : "synthesize"
metadata :
category : "research"
author : "your-team"
Register Template
Initialize registry with your template directory: // In orchestrator initialization
registry := templates . InitTemplateRegistry (
"./templates/builtin" ,
"./templates/custom" , // Your custom templates
)
// Validate all templates
if err := registry . Finalize (); err != nil {
log . Fatal ( err )
}
Use Template
Via gRPC API: grpcurl -plaintext -d '{
"query": "Perform research on AI trends",
"context": {
"template": "research_workflow",
"template_version": "1.0.0",
"disable_ai": true
}
}' localhost:50052 shannon.orchestrator.OrchestratorService/SubmitTask
Via HTTP Gateway: curl -X POST http://localhost:8080/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{
"query": "Perform research on AI trends",
"context": {
"template": "research_workflow",
"template_version": "1.0.0"
}
}'
List Available Templates
# Via gRPC
grpcurl -plaintext -d '{}' localhost:50052 \
shannon.orchestrator.OrchestratorService/ListTemplates
Note: HTTP Gateway template listing endpoint may not be implemented yet. Use gRPC for template discovery.
Template Best Practices
Use `extends` for Common Defaults
# Base template (base.yaml)
name : "base_research"
version : "1.0.0"
defaults :
model_tier : "medium"
budget_agent_max : 5000
require_approval : false
# Child template (advanced_research.yaml)
name : "advanced_research"
version : "1.0.0"
extends : [ "base_research" ] # Inherits all defaults
defaults :
budget_agent_max : 10000 # Override specific values
Validate with `registry.Finalize()`
registry := templates . InitTemplateRegistry ( "./templates" )
// Validates:
// - YAML syntax
// - Required fields
// - Parameter types
// - Tool references
if err := registry . Finalize (); err != nil {
log . Fatalf ( "Template validation failed: %v " , err )
}
For extending Shannon’s capabilities
Shannon supports three tool integration methods:
MCP Tools External HTTP APIs with zero code changes
OpenAPI Tools Auto-generated from OpenAPI specs
Built-in Tools Python tools for complex logic
Security Considerations
Always use tools_allowlist in templates to restrict which tools can be used.
Good:
# Template with restricted tools
tools_allowlist :
- "web_search"
- "calculator"
- "my_custom_tool"
Bad:
# Don't allow all tools
tools_allowlist : [ "*" ]
# config/features.yaml
experimental_tools :
enabled : false # Disabled by default
custom_analytics :
enabled : ${ENABLE_CUSTOM_ANALYTICS} # Env-based toggle
# In tool registration
if config.get( "experimental_tools.enabled" ):
registry.register(ExperimentalTool)
Complete Tools Guide See the complete guide for adding MCP, OpenAPI, and built-in Python tools
Vendor Extensions
For domain-specific agents and API integrations
The vendor adapter pattern allows you to integrate proprietary APIs and specialized agents without modifying Shannon’s core code.
Architecture
Generic Shannon (Open Source)
├── python/llm-service/llm_service/tools/openapi_tool.py # Generic loader
├── python/llm-service/llm_service/roles/presets.py # Generic roles
├── go/orchestrator/internal/activities/agent.go # Generic mirroring
└── config/shannon.yaml # Base config
Vendor Extensions (Private)
├── config/overlays/shannon.vendor.yaml # Vendor configs
├── config/openapi_specs/vendor_api.yaml # API specs
├── python/llm-service/llm_service/tools/vendor_adapters/ # Transformations
│ ├── __init__.py # Registry
│ └── vendor.py # VendorAdapter
└── python/llm-service/llm_service/roles/vendor/ # Agent roles
├── __init__.py
└── custom_agent.py # System prompts
When to Use Vendor Extensions
Use when you need:
Domain-specific API integrations (analytics, CRM, e-commerce)
Custom field name transformations
Specialized agent roles with domain knowledge
Session context injection (account IDs, tenant IDs)
Private/proprietary tool configurations
Quick Start
Create Vendor Adapter
# python/llm-service/llm_service/tools/vendor_adapters/myvendor.py
class MyVendorAdapter :
def transform_body ( self , body , operation_id , prompt_params ):
# Field aliasing
if "metrics" in body:
body[ "metrics" ] = [m.replace( "users" , "mv:users" ) for m in body[ "metrics" ]]
# Inject session context
if prompt_params:
body.update(prompt_params)
return body
Register Adapter
# python/llm-service/llm_service/tools/vendor_adapters/__init__.py
def get_vendor_adapter ( name : str ):
if name.lower() == "myvendor" :
from .myvendor import MyVendorAdapter
return MyVendorAdapter()
return None
Create Config Overlay
# config/overlays/shannon.myvendor.yaml
openapi_tools :
myvendor_api :
enabled : true
spec_url : file:///app/config/openapi_specs/myvendor_api.yaml
auth_type : bearer
auth_config :
vendor : myvendor # Triggers adapter loading
token : "${MYVENDOR_API_TOKEN}"
category : custom
(Optional) Create Specialized Agent
# python/llm-service/llm_service/roles/myvendor/custom_agent.py
CUSTOM_AGENT_PRESET = {
"name" : "myvendor_agent" ,
"system_prompt" : "You are a specialized agent for..." ,
"allowed_tools" : [ "myvendor_query" , "myvendor_analyze" ],
"temperature" : 0.7 ,
}
Register with graceful fallback: # roles/presets.py
try :
from .myvendor.custom_agent import CUSTOM_AGENT_PRESET
_PRESETS [ "myvendor_agent" ] = CUSTOM_AGENT_PRESET
except ImportError :
pass # Shannon works without vendor module
Use via Environment
SHANNON_CONFIG_PATH = config/overlays/shannon.myvendor.yaml
MYVENDOR_API_TOKEN = your_token_here
Benefits
✅ Zero Shannon core changes - All vendor logic isolated
✅ Clean separation - Generic infrastructure vs. vendor-specific
✅ Conditional loading - Graceful fallback if vendor module unavailable
✅ Easy to maintain - Vendor code in separate directories
✅ Testable in isolation - Unit test adapters independently
Complete Vendor Adapters Guide Comprehensive guide with examples, testing strategies, and best practices
Human Approval
For gating sensitive operations
Wire require_approval through the SubmitTask request for human-in-the-loop control.
Configuration
# config/features.yaml
approvals :
enabled : true
dangerous_tools :
- "file_write"
- "code_execution"
- "database_query"
complexity_threshold : 0.7 # Require approval for complex tasks
timeout_seconds : 7200 # 2 hour approval window
API Usage
curl -X POST http://localhost:8080/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{
"query": "Delete all records from users table",
"require_approval": true
}'
Approval Flow
Task submitted with require_approval: true
Orchestrator pauses before execution
Approval request sent via webhook/UI
User approves/rejects via API
Workflow continues or terminates
Gateway endpoint (recommended):
curl -X POST "http://localhost:8080/api/v1/approvals/decision" -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" -d '{
"workflow_id": "<workflow-id>",
"approval_id": "<approval-id>",
"approved": true,
"feedback": "Approved for execution"
}'
Legacy admin endpoint at http://localhost:8081/approvals/decision is deprecated. Use the gateway endpoint instead.
Approval gates are enforced in the router before execution.
Feature Flags & Config
Runtime configuration without code changes
Many behaviors are controlled via config/features.yaml and environment variables, loaded through GetWorkflowConfig.
Common Feature Flags
# config/features.yaml
# Template Fallback
template_fallback :
enabled : true # Fallback to AI if template fails
# Tool Selection
tool_selection :
enabled : true # Auto-select tools based on task
max_tools : 5 # Maximum tools per task
# Cognitive Patterns
patterns :
cot_enabled : true # Chain-of-Thought
tot_enabled : true # Tree-of-Thoughts
react_enabled : true # ReAct
# Budget Controls
budgets :
enforce_token_limits : true
enforce_cost_limits : true
default_max_tokens : 10000
default_max_cost_usd : 0.50
# Streaming
streaming :
enabled : true
buffer_size : 1000
chunk_size : 512
Environment Variable Override
# Override via environment
TEMPLATE_FALLBACK_ENABLED = 1
TOOL_SELECTION_MAX_TOOLS = 10
DEFAULT_MAX_TOKENS = 20000
# Start services with overrides
docker compose up -d
Dynamic Config Loading
// In orchestrator
config := GetWorkflowConfig ()
if config . TemplateFallbackEnabled {
// Use AI fallback
}
if config . BudgetsEnforceTokenLimits {
// Apply token budget
}
Synthesis Templates (Output Customization)
For customizing how Shannon formats final research answers
Synthesis templates control how multi-agent research results are formatted and presented. They’re particularly useful for Deep Research workflows.
When to Use
Customize output format for specific domains (market research, academic, executive summaries)
Enforce citation styles
Control answer structure and length
Inject domain-specific formatting rules
Template Methods
Method Context Parameter Use Case Named template synthesis_template: "name"Reusable .tmpl files Verbatim override synthesis_template_override: "..."One-time custom format Min length synthesis_min_length: NEnforce minimum character count
Using Named Templates
Create templates in config/templates/synthesis/:
{{ /* config/templates/synthesis/market_research.tmpl */ }}
{{ template "_base.tmpl" }}
## Executive Summary
Provide a 2 - 3 sentence summary of key findings .
## Market Analysis
- Key trends
- Competitive landscape
- Opportunities and risks
## Data Sources
Use [ n ] citation format for all claims .
## Recommendations
Actionable insights based on the analysis .
Use via API:
curl -X POST http://localhost:8080/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{
"query": "Analyze the electric vehicle market in 2025",
"context": {
"force_research": true,
"synthesis_template": "market_research"
}
}'
Verbatim Override
For one-time custom formatting without creating a template file:
curl -X POST http://localhost:8080/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{
"query": "Compare major cloud providers",
"context": {
"force_research": true,
"synthesis_template_override": "You are a cloud infrastructure analyst.\n\n## Analysis Format\n- Start with a comparison table\n- Use [n] citations\n- Include cost analysis section\n- End with recommendations"
}
}'
When using synthesis_template_override, you bypass the base template’s citation contract. You must include citation rules ([n] format) in your override text.
Minimum Length Control
Enforce a minimum output length:
curl -X POST http://localhost:8080/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{
"query": "Comprehensive analysis of AI governance",
"context": {
"force_research": true,
"synthesis_min_length": 5000
}
}'
Template Selection Logic
Selection is based on context and workflow signals:
If context.synthesis_template is set → use that named template.
Else if any of:
context.workflow_type == "research"
context.force_research == true
context.synthesis_style == "comprehensive"
context.research_areas is non-empty
→ use research_comprehensive.tmpl.
Else if context.synthesis_style == "concise" → use research_concise.tmpl.
Otherwise → use normal_default.tmpl.
Available Templates
Template Description _base.tmplProtected base with citation rules normal_default.tmplDefault non-research synthesis (minimal structure) research_comprehensive.tmplDetailed research format with strong coverage requirements research_concise.tmplShorter research synthesis with lighter word requirements research_with_facts.tmplResearch format optimized for fact extraction metadata test_bullet_summary.tmplExample bullet-point summary template
Best Practices
Always extend _base.tmpl - Ensures citation contract is maintained
Use named templates for recurring formats
Use override for one-off customizations
Test templates with sample queries before production use
Template Directory Templates are located in config/templates/synthesis/. See README.md in that directory for template authoring guidelines.
Best Practices Summary
Generic infrastructure: Committed to open source
Vendor-specific code: Kept private in separate directories
Configuration overlays: Domain-specific settings isolated
Conditional imports: Graceful fallback for optional modules
Use stable interfaces (ToolRegistry, TemplateRegistry, etc.)
Avoid forking core subsystems
Keep customizations in separate directories
Use feature flags for experimental changes
Allowlist tools in templates
Enable approvals for dangerous operations
Use domain allowlisting for external APIs
Keep secrets in environment variables
Unit test vendor adapters in isolation
Integration test with Shannon services
Use replay testing for workflow determinism
Validate templates with registry.Finalize()
Extension Decision Tree
Next Steps