Skip to content

Centralize agent context planning - #879

Merged
anandh8x merged 3 commits into
mainfrom
feat/unified-context-planner
Aug 8, 2026
Merged

Centralize agent context planning#879
anandh8x merged 3 commits into
mainfrom
feat/unified-context-planner

Conversation

@anandh8x

@anandh8x anandh8x commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • centralize main agent-loop and automatic-compaction request construction behind a context planner
  • preserve the complete existing message and tool surface while exposing content-free context-block accounting
  • trace stable-prefix fingerprints with explicit invalidation reasons

Why

Zero needs one safe seam for future relevance selection, deduplication, pruning, and cache-aware context policies. This change establishes that seam without filtering context or reducing model capability. It does not claim deterministic token savings by itself.

Evaluation

  • identical before/after provider-visible prefix and initial context estimate: 9,177 tokens
  • matched GPT-5.5 bounded task remained correct and complete
  • bounded run used 5 → 4 tool calls and 31,104 → 29,008 input tokens; treated as run variation rather than a guaranteed planner saving
  • a longer repository-analysis task also completed correctly, with the changed run identifying an additional relevant session-log read path

Validation

  • make fmt-check
  • go vet ./...
  • focused agent and trace tests
  • go run ./cmd/zero-release build
  • go run ./cmd/zero-release smoke
  • make lint-static (0 issues)
  • make vulncheck (no vulnerabilities found)
  • git diff HEAD --check

go test ./... passes except for the existing TestRunDoctorFormatsRedactedProviderDiagnostics and TestRunDoctorConnectivityProbesProvider failures in internal/cli; both reproduce unchanged from the untouched base commit.

Summary by CodeRabbit

  • New Features

    • Added detailed context breakdowns for system, tool, and conversation content.
    • Added visibility into prompt-cache prefix changes, including invalidation reasons.
    • Improved tracing to record and restore prefix invalidation details.
    • Added context reporting for initial, replacement, retry, and compaction requests.
  • Bug Fixes

    • Standardized request planning across responses, retries, compaction, and final answers.
    • Preserved provider request settings and context data consistently across planning steps.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The agent now uses a shared contextPlanner to build requests, measure context blocks, track prefix changes, and report diagnostics. Trace events preserve prefix invalidation reasons. Compaction and retry paths use the same planning flow.

Changes

Context planning and diagnostics

Layer / File(s) Summary
Context measurement metadata
internal/agent/context_measurement.go, internal/agent/context_measurement_test.go
ContextBreakdown now includes context blocks, complete prefix hashes, and invalidation reasons. Measurement records system, tool, and conversation blocks.
Deterministic context planning
internal/agent/context_planner.go, internal/agent/context_planner_test.go
contextPlanner preserves provider request data, isolates planned requests, measures context, fingerprints prefixes, and explains component changes.
Agent request and callback integration
internal/agent/loop.go, internal/agent/loop_test.go, internal/agent/types.go
Agent request paths use the planner. Context callbacks and traces receive planner-derived breakdown and prefix evidence.
Planner-backed compaction and recovery
internal/agent/compaction.go, internal/agent/compaction_test.go
Compaction summaries, recursive reductions, and context-limit recovery use planner-backed requests and record replacement plans.
Prefix invalidation trace persistence
internal/trace/trace.go, internal/trace/emit.go, internal/trace/parse.go, internal/trace/recorder.go, internal/trace/trace_test.go
Prefix hash events now serialize, parse, and round-trip InvalidationReason values in request order.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Run
  participant contextPlanner
  participant Provider
  participant Compaction
  participant OnContext
  participant Trace
  Run->>contextPlanner: Plan messages, tools, and settings
  contextPlanner->>Provider: Return completion request
  Run->>OnContext: Report context breakdown
  Run->>Trace: Emit prefix hash and invalidation reason
  Provider-->>Run: Return context-limit error
  Run->>Compaction: Summarize and reduce context
  Compaction->>contextPlanner: Plan replacement request
  contextPlanner->>Provider: Stream replacement request
Loading

Possibly related PRs

  • Gitlawb/zero#838: Both changes modify internal/agent/compaction.go and the compaction request flow.
  • Gitlawb/zero#873: Both changes modify the compaction summarization flow in internal/agent/compaction.go.

