- Status: stable, versioned session contract
- Contract:
wright-agent/v1 - Schema:
schemas/wright-agent-v1.schema.json
wright-agent/v1 is the transport-neutral request and response contract of
wright_driver::service::ToolService. Its Rust ToolRequest and ToolResponse
types are the executable definition. capabilities reports the contract
identity and supported operations before a client uses them.
The separate wright-result/v1 contract describes command result envelopes.
Capabilities.contract continues to identify that result envelope;
Capabilities.agent_contract identifies this service contract. Results from
compile, check, analyze, and inspect carry a wright-result/v1
envelope inside the agent response.
Start a session with wright serve [INPUT]. INPUT is a source file or
project directory; omission uses the current directory. Standard input is
reserved for requests, so - is not a valid source input. The server loads one
project and processes requests until standard input closes. The default
transport is one JSON request per line on stdio:
{"op":"capabilities"}
{"op":"findings"}
{"op":"check"}Stdio returns one ToolResponse per request. A successful response has a
result member; an application-level refusal has an error member containing
code and message. The schema defines supported request fields; current
deserialization ignores unknown fields for forward compatibility. Clients
should send only fields defined by the negotiated schema.
Use wright serve --transport jsonrpc [INPUT] for JSON-RPC 2.0. The canonical
agent method is request, with a ToolRequest in params:
{"jsonrpc":"2.0","id":1,"method":"request","params":{"op":"findings"}}Its JSON-RPC result contains the same successful service result as the stdio
ToolResponse.result. A service refusal is returned in that result as
{"error":{"code":"...","message":"..."}}. JSON-RPC framing errors use
the top-level error member and standard JSON-RPC error codes. The direct
JSON-RPC methods compile, check, analyze, and inspect remain aliases for
the corresponding operations and return the same wright-result/v1 envelope
they returned before this contract was introduced.
One-shot CLI workflows such as wright check --format json continue to return
their wright-result/v1 envelope. They use the same CompilerSession
workflows as the session service. The semantic query commands wright inspect symbols, wright inspect refs, wright inspect cfg, wright inspect callgraph, and wright inspect cost likewise run through ToolService
operations (symbols, references + usage, cfg, callGraph,
costEstimate) and report the same result payloads (#429); the CLI adds no
divergent semantics. The in-process embedding API can call
ToolService::handle directly.
Use wright serve --transport mcp [INPUT] for MCP over stdio (#473,
ADR-0020). The adapter speaks newline-delimited JSON-RPC 2.0 and implements
initialize (with protocol-version negotiation), ping, tools/list, and
tools/call. Each operation in the initial set below is one tool named
wright_ plus the operation in snake case:
| Operation | Tool |
|---|---|
project |
wright_project |
symbols |
wright_symbols |
references |
wright_references |
usage |
wright_usage |
callGraph |
wright_call_graph |
check |
wright_check |
lint |
wright_lint |
costEstimate |
wright_cost_estimate |
semanticRename |
wright_semantic_rename |
validateEditTransaction |
wright_validate_edit_transaction |
tools/list contains a tool only when its operation is in this set and
advertised by capabilities.operations. Each tool's inputSchema is derived
from the operation's request schema with op removed (the tool name carries
it); the edit tools also omit sources, which then defaults to the on-disk
text (#472). tools/call arguments are the request fields.
A successful service result is returned unchanged as the tool result's JSON
text content. A service refusal is a tool result with isError: true whose
content carries the same {code, message}. Edit operations report their
outcome inside the result payload (ok plus diagnostics), which passes
through as ordinary content — the adapter does not reinterpret it. MCP
protocol errors (unknown tool or method, malformed arguments) are JSON-RPC
error responses and never become tool results.
The released wright binary includes serve, so each supported installation
channel can use the session contract without a separate runtime.
All requests are JSON objects with a required op string. Request fields and
the common response/error shapes are defined by the committed JSON Schema. The
table lists every operation advertised by capabilities.operations and names
the successful result payload.
| Operation | Request fields | Successful result |
|---|---|---|
capabilities |
none | Service name/version, wright-agent/v1, result contract, operation names, languages, and profiles |
compile |
none | wright-result/v1 compile envelope |
check |
none | wright-result/v1 check envelope |
analyze |
none | wright-result/v1 analysis envelope |
inspect |
none | wright-result/v1 inspection envelope |
project |
none | Loaded program origin, files, counts, and findings summary |
rules |
none | Canonical Workshop rules |
symbols |
optional kind |
Symbols, optionally filtered by kind |
references |
required symbol (id or name) |
References for the symbol |
usage |
required symbol (id or name) |
Usage counts for the symbol, plus its resolved id and kind |
cfg |
required rule (index or name) |
Control-flow graph for the rule |
findings |
optional selection | Wright static-analysis findings; {"findings": [...], "selection": {...}} when a selection is applied |
persistentObjects |
none | Persistent Workshop object facts |
lint |
optional selection | Lint findings, per-rule id/effective severity, effective configuration, and selection when applied |
lintRules |
none | Registered lint rules with full metadata and effective configuration |
callGraph |
none | Subroutine call graph |
costEstimate |
optional selection | Exact generated-resource counts, findings, and selection when applied |
targetMetadata |
none | Canonical target/catalog metadata |
validateEditTransaction |
transaction, optional sources |
Atomic validation status, diagnostics, and previews when valid |
semanticRename |
target, optional sources |
Validated rename transaction or structured refusal |
providerSemanticRename |
language_id, documents, position_document_uri, position, new_name, optional project_root, sources |
Provider-resolved rename transaction or structured refusal |
providerValidateEdit |
language_id, documents, transaction, sources, optional project_root |
Provider-validated transaction or structured refusal |
A request that consults the loaded program is served from the project as it
exists on disk at request time. The service fingerprints the input's
observable file set — a file input's own content, a directory input's
resolved members, or every file under a source-language project's entry
directory — and reloads when it changes, so an edit, an added file, or a
removed file is reflected in the next symbols, references, check,
lint, or other program-reading request without restarting the session. An
unchanged input is never reloaded. capabilities (service metadata),
targetMetadata (the static catalog), and provider* operations
(caller-supplied documents) do not consult the loaded program and are
answered regardless of disk state.
A reload that fails — the entry was removed, or the source no longer loads —
refuses the request with the loader's structured diagnostic code
(input-io, parse-error, input-kind-ambiguous, a provider diagnostic).
The session keeps refusing until the input loads again; the previously
loaded program is never served silently.
Numeric symbol ids and rule indexes are valid only for the program that
issued them. After a content-changing reload, a request carrying a numeric
symbol, rule, or semanticRename target id is refused stale-id until
the client observes the new space: a successful symbols response
re-establishes symbol ids, a successful rules response re-establishes
rule indexes, and an ambiguous-symbol/ambiguous-rule refusal
re-establishes its own space because it already names the current
candidates. Name addressing resolves against the current program in both
states and is never stale.
The responses that issue numeric ids or indexes into the loaded program are
symbols (symbol ids), rules (rule indexes), usage (the resolved id),
references, findings, lint, persistentObjects, inspect, analyze,
and the ambiguous-* refusal candidates — plus skipped[].rule in lint
and lintRules. cfg block ids are local to that one response and are not
program addresses; project, callGraph, costEstimate, targetMetadata,
capabilities, the compile/check envelopes, and the edit/provider
payloads issue no program ids. Positional fields such as rule, action,
value, and span.file inside findings and references are coordinates into
the current program rather than reusable addresses, but they are always
computed from the program that was live at request time.
references and usage accept symbol as either a numeric symbol id or the
declared symbol name; cfg accepts rule as either the rule index or the
declared rule name. Names resolve against the loaded program's semantic index
in the driver, so a request never has to learn the program's numbering —
symbol ids and rule indexes are different spaces (a rule's symbol id is not
its rule index). Numeric ids keep their established meaning, and both
addressings return the same payload for the same target.
An unmatched name returns a structured unknown-symbol or unknown-rule
error; a name shared by more than one symbol — or more than one rule, for
cfg — returns ambiguous-symbol/ambiguous-rule listing the candidate
numeric ids. Resolution never guesses or returns an empty success. usage
additionally echoes the resolved id and kind so a name-addressed caller
can correlate the result with symbols.
findings, lint, and costEstimate accept optional selection fields:
severity: a threshold —errorreports errors only,warningerrors and warnings,infoeverything.rule: one lint rule id (the findingcode). An unknown id is a structuredinvalid-selectionerror, never a silent empty result.file: one source file. The reportedspan.pathspelling differs per surface, so the argument resolves to the same canonical file — the path as passed, root-relative, or absolute spellings all select it.costEstimatefindings carry no span, sofileselects nothing there.max: a bound on the reported count, applied after filtering.
The CLI options --severity, --rule-id, --file, and --max on lint,
check, and analyze drive the same wright-driver selection, so both
surfaces return the same selected set for the same input and selection.
When a request applies any selection field, the result reports
selection: {"total": <set before selection>, "withheld": <dropped by max>}:
findings becomes {"findings": [...], "selection": {...}}, while lint
and costEstimate add a selection member to their existing result objects.
Requests without selection fields receive the previous shapes unchanged.
Edit transactions use source identities and half-open, 1-based line/column ranges. Provider positions use 0-based line/character coordinates. Wright proposes and validates edits; a caller remains responsible for applying them. Stale, overlapping, unsupported, or semantically invalid edits return an explicit refusal without a partial edit set.
On raw Workshop input both operations are supported directly — workshop-rs
owns the source semantics and Wright orchestrates the transaction:
validateEditTransactionapplies the caller's transaction to the currentsourcesand reparses/revalidates the edited project through the session's ownworkshop-rspath.sourcesis optional (#472): when omitted or null, the service reads the current text of the files the transaction names (eachedits[].source) from disk; when supplied, it remains the precondition text, so an embedder holding unsaved buffers keeps that ability. A transaction that produces malformed or invalid Workshop refuses with the real parse/validation diagnostics and no partial preview; a valid one returnsok: truewith apreviewof each edited source (edited text plus the post-edit identity).semanticRenameresolvestargeteither bysymbol— a numeric id or a declared name, exactly the addressingreferences/usageuse — or by asource/line/colposition inside one identifier occurrence. Global variables, player variables, and subroutines rename through the exact identifier spansworkshop-rsrecords: the declaration, theSubroutineevent binding,Call Subroutinecallees,Start Rulesubroutine arguments,Set/Modify/Forvariable arguments, andGlobal.name/Event Player.namevalue references all rewrite, while prefixes, comments, strings, and unrelated identifiers stay untouched. There is no textual-search fallback — an occurrence whose recorded span does not cover exactly the identifier refuses withrename-unmapped-span.sourcesis optional (#472): when omitted or null, the service reads the loaded input file's current text from disk; when supplied, it must carry the current text of the loaded input file. A text that differs from the loaded program's refuses withedit-stale-sourcebecause provenance spans would no longer index it. Atoname that already declares a same-kind symbol refuses withrename-name-collision, and one that does not survive reparsing refuses with the parse diagnostic; rule names are not rename targets (rename-unsupported-kind).- Non-Workshop input stays at the provider boundary: OPY refuses with
edit-requires-providernamingproviderValidateEdit/providerSemanticRename, and kinds without a shipped provider keep theirsource-provider-unavailablerefusal.
wright rename <NAME> <NEW_NAME> [INPUT] exposes the same semantic rename
on the CLI: it prints the validated diff by default and applies it
atomically with --write, refusing edit-stale-source when the file
changed since validation.
The service response is either { "result": value } or
{ "error": { "code": string, "message": string } }. Error codes are the
machine-readable discriminator; error message wording is for people and is not
stable. Transport framing errors are separate from service refusals.
Workflow envelopes retain wright-result/v1 fields, including structured
diagnostics. Provider-owned diagnostics remain distinguishable through their
provider status and origin metadata. Wright findings are returned by the
findings/lint operations rather than converted into owner diagnostics. Source
spans retain their source path when mapped; missing or invalid source mapping
is reported as unmapped. Unsupported provider operations remain explicit
diagnostics or mutation refusals, not guessed source locations or textual
fallbacks.
capabilities is the version negotiation step. Clients should check
agent_contract and the advertised operation list before sending requests.
Clients should ignore unknown response fields and must not assume an operation
exists unless it is advertised.
wright-agent/v1 permits additive optional response fields and additional
operations whose requests remain valid for existing clients. Removing or
renaming an operation or field, changing a field's type or meaning, or changing
the response/error model requires a new major contract such as
wright-agent/v2; the v1 schema and its compatibility tests remain in place.
The CLI result envelope has its independent wright-result/v1 version; its
evolution policy is defined in docs/cli/machine-contract.md.
One recorded exception applies to the same rule in both contracts, decided
before the 1.0 contract freeze (#134): the lint result's rules member
lists only each rule's id and effectiveSeverity (#431). Full rule
metadata — summary, rationale, documentation, known limits, evidence, tags —
is served once by lintRules.
ADR-0020 records a second pre-freeze exception (#472): sources is optional
on validateEditTransaction and semanticRename rather than required, so
agent callers stop resending whole files.
The optional guide distributed by wrightkit/skills teaches clients to
discover and use these capabilities. It is not required to expose, execute, or
validate any semantic operation.