Skip to content

Commit 357a68c

Browse files
committed
Add progress reporting to more handlers and extend guide where progress reporting should be used
1 parent 1e29870 commit 357a68c

7 files changed

Lines changed: 212 additions & 180 deletions

File tree

docs/guides/designing-actions.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,23 @@ Partial results and progress are orthogonal:
207207

208208
Do not put status messages into partial results just to show activity, and do not use progress updates to smuggle result data. If the client should be able to consume it as structured output, it belongs in the result. If it only helps the user understand what the action is currently doing, it belongs in progress.
209209

210+
### Progress must reflect the user's unit of work
211+
212+
When a user sees "file X formatted" or "file X linted", they expect **all** handlers to have been applied to that file — not that one particular handler finished its pass. The progress grain must match the user's mental model, not the handler execution model.
213+
214+
This has a direct consequence on how multi-file, multi-handler actions structure their execution:
215+
216+
| Execution model | Handler order | Progress grain | User perception |
217+
| --- | --- | --- | --- |
218+
| **Per-item** (iterable payload) | All handlers run on file 1, then all on file 2, … | Per-file, after all handlers | "file X done" ✓ |
219+
| **Per-handler batch** (flat payload) | Handler A on all files, then handler B on all files, … | Per-handler pass, or only at end | "handler A done" or no useful progress ✗ |
220+
221+
**Rule: when an action processes multiple items through multiple handlers and reports file-level progress, prefer the iterable payload model.** This ensures each partial result — and each progress step — represents a fully processed item.
222+
223+
The per-handler batch model is appropriate when handlers genuinely need the full file list at once (e.g. a whole-program analysis pass), or when there is only a single handler. But if the action chains several independent per-file tools (formatter A → formatter B → save), the iterable model gives correct progress semantics by construction.
224+
225+
**Corollary: single-tool handlers in an iterable-payload action should not report their own progress.** The parent action already advances progress after all handlers complete for each item. A handler reporting "processed file X" independently would be redundant at best — and misleading at worst, since from the user's perspective the file is not done until every handler has run. Handlers should focus on doing their work and returning a result; the orchestrating action owns the progress narrative.
226+
210227
### Action boundaries stay explicit
211228

212229
When one action delegates to another, neither partial results nor progress should be treated as something that "just flows through".

finecode_builtin_handlers/src/finecode_builtin_handlers/create_envs_dispatch.py

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -41,25 +41,28 @@ async def run(
4141
)
4242

4343
tasks: list[asyncio.Task[create_envs_action.CreateEnvsRunResult]] = []
44-
try:
45-
async with asyncio.TaskGroup() as tg:
46-
for env in run_context.envs:
47-
task = tg.create_task(
48-
self.action_runner.run_action(
49-
action=create_env_action_instance,
50-
payload=create_env_action.CreateEnvRunPayload(
51-
env=env,
52-
recreate=payload.recreate,
53-
),
54-
meta=run_context.meta,
55-
)
56-
)
57-
tasks.append(task)
58-
except ExceptionGroup as eg:
59-
error_str = ". ".join([str(e) for e in eg.exceptions])
60-
raise code_action.ActionFailedException(error_str) from eg
44+
async with run_context.progress("Creating environments", total=len(run_context.envs)) as progress:
45+
async def _create_and_advance(env):
46+
result = await self.action_runner.run_action(
47+
action=create_env_action_instance,
48+
payload=create_env_action.CreateEnvRunPayload(
49+
env=env,
50+
recreate=payload.recreate,
51+
),
52+
meta=run_context.meta,
53+
)
54+
await progress.advance(message=f"Created {env.name}")
55+
return result
6156

62-
errors: list[str] = []
63-
for task in tasks:
64-
errors += task.result().errors
65-
return create_envs_action.CreateEnvsRunResult(errors=errors)
57+
try:
58+
async with asyncio.TaskGroup() as tg:
59+
for env in run_context.envs:
60+
tasks.append(tg.create_task(_create_and_advance(env)))
61+
except ExceptionGroup as eg:
62+
error_str = ". ".join([str(e) for e in eg.exceptions])
63+
raise code_action.ActionFailedException(error_str) from eg
64+
65+
errors: list[str] = []
66+
for task in tasks:
67+
errors += task.result().errors
68+
return create_envs_action.CreateEnvsRunResult(errors=errors)

