client = WP_Agentic_Writer_Memanto_Client::get_instance(); // Session lifecycle. add_action( "wpaw_memanto_session_start", [$this, "on_session_start"], 10, 3, ); add_action( "wpaw_memanto_session_end", [$this, "on_session_end"], 10, 2, ); // Write-through: remember on meaningful events. add_action( "wpaw_memanto_user_message", [$this, "on_user_message"], 10, 3, ); add_action( "wpaw_memanto_plan_generated", [$this, "on_plan_generated"], 10, 2, ); add_action( "wpaw_memanto_plan_approved", [$this, "on_plan_approved"], 10, 2, ); add_action( "wpaw_memanto_plan_rejected", [$this, "on_plan_rejected"], 10, 2, ); add_action( "wpaw_memanto_section_written", [$this, "on_section_written"], 10, 3, ); add_action( "wpaw_memanto_block_refined", [$this, "on_block_refined"], 10, 3, ); add_action( "wpaw_memanto_config_saved", [$this, "on_config_saved"], 10, 2, ); } // ========================================================================= // Session Lifecycle // ========================================================================= /** * Called when a conversation session starts. * Ensures agents exist and recalls previous session state. * * @param string $session_id Session ID. * @param int $post_id Post ID. * @param int $user_id WordPress user ID. */ public function on_session_start($session_id, $post_id, $user_id) { if (!$this->client->is_active()) { return; } // Ensure user agent exists. if ($user_id > 0) { $this->client->ensure_agent( $this->client->get_user_agent_id($user_id), ); } // Ensure post agent exists. if ($post_id > 0) { $this->client->ensure_agent( $this->client->get_post_agent_id($post_id), ); } } /** * Called when a conversation session ends. * Stores a session summary memory and deactivates the session. * * @param string $session_id Session ID. * @param int $post_id Post ID. */ public function on_session_end($session_id, $post_id) { if (!$this->client->is_active() || $post_id <= 0) { return; } $post_agent = $this->client->get_post_agent_id($post_id); // Store a session summary. $this->client->remember( $post_agent, "Session ended: " . $session_id, "context", ["session:" . $session_id, "post:" . $post_id], "Session end", ); // Deactivate MEMANTO session to trigger summary generation. $this->client->deactivate_session($post_agent); } // ========================================================================= // Write-Through: Remember on Meaningful Events // ========================================================================= /** * Store a memory when user sends a chat message. * * @param string $session_id Session ID. * @param string $content User message content. * @param int $post_id Post ID. */ public function on_user_message($session_id, $content, $post_id) { if (!$this->client->is_active() || $post_id <= 0) { return; } $this->client->remember( $this->client->get_post_agent_id($post_id), "User instruction: " . wp_strip_all_tags($content), "instruction", ["post:" . $post_id, "session:" . $session_id], ); } /** * Store a memory when a plan is generated. * * @param int $post_id Post ID. * @param array $plan Plan data. */ public function on_plan_generated($post_id, $plan) { if (!$this->client->is_active() || $post_id <= 0) { return; } $title = $plan["title"] ?? "Untitled"; $sections = isset($plan["sections"]) && is_array($plan["sections"]) ? count($plan["sections"]) : 0; $this->client->remember( $this->client->get_post_agent_id($post_id), sprintf('Plan generated: "%s" with %d sections', $title, $sections), "artifact", ["post:" . $post_id, "type:plan"], "Plan: " . $title, ); } /** * Store a memory when user approves a plan. * * @param int $post_id Post ID. * @param array $plan Plan data. */ public function on_plan_approved($post_id, $plan) { if (!$this->client->is_active() || $post_id <= 0) { return; } $title = $plan["title"] ?? "Untitled"; $this->client->remember( $this->client->get_post_agent_id($post_id), sprintf('User approved plan: "%s"', $title), "decision", ["post:" . $post_id, "type:plan"], "Plan approved", ); } /** * Store a memory when user rejects or requests plan changes. * * @param int $post_id Post ID. * @param string $reason Rejection/revision reason. */ public function on_plan_rejected($post_id, $reason) { if (!$this->client->is_active() || $post_id <= 0) { return; } $this->client->remember( $this->client->get_post_agent_id($post_id), "User requested plan revision: " . wp_strip_all_tags($reason), "error", ["post:" . $post_id, "type:plan"], "Plan revision", ); } /** * Store a memory when a section is written. * * @param int $post_id Post ID. * @param string $section_id Section identifier. * @param string $summary Brief section summary. */ public function on_section_written($post_id, $section_id, $summary) { if (!$this->client->is_active() || $post_id <= 0) { return; } $this->client->remember( $this->client->get_post_agent_id($post_id), "Section written (" . $section_id . "): " . wp_strip_all_tags($summary), "artifact", ["post:" . $post_id, "section:" . $section_id], "Section: " . $section_id, ); } /** * Store a memory when a block is refined. * * @param int $post_id Post ID. * @param string $block_id Block identifier. * @param string $instruction Refinement instruction. */ public function on_block_refined($post_id, $block_id, $instruction) { if (!$this->client->is_active() || $post_id <= 0) { return; } $this->client->remember( $this->client->get_post_agent_id($post_id), "Block refined (" . $block_id . "): " . wp_strip_all_tags($instruction), "instruction", ["post:" . $post_id, "block:" . $block_id], ); } /** * Store a memory when post config is saved. * * @param int $post_id Post ID. * @param array $config Post config data. */ public function on_config_saved($post_id, $config) { if (!$this->client->is_active()) { return; } $config_summary = sprintf( "tone=%s, audience=%s, length=%s, language=%s", $config["tone"] ?? "default", $config["audience"] ?? "general", $config["article_length"] ?? "medium", $config["language"] ?? "auto", ); // Store to post agent. if ($post_id > 0) { $this->client->remember( $this->client->get_post_agent_id($post_id), "Article config: " . $config_summary, "preference", ["post:" . $post_id], "Post config", ); } // Store to user agent (cross-post preferences). $user_id = get_current_user_id(); if ($user_id > 0) { $this->client->remember( $this->client->get_user_agent_id($user_id), "User preference: " . $config_summary, "preference", ["user:" . $user_id], "Writing preferences", ); } } // ========================================================================= // Recall: Retrieve Memories for Context Enrichment // ========================================================================= /** * Recall relevant memories to enrich AI prompt context. * * @param int $post_id Post ID. * @param int $user_id WordPress user ID. * @param string $current_message User's current message (for semantic search). * @return array Recalled memory items, each with 'type', 'content', 'title'. */ public function recall_for_context( $post_id, $user_id, $current_message = "", ) { if (!$this->client->is_active()) { return []; } $memories = []; $seen = []; // 1. Recent post memories. if ($post_id > 0) { $post_agent = $this->client->get_post_agent_id($post_id); $recent = $this->client->recall_recent($post_agent, 10); foreach ($this->normalize_memories($recent) as $item) { $hash = md5($item["content"]); if (!isset($seen[$hash])) { $seen[$hash] = true; $memories[] = $item; } } // 2. Semantic recall based on current message. if (!empty($current_message)) { $semantic = $this->client->recall( $post_agent, $current_message, [], 5, ); foreach ($this->normalize_memories($semantic) as $item) { $hash = md5($item["content"]); if (!isset($seen[$hash])) { $seen[$hash] = true; $memories[] = $item; } } } } // 3. User preferences (cross-post). if ($user_id > 0) { $user_prefs = $this->client->recall( $this->client->get_user_agent_id($user_id), "writing preferences tone audience language", ["preference"], 5, ); foreach ($this->normalize_memories($user_prefs) as $item) { $hash = md5($item["content"]); if (!isset($seen[$hash])) { $seen[$hash] = true; $memories[] = $item; } } } return $memories; } /** * Restore session state from MEMANTO when reopening a post. * * Returns a structured restore payload that the frontend can use to: * - Display a "Restored from memory" badge * - Build a restored-session system message for the AI * - Pre-fill config from prior preferences * * @since 0.4.0 * @param int $post_id Post ID being reopened. * @param int $user_id Current user ID. * @return array { restored: bool, memories: array, preferences: array, summary: string } */ public function restore_session($post_id, $user_id) { $empty = [ "restored" => false, "memories" => [], "preferences" => [], "summary" => "", ]; if (!$this->client->is_active() || $post_id <= 0) { return $empty; } // Recall recent post memories (no current message — restore is about history). $post_agent = $this->client->get_post_agent_id($post_id); $recent = $this->client->recall_recent($post_agent, 15); $memories = $this->normalize_memories($recent); // Recall user preferences (for cross-post config carry-over). $preferences = []; if ($user_id > 0) { $user_prefs = $this->client->recall( $this->client->get_user_agent_id($user_id), "writing preferences tone audience language length", ["preference"], 5, ); $preferences = $this->normalize_memories($user_prefs); } if (empty($memories) && empty($preferences)) { return $empty; } // Build a human-readable summary for the restore badge tooltip. $summary = $this->build_restore_summary($memories, $preferences); return [ "restored" => true, "memories" => $memories, "preferences" => $preferences, "summary" => $summary, ]; } /** * Build a compact summary string of restored memories. * * @param array $memories Restored memories. * @param array $preferences Restored preferences. * @return string Summary text. */ private function build_restore_summary($memories, $preferences) { $parts = []; if (!empty($memories)) { $parts[] = sprintf( /* translators: %d: number of memories */ _n( "%d memory", "%d memories", count($memories), "wp-agentic-writer", ), count($memories), ); } if (!empty($preferences)) { $parts[] = sprintf( /* translators: %d: number of preferences */ _n( "%d preference", "%d preferences", count($preferences), "wp-agentic-writer", ), count($preferences), ); } return implode(", ", $parts); } /** * Get user's writing preferences recalled from MEMANTO. * * Used when creating a new post to pre-fill the post config * with the user's habitual tone, audience, etc. * * @since 0.4.0 * @param int $user_id WordPress user ID. * @return array { restored: bool, config: array } */ public function get_user_preferences_for_new_post($user_id) { $empty = ["restored" => false, "config" => []]; if (!$this->client->is_active() || $user_id <= 0) { return $empty; } $user_prefs = $this->client->recall( $this->client->get_user_agent_id($user_id), "writing preferences tone audience language length", ["preference"], 3, ); $prefs = $this->normalize_memories($user_prefs); if (empty($prefs)) { return $empty; } // Extract config fields from preference content. // Memory format: "User preference: tone=professional, audience=experts, length=long, language=en" $config = $this->extract_config_from_preferences($prefs); if (empty($config)) { return $empty; } return ["restored" => true, "config" => $config]; } /** * Parse preference memory contents and extract config fields. * * @param array $prefs Normalized preference memories. * @return array Extracted config: { tone, audience, article_length, language }. */ private function extract_config_from_preferences($prefs) { $config = []; // Combine all preference content into one blob for parsing. $blob = ""; foreach ($prefs as $pref) { $blob .= " " . ($pref["content"] ?? ""); } // Match key=value patterns. if (preg_match('/tone\s*=\s*([^,\n]+)/i', $blob, $m)) { $val = trim($m[1]); if ("" !== $val && "default" !== strtolower($val)) { $config["tone"] = $val; } } if (preg_match('/audience\s*=\s*([^,\n]+)/i', $blob, $m)) { $val = trim($m[1]); if ("" !== $val && "general" !== strtolower($val)) { $config["audience"] = $val; } } if (preg_match('/(?:article_)?length\s*=\s*([^,\n]+)/i', $blob, $m)) { $val = trim($m[1]); if ("" !== $val && "medium" !== strtolower($val)) { $config["article_length"] = $val; } } if (preg_match('/language\s*=\s*([^,\n]+)/i', $blob, $m)) { $val = trim($m[1]); if ("" !== $val && "auto" !== strtolower($val)) { $config["language"] = $val; } } return $config; } /** * Build a "restored session" system message for the AI. * * This message summarizes prior post work so the AI can resume * mid-article without re-asking the user for context. * * @since 0.4.0 * @param array $restore_payload Result from restore_session(). * @return string System message content (empty if nothing to restore). */ public function build_session_restore_message($restore_payload) { if (empty($restore_payload["restored"])) { return ""; } $memories = $restore_payload["memories"] ?? []; $preferences = $restore_payload["preferences"] ?? []; if (empty($memories) && empty($preferences)) { return ""; } $lines = ["SESSION RESTORED FROM MEMORY"]; $lines[] = "The user has returned to this post. Below is context from prior sessions."; $lines[] = "Use this to continue where the conversation left off without re-asking."; // Summarize prior post work. if (!empty($memories)) { $lines[] = ""; $lines[] = "## Prior session activity"; $grouped = []; foreach ($memories as $memory) { $type = $memory["type"] ?? "context"; if (!isset($grouped[$type])) { $grouped[$type] = []; } $grouped[$type][] = $memory; } // Render in priority order: plan > decision > instruction > artifact > error > context. $priority = [ "artifact", "decision", "instruction", "error", "learning", "context", "preference", ]; foreach ($priority as $type) { if (empty($grouped[$type])) { continue; } $type_label = ucfirst($type) . "s"; $lines[] = "### {$type_label}"; foreach ($grouped[$type] as $m) { $content = trim($m["content"] ?? ""); if ("" === $content) { continue; } $title = !empty($m["title"]) ? " [" . trim($m["title"]) . "]" : ""; $lines[] = "- {$content}{$title}"; } } } // Append user preferences (compact). if (!empty($preferences)) { $lines[] = ""; $lines[] = "## User writing preferences"; foreach ($preferences as $pref) { $content = trim($pref["content"] ?? ""); if ("" !== $content) { $lines[] = "- {$content}"; } } } return implode("\n", $lines); } /** * Normalize raw MEMANTO response into a uniform array. * * @param array $raw Raw response from recall/recall_recent. * @return array Normalized items: { type, content, title }. */ private function normalize_memories($raw) { if (!is_array($raw)) { return []; } // Handle different possible response shapes. $items = $raw; // If response has a 'memories' or 'results' key, unwrap. if (isset($raw["memories"]) && is_array($raw["memories"])) { $items = $raw["memories"]; } elseif (isset($raw["results"]) && is_array($raw["results"])) { $items = $raw["results"]; } $normalized = []; foreach ($items as $item) { if (!is_array($item)) { continue; } $normalized[] = [ "type" => $item["type"] ?? "context", "content" => $item["content"] ?? ($item["text"] ?? ""), "title" => $item["title"] ?? "", ]; } return $normalized; } }