Skip to content

Quick Start

Quick Start

This walkthrough takes you from zero to a governed tool call in three steps: install the SDK, initialise the runtime, and wrap your tools so every call is checked against the AI Agent Assembly gateway. The whole thing is a single main you can copy, paste, and run.

Agent registration is not reachable from a plain go get today — following this quick-start will not make your agent appear in the dashboard. The steps below wrap and govern tool calls, but the register handshake runs only under the opt-in native cgo binding (-tags aa_ffi_go, CGO_ENABLED=1), and that native library (libaa_ffi_go) is not published anywhere yet: building with -tags aa_ffi_go fails with ld: library 'aa_ffi_go' not found outside a full monorepo checkout. The default pure-Go build has no native transport, so it does not register even when WithSidecarAddress is set (see that option’s godoc). Publishing the native library — or dropping the cgo requirement — is a separate product decision; track status in AAASM-4547 and AAASM-4469.

Prerequisites

  • Go ≥ 1.26 (the floor declared in go.mod).

  • For local development: nothing else — Init auto-discovers a gateway on http://localhost:7391, and starts one for you if none is running (the aasm CLI must be on your PATH).

    Local-mode transports — :7391 REST + :50051 gRPC. Init shells out to the following command to auto-start the gateway:

    aasm start --mode local --foreground

    The :7391 auto-discovery above only resolves the REST gateway URL. Agent registration is a separate concern that talks to the gateway’s gRPC endpoint (default 127.0.0.1:50051) — Init does not auto-derive this address the way the Python and Node SDKs do. Reaching it requires an explicit WithSidecarAddress (or WithSidecarBinary) option. On the default pure-Go build Init returns ErrSidecarUnavailable with or without that option, because the build links no native transport and the fallback connector reaches no sidecar. And per the warning at the top of this page, the registration handshake itself only runs under the opt-in native cgo binding today.

    To confirm both surfaces are actually up rather than guessing from Init’s behavior, check them directly:

    curl http://localhost:7391/healthz   # REST — real JSON: mode, storage, version, uptime_secs
    nc -z localhost 50051 && echo "gRPC port open"   # gRPC has no health endpoint yet; this only confirms the port accepts connections
  • For production: a gateway URL and, if your gateway requires auth, an API key. Both can come from options, environment variables, or a config file — see Configuration.

  • (Optional) a C compiler, only if you opt into the native FFI transport with -tags aa_ffi_go. The default transport is pure-Go and needs none.

Step 1 — Install

go get github.com/ai-agent-assembly/go-sdk

Step 2 — Initialise the runtime

Init returns an *assembly.Assembly — your runtime handle. Always Close it when you’re done so the connection (and any managed sidecar) is released.

package main

import (
    "context"
    "errors"
    "log"

    "github.com/ai-agent-assembly/go-sdk/assembly"
)

func main() {
    // Stamp this agent's identity onto the context. The SDK stamps it onto
    // every check and record; the check is what reaches the gateway.
    ctx := assembly.WithAgentID(context.Background(), "my-agent")

    a, err := assembly.Init(ctx,
        assembly.WithGatewayURL("https://gateway.example.com"),
        assembly.WithAPIKey("..."), // optional — omit for local, unauthenticated dev
    )
    switch {
    case errors.Is(err, assembly.ErrSidecarUnavailable):
        // Expected on the default pure-Go build: it links no native transport,
        // so boot reaches no runtime. Step 3 shows what still applies.
        log.Println("init:", err)
    case err != nil:
        log.Fatalf("init assembly runtime: %v", err)
    default:
        defer func() {
            if err := a.Close(); err != nil {
                log.Printf("close assembly runtime: %v", err)
            }
        }()
    }

    log.Println("step 2 complete")
}

Run against a default go get install, this program reports ErrSidecarUnavailable and exits 0.

For local development you can drop both options entirely — assembly.Init(ctx) resolves the gateway from the environment, then ~/.aasm/config.yaml, then the local default. See Configuration for the full resolution order.

Step 3 — Wrap your tools

Your tools just need to satisfy the SDK’s small Tool interface:

type Tool interface {
    Name() string
    Description() string
    Call(ctx context.Context, input string) (string, error)
}

WrapTools takes your []Tool and a governance client, and returns a new []Tool where every Call is governed:

governed := assembly.WrapTools(myTools, nil)

The second argument is the GovernanceClient that talks to the gateway. Under the default fail-closed enforce posture, passing nil denies every wrapped call (ErrGovernanceUnavailable) rather than running it unchecked — pass assembly.WithFailClosed(false) for a true passthrough wrapper (the tools run, no Check/RecordResult calls) while you wire in a real client, ready to enforce policy (see Handle allow/deny decisions and errors).

