feat: Implement Phase 1 of SDK agent architecture with hook integration

- Added CLI commands for context, new session, save observation, and summary.
- Created HooksDatabase for managing SDK sessions and observations.
- Implemented migration 004 to add new tables: sdk_sessions, observation_queue, observations, and session_summaries.
- Developed hook functions for context display, session initialization, observation queuing, and session finalization.
- Added comprehensive tests for database schema and hook functionality.
- Documented Phase 1 implementation in PHASE1-COMPLETE.md.
This commit is contained in:
Alex Newman
2025-10-15 19:06:51 -04:00
parent 917ab9740c
commit e81ea69143
12 changed files with 1294 additions and 129 deletions
+92
View File
@@ -0,0 +1,92 @@
import { HooksDatabase } from '../services/sqlite/HooksDatabase.js';
import { PathDiscovery } from '../services/path-discovery.js';
import path from 'path';
export interface SessionStartInput {
session_id: string;
cwd: string;
source?: string;
[key: string]: any;
}
/**
* Context Hook - SessionStart
* Shows user what happened in recent sessions
*/
export function contextHook(input: SessionStartInput): void {
try {
// Only run on startup (not on resume)
if (input.source && input.source !== 'startup') {
console.log(''); // Output nothing, just exit
process.exit(0);
}
// Extract project from cwd
const project = path.basename(input.cwd);
// Get recent summaries
const db = new HooksDatabase();
const summaries = db.getRecentSummaries(project, 5);
db.close();
// If no summaries, exit silently
if (summaries.length === 0) {
console.log(''); // Output nothing
process.exit(0);
}
// Format output for Claude
const output: string[] = [];
output.push('# Recent Session Context');
output.push('');
output.push(`Here's what happened in recent ${project} sessions:`);
output.push('');
for (const summary of summaries) {
output.push('---');
output.push('');
if (summary.request) {
output.push(`**Request:** ${summary.request}`);
}
if (summary.completed) {
output.push(`**Completed:** ${summary.completed}`);
}
if (summary.learned) {
output.push(`**Learned:** ${summary.learned}`);
}
if (summary.next_steps) {
output.push(`**Next Steps:** ${summary.next_steps}`);
}
if (summary.files_edited) {
try {
const files = JSON.parse(summary.files_edited);
if (Array.isArray(files) && files.length > 0) {
output.push(`**Files Edited:** ${files.join(', ')}`);
}
} catch {
// If not valid JSON, show as text
if (summary.files_edited.trim()) {
output.push(`**Files Edited:** ${summary.files_edited}`);
}
}
}
output.push(`**Date:** ${summary.created_at.split('T')[0]}`);
output.push('');
}
// Output to stdout for Claude Code to inject
console.log(output.join('\n'));
process.exit(0);
} catch (error: any) {
// On error, exit silently - don't block Claude Code
console.error(`[claude-mem context error: ${error.message}]`);
process.exit(0);
}
}
+4
View File
@@ -0,0 +1,4 @@
export { contextHook } from './context.js';
export { saveHook } from './save.js';
export { newHook } from './new.js';
export { summaryHook } from './summary.js';
+60
View File
@@ -0,0 +1,60 @@
import { HooksDatabase } from '../services/sqlite/HooksDatabase.js';
import path from 'path';
import { spawn } from 'child_process';
export interface UserPromptSubmitInput {
session_id: string;
cwd: string;
prompt: string;
[key: string]: any;
}
/**
* New Hook - UserPromptSubmit
* Initializes SDK memory session in background
*/
export function newHook(input: UserPromptSubmitInput): void {
try {
const { session_id, cwd, prompt } = input;
// Extract project from cwd
const project = path.basename(cwd);
// Check if session already exists
const db = new HooksDatabase();
const existing = db.findActiveSDKSession(session_id);
if (existing) {
// Session already initialized, just continue
db.close();
console.log('{"continue": true, "suppressOutput": true}');
process.exit(0);
}
// Create SDK session record
const sessionId = db.createSDKSession(session_id, project, prompt);
db.close();
// Start SDK worker in background
// The SDK worker will be implemented in a separate file
// For now, we just create the session record
// TODO: Spawn SDK worker as detached process
// const workerPath = path.join(__dirname, '..', 'sdk', 'worker.js');
// const child = spawn('bun', [workerPath, sessionId.toString()], {
// detached: true,
// stdio: 'ignore'
// });
// child.unref();
// Output hook response
console.log('{"continue": true, "suppressOutput": true}');
process.exit(0);
} catch (error: any) {
// On error, don't block Claude Code
console.error(`[claude-mem new error: ${error.message}]`);
console.log('{"continue": true, "suppressOutput": true}');
process.exit(0);
}
}
+72
View File
@@ -0,0 +1,72 @@
import { HooksDatabase } from '../services/sqlite/HooksDatabase.js';
import path from 'path';
export interface PostToolUseInput {
session_id: string;
cwd: string;
tool_name: string;
tool_input: any;
tool_output: any;
[key: string]: any;
}
// Tools to skip (low value or too frequent)
const SKIP_TOOLS = new Set([
'TodoWrite',
'ListMcpResourcesTool'
]);
/**
* Save Hook - PostToolUse
* Queues tool observations for SDK processing
*/
export function saveHook(input: PostToolUseInput): void {
try {
const { session_id, cwd, tool_name, tool_input, tool_output } = input;
// Skip certain tools
if (SKIP_TOOLS.has(tool_name)) {
console.log('{"continue": true, "suppressOutput": true}');
process.exit(0);
}
// Extract project from cwd
const project = path.basename(cwd);
// Find active SDK session
const db = new HooksDatabase();
const session = db.findActiveSDKSession(session_id);
if (!session) {
// No active session yet - this can happen if UserPromptSubmit hasn't run
// Just exit silently
db.close();
console.log('{"continue": true, "suppressOutput": true}');
process.exit(0);
}
// Queue the observation
// SDK session ID might be null if init message hasn't arrived yet
// Use the internal ID as a fallback
const sdkSessionId = session.sdk_session_id || `pending-${session.id}`;
db.queueObservation(
sdkSessionId,
tool_name,
JSON.stringify(tool_input),
JSON.stringify(tool_output)
);
db.close();
// Output hook response
console.log('{"continue": true, "suppressOutput": true}');
process.exit(0);
} catch (error: any) {
// On error, don't block Claude Code
console.error(`[claude-mem save error: ${error.message}]`);
console.log('{"continue": true, "suppressOutput": true}');
process.exit(0);
}
}
+50
View File
@@ -0,0 +1,50 @@
import { HooksDatabase } from '../services/sqlite/HooksDatabase.js';
export interface StopInput {
session_id: string;
cwd: string;
[key: string]: any;
}
/**
* Summary Hook - Stop
* Signals SDK to finalize and generate summary
*/
export function summaryHook(input: StopInput): void {
try {
const { session_id } = input;
// Find active SDK session
const db = new HooksDatabase();
const session = db.findActiveSDKSession(session_id);
if (!session) {
// No active session - nothing to finalize
db.close();
console.log('{"continue": true, "suppressOutput": true}');
process.exit(0);
}
// Insert special FINALIZE message into observation queue
const sdkSessionId = session.sdk_session_id || `pending-${session.id}`;
db.queueObservation(
sdkSessionId,
'FINALIZE',
'{}',
'{}'
);
db.close();
// Output hook response
console.log('{"continue": true, "suppressOutput": true}');
process.exit(0);
} catch (error: any) {
// On error, don't block Claude Code
console.error(`[claude-mem summary error: ${error.message}]`);
console.log('{"continue": true, "suppressOutput": true}');
process.exit(0);
}
}