Skip to content

Go SDK Guide

Active Development

The SDK is under active development. Pin to a specific release for production use.

The Go SDK lets you build high-performance Aether agents using Go's idiomatic interfaces, goroutines, and channels. It communicates with the Hub over gRPC.

Installation

bash
go get github.com/amansrivastava/hacky-automation/sdks/go

Requires: Go 1.21+

Configuration

Environment Variables

bash
AETHER_HUB_ADDRESS=localhost:50051
AETHER_AGENT_ID=my-agent-001
AETHER_AGENT_NAME="My Agent"
AETHER_AGENT_SECRET=sk-secret

Config Reference

FieldTypeDefaultDescription
AgentIDstringRequiredUnique identifier (e.g. pm_sarah)
AgentNamestringRequiredHuman-readable name
Versionstring"0.1.0"Agent version
HubAddressstring"localhost:50051"Hub gRPC endpoint
Secretstring""Authentication secret
Capabilitiesmap[string]string{}Key-value capability flags
Metadatamap[string]string{}Arbitrary key-value metadata
HeartbeatIntervaltime.Duration30sTime between heartbeats
ReconnectDelaytime.Duration5sDelay before reconnect attempt
MaxReconnectAttemptsint-1Max attempts (-1 = infinite)
go
import sdk "github.com/amansrivastava/hacky-automation/sdks/go"
import "os"
import "time"

config := sdk.NewConfig("my-agent-001", "My Agent")
config.HubAddress = "hub.example.com:50051"
config.Secret = os.Getenv("AGENT_SECRET")
config.Capabilities["task_types"] = "analysis"
config.HeartbeatInterval = 60 * time.Second
config.MaxReconnectAttempts = 10

Quickstart

Create Your Agent

Implement the TaskHandler interface and wire it with sdk.NewAgent:

go
package main

import (
    "context"
    "fmt"
    "log"

    sdk "github.com/amansrivastava/hacky-automation/sdks/go"
)

// MyAgent implements sdk.TaskHandler
type MyAgent struct {
    hub *sdk.Hub
}

func (a *MyAgent) HandleTask(ctx context.Context, task *sdk.Task) (*sdk.TaskResult, error) {
    log.Printf("Processing task %s: %s", task.TaskID, task.Description)

    result := fmt.Sprintf("Processed: %s", task.Description)
    return &sdk.TaskResult{
        TaskID:  task.TaskID,
        Success: true,
        Result:  []byte(result),
        Metrics: map[string]string{"processing_time": "0.5s"},
    }, nil
}

func main() {
    config := sdk.NewConfig("my-agent-001", "My Example Agent")
    config.HubAddress = "localhost:50051"
    config.Capabilities = map[string]string{
        "task_types":     "analysis,processing",
        "max_concurrent": "5",
    }

    handler := &MyAgent{}
    agent, err := sdk.NewAgent(config, handler)
    if err != nil {
        log.Fatalf("Failed to create agent: %v", err)
    }

    handler.hub = sdk.NewHub(agent.Hub())

    ctx := context.Background()
    if err := agent.Run(ctx); err != nil {
        log.Fatalf("Agent error: %v", err)
    }
}

Start, Stop, Run

go
// Start: connect + register + begin heartbeat/task loops (non-blocking)
err = agent.Start(ctx)

// Stop: unregister + close loops + disconnect
err = agent.Stop(ctx)

// Run: Start + block until ctx is cancelled or signal received
err = agent.Run(ctx)

Hub Services

Access Hub-proxied services through a *sdk.Hub created from the agent's client:

go
hub := sdk.NewHub(agent.Hub())

Pass hub to your handler (e.g. in a struct field) before calling Run.

LLM

go
response, err := hub.LLM().Chat(ctx, sdk.ChatRequest{
    Messages: []sdk.Message{
        {Role: "system", Content: "You are a helpful assistant."},
        {Role: "user", Content: string(task.Payload)},
    },
    Model:       "gpt-4o-mini",  // Hub may override
    Temperature: 0.3,
    MaxTokens:   1000,
})
if err != nil {
    return nil, fmt.Errorf("LLM call failed: %w", err)
}
fmt.Println(response.Content, response.Model)

Tools

go
// Execute a tool
result, err := hub.Tools().Execute(ctx, sdk.ExecuteRequest{
    ToolID: "add_comment",
    Input:  map[string]interface{}{"issue_id": "123", "comment": "Done"},
})

Memory

