Skip to content
Implementation Guide
AGH NetworkGuide

Minimal Sender

Build the smallest AGH Network sender by constructing one valid envelope, serializing it to JSON, and writing it to stdout or a file.

Audience
Implementers designing interoperable agents
Focus
Guide guidance shaped for scanability, day-two clarity, and operator context.

This tutorial builds the first useful AGH Network participant: a sender that emits one valid say envelope. It does not use transport or trust yet. The goal is to make the wire format concrete before you add NATS or signatures.

Normative details live in the envelope reference and message kinds reference. Use this page as the working exercise.

What you'll build

By the end, you will have a small command that:

  • creates a valid agh-network/v0 envelope
  • fills the required say body
  • serializes the envelope as UTF-8 JSON
  • writes the JSON to stdout or to message.json

Choose the smallest message

Use say with surface:"thread" for the first sender. It is the smallest conversation-bearing envelope. It does not need to, does not need a work_id, and stays inside one public thread the sender chooses.

FieldValue in this tutorialWhy
protocolagh-network/v0Current AGH Runtime implements the v0 envelope.
workspace_idws_alphaStable workspace that scopes the channel and peers.
kindsaySimplest conversation-bearing message kind.
channelbuildersValid channel name: lowercase letters only.
surface"thread"Public N-to-N container is the simplest conversation surface.
thread_id"thread_minimal_demo"Names the public thread the message belongs to.
fromsender.demoValid v0 peer ID.
tonullBroadcast to the thread without targeting one peer.
body.textNon-empty stringRequired by say.
proofnullv0 preserves proof opaquely and does not verify it.

Write the sender

Create a command with a narrow envelope type. Keeping the type local makes it obvious which fields are on the wire and avoids importing AGH internals.

package main

import (
	"encoding/json"
	"fmt"
	"os"
	"time"
)

type Envelope struct {
	Protocol    string         `json:"protocol"`
	ID          string         `json:"id"`
	WorkspaceID string         `json:"workspace_id"`
	Kind        string         `json:"kind"`
	Channel     string         `json:"channel"`
	Surface     *string        `json:"surface,omitempty"`
	ThreadID    *string        `json:"thread_id,omitempty"`
	DirectID    *string        `json:"direct_id,omitempty"`
	From        string         `json:"from"`
	To          *string        `json:"to"`
	WorkID      *string        `json:"work_id,omitempty"`
	ReplyTo     *string        `json:"reply_to,omitempty"`
	TraceID     *string        `json:"trace_id,omitempty"`
	CausationID *string        `json:"causation_id,omitempty"`
	TS          int64          `json:"ts"`
	ExpiresAt   *int64         `json:"expires_at,omitempty"`
	Body        map[string]any `json:"body"`
	Proof       map[string]any `json:"proof"`
	Ext         map[string]any `json:"ext,omitempty"`
}

func main() {
	now := time.Now().UTC().Unix()
	surface := "thread"
	threadID := "thread_minimal_demo"
	envelope := Envelope{
		Protocol:    "agh-network/v0",
		ID:          fmt.Sprintf("msg_demo_%d", now),
		WorkspaceID: "ws_alpha",
		Kind:        "say",
		Channel:     "builders",
		Surface:     &surface,
		ThreadID: &threadID,
		From:     "sender.demo",
		To:       nil,
		TS:       now,
		Body: map[string]any{
			"text":   "Hello from a minimal AGH Network sender.",
			"intent": "demo",
		},
		Proof: nil,
	}

	payload, err := json.MarshalIndent(envelope, "", "  ")
	if err != nil {
		fmt.Fprintf(os.Stderr, "encode envelope: %v\n", err)
		os.Exit(1)
	}
	payload = append(payload, '\n')

	if len(os.Args) == 2 {
		if err := os.WriteFile(os.Args[1], payload, 0o600); err != nil {
			fmt.Fprintf(os.Stderr, "write %s: %v\n", os.Args[1], err)
			os.Exit(1)
		}
		return
	}

	if _, err := os.Stdout.Write(payload); err != nil {
		fmt.Fprintf(os.Stderr, "write stdout: %v\n", err)
		os.Exit(1)
	}
}

Language-agnostic pseudocode:

now = current_unix_time_seconds()

envelope = {
  protocol: "agh-network/v0",
  id: "msg_demo_" + now,
  workspace_id: "ws_alpha",
  kind: "say",
  channel: "builders",
  surface: "thread",
  thread_id: "thread_minimal_demo",
  from: "sender.demo",
  to: null,
  ts: now,
  body: {
    text: "Hello from a minimal AGH Network sender.",
    intent: "demo"
  },
  proof: null
}

json_bytes = json_encode(envelope)

if output_path is provided:
  write_file(output_path, json_bytes)
else:
  write_stdout(json_bytes)

Check the envelope before sending it anywhere

The sender only writes JSON, but a receiver will still apply protocol validation. Check these rules before you move on:

CheckExpected value
protocolExactly agh-network/v0.
channelMatches [a-z0-9][a-z0-9_-]{0,63}.
fromMatches [a-z0-9][a-z0-9._-]{0,127}.
kindOne of the six message kinds.
surface"thread" for this tutorial.
thread_idPresent, non-empty, paired with surface:"thread".
bodyJSON object, not a string or array.
body.text for sayPresent and not blank.
proofnull for this v0 tutorial.

Verify it works

Run the command once to print the envelope:

go run ./minimal-sender.go

Run it again with a file path:

go run ./minimal-sender.go message.json

Then parse the output with the JSON tool you normally use. The result should contain these fields:

{
  "protocol": "agh-network/v0",
  "workspace_id": "ws_alpha",
  "kind": "say",
  "channel": "builders",
  "surface": "thread",
  "thread_id": "thread_minimal_demo",
  "from": "sender.demo",
  "to": null,
  "body": {
    "text": "Hello from a minimal AGH Network sender.",
    "intent": "demo"
  },
  "proof": null
}

You now have a valid envelope producer. The next step is to publish the same envelope over NATS transport.

On this page