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 ¶
const CopilotID = "copilot"
CopilotID is the default agent.
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 ¶
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.
var ErrBusy = errors.New("the assistant is still answering — stop it first")
ErrBusy is returned by Send while an answer is still streaming.
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 ¶
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.
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 ¶
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 ¶
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) Cancel ¶
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 ¶
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) Send ¶
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.
type Column ¶
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).
// 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 ¶
Prompt is a question ready to send, and the line the transcript shows about what went with it.
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 ¶
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 ¶
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.