Skip to content

Commit 7aa3509

Browse files
committed
Finish initial implementation of IWorkspaceActionRunner and allow to run actions in workspace sequentially
1 parent ed1cb11 commit 7aa3509

6 files changed

Lines changed: 69 additions & 7 deletions

File tree

docs/wm-er-protocol.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ The protocol is LSP-shaped with a small set of custom commands.
7676
2. `params` (object)
7777
3. `options` (object, optional)
7878
- Options (snake_case keys are expected):
79-
- `meta`: `{ "trigger": "user|system|unknown", "dev_env": "ide|cli|ai|precommit|ci" }`
79+
- `meta`: `{ "trigger": "user|system|unknown", "dev_env": "ide|cli|ai|precommit|ci", "orchestration_depth": int }`
80+
- `orchestration_depth`: cross-boundary hop counter, defaults to `0`. The ER propagates it unchanged via `RunActionMeta.orchestration_depth`.
8081
- `partial_result_token`: `int | string` (used to correlate `$/progress`)
8182
- `result_formats`: `["json", "string"]` (defaults to `["json"]`)
8283
- Result (success):
@@ -144,6 +145,24 @@ The protocol is LSP-shaped with a small set of custom commands.
144145
- Result: `{ "config": "<stringified JSON config>" }`
145146
- Used by ER during `finecodeRunner/updateConfig` to resolve project config.
146147

148+
- `finecode/runActionInProject`
149+
- Params:
150+
- `actionSource` (string): import path of the action class (e.g. `"myext.actions.lint.LintAction"`)
151+
- `payload` (object): serialized action payload (`dataclasses.asdict`)
152+
- `meta` (object): `{ "trigger": string, "devEnv": string, "orchestrationDepth": int }`
153+
- Result: `{ "result": <json result object>, "returnCode": 0|1 }`
154+
- Runs the action at project scope (all env-runners of the ER's own project). WM enforces `OrchestrationPolicy.max_recursion_depth` before dispatching.
155+
156+
- `finecode/runActionInWorkspace`
157+
- Params:
158+
- `actionSource` (string): import path of the action class
159+
- `payload` (object): serialized action payload
160+
- `meta` (object): `{ "trigger": string, "devEnv": string, "orchestrationDepth": int }`
161+
- `projectPaths` (list[string] | null): explicit POSIX project paths, or `null` for all projects that declare the action
162+
- `concurrently` (boolean, default `true`): run projects concurrently.
163+
- Result: `{ "resultsByProject": { "<posix path>": <json result>, ... } }`
164+
- Fans out the action across the specified projects (or all projects that declare it). WM enforces `OrchestrationPolicy.max_project_fanout` before dispatching.
165+
147166
**Notifications**
148167

149168
- `$/progress`

finecode_extension_api/src/finecode_extension_api/interfaces/iworkspaceactionrunner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,5 @@ async def run_action_in_projects(
2828
payload: PayloadT,
2929
meta: code_action.RunActionMeta,
3030
project_paths: list[pathlib.Path] | None = None,
31+
concurrently: bool = True,
3132
) -> dict[pathlib.Path, ResultT]: ...

finecode_extension_runner/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ dependencies = [
1414
"deepmerge==2.0.*",
1515
"debugpy==1.8.*",
1616
"ordered-set==4.1.*",
17+
"apischema==0.19.*",
1718
]
1819

1920
[dependency-groups]

finecode_extension_runner/src/finecode_extension_runner/impls/workspace_action_runner.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from __future__ import annotations
22

33
import dataclasses
4+
import json
45
import pathlib
56
import typing
67
from typing import Any, Awaitable, Callable
78

9+
import apischema
810
from finecode_extension_api import code_action
911
from finecode_extension_api.interfaces import iactionrunner, iworkspaceactionrunner
1012
from finecode_extension_runner import run_utils
@@ -25,6 +27,7 @@ async def run_action_in_projects(
2527
payload: PayloadT,
2628
meta: code_action.RunActionMeta,
2729
project_paths: list[pathlib.Path] | None = None,
30+
concurrently: bool = True,
2831
) -> dict[pathlib.Path, ResultT]:
2932
action_source: str = action.source
3033
action_cls = run_utils.import_module_member_by_source_str(action_source)
@@ -41,9 +44,17 @@ async def run_action_in_projects(
4144
"projectPaths": [p.as_posix() for p in project_paths]
4245
if project_paths is not None
4346
else None,
47+
"concurrently": concurrently,
4448
},
4549
)
50+
# raw.resultsByProject is a stringified JSON string — pygls converts
51+
# JSON-RPC responses to Object and mangles path-keyed dicts (slashes
52+
# are invalid Python identifiers), so we stringify on the WM side and
53+
# parse back here, following the same pattern as projects/getRawConfig.
54+
results_by_project: dict = json.loads(raw.resultsByProject)
4655
return {
47-
pathlib.Path(k): action_cls.RESULT_TYPE(**v)
48-
for k, v in raw.get("resultsByProject", {}).items()
56+
pathlib.Path(k): apischema.deserialize(
57+
action_cls.RESULT_TYPE, next(iter(v.values()), {})
58+
)
59+
for k, v in results_by_project.items()
4960
}