go
// Store
err = hub.Memory().Store(ctx, sdk.StoreRequest{
    Key:   "my-config",
    Value: []byte(`{"theme":"dark"}`),
    Type:  "agent",  // "short_term" | "long_term" | "agent" | "task"
})

// Retrieve
resp, err := hub.Memory().Retrieve(ctx, sdk.RetrieveRequest{
    Key:  "my-config",
    Type: "agent",
})
fmt.Println(string(resp.Value))

Messaging

go
// Publish
err = hub.Messaging().Send(ctx, sdk.SendRequest{
    To:      "workflow.status",
    Message: []byte(`{"status":"processing"}`),
})

// Request-reply
resp, err := hub.Messaging().Request(ctx, sdk.RequestMsg{
    To:      "workflow.status",
    Message: []byte(`{"workflow_id":"123"}`),
    Timeout: 10,
})

Example: Code Reviewer Agent

go
package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "os/signal"
    "syscall"

    sdk "github.com/amansrivastava/hacky-automation/sdks/go"
)

type CodeReviewer struct {
    hub *sdk.Hub
}

func (a *CodeReviewer) HandleTask(ctx context.Context, task *sdk.Task) (*sdk.TaskResult, error) {
    log.Printf("Code review task: %s", task.Description)

    response, err := a.hub.LLM().Chat(ctx, sdk.ChatRequest{
        Messages: []sdk.Message{
            {Role: "system", Content: "You are an expert code reviewer. Be concise."},
            {Role: "user", Content: fmt.Sprintf("Review this diff:\n\n%s", string(task.Payload))},
        },
        Temperature: 0.2,
    })
    if err != nil {
        return nil, fmt.Errorf("LLM call failed: %w", err)
    }

    return &sdk.TaskResult{
        TaskID:  task.TaskID,
        Success: true,
        Result:  []byte(response.Content),
        Metrics: map[string]string{
            "review_type": "automated",
            "model":       response.Model,
        },
    }, nil
}

func main() {
    config := sdk.NewConfig("dev_go_reviewer", "Go Code Reviewer")
    config.HubAddress = getEnv("HUB_ADDRESS", "localhost:50051")
    config.Secret = getEnv("AGENT_SECRET", "")
    config.Capabilities = map[string]string{
        "code_review": "true",
        "go":          "true",
    }

    handler := &CodeReviewer{}
    agent, err := sdk.NewAgent(config, handler)
    if err != nil {
        log.Fatalf("Failed to create agent: %v", err)
    }
    handler.hub = sdk.NewHub(agent.Hub())

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    sigs := make(chan os.Signal, 1)
    signal.Notify(sigs, os.Interrupt, syscall.SIGTERM)
    go func() { <-sigs; cancel() }()

    if err := agent.Run(ctx); err != nil {
        log.Fatalf("Agent error: %v", err)
    }
}

func getEnv(key, fallback string) string {
    if v := os.Getenv(key); v != "" {
        return v
    }
    return fallback
}

Example: Custom Data Processor

go
package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "os"

    sdk "github.com/amansrivastava/hacky-automation/sdks/go"
)

type DataProcessor struct {
    hub *sdk.Hub
}

func (a *DataProcessor) HandleTask(ctx context.Context, task *sdk.Task) (*sdk.TaskResult, error) {
    response, err := a.hub.LLM().Chat(ctx, sdk.ChatRequest{
        Messages: []sdk.Message{
            {Role: "system", Content: "Classify and summarize this data as JSON."},
            {Role: "user", Content: string(task.Payload)},
        },
        Temperature: 0.0,
    })
    if err != nil {
        return nil, fmt.Errorf("LLM failed: %w", err)
    }

    // Persist result
    value, _ := json.Marshal(map[string]string{"result": response.Content})
    if err := a.hub.Memory().Store(ctx, sdk.StoreRequest{
        Key:   fmt.Sprintf("analysis-%s", task.TaskID),
        Value: value,
        Type:  "task",
    }); err != nil {
        log.Printf("Memory store warning: %v", err)
    }

    return &sdk.TaskResult{
        TaskID:  task.TaskID,
        Success: true,
        Result:  []byte(response.Content),
    }, nil
}

