Skip to content

Commit f04e4e7

Browse files
authored
Merge pull request #146 from FSoft-AI4Code/fix/module-clustering-code-size-coverage
Fix module clustering size estimates and source file coverage
2 parents 364b67a + e2153e0 commit f04e4e7

23 files changed

Lines changed: 1032 additions & 162 deletions

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ tests/*
4848
!tests/test_artifact_analyzer.py
4949
!tests/test_scala_analyzer.py
5050
!tests/test_agent_limits.py
51+
!tests/test_module_scope.py
5152

5253
# Jupyter
5354
*.ipynb

‎codewiki/cli/adapters/doc_generator.py‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,10 @@ async def _run_backend_generation(self, backend_config: BackendConfig):
278278
super_group_modules,
279279
)
280280
from codewiki.src.config import FIRST_MODULE_TREE_FILENAME, MODULE_TREE_FILENAME
281+
from codewiki.src.be.module_scope import (
282+
get_module_token_count,
283+
validate_module_file_coverage,
284+
)
281285
from codewiki.src.utils import file_manager
282286

283287
working_dir = str(self.output_dir.absolute())
@@ -298,19 +302,20 @@ async def _run_backend_generation(self, backend_config: BackendConfig):
298302
self.progress_tracker.update_stage(0.5, "Loaded cached module tree")
299303
else:
300304
if self.verbose:
301-
clustering_tokens = get_clustering_input_token_count(leaf_nodes, components)
305+
clustering_tokens = get_module_token_count(components)
302306
self.progress_tracker.update_stage(
303307
0.3,
304308
(
305309
f"Preparing {len(leaf_nodes)} leaf nodes for clustering "
306-
f"({clustering_tokens} tokens, threshold "
310+
f"(code_tokens={clustering_tokens}, leaf_tokens="
311+
f"{get_clustering_input_token_count(leaf_nodes, components)}, threshold "
307312
f"{backend_config.max_token_per_module})"
308313
),
309314
)
310315
if clustering_tokens <= backend_config.max_token_per_module:
311316
self.progress_tracker.update_stage(
312317
0.4,
313-
"Skipping LLM clustering; selected leaf nodes fit within the module token threshold",
318+
"Skipping LLM clustering; code size fits within the module token threshold",
314319
)
315320
else:
316321
self.progress_tracker.update_stage(
@@ -342,6 +347,7 @@ async def _run_backend_generation(self, backend_config: BackendConfig):
342347
from codewiki.src.be.module_naming import dedupe_module_tree_names
343348

344349
module_tree = dedupe_module_tree_names(module_tree)
350+
validate_module_file_coverage(module_tree, components)
345351
file_manager.save_json(module_tree, first_module_tree_path)
346352
file_manager.save_json(module_tree, module_tree_path)
347353

‎codewiki/src/be/agent_tools/deps.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,17 @@ class CodeWikiDeps:
2020
# (create / str_replace / insert / undo_edit) to a docs file whose
2121
# absolute, resolved path is not in this set. ``None`` = unrestricted.
2222
allowed_write_paths: set[str] | None = None
23+
# Entry points belonging to this agent. Sizes expand these IDs to all
24+
# components in their files; delegation may never claim a sibling's file.
25+
scope_component_ids: list[str] | None = None
26+
27+
def module_component_ids(self) -> list[str]:
28+
if self.scope_component_ids is not None:
29+
return self.scope_component_ids
30+
if not self.path_to_current_module:
31+
return list(self.components)
32+
level = self.module_tree
33+
for name in self.path_to_current_module:
34+
info = level[name]
35+
level = info.get("children", {})
36+
return info.get("components", [])

‎codewiki/src/be/agent_tools/generate_sub_module_documentations.py‎

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@
1212
format_system_prompt,
1313
format_user_prompt,
1414
)
15-
from codewiki.src.be.utils import is_complex_module, count_tokens
16-
from codewiki.src.be.cluster_modules import format_potential_core_components
15+
from codewiki.src.be.utils import is_complex_module
16+
from codewiki.src.be.module_scope import (
17+
component_files,
18+
get_module_token_count,
19+
prepare_submodule_partition,
20+
)
1721

1822
import logging
1923
from codewiki.src.be.api_usage import usage_target
@@ -38,6 +42,8 @@ async def generate_sub_module_documentation(
3842

3943
deps = ctx.deps
4044
previous_module_name = deps.current_module_name
45+
if deps.current_depth >= deps.max_depth:
46+
return "Maximum delegation depth reached; write this module directly."
4147

4248
# Create fallback models from config
4349
fallback_models = create_fallback_models(deps.config)
@@ -64,10 +70,20 @@ async def generate_sub_module_documentation(
6470
if requested_name in name_map
6571
}
6672

67-
# add the sub-module to the module tree
6873
value = deps.module_tree
6974
for key in deps.path_to_current_module:
7075
value = value[key]["children"]
76+
final_specs = prepare_submodule_partition(
77+
final_specs, deps.module_component_ids(), deps.components, value
78+
)
79+
for requested, final_name in list(name_map.items()):
80+
if final_name not in final_specs:
81+
plan.skipped[requested] = "no unassigned files in the current module scope"
82+
del name_map[requested]
83+
if not final_specs:
84+
return skipped_report(plan.skipped, parent_doc)
85+
86+
# Reserve every child's complete file scope before running any agent.
7187
for sub_module_name, core_component_ids in final_specs.items():
7288
value[sub_module_name] = {"components": core_component_ids, "children": {}}
7389

@@ -78,14 +94,15 @@ async def generate_sub_module_documentation(
7894

7995
logger.info(f"{indent}{arrow} Generating documentation for sub-module: {sub_module_name}")
8096

81-
num_tokens = count_tokens(
82-
format_potential_core_components(core_component_ids, ctx.deps.components)[-1]
97+
num_tokens = get_module_token_count(
98+
deps.components, component_files(core_component_ids, deps.components)
8399
)
100+
child_depth = deps.current_depth + 1
84101

85102
if (
86103
is_complex_module(ctx.deps.components, core_component_ids)
87-
and ctx.deps.current_depth < ctx.deps.max_depth
88-
and num_tokens >= ctx.deps.config.max_token_per_leaf_module * ctx.deps.current_depth
104+
and child_depth < ctx.deps.max_depth
105+
and num_tokens >= ctx.deps.config.max_token_per_leaf_module * child_depth
89106
):
90107
sub_agent = Agent(
91108
model=fallback_models,
@@ -119,6 +136,8 @@ async def generate_sub_module_documentation(
119136
tools=[read_code_components_tool, str_replace_editor_tool],
120137
)
121138

139+
previous_scope = deps.scope_component_ids
140+
deps.scope_component_ids = core_component_ids
122141
deps.current_module_name = sub_module_name
123142
deps.path_to_current_module.append(sub_module_name)
124143
deps.current_depth += 1
@@ -143,6 +162,7 @@ async def generate_sub_module_documentation(
143162
deps.path_to_current_module.pop()
144163
deps.current_depth -= 1
145164
deps.current_module_name = previous_module_name
165+
deps.scope_component_ids = previous_scope
146166

147167
# restore the previous module name
148168
deps.current_module_name = previous_module_name

‎codewiki/src/be/caw_backend.py‎

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,19 @@
3131

3232
from codewiki.src.be.agent_tools.deps import CodeWikiDeps
3333
from codewiki.src.be.backend import AgentReply, LLMBackend
34-
from codewiki.src.be.cluster_modules import format_potential_core_components
34+
from codewiki.src.be.module_scope import (
35+
complete_file_candidates,
36+
component_files,
37+
get_module_token_count,
38+
validate_module_file_coverage,
39+
)
3540
from codewiki.src.be.dependency_analyzer.models.core import Node
3641
from codewiki.src.be.prompt_template import (
3742
format_leaf_system_prompt,
3843
format_system_prompt,
3944
format_user_prompt,
4045
)
41-
from codewiki.src.be.utils import count_tokens, is_complex_module, set_main_loop
46+
from codewiki.src.be.utils import is_complex_module, set_main_loop
4247
from codewiki.src.be.doc_layout import config_layout, find_doc
4348
from codewiki.src.config import MODULE_TREE_FILENAME, OVERVIEW_FILENAME, Config
4449
from codewiki.src.utils import file_manager
@@ -399,21 +404,18 @@ def _run_module_agent_sync(
399404
return module_tree
400405

401406
custom_instructions = config.get_prompt_addition()
407+
if not module_path:
408+
core_component_ids = complete_file_candidates(core_component_ids, components)
402409

403-
# Mirror PydanticAIBackend's early-cut: a module is only worth
404-
# delegating to sub-agents when it spans multiple files AND has enough
405-
# content to justify the cost AND we still have recursion budget.
406-
# Without this gate the caw path would give every multi-file sub-module
407-
# the recursive SYSTEM_PROMPT + delegation tool and fan out one extra
408-
# agent call per sub-spec even when a single leaf write would suffice.
409-
# See generate_sub_module_documentation_tool for the pydantic-ai
410-
# equivalent.
411-
_, components_with_code = format_potential_core_components(core_component_ids, components)
412-
num_tokens = count_tokens(components_with_code)
410+
# Whole-repository agents may optionally delegate even below the leaf
411+
# token budget. Nested modules retain the cost gate and depth limit.
412+
num_tokens = get_module_token_count(
413+
components, component_files(core_component_ids, components)
414+
)
413415
can_delegate = (
414416
is_complex_module(components, core_component_ids)
415417
and start_depth < config.max_depth
416-
and num_tokens >= config.max_token_per_leaf_module
418+
and (not module_path or num_tokens >= config.max_token_per_leaf_module)
417419
)
418420
logger.info(
419421
f"Module {module_name} can delegate: {can_delegate} - is_complex_module: {is_complex_module(components, core_component_ids)} - start_depth: {start_depth} - num_tokens: {num_tokens} - max_depth: {config.max_depth} - max_token_per_leaf_module: {config.max_token_per_leaf_module}"
@@ -440,6 +442,7 @@ def _run_module_agent_sync(
440442
current_depth=start_depth,
441443
config=config,
442444
custom_instructions=custom_instructions,
445+
scope_component_ids=core_component_ids,
443446
)
444447

445448
toolkit = CawToolKit(deps=deps, backend=self, allow_subagent=can_delegate)
@@ -521,6 +524,7 @@ def _run_module_agent_sync(
521524
traj.total_tool_calls,
522525
(usage or {}).get("cost_usd", 0.0),
523526
)
527+
validate_module_file_coverage(deps.module_tree, components)
524528
file_manager.save_json(deps.module_tree, module_tree_path)
525529
return deps.module_tree
526530
except Exception as e:

‎codewiki/src/be/caw_toolkit.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from codewiki.src.be.agent_tools.deps import CodeWikiDeps
3131
from codewiki.src.be.doc_layout import config_layout, module_doc_file
3232
from codewiki.src.be.module_naming import skipped_report, plan_sub_module_specs, sub_module_report
33+
from codewiki.src.be.module_scope import prepare_submodule_partition
3334

3435
if TYPE_CHECKING:
3536
from codewiki.src.be.caw_backend import CawBackend
@@ -325,6 +326,17 @@ def _run_sub_modules(self, sub_module_specs: dict[str, list[str]]) -> str:
325326
value = deps.module_tree
326327
for key in deps.path_to_current_module:
327328
value = value[key]["children"]
329+
final_specs = prepare_submodule_partition(
330+
final_specs, deps.module_component_ids(), deps.components, value
331+
)
332+
for requested, final_name in list(name_map.items()):
333+
if final_name not in final_specs:
334+
plan.skipped[requested] = "no unassigned files in the current module scope"
335+
del name_map[requested]
336+
if not final_specs:
337+
return skipped_report(
338+
plan.skipped, module_doc_file(previous_module_name, parent_path, layout)
339+
)
328340
for sub_name, core_ids in final_specs.items():
329341
value[sub_name] = {"components": core_ids, "children": {}}
330342

0 commit comments

Comments
 (0)