Render a multi-file project into a single prompt-friendly string. The format lists all file paths first, then prints each file's contents with line numbers.
(
query: str,
project: dict[str, str],
prior_assistant_messages: list[Any] | None = None,
)
| 3 | |
| 4 | |
| 5 | def build_project_input( |
| 6 | query: str, |
| 7 | project: dict[str, str], |
| 8 | prior_assistant_messages: list[Any] | None = None, |
| 9 | ) -> str: |
| 10 | """Render a multi-file project into a single prompt-friendly string. |
| 11 | |
| 12 | The format lists all file paths first, then prints each file's contents with line numbers. |
| 13 | """ |
| 14 | prior_block = "" |
| 15 | if prior_assistant_messages: |
| 16 | # Accept either list[str] (legacy assistant-only) or list[dict{role,content}] (full dialogue) |
| 17 | if isinstance( |
| 18 | prior_assistant_messages[0] if len(prior_assistant_messages) > 0 else None, |
| 19 | dict, |
| 20 | ): |
| 21 | lines: list[str] = [] |
| 22 | for m in prior_assistant_messages: # type: ignore[assignment] |
| 23 | role = str(m.get("role", "")) |
| 24 | content = str(m.get("content", "")) |
| 25 | if not content: |
| 26 | continue |
| 27 | lines.append(f"- {role}: {content}") |
| 28 | if lines: |
| 29 | prior_block = ( |
| 30 | "\n---\nPrevious conversation (for context):\n" |
| 31 | + "\n".join(lines) |
| 32 | + "\n" |
| 33 | ) |
| 34 | else: |
| 35 | joined = "\n\n".join([f"- {m}" for m in prior_assistant_messages]) |
| 36 | prior_block = ( |
| 37 | f"\n---\nPrevious assistant answers (for context only):\n{joined}\n" |
| 38 | ) |
| 39 | |
| 40 | # Compose a bounded project view to stay under model limits |
| 41 | max_total_chars = int(os.getenv("AGENT_MAX_PROJECT_CHARS", "60000")) |
| 42 | max_per_file_chars = int(os.getenv("AGENT_MAX_PER_FILE_CHARS", "10000")) |
| 43 | max_list_entries = int(os.getenv("AGENT_MAX_PATH_LIST", "500")) |
| 44 | |
| 45 | sorted_paths = sorted(project.keys()) |
| 46 | # Truncate path list for very large projects; include a tail note |
| 47 | listed_paths = sorted_paths[:max_list_entries] |
| 48 | file_list = "\n".join( |
| 49 | listed_paths |
| 50 | + ( |
| 51 | [f"... ({len(sorted_paths) - len(listed_paths)} more omitted)"] |
| 52 | if len(sorted_paths) > len(listed_paths) |
| 53 | else [] |
| 54 | ) |
| 55 | ) |
| 56 | |
| 57 | files_rendered: list[str] = [] |
| 58 | remaining = max_total_chars |
| 59 | for path in sorted_paths: |
| 60 | if remaining <= 0: |
| 61 | break |
| 62 | content = project[path] or "" |
no test coverage detected