Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 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/docs/wire-manifest.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,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
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
/**
* `contextMemory` domain helper — derives the v1-compatible full-compaction
* handoff shape for live rewrites, wire replay, and snapshot reducers.
*
* 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.
* Builds the bounded context window produced by compaction and exposes the
* shared user-message selection rules used by live execution and replay.

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 Restore the contextMemory identity in the header

This rewritten header no longer begins with the required `contextMemory` domain — ... identity line and omits the helper’s cross-domain roles, including token estimation through kosong and reminder rendering through systemReminder. Restore those ownership details in the top-of-file block so the module remains discoverable under the package’s mandatory header convention.

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

Useful? React with 👍 / 👎.

*/

import { estimateTokens, estimateTokensForMessage, estimateTokensForMessages } from '#/kosong/contract/tokens';
Expand All @@ -24,7 +16,6 @@ export const COMPACTION_ELISION_VARIANT = 'compaction_elision';

type MessageLike = ContextMessage;

/** Injectable token-count estimates; see the file header for who passes what. */
export interface TokenEstimate {
readonly text: (text: string) => number;
readonly message: (message: MessageLike) => number;
Expand All @@ -51,15 +42,7 @@ export interface ContextCompactionShapeInput {
readonly compactedCount: number;
readonly tokensBefore: number;
readonly tokensAfter?: number;
/** Measured output tokens of the compaction LLM exchange — the REAL size of
* the generated summary. Preferred over the summary-text estimate in the
* `tokensAfter` fallback when present. */
readonly summaryOutputTokens?: number;
/** Estimated fixed request overhead (system prompt + non-deferred tool
* schemas) surviving the compaction; counted into the `tokensAfter`
* fallback so the result stays on the same full-request basis as the
* measured exchange anchors. Live path only — replay reads the persisted
* `tokensAfter` verbatim. */
readonly requestOverheadTokens?: number;
readonly keptUserMessageCount?: number;
readonly keptHeadUserMessageCount?: number;
Expand Down Expand Up @@ -139,6 +122,7 @@ export function buildContextCompactionShape(
};
}


export function buildCompactionSummaryText(summary: string): string {
const suffix = summary.trim();
return `${COMPACTION_SUMMARY_PREFIX}\n${suffix.length > 0 ? suffix : '(no summary available)'}`;
Expand Down
18 changes: 15 additions & 3 deletions packages/agent-core-v2/src/agent/contextMemory/vacuousContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,19 @@
import type { ContentPart } from '#/kosong/contract/message';

export function isVacuousContentPart(part: ContentPart): boolean {
if (part.type === 'text') return part.text.trim().length === 0;
if (part.type === 'think') return part.encrypted === undefined && part.think.trim().length === 0;
return false;
switch (part.type) {
case 'text':
return part.text.trim().length === 0;
case 'think':
return part.encrypted === undefined && part.think.trim().length === 0;
case 'image_url':
case 'audio_url':
case 'video_url':
return false;
default: {
const exhaustive: never = part;
void exhaustive;
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@
* Defines wire-safe history projections and an opaque snapshot of the media
* identities that a provider rejected, allowing later steps to strip only
* that content while preserving newly generated recovery media.
*
* Projection variability is expressed as data: a `ProjectionPolicy` —
* `structure: 'strict'` adds the structural repairs strict providers need
* (duplicate tool calls dropped, consecutive assistants merged, leading
* non-user messages dropped); `media` selects the provider-rejection
* fallback (`'degraded'` replaces all but the most recent media with text
* markers after an HTTP 413; `{ strip }` replaces exactly the snapshotted
* media identities after a rejected-format or still-too-large resend).
*/

import { createDecorator } from '#/_base/di/instantiation';
Expand All @@ -17,17 +25,19 @@ export interface MediaStripSnapshot {
readonly [mediaStripSnapshotBrand]: undefined;
}

export interface ProjectionPolicy {
readonly structure?: 'strict';
readonly media?: 'degraded' | { readonly strip: MediaStripSnapshot };
}

export interface IAgentContextProjectorService {
readonly _serviceBrand: undefined;

project(messages: readonly ContextMessage[]): readonly Message[];
projectStrict(messages: readonly ContextMessage[]): readonly Message[];
projectMediaDegraded(messages: readonly ContextMessage[]): readonly Message[];
captureMediaStripSnapshot(messages: readonly ContextMessage[]): MediaStripSnapshot;
projectMediaStripped(
project(
messages: readonly ContextMessage[],
snapshot?: MediaStripSnapshot,
policy?: ProjectionPolicy,
): readonly Message[];
captureMediaStripSnapshot(messages: readonly ContextMessage[]): MediaStripSnapshot;
}

export const IAgentContextProjectorService = createDecorator<IAgentContextProjectorService>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,13 @@
* repair-dedup signature (`lastRepairSignature`) is registered into
* `agentState` (`IAgentStateService`) and read/written through it.
*
* `projectMediaDegraded` / `projectMediaStripped` are the fallback
* projections for the two deterministic provider rejections: media-degraded
* (all but the most recent media replaced by text markers) resends after an
* HTTP 413 body-size rejection; media-stripped captures every media identity
* present when degraded media is still too large or an image format is
* rejected, then replaces only that snapshot on later steps so a newly
* generated recovery image remains visible. Both are read-side only — the
* history keeps its media.
* `policy.media` selects the fallback projections for the two deterministic
* provider rejections: `'degraded'` (all but the most recent media replaced
* by text markers) resends after an HTTP 413 body-size rejection;
* `{ strip }` replaces only the snapshotted media identities present when
* degraded media is still too large or an image format is rejected, so a
* newly generated recovery image remains visible on later steps. Both are
* read-side only — the history keeps its media.
*/

import { createHash } from 'node:crypto';
Expand All @@ -40,6 +39,7 @@ import { ITelemetryService } from '#/app/telemetry/telemetry';
import {
IAgentContextProjectorService,
type MediaStripSnapshot,
type ProjectionPolicy,
} from './contextProjector';

export const contextProjectorLastRepairSignatureKey = defineState<string | null>(
Expand All @@ -66,36 +66,24 @@ export class AgentContextProjectorService implements IAgentContextProjectorServi
this.states.set(contextProjectorLastRepairSignatureKey, value);
}

project(messages: readonly ContextMessage[]): readonly Message[] {
return this.projectWithTrace(messages, project);
}

projectStrict(messages: readonly ContextMessage[]): readonly Message[] {
return this.projectWithTrace(messages, projectStrict);
}

projectMediaDegraded(messages: readonly ContextMessage[]): readonly Message[] {
return degradeOlderMediaParts(
this.projectWithTrace(messages, project),
MEDIA_DEGRADE_KEEP_RECENT,
project(
messages: readonly ContextMessage[],
policy: ProjectionPolicy = {},
): readonly Message[] {
const projected = this.projectWithTrace(
messages,
policy.structure === 'strict' ? projectStrict : project,
);
const media = policy.media;
if (media === undefined) return projected;
if (media === 'degraded') return degradeOlderMediaParts(projected, MEDIA_DEGRADE_KEEP_RECENT);
return stripMediaPartsBySnapshot(projected, media.strip);
}

captureMediaStripSnapshot(messages: readonly ContextMessage[]): MediaStripSnapshot {
return captureMediaStripSnapshot(this.projectWithTrace(messages, project));
}

projectMediaStripped(
messages: readonly ContextMessage[],
snapshot?: MediaStripSnapshot,
): readonly Message[] {
const projected = this.projectWithTrace(messages, project);
return stripMediaPartsBySnapshot(
projected,
snapshot ?? captureMediaStripSnapshot(projected),
);
}

private projectWithTrace(
messages: readonly ContextMessage[],
fn: (history: readonly ContextMessage[], onAnomaly?: (anomaly: ProjectionAnomaly) => void) => Message[],
Expand All @@ -121,26 +109,17 @@ export class AgentContextProjectorService implements IAgentContextProjectorServi
if (signature === this.lastRepairSignature) return;
this.lastRepairSignature = signature;

let reordered = 0;
let synthesized = 0;
let droppedOrphan = 0;
let duplicateCallsDropped = 0;
let duplicateResultsDropped = 0;
let leadingDropped = 0;
let assistantsMerged = 0;
let whitespaceDropped = 0;
let vacuousDropped = 0;
for (const anomaly of notable) {
if (anomaly.kind === 'tool_result_reordered') reordered += 1;
else if (anomaly.kind === 'tool_result_synthesized') synthesized += 1;
else if (anomaly.kind === 'orphan_tool_result_dropped') droppedOrphan += 1;
else if (anomaly.kind === 'duplicate_tool_call_dropped') duplicateCallsDropped += 1;
else if (anomaly.kind === 'duplicate_tool_result_dropped') duplicateResultsDropped += 1;
else if (anomaly.kind === 'leading_non_user_dropped') leadingDropped += 1;
else if (anomaly.kind === 'consecutive_assistants_merged') assistantsMerged += 1;
else if (anomaly.kind === 'vacuous_message_dropped') vacuousDropped += 1;
else whitespaceDropped += 1;
}
const {
reordered,
synthesized,
droppedOrphan,
duplicateCallsDropped,
duplicateResultsDropped,
leadingDropped,
assistantsMerged,
whitespaceDropped,
vacuousDropped,
} = summarizeProjectionRepairs(notable);
const toolCallIds = [
...new Set(
notable.flatMap((anomaly) => ('toolCallId' in anomaly ? [anomaly.toolCallId] : [])),
Expand Down Expand Up @@ -183,6 +162,46 @@ type ProjectionAnomaly =
| { readonly kind: 'whitespace_text_dropped'; readonly role: string }
| { readonly kind: 'vacuous_message_dropped'; readonly role: string };

interface ProjectionRepairSummary {
readonly reordered: number;
readonly synthesized: number;
readonly droppedOrphan: number;
readonly duplicateCallsDropped: number;
readonly duplicateResultsDropped: number;
readonly leadingDropped: number;
readonly assistantsMerged: number;
readonly whitespaceDropped: number;
readonly vacuousDropped: number;
}

function summarizeProjectionRepairs(
anomalies: readonly ProjectionAnomaly[],
): ProjectionRepairSummary {
const summary = {
reordered: 0,
synthesized: 0,
droppedOrphan: 0,
duplicateCallsDropped: 0,
duplicateResultsDropped: 0,
leadingDropped: 0,
assistantsMerged: 0,
whitespaceDropped: 0,
vacuousDropped: 0,
};
for (const anomaly of anomalies) {
if (anomaly.kind === 'tool_result_reordered') summary.reordered += 1;
else if (anomaly.kind === 'tool_result_synthesized') summary.synthesized += 1;
else if (anomaly.kind === 'orphan_tool_result_dropped') summary.droppedOrphan += 1;
else if (anomaly.kind === 'duplicate_tool_call_dropped') summary.duplicateCallsDropped += 1;
else if (anomaly.kind === 'duplicate_tool_result_dropped') summary.duplicateResultsDropped += 1;
else if (anomaly.kind === 'leading_non_user_dropped') summary.leadingDropped += 1;
else if (anomaly.kind === 'consecutive_assistants_merged') summary.assistantsMerged += 1;
else if (anomaly.kind === 'vacuous_message_dropped') summary.vacuousDropped += 1;
else summary.whitespaceDropped += 1;
}
return summary;
}

type OnAnomaly = (anomaly: ProjectionAnomaly) => void;

export const MEDIA_DEGRADE_KEEP_RECENT = 2;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const llmRequestSchema = z.object({
messageCount: z.number(),
turnStep: z.string().optional(),
attempt: z.string().optional(),
projection: z.enum(['strict', 'media-degraded', 'media-stripped']).optional(),
projection: z.enum(['strict', 'media-degraded', 'media-stripped', 'strict-media-degraded', 'strict-media-stripped']).optional(),
droppedCount: z.number().optional(),
});

Expand Down
Loading
Loading