Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
8780d6a
refactor(agent-core-v2): carry the context fold cursor in state and c…
7Sageer Aug 13, 2026
2d41945
test(agent-core-v2): move fold parity rationales into the test file h…
7Sageer Aug 13, 2026
e83ad3f
docs(agent-core-v2): move fold declaration comments into module headers
7Sageer Aug 13, 2026
2b47fe7
refactor(agent-core-v2): merge FoldFrame into generic ContextState
7Sageer Aug 13, 2026
e7fe16e
refactor(agent-core-v2): compile-enforce part/event handling decision…
7Sageer Aug 13, 2026
55b9e0b
refactor(agent-core-v2): converge undo-cut decision in conversationTime
7Sageer Aug 14, 2026
c5cf562
fix(agent-core-v2): drop removed prompts' injections on multi-turn tr…
7Sageer Aug 14, 2026
a8ee42c
refactor(agent-core-v2): accumulate request projection repairs as policy
7Sageer Aug 14, 2026
5ddab0c
refactor(agent-core-v2): derive the visible context window from an ap…
7Sageer Aug 14, 2026
8892e84
fix(agent-core-v2): rehydrate exactly the messages the visible window…
7Sageer Aug 14, 2026
5ba63a4
fix(agent-core-v2): pop the visible-tail swarm reminder behind a lega…
7Sageer Aug 14, 2026
a7a0b45
fix(agent-core-v2): settle open transcript frames when compaction lan…
7Sageer Aug 14, 2026
29c0a54
fix(agent-core-v2): settle open frames at the compaction marker
7Sageer Aug 17, 2026
51f31ae
Merge remote-tracking branch 'origin/main' into refactor-context
7Sageer Aug 17, 2026
641157e
refactor(agent-core-v2): tighten naming and comments in context memor…
7Sageer Aug 17, 2026
2c36afe
fix(agent-core-v2): preserve bounded context state
7Sageer Aug 17, 2026
a085428
Merge remote-tracking branch 'origin/main' into refactor-context
7Sageer Aug 17, 2026
450645f
docs(agent-core-v2): restore the domain identity line in the compacti…
7Sageer Aug 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/agent-core-v2/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ One accepted exception: `features/tower/protocol` manages the `.tower/` director

## Conversation undo

`context.undo` is the only persisted undo fact. `contextMemory/conversationTime.ts` owns the conversation clock (`isUndoAnchor` — the single tick predicate used by `computeUndoCut`, the checkpoint reducers, and the transcript reducer) and the checkpoint protocol. A wire Model whose state must follow conversation undo (todo, plan, task-notification delivery, …) **MUST** be defined with `defineCheckpointedModel` — never hand-roll the push/clear/restore reducers — which also registers it into `CHECKPOINTED_MODELS` for the undo pipeline's pre-cut depth check. World-time state (turn counters, task registries, revision counters) must stay outside checkpointed Models.
`context.undo` is the only persisted undo fact. `contextMemory/conversationTime.ts` owns the conversation clock (`isUndoAnchor` — the single tick predicate — plus `computeUndoCut` / `computeUndoCutFrom`, the single anchor-walk decision: applied destructively by the `context.undo` Op and non-destructively by the transcript reducer, so a blocked undo reads identically on both sides) and the checkpoint protocol. A wire Model whose state must follow conversation undo (todo, plan, task-notification delivery, …) **MUST** be defined with `defineCheckpointedModel` — never hand-roll the push/clear/restore reducers — which also registers it into `CHECKPOINTED_MODELS` for the undo pipeline's pre-cut depth check. World-time state (turn counters, task registries, revision counters) must stay outside checkpointed Models.

## Model-facing reminders

Expand Down
12 changes: 11 additions & 1 deletion packages/agent-core-v2/docs/wire-manifest.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,16 @@ interface ContextAppendMessagePayload {
origin?: 'user' | 'skill_activation' | 'plugin_command' | 'injection' | 'shell_command' | 'compaction_summary' | 'system_trigger' | 'task' | 'cron_job' | 'cron_missed' | 'hook_result' | 'retry' | undefined;
isError?: boolean;
note?: string;
compaction?: {
compactedCount: number;
tokensBefore: number;
tokensAfter?: number;
summaryOutputTokens?: number;
keptUserMessageCount?: number;
keptHeadUserMessageCount?: number;
droppedCount?: number;
legacyTail?: boolean;
};
};
}

Expand Down Expand Up @@ -344,7 +354,7 @@ interface LlmRequestPayload {
messageCount: number;
turnStep?: string;
attempt?: string;
projection?: 'strict' | 'media-degraded' | 'media-stripped';
projection?: 'strict' | 'media-degraded' | 'media-stripped' | 'strict-media-degraded' | 'strict-media-stripped';
droppedCount?: number;
}

Expand Down
106 changes: 78 additions & 28 deletions packages/agent-core-v2/src/agent/contextMemory/compactionHandoff.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
/**
* `contextMemory` domain helper — derives the v1-compatible full-compaction
* handoff shape for live rewrites, wire replay, and snapshot reducers.
* `contextMemory` domain — derives compaction shapes, markers, and visible
* windows.
*
* Token budgeting runs through an injectable {@link TokenEstimate}: the live
* path (`AgentContextMemoryService.applyCompaction`) passes the estimator
* from `IAgentTokenCountingService` (the raw heuristics — the
* `[token_counting]` strategy never gates internal estimates); the pure
* wire-replay / reducer paths keep the same heuristics — their estimate
* fallback only fires when a record lacks `tokensAfter`, so the measured
* chain is unaffected.
* Exposes shared token estimation and user-message selection for live
* compaction and replay. Legacy records are handled during window derivation.
*/

import { estimateTokens, estimateTokensForMessage, estimateTokensForMessages } from '#/kosong/contract/tokens';
import type { ContentPart } from '#/kosong/contract/message';
import { wrapSystemReminder } from '#/agent/systemReminder/systemReminder';
import summaryPrefixTemplate from './compaction-summary-prefix.md?raw';
import type { ContextMessage, PromptOrigin } from './types';
import type { CompactionMeta, ContextMessage, PromptOrigin } from './types';

export const COMPACTION_SUMMARY_PREFIX = summaryPrefixTemplate.trimEnd();
export const COMPACT_USER_MESSAGE_MAX_TOKENS = 20_000;
Expand Down Expand Up @@ -84,24 +79,6 @@ export function buildContextCompactionShape(
input: ContextCompactionShapeInput,
estimate: TokenEstimate = defaultTokenEstimate,
): ContextCompactionShape {
if (usesLegacyTailShape(input)) {
const contextSummary = input.contextSummary ?? input.summary;
const messages = [
input.legacySummaryMessage ?? createCompactionSummaryMessage(contextSummary),
...history.slice(input.compactedCount),
];
return {
summary: input.summary,
contextSummary,
compactedCount: input.compactedCount,
tokensBefore: input.tokensBefore,
tokensAfter: input.tokensAfter ?? estimate.messages(messages),
keptUserMessageCount: 0,
droppedCount: input.droppedCount,
messages,
};
}

const compactableUserMessages = collectCompactableUserMessages(history);
const selection = selectCompactionUserMessages(
compactableUserMessages,
Expand Down Expand Up @@ -139,6 +116,79 @@ export function buildContextCompactionShape(
};
}

export function compactedWindowMessageCount(
keptUserMessageCount: number | undefined,
keptHeadUserMessageCount: number | undefined,
): number | undefined {
if (keptUserMessageCount === undefined) return undefined;
return keptUserMessageCount + (keptHeadUserMessageCount === undefined ? 1 : 2);
}

/**
* The message the `context.apply_compaction` fold appends: the summary
* message the destructive rewrite used to synthesize, plus the record fields
* mirrored as {@link CompactionMeta} so the append-only log stays
* self-describing. Legacy records carrying a verbatim summary message keep it
* (meta attached) exactly as the rewrite kept it.
*/
export function createCompactionMarkerMessage(input: ContextCompactionShapeInput): ContextMessage {
const meta: CompactionMeta = {
compactedCount: input.compactedCount,
tokensBefore: input.tokensBefore,
tokensAfter: input.tokensAfter,
summaryOutputTokens: input.summaryOutputTokens,
keptUserMessageCount: input.keptUserMessageCount,
keptHeadUserMessageCount: input.keptHeadUserMessageCount,
droppedCount: input.droppedCount,
legacyTail: input.legacyTail === true ? true : undefined,
};
const base =
usesLegacyTailShape(input) && input.legacySummaryMessage !== undefined
? input.legacySummaryMessage
: createCompactionSummaryMessage(input.contextSummary ?? input.summary);
return { ...base, compaction: meta };
}

/**
* Read-time counterpart of {@link buildContextCompactionShape}: folds one
* marker into the pre-compaction visible `window`, returning the new visible
* window. The marker enters the window stripped of its `CompactionMeta` (the
* meta is log bookkeeping, not conversation content), making the result
* byte-identical to what the destructive rewrite produced. Selection is
* re-derived deterministically from the window — the media-flat token
* heuristics make it dehydrate/rehydrate-invariant, and the persisted
* kept-user counts stay informational.
*/
export function deriveVisibleWindowAfterCompaction(
window: readonly ContextMessage[],
marker: ContextMessage,
): readonly ContextMessage[] {
const meta = marker.compaction;
const summary = stripCompactionMeta(marker);
if (meta?.legacyTail === true) {
return [summary, ...window.slice(meta.compactedCount)];
}
const selection = selectCompactionUserMessages(
collectCompactableUserMessages(window),
COMPACT_USER_MESSAGE_MAX_TOKENS,
COMPACT_USER_MESSAGE_HEAD_TOKENS,
defaultTokenEstimate.message,
);
const elision = selection.elided
? createCompactionElisionMessage(selection.omittedTokens)
: undefined;
return elision === undefined
? [...selection.head, ...selection.tail, summary]
: [...selection.head, elision, ...selection.tail, summary];
}

function stripCompactionMeta(message: ContextMessage): ContextMessage {
if (message.compaction === undefined) return message;
const { compaction: _meta, ...stripped } = message;
void _meta;
return stripped;
}

export function buildCompactionSummaryText(summary: string): string {
const suffix = summary.trim();
return `${COMPACTION_SUMMARY_PREFIX}\n${suffix.length > 0 ? suffix : '(no summary available)'}`;
Expand Down
12 changes: 11 additions & 1 deletion packages/agent-core-v2/src/agent/contextMemory/contextMemory.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createDecorator } from "#/_base/di/instantiation";

import type { UndoCut } from './contextOps';
import type { UndoCut } from './conversationTime';
import type { LoopRecordedEvent } from './loopEventFold';
import type { ContextMessage } from './types';

Expand Down Expand Up @@ -38,8 +38,18 @@ export interface ContextCompactionResult {
export interface IAgentContextMemoryService {
readonly _serviceBrand: undefined;

/** The model-visible window: the append-only folded log derived through
* `visibleWindow.deriveVisibleMessages` (compaction markers folded away).
* This is the history every consumer — LLM requests, token counting, undo,
* injections — should read. */
get(): readonly ContextMessage[];

/** The raw append-only folded log, pre-compaction history and summary
* markers included. Log entries keep stable identities across appends —
* use it for integrity checks (e.g. the compaction safety check), never
* for model-facing content. */
getMessageLog(): readonly ContextMessage[];

append(...messages: readonly ContextMessage[]): void;

appendLoopEvent(event: LoopRecordedEvent): void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
* `contextMemory` domain — `IAgentContextMemoryService` implementation.
*
* Owns per-agent conversation history through `wire`, maintains measurements
* with `tokenCounting`, and broadcasts live mutations through `event`. Every
* splice-shaped mutation (`clear` / `applyCompaction` / `undo`, plus verified
* cross-model trailing removal) publishes `context.spliced` from the live path
* only — replay rebuilds silently — and truncates the measured-anchor ledger
* when a cut crosses an anchor, letting `tokenCounting` restore the surviving
* prefix's REAL size from the remaining anchors. Bound at Agent scope.
* with `tokenCounting`, and broadcasts live mutations through `event`. The
* wire state is an append-only folded log; `get()` serves the model-visible
* window derived by `visibleWindow.deriveVisibleMessages`, while `getMessageLog()`
* exposes the raw log for integrity checks. Every splice-shaped mutation
* (`clear` / `applyCompaction` / `undo`, plus verified cross-model trailing
* removal) publishes `context.spliced` from the live path only — replay
* rebuilds silently — and truncates the measured-anchor ledger when a cut
* crosses an anchor, letting `tokenCounting` restore the surviving prefix's
* REAL size from the remaining anchors. Bound at Agent scope.
*/
Comment on lines 1 to 11

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 Restore the required domain implementation header

The shortened header no longer starts with the required `contextMemory` domain — ... identity line and omits the implementation’s cross-domain collaborators (wire, tokenCounting, and event) and Agent binding scope. Restore those boundary details here, and in the other source headers shortened by this change, so the module documentation follows the package convention.

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

Useful? React with 👍 / 👎.


import { Disposable } from '#/_base/di/lifecycle';
Expand All @@ -30,18 +33,21 @@ import {
} from './contextMemory';
import { buildContextCompactionShape, type TokenEstimate } from './compactionHandoff';
import {
computeUndoCut,
ContextModel,
contextAppendLoopEvent,
contextAppendMessage,
contextApplyCompaction,
contextClear,
contextUndo,
} from './contextOps';
import {
computeUndoCut,
isFullyUndoable,
type UndoCut,
} from './contextOps';
} from './conversationTime';
import type { LoopRecordedEvent } from './loopEventFold';
import type { ContextMessage } from './types';
import { deriveVisibleMessages } from './visibleWindow';

declare module '#/app/event/eventBus' {
interface DomainEventMap {
Expand Down Expand Up @@ -75,7 +81,11 @@ export class AgentContextMemoryService extends Disposable implements IAgentConte
}

get(): readonly ContextMessage[] {
return this.wire.getModel(ContextModel) as readonly ContextMessage[];
return deriveVisibleMessages(this.getMessageLog());
}

getMessageLog(): readonly ContextMessage[] {
return this.wire.getModel(ContextModel).messages as readonly ContextMessage[];
}

append(...messages: readonly ContextMessage[]): void {
Expand Down
Loading
Loading