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

ai

package
v0.13.2 Latest Latest
Warning

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

Go to latest
Published: Oct 22, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package ai is dbc's AI chat assistance: a conversation with a coding agent about the query in the editor and the result it produced.

WHY AN AGENT PROCESS AND NOT AN HTTP API. dbc does not talk to any model provider itself. It runs an agent binary that speaks the Agent Client Protocol (ACP) over stdio — GitHub's copilot-language-server by default — the same way ced (github.com/rohanthewiz/ced) does. That buys three things dbc would otherwise have to build and maintain:

  • AUTH IS THE AGENT'S. Copilot's OAuth tokens, refresh and keychain storage live in the binary, and are shared with every editor the user already signed in from. dbc stores no credential and reads no key.
  • IT IS THE SUPPORTED PATH. ACP is how Zed and JetBrains host Copilot chat; api.githubcopilot.com is an internal endpoint that can change under anyone calling it directly.
  • OTHER BACKENDS COME FREE. Any ACP agent is a drop-in: Claude Code and Gemini are registered below and differ only in the binary name.

The package is UI-agnostic. A Chat reports everything that happens as Events on a channel, so any UI — today the Bubble Tea one in package tui — consumes it the same way.

Index

Constants

View Source
const CopilotID = "copilot"

CopilotID is the default agent.

View Source
const DefaultContextRows = 10

DefaultContextRows is how many result rows go with a question when the connection allows rows and ai_context_rows is not set.

Variables

View Source
var ErrAuthRequired = errors.New("sign-in required")

ErrAuthRequired matches (errors.Is) an EventExit or EventTurnDone error that the agent refused for lack of sign-in. A UI uses it to offer the agent's sign-in — for an agent that CanSignIn, one dbc runs itself.

View Source
var ErrBusy = errors.New("the assistant is still answering — stop it first")

ErrBusy is returned by Send while an answer is still streaming.

View Source
var ErrNotReady = errors.New("the assistant is not connected yet")

ErrNotReady is returned by Send before EventReady, and after EventExit.

Functions

This section is empty.

Types

type Agent

type Agent struct {
	ID     string   // stable config value (ai_agent)
	Name   string   // display name for the pane title and transcript
	Binary string   // executable resolved on PATH; its presence is the opt-in
	Args   []string // argv after the binary

	// Install says how to get the binary, shown when it is missing — the chat
	// pane is the one place a missing integration is worth explaining, since
	// the user opened it on purpose.
	Install string

	// Auth says how to sign in, shown when the agent refuses for lack of it.
	Auth string

	// SignInArgs is the argv after the binary that runs it as a language
	// server offering GitHub's device-flow sign-in (see signin.go). Nil when
	// dbc cannot sign the user in for this agent — then Auth is the only
	// way, and it names the agent's own login command.
	SignInArgs []string
}

Agent describes one ACP backend.

func AgentByID

func AgentByID(id string) (a Agent, ok bool)

AgentByID resolves a configured agent id. Empty means the default, and so does an unknown id — with ok=false so the caller can say it fell back, because a typo in ai_agent should not silently look like a choice.

func Agents

func Agents() []Agent

Agents lists the known backends, default first.

func (Agent) CanSignIn

func (a Agent) CanSignIn() bool

CanSignIn reports whether dbc can run this agent's sign-in itself.

func (Agent) Installed

func (a Agent) Installed() bool

Installed reports whether the agent's binary is on PATH.

type AuthStatus

type AuthStatus struct {
	Status string `json:"status"` // "OK", "AlreadySignedIn", "NotSignedIn", "NotAuthorized", …
	User   string `json:"user"`
}

AuthStatus is the server's account state: its status word and, when signed in, the GitHub login.

func (AuthStatus) NoSubscription

func (s AuthStatus) NoSubscription() bool

NoSubscription reports the one failure worth its own words: the GitHub sign-in worked, but that account has no Copilot access, so signing in again will not help.

func (AuthStatus) SignedIn

func (s AuthStatus) SignedIn() bool

SignedIn reports whether the status means a usable account. Anything unrecognised counts as signed out, which fails safe: the user just signs in again. MaybeOK (the server could not reach GitHub to confirm a stored token) counts as in, as the server itself treats it.

type Chat

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

Chat is one live conversation. All methods are safe from any goroutine.

func Start

func Start(agent Agent, opt Options) *Chat

Start launches the agent and runs the handshake in the background. It returns at once; EventReady or EventExit reports how it went.

A missing binary is reported as EventExit with an error that carries the install hint, rather than as a failure of Start itself, so the UI has one path for "the assistant is unavailable" however it came about.

func StartPipes

func StartPipes(agent Agent, opt Options, r io.Reader, w io.Writer) *Chat

