Plugin Development Guide¶
Overview¶
Hector's plugin system allows you to extend core functionality without modifying Hector's codebase. Write plugins in any language that supports gRPC (Go, Python, Rust, JavaScript, etc.) and integrate them seamlessly.
Plugin Architecture¶
Key Features¶
- Language Agnostic - Write in Go, Python, Rust, JavaScript, or any language with gRPC support
- Process Isolation - Plugins run in separate processes for stability and security
- gRPC Protocol - Industry-standard RPC framework for high performance
- Auto-Discovery - Plugins can be automatically discovered from configured paths
- Hot-Reloadable - Plugins can be updated without restarting Hector (future)
Plugin Types¶
Type | Purpose | Interface |
---|---|---|
llm_provider |
Custom LLM integrations | Generate text, streaming, tool calling |
database_provider |
Vector database backends | Store/search embeddings, collections |
embedder_provider |
Embedding generation | Convert text to vectors |
tool_provider |
Custom tools | Execute domain-specific operations |
reasoning_strategy |
Reasoning approaches | Custom agent reasoning patterns |
Why gRPC Only?¶
Hector uses gRPC exclusively for plugins (not Go's native plugin system) because:
- Cross-Language - Write plugins in any language
- Process Isolation - Plugin crashes don't affect Hector
- Production-Ready - Used by Terraform, Vault, Consul
- Cross-Platform - Works on Windows, macOS, Linux
- Version Independent - No Go version matching required
- Network Transparent - Plugins can run locally or remotely
Quick Start¶
Prerequisites¶
- Go 1.24+ (for building Hector)
- gRPC - Any language with gRPC support
- Protobuf - Protocol buffer compiler
- IDE - Your preferred development environment
┌──────────────────────────────────────────────────────────────┐
│ Hector Core │
│ ┌─────────────┬─────────────┬─────────────┐ │
│ │ Runtime │ Plugin │ Service │ │
│ │ System │ Manager │ Registry │ │
│ └──────┬──────┴──────┬──────┴──────┬──────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Plugin Types │ │
│ │ ┌─────────┬─────────┬─────────┬─────────┬─────────┐ │ │
│ │ │ LLM │ DB │Embedder │ Tool │Reasoning│ │ │
│ │ │Provider │Provider │Provider │Provider │Strategy │ │ │
│ │ └────┬────┴────┬────┴────┬────┴────┬────┴────┬────┘ │ │
│ │ │ │ │ │ │ │ │
│ │ ▼ ▼ ▼ ▼ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────┐│ │
│ │ │ Plugin Interface ││ │
│ │ │ ┌─────────────┬─────────────┐ ││ │
│ │ │ │ gRPC │ Protobuf │ ││ │
│ │ │ │ Interface │ Messages │ ││ │
│ │ │ └─────────────┴─────────────┘ ││ │
│ │ └─────────────────────────────────────────────────────┘│ │
│ └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
¶
┌──────────────────────────────────────────────────────────────┐
│ Hector Core │
│ ┌─────────────┬─────────────┬─────────────┐ │
│ │ Runtime │ Plugin │ Service │ │
│ │ System │ Manager │ Registry │ │
│ └──────┬──────┴──────┬──────┴──────┬──────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Plugin Types │ │
│ │ ┌─────────┬─────────┬─────────┬─────────┬─────────┐ │ │
│ │ │ LLM │ DB │Embedder │ Tool │Reasoning│ │ │
│ │ │Provider │Provider │Provider │Provider │Strategy │ │ │
│ │ └────┬────┴────┬────┴────┬────┴────┬────┴────┬────┘ │ │
│ │ │ │ │ │ │ │ │
│ │ ▼ ▼ ▼ ▼ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────┐│ │
│ │ │ Plugin Interface ││ │
│ │ │ ┌─────────────┬─────────────┐ ││ │
│ │ │ │ gRPC │ Protobuf │ ││ │
│ │ │ │ Interface │ Messages │ ││ │
│ │ │ └─────────────┴─────────────┘ ││ │
│ │ └─────────────────────────────────────────────────────┘│ │
│ └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
LLM Provider Plugin¶
Create custom language model integrations.
LLM Provider Interface¶
service LLMProvider {
rpc GenerateText(GenerateTextRequest) returns (GenerateTextResponse);
rpc StreamText(StreamTextRequest) returns (stream StreamTextResponse);
rpc CallTool(CallToolRequest) returns (CallToolResponse);
rpc ListModels(ListModelsRequest) returns (ListModelsResponse);
}
Go Implementation Example¶
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
pb "github.com/kadirpekel/hector/pkg/plugins/grpc/pb"
)
type CustomLLMProvider struct {
pb.UnimplementedLLMProviderServer
apiKey string
}
func (s *CustomLLMProvider) GenerateText(ctx context.Context, req *pb.GenerateTextRequest) (*pb.GenerateTextResponse, error) {
// Custom LLM API call
response, err := s.callCustomLLM(req.Prompt, req.Model)
if err != nil {
return nil, err
}
return &pb.GenerateTextResponse{
Text: response.Text,
Usage: &pb.TokenUsage{
PromptTokens: response.PromptTokens,
CompletionTokens: response.CompletionTokens,
TotalTokens: response.TotalTokens,
},
}, nil
}
func (s *CustomLLMProvider) StreamText(ctx context.Context, req *pb.StreamTextRequest) (*pb.StreamTextResponse, error) {
// Streaming implementation
stream := make(chan *pb.StreamTextResponse)
go func() {
defer close(stream)
// Stream tokens from custom LLM
for token := range s.streamCustomLLM(req.Prompt, req.Model) {
stream <- &pb.StreamTextResponse{
Text: token,
Done: false,
}
}
stream <- &pb.StreamTextResponse{
Text: "",
Done: true,
}
}()
return stream, nil
}
func main() {
lis, err := net.Listen("tcp", ":8081")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterLLMProviderServer(s, &CustomLLMProvider{
apiKey: os.Getenv("CUSTOM_LLM_API_KEY"),
})
log.Println("Custom LLM provider starting on :8081")
if err := s.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}
Python Implementation Example¶
import grpc
from concurrent import futures
import asyncio
from hector_plugins_pb2 import *
from hector_plugins_pb2_grpc import *
class CustomLLMProvider(LLMProviderServicer):
def __init__(self, api_key: str):
self.api_key = api_key
async def GenerateText(self, request, context):
# Custom LLM API call
response = await self.call_custom_llm(request.prompt, request.model)
return GenerateTextResponse(
text=response.text,
usage=TokenUsage(
prompt_tokens=response.prompt_tokens,
completion_tokens=response.completion_tokens,
total_tokens=response.total_tokens
)
)
async def StreamText(self, request, context):
# Streaming implementation
async for token in self.stream_custom_llm(request.prompt, request.model):
yield StreamTextResponse(
text=token,
done=False
)
yield StreamTextResponse(
text="",
done=True
)
async def serve():
server = grpc.aio.server(futures.ThreadPoolExecutor(max_workers=10))
add_LLMProviderServicer_to_server(CustomLLMProvider(os.getenv("CUSTOM_LLM_API_KEY")), server)
listen_addr = '[::]:8081'
server.add_insecure_port(listen_addr)
print(f"Custom LLM provider starting on {listen_addr}")
await server.start()
await server.wait_for_termination()
if __name__ == '__main__':
asyncio.run(serve())
Configuration¶
plugins:
llm_providers:
custom_llm:
type: "grpc"
path: "./plugins/custom-llm-plugin"
config:
api_key: "${CUSTOM_LLM_API_KEY}"
endpoint: "http://localhost:8081"
timeout: "30s"
llms:
custom:
type: "plugin:custom_llm"
model: "custom-model-v1"
temperature: 0.7
max_tokens: 4000
Database Provider Plugin¶
Create custom vector database integrations.
Database Provider Interface¶
service DatabaseProvider {
rpc CreateCollection(CreateCollectionRequest) returns (CreateCollectionResponse);
rpc DeleteCollection(DeleteCollectionRequest) returns (DeleteCollectionResponse);
rpc UpsertVectors(UpsertVectorsRequest) returns (UpsertVectorsResponse);
rpc SearchVectors(SearchVectorsRequest) returns (SearchVectorsResponse);
rpc DeleteVectors(DeleteVectorsRequest) returns (DeleteVectorsResponse);
}
Go Implementation Example¶
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
pb "github.com/kadirpekel/hector/pkg/plugins/grpc/pb"
)
type CustomDatabaseProvider struct {
pb.UnimplementedDatabaseProviderServer
client *CustomDBClient
}
func (s *CustomDatabaseProvider) CreateCollection(ctx context.Context, req *pb.CreateCollectionRequest) (*pb.CreateCollectionResponse, error) {
err := s.client.CreateCollection(req.Name, req.Dimension)
if err != nil {
return &pb.CreateCollectionResponse{
Success: false,
Error: err.Error(),
}, nil
}
return &pb.CreateCollectionResponse{
Success: true,
}, nil
}
func (s *CustomDatabaseProvider) UpsertVectors(ctx context.Context, req *pb.UpsertVectorsRequest) (*pb.UpsertVectorsResponse, error) {
vectors := make([]*CustomVector, len(req.Vectors))
for i, v := range req.Vectors {
vectors[i] = &CustomVector{
ID: v.Id,
Vector: v.Vector,
Metadata: v.Metadata,
}
}
err := s.client.UpsertVectors(req.Collection, vectors)
if err != nil {
return &pb.UpsertVectorsResponse{
Success: false,
Error: err.Error(),
}, nil
}
return &pb.UpsertVectorsResponse{
Success: true,
}, nil
}
func (s *CustomDatabaseProvider) SearchVectors(ctx context.Context, req *pb.SearchVectorsRequest) (*pb.SearchVectorsResponse, error) {
results, err := s.client.SearchVectors(req.Collection, req.Vector, req.Limit, req.Threshold)
if err != nil {
return &pb.SearchVectorsResponse{
Success: false,
Error: err.Error(),
}, nil
}
pbResults := make([]*pb.VectorResult, len(results))
for i, r := range results {
pbResults[i] = &pb.VectorResult{
Id: r.ID,
Score: r.Score,
Metadata: r.Metadata,
}
}
return &pb.SearchVectorsResponse{
Success: true,
Results: pbResults,
}, nil
}
func main() {
lis, err := net.Listen("tcp", ":8082")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterDatabaseProviderServer(s, &CustomDatabaseProvider{
client: NewCustomDBClient(os.Getenv("CUSTOM_DB_URL")),
})
log.Println("Custom database provider starting on :8082")
if err := s.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}
Configuration¶
plugins:
database_providers:
custom_db:
type: "grpc"
path: "./plugins/custom-db-plugin"
config:
url: "${CUSTOM_DB_URL}"
api_key: "${CUSTOM_DB_API_KEY}"
timeout: "30s"
databases:
custom:
type: "plugin:custom_db"
config:
collection: "my_collection"
dimension: 768
Embedder Provider Plugin¶
Create custom embedding generation services.
Embedder Provider Interface¶
service EmbedderProvider {
rpc EmbedText(EmbedTextRequest) returns (EmbedTextResponse);
rpc EmbedBatch(EmbedBatchRequest) returns (EmbedBatchResponse);
rpc GetDimensions(GetDimensionsRequest) returns (GetDimensionsResponse);
}
Go Implementation Example¶
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
pb "github.com/kadirpekel/hector/pkg/plugins/grpc/pb"
)
type CustomEmbedderProvider struct {
pb.UnimplementedEmbedderProviderServer
client *CustomEmbedderClient
}
func (s *CustomEmbedderProvider) EmbedText(ctx context.Context, req *pb.EmbedTextRequest) (*pb.EmbedTextResponse, error) {
embedding, err := s.client.EmbedText(req.Text)
if err != nil {
return &pb.EmbedTextResponse{
Success: false,
Error: err.Error(),
}, nil
}
return &pb.EmbedTextResponse{
Success: true,
Vector: embedding,
}, nil
}
func (s *CustomEmbedderProvider) EmbedBatch(ctx context.Context, req *pb.EmbedBatchRequest) (*pb.EmbedBatchResponse, error) {
embeddings, err := s.client.EmbedBatch(req.Texts)
if err != nil {
return &pb.EmbedBatchResponse{
Success: false,
Error: err.Error(),
}, nil
}
pbEmbeddings := make([]*pb.Embedding, len(embeddings))
for i, e := range embeddings {
pbEmbeddings[i] = &pb.Embedding{
Vector: e.Vector,
}
}
return &pb.EmbedBatchResponse{
Success: true,
Embeddings: pbEmbeddings,
}, nil
}
func main() {
lis, err := net.Listen("tcp", ":8083")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterEmbedderProviderServer(s, &CustomEmbedderProvider{
client: NewCustomEmbedderClient(os.Getenv("CUSTOM_EMBEDDER_URL")),
})
log.Println("Custom embedder provider starting on :8083")
if err := s.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}
Configuration¶
plugins:
embedder_providers:
custom_embedder:
type: "grpc"
path: "./plugins/custom-embedder-plugin"
config:
url: "${CUSTOM_EMBEDDER_URL}"
api_key: "${CUSTOM_EMBEDDER_API_KEY}"
timeout: "30s"
embedders:
custom:
type: "plugin:custom_embedder"
config:
model: "custom-embedding-model"
dimension: 768
Tool Provider Plugin¶
Create custom tools for domain-specific operations.
Tool Provider Interface¶
service ToolProvider {
rpc ListTools(ListToolsRequest) returns (ListToolsResponse);
rpc ExecuteTool(ExecuteToolRequest) returns (ExecuteToolResponse);
rpc GetToolSchema(GetToolSchemaRequest) returns (GetToolSchemaResponse);
}
Go Implementation Example¶
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
pb "github.com/kadirpekel/hector/pkg/plugins/grpc/pb"
)
type CustomToolProvider struct {
pb.UnimplementedToolProviderServer
}
func (s *CustomToolProvider) ListTools(ctx context.Context, req *pb.ListToolsRequest) (*pb.ListToolsResponse, error) {
tools := []*pb.Tool{
{
Name: "custom_api_call",
Description: "Make API calls to external services",
Parameters: map[string]interface{}{
"url": "string",
"method": "string",
"headers": "object",
"body": "string",
},
},
{
Name: "custom_data_processing",
Description: "Process data using custom algorithms",
Parameters: map[string]interface{}{
"data": "array",
"algorithm": "string",
"options": "object",
},
},
}
return &pb.ListToolsResponse{
Tools: tools,
}, nil
}
func (s *CustomToolProvider) ExecuteTool(ctx context.Context, req *pb.ExecuteToolRequest) (*pb.ExecuteToolResponse, error) {
switch req.Tool {
case "custom_api_call":
result, err := s.executeAPICall(req.Parameters)
if err != nil {
return &pb.ExecuteToolResponse{
Success: false,
Error: err.Error(),
}, nil
}
return &pb.ExecuteToolResponse{
Success: true,
Result: result,
}, nil
case "custom_data_processing":
result, err := s.processData(req.Parameters)
if err != nil {
return &pb.ExecuteToolResponse{
Success: false,
Error: err.Error(),
}, nil
}
return &pb.ExecuteToolResponse{
Success: true,
Result: result,
}, nil
default:
return &pb.ExecuteToolResponse{
Success: false,
Error: "Unknown tool: " + req.Tool,
}, nil
}
}
func main() {
lis, err := net.Listen("tcp", ":8084")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterToolProviderServer(s, &CustomToolProvider{})
log.Println("Custom tool provider starting on :8084")
if err := s.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}
Configuration¶
plugins:
tool_providers:
custom_tools:
type: "grpc"
path: "./plugins/custom-tools-plugin"
config:
api_key: "${CUSTOM_TOOLS_API_KEY}"
timeout: "30s"
tools:
custom_api_call:
type: "plugin:custom_tools"
config:
tool_name: "custom_api_call"
custom_data_processing:
type: "plugin:custom_tools"
config:
tool_name: "custom_data_processing"
Reasoning Strategy Plugin¶
Create custom reasoning strategies for agent decision-making.
Reasoning Strategy Interface¶
service ReasoningStrategy {
rpc Initialize(InitializeRequest) returns (InitializeResponse);
rpc ProcessIteration(ProcessIterationRequest) returns (ProcessIterationResponse);
rpc Finalize(FinalizeRequest) returns (FinalizeResponse);
}
Go Implementation Example¶
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
pb "github.com/kadirpekel/hector/pkg/plugins/grpc/pb"
)
type CustomReasoningStrategy struct {
pb.UnimplementedReasoningStrategyServer
}
func (s *CustomReasoningStrategy) Initialize(ctx context.Context, req *pb.InitializeRequest) (*pb.InitializeResponse, error) {
// Initialize custom reasoning strategy
state := &CustomReasoningState{
MaxIterations: req.MaxIterations,
Temperature: req.Temperature,
Context: req.Context,
}
return &pb.InitializeResponse{
Success: true,
State: state,
}, nil
}
func (s *CustomReasoningStrategy) ProcessIteration(ctx context.Context, req *pb.ProcessIterationRequest) (*pb.ProcessIterationResponse, error) {
// Process single reasoning iteration
result, err := s.processIteration(req.State, req.Input)
if err != nil {
return &pb.ProcessIterationResponse{
Success: false,
Error: err.Error(),
}, nil
}
return &pb.ProcessIterationResponse{
Success: true,
Result: result,
Done: result.Done,
State: result.State,
}, nil
}
func (s *CustomReasoningStrategy) Finalize(ctx context.Context, req *pb.FinalizeRequest) (*pb.FinalizeResponse, error) {
// Finalize reasoning process
finalResult, err := s.finalize(req.State)
if err != nil {
return &pb.FinalizeResponse{
Success: false,
Error: err.Error(),
}, nil
}
return &pb.FinalizeResponse{
Success: true,
Result: finalResult,
}, nil
}
func main() {
lis, err := net.Listen("tcp", ":8085")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterReasoningStrategyServer(s, &CustomReasoningStrategy{})
log.Println("Custom reasoning strategy starting on :8085")
if err := s.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}
Configuration¶
plugins:
reasoning_strategies:
custom_reasoning:
type: "grpc"
path: "./plugins/custom-reasoning-plugin"
config:
max_iterations: 20
temperature: 0.7
agents:
my_agent:
name: "My Agent"
llm: "gpt-4o"
reasoning:
engine: "plugin:custom_reasoning"
max_iterations: 20
enable_streaming: true
Plugin Development Workflow¶
1. Setup Development Environment¶
# Clone Hector repository
git clone https://github.com/kadirpekel/hector.git
cd hector
# Install dependencies
go mod download
# Generate protobuf files
make generate-proto
2. Create Plugin Project¶
# Create plugin directory
mkdir my-plugin
cd my-plugin
# Initialize Go module
go mod init my-plugin
# Add Hector protobuf dependency
go get github.com/kadirpekel/hector/pkg/plugins/grpc/pb
3. Implement Plugin Interface¶
// main.go
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
pb "github.com/kadirpekel/hector/pkg/plugins/grpc/pb"
)
type MyPlugin struct {
pb.UnimplementedLLMProviderServer
}
func (s *MyPlugin) GenerateText(ctx context.Context, req *pb.GenerateTextRequest) (*pb.GenerateTextResponse, error) {
// Implement your custom logic
return &pb.GenerateTextResponse{
Text: "Custom response",
}, nil
}
func main() {
lis, err := net.Listen("tcp", ":8081")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterLLMProviderServer(s, &MyPlugin{})
log.Println("My plugin starting on :8081")
if err := s.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}
4. Build and Test Plugin¶
# Build plugin
go build -o my-plugin main.go
# Test plugin
./my-plugin
5. Configure Hector¶
plugins:
llm_providers:
my_plugin:
type: "grpc"
path: "./my-plugin"
config:
api_key: "${MY_PLUGIN_API_KEY}"
llms:
custom:
type: "plugin:my_plugin"
model: "custom-model"
6. Test Integration¶
# Start Hector with plugin
hector serve --config config.yaml
# Test plugin functionality
hector call my_agent "Hello" --llm custom
Plugin Testing¶
Unit Testing¶
package main
import (
"context"
"testing"
pb "github.com/kadirpekel/hector/pkg/plugins/grpc/pb"
)
func TestMyPlugin(t *testing.T) {
plugin := &MyPlugin{}
req := &pb.GenerateTextRequest{
Prompt: "Hello, world!",
Model: "custom-model",
}
resp, err := plugin.GenerateText(context.Background(), req)
if err != nil {
t.Fatalf("GenerateText failed: %v", err)
}
if resp.Text == "" {
t.Error("Expected non-empty response")
}
}
Integration Testing¶
# test-config.yaml
plugins:
llm_providers:
test_plugin:
type: "grpc"
path: "./test-plugin"
config:
api_key: "test-key"
llms:
test:
type: "plugin:test_plugin"
model: "test-model"
agents:
test_agent:
name: "Test Agent"
llm: "test"
# Run integration tests
hector serve --config test-config.yaml &
sleep 5
hector call test_agent "Test message"
Plugin Packaging¶
Docker Packaging¶
FROM golang:1.24-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o my-plugin main.go
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/my-plugin .
CMD ["./my-plugin"]
Distribution¶
# Build for multiple platforms
GOOS=linux GOARCH=amd64 go build -o my-plugin-linux main.go
GOOS=windows GOARCH=amd64 go build -o my-plugin-windows.exe main.go
GOOS=darwin GOARCH=amd64 go build -o my-plugin-macos main.go
# Create distribution package
tar -czf my-plugin-v1.0.0.tar.gz my-plugin-*
Plugin Security¶
Security Best Practices¶
- Input Validation - Validate all inputs
- Authentication - Implement proper authentication
- Timeouts - Set appropriate timeouts
- Sandboxing - Run in isolated environments
- Logging - Log security events
Security Configuration¶
plugins:
my_plugin:
type: "grpc"
path: "./my-plugin"
# Security settings
security:
sandbox: true
timeout: "30s"
max_memory: "1GB"
allowed_networks: ["localhost"]
config:
api_key: "${MY_PLUGIN_API_KEY}"