Step 380: rejection rerouting feedback loop

This commit is contained in:
Bill
2026-02-16 12:36:36 -07:00
parent e2dd2b1d3a
commit b99957e2fc
7 changed files with 475 additions and 4 deletions

View File

@@ -59,9 +59,12 @@ public:
// Always include annotations (high priority)
// Annotations are embedded in nodeAst for now
// Skeleton intent from result reasoning (if re-routed)
if (!item.result.reasoning.empty() &&
item.result.reasoning.find("feedback") != std::string::npos) {
// Rejection feedback is first-class context for re-routed attempts.
if (!item.rejectionFeedback.empty()) {
ctx.feedbackFromRejection = item.rejectionFeedback;
} else if (!item.result.reasoning.empty() &&
item.result.reasoning.find("feedback") != std::string::npos) {
// Backward-compatible fallback for older serialized items.
ctx.feedbackFromRejection = item.result.reasoning;
}

View File

@@ -182,6 +182,36 @@ public:
return decisions;
}
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;
}
// Route and apply: updates the WorkItem's workerType + reviewRequired
void routeAndApply(WorkItem& item) const {
auto decision = route(item);
@@ -189,4 +219,24 @@ public:
item.reviewRequired = decision.reviewRequired;
item.contextWidth = decision.contextWidth;
}
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];
}
};

View File

@@ -52,6 +52,34 @@ struct WorkItemResult {
}
};
struct RejectionAttempt {
std::string workerType;
WorkItemResult result;
std::string feedback;
std::string rejectedAt;
std::string rejectedBy;
json toJson() const {
return json{
{"workerType", workerType},
{"result", result.toJson()},
{"feedback", feedback},
{"rejectedAt", rejectedAt},
{"rejectedBy", rejectedBy}
};
}
static RejectionAttempt fromJson(const json& j) {
RejectionAttempt a;
if (j.contains("workerType")) a.workerType = j["workerType"].get<std::string>();
if (j.contains("result")) a.result = WorkItemResult::fromJson(j["result"]);
if (j.contains("feedback")) a.feedback = j["feedback"].get<std::string>();
if (j.contains("rejectedAt")) a.rejectedAt = j["rejectedAt"].get<std::string>();
if (j.contains("rejectedBy")) a.rejectedBy = j["rejectedBy"].get<std::string>();
return a;
}
};
// --- WorkItem status values ---
inline const std::string WI_PENDING = "pending";
@@ -89,6 +117,8 @@ struct WorkItem {
// Result
WorkItemResult result;
std::string rejectionFeedback;
std::vector<RejectionAttempt> rejectionHistory;
};
// --- Helper: ISO timestamp ---
@@ -195,6 +225,10 @@ inline json workItemToJson(const WorkItem& wi) {
j["assignedAt"] = wi.assignedAt;
j["completedAt"] = wi.completedAt;
j["result"] = wi.result.toJson();
j["rejectionFeedback"] = wi.rejectionFeedback;
json attempts = json::array();
for (const auto& a : wi.rejectionHistory) attempts.push_back(a.toJson());
j["rejectionHistory"] = attempts;
return j;
}
@@ -216,5 +250,11 @@ inline WorkItem workItemFromJson(const json& j) {
if (j.contains("assignedAt")) wi.assignedAt = j["assignedAt"].get<std::string>();
if (j.contains("completedAt")) wi.completedAt = j["completedAt"].get<std::string>();
if (j.contains("result")) wi.result = WorkItemResult::fromJson(j["result"]);
if (j.contains("rejectionFeedback")) wi.rejectionFeedback = j["rejectionFeedback"].get<std::string>();
if (j.contains("rejectionHistory")) {
for (const auto& a : j["rejectionHistory"]) {
wi.rejectionHistory.push_back(RejectionAttempt::fromJson(a));
}
}
return wi;
}

View File

@@ -170,6 +170,32 @@ public:
return out;
}
bool rejectAndRequeue(const std::string& itemId,
const std::string& feedback,
const std::string& rejectedBy = "reviewer") {
auto maybeItem = state_->queue.getItem(itemId);
if (!maybeItem.has_value()) return false;
WorkItem item = maybeItem.value();
if (item.status != WI_REVIEW && item.status != WI_IN_PROGRESS) return false;
RejectionAttempt attempt;
attempt.workerType = item.workerType;
attempt.result = item.result;
attempt.feedback = feedback;
attempt.rejectedAt = workItemTimestamp();
attempt.rejectedBy = rejectedBy;
item.rejectionHistory.push_back(attempt);
item.rejectionFeedback = feedback;
item.result.reasoning = "feedback: " + feedback;
applyFeedbackHints(item, feedback);
item.status = WI_REJECTED;
transitionWorkItem(item, WI_READY);
item.reviewRequired = true;
return state_->queue.updateItem(itemId, item);
}
private:
WorkflowState* state_ = nullptr;
RoutingEngine* routing_ = nullptr;
@@ -217,7 +243,8 @@ private:
if (!maybeItem.has_value()) return events;
WorkItem item = maybeItem.value();
RoutingDecision decision = routing_->route(item);
RoutingDecision decision = routing_->routeWithHistory(
item, static_cast<int>(item.rejectionHistory.size()));
item.workerType = decision.workerType;
item.contextWidth = decision.contextWidth;
item.reviewRequired = decision.reviewRequired;
@@ -281,4 +308,17 @@ private:
return events;
}
static void applyFeedbackHints(WorkItem& item, const std::string& feedback) {
auto contains = [&](const std::string& token) {
return feedback.find(token) != std::string::npos;
};
if (contains("context=local")) item.contextWidth = "local";
if (contains("context=file")) item.contextWidth = "file";
if (contains("context=project")) item.contextWidth = "project";
if (contains("context=cross-project")) item.contextWidth = "cross-project";
if (contains("worker=human")) item.workerType = "human";
if (contains("worker=llm")) item.workerType = "llm";
if (contains("worker=slm")) item.workerType = "slm";
}
};