[bldr]docs
gRPC APIDaemon

Diagnostics

── Diagnostics ───────────────────────────────────────────────────────────── Editor tooling for bldr's own inputs (bldr-workspace.yml, build scripts), served by the daemon rather than by an IDE extension. The daemon is the only process that already knows what a workspace *means* — its members, the content each one resolves to, the build graph they produce — so an extension that re-derived any of that would be a second implementation, permanently a little behind this one. The editor therefore speaks ordinary LSP to a thin passthrough (bldr internal lsp), which does nothing but framing, and every answer comes from the node.

── Diagnostics ───────────────────────────────────────────────────────────── Editor tooling for bldr's own inputs (bldr-workspace.yml, build scripts), served by the daemon rather than by an IDE extension. The daemon is the only process that already knows what a workspace means — its members, the content each one resolves to, the build graph they produce — so an extension that re-derived any of that would be a second implementation, permanently a little behind this one. The editor therefore speaks ordinary LSP to a thin passthrough (bldr internal lsp), which does nothing but framing, and every answer comes from the node.

Service builder.Diagnostics, 1 rpcs.

StreamLanguageServerProtocol(stream LspClientMessage) -> stream LspServerMessage

One whole LSP session as one bidirectional stream: the editor's JSON-RPC requests and notifications in, the server's responses and notifications out. Closing either half ends the session; there is no resume.

Only the JSON-RPC payloads travel here. LSP's Content-Length framing is a property of the byte pipe the editor speaks (stdio or TCP) and is stripped by the passthrough — gRPC already delimits messages, so carrying the framing too would mean two message boundaries that can disagree.

Request: LspClientMessage

One message from the editor side of a session.

The header MUST be the first message on the stream, and MUST be sent exactly once. A jsonrpc frame before it, or a second header after it, is a protocol error: the daemon fails the RPC with INVALID_ARGUMENT naming the rule that was broken, rather than guessing at a session it cannot place.

A oneof (rather than optional fields on one flat message) is what makes "header first, then frames" expressible on the wire at all: every message is unambiguously one or the other, so neither side has to infer which it got from which fields happen to be set.

message LspClientMessage {
  oneof message {
    LspSessionHeader header = 1;
    string jsonrpc = 2;
  }
}
Field
header 
jsonrpcOne complete JSON-RPC message, byte-for-byte as the editor wrote it, with the Content-Length framing removed.

Response: LspServerMessage

One message from the daemon side of a session: a single complete JSON-RPC message (a response, or a server-initiated notification), unframed.

No oneof here, because the server has nothing to say that is not JSON-RPC: anything it must report out-of-band — a rejected header, a session it cannot serve — is a gRPC status, which ends the stream, and that is the distinction worth keeping.

message LspServerMessage {
  optional string jsonrpc = 1;
}

Types used above

LspSessionHeader

The opening frame of an LSP session, sent once before any JSON-RPC payload.

LSP has nowhere to put "where is the editor sitting". initialize carries a root URI, but it arrives only after the editor has decided to talk, and a daemon serving several editors at once cannot read the caller's directory off its own process. So a session states its context up front, before the first word of the protocol it is tunnelling.

message LspSessionHeader {
  optional string cwd = 1;
  optional map<string, string> options = 2;
}
Field
cwdAbsolute working directory of the passthrough process. The daemon resolves the enclosing workspace from it exactly as bldr build does, so an editor opened anywhere inside a workspace gets that workspace's answers.
optionsFree-form key=value settings from the IDE extension (-o KEY=VALUE on the passthrough). Nothing reads these yet. They exist so that the first setting an extension needs to pass — a log level, a feature toggle, an explicit workspace override — does not force a wire change, and with it a lock-step upgrade of daemon, CLI and extension for what is one string.

On this page