Skip to content

refactor(agent-core-v2): rebuild context projection as a staged block pipeline - #3001

Merged
7Sageer merged 22 commits into
mainfrom
refactor-context
Aug 17, 2026
Merged

refactor(agent-core-v2): rebuild context projection as a staged block pipeline#3001
7Sageer merged 22 commits into
mainfrom
refactor-context

Conversation

@7Sageer

@7Sageer 7Sageer commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Related Issue

No linked issue — the problem is described below.

Problem

The context projector (stored history → provider wire messages) had grown into a single ~650-line service file mixing three concerns: the DI service with repair reporting, the media degrade/strip fallbacks, and the structural projection itself. The structural projection fused partial filtering, tool call/result pairing, displaced-result reordering, synthetic interrupted results, user-message merging, and content cleaning into one loop over shared mutable state, relying on a shared sentinel message patched later by output index — an unstated invariant that made the repairs hard to reason about and impossible to test independently.

What changed

  • Split contextProjectorService.ts by concern: mediaProjection.ts (the read-side media degrade/strip fallbacks), projection.ts (the pure structural transform), and a slim service keeping only the DI binding and deduped repair warning/telemetry. The package's public export surface is unchanged.
  • Rebuilt the structural projection as a two-stage pipeline. pairBlocks walks the history once, normalizes content, and groups tool exchanges into blocks that own their calls' results — a displaced result attaches to its call, an orphan is dropped, a superseded call is closed. flattenBlocks serializes the blocks back to wire order (results follow their owning message in call order), closes still-open calls with synthetic interrupted results, and merges consecutive user prompts. The slot sentinel and index back-patching are gone.
  • The two previously implicit rules are now named and documented in the module header: a history slice without any assistant message is a sizing slice (tool messages project like any other message), and a synthesized close counts as trailing exactly when no non-tool, non-partial message follows the owning message.
  • One deliberate behavior fix on degenerate data: a user-role message carrying tool calls previously went through the merge path and silently lost its tool calls; it now forms an exchange and keeps them. No realistic history produces this, and no test pinned it.

Behavior is pinned unchanged by the existing suites: the projector and llmRequester tests pass unmodified, and the full agent-core-v2 suite (5242 tests), typecheck, and import-boundary checks are green.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • Existing projector and llmRequester suites pin the behavior and pass unmodified (behavior-preserving refactor; no new tests needed).
  • Ran gen-changesets skill, or this PR needs no changeset. (No changeset: agent-core-v2 internal refactor, not user-perceivable.)
  • Ran gen-docs skill, or this PR needs no doc update. (No user-facing behavior or docs impact.)

7Sageer added 20 commits August 13, 2026 11:30
…onverge fold/projection internals

- ContextModel state is now { messages, fold }: the loop-event fold cursor
  (openStepUuid / pending / deferred) lives in the state instead of a
  module-level WeakMap keyed by array identity, so wholesale replacements
  (undo / clear / compaction / swarm exit) reset it structurally via
  EMPTY_FOLD instead of a manual resetFold at five call sites.
- The display transcript and the wire model now share one generic fold
  kernel (FoldFrame / FoldEntryAdapter), eliminating the mirrored second
  implementation. Events tagged with a non-open step uuid are dropped and
  step.end settles only the step it names — defensive in abnormal streams,
  identical on well-formed ones (v1 replay unaffected).
- IAgentContextProjectorService converges to project(messages, policy) with
  a ProjectionPolicy data object; llmRequester builds the policy from retry
  state instead of selecting among four methods.
- Blob rehydrate now also covers messages still deferred in the fold cursor.
- ContextState is deeply frozen at the op boundary to preserve the consumer
  immutability the wire's shallow freeze gave the bare array state.
…s in context memory

- isVacuousContentPart and dehydrateRecord now switch exhaustively over
  ContentPart / LoopRecordedEvent variants, so a new variant fails
  compilation until it takes an explicit position
- the transcript/model parity comparator spreads whole messages and masks
  only summary content, so new ContextMessage fields join the comparison
  automatically
- correct two stale header comments: local message ids persist with
  append_message records, and undo's prompt-owned-injection pairing
  depends on them after a resume
The model Op and the display transcript each walked the undo anchors with
their own loop, and the transcript partially removed the tail when an undo
was blocked (compaction summary / clear floor / too few anchors) while the
model side no-ops at the precheck. Move the walk into conversationTime as
computeUndoCut/computeUndoCutFrom applied destructively by the context.undo
Op and non-destructively by the transcript reducer, so a blocked undo reads
identically on both sides.

