mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-17 10:02:23 +00:00
Add auto-compaction events to AgentSession
- Add AgentSessionEvent type extending AgentEvent with auto_compaction_start/end - Emit events when auto-compaction starts and completes - TUI shows loader during auto-compaction with escape to cancel - Rebuilds chat UI when auto-compaction succeeds
This commit is contained in:
parent
803d4b65ee
commit
91b89578c1
3 changed files with 109 additions and 26 deletions
|
|
@ -25,8 +25,14 @@ import { loadSessionFromEntries, type SessionManager } from "./session-manager.j
|
||||||
import type { SettingsManager } from "./settings-manager.js";
|
import type { SettingsManager } from "./settings-manager.js";
|
||||||
import { expandSlashCommand, type FileSlashCommand } from "./slash-commands.js";
|
import { expandSlashCommand, type FileSlashCommand } from "./slash-commands.js";
|
||||||
|
|
||||||
/** Listener function for agent events */
|
/** Session-specific events that extend the core AgentEvent */
|
||||||
export type AgentEventListener = (event: AgentEvent) => void;
|
export type AgentSessionEvent =
|
||||||
|
| AgentEvent
|
||||||
|
| { type: "auto_compaction_start" }
|
||||||
|
| { type: "auto_compaction_end"; result: CompactionResult | null; aborted: boolean };
|
||||||
|
|
||||||
|
/** Listener function for agent session events */
|
||||||
|
export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Types
|
// Types
|
||||||
|
|
@ -97,13 +103,14 @@ export class AgentSession {
|
||||||
|
|
||||||
// Event subscription state
|
// Event subscription state
|
||||||
private _unsubscribeAgent?: () => void;
|
private _unsubscribeAgent?: () => void;
|
||||||
private _eventListeners: AgentEventListener[] = [];
|
private _eventListeners: AgentSessionEventListener[] = [];
|
||||||
|
|
||||||
// Message queue state
|
// Message queue state
|
||||||
private _queuedMessages: string[] = [];
|
private _queuedMessages: string[] = [];
|
||||||
|
|
||||||
// Compaction state
|
// Compaction state
|
||||||
private _compactionAbortController: AbortController | null = null;
|
private _compactionAbortController: AbortController | null = null;
|
||||||
|
private _autoCompactionAbortController: AbortController | null = null;
|
||||||
|
|
||||||
// Bash execution state
|
// Bash execution state
|
||||||
private _bashAbortController: AbortController | null = null;
|
private _bashAbortController: AbortController | null = null;
|
||||||
|
|
@ -121,12 +128,17 @@ export class AgentSession {
|
||||||
// Event Subscription
|
// Event Subscription
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
/** Internal handler for agent events - shared by subscribe and reconnect */
|
/** Emit an event to all listeners */
|
||||||
private _handleAgentEvent = async (event: AgentEvent): Promise<void> => {
|
private _emit(event: AgentSessionEvent): void {
|
||||||
// Notify all listeners
|
|
||||||
for (const l of this._eventListeners) {
|
for (const l of this._eventListeners) {
|
||||||
l(event);
|
l(event);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Internal handler for agent events - shared by subscribe and reconnect */
|
||||||
|
private _handleAgentEvent = async (event: AgentEvent): Promise<void> => {
|
||||||
|
// Notify all listeners
|
||||||
|
this._emit(event);
|
||||||
|
|
||||||
// Handle session persistence
|
// Handle session persistence
|
||||||
if (event.type === "message_end") {
|
if (event.type === "message_end") {
|
||||||
|
|
@ -139,7 +151,7 @@ export class AgentSession {
|
||||||
|
|
||||||
// Check auto-compaction after assistant messages
|
// Check auto-compaction after assistant messages
|
||||||
if (event.message.role === "assistant") {
|
if (event.message.role === "assistant") {
|
||||||
await this.checkAutoCompaction();
|
await this._runAutoCompaction();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -149,7 +161,7 @@ export class AgentSession {
|
||||||
* Session persistence is handled internally (saves messages on message_end).
|
* Session persistence is handled internally (saves messages on message_end).
|
||||||
* Multiple listeners can be added. Returns unsubscribe function for this listener.
|
* Multiple listeners can be added. Returns unsubscribe function for this listener.
|
||||||
*/
|
*/
|
||||||
subscribe(listener: AgentEventListener): () => void {
|
subscribe(listener: AgentSessionEventListener): () => void {
|
||||||
this._eventListeners.push(listener);
|
this._eventListeners.push(listener);
|
||||||
|
|
||||||
// Set up agent subscription if not already done
|
// Set up agent subscription if not already done
|
||||||
|
|
@ -561,20 +573,20 @@ export class AgentSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancel in-progress compaction.
|
* Cancel in-progress compaction (manual or auto).
|
||||||
*/
|
*/
|
||||||
abortCompaction(): void {
|
abortCompaction(): void {
|
||||||
this._compactionAbortController?.abort();
|
this._compactionAbortController?.abort();
|
||||||
|
this._autoCompactionAbortController?.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if auto-compaction should run, and run it if so.
|
* Internal: Run auto-compaction with events.
|
||||||
* Called internally after assistant messages.
|
* Called after assistant messages complete.
|
||||||
* @returns Result if compaction occurred, null otherwise
|
|
||||||
*/
|
*/
|
||||||
async checkAutoCompaction(): Promise<CompactionResult | null> {
|
private async _runAutoCompaction(): Promise<void> {
|
||||||
const settings = this.settingsManager.getCompactionSettings();
|
const settings = this.settingsManager.getCompactionSettings();
|
||||||
if (!settings.enabled) return null;
|
if (!settings.enabled) return;
|
||||||
|
|
||||||
// Get last non-aborted assistant message
|
// Get last non-aborted assistant message
|
||||||
const messages = this.messages;
|
const messages = this.messages;
|
||||||
|
|
@ -589,33 +601,57 @@ export class AgentSession {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!lastAssistant) return null;
|
if (!lastAssistant) return;
|
||||||
|
|
||||||
const contextTokens = calculateContextTokens(lastAssistant.usage);
|
const contextTokens = calculateContextTokens(lastAssistant.usage);
|
||||||
const contextWindow = this.model?.contextWindow ?? 0;
|
const contextWindow = this.model?.contextWindow ?? 0;
|
||||||
|
|
||||||
if (!shouldCompact(contextTokens, contextWindow, settings)) return null;
|
if (!shouldCompact(contextTokens, contextWindow, settings)) return;
|
||||||
|
|
||||||
|
// Emit start event
|
||||||
|
this._emit({ type: "auto_compaction_start" });
|
||||||
|
this._autoCompactionAbortController = new AbortController();
|
||||||
|
|
||||||
// Perform auto-compaction (don't abort current operation for auto)
|
|
||||||
try {
|
try {
|
||||||
if (!this.model) return null;
|
if (!this.model) {
|
||||||
|
this._emit({ type: "auto_compaction_end", result: null, aborted: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const apiKey = await getApiKeyForModel(this.model);
|
const apiKey = await getApiKeyForModel(this.model);
|
||||||
if (!apiKey) return null;
|
if (!apiKey) {
|
||||||
|
this._emit({ type: "auto_compaction_end", result: null, aborted: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const entries = this.sessionManager.loadEntries();
|
const entries = this.sessionManager.loadEntries();
|
||||||
const compactionEntry = await compact(entries, this.model, settings, apiKey);
|
const compactionEntry = await compact(
|
||||||
|
entries,
|
||||||
|
this.model,
|
||||||
|
settings,
|
||||||
|
apiKey,
|
||||||
|
this._autoCompactionAbortController.signal,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this._autoCompactionAbortController.signal.aborted) {
|
||||||
|
this._emit({ type: "auto_compaction_end", result: null, aborted: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.sessionManager.saveCompaction(compactionEntry);
|
this.sessionManager.saveCompaction(compactionEntry);
|
||||||
const loaded = loadSessionFromEntries(this.sessionManager.loadEntries());
|
const loaded = loadSessionFromEntries(this.sessionManager.loadEntries());
|
||||||
this.agent.replaceMessages(loaded.messages);
|
this.agent.replaceMessages(loaded.messages);
|
||||||
|
|
||||||
return {
|
const result: CompactionResult = {
|
||||||
tokensBefore: compactionEntry.tokensBefore,
|
tokensBefore: compactionEntry.tokensBefore,
|
||||||
summary: compactionEntry.summary,
|
summary: compactionEntry.summary,
|
||||||
};
|
};
|
||||||
|
this._emit({ type: "auto_compaction_end", result, aborted: false });
|
||||||
} catch {
|
} catch {
|
||||||
return null; // Silently fail auto-compaction
|
// Silently fail auto-compaction but emit end event
|
||||||
|
this._emit({ type: "auto_compaction_end", result: null, aborted: false });
|
||||||
|
} finally {
|
||||||
|
this._autoCompactionAbortController = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export {
|
export {
|
||||||
type AgentEventListener,
|
|
||||||
AgentSession,
|
AgentSession,
|
||||||
type AgentSessionConfig,
|
type AgentSessionConfig,
|
||||||
|
type AgentSessionEvent,
|
||||||
|
type AgentSessionEventListener,
|
||||||
type CompactionResult,
|
type CompactionResult,
|
||||||
type ModelCycleResult,
|
type ModelCycleResult,
|
||||||
type PromptOptions,
|
type PromptOptions,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import * as fs from "node:fs";
|
import * as fs from "node:fs";
|
||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import type { AgentEvent, AgentState, AppMessage } from "@mariozechner/pi-agent-core";
|
import type { AgentState, AppMessage } from "@mariozechner/pi-agent-core";
|
||||||
import type { AssistantMessage, Message } from "@mariozechner/pi-ai";
|
import type { AssistantMessage, Message } from "@mariozechner/pi-ai";
|
||||||
import type { SlashCommand } from "@mariozechner/pi-tui";
|
import type { SlashCommand } from "@mariozechner/pi-tui";
|
||||||
import {
|
import {
|
||||||
|
|
@ -24,7 +24,7 @@ import {
|
||||||
} from "@mariozechner/pi-tui";
|
} from "@mariozechner/pi-tui";
|
||||||
import { exec } from "child_process";
|
import { exec } from "child_process";
|
||||||
import { APP_NAME, getDebugLogPath, getOAuthPath } from "../../config.js";
|
import { APP_NAME, getDebugLogPath, getOAuthPath } from "../../config.js";
|
||||||
import type { AgentSession } from "../../core/agent-session.js";
|
import type { AgentSession, AgentSessionEvent } from "../../core/agent-session.js";
|
||||||
import { isBashExecutionMessage } from "../../core/messages.js";
|
import { isBashExecutionMessage } from "../../core/messages.js";
|
||||||
import { invalidateOAuthCache } from "../../core/model-config.js";
|
import { invalidateOAuthCache } from "../../core/model-config.js";
|
||||||
import { listOAuthProviders, login, logout, type SupportedOAuthProvider } from "../../core/oauth/index.js";
|
import { listOAuthProviders, login, logout, type SupportedOAuthProvider } from "../../core/oauth/index.js";
|
||||||
|
|
@ -94,6 +94,10 @@ export class InteractiveMode {
|
||||||
// Track pending bash components (shown in pending area, moved to chat on submit)
|
// Track pending bash components (shown in pending area, moved to chat on submit)
|
||||||
private pendingBashComponents: BashExecutionComponent[] = [];
|
private pendingBashComponents: BashExecutionComponent[] = [];
|
||||||
|
|
||||||
|
// Auto-compaction state
|
||||||
|
private autoCompactionLoader: Loader | null = null;
|
||||||
|
private autoCompactionEscapeHandler?: () => void;
|
||||||
|
|
||||||
// Convenience accessors
|
// Convenience accessors
|
||||||
private get agent() {
|
private get agent() {
|
||||||
return this.session.agent;
|
return this.session.agent;
|
||||||
|
|
@ -430,7 +434,7 @@ export class InteractiveMode {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleEvent(event: AgentEvent, state: AgentState): Promise<void> {
|
private async handleEvent(event: AgentSessionEvent, state: AgentState): Promise<void> {
|
||||||
if (!this.isInitialized) {
|
if (!this.isInitialized) {
|
||||||
await this.init();
|
await this.init();
|
||||||
}
|
}
|
||||||
|
|
@ -555,6 +559,48 @@ export class InteractiveMode {
|
||||||
this.pendingTools.clear();
|
this.pendingTools.clear();
|
||||||
this.ui.requestRender();
|
this.ui.requestRender();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "auto_compaction_start":
|
||||||
|
// Set up escape to abort auto-compaction
|
||||||
|
this.autoCompactionEscapeHandler = this.editor.onEscape;
|
||||||
|
this.editor.onEscape = () => {
|
||||||
|
this.session.abortCompaction();
|
||||||
|
};
|
||||||
|
// Show compacting indicator
|
||||||
|
this.statusContainer.clear();
|
||||||
|
this.autoCompactionLoader = new Loader(
|
||||||
|
this.ui,
|
||||||
|
(spinner) => theme.fg("accent", spinner),
|
||||||
|
(text) => theme.fg("muted", text),
|
||||||
|
"Auto-compacting... (esc to cancel)",
|
||||||
|
);
|
||||||
|
this.statusContainer.addChild(this.autoCompactionLoader);
|
||||||
|
this.ui.requestRender();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "auto_compaction_end":
|
||||||
|
// Restore escape handler
|
||||||
|
if (this.autoCompactionEscapeHandler) {
|
||||||
|
this.editor.onEscape = this.autoCompactionEscapeHandler;
|
||||||
|
this.autoCompactionEscapeHandler = undefined;
|
||||||
|
}
|
||||||
|
// Stop loader
|
||||||
|
if (this.autoCompactionLoader) {
|
||||||
|
this.autoCompactionLoader.stop();
|
||||||
|
this.autoCompactionLoader = null;
|
||||||
|
this.statusContainer.clear();
|
||||||
|
}
|
||||||
|
// Handle result
|
||||||
|
if (event.aborted) {
|
||||||
|
this.showStatus("Auto-compaction cancelled");
|
||||||
|
} else if (event.result) {
|
||||||
|
// Rebuild chat to show compacted state
|
||||||
|
this.chatContainer.clear();
|
||||||
|
this.rebuildChatFromMessages();
|
||||||
|
this.showStatus(`Auto-compacted: ${event.result.tokensBefore.toLocaleString()} tokens`);
|
||||||
|
}
|
||||||
|
this.ui.requestRender();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue