FineCode WM-ER Protocol¶
This document describes the communication protocol between the FineCode Workspace Manager (WM) and Extension Runners (ER). WM is the JSON-RPC client; each ER is a JSON-RPC server.
The WM-ER protocol uses JSON-RPC 2.0 with LSP-style wire framing. Lifecycle method
names (initialize, initialized, shutdown, exit) and text-document notification
names follow LSP conventions; all FineCode-specific commands use direct JSON-RPC
method names.
Transport¶
- JSON-RPC 2.0
- LSP-style framing over stdio:
Content-Length: N\r\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n{json} - WM spawns ER processes with:
python -m finecode_extension_runner.cli start --project-path=... --env-name=...--debugenables a debugpy attach flow before WM connects- All parameter object keys use camelCase.
Lifecycle¶
- WM starts the ER process (per project + env).
- WM sends
initializeand waits for the ER response. - WM sends
initialized. - WM sends
finecodeRunner/updateConfigto bootstrap handlers and services. - ER processes it and returns
{}. - WM sends
finecodeRunner/resolveActionMetato get action meta info. - ER returns a complete map of
configSource → { canonical_source, runs_concurrently }for every action whose class can be imported in this env. Actions that fail to import are omitted. - WM stores these on its
Actiondomain objects before the runner is considered ready. - On shutdown: WM sends
shutdownthenexit.
Multi-env runs: when an action's handlers span more than one env, the WM orchestrates execution segment-by-segment using
actions/runHandlersso that the serialized run context (previousResult) crosses the wire only at actual env boundaries. See Multi-Env Action Orchestration.
Message Catalog¶
WM -> ER¶
Requests
initialize- Standard LSP initialize request.
-
Example params (trimmed):
-
shutdown -
Standard LSP shutdown request.
-
finecodeRunner/updateConfig - Params:
{ "workingDir": string, "projectName": string, "projectDefPath": string, "config": object } - Config shape (top-level):
actions: list of action objects (name,handlers,source,config)action_handler_configs: map of handler source → configservices: list of service declarations (optional)handlers_to_initialize: map of action name → handler names (optional)
-
Result:
{}(empty object) -
finecodeRunner/getInfo - Params:
{} - Result:
{ "logFilePath": "/abs/path/to/runner.log" | null } -
Returns runtime information about the runner. Currently reports the path to the runner's log file, or
nullif logging to a file is not configured. -
actions/run - Params:
{ "actionName": string, "params": object, "options": object | null } - Options keys (camelCase):
meta:{ "trigger": "user|system|unknown", "devEnv": "ide|cli|ai|git_hook|ci", "orchestrationDepth": int }orchestrationDepth: cross-boundary hop counter, defaults to0. The ER propagates it unchanged viaRunActionMeta.orchestration_depth.partialResultToken:int | string(used to correlate$/progress)resultFormats:["json", "string"](defaults to["json"])
- Result (success):
- Result (streamed): used when
partialResultTokenwas provided and all results were delivered via$/progressnotifications. The final response is an explicit completion signal —result_by_formatis intentionally empty. The WM treats this as a valid completion; an emptyresult_by_formatwith any other status is a protocol error. - Result (stopped):
- Result (error):
-
Note:
result_by_formatis a JSON-encoded string (not a nested object) — the WM decodes it withjson.loadsafter receiving the response. -
actions/runHandlers - Runs a named subset of an action's handlers sequentially within this ER,
seeding
context.current_resultfrom a prior segment's serialized result. Used by the WM to orchestrate multi-env action runs; not used for single-env actions (those still useactions/run). - Params:
actionName(string): action name as registered viafinecodeRunner/updateConfighandlerNames(list of string): ordered list of handler names to execute; all must belong to this ER's envpreviousResult(object | null): serializedRunActionResult(dataclasses.asdict) from the last handler of the preceding segment, ornullfor the first segment. Reconstructed ascontext.current_resultbefore the first handler inhandlerNamesis invoked.options(object | null): same keys asactions/run.resultFormatsshould be omitted (or[]) for intermediate segments and non-empty only for the final segment of a run.
- Result (success):
{ "status": "success", "result": {"<resultField>": "..."}, "resultByFormat": {"json": {"...": "..."}, "string": "..."}, "returnCode": 0 }result: serializedRunActionResultafter all specified handlers ran (dataclasses.asdict). Pass aspreviousResultto the next segment'sactions/runHandlerscall.resultByFormat: formatted results in the requested formats;{}whenresultFormatswas empty in options.
- Result (streamed): used when
partialResultTokenwas provided and all results were delivered via$/progress.resultis still populated for context chaining. - Result (stopped):
-
Result (error):
{"error": "message"} -
actions/getPayloadSchemas - Params:
{} - Result:
{ action_name: JSON Schema fragment | null } -
Returns a payload schema for every action currently known to the runner. Each schema has
properties(field name → JSON Schema type object) andrequired(list of field names without defaults).nullmeans the action class could not be imported. -
actions/mergeResults - Params:
{ "actionName": string, "results": list } results: list of serializedRunActionResultobjects (dataclasses.asdict), one per concurrent segment or handler. Used by the WM after a concurrent multi-env run to merge the per-env results into a single final result.-
Result:
{ "merged": <serialized RunActionResult> }or{ "error": "..." } -
actions/reload - Params:
{ "actionName": string } -
Result:
{} -
finecodeRunner/resolveActionMeta - Params:
{}(no params) - Result: complete map of
{ "<configSource>": { "canonical_source": string, "runs_concurrently": bool }, ... }for every action whose class can be imported in this env. Actions that fail to import are omitted entirely. Example:{ "myext.LintAction": { "canonical_source": "myext.actions.lint.LintAction", "runs_concurrently": true } } -
Called by the WM after
finecodeRunner/updateConfigcompletes to store canonical sources and execution modes on itsActiondomain objects before the runner is considered ready. The WM usescanonical_sourceas the primary identifier in all subsequent action lookups. Actions absent from the response (import failure) keepcanonical_source = Noneuntil another runner for the same project resolves them. -
actions/resolveSource - Params:
{ "source": string }— an arbitrary import-path alias to resolve. - Result:
{ "canonicalSource": string }— the fully qualified class path (cls.__module__ + "." + cls.__qualname__). - Raises a JSON-RPC error if the alias cannot be imported or resolved.
-
Used during action lookup when a caller provides an alias not already known from
finecodeRunner/resolveActionMeta(full ADR-0019 support). -
packages/resolvePath - Params:
{ "packageName": string } - Result:
{ "packagePath": "/abs/path/to/package" }
Notifications
initialized(standard LSP)textDocument/didOpentextDocument/didChangetextDocument/didClose$/cancelRequest- Sent by WM when an in-flight request should be cancelled.
ER -> WM¶
Requests
workspace/applyEdit- Standard LSP request for applying workspace edits.
-
WM forwards this to its active client (IDE) if available.
-
projects/getRawConfig - Params:
{ "projectDefPath": "/abs/path/to/project/finecode.toml" } - Result:
{ "config": "<stringified JSON config>" } -
Used by ER during
finecodeRunner/updateConfigto resolve project config. -
finecode/runActionInProject - Params:
actionSource(string): fully qualified import path of the action class —f"{cls.__module__}.{cls.__qualname__}"(e.g."myext.actions.lint.LintAction"). Must not be a re-exported alias such as"myext.LintAction". The WM resolves the action name by matching against the canonical source reported byfinecodeRunner/resolveActionMeta; a re-exported path will not match and the request will fail.payload(object): serialized action payload (dataclasses.asdict)meta(object):{ "trigger": string, "devEnv": string, "orchestrationDepth": int }
- Result:
{ "result": <json result object>, "returnCode": 0|1 } -
Runs the action at project scope (all env-runners of the ER's own project). WM enforces
OrchestrationPolicy.max_recursion_depthbefore dispatching. -
finecode/runActionInWorkspace - Params:
actionSource(string): fully qualified import path of the action class — same constraint asfinecode/runActionInProjectabove.payload(object): serialized action payloadmeta(object):{ "trigger": string, "devEnv": string, "orchestrationDepth": int }projectPaths(list[string] | null): explicit POSIX project paths, ornullfor all projects that declare the actionconcurrently(boolean, defaulttrue): run projects concurrently.
- Result:
{ "resultsByProject": { "<posix path>": <json result>, ... } } - Fans out the action across the specified projects (or all projects that declare it). WM enforces
OrchestrationPolicy.max_project_fanoutbefore dispatching.
Notifications
$/progress- Params:
{ "token": <token>, "value": "<stringified JSON partial result>" } - The
tokenmust matchpartialResultTokenfromactions/runoractions/runHandlers. valueis a JSON string produced by the ER from a partial run result.- When
$/progressis used to deliver results, the finalactions/runoractions/runHandlersresponse must havestatus: "streamed"and emptyresult_by_format. See result (streamed) entries above.
Multi-Env Action Orchestration¶
When an action's handlers span more than one env, the WM cannot delegate the
whole run to a single ER via actions/run. Instead the WM becomes the
orchestrator and drives execution using actions/runHandlers.
Sequential mode (default)¶
The WM groups the action's handlers into consecutive same-env segments:
handlers: [h1/env1, h2/env1, h3/env1, h4/env2]
segments: [(env1, [h1, h2, h3]), (env2, [h4])]
handlers: [h1/env1, h2/env2, h3/env1]
segments: [(env1, [h1]), (env2, [h2]), (env1, [h3])]
Execution:
- WM calls
actions/runHandlersfor segment 1 withpreviousResult: null. - For each subsequent segment, WM calls
actions/runHandlerson that segment's ER withpreviousResultset to theresultreturned by the previous call. The ER reconstructs this ascontext.current_resultbefore the first handler in the segment runs. - If any call returns
status: "stopped", WM stops the chain and returns that result to the caller. resultFormatsis passed only in the final segment's options — earlier segments returnresultByFormat: {}to avoid unnecessary serialization.- WM assembles the final response from the last segment's
resultandresultByFormat.
Concurrent mode¶
The WM groups handlers by env (order within an env does not matter for concurrent execution):
Execution:
- WM dispatches
actions/runHandlersto all env groups in parallel, all withpreviousResult: null. - WM collects all
resultobjects from the parallel calls. - WM calls
actions/mergeResultson any available ER for the action, passing the collectedresultobjects. - The merged result and its formatted representation form the final response.
Single-env actions¶
When all handlers are in the same env, the WM uses actions/run — a single
delegated call where the ER manages handler sequencing internally.
actions/runHandlers is only used when handlers span multiple envs.
walRunId continuity¶
The WM generates a single walRunId for the whole logical action run and
passes it in every actions/runHandlers call's options. Each ER emits WAL
events tagged with that ID for the handler(s) it executes, so traces can be
correlated across envs for the same logical run.
Error Handling and Cancellation¶
- JSON-RPC errors are used for protocol-level failures.
- Command-level errors are returned via
{ "error": "..." }in command results. - WM cancels in-flight requests by sending
$/cancelRequestwith the request id.
Document Sync Notes¶
WM forwards open-file events to ER so actions can operate on in-memory document
state. ER may send workspace/applyEdit when handlers modify files; WM applies
these edits via its active client when possible.