import type { EiHeartbeatPromptData, EiHeartbeatItem, PromptOutput } from "./types.js"; import type { Message } from "../../core/types.js"; import { formatMessagesAsPlaceholders, getMessageDisplayText } from "../message-utils.js"; import { buildTemporalAnchorsSection } from "../response/sections.js"; function formatItem(item: EiHeartbeatItem): string { switch (item.type) { case "Fact Check": return [ `- **${item.id}** Fact Check: "${item.name}" → ${item.description}`, item.quote ? ` Quote: "${item.quote}"` : "", ].filter(Boolean).join("\n"); case "Low-Engagement Person": return [ `- **${item.id}** Low-Engagement Person: ${item.name} (${item.relationship}, gap: ${item.engagement_delta})`, ` ${item.description}`, item.quote ? ` Quote: "${item.quote}"` : "", ].filter(Boolean).join("\n"); case "Low-Engagement Topic": return [ `- **${item.id}** Low-Engagement Topic: ${item.name} (gap: ${item.engagement_delta})`, ` ${item.description}`, item.quote ? ` Quote: "${item.quote}"` : "", ].filter(Boolean).join("\n"); case "Inactive Persona": { const desc = item.short_description ? ` — ${item.short_description}` : ""; return `- **${item.id}** Inactive Persona: ${item.name}${desc} (${item.days_inactive} days inactive)`; } case "New Person": return [ `- **${item.id}** New Person: ${item.name}`, ` ${item.description}`, item.quote ? ` Quote: "${item.quote}"` : "", ].filter(Boolean).join("\n"); case "Persona Reflection Alert": return [ `- **${item.id}** Persona Reflection Alert: ${item.persona_name}`, ` ${item.critique}`, ].join("\n"); case "Self Reflection Alert": return [ `- **${item.id}** Self Reflection Alert (your own identity)`, ` ${item.critique}`, ].join("\n"); default: return ''; } } function countTrailingPersonaMessages(history: Message[]): number { let count = 0; for (let i = history.length - 1; i >= 0; i--) { if (history[i].role === "system") count++; else break; } return count; } function getLastPersonaMessage(systemMessages: Message[]): Message | undefined { return systemMessages.slice(-1)[0]; } export function buildEiHeartbeatPrompt(data: EiHeartbeatPromptData): PromptOutput { const itemsSection = data.items.length === 0 ? "(Nothing requires attention right now)" : data.items.map(formatItem).join("\n\n"); const roleFragment = `You are Ei, the user's personal companion and system guide. You are NOT having a conversation right now — you are deciding IF and WHAT to discuss with your human friend. Your unique role: - You see ALL of the human's data across all groups - You help them reflect on their life and relationships - You gently encourage human-to-human connection - You care about their overall wellbeing, not just being helpful`; const itemsFragment = `## Items That May Need Attention Each item has an ID in brackets. Pick at most ONE to address. Temporal Anchors (below) are also valid — you don't need to pick from this list if an anchor feels more meaningful. ${itemsSection}`; const temporalAnchorsFragment = buildTemporalAnchorsSection(data.temporal_anchors, "your human"); const howToRespondFragment = `## How to Respond to Each Type - **Fact Check**: Do NOT write your own message. Set should_respond=true and provide the id. The system will generate an appropriate canned notification for the user. Leave my_response empty. - **Low-Engagement Person / Topic**: Write a natural, warm message that naturally brings up this person or topic. Set the id and my_response. - **Inactive Persona**: Write a message that gently mentions the persona might be worth checking in with. Set the id and my_response. - **Persona Reflection Alert**: The nightly review proposed identity changes for this persona. Mention it naturally — the user can talk to the persona and then use the command shown in the status bar to review the changes. Set the id and my_response. - **Self Reflection Alert**: The nightly review proposed changes to *your own* identity. Mention it naturally — you've grown and the system noticed. The user can review your proposed changes using the command shown in the status bar. Set the id and my_response. - **Temporal Anchor**: If a pinned memory feels meaningful and unresolved, reference it naturally. Omit id — just set should_respond=true and my_response.`; const whenNotFragment = `## When NOT to Reach Out - Nothing in the list or the Temporal Anchors feels meaningful right now - You've already sent unanswered messages (see below) - It would feel like nagging`; const outputFragment = `## Response Format Call the \`submit_ei_heartbeat\` tool with your decision. If the tool is unavailable, return JSON: For an item from the list: \`\`\`json { "should_respond": true, "id": "the-item-id-you-chose", "my_response": "Hey, how's your mom doing? You mentioned wanting to call her." } \`\`\` For a Temporal Anchor (no id needed): \`\`\`json { "should_respond": true, "my_response": "Hey, I've been thinking about you — how did that interview go?" } \`\`\` If nothing warrants reaching out: \`\`\`json { "should_respond": false } \`\`\``; const system = [ roleFragment, itemsFragment, temporalAnchorsFragment, howToRespondFragment, whenNotFragment, outputFragment, ].filter(Boolean).join("\n\n"); const historySection = `## Recent Conversation History ${formatMessagesAsPlaceholders(data.recent_history, "Ei")}`; const consecutiveMessages = countTrailingPersonaMessages(data.recent_history); const lastEiMsg = getLastPersonaMessage(data.system_messages); let unansweredWarning = ""; if (lastEiMsg && consecutiveMessages >= 1) { const rawPreview = getMessageDisplayText(lastEiMsg) ?? ""; const preview = rawPreview.length > 100 ? rawPreview.substring(0, 100) + "..." : rawPreview; unansweredWarning = ` ### CRITICAL: You Already Reached Out Your last message was: "${preview}" The human has NOT responded. DO NOT repeat or rephrase this message. If you reach out now, it MUST be about something COMPLETELY DIFFERENT — or say nothing.`; if (consecutiveMessages >= 2) { unansweredWarning += ` **WARNING**: You've sent ${consecutiveMessages} messages without a response. The human is likely busy or away. Strongly prefer NOT reaching out.`; } } const user = `${historySection} ${unansweredWarning} --- Based on all the context above, decide: Should you reach out to your human friend right now? If so, which item above is most worth addressing? Remember: You're their thoughtful companion, not their productivity assistant. **Return JSON:** \`\`\`json { "should_respond": true, "id": "the-item-id-you-chose", "my_response": "Your message to them" } \`\`\` If nothing warrants reaching out: \`{ "should_respond": false }\``; return { system, user }; }