aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
307 lines (261 loc) • 8.82 kB
JavaScript
/**
* @module ConfidenceScorer
* @description Confidence scoring algorithm for workflow suggestions
* @story WIS-2 - Workflow Registry Enhancement
* @version 1.0.0
*/
;
/**
* Scoring weights for confidence calculation
* @type {Object}
*/
const SCORING_WEIGHTS = {
COMMAND_MATCH: 0.4,
AGENT_MATCH: 0.25,
HISTORY_DEPTH: 0.2,
PROJECT_STATE: 0.15,
};
/**
* ConfidenceScorer class for calculating workflow suggestion confidence
*/
class ConfidenceScorer {
/**
* Create a ConfidenceScorer instance
* @param {Object} options - Configuration options
* @param {Object} options.weights - Custom scoring weights
*/
constructor(options = {}) {
this.weights = { ...SCORING_WEIGHTS, ...options.weights };
this.validateWeights();
}
/**
* Validate that weights sum to 1.0
* @throws {Error} If weights don't sum to 1.0
*/
validateWeights() {
const sum = Object.values(this.weights).reduce((a, b) => a + b, 0);
if (Math.abs(sum - 1.0) > 0.001) {
throw new Error(`Scoring weights must sum to 1.0, got ${sum}`);
}
}
/**
* Calculate confidence score for a suggestion given context
* @param {Object} suggestion - The workflow suggestion
* @param {string} suggestion.trigger - Command or condition that triggers transition
* @param {string[]} suggestion.agentSequence - Expected agent sequence
* @param {string[]} suggestion.keyCommands - Key commands for this workflow
* @param {Object} context - Current session context
* @param {string} context.lastCommand - Last executed command
* @param {string[]} context.lastCommands - Recent command history
* @param {string} context.agentId - Current active agent
* @param {Object} context.projectState - Current project state
* @returns {number} Normalized score between 0.0 and 1.0
*/
score(suggestion, context) {
if (!suggestion || !context) {
return 0;
}
const commandMatch = this.matchCommand(suggestion.trigger, context.lastCommand);
const agentMatch = this.matchAgent(suggestion.agentSequence, context.agentId);
const historyDepth = this.matchHistory(suggestion.keyCommands, context.lastCommands);
const projectState = this.matchProjectState(suggestion, context.projectState);
const rawScore =
commandMatch * this.weights.COMMAND_MATCH +
agentMatch * this.weights.AGENT_MATCH +
historyDepth * this.weights.HISTORY_DEPTH +
projectState * this.weights.PROJECT_STATE;
return this.normalize(rawScore);
}
/**
* Match command trigger against last executed command
* @param {string} trigger - Expected trigger command
* @param {string} lastCommand - Last executed command
* @returns {number} Match score 0.0-1.0
*/
matchCommand(trigger, lastCommand) {
if (!trigger || !lastCommand) {
return 0;
}
const normalizedTrigger = this.normalizeCommand(trigger);
const normalizedLast = this.normalizeCommand(lastCommand);
// Exact match
if (normalizedTrigger === normalizedLast) {
return 1.0;
}
// Trigger contains the command
if (normalizedTrigger.includes(normalizedLast)) {
return 0.9;
}
// Command contains trigger keyword
if (normalizedLast.includes(normalizedTrigger.split(' ')[0])) {
return 0.7;
}
// Partial word match
const triggerWords = normalizedTrigger.split(/[\s-_]+/);
const lastWords = normalizedLast.split(/[\s-_]+/);
const commonWords = triggerWords.filter((w) => lastWords.includes(w));
if (commonWords.length > 0) {
return 0.5 * (commonWords.length / Math.max(triggerWords.length, 1));
}
return 0;
}
/**
* Match agent sequence against current agent
* @param {string[]} agentSequence - Expected agent sequence
* @param {string} currentAgent - Current active agent
* @returns {number} Match score 0.0-1.0
*/
matchAgent(agentSequence, currentAgent) {
if (!agentSequence || !currentAgent) {
return 0;
}
const normalizedAgent = currentAgent.replace('@', '').toLowerCase();
// Check if current agent is in the sequence
const agentIndex = agentSequence.findIndex((agent) => agent.toLowerCase() === normalizedAgent);
if (agentIndex === -1) {
return 0;
}
// Higher score for agents later in sequence (workflow is progressing)
const progressScore = (agentIndex + 1) / agentSequence.length;
// Bonus for being the expected next agent
if (agentIndex === 0) {
return 0.6 + progressScore * 0.4;
}
return progressScore;
}
/**
* Match command history against key workflow commands
* @param {string[]} keyCommands - Key commands for the workflow
* @param {string[]} lastCommands - Recent command history
* @returns {number} Match score 0.0-1.0
*/
matchHistory(keyCommands, lastCommands) {
if (!keyCommands || !lastCommands || keyCommands.length === 0) {
return 0;
}
const normalizedKeys = keyCommands.map((cmd) => this.normalizeCommand(cmd));
const normalizedHistory = lastCommands.map((cmd) => this.normalizeCommand(cmd));
let matchCount = 0;
let recentBonus = 0;
for (let i = 0; i < normalizedHistory.length; i++) {
const historyCmd = normalizedHistory[i];
for (const keyCmd of normalizedKeys) {
if (historyCmd.includes(keyCmd) || keyCmd.includes(historyCmd)) {
matchCount++;
// Recent commands get higher weight (decay factor)
const recency = 1 - i / normalizedHistory.length;
recentBonus += recency * 0.1;
break;
}
}
}
const baseScore = Math.min(matchCount / keyCommands.length, 1.0);
return Math.min(baseScore + recentBonus, 1.0);
}
/**
* Match suggestion against project state
* @param {Object} suggestion - The workflow suggestion
* @param {Object} projectState - Current project state
* @returns {number} Match score 0.0-1.0
*/
matchProjectState(suggestion, projectState) {
if (!suggestion || !projectState) {
return 0.5; // Neutral score when state unknown
}
const score = 0.5;
const factors = [];
// Check if story is in progress
if (projectState.activeStory) {
factors.push(0.2);
}
// Check if uncommitted changes exist
if (projectState.hasUncommittedChanges) {
// Git-related suggestions get higher score
if (
suggestion.trigger?.includes('git') ||
suggestion.trigger?.includes('commit') ||
suggestion.trigger?.includes('push')
) {
factors.push(0.3);
}
}
// Check if tests are failing
if (projectState.failingTests) {
// QA-related suggestions get higher score
if (
suggestion.trigger?.includes('test') ||
suggestion.trigger?.includes('qa') ||
suggestion.trigger?.includes('fix')
) {
factors.push(0.3);
}
}
// Check current workflow phase
if (projectState.workflowPhase && suggestion.phase) {
if (projectState.workflowPhase === suggestion.phase) {
factors.push(0.2);
}
}
return Math.min(score + factors.reduce((a, b) => a + b, 0), 1.0);
}
/**
* Normalize a command string for comparison
* @param {string} command - Command to normalize
* @returns {string} Normalized command
*/
normalizeCommand(command) {
if (!command) return '';
return command
.toLowerCase()
.replace(/\s+completed\s*$/i, '')
.replace(/\s+successfully\s*$/i, '')
.replace(/^\*/, '')
.replace(/['"]/g, '')
.trim();
}
/**
* Normalize score to 0.0-1.0 range
* @param {number} score - Raw score
* @returns {number} Normalized score
*/
normalize(score) {
return Math.max(0, Math.min(1, score));
}
/**
* Calculate scores for multiple suggestions and rank them
* @param {Object[]} suggestions - Array of suggestions
* @param {Object} context - Current session context
* @returns {Object[]} Sorted suggestions with scores
*/
rankSuggestions(suggestions, context) {
if (!suggestions || suggestions.length === 0) {
return [];
}
return suggestions
.map((suggestion) => ({
...suggestion,
score: this.score(suggestion, context),
}))
.sort((a, b) => b.score - a.score);
}
/**
* Get scoring weights
* @returns {Object} Current scoring weights
*/
getWeights() {
return { ...this.weights };
}
}
/**
* Create a new ConfidenceScorer instance
* @param {Object} options - Configuration options
* @returns {ConfidenceScorer} New scorer instance
*/
function createConfidenceScorer(options = {}) {
return new ConfidenceScorer(options);
}
module.exports = {
ConfidenceScorer,
createConfidenceScorer,
SCORING_WEIGHTS,
};