Skip to content

Commit fac643f

Browse files
anhnh2002claude
andcommitted
Reference children docs by path in overview prompts instead of inlining
Parent/repo overview prompts embedded the entire module tree — every module's components list — as indent-4 JSON plus the full markdown of every child doc. On large repos (wazuh: ~2.2M chars) that exceeds codex's hard per-turn input cap of 1,048,576 chars, so every overview call failed with input_too_large, surfaced misleadingly as "LLM returned empty content". - build_overview_structure now strips components lists recursively and sets an absolute docs_path per child instead of inlining docs (None when the doc is missing). Wazuh-scale prompts drop from ~2.2M to ~83K chars. - REPO_OVERVIEW_PROMPT / MODULE_OVERVIEW_PROMPT instruct the agent to read the referenced files itself (backend.complete runs with ToolGroup.READER, so file reads are available). Placeholders are unchanged, keeping the MCP prompt_server rendering path working. - Pin coding-agent-wrapper to the fork branch carrying the matching codex provider fixes (stdin prompts, resume sandbox flag, and surfacing CLI failures that were silently returned as empty turns). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent c9d0c4a commit fac643f

4 files changed

Lines changed: 114 additions & 11 deletions

File tree

‎codewiki/src/be/documentation_generator.py‎

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,17 @@ def is_leaf_module(self, module_info: Dict[str, Any]) -> bool:
117117