src/finecode/wm_server/runner/_internal_client_types.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1412,6 +1412,29 @@ class RunActionInWorkspaceParams:
14121412
payload: dict
14131413
meta: RunActionInProjectMeta
14141414
project_paths: list[str] | None = None
1415+
concurrently: bool = True
1416+
1417+
1418+
@dataclasses.dataclass
1419+
class RunActionInWorkspaceResult(BaseResult):
1420+
# Stringified JSON, not a dict — same pattern as GetProjectRawConfigResult.
1421+
# Reason: pygls deserializes JSON-RPC responses into attribute-style Object
1422+
# instances. Dict keys that are POSIX paths (containing "/") are not valid
1423+
# Python identifiers, so pygls mangles them to "_0", "_1", etc., losing the
1424+
# original paths. Returning the payload as a JSON string bypasses pygls
1425+
# deserialization; the ER side does json.loads() to recover the real dict.
1426+
results_by_project: str
1427+
1428+
1429+
@dataclasses.dataclass
1430+
class RunActionInWorkspaceRequest(BaseRequest):
1431+
params: RunActionInWorkspaceParams
1432+
method = RUN_ACTION_IN_WORKSPACE
1433+
1434+
1435+
@dataclasses.dataclass
1436+
class RunActionInWorkspaceResponse(BaseResponse):
1437+
result: RunActionInWorkspaceResult
14151438

14161439

14171440
@dataclasses.dataclass
@@ -1613,4 +1636,10 @@ class ExitNotification(BaseNotification):
16131636
None,
16141637
None,
16151638
),
1639+
RUN_ACTION_IN_WORKSPACE: (
1640+
RunActionInWorkspaceRequest,
1641+
RunActionInWorkspaceParams,
1642+
RunActionInWorkspaceResponse,
1643+
RunActionInWorkspaceResult,
1644+
),
16161645
}

src/finecode/wm_server/runner/runner_manager.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -306,18 +306,19 @@ async def handle_run_action_in_workspace(
306306
run_trigger=run_trigger,
307307
dev_env=dev_env,
308308
orchestration_depth=params.meta.orchestration_depth,
309+
concurrently=params.concurrently,
309310
)
310311
except ActionRunFailed as exc:
311312
raise ValueError(exc.message) from exc
312-
return {
313-
"resultsByProject": {
313+
return _internal_client_types.RunActionInWorkspaceResult(
314+
results_by_project=json.dumps({
314315
k.as_posix(): {
315316
action: resp.result_by_format.get("json", {})
316317
for action, resp in v.items()
317318
}
318319
for k, v in results.items()
319-
}
320-
}
320+
})
321+
)
321322

322323
runner.client.feature(
323324
_internal_client_types.RUN_ACTION_IN_WORKSPACE,

0 commit comments

Comments
 (0)