Also: make isUndoAnchor exhaustive over origin kinds with a never assertion,
mirrors the compaction result message count via compactionHandoff, and
extend UndoCut with anchorIndex distinguishing the counted anchor from the
injection-extended cut point.
…anscript undo

The transcript's kept-loop retained every injection after the oldest
counted anchor, so with count > 1 a prompt-owned injection of a newer
removed prompt (e.g. an image-compression caption) survived the display
undo while the model Op removed it. Collect the removed anchors' ids on
the same pass and keep only injections not owned by them, so the header's
'prompt-owned ones leave with their prompt' holds for every count.
The llmRequester retry chain kept a RequestProjection union and translated
it into a ProjectionPolicy per attempt; repairs were mutually exclusive,
so a strict resend rejected again for body size or image format either
aborted or silently dropped the strict repair. Retry state is now the
ProjectionPolicy itself: each rejection adds its repair on its own axis
(media: 413 -> degraded -> strip; wire: structure -> strict) without
discarding the other, requestInput's translation layer and the unreachable
snapshot ??= disappear, and the persisted llm.request projection name
derives from the policy (the op enum gains strict-media-degraded /
strict-media-stripped). Also narrows ProjectionPolicy to the variants
actually produced (wire 'strict'; media 'degraded' | { strip }), dropping
the dead 'default'/'keep' literals and their guard.
…pend-only log

context.apply_compaction now appends a summary marker carrying the record
fields as CompactionMeta instead of replacing the folded history; the
model-visible window is derived at read time (visibleWindow) with the same
[head, elision?, tail, summary] layout, one deterministic derivation for
live dispatch and replay alike. The record format is unchanged, so v1- and
v2-written sessions keep replaying identically both ways.

- undo maps the visible-window cut back to a log position (the verbatim
  legacy-summary edge falls back to the pre-append-only destructive cut)
- replay rehydrate only loads blobs the window derivation can surface
- contextInjector drops position tracking; injection positions become a
  read-time scan that splices can never desync
- fullCompaction's safety check moves to the stable-identity log
… surfaces

The first append-only rehydrate rule kept pre-marker real user input plus
markers, but a legacyTail derivation keeps window.slice(compactedCount)
visible — assistant/tool media in that range stayed blobref after replay
and could be resent unresolved. Decide survivors by identity membership in
the derived window instead, which covers every derivation branch at once
and skips the unselected pre-marker pool as a bonus.
…cy marker

SwarmService.exit decides the pop on the derived window's tail, but the
swarm_mode.exit reducer tested the raw log tail — after a legacyTail
compaction the survivor reminder is visible at the window tail while the
marker is the log tail, so the pop silently skipped and the stale reminder
stayed in the model context. Mirror the visible-tail decision in the
reducer and remove the entry by its stable log identity.
…ds mid-fold

An overflow-triggered compaction arrives with the failed attempt's vacuous
partial still open. The transcript appended the summary marker and reset
the fold but left the frame; the retried step's step.begin then settled it
(-1) alongside the new frame (+1), so foldedLength stayed one short of the
model-visible window and kap-server's live-tail merge could duplicate the
retry tail. Settle the frame at the marker through the shared kernel;
recoverFoldedLength recomputes the absolute count right after either way.
Apply settleModelOpenStep inside context.apply_compaction so a marker only
ever lands on a settled frame: no partial survives a marker and nothing
mutates the log behind one, making the append-only invariant structural
(the identity-prefix check in historySafeToCompact relied on it) instead
of timing-dependent. Mirrors the transcript's settle-at-marker.

Also freeze the derived visible window before caching it (an in-place
consumer mutation now throws instead of silently polluting the shared
cache), and drop the production-unreachable legacy branch of
buildContextCompactionShape so the legacy tail layout lives only in
deriveCompactionWindow.
…y internals

- Slim file headers to the package header-only comment convention
- Rename PR-introduced identifiers for clarity: getMessageLog,
  ProjectionPolicy.structure, pendingToolCallIds/deferredEntries,
  removedEntryCount, deriveVisibleWindowAfterCompaction,
  compactedWindowMessageCount
- Extract nextProjectionPolicyForError, removeUndoOwnedEntries and
  summarizeProjectionRepairs; name fold intermediates after their
  business stage