Suggested reviewers: gnanam1990, vasanthdev2004

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the PR's primary change: centralizing agent context planning.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/unified-context-planner

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/agent/context_planner.go`:
- Around line 46-50: Make prefix planning request-specific while retaining the
configured run planner and its state: update the context-planning path around
buildPromptSubstringsFromParts to accept or safely derive prompt parts from the
current request before computing the fingerprint. In
internal/agent/context_planner.go lines 46-50, use those request-specific parts;
in internal/agent/compaction.go lines 627-631, pass the configured run planner
with compaction-specific prompt parts and record its context evidence before
streaming. Add regression coverage for differing configured/request system
content and compaction planning.

In `@internal/agent/loop.go`:
- Line 323: Update each request-planning path around planner.Plan calls at lines
323, 407, 465, and 966 to retain the returned plan and invoke recordContextPlan
for that plan before streaming or sending the provider request. Ensure
reactive-compaction retries record the retained post-compaction plan, so
OnContext and PrefixHashes correspond to the request actually sent.

In `@internal/trace/emit.go`:
- Line 108: Update the custom encoder containing the PrefixHash
invalidation_reason field to omit that key when p.InvalidationReason is empty,
preserving the omitempty wire contract; retain it when a non-empty reason
exists. Add a regression test covering encoding with an empty invalidation
reason and verifying the field is absent.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4b1eef45-0317-4db3-bb97-e283e8d1ae88

📥 Commits

Reviewing files that changed from the base of the PR and between ff608c7 and 14f8a1a.

📒 Files selected for processing (11)
  • internal/agent/compaction.go
  • internal/agent/context_measurement.go
  • internal/agent/context_measurement_test.go
  • internal/agent/context_planner.go
  • internal/agent/context_planner_test.go
  • internal/agent/loop.go
  • internal/agent/loop_test.go
  • internal/trace/emit.go
  • internal/trace/parse.go
  • internal/trace/trace.go
  • internal/trace/trace_test.go

Comment thread internal/agent/context_planner.go Outdated
Comment thread internal/agent/loop.go Outdated
Comment thread internal/trace/emit.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/agent/context_planner.go`:
- Around line 50-54: Deep-copy tool definitions, including each nested
Parameters map, before assigning them to CompletionRequest.Tools in the Plan
flow. Ensure the request owns an independent schema from toolDefs so later
caller mutations cannot diverge from PrefixFingerprint and Breakdown.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e877666c-d646-4079-a646-70d84a965dcd

📥 Commits

Reviewing files that changed from the base of the PR and between 14f8a1a and 0368c2b.

📒 Files selected for processing (9)
  • internal/agent/compaction.go
  • internal/agent/compaction_test.go
  • internal/agent/context_planner.go
  • internal/agent/context_planner_test.go
  • internal/agent/loop.go
  • internal/agent/types.go
  • internal/trace/emit.go
  • internal/trace/recorder.go
  • internal/trace/trace_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/agent/loop.go
  • internal/trace/trace_test.go
  • internal/trace/emit.go

Comment thread internal/agent/context_planner.go Outdated

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed as a draft. No blocking findings, and the claims hold where I checked them rather than took them.