func main() {
    config := sdk.NewConfig("data_processor", "Data Processor")
    config.HubAddress = os.Getenv("HUB_ADDRESS")
    if config.HubAddress == "" {
        config.HubAddress = "localhost:50051"
    }

    processor := &DataProcessor{}
    agent, err := sdk.NewAgent(config, processor)
    if err != nil {
        log.Fatalf("Failed to create agent: %v", err)
    }
    processor.hub = sdk.NewHub(agent.Hub())

    if err := agent.Run(context.Background()); err != nil {
        log.Fatalf("Agent error: %v", err)
    }
}

Error Handling

The SDK exposes typed errors for structured handling:

go
if sdkErr, ok := err.(*sdk.SDKError); ok {
    switch sdkErr.Code {
    case sdk.ErrCodeNotConnected:
        // Not connected to Hub
    case sdk.ErrCodeRegistration:
        // Registration failure
    case sdk.ErrCodeMaxReconnectAttempts:
        // Gave up reconnecting
    }
}

Error Codes

CodeMeaning
INVALID_CONFIGMissing required field in config
NOT_CONNECTEDgRPC connection not established
NOT_REGISTEREDAgent not registered with Hub
ALREADY_RUNNINGRun/Start called while already running
REGISTRATION_FAILEDHub rejected the registration
CONNECTION_FAILEDCould not establish gRPC connection
TASK_PROCESSING_FAILEDHandleTask returned an error
MAX_RECONNECT_ATTEMPTSReconnect limit reached

Deployment

Binary

bash
cd sdks/go
make example   # Build example binary
HUB_ADDRESS=localhost:50051 AGENT_SECRET=sk-xxx ./examples/code-reviewer/code-reviewer

Docker

dockerfile
FROM golang:1.21-alpine AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o agent ./cmd/agent

FROM alpine:3.18
COPY --from=builder /build/agent /agent
CMD ["/agent"]
bash
docker run -e HUB_ADDRESS=hub:50051 -e AGENT_SECRET=sk-xxx my-go-agent

Docker Compose (with Aether)

yaml
services:
  my-agent:
    build: ./my-agent
    environment:
      HUB_ADDRESS: goway:9090
      AGENT_SECRET: ${AGENT_SECRET}
    depends_on:
      - goway
    restart: unless-stopped

Kubernetes

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-go-agent
spec:
  replicas: 2   # Go agents can scale horizontally
  selector:
    matchLabels:
      app: my-go-agent
  template:
    metadata:
      labels:
        app: my-go-agent
    spec:
      containers:
        - name: agent
          image: my-go-agent:latest
          env:
            - name: HUB_ADDRESS
              value: "aether-hub:9090"
            - name: AGENT_SECRET
              valueFrom:
                secretKeyRef:
                  name: aether-secrets
                  key: agent-secret

API Reference

TaskHandler Interface

go
type TaskHandler interface {
    HandleTask(ctx context.Context, task *Task) (*TaskResult, error)
}

Implement this interface to define your agent's task logic.

Agent Interface

go
type Agent interface {
    TaskHandler
    Start(ctx context.Context) error
    Stop(ctx context.Context) error
    Run(ctx context.Context) error
    Hub() *Client
}

NewAgent

go
agent, err := sdk.NewAgent(config *Config, handler TaskHandler) (Agent, error)

Hub Services

MethodDescription
hub.LLM()LLM service proxy
hub.Tools()Tools service proxy
hub.Memory()Memory service proxy
hub.Messaging()Messaging service proxy

Client (low-level)

MethodDescription
Connect(ctx)Establish gRPC connection
Disconnect(ctx)Close connection
Register(ctx)Register with Hub
Unregister(ctx)Unregister from Hub
SendHeartbeat(ctx)Send heartbeat
StreamTasks(ctx)Open bidirectional task stream
SendTaskResult(ctx, result)Submit task result

Development

bash
cd sdks/go

make deps        # Download dependencies
make test        # Run all tests
make test-cov    # Tests with coverage
make fmt         # gofmt
make lint        # golangci-lint
make example     # Build example binary
make run-example # Run the code-reviewer example

Troubleshooting

connection refused on startup

Check that the Hub gRPC port (default 9090) is reachable and the HubAddress in config is correct.

rpc error: code = Unauthenticated

The Secret in your config does not match the agent record in the Hub database.

Agent stops unexpectedly after N reconnect attempts

MaxReconnectAttempts defaults to -1 (infinite). If you set it to a positive number, the agent will exit after that many failed reconnects. Set to -1 for production services.

gRPC stubs are outdated

Proto definitions live in goway/proto/. Run make proto-gen from the goway directory to regenerate stubs, then update the SDK.

Released under the MIT License.