- Regroup splice-replay tests by topic and unify projection-call
  recording in llmRequester tests
… pipeline

Split the 650-line projector service into three modules by concern:
mediaProjection (read-side media degrade/strip fallbacks), projection
(the structural transform), and the service (DI binding plus repair
reporting). Rebuild the structural projection as a two-stage pipeline:
pairBlocks groups tool exchanges into blocks that own their calls'
results, flattenBlocks serializes them back to wire order and merges
consecutive user prompts. The shared slot sentinel and index
back-patching are gone; the trailing-close and sizing-slice rules are
named and documented in the module header. Behavior is pinned unchanged
by the existing projector and llmRequester suites.
# Conflicts:
#	packages/agent-core-v2/src/agent/contextProjector/contextProjectorService.ts
@changeset-bot

changeset-bot Bot commented Aug 17, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: f86e2cf

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@7Hanrui

7Hanrui commented Aug 17, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dc282494db

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +6 to +10
* `project` runs as a two-stage pipeline. `pairBlocks` walks the history
* once, normalizing each message's content (tool-result facts rendered for
* the model, blank text dropped, wholly-vacuous messages dropped, partial
* messages skipped) and grouping tool exchanges into blocks: an exchange
* owns its calls' results, so a displaced result attaches to its call, an

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep the helper header at the external-role level

This header walks through the internal pairBlocks/flattenBlocks stages and their mechanics, while the package convention explicitly forbids narrating implementation steps or enumerating exports in module comments. Reduce it to the helper's externally visible responsibility so the comment does not become stale whenever the pipeline is rearranged.

AGENTS.md reference: packages/agent-core-v2/AGENTS.md:L36-L40

Useful? React with 👍 / 👎.

@7Hanrui

7Hanrui commented Aug 17, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f55abfa05d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +4 to +6
* Delegates the stored-history → wire-message transform to the pure
* `projection.ts` pipeline (`project` / `projectStrict`) and the read-side
* media fallbacks selected by `policy.media` to `mediaProjection.ts`, and

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep the service header at the external-role level

Rewrite this header around the service's externally visible responsibility rather than naming projection.ts, project/projectStrict, and the delegation stages; these implementation details will become stale when the pipeline is reorganized and are expressly excluded from module headers.

AGENTS.md reference: packages/agent-core-v2/AGENTS.md:L36-L40

Useful? React with 👍 / 👎.

Comment on lines +2 to +6
* `contextProjector` domain helper — read-side media fallbacks for the two
* deterministic provider rejections.
*
* `degradeOlderMediaParts` replaces all but the most recent media parts with
* text markers after an HTTP 413 body-size rejection; a strip snapshot

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Describe the media module without enumerating its pipeline

Use the required `contextProjector` domain — <role> identity and describe only the module's responsibility; the current header adds “helper” to the identity and enumerates degradeOlderMediaParts, snapshot helpers, and their mechanics, contrary to the package's external-role-only convention.

AGENTS.md reference: packages/agent-core-v2/AGENTS.md:L36-L40

Useful? React with 👍 / 👎.

@7Hanrui

7Hanrui commented Aug 17, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f86e2cffb3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

type ProjectionPolicy,
} from './contextProjector';
import {
MEDIA_DEGRADE_KEEP_RECENT,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the existing contextProjectorService subpath exports

Any consumer importing MEDIA_DEGRADE_KEEP_RECENT, MEDIA_STRIPPED_PLACEHOLDERS, or the media helpers from @moonshot-ai/agent-core-v2/agent/contextProjector/contextProjectorService now fails because these symbols were moved without being re-exported from that module; this is a supported public subpath under the package's ./* export pattern, even though the root barrel was updated. Re-export the moved symbols from contextProjectorService.ts to keep the stated public surface unchanged.

AGENTS.md reference: AGENTS.md:L61-L61

Useful? React with 👍 / 👎.

@7Sageer
7Sageer marked this pull request as ready for review August 17, 2026 12:32
@pkg-pr-new

pkg-pr-new Bot commented Aug 17, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@f86e2cf
npx https://pkg.pr.new/@moonshot-ai/kimi-code@f86e2cf

commit: f86e2cf

@7Sageer
7Sageer merged commit 5dffed2 into main Aug 17, 2026
16 checks passed
@7Sageer
7Sageer deleted the refactor-context branch August 17, 2026 12:34
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.

2 participants