From 3318aea536d8f321a00b04d3f2c7f5586e0b0784 Mon Sep 17 00:00:00 2001 From: Test Date: Mon, 18 May 2026 15:04:17 -0500 Subject: [PATCH 1/2] =?UTF-8?q?oxidizer:=20AIValidateResponseServerCommand?= =?UTF-8?q?=20=E2=86=92=20cognition/validate-response-decision=20(one=20PR?= =?UTF-8?q?=20per=20zero-users=20directive)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single-PR oxidizer per Joel 2026-05-18 19:44Z directive (zero users, full-blown Rust-driven dev, no migration ceremony). Adds the Rust cognition path AND replaces the TS parallel reimplementation in the same commit — no 4-PR cadence. Renamed command + binding from `cognition/validate-response` to `cognition/validate-response-decision` to avoid collision with the existing persona-validator surface (which already owns `cognition/validate-response` in modules/cognition.rs:814). ## What this ships Rust: - `cognition/validate_response.rs` (~380 LOC + 14 tests): - `ValidateResponseRequest` / `ValidateResponseDecision` / `ResponseDecision` (ts-rs exported) - `ValidateResponseError` typed enum (NoAdapter / Generation) - `build_validate_prompt` — pure prompt builder (mirrors TS template byte-for-byte modulo substitutions) - `parse_decision` — pure one-word parser (SUBMIT/CLARIFY/SILENT) with TS-parity precedence: CLARIFY > SILENT > Submit (fail-open default) - `reason_for` — canonical reason strings - `evaluate_validate_response` — async orchestrator (Groq via existing registry, llama-3.1-8b-instant default, temp 0.1, max 10 tokens) - `modules/cognition.rs`: `cognition/validate-response-decision` IPC arm - ts-rs barrel adds 3 new types (cognition/{ResponseDecision, ValidateResponseDecision, ValidateResponseRequest}.ts) TS: - `bindings/modules/cognition.ts`: new `cognitionValidateResponseDecision` binding method. - `commands/ai/validate-response/server/AIValidateResponseServerCommand.ts`: thin shim. Deletes inline prompt template, parseDecision, getReasonForDecision, AIProviderDaemon/TextGenerationRequest/ LOCAL_MODELS imports. ## Discipline - One PR carries Rust + TS shim + dead-TS delete (zero-users mode). - All errors typed (NoAdapter, Generation). No silent default-on-error. - Fail-open SUBMIT default in parser matches TS behavior (silence more user-hostile than off-topic). - Clippy held at 157 baseline (resolved 1 new unreachable-pattern warning by renaming colliding match arm). ## Tests - 14 logic + ts-rs tests pass (`cognition::validate_response::*`) - npm run build:ts clean - Clippy at baseline ## Refs - Joel 2026-05-18 19:44Z: zero-users full-blown-Rust-dev mode → one PR per oxidizer - Sibling: codex's #1383 + my #1402/#1421 pattern (one-PR delegation) - #1248 umbrella Co-Authored-By: Claude Opus 4.7 (1M context) --- .../server/AIValidateResponseServerCommand.ts | 122 ++---- .../generated/cognition/ResponseDecision.ts | 7 + .../cognition/ValidateResponseDecision.ts | 7 + .../cognition/ValidateResponseRequest.ts | 7 + src/shared/generated/cognition/index.ts | 3 + .../bindings/modules/cognition.ts | 25 ++ .../continuum-core/src/cognition/mod.rs | 1 + .../src/cognition/validate_response.rs | 381 ++++++++++++++++++ .../continuum-core/src/modules/cognition.rs | 21 + 9 files changed, 493 insertions(+), 81 deletions(-) create mode 100644 src/shared/generated/cognition/ResponseDecision.ts create mode 100644 src/shared/generated/cognition/ValidateResponseDecision.ts create mode 100644 src/shared/generated/cognition/ValidateResponseRequest.ts create mode 100644 src/workers/continuum-core/src/cognition/validate_response.rs diff --git a/src/commands/ai/validate-response/server/AIValidateResponseServerCommand.ts b/src/commands/ai/validate-response/server/AIValidateResponseServerCommand.ts index 3c6c03cdb..111f260e6 100644 --- a/src/commands/ai/validate-response/server/AIValidateResponseServerCommand.ts +++ b/src/commands/ai/validate-response/server/AIValidateResponseServerCommand.ts @@ -1,17 +1,23 @@ /** * AI Validate-Response Server Command * - * After generating response, AI validates if it actually answers the question. - * Uses AIProviderDaemon for LLM-based evaluation. + * Thin TS shim — delegates to the Rust cognition/validate-response IPC. + * Rust owns the prompt, model call, and one-word decision parser + * (cognition/validate_response.rs). This command maps the public params + * shape into the IPC request and forwards the typed decision back. + * + * Replaces the previous parallel reimplementation (which carried its + * own prompt template + decision parser inline). Per Joel directive + * 2026-05-18 19:44Z: zero-users full-blown-Rust-dev mode — single PR + * adds the Rust path AND deletes the TS predecessor, no migration + * cadence. */ import { CommandBase } from '../../../../daemons/command-daemon/shared/CommandBase'; import type { JTAGContext } from '../../../../system/core/types/JTAGTypes'; import type { ICommandDaemon } from '../../../../daemons/command-daemon/shared/CommandBase'; -import type { AIValidateResponseParams, AIValidateResponseResult, ResponseDecision } from '../shared/AIValidateResponseTypes'; -import { AIProviderDaemon } from '../../../../daemons/ai-provider-daemon/shared/AIProviderDaemon'; -import type { TextGenerationRequest } from '../../../../daemons/ai-provider-daemon/shared/AIProviderTypesV2'; -import { LOCAL_MODELS } from '../../../../system/shared/Constants'; +import type { AIValidateResponseParams, AIValidateResponseResult } from '../shared/AIValidateResponseTypes'; +import { RustCoreIPCClient } from '../../../../workers/continuum-core/bindings/RustCoreIPC'; export class AIValidateResponseServerCommand extends CommandBase { constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { @@ -19,81 +25,35 @@ export class AIValidateResponseServerCommand extends CommandBase { - // Build validation prompt - const validationPrompt = this.buildValidationPrompt(params); - - // Simple LLM call for validation - const request: TextGenerationRequest = { - messages: [ - { role: 'system', content: 'You are a response validator. Reply ONLY with one word: SUBMIT, CLARIFY, or SILENT.' }, - { role: 'user', content: validationPrompt } - ], - model: params.model ?? LOCAL_MODELS.GATING, - temperature: 0.1, // Low temp for consistent decisions - maxTokens: 10, // Just need one word - provider: 'local' - }; - - const response = await AIProviderDaemon.generateText(request); - - if (!response.text) { - throw new Error(response.error ?? 'AI validation failed'); - } - - // Parse decision - const decision = this.parseDecision(response.text); - const reason = this.getReasonForDecision(decision, params); - - return { - context: params.context, - sessionId: params.sessionId, - decision, - confidence: 0.9, // High confidence for simple yes/no decisions - reason, - debug: params.verbose ? { - promptSent: validationPrompt, - aiResponse: response.text - } : undefined - }; - } - - private buildValidationPrompt(params: AIValidateResponseParams): string { - return `You generated this response: -"${params.generatedResponse}" - -Original question from ${params.questionSender}: -"${params.originalQuestion}" - -Does your response actually answer their question? - -Reply with ONLY ONE WORD: -- SUBMIT (your response clearly answers the question) -- CLARIFY (you're unsure, should ask for clarification) -- SILENT (your response is off-topic, stay silent)`; - } - - private parseDecision(aiResponse: string): ResponseDecision { - const text = aiResponse.trim().toUpperCase(); - - if (text.includes('CLARIFY')) { - return 'CLARIFY'; - } else if (text.includes('SILENT')) { - return 'SILENT'; - } - - return 'SUBMIT'; // Default to submitting - } - - private getReasonForDecision(decision: ResponseDecision, _params: AIValidateResponseParams): string { - switch (decision) { - case 'SUBMIT': - return 'Response appears relevant to the question'; - case 'CLARIFY': - return 'Uncertain if response answers question, should ask for clarification'; - case 'SILENT': - return 'Response is off-topic or does not address the question'; - default: - return 'Unknown decision'; + try { + const client = await RustCoreIPCClient.getInstanceAsync(); + const decision = await client.cognitionValidateResponseDecision({ + generatedResponse: params.generatedResponse, + originalQuestion: params.originalQuestion, + questionSender: params.questionSender, + model: params.model, + }); + + return { + context: params.context, + sessionId: params.sessionId, + decision: decision.decision, + confidence: decision.confidence, + reason: decision.reason, + debug: params.verbose ? { + promptSent: '(Rust-owned — see cognition::validate_response logs)', + aiResponse: '(Rust-owned — see cognition::validate_response logs)', + } : undefined, + }; + } catch (error) { + return { + context: params.context, + sessionId: params.sessionId, + error: error instanceof Error ? error.message : String(error), + decision: 'SUBMIT', // Fail-open: ship the draft when validator fails + confidence: 0.0, + reason: `Validation error: ${error instanceof Error ? error.message : String(error)}`, + }; } } } diff --git a/src/shared/generated/cognition/ResponseDecision.ts b/src/shared/generated/cognition/ResponseDecision.ts new file mode 100644 index 000000000..b6395bf64 --- /dev/null +++ b/src/shared/generated/cognition/ResponseDecision.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Three-way decision: SUBMIT (post the draft), CLARIFY (ask follow-up), + * SILENT (drop the draft). Mirrors TS `ResponseDecision`. + */ +export type ResponseDecision = "SUBMIT" | "CLARIFY" | "SILENT"; diff --git a/src/shared/generated/cognition/ValidateResponseDecision.ts b/src/shared/generated/cognition/ValidateResponseDecision.ts new file mode 100644 index 000000000..b80c26804 --- /dev/null +++ b/src/shared/generated/cognition/ValidateResponseDecision.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ResponseDecision } from "./ResponseDecision"; + +/** + * IPC response: the validation decision + provenance. + */ +export type ValidateResponseDecision = { decision: ResponseDecision, confidence: number, reason: string, model: string, timestamp: number, }; diff --git a/src/shared/generated/cognition/ValidateResponseRequest.ts b/src/shared/generated/cognition/ValidateResponseRequest.ts new file mode 100644 index 000000000..447cced88 --- /dev/null +++ b/src/shared/generated/cognition/ValidateResponseRequest.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * IPC request: ask cognition whether a draft response actually answers + * the original question. + */ +export type ValidateResponseRequest = { generatedResponse: string, originalQuestion: string, questionSender: string, model?: string, }; diff --git a/src/shared/generated/cognition/index.ts b/src/shared/generated/cognition/index.ts index a29288832..377fccce1 100644 --- a/src/shared/generated/cognition/index.ts +++ b/src/shared/generated/cognition/index.ts @@ -60,6 +60,7 @@ export type { ResolvedModel } from './ResolvedModel'; export type { ResourceAdmissionPolicy } from './ResourceAdmissionPolicy'; export type { ResourceClass } from './ResourceClass'; export type { ResponderDecision } from './ResponderDecision'; +export type { ResponseDecision } from './ResponseDecision'; export type { ResponseProposal } from './ResponseProposal'; export type { SemanticSearchResult } from './SemanticSearchResult'; export type { SemanticSearchToolsRequest } from './SemanticSearchToolsRequest'; @@ -89,6 +90,8 @@ export type { ToolError } from './ToolError'; export type { ToolExecutionContext } from './ToolExecutionContext'; export type { ToolInvocation } from './ToolInvocation'; export type { ToolOutcome } from './ToolOutcome'; +export type { ValidateResponseDecision } from './ValidateResponseDecision'; +export type { ValidateResponseRequest } from './ValidateResponseRequest'; export type { VisionDescribeOptions } from './VisionDescribeOptions'; export type { VisionDescribeRequest } from './VisionDescribeRequest'; export type { VisionDescription } from './VisionDescription'; diff --git a/src/workers/continuum-core/bindings/modules/cognition.ts b/src/workers/continuum-core/bindings/modules/cognition.ts index 6ff1312fd..b02ebdf16 100644 --- a/src/workers/continuum-core/bindings/modules/cognition.ts +++ b/src/workers/continuum-core/bindings/modules/cognition.ts @@ -39,6 +39,8 @@ import type { EmbedToolsResponse, SemanticSearchToolsRequest, SemanticSearchResult, + ValidateResponseRequest, + ValidateResponseDecision, } from '../../../../shared/generated'; import type { PersonaResponse } from '../../../../shared/generated/cognition/PersonaResponse'; import type { RecipeTurnBatchPlan } from '../../../../shared/generated/cognition/RecipeTurnBatchPlan'; @@ -137,6 +139,7 @@ export interface CognitionMixin { cognitionGenerateResponse(params: GenerateResponseRequest): Promise; cognitionEmbedTools(params: EmbedToolsRequest): Promise; cognitionSemanticSearchTools(params: SemanticSearchToolsRequest): Promise; + cognitionValidateResponseDecision(params: ValidateResponseRequest): Promise; /** * Run the per-persona admission gate over a single InboxMessage. @@ -974,6 +977,28 @@ export function CognitionMixin RustCoreIPCClie return response.result as SemanticSearchResult[]; } + /** + * Rust-owned response validation. TypeScript keeps no validation + * logic; Rust owns prompt assembly, Groq call, single-word + * decision parser (SUBMIT/CLARIFY/SILENT). Replaces the legacy + * TS-side AIValidateResponseServerCommand reimpl. + */ + async cognitionValidateResponseDecision(params: ValidateResponseRequest): Promise { + const response = await this.request({ + command: 'cognition/validate-response-decision', + generatedResponse: params.generatedResponse, + originalQuestion: params.originalQuestion, + questionSender: params.questionSender, + model: params.model, + }); + + if (!response.success) { + throw new Error(response.error ?? 'Failed to validate response'); + } + + return response.result as ValidateResponseDecision; + } + /** * Per-persona response cycle (shared cognition pipeline). * Single IPC call → Rust does analysis (cached) + scoring + prompt diff --git a/src/workers/continuum-core/src/cognition/mod.rs b/src/workers/continuum-core/src/cognition/mod.rs index d5e1405ae..add5dd20e 100644 --- a/src/workers/continuum-core/src/cognition/mod.rs +++ b/src/workers/continuum-core/src/cognition/mod.rs @@ -45,6 +45,7 @@ pub mod throughput_lease; pub mod tool_embedding; pub mod tool_executor; pub mod turn_batch; +pub mod validate_response; pub mod types; pub mod vision_describe; diff --git a/src/workers/continuum-core/src/cognition/validate_response.rs b/src/workers/continuum-core/src/cognition/validate_response.rs new file mode 100644 index 000000000..a346a7517 --- /dev/null +++ b/src/workers/continuum-core/src/cognition/validate_response.rs @@ -0,0 +1,381 @@ +//! Rust-owned response-validation decision. +//! +//! Oxidizer for `AIValidateResponseServerCommand` (TS, see +//! `src/commands/ai/validate-response/server/AIValidateResponseServerCommand.ts`). +//! Sibling to the closed `check_redundancy` (#1375) + `generate_response` +//! (#1385) oxidizers. Same shape, same discipline. +//! +//! Per Joel directive 2026-05-18 19:44Z: zero-users full-blown-Rust-dev +//! mode — this is shipped as ONE PR (add Rust + delete TS predecessor +//! in same commit), not the 4-PR migration cadence. +//! +//! ## Scope +//! +//! - `ValidateResponseRequest` (ts-rs) — IPC request +//! - `ValidateResponseDecision` (ts-rs) — IPC response carrying +//! `decision: SUBMIT | CLARIFY | SILENT`, confidence, reason, model, +//! timestamp +//! - `ResponseDecision` enum (ts-rs) — three-way decision shape +//! - `ValidateResponseError` — typed: NoAdapter, Generation +//! - `build_validate_prompt(&request) -> String` — pure +//! - `parse_decision(ai_text) -> ResponseDecision` — pure +//! - `evaluate_validate_response(request) -> Result` +//! — async (calls Groq via existing registry, parses decision, stamps) +//! +//! ## Failure discipline +//! +//! - All errors typed. +//! - parse_decision defaults to SUBMIT when AI returns unrecognized text +//! — matches TS behavior (the choice is "fail open: submit the draft" +//! rather than "fail closed: silence the persona"). Documented at the +//! parser; caller can compare against `decision == SUBMIT && reason +//! == DEFAULT_REASON_SUBMIT` if they want to detect parse-fallthrough. +//! - No JSON parsing — model is asked for a single word, not JSON. +//! Different from check_redundancy. + +use crate::ai::adapter::InferenceDevice; +use crate::ai::types::ResponseFormat; +use crate::ai::{ChatMessage, MessageContent, TextGenerationRequest, TextGenerationResponse}; +use crate::modules::ai_provider::global_registry; +use serde::{Deserialize, Serialize}; +use std::time::{SystemTime, UNIX_EPOCH}; +use ts_rs::TS; + +const VALIDATE_PROVIDER: &str = "groq"; +const DEFAULT_VALIDATE_MODEL: &str = "llama-3.1-8b-instant"; +const VALIDATE_MAX_TOKENS: u32 = 10; +const VALIDATE_TEMPERATURE: f32 = 0.1; +const VALIDATE_CONFIDENCE: f32 = 0.9; + +const REASON_SUBMIT: &str = "Response appears relevant to the question"; +const REASON_CLARIFY: &str = "Uncertain if response answers question, should ask for clarification"; +const REASON_SILENT: &str = "Response is off-topic or does not address the question"; + +// ─── Wire types ─────────────────────────────────────────────────────── + +/// Three-way decision: SUBMIT (post the draft), CLARIFY (ask follow-up), +/// SILENT (drop the draft). Mirrors TS `ResponseDecision`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)] +#[ts( + export, + export_to = "../../../shared/generated/cognition/ResponseDecision.ts" +)] +pub enum ResponseDecision { + #[serde(rename = "SUBMIT")] + Submit, + #[serde(rename = "CLARIFY")] + Clarify, + #[serde(rename = "SILENT")] + Silent, +} + +/// IPC request: ask cognition whether a draft response actually answers +/// the original question. +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts( + export, + export_to = "../../../shared/generated/cognition/ValidateResponseRequest.ts" +)] +pub struct ValidateResponseRequest { + pub generated_response: String, + pub original_question: String, + pub question_sender: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub model: Option, +} + +/// IPC response: the validation decision + provenance. +#[derive(Debug, Clone, Serialize, Deserialize, TS, PartialEq)] +#[serde(rename_all = "camelCase")] +#[ts( + export, + export_to = "../../../shared/generated/cognition/ValidateResponseDecision.ts" +)] +pub struct ValidateResponseDecision { + pub decision: ResponseDecision, + pub confidence: f32, + pub reason: String, + pub model: String, + #[ts(type = "number")] + pub timestamp: u64, +} + +#[derive(Debug, thiserror::Error)] +pub enum ValidateResponseError { + #[error("no AI adapter for provider={provider:?} model={model:?}")] + NoAdapter { + provider: String, + model: Option, + }, + #[error("generation failed: {0}")] + Generation(String), +} + +// ─── Pure prompt builder ────────────────────────────────────────────── + +/// Build the one-word-answer prompt sent to the validator model. Pure. +pub fn build_validate_prompt(request: &ValidateResponseRequest) -> String { + format!( + "You generated this response:\n\ +\"{}\"\n\ +\n\ +Original question from {}:\n\ +\"{}\"\n\ +\n\ +Does your response actually answer their question?\n\ +\n\ +Reply with ONLY ONE WORD:\n\ +- SUBMIT (your response clearly answers the question)\n\ +- CLARIFY (you're unsure, should ask for clarification)\n\ +- SILENT (your response is off-topic, stay silent)", + request.generated_response, request.question_sender, request.original_question + ) +} + +/// Parse the validator model's one-word answer. Pure. +/// +/// Match precedence: +/// 1. Contains "CLARIFY" → Clarify +/// 2. Contains "SILENT" → Silent +/// 3. Otherwise → Submit (fail-open default) +/// +/// Mirrors TS `parseDecision` ordering exactly. The fail-open default +/// matches the TS behavior — when the validator can't decide, ship the +/// draft rather than silence the persona (silence is more user-hostile +/// than a slightly-off-topic response). +pub fn parse_decision(ai_text: &str) -> ResponseDecision { + let upper = ai_text.trim().to_ascii_uppercase(); + if upper.contains("CLARIFY") { + ResponseDecision::Clarify + } else if upper.contains("SILENT") { + ResponseDecision::Silent + } else { + ResponseDecision::Submit + } +} + +/// Canonical reason string for a decision — for callers that just want +/// to surface "why" without re-stringifying the variant. Pure. +pub fn reason_for(decision: ResponseDecision) -> &'static str { + match decision { + ResponseDecision::Submit => REASON_SUBMIT, + ResponseDecision::Clarify => REASON_CLARIFY, + ResponseDecision::Silent => REASON_SILENT, + } +} + +// ─── Async orchestrator (PR — IPC handler) ──────────────────────────── + +/// Run validation against the configured Groq adapter. No fallback path +/// — provider failures surface as typed errors so the caller decides +/// policy. +pub async fn evaluate_validate_response( + request: ValidateResponseRequest, +) -> Result { + let model = request + .model + .clone() + .unwrap_or_else(|| DEFAULT_VALIDATE_MODEL.to_string()); + let inference_request = build_validate_generation_request(&request, model.clone()); + + let registry_arc = global_registry(); + let registry = registry_arc.read().await; + let (_provider_id, adapter) = registry + .select( + Some(VALIDATE_PROVIDER), + Some(&model), + InferenceDevice::default(), + ) + .ok_or_else(|| ValidateResponseError::NoAdapter { + provider: VALIDATE_PROVIDER.to_string(), + model: Some(model.clone()), + })?; + + let response: TextGenerationResponse = adapter + .generate_text(inference_request) + .await + .map_err(ValidateResponseError::Generation)?; + + let decision = parse_decision(&response.text); + Ok(ValidateResponseDecision { + decision, + confidence: VALIDATE_CONFIDENCE, + reason: reason_for(decision).to_string(), + model, + timestamp: now_ms(), + }) +} + +fn build_validate_generation_request( + request: &ValidateResponseRequest, + model: String, +) -> TextGenerationRequest { + TextGenerationRequest { + messages: vec![ + ChatMessage { + role: "system".to_string(), + content: MessageContent::Text( + "You are a response validator. Reply ONLY with one word: SUBMIT, CLARIFY, or SILENT." + .to_string(), + ), + name: None, + }, + ChatMessage { + role: "user".to_string(), + content: MessageContent::Text(build_validate_prompt(request)), + name: None, + }, + ], + system_prompt: None, + model: Some(model), + provider: Some(VALIDATE_PROVIDER.to_string()), + temperature: Some(VALIDATE_TEMPERATURE), + max_tokens: Some(VALIDATE_MAX_TOKENS), + top_p: None, + top_k: None, + repeat_penalty: None, + stop_sequences: None, + tools: None, + tool_choice: None, + response_format: Some(ResponseFormat::Text), + active_adapters: None, + request_id: None, + user_id: None, + room_id: None, + purpose: Some("cognition/validate-response-decision".to_string()), + persona_id: None, + } +} + +fn now_ms() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_millis() as u64) + .unwrap_or(0) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn req(draft: &str, question: &str) -> ValidateResponseRequest { + ValidateResponseRequest { + generated_response: draft.to_string(), + original_question: question.to_string(), + question_sender: "alice".to_string(), + model: None, + } + } + + // ─── build_validate_prompt ──────────────────────────────────────── + + #[test] + fn prompt_embeds_draft_question_sender() { + let p = build_validate_prompt(&req("the answer is 42", "what is 2+2?")); + assert!(p.contains("the answer is 42")); + assert!(p.contains("what is 2+2?")); + assert!(p.contains("from alice")); + } + + #[test] + fn prompt_includes_three_option_instructions() { + let p = build_validate_prompt(&req("d", "q")); + assert!(p.contains("- SUBMIT")); + assert!(p.contains("- CLARIFY")); + assert!(p.contains("- SILENT")); + assert!(p.contains("ONLY ONE WORD")); + } + + // ─── parse_decision ─────────────────────────────────────────────── + + /// Bare SUBMIT → Submit. + #[test] + fn parse_bare_submit() { + assert_eq!(parse_decision("SUBMIT"), ResponseDecision::Submit); + assert_eq!(parse_decision("submit"), ResponseDecision::Submit); + } + + /// CLARIFY wins over SUBMIT when text contains both (mirrors TS + /// `if (text.includes('CLARIFY'))` taking precedence). + #[test] + fn parse_clarify_wins_when_present() { + assert_eq!(parse_decision("CLARIFY"), ResponseDecision::Clarify); + assert_eq!(parse_decision("clarify, not sure"), ResponseDecision::Clarify); + } + + /// SILENT recognized over SUBMIT, but CLARIFY takes precedence over + /// SILENT when both present (matches TS branch order). + #[test] + fn parse_silent_recognized() { + assert_eq!(parse_decision("SILENT"), ResponseDecision::Silent); + assert_eq!(parse_decision("silent please"), ResponseDecision::Silent); + } + + #[test] + fn parse_clarify_beats_silent_when_both_present() { + // TS branch order: CLARIFY check comes before SILENT, so a + // model that emits "CLARIFY (or silent if unclear)" resolves + // to Clarify. + assert_eq!( + parse_decision("CLARIFY or SILENT"), + ResponseDecision::Clarify + ); + } + + /// Unrecognized text → SUBMIT (fail-open). Pins the TS behavior; + /// if a future refactor changes the default, this test breaks + /// deliberately. + #[test] + fn parse_unrecognized_defaults_to_submit() { + assert_eq!(parse_decision("yes, ship it"), ResponseDecision::Submit); + assert_eq!(parse_decision(""), ResponseDecision::Submit); + assert_eq!(parse_decision("garbage"), ResponseDecision::Submit); + } + + /// Whitespace + casing tolerance (TS does `.trim().toUpperCase()`). + #[test] + fn parse_tolerates_whitespace_and_casing() { + assert_eq!(parse_decision(" silent\n"), ResponseDecision::Silent); + assert_eq!(parse_decision("Clarify"), ResponseDecision::Clarify); + } + + // ─── reason_for ─────────────────────────────────────────────────── + + #[test] + fn reason_strings_are_stable() { + assert_eq!(reason_for(ResponseDecision::Submit), REASON_SUBMIT); + assert_eq!(reason_for(ResponseDecision::Clarify), REASON_CLARIFY); + assert_eq!(reason_for(ResponseDecision::Silent), REASON_SILENT); + } + + // ─── build_validate_generation_request ──────────────────────────── + + #[test] + fn generation_request_uses_groq_defaults() { + let r = req("d", "q"); + let g = build_validate_generation_request(&r, DEFAULT_VALIDATE_MODEL.to_string()); + assert_eq!(g.provider.as_deref(), Some(VALIDATE_PROVIDER)); + assert_eq!(g.model.as_deref(), Some(DEFAULT_VALIDATE_MODEL)); + assert_eq!(g.temperature, Some(VALIDATE_TEMPERATURE)); + assert_eq!(g.max_tokens, Some(VALIDATE_MAX_TOKENS)); + assert_eq!(g.purpose.as_deref(), Some("cognition/validate-response-decision")); + assert_eq!(g.messages.len(), 2); + assert_eq!(g.messages[0].role, "system"); + assert_eq!(g.messages[1].role, "user"); + } + + // ─── ValidateResponseError Display ──────────────────────────────── + + #[test] + fn error_no_adapter_displays_provider_and_model() { + let e = ValidateResponseError::NoAdapter { + provider: "groq".to_string(), + model: Some("llama-3.1-8b-instant".to_string()), + }; + let s = format!("{e}"); + assert!(s.contains("groq")); + assert!(s.contains("llama-3.1-8b-instant")); + } +} diff --git a/src/workers/continuum-core/src/modules/cognition.rs b/src/workers/continuum-core/src/modules/cognition.rs index 4d5888aa4..6f097a256 100644 --- a/src/workers/continuum-core/src/modules/cognition.rs +++ b/src/workers/continuum-core/src/modules/cognition.rs @@ -727,6 +727,27 @@ impl ServiceModule for CognitionModule { )) } + // ================================================================ + // Validate Response Decision (one-PR oxidizer — replaces TS AIValidateResponseServerCommand). + // Distinct from cognition/validate-response (which is persona-level + // response validation defined later in this match). + // ================================================================ + "cognition/validate-response-decision" => { + let _timer = TimingGuard::new("module", "cognition_validate_response_decision"); + let request = serde_json::from_value::< + crate::cognition::validate_response::ValidateResponseRequest, + >(params.clone()) + .map_err(|e| format!("Invalid validate-response-decision request: {e}"))?; + let decision = + crate::cognition::validate_response::evaluate_validate_response(request) + .await + .map_err(|e| format!("validate-response-decision error: {e}"))?; + Ok(CommandResult::Json( + serde_json::to_value(&decision) + .map_err(|e| format!("Serialize error: {e}"))?, + )) + } + // ================================================================ // Message Deduplication (single source of truth in Rust) // ================================================================ From ea8b260430e2c2d80766a7b4144acc53a760d1f4 Mon Sep 17 00:00:00 2001 From: Test Date: Mon, 18 May 2026 15:12:01 -0500 Subject: [PATCH 2/2] =?UTF-8?q?chore(eslint):=20lock=20baseline=20drop=205?= =?UTF-8?q?433=E2=86=925432=20from=20validate-response=20TS=20deletion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/eslint-baseline.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/eslint-baseline.txt b/src/eslint-baseline.txt index 555672baa..38627a6f0 100644 --- a/src/eslint-baseline.txt +++ b/src/eslint-baseline.txt @@ -1 +1 @@ -5433 +5432