/**
* SDK Prompts Module
* Generates prompts for the Claude Agent SDK memory worker
*/
import { logger } from '../utils/logger.js';
import type { ModeConfig } from '../services/domain/types.js';
export interface Observation {
id: number;
tool_name: string;
tool_input: string;
tool_output: string;
created_at_epoch: number;
cwd?: string;
}
export interface SDKSession {
id: number;
memory_session_id: string | null;
project: string;
user_prompt: string;
last_assistant_message?: string;
}
/**
* Build initial prompt to initialize the SDK agent
*/
export function buildInitPrompt(project: string, sessionId: string, userPrompt: string, mode: ModeConfig): string {
return `${mode.prompts.system_identity}
${userPrompt}
${new Date().toISOString().split('T')[0]}
${mode.prompts.observer_role}
${mode.prompts.spatial_awareness}
${mode.prompts.recording_focus}
${mode.prompts.skip_guidance}
${mode.prompts.output_format_header}
\`\`\`xml
[ ${mode.observation_types.map(t => t.id).join(' | ')} ]
${mode.prompts.xml_title_placeholder}
${mode.prompts.xml_subtitle_placeholder}
${mode.prompts.xml_fact_placeholder}
${mode.prompts.xml_fact_placeholder}
${mode.prompts.xml_fact_placeholder}
${mode.prompts.xml_narrative_placeholder}
${mode.prompts.xml_concept_placeholder}
${mode.prompts.xml_concept_placeholder}
${mode.prompts.xml_file_placeholder}
${mode.prompts.xml_file_placeholder}
${mode.prompts.xml_file_placeholder}
${mode.prompts.xml_file_placeholder}
\`\`\`
${mode.prompts.format_examples}
${mode.prompts.footer}
${mode.prompts.header_memory_start}`;
}
/**
* Build prompt to send tool observation to SDK agent
*/
export function buildObservationPrompt(obs: Observation): string {
// Safely parse tool_input and tool_output - they're already JSON strings
let toolInput: any;
let toolOutput: any;
try {
toolInput = typeof obs.tool_input === 'string' ? JSON.parse(obs.tool_input) : obs.tool_input;
} catch (error) {
logger.debug('SDK', 'Tool input is plain string, using as-is', {
toolName: obs.tool_name
}, error as Error);
toolInput = obs.tool_input;
}
try {
toolOutput = typeof obs.tool_output === 'string' ? JSON.parse(obs.tool_output) : obs.tool_output;
} catch (error) {
logger.debug('SDK', 'Tool output is plain string, using as-is', {
toolName: obs.tool_name
}, error as Error);
toolOutput = obs.tool_output;
}
return `
${obs.tool_name}
${new Date(obs.created_at_epoch).toISOString()}${obs.cwd ? `\n ${obs.cwd}` : ''}
${JSON.stringify(toolInput, null, 2)}
${JSON.stringify(toolOutput, null, 2)}
Return either one or more ... blocks, or an empty response if this tool use should be skipped.
Concrete debugging findings from logs, queue state, database rows, session routing, or code-path inspection count as durable discoveries and should be recorded.
Never reply with prose such as "Skipping", "No substantive tool executions", or any explanation outside XML. Non-XML text is discarded.`;
}
/**
* Build prompt to generate progress summary
*/
export function buildSummaryPrompt(session: SDKSession, mode: ModeConfig): string {
const lastAssistantMessage = session.last_assistant_message || (() => {
logger.error('SDK', 'Missing last_assistant_message in session for summary prompt', {
sessionId: session.id
});
return '';
})();
return `--- MODE SWITCH: PROGRESS SUMMARY ---
Do NOT output tags. This is a summary request, not an observation request.
Your response MUST use tags ONLY. Any output will be discarded.
${mode.prompts.header_summary_checkpoint}
${mode.prompts.summary_instruction}
${mode.prompts.summary_context_label}
${lastAssistantMessage}
${mode.prompts.summary_format_instruction}
${mode.prompts.xml_summary_request_placeholder}
${mode.prompts.xml_summary_investigated_placeholder}
${mode.prompts.xml_summary_learned_placeholder}
${mode.prompts.xml_summary_completed_placeholder}
${mode.prompts.xml_summary_next_steps_placeholder}
${mode.prompts.xml_summary_notes_placeholder}
${mode.prompts.summary_footer}`;
}
/**
* Build prompt for continuation of existing session
*
* CRITICAL: Why contentSessionId Parameter is Required
* ====================================================
* This function receives contentSessionId from SDKAgent.ts, which comes from:
* - SessionManager.initializeSession (fetched from database)
* - SessionStore.createSDKSession (stored by new-hook.ts)
* - new-hook.ts receives it from Claude Code's hook context
*
* The contentSessionId is the SAME session_id used by:
* - NEW hook (to create/fetch session)
* - SAVE hook (to store observations)
* - This continuation prompt (to maintain session context)
*
* This is how everything stays connected - ONE session_id threading through
* all hooks and prompts in the same conversation.
*
* Called when: promptNumber > 1 (see SDKAgent.ts line 150)
* First prompt: Uses buildInitPrompt instead (promptNumber === 1)
*/
export function buildContinuationPrompt(userPrompt: string, promptNumber: number, contentSessionId: string, mode: ModeConfig): string {
return `${mode.prompts.continuation_greeting}
${userPrompt}
${new Date().toISOString().split('T')[0]}
${mode.prompts.system_identity}
${mode.prompts.observer_role}
${mode.prompts.spatial_awareness}
${mode.prompts.recording_focus}
${mode.prompts.skip_guidance}
${mode.prompts.continuation_instruction}
${mode.prompts.output_format_header}
\`\`\`xml
[ ${mode.observation_types.map(t => t.id).join(' | ')} ]
${mode.prompts.xml_title_placeholder}
${mode.prompts.xml_subtitle_placeholder}
${mode.prompts.xml_fact_placeholder}
${mode.prompts.xml_fact_placeholder}
${mode.prompts.xml_fact_placeholder}
${mode.prompts.xml_narrative_placeholder}
${mode.prompts.xml_concept_placeholder}
${mode.prompts.xml_concept_placeholder}
${mode.prompts.xml_file_placeholder}
${mode.prompts.xml_file_placeholder}
${mode.prompts.xml_file_placeholder}
${mode.prompts.xml_file_placeholder}
\`\`\`
${mode.prompts.format_examples}
${mode.prompts.footer}
${mode.prompts.header_memory_continued}`;
}