Skip to main content
Agent Chat is a demo application that uses Agent SDK for Go end-to-end — React UI, Go API, Temporal-backed agent runs, and real-time streaming over SSE.
Demo app for learning and reference. Not intended for production use as-is.

Why it exists

Most agent frameworks run in-process — if your server restarts, the agent run is lost. Agent Chat demonstrates the Temporal-first model:
CapabilityHow Agent Chat shows it
Durable conversationsChat history and agent runs survive server restarts
Split client / workerAPI starts workflows; worker process executes them
StreamingAG-UI–shaped JSON over SSE to the browser
Conversation persistencePostgreSQL for app data; SDK conversation bridge for agent context
Use it as a blueprint when building your own HTTP-backed agent application.

Stack

LayerTechnology
Agent runtimeAgent SDK for Go + Temporal
APIGo — chi router, pgx, REST + SSE
UIReact Router 7 + Vite + Tailwind CSS v4
DataPostgreSQL (conversations and messages)
InfraDocker Compose — Postgres, Temporal, API, worker, UI

Architecture

The API and Temporal worker are separate processes from the same server image. APP_MODE selects the role:
ModeProcessSDK pattern
serverHTTP API + agent clientNewAgent with DisableLocalWorker + EnableRemoteWorkers
workerTemporal workerNewAgentWorker with matching config
Both share agent options via agentsetup.CommonOptions — same name, LLM client, conversation config, and Temporal settings — so the SDK fingerprint check passes.
Browser → React UI → Go API (agent client)

                     Temporal cluster

                     Worker process (AgentWorker)

                     LLM provider
Conversations and messages live in PostgreSQL. The SDK conversation bridge (server/agent/) feeds message history into WithConversation on each run.

Key code patterns

1. Split client / worker with shared config Both API and worker call CommonOptions() to get identical SDK options. The API adds DisableLocalWorker + EnableRemoteWorkers; the worker uses the options as-is. This ensures the SDK fingerprint check passes at activity entry.
// server/agent/setup.go — shared by API and worker
func CommonOptions(cfg Config, llmClient interfaces.LLMClient, conv conversation.Config) []sdkagent.Option {
    return []sdkagent.Option{
        sdkagent.WithTemporalConfig(&sdkagent.TemporalConfig{
            Host: cfg.Temporal.Host, Port: cfg.Temporal.Port,
            Namespace: cfg.Temporal.Namespace, TaskQueue: cfg.Temporal.TaskQueue,
        }),
        sdkagent.WithName(cfg.Agent.Name),
        sdkagent.WithSystemPrompt(cfg.Agent.SystemPrompt),
        sdkagent.WithLLMClient(llmClient),
        sdkagent.WithConversation(conv),
        sdkagent.WithStream(true),
        sdkagent.WithToolApprovalPolicy(sdkagent.AutoToolApprovalPolicy()),
    }
}

// API process — adds client-only options
opts := append(CommonOptions(cfg, llmClient, conv),
    sdkagent.DisableLocalWorker(),
    sdkagent.EnableRemoteWorkers(),
)
a, _ := sdkagent.NewAgent(opts...)

// Worker process — uses shared options directly
w, _ := sdkagent.NewAgentWorker(CommonOptions(cfg, llmClient, conv)...)
See Worker Separation. 2. SSE streaming bridge The API streams SDK events directly to the browser as AG-UI JSON. Each event is serialized with ev.ToJSON() and written to the SSE response. The Temporal workflow continues even if the browser disconnects.
// server/api/stream.go
eventCh, err := agent.Stream(ctx, userMessage, opts)
if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
    return
}