The seam is genuinely complete. grep zeroruntime.CompletionRequest{ across internal/agent returns exactly one construction site, inside the planner itself, with every other path going through planner.Plan. That is the strongest evidence the centralization is real rather than partial, and it is the thing most likely to be half-done in a change like this.

Request identity is properly tested. TestContextPlannerPreservesProviderRequest uses reflect.DeepEqual on the whole request, not substring checks, and then verifies deep-copy semantics three levels into the nested map[string]any schema plus the image byte slice. Nested-map aliasing is the classic Go trap in exactly this shape and you tested for it, including that a genuinely changed schema does refresh the snapshot and the fingerprint while leaving the earlier plan's request untouched.

The trace stays content-free. explainPrefixChange returns only initial, unchanged, base_instructions, confirmation_policy, project_context and skills, every one derived from a hash comparison rather than from content. That matters because the trace vocabulary is a published coordination surface and the same property was the review axis on #761.

Planner lifetime checks out too: created per Run at loop.go:186, so previousPrefix starts nil and the first plan reports initial rather than inheriting another session's state.

internal/trace passes. internal/agent fails only on TestEagerToolSchemaTokenBudget, which is the ceiling issue #877 fixes, inherited from main and not yours.

Also worth saying: describing the token and tool-call differences as run variation rather than a guaranteed saving is the right call, and rarer than it should be.

Ready for approval as far as I am concerned once it leaves draft.

@anandh8x
anandh8x marked this pull request as ready for review August 8, 2026 07:58
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Zero automated PR review

Verdict: No blockers found

Blockers

  • None found.

Validation

  • [pass] Diff hygiene: git diff --check
  • [pass] Tests: go test ./...
  • [pass] Build: go run ./cmd/zero-release build
  • [pass] Smoke build: go run ./cmd/zero-release smoke

Scope

Head: bea0c2147211
Changed files (14): internal/agent/compaction.go, internal/agent/compaction_test.go, internal/agent/context_measurement.go, internal/agent/context_measurement_test.go, internal/agent/context_planner.go, internal/agent/context_planner_test.go, internal/agent/loop.go, internal/agent/loop_test.go, internal/agent/types.go, internal/trace/emit.go, internal/trace/parse.go, internal/trace/recorder.go, and 2 more

This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality.

@Vasanthdev2004

Copy link
Copy Markdown
Collaborator

@anandh8x reviewed above, and there is nothing blocking in it. If you were holding it in draft waiting on review feedback, take it out whenever you like and I will approve.

One thing worth knowing: your only failing test is TestEagerToolSchemaTokenBudget, which is inherited from main rather than yours. #877 raises that ceiling and needs a human approval; once it lands your suite goes green with no change on your side.

If you are holding it in draft for a reason of your own, ignore that and carry on.

Two things I checked and am confirming back, since they are the claims a reviewer would usually just take on trust. zeroruntime.CompletionRequest{} now appears exactly once in internal/agent, inside the planner, so the centralization is complete rather than partial. And explainPrefixChange only ever emits fixed tokens derived from hash comparisons, so the trace stays content-free, which was the axis #761 was reviewed on.

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Re-checked on bea0c214 rather than standing on the earlier read: same commit, now out of draft, up to date with main, and all three smoke jobs green.

Two things I re-verified on this head rather than trusting my own earlier note.

zeroruntime.CompletionRequest{} is constructed in exactly one place in internal/agent now, context_planner.go:61, and everything else goes through planner.Plan. Partial adoption is the usual way a centralization lands half-done, and this one is complete.

explainPrefixChange can only emit fixed literals: initial, unchanged, base_instructions, confirmation_policy, project_context, skills, system_prompt, tools, schema, prefix_changed, comma-joined. No content, no hashes, no paths. That was the review axis on #761 and it holds.

One caveat that is not yours. TestEagerToolSchemaTokenBudget fails on main and therefore on your branch, and CI does not catch it, which is why these checks are green. #877 raises the ceiling and is still waiting on a human approval. Nothing for you to do, but do not read the green tick as that test passing.

@gnanam1990 gnanam1990 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Reviewed bea0c214, re-confirmed as the live head before posting.

@Vasanthdev2004 already covered seam completeness, the reflect.DeepEqual request-identity test, deep-copy semantics, trace vocabulary, and planner lifetime, and I agree with all of it — I won't restate it. Everything below is what I checked that his review did not.

The central claim holds on the wire, not just in a test

The description says the provider-visible prefix is identical before and after. That is the claim a refactor like this lives or dies on, and a test asserting it is not the same as it being true, so I captured the actual bytes.

I built ff608c78 (the merge base) and bea0c214, pointed both at a recording HTTP provider, and ran the same prompt from the same workspace path and same isolated config dir:

base sha256: 383f7d26920969d1eb67...
pr   sha256: 383f7d26920969d1eb67...
byte-identical

Worth noting the first attempt showed a large diff, and it was my harness at fault, not the patch — the two runs used different workspace directories and the system prompt embeds that path. Equal paths, identical bytes. The claim is real.

Main finding: one load-bearing part of the freeze cache key is untested

snapshotTools keys the frozen schemas on ToolsHash + "\x00" + SchemaHash (context_planner.go:87). I checked the key is complete first — ToolDefinition has exactly Name, Description, Parameters, and toolSubstrings fingerprints all three — so invariant #10 is satisfied. No issue there.

But the SchemaHash half is not pinned by any test. Dropping it:

key := fingerprint.ToolsHash    // SchemaHash removed

leaves go test ./internal/agent/ -run 'Planner|Context' green. That component is load-bearing: with name and description unchanged and only Parameters differing between turns, ToolsHash alone is stable, so the planner would serve the previous turn's frozen schemas and the provider would receive a stale tool surface.

The code is correct — I wrote a throwaway probe and it passes as-written, and fails with the mutation applied, printing the stale schema. So this is a missing guard, not a defect. Twelve lines close it:

func TestSchemaOnlyChangeRefreshesFrozenTools(t *testing.T) {
	planner := newContextPlanner(contextPlannerConfig{})
	messages := []zeroruntime.Message{{Role: zeroruntime.MessageRoleUser, Content: "hi"}}
	base := map[string]any{"type": "object", "properties": map[string]any{"path": map[string]any{"type": "string"}}}
	grown := map[string]any{"type": "object", "properties": map[string]any{"path": map[string]any{"type": "string"}, "limit": map[string]any{"type": "number"}}}

	// Name and description identical: only the schema moves, so ToolsHash alone
	// cannot notice and the freeze must consult SchemaHash.
	_ = planner.Plan(messages, []zeroruntime.ToolDefinition{{Name: "read_file", Description: "read a file", Parameters: base}}, "")
	plan := planner.Plan(messages, []zeroruntime.ToolDefinition{{Name: "read_file", Description: "read a file", Parameters: grown}}, "")

	properties, _ := plan.Request.Tools[0].Parameters["properties"].(map[string]any)
	if _, ok := properties["limit"]; !ok {
		t.Fatalf("second plan served a stale frozen schema: %#v", plan.Request.Tools[0].Parameters)
	}
}

For contrast, the deep-copy guard is pinned: replacing copyToolDefinitions(toolDefs) with toolDefs fails TestContextPlannerPreservesProviderRequest immediately. The freeze commit landed five minutes before the first review, so this is the least-examined part of the PR and worth the extra guard.

I also checked the specific risk the freeze introduces — the snapshot is returned by reference and shared across stable turns, so a downstream mutation would corrupt every later request. The only .Tools[...] write anywhere is providers/anthropic/provider.go:406, and it mutates that provider's own mapped.Tools, not the zeroruntime slice. No in-place Parameters[...] writes in any provider. Safe.

Negative space: the seam is agent-loop-scoped, and that boundary is worth stating

zeroruntime.CompletionRequest{ has exactly one construction site inside internal/agent, as noted. Repo-wide there are five more:

internal/tui/session_title.go:147
internal/tui/plan_step_detail.go:376
internal/tui/session_controls.go:925
internal/tui/recap.go:68
internal/cli/workflows.go:980

The description scopes the change to "main agent-loop and automatic-compaction request construction," so this is consistent rather than a gap. I raise it because the stated purpose is a seam for future "relevance selection, deduplication, pruning, and cache-aware context policies" — and when those land in the planner, these five paths will not get them.

The session_controls.go:925 one is the interesting one: #873 unified manual and automatic compaction summarization so both share agent.SummarizeCompactionMessages, while this PR centralizes request construction. Manual compaction still builds its own request in internal/tui. The two unifications stop just short of meeting. Not this PR's job, but with both in flight it is worth someone writing down which side owns that request.

Minor: compaction's fallback planners lose the context window

compaction.go:576 and :591 construct newContextPlanner(contextPlannerConfig{}), while :432 passes the real options.ContextWindow. contextWindow only feeds breakdown.UsedFraction (context_measurement.go:80) and gates no decision, so the blast radius is accounting: any breakdown produced through those paths reports UsedFraction 0. :591 passes a nil recorder so nothing is emitted, and :576 is defensive against a planner that the constructor already sets. Reporting-only and probably unreachable — flagging so it is a deliberate choice rather than a surprise if the accounting is ever consumed.

Verified

  • Provider request byte-identical vs merge base, as above.
  • go test ./internal/agent/ ./internal/trace/ — both fully green at bea0c214, including TestEagerToolSchemaTokenBudget, which was failing when this PR was first reviewed.
  • The pre-existing-failure claim is accurate. TestRunDoctorFormatsRedactedProviderDiagnostics and TestRunDoctorConnectivityProbesProvider fail identically on untouched ff608c78. Checked rather than taken.
  • Mutation-tested the deep copy (caught) and the schema cache key (survived — the finding above).
  • Platform coverage, which the description does not claim: I ran the built binary through complete feature runs on all three OSes — file writes, nested paths, sandbox refusal, --add-dir grants, control characters, specialist child processes, and exec_command.
macOS Linux Windows
bea0c214 7/7 7/7 6/7

The single Windows failure is exec_command, and it is not from this PR: PowerShell cannot start under the sandbox's WRITE_RESTRICTED token (.NET fails BCrypt.dll init, 0x8007045A), and main fails it identically. Since this PR touches the agent loop's hot path, I wanted real three-OS execution rather than an assumption, and it is clean.

Nothing here blocks. The schema-key test is the one thing I would add before merge, and it is twelve lines.

@anandh8x
anandh8x merged commit 7f39a63 into main Aug 8, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants