Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 7 additions & 172 deletions src/commands/ai/should-respond/shared/AIShouldRespondCommand.ts
Original file line number Diff line number Diff line change
@@ -1,183 +1,18 @@
/**
* AI Should-Respond Command - Shared Logic
* AI Should-Respond Command - Shared base class
*
* Sentinel/Coordinator pattern: Use AI to intelligently gate persona responses
* Sentinel/Coordinator pattern: Use AI to intelligently gate persona responses.
*
* Uses the local Qwen gating model to analyze full conversation context
* and decide if a persona should respond to a message.
* Per continuum#1420 (oxidizer) the actual gating logic — prompt
* assembly, model call, decision parsing — lives in Rust at
* `cognition/should_respond.rs::evaluate_gating`. The Server impl
* delegates via `RustCoreIPCClient.cognitionShouldRespond`. This base
* class is the shared shell that Server + Browser commands extend.
*/

import { CommandBase } from '../../../../daemons/command-daemon/shared/CommandBase';
import type { CommandParams, CommandResult } from '../../../../system/core/types/JTAGTypes';
import type { AIShouldRespondParams, AIShouldRespondResult } from './AIShouldRespondTypes';

export abstract class AIShouldRespondCommand extends CommandBase<CommandParams, CommandResult> {
static readonly commandName = 'ai/should-respond';

/**
* Build the gating instruction that gets appended AFTER the conversation history
*
* The LLM will see:
* 1. System: "You are a conversation coordinator..."
* 2. [Full conversation history as proper messages]
* 3. User: [This gating instruction]
*/
protected buildGatingInstruction(params: AIShouldRespondParams): string {
const { personaName } = params;

return `You are "${personaName}" in a group chat. Should you respond to the message marked >>> like this <<<?

CRITICAL RULES:
1. If someone ALREADY answered the question → shouldRespond: FALSE, stay silent
2. If you would just repeat what was already said → shouldRespond: FALSE, stay silent
3. If the answer is WRONG and needs correction → shouldRespond: TRUE, correct it
4. If nobody helped yet and question needs answer → shouldRespond: TRUE, help them
5. If you have a DISTINCT new angle not covered → shouldRespond: TRUE, add your perspective

EXAMPLES:
- "Helper AI already explained async/await well" → shouldRespond: FALSE
- "Answer exists but is incomplete, I can add X" → shouldRespond: TRUE
- "Nobody answered the question yet" → shouldRespond: TRUE
- "Answer is wrong, correct answer is Y" → shouldRespond: TRUE

Return JSON only:
{
"shouldRespond": true/false,
"confidence": 0.0-1.0,
"reason": "brief why/why not"
}`;
}

/**
* DEPRECATED: Old method that flattened conversation to string
* Kept for reference but should not be used
*/
protected buildGatingPrompt(params: AIShouldRespondParams): string {
const { personaName, ragContext, triggerMessage } = params;

// Validate ragContext
if (!ragContext) {
throw new Error('ragContext is required for buildGatingPrompt');
}

// Extract conversation history from RAG context
// IMPORTANT: Take more context to see past AI chatter, but highlight the trigger message
const recentMessages = ragContext.conversationHistory?.slice(-15) ?? [];

// Build conversation text with the trigger message HIGHLIGHTED
const conversationLines = recentMessages.map(msg => {
const line = `${msg.name ?? msg.role}: ${msg.content}`;
// Check if this is the trigger message (match by content and sender)
const isTrigger = msg.content === triggerMessage.content &&
msg.name === triggerMessage.senderName;
return isTrigger ? `>>> ${line} <<<` : line;
});

// If trigger message isn't in recent history, append it explicitly
const triggerInHistory = recentMessages.some(msg =>
msg.content === triggerMessage.content &&
msg.name === triggerMessage.senderName
);

if (!triggerInHistory) {
conversationLines.push(`>>> ${triggerMessage.senderName}: ${triggerMessage.content} <<<`);
}

const conversationText = conversationLines.join('\n');

// Extract persona identity for context
const members = `${ragContext.identity?.name ?? personaName} and others`;

return `You are a conversation coordinator for a multi-party chat room.

**Your Job**: Decide if "${personaName}" should respond to the message marked with >>> arrows <<<.

**Room Members**: ${members}

**Recent Conversation** (message to evaluate is marked with >>> arrows <<<):
${conversationText}

**Decision Rules**:
1. If ${personaName} is directly mentioned by name → respond
2. If this is a question and ${personaName} has unique expertise → respond
3. If someone else JUST answered the same question → DON'T respond (avoid spam)
4. If ${personaName} has spoken in 3+ of last 5 messages → DON'T respond (dominating)
5. If message is off-topic for ${personaName}'s expertise → DON'T respond
6. When in doubt, err on the side of SILENCE (better to miss one than spam)

**Response Format** (JSON only):
{
"shouldRespond": true/false,
"confidence": 0.0-1.0,
"reason": "brief explanation",
"factors": {
"mentioned": true/false,
"questionAsked": true/false,
"domainRelevant": true/false,
"recentlySpoke": true/false,
"othersAnswered": true/false
}
}`;
}

/**
* Parse AI response into structured result
*
* The AI should return JSON, but we'll handle both JSON and natural language
*/
protected parseGatingResponse(aiText: string): Partial<AIShouldRespondResult> {
try {
// Try to extract JSON from response
const jsonMatch = aiText.match(/\{[\s\S]*\}/);
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[0]);
return {
shouldRespond: parsed.shouldRespond ?? false,
confidence: parsed.confidence ?? 0.5,
reason: parsed.reason ?? 'No reason provided',
factors: parsed.factors ?? {
mentioned: false,
questionAsked: false,
domainRelevant: false,
recentlySpoke: false,
othersAnswered: false
}
};
}

// Fallback: Look for keywords in natural language response
const lowerText = aiText.toLowerCase();
const shouldRespond = lowerText.includes('should respond') ||
lowerText.includes('yes') ||
lowerText.includes('true');

return {
shouldRespond,
confidence: 0.5,
reason: aiText.slice(0, 200),
factors: {
mentioned: lowerText.includes('mentioned'),
questionAsked: lowerText.includes('question'),
domainRelevant: lowerText.includes('relevant') || lowerText.includes('expertise'),
recentlySpoke: lowerText.includes('recent') || lowerText.includes('dominating'),
othersAnswered: lowerText.includes('answered') || lowerText.includes('already')
}
};
} catch (error) {
console.error('Failed to parse gating AI response:', error);
// Default to NOT responding on parse errors (fail safe)
return {
shouldRespond: false,
confidence: 0.0,
reason: 'Failed to parse AI response',
factors: {
mentioned: false,
questionAsked: false,
domainRelevant: false,
recentlySpoke: false,
othersAnswered: false
}
};
}
}
}
Loading