2026-02-15 19:09:12 -07:00
|
|
|
#pragma once
|
|
|
|
|
// Step 326: RoutingEngine — Annotation-to-Dispatch Logic
|
|
|
|
|
//
|
|
|
|
|
// Given a WorkItem with routing annotations, produces a RoutingDecision
|
|
|
|
|
// that says what kind of worker should handle it and how much context
|
|
|
|
|
// to provide. Rules follow a priority cascade from explicit annotations
|
|
|
|
|
// through complexity and context width heuristics to pattern defaults.
|
|
|
|
|
|
|
|
|
|
#include "WorkItem.h"
|
|
|
|
|
#include <string>
|
|
|
|
|
#include <vector>
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
|
|
|
|
|
// --- RoutingDecision ---
|
|
|
|
|
|
|
|
|
|
struct RoutingDecision {
|
|
|
|
|
std::string workerType; // "deterministic" | "template" | "slm" | "llm" | "human"
|
|
|
|
|
std::string contextWidth; // "local" | "file" | "project" | "cross-project"
|
|
|
|
|
int contextBudgetTokens = 0; // estimated tokens for context window
|
|
|
|
|
bool reviewRequired = false; // whether result needs human review
|
|
|
|
|
float confidence = 0.0f; // engine's confidence in this routing (0.0-1.0)
|
|
|
|
|
std::string reasoning; // human-readable explanation
|
|
|
|
|
std::string agentRole; // "linter" | "refactor" | "generator"
|
|
|
|
|
|
|
|
|
|
json toJson() const {
|
|
|
|
|
return json{
|
|
|
|
|
{"workerType", workerType},
|
|
|
|
|
{"contextWidth", contextWidth},
|
|
|
|
|
{"contextBudgetTokens", contextBudgetTokens},
|
|
|
|
|
{"reviewRequired", reviewRequired},
|
|
|
|
|
{"confidence", confidence},
|
|
|
|
|
{"reasoning", reasoning},
|
|
|
|
|
{"agentRole", agentRole}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static RoutingDecision fromJson(const json& j) {
|
|
|
|
|
RoutingDecision d;
|
|
|
|
|
if (j.contains("workerType")) d.workerType = j["workerType"].get<std::string>();
|
|
|
|
|
if (j.contains("contextWidth")) d.contextWidth = j["contextWidth"].get<std::string>();
|
|
|
|
|
if (j.contains("contextBudgetTokens")) d.contextBudgetTokens = j["contextBudgetTokens"].get<int>();
|
|
|
|
|
if (j.contains("reviewRequired")) d.reviewRequired = j["reviewRequired"].get<bool>();
|
|
|
|
|
if (j.contains("confidence")) d.confidence = j["confidence"].get<float>();
|
|
|
|
|
if (j.contains("reasoning")) d.reasoning = j["reasoning"].get<std::string>();
|
|
|
|
|
if (j.contains("agentRole")) d.agentRole = j["agentRole"].get<std::string>();
|
|
|
|
|
return d;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// --- Context budget estimation ---
|
|
|
|
|
|
|
|
|
|
inline int estimateContextBudget(const std::string& contextWidth) {
|
|
|
|
|
if (contextWidth == "local") return 500;
|
|
|
|
|
if (contextWidth == "file") return 2000;
|
|
|
|
|
if (contextWidth == "project") return 8000;
|
|
|
|
|
if (contextWidth == "cross-project") return 16000;
|
|
|
|
|
return 500; // default to local
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Agent role from worker type ---
|
|
|
|
|
|
|
|
|
|
inline std::string agentRoleForWorker(const std::string& workerType) {
|
|
|
|
|
if (workerType == "deterministic" || workerType == "template") return "refactor";
|
|
|
|
|
if (workerType == "human") return "generator";
|
|
|
|
|
return "generator"; // slm, llm
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Pattern detection ---
|
|
|
|
|
|
|
|
|
|
inline bool isGetterSetterPattern(const std::string& nodeName) {
|
|
|
|
|
if (nodeName.size() < 4) return false;
|
|
|
|
|
std::string lower = nodeName;
|
|
|
|
|
std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
|
|
|
|
|
return lower.substr(0, 3) == "get" || lower.substr(0, 3) == "set" ||
|
|
|
|
|
lower.substr(0, 2) == "is";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- RoutingEngine ---
|
|
|
|
|
|
|
|
|
|
class RoutingEngine {
|
|
|
|
|
public:
|
|
|
|
|
// Main entry point: route a single work item
|
|
|
|
|
RoutingDecision route(const WorkItem& item) const {
|
|
|
|
|
RoutingDecision decision;
|
|
|
|
|
decision.contextWidth = item.contextWidth.empty() ? "local" : item.contextWidth;
|
|
|
|
|
decision.reviewRequired = item.reviewRequired;
|
|
|
|
|
decision.contextBudgetTokens = estimateContextBudget(decision.contextWidth);
|
|
|
|
|
|
|
|
|
|
// Track how many annotations contributed to the decision
|
|
|
|
|
int annotationSignals = 0;
|
|
|
|
|
|
|
|
|
|
// Rule 1: Explicit @Automatability override
|
|
|
|
|
if (!item.workerType.empty() && item.workerType != "llm") {
|
|
|
|
|
// Non-default workerType means architect explicitly set it
|
|
|
|
|
decision.workerType = item.workerType;
|
|
|
|
|
decision.reasoning = "Explicit @Automatability(" + item.workerType + ")";
|
|
|
|
|
decision.confidence = 0.95f;
|
|
|
|
|
annotationSignals++;
|
|
|
|
|
// Human routing always requires review
|
|
|
|
|
if (item.workerType == "human") {
|
|
|
|
|
decision.reviewRequired = true;
|
|
|
|
|
decision.confidence = 0.9f;
|
|
|
|
|
decision.reasoning = "Routed to human (high ambiguity or explicit)";
|
|
|
|
|
}
|
|
|
|
|
decision.agentRole = agentRoleForWorker(decision.workerType);
|
|
|
|
|
return decision;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Rule 3: Review gate
|
|
|
|
|
if (item.reviewRequired) {
|
|
|
|
|
annotationSignals++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Rule 5: Context width escalation (evaluated before complexity
|
|
|
|
|
// since we may not have complexity annotations)
|
|
|
|
|
if (decision.contextWidth == "cross-project") {
|
|
|
|
|
decision.workerType = "llm";
|
|
|
|
|
decision.reasoning = "Cross-project context requires LLM";
|
|
|
|
|
decision.confidence = 0.8f;
|
|
|
|
|
annotationSignals++;
|
|
|
|
|
decision.agentRole = "generator";
|
|
|
|
|
return decision;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (decision.contextWidth == "project") {
|
|
|
|
|
decision.workerType = "llm";
|
|
|
|
|
decision.reasoning = "Project-wide context favors LLM";
|
|
|
|
|
decision.confidence = 0.75f;
|
|
|
|
|
annotationSignals++;
|
|
|
|
|
decision.agentRole = "generator";
|
|
|
|
|
return decision;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Rule 6: Pattern defaults (no complexity annotations available
|
|
|
|
|
// through WorkItem, so use heuristics)
|
|
|
|
|
|
2026-02-15 19:20:00 -07:00
|
|
|
// Getter/setter/accessor → template
|
2026-02-15 19:09:12 -07:00
|
|
|
if (isGetterSetterPattern(item.nodeName)) {
|
2026-02-15 19:20:00 -07:00
|
|
|
decision.workerType = "template";
|
|
|
|
|
decision.reasoning = "Getter/setter pattern → template";
|
2026-02-15 19:09:12 -07:00
|
|
|
decision.confidence = 0.9f;
|
|
|
|
|
decision.agentRole = "refactor";
|
|
|
|
|
return decision;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Context-based defaults
|
|
|
|
|
if (decision.contextWidth == "local") {
|
|
|
|
|
// Local context: simple enough for template or SLM
|
|
|
|
|
decision.workerType = "slm";
|
|
|
|
|
decision.reasoning = "Local context, default to SLM";
|
|
|
|
|
decision.confidence = 0.6f;
|
|
|
|
|
decision.agentRole = "generator";
|
|
|
|
|
} else if (decision.contextWidth == "file") {
|
|
|
|
|
decision.workerType = "slm";
|
|
|
|
|
decision.reasoning = "File context, default to SLM";
|
|
|
|
|
decision.confidence = 0.55f;
|
|
|
|
|
decision.agentRole = "generator";
|
|
|
|
|
} else {
|
|
|
|
|
// Fallback
|
|
|
|
|
decision.workerType = "llm";
|
|
|
|
|
decision.reasoning = "Default routing to LLM";
|
|
|
|
|
decision.confidence = 0.5f;
|
|
|
|
|
decision.agentRole = "generator";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Boost confidence if we have annotation signals
|
|
|
|
|
if (annotationSignals > 0) {
|
|
|
|
|
decision.confidence = std::min(0.95f,
|
|
|
|
|
decision.confidence + 0.1f * annotationSignals);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return decision;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Batch routing
|
|
|
|
|
std::vector<RoutingDecision> routeBatch(const std::vector<WorkItem>& items) const {
|
|
|
|
|
std::vector<RoutingDecision> decisions;
|
|
|
|
|
decisions.reserve(items.size());
|
|
|
|
|
for (const auto& item : items) {
|
|
|
|
|
decisions.push_back(route(item));
|
|
|
|
|
}
|
|
|
|
|
return decisions;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-16 12:36:36 -07:00
|
|
|
RoutingDecision routeWithHistory(const WorkItem& item, int rejectionCount) const {
|
|
|
|
|
RoutingDecision base = route(item);
|
|
|
|
|
if (rejectionCount <= 0) return base;
|
|
|
|
|
|
|
|
|
|
// Escalate one level per rejection attempt, based on the last rejected
|
|
|
|
|
// worker. This prevents skipping stages (e.g. slm -> human).
|
|
|
|
|
std::string priorWorker = base.workerType;
|
|
|
|
|
if (!item.rejectionHistory.empty() &&
|
|
|
|
|
!item.rejectionHistory.back().workerType.empty()) {
|
|
|
|
|
priorWorker = item.rejectionHistory.back().workerType;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (priorWorker == "human") {
|
|
|
|
|
base.workerType = "human";
|
|
|
|
|
base.reviewRequired = true;
|
|
|
|
|
base.reasoning += " | remains human after rejection";
|
|
|
|
|
base.agentRole = agentRoleForWorker(base.workerType);
|
|
|
|
|
return base;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
base.workerType = escalateWorker(priorWorker);
|
|
|
|
|
base.contextWidth = widenedContext(base.contextWidth, 1);
|
|
|
|
|
base.contextBudgetTokens = estimateContextBudget(base.contextWidth);
|
|
|
|
|
base.reviewRequired = true;
|
|
|
|
|
base.confidence = std::max(0.4f, base.confidence - 0.10f);
|
|
|
|
|
base.reasoning += " | escalated after rejection history";
|
|
|
|
|
base.agentRole = agentRoleForWorker(base.workerType);
|
|
|
|
|
return base;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-15 19:09:12 -07:00
|
|
|
// Route and apply: updates the WorkItem's workerType + reviewRequired
|
|
|
|
|
void routeAndApply(WorkItem& item) const {
|
|
|
|
|
auto decision = route(item);
|
|
|
|
|
item.workerType = decision.workerType;
|
|
|
|
|
item.reviewRequired = decision.reviewRequired;
|
|
|
|
|
item.contextWidth = decision.contextWidth;
|
|
|
|
|
}
|
2026-02-16 12:36:36 -07:00
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
static std::string escalateWorker(const std::string& workerType) {
|
|
|
|
|
if (workerType == "deterministic" || workerType == "template") return "slm";
|
|
|
|
|
if (workerType == "slm") return "llm";
|
|
|
|
|
return "human";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static std::string widenedContext(const std::string& ctx, int rejectionCount) {
|
|
|
|
|
std::vector<std::string> order = {"local", "file", "project", "cross-project"};
|
|
|
|
|
int idx = 0;
|
|
|
|
|
for (size_t i = 0; i < order.size(); ++i) {
|
|
|
|
|
if (order[i] == ctx) {
|
|
|
|
|
idx = static_cast<int>(i);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
idx = std::min(3, idx + rejectionCount);
|
|
|
|
|
return order[idx];
|
|
|
|
|
}
|
2026-02-15 19:09:12 -07:00
|
|
|
};
|