Skip to content

Feature/agent compaction#68

Open
Pradeesh333 wants to merge 4 commits into
xynehq:mainfrom
Pradeesh333:feature/agent-compaction
Open

Feature/agent compaction#68
Pradeesh333 wants to merge 4 commits into
xynehq:mainfrom
Pradeesh333:feature/agent-compaction

Conversation

@Pradeesh333
Copy link
Copy Markdown

deterministic compaction when tokens exceeds 180_000 by trimming old tool pairs

@codecov-commenter
Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 23.25581% with 66 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/core/compaction.ts 22.66% 57 Missing and 1 partial ⚠️
src/core/engine.ts 27.27% 8 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds deterministic, token-threshold-based message compaction to prevent oversized runs by trimming older tool-call/tool-result pairs once the message history exceeds ~180k estimated tokens.

Changes:

  • Introduces src/core/compaction.ts with token estimation and tool-group trimming logic plus a trace event helper.
  • Hooks compaction into runInternal in src/core/engine.ts and emits a memory_operation trace event for compaction.
  • Extends TraceEvent['memory_operation'] to support operation: 'compact' and optional metadata.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 5 comments.

File Description
src/core/types.ts Extends trace event typing for compaction metadata.
src/core/engine.ts Triggers compaction before agent execution and emits a compaction trace event.
src/core/compaction.ts Implements token estimation + deterministic trimming of tool interaction groups.
package-lock.json Updates lockfile dependency set (includes cfb).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/core/engine.ts Outdated
Comment on lines +448 to +451
safeConsole.log(`[JAF:ENGINE] Compaction complete: removed ${result.removedCount} messages, reduced from ${result.originalTokens} to ${result.compactedTokens} tokens`);

// Emit compaction event
config.onEvent?.(createCompactionEvent(result, state.runId as string, state.traceId as string));
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

shouldCompact can be true even when there are no removable tool groups (e.g., long user/assistant text history), in which case compactState returns removedCount === 0 and token counts don’t change. The current logs and createCompactionEvent emission still claim compaction completed; consider guarding the “complete” log + event behind result.removedCount > 0, and emitting a distinct “compaction skipped/no-op” message if nothing could be removed.

Suggested change
safeConsole.log(`[JAF:ENGINE] Compaction complete: removed ${result.removedCount} messages, reduced from ${result.originalTokens} to ${result.compactedTokens} tokens`);
// Emit compaction event
config.onEvent?.(createCompactionEvent(result, state.runId as string, state.traceId as string));
if (result.removedCount > 0) {
safeConsole.log(`[JAF:ENGINE] Compaction complete: removed ${result.removedCount} messages, reduced from ${result.originalTokens} to ${result.compactedTokens} tokens`);
// Emit compaction event
config.onEvent?.(createCompactionEvent(result, state.runId as string, state.traceId as string));
} else {
safeConsole.log(`[JAF:ENGINE] Compaction skipped: no removable messages found; token count remains ${result.originalTokens}`);
}

Copilot uses AI. Check for mistakes.
Comment thread src/core/compaction.ts Outdated
@@ -0,0 +1,243 @@
import { Message, RunState, TraceEvent, getTextContent } from './types.js';
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

getTextContent is imported but never used in this module. This will fail lint/typecheck in repos that enforce no-unused-vars; remove the import or use it as part of token estimation.

Suggested change
import { Message, RunState, TraceEvent, getTextContent } from './types.js';
import { Message, RunState, TraceEvent } from './types.js';

Copilot uses AI. Check for mistakes.
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.

This is relevant

Comment thread src/core/compaction.ts Outdated
Comment on lines +155 to +164
// Calculate tokens for the entire group
let groupTokens = 0;
const groupIndices: number[] = [];

for (let j = i; j < messages.length; j++) {
if (toolGroups.get(j) === groupId) {
groupTokens += estimateMessageTokens(messages[j]);
groupIndices.push(j);
}
}
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

trimToolMessages computes each tool-group’s token total by scanning from the current index to the end of the message list. In the compaction-triggered scenario (very large histories) this becomes O(n²) and can add significant latency. Consider precomputing a map of groupId -> {indices, tokens} in a single pass and then trimming groups from the front using that precomputed structure.

Copilot uses AI. Check for mistakes.
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.

relevant

Comment thread src/core/compaction.ts
Comment on lines +224 to +230
traceId: string
): TraceEvent {
return {
type: 'memory_operation',
data: {
operation: 'compact',
conversationId: runId,
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

createCompactionEvent sets conversationId to runId, but elsewhere the engine/memory integration treats conversationId as config.conversationId (the persisted conversation key). Using runId here makes this event ambiguous and can break consumers correlating memory operations by conversation. Consider passing the actual conversation id (e.g., config.conversationId when available) and, if absent, falling back to runId explicitly so semantics are clear.

Suggested change
traceId: string
): TraceEvent {
return {
type: 'memory_operation',
data: {
operation: 'compact',
conversationId: runId,
traceId: string,
conversationId?: string
): TraceEvent {
return {
type: 'memory_operation',
data: {
operation: 'compact',
conversationId: conversationId ?? runId,

Copilot uses AI. Check for mistakes.
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.

relevant

Comment thread src/core/engine.ts
Comment on lines +439 to 455
// Token-based compaction: check and trim if messages exceed limit
let compactionState = state;
if (shouldCompact(state.messages, defaultCompactionConfig)) {
const currentTokens = estimateTotalTokens(state.messages);
safeConsole.log(`[JAF:ENGINE] Compaction triggered: ${currentTokens} tokens exceed ${defaultCompactionConfig.maxTokenLimit} limit`);

const { state: compactedState, result } = compactState(state, defaultCompactionConfig);
compactionState = compactedState;

safeConsole.log(`[JAF:ENGINE] Compaction complete: removed ${result.removedCount} messages, reduced from ${result.originalTokens} to ${result.compactedTokens} tokens`);

// Emit compaction event
config.onEvent?.(createCompactionEvent(result, state.runId as string, state.traceId as string));
}

const currentAgent = config.agentRegistry.get(compactionState.currentAgentName);
if (!currentAgent) {
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

Compaction updates are computed into compactionState, but the function continues to use the original state for subsequent processing (events, messageCount, and presumably the LLM prompt later). As a result, the compaction has no effect on token usage and the emitted agent_processing event still reports the pre-compaction message list. Consider reassigning state to the compacted state (or consistently use compactionState for all downstream reads) so the trimmed message history is actually used.

Copilot uses AI. Check for mistakes.
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.

relevant

Comment thread src/core/engine.ts Outdated

// Token-based compaction: check and trim if messages exceed limit
let compactionState = state;
if (shouldCompact(state.messages, defaultCompactionConfig)) {
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.

We are always checking here whether to compact or not based on default hardcoded config , but ideally we should provide the developer consuming this framework to pass the values for this configuration, whether to enable of disable compaction and what limits to set , and the hardcoded values can be used as fallback

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.

4 participants