feat(agent): group semantically related files for multi-file review - #808
feat(agent): group semantically related files for multi-file review#808lizhengfeng101 wants to merge 11 commits into
Conversation
|
🔍 OpenCodeReview found 4 issue(s) in this PR.
|
| if a.args.CommentWorkerPool != nil { | ||
| a.args.CommentWorkerPool.AwaitKey(newPath) | ||
| a.args.CommentWorkerPool.AwaitKey(groupKey) | ||
| } |
There was a problem hiding this comment.
[bug · critical]
CommentWorkerPool key mismatch: The AwaitKey call here uses groupKey (derived from fileGroupKey(g.Diffs)), but the LLM loop's executeToolCall (in internal/llmloop/loop.go:434) still submits work via pool.SubmitFor(newPath, ...) where newPath is the individual file path. Since groupKey for multi-file groups is a comma-separated sorted string of paths, AwaitKey(groupKey) will wait on a key that was never submitted, potentially blocking indefinitely or returning immediately (depending on the AwaitKey implementation for unknown keys). Either the submission key must also be groupKey, or a different awaiting strategy is needed.
| switch { | ||
| case d.IsNew: | ||
| status = "ADDED" | ||
| case d.IsDeleted: | ||
| status = "DELETED" | ||
| case d.IsRenamed: | ||
| status = "RENAMED" | ||
| } |
There was a problem hiding this comment.
[maintainability · low]
The IsDeleted case in the switch statement is unreachable dead code. In agent.go:521-527, deleted files are filtered out before calling groupDiffs, so d.IsDeleted will never be true for any diff passed to this function. This dead code is misleading and adds unnecessary complexity. Either remove the case or add a comment explaining it's defensive programming for future-proofing.
| content = strings.TrimSpace(content) | ||
| // Strip markdown code fences if present | ||
| if strings.HasPrefix(content, "```") { | ||
| lines := strings.Split(content, "\n") | ||
| if len(lines) >= 2 { | ||
| lines = lines[1:] | ||
| } | ||
| if len(lines) > 0 && strings.HasPrefix(strings.TrimSpace(lines[len(lines)-1]), "```") { | ||
| lines = lines[:len(lines)-1] | ||
| } | ||
| content = strings.Join(lines, "\n") | ||
| } |
There was a problem hiding this comment.
[maintainability · medium]
The fence-stripping logic in parseGroupingResponse duplicates functionality already available in stripMarkdownFences (util.go:34). The existing utility is more robust and handles language-tagged fences like ```json. Additionally, this manual implementation doesn't handle cases where the LLM adds explanatory text before/after the JSON block, which will cause parsing failures and trigger the per-file fallback unnecessarily. Consider using stripMarkdownFences or implementing JSON extraction logic similar to `parseFilterResponse` to improve robustness.
Suggestion:
| content = strings.TrimSpace(content) | |
| // Strip markdown code fences if present | |
| if strings.HasPrefix(content, "```") { | |
| lines := strings.Split(content, "\n") | |
| if len(lines) >= 2 { | |
| lines = lines[1:] | |
| } | |
| if len(lines) > 0 && strings.HasPrefix(strings.TrimSpace(lines[len(lines)-1]), "```") { | |
| lines = lines[:len(lines)-1] | |
| } | |
| content = strings.Join(lines, "\n") | |
| } | |
| content = stripMarkdownFences(content) |
| if total <= int64(tokenLimit) { | ||
| result = append(result, g) | ||
| } else { | ||
| for _, d := range g.Diffs { | ||
| result = append(result, FileGroup{ | ||
| Label: g.Label + " (split: " + d.NewPath + ")", | ||
| Diffs: []model.Diff{d}, | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
[performance · medium]
When a group's total tokens exceed tokenLimit, every file in the group is split into its own single-file group, even if some files are small and could be safely combined. This can result in many unnecessary LLM calls. Consider a smarter splitting strategy that attempts to pack files into groups that fit within the budget, rather than immediately falling back to per-file dispatch for all files in the group.
7924e26 to
dfcf8aa
Compare
Introduce LLM-based semantic file grouping so that related files (e.g.
implementation + test, i18n variants, interface + impl) are reviewed
together in a single LLM call instead of individually. This enables
cross-file consistency checks and reduces total LLM calls.
Key changes:
- Add grouping.go with LLM-based file grouping (GROUPING_TASK template)
that clusters changed files by semantic relationship, with fallback
to per-file dispatch on any error
- Refactor dispatchSubtasks to iterate over FileGroup instead of
individual Diff, updating budget estimation, error handling, panic
recovery, and session recording to work at group granularity
- Move the 'path' field from tool-level to per-comment level in
code_comment tool schema, since one review call now covers multiple
files — remove the forced path override in loop.go
- Rewrite plan phase output from JSON to a structured Review Directive
with MUST/SHOULD/MAY severity tiers and concrete verification actions
- Update main_task prompts to accept {{diffs}} (multi-file XML) instead
of {{diff}} + {{current_file_path}}, with cross-file review guidance
- Add enforceGroupTokenBudget and enforceMaxFilesPerGroup safety limits
- Treat partial completion (comments produced before round exhaustion)
as partial success instead of hard error
ParseComments skips comments with empty path. After removing the top-level args path override, these tests need path in each comment object to match the new per-item path contract. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(llmloop): backfill comment thinking from the turn's reasoning or message Expose ChatResponse.ReasoningContent and backfill per-comment thinking with the current turn's reasoning content, falling back to the assistant message for models that do not expose reasoning, so --format json output carries thinking even when the model omits it. * fix(llmloop): drop content fallback for comment thinking backfill The turn's assistant message is usually a short user-facing preamble rather than real reasoning, so backfill per-comment thinking only from the model's native reasoning_content and leave it empty otherwise. Add a full-wiring RunPerFile test for the reasoning backfill and a regression test that fails if the content fallback returns. Sync the thinking docs across en/zh/ja/ru. * docs(llmloop): note that turn-level thinking is shared by design Document in the main loop and at the code_comment backfill site that the model emits reasoning once per turn, so every tool call and comment in the same turn intentionally shares the same thinking.
The multi-file grouping change moved path from a top-level arg to per-comment objects and removed the args["path"] = newPath injection. This broke single-file RunPerFile calls where the model does not emit path per comment. Add ParseCommentsWithPath that applies newPath as a default when comments lack an explicit path, restoring the previous behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The path field is already marked required in the tool JSON schema with a clear description. Repeating it in the system prompt wastes tokens without adding signal. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The placeholder renders a plain one-per-line list, not a table. The old name misleadingly suggests markdown table formatting. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…<review_files> The system prompt mentioned <review_files> but no such tag exists in the rendered user message. The files are wrapped in <file path="..."> elements by buildConcatenatedDiffs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add <review_files> wrapper in the user prompt template around {{diffs}}
so the system prompt can reference it as the explicit review scope,
consistent with <other_changed_files> and <user_task> containers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Consolidate scope constraints into Strict Focus Rules; remove duplicated 'don't' items from Capabilities - Remove redundant path instruction from user prompt (already in tool schema) - Fix末尾 instruction to reference <review_files> instead of <file> - Fix plan output format: each category numbers independently - Replace // comments with plain text in user prompts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The grouping refactor deleted explanatory comments about budget look-ahead semantics, why SetRunFailure is NOT used, and the panic-isolation contract. These explain non-obvious WHY decisions and are restored in condensed form. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
dfcf8aa to
df8e0d1
Compare
Summary
.go+_test.go, header + implementation) are reviewed together in a single LLM conversation for better cross-file analysis.grouping.gowith grouping logic andenforceMaxFilesPerGroup(max 10 files/group) + token budget enforcement.[quick]/[deep]cost tags; remove MAY category to reduce wasted investigation rounds.main_task_system.mdto encourage "comment as you go" instead of batching comments until all items are investigated.CommentCollectoralready has comments, return them instead of discarding. Rounds-exhausted groups with existing comments are no longer marked as hard errors.code_commenttool to accept apathfield for multi-file group attribution.Motivation
Per-file review misses cross-file inconsistencies (e.g., interface change without updating callers). Grouping related files in one conversation enables the model to catch these issues. The prompt and failure-handling changes address recall regression observed during evaluation (model over-investigating without producing comments, and produced comments being discarded on partial failures).
Test plan
make checkpasses (format, vet, license headers)make testpasses (unit tests including new grouping tests)