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:
+67
-21
@@ -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
@@ -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`;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user