From 9d68190bf0d077d0f07844e776f973df08b66b91 Mon Sep 17 00:00:00 2001 From: Test Date: Mon, 18 May 2026 14:14:02 -0500 Subject: [PATCH] =?UTF-8?q?oxidizer(#1420):=20AIShouldRespondServerCommand?= =?UTF-8?q?=20=E2=80=94=20delegate=20to=20cognition/should-respond,=20drop?= =?UTF-8?q?=20parallel=20reimpl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AIShouldRespondServerCommand carried a separate gating implementation (custom prompt + JSON-repair-via-second-LLM retry) parallel to the canonical cognition/should-respond Rust path that AIDecisionService. evaluateGating already used. Two paths could drift independently; the TS prompt was already stale relative to the Rust template. This PR collapses both into one: the TS command now constructs the AIDecisionContext from the public AIShouldRespondParams shape and delegates to client.cognitionShouldRespond. Rust owns the prompt, model call, parser, and typed AIGatingDecision contract. ## Diff - AIShouldRespondServerCommand.ts: -85 / +59 LOC (net -26) - ESLint baseline ratchet: 5435 -> 5433 (-2) ## Dead TS deleted - `import { AIProviderDaemon }` (no longer referenced in this file) - `import { TextGenerationRequest }` (parent-class type only; not used by the delegation) - `import { LOCAL_MODELS }` (Rust evaluate_gating carries its own DEFAULT_GATING_MODEL constant; missing model defaults Rust-side) - Inline gating instruction build + message-array construction (Rust cognition/should_respond.rs::build_gating_prompt owns it) - JSON-repair-via-second-LLM retry path (Rust returns typed errors; caller decides retry policy at the IPC seam) - Stale `>>> trigger <<<` marking logic (Rust handles trigger marking inside build_gating_prompt — verified parity) ## What stays - The thin TS shim: param -> RustAIDecisionContext mapping + AIShouldRespondResult construction. - Verbose debug mode: still emits ragContext.messageCount + conversationPreview (TS-derivable, no Rust round-trip needed). `promptSent` / `aiResponse` debug fields now sentinel-pointer to the Rust logs (`cognition::should_respond`) where they actually live. - Catch-around-throw error path (matches sibling shim discipline). ## Discipline - Cast `params -> RustAIDecisionContext` mirrors the existing pattern in AIDecisionService.evaluateGating (`as unknown as` for the structurally-matching surface). - Synthetic `triggerMessage.id` derived from the timestamp so repeat calls don't multiply observability noise (params don't carry one; Rust requires it). - No fail-open default — failures throw, caller catches via the existing error-return path. ## Refs - #1420 sub-card (just filed) - Existing Rust: cognition/should_respond.rs::evaluate_gating (already shipped, in production via AIDecisionService.evaluateGating) - Sibling pattern: codex's #1383 check_redundancy delegation, my #1402 generate_response delegation - #1248 umbrella Co-Authored-By: Claude Opus 4.7 (1M context) --- .../server/AIShouldRespondServerCommand.ts | 144 +++++++----------- src/eslint-baseline.txt | 2 +- 2 files changed, 60 insertions(+), 86 deletions(-) diff --git a/src/commands/ai/should-respond/server/AIShouldRespondServerCommand.ts b/src/commands/ai/should-respond/server/AIShouldRespondServerCommand.ts index b0b410d0f..38519f81a 100644 --- a/src/commands/ai/should-respond/server/AIShouldRespondServerCommand.ts +++ b/src/commands/ai/should-respond/server/AIShouldRespondServerCommand.ts @@ -1,16 +1,26 @@ /** * AI Should-Respond Server Command * - * Uses AIProviderDaemon with proper RAG context (message array, not flattened string) + * Thin TS shim — delegates to the Rust cognition/should-respond IPC + * (cognition/should_respond.rs). Rust owns the gating prompt, model + * call, and parser; this command maps the public params shape into + * the IPC request and forwards the typed decision back. + * + * Prior to continuum#1420 this command carried a parallel + * reimplementation of gating with a stale prompt + JSON-repair retry + * loop — that drifted from the canonical Rust path used by + * AIDecisionService.evaluateGating. The delegation removes both + * paths' divergence risk. */ import { AIShouldRespondCommand } from '../shared/AIShouldRespondCommand'; import type { JTAGContext } from '../../../../system/core/types/JTAGTypes'; import type { ICommandDaemon } from '../../../../daemons/command-daemon/shared/CommandBase'; import type { AIShouldRespondParams, AIShouldRespondResult } from '../shared/AIShouldRespondTypes'; -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 { RustCoreIPCClient } from '../../../../workers/continuum-core/bindings/RustCoreIPC'; +import type { + AIDecisionContext as RustAIDecisionContext, +} from '../../../../shared/generated'; export class AIShouldRespondServerCommand extends AIShouldRespondCommand { constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { @@ -19,111 +29,75 @@ export class AIShouldRespondServerCommand extends AIShouldRespondCommand { async execute(params: AIShouldRespondParams): Promise { try { - // Validate ragContext for LLM strategy if (!params.ragContext) { throw new Error('ragContext is required for LLM strategy'); } - // Build gating instruction - const gatingInstruction = this.buildGatingInstruction(params); - - // Mark the trigger message in conversation history with >>> arrows <<< - const markedHistory = params.ragContext.conversationHistory.map(msg => { - const isTrigger = msg.content === params.triggerMessage.content && - msg.name === params.triggerMessage.senderName; - - if (isTrigger) { - return { - ...msg, - content: `>>> ${msg.content} <<<` - }; - } - return msg; + // Build the Rust IPC context from the public params shape. + // The Rust side (cognition/should_respond.rs::AIDecisionContext) + // structurally matches the TS RAGContext fields we forward; + // the cast mirrors what AIDecisionService.evaluateGating does + // for the same surface. + const context = { + personaId: params.personaId, + personaName: params.personaName, + roomId: params.contextId, + triggerMessage: { + // Rust requires a stable id on the trigger. Params don't + // carry one (callers identify the message by content + + // sender timestamp); synthesize a deterministic-looking + // id from the timestamp so repeat calls don't multiply + // observability noise. + id: `trigger-${params.triggerMessage.timestamp}`, + senderName: params.triggerMessage.senderName, + content: { text: params.triggerMessage.content }, + }, + ragContext: params.ragContext, + systemPrompt: params.ragContext.identity?.systemPrompt, + } as unknown as RustAIDecisionContext; + + const client = await RustCoreIPCClient.getInstanceAsync(); + const decision = await client.cognitionShouldRespond({ + context, + model: params.model, }); - // Build proper messages array: system + conversation history (with marked trigger) + gating instruction - const request: TextGenerationRequest = { - messages: [ - { role: 'system', content: 'You are a conversation coordinator. Respond ONLY with JSON.' }, - ...markedHistory, // Conversation with trigger message marked - { role: 'user', content: gatingInstruction } - ], - model: params.model ?? LOCAL_MODELS.DEFAULT, - temperature: 0.3, - maxTokens: 200, - provider: 'local' - }; - - const response = await AIProviderDaemon.generateText(request); - - if (!response.text) { - throw new Error(response.error ?? 'AI generation failed'); - } - - // Try to parse JSON - if it fails, use a better model to fix it - let parsed = this.parseGatingResponse(response.text); - - // If parsing failed (confidence = 0.0 means parse error), retry with better model to fix JSON - if (parsed.confidence === 0.0 && parsed.reason === 'Failed to parse AI response') { - console.warn(`⚠️ Gating JSON parse failed with ${request.model}, retrying with local Qwen to fix malformed JSON`); - - const fixRequest: TextGenerationRequest = { - messages: [ - { role: 'system', content: 'You are a JSON repair tool. Fix malformed JSON and return valid JSON only.' }, - { role: 'user', content: `This JSON is malformed:\n\n${response.text}\n\nFix it and return ONLY valid JSON with this exact structure:\n{\n "shouldRespond": true/false,\n "confidence": 0.0-1.0,\n "reason": "string",\n "factors": {\n "mentioned": true/false,\n "questionAsked": true/false,\n "domainRelevant": true/false,\n "recentlySpoke": true/false,\n "othersAnswered": true/false\n }\n}` } - ], - model: LOCAL_MODELS.DEFAULT, - temperature: 0.1, // Low temp for structured output - maxTokens: 200, - provider: 'local' - }; - - const fixedResponse = await AIProviderDaemon.generateText(fixRequest); - if (fixedResponse.text) { - parsed = this.parseGatingResponse(fixedResponse.text); - if (parsed.confidence !== 0.0) { - console.log(`✅ JSON repair succeeded with local Qwen`); - } else { - throw new Error(`JSON repair failed even with local Qwen. Original: ${response.text.slice(0, 200)}`); - } - } else { - throw new Error(`JSON repair request failed: ${fixedResponse.error}`); - } - } - - const confidence = parsed.confidence ?? 0.5; - - // Build debug output if verbose mode enabled + // Verbose debug surface: TS keeps message count + preview + // (derivable from params without Rust round-trip). Dropped: + // `promptSent` + `aiResponse` (Rust owns prompt assembly + + // sees the raw response; operator inspects Rust logs at + // `cognition::should_respond` for that detail). let debugOutput: AIShouldRespondResult['debug'] = undefined; if (params.verbose) { const conversationText = params.ragContext.conversationHistory .map(msg => `${msg.role}: ${msg.content}`) .join('\n'); - debugOutput = { ragContext: { messageCount: params.ragContext.conversationHistory.length, - conversationPreview: conversationText.substring(0, 500) + (conversationText.length > 500 ? '...' : '') + conversationPreview: + conversationText.substring(0, 500) + + (conversationText.length > 500 ? '...' : ''), }, - promptSent: gatingInstruction, - aiResponse: response.text + promptSent: '(Rust-owned — see cognition::should_respond logs)', + aiResponse: '(Rust-owned — see cognition::should_respond logs)', }; } return { context: params.context, sessionId: params.sessionId, - shouldRespond: parsed.shouldRespond ?? false, - confidence, - reason: parsed.reason ?? 'No reason provided', - factors: parsed.factors ?? { + shouldRespond: decision.shouldRespond, + confidence: decision.confidence, + reason: decision.reason, + factors: decision.factors ?? { mentioned: false, questionAsked: false, domainRelevant: false, recentlySpoke: false, - othersAnswered: false + othersAnswered: false, }, - debug: debugOutput + debug: debugOutput, }; } catch (error) { console.error('❌ AI Should-Respond: Command failed:', error); @@ -139,8 +113,8 @@ export class AIShouldRespondServerCommand extends AIShouldRespondCommand { questionAsked: false, domainRelevant: false, recentlySpoke: false, - othersAnswered: false - } + othersAnswered: false, + }, }; } } diff --git a/src/eslint-baseline.txt b/src/eslint-baseline.txt index 48ea2a198..555672baa 100644 --- a/src/eslint-baseline.txt +++ b/src/eslint-baseline.txt @@ -1 +1 @@ -5435 +5433