118118
def build_overview_structure(self, module_tree: Dict[str, Any], module_path: List[str],
119119
working_dir: str) -> Dict[str, Any]:
120-
"""Build structure for overview generation with 1-depth children docs and target indicator."""
121-
120+
"""Build structure for overview generation with 1-depth children doc paths and target indicator.
121+
122+
Children docs are referenced by absolute file path (``docs_path``)
123+
rather than inlined, and ``components`` lists are stripped: inlining
124+
the full tree plus docs blew past provider input caps (codex rejects
125+
turns over 1,048,576 chars) on large repos. The overview agent reads
126+
the referenced files itself.
127+
"""
128+
122129
processed_module_tree = deepcopy(module_tree)
130+
self._strip_components(processed_module_tree)
123131
module_info = processed_module_tree
124132
for path_part in module_path:
125133
module_info = module_info[path_part]
@@ -134,13 +142,25 @@ def build_overview_structure(self, module_tree: Dict[str, Any], module_path: Lis
134142
for child_name, child_info in module_info.items():
135143
child_docs_path = self._resolve_child_docs_path(working_dir, child_name)
136144
if child_docs_path is not None:
137-
child_info["docs"] = file_manager.load_text(child_docs_path)
145+
child_info["docs_path"] = child_docs_path
138146
else:
139147
logger.warning(f"Module docs not found at {os.path.join(working_dir, f'{child_name}.md')}")
140-
child_info["docs"] = ""
148+
child_info["docs_path"] = None
141149

142150
return processed_module_tree
143151

152+
@classmethod
153+
def _strip_components(cls, tree: Dict[str, Any]) -> None:
154+
"""Recursively drop ``components`` lists — they dominate the tree's
155+
serialized size and add nothing to an overview prompt."""
156+
for module_info in tree.values():
157+
if not isinstance(module_info, dict):
158+
continue
159+
module_info.pop("components", None)
160+
children = module_info.get("children")
161+
if isinstance(children, dict):
162+
cls._strip_components(children)
163+
144164
@staticmethod
145165
def _resolve_child_docs_path(working_dir: str, child_name: str) -> str | None:
146166
"""Resolve the on-disk path for a child module's .md doc.
@@ -274,17 +294,18 @@ async def generate_parent_module_docs(self, module_path: List[str],
274294
logger.info(f"✓ Parent docs already exists at {parent_docs_path}")
275295
return module_tree
276296

277-
# Create repo structure with 1-depth children docs and target indicator
297+
# Create repo structure with 1-depth children doc paths and target indicator
278298
repo_structure = self.build_overview_structure(module_tree, module_path, working_dir)
279299

280300
prompt = MODULE_OVERVIEW_PROMPT.format(
281301
module_name=module_name,
282-
repo_structure=json.dumps(repo_structure, indent=4)
302+
repo_structure=json.dumps(repo_structure, indent=2)
283303
) if len(module_path) >= 1 else REPO_OVERVIEW_PROMPT.format(
284304
repo_name=module_name,
285-
repo_structure=json.dumps(repo_structure, indent=4)
305+
repo_structure=json.dumps(repo_structure, indent=2)
286306
)
287-
307+
logger.debug(f"Overview prompt for {module_name}: {len(prompt)} chars")
308+
288309
try:
289310
parent_docs = self.backend.complete(prompt)
290311
if not parent_docs:

‎codewiki/src/be/prompt_template.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,13 @@
9898
- The end-to-end architecture of the repository visualized by mermaid diagrams
9999
- The references to the core modules documentation
100100
101-
Provide `{repo_name}` repo structure and its core modules documentation:
101+
Provide `{repo_name}` repo structure:
102102
<REPO_STRUCTURE>
103103
{repo_structure}
104104
</REPO_STRUCTURE>
105105
106+
The core modules' documentation is NOT inlined above. Each top-level module carries a `docs_path` field with the absolute path to its documentation file — read those files with your file-reading tools before writing the overview (skip entries whose `docs_path` is null).
107+
106108
Please generate the overview of the `{repo_name}` repository in markdown format with the following structure:
107109
<OVERVIEW>
108110
overview_content
@@ -117,11 +119,13 @@
117119
- The architecture of the module visualized by mermaid diagrams
118120
- The references to the core components documentation
119121
120-
Provide repo structure and core components documentation of the `{module_name}` module:
122+
Provide repo structure of the `{module_name}` module (marked with `is_target_for_overview_generation`):
121123
<REPO_STRUCTURE>
122124
{repo_structure}
123125
</REPO_STRUCTURE>
124126
127+
The child modules' documentation is NOT inlined above. Each child of the target module carries a `docs_path` field with the absolute path to its documentation file — read those files with your file-reading tools before writing the overview (skip entries whose `docs_path` is null).
128+
125129
Please generate the overview of the `{module_name}` module in markdown format with the following structure:
126130
<OVERVIEW>
127131
overview_content

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ dependencies = [
5858
"python-multipart>=0.0.20",
5959
"colorama>=0.4.6",
6060
"logfire>=4.1.0",
61-
"coding-agent-wrapper>=0.1.2",
61+
"coding-agent-wrapper @ git+https://github.com/anhnh2002/caw@fix/codex-exec-robustness",
6262
"mcp>=1.0.0"
6363
]
6464

‎tests/test_overview_structure.py‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""Tests for build_overview_structure's prompt-size discipline.
2+
3+
Overview prompts used to inline the whole module tree (every module's
4+
``components`` list) plus the full markdown of every child doc. On large
5+
repos that blew past provider input caps — codex rejects any turn over
6+
1,048,576 characters — and every parent/repo overview failed. The structure
7+
must now strip ``components`` and reference children docs by absolute
8+
``docs_path`` instead of inlining them.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import json
14+
import os
15+
16+
from codewiki.src.be.documentation_generator import DocumentationGenerator
17+
18+
CODEX_INPUT_CAP = 1_048_576
19+
20+
21+
def _generator() -> DocumentationGenerator:
22+
# build_overview_structure touches neither config nor backend; skip the
23+
# heavyweight __init__ (graph builder, backend resolution).
24+
return DocumentationGenerator.__new__(DocumentationGenerator)
25+
26+
27+
def _make_tree(n_modules: int, n_components: int) -> dict:
28+
return {
29+
f"module_{i}": {
30+
"components": [f"src/file_{i}_{j}.c::func_{j}" for j in range(n_components)],
31+
"children": {
32+
f"module_{i}_child": {
33+
"components": [f"src/file_{i}_{j}.c::helper_{j}" for j in range(n_components)],
34+
"children": {},
35+
}
36+
},
37+
}
38+
for i in range(n_modules)
39+
}
40+
41+
42+
def test_components_are_stripped_at_every_depth(tmp_path):
43+
tree = _make_tree(n_modules=3, n_components=5)
44+
result = _generator().build_overview_structure(tree, [], str(tmp_path))
45+
assert "components" not in json.dumps(result)
46+
# The original tree is untouched (deepcopy semantics).
47+
assert tree["module_0"]["components"]
48+
49+
50+
def test_children_docs_referenced_by_absolute_path_not_inlined(tmp_path):
51+
tree = _make_tree(n_modules=2, n_components=1)
52+
doc = tmp_path / "module_0.md"
53+
doc.write_text("# module_0\n\nA very long body that must not appear in the prompt.")
54+
55+
result = _generator().build_overview_structure(tree, [], str(tmp_path))
56+
57+
assert result["module_0"]["docs_path"] == str(doc)
58+
assert os.path.isabs(result["module_0"]["docs_path"])
59+
assert result["module_1"]["docs_path"] is None # missing doc -> null, no crash
60+
assert "must not appear" not in json.dumps(result)
61+
62+
63+
def test_target_module_is_marked(tmp_path):
64+
tree = _make_tree(n_modules=2, n_components=1)
65+
result = _generator().build_overview_structure(tree, ["module_1"], str(tmp_path))
66+
assert result["module_1"]["is_target_for_overview_generation"] is True
67+
# Children of the target get docs_path entries.
68+
assert "docs_path" in result["module_1"]["children"]["module_1_child"]
69+
70+
71+
def test_wazuh_scale_tree_stays_under_codex_input_cap(tmp_path):
72+
# Modeled on the real failure: ~30 top-level modules, hundreds of
73+
# components each, which serialized to ~2.1M chars before the fix.
74+
tree = _make_tree(n_modules=30, n_components=600)
75+
assert len(json.dumps(tree, indent=2)) > CODEX_INPUT_CAP # the old shape overflowed
76+
77+
result = _generator().build_overview_structure(tree, [], str(tmp_path))
78+
assert len(json.dumps(result, indent=2)) < CODEX_INPUT_CAP / 10

0 commit comments

Comments
 (0)