StartPipes runs a conversation over an existing connection to an agent — r is what the agent writes, w is what it reads — instead of spawning a process. It serves an agent reached some other way (a socket, a container's stdio) and is how tests drive the Chat against a scripted one (see package aitest). Closing the Chat closes w.

func (*Chat) Agent

func (c *Chat) Agent() Agent

Agent returns the backend this chat runs.

func (*Chat) Busy

func (c *Chat) Busy() bool

Busy reports whether an answer is streaming.

func (*Chat) Cancel

func (c *Chat) Cancel() bool

Cancel asks the agent to stop the answer in flight. The turn then ends with EventTurnDone and stopReason "cancelled". A second Cancel in the same turn does nothing — the agent is already unwinding. Reports whether a cancel was sent.

func (*Chat) Close

func (c *Chat) Close()

Close ends the conversation and the agent process. Safe to call more than once, and before the handshake has finished.

func (*Chat) Done

func (c *Chat) Done() <-chan struct{}

Done is closed by Close. A reader waiting on Events selects on it too, so it stops waiting when the conversation it was reading is gone rather than blocking forever on a channel nothing will write to again.

func (*Chat) Events

func (c *Chat) Events() <-chan Event

Events is the conversation's event stream. It is never closed: after EventExit nothing more is sent, and a reader that stops reading after Close leaks nothing, because delivery gives up once Close has run.

func (*Chat) Ready

func (c *Chat) Ready() bool

Ready reports whether Send can be called.

func (*Chat) Send

func (c *Chat) Send(text string) error

Send starts one turn with the given prompt text. It returns at once; the answer streams in as EventText and ends with EventTurnDone.

The prompt goes as a single text block. ACP also has embedded-resource blocks for attachments, but dbc's context is small and structured (a query, maybe an error, a few rows) and reads best to a model as prose with fenced blocks — which every agent accepts, so there is one code path.

func (*Chat) SetModel

func (c *Chat) SetModel(id string) error

SetModel switches the session's model. It blocks for the round trip, so a UI calls it off its event loop.

type Column

type Column struct {
	Name string
	Type string
}

Column is a column's name and declared type ("" when the database does not say, as for a SQLite view's computed column).

type Context

type Context struct {
	Conn   string // connection name
	Driver string // postgres | mysql | sqlite | bytdb — the model needs the dialect

	Query string // the statement the question is about; "" if none
	Err   string // the error the last run of it failed with, if it failed

	// Script names the Go script (its file name, "nightly.go") when the
	// question is asked from a script tab. Query then holds the script's
	// source, not SQL: Build fences it as Go and calls it "the script", so
	// the model neither reads Go as a malformed statement nor answers with
	// SQL that rewrites the whole file as one query. Err and the result
	// fields are then the last run of THIS script, when it was the last
	// thing the tab ran.
	Script string
	// Pipeline names the pipeline or job file ("orders.json") when the
	// question is asked from a pipeline or job tab, and PipelineKind says
	// which ("pipeline", "job"). Query then holds its JSON spec, not SQL:
	// Build fences it as JSON and calls it the pipeline (or job), so the
	// model neither reads the spec as a malformed statement nor answers
	// with SQL meant to replace it. Err is its last run's error, and the
	// result fields a preview's rows on screen.
	Pipeline     string
	PipelineKind string
	// ScriptConns are the configured connections, "name (driver)", for a
	// script, pipeline or job question: a script names its connections in
	// strings and a pipeline's nodes in their conn fields (neither tab has
	// a connection of its own), so these are the names they can use and
	// the dialect each one's SQL must be written in. Names and drivers are
	// configuration, not data, so they go on every connection.
	ScriptConns []string
	// ScriptAPI is the sdb API summary (sdbapi.Summary) the script is
	// written against, with the pipeline plugins (pipeline.Summary) a
	// pipeline's nodes — and a script's pipelines — are made of. The caller
	// sets it on the first script, pipeline or job question of a
	// conversation only: the agent keeps the session's history, so the
	// same ~2k tokens on every turn would buy nothing — the preamble's rule.
	ScriptAPI string

	// The last result. Columns nil means there is none (never run, or an
	// exec with nothing to show).
	Columns   []string
	Rows      [][]string
	Truncated bool // the fetch hit max_rows, so len(Rows) is not the table's size

	// Hidden lists the result columns (indices into Columns, ascending)
	// hidden in the grid. Build leaves their values out and names them. The
	// full rows are passed rather than a projected copy because the context
	// chip builds a Context every frame, and Build only projects the handful
	// of rows it actually sends.
	Hidden []int

	// Order is the grid's row order after a header sort: Order[i] is the
	// result row shown i-th. It may hold only a prefix — the rows that
	// could be sent — since the chip builds a Context every frame and a
	// full copy of a 10,000-row permutation per frame buys nothing. nil
	// means the result's own order. SortedBy names the sorted column (""
	// when unsorted) and SortDesc its direction.
	Order    []int
	SortedBy string
	SortDesc bool

	// Shared marks a result the user SHARED with the assistant on purpose —
	// a result tab picked with "Share with the assistant" — rather than the
	// one that rides along because it is Query's own last run. A shared
	// result is framed as such, goes even beside an error (Err is then the
	// query's, the result is another run's), and is named in the note.
	// SharedLabel names it as the UI does ("result 2"); SharedFrom is the
	// statement it is the result of, when that is not Query ("" when it is,
	// or for a script's shown result, whose statement is not known).
	Shared      bool
	SharedLabel string
	SharedFrom  string

	// SendRows is the connection's ai_rows opt-in; MaxRows is ai_context_rows.
	SendRows bool
	MaxRows  int

	// Plan is the query plan the user explained for Query, rendered as text
	// with dbc's findings ("" when there is none). A plan is how the
	// database will run the statement — table and index names, row counts
	// and timings — not what the rows hold, so like the schema it goes on
	// every connection. The literals it quotes in its conditions are the
	// user's own, from the SQL that is sent anyway.
	Plan string

	// Tables are the catalog's tables the query or question mentions. A
	// table with no Columns is still named in the Note — that is how the
	// context chip forecasts "schema of cats" before the columns have been
	// looked up — but is left out of the prompt text, so a caller building
	// the real prompt drops the ones the lookup could not describe.
	Tables []Table
}

