mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-17 06:04:51 +00:00
WP9+WP10: Add AgentSession session management and utility methods
This commit is contained in:
parent
94ff0b0962
commit
934c2bc5d3
3 changed files with 213 additions and 11 deletions
|
|
@ -1075,13 +1075,13 @@ exportToHtml(outputPath?: string): string {
|
||||||
**Verification:**
|
**Verification:**
|
||||||
1. `npm run check` passes
|
1. `npm run check` passes
|
||||||
|
|
||||||
- [ ] Add `SessionStats` interface
|
- [x] Add `SessionStats` interface
|
||||||
- [ ] Add `switchSession()` method
|
- [x] Add `switchSession()` method
|
||||||
- [ ] Add `branch()` method
|
- [x] Add `branch()` method
|
||||||
- [ ] Add `getUserMessagesForBranching()` method
|
- [x] Add `getUserMessagesForBranching()` method
|
||||||
- [ ] Add `getSessionStats()` method
|
- [x] Add `getSessionStats()` method
|
||||||
- [ ] Add `exportToHtml()` method
|
- [x] Add `exportToHtml()` method
|
||||||
- [ ] Verify with `npm run check`
|
- [x] Verify with `npm run check`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -1138,10 +1138,10 @@ getQueuedMessages(): readonly string[] {
|
||||||
**Verification:**
|
**Verification:**
|
||||||
1. `npm run check` passes
|
1. `npm run check` passes
|
||||||
|
|
||||||
- [ ] Add `getLastAssistantText()` method
|
- [x] Add `getLastAssistantText()` method
|
||||||
- [ ] Add `queuedMessageCount` getter
|
- [x] Add `queuedMessageCount` getter (done in WP4)
|
||||||
- [ ] Add `getQueuedMessages()` method
|
- [x] Add `getQueuedMessages()` method (done in WP4)
|
||||||
- [ ] Verify with `npm run check`
|
- [x] Verify with `npm run check`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import type { Agent, AgentEvent, AgentState, AppMessage, Attachment, ThinkingLev
|
||||||
import type { AssistantMessage, Model } from "@mariozechner/pi-ai";
|
import type { AssistantMessage, Model } from "@mariozechner/pi-ai";
|
||||||
import { calculateContextTokens, compact, shouldCompact } from "../compaction.js";
|
import { calculateContextTokens, compact, shouldCompact } from "../compaction.js";
|
||||||
import { getModelsPath } from "../config.js";
|
import { getModelsPath } from "../config.js";
|
||||||
|
import { exportSessionToHtml } from "../export-html.js";
|
||||||
import type { BashExecutionMessage } from "../messages.js";
|
import type { BashExecutionMessage } from "../messages.js";
|
||||||
import { getApiKeyForModel, getAvailableModels } from "../model-config.js";
|
import { getApiKeyForModel, getAvailableModels } from "../model-config.js";
|
||||||
import { loadSessionFromEntries, type SessionManager } from "../session-manager.js";
|
import { loadSessionFromEntries, type SessionManager } from "../session-manager.js";
|
||||||
|
|
@ -63,6 +64,25 @@ export interface CompactionResult {
|
||||||
summary: string;
|
summary: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Session statistics for /session command */
|
||||||
|
export interface SessionStats {
|
||||||
|
sessionFile: string;
|
||||||
|
sessionId: string;
|
||||||
|
userMessages: number;
|
||||||
|
assistantMessages: number;
|
||||||
|
toolCalls: number;
|
||||||
|
toolResults: number;
|
||||||
|
totalMessages: number;
|
||||||
|
tokens: {
|
||||||
|
input: number;
|
||||||
|
output: number;
|
||||||
|
cacheRead: number;
|
||||||
|
cacheWrite: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
cost: number;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// AgentSession Class
|
// AgentSession Class
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -681,4 +701,185 @@ export class AgentSession {
|
||||||
get isBashRunning(): boolean {
|
get isBashRunning(): boolean {
|
||||||
return this._bashAbortController !== null;
|
return this._bashAbortController !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Session Management
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch to a different session file.
|
||||||
|
* Aborts current operation, loads messages, restores model/thinking.
|
||||||
|
* Listeners are preserved and will continue receiving events.
|
||||||
|
*/
|
||||||
|
async switchSession(sessionPath: string): Promise<void> {
|
||||||
|
this._disconnectFromAgent();
|
||||||
|
await this.abort();
|
||||||
|
this._queuedMessages = [];
|
||||||
|
|
||||||
|
// Set new session
|
||||||
|
this.sessionManager.setSessionFile(sessionPath);
|
||||||
|
|
||||||
|
// Reload messages
|
||||||
|
const loaded = loadSessionFromEntries(this.sessionManager.loadEntries());
|
||||||
|
this.agent.replaceMessages(loaded.messages);
|
||||||
|
|
||||||
|
// Restore model if saved
|
||||||
|
const savedModel = this.sessionManager.loadModel();
|
||||||
|
if (savedModel) {
|
||||||
|
const availableModels = (await getAvailableModels()).models;
|
||||||
|
const match = availableModels.find((m) => m.provider === savedModel.provider && m.id === savedModel.modelId);
|
||||||
|
if (match) {
|
||||||
|
this.agent.setModel(match);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore thinking level if saved
|
||||||
|
const savedThinking = this.sessionManager.loadThinkingLevel();
|
||||||
|
if (savedThinking) {
|
||||||
|
this.agent.setThinkingLevel(savedThinking as ThinkingLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._reconnectToAgent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a branch from a specific entry index.
|
||||||
|
* @param entryIndex Index into session entries to branch from
|
||||||
|
* @returns The text of the selected user message (for editor pre-fill)
|
||||||
|
*/
|
||||||
|
branch(entryIndex: number): string {
|
||||||
|
const entries = this.sessionManager.loadEntries();
|
||||||
|
const selectedEntry = entries[entryIndex];
|
||||||
|
|
||||||
|
if (!selectedEntry || selectedEntry.type !== "message" || selectedEntry.message.role !== "user") {
|
||||||
|
throw new Error("Invalid entry index for branching");
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedText = this._extractUserMessageText(selectedEntry.message.content);
|
||||||
|
|
||||||
|
// Create branched session
|
||||||
|
const newSessionFile = this.sessionManager.createBranchedSessionFromEntries(entries, entryIndex);
|
||||||
|
this.sessionManager.setSessionFile(newSessionFile);
|
||||||
|
|
||||||
|
// Reload
|
||||||
|
const loaded = loadSessionFromEntries(this.sessionManager.loadEntries());
|
||||||
|
this.agent.replaceMessages(loaded.messages);
|
||||||
|
|
||||||
|
return selectedText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all user messages from session for branch selector.
|
||||||
|
*/
|
||||||
|
getUserMessagesForBranching(): Array<{ entryIndex: number; text: string }> {
|
||||||
|
const entries = this.sessionManager.loadEntries();
|
||||||
|
const result: Array<{ entryIndex: number; text: string }> = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < entries.length; i++) {
|
||||||
|
const entry = entries[i];
|
||||||
|
if (entry.type !== "message") continue;
|
||||||
|
if (entry.message.role !== "user") continue;
|
||||||
|
|
||||||
|
const text = this._extractUserMessageText(entry.message.content);
|
||||||
|
if (text) {
|
||||||
|
result.push({ entryIndex: i, text });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _extractUserMessageText(content: string | Array<{ type: string; text?: string }>): string {
|
||||||
|
if (typeof content === "string") return content;
|
||||||
|
if (Array.isArray(content)) {
|
||||||
|
return content
|
||||||
|
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
||||||
|
.map((c) => c.text)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get session statistics.
|
||||||
|
*/
|
||||||
|
getSessionStats(): SessionStats {
|
||||||
|
const state = this.state;
|
||||||
|
const userMessages = state.messages.filter((m) => m.role === "user").length;
|
||||||
|
const assistantMessages = state.messages.filter((m) => m.role === "assistant").length;
|
||||||
|
const toolResults = state.messages.filter((m) => m.role === "toolResult").length;
|
||||||
|
|
||||||
|
let toolCalls = 0;
|
||||||
|
let totalInput = 0;
|
||||||
|
let totalOutput = 0;
|
||||||
|
let totalCacheRead = 0;
|
||||||
|
let totalCacheWrite = 0;
|
||||||
|
let totalCost = 0;
|
||||||
|
|
||||||
|
for (const message of state.messages) {
|
||||||
|
if (message.role === "assistant") {
|
||||||
|
const assistantMsg = message as AssistantMessage;
|
||||||
|
toolCalls += assistantMsg.content.filter((c) => c.type === "toolCall").length;
|
||||||
|
totalInput += assistantMsg.usage.input;
|
||||||
|
totalOutput += assistantMsg.usage.output;
|
||||||
|
totalCacheRead += assistantMsg.usage.cacheRead;
|
||||||
|
totalCacheWrite += assistantMsg.usage.cacheWrite;
|
||||||
|
totalCost += assistantMsg.usage.cost.total;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessionFile: this.sessionFile,
|
||||||
|
sessionId: this.sessionId,
|
||||||
|
userMessages,
|
||||||
|
assistantMessages,
|
||||||
|
toolCalls,
|
||||||
|
toolResults,
|
||||||
|
totalMessages: state.messages.length,
|
||||||
|
tokens: {
|
||||||
|
input: totalInput,
|
||||||
|
output: totalOutput,
|
||||||
|
cacheRead: totalCacheRead,
|
||||||
|
cacheWrite: totalCacheWrite,
|
||||||
|
total: totalInput + totalOutput + totalCacheRead + totalCacheWrite,
|
||||||
|
},
|
||||||
|
cost: totalCost,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export session to HTML.
|
||||||
|
* @param outputPath Optional output path (defaults to session directory)
|
||||||
|
* @returns Path to exported file
|
||||||
|
*/
|
||||||
|
exportToHtml(outputPath?: string): string {
|
||||||
|
return exportSessionToHtml(this.sessionManager, this.state, outputPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Utilities
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get text content of last assistant message.
|
||||||
|
* Useful for /copy command.
|
||||||
|
* @returns Text content, or null if no assistant message exists
|
||||||
|
*/
|
||||||
|
getLastAssistantText(): string | null {
|
||||||
|
const lastAssistant = this.messages
|
||||||
|
.slice()
|
||||||
|
.reverse()
|
||||||
|
.find((m) => m.role === "assistant");
|
||||||
|
|
||||||
|
if (!lastAssistant) return null;
|
||||||
|
|
||||||
|
let text = "";
|
||||||
|
for (const content of (lastAssistant as AssistantMessage).content) {
|
||||||
|
if (content.type === "text") {
|
||||||
|
text += content.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return text.trim() || null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,5 +9,6 @@ export {
|
||||||
type CompactionResult,
|
type CompactionResult,
|
||||||
type ModelCycleResult,
|
type ModelCycleResult,
|
||||||
type PromptOptions,
|
type PromptOptions,
|
||||||
|
type SessionStats,
|
||||||
} from "./agent-session.js";
|
} from "./agent-session.js";
|
||||||
export { type BashExecutorOptions, type BashResult, executeBash } from "./bash-executor.js";
|
export { type BashExecutorOptions, type BashResult, executeBash } from "./bash-executor.js";
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue