mirror of
https://github.com/alibaba/open-code-review.git
synced 2026-09-14 19:59:52 +08:00
main
5 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9cd9297305 |
refactor: retire per-file naming left over from grouped review (#1168)
* refactor(llmloop): rename RunPerFile to RunMainTask The loop stopped being per-file when review switched from one subtask per file to one subtask per group: internal/agent passes a groupKey that may name several files, while internal/scan still passes a single path. The name asserted a granularity only one of the two callers has. RunMainTask names what the function actually drives — one MAIN_TASK conversation — matching the vocabulary already in place around it (MainLoopStop, the main.loop span, session.MainTask). RunSubtask would have been a second misfit, since review calls this once per review round and one subtask spans several rounds. The newPath parameter is renamed taskKey along the whole private chain (runGraceRound, executeToolCall, addNextMessage, runCompression, triggerAsyncCompression, requestCtx). Deps.NewRequestMeta keeps filePath: the session layer still spells that slot "file path" in its persisted records, and requestCtx now documents the seam instead of hiding it. ToolFailureDetail.FilePath is where a group key becomes externally visible — it reaches --format json as file_path — so the field now says so rather than letting a consumer assume it parses as one path. Two comments claimed executeToolCall overrides a comment's path with the loop's path argument. It has always been a fallback that applies only when the model named no path, and the difference decides whether a grouped review files its comments under the right files, so both now describe the fallback. Pure rename plus comment edits; no behavior change. * refactor(cli): retire per-file naming left over from grouped review perFileTimeout was the last holdout of the old granularity in the CLI options: its flag is --timeout, its help already said "concurrent task timeout", and it feeds Args.ConcurrentTaskTimeout. Only the field name still claimed a per-file deadline it never set. Two review help strings also outlived the change to grouped subtasks, and disagreed with cli-reference.md, which already documents both as per-group: --concurrency dispatches file groups, and --max-tokens caps a group's prompt. The scan command keeps its per-file wording, where one subtask is still one file. Same for six agent comments that describe per-group work as per-file — the budget look-ahead, the dispatch step and its function doc, the only deadline classifyItemError can see, and the goroutines allDiffs is read from — plus Run's pipeline summary, which omitted the grouping step altogether. Granularity that really is per-file stays as it is, and the two are easy to confuse: filterLargeDiffs measures each file's diff against the token ceiling on its own, review items and their status badges are per file, and a group's prompt embeds one XML element per file. The comment in review_cmd.go now names filterLargeDiffs so the distinction is checkable rather than a matter of trusting the adjective. The estimator keeps both its name avgMainRoundsPerFile and its arithmetic: it still bills every file for its own MAIN_TASK rounds, so a group of files is over-estimated. That is a costing bug rather than a naming one and needs its own change; only estimateDiffFileTokens' doc comment is corrected here, since the look-ahead consuming it sums per group. |
||
|
|
c7dd11e03e |
fix(llm): preserve provider reasoning/thinking across assistant turns (#1070)
* fix(llm): preserve provider reasoning/thinking across assistant turns Assistant tool-call history was rebuilt from Content()/ToolCalls() alone, so provider reasoning state was dropped on replay across all three supported protocols: - OpenAI-compatible Chat Completions: reasoning_content was parsed but never sent back on the next request. - Anthropic Messages: thinking/redacted_thinking blocks were flattened to a display string, losing the signature required to replay them. - OpenAI Responses: reasoning items (encrypted_content) were dropped entirely from the next request's input. Adds Message.Native (NativeTurn), an opaque per-protocol replay payload built by reusing each SDK's own response->request converter (anthropic.Message.ToParam(), each Responses output item's ToParam()) so the wire format is never hand-rolled. Each adapter type-asserts its own payload before reuse and falls back to the existing reconstruction otherwise, so cross-provider or native-less turns are unaffected. Also: - Adds Message.ReasoningContent, a plain-text projection used by the compression summarizer so a reasoning-only turn isn't rendered blank. - Fixes NativeTurn.EstimatedTokens() double-counting visible content already counted via ExtractText(), which could trigger compression earlier than necessary. - Guards the Responses adapter against carrying a dangling reasoning item with no accompanying message/function_call into Native. - Persists reasoning_content in the session JSONL transcript and surfaces it in the session viewer, for audit visibility into no-visible-answer turns. - Makes Anthropic extended thinking (extra_body.thinking) safe to enable via the existing provider config: dropped per-request when it would conflict with a forced tool_choice or exceed that call's max_tokens, rather than failing the whole request. - Removes GroupingTask's hardcoded 4096 max_tokens cap in favor of the template's own completion limit, which was the actual trigger for the max_tokens/budget_tokens conflict above. Refs #805 * fix(llm): tighten follow-ups from reasoning/thinking replay fix - Compare Anthropic extended thinking's budget_tokens against the effective max_tokens (falling back to the same 8192 default buildAnthropicParams uses) instead of the raw, possibly-zero ChatRequest.MaxTokens, so the guard doesn't get skipped when the caller leaves MaxTokens unset. - Preserve a degenerate turn in history when it has reasoning text but neither visible content nor a replayable Native payload (e.g. an openai-responses reasoning item with no accompanying message/function_call), instead of dropping all trace of it. - Unify token accounting between CountMessagesTokens and computeActiveZoneSize via a shared messageTokens() helper, so reasoning-heavy conversations don't get judged compressible by one and still-over-budget by the other. - Only write reasoning_content to the session JSONL transcript when non-empty, matching the existing pattern for comments/error/ sourceSessionId. * fix(session): persist LLM native replay payload for training reflow Session JSONL carried a human-readable ReasoningContent projection but dropped the opaque replay state (llm.Message.Native) that holds an Anthropic thinking block's signature or an OpenAI Responses reasoning item's encrypted_content — the fields a full session export/training reflow actually needs. - ResponseRecord gains a Native field; SetResponse populates it from resp.Native() and persists it verbatim as native_payload on the llm_response JSONL record. - copyMessagesForJSON now includes tool_calls (previously dropped) and native_payload per assistant message, so llm_request records are a complete, replayable projection of what was actually sent. - copyMessages (the in-memory deep copy backing TaskRecord.RequestMessages) now also carries Native/ReasoningContent, closing a gap where its own "deep copy" contract silently dropped both fields. Nothing parses or reshapes the payload on the way to disk — it is written exactly as the provider adapter would replay it. * refactor: trim verbose comments and add VisibleContent() unit tests - Remove redundant 'what' comments and Go-basics explanations across the NativeTurn/reasoning replay implementation - Keep only non-obvious 'why' comments (external API constraints, aliasing hazards, fallback semantics) - Add 6 direct unit tests for ChatResponse.VisibleContent() covering: no choices, nil content, empty string, normal text, think-tag stripping, and whitespace-only (key difference from Content()) Net: -185 lines of comments, +66 lines of tests. * docs: clarify json:"-" on ResponseMessage.Native * style: fix gofmt alignment in client.go and history.go |
||
|
|
a66240084b |
feat(agent): group semantically related files for multi-file review (#808)
* feat(agent): group semantically related files for multi-file review
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
* test(agent): improve grouping test coverage
* fix(llmloop): add per-item path to thinking backfill tests
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 turn output (#773)
* 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.
* fix(tool): fall back to newPath when comment omits path field
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>
* refactor(prompt): remove redundant Comment Attribution section
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>
* refactor(agent): rename file_metadata_table to file_list
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>
* fix(prompt): reference actual <file> elements instead of nonexistent <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>
* feat(prompt): wrap diffs in <review_files> container
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>
* refactor(prompt): clean up system/user prompts for multi-file review
- 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>
* docs(agent): restore design-decision comments lost during refactor
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>
* fix(prompt): restore review recall lost to plan-phase suppression
The semantic-grouping branch cut comment recall from 20.0% to 12.7%
(F1 25.2% -> 19.1%) on the 194-PR benchmark. Ground truth shows the
396 dropped comments had a 27.8% hit rate — only 14% better than a
random cut of the same size, so the loss was indiscriminate thinning
rather than targeted noise removal.
Four causes, all addressed here:
- The plan prompt lost its third severity tier. code_comment accepts
four severities but the plan offered only MUST/SHOULD, so nothing
could feed a `low` comment. Add CONSIDER.
- main_task_system.md lost its only positive instruction to produce
feedback ("pointing out areas for improvement"), leaving a prompt
made entirely of restrictions. Restore it and the obligation wording
in Role.
- Nothing obliged the model to cover every file in a group, so secondary
members were starved: .h files lost 79% of their comments and went
silent 68% of the time, against ~30% for implementation files at the
same comment density. Gate task_done on a per-<file> pass.
- PLAN_MODE_LINE_THRESHOLD was calibrated per file but is now compared
against a group's summed churn, making the plan phase effectively
unconditional. Gate on the largest single-file churn instead.
Also relax the [deep] abandonment threshold from 3-4 to 6-8 tool calls
(tool calls per file had fallen 4.12 -> 2.92) and make plan adherence
rule 4 an obligation rather than a permission.
The plan.skipped event keeps its original lines.changed attribute and
gains lines.changed.max_file, so existing dashboards keep resolving;
the telemetry docs are synced across all four locales, which also
picks up the file.path -> group.label rename from the grouping commit.
Round-budget and failure-attribution fixes are deliberately left out so
their effect can be measured separately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(llmloop): use per-comment path in re-location identity tests
These predate the code_comment schema migration (path moved from
tool-level to per-comment level in the grouping commit) and only
surfaced once the grouping commit was rebased ahead of them: the
tool-level path was silently ignored, comments were dropped for an
empty path, and TestReLocation_Identity got 0 requests instead of 1.
* refactor(prompt): emit the review plan as plain text instead of JSON
Nothing parses the plan output: executeGroupPlanPhase hands
resp.Content() straight into {{plan_guidance}}, so the JSON envelope
only cost output tokens and forced the model to escape newlines in the
code snippets every plan item quotes. Malformed JSON degraded silently
into the main-task prompt because no validation stood between them.
Keep the exact field set (change_summary, issues[].severity,
issues[].description, issues[].tool_guidance[].name/arguments/reason)
and carry it in plain text instead: a Summary: line, numbered issues
tagged [high|medium|low], and one arrow line per tool guidance. Headings
and code fences are now forbidden in the output, because the plan is
injected under a "### Review Plan" heading inside <user_task> and the
plan result is never fence-stripped. Align the plan user message, which
still asked for a JSON fence.
Restore the main_task system prompt to its pre-grouping wording, keeping
the trimmed Role section and dropping Review Plan Adherence, whose
MUST/SHOULD/CONSIDER and [quick]/[deep] vocabulary no longer exists in
the plan output. Two rules stay rescoped to the group instead: Strict
Focus Rules would otherwise tell the model to ignore its own group
members, and without the per-file pass requirement a group's secondary
files get marked completed in the manifest without ever being read.
* fix(agent): tag each rule block with the files it governs
A group spanning two languages resolved two rule sets and concatenated
them with a bare newline, so the model received (say) the Java checklist
immediately followed by the MyBatis one and nothing saying which file
each governs. A group holding exactly that mix is what the grouping
prompt asks for: interface plus implementation, i18n/config variants of
one resource.
Group the files by resolved rule text and wrap each block in
<rules for="...">, matching the <review_files>/<file path="..."> framing
already used for the diffs. A group covered by a single rule set — every
single-file group, and every group whose files share a language —
returns that text bare, so the rendered prompt stays byte-identical for
those runs and the prompt-cache prefix does not churn.
Resolve in path order as well. The diffs arrive in the grouping LLM's
response order, which varies between runs and would otherwise reorder
the blocks; clone before sorting, since the caller's slice is shared
with a.diffs.
Drop the four single-file helpers the grouping refactor left behind
(resolveSystemRule, buildChangeFilesExcept, executePlanPhase,
buildFilterCommentsJSON). All four had no caller outside their own
tests, and executePlanPhase still substituted {{current_file_path}} and
{{diff}}, placeholders no longer present in any template — reading it
suggested the single-file path was still live.
Their tests are ported to the group functions rather than deleted:
executeGroupPlanPhase, buildChangeFilesExceptGroup and
buildGroupFilterCommentsJSON had no coverage at all, so deleting the
tests with the code would have left the live path untested. The ports
assert what the group versions changed — plan records filed under the
group key, comment ids running globally across files, every group member
excluded from the change-file list including a renamed member's old
path.
* fix(agent): drop the stray trailing newline from the change-file list
buildChangeFilesExceptGroup appended the separator after each entry,
guarded by the entry's index into a.diffs. The loop skips binaries and
every group member, so whenever the final diff was one of the skipped
ones the last emitted entry still passed the index test and the list
ended with a newline. Excluding a whole group rather than a single file
makes that the common case, not the rare one.
Emit the separator before each entry instead, guarded on anything having
been written yet. Separators between entries were never affected — any
emitted entry followed by another necessarily sits below the final index
— so this only removes the trailing one.
Assert the exact string in three cases: a skipped final diff, a skip
between two entries, and everything excluded. The existing checks used
strings.Contains, which cannot see a trailing newline at all.
* fix(prompt): adapt the review filter to multi-file groups
The filter template still described its input as "one file's diff" and
fenced it with ```{{path}}, while executeGroupReviewFilter substitutes
the comma-joined group key and a multi-file <file> sequence. The fence
language read ```src/a.go,src/b.go, and the diff body is now XML that can
itself contain a fence and truncate the block.
The substantive cost was Ground A. Its shapes are inherently per-file —
"discusses the body of a function, on a file that only declares it",
"discusses host-language logic on a file that holds none" — but nothing
told the model to pair a comment with the <file> matching its path, so a
sibling file holding the construct could rescue a comment that its own
file refutes. The filter got more lenient, which is its safe direction,
but the ground stopped doing its job.
State the pairing rule up front, and keep the two grounds asymmetric:
Ground A is judged against the subject file alone, Ground B against any
file in the group, because a comment calling an identifier unused is
genuinely wrong once any of these files uses it. Sibling files stay
usable as evidence — that is what group-level filtering is for — they
just cannot substitute for the subject file.
Carry the diffs in <review_files> instead of a fenced block, matching
main_task_user.md, which also retires the bogus fence language and the
truncation risk. The system message loses "you can see only a single
diff" for the same reason. {{path}} is left substituted in agent.go: no
shipped template uses it now, but a user template still might.
* fix(agent): classify group coverage per file, not per group
A file group that stops before task_done (round/token budget
exhaustion) could still have produced usable comments for some of its
files. The dispatch loop was marking every file in the group as
Failed regardless, so a single stuck file dragged its whole group -
and, when it was the only dispatched group, the whole run - down to
terminal_state=failed even though real coverage existed.
Classify each file by whether it actually has comments: files with
output are marked Completed, files without stay Failed. This lets
computeTerminal report partial (exit 0) instead of failed (exit 1)
when a group is a genuine mix of the two.
Also fix subtaskFailed to count only the files marked failed in this
loop instead of assuming reportAsError implies the whole group
failed - a group can now legitimately end up partially completed.
* feat(agent): add multi-round review to improve recall coverage
Wrap the Main Loop + Review Filter in a per-group round loop
(default MAX_REVIEW_ROUNDS=2). Each round after the first injects
confirmed findings from prior rounds into the prompt, instructing the
model to find different issues rather than repeating known ones.
Key design points:
- Round 1 prompt is byte-identical to the previous single-round behavior
- Round 2+ strips the plan to avoid it acting as a coverage ceiling
- Per-path baselines (not global Snapshot) for concurrency safety
- Per-round filter ensures only verified findings enter confirmed set
- Early stop: zero new confirmed, or >= 30 confirmed, or budget exceeded
- Per-group timeout auto-scales 1.5x when rounds > 1
- budgetExceeded field converted to atomic.Bool to fix data race
- UTF-8 safe rune-based truncation for confirmed comment serialization
* feat(config): raise MAX_TOOL_REQUEST_TIMES to 100, minMaxTools to 50
Grouped file review needs more tool call rounds than single-file mode
since one RunPerFile call now serves multiple files. Raise the default
from 30 to 100 and the user-configurable minimum from 10 to 50.
* feat(config): add group plan threshold and separate prompt/completion token limits
- Add PLAN_MODE_GROUP_LINE_THRESHOLD (default 120) so multi-file groups
trigger the plan phase when their combined churn is substantial, even
if no single file exceeds the per-file threshold. This fixes round-1
review quality regression for grouped files.
- Separate MAX_TOKENS (prompt ceiling, raised to 200000) from
MAX_COMPLETION_TOKENS (output cap, 16384). Previously both roles were
conflated in a single value. This allows larger file groups to stay
intact without inflating the model's output budget.
- Add Template.PlanRequired() helper with dual-threshold logic
- Extract groupChurn() helper from executeGroupSubtask
- Remove runtime MaxCompletionTokens=MaxTokens assignment in review/scan
commands (now loaded from template JSON directly)
* feat(config): add --effort flag to control review rounds
Introduce a review effort preset (--effort low|medium|high) that maps
to the number of review rounds:
- low: 1 round (fast, suitable for large PRs or time-sensitive CI)
- medium (default): 2 rounds (best balance of recall and cost)
- high: 3 rounds (maximum coverage)
Supported via CLI flag, persistent config (ocr config set effort low),
and three-tier precedence (CLI > config > default).
Also reorder tools.json to place file_read after code_search.
* feat(output): include file group info in --format json output
Add a 'groups' field to the JSON output showing which files were
grouped together for review. Each entry has a label and file list.
This enables post-run analysis of grouping behavior for evaluation.
Also reorder Strict Focus Rules in main_task_system.md and move
file_read after code_search in tools.json.
* fix(prompt): rewrite Strict Focus Rules for clarity
- Lead with positive scope instruction (review every file individually)
- Encourage cross-file observations within <review_files>
- Clarify that context tools are for background info, and comments must
target code within <review_files> — avoids ambiguity that could
suppress legitimate findings discovered via context gathering
* style: gofmt -s output.go
* docs: sync documentation with semantic grouping and effort features
Update all four language variants (en, zh, ja, ru) to reflect:
- MAX_TOOL_REQUEST_TIMES default 30 → 100
- MAX_TOKENS 58888 → 200000 (prompt ceiling only)
- New MAX_COMPLETION_TOKENS = 16384 (output cap)
- New PLAN_MODE_GROUP_LINE_THRESHOLD = 100
- New --effort low|medium|high flag (1/2/3 rounds)
- Semantic file grouping pipeline
- --max-tools raise-only semantics (min 50)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: xujiejie <80671406+yingjiexu2002@users.noreply.github.com>
|
||
|
|
71d29810be |
feat(llmloop): backfill comment thinking from turn output (#773)
* 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. |
||
|
|
840f85f9bc |
test: raise statement coverage to 90% and enforce it in CI (#747)
* test: raise statement coverage to 90% and enforce it in CI Add unit tests across the cmd and internal packages to bring total statement coverage above 90%, and gate future regressions. - Cover CLI helpers, provider TUI handlers, resume/manifest paths, and error branches in config, llm, llmloop, scan, session, agent, viewer, mcp, pathutil, and telemetry. - Raise the coverage threshold from 80% to 90% in the Makefile (COVERAGE_THRESHOLD) and in the CI "Check coverage threshold" step. - Ignore generated coverage.out and coverage.html artifacts. Total statement coverage is now 90.5%, measured consistently by both `make coverage` and the CI `go test ./...` scope. * test: widen statement coverage margin with environment-independent unit tests Add table-driven unit tests for pure, environment-independent functions to raise the statement-coverage safety margin above the 90% threshold: - session.ResumeState.ValidateScanOptions (70% -> 100%) - rules.SystemRule.UnmarshalJSON error branches (71% -> 82%) - llmloop.stripMarkdownFences no-newline branch (82% -> 100%) - diff.firstLine empty/blank-input branch - diff.extractCodeBlock missing-newline and no-closing-fence branches - main.truncate n<=1 and normalization branches - agent.Agent nil-receiver accessor guards |