[Feature] AI Copilot: Automatic Context Compaction and Context Usage Visibility#1756
[Feature] AI Copilot: Automatic Context Compaction and Context Usage Visibility#1756NaveenSanjaya wants to merge 23 commits intowso2:release/bi-1.8.xfrom
Conversation
…brary and integrate it into the Ballerina AI panel and agent.
…stom instructions
…tead of PRE_TURN_THRESHOLD
…ndReviewActionsNotification method
- Updated `AgentExecutor` to adjust the compaction threshold to 80% of the context window. - Enhanced `CompactionManager` to ensure `.ballerina/copilot/compaction-backups/` is added to the project's .gitignore if not already present. - Refactored `ContextUsageWidget` styles for improved layout and opacity handling. - Modified `AIChat` component to trim messages after restoring a checkpoint, ensuring the UI reflects the correct state and clears stale context usage.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds an end-to-end conversation compaction feature: new compaction library package, agent integration for pre‑flight and mid‑stream compaction, RPCs/types/UI for manual compaction and context‑usage, storage metadata for compacted generations, and UI widgets/events to surface context usage and compaction lifecycle. Changes
Sequence Diagram(s)sequenceDiagram
rect rgba(200,230,255,0.5)
actor User
participant AgentExecutor
participant CompactionManager
participant CompactionEngine
participant LLM
participant Storage
end
User->>AgentExecutor: send query / start multi-step session
AgentExecutor->>CompactionManager: checkAndCompact(workspace, thread)
alt needs compaction
CompactionManager->>CompactionEngine: compact(history, mode='auto')
CompactionEngine->>LLM: summarization request (summarizationCallback)
LLM-->>CompactionEngine: summary
CompactionEngine-->>CompactionManager: compaction result
CompactionManager->>Storage: backup & replace history
CompactionManager-->>AgentExecutor: compaction_end
else no compaction
CompactionManager-->>AgentExecutor: no-op
end
AgentExecutor->>AgentExecutor: start streaming with CompactionGuard
loop per step
AgentExecutor->>CompactionManager: maybeCompact (via CompactionGuard)
alt guard triggers compaction
CompactionManager->>CompactionEngine: compact(recent, mode='manual-midstream')
CompactionEngine->>LLM: summarization request
LLM-->>CompactionEngine: summary
CompactionEngine-->>AgentExecutor: updated messages
end
AgentExecutor->>AgentExecutor: emit usage_metrics
end
AgentExecutor-->>User: final response
sequenceDiagram
participant Webview as UI
participant RpcClient
participant RpcManager
participant RpcHandler
Webview->>RpcClient: getShowContextUsage()
RpcClient->>RpcManager: send RPC
RpcManager->>RpcHandler: getShowContextUsage()
RpcHandler-->>RpcManager: boolean
RpcManager-->>RpcClient: boolean
RpcClient-->>Webview: showContextUsage
Note right of Webview: UI also receives onChatNotify events: usage_metrics, compaction_start/end/failed
RpcHandler->>Webview: forward compaction_start/end/failed and usage_metrics via onChatNotify
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 16
🧹 Nitpick comments (5)
workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChatInput/index.tsx (1)
140-140: Consider sharing the context-usage type instead of duplicating it inline.The inline object type at Line 140 mirrors
ContextUsageWidgetprops and can drift over time. Reusing a shared exported type here would keep UI contracts synchronized.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChatInput/index.tsx` at line 140, The inline type for the prop contextUsage in AIChatInput duplicates the shape used by ContextUsageWidget props; export and reuse a shared type instead. Create or import a shared exported type (e.g., ContextUsage or ContextUsageProps) from the module that defines ContextUsageWidget, then replace the inline object type on the contextUsage prop in AIChatInput with that shared type so both AIChatInput and ContextUsageWidget reference the same exported type.workspaces/common-libs/copilot-utilities/src/compaction/config/defaults.ts (1)
31-35: Centralize derived threshold constants to reduce cross-module drift.These values are correct, but they’re duplicated in multiple places. Exporting derived constants (e.g., effective window / pre-turn threshold) from here and consuming them downstream would prevent future mismatch bugs.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@workspaces/common-libs/copilot-utilities/src/compaction/config/defaults.ts` around lines 31 - 35, The DEFAULT_MODEL_CONFIG currently embeds derived values (maxContextWindow, maxOutputTokens, autoCompactBuffer) that are also duplicated downstream; introduce and export explicit derived constants (e.g., EFFECTIVE_CONTEXT_WINDOW, PRE_TURN_THRESHOLD, EFFECTIVE_MAX_OUTPUT_TOKENS) from this module alongside ModelConfig and DEFAULT_MODEL_CONFIG, compute them from the existing numeric values here, and update downstream modules to import and use those new constants instead of hardcoding the same numbers so all consumers reference a single source of truth.workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-handler.ts (1)
157-163: Configuration change listener disposable is not managed.The
workspace.onDidChangeConfigurationreturns aDisposablethat should be tracked and disposed when the extension deactivates. Currently, this listener will persist for the lifetime of the extension host, which is acceptable but not ideal practice.If
registerAiPanelRpcHandlersis expected to return disposables for cleanup, consider returning this subscription. Otherwise, this is a minor concern since the listener is intentionally long-lived.♻️ Option to track the disposable
-export function registerAiPanelRpcHandlers(messenger: Messenger) { +export function registerAiPanelRpcHandlers(messenger: Messenger): vscode.Disposable[] { const rpcManger = new AiPanelRpcManager(); // ... existing handlers ... // Notify webview immediately when the showContextUsage setting is toggled - workspace.onDidChangeConfiguration((e) => { + const configDisposable = workspace.onDidChangeConfiguration((e) => { if (e.affectsConfiguration('ballerina.ai.showContextUsage')) { const value = workspace.getConfiguration('ballerina').get<boolean>('ai.showContextUsage', false); sendConfigChangeNotification('showContextUsage', value); } }); + + return [configDisposable]; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-handler.ts` around lines 157 - 163, The onDidChangeConfiguration subscription created by workspace.onDidChangeConfiguration in registerAiPanelRpcHandlers is not being tracked for disposal; capture its returned Disposable (e.g., const configListener = workspace.onDidChangeConfiguration(...)) and either push it into the extension's disposables (context.subscriptions.push(configListener)) or include it in the function's returned Disposable array so callers can clean it up; update registerAiPanelRpcHandlers to return or accept a disposables collector and add the configListener to that collection to ensure proper cleanup.workspaces/common-libs/copilot-utilities/src/compaction/core/ThresholdCalculator.ts (1)
42-45: Consider validating against negative/zero thresholds.If a caller provides a custom
ModelConfigwhereautoCompactBuffer >= (maxContextWindow - maxOutputTokens), the threshold would be zero or negative, causing compaction to trigger on every check. This is unlikely with the default config but could occur with misconfiguration.🛡️ Optional defensive check
getAutoCompactThreshold(): number { const effectiveWindow = this.config.maxContextWindow - this.config.maxOutputTokens; - return effectiveWindow - this.config.autoCompactBuffer; + const threshold = effectiveWindow - this.config.autoCompactBuffer; + if (threshold <= 0) { + console.warn(`Invalid compaction threshold ${threshold}, using effectiveWindow/2`); + return Math.floor(effectiveWindow / 2); + } + return threshold; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@workspaces/common-libs/copilot-utilities/src/compaction/core/ThresholdCalculator.ts` around lines 42 - 45, The getAutoCompactThreshold method in ThresholdCalculator can return zero or negative when autoCompactBuffer >= (maxContextWindow - maxOutputTokens); add a defensive check in getAutoCompactThreshold to compute the effectiveWindow as now, then if effectiveWindow - autoCompactBuffer <= 0 return a safe minimum (e.g., 1) or throw a clear configuration error; update the logic in getAutoCompactThreshold to use Math.max(1, effectiveWindow - this.config.autoCompactBuffer) (or validate and throw) so compaction doesn't trigger on every check and include a brief comment referencing ModelConfig.autoCompactBuffer, maxContextWindow and maxOutputTokens.workspaces/common-libs/copilot-utilities/src/compaction/utils/messageUtils.ts (1)
33-36: Consider stronger return type instead ofany[].The return type
any[]loses type information. Since this returns a fixed structure of user/assistant messages, consider defining a proper type or using the existing message interface from your codebase.♻️ Proposed type improvement
+interface ContinuationMessage { + role: 'user' | 'assistant'; + content: string; +} + export function createContinuationMessages( summary: string, projectState?: ProjectStateContext -): any[] { +): ContinuationMessage[] {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@workspaces/common-libs/copilot-utilities/src/compaction/utils/messageUtils.ts` around lines 33 - 36, The function createContinuationMessages currently returns any[] which loses type safety; update its signature to return the concrete message type used across the codebase (e.g., Message[] or ChatMessage[]), import or reference the existing message interface instead of any, and ensure the objects you build inside createContinuationMessages conform to that interface (include role/content/types matching Message or ChatMessage); also update any callers of createContinuationMessages to accept the stronger type and fix any property mismatches using ProjectStateContext where needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@workspaces/ballerina/ballerina-extension/src/features/ai/agent/AgentExecutor.ts`:
- Around line 275-282: The original user prompt is being mangled because
originalUserMessage is set to JSON.stringify(userMessageContent); instead pass
the raw verbatim text so compaction reinserts the exact prompt. Update the
CompactionGuard construction (the originalUserMessage property) to use
userMessageContent directly (or ensure it's not stringified/escaped) instead of
JSON.stringify(userMessageContent) in AgentExecutor.ts where CompactionGuard is
instantiated.
- Around line 198-204: The compaction is building projectState with a snapshot
and the wrong workspace path, causing stale state across threads; change
workspaceId to use the same live path used elsewhere (use
this.config.executionContext.workspacePath ||
this.config.executionContext.projectPath), set threadId from the execution
context (this.config.executionContext.threadId || 'default'), and stop
snapshotting modifiedFiles early—assign projectState.modifiedFiles to the live
modifiedFiles collection (not Array.from(modifiedFiles)) or otherwise read
modifiedFiles at compaction time; keep tempProjectPath and workingDirectory as
before but ensure workingDirectory uses the resolved workspaceId so compaction
uses the same live project state as the rest of the executor.
In
`@workspaces/ballerina/ballerina-extension/src/features/ai/agent/compaction/CompactionGuard.ts`:
- Around line 167-179: The guard currently treats a return value of
messages.length from findCleanSplitPoint as a valid split, which makes
oldMessages = whole history and recentMessages = [], causing full-history
compaction; change performCompaction to detect the "no safe split" signal (the
value messages.length) and handle it explicitly: either abort compaction and
return the original messages unchanged or set cleanSplitIndex so the last
preserveRecentMessageCount messages are preserved verbatim; update the logic
around cleanSplitIndex/oldMessages/recentMessages in performCompaction to use
that special-case behavior (refer to findCleanSplitPoint, performCompaction,
oldMessages, recentMessages, and preserveRecentMessageCount).
- Around line 336-348: The bug is that resolvedKeys is being populated from
earlier successes, which incorrectly resolves failures that occur after those
successes; change the scan to iterate events in reverse (from last to first) and
add a success's `${events[i].toolName}::${events[i].target}` to resolvedKeys
only when that success appears after a failure, so unresolvedFailures (the
filter using unresolvedKeys) correctly retains failures that occur after any
success; update the loop that builds resolvedKeys (currently iterating forward)
to iterate backwards over events and keep the same key format used by
unresolvedFailures, referencing events, resolvedKeys, unresolvedFailures,
isError, toolName and target.
In
`@workspaces/ballerina/ballerina-extension/src/features/ai/compaction-manager.ts`:
- Around line 243-259: The current ensureGitignoreEntry silently no-ops on any
read error, leaving repos without a .gitignore unprotected; update
ensureGitignoreEntry(workspaceId: string) to specifically handle the
missing-file case by creating a new .gitignore containing the required entry
('.ballerina/' or the more specific '.ballerina/copilot/compaction-backups/' as
appropriate) using fs.promises.writeFile with 'utf-8', while preserving the
existing behavior for other errors (rethrow or log); locate ensureGitignoreEntry
in compaction-manager.ts and add an ENOENT branch that writes the entry plus
newline instead of swallowing the error.
In `@workspaces/ballerina/ballerina-extension/src/features/ai/utils/events.ts`:
- Around line 116-121: The switch case for "web_tool_approval_request" is
missing a break and falls through into "compaction_start"; after calling
sendWebToolApprovalNotification(event.requestId, event.toolName, event.content)
add a break so the handler exits and does not execute the compaction logging and
RPCLayer._messenger.sendNotification(onChatNotify, { type: "webview",
webviewType: AiPanelWebview.viewType }, event) meant only for
"compaction_start".
In
`@workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-manager.ts`:
- Around line 727-748: compactConversation uses
StateMachine.context().projectPath to build the workspace key, but the rest of
this manager uses resolveProjectRootPath() so active-execution and compaction
lookups miss the correct thread; replace the workspaceId assignment in
compactConversation (currently using StateMachine.context().projectPath) with
the project root from resolveProjectRootPath() so
chatStateStorage.getActiveExecution(...) and
compactionManager.manualCompact(...) operate on the same workspace key as the
rest of the class.
In
`@workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/ai-panel/rpc-client.ts`:
- Around line 318-320: The getShowContextUsage method has its closing brace
misindented and the class is missing a blank line before the final closing
brace; adjust the closing brace for getShowContextUsage so it aligns with the
method declaration (not the method body) and add a single blank line before the
class's final closing brace to match the formatting of other methods in the
class containing getShowContextUsage, _messenger.sendRequest and HOST_EXTENSION.
In
`@workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/compaction/ContextUsageWidget/index.tsx`:
- Around line 22-25: The tooltip percentages are being computed against
PRE_TURN_THRESHOLD (178,808) while the header/ring use MAX_CONTEXT_WINDOW
(200_000), causing mismatched totals; update the toPct function (and any places
where it divides by PRE_TURN_THRESHOLD) to divide by MAX_CONTEXT_WINDOW instead
(or otherwise normalize using MAX_CONTEXT_WINDOW) so all category and overall
percentages use the same 200_000 scale; ensure every usage of toPct (including
the sections referenced by the reviewer) is updated so tooltip values sum to the
displayed overall usage.
In
`@workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/index.tsx`:
- Around line 1837-1839: The Footer component animates its own trailing dots, so
passing a loadingLabel with an ellipsis causes duplicated/missing dots; update
the AIChat props so loadingLabel is the base string when compacting (e.g. pass
"Compacting conversation" instead of "Compacting conversation...") by adjusting
the prop where isLoading/isCompacting are set (look for the Footer usage and the
isLoading/loadingLabel props in AIChat/Index.tsx) so Footer can append its own
dots correctly.
In `@workspaces/common-libs/copilot-utilities/src/compaction/CompactionEngine.ts`:
- Around line 214-247: In compactWithRetry (CompactionEngine.ts) the method
currently falls through to return success: true even when compactedTokens >=
threshold after exhausting retries; change the final return so that if
compactedTokens >= threshold and retryCount >= maxRetries the method returns
success: false (or throws) and includes the same metadata (compactedTokens,
originalTokens, retriesUsed, compactedMessages, summary and a descriptive
failure reason) so callers know compaction failed to meet budget instead of
treating it as success.
- Around line 113-158: The concurrency guard currently throws before the try
block, causing overlapping callers to receive a rejected promise instead of a
graceful failure result; update compact(messages, options) so the check for
this.isCompacting happens inside the try and returns the same failure-shaped
object the catch block returns (use this.tokenEstimator.estimateTokens(messages)
and populate metadata similar to the catch-return), then only set
this.isCompacting = true when proceeding with compaction before calling
this.compactWithRetry(messages, options, 0); keep the finally block to reset
this.isCompacting = false and continue using compactWithRetry, tokenEstimator,
and summarizationService as before.
In
`@workspaces/common-libs/copilot-utilities/src/compaction/core/SummarizationService.ts`:
- Around line 58-75: The current guard skips pushing the terminal "SUMMARIZATION
MODE" user prompt when preparedMessages already ends with a user message; always
append this terminal summarization instruction to ensure compaction is
deterministic. Modify the logic around preparedMessages (in
SummarizationService) so that you unconditionally push the user message with the
"END OF CONVERSATION TO SUMMARIZE" / "SUMMARIZATION MODE" content (or, if you
must, push it even when the last item is a user by inserting after the last
element) rather than only when preparedMessages.length === 0 || last.role !==
'user'; ensure the unique prompt text and tags remain exactly as shown so the
model enters summarization mode consistently.
- Around line 96-129: The code in SummarizationService is logging and throwing
raw LLM responses (variables response, extractedSummary, withoutAnalysis) which
can leak sensitive prompt/tooling details; remove or replace any
console.log/console.warn that prints full response content and change the thrown
Error that embeds response.substring(0,200) so it does not include raw text.
Instead log only non-sensitive metadata (e.g., response length, whether
<summary> tags were found) or a hashed/truncated token-safe preview, and when
throwing, include no raw content — only a safe diagnostic message referencing
SummarizationService and the response length/status.
In
`@workspaces/common-libs/copilot-utilities/src/compaction/core/TokenEstimator.ts`:
- Around line 72-100: The current estimateMessageTokens spreads
tokenCountCallback(messages) evenly across each message and caches per-message
averages, causing wrong estimates for subsets; change it to cache the exact
prompt shape: compute a composite hash for the whole messages array (use
this.hashContent(messages) or a dedicated method), store one cache entry keyed
by that composite hash with tokenCount: totalTokens and contentHash:
compositeHash, and on subsequent calls check for that composite cache first (in
estimateMessageTokens) instead of assuming per-message averages; remove or stop
creating averaged per-message cache entries so per-subset estimates won't drift.
In
`@workspaces/common-libs/copilot-utilities/src/compaction/utils/messagePreparation.ts`:
- Around line 49-87: The current mapping still returns messages whose content
becomes an empty array (filteredContent) after stripping null/thinking blocks,
causing downstream providers to reject empty messages; update the logic around
filteredContent (the variable created inside the map and the return { ...msg,
content: filteredContent }) to skip or remove any message where
filteredContent.length === 0 (e.g., return null/undefined for those entries and
filter them out, or filter the mapped array afterwards) so summarizationCallback
and providers never receive messages with content: [].
---
Nitpick comments:
In
`@workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-handler.ts`:
- Around line 157-163: The onDidChangeConfiguration subscription created by
workspace.onDidChangeConfiguration in registerAiPanelRpcHandlers is not being
tracked for disposal; capture its returned Disposable (e.g., const
configListener = workspace.onDidChangeConfiguration(...)) and either push it
into the extension's disposables (context.subscriptions.push(configListener)) or
include it in the function's returned Disposable array so callers can clean it
up; update registerAiPanelRpcHandlers to return or accept a disposables
collector and add the configListener to that collection to ensure proper
cleanup.
In
`@workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChatInput/index.tsx`:
- Line 140: The inline type for the prop contextUsage in AIChatInput duplicates
the shape used by ContextUsageWidget props; export and reuse a shared type
instead. Create or import a shared exported type (e.g., ContextUsage or
ContextUsageProps) from the module that defines ContextUsageWidget, then replace
the inline object type on the contextUsage prop in AIChatInput with that shared
type so both AIChatInput and ContextUsageWidget reference the same exported
type.
In `@workspaces/common-libs/copilot-utilities/src/compaction/config/defaults.ts`:
- Around line 31-35: The DEFAULT_MODEL_CONFIG currently embeds derived values
(maxContextWindow, maxOutputTokens, autoCompactBuffer) that are also duplicated
downstream; introduce and export explicit derived constants (e.g.,
EFFECTIVE_CONTEXT_WINDOW, PRE_TURN_THRESHOLD, EFFECTIVE_MAX_OUTPUT_TOKENS) from
this module alongside ModelConfig and DEFAULT_MODEL_CONFIG, compute them from
the existing numeric values here, and update downstream modules to import and
use those new constants instead of hardcoding the same numbers so all consumers
reference a single source of truth.
In
`@workspaces/common-libs/copilot-utilities/src/compaction/core/ThresholdCalculator.ts`:
- Around line 42-45: The getAutoCompactThreshold method in ThresholdCalculator
can return zero or negative when autoCompactBuffer >= (maxContextWindow -
maxOutputTokens); add a defensive check in getAutoCompactThreshold to compute
the effectiveWindow as now, then if effectiveWindow - autoCompactBuffer <= 0
return a safe minimum (e.g., 1) or throw a clear configuration error; update the
logic in getAutoCompactThreshold to use Math.max(1, effectiveWindow -
this.config.autoCompactBuffer) (or validate and throw) so compaction doesn't
trigger on every check and include a brief comment referencing
ModelConfig.autoCompactBuffer, maxContextWindow and maxOutputTokens.
In
`@workspaces/common-libs/copilot-utilities/src/compaction/utils/messageUtils.ts`:
- Around line 33-36: The function createContinuationMessages currently returns
any[] which loses type safety; update its signature to return the concrete
message type used across the codebase (e.g., Message[] or ChatMessage[]), import
or reference the existing message interface instead of any, and ensure the
objects you build inside createContinuationMessages conform to that interface
(include role/content/types matching Message or ChatMessage); also update any
callers of createContinuationMessages to accept the stronger type and fix any
property mismatches using ProjectStateContext where needed.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: ec83eca1-1e13-4277-9ae1-ac22d9bde8b1
⛔ Files ignored due to path filters (1)
common/config/rush/pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (38)
rush.jsonworkspaces/ballerina/ballerina-core/src/interfaces/ai-panel.tsworkspaces/ballerina/ballerina-core/src/rpc-types/ai-panel/index.tsworkspaces/ballerina/ballerina-core/src/rpc-types/ai-panel/interfaces.tsworkspaces/ballerina/ballerina-core/src/rpc-types/ai-panel/rpc-type.tsworkspaces/ballerina/ballerina-core/src/state-machine-types.tsworkspaces/ballerina/ballerina-extension/package.jsonworkspaces/ballerina/ballerina-extension/src/features/ai/agent/AgentExecutor.tsworkspaces/ballerina/ballerina-extension/src/features/ai/agent/compaction/CompactionGuard.tsworkspaces/ballerina/ballerina-extension/src/features/ai/agent/compaction/contextExhausted.tsworkspaces/ballerina/ballerina-extension/src/features/ai/agent/stream-handlers/stream-context.tsworkspaces/ballerina/ballerina-extension/src/features/ai/compaction-manager.tsworkspaces/ballerina/ballerina-extension/src/features/ai/utils/ai-utils.tsworkspaces/ballerina/ballerina-extension/src/features/ai/utils/events.tsworkspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-handler.tsworkspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-manager.tsworkspaces/ballerina/ballerina-extension/src/views/ai-panel/chatStateStorage.tsworkspaces/ballerina/ballerina-rpc-client/src/rpc-clients/ai-panel/rpc-client.tsworkspaces/ballerina/ballerina-visualizer/package.jsonworkspaces/ballerina/ballerina-visualizer/src/views/AIPanel/commandTemplates/data/commandTemplates.const.tsworkspaces/ballerina/ballerina-visualizer/src/views/AIPanel/commandTemplates/data/placeholderTags.const.tsworkspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/Footer/index.tsxworkspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/compaction/ContextUsageWidget/index.tsxworkspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/index.tsxworkspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChatInput/index.tsxworkspaces/common-libs/copilot-utilities/package.jsonworkspaces/common-libs/copilot-utilities/src/compaction/CompactionEngine.tsworkspaces/common-libs/copilot-utilities/src/compaction/config/defaults.tsworkspaces/common-libs/copilot-utilities/src/compaction/core/SummarizationService.tsworkspaces/common-libs/copilot-utilities/src/compaction/core/ThresholdCalculator.tsworkspaces/common-libs/copilot-utilities/src/compaction/core/TokenEstimator.tsworkspaces/common-libs/copilot-utilities/src/compaction/index.tsworkspaces/common-libs/copilot-utilities/src/compaction/prompts/summarizationPrompt.tsworkspaces/common-libs/copilot-utilities/src/compaction/types/index.tsworkspaces/common-libs/copilot-utilities/src/compaction/utils/messagePreparation.tsworkspaces/common-libs/copilot-utilities/src/compaction/utils/messageUtils.tsworkspaces/common-libs/copilot-utilities/src/index.tsworkspaces/common-libs/copilot-utilities/tsconfig.json
workspaces/ballerina/ballerina-extension/src/features/ai/agent/AgentExecutor.ts
Outdated
Show resolved
Hide resolved
workspaces/ballerina/ballerina-extension/src/features/ai/agent/AgentExecutor.ts
Show resolved
Hide resolved
workspaces/ballerina/ballerina-extension/src/features/ai/agent/compaction/CompactionGuard.ts
Show resolved
Hide resolved
workspaces/ballerina/ballerina-extension/src/features/ai/agent/compaction/CompactionGuard.ts
Outdated
Show resolved
Hide resolved
workspaces/ballerina/ballerina-extension/src/features/ai/compaction-manager.ts
Show resolved
Hide resolved
workspaces/common-libs/copilot-utilities/src/compaction/CompactionEngine.ts
Show resolved
Hide resolved
workspaces/common-libs/copilot-utilities/src/compaction/core/SummarizationService.ts
Outdated
Show resolved
Hide resolved
workspaces/common-libs/copilot-utilities/src/compaction/core/SummarizationService.ts
Show resolved
Hide resolved
workspaces/common-libs/copilot-utilities/src/compaction/core/TokenEstimator.ts
Outdated
Show resolved
Hide resolved
workspaces/common-libs/copilot-utilities/src/compaction/utils/messagePreparation.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
🧹 Nitpick comments (1)
workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/Footer/index.tsx (1)
176-197: Optional: simplify loading-dot animation state.Current logic works, but parsing/removing trailing dots from the rendered string each tick is harder to maintain than tracking dot count directly.
♻️ Suggested simplification
- const [animatedText, setAnimatedText] = useState("Generating."); + const [animatedText, setAnimatedText] = useState("Generating."); + const [dotCount, setDotCount] = useState(1); useEffect(() => { if (isLoading) { - const baseText = loadingLabel || "Generating"; - setAnimatedText(baseText + "."); + const baseText = loadingLabel || "Generating"; + setDotCount(1); + setAnimatedText(`${baseText}.`); const interval = setInterval(() => { - setAnimatedText((prev) => { - // Extract the base text without dots - const dots = prev.match(/\.+$/)?.[0] || ""; - const base = prev.slice(0, prev.length - dots.length); - - // Cycle through 1, 2, 3 dots - if (dots.length >= 3) return base + "."; - return prev + "."; - }); + setDotCount((prev) => { + const next = prev >= 3 ? 1 : prev + 1; + setAnimatedText(`${baseText}${".".repeat(next)}`); + return next; + }); }, 500); return () => clearInterval(interval); } }, [isLoading, loadingLabel]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/Footer/index.tsx` around lines 176 - 197, The loading-dot animation in the useEffect is overcomplicating by parsing trailing dots from animatedText each tick; change the implementation to track dot count directly (e.g., a dotCount state or ref) and update animatedText by concatenating loadingLabel/baseText with "." repeated dotCount inside the interval; update symbols: useEffect (the effect that depends on isLoading and loadingLabel), animatedText and setAnimatedText, and ensure the interval increments dotCount modulo 3 and clears on cleanup when isLoading becomes false.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In
`@workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/Footer/index.tsx`:
- Around line 176-197: The loading-dot animation in the useEffect is
overcomplicating by parsing trailing dots from animatedText each tick; change
the implementation to track dot count directly (e.g., a dotCount state or ref)
and update animatedText by concatenating loadingLabel/baseText with "." repeated
dotCount inside the interval; update symbols: useEffect (the effect that depends
on isLoading and loadingLabel), animatedText and setAnimatedText, and ensure the
interval increments dotCount modulo 3 and clears on cleanup when isLoading
becomes false.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 61fd9450-d6c0-4798-93d6-9ddd0f8906c8
📒 Files selected for processing (7)
workspaces/ballerina/ballerina-extension/package.jsonworkspaces/ballerina/ballerina-extension/src/features/ai/agent/AgentExecutor.tsworkspaces/ballerina/ballerina-extension/src/features/ai/agent/stream-handlers/stream-context.tsworkspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-manager.tsworkspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/Footer/index.tsxworkspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/index.tsxworkspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChatInput/index.tsx
✅ Files skipped from review due to trivial changes (4)
- workspaces/ballerina/ballerina-extension/src/features/ai/agent/stream-handlers/stream-context.ts
- workspaces/ballerina/ballerina-extension/package.json
- workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/index.tsx
- workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-manager.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChatInput/index.tsx
- workspaces/ballerina/ballerina-extension/src/features/ai/agent/AgentExecutor.ts
- Update AgentExecutor to handle workspace paths more flexibly. - Improve CompactionGuard to log when no safe split point is found. - Refine CompactionManager to create .gitignore if it doesn't exist. - Add logging for compaction start in event handler. - Adjust AiPanelRpcManager to resolve project root path correctly. - Fix AiPanelRpcClient formatting for loading label. - Update ContextUsageWidget to use MAX_CONTEXT_WINDOW for percentage calculation. - Modify SummarizationService to ensure proper message formatting for API calls. - Optimize TokenEstimator for caching and token estimation. - Enhance message preparation utility to filter out empty content.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (4)
workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/index.tsx (2)
1057-1093: Remove or document the commented-out Command.Compact block.This large commented block adds noise. If the feature is deferred, consider removing it and tracking in a separate issue. If it's temporarily disabled for debugging, add a TODO comment explaining the reason.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/index.tsx` around lines 1057 - 1093, The large commented-out Command.Compact block should be either removed or annotated with a TODO explaining why it’s disabled; locate the block that references Command.Compact and the sequence calling rpcClient.getAiPanelRpcClient().compactConversation, convertToUIMessages, and the state setters (setIsCompacting, setIsLoading, setIsCodeLoading, setMessages, setCurrentFileArray, setLastQuestionIndex, setCurrentGeneratingPromptIndex) in AIChat/index.tsx and either delete the entire commented block to reduce noise or replace it with a short TODO comment that names the feature (Compact), why it’s disabled, and a pointer to an issue/PR tracking re-enabling it.
693-698: Consider using a constant for MAX_CONTEXT_WINDOW.The magic number
200_000is hardcoded here but represents the model's context limit which could change. This is also used in threshold calculations elsewhere in the compaction system.♻️ Suggested improvement
+const MAX_CONTEXT_WINDOW = 200_000; + // In the event handler: - const MAX_CONTEXT_WINDOW = 200_000; const percentage = Math.min(100, Math.round((inputTokens / MAX_CONTEXT_WINDOW) * 100));Or better, have the backend include the percentage in the
usage_metricsevent since it already knows the model config.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/index.tsx` around lines 693 - 698, Replace the hardcoded magic number 200_000 with a shared constant (e.g., MODEL_CONTEXT_WINDOW or CONTEXT_WINDOW_LIMIT) and use that constant here inside the "usage_metrics" branch (where inputTokens, percentage, breakdown and setContextUsage are used) so the model context limit is centralized; export/import the constant from the common config/constants module used by the compaction system and update other threshold calculations to reference the same constant (or alternatively have the backend provide the percentage and consume that field here).workspaces/ballerina/ballerina-extension/src/features/ai/agent/compaction/CompactionGuard.ts (1)
195-212: Consider making MID_STREAM_INSTRUCTIONS a module constant.The instruction block is recreated on every compaction. Moving it to module scope would be cleaner and make it easier to maintain/test.
♻️ Suggested refactor
+const MID_STREAM_INSTRUCTIONS = `## Mid-Stream Compaction Context + +CRITICAL: This compaction is happening MID-TASK. The assistant is in the middle of executing a task and will continue immediately after reading this summary. Prioritize: + +1. **Original User Request**: Include the EXACT user request verbatim +2. **Task Progress**: What has been accomplished vs what remains +3. **Files Modified**: List ALL file paths created, read, or modified +4. **Current State**: What was being worked on at the moment of compaction +5. **Pending Work**: Specific next steps needed to complete the task +6. **Errors**: Any unresolved errors or blockers + +The assistant MUST be able to seamlessly continue the task from this summary alone.`; + export class CompactionGuard { // ... in performCompaction(): - const MID_STREAM_INSTRUCTIONS = `...`; const compactionResult = await this.config.engine.compact(oldMessages, { mode: 'auto', projectState: this.config.projectState, abortSignal: this.config.abortSignal, customInstructions: MID_STREAM_INSTRUCTIONS, });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@workspaces/ballerina/ballerina-extension/src/features/ai/agent/compaction/CompactionGuard.ts` around lines 195 - 212, Move the MID_STREAM_INSTRUCTIONS string out of the CompactionGuard method into module scope as a top-level constant (e.g., export const MID_STREAM_INSTRUCTIONS) so it is created once and easier to maintain/test; then update the call to this.config.engine.compact(oldMessages, {..., customInstructions: MID_STREAM_INSTRUCTIONS, ...}) to import/reference that module-level constant and remove the local declaration inside the method (symbols to touch: MID_STREAM_INSTRUCTIONS, CompactionGuard, this.config.engine.compact).workspaces/common-libs/copilot-utilities/src/compaction/CompactionEngine.ts (1)
183-196: Consider validating content type more strictly.The validation checks for presence of
contentbut not its type. Messages withcontent: {}(object instead of string/array) would pass validation but may cause issues downstream in the summarization flow.♻️ Optional stricter validation
private validateMessages(messages: any[]): void { if (!Array.isArray(messages)) { throw new Error('Messages must be an array'); } for (const msg of messages) { if (!msg.role || !['user', 'assistant', 'system', 'tool'].includes(msg.role)) { throw new Error(`Invalid message role: ${msg.role}`); } if (msg.content === undefined || msg.content === null) { throw new Error('Message missing content property'); } + // Content should be string or array + if (typeof msg.content !== 'string' && !Array.isArray(msg.content)) { + throw new Error(`Invalid content type for message: expected string or array`); + } } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@workspaces/common-libs/copilot-utilities/src/compaction/CompactionEngine.ts` around lines 183 - 196, The validateMessages method currently only checks presence of msg.content; update validateMessages to also enforce content type by verifying for each msg in messages that msg.content is either a string or an array of strings (e.g., typeof msg.content === 'string' || (Array.isArray(msg.content) && msg.content.every(c => typeof c === 'string'))), and throw a clear error from validateMessages (referencing validateMessages, messages, msg.role, msg.content) when the content type is invalid so downstream summarization won't receive objects or other unsupported types.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-manager.ts`:
- Around line 710-752: In compactConversation, guard against undefined
compaction results from compactionManager.manualCompact by null-checking result
and its fields (e.g., result.reductionPercentage, result.originalTokens,
result.compactedTokens, result.summary) before using them; replace direct uses
like result.reductionPercentage.toFixed(1) with a safe path (check typeof
reductionPercentage === 'number' then toFixed, or use a default numeric value
via nullish coalescing) and ensure the returned CompactConversationResponse uses
sensible defaults when fields are missing so logging and response construction
in compactConversation and any calls to compactionManager.manualCompact cannot
throw on undefined properties.
In
`@workspaces/common-libs/copilot-utilities/src/compaction/utils/messagePreparation.ts`:
- Around line 99-123: convertToolMessageToText currently returns an object with
content:'' when toolMsg.content is empty or contains only nulls; change it to
return null in that case so callers can filter it out. Inside
convertToolMessageToText, compute content array and textDescriptions as now,
then if textDescriptions is empty (or all items filtered out) return null
instead of {role:'user', content: textDescriptions}; ensure callers already
using .filter((msg: any) => msg !== null) will drop these null returns.
---
Nitpick comments:
In
`@workspaces/ballerina/ballerina-extension/src/features/ai/agent/compaction/CompactionGuard.ts`:
- Around line 195-212: Move the MID_STREAM_INSTRUCTIONS string out of the
CompactionGuard method into module scope as a top-level constant (e.g., export
const MID_STREAM_INSTRUCTIONS) so it is created once and easier to
maintain/test; then update the call to this.config.engine.compact(oldMessages,
{..., customInstructions: MID_STREAM_INSTRUCTIONS, ...}) to import/reference
that module-level constant and remove the local declaration inside the method
(symbols to touch: MID_STREAM_INSTRUCTIONS, CompactionGuard,
this.config.engine.compact).
In
`@workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/index.tsx`:
- Around line 1057-1093: The large commented-out Command.Compact block should be
either removed or annotated with a TODO explaining why it’s disabled; locate the
block that references Command.Compact and the sequence calling
rpcClient.getAiPanelRpcClient().compactConversation, convertToUIMessages, and
the state setters (setIsCompacting, setIsLoading, setIsCodeLoading, setMessages,
setCurrentFileArray, setLastQuestionIndex, setCurrentGeneratingPromptIndex) in
AIChat/index.tsx and either delete the entire commented block to reduce noise or
replace it with a short TODO comment that names the feature (Compact), why it’s
disabled, and a pointer to an issue/PR tracking re-enabling it.
- Around line 693-698: Replace the hardcoded magic number 200_000 with a shared
constant (e.g., MODEL_CONTEXT_WINDOW or CONTEXT_WINDOW_LIMIT) and use that
constant here inside the "usage_metrics" branch (where inputTokens, percentage,
breakdown and setContextUsage are used) so the model context limit is
centralized; export/import the constant from the common config/constants module
used by the compaction system and update other threshold calculations to
reference the same constant (or alternatively have the backend provide the
percentage and consume that field here).
In `@workspaces/common-libs/copilot-utilities/src/compaction/CompactionEngine.ts`:
- Around line 183-196: The validateMessages method currently only checks
presence of msg.content; update validateMessages to also enforce content type by
verifying for each msg in messages that msg.content is either a string or an
array of strings (e.g., typeof msg.content === 'string' ||
(Array.isArray(msg.content) && msg.content.every(c => typeof c === 'string'))),
and throw a clear error from validateMessages (referencing validateMessages,
messages, msg.role, msg.content) when the content type is invalid so downstream
summarization won't receive objects or other unsupported types.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 304183b4-87a0-40a2-a541-f5af2891ede9
📒 Files selected for processing (12)
workspaces/ballerina/ballerina-extension/src/features/ai/agent/AgentExecutor.tsworkspaces/ballerina/ballerina-extension/src/features/ai/agent/compaction/CompactionGuard.tsworkspaces/ballerina/ballerina-extension/src/features/ai/compaction-manager.tsworkspaces/ballerina/ballerina-extension/src/features/ai/utils/events.tsworkspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-manager.tsworkspaces/ballerina/ballerina-rpc-client/src/rpc-clients/ai-panel/rpc-client.tsworkspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/compaction/ContextUsageWidget/index.tsxworkspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/index.tsxworkspaces/common-libs/copilot-utilities/src/compaction/CompactionEngine.tsworkspaces/common-libs/copilot-utilities/src/compaction/core/SummarizationService.tsworkspaces/common-libs/copilot-utilities/src/compaction/core/TokenEstimator.tsworkspaces/common-libs/copilot-utilities/src/compaction/utils/messagePreparation.ts
✅ Files skipped from review due to trivial changes (2)
- workspaces/ballerina/ballerina-extension/src/features/ai/compaction-manager.ts
- workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/ai-panel/rpc-client.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- workspaces/ballerina/ballerina-extension/src/features/ai/utils/events.ts
- workspaces/common-libs/copilot-utilities/src/compaction/core/TokenEstimator.ts
- workspaces/ballerina/ballerina-extension/src/features/ai/agent/AgentExecutor.ts
- workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/compaction/ContextUsageWidget/index.tsx
| async compactConversation(params: CompactConversationRequest): Promise<CompactConversationResponse> { | ||
| const workspaceId = resolveProjectRootPath(); | ||
| const threadId = 'default'; | ||
|
|
||
| // M05: Reject manual compact if an AI generation is in progress | ||
| const activeExecution = chatStateStorage.getActiveExecution(workspaceId, threadId); | ||
| if (activeExecution) { | ||
| return { | ||
| success: false, | ||
| error: 'Cannot compact while a generation is in progress. Please wait for it to complete or stop it first.', | ||
| }; | ||
| } | ||
|
|
||
| try { | ||
| const model = await getAnthropicClient(ANTHROPIC_SONNET_4); | ||
|
|
||
| const result = await compactionManager.manualCompact( | ||
| workspaceId, | ||
| threadId, | ||
| model, | ||
| params.customInstructions | ||
| ); | ||
|
|
||
| console.log( | ||
| `[RPC] Compacted conversation for workspace: ${workspaceId} ` + | ||
| `(${result.reductionPercentage.toFixed(1)}% reduction)` | ||
| ); | ||
|
|
||
| return { | ||
| success: true, | ||
| originalTokens: result.originalTokens, | ||
| compactedTokens: result.compactedTokens, | ||
| reductionPercentage: result.reductionPercentage, | ||
| summary: result.summary, | ||
| }; | ||
| } catch (error) { | ||
| console.error(`[RPC] Compaction failed for workspace: ${workspaceId}`, error); | ||
| return { | ||
| success: false, | ||
| error: error instanceof Error ? error.message : 'Compaction failed', | ||
| }; | ||
| } | ||
| } |
There was a problem hiding this comment.
Guard against undefined properties when accessing compaction result.
The result object from compactionManager.manualCompact() may have undefined fields if the underlying engine encounters edge cases. Line 735 calls result.reductionPercentage.toFixed(1) which will throw if reductionPercentage is undefined.
Based on Context snippet 3 (compaction-manager.ts:202-230), manualCompact returns the raw result from engine.compact() without augmentation.
🛡️ Proposed defensive fix
console.log(
`[RPC] Compacted conversation for workspace: ${workspaceId} ` +
- `(${result.reductionPercentage.toFixed(1)}% reduction)`
+ `(${(result.reductionPercentage ?? 0).toFixed(1)}% reduction)`
);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-manager.ts`
around lines 710 - 752, In compactConversation, guard against undefined
compaction results from compactionManager.manualCompact by null-checking result
and its fields (e.g., result.reductionPercentage, result.originalTokens,
result.compactedTokens, result.summary) before using them; replace direct uses
like result.reductionPercentage.toFixed(1) with a safe path (check typeof
reductionPercentage === 'number' then toFixed, or use a default numeric value
via nullish coalescing) and ensure the returned CompactConversationResponse uses
sensible defaults when fields are missing so logging and response construction
in compactConversation and any calls to compactionManager.manualCompact cannot
throw on undefined properties.
| function convertToolMessageToText(toolMsg: any): any { | ||
| const content = Array.isArray(toolMsg.content) ? toolMsg.content : [toolMsg.content]; | ||
| const textDescriptions = content | ||
| .filter((item: any) => item != null) | ||
| .map((item: any) => { | ||
| if (item.type === 'tool-result') { | ||
| const resultStr = item.result != null ? JSON.stringify(item.result) : ''; | ||
| let truncated = resultStr; | ||
| if (resultStr.length > 1000) { | ||
| truncated = resultStr.substring(0, 400) + | ||
| '\n\n...[omitted]...\n\n' + | ||
| resultStr.substring(resultStr.length - 600); | ||
| } | ||
| return `[Tool: ${item.toolName || 'unknown'} returned: ${truncated}]`; | ||
| } | ||
| const itemStr = JSON.stringify(item) || ''; | ||
| return itemStr.substring(0, 200); | ||
| }) | ||
| .join('\n'); | ||
|
|
||
| return { | ||
| role: 'user', | ||
| content: textDescriptions, | ||
| }; | ||
| } |
There was a problem hiding this comment.
convertToolMessageToText can return empty content.
If toolMsg.content is an empty array or contains only null values, textDescriptions will be an empty string. This returns a message with content: '', which may cause issues downstream similar to the empty array case handled above.
🛡️ Proposed fix to return null for empty tool messages
function convertToolMessageToText(toolMsg: any): any {
const content = Array.isArray(toolMsg.content) ? toolMsg.content : [toolMsg.content];
const textDescriptions = content
.filter((item: any) => item != null)
.map((item: any) => {
if (item.type === 'tool-result') {
const resultStr = item.result != null ? JSON.stringify(item.result) : '';
let truncated = resultStr;
if (resultStr.length > 1000) {
truncated = resultStr.substring(0, 400) +
'\n\n...[omitted]...\n\n' +
resultStr.substring(resultStr.length - 600);
}
return `[Tool: ${item.toolName || 'unknown'} returned: ${truncated}]`;
}
const itemStr = JSON.stringify(item) || '';
return itemStr.substring(0, 200);
})
.join('\n');
+ if (!textDescriptions) {
+ return null;
+ }
+
return {
role: 'user',
content: textDescriptions,
};
}Then adjust the caller to filter nulls from convertToolMessageToText:
.map((msg: any) => {
// Convert tool messages (role: 'tool') to user messages with text
if (msg.role === 'tool') {
return convertToolMessageToText(msg);
}The existing .filter((msg: any) => msg !== null) at line 92 will handle this.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@workspaces/common-libs/copilot-utilities/src/compaction/utils/messagePreparation.ts`
around lines 99 - 123, convertToolMessageToText currently returns an object with
content:'' when toolMsg.content is empty or contains only nulls; change it to
return null in that case so callers can filter it out. Inside
convertToolMessageToText, compute content array and textDescriptions as now,
then if textDescriptions is empty (or all items filtered out) return null
instead of {role:'user', content: textDescriptions}; ensure callers already
using .filter((msg: any) => msg !== null) will drop these null returns.
workspaces/common-libs/copilot-utilities/src/compaction/core/ThresholdCalculator.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (1)
workspaces/common-libs/copilot-utilities/src/compaction/CompactionEngine.ts (1)
116-139:⚠️ Potential issue | 🔴 CriticalOnly the caller that acquired the compaction lock should release it.
If one request is already compacting, a second request takes the early-return path, then its
finallyon Line 176 clearsisCompactinganyway. That lets a third caller entercompactWithRetry()concurrently while the first compaction is still running.🔒 Proposed fix
async compact(messages: any[], options: CompactionOptions): Promise<CompactionResult> { + let acquiredLock = false; try { // Concurrency guard — prevent parallel compaction calls gracefully if (this.isCompacting) { console.warn('[CompactionEngine] Compaction already in progress. Bypassing request.'); const originalTokens = await this.tokenEstimator.estimateTokens(messages); @@ } this.isCompacting = true; + acquiredLock = true; // Ensure summarization callback is bound if (!this.summarizationService) { throw new Error( @@ } catch (error) { // Graceful degradation — return failure result instead of throwing console.error('[CompactionEngine] Compaction failed:', error); @@ } finally { - this.isCompacting = false; + if (acquiredLock) { + this.isCompacting = false; + } } }Also applies to: 175-176
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@workspaces/common-libs/copilot-utilities/src/compaction/CompactionEngine.ts` around lines 116 - 139, The early-return path in compactWithRetry allows a caller that did not acquire the compaction lock to later clear isCompacting in the finally block; fix by tracking lock ownership with a local flag (e.g., acquiredLock) so only the caller that sets this.isCompacting = true (in CompactionEngine.compactWithRetry / method containing isCompacting and tokenEstimator.estimateTokens) will clear it in the finally; ensure the early-return branch does not set acquiredLock and the finally only resets this.isCompacting when acquiredLock is true, preserving concurrency safety.
🧹 Nitpick comments (2)
workspaces/common-libs/copilot-utilities/src/compaction/config/defaults.ts (1)
24-35: Avoid comment-enforced sync formaxOutputTokens; make it a single source of truth.Lines 24-25 rely on manual synchronization with
AgentExecutor.ts. That can drift and silently skew compaction timing (too early or too late). Export a shared constant and have both modules consume it instead of duplicating the value.♻️ Proposed change in this file
import { ModelConfig } from '../types'; +export const DEFAULT_MAX_OUTPUT_TOKENS = 8_192; + /** * Default model configuration for Claude Sonnet 4. * - * maxOutputTokens MUST match the value configured in AgentExecutor.ts - * streamText call. If AgentExecutor changes maxOutputTokens, update this constant. + * `maxOutputTokens` must be consumed as a shared constant by AgentExecutor.ts + * to avoid config drift. * * Threshold calculation: - * effectiveWindow = maxContextWindow - maxOutputTokens = 200_000 - 8_192 = 191_808 + * effectiveWindow = maxContextWindow - maxOutputTokens = 200_000 - 8_192 = 191_808 * autoCompactThreshold = effectiveWindow - autoCompactBuffer = 191_808 - 13_000 = 178_808 */ export const DEFAULT_MODEL_CONFIG: ModelConfig = { maxContextWindow: 200_000, - maxOutputTokens: 8_192, // Matches AgentExecutor.ts streamText maxOutputTokens + maxOutputTokens: DEFAULT_MAX_OUTPUT_TOKENS, autoCompactBuffer: 13_000, };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@workspaces/common-libs/copilot-utilities/src/compaction/config/defaults.ts` around lines 24 - 35, DEFAULT_MODEL_CONFIG currently hardcodes maxOutputTokens (8_192) and relies on a comment to keep it in sync with AgentExecutor.ts; instead create and export a single shared constant (e.g., SHARED_MAX_OUTPUT_TOKENS) from a common place and import it into both this file and AgentExecutor.ts, then set DEFAULT_MODEL_CONFIG.maxOutputTokens = SHARED_MAX_OUTPUT_TOKENS and remove the comment-enforced sync to ensure one source of truth.workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/compaction/ContextUsageWidget/index.tsx (1)
193-199: Add keyboard-triggered tooltip behavior for accessibility.Tooltip visibility is mouse-only right now. Please make
WidgetContainerfocusable and wireonFocus/onBlurto the same show/hide handlers so keyboard users can access context details.Also applies to: 226-226
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/compaction/ContextUsageWidget/index.tsx` around lines 193 - 199, Make the WidgetContainer keyboard-accessible by adding tabIndex={0} and wiring its onFocus and onBlur to the same handlers used for mouse (scheduleShow and scheduleHide); update every WidgetContainer instance (including the second occurrence around the later block) so keyboard users can trigger the tooltip the same way as mouse users (keep existing onMouseEnter/onMouseLeave and just add onFocus/onBlur).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@workspaces/ballerina/ballerina-extension/src/features/ai/agent/compaction/CompactionGuard.ts`:
- Around line 208-281: The current flow only ensures
compactionResult.compactedMessages fits budget but then appends failureNotes,
the original-request reminder, acknowledgement, and recentMessages which can
push the final prompt over this.config.tokenThreshold; after building
compactedMessages (using extractFailureNotes and recentMessages) re-check the
combined token count against this.config.tokenThreshold and if it exceeds, call
this.config.engine.compact again (or a dedicated budget-aware method) with the
full replacement content (or iteratively trim/re-summarize recentMessages) until
the total tokens are within threshold, update compaction metadata
(compactionResult.metadata, reductionPercentage), persist via persistCallback,
and only then set lastCompactedMessages, sdkMessagesCountAtLastCompaction and
emit the compaction_end event.
- Around line 171-180: When no safe split point is found in CompactionGuard
(inside the method that uses findCleanSplitPoint and
preserveRecentMessageCount), treat it as a compaction failure rather than a
no-op: set this._lastCompactionFailed = true and call this.config.eventHandler
with a terminal failure event (e.g. { type: 'context_exhausted' } or `{ type:
'compaction_failed' }` consistent with existing event types) before returning so
the UI/state is notified that compaction failed.
- Around line 113-120: The current check uses message array length to detect
prior compaction which can fail when compaction preserves the same count; change
the branch in CompactionGuard (the if that checks lastInputTokens <
this.config.tokenThreshold) to return effectiveMessages whenever
this.lastCompactedMessages exists and effectiveMessages differ from messages by
content rather than length; implement a small helper (e.g., areMessagesEqual or
serializeMessages) to compare message arrays (compare role+content or
JSON.stringify each message) and replace the length check
"effectiveMessages.length < messages.length" with
"!areMessagesEqual(effectiveMessages, messages)". Ensure the new helper is used
by the branch that currently references lastCompactedMessages so compaction is
detected based on actual content difference.
In
`@workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/compaction/ContextUsageWidget/index.tsx`:
- Around line 165-167: The show/hide timers (showTimer, hideTimer) and
setVisible are causing stale callbacks and unmount-time state updates; before
scheduling a new setTimeout clear any existing timer of the same type
(clearTimeout(showTimer.current) / clearTimeout(hideTimer.current) and set the
ref to null) and, in a useEffect cleanup, clear both timers and avoid calling
setVisible after unmount (either by clearing timers in the cleanup or tracking
an isMounted ref checked before calling setVisible). Update the code paths that
schedule timers (the places around the existing showTimer/hideTimer usage and
the setVisible calls) to always clear the prior timer first and add a cleanup
effect that clears any remaining timers and nulls the refs to prevent delayed
callbacks after unmount.
In `@workspaces/common-libs/copilot-utilities/src/compaction/CompactionEngine.ts`:
- Line 118: The failure branches in CompactionEngine.compact call
this.tokenEstimator.estimateTokens(messages) again when building the failure
response, which can throw if tokenEstimator/tokenCountCallback is the failing
dependency; change compact() to compute originalTokens once (using the existing
const originalTokens = await this.tokenEstimator.estimateTokens(messages);) and
reuse that cached value when constructing any failure response instead of
calling estimateTokens again, or wrap additional estimateTokens calls in a
try/catch and fall back to the cached originalTokens; ensure the code paths that
return { success: false, originalTokens: ... } reference the originalTokens
variable (and not a fresh call) and handle the case where originalTokens is
undefined by providing a safe fallback (e.g., null or 0).
---
Duplicate comments:
In `@workspaces/common-libs/copilot-utilities/src/compaction/CompactionEngine.ts`:
- Around line 116-139: The early-return path in compactWithRetry allows a caller
that did not acquire the compaction lock to later clear isCompacting in the
finally block; fix by tracking lock ownership with a local flag (e.g.,
acquiredLock) so only the caller that sets this.isCompacting = true (in
CompactionEngine.compactWithRetry / method containing isCompacting and
tokenEstimator.estimateTokens) will clear it in the finally; ensure the
early-return branch does not set acquiredLock and the finally only resets
this.isCompacting when acquiredLock is true, preserving concurrency safety.
---
Nitpick comments:
In
`@workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/compaction/ContextUsageWidget/index.tsx`:
- Around line 193-199: Make the WidgetContainer keyboard-accessible by adding
tabIndex={0} and wiring its onFocus and onBlur to the same handlers used for
mouse (scheduleShow and scheduleHide); update every WidgetContainer instance
(including the second occurrence around the later block) so keyboard users can
trigger the tooltip the same way as mouse users (keep existing
onMouseEnter/onMouseLeave and just add onFocus/onBlur).
In `@workspaces/common-libs/copilot-utilities/src/compaction/config/defaults.ts`:
- Around line 24-35: DEFAULT_MODEL_CONFIG currently hardcodes maxOutputTokens
(8_192) and relies on a comment to keep it in sync with AgentExecutor.ts;
instead create and export a single shared constant (e.g.,
SHARED_MAX_OUTPUT_TOKENS) from a common place and import it into both this file
and AgentExecutor.ts, then set DEFAULT_MODEL_CONFIG.maxOutputTokens =
SHARED_MAX_OUTPUT_TOKENS and remove the comment-enforced sync to ensure one
source of truth.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 25d3b1d2-3590-4ed1-82b9-786ff7d5a098
📒 Files selected for processing (14)
workspaces/ballerina/ballerina-extension/src/features/ai/agent/compaction/CompactionGuard.tsworkspaces/ballerina/ballerina-extension/src/features/ai/agent/compaction/contextExhausted.tsworkspaces/ballerina/ballerina-extension/src/features/ai/compaction-manager.tsworkspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/compaction/ContextUsageWidget/index.tsxworkspaces/common-libs/copilot-utilities/src/compaction/CompactionEngine.tsworkspaces/common-libs/copilot-utilities/src/compaction/config/defaults.tsworkspaces/common-libs/copilot-utilities/src/compaction/core/SummarizationService.tsworkspaces/common-libs/copilot-utilities/src/compaction/core/ThresholdCalculator.tsworkspaces/common-libs/copilot-utilities/src/compaction/core/TokenEstimator.tsworkspaces/common-libs/copilot-utilities/src/compaction/index.tsworkspaces/common-libs/copilot-utilities/src/compaction/prompts/summarizationPrompt.tsworkspaces/common-libs/copilot-utilities/src/compaction/types/index.tsworkspaces/common-libs/copilot-utilities/src/compaction/utils/messagePreparation.tsworkspaces/common-libs/copilot-utilities/src/compaction/utils/messageUtils.ts
✅ Files skipped from review due to trivial changes (5)
- workspaces/ballerina/ballerina-extension/src/features/ai/agent/compaction/contextExhausted.ts
- workspaces/common-libs/copilot-utilities/src/compaction/core/ThresholdCalculator.ts
- workspaces/common-libs/copilot-utilities/src/compaction/types/index.ts
- workspaces/common-libs/copilot-utilities/src/compaction/prompts/summarizationPrompt.ts
- workspaces/common-libs/copilot-utilities/src/compaction/index.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- workspaces/common-libs/copilot-utilities/src/compaction/utils/messageUtils.ts
- workspaces/common-libs/copilot-utilities/src/compaction/utils/messagePreparation.ts
- workspaces/common-libs/copilot-utilities/src/compaction/core/SummarizationService.ts
- workspaces/ballerina/ballerina-extension/src/features/ai/compaction-manager.ts
workspaces/ballerina/ballerina-extension/src/features/ai/agent/compaction/CompactionGuard.ts
Show resolved
Hide resolved
workspaces/ballerina/ballerina-extension/src/features/ai/agent/compaction/CompactionGuard.ts
Show resolved
Hide resolved
workspaces/ballerina/ballerina-extension/src/features/ai/agent/compaction/CompactionGuard.ts
Outdated
Show resolved
Hide resolved
...erina-visualizer/src/views/AIPanel/components/AIChat/compaction/ContextUsageWidget/index.tsx
Show resolved
Hide resolved
workspaces/common-libs/copilot-utilities/src/compaction/CompactionEngine.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
workspaces/common-libs/copilot-utilities/src/compaction/CompactionEngine.ts (1)
208-235: Recursive retry re-estimates tokens on each call, adding latency.Each recursive call to
compactWithRetryre-computesoriginalTokensviathis.tokenEstimator.estimateTokens(messages)at line 214, even though the original messages never change during retries. Consider passing the pre-computed token count as a parameter to avoid redundant estimation calls on retries.Proposed optimization
private async compactWithRetry( messages: any[], options: CompactionOptions, - retryCount: number + retryCount: number, + precomputedOriginalTokens?: number ): Promise<CompactionResult> { const maxRetries = options.maxRetries ?? 3; - const originalTokens = await this.tokenEstimator.estimateTokens(messages); + const originalTokens = precomputedOriginalTokens ?? await this.tokenEstimator.estimateTokens(messages); const threshold = this.thresholdCalculator.getAutoCompactThreshold(); ... if (compactedTokens >= threshold && retryCount < maxRetries) { ... - return this.compactWithRetry(messages, options, retryCount + 1); + return this.compactWithRetry(messages, options, retryCount + 1, originalTokens); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@workspaces/common-libs/copilot-utilities/src/compaction/CompactionEngine.ts` around lines 208 - 235, The method compactWithRetry recomputes originalTokens on every retry by calling this.tokenEstimator.estimateTokens(messages); modify compactWithRetry to accept an optional originalTokens parameter (e.g., compactWithRetry(messages, options, retryCount, originalTokens?)) and only call this.tokenEstimator.estimateTokens(messages) when originalTokens is undefined, then pass the computed originalTokens into recursive retry calls so retries reuse the precomputed token count; update any callers of compactWithRetry accordingly to supply the original token count on subsequent retries and keep the rest of the logic (threshold, summarization, continuationMessages) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@workspaces/ballerina/ballerina-extension/src/features/ai/agent/compaction/CompactionGuard.ts`:
- Around line 52-60: The areMessagesEqual function currently uses strict
equality on ModelMessage.content which fails when content is an array; update
areMessagesEqual to normalize and deep-compare content values: if both contents
are strings compare directly, if both are arrays compare lengths and then
compare each element (recursively or via a stable serialization) for equality,
and treat mismatched types as unequal; implement the deep-compare logic as a
small helper (e.g., deepEqualContent) and call it from areMessagesEqual so role
and content comparisons handle multi-part message content correctly.
- Around line 269-285: The re-compaction loop in CompactionGuard.ts currently
replaces compactedMessages with only compactionResult.compactedMessages (losing
failureNotes, taskReminder, and recentMessages) and can loop forever; update the
loop in the while that uses tokenStatus/currentTokens to (1) introduce a max
iteration guard (e.g., maxAttempts or maxRecompactIterations) and break/throw
after exceeded, and (2) when rebuilding compactedMessages after a successful
compact, merge/append the preserved segments (failureNotes, taskReminder,
preserved recentMessages) back into the resulting array instead of replacing it
so those entries are not discarded; use the existing symbols compactedMessages,
compactionResult, failureNotes, taskReminder, recentMessages, tokenStatus, and
this.config.engine.compact to locate and implement the fix.
- Around line 319-326: The JSDoc describing extractFailureNotes is currently
placed after the findCleanSplitPoint signature and is orphaned; move that JSDoc
block (the comment starting "Find a clean split point..." / the subsequent
comment block meant for extractFailureNotes) so it sits immediately above the
extractFailureNotes method declaration (named extractFailureNotes) instead of
after findCleanSplitPoint, ensuring findCleanSplitPoint retains only its own
JSDoc and extractFailureNotes has its intended documentation.
In `@workspaces/common-libs/copilot-utilities/src/compaction/CompactionEngine.ts`:
- Around line 57-63: The JSDoc above setSummarizationCallback has a typo ("ind"
→ "Bind"); update the comment for the setSummarizationCallback method in
CompactionEngine.ts to read "Bind or replace the summarization callback." and
keep the rest of the doc intact (it should still mention that it must be called
before compact() with the caller's authenticated model instance); this
references the setSummarizationCallback method and the SummarizationService
usage so you can locate the correct comment to fix.
---
Nitpick comments:
In `@workspaces/common-libs/copilot-utilities/src/compaction/CompactionEngine.ts`:
- Around line 208-235: The method compactWithRetry recomputes originalTokens on
every retry by calling this.tokenEstimator.estimateTokens(messages); modify
compactWithRetry to accept an optional originalTokens parameter (e.g.,
compactWithRetry(messages, options, retryCount, originalTokens?)) and only call
this.tokenEstimator.estimateTokens(messages) when originalTokens is undefined,
then pass the computed originalTokens into recursive retry calls so retries
reuse the precomputed token count; update any callers of compactWithRetry
accordingly to supply the original token count on subsequent retries and keep
the rest of the logic (threshold, summarization, continuationMessages)
unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 739ef760-7509-49b5-b98c-c18770b629a4
📒 Files selected for processing (2)
workspaces/ballerina/ballerina-extension/src/features/ai/agent/compaction/CompactionGuard.tsworkspaces/common-libs/copilot-utilities/src/compaction/CompactionEngine.ts
| function areMessagesEqual(a: ModelMessage[], b: ModelMessage[]): boolean { | ||
| if (a.length !== b.length) return false; | ||
| for (let i = 0; i < a.length; i++) { | ||
| if (a[i].role !== b[i].role || a[i].content !== b[i].content) { | ||
| return false; | ||
| } | ||
| } | ||
| return true; | ||
| } |
There was a problem hiding this comment.
Content comparison may fail for multi-part message content.
The areMessagesEqual function compares content using strict equality, but message content can be either a string or an array of content parts (tool-calls, tool-results, etc.). Comparing arrays with !== will always return true even for identical arrays.
Proposed fix
function areMessagesEqual(a: ModelMessage[], b: ModelMessage[]): boolean {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
- if (a[i].role !== b[i].role || a[i].content !== b[i].content) {
+ if (a[i].role !== b[i].role) {
+ return false;
+ }
+ const contentA = a[i].content;
+ const contentB = b[i].content;
+ if (typeof contentA !== typeof contentB) {
+ return false;
+ }
+ if (typeof contentA === 'string') {
+ if (contentA !== contentB) return false;
+ } else {
+ // For array content, compare serialized form
+ if (JSON.stringify(contentA) !== JSON.stringify(contentB)) return false;
+ }
- return false;
- }
}
return true;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function areMessagesEqual(a: ModelMessage[], b: ModelMessage[]): boolean { | |
| if (a.length !== b.length) return false; | |
| for (let i = 0; i < a.length; i++) { | |
| if (a[i].role !== b[i].role || a[i].content !== b[i].content) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| function areMessagesEqual(a: ModelMessage[], b: ModelMessage[]): boolean { | |
| if (a.length !== b.length) return false; | |
| for (let i = 0; i < a.length; i++) { | |
| if (a[i].role !== b[i].role) { | |
| return false; | |
| } | |
| const contentA = a[i].content; | |
| const contentB = b[i].content; | |
| if (typeof contentA !== typeof contentB) { | |
| return false; | |
| } | |
| if (typeof contentA === 'string') { | |
| if (contentA !== contentB) return false; | |
| } else { | |
| // For array content, compare serialized form | |
| if (JSON.stringify(contentA) !== JSON.stringify(contentB)) return false; | |
| } | |
| } | |
| return true; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@workspaces/ballerina/ballerina-extension/src/features/ai/agent/compaction/CompactionGuard.ts`
around lines 52 - 60, The areMessagesEqual function currently uses strict
equality on ModelMessage.content which fails when content is an array; update
areMessagesEqual to normalize and deep-compare content values: if both contents
are strings compare directly, if both are arrays compare lengths and then
compare each element (recursively or via a stable serialization) for equality,
and treat mismatched types as unequal; implement the deep-compare logic as a
small helper (e.g., deepEqualContent) and call it from areMessagesEqual so role
and content comparisons handle multi-part message content correctly.
Purpose
Resolves: wso2/product-integrator#188
Goals
Approach
Automatic Context Compaction
Two complementary compaction triggers are implemented:
Pre-turn compaction (
CompactionManager) fires at the start of each user turn if accumulated history exceeds 178,808 tokens (200K − 8,192 output buffer − 13K system overhead). Replaces all previous generations inchatStateStoragewith a single compacted summary generation, so the next turn starts clean.Mid-stream compaction (
CompactionGuardvia Vercel AI SDKprepareStep) fires between agent steps within a running turn ifinputTokensfrom the last step exceed 160K (80% of 200K). Compacts in-memory and also persists tochatStateStorageto prevent a redundant pre-turn compaction on the next user message.Both paths use a shared
CompactionEngine(in@wso2/copilot-utilities) which calls the Anthropic API to produce a structured summary, preserving:Pre-compaction history is backed up to
.ballerina/copilot/compaction-backups/and.ballerina/is added to.gitignoreautomatically.Context Usage Widget
A circular progress ring in the AI chat input toolbar shows real-time context usage. On hover, a tooltip displays:
97.3K / 200K tokens • 49%)inputTokenstotalThe widget is controlled by the
ballerina.ai.showContextUsageVS Code setting (default:false).Checkpoint Restore Integration
UI:

UI Component Development
Release note
Documentation
Training
Certification
Marketing
Test environment
Summary by CodeRabbit
New Features
Improvements