feat(context): detect compaction events from SDK compact_boundary messages
This commit is contained in:
parent
d898586181
commit
f4ec2f3762
2 changed files with 88 additions and 1 deletions
|
|
@ -8,6 +8,7 @@ export type AgentMessageType =
|
||||||
| 'tool_call'
|
| 'tool_call'
|
||||||
| 'tool_result'
|
| 'tool_result'
|
||||||
| 'status'
|
| 'status'
|
||||||
|
| 'compaction'
|
||||||
| 'cost'
|
| 'cost'
|
||||||
| 'error'
|
| 'error'
|
||||||
| 'unknown';
|
| 'unknown';
|
||||||
|
|
@ -62,6 +63,11 @@ export interface CostContent {
|
||||||
errors?: string[];
|
errors?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CompactionContent {
|
||||||
|
trigger: 'manual' | 'auto';
|
||||||
|
preTokens: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ErrorContent {
|
export interface ErrorContent {
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
@ -125,6 +131,21 @@ function adaptSystemMessage(
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (subtype === 'compact_boundary') {
|
||||||
|
const meta = typeof raw.compact_metadata === 'object' && raw.compact_metadata !== null
|
||||||
|
? raw.compact_metadata as Record<string, unknown>
|
||||||
|
: {};
|
||||||
|
return [{
|
||||||
|
id: uuid,
|
||||||
|
type: 'compaction',
|
||||||
|
content: {
|
||||||
|
trigger: str(meta.trigger, 'auto') as 'manual' | 'auto',
|
||||||
|
preTokens: num(meta.pre_tokens),
|
||||||
|
} satisfies CompactionContent,
|
||||||
|
timestamp,
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
return [{
|
return [{
|
||||||
id: uuid,
|
id: uuid,
|
||||||
type: 'status',
|
type: 'status',
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getAgentSession, getTotalCost, type AgentSession } from '../../stores/agents.svelte';
|
import { getAgentSession, getTotalCost, type AgentSession } from '../../stores/agents.svelte';
|
||||||
import type { AgentMessage, ToolCallContent, CostContent } from '../../adapters/sdk-messages';
|
import type { AgentMessage, ToolCallContent, CostContent, CompactionContent } from '../../adapters/sdk-messages';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
sessionId: string | null;
|
sessionId: string | null;
|
||||||
|
|
@ -16,6 +16,33 @@
|
||||||
// Context window size (Claude 3.5 Sonnet = 200k, Opus = 200k)
|
// Context window size (Claude 3.5 Sonnet = 200k, Opus = 200k)
|
||||||
const CONTEXT_WINDOW = 200_000;
|
const CONTEXT_WINDOW = 200_000;
|
||||||
|
|
||||||
|
// --- Compaction tracking ---
|
||||||
|
interface CompactionEvent {
|
||||||
|
timestamp: number;
|
||||||
|
trigger: 'manual' | 'auto';
|
||||||
|
preTokens: number;
|
||||||
|
messageIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
let compactions = $derived.by((): CompactionEvent[] => {
|
||||||
|
const events: CompactionEvent[] = [];
|
||||||
|
for (let i = 0; i < messages.length; i++) {
|
||||||
|
const msg = messages[i];
|
||||||
|
if (msg.type === 'compaction') {
|
||||||
|
const c = msg.content as CompactionContent;
|
||||||
|
events.push({
|
||||||
|
timestamp: msg.timestamp,
|
||||||
|
trigger: c.trigger,
|
||||||
|
preTokens: c.preTokens,
|
||||||
|
messageIndex: i,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return events;
|
||||||
|
});
|
||||||
|
|
||||||
|
let lastCompaction = $derived(compactions.length > 0 ? compactions[compactions.length - 1] : null);
|
||||||
|
|
||||||
// --- Token category breakdown ---
|
// --- Token category breakdown ---
|
||||||
interface TokenCategory {
|
interface TokenCategory {
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -294,6 +321,33 @@
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
if (msg.type === 'init' || msg.type === 'status') continue;
|
if (msg.type === 'init' || msg.type === 'status') continue;
|
||||||
|
|
||||||
|
if (msg.type === 'compaction') {
|
||||||
|
// Insert compaction boundary marker
|
||||||
|
if (currentTurn) {
|
||||||
|
turnNodes.push(currentTurn);
|
||||||
|
currentTurn = null;
|
||||||
|
currentToolCall = null;
|
||||||
|
}
|
||||||
|
const c = msg.content as CompactionContent;
|
||||||
|
turnNodes.push({
|
||||||
|
id: `compact-${msg.id}`,
|
||||||
|
label: `Compacted (${c.trigger})`,
|
||||||
|
type: 'turn',
|
||||||
|
color: 'var(--ctp-red)',
|
||||||
|
tokens: c.preTokens,
|
||||||
|
children: [{
|
||||||
|
id: `compact-detail-${msg.id}`,
|
||||||
|
label: `${formatTokens(c.preTokens)} tokens removed`,
|
||||||
|
type: 'error',
|
||||||
|
color: 'var(--ctp-red)',
|
||||||
|
tokens: 0,
|
||||||
|
children: [],
|
||||||
|
detail: `Context was ${c.trigger === 'auto' ? 'automatically' : 'manually'} compacted. ${formatTokens(c.preTokens)} tokens of earlier conversation were summarized.`,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (msg.type === 'cost') {
|
if (msg.type === 'cost') {
|
||||||
// End of turn
|
// End of turn
|
||||||
if (currentTurn) {
|
if (currentTurn) {
|
||||||
|
|
@ -616,6 +670,12 @@
|
||||||
<div class="stat status-pill" class:running={session.status === 'running'} class:done={session.status === 'done'} class:error={session.status === 'error'}>
|
<div class="stat status-pill" class:running={session.status === 'running'} class:done={session.status === 'done'} class:error={session.status === 'error'}>
|
||||||
<span class="stat-value">{session.status}</span>
|
<span class="stat-value">{session.status}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{#if compactions.length > 0}
|
||||||
|
<div class="stat compaction-pill" title="Context compacted {compactions.length} time{compactions.length > 1 ? 's' : ''}. Last: {lastCompaction?.trigger} ({formatTokens(lastCompaction?.preTokens ?? 0)} tokens summarized)">
|
||||||
|
<span class="stat-value">{compactions.length}×</span>
|
||||||
|
<span class="stat-label">compacted</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Context Meter -->
|
<!-- Context Meter -->
|
||||||
|
|
@ -987,6 +1047,12 @@
|
||||||
.status-pill.error { background: color-mix(in srgb, var(--ctp-red) 15%, transparent); }
|
.status-pill.error { background: color-mix(in srgb, var(--ctp-red) 15%, transparent); }
|
||||||
.status-pill.error .stat-value { color: var(--ctp-red); }
|
.status-pill.error .stat-value { color: var(--ctp-red); }
|
||||||
|
|
||||||
|
.compaction-pill {
|
||||||
|
background: color-mix(in srgb, var(--ctp-yellow) 15%, transparent);
|
||||||
|
}
|
||||||
|
.compaction-pill .stat-value { color: var(--ctp-yellow); }
|
||||||
|
.compaction-pill .stat-label { color: var(--ctp-yellow); opacity: 0.7; }
|
||||||
|
|
||||||
/* Context meter */
|
/* Context meter */
|
||||||
.meter-section {
|
.meter-section {
|
||||||
padding: 0.5rem 0.625rem;
|
padding: 0.5rem 0.625rem;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue