Aiden chat from a Go backend
Use a server-side SDK client for a Grafana plugin or another chat application.
Keep API tokens in your application’s encrypted secret configuration. This journey
is Go-first today: start a turn with StartAgentChat, stream events with
SubscribeExecution, and stop a session with TerminateSession.
The Go module is invite-only while the SDK repository is private. Ask StackGen
for GitHub access before go get (Python and TypeScript stay on public PyPI / npm).
| Step | What the SDK does |
|---|---|
| 1. Start | POST …/agents/{name}/chat/start returns trace_id and session_id |
| 2. Stream | GET …/executions/{trace_id}/subscribe until the turn settles |
| 3. Stop | On failure, TerminateSession asks Aiden to cancel in-flight work |
How to run
export STACKGEN_URL=https://app.stackgen.com
export STACKGEN_TOKEN=stackgen_…
export STACKGEN_WORKSPACE_ID=<workspace-uuid>
go run ./examples/go/aiden-chat/agent-chat.go \
--agent grafana-assistant \
--message 'List the available data sources without changing anything.'export STACKGEN_URL=https://app.stackgen.com
export STACKGEN_TOKEN=stackgen_…
export STACKGEN_WORKSPACE_ID=<workspace-uuid>
pip install stackgen-sdk
# No packaged CLI for this journey yet. Paste the Minimal snippet below into a script.export STACKGEN_URL=https://app.stackgen.com
export STACKGEN_TOKEN=stackgen_…
export STACKGEN_WORKSPACE_ID=<workspace-uuid>
npm install stackgen-sdk tsx
# No packaged CLI for this journey yet. Paste the Minimal snippet below into a script.Minimal
client, err := stackgen.NewClient(stackgen.Config{
BaseURL: "https://app.stackgen.com",
APIToken: os.Getenv("STACKGEN_TOKEN"),
WorkspaceID: os.Getenv("STACKGEN_WORKSPACE_ID"),
})
if err != nil { return err }
ctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
defer cancel()
run, err := client.Aiden.StartAgentChat(ctx, "grafana-assistant", aiden.StartChatRequest{
Message: "List the available data sources without changing anything.",
// SessionID: previousSessionID, // continue a conversation owned by this user
})
if err != nil { return err }
err = client.Aiden.SubscribeExecution(ctx, run.TraceID, func(event aiden.ExecutionEvent) error {
if !event.Subagent && event.Type == "TEXT_MESSAGE_CONTENT" {
fmt.Print(event.Text("Content", "content"))
}
return nil
})
if err != nil {
cleanup, done := context.WithTimeout(context.Background(), 20*time.Second)
defer done()
_ = client.Aiden.TerminateSession(cleanup, run.SessionID)
}
return errimport os
from stackgen import StackgenClient, StackgenConfig
client = StackgenClient(
StackgenConfig(
base_url="https://app.stackgen.com",
api_token=os.environ["STACKGEN_TOKEN"],
workspace_id=os.environ["STACKGEN_WORKSPACE_ID"],
)
)
run = client.aiden.start_agent_chat(
"grafana-assistant",
{
"message": "List the available data sources without changing anything.",
# "session_id": previous_session_id, # continue a conversation owned by this user
},
)
done = client.aiden.wait_for_subscribe_complete(run["trace_id"])
if not done.ok:
client.aiden.terminate_session(run["session_id"], reason="chat turn failed")
raise SystemExit("chat turn did not finish")
print(run["session_id"])import { StackgenClient, StackgenConfig } from "stackgen-sdk";
const client = new StackgenClient(
new StackgenConfig({
baseUrl: "https://app.stackgen.com",
apiToken: process.env.STACKGEN_TOKEN!,
workspaceId: process.env.STACKGEN_WORKSPACE_ID!,
}),
);
const run = await client.aiden.startAgentChat("grafana-assistant", {
message: "List the available data sources without changing anything.",
// session_id: previousSessionId, // continue a conversation owned by this user
});
const done = await client.aiden.waitForSubscribeComplete(String(run.trace_id));
if (!done.ok) {
await client.aiden.terminateSession(String(run.session_id), { reason: "chat turn failed" });
throw new Error("chat turn did not finish");
}
console.log(run.session_id);Python and TypeScript wait for the turn to finish. Streaming text deltas to a
browser is Go-only today (SubscribeExecution below).
Imports: context, fmt, os, time,
github.com/appcd-dev/stackgen-sdk/clients/go, and
github.com/appcd-dev/stackgen-sdk/clients/go/aiden.
SubscribeExecution parses multiline SSE, normalizes CloudEvents and wrapped
AG-UI payloads, and confirms completion against the execution record. Handle
snapshot events (Delta: false) as replacements. Ignore echoed user messages and
subagent answers when rendering a root assistant reply. CUSTOM events named
runtime.result contain an explicit final answer delta in value.delta.
The helper rejects malformed, interrupted, timed-out, and failed streams. It does
not automatically replay streams because replay can coalesce text deltas.
The total deadline defaults to 15 minutes or TimeoutSeconds; a caller’s earlier
context deadline wins. Start operations are never retried automatically.
Workspace pickers and integrations
client.Workspaces.List(ctx) returns accessible workspace IDs and names.
client.Workspaces.Create(ctx, "observability", "Grafana assistant workspace")
creates a workspace and adds the token’s calling user as workspace admin.
Construct a new client with the selected workspace’s ID as WorkspaceID, then call
ListAgents and ListIntegrations. Only send names and descriptions to the
browser; integration responses may contain credential references.
On TOOL_APPROVAL_REQUEST, present the approval id, tool name, and arguments.
GetApproval reads status and ownership; ResolveApproval approves or rejects
only that tool call. Enforce ownership in your backend before resolving with a
shared token.
For a new Grafana connection, call UpsertIntegrationWithSecretsContext with
Type: "grafana", Scope: "PROJECT", and secrets base_url / api_token.
Check GetIntegration before first-time setup and reuse an existing connection.
Then attach the integration name to the agent’s complete integrations list.
Treat session identifiers as privileged: with a shared backend token, your application must enforce per-user session ownership before continuation or stop.
Complete scripts
The complete script is Go only. Python and TypeScript use the Minimal snippet above.
// Start an Aiden agent chat from a Go backend and stream the reply.
//
// Documented on https://appcd-dev.github.io/stackgen-sdk/code-samples/aiden-chat-backend/
//
// export STACKGEN_URL=https://app.stackgen.com
// export STACKGEN_TOKEN=stackgen_…
// export STACKGEN_WORKSPACE_ID=<workspace-uuid>
// go run ./examples/go/aiden-chat/agent-chat.go \
// -agent my-ops-agent \
// -message 'List available data sources without changing anything.'
//
// Use -retry-max to control how many times transient failures (network
// errors, 429, 5xx) are retried with exponential backoff before giving up.
package main
import (
"context"
"flag"
"fmt"
"os"
"strings"
"time"
"github.com/hashicorp/go-retryablehttp"
stackgen "github.com/appcd-dev/stackgen-sdk/clients/go"
"github.com/appcd-dev/stackgen-sdk/clients/go/aiden"
)
func envOr(keys ...string) string {
for _, key := range keys {
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
return v
}
}
return ""
}
func main() {
baseURL := flag.String("base-url", envOr("STACKGEN_URL", "STACKGEN_BASE_URL"), "Mothership URL")
apiToken := flag.String("api-token", envOr("STACKGEN_TOKEN", "STACKGEN_API_TOKEN"), "API token")
orgID := flag.String("workspace-id", envOr("STACKGEN_WORKSPACE_ID", "STACKGEN_PROJECT", "STACKGEN_PROJECT_ID", "STACKGEN_ORG_ID", "ORG_ID"), "Workspace UUID (sent as orgId)")
flag.StringVar(orgID, "org-id", *orgID, "Deprecated: use -workspace-id")
agent := flag.String("agent", "", "Agent name to chat with")
message := flag.String("message", "", "User message for this turn")
sessionID := flag.String("session-id", "", "Optional session to continue")
timeoutSeconds := flag.Int("timeout-seconds", 600, "Overall timeout")
retryMax := flag.Int("retry-max", 4, "Max retries for transient HTTP failures (network errors, 429, 5xx)")
flag.Parse()
if strings.TrimSpace(*baseURL) == "" || strings.TrimSpace(*apiToken) == "" || strings.TrimSpace(*orgID) == "" {
fmt.Fprintln(os.Stderr, "error: set -base-url/-api-token/-workspace-id or STACKGEN_URL/STACKGEN_TOKEN/STACKGEN_WORKSPACE_ID")
os.Exit(2)
}
if strings.TrimSpace(*agent) == "" {
fmt.Fprintln(os.Stderr, "error: -agent is required")
os.Exit(2)
}
if strings.TrimSpace(*message) == "" {
fmt.Fprintln(os.Stderr, "error: -message is required")
os.Exit(2)
}
rc := retryablehttp.NewClient()
rc.RetryMax = *retryMax
rc.Logger = nil
httpClient := rc.StandardClient()
client, err := stackgen.NewClient(stackgen.Config{
BaseURL: *baseURL,
APIToken: *apiToken,
WorkspaceID: *orgID,
TimeoutSeconds: *timeoutSeconds,
HTTPClient: httpClient,
})
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*timeoutSeconds)*time.Second)
defer cancel()
run, err := client.Aiden.StartAgentChat(ctx, *agent, aiden.StartChatRequest{
Message: *message,
SessionID: strings.TrimSpace(*sessionID),
})
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Fprintf(os.Stderr, "session_id=%s trace_id=%s run_id=%s\n", run.SessionID, run.TraceID, run.RunID)
err = client.Aiden.SubscribeExecution(ctx, run.TraceID, func(event aiden.ExecutionEvent) error {
if event.Subagent {
return nil
}
if event.Type == "TEXT_MESSAGE_CONTENT" {
fmt.Print(event.Text("Content", "content"))
}
return nil
})
if err != nil {
cleanup, done := context.WithTimeout(context.Background(), 20*time.Second)
defer done()
_ = client.Aiden.TerminateSession(cleanup, run.SessionID)
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Fprintln(os.Stderr)
fmt.Fprintln(os.Stderr, "subscribe complete ok=true")
}Related
- Stream agent chat — Python and TypeScript live AG-UI streaming
- Webhook → session → artifact — dual-token webhook journeys (also in Go)
- Ask → session artifacts. Ask wait and download.