Context is the state of the editor and results at the moment of asking.

type Event

type Event struct {
	Kind       EventKind
	Text       string
	Models     []Model
	ModelID    string
	StopReason string
	Err        error
}

Event is one thing that happened in a conversation.

type EventKind

type EventKind int

EventKind says what an Event carries.

const (
	// EventReady: the handshake finished and Send may be called. Models and
	// ModelID describe the roster the agent offers.
	EventReady EventKind = iota
	// EventText: the next chunk of the agent's answer. Chunks of one answer
	// arrive in order; the UI appends them.
	EventText
	// EventTool: the agent started a tool call; Text is its title.
	EventTool
	// EventNote: something dbc did on the user's behalf that belongs in the
	// transcript, e.g. declining a permission request.
	EventNote
	// EventTurnDone: the answer to one Send is complete. StopReason is the
	// agent's ("end_turn", "cancelled", …); Err is set when the turn failed.
	EventTurnDone
	// EventExit: the connection is gone — the handshake failed or the process
	// died. Err says why (nil after a deliberate Close). No events follow.
	EventExit
)

type Model

type Model struct {
	ID    string
	Name  string
	Usage string // Copilot's premium-request multiplier ("1x", "0x"), if given
}

Model is one entry of the agent's model roster.

type Options

type Options struct {
	// Dir is the agent's working directory and the ACP session's cwd. It must
	// be absolute; the spec requires it.
	Dir string
	// Model is the preferred model id. Empty, or an id the roster no longer
	// offers, keeps the agent's default — a stale saved preference must never
	// break the handshake.
	Model string
}

Options configure a new Chat.

type Prompt

type Prompt struct {
	Text string
	Note string
}

Prompt is a question ready to send, and the line the transcript shows about what went with it.

func Build

func Build(question string, ctx Context, first bool) Prompt

Build renders a question plus its context as one prompt. first says whether this is the first turn of the conversation, which is the only one that carries the preamble — the agent keeps the session's history, so repeating it would cost tokens every turn for nothing.

type SignIn

type SignIn struct {
	UserCode        string
	VerificationURI string
	Status          AuthStatus
	// contains filtered or unexported fields
}

SignIn is a device-flow sign-in in progress.

When the account was already signed in, Status says so, UserCode is empty, and the server has already been closed; Wait then just returns Status. Otherwise the UI shows UserCode and VerificationURI and calls Wait.

func BeginSignIn

func BeginSignIn(a Agent, dir string) (*SignIn, error)

BeginSignIn starts the agent's language server in LSP mode and asks it for a device code. It blocks for the handshake and the code request — up to the initTimeout, since a cold start can sit on a Keychain prompt — so a UI calls it off its event loop.

func BeginSignInPipes

func BeginSignInPipes(dir string, r io.Reader, w io.Writer) (*SignIn, error)

BeginSignInPipes is BeginSignIn over an existing connection to a language server — r is what the server writes, w what it reads — as StartPipes is for a chat. It is how tests drive the flow against a scripted server (see package aitest). Closing the SignIn closes w.

func (*SignIn) Close

func (s *SignIn) Close()

Close abandons the sign-in and ends the server. Safe to call more than once, before or during Wait (which then returns an error).

func (*SignIn) Wait

func (s *SignIn) Wait() (AuthStatus, error)

Wait finishes the device flow: it blocks until the user has entered the code in the browser, or the code expires, then closes the server. The status says which ending it was — SignedIn, NoSubscription, or neither.

type Table

type Table struct {
	Name    string
	View    bool
	Columns []Column
}

Table is one table's shape: its name as the user would write it, and its columns in declared order.

Directories

Path Synopsis
Package aitest is a scripted ACP agent for testing code that uses package ai, without spawning or signing in to a real one.
Package aitest is a scripted ACP agent for testing code that uses package ai, without spawning or signing in to a real one.

Jump to

Keyboard shortcuts

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