finecode_builtin_handlers/src/finecode_builtin_handlers/format.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,18 @@ async def run(
6666
format_files_action_instance = self.action_runner.get_action_by_source(
6767
format_files_action.FormatFilesAction
6868
)
69-
async for partial in self.action_runner.run_action_iter(
70-
action=format_files_action_instance,
71-
payload=format_files_action.FormatFilesRunPayload(
72-
file_paths=file_uris,
73-
save=payload.save,
74-
),
75-
meta=run_meta,
76-
):
77-
yield format_action.FormatRunResult(
78-
result_by_file_path=partial.result_by_file_path
79-
)
69+
async with run_context.progress("Formatting files", total=len(file_uris)) as progress:
70+
async for partial in self.action_runner.run_action_iter(
71+
action=format_files_action_instance,
72+
payload=format_files_action.FormatFilesRunPayload(
73+
file_paths=file_uris,
74+
save=payload.save,
75+
),
76+
meta=run_meta,
77+
):
78+
files = list(partial.result_by_file_path.keys())
79+
msg = str(files[0]) if files else None
80+
await progress.advance(message=msg)
81+
yield format_action.FormatRunResult(
82+
result_by_file_path=partial.result_by_file_path
83+
)

finecode_builtin_handlers/src/finecode_builtin_handlers/install_env_install_deps.py

Lines changed: 40 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -45,42 +45,45 @@ async def run(
4545
install_deps_in_env_action.InstallDepsInEnvAction,
4646
)
4747

48-
deps_groups = project_def.get("dependency-groups", {})
49-
env_raw_deps = deps_groups.get(env.name, [])
50-
env_deps_config = (
51-
project_def.get("tool", {})
52-
.get("finecode", {})
53-
.get("env", {})
54-
.get(env.name, {})
55-
.get("dependencies", {})
56-
)
57-
project_def_path = resource_uri_to_path(env.project_def_path)
58-
dependencies: list[dict] = []
59-
process_raw_deps(
60-
env_raw_deps,
61-
env_deps_config,
62-
dependencies,
63-
deps_groups,
64-
project_def_path=project_def_path,
65-
)
48+
async with run_context.progress(f"Installing {env.name}") as progress:
49+
await progress.report("Reading configuration")
50+
deps_groups = project_def.get("dependency-groups", {})
51+
env_raw_deps = deps_groups.get(env.name, [])
52+
env_deps_config = (
53+
project_def.get("tool", {})
54+
.get("finecode", {})
55+
.get("env", {})
56+
.get(env.name, {})
57+
.get("dependencies", {})
58+
)
59+
project_def_path = resource_uri_to_path(env.project_def_path)
60+
dependencies: list[dict] = []
61+
process_raw_deps(
62+
env_raw_deps,
63+
env_deps_config,
64+
dependencies,
65+
deps_groups,
66+
project_def_path=project_def_path,
67+
)
6668

67-
install_deps_payload = install_deps_in_env_action.InstallDepsInEnvRunPayload(
68-
env_name=env.name,
69-
venv_dir_path=env.venv_dir_path,
70-
project_dir_path=path_to_resource_uri(project_def_path.parent),
71-
dependencies=[
72-
install_deps_in_env_action.Dependency(
73-
name=dep["name"],
74-
version_or_source=dep["version_or_source"],
75-
editable=dep["editable"],
76-
)
77-
for dep in dependencies
78-
],
79-
)
69+
install_deps_payload = install_deps_in_env_action.InstallDepsInEnvRunPayload(
70+
env_name=env.name,
71+
venv_dir_path=env.venv_dir_path,
72+
project_dir_path=path_to_resource_uri(project_def_path.parent),
73+
dependencies=[
74+
install_deps_in_env_action.Dependency(
75+
name=dep["name"],
76+
version_or_source=dep["version_or_source"],
77+
editable=dep["editable"],
78+
)
79+
for dep in dependencies
80+
],
81+
)
8082

81-
result = await self.action_runner.run_action(
82-
action=install_deps_in_env_action_instance,
83-
payload=install_deps_payload,
84-
meta=run_context.meta,
85-
)
86-
return InstallEnvsRunResult(errors=result.errors)
83+
await progress.report("Installing dependencies")
84+
result = await self.action_runner.run_action(
85+
action=install_deps_in_env_action_instance,
86+
payload=install_deps_payload,
87+
meta=run_context.meta,
88+
)
89+
return InstallEnvsRunResult(errors=result.errors)

finecode_builtin_handlers/src/finecode_builtin_handlers/install_envs_dispatch.py

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -43,24 +43,25 @@ async def run(
4343
tasks: list[
4444
asyncio.Task[install_envs_action.InstallEnvsRunResult]
4545
] = []
46-
try:
47-
async with asyncio.TaskGroup() as tg:
48-
for env in run_context.envs:
49-
task = tg.create_task(
50-
self.action_runner.run_action(
51-
action=install_env_action_instance,
52-
payload=install_env_action.InstallEnvRunPayload(
53-
env=env,
54-
),
55-
meta=run_context.meta,
56-
)
57-
)
58-
tasks.append(task)
59-
except ExceptionGroup as eg:
60-
error_str = ". ".join([str(e) for e in eg.exceptions])
61-
raise code_action.ActionFailedException(error_str) from eg
46+
async with run_context.progress("Installing environments", total=len(run_context.envs)) as progress:
47+
async def _install_and_advance(env):
48+
result = await self.action_runner.run_action(
49+
action=install_env_action_instance,
50+
payload=install_env_action.InstallEnvRunPayload(env=env),
51+
meta=run_context.meta,
52+
)
53+
await progress.advance(message=f"Installed {env.name}")
54+
return result
6255

63-
errors: list[str] = []
64-
for task in tasks:
65-
errors += task.result().errors
66-
return install_envs_action.InstallEnvsRunResult(errors=errors)
56+
try:
57+
async with asyncio.TaskGroup() as tg:
58+
for env in run_context.envs:
59+
tasks.append(tg.create_task(_install_and_advance(env)))
60+
except ExceptionGroup as eg:
61+
error_str = ". ".join([str(e) for e in eg.exceptions])
62+
raise code_action.ActionFailedException(error_str) from eg
63+
64+
errors: list[str] = []
65+
for task in tasks:
66+
errors += task.result().errors
67+
return install_envs_action.InstallEnvsRunResult(errors=errors)

0 commit comments

Comments
 (0)