flusher := w.(http.Flusher)
for ev := range eventCh {
    if ev == nil {
        continue
    }
    b, _ := ev.ToJSON()
    fmt.Fprintf(w, "data: %s\n\n", b)
    flusher.Flush()

    if ev.Type() == sdkagent.AgentEventTypeRunFinished {
        // Persist the final message to Postgres after run completes
        persistMessage(ctx, db, conversationID, ev)
    }
}
See AG-UI Protocol and Streaming. 3. Custom Postgres conversation backend Agent Chat implements interfaces.Conversation over PostgreSQL instead of the in-memory or Redis backends. This lets conversation history survive restarts and be shared across worker replicas.
// server/agent/conversation.go
type PGConversation struct{ db *pgxpool.Pool }

func (c *PGConversation) AddMessage(ctx context.Context, id string, msg interfaces.Message) error {
    _, err := c.db.Exec(ctx,
        `INSERT INTO messages (conversation_id, role, content) VALUES ($1, $2, $3)`,
        id, msg.Role, msg.Content)
    return err
}

func (c *PGConversation) ListMessages(ctx context.Context, id string, opts ...interfaces.ListMessagesOption) ([]interfaces.Message, error) {
    // query and return ordered messages for this conversation ID
}

func (c *PGConversation) Clear(ctx context.Context, id string) error { /* DELETE */ }
func (c *PGConversation) IsDistributed() bool { return true } // shared across processes
Pass it to WithConversation — the SDK injects history into every LLM call automatically. See Conversation.

SDK features demonstrated

FeatureAgent Chat usage
Temporal runtimeEvery chat message triggers a durable workflow
ConversationCustom PostgreSQL-backed interfaces.Conversation
StreamingStream() with SSE bridge forwarding ev.ToJSON()
AG-UI ProtocolTEXT_MESSAGE_CONTENT, RUN_FINISHED, RUN_ERROR events
LLM ProvidersOpenAI, Anthropic, or Gemini via env config

Prerequisites

  • Docker and Docker Compose — all services run in containers; no local Go or Node install required
  • LLM API key — for OpenAI, Anthropic, or Gemini (set in server/.env)
  • Ports 3000 (UI), 9090 (API), 7233 + 8233 (Temporal), 5432 (Postgres) available locally

Run locally

# 1. Clone
git clone https://github.com/agenticenv/agent-chat.git
cd agent-chat

# 2. Configure
cp server/.env.example server/.env
# Open server/.env and set:
#   LLM_API_KEY=sk-your-key
#   LLM_PROVIDER=openai          # openai | anthropic | gemini
#   LLM_MODEL=gpt-4o

# 3. Start all services
docker compose up -d --build
Stop with docker compose down. Optional: LLM_BASE_URL, AGENT_SYSTEM_PROMPT, AGENT_NAME, AGENT_CONVERSATION_WINDOW_SIZE. For streaming toggle, copy ui/.env.example to ui/.env and set ENABLE_STREAM=true (SSE) or false (REST).

API and streaming

EndpointMethodPurpose
/api/conversationsGET, POSTList and create conversations
/api/conversations/:id/messagesGET, POSTMessage history; blocking agent reply
/api/conversations/:id/messages/streamPOSTSSE stream of AG-UI JSON events
The stream endpoint forwards SDK events via ev.ToJSON(). After RUN_FINISHED, the server emits a MESSAGE_PERSISTED extension frame with the database message id. Closing the browser stream does not cancel the Temporal workflow — use GET /api/conversations/:id/messages to reconcile state.

UI streaming

The UI maps AG-UI events to chat bubbles:
EventUI behavior
TEXT_MESSAGE_CONTENTIncremental assistant text via delta
RUN_ERRORDisplay error message
MESSAGE_PERSISTEDReplace streaming bubble with persisted message id
Toggle streaming vs REST via ENABLE_STREAM in Docker (ui service env) or VITE_ENABLE_STREAM for local UI dev.

Durable Agent

SDK durable execution example

Agent Worker

Client and worker split example

AG-UI Protocol

Event format and frontend integration

AG-UI Example

Minimal SSE server in the SDK repo
Source code: github.com/agenticenv/agent-chat (separate repository).