get_context($session_id, $post_id) : []; $session_context = $saved_context["context"] ?? []; $messages = $saved_context["messages"] ?? []; if (empty($messages)) { $messages = $this->get_request_messages($request_params); } $token_policy = $this->get_token_policy($session_context); $recent_messages = $this->prepare_recent_messages( $messages, $token_policy["max_recent_messages"], ); $recent_messages = $this->remove_active_user_message( $recent_messages, $request_params["latestUserMessage"] ?? "", ); $post_config = $this->resolve_post_config( $saved_context, $request_params, ); $plan = $this->resolve_plan($saved_context, $request_params, $post_id); $working_context = $this->build_working_context( $task, $session_context, $recent_messages, $plan, $post_config, $request_params, $post_id, ); // Check MEMANTO status for audit (lightweight: just checks is_active, no recall). $memanto_active = WP_Agentic_Writer_Memanto_Client::get_instance()->is_active(); return [ "system_context" => "", "working_context" => $working_context, "active_content" => $this->get_active_content($request_params), "research_context" => $this->build_research_context( $session_context, $request_params, $token_policy["max_research_snippets"], ), "audit" => [ "included_recent_messages" => count($recent_messages), "included_research_items" => $this->count_research_items( $session_context, $request_params, $token_policy["max_research_snippets"], ), "estimated_input_tokens" => $this->estimate_tokens( $working_context, ), "used_full_history" => false, "memanto_active" => $memanto_active, ], ]; } /** * Build a system message that can be inserted after the primary system prompt. * * @param string $task Task name. * @param string $session_id Session ID. * @param int $post_id Post ID. * @param array $request_params Request params. * @return array Context system message and audit metadata. */ public function build_system_message( $task, $session_id, $post_id, $request_params = [], ) { $package = $this->build_for_task( $task, $session_id, $post_id, $request_params, ); $content = trim( $package["working_context"] . "\n" . $package["active_content"] . "\n" . $package["research_context"], ); if ("" === $content) { return [ "message" => null, "audit" => $package["audit"], ]; } return [ "message" => [ "role" => "system", "content" => $content, ], "audit" => $package["audit"], ]; } /** * Get task token policy. * * @param array $session_context Session context. * @return array Token policy. */ private function get_token_policy($session_context) { $policy = isset($session_context["token_policy"]) && is_array($session_context["token_policy"]) ? $session_context["token_policy"] : []; return [ "max_recent_messages" => max( 2, (int) ($policy["max_recent_messages"] ?? 6), ), "max_summary_tokens" => max( 200, (int) ($policy["max_summary_tokens"] ?? 600), ), "max_research_snippets" => max( 0, (int) ($policy["max_research_snippets"] ?? 5), ), ]; } /** * Get messages from request fallback. * * @param array $request_params Request params. * @return array Messages. */ private function get_request_messages($request_params) { if ( !empty($request_params["messages"]) && is_array($request_params["messages"]) ) { return $request_params["messages"]; } if ( !empty($request_params["chatHistory"]) && is_array($request_params["chatHistory"]) ) { return $request_params["chatHistory"]; } return []; } /** * Prepare compact recent messages. * * @param array $messages Messages. * @param int $max_messages Max messages. * @return array Recent messages. */ private function prepare_recent_messages($messages, $max_messages) { $prepared = []; foreach ((array) $messages as $message) { $role = isset($message["role"]) ? (string) $message["role"] : ""; if (!in_array($role, ["user", "assistant"], true)) { continue; } $content = isset($message["content"]) ? trim(wp_strip_all_tags((string) $message["content"])) : ""; if ("" === $content) { continue; } $prepared[] = [ "role" => $role, "content" => $this->truncate_text($content, 900), ]; } if (count($prepared) > $max_messages) { $prepared = array_slice($prepared, -1 * $max_messages); } return $prepared; } /** * Avoid echoing the active user turn inside the saved-context excerpt. * * @param array $messages Recent messages. * @param string $active_user_message Active user message. * @return array Messages without duplicate active turn. */ private function remove_active_user_message($messages, $active_user_message) { $active_user_message = trim( wp_strip_all_tags((string) $active_user_message), ); if ("" === $active_user_message || empty($messages)) { return $messages; } for ($i = count($messages) - 1; $i >= 0; $i--) { if ("user" !== ($messages[$i]["role"] ?? "")) { continue; } $content = trim((string) ($messages[$i]["content"] ?? "")); if ($content === $active_user_message) { array_splice($messages, $i, 1); } break; } return $messages; } /** * Resolve post config. * * @param array $saved_context Saved context package. * @param array $request_params Request params. * @return array Post config. */ private function resolve_post_config($saved_context, $request_params) { $config = $saved_context["post_config"] ?? []; if ( !empty($request_params["postConfig"]) && is_array($request_params["postConfig"]) ) { $config = wp_parse_args($request_params["postConfig"], $config); } return is_array($config) ? $config : []; } /** * Resolve current plan. * * @param array $saved_context Saved context package. * @param array $request_params Request params. * @param int $post_id Post ID. * @return array|null Plan. */ private function resolve_plan($saved_context, $request_params, $post_id) { if ( !empty($saved_context["plan"]) && is_array($saved_context["plan"]) ) { return $saved_context["plan"]; } if ( !empty($request_params["plan"]) && is_array($request_params["plan"]) ) { return $request_params["plan"]; } if ($post_id > 0) { $plan = get_post_meta($post_id, "_wpaw_plan", true); if (is_array($plan)) { return $plan; } } return null; } /** * Build compact working context. * * @param string $task Task name. * @param array $session_context Session context. * @param array $recent_messages Recent messages. * @param array $plan Current plan. * @param array $post_config Post config. * @param array $request_params Request params. * @param int $post_id Post ID. * @return string Context text. */ private function build_working_context( $task, $session_context, $recent_messages, $plan, $post_config, $request_params, $post_id = 0, ) { $sections = []; $sections[] = "BACKEND CONTINUITY CONTEXT\nUse this compact WordPress-saved context as continuity. Do not assume OpenRouter remembers prior turns."; $sections[] = "Current task: " . sanitize_key($task); // MEMANTO persistent memory injection. $memanto_context = $this->build_memanto_context( $post_id, $request_params["latestUserMessage"] ?? "", ); if ("" !== $memanto_context) { $sections[] = $memanto_context; } $summary = $session_context["working_summary"]["text"] ?? ""; if ("" !== trim((string) $summary)) { $sections[] = "Working summary:\n" . $this->truncate_text((string) $summary, 1600); } $config_summary = $this->summarize_post_config($post_config); if ("" !== $config_summary) { $sections[] = "Article configuration:\n" . $config_summary; } $plan_summary = $this->summarize_plan($plan); if ("" !== $plan_summary) { $sections[] = "Current plan:\n" . $plan_summary; } $decision_summary = $this->summarize_context_items( $session_context, "decisions", "Decisions", ); if ("" !== $decision_summary) { $sections[] = $decision_summary; } $rejection_summary = $this->summarize_context_items( $session_context, "rejections", "Rejected directions", ); if ("" !== $rejection_summary) { $sections[] = $rejection_summary; } if (!empty($request_params["context"])) { $sections[] = "User supplied context:\n" . $this->truncate_text((string) $request_params["context"], 1600); } if (!empty($recent_messages)) { $lines = []; foreach ($recent_messages as $message) { $lines[] = ucfirst($message["role"]) . ": " . $message["content"]; } $sections[] = "Recent saved conversation excerpts:\n" . implode("\n", $lines); } return implode("\n\n", array_filter($sections)); } /** * Summarize post config. * * @param array $post_config Post config. * @return string Summary. */ private function summarize_post_config($post_config) { $lines = []; $keys = [ "article_length" => "Article length", "language" => "Language", "tone" => "Tone", "audience" => "Audience", "experience_level" => "Experience level", "seo_focus_keyword" => "SEO focus keyword", "seo_secondary_keywords" => "SEO secondary keywords", ]; foreach ($keys as $key => $label) { if ( isset($post_config[$key]) && "" !== trim((string) $post_config[$key]) ) { $lines[] = "- " . $label . ": " . trim((string) $post_config[$key]); } } if (isset($post_config["include_images"])) { $lines[] = "- Include images: " . ($post_config["include_images"] ? "yes" : "no"); } if (isset($post_config["web_search"])) { $lines[] = "- Web search: " . ($post_config["web_search"] ? "yes" : "no"); } return implode("\n", $lines); } /** * Summarize current plan. * * @param array|null $plan Plan. * @return string Summary. */ private function summarize_plan($plan) { if (empty($plan) || !is_array($plan)) { return ""; } $lines = []; if (!empty($plan["title"])) { $lines[] = "Title: " . $plan["title"]; } if (!empty($plan["sections"]) && is_array($plan["sections"])) { foreach ($plan["sections"] as $index => $section) { $heading = $section["heading"] ?? ($section["title"] ?? ""); if ("" === trim((string) $heading)) { continue; } $status = $section["status"] ?? "pending"; $lines[] = sprintf( "%d. [%s] %s", $index + 1, $status, $heading, ); } } return implode("\n", $lines); } /** * Summarize context array items. * * @param array $session_context Session context. * @param string $key Context key. * @param string $label Label. * @return string Summary. */ private function summarize_context_items($session_context, $key, $label) { if ( empty($session_context[$key]) || !is_array($session_context[$key]) ) { return ""; } $lines = []; foreach (array_slice($session_context[$key], -8) as $item) { $summary = $item["summary"] ?? ""; if ("" === trim((string) $summary)) { continue; } $target = !empty($item["target"]) ? "[" . $item["target"] . "] " : ""; $lines[] = "- " . $target . $summary; } return empty($lines) ? "" : $label . ":\n" . implode("\n", $lines); } /** * Get active content from request params. * * @param array $request_params Request params. * @return string Active content context. */ private function get_active_content($request_params) { $candidates = [ "activeContent", "blockContent", "selectedText", "sectionContent", "articleContent", ]; $lines = []; foreach ($candidates as $key) { if ( !empty($request_params[$key]) && is_string($request_params[$key]) ) { $lines[] = $key . ":\n" . $this->truncate_text($request_params[$key], 2200); } } return empty($lines) ? "" : "ACTIVE CONTENT SLICE\n" . implode("\n\n", $lines); } /** * Build research context. * * @param array $session_context Session context. * @param array $request_params Request params. * @param int $limit Max snippets. * @return string Research context. */ private function build_research_context( $session_context, $request_params, $limit, ) { if ($limit <= 0) { return ""; } $items = []; if ( !empty($session_context["research_notes"]) && is_array($session_context["research_notes"]) ) { $items = array_merge($items, $session_context["research_notes"]); } if ( !empty($request_params["researchNotes"]) && is_array($request_params["researchNotes"]) ) { $items = array_merge($items, $request_params["researchNotes"]); } $items = array_slice($items, -1 * $limit); $lines = []; foreach ($items as $item) { if (!is_array($item)) { continue; } $title = $item["title"] ?? ($item["source"] ?? "Research note"); $excerpt = $item["excerpt"] ?? ($item["notes"] ?? ""); if ("" === trim((string) $excerpt)) { continue; } $lines[] = "- " . $title . ": " . $this->truncate_text((string) $excerpt, 700); } return empty($lines) ? "" : "RELEVANT RESEARCH\n" . implode("\n", $lines); } /** * Count included research items. * * @param array $session_context Session context. * @param array $request_params Request params. * @param int $limit Max snippets. * @return int Count. */ private function count_research_items( $session_context, $request_params, $limit, ) { if ($limit <= 0) { return 0; } $count = 0; if ( !empty($session_context["research_notes"]) && is_array($session_context["research_notes"]) ) { $count += count($session_context["research_notes"]); } if ( !empty($request_params["researchNotes"]) && is_array($request_params["researchNotes"]) ) { $count += count($request_params["researchNotes"]); } return min($limit, $count); } /** * Build MEMANTO persistent memory context section. * * Calls recall_for_context() via the MEMANTO Context Enhancer * and formats the returned memories into a prompt-ready section. * Returns empty string when MEMANTO is inactive or no memories found. * * @param int $post_id Post ID. * @param string $current_message User's current message for semantic search. * @return string Formatted memory section or empty string. */ private function build_memanto_context($post_id, $current_message = "") { // Guard: skip entirely if MEMANTO is not active. $memanto = WP_Agentic_Writer_Memanto_Context_Enhancer::get_instance(); $memories = $memanto->recall_for_context( $post_id, get_current_user_id(), $current_message, ); if (empty($memories) || !is_array($memories)) { return ""; } $lines = []; foreach ($memories as $memory) { $type = ucfirst($memory["type"] ?? "memory"); $content = trim((string) ($memory["content"] ?? "")); if ("" === $content) { continue; } $title = !empty($memory["title"]) ? " [" . trim($memory["title"]) . "]" : ""; $lines[] = "- ({$type}){$title} {$content}"; } if (empty($lines)) { return ""; } return "PERSISTENT MEMORY (from MEMANTO)\n" . "The following are memories recalled from prior sessions and interactions. " . "Use them to maintain continuity, respect user preferences, and avoid repeating past mistakes.\n" . implode("\n", $lines); } /** * Truncate text safely. * * @param string $text Text. * @param int $limit Character limit. * @return string Truncated text. */ private function truncate_text($text, $limit) { $text = trim((string) $text); if (strlen($text) <= $limit) { return $text; } return substr($text, 0, $limit) . "..."; } /** * Estimate tokens from character length. * * @param string $text Text. * @return int Estimated tokens. */ private function estimate_tokens($text) { return (int) ceil(strlen((string) $text) / 4); } }