> ## Documentation Index
> Fetch the complete documentation index at: https://docs.epode.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Go SDK: net/http Middleware for Agent Feedback Apps

> Wrap any net/http handler with Agent Feedback using zero external dependencies. JSON and HTML responses are instrumented; streams are left untouched.

The Go SDK wraps any `http.Handler` with a single function call. It instruments finite JSON object and HTML responses, detects `Flush` calls and leaves streams completely untouched, and sends telemetry through a bounded background goroutine. It uses only the Go standard library — no external dependencies are required.

## Installation

<Steps>
  <Step title="Add the module">
    ```bash theme={null}
    go get github.com/open-software-network/os-epode/sdk/go
    ```
  </Step>

  <Step title="Import the package">
    ```go theme={null}
    import agentfeedback "github.com/open-software-network/os-epode/sdk/go"
    ```
  </Step>

  <Step title="Create a product key">
    Open the Agent Feedback dashboard, create a product, and copy the API key. Store it as `AGENT_FEEDBACK_KEY` in your environment.
  </Step>
</Steps>

***

## Creating the middleware

Call `agentfeedback.New` with an `Options` struct. The function validates your API key and starts the background telemetry worker immediately.

```go theme={null}
package main

import (
    "context"
    "log"
    "net/http"
    "os"

    agentfeedback "github.com/open-software-network/os-epode/sdk/go"
)

func main() {
    feedback, err := agentfeedback.New(agentfeedback.Options{
        APIKey:  os.Getenv("AGENT_FEEDBACK_KEY"),
        Include: []string{"/search", "/docs/**"},
        CustomerRef: func(r *http.Request) string {
            return r.Header.Get("X-Account-ID")
        },
    })
    if err != nil {
        log.Fatal(err)
    }
    defer feedback.Shutdown(context.Background())

    mux := http.NewServeMux()
    mux.HandleFunc("/search", searchHandler)

    log.Fatal(http.ListenAndServe(":8080", feedback.Middleware(mux)))
}
```

***

## Options struct fields

Pass an `agentfeedback.Options` struct to `agentfeedback.New`. Only `APIKey` is required.

<ParamField body="APIKey" type="string" required>
  Your Agent Feedback product key. Must match the pattern `af_live_<keyId>_<secret>`. Store this as an environment variable.
</ParamField>

<ParamField body="Endpoint" type="string" default="https://agent-feedback-api-production.up.railway.app">
  Override the Agent Feedback API endpoint. Trailing slashes are trimmed automatically.
</ParamField>

<ParamField body="Include" type="[]string">
  Glob-style path patterns that opt routes in. When empty, all routes are instrumented (subject to `Exclude`). Supports `*` (any path segment) and `**` (any path). Example: `[]string{"/search", "/docs/**"}`.
</ParamField>

<ParamField body="Exclude" type="[]string">
  Glob-style patterns to skip. The default exclusion list (`/health`, `/healthz`, `/metrics`, `/favicon.ico`, `/robots.txt`, `/_agent-feedback/*`, `/api/v2/outcomes`) is always applied. Patterns you add here extend that list.
</ParamField>

<ParamField body="FeedbackMode" type="FeedbackMode" default="FeedbackAuto">
  Controls the feedback instruction sent to agents. Use the package constants: `agentfeedback.FeedbackAuto`, `agentfeedback.FeedbackAsk`, or `agentfeedback.FeedbackOff`.
</ParamField>

<ParamField body="CustomerRef" type="func(*http.Request) string">
  Function that extracts an opaque customer identifier from the request. Used to group interactions by account in the dashboard.
</ParamField>

<ParamField body="SessionRef" type="func(*http.Request) string">
  Function that extracts a session identifier from the request. Links a sequence of interactions for session-level analytics.
</ParamField>

<ParamField body="RuntimeHint" type="func(*http.Request) string">
  Function that returns a hint about the agent runtime making the request. Used for runtime-level breakdown in the dashboard.
</ParamField>

<ParamField body="FlushInterval" type="time.Duration" default="500ms">
  How often the background goroutine flushes telemetry events. The queue also flushes immediately when 50 events accumulate.
</ParamField>

<ParamField body="MaxQueueSize" type="int" default="1000">
  Capacity of the telemetry channel. When the channel is full, the oldest event is discarded to make room for the newest.
</ParamField>

<ParamField body="HTTPClient" type="*http.Client">
  Override the HTTP client used for telemetry delivery. Defaults to a client with a 3-second timeout.
</ParamField>

<ParamField body="Sender" type="func(context.Context, string, http.Header, []byte) error">
  Provide a custom telemetry sender for testing or proxying. When set, `HTTPClient` is ignored.
</ParamField>

***

## Stream detection

The middleware wraps `http.ResponseWriter` in a capture buffer. If the handler calls `Flush()` before finishing, the buffer is flushed directly to the underlying writer and the response is marked as streamed — instrumentation is skipped and no body buffering occurs. Streaming handlers are never blocked or modified.

***

## Graceful shutdown

Call `feedback.Shutdown(ctx)` at server shutdown to drain the telemetry queue. Pass a context with a deadline to set a maximum wait time.

```go theme={null}
srv := &http.Server{Addr: ":8080", Handler: feedback.Middleware(mux)}

go func() {
    <-quit
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    _ = srv.Shutdown(ctx)
    _ = feedback.Shutdown(ctx)
}()
```

***

## Agent helper functions

Import `FeedbackFromResponse` and `SubmitProductOutcome` from the same package to build a feedback-aware agent runtime that submits outcomes deterministically.

### `FeedbackFromResponse`

Reads the `*Envelope` from an `*http.Response` and the response body bytes. Checks the JSON body for `_agentFeedback`, then the HTML body for `<script id="agent-feedback">`, then the `Agent-Feedback` response header.

```go theme={null}
resp, err := http.Get("https://api.example.com/search?q=logs")
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)

envelope, err := agentfeedback.FeedbackFromResponse(resp, body)
if err != nil {
    log.Println("no feedback contract:", err)
}
```

### `SubmitProductOutcome`

Submits a compact outcome review to the URL embedded in the envelope. Validates the contract and enforces the allowed-origin allow-list.

```go theme={null}
result, err := agentfeedback.SubmitProductOutcome(
    context.Background(),
    envelope,
    agentfeedback.OutcomeReview{
        Outcome: "success",
        Note:    "The search returned the log entries needed to resolve the incident.",
    },
    nil,  // nil uses the default trusted origin
    nil,  // nil uses http.DefaultClient
)
if err != nil {
    log.Println("outcome submission failed:", err)
}
```

<Note>
  Pass a non-nil `allowedOrigins` slice only when submitting to a self-hosted Agent Feedback endpoint. The default trusted origin is `https://agent-feedback-api-production.up.railway.app`.
</Note>

<Tip>
  The Go SDK uses only the standard library for its middleware and agent helpers. The only HTTP call it makes is the background telemetry batch — your product response never waits for it.
</Tip>
