Session compaction + project_id from WS events
Memory: - compactSession(): LLM summarization via Haiku (cheap) - needsCompaction(): checks if session file > 50K chars - runCompaction(): summarize → append recent → truncate session Router: - Uses project_id from WS event data (injected by Tracker) - Falls back to chat_id→project mapping - Background compaction check after each message (non-blocking)
This commit is contained in:
parent
b2ef840c8e
commit
1c7fdf8d77
@ -170,3 +170,102 @@ export function summarizeToolLog(toolLog: Array<{ name: string; result?: string
|
|||||||
const toolNames = [...new Set(toolLog.map(t => t.name))];
|
const toolNames = [...new Set(toolLog.map(t => t.name))];
|
||||||
return `Used tools: ${toolNames.join(', ')}`;
|
return `Used tools: ${toolNames.join(', ')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Session compaction ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compact a session: summarize the JSONL session file into a few lines,
|
||||||
|
* append to recent.md, and optionally update context.md.
|
||||||
|
*
|
||||||
|
* Uses Anthropic API directly (haiku for cheap summarization).
|
||||||
|
*/
|
||||||
|
export async function compactSession(
|
||||||
|
agentHome: string,
|
||||||
|
projectId: string,
|
||||||
|
sessionContent: string,
|
||||||
|
apiKey: string,
|
||||||
|
): Promise<string> {
|
||||||
|
if (!sessionContent.trim()) return '';
|
||||||
|
|
||||||
|
const prompt = `Summarize the following agent session into 3-5 bullet points.
|
||||||
|
Focus on: what was done, key decisions made, problems encountered, results.
|
||||||
|
Be concise. Output only bullet points, no headers.
|
||||||
|
|
||||||
|
Session:
|
||||||
|
${sessionContent.slice(0, 8000)}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-api-key': apiKey,
|
||||||
|
'anthropic-version': '2023-06-01',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: 'claude-haiku-4-5',
|
||||||
|
max_tokens: 300,
|
||||||
|
messages: [{ role: 'user', content: prompt }],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
log.warn({ status: response.status }, 'Compaction API call failed');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json() as { content: Array<{ text: string }> };
|
||||||
|
const summary = data.content?.[0]?.text || '';
|
||||||
|
log.info({ projectId, summaryChars: summary.length }, 'Session compacted');
|
||||||
|
return summary;
|
||||||
|
} catch (err) {
|
||||||
|
log.warn({ err }, 'Session compaction failed');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a session file is large enough to warrant compaction.
|
||||||
|
* Returns the session content if compaction needed, empty string otherwise.
|
||||||
|
*/
|
||||||
|
export function needsCompaction(sessionFile: string, maxChars: number = 50_000): string {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(sessionFile)) return '';
|
||||||
|
const content = fs.readFileSync(sessionFile, 'utf-8');
|
||||||
|
if (content.length > maxChars) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// skip
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run compaction on a session: summarize → append recent → truncate session.
|
||||||
|
*/
|
||||||
|
export async function runCompaction(
|
||||||
|
agentHome: string,
|
||||||
|
projectId: string,
|
||||||
|
sessionFile: string,
|
||||||
|
apiKey: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const content = needsCompaction(sessionFile);
|
||||||
|
if (!content) return;
|
||||||
|
|
||||||
|
log.info({ projectId, sessionChars: content.length }, 'Starting session compaction');
|
||||||
|
|
||||||
|
const summary = await compactSession(agentHome, projectId, content, apiKey);
|
||||||
|
if (!summary) return;
|
||||||
|
|
||||||
|
// Append summary to recent.md
|
||||||
|
appendRecent(agentHome, projectId, `[compaction] ${summary.replace(/\n/g, ' | ')}`);
|
||||||
|
|
||||||
|
// Truncate session file — keep last 10K chars as context continuity
|
||||||
|
const keepChars = 10_000;
|
||||||
|
const truncated = content.length > keepChars
|
||||||
|
? content.slice(content.length - keepChars)
|
||||||
|
: content;
|
||||||
|
fs.writeFileSync(sessionFile, truncated, 'utf-8');
|
||||||
|
log.info({ projectId, oldChars: content.length, newChars: truncated.length }, 'Session truncated after compaction');
|
||||||
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
import { runAgent } from './agent.js';
|
import { runAgent } from './agent.js';
|
||||||
import { appendRecent, summarizeToolLog } from './memory.js';
|
import { appendRecent, summarizeToolLog, runCompaction } from './memory.js';
|
||||||
import { TrackerClient } from './tracker/client.js';
|
import { TrackerClient } from './tracker/client.js';
|
||||||
import { createTrackerTools } from './tools/index.js';
|
import { createTrackerTools } from './tools/index.js';
|
||||||
import type { ToolDefinition } from '@mariozechner/pi-coding-agent';
|
import type { ToolDefinition } from '@mariozechner/pi-coding-agent';
|
||||||
@ -87,6 +87,7 @@ export class EventRouter {
|
|||||||
const taskId = data.task_id as string | undefined;
|
const taskId = data.task_id as string | undefined;
|
||||||
const chatId = data.chat_id as string | undefined;
|
const chatId = data.chat_id as string | undefined;
|
||||||
const taskKey = data.task_key as string | undefined;
|
const taskKey = data.task_key as string | undefined;
|
||||||
|
const eventProjectId = data.project_id as string | undefined;
|
||||||
|
|
||||||
// Extract author info from nested author object if present
|
// Extract author info from nested author object if present
|
||||||
const author = data.author as Record<string, unknown> | undefined;
|
const author = data.author as Record<string, unknown> | undefined;
|
||||||
@ -121,7 +122,7 @@ export class EventRouter {
|
|||||||
this.log.info('│ %s %s: "%s"', ctx, from, content.slice(0, 200));
|
this.log.info('│ %s %s: "%s"', ctx, from, content.slice(0, 200));
|
||||||
|
|
||||||
const target = chatId ? { chat_id: chatId } : taskId ? { task_id: taskId } : null;
|
const target = chatId ? { chat_id: chatId } : taskId ? { task_id: taskId } : null;
|
||||||
const projectId = this.resolveProjectId(chatId);
|
const projectId = eventProjectId || this.resolveProjectId(chatId);
|
||||||
|
|
||||||
// Stream start
|
// Stream start
|
||||||
if (this.wsTransport && target) {
|
if (this.wsTransport && target) {
|
||||||
@ -167,6 +168,17 @@ export class EventRouter {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.log.warn({ err }, 'Failed to flush memory');
|
this.log.warn({ err }, 'Failed to flush memory');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Background compaction check (non-blocking)
|
||||||
|
if (this.config.sessionId) {
|
||||||
|
const sessionFile = path.join(this.config.agentHome, 'sessions', `${this.config.sessionId}.jsonl`);
|
||||||
|
const apiKey = process.env.ANTHROPIC_API_KEY || '';
|
||||||
|
if (apiKey) {
|
||||||
|
runCompaction(this.config.agentHome, projectId, sessionFile, apiKey).catch(err => {
|
||||||
|
this.log.warn({ err }, 'Background compaction failed');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.log.info('└── MESSAGE handled');
|
this.log.info('└── MESSAGE handled');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user