Help us shape the future of Go! Take the 2026 Go Developer Survey

aisdk

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 6, 2026 License: MIT Imports: 5 Imported by: 0

README

ai-sdk-go

A Go backend for the Vercel AI SDK 6 UIMessage stream protocol, plus streaming LLM providers and an agentic tool loop.

Four layers, each usable on its own:

Import What it gives you
github.com/xujiahua/ai-sdk-go The AI SDK 6 data stream protocol: an SSE writer emitting the events @ai-sdk/react's useChat expects, and conversion between UI messages and model messages.
github.com/xujiahua/ai-sdk-go/llm One streaming Provider contract over OpenAI-compatible and Anthropic APIs.
github.com/xujiahua/ai-sdk-go/llm/acp A local agent as the model, driven over the Agent Client Protocol on stdio.
github.com/xujiahua/ai-sdk-go/agent The chat endpoint: an http.Handler running the tool loop, with skill prompts and capability-gated tools.

The core has no third-party dependencies. Tools, logging and the HTTP router are interfaces you supply.

Status: incubating. The API is not yet stable.

Serving a chat endpoint

a := agent.New(llm.NewOpenAI(),
    agent.WithLLMConfig(llm.Config{APIKey: key, Model: "gpt-4o"}),
    agent.WithTools(myTools, map[string]string{"search": "execution"}),
    agent.WithLogger(slog.Default()), // *slog.Logger satisfies the interface as-is
)

mux := http.NewServeMux()
mux.Handle("/chat", a) // point useChat at this

The agent is a plain http.Handler, so any router mounts it in one line:

Router
net/http mux.Handle("POST /chat", a)
chi r.Method("POST", "/chat", a)
gin g.POST("/chat", gin.WrapH(a))
echo e.POST("/chat", echo.WrapHandler(a))

Running it without a browser

Agent.Run drives one turn against a Sink, so HTTP is not required: the SSE renderer is simply the Sink that speaks the AI SDK protocol. cmd/aisdk-agent is a terminal agent built on that seam — the same loop, the same tools, the same capability gate, printed to a terminal.

# a local Kiro CLI as the model — no API key needed
go run ./cmd/aisdk-agent

# the tool loop, deterministically and with no model at all
go run ./cmd/aisdk-agent -provider script -once "roll two dice"

# the capability gate: withheld, then authorized
go run ./cmd/aisdk-agent -provider script -skill operator -once "wipe the state"
go run ./cmd/aisdk-agent -provider script -skill operator -privileged -once "wipe the state"

# an OpenAI-compatible endpoint instead
AISDK_API_KEY=sk-… go run ./cmd/aisdk-agent -provider openai -model gpt-4o

The REPL does its own line editing (backspace, Ctrl-U, Ctrl-W) because a terminal in canonical mode erases one byte per backspace: deleting a CJK character would otherwise leave a broken UTF-8 fragment in the line and half a glyph on screen. With no terminal — piped input — it falls back to reading whole lines.

Capabilities

A tool carries a capability tag; a skill declares which tags it may call; and the host separately declares whether this endpoint may expose privileged tags at all. A tool is offered to the model only when both agree, so a client that picks its own skill cannot escalate. An untagged tool is treated as privileged.

The protocol is pinned

testdata/ holds golden files for the response headers, every event the writer emits, and whole turns through the agent — text, a server-executed tool round trip, a provider-executed tool event, an error, and a gated tool. They are the contract with a client this module does not control:

go test ./...                  # verify
go test . ./agent -update      # rewrite, then review the diff

Only those two packages hold golden files, so only their test binaries define -update; passing it to the others fails with "flag provided but not defined".

Compatibility beyond those fixtures is load-bearing elsewhere: this backend serves @ai-sdk/react in production.

Documentation

Overview

Package aisdk implements the Vercel AI SDK 6 data stream protocol (the SSE-based UIMessage stream) for Go backends: a streaming writer emitting the events @ai-sdk/react's useChat hook expects, and conversion between UI messages and model messages.

Protocol reference: https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ChatRequest

type ChatRequest struct {
	Messages    []UIMessage     `json:"messages"`
	PageContext json.RawMessage `json:"pageContext,omitempty"`
	ClientTools []Tool          `json:"clientTools,omitempty"`
}

ChatRequest is the JSON body POSTed by useChat to the chat endpoint. PageContext and ClientTools are extensions a client may add; both are optional, so a plain chat client keeps working unchanged.

type Logger

type Logger interface {
	Debug(msg string, args ...any)
	Info(msg string, args ...any)
	Warn(msg string, args ...any)
	Error(msg string, args ...any)
}

Logger is the whole logging surface this module needs.

The method set deliberately mirrors *slog.Logger, so a plain slog logger satisfies it with no adapter at all. Nothing in the chat backend reaches for a package-global logger: an embedding application injects its own logging stack (a logrus or zap adapter is a handful of lines) and gets every log line in its own format.

func OrNop

func OrNop(l Logger) Logger

OrNop normalizes a possibly-nil logger, so call sites can log unconditionally instead of guarding each one.

type LoggerAware

type LoggerAware interface {
	SetLogger(Logger)
}

LoggerAware is implemented by components that can adopt a logger after construction (the providers do). It lets one injection reach the whole request path without every constructor taking a logger.

type MessagePart

type MessagePart struct {
	Type       string `json:"type"`
	Text       string `json:"text,omitempty"`
	ToolCallID string `json:"toolCallId,omitempty"`
	ToolName   string `json:"toolName,omitempty"`
	State      string `json:"state,omitempty"`
	Input      any    `json:"input,omitempty"`
	Output     any    `json:"output,omitempty"`
}

MessagePart is a union part inside a UIMessage. Tool parts carry the tool name in Type ("tool-<name>") for static tools, or in ToolName for the "dynamic-tool" type. A completed tool part has a non-nil Output.

type ModelMessage

type ModelMessage struct {
	Role       string `json:"role"`
	Content    string `json:"content"`
	ToolCallID string `json:"tool_call_id,omitempty"`
	ToolCalls  []any  `json:"-"`
}

ModelMessage represents a simplified message for the LLM API call.

func ConvertToModelMessages

func ConvertToModelMessages(msgs []UIMessage) []ModelMessage

ConvertToModelMessages converts UIMessages to role/content messages for LLM APIs. Text parts are concatenated. Completed tool parts (those with an Output) are reconstructed into an assistant message carrying tool_calls plus a following tool-result message — so tool context (including the results of client-executed tools) survives the re-POST that resumes the agent loop. Pending tool parts (no Output) are skipped so we never emit a dangling tool_call the LLM API would reject.

type NopLogger

type NopLogger struct{}

NopLogger drops every line. It is the fallback whenever a component was built without a logger, so a missing injection degrades to silence rather than to a nil-pointer panic.

func (NopLogger) Debug

func (NopLogger) Debug(string, ...any)

func (NopLogger) Error

func (NopLogger) Error(string, ...any)

func (NopLogger) Info

func (NopLogger) Info(string, ...any)

func (NopLogger) Warn

func (NopLogger) Warn(string, ...any)

type Tool

type Tool struct {
	Type     string       `json:"type"`
	Function ToolFunction `json:"function"`
}

Tool is one tool advertised to the model, in the OpenAI function-calling shape. The Anthropic and ACP providers translate from it, so this stays the single internal tool representation.

type ToolFunction

type ToolFunction struct {
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
	Parameters  any    `json:"parameters"`
}

ToolFunction describes a callable function: its name, its purpose, and its JSON Schema parameters.

type UIMessage

type UIMessage struct {
	ID    string        `json:"id"`
	Role  string        `json:"role"` // "user", "assistant", "system"
	Parts []MessagePart `json:"parts"`
}

UIMessage is the shape received from the AI SDK 6 frontend (useChat).

type UIMessageStream

type UIMessageStream struct {
	// contains filtered or unexported fields
}

UIMessageStream writes AI SDK 6 Data Stream Protocol events to an http.ResponseWriter. Each event is a Server-Sent Event (SSE) line:

data: {"type":"...","...":"..."}\n\n

The stream ends with:

data: [DONE]\n\n

func NewUIMessageStream

func NewUIMessageStream(w http.ResponseWriter) *UIMessageStream

NewUIMessageStream prepares the ResponseWriter for SSE streaming and returns a stream handle. This is the sole owner of the SSE response headers — callers set only their own (e.g. CORS) before handing the writer over. A writer that already carries a Content-Type is left untouched, so an application that insists on writing the head itself is not overridden.

func (*UIMessageStream) Done

func (s *UIMessageStream) Done()

Done writes the terminal [DONE] marker and marks the stream closed.

func (*UIMessageStream) Finish

func (s *UIMessageStream) Finish() error

Finish emits the finish event (message complete).

func (*UIMessageStream) FinishStep

func (s *UIMessageStream) FinishStep() error

FinishStep emits a finish-step event.

func (*UIMessageStream) MessageStart

func (s *UIMessageStream) MessageStart(messageID string) error

MessageStart emits the start-of-message event.

func (*UIMessageStream) ReasoningDelta

func (s *UIMessageStream) ReasoningDelta(id, delta string) error

ReasoningDelta emits an incremental reasoning chunk.

func (*UIMessageStream) ReasoningEnd

func (s *UIMessageStream) ReasoningEnd(id string) error

ReasoningEnd closes a reasoning block.

func (*UIMessageStream) ReasoningStart

func (s *UIMessageStream) ReasoningStart(id string) error

ReasoningStart begins a reasoning block.

func (*UIMessageStream) Send

func (s *UIMessageStream) Send(event any) error

Send marshals the event to JSON and writes it as an SSE data line.

func (*UIMessageStream) SendError

func (s *UIMessageStream) SendError(msg string) error

SendError writes an error event to the stream.

func (*UIMessageStream) StartStep

func (s *UIMessageStream) StartStep() error

StartStep emits a start-step event.

func (*UIMessageStream) TextDelta

func (s *UIMessageStream) TextDelta(id, delta string) error

TextDelta emits an incremental text chunk.

func (*UIMessageStream) TextEnd

func (s *UIMessageStream) TextEnd(id string) error

TextEnd closes a text block.

func (*UIMessageStream) TextStart

func (s *UIMessageStream) TextStart(id string) error

TextStart begins a text block with the given ID.

func (*UIMessageStream) ToolInputAvailable

func (s *UIMessageStream) ToolInputAvailable(toolCallID, toolName string, input any) error

ToolInputAvailable signals that tool input is fully available.

func (*UIMessageStream) ToolInputDelta

func (s *UIMessageStream) ToolInputDelta(toolCallID, inputTextDelta string) error

ToolInputDelta sends a partial tool input text chunk.

func (*UIMessageStream) ToolInputStart

func (s *UIMessageStream) ToolInputStart(toolCallID, toolName string) error

ToolInputStart begins streaming tool input.

func (*UIMessageStream) ToolOutputAvailable

func (s *UIMessageStream) ToolOutputAvailable(toolCallID string, output any) error

ToolOutputAvailable sends the tool execution result.

Directories

Path Synopsis
Package agent is the chat endpoint: an http.Handler that runs an agentic tool loop over a streaming model and renders the result as an AI SDK 6 UIMessage stream.
Package agent is the chat endpoint: an http.Handler that runs an agentic tool loop over a streaming model and renders the result as an AI SDK 6 UIMessage stream.
cmd
aisdk-agent command
Command aisdk-agent is a terminal agent: the same tool loop the HTTP endpoint serves, rendered to a terminal instead of to an AI SDK stream.
Command aisdk-agent is a terminal agent: the same tool loop the HTTP endpoint serves, rendered to a terminal instead of to an AI SDK stream.
llm
Package llm is one streaming contract over several model APIs.
Package llm is one streaming contract over several model APIs.
acp
Package acp drives a local agent process as the model, over the Agent Client Protocol on stdin/stdout.
Package acp drives a local agent process as the model, over the Agent Client Protocol on stdin/stdout.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL