Step 378: Add workflow orchestrator core loop

This commit is contained in:
Bill
2026-02-16 12:28:43 -07:00
parent 4f0b7994f3
commit 51c5e57597
4 changed files with 534 additions and 0 deletions

View File

@@ -2281,4 +2281,13 @@ target_link_libraries(step377_test PRIVATE
tree_sitter_javascript tree_sitter_typescript
tree_sitter_java tree_sitter_rust tree_sitter_go)
add_executable(step378_test tests/step378_test.cpp)
target_include_directories(step378_test PRIVATE src)
target_link_libraries(step378_test PRIVATE
nlohmann_json::nlohmann_json
unofficial::tree-sitter::tree-sitter
tree_sitter_python tree_sitter_cpp tree_sitter_elisp
tree_sitter_javascript tree_sitter_typescript
tree_sitter_java tree_sitter_rust tree_sitter_go)
# Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)

View File

@@ -0,0 +1,231 @@
#pragma once
#include "WorkflowState.h"
#include "RoutingEngine.h"
#include "WorkerRegistry.h"
#include "ContextAssembler.h"
#include "ReviewGate.h"
#include <functional>
#include <string>
#include <vector>
struct OrchestratorEvent {
std::string type;
std::string itemId;
json detail;
std::string timestamp;
};
struct BlockerInfo {
std::string type;
std::vector<std::string> itemIds;
std::string description;
};
class WorkflowOrchestrator {
public:
WorkflowOrchestrator(WorkflowState& workflowState,
RoutingEngine& routingEngine,
WorkerRegistry& workerRegistry,
ContextAssembler& contextAssembler,
ReviewGate& reviewGate)
: state_(&workflowState), routing_(&routingEngine), workers_(&workerRegistry),
context_(&contextAssembler), review_(&reviewGate) {}
void setBuffers(const std::map<std::string, BufferInfo>& buffers) {
buffers_ = buffers;
}
void setReviewPolicy(const ReviewPolicy& policy) {
reviewPolicy_ = policy;
}
OrchestratorEvent step() {
auto ready = state_->queue.getReady();
if (ready.empty()) {
return blockedEvent();
}
return advanceItem(ready.front()).back();
}
std::vector<OrchestratorEvent> advance() {
std::vector<OrchestratorEvent> all;
auto ready = state_->queue.getReady();
for (const auto& item : ready) {
auto events = advanceItem(item);
all.insert(all.end(), events.begin(), events.end());
}
return all;
}
WorkflowStats runToCompletion() {
while (true) {
auto events = advance();
if (events.empty()) break;
bool anyProgress = false;
for (const auto& ev : events) {
if (ev.type != "blocked") {
anyProgress = true;
break;
}
}
if (!anyProgress) break;
}
return state_->getStats();
}
WorkflowStats runUntil(const std::function<bool(const WorkflowState&)>& predicate) {
while (!predicate(*state_)) {
auto events = advance();
if (events.empty()) break;
bool anyProgress = false;
for (const auto& ev : events) {
if (ev.type != "blocked") {
anyProgress = true;
break;
}
}
if (!anyProgress) break;
}
return state_->getStats();
}
std::vector<BlockerInfo> getBlockers() {
std::vector<BlockerInfo> out;
auto reviewItems = state_->queue.getByStatus(WI_REVIEW);
if (!reviewItems.empty()) {
BlockerInfo b;
b.type = "needs-review";
b.description = "Items are waiting in review.";
for (const auto& i : reviewItems) b.itemIds.push_back(i.id);
out.push_back(b);
}
BlockerInfo human;
human.type = "needs-human";
human.description = "Items are routed to human execution.";
BlockerInfo external;
external.type = "needs-external-model";
external.description = "Items are prepared for SLM/LLM external execution.";
for (const auto& status : {WI_ASSIGNED, WI_IN_PROGRESS}) {
for (const auto& item : state_->queue.getByStatus(status)) {
if (item.workerType == "human") {
human.itemIds.push_back(item.id);
} else if (item.workerType == "slm" || item.workerType == "llm") {
external.itemIds.push_back(item.id);
}
}
}
if (!human.itemIds.empty()) out.push_back(human);
if (!external.itemIds.empty()) out.push_back(external);
return out;
}
private:
WorkflowState* state_ = nullptr;
RoutingEngine* routing_ = nullptr;
WorkerRegistry* workers_ = nullptr;
ContextAssembler* context_ = nullptr;
ReviewGate* review_ = nullptr;
ReviewPolicy reviewPolicy_ = ReviewPolicy::getDefault();
std::map<std::string, BufferInfo> buffers_;
static OrchestratorEvent makeEvent(const std::string& type,
const std::string& itemId,
const json& detail = json::object()) {
OrchestratorEvent ev;
ev.type = type;
ev.itemId = itemId;
ev.detail = detail;
ev.timestamp = workItemTimestamp();
return ev;
}
OrchestratorEvent blockedEvent() {
auto blockers = getBlockers();
if (blockers.empty()) {
return makeEvent("blocked", "", json{{"reason", "no-ready-items"}});
}
return makeEvent("blocked", "", json{{"blockers", toJsonBlockers(blockers)}});
}
static json toJsonBlockers(const std::vector<BlockerInfo>& blockers) {
json arr = json::array();
for (const auto& b : blockers) {
arr.push_back({{"type", b.type},
{"itemIds", b.itemIds},
{"description", b.description}});
}
return arr;
}
std::vector<OrchestratorEvent> advanceItem(const WorkItem& readyItem) {
std::vector<OrchestratorEvent> events;
auto maybeItem = state_->queue.getItem(readyItem.id);
if (!maybeItem.has_value()) return events;
WorkItem item = maybeItem.value();
RoutingDecision decision = routing_->route(item);
item.workerType = decision.workerType;
item.contextWidth = decision.contextWidth;
item.reviewRequired = decision.reviewRequired;
transitionWorkItem(item, WI_ASSIGNED);
transitionWorkItem(item, WI_IN_PROGRESS);
state_->queue.updateItem(item.id, item);
events.push_back(makeEvent("routed", item.id, decision.toJson()));
WorkerContext ctx = context_->assembleContext(item, buffers_, item.contextWidth,
decision.contextBudgetTokens);
events.push_back(makeEvent("context-assembled", item.id,
json{{"contextWidth", item.contextWidth},
{"tokensBudget", decision.contextBudgetTokens}}));
WorkerInterface* worker = workers_->getWorker(item.workerType);
if (!worker) {
events.push_back(makeEvent("blocked", item.id,
json{{"reason", "worker-not-registered"},
{"workerType", item.workerType}}));
return events;
}
item.result = worker->execute(item, ctx);
state_->queue.updateItem(item.id, item);
events.push_back(makeEvent("executed", item.id,
json{{"workerType", item.workerType},
{"confidence", item.result.confidence}}));
if (item.workerType == "human") {
events.push_back(makeEvent("blocked", item.id,
json{{"reason", "needs-human"}}));
return events;
}
if (item.workerType == "slm" || item.workerType == "llm") {
events.push_back(makeEvent("blocked", item.id,
json{{"reason", "needs-external-model"}}));
return events;
}
ReviewDecision reviewDecision = review_->shouldAutoApprove(item, item.result, reviewPolicy_);
if (reviewDecision.approved) {
state_->queue.updateItem(item.id, item);
state_->queue.complete(item.id);
events.push_back(makeEvent("auto-approved", item.id,
json{{"rule", reviewDecision.ruleMatched}}));
events.push_back(makeEvent("completed", item.id));
} else {
transitionWorkItem(item, WI_REVIEW);
state_->queue.updateItem(item.id, item);
events.push_back(makeEvent("sent-to-review", item.id,
json{{"reason", reviewDecision.reasoning}}));
}
return events;
}
};

View File

@@ -0,0 +1,249 @@
// Step 378: WorkflowOrchestrator main loop (12 tests)
#include <cassert>
#include <iostream>
#include <string>
#include "WorkflowOrchestrator.h"
static WorkItem makeItem(const std::string& id,
const std::string& name,
const std::string& workerType,
bool reviewRequired = false) {
WorkItem w;
w.id = id;
w.nodeId = id + "_node";
w.nodeName = name;
w.nodeType = "Function";
w.bufferId = "buf";
w.contextWidth = "local";
w.workerType = workerType;
w.reviewRequired = reviewRequired;
w.priority = "medium";
w.status = WI_PENDING;
w.createdAt = workItemTimestamp();
return w;
}
int main() {
int passed = 0;
// Test 1: single deterministic/template task completes in one step
{
WorkflowState ws("p1");
ws.queue.enqueue(makeItem("wi1", "getName", "template"));
RoutingEngine routing;
auto workers = WorkerRegistry::getDefaultRegistry();
ContextAssembler assembler;
ReviewGate review;
WorkflowOrchestrator orch(ws, routing, workers, assembler, review);
OrchestratorEvent ev = orch.step();
assert(ev.type == "completed");
assert(ws.queue.getByStatus(WI_COMPLETE).size() == 1);
std::cout << "Test 1 PASSED: single template task completes in one step\n";
passed++;
}
// Test 2: dependency cascade after completion
{
WorkflowState ws("p2");
WorkItem a = makeItem("a", "getA", "template");
WorkItem b = makeItem("b", "getB", "template");
b.dependencies.push_back("a");
ws.queue.enqueue(a);
ws.queue.enqueue(b);
RoutingEngine routing;
auto workers = WorkerRegistry::getDefaultRegistry();
ContextAssembler assembler;
ReviewGate review;
WorkflowOrchestrator orch(ws, routing, workers, assembler, review);
orch.step();
assert(ws.queue.getByStatus(WI_COMPLETE).size() == 1);
assert(ws.queue.getByStatus(WI_READY).size() == 1); // b promoted
std::cout << "Test 2 PASSED: dependency cascade promotion\n";
passed++;
}
// Test 3: human task blocks as needs-human
{
WorkflowState ws("p3");
ws.queue.enqueue(makeItem("wi3", "hardTask", "human"));
RoutingEngine routing;
auto workers = WorkerRegistry::getDefaultRegistry();
ContextAssembler assembler;
ReviewGate review;
WorkflowOrchestrator orch(ws, routing, workers, assembler, review);
orch.step();
auto blockers = orch.getBlockers();
bool hasHuman = false;
for (const auto& b : blockers) if (b.type == "needs-human") hasHuman = true;
assert(hasHuman);
std::cout << "Test 3 PASSED: human task blocks\n";
passed++;
}
// Test 4: SLM task blocks as needs-external-model
{
WorkflowState ws("p4");
ws.queue.enqueue(makeItem("wi4", "fileTask", "slm"));
RoutingEngine routing;
auto workers = WorkerRegistry::getDefaultRegistry();
ContextAssembler assembler;
ReviewGate review;
WorkflowOrchestrator orch(ws, routing, workers, assembler, review);
orch.step();
auto blockers = orch.getBlockers();
bool hasExternal = false;
for (const auto& b : blockers) if (b.type == "needs-external-model") hasExternal = true;
assert(hasExternal);
std::cout << "Test 4 PASSED: agent task blocks for external model\n";
passed++;
}
// Test 5: runToCompletion processes deterministic tasks
{
WorkflowState ws("p5");
ws.queue.enqueue(makeItem("w1", "getOne", "template"));
ws.queue.enqueue(makeItem("w2", "getTwo", "template"));
RoutingEngine routing;
auto workers = WorkerRegistry::getDefaultRegistry();
ContextAssembler assembler;
ReviewGate review;
WorkflowOrchestrator orch(ws, routing, workers, assembler, review);
auto stats = orch.runToCompletion();
assert(stats.complete == 2);
std::cout << "Test 5 PASSED: runToCompletion completes deterministic tasks\n";
passed++;
}
// Test 6: advance emits stage events
{
WorkflowState ws("p6");
ws.queue.enqueue(makeItem("wi6", "getX", "template"));
RoutingEngine routing;
auto workers = WorkerRegistry::getDefaultRegistry();
ContextAssembler assembler;
ReviewGate review;
WorkflowOrchestrator orch(ws, routing, workers, assembler, review);
auto events = orch.advance();
bool routed = false, context = false, executed = false, completed = false;
for (const auto& e : events) {
if (e.type == "routed") routed = true;
if (e.type == "context-assembled") context = true;
if (e.type == "executed") executed = true;
if (e.type == "completed") completed = true;
}
assert(routed && context && executed && completed);
std::cout << "Test 6 PASSED: stage events emitted\n";
passed++;
}
// Test 7: blockers report review items
{
WorkflowState ws("p7");
ws.queue.enqueue(makeItem("wi7", "getY", "template", true)); // force review
RoutingEngine routing;
auto workers = WorkerRegistry::getDefaultRegistry();
ContextAssembler assembler;
ReviewGate review;
WorkflowOrchestrator orch(ws, routing, workers, assembler, review);
orch.step();
auto blockers = orch.getBlockers();
bool hasReview = false;
for (const auto& b : blockers) if (b.type == "needs-review") hasReview = true;
assert(hasReview);
std::cout << "Test 7 PASSED: review blockers reported\n";
passed++;
}
// Test 8: empty workflow returns immediately
{
WorkflowState ws("p8");
RoutingEngine routing;
auto workers = WorkerRegistry::getDefaultRegistry();
ContextAssembler assembler;
ReviewGate review;
WorkflowOrchestrator orch(ws, routing, workers, assembler, review);
auto stats = orch.runToCompletion();
assert(stats.total == 0);
auto ev = orch.step();
assert(ev.type == "blocked");
std::cout << "Test 8 PASSED: empty workflow no-op\n";
passed++;
}
// Test 9: mixed workflow advances what it can
{
WorkflowState ws("p9");
ws.queue.enqueue(makeItem("d1", "getFast", "template"));
ws.queue.enqueue(makeItem("h1", "manual", "human"));
RoutingEngine routing;
auto workers = WorkerRegistry::getDefaultRegistry();
ContextAssembler assembler;
ReviewGate review;
WorkflowOrchestrator orch(ws, routing, workers, assembler, review);
auto stats = orch.runToCompletion();
assert(stats.complete == 1);
assert(!orch.getBlockers().empty());
std::cout << "Test 9 PASSED: mixed workflow partially advances\n";
passed++;
}
// Test 10: runUntil predicate can stop early
{
WorkflowState ws("p10");
ws.queue.enqueue(makeItem("a1", "getA", "template"));
ws.queue.enqueue(makeItem("a2", "getB", "template"));
RoutingEngine routing;
auto workers = WorkerRegistry::getDefaultRegistry();
ContextAssembler assembler;
ReviewGate review;
WorkflowOrchestrator orch(ws, routing, workers, assembler, review);
auto stats = orch.runUntil([](const WorkflowState& s) {
return s.getStats().complete >= 1;
});
assert(stats.complete >= 1);
std::cout << "Test 10 PASSED: runUntil predicate stop\n";
passed++;
}
// Test 11: blocked event includes blocker detail when applicable
{
WorkflowState ws("p11");
ws.queue.enqueue(makeItem("h2", "manual2", "human"));
RoutingEngine routing;
auto workers = WorkerRegistry::getDefaultRegistry();
ContextAssembler assembler;
ReviewGate review;
WorkflowOrchestrator orch(ws, routing, workers, assembler, review);
orch.step(); // process human task first
OrchestratorEvent ev = orch.step(); // now no ready, returns blocked
assert(ev.type == "blocked");
assert(ev.detail.contains("blockers"));
std::cout << "Test 11 PASSED: blocked event includes blocker detail\n";
passed++;
}
// Test 12: agent task stores context payload in result
{
WorkflowState ws("p12");
ws.queue.enqueue(makeItem("ll1", "llTask", "llm"));
RoutingEngine routing;
auto workers = WorkerRegistry::getDefaultRegistry();
ContextAssembler assembler;
ReviewGate review;
WorkflowOrchestrator orch(ws, routing, workers, assembler, review);
orch.step();
auto item = ws.queue.getItem("ll1");
assert(item.has_value());
assert(item->result.astJson.is_object());
assert(item->result.astJson.contains("workerType"));
std::cout << "Test 12 PASSED: agent context payload preserved\n";
passed++;
}
std::cout << "\nResults: " << passed << "/12\n";
assert(passed == 12);
return 0;
}

View File

@@ -2744,6 +2744,51 @@ Scheme.
- `editor/tests/step377_test.cpp` remains within project file-size guidance
for test artifacts (`188` lines)
### Step 378: Orchestrator Core Main Loop
**Status:** PASS (12/12 tests)
Implemented Sprint 15 orchestration-loop core as a dedicated workflow engine
that advances ready work items through routing, context assembly, execution,
review, completion, and blocker reporting.
**Files created:**
- `editor/src/WorkflowOrchestrator.h` — orchestration core:
- `step()` (advance one ready item one stage-set)
- `advance()` (advance all currently ready items)
- `runToCompletion()` (progress until no further non-blocked progress)
- `runUntil(predicate)` (predicate-controlled progression)
- `getBlockers()` with typed blockers:
`needs-human`, `needs-review`, `needs-external-model`
- event stream via `OrchestratorEvent` (`routed`, `context-assembled`,
`executed`, `auto-approved`, `sent-to-review`, `completed`, `blocked`)
- `editor/tests/step378_test.cpp` — 12 tests covering:
1. single deterministic/template completion in one step
2. dependency cascade promotion
3. human-task blocker classification
4. agent-task external-model blocker classification
5. `runToCompletion()` deterministic completion behavior
6. stage event emission coverage
7. review blocker reporting
8. empty-workflow no-op behavior
9. mixed workflow partial progress behavior
10. `runUntil(...)` predicate stop behavior
11. blocked-event detail payload includes blockers
12. agent context payload preservation
**Files modified:**
- `editor/CMakeLists.txt``step378_test` target
**Verification run:**
- `step378_test` — PASS (12/12) new step coverage
- `step377_test` — PASS (8/8) regression coverage
- `step376_test` — PASS (12/12) regression coverage
- `step375_test` — PASS (12/12) regression coverage
- `step331_test` — PASS (8/8) orchestration-foundation regression
**Architecture gate check:**
- `editor/src/WorkflowOrchestrator.h` is within header size limit
(`231` lines <= `600`)
# Roadmap Planning — Sprints 12-25+
## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)