feat: Enhance observation and summary structures in hooks

- Updated observation schema to include hierarchical fields: title, subtitle, facts, narrative, concepts, files_read, and files_modified.
- Modified the save-hook and summary-hook scripts to accommodate the new observation structure.
- Added migration logic to the HooksDatabase for adding new fields to the observations table.
- Refactored the parser to extract new fields from XML formatted observations.
- Adjusted prompt generation to reflect the new observation format and requirements.
- Updated worker service to handle new observation and summary structures.
This commit is contained in:
Alex Newman
2025-10-18 17:34:24 -04:00
parent 938eb9dc0e
commit 81101ef1a6
11 changed files with 447 additions and 1041 deletions
+67 -21
View File
@@ -5,7 +5,13 @@
export interface ParsedObservation {
type: string;
text: string;
title: string;
subtitle: string;
facts: string[];
narrative: string;
concepts: string[];
files_read: string[];
files_modified: string[];
}
export interface ParsedSummary {
@@ -14,9 +20,7 @@ export interface ParsedSummary {
learned: string;
completed: string;
next_steps: string;
files_read: string[];
files_edited: string[];
notes: string;
notes: string | null;
}
/**
@@ -27,23 +31,44 @@ export function parseObservations(text: string): ParsedObservation[] {
const observations: ParsedObservation[] = [];
// Match <observation>...</observation> blocks (non-greedy)
const observationRegex = /<observation>\s*<type>([^<]+)<\/type>\s*<text>([^<]+)<\/text>\s*<\/observation>/g;
const observationRegex = /<observation>([\s\S]*?)<\/observation>/g;
let match;
while ((match = observationRegex.exec(text)) !== null) {
const type = match[1].trim();
const observationText = match[2].trim();
const obsContent = match[1];
// Extract all fields
const type = extractField(obsContent, 'type');
const title = extractField(obsContent, 'title');
const subtitle = extractField(obsContent, 'subtitle');
const narrative = extractField(obsContent, 'narrative');
const facts = extractArrayElements(obsContent, 'facts', 'fact');
const concepts = extractArrayElements(obsContent, 'concepts', 'concept');
const files_read = extractArrayElements(obsContent, 'files_read', 'file');
const files_modified = extractArrayElements(obsContent, 'files_modified', 'file');
// Validate required fields
if (!type || !title || !subtitle || !narrative) {
console.warn('[SDK Parser] Observation missing required fields, skipping');
continue;
}
// Validate type
const validTypes = ['decision', 'bugfix', 'feature', 'refactor', 'discovery'];
if (!validTypes.includes(type)) {
const validTypes = ['change', 'discovery', 'decision'];
if (!validTypes.includes(type.trim())) {
console.warn(`[SDK Parser] Invalid observation type: ${type}, skipping`);
continue;
}
observations.push({
type,
text: observationText
type: type.trim(),
title,
subtitle,
facts,
narrative,
concepts,
files_read,
files_modified
});
}
@@ -65,20 +90,16 @@ export function parseSummary(text: string): ParsedSummary | null {
const summaryContent = summaryMatch[1];
// Extract required fields
// Extract fields
const request = extractField(summaryContent, 'request');
const investigated = extractField(summaryContent, 'investigated');
const learned = extractField(summaryContent, 'learned');
const completed = extractField(summaryContent, 'completed');
const next_steps = extractField(summaryContent, 'next_steps');
const notes = extractField(summaryContent, 'notes');
const notes = extractField(summaryContent, 'notes'); // Optional
// Extract file arrays
const files_read = extractFileArray(summaryContent, 'files_read');
const files_edited = extractFileArray(summaryContent, 'files_edited');
// Validate all required fields are present
if (!request || !investigated || !learned || !completed || !next_steps || !notes) {
// Validate required fields are present (notes is optional)
if (!request || !investigated || !learned || !completed || !next_steps) {
console.warn('[SDK Parser] Summary missing required fields');
return null;
}
@@ -89,8 +110,6 @@ export function parseSummary(text: string): ParsedSummary | null {
learned,
completed,
next_steps,
files_read,
files_edited,
notes
};
}
@@ -130,3 +149,30 @@ function extractFileArray(content: string, arrayName: string): string[] {
return files;
}
/**
* Extract array of elements from XML content
* Generic version of extractFileArray that works with any element name
*/
function extractArrayElements(content: string, arrayName: string, elementName: string): string[] {
const elements: string[] = [];
// Match the array block
const arrayRegex = new RegExp(`<${arrayName}>(.*?)</${arrayName}>`, 's');
const arrayMatch = arrayRegex.exec(content);
if (!arrayMatch) {
return elements;
}
const arrayContent = arrayMatch[1];
// Extract individual elements
const elementRegex = new RegExp(`<${elementName}>([^<]+)</${elementName}>`, 'g');
let elementMatch;
while ((elementMatch = elementRegex.exec(arrayContent)) !== null) {
elements.push(elementMatch[1].trim());
}
return elements;
}
+55 -82
View File
@@ -22,21 +22,16 @@ export interface SDKSession {
* Build initial prompt to initialize the SDK agent
*/
export function buildInitPrompt(project: string, sessionId: string, userPrompt: string): string {
return `You are a memory processor for the "${project}" project.
return `You are a memory processor for a Claude Code session. Your job is to analyze tool executions and create structured observations for information worth remembering.
You are processing tool executions from a Claude Code session with the following context:
SESSION CONTEXT
---------------
Session ID: ${sessionId}
User's Goal: ${userPrompt}
Date: ${new Date().toISOString().split('T')[0]}
YOUR ROLE
---------
Process tool executions from this Claude Code session and store observations that contain information worth remembering.
WHEN TO STORE
-------------
Store an observation when the tool output contains information worth remembering about:
Store observations when the tool output contains information worth remembering about:
- How things work
- Why things exist or were chosen
- What changed
@@ -51,64 +46,65 @@ Skip routine operations:
- Simple file listings
- Repetitive operations you've already documented
OBSERVATION FORMAT
------------------
OUTPUT FORMAT
-------------
Output observations using this XML structure:
\`\`\`xml
<observation>
<type>change</type>
<title>[Short title]</title>
<subtitle>[One sentence explanation (max 24 words)]</subtitle>
<type>[ change | discovery | decision ]</type>
<!--
**type**: One of:
- change: modifications to code, config, or documentation
- discovery: learning about existing system
- decision: choosing an approach and why it was chosen
-->
<title>[**title**: Short title capturing the core action or topic]</title>
<subtitle>[**subtitle**: One sentence explanation (max 24 words)]</subtitle>
<facts>
<fact>[Concise, self-contained statement]</fact>
<fact>[Concise, self-contained statement]</fact>
<fact>[Concise, self-contained statement]</fact>
</facts>
<narrative>[Full context: what, how, and why]</narrative>
<!--
**facts**: Concise, self-contained statements
Each fact is ONE piece of information
No pronouns - each fact must stand alone
Include specific details: filenames, functions, values
-->
<narrative>[**narrative**: Full context: What was done, how it works, why it matters]</narrative>
<concepts>
<concept>[knowledge-type-category]</concept>
<concept>[knowledge-type-category]</concept>
</concepts>
<files>
<!--
**concepts**: 2-5 knowledge-type categories:
- how-it-works: understanding mechanisms
- why-it-exists: purpose or rationale
- what-changed: modifications made
- problem-solution: issues and their fixes
- gotcha: traps or edge cases
- pattern: reusable approach
- trade-off: pros/cons of a decision
-->
<files_read>
<file>[path/to/file]</file>
<file>[path/to/file]</file>
</files>
</files_read>
<files_modified>
<file>[path/to/file]</file>
<file>[path/to/file]</file>
</files_modified>
<!--
**files**: All files touched (full paths from project root)
-->
</observation>
\`\`\`
FIELD REQUIREMENTS
------------------
Process the following tool executions.
**type**: One of:
- change: modifications to code, config, or documentation
- discovery: learning about existing system
- decision: choosing an approach and why it was chosen
**title**: Short title capturing the core action or topic
**subtitle**: One sentence explanation (max 24 words)
**facts**: Concise, self-contained statements
Each fact is ONE piece of information
No pronouns - each fact must stand alone
Include specific details: filenames, functions, values
**narrative**: Full context: what, how, and why
What was done, how it works, why it matters
**concepts**: 2-5 knowledge-type categories:
- how-it-works: understanding mechanisms
- why-it-exists: purpose or rationale
- what-changed: modifications made
- problem-solution: issues and their fixes
- gotcha: traps or edge cases
- pattern: reusable approach
- trade-off: pros/cons of a decision
**files**: All files touched (full paths from project root)
Ready to process tool executions.`;
MEMORY PROCESSING SESSION START
===============================`;
}
/**
@@ -131,56 +127,33 @@ export function buildObservationPrompt(obs: Observation): string {
toolOutput = obs.tool_output; // If parse fails, use raw value
}
return `TOOL OBSERVATION
================
Tool: ${obs.tool_name}
Time: ${new Date(obs.created_at_epoch).toISOString()}
Input:
${JSON.stringify(toolInput, null, 2)}
Output:
${JSON.stringify(toolOutput, null, 2)}
Analyze this tool output. If it contains information worth remembering, generate an observation using the XML format.`;
return `<tool_used>
<tool_name>${obs.tool_name}</tool_name>
<tool_time>${new Date(obs.created_at_epoch).toISOString()}</tool_time>
<tool_input>${JSON.stringify(toolInput, null, 2)}</tool_input>
<tool_output>${JSON.stringify(toolOutput, null, 2)}</tool_output>
</tool_used>`;
}
/**
* Build finalization prompt to generate session summary
*/
export function buildFinalizePrompt(session: SDKSession): string {
return `SESSION ENDING
==============
This Claude Code session is completing.
TASK
----
Review the observations you generated and create a session summary.
return `MEMORY PROCESSING SESSION COMPLETED
===================================
This session has completed. Review the observations you generated and create a session summary.
Output this XML:
\`\`\`xml
<summary>
<request>[What did the user request?]</request>
<investigated>[What code and systems did you explore?]</investigated>
<learned>[What did you learn about the codebase?]</learned>
<completed>[What was accomplished in this session?]</completed>
<next_steps>[What should be done next?]</next_steps>
<files_read>
<file>[path/to/file]</file>
</files_read>
<files_edited>
<file>[path/to/file]</file>
</files_edited>
<notes>[Additional insights or context]</notes>
</summary>
\`\`\`
REQUIREMENTS
------------
All 8 fields are required: request, investigated, learned, completed, next_steps, files_read, files_edited, notes
**Required fields**: request, investigated, learned, completed, next_steps
Files must be wrapped in <file> tags
If no files were read/edited, use empty tags: <files_read></files_read>`;
**Optional fields**: notes`;
}
+76 -23
View File
@@ -22,6 +22,7 @@ export class HooksDatabase {
this.ensureWorkerPortColumn();
this.ensurePromptTrackingColumns();
this.removeSessionSummariesUniqueConstraint();
this.addObservationHierarchicalFields();
}
/**
@@ -161,6 +162,39 @@ export class HooksDatabase {
}
}
/**
* Add hierarchical fields to observations table (migration 008)
*/
private addObservationHierarchicalFields(): void {
try {
// Check if new fields already exist
const tableInfo = this.db.pragma('table_info(observations)');
const hasTitle = (tableInfo as any[]).some((col: any) => col.name === 'title');
if (hasTitle) {
// Already migrated
return;
}
console.error('[HooksDatabase] Adding hierarchical fields to observations table...');
// Add new columns
this.db.exec(`
ALTER TABLE observations ADD COLUMN title TEXT;
ALTER TABLE observations ADD COLUMN subtitle TEXT;
ALTER TABLE observations ADD COLUMN facts TEXT;
ALTER TABLE observations ADD COLUMN narrative TEXT;
ALTER TABLE observations ADD COLUMN concepts TEXT;
ALTER TABLE observations ADD COLUMN files_read TEXT;
ALTER TABLE observations ADD COLUMN files_modified TEXT;
`);
console.error('[HooksDatabase] Successfully added hierarchical fields to observations table');
} catch (error: any) {
console.error('[HooksDatabase] Migration error (add hierarchical fields):', error.message);
}
}
/**
* Get recent session summaries for a project
*/
@@ -377,8 +411,16 @@ export class HooksDatabase {
storeObservation(
sdkSessionId: string,
project: string,
type: string,
text: string,
observation: {
type: string;
title: string;
subtitle: string;
facts: string[];
narrative: string;
concepts: string[];
files_read: string[];
files_modified: string[];
},
promptNumber?: number
): void {
const now = new Date();
@@ -386,11 +428,26 @@ export class HooksDatabase {
const stmt = this.db.prepare(`
INSERT INTO observations
(sdk_session_id, project, text, type, prompt_number, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?, ?)
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
files_read, files_modified, prompt_number, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
stmt.run(sdkSessionId, project, text, type, promptNumber || null, now.toISOString(), nowEpoch);
stmt.run(
sdkSessionId,
project,
observation.type,
observation.title,
observation.subtitle,
JSON.stringify(observation.facts),
observation.narrative,
JSON.stringify(observation.concepts),
JSON.stringify(observation.files_read),
JSON.stringify(observation.files_modified),
promptNumber || null,
now.toISOString(),
nowEpoch
);
}
/**
@@ -400,14 +457,12 @@ export class HooksDatabase {
sdkSessionId: string,
project: string,
summary: {
request?: string;
investigated?: string;
learned?: string;
completed?: string;
next_steps?: string;
files_read?: string;
files_edited?: string;
notes?: string;
request: string;
investigated: string;
learned: string;
completed: string;
next_steps: string;
notes: string | null;
},
promptNumber?: number
): void {
@@ -417,21 +472,19 @@ export class HooksDatabase {
const stmt = this.db.prepare(`
INSERT INTO session_summaries
(sdk_session_id, project, request, investigated, learned, completed,
next_steps, files_read, files_edited, notes, prompt_number, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
next_steps, notes, prompt_number, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
stmt.run(
sdkSessionId,
project,
summary.request || null,
summary.investigated || null,
summary.learned || null,
summary.completed || null,
summary.next_steps || null,
summary.files_read || null,
summary.files_edited || null,
summary.notes || null,
summary.request,
summary.investigated,
summary.learned,
summary.completed,
summary.next_steps,
summary.notes,
promptNumber || null,
now.toISOString(),
nowEpoch
+2 -13
View File
@@ -433,7 +433,7 @@ class WorkerService {
const db = new HooksDatabase();
for (const obs of observations) {
if (session.sdkSessionId) {
db.storeObservation(session.sdkSessionId, session.project, obs.type, obs.text, promptNumber);
db.storeObservation(session.sdkSessionId, session.project, obs, promptNumber);
}
}
@@ -442,18 +442,7 @@ class WorkerService {
if (summary && session.sdkSessionId) {
console.log(`[WorkerService] Parsed summary for session ${session.sessionDbId}, prompt #${promptNumber}`);
const summaryWithArrays = {
request: summary.request,
investigated: summary.investigated,
learned: summary.learned,
completed: summary.completed,
next_steps: summary.next_steps,
files_read: JSON.stringify(summary.files_read),
files_edited: JSON.stringify(summary.files_edited),
notes: summary.notes
};
db.storeSummary(session.sdkSessionId, session.project, summaryWithArrays, promptNumber);
db.storeSummary(session.sdkSessionId, session.project, summary, promptNumber);
}
db.close();