Step 326: RoutingEngine — Annotation-to-Dispatch Logic (12/12 tests)
Routing engine maps WorkItem annotations to dispatch decisions: worker type, context budget, review requirements. Priority cascade from explicit overrides through pattern detection to context-based defaults. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1956,4 +1956,9 @@ target_link_libraries(step325_test PRIVATE
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
# Step 326: RoutingEngine — Annotation-to-Dispatch Logic
|
||||
add_executable(step326_test tests/step326_test.cpp)
|
||||
target_include_directories(step326_test PRIVATE src)
|
||||
target_link_libraries(step326_test PRIVATE nlohmann_json::nlohmann_json)
|
||||
|
||||
# Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)
|
||||
|
||||
192
editor/src/RoutingEngine.h
Normal file
192
editor/src/RoutingEngine.h
Normal file
@@ -0,0 +1,192 @@
|
||||
#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)
|
||||
|
||||
// Getter/setter/accessor → deterministic
|
||||
if (isGetterSetterPattern(item.nodeName)) {
|
||||
decision.workerType = "deterministic";
|
||||
decision.reasoning = "Getter/setter pattern → deterministic";
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
};
|
||||
190
editor/tests/step326_test.cpp
Normal file
190
editor/tests/step326_test.cpp
Normal file
@@ -0,0 +1,190 @@
|
||||
// Step 326: RoutingEngine — Annotation-to-Dispatch Logic (12 tests)
|
||||
// Tests routing rules: explicit override, ambiguity, context width,
|
||||
// pattern defaults, batch routing, confidence, context budget.
|
||||
|
||||
#include "RoutingEngine.h"
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
static int passed = 0, failed = 0;
|
||||
#define TEST(name) { std::cout << " " << #name << "... "; }
|
||||
#define PASS() { std::cout << "PASS\n"; ++passed; }
|
||||
#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; }
|
||||
#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {}
|
||||
|
||||
static WorkItem makeItem(const std::string& name,
|
||||
const std::string& workerType = "",
|
||||
const std::string& contextWidth = "local",
|
||||
bool reviewRequired = false) {
|
||||
WorkItem wi;
|
||||
wi.id = "wi-" + name;
|
||||
wi.nodeId = "n-" + name;
|
||||
wi.nodeName = name;
|
||||
wi.nodeType = "Function";
|
||||
wi.bufferId = "buf";
|
||||
wi.workerType = workerType;
|
||||
wi.contextWidth = contextWidth;
|
||||
wi.reviewRequired = reviewRequired;
|
||||
wi.priority = "medium";
|
||||
wi.createdAt = workItemTimestamp();
|
||||
return wi;
|
||||
}
|
||||
|
||||
RoutingEngine engine;
|
||||
|
||||
// 1. Explicit @Automatability overrides inferred routing
|
||||
void test_explicit_override() {
|
||||
TEST(explicit_override);
|
||||
auto wi = makeItem("compute", "deterministic", "project");
|
||||
auto d = engine.route(wi);
|
||||
CHECK(d.workerType == "deterministic", "explicit deterministic honored");
|
||||
CHECK(d.confidence > 0.9f, "high confidence for explicit");
|
||||
CHECK(d.reasoning.find("Explicit") != std::string::npos, "reasoning mentions explicit");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 2. High ambiguity → human
|
||||
void test_ambiguity_human() {
|
||||
TEST(ambiguity_human);
|
||||
auto wi = makeItem("ambiguous", "human", "local");
|
||||
auto d = engine.route(wi);
|
||||
CHECK(d.workerType == "human", "routed to human");
|
||||
CHECK(d.reviewRequired, "review required for human");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 3. Low complexity + local → deterministic (getter pattern)
|
||||
void test_getter_deterministic() {
|
||||
TEST(getter_deterministic);
|
||||
auto wi = makeItem("getName", "", "local");
|
||||
auto d = engine.route(wi);
|
||||
CHECK(d.workerType == "deterministic", "getter → deterministic");
|
||||
CHECK(d.agentRole == "refactor", "agent role = refactor");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 4. Setter pattern → deterministic
|
||||
void test_setter_deterministic() {
|
||||
TEST(setter_deterministic);
|
||||
auto wi = makeItem("setValue", "", "local");
|
||||
auto d = engine.route(wi);
|
||||
CHECK(d.workerType == "deterministic", "setter → deterministic");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 5. Cross-project → LLM
|
||||
void test_cross_project_llm() {
|
||||
TEST(cross_project_llm);
|
||||
auto wi = makeItem("orchestrate", "", "cross-project");
|
||||
auto d = engine.route(wi);
|
||||
CHECK(d.workerType == "llm", "cross-project → llm");
|
||||
CHECK(d.contextBudgetTokens == 16000, "16000 token budget");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 6. Project context → LLM
|
||||
void test_project_llm() {
|
||||
TEST(project_llm);
|
||||
auto wi = makeItem("buildGraph", "", "project");
|
||||
auto d = engine.route(wi);
|
||||
CHECK(d.workerType == "llm", "project → llm");
|
||||
CHECK(d.contextBudgetTokens == 8000, "8000 token budget");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 7. Review annotation sets reviewRequired
|
||||
void test_review_annotation() {
|
||||
TEST(review_annotation);
|
||||
auto wi = makeItem("sensitive", "", "local", true);
|
||||
auto d = engine.route(wi);
|
||||
CHECK(d.reviewRequired, "review preserved from annotation");
|
||||
// Should also boost confidence slightly
|
||||
CHECK(d.confidence > 0.5f, "confidence boosted by annotation signal");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 8. Batch routing
|
||||
void test_batch_routing() {
|
||||
TEST(batch_routing);
|
||||
std::vector<WorkItem> items = {
|
||||
makeItem("getName", "", "local"),
|
||||
makeItem("compute", "", "project"),
|
||||
makeItem("transform", "human", "local"),
|
||||
};
|
||||
auto decisions = engine.routeBatch(items);
|
||||
CHECK(decisions.size() == 3, "3 decisions");
|
||||
CHECK(decisions[0].workerType == "deterministic", "first=deterministic");
|
||||
CHECK(decisions[1].workerType == "llm", "second=llm");
|
||||
CHECK(decisions[2].workerType == "human", "third=human");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 9. No-annotation defaults (local context, no patterns)
|
||||
void test_default_local() {
|
||||
TEST(default_local);
|
||||
auto wi = makeItem("processData", "", "local");
|
||||
auto d = engine.route(wi);
|
||||
CHECK(d.workerType == "slm", "local default → slm");
|
||||
CHECK(d.contextBudgetTokens == 500, "500 token budget");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 10. File context default
|
||||
void test_default_file() {
|
||||
TEST(default_file);
|
||||
auto wi = makeItem("handleRequest", "", "file");
|
||||
auto d = engine.route(wi);
|
||||
CHECK(d.workerType == "slm", "file default → slm");
|
||||
CHECK(d.contextBudgetTokens == 2000, "2000 token budget");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 11. Confidence reflects annotation coverage
|
||||
void test_confidence_levels() {
|
||||
TEST(confidence_levels);
|
||||
// Explicit annotation: highest confidence
|
||||
auto wi1 = makeItem("f1", "template", "local");
|
||||
auto d1 = engine.route(wi1);
|
||||
|
||||
// No annotations, just defaults: lower confidence
|
||||
auto wi2 = makeItem("f2", "", "local");
|
||||
auto d2 = engine.route(wi2);
|
||||
|
||||
CHECK(d1.confidence > d2.confidence,
|
||||
"explicit has higher confidence than default");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 12. Context budget reasonable for each width
|
||||
void test_context_budgets() {
|
||||
TEST(context_budgets);
|
||||
CHECK(estimateContextBudget("local") == 500, "local=500");
|
||||
CHECK(estimateContextBudget("file") == 2000, "file=2000");
|
||||
CHECK(estimateContextBudget("project") == 8000, "project=8000");
|
||||
CHECK(estimateContextBudget("cross-project") == 16000, "cross-project=16000");
|
||||
CHECK(estimateContextBudget("unknown") == 500, "unknown defaults to 500");
|
||||
PASS();
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "=== Step 326: RoutingEngine Annotation-to-Dispatch Logic ===\n";
|
||||
try {
|
||||
test_explicit_override();
|
||||
test_ambiguity_human();
|
||||
test_getter_deterministic();
|
||||
test_setter_deterministic();
|
||||
test_cross_project_llm();
|
||||
test_project_llm();
|
||||
test_review_annotation();
|
||||
test_batch_routing();
|
||||
test_default_local();
|
||||
test_default_file();
|
||||
test_confidence_levels();
|
||||
test_context_budgets();
|
||||
} catch (const std::exception& e) {
|
||||
std::cout << "EXCEPTION: " << e.what() << "\n";
|
||||
++failed;
|
||||
}
|
||||
std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n";
|
||||
return failed > 0 ? 1 : 0;
|
||||
}
|
||||
21
progress.md
21
progress.md
@@ -1333,6 +1333,27 @@ rejection flow, persistence roundtrip, stats accuracy, and a combined
|
||||
|
||||
**Phase 12a complete:** 6/6 steps (320-325), 68/68 tests passing
|
||||
|
||||
## Phase 12b: Routing Engine + Workers
|
||||
|
||||
### Step 326: RoutingEngine — Annotation-to-Dispatch Logic
|
||||
**Status:** PASS (12/12 tests)
|
||||
|
||||
Given a WorkItem with routing annotations, produces a RoutingDecision with
|
||||
worker type, context width, token budget, review requirements, confidence,
|
||||
and reasoning. Priority cascade: explicit override → ambiguity gate →
|
||||
context width escalation → getter/setter patterns → defaults.
|
||||
|
||||
**Files created:**
|
||||
- `editor/src/RoutingEngine.h` — RoutingDecision struct (with JSON helpers),
|
||||
estimateContextBudget, agentRoleForWorker, isGetterSetterPattern, RoutingEngine
|
||||
class with route, routeBatch, routeAndApply
|
||||
- `editor/tests/step326_test.cpp` — 12 tests: explicit override, ambiguity→human,
|
||||
getter/setter→deterministic, cross-project→llm, project→llm, review annotation,
|
||||
batch routing, default local/file, confidence levels, context budgets
|
||||
|
||||
**Files modified:**
|
||||
- `editor/CMakeLists.txt` — step326_test target
|
||||
|
||||
---
|
||||
|
||||
# Roadmap Planning — Sprints 12-25+
|
||||
|
||||
Reference in New Issue
Block a user