Hand governed to your agent in place of the originals. From here on, each call against a governed tool is checked against the gateway policy before execution, and its outcome is offered to RecordResult after — the client this SDK ships writes that record to the runtime’s native event channel, which is a handoff and not an audit guarantee. The send is unacknowledged, and because the dispatch is never joined, a defer a.Close() immediately after a call loses the record every time (inspect Assembly.AuditSink() to tell which run you are in, AAASM-5750). Downstream of the handoff, AAASM-5783 is open on report_event payloads reaching neither the live stream nor the durable entry, so no SDK can claim ADR 0033 §6 Observed until it lands.

Govern your first agent

Pick your framework — each tab shows the governance slice copied verbatim from a runnable example in the examples repo (a CI drift check keeps them in lockstep). Go’s per-framework surface is thin today, so the tabs are LangChainGo (the framework path) and Plain (the framework-agnostic path). Two more validated Go examples already exist — Tool Policy and CLI Runtime (sidecar). Those are patterns (an allow/deny policy demo and sidecar wiring), not “first agent” frameworks. They’re intentionally left out of this quick-start; see metadata/quickstart/README.md for the tab-selection rationale. A new tab appears automatically once a new Go framework example lands.

Governance slice from the runnable go/langchaingo/main.go example.

// Wrap the LangChainGo tools with Agent Assembly governance. The wrapped
// values still satisfy langchaingo's tools.Tool, so they can be handed
// straight to a LangChainGo agent/executor.
governed := assembly.WrapTools(
	[]assembly.Tool{&searchTool{}, &sendEmailTool{}},
	&policyClient{},
)

Putting it together

package main

import (
    "context"
    "errors"
    "log"

    "github.com/ai-agent-assembly/go-sdk/assembly"
)

// echoTool is a minimal Tool implementation.
type echoTool struct{}

func (echoTool) Name() string        { return "echo" }
func (echoTool) Description() string { return "returns its input unchanged" }
func (echoTool) Call(_ context.Context, input string) (string, error) {
    return input, nil
}

// localPolicy is an in-process GovernanceClient, so this program runs with no
// gateway. Swap it for a gateway-backed client — the WrapTools call below and
// your tool code do not change.
type localPolicy struct{}

func (localPolicy) Check(_ context.Context, req assembly.CheckRequest) (assembly.Decision, error) {
    if req.ToolName != "echo" {
        return assembly.Decision{Denied: true, Reason: "only echo is allowed here"}, nil
    }
    return assembly.Decision{Reason: "allowed by the in-process stand-in"}, nil
}

func (localPolicy) WaitForApproval(_ context.Context, _ assembly.ApprovalRequest) (assembly.Decision, error) {
    return assembly.Decision{}, nil
}

func (localPolicy) RecordResult(_ context.Context, _ assembly.RecordRequest) error { return nil }

func (localPolicy) Close() error { return nil }

func main() {
    ctx := assembly.WithAgentID(context.Background(), "my-agent")

    a, err := assembly.Init(ctx,
        assembly.WithGatewayURL("https://gateway.example.com"),
        assembly.WithAPIKey("..."),
    )
    switch {
    case errors.Is(err, assembly.ErrSidecarUnavailable):
        log.Println("init:", err)
    case err != nil:
        log.Fatalf("init: %v", err)
    default:
        defer func() { _ = a.Close() }()
    }

    tools := []assembly.Tool{echoTool{}}
    governed := assembly.WrapTools(tools, localPolicy{})

    out, err := governed[0].Call(ctx, "hello, governance")
    if err != nil {
        log.Fatalf("tool call: %v", err)
    }
    log.Println("result:", out) // result: hello, governance
}

What to expect

  • Init reports the runtime unavailable on the default pure-Go build: it returns ErrSidecarUnavailable with or without WithSidecarAddress, since that build links no native transport. See Troubleshooting.
  • The governed call returns the inner tool’s result, because localPolicy evaluated it and allowed it.
  • A Decision{Denied: true} surfaces as a *assembly.PolicyViolationError and the tool body is Denied before execution.
  • Substituting nil for localPolicy leaves the call Denied before execution with ErrGovernanceUnavailable, under the default fail-closed enforce posture.

Where to next

  • Core Concepts — what’s actually happening inside the SDK.
  • Examples — wire the SDK into the framework you actually use.
  • Guides — wrap a real agent, integrate a framework, handle decisions.
  • Configuration — every Init option, defaults, and enforcement modes.
  • Troubleshooting — what to do when Init or a check fails.
Last updated on • Bryant