Go Agent SDK

Module: github.com/mirastacklabs-ai/mirastack-agents-sdk-go License: AGPL v3

The Go Agent SDK is the official library for building MIRASTACK agents in Go. It handles all the gRPC plumbing between your agent and the engine, so you focus entirely on your agent’s logic.


Installation

go get github.com/mirastacklabs-ai/mirastack-agents-sdk-go

Go 1.22 or later is required.


Core Concepts

The Plugin Interface

Every agent implements this interface:

type Plugin interface {
    Info()          PluginInfo
    Schema()        PluginSchema
    Execute(ctx context.Context, req ExecuteRequest) (ExecuteResponse, error)
    HealthCheck(ctx context.Context) error
    ConfigUpdated(ctx context.Context, config map[string]string) error
}

mirastack.Serve()

The entry point for every agent. Call it from main() with your plugin:

err := mirastack.Serve(plugin)

Serve blocks indefinitely. It starts the plugin’s gRPC server, reads MIRASTACK_ENGINE_ADDR from the environment, connects to the engine, self-registers via the RegisterPlugin RPC, and handles all incoming gRPC calls. On shutdown (SIGINT/SIGTERM), it deregisters from the engine before stopping.


Types Reference

PluginInfo

Returned by Info(). Tells the engine and the tool catalog who this agent is.

type PluginInfo struct {
    Name             string          // Unique identifier, snake_case, e.g. "query_vmetrics"
    Version          string          // Semantic version, e.g. "1.0.0"
    Description      string          // Plain-English description (shown in miractl agent list)
    Permission       Permission      // PermissionRead | PermissionModify | PermissionAdmin
    DevOpsStages     []string        // Which DevOps Infinity Loop stages this agent covers
    IntentPatterns   []IntentPattern // Optional: natural language patterns for intent routing
    PromptTemplates  []PromptTemplate // Optional: prompt templates contributed to the engine
}

DevOps stages you can declare:

Stage When to use it
observe Reading metrics, logs, traces, alerts
operate RCA, correlation, incident response
deploy Kubernetes operations, deployments, rollbacks
release Change events, version management
plan Capacity planning, SLO/SLA analysis, coverage
build Build system integrations
test Test coverage, flaky test detection
code Code analysis, dependency scanning

Permission

const (
    PermissionRead   Permission = "READ"   // Query only — no side effects
    PermissionModify Permission = "MODIFY" // Changes state — always requires approval
    PermissionAdmin  Permission = "ADMIN"  // Critical ops (delete, destroy) — always requires approval
)

PluginSchema

Returned by Schema(). Defines every action the agent exposes.

type PluginSchema struct {
    Actions []Action
}

type Action struct {
    Name        string                 // snake_case action name
    Description string                 // LLM-readable description (write for the LLM, not for humans)
    InputParams map[string]ParamSchema // Parameters this action accepts
    OutputParams map[string]ParamSchema // Optional: describe the output shape
    Permission  Permission             // Can override the plugin-level permission per action
    Intents     []string               // Optional: intent pattern hints for this specific action
}

type ParamSchema struct {
    Type        string   // "string" | "integer" | "boolean" | "number" | "array" | "object"
    Required    bool
    Description string
    Default     string   // Optional default value
    Enum        []string // Optional allowed values
}

ExecuteRequest

Received in Execute(). Contains everything your agent needs to do its work.

type ExecuteRequest struct {
    Params       map[string]string // Action parameters from the engine or workflow
    TimeRange    *TimeRange        // Parsed time range (nil for non-time queries)
    EngineClient EngineClient      // Lets you call back into the engine (cache, approval, log)
}

TimeRange

Always prefer TimeRange over raw time params. The engine has already parsed and normalised the user’s time expression for you.

type TimeRange struct {
    StartEpochMs       int64  // UTC epoch in milliseconds
    EndEpochMs         int64  // UTC epoch in milliseconds
    Timezone           string // IANA timezone name (display only)
    OriginalExpression string // What the user typed (e.g. "last 30 minutes")
}

ExecuteResponse

Returned from Execute().

type ExecuteResponse struct {
    Output map[string]string // Key-value results. Use "result" as the primary key with JSON value.
    Logs   []string          // Non-fatal messages, warnings, or error descriptions
}

Tip: Always marshal your result to JSON and put it in Output["result"]. This is the convention the engine and workflows expect.


EngineClient — Calling Back into the Engine

The EngineClient on ExecuteRequest lets your agent communicate with the engine during execution.

Cache

Store and retrieve transient data. Useful for caching slow backend responses.

// Read from cache
value, err := req.EngineClient.CacheGet(ctx, "my_agent:key")

// Write to cache with a TTL (seconds). 0 = no expiry.
err = req.EngineClient.CacheSet(ctx, "my_agent:key", jsonValue, 300)

Key naming convention: always prefix with your agent name to avoid collisions — "my_agent:service_list", "my_agent:metric_names".

Log Event

Write an audit or diagnostic event to the engine’s audit log:

err = req.EngineClient.LogEvent(ctx, mirastack.LogEntry{
    Event:   "my_agent.query_executed",
    Message: "Range query completed",
    Data: map[string]string{
        "action": "range_query",
        "rows":   "1250",
    },
})

Publish Result

Publish an intermediate result to the execution stream (useful for long-running, multi-step actions):

err = req.EngineClient.PublishResult(ctx, partialResultJSON)

datetimeutils Package

Import: github.com/mirastacklabs-ai/mirastack-agents-sdk-go/datetimeutils

Converts the TimeRange epoch millisecond values into what your backend actually expects. Never parse or format time yourself.

Function Output format Use with
FormatEpochSeconds(ms int64) string "1774973400" VictoriaMetrics, Prometheus
FormatEpochMillis(ms int64) string "1774973400000" Jaeger
FormatEpochMicros(ms int64) string "1774973400000000" VictoriaTraces
FormatRFC3339(ms int64) string "2026-04-08T00:00:00Z" VictoriaLogs, most REST APIs
FormatLookbackMillis(start, end int64) string Duration as ms string Window calculations
FormatInTimezone(ms int64, tz string) string Human-readable in given timezone Display purposes
NowUTCMs() int64 Current UTC epoch ms Default time range fallbacks

Pattern: time range with fallback

import dt "github.com/mirastacklabs-ai/mirastack-agents-sdk-go/datetimeutils"

func (p *Plugin) actionRangeQuery(ctx context.Context, params map[string]string, tr *mirastack.TimeRange) (mirastack.ExecuteResponse, error) {
    var start, end string
    if tr != nil && tr.StartEpochMs > 0 {
        start = dt.FormatEpochSeconds(tr.StartEpochMs)
        end   = dt.FormatEpochSeconds(tr.EndEpochMs)
    } else {
        // Fallback for direct API calls (e.g. miractl action invoke)
        start = params["start"]
        end   = params["end"]
    }
    // use start, end to call your backend
}

IntentPattern and PromptTemplate

These are optional but powerful. They let your agent contribute intelligence back into the engine.

IntentPattern

Register natural-language patterns that map to your agent’s actions. When a user types something that matches a pattern, the engine can route to your agent without needing an explicit workflow:

IntentPatterns: []mirastack.IntentPattern{
    {
        Pattern: "query metrics for *",
        Action:  "range_query",
    },
    {
        Pattern: "what are the error rates for *",
        Action:  "range_query",
    },
},

PromptTemplate

Contribute prompt templates to the engine’s template store. The engine uses these when it constructs LLM prompts for workflows that involve your agent:

PromptTemplates: []mirastack.PromptTemplate{
    {
        Name:    "metrics_analysis",
        
        Content: "You are analyzing metrics data for {{.service}}. The data shows: {{.metrics_output}}. Identify anomalies, trends, and potential root causes.",
        
    },
},

Error Handling Rules

  • Return processing errors via ExecuteResponse.Logs, not as a Go error return
  • Return a Go error only for fatal infrastructure failures (e.g., gRPC connection lost)
  • Never panic — recover gracefully
  • Never expose backend URLs, credentials, stack traces, or internal details in error messages
// Correct: non-fatal backend error
if err != nil {
    return mirastack.ExecuteResponse{
        Logs: []string{fmt.Sprintf("backend query failed: %v", err)},
    }, nil
}

// Correct: fatal infrastructure error (will cause engine to mark agent unhealthy)
if conn == nil {
    return mirastack.ExecuteResponse{}, fmt.Errorf("no backend connection available")
}

Complete Minimal Example

package main

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

    mirastack "github.com/mirastacklabs-ai/mirastack-agents-sdk-go"
    dt "github.com/mirastacklabs-ai/mirastack-agents-sdk-go/datetimeutils"
)

type Plugin struct{ backendURL string }

func (p *Plugin) Info() mirastack.PluginInfo {
    return mirastack.PluginInfo{
        Name: "my_agent", Version: "1.0.0",
        Description:  "A minimal example agent",
        Permission:   mirastack.PermissionRead,
        DevOpsStages: []string{"observe"},
    }
}

func (p *Plugin) Schema() mirastack.PluginSchema {
    
    return mirastack.PluginSchema{Actions: []mirastack.Action{{
        Name:        "get_status",
        Description: "Get the current status of a named service from My Backend.",
        InputParams: map[string]mirastack.ParamSchema{
            "service": {Type: "string", Required: true, Description: "Service name"},
        },
    }}}
    
}

func (p *Plugin) Execute(ctx context.Context, req mirastack.ExecuteRequest) (mirastack.ExecuteResponse, error) {
    if req.Params["action"] != "get_status" {
        return mirastack.ExecuteResponse{Logs: []string{fmt.Sprintf("unknown action: %s", req.Params["action"])}}, nil
    }
    result := map[string]string{"service": req.Params["service"], "status": "ok"}
    if req.TimeRange != nil {
        result["queried_at"] = dt.FormatRFC3339(req.TimeRange.EndEpochMs)
    }
    out, _ := json.Marshal(result)
    return mirastack.ExecuteResponse{Output: map[string]string{"result": string(out)}}, nil
}

func (p *Plugin) HealthCheck(ctx context.Context) error              { return nil }
func (p *Plugin) ConfigUpdated(_ context.Context, _ map[string]string) error { return nil }

func main() {
    // MIRASTACK_ENGINE_ADDR is read by the SDK automatically.
    if err := mirastack.Serve(&Plugin{backendURL: os.Getenv("MY_BACKEND_URL")}); err != nil {
        log.Fatal